summaryrefslogtreecommitdiffstats
path: root/dom/media
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
commit0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch)
treea31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /dom/media
parentInitial commit. (diff)
downloadfirefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz
firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/media')
-rw-r--r--dom/media/ADTSDecoder.cpp47
-rw-r--r--dom/media/ADTSDecoder.h30
-rw-r--r--dom/media/ADTSDemuxer.cpp795
-rw-r--r--dom/media/ADTSDemuxer.h152
-rw-r--r--dom/media/AsyncLogger.h305
-rw-r--r--dom/media/AudibilityMonitor.h100
-rw-r--r--dom/media/AudioBufferUtils.h208
-rw-r--r--dom/media/AudioCaptureTrack.cpp139
-rw-r--r--dom/media/AudioCaptureTrack.h43
-rw-r--r--dom/media/AudioChannelFormat.cpp16
-rw-r--r--dom/media/AudioChannelFormat.h253
-rw-r--r--dom/media/AudioCompactor.cpp65
-rw-r--r--dom/media/AudioCompactor.h128
-rw-r--r--dom/media/AudioConfig.cpp343
-rw-r--r--dom/media/AudioConfig.h278
-rw-r--r--dom/media/AudioConverter.cpp480
-rw-r--r--dom/media/AudioConverter.h277
-rw-r--r--dom/media/AudioDeviceInfo.cpp165
-rw-r--r--dom/media/AudioDeviceInfo.h58
-rw-r--r--dom/media/AudioDriftCorrection.h209
-rw-r--r--dom/media/AudioInputSource.cpp222
-rw-r--r--dom/media/AudioInputSource.h127
-rw-r--r--dom/media/AudioMixer.h145
-rw-r--r--dom/media/AudioPacketizer.h174
-rw-r--r--dom/media/AudioRingBuffer.cpp471
-rw-r--r--dom/media/AudioRingBuffer.h116
-rw-r--r--dom/media/AudioSampleFormat.h228
-rw-r--r--dom/media/AudioSegment.cpp262
-rw-r--r--dom/media/AudioSegment.h539
-rw-r--r--dom/media/AudioStream.cpp752
-rw-r--r--dom/media/AudioStream.h394
-rw-r--r--dom/media/AudioStreamTrack.cpp87
-rw-r--r--dom/media/AudioStreamTrack.h56
-rw-r--r--dom/media/AudioTrack.cpp70
-rw-r--r--dom/media/AudioTrack.h52
-rw-r--r--dom/media/AudioTrackList.cpp32
-rw-r--r--dom/media/AudioTrackList.h38
-rw-r--r--dom/media/BackgroundVideoDecodingPermissionObserver.cpp154
-rw-r--r--dom/media/BackgroundVideoDecodingPermissionObserver.h51
-rw-r--r--dom/media/BaseMediaResource.cpp171
-rw-r--r--dom/media/BaseMediaResource.h151
-rw-r--r--dom/media/Benchmark.cpp395
-rw-r--r--dom/media/Benchmark.h119
-rw-r--r--dom/media/BitReader.cpp197
-rw-r--r--dom/media/BitReader.h54
-rw-r--r--dom/media/BitWriter.cpp96
-rw-r--r--dom/media/BitWriter.h43
-rw-r--r--dom/media/BufferMediaResource.h76
-rw-r--r--dom/media/BufferReader.h320
-rw-r--r--dom/media/ByteWriter.h61
-rw-r--r--dom/media/CallbackThreadRegistry.cpp101
-rw-r--r--dom/media/CallbackThreadRegistry.h60
-rw-r--r--dom/media/CanvasCaptureMediaStream.cpp214
-rw-r--r--dom/media/CanvasCaptureMediaStream.h132
-rw-r--r--dom/media/ChannelMediaDecoder.cpp567
-rw-r--r--dom/media/ChannelMediaDecoder.h169
-rw-r--r--dom/media/ChannelMediaResource.cpp1053
-rw-r--r--dom/media/ChannelMediaResource.h273
-rw-r--r--dom/media/CloneableWithRangeMediaResource.cpp228
-rw-r--r--dom/media/CloneableWithRangeMediaResource.h101
-rw-r--r--dom/media/CrossGraphPort.cpp205
-rw-r--r--dom/media/CrossGraphPort.h105
-rw-r--r--dom/media/CubebInputStream.cpp172
-rw-r--r--dom/media/CubebInputStream.h81
-rw-r--r--dom/media/CubebUtils.cpp851
-rw-r--r--dom/media/CubebUtils.h102
-rw-r--r--dom/media/DOMMediaStream.cpp537
-rw-r--r--dom/media/DOMMediaStream.h252
-rw-r--r--dom/media/DecoderTraits.cpp345
-rw-r--r--dom/media/DecoderTraits.h73
-rw-r--r--dom/media/DeviceInputTrack.cpp694
-rw-r--r--dom/media/DeviceInputTrack.h302
-rw-r--r--dom/media/DriftCompensation.h137
-rw-r--r--dom/media/DynamicResampler.cpp477
-rw-r--r--dom/media/DynamicResampler.h409
-rw-r--r--dom/media/ExternalEngineStateMachine.cpp1196
-rw-r--r--dom/media/ExternalEngineStateMachine.h328
-rw-r--r--dom/media/FileBlockCache.cpp506
-rw-r--r--dom/media/FileBlockCache.h193
-rw-r--r--dom/media/FileMediaResource.cpp223
-rw-r--r--dom/media/FileMediaResource.h136
-rw-r--r--dom/media/ForwardedInputTrack.cpp291
-rw-r--r--dom/media/ForwardedInputTrack.h68
-rw-r--r--dom/media/FrameStatistics.h196
-rw-r--r--dom/media/GetUserMediaRequest.cpp127
-rw-r--r--dom/media/GetUserMediaRequest.h93
-rw-r--r--dom/media/GraphDriver.cpp1296
-rw-r--r--dom/media/GraphDriver.h821
-rw-r--r--dom/media/GraphRunner.cpp176
-rw-r--r--dom/media/GraphRunner.h121
-rw-r--r--dom/media/IdpSandbox.sys.mjs284
-rw-r--r--dom/media/ImageToI420.cpp148
-rw-r--r--dom/media/ImageToI420.h26
-rw-r--r--dom/media/Intervals.h762
-rw-r--r--dom/media/MPSCQueue.h132
-rw-r--r--dom/media/MediaBlockCacheBase.h81
-rw-r--r--dom/media/MediaCache.cpp2816
-rw-r--r--dom/media/MediaCache.h557
-rw-r--r--dom/media/MediaChannelStatistics.h89
-rw-r--r--dom/media/MediaContainerType.cpp35
-rw-r--r--dom/media/MediaContainerType.h51
-rw-r--r--dom/media/MediaData.cpp599
-rw-r--r--dom/media/MediaData.h712
-rw-r--r--dom/media/MediaDataDemuxer.h213
-rw-r--r--dom/media/MediaDecoder.cpp1681
-rw-r--r--dom/media/MediaDecoder.h836
-rw-r--r--dom/media/MediaDecoderOwner.h200
-rw-r--r--dom/media/MediaDecoderStateMachine.cpp4824
-rw-r--r--dom/media/MediaDecoderStateMachine.h586
-rw-r--r--dom/media/MediaDecoderStateMachineBase.cpp184
-rw-r--r--dom/media/MediaDecoderStateMachineBase.h299
-rw-r--r--dom/media/MediaDeviceInfo.cpp42
-rw-r--r--dom/media/MediaDeviceInfo.h59
-rw-r--r--dom/media/MediaDevices.cpp804
-rw-r--r--dom/media/MediaDevices.h140
-rw-r--r--dom/media/MediaEventSource.h594
-rw-r--r--dom/media/MediaFormatReader.cpp3432
-rw-r--r--dom/media/MediaFormatReader.h885
-rw-r--r--dom/media/MediaInfo.cpp58
-rw-r--r--dom/media/MediaInfo.h673
-rw-r--r--dom/media/MediaMIMETypes.cpp267
-rw-r--r--dom/media/MediaMIMETypes.h222
-rw-r--r--dom/media/MediaManager.cpp4404
-rw-r--r--dom/media/MediaManager.h422
-rw-r--r--dom/media/MediaMetadataManager.h97
-rw-r--r--dom/media/MediaPlaybackDelayPolicy.cpp166
-rw-r--r--dom/media/MediaPlaybackDelayPolicy.h84
-rw-r--r--dom/media/MediaPromiseDefs.h19
-rw-r--r--dom/media/MediaQueue.h277
-rw-r--r--dom/media/MediaRecorder.cpp1894
-rw-r--r--dom/media/MediaRecorder.h187
-rw-r--r--dom/media/MediaResource.cpp425
-rw-r--r--dom/media/MediaResource.h283
-rw-r--r--dom/media/MediaResourceCallback.h66
-rw-r--r--dom/media/MediaResult.h82
-rw-r--r--dom/media/MediaSegment.h501
-rw-r--r--dom/media/MediaShutdownManager.cpp202
-rw-r--r--dom/media/MediaShutdownManager.h96
-rw-r--r--dom/media/MediaSpan.h131
-rw-r--r--dom/media/MediaStatistics.h78
-rw-r--r--dom/media/MediaStreamError.cpp112
-rw-r--r--dom/media/MediaStreamError.h113
-rw-r--r--dom/media/MediaStreamTrack.cpp638
-rw-r--r--dom/media/MediaStreamTrack.h638
-rw-r--r--dom/media/MediaStreamWindowCapturer.cpp77
-rw-r--r--dom/media/MediaStreamWindowCapturer.h51
-rw-r--r--dom/media/MediaTimer.cpp192
-rw-r--r--dom/media/MediaTimer.h161
-rw-r--r--dom/media/MediaTrack.cpp34
-rw-r--r--dom/media/MediaTrack.h79
-rw-r--r--dom/media/MediaTrackGraph.cpp4129
-rw-r--r--dom/media/MediaTrackGraph.h1185
-rw-r--r--dom/media/MediaTrackGraphImpl.h1066
-rw-r--r--dom/media/MediaTrackList.cpp142
-rw-r--r--dom/media/MediaTrackList.h111
-rw-r--r--dom/media/MediaTrackListener.cpp95
-rw-r--r--dom/media/MediaTrackListener.h184
-rw-r--r--dom/media/MemoryBlockCache.cpp213
-rw-r--r--dom/media/MemoryBlockCache.h87
-rw-r--r--dom/media/Pacer.h164
-rw-r--r--dom/media/PeerConnection.sys.mjs2022
-rw-r--r--dom/media/PeerConnectionIdp.sys.mjs378
-rw-r--r--dom/media/PrincipalChangeObserver.h27
-rw-r--r--dom/media/PrincipalHandle.h62
-rw-r--r--dom/media/QueueObject.cpp29
-rw-r--r--dom/media/QueueObject.h32
-rw-r--r--dom/media/ReaderProxy.cpp216
-rw-r--r--dom/media/ReaderProxy.h120
-rw-r--r--dom/media/SeekJob.cpp31
-rw-r--r--dom/media/SeekJob.h32
-rw-r--r--dom/media/SeekTarget.h90
-rw-r--r--dom/media/SelfRef.h46
-rw-r--r--dom/media/SharedBuffer.h116
-rw-r--r--dom/media/TimeUnits.cpp430
-rw-r--r--dom/media/TimeUnits.h342
-rw-r--r--dom/media/Tracing.cpp86
-rw-r--r--dom/media/Tracing.h102
-rw-r--r--dom/media/UnderrunHandler.h22
-rw-r--r--dom/media/UnderrunHandlerLinux.cpp78
-rw-r--r--dom/media/UnderrunHandlerNoop.cpp14
-rw-r--r--dom/media/VideoFrameContainer.cpp257
-rw-r--r--dom/media/VideoFrameContainer.h149
-rw-r--r--dom/media/VideoFrameConverter.h439
-rw-r--r--dom/media/VideoLimits.h21
-rw-r--r--dom/media/VideoOutput.h307
-rw-r--r--dom/media/VideoPlaybackQuality.cpp36
-rw-r--r--dom/media/VideoPlaybackQuality.h48
-rw-r--r--dom/media/VideoSegment.cpp108
-rw-r--r--dom/media/VideoSegment.h187
-rw-r--r--dom/media/VideoStreamTrack.cpp91
-rw-r--r--dom/media/VideoStreamTrack.h58
-rw-r--r--dom/media/VideoTrack.cpp92
-rw-r--r--dom/media/VideoTrack.h61
-rw-r--r--dom/media/VideoTrackList.cpp80
-rw-r--r--dom/media/VideoTrackList.h51
-rw-r--r--dom/media/VideoUtils.cpp1046
-rw-r--r--dom/media/VideoUtils.h579
-rw-r--r--dom/media/VorbisUtils.h27
-rw-r--r--dom/media/WavDumper.h136
-rw-r--r--dom/media/WebMSample.h22788
-rw-r--r--dom/media/XiphExtradata.cpp77
-rw-r--r--dom/media/XiphExtradata.h27
-rw-r--r--dom/media/autoplay/AutoplayPolicy.cpp497
-rw-r--r--dom/media/autoplay/AutoplayPolicy.h80
-rw-r--r--dom/media/autoplay/GVAutoplayPermissionRequest.cpp236
-rw-r--r--dom/media/autoplay/GVAutoplayPermissionRequest.h86
-rw-r--r--dom/media/autoplay/GVAutoplayRequestStatusIPC.h23
-rw-r--r--dom/media/autoplay/GVAutoplayRequestUtils.h25
-rw-r--r--dom/media/autoplay/moz.build32
-rw-r--r--dom/media/autoplay/nsIAutoplay.idl17
-rw-r--r--dom/media/autoplay/test/browser/audio.oggbin0 -> 14290 bytes
-rw-r--r--dom/media/autoplay/test/browser/browser.ini27
-rw-r--r--dom/media/autoplay/test/browser/browser_autoplay_policy_detection_click_to_play.js120
-rw-r--r--dom/media/autoplay/test/browser/browser_autoplay_policy_detection_global_and_site_sticky.js168
-rw-r--r--dom/media/autoplay/test/browser/browser_autoplay_policy_detection_global_sticky.js105
-rw-r--r--dom/media/autoplay/test/browser/browser_autoplay_policy_play_twice.js54
-rw-r--r--dom/media/autoplay/test/browser/browser_autoplay_policy_request_permission.js269
-rw-r--r--dom/media/autoplay/test/browser/browser_autoplay_policy_touchScroll.js103
-rw-r--r--dom/media/autoplay/test/browser/browser_autoplay_policy_user_gestures.js277
-rw-r--r--dom/media/autoplay/test/browser/browser_autoplay_policy_webRTC_permission.js67
-rw-r--r--dom/media/autoplay/test/browser/browser_autoplay_policy_web_audio.js217
-rw-r--r--dom/media/autoplay/test/browser/browser_autoplay_policy_web_audio_with_gum.js174
-rw-r--r--dom/media/autoplay/test/browser/browser_autoplay_videoDocument.js80
-rw-r--r--dom/media/autoplay/test/browser/file_empty.html8
-rw-r--r--dom/media/autoplay/test/browser/file_mediaplayback_frame.html21
-rw-r--r--dom/media/autoplay/test/browser/file_nonAutoplayAudio.html7
-rw-r--r--dom/media/autoplay/test/browser/file_video.html9
-rw-r--r--dom/media/autoplay/test/browser/head.js149
-rw-r--r--dom/media/autoplay/test/mochitest/AutoplayTestUtils.js46
-rw-r--r--dom/media/autoplay/test/mochitest/file_autoplay_gv_play_request_frame.html24
-rw-r--r--dom/media/autoplay/test/mochitest/file_autoplay_gv_play_request_window.html65
-rw-r--r--dom/media/autoplay/test/mochitest/file_autoplay_policy_activation_frame.html32
-rw-r--r--dom/media/autoplay/test/mochitest/file_autoplay_policy_activation_window.html80
-rw-r--r--dom/media/autoplay/test/mochitest/file_autoplay_policy_eventdown_activation.html85
-rw-r--r--dom/media/autoplay/test/mochitest/file_autoplay_policy_key_blacklist.html148
-rw-r--r--dom/media/autoplay/test/mochitest/file_autoplay_policy_play_before_loadedmetadata.html63
-rw-r--r--dom/media/autoplay/test/mochitest/file_autoplay_policy_unmute_pauses.html65
-rw-r--r--dom/media/autoplay/test/mochitest/mochitest.ini54
-rw-r--r--dom/media/autoplay/test/mochitest/test_autoplay.html36
-rw-r--r--dom/media/autoplay/test/mochitest/test_autoplay_contentEditable.html67
-rw-r--r--dom/media/autoplay/test/mochitest/test_autoplay_gv_play_request.html221
-rw-r--r--dom/media/autoplay/test/mochitest/test_autoplay_policy.html174
-rw-r--r--dom/media/autoplay/test/mochitest/test_autoplay_policy_activation.html180
-rw-r--r--dom/media/autoplay/test/mochitest/test_autoplay_policy_eventdown_activation.html55
-rw-r--r--dom/media/autoplay/test/mochitest/test_autoplay_policy_key_blacklist.html47
-rw-r--r--dom/media/autoplay/test/mochitest/test_autoplay_policy_permission.html80
-rw-r--r--dom/media/autoplay/test/mochitest/test_autoplay_policy_play_before_loadedmetadata.html73
-rw-r--r--dom/media/autoplay/test/mochitest/test_autoplay_policy_unmute_pauses.html64
-rw-r--r--dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_AudioParamStream.html171
-rw-r--r--dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_createMediaStreamSource.html119
-rw-r--r--dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_mediaElementAudioSourceNode.html105
-rw-r--r--dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_notResumePageInvokedSuspendedAudioContext.html96
-rw-r--r--dom/media/autoplay/test/mochitest/test_streams_autoplay.html47
-rwxr-xr-xdom/media/benchmark/sample7
-rw-r--r--dom/media/bridge/IPeerConnection.idl58
-rw-r--r--dom/media/bridge/MediaModule.cpp16
-rw-r--r--dom/media/bridge/components.conf25
-rw-r--r--dom/media/bridge/moz.build36
-rw-r--r--dom/media/components.conf74
-rw-r--r--dom/media/doctor/DDLifetime.cpp33
-rw-r--r--dom/media/doctor/DDLifetime.h72
-rw-r--r--dom/media/doctor/DDLifetimes.cpp84
-rw-r--r--dom/media/doctor/DDLifetimes.h130
-rw-r--r--dom/media/doctor/DDLogCategory.cpp30
-rw-r--r--dom/media/doctor/DDLogCategory.h41
-rw-r--r--dom/media/doctor/DDLogMessage.cpp42
-rw-r--r--dom/media/doctor/DDLogMessage.h48
-rw-r--r--dom/media/doctor/DDLogObject.cpp22
-rw-r--r--dom/media/doctor/DDLogObject.h62
-rw-r--r--dom/media/doctor/DDLogUtils.cpp11
-rw-r--r--dom/media/doctor/DDLogUtils.h33
-rw-r--r--dom/media/doctor/DDLogValue.cpp120
-rw-r--r--dom/media/doctor/DDLogValue.h43
-rw-r--r--dom/media/doctor/DDLoggedTypeTraits.h104
-rw-r--r--dom/media/doctor/DDMediaLog.cpp27
-rw-r--r--dom/media/doctor/DDMediaLog.h42
-rw-r--r--dom/media/doctor/DDMediaLogs.cpp667
-rw-r--r--dom/media/doctor/DDMediaLogs.h193
-rw-r--r--dom/media/doctor/DDMessageIndex.h26
-rw-r--r--dom/media/doctor/DDTimeStamp.cpp20
-rw-r--r--dom/media/doctor/DDTimeStamp.h24
-rw-r--r--dom/media/doctor/DecoderDoctorDiagnostics.cpp1319
-rw-r--r--dom/media/doctor/DecoderDoctorDiagnostics.h167
-rw-r--r--dom/media/doctor/DecoderDoctorLogger.cpp176
-rw-r--r--dom/media/doctor/DecoderDoctorLogger.h472
-rw-r--r--dom/media/doctor/MultiWriterQueue.h523
-rw-r--r--dom/media/doctor/RollingNumber.h163
-rw-r--r--dom/media/doctor/moz.build40
-rw-r--r--dom/media/doctor/test/browser/browser.ini7
-rw-r--r--dom/media/doctor/test/browser/browser_decoderDoctor.js356
-rw-r--r--dom/media/doctor/test/browser/browser_doctor_notification.js265
-rw-r--r--dom/media/doctor/test/gtest/TestMultiWriterQueue.cpp382
-rw-r--r--dom/media/doctor/test/gtest/TestRollingNumber.cpp146
-rw-r--r--dom/media/doctor/test/gtest/moz.build19
-rw-r--r--dom/media/eme/CDMCaps.cpp112
-rw-r--r--dom/media/eme/CDMCaps.h82
-rw-r--r--dom/media/eme/CDMProxy.h323
-rw-r--r--dom/media/eme/DecryptorProxyCallback.h54
-rw-r--r--dom/media/eme/DetailedPromise.cpp87
-rw-r--r--dom/media/eme/DetailedPromise.h104
-rw-r--r--dom/media/eme/EMEUtils.cpp119
-rw-r--r--dom/media/eme/EMEUtils.h106
-rw-r--r--dom/media/eme/KeySystemConfig.cpp213
-rw-r--r--dom/media/eme/KeySystemConfig.h157
-rw-r--r--dom/media/eme/KeySystemNames.h33
-rw-r--r--dom/media/eme/MediaEncryptedEvent.cpp108
-rw-r--r--dom/media/eme/MediaEncryptedEvent.h60
-rw-r--r--dom/media/eme/MediaKeyError.cpp27
-rw-r--r--dom/media/eme/MediaKeyError.h33
-rw-r--r--dom/media/eme/MediaKeyMessageEvent.cpp103
-rw-r--r--dom/media/eme/MediaKeyMessageEvent.h64
-rw-r--r--dom/media/eme/MediaKeySession.cpp622
-rw-r--r--dom/media/eme/MediaKeySession.h142
-rw-r--r--dom/media/eme/MediaKeyStatusMap.cpp99
-rw-r--r--dom/media/eme/MediaKeyStatusMap.h92
-rw-r--r--dom/media/eme/MediaKeySystemAccess.cpp1086
-rw-r--r--dom/media/eme/MediaKeySystemAccess.h81
-rw-r--r--dom/media/eme/MediaKeySystemAccessManager.cpp684
-rw-r--r--dom/media/eme/MediaKeySystemAccessManager.h229
-rw-r--r--dom/media/eme/MediaKeySystemAccessPermissionRequest.cpp91
-rw-r--r--dom/media/eme/MediaKeySystemAccessPermissionRequest.h74
-rw-r--r--dom/media/eme/MediaKeys.cpp844
-rw-r--r--dom/media/eme/MediaKeys.h236
-rw-r--r--dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.cpp115
-rw-r--r--dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.h63
-rw-r--r--dom/media/eme/mediadrm/MediaDrmCDMProxy.cpp453
-rw-r--r--dom/media/eme/mediadrm/MediaDrmCDMProxy.h186
-rw-r--r--dom/media/eme/mediadrm/MediaDrmProxySupport.cpp272
-rw-r--r--dom/media/eme/mediadrm/MediaDrmProxySupport.h68
-rw-r--r--dom/media/eme/mediadrm/moz.build19
-rw-r--r--dom/media/eme/mediafoundation/WMFCDMImpl.cpp128
-rw-r--r--dom/media/eme/mediafoundation/WMFCDMImpl.h100
-rw-r--r--dom/media/eme/mediafoundation/WMFCDMProxy.cpp306
-rw-r--r--dom/media/eme/mediafoundation/WMFCDMProxy.h134
-rw-r--r--dom/media/eme/mediafoundation/WMFCDMProxyCallback.cpp72
-rw-r--r--dom/media/eme/mediafoundation/WMFCDMProxyCallback.h38
-rw-r--r--dom/media/eme/mediafoundation/moz.build21
-rw-r--r--dom/media/eme/moz.build54
-rw-r--r--dom/media/encoder/ContainerWriter.h75
-rw-r--r--dom/media/encoder/EncodedFrame.h64
-rw-r--r--dom/media/encoder/MediaEncoder.cpp1142
-rw-r--r--dom/media/encoder/MediaEncoder.h400
-rw-r--r--dom/media/encoder/Muxer.cpp185
-rw-r--r--dom/media/encoder/Muxer.h71
-rw-r--r--dom/media/encoder/OpusTrackEncoder.cpp454
-rw-r--r--dom/media/encoder/OpusTrackEncoder.h117
-rw-r--r--dom/media/encoder/TrackEncoder.cpp822
-rw-r--r--dom/media/encoder/TrackEncoder.h501
-rw-r--r--dom/media/encoder/TrackMetadataBase.h76
-rw-r--r--dom/media/encoder/VP8TrackEncoder.cpp720
-rw-r--r--dom/media/encoder/VP8TrackEncoder.h167
-rw-r--r--dom/media/encoder/moz.build42
-rw-r--r--dom/media/fake-cdm/cdm-fake.cpp63
-rw-r--r--dom/media/fake-cdm/cdm-test-decryptor.cpp433
-rw-r--r--dom/media/fake-cdm/cdm-test-decryptor.h106
-rw-r--r--dom/media/fake-cdm/cdm-test-output-protection.h126
-rw-r--r--dom/media/fake-cdm/cdm-test-storage.cpp195
-rw-r--r--dom/media/fake-cdm/cdm-test-storage.h45
-rw-r--r--dom/media/fake-cdm/manifest.json9
-rw-r--r--dom/media/fake-cdm/moz.build33
-rw-r--r--dom/media/flac/FlacDecoder.cpp45
-rw-r--r--dom/media/flac/FlacDecoder.h30
-rw-r--r--dom/media/flac/FlacDemuxer.cpp1027
-rw-r--r--dom/media/flac/FlacDemuxer.h112
-rw-r--r--dom/media/flac/FlacFrameParser.cpp244
-rw-r--r--dom/media/flac/FlacFrameParser.h72
-rw-r--r--dom/media/flac/moz.build24
-rw-r--r--dom/media/fuzz/FuzzMedia.cpp66
-rw-r--r--dom/media/fuzz/moz.build29
-rw-r--r--dom/media/gmp-plugin-openh264/fakeopenh264.info4
-rw-r--r--dom/media/gmp-plugin-openh264/gmp-fake-openh264.cpp406
-rw-r--r--dom/media/gmp-plugin-openh264/moz.build25
-rw-r--r--dom/media/gmp/CDMStorageIdProvider.cpp68
-rw-r--r--dom/media/gmp/CDMStorageIdProvider.h41
-rw-r--r--dom/media/gmp/ChromiumCDMAdapter.cpp307
-rw-r--r--dom/media/gmp/ChromiumCDMAdapter.h81
-rw-r--r--dom/media/gmp/ChromiumCDMCallback.h56
-rw-r--r--dom/media/gmp/ChromiumCDMCallbackProxy.cpp159
-rw-r--r--dom/media/gmp/ChromiumCDMCallbackProxy.h62
-rw-r--r--dom/media/gmp/ChromiumCDMChild.cpp866
-rw-r--r--dom/media/gmp/ChromiumCDMChild.h148
-rw-r--r--dom/media/gmp/ChromiumCDMParent.cpp1319
-rw-r--r--dom/media/gmp/ChromiumCDMParent.h232
-rw-r--r--dom/media/gmp/ChromiumCDMProxy.cpp636
-rw-r--r--dom/media/gmp/ChromiumCDMProxy.h136
-rw-r--r--dom/media/gmp/DecryptJob.cpp46
-rw-r--r--dom/media/gmp/DecryptJob.h36
-rw-r--r--dom/media/gmp/GMPCallbackBase.h24
-rw-r--r--dom/media/gmp/GMPChild.cpp657
-rw-r--r--dom/media/gmp/GMPChild.h106
-rw-r--r--dom/media/gmp/GMPContentChild.cpp131
-rw-r--r--dom/media/gmp/GMPContentChild.h66
-rw-r--r--dom/media/gmp/GMPContentParent.cpp204
-rw-r--r--dom/media/gmp/GMPContentParent.h94
-rw-r--r--dom/media/gmp/GMPCrashHelper.h37
-rw-r--r--dom/media/gmp/GMPCrashHelperHolder.cpp31
-rw-r--r--dom/media/gmp/GMPCrashHelperHolder.h63
-rw-r--r--dom/media/gmp/GMPDiskStorage.cpp454
-rw-r--r--dom/media/gmp/GMPLoader.cpp203
-rw-r--r--dom/media/gmp/GMPLoader.h80
-rw-r--r--dom/media/gmp/GMPLog.h62
-rw-r--r--dom/media/gmp/GMPMemoryStorage.cpp75
-rw-r--r--dom/media/gmp/GMPMessageUtils.h190
-rw-r--r--dom/media/gmp/GMPNativeTypes.h17
-rw-r--r--dom/media/gmp/GMPParent.cpp1209
-rw-r--r--dom/media/gmp/GMPParent.h244
-rw-r--r--dom/media/gmp/GMPPlatform.cpp281
-rw-r--r--dom/media/gmp/GMPPlatform.h32
-rw-r--r--dom/media/gmp/GMPProcessChild.cpp45
-rw-r--r--dom/media/gmp/GMPProcessChild.h33
-rw-r--r--dom/media/gmp/GMPProcessParent.cpp297
-rw-r--r--dom/media/gmp/GMPProcessParent.h103
-rw-r--r--dom/media/gmp/GMPSanitizedExports.h22
-rw-r--r--dom/media/gmp/GMPService.cpp570
-rw-r--r--dom/media/gmp/GMPService.h130
-rw-r--r--dom/media/gmp/GMPServiceChild.cpp586
-rw-r--r--dom/media/gmp/GMPServiceChild.h164
-rw-r--r--dom/media/gmp/GMPServiceParent.cpp1943
-rw-r--r--dom/media/gmp/GMPServiceParent.h281
-rw-r--r--dom/media/gmp/GMPSharedMemManager.cpp89
-rw-r--r--dom/media/gmp/GMPSharedMemManager.h78
-rw-r--r--dom/media/gmp/GMPStorage.h39
-rw-r--r--dom/media/gmp/GMPStorageChild.cpp244
-rw-r--r--dom/media/gmp/GMPStorageChild.h94
-rw-r--r--dom/media/gmp/GMPStorageParent.cpp194
-rw-r--r--dom/media/gmp/GMPStorageParent.h47
-rw-r--r--dom/media/gmp/GMPTimerChild.cpp59
-rw-r--r--dom/media/gmp/GMPTimerChild.h45
-rw-r--r--dom/media/gmp/GMPTimerParent.cpp105
-rw-r--r--dom/media/gmp/GMPTimerParent.h56
-rw-r--r--dom/media/gmp/GMPTypes.ipdlh116
-rw-r--r--dom/media/gmp/GMPUtils.cpp228
-rw-r--r--dom/media/gmp/GMPUtils.h83
-rw-r--r--dom/media/gmp/GMPVideoDecoderChild.cpp210
-rw-r--r--dom/media/gmp/GMPVideoDecoderChild.h75
-rw-r--r--dom/media/gmp/GMPVideoDecoderParent.cpp461
-rw-r--r--dom/media/gmp/GMPVideoDecoderParent.h103
-rw-r--r--dom/media/gmp/GMPVideoDecoderProxy.h57
-rw-r--r--dom/media/gmp/GMPVideoEncodedFrameImpl.cpp226
-rw-r--r--dom/media/gmp/GMPVideoEncodedFrameImpl.h116
-rw-r--r--dom/media/gmp/GMPVideoEncoderChild.cpp203
-rw-r--r--dom/media/gmp/GMPVideoEncoderChild.h75
-rw-r--r--dom/media/gmp/GMPVideoEncoderParent.cpp307
-rw-r--r--dom/media/gmp/GMPVideoEncoderParent.h85
-rw-r--r--dom/media/gmp/GMPVideoEncoderProxy.h56
-rw-r--r--dom/media/gmp/GMPVideoHost.cpp94
-rw-r--r--dom/media/gmp/GMPVideoHost.h54
-rw-r--r--dom/media/gmp/GMPVideoPlaneImpl.cpp179
-rw-r--r--dom/media/gmp/GMPVideoPlaneImpl.h61
-rw-r--r--dom/media/gmp/GMPVideoi420FrameImpl.cpp328
-rw-r--r--dom/media/gmp/GMPVideoi420FrameImpl.h80
-rw-r--r--dom/media/gmp/PChromiumCDM.ipdl129
-rw-r--r--dom/media/gmp/PGMP.ipdl59
-rw-r--r--dom/media/gmp/PGMPContent.ipdl37
-rw-r--r--dom/media/gmp/PGMPService.ipdl38
-rw-r--r--dom/media/gmp/PGMPStorage.ipdl37
-rw-r--r--dom/media/gmp/PGMPTimer.ipdl26
-rw-r--r--dom/media/gmp/PGMPVideoDecoder.ipdl50
-rw-r--r--dom/media/gmp/PGMPVideoEncoder.ipdl48
-rw-r--r--dom/media/gmp/README.txt1
-rw-r--r--dom/media/gmp/gmp-api/gmp-entrypoints.h73
-rw-r--r--dom/media/gmp/gmp-api/gmp-errors.h59
-rw-r--r--dom/media/gmp/gmp-api/gmp-platform.h102
-rw-r--r--dom/media/gmp/gmp-api/gmp-storage.h110
-rw-r--r--dom/media/gmp/gmp-api/gmp-video-codec.h302
-rw-r--r--dom/media/gmp/gmp-api/gmp-video-decode.h125
-rw-r--r--dom/media/gmp/gmp-api/gmp-video-encode.h133
-rw-r--r--dom/media/gmp/gmp-api/gmp-video-frame-encoded.h92
-rw-r--r--dom/media/gmp/gmp-api/gmp-video-frame-i420.h142
-rw-r--r--dom/media/gmp/gmp-api/gmp-video-frame.h48
-rw-r--r--dom/media/gmp/gmp-api/gmp-video-host.h53
-rw-r--r--dom/media/gmp/gmp-api/gmp-video-plane.h94
-rw-r--r--dom/media/gmp/moz.build149
-rw-r--r--dom/media/gmp/mozIGeckoMediaPluginChromeService.idl58
-rw-r--r--dom/media/gmp/mozIGeckoMediaPluginService.idl122
-rw-r--r--dom/media/gmp/rlz/OWNERS4
-rw-r--r--dom/media/gmp/rlz/README.mozilla4
-rw-r--r--dom/media/gmp/rlz/lib/assert.h14
-rw-r--r--dom/media/gmp/rlz/lib/crc8.cc90
-rw-r--r--dom/media/gmp/rlz/lib/crc8.h24
-rw-r--r--dom/media/gmp/rlz/lib/machine_id.cc93
-rw-r--r--dom/media/gmp/rlz/lib/machine_id.h33
-rw-r--r--dom/media/gmp/rlz/lib/string_utils.cc34
-rw-r--r--dom/media/gmp/rlz/lib/string_utils.h20
-rw-r--r--dom/media/gmp/rlz/mac/lib/machine_id_mac.cc322
-rw-r--r--dom/media/gmp/rlz/moz.build34
-rw-r--r--dom/media/gmp/rlz/win/lib/machine_id_win.cc136
-rw-r--r--dom/media/gmp/widevine-adapter/WidevineFileIO.cpp100
-rw-r--r--dom/media/gmp/widevine-adapter/WidevineFileIO.h41
-rw-r--r--dom/media/gmp/widevine-adapter/WidevineUtils.cpp61
-rw-r--r--dom/media/gmp/widevine-adapter/WidevineUtils.h84
-rw-r--r--dom/media/gmp/widevine-adapter/WidevineVideoFrame.cpp142
-rw-r--r--dom/media/gmp/widevine-adapter/WidevineVideoFrame.h54
-rw-r--r--dom/media/gmp/widevine-adapter/content_decryption_module.h1359
-rw-r--r--dom/media/gmp/widevine-adapter/content_decryption_module_export.h38
-rw-r--r--dom/media/gmp/widevine-adapter/content_decryption_module_ext.h64
-rw-r--r--dom/media/gmp/widevine-adapter/content_decryption_module_proxy.h121
-rw-r--r--dom/media/gmp/widevine-adapter/moz.build21
-rw-r--r--dom/media/gtest/AudioGenerator.h64
-rw-r--r--dom/media/gtest/AudioVerifier.h135
-rw-r--r--dom/media/gtest/Cargo.toml8
-rw-r--r--dom/media/gtest/GMPTestMonitor.h42
-rw-r--r--dom/media/gtest/MockCubeb.cpp673
-rw-r--r--dom/media/gtest/MockCubeb.h527
-rw-r--r--dom/media/gtest/MockMediaResource.cpp91
-rw-r--r--dom/media/gtest/MockMediaResource.h56
-rw-r--r--dom/media/gtest/TestAudioBuffers.cpp59
-rw-r--r--dom/media/gtest/TestAudioCallbackDriver.cpp224
-rw-r--r--dom/media/gtest/TestAudioCompactor.cpp131
-rw-r--r--dom/media/gtest/TestAudioDecoderInputTrack.cpp447
-rw-r--r--dom/media/gtest/TestAudioDeviceEnumerator.cpp271
-rw-r--r--dom/media/gtest/TestAudioDriftCorrection.cpp436
-rw-r--r--dom/media/gtest/TestAudioInputProcessing.cpp386
-rw-r--r--dom/media/gtest/TestAudioInputSource.cpp275
-rw-r--r--dom/media/gtest/TestAudioMixer.cpp174
-rw-r--r--dom/media/gtest/TestAudioPacketizer.cpp163
-rw-r--r--dom/media/gtest/TestAudioRingBuffer.cpp993
-rw-r--r--dom/media/gtest/TestAudioSegment.cpp470
-rw-r--r--dom/media/gtest/TestAudioTrackEncoder.cpp298
-rw-r--r--dom/media/gtest/TestAudioTrackGraph.cpp2537
-rw-r--r--dom/media/gtest/TestBenchmarkStorage.cpp92
-rw-r--r--dom/media/gtest/TestBitWriter.cpp97
-rw-r--r--dom/media/gtest/TestBlankVideoDataCreator.cpp30
-rw-r--r--dom/media/gtest/TestBufferReader.cpp53
-rw-r--r--dom/media/gtest/TestCDMStorage.cpp1347
-rw-r--r--dom/media/gtest/TestCubebInputStream.cpp188
-rw-r--r--dom/media/gtest/TestDataMutex.cpp46
-rw-r--r--dom/media/gtest/TestDecoderBenchmark.cpp66
-rw-r--r--dom/media/gtest/TestDeviceInputTrack.cpp563
-rw-r--r--dom/media/gtest/TestDriftCompensation.cpp86
-rw-r--r--dom/media/gtest/TestDynamicResampler.cpp1556
-rw-r--r--dom/media/gtest/TestGMPCrossOrigin.cpp212
-rw-r--r--dom/media/gtest/TestGMPRemoveAndDelete.cpp472
-rw-r--r--dom/media/gtest/TestGMPUtils.cpp84
-rw-r--r--dom/media/gtest/TestGroupId.cpp302
-rw-r--r--dom/media/gtest/TestIntervalSet.cpp819
-rw-r--r--dom/media/gtest/TestKeyValueStorage.cpp109
-rw-r--r--dom/media/gtest/TestMP3Demuxer.cpp579
-rw-r--r--dom/media/gtest/TestMP4Demuxer.cpp613
-rw-r--r--dom/media/gtest/TestMediaCodecsSupport.cpp157
-rw-r--r--dom/media/gtest/TestMediaDataDecoder.cpp98
-rw-r--r--dom/media/gtest/TestMediaDataEncoder.cpp510
-rw-r--r--dom/media/gtest/TestMediaEventSource.cpp490
-rw-r--r--dom/media/gtest/TestMediaMIMETypes.cpp284
-rw-r--r--dom/media/gtest/TestMediaQueue.cpp288
-rw-r--r--dom/media/gtest/TestMediaSpan.cpp110
-rw-r--r--dom/media/gtest/TestMediaUtils.cpp240
-rw-r--r--dom/media/gtest/TestMuxer.cpp212
-rw-r--r--dom/media/gtest/TestOggWriter.cpp62
-rw-r--r--dom/media/gtest/TestOpusParser.cpp24
-rw-r--r--dom/media/gtest/TestPacer.cpp189
-rw-r--r--dom/media/gtest/TestRTCStatsTimestampMaker.cpp113
-rw-r--r--dom/media/gtest/TestRust.cpp10
-rw-r--r--dom/media/gtest/TestTimeUnit.cpp281
-rw-r--r--dom/media/gtest/TestVPXDecoding.cpp96
-rw-r--r--dom/media/gtest/TestVideoFrameConverter.cpp504
-rw-r--r--dom/media/gtest/TestVideoSegment.cpp44
-rw-r--r--dom/media/gtest/TestVideoTrackEncoder.cpp1467
-rw-r--r--dom/media/gtest/TestVideoUtils.cpp128
-rw-r--r--dom/media/gtest/TestWebMBuffered.cpp234
-rw-r--r--dom/media/gtest/TestWebMWriter.cpp388
-rw-r--r--dom/media/gtest/WaitFor.cpp19
-rw-r--r--dom/media/gtest/WaitFor.h134
-rw-r--r--dom/media/gtest/YUVBufferGenerator.cpp144
-rw-r--r--dom/media/gtest/YUVBufferGenerator.h32
-rw-r--r--dom/media/gtest/dash_dashinit.mp4bin0 -> 80388 bytes
-rw-r--r--dom/media/gtest/hello.rs6
-rw-r--r--dom/media/gtest/id3v2header.mp3bin0 -> 191302 bytes
-rw-r--r--dom/media/gtest/moz.build148
-rw-r--r--dom/media/gtest/mp4_demuxer/TestInterval.cpp88
-rw-r--r--dom/media/gtest/mp4_demuxer/TestMP4.cpp133
-rw-r--r--dom/media/gtest/mp4_demuxer/TestParser.cpp1019
-rw-r--r--dom/media/gtest/mp4_demuxer/moz.build66
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1156505.mp4bin0 -> 296 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1181213.mp4bin0 -> 2834 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1181215.mp4bin0 -> 3086 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1181223.mp4bin0 -> 2834 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1181719.mp4bin0 -> 3095 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1185230.mp4bin0 -> 3250 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1187067.mp4bin0 -> 2835 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1200326.mp4bin0 -> 1694 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1204580.mp4bin0 -> 5833 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1216748.mp4bin0 -> 296 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1296473.mp4bin0 -> 5995 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1296532.mp4bin0 -> 152132 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1301065-harder.mp4bin0 -> 632 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1301065-i64max.mp4bin0 -> 632 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1301065-i64min.mp4bin0 -> 632 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1301065-max-ez.mp4bin0 -> 632 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1301065-max-ok.mp4bin0 -> 632 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1301065-overfl.mp4bin0 -> 632 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1301065-u32max.mp4bin0 -> 632 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1301065-u64max.mp4bin0 -> 632 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1301065.mp4bin0 -> 632 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1329061.movbin0 -> 93681 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1351094.mp4bin0 -> 80388 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1388991.mp4bin0 -> 288821 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1389299.mp4bin0 -> 152132 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1389527.mp4bin0 -> 92225 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1395244.mp4bin0 -> 13651 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1410565.mp4bin0 -> 955656 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1513651-2-sample-description-entries.mp4bin0 -> 1100 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1519617-cenc-init-with-track_id-0.mp4bin0 -> 767 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1519617-track2-trafs-removed.mp4bin0 -> 282228 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1519617-video-has-track_id-0.mp4bin0 -> 282024 bytes
-rw-r--r--dom/media/gtest/mp4_demuxer/test_case_1714125-2-sample-description-entires-with-identical-crypto.mp4bin0 -> 1119 bytes
-rw-r--r--dom/media/gtest/negative_duration.mp4bin0 -> 684 bytes
-rw-r--r--dom/media/gtest/noise.mp3bin0 -> 965257 bytes
-rw-r--r--dom/media/gtest/noise_vbr.mp3bin0 -> 583679 bytes
-rw-r--r--dom/media/gtest/short-zero-in-moov.mp4bin0 -> 13655 bytes
-rw-r--r--dom/media/gtest/short-zero-inband.movbin0 -> 93641 bytes
-rw-r--r--dom/media/gtest/small-shot-false-positive.mp3bin0 -> 6845 bytes
-rw-r--r--dom/media/gtest/small-shot-partial-xing.mp3bin0 -> 6825 bytes
-rw-r--r--dom/media/gtest/small-shot.mp3bin0 -> 6825 bytes
-rw-r--r--dom/media/gtest/test.webmbin0 -> 1980 bytes
-rw-r--r--dom/media/gtest/test_InvalidElementId.webmbin0 -> 1122 bytes
-rw-r--r--dom/media/gtest/test_InvalidElementSize.webmbin0 -> 1122 bytes
-rw-r--r--dom/media/gtest/test_InvalidLargeEBMLMaxIdLength.webmbin0 -> 1122 bytes
-rw-r--r--dom/media/gtest/test_InvalidLargeElementId.webmbin0 -> 1129 bytes
-rw-r--r--dom/media/gtest/test_InvalidSmallEBMLMaxIdLength.webmbin0 -> 1122 bytes
-rw-r--r--dom/media/gtest/test_ValidLargeEBMLMaxIdLength.webmbin0 -> 1128 bytes
-rw-r--r--dom/media/gtest/test_ValidSmallEBMLMaxSizeLength.webmbin0 -> 1116 bytes
-rw-r--r--dom/media/gtest/test_case_1224361.vp8.ivfbin0 -> 1497 bytes
-rw-r--r--dom/media/gtest/test_case_1224363.vp8.ivfbin0 -> 1388 bytes
-rw-r--r--dom/media/gtest/test_case_1224369.vp8.ivfbin0 -> 204 bytes
-rw-r--r--dom/media/gtest/test_vbri.mp3bin0 -> 16519 bytes
-rw-r--r--dom/media/hls/HLSDecoder.cpp312
-rw-r--r--dom/media/hls/HLSDecoder.h79
-rw-r--r--dom/media/hls/HLSDemuxer.cpp628
-rw-r--r--dom/media/hls/HLSDemuxer.h137
-rw-r--r--dom/media/hls/HLSUtils.cpp12
-rw-r--r--dom/media/hls/HLSUtils.h21
-rw-r--r--dom/media/hls/moz.build24
-rw-r--r--dom/media/imagecapture/CaptureTask.cpp197
-rw-r--r--dom/media/imagecapture/CaptureTask.h90
-rw-r--r--dom/media/imagecapture/ImageCapture.cpp212
-rw-r--r--dom/media/imagecapture/ImageCapture.h95
-rw-r--r--dom/media/imagecapture/moz.build16
-rw-r--r--dom/media/ipc/MFCDMChild.cpp468
-rw-r--r--dom/media/ipc/MFCDMChild.h146
-rw-r--r--dom/media/ipc/MFCDMParent.cpp632
-rw-r--r--dom/media/ipc/MFCDMParent.h116
-rw-r--r--dom/media/ipc/MFCDMSerializers.h52
-rw-r--r--dom/media/ipc/MFMediaEngineChild.cpp394
-rw-r--r--dom/media/ipc/MFMediaEngineChild.h136
-rw-r--r--dom/media/ipc/MFMediaEngineParent.cpp715
-rw-r--r--dom/media/ipc/MFMediaEngineParent.h144
-rw-r--r--dom/media/ipc/MFMediaEngineUtils.cpp179
-rw-r--r--dom/media/ipc/MFMediaEngineUtils.h163
-rw-r--r--dom/media/ipc/MediaIPCUtils.h376
-rw-r--r--dom/media/ipc/PMFCDM.ipdl116
-rw-r--r--dom/media/ipc/PMFMediaEngine.ipdl62
-rw-r--r--dom/media/ipc/PMediaDecoderParams.ipdlh30
-rw-r--r--dom/media/ipc/PRDD.ipdl128
-rw-r--r--dom/media/ipc/PRemoteDecoder.ipdl80
-rw-r--r--dom/media/ipc/PRemoteDecoderManager.ipdl70
-rw-r--r--dom/media/ipc/RDDChild.cpp225
-rw-r--r--dom/media/ipc/RDDChild.h86
-rw-r--r--dom/media/ipc/RDDParent.cpp340
-rw-r--r--dom/media/ipc/RDDParent.h81
-rw-r--r--dom/media/ipc/RDDProcessHost.cpp312
-rw-r--r--dom/media/ipc/RDDProcessHost.h161
-rw-r--r--dom/media/ipc/RDDProcessImpl.cpp51
-rw-r--r--dom/media/ipc/RDDProcessImpl.h39
-rw-r--r--dom/media/ipc/RDDProcessManager.cpp413
-rw-r--r--dom/media/ipc/RDDProcessManager.h128
-rw-r--r--dom/media/ipc/RemoteAudioDecoder.cpp121
-rw-r--r--dom/media/ipc/RemoteAudioDecoder.h53
-rw-r--r--dom/media/ipc/RemoteDecodeUtils.cpp103
-rw-r--r--dom/media/ipc/RemoteDecodeUtils.h31
-rw-r--r--dom/media/ipc/RemoteDecoderChild.cpp315
-rw-r--r--dom/media/ipc/RemoteDecoderChild.h90
-rw-r--r--dom/media/ipc/RemoteDecoderManagerChild.cpp886
-rw-r--r--dom/media/ipc/RemoteDecoderManagerChild.h153
-rw-r--r--dom/media/ipc/RemoteDecoderManagerParent.cpp351
-rw-r--r--dom/media/ipc/RemoteDecoderManagerParent.h101
-rw-r--r--dom/media/ipc/RemoteDecoderModule.cpp87
-rw-r--r--dom/media/ipc/RemoteDecoderModule.h52
-rw-r--r--dom/media/ipc/RemoteDecoderParent.cpp228
-rw-r--r--dom/media/ipc/RemoteDecoderParent.h74
-rw-r--r--dom/media/ipc/RemoteImageHolder.cpp172
-rw-r--r--dom/media/ipc/RemoteImageHolder.h66
-rw-r--r--dom/media/ipc/RemoteMediaData.cpp374
-rw-r--r--dom/media/ipc/RemoteMediaData.h395
-rw-r--r--dom/media/ipc/RemoteMediaDataDecoder.cpp163
-rw-r--r--dom/media/ipc/RemoteMediaDataDecoder.h68
-rw-r--r--dom/media/ipc/RemoteVideoDecoder.cpp296
-rw-r--r--dom/media/ipc/RemoteVideoDecoder.h80
-rw-r--r--dom/media/ipc/ShmemRecycleAllocator.h60
-rw-r--r--dom/media/ipc/moz.build107
-rw-r--r--dom/media/mediacapabilities/BenchmarkStorageChild.cpp34
-rw-r--r--dom/media/mediacapabilities/BenchmarkStorageChild.h28
-rw-r--r--dom/media/mediacapabilities/BenchmarkStorageParent.cpp130
-rw-r--r--dom/media/mediacapabilities/BenchmarkStorageParent.h43
-rw-r--r--dom/media/mediacapabilities/DecoderBenchmark.cpp243
-rw-r--r--dom/media/mediacapabilities/DecoderBenchmark.h77
-rw-r--r--dom/media/mediacapabilities/KeyValueStorage.cpp234
-rw-r--r--dom/media/mediacapabilities/KeyValueStorage.h48
-rw-r--r--dom/media/mediacapabilities/MediaCapabilities.cpp658
-rw-r--r--dom/media/mediacapabilities/MediaCapabilities.h104
-rw-r--r--dom/media/mediacapabilities/PBenchmarkStorage.ipdl23
-rw-r--r--dom/media/mediacapabilities/moz.build32
-rw-r--r--dom/media/mediacontrol/AudioFocusManager.cpp134
-rw-r--r--dom/media/mediacontrol/AudioFocusManager.h54
-rw-r--r--dom/media/mediacontrol/ContentMediaController.cpp376
-rw-r--r--dom/media/mediacontrol/ContentMediaController.h109
-rw-r--r--dom/media/mediacontrol/ContentPlaybackController.cpp210
-rw-r--r--dom/media/mediacontrol/ContentPlaybackController.h73
-rw-r--r--dom/media/mediacontrol/FetchImageHelper.cpp164
-rw-r--r--dom/media/mediacontrol/FetchImageHelper.h82
-rw-r--r--dom/media/mediacontrol/MediaControlIPC.h75
-rw-r--r--dom/media/mediacontrol/MediaControlKeyManager.cpp228
-rw-r--r--dom/media/mediacontrol/MediaControlKeyManager.h73
-rw-r--r--dom/media/mediacontrol/MediaControlKeySource.cpp122
-rw-r--r--dom/media/mediacontrol/MediaControlKeySource.h122
-rw-r--r--dom/media/mediacontrol/MediaControlService.cpp540
-rw-r--r--dom/media/mediacontrol/MediaControlService.h181
-rw-r--r--dom/media/mediacontrol/MediaControlUtils.cpp26
-rw-r--r--dom/media/mediacontrol/MediaControlUtils.h216
-rw-r--r--dom/media/mediacontrol/MediaController.cpp560
-rw-r--r--dom/media/mediacontrol/MediaController.h214
-rw-r--r--dom/media/mediacontrol/MediaPlaybackStatus.cpp142
-rw-r--r--dom/media/mediacontrol/MediaPlaybackStatus.h139
-rw-r--r--dom/media/mediacontrol/MediaStatusManager.cpp482
-rw-r--r--dom/media/mediacontrol/MediaStatusManager.h276
-rw-r--r--dom/media/mediacontrol/PositionStateEvent.h60
-rw-r--r--dom/media/mediacontrol/moz.build43
-rw-r--r--dom/media/mediacontrol/tests/browser/browser.ini53
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_audio_focus_management.js179
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_control_page_with_audible_and_inaudible_media.js94
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_default_action_handler.js422
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_media_control_audio_focus_within_a_page.js358
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_media_control_before_media_starts.js205
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_media_control_captured_audio.js45
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_media_control_keys_event.js62
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_media_control_main_controller.js341
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_media_control_metadata.js416
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_media_control_non_eligible_media.js204
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_media_control_playback_state.js116
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_media_control_position_state.js150
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_media_control_seekto.js89
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_media_control_stop_timer.js79
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_media_control_supported_keys.js130
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_nosrc_and_error_media.js102
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_only_control_non_real_time_media.js76
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_remove_controllable_media_for_active_controller.js108
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_resume_latest_paused_media.js189
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_seek_captured_audio.js59
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_stop_control_after_media_reaches_to_end.js108
-rw-r--r--dom/media/mediacontrol/tests/browser/browser_suspend_inactive_tab.js131
-rw-r--r--dom/media/mediacontrol/tests/browser/file_audio_and_inaudible_media.html10
-rw-r--r--dom/media/mediacontrol/tests/browser/file_autoplay.html9
-rw-r--r--dom/media/mediacontrol/tests/browser/file_empty_title.html9
-rw-r--r--dom/media/mediacontrol/tests/browser/file_error_media.html9
-rw-r--r--dom/media/mediacontrol/tests/browser/file_iframe_media.html94
-rw-r--r--dom/media/mediacontrol/tests/browser/file_main_frame_with_multiple_child_session_frames.html11
-rw-r--r--dom/media/mediacontrol/tests/browser/file_multiple_audible_media.html11
-rw-r--r--dom/media/mediacontrol/tests/browser/file_muted_autoplay.html9
-rw-r--r--dom/media/mediacontrol/tests/browser/file_no_src_media.html9
-rw-r--r--dom/media/mediacontrol/tests/browser/file_non_autoplay.html11
-rw-r--r--dom/media/mediacontrol/tests/browser/file_non_eligible_media.html14
-rw-r--r--dom/media/mediacontrol/tests/browser/file_non_looping_media.html9
-rw-r--r--dom/media/mediacontrol/tests/browser/head.js402
-rw-r--r--dom/media/mediacontrol/tests/gtest/MediaKeyListenerTest.h39
-rw-r--r--dom/media/mediacontrol/tests/gtest/TestAudioFocusManager.cpp163
-rw-r--r--dom/media/mediacontrol/tests/gtest/TestMediaControlService.cpp64
-rw-r--r--dom/media/mediacontrol/tests/gtest/TestMediaController.cpp204
-rw-r--r--dom/media/mediacontrol/tests/gtest/TestMediaKeysEvent.cpp49
-rw-r--r--dom/media/mediacontrol/tests/gtest/TestMediaKeysEventMac.mm136
-rw-r--r--dom/media/mediacontrol/tests/gtest/TestMediaKeysEventMediaCenter.mm166
-rw-r--r--dom/media/mediacontrol/tests/gtest/moz.build23
-rw-r--r--dom/media/mediasession/MediaMetadata.cpp154
-rw-r--r--dom/media/mediasession/MediaMetadata.h97
-rw-r--r--dom/media/mediasession/MediaSession.cpp335
-rw-r--r--dom/media/mediasession/MediaSession.h132
-rw-r--r--dom/media/mediasession/MediaSessionIPCUtils.h102
-rw-r--r--dom/media/mediasession/moz.build24
-rw-r--r--dom/media/mediasession/test/MediaSessionTestUtils.js30
-rw-r--r--dom/media/mediasession/test/browser.ini9
-rw-r--r--dom/media/mediasession/test/browser_active_mediasession_among_tabs.js201
-rw-r--r--dom/media/mediasession/test/crashtests/crashtests.list1
-rw-r--r--dom/media/mediasession/test/crashtests/inactive-mediasession.html16
-rw-r--r--dom/media/mediasession/test/file_media_session.html31
-rw-r--r--dom/media/mediasession/test/file_trigger_actionhanlder_frame.html40
-rw-r--r--dom/media/mediasession/test/file_trigger_actionhanlder_window.html107
-rw-r--r--dom/media/mediasession/test/mochitest.ini12
-rw-r--r--dom/media/mediasession/test/test_setactionhandler.html92
-rw-r--r--dom/media/mediasession/test/test_trigger_actionhanlder.html57
-rw-r--r--dom/media/mediasink/AudioDecoderInputTrack.cpp681
-rw-r--r--dom/media/mediasink/AudioDecoderInputTrack.h242
-rw-r--r--dom/media/mediasink/AudioSink.cpp664
-rw-r--r--dom/media/mediasink/AudioSink.h188
-rw-r--r--dom/media/mediasink/AudioSinkWrapper.cpp496
-rw-r--r--dom/media/mediasink/AudioSinkWrapper.h161
-rw-r--r--dom/media/mediasink/DecodedStream.cpp1171
-rw-r--r--dom/media/mediasink/DecodedStream.h154
-rw-r--r--dom/media/mediasink/MediaSink.h142
-rw-r--r--dom/media/mediasink/VideoSink.cpp706
-rw-r--r--dom/media/mediasink/VideoSink.h177
-rw-r--r--dom/media/mediasink/moz.build25
-rw-r--r--dom/media/mediasource/AsyncEventRunner.h32
-rw-r--r--dom/media/mediasource/ContainerParser.cpp767
-rw-r--r--dom/media/mediasource/ContainerParser.h97
-rw-r--r--dom/media/mediasource/MediaSource.cpp698
-rw-r--r--dom/media/mediasource/MediaSource.h182
-rw-r--r--dom/media/mediasource/MediaSourceDecoder.cpp372
-rw-r--r--dom/media/mediasource/MediaSourceDecoder.h101
-rw-r--r--dom/media/mediasource/MediaSourceDemuxer.cpp530
-rw-r--r--dom/media/mediasource/MediaSourceDemuxer.h172
-rw-r--r--dom/media/mediasource/MediaSourceUtils.cpp49
-rw-r--r--dom/media/mediasource/MediaSourceUtils.h20
-rw-r--r--dom/media/mediasource/ResourceQueue.cpp204
-rw-r--r--dom/media/mediasource/ResourceQueue.h88
-rw-r--r--dom/media/mediasource/SourceBuffer.cpp765
-rw-r--r--dom/media/mediasource/SourceBuffer.h207
-rw-r--r--dom/media/mediasource/SourceBufferAttributes.h116
-rw-r--r--dom/media/mediasource/SourceBufferList.cpp187
-rw-r--r--dom/media/mediasource/SourceBufferList.h110
-rw-r--r--dom/media/mediasource/SourceBufferResource.cpp144
-rw-r--r--dom/media/mediasource/SourceBufferResource.h143
-rw-r--r--dom/media/mediasource/SourceBufferTask.h126
-rw-r--r--dom/media/mediasource/TrackBuffersManager.cpp3092
-rw-r--r--dom/media/mediasource/TrackBuffersManager.h568
-rw-r--r--dom/media/mediasource/gtest/TestContainerParser.cpp148
-rw-r--r--dom/media/mediasource/gtest/TestExtractAV1CodecDetails.cpp290
-rw-r--r--dom/media/mediasource/gtest/TestExtractVPXCodecDetails.cpp141
-rw-r--r--dom/media/mediasource/gtest/moz.build22
-rw-r--r--dom/media/mediasource/moz.build42
-rw-r--r--dom/media/mediasource/test/.eslintrc.js28
-rw-r--r--dom/media/mediasource/test/1516754.webmbin0 -> 1081344 bytes
-rw-r--r--dom/media/mediasource/test/1516754.webm^headers^1
-rw-r--r--dom/media/mediasource/test/aac20-48000-64000-1.m4sbin0 -> 24328 bytes
-rw-r--r--dom/media/mediasource/test/aac20-48000-64000-1.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/aac20-48000-64000-2.m4sbin0 -> 24132 bytes
-rw-r--r--dom/media/mediasource/test/aac20-48000-64000-2.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/aac20-48000-64000-init.mp4bin0 -> 1246 bytes
-rw-r--r--dom/media/mediasource/test/aac20-48000-64000-init.mp4^headers^1
-rw-r--r--dom/media/mediasource/test/aac51-48000-128000-1.m4sbin0 -> 48979 bytes
-rw-r--r--dom/media/mediasource/test/aac51-48000-128000-1.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/aac51-48000-128000-2.m4sbin0 -> 47727 bytes
-rw-r--r--dom/media/mediasource/test/aac51-48000-128000-2.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/aac51-48000-128000-init.mp4bin0 -> 634 bytes
-rw-r--r--dom/media/mediasource/test/aac51-48000-128000-init.mp4^headers^1
-rw-r--r--dom/media/mediasource/test/avc3/init.mp4bin0 -> 687 bytes
-rw-r--r--dom/media/mediasource/test/avc3/init.mp4^headers^1
-rw-r--r--dom/media/mediasource/test/avc3/segment1.m4sbin0 -> 696869 bytes
-rw-r--r--dom/media/mediasource/test/avc3/segment1.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop1.m4sbin0 -> 24424 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop1.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop10.m4sbin0 -> 18279 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop10.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop11.m4sbin0 -> 24607 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop11.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop12.m4sbin0 -> 22676 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop12.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop13.m4sbin0 -> 9847 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop13.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop2.m4sbin0 -> 22205 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop2.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop2s.mp4bin0 -> 48024 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop2s.mp4^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop3.m4sbin0 -> 24013 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop3.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop4.m4sbin0 -> 23112 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop4.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop5.m4sbin0 -> 18367 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop5.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop6.m4sbin0 -> 24455 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop6.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop7.m4sbin0 -> 22442 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop7.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop8.m4sbin0 -> 24356 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop8.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop9.m4sbin0 -> 23252 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop9.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_300-3s.webmbin0 -> 79429 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_300-3s.webm^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_480_624kbps-video1.m4sbin0 -> 66806 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_480_624kbps-video1.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_480_624kbps-video2.m4sbin0 -> 65292 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_480_624kbps-video2.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_480_624kbps-videoinit.mp4bin0 -> 1410 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_480_624kbps-videoinit.mp4^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio1.m4sbin0 -> 694 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio1.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio10.m4sbin0 -> 879 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio10.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio11.m4sbin0 -> 208 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio11.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio2.m4sbin0 -> 750 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio2.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio3.m4sbin0 -> 724 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio3.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio4.m4sbin0 -> 806 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio4.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio5.m4sbin0 -> 822 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio5.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio6.m4sbin0 -> 833 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio6.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio7.m4sbin0 -> 888 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio7.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio8.m4sbin0 -> 829 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio8.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio9.m4sbin0 -> 778 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio9.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audioinit.mp4bin0 -> 825 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audioinit.mp4^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_dash.mpd48
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.0-1.m4sbin0 -> 110108 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.0-1.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.0-2.m4sbin0 -> 116079 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.0-2.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.0-init.mp4bin0 -> 1441 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.0-init.mp4^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.1-1.m4sbin0 -> 110108 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.1-1.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.1-2.m4sbin0 -> 116079 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.1-2.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.1-init.mp4bin0 -> 1453 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.1-init.mp4^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.2-1.m4sbin0 -> 110108 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.2-1.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.2-2.m4sbin0 -> 116079 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.2-2.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.2-init.mp4bin0 -> 1453 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.2-init.mp4^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.3-1.m4sbin0 -> 110108 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.3-1.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.3-2.m4sbin0 -> 116079 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.3-2.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.3-init.mp4bin0 -> 1453 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.3-init.mp4^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.4-1.m4sbin0 -> 110108 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.4-1.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.4-2.m4sbin0 -> 116079 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.4-2.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.4-init.mp4bin0 -> 1453 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.4-init.mp4^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.5-1.m4sbin0 -> 110108 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.5-1.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.5-2.m4sbin0 -> 116079 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.5-2.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.5-init.mp4bin0 -> 1453 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.5-init.mp4^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.6-1.m4sbin0 -> 110108 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.6-1.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.6-2.m4sbin0 -> 116079 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.6-2.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.6-init.mp4bin0 -> 1453 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.6-init.mp4^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.7-1.m4sbin0 -> 110108 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.7-1.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.7-2.m4sbin0 -> 116079 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.7-2.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.7-init.mp4bin0 -> 1453 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.7-init.mp4^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.8-1.m4sbin0 -> 110108 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.8-1.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.8-2.m4sbin0 -> 116079 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.8-2.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.8-init.mp4bin0 -> 1453 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.8-init.mp4^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.9-1.m4sbin0 -> 110108 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.9-1.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.9-2.m4sbin0 -> 116079 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.9-2.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.9-init.mp4bin0 -> 1453 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_0.9-init.mp4^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_1.0-1.m4sbin0 -> 110108 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_1.0-1.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_1.0-2.m4sbin0 -> 116079 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_1.0-2.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_1.0-init.mp4bin0 -> 1453 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_1.0-init.mp4^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_1.1-1.m4sbin0 -> 143079 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_1.1-1.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_1.1-2.m4sbin0 -> 137858 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_1.1-2.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_1.1-init.mp4bin0 -> 1336 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_offset_1.1-init.mp4^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_trailing_skip_box_video1.m4sbin0 -> 1023860 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_trailing_skip_box_video1.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video1.m4sbin0 -> 23860 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video1.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video10.m4sbin0 -> 18109 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video10.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video11.m4sbin0 -> 23969 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video11.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video12.m4sbin0 -> 21937 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video12.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video13.m4sbin0 -> 16265 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video13.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video2.m4sbin0 -> 21595 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video2.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video3.m4sbin0 -> 23429 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video3.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video4.m4sbin0 -> 22446 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video4.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video5.m4sbin0 -> 18191 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video5.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video6.m4sbin0 -> 23773 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video6.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video7.m4sbin0 -> 21749 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video7.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video8.m4sbin0 -> 23608 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video8.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video9.m4sbin0 -> 22553 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video9.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_videoinit.mp4bin0 -> 887 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_videoinit.mp4^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbopinit.mp4bin0 -> 1395 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbopinit.mp4^headers^1
-rw-r--r--dom/media/mediasource/test/bug1718709_high_res.mp4bin0 -> 1038283 bytes
-rw-r--r--dom/media/mediasource/test/bug1718709_low_res.mp4bin0 -> 245318 bytes
-rw-r--r--dom/media/mediasource/test/crashtests/1005366.html27
-rw-r--r--dom/media/mediasource/test/crashtests/1059035.html26
-rw-r--r--dom/media/mediasource/test/crashtests/926665.html26
-rw-r--r--dom/media/mediasource/test/crashtests/931388.html17
-rw-r--r--dom/media/mediasource/test/crashtests/crashtests.list4
-rw-r--r--dom/media/mediasource/test/flac/00001.m4sbin0 -> 658125 bytes
-rw-r--r--dom/media/mediasource/test/flac/00001.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/flac/00002.m4sbin0 -> 685567 bytes
-rw-r--r--dom/media/mediasource/test/flac/00002.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/flac/00003.m4sbin0 -> 747868 bytes
-rw-r--r--dom/media/mediasource/test/flac/00003.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/flac/IS.mp4bin0 -> 608 bytes
-rw-r--r--dom/media/mediasource/test/flac/IS.mp4^headers^1
-rw-r--r--dom/media/mediasource/test/init-trackid2.mp4bin0 -> 9108 bytes
-rw-r--r--dom/media/mediasource/test/init-trackid2.mp4^headers^1
-rw-r--r--dom/media/mediasource/test/init-trackid3.mp4bin0 -> 9108 bytes
-rw-r--r--dom/media/mediasource/test/init-trackid3.mp4^headers^1
-rw-r--r--dom/media/mediasource/test/mediasource.js235
-rw-r--r--dom/media/mediasource/test/mochitest.ini213
-rw-r--r--dom/media/mediasource/test/seek.webmbin0 -> 215529 bytes
-rw-r--r--dom/media/mediasource/test/seek.webm^headers^1
-rw-r--r--dom/media/mediasource/test/seek_lowres.webmbin0 -> 100749 bytes
-rw-r--r--dom/media/mediasource/test/seek_lowres.webm^headers^1
-rw-r--r--dom/media/mediasource/test/segment-2.0001.m4sbin0 -> 34778 bytes
-rw-r--r--dom/media/mediasource/test/segment-2.0001.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/segment-2.0002.m4sbin0 -> 34653 bytes
-rw-r--r--dom/media/mediasource/test/segment-2.0002.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/segment-3.0001.m4sbin0 -> 34787 bytes
-rw-r--r--dom/media/mediasource/test/segment-3.0001.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/segment-3.0002.m4sbin0 -> 34640 bytes
-rw-r--r--dom/media/mediasource/test/segment-3.0002.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/tags_before_cluster.webmbin0 -> 111714 bytes
-rw-r--r--dom/media/mediasource/test/tags_before_cluster.webm^header^1
-rw-r--r--dom/media/mediasource/test/test_AVC3_mp4.html38
-rw-r--r--dom/media/mediasource/test/test_AbortAfterPartialMediaSegment.html62
-rw-r--r--dom/media/mediasource/test/test_AppendPartialInitSegment.html43
-rw-r--r--dom/media/mediasource/test/test_AudioChange_mp4.html49
-rw-r--r--dom/media/mediasource/test/test_AudioChange_mp4_WebAudio.html55
-rw-r--r--dom/media/mediasource/test/test_AutoRevocation.html40
-rw-r--r--dom/media/mediasource/test/test_BufferedSeek.html44
-rw-r--r--dom/media/mediasource/test/test_BufferedSeek_mp4.html43
-rw-r--r--dom/media/mediasource/test/test_BufferingWait.html52
-rw-r--r--dom/media/mediasource/test/test_BufferingWait_mp4.html49
-rw-r--r--dom/media/mediasource/test/test_ChangeType.html84
-rw-r--r--dom/media/mediasource/test/test_ChangeWhileWaitingOnMissingData_mp4.html37
-rw-r--r--dom/media/mediasource/test/test_DifferentStreamStartTimes.html54
-rw-r--r--dom/media/mediasource/test/test_DrainOnMissingData_mp4.html49
-rw-r--r--dom/media/mediasource/test/test_DurationChange.html71
-rw-r--r--dom/media/mediasource/test/test_DurationUpdated.html48
-rw-r--r--dom/media/mediasource/test/test_DurationUpdated_mp4.html47
-rw-r--r--dom/media/mediasource/test/test_EndOfStream.html29
-rw-r--r--dom/media/mediasource/test/test_EndOfStream_mp4.html29
-rw-r--r--dom/media/mediasource/test/test_EndedEvent.html31
-rw-r--r--dom/media/mediasource/test/test_Eviction_mp4.html63
-rw-r--r--dom/media/mediasource/test/test_ExperimentalAsync.html102
-rw-r--r--dom/media/mediasource/test/test_FrameSelection.html64
-rw-r--r--dom/media/mediasource/test/test_FrameSelection_mp4.html49
-rw-r--r--dom/media/mediasource/test/test_HEAAC_extradata.html89
-rw-r--r--dom/media/mediasource/test/test_HaveMetadataUnbufferedSeek.html38
-rw-r--r--dom/media/mediasource/test/test_HaveMetadataUnbufferedSeek_mp4.html42
-rw-r--r--dom/media/mediasource/test/test_InputBufferIsCleared.html58
-rw-r--r--dom/media/mediasource/test/test_LiveSeekable.html84
-rw-r--r--dom/media/mediasource/test/test_LoadedDataFired_mp4.html57
-rw-r--r--dom/media/mediasource/test/test_LoadedMetadataFired.html31
-rw-r--r--dom/media/mediasource/test/test_LoadedMetadataFired_mp4.html31
-rw-r--r--dom/media/mediasource/test/test_MediaSource.html92
-rw-r--r--dom/media/mediasource/test/test_MediaSource_capture_gc.html72
-rw-r--r--dom/media/mediasource/test/test_MediaSource_disabled.html31
-rw-r--r--dom/media/mediasource/test/test_MediaSource_flac_mp4.html33
-rw-r--r--dom/media/mediasource/test/test_MediaSource_memory_reporting.html47
-rw-r--r--dom/media/mediasource/test/test_MediaSource_mp4.html90
-rw-r--r--dom/media/mediasource/test/test_MultipleInitSegments.html49
-rw-r--r--dom/media/mediasource/test/test_MultipleInitSegments_mp4.html44
-rw-r--r--dom/media/mediasource/test/test_NoAudioLoopBackData.html78
-rw-r--r--dom/media/mediasource/test/test_NoAudioLoopBackData_Muted.html79
-rw-r--r--dom/media/mediasource/test/test_NoVideoLoopBackData.html81
-rw-r--r--dom/media/mediasource/test/test_OnEvents.html42
-rw-r--r--dom/media/mediasource/test/test_PlayEvents.html115
-rw-r--r--dom/media/mediasource/test/test_PlayEventsAutoPlaying.html58
-rw-r--r--dom/media/mediasource/test/test_PlayEventsAutoPlaying2.html58
-rw-r--r--dom/media/mediasource/test/test_RemoveSourceBuffer.html52
-rw-r--r--dom/media/mediasource/test/test_Resolution_change_should_not_cause_video_freeze.html49
-rw-r--r--dom/media/mediasource/test/test_ResumeAfterClearing_mp4.html44
-rw-r--r--dom/media/mediasource/test/test_SeekNoData_mp4.html57
-rw-r--r--dom/media/mediasource/test/test_SeekToEnd_mp4.html54
-rw-r--r--dom/media/mediasource/test/test_SeekToLastFrame_mp4.html34
-rw-r--r--dom/media/mediasource/test/test_SeekTwice_mp4.html45
-rw-r--r--dom/media/mediasource/test/test_SeekableBeforeAndAfterEndOfStream.html54
-rw-r--r--dom/media/mediasource/test/test_SeekableBeforeAndAfterEndOfStreamSplit.html60
-rw-r--r--dom/media/mediasource/test/test_SeekableBeforeAndAfterEndOfStreamSplit_mp4.html60
-rw-r--r--dom/media/mediasource/test/test_SeekableBeforeAndAfterEndOfStream_mp4.html55
-rw-r--r--dom/media/mediasource/test/test_SeekedEvent_mp4.html48
-rw-r--r--dom/media/mediasource/test/test_Sequence_mp4.html37
-rw-r--r--dom/media/mediasource/test/test_SetModeThrows.html34
-rw-r--r--dom/media/mediasource/test/test_SplitAppend.html36
-rw-r--r--dom/media/mediasource/test/test_SplitAppendDelay.html38
-rw-r--r--dom/media/mediasource/test/test_SplitAppendDelay_mp4.html39
-rw-r--r--dom/media/mediasource/test/test_SplitAppend_mp4.html38
-rw-r--r--dom/media/mediasource/test/test_Threshold_mp4.html73
-rw-r--r--dom/media/mediasource/test/test_TimestampOffset_mp4.html76
-rw-r--r--dom/media/mediasource/test/test_TruncatedDuration.html55
-rw-r--r--dom/media/mediasource/test/test_TruncatedDuration_mp4.html59
-rw-r--r--dom/media/mediasource/test/test_WMFUnmatchedAudioDataTime.html32
-rw-r--r--dom/media/mediasource/test/test_WaitingOnMissingData.html60
-rw-r--r--dom/media/mediasource/test/test_WaitingOnMissingDataEnded_mp4.html47
-rw-r--r--dom/media/mediasource/test/test_WaitingOnMissingData_mp4.html61
-rw-r--r--dom/media/mediasource/test/test_WaitingToEndedTransition_mp4.html52
-rw-r--r--dom/media/mediasource/test/test_WebMTagsBeforeCluster.html47
-rw-r--r--dom/media/mediasource/test/test_trackidchange_mp4.html32
-rw-r--r--dom/media/mediasource/test/whitenoise-he-aac-5s.mp4bin0 -> 27078 bytes
-rw-r--r--dom/media/mediasource/test/wmf_mismatchedaudiotime.mp4bin0 -> 48906 bytes
-rw-r--r--dom/media/metrics.yaml51
-rw-r--r--dom/media/moz.build416
-rw-r--r--dom/media/mp3/MP3Decoder.cpp45
-rw-r--r--dom/media/mp3/MP3Decoder.h29
-rw-r--r--dom/media/mp3/MP3Demuxer.cpp890
-rw-r--r--dom/media/mp3/MP3Demuxer.h187
-rw-r--r--dom/media/mp3/MP3FrameParser.cpp817
-rw-r--r--dom/media/mp3/MP3FrameParser.h374
-rw-r--r--dom/media/mp3/moz.build22
-rw-r--r--dom/media/mp4/Atom.h21
-rw-r--r--dom/media/mp4/AtomType.h29
-rw-r--r--dom/media/mp4/Box.cpp230
-rw-r--r--dom/media/mp4/Box.h100
-rw-r--r--dom/media/mp4/BufferStream.cpp59
-rw-r--r--dom/media/mp4/BufferStream.h45
-rw-r--r--dom/media/mp4/ByteStream.h41
-rw-r--r--dom/media/mp4/DecoderData.cpp357
-rw-r--r--dom/media/mp4/DecoderData.h76
-rw-r--r--dom/media/mp4/MP4Decoder.cpp222
-rw-r--r--dom/media/mp4/MP4Decoder.h52
-rw-r--r--dom/media/mp4/MP4Demuxer.cpp620
-rw-r--r--dom/media/mp4/MP4Demuxer.h52
-rw-r--r--dom/media/mp4/MP4Interval.h137
-rw-r--r--dom/media/mp4/MP4Metadata.cpp507
-rw-r--r--dom/media/mp4/MP4Metadata.h116
-rw-r--r--dom/media/mp4/MoofParser.cpp1286
-rw-r--r--dom/media/mp4/MoofParser.h361
-rw-r--r--dom/media/mp4/ResourceStream.cpp56
-rw-r--r--dom/media/mp4/ResourceStream.h48
-rw-r--r--dom/media/mp4/SampleIterator.cpp712
-rw-r--r--dom/media/mp4/SampleIterator.h134
-rw-r--r--dom/media/mp4/SinfParser.cpp95
-rw-r--r--dom/media/mp4/SinfParser.h56
-rw-r--r--dom/media/mp4/moz.build45
-rw-r--r--dom/media/nsIAudioDeviceInfo.idl54
-rw-r--r--dom/media/nsIDocumentActivity.h31
-rw-r--r--dom/media/nsIMediaDevice.idl18
-rw-r--r--dom/media/nsIMediaManager.idl42
-rw-r--r--dom/media/ogg/OggCodecState.cpp1800
-rw-r--r--dom/media/ogg/OggCodecState.h628
-rw-r--r--dom/media/ogg/OggCodecStore.cpp31
-rw-r--r--dom/media/ogg/OggCodecStore.h37
-rw-r--r--dom/media/ogg/OggDecoder.cpp82
-rw-r--r--dom/media/ogg/OggDecoder.h29
-rw-r--r--dom/media/ogg/OggDemuxer.cpp2172
-rw-r--r--dom/media/ogg/OggDemuxer.h363
-rw-r--r--dom/media/ogg/OggRLBox.h30
-rw-r--r--dom/media/ogg/OggRLBoxTypes.h17
-rw-r--r--dom/media/ogg/OggWriter.cpp197
-rw-r--r--dom/media/ogg/OggWriter.h55
-rw-r--r--dom/media/ogg/OpusParser.cpp217
-rw-r--r--dom/media/ogg/OpusParser.h48
-rw-r--r--dom/media/ogg/moz.build32
-rw-r--r--dom/media/platforms/AllocationPolicy.cpp241
-rw-r--r--dom/media/platforms/AllocationPolicy.h183
-rw-r--r--dom/media/platforms/MediaCodecsSupport.cpp206
-rw-r--r--dom/media/platforms/MediaCodecsSupport.h194
-rw-r--r--dom/media/platforms/MediaTelemetryConstants.h21
-rw-r--r--dom/media/platforms/PDMFactory.cpp904
-rw-r--r--dom/media/platforms/PDMFactory.h113
-rw-r--r--dom/media/platforms/PEMFactory.cpp74
-rw-r--r--dom/media/platforms/PEMFactory.h40
-rw-r--r--dom/media/platforms/PlatformDecoderModule.cpp55
-rw-r--r--dom/media/platforms/PlatformDecoderModule.h567
-rw-r--r--dom/media/platforms/PlatformEncoderModule.h408
-rw-r--r--dom/media/platforms/ReorderQueue.h28
-rw-r--r--dom/media/platforms/SimpleMap.h55
-rw-r--r--dom/media/platforms/agnostic/AOMDecoder.cpp1066
-rw-r--r--dom/media/platforms/agnostic/AOMDecoder.h287
-rw-r--r--dom/media/platforms/agnostic/AgnosticDecoderModule.cpp218
-rw-r--r--dom/media/platforms/agnostic/AgnosticDecoderModule.h39
-rw-r--r--dom/media/platforms/agnostic/BlankDecoderModule.cpp144
-rw-r--r--dom/media/platforms/agnostic/BlankDecoderModule.h68
-rw-r--r--dom/media/platforms/agnostic/DAV1DDecoder.cpp382
-rw-r--r--dom/media/platforms/agnostic/DAV1DDecoder.h68
-rw-r--r--dom/media/platforms/agnostic/DummyMediaDataDecoder.cpp80
-rw-r--r--dom/media/platforms/agnostic/DummyMediaDataDecoder.h68
-rw-r--r--dom/media/platforms/agnostic/NullDecoderModule.cpp57
-rw-r--r--dom/media/platforms/agnostic/OpusDecoder.cpp380
-rw-r--r--dom/media/platforms/agnostic/OpusDecoder.h70
-rw-r--r--dom/media/platforms/agnostic/TheoraDecoder.cpp267
-rw-r--r--dom/media/platforms/agnostic/TheoraDecoder.h64
-rw-r--r--dom/media/platforms/agnostic/VPXDecoder.cpp676
-rw-r--r--dom/media/platforms/agnostic/VPXDecoder.h208
-rw-r--r--dom/media/platforms/agnostic/VorbisDecoder.cpp364
-rw-r--r--dom/media/platforms/agnostic/VorbisDecoder.h66
-rw-r--r--dom/media/platforms/agnostic/WAVDecoder.cpp162
-rw-r--r--dom/media/platforms/agnostic/WAVDecoder.h44
-rw-r--r--dom/media/platforms/agnostic/bytestreams/Adts.cpp94
-rw-r--r--dom/media/platforms/agnostic/bytestreams/Adts.h22
-rw-r--r--dom/media/platforms/agnostic/bytestreams/AnnexB.cpp364
-rw-r--r--dom/media/platforms/agnostic/bytestreams/AnnexB.h66
-rw-r--r--dom/media/platforms/agnostic/bytestreams/H264.cpp1356
-rw-r--r--dom/media/platforms/agnostic/bytestreams/H264.h525
-rw-r--r--dom/media/platforms/agnostic/bytestreams/gtest/TestAnnexB.cpp144
-rw-r--r--dom/media/platforms/agnostic/bytestreams/gtest/moz.build11
-rw-r--r--dom/media/platforms/agnostic/bytestreams/moz.build35
-rw-r--r--dom/media/platforms/agnostic/eme/ChromiumCDMVideoDecoder.cpp156
-rw-r--r--dom/media/platforms/agnostic/eme/ChromiumCDMVideoDecoder.h54
-rw-r--r--dom/media/platforms/agnostic/eme/DecryptThroughputLimit.h103
-rw-r--r--dom/media/platforms/agnostic/eme/EMEDecoderModule.cpp479
-rw-r--r--dom/media/platforms/agnostic/eme/EMEDecoderModule.h79
-rw-r--r--dom/media/platforms/agnostic/eme/SamplesWaitingForKey.cpp79
-rw-r--r--dom/media/platforms/agnostic/eme/SamplesWaitingForKey.h68
-rw-r--r--dom/media/platforms/agnostic/eme/moz.build22
-rw-r--r--dom/media/platforms/agnostic/gmp/GMPDecoderModule.cpp94
-rw-r--r--dom/media/platforms/agnostic/gmp/GMPDecoderModule.h58
-rw-r--r--dom/media/platforms/agnostic/gmp/GMPVideoDecoder.cpp489
-rw-r--r--dom/media/platforms/agnostic/gmp/GMPVideoDecoder.h129
-rw-r--r--dom/media/platforms/agnostic/gmp/moz.build24
-rw-r--r--dom/media/platforms/android/AndroidDataEncoder.cpp534
-rw-r--r--dom/media/platforms/android/AndroidDataEncoder.h110
-rw-r--r--dom/media/platforms/android/AndroidDecoderModule.cpp243
-rw-r--r--dom/media/platforms/android/AndroidDecoderModule.h60
-rw-r--r--dom/media/platforms/android/AndroidEncoderModule.cpp56
-rw-r--r--dom/media/platforms/android/AndroidEncoderModule.h23
-rw-r--r--dom/media/platforms/android/JavaCallbacksSupport.h73
-rw-r--r--dom/media/platforms/android/RemoteDataDecoder.cpp1136
-rw-r--r--dom/media/platforms/android/RemoteDataDecoder.h112
-rw-r--r--dom/media/platforms/apple/AppleATDecoder.cpp672
-rw-r--r--dom/media/platforms/apple/AppleATDecoder.h80
-rw-r--r--dom/media/platforms/apple/AppleDecoderModule.cpp230
-rw-r--r--dom/media/platforms/apple/AppleDecoderModule.h62
-rw-r--r--dom/media/platforms/apple/AppleEncoderModule.cpp25
-rw-r--r--dom/media/platforms/apple/AppleEncoderModule.h27
-rw-r--r--dom/media/platforms/apple/AppleUtils.h88
-rw-r--r--dom/media/platforms/apple/AppleVTDecoder.cpp761
-rw-r--r--dom/media/platforms/apple/AppleVTDecoder.h145
-rw-r--r--dom/media/platforms/apple/AppleVTEncoder.cpp628
-rw-r--r--dom/media/platforms/apple/AppleVTEncoder.h85
-rw-r--r--dom/media/platforms/ffmpeg/FFmpegAudioDecoder.cpp421
-rw-r--r--dom/media/platforms/ffmpeg/FFmpegAudioDecoder.h65
-rw-r--r--dom/media/platforms/ffmpeg/FFmpegDataDecoder.cpp308
-rw-r--r--dom/media/platforms/ffmpeg/FFmpegDataDecoder.h90
-rw-r--r--dom/media/platforms/ffmpeg/FFmpegDecoderModule.cpp13
-rw-r--r--dom/media/platforms/ffmpeg/FFmpegDecoderModule.h129
-rw-r--r--dom/media/platforms/ffmpeg/FFmpegLibWrapper.cpp358
-rw-r--r--dom/media/platforms/ffmpeg/FFmpegLibWrapper.h180
-rw-r--r--dom/media/platforms/ffmpeg/FFmpegLibs.h50
-rw-r--r--dom/media/platforms/ffmpeg/FFmpegLog.h23
-rw-r--r--dom/media/platforms/ffmpeg/FFmpegRDFTTypes.h34
-rw-r--r--dom/media/platforms/ffmpeg/FFmpegRuntimeLinker.cpp199
-rw-r--r--dom/media/platforms/ffmpeg/FFmpegRuntimeLinker.h45
-rw-r--r--dom/media/platforms/ffmpeg/FFmpegVideoDecoder.cpp1627
-rw-r--r--dom/media/platforms/ffmpeg/FFmpegVideoDecoder.h233
-rw-r--r--dom/media/platforms/ffmpeg/FFmpegVideoFramePool.cpp414
-rw-r--r--dom/media/platforms/ffmpeg/FFmpegVideoFramePool.h155
-rw-r--r--dom/media/platforms/ffmpeg/README_mozilla11
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg57/include/COPYING.LGPLv2.1504
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg57/include/libavcodec/avcodec.h5418
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg57/include/libavcodec/avfft.h118
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg57/include/libavcodec/vaapi.h189
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg57/include/libavcodec/vdpau.h253
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg57/include/libavcodec/version.h210
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/attributes.h168
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/avconfig.h7
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/avutil.h343
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/buffer.h274
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/channel_layout.h223
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/common.h519
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/cpu.h117
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/dict.h198
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/error.h126
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/frame.h713
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/intfloat.h77
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/log.h359
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/macros.h50
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/mathematics.h165
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/mem.h406
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/pixfmt.h469
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/rational.h173
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/samplefmt.h271
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/version.h129
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg57/moz.build31
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/COPYING.LGPLv2.1504
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/avcodec.h4184
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/avfft.h118
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/bsf.h325
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/codec.h480
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/codec_desc.h128
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/codec_id.h629
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/codec_par.h234
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/packet.h774
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/vaapi.h86
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/vdpau.h176
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/version.h137
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/attributes.h167
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/avconfig.h6
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/avutil.h365
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/buffer.h291
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/channel_layout.h232
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/common.h560
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/cpu.h130
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/dict.h200
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/error.h126
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/frame.h893
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/hwcontext.h584
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/hwcontext_drm.h169
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/hwcontext_vaapi.h117
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/intfloat.h77
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/log.h362
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/macros.h50
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/mathematics.h242
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/mem.h700
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/pixfmt.h529
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/rational.h214
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/samplefmt.h272
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/version.h139
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg58/moz.build39
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/COPYING.LGPLv2.1504
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/avcodec.h3204
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/avfft.h119
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/bsf.h320
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/codec.h513
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/codec_desc.h128
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/codec_id.h637
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/codec_par.h236
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/defs.h171
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/packet.h724
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/vdpau.h156
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/version.h67
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/attributes.h173
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/avconfig.h6
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/avutil.h366
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/buffer.h324
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/channel_layout.h270
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/common.h590
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/cpu.h138
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/dict.h215
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/error.h158
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/frame.h927
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/hwcontext.h601
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/hwcontext_drm.h169
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/hwcontext_vaapi.h117
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/intfloat.h73
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/log.h388
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/macros.h87
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/mathematics.h247
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/mem.h708
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/pixfmt.h808
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/rational.h221
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/samplefmt.h276
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/version.h118
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg59/moz.build39
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/COPYING.LGPLv2.1504
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/avcodec.h3230
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/avdct.h85
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/avfft.h119
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/bsf.h335
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/codec.h387
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/codec_desc.h128
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/codec_id.h669
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/codec_par.h247
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/defs.h203
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/packet.h730
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/vdpau.h156
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/version.h45
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/version_major.h52
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/attributes.h173
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/avconfig.h6
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/avutil.h371
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/buffer.h324
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/channel_layout.h842
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/common.h589
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/cpu.h150
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/dict.h259
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/error.h158
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/frame.h960
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/hwcontext.h606
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/hwcontext_drm.h169
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/hwcontext_vaapi.h117
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/intfloat.h73
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/log.h388
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/macros.h87
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/mathematics.h249
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/mem.h613
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/pixfmt.h891
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/rational.h222
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/samplefmt.h274
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/version.h122
-rw-r--r--dom/media/platforms/ffmpeg/ffmpeg60/moz.build39
-rw-r--r--dom/media/platforms/ffmpeg/ffvpx/FFVPXRuntimeLinker.cpp130
-rw-r--r--dom/media/platforms/ffmpeg/ffvpx/FFVPXRuntimeLinker.h37
-rw-r--r--dom/media/platforms/ffmpeg/ffvpx/moz.build50
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/COPYING.LGPLv2.1504
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavcodec/avcodec.h4761
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavcodec/avfft.h99
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavcodec/dxva2.h71
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavcodec/old_codec_ids.h398
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavcodec/opt.h34
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavcodec/vaapi.h167
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavcodec/vda.h144
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavcodec/vdpau.h88
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavcodec/version.h126
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavcodec/xvmc.h151
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/adler32.h43
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/aes.h57
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/attributes.h136
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/audio_fifo.h146
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/audioconvert.h130
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/avassert.h66
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/avconfig.h6
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/avstring.h175
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/avutil.h326
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/base64.h65
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/blowfish.h77
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/bprint.h169
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/bswap.h109
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/common.h398
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/cpu.h56
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/crc.h44
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/dict.h121
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/error.h81
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/eval.h113
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/fifo.h141
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/file.h52
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/imgutils.h138
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/intfloat.h73
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/intfloat_readwrite.h40
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/intreadwrite.h522
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/lfg.h62
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/log.h172
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/lzo.h77
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/mathematics.h122
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/md5.h46
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/mem.h136
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/old_pix_fmts.h171
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/opt.h591
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/parseutils.h124
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/pixdesc.h177
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/pixfmt.h198
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/random_seed.h44
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/rational.h144
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/samplefmt.h148
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/sha.h66
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/time.h41
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/timecode.h140
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/timestamp.h74
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/version.h132
-rw-r--r--dom/media/platforms/ffmpeg/libav53/include/libavutil/xtea.h62
-rw-r--r--dom/media/platforms/ffmpeg/libav53/moz.build23
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/COPYING.LGPLv2.1504
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavcodec/avcodec.h4658
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavcodec/avfft.h116
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavcodec/dxva2.h88
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavcodec/old_codec_ids.h366
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavcodec/vaapi.h173
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavcodec/vda.h217
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavcodec/vdpau.h94
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavcodec/version.h95
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavcodec/xvmc.h168
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/adler32.h43
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/aes.h67
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/attributes.h122
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/audio_fifo.h146
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/audioconvert.h6
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/avassert.h66
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/avconfig.h6
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/avstring.h191
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/avutil.h275
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/base64.h65
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/blowfish.h76
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/bswap.h109
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/channel_layout.h182
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/common.h406
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/cpu.h84
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/crc.h74
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/dict.h129
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/error.h83
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/eval.h113
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/fifo.h131
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/file.h54
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/imgutils.h138
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/intfloat.h77
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/intfloat_readwrite.h40
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/intreadwrite.h549
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/lfg.h62
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/log.h173
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/lzo.h66
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/mathematics.h111
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/md5.h51
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/mem.h183
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/old_pix_fmts.h128
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/opt.h516
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/parseutils.h124
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/pixdesc.h223
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/pixfmt.h268
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/random_seed.h44
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/rational.h155
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/samplefmt.h220
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/sha.h76
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/time.h39
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/version.h87
-rw-r--r--dom/media/platforms/ffmpeg/libav54/include/libavutil/xtea.h61
-rw-r--r--dom/media/platforms/ffmpeg/libav54/moz.build23
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/COPYING.LGPLv2.1504
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavcodec/avcodec.h4356
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavcodec/avfft.h118
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavcodec/dxva2.h88
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavcodec/vaapi.h173
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavcodec/vda.h142
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavcodec/vdpau.h189
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavcodec/version.h127
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavcodec/xvmc.h174
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/adler32.h43
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/aes.h67
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/attributes.h126
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/audio_fifo.h146
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/audioconvert.h6
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/avassert.h66
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/avconfig.h6
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/avstring.h226
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/avutil.h284
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/base64.h65
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/blowfish.h76
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/bswap.h111
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/buffer.h267
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/channel_layout.h186
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/common.h406
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/cpu.h87
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/crc.h74
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/dict.h146
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/downmix_info.h114
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/error.h82
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/eval.h113
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/fifo.h131
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/file.h54
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/frame.h552
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/hmac.h95
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/imgutils.h138
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/intfloat.h77
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/intreadwrite.h549
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/lfg.h62
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/log.h262
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/lzo.h66
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/macros.h48
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/mathematics.h111
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/md5.h51
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/mem.h265
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/old_pix_fmts.h134
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/opt.h516
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/parseutils.h124
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/pixdesc.h276
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/pixfmt.h283
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/random_seed.h44
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/rational.h155
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/samplefmt.h220
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/sha.h76
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/stereo3d.h147
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/time.h39
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/version.h116
-rw-r--r--dom/media/platforms/ffmpeg/libav55/include/libavutil/xtea.h61
-rw-r--r--dom/media/platforms/ffmpeg/libav55/moz.build29
-rw-r--r--dom/media/platforms/ffmpeg/moz.build28
-rw-r--r--dom/media/platforms/moz.build141
-rw-r--r--dom/media/platforms/omx/OmxCoreLibLinker.cpp113
-rw-r--r--dom/media/platforms/omx/OmxCoreLibLinker.h36
-rw-r--r--dom/media/platforms/omx/OmxDataDecoder.cpp1001
-rw-r--r--dom/media/platforms/omx/OmxDataDecoder.h224
-rw-r--r--dom/media/platforms/omx/OmxDecoderModule.cpp59
-rw-r--r--dom/media/platforms/omx/OmxDecoderModule.h33
-rw-r--r--dom/media/platforms/omx/OmxFunctionList.h13
-rw-r--r--dom/media/platforms/omx/OmxPlatformLayer.cpp307
-rw-r--r--dom/media/platforms/omx/OmxPlatformLayer.h103
-rw-r--r--dom/media/platforms/omx/OmxPromiseLayer.cpp355
-rw-r--r--dom/media/platforms/omx/OmxPromiseLayer.h243
-rw-r--r--dom/media/platforms/omx/PureOmxPlatformLayer.cpp405
-rw-r--r--dom/media/platforms/omx/PureOmxPlatformLayer.h110
-rw-r--r--dom/media/platforms/omx/moz.build36
-rw-r--r--dom/media/platforms/wmf/DXVA2Manager.cpp1512
-rw-r--r--dom/media/platforms/wmf/DXVA2Manager.h94
-rw-r--r--dom/media/platforms/wmf/MFCDMExtra.h307
-rw-r--r--dom/media/platforms/wmf/MFCDMProxy.cpp74
-rw-r--r--dom/media/platforms/wmf/MFCDMProxy.h71
-rw-r--r--dom/media/platforms/wmf/MFCDMSession.cpp314
-rw-r--r--dom/media/platforms/wmf/MFCDMSession.h93
-rw-r--r--dom/media/platforms/wmf/MFContentProtectionManager.cpp164
-rw-r--r--dom/media/platforms/wmf/MFContentProtectionManager.h79
-rw-r--r--dom/media/platforms/wmf/MFMediaEngineAudioStream.cpp137
-rw-r--r--dom/media/platforms/wmf/MFMediaEngineAudioStream.h51
-rw-r--r--dom/media/platforms/wmf/MFMediaEngineDecoderModule.cpp174
-rw-r--r--dom/media/platforms/wmf/MFMediaEngineDecoderModule.h45
-rw-r--r--dom/media/platforms/wmf/MFMediaEngineExtension.cpp88
-rw-r--r--dom/media/platforms/wmf/MFMediaEngineExtension.h49
-rw-r--r--dom/media/platforms/wmf/MFMediaEngineExtra.h715
-rw-r--r--dom/media/platforms/wmf/MFMediaEngineNotify.cpp32
-rw-r--r--dom/media/platforms/wmf/MFMediaEngineNotify.h55
-rw-r--r--dom/media/platforms/wmf/MFMediaEngineStream.cpp596
-rw-r--r--dom/media/platforms/wmf/MFMediaEngineStream.h228
-rw-r--r--dom/media/platforms/wmf/MFMediaEngineVideoStream.cpp372
-rw-r--r--dom/media/platforms/wmf/MFMediaEngineVideoStream.h107
-rw-r--r--dom/media/platforms/wmf/MFMediaSource.cpp605
-rw-r--r--dom/media/platforms/wmf/MFMediaSource.h188
-rw-r--r--dom/media/platforms/wmf/MFPMPHostWrapper.cpp66
-rw-r--r--dom/media/platforms/wmf/MFPMPHostWrapper.h42
-rw-r--r--dom/media/platforms/wmf/MFTDecoder.cpp430
-rw-r--r--dom/media/platforms/wmf/MFTDecoder.h132
-rw-r--r--dom/media/platforms/wmf/MFTEncoder.cpp754
-rw-r--r--dom/media/platforms/wmf/MFTEncoder.h144
-rw-r--r--dom/media/platforms/wmf/WMF.h198
-rw-r--r--dom/media/platforms/wmf/WMFAudioMFTManager.cpp315
-rw-r--r--dom/media/platforms/wmf/WMFAudioMFTManager.h69
-rw-r--r--dom/media/platforms/wmf/WMFDataEncoderUtils.h165
-rw-r--r--dom/media/platforms/wmf/WMFDecoderModule.cpp454
-rw-r--r--dom/media/platforms/wmf/WMFDecoderModule.h58
-rw-r--r--dom/media/platforms/wmf/WMFEncoderModule.cpp43
-rw-r--r--dom/media/platforms/wmf/WMFEncoderModule.h24
-rw-r--r--dom/media/platforms/wmf/WMFMediaDataDecoder.cpp279
-rw-r--r--dom/media/platforms/wmf/WMFMediaDataDecoder.h182
-rw-r--r--dom/media/platforms/wmf/WMFMediaDataEncoder.h337
-rw-r--r--dom/media/platforms/wmf/WMFUtils.cpp632
-rw-r--r--dom/media/platforms/wmf/WMFUtils.h104
-rw-r--r--dom/media/platforms/wmf/WMFVideoMFTManager.cpp1096
-rw-r--r--dom/media/platforms/wmf/WMFVideoMFTManager.h132
-rw-r--r--dom/media/platforms/wmf/gtest/TestCanCreateMFTDecoder.cpp21
-rw-r--r--dom/media/platforms/wmf/gtest/moz.build15
-rw-r--r--dom/media/platforms/wmf/moz.build85
-rw-r--r--dom/media/platforms/wrappers/AudioTrimmer.cpp221
-rw-r--r--dom/media/platforms/wrappers/AudioTrimmer.h56
-rw-r--r--dom/media/platforms/wrappers/MediaChangeMonitor.cpp951
-rw-r--r--dom/media/platforms/wrappers/MediaChangeMonitor.h142
-rw-r--r--dom/media/platforms/wrappers/MediaDataDecoderProxy.cpp149
-rw-r--r--dom/media/platforms/wrappers/MediaDataDecoderProxy.h64
-rw-r--r--dom/media/systemservices/CamerasChild.cpp535
-rw-r--r--dom/media/systemservices/CamerasChild.h262
-rw-r--r--dom/media/systemservices/CamerasParent.cpp1220
-rw-r--r--dom/media/systemservices/CamerasParent.h157
-rw-r--r--dom/media/systemservices/CamerasTypes.cpp26
-rw-r--r--dom/media/systemservices/CamerasTypes.h38
-rw-r--r--dom/media/systemservices/MediaChild.cpp95
-rw-r--r--dom/media/systemservices/MediaChild.h60
-rw-r--r--dom/media/systemservices/MediaParent.cpp536
-rw-r--r--dom/media/systemservices/MediaParent.h91
-rw-r--r--dom/media/systemservices/MediaSystemResourceClient.cpp67
-rw-r--r--dom/media/systemservices/MediaSystemResourceClient.h91
-rw-r--r--dom/media/systemservices/MediaSystemResourceManager.cpp358
-rw-r--r--dom/media/systemservices/MediaSystemResourceManager.h81
-rw-r--r--dom/media/systemservices/MediaSystemResourceManagerChild.cpp42
-rw-r--r--dom/media/systemservices/MediaSystemResourceManagerChild.h65
-rw-r--r--dom/media/systemservices/MediaSystemResourceManagerParent.cpp75
-rw-r--r--dom/media/systemservices/MediaSystemResourceManagerParent.h59
-rw-r--r--dom/media/systemservices/MediaSystemResourceMessageUtils.h24
-rw-r--r--dom/media/systemservices/MediaSystemResourceService.cpp222
-rw-r--r--dom/media/systemservices/MediaSystemResourceService.h83
-rw-r--r--dom/media/systemservices/MediaSystemResourceTypes.h23
-rw-r--r--dom/media/systemservices/MediaTaskUtils.h52
-rw-r--r--dom/media/systemservices/MediaUtils.cpp126
-rw-r--r--dom/media/systemservices/MediaUtils.h332
-rw-r--r--dom/media/systemservices/OSXRunLoopSingleton.cpp41
-rw-r--r--dom/media/systemservices/OSXRunLoopSingleton.h24
-rw-r--r--dom/media/systemservices/PCameras.ipdl93
-rw-r--r--dom/media/systemservices/PMedia.ipdl55
-rw-r--r--dom/media/systemservices/PMediaSystemResourceManager.ipdl38
-rw-r--r--dom/media/systemservices/ShmemPool.cpp99
-rw-r--r--dom/media/systemservices/ShmemPool.h181
-rw-r--r--dom/media/systemservices/VideoEngine.cpp245
-rw-r--r--dom/media/systemservices/VideoEngine.h117
-rw-r--r--dom/media/systemservices/VideoFrameUtils.cpp90
-rw-r--r--dom/media/systemservices/VideoFrameUtils.h48
-rw-r--r--dom/media/systemservices/android_video_capture/device_info_android.cc316
-rw-r--r--dom/media/systemservices/android_video_capture/device_info_android.h73
-rw-r--r--dom/media/systemservices/android_video_capture/java/src/org/webrtc/videoengine/CaptureCapabilityAndroid.java25
-rw-r--r--dom/media/systemservices/android_video_capture/java/src/org/webrtc/videoengine/VideoCaptureAndroid.java216
-rw-r--r--dom/media/systemservices/android_video_capture/java/src/org/webrtc/videoengine/VideoCaptureDeviceInfoAndroid.java121
-rw-r--r--dom/media/systemservices/android_video_capture/video_capture_android.cc270
-rw-r--r--dom/media/systemservices/android_video_capture/video_capture_android.h47
-rw-r--r--dom/media/systemservices/moz.build112
-rw-r--r--dom/media/systemservices/objc_video_capture/device_info.h56
-rw-r--r--dom/media/systemservices/objc_video_capture/device_info.mm170
-rw-r--r--dom/media/systemservices/objc_video_capture/device_info_avfoundation.h71
-rw-r--r--dom/media/systemservices/objc_video_capture/device_info_avfoundation.mm213
-rw-r--r--dom/media/systemservices/objc_video_capture/device_info_objc.h38
-rw-r--r--dom/media/systemservices/objc_video_capture/device_info_objc.mm166
-rw-r--r--dom/media/systemservices/objc_video_capture/rtc_video_capture_objc.h39
-rw-r--r--dom/media/systemservices/objc_video_capture/rtc_video_capture_objc.mm355
-rw-r--r--dom/media/systemservices/objc_video_capture/video_capture.h41
-rw-r--r--dom/media/systemservices/objc_video_capture/video_capture.mm102
-rw-r--r--dom/media/systemservices/objc_video_capture/video_capture_avfoundation.h79
-rw-r--r--dom/media/systemservices/objc_video_capture/video_capture_avfoundation.mm306
-rw-r--r--dom/media/systemservices/video_engine/browser_capture_impl.h78
-rw-r--r--dom/media/systemservices/video_engine/desktop_capture_impl.cc776
-rw-r--r--dom/media/systemservices/video_engine/desktop_capture_impl.h246
-rw-r--r--dom/media/systemservices/video_engine/desktop_device_info.cc488
-rw-r--r--dom/media/systemservices/video_engine/desktop_device_info.h84
-rw-r--r--dom/media/systemservices/video_engine/platform_uithread.cc198
-rw-r--r--dom/media/systemservices/video_engine/platform_uithread.h96
-rw-r--r--dom/media/systemservices/video_engine/tab_capturer.cc331
-rw-r--r--dom/media/systemservices/video_engine/tab_capturer.h88
-rw-r--r--dom/media/test/16bit_wave_extrametadata.wavbin0 -> 97814 bytes
-rw-r--r--dom/media/test/16bit_wave_extrametadata.wav^headers^1
-rw-r--r--dom/media/test/320x240.ogvbin0 -> 28942 bytes
-rw-r--r--dom/media/test/320x240.ogv^headers^1
-rw-r--r--dom/media/test/448636.ogvbin0 -> 7799 bytes
-rw-r--r--dom/media/test/448636.ogv^headers^1
-rw-r--r--dom/media/test/A4.ogvbin0 -> 94372 bytes
-rw-r--r--dom/media/test/A4.ogv^headers^1
-rw-r--r--dom/media/test/TestPatternHDR.mp4bin0 -> 179294 bytes
-rw-r--r--dom/media/test/VID_0001.oggbin0 -> 633435 bytes
-rw-r--r--dom/media/test/VID_0001.ogg^headers^1
-rw-r--r--dom/media/test/adts.aacbin0 -> 8537 bytes
-rw-r--r--dom/media/test/adts.aac^headers^1
-rw-r--r--dom/media/test/allowed.sjs61
-rw-r--r--dom/media/test/ambisonics.mp4bin0 -> 1053904 bytes
-rw-r--r--dom/media/test/ambisonics.mp4^headers^1
-rw-r--r--dom/media/test/audio-gaps-short.oggbin0 -> 5233 bytes
-rw-r--r--dom/media/test/audio-gaps-short.ogg^headers^1
-rw-r--r--dom/media/test/audio-gaps.oggbin0 -> 12306 bytes
-rw-r--r--dom/media/test/audio-gaps.ogg^headers^1
-rw-r--r--dom/media/test/audio-overhang.oggbin0 -> 45463 bytes
-rw-r--r--dom/media/test/audio-overhang.ogg^headers^1
-rw-r--r--dom/media/test/audio.wavbin0 -> 1422 bytes
-rw-r--r--dom/media/test/audio.wav^headers^1
-rw-r--r--dom/media/test/av1.mp4bin0 -> 13089 bytes
-rw-r--r--dom/media/test/av1.mp4^headers^1
-rw-r--r--dom/media/test/background_video.js224
-rw-r--r--dom/media/test/badtags.oggbin0 -> 5033 bytes
-rw-r--r--dom/media/test/badtags.ogg^headers^1
-rw-r--r--dom/media/test/bear-640x360-a_frag-cenc-key_rotation.mp4bin0 -> 80372 bytes
-rw-r--r--dom/media/test/bear-640x360-v_frag-cenc-key_rotation.mp4bin0 -> 280361 bytes
-rw-r--r--dom/media/test/beta-phrasebook.oggbin0 -> 47411 bytes
-rw-r--r--dom/media/test/beta-phrasebook.ogg^headers^1
-rw-r--r--dom/media/test/big-buck-bunny-cenc-avc3-1.m4sbin0 -> 60041 bytes
-rw-r--r--dom/media/test/big-buck-bunny-cenc-avc3-1.m4s^headers^1
-rw-r--r--dom/media/test/big-buck-bunny-cenc-avc3-init.mp4bin0 -> 819 bytes
-rw-r--r--dom/media/test/big-buck-bunny-cenc-avc3-init.mp4^headers^1
-rw-r--r--dom/media/test/big-short.wavbin0 -> 12366 bytes
-rw-r--r--dom/media/test/big-short.wav^headers^1
-rw-r--r--dom/media/test/big.wavbin0 -> 102444 bytes
-rw-r--r--dom/media/test/big.wav^headers^1
-rw-r--r--dom/media/test/bipbop-cenc-audio-key1.xml28
-rw-r--r--dom/media/test/bipbop-cenc-audio-key2.xml28
-rw-r--r--dom/media/test/bipbop-cenc-audio1.m4sbin0 -> 921 bytes
-rw-r--r--dom/media/test/bipbop-cenc-audio1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop-cenc-audio2.m4sbin0 -> 565 bytes
-rw-r--r--dom/media/test/bipbop-cenc-audio2.m4s^headers^1
-rw-r--r--dom/media/test/bipbop-cenc-audio3.m4sbin0 -> 977 bytes
-rw-r--r--dom/media/test/bipbop-cenc-audio3.m4s^headers^1
-rw-r--r--dom/media/test/bipbop-cenc-audioinit.mp4bin0 -> 1000 bytes
-rw-r--r--dom/media/test/bipbop-cenc-audioinit.mp4^headers^1
-rw-r--r--dom/media/test/bipbop-cenc-video-10s.mp4bin0 -> 299914 bytes
-rw-r--r--dom/media/test/bipbop-cenc-video-10s.mp4^headers^1
-rw-r--r--dom/media/test/bipbop-cenc-video-key1.xml28
-rw-r--r--dom/media/test/bipbop-cenc-video-key2.xml28
-rw-r--r--dom/media/test/bipbop-cenc-video1.m4sbin0 -> 25211 bytes
-rw-r--r--dom/media/test/bipbop-cenc-video1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop-cenc-video2.m4sbin0 -> 22934 bytes
-rw-r--r--dom/media/test/bipbop-cenc-video2.m4s^headers^1
-rw-r--r--dom/media/test/bipbop-cenc-videoinit.mp4bin0 -> 1058 bytes
-rw-r--r--dom/media/test/bipbop-cenc-videoinit.mp4^headers^1
-rw-r--r--dom/media/test/bipbop-cenc.sh29
-rw-r--r--dom/media/test/bipbop-clearkey-keyrotation-clear-lead-audio.mp4bin0 -> 8675 bytes
-rw-r--r--dom/media/test/bipbop-clearkey-keyrotation-clear-lead-audio.mp4^headers^1
-rw-r--r--dom/media/test/bipbop-clearkey-keyrotation-clear-lead-video.mp4bin0 -> 278040 bytes
-rw-r--r--dom/media/test/bipbop-clearkey-keyrotation-clear-lead-video.mp4^headers^1
-rw-r--r--dom/media/test/bipbop-frag-cenc.xml57
-rw-r--r--dom/media/test/bipbop-lateaudio.mp4bin0 -> 70404 bytes
-rw-r--r--dom/media/test/bipbop-lateaudio.mp4^headers^1
-rw-r--r--dom/media/test/bipbop-no-edts.mp4bin0 -> 285681 bytes
-rw-r--r--dom/media/test/bipbop.mp4bin0 -> 285765 bytes
-rw-r--r--dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-1.m4sbin0 -> 921 bytes
-rw-r--r--dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-2.m4sbin0 -> 565 bytes
-rw-r--r--dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-2.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-3.m4sbin0 -> 977 bytes
-rw-r--r--dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-3.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-4.m4sbin0 -> 389 bytes
-rw-r--r--dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-4.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-init.mp4bin0 -> 1020 bytes
-rw-r--r--dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-1.m4sbin0 -> 921 bytes
-rw-r--r--dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-2.m4sbin0 -> 565 bytes
-rw-r--r--dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-2.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-3.m4sbin0 -> 977 bytes
-rw-r--r--dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-3.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-4.m4sbin0 -> 389 bytes
-rw-r--r--dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-4.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-init.mp4bin0 -> 1020 bytes
-rw-r--r--dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_225w_175kbps-cenc-video-key1-1.m4sbin0 -> 37646 bytes
-rw-r--r--dom/media/test/bipbop_225w_175kbps-cenc-video-key1-1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_225w_175kbps-cenc-video-key1-init.mp4bin0 -> 1086 bytes
-rw-r--r--dom/media/test/bipbop_225w_175kbps-cenc-video-key1-init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_225w_175kbps-cenc-video-key2-1.m4sbin0 -> 37646 bytes
-rw-r--r--dom/media/test/bipbop_225w_175kbps-cenc-video-key2-1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_225w_175kbps-cenc-video-key2-init.mp4bin0 -> 1086 bytes
-rw-r--r--dom/media/test/bipbop_225w_175kbps-cenc-video-key2-init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_225w_175kbps.mp4bin0 -> 38713 bytes
-rw-r--r--dom/media/test/bipbop_225w_175kbps.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_300_215kbps-cenc-audio-key1-1.m4sbin0 -> 921 bytes
-rw-r--r--dom/media/test/bipbop_300_215kbps-cenc-audio-key1-1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_300_215kbps-cenc-audio-key1-2.m4sbin0 -> 565 bytes
-rw-r--r--dom/media/test/bipbop_300_215kbps-cenc-audio-key1-2.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_300_215kbps-cenc-audio-key1-3.m4sbin0 -> 977 bytes
-rw-r--r--dom/media/test/bipbop_300_215kbps-cenc-audio-key1-3.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_300_215kbps-cenc-audio-key1-4.m4sbin0 -> 389 bytes
-rw-r--r--dom/media/test/bipbop_300_215kbps-cenc-audio-key1-4.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_300_215kbps-cenc-audio-key1-init.mp4bin0 -> 874 bytes
-rw-r--r--dom/media/test/bipbop_300_215kbps-cenc-audio-key1-init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_300_215kbps-cenc-audio-key2-1.m4sbin0 -> 921 bytes
-rw-r--r--dom/media/test/bipbop_300_215kbps-cenc-audio-key2-1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_300_215kbps-cenc-audio-key2-2.m4sbin0 -> 565 bytes
-rw-r--r--dom/media/test/bipbop_300_215kbps-cenc-audio-key2-2.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_300_215kbps-cenc-audio-key2-3.m4sbin0 -> 977 bytes
-rw-r--r--dom/media/test/bipbop_300_215kbps-cenc-audio-key2-3.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_300_215kbps-cenc-audio-key2-4.m4sbin0 -> 389 bytes
-rw-r--r--dom/media/test/bipbop_300_215kbps-cenc-audio-key2-4.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_300_215kbps-cenc-audio-key2-init.mp4bin0 -> 874 bytes
-rw-r--r--dom/media/test/bipbop_300_215kbps-cenc-audio-key2-init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_300_215kbps-cenc-video-key1-1.m4sbin0 -> 25211 bytes
-rw-r--r--dom/media/test/bipbop_300_215kbps-cenc-video-key1-1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_300_215kbps-cenc-video-key1-2.m4sbin0 -> 22938 bytes
-rw-r--r--dom/media/test/bipbop_300_215kbps-cenc-video-key1-2.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_300_215kbps-cenc-video-key1-init.mp4bin0 -> 932 bytes
-rw-r--r--dom/media/test/bipbop_300_215kbps-cenc-video-key1-init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_300_215kbps-cenc-video-key2-1.m4sbin0 -> 25211 bytes
-rw-r--r--dom/media/test/bipbop_300_215kbps-cenc-video-key2-1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_300_215kbps-cenc-video-key2-2.m4sbin0 -> 22938 bytes
-rw-r--r--dom/media/test/bipbop_300_215kbps-cenc-video-key2-2.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_300_215kbps-cenc-video-key2-init.mp4bin0 -> 932 bytes
-rw-r--r--dom/media/test/bipbop_300_215kbps-cenc-video-key2-init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_300_215kbps.mp4bin0 -> 48393 bytes
-rw-r--r--dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-1.m4sbin0 -> 921 bytes
-rw-r--r--dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-2.m4sbin0 -> 565 bytes
-rw-r--r--dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-2.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-3.m4sbin0 -> 977 bytes
-rw-r--r--dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-3.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-4.m4sbin0 -> 389 bytes
-rw-r--r--dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-4.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-init.mp4bin0 -> 1020 bytes
-rw-r--r--dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-1.m4sbin0 -> 921 bytes
-rw-r--r--dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-2.m4sbin0 -> 565 bytes
-rw-r--r--dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-2.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-3.m4sbin0 -> 977 bytes
-rw-r--r--dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-3.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-4.m4sbin0 -> 389 bytes
-rw-r--r--dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-4.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-init.mp4bin0 -> 1020 bytes
-rw-r--r--dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_300wp_227kbps-cenc-video-key1-1.m4sbin0 -> 25211 bytes
-rw-r--r--dom/media/test/bipbop_300wp_227kbps-cenc-video-key1-1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_300wp_227kbps-cenc-video-key1-2.m4sbin0 -> 22938 bytes
-rw-r--r--dom/media/test/bipbop_300wp_227kbps-cenc-video-key1-2.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_300wp_227kbps-cenc-video-key1-init.mp4bin0 -> 1094 bytes
-rw-r--r--dom/media/test/bipbop_300wp_227kbps-cenc-video-key1-init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_300wp_227kbps-cenc-video-key2-1.m4sbin0 -> 25211 bytes
-rw-r--r--dom/media/test/bipbop_300wp_227kbps-cenc-video-key2-1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_300wp_227kbps-cenc-video-key2-2.m4sbin0 -> 22938 bytes
-rw-r--r--dom/media/test/bipbop_300wp_227kbps-cenc-video-key2-2.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_300wp_227kbps-cenc-video-key2-init.mp4bin0 -> 1094 bytes
-rw-r--r--dom/media/test/bipbop_300wp_227kbps-cenc-video-key2-init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_300wp_227kbps.mp4bin0 -> 48355 bytes
-rw-r--r--dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-1.m4sbin0 -> 921 bytes
-rw-r--r--dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-2.m4sbin0 -> 565 bytes
-rw-r--r--dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-2.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-3.m4sbin0 -> 977 bytes
-rw-r--r--dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-3.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-4.m4sbin0 -> 389 bytes
-rw-r--r--dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-4.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-init.mp4bin0 -> 1020 bytes
-rw-r--r--dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-1.m4sbin0 -> 921 bytes
-rw-r--r--dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-2.m4sbin0 -> 565 bytes
-rw-r--r--dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-2.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-3.m4sbin0 -> 977 bytes
-rw-r--r--dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-3.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-4.m4sbin0 -> 389 bytes
-rw-r--r--dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-4.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-init.mp4bin0 -> 1020 bytes
-rw-r--r--dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_360w_253kbps-cenc-video-key1-1.m4sbin0 -> 53149 bytes
-rw-r--r--dom/media/test/bipbop_360w_253kbps-cenc-video-key1-1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_360w_253kbps-cenc-video-key1-init.mp4bin0 -> 1088 bytes
-rw-r--r--dom/media/test/bipbop_360w_253kbps-cenc-video-key1-init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_360w_253kbps-cenc-video-key2-1.m4sbin0 -> 53149 bytes
-rw-r--r--dom/media/test/bipbop_360w_253kbps-cenc-video-key2-1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_360w_253kbps-cenc-video-key2-init.mp4bin0 -> 1088 bytes
-rw-r--r--dom/media/test/bipbop_360w_253kbps-cenc-video-key2-init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_360w_253kbps-clearkey-audio.webmbin0 -> 7553 bytes
-rw-r--r--dom/media/test/bipbop_360w_253kbps-clearkey-audio.webm^headers^1
-rw-r--r--dom/media/test/bipbop_360w_253kbps-clearkey-video-vp8.webmbin0 -> 44671 bytes
-rw-r--r--dom/media/test/bipbop_360w_253kbps-clearkey-video-vp8.webm^headers^1
-rw-r--r--dom/media/test/bipbop_360w_253kbps-clearkey-video-vp9.webmbin0 -> 46030 bytes
-rw-r--r--dom/media/test/bipbop_360w_253kbps-clearkey-video-vp9.webm^headers^1
-rw-r--r--dom/media/test/bipbop_360w_253kbps.mp4bin0 -> 54218 bytes
-rw-r--r--dom/media/test/bipbop_480_624kbps-cenc-audio-key1-1.m4sbin0 -> 921 bytes
-rw-r--r--dom/media/test/bipbop_480_624kbps-cenc-audio-key1-1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480_624kbps-cenc-audio-key1-2.m4sbin0 -> 565 bytes
-rw-r--r--dom/media/test/bipbop_480_624kbps-cenc-audio-key1-2.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480_624kbps-cenc-audio-key1-3.m4sbin0 -> 977 bytes
-rw-r--r--dom/media/test/bipbop_480_624kbps-cenc-audio-key1-3.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480_624kbps-cenc-audio-key1-4.m4sbin0 -> 389 bytes
-rw-r--r--dom/media/test/bipbop_480_624kbps-cenc-audio-key1-4.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480_624kbps-cenc-audio-key1-init.mp4bin0 -> 874 bytes
-rw-r--r--dom/media/test/bipbop_480_624kbps-cenc-audio-key1-init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_480_624kbps-cenc-audio-key2-1.m4sbin0 -> 921 bytes
-rw-r--r--dom/media/test/bipbop_480_624kbps-cenc-audio-key2-1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480_624kbps-cenc-audio-key2-2.m4sbin0 -> 565 bytes
-rw-r--r--dom/media/test/bipbop_480_624kbps-cenc-audio-key2-2.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480_624kbps-cenc-audio-key2-3.m4sbin0 -> 977 bytes
-rw-r--r--dom/media/test/bipbop_480_624kbps-cenc-audio-key2-3.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480_624kbps-cenc-audio-key2-4.m4sbin0 -> 389 bytes
-rw-r--r--dom/media/test/bipbop_480_624kbps-cenc-audio-key2-4.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480_624kbps-cenc-audio-key2-init.mp4bin0 -> 874 bytes
-rw-r--r--dom/media/test/bipbop_480_624kbps-cenc-audio-key2-init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_480_624kbps-cenc-video-key1-1.m4sbin0 -> 68025 bytes
-rw-r--r--dom/media/test/bipbop_480_624kbps-cenc-video-key1-1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480_624kbps-cenc-video-key1-2.m4sbin0 -> 66457 bytes
-rw-r--r--dom/media/test/bipbop_480_624kbps-cenc-video-key1-2.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480_624kbps-cenc-video-key1-init.mp4bin0 -> 932 bytes
-rw-r--r--dom/media/test/bipbop_480_624kbps-cenc-video-key1-init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_480_624kbps-cenc-video-key2-1.m4sbin0 -> 68025 bytes
-rw-r--r--dom/media/test/bipbop_480_624kbps-cenc-video-key2-1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480_624kbps-cenc-video-key2-2.m4sbin0 -> 66457 bytes
-rw-r--r--dom/media/test/bipbop_480_624kbps-cenc-video-key2-2.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480_624kbps-cenc-video-key2-init.mp4bin0 -> 932 bytes
-rw-r--r--dom/media/test/bipbop_480_624kbps-cenc-video-key2-init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_480_624kbps.mp4bin0 -> 133264 bytes
-rw-r--r--dom/media/test/bipbop_480_959kbps-cenc-audio-key1-1.m4sbin0 -> 921 bytes
-rw-r--r--dom/media/test/bipbop_480_959kbps-cenc-audio-key1-1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480_959kbps-cenc-audio-key1-2.m4sbin0 -> 565 bytes
-rw-r--r--dom/media/test/bipbop_480_959kbps-cenc-audio-key1-2.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480_959kbps-cenc-audio-key1-3.m4sbin0 -> 977 bytes
-rw-r--r--dom/media/test/bipbop_480_959kbps-cenc-audio-key1-3.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480_959kbps-cenc-audio-key1-4.m4sbin0 -> 389 bytes
-rw-r--r--dom/media/test/bipbop_480_959kbps-cenc-audio-key1-4.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480_959kbps-cenc-audio-key1-init.mp4bin0 -> 874 bytes
-rw-r--r--dom/media/test/bipbop_480_959kbps-cenc-audio-key1-init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_480_959kbps-cenc-audio-key2-1.m4sbin0 -> 921 bytes
-rw-r--r--dom/media/test/bipbop_480_959kbps-cenc-audio-key2-1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480_959kbps-cenc-audio-key2-2.m4sbin0 -> 565 bytes
-rw-r--r--dom/media/test/bipbop_480_959kbps-cenc-audio-key2-2.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480_959kbps-cenc-audio-key2-3.m4sbin0 -> 977 bytes
-rw-r--r--dom/media/test/bipbop_480_959kbps-cenc-audio-key2-3.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480_959kbps-cenc-audio-key2-4.m4sbin0 -> 389 bytes
-rw-r--r--dom/media/test/bipbop_480_959kbps-cenc-audio-key2-4.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480_959kbps-cenc-audio-key2-init.mp4bin0 -> 874 bytes
-rw-r--r--dom/media/test/bipbop_480_959kbps-cenc-audio-key2-init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_480_959kbps-cenc-video-key1-1.m4sbin0 -> 101203 bytes
-rw-r--r--dom/media/test/bipbop_480_959kbps-cenc-video-key1-1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480_959kbps-cenc-video-key1-2.m4sbin0 -> 99366 bytes
-rw-r--r--dom/media/test/bipbop_480_959kbps-cenc-video-key1-2.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480_959kbps-cenc-video-key1-init.mp4bin0 -> 932 bytes
-rw-r--r--dom/media/test/bipbop_480_959kbps-cenc-video-key1-init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_480_959kbps-cenc-video-key2-1.m4sbin0 -> 101203 bytes
-rw-r--r--dom/media/test/bipbop_480_959kbps-cenc-video-key2-1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480_959kbps-cenc-video-key2-2.m4sbin0 -> 99366 bytes
-rw-r--r--dom/media/test/bipbop_480_959kbps-cenc-video-key2-2.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480_959kbps-cenc-video-key2-init.mp4bin0 -> 932 bytes
-rw-r--r--dom/media/test/bipbop_480_959kbps-cenc-video-key2-init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_480_959kbps.mp4bin0 -> 199351 bytes
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-1.m4sbin0 -> 921 bytes
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-2.m4sbin0 -> 565 bytes
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-2.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-3.m4sbin0 -> 977 bytes
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-3.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-4.m4sbin0 -> 389 bytes
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-4.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-init.mp4bin0 -> 1020 bytes
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-1.m4sbin0 -> 921 bytes
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-2.m4sbin0 -> 565 bytes
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-2.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-3.m4sbin0 -> 977 bytes
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-3.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-4.m4sbin0 -> 389 bytes
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-4.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-init.mp4bin0 -> 1020 bytes
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps-cenc-video-key1-1.m4sbin0 -> 101203 bytes
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps-cenc-video-key1-1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps-cenc-video-key1-2.m4sbin0 -> 99366 bytes
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps-cenc-video-key1-2.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps-cenc-video-key1-init.mp4bin0 -> 1094 bytes
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps-cenc-video-key1-init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps-cenc-video-key2-1.m4sbin0 -> 101203 bytes
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps-cenc-video-key2-1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps-cenc-video-key2-2.m4sbin0 -> 99366 bytes
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps-cenc-video-key2-2.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps-cenc-video-key2-init.mp4bin0 -> 1094 bytes
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps-cenc-video-key2-init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_480wp_1001kbps.mp4bin0 -> 199911 bytes
-rw-r--r--dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-1.m4sbin0 -> 921 bytes
-rw-r--r--dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-2.m4sbin0 -> 565 bytes
-rw-r--r--dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-2.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-3.m4sbin0 -> 977 bytes
-rw-r--r--dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-3.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-4.m4sbin0 -> 389 bytes
-rw-r--r--dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-4.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-init.mp4bin0 -> 1020 bytes
-rw-r--r--dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-1.m4sbin0 -> 921 bytes
-rw-r--r--dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-2.m4sbin0 -> 565 bytes
-rw-r--r--dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-2.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-3.m4sbin0 -> 977 bytes
-rw-r--r--dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-3.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-4.m4sbin0 -> 389 bytes
-rw-r--r--dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-4.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-init.mp4bin0 -> 1020 bytes
-rw-r--r--dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_480wp_663kbps-cenc-video-key1-1.m4sbin0 -> 68025 bytes
-rw-r--r--dom/media/test/bipbop_480wp_663kbps-cenc-video-key1-1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480wp_663kbps-cenc-video-key1-2.m4sbin0 -> 66457 bytes
-rw-r--r--dom/media/test/bipbop_480wp_663kbps-cenc-video-key1-2.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480wp_663kbps-cenc-video-key1-init.mp4bin0 -> 1094 bytes
-rw-r--r--dom/media/test/bipbop_480wp_663kbps-cenc-video-key1-init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_480wp_663kbps-cenc-video-key2-1.m4sbin0 -> 68025 bytes
-rw-r--r--dom/media/test/bipbop_480wp_663kbps-cenc-video-key2-1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480wp_663kbps-cenc-video-key2-2.m4sbin0 -> 66457 bytes
-rw-r--r--dom/media/test/bipbop_480wp_663kbps-cenc-video-key2-2.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_480wp_663kbps-cenc-video-key2-init.mp4bin0 -> 1094 bytes
-rw-r--r--dom/media/test/bipbop_480wp_663kbps-cenc-video-key2-init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_480wp_663kbps.mp4bin0 -> 133824 bytes
-rw-r--r--dom/media/test/bipbop_audio_aac_22.05k.mp4bin0 -> 2424 bytes
-rw-r--r--dom/media/test/bipbop_audio_aac_22.05k.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_audio_aac_44.1k.mp4bin0 -> 3239 bytes
-rw-r--r--dom/media/test/bipbop_audio_aac_44.1k.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_audio_aac_48k.mp4bin0 -> 3286 bytes
-rw-r--r--dom/media/test/bipbop_audio_aac_48k.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_audio_aac_88.2k.mp4bin0 -> 3769 bytes
-rw-r--r--dom/media/test/bipbop_audio_aac_88.2k.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_audio_aac_8k.mp4bin0 -> 1707 bytes
-rw-r--r--dom/media/test/bipbop_audio_aac_8k.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_audio_aac_96k.mp4bin0 -> 4010 bytes
-rw-r--r--dom/media/test/bipbop_audio_aac_96k.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_cbcs_10_0_audio_1.m4sbin0 -> 1364 bytes
-rw-r--r--dom/media/test/bipbop_cbcs_10_0_audio_1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_cbcs_10_0_audio_init.mp4bin0 -> 936 bytes
-rw-r--r--dom/media/test/bipbop_cbcs_10_0_audio_init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_cbcs_10_0_video_1.m4sbin0 -> 57044 bytes
-rw-r--r--dom/media/test/bipbop_cbcs_10_0_video_1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_cbcs_10_0_video_init.mp4bin0 -> 972 bytes
-rw-r--r--dom/media/test/bipbop_cbcs_10_0_video_init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_cbcs_1_9_audio_1.m4sbin0 -> 1364 bytes
-rw-r--r--dom/media/test/bipbop_cbcs_1_9_audio_1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_cbcs_1_9_audio_init.mp4bin0 -> 936 bytes
-rw-r--r--dom/media/test/bipbop_cbcs_1_9_audio_init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_cbcs_1_9_video_1.m4sbin0 -> 57044 bytes
-rw-r--r--dom/media/test/bipbop_cbcs_1_9_video_1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_cbcs_1_9_video_init.mp4bin0 -> 972 bytes
-rw-r--r--dom/media/test/bipbop_cbcs_1_9_video_init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_cbcs_5_5_audio_1.m4sbin0 -> 1364 bytes
-rw-r--r--dom/media/test/bipbop_cbcs_5_5_audio_1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_cbcs_5_5_audio_init.mp4bin0 -> 936 bytes
-rw-r--r--dom/media/test/bipbop_cbcs_5_5_audio_init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_cbcs_5_5_video_1.m4sbin0 -> 57044 bytes
-rw-r--r--dom/media/test/bipbop_cbcs_5_5_video_1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_cbcs_5_5_video_init.mp4bin0 -> 972 bytes
-rw-r--r--dom/media/test/bipbop_cbcs_5_5_video_init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_cbcs_7_7_audio_1.m4sbin0 -> 1364 bytes
-rw-r--r--dom/media/test/bipbop_cbcs_7_7_audio_1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_cbcs_7_7_audio_init.mp4bin0 -> 936 bytes
-rw-r--r--dom/media/test/bipbop_cbcs_7_7_audio_init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_cbcs_7_7_video_1.m4sbin0 -> 57044 bytes
-rw-r--r--dom/media/test/bipbop_cbcs_7_7_video_1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_cbcs_7_7_video_init.mp4bin0 -> 972 bytes
-rw-r--r--dom/media/test/bipbop_cbcs_7_7_video_init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_cbcs_9_8_audio_1.m4sbin0 -> 1364 bytes
-rw-r--r--dom/media/test/bipbop_cbcs_9_8_audio_1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_cbcs_9_8_audio_init.mp4bin0 -> 936 bytes
-rw-r--r--dom/media/test/bipbop_cbcs_9_8_audio_init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_cbcs_9_8_video_1.m4sbin0 -> 57044 bytes
-rw-r--r--dom/media/test/bipbop_cbcs_9_8_video_1.m4s^headers^1
-rw-r--r--dom/media/test/bipbop_cbcs_9_8_video_init.mp4bin0 -> 972 bytes
-rw-r--r--dom/media/test/bipbop_cbcs_9_8_video_init.mp4^headers^1
-rw-r--r--dom/media/test/bipbop_short_pixel_metadata_bigger_than_in_stream_vp8.webmbin0 -> 48942 bytes
-rw-r--r--dom/media/test/bipbop_short_pixel_metadata_bigger_than_in_stream_vp8.webm^headers^1
-rw-r--r--dom/media/test/bipbop_short_pixel_metadata_narrower_than_in_stream_vp8.webmbin0 -> 48942 bytes
-rw-r--r--dom/media/test/bipbop_short_pixel_metadata_narrower_than_in_stream_vp8.webm^headers^1
-rw-r--r--dom/media/test/bipbop_short_pixel_metadata_smaller_than_in_stream_vp8.webmbin0 -> 48942 bytes
-rw-r--r--dom/media/test/bipbop_short_pixel_metadata_smaller_than_in_stream_vp8.webm^headers^1
-rw-r--r--dom/media/test/bipbop_short_vp8.webmbin0 -> 48942 bytes
-rw-r--r--dom/media/test/bipbop_short_vp8.webm^headers^1
-rw-r--r--dom/media/test/black100x100-aspect3to2.ogvbin0 -> 3428 bytes
-rw-r--r--dom/media/test/black100x100-aspect3to2.ogv^headers^1
-rw-r--r--dom/media/test/bogus.duh45
-rw-r--r--dom/media/test/bogus.ogv45
-rw-r--r--dom/media/test/bogus.ogv^headers^1
-rw-r--r--dom/media/test/bogus.wav45
-rw-r--r--dom/media/test/bogus.wav^headers^1
-rw-r--r--dom/media/test/browser/browser.ini28
-rw-r--r--dom/media/test/browser/browser_encrypted_play_time_telemetry.js266
-rw-r--r--dom/media/test/browser/browser_tab_visibility_and_play_time.js216
-rw-r--r--dom/media/test/browser/browser_telemetry_video_hardware_decoding_support.js106
-rw-r--r--dom/media/test/browser/file_empty_page.html8
-rw-r--r--dom/media/test/browser/file_media.html10
-rw-r--r--dom/media/test/browser/wmfme/browser.ini11
-rw-r--r--dom/media/test/browser/wmfme/browser_wmfme_crash.js52
-rw-r--r--dom/media/test/browser/wmfme/browser_wmfme_max_crashes.js69
-rw-r--r--dom/media/test/browser/wmfme/file_video.html9
-rw-r--r--dom/media/test/browser/wmfme/head.js200
-rw-r--r--dom/media/test/bug1066943.webmbin0 -> 18442 bytes
-rw-r--r--dom/media/test/bug1066943.webm^headers^1
-rw-r--r--dom/media/test/bug1301226-odd.wavbin0 -> 244 bytes
-rw-r--r--dom/media/test/bug1301226-odd.wav^headers^1
-rw-r--r--dom/media/test/bug1301226.wavbin0 -> 240 bytes
-rw-r--r--dom/media/test/bug1301226.wav^headers^1
-rw-r--r--dom/media/test/bug1377278.webmbin0 -> 215594 bytes
-rw-r--r--dom/media/test/bug1377278.webm^headers^1
-rw-r--r--dom/media/test/bug1535980.webmbin0 -> 81467 bytes
-rw-r--r--dom/media/test/bug1535980.webm^headers^1
-rw-r--r--dom/media/test/bug1799787.webmbin0 -> 1053 bytes
-rw-r--r--dom/media/test/bug1799787.webm^headers^1
-rw-r--r--dom/media/test/bug461281.oggbin0 -> 16521 bytes
-rw-r--r--dom/media/test/bug461281.ogg^headers^1
-rw-r--r--dom/media/test/bug482461-theora.ogvbin0 -> 280904 bytes
-rw-r--r--dom/media/test/bug482461-theora.ogv^headers^1
-rw-r--r--dom/media/test/bug482461.ogvbin0 -> 305785 bytes
-rw-r--r--dom/media/test/bug482461.ogv^headers^1
-rw-r--r--dom/media/test/bug495129.ogvbin0 -> 122207 bytes
-rw-r--r--dom/media/test/bug495129.ogv^headers^1
-rw-r--r--dom/media/test/bug495794.oggbin0 -> 4837 bytes
-rw-r--r--dom/media/test/bug495794.ogg^headers^1
-rw-r--r--dom/media/test/bug498380.ogvbin0 -> 65535 bytes
-rw-r--r--dom/media/test/bug498380.ogv^headers^1
-rw-r--r--dom/media/test/bug498855-1.ogvbin0 -> 20480 bytes
-rw-r--r--dom/media/test/bug498855-1.ogv^headers^1
-rw-r--r--dom/media/test/bug498855-2.ogvbin0 -> 20480 bytes
-rw-r--r--dom/media/test/bug498855-2.ogv^headers^1
-rw-r--r--dom/media/test/bug498855-3.ogvbin0 -> 20480 bytes
-rw-r--r--dom/media/test/bug498855-3.ogv^headers^1
-rw-r--r--dom/media/test/bug499519.ogvbin0 -> 20480 bytes
-rw-r--r--dom/media/test/bug499519.ogv^headers^1
-rw-r--r--dom/media/test/bug500311.ogvbin0 -> 55834 bytes
-rw-r--r--dom/media/test/bug500311.ogv^headers^1
-rw-r--r--dom/media/test/bug501279.oggbin0 -> 2361 bytes
-rw-r--r--dom/media/test/bug501279.ogg^headers^1
-rw-r--r--dom/media/test/bug504613.ogvbin0 -> 35000 bytes
-rw-r--r--dom/media/test/bug504613.ogv^headers^1
-rw-r--r--dom/media/test/bug504644.ogvbin0 -> 131114 bytes
-rw-r--r--dom/media/test/bug504644.ogv^headers^1
-rw-r--r--dom/media/test/bug504843.ogvbin0 -> 65536 bytes
-rw-r--r--dom/media/test/bug504843.ogv^headers^1
-rw-r--r--dom/media/test/bug506094.ogvbin0 -> 8195 bytes
-rw-r--r--dom/media/test/bug506094.ogv^headers^1
-rw-r--r--dom/media/test/bug516323.indexed.ogvbin0 -> 162193 bytes
-rw-r--r--dom/media/test/bug516323.indexed.ogv^headers^1
-rw-r--r--dom/media/test/bug516323.ogvbin0 -> 161789 bytes
-rw-r--r--dom/media/test/bug516323.ogv^headers^1
-rw-r--r--dom/media/test/bug520493.oggbin0 -> 3901 bytes
-rw-r--r--dom/media/test/bug520493.ogg^headers^1
-rw-r--r--dom/media/test/bug520500.oggbin0 -> 21978 bytes
-rw-r--r--dom/media/test/bug520500.ogg^headers^1
-rw-r--r--dom/media/test/bug520908.ogvbin0 -> 28942 bytes
-rw-r--r--dom/media/test/bug520908.ogv^headers^1
-rw-r--r--dom/media/test/bug523816.ogvbin0 -> 40585 bytes
-rw-r--r--dom/media/test/bug523816.ogv^headers^1
-rw-r--r--dom/media/test/bug533822.oggbin0 -> 35010 bytes
-rw-r--r--dom/media/test/bug533822.ogg^headers^1
-rw-r--r--dom/media/test/bug556821.ogvbin0 -> 196608 bytes
-rw-r--r--dom/media/test/bug556821.ogv^headers^1
-rw-r--r--dom/media/test/bug557094.ogvbin0 -> 76966 bytes
-rw-r--r--dom/media/test/bug557094.ogv^headers^1
-rw-r--r--dom/media/test/bug603918.webmbin0 -> 103227 bytes
-rw-r--r--dom/media/test/bug603918.webm^headers^1
-rw-r--r--dom/media/test/bug604067.webmbin0 -> 103227 bytes
-rw-r--r--dom/media/test/bug604067.webm^headers^1
-rw-r--r--dom/media/test/bunny.webmbin0 -> 195455 bytes
-rw-r--r--dom/media/test/bunny_hd_5s.mp4bin0 -> 845651 bytes
-rw-r--r--dom/media/test/can_play_type_dash.js27
-rw-r--r--dom/media/test/can_play_type_ogg.js72
-rw-r--r--dom/media/test/can_play_type_wave.js30
-rw-r--r--dom/media/test/can_play_type_webm.js39
-rw-r--r--dom/media/test/cancellable_request.sjs162
-rw-r--r--dom/media/test/chain.oggbin0 -> 63610 bytes
-rw-r--r--dom/media/test/chain.ogg^headers^1
-rw-r--r--dom/media/test/chain.ogvbin0 -> 45463 bytes
-rw-r--r--dom/media/test/chain.ogv^headers^1
-rw-r--r--dom/media/test/chain.opusbin0 -> 50101 bytes
-rw-r--r--dom/media/test/chain.opus^headers^1
-rw-r--r--dom/media/test/chained-audio-video.oggbin0 -> 92552 bytes
-rw-r--r--dom/media/test/chained-audio-video.ogg^headers^1
-rw-r--r--dom/media/test/chained-video.ogvbin0 -> 57906 bytes
-rw-r--r--dom/media/test/chained-video.ogv^headers^1
-rw-r--r--dom/media/test/chrome/chrome.ini10
-rw-r--r--dom/media/test/chrome/test_accumulated_play_time.html692
-rw-r--r--dom/media/test/chrome/test_telemetry_source_buffer_type.html75
-rw-r--r--dom/media/test/chromeHelper.js23
-rw-r--r--dom/media/test/cloneElementVisually_helpers.js232
-rw-r--r--dom/media/test/contentType.sjs77
-rw-r--r--dom/media/test/crashtests/0-timescale.html14
-rw-r--r--dom/media/test/crashtests/0-timescale.mp4bin0 -> 14718 bytes
-rw-r--r--dom/media/test/crashtests/1012609.html9
-rw-r--r--dom/media/test/crashtests/1015662.html4
-rw-r--r--dom/media/test/crashtests/1028458.html23
-rw-r--r--dom/media/test/crashtests/1041466.html21
-rw-r--r--dom/media/test/crashtests/1045650.html18
-rw-r--r--dom/media/test/crashtests/1080986.html3
-rw-r--r--dom/media/test/crashtests/1080986.wavbin0 -> 592 bytes
-rw-r--r--dom/media/test/crashtests/1122218.html24
-rw-r--r--dom/media/test/crashtests/1127188.html3
-rw-r--r--dom/media/test/crashtests/1157994.html21
-rw-r--r--dom/media/test/crashtests/1158427.html21
-rw-r--r--dom/media/test/crashtests/1180881.html8
-rw-r--r--dom/media/test/crashtests/1180881.webmbin0 -> 524 bytes
-rw-r--r--dom/media/test/crashtests/1185176.html24
-rw-r--r--dom/media/test/crashtests/1185191.html21
-rw-r--r--dom/media/test/crashtests/1185192.html18
-rw-r--r--dom/media/test/crashtests/1197935.html8
-rw-r--r--dom/media/test/crashtests/1197935.mp4bin0 -> 1806042 bytes
-rw-r--r--dom/media/test/crashtests/1223670.html23
-rw-r--r--dom/media/test/crashtests/1236639.html9
-rw-r--r--dom/media/test/crashtests/1236639.mp3bin0 -> 1080 bytes
-rw-r--r--dom/media/test/crashtests/1257700.html8
-rw-r--r--dom/media/test/crashtests/1257700.webmbin0 -> 59264 bytes
-rw-r--r--dom/media/test/crashtests/1267263.html19
-rw-r--r--dom/media/test/crashtests/1270303.html8
-rw-r--r--dom/media/test/crashtests/1270303.webmbin0 -> 5822 bytes
-rw-r--r--dom/media/test/crashtests/1291702.html72
-rw-r--r--dom/media/test/crashtests/1368490.html30
-rw-r--r--dom/media/test/crashtests/1378826.html46
-rw-r--r--dom/media/test/crashtests/1384248.html10
-rw-r--r--dom/media/test/crashtests/1388372.html13
-rw-r--r--dom/media/test/crashtests/1389304.html32
-rw-r--r--dom/media/test/crashtests/1389304.mp4bin0 -> 198320 bytes
-rw-r--r--dom/media/test/crashtests/1393272.webmbin0 -> 6781 bytes
-rw-r--r--dom/media/test/crashtests/1411322.html18
-rw-r--r--dom/media/test/crashtests/1450845.html34
-rw-r--r--dom/media/test/crashtests/1489160.html10
-rw-r--r--dom/media/test/crashtests/1494073.html19
-rw-r--r--dom/media/test/crashtests/1517199.html17
-rw-r--r--dom/media/test/crashtests/1526044.html19
-rw-r--r--dom/media/test/crashtests/1530897.webmbin0 -> 509 bytes
-rw-r--r--dom/media/test/crashtests/1538727.html14
-rw-r--r--dom/media/test/crashtests/1545133.html34
-rw-r--r--dom/media/test/crashtests/1547784.html33
-rw-r--r--dom/media/test/crashtests/1547899.html20
-rw-r--r--dom/media/test/crashtests/1560215.html20
-rw-r--r--dom/media/test/crashtests/1569645.html23
-rw-r--r--dom/media/test/crashtests/1575271.html25
-rw-r--r--dom/media/test/crashtests/1577184.html15
-rw-r--r--dom/media/test/crashtests/1587248.html23
-rw-r--r--dom/media/test/crashtests/1594466.html22
-rw-r--r--dom/media/test/crashtests/1601385.html12
-rw-r--r--dom/media/test/crashtests/1601422.html20
-rw-r--r--dom/media/test/crashtests/1604941.html22
-rw-r--r--dom/media/test/crashtests/1608286.html50
-rw-r--r--dom/media/test/crashtests/1673525.html15
-rw-r--r--dom/media/test/crashtests/1673526-1.html20
-rw-r--r--dom/media/test/crashtests/1673526-2.html20
-rw-r--r--dom/media/test/crashtests/1693043.html21
-rw-r--r--dom/media/test/crashtests/1696511.html22
-rw-r--r--dom/media/test/crashtests/1697521.html19
-rw-r--r--dom/media/test/crashtests/1708790.html22
-rw-r--r--dom/media/test/crashtests/1709130.html19
-rw-r--r--dom/media/test/crashtests/1734008.html22
-rw-r--r--dom/media/test/crashtests/1734008.webmbin0 -> 8253 bytes
-rw-r--r--dom/media/test/crashtests/1741677.html15
-rw-r--r--dom/media/test/crashtests/1748272.html12
-rw-r--r--dom/media/test/crashtests/1752917.html18
-rw-r--r--dom/media/test/crashtests/1762620.html8
-rw-r--r--dom/media/test/crashtests/1765842.html8
-rw-r--r--dom/media/test/crashtests/1765842.webmbin0 -> 17210 bytes
-rw-r--r--dom/media/test/crashtests/1787281.html13
-rw-r--r--dom/media/test/crashtests/1787281.mp4bin0 -> 2736 bytes
-rw-r--r--dom/media/test/crashtests/1798778.html11
-rw-r--r--dom/media/test/crashtests/1833894.mp4bin0 -> 1000465 bytes
-rw-r--r--dom/media/test/crashtests/1833896.mp4bin0 -> 38215 bytes
-rw-r--r--dom/media/test/crashtests/1835164.html13
-rw-r--r--dom/media/test/crashtests/1835164.opusbin0 -> 2250 bytes
-rw-r--r--dom/media/test/crashtests/1840002.webmbin0 -> 512 bytes
-rw-r--r--dom/media/test/crashtests/1845350.mp4bin0 -> 1045 bytes
-rw-r--r--dom/media/test/crashtests/255ch.wavbin0 -> 68318 bytes
-rw-r--r--dom/media/test/crashtests/459439-1.html36
-rw-r--r--dom/media/test/crashtests/466607-1.html14
-rw-r--r--dom/media/test/crashtests/466945-1.html25
-rw-r--r--dom/media/test/crashtests/468763-1.html1
-rw-r--r--dom/media/test/crashtests/474744-1.html15
-rw-r--r--dom/media/test/crashtests/481136-1.html3
-rw-r--r--dom/media/test/crashtests/492286-1.xhtml1
-rw-r--r--dom/media/test/crashtests/493915-1.html18
-rw-r--r--dom/media/test/crashtests/495794-1.html8
-rw-r--r--dom/media/test/crashtests/495794-1.oggbin0 -> 4837 bytes
-rw-r--r--dom/media/test/crashtests/497734-1.xhtml21
-rw-r--r--dom/media/test/crashtests/497734-2.html17
-rw-r--r--dom/media/test/crashtests/576612-1.html15
-rw-r--r--dom/media/test/crashtests/691096-1.html31
-rw-r--r--dom/media/test/crashtests/752784-1.html15
-rw-r--r--dom/media/test/crashtests/789075-1.html20
-rw-r--r--dom/media/test/crashtests/789075.webmbin0 -> 12294 bytes
-rw-r--r--dom/media/test/crashtests/795892-1.html23
-rw-r--r--dom/media/test/crashtests/844563.html5
-rw-r--r--dom/media/test/crashtests/846612.html8
-rw-r--r--dom/media/test/crashtests/852838.html11
-rw-r--r--dom/media/test/crashtests/865004.html19
-rw-r--r--dom/media/test/crashtests/865537-1.html13
-rw-r--r--dom/media/test/crashtests/865550.html22
-rw-r--r--dom/media/test/crashtests/868504.html14
-rw-r--r--dom/media/test/crashtests/874869.html15
-rw-r--r--dom/media/test/crashtests/874915.html24
-rw-r--r--dom/media/test/crashtests/874934.html23
-rw-r--r--dom/media/test/crashtests/874952.html11
-rw-r--r--dom/media/test/crashtests/875144.html81
-rw-r--r--dom/media/test/crashtests/875596.html12
-rw-r--r--dom/media/test/crashtests/875911.html3
-rw-r--r--dom/media/test/crashtests/876024-1.html5
-rw-r--r--dom/media/test/crashtests/876024-2.html17
-rw-r--r--dom/media/test/crashtests/876118.html16
-rw-r--r--dom/media/test/crashtests/876207.html30
-rw-r--r--dom/media/test/crashtests/876215.html14
-rw-r--r--dom/media/test/crashtests/876249.html27
-rw-r--r--dom/media/test/crashtests/876252.html23
-rw-r--r--dom/media/test/crashtests/876834.html4
-rw-r--r--dom/media/test/crashtests/877527.html37
-rw-r--r--dom/media/test/crashtests/877820.html4
-rw-r--r--dom/media/test/crashtests/878014.html31
-rw-r--r--dom/media/test/crashtests/878328.html5
-rw-r--r--dom/media/test/crashtests/878407.html11
-rw-r--r--dom/media/test/crashtests/878478.html30
-rw-r--r--dom/media/test/crashtests/880129.html9
-rw-r--r--dom/media/test/crashtests/880202.html33
-rw-r--r--dom/media/test/crashtests/880342-1.html208
-rw-r--r--dom/media/test/crashtests/880342-2.html8
-rw-r--r--dom/media/test/crashtests/880384.html8
-rw-r--r--dom/media/test/crashtests/880404.html6
-rw-r--r--dom/media/test/crashtests/880724.html13
-rw-r--r--dom/media/test/crashtests/881775.html25
-rw-r--r--dom/media/test/crashtests/882956.html15
-rw-r--r--dom/media/test/crashtests/884459.html12
-rw-r--r--dom/media/test/crashtests/889042.html4
-rw-r--r--dom/media/test/crashtests/907986-1.html17
-rw-r--r--dom/media/test/crashtests/907986-2.html17
-rw-r--r--dom/media/test/crashtests/907986-3.html17
-rw-r--r--dom/media/test/crashtests/907986-4.html15
-rw-r--r--dom/media/test/crashtests/910171-1.html17
-rw-r--r--dom/media/test/crashtests/920987.html6
-rw-r--r--dom/media/test/crashtests/925619-1.html14
-rw-r--r--dom/media/test/crashtests/925619-2.html15
-rw-r--r--dom/media/test/crashtests/926619.html24
-rw-r--r--dom/media/test/crashtests/933151.html16
-rw-r--r--dom/media/test/crashtests/933156.html23
-rw-r--r--dom/media/test/crashtests/944851.html17
-rw-r--r--dom/media/test/crashtests/952756.html19
-rw-r--r--dom/media/test/crashtests/986901.html16
-rw-r--r--dom/media/test/crashtests/990794.html22
-rw-r--r--dom/media/test/crashtests/995289.html9
-rw-r--r--dom/media/test/crashtests/adts-truncated.aacbin0 -> 512 bytes
-rw-r--r--dom/media/test/crashtests/adts.aacbin0 -> 8537 bytes
-rw-r--r--dom/media/test/crashtests/analyser-channels-1.html16
-rw-r--r--dom/media/test/crashtests/audiocontext-after-unload-1.html27
-rw-r--r--dom/media/test/crashtests/audiocontext-after-xhr.html13
-rw-r--r--dom/media/test/crashtests/audiocontext-double-suspend.html5
-rw-r--r--dom/media/test/crashtests/audioworkletnode-after-unload-1.html27
-rw-r--r--dom/media/test/crashtests/buffer-source-duration-1.html14
-rw-r--r--dom/media/test/crashtests/buffer-source-ended-1.html16
-rw-r--r--dom/media/test/crashtests/buffer-source-resampling-start-1.html16
-rw-r--r--dom/media/test/crashtests/buffer-source-slow-resampling-1.html34
-rw-r--r--dom/media/test/crashtests/channel-count-in-metadata-different-than-in-content.mp4bin0 -> 13651 bytes
-rw-r--r--dom/media/test/crashtests/convolver-memory-report-1.html25
-rw-r--r--dom/media/test/crashtests/copyFromChannel-2.html16
-rw-r--r--dom/media/test/crashtests/cors.webmbin0 -> 215529 bytes
-rw-r--r--dom/media/test/crashtests/cors.webm^headers^1
-rw-r--r--dom/media/test/crashtests/crashtests.list169
-rw-r--r--dom/media/test/crashtests/disconnect-wrong-destination.html13
-rw-r--r--dom/media/test/crashtests/doppler-1.html23
-rw-r--r--dom/media/test/crashtests/empty-buffer-source.html14
-rw-r--r--dom/media/test/crashtests/empty-samples.webm0
-rw-r--r--dom/media/test/crashtests/encrypted-track-with-bad-sample-description-index.mp4bin0 -> 198320 bytes
-rw-r--r--dom/media/test/crashtests/encrypted-track-with-sample-missing-cenc-aux.mp4bin0 -> 152132 bytes
-rw-r--r--dom/media/test/crashtests/encrypted-track-without-tenc.mp4bin0 -> 152132 bytes
-rw-r--r--dom/media/test/crashtests/media-element-source-seek-1.html27
-rw-r--r--dom/media/test/crashtests/mp4_box_emptyrange.mp4bin0 -> 918 bytes
-rw-r--r--dom/media/test/crashtests/offline-buffer-source-ended-1.html15
-rw-r--r--dom/media/test/crashtests/oscillator-ended-1.html15
-rw-r--r--dom/media/test/crashtests/oscillator-ended-2.html15
-rw-r--r--dom/media/test/crashtests/sound.oggbin0 -> 2603 bytes
-rw-r--r--dom/media/test/crashtests/test.mp4bin0 -> 11817 bytes
-rw-r--r--dom/media/test/crashtests/track-with-zero-dimensions.mp4bin0 -> 11817 bytes
-rw-r--r--dom/media/test/crashtests/video-crash.webmbin0 -> 58482 bytes
-rw-r--r--dom/media/test/crashtests/video-replay-after-audio-end.html43
-rw-r--r--dom/media/test/dash/dash-manifest-garbled-webm.mpd35
-rw-r--r--dom/media/test/dash/dash-manifest-garbled.mpd1
-rw-r--r--dom/media/test/dash/dash-manifest-sjs.mpd35
-rw-r--r--dom/media/test/dash/dash-manifest.mpd35
-rw-r--r--dom/media/test/dash/dash-webm-audio-128k.webmbin0 -> 41946 bytes
-rw-r--r--dom/media/test/dash/dash-webm-video-320x180.webmbin0 -> 35123 bytes
-rw-r--r--dom/media/test/dash/dash-webm-video-428x240.webmbin0 -> 50206 bytes
-rw-r--r--dom/media/test/dash/garbled.webm1
-rw-r--r--dom/media/test/dash_detect_stream_switch.sjs143
-rw-r--r--dom/media/test/detodos-recorder-test.opusbin0 -> 1507 bytes
-rw-r--r--dom/media/test/detodos-recorder-test.opus^headers^1
-rw-r--r--dom/media/test/detodos-short.opusbin0 -> 648 bytes
-rw-r--r--dom/media/test/detodos-short.opus^headers^1
-rw-r--r--dom/media/test/detodos-short.webmbin0 -> 1085 bytes
-rw-r--r--dom/media/test/detodos-short.webm^headers^1
-rw-r--r--dom/media/test/detodos.opusbin0 -> 6270 bytes
-rw-r--r--dom/media/test/detodos.opus^headers^1
-rw-r--r--dom/media/test/detodos.webmbin0 -> 11701 bytes
-rw-r--r--dom/media/test/detodos.webm^headers^1
-rw-r--r--dom/media/test/dirac.oggbin0 -> 106338 bytes
-rw-r--r--dom/media/test/dirac.ogg^headers^1
-rw-r--r--dom/media/test/dynamic_resource.sjs53
-rw-r--r--dom/media/test/eme.js479
-rw-r--r--dom/media/test/eme_standalone.js286
-rw-r--r--dom/media/test/empty_size.mp3bin0 -> 90368 bytes
-rw-r--r--dom/media/test/file_access_controls.html160
-rw-r--r--dom/media/test/file_eme_createMediaKeys.html47
-rw-r--r--dom/media/test/file_playback_and_bfcache.html57
-rw-r--r--dom/media/test/flac-noheader-s16.flacbin0 -> 242826 bytes
-rw-r--r--dom/media/test/flac-noheader-s16.flac^headers^1
-rw-r--r--dom/media/test/flac-s24.flacbin0 -> 980951 bytes
-rw-r--r--dom/media/test/flac-s24.flac^headers^1
-rw-r--r--dom/media/test/flac-sample-cenc.mp4bin0 -> 336823 bytes
-rw-r--r--dom/media/test/flac-sample-cenc.mp4^headers^1
-rw-r--r--dom/media/test/flac-sample.mp4bin0 -> 876556 bytes
-rw-r--r--dom/media/test/flac-sample.mp4^headers^1
-rw-r--r--dom/media/test/fragment_noplay.js19
-rw-r--r--dom/media/test/fragment_play.js92
-rw-r--r--dom/media/test/gUM_support.js103
-rw-r--r--dom/media/test/gizmo-frag.mp4bin0 -> 152132 bytes
-rw-r--r--dom/media/test/gizmo-noaudio.mp4bin0 -> 342980 bytes
-rw-r--r--dom/media/test/gizmo-noaudio.mp4^headers^1
-rw-r--r--dom/media/test/gizmo-noaudio.webmbin0 -> 112663 bytes
-rw-r--r--dom/media/test/gizmo-noaudio.webm^headers^1
-rw-r--r--dom/media/test/gizmo-short.mp4bin0 -> 29905 bytes
-rw-r--r--dom/media/test/gizmo-short.mp4^headers^1
-rw-r--r--dom/media/test/gizmo.mp4bin0 -> 455255 bytes
-rw-r--r--dom/media/test/gizmo.mp4^headers^1
-rw-r--r--dom/media/test/gizmo.webmbin0 -> 159035 bytes
-rw-r--r--dom/media/test/gizmo.webm^headers^1
-rw-r--r--dom/media/test/gzipped_mp4.sjs25
-rw-r--r--dom/media/test/hls/400x300_prog_index.m3u810
-rw-r--r--dom/media/test/hls/400x300_prog_index_5s.m3u88
-rw-r--r--dom/media/test/hls/400x300_seg0.tsbin0 -> 291588 bytes
-rw-r--r--dom/media/test/hls/400x300_seg0_5s.tsbin0 -> 168636 bytes
-rw-r--r--dom/media/test/hls/400x300_seg1.tsbin0 -> 288204 bytes
-rw-r--r--dom/media/test/hls/416x243_prog_index_5s.m3u88
-rw-r--r--dom/media/test/hls/416x243_seg0_5s.tsbin0 -> 197400 bytes
-rw-r--r--dom/media/test/hls/640x480_prog_index.m3u810
-rw-r--r--dom/media/test/hls/640x480_seg0.tsbin0 -> 814228 bytes
-rw-r--r--dom/media/test/hls/640x480_seg1.tsbin0 -> 796368 bytes
-rw-r--r--dom/media/test/hls/960x720_prog_index.m3u810
-rw-r--r--dom/media/test/hls/960x720_seg0.tsbin0 -> 1878120 bytes
-rw-r--r--dom/media/test/hls/960x720_seg1.tsbin0 -> 1839392 bytes
-rw-r--r--dom/media/test/hls/bipbop_16x9_single.m3u85
-rw-r--r--dom/media/test/hls/bipbop_4x3_single.m3u84
-rw-r--r--dom/media/test/hls/bipbop_4x3_variant.m3u810
-rw-r--r--dom/media/test/huge-id3.mp3bin0 -> 141774 bytes
-rw-r--r--dom/media/test/huge-id3.mp3^headers^1
-rw-r--r--dom/media/test/id3tags.mp3bin0 -> 3530 bytes
-rw-r--r--dom/media/test/id3tags.mp3^headers^1
-rw-r--r--dom/media/test/id3v1afterlongid3v2.mp3bin0 -> 10229 bytes
-rw-r--r--dom/media/test/invalid-cmap-s0c0.opusbin0 -> 6835 bytes
-rw-r--r--dom/media/test/invalid-cmap-s0c0.opus^headers^1
-rw-r--r--dom/media/test/invalid-cmap-s0c2.opusbin0 -> 6834 bytes
-rw-r--r--dom/media/test/invalid-cmap-s0c2.opus^headers^1
-rw-r--r--dom/media/test/invalid-cmap-s1c2.opusbin0 -> 6848 bytes
-rw-r--r--dom/media/test/invalid-cmap-s1c2.opus^headers^1
-rw-r--r--dom/media/test/invalid-cmap-short.opusbin0 -> 6854 bytes
-rw-r--r--dom/media/test/invalid-cmap-short.opus^headers^1
-rw-r--r--dom/media/test/invalid-discard_on_multi_blocks.webmbin0 -> 19636 bytes
-rw-r--r--dom/media/test/invalid-discard_on_multi_blocks.webm^headers^1
-rw-r--r--dom/media/test/invalid-excess_discard.webmbin0 -> 18442 bytes
-rw-r--r--dom/media/test/invalid-excess_discard.webm^headers^1
-rw-r--r--dom/media/test/invalid-excess_neg_discard.webmbin0 -> 18442 bytes
-rw-r--r--dom/media/test/invalid-excess_neg_discard.webm^headers^1
-rw-r--r--dom/media/test/invalid-m0c0.opusbin0 -> 2471 bytes
-rw-r--r--dom/media/test/invalid-m0c0.opus^headers^1
-rw-r--r--dom/media/test/invalid-m0c3.opusbin0 -> 2471 bytes
-rw-r--r--dom/media/test/invalid-m0c3.opus^headers^1
-rw-r--r--dom/media/test/invalid-m1c0.opusbin0 -> 6836 bytes
-rw-r--r--dom/media/test/invalid-m1c0.opus^headers^1
-rw-r--r--dom/media/test/invalid-m1c9.opusbin0 -> 6836 bytes
-rw-r--r--dom/media/test/invalid-m1c9.opus^headers^1
-rw-r--r--dom/media/test/invalid-m2c0.opusbin0 -> 2471 bytes
-rw-r--r--dom/media/test/invalid-m2c0.opus^headers^1
-rw-r--r--dom/media/test/invalid-m2c1.opusbin0 -> 2455 bytes
-rw-r--r--dom/media/test/invalid-m2c1.opus^headers^1
-rw-r--r--dom/media/test/invalid-neg_discard.webmbin0 -> 18442 bytes
-rw-r--r--dom/media/test/invalid-neg_discard.webm^headers^1
-rw-r--r--dom/media/test/invalid-preskip.webmbin0 -> 7251 bytes
-rw-r--r--dom/media/test/invalid-preskip.webm^headers^1
-rw-r--r--dom/media/test/make-headers.sh18
-rw-r--r--dom/media/test/manifest.js2548
-rw-r--r--dom/media/test/midflight-redirect.sjs87
-rw-r--r--dom/media/test/mochitest.ini885
-rw-r--r--dom/media/test/mochitest_background_video.ini787
-rw-r--r--dom/media/test/mochitest_bugs.ini795
-rw-r--r--dom/media/test/mochitest_compat.ini910
-rw-r--r--dom/media/test/mochitest_eme.ini874
-rw-r--r--dom/media/test/mochitest_media_recorder.ini826
-rw-r--r--dom/media/test/mochitest_seek.ini805
-rw-r--r--dom/media/test/mochitest_stream.ini784
-rw-r--r--dom/media/test/multi_id3v2.mp3bin0 -> 5039737 bytes
-rw-r--r--dom/media/test/multiple-bos-more-header-fileds.oggbin0 -> 27527 bytes
-rw-r--r--dom/media/test/multiple-bos-more-header-fileds.ogg^headers^1
-rw-r--r--dom/media/test/multiple-bos.oggbin0 -> 33045 bytes
-rw-r--r--dom/media/test/multiple-bos.ogg^headers^1
-rw-r--r--dom/media/test/no-container-codec-delay.webmbin0 -> 66250 bytes
-rw-r--r--dom/media/test/no-cues.webmbin0 -> 220609 bytes
-rw-r--r--dom/media/test/no-cues.webm^headers^1
-rw-r--r--dom/media/test/notags.mp3bin0 -> 2506 bytes
-rw-r--r--dom/media/test/notags.mp3^headers^1
-rw-r--r--dom/media/test/opus-mapping2.mp4bin0 -> 308048 bytes
-rw-r--r--dom/media/test/opus-mapping2.mp4^headers^1
-rw-r--r--dom/media/test/opus-mapping2.webmbin0 -> 309387 bytes
-rw-r--r--dom/media/test/opus-mapping2.webm^headers^1
-rw-r--r--dom/media/test/opus-sample-cenc.mp4bin0 -> 21958 bytes
-rw-r--r--dom/media/test/opus-sample-cenc.mp4^headers^1
-rw-r--r--dom/media/test/opus-sample.mp4bin0 -> 105690 bytes
-rw-r--r--dom/media/test/opus-sample.mp4^headers^1
-rw-r--r--dom/media/test/owl-funnier-id3.mp3bin0 -> 69603 bytes
-rw-r--r--dom/media/test/owl-funnier-id3.mp3^headers^1
-rw-r--r--dom/media/test/owl-funny-id3.mp3bin0 -> 71696 bytes
-rw-r--r--dom/media/test/owl-funny-id3.mp3^headers^1
-rw-r--r--dom/media/test/owl-short.mp3bin0 -> 11016 bytes
-rw-r--r--dom/media/test/owl-short.mp3^headers^1
-rw-r--r--dom/media/test/owl.mp3bin0 -> 67430 bytes
-rw-r--r--dom/media/test/owl.mp3^headers^1
-rw-r--r--dom/media/test/padding-spanning-multiple-packets.mp3bin0 -> 117600 bytes
-rw-r--r--dom/media/test/pixel_aspect_ratio.mp4bin0 -> 1806042 bytes
-rw-r--r--dom/media/test/play_promise.js3
-rw-r--r--dom/media/test/poster-test.jpgbin0 -> 58493 bytes
-rw-r--r--dom/media/test/r11025_msadpcm_c1.wavbin0 -> 5978 bytes
-rw-r--r--dom/media/test/r11025_msadpcm_c1.wav^headers^1
-rw-r--r--dom/media/test/r11025_s16_c1-short.wavbin0 -> 8270 bytes
-rw-r--r--dom/media/test/r11025_s16_c1-short.wav^headers^1
-rw-r--r--dom/media/test/r11025_s16_c1.wavbin0 -> 22094 bytes
-rw-r--r--dom/media/test/r11025_s16_c1.wav^headers^1
-rw-r--r--dom/media/test/r11025_s16_c1_trailing.wavbin0 -> 22095 bytes
-rw-r--r--dom/media/test/r11025_s16_c1_trailing.wav^headers^1
-rw-r--r--dom/media/test/r11025_u8_c1.wavbin0 -> 11069 bytes
-rw-r--r--dom/media/test/r11025_u8_c1.wav^headers^1
-rw-r--r--dom/media/test/r11025_u8_c1_trunc.wavbin0 -> 20000 bytes
-rw-r--r--dom/media/test/r11025_u8_c1_trunc.wav^headers^1
-rw-r--r--dom/media/test/r16000_u8_c1_list.wavbin0 -> 68318 bytes
-rw-r--r--dom/media/test/r16000_u8_c1_list.wav^headers^1
-rw-r--r--dom/media/test/rdd_process_xpcom/RddProcessTest.cpp69
-rw-r--r--dom/media/test/rdd_process_xpcom/RddProcessTest.h28
-rw-r--r--dom/media/test/rdd_process_xpcom/components.conf15
-rw-r--r--dom/media/test/rdd_process_xpcom/moz.build21
-rw-r--r--dom/media/test/rdd_process_xpcom/nsIRddProcessTest.idl25
-rw-r--r--dom/media/test/reactivate_helper.html57
-rw-r--r--dom/media/test/red-46x48.mp4bin0 -> 1548 bytes
-rw-r--r--dom/media/test/red-46x48.mp4^headers^1
-rw-r--r--dom/media/test/red-48x46.mp4bin0 -> 1548 bytes
-rw-r--r--dom/media/test/red-48x46.mp4^headers^1
-rw-r--r--dom/media/test/redirect.sjs35
-rw-r--r--dom/media/test/referer.sjs49
-rw-r--r--dom/media/test/reftest/av1hdr2020.mp4bin0 -> 109327 bytes
-rw-r--r--dom/media/test/reftest/av1hdr2020.pngbin0 -> 4162799 bytes
-rw-r--r--dom/media/test/reftest/bipbop_300_215kbps.mp4.lastframe-ref.html4
-rw-r--r--dom/media/test/reftest/bipbop_300_215kbps.mp4.lastframe.html19
-rw-r--r--dom/media/test/reftest/color_quads/720p.pngbin0 -> 8722 bytes
-rw-r--r--dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.gbrp.av1.mp4bin0 -> 968 bytes
-rw-r--r--dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.gbrp.av1.webmbin0 -> 669 bytes
-rw-r--r--dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.gbrp.h264.mp4bin0 -> 1874 bytes
-rw-r--r--dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.gbrp.vp9.mp4bin0 -> 1102 bytes
-rw-r--r--dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.gbrp.vp9.webmbin0 -> 808 bytes
-rw-r--r--dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p.av1.mp4bin0 -> 1016 bytes
-rw-r--r--dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p.av1.webmbin0 -> 717 bytes
-rw-r--r--dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p.h264.mp4bin0 -> 1951 bytes
-rw-r--r--dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p.vp9.mp4bin0 -> 1116 bytes
-rw-r--r--dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p.vp9.webmbin0 -> 822 bytes
-rw-r--r--dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p10.av1.mp4bin0 -> 1031 bytes
-rw-r--r--dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p10.av1.webmbin0 -> 732 bytes
-rw-r--r--dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p10.h264.mp4bin0 -> 1990 bytes
-rw-r--r--dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p10.vp9.mp4bin0 -> 1153 bytes
-rw-r--r--dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p10.vp9.webmbin0 -> 859 bytes
-rw-r--r--dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.gbrp.av1.mp4bin0 -> 968 bytes
-rw-r--r--dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.gbrp.av1.webmbin0 -> 669 bytes
-rw-r--r--dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.gbrp.h264.mp4bin0 -> 1873 bytes
-rw-r--r--dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.gbrp.vp9.mp4bin0 -> 1102 bytes
-rw-r--r--dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.gbrp.vp9.webmbin0 -> 808 bytes
-rw-r--r--dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p.av1.mp4bin0 -> 1012 bytes
-rw-r--r--dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p.av1.webmbin0 -> 713 bytes
-rw-r--r--dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p.h264.mp4bin0 -> 1946 bytes
-rw-r--r--dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p.vp9.mp4bin0 -> 1111 bytes
-rw-r--r--dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p.vp9.webmbin0 -> 817 bytes
-rw-r--r--dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p10.av1.mp4bin0 -> 1036 bytes
-rw-r--r--dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p10.av1.webmbin0 -> 737 bytes
-rw-r--r--dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p10.h264.mp4bin0 -> 1989 bytes
-rw-r--r--dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p10.vp9.mp4bin0 -> 1148 bytes
-rw-r--r--dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p10.vp9.webmbin0 -> 854 bytes
-rw-r--r--dom/media/test/reftest/color_quads/reftest.list69
-rw-r--r--dom/media/test/reftest/frame_order.mp4bin0 -> 7971 bytes
-rw-r--r--dom/media/test/reftest/frame_order_mp4-ref.html13
-rw-r--r--dom/media/test/reftest/frame_order_mp4.html37
-rw-r--r--dom/media/test/reftest/gen_combos.py257
-rw-r--r--dom/media/test/reftest/generateREF.html104
-rw-r--r--dom/media/test/reftest/gizmo.mp4.55thframe-ref.html7
-rw-r--r--dom/media/test/reftest/gizmo.mp4.seek.html36
-rw-r--r--dom/media/test/reftest/image-10bits-rendering-720-90-ref.html4
-rw-r--r--dom/media/test/reftest/image-10bits-rendering-720-90-video.html22
-rw-r--r--dom/media/test/reftest/image-10bits-rendering-720-ref.html4
-rw-r--r--dom/media/test/reftest/image-10bits-rendering-720-video.html19
-rw-r--r--dom/media/test/reftest/image-10bits-rendering-720.video.html19
-rw-r--r--dom/media/test/reftest/image-10bits-rendering-90-ref.html4
-rw-r--r--dom/media/test/reftest/image-10bits-rendering-90-video.html22
-rw-r--r--dom/media/test/reftest/image-10bits-rendering-ref.html4
-rw-r--r--dom/media/test/reftest/image-10bits-rendering-video.html22
-rw-r--r--dom/media/test/reftest/incorrect_display_in_bytestream_vp8-ref.html13
-rw-r--r--dom/media/test/reftest/incorrect_display_in_bytestream_vp8.html33
-rw-r--r--dom/media/test/reftest/incorrect_display_in_bytestream_vp8.webmbin0 -> 84160 bytes
-rw-r--r--dom/media/test/reftest/incorrect_display_in_bytestream_vp9-ref.html12
-rw-r--r--dom/media/test/reftest/incorrect_display_in_bytestream_vp9.html33
-rw-r--r--dom/media/test/reftest/incorrect_display_in_bytestream_vp9.webmbin0 -> 740554 bytes
-rw-r--r--dom/media/test/reftest/reftest.list15
-rw-r--r--dom/media/test/reftest/reftest_img.html20
-rw-r--r--dom/media/test/reftest/reftest_video.html64
-rw-r--r--dom/media/test/reftest/short.mp4.firstframe-ref.html4
-rw-r--r--dom/media/test/reftest/short.mp4.firstframe.html19
-rw-r--r--dom/media/test/reftest/short.mp4.lastframe-ref.html4
-rw-r--r--dom/media/test/reftest/short.mp4.lastframe.html42
-rw-r--r--dom/media/test/reftest/uneven_frame_duration_video-ref.html7
-rw-r--r--dom/media/test/reftest/uneven_frame_duration_video.html39
-rw-r--r--dom/media/test/reftest/uneven_frame_durations.mp4bin0 -> 2424023 bytes
-rw-r--r--dom/media/test/reftest/uneven_frame_durations_3.8s_frame.pngbin0 -> 224136 bytes
-rw-r--r--dom/media/test/reftest/vp9hdr2020.pngbin0 -> 5083456 bytes
-rw-r--r--dom/media/test/reftest/vp9hdr2020.webmbin0 -> 108855 bytes
-rw-r--r--dom/media/test/resolution-change.webmbin0 -> 7166 bytes
-rw-r--r--dom/media/test/resolution-change.webm^headers^1
-rw-r--r--dom/media/test/sample-encrypted-sgpdstbl-sbgptraf.mp4bin0 -> 122703 bytes
-rw-r--r--dom/media/test/sample-encrypted-sgpdstbl-sbgptraf.mp4^headers^1
-rw-r--r--dom/media/test/sample-fisbone-skeleton4.ogvbin0 -> 8747 bytes
-rw-r--r--dom/media/test/sample-fisbone-skeleton4.ogv^headers^1
-rw-r--r--dom/media/test/sample-fisbone-wrong-header.ogvbin0 -> 8703 bytes
-rw-r--r--dom/media/test/sample-fisbone-wrong-header.ogv^headers^1
-rw-r--r--dom/media/test/sample.3g2bin0 -> 28561 bytes
-rw-r--r--dom/media/test/sample.3gpbin0 -> 28561 bytes
-rw-r--r--dom/media/test/seek-short.ogvbin0 -> 79921 bytes
-rw-r--r--dom/media/test/seek-short.ogv^headers^1
-rw-r--r--dom/media/test/seek-short.webmbin0 -> 19267 bytes
-rw-r--r--dom/media/test/seek-short.webm^headers^1
-rw-r--r--dom/media/test/seek.ogvbin0 -> 285310 bytes
-rw-r--r--dom/media/test/seek.ogv^headers^1
-rw-r--r--dom/media/test/seek.webmbin0 -> 215529 bytes
-rw-r--r--dom/media/test/seek.webm^headers^1
-rw-r--r--dom/media/test/seekLies.sjs22
-rw-r--r--dom/media/test/seek_support.js61
-rw-r--r--dom/media/test/seek_with_sound.oggbin0 -> 299507 bytes
-rw-r--r--dom/media/test/seek_with_sound.ogg^headers^1
-rw-r--r--dom/media/test/short-aac-encrypted-audio.mp4bin0 -> 5267 bytes
-rw-r--r--dom/media/test/short-aac-encrypted-audio.mp4^headers^1
-rw-r--r--dom/media/test/short-audio-fragmented-cenc-without-pssh.mp4bin0 -> 9261 bytes
-rw-r--r--dom/media/test/short-audio-fragmented-cenc-without-pssh.mp4^headers^1
-rw-r--r--dom/media/test/short-cenc-pssh-in-moof.mp4bin0 -> 14860 bytes
-rw-r--r--dom/media/test/short-cenc.mp4bin0 -> 14860 bytes
-rw-r--r--dom/media/test/short-cenc.xml37
-rw-r--r--dom/media/test/short-video.ogvbin0 -> 16049 bytes
-rw-r--r--dom/media/test/short-video.ogv^headers^1
-rw-r--r--dom/media/test/short-vp9-encrypted-video.mp4bin0 -> 6727 bytes
-rw-r--r--dom/media/test/short-vp9-encrypted-video.mp4^headers^1
-rw-r--r--dom/media/test/short.mp4bin0 -> 13708 bytes
-rw-r--r--dom/media/test/short.mp4.gzbin0 -> 6708 bytes
-rw-r--r--dom/media/test/short.mp4^headers^1
-rw-r--r--dom/media/test/sin-441-1s-44100-afconvert.mp4bin0 -> 9738 bytes
-rw-r--r--dom/media/test/sin-441-1s-44100-fdk_aac.mp4bin0 -> 9986 bytes
-rw-r--r--dom/media/test/sin-441-1s-44100-lame.mp3bin0 -> 8586 bytes
-rw-r--r--dom/media/test/sin-441-1s-44100.flacbin0 -> 24203 bytes
-rw-r--r--dom/media/test/sin-441-1s-44100.oggbin0 -> 5180 bytes
-rw-r--r--dom/media/test/sin-441-1s-44100.opusbin0 -> 10634 bytes
-rw-r--r--dom/media/test/sine.webmbin0 -> 17510 bytes
-rw-r--r--dom/media/test/sine.webm^headers^1
-rw-r--r--dom/media/test/single-xing-header-no-content-length.mp3bin0 -> 88834 bytes
-rw-r--r--dom/media/test/single-xing-header-no-content-length.mp3^headers^3
-rw-r--r--dom/media/test/sintel-short-clearkey-subsample-encrypted-audio.webmbin0 -> 30362 bytes
-rw-r--r--dom/media/test/sintel-short-clearkey-subsample-encrypted-audio.webm^headers^1
-rw-r--r--dom/media/test/sintel-short-clearkey-subsample-encrypted-video.webmbin0 -> 46703 bytes
-rw-r--r--dom/media/test/sintel-short-clearkey-subsample-encrypted-video.webm^headers^1
-rw-r--r--dom/media/test/small-shot-mp3.mp4bin0 -> 7491 bytes
-rw-r--r--dom/media/test/small-shot-mp3.mp4^headers^1
-rw-r--r--dom/media/test/small-shot.flacbin0 -> 16430 bytes
-rw-r--r--dom/media/test/small-shot.m4abin0 -> 2710 bytes
-rw-r--r--dom/media/test/small-shot.mp3bin0 -> 6825 bytes
-rw-r--r--dom/media/test/small-shot.mp3^headers^1
-rw-r--r--dom/media/test/small-shot.oggbin0 -> 6416 bytes
-rw-r--r--dom/media/test/small-shot.ogg^headers^1
-rw-r--r--dom/media/test/sound.oggbin0 -> 2603 bytes
-rw-r--r--dom/media/test/sound.ogg^headers^1
-rw-r--r--dom/media/test/spacestorm-1000Hz-100ms.oggbin0 -> 3270 bytes
-rw-r--r--dom/media/test/spacestorm-1000Hz-100ms.ogg^headers^1
-rw-r--r--dom/media/test/split.webmbin0 -> 105755 bytes
-rw-r--r--dom/media/test/split.webm^headers^1
-rw-r--r--dom/media/test/street.mp4bin0 -> 1505751 bytes
-rw-r--r--dom/media/test/street.mp4^headers^1
-rw-r--r--dom/media/test/sync.webmbin0 -> 397383 bytes
-rw-r--r--dom/media/test/test-1-mono.opusbin0 -> 4086 bytes
-rw-r--r--dom/media/test/test-1-mono.opus^headers^1
-rw-r--r--dom/media/test/test-2-stereo.opusbin0 -> 24973 bytes
-rw-r--r--dom/media/test/test-2-stereo.opus^headers^1
-rw-r--r--dom/media/test/test-3-LCR.opusbin0 -> 39471 bytes
-rw-r--r--dom/media/test/test-3-LCR.opus^headers^1
-rw-r--r--dom/media/test/test-4-quad.opusbin0 -> 129906 bytes
-rw-r--r--dom/media/test/test-4-quad.opus^headers^1
-rw-r--r--dom/media/test/test-5-5.0.opusbin0 -> 164935 bytes
-rw-r--r--dom/media/test/test-5-5.0.opus^headers^1
-rw-r--r--dom/media/test/test-6-5.1.opusbin0 -> 288195 bytes
-rw-r--r--dom/media/test/test-6-5.1.opus^headers^1
-rw-r--r--dom/media/test/test-7-6.1.opusbin0 -> 401668 bytes
-rw-r--r--dom/media/test/test-7-6.1.opus^headers^1
-rw-r--r--dom/media/test/test-8-7.1.opusbin0 -> 543119 bytes
-rw-r--r--dom/media/test/test-8-7.1.opus^headers^1
-rw-r--r--dom/media/test/test-stereo-phase-inversion-180.opusbin0 -> 14011 bytes
-rw-r--r--dom/media/test/test-stereo-phase-inversion-180.opus^headers^1
-rw-r--r--dom/media/test/test_VideoPlaybackQuality.html61
-rw-r--r--dom/media/test/test_VideoPlaybackQuality_disabled.html37
-rw-r--r--dom/media/test/test_access_control.html62
-rw-r--r--dom/media/test/test_arraybuffer.html83
-rw-r--r--dom/media/test/test_aspectratio_mp4.html46
-rw-r--r--dom/media/test/test_audio1.html33
-rw-r--r--dom/media/test/test_audio2.html33
-rw-r--r--dom/media/test/test_audioDocumentTitle.html56
-rw-r--r--dom/media/test/test_background_video_cancel_suspend_taint.html70
-rw-r--r--dom/media/test/test_background_video_cancel_suspend_visible.html69
-rw-r--r--dom/media/test/test_background_video_drawimage_with_suspended_video.html76
-rw-r--r--dom/media/test/test_background_video_ended_event.html48
-rw-r--r--dom/media/test/test_background_video_no_suspend_disabled.html36
-rw-r--r--dom/media/test/test_background_video_no_suspend_not_in_tree.html56
-rw-r--r--dom/media/test/test_background_video_no_suspend_short_vid.html38
-rw-r--r--dom/media/test/test_background_video_resume_after_end_show_last_frame.html131
-rw-r--r--dom/media/test/test_background_video_resume_looping_video_without_audio.html81
-rw-r--r--dom/media/test/test_background_video_suspend.html81
-rw-r--r--dom/media/test/test_background_video_suspend_ends.html55
-rw-r--r--dom/media/test/test_background_video_suspend_ready_state.html74
-rw-r--r--dom/media/test/test_background_video_tainted_by_capturestream.html46
-rw-r--r--dom/media/test/test_background_video_tainted_by_createimagebitmap.html42
-rw-r--r--dom/media/test/test_background_video_tainted_by_drawimage.html58
-rw-r--r--dom/media/test/test_buffered.html117
-rw-r--r--dom/media/test/test_bug1113600.html50
-rw-r--r--dom/media/test/test_bug1120222.html42
-rw-r--r--dom/media/test/test_bug1242338.html66
-rw-r--r--dom/media/test/test_bug1248229.html35
-rw-r--r--dom/media/test/test_bug1431810_opus_downmix_to_mono.html139
-rw-r--r--dom/media/test/test_bug1512958.html74
-rw-r--r--dom/media/test/test_bug1553262.html31
-rw-r--r--dom/media/test/test_bug448534.html71
-rw-r--r--dom/media/test/test_bug463162.xhtml78
-rw-r--r--dom/media/test/test_bug465498.html83
-rw-r--r--dom/media/test/test_bug495145.html95
-rw-r--r--dom/media/test/test_bug495300.html63
-rw-r--r--dom/media/test/test_bug654550.html84
-rw-r--r--dom/media/test/test_bug686942.html68
-rw-r--r--dom/media/test/test_bug726904.html56
-rw-r--r--dom/media/test/test_bug874897.html68
-rw-r--r--dom/media/test/test_bug879717.html130
-rw-r--r--dom/media/test/test_bug895305.html42
-rw-r--r--dom/media/test/test_bug919265.html30
-rw-r--r--dom/media/test/test_can_play_type.html40
-rw-r--r--dom/media/test/test_can_play_type_mpeg.html168
-rw-r--r--dom/media/test/test_can_play_type_no_ogg.html42
-rw-r--r--dom/media/test/test_can_play_type_ogg.html37
-rw-r--r--dom/media/test/test_can_play_type_wave.html30
-rw-r--r--dom/media/test/test_can_play_type_webm.html39
-rw-r--r--dom/media/test/test_capture_stream_av_sync.html276
-rw-r--r--dom/media/test/test_chaining.html92
-rw-r--r--dom/media/test/test_cloneElementVisually_ended_video.html48
-rw-r--r--dom/media/test/test_cloneElementVisually_mediastream.html70
-rw-r--r--dom/media/test/test_cloneElementVisually_mediastream_multitrack.html88
-rw-r--r--dom/media/test/test_cloneElementVisually_no_suspend.html90
-rw-r--r--dom/media/test/test_cloneElementVisually_paused.html45
-rw-r--r--dom/media/test/test_cloneElementVisually_poster.html53
-rw-r--r--dom/media/test/test_cloneElementVisually_resource_change.html67
-rw-r--r--dom/media/test/test_clone_media_element.html54
-rw-r--r--dom/media/test/test_closing_connections.html58
-rw-r--r--dom/media/test/test_constants.html228
-rw-r--r--dom/media/test/test_controls.html33
-rw-r--r--dom/media/test/test_cueless_webm_seek-1.html136
-rw-r--r--dom/media/test/test_cueless_webm_seek-2.html126
-rw-r--r--dom/media/test/test_cueless_webm_seek-3.html120
-rw-r--r--dom/media/test/test_currentTime.html19
-rw-r--r--dom/media/test/test_debug_data_helpers.html74
-rw-r--r--dom/media/test/test_decode_error.html66
-rw-r--r--dom/media/test/test_decode_error_crossorigin.html54
-rw-r--r--dom/media/test/test_decoder_disable.html78
-rw-r--r--dom/media/test/test_defaultMuted.html54
-rw-r--r--dom/media/test/test_delay_load.html108
-rw-r--r--dom/media/test/test_duration_after_error.html54
-rw-r--r--dom/media/test/test_eme_autoplay.html115
-rw-r--r--dom/media/test/test_eme_canvas_blocked.html58
-rw-r--r--dom/media/test/test_eme_createMediaKeys_iframes.html192
-rw-r--r--dom/media/test/test_eme_detach_media_keys.html63
-rw-r--r--dom/media/test/test_eme_detach_reattach_same_mediakeys_during_playback.html141
-rw-r--r--dom/media/test/test_eme_getstatusforpolicy.html55
-rw-r--r--dom/media/test/test_eme_initDataTypes.html130
-rw-r--r--dom/media/test/test_eme_missing_pssh.html92
-rw-r--r--dom/media/test/test_eme_non_mse_fails.html95
-rw-r--r--dom/media/test/test_eme_playback.html188
-rw-r--r--dom/media/test/test_eme_protection_query.html250
-rw-r--r--dom/media/test/test_eme_pssh_in_moof.html141
-rw-r--r--dom/media/test/test_eme_requestKeySystemAccess.html477
-rw-r--r--dom/media/test/test_eme_requestMediaKeySystemAccess_with_app_approval.html202
-rw-r--r--dom/media/test/test_eme_request_notifications.html82
-rw-r--r--dom/media/test/test_eme_sample_groups_playback.html130
-rw-r--r--dom/media/test/test_eme_session_callable_value.html34
-rw-r--r--dom/media/test/test_eme_setMediaKeys_before_attach_MediaSource.html37
-rw-r--r--dom/media/test/test_eme_special_key_system.html63
-rw-r--r--dom/media/test/test_eme_stream_capture_blocked_case1.html59
-rw-r--r--dom/media/test/test_eme_stream_capture_blocked_case2.html52
-rw-r--r--dom/media/test/test_eme_stream_capture_blocked_case3.html50
-rw-r--r--dom/media/test/test_eme_unsetMediaKeys_then_capture.html108
-rw-r--r--dom/media/test/test_eme_waitingforkey.html78
-rw-r--r--dom/media/test/test_eme_wv_privacy.html53
-rw-r--r--dom/media/test/test_empty_resource.html58
-rw-r--r--dom/media/test/test_error_in_video_document.html59
-rw-r--r--dom/media/test/test_error_on_404.html84
-rw-r--r--dom/media/test/test_fastSeek-forwards.html77
-rw-r--r--dom/media/test/test_fastSeek.html88
-rw-r--r--dom/media/test/test_fragment_noplay.html127
-rw-r--r--dom/media/test/test_fragment_play.html91
-rw-r--r--dom/media/test/test_hls_player_independency.html53
-rw-r--r--dom/media/test/test_imagecapture.html128
-rw-r--r--dom/media/test/test_info_leak.html175
-rw-r--r--dom/media/test/test_invalid_reject.html58
-rw-r--r--dom/media/test/test_invalid_reject_play.html44
-rw-r--r--dom/media/test/test_invalid_seek.html32
-rw-r--r--dom/media/test/test_load.html217
-rw-r--r--dom/media/test/test_load_candidates.html84
-rw-r--r--dom/media/test/test_load_same_resource.html106
-rw-r--r--dom/media/test/test_load_source.html76
-rw-r--r--dom/media/test/test_load_source_empty_type.html36
-rw-r--r--dom/media/test/test_loop.html57
-rw-r--r--dom/media/test/test_looping_eventsOrder.html52
-rw-r--r--dom/media/test/test_media_selection.html142
-rw-r--r--dom/media/test/test_media_sniffer.html67
-rw-r--r--dom/media/test/test_mediacapabilities_resistfingerprinting.html69
-rw-r--r--dom/media/test/test_mediarecorder_avoid_recursion.html61
-rw-r--r--dom/media/test/test_mediarecorder_bitrate.html127
-rw-r--r--dom/media/test/test_mediarecorder_creation.html45
-rw-r--r--dom/media/test/test_mediarecorder_creation_fail.html61
-rw-r--r--dom/media/test/test_mediarecorder_fires_start_event_once_when_erroring.html45
-rw-r--r--dom/media/test/test_mediarecorder_multipletracks.html68
-rw-r--r--dom/media/test/test_mediarecorder_onerror_pause.html107
-rw-r--r--dom/media/test/test_mediarecorder_pause_resume_video.html130
-rw-r--r--dom/media/test/test_mediarecorder_playback_can_repeat.html87
-rw-r--r--dom/media/test/test_mediarecorder_principals.html132
-rw-r--r--dom/media/test/test_mediarecorder_record_4ch_audiocontext.html76
-rw-r--r--dom/media/test/test_mediarecorder_record_addtracked_stream.html182
-rw-r--r--dom/media/test/test_mediarecorder_record_audiocontext.html65
-rw-r--r--dom/media/test/test_mediarecorder_record_audiocontext_mlk.html24
-rw-r--r--dom/media/test/test_mediarecorder_record_audionode.html135
-rw-r--r--dom/media/test/test_mediarecorder_record_canvas_captureStream.html75
-rw-r--r--dom/media/test/test_mediarecorder_record_changing_video_resolution.html174
-rw-r--r--dom/media/test/test_mediarecorder_record_downsize_resolution.html148
-rw-r--r--dom/media/test/test_mediarecorder_record_getdata_afterstart.html81
-rw-r--r--dom/media/test/test_mediarecorder_record_gum_video_timeslice.html94
-rw-r--r--dom/media/test/test_mediarecorder_record_gum_video_timeslice_mixed.html100
-rw-r--r--dom/media/test/test_mediarecorder_record_immediate_stop.html115
-rw-r--r--dom/media/test/test_mediarecorder_record_no_timeslice.html106
-rw-r--r--dom/media/test/test_mediarecorder_record_session.html75
-rw-r--r--dom/media/test/test_mediarecorder_record_startstopstart.html75
-rw-r--r--dom/media/test/test_mediarecorder_record_timeslice.html105
-rw-r--r--dom/media/test/test_mediarecorder_record_upsize_resolution.html148
-rw-r--r--dom/media/test/test_mediarecorder_reload_crash.html29
-rw-r--r--dom/media/test/test_mediarecorder_state_event_order.html83
-rw-r--r--dom/media/test/test_mediarecorder_state_transition.html280
-rw-r--r--dom/media/test/test_mediarecorder_webm_support.html56
-rw-r--r--dom/media/test/test_mediastream_as_eventarget.html33
-rw-r--r--dom/media/test/test_mediatrack_consuming_mediaresource.html198
-rw-r--r--dom/media/test/test_mediatrack_consuming_mediastream.html146
-rw-r--r--dom/media/test/test_mediatrack_events.html135
-rw-r--r--dom/media/test/test_mediatrack_parsing_ogg.html72
-rw-r--r--dom/media/test/test_mediatrack_replay_from_end.html160
-rw-r--r--dom/media/test/test_metadata.html82
-rw-r--r--dom/media/test/test_midflight_redirect_blocked.html87
-rw-r--r--dom/media/test/test_mixed_principals.html83
-rw-r--r--dom/media/test/test_mozHasAudio.html42
-rw-r--r--dom/media/test/test_mp3_broadcast.html52
-rw-r--r--dom/media/test/test_mp3_with_multiple_ID3v2.html30
-rw-r--r--dom/media/test/test_multiple_mediastreamtracks.html47
-rw-r--r--dom/media/test/test_networkState.html47
-rw-r--r--dom/media/test/test_new_audio.html48
-rw-r--r--dom/media/test/test_no_load_event.html53
-rw-r--r--dom/media/test/test_not_reset_playbackRate_when_removing_nonloaded_media_from_document.html46
-rw-r--r--dom/media/test/test_paused.html21
-rw-r--r--dom/media/test/test_paused_after_ended.html53
-rw-r--r--dom/media/test/test_periodic_timeupdate.html100
-rw-r--r--dom/media/test/test_play_events.html61
-rw-r--r--dom/media/test/test_play_events_2.html60
-rw-r--r--dom/media/test/test_play_promise_1.html42
-rw-r--r--dom/media/test/test_play_promise_10.html40
-rw-r--r--dom/media/test/test_play_promise_11.html40
-rw-r--r--dom/media/test/test_play_promise_12.html45
-rw-r--r--dom/media/test/test_play_promise_13.html49
-rw-r--r--dom/media/test/test_play_promise_14.html56
-rw-r--r--dom/media/test/test_play_promise_15.html51
-rw-r--r--dom/media/test/test_play_promise_16.html47
-rw-r--r--dom/media/test/test_play_promise_17.html43
-rw-r--r--dom/media/test/test_play_promise_18.html46
-rw-r--r--dom/media/test/test_play_promise_2.html43
-rw-r--r--dom/media/test/test_play_promise_3.html47
-rw-r--r--dom/media/test/test_play_promise_4.html41
-rw-r--r--dom/media/test/test_play_promise_5.html44
-rw-r--r--dom/media/test/test_play_promise_6.html45
-rw-r--r--dom/media/test/test_play_promise_7.html47
-rw-r--r--dom/media/test/test_play_promise_8.html47
-rw-r--r--dom/media/test/test_play_promise_9.html44
-rw-r--r--dom/media/test/test_play_twice.html95
-rw-r--r--dom/media/test/test_playback.html108
-rw-r--r--dom/media/test/test_playback_and_bfcache.html72
-rw-r--r--dom/media/test/test_playback_errors.html48
-rw-r--r--dom/media/test/test_playback_hls.html91
-rw-r--r--dom/media/test/test_playback_rate.html175
-rw-r--r--dom/media/test/test_playback_rate_playpause.html66
-rw-r--r--dom/media/test/test_playback_reactivate.html77
-rw-r--r--dom/media/test/test_played.html288
-rw-r--r--dom/media/test/test_preload_actions.html582
-rw-r--r--dom/media/test/test_preload_attribute.html44
-rw-r--r--dom/media/test/test_preload_suspend.html112
-rw-r--r--dom/media/test/test_preserve_playbackrate_after_ui_play.html60
-rw-r--r--dom/media/test/test_progress.html52
-rw-r--r--dom/media/test/test_reactivate.html64
-rw-r--r--dom/media/test/test_readyState.html51
-rw-r--r--dom/media/test/test_referer.html88
-rw-r--r--dom/media/test/test_replay_metadata.html120
-rw-r--r--dom/media/test/test_reset_events_async.html58
-rw-r--r--dom/media/test/test_reset_src.html98
-rw-r--r--dom/media/test/test_resolution_change.html52
-rw-r--r--dom/media/test/test_resume.html47
-rw-r--r--dom/media/test/test_seamless_looping.html199
-rw-r--r--dom/media/test/test_seamless_looping_cancel_looping_future_frames.html61
-rw-r--r--dom/media/test/test_seamless_looping_duration.html63
-rw-r--r--dom/media/test/test_seamless_looping_media_element_state.html51
-rw-r--r--dom/media/test/test_seamless_looping_resume_video_decoding.html64
-rw-r--r--dom/media/test/test_seamless_looping_seek_current_time.html62
-rw-r--r--dom/media/test/test_seamless_looping_video.html73
-rw-r--r--dom/media/test/test_seek-1.html84
-rw-r--r--dom/media/test/test_seek-10.html56
-rw-r--r--dom/media/test/test_seek-11.html76
-rw-r--r--dom/media/test/test_seek-12.html59
-rw-r--r--dom/media/test/test_seek-13.html72
-rw-r--r--dom/media/test/test_seek-14.html43
-rw-r--r--dom/media/test/test_seek-2.html76
-rw-r--r--dom/media/test/test_seek-3.html68
-rw-r--r--dom/media/test/test_seek-4.html70
-rw-r--r--dom/media/test/test_seek-5.html69
-rw-r--r--dom/media/test/test_seek-6.html64
-rw-r--r--dom/media/test/test_seek-7.html59
-rw-r--r--dom/media/test/test_seek-8.html42
-rw-r--r--dom/media/test/test_seek-9.html41
-rw-r--r--dom/media/test/test_seekLies.html28
-rw-r--r--dom/media/test/test_seekToNextFrame.html95
-rw-r--r--dom/media/test/test_seek_duration.html55
-rw-r--r--dom/media/test/test_seek_negative.html77
-rw-r--r--dom/media/test/test_seek_nosrc.html58
-rw-r--r--dom/media/test/test_seek_out_of_range.html48
-rw-r--r--dom/media/test/test_seek_promise_bug1344357.html35
-rw-r--r--dom/media/test/test_seekable1.html66
-rw-r--r--dom/media/test/test_source.html91
-rw-r--r--dom/media/test/test_source_null.html33
-rw-r--r--dom/media/test/test_source_write.html40
-rw-r--r--dom/media/test/test_standalone.html61
-rw-r--r--dom/media/test/test_streams_capture_origin.html90
-rw-r--r--dom/media/test/test_streams_element_capture.html120
-rw-r--r--dom/media/test/test_streams_element_capture_mediatrack.html100
-rw-r--r--dom/media/test/test_streams_element_capture_playback.html47
-rw-r--r--dom/media/test/test_streams_element_capture_reset.html174
-rw-r--r--dom/media/test/test_streams_element_capture_twice.html79
-rw-r--r--dom/media/test/test_streams_firstframe.html67
-rw-r--r--dom/media/test/test_streams_gc.html44
-rw-r--r--dom/media/test/test_streams_individual_pause.html77
-rw-r--r--dom/media/test/test_streams_srcObject.html60
-rw-r--r--dom/media/test/test_streams_tracks.html66
-rw-r--r--dom/media/test/test_suspend_media_by_inactive_docshell.html67
-rw-r--r--dom/media/test/test_temporary_file_blob_video_plays.html75
-rw-r--r--dom/media/test/test_timeupdate_small_files.html88
-rw-r--r--dom/media/test/test_unseekable.html101
-rw-r--r--dom/media/test/test_videoDocumentTitle.html57
-rw-r--r--dom/media/test/test_videoPlaybackQuality_totalFrames.html46
-rw-r--r--dom/media/test/test_video_dimensions.html96
-rw-r--r--dom/media/test/test_video_gzip_encoding.html25
-rw-r--r--dom/media/test/test_video_in_audio_element.html68
-rw-r--r--dom/media/test/test_video_low_power_telemetry.html205
-rw-r--r--dom/media/test/test_video_stats_resistfingerprinting.html90
-rw-r--r--dom/media/test/test_video_to_canvas.html68
-rw-r--r--dom/media/test/test_volume.html41
-rw-r--r--dom/media/test/test_vp9_superframes.html31
-rw-r--r--dom/media/test/test_wav_ended1.html43
-rw-r--r--dom/media/test/test_wav_ended2.html62
-rw-r--r--dom/media/test/tone2s-silence4s-tone2s.opusbin0 -> 67207 bytes
-rw-r--r--dom/media/test/two-xing-header-no-content-length.mp3bin0 -> 45594 bytes
-rw-r--r--dom/media/test/two-xing-header-no-content-length.mp3^headers^3
-rw-r--r--dom/media/test/variable-channel.oggbin0 -> 27749 bytes
-rw-r--r--dom/media/test/variable-channel.ogg^headers^1
-rw-r--r--dom/media/test/variable-channel.opusbin0 -> 46597 bytes
-rw-r--r--dom/media/test/variable-channel.opus^headers^1
-rw-r--r--dom/media/test/variable-preskip.opusbin0 -> 17660 bytes
-rw-r--r--dom/media/test/variable-preskip.opus^headers^1
-rw-r--r--dom/media/test/variable-samplerate.oggbin0 -> 22325 bytes
-rw-r--r--dom/media/test/variable-samplerate.ogg^headers^1
-rw-r--r--dom/media/test/variable-samplerate.opusbin0 -> 28111 bytes
-rw-r--r--dom/media/test/variable-samplerate.opus^headers^1
-rw-r--r--dom/media/test/vbr-head.mp3bin0 -> 4474 bytes
-rw-r--r--dom/media/test/vbr-head.mp3^headers^1
-rw-r--r--dom/media/test/vbr.mp3bin0 -> 300553 bytes
-rw-r--r--dom/media/test/vbr.mp3^headers^1
-rw-r--r--dom/media/test/very-short.mp3bin0 -> 1612 bytes
-rw-r--r--dom/media/test/video-overhang.oggbin0 -> 301831 bytes
-rw-r--r--dom/media/test/video-overhang.ogg^headers^1
-rw-r--r--dom/media/test/vp9-short.webmbin0 -> 3107 bytes
-rw-r--r--dom/media/test/vp9-short.webm^headers^1
-rw-r--r--dom/media/test/vp9-superframes.webmbin0 -> 173187 bytes
-rw-r--r--dom/media/test/vp9-superframes.webm^headers^1
-rw-r--r--dom/media/test/vp9.webmbin0 -> 97465 bytes
-rw-r--r--dom/media/test/vp9.webm^headers^1
-rw-r--r--dom/media/test/vp9cake-short.webmbin0 -> 25155 bytes
-rw-r--r--dom/media/test/vp9cake-short.webm^headers^1
-rw-r--r--dom/media/test/vp9cake.webmbin0 -> 141743 bytes
-rw-r--r--dom/media/test/vp9cake.webm^headers^1
-rw-r--r--dom/media/test/wave_metadata.wavbin0 -> 42706 bytes
-rw-r--r--dom/media/test/wave_metadata.wav^headers^1
-rw-r--r--dom/media/test/wave_metadata_bad_len.wavbin0 -> 42706 bytes
-rw-r--r--dom/media/test/wave_metadata_bad_len.wav^headers^1
-rw-r--r--dom/media/test/wave_metadata_bad_no_null.wavbin0 -> 42706 bytes
-rw-r--r--dom/media/test/wave_metadata_bad_no_null.wav^headers^1
-rw-r--r--dom/media/test/wave_metadata_bad_utf8.wavbin0 -> 42704 bytes
-rw-r--r--dom/media/test/wave_metadata_bad_utf8.wav^headers^1
-rw-r--r--dom/media/test/wave_metadata_unknown_tag.wavbin0 -> 42706 bytes
-rw-r--r--dom/media/test/wave_metadata_unknown_tag.wav^headers^1
-rw-r--r--dom/media/test/wave_metadata_utf8.wavbin0 -> 42704 bytes
-rw-r--r--dom/media/test/wave_metadata_utf8.wav^headers^1
-rw-r--r--dom/media/test/wavedata_alaw.wavbin0 -> 11067 bytes
-rw-r--r--dom/media/test/wavedata_alaw.wav^headers^1
-rw-r--r--dom/media/test/wavedata_float.wavbin0 -> 176458 bytes
-rw-r--r--dom/media/test/wavedata_float.wav^headers^1
-rw-r--r--dom/media/test/wavedata_s16.wavbin0 -> 22062 bytes
-rw-r--r--dom/media/test/wavedata_s16.wav^headers^1
-rw-r--r--dom/media/test/wavedata_s24.wavbin0 -> 33071 bytes
-rw-r--r--dom/media/test/wavedata_s24.wav^headers^1
-rw-r--r--dom/media/test/wavedata_u8.wavbin0 -> 11037 bytes
-rw-r--r--dom/media/test/wavedata_u8.wav^headers^1
-rw-r--r--dom/media/test/wavedata_ulaw.wavbin0 -> 11067 bytes
-rw-r--r--dom/media/test/wavedata_ulaw.wav^headers^1
-rw-r--r--dom/media/test/white-short.webmbin0 -> 1573 bytes
-rw-r--r--dom/media/tests/crashtests/1281695.html24
-rw-r--r--dom/media/tests/crashtests/1306476.html47
-rw-r--r--dom/media/tests/crashtests/1348381.html22
-rw-r--r--dom/media/tests/crashtests/1367930_1.html39
-rw-r--r--dom/media/tests/crashtests/1367930_2.html25
-rw-r--r--dom/media/tests/crashtests/1429507_1.html13
-rw-r--r--dom/media/tests/crashtests/1429507_2.html15
-rw-r--r--dom/media/tests/crashtests/1443212.html13
-rw-r--r--dom/media/tests/crashtests/1453030.html19
-rw-r--r--dom/media/tests/crashtests/1468451.html10
-rw-r--r--dom/media/tests/crashtests/1490700.html28
-rw-r--r--dom/media/tests/crashtests/1505957.html23
-rw-r--r--dom/media/tests/crashtests/1509442-1.html14
-rw-r--r--dom/media/tests/crashtests/1509442.html7
-rw-r--r--dom/media/tests/crashtests/1510848.html4
-rw-r--r--dom/media/tests/crashtests/1511130.html12
-rw-r--r--dom/media/tests/crashtests/1516292.html16
-rw-r--r--dom/media/tests/crashtests/1573536.html18
-rw-r--r--dom/media/tests/crashtests/1576938.html16
-rw-r--r--dom/media/tests/crashtests/1594136.html15
-rw-r--r--dom/media/tests/crashtests/1749308.html18
-rw-r--r--dom/media/tests/crashtests/1764915.html14
-rw-r--r--dom/media/tests/crashtests/1764933.html15
-rw-r--r--dom/media/tests/crashtests/1764940.html14
-rw-r--r--dom/media/tests/crashtests/1766668.html11
-rw-r--r--dom/media/tests/crashtests/1783765.html22
-rw-r--r--dom/media/tests/crashtests/780790.html16
-rw-r--r--dom/media/tests/crashtests/791270.html17
-rw-r--r--dom/media/tests/crashtests/791278.html20
-rw-r--r--dom/media/tests/crashtests/791330.html35
-rw-r--r--dom/media/tests/crashtests/799419.html32
-rw-r--r--dom/media/tests/crashtests/801227.html35
-rw-r--r--dom/media/tests/crashtests/802982.html27
-rw-r--r--dom/media/tests/crashtests/812785.html70
-rw-r--r--dom/media/tests/crashtests/822197.html28
-rw-r--r--dom/media/tests/crashtests/834100.html25
-rw-r--r--dom/media/tests/crashtests/836349.html20
-rw-r--r--dom/media/tests/crashtests/837324.html25
-rw-r--r--dom/media/tests/crashtests/855796.html66
-rw-r--r--dom/media/tests/crashtests/860143.html25
-rw-r--r--dom/media/tests/crashtests/861958.html24
-rw-r--r--dom/media/tests/crashtests/863929.html66
-rw-r--r--dom/media/tests/crashtests/crashtests.list42
-rw-r--r--dom/media/tests/crashtests/datachannel-oom.html22
-rw-r--r--dom/media/tools/generateGmpJson.py170
-rw-r--r--dom/media/utils/MediaElementEventRunners.cpp140
-rw-r--r--dom/media/utils/MediaElementEventRunners.h190
-rw-r--r--dom/media/utils/PerformanceRecorder.cpp308
-rw-r--r--dom/media/utils/PerformanceRecorder.h407
-rw-r--r--dom/media/utils/TelemetryProbesReporter.cpp673
-rw-r--r--dom/media/utils/TelemetryProbesReporter.h172
-rw-r--r--dom/media/utils/gtest/TestPerformanceRecorder.cpp110
-rw-r--r--dom/media/utils/gtest/moz.build15
-rw-r--r--dom/media/utils/moz.build26
-rw-r--r--dom/media/wave/WaveDecoder.cpp56
-rw-r--r--dom/media/wave/WaveDecoder.h28
-rw-r--r--dom/media/wave/WaveDemuxer.cpp739
-rw-r--r--dom/media/wave/WaveDemuxer.h266
-rw-r--r--dom/media/wave/moz.build20
-rw-r--r--dom/media/webaudio/AlignedTArray.h115
-rw-r--r--dom/media/webaudio/AlignmentUtils.h29
-rw-r--r--dom/media/webaudio/AnalyserNode.cpp389
-rw-r--r--dom/media/webaudio/AnalyserNode.h82
-rw-r--r--dom/media/webaudio/AudioBlock.cpp166
-rw-r--r--dom/media/webaudio/AudioBlock.h134
-rw-r--r--dom/media/webaudio/AudioBuffer.cpp480
-rw-r--r--dom/media/webaudio/AudioBuffer.h138
-rw-r--r--dom/media/webaudio/AudioBufferSourceNode.cpp845
-rw-r--r--dom/media/webaudio/AudioBufferSourceNode.h129
-rw-r--r--dom/media/webaudio/AudioContext.cpp1409
-rw-r--r--dom/media/webaudio/AudioContext.h479
-rw-r--r--dom/media/webaudio/AudioDestinationNode.cpp675
-rw-r--r--dom/media/webaudio/AudioDestinationNode.h133
-rw-r--r--dom/media/webaudio/AudioEventTimeline.cpp369
-rw-r--r--dom/media/webaudio/AudioEventTimeline.h387
-rw-r--r--dom/media/webaudio/AudioListener.cpp136
-rw-r--r--dom/media/webaudio/AudioListener.h85
-rw-r--r--dom/media/webaudio/AudioNode.cpp613
-rw-r--r--dom/media/webaudio/AudioNode.h297
-rw-r--r--dom/media/webaudio/AudioNodeEngine.cpp440
-rw-r--r--dom/media/webaudio/AudioNodeEngine.h392
-rw-r--r--dom/media/webaudio/AudioNodeEngineGeneric.h58
-rw-r--r--dom/media/webaudio/AudioNodeEngineGenericImpl.h341
-rw-r--r--dom/media/webaudio/AudioNodeEngineNEON.cpp9
-rw-r--r--dom/media/webaudio/AudioNodeEngineSSE2.cpp10
-rw-r--r--dom/media/webaudio/AudioNodeEngineSSE4_2_FMA3.cpp10
-rw-r--r--dom/media/webaudio/AudioNodeExternalInputTrack.cpp225
-rw-r--r--dom/media/webaudio/AudioNodeExternalInputTrack.h46
-rw-r--r--dom/media/webaudio/AudioNodeTrack.cpp723
-rw-r--r--dom/media/webaudio/AudioNodeTrack.h237
-rw-r--r--dom/media/webaudio/AudioParam.cpp169
-rw-r--r--dom/media/webaudio/AudioParam.h245
-rw-r--r--dom/media/webaudio/AudioParamDescriptorMap.h22
-rw-r--r--dom/media/webaudio/AudioParamMap.cpp21
-rw-r--r--dom/media/webaudio/AudioParamMap.h34
-rw-r--r--dom/media/webaudio/AudioParamTimeline.h140
-rw-r--r--dom/media/webaudio/AudioProcessingEvent.cpp44
-rw-r--r--dom/media/webaudio/AudioProcessingEvent.h71
-rw-r--r--dom/media/webaudio/AudioScheduledSourceNode.cpp17
-rw-r--r--dom/media/webaudio/AudioScheduledSourceNode.h33
-rw-r--r--dom/media/webaudio/AudioWorkletGlobalScope.cpp370
-rw-r--r--dom/media/webaudio/AudioWorkletGlobalScope.h84
-rw-r--r--dom/media/webaudio/AudioWorkletImpl.cpp88
-rw-r--r--dom/media/webaudio/AudioWorkletImpl.h63
-rw-r--r--dom/media/webaudio/AudioWorkletNode.cpp896
-rw-r--r--dom/media/webaudio/AudioWorkletNode.h68
-rw-r--r--dom/media/webaudio/AudioWorkletProcessor.cpp49
-rw-r--r--dom/media/webaudio/AudioWorkletProcessor.h50
-rw-r--r--dom/media/webaudio/BiquadFilterNode.cpp349
-rw-r--r--dom/media/webaudio/BiquadFilterNode.h71
-rw-r--r--dom/media/webaudio/ChannelMergerNode.cpp100
-rw-r--r--dom/media/webaudio/ChannelMergerNode.h67
-rw-r--r--dom/media/webaudio/ChannelSplitterNode.cpp105
-rw-r--r--dom/media/webaudio/ChannelSplitterNode.h72
-rw-r--r--dom/media/webaudio/ConstantSourceNode.cpp279
-rw-r--r--dom/media/webaudio/ConstantSourceNode.h60
-rw-r--r--dom/media/webaudio/ConvolverNode.cpp479
-rw-r--r--dom/media/webaudio/ConvolverNode.h77
-rw-r--r--dom/media/webaudio/DelayBuffer.cpp235
-rw-r--r--dom/media/webaudio/DelayBuffer.h105
-rw-r--r--dom/media/webaudio/DelayNode.cpp223
-rw-r--r--dom/media/webaudio/DelayNode.h55
-rw-r--r--dom/media/webaudio/DynamicsCompressorNode.cpp225
-rw-r--r--dom/media/webaudio/DynamicsCompressorNode.h91
-rw-r--r--dom/media/webaudio/FFTBlock.cpp223
-rw-r--r--dom/media/webaudio/FFTBlock.h346
-rw-r--r--dom/media/webaudio/GainNode.cpp149
-rw-r--r--dom/media/webaudio/GainNode.h53
-rw-r--r--dom/media/webaudio/IIRFilterNode.cpp258
-rw-r--r--dom/media/webaudio/IIRFilterNode.h56
-rw-r--r--dom/media/webaudio/MediaBufferDecoder.cpp766
-rw-r--r--dom/media/webaudio/MediaBufferDecoder.h70
-rw-r--r--dom/media/webaudio/MediaElementAudioSourceNode.cpp105
-rw-r--r--dom/media/webaudio/MediaElementAudioSourceNode.h67
-rw-r--r--dom/media/webaudio/MediaStreamAudioDestinationNode.cpp150
-rw-r--r--dom/media/webaudio/MediaStreamAudioDestinationNode.h58
-rw-r--r--dom/media/webaudio/MediaStreamAudioSourceNode.cpp278
-rw-r--r--dom/media/webaudio/MediaStreamAudioSourceNode.h127
-rw-r--r--dom/media/webaudio/MediaStreamTrackAudioSourceNode.cpp200
-rw-r--r--dom/media/webaudio/MediaStreamTrackAudioSourceNode.h113
-rw-r--r--dom/media/webaudio/OscillatorNode.cpp542
-rw-r--r--dom/media/webaudio/OscillatorNode.h92
-rw-r--r--dom/media/webaudio/PannerNode.cpp726
-rw-r--r--dom/media/webaudio/PannerNode.h221
-rw-r--r--dom/media/webaudio/PanningUtils.h62
-rw-r--r--dom/media/webaudio/PeriodicWave.cpp146
-rw-r--r--dom/media/webaudio/PeriodicWave.h59
-rw-r--r--dom/media/webaudio/PlayingRefChangeHandler.h42
-rw-r--r--dom/media/webaudio/ReportDecodeResultTask.h37
-rw-r--r--dom/media/webaudio/ScriptProcessorNode.cpp549
-rw-r--r--dom/media/webaudio/ScriptProcessorNode.h131
-rw-r--r--dom/media/webaudio/StereoPannerNode.cpp197
-rw-r--r--dom/media/webaudio/StereoPannerNode.h74
-rw-r--r--dom/media/webaudio/ThreeDPoint.cpp38
-rw-r--r--dom/media/webaudio/ThreeDPoint.h68
-rw-r--r--dom/media/webaudio/WaveShaperNode.cpp390
-rw-r--r--dom/media/webaudio/WaveShaperNode.h71
-rw-r--r--dom/media/webaudio/WebAudioUtils.cpp149
-rw-r--r--dom/media/webaudio/WebAudioUtils.h209
-rw-r--r--dom/media/webaudio/blink/Biquad.cpp439
-rw-r--r--dom/media/webaudio/blink/Biquad.h108
-rw-r--r--dom/media/webaudio/blink/DenormalDisabler.h170
-rw-r--r--dom/media/webaudio/blink/DynamicsCompressor.cpp315
-rw-r--r--dom/media/webaudio/blink/DynamicsCompressor.h135
-rw-r--r--dom/media/webaudio/blink/DynamicsCompressorKernel.cpp494
-rw-r--r--dom/media/webaudio/blink/DynamicsCompressorKernel.h125
-rw-r--r--dom/media/webaudio/blink/FFTConvolver.cpp118
-rw-r--r--dom/media/webaudio/blink/FFTConvolver.h87
-rw-r--r--dom/media/webaudio/blink/HRTFDatabase.cpp135
-rw-r--r--dom/media/webaudio/blink/HRTFDatabase.h104
-rw-r--r--dom/media/webaudio/blink/HRTFDatabaseLoader.cpp215
-rw-r--r--dom/media/webaudio/blink/HRTFDatabaseLoader.h155
-rw-r--r--dom/media/webaudio/blink/HRTFElevation.cpp338
-rw-r--r--dom/media/webaudio/blink/HRTFElevation.h114
-rw-r--r--dom/media/webaudio/blink/HRTFKernel.cpp109
-rw-r--r--dom/media/webaudio/blink/HRTFKernel.h129
-rw-r--r--dom/media/webaudio/blink/HRTFPanner.cpp328
-rw-r--r--dom/media/webaudio/blink/HRTFPanner.h120
-rw-r--r--dom/media/webaudio/blink/IIRFilter.cpp178
-rw-r--r--dom/media/webaudio/blink/IIRFilter.h62
-rw-r--r--dom/media/webaudio/blink/IRC_Composite_C_R0195-incl.cpp4571
-rw-r--r--dom/media/webaudio/blink/PeriodicWave.cpp353
-rw-r--r--dom/media/webaudio/blink/PeriodicWave.h123
-rw-r--r--dom/media/webaudio/blink/README24
-rw-r--r--dom/media/webaudio/blink/Reverb.cpp277
-rw-r--r--dom/media/webaudio/blink/Reverb.h80
-rw-r--r--dom/media/webaudio/blink/ReverbAccumulationBuffer.cpp114
-rw-r--r--dom/media/webaudio/blink/ReverbAccumulationBuffer.h76
-rw-r--r--dom/media/webaudio/blink/ReverbConvolver.cpp273
-rw-r--r--dom/media/webaudio/blink/ReverbConvolver.h94
-rw-r--r--dom/media/webaudio/blink/ReverbConvolverStage.cpp101
-rw-r--r--dom/media/webaudio/blink/ReverbConvolverStage.h84
-rw-r--r--dom/media/webaudio/blink/ReverbInputBuffer.cpp85
-rw-r--r--dom/media/webaudio/blink/ReverbInputBuffer.h73
-rw-r--r--dom/media/webaudio/blink/ZeroPole.cpp83
-rw-r--r--dom/media/webaudio/blink/ZeroPole.h63
-rw-r--r--dom/media/webaudio/blink/moz.build36
-rw-r--r--dom/media/webaudio/moz.build153
-rw-r--r--dom/media/webaudio/test/8kHz-320kbps-6ch.aacbin0 -> 22657 bytes
-rw-r--r--dom/media/webaudio/test/audio-expected.wavbin0 -> 190764 bytes
-rw-r--r--dom/media/webaudio/test/audio-mono-expected-2.wavbin0 -> 103788 bytes
-rw-r--r--dom/media/webaudio/test/audio-mono-expected.wavbin0 -> 103788 bytes
-rw-r--r--dom/media/webaudio/test/audio-quad.wavbin0 -> 5128 bytes
-rw-r--r--dom/media/webaudio/test/audio.ogvbin0 -> 16049 bytes
-rw-r--r--dom/media/webaudio/test/audioBufferSourceNodeDetached_worker.js3
-rw-r--r--dom/media/webaudio/test/audiovideo.mp4bin0 -> 139713 bytes
-rw-r--r--dom/media/webaudio/test/blink/README9
-rw-r--r--dom/media/webaudio/test/blink/audio-testing.js192
-rw-r--r--dom/media/webaudio/test/blink/biquad-filters.js368
-rw-r--r--dom/media/webaudio/test/blink/biquad-testing.js153
-rw-r--r--dom/media/webaudio/test/blink/convolution-testing.js182
-rw-r--r--dom/media/webaudio/test/blink/mochitest.ini22
-rw-r--r--dom/media/webaudio/test/blink/panner-model-testing.js210
-rw-r--r--dom/media/webaudio/test/blink/test_biquadFilterNodeAllPass.html32
-rw-r--r--dom/media/webaudio/test/blink/test_biquadFilterNodeAutomation.html351
-rw-r--r--dom/media/webaudio/test/blink/test_biquadFilterNodeBandPass.html34
-rw-r--r--dom/media/webaudio/test/blink/test_biquadFilterNodeGetFrequencyResponse.html261
-rw-r--r--dom/media/webaudio/test/blink/test_biquadFilterNodeHighPass.html33
-rw-r--r--dom/media/webaudio/test/blink/test_biquadFilterNodeHighShelf.html33
-rw-r--r--dom/media/webaudio/test/blink/test_biquadFilterNodeLowPass.html34
-rw-r--r--dom/media/webaudio/test/blink/test_biquadFilterNodeLowShelf.html34
-rw-r--r--dom/media/webaudio/test/blink/test_biquadFilterNodeNotch.html33
-rw-r--r--dom/media/webaudio/test/blink/test_biquadFilterNodePeaking.html34
-rw-r--r--dom/media/webaudio/test/blink/test_biquadFilterNodeTail.html76
-rw-r--r--dom/media/webaudio/test/blink/test_iirFilterNode.html467
-rw-r--r--dom/media/webaudio/test/blink/test_iirFilterNodeGetFrequencyResponse.html97
-rw-r--r--dom/media/webaudio/test/corsServer.sjs26
-rw-r--r--dom/media/webaudio/test/file_nodeCreationDocumentGone.html4
-rwxr-xr-xdom/media/webaudio/test/generate-test-files.py52
-rw-r--r--dom/media/webaudio/test/half-a-second-1ch-44100-aac-afconvert.mp4bin0 -> 6560 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-1ch-44100-aac.aacbin0 -> 4826 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-1ch-44100-aac.mp4bin0 -> 5584 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-1ch-44100-flac.flacbin0 -> 17320 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-1ch-44100-libmp3lame.mp3bin0 -> 4615 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-1ch-44100-libopus.mp4bin0 -> 7171 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-1ch-44100-libopus.opusbin0 -> 6469 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-1ch-44100-libopus.webmbin0 -> 6991 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-1ch-44100-libvorbis.oggbin0 -> 4320 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-1ch-44100-libvorbis.webmbin0 -> 4878 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-1ch-44100.wavbin0 -> 44144 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-1ch-48000-aac.aacbin0 -> 4840 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-1ch-48000-aac.mp4bin0 -> 5592 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-1ch-48000-flac.flacbin0 -> 18577 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-1ch-48000-libmp3lame.mp3bin0 -> 4461 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-1ch-48000-libopus.mp4bin0 -> 6738 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-1ch-48000-libopus.opusbin0 -> 6031 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-1ch-48000-libopus.webmbin0 -> 6558 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-1ch-48000-libvorbis.oggbin0 -> 4559 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-1ch-48000-libvorbis.webmbin0 -> 5142 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-1ch-48000.wavbin0 -> 48044 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-2ch-44100-aac.aacbin0 -> 8755 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-2ch-44100-aac.mp4bin0 -> 9513 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-2ch-44100-flac.flacbin0 -> 23279 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-2ch-44100-libmp3lame.mp3bin0 -> 9030 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-2ch-44100-libopus.mp4bin0 -> 11593 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-2ch-44100-libopus.opusbin0 -> 10905 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-2ch-44100-libopus.webmbin0 -> 11413 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-2ch-44100-libvorbis.oggbin0 -> 5478 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-2ch-44100-libvorbis.webmbin0 -> 6033 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-2ch-44100.wavbin0 -> 88244 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-2ch-48000-aac.aacbin0 -> 8727 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-2ch-48000-aac.mp4bin0 -> 9479 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-2ch-48000-flac.flacbin0 -> 24984 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-2ch-48000-libmp3lame.mp3bin0 -> 8685 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-2ch-48000-libopus.mp4bin0 -> 12247 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-2ch-48000-libopus.opusbin0 -> 11559 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-2ch-48000-libopus.webmbin0 -> 12067 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-2ch-48000-libvorbis.oggbin0 -> 5784 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-2ch-48000-libvorbis.webmbin0 -> 6364 bytes
-rw-r--r--dom/media/webaudio/test/half-a-second-2ch-48000.wavbin0 -> 96044 bytes
-rw-r--r--dom/media/webaudio/test/invalid.txt1
-rw-r--r--dom/media/webaudio/test/invalidContent.flac1
-rw-r--r--dom/media/webaudio/test/layouttest-glue.js18
-rw-r--r--dom/media/webaudio/test/mochitest.ini215
-rw-r--r--dom/media/webaudio/test/mochitest_audio.ini69
-rw-r--r--dom/media/webaudio/test/mochitest_bugs.ini65
-rw-r--r--dom/media/webaudio/test/mochitest_media.ini64
-rw-r--r--dom/media/webaudio/test/nil-packet.oggbin0 -> 9760 bytes
-rw-r--r--dom/media/webaudio/test/noaudio.webmbin0 -> 105755 bytes
-rw-r--r--dom/media/webaudio/test/sine-440-10s.opusbin0 -> 94428 bytes
-rw-r--r--dom/media/webaudio/test/sixteen-frames.mp3bin0 -> 625 bytes
-rw-r--r--dom/media/webaudio/test/small-shot-expected.wavbin0 -> 53036 bytes
-rw-r--r--dom/media/webaudio/test/small-shot-mono-expected.wavbin0 -> 26540 bytes
-rw-r--r--dom/media/webaudio/test/small-shot.mp3bin0 -> 6825 bytes
-rw-r--r--dom/media/webaudio/test/small-shot.oggbin0 -> 6416 bytes
-rw-r--r--dom/media/webaudio/test/sweep-300-330-1sec.opusbin0 -> 8889 bytes
-rw-r--r--dom/media/webaudio/test/test_AudioBuffer.html104
-rw-r--r--dom/media/webaudio/test/test_AudioContext.html23
-rw-r--r--dom/media/webaudio/test/test_AudioContext_disabled.html56
-rw-r--r--dom/media/webaudio/test/test_AudioListener.html26
-rw-r--r--dom/media/webaudio/test/test_AudioNodeDevtoolsAPI.html59
-rw-r--r--dom/media/webaudio/test/test_AudioParamDevtoolsAPI.html49
-rw-r--r--dom/media/webaudio/test/test_OfflineAudioContext.html118
-rw-r--r--dom/media/webaudio/test/test_ScriptProcessorCollected1.html77
-rw-r--r--dom/media/webaudio/test/test_WebAudioMemoryReporting.html54
-rw-r--r--dom/media/webaudio/test/test_analyserNode.html178
-rw-r--r--dom/media/webaudio/test/test_analyserNodeMinimum.html51
-rw-r--r--dom/media/webaudio/test/test_analyserNodeOutput.html43
-rw-r--r--dom/media/webaudio/test/test_analyserNodePassThrough.html47
-rw-r--r--dom/media/webaudio/test/test_analyserNodeWithGain.html47
-rw-r--r--dom/media/webaudio/test/test_analyserScale.html59
-rw-r--r--dom/media/webaudio/test/test_audioBufferSourceNode.html44
-rw-r--r--dom/media/webaudio/test/test_audioBufferSourceNodeDetached.html58
-rw-r--r--dom/media/webaudio/test/test_audioBufferSourceNodeEnded.html36
-rw-r--r--dom/media/webaudio/test/test_audioBufferSourceNodeLazyLoopParam.html47
-rw-r--r--dom/media/webaudio/test/test_audioBufferSourceNodeLoop.html45
-rw-r--r--dom/media/webaudio/test/test_audioBufferSourceNodeLoopStartEnd.html48
-rw-r--r--dom/media/webaudio/test/test_audioBufferSourceNodeLoopStartEndSame.html44
-rw-r--r--dom/media/webaudio/test/test_audioBufferSourceNodeNoStart.html33
-rw-r--r--dom/media/webaudio/test/test_audioBufferSourceNodeNullBuffer.html31
-rw-r--r--dom/media/webaudio/test/test_audioBufferSourceNodeOffset.html55
-rw-r--r--dom/media/webaudio/test/test_audioBufferSourceNodePassThrough.html45
-rw-r--r--dom/media/webaudio/test/test_audioBufferSourceNodeRate.html58
-rw-r--r--dom/media/webaudio/test/test_audioContextGC.html162
-rw-r--r--dom/media/webaudio/test/test_audioContextParams_recordNonDefaultSampleRate.html48
-rw-r--r--dom/media/webaudio/test/test_audioContextParams_sampleRate.html81
-rw-r--r--dom/media/webaudio/test/test_audioContextSuspendResumeClose.html419
-rw-r--r--dom/media/webaudio/test/test_audioDestinationNode.html26
-rw-r--r--dom/media/webaudio/test/test_audioParamChaining.html77
-rw-r--r--dom/media/webaudio/test/test_audioParamExponentialRamp.html58
-rw-r--r--dom/media/webaudio/test/test_audioParamGain.html61
-rw-r--r--dom/media/webaudio/test/test_audioParamLinearRamp.html54
-rw-r--r--dom/media/webaudio/test/test_audioParamSetCurveAtTime.html54
-rw-r--r--dom/media/webaudio/test/test_audioParamSetTargetAtTime.html55
-rw-r--r--dom/media/webaudio/test/test_audioParamSetTargetAtTimeZeroTimeConstant.html58
-rw-r--r--dom/media/webaudio/test/test_audioParamSetValueAtTime.html52
-rw-r--r--dom/media/webaudio/test/test_audioParamTimelineDestinationOffset.html45
-rw-r--r--dom/media/webaudio/test/test_badConnect.html52
-rw-r--r--dom/media/webaudio/test/test_biquadFilterNode.html86
-rw-r--r--dom/media/webaudio/test/test_biquadFilterNodePassThrough.html47
-rw-r--r--dom/media/webaudio/test/test_biquadFilterNodeWithGain.html61
-rw-r--r--dom/media/webaudio/test/test_bug1027864.html74
-rw-r--r--dom/media/webaudio/test/test_bug1056032.html35
-rw-r--r--dom/media/webaudio/test/test_bug1113634.html58
-rw-r--r--dom/media/webaudio/test/test_bug1118372.html46
-rw-r--r--dom/media/webaudio/test/test_bug1255618.html41
-rw-r--r--dom/media/webaudio/test/test_bug1267579.html46
-rw-r--r--dom/media/webaudio/test/test_bug1355798.html30
-rw-r--r--dom/media/webaudio/test/test_bug1447273.html175
-rw-r--r--dom/media/webaudio/test/test_bug808374.html22
-rw-r--r--dom/media/webaudio/test/test_bug827541.html24
-rw-r--r--dom/media/webaudio/test/test_bug839753.html18
-rw-r--r--dom/media/webaudio/test/test_bug845960.html18
-rw-r--r--dom/media/webaudio/test/test_bug856771.html26
-rw-r--r--dom/media/webaudio/test/test_bug866570.html18
-rw-r--r--dom/media/webaudio/test/test_bug866737.html36
-rw-r--r--dom/media/webaudio/test/test_bug867089.html43
-rw-r--r--dom/media/webaudio/test/test_bug867174.html38
-rw-r--r--dom/media/webaudio/test/test_bug873335.html22
-rw-r--r--dom/media/webaudio/test/test_bug875221.html239
-rw-r--r--dom/media/webaudio/test/test_bug875402.html47
-rw-r--r--dom/media/webaudio/test/test_bug894150.html21
-rw-r--r--dom/media/webaudio/test/test_bug956489.html56
-rw-r--r--dom/media/webaudio/test/test_bug964376.html64
-rw-r--r--dom/media/webaudio/test/test_bug966247.html46
-rw-r--r--dom/media/webaudio/test/test_bug972678.html62
-rw-r--r--dom/media/webaudio/test/test_channelMergerNode.html57
-rw-r--r--dom/media/webaudio/test/test_channelMergerNodeWithVolume.html60
-rw-r--r--dom/media/webaudio/test/test_channelSplitterNode.html71
-rw-r--r--dom/media/webaudio/test/test_channelSplitterNodeWithVolume.html76
-rw-r--r--dom/media/webaudio/test/test_convolver-upmixing-1-channel-response.html143
-rw-r--r--dom/media/webaudio/test/test_convolverNode.html31
-rw-r--r--dom/media/webaudio/test/test_convolverNodeChannelCount.html61
-rw-r--r--dom/media/webaudio/test/test_convolverNodeChannelInterpretationChanges.html169
-rw-r--r--dom/media/webaudio/test/test_convolverNodeDelay.html72
-rw-r--r--dom/media/webaudio/test/test_convolverNodeFiniteInfluence.html44
-rw-r--r--dom/media/webaudio/test/test_convolverNodeNormalization.html83
-rw-r--r--dom/media/webaudio/test/test_convolverNodeOOM.html46
-rw-r--r--dom/media/webaudio/test/test_convolverNodePassThrough.html48
-rw-r--r--dom/media/webaudio/test/test_convolverNodeWithGain.html62
-rw-r--r--dom/media/webaudio/test/test_convolverNode_mono_mono.html73
-rw-r--r--dom/media/webaudio/test/test_currentTime.html27
-rw-r--r--dom/media/webaudio/test/test_decodeAudioDataOnDetachedBuffer.html50
-rw-r--r--dom/media/webaudio/test/test_decodeAudioDataPromise.html62
-rw-r--r--dom/media/webaudio/test/test_decodeAudioError.html74
-rw-r--r--dom/media/webaudio/test/test_decodeMultichannel.html75
-rw-r--r--dom/media/webaudio/test/test_decodeOpusTail.html28
-rw-r--r--dom/media/webaudio/test/test_decoderDelay.html144
-rw-r--r--dom/media/webaudio/test/test_delayNode.html101
-rw-r--r--dom/media/webaudio/test/test_delayNodeAtMax.html53
-rw-r--r--dom/media/webaudio/test/test_delayNodeChannelChanges.html98
-rw-r--r--dom/media/webaudio/test/test_delayNodeCycles.html157
-rw-r--r--dom/media/webaudio/test/test_delayNodePassThrough.html53
-rw-r--r--dom/media/webaudio/test/test_delayNodeSmallMaxDelay.html43
-rw-r--r--dom/media/webaudio/test/test_delayNodeTailIncrease.html71
-rw-r--r--dom/media/webaudio/test/test_delayNodeTailWithDisconnect.html95
-rw-r--r--dom/media/webaudio/test/test_delayNodeTailWithGain.html72
-rw-r--r--dom/media/webaudio/test/test_delayNodeTailWithReconnect.html136
-rw-r--r--dom/media/webaudio/test/test_delayNodeWithGain.html54
-rw-r--r--dom/media/webaudio/test/test_delaynode-channel-count-1.html104
-rw-r--r--dom/media/webaudio/test/test_disconnectAll.html51
-rw-r--r--dom/media/webaudio/test/test_disconnectAudioParam.html58
-rw-r--r--dom/media/webaudio/test/test_disconnectAudioParamFromOutput.html67
-rw-r--r--dom/media/webaudio/test/test_disconnectExceptions.html75
-rw-r--r--dom/media/webaudio/test/test_disconnectFromAudioNode.html55
-rw-r--r--dom/media/webaudio/test/test_disconnectFromAudioNodeAndOutput.html59
-rw-r--r--dom/media/webaudio/test/test_disconnectFromAudioNodeAndOutputAndInput.html57
-rw-r--r--dom/media/webaudio/test/test_disconnectFromAudioNodeMultipleConnection.html56
-rw-r--r--dom/media/webaudio/test/test_disconnectFromOutput.html54
-rw-r--r--dom/media/webaudio/test/test_dynamicsCompressorNode.html68
-rw-r--r--dom/media/webaudio/test/test_dynamicsCompressorNodePassThrough.html47
-rw-r--r--dom/media/webaudio/test/test_dynamicsCompressorNodeWithGain.html51
-rw-r--r--dom/media/webaudio/test/test_event_listener_leaks.html47
-rw-r--r--dom/media/webaudio/test/test_gainNode.html72
-rw-r--r--dom/media/webaudio/test/test_gainNodeInLoop.html48
-rw-r--r--dom/media/webaudio/test/test_gainNodePassThrough.html49
-rw-r--r--dom/media/webaudio/test/test_iirFilterNodePassThrough.html47
-rw-r--r--dom/media/webaudio/test/test_maxChannelCount.html38
-rw-r--r--dom/media/webaudio/test/test_mediaDecoding.html388
-rw-r--r--dom/media/webaudio/test/test_mediaElementAudioSourceNode.html74
-rw-r--r--dom/media/webaudio/test/test_mediaElementAudioSourceNodeCrossOrigin.html94
-rw-r--r--dom/media/webaudio/test/test_mediaElementAudioSourceNodeFidelity.html137
-rw-r--r--dom/media/webaudio/test/test_mediaElementAudioSourceNodePassThrough.html66
-rw-r--r--dom/media/webaudio/test/test_mediaElementAudioSourceNodeVideo.html70
-rw-r--r--dom/media/webaudio/test/test_mediaStreamAudioDestinationNode.html50
-rw-r--r--dom/media/webaudio/test/test_mediaStreamAudioSourceNode.html50
-rw-r--r--dom/media/webaudio/test/test_mediaStreamAudioSourceNodeCrossOrigin.html60
-rw-r--r--dom/media/webaudio/test/test_mediaStreamAudioSourceNodeNoGC.html116
-rw-r--r--dom/media/webaudio/test/test_mediaStreamAudioSourceNodePassThrough.html55
-rw-r--r--dom/media/webaudio/test/test_mediaStreamAudioSourceNodeResampling.html74
-rw-r--r--dom/media/webaudio/test/test_mediaStreamTrackAudioSourceNode.html54
-rw-r--r--dom/media/webaudio/test/test_mediaStreamTrackAudioSourceNodeCrossOrigin.html53
-rw-r--r--dom/media/webaudio/test/test_mediaStreamTrackAudioSourceNodeVideo.html27
-rw-r--r--dom/media/webaudio/test/test_mixingRules.html402
-rw-r--r--dom/media/webaudio/test/test_nodeCreationDocumentGone.html34
-rw-r--r--dom/media/webaudio/test/test_nodeToParamConnection.html60
-rw-r--r--dom/media/webaudio/test/test_notAllowedToStartAudioContextGC.html57
-rw-r--r--dom/media/webaudio/test/test_offlineDestinationChannelCountLess.html42
-rw-r--r--dom/media/webaudio/test/test_offlineDestinationChannelCountMore.html46
-rw-r--r--dom/media/webaudio/test/test_oscillatorNode.html60
-rw-r--r--dom/media/webaudio/test/test_oscillatorNode2.html53
-rw-r--r--dom/media/webaudio/test/test_oscillatorNodeNegativeFrequency.html50
-rw-r--r--dom/media/webaudio/test/test_oscillatorNodePassThrough.html43
-rw-r--r--dom/media/webaudio/test/test_oscillatorNodeStart.html38
-rw-r--r--dom/media/webaudio/test/test_oscillatorTypeChange.html58
-rw-r--r--dom/media/webaudio/test/test_pannerNode.html71
-rw-r--r--dom/media/webaudio/test/test_pannerNodeAbove.html50
-rw-r--r--dom/media/webaudio/test/test_pannerNodeAtZeroDistance.html149
-rw-r--r--dom/media/webaudio/test/test_pannerNodeChannelCount.html52
-rw-r--r--dom/media/webaudio/test/test_pannerNodeHRTFSymmetry.html107
-rw-r--r--dom/media/webaudio/test/test_pannerNodePassThrough.html53
-rw-r--r--dom/media/webaudio/test/test_pannerNodeTail.html232
-rw-r--r--dom/media/webaudio/test/test_pannerNode_audioparam_distance.html43
-rw-r--r--dom/media/webaudio/test/test_pannerNode_equalPower.html26
-rw-r--r--dom/media/webaudio/test/test_pannerNode_maxDistance.html64
-rw-r--r--dom/media/webaudio/test/test_periodicWave.html130
-rw-r--r--dom/media/webaudio/test/test_periodicWaveBandLimiting.html86
-rw-r--r--dom/media/webaudio/test/test_periodicWaveDisableNormalization.html98
-rw-r--r--dom/media/webaudio/test/test_retrospective-exponentialRampToValueAtTime.html51
-rw-r--r--dom/media/webaudio/test/test_retrospective-linearRampToValueAtTime.html51
-rw-r--r--dom/media/webaudio/test/test_retrospective-setTargetAtTime.html51
-rw-r--r--dom/media/webaudio/test/test_retrospective-setValueAtTime.html54
-rw-r--r--dom/media/webaudio/test/test_retrospective-setValueCurveAtTime.html49
-rw-r--r--dom/media/webaudio/test/test_scriptProcessorNode.html132
-rw-r--r--dom/media/webaudio/test/test_scriptProcessorNodeChannelCount.html80
-rw-r--r--dom/media/webaudio/test/test_scriptProcessorNodeNotConnected.html34
-rw-r--r--dom/media/webaudio/test/test_scriptProcessorNodePassThrough.html103
-rw-r--r--dom/media/webaudio/test/test_scriptProcessorNodeZeroInputOutput.html39
-rw-r--r--dom/media/webaudio/test/test_scriptProcessorNode_playbackTime1.html52
-rw-r--r--dom/media/webaudio/test/test_sequentialBufferSourceWithResampling.html72
-rw-r--r--dom/media/webaudio/test/test_setValueCurveWithNonFiniteElements.html60
-rw-r--r--dom/media/webaudio/test/test_singleSourceDest.html70
-rw-r--r--dom/media/webaudio/test/test_slowStart.html48
-rw-r--r--dom/media/webaudio/test/test_stereoPannerNode.html295
-rw-r--r--dom/media/webaudio/test/test_stereoPannerNodePassThrough.html47
-rw-r--r--dom/media/webaudio/test/test_stereoPanningWithGain.html49
-rw-r--r--dom/media/webaudio/test/test_waveDecoder.html69
-rw-r--r--dom/media/webaudio/test/test_waveShaper.html60
-rw-r--r--dom/media/webaudio/test/test_waveShaperGain.html73
-rw-r--r--dom/media/webaudio/test/test_waveShaperInvalidLengthCurve.html66
-rw-r--r--dom/media/webaudio/test/test_waveShaperNoCurve.html43
-rw-r--r--dom/media/webaudio/test/test_waveShaperPassThrough.html55
-rw-r--r--dom/media/webaudio/test/test_webAudio_muteTab.html95
-rw-r--r--dom/media/webaudio/test/ting-44.1k-1ch.oggbin0 -> 8566 bytes
-rw-r--r--dom/media/webaudio/test/ting-44.1k-1ch.wavbin0 -> 61228 bytes
-rw-r--r--dom/media/webaudio/test/ting-44.1k-2ch.oggbin0 -> 10422 bytes
-rw-r--r--dom/media/webaudio/test/ting-44.1k-2ch.wavbin0 -> 122412 bytes
-rw-r--r--dom/media/webaudio/test/ting-48k-1ch.oggbin0 -> 8680 bytes
-rw-r--r--dom/media/webaudio/test/ting-48k-1ch.wavbin0 -> 66638 bytes
-rw-r--r--dom/media/webaudio/test/ting-48k-2ch.oggbin0 -> 10701 bytes
-rw-r--r--dom/media/webaudio/test/ting-48k-2ch.wavbin0 -> 133232 bytes
-rw-r--r--dom/media/webaudio/test/ting-dualchannel44.1.wavbin0 -> 122412 bytes
-rw-r--r--dom/media/webaudio/test/ting-dualchannel48.wavbin0 -> 122412 bytes
-rw-r--r--dom/media/webaudio/test/webaudio.js319
-rw-r--r--dom/media/webcodecs/VideoColorSpace.cpp48
-rw-r--r--dom/media/webcodecs/VideoColorSpace.h64
-rw-r--r--dom/media/webcodecs/VideoFrame.cpp2391
-rw-r--r--dom/media/webcodecs/VideoFrame.h248
-rw-r--r--dom/media/webcodecs/moz.build19
-rw-r--r--dom/media/webcodecs/test/mochitest.ini7
-rw-r--r--dom/media/webcodecs/test/test_videoFrame_mismatched_codedSize.html30
-rw-r--r--dom/media/webm/EbmlComposer.cpp185
-rw-r--r--dom/media/webm/EbmlComposer.h81
-rw-r--r--dom/media/webm/NesteggPacketHolder.h135
-rw-r--r--dom/media/webm/WebMBufferedParser.cpp676
-rw-r--r--dom/media/webm/WebMBufferedParser.h309
-rw-r--r--dom/media/webm/WebMDecoder.cpp124
-rw-r--r--dom/media/webm/WebMDecoder.h35
-rw-r--r--dom/media/webm/WebMDemuxer.cpp1345
-rw-r--r--dom/media/webm/WebMDemuxer.h288
-rw-r--r--dom/media/webm/WebMWriter.cpp112
-rw-r--r--dom/media/webm/WebMWriter.h69
-rw-r--r--dom/media/webm/moz.build28
-rw-r--r--dom/media/webrtc/CubebDeviceEnumerator.cpp334
-rw-r--r--dom/media/webrtc/CubebDeviceEnumerator.h87
-rw-r--r--dom/media/webrtc/MediaEngine.h66
-rw-r--r--dom/media/webrtc/MediaEngineFake.cpp653
-rw-r--r--dom/media/webrtc/MediaEngineFake.h40
-rw-r--r--dom/media/webrtc/MediaEnginePrefs.h101
-rw-r--r--dom/media/webrtc/MediaEngineRemoteVideoSource.cpp907
-rw-r--r--dom/media/webrtc/MediaEngineRemoteVideoSource.h242
-rw-r--r--dom/media/webrtc/MediaEngineSource.cpp69
-rw-r--r--dom/media/webrtc/MediaEngineSource.h255
-rw-r--r--dom/media/webrtc/MediaEngineWebRTC.cpp299
-rw-r--r--dom/media/webrtc/MediaEngineWebRTC.h53
-rw-r--r--dom/media/webrtc/MediaEngineWebRTCAudio.cpp1329
-rw-r--r--dom/media/webrtc/MediaEngineWebRTCAudio.h295
-rw-r--r--dom/media/webrtc/MediaTrackConstraints.cpp560
-rw-r--r--dom/media/webrtc/MediaTrackConstraints.h371
-rw-r--r--dom/media/webrtc/MediaTransportChild.h38
-rw-r--r--dom/media/webrtc/MediaTransportParent.h69
-rw-r--r--dom/media/webrtc/PMediaTransport.ipdl105
-rw-r--r--dom/media/webrtc/PWebrtcGlobal.ipdl41
-rw-r--r--dom/media/webrtc/PeerIdentity.cpp80
-rw-r--r--dom/media/webrtc/PeerIdentity.h70
-rw-r--r--dom/media/webrtc/RTCCertificate.cpp438
-rw-r--r--dom/media/webrtc/RTCCertificate.h98
-rw-r--r--dom/media/webrtc/RTCIdentityProviderRegistrar.cpp70
-rw-r--r--dom/media/webrtc/RTCIdentityProviderRegistrar.h59
-rw-r--r--dom/media/webrtc/SineWaveGenerator.h58
-rw-r--r--dom/media/webrtc/WebrtcGlobal.h506
-rw-r--r--dom/media/webrtc/WebrtcIPCTraits.h89
-rw-r--r--dom/media/webrtc/common/CandidateInfo.h27
-rw-r--r--dom/media/webrtc/common/CommonTypes.h52
-rw-r--r--dom/media/webrtc/common/EncodingConstraints.h58
-rw-r--r--dom/media/webrtc/common/MediaEngineWrapper.h32
-rw-r--r--dom/media/webrtc/common/NullDeleter.h14
-rw-r--r--dom/media/webrtc/common/NullTransport.h56
-rw-r--r--dom/media/webrtc/common/Wrapper.h157
-rw-r--r--dom/media/webrtc/common/YuvStamper.cpp394
-rw-r--r--dom/media/webrtc/common/YuvStamper.h77
-rw-r--r--dom/media/webrtc/common/browser_logging/CSFLog.cpp85
-rw-r--r--dom/media/webrtc/common/browser_logging/CSFLog.h58
-rw-r--r--dom/media/webrtc/common/browser_logging/WebRtcLog.cpp162
-rw-r--r--dom/media/webrtc/common/browser_logging/WebRtcLog.h17
-rw-r--r--dom/media/webrtc/common/csf_common.h90
-rw-r--r--dom/media/webrtc/common/moz.build23
-rw-r--r--dom/media/webrtc/common/time_profiling/timecard.c112
-rw-r--r--dom/media/webrtc/common/time_profiling/timecard.h74
-rw-r--r--dom/media/webrtc/jsapi/MediaTransportHandler.cpp1727
-rw-r--r--dom/media/webrtc/jsapi/MediaTransportHandler.h167
-rw-r--r--dom/media/webrtc/jsapi/MediaTransportHandlerIPC.cpp414
-rw-r--r--dom/media/webrtc/jsapi/MediaTransportHandlerIPC.h96
-rw-r--r--dom/media/webrtc/jsapi/MediaTransportParent.cpp240
-rw-r--r--dom/media/webrtc/jsapi/PacketDumper.cpp124
-rw-r--r--dom/media/webrtc/jsapi/PacketDumper.h52
-rw-r--r--dom/media/webrtc/jsapi/PeerConnectionCtx.cpp650
-rw-r--r--dom/media/webrtc/jsapi/PeerConnectionCtx.h194
-rw-r--r--dom/media/webrtc/jsapi/PeerConnectionImpl.cpp4640
-rw-r--r--dom/media/webrtc/jsapi/PeerConnectionImpl.h969
-rw-r--r--dom/media/webrtc/jsapi/RTCDTMFSender.cpp159
-rw-r--r--dom/media/webrtc/jsapi/RTCDTMFSender.h78
-rw-r--r--dom/media/webrtc/jsapi/RTCDtlsTransport.cpp69
-rw-r--r--dom/media/webrtc/jsapi/RTCDtlsTransport.h43
-rw-r--r--dom/media/webrtc/jsapi/RTCRtpReceiver.cpp942
-rw-r--r--dom/media/webrtc/jsapi/RTCRtpReceiver.h198
-rw-r--r--dom/media/webrtc/jsapi/RTCRtpSender.cpp1654
-rw-r--r--dom/media/webrtc/jsapi/RTCRtpSender.h260
-rw-r--r--dom/media/webrtc/jsapi/RTCRtpTransceiver.cpp1080
-rw-r--r--dom/media/webrtc/jsapi/RTCRtpTransceiver.h243
-rw-r--r--dom/media/webrtc/jsapi/RTCSctpTransport.cpp53
-rw-r--r--dom/media/webrtc/jsapi/RTCSctpTransport.h65
-rw-r--r--dom/media/webrtc/jsapi/RTCStatsIdGenerator.cpp90
-rw-r--r--dom/media/webrtc/jsapi/RTCStatsIdGenerator.h42
-rw-r--r--dom/media/webrtc/jsapi/RTCStatsReport.cpp213
-rw-r--r--dom/media/webrtc/jsapi/RTCStatsReport.h205
-rw-r--r--dom/media/webrtc/jsapi/RemoteTrackSource.cpp73
-rw-r--r--dom/media/webrtc/jsapi/RemoteTrackSource.h64
-rw-r--r--dom/media/webrtc/jsapi/WebrtcGlobalChild.h42
-rw-r--r--dom/media/webrtc/jsapi/WebrtcGlobalInformation.cpp829
-rw-r--r--dom/media/webrtc/jsapi/WebrtcGlobalInformation.h102
-rw-r--r--dom/media/webrtc/jsapi/WebrtcGlobalParent.h52
-rw-r--r--dom/media/webrtc/jsapi/WebrtcGlobalStatsHistory.cpp282
-rw-r--r--dom/media/webrtc/jsapi/WebrtcGlobalStatsHistory.h88
-rw-r--r--dom/media/webrtc/jsapi/moz.build51
-rw-r--r--dom/media/webrtc/jsep/JsepCodecDescription.h1196
-rw-r--r--dom/media/webrtc/jsep/JsepSession.h287
-rw-r--r--dom/media/webrtc/jsep/JsepSessionImpl.cpp2457
-rw-r--r--dom/media/webrtc/jsep/JsepSessionImpl.h286
-rw-r--r--dom/media/webrtc/jsep/JsepTrack.cpp670
-rw-r--r--dom/media/webrtc/jsep/JsepTrack.h305
-rw-r--r--dom/media/webrtc/jsep/JsepTrackEncoding.h62
-rw-r--r--dom/media/webrtc/jsep/JsepTransceiver.h220
-rw-r--r--dom/media/webrtc/jsep/JsepTransport.h102
-rw-r--r--dom/media/webrtc/jsep/SsrcGenerator.cpp22
-rw-r--r--dom/media/webrtc/jsep/SsrcGenerator.h20
-rw-r--r--dom/media/webrtc/jsep/moz.build18
-rw-r--r--dom/media/webrtc/libwebrtcglue/AudioConduit.cpp975
-rw-r--r--dom/media/webrtc/libwebrtcglue/AudioConduit.h303
-rw-r--r--dom/media/webrtc/libwebrtcglue/CallWorkerThread.h116
-rw-r--r--dom/media/webrtc/libwebrtcglue/CodecConfig.h237
-rw-r--r--dom/media/webrtc/libwebrtcglue/GmpVideoCodec.cpp22
-rw-r--r--dom/media/webrtc/libwebrtcglue/GmpVideoCodec.h27
-rw-r--r--dom/media/webrtc/libwebrtcglue/MediaConduitControl.h77
-rw-r--r--dom/media/webrtc/libwebrtcglue/MediaConduitErrors.h46
-rw-r--r--dom/media/webrtc/libwebrtcglue/MediaConduitInterface.cpp151
-rw-r--r--dom/media/webrtc/libwebrtcglue/MediaConduitInterface.h495
-rw-r--r--dom/media/webrtc/libwebrtcglue/MediaDataCodec.cpp70
-rw-r--r--dom/media/webrtc/libwebrtcglue/MediaDataCodec.h32
-rw-r--r--dom/media/webrtc/libwebrtcglue/RtpRtcpConfig.h24
-rw-r--r--dom/media/webrtc/libwebrtcglue/RunningStat.h48
-rw-r--r--dom/media/webrtc/libwebrtcglue/SystemTime.cpp60
-rw-r--r--dom/media/webrtc/libwebrtcglue/SystemTime.h44
-rw-r--r--dom/media/webrtc/libwebrtcglue/TaskQueueWrapper.h182
-rw-r--r--dom/media/webrtc/libwebrtcglue/VideoConduit.cpp1902
-rw-r--r--dom/media/webrtc/libwebrtcglue/VideoConduit.h494
-rw-r--r--dom/media/webrtc/libwebrtcglue/VideoStreamFactory.cpp387
-rw-r--r--dom/media/webrtc/libwebrtcglue/VideoStreamFactory.h132
-rw-r--r--dom/media/webrtc/libwebrtcglue/WebrtcCallWrapper.cpp105
-rw-r--r--dom/media/webrtc/libwebrtcglue/WebrtcCallWrapper.h114
-rw-r--r--dom/media/webrtc/libwebrtcglue/WebrtcGmpVideoCodec.cpp1043
-rw-r--r--dom/media/webrtc/libwebrtcglue/WebrtcGmpVideoCodec.h507
-rw-r--r--dom/media/webrtc/libwebrtcglue/WebrtcImageBuffer.h53
-rw-r--r--dom/media/webrtc/libwebrtcglue/WebrtcMediaDataDecoderCodec.cpp209
-rw-r--r--dom/media/webrtc/libwebrtcglue/WebrtcMediaDataDecoderCodec.h70
-rw-r--r--dom/media/webrtc/libwebrtcglue/WebrtcMediaDataEncoderCodec.cpp518
-rw-r--r--dom/media/webrtc/libwebrtcglue/WebrtcMediaDataEncoderCodec.h78
-rw-r--r--dom/media/webrtc/libwebrtcglue/WebrtcVideoCodecFactory.cpp139
-rw-r--r--dom/media/webrtc/libwebrtcglue/WebrtcVideoCodecFactory.h124
-rw-r--r--dom/media/webrtc/libwebrtcglue/moz.build35
-rw-r--r--dom/media/webrtc/metrics.yaml358
-rw-r--r--dom/media/webrtc/moz.build132
-rw-r--r--dom/media/webrtc/sdp/HybridSdpParser.cpp88
-rw-r--r--dom/media/webrtc/sdp/HybridSdpParser.h38
-rw-r--r--dom/media/webrtc/sdp/ParsingResultComparer.cpp331
-rw-r--r--dom/media/webrtc/sdp/ParsingResultComparer.h57
-rw-r--r--dom/media/webrtc/sdp/RsdparsaSdp.cpp126
-rw-r--r--dom/media/webrtc/sdp/RsdparsaSdp.h72
-rw-r--r--dom/media/webrtc/sdp/RsdparsaSdpAttributeList.cpp1301
-rw-r--r--dom/media/webrtc/sdp/RsdparsaSdpAttributeList.h157
-rw-r--r--dom/media/webrtc/sdp/RsdparsaSdpGlue.cpp106
-rw-r--r--dom/media/webrtc/sdp/RsdparsaSdpGlue.h36
-rw-r--r--dom/media/webrtc/sdp/RsdparsaSdpInc.h510
-rw-r--r--dom/media/webrtc/sdp/RsdparsaSdpMediaSection.cpp253
-rw-r--r--dom/media/webrtc/sdp/RsdparsaSdpMediaSection.h71
-rw-r--r--dom/media/webrtc/sdp/RsdparsaSdpParser.cpp73
-rw-r--r--dom/media/webrtc/sdp/RsdparsaSdpParser.h34
-rw-r--r--dom/media/webrtc/sdp/Sdp.h166
-rw-r--r--dom/media/webrtc/sdp/SdpAttribute.cpp1562
-rw-r--r--dom/media/webrtc/sdp/SdpAttribute.h1907
-rw-r--r--dom/media/webrtc/sdp/SdpAttributeList.h90
-rw-r--r--dom/media/webrtc/sdp/SdpEnum.h64
-rw-r--r--dom/media/webrtc/sdp/SdpHelper.cpp801
-rw-r--r--dom/media/webrtc/sdp/SdpHelper.h109
-rw-r--r--dom/media/webrtc/sdp/SdpLog.cpp68
-rw-r--r--dom/media/webrtc/sdp/SdpLog.h17
-rw-r--r--dom/media/webrtc/sdp/SdpMediaSection.cpp197
-rw-r--r--dom/media/webrtc/sdp/SdpMediaSection.h317
-rw-r--r--dom/media/webrtc/sdp/SdpParser.h81
-rw-r--r--dom/media/webrtc/sdp/SdpPref.cpp107
-rw-r--r--dom/media/webrtc/sdp/SdpPref.h82
-rw-r--r--dom/media/webrtc/sdp/SdpTelemetry.cpp63
-rw-r--r--dom/media/webrtc/sdp/SdpTelemetry.h43
-rw-r--r--dom/media/webrtc/sdp/SipccSdp.cpp173
-rw-r--r--dom/media/webrtc/sdp/SipccSdp.h82
-rw-r--r--dom/media/webrtc/sdp/SipccSdpAttributeList.cpp1386
-rw-r--r--dom/media/webrtc/sdp/SipccSdpAttributeList.h145
-rw-r--r--dom/media/webrtc/sdp/SipccSdpMediaSection.cpp401
-rw-r--r--dom/media/webrtc/sdp/SipccSdpMediaSection.h101
-rw-r--r--dom/media/webrtc/sdp/SipccSdpParser.cpp88
-rw-r--r--dom/media/webrtc/sdp/SipccSdpParser.h35
-rw-r--r--dom/media/webrtc/sdp/moz.build48
-rw-r--r--dom/media/webrtc/sdp/rsdparsa_capi/Cargo.toml12
-rw-r--r--dom/media/webrtc/sdp/rsdparsa_capi/src/attribute.rs1472
-rw-r--r--dom/media/webrtc/sdp/rsdparsa_capi/src/lib.rs298
-rw-r--r--dom/media/webrtc/sdp/rsdparsa_capi/src/media_section.rs233
-rw-r--r--dom/media/webrtc/sdp/rsdparsa_capi/src/network.rs266
-rw-r--r--dom/media/webrtc/sdp/rsdparsa_capi/src/types.rs199
-rw-r--r--dom/media/webrtc/tests/crashtests/1770075.html8
-rw-r--r--dom/media/webrtc/tests/crashtests/1789908.html25
-rw-r--r--dom/media/webrtc/tests/crashtests/1799168.html16
-rw-r--r--dom/media/webrtc/tests/crashtests/1816708.html21
-rw-r--r--dom/media/webrtc/tests/crashtests/1821477.html16
-rw-r--r--dom/media/webrtc/tests/crashtests/crashtests.list7
-rw-r--r--dom/media/webrtc/tests/fuzztests/moz.build22
-rw-r--r--dom/media/webrtc/tests/fuzztests/sdp_parser_libfuzz.cpp30
-rw-r--r--dom/media/webrtc/tests/mochitests/NetworkPreparationChromeScript.js43
-rw-r--r--dom/media/webrtc/tests/mochitests/addTurnsSelfsignedCert.js32
-rw-r--r--dom/media/webrtc/tests/mochitests/blacksilence.js134
-rw-r--r--dom/media/webrtc/tests/mochitests/dataChannel.js352
-rw-r--r--dom/media/webrtc/tests/mochitests/head.js1445
-rw-r--r--dom/media/webrtc/tests/mochitests/helpers_from_wpt/sdp.js889
-rw-r--r--dom/media/webrtc/tests/mochitests/iceTestUtils.js302
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/identityPcTest.js79
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/idp-bad.js1
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/idp-min.js24
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/idp-redirect-http-trick.js3
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/idp-redirect-http-trick.js^headers^2
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/idp-redirect-http.js3
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/idp-redirect-http.js^headers^2
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-double.js3
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-double.js^headers^2
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-odd-path.js3
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-odd-path.js^headers^2
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/idp-redirect-https.js3
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/idp-redirect-https.js^headers^2
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/idp.js119
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/idp.sjs18
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/login.html31
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/mochitest.ini47
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/test_fingerprints.html91
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/test_getIdentityAssertion.html101
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/test_idpproxy.html178
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/test_loginNeeded.html72
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/test_peerConnection_asymmetricIsolation.html31
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/test_peerConnection_peerIdentity.html21
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/test_setIdentityProvider.html67
-rw-r--r--dom/media/webrtc/tests/mochitests/identity/test_setIdentityProviderWithErrors.html57
-rw-r--r--dom/media/webrtc/tests/mochitests/mediaStreamPlayback.js241
-rw-r--r--dom/media/webrtc/tests/mochitests/mochitest.ini65
-rw-r--r--dom/media/webrtc/tests/mochitests/mochitest_datachannel.ini52
-rw-r--r--dom/media/webrtc/tests/mochitests/mochitest_getusermedia.ini105
-rw-r--r--dom/media/webrtc/tests/mochitests/mochitest_peerconnection.ini311
-rw-r--r--dom/media/webrtc/tests/mochitests/network.js16
-rw-r--r--dom/media/webrtc/tests/mochitests/nonTrickleIce.js97
-rw-r--r--dom/media/webrtc/tests/mochitests/parser_rtp.js131
-rw-r--r--dom/media/webrtc/tests/mochitests/pc.js2495
-rw-r--r--dom/media/webrtc/tests/mochitests/peerconnection_audio_forced_sample_rate.js32
-rw-r--r--dom/media/webrtc/tests/mochitests/sdpUtils.js398
-rw-r--r--dom/media/webrtc/tests/mochitests/simulcast.js232
-rw-r--r--dom/media/webrtc/tests/mochitests/stats.js1596
-rw-r--r--dom/media/webrtc/tests/mochitests/templates.js615
-rw-r--r--dom/media/webrtc/tests/mochitests/test_1488832.html37
-rw-r--r--dom/media/webrtc/tests/mochitests/test_1717318.html26
-rw-r--r--dom/media/webrtc/tests/mochitests/test_a_noOp.html32
-rw-r--r--dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudio.html25
-rw-r--r--dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideo.html26
-rw-r--r--dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideoCombined.html26
-rw-r--r--dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideoNoBundle.html27
-rw-r--r--dom/media/webrtc/tests/mochitests/test_dataChannel_basicDataOnly.html24
-rw-r--r--dom/media/webrtc/tests/mochitests/test_dataChannel_basicVideo.html25
-rw-r--r--dom/media/webrtc/tests/mochitests/test_dataChannel_bug1013809.html27
-rw-r--r--dom/media/webrtc/tests/mochitests/test_dataChannel_dataOnlyBufferedAmountLow.html25
-rw-r--r--dom/media/webrtc/tests/mochitests/test_dataChannel_dtlsVersions.html38
-rw-r--r--dom/media/webrtc/tests/mochitests/test_dataChannel_hostnameObfuscation.html59
-rw-r--r--dom/media/webrtc/tests/mochitests/test_dataChannel_noOffer.html33
-rw-r--r--dom/media/webrtc/tests/mochitests/test_dataChannel_stats.html50
-rw-r--r--dom/media/webrtc/tests/mochitests/test_defaultAudioConstraints.html80
-rw-r--r--dom/media/webrtc/tests/mochitests/test_enumerateDevices.html141
-rw-r--r--dom/media/webrtc/tests/mochitests/test_enumerateDevices_getUserMediaFake.html63
-rw-r--r--dom/media/webrtc/tests/mochitests/test_enumerateDevices_iframe.html28
-rw-r--r--dom/media/webrtc/tests/mochitests/test_enumerateDevices_iframe_pre_gum.html22
-rw-r--r--dom/media/webrtc/tests/mochitests/test_enumerateDevices_legacy.html147
-rw-r--r--dom/media/webrtc/tests/mochitests/test_enumerateDevices_navigation.html54
-rw-r--r--dom/media/webrtc/tests/mochitests/test_fingerprinting_resistance.html112
-rw-r--r--dom/media/webrtc/tests/mochitests/test_forceSampleRate.html23
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_GC_MediaStream.html59
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_active_autoplay.html61
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_addTrackRemoveTrack.html169
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_addtrack_removetrack_events.html110
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_audioCapture.html104
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints.html93
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints_concurrentIframes.html157
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints_concurrentStreams.html123
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_basicAudio.html27
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_basicAudio_loopback.html99
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_basicScreenshare.html260
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_basicTabshare.html67
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideo.html30
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideoAudio.html30
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideo_playAfterLoadedmetadata.html42
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_basicWindowshare.html39
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_bug1223696.html54
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_callbacks.html35
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_constraints.html166
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_cubebDisabled.html42
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_cubebDisabledFakeStreams.html43
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_getTrackById.html50
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_gumWithinGum.html38
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_loadedmetadata.html39
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_audio.html116
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_tracks.html179
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_video.html91
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamClone.html258
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamConstructors.html171
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamTrackClone.html170
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_nonDefaultRate.html37
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_peerIdentity.html51
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_permission.html104
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_permission_iframe.html30
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_playAudioTwice.html25
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_playVideoAudioTwice.html26
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_playVideoTwice.html26
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_scarySources.html51
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_spinEventLoop.html28
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_trackCloneCleanup.html32
-rw-r--r--dom/media/webrtc/tests/mochitests/test_getUserMedia_trackEnded.html68
-rw-r--r--dom/media/webrtc/tests/mochitests/test_groupId.html53
-rw-r--r--dom/media/webrtc/tests/mochitests/test_multi_mics.html66
-rw-r--r--dom/media/webrtc/tests/mochitests/test_ondevicechange.html180
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_addAudioTrackToExistingVideoStream.html55
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_addDataChannel.html33
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_addDataChannelNoBundle.html44
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondAudioStream.html45
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondAudioStreamNoBundle.html53
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondVideoStream.html53
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondVideoStreamNoBundle.html60
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_addtrack_removetrack_events.html75
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_answererAddSecondAudioStream.html32
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_audioChannels.html102
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_audioCodecs.html81
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_audioContributingSources.html144
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_audioRenegotiationInactiveAnswer.html69
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_audioSynchronizationSources.html95
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_audioSynchronizationSourcesUnidirectional.html54
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio.html25
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioDynamicPtMissingRtpmap.html36
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelay.html47
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTCP.html42
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTCPWithStun300.html54
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTLS.html41
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayWithStun300.html53
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATSrflx.html44
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNoisyUDPBlock.html41
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioPcmaPcmuOnly.html39
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioRelayPolicy.html83
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioRequireEOC.html35
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVerifyRtpHeaderExtensions.html63
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideo.html24
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoCombined.html24
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoBundle.html25
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoBundleNoRtcpMux.html39
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoRtcpMux.html38
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoTransceivers.html31
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyExtmap.html97
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyExtmapSendonly.html97
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyTooLongMidFails.html47
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio_forced_higher_rate.html19
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio_forced_lower_rate.html19
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicH264Video.html26
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicScreenshare.html56
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicVideo.html23
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicVideoVerifyRtpHeaderExtensions.html82
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_basicWindowshare.html25
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_bug1013809.html25
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_bug1042791.html36
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_bug1227781.html37
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_bug1512281.html47
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_bug1773067.html32
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_bug822674.html26
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_bug825703.html140
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_bug827843.html50
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_bug834153.html36
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_callbacks.html86
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_2d.html81
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_2d_noSSRC.html83
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_webgl.html130
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_capturedVideo.html81
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_certificates.html185
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_checkPacketDumpHook.html107
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_close.html134
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_closeDuringIce.html79
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_codecNegotiationFailure.html111
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_constructedStream.html67
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_disabledVideoPreNegotiation.html45
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_encodingsNegotiation.html85
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_errorCallbacks.html55
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_extmapRenegotiation.html325
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_forwarding_basicAudioVideoCombined.html41
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithSetConfiguration.html450
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithStun300.html269
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithStun300IPv6.html283
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_glean.html488
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_iceFailure.html84
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_insertDTMF.html76
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_localReofferRollback.html44
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_localRollback.html47
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_maxFsConstraint.html112
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_multiple_captureStream_canvas_2d.html115
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleAnswer.html25
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleOffer.html25
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleOfferAnswer.html26
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_nonDefaultRate.html200
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveAudio.html23
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveVideo.html23
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveVideoAudio.html23
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_portRestrictions.html63
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_promiseSendOnly.html61
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_recordReceiveTrack.html101
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_relayOnly.html60
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_remoteReofferRollback.html50
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_remoteRollback.html51
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_removeAudioTrack.html57
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddAudioTrack.html87
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddAudioTrackNoBundle.html76
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddVideoTrack.html98
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddVideoTrackNoBundle.html89
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_removeVideoTrack.html64
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_renderAfterRenegotiation.html89
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_replaceNullTrackThenRenegotiateAudio.html53
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_replaceNullTrackThenRenegotiateVideo.html63
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack.html187
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_camera.html48
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_disabled.html60
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_microphone.html46
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_replaceVideoThenRenegotiate.html74
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_restartIce.html41
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceBadAnswer.html58
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalAndRemoteRollback.html82
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalAndRemoteRollbackNoSubsequentRestart.html77
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalRollback.html76
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalRollbackNoSubsequentRestart.html60
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoBundle.html43
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoBundleNoRtcpMux.html44
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoRtcpMux.html43
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_restrictBandwidthTargetBitrate.html29
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_restrictBandwidthWithTias.html30
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_rtcp_rsize.html81
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_scaleResolution.html119
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_scaleResolution_oldSetParameters.html122
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_sender_and_receiver_stats.html73
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalAnswerInHaveLocalOffer.html34
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalAnswerInStable.html34
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalOfferInHaveRemoteOffer.html31
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters.html470
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_maxFramerate.html63
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_maxFramerate_oldSetParameters.html60
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_oldSetParameters.html86
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_scaleResolutionDownBy.html98
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_scaleResolutionDownBy_oldSetParameters.html96
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteAnswerInHaveRemoteOffer.html34
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteAnswerInStable.html34
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteOfferInHaveLocalOffer.html37
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer.html121
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_lowResFirst.html113
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_lowResFirst_oldSetParameters.html115
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_oldSetParameters.html115
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOddResolution.html183
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOddResolution_oldSetParameters.html172
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer.html109
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_lowResFirst.html109
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_lowResFirst_oldSetParameters.html112
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_oldSetParameters.html112
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_stats.html42
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_stats_jitter.html58
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_stats_oneway.html65
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_stats_relayProtocol.html58
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_stereoFmtpPref.html61
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_syncSetDescription.html53
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_telephoneEventFirst.html56
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_threeUnbundledConnections.html134
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_throwInCallbacks.html83
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_toJSON.html39
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_trackDisabling.html108
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_trackDisabling_clones.html162
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_trackless_sender_stats.html56
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioStreams.html23
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioTracksInOneStream.html37
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreams.html26
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreamsCombined.html70
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreamsCombinedNoBundle.html107
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_twoVideoStreams.html23
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_twoVideoTracksInOneStream.html37
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_verifyAudioAfterRenegotiation.html99
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_verifyDescriptions.html58
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_verifyVideoAfterRenegotiation.html123
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_videoCodecs.html142
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_videoRenegotiationInactiveAnswer.html95
-rw-r--r--dom/media/webrtc/tests/mochitests/test_peerConnection_webAudio.html43
-rw-r--r--dom/media/webrtc/tests/mochitests/test_selftest.html37
-rw-r--r--dom/media/webrtc/tests/mochitests/test_setSinkId.html83
-rw-r--r--dom/media/webrtc/tests/mochitests/test_setSinkId_default_addTrack.html52
-rw-r--r--dom/media/webrtc/tests/mochitests/test_setSinkId_preMutedElement.html100
-rw-r--r--dom/media/webrtc/tests/mochitests/test_unfocused_pref.html49
-rw-r--r--dom/media/webrtc/tests/mochitests/turnConfig.js16
-rw-r--r--dom/media/webrtc/third_party_build/README.md17
-rw-r--r--dom/media/webrtc/third_party_build/build_no_op_commits.sh126
-rw-r--r--dom/media/webrtc/third_party_build/commit-build-file-changes.sh47
-rw-r--r--dom/media/webrtc/third_party_build/default_config_env42
-rw-r--r--dom/media/webrtc/third_party_build/detect_upstream_revert.sh93
-rw-r--r--dom/media/webrtc/third_party_build/elm_arcconfig.patch10
-rw-r--r--dom/media/webrtc/third_party_build/elm_rebase.sh247
-rw-r--r--dom/media/webrtc/third_party_build/extract-for-git.py145
-rw-r--r--dom/media/webrtc/third_party_build/fast-forward-libwebrtc.sh256
-rw-r--r--dom/media/webrtc/third_party_build/fetch_github_repo.py122
-rw-r--r--dom/media/webrtc/third_party_build/filter_git_changes.py72
-rw-r--r--dom/media/webrtc/third_party_build/gn-configs/README.md16
-rw-r--r--dom/media/webrtc/third_party_build/gn-configs/webrtc.json83
-rw-r--r--dom/media/webrtc/third_party_build/lookup_branch_head.py98
-rw-r--r--dom/media/webrtc/third_party_build/loop-ff.sh236
-rwxr-xr-xdom/media/webrtc/third_party_build/make_upstream_revert_noop.sh101
-rw-r--r--dom/media/webrtc/third_party_build/pre-warmed-milestone.cache1
-rw-r--r--dom/media/webrtc/third_party_build/prep_repo.sh93
-rw-r--r--dom/media/webrtc/third_party_build/push_official_branch.sh50
-rw-r--r--dom/media/webrtc/third_party_build/restore_elm_arcconfig.py27
-rw-r--r--dom/media/webrtc/third_party_build/restore_patch_stack.py107
-rw-r--r--dom/media/webrtc/third_party_build/run_operations.py78
-rw-r--r--dom/media/webrtc/third_party_build/save_patch_stack.py142
-rw-r--r--dom/media/webrtc/third_party_build/update_default_config.sh46
-rw-r--r--dom/media/webrtc/third_party_build/use_config_env.sh89
-rw-r--r--dom/media/webrtc/third_party_build/vendor-libwebrtc.py419
-rw-r--r--dom/media/webrtc/third_party_build/verify_vendoring.sh55
-rw-r--r--dom/media/webrtc/third_party_build/webrtc.mozbuild40
-rw-r--r--dom/media/webrtc/third_party_build/write_default_config.py104
-rw-r--r--dom/media/webrtc/transport/README45
-rw-r--r--dom/media/webrtc/transport/SrtpFlow.cpp259
-rw-r--r--dom/media/webrtc/transport/SrtpFlow.h69
-rw-r--r--dom/media/webrtc/transport/WebrtcTCPSocketWrapper.cpp123
-rw-r--r--dom/media/webrtc/transport/WebrtcTCPSocketWrapper.h69
-rw-r--r--dom/media/webrtc/transport/build/moz.build44
-rw-r--r--dom/media/webrtc/transport/common.build94
-rw-r--r--dom/media/webrtc/transport/dtlsidentity.cpp331
-rw-r--r--dom/media/webrtc/transport/dtlsidentity.h101
-rw-r--r--dom/media/webrtc/transport/fuzztest/moz.build31
-rw-r--r--dom/media/webrtc/transport/fuzztest/stun_parser_libfuzz.cpp35
-rw-r--r--dom/media/webrtc/transport/ipc/NrIceStunAddrMessageUtils.h54
-rw-r--r--dom/media/webrtc/transport/ipc/PStunAddrsParams.h33
-rw-r--r--dom/media/webrtc/transport/ipc/PStunAddrsRequest.ipdl35
-rw-r--r--dom/media/webrtc/transport/ipc/PWebrtcTCPSocket.ipdl42
-rw-r--r--dom/media/webrtc/transport/ipc/StunAddrsRequestChild.cpp45
-rw-r--r--dom/media/webrtc/transport/ipc/StunAddrsRequestChild.h64
-rw-r--r--dom/media/webrtc/transport/ipc/StunAddrsRequestParent.cpp262
-rw-r--r--dom/media/webrtc/transport/ipc/StunAddrsRequestParent.h82
-rw-r--r--dom/media/webrtc/transport/ipc/WebrtcProxyConfig.ipdlh23
-rw-r--r--dom/media/webrtc/transport/ipc/WebrtcTCPSocket.cpp785
-rw-r--r--dom/media/webrtc/transport/ipc/WebrtcTCPSocket.h104
-rw-r--r--dom/media/webrtc/transport/ipc/WebrtcTCPSocketCallback.h28
-rw-r--r--dom/media/webrtc/transport/ipc/WebrtcTCPSocketChild.cpp96
-rw-r--r--dom/media/webrtc/transport/ipc/WebrtcTCPSocketChild.h47
-rw-r--r--dom/media/webrtc/transport/ipc/WebrtcTCPSocketLog.cpp11
-rw-r--r--dom/media/webrtc/transport/ipc/WebrtcTCPSocketLog.h20
-rw-r--r--dom/media/webrtc/transport/ipc/WebrtcTCPSocketParent.cpp122
-rw-r--r--dom/media/webrtc/transport/ipc/WebrtcTCPSocketParent.h59
-rw-r--r--dom/media/webrtc/transport/ipc/moz.build54
-rw-r--r--dom/media/webrtc/transport/logging.h65
-rw-r--r--dom/media/webrtc/transport/m_cpp_utils.h25
-rw-r--r--dom/media/webrtc/transport/mdns_service/Cargo.toml14
-rw-r--r--dom/media/webrtc/transport/mdns_service/mdns_service.h28
-rw-r--r--dom/media/webrtc/transport/mdns_service/src/lib.rs843
-rw-r--r--dom/media/webrtc/transport/mediapacket.cpp145
-rw-r--r--dom/media/webrtc/transport/mediapacket.h117
-rw-r--r--dom/media/webrtc/transport/moz.build23
-rw-r--r--dom/media/webrtc/transport/nr_socket_proxy_config.cpp34
-rw-r--r--dom/media/webrtc/transport/nr_socket_proxy_config.h41
-rw-r--r--dom/media/webrtc/transport/nr_socket_prsock.cpp1785
-rw-r--r--dom/media/webrtc/transport/nr_socket_prsock.h320
-rw-r--r--dom/media/webrtc/transport/nr_socket_tcp.cpp310
-rw-r--r--dom/media/webrtc/transport/nr_socket_tcp.h117
-rw-r--r--dom/media/webrtc/transport/nr_timer.cpp256
-rw-r--r--dom/media/webrtc/transport/nricectx.cpp1106
-rw-r--r--dom/media/webrtc/transport/nricectx.h421
-rw-r--r--dom/media/webrtc/transport/nricemediastream.cpp709
-rw-r--r--dom/media/webrtc/transport/nricemediastream.h225
-rw-r--r--dom/media/webrtc/transport/nriceresolver.cpp234
-rw-r--r--dom/media/webrtc/transport/nriceresolver.h119
-rw-r--r--dom/media/webrtc/transport/nriceresolverfake.cpp174
-rw-r--r--dom/media/webrtc/transport/nriceresolverfake.h137
-rw-r--r--dom/media/webrtc/transport/nricestunaddr.cpp93
-rw-r--r--dom/media/webrtc/transport/nricestunaddr.h36
-rw-r--r--dom/media/webrtc/transport/nrinterfaceprioritizer.cpp258
-rw-r--r--dom/media/webrtc/transport/nrinterfaceprioritizer.h17
-rw-r--r--dom/media/webrtc/transport/rlogconnector.cpp186
-rw-r--r--dom/media/webrtc/transport/rlogconnector.h127
-rw-r--r--dom/media/webrtc/transport/runnable_utils.h222
-rw-r--r--dom/media/webrtc/transport/sigslot.h619
-rw-r--r--dom/media/webrtc/transport/simpletokenbucket.cpp60
-rw-r--r--dom/media/webrtc/transport/simpletokenbucket.h54
-rw-r--r--dom/media/webrtc/transport/srtp/README_MOZILLA7
-rw-r--r--dom/media/webrtc/transport/srtp/moz.build8
-rw-r--r--dom/media/webrtc/transport/stun_socket_filter.cpp432
-rw-r--r--dom/media/webrtc/transport/stun_socket_filter.h41
-rw-r--r--dom/media/webrtc/transport/test/TestSyncRunnable.cpp56
-rw-r--r--dom/media/webrtc/transport/test/buffered_stun_socket_unittest.cpp245
-rw-r--r--dom/media/webrtc/transport/test/dummysocket.h217
-rw-r--r--dom/media/webrtc/transport/test/gtest_ringbuffer_dumper.h78
-rw-r--r--dom/media/webrtc/transport/test/gtest_utils.h201
-rw-r--r--dom/media/webrtc/transport/test/ice_unittest.cpp4400
-rw-r--r--dom/media/webrtc/transport/test/moz.build104
-rw-r--r--dom/media/webrtc/transport/test/mtransport_test_utils.h57
-rw-r--r--dom/media/webrtc/transport/test/multi_tcp_socket_unittest.cpp501
-rw-r--r--dom/media/webrtc/transport/test/nrappkit_unittest.cpp123
-rw-r--r--dom/media/webrtc/transport/test/proxy_tunnel_socket_unittest.cpp277
-rw-r--r--dom/media/webrtc/transport/test/rlogconnector_unittest.cpp255
-rw-r--r--dom/media/webrtc/transport/test/runnable_utils_unittest.cpp353
-rw-r--r--dom/media/webrtc/transport/test/sctp_unittest.cpp381
-rw-r--r--dom/media/webrtc/transport/test/simpletokenbucket_unittest.cpp114
-rw-r--r--dom/media/webrtc/transport/test/sockettransportservice_unittest.cpp181
-rw-r--r--dom/media/webrtc/transport/test/stunserver.cpp652
-rw-r--r--dom/media/webrtc/transport/test/stunserver.h123
-rw-r--r--dom/media/webrtc/transport/test/test_nr_socket_ice_unittest.cpp409
-rw-r--r--dom/media/webrtc/transport/test/test_nr_socket_unittest.cpp800
-rw-r--r--dom/media/webrtc/transport/test/transport_unittests.cpp1400
-rw-r--r--dom/media/webrtc/transport/test/turn_unittest.cpp432
-rw-r--r--dom/media/webrtc/transport/test/webrtcproxychannel_unittest.cpp754
-rw-r--r--dom/media/webrtc/transport/test_nr_socket.cpp1135
-rw-r--r--dom/media/webrtc/transport/test_nr_socket.h370
-rw-r--r--dom/media/webrtc/transport/third_party/moz.build39
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/COPYRIGHT36
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/README74
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/moz.yaml117
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/nicer.gyp276
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/non-unified-build.patch40
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/crypto/nr_crypto.c67
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/crypto/nr_crypto.h52
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_candidate.c1052
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_candidate.h124
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_candidate_pair.c689
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_candidate_pair.h101
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_codeword.h41
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_component.c1786
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_component.h111
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_ctx.c1125
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_ctx.h188
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_handler.h84
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_media_stream.c1087
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_media_stream.h146
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_parser.c564
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_peer_ctx.c875
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_peer_ctx.h101
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_reg.h81
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_socket.c404
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_socket.h98
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/net/local_addr.c70
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/net/local_addr.h62
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/net/nr_interface_prioritizer.c88
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/net/nr_interface_prioritizer.h66
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/net/nr_resolver.c85
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/net/nr_resolver.h96
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/net/nr_socket.c187
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/net/nr_socket.h123
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/net/nr_socket_local.h41
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/net/nr_socket_multi_tcp.c642
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/net/nr_socket_multi_tcp.h53
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/net/nr_socket_wrapper.c84
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/net/nr_socket_wrapper.h63
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/net/transport_addr.c559
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/net/transport_addr.h128
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/net/transport_addr_reg.c230
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/net/transport_addr_reg.h46
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs-bsd.c110
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs-bsd.h13
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs-netlink.c285
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs-netlink.h45
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs-win32.c210
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs-win32.h13
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs.c176
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs.h43
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/stun/nr_socket_buffered_stun.c656
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/stun/nr_socket_buffered_stun.h66
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/stun/nr_socket_turn.c195
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/stun/nr_socket_turn.h48
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/stun/stun.h218
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_build.c611
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_build.h147
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_client_ctx.c888
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_client_ctx.h200
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_codec.c1550
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_codec.h78
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_hint.c245
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_hint.h44
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_msg.c364
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_msg.h208
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_proc.c554
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_proc.h53
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_reg.h58
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_server_ctx.c468
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_server_ctx.h80
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_util.c352
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_util.h62
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/stun/turn_client_ctx.c1277
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/stun/turn_client_ctx.h161
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/util/cb_args.c57
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/util/cb_args.h41
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/util/ice_util.c71
-rw-r--r--dom/media/webrtc/transport/third_party/nICEr/src/util/ice_util.h41
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/COPYRIGHT159
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/README133
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/README_MOZILLA21
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/nrappkit.gyp251
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/event/async_timer.h54
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/event/async_wait.h83
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/event/async_wait_int.h62
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/log/r_log.c696
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/log/r_log.h85
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/plugin/nr_plugin.h57
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/port/android/include/android_funcs.h62
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/port/android/include/csi_platform.h55
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/port/android/include/sys/ttycom.h38
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/port/android/port-impl.mk31
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/port/darwin/include/csi_platform.h57
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/port/generic/include/sys/queue.h562
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/port/linux/include/csi_platform.h55
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/port/linux/include/linux_funcs.h62
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/port/linux/include/sys/ttycom.h38
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/port/linux/port-impl.mk31
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/port/win32/include/csi_platform.h107
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/registry/c2ru.c320
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/registry/c2ru.h96
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/registry/registry.c604
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/registry/registry.h154
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/registry/registry_int.h97
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/registry/registry_local.c1168
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/registry/registry_vtbl.h96
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/registry/registrycb.c440
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/share/nr_api.h51
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/share/nr_common.h108
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/share/nr_reg_keys.h167
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/stats/nrstats.h118
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/util/byteorder.c73
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/util/byteorder.h47
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/util/hex.c109
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/util/hex.h47
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/assoc.h90
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/debug.c127
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/debug.h94
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_assoc.c539
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_assoc.h126
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_common.h100
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_crc32.c175
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_crc32.h14
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_data.c248
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_data.h108
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_defaults.h91
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_errors.c136
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_errors.h127
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_includes.h98
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_list.c273
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_list.h106
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_macros.h137
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_memory.c198
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_memory.h101
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_replace.c107
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_thread.h68
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_time.c235
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_time.h109
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_types.h213
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/util/p_buf.c215
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/util/p_buf.h72
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/util/util.c775
-rw-r--r--dom/media/webrtc/transport/third_party/nrappkit/src/util/util.h73
-rw-r--r--dom/media/webrtc/transport/transportflow.cpp74
-rw-r--r--dom/media/webrtc/transport/transportflow.h105
-rw-r--r--dom/media/webrtc/transport/transportlayer.cpp49
-rw-r--r--dom/media/webrtc/transport/transportlayer.h108
-rw-r--r--dom/media/webrtc/transport/transportlayerdtls.cpp1558
-rw-r--r--dom/media/webrtc/transport/transportlayerdtls.h187
-rw-r--r--dom/media/webrtc/transport/transportlayerice.cpp168
-rw-r--r--dom/media/webrtc/transport/transportlayerice.h60
-rw-r--r--dom/media/webrtc/transport/transportlayerlog.cpp48
-rw-r--r--dom/media/webrtc/transport/transportlayerlog.h38
-rw-r--r--dom/media/webrtc/transport/transportlayerloopback.cpp119
-rw-r--r--dom/media/webrtc/transport/transportlayerloopback.h109
-rw-r--r--dom/media/webrtc/transport/transportlayersrtp.cpp222
-rw-r--r--dom/media/webrtc/transport/transportlayersrtp.h43
-rw-r--r--dom/media/webrtc/transportbridge/MediaPipeline.cpp1655
-rw-r--r--dom/media/webrtc/transportbridge/MediaPipeline.h454
-rw-r--r--dom/media/webrtc/transportbridge/MediaPipelineFilter.cpp153
-rw-r--r--dom/media/webrtc/transportbridge/MediaPipelineFilter.h89
-rw-r--r--dom/media/webrtc/transportbridge/RtpLogger.cpp67
-rw-r--r--dom/media/webrtc/transportbridge/RtpLogger.h28
-rw-r--r--dom/media/webrtc/transportbridge/moz.build27
-rw-r--r--dom/media/webspeech/moz.build12
-rw-r--r--dom/media/webspeech/recognition/OnlineSpeechRecognitionService.cpp462
-rw-r--r--dom/media/webspeech/recognition/OnlineSpeechRecognitionService.h132
-rw-r--r--dom/media/webspeech/recognition/SpeechGrammar.cpp57
-rw-r--r--dom/media/webspeech/recognition/SpeechGrammar.h64
-rw-r--r--dom/media/webspeech/recognition/SpeechGrammarList.cpp76
-rw-r--r--dom/media/webspeech/recognition/SpeechGrammarList.h73
-rw-r--r--dom/media/webspeech/recognition/SpeechRecognition.cpp1170
-rw-r--r--dom/media/webspeech/recognition/SpeechRecognition.h314
-rw-r--r--dom/media/webspeech/recognition/SpeechRecognitionAlternative.cpp44
-rw-r--r--dom/media/webspeech/recognition/SpeechRecognitionAlternative.h49
-rw-r--r--dom/media/webspeech/recognition/SpeechRecognitionResult.cpp59
-rw-r--r--dom/media/webspeech/recognition/SpeechRecognitionResult.h54
-rw-r--r--dom/media/webspeech/recognition/SpeechRecognitionResultList.cpp58
-rw-r--r--dom/media/webspeech/recognition/SpeechRecognitionResultList.h53
-rw-r--r--dom/media/webspeech/recognition/SpeechTrackListener.cpp92
-rw-r--r--dom/media/webspeech/recognition/SpeechTrackListener.h50
-rw-r--r--dom/media/webspeech/recognition/endpointer.cc193
-rw-r--r--dom/media/webspeech/recognition/endpointer.h180
-rw-r--r--dom/media/webspeech/recognition/energy_endpointer.cc393
-rw-r--r--dom/media/webspeech/recognition/energy_endpointer.h180
-rw-r--r--dom/media/webspeech/recognition/energy_endpointer_params.cc77
-rw-r--r--dom/media/webspeech/recognition/energy_endpointer_params.h159
-rw-r--r--dom/media/webspeech/recognition/moz.build64
-rw-r--r--dom/media/webspeech/recognition/nsISpeechRecognitionService.idl43
-rw-r--r--dom/media/webspeech/recognition/test/FakeSpeechRecognitionService.cpp118
-rw-r--r--dom/media/webspeech/recognition/test/FakeSpeechRecognitionService.h40
-rw-r--r--dom/media/webspeech/recognition/test/head.js200
-rw-r--r--dom/media/webspeech/recognition/test/hello.oggbin0 -> 11328 bytes
-rw-r--r--dom/media/webspeech/recognition/test/hello.ogg^headers^1
-rw-r--r--dom/media/webspeech/recognition/test/http_requesthandler.sjs85
-rw-r--r--dom/media/webspeech/recognition/test/mochitest.ini35
-rw-r--r--dom/media/webspeech/recognition/test/silence.oggbin0 -> 106941 bytes
-rw-r--r--dom/media/webspeech/recognition/test/silence.ogg^headers^1
-rw-r--r--dom/media/webspeech/recognition/test/sinoid+hello.oggbin0 -> 29514 bytes
-rw-r--r--dom/media/webspeech/recognition/test/sinoid+hello.ogg^headers^1
-rw-r--r--dom/media/webspeech/recognition/test/test_abort.html73
-rw-r--r--dom/media/webspeech/recognition/test/test_audio_capture_error.html42
-rw-r--r--dom/media/webspeech/recognition/test/test_call_start_from_end_handler.html102
-rw-r--r--dom/media/webspeech/recognition/test/test_nested_eventloop.html82
-rw-r--r--dom/media/webspeech/recognition/test/test_online_400_response.html47
-rw-r--r--dom/media/webspeech/recognition/test/test_online_empty_result_handling.html48
-rw-r--r--dom/media/webspeech/recognition/test/test_online_hangup.html47
-rw-r--r--dom/media/webspeech/recognition/test/test_online_http.html89
-rw-r--r--dom/media/webspeech/recognition/test/test_online_http_webkit.html90
-rw-r--r--dom/media/webspeech/recognition/test/test_online_malformed_result_handling.html48
-rw-r--r--dom/media/webspeech/recognition/test/test_preference_enable.html43
-rw-r--r--dom/media/webspeech/recognition/test/test_recognition_service_error.html45
-rw-r--r--dom/media/webspeech/recognition/test/test_success_without_recognition_service.html45
-rw-r--r--dom/media/webspeech/recognition/test/test_timeout.html42
-rw-r--r--dom/media/webspeech/synth/SpeechSynthesis.cpp315
-rw-r--r--dom/media/webspeech/synth/SpeechSynthesis.h88
-rw-r--r--dom/media/webspeech/synth/SpeechSynthesisUtterance.cpp137
-rw-r--r--dom/media/webspeech/synth/SpeechSynthesisUtterance.h115
-rw-r--r--dom/media/webspeech/synth/SpeechSynthesisVoice.cpp72
-rw-r--r--dom/media/webspeech/synth/SpeechSynthesisVoice.h55
-rw-r--r--dom/media/webspeech/synth/android/SpeechSynthesisService.cpp215
-rw-r--r--dom/media/webspeech/synth/android/SpeechSynthesisService.h68
-rw-r--r--dom/media/webspeech/synth/android/components.conf17
-rw-r--r--dom/media/webspeech/synth/android/moz.build19
-rw-r--r--dom/media/webspeech/synth/cocoa/OSXSpeechSynthesizerService.h42
-rw-r--r--dom/media/webspeech/synth/cocoa/OSXSpeechSynthesizerService.mm431
-rw-r--r--dom/media/webspeech/synth/cocoa/components.conf17
-rw-r--r--dom/media/webspeech/synth/cocoa/moz.build15
-rw-r--r--dom/media/webspeech/synth/crashtests/1230428.html32
-rw-r--r--dom/media/webspeech/synth/crashtests/crashtests.list1
-rw-r--r--dom/media/webspeech/synth/ipc/PSpeechSynthesis.ipdl50
-rw-r--r--dom/media/webspeech/synth/ipc/PSpeechSynthesisRequest.ipdl48
-rw-r--r--dom/media/webspeech/synth/ipc/SpeechSynthesisChild.cpp169
-rw-r--r--dom/media/webspeech/synth/ipc/SpeechSynthesisChild.h107
-rw-r--r--dom/media/webspeech/synth/ipc/SpeechSynthesisParent.cpp221
-rw-r--r--dom/media/webspeech/synth/ipc/SpeechSynthesisParent.h102
-rw-r--r--dom/media/webspeech/synth/moz.build65
-rw-r--r--dom/media/webspeech/synth/nsISpeechService.idl143
-rw-r--r--dom/media/webspeech/synth/nsISynthVoiceRegistry.idl77
-rw-r--r--dom/media/webspeech/synth/nsSpeechTask.cpp389
-rw-r--r--dom/media/webspeech/synth/nsSpeechTask.h128
-rw-r--r--dom/media/webspeech/synth/nsSynthVoiceRegistry.cpp762
-rw-r--r--dom/media/webspeech/synth/nsSynthVoiceRegistry.h99
-rw-r--r--dom/media/webspeech/synth/speechd/SpeechDispatcherService.cpp538
-rw-r--r--dom/media/webspeech/synth/speechd/SpeechDispatcherService.h65
-rw-r--r--dom/media/webspeech/synth/speechd/components.conf17
-rw-r--r--dom/media/webspeech/synth/speechd/moz.build15
-rw-r--r--dom/media/webspeech/synth/test/common.js104
-rw-r--r--dom/media/webspeech/synth/test/components.conf17
-rw-r--r--dom/media/webspeech/synth/test/file_bfcache_page1.html18
-rw-r--r--dom/media/webspeech/synth/test/file_bfcache_page2.html14
-rw-r--r--dom/media/webspeech/synth/test/file_global_queue.html69
-rw-r--r--dom/media/webspeech/synth/test/file_global_queue_cancel.html88
-rw-r--r--dom/media/webspeech/synth/test/file_global_queue_pause.html130
-rw-r--r--dom/media/webspeech/synth/test/file_indirect_service_events.html102
-rw-r--r--dom/media/webspeech/synth/test/file_setup.html96
-rw-r--r--dom/media/webspeech/synth/test/file_speech_cancel.html100
-rw-r--r--dom/media/webspeech/synth/test/file_speech_error.html46
-rw-r--r--dom/media/webspeech/synth/test/file_speech_queue.html86
-rw-r--r--dom/media/webspeech/synth/test/file_speech_repeating_utterance.html26
-rw-r--r--dom/media/webspeech/synth/test/file_speech_simple.html53
-rw-r--r--dom/media/webspeech/synth/test/mochitest.ini29
-rw-r--r--dom/media/webspeech/synth/test/nsFakeSynthServices.cpp288
-rw-r--r--dom/media/webspeech/synth/test/nsFakeSynthServices.h42
-rw-r--r--dom/media/webspeech/synth/test/startup/file_voiceschanged.html32
-rw-r--r--dom/media/webspeech/synth/test/startup/mochitest.ini8
-rw-r--r--dom/media/webspeech/synth/test/startup/test_voiceschanged.html32
-rw-r--r--dom/media/webspeech/synth/test/test_bfcache.html46
-rw-r--r--dom/media/webspeech/synth/test/test_global_queue.html35
-rw-r--r--dom/media/webspeech/synth/test/test_global_queue_cancel.html35
-rw-r--r--dom/media/webspeech/synth/test/test_global_queue_pause.html35
-rw-r--r--dom/media/webspeech/synth/test/test_indirect_service_events.html36
-rw-r--r--dom/media/webspeech/synth/test/test_setup.html32
-rw-r--r--dom/media/webspeech/synth/test/test_speech_cancel.html35
-rw-r--r--dom/media/webspeech/synth/test/test_speech_error.html35
-rw-r--r--dom/media/webspeech/synth/test/test_speech_queue.html37
-rw-r--r--dom/media/webspeech/synth/test/test_speech_repeating_utterance.html18
-rw-r--r--dom/media/webspeech/synth/test/test_speech_simple.html34
-rw-r--r--dom/media/webspeech/synth/windows/SapiService.cpp445
-rw-r--r--dom/media/webspeech/synth/windows/SapiService.h57
-rw-r--r--dom/media/webspeech/synth/windows/components.conf17
-rw-r--r--dom/media/webspeech/synth/windows/moz.build17
-rw-r--r--dom/media/webvtt/TextTrack.cpp385
-rw-r--r--dom/media/webvtt/TextTrack.h148
-rw-r--r--dom/media/webvtt/TextTrackCue.cpp260
-rw-r--r--dom/media/webvtt/TextTrackCue.h342
-rw-r--r--dom/media/webvtt/TextTrackCueList.cpp125
-rw-r--r--dom/media/webvtt/TextTrackCueList.h73
-rw-r--r--dom/media/webvtt/TextTrackList.cpp192
-rw-r--r--dom/media/webvtt/TextTrackList.h79
-rw-r--r--dom/media/webvtt/TextTrackRegion.cpp58
-rw-r--r--dom/media/webvtt/TextTrackRegion.h138
-rw-r--r--dom/media/webvtt/WebVTTListener.cpp212
-rw-r--r--dom/media/webvtt/WebVTTListener.h69
-rw-r--r--dom/media/webvtt/WebVTTParserWrapper.sys.mjs56
-rw-r--r--dom/media/webvtt/components.conf14
-rw-r--r--dom/media/webvtt/moz.build52
-rw-r--r--dom/media/webvtt/nsIWebVTTListener.idl37
-rw-r--r--dom/media/webvtt/nsIWebVTTParserWrapper.idl94
-rw-r--r--dom/media/webvtt/package.json6
-rw-r--r--dom/media/webvtt/test/crashtests/1304948.html33
-rw-r--r--dom/media/webvtt/test/crashtests/1319486.html27
-rw-r--r--dom/media/webvtt/test/crashtests/1533909.html17
-rw-r--r--dom/media/webvtt/test/crashtests/882549.html13
-rw-r--r--dom/media/webvtt/test/crashtests/894104.html20
-rw-r--r--dom/media/webvtt/test/crashtests/crashtests.list5
-rw-r--r--dom/media/webvtt/test/mochitest/bad-signature.vtt1
-rw-r--r--dom/media/webvtt/test/mochitest/basic.vtt29
-rw-r--r--dom/media/webvtt/test/mochitest/bug883173.vtt16
-rw-r--r--dom/media/webvtt/test/mochitest/long.vtt8001
-rw-r--r--dom/media/webvtt/test/mochitest/manifest.js27
-rw-r--r--dom/media/webvtt/test/mochitest/mochitest.ini50
-rw-r--r--dom/media/webvtt/test/mochitest/parser.vtt6
-rw-r--r--dom/media/webvtt/test/mochitest/region.vtt6
-rw-r--r--dom/media/webvtt/test/mochitest/sequential.vtt10
-rw-r--r--dom/media/webvtt/test/mochitest/test_bug1018933.html50
-rw-r--r--dom/media/webvtt/test/mochitest/test_bug1242594.html46
-rw-r--r--dom/media/webvtt/test/mochitest/test_bug883173.html39
-rw-r--r--dom/media/webvtt/test/mochitest/test_bug895091.html60
-rw-r--r--dom/media/webvtt/test/mochitest/test_bug957847.html30
-rw-r--r--dom/media/webvtt/test/mochitest/test_testtrack_cors_no_response.html41
-rw-r--r--dom/media/webvtt/test/mochitest/test_texttrack.html158
-rw-r--r--dom/media/webvtt/test/mochitest/test_texttrack_cors_preload_none.html40
-rw-r--r--dom/media/webvtt/test/mochitest/test_texttrack_mode_change_during_loading.html75
-rw-r--r--dom/media/webvtt/test/mochitest/test_texttrack_moz.html60
-rw-r--r--dom/media/webvtt/test/mochitest/test_texttrackcue.html298
-rw-r--r--dom/media/webvtt/test/mochitest/test_texttrackcue_moz.html34
-rw-r--r--dom/media/webvtt/test/mochitest/test_texttrackevents_video.html91
-rw-r--r--dom/media/webvtt/test/mochitest/test_texttracklist.html51
-rw-r--r--dom/media/webvtt/test/mochitest/test_texttracklist_moz.html34
-rw-r--r--dom/media/webvtt/test/mochitest/test_texttrackregion.html57
-rw-r--r--dom/media/webvtt/test/mochitest/test_trackelementevent.html77
-rw-r--r--dom/media/webvtt/test/mochitest/test_trackelementsrc.html53
-rw-r--r--dom/media/webvtt/test/mochitest/test_trackevent.html69
-rw-r--r--dom/media/webvtt/test/mochitest/test_vttparser.html44
-rw-r--r--dom/media/webvtt/test/mochitest/test_webvtt_empty_displaystate.html98
-rw-r--r--dom/media/webvtt/test/mochitest/test_webvtt_event_same_time.html63
-rw-r--r--dom/media/webvtt/test/mochitest/test_webvtt_infinite_processing_loop.html49
-rw-r--r--dom/media/webvtt/test/mochitest/test_webvtt_overlapping_time.html100
-rw-r--r--dom/media/webvtt/test/mochitest/test_webvtt_positionalign.html113
-rw-r--r--dom/media/webvtt/test/mochitest/test_webvtt_seeking.html110
-rw-r--r--dom/media/webvtt/test/mochitest/test_webvtt_update_display_after_adding_or_removing_cue.html93
-rw-r--r--dom/media/webvtt/test/mochitest/vttPositionAlign.vtt86
-rw-r--r--dom/media/webvtt/test/reftest/black.mp4bin0 -> 15036 bytes
-rw-r--r--dom/media/webvtt/test/reftest/cues_time_overlapping.webvtt7
-rw-r--r--dom/media/webvtt/test/reftest/reftest.list3
-rw-r--r--dom/media/webvtt/test/reftest/vtt_overlapping_time-ref.html29
-rw-r--r--dom/media/webvtt/test/reftest/vtt_overlapping_time.html30
-rw-r--r--dom/media/webvtt/test/reftest/vtt_reflow_display-ref.html28
-rw-r--r--dom/media/webvtt/test/reftest/vtt_reflow_display.css33
-rw-r--r--dom/media/webvtt/test/reftest/vtt_reflow_display.html37
-rw-r--r--dom/media/webvtt/test/reftest/vtt_update_display_after_removed_cue.html36
-rw-r--r--dom/media/webvtt/test/reftest/vtt_update_display_after_removed_cue_ref.html6
-rw-r--r--dom/media/webvtt/test/reftest/white.webmbin0 -> 10880 bytes
-rw-r--r--dom/media/webvtt/test/xpcshell/test_parser.js158
-rw-r--r--dom/media/webvtt/test/xpcshell/xpcshell.ini3
-rw-r--r--dom/media/webvtt/update-webvtt.js61
-rw-r--r--dom/media/webvtt/vtt.sys.mjs1663
4513 files changed, 646369 insertions, 0 deletions
diff --git a/dom/media/ADTSDecoder.cpp b/dom/media/ADTSDecoder.cpp
new file mode 100644
index 0000000000..78c272e307
--- /dev/null
+++ b/dom/media/ADTSDecoder.cpp
@@ -0,0 +1,47 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ADTSDecoder.h"
+#include "MediaContainerType.h"
+#include "PDMFactory.h"
+
+namespace mozilla {
+
+/* static */
+bool ADTSDecoder::IsEnabled() {
+ RefPtr<PDMFactory> platform = new PDMFactory();
+ return platform->SupportsMimeType("audio/mp4a-latm"_ns) !=
+ media::DecodeSupport::Unsupported;
+}
+
+/* static */
+bool ADTSDecoder::IsSupportedType(const MediaContainerType& aContainerType) {
+ if (aContainerType.Type() == MEDIAMIMETYPE("audio/aac") ||
+ aContainerType.Type() == MEDIAMIMETYPE("audio/aacp") ||
+ aContainerType.Type() == MEDIAMIMETYPE("audio/x-aac")) {
+ return IsEnabled() && (aContainerType.ExtendedType().Codecs().IsEmpty() ||
+ aContainerType.ExtendedType().Codecs() == "aac");
+ }
+
+ return false;
+}
+
+/* static */
+nsTArray<UniquePtr<TrackInfo>> ADTSDecoder::GetTracksInfo(
+ const MediaContainerType& aType) {
+ nsTArray<UniquePtr<TrackInfo>> tracks;
+ if (!IsSupportedType(aType)) {
+ return tracks;
+ }
+
+ tracks.AppendElement(
+ CreateTrackInfoWithMIMETypeAndContainerTypeExtraParameters(
+ "audio/mp4a-latm"_ns, aType));
+
+ return tracks;
+}
+
+} // namespace mozilla
diff --git a/dom/media/ADTSDecoder.h b/dom/media/ADTSDecoder.h
new file mode 100644
index 0000000000..0260c52de1
--- /dev/null
+++ b/dom/media/ADTSDecoder.h
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef ADTS_DECODER_H_
+#define ADTS_DECODER_H_
+
+#include "mozilla/UniquePtr.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+
+class MediaContainerType;
+class TrackInfo;
+
+class ADTSDecoder {
+ public:
+ // Returns true if the ADTS backend is pref'ed on, and we're running on a
+ // platform that is likely to have decoders for the format.
+ static bool IsEnabled();
+ static bool IsSupportedType(const MediaContainerType& aContainerType);
+ static nsTArray<UniquePtr<TrackInfo>> GetTracksInfo(
+ const MediaContainerType& aType);
+};
+
+} // namespace mozilla
+
+#endif // !ADTS_DECODER_H_
diff --git a/dom/media/ADTSDemuxer.cpp b/dom/media/ADTSDemuxer.cpp
new file mode 100644
index 0000000000..4fb5d58f66
--- /dev/null
+++ b/dom/media/ADTSDemuxer.cpp
@@ -0,0 +1,795 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ADTSDemuxer.h"
+
+#include "TimeUnits.h"
+#include "VideoUtils.h"
+#include "mozilla/UniquePtr.h"
+#include <inttypes.h>
+
+extern mozilla::LazyLogModule gMediaDemuxerLog;
+#define ADTSLOG(msg, ...) \
+ DDMOZ_LOG(gMediaDemuxerLog, LogLevel::Debug, msg, ##__VA_ARGS__)
+#define ADTSLOGV(msg, ...) \
+ DDMOZ_LOG(gMediaDemuxerLog, LogLevel::Verbose, msg, ##__VA_ARGS__)
+
+namespace mozilla {
+namespace adts {
+
+// adts::FrameHeader - Holds the ADTS frame header and its parsing
+// state.
+//
+// ADTS Frame Structure
+//
+// 11111111 1111BCCD EEFFFFGH HHIJKLMM MMMMMMMM MMMOOOOO OOOOOOPP(QQQQQQQQ
+// QQQQQQQQ)
+//
+// Header consists of 7 or 9 bytes(without or with CRC).
+// Letter Length(bits) Description
+// { sync } 12 syncword 0xFFF, all bits must be 1
+// B 1 MPEG Version: 0 for MPEG-4, 1 for MPEG-2
+// C 2 Layer: always 0
+// D 1 protection absent, Warning, set to 1 if there is no
+// CRC and 0 if there is CRC
+// E 2 profile, the MPEG-4 Audio Object Type minus 1
+// F 4 MPEG-4 Sampling Frequency Index (15 is forbidden)
+// H 3 MPEG-4 Channel Configuration (in the case of 0, the
+// channel configuration is sent via an in-band PCE)
+// M 13 frame length, this value must include 7 or 9 bytes of
+// header length: FrameLength =
+// (ProtectionAbsent == 1 ? 7 : 9) + size(AACFrame)
+// O 11 Buffer fullness
+// P 2 Number of AAC frames(RDBs) in ADTS frame minus 1, for
+// maximum compatibility always use 1 AAC frame per ADTS
+// frame
+// Q 16 CRC if protection absent is 0
+class FrameHeader {
+ public:
+ uint32_t mFrameLength;
+ uint32_t mSampleRate;
+ uint32_t mSamples;
+ uint32_t mChannels;
+ uint8_t mObjectType;
+ uint8_t mSamplingIndex;
+ uint8_t mChannelConfig;
+ uint8_t mNumAACFrames;
+ bool mHaveCrc;
+
+ // Returns whether aPtr matches a valid ADTS header sync marker
+ static bool MatchesSync(const uint8_t* aPtr) {
+ return aPtr[0] == 0xFF && (aPtr[1] & 0xF6) == 0xF0;
+ }
+
+ FrameHeader() { Reset(); }
+
+ // Header size
+ uint64_t HeaderSize() const { return (mHaveCrc) ? 9 : 7; }
+
+ bool IsValid() const { return mFrameLength > 0; }
+
+ // Resets the state to allow for a new parsing session.
+ void Reset() { PodZero(this); }
+
+ // Returns whether the byte creates a valid sequence up to this point.
+ bool Parse(const uint8_t* aPtr) {
+ const uint8_t* p = aPtr;
+
+ if (!MatchesSync(p)) {
+ return false;
+ }
+
+ // AAC has 1024 samples per frame per channel.
+ mSamples = 1024;
+
+ mHaveCrc = !(p[1] & 0x01);
+ mObjectType = ((p[2] & 0xC0) >> 6) + 1;
+ mSamplingIndex = (p[2] & 0x3C) >> 2;
+ mChannelConfig = (p[2] & 0x01) << 2 | (p[3] & 0xC0) >> 6;
+ mFrameLength = static_cast<uint32_t>(
+ (p[3] & 0x03) << 11 | (p[4] & 0xFF) << 3 | (p[5] & 0xE0) >> 5);
+ mNumAACFrames = (p[6] & 0x03) + 1;
+
+ static const uint32_t SAMPLE_RATES[16] = {96000, 88200, 64000, 48000, 44100,
+ 32000, 24000, 22050, 16000, 12000,
+ 11025, 8000, 7350};
+ mSampleRate = SAMPLE_RATES[mSamplingIndex];
+
+ MOZ_ASSERT(mChannelConfig < 8);
+ mChannels = (mChannelConfig == 7) ? 8 : mChannelConfig;
+
+ return true;
+ }
+};
+
+// adts::Frame - Frame meta container used to parse and hold a frame
+// header and side info.
+class Frame {
+ public:
+ Frame() : mOffset(0), mHeader() {}
+
+ uint64_t Offset() const { return mOffset; }
+ size_t Length() const {
+ // TODO: If fields are zero'd when invalid, this check wouldn't be
+ // necessary.
+ if (!mHeader.IsValid()) {
+ return 0;
+ }
+
+ return mHeader.mFrameLength;
+ }
+
+ // Returns the offset to the start of frame's raw data.
+ uint64_t PayloadOffset() const { return mOffset + mHeader.HeaderSize(); }
+
+ // Returns the length of the frame's raw data (excluding the header) in bytes.
+ size_t PayloadLength() const {
+ // TODO: If fields are zero'd when invalid, this check wouldn't be
+ // necessary.
+ if (!mHeader.IsValid()) {
+ return 0;
+ }
+
+ return mHeader.mFrameLength - mHeader.HeaderSize();
+ }
+
+ // Returns the parsed frame header.
+ const FrameHeader& Header() const { return mHeader; }
+
+ bool IsValid() const { return mHeader.IsValid(); }
+
+ // Resets the frame header and data.
+ void Reset() {
+ mHeader.Reset();
+ mOffset = 0;
+ }
+
+ // Returns whether the valid
+ bool Parse(uint64_t aOffset, const uint8_t* aStart, const uint8_t* aEnd) {
+ MOZ_ASSERT(aStart && aEnd);
+
+ bool found = false;
+ const uint8_t* ptr = aStart;
+ // Require at least 7 bytes of data at the end of the buffer for the minimum
+ // ADTS frame header.
+ while (ptr < aEnd - 7 && !found) {
+ found = mHeader.Parse(ptr);
+ ptr++;
+ }
+
+ mOffset = aOffset + (static_cast<size_t>(ptr - aStart)) - 1u;
+
+ return found;
+ }
+
+ private:
+ // The offset to the start of the header.
+ uint64_t mOffset;
+
+ // The currently parsed frame header.
+ FrameHeader mHeader;
+};
+
+class FrameParser {
+ public:
+ // Returns the currently parsed frame. Reset via Reset or EndFrameSession.
+ const Frame& CurrentFrame() const { return mFrame; }
+
+ // Returns the first parsed frame. Reset via Reset.
+ const Frame& FirstFrame() const { return mFirstFrame; }
+
+ // Resets the parser. Don't use between frames as first frame data is reset.
+ void Reset() {
+ EndFrameSession();
+ mFirstFrame.Reset();
+ }
+
+ // Clear the last parsed frame to allow for next frame parsing, i.e.:
+ // - sets PrevFrame to CurrentFrame
+ // - resets the CurrentFrame
+ // - resets ID3Header if no valid header was parsed yet
+ void EndFrameSession() { mFrame.Reset(); }
+
+ // Parses contents of given ByteReader for a valid frame header and returns
+ // true if one was found. After returning, the variable passed to
+ // 'aBytesToSkip' holds the amount of bytes to be skipped (if any) in order to
+ // jump across a large ID3v2 tag spanning multiple buffers.
+ bool Parse(uint64_t aOffset, const uint8_t* aStart, const uint8_t* aEnd) {
+ const bool found = mFrame.Parse(aOffset, aStart, aEnd);
+
+ if (mFrame.Length() && !mFirstFrame.Length()) {
+ mFirstFrame = mFrame;
+ }
+
+ return found;
+ }
+
+ private:
+ // We keep the first parsed frame around for static info access, the
+ // previously parsed frame for debugging and the currently parsed frame.
+ Frame mFirstFrame;
+ Frame mFrame;
+};
+
+// Initialize the AAC AudioSpecificConfig.
+// Only handles two-byte version for AAC-LC.
+static void InitAudioSpecificConfig(const Frame& frame,
+ MediaByteBuffer* aBuffer) {
+ const FrameHeader& header = frame.Header();
+ MOZ_ASSERT(header.IsValid());
+
+ int audioObjectType = header.mObjectType;
+ int samplingFrequencyIndex = header.mSamplingIndex;
+ int channelConfig = header.mChannelConfig;
+
+ uint8_t asc[2];
+ asc[0] = (audioObjectType & 0x1F) << 3 | (samplingFrequencyIndex & 0x0E) >> 1;
+ asc[1] = (samplingFrequencyIndex & 0x01) << 7 | (channelConfig & 0x0F) << 3;
+
+ aBuffer->AppendElements(asc, 2);
+}
+
+} // namespace adts
+
+using media::TimeUnit;
+
+// ADTSDemuxer
+
+ADTSDemuxer::ADTSDemuxer(MediaResource* aSource) : mSource(aSource) {
+ DDLINKCHILD("source", aSource);
+}
+
+bool ADTSDemuxer::InitInternal() {
+ if (!mTrackDemuxer) {
+ mTrackDemuxer = new ADTSTrackDemuxer(mSource);
+ DDLINKCHILD("track demuxer", mTrackDemuxer.get());
+ }
+ return mTrackDemuxer->Init();
+}
+
+RefPtr<ADTSDemuxer::InitPromise> ADTSDemuxer::Init() {
+ if (!InitInternal()) {
+ ADTSLOG("Init() failure: waiting for data");
+
+ return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_METADATA_ERR,
+ __func__);
+ }
+
+ ADTSLOG("Init() successful");
+ return InitPromise::CreateAndResolve(NS_OK, __func__);
+}
+
+uint32_t ADTSDemuxer::GetNumberTracks(TrackInfo::TrackType aType) const {
+ return (aType == TrackInfo::kAudioTrack) ? 1 : 0;
+}
+
+already_AddRefed<MediaTrackDemuxer> ADTSDemuxer::GetTrackDemuxer(
+ TrackInfo::TrackType aType, uint32_t aTrackNumber) {
+ if (!mTrackDemuxer) {
+ return nullptr;
+ }
+
+ return RefPtr<ADTSTrackDemuxer>(mTrackDemuxer).forget();
+}
+
+bool ADTSDemuxer::IsSeekable() const {
+ int64_t length = mSource->GetLength();
+ if (length > -1) return true;
+ return false;
+}
+
+// ADTSTrackDemuxer
+ADTSTrackDemuxer::ADTSTrackDemuxer(MediaResource* aSource)
+ : mSource(aSource),
+ mParser(new adts::FrameParser()),
+ mOffset(0),
+ mNumParsedFrames(0),
+ mFrameIndex(0),
+ mTotalFrameLen(0),
+ mSamplesPerFrame(0),
+ mSamplesPerSecond(0),
+ mChannels(0) {
+ DDLINKCHILD("source", aSource);
+ Reset();
+}
+
+ADTSTrackDemuxer::~ADTSTrackDemuxer() { delete mParser; }
+
+bool ADTSTrackDemuxer::Init() {
+ FastSeek(TimeUnit::Zero());
+ // Read the first frame to fetch sample rate and other meta data.
+ RefPtr<MediaRawData> frame(GetNextFrame(FindNextFrame(true)));
+
+ ADTSLOG("Init StreamLength()=%" PRId64 " first-frame-found=%d",
+ StreamLength(), !!frame);
+
+ if (!frame) {
+ return false;
+ }
+
+ // Rewind back to the stream begin to avoid dropping the first frame.
+ FastSeek(TimeUnit::Zero());
+
+ if (!mSamplesPerSecond) {
+ return false;
+ }
+
+ if (!mInfo) {
+ mInfo = MakeUnique<AudioInfo>();
+ }
+
+ mInfo->mRate = mSamplesPerSecond;
+ mInfo->mChannels = mChannels;
+ mInfo->mBitDepth = 16;
+ mInfo->mDuration = Duration();
+
+ // AAC Specific information
+ mInfo->mMimeType = "audio/mp4a-latm";
+
+ // Configure AAC codec-specific values.
+ // For AAC, mProfile and mExtendedProfile contain the audioObjectType from
+ // Table 1.3 -- Audio Profile definition, ISO/IEC 14496-3. Eg. 2 == AAC LC
+ mInfo->mProfile = mInfo->mExtendedProfile =
+ mParser->FirstFrame().Header().mObjectType;
+ AudioCodecSpecificBinaryBlob blob;
+ InitAudioSpecificConfig(mParser->FirstFrame(), blob.mBinaryBlob);
+ mInfo->mCodecSpecificConfig = AudioCodecSpecificVariant{std::move(blob)};
+
+ ADTSLOG("Init mInfo={mRate=%u mChannels=%u mBitDepth=%u mDuration=%" PRId64
+ "}",
+ mInfo->mRate, mInfo->mChannels, mInfo->mBitDepth,
+ mInfo->mDuration.ToMicroseconds());
+
+ // AAC encoder delay can be 2112 (typical value when using Apple AAC encoder),
+ // or 1024 (typical value when encoding using fdk_aac, often via ffmpeg).
+ // See
+ // https://developer.apple.com/library/content/documentation/QuickTime/QTFF/QTFFAppenG/QTFFAppenG.html
+ // In an attempt to not trim valid audio data, and because ADTS doesn't
+ // provide a way to know this pre-roll value, this offets by 1024 frames.
+ mPreRoll = TimeUnit(1024, mSamplesPerSecond);
+ return mChannels;
+}
+
+UniquePtr<TrackInfo> ADTSTrackDemuxer::GetInfo() const {
+ return mInfo->Clone();
+}
+
+RefPtr<ADTSTrackDemuxer::SeekPromise> ADTSTrackDemuxer::Seek(
+ const TimeUnit& aTime) {
+ // Efficiently seek to the position.
+ const TimeUnit time = aTime > mPreRoll ? aTime - mPreRoll : TimeUnit::Zero();
+ FastSeek(time);
+ // Correct seek position by scanning the next frames.
+ const TimeUnit seekTime = ScanUntil(time);
+
+ return SeekPromise::CreateAndResolve(seekTime, __func__);
+}
+
+TimeUnit ADTSTrackDemuxer::FastSeek(const TimeUnit& aTime) {
+ ADTSLOG("FastSeek(%" PRId64 ") avgFrameLen=%f mNumParsedFrames=%" PRIu64
+ " mFrameIndex=%" PRId64 " mOffset=%" PRIu64,
+ aTime.ToMicroseconds(), AverageFrameLength(), mNumParsedFrames,
+ mFrameIndex, mOffset);
+
+ const uint64_t firstFrameOffset = mParser->FirstFrame().Offset();
+ if (!aTime.ToMicroseconds()) {
+ // Quick seek to the beginning of the stream.
+ mOffset = firstFrameOffset;
+ } else if (AverageFrameLength() > 0) {
+ mOffset =
+ firstFrameOffset + FrameIndexFromTime(aTime) * AverageFrameLength();
+ }
+
+ const int64_t streamLength = StreamLength();
+ if (mOffset > firstFrameOffset && streamLength > 0) {
+ mOffset = std::min(static_cast<uint64_t>(streamLength - 1), mOffset);
+ }
+
+ mFrameIndex = FrameIndexFromOffset(mOffset);
+ mParser->EndFrameSession();
+
+ ADTSLOG("FastSeek End avgFrameLen=%f mNumParsedFrames=%" PRIu64
+ " mFrameIndex=%" PRId64 " mFirstFrameOffset=%" PRIu64
+ " mOffset=%" PRIu64 " SL=%" PRIu64 "",
+ AverageFrameLength(), mNumParsedFrames, mFrameIndex, firstFrameOffset,
+ mOffset, streamLength);
+
+ return Duration(mFrameIndex);
+}
+
+TimeUnit ADTSTrackDemuxer::ScanUntil(const TimeUnit& aTime) {
+ ADTSLOG("ScanUntil(%" PRId64 ") avgFrameLen=%f mNumParsedFrames=%" PRIu64
+ " mFrameIndex=%" PRId64 " mOffset=%" PRIu64,
+ aTime.ToMicroseconds(), AverageFrameLength(), mNumParsedFrames,
+ mFrameIndex, mOffset);
+
+ if (!aTime.ToMicroseconds()) {
+ return FastSeek(aTime);
+ }
+
+ if (Duration(mFrameIndex) > aTime) {
+ FastSeek(aTime);
+ }
+
+ while (SkipNextFrame(FindNextFrame()) && Duration(mFrameIndex + 1) < aTime) {
+ ADTSLOGV("ScanUntil* avgFrameLen=%f mNumParsedFrames=%" PRIu64
+ " mFrameIndex=%" PRId64 " mOffset=%" PRIu64 " Duration=%" PRId64,
+ AverageFrameLength(), mNumParsedFrames, mFrameIndex, mOffset,
+ Duration(mFrameIndex + 1).ToMicroseconds());
+ }
+
+ ADTSLOG("ScanUntil End avgFrameLen=%f mNumParsedFrames=%" PRIu64
+ " mFrameIndex=%" PRId64 " mOffset=%" PRIu64,
+ AverageFrameLength(), mNumParsedFrames, mFrameIndex, mOffset);
+
+ return Duration(mFrameIndex);
+}
+
+RefPtr<ADTSTrackDemuxer::SamplesPromise> ADTSTrackDemuxer::GetSamples(
+ int32_t aNumSamples) {
+ ADTSLOGV("GetSamples(%d) Begin mOffset=%" PRIu64 " mNumParsedFrames=%" PRIu64
+ " mFrameIndex=%" PRId64 " mTotalFrameLen=%" PRIu64
+ " mSamplesPerFrame=%d "
+ "mSamplesPerSecond=%d mChannels=%d",
+ aNumSamples, mOffset, mNumParsedFrames, mFrameIndex, mTotalFrameLen,
+ mSamplesPerFrame, mSamplesPerSecond, mChannels);
+
+ MOZ_ASSERT(aNumSamples);
+
+ RefPtr<SamplesHolder> frames = new SamplesHolder();
+
+ while (aNumSamples--) {
+ RefPtr<MediaRawData> frame(GetNextFrame(FindNextFrame()));
+ if (!frame) break;
+ frames->AppendSample(frame);
+ }
+
+ ADTSLOGV(
+ "GetSamples() End mSamples.Size()=%zu aNumSamples=%d mOffset=%" PRIu64
+ " mNumParsedFrames=%" PRIu64 " mFrameIndex=%" PRId64
+ " mTotalFrameLen=%" PRIu64
+ " mSamplesPerFrame=%d mSamplesPerSecond=%d "
+ "mChannels=%d",
+ frames->GetSamples().Length(), aNumSamples, mOffset, mNumParsedFrames,
+ mFrameIndex, mTotalFrameLen, mSamplesPerFrame, mSamplesPerSecond,
+ mChannels);
+
+ if (frames->GetSamples().IsEmpty()) {
+ return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_END_OF_STREAM,
+ __func__);
+ }
+
+ return SamplesPromise::CreateAndResolve(frames, __func__);
+}
+
+void ADTSTrackDemuxer::Reset() {
+ ADTSLOG("Reset()");
+ MOZ_ASSERT(mParser);
+ if (mParser) {
+ mParser->Reset();
+ }
+ FastSeek(TimeUnit::Zero());
+}
+
+RefPtr<ADTSTrackDemuxer::SkipAccessPointPromise>
+ADTSTrackDemuxer::SkipToNextRandomAccessPoint(const TimeUnit& aTimeThreshold) {
+ // Will not be called for audio-only resources.
+ return SkipAccessPointPromise::CreateAndReject(
+ SkipFailureHolder(NS_ERROR_DOM_MEDIA_DEMUXER_ERR, 0), __func__);
+}
+
+int64_t ADTSTrackDemuxer::GetResourceOffset() const { return mOffset; }
+
+media::TimeIntervals ADTSTrackDemuxer::GetBuffered() {
+ auto duration = Duration();
+
+ if (duration.IsInfinite()) {
+ return media::TimeIntervals();
+ }
+
+ AutoPinned<MediaResource> stream(mSource.GetResource());
+ return GetEstimatedBufferedTimeRanges(stream, duration.ToMicroseconds());
+}
+
+int64_t ADTSTrackDemuxer::StreamLength() const { return mSource.GetLength(); }
+
+TimeUnit ADTSTrackDemuxer::Duration() const {
+ if (!mNumParsedFrames) {
+ return TimeUnit::Invalid();
+ }
+
+ const int64_t streamLen = StreamLength();
+ if (streamLen < 0) {
+ // Unknown length, we can't estimate duration, this is probably a live
+ // stream.
+ return TimeUnit::FromInfinity();
+ }
+ const int64_t firstFrameOffset = mParser->FirstFrame().Offset();
+ int64_t numFrames = (streamLen - firstFrameOffset) / AverageFrameLength();
+ return Duration(numFrames);
+}
+
+TimeUnit ADTSTrackDemuxer::Duration(int64_t aNumFrames) const {
+ if (!mSamplesPerSecond) {
+ return TimeUnit::Invalid();
+ }
+
+ return TimeUnit(aNumFrames * mSamplesPerFrame, mSamplesPerSecond);
+}
+
+const adts::Frame& ADTSTrackDemuxer::FindNextFrame(
+ bool findFirstFrame /*= false*/) {
+ static const int BUFFER_SIZE = 4096;
+ static const int MAX_SKIPPED_BYTES = 10 * BUFFER_SIZE;
+
+ ADTSLOGV("FindNext() Begin mOffset=%" PRIu64 " mNumParsedFrames=%" PRIu64
+ " mFrameIndex=%" PRId64 " mTotalFrameLen=%" PRIu64
+ " mSamplesPerFrame=%d mSamplesPerSecond=%d mChannels=%d",
+ mOffset, mNumParsedFrames, mFrameIndex, mTotalFrameLen,
+ mSamplesPerFrame, mSamplesPerSecond, mChannels);
+
+ uint8_t buffer[BUFFER_SIZE];
+ uint32_t read = 0;
+
+ bool foundFrame = false;
+ uint64_t frameHeaderOffset = mOffset;
+
+ // Prepare the parser for the next frame parsing session.
+ mParser->EndFrameSession();
+
+ // Check whether we've found a valid ADTS frame.
+ while (!foundFrame) {
+ if ((read = Read(buffer, frameHeaderOffset, BUFFER_SIZE)) == 0) {
+ ADTSLOG("FindNext() EOS without a frame");
+ break;
+ }
+
+ if (frameHeaderOffset - mOffset > MAX_SKIPPED_BYTES) {
+ ADTSLOG("FindNext() exceeded MAX_SKIPPED_BYTES without a frame");
+ break;
+ }
+
+ const adts::Frame& currentFrame = mParser->CurrentFrame();
+ foundFrame = mParser->Parse(frameHeaderOffset, buffer, buffer + read);
+ if (findFirstFrame && foundFrame) {
+ // Check for sync marker after the found frame, since it's
+ // possible to find sync marker in AAC data. If sync marker
+ // exists after the current frame then we've found a frame
+ // header.
+ uint64_t nextFrameHeaderOffset =
+ currentFrame.Offset() + currentFrame.Length();
+ uint32_t read = Read(buffer, nextFrameHeaderOffset, 2);
+ if (read != 2 || !adts::FrameHeader::MatchesSync(buffer)) {
+ frameHeaderOffset = currentFrame.Offset() + 1;
+ mParser->Reset();
+ foundFrame = false;
+ continue;
+ }
+ }
+
+ if (foundFrame) {
+ break;
+ }
+
+ // Minimum header size is 7 bytes.
+ uint64_t advance = read - 7;
+
+ // Check for offset overflow.
+ if (frameHeaderOffset + advance <= frameHeaderOffset) {
+ break;
+ }
+
+ frameHeaderOffset += advance;
+ }
+
+ if (!foundFrame || !mParser->CurrentFrame().Length()) {
+ ADTSLOG(
+ "FindNext() Exit foundFrame=%d mParser->CurrentFrame().Length()=%zu ",
+ foundFrame, mParser->CurrentFrame().Length());
+ mParser->Reset();
+ return mParser->CurrentFrame();
+ }
+
+ ADTSLOGV("FindNext() End mOffset=%" PRIu64 " mNumParsedFrames=%" PRIu64
+ " mFrameIndex=%" PRId64 " frameHeaderOffset=%" PRId64
+ " mTotalFrameLen=%" PRIu64
+ " mSamplesPerFrame=%d mSamplesPerSecond=%d"
+ " mChannels=%d",
+ mOffset, mNumParsedFrames, mFrameIndex, frameHeaderOffset,
+ mTotalFrameLen, mSamplesPerFrame, mSamplesPerSecond, mChannels);
+
+ return mParser->CurrentFrame();
+}
+
+bool ADTSTrackDemuxer::SkipNextFrame(const adts::Frame& aFrame) {
+ if (!mNumParsedFrames || !aFrame.Length()) {
+ RefPtr<MediaRawData> frame(GetNextFrame(aFrame));
+ return frame;
+ }
+
+ UpdateState(aFrame);
+
+ ADTSLOGV("SkipNext() End mOffset=%" PRIu64 " mNumParsedFrames=%" PRIu64
+ " mFrameIndex=%" PRId64 " mTotalFrameLen=%" PRIu64
+ " mSamplesPerFrame=%d mSamplesPerSecond=%d mChannels=%d",
+ mOffset, mNumParsedFrames, mFrameIndex, mTotalFrameLen,
+ mSamplesPerFrame, mSamplesPerSecond, mChannels);
+
+ return true;
+}
+
+already_AddRefed<MediaRawData> ADTSTrackDemuxer::GetNextFrame(
+ const adts::Frame& aFrame) {
+ ADTSLOG("GetNext() Begin({mOffset=%" PRIu64 " HeaderSize()=%" PRIu64
+ " Length()=%zu})",
+ aFrame.Offset(), aFrame.Header().HeaderSize(),
+ aFrame.PayloadLength());
+ if (!aFrame.IsValid()) return nullptr;
+
+ const int64_t offset = aFrame.PayloadOffset();
+ const uint32_t length = aFrame.PayloadLength();
+
+ RefPtr<MediaRawData> frame = new MediaRawData();
+ frame->mOffset = offset;
+
+ UniquePtr<MediaRawDataWriter> frameWriter(frame->CreateWriter());
+ if (!frameWriter->SetSize(length)) {
+ ADTSLOG("GetNext() Exit failed to allocated media buffer");
+ return nullptr;
+ }
+
+ const uint32_t read = Read(frameWriter->Data(), offset, length);
+ if (read != length) {
+ ADTSLOG("GetNext() Exit read=%u frame->Size()=%zu", read, frame->Size());
+ return nullptr;
+ }
+
+ UpdateState(aFrame);
+
+ TimeUnit rawpts = Duration(mFrameIndex - 1) - mPreRoll;
+ TimeUnit rawDuration = Duration(1);
+ TimeUnit rawend = rawpts + rawDuration;
+
+ frame->mTime = std::max(TimeUnit::Zero(), rawpts);
+ frame->mDuration = Duration(1);
+ frame->mTimecode = frame->mTime;
+ frame->mKeyframe = true;
+
+ // Handle decoder delay. A packet must be trimmed if its pts, adjusted for
+ // decoder delay, is negative. A packet can be trimmed entirely.
+ if (rawpts.IsNegative()) {
+ frame->mDuration = std::max(TimeUnit::Zero(), rawend - frame->mTime);
+ }
+
+ // ADTS frames can have a presentation duration of zero, e.g. when a frame is
+ // part of preroll.
+ MOZ_ASSERT(frame->mDuration.IsPositiveOrZero());
+
+ ADTSLOG("ADTS packet demuxed: pts [%lf, %lf] (duration: %lf)",
+ frame->mTime.ToSeconds(), frame->GetEndTime().ToSeconds(),
+ frame->mDuration.ToSeconds());
+
+ // Indicate original packet information to trim after decoding.
+ if (frame->mDuration != rawDuration) {
+ frame->mOriginalPresentationWindow =
+ Some(media::TimeInterval{rawpts, rawend});
+ ADTSLOG("Total packet time excluding trimming: [%lf, %lf]",
+ rawpts.ToSeconds(), rawend.ToSeconds());
+ }
+
+ ADTSLOGV("GetNext() End mOffset=%" PRIu64 " mNumParsedFrames=%" PRIu64
+ " mFrameIndex=%" PRId64 " mTotalFrameLen=%" PRIu64
+ " mSamplesPerFrame=%d mSamplesPerSecond=%d mChannels=%d",
+ mOffset, mNumParsedFrames, mFrameIndex, mTotalFrameLen,
+ mSamplesPerFrame, mSamplesPerSecond, mChannels);
+
+ return frame.forget();
+}
+
+int64_t ADTSTrackDemuxer::FrameIndexFromOffset(uint64_t aOffset) const {
+ uint64_t frameIndex = 0;
+
+ if (AverageFrameLength() > 0) {
+ frameIndex =
+ (aOffset - mParser->FirstFrame().Offset()) / AverageFrameLength();
+ }
+
+ ADTSLOGV("FrameIndexFromOffset(%" PRId64 ") -> %" PRId64, aOffset,
+ frameIndex);
+ return frameIndex;
+}
+
+int64_t ADTSTrackDemuxer::FrameIndexFromTime(const TimeUnit& aTime) const {
+ int64_t frameIndex = 0;
+ if (mSamplesPerSecond > 0 && mSamplesPerFrame > 0) {
+ frameIndex = aTime.ToSeconds() * mSamplesPerSecond / mSamplesPerFrame - 1;
+ }
+
+ ADTSLOGV("FrameIndexFromOffset(%fs) -> %" PRId64, aTime.ToSeconds(),
+ frameIndex);
+ return std::max<int64_t>(0, frameIndex);
+}
+
+void ADTSTrackDemuxer::UpdateState(const adts::Frame& aFrame) {
+ uint32_t frameLength = aFrame.Length();
+ // Prevent overflow.
+ if (mTotalFrameLen + frameLength < mTotalFrameLen) {
+ // These variables have a linear dependency and are only used to derive the
+ // average frame length.
+ mTotalFrameLen /= 2;
+ mNumParsedFrames /= 2;
+ }
+
+ // Full frame parsed, move offset to its end.
+ mOffset = aFrame.Offset() + frameLength;
+ mTotalFrameLen += frameLength;
+
+ if (!mSamplesPerFrame) {
+ const adts::FrameHeader& header = aFrame.Header();
+ mSamplesPerFrame = header.mSamples;
+ mSamplesPerSecond = header.mSampleRate;
+ mChannels = header.mChannels;
+ }
+
+ ++mNumParsedFrames;
+ ++mFrameIndex;
+ MOZ_ASSERT(mFrameIndex > 0);
+}
+
+uint32_t ADTSTrackDemuxer::Read(uint8_t* aBuffer, int64_t aOffset,
+ int32_t aSize) {
+ ADTSLOGV("ADTSTrackDemuxer::Read(%p %" PRId64 " %d)", aBuffer, aOffset,
+ aSize);
+
+ const int64_t streamLen = StreamLength();
+ if (mInfo && streamLen > 0) {
+ int64_t max = streamLen > aOffset ? streamLen - aOffset : 0;
+ // Prevent blocking reads after successful initialization.
+ aSize = std::min<int64_t>(aSize, max);
+ }
+
+ uint32_t read = 0;
+ ADTSLOGV("ADTSTrackDemuxer::Read -> ReadAt(%d)", aSize);
+ const nsresult rv = mSource.ReadAt(aOffset, reinterpret_cast<char*>(aBuffer),
+ static_cast<uint32_t>(aSize), &read);
+ NS_ENSURE_SUCCESS(rv, 0);
+ return read;
+}
+
+double ADTSTrackDemuxer::AverageFrameLength() const {
+ if (mNumParsedFrames) {
+ return static_cast<double>(mTotalFrameLen) / mNumParsedFrames;
+ }
+
+ return 0.0;
+}
+
+/* static */
+bool ADTSDemuxer::ADTSSniffer(const uint8_t* aData, const uint32_t aLength) {
+ if (aLength < 7) {
+ return false;
+ }
+ if (!adts::FrameHeader::MatchesSync(aData)) {
+ return false;
+ }
+ auto parser = MakeUnique<adts::FrameParser>();
+
+ if (!parser->Parse(0, aData, aData + aLength)) {
+ return false;
+ }
+ const adts::Frame& currentFrame = parser->CurrentFrame();
+ // Check for sync marker after the found frame, since it's
+ // possible to find sync marker in AAC data. If sync marker
+ // exists after the current frame then we've found a frame
+ // header.
+ uint64_t nextFrameHeaderOffset =
+ currentFrame.Offset() + currentFrame.Length();
+ return aLength > nextFrameHeaderOffset &&
+ aLength - nextFrameHeaderOffset >= 2 &&
+ adts::FrameHeader::MatchesSync(aData + nextFrameHeaderOffset);
+}
+
+} // namespace mozilla
diff --git a/dom/media/ADTSDemuxer.h b/dom/media/ADTSDemuxer.h
new file mode 100644
index 0000000000..40ff44898e
--- /dev/null
+++ b/dom/media/ADTSDemuxer.h
@@ -0,0 +1,152 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef ADTS_DEMUXER_H_
+#define ADTS_DEMUXER_H_
+
+#include "mozilla/Attributes.h"
+#include "mozilla/Maybe.h"
+#include "MediaDataDemuxer.h"
+#include "MediaResource.h"
+
+namespace mozilla {
+
+namespace adts {
+class Frame;
+class FrameParser;
+} // namespace adts
+
+class ADTSTrackDemuxer;
+
+DDLoggedTypeDeclNameAndBase(ADTSDemuxer, MediaDataDemuxer);
+
+class ADTSDemuxer : public MediaDataDemuxer,
+ public DecoderDoctorLifeLogger<ADTSDemuxer> {
+ public:
+ // MediaDataDemuxer interface.
+ explicit ADTSDemuxer(MediaResource* aSource);
+ RefPtr<InitPromise> Init() override;
+ uint32_t GetNumberTracks(TrackInfo::TrackType aType) const override;
+ already_AddRefed<MediaTrackDemuxer> GetTrackDemuxer(
+ TrackInfo::TrackType aType, uint32_t aTrackNumber) override;
+ bool IsSeekable() const override;
+
+ // Return true if a valid ADTS frame header could be found.
+ static bool ADTSSniffer(const uint8_t* aData, const uint32_t aLength);
+
+ private:
+ bool InitInternal();
+
+ RefPtr<MediaResource> mSource;
+ RefPtr<ADTSTrackDemuxer> mTrackDemuxer;
+};
+
+DDLoggedTypeNameAndBase(ADTSTrackDemuxer, MediaTrackDemuxer);
+
+class ADTSTrackDemuxer : public MediaTrackDemuxer,
+ public DecoderDoctorLifeLogger<ADTSTrackDemuxer> {
+ public:
+ explicit ADTSTrackDemuxer(MediaResource* aSource);
+
+ // Initializes the track demuxer by reading the first frame for meta data.
+ // Returns initialization success state.
+ bool Init();
+
+ // Returns the total stream length if known, -1 otherwise.
+ int64_t StreamLength() const;
+
+ // Returns the estimated stream duration, or a 0-duration if unknown.
+ media::TimeUnit Duration() const;
+
+ // Returns the estimated duration up to the given frame number,
+ // or a 0-duration if unknown.
+ media::TimeUnit Duration(int64_t aNumFrames) const;
+
+ // MediaTrackDemuxer interface.
+ UniquePtr<TrackInfo> GetInfo() const override;
+ RefPtr<SeekPromise> Seek(const media::TimeUnit& aTime) override;
+ RefPtr<SamplesPromise> GetSamples(int32_t aNumSamples = 1) override;
+ void Reset() override;
+ RefPtr<SkipAccessPointPromise> SkipToNextRandomAccessPoint(
+ const media::TimeUnit& aTimeThreshold) override;
+ int64_t GetResourceOffset() const override;
+ media::TimeIntervals GetBuffered() override;
+
+ private:
+ // Destructor.
+ ~ADTSTrackDemuxer();
+
+ // Fast approximate seeking to given time.
+ media::TimeUnit FastSeek(const media::TimeUnit& aTime);
+
+ // Seeks by scanning the stream up to the given time for more accurate
+ // results.
+ media::TimeUnit ScanUntil(const media::TimeUnit& aTime);
+
+ // Finds the next valid frame and returns its byte range.
+ const adts::Frame& FindNextFrame(bool findFirstFrame = false);
+
+ // Skips the next frame given the provided byte range.
+ bool SkipNextFrame(const adts::Frame& aFrame);
+
+ // Returns the next ADTS frame, if available.
+ already_AddRefed<MediaRawData> GetNextFrame(const adts::Frame& aFrame);
+
+ // Updates post-read meta data.
+ void UpdateState(const adts::Frame& aFrame);
+
+ // Returns the frame index for the given offset.
+ int64_t FrameIndexFromOffset(uint64_t aOffset) const;
+
+ // Returns the frame index for the given time.
+ int64_t FrameIndexFromTime(const media::TimeUnit& aTime) const;
+
+ // Reads aSize bytes into aBuffer from the source starting at aOffset.
+ // Returns the actual size read.
+ uint32_t Read(uint8_t* aBuffer, int64_t aOffset, int32_t aSize);
+
+ // Returns the average frame length derived from the previously parsed frames.
+ double AverageFrameLength() const;
+
+ // The (hopefully) ADTS resource.
+ MediaResourceIndex mSource;
+
+ // ADTS frame parser used to detect frames and extract side info.
+ adts::FrameParser* mParser;
+
+ // Current byte offset in the source stream.
+ uint64_t mOffset;
+
+ // Total parsed frames.
+ uint64_t mNumParsedFrames;
+
+ // Current frame index.
+ int64_t mFrameIndex;
+
+ // Sum of parsed frames' lengths in bytes.
+ uint64_t mTotalFrameLen;
+
+ // Samples per frame metric derived from frame headers or 0 if none available.
+ uint32_t mSamplesPerFrame;
+
+ // Samples per second metric derived from frame headers or 0 if none
+ // available.
+ uint32_t mSamplesPerSecond;
+
+ // Channel count derived from frame headers or 0 if none available.
+ uint32_t mChannels;
+
+ // Audio track config info.
+ UniquePtr<AudioInfo> mInfo;
+
+ // Amount of pre-roll time when seeking.
+ // AAC encoder delay is by default 2112 audio frames.
+ media::TimeUnit mPreRoll;
+};
+
+} // namespace mozilla
+
+#endif // !ADTS_DEMUXER_H_
diff --git a/dom/media/AsyncLogger.h b/dom/media/AsyncLogger.h
new file mode 100644
index 0000000000..adc4101382
--- /dev/null
+++ b/dom/media/AsyncLogger.h
@@ -0,0 +1,305 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* Implementation of an asynchronous lock-free logging system. */
+
+#ifndef mozilla_dom_AsyncLogger_h
+#define mozilla_dom_AsyncLogger_h
+
+#include <atomic>
+#include <thread>
+#include <cinttypes>
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/BaseProfilerMarkerTypes.h"
+#include "mozilla/MathAlgorithms.h"
+#include "mozilla/Sprintf.h"
+#include "mozilla/TimeStamp.h"
+#include "GeckoProfiler.h"
+#include "MPSCQueue.h"
+
+#if defined(_WIN32)
+# include <process.h>
+# define getpid() _getpid()
+#else
+# include <unistd.h>
+#endif
+
+namespace mozilla {
+
+// Allows writing 0-terminated C-strings in a buffer, and returns the start
+// index of the string that's been appended. Automatically truncates the strings
+// as needed if the storage is too small, returning true when that's the case.
+class MOZ_STACK_CLASS StringWriter {
+ public:
+ StringWriter(char* aMemory, size_t aLength)
+ : mMemory(aMemory), mWriteIndex(0), mLength(aLength) {}
+
+ bool AppendCString(const char* aString, size_t* aIndexStart) {
+ *aIndexStart = mWriteIndex;
+ if (!aString) {
+ return false;
+ }
+ size_t toCopy = strlen(aString);
+ bool truncated = false;
+
+ if (toCopy > Available()) {
+ truncated = true;
+ toCopy = Available() - 1;
+ }
+
+ memcpy(&(mMemory[mWriteIndex]), aString, toCopy);
+ mWriteIndex += toCopy;
+ mMemory[mWriteIndex] = 0;
+ mWriteIndex++;
+
+ return truncated;
+ }
+
+ private:
+ size_t Available() {
+ MOZ_ASSERT(mLength > mWriteIndex);
+ return mLength - mWriteIndex;
+ }
+
+ char* mMemory;
+ size_t mWriteIndex;
+ size_t mLength;
+};
+
+const size_t PAYLOAD_TOTAL_SIZE = 2 << 9;
+
+// This class implements a lock-free asynchronous logger, that
+// adds profiler markers.
+// Any thread can use this logger without external synchronization and without
+// being blocked. This log is suitable for use in real-time audio threads.
+// This class uses a thread internally, and must be started and stopped
+// manually.
+// If profiling is disabled, all the calls are no-op and cheap.
+class AsyncLogger {
+ public:
+ enum class TracingPhase : uint8_t { BEGIN, END, COMPLETE };
+
+ const char TRACING_PHASE_STRINGS[3] = {'B', 'E', 'X'};
+
+ struct TextPayload {
+ char mPayload[PAYLOAD_TOTAL_SIZE - MPSC_MSG_RESERVED];
+ };
+
+ // The order of the fields is important here to minimize padding.
+ struct TracePayload {
+#define MEMBERS_EXCEPT_NAME \
+ /* If this marker is of phase B or E (begin or end), this is the time at \
+ * which it was captured. */ \
+ TimeStamp mTimestamp; \
+ /* The thread on which this tracepoint was gathered. */ \
+ ProfilerThreadId mTID; \
+ /* If this marker is of phase X (COMPLETE), this holds the duration of the \
+ * event in microseconds. Else, the value is not used. */ \
+ uint32_t mDurationUs; \
+ /* A trace payload can be either: \
+ * - Begin - this marks the beginning of a temporal region \
+ * - End - this marks the end of a temporal region \
+ * - Complete - this is a timestamp and a length, forming complete a \
+ * temporal region */ \
+ TracingPhase mPhase; \
+ /* Offset at which the comment part of the string starts, in mName */ \
+ uint8_t mCommentStart;
+
+ MEMBERS_EXCEPT_NAME;
+
+ private:
+ // Mock structure, to know where the first character of the name will be.
+ struct MembersWithChar {
+ MEMBERS_EXCEPT_NAME;
+ char c;
+ };
+ static constexpr size_t scRemainingSpaceForName =
+ PAYLOAD_TOTAL_SIZE - offsetof(MembersWithChar, c) -
+ ((MPSC_MSG_RESERVED + alignof(MembersWithChar) - 1) &
+ ~(alignof(MembersWithChar) - 1));
+#undef MEMBERS_EXCEPT_NAME
+
+ public:
+ // An arbitrary string, usually containing a function signature or a
+ // recognizable tag of some sort, to be displayed when analyzing the
+ // profile.
+ char mName[scRemainingSpaceForName];
+ };
+
+ // The goal here is to make it easy on the allocator. We pack a pointer in the
+ // message struct, and we still want to do power of two allocations to
+ // minimize allocator slop.
+ static_assert(sizeof(MPSCQueue<TracePayload>::Message) == PAYLOAD_TOTAL_SIZE,
+ "MPSCQueue internal allocations has an unexpected size.");
+
+ explicit AsyncLogger() : mThread(nullptr), mRunning(false) {}
+
+ void Start() {
+ MOZ_ASSERT(!mRunning, "Double calls to AsyncLogger::Start");
+ mRunning = true;
+ Run();
+ }
+
+ void Stop() {
+ if (mRunning) {
+ mRunning = false;
+ }
+ }
+
+ // Log something that has a beginning and an end
+ void Log(const char* aName, const char* aCategory, const char* aComment,
+ TracingPhase aPhase) {
+ if (!Enabled()) {
+ return;
+ }
+
+ auto* msg = new MPSCQueue<TracePayload>::Message();
+
+ msg->data.mTID = profiler_current_thread_id();
+ msg->data.mPhase = aPhase;
+ msg->data.mTimestamp = TimeStamp::Now();
+ msg->data.mDurationUs = 0; // unused, duration is end - begin
+
+ StringWriter writer(msg->data.mName, ArrayLength(msg->data.mName));
+
+ size_t commentIndex;
+ DebugOnly<bool> truncated = writer.AppendCString(aName, &commentIndex);
+ MOZ_ASSERT(!truncated, "Tracing payload truncated: name");
+
+ if (aComment) {
+ truncated = writer.AppendCString(aComment, &commentIndex);
+ MOZ_ASSERT(!truncated, "Tracing payload truncated: comment");
+ msg->data.mCommentStart = commentIndex;
+ } else {
+ msg->data.mCommentStart = 0;
+ }
+ mMessageQueueProfiler.Push(msg);
+ }
+
+ // Log something that has a beginning and a duration
+ void LogDuration(const char* aName, const char* aCategory, uint64_t aDuration,
+ uint64_t aFrames, uint64_t aSampleRate) {
+ if (Enabled()) {
+ auto* msg = new MPSCQueue<TracePayload>::Message();
+ msg->data.mTID = profiler_current_thread_id();
+ msg->data.mPhase = TracingPhase::COMPLETE;
+ msg->data.mTimestamp = TimeStamp::Now();
+ msg->data.mDurationUs =
+ (static_cast<double>(aFrames) / aSampleRate) * 1e6;
+ size_t len = std::min(strlen(aName), ArrayLength(msg->data.mName));
+ memcpy(msg->data.mName, aName, len);
+ msg->data.mName[len] = 0;
+ mMessageQueueProfiler.Push(msg);
+ }
+ }
+
+ bool Enabled() { return mRunning; }
+
+ private:
+ void Run() {
+ mThread.reset(new std::thread([this]() {
+ AUTO_PROFILER_REGISTER_THREAD("AsyncLogger");
+ while (mRunning) {
+ {
+ struct TracingMarkerWithComment {
+ static constexpr Span<const char> MarkerTypeName() {
+ return MakeStringSpan("Real-Time");
+ }
+ static void StreamJSONMarkerData(
+ baseprofiler::SpliceableJSONWriter& aWriter,
+ const ProfilerString8View& aText) {
+ aWriter.StringProperty("name", aText);
+ }
+ static MarkerSchema MarkerTypeDisplay() {
+ using MS = MarkerSchema;
+ MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable};
+ schema.SetChartLabel("{marker.data.name}");
+ schema.SetTableLabel("{marker.name} - {marker.data.name}");
+ schema.AddKeyLabelFormatSearchable("name", "Comment",
+ MS::Format::String,
+ MS::Searchable::Searchable);
+ return schema;
+ }
+ };
+
+ struct TracingMarker {
+ static constexpr Span<const char> MarkerTypeName() {
+ return MakeStringSpan("Real-time");
+ }
+ static void StreamJSONMarkerData(
+ baseprofiler::SpliceableJSONWriter& aWriter) {}
+ static MarkerSchema MarkerTypeDisplay() {
+ using MS = MarkerSchema;
+ MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable};
+ // Nothing outside the defaults.
+ return schema;
+ }
+ };
+
+ TracePayload message;
+ while (mMessageQueueProfiler.Pop(&message) && mRunning) {
+ if (message.mPhase != TracingPhase::COMPLETE) {
+ if (!message.mCommentStart) {
+ profiler_add_marker(
+ ProfilerString8View::WrapNullTerminatedString(
+ message.mName),
+ geckoprofiler::category::MEDIA_RT,
+ {MarkerThreadId(message.mTID),
+ (message.mPhase == TracingPhase::BEGIN)
+ ? MarkerTiming::IntervalStart(message.mTimestamp)
+ : MarkerTiming::IntervalEnd(message.mTimestamp)},
+ TracingMarker{});
+ } else {
+ profiler_add_marker(
+ ProfilerString8View::WrapNullTerminatedString(
+ message.mName),
+ geckoprofiler::category::MEDIA_RT,
+ {MarkerThreadId(message.mTID),
+ (message.mPhase == TracingPhase::BEGIN)
+ ? MarkerTiming::IntervalStart(message.mTimestamp)
+ : MarkerTiming::IntervalEnd(message.mTimestamp)},
+ TracingMarkerWithComment{},
+ ProfilerString8View::WrapNullTerminatedString(
+ &(message.mName[message.mCommentStart])));
+ }
+ } else {
+ profiler_add_marker(
+ ProfilerString8View::WrapNullTerminatedString(message.mName),
+ geckoprofiler::category::MEDIA_RT,
+ {MarkerThreadId(message.mTID),
+ MarkerTiming::Interval(
+ message.mTimestamp,
+ message.mTimestamp + TimeDuration::FromMicroseconds(
+ message.mDurationUs))},
+ TracingMarker{});
+ }
+ }
+ }
+ Sleep();
+ }
+ }));
+ // cleanup is done via mRunning
+ mThread->detach();
+ }
+
+ uint64_t NowInUs() {
+ static TimeStamp base = TimeStamp::Now();
+ return (TimeStamp::Now() - base).ToMicroseconds();
+ }
+
+ void Sleep() { std::this_thread::sleep_for(std::chrono::milliseconds(10)); }
+
+ std::unique_ptr<std::thread> mThread;
+ MPSCQueue<TracePayload> mMessageQueueProfiler;
+ std::atomic<bool> mRunning;
+};
+
+} // end namespace mozilla
+
+#if defined(_WIN32)
+# undef getpid
+#endif
+
+#endif // mozilla_dom_AsyncLogger_h
diff --git a/dom/media/AudibilityMonitor.h b/dom/media/AudibilityMonitor.h
new file mode 100644
index 0000000000..3206262eab
--- /dev/null
+++ b/dom/media/AudibilityMonitor.h
@@ -0,0 +1,100 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_AUDIBILITYMONITOR_H_
+#define DOM_MEDIA_AUDIBILITYMONITOR_H_
+
+#include <cstdint>
+
+#include "AudioSampleFormat.h"
+#include "WebAudioUtils.h"
+#include "AudioBlock.h"
+#include "MediaData.h"
+
+namespace mozilla {
+
+class AudibilityMonitor {
+ public:
+ // ≈ 20 * log10(pow(2, 12)), noise floor of 12bit audio
+ const float AUDIBILITY_THRESHOLD =
+ dom::WebAudioUtils::ConvertDecibelsToLinear(-72.);
+
+ AudibilityMonitor(uint32_t aSamplerate, float aSilenceDurationSeconds)
+ : mSamplerate(aSamplerate),
+ mSilenceDurationSeconds(aSilenceDurationSeconds),
+ mSilentFramesInARow(0),
+ mEverAudible(false) {}
+
+ void Process(const AudioData* aData) {
+ ProcessInterleaved(aData->Data(), aData->mChannels);
+ }
+
+ void Process(const AudioBlock& aData) {
+ if (aData.IsNull() || aData.IsMuted()) {
+ mSilentFramesInARow += aData.GetDuration();
+ return;
+ }
+ ProcessPlanar(aData.ChannelData<float>(), aData.GetDuration());
+ }
+
+ void ProcessPlanar(const nsTArray<const float*>& aPlanar, TrackTime aFrames) {
+ uint32_t lastFrameAudibleAcrossChannels = 0;
+ for (uint32_t channel = 0; channel < aPlanar.Length(); channel++) {
+ uint32_t lastSampleAudible = 0;
+ for (uint32_t frame = 0; frame < aFrames; frame++) {
+ if (std::fabs(aPlanar[channel][frame]) > AUDIBILITY_THRESHOLD) {
+ mEverAudible = true;
+ mSilentFramesInARow = 0;
+ lastSampleAudible = frame;
+ }
+ }
+ lastFrameAudibleAcrossChannels =
+ std::max(lastFrameAudibleAcrossChannels, lastSampleAudible);
+ }
+ mSilentFramesInARow += aFrames - lastFrameAudibleAcrossChannels - 1;
+ }
+
+ void ProcessInterleaved(const Span<AudioDataValue>& aInterleaved,
+ size_t aChannels) {
+ MOZ_ASSERT(aInterleaved.Length() % aChannels == 0);
+ uint32_t frameCount = aInterleaved.Length() / aChannels;
+ AudioDataValue* samples = aInterleaved.Elements();
+
+ uint32_t readIndex = 0;
+ for (uint32_t i = 0; i < frameCount; i++) {
+ bool atLeastOneAudible = false;
+ for (uint32_t j = 0; j < aChannels; j++) {
+ if (std::fabs(AudioSampleToFloat(samples[readIndex++])) >
+ AUDIBILITY_THRESHOLD) {
+ atLeastOneAudible = true;
+ }
+ }
+ if (atLeastOneAudible) {
+ mSilentFramesInARow = 0;
+ mEverAudible = true;
+ } else {
+ mSilentFramesInARow++;
+ }
+ }
+ }
+
+ // A stream is considered audible if there was audible content in the last
+ // `mSilenceDurationSeconds` seconds, or it has never been audible for now.
+ bool RecentlyAudible() {
+ return mEverAudible && (static_cast<float>(mSilentFramesInARow) /
+ mSamplerate) < mSilenceDurationSeconds;
+ }
+
+ private:
+ const uint32_t mSamplerate;
+ const float mSilenceDurationSeconds;
+ uint64_t mSilentFramesInARow;
+ bool mEverAudible;
+};
+
+}; // namespace mozilla
+
+#endif // DOM_MEDIA_AUDIBILITYMONITOR_H_
diff --git a/dom/media/AudioBufferUtils.h b/dom/media/AudioBufferUtils.h
new file mode 100644
index 0000000000..bb50fe3728
--- /dev/null
+++ b/dom/media/AudioBufferUtils.h
@@ -0,0 +1,208 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_SCRATCHBUFFER_H_
+#define MOZILLA_SCRATCHBUFFER_H_
+
+#include "mozilla/PodOperations.h"
+#include "mozilla/UniquePtr.h"
+#include "nsDebug.h"
+
+#include <algorithm>
+
+namespace mozilla {
+
+/**
+ * The classes in this file provide a interface that uses frames as a unit.
+ * However, they store their offsets in samples (because it's handy for pointer
+ * operations). Those functions can convert between the two units.
+ */
+static inline uint32_t FramesToSamples(uint32_t aChannels, uint32_t aFrames) {
+ return aFrames * aChannels;
+}
+
+static inline uint32_t SamplesToFrames(uint32_t aChannels, uint32_t aSamples) {
+ MOZ_ASSERT(!(aSamples % aChannels), "Frame alignment is wrong.");
+ return aSamples / aChannels;
+}
+
+/**
+ * Class that gets a buffer pointer from an audio callback and provides a safe
+ * interface to manipulate this buffer, and to ensure we are not missing frames
+ * by the end of the callback.
+ */
+template <typename T>
+class AudioCallbackBufferWrapper {
+ public:
+ AudioCallbackBufferWrapper()
+ : mBuffer(nullptr), mSamples(0), mSampleWriteOffset(1), mChannels(0) {}
+
+ explicit AudioCallbackBufferWrapper(uint32_t aChannels)
+ : mBuffer(nullptr),
+ mSamples(0),
+ mSampleWriteOffset(1),
+ mChannels(aChannels)
+
+ {
+ MOZ_ASSERT(aChannels);
+ }
+
+ AudioCallbackBufferWrapper& operator=(
+ const AudioCallbackBufferWrapper& aOther) {
+ MOZ_ASSERT(!aOther.mBuffer,
+ "Don't use this ctor after AudioCallbackDriver::Init");
+ MOZ_ASSERT(aOther.mSamples == 0,
+ "Don't use this ctor after AudioCallbackDriver::Init");
+ MOZ_ASSERT(aOther.mSampleWriteOffset == 1,
+ "Don't use this ctor after AudioCallbackDriver::Init");
+ MOZ_ASSERT(aOther.mChannels != 0);
+
+ mBuffer = nullptr;
+ mSamples = 0;
+ mSampleWriteOffset = 1;
+ mChannels = aOther.mChannels;
+
+ return *this;
+ }
+
+ /**
+ * Set the buffer in this wrapper. This is to be called at the beginning of
+ * the callback.
+ */
+ void SetBuffer(T* aBuffer, uint32_t aFrames) {
+ MOZ_ASSERT(!mBuffer && !mSamples, "SetBuffer called twice.");
+ mBuffer = aBuffer;
+ mSamples = FramesToSamples(mChannels, aFrames);
+ mSampleWriteOffset = 0;
+ }
+
+ /**
+ * Write some frames to the internal buffer. Free space in the buffer should
+ * be check prior to calling this.
+ */
+ void WriteFrames(T* aBuffer, uint32_t aFrames) {
+ MOZ_ASSERT(aFrames <= Available(),
+ "Writing more that we can in the audio buffer.");
+
+ PodCopy(mBuffer + mSampleWriteOffset, aBuffer,
+ FramesToSamples(mChannels, aFrames));
+ mSampleWriteOffset += FramesToSamples(mChannels, aFrames);
+ }
+
+ /**
+ * Number of frames that can be written to the buffer.
+ */
+ uint32_t Available() {
+ return SamplesToFrames(mChannels, mSamples - mSampleWriteOffset);
+ }
+
+ /**
+ * Check that the buffer is completly filled, and reset internal state so this
+ * instance can be reused.
+ */
+ void BufferFilled() {
+ MOZ_ASSERT(Available() == 0, "Frames should have been written");
+ MOZ_ASSERT(mSamples, "Buffer not set.");
+ mSamples = 0;
+ mSampleWriteOffset = 0;
+ mBuffer = nullptr;
+ }
+
+ private:
+ /* This is not an owned pointer, but the pointer passed to use via the audio
+ * callback. */
+ T* mBuffer;
+ /* The number of samples of this audio buffer. */
+ uint32_t mSamples;
+ /* The position at which new samples should be written. We want to return to
+ * the audio callback iff this is equal to mSamples. */
+ uint32_t mSampleWriteOffset;
+ uint32_t mChannels;
+};
+
+/**
+ * This is a class that interfaces with the AudioCallbackBufferWrapper, and is
+ * responsible for storing the excess of data produced by the MediaTrackGraph
+ * because of different rounding constraints, to be used the next time the audio
+ * backend calls back.
+ */
+template <typename T, uint32_t BLOCK_SIZE>
+class SpillBuffer {
+ public:
+ SpillBuffer() : mBuffer(nullptr), mPosition(0), mChannels(0) {}
+
+ explicit SpillBuffer(uint32_t aChannels)
+ : mPosition(0), mChannels(aChannels) {
+ MOZ_ASSERT(aChannels);
+ mBuffer = MakeUnique<T[]>(BLOCK_SIZE * mChannels);
+ PodZero(mBuffer.get(), BLOCK_SIZE * mChannels);
+ }
+
+ SpillBuffer& operator=(SpillBuffer& aOther) {
+ MOZ_ASSERT(aOther.mPosition == 0,
+ "Don't use this ctor after AudioCallbackDriver::Init");
+ MOZ_ASSERT(aOther.mChannels != 0);
+ MOZ_ASSERT(aOther.mBuffer);
+
+ mPosition = aOther.mPosition;
+ mChannels = aOther.mChannels;
+ mBuffer = std::move(aOther.mBuffer);
+
+ return *this;
+ }
+
+ SpillBuffer& operator=(SpillBuffer&& aOther) {
+ return this->operator=(aOther);
+ }
+
+ /* Empty the spill buffer into the buffer of the audio callback. This returns
+ * the number of frames written. */
+ uint32_t Empty(AudioCallbackBufferWrapper<T>& aBuffer) {
+ uint32_t framesToWrite =
+ std::min(aBuffer.Available(), SamplesToFrames(mChannels, mPosition));
+
+ aBuffer.WriteFrames(mBuffer.get(), framesToWrite);
+
+ mPosition -= FramesToSamples(mChannels, framesToWrite);
+ // If we didn't empty the spill buffer for some reason, shift the remaining
+ // data down
+ if (mPosition > 0) {
+ MOZ_ASSERT(FramesToSamples(mChannels, framesToWrite) + mPosition <=
+ BLOCK_SIZE * mChannels);
+ PodMove(mBuffer.get(),
+ mBuffer.get() + FramesToSamples(mChannels, framesToWrite),
+ mPosition);
+ }
+
+ return framesToWrite;
+ }
+ /* Fill the spill buffer from aInput, containing aFrames frames, return the
+ * number of frames written to the spill buffer */
+ uint32_t Fill(T* aInput, uint32_t aFrames) {
+ uint32_t framesToWrite =
+ std::min(aFrames, BLOCK_SIZE - SamplesToFrames(mChannels, mPosition));
+
+ MOZ_ASSERT(FramesToSamples(mChannels, framesToWrite) + mPosition <=
+ BLOCK_SIZE * mChannels);
+ PodCopy(mBuffer.get() + mPosition, aInput,
+ FramesToSamples(mChannels, framesToWrite));
+
+ mPosition += FramesToSamples(mChannels, framesToWrite);
+
+ return framesToWrite;
+ }
+
+ private:
+ /* The spilled data. */
+ UniquePtr<T[]> mBuffer;
+ /* The current write position, in samples, in the buffer when filling, or the
+ * amount of buffer filled when emptying. */
+ uint32_t mPosition;
+ uint32_t mChannels;
+};
+
+} // namespace mozilla
+
+#endif // MOZILLA_SCRATCHBUFFER_H_
diff --git a/dom/media/AudioCaptureTrack.cpp b/dom/media/AudioCaptureTrack.cpp
new file mode 100644
index 0000000000..f30581ee69
--- /dev/null
+++ b/dom/media/AudioCaptureTrack.cpp
@@ -0,0 +1,139 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaTrackGraphImpl.h"
+#include "MediaTrackListener.h"
+#include "mozilla/MathAlgorithms.h"
+#include "mozilla/Unused.h"
+
+#include "AudioSegment.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Attributes.h"
+#include "AudioCaptureTrack.h"
+#include "ImageContainer.h"
+#include "AudioNodeEngine.h"
+#include "AudioNodeTrack.h"
+#include "AudioNodeExternalInputTrack.h"
+#include "webaudio/MediaStreamAudioDestinationNode.h"
+#include <algorithm>
+#include "DOMMediaStream.h"
+
+using namespace mozilla::layers;
+using namespace mozilla::dom;
+using namespace mozilla::gfx;
+
+namespace mozilla {
+
+// We are mixing to mono until PeerConnection can accept stereo
+static const uint32_t MONO = 1;
+
+AudioCaptureTrack::AudioCaptureTrack(TrackRate aRate)
+ : ProcessedMediaTrack(aRate, MediaSegment::AUDIO, new AudioSegment()),
+ mStarted(false) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_COUNT_CTOR(AudioCaptureTrack);
+ mMixer.AddCallback(WrapNotNull(this));
+}
+
+AudioCaptureTrack::~AudioCaptureTrack() {
+ MOZ_COUNT_DTOR(AudioCaptureTrack);
+ mMixer.RemoveCallback(this);
+}
+
+void AudioCaptureTrack::Start() {
+ class Message : public ControlMessage {
+ public:
+ explicit Message(AudioCaptureTrack* aTrack)
+ : ControlMessage(aTrack), mTrack(aTrack) {}
+
+ virtual void Run() { mTrack->mStarted = true; }
+
+ protected:
+ AudioCaptureTrack* mTrack;
+ };
+ GraphImpl()->AppendMessage(MakeUnique<Message>(this));
+}
+
+void AudioCaptureTrack::ProcessInput(GraphTime aFrom, GraphTime aTo,
+ uint32_t aFlags) {
+ if (!mStarted) {
+ return;
+ }
+
+ uint32_t inputCount = mInputs.Length();
+
+ if (mEnded) {
+ return;
+ }
+
+ // If the captured track is connected back to a object on the page (be it an
+ // HTMLMediaElement with a track as source, or an AudioContext), a cycle
+ // situation occur. This can work if it's an AudioContext with at least one
+ // DelayNode, but the MTG will mute the whole cycle otherwise.
+ if (InMutedCycle() || inputCount == 0) {
+ GetData<AudioSegment>()->AppendNullData(aTo - aFrom);
+ } else {
+ // We mix down all the tracks of all inputs, to a stereo track. Everything
+ // is {up,down}-mixed to stereo.
+ mMixer.StartMixing();
+ AudioSegment output;
+ for (uint32_t i = 0; i < inputCount; i++) {
+ MediaTrack* s = mInputs[i]->GetSource();
+ AudioSegment* inputSegment = s->GetData<AudioSegment>();
+ TrackTime inputStart = s->GraphTimeToTrackTimeWithBlocking(aFrom);
+ TrackTime inputEnd = s->GraphTimeToTrackTimeWithBlocking(aTo);
+ AudioSegment toMix;
+ if (s->Ended() && inputSegment->GetDuration() <= inputStart) {
+ toMix.AppendNullData(aTo - aFrom);
+ } else {
+ toMix.AppendSlice(*inputSegment, inputStart, inputEnd);
+ // Care for tracks blocked in the [aTo, aFrom] range.
+ if (inputEnd - inputStart < aTo - aFrom) {
+ toMix.AppendNullData((aTo - aFrom) - (inputEnd - inputStart));
+ }
+ }
+ toMix.Mix(mMixer, MONO, Graph()->GraphRate());
+ }
+ // This calls MixerCallback below
+ mMixer.FinishMixing();
+ }
+}
+
+uint32_t AudioCaptureTrack::NumberOfChannels() const {
+ return GetData<AudioSegment>()->MaxChannelCount();
+}
+
+void AudioCaptureTrack::MixerCallback(AudioDataValue* aMixedBuffer,
+ AudioSampleFormat aFormat,
+ uint32_t aChannels, uint32_t aFrames,
+ uint32_t aSampleRate) {
+ AutoTArray<nsTArray<AudioDataValue>, MONO> output;
+ AutoTArray<const AudioDataValue*, MONO> bufferPtrs;
+ output.SetLength(MONO);
+ bufferPtrs.SetLength(MONO);
+
+ uint32_t written = 0;
+ // We need to copy here, because the mixer will reuse the storage, we should
+ // not hold onto it. Buffers are in planar format.
+ for (uint32_t channel = 0; channel < aChannels; channel++) {
+ AudioDataValue* out = output[channel].AppendElements(aFrames);
+ PodCopy(out, aMixedBuffer + written, aFrames);
+ bufferPtrs[channel] = out;
+ written += aFrames;
+ }
+ AudioChunk chunk;
+ chunk.mBuffer =
+ new mozilla::SharedChannelArrayBuffer<AudioDataValue>(std::move(output));
+ chunk.mDuration = aFrames;
+ chunk.mBufferFormat = aFormat;
+ chunk.mChannelData.SetLength(MONO);
+ for (uint32_t channel = 0; channel < aChannels; channel++) {
+ chunk.mChannelData[channel] = bufferPtrs[channel];
+ }
+
+ // Now we have mixed data, simply append it.
+ GetData<AudioSegment>()->AppendAndConsumeChunk(std::move(chunk));
+}
+} // namespace mozilla
diff --git a/dom/media/AudioCaptureTrack.h b/dom/media/AudioCaptureTrack.h
new file mode 100644
index 0000000000..c4de4b0581
--- /dev/null
+++ b/dom/media/AudioCaptureTrack.h
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_AUDIOCAPTURETRACK_H_
+#define MOZILLA_AUDIOCAPTURETRACK_H_
+
+#include "MediaTrackGraph.h"
+#include "AudioMixer.h"
+#include <algorithm>
+
+namespace mozilla {
+
+class AbstractThread;
+class DOMMediaStream;
+
+/**
+ * See MediaTrackGraph::CreateAudioCaptureTrack.
+ */
+class AudioCaptureTrack : public ProcessedMediaTrack,
+ public MixerCallbackReceiver {
+ public:
+ explicit AudioCaptureTrack(TrackRate aRate);
+ virtual ~AudioCaptureTrack();
+
+ void Start();
+
+ void ProcessInput(GraphTime aFrom, GraphTime aTo, uint32_t aFlags) override;
+
+ uint32_t NumberOfChannels() const override;
+
+ protected:
+ void MixerCallback(AudioDataValue* aMixedBuffer, AudioSampleFormat aFormat,
+ uint32_t aChannels, uint32_t aFrames,
+ uint32_t aSampleRate) override;
+ AudioMixer mMixer;
+ bool mStarted;
+ bool mTrackCreated;
+};
+} // namespace mozilla
+
+#endif /* MOZILLA_AUDIOCAPTURETRACK_H_ */
diff --git a/dom/media/AudioChannelFormat.cpp b/dom/media/AudioChannelFormat.cpp
new file mode 100644
index 0000000000..6caf72c3ba
--- /dev/null
+++ b/dom/media/AudioChannelFormat.cpp
@@ -0,0 +1,16 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioChannelFormat.h"
+
+#include <algorithm>
+
+namespace mozilla {
+
+uint32_t GetAudioChannelsSuperset(uint32_t aChannels1, uint32_t aChannels2) {
+ return std::max(aChannels1, aChannels2);
+}
+
+} // namespace mozilla
diff --git a/dom/media/AudioChannelFormat.h b/dom/media/AudioChannelFormat.h
new file mode 100644
index 0000000000..78a2d902a6
--- /dev/null
+++ b/dom/media/AudioChannelFormat.h
@@ -0,0 +1,253 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef MOZILLA_AUDIOCHANNELFORMAT_H_
+#define MOZILLA_AUDIOCHANNELFORMAT_H_
+
+#include <stdint.h>
+
+#include "mozilla/PodOperations.h"
+#include "nsTArrayForwardDeclare.h"
+#include "AudioSampleFormat.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+
+/*
+ * This file provides utilities for upmixing and downmixing channels.
+ *
+ * The channel layouts, upmixing and downmixing are consistent with the
+ * Web Audio spec.
+ *
+ * Channel layouts for up to 6 channels:
+ * mono { M }
+ * stereo { L, R }
+ * { L, R, C }
+ * quad { L, R, SL, SR }
+ * { L, R, C, SL, SR }
+ * 5.1 { L, R, C, LFE, SL, SR }
+ *
+ * Only 1, 2, 4 and 6 are currently defined in Web Audio.
+ */
+
+enum {
+ SURROUND_L,
+ SURROUND_R,
+ SURROUND_C,
+ SURROUND_LFE,
+ SURROUND_SL,
+ SURROUND_SR
+};
+
+const uint32_t CUSTOM_CHANNEL_LAYOUTS = 6;
+
+// This is defined by some Windows SDK header.
+#undef IGNORE
+
+const int IGNORE = CUSTOM_CHANNEL_LAYOUTS;
+const float IGNORE_F = 0.0f;
+
+const int gMixingMatrixIndexByChannels[CUSTOM_CHANNEL_LAYOUTS - 1] = {0, 5, 9,
+ 12, 14};
+
+/**
+ * Return a channel count whose channel layout includes all the channels from
+ * aChannels1 and aChannels2.
+ */
+uint32_t GetAudioChannelsSuperset(uint32_t aChannels1, uint32_t aChannels2);
+
+/**
+ * DownMixMatrix represents a conversion matrix efficiently by exploiting the
+ * fact that each input channel contributes to at most one output channel,
+ * except possibly for the C input channel in layouts that have one. Also,
+ * every input channel is multiplied by the same coefficient for every output
+ * channel it contributes to.
+ */
+const float SQRT_ONE_HALF = 0.7071067811865476f;
+
+struct DownMixMatrix {
+ // Every input channel c is copied to output channel mInputDestination[c]
+ // after multiplying by mInputCoefficient[c].
+ uint8_t mInputDestination[CUSTOM_CHANNEL_LAYOUTS];
+ // If not IGNORE, then the C channel is copied to this output channel after
+ // multiplying by its coefficient.
+ uint8_t mCExtraDestination;
+ float mInputCoefficient[CUSTOM_CHANNEL_LAYOUTS];
+};
+
+static const DownMixMatrix gDownMixMatrices[CUSTOM_CHANNEL_LAYOUTS *
+ (CUSTOM_CHANNEL_LAYOUTS - 1) /
+ 2] = {
+ // Downmixes to mono
+ {{0, 0}, IGNORE, {0.5f, 0.5f}},
+ {{0, IGNORE, IGNORE}, IGNORE, {1.0f, IGNORE_F, IGNORE_F}},
+ {{0, 0, 0, 0}, IGNORE, {0.25f, 0.25f, 0.25f, 0.25f}},
+ {{0, IGNORE, IGNORE, IGNORE, IGNORE},
+ IGNORE,
+ {1.0f, IGNORE_F, IGNORE_F, IGNORE_F, IGNORE_F}},
+ {{0, 0, 0, IGNORE, 0, 0},
+ IGNORE,
+ {SQRT_ONE_HALF, SQRT_ONE_HALF, 1.0f, IGNORE_F, 0.5f, 0.5f}},
+ // Downmixes to stereo
+ {{0, 1, IGNORE}, IGNORE, {1.0f, 1.0f, IGNORE_F}},
+ {{0, 1, 0, 1}, IGNORE, {0.5f, 0.5f, 0.5f, 0.5f}},
+ {{0, 1, IGNORE, IGNORE, IGNORE},
+ IGNORE,
+ {1.0f, 1.0f, IGNORE_F, IGNORE_F, IGNORE_F}},
+ {{0, 1, 0, IGNORE, 0, 1},
+ 1,
+ {1.0f, 1.0f, SQRT_ONE_HALF, IGNORE_F, SQRT_ONE_HALF, SQRT_ONE_HALF}},
+ // Downmixes to 3-channel
+ {{0, 1, 2, IGNORE}, IGNORE, {1.0f, 1.0f, 1.0f, IGNORE_F}},
+ {{0, 1, 2, IGNORE, IGNORE}, IGNORE, {1.0f, 1.0f, 1.0f, IGNORE_F, IGNORE_F}},
+ {{0, 1, 2, IGNORE, IGNORE, IGNORE},
+ IGNORE,
+ {1.0f, 1.0f, 1.0f, IGNORE_F, IGNORE_F, IGNORE_F}},
+ // Downmixes to quad
+ {{0, 1, 2, 3, IGNORE}, IGNORE, {1.0f, 1.0f, 1.0f, 1.0f, IGNORE_F}},
+ {{0, 1, 0, IGNORE, 2, 3},
+ 1,
+ {1.0f, 1.0f, SQRT_ONE_HALF, IGNORE_F, 1.0f, 1.0f}},
+ // Downmixes to 5-channel
+ {{0, 1, 2, 3, 4, IGNORE},
+ IGNORE,
+ {1.0f, 1.0f, 1.0f, 1.0f, 1.0f, IGNORE_F}}};
+
+/**
+ * Given an array of input channels, downmix to aOutputChannelCount, and copy
+ * the results to the channel buffers in aOutputChannels. Don't call this with
+ * input count <= output count.
+ */
+template <typename T>
+void AudioChannelsDownMix(const nsTArray<const T*>& aChannelArray,
+ T** aOutputChannels, uint32_t aOutputChannelCount,
+ uint32_t aDuration) {
+ uint32_t inputChannelCount = aChannelArray.Length();
+ const T* const* inputChannels = aChannelArray.Elements();
+ NS_ASSERTION(inputChannelCount > aOutputChannelCount, "Nothing to do");
+
+ if (inputChannelCount > 6) {
+ // Just drop the unknown channels.
+ for (uint32_t o = 0; o < aOutputChannelCount; ++o) {
+ PodCopy(aOutputChannels[o], inputChannels[o], aDuration);
+ }
+ return;
+ }
+
+ // Ignore unknown channels, they're just dropped.
+ inputChannelCount = std::min<uint32_t>(6, inputChannelCount);
+
+ const DownMixMatrix& m =
+ gDownMixMatrices[gMixingMatrixIndexByChannels[aOutputChannelCount - 1] +
+ inputChannelCount - aOutputChannelCount - 1];
+
+ // This is slow, but general. We can define custom code for special
+ // cases later.
+ for (uint32_t s = 0; s < aDuration; ++s) {
+ // Reserve an extra junk channel at the end for the cases where we
+ // want an input channel to contribute to nothing
+ T outputChannels[CUSTOM_CHANNEL_LAYOUTS + 1] = {0};
+ for (uint32_t c = 0; c < inputChannelCount; ++c) {
+ outputChannels[m.mInputDestination[c]] +=
+ m.mInputCoefficient[c] * (static_cast<const T*>(inputChannels[c]))[s];
+ }
+ // Utilize the fact that in every layout, C is the third channel.
+ if (m.mCExtraDestination != IGNORE) {
+ outputChannels[m.mCExtraDestination] +=
+ m.mInputCoefficient[SURROUND_C] *
+ (static_cast<const T*>(inputChannels[SURROUND_C]))[s];
+ }
+
+ for (uint32_t c = 0; c < aOutputChannelCount; ++c) {
+ aOutputChannels[c][s] = outputChannels[c];
+ }
+ }
+}
+
+/**
+ * UpMixMatrix represents a conversion matrix by exploiting the fact that
+ * each output channel comes from at most one input channel.
+ */
+struct UpMixMatrix {
+ uint8_t mInputDestination[CUSTOM_CHANNEL_LAYOUTS];
+};
+
+static const UpMixMatrix gUpMixMatrices[CUSTOM_CHANNEL_LAYOUTS *
+ (CUSTOM_CHANNEL_LAYOUTS - 1) / 2] = {
+ // Upmixes from mono
+ {{0, 0}},
+ {{0, IGNORE, IGNORE}},
+ {{0, 0, IGNORE, IGNORE}},
+ {{0, IGNORE, IGNORE, IGNORE, IGNORE}},
+ {{IGNORE, IGNORE, 0, IGNORE, IGNORE, IGNORE}},
+ // Upmixes from stereo
+ {{0, 1, IGNORE}},
+ {{0, 1, IGNORE, IGNORE}},
+ {{0, 1, IGNORE, IGNORE, IGNORE}},
+ {{0, 1, IGNORE, IGNORE, IGNORE, IGNORE}},
+ // Upmixes from 3-channel
+ {{0, 1, 2, IGNORE}},
+ {{0, 1, 2, IGNORE, IGNORE}},
+ {{0, 1, 2, IGNORE, IGNORE, IGNORE}},
+ // Upmixes from quad
+ {{0, 1, 2, 3, IGNORE}},
+ {{0, 1, IGNORE, IGNORE, 2, 3}},
+ // Upmixes from 5-channel
+ {{0, 1, 2, 3, 4, IGNORE}}};
+
+/**
+ * Given an array of input channel data, and an output channel count,
+ * replaces the array with an array of upmixed channels.
+ * This shuffles the array and may set some channel buffers to aZeroChannel.
+ * Don't call this with input count >= output count.
+ * This may return *more* channels than requested. In that case, downmixing
+ * is required to to get to aOutputChannelCount. (This is how we handle
+ * odd cases like 3 -> 4 upmixing.)
+ * If aChannelArray.Length() was the input to one of a series of
+ * GetAudioChannelsSuperset calls resulting in aOutputChannelCount,
+ * no downmixing will be required.
+ */
+template <typename T>
+void AudioChannelsUpMix(nsTArray<const T*>* aChannelArray,
+ uint32_t aOutputChannelCount, const T* aZeroChannel) {
+ uint32_t inputChannelCount = aChannelArray->Length();
+ uint32_t outputChannelCount =
+ GetAudioChannelsSuperset(aOutputChannelCount, inputChannelCount);
+ NS_ASSERTION(outputChannelCount > inputChannelCount, "No up-mix needed");
+ MOZ_ASSERT(inputChannelCount > 0, "Bad number of channels");
+ MOZ_ASSERT(outputChannelCount > 0, "Bad number of channels");
+
+ aChannelArray->SetLength(outputChannelCount);
+
+ if (inputChannelCount < CUSTOM_CHANNEL_LAYOUTS &&
+ outputChannelCount <= CUSTOM_CHANNEL_LAYOUTS) {
+ const UpMixMatrix& m =
+ gUpMixMatrices[gMixingMatrixIndexByChannels[inputChannelCount - 1] +
+ outputChannelCount - inputChannelCount - 1];
+
+ const T* outputChannels[CUSTOM_CHANNEL_LAYOUTS];
+
+ for (uint32_t i = 0; i < outputChannelCount; ++i) {
+ uint8_t channelIndex = m.mInputDestination[i];
+ if (channelIndex == IGNORE) {
+ outputChannels[i] = aZeroChannel;
+ } else {
+ outputChannels[i] = aChannelArray->ElementAt(channelIndex);
+ }
+ }
+ for (uint32_t i = 0; i < outputChannelCount; ++i) {
+ aChannelArray->ElementAt(i) = outputChannels[i];
+ }
+ return;
+ }
+
+ for (uint32_t i = inputChannelCount; i < outputChannelCount; ++i) {
+ aChannelArray->ElementAt(i) = aZeroChannel;
+ }
+}
+
+} // namespace mozilla
+
+#endif /* MOZILLA_AUDIOCHANNELFORMAT_H_ */
diff --git a/dom/media/AudioCompactor.cpp b/dom/media/AudioCompactor.cpp
new file mode 100644
index 0000000000..54e723d55e
--- /dev/null
+++ b/dom/media/AudioCompactor.cpp
@@ -0,0 +1,65 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "AudioCompactor.h"
+#if defined(MOZ_MEMORY)
+# include "mozmemory.h"
+#endif
+
+namespace mozilla {
+
+static size_t MallocGoodSize(size_t aSize) {
+#if defined(MOZ_MEMORY)
+ return malloc_good_size(aSize);
+#else
+ return aSize;
+#endif
+}
+
+static size_t TooMuchSlop(size_t aSize, size_t aAllocSize, size_t aMaxSlop) {
+ // If the allocated size is less then our target size, then we
+ // are chunking. This means it will be completely filled with
+ // zero slop.
+ size_t slop = (aAllocSize > aSize) ? (aAllocSize - aSize) : 0;
+ return slop > aMaxSlop;
+}
+
+uint32_t AudioCompactor::GetChunkSamples(uint32_t aFrames, uint32_t aChannels,
+ size_t aMaxSlop) {
+ size_t size = AudioDataSize(aFrames, aChannels);
+ size_t chunkSize = MallocGoodSize(size);
+
+ // Reduce the chunk size until we meet our slop goal or the chunk
+ // approaches an unreasonably small size.
+ while (chunkSize > 64 && TooMuchSlop(size, chunkSize, aMaxSlop)) {
+ chunkSize = MallocGoodSize(chunkSize / 2);
+ }
+
+ // Calculate the number of samples based on expected malloc size
+ // in order to allow as many frames as possible to be packed.
+ return chunkSize / sizeof(AudioDataValue);
+}
+
+uint32_t AudioCompactor::NativeCopy::operator()(AudioDataValue* aBuffer,
+ uint32_t aSamples) {
+ NS_ASSERTION(aBuffer, "cannot copy to null buffer pointer");
+ NS_ASSERTION(aSamples, "cannot copy zero values");
+
+ size_t bufferBytes = aSamples * sizeof(AudioDataValue);
+ size_t maxBytes = std::min(bufferBytes, mSourceBytes - mNextByte);
+ uint32_t frames = maxBytes / BytesPerFrame(mChannels);
+ size_t bytes = frames * BytesPerFrame(mChannels);
+
+ NS_ASSERTION((mNextByte + bytes) <= mSourceBytes,
+ "tried to copy beyond source buffer");
+ NS_ASSERTION(bytes <= bufferBytes, "tried to copy beyond destination buffer");
+
+ memcpy(aBuffer, mSource + mNextByte, bytes);
+
+ mNextByte += bytes;
+ return frames;
+}
+
+} // namespace mozilla
diff --git a/dom/media/AudioCompactor.h b/dom/media/AudioCompactor.h
new file mode 100644
index 0000000000..8281686977
--- /dev/null
+++ b/dom/media/AudioCompactor.h
@@ -0,0 +1,128 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#if !defined(AudioCompactor_h)
+# define AudioCompactor_h
+
+# include "MediaQueue.h"
+# include "MediaData.h"
+# include "VideoUtils.h"
+
+namespace mozilla {
+
+class AudioCompactor {
+ public:
+ explicit AudioCompactor(MediaQueue<AudioData>& aQueue) : mQueue(aQueue) {
+ // Determine padding size used by AlignedBuffer.
+ size_t paddedSize = AlignedAudioBuffer::AlignmentPaddingSize();
+ mSamplesPadding = paddedSize / sizeof(AudioDataValue);
+ if (mSamplesPadding * sizeof(AudioDataValue) < paddedSize) {
+ // Round up.
+ mSamplesPadding++;
+ }
+ }
+
+ // Push audio data into the underlying queue with minimal heap allocation
+ // slop. This method is responsible for allocating AudioDataValue[] buffers.
+ // The caller must provide a functor to copy the data into the buffers. The
+ // functor must provide the following signature:
+ //
+ // uint32_t operator()(AudioDataValue *aBuffer, uint32_t aSamples);
+ //
+ // The functor must copy as many complete frames as possible to the provided
+ // buffer given its length (in AudioDataValue elements). The number of frames
+ // copied must be returned. This copy functor must support being called
+ // multiple times in order to copy the audio data fully. The copy functor
+ // must copy full frames as partial frames will be ignored.
+ template <typename CopyFunc>
+ bool Push(int64_t aOffset, int64_t aTime, int32_t aSampleRate,
+ uint32_t aFrames, uint32_t aChannels, CopyFunc aCopyFunc) {
+ auto time = media::TimeUnit::FromMicroseconds(aTime);
+
+ // If we are losing more than a reasonable amount to padding, try to chunk
+ // the data.
+ size_t maxSlop = AudioDataSize(aFrames, aChannels) / MAX_SLOP_DIVISOR;
+
+ while (aFrames > 0) {
+ uint32_t samples = GetChunkSamples(aFrames, aChannels, maxSlop);
+ if (samples / aChannels > mSamplesPadding / aChannels + 1) {
+ samples -= mSamplesPadding;
+ }
+ AlignedAudioBuffer buffer(samples);
+ if (!buffer) {
+ return false;
+ }
+
+ // Copy audio data to buffer using caller-provided functor.
+ uint32_t framesCopied = aCopyFunc(buffer.get(), samples);
+
+ NS_ASSERTION(framesCopied <= aFrames, "functor copied too many frames");
+ buffer.SetLength(size_t(framesCopied) * aChannels);
+
+ auto duration = media::TimeUnit(framesCopied, aSampleRate);
+ if (!duration.IsValid()) {
+ return false;
+ }
+
+ RefPtr<AudioData> data = new AudioData(aOffset, time, std::move(buffer),
+ aChannels, aSampleRate);
+ MOZ_DIAGNOSTIC_ASSERT(duration == data->mDuration, "must be equal");
+ mQueue.Push(data);
+
+ // Remove the frames we just pushed into the queue and loop if there is
+ // more to be done.
+ time += duration;
+ aFrames -= framesCopied;
+
+ // NOTE: No need to update aOffset as its only an approximation anyway.
+ }
+
+ return true;
+ }
+
+ // Copy functor suitable for copying audio samples already in the
+ // AudioDataValue format/layout expected by AudioStream on this platform.
+ class NativeCopy {
+ public:
+ NativeCopy(const uint8_t* aSource, size_t aSourceBytes, uint32_t aChannels)
+ : mSource(aSource),
+ mSourceBytes(aSourceBytes),
+ mChannels(aChannels),
+ mNextByte(0) {}
+
+ uint32_t operator()(AudioDataValue* aBuffer, uint32_t aSamples);
+
+ private:
+ const uint8_t* const mSource;
+ const size_t mSourceBytes;
+ const uint32_t mChannels;
+ size_t mNextByte;
+ };
+
+ // Allow 12.5% slop before chunking kicks in. Public so that the gtest can
+ // access it.
+ static const size_t MAX_SLOP_DIVISOR = 8;
+
+ private:
+ // Compute the number of AudioDataValue samples that will be fit the most
+ // frames while keeping heap allocation slop less than the given threshold.
+ static uint32_t GetChunkSamples(uint32_t aFrames, uint32_t aChannels,
+ size_t aMaxSlop);
+
+ static size_t BytesPerFrame(uint32_t aChannels) {
+ return sizeof(AudioDataValue) * aChannels;
+ }
+
+ static size_t AudioDataSize(uint32_t aFrames, uint32_t aChannels) {
+ return aFrames * BytesPerFrame(aChannels);
+ }
+
+ MediaQueue<AudioData>& mQueue;
+ size_t mSamplesPadding;
+};
+
+} // namespace mozilla
+
+#endif // AudioCompactor_h
diff --git a/dom/media/AudioConfig.cpp b/dom/media/AudioConfig.cpp
new file mode 100644
index 0000000000..cf85f488f7
--- /dev/null
+++ b/dom/media/AudioConfig.cpp
@@ -0,0 +1,343 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioConfig.h"
+
+namespace mozilla {
+
+typedef AudioConfig::ChannelLayout ChannelLayout;
+
+/**
+ * AudioConfig::ChannelLayout
+ */
+
+/*
+ SMPTE channel layout (also known as wave order)
+ DUAL-MONO L R
+ DUAL-MONO-LFE L R LFE
+ MONO M
+ MONO-LFE M LFE
+ STEREO L R
+ STEREO-LFE L R LFE
+ 3F L R C
+ 3F-LFE L R C LFE
+ 2F1 L R S
+ 2F1-LFE L R LFE S
+ 3F1 L R C S
+ 3F1-LFE L R C LFE S
+ 2F2 L R LS RS
+ 2F2-LFE L R LFE LS RS
+ 3F2 L R C LS RS
+ 3F2-LFE L R C LFE LS RS
+ 3F3R-LFE L R C LFE BC LS RS
+ 3F4-LFE L R C LFE Rls Rrs LS RS
+*/
+
+void AudioConfig::ChannelLayout::UpdateChannelMap() {
+ mValid = mChannels.Length() <= MAX_CHANNELS;
+ mChannelMap = UNKNOWN_MAP;
+ if (mValid) {
+ mChannelMap = Map();
+ mValid = mChannelMap > 0;
+ }
+}
+
+auto AudioConfig::ChannelLayout::Map() const -> ChannelMap {
+ if (mChannelMap != UNKNOWN_MAP) {
+ return mChannelMap;
+ }
+ if (mChannels.Length() > MAX_CHANNELS) {
+ return UNKNOWN_MAP;
+ }
+ ChannelMap map = UNKNOWN_MAP;
+ for (size_t i = 0; i < mChannels.Length(); i++) {
+ if (uint32_t(mChannels[i]) > sizeof(ChannelMap) * 8) {
+ return UNKNOWN_MAP;
+ }
+ ChannelMap mask = 1 << mChannels[i];
+ if (mChannels[i] == CHANNEL_INVALID || (mChannelMap & mask)) {
+ // Invalid configuration.
+ return UNKNOWN_MAP;
+ }
+ map |= mask;
+ }
+ return map;
+}
+
+const AudioConfig::Channel*
+AudioConfig::ChannelLayout::DefaultLayoutForChannels(uint32_t aChannels) const {
+ switch (aChannels) {
+ case 1: // MONO
+ {
+ static const Channel config[] = {CHANNEL_FRONT_CENTER};
+ return config;
+ }
+ case 2: // STEREO
+ {
+ static const Channel config[] = {CHANNEL_FRONT_LEFT, CHANNEL_FRONT_RIGHT};
+ return config;
+ }
+ case 3: // 3F
+ {
+ static const Channel config[] = {CHANNEL_FRONT_LEFT, CHANNEL_FRONT_RIGHT,
+ CHANNEL_FRONT_CENTER};
+ return config;
+ }
+ case 4: // QUAD
+ {
+ static const Channel config[] = {CHANNEL_FRONT_LEFT, CHANNEL_FRONT_RIGHT,
+ CHANNEL_BACK_LEFT, CHANNEL_BACK_RIGHT};
+ return config;
+ }
+ case 5: // 3F2
+ {
+ static const Channel config[] = {CHANNEL_FRONT_LEFT, CHANNEL_FRONT_RIGHT,
+ CHANNEL_FRONT_CENTER, CHANNEL_SIDE_LEFT,
+ CHANNEL_SIDE_RIGHT};
+ return config;
+ }
+ case 6: // 3F2-LFE
+ {
+ static const Channel config[] = {
+ CHANNEL_FRONT_LEFT, CHANNEL_FRONT_RIGHT, CHANNEL_FRONT_CENTER,
+ CHANNEL_LFE, CHANNEL_SIDE_LEFT, CHANNEL_SIDE_RIGHT};
+ return config;
+ }
+ case 7: // 3F3R-LFE
+ {
+ static const Channel config[] = {
+ CHANNEL_FRONT_LEFT, CHANNEL_FRONT_RIGHT, CHANNEL_FRONT_CENTER,
+ CHANNEL_LFE, CHANNEL_BACK_CENTER, CHANNEL_SIDE_LEFT,
+ CHANNEL_SIDE_RIGHT};
+ return config;
+ }
+ case 8: // 3F4-LFE
+ {
+ static const Channel config[] = {
+ CHANNEL_FRONT_LEFT, CHANNEL_FRONT_RIGHT, CHANNEL_FRONT_CENTER,
+ CHANNEL_LFE, CHANNEL_BACK_LEFT, CHANNEL_BACK_RIGHT,
+ CHANNEL_SIDE_LEFT, CHANNEL_SIDE_RIGHT};
+ return config;
+ }
+ default:
+ return nullptr;
+ }
+}
+
+/* static */ AudioConfig::ChannelLayout
+AudioConfig::ChannelLayout::SMPTEDefault(const ChannelLayout& aChannelLayout) {
+ if (!aChannelLayout.IsValid()) {
+ return aChannelLayout;
+ }
+ return SMPTEDefault(aChannelLayout.Map());
+}
+
+/* static */
+ChannelLayout AudioConfig::ChannelLayout::SMPTEDefault(ChannelMap aMap) {
+ // First handle the most common cases.
+ switch (aMap) {
+ case LMONO_MAP:
+ return ChannelLayout{CHANNEL_FRONT_CENTER};
+ case LSTEREO_MAP:
+ return ChannelLayout{CHANNEL_FRONT_LEFT, CHANNEL_FRONT_RIGHT};
+ case L3F_MAP:
+ return ChannelLayout{CHANNEL_FRONT_LEFT, CHANNEL_FRONT_RIGHT,
+ CHANNEL_FRONT_CENTER};
+ case L3F_LFE_MAP:
+ return ChannelLayout{CHANNEL_FRONT_LEFT, CHANNEL_FRONT_RIGHT,
+ CHANNEL_FRONT_CENTER, CHANNEL_LFE};
+ case L2F1_MAP:
+ return ChannelLayout{CHANNEL_FRONT_LEFT, CHANNEL_FRONT_RIGHT,
+ CHANNEL_BACK_CENTER};
+ case L2F1_LFE_MAP:
+ return ChannelLayout{CHANNEL_FRONT_LEFT, CHANNEL_FRONT_RIGHT, CHANNEL_LFE,
+ CHANNEL_BACK_CENTER};
+ case L3F1_MAP:
+ return ChannelLayout{CHANNEL_FRONT_LEFT, CHANNEL_FRONT_RIGHT,
+ CHANNEL_FRONT_CENTER, CHANNEL_BACK_CENTER};
+ case L3F1_LFE_MAP:
+ return ChannelLayout{CHANNEL_FRONT_LEFT, CHANNEL_FRONT_RIGHT,
+ CHANNEL_FRONT_CENTER, CHANNEL_LFE,
+ CHANNEL_BACK_CENTER};
+ case L2F2_MAP:
+ return ChannelLayout{CHANNEL_FRONT_LEFT, CHANNEL_FRONT_RIGHT,
+ CHANNEL_SIDE_LEFT, CHANNEL_SIDE_RIGHT};
+ case L2F2_LFE_MAP:
+ return ChannelLayout{CHANNEL_FRONT_LEFT, CHANNEL_FRONT_RIGHT, CHANNEL_LFE,
+ CHANNEL_SIDE_LEFT, CHANNEL_SIDE_RIGHT};
+ case LQUAD_MAP:
+ return ChannelLayout{CHANNEL_FRONT_LEFT, CHANNEL_FRONT_RIGHT,
+ CHANNEL_BACK_LEFT, CHANNEL_BACK_RIGHT};
+ case LQUAD_LFE_MAP:
+ return ChannelLayout{CHANNEL_FRONT_LEFT, CHANNEL_FRONT_RIGHT, CHANNEL_LFE,
+ CHANNEL_BACK_LEFT, CHANNEL_BACK_RIGHT};
+ case L3F2_MAP:
+ return ChannelLayout{CHANNEL_FRONT_LEFT, CHANNEL_FRONT_RIGHT,
+ CHANNEL_FRONT_CENTER, CHANNEL_SIDE_LEFT,
+ CHANNEL_SIDE_RIGHT};
+ case L3F2_LFE_MAP:
+ return ChannelLayout{CHANNEL_FRONT_LEFT, CHANNEL_FRONT_RIGHT,
+ CHANNEL_FRONT_CENTER, CHANNEL_LFE,
+ CHANNEL_SIDE_LEFT, CHANNEL_SIDE_RIGHT};
+ case L3F2_BACK_MAP:
+ return ChannelLayout{CHANNEL_FRONT_LEFT, CHANNEL_FRONT_RIGHT,
+ CHANNEL_FRONT_CENTER, CHANNEL_BACK_LEFT,
+ CHANNEL_BACK_RIGHT};
+ case L3F2_BACK_LFE_MAP:
+ return ChannelLayout{CHANNEL_FRONT_LEFT, CHANNEL_FRONT_RIGHT,
+ CHANNEL_FRONT_CENTER, CHANNEL_LFE,
+ CHANNEL_BACK_LEFT, CHANNEL_BACK_RIGHT};
+ case L3F3R_LFE_MAP:
+ return ChannelLayout{CHANNEL_FRONT_LEFT, CHANNEL_FRONT_RIGHT,
+ CHANNEL_FRONT_CENTER, CHANNEL_LFE,
+ CHANNEL_BACK_CENTER, CHANNEL_SIDE_LEFT,
+ CHANNEL_SIDE_RIGHT};
+ case L3F4_LFE_MAP:
+ return ChannelLayout{CHANNEL_FRONT_LEFT, CHANNEL_FRONT_RIGHT,
+ CHANNEL_FRONT_CENTER, CHANNEL_LFE,
+ CHANNEL_BACK_LEFT, CHANNEL_BACK_RIGHT,
+ CHANNEL_SIDE_LEFT, CHANNEL_SIDE_RIGHT};
+ default:
+ break;
+ }
+
+ static_assert(MAX_CHANNELS <= sizeof(ChannelMap) * 8,
+ "Must be able to fit channels on bit mask");
+ AutoTArray<Channel, MAX_CHANNELS> layout;
+ uint32_t channels = 0;
+
+ uint32_t i = 0;
+ while (aMap) {
+ if (aMap & 1) {
+ channels++;
+ if (channels > MAX_CHANNELS) {
+ return ChannelLayout();
+ }
+ layout.AppendElement(static_cast<Channel>(i));
+ }
+ aMap >>= 1;
+ i++;
+ }
+ return ChannelLayout(channels, layout.Elements());
+}
+
+bool AudioConfig::ChannelLayout::MappingTable(const ChannelLayout& aOther,
+ nsTArray<uint8_t>* aMap) const {
+ if (!IsValid() || !aOther.IsValid() || Map() != aOther.Map()) {
+ if (aMap) {
+ aMap->SetLength(0);
+ }
+ return false;
+ }
+ if (!aMap) {
+ return true;
+ }
+ aMap->SetLength(Count());
+ for (uint32_t i = 0; i < Count(); i++) {
+ for (uint32_t j = 0; j < Count(); j++) {
+ if (aOther[j] == mChannels[i]) {
+ (*aMap)[j] = i;
+ break;
+ }
+ }
+ }
+ return true;
+}
+
+/**
+ * AudioConfig::ChannelConfig
+ */
+
+/* static */ const char* AudioConfig::FormatToString(
+ AudioConfig::SampleFormat aFormat) {
+ switch (aFormat) {
+ case FORMAT_U8:
+ return "unsigned 8 bit";
+ case FORMAT_S16:
+ return "signed 16 bit";
+ case FORMAT_S24:
+ return "signed 24 bit MSB";
+ case FORMAT_S24LSB:
+ return "signed 24 bit LSB";
+ case FORMAT_S32:
+ return "signed 32 bit";
+ case FORMAT_FLT:
+ return "32 bit floating point";
+ case FORMAT_NONE:
+ return "none";
+ default:
+ return "unknown";
+ }
+}
+/* static */
+uint32_t AudioConfig::SampleSize(AudioConfig::SampleFormat aFormat) {
+ switch (aFormat) {
+ case FORMAT_U8:
+ return 1;
+ case FORMAT_S16:
+ return 2;
+ case FORMAT_S24:
+ [[fallthrough]];
+ case FORMAT_S24LSB:
+ [[fallthrough]];
+ case FORMAT_S32:
+ [[fallthrough]];
+ case FORMAT_FLT:
+ return 4;
+ case FORMAT_NONE:
+ default:
+ return 0;
+ }
+}
+
+/* static */
+uint32_t AudioConfig::FormatToBits(AudioConfig::SampleFormat aFormat) {
+ switch (aFormat) {
+ case FORMAT_U8:
+ return 8;
+ case FORMAT_S16:
+ return 16;
+ case FORMAT_S24LSB:
+ [[fallthrough]];
+ case FORMAT_S24:
+ return 24;
+ case FORMAT_S32:
+ [[fallthrough]];
+ case FORMAT_FLT:
+ return 32;
+ case FORMAT_NONE:
+ [[fallthrough]];
+ default:
+ return 0;
+ }
+}
+
+AudioConfig::AudioConfig(const ChannelLayout& aChannelLayout, uint32_t aRate,
+ AudioConfig::SampleFormat aFormat, bool aInterleaved)
+ : mChannelLayout(aChannelLayout),
+ mChannels(aChannelLayout.Count()),
+ mRate(aRate),
+ mFormat(aFormat),
+ mInterleaved(aInterleaved) {}
+
+AudioConfig::AudioConfig(const ChannelLayout& aChannelLayout,
+ uint32_t aChannels, uint32_t aRate,
+ AudioConfig::SampleFormat aFormat, bool aInterleaved)
+ : mChannelLayout(aChannelLayout),
+ mChannels(aChannels),
+ mRate(aRate),
+ mFormat(aFormat),
+ mInterleaved(aInterleaved) {}
+
+AudioConfig::AudioConfig(uint32_t aChannels, uint32_t aRate,
+ AudioConfig::SampleFormat aFormat, bool aInterleaved)
+ : mChannelLayout(aChannels),
+ mChannels(aChannels),
+ mRate(aRate),
+ mFormat(aFormat),
+ mInterleaved(aInterleaved) {}
+
+} // namespace mozilla
diff --git a/dom/media/AudioConfig.h b/dom/media/AudioConfig.h
new file mode 100644
index 0000000000..d0433b1083
--- /dev/null
+++ b/dom/media/AudioConfig.h
@@ -0,0 +1,278 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#if !defined(AudioLayout_h)
+# define AudioLayout_h
+
+# include <cstdint>
+# include <initializer_list>
+# include "mozilla/MathAlgorithms.h"
+# include "nsTArray.h"
+# include "cubeb/cubeb.h"
+
+namespace mozilla {
+
+class AudioConfig {
+ public:
+ // Channel definition is conveniently defined to be in the same order as
+ // WAVEFORMAT && SMPTE, even though this is unused for now.
+ enum Channel {
+ CHANNEL_INVALID = -1,
+ CHANNEL_FRONT_LEFT = 0,
+ CHANNEL_FRONT_RIGHT,
+ CHANNEL_FRONT_CENTER,
+ CHANNEL_LFE,
+ CHANNEL_BACK_LEFT,
+ CHANNEL_BACK_RIGHT,
+ CHANNEL_FRONT_LEFT_OF_CENTER,
+ CHANNEL_FRONT_RIGHT_OF_CENTER,
+ CHANNEL_BACK_CENTER,
+ CHANNEL_SIDE_LEFT,
+ CHANNEL_SIDE_RIGHT,
+ // From WAVEFORMAT definition.
+ CHANNEL_TOP_CENTER,
+ CHANNEL_TOP_FRONT_LEFT,
+ CHANNEL_TOP_FRONT_CENTER,
+ CHANNEL_TOP_FRONT_RIGHT,
+ CHANNEL_TOP_BACK_LEFT,
+ CHANNEL_TOP_BACK_CENTER,
+ CHANNEL_TOP_BACK_RIGHT
+ };
+
+ class ChannelLayout {
+ public:
+ // The maximum number of channels a channel map can represent.
+ static constexpr uint32_t MAX_CHANNELS = 32;
+
+ typedef uint32_t ChannelMap;
+
+ ChannelLayout() : mChannelMap(UNKNOWN_MAP), mValid(false) {}
+ explicit ChannelLayout(uint32_t aChannels)
+ : ChannelLayout(aChannels, DefaultLayoutForChannels(aChannels)) {}
+ ChannelLayout(uint32_t aChannels, const Channel* aConfig)
+ : ChannelLayout() {
+ if (aChannels == 0 || !aConfig) {
+ return;
+ }
+ mChannels.AppendElements(aConfig, aChannels);
+ UpdateChannelMap();
+ }
+ explicit ChannelLayout(std::initializer_list<Channel> aChannelList)
+ : ChannelLayout(aChannelList.size(), aChannelList.begin()) {}
+ bool operator==(const ChannelLayout& aOther) const {
+ return mChannels == aOther.mChannels;
+ }
+ bool operator!=(const ChannelLayout& aOther) const {
+ return mChannels != aOther.mChannels;
+ }
+ const Channel& operator[](uint32_t aIndex) const {
+ MOZ_ASSERT(mChannels.Length() > aIndex);
+ return mChannels[aIndex];
+ }
+ uint32_t Count() const { return mChannels.Length(); }
+ ChannelMap Map() const;
+
+ // Calculate the mapping table from the current layout to aOther such that
+ // one can easily go from one layout to the other by doing:
+ // out[channel] = in[map[channel]].
+ // Returns true if the reordering is possible or false otherwise.
+ // If true, then aMap, if set, will be updated to contain the mapping table
+ // allowing conversion from the current layout to aOther.
+ // If aMap is empty, then MappingTable can be used to simply determine if
+ // the current layout can be easily reordered to aOther.
+ bool MappingTable(const ChannelLayout& aOther,
+ nsTArray<uint8_t>* aMap = nullptr) const;
+ bool IsValid() const { return mValid; }
+ bool HasChannel(Channel aChannel) const {
+ return mChannelMap & (1 << aChannel);
+ }
+ // Return the number of channels found in this ChannelMap.
+ static uint32_t Channels(ChannelMap aMap) {
+ static_assert(sizeof(ChannelMap) == sizeof(uint32_t),
+ "Must adjust ChannelMap type");
+ return CountPopulation32(aMap);
+ }
+
+ static ChannelLayout SMPTEDefault(const ChannelLayout& aChannelLayout);
+ static ChannelLayout SMPTEDefault(ChannelMap aMap);
+
+ static constexpr ChannelMap UNKNOWN_MAP = 0;
+
+ // Common channel layout definitions.
+ static constexpr ChannelMap LMONO_MAP = 1 << CHANNEL_FRONT_CENTER;
+ static constexpr ChannelMap LMONO_LFE_MAP =
+ 1 << CHANNEL_FRONT_CENTER | 1 << CHANNEL_LFE;
+ static constexpr ChannelMap LSTEREO_MAP =
+ 1 << CHANNEL_FRONT_LEFT | 1 << CHANNEL_FRONT_RIGHT;
+ static constexpr ChannelMap LSTEREO_LFE_MAP =
+ 1 << CHANNEL_FRONT_LEFT | 1 << CHANNEL_FRONT_RIGHT | 1 << CHANNEL_LFE;
+ static constexpr ChannelMap L3F_MAP = 1 << CHANNEL_FRONT_LEFT |
+ 1 << CHANNEL_FRONT_RIGHT |
+ 1 << CHANNEL_FRONT_CENTER;
+ static constexpr ChannelMap L3F_LFE_MAP =
+ 1 << CHANNEL_FRONT_LEFT | 1 << CHANNEL_FRONT_RIGHT |
+ 1 << CHANNEL_FRONT_CENTER | 1 << CHANNEL_LFE;
+ static constexpr ChannelMap L2F1_MAP = 1 << CHANNEL_FRONT_LEFT |
+ 1 << CHANNEL_FRONT_RIGHT |
+ 1 << CHANNEL_BACK_CENTER;
+ static constexpr ChannelMap L2F1_LFE_MAP =
+ 1 << CHANNEL_FRONT_LEFT | 1 << CHANNEL_FRONT_RIGHT | 1 << CHANNEL_LFE |
+ 1 << CHANNEL_BACK_CENTER;
+ static constexpr ChannelMap L3F1_MAP =
+ 1 << CHANNEL_FRONT_LEFT | 1 << CHANNEL_FRONT_RIGHT |
+ 1 << CHANNEL_FRONT_CENTER | 1 << CHANNEL_BACK_CENTER;
+ static constexpr ChannelMap LSURROUND_MAP = L3F1_MAP;
+ static constexpr ChannelMap L3F1_LFE_MAP =
+ 1 << CHANNEL_FRONT_LEFT | 1 << CHANNEL_FRONT_RIGHT |
+ 1 << CHANNEL_FRONT_CENTER | 1 << CHANNEL_LFE | 1 << CHANNEL_BACK_CENTER;
+ static constexpr ChannelMap L2F2_MAP =
+ 1 << CHANNEL_FRONT_LEFT | 1 << CHANNEL_FRONT_RIGHT |
+ 1 << CHANNEL_SIDE_LEFT | 1 << CHANNEL_SIDE_RIGHT;
+ static constexpr ChannelMap L2F2_LFE_MAP =
+ 1 << CHANNEL_FRONT_LEFT | 1 << CHANNEL_FRONT_RIGHT | 1 << CHANNEL_LFE |
+ 1 << CHANNEL_SIDE_LEFT | 1 << CHANNEL_SIDE_RIGHT;
+ static constexpr ChannelMap LQUAD_MAP =
+ 1 << CHANNEL_FRONT_LEFT | 1 << CHANNEL_FRONT_RIGHT |
+ 1 << CHANNEL_BACK_LEFT | 1 << CHANNEL_BACK_RIGHT;
+ static constexpr ChannelMap LQUAD_LFE_MAP =
+ 1 << CHANNEL_FRONT_LEFT | 1 << CHANNEL_FRONT_RIGHT | 1 << CHANNEL_LFE |
+ 1 << CHANNEL_BACK_LEFT | 1 << CHANNEL_BACK_RIGHT;
+ static constexpr ChannelMap L3F2_MAP =
+ 1 << CHANNEL_FRONT_LEFT | 1 << CHANNEL_FRONT_RIGHT |
+ 1 << CHANNEL_FRONT_CENTER | 1 << CHANNEL_SIDE_LEFT |
+ 1 << CHANNEL_SIDE_RIGHT;
+ static constexpr ChannelMap L3F2_LFE_MAP =
+ 1 << CHANNEL_FRONT_LEFT | 1 << CHANNEL_FRONT_RIGHT |
+ 1 << CHANNEL_FRONT_CENTER | 1 << CHANNEL_LFE | 1 << CHANNEL_SIDE_LEFT |
+ 1 << CHANNEL_SIDE_RIGHT;
+ // 3F2_LFE Alias
+ static constexpr ChannelMap L5POINT1_SURROUND_MAP = L3F2_LFE_MAP;
+ static constexpr ChannelMap L3F2_BACK_MAP =
+ 1 << CHANNEL_FRONT_LEFT | 1 << CHANNEL_FRONT_RIGHT |
+ 1 << CHANNEL_FRONT_CENTER | 1 << CHANNEL_BACK_LEFT |
+ 1 << CHANNEL_BACK_RIGHT;
+ static constexpr ChannelMap L3F2_BACK_LFE_MAP =
+ L3F2_BACK_MAP | 1 << CHANNEL_LFE;
+ static constexpr ChannelMap L3F3R_LFE_MAP =
+ 1 << CHANNEL_FRONT_LEFT | 1 << CHANNEL_FRONT_RIGHT |
+ 1 << CHANNEL_FRONT_CENTER | 1 << CHANNEL_LFE |
+ 1 << CHANNEL_BACK_CENTER | 1 << CHANNEL_SIDE_LEFT |
+ 1 << CHANNEL_SIDE_RIGHT;
+ static ChannelLayout L3F4_LFE;
+ static constexpr ChannelMap L3F4_LFE_MAP =
+ 1 << CHANNEL_FRONT_LEFT | 1 << CHANNEL_FRONT_RIGHT |
+ 1 << CHANNEL_FRONT_CENTER | 1 << CHANNEL_LFE | 1 << CHANNEL_BACK_LEFT |
+ 1 << CHANNEL_BACK_RIGHT | 1 << CHANNEL_SIDE_LEFT |
+ 1 << CHANNEL_SIDE_RIGHT;
+ // 3F4_LFE Alias
+ static ChannelLayout L7POINT1_SURROUND;
+ static constexpr ChannelMap L7POINT1_SURROUND_MAP = L3F4_LFE_MAP;
+
+ // Statically check that we can static_cast a Gecko ChannelLayout to a
+ // cubeb_channel_layout.
+ static_assert(CUBEB_LAYOUT_UNDEFINED == UNKNOWN_MAP);
+ static_assert(CUBEB_LAYOUT_MONO == LMONO_MAP);
+ static_assert(CUBEB_LAYOUT_MONO_LFE == LMONO_LFE_MAP);
+ static_assert(CUBEB_LAYOUT_STEREO == LSTEREO_MAP);
+ static_assert(CUBEB_LAYOUT_STEREO_LFE == LSTEREO_LFE_MAP);
+ static_assert(CUBEB_LAYOUT_3F == L3F_MAP);
+ static_assert(CUBEB_LAYOUT_3F_LFE == L3F_LFE_MAP);
+ static_assert(CUBEB_LAYOUT_2F1 == L2F1_MAP);
+ static_assert(CUBEB_LAYOUT_2F1_LFE == L2F1_LFE_MAP);
+ static_assert(CUBEB_LAYOUT_3F1 == L3F1_MAP);
+ static_assert(CUBEB_LAYOUT_3F1_LFE == L3F1_LFE_MAP);
+ static_assert(CUBEB_LAYOUT_2F2 == L2F2_MAP);
+ static_assert(CUBEB_LAYOUT_3F2_LFE == L3F2_LFE_MAP);
+ static_assert(CUBEB_LAYOUT_QUAD == LQUAD_MAP);
+ static_assert(CUBEB_LAYOUT_QUAD_LFE == LQUAD_LFE_MAP);
+ static_assert(CUBEB_LAYOUT_3F2 == L3F2_MAP);
+ static_assert(CUBEB_LAYOUT_3F2_LFE == L3F2_LFE_MAP);
+ static_assert(CUBEB_LAYOUT_3F2_BACK == L3F2_BACK_MAP);
+ static_assert(CUBEB_LAYOUT_3F2_LFE_BACK == L3F2_BACK_LFE_MAP);
+ static_assert(CUBEB_LAYOUT_3F3R_LFE == L3F3R_LFE_MAP);
+ static_assert(CUBEB_LAYOUT_3F4_LFE == L3F4_LFE_MAP);
+
+ private:
+ void UpdateChannelMap();
+ const Channel* DefaultLayoutForChannels(uint32_t aChannels) const;
+ CopyableAutoTArray<Channel, MAX_CHANNELS> mChannels;
+ ChannelMap mChannelMap;
+ bool mValid;
+ };
+
+ enum SampleFormat {
+ FORMAT_NONE = 0,
+ FORMAT_U8,
+ FORMAT_S16,
+ FORMAT_S24LSB,
+ FORMAT_S24,
+ FORMAT_S32,
+ FORMAT_FLT,
+# if defined(MOZ_SAMPLE_TYPE_FLOAT32)
+ FORMAT_DEFAULT = FORMAT_FLT
+# elif defined(MOZ_SAMPLE_TYPE_S16)
+ FORMAT_DEFAULT = FORMAT_S16
+# else
+# error "Not supported audio type"
+# endif
+ };
+
+ AudioConfig(const ChannelLayout& aChannelLayout, uint32_t aRate,
+ AudioConfig::SampleFormat aFormat = FORMAT_DEFAULT,
+ bool aInterleaved = true);
+ AudioConfig(const ChannelLayout& aChannelLayout, uint32_t aChannels,
+ uint32_t aRate,
+ AudioConfig::SampleFormat aFormat = FORMAT_DEFAULT,
+ bool aInterleaved = true);
+ // Will create a channel configuration from default SMPTE ordering.
+ AudioConfig(uint32_t aChannels, uint32_t aRate,
+ AudioConfig::SampleFormat aFormat = FORMAT_DEFAULT,
+ bool aInterleaved = true);
+
+ const ChannelLayout& Layout() const { return mChannelLayout; }
+ uint32_t Channels() const {
+ if (!mChannelLayout.IsValid()) {
+ return mChannels;
+ }
+ return mChannelLayout.Count();
+ }
+ uint32_t Rate() const { return mRate; }
+ SampleFormat Format() const { return mFormat; }
+ bool Interleaved() const { return mInterleaved; }
+ bool operator==(const AudioConfig& aOther) const {
+ return mChannelLayout == aOther.mChannelLayout && mRate == aOther.mRate &&
+ mFormat == aOther.mFormat && mInterleaved == aOther.mInterleaved;
+ }
+ bool operator!=(const AudioConfig& aOther) const {
+ return !(*this == aOther);
+ }
+
+ bool IsValid() const {
+ return mChannelLayout.IsValid() && Format() != FORMAT_NONE && Rate() > 0;
+ }
+
+ static const char* FormatToString(SampleFormat aFormat);
+ static uint32_t SampleSize(SampleFormat aFormat);
+ static uint32_t FormatToBits(SampleFormat aFormat);
+
+ private:
+ // Channels configuration.
+ ChannelLayout mChannelLayout;
+
+ // Channel count.
+ uint32_t mChannels;
+
+ // Sample rate.
+ uint32_t mRate;
+
+ // Sample format.
+ SampleFormat mFormat;
+
+ bool mInterleaved;
+};
+
+} // namespace mozilla
+
+#endif // AudioLayout_h
diff --git a/dom/media/AudioConverter.cpp b/dom/media/AudioConverter.cpp
new file mode 100644
index 0000000000..1f58608043
--- /dev/null
+++ b/dom/media/AudioConverter.cpp
@@ -0,0 +1,480 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioConverter.h"
+#include <speex/speex_resampler.h>
+#include <string.h>
+#include <cmath>
+
+/*
+ * Parts derived from MythTV AudioConvert Class
+ * Created by Jean-Yves Avenard.
+ *
+ * Copyright (C) Bubblestuff Pty Ltd 2013
+ * Copyright (C) foobum@gmail.com 2010
+ */
+
+namespace mozilla {
+
+AudioConverter::AudioConverter(const AudioConfig& aIn, const AudioConfig& aOut)
+ : mIn(aIn), mOut(aOut), mResampler(nullptr) {
+ MOZ_DIAGNOSTIC_ASSERT(CanConvert(aIn, aOut),
+ "The conversion is not supported");
+ mIn.Layout().MappingTable(mOut.Layout(), &mChannelOrderMap);
+ if (aIn.Rate() != aOut.Rate()) {
+ RecreateResampler();
+ }
+}
+
+AudioConverter::~AudioConverter() {
+ if (mResampler) {
+ speex_resampler_destroy(mResampler);
+ mResampler = nullptr;
+ }
+}
+
+bool AudioConverter::CanConvert(const AudioConfig& aIn,
+ const AudioConfig& aOut) {
+ if (aIn.Format() != aOut.Format() ||
+ aIn.Interleaved() != aOut.Interleaved()) {
+ NS_WARNING("No format conversion is supported at this stage");
+ return false;
+ }
+ if (aIn.Channels() != aOut.Channels() && aOut.Channels() > 2) {
+ NS_WARNING(
+ "Only down/upmixing to mono or stereo is supported at this stage");
+ return false;
+ }
+ if (!aOut.Interleaved()) {
+ NS_WARNING("planar audio format not supported");
+ return false;
+ }
+ return true;
+}
+
+bool AudioConverter::CanWorkInPlace() const {
+ bool needDownmix = mIn.Channels() > mOut.Channels();
+ bool needUpmix = mIn.Channels() < mOut.Channels();
+ bool canDownmixInPlace =
+ mIn.Channels() * AudioConfig::SampleSize(mIn.Format()) >=
+ mOut.Channels() * AudioConfig::SampleSize(mOut.Format());
+ bool needResample = mIn.Rate() != mOut.Rate();
+ bool canResampleInPlace = mIn.Rate() >= mOut.Rate();
+ // We should be able to work in place if 1s of audio input takes less space
+ // than 1s of audio output. However, as we downmix before resampling we can't
+ // perform any upsampling in place (e.g. if incoming rate >= outgoing rate)
+ return !needUpmix && (!needDownmix || canDownmixInPlace) &&
+ (!needResample || canResampleInPlace);
+}
+
+size_t AudioConverter::ProcessInternal(void* aOut, const void* aIn,
+ size_t aFrames) {
+ if (!aFrames) {
+ return 0;
+ }
+ if (mIn.Channels() > mOut.Channels()) {
+ return DownmixAudio(aOut, aIn, aFrames);
+ } else if (mIn.Channels() < mOut.Channels()) {
+ return UpmixAudio(aOut, aIn, aFrames);
+ } else if (mIn.Layout() != mOut.Layout() && CanReorderAudio()) {
+ ReOrderInterleavedChannels(aOut, aIn, aFrames);
+ } else if (aIn != aOut) {
+ memmove(aOut, aIn, FramesOutToBytes(aFrames));
+ }
+ return aFrames;
+}
+
+// Reorder interleaved channels.
+// Can work in place (e.g aOut == aIn).
+template <class AudioDataType>
+void _ReOrderInterleavedChannels(AudioDataType* aOut, const AudioDataType* aIn,
+ uint32_t aFrames, uint32_t aChannels,
+ const uint8_t* aChannelOrderMap) {
+ MOZ_DIAGNOSTIC_ASSERT(aChannels <= AudioConfig::ChannelLayout::MAX_CHANNELS);
+ AudioDataType val[AudioConfig::ChannelLayout::MAX_CHANNELS];
+ for (uint32_t i = 0; i < aFrames; i++) {
+ for (uint32_t j = 0; j < aChannels; j++) {
+ val[j] = aIn[aChannelOrderMap[j]];
+ }
+ for (uint32_t j = 0; j < aChannels; j++) {
+ aOut[j] = val[j];
+ }
+ aOut += aChannels;
+ aIn += aChannels;
+ }
+}
+
+void AudioConverter::ReOrderInterleavedChannels(void* aOut, const void* aIn,
+ size_t aFrames) const {
+ MOZ_DIAGNOSTIC_ASSERT(mIn.Channels() == mOut.Channels());
+ MOZ_DIAGNOSTIC_ASSERT(CanReorderAudio());
+
+ if (mChannelOrderMap.IsEmpty() || mOut.Channels() == 1 ||
+ mOut.Layout() == mIn.Layout()) {
+ // If channel count is 1, planar and non-planar formats are the same or
+ // there's nothing to reorder, or if we don't know how to re-order.
+ if (aOut != aIn) {
+ memmove(aOut, aIn, FramesOutToBytes(aFrames));
+ }
+ return;
+ }
+
+ uint32_t bits = AudioConfig::FormatToBits(mOut.Format());
+ switch (bits) {
+ case 8:
+ _ReOrderInterleavedChannels((uint8_t*)aOut, (const uint8_t*)aIn, aFrames,
+ mIn.Channels(), mChannelOrderMap.Elements());
+ break;
+ case 16:
+ _ReOrderInterleavedChannels((int16_t*)aOut, (const int16_t*)aIn, aFrames,
+ mIn.Channels(), mChannelOrderMap.Elements());
+ break;
+ default:
+ MOZ_DIAGNOSTIC_ASSERT(AudioConfig::SampleSize(mOut.Format()) == 4);
+ _ReOrderInterleavedChannels((int32_t*)aOut, (const int32_t*)aIn, aFrames,
+ mIn.Channels(), mChannelOrderMap.Elements());
+ break;
+ }
+}
+
+static inline int16_t clipTo15(int32_t aX) {
+ return aX < -32768 ? -32768 : aX <= 32767 ? aX : 32767;
+}
+
+template <typename TYPE>
+static void dumbUpDownMix(TYPE* aOut, int32_t aOutChannels, const TYPE* aIn,
+ int32_t aInChannels, int32_t aFrames) {
+ if (aIn == aOut) {
+ return;
+ }
+ int32_t commonChannels = std::min(aInChannels, aOutChannels);
+
+ for (int32_t i = 0; i < aFrames; i++) {
+ for (int32_t j = 0; j < commonChannels; j++) {
+ aOut[i * aOutChannels + j] = aIn[i * aInChannels + j];
+ }
+ if (aOutChannels > aInChannels) {
+ for (int32_t j = 0; j < aInChannels - aOutChannels; j++) {
+ aOut[i * aOutChannels + j] = 0;
+ }
+ }
+ }
+}
+
+size_t AudioConverter::DownmixAudio(void* aOut, const void* aIn,
+ size_t aFrames) const {
+ MOZ_DIAGNOSTIC_ASSERT(mIn.Format() == AudioConfig::FORMAT_S16 ||
+ mIn.Format() == AudioConfig::FORMAT_FLT);
+ MOZ_DIAGNOSTIC_ASSERT(mIn.Channels() >= mOut.Channels());
+ MOZ_DIAGNOSTIC_ASSERT(mOut.Layout() == AudioConfig::ChannelLayout(2) ||
+ mOut.Layout() == AudioConfig::ChannelLayout(1));
+
+ uint32_t inChannels = mIn.Channels();
+ uint32_t outChannels = mOut.Channels();
+
+ if (inChannels == outChannels) {
+ if (aOut != aIn) {
+ memmove(aOut, aIn, FramesOutToBytes(aFrames));
+ }
+ return aFrames;
+ }
+
+ if (!mIn.Layout().IsValid() || !mOut.Layout().IsValid()) {
+ // Dumb copy dropping extra channels.
+ if (mIn.Format() == AudioConfig::FORMAT_FLT) {
+ dumbUpDownMix(static_cast<float*>(aOut), outChannels,
+ static_cast<const float*>(aIn), inChannels, aFrames);
+ } else if (mIn.Format() == AudioConfig::FORMAT_S16) {
+ dumbUpDownMix(static_cast<int16_t*>(aOut), outChannels,
+ static_cast<const int16_t*>(aIn), inChannels, aFrames);
+ } else {
+ MOZ_DIAGNOSTIC_ASSERT(false, "Unsupported data type");
+ }
+ return aFrames;
+ }
+
+ MOZ_ASSERT(
+ mIn.Layout() == AudioConfig::ChannelLayout::SMPTEDefault(mIn.Layout()),
+ "Can only downmix input data in SMPTE layout");
+ if (inChannels > 2) {
+ if (mIn.Format() == AudioConfig::FORMAT_FLT) {
+ // Downmix matrix. Per-row normalization 1 for rows 3,4 and 2 for rows
+ // 5-8.
+ static const float dmatrix[6][8][2] = {
+ /*3*/ {{0.5858f, 0}, {0, 0.5858f}, {0.4142f, 0.4142f}},
+ /*4*/
+ {{0.4226f, 0}, {0, 0.4226f}, {0.366f, 0.2114f}, {0.2114f, 0.366f}},
+ /*5*/
+ {{0.6510f, 0},
+ {0, 0.6510f},
+ {0.4600f, 0.4600f},
+ {0.5636f, 0.3254f},
+ {0.3254f, 0.5636f}},
+ /*6*/
+ {{0.5290f, 0},
+ {0, 0.5290f},
+ {0.3741f, 0.3741f},
+ {0.3741f, 0.3741f},
+ {0.4582f, 0.2645f},
+ {0.2645f, 0.4582f}},
+ /*7*/
+ {{0.4553f, 0},
+ {0, 0.4553f},
+ {0.3220f, 0.3220f},
+ {0.3220f, 0.3220f},
+ {0.2788f, 0.2788f},
+ {0.3943f, 0.2277f},
+ {0.2277f, 0.3943f}},
+ /*8*/
+ {{0.3886f, 0},
+ {0, 0.3886f},
+ {0.2748f, 0.2748f},
+ {0.2748f, 0.2748f},
+ {0.3366f, 0.1943f},
+ {0.1943f, 0.3366f},
+ {0.3366f, 0.1943f},
+ {0.1943f, 0.3366f}},
+ };
+ // Re-write the buffer with downmixed data
+ const float* in = static_cast<const float*>(aIn);
+ float* out = static_cast<float*>(aOut);
+ for (uint32_t i = 0; i < aFrames; i++) {
+ float sampL = 0.0;
+ float sampR = 0.0;
+ for (uint32_t j = 0; j < inChannels; j++) {
+ sampL += in[i * inChannels + j] * dmatrix[inChannels - 3][j][0];
+ sampR += in[i * inChannels + j] * dmatrix[inChannels - 3][j][1];
+ }
+ if (outChannels == 2) {
+ *out++ = sampL;
+ *out++ = sampR;
+ } else {
+ *out++ = (sampL + sampR) * 0.5;
+ }
+ }
+ } else if (mIn.Format() == AudioConfig::FORMAT_S16) {
+ // Downmix matrix. Per-row normalization 1 for rows 3,4 and 2 for rows
+ // 5-8. Coefficients in Q14.
+ static const int16_t dmatrix[6][8][2] = {
+ /*3*/ {{9598, 0}, {0, 9598}, {6786, 6786}},
+ /*4*/ {{6925, 0}, {0, 6925}, {5997, 3462}, {3462, 5997}},
+ /*5*/
+ {{10663, 0}, {0, 10663}, {7540, 7540}, {9234, 5331}, {5331, 9234}},
+ /*6*/
+ {{8668, 0},
+ {0, 8668},
+ {6129, 6129},
+ {6129, 6129},
+ {7507, 4335},
+ {4335, 7507}},
+ /*7*/
+ {{7459, 0},
+ {0, 7459},
+ {5275, 5275},
+ {5275, 5275},
+ {4568, 4568},
+ {6460, 3731},
+ {3731, 6460}},
+ /*8*/
+ {{6368, 0},
+ {0, 6368},
+ {4502, 4502},
+ {4502, 4502},
+ {5514, 3184},
+ {3184, 5514},
+ {5514, 3184},
+ {3184, 5514}}};
+ // Re-write the buffer with downmixed data
+ const int16_t* in = static_cast<const int16_t*>(aIn);
+ int16_t* out = static_cast<int16_t*>(aOut);
+ for (uint32_t i = 0; i < aFrames; i++) {
+ int32_t sampL = 0;
+ int32_t sampR = 0;
+ for (uint32_t j = 0; j < inChannels; j++) {
+ sampL += in[i * inChannels + j] * dmatrix[inChannels - 3][j][0];
+ sampR += in[i * inChannels + j] * dmatrix[inChannels - 3][j][1];
+ }
+ sampL = clipTo15((sampL + 8192) >> 14);
+ sampR = clipTo15((sampR + 8192) >> 14);
+ if (outChannels == 2) {
+ *out++ = sampL;
+ *out++ = sampR;
+ } else {
+ *out++ = (sampL + sampR) * 0.5;
+ }
+ }
+ } else {
+ MOZ_DIAGNOSTIC_ASSERT(false, "Unsupported data type");
+ }
+ return aFrames;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(inChannels == 2 && outChannels == 1);
+ if (mIn.Format() == AudioConfig::FORMAT_FLT) {
+ const float* in = static_cast<const float*>(aIn);
+ float* out = static_cast<float*>(aOut);
+ for (size_t fIdx = 0; fIdx < aFrames; ++fIdx) {
+ float sample = 0.0;
+ // The sample of the buffer would be interleaved.
+ sample = (in[fIdx * inChannels] + in[fIdx * inChannels + 1]) * 0.5;
+ *out++ = sample;
+ }
+ } else if (mIn.Format() == AudioConfig::FORMAT_S16) {
+ const int16_t* in = static_cast<const int16_t*>(aIn);
+ int16_t* out = static_cast<int16_t*>(aOut);
+ for (size_t fIdx = 0; fIdx < aFrames; ++fIdx) {
+ int32_t sample = 0.0;
+ // The sample of the buffer would be interleaved.
+ sample = (in[fIdx * inChannels] + in[fIdx * inChannels + 1]) * 0.5;
+ *out++ = sample;
+ }
+ } else {
+ MOZ_DIAGNOSTIC_ASSERT(false, "Unsupported data type");
+ }
+ return aFrames;
+}
+
+size_t AudioConverter::ResampleAudio(void* aOut, const void* aIn,
+ size_t aFrames) {
+ if (!mResampler) {
+ return 0;
+ }
+ uint32_t outframes = ResampleRecipientFrames(aFrames);
+ uint32_t inframes = aFrames;
+
+ int error;
+ if (mOut.Format() == AudioConfig::FORMAT_FLT) {
+ const float* in = reinterpret_cast<const float*>(aIn);
+ float* out = reinterpret_cast<float*>(aOut);
+ error = speex_resampler_process_interleaved_float(mResampler, in, &inframes,
+ out, &outframes);
+ } else if (mOut.Format() == AudioConfig::FORMAT_S16) {
+ const int16_t* in = reinterpret_cast<const int16_t*>(aIn);
+ int16_t* out = reinterpret_cast<int16_t*>(aOut);
+ error = speex_resampler_process_interleaved_int(mResampler, in, &inframes,
+ out, &outframes);
+ } else {
+ MOZ_DIAGNOSTIC_ASSERT(false, "Unsupported data type");
+ error = RESAMPLER_ERR_ALLOC_FAILED;
+ }
+ MOZ_ASSERT(error == RESAMPLER_ERR_SUCCESS);
+ if (error != RESAMPLER_ERR_SUCCESS) {
+ speex_resampler_destroy(mResampler);
+ mResampler = nullptr;
+ return 0;
+ }
+ MOZ_ASSERT(inframes == aFrames, "Some frames will be dropped");
+ return outframes;
+}
+
+void AudioConverter::RecreateResampler() {
+ if (mResampler) {
+ speex_resampler_destroy(mResampler);
+ }
+ int error;
+ mResampler = speex_resampler_init(mOut.Channels(), mIn.Rate(), mOut.Rate(),
+ SPEEX_RESAMPLER_QUALITY_DEFAULT, &error);
+
+ if (error == RESAMPLER_ERR_SUCCESS) {
+ speex_resampler_skip_zeros(mResampler);
+ } else {
+ NS_WARNING("Failed to initialize resampler.");
+ mResampler = nullptr;
+ }
+}
+
+size_t AudioConverter::DrainResampler(void* aOut) {
+ if (!mResampler) {
+ return 0;
+ }
+ int frames = speex_resampler_get_input_latency(mResampler);
+ AlignedByteBuffer buffer(FramesOutToBytes(frames));
+ if (!buffer) {
+ // OOM
+ return 0;
+ }
+ frames = ResampleAudio(aOut, buffer.Data(), frames);
+ // Tore down the resampler as it's easier than handling follow-up.
+ RecreateResampler();
+ return frames;
+}
+
+size_t AudioConverter::UpmixAudio(void* aOut, const void* aIn,
+ size_t aFrames) const {
+ MOZ_ASSERT(mIn.Format() == AudioConfig::FORMAT_S16 ||
+ mIn.Format() == AudioConfig::FORMAT_FLT);
+ MOZ_ASSERT(mIn.Channels() < mOut.Channels());
+ MOZ_ASSERT(mIn.Channels() == 1, "Can only upmix mono for now");
+ MOZ_ASSERT(mOut.Channels() == 2, "Can only upmix to stereo for now");
+
+ if (!mIn.Layout().IsValid() || !mOut.Layout().IsValid() ||
+ mOut.Channels() != 2) {
+ // Dumb copy the channels and insert silence for the extra channels.
+ if (mIn.Format() == AudioConfig::FORMAT_FLT) {
+ dumbUpDownMix(static_cast<float*>(aOut), mOut.Channels(),
+ static_cast<const float*>(aIn), mIn.Channels(), aFrames);
+ } else if (mIn.Format() == AudioConfig::FORMAT_S16) {
+ dumbUpDownMix(static_cast<int16_t*>(aOut), mOut.Channels(),
+ static_cast<const int16_t*>(aIn), mIn.Channels(), aFrames);
+ } else {
+ MOZ_DIAGNOSTIC_ASSERT(false, "Unsupported data type");
+ }
+ return aFrames;
+ }
+
+ // Upmix mono to stereo.
+ // This is a very dumb mono to stereo upmixing, power levels are preserved
+ // following the calculation: left = right = -3dB*mono.
+ if (mIn.Format() == AudioConfig::FORMAT_FLT) {
+ const float m3db = std::sqrt(0.5); // -3dB = sqrt(1/2)
+ const float* in = static_cast<const float*>(aIn);
+ float* out = static_cast<float*>(aOut);
+ for (size_t fIdx = 0; fIdx < aFrames; ++fIdx) {
+ float sample = in[fIdx] * m3db;
+ // The samples of the buffer would be interleaved.
+ *out++ = sample;
+ *out++ = sample;
+ }
+ } else if (mIn.Format() == AudioConfig::FORMAT_S16) {
+ const int16_t* in = static_cast<const int16_t*>(aIn);
+ int16_t* out = static_cast<int16_t*>(aOut);
+ for (size_t fIdx = 0; fIdx < aFrames; ++fIdx) {
+ int16_t sample =
+ ((int32_t)in[fIdx] * 11585) >> 14; // close enough to i*sqrt(0.5)
+ // The samples of the buffer would be interleaved.
+ *out++ = sample;
+ *out++ = sample;
+ }
+ } else {
+ MOZ_DIAGNOSTIC_ASSERT(false, "Unsupported data type");
+ }
+
+ return aFrames;
+}
+
+size_t AudioConverter::ResampleRecipientFrames(size_t aFrames) const {
+ if (!aFrames && mIn.Rate() != mOut.Rate()) {
+ if (!mResampler) {
+ return 0;
+ }
+ // We drain by pushing in get_input_latency() samples of 0
+ aFrames = speex_resampler_get_input_latency(mResampler);
+ }
+ return (uint64_t)aFrames * mOut.Rate() / mIn.Rate() + 1;
+}
+
+size_t AudioConverter::FramesOutToSamples(size_t aFrames) const {
+ return aFrames * mOut.Channels();
+}
+
+size_t AudioConverter::SamplesInToFrames(size_t aSamples) const {
+ return aSamples / mIn.Channels();
+}
+
+size_t AudioConverter::FramesOutToBytes(size_t aFrames) const {
+ return FramesOutToSamples(aFrames) * AudioConfig::SampleSize(mOut.Format());
+}
+} // namespace mozilla
diff --git a/dom/media/AudioConverter.h b/dom/media/AudioConverter.h
new file mode 100644
index 0000000000..0ace580b26
--- /dev/null
+++ b/dom/media/AudioConverter.h
@@ -0,0 +1,277 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(AudioConverter_h)
+# define AudioConverter_h
+
+# include "MediaInfo.h"
+
+// Forward declaration
+typedef struct SpeexResamplerState_ SpeexResamplerState;
+
+namespace mozilla {
+
+template <AudioConfig::SampleFormat T>
+struct AudioDataBufferTypeChooser;
+template <>
+struct AudioDataBufferTypeChooser<AudioConfig::FORMAT_U8> {
+ typedef uint8_t Type;
+};
+template <>
+struct AudioDataBufferTypeChooser<AudioConfig::FORMAT_S16> {
+ typedef int16_t Type;
+};
+template <>
+struct AudioDataBufferTypeChooser<AudioConfig::FORMAT_S24LSB> {
+ typedef int32_t Type;
+};
+template <>
+struct AudioDataBufferTypeChooser<AudioConfig::FORMAT_S24> {
+ typedef int32_t Type;
+};
+template <>
+struct AudioDataBufferTypeChooser<AudioConfig::FORMAT_S32> {
+ typedef int32_t Type;
+};
+template <>
+struct AudioDataBufferTypeChooser<AudioConfig::FORMAT_FLT> {
+ typedef float Type;
+};
+
+// 'Value' is the type used externally to deal with stored value.
+// AudioDataBuffer can perform conversion between different SampleFormat
+// content.
+template <AudioConfig::SampleFormat Format,
+ typename Value = typename AudioDataBufferTypeChooser<Format>::Type>
+class AudioDataBuffer {
+ public:
+ AudioDataBuffer() = default;
+ AudioDataBuffer(Value* aBuffer, size_t aLength) : mBuffer(aBuffer, aLength) {}
+ explicit AudioDataBuffer(const AudioDataBuffer& aOther)
+ : mBuffer(aOther.mBuffer) {}
+ AudioDataBuffer(AudioDataBuffer&& aOther)
+ : mBuffer(std::move(aOther.mBuffer)) {}
+ template <AudioConfig::SampleFormat OtherFormat, typename OtherValue>
+ explicit AudioDataBuffer(
+ const AudioDataBuffer<OtherFormat, OtherValue>& other) {
+ // TODO: Convert from different type, may use asm routines.
+ MOZ_CRASH("Conversion not implemented yet");
+ }
+
+ // A u8, s16 and float aligned buffer can only be treated as
+ // FORMAT_U8, FORMAT_S16 and FORMAT_FLT respectively.
+ // So allow them as copy and move constructors.
+ explicit AudioDataBuffer(const AlignedByteBuffer& aBuffer)
+ : mBuffer(aBuffer) {
+ static_assert(Format == AudioConfig::FORMAT_U8,
+ "Conversion not implemented yet");
+ }
+ explicit AudioDataBuffer(const AlignedShortBuffer& aBuffer)
+ : mBuffer(aBuffer) {
+ static_assert(Format == AudioConfig::FORMAT_S16,
+ "Conversion not implemented yet");
+ }
+ explicit AudioDataBuffer(const AlignedFloatBuffer& aBuffer)
+ : mBuffer(aBuffer) {
+ static_assert(Format == AudioConfig::FORMAT_FLT,
+ "Conversion not implemented yet");
+ }
+ explicit AudioDataBuffer(AlignedByteBuffer&& aBuffer)
+ : mBuffer(std::move(aBuffer)) {
+ static_assert(Format == AudioConfig::FORMAT_U8,
+ "Conversion not implemented yet");
+ }
+ explicit AudioDataBuffer(AlignedShortBuffer&& aBuffer)
+ : mBuffer(std::move(aBuffer)) {
+ static_assert(Format == AudioConfig::FORMAT_S16,
+ "Conversion not implemented yet");
+ }
+ explicit AudioDataBuffer(AlignedFloatBuffer&& aBuffer)
+ : mBuffer(std::move(aBuffer)) {
+ static_assert(Format == AudioConfig::FORMAT_FLT,
+ "Conversion not implemented yet");
+ }
+ AudioDataBuffer& operator=(AudioDataBuffer&& aOther) {
+ mBuffer = std::move(aOther.mBuffer);
+ return *this;
+ }
+ AudioDataBuffer& operator=(const AudioDataBuffer& aOther) {
+ mBuffer = aOther.mBuffer;
+ return *this;
+ }
+
+ Value* Data() const { return mBuffer.Data(); }
+ size_t Length() const { return mBuffer.Length(); }
+ size_t Size() const { return mBuffer.Size(); }
+ AlignedBuffer<Value> Forget() {
+ // Correct type -> Just give values as-is.
+ return std::move(mBuffer);
+ }
+
+ private:
+ AlignedBuffer<Value> mBuffer;
+};
+
+typedef AudioDataBuffer<AudioConfig::FORMAT_DEFAULT> AudioSampleBuffer;
+
+class AudioConverter {
+ public:
+ AudioConverter(const AudioConfig& aIn, const AudioConfig& aOut);
+ ~AudioConverter();
+
+ // Convert the AudioDataBuffer.
+ // Conversion will be done in place if possible. Otherwise a new buffer will
+ // be returned.
+ // Providing an empty buffer and resampling is expected, the resampler
+ // will be drained.
+ template <AudioConfig::SampleFormat Format, typename Value>
+ AudioDataBuffer<Format, Value> Process(
+ AudioDataBuffer<Format, Value>&& aBuffer) {
+ MOZ_DIAGNOSTIC_ASSERT(mIn.Format() == mOut.Format() &&
+ mIn.Format() == Format);
+ AudioDataBuffer<Format, Value> buffer = std::move(aBuffer);
+ if (CanWorkInPlace()) {
+ AlignedBuffer<Value> temp = buffer.Forget();
+ Process(temp, temp.Data(), SamplesInToFrames(temp.Length()));
+ return AudioDataBuffer<Format, Value>(std::move(temp));
+ ;
+ }
+ return Process(buffer);
+ }
+
+ template <AudioConfig::SampleFormat Format, typename Value>
+ AudioDataBuffer<Format, Value> Process(
+ const AudioDataBuffer<Format, Value>& aBuffer) {
+ MOZ_DIAGNOSTIC_ASSERT(mIn.Format() == mOut.Format() &&
+ mIn.Format() == Format);
+ // Perform the downmixing / reordering in temporary buffer.
+ size_t frames = SamplesInToFrames(aBuffer.Length());
+ AlignedBuffer<Value> temp1;
+ if (!temp1.SetLength(FramesOutToSamples(frames))) {
+ return AudioDataBuffer<Format, Value>(std::move(temp1));
+ }
+ frames = ProcessInternal(temp1.Data(), aBuffer.Data(), frames);
+ if (mIn.Rate() == mOut.Rate()) {
+ MOZ_ALWAYS_TRUE(temp1.SetLength(FramesOutToSamples(frames)));
+ return AudioDataBuffer<Format, Value>(std::move(temp1));
+ }
+
+ // At this point, temp1 contains the buffer reordered and downmixed.
+ // If we are downsampling we can re-use it.
+ AlignedBuffer<Value>* outputBuffer = &temp1;
+ AlignedBuffer<Value> temp2;
+ if (!frames || mOut.Rate() > mIn.Rate()) {
+ // We are upsampling or about to drain, we can't work in place.
+ // Allocate another temporary buffer where the upsampling will occur.
+ if (!temp2.SetLength(
+ FramesOutToSamples(ResampleRecipientFrames(frames)))) {
+ return AudioDataBuffer<Format, Value>(std::move(temp2));
+ }
+ outputBuffer = &temp2;
+ }
+ if (!frames) {
+ frames = DrainResampler(outputBuffer->Data());
+ } else {
+ frames = ResampleAudio(outputBuffer->Data(), temp1.Data(), frames);
+ }
+ MOZ_ALWAYS_TRUE(outputBuffer->SetLength(FramesOutToSamples(frames)));
+ return AudioDataBuffer<Format, Value>(std::move(*outputBuffer));
+ }
+
+ // Attempt to convert the AudioDataBuffer in place.
+ // Will return 0 if the conversion wasn't possible.
+ template <typename Value>
+ size_t Process(Value* aBuffer, size_t aFrames) {
+ MOZ_DIAGNOSTIC_ASSERT(mIn.Format() == mOut.Format());
+ if (!CanWorkInPlace()) {
+ return 0;
+ }
+ size_t frames = ProcessInternal(aBuffer, aBuffer, aFrames);
+ if (frames && mIn.Rate() != mOut.Rate()) {
+ frames = ResampleAudio(aBuffer, aBuffer, aFrames);
+ }
+ return frames;
+ }
+
+ template <typename Value>
+ size_t Process(AlignedBuffer<Value>& aOutBuffer, const Value* aInBuffer,
+ size_t aFrames) {
+ MOZ_DIAGNOSTIC_ASSERT(mIn.Format() == mOut.Format());
+ MOZ_ASSERT((aFrames && aInBuffer) || !aFrames);
+ // Up/down mixing first
+ if (!aOutBuffer.SetLength(FramesOutToSamples(aFrames))) {
+ MOZ_ALWAYS_TRUE(aOutBuffer.SetLength(0));
+ return 0;
+ }
+ size_t frames = ProcessInternal(aOutBuffer.Data(), aInBuffer, aFrames);
+ MOZ_ASSERT(frames == aFrames);
+ // Check if resampling is needed
+ if (mIn.Rate() == mOut.Rate()) {
+ return frames;
+ }
+ // Prepare output in cases of drain or up-sampling
+ if ((!frames || mOut.Rate() > mIn.Rate()) &&
+ !aOutBuffer.SetLength(
+ FramesOutToSamples(ResampleRecipientFrames(frames)))) {
+ MOZ_ALWAYS_TRUE(aOutBuffer.SetLength(0));
+ return 0;
+ }
+ if (!frames) {
+ frames = DrainResampler(aOutBuffer.Data());
+ } else {
+ frames = ResampleAudio(aOutBuffer.Data(), aInBuffer, frames);
+ }
+ // Update with the actual buffer length
+ MOZ_ALWAYS_TRUE(aOutBuffer.SetLength(FramesOutToSamples(frames)));
+ return frames;
+ }
+
+ bool CanWorkInPlace() const;
+ bool CanReorderAudio() const {
+ return mIn.Layout().MappingTable(mOut.Layout());
+ }
+ static bool CanConvert(const AudioConfig& aIn, const AudioConfig& aOut);
+
+ const AudioConfig& InputConfig() const { return mIn; }
+ const AudioConfig& OutputConfig() const { return mOut; }
+
+ private:
+ const AudioConfig mIn;
+ const AudioConfig mOut;
+ // mChannelOrderMap will be empty if we do not know how to proceed with this
+ // channel layout.
+ AutoTArray<uint8_t, AudioConfig::ChannelLayout::MAX_CHANNELS>
+ mChannelOrderMap;
+ /**
+ * ProcessInternal
+ * Parameters:
+ * aOut : destination buffer where converted samples will be copied
+ * aIn : source buffer
+ * aSamples: number of frames in source buffer
+ *
+ * Return Value: number of frames converted or 0 if error
+ */
+ size_t ProcessInternal(void* aOut, const void* aIn, size_t aFrames);
+ void ReOrderInterleavedChannels(void* aOut, const void* aIn,
+ size_t aFrames) const;
+ size_t DownmixAudio(void* aOut, const void* aIn, size_t aFrames) const;
+ size_t UpmixAudio(void* aOut, const void* aIn, size_t aFrames) const;
+
+ size_t FramesOutToSamples(size_t aFrames) const;
+ size_t SamplesInToFrames(size_t aSamples) const;
+ size_t FramesOutToBytes(size_t aFrames) const;
+
+ // Resampler context.
+ SpeexResamplerState* mResampler;
+ size_t ResampleAudio(void* aOut, const void* aIn, size_t aFrames);
+ size_t ResampleRecipientFrames(size_t aFrames) const;
+ void RecreateResampler();
+ size_t DrainResampler(void* aOut);
+};
+
+} // namespace mozilla
+
+#endif /* AudioConverter_h */
diff --git a/dom/media/AudioDeviceInfo.cpp b/dom/media/AudioDeviceInfo.cpp
new file mode 100644
index 0000000000..b37efafa32
--- /dev/null
+++ b/dom/media/AudioDeviceInfo.cpp
@@ -0,0 +1,165 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioDeviceInfo.h"
+
+NS_IMPL_ISUPPORTS(AudioDeviceInfo, nsIAudioDeviceInfo)
+
+using namespace mozilla;
+using namespace mozilla::CubebUtils;
+
+AudioDeviceInfo::AudioDeviceInfo(cubeb_device_info* aInfo)
+ : AudioDeviceInfo(aInfo->devid, NS_ConvertUTF8toUTF16(aInfo->friendly_name),
+ NS_ConvertUTF8toUTF16(aInfo->group_id),
+ NS_ConvertUTF8toUTF16(aInfo->vendor_name), aInfo->type,
+ aInfo->state, aInfo->preferred, aInfo->format,
+ aInfo->default_format, aInfo->max_channels,
+ aInfo->default_rate, aInfo->max_rate, aInfo->min_rate,
+ aInfo->latency_lo, aInfo->latency_hi) {}
+
+AudioDeviceInfo::AudioDeviceInfo(
+ AudioDeviceID aID, const nsAString& aName, const nsAString& aGroupId,
+ const nsAString& aVendor, uint16_t aType, uint16_t aState,
+ uint16_t aPreferred, uint16_t aSupportedFormat, uint16_t aDefaultFormat,
+ uint32_t aMaxChannels, uint32_t aDefaultRate, uint32_t aMaxRate,
+ uint32_t aMinRate, uint32_t aMaxLatency, uint32_t aMinLatency)
+ : mDeviceId(aID),
+ mName(aName),
+ mGroupId(aGroupId),
+ mVendor(aVendor),
+ mType(aType),
+ mState(aState),
+ mPreferred(aPreferred),
+ mSupportedFormat(aSupportedFormat),
+ mDefaultFormat(aDefaultFormat),
+ mMaxChannels(aMaxChannels),
+ mDefaultRate(aDefaultRate),
+ mMaxRate(aMaxRate),
+ mMinRate(aMinRate),
+ mMaxLatency(aMaxLatency),
+ mMinLatency(aMinLatency) {
+ MOZ_ASSERT(
+ mType == TYPE_UNKNOWN || mType == TYPE_INPUT || mType == TYPE_OUTPUT,
+ "Wrong type");
+ MOZ_ASSERT(mState == STATE_DISABLED || mState == STATE_UNPLUGGED ||
+ mState == STATE_ENABLED,
+ "Wrong state");
+ MOZ_ASSERT(
+ mPreferred == PREF_NONE || mPreferred == PREF_ALL ||
+ mPreferred & (PREF_MULTIMEDIA | PREF_VOICE | PREF_NOTIFICATION),
+ "Wrong preferred value");
+ MOZ_ASSERT(mSupportedFormat & (FMT_S16LE | FMT_S16BE | FMT_F32LE | FMT_F32BE),
+ "Wrong supported format");
+ MOZ_ASSERT(mDefaultFormat == FMT_S16LE || mDefaultFormat == FMT_S16BE ||
+ mDefaultFormat == FMT_F32LE || mDefaultFormat == FMT_F32BE,
+ "Wrong default format");
+}
+
+AudioDeviceID AudioDeviceInfo::DeviceID() const { return mDeviceId; }
+const nsString& AudioDeviceInfo::Name() const { return mName; }
+uint32_t AudioDeviceInfo::MaxChannels() const { return mMaxChannels; }
+uint32_t AudioDeviceInfo::Type() const { return mType; }
+uint32_t AudioDeviceInfo::State() const { return mState; }
+const nsString& AudioDeviceInfo::GroupID() const { return mGroupId; }
+
+bool AudioDeviceInfo::Preferred() const { return mPreferred; }
+
+/* readonly attribute DOMString name; */
+NS_IMETHODIMP
+AudioDeviceInfo::GetName(nsAString& aName) {
+ aName = mName;
+ return NS_OK;
+}
+
+/* readonly attribute DOMString groupId; */
+NS_IMETHODIMP
+AudioDeviceInfo::GetGroupId(nsAString& aGroupId) {
+ aGroupId = mGroupId;
+ return NS_OK;
+}
+
+/* readonly attribute DOMString vendor; */
+NS_IMETHODIMP
+AudioDeviceInfo::GetVendor(nsAString& aVendor) {
+ aVendor = mVendor;
+ return NS_OK;
+}
+
+/* readonly attribute unsigned short type; */
+NS_IMETHODIMP
+AudioDeviceInfo::GetType(uint16_t* aType) {
+ *aType = mType;
+ return NS_OK;
+}
+
+/* readonly attribute unsigned short state; */
+NS_IMETHODIMP
+AudioDeviceInfo::GetState(uint16_t* aState) {
+ *aState = mState;
+ return NS_OK;
+}
+
+/* readonly attribute unsigned short preferred; */
+NS_IMETHODIMP
+AudioDeviceInfo::GetPreferred(uint16_t* aPreferred) {
+ *aPreferred = mPreferred;
+ return NS_OK;
+}
+
+/* readonly attribute unsigned short supportedFormat; */
+NS_IMETHODIMP
+AudioDeviceInfo::GetSupportedFormat(uint16_t* aSupportedFormat) {
+ *aSupportedFormat = mSupportedFormat;
+ return NS_OK;
+}
+
+/* readonly attribute unsigned short defaultFormat; */
+NS_IMETHODIMP
+AudioDeviceInfo::GetDefaultFormat(uint16_t* aDefaultFormat) {
+ *aDefaultFormat = mDefaultFormat;
+ return NS_OK;
+}
+
+/* readonly attribute unsigned long maxChannels; */
+NS_IMETHODIMP
+AudioDeviceInfo::GetMaxChannels(uint32_t* aMaxChannels) {
+ *aMaxChannels = mMaxChannels;
+ return NS_OK;
+}
+
+/* readonly attribute unsigned long defaultRate; */
+NS_IMETHODIMP
+AudioDeviceInfo::GetDefaultRate(uint32_t* aDefaultRate) {
+ *aDefaultRate = mDefaultRate;
+ return NS_OK;
+}
+
+/* readonly attribute unsigned long maxRate; */
+NS_IMETHODIMP
+AudioDeviceInfo::GetMaxRate(uint32_t* aMaxRate) {
+ *aMaxRate = mMaxRate;
+ return NS_OK;
+}
+
+/* readonly attribute unsigned long minRate; */
+NS_IMETHODIMP
+AudioDeviceInfo::GetMinRate(uint32_t* aMinRate) {
+ *aMinRate = mMinRate;
+ return NS_OK;
+}
+
+/* readonly attribute unsigned long maxLatency; */
+NS_IMETHODIMP
+AudioDeviceInfo::GetMaxLatency(uint32_t* aMaxLatency) {
+ *aMaxLatency = mMaxLatency;
+ return NS_OK;
+}
+
+/* readonly attribute unsigned long minLatency; */
+NS_IMETHODIMP
+AudioDeviceInfo::GetMinLatency(uint32_t* aMinLatency) {
+ *aMinLatency = mMinLatency;
+ return NS_OK;
+}
diff --git a/dom/media/AudioDeviceInfo.h b/dom/media/AudioDeviceInfo.h
new file mode 100644
index 0000000000..e6f7ba1f27
--- /dev/null
+++ b/dom/media/AudioDeviceInfo.h
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_AudioDeviceInfo_H
+#define MOZILLA_AudioDeviceInfo_H
+
+#include "nsIAudioDeviceInfo.h"
+#include "CubebUtils.h"
+#include "mozilla/Maybe.h"
+
+// This is mapped to the cubeb_device_info.
+class AudioDeviceInfo final : public nsIAudioDeviceInfo {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIAUDIODEVICEINFO
+
+ using AudioDeviceID = mozilla::CubebUtils::AudioDeviceID;
+
+ AudioDeviceInfo(const AudioDeviceID aID, const nsAString& aName,
+ const nsAString& aGroupId, const nsAString& aVendor,
+ uint16_t aType, uint16_t aState, uint16_t aPreferred,
+ uint16_t aSupportedFormat, uint16_t aDefaultFormat,
+ uint32_t aMaxChannels, uint32_t aDefaultRate,
+ uint32_t aMaxRate, uint32_t aMinRate, uint32_t aMaxLatency,
+ uint32_t aMinLatency);
+ explicit AudioDeviceInfo(cubeb_device_info* aInfo);
+
+ AudioDeviceID DeviceID() const;
+ const nsString& Name() const;
+ uint32_t MaxChannels() const;
+ uint32_t Type() const;
+ uint32_t State() const;
+ const nsString& GroupID() const;
+ bool Preferred() const;
+
+ private:
+ virtual ~AudioDeviceInfo() = default;
+
+ const AudioDeviceID mDeviceId;
+ const nsString mName;
+ const nsString mGroupId;
+ const nsString mVendor;
+ const uint16_t mType;
+ const uint16_t mState;
+ const uint16_t mPreferred;
+ const uint16_t mSupportedFormat;
+ const uint16_t mDefaultFormat;
+ const uint32_t mMaxChannels;
+ const uint32_t mDefaultRate;
+ const uint32_t mMaxRate;
+ const uint32_t mMinRate;
+ const uint32_t mMaxLatency;
+ const uint32_t mMinLatency;
+};
+
+#endif // MOZILLA_AudioDeviceInfo_H
diff --git a/dom/media/AudioDriftCorrection.h b/dom/media/AudioDriftCorrection.h
new file mode 100644
index 0000000000..d94025adec
--- /dev/null
+++ b/dom/media/AudioDriftCorrection.h
@@ -0,0 +1,209 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_AUDIO_DRIFT_CORRECTION_H_
+#define MOZILLA_AUDIO_DRIFT_CORRECTION_H_
+
+#include "DynamicResampler.h"
+
+namespace mozilla {
+
+extern LazyLogModule gMediaTrackGraphLog;
+
+/**
+ * ClockDrift calculates the diverge of the source clock from the nominal
+ * (provided) rate compared to the target clock, which is considered the master
+ * clock. In the case of different sampling rates, it is assumed that resampling
+ * will take place so the returned correction is estimated after the resampling.
+ * That means that resampling is taken into account in the calculations but it
+ * does appear in the correction. The correction must be applied to the top of
+ * the resampling.
+ *
+ * It works by measuring the incoming, the outgoing frames, and the amount of
+ * buffered data and estimates the correction needed. The correction logic has
+ * been created with two things in mind. First, not to run out of frames because
+ * that means the audio will glitch. Second, not to change the correction very
+ * often because this will result in a change in the resampling ratio. The
+ * resampler recreates its internal memory when the ratio changes which has a
+ * performance impact.
+ *
+ * The pref `media.clock drift.buffering` can be used to configure the desired
+ * internal buffering. Right now it is at 50ms. But it can be increased if there
+ * are audio quality problems.
+ */
+class ClockDrift final {
+ public:
+ /**
+ * Provide the nominal source and the target sample rate.
+ */
+ ClockDrift(uint32_t aSourceRate, uint32_t aTargetRate,
+ uint32_t aDesiredBuffering)
+ : mSourceRate(aSourceRate),
+ mTargetRate(aTargetRate),
+ mDesiredBuffering(aDesiredBuffering) {}
+
+ /**
+ * The correction in the form of a ratio. A correction of 0.98 means that the
+ * target is 2% slower compared to the source or 1.03 which means that the
+ * target is 3% faster than the source.
+ */
+ float GetCorrection() { return mCorrection; }
+
+ /**
+ * Update the available source frames, target frames, and the current
+ * buffer, in every iteration. If the conditions are met a new correction is
+ * calculated. A new correction is calculated in the following cases:
+ * 1. Every mAdjustmentIntervalMs milliseconds (1000ms).
+ * 2. Every time we run low on buffered frames (less than 20ms).
+ * In addition to that, the correction is clamped to 10% to avoid sound
+ * distortion so the result will be in [0.9, 1.1].
+ */
+ void UpdateClock(uint32_t aSourceFrames, uint32_t aTargetFrames,
+ uint32_t aBufferedFrames, uint32_t aRemainingFrames) {
+ if (mSourceClock >= mSourceRate / 10 || mTargetClock >= mTargetRate / 10) {
+ // Only update the correction if 100ms has passed since last update.
+ if (aBufferedFrames < mDesiredBuffering * 4 / 10 /*40%*/ ||
+ aRemainingFrames < mDesiredBuffering * 4 / 10 /*40%*/) {
+ // We are getting close to the lower or upper bound of the internal
+ // buffer. Steer clear.
+ CalculateCorrection(0.9, aBufferedFrames, aRemainingFrames);
+ } else if ((mTargetClock * 1000 / mTargetRate) >= mAdjustmentIntervalMs ||
+ (mSourceClock * 1000 / mSourceRate) >= mAdjustmentIntervalMs) {
+ // The adjustment interval has passed on one side. Recalculate.
+ CalculateCorrection(0.6, aBufferedFrames, aRemainingFrames);
+ }
+ }
+ mTargetClock += aTargetFrames;
+ mSourceClock += aSourceFrames;
+ }
+
+ private:
+ /**
+ * aCalculationWeight is a percentage [0, 1] with which the calculated
+ * correction will be weighted. The existing correction will be weighted with
+ * 1 - aCalculationWeight. This gives some inertia to the speed at which the
+ * correction changes, for smoother changes.
+ */
+ void CalculateCorrection(float aCalculationWeight, uint32_t aBufferedFrames,
+ uint32_t aRemainingFrames) {
+ // We want to maintain the desired buffer
+ uint32_t bufferedFramesDiff = aBufferedFrames - mDesiredBuffering;
+ uint32_t resampledSourceClock =
+ std::max(1u, mSourceClock + bufferedFramesDiff);
+ if (mTargetRate != mSourceRate) {
+ resampledSourceClock *= static_cast<float>(mTargetRate) / mSourceRate;
+ }
+
+ MOZ_LOG(gMediaTrackGraphLog, LogLevel::Verbose,
+ ("ClockDrift %p Calculated correction %.3f (with weight: %.1f -> "
+ "%.3f) (buffer: %u, desired: %u, remaining: %u)",
+ this, static_cast<float>(mTargetClock) / resampledSourceClock,
+ aCalculationWeight,
+ (1 - aCalculationWeight) * mCorrection +
+ aCalculationWeight * mTargetClock / resampledSourceClock,
+ aBufferedFrames, mDesiredBuffering, aRemainingFrames));
+
+ mCorrection = (1 - aCalculationWeight) * mCorrection +
+ aCalculationWeight * mTargetClock / resampledSourceClock;
+
+ // Clamp to range [0.9, 1.1] to avoid distortion
+ mCorrection = std::min(std::max(mCorrection, 0.9f), 1.1f);
+
+ // Reset the counters to prepare for the next period.
+ mTargetClock = 0;
+ mSourceClock = 0;
+ }
+
+ public:
+ const uint32_t mSourceRate;
+ const uint32_t mTargetRate;
+ const uint32_t mAdjustmentIntervalMs = 1000;
+ const uint32_t mDesiredBuffering;
+
+ private:
+ float mCorrection = 1.0;
+
+ uint32_t mSourceClock = 0;
+ uint32_t mTargetClock = 0;
+};
+
+/**
+ * Correct the drift between two independent clocks, the source, and the target
+ * clock. The target clock is the master clock so the correction syncs the drift
+ * of the source clock to the target. The nominal sampling rates of source and
+ * target must be provided. If the source and the target operate in different
+ * sample rate the drift correction will be performed on the top of resampling
+ * from the source rate to the target rate.
+ *
+ * It works with AudioSegment in order to be able to be used from the
+ * MediaTrackGraph/MediaTrack. The audio buffers are pre-allocated so there is
+ * no new allocation takes place during operation. The preallocation capacity is
+ * 100ms for input and 100ms for output. The class consists of ClockDrift and
+ * AudioResampler check there for more details.
+ *
+ * The class is not thread-safe. The construction can happen in any thread but
+ * the member method must be used in a single thread that can be different than
+ * the construction thread. Appropriate for being used in the high priority
+ * audio thread.
+ */
+class AudioDriftCorrection final {
+ const uint32_t kMinBufferMs = 5;
+
+ public:
+ AudioDriftCorrection(uint32_t aSourceRate, uint32_t aTargetRate,
+ uint32_t aBufferMs,
+ const PrincipalHandle& aPrincipalHandle)
+ : mDesiredBuffering(std::max(kMinBufferMs, aBufferMs) * aSourceRate /
+ 1000),
+ mTargetRate(aTargetRate),
+ mClockDrift(aSourceRate, aTargetRate, mDesiredBuffering),
+ mResampler(aSourceRate, aTargetRate, mDesiredBuffering,
+ aPrincipalHandle) {}
+
+ /**
+ * The source audio frames and request the number of target audio frames must
+ * be provided. The duration of the source and the output is considered as the
+ * source clock and the target clock. The input is buffered internally so some
+ * latency exists. The returned AudioSegment must be cleaned up because the
+ * internal buffer will be reused after 100ms. If the drift correction (and
+ * possible resampling) is not possible due to lack of input data an empty
+ * AudioSegment will be returned. Not thread-safe.
+ */
+ AudioSegment RequestFrames(const AudioSegment& aInput,
+ uint32_t aOutputFrames) {
+ // Very important to go first since the Dynamic will get the sample format
+ // from the chunk.
+ if (aInput.GetDuration()) {
+ // Always go through the resampler because the clock might shift later.
+ mResampler.AppendInput(aInput);
+ }
+ mClockDrift.UpdateClock(aInput.GetDuration(), aOutputFrames,
+ mResampler.InputReadableFrames(),
+ mResampler.InputWritableFrames());
+ TrackRate receivingRate = mTargetRate * mClockDrift.GetCorrection();
+ // Update resampler's rate if there is a new correction.
+ mResampler.UpdateOutRate(receivingRate);
+ // If it does not have enough frames the result will be an empty segment.
+ AudioSegment output = mResampler.Resample(aOutputFrames);
+ if (output.IsEmpty()) {
+ NS_WARNING("Got nothing from the resampler");
+ output.AppendNullData(aOutputFrames);
+ }
+ return output;
+ }
+
+ // Only accessible from the same thread that is driving RequestFrames().
+ uint32_t CurrentBuffering() const { return mResampler.InputReadableFrames(); }
+
+ const uint32_t mDesiredBuffering;
+ const uint32_t mTargetRate;
+
+ private:
+ ClockDrift mClockDrift;
+ AudioResampler mResampler;
+};
+
+}; // namespace mozilla
+#endif /* MOZILLA_AUDIO_DRIFT_CORRECTION_H_ */
diff --git a/dom/media/AudioInputSource.cpp b/dom/media/AudioInputSource.cpp
new file mode 100644
index 0000000000..15d35bb373
--- /dev/null
+++ b/dom/media/AudioInputSource.cpp
@@ -0,0 +1,222 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#include "AudioInputSource.h"
+
+#include "CallbackThreadRegistry.h"
+#include "GraphDriver.h"
+
+namespace mozilla {
+
+extern mozilla::LazyLogModule gMediaTrackGraphLog;
+
+#ifdef LOG_INTERNAL
+# undef LOG_INTERNAL
+#endif // LOG_INTERNAL
+#define LOG_INTERNAL(level, msg, ...) \
+ MOZ_LOG(gMediaTrackGraphLog, LogLevel::level, (msg, ##__VA_ARGS__))
+
+#ifdef LOG
+# undef LOG
+#endif // LOG
+#define LOG(msg, ...) LOG_INTERNAL(Debug, msg, ##__VA_ARGS__)
+
+#ifdef LOGW
+# undef LOGW
+#endif // LOGW
+#define LOGW(msg, ...) LOG_INTERNAL(Warning, msg, ##__VA_ARGS__)
+
+#ifdef LOGE
+# undef LOGE
+#endif // LOGE
+#define LOGE(msg, ...) LOG_INTERNAL(Error, msg, ##__VA_ARGS__)
+
+#ifdef LOGV
+# undef LOGV
+#endif // LOGV
+#define LOGV(msg, ...) LOG_INTERNAL(Verbose, msg, ##__VA_ARGS__)
+
+AudioInputSource::AudioInputSource(RefPtr<EventListener>&& aListener,
+ Id aSourceId,
+ CubebUtils::AudioDeviceID aDeviceId,
+ uint32_t aChannelCount, bool aIsVoice,
+ const PrincipalHandle& aPrincipalHandle,
+ TrackRate aSourceRate, TrackRate aTargetRate,
+ uint32_t aBufferMs)
+ : mId(aSourceId),
+ mDeviceId(aDeviceId),
+ mChannelCount(aChannelCount),
+ mRate(aSourceRate),
+ mIsVoice(aIsVoice),
+ mPrincipalHandle(aPrincipalHandle),
+ mSandboxed(CubebUtils::SandboxEnabled()),
+ mAudioThreadId(ProfilerThreadId{}),
+ mEventListener(std::move(aListener)),
+ mTaskThread(CUBEB_TASK_THREAD),
+ mDriftCorrector(static_cast<uint32_t>(aSourceRate),
+ static_cast<uint32_t>(aTargetRate), aBufferMs,
+ aPrincipalHandle) {
+ MOZ_ASSERT(mChannelCount > 0);
+ MOZ_ASSERT(mEventListener);
+}
+
+void AudioInputSource::Start() {
+ // This is called on MediaTrackGraph's graph thread, which can be the cubeb
+ // stream's callback thread. Running cubeb operations within cubeb stream
+ // callback thread can cause the deadlock on Linux, so we dispatch those
+ // operations to the task thread.
+ MOZ_ASSERT(mTaskThread);
+
+ // mSPSCQueue will have a new consumer.
+ mSPSCQueue.ResetConsumerThreadId();
+
+ LOG("AudioInputSource %p, start", this);
+ MOZ_ALWAYS_SUCCEEDS(mTaskThread->Dispatch(
+ NS_NewRunnableFunction(__func__, [self = RefPtr(this)]() mutable {
+ self->mStream = CubebInputStream::Create(
+ self->mDeviceId, self->mChannelCount,
+ static_cast<uint32_t>(self->mRate), self->mIsVoice, self.get());
+ if (!self->mStream) {
+ LOGE("AudioInputSource %p, cannot create an audio input stream!",
+ self.get());
+ return;
+ }
+ if (int r = self->mStream->Start(); r != CUBEB_OK) {
+ LOGE(
+ "AudioInputSource %p, cannot start its audio input stream! The "
+ "stream is destroyed directly!",
+ self.get());
+ self->mStream = nullptr;
+ }
+ })));
+}
+
+void AudioInputSource::Stop() {
+ // This is called on MediaTrackGraph's graph thread, which can be the cubeb
+ // stream's callback thread. Running cubeb operations within cubeb stream
+ // callback thread can cause the deadlock on Linux, so we dispatch those
+ // operations to the task thread.
+ MOZ_ASSERT(mTaskThread);
+
+ LOG("AudioInputSource %p, stop", this);
+ MOZ_ALWAYS_SUCCEEDS(mTaskThread->Dispatch(
+ NS_NewRunnableFunction(__func__, [self = RefPtr(this)]() mutable {
+ if (!self->mStream) {
+ LOGE("AudioInputSource %p, has no audio input stream to stop!",
+ self.get());
+ return;
+ }
+ if (int r = self->mStream->Stop(); r != CUBEB_OK) {
+ LOGE(
+ "AudioInputSource %p, cannot stop its audio input stream! The "
+ "stream is going to be destroyed forcefully",
+ self.get());
+ }
+ self->mStream = nullptr;
+ })));
+}
+
+AudioSegment AudioInputSource::GetAudioSegment(TrackTime aDuration,
+ Consumer aConsumer) {
+ if (aConsumer == Consumer::Changed) {
+ // Reset queue's consumer to avoid hitting the assertion for checking the
+ // consistency of mSPSCQueue's mConsumerId in Dequeue.
+ mSPSCQueue.ResetConsumerThreadId();
+ }
+
+ AudioSegment raw;
+ while (mSPSCQueue.AvailableRead()) {
+ AudioChunk chunk;
+ DebugOnly<int> reads = mSPSCQueue.Dequeue(&chunk, 1);
+ MOZ_ASSERT(reads);
+ raw.AppendAndConsumeChunk(std::move(chunk));
+ }
+
+ return mDriftCorrector.RequestFrames(raw, static_cast<uint32_t>(aDuration));
+}
+
+long AudioInputSource::DataCallback(const void* aBuffer, long aFrames) {
+ const AudioDataValue* source =
+ reinterpret_cast<const AudioDataValue*>(aBuffer);
+
+ AudioChunk c = AudioChunk::FromInterleavedBuffer(
+ source, static_cast<size_t>(aFrames), mChannelCount, mPrincipalHandle);
+
+ // Reset queue's producer to avoid hitting the assertion for checking the
+ // consistency of mSPSCQueue's mProducerId in Enqueue. This can happen when:
+ // 1) cubeb stream is reinitialized behind the scenes for the device changed
+ // events, e.g., users plug/unplug a TRRS mic into/from the built-in jack port
+ // of some old macbooks.
+ // 2) After Start() to Stop() cycle finishes, user call Start() again.
+ if (CheckThreadIdChanged()) {
+ mSPSCQueue.ResetProducerThreadId();
+ if (!mSandboxed) {
+ CallbackThreadRegistry::Get()->Register(mAudioThreadId,
+ "NativeAudioCallback");
+ }
+ }
+
+ int writes = mSPSCQueue.Enqueue(c);
+ if (writes == 0) {
+ LOGW("AudioInputSource %p, buffer is full. Dropping %ld frames", this,
+ aFrames);
+ } else {
+ LOGV("AudioInputSource %p, enqueue %ld frames (%d AudioChunks)", this,
+ aFrames, writes);
+ }
+ return aFrames;
+}
+
+void AudioInputSource::StateCallback(cubeb_state aState) {
+ EventListener::State state;
+ if (aState == CUBEB_STATE_STARTED) {
+ LOG("AudioInputSource %p, stream started", this);
+ state = EventListener::State::Started;
+ } else if (aState == CUBEB_STATE_STOPPED) {
+ LOG("AudioInputSource %p, stream stopped", this);
+ state = EventListener::State::Stopped;
+ } else if (aState == CUBEB_STATE_DRAINED) {
+ LOG("AudioInputSource %p, stream is drained", this);
+ state = EventListener::State::Drained;
+ } else {
+ MOZ_ASSERT(aState == CUBEB_STATE_ERROR);
+ LOG("AudioInputSource %p, error happend", this);
+ state = EventListener::State::Error;
+ }
+ // This can be called on any thread, so we forward the event to main thread
+ // first.
+ NS_DispatchToMainThread(
+ NS_NewRunnableFunction(__func__, [self = RefPtr(this), s = state] {
+ self->mEventListener->AudioStateCallback(self->mId, s);
+ }));
+}
+
+void AudioInputSource::DeviceChangedCallback() {
+ LOG("AudioInputSource %p, device changed", this);
+ // This can be called on any thread, so we forward the event to main thread
+ // first.
+ NS_DispatchToMainThread(
+ NS_NewRunnableFunction(__func__, [self = RefPtr(this)] {
+ self->mEventListener->AudioDeviceChanged(self->mId);
+ }));
+}
+
+bool AudioInputSource::CheckThreadIdChanged() {
+ ProfilerThreadId id = profiler_current_thread_id();
+ if (id != mAudioThreadId) {
+ mAudioThreadId = id;
+ return true;
+ }
+ return false;
+}
+
+#undef LOG_INTERNAL
+#undef LOG
+#undef LOGW
+#undef LOGE
+#undef LOGV
+
+} // namespace mozilla
diff --git a/dom/media/AudioInputSource.h b/dom/media/AudioInputSource.h
new file mode 100644
index 0000000000..df5bd3fafb
--- /dev/null
+++ b/dom/media/AudioInputSource.h
@@ -0,0 +1,127 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_AudioInputSource_H_
+#define DOM_MEDIA_AudioInputSource_H_
+
+#include "AudioDriftCorrection.h"
+#include "AudioSegment.h"
+#include "CubebInputStream.h"
+#include "CubebUtils.h"
+#include "mozilla/ProfilerUtils.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/SPSCQueue.h"
+#include "mozilla/SharedThreadPool.h"
+
+namespace mozilla {
+
+// This is an interface to operate an input-only audio stream within a
+// cubeb-task thread on a specific thread. Once the class instance is created,
+// all its operations must be called on the same thread.
+//
+// The audio data is periodically produced by the underlying audio stream on the
+// stream's callback thread, and can be safely read by GetAudioSegment() on a
+// specific thread.
+class AudioInputSource : public CubebInputStream::Listener {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AudioInputSource, override);
+
+ using Id = uint32_t;
+
+ class EventListener {
+ public:
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING;
+
+ // This two events will be fired on main thread.
+ virtual void AudioDeviceChanged(Id aId) = 0;
+ enum class State { Started, Stopped, Drained, Error };
+ virtual void AudioStateCallback(Id aId, State aState) = 0;
+
+ protected:
+ EventListener() = default;
+ virtual ~EventListener() = default;
+ };
+
+ AudioInputSource(RefPtr<EventListener>&& aListener, Id aSourceId,
+ CubebUtils::AudioDeviceID aDeviceId, uint32_t aChannelCount,
+ bool aIsVoice, const PrincipalHandle& aPrincipalHandle,
+ TrackRate aSourceRate, TrackRate aTargetRate,
+ uint32_t aBufferMs);
+
+ // The following functions should always be called in the same thread: They
+ // are always run on MediaTrackGraph's graph thread.
+ // Starts producing audio data.
+ void Start();
+ // Stops producing audio data.
+ void Stop();
+ // Returns the AudioSegment with aDuration of data inside.
+ // The graph thread can change behind the scene, e.g., cubeb stream reinit due
+ // to default output device changed). When this happens, we need to notify
+ // mSPSCQueue to change its data consumer.
+ enum class Consumer { Same, Changed };
+ AudioSegment GetAudioSegment(TrackTime aDuration, Consumer aConsumer);
+
+ // CubebInputStream::Listener interface: These are used only for the
+ // underlying audio stream. No user should call these APIs.
+ // This will be fired on audio callback thread.
+ long DataCallback(const void* aBuffer, long aFrames) override;
+ // This can be fired on any thread.
+ void StateCallback(cubeb_state aState) override;
+ // This can be fired on any thread.
+ void DeviceChangedCallback() override;
+
+ // Any threads:
+ // The unique id of this source.
+ const Id mId;
+ // The id of this audio device producing the data.
+ const CubebUtils::AudioDeviceID mDeviceId;
+ // The channel count of audio data produced.
+ const uint32_t mChannelCount;
+ // The sample rate of the audio data produced.
+ const TrackRate mRate;
+ // Indicate whether the audio stream is for voice or not.
+ const bool mIsVoice;
+ // The principal of the audio data produced.
+ const PrincipalHandle mPrincipalHandle;
+
+ protected:
+ ~AudioInputSource() = default;
+
+ private:
+ // Underlying audio thread only.
+ bool CheckThreadIdChanged();
+
+ // Any thread.
+ const bool mSandboxed;
+
+ // Thread id of the underlying audio thread. Underlying audio thread only.
+ std::atomic<ProfilerThreadId> mAudioThreadId;
+
+ // Forward the underlying event from main thread.
+ const RefPtr<EventListener> mEventListener;
+
+ // Shared thread pool containing only one thread for cubeb operations.
+ // The cubeb operations: Start() and Stop() will be called on
+ // MediaTrackGraph's graph thread, which can be the cubeb stream's callback
+ // thread. Running cubeb operations within cubeb stream callback thread can
+ // cause the deadlock on Linux, so we dispatch those operations to the task
+ // thread.
+ const RefPtr<SharedThreadPool> mTaskThread;
+
+ // Correct the drift between the underlying audio stream and its reader.
+ AudioDriftCorrection mDriftCorrector;
+
+ // An input-only cubeb stream operated within mTaskThread.
+ UniquePtr<CubebInputStream> mStream;
+
+ // A single-producer-single-consumer lock-free queue whose data is produced by
+ // the audio callback thread and consumed by AudioInputSource's data reader.
+ SPSCQueue<AudioChunk> mSPSCQueue{30};
+};
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_AudioInputSource_H_
diff --git a/dom/media/AudioMixer.h b/dom/media/AudioMixer.h
new file mode 100644
index 0000000000..3db156a0ec
--- /dev/null
+++ b/dom/media/AudioMixer.h
@@ -0,0 +1,145 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_AUDIOMIXER_H_
+#define MOZILLA_AUDIOMIXER_H_
+
+#include "AudioSampleFormat.h"
+#include "AudioStream.h"
+#include "nsTArray.h"
+#include "mozilla/LinkedList.h"
+#include "mozilla/NotNull.h"
+#include "mozilla/PodOperations.h"
+
+namespace mozilla {
+
+struct MixerCallbackReceiver {
+ virtual void MixerCallback(AudioDataValue* aMixedBuffer,
+ AudioSampleFormat aFormat, uint32_t aChannels,
+ uint32_t aFrames, uint32_t aSampleRate) = 0;
+};
+/**
+ * This class mixes multiple streams of audio together to output a single audio
+ * stream.
+ *
+ * AudioMixer::Mix is to be called repeatedly with buffers that have the same
+ * length, sample rate, sample format and channel count. This class works with
+ * interleaved and plannar buffers, but the buffer mixed must be of the same
+ * type during a mixing cycle.
+ *
+ * When all the tracks have been mixed, calling FinishMixing will call back with
+ * a buffer containing the mixed audio data.
+ *
+ * This class is not thread safe.
+ */
+class AudioMixer {
+ public:
+ AudioMixer() : mFrames(0), mChannels(0), mSampleRate(0) {}
+
+ ~AudioMixer() {
+ MixerCallback* cb;
+ while ((cb = mCallbacks.popFirst())) {
+ delete cb;
+ }
+ }
+
+ void StartMixing() { mSampleRate = mChannels = mFrames = 0; }
+
+ /* Get the data from the mixer. This is supposed to be called when all the
+ * tracks have been mixed in. The caller should not hold onto the data. */
+ void FinishMixing() {
+ MOZ_ASSERT(mChannels && mSampleRate, "Mix not called for this cycle?");
+ for (MixerCallback* cb = mCallbacks.getFirst(); cb != nullptr;
+ cb = cb->getNext()) {
+ MixerCallbackReceiver* receiver = cb->mReceiver;
+ MOZ_ASSERT(receiver);
+ receiver->MixerCallback(mMixedAudio.Elements(),
+ AudioSampleTypeToFormat<AudioDataValue>::Format,
+ mChannels, mFrames, mSampleRate);
+ }
+ PodZero(mMixedAudio.Elements(), mMixedAudio.Length());
+ mSampleRate = mChannels = mFrames = 0;
+ }
+
+ /* Add a buffer to the mix. The buffer can be null if there's nothing to mix
+ * but the callback is still needed. */
+ void Mix(AudioDataValue* aSamples, uint32_t aChannels, uint32_t aFrames,
+ uint32_t aSampleRate) {
+ if (!mFrames && !mChannels) {
+ mFrames = aFrames;
+ mChannels = aChannels;
+ mSampleRate = aSampleRate;
+ EnsureCapacityAndSilence();
+ }
+
+ MOZ_ASSERT(aFrames == mFrames);
+ MOZ_ASSERT(aChannels == mChannels);
+ MOZ_ASSERT(aSampleRate == mSampleRate);
+
+ if (!aSamples) {
+ return;
+ }
+
+ for (uint32_t i = 0; i < aFrames * aChannels; i++) {
+ mMixedAudio[i] += aSamples[i];
+ }
+ }
+
+ void AddCallback(NotNull<MixerCallbackReceiver*> aReceiver) {
+ mCallbacks.insertBack(new MixerCallback(aReceiver));
+ }
+
+ bool FindCallback(MixerCallbackReceiver* aReceiver) {
+ for (MixerCallback* cb = mCallbacks.getFirst(); cb != nullptr;
+ cb = cb->getNext()) {
+ if (cb->mReceiver == aReceiver) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ bool RemoveCallback(MixerCallbackReceiver* aReceiver) {
+ for (MixerCallback* cb = mCallbacks.getFirst(); cb != nullptr;
+ cb = cb->getNext()) {
+ if (cb->mReceiver == aReceiver) {
+ cb->remove();
+ delete cb;
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private:
+ void EnsureCapacityAndSilence() {
+ if (mFrames * mChannels > mMixedAudio.Length()) {
+ mMixedAudio.SetLength(mFrames * mChannels);
+ }
+ PodZero(mMixedAudio.Elements(), mMixedAudio.Length());
+ }
+
+ class MixerCallback : public LinkedListElement<MixerCallback> {
+ public:
+ explicit MixerCallback(NotNull<MixerCallbackReceiver*> aReceiver)
+ : mReceiver(aReceiver) {}
+ NotNull<MixerCallbackReceiver*> mReceiver;
+ };
+
+ /* Function that is called when the mixing is done. */
+ LinkedList<MixerCallback> mCallbacks;
+ /* Number of frames for this mixing block. */
+ uint32_t mFrames;
+ /* Number of channels for this mixing block. */
+ uint32_t mChannels;
+ /* Sample rate the of the mixed data. */
+ uint32_t mSampleRate;
+ /* Buffer containing the mixed audio data. */
+ nsTArray<AudioDataValue> mMixedAudio;
+};
+
+} // namespace mozilla
+
+#endif // MOZILLA_AUDIOMIXER_H_
diff --git a/dom/media/AudioPacketizer.h b/dom/media/AudioPacketizer.h
new file mode 100644
index 0000000000..8df04c0c5c
--- /dev/null
+++ b/dom/media/AudioPacketizer.h
@@ -0,0 +1,174 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef AudioPacketizer_h_
+#define AudioPacketizer_h_
+
+#include <mozilla/PodOperations.h>
+#include <mozilla/Assertions.h>
+#include <mozilla/UniquePtr.h>
+#include <AudioSampleFormat.h>
+
+// Enable this to warn when `Output` has been called but not enough data was
+// buffered.
+// #define LOG_PACKETIZER_UNDERRUN
+
+namespace mozilla {
+/**
+ * This class takes arbitrary input data, and returns packets of a specific
+ * size. In the process, it can convert audio samples from 16bit integers to
+ * float (or vice-versa).
+ *
+ * Input and output, as well as length units in the public interface are
+ * interleaved frames.
+ *
+ * Allocations of output buffer can be performed by this class. Buffers can
+ * simply be delete-d. This is because packets are intended to be sent off to
+ * non-gecko code using normal pointers/length pairs
+ *
+ * Alternatively, consumers can pass in a buffer in which the output is copied.
+ * The buffer needs to be large enough to store a packet worth of audio.
+ *
+ * The implementation uses a circular buffer using absolute virtual indices.
+ */
+template <typename InputType, typename OutputType>
+class AudioPacketizer {
+ public:
+ AudioPacketizer(uint32_t aPacketSize, uint32_t aChannels)
+ : mPacketSize(aPacketSize),
+ mChannels(aChannels),
+ mReadIndex(0),
+ mWriteIndex(0),
+ // Start off with a single packet
+ mStorage(new InputType[aPacketSize * aChannels]),
+ mLength(aPacketSize * aChannels) {
+ MOZ_ASSERT(aPacketSize > 0 && aChannels > 0,
+ "The packet size and the number of channel should be strictly "
+ "positive");
+ }
+
+ void Input(const InputType* aFrames, uint32_t aFrameCount) {
+ uint32_t inputSamples = aFrameCount * mChannels;
+ // Need to grow the storage. This should rarely happen, if at all, once the
+ // array has the right size.
+ if (inputSamples > EmptySlots()) {
+ // Calls to Input and Output are roughtly interleaved
+ // (Input,Output,Input,Output, etc.), or balanced
+ // (Input,Input,Input,Output,Output,Output), so we update the buffer to
+ // the exact right size in order to not waste space.
+ uint32_t newLength = AvailableSamples() + inputSamples;
+ uint32_t toCopy = AvailableSamples();
+ UniquePtr<InputType[]> oldStorage = std::move(mStorage);
+ mStorage = mozilla::MakeUnique<InputType[]>(newLength);
+ // Copy the old data at the beginning of the new storage.
+ if (WriteIndex() >= ReadIndex()) {
+ PodCopy(mStorage.get(), oldStorage.get() + ReadIndex(),
+ AvailableSamples());
+ } else {
+ uint32_t firstPartLength = mLength - ReadIndex();
+ uint32_t secondPartLength = AvailableSamples() - firstPartLength;
+ PodCopy(mStorage.get(), oldStorage.get() + ReadIndex(),
+ firstPartLength);
+ PodCopy(mStorage.get() + firstPartLength, oldStorage.get(),
+ secondPartLength);
+ }
+ mWriteIndex = toCopy;
+ mReadIndex = 0;
+ mLength = newLength;
+ }
+
+ if (WriteIndex() + inputSamples <= mLength) {
+ PodCopy(mStorage.get() + WriteIndex(), aFrames, aFrameCount * mChannels);
+ } else {
+ uint32_t firstPartLength = mLength - WriteIndex();
+ uint32_t secondPartLength = inputSamples - firstPartLength;
+ PodCopy(mStorage.get() + WriteIndex(), aFrames, firstPartLength);
+ PodCopy(mStorage.get(), aFrames + firstPartLength, secondPartLength);
+ }
+
+ mWriteIndex += inputSamples;
+ }
+
+ OutputType* Output() {
+ uint32_t samplesNeeded = mPacketSize * mChannels;
+ OutputType* out = new OutputType[samplesNeeded];
+
+ Output(out);
+
+ return out;
+ }
+
+ void Output(OutputType* aOutputBuffer) {
+ uint32_t samplesNeeded = mPacketSize * mChannels;
+
+ // Under-run. Pad the end of the buffer with silence.
+ if (AvailableSamples() < samplesNeeded) {
+#ifdef LOG_PACKETIZER_UNDERRUN
+ char buf[256];
+ snprintf(buf, 256,
+ "AudioPacketizer %p underrun: available: %u, needed: %u\n", this,
+ AvailableSamples(), samplesNeeded);
+ NS_WARNING(buf);
+#endif
+ uint32_t zeros = samplesNeeded - AvailableSamples();
+ PodZero(aOutputBuffer + AvailableSamples(), zeros);
+ samplesNeeded -= zeros;
+ }
+ if (ReadIndex() + samplesNeeded <= mLength) {
+ ConvertAudioSamples<InputType, OutputType>(mStorage.get() + ReadIndex(),
+ aOutputBuffer, samplesNeeded);
+ } else {
+ uint32_t firstPartLength = mLength - ReadIndex();
+ uint32_t secondPartLength = samplesNeeded - firstPartLength;
+ ConvertAudioSamples<InputType, OutputType>(
+ mStorage.get() + ReadIndex(), aOutputBuffer, firstPartLength);
+ ConvertAudioSamples<InputType, OutputType>(
+ mStorage.get(), aOutputBuffer + firstPartLength, secondPartLength);
+ }
+ mReadIndex += samplesNeeded;
+ }
+
+ void Clear() {
+ mReadIndex = 0;
+ mWriteIndex = 0;
+ }
+
+ uint32_t PacketsAvailable() const {
+ return AvailableSamples() / mChannels / mPacketSize;
+ }
+
+ uint32_t FramesAvailable() const { return AvailableSamples() / mChannels; }
+
+ bool Empty() const { return mWriteIndex == mReadIndex; }
+
+ bool Full() const { return mWriteIndex - mReadIndex == mLength; }
+
+ // Size of one packet of audio, in frames
+ const uint32_t mPacketSize;
+ // Number of channels of the stream flowing through this packetizer
+ const uint32_t mChannels;
+
+ private:
+ uint32_t ReadIndex() const { return mReadIndex % mLength; }
+
+ uint32_t WriteIndex() const { return mWriteIndex % mLength; }
+
+ uint32_t AvailableSamples() const { return mWriteIndex - mReadIndex; }
+
+ uint32_t EmptySlots() const { return mLength - AvailableSamples(); }
+
+ // Two virtual index into the buffer: the read position and the write
+ // position.
+ uint64_t mReadIndex;
+ uint64_t mWriteIndex;
+ // Storage for the samples
+ mozilla::UniquePtr<InputType[]> mStorage;
+ // Length of the buffer, in samples
+ uint32_t mLength;
+};
+
+} // namespace mozilla
+
+#endif // AudioPacketizer_h_
diff --git a/dom/media/AudioRingBuffer.cpp b/dom/media/AudioRingBuffer.cpp
new file mode 100644
index 0000000000..917d4880a1
--- /dev/null
+++ b/dom/media/AudioRingBuffer.cpp
@@ -0,0 +1,471 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioRingBuffer.h"
+
+#include "MediaData.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/PodOperations.h"
+
+namespace mozilla {
+
+/**
+ * RingBuffer is used to preallocate a buffer of a specific size in bytes and
+ * then to use it for writing and reading values without any re-allocation or
+ * memory moving. Please note that the total byte size of the buffer modulo the
+ * size of the chosen type must be zero. The RingBuffer has been created with
+ * audio sample values types in mind which are integer or float. However, it
+ * can be used with any trivial type. It is _not_ thread-safe! The constructor
+ * can be called on any thread but the reads and write must happen on the same
+ * thread, which can be different than the construction thread.
+ */
+template <typename T>
+class RingBuffer final {
+ public:
+ explicit RingBuffer(AlignedByteBuffer&& aMemoryBuffer)
+ : mStorage(ConvertToSpan(aMemoryBuffer)),
+ mMemoryBuffer(std::move(aMemoryBuffer)) {
+ MOZ_ASSERT(std::is_trivial<T>::value);
+ MOZ_ASSERT(!mStorage.IsEmpty());
+ }
+
+ /**
+ * Write `aSamples` number of zeros in the buffer.
+ */
+ uint32_t WriteSilence(uint32_t aSamples) {
+ MOZ_ASSERT(aSamples);
+ return Write(Span<T>(), aSamples);
+ }
+
+ /**
+ * Copy `aBuffer` to the RingBuffer.
+ */
+ uint32_t Write(const Span<const T>& aBuffer) {
+ MOZ_ASSERT(!aBuffer.IsEmpty());
+ return Write(aBuffer, aBuffer.Length());
+ }
+
+ private:
+ /**
+ * Copy `aSamples` number of elements from `aBuffer` to the RingBuffer. If
+ * `aBuffer` is empty append `aSamples` of zeros.
+ */
+ uint32_t Write(const Span<const T>& aBuffer, uint32_t aSamples) {
+ MOZ_ASSERT(aSamples > 0 &&
+ aBuffer.Length() <= static_cast<uint32_t>(aSamples));
+
+ if (IsFull()) {
+ return 0;
+ }
+
+ uint32_t toWrite = std::min(AvailableWrite(), aSamples);
+ uint32_t part1 = std::min(Capacity() - mWriteIndex, toWrite);
+ uint32_t part2 = toWrite - part1;
+
+ Span<T> part1Buffer = mStorage.Subspan(mWriteIndex, part1);
+ Span<T> part2Buffer = mStorage.To(part2);
+
+ if (!aBuffer.IsEmpty()) {
+ Span<const T> fromPart1 = aBuffer.To(part1);
+ Span<const T> fromPart2 = aBuffer.Subspan(part1, part2);
+
+ CopySpan(part1Buffer, fromPart1);
+ CopySpan(part2Buffer, fromPart2);
+ } else {
+ // The aBuffer is empty, append zeros.
+ PodZero(part1Buffer.Elements(), part1Buffer.Length());
+ PodZero(part2Buffer.Elements(), part2Buffer.Length());
+ }
+
+ mWriteIndex = NextIndex(mWriteIndex, toWrite);
+
+ return toWrite;
+ }
+
+ public:
+ /**
+ * Copy `aSamples` number of elements from `aBuffer` to the RingBuffer. The
+ * `aBuffer` does not change.
+ */
+ uint32_t Write(const RingBuffer& aBuffer, uint32_t aSamples) {
+ MOZ_ASSERT(aSamples);
+
+ if (IsFull()) {
+ return 0;
+ }
+
+ uint32_t toWriteThis = std::min(AvailableWrite(), aSamples);
+ uint32_t toReadThat = std::min(aBuffer.AvailableRead(), toWriteThis);
+ uint32_t part1 =
+ std::min(aBuffer.Capacity() - aBuffer.mReadIndex, toReadThat);
+ uint32_t part2 = toReadThat - part1;
+
+ Span<T> part1Buffer = aBuffer.mStorage.Subspan(aBuffer.mReadIndex, part1);
+ DebugOnly<uint32_t> ret = Write(part1Buffer);
+ MOZ_ASSERT(ret == part1);
+ if (part2) {
+ Span<T> part2Buffer = aBuffer.mStorage.To(part2);
+ ret = Write(part2Buffer);
+ MOZ_ASSERT(ret == part2);
+ }
+
+ return toReadThat;
+ }
+
+ /**
+ * Copy `aBuffer.Length()` number of elements from RingBuffer to `aBuffer`.
+ */
+ uint32_t Read(const Span<T>& aBuffer) {
+ MOZ_ASSERT(!aBuffer.IsEmpty());
+ MOZ_ASSERT(aBuffer.size() <= std::numeric_limits<uint32_t>::max());
+
+ if (IsEmpty()) {
+ return 0;
+ }
+
+ uint32_t toRead = std::min<uint32_t>(AvailableRead(), aBuffer.Length());
+ uint32_t part1 = std::min(Capacity() - mReadIndex, toRead);
+ uint32_t part2 = toRead - part1;
+
+ Span<T> part1Buffer = mStorage.Subspan(mReadIndex, part1);
+ Span<T> part2Buffer = mStorage.To(part2);
+
+ Span<T> toPart1 = aBuffer.To(part1);
+ Span<T> toPart2 = aBuffer.Subspan(part1, part2);
+
+ CopySpan(toPart1, part1Buffer);
+ CopySpan(toPart2, part2Buffer);
+
+ mReadIndex = NextIndex(mReadIndex, toRead);
+
+ return toRead;
+ }
+
+ /**
+ * Provide `aCallable` that will be called with the internal linear read
+ * buffers and the number of samples available for reading. The `aCallable`
+ * will be called at most 2 times. The `aCallable` must return the number of
+ * samples that have been actually read. If that number is smaller than the
+ * available number of samples, provided in the argument, the `aCallable` will
+ * not be called again. The RingBuffer's available read samples will be
+ * decreased by the number returned from the `aCallable`.
+ *
+ * The important aspects of this method are that first, it makes it possible
+ * to avoid extra copies to an intermediates buffer, and second, each buffer
+ * provided to `aCallable is a linear piece of memory which can be used
+ * directly to a resampler for example.
+ *
+ * In general, the problem with ring buffers is that they cannot provide one
+ * linear chunk of memory so extra copies, to a linear buffer, are often
+ * needed. This method bridge that gap by breaking the ring buffer's
+ * internal read memory into linear pieces and making it available through
+ * the `aCallable`. In the body of the `aCallable` those buffers can be used
+ * directly without any copy or intermediate steps.
+ */
+ uint32_t ReadNoCopy(
+ std::function<uint32_t(const Span<const T>&)>&& aCallable) {
+ if (IsEmpty()) {
+ return 0;
+ }
+
+ uint32_t part1 = std::min(Capacity() - mReadIndex, AvailableRead());
+ uint32_t part2 = AvailableRead() - part1;
+
+ Span<T> part1Buffer = mStorage.Subspan(mReadIndex, part1);
+ uint32_t toRead = aCallable(part1Buffer);
+ MOZ_ASSERT(toRead <= part1);
+
+ if (toRead == part1 && part2) {
+ Span<T> part2Buffer = mStorage.To(part2);
+ toRead += aCallable(part2Buffer);
+ MOZ_ASSERT(toRead <= part1 + part2);
+ }
+
+ mReadIndex = NextIndex(mReadIndex, toRead);
+
+ return toRead;
+ }
+
+ /**
+ * Remove the next `aSamples` number of samples from the ring buffer.
+ */
+ uint32_t Discard(uint32_t aSamples) {
+ MOZ_ASSERT(aSamples);
+
+ if (IsEmpty()) {
+ return 0;
+ }
+
+ uint32_t toDiscard = std::min(AvailableRead(), aSamples);
+ mReadIndex = NextIndex(mReadIndex, toDiscard);
+
+ return toDiscard;
+ }
+
+ /**
+ * Empty the ring buffer.
+ */
+ uint32_t Clear() {
+ if (IsEmpty()) {
+ return 0;
+ }
+
+ uint32_t toDiscard = AvailableRead();
+ mReadIndex = NextIndex(mReadIndex, toDiscard);
+
+ return toDiscard;
+ }
+
+ /**
+ * Returns true if the full capacity of the ring buffer is being used. When
+ * full any attempt to write more samples to the ring buffer will fail.
+ */
+ bool IsFull() const { return (mWriteIndex + 1) % Capacity() == mReadIndex; }
+
+ /**
+ * Returns true if the ring buffer is empty. When empty any attempt to read
+ * more samples from the ring buffer will fail.
+ */
+ bool IsEmpty() const { return mWriteIndex == mReadIndex; }
+
+ /**
+ * The number of samples available for writing.
+ */
+ uint32_t AvailableWrite() const {
+ /* We subtract one element here to always keep at least one sample
+ * free in the buffer, to distinguish between full and empty array. */
+ uint32_t rv = mReadIndex - mWriteIndex - 1;
+ if (mWriteIndex >= mReadIndex) {
+ rv += Capacity();
+ }
+ return rv;
+ }
+
+ /**
+ * The number of samples available for reading.
+ */
+ uint32_t AvailableRead() const {
+ if (mWriteIndex >= mReadIndex) {
+ return mWriteIndex - mReadIndex;
+ }
+ return mWriteIndex + Capacity() - mReadIndex;
+ }
+
+ private:
+ uint32_t NextIndex(uint32_t aIndex, uint32_t aStep) const {
+ MOZ_ASSERT(aStep < Capacity());
+ MOZ_ASSERT(aIndex < Capacity());
+ return (aIndex + aStep) % Capacity();
+ }
+
+ uint32_t Capacity() const { return mStorage.Length(); }
+
+ Span<T> ConvertToSpan(const AlignedByteBuffer& aOther) const {
+ MOZ_ASSERT(aOther.Length() >= sizeof(T));
+ return Span<T>(reinterpret_cast<T*>(aOther.Data()),
+ aOther.Length() / sizeof(T));
+ }
+
+ void CopySpan(Span<T>& aTo, const Span<const T>& aFrom) {
+ MOZ_ASSERT(aTo.Length() == aFrom.Length());
+ std::copy(aFrom.cbegin(), aFrom.cend(), aTo.begin());
+ }
+
+ private:
+ uint32_t mReadIndex = 0;
+ uint32_t mWriteIndex = 0;
+ /* Points to the mMemoryBuffer. */
+ const Span<T> mStorage;
+ /* The actual allocated memory set from outside. It is set in the ctor and it
+ * is not used again. It is here to control the lifetime of the memory. The
+ * memory is accessed through the mStorage. The idea is that the memory used
+ * from the RingBuffer can be pre-allocated. */
+ const AlignedByteBuffer mMemoryBuffer;
+};
+
+/** AudioRingBuffer **/
+
+/* The private members of AudioRingBuffer. */
+class AudioRingBuffer::AudioRingBufferPrivate {
+ public:
+ AudioSampleFormat mSampleFormat = AUDIO_FORMAT_SILENCE;
+ Maybe<RingBuffer<float>> mFloatRingBuffer;
+ Maybe<RingBuffer<int16_t>> mIntRingBuffer;
+ Maybe<AlignedByteBuffer> mBackingBuffer;
+};
+
+AudioRingBuffer::AudioRingBuffer(uint32_t aSizeInBytes)
+ : mPtr(MakeUnique<AudioRingBufferPrivate>()) {
+ MOZ_ASSERT(aSizeInBytes > 0);
+ mPtr->mBackingBuffer.emplace(aSizeInBytes);
+ MOZ_ASSERT(mPtr->mBackingBuffer);
+}
+
+AudioRingBuffer::~AudioRingBuffer() = default;
+
+void AudioRingBuffer::SetSampleFormat(AudioSampleFormat aFormat) {
+ MOZ_ASSERT(mPtr->mSampleFormat == AUDIO_FORMAT_SILENCE);
+ MOZ_ASSERT(aFormat == AUDIO_FORMAT_S16 || aFormat == AUDIO_FORMAT_FLOAT32);
+ MOZ_ASSERT(!mPtr->mIntRingBuffer);
+ MOZ_ASSERT(!mPtr->mFloatRingBuffer);
+ MOZ_ASSERT(mPtr->mBackingBuffer);
+
+ mPtr->mSampleFormat = aFormat;
+ if (mPtr->mSampleFormat == AUDIO_FORMAT_S16) {
+ mPtr->mIntRingBuffer.emplace(mPtr->mBackingBuffer.extract());
+ MOZ_ASSERT(!mPtr->mBackingBuffer);
+ return;
+ }
+ mPtr->mFloatRingBuffer.emplace(mPtr->mBackingBuffer.extract());
+ MOZ_ASSERT(!mPtr->mBackingBuffer);
+}
+
+uint32_t AudioRingBuffer::Write(const Span<const float>& aBuffer) {
+ MOZ_ASSERT(mPtr->mSampleFormat == AUDIO_FORMAT_FLOAT32);
+ MOZ_ASSERT(!mPtr->mIntRingBuffer);
+ MOZ_ASSERT(!mPtr->mBackingBuffer);
+ return mPtr->mFloatRingBuffer->Write(aBuffer);
+}
+
+uint32_t AudioRingBuffer::Write(const Span<const int16_t>& aBuffer) {
+ MOZ_ASSERT(mPtr->mSampleFormat == AUDIO_FORMAT_S16);
+ MOZ_ASSERT(!mPtr->mFloatRingBuffer);
+ MOZ_ASSERT(!mPtr->mBackingBuffer);
+ return mPtr->mIntRingBuffer->Write(aBuffer);
+}
+
+uint32_t AudioRingBuffer::Write(const AudioRingBuffer& aBuffer,
+ uint32_t aSamples) {
+ MOZ_ASSERT(mPtr->mSampleFormat == AUDIO_FORMAT_S16 ||
+ mPtr->mSampleFormat == AUDIO_FORMAT_FLOAT32);
+ MOZ_ASSERT(!mPtr->mBackingBuffer);
+ if (mPtr->mSampleFormat == AUDIO_FORMAT_S16) {
+ MOZ_ASSERT(!mPtr->mFloatRingBuffer);
+ return mPtr->mIntRingBuffer->Write(aBuffer.mPtr->mIntRingBuffer.ref(),
+ aSamples);
+ }
+ MOZ_ASSERT(!mPtr->mIntRingBuffer);
+ return mPtr->mFloatRingBuffer->Write(aBuffer.mPtr->mFloatRingBuffer.ref(),
+ aSamples);
+}
+
+uint32_t AudioRingBuffer::WriteSilence(uint32_t aSamples) {
+ MOZ_ASSERT(mPtr->mSampleFormat == AUDIO_FORMAT_S16 ||
+ mPtr->mSampleFormat == AUDIO_FORMAT_FLOAT32);
+ MOZ_ASSERT(!mPtr->mBackingBuffer);
+ if (mPtr->mSampleFormat == AUDIO_FORMAT_S16) {
+ MOZ_ASSERT(!mPtr->mFloatRingBuffer);
+ return mPtr->mIntRingBuffer->WriteSilence(aSamples);
+ }
+ MOZ_ASSERT(!mPtr->mIntRingBuffer);
+ return mPtr->mFloatRingBuffer->WriteSilence(aSamples);
+}
+
+uint32_t AudioRingBuffer::Read(const Span<float>& aBuffer) {
+ MOZ_ASSERT(mPtr->mSampleFormat == AUDIO_FORMAT_FLOAT32);
+ MOZ_ASSERT(!mPtr->mIntRingBuffer);
+ MOZ_ASSERT(!mPtr->mBackingBuffer);
+ return mPtr->mFloatRingBuffer->Read(aBuffer);
+}
+
+uint32_t AudioRingBuffer::Read(const Span<int16_t>& aBuffer) {
+ MOZ_ASSERT(mPtr->mSampleFormat == AUDIO_FORMAT_S16);
+ MOZ_ASSERT(!mPtr->mFloatRingBuffer);
+ MOZ_ASSERT(!mPtr->mBackingBuffer);
+ return mPtr->mIntRingBuffer->Read(aBuffer);
+}
+
+uint32_t AudioRingBuffer::ReadNoCopy(
+ std::function<uint32_t(const Span<const float>&)>&& aCallable) {
+ MOZ_ASSERT(mPtr->mSampleFormat == AUDIO_FORMAT_FLOAT32);
+ MOZ_ASSERT(!mPtr->mIntRingBuffer);
+ MOZ_ASSERT(!mPtr->mBackingBuffer);
+ return mPtr->mFloatRingBuffer->ReadNoCopy(std::move(aCallable));
+}
+
+uint32_t AudioRingBuffer::ReadNoCopy(
+ std::function<uint32_t(const Span<const int16_t>&)>&& aCallable) {
+ MOZ_ASSERT(mPtr->mSampleFormat == AUDIO_FORMAT_S16);
+ MOZ_ASSERT(!mPtr->mFloatRingBuffer);
+ MOZ_ASSERT(!mPtr->mBackingBuffer);
+ return mPtr->mIntRingBuffer->ReadNoCopy(std::move(aCallable));
+}
+
+uint32_t AudioRingBuffer::Discard(uint32_t aSamples) {
+ MOZ_ASSERT(mPtr->mSampleFormat == AUDIO_FORMAT_S16 ||
+ mPtr->mSampleFormat == AUDIO_FORMAT_FLOAT32);
+ MOZ_ASSERT(!mPtr->mBackingBuffer);
+ if (mPtr->mSampleFormat == AUDIO_FORMAT_S16) {
+ MOZ_ASSERT(!mPtr->mFloatRingBuffer);
+ return mPtr->mIntRingBuffer->Discard(aSamples);
+ }
+ MOZ_ASSERT(!mPtr->mIntRingBuffer);
+ return mPtr->mFloatRingBuffer->Discard(aSamples);
+}
+
+uint32_t AudioRingBuffer::Clear() {
+ MOZ_ASSERT(mPtr->mSampleFormat == AUDIO_FORMAT_S16 ||
+ mPtr->mSampleFormat == AUDIO_FORMAT_FLOAT32);
+ MOZ_ASSERT(!mPtr->mBackingBuffer);
+ if (mPtr->mSampleFormat == AUDIO_FORMAT_S16) {
+ MOZ_ASSERT(!mPtr->mFloatRingBuffer);
+ MOZ_ASSERT(mPtr->mIntRingBuffer);
+ return mPtr->mIntRingBuffer->Clear();
+ }
+ MOZ_ASSERT(!mPtr->mIntRingBuffer);
+ MOZ_ASSERT(mPtr->mFloatRingBuffer);
+ return mPtr->mFloatRingBuffer->Clear();
+}
+
+bool AudioRingBuffer::IsFull() const {
+ MOZ_ASSERT(mPtr->mSampleFormat == AUDIO_FORMAT_S16 ||
+ mPtr->mSampleFormat == AUDIO_FORMAT_FLOAT32);
+ MOZ_ASSERT(!mPtr->mBackingBuffer);
+ if (mPtr->mSampleFormat == AUDIO_FORMAT_S16) {
+ MOZ_ASSERT(!mPtr->mFloatRingBuffer);
+ return mPtr->mIntRingBuffer->IsFull();
+ }
+ MOZ_ASSERT(!mPtr->mIntRingBuffer);
+ return mPtr->mFloatRingBuffer->IsFull();
+}
+
+bool AudioRingBuffer::IsEmpty() const {
+ MOZ_ASSERT(mPtr->mSampleFormat == AUDIO_FORMAT_S16 ||
+ mPtr->mSampleFormat == AUDIO_FORMAT_FLOAT32);
+ MOZ_ASSERT(!mPtr->mBackingBuffer);
+ if (mPtr->mSampleFormat == AUDIO_FORMAT_S16) {
+ MOZ_ASSERT(!mPtr->mFloatRingBuffer);
+ return mPtr->mIntRingBuffer->IsEmpty();
+ }
+ MOZ_ASSERT(!mPtr->mIntRingBuffer);
+ return mPtr->mFloatRingBuffer->IsEmpty();
+}
+
+uint32_t AudioRingBuffer::AvailableWrite() const {
+ MOZ_ASSERT(mPtr->mSampleFormat == AUDIO_FORMAT_S16 ||
+ mPtr->mSampleFormat == AUDIO_FORMAT_FLOAT32);
+ MOZ_ASSERT(!mPtr->mBackingBuffer);
+ if (mPtr->mSampleFormat == AUDIO_FORMAT_S16) {
+ MOZ_ASSERT(!mPtr->mFloatRingBuffer);
+ return mPtr->mIntRingBuffer->AvailableWrite();
+ }
+ MOZ_ASSERT(!mPtr->mIntRingBuffer);
+ return mPtr->mFloatRingBuffer->AvailableWrite();
+}
+
+uint32_t AudioRingBuffer::AvailableRead() const {
+ MOZ_ASSERT(mPtr->mSampleFormat == AUDIO_FORMAT_S16 ||
+ mPtr->mSampleFormat == AUDIO_FORMAT_FLOAT32);
+ MOZ_ASSERT(!mPtr->mBackingBuffer);
+ if (mPtr->mSampleFormat == AUDIO_FORMAT_S16) {
+ MOZ_ASSERT(!mPtr->mFloatRingBuffer);
+ return mPtr->mIntRingBuffer->AvailableRead();
+ }
+ MOZ_ASSERT(!mPtr->mIntRingBuffer);
+ return mPtr->mFloatRingBuffer->AvailableRead();
+}
+
+} // namespace mozilla
diff --git a/dom/media/AudioRingBuffer.h b/dom/media/AudioRingBuffer.h
new file mode 100644
index 0000000000..305e414bb8
--- /dev/null
+++ b/dom/media/AudioRingBuffer.h
@@ -0,0 +1,116 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_AUDIO_RING_BUFFER_H_
+#define MOZILLA_AUDIO_RING_BUFFER_H_
+
+#include "AudioSampleFormat.h"
+#include "mozilla/Span.h"
+
+#include <functional>
+
+namespace mozilla {
+
+/**
+ * AudioRingBuffer works with audio sample format float or short. The
+ * implementation wrap around the RingBuffer thus it is not thread-safe. Reads
+ * and writes must happen in the same thread which may be different than the
+ * construction thread. The memory is pre-allocated in the constructor. The
+ * sample format has to be specified in order to be used.
+ */
+class AudioRingBuffer final {
+ public:
+ explicit AudioRingBuffer(uint32_t aSizeInBytes);
+ ~AudioRingBuffer();
+
+ /**
+ * Set the sample format to either short or float. The sample format must be
+ * set before the using any other method.
+ */
+ void SetSampleFormat(AudioSampleFormat aFormat);
+
+ /**
+ * Write `aBuffer.Length()` number of samples when the format is float.
+ */
+ uint32_t Write(const Span<const float>& aBuffer);
+
+ /**
+ * Write `aBuffer.Length()` number of samples when the format is short.
+ */
+ uint32_t Write(const Span<const int16_t>& aBuffer);
+
+ /**
+ * Write `aSamples` number of samples from `aBuffer`. Note the `aBuffer` does
+ * not change.
+ */
+ uint32_t Write(const AudioRingBuffer& aBuffer, uint32_t aSamples);
+
+ /**
+ * Write `aSamples` number of zeros.
+ */
+ uint32_t WriteSilence(uint32_t aSamples);
+
+ /**
+ * Read `aBuffer.Length()` number of samples when the format is float.
+ */
+ uint32_t Read(const Span<float>& aBuffer);
+
+ /**
+ * Read `aBuffer.Length()` number of samples when the format is short.
+ */
+ uint32_t Read(const Span<int16_t>& aBuffer);
+
+ /**
+ * Read the internal buffer without extra copies when sample format is float.
+ * Check also the RingBuffer::ReadNoCopy() for more details.
+ */
+ uint32_t ReadNoCopy(
+ std::function<uint32_t(const Span<const float>&)>&& aCallable);
+
+ /**
+ * Read the internal buffer without extra copies when sample format is short.
+ * Check also the RingBuffer::ReadNoCopy() for more details.
+ */
+ uint32_t ReadNoCopy(
+ std::function<uint32_t(const Span<const int16_t>&)>&& aCallable);
+
+ /**
+ * Remove `aSamples` number of samples.
+ */
+ uint32_t Discard(uint32_t aSamples);
+
+ /**
+ * Remove all available samples.
+ */
+ uint32_t Clear();
+
+ /**
+ * Return true if the buffer is full.
+ */
+ bool IsFull() const;
+
+ /**
+ * Return true if the buffer is empty.
+ */
+ bool IsEmpty() const;
+
+ /**
+ * Return the number of samples available for writing.
+ */
+ uint32_t AvailableWrite() const;
+
+ /**
+ * Return the number of samples available for reading.
+ */
+ uint32_t AvailableRead() const;
+
+ private:
+ class AudioRingBufferPrivate;
+ UniquePtr<AudioRingBufferPrivate> mPtr;
+};
+
+} // namespace mozilla
+
+#endif // MOZILLA_AUDIO_RING_BUFFER_H_
diff --git a/dom/media/AudioSampleFormat.h b/dom/media/AudioSampleFormat.h
new file mode 100644
index 0000000000..f53021262b
--- /dev/null
+++ b/dom/media/AudioSampleFormat.h
@@ -0,0 +1,228 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef MOZILLA_AUDIOSAMPLEFORMAT_H_
+#define MOZILLA_AUDIOSAMPLEFORMAT_H_
+
+#include "mozilla/Assertions.h"
+#include <algorithm>
+
+namespace mozilla {
+
+/**
+ * Audio formats supported in MediaTracks and media elements.
+ *
+ * Only one of these is supported by AudioStream, and that is determined
+ * at compile time (roughly, FLOAT32 on desktops, S16 on mobile). Media decoders
+ * produce that format only; queued AudioData always uses that format.
+ */
+enum AudioSampleFormat {
+ // Silence: format will be chosen later
+ AUDIO_FORMAT_SILENCE,
+ // Native-endian signed 16-bit audio samples
+ AUDIO_FORMAT_S16,
+ // Signed 32-bit float samples
+ AUDIO_FORMAT_FLOAT32,
+// The format used for output by AudioStream.
+#ifdef MOZ_SAMPLE_TYPE_S16
+ AUDIO_OUTPUT_FORMAT = AUDIO_FORMAT_S16
+#else
+ AUDIO_OUTPUT_FORMAT = AUDIO_FORMAT_FLOAT32
+#endif
+};
+
+enum { MAX_AUDIO_SAMPLE_SIZE = sizeof(float) };
+
+template <AudioSampleFormat Format>
+class AudioSampleTraits;
+
+template <>
+class AudioSampleTraits<AUDIO_FORMAT_FLOAT32> {
+ public:
+ typedef float Type;
+};
+template <>
+class AudioSampleTraits<AUDIO_FORMAT_S16> {
+ public:
+ typedef int16_t Type;
+};
+
+typedef AudioSampleTraits<AUDIO_OUTPUT_FORMAT>::Type AudioDataValue;
+
+template <typename T>
+class AudioSampleTypeToFormat;
+
+template <>
+class AudioSampleTypeToFormat<float> {
+ public:
+ static const AudioSampleFormat Format = AUDIO_FORMAT_FLOAT32;
+};
+
+template <>
+class AudioSampleTypeToFormat<short> {
+ public:
+ static const AudioSampleFormat Format = AUDIO_FORMAT_S16;
+};
+
+// Single-sample conversion
+/*
+ * Use "2^N" conversion since it's simple, fast, "bit transparent", used by
+ * many other libraries and apparently behaves reasonably.
+ * http://blog.bjornroche.com/2009/12/int-float-int-its-jungle-out-there.html
+ * http://blog.bjornroche.com/2009/12/linearity-and-dynamic-range-in-int.html
+ */
+inline float AudioSampleToFloat(float aValue) { return aValue; }
+inline float AudioSampleToFloat(int16_t aValue) { return aValue / 32768.0f; }
+inline float AudioSampleToFloat(int32_t aValue) {
+ return aValue / (float)(1U << 31);
+}
+
+template <typename T>
+T FloatToAudioSample(float aValue);
+
+template <>
+inline float FloatToAudioSample<float>(float aValue) {
+ return aValue;
+}
+template <>
+inline int16_t FloatToAudioSample<int16_t>(float aValue) {
+ float v = aValue * 32768.0f;
+ float clamped = std::max(-32768.0f, std::min(32767.0f, v));
+ return int16_t(clamped);
+}
+
+template <typename T>
+T UInt8bitToAudioSample(uint8_t aValue);
+
+template <>
+inline float UInt8bitToAudioSample<float>(uint8_t aValue) {
+ return aValue * (static_cast<float>(2) / UINT8_MAX) - static_cast<float>(1);
+}
+template <>
+inline int16_t UInt8bitToAudioSample<int16_t>(uint8_t aValue) {
+ return static_cast<int16_t>((aValue << 8) + aValue + INT16_MIN);
+}
+
+template <typename T>
+T IntegerToAudioSample(int16_t aValue);
+
+template <>
+inline float IntegerToAudioSample<float>(int16_t aValue) {
+ return aValue / 32768.0f;
+}
+template <>
+inline int16_t IntegerToAudioSample<int16_t>(int16_t aValue) {
+ return aValue;
+}
+
+template <typename T>
+T Int24bitToAudioSample(int32_t aValue);
+
+template <>
+inline float Int24bitToAudioSample<float>(int32_t aValue) {
+ return aValue / static_cast<float>(1 << 23);
+}
+template <>
+inline int16_t Int24bitToAudioSample<int16_t>(int32_t aValue) {
+ return static_cast<int16_t>(aValue / 256);
+}
+
+template <typename SrcT, typename DstT>
+inline void ConvertAudioSample(SrcT aIn, DstT& aOut);
+
+template <>
+inline void ConvertAudioSample(int16_t aIn, int16_t& aOut) {
+ aOut = aIn;
+}
+
+template <>
+inline void ConvertAudioSample(int16_t aIn, float& aOut) {
+ aOut = AudioSampleToFloat(aIn);
+}
+
+template <>
+inline void ConvertAudioSample(float aIn, float& aOut) {
+ aOut = aIn;
+}
+
+template <>
+inline void ConvertAudioSample(float aIn, int16_t& aOut) {
+ aOut = FloatToAudioSample<int16_t>(aIn);
+}
+
+// Sample buffer conversion
+
+template <typename From, typename To>
+inline void ConvertAudioSamples(const From* aFrom, To* aTo, int aCount) {
+ for (int i = 0; i < aCount; ++i) {
+ aTo[i] = FloatToAudioSample<To>(AudioSampleToFloat(aFrom[i]));
+ }
+}
+inline void ConvertAudioSamples(const int16_t* aFrom, int16_t* aTo,
+ int aCount) {
+ memcpy(aTo, aFrom, sizeof(*aTo) * aCount);
+}
+inline void ConvertAudioSamples(const float* aFrom, float* aTo, int aCount) {
+ memcpy(aTo, aFrom, sizeof(*aTo) * aCount);
+}
+
+// Sample buffer conversion with scale
+
+template <typename From, typename To>
+inline void ConvertAudioSamplesWithScale(const From* aFrom, To* aTo, int aCount,
+ float aScale) {
+ if (aScale == 1.0f) {
+ ConvertAudioSamples(aFrom, aTo, aCount);
+ return;
+ }
+ for (int i = 0; i < aCount; ++i) {
+ aTo[i] = FloatToAudioSample<To>(AudioSampleToFloat(aFrom[i]) * aScale);
+ }
+}
+inline void ConvertAudioSamplesWithScale(const int16_t* aFrom, int16_t* aTo,
+ int aCount, float aScale) {
+ if (aScale == 1.0f) {
+ ConvertAudioSamples(aFrom, aTo, aCount);
+ return;
+ }
+ if (0.0f <= aScale && aScale < 1.0f) {
+ int32_t scale = int32_t((1 << 16) * aScale);
+ for (int i = 0; i < aCount; ++i) {
+ aTo[i] = int16_t((int32_t(aFrom[i]) * scale) >> 16);
+ }
+ return;
+ }
+ for (int i = 0; i < aCount; ++i) {
+ aTo[i] = FloatToAudioSample<int16_t>(AudioSampleToFloat(aFrom[i]) * aScale);
+ }
+}
+
+// In place audio sample scaling.
+inline void ScaleAudioSamples(float* aBuffer, int aCount, float aScale) {
+ for (int32_t i = 0; i < aCount; ++i) {
+ aBuffer[i] *= aScale;
+ }
+}
+
+inline void ScaleAudioSamples(short* aBuffer, int aCount, float aScale) {
+ int32_t volume = int32_t((1 << 16) * aScale);
+ for (int32_t i = 0; i < aCount; ++i) {
+ aBuffer[i] = short((int32_t(aBuffer[i]) * volume) >> 16);
+ }
+}
+
+inline const void* AddAudioSampleOffset(const void* aBase,
+ AudioSampleFormat aFormat,
+ int32_t aOffset) {
+ static_assert(AUDIO_FORMAT_S16 == 1, "Bad constant");
+ static_assert(AUDIO_FORMAT_FLOAT32 == 2, "Bad constant");
+ MOZ_ASSERT(aFormat == AUDIO_FORMAT_S16 || aFormat == AUDIO_FORMAT_FLOAT32);
+
+ return static_cast<const uint8_t*>(aBase) + aFormat * 2 * aOffset;
+}
+
+} // namespace mozilla
+
+#endif /* MOZILLA_AUDIOSAMPLEFORMAT_H_ */
diff --git a/dom/media/AudioSegment.cpp b/dom/media/AudioSegment.cpp
new file mode 100644
index 0000000000..30352a1d17
--- /dev/null
+++ b/dom/media/AudioSegment.cpp
@@ -0,0 +1,262 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioSegment.h"
+#include "AudioMixer.h"
+#include "AudioChannelFormat.h"
+#include <speex/speex_resampler.h>
+
+namespace mozilla {
+
+const uint8_t
+ SilentChannel::gZeroChannel[MAX_AUDIO_SAMPLE_SIZE *
+ SilentChannel::AUDIO_PROCESSING_FRAMES] = {0};
+
+template <>
+const float* SilentChannel::ZeroChannel<float>() {
+ return reinterpret_cast<const float*>(SilentChannel::gZeroChannel);
+}
+
+template <>
+const int16_t* SilentChannel::ZeroChannel<int16_t>() {
+ return reinterpret_cast<const int16_t*>(SilentChannel::gZeroChannel);
+}
+
+void AudioSegment::ApplyVolume(float aVolume) {
+ for (ChunkIterator ci(*this); !ci.IsEnded(); ci.Next()) {
+ ci->mVolume *= aVolume;
+ }
+}
+
+void AudioSegment::ResampleChunks(nsAutoRef<SpeexResamplerState>& aResampler,
+ uint32_t* aResamplerChannelCount,
+ uint32_t aInRate, uint32_t aOutRate) {
+ if (mChunks.IsEmpty()) {
+ return;
+ }
+
+ AudioSampleFormat format = AUDIO_FORMAT_SILENCE;
+ for (ChunkIterator ci(*this); !ci.IsEnded(); ci.Next()) {
+ if (ci->mBufferFormat != AUDIO_FORMAT_SILENCE) {
+ format = ci->mBufferFormat;
+ }
+ }
+
+ switch (format) {
+ // If the format is silence at this point, all the chunks are silent. The
+ // actual function we use does not matter, it's just a matter of changing
+ // the chunks duration.
+ case AUDIO_FORMAT_SILENCE:
+ case AUDIO_FORMAT_FLOAT32:
+ Resample<float>(aResampler, aResamplerChannelCount, aInRate, aOutRate);
+ break;
+ case AUDIO_FORMAT_S16:
+ Resample<int16_t>(aResampler, aResamplerChannelCount, aInRate, aOutRate);
+ break;
+ default:
+ MOZ_ASSERT(false);
+ break;
+ }
+}
+
+size_t AudioSegment::WriteToInterleavedBuffer(nsTArray<AudioDataValue>& aBuffer,
+ uint32_t aChannels) const {
+ size_t offset = 0;
+ if (GetDuration() <= 0) {
+ MOZ_ASSERT(GetDuration() == 0);
+ return offset;
+ }
+
+ // Calculate how many samples in this segment
+ size_t frames = static_cast<size_t>(GetDuration());
+ CheckedInt<size_t> samples(frames);
+ samples *= static_cast<size_t>(aChannels);
+ MOZ_ASSERT(samples.isValid());
+ if (!samples.isValid()) {
+ return offset;
+ }
+
+ // Enlarge buffer space if needed
+ if (samples.value() > aBuffer.Capacity()) {
+ aBuffer.SetCapacity(samples.value());
+ }
+ aBuffer.SetLengthAndRetainStorage(samples.value());
+ aBuffer.ClearAndRetainStorage();
+
+ // Convert the de-interleaved chunks into an interleaved buffer. Note that
+ // we may upmix or downmix the audio data if the channel in the chunks
+ // mismatch with aChannels
+ for (ConstChunkIterator ci(*this); !ci.IsEnded(); ci.Next()) {
+ const AudioChunk& c = *ci;
+ size_t samplesInChunk = static_cast<size_t>(c.mDuration) * aChannels;
+ switch (c.mBufferFormat) {
+ case AUDIO_FORMAT_S16:
+ WriteChunk<int16_t>(c, aChannels, c.mVolume,
+ aBuffer.Elements() + offset);
+ break;
+ case AUDIO_FORMAT_FLOAT32:
+ WriteChunk<float>(c, aChannels, c.mVolume, aBuffer.Elements() + offset);
+ break;
+ case AUDIO_FORMAT_SILENCE:
+ PodZero(aBuffer.Elements() + offset, samplesInChunk);
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unknown format");
+ PodZero(aBuffer.Elements() + offset, samplesInChunk);
+ break;
+ }
+ offset += samplesInChunk;
+ }
+ MOZ_DIAGNOSTIC_ASSERT(samples.value() == offset,
+ "Segment's duration is incorrect");
+ aBuffer.SetLengthAndRetainStorage(offset);
+ return offset;
+}
+
+// This helps to to safely get a pointer to the position we want to start
+// writing a planar audio buffer, depending on the channel and the offset in the
+// buffer.
+static AudioDataValue* PointerForOffsetInChannel(AudioDataValue* aData,
+ size_t aLengthSamples,
+ uint32_t aChannelCount,
+ uint32_t aChannel,
+ uint32_t aOffsetSamples) {
+ size_t samplesPerChannel = aLengthSamples / aChannelCount;
+ size_t beginningOfChannel = samplesPerChannel * aChannel;
+ MOZ_ASSERT(aChannel * samplesPerChannel + aOffsetSamples < aLengthSamples,
+ "Offset request out of bounds.");
+ return aData + beginningOfChannel + aOffsetSamples;
+}
+
+void AudioSegment::Mix(AudioMixer& aMixer, uint32_t aOutputChannels,
+ uint32_t aSampleRate) {
+ AutoTArray<AudioDataValue,
+ SilentChannel::AUDIO_PROCESSING_FRAMES * GUESS_AUDIO_CHANNELS>
+ buf;
+ AutoTArray<const AudioDataValue*, GUESS_AUDIO_CHANNELS> channelData;
+ uint32_t offsetSamples = 0;
+ uint32_t duration = GetDuration();
+
+ if (duration <= 0) {
+ MOZ_ASSERT(duration == 0);
+ return;
+ }
+
+ uint32_t outBufferLength = duration * aOutputChannels;
+ buf.SetLength(outBufferLength);
+
+ for (ChunkIterator ci(*this); !ci.IsEnded(); ci.Next()) {
+ AudioChunk& c = *ci;
+ uint32_t frames = c.mDuration;
+
+ // If the chunk is silent, simply write the right number of silence in the
+ // buffers.
+ if (c.mBufferFormat == AUDIO_FORMAT_SILENCE) {
+ for (uint32_t channel = 0; channel < aOutputChannels; channel++) {
+ AudioDataValue* ptr =
+ PointerForOffsetInChannel(buf.Elements(), outBufferLength,
+ aOutputChannels, channel, offsetSamples);
+ PodZero(ptr, frames);
+ }
+ } else {
+ // Othewise, we need to upmix or downmix appropriately, depending on the
+ // desired input and output channels.
+ channelData.SetLength(c.mChannelData.Length());
+ for (uint32_t i = 0; i < channelData.Length(); ++i) {
+ channelData[i] = static_cast<const AudioDataValue*>(c.mChannelData[i]);
+ }
+ if (channelData.Length() < aOutputChannels) {
+ // Up-mix.
+ AudioChannelsUpMix(&channelData, aOutputChannels,
+ SilentChannel::ZeroChannel<AudioDataValue>());
+ for (uint32_t channel = 0; channel < aOutputChannels; channel++) {
+ AudioDataValue* ptr = PointerForOffsetInChannel(
+ buf.Elements(), outBufferLength, aOutputChannels, channel,
+ offsetSamples);
+ PodCopy(ptr,
+ reinterpret_cast<const AudioDataValue*>(channelData[channel]),
+ frames);
+ }
+ MOZ_ASSERT(channelData.Length() == aOutputChannels);
+ } else if (channelData.Length() > aOutputChannels) {
+ // Down mix.
+ AutoTArray<AudioDataValue*, GUESS_AUDIO_CHANNELS> outChannelPtrs;
+ outChannelPtrs.SetLength(aOutputChannels);
+ uint32_t offsetSamples = 0;
+ for (uint32_t channel = 0; channel < aOutputChannels; channel++) {
+ outChannelPtrs[channel] = PointerForOffsetInChannel(
+ buf.Elements(), outBufferLength, aOutputChannels, channel,
+ offsetSamples);
+ }
+ AudioChannelsDownMix(channelData, outChannelPtrs.Elements(),
+ aOutputChannels, frames);
+ } else {
+ // The channel count is already what we want, just copy it over.
+ for (uint32_t channel = 0; channel < aOutputChannels; channel++) {
+ AudioDataValue* ptr = PointerForOffsetInChannel(
+ buf.Elements(), outBufferLength, aOutputChannels, channel,
+ offsetSamples);
+ PodCopy(ptr,
+ reinterpret_cast<const AudioDataValue*>(channelData[channel]),
+ frames);
+ }
+ }
+ }
+ offsetSamples += frames;
+ }
+
+ if (offsetSamples) {
+ MOZ_ASSERT(offsetSamples == outBufferLength / aOutputChannels,
+ "We forgot to write some samples?");
+ aMixer.Mix(buf.Elements(), aOutputChannels, offsetSamples, aSampleRate);
+ }
+}
+
+void AudioSegment::WriteTo(AudioMixer& aMixer, uint32_t aOutputChannels,
+ uint32_t aSampleRate) {
+ AutoTArray<AudioDataValue,
+ SilentChannel::AUDIO_PROCESSING_FRAMES * GUESS_AUDIO_CHANNELS>
+ buf;
+ // Offset in the buffer that will be written to the mixer, in samples.
+ uint32_t offset = 0;
+
+ if (GetDuration() <= 0) {
+ MOZ_ASSERT(GetDuration() == 0);
+ return;
+ }
+
+ uint32_t outBufferLength = GetDuration() * aOutputChannels;
+ buf.SetLength(outBufferLength);
+
+ for (ChunkIterator ci(*this); !ci.IsEnded(); ci.Next()) {
+ AudioChunk& c = *ci;
+
+ switch (c.mBufferFormat) {
+ case AUDIO_FORMAT_S16:
+ WriteChunk<int16_t>(c, aOutputChannels, c.mVolume,
+ buf.Elements() + offset);
+ break;
+ case AUDIO_FORMAT_FLOAT32:
+ WriteChunk<float>(c, aOutputChannels, c.mVolume,
+ buf.Elements() + offset);
+ break;
+ case AUDIO_FORMAT_SILENCE:
+ // The mixer is expecting interleaved data, so this is ok.
+ PodZero(buf.Elements() + offset, c.mDuration * aOutputChannels);
+ break;
+ default:
+ MOZ_ASSERT(false, "Not handled");
+ }
+
+ offset += c.mDuration * aOutputChannels;
+ }
+
+ if (offset) {
+ aMixer.Mix(buf.Elements(), aOutputChannels, offset / aOutputChannels,
+ aSampleRate);
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/AudioSegment.h b/dom/media/AudioSegment.h
new file mode 100644
index 0000000000..a13db6fec6
--- /dev/null
+++ b/dom/media/AudioSegment.h
@@ -0,0 +1,539 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_AUDIOSEGMENT_H_
+#define MOZILLA_AUDIOSEGMENT_H_
+
+#include <speex/speex_resampler.h>
+#include "MediaTrackGraph.h"
+#include "MediaSegment.h"
+#include "AudioSampleFormat.h"
+#include "AudioChannelFormat.h"
+#include "SharedBuffer.h"
+#include "WebAudioUtils.h"
+#include "mozilla/ScopeExit.h"
+#include "nsAutoRef.h"
+#ifdef MOZILLA_INTERNAL_API
+# include "mozilla/TimeStamp.h"
+#endif
+#include <float.h>
+
+namespace mozilla {
+struct AudioChunk;
+class AudioSegment;
+} // namespace mozilla
+MOZ_DECLARE_RELOCATE_USING_MOVE_CONSTRUCTOR(mozilla::AudioChunk)
+
+/**
+ * This allows compilation of nsTArray<AudioSegment> and
+ * AutoTArray<AudioSegment> since without it, static analysis fails on the
+ * mChunks member being a non-memmovable AutoTArray.
+ *
+ * Note that AudioSegment(const AudioSegment&) is deleted, so this should
+ * never come into effect.
+ */
+MOZ_DECLARE_RELOCATE_USING_MOVE_CONSTRUCTOR(mozilla::AudioSegment)
+
+namespace mozilla {
+
+template <typename T>
+class SharedChannelArrayBuffer : public ThreadSharedObject {
+ public:
+ explicit SharedChannelArrayBuffer(nsTArray<nsTArray<T> >&& aBuffers)
+ : mBuffers(std::move(aBuffers)) {}
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override {
+ size_t amount = 0;
+ amount += mBuffers.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (size_t i = 0; i < mBuffers.Length(); i++) {
+ amount += mBuffers[i].ShallowSizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ return amount;
+ }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ nsTArray<nsTArray<T> > mBuffers;
+};
+
+class AudioMixer;
+
+/**
+ * For auto-arrays etc, guess this as the common number of channels.
+ */
+const int GUESS_AUDIO_CHANNELS = 2;
+
+// We ensure that the graph advances in steps that are multiples of the Web
+// Audio block size
+const uint32_t WEBAUDIO_BLOCK_SIZE_BITS = 7;
+const uint32_t WEBAUDIO_BLOCK_SIZE = 1 << WEBAUDIO_BLOCK_SIZE_BITS;
+
+template <typename SrcT, typename DestT>
+static void InterleaveAndConvertBuffer(const SrcT* const* aSourceChannels,
+ uint32_t aLength, float aVolume,
+ uint32_t aChannels, DestT* aOutput) {
+ DestT* output = aOutput;
+ for (size_t i = 0; i < aLength; ++i) {
+ for (size_t channel = 0; channel < aChannels; ++channel) {
+ float v = AudioSampleToFloat(aSourceChannels[channel][i]) * aVolume;
+ *output = FloatToAudioSample<DestT>(v);
+ ++output;
+ }
+ }
+}
+
+template <typename SrcT, typename DestT>
+static void DeinterleaveAndConvertBuffer(const SrcT* aSourceBuffer,
+ uint32_t aFrames, uint32_t aChannels,
+ DestT** aOutput) {
+ for (size_t i = 0; i < aChannels; i++) {
+ size_t interleavedIndex = i;
+ for (size_t j = 0; j < aFrames; j++) {
+ ConvertAudioSample(aSourceBuffer[interleavedIndex], aOutput[i][j]);
+ interleavedIndex += aChannels;
+ }
+ }
+}
+
+class SilentChannel {
+ public:
+ static const int AUDIO_PROCESSING_FRAMES = 640; /* > 10ms of 48KHz audio */
+ static const uint8_t
+ gZeroChannel[MAX_AUDIO_SAMPLE_SIZE * AUDIO_PROCESSING_FRAMES];
+ // We take advantage of the fact that zero in float and zero in int have the
+ // same all-zeros bit layout.
+ template <typename T>
+ static const T* ZeroChannel();
+};
+
+/**
+ * Given an array of input channels (aChannelData), downmix to aOutputChannels,
+ * interleave the channel data. A total of aOutputChannels*aDuration
+ * interleaved samples will be copied to a channel buffer in aOutput.
+ */
+template <typename SrcT, typename DestT>
+void DownmixAndInterleave(const nsTArray<const SrcT*>& aChannelData,
+ int32_t aDuration, float aVolume,
+ uint32_t aOutputChannels, DestT* aOutput) {
+ if (aChannelData.Length() == aOutputChannels) {
+ InterleaveAndConvertBuffer(aChannelData.Elements(), aDuration, aVolume,
+ aOutputChannels, aOutput);
+ } else {
+ AutoTArray<SrcT*, GUESS_AUDIO_CHANNELS> outputChannelData;
+ AutoTArray<SrcT,
+ SilentChannel::AUDIO_PROCESSING_FRAMES * GUESS_AUDIO_CHANNELS>
+ outputBuffers;
+ outputChannelData.SetLength(aOutputChannels);
+ outputBuffers.SetLength(aDuration * aOutputChannels);
+ for (uint32_t i = 0; i < aOutputChannels; i++) {
+ outputChannelData[i] = outputBuffers.Elements() + aDuration * i;
+ }
+ AudioChannelsDownMix(aChannelData, outputChannelData.Elements(),
+ aOutputChannels, aDuration);
+ InterleaveAndConvertBuffer(outputChannelData.Elements(), aDuration, aVolume,
+ aOutputChannels, aOutput);
+ }
+}
+
+/**
+ * An AudioChunk represents a multi-channel buffer of audio samples.
+ * It references an underlying ThreadSharedObject which manages the lifetime
+ * of the buffer. An AudioChunk maintains its own duration and channel data
+ * pointers so it can represent a subinterval of a buffer without copying.
+ * An AudioChunk can store its individual channels anywhere; it maintains
+ * separate pointers to each channel's buffer.
+ */
+struct AudioChunk {
+ typedef mozilla::AudioSampleFormat SampleFormat;
+
+ AudioChunk() = default;
+
+ template <typename T>
+ AudioChunk(already_AddRefed<ThreadSharedObject> aBuffer,
+ const nsTArray<const T*>& aChannelData, TrackTime aDuration,
+ PrincipalHandle aPrincipalHandle)
+ : mDuration(aDuration),
+ mBuffer(aBuffer),
+ mBufferFormat(AudioSampleTypeToFormat<T>::Format),
+ mPrincipalHandle(std::move(aPrincipalHandle)) {
+ MOZ_ASSERT(!mBuffer == aChannelData.IsEmpty(), "Appending invalid data ?");
+ for (const T* data : aChannelData) {
+ mChannelData.AppendElement(data);
+ }
+ }
+
+ // Generic methods
+ void SliceTo(TrackTime aStart, TrackTime aEnd) {
+ MOZ_ASSERT(aStart >= 0, "Slice out of bounds: invalid start");
+ MOZ_ASSERT(aStart < aEnd, "Slice out of bounds: invalid range");
+ MOZ_ASSERT(aEnd <= mDuration, "Slice out of bounds: invalid end");
+
+ if (mBuffer) {
+ MOZ_ASSERT(aStart < INT32_MAX,
+ "Can't slice beyond 32-bit sample lengths");
+ for (uint32_t channel = 0; channel < mChannelData.Length(); ++channel) {
+ mChannelData[channel] = AddAudioSampleOffset(
+ mChannelData[channel], mBufferFormat, int32_t(aStart));
+ }
+ }
+ mDuration = aEnd - aStart;
+ }
+ TrackTime GetDuration() const { return mDuration; }
+ bool CanCombineWithFollowing(const AudioChunk& aOther) const {
+ if (aOther.mBuffer != mBuffer) {
+ return false;
+ }
+ if (!mBuffer) {
+ return true;
+ }
+ if (aOther.mVolume != mVolume) {
+ return false;
+ }
+ if (aOther.mPrincipalHandle != mPrincipalHandle) {
+ return false;
+ }
+ NS_ASSERTION(aOther.mBufferFormat == mBufferFormat,
+ "Wrong metadata about buffer");
+ NS_ASSERTION(aOther.mChannelData.Length() == mChannelData.Length(),
+ "Mismatched channel count");
+ if (mDuration > INT32_MAX) {
+ return false;
+ }
+ for (uint32_t channel = 0; channel < mChannelData.Length(); ++channel) {
+ if (aOther.mChannelData[channel] !=
+ AddAudioSampleOffset(mChannelData[channel], mBufferFormat,
+ int32_t(mDuration))) {
+ return false;
+ }
+ }
+ return true;
+ }
+ bool IsNull() const { return mBuffer == nullptr; }
+ void SetNull(TrackTime aDuration) {
+ mBuffer = nullptr;
+ mChannelData.Clear();
+ mDuration = aDuration;
+ mVolume = 1.0f;
+ mBufferFormat = AUDIO_FORMAT_SILENCE;
+ mPrincipalHandle = PRINCIPAL_HANDLE_NONE;
+ }
+
+ uint32_t ChannelCount() const { return mChannelData.Length(); }
+
+ bool IsMuted() const { return mVolume == 0.0f; }
+
+ size_t SizeOfExcludingThisIfUnshared(MallocSizeOf aMallocSizeOf) const {
+ return SizeOfExcludingThis(aMallocSizeOf, true);
+ }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf, bool aUnshared) const {
+ size_t amount = 0;
+
+ // Possibly owned:
+ // - mBuffer - Can hold data that is also in the decoded audio queue. If it
+ // is not shared, or unshared == false it gets counted.
+ if (mBuffer && (!aUnshared || !mBuffer->IsShared())) {
+ amount += mBuffer->SizeOfIncludingThis(aMallocSizeOf);
+ }
+
+ // Memory in the array is owned by mBuffer.
+ amount += mChannelData.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ return amount;
+ }
+
+ template <typename T>
+ const nsTArray<const T*>& ChannelData() const {
+ MOZ_ASSERT(AudioSampleTypeToFormat<T>::Format == mBufferFormat);
+ return *reinterpret_cast<const AutoTArray<const T*, GUESS_AUDIO_CHANNELS>*>(
+ &mChannelData);
+ }
+
+ /**
+ * ChannelFloatsForWrite() should be used only when mBuffer is owned solely
+ * by the calling thread.
+ */
+ template <typename T>
+ T* ChannelDataForWrite(size_t aChannel) {
+ MOZ_ASSERT(AudioSampleTypeToFormat<T>::Format == mBufferFormat);
+ MOZ_ASSERT(!mBuffer->IsShared());
+ return static_cast<T*>(const_cast<void*>(mChannelData[aChannel]));
+ }
+
+ template <typename T>
+ static AudioChunk FromInterleavedBuffer(
+ const T* aBuffer, size_t aFrames, uint32_t aChannels,
+ const PrincipalHandle& aPrincipalHandle) {
+ CheckedInt<size_t> bufferSize(sizeof(T));
+ bufferSize *= aFrames;
+ bufferSize *= aChannels;
+ RefPtr<SharedBuffer> buffer = SharedBuffer::Create(bufferSize);
+
+ AutoTArray<T*, 8> deinterleaved;
+ if (aChannels == 1) {
+ PodCopy(static_cast<T*>(buffer->Data()), aBuffer, aFrames);
+ deinterleaved.AppendElement(static_cast<T*>(buffer->Data()));
+ } else {
+ deinterleaved.SetLength(aChannels);
+ T* samples = static_cast<T*>(buffer->Data());
+
+ size_t offset = 0;
+ for (uint32_t i = 0; i < aChannels; ++i) {
+ deinterleaved[i] = samples + offset;
+ offset += aFrames;
+ }
+
+ DeinterleaveAndConvertBuffer(aBuffer, static_cast<uint32_t>(aFrames),
+ aChannels, deinterleaved.Elements());
+ }
+
+ AutoTArray<const T*, GUESS_AUDIO_CHANNELS> channelData;
+ channelData.AppendElements(deinterleaved);
+ return AudioChunk(buffer.forget(), channelData,
+ static_cast<TrackTime>(aFrames), aPrincipalHandle);
+ }
+
+ const PrincipalHandle& GetPrincipalHandle() const { return mPrincipalHandle; }
+
+ TrackTime mDuration = 0; // in frames within the buffer
+ RefPtr<ThreadSharedObject> mBuffer; // the buffer object whose lifetime is
+ // managed; null means data is all zeroes
+ // one pointer per channel; empty if and only if mBuffer is null
+ CopyableAutoTArray<const void*, GUESS_AUDIO_CHANNELS> mChannelData;
+ float mVolume = 1.0f; // volume multiplier to apply
+ // format of frames in mBuffer (or silence if mBuffer is null)
+ SampleFormat mBufferFormat = AUDIO_FORMAT_SILENCE;
+ // principalHandle for the data in this chunk.
+ // This can be compared to an nsIPrincipal* when back on main thread.
+ PrincipalHandle mPrincipalHandle = PRINCIPAL_HANDLE_NONE;
+};
+
+/**
+ * A list of audio samples consisting of a sequence of slices of SharedBuffers.
+ * The audio rate is determined by the track, not stored in this class.
+ */
+class AudioSegment : public MediaSegmentBase<AudioSegment, AudioChunk> {
+ // The channel count that MaxChannelCount() returned last time it was called.
+ uint32_t mMemoizedMaxChannelCount = 0;
+
+ public:
+ typedef mozilla::AudioSampleFormat SampleFormat;
+
+ AudioSegment() : MediaSegmentBase<AudioSegment, AudioChunk>(AUDIO) {}
+
+ AudioSegment(AudioSegment&& aSegment) = default;
+
+ AudioSegment(const AudioSegment&) = delete;
+ AudioSegment& operator=(const AudioSegment&) = delete;
+
+ ~AudioSegment() = default;
+
+ // Resample the whole segment in place. `aResampler` is an instance of a
+ // resampler, initialized with `aResamplerChannelCount` channels. If this
+ // function finds a chunk with more channels, `aResampler` is destroyed and a
+ // new resampler is created, and `aResamplerChannelCount` is updated with the
+ // new channel count value.
+ template <typename T>
+ void Resample(nsAutoRef<SpeexResamplerState>& aResampler,
+ uint32_t* aResamplerChannelCount, uint32_t aInRate,
+ uint32_t aOutRate) {
+ mDuration = 0;
+
+ for (ChunkIterator ci(*this); !ci.IsEnded(); ci.Next()) {
+ AutoTArray<nsTArray<T>, GUESS_AUDIO_CHANNELS> output;
+ AutoTArray<const T*, GUESS_AUDIO_CHANNELS> bufferPtrs;
+ AudioChunk& c = *ci;
+ // If this chunk is null, don't bother resampling, just alter its duration
+ if (c.IsNull()) {
+ c.mDuration = (c.mDuration * aOutRate) / aInRate;
+ mDuration += c.mDuration;
+ continue;
+ }
+ uint32_t channels = c.mChannelData.Length();
+ // This might introduce a discontinuity, but a channel count change in the
+ // middle of a stream is not that common. This also initializes the
+ // resampler as late as possible.
+ if (channels != *aResamplerChannelCount) {
+ SpeexResamplerState* state =
+ speex_resampler_init(channels, aInRate, aOutRate,
+ SPEEX_RESAMPLER_QUALITY_DEFAULT, nullptr);
+ MOZ_ASSERT(state);
+ aResampler.own(state);
+ *aResamplerChannelCount = channels;
+ }
+ output.SetLength(channels);
+ bufferPtrs.SetLength(channels);
+ uint32_t inFrames = c.mDuration;
+ // Round up to allocate; the last frame may not be used.
+ NS_ASSERTION((UINT64_MAX - aInRate + 1) / c.mDuration >= aOutRate,
+ "Dropping samples");
+ uint32_t outSize =
+ (static_cast<uint64_t>(c.mDuration) * aOutRate + aInRate - 1) /
+ aInRate;
+ for (uint32_t i = 0; i < channels; i++) {
+ T* out = output[i].AppendElements(outSize);
+ uint32_t outFrames = outSize;
+
+ const T* in = static_cast<const T*>(c.mChannelData[i]);
+ dom::WebAudioUtils::SpeexResamplerProcess(aResampler.get(), i, in,
+ &inFrames, out, &outFrames);
+ MOZ_ASSERT(inFrames == c.mDuration);
+
+ bufferPtrs[i] = out;
+ output[i].SetLength(outFrames);
+ }
+ MOZ_ASSERT(channels > 0);
+ c.mDuration = output[0].Length();
+ c.mBuffer = new mozilla::SharedChannelArrayBuffer<T>(std::move(output));
+ for (uint32_t i = 0; i < channels; i++) {
+ c.mChannelData[i] = bufferPtrs[i];
+ }
+ mDuration += c.mDuration;
+ }
+ }
+
+ void ResampleChunks(nsAutoRef<SpeexResamplerState>& aResampler,
+ uint32_t* aResamplerChannelCount, uint32_t aInRate,
+ uint32_t aOutRate);
+
+ template <typename T>
+ void AppendFrames(already_AddRefed<ThreadSharedObject> aBuffer,
+ const nsTArray<const T*>& aChannelData, TrackTime aDuration,
+ const PrincipalHandle& aPrincipalHandle) {
+ AppendAndConsumeChunk(AudioChunk(std::move(aBuffer), aChannelData,
+ aDuration, aPrincipalHandle));
+ }
+ void AppendSegment(const AudioSegment* aSegment) {
+ MOZ_ASSERT(aSegment);
+
+ for (const AudioChunk& c : aSegment->mChunks) {
+ AudioChunk* chunk = AppendChunk(c.GetDuration());
+ chunk->mBuffer = c.mBuffer;
+ chunk->mChannelData = c.mChannelData;
+ chunk->mBufferFormat = c.mBufferFormat;
+ chunk->mPrincipalHandle = c.mPrincipalHandle;
+ }
+ }
+ template <typename T>
+ void AppendFromInterleavedBuffer(const T* aBuffer, size_t aFrames,
+ uint32_t aChannels,
+ const PrincipalHandle& aPrincipalHandle) {
+ AppendAndConsumeChunk(AudioChunk::FromInterleavedBuffer<T>(
+ aBuffer, aFrames, aChannels, aPrincipalHandle));
+ }
+ // Write the segement data into an interleaved buffer. Do mixing if the
+ // AudioChunk's channel count in the segment is different from aChannels.
+ // Returns sample count of the converted audio data. The converted data will
+ // be stored into aBuffer.
+ size_t WriteToInterleavedBuffer(nsTArray<AudioDataValue>& aBuffer,
+ uint32_t aChannels) const;
+ // Consumes aChunk, and append it to the segment if its duration is not zero.
+ void AppendAndConsumeChunk(AudioChunk&& aChunk) {
+ AudioChunk unused;
+ AudioChunk* chunk = &unused;
+
+ // Always consume aChunk. The chunk's mBuffer can be non-null even if its
+ // duration is 0.
+ auto consume = MakeScopeExit([&] {
+ chunk->mBuffer = std::move(aChunk.mBuffer);
+ chunk->mChannelData = std::move(aChunk.mChannelData);
+
+ MOZ_ASSERT(chunk->mBuffer || chunk->mChannelData.IsEmpty(),
+ "Appending invalid data ?");
+
+ chunk->mVolume = aChunk.mVolume;
+ chunk->mBufferFormat = aChunk.mBufferFormat;
+ chunk->mPrincipalHandle = std::move(aChunk.mPrincipalHandle);
+ });
+
+ if (aChunk.GetDuration() == 0) {
+ return;
+ }
+
+ if (!mChunks.IsEmpty() &&
+ mChunks.LastElement().CanCombineWithFollowing(aChunk)) {
+ mChunks.LastElement().mDuration += aChunk.GetDuration();
+ mDuration += aChunk.GetDuration();
+ return;
+ }
+
+ chunk = AppendChunk(aChunk.mDuration);
+ }
+ void ApplyVolume(float aVolume);
+ // Mix the segment into a mixer, interleaved. This is useful to output a
+ // segment to a system audio callback. It up or down mixes to aChannelCount
+ // channels.
+ void WriteTo(AudioMixer& aMixer, uint32_t aChannelCount,
+ uint32_t aSampleRate);
+ // Mix the segment into a mixer, keeping it planar, up or down mixing to
+ // aChannelCount channels.
+ void Mix(AudioMixer& aMixer, uint32_t aChannelCount, uint32_t aSampleRate);
+
+ // Returns the maximum channel count across all chunks in this segment.
+ // Should there be no chunk with a channel count we return the memoized return
+ // value from last time this method was called.
+ uint32_t MaxChannelCount() {
+ uint32_t channelCount = 0;
+ for (ChunkIterator ci(*this); !ci.IsEnded(); ci.Next()) {
+ if (ci->ChannelCount()) {
+ channelCount = std::max(channelCount, ci->ChannelCount());
+ }
+ }
+ if (channelCount == 0) {
+ return mMemoizedMaxChannelCount;
+ }
+ return mMemoizedMaxChannelCount = channelCount;
+ }
+
+ static Type StaticType() { return AUDIO; }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ PrincipalHandle GetOldestPrinciple() const {
+ const AudioChunk* chunk = mChunks.IsEmpty() ? nullptr : &mChunks[0];
+ return chunk ? chunk->GetPrincipalHandle() : PRINCIPAL_HANDLE_NONE;
+ }
+
+ // Iterate on each chunks until the input function returns true.
+ template <typename Function>
+ void IterateOnChunks(const Function&& aFunction) {
+ for (uint32_t idx = 0; idx < mChunks.Length(); idx++) {
+ if (aFunction(&mChunks[idx])) {
+ return;
+ }
+ }
+ }
+};
+
+template <typename SrcT>
+void WriteChunk(const AudioChunk& aChunk, uint32_t aOutputChannels,
+ float aVolume, AudioDataValue* aOutputBuffer) {
+ AutoTArray<const SrcT*, GUESS_AUDIO_CHANNELS> channelData;
+
+ channelData = aChunk.ChannelData<SrcT>().Clone();
+
+ if (channelData.Length() < aOutputChannels) {
+ // Up-mix. Note that this might actually make channelData have more
+ // than aOutputChannels temporarily.
+ AudioChannelsUpMix(&channelData, aOutputChannels,
+ SilentChannel::ZeroChannel<SrcT>());
+ }
+ if (channelData.Length() > aOutputChannels) {
+ // Down-mix.
+ DownmixAndInterleave(channelData, aChunk.mDuration, aVolume,
+ aOutputChannels, aOutputBuffer);
+ } else {
+ InterleaveAndConvertBuffer(channelData.Elements(), aChunk.mDuration,
+ aVolume, aOutputChannels, aOutputBuffer);
+ }
+}
+
+} // namespace mozilla
+
+#endif /* MOZILLA_AUDIOSEGMENT_H_ */
diff --git a/dom/media/AudioStream.cpp b/dom/media/AudioStream.cpp
new file mode 100644
index 0000000000..33ac15563d
--- /dev/null
+++ b/dom/media/AudioStream.cpp
@@ -0,0 +1,752 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include <stdio.h>
+#include <math.h>
+#include <string.h>
+#include "mozilla/Logging.h"
+#include "prdtoa.h"
+#include "AudioStream.h"
+#include "VideoUtils.h"
+#include "mozilla/dom/AudioDeviceInfo.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/Sprintf.h"
+#include "mozilla/Unused.h"
+#include <algorithm>
+#include "mozilla/Telemetry.h"
+#include "CubebUtils.h"
+#include "nsNativeCharsetUtils.h"
+#include "nsPrintfCString.h"
+#include "AudioConverter.h"
+#include "UnderrunHandler.h"
+#if defined(XP_WIN)
+# include "nsXULAppAPI.h"
+#endif
+#include "Tracing.h"
+#include "webaudio/blink/DenormalDisabler.h"
+#include "CallbackThreadRegistry.h"
+#include "mozilla/StaticPrefs_media.h"
+
+// Use abort() instead of exception in SoundTouch.
+#define ST_NO_EXCEPTION_HANDLING 1
+#include "soundtouch/SoundTouchFactory.h"
+
+namespace mozilla {
+
+#undef LOG
+#undef LOGW
+#undef LOGE
+
+LazyLogModule gAudioStreamLog("AudioStream");
+// For simple logs
+#define LOG(x, ...) \
+ MOZ_LOG(gAudioStreamLog, mozilla::LogLevel::Debug, \
+ ("%p " x, this, ##__VA_ARGS__))
+#define LOGW(x, ...) \
+ MOZ_LOG(gAudioStreamLog, mozilla::LogLevel::Warning, \
+ ("%p " x, this, ##__VA_ARGS__))
+#define LOGE(x, ...) \
+ NS_DebugBreak(NS_DEBUG_WARNING, \
+ nsPrintfCString("%p " x, this, ##__VA_ARGS__).get(), nullptr, \
+ __FILE__, __LINE__)
+
+/**
+ * Keep a list of frames sent to the audio engine in each DataCallback along
+ * with the playback rate at the moment. Since the playback rate and number of
+ * underrun frames can vary in each callback. We need to keep the whole history
+ * in order to calculate the playback position of the audio engine correctly.
+ */
+class FrameHistory {
+ struct Chunk {
+ uint32_t servicedFrames;
+ uint32_t totalFrames;
+ uint32_t rate;
+ };
+
+ template <typename T>
+ static T FramesToUs(uint32_t frames, uint32_t rate) {
+ return static_cast<T>(frames) * USECS_PER_S / rate;
+ }
+
+ public:
+ FrameHistory() : mBaseOffset(0), mBasePosition(0) {}
+
+ void Append(uint32_t aServiced, uint32_t aUnderrun, uint32_t aRate) {
+ /* In most case where playback rate stays the same and we don't underrun
+ * frames, we are able to merge chunks to avoid lose of precision to add up
+ * in compressing chunks into |mBaseOffset| and |mBasePosition|.
+ */
+ if (!mChunks.IsEmpty()) {
+ Chunk& c = mChunks.LastElement();
+ // 2 chunks (c1 and c2) can be merged when rate is the same and
+ // adjacent frames are zero. That is, underrun frames in c1 are zero
+ // or serviced frames in c2 are zero.
+ if (c.rate == aRate &&
+ (c.servicedFrames == c.totalFrames || aServiced == 0)) {
+ c.servicedFrames += aServiced;
+ c.totalFrames += aServiced + aUnderrun;
+ return;
+ }
+ }
+ Chunk* p = mChunks.AppendElement();
+ p->servicedFrames = aServiced;
+ p->totalFrames = aServiced + aUnderrun;
+ p->rate = aRate;
+ }
+
+ /**
+ * @param frames The playback position in frames of the audio engine.
+ * @return The playback position in microseconds of the audio engine,
+ * adjusted by playback rate changes and underrun frames.
+ */
+ int64_t GetPosition(int64_t frames) {
+ // playback position should not go backward.
+ MOZ_ASSERT(frames >= mBaseOffset);
+ while (true) {
+ if (mChunks.IsEmpty()) {
+ return static_cast<int64_t>(mBasePosition);
+ }
+ const Chunk& c = mChunks[0];
+ if (frames <= mBaseOffset + c.totalFrames) {
+ uint32_t delta = frames - mBaseOffset;
+ delta = std::min(delta, c.servicedFrames);
+ return static_cast<int64_t>(mBasePosition) +
+ FramesToUs<int64_t>(delta, c.rate);
+ }
+ // Since the playback position of the audio engine will not go backward,
+ // we are able to compress chunks so that |mChunks| won't grow
+ // unlimitedly. Note that we lose precision in converting integers into
+ // floats and inaccuracy will accumulate over time. However, for a 24hr
+ // long, sample rate = 44.1k file, the error will be less than 1
+ // microsecond after playing 24 hours. So we are fine with that.
+ mBaseOffset += c.totalFrames;
+ mBasePosition += FramesToUs<double>(c.servicedFrames, c.rate);
+ mChunks.RemoveElementAt(0);
+ }
+ }
+
+ private:
+ AutoTArray<Chunk, 7> mChunks;
+ int64_t mBaseOffset;
+ double mBasePosition;
+};
+
+AudioStream::AudioStream(DataSource& aSource, uint32_t aInRate,
+ uint32_t aOutputChannels,
+ AudioConfig::ChannelLayout::ChannelMap aChannelMap)
+ : mTimeStretcher(nullptr),
+ mAudioClock(aInRate),
+ mChannelMap(aChannelMap),
+ mMonitor("AudioStream"),
+ mOutChannels(aOutputChannels),
+ mState(INITIALIZED),
+ mDataSource(aSource),
+ mAudioThreadId(ProfilerThreadId{}),
+ mSandboxed(CubebUtils::SandboxEnabled()),
+ mPlaybackComplete(false),
+ mPlaybackRate(1.0f),
+ mPreservesPitch(true),
+ mCallbacksStarted(false) {}
+
+AudioStream::~AudioStream() {
+ LOG("deleted, state %d", mState.load());
+ MOZ_ASSERT(mState == SHUTDOWN && !mCubebStream,
+ "Should've called Shutdown() before deleting an AudioStream");
+}
+
+size_t AudioStream::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t amount = aMallocSizeOf(this);
+
+ // Possibly add in the future:
+ // - mTimeStretcher
+ // - mCubebStream
+
+ return amount;
+}
+
+nsresult AudioStream::EnsureTimeStretcherInitialized() {
+ AssertIsOnAudioThread();
+ if (!mTimeStretcher) {
+ mTimeStretcher = soundtouch::createSoundTouchObj();
+ mTimeStretcher->setSampleRate(mAudioClock.GetInputRate());
+ mTimeStretcher->setChannels(mOutChannels);
+ mTimeStretcher->setPitch(1.0);
+
+ // SoundTouch v2.1.2 uses automatic time-stretch settings with the following
+ // values:
+ // Tempo 0.5: 90ms sequence, 20ms seekwindow, 8ms overlap
+ // Tempo 2.0: 40ms sequence, 15ms seekwindow, 8ms overlap
+ // We are going to use a smaller 10ms sequence size to improve speech
+ // clarity, giving more resolution at high tempo and less reverb at low
+ // tempo. Maintain 15ms seekwindow and 8ms overlap for smoothness.
+ mTimeStretcher->setSetting(
+ SETTING_SEQUENCE_MS,
+ StaticPrefs::media_audio_playbackrate_soundtouch_sequence_ms());
+ mTimeStretcher->setSetting(
+ SETTING_SEEKWINDOW_MS,
+ StaticPrefs::media_audio_playbackrate_soundtouch_seekwindow_ms());
+ mTimeStretcher->setSetting(
+ SETTING_OVERLAP_MS,
+ StaticPrefs::media_audio_playbackrate_soundtouch_overlap_ms());
+ }
+ return NS_OK;
+}
+
+nsresult AudioStream::SetPlaybackRate(double aPlaybackRate) {
+ TRACE("AudioStream::SetPlaybackRate");
+ NS_ASSERTION(
+ aPlaybackRate > 0.0,
+ "Can't handle negative or null playbackrate in the AudioStream.");
+ if (aPlaybackRate == mPlaybackRate) {
+ return NS_OK;
+ }
+
+ mPlaybackRate = static_cast<float>(aPlaybackRate);
+
+ return NS_OK;
+}
+
+nsresult AudioStream::SetPreservesPitch(bool aPreservesPitch) {
+ TRACE("AudioStream::SetPreservesPitch");
+ if (aPreservesPitch == mPreservesPitch) {
+ return NS_OK;
+ }
+
+ mPreservesPitch = aPreservesPitch;
+
+ return NS_OK;
+}
+
+template <typename Function, typename... Args>
+int AudioStream::InvokeCubeb(Function aFunction, Args&&... aArgs) {
+ mMonitor.AssertCurrentThreadOwns();
+ MonitorAutoUnlock mon(mMonitor);
+ return aFunction(mCubebStream.get(), std::forward<Args>(aArgs)...);
+}
+
+nsresult AudioStream::Init(AudioDeviceInfo* aSinkInfo)
+ MOZ_NO_THREAD_SAFETY_ANALYSIS {
+ auto startTime = TimeStamp::Now();
+ TRACE("AudioStream::Init");
+
+ LOG("%s channels: %d, rate: %d", __FUNCTION__, mOutChannels,
+ mAudioClock.GetInputRate());
+
+ mSinkInfo = aSinkInfo;
+
+ cubeb_stream_params params;
+ params.rate = mAudioClock.GetInputRate();
+ params.channels = mOutChannels;
+ params.layout = static_cast<uint32_t>(mChannelMap);
+ params.format = CubebUtils::ToCubebFormat<AUDIO_OUTPUT_FORMAT>::value;
+ params.prefs = CubebUtils::GetDefaultStreamPrefs(CUBEB_DEVICE_TYPE_OUTPUT);
+
+ // This is noop if MOZ_DUMP_AUDIO is not set.
+ mDumpFile.Open("AudioStream", mOutChannels, mAudioClock.GetInputRate());
+
+ cubeb* cubebContext = CubebUtils::GetCubebContext();
+ if (!cubebContext) {
+ LOGE("Can't get cubeb context!");
+ CubebUtils::ReportCubebStreamInitFailure(true);
+ return NS_ERROR_DOM_MEDIA_CUBEB_INITIALIZATION_ERR;
+ }
+
+ return OpenCubeb(cubebContext, params, startTime,
+ CubebUtils::GetFirstStream());
+}
+
+nsresult AudioStream::OpenCubeb(cubeb* aContext, cubeb_stream_params& aParams,
+ TimeStamp aStartTime, bool aIsFirst) {
+ TRACE("AudioStream::OpenCubeb");
+ MOZ_ASSERT(aContext);
+
+ cubeb_stream* stream = nullptr;
+ /* Convert from milliseconds to frames. */
+ uint32_t latency_frames =
+ CubebUtils::GetCubebPlaybackLatencyInMilliseconds() * aParams.rate / 1000;
+ cubeb_devid deviceID = nullptr;
+ if (mSinkInfo && mSinkInfo->DeviceID()) {
+ deviceID = mSinkInfo->DeviceID();
+ }
+ if (CubebUtils::CubebStreamInit(aContext, &stream, "AudioStream", nullptr,
+ nullptr, deviceID, &aParams, latency_frames,
+ DataCallback_S, StateCallback_S,
+ this) == CUBEB_OK) {
+ mCubebStream.reset(stream);
+ CubebUtils::ReportCubebBackendUsed();
+ } else {
+ LOGE("OpenCubeb() failed to init cubeb");
+ CubebUtils::ReportCubebStreamInitFailure(aIsFirst);
+ return NS_ERROR_FAILURE;
+ }
+
+ TimeDuration timeDelta = TimeStamp::Now() - aStartTime;
+ LOG("creation time %sfirst: %u ms", aIsFirst ? "" : "not ",
+ (uint32_t)timeDelta.ToMilliseconds());
+
+ return NS_OK;
+}
+
+void AudioStream::SetVolume(double aVolume) {
+ TRACE("AudioStream::SetVolume");
+ MOZ_ASSERT(aVolume >= 0.0 && aVolume <= 1.0, "Invalid volume");
+
+ MOZ_ASSERT(mState != SHUTDOWN, "Don't set volume after shutdown.");
+ if (mState == ERRORED) {
+ return;
+ }
+
+ MonitorAutoLock mon(mMonitor);
+ if (InvokeCubeb(cubeb_stream_set_volume,
+ aVolume * CubebUtils::GetVolumeScale()) != CUBEB_OK) {
+ LOGE("Could not change volume on cubeb stream.");
+ }
+}
+
+void AudioStream::SetStreamName(const nsAString& aStreamName) {
+ TRACE("AudioStream::SetStreamName");
+
+ nsAutoCString aRawStreamName;
+ nsresult rv = NS_CopyUnicodeToNative(aStreamName, aRawStreamName);
+
+ if (NS_FAILED(rv) || aStreamName.IsEmpty()) {
+ return;
+ }
+
+ MonitorAutoLock mon(mMonitor);
+ if (InvokeCubeb(cubeb_stream_set_name, aRawStreamName.get()) != CUBEB_OK) {
+ LOGE("Could not set cubeb stream name.");
+ }
+}
+
+nsresult AudioStream::Start(
+ MozPromiseHolder<MediaSink::EndedPromise>& aEndedPromise) {
+ TRACE("AudioStream::Start");
+ MOZ_ASSERT(mState == INITIALIZED);
+ mState = STARTED;
+ RefPtr<MediaSink::EndedPromise> promise;
+ {
+ MonitorAutoLock mon(mMonitor);
+ // As cubeb might call audio stream's state callback very soon after we
+ // start cubeb, we have to create the promise beforehand in order to handle
+ // the case where we immediately get `drained`.
+ mEndedPromise = std::move(aEndedPromise);
+ mPlaybackComplete = false;
+
+ if (InvokeCubeb(cubeb_stream_start) != CUBEB_OK) {
+ mState = ERRORED;
+ }
+ }
+
+ LOG("started, state %s", mState == STARTED ? "STARTED"
+ : mState == DRAINED ? "DRAINED"
+ : "ERRORED");
+ if (mState == STARTED || mState == DRAINED) {
+ return NS_OK;
+ }
+ return NS_ERROR_FAILURE;
+}
+
+void AudioStream::Pause() {
+ TRACE("AudioStream::Pause");
+ MOZ_ASSERT(mState != INITIALIZED, "Must be Start()ed.");
+ MOZ_ASSERT(mState != STOPPED, "Already Pause()ed.");
+ MOZ_ASSERT(mState != SHUTDOWN, "Already Shutdown()ed.");
+
+ // Do nothing if we are already drained or errored.
+ if (mState == DRAINED || mState == ERRORED) {
+ return;
+ }
+
+ MonitorAutoLock mon(mMonitor);
+ if (InvokeCubeb(cubeb_stream_stop) != CUBEB_OK) {
+ mState = ERRORED;
+ } else if (mState != DRAINED && mState != ERRORED) {
+ // Don't transition to other states if we are already
+ // drained or errored.
+ mState = STOPPED;
+ }
+}
+
+void AudioStream::Resume() {
+ TRACE("AudioStream::Resume");
+ MOZ_ASSERT(mState != INITIALIZED, "Must be Start()ed.");
+ MOZ_ASSERT(mState != STARTED, "Already Start()ed.");
+ MOZ_ASSERT(mState != SHUTDOWN, "Already Shutdown()ed.");
+
+ // Do nothing if we are already drained or errored.
+ if (mState == DRAINED || mState == ERRORED) {
+ return;
+ }
+
+ MonitorAutoLock mon(mMonitor);
+ if (InvokeCubeb(cubeb_stream_start) != CUBEB_OK) {
+ mState = ERRORED;
+ } else if (mState != DRAINED && mState != ERRORED) {
+ // Don't transition to other states if we are already
+ // drained or errored.
+ mState = STARTED;
+ }
+}
+
+Maybe<MozPromiseHolder<MediaSink::EndedPromise>> AudioStream::Shutdown(
+ ShutdownCause aCause) {
+ TRACE("AudioStream::Shutdown");
+ LOG("Shutdown, state %d", mState.load());
+
+ MonitorAutoLock mon(mMonitor);
+ if (mCubebStream) {
+ // Force stop to put the cubeb stream in a stable state before deletion.
+ InvokeCubeb(cubeb_stream_stop);
+ // Must not try to shut down cubeb from within the lock! wasapi may still
+ // call our callback after Pause()/stop()!?! Bug 996162
+ cubeb_stream* cubeb = mCubebStream.release();
+ MonitorAutoUnlock unlock(mMonitor);
+ cubeb_stream_destroy(cubeb);
+ }
+
+ // After `cubeb_stream_stop` has been called, there is no audio thread
+ // anymore. We can delete the time stretcher.
+ if (mTimeStretcher) {
+ soundtouch::destroySoundTouchObj(mTimeStretcher);
+ mTimeStretcher = nullptr;
+ }
+
+ mState = SHUTDOWN;
+ // When shutting down, if this AudioStream is shutting down because the
+ // HTMLMediaElement is now muted, hand back the ended promise, so that it can
+ // properly be resolved if the end of the media is reached while muted (i.e.
+ // without having an AudioStream)
+ if (aCause != ShutdownCause::Muting) {
+ mEndedPromise.ResolveIfExists(true, __func__);
+ return Nothing();
+ }
+ return Some(std::move(mEndedPromise));
+}
+
+int64_t AudioStream::GetPosition() {
+ TRACE("AudioStream::GetPosition");
+#ifndef XP_MACOSX
+ MonitorAutoLock mon(mMonitor);
+#endif
+ int64_t frames = GetPositionInFramesUnlocked();
+ return frames >= 0 ? mAudioClock.GetPosition(frames) : -1;
+}
+
+int64_t AudioStream::GetPositionInFrames() {
+ TRACE("AudioStream::GetPositionInFrames");
+#ifndef XP_MACOSX
+ MonitorAutoLock mon(mMonitor);
+#endif
+ int64_t frames = GetPositionInFramesUnlocked();
+
+ return frames >= 0 ? mAudioClock.GetPositionInFrames(frames) : -1;
+}
+
+int64_t AudioStream::GetPositionInFramesUnlocked() {
+ TRACE("AudioStream::GetPositionInFramesUnlocked");
+#ifndef XP_MACOSX
+ mMonitor.AssertCurrentThreadOwns();
+#endif
+
+ if (mState == ERRORED) {
+ return -1;
+ }
+
+ uint64_t position = 0;
+ int rv;
+
+#ifndef XP_MACOSX
+ rv = InvokeCubeb(cubeb_stream_get_position, &position);
+#else
+ rv = cubeb_stream_get_position(mCubebStream.get(), &position);
+#endif
+
+ if (rv != CUBEB_OK) {
+ return -1;
+ }
+ return static_cast<int64_t>(std::min<uint64_t>(position, INT64_MAX));
+}
+
+bool AudioStream::IsValidAudioFormat(Chunk* aChunk) {
+ if (aChunk->Rate() != mAudioClock.GetInputRate()) {
+ LOGW("mismatched sample %u, mInRate=%u", aChunk->Rate(),
+ mAudioClock.GetInputRate());
+ return false;
+ }
+
+ return aChunk->Channels() <= 8;
+}
+
+void AudioStream::GetUnprocessed(AudioBufferWriter& aWriter) {
+ TRACE("AudioStream::GetUnprocessed");
+ AssertIsOnAudioThread();
+ // Flush the timestretcher pipeline, if we were playing using a playback rate
+ // other than 1.0.
+ if (mTimeStretcher && mTimeStretcher->numSamples()) {
+ auto* timeStretcher = mTimeStretcher;
+ aWriter.Write(
+ [timeStretcher](AudioDataValue* aPtr, uint32_t aFrames) {
+ return timeStretcher->receiveSamples(aPtr, aFrames);
+ },
+ aWriter.Available());
+
+ // TODO: There might be still unprocessed samples in the stretcher.
+ // We should either remove or flush them so they won't be in the output
+ // next time we switch a playback rate other than 1.0.
+ NS_WARNING_ASSERTION(mTimeStretcher->numUnprocessedSamples() == 0,
+ "no samples");
+ } else if (mTimeStretcher) {
+ // Don't need it anymore: playbackRate is 1.0, and the time stretcher has
+ // been flushed.
+ soundtouch::destroySoundTouchObj(mTimeStretcher);
+ mTimeStretcher = nullptr;
+ }
+
+ while (aWriter.Available() > 0) {
+ uint32_t count = mDataSource.PopFrames(aWriter.Ptr(), aWriter.Available(),
+ mAudioThreadChanged);
+ if (count == 0) {
+ break;
+ }
+ aWriter.Advance(count);
+ }
+}
+
+void AudioStream::GetTimeStretched(AudioBufferWriter& aWriter) {
+ TRACE("AudioStream::GetTimeStretched");
+ AssertIsOnAudioThread();
+ if (EnsureTimeStretcherInitialized() != NS_OK) {
+ return;
+ }
+
+ uint32_t toPopFrames =
+ ceil(aWriter.Available() * mAudioClock.GetPlaybackRate());
+
+ while (mTimeStretcher->numSamples() < aWriter.Available()) {
+ // pop into a temp buffer, and put into the stretcher.
+ AutoTArray<AudioDataValue, 1000> buf;
+ auto size = CheckedUint32(mOutChannels) * toPopFrames;
+ if (!size.isValid()) {
+ // The overflow should not happen in normal case.
+ LOGW("Invalid member data: %d channels, %d frames", mOutChannels,
+ toPopFrames);
+ return;
+ }
+ buf.SetLength(size.value());
+ // ensure no variable channel count or something like that
+ uint32_t count =
+ mDataSource.PopFrames(buf.Elements(), toPopFrames, mAudioThreadChanged);
+ if (count == 0) {
+ break;
+ }
+ mTimeStretcher->putSamples(buf.Elements(), count);
+ }
+
+ auto* timeStretcher = mTimeStretcher;
+ aWriter.Write(
+ [timeStretcher](AudioDataValue* aPtr, uint32_t aFrames) {
+ return timeStretcher->receiveSamples(aPtr, aFrames);
+ },
+ aWriter.Available());
+}
+
+bool AudioStream::CheckThreadIdChanged() {
+ ProfilerThreadId id = profiler_current_thread_id();
+ if (id != mAudioThreadId) {
+ mAudioThreadId = id;
+ mAudioThreadChanged = true;
+ return true;
+ }
+ mAudioThreadChanged = false;
+ return false;
+}
+
+void AudioStream::AssertIsOnAudioThread() const {
+ // This can be called right after CheckThreadIdChanged, because the audio
+ // thread can change when not sandboxed.
+ MOZ_ASSERT(mAudioThreadId.load() == profiler_current_thread_id());
+}
+
+void AudioStream::UpdatePlaybackRateIfNeeded() {
+ AssertIsOnAudioThread();
+ if (mAudioClock.GetPreservesPitch() == mPreservesPitch &&
+ mAudioClock.GetPlaybackRate() == mPlaybackRate) {
+ return;
+ }
+
+ EnsureTimeStretcherInitialized();
+
+ mAudioClock.SetPlaybackRate(mPlaybackRate);
+ mAudioClock.SetPreservesPitch(mPreservesPitch);
+
+ if (mPreservesPitch) {
+ mTimeStretcher->setTempo(mPlaybackRate);
+ mTimeStretcher->setRate(1.0f);
+ } else {
+ mTimeStretcher->setTempo(1.0f);
+ mTimeStretcher->setRate(mPlaybackRate);
+ }
+}
+
+long AudioStream::DataCallback(void* aBuffer, long aFrames) {
+ if (CheckThreadIdChanged() && !mSandboxed) {
+ CallbackThreadRegistry::Get()->Register(mAudioThreadId,
+ "NativeAudioCallback");
+ }
+ WebCore::DenormalDisabler disabler;
+ if (!mCallbacksStarted) {
+ mCallbacksStarted = true;
+ }
+
+ TRACE_AUDIO_CALLBACK_BUDGET(aFrames, mAudioClock.GetInputRate());
+ TRACE("AudioStream::DataCallback");
+ MOZ_ASSERT(mState != SHUTDOWN, "No data callback after shutdown");
+
+ if (SoftRealTimeLimitReached()) {
+ DemoteThreadFromRealTime();
+ }
+
+ UpdatePlaybackRateIfNeeded();
+
+ auto writer = AudioBufferWriter(
+ Span<AudioDataValue>(reinterpret_cast<AudioDataValue*>(aBuffer),
+ mOutChannels * aFrames),
+ mOutChannels, aFrames);
+
+ if (mAudioClock.GetInputRate() == mAudioClock.GetOutputRate()) {
+ GetUnprocessed(writer);
+ } else {
+ GetTimeStretched(writer);
+ }
+
+ // Always send audible frames first, and silent frames later.
+ // Otherwise it will break the assumption of FrameHistory.
+ if (!mDataSource.Ended()) {
+#ifndef XP_MACOSX
+ MonitorAutoLock mon(mMonitor);
+#endif
+ mAudioClock.UpdateFrameHistory(aFrames - writer.Available(),
+ writer.Available(), mAudioThreadChanged);
+ if (writer.Available() > 0) {
+ TRACE_COMMENT("AudioStream::DataCallback", "Underrun: %d frames missing",
+ writer.Available());
+ LOGW("lost %d frames", writer.Available());
+ writer.WriteZeros(writer.Available());
+ }
+ } else {
+ // No more new data in the data source, and the drain has completed. We
+ // don't need the time stretcher anymore at this point.
+ if (mTimeStretcher && writer.Available()) {
+ soundtouch::destroySoundTouchObj(mTimeStretcher);
+ mTimeStretcher = nullptr;
+ }
+#ifndef XP_MACOSX
+ MonitorAutoLock mon(mMonitor);
+#endif
+ mAudioClock.UpdateFrameHistory(aFrames - writer.Available(), 0,
+ mAudioThreadChanged);
+ }
+
+ mDumpFile.Write(static_cast<const AudioDataValue*>(aBuffer),
+ aFrames * mOutChannels);
+
+ if (!mSandboxed && writer.Available() != 0) {
+ CallbackThreadRegistry::Get()->Unregister(mAudioThreadId);
+ }
+ return aFrames - writer.Available();
+}
+
+void AudioStream::StateCallback(cubeb_state aState) {
+ MOZ_ASSERT(mState != SHUTDOWN, "No state callback after shutdown");
+ LOG("StateCallback, mState=%d cubeb_state=%d", mState.load(), aState);
+
+ MonitorAutoLock mon(mMonitor);
+ if (aState == CUBEB_STATE_DRAINED) {
+ LOG("Drained");
+ mState = DRAINED;
+ mPlaybackComplete = true;
+ mEndedPromise.ResolveIfExists(true, __func__);
+ } else if (aState == CUBEB_STATE_ERROR) {
+ LOGE("StateCallback() state %d cubeb error", mState.load());
+ mState = ERRORED;
+ mPlaybackComplete = true;
+ mEndedPromise.RejectIfExists(NS_ERROR_FAILURE, __func__);
+ }
+}
+
+bool AudioStream::IsPlaybackCompleted() const { return mPlaybackComplete; }
+
+AudioClock::AudioClock(uint32_t aInRate)
+ : mOutRate(aInRate),
+ mInRate(aInRate),
+ mPreservesPitch(true),
+ mFrameHistory(new FrameHistory()) {}
+
+// Audio thread only
+void AudioClock::UpdateFrameHistory(uint32_t aServiced, uint32_t aUnderrun,
+ bool aAudioThreadChanged) {
+#ifdef XP_MACOSX
+ if (aAudioThreadChanged) {
+ mCallbackInfoQueue.ResetThreadIds();
+ }
+ // Flush the local items, if any, and then attempt to enqueue the current
+ // item. This is only a fallback mechanism, under non-critical load this is
+ // just going to enqueue an item in the queue.
+ while (!mAudioThreadCallbackInfo.IsEmpty()) {
+ CallbackInfo& info = mAudioThreadCallbackInfo[0];
+ // If still full, keep it audio-thread side for now.
+ if (mCallbackInfoQueue.Enqueue(info) != 1) {
+ break;
+ }
+ mAudioThreadCallbackInfo.RemoveElementAt(0);
+ }
+ CallbackInfo info(aServiced, aUnderrun, mOutRate);
+ if (mCallbackInfoQueue.Enqueue(info) != 1) {
+ NS_WARNING(
+ "mCallbackInfoQueue full, storing the values in the audio thread.");
+ mAudioThreadCallbackInfo.AppendElement(info);
+ }
+#else
+ MutexAutoLock lock(mMutex);
+ mFrameHistory->Append(aServiced, aUnderrun, mOutRate);
+#endif
+}
+
+int64_t AudioClock::GetPositionInFrames(int64_t aFrames) {
+ CheckedInt64 v = UsecsToFrames(GetPosition(aFrames), mInRate);
+ return v.isValid() ? v.value() : -1;
+}
+
+int64_t AudioClock::GetPosition(int64_t frames) {
+#ifdef XP_MACOSX
+ // Dequeue all history info, and apply them before returning the position
+ // based on frame history.
+ CallbackInfo info;
+ while (mCallbackInfoQueue.Dequeue(&info, 1)) {
+ mFrameHistory->Append(info.mServiced, info.mUnderrun, info.mOutputRate);
+ }
+#else
+ MutexAutoLock lock(mMutex);
+#endif
+ return mFrameHistory->GetPosition(frames);
+}
+
+void AudioClock::SetPlaybackRate(double aPlaybackRate) {
+ mOutRate = static_cast<uint32_t>(mInRate / aPlaybackRate);
+}
+
+double AudioClock::GetPlaybackRate() const {
+ return static_cast<double>(mInRate) / mOutRate;
+}
+
+void AudioClock::SetPreservesPitch(bool aPreservesPitch) {
+ mPreservesPitch = aPreservesPitch;
+}
+
+bool AudioClock::GetPreservesPitch() const { return mPreservesPitch; }
+
+} // namespace mozilla
diff --git a/dom/media/AudioStream.h b/dom/media/AudioStream.h
new file mode 100644
index 0000000000..84984452e7
--- /dev/null
+++ b/dom/media/AudioStream.h
@@ -0,0 +1,394 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#if !defined(AudioStream_h_)
+# define AudioStream_h_
+
+# include "AudioSampleFormat.h"
+# include "CubebUtils.h"
+# include "MediaInfo.h"
+# include "MediaSink.h"
+# include "mozilla/Atomics.h"
+# include "mozilla/Monitor.h"
+# include "mozilla/MozPromise.h"
+# include "mozilla/ProfilerUtils.h"
+# include "mozilla/RefPtr.h"
+# include "mozilla/Result.h"
+# include "mozilla/TimeStamp.h"
+# include "mozilla/UniquePtr.h"
+# include "mozilla/SPSCQueue.h"
+# include "nsCOMPtr.h"
+# include "nsThreadUtils.h"
+# include "WavDumper.h"
+
+namespace soundtouch {
+class MOZ_EXPORT SoundTouch;
+}
+
+namespace mozilla {
+
+struct CubebDestroyPolicy {
+ void operator()(cubeb_stream* aStream) const {
+ cubeb_stream_destroy(aStream);
+ }
+};
+
+enum class ShutdownCause {
+ // Regular shutdown, signal the end of the audio stream.
+ Regular,
+ // Shutdown for muting, don't signal the end of the audio stream.
+ Muting
+};
+
+class AudioStream;
+class FrameHistory;
+class AudioConfig;
+
+// A struct that contains the number of frames serviced or underrun by a
+// callback, alongside the sample-rate for this callback (in case of playback
+// rate change, it can be variable).
+struct CallbackInfo {
+ CallbackInfo() = default;
+ CallbackInfo(uint32_t aServiced, uint32_t aUnderrun, uint32_t aOutputRate)
+ : mServiced(aServiced), mUnderrun(aUnderrun), mOutputRate(aOutputRate) {}
+ uint32_t mServiced = 0;
+ uint32_t mUnderrun = 0;
+ uint32_t mOutputRate = 0;
+};
+
+class AudioClock {
+ public:
+ explicit AudioClock(uint32_t aInRate);
+
+ // Update the number of samples that has been written in the audio backend.
+ // Called on the audio thread only.
+ void UpdateFrameHistory(uint32_t aServiced, uint32_t aUnderrun,
+ bool aAudioThreadChanged);
+
+ /**
+ * @param aFrames The playback position in frames of the audio engine.
+ * @return The playback position in frames of the stream,
+ * adjusted by playback rate changes and underrun frames.
+ */
+ int64_t GetPositionInFrames(int64_t aFrames);
+
+ /**
+ * @param frames The playback position in frames of the audio engine.
+ * @return The playback position in microseconds of the stream,
+ * adjusted by playback rate changes and underrun frames.
+ */
+ int64_t GetPosition(int64_t frames);
+
+ // Set the playback rate.
+ // Called on the audio thread only.
+ void SetPlaybackRate(double aPlaybackRate);
+ // Get the current playback rate.
+ // Called on the audio thread only.
+ double GetPlaybackRate() const;
+ // Set if we are preserving the pitch.
+ // Called on the audio thread only.
+ void SetPreservesPitch(bool aPreservesPitch);
+ // Get the current pitch preservation state.
+ // Called on the audio thread only.
+ bool GetPreservesPitch() const;
+
+ // Called on either thread.
+ uint32_t GetInputRate() const { return mInRate; }
+ uint32_t GetOutputRate() const { return mOutRate; }
+
+ private:
+ // Output rate in Hz (characteristic of the playback rate). Written on the
+ // audio thread, read on either thread.
+ Atomic<uint32_t> mOutRate;
+ // Input rate in Hz (characteristic of the media being played).
+ const uint32_t mInRate;
+ // True if the we are timestretching, false if we are resampling. Accessed on
+ // the audio thread only.
+ bool mPreservesPitch;
+ // The history of frames sent to the audio engine in each DataCallback.
+ // Only accessed from non-audio threads on macOS, accessed on both threads and
+ // protected by the AudioStream monitor on other platforms.
+ const UniquePtr<FrameHistory> mFrameHistory
+# ifndef XP_MACOSX
+ MOZ_GUARDED_BY(mMutex)
+# endif
+ ;
+# ifdef XP_MACOSX
+ // Enqueued on the audio thread, dequeued from the other thread. The maximum
+ // size of this queue has been chosen empirically.
+ SPSCQueue<CallbackInfo> mCallbackInfoQueue{100};
+ // If it isn't possible to send the callback info to the non-audio thread,
+ // store them here until it's possible to send them. This is an unlikely
+ // fallback path. The size of this array has been chosen empirically. Only
+ // ever accessed on the audio thread.
+ AutoTArray<CallbackInfo, 5> mAudioThreadCallbackInfo;
+# else
+ Mutex mMutex{"AudioClock"};
+# endif
+};
+
+/*
+ * A bookkeeping class to track the read/write position of an audio buffer.
+ */
+class AudioBufferCursor {
+ public:
+ AudioBufferCursor(Span<AudioDataValue> aSpan, uint32_t aChannels,
+ uint32_t aFrames)
+ : mChannels(aChannels), mSpan(aSpan), mFrames(aFrames) {}
+
+ // Advance the cursor to account for frames that are consumed.
+ uint32_t Advance(uint32_t aFrames) {
+ MOZ_DIAGNOSTIC_ASSERT(Contains(aFrames));
+ MOZ_ASSERT(mFrames >= aFrames);
+ mFrames -= aFrames;
+ mOffset += mChannels * aFrames;
+ return aFrames;
+ }
+
+ // The number of frames available for read/write in this buffer.
+ uint32_t Available() const { return mFrames; }
+
+ // Return a pointer where read/write should begin.
+ AudioDataValue* Ptr() const {
+ MOZ_DIAGNOSTIC_ASSERT(mOffset <= mSpan.Length());
+ return mSpan.Elements() + mOffset;
+ }
+
+ protected:
+ bool Contains(uint32_t aFrames) const {
+ return mSpan.Length() >= mOffset + mChannels * aFrames;
+ }
+ const uint32_t mChannels;
+
+ private:
+ const Span<AudioDataValue> mSpan;
+ size_t mOffset = 0;
+ uint32_t mFrames;
+};
+
+/*
+ * A helper class to encapsulate pointer arithmetic and provide means to modify
+ * the underlying audio buffer.
+ */
+class AudioBufferWriter : public AudioBufferCursor {
+ public:
+ AudioBufferWriter(Span<AudioDataValue> aSpan, uint32_t aChannels,
+ uint32_t aFrames)
+ : AudioBufferCursor(aSpan, aChannels, aFrames) {}
+
+ uint32_t WriteZeros(uint32_t aFrames) {
+ MOZ_DIAGNOSTIC_ASSERT(Contains(aFrames));
+ memset(Ptr(), 0, sizeof(AudioDataValue) * mChannels * aFrames);
+ return Advance(aFrames);
+ }
+
+ uint32_t Write(const AudioDataValue* aPtr, uint32_t aFrames) {
+ MOZ_DIAGNOSTIC_ASSERT(Contains(aFrames));
+ memcpy(Ptr(), aPtr, sizeof(AudioDataValue) * mChannels * aFrames);
+ return Advance(aFrames);
+ }
+
+ // Provide a write fuction to update the audio buffer with the following
+ // signature: uint32_t(const AudioDataValue* aPtr, uint32_t aFrames)
+ // aPtr: Pointer to the audio buffer.
+ // aFrames: The number of frames available in the buffer.
+ // return: The number of frames actually written by the function.
+ template <typename Function>
+ uint32_t Write(const Function& aFunction, uint32_t aFrames) {
+ MOZ_DIAGNOSTIC_ASSERT(Contains(aFrames));
+ return Advance(aFunction(Ptr(), aFrames));
+ }
+
+ using AudioBufferCursor::Available;
+};
+
+// Access to a single instance of this class must be synchronized by
+// callers, or made from a single thread. One exception is that access to
+// GetPosition, GetPositionInFrames, SetVolume, and Get{Rate,Channels},
+// SetMicrophoneActive is thread-safe without external synchronization.
+class AudioStream final {
+ virtual ~AudioStream();
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AudioStream)
+
+ class Chunk {
+ public:
+ // Return a pointer to the audio data.
+ virtual const AudioDataValue* Data() const = 0;
+ // Return the number of frames in this chunk.
+ virtual uint32_t Frames() const = 0;
+ // Return the number of audio channels.
+ virtual uint32_t Channels() const = 0;
+ // Return the sample rate of this chunk.
+ virtual uint32_t Rate() const = 0;
+ // Return a writable pointer for downmixing.
+ virtual AudioDataValue* GetWritable() const = 0;
+ virtual ~Chunk() = default;
+ };
+
+ class DataSource {
+ public:
+ // Attempt to acquire aFrames frames of audio, and returns the number of
+ // frames successfuly acquired.
+ virtual uint32_t PopFrames(AudioDataValue* aAudio, uint32_t aFrames,
+ bool aAudioThreadChanged) = 0;
+ // Return true if no more data will be added to the source.
+ virtual bool Ended() const = 0;
+
+ protected:
+ virtual ~DataSource() = default;
+ };
+
+ // aOutputChannels is the number of audio channels (1 for mono, 2 for stereo,
+ // etc), aChannelMap is the indicator for channel layout(mono, stereo, 5.1 or
+ // 7.1 ). Initialize the audio stream.and aRate is the sample rate
+ // (22050Hz, 44100Hz, etc).
+ AudioStream(DataSource& aSource, uint32_t aInRate, uint32_t aOutputChannels,
+ AudioConfig::ChannelLayout::ChannelMap aChannelMap);
+
+ nsresult Init(AudioDeviceInfo* aSinkInfo);
+
+ // Closes the stream. All future use of the stream is an error.
+ Maybe<MozPromiseHolder<MediaSink::EndedPromise>> Shutdown(
+ ShutdownCause = ShutdownCause::Regular);
+
+ void Reset();
+
+ // Set the current volume of the audio playback. This is a value from
+ // 0 (meaning muted) to 1 (meaning full volume). Thread-safe.
+ void SetVolume(double aVolume);
+
+ void SetStreamName(const nsAString& aStreamName);
+
+ // Start the stream.
+ nsresult Start(MozPromiseHolder<MediaSink::EndedPromise>& aEndedPromise);
+
+ // Pause audio playback.
+ void Pause();
+
+ // Resume audio playback.
+ void Resume();
+
+ // Return the position in microseconds of the audio frame being played by
+ // the audio hardware, compensated for playback rate change. Thread-safe.
+ int64_t GetPosition();
+
+ // Return the position, measured in audio frames played since the stream
+ // was opened, of the audio hardware. Thread-safe.
+ int64_t GetPositionInFrames();
+
+ uint32_t GetOutChannels() const { return mOutChannels; }
+
+ // Set playback rate as a multiple of the intrinsic playback rate. This is
+ // to be called only with aPlaybackRate > 0.0.
+ nsresult SetPlaybackRate(double aPlaybackRate);
+ // Switch between resampling (if false) and time stretching (if true,
+ // default).
+ nsresult SetPreservesPitch(bool aPreservesPitch);
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const;
+
+ bool IsPlaybackCompleted() const;
+
+ // Returns true if at least one DataCallback has been called.
+ bool CallbackStarted() const { return mCallbacksStarted; }
+
+ protected:
+ friend class AudioClock;
+
+ // Return the position, measured in audio frames played since the stream was
+ // opened, of the audio hardware, not adjusted for the changes of playback
+ // rate or underrun frames.
+ // Caller must own the monitor.
+ int64_t GetPositionInFramesUnlocked();
+
+ private:
+ nsresult OpenCubeb(cubeb* aContext, cubeb_stream_params& aParams,
+ TimeStamp aStartTime, bool aIsFirst);
+
+ static long DataCallback_S(cubeb_stream*, void* aThis,
+ const void* /* aInputBuffer */,
+ void* aOutputBuffer, long aFrames) {
+ return static_cast<AudioStream*>(aThis)->DataCallback(aOutputBuffer,
+ aFrames);
+ }
+
+ static void StateCallback_S(cubeb_stream*, void* aThis, cubeb_state aState) {
+ static_cast<AudioStream*>(aThis)->StateCallback(aState);
+ }
+
+ long DataCallback(void* aBuffer, long aFrames);
+ void StateCallback(cubeb_state aState);
+
+ // Audio thread only
+ nsresult EnsureTimeStretcherInitialized();
+ void GetUnprocessed(AudioBufferWriter& aWriter);
+ void GetTimeStretched(AudioBufferWriter& aWriter);
+ void UpdatePlaybackRateIfNeeded();
+
+ // Return true if audio frames are valid (correct sampling rate and valid
+ // channel count) otherwise false.
+ bool IsValidAudioFormat(Chunk* aChunk) MOZ_REQUIRES(mMonitor);
+
+ template <typename Function, typename... Args>
+ int InvokeCubeb(Function aFunction, Args&&... aArgs) MOZ_REQUIRES(mMonitor);
+ bool CheckThreadIdChanged();
+ void AssertIsOnAudioThread() const;
+
+ soundtouch::SoundTouch* mTimeStretcher;
+
+ AudioClock mAudioClock;
+
+ WavDumper mDumpFile;
+
+ const AudioConfig::ChannelLayout::ChannelMap mChannelMap;
+
+ // The monitor is held to protect all access to member variables below.
+ Monitor mMonitor MOZ_UNANNOTATED;
+
+ const uint32_t mOutChannels;
+
+ // Owning reference to a cubeb_stream. Set in Init(), cleared in Shutdown, so
+ // no lock is needed to access.
+ UniquePtr<cubeb_stream, CubebDestroyPolicy> mCubebStream;
+
+ enum StreamState {
+ INITIALIZED, // Initialized, playback has not begun.
+ STARTED, // cubeb started.
+ STOPPED, // Stopped by a call to Pause().
+ DRAINED, // StateCallback has indicated that the drain is complete.
+ ERRORED, // Stream disabled due to an internal error.
+ SHUTDOWN // Shutdown has been called
+ };
+
+ std::atomic<StreamState> mState;
+
+ // DataSource::PopFrames can never be called concurrently.
+ // DataSource::IsEnded uses only atomics.
+ DataSource& mDataSource;
+
+ // The device info of the current sink. If null
+ // the default device is used. It is set
+ // during the Init() in decoder thread.
+ RefPtr<AudioDeviceInfo> mSinkInfo;
+ // Contains the id of the audio thread, from profiler_get_thread_id.
+ std::atomic<ProfilerThreadId> mAudioThreadId;
+ const bool mSandboxed = false;
+
+ MozPromiseHolder<MediaSink::EndedPromise> mEndedPromise
+ MOZ_GUARDED_BY(mMonitor);
+ std::atomic<bool> mPlaybackComplete;
+ // Both written on the MDSM thread, read on the audio thread.
+ std::atomic<float> mPlaybackRate;
+ std::atomic<bool> mPreservesPitch;
+ // Audio thread only
+ bool mAudioThreadChanged = false;
+ Atomic<bool> mCallbacksStarted;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/AudioStreamTrack.cpp b/dom/media/AudioStreamTrack.cpp
new file mode 100644
index 0000000000..6ea4938033
--- /dev/null
+++ b/dom/media/AudioStreamTrack.cpp
@@ -0,0 +1,87 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioStreamTrack.h"
+
+#include "MediaTrackGraph.h"
+#include "nsContentUtils.h"
+
+namespace mozilla::dom {
+
+RefPtr<GenericPromise> AudioStreamTrack::AddAudioOutput(
+ void* aKey, AudioDeviceInfo* aSink) {
+ MOZ_ASSERT(!mCrossGraphs.Get(aKey),
+ "A previous audio output for this aKey should have been removed");
+
+ if (Ended()) {
+ return GenericPromise::CreateAndResolve(true, __func__);
+ }
+
+ UniquePtr<CrossGraphPort> manager;
+ if (!aSink || !(manager = CrossGraphPort::Connect(this, aSink, mWindow))) {
+ // We are setting the default output device.
+ mTrack->AddAudioOutput(aKey);
+ return GenericPromise::CreateAndResolve(true, __func__);
+ }
+
+ // We are setting a non-default output device.
+ const UniquePtr<CrossGraphPort>& crossGraph = mCrossGraphs.WithEntryHandle(
+ aKey, [&manager](auto entry) -> UniquePtr<CrossGraphPort>& {
+ return entry.Insert(std::move(manager));
+ });
+ crossGraph->AddAudioOutput(aKey);
+ return crossGraph->EnsureConnected();
+}
+
+void AudioStreamTrack::RemoveAudioOutput(void* aKey) {
+ if (Ended()) {
+ return;
+ }
+ if (auto entry = mCrossGraphs.Lookup(aKey)) {
+ // The audio output for this track is directed to a non-default device.
+ // The CrossGraphPort for this output is no longer required so remove it.
+ entry.Remove();
+ return;
+ }
+ mTrack->RemoveAudioOutput(aKey);
+}
+
+void AudioStreamTrack::SetAudioOutputVolume(void* aKey, float aVolume) {
+ if (Ended()) {
+ return;
+ }
+ if (CrossGraphPort* cgm = mCrossGraphs.Get(aKey)) {
+ cgm->SetAudioOutputVolume(aKey, aVolume);
+ return;
+ }
+ mTrack->SetAudioOutputVolume(aKey, aVolume);
+}
+
+void AudioStreamTrack::GetLabel(nsAString& aLabel, CallerType aCallerType) {
+ nsIGlobalObject* global =
+ GetParentObject() ? GetParentObject()->AsGlobal() : nullptr;
+ if (nsContentUtils::ShouldResistFingerprinting(aCallerType, global,
+ RFPTarget::StreamTrackLabel)) {
+ aLabel.AssignLiteral("Internal Microphone");
+ return;
+ }
+ MediaStreamTrack::GetLabel(aLabel, aCallerType);
+}
+
+already_AddRefed<MediaStreamTrack> AudioStreamTrack::CloneInternal() {
+ return do_AddRef(new AudioStreamTrack(mWindow, mInputTrack, mSource,
+ ReadyState(), Muted(), mConstraints));
+}
+
+void AudioStreamTrack::SetReadyState(MediaStreamTrackState aState) {
+ if (!mCrossGraphs.IsEmpty() && !Ended() &&
+ mReadyState == MediaStreamTrackState::Live &&
+ aState == MediaStreamTrackState::Ended) {
+ mCrossGraphs.Clear();
+ }
+ MediaStreamTrack::SetReadyState(aState);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/AudioStreamTrack.h b/dom/media/AudioStreamTrack.h
new file mode 100644
index 0000000000..2bd8b18108
--- /dev/null
+++ b/dom/media/AudioStreamTrack.h
@@ -0,0 +1,56 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef AUDIOSTREAMTRACK_H_
+#define AUDIOSTREAMTRACK_H_
+
+#include "MediaStreamTrack.h"
+#include "DOMMediaStream.h"
+#include "CrossGraphPort.h"
+#include "nsClassHashtable.h"
+
+namespace mozilla::dom {
+
+class AudioStreamTrack : public MediaStreamTrack {
+ public:
+ AudioStreamTrack(
+ nsPIDOMWindowInner* aWindow, mozilla::MediaTrack* aInputTrack,
+ MediaStreamTrackSource* aSource,
+ MediaStreamTrackState aReadyState = MediaStreamTrackState::Live,
+ bool aMuted = false,
+ const MediaTrackConstraints& aConstraints = MediaTrackConstraints())
+ : MediaStreamTrack(aWindow, aInputTrack, aSource, aReadyState, aMuted,
+ aConstraints) {}
+
+ AudioStreamTrack* AsAudioStreamTrack() override { return this; }
+ const AudioStreamTrack* AsAudioStreamTrack() const override { return this; }
+
+ // Direct output to aSink, or the default output device if aSink is null.
+ // No more than one output may exist for a single aKey at any one time.
+ // Returns a promise that resolves immediately for the default device or
+ // when a non-default device is processing audio.
+ RefPtr<GenericPromise> AddAudioOutput(void* aKey, AudioDeviceInfo* aSink);
+ void RemoveAudioOutput(void* aKey);
+ void SetAudioOutputVolume(void* aKey, float aVolume);
+
+ // WebIDL
+ void GetKind(nsAString& aKind) override { aKind.AssignLiteral("audio"); }
+
+ void GetLabel(nsAString& aLabel, CallerType aCallerType) override;
+
+ protected:
+ already_AddRefed<MediaStreamTrack> CloneInternal() override;
+ void SetReadyState(MediaStreamTrackState aState) override;
+
+ private:
+ // Track CrossGraphPort per AudioOutput key. This is required in order to
+ // redirect all AudioOutput requests (add, remove, set volume) to the
+ // receiver track, which belongs to the remote graph. MainThread only.
+ nsClassHashtable<nsPtrHashKey<void>, CrossGraphPort> mCrossGraphs;
+};
+
+} // namespace mozilla::dom
+
+#endif /* AUDIOSTREAMTRACK_H_ */
diff --git a/dom/media/AudioTrack.cpp b/dom/media/AudioTrack.cpp
new file mode 100644
index 0000000000..e6ee43a0f8
--- /dev/null
+++ b/dom/media/AudioTrack.cpp
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 et tw=78: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/AudioStreamTrack.h"
+#include "mozilla/dom/AudioTrack.h"
+#include "mozilla/dom/AudioTrackBinding.h"
+#include "mozilla/dom/AudioTrackList.h"
+#include "mozilla/dom/HTMLMediaElement.h"
+
+namespace mozilla::dom {
+
+AudioTrack::AudioTrack(nsIGlobalObject* aOwnerGlobal, const nsAString& aId,
+ const nsAString& aKind, const nsAString& aLabel,
+ const nsAString& aLanguage, bool aEnabled,
+ AudioStreamTrack* aStreamTrack)
+ : MediaTrack(aOwnerGlobal, aId, aKind, aLabel, aLanguage),
+ mEnabled(aEnabled),
+ mAudioStreamTrack(aStreamTrack) {}
+
+AudioTrack::~AudioTrack() = default;
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(AudioTrack, MediaTrack, mAudioStreamTrack)
+
+NS_IMPL_ADDREF_INHERITED(AudioTrack, MediaTrack)
+NS_IMPL_RELEASE_INHERITED(AudioTrack, MediaTrack)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AudioTrack)
+NS_INTERFACE_MAP_END_INHERITING(MediaTrack)
+
+JSObject* AudioTrack::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return AudioTrack_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void AudioTrack::SetEnabled(bool aEnabled) {
+ SetEnabledInternal(aEnabled, MediaTrack::DEFAULT);
+}
+
+void AudioTrack::SetEnabledInternal(bool aEnabled, int aFlags) {
+ if (aEnabled == mEnabled) {
+ return;
+ }
+ mEnabled = aEnabled;
+
+ // If this AudioTrack is no longer in its original AudioTrackList, then
+ // whether it is enabled or not has no effect on its original list.
+ if (!mList) {
+ return;
+ }
+
+ if (mEnabled) {
+ HTMLMediaElement* element = mList->GetMediaElement();
+ if (element) {
+ element->NotifyMediaTrackEnabled(this);
+ }
+ } else {
+ HTMLMediaElement* element = mList->GetMediaElement();
+ if (element) {
+ element->NotifyMediaTrackDisabled(this);
+ }
+ }
+
+ if (!(aFlags & MediaTrack::FIRE_NO_EVENTS)) {
+ mList->CreateAndDispatchChangeEvent();
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/AudioTrack.h b/dom/media/AudioTrack.h
new file mode 100644
index 0000000000..29104801ca
--- /dev/null
+++ b/dom/media/AudioTrack.h
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 et tw=78: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_AudioTrack_h
+#define mozilla_dom_AudioTrack_h
+
+#include "MediaTrack.h"
+
+namespace mozilla::dom {
+
+class AudioStreamTrack;
+
+class AudioTrack : public MediaTrack {
+ public:
+ AudioTrack(nsIGlobalObject* aOwnerGlobal, const nsAString& aId,
+ const nsAString& aKind, const nsAString& aLabel,
+ const nsAString& aLanguage, bool aEnabled,
+ AudioStreamTrack* aStreamTrack = nullptr);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(AudioTrack, MediaTrack)
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ AudioTrack* AsAudioTrack() override { return this; }
+
+ void SetEnabledInternal(bool aEnabled, int aFlags) override;
+
+ // Get associated audio stream track when the audio track comes from
+ // MediaStream. This might be nullptr when the src of owning HTMLMediaElement
+ // is not MediaStream.
+ AudioStreamTrack* GetAudioStreamTrack() { return mAudioStreamTrack; }
+
+ // WebIDL
+ bool Enabled() const { return mEnabled; }
+
+ void SetEnabled(bool aEnabled);
+
+ private:
+ virtual ~AudioTrack();
+
+ bool mEnabled;
+ RefPtr<AudioStreamTrack> mAudioStreamTrack;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_AudioTrack_h
diff --git a/dom/media/AudioTrackList.cpp b/dom/media/AudioTrackList.cpp
new file mode 100644
index 0000000000..0ba1f880e9
--- /dev/null
+++ b/dom/media/AudioTrackList.cpp
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/AudioTrack.h"
+#include "mozilla/dom/AudioTrackList.h"
+#include "mozilla/dom/AudioTrackListBinding.h"
+
+namespace mozilla::dom {
+
+JSObject* AudioTrackList::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return AudioTrackList_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+AudioTrack* AudioTrackList::operator[](uint32_t aIndex) {
+ MediaTrack* track = MediaTrackList::operator[](aIndex);
+ return track->AsAudioTrack();
+}
+
+AudioTrack* AudioTrackList::IndexedGetter(uint32_t aIndex, bool& aFound) {
+ MediaTrack* track = MediaTrackList::IndexedGetter(aIndex, aFound);
+ return track ? track->AsAudioTrack() : nullptr;
+}
+
+AudioTrack* AudioTrackList::GetTrackById(const nsAString& aId) {
+ MediaTrack* track = MediaTrackList::GetTrackById(aId);
+ return track ? track->AsAudioTrack() : nullptr;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/AudioTrackList.h b/dom/media/AudioTrackList.h
new file mode 100644
index 0000000000..3048eeea35
--- /dev/null
+++ b/dom/media/AudioTrackList.h
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 et tw=78: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_AudioTrackList_h
+#define mozilla_dom_AudioTrackList_h
+
+#include "MediaTrack.h"
+#include "MediaTrackList.h"
+
+namespace mozilla::dom {
+
+class AudioTrack;
+
+class AudioTrackList : public MediaTrackList {
+ public:
+ AudioTrackList(nsIGlobalObject* aOwnerObject, HTMLMediaElement* aMediaElement)
+ : MediaTrackList(aOwnerObject, aMediaElement) {}
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ AudioTrack* operator[](uint32_t aIndex);
+
+ // WebIDL
+ AudioTrack* IndexedGetter(uint32_t aIndex, bool& aFound);
+
+ AudioTrack* GetTrackById(const nsAString& aId);
+
+ protected:
+ AudioTrackList* AsAudioTrackList() override { return this; }
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_AudioTrackList_h
diff --git a/dom/media/BackgroundVideoDecodingPermissionObserver.cpp b/dom/media/BackgroundVideoDecodingPermissionObserver.cpp
new file mode 100644
index 0000000000..06654dbeef
--- /dev/null
+++ b/dom/media/BackgroundVideoDecodingPermissionObserver.cpp
@@ -0,0 +1,154 @@
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "BackgroundVideoDecodingPermissionObserver.h"
+
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "MediaDecoder.h"
+#include "nsContentUtils.h"
+#include "mozilla/dom/Document.h"
+
+namespace mozilla {
+
+BackgroundVideoDecodingPermissionObserver::
+ BackgroundVideoDecodingPermissionObserver(MediaDecoder* aDecoder)
+ : mDecoder(aDecoder), mIsRegisteredForEvent(false) {
+ MOZ_ASSERT(mDecoder);
+}
+
+NS_IMETHODIMP
+BackgroundVideoDecodingPermissionObserver::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* aData) {
+ if (!StaticPrefs::media_resume_bkgnd_video_on_tabhover()) {
+ return NS_OK;
+ }
+
+ if (!IsValidEventSender(aSubject)) {
+ return NS_OK;
+ }
+
+ if (strcmp(aTopic, "unselected-tab-hover") == 0) {
+ bool allowed = !NS_strcmp(aData, u"true");
+ mDecoder->SetIsBackgroundVideoDecodingAllowed(allowed);
+ }
+ return NS_OK;
+}
+
+void BackgroundVideoDecodingPermissionObserver::RegisterEvent() {
+ MOZ_ASSERT(!mIsRegisteredForEvent);
+ nsCOMPtr<nsIObserverService> observerService = services::GetObserverService();
+ if (observerService) {
+ observerService->AddObserver(this, "unselected-tab-hover", false);
+ mIsRegisteredForEvent = true;
+ if (nsContentUtils::IsInStableOrMetaStableState()) {
+ // Events shall not be fired synchronously to prevent anything visible
+ // from the scripts while we are in stable state.
+ if (nsCOMPtr<dom::Document> doc = GetOwnerDoc()) {
+ doc->Dispatch(
+ TaskCategory::Other,
+ NewRunnableMethod(
+ "BackgroundVideoDecodingPermissionObserver::"
+ "EnableEvent",
+ this, &BackgroundVideoDecodingPermissionObserver::EnableEvent));
+ }
+ } else {
+ EnableEvent();
+ }
+ }
+}
+
+void BackgroundVideoDecodingPermissionObserver::UnregisterEvent() {
+ MOZ_ASSERT(mIsRegisteredForEvent);
+ nsCOMPtr<nsIObserverService> observerService = services::GetObserverService();
+ if (observerService) {
+ observerService->RemoveObserver(this, "unselected-tab-hover");
+ mIsRegisteredForEvent = false;
+ mDecoder->SetIsBackgroundVideoDecodingAllowed(false);
+ if (nsContentUtils::IsInStableOrMetaStableState()) {
+ // Events shall not be fired synchronously to prevent anything visible
+ // from the scripts while we are in stable state.
+ if (nsCOMPtr<dom::Document> doc = GetOwnerDoc()) {
+ doc->Dispatch(
+ TaskCategory::Other,
+ NewRunnableMethod(
+ "BackgroundVideoDecodingPermissionObserver::"
+ "DisableEvent",
+ this,
+ &BackgroundVideoDecodingPermissionObserver::DisableEvent));
+ }
+ } else {
+ DisableEvent();
+ }
+ }
+}
+
+BackgroundVideoDecodingPermissionObserver::
+ ~BackgroundVideoDecodingPermissionObserver() {
+ MOZ_ASSERT(!mIsRegisteredForEvent);
+}
+
+void BackgroundVideoDecodingPermissionObserver::EnableEvent() const {
+ // If we can't get document or outer window, then you can't reach the chrome
+ // <browser> either, so we don't need want to dispatch the event.
+ dom::Document* doc = GetOwnerDoc();
+ if (!doc || !doc->GetWindow()) {
+ return;
+ }
+
+ RefPtr<AsyncEventDispatcher> asyncDispatcher =
+ new AsyncEventDispatcher(doc, u"UnselectedTabHover:Enable"_ns,
+ CanBubble::eYes, ChromeOnlyDispatch::eYes);
+ asyncDispatcher->PostDOMEvent();
+}
+
+void BackgroundVideoDecodingPermissionObserver::DisableEvent() const {
+ // If we can't get document or outer window, then you can't reach the chrome
+ // <browser> either, so we don't need want to dispatch the event.
+ dom::Document* doc = GetOwnerDoc();
+ if (!doc || !doc->GetWindow()) {
+ return;
+ }
+
+ RefPtr<AsyncEventDispatcher> asyncDispatcher =
+ new AsyncEventDispatcher(doc, u"UnselectedTabHover:Disable"_ns,
+ CanBubble::eYes, ChromeOnlyDispatch::eYes);
+ asyncDispatcher->PostDOMEvent();
+}
+
+dom::BrowsingContext* BackgroundVideoDecodingPermissionObserver::GetOwnerBC()
+ const {
+ dom::Document* doc = GetOwnerDoc();
+ return doc ? doc->GetBrowsingContext() : nullptr;
+}
+
+dom::Document* BackgroundVideoDecodingPermissionObserver::GetOwnerDoc() const {
+ if (!mDecoder->GetOwner()) {
+ return nullptr;
+ }
+
+ return mDecoder->GetOwner()->GetDocument();
+}
+
+bool BackgroundVideoDecodingPermissionObserver::IsValidEventSender(
+ nsISupports* aSubject) const {
+ nsCOMPtr<nsPIDOMWindowInner> senderInner(do_QueryInterface(aSubject));
+ if (!senderInner) {
+ return false;
+ }
+
+ RefPtr<dom::BrowsingContext> senderBC = senderInner->GetBrowsingContext();
+ if (!senderBC) {
+ return false;
+ }
+ // Valid sender should be in the same browsing context tree as where owner is.
+ return GetOwnerBC() ? GetOwnerBC()->Top() == senderBC->Top() : false;
+}
+
+NS_IMPL_ISUPPORTS(BackgroundVideoDecodingPermissionObserver, nsIObserver)
+
+} // namespace mozilla
diff --git a/dom/media/BackgroundVideoDecodingPermissionObserver.h b/dom/media/BackgroundVideoDecodingPermissionObserver.h
new file mode 100644
index 0000000000..ee8e8164de
--- /dev/null
+++ b/dom/media/BackgroundVideoDecodingPermissionObserver.h
@@ -0,0 +1,51 @@
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(BackgroundVideoDecodingPermissionObserver_h_)
+# define BackgroundVideoDecodingPermissionObserver_h_
+
+# include "nsIObserver.h"
+# include "nsISupportsImpl.h"
+
+class nsISupports;
+class nsPIDOMWindowOuter;
+
+namespace mozilla {
+
+namespace dom {
+class Document;
+class BrowsingContext;
+} // namespace dom
+
+class MediaDecoder;
+
+class BackgroundVideoDecodingPermissionObserver final : public nsIObserver {
+ public:
+ NS_DECL_ISUPPORTS
+
+ explicit BackgroundVideoDecodingPermissionObserver(MediaDecoder* aDecoder);
+
+ NS_IMETHOD Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) override;
+ void RegisterEvent();
+ void UnregisterEvent();
+
+ private:
+ ~BackgroundVideoDecodingPermissionObserver();
+ void EnableEvent() const;
+ void DisableEvent() const;
+ dom::BrowsingContext* GetOwnerBC() const;
+ dom::Document* GetOwnerDoc() const;
+ bool IsValidEventSender(nsISupports* aSubject) const;
+
+ // The life cycle of observer would always be shorter than decoder, so we
+ // use raw pointer here.
+ MediaDecoder* mDecoder;
+ bool mIsRegisteredForEvent;
+};
+
+} // namespace mozilla
+
+#endif // BackgroundVideoDecodingPermissionObserver_h_
diff --git a/dom/media/BaseMediaResource.cpp b/dom/media/BaseMediaResource.cpp
new file mode 100644
index 0000000000..e5ba50109a
--- /dev/null
+++ b/dom/media/BaseMediaResource.cpp
@@ -0,0 +1,171 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "BaseMediaResource.h"
+
+#include "ChannelMediaResource.h"
+#include "CloneableWithRangeMediaResource.h"
+#include "FileMediaResource.h"
+#include "MediaContainerType.h"
+#include "mozilla/dom/BlobImpl.h"
+#include "mozilla/dom/BlobURLProtocolHandler.h"
+#include "mozilla/dom/HTMLMediaElement.h"
+#include "mozilla/InputStreamLengthHelper.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsICloneableInputStream.h"
+#include "nsIFile.h"
+#include "nsIFileChannel.h"
+#include "nsIInputStream.h"
+#include "nsNetUtil.h"
+
+namespace mozilla {
+
+already_AddRefed<BaseMediaResource> BaseMediaResource::Create(
+ MediaResourceCallback* aCallback, nsIChannel* aChannel,
+ bool aIsPrivateBrowsing) {
+ NS_ASSERTION(NS_IsMainThread(),
+ "MediaResource::Open called on non-main thread");
+
+ // If the channel was redirected, we want the post-redirect URI;
+ // but if the URI scheme was expanded, say from chrome: to jar:file:,
+ // we want the original URI.
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(uri));
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ nsAutoCString contentTypeString;
+ aChannel->GetContentType(contentTypeString);
+ Maybe<MediaContainerType> containerType =
+ MakeMediaContainerType(contentTypeString);
+ if (!containerType) {
+ return nullptr;
+ }
+
+ // Let's try to create a FileMediaResource in case the channel is a nsIFile
+ nsCOMPtr<nsIFileChannel> fc = do_QueryInterface(aChannel);
+ if (fc) {
+ RefPtr<BaseMediaResource> resource =
+ new FileMediaResource(aCallback, aChannel, uri);
+ return resource.forget();
+ }
+
+ int64_t streamLength = -1;
+
+ RefPtr<mozilla::dom::BlobImpl> blobImpl;
+ if (dom::IsBlobURI(uri) &&
+ NS_SUCCEEDED(NS_GetBlobForBlobURI(uri, getter_AddRefs(blobImpl))) &&
+ blobImpl) {
+ IgnoredErrorResult rv;
+
+ nsCOMPtr<nsIInputStream> stream;
+ blobImpl->CreateInputStream(getter_AddRefs(stream), rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ return nullptr;
+ }
+
+ // If this stream knows its own size synchronously, we can still use
+ // FileMediaResource. If the size is known, it means that the reading
+ // doesn't require any async operation. This is required because
+ // FileMediaResource doesn't work with nsIAsyncInputStreams.
+ int64_t length;
+ if (InputStreamLengthHelper::GetSyncLength(stream, &length) &&
+ length >= 0) {
+ RefPtr<BaseMediaResource> resource =
+ new FileMediaResource(aCallback, aChannel, uri, length);
+ return resource.forget();
+ }
+
+ // Also if the stream doesn't know its own size synchronously, we can still
+ // read the length from the blob.
+ uint64_t size = blobImpl->GetSize(rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ return nullptr;
+ }
+
+ // Maybe this blob URL can be cloned with a range.
+ nsCOMPtr<nsICloneableInputStreamWithRange> cloneableWithRange =
+ do_QueryInterface(stream);
+ if (cloneableWithRange) {
+ RefPtr<BaseMediaResource> resource = new CloneableWithRangeMediaResource(
+ aCallback, aChannel, uri, stream, size);
+ return resource.forget();
+ }
+
+ // We know the size of the stream for blobURLs, let's use it.
+ streamLength = size;
+ }
+
+ RefPtr<BaseMediaResource> resource = new ChannelMediaResource(
+ aCallback, aChannel, uri, streamLength, aIsPrivateBrowsing);
+ return resource.forget();
+}
+
+void BaseMediaResource::SetLoadInBackground(bool aLoadInBackground) {
+ if (aLoadInBackground == mLoadInBackground) {
+ return;
+ }
+ mLoadInBackground = aLoadInBackground;
+ if (!mChannel) {
+ // No channel, resource is probably already loaded.
+ return;
+ }
+
+ MediaDecoderOwner* owner = mCallback->GetMediaOwner();
+ if (!owner) {
+ NS_WARNING("Null owner in MediaResource::SetLoadInBackground()");
+ return;
+ }
+ RefPtr<dom::HTMLMediaElement> element = owner->GetMediaElement();
+ if (!element) {
+ NS_WARNING("Null element in MediaResource::SetLoadInBackground()");
+ return;
+ }
+
+ bool isPending = false;
+ if (NS_SUCCEEDED(mChannel->IsPending(&isPending)) && isPending) {
+ nsLoadFlags loadFlags;
+ DebugOnly<nsresult> rv = mChannel->GetLoadFlags(&loadFlags);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "GetLoadFlags() failed!");
+
+ if (aLoadInBackground) {
+ loadFlags |= nsIRequest::LOAD_BACKGROUND;
+ } else {
+ loadFlags &= ~nsIRequest::LOAD_BACKGROUND;
+ }
+ Unused << NS_WARN_IF(NS_FAILED(ModifyLoadFlags(loadFlags)));
+ }
+}
+
+nsresult BaseMediaResource::ModifyLoadFlags(nsLoadFlags aFlags) {
+ nsCOMPtr<nsILoadGroup> loadGroup;
+ nsresult rv = mChannel->GetLoadGroup(getter_AddRefs(loadGroup));
+ MOZ_ASSERT(NS_SUCCEEDED(rv), "GetLoadGroup() failed!");
+
+ bool inLoadGroup = false;
+ if (loadGroup) {
+ nsresult status;
+ mChannel->GetStatus(&status);
+
+ rv = loadGroup->RemoveRequest(mChannel, nullptr, status);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ inLoadGroup = true;
+ }
+
+ rv = mChannel->SetLoadFlags(aFlags);
+ MOZ_ASSERT(NS_SUCCEEDED(rv), "SetLoadFlags() failed!");
+
+ if (inLoadGroup) {
+ rv = loadGroup->AddRequest(mChannel, nullptr);
+ MOZ_ASSERT(NS_SUCCEEDED(rv), "AddRequest() failed!");
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/dom/media/BaseMediaResource.h b/dom/media/BaseMediaResource.h
new file mode 100644
index 0000000000..29cde01e8c
--- /dev/null
+++ b/dom/media/BaseMediaResource.h
@@ -0,0 +1,151 @@
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef BaseMediaResource_h
+#define BaseMediaResource_h
+
+#include "MediaResource.h"
+#include "MediaResourceCallback.h"
+#include "MediaCache.h"
+#include "nsIChannel.h"
+#include "nsIURI.h"
+#include "nsIStreamListener.h"
+#include "mozilla/dom/MediaDebugInfoBinding.h"
+
+class nsIPrincipal;
+
+namespace mozilla {
+
+DDLoggedTypeDeclNameAndBase(BaseMediaResource, MediaResource);
+
+class BaseMediaResource : public MediaResource,
+ public DecoderDoctorLifeLogger<BaseMediaResource> {
+ public:
+ /**
+ * Create a resource, reading data from the channel. Call on main thread only.
+ * The caller must follow up by calling resource->Open().
+ */
+ static already_AddRefed<BaseMediaResource> Create(
+ MediaResourceCallback* aCallback, nsIChannel* aChannel,
+ bool aIsPrivateBrowsing);
+
+ // Pass true to limit the amount of readahead data (specified by
+ // "media.cache_readahead_limit") or false to read as much as the
+ // cache size allows.
+ virtual void ThrottleReadahead(bool bThrottle) {}
+
+ // This is the client's estimate of the playback rate assuming
+ // the media plays continuously. The cache can't guess this itself
+ // because it doesn't know when the decoder was paused, buffering, etc.
+ virtual void SetPlaybackRate(uint32_t aBytesPerSecond) = 0;
+
+ // Get the estimated download rate in bytes per second (assuming no
+ // pausing of the channel is requested by Gecko).
+ // *aIsReliable is set to true if we think the estimate is useful.
+ virtual double GetDownloadRate(bool* aIsReliable) = 0;
+
+ // Moves any existing channel loads into or out of background. Background
+ // loads don't block the load event. This also determines whether or not any
+ // new loads initiated (for example to seek) will be in the background.
+ void SetLoadInBackground(bool aLoadInBackground);
+
+ // Suspend any downloads that are in progress.
+ // If aCloseImmediately is set, resources should be released immediately
+ // since we don't expect to resume again any time soon. Otherwise we
+ // may resume again soon so resources should be held for a little
+ // while.
+ virtual void Suspend(bool aCloseImmediately) = 0;
+
+ // Resume any downloads that have been suspended.
+ virtual void Resume() = 0;
+
+ // The mode is initially MODE_METADATA.
+ virtual void SetReadMode(MediaCacheStream::ReadMode aMode) = 0;
+
+ // Returns true if the resource can be seeked to unbuffered ranges, i.e.
+ // for an HTTP network stream this returns true if HTTP1.1 Byte Range
+ // requests are supported by the connection/server.
+ virtual bool IsTransportSeekable() = 0;
+
+ // Get the current principal for the channel
+ virtual already_AddRefed<nsIPrincipal> GetCurrentPrincipal() = 0;
+
+ // Return true if the loading of this resource required cross-origin
+ // redirects.
+ virtual bool HadCrossOriginRedirects() = 0;
+
+ /**
+ * Open the stream. This creates a stream listener and returns it in
+ * aStreamListener; this listener needs to be notified of incoming data.
+ */
+ virtual nsresult Open(nsIStreamListener** aStreamListener) = 0;
+
+ // If this returns false, then we shouldn't try to clone this MediaResource
+ // because its underlying resources are not suitable for reuse (e.g.
+ // because the underlying connection has been lost, or this resource
+ // just can't be safely cloned). If this returns true, CloneData could
+ // still fail. If this returns false, CloneData should not be called.
+ virtual bool CanClone() { return false; }
+
+ // Create a new stream of the same type that refers to the same URI
+ // with a new channel. Any cached data associated with the original
+ // stream should be accessible in the new stream too.
+ virtual already_AddRefed<BaseMediaResource> CloneData(
+ MediaResourceCallback* aCallback) {
+ return nullptr;
+ }
+
+ // Returns true if the resource is a live stream.
+ virtual bool IsLiveStream() const { return false; }
+
+ virtual size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ // Might be useful to track in the future:
+ // - mChannel
+ // - mURI (possibly owned, looks like just a ref from mChannel)
+ // Not owned:
+ // - mCallback
+ return 0;
+ }
+
+ virtual size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ virtual void GetDebugInfo(dom::MediaResourceDebugInfo& aInfo) {}
+
+ protected:
+ BaseMediaResource(MediaResourceCallback* aCallback, nsIChannel* aChannel,
+ nsIURI* aURI)
+ : mCallback(aCallback),
+ mChannel(aChannel),
+ mURI(aURI),
+ mLoadInBackground(false) {}
+ virtual ~BaseMediaResource() = default;
+
+ // Set the request's load flags to aFlags. If the request is part of a
+ // load group, the request is removed from the group, the flags are set, and
+ // then the request is added back to the load group.
+ nsresult ModifyLoadFlags(nsLoadFlags aFlags);
+
+ RefPtr<MediaResourceCallback> mCallback;
+
+ // Channel used to download the media data. Must be accessed
+ // from the main thread only.
+ nsCOMPtr<nsIChannel> mChannel;
+
+ // URI in case the stream needs to be re-opened. Access from
+ // main thread only.
+ nsCOMPtr<nsIURI> mURI;
+
+ // True if SetLoadInBackground() has been called with
+ // aLoadInBackground = true, i.e. when the document load event is not
+ // blocked by this resource, and all channel loads will be in the
+ // background.
+ bool mLoadInBackground;
+};
+
+} // namespace mozilla
+
+#endif // BaseMediaResource_h
diff --git a/dom/media/Benchmark.cpp b/dom/media/Benchmark.cpp
new file mode 100644
index 0000000000..37bb2dcc22
--- /dev/null
+++ b/dom/media/Benchmark.cpp
@@ -0,0 +1,395 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "Benchmark.h"
+
+#include "BufferMediaResource.h"
+#include "MediaData.h"
+#include "MediaDataDecoderProxy.h"
+#include "PDMFactory.h"
+#include "VideoUtils.h"
+#include "WebMDemuxer.h"
+#include "mozilla/AbstractThread.h"
+#include "mozilla/Components.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/SharedThreadPool.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/TaskQueue.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "nsGkAtoms.h"
+#include "nsIGfxInfo.h"
+
+#ifndef MOZ_WIDGET_ANDROID
+# include "WebMSample.h"
+#endif
+
+using namespace mozilla::gfx;
+
+namespace mozilla {
+
+// Update this version number to force re-running the benchmark. Such as when
+// an improvement to FFVP9 or LIBVPX is deemed worthwhile.
+const uint32_t VP9Benchmark::sBenchmarkVersionID = 5;
+
+const char* VP9Benchmark::sBenchmarkFpsPref = "media.benchmark.vp9.fps";
+const char* VP9Benchmark::sBenchmarkFpsVersionCheck =
+ "media.benchmark.vp9.versioncheck";
+bool VP9Benchmark::sHasRunTest = false;
+
+// static
+bool VP9Benchmark::ShouldRun() {
+#if defined(MOZ_WIDGET_ANDROID)
+ // Assume that the VP9 software decoder will always be too slow.
+ return false;
+#else
+# if defined(MOZ_APPLEMEDIA)
+ const nsCOMPtr<nsIGfxInfo> gfxInfo = components::GfxInfo::Service();
+ nsString vendorID, deviceID;
+ gfxInfo->GetAdapterVendorID(vendorID);
+ // We won't run the VP9 benchmark on mac using an Intel GPU as performance are
+ // poor, see bug 1404042.
+ if (vendorID.EqualsLiteral("0x8086")) {
+ return false;
+ }
+ // Fall Through
+# endif
+ return true;
+#endif
+}
+
+// static
+uint32_t VP9Benchmark::MediaBenchmarkVp9Fps() {
+ if (!ShouldRun()) {
+ return 0;
+ }
+ return StaticPrefs::media_benchmark_vp9_fps();
+}
+
+// static
+bool VP9Benchmark::IsVP9DecodeFast(bool aDefault) {
+#if defined(MOZ_WIDGET_ANDROID)
+ return false;
+#else
+ if (!ShouldRun()) {
+ return false;
+ }
+ static StaticMutex sMutex MOZ_UNANNOTATED;
+ uint32_t decodeFps = StaticPrefs::media_benchmark_vp9_fps();
+ uint32_t hadRecentUpdate = StaticPrefs::media_benchmark_vp9_versioncheck();
+ bool needBenchmark;
+ {
+ StaticMutexAutoLock lock(sMutex);
+ needBenchmark = !sHasRunTest &&
+ (decodeFps == 0 || hadRecentUpdate != sBenchmarkVersionID);
+ sHasRunTest = true;
+ }
+
+ if (needBenchmark) {
+ RefPtr<WebMDemuxer> demuxer = new WebMDemuxer(
+ new BufferMediaResource(sWebMSample, sizeof(sWebMSample)));
+ RefPtr<Benchmark> estimiser = new Benchmark(
+ demuxer,
+ {StaticPrefs::media_benchmark_frames(), // frames to measure
+ 1, // start benchmarking after decoding this frame.
+ 8, // loop after decoding that many frames.
+ TimeDuration::FromMilliseconds(
+ StaticPrefs::media_benchmark_timeout())});
+ estimiser->Run()->Then(
+ AbstractThread::MainThread(), __func__,
+ [](uint32_t aDecodeFps) {
+ if (XRE_IsContentProcess()) {
+ dom::ContentChild* contentChild = dom::ContentChild::GetSingleton();
+ if (contentChild) {
+ contentChild->SendNotifyBenchmarkResult(u"VP9"_ns, aDecodeFps);
+ }
+ } else {
+ Preferences::SetUint(sBenchmarkFpsPref, aDecodeFps);
+ Preferences::SetUint(sBenchmarkFpsVersionCheck,
+ sBenchmarkVersionID);
+ }
+ },
+ []() {});
+ }
+
+ if (decodeFps == 0) {
+ return aDefault;
+ }
+
+ return decodeFps >= StaticPrefs::media_benchmark_vp9_threshold();
+#endif
+}
+
+Benchmark::Benchmark(MediaDataDemuxer* aDemuxer, const Parameters& aParameters)
+ : QueueObject(
+ TaskQueue::Create(GetMediaThreadPool(MediaThreadType::SUPERVISOR),
+ "Benchmark::QueueObject")),
+ mParameters(aParameters),
+ mKeepAliveUntilComplete(this),
+ mPlaybackState(this, aDemuxer) {
+ MOZ_COUNT_CTOR(Benchmark);
+}
+
+Benchmark::~Benchmark() { MOZ_COUNT_DTOR(Benchmark); }
+
+RefPtr<Benchmark::BenchmarkPromise> Benchmark::Run() {
+ RefPtr<Benchmark> self = this;
+ return InvokeAsync(Thread(), __func__, [self] {
+ RefPtr<BenchmarkPromise> p = self->mPromise.Ensure(__func__);
+ self->mPlaybackState.Dispatch(NS_NewRunnableFunction(
+ "Benchmark::Run", [self]() { self->mPlaybackState.DemuxSamples(); }));
+ return p;
+ });
+}
+
+void Benchmark::ReturnResult(uint32_t aDecodeFps) {
+ MOZ_ASSERT(OnThread());
+
+ mPromise.ResolveIfExists(aDecodeFps, __func__);
+}
+
+void Benchmark::ReturnError(const MediaResult& aError) {
+ MOZ_ASSERT(OnThread());
+
+ mPromise.RejectIfExists(aError, __func__);
+}
+
+void Benchmark::Dispose() {
+ MOZ_ASSERT(OnThread());
+
+ mKeepAliveUntilComplete = nullptr;
+}
+
+void Benchmark::Init() {
+ MOZ_ASSERT(NS_IsMainThread());
+ gfxVars::Initialize();
+}
+
+BenchmarkPlayback::BenchmarkPlayback(Benchmark* aGlobalState,
+ MediaDataDemuxer* aDemuxer)
+ : QueueObject(
+ TaskQueue::Create(GetMediaThreadPool(MediaThreadType::SUPERVISOR),
+ "BenchmarkPlayback::QueueObject")),
+ mGlobalState(aGlobalState),
+ mDecoderTaskQueue(TaskQueue::Create(
+ GetMediaThreadPool(MediaThreadType::PLATFORM_DECODER),
+ "BenchmarkPlayback::mDecoderTaskQueue")),
+ mDemuxer(aDemuxer),
+ mSampleIndex(0),
+ mFrameCount(0),
+ mFinished(false),
+ mDrained(false) {}
+
+void BenchmarkPlayback::DemuxSamples() {
+ MOZ_ASSERT(OnThread());
+
+ RefPtr<Benchmark> ref(mGlobalState);
+ mDemuxer->Init()->Then(
+ Thread(), __func__,
+ [this, ref](nsresult aResult) {
+ MOZ_ASSERT(OnThread());
+ if (mDemuxer->GetNumberTracks(TrackInfo::kVideoTrack)) {
+ mTrackDemuxer = mDemuxer->GetTrackDemuxer(TrackInfo::kVideoTrack, 0);
+ } else if (mDemuxer->GetNumberTracks(TrackInfo::kAudioTrack)) {
+ mTrackDemuxer = mDemuxer->GetTrackDemuxer(TrackInfo::kAudioTrack, 0);
+ }
+ if (!mTrackDemuxer) {
+ Error(MediaResult(NS_ERROR_FAILURE, "Can't create track demuxer"));
+ return;
+ }
+ DemuxNextSample();
+ },
+ [this, ref](const MediaResult& aError) { Error(aError); });
+}
+
+void BenchmarkPlayback::DemuxNextSample() {
+ MOZ_ASSERT(OnThread());
+
+ RefPtr<Benchmark> ref(mGlobalState);
+ RefPtr<MediaTrackDemuxer::SamplesPromise> promise =
+ mTrackDemuxer->GetSamples();
+ promise->Then(
+ Thread(), __func__,
+ [this, ref](RefPtr<MediaTrackDemuxer::SamplesHolder> aHolder) {
+ mSamples.AppendElements(std::move(aHolder->GetMovableSamples()));
+ if (ref->mParameters.mStopAtFrame &&
+ mSamples.Length() == ref->mParameters.mStopAtFrame.ref()) {
+ InitDecoder(mTrackDemuxer->GetInfo());
+ } else {
+ Dispatch(
+ NS_NewRunnableFunction("BenchmarkPlayback::DemuxNextSample",
+ [this, ref]() { DemuxNextSample(); }));
+ }
+ },
+ [this, ref](const MediaResult& aError) {
+ switch (aError.Code()) {
+ case NS_ERROR_DOM_MEDIA_END_OF_STREAM:
+ InitDecoder(mTrackDemuxer->GetInfo());
+ break;
+ default:
+ Error(aError);
+ break;
+ }
+ });
+}
+
+void BenchmarkPlayback::InitDecoder(UniquePtr<TrackInfo>&& aInfo) {
+ MOZ_ASSERT(OnThread());
+
+ if (!aInfo) {
+ Error(MediaResult(NS_ERROR_FAILURE, "Invalid TrackInfo"));
+ return;
+ }
+
+ RefPtr<PDMFactory> platform = new PDMFactory();
+ mInfo = std::move(aInfo);
+ RefPtr<Benchmark> ref(mGlobalState);
+ platform->CreateDecoder(CreateDecoderParams{*mInfo})
+ ->Then(
+ Thread(), __func__,
+ [this, ref](RefPtr<MediaDataDecoder>&& aDecoder) {
+ mDecoder = new MediaDataDecoderProxy(
+ aDecoder.forget(), do_AddRef(mDecoderTaskQueue.get()));
+ mDecoder->Init()->Then(
+ Thread(), __func__,
+ [this, ref](TrackInfo::TrackType aTrackType) {
+ InputExhausted();
+ },
+ [this, ref](const MediaResult& aError) { Error(aError); });
+ },
+ [this, ref](const MediaResult& aError) { Error(aError); });
+}
+
+void BenchmarkPlayback::FinalizeShutdown() {
+ MOZ_ASSERT(OnThread());
+
+ MOZ_ASSERT(mFinished, "GlobalShutdown must have been run");
+ MOZ_ASSERT(!mDecoder, "mDecoder must have been shutdown already");
+ MOZ_ASSERT(!mDemuxer, "mDemuxer must have been shutdown already");
+ MOZ_DIAGNOSTIC_ASSERT(mDecoderTaskQueue->IsEmpty());
+ mDecoderTaskQueue = nullptr;
+
+ RefPtr<Benchmark> ref(mGlobalState);
+ ref->Thread()->Dispatch(NS_NewRunnableFunction(
+ "BenchmarkPlayback::FinalizeShutdown", [ref]() { ref->Dispose(); }));
+}
+
+void BenchmarkPlayback::GlobalShutdown() {
+ MOZ_ASSERT(OnThread());
+
+ MOZ_ASSERT(!mFinished, "We've already shutdown");
+
+ mFinished = true;
+
+ if (mTrackDemuxer) {
+ mTrackDemuxer->Reset();
+ mTrackDemuxer->BreakCycles();
+ mTrackDemuxer = nullptr;
+ }
+ mDemuxer = nullptr;
+
+ if (mDecoder) {
+ RefPtr<Benchmark> ref(mGlobalState);
+ mDecoder->Flush()->Then(
+ Thread(), __func__,
+ [ref, this]() {
+ mDecoder->Shutdown()->Then(
+ Thread(), __func__, [ref, this]() { FinalizeShutdown(); },
+ []() { MOZ_CRASH("not reached"); });
+ mDecoder = nullptr;
+ mInfo = nullptr;
+ },
+ []() { MOZ_CRASH("not reached"); });
+ } else {
+ FinalizeShutdown();
+ }
+}
+
+void BenchmarkPlayback::Output(MediaDataDecoder::DecodedData&& aResults) {
+ MOZ_ASSERT(OnThread());
+ MOZ_ASSERT(!mFinished);
+
+ RefPtr<Benchmark> ref(mGlobalState);
+ mFrameCount += aResults.Length();
+ if (!mDecodeStartTime && mFrameCount >= ref->mParameters.mStartupFrame) {
+ mDecodeStartTime = Some(TimeStamp::Now());
+ }
+ TimeStamp now = TimeStamp::Now();
+ uint32_t frames = mFrameCount - ref->mParameters.mStartupFrame;
+ TimeDuration elapsedTime = now - mDecodeStartTime.refOr(now);
+ if (((frames == ref->mParameters.mFramesToMeasure) &&
+ mFrameCount > ref->mParameters.mStartupFrame && frames > 0) ||
+ elapsedTime >= ref->mParameters.mTimeout || mDrained) {
+ uint32_t decodeFps = frames / elapsedTime.ToSeconds();
+ GlobalShutdown();
+ ref->Dispatch(NS_NewRunnableFunction(
+ "BenchmarkPlayback::Output",
+ [ref, decodeFps]() { ref->ReturnResult(decodeFps); }));
+ }
+}
+
+void BenchmarkPlayback::Error(const MediaResult& aError) {
+ MOZ_ASSERT(OnThread());
+
+ RefPtr<Benchmark> ref(mGlobalState);
+ GlobalShutdown();
+ ref->Dispatch(
+ NS_NewRunnableFunction("BenchmarkPlayback::Error",
+ [ref, aError]() { ref->ReturnError(aError); }));
+}
+
+void BenchmarkPlayback::InputExhausted() {
+ MOZ_ASSERT(OnThread());
+ MOZ_ASSERT(!mFinished);
+
+ if (mSampleIndex >= mSamples.Length()) {
+ Error(MediaResult(NS_ERROR_FAILURE, "Nothing left to decode"));
+ return;
+ }
+
+ RefPtr<MediaRawData> sample = mSamples[mSampleIndex];
+ RefPtr<Benchmark> ref(mGlobalState);
+ RefPtr<MediaDataDecoder::DecodePromise> p = mDecoder->Decode(sample);
+
+ mSampleIndex++;
+ if (mSampleIndex == mSamples.Length() && !ref->mParameters.mStopAtFrame) {
+ // Complete current frame decode then drain if still necessary.
+ p->Then(
+ Thread(), __func__,
+ [ref, this](MediaDataDecoder::DecodedData&& aResults) {
+ Output(std::move(aResults));
+ if (!mFinished) {
+ mDecoder->Drain()->Then(
+ Thread(), __func__,
+ [ref, this](MediaDataDecoder::DecodedData&& aResults) {
+ mDrained = true;
+ Output(std::move(aResults));
+ MOZ_ASSERT(mFinished, "We must be done now");
+ },
+ [ref, this](const MediaResult& aError) { Error(aError); });
+ }
+ },
+ [ref, this](const MediaResult& aError) { Error(aError); });
+ } else {
+ if (mSampleIndex == mSamples.Length() && ref->mParameters.mStopAtFrame) {
+ mSampleIndex = 0;
+ }
+ // Continue decoding
+ p->Then(
+ Thread(), __func__,
+ [ref, this](MediaDataDecoder::DecodedData&& aResults) {
+ Output(std::move(aResults));
+ if (!mFinished) {
+ InputExhausted();
+ }
+ },
+ [ref, this](const MediaResult& aError) { Error(aError); });
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/Benchmark.h b/dom/media/Benchmark.h
new file mode 100644
index 0000000000..b76942edc5
--- /dev/null
+++ b/dom/media/Benchmark.h
@@ -0,0 +1,119 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_BENCHMARK_H
+#define MOZILLA_BENCHMARK_H
+
+#include "MediaDataDemuxer.h"
+#include "PlatformDecoderModule.h"
+#include "QueueObject.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/UniquePtr.h"
+#include "nsCOMPtr.h"
+
+namespace mozilla {
+
+class TaskQueue;
+class Benchmark;
+
+class BenchmarkPlayback : public QueueObject {
+ friend class Benchmark;
+ BenchmarkPlayback(Benchmark* aGlobalState, MediaDataDemuxer* aDemuxer);
+ void DemuxSamples();
+ void DemuxNextSample();
+ void GlobalShutdown();
+ void InitDecoder(UniquePtr<TrackInfo>&& aInfo);
+
+ void Output(MediaDataDecoder::DecodedData&& aResults);
+ void Error(const MediaResult& aError);
+ void InputExhausted();
+
+ // Shutdown trackdemuxer and demuxer if any and shutdown the task queues.
+ void FinalizeShutdown();
+
+ Atomic<Benchmark*> mGlobalState;
+
+ RefPtr<TaskQueue> mDecoderTaskQueue;
+ RefPtr<MediaDataDecoder> mDecoder;
+
+ // Object only accessed on Thread()
+ RefPtr<MediaDataDemuxer> mDemuxer;
+ RefPtr<MediaTrackDemuxer> mTrackDemuxer;
+ nsTArray<RefPtr<MediaRawData>> mSamples;
+ UniquePtr<TrackInfo> mInfo;
+ size_t mSampleIndex;
+ Maybe<TimeStamp> mDecodeStartTime;
+ uint32_t mFrameCount;
+ bool mFinished;
+ bool mDrained;
+};
+
+// Init() must have been called at least once prior on the
+// main thread.
+class Benchmark : public QueueObject {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(Benchmark)
+
+ struct Parameters {
+ Parameters()
+ : mFramesToMeasure(UINT32_MAX),
+ mStartupFrame(1),
+ mTimeout(TimeDuration::Forever()) {}
+
+ Parameters(uint32_t aFramesToMeasure, uint32_t aStartupFrame,
+ uint32_t aStopAtFrame, const TimeDuration& aTimeout)
+ : mFramesToMeasure(aFramesToMeasure),
+ mStartupFrame(aStartupFrame),
+ mStopAtFrame(Some(aStopAtFrame)),
+ mTimeout(aTimeout) {}
+
+ const uint32_t mFramesToMeasure;
+ const uint32_t mStartupFrame;
+ const Maybe<uint32_t> mStopAtFrame;
+ const TimeDuration mTimeout;
+ };
+
+ typedef MozPromise<uint32_t, MediaResult, /* IsExclusive = */ true>
+ BenchmarkPromise;
+
+ explicit Benchmark(MediaDataDemuxer* aDemuxer,
+ const Parameters& aParameters = Parameters());
+ RefPtr<BenchmarkPromise> Run();
+
+ // Must be called on the main thread.
+ static void Init();
+
+ private:
+ friend class BenchmarkPlayback;
+ virtual ~Benchmark();
+ void ReturnResult(uint32_t aDecodeFps);
+ void ReturnError(const MediaResult& aError);
+ void Dispose();
+ const Parameters mParameters;
+ RefPtr<Benchmark> mKeepAliveUntilComplete;
+ BenchmarkPlayback mPlaybackState;
+ MozPromiseHolder<BenchmarkPromise> mPromise;
+};
+
+class VP9Benchmark {
+ public:
+ static bool IsVP9DecodeFast(bool aDefault = false);
+ static const char* sBenchmarkFpsPref;
+ static const char* sBenchmarkFpsVersionCheck;
+ static const uint32_t sBenchmarkVersionID;
+ static bool sHasRunTest;
+ // Return the value of media.benchmark.vp9.fps preference (which will be 0 if
+ // not known)
+ static uint32_t MediaBenchmarkVp9Fps();
+
+ private:
+ static bool ShouldRun();
+};
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/BitReader.cpp b/dom/media/BitReader.cpp
new file mode 100644
index 0000000000..f66ab76f19
--- /dev/null
+++ b/dom/media/BitReader.cpp
@@ -0,0 +1,197 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Derived from Stagefright's ABitReader.
+
+#include "BitReader.h"
+
+namespace mozilla {
+
+BitReader::BitReader(const mozilla::MediaByteBuffer* aBuffer)
+ : BitReader(aBuffer->Elements(), aBuffer->Length() * 8) {}
+
+BitReader::BitReader(const mozilla::MediaByteBuffer* aBuffer, size_t aBits)
+ : BitReader(aBuffer->Elements(), aBits) {}
+
+BitReader::BitReader(const uint8_t* aBuffer, size_t aBits)
+ : mData(aBuffer),
+ mOriginalBitSize(aBits),
+ mTotalBitsLeft(aBits),
+ mSize((aBits + 7) / 8),
+ mReservoir(0),
+ mNumBitsLeft(0) {}
+
+BitReader::~BitReader() = default;
+
+uint32_t BitReader::ReadBits(size_t aNum) {
+ MOZ_ASSERT(aNum <= 32);
+ if (mTotalBitsLeft < aNum) {
+ NS_ASSERTION(false, "Reading past end of buffer");
+ return 0;
+ }
+ uint32_t result = 0;
+ while (aNum > 0) {
+ if (mNumBitsLeft == 0) {
+ FillReservoir();
+ }
+
+ size_t m = aNum;
+ if (m > mNumBitsLeft) {
+ m = mNumBitsLeft;
+ }
+
+ if (m == 32) {
+ result = mReservoir;
+ mReservoir = 0;
+ } else {
+ result = (result << m) | (mReservoir >> (32 - m));
+ mReservoir <<= m;
+ }
+ mNumBitsLeft -= m;
+ mTotalBitsLeft -= m;
+
+ aNum -= m;
+ }
+
+ return result;
+}
+
+// Read unsigned integer Exp-Golomb-coded.
+uint32_t BitReader::ReadUE() {
+ uint32_t i = 0;
+
+ while (ReadBit() == 0 && i < 32) {
+ i++;
+ }
+ if (i == 32) {
+ // This can happen if the data is invalid, or if it's
+ // short, since ReadBit() will return 0 when it runs
+ // off the end of the buffer.
+ NS_WARNING("Invalid H.264 data");
+ return 0;
+ }
+ uint32_t r = ReadBits(i);
+ r += (uint32_t(1) << i) - 1;
+
+ return r;
+}
+
+// Read signed integer Exp-Golomb-coded.
+int32_t BitReader::ReadSE() {
+ int32_t r = ReadUE();
+ if (r & 1) {
+ return (r + 1) / 2;
+ } else {
+ return -r / 2;
+ }
+}
+
+uint64_t BitReader::ReadU64() {
+ uint64_t hi = ReadU32();
+ uint32_t lo = ReadU32();
+ return (hi << 32) | lo;
+}
+
+CheckedUint64 BitReader::ReadULEB128() {
+ // See https://en.wikipedia.org/wiki/LEB128#Decode_unsigned_integer
+ CheckedUint64 value = 0;
+ for (size_t i = 0; i < sizeof(uint64_t) * 8 / 7; i++) {
+ bool more = ReadBit();
+ value += static_cast<uint64_t>(ReadBits(7)) << (i * 7);
+ if (!more) {
+ break;
+ }
+ }
+ return value;
+}
+
+uint64_t BitReader::ReadUTF8() {
+ int64_t val = ReadBits(8);
+ uint32_t top = (val & 0x80) >> 1;
+
+ if ((val & 0xc0) == 0x80 || val >= 0xFE) {
+ // error.
+ return -1;
+ }
+ while (val & top) {
+ int tmp = ReadBits(8) - 128;
+ if (tmp >> 6) {
+ // error.
+ return -1;
+ }
+ val = (val << 6) + tmp;
+ top <<= 5;
+ }
+ val &= (top << 1) - 1;
+ return val;
+}
+
+size_t BitReader::BitCount() const { return mOriginalBitSize - mTotalBitsLeft; }
+
+size_t BitReader::BitsLeft() const { return mTotalBitsLeft; }
+
+void BitReader::FillReservoir() {
+ if (mSize == 0) {
+ NS_ASSERTION(false, "Attempting to fill reservoir from past end of data");
+ return;
+ }
+
+ mReservoir = 0;
+ size_t i;
+ for (i = 0; mSize > 0 && i < 4; i++) {
+ mReservoir = (mReservoir << 8) | *mData;
+ mData++;
+ mSize--;
+ }
+
+ mNumBitsLeft = 8 * i;
+ mReservoir <<= 32 - mNumBitsLeft;
+}
+
+/* static */
+uint32_t BitReader::GetBitLength(const mozilla::MediaByteBuffer* aNAL) {
+ size_t size = aNAL->Length();
+
+ while (size > 0 && aNAL->ElementAt(size - 1) == 0) {
+ size--;
+ }
+
+ if (!size) {
+ return 0;
+ }
+
+ if (size > UINT32_MAX / 8) {
+ // We can't represent it, we'll use as much as we can.
+ return UINT32_MAX;
+ }
+
+ uint8_t v = aNAL->ElementAt(size - 1);
+ size *= 8;
+
+ // Remove the stop bit and following trailing zeros.
+ if (v) {
+ // Count the consecutive zero bits (trailing) on the right by binary search.
+ // Adapted from Matt Whitlock algorithm to only bother with 8 bits integers.
+ uint32_t c;
+ if (v & 1) {
+ // Special case for odd v (assumed to happen half of the time).
+ c = 0;
+ } else {
+ c = 1;
+ if ((v & 0xf) == 0) {
+ v >>= 4;
+ c += 4;
+ }
+ if ((v & 0x3) == 0) {
+ v >>= 2;
+ c += 2;
+ }
+ c -= v & 0x1;
+ }
+ size -= c + 1;
+ }
+ return size;
+}
+
+} // namespace mozilla
diff --git a/dom/media/BitReader.h b/dom/media/BitReader.h
new file mode 100644
index 0000000000..21c28f2c8c
--- /dev/null
+++ b/dom/media/BitReader.h
@@ -0,0 +1,54 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef BIT_READER_H_
+#define BIT_READER_H_
+
+#include "MediaData.h"
+
+namespace mozilla {
+
+class BitReader {
+ public:
+ explicit BitReader(const MediaByteBuffer* aBuffer);
+ BitReader(const MediaByteBuffer* aBuffer, size_t aBits);
+ BitReader(const uint8_t* aBuffer, size_t aBits);
+ ~BitReader();
+ uint32_t ReadBits(size_t aNum);
+ bool ReadBit() { return ReadBits(1) != 0; }
+ uint32_t ReadU32() { return ReadBits(32); }
+ uint64_t ReadU64();
+
+ // Read the UTF-8 sequence and convert it to its 64-bit UCS-4 encoded form.
+ // Return 0xfffffffffffffff if sequence was invalid.
+ uint64_t ReadUTF8();
+ // Read unsigned integer Exp-Golomb-coded.
+ uint32_t ReadUE();
+ // Read signed integer Exp-Golomb-coded.
+ int32_t ReadSE();
+ // Read unsigned integer Little Endian Base 128 coded.
+ // Limited to unsigned 64 bits.
+ CheckedUint64 ReadULEB128();
+
+ // Return the number of bits parsed so far;
+ size_t BitCount() const;
+ // Return the number of bits left.
+ size_t BitsLeft() const;
+
+ // Return RBSP bit length.
+ static uint32_t GetBitLength(const MediaByteBuffer* aNAL);
+
+ private:
+ void FillReservoir();
+ const uint8_t* mData;
+ const size_t mOriginalBitSize;
+ size_t mTotalBitsLeft;
+ size_t mSize; // Size left in bytes
+ uint32_t mReservoir; // Left-aligned bits
+ size_t mNumBitsLeft; // Number of bits left in reservoir.
+};
+
+} // namespace mozilla
+
+#endif // BIT_READER_H_
diff --git a/dom/media/BitWriter.cpp b/dom/media/BitWriter.cpp
new file mode 100644
index 0000000000..a52e49c106
--- /dev/null
+++ b/dom/media/BitWriter.cpp
@@ -0,0 +1,96 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "BitWriter.h"
+#include "MediaData.h"
+#include "mozilla/MathAlgorithms.h"
+
+namespace mozilla {
+
+constexpr uint8_t golombLen[256] = {
+ 1, 3, 3, 5, 5, 5, 5, 7, 7, 7, 7, 7, 7, 7, 7, 9, 9, 9, 9,
+ 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 11, 11, 11, 11, 11, 11, 11,
+ 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
+ 11, 11, 11, 11, 11, 11, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
+ 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
+ 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13,
+ 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 15, 15, 15, 15, 15, 15,
+ 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
+ 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
+ 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
+ 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
+ 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
+ 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
+ 15, 15, 15, 15, 15, 15, 15, 15, 17,
+};
+
+BitWriter::BitWriter(MediaByteBuffer* aBuffer) : mBuffer(aBuffer) {}
+
+BitWriter::~BitWriter() = default;
+
+void BitWriter::WriteBits(uint64_t aValue, size_t aBits) {
+ MOZ_ASSERT(aBits <= sizeof(uint64_t) * 8);
+
+ while (aBits) {
+ if (mBitIndex == 0) {
+ mBuffer->AppendElement(0);
+ }
+
+ const uint8_t clearMask = ~(~0u << (8 - mBitIndex));
+ uint8_t mask = 0;
+
+ if (mBitIndex + aBits > 8) {
+ // Not enough bits in the current byte to write all the bits
+ // required, we'll process what we can and continue with the left over.
+ const uint8_t leftOverBits = mBitIndex + aBits - 8;
+ const uint64_t leftOver = aValue & (~uint64_t(0) >> (8 - mBitIndex));
+ mask = aValue >> leftOverBits;
+
+ mBitIndex = 8;
+ aValue = leftOver;
+ aBits = leftOverBits;
+ } else {
+ const uint8_t offset = 8 - mBitIndex - aBits;
+ mask = aValue << offset;
+
+ mBitIndex += aBits;
+ aBits = 0;
+ }
+
+ mBuffer->ElementAt(mPosition) |= mask & clearMask;
+
+ if (mBitIndex == 8) {
+ mPosition++;
+ mBitIndex = 0;
+ }
+ }
+}
+
+void BitWriter::WriteUE(uint32_t aValue) {
+ MOZ_ASSERT(aValue <= (UINT32_MAX - 1));
+
+ if (aValue < 256) {
+ WriteBits(aValue + 1, golombLen[aValue]);
+ } else {
+ const uint32_t e = FloorLog2(aValue + 1);
+ WriteBits(aValue + 1, e * 2 + 1);
+ }
+}
+
+void BitWriter::WriteULEB128(uint64_t aValue) {
+ // See https://en.wikipedia.org/wiki/LEB128#Encode_unsigned_integer
+ do {
+ uint8_t byte = aValue & 0x7F;
+ aValue >>= 7;
+ WriteBit(aValue != 0);
+ WriteBits(byte, 7);
+ } while (aValue != 0);
+}
+
+void BitWriter::CloseWithRbspTrailing() {
+ WriteBit(true);
+ WriteBits(0, (8 - mBitIndex) & 7);
+}
+
+} // namespace mozilla
diff --git a/dom/media/BitWriter.h b/dom/media/BitWriter.h
new file mode 100644
index 0000000000..64e28c7ebc
--- /dev/null
+++ b/dom/media/BitWriter.h
@@ -0,0 +1,43 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef BIT_WRITER_H_
+#define BIT_WRITER_H_
+
+#include "mozilla/RefPtr.h"
+
+namespace mozilla {
+
+class MediaByteBuffer;
+
+class BitWriter {
+ public:
+ explicit BitWriter(MediaByteBuffer* aBuffer);
+ virtual ~BitWriter();
+ void WriteBits(uint64_t aValue, size_t aBits);
+ void WriteBit(bool aValue) { WriteBits(aValue, 1); }
+ void WriteU8(uint8_t aValue) { WriteBits(aValue, 8); }
+ void WriteU32(uint32_t aValue) { WriteBits(aValue, 32); }
+ void WriteU64(uint64_t aValue) { WriteBits(aValue, 64); }
+
+ // Write unsigned integer into Exp-Golomb-coded. 2^32-2 at most
+ void WriteUE(uint32_t aValue);
+ // Write unsigned integer Little Endian Base 128 coded.
+ void WriteULEB128(uint64_t aValue);
+
+ // Write RBSP trailing bits.
+ void CloseWithRbspTrailing();
+
+ // Return the number of bits written so far;
+ size_t BitCount() const { return mPosition * 8 + mBitIndex; }
+
+ private:
+ RefPtr<MediaByteBuffer> mBuffer;
+ size_t mPosition = 0;
+ uint8_t mBitIndex = 0;
+};
+
+} // namespace mozilla
+
+#endif // BIT_WRITER_H_
diff --git a/dom/media/BufferMediaResource.h b/dom/media/BufferMediaResource.h
new file mode 100644
index 0000000000..693704a2ae
--- /dev/null
+++ b/dom/media/BufferMediaResource.h
@@ -0,0 +1,76 @@
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(BufferMediaResource_h_)
+# define BufferMediaResource_h_
+
+# include "MediaResource.h"
+# include "nsISeekableStream.h"
+# include <algorithm>
+
+namespace mozilla {
+
+DDLoggedTypeDeclNameAndBase(BufferMediaResource, MediaResource);
+
+// A simple MediaResource based on an in memory buffer. This class accepts
+// the address and the length of the buffer, and simulates a read/seek API
+// on top of it. The Read implementation involves copying memory, which is
+// unfortunate, but the MediaResource interface mandates that.
+class BufferMediaResource
+ : public MediaResource,
+ public DecoderDoctorLifeLogger<BufferMediaResource> {
+ public:
+ BufferMediaResource(const uint8_t* aBuffer, uint32_t aLength)
+ : mBuffer(aBuffer), mLength(aLength) {}
+
+ protected:
+ virtual ~BufferMediaResource() = default;
+
+ private:
+ // These methods are called off the main thread.
+ nsresult ReadAt(int64_t aOffset, char* aBuffer, uint32_t aCount,
+ uint32_t* aBytes) override {
+ if (aOffset < 0 || aOffset > mLength) {
+ return NS_ERROR_FAILURE;
+ }
+ *aBytes = std::min(mLength - static_cast<uint32_t>(aOffset), aCount);
+ memcpy(aBuffer, mBuffer + aOffset, *aBytes);
+ return NS_OK;
+ }
+ // Memory-based and no locks, caching discouraged.
+ bool ShouldCacheReads() override { return false; }
+
+ void Pin() override {}
+ void Unpin() override {}
+ int64_t GetLength() override { return mLength; }
+ int64_t GetNextCachedData(int64_t aOffset) override { return aOffset; }
+ int64_t GetCachedDataEnd(int64_t aOffset) override {
+ return std::max(aOffset, int64_t(mLength));
+ }
+ bool IsDataCachedToEndOfResource(int64_t aOffset) override { return true; }
+ nsresult ReadFromCache(char* aBuffer, int64_t aOffset,
+ uint32_t aCount) override {
+ if (aOffset < 0) {
+ return NS_ERROR_FAILURE;
+ }
+
+ uint32_t bytes = std::min(mLength - static_cast<uint32_t>(aOffset), aCount);
+ memcpy(aBuffer, mBuffer + aOffset, bytes);
+ return NS_OK;
+ }
+
+ nsresult GetCachedRanges(MediaByteRangeSet& aRanges) override {
+ aRanges += MediaByteRange(0, int64_t(mLength));
+ return NS_OK;
+ }
+
+ private:
+ const uint8_t* mBuffer;
+ uint32_t mLength;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/BufferReader.h b/dom/media/BufferReader.h
new file mode 100644
index 0000000000..6cd332de53
--- /dev/null
+++ b/dom/media/BufferReader.h
@@ -0,0 +1,320 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef BUFFER_READER_H_
+#define BUFFER_READER_H_
+
+#include <string.h>
+#include "mozilla/EndianUtils.h"
+#include "nscore.h"
+#include "nsTArray.h"
+#include "MediaData.h"
+#include "MediaSpan.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Result.h"
+
+namespace mozilla {
+
+extern mozilla::LazyLogModule gMP4MetadataLog;
+
+class MOZ_RAII BufferReader {
+ public:
+ BufferReader() : mPtr(nullptr), mRemaining(0), mLength(0) {}
+ BufferReader(const uint8_t* aData, size_t aSize)
+ : mPtr(aData), mRemaining(aSize), mLength(aSize) {}
+ template <size_t S>
+ explicit BufferReader(const AutoTArray<uint8_t, S>& aData)
+ : mPtr(aData.Elements()),
+ mRemaining(aData.Length()),
+ mLength(aData.Length()) {}
+ explicit BufferReader(const nsTArray<uint8_t>& aData)
+ : mPtr(aData.Elements()),
+ mRemaining(aData.Length()),
+ mLength(aData.Length()) {}
+ explicit BufferReader(const mozilla::MediaByteBuffer* aData)
+ : mPtr(aData->Elements()),
+ mRemaining(aData->Length()),
+ mLength(aData->Length()) {}
+ explicit BufferReader(const mozilla::MediaSpan& aData)
+ : mPtr(aData.Elements()),
+ mRemaining(aData.Length()),
+ mLength(aData.Length()) {}
+
+ void SetData(const nsTArray<uint8_t>& aData) {
+ MOZ_ASSERT(!mPtr && !mRemaining);
+ mPtr = aData.Elements();
+ mRemaining = aData.Length();
+ mLength = mRemaining;
+ }
+
+ ~BufferReader() = default;
+
+ size_t Offset() const { return mLength - mRemaining; }
+
+ size_t Remaining() const { return mRemaining; }
+
+ mozilla::Result<uint8_t, nsresult> ReadU8() {
+ auto ptr = Read(1);
+ if (!ptr) {
+ MOZ_LOG(gMP4MetadataLog, mozilla::LogLevel::Error,
+ ("%s: failure", __func__));
+ return mozilla::Err(NS_ERROR_FAILURE);
+ }
+ return *ptr;
+ }
+
+ mozilla::Result<uint16_t, nsresult> ReadU16() {
+ auto ptr = Read(2);
+ if (!ptr) {
+ MOZ_LOG(gMP4MetadataLog, mozilla::LogLevel::Error,
+ ("%s: failure", __func__));
+ return mozilla::Err(NS_ERROR_FAILURE);
+ }
+ return mozilla::BigEndian::readUint16(ptr);
+ }
+
+ mozilla::Result<int16_t, nsresult> ReadLE16() {
+ auto ptr = Read(2);
+ if (!ptr) {
+ MOZ_LOG(gMP4MetadataLog, mozilla::LogLevel::Error,
+ ("%s: failure", __func__));
+ return mozilla::Err(NS_ERROR_FAILURE);
+ }
+ return mozilla::LittleEndian::readInt16(ptr);
+ }
+
+ mozilla::Result<uint32_t, nsresult> ReadU24() {
+ auto ptr = Read(3);
+ if (!ptr) {
+ MOZ_LOG(gMP4MetadataLog, mozilla::LogLevel::Error,
+ ("%s: failure", __func__));
+ return mozilla::Err(NS_ERROR_FAILURE);
+ }
+ return ptr[0] << 16 | ptr[1] << 8 | ptr[2];
+ }
+
+ mozilla::Result<int32_t, nsresult> Read24() {
+ return ReadU24().map([](uint32_t x) { return (int32_t)x; });
+ }
+
+ mozilla::Result<int32_t, nsresult> ReadLE24() {
+ auto ptr = Read(3);
+ if (!ptr) {
+ MOZ_LOG(gMP4MetadataLog, mozilla::LogLevel::Error,
+ ("%s: failure", __func__));
+ return mozilla::Err(NS_ERROR_FAILURE);
+ }
+ int32_t result = int32_t(ptr[2] << 16 | ptr[1] << 8 | ptr[0]);
+ if (result & 0x00800000u) {
+ result -= 0x1000000;
+ }
+ return result;
+ }
+
+ mozilla::Result<uint32_t, nsresult> ReadU32() {
+ auto ptr = Read(4);
+ if (!ptr) {
+ MOZ_LOG(gMP4MetadataLog, mozilla::LogLevel::Error,
+ ("%s: failure", __func__));
+ return mozilla::Err(NS_ERROR_FAILURE);
+ }
+ return mozilla::BigEndian::readUint32(ptr);
+ }
+
+ mozilla::Result<int32_t, nsresult> Read32() {
+ auto ptr = Read(4);
+ if (!ptr) {
+ MOZ_LOG(gMP4MetadataLog, mozilla::LogLevel::Error,
+ ("%s: failure", __func__));
+ return mozilla::Err(NS_ERROR_FAILURE);
+ }
+ return mozilla::BigEndian::readInt32(ptr);
+ }
+
+ mozilla::Result<uint32_t, nsresult> ReadLEU32() {
+ auto ptr = Read(4);
+ if (!ptr) {
+ MOZ_LOG(gMP4MetadataLog, mozilla::LogLevel::Error,
+ ("%s: failure", __func__));
+ return mozilla::Err(NS_ERROR_FAILURE);
+ }
+ return mozilla::LittleEndian::readUint32(ptr);
+ }
+
+ mozilla::Result<uint64_t, nsresult> ReadU64() {
+ auto ptr = Read(8);
+ if (!ptr) {
+ MOZ_LOG(gMP4MetadataLog, mozilla::LogLevel::Error,
+ ("%s: failure", __func__));
+ return mozilla::Err(NS_ERROR_FAILURE);
+ }
+ return mozilla::BigEndian::readUint64(ptr);
+ }
+
+ mozilla::Result<int64_t, nsresult> Read64() {
+ auto ptr = Read(8);
+ if (!ptr) {
+ MOZ_LOG(gMP4MetadataLog, mozilla::LogLevel::Error,
+ ("%s: failure", __func__));
+ return mozilla::Err(NS_ERROR_FAILURE);
+ }
+ return mozilla::BigEndian::readInt64(ptr);
+ }
+
+ const uint8_t* Read(size_t aCount) {
+ if (aCount > mRemaining) {
+ mPtr += mRemaining;
+ mRemaining = 0;
+ return nullptr;
+ }
+ mRemaining -= aCount;
+
+ const uint8_t* result = mPtr;
+ mPtr += aCount;
+
+ return result;
+ }
+
+ const uint8_t* Rewind(size_t aCount) {
+ MOZ_ASSERT(aCount <= Offset());
+ size_t rewind = Offset();
+ if (aCount < rewind) {
+ rewind = aCount;
+ }
+ mRemaining += rewind;
+ mPtr -= rewind;
+ return mPtr;
+ }
+
+ mozilla::Result<uint8_t, nsresult> PeekU8() const {
+ auto ptr = Peek(1);
+ if (!ptr) {
+ MOZ_LOG(gMP4MetadataLog, mozilla::LogLevel::Error,
+ ("%s: failure", __func__));
+ return mozilla::Err(NS_ERROR_FAILURE);
+ }
+ return *ptr;
+ }
+
+ mozilla::Result<uint16_t, nsresult> PeekU16() const {
+ auto ptr = Peek(2);
+ if (!ptr) {
+ MOZ_LOG(gMP4MetadataLog, mozilla::LogLevel::Error,
+ ("%s: failure", __func__));
+ return mozilla::Err(NS_ERROR_FAILURE);
+ }
+ return mozilla::BigEndian::readUint16(ptr);
+ }
+
+ mozilla::Result<uint32_t, nsresult> PeekU24() const {
+ auto ptr = Peek(3);
+ if (!ptr) {
+ MOZ_LOG(gMP4MetadataLog, mozilla::LogLevel::Error,
+ ("%s: failure", __func__));
+ return mozilla::Err(NS_ERROR_FAILURE);
+ }
+ return ptr[0] << 16 | ptr[1] << 8 | ptr[2];
+ }
+
+ mozilla::Result<int32_t, nsresult> Peek24() const {
+ return PeekU24().map([](uint32_t x) { return (int32_t)x; });
+ }
+
+ mozilla::Result<uint32_t, nsresult> PeekU32() {
+ auto ptr = Peek(4);
+ if (!ptr) {
+ MOZ_LOG(gMP4MetadataLog, mozilla::LogLevel::Error,
+ ("%s: failure", __func__));
+ return mozilla::Err(NS_ERROR_FAILURE);
+ }
+ return mozilla::BigEndian::readUint32(ptr);
+ }
+
+ const uint8_t* Peek(size_t aCount) const {
+ if (aCount > mRemaining) {
+ return nullptr;
+ }
+ return mPtr;
+ }
+
+ const uint8_t* Seek(size_t aOffset) {
+ if (aOffset >= mLength) {
+ MOZ_LOG(gMP4MetadataLog, mozilla::LogLevel::Error,
+ ("%s: failure, offset: %zu", __func__, aOffset));
+ return nullptr;
+ }
+
+ mPtr = mPtr - Offset() + aOffset;
+ mRemaining = mLength - aOffset;
+ return mPtr;
+ }
+
+ const uint8_t* Reset() {
+ mPtr -= Offset();
+ mRemaining = mLength;
+ return mPtr;
+ }
+
+ uint32_t Align() const { return 4 - ((intptr_t)mPtr & 3); }
+
+ template <typename T>
+ bool CanReadType() const {
+ return mRemaining >= sizeof(T);
+ }
+
+ template <typename T>
+ T ReadType() {
+ auto ptr = Read(sizeof(T));
+ if (!ptr) {
+ MOZ_LOG(gMP4MetadataLog, mozilla::LogLevel::Error,
+ ("%s: failure", __func__));
+ return 0;
+ }
+ // handle unaligned accesses by memcpying
+ T ret;
+ memcpy(&ret, ptr, sizeof(T));
+ return ret;
+ }
+
+ template <typename T>
+ [[nodiscard]] bool ReadArray(nsTArray<T>& aDest, size_t aLength) {
+ auto ptr = Read(aLength * sizeof(T));
+ if (!ptr) {
+ MOZ_LOG(gMP4MetadataLog, mozilla::LogLevel::Error,
+ ("%s: failure", __func__));
+ return false;
+ }
+
+ aDest.Clear();
+ aDest.AppendElements(reinterpret_cast<const T*>(ptr), aLength);
+ return true;
+ }
+
+ template <typename T>
+ [[nodiscard]] bool ReadArray(FallibleTArray<T>& aDest, size_t aLength) {
+ auto ptr = Read(aLength * sizeof(T));
+ if (!ptr) {
+ MOZ_LOG(gMP4MetadataLog, mozilla::LogLevel::Error,
+ ("%s: failure", __func__));
+ return false;
+ }
+
+ aDest.Clear();
+ if (!aDest.SetCapacity(aLength, mozilla::fallible)) {
+ return false;
+ }
+ MOZ_ALWAYS_TRUE(aDest.AppendElements(reinterpret_cast<const T*>(ptr),
+ aLength, mozilla::fallible));
+ return true;
+ }
+
+ private:
+ const uint8_t* mPtr;
+ size_t mRemaining;
+ size_t mLength;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/ByteWriter.h b/dom/media/ByteWriter.h
new file mode 100644
index 0000000000..cc7f5ecf3f
--- /dev/null
+++ b/dom/media/ByteWriter.h
@@ -0,0 +1,61 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef BYTE_WRITER_H_
+#define BYTE_WRITER_H_
+
+#include "mozilla/EndianUtils.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+
+template <typename Endianess>
+class ByteWriter {
+ public:
+ explicit ByteWriter(nsTArray<uint8_t>& aData) : mPtr(aData) {}
+ ~ByteWriter() = default;
+
+ [[nodiscard]] bool WriteU8(uint8_t aByte) { return Write(&aByte, 1); }
+
+ [[nodiscard]] bool WriteU16(uint16_t aShort) {
+ uint8_t c[2];
+ Endianess::writeUint16(&c[0], aShort);
+ return Write(&c[0], 2);
+ }
+
+ [[nodiscard]] bool WriteU32(uint32_t aLong) {
+ uint8_t c[4];
+ Endianess::writeUint32(&c[0], aLong);
+ return Write(&c[0], 4);
+ }
+
+ [[nodiscard]] bool Write32(int32_t aLong) {
+ uint8_t c[4];
+ Endianess::writeInt32(&c[0], aLong);
+ return Write(&c[0], 4);
+ }
+
+ [[nodiscard]] bool WriteU64(uint64_t aLongLong) {
+ uint8_t c[8];
+ Endianess::writeUint64(&c[0], aLongLong);
+ return Write(&c[0], 8);
+ }
+
+ [[nodiscard]] bool Write64(int64_t aLongLong) {
+ uint8_t c[8];
+ Endianess::writeInt64(&c[0], aLongLong);
+ return Write(&c[0], 8);
+ }
+
+ [[nodiscard]] bool Write(const uint8_t* aSrc, size_t aCount) {
+ return mPtr.AppendElements(aSrc, aCount, mozilla::fallible);
+ }
+
+ private:
+ nsTArray<uint8_t>& mPtr;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/CallbackThreadRegistry.cpp b/dom/media/CallbackThreadRegistry.cpp
new file mode 100644
index 0000000000..f4d2af5bd1
--- /dev/null
+++ b/dom/media/CallbackThreadRegistry.cpp
@@ -0,0 +1,101 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CallbackThreadRegistry.h"
+#include "mozilla/ClearOnShutdown.h"
+
+namespace mozilla {
+struct CallbackThreadRegistrySingleton {
+ CallbackThreadRegistrySingleton()
+ : mRegistry(MakeUnique<CallbackThreadRegistry>()) {
+ NS_DispatchToMainThread(
+ NS_NewRunnableFunction(__func__, [registry = &mRegistry] {
+ const auto phase = ShutdownPhase::XPCOMShutdownFinal;
+ MOZ_DIAGNOSTIC_ASSERT(!PastShutdownPhase(phase));
+ ClearOnShutdown(registry, phase);
+ }));
+ }
+
+ UniquePtr<CallbackThreadRegistry> mRegistry;
+};
+
+CallbackThreadRegistry::CallbackThreadRegistry()
+ : mThreadIds("CallbackThreadRegistry::mThreadIds") {}
+
+/* static */
+CallbackThreadRegistry* CallbackThreadRegistry::Get() {
+ static CallbackThreadRegistrySingleton sSingleton;
+ return sSingleton.mRegistry.get();
+}
+
+static bool CanLeak() {
+#ifdef NS_BUILD_REFCNT_LOGGING
+ static const bool logging =
+ getenv("XPCOM_MEM_LEAK_LOG") || getenv("XPCOM_MEM_BLOAT_LOG") ||
+ getenv("XPCOM_MEM_REFCNT_LOG") || getenv("XPCOM_MEM_ALLOC_LOG") ||
+ getenv("XPCOM_MEM_COMPTR_LOG");
+ return logging;
+#else
+ return false;
+#endif
+}
+
+void CallbackThreadRegistry::Register(ProfilerThreadId aThreadId,
+ const char* aName) {
+ if (!aThreadId.IsSpecified()) {
+ // profiler_current_thread_id is unspecified on unsupported platforms.
+ return;
+ }
+
+ if (CanLeak()) {
+ NS_WARNING(
+ "Not registering callback thread due to refcount logging; it may show "
+ "up as a leak of the TLS-backed nsThread wrapper if the thread "
+ "outlives xpcom shutdown.");
+ return;
+ }
+
+ auto threadIds = mThreadIds.Lock();
+ for (uint32_t i = 0; i < threadIds->Length(); i++) {
+ if ((*threadIds)[i].mId == aThreadId) {
+ (*threadIds)[i].mUserCount++;
+ return;
+ }
+ }
+ ThreadUserCount tuc;
+ tuc.mId = aThreadId;
+ tuc.mUserCount = 1;
+ threadIds->AppendElement(tuc);
+ PROFILER_REGISTER_THREAD(aName);
+}
+
+void CallbackThreadRegistry::Unregister(ProfilerThreadId aThreadId) {
+ if (!aThreadId.IsSpecified()) {
+ // profiler_current_thread_id is unspedified on unsupported platforms.
+ return;
+ }
+
+ if (CanLeak()) {
+ return;
+ }
+
+ auto threadIds = mThreadIds.Lock();
+ for (uint32_t i = 0; i < threadIds->Length(); i++) {
+ if ((*threadIds)[i].mId == aThreadId) {
+ MOZ_ASSERT((*threadIds)[i].mUserCount > 0);
+ (*threadIds)[i].mUserCount--;
+
+ if ((*threadIds)[i].mUserCount == 0) {
+ PROFILER_UNREGISTER_THREAD();
+ threadIds->RemoveElementAt(i);
+ }
+ return;
+ }
+ }
+ MOZ_ASSERT_UNREACHABLE("Current thread was not registered");
+}
+
+} // namespace mozilla
diff --git a/dom/media/CallbackThreadRegistry.h b/dom/media/CallbackThreadRegistry.h
new file mode 100644
index 0000000000..a154b3a35f
--- /dev/null
+++ b/dom/media/CallbackThreadRegistry.h
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef CALLBACKTHREADREGISTRY_H
+#define CALLBACKTHREADREGISTRY_H
+
+#include <cstdint>
+#include <mozilla/DataMutex.h>
+#include <nsTArray.h>
+#include <thread>
+#include <GeckoProfiler.h>
+
+namespace mozilla {
+
+// This class is a singleton that tracks various callback threads and makes
+// sure they are registered or unregistered to the profiler safely and
+// consistently.
+//
+// Register and Unregister are fairly expensive and shouldn't be used in a hot
+// path.
+class CallbackThreadRegistry final {
+ public:
+ CallbackThreadRegistry();
+
+ ~CallbackThreadRegistry() {
+ // It would be nice to be able to assert that all threads have been
+ // unregistered, but we can't: it's legal to suspend an audio stream, so
+ // that the callback isn't called, and then immediately destroy it.
+ }
+
+ CallbackThreadRegistry(const CallbackThreadRegistry&) = delete;
+ CallbackThreadRegistry& operator=(const CallbackThreadRegistry&) = delete;
+ CallbackThreadRegistry(CallbackThreadRegistry&&) = delete;
+ CallbackThreadRegistry& operator=(CallbackThreadRegistry&&) = delete;
+
+ // Returns the global instance of CallbackThreadRegistry. Safe from all
+ // threads.
+ static CallbackThreadRegistry* Get();
+
+ // This is intended to be called in the first callback of a callback
+ // thread.
+ void Register(ProfilerThreadId aThreadId, const char* aName);
+
+ // This is intended to be called when an object stops an audio callback thread
+ void Unregister(ProfilerThreadId aThreadId);
+
+ private:
+ struct ThreadUserCount {
+ ProfilerThreadId mId; // from profiler_current_thread_id
+ int mUserCount = 0;
+ };
+ DataMutex<nsTArray<ThreadUserCount>> mThreadIds;
+};
+
+} // namespace mozilla
+
+#endif // CALLBACKTHREADREGISTRY_H
diff --git a/dom/media/CanvasCaptureMediaStream.cpp b/dom/media/CanvasCaptureMediaStream.cpp
new file mode 100644
index 0000000000..a9d16b895b
--- /dev/null
+++ b/dom/media/CanvasCaptureMediaStream.cpp
@@ -0,0 +1,214 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CanvasCaptureMediaStream.h"
+
+#include "DOMMediaStream.h"
+#include "ImageContainer.h"
+#include "MediaTrackGraph.h"
+#include "Tracing.h"
+#include "VideoSegment.h"
+#include "gfxPlatform.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/dom/CanvasCaptureMediaStreamBinding.h"
+#include "mozilla/gfx/2D.h"
+#include "nsContentUtils.h"
+
+using namespace mozilla::layers;
+using namespace mozilla::gfx;
+
+namespace mozilla::dom {
+
+OutputStreamDriver::OutputStreamDriver(SourceMediaTrack* aSourceStream,
+ const PrincipalHandle& aPrincipalHandle)
+ : FrameCaptureListener(),
+ mSourceStream(aSourceStream),
+ mPrincipalHandle(aPrincipalHandle) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mSourceStream);
+}
+
+OutputStreamDriver::~OutputStreamDriver() {
+ MOZ_ASSERT(NS_IsMainThread());
+ EndTrack();
+}
+
+void OutputStreamDriver::EndTrack() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!mSourceStream->IsDestroyed()) {
+ mSourceStream->Destroy();
+ }
+}
+
+void OutputStreamDriver::SetImage(RefPtr<layers::Image>&& aImage,
+ const TimeStamp& aTime) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ VideoSegment segment;
+ const auto size = aImage->GetSize();
+ segment.AppendFrame(aImage.forget(), size, mPrincipalHandle, false, aTime);
+ mSourceStream->AppendData(&segment);
+}
+
+// ----------------------------------------------------------------------
+
+class TimerDriver : public OutputStreamDriver {
+ public:
+ explicit TimerDriver(SourceMediaTrack* aSourceStream, const double& aFPS,
+ const PrincipalHandle& aPrincipalHandle)
+ : OutputStreamDriver(aSourceStream, aPrincipalHandle),
+ mFrameInterval(aFPS == 0.0 ? TimeDuration::Forever()
+ : TimeDuration::FromSeconds(1.0 / aFPS)) {}
+
+ void RequestFrameCapture() override { mExplicitCaptureRequested = true; }
+
+ bool FrameCaptureRequested(const TimeStamp& aTime) const override {
+ if (mLastFrameTime.IsNull()) {
+ // All CanvasCaptureMediaStreams shall at least get one frame.
+ return true;
+ }
+
+ if (mExplicitCaptureRequested) {
+ return true;
+ }
+
+ if ((aTime - mLastFrameTime) >= mFrameInterval) {
+ return true;
+ }
+
+ return false;
+ }
+
+ void NewFrame(already_AddRefed<Image> aImage,
+ const TimeStamp& aTime) override {
+ nsCString str;
+ if (profiler_thread_is_being_profiled_for_markers()) {
+ TimeDuration sinceLast =
+ aTime - (mLastFrameTime.IsNull() ? aTime : mLastFrameTime);
+ str.AppendPrintf(
+ "TimerDriver %staking frame (%sexplicitly requested; after %.2fms; "
+ "interval cap %.2fms)",
+ sinceLast >= mFrameInterval ? "" : "NOT ",
+ mExplicitCaptureRequested ? "" : "NOT ", sinceLast.ToMilliseconds(),
+ mFrameInterval.ToMilliseconds());
+ }
+ AUTO_PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {}, str);
+
+ RefPtr<Image> image = aImage;
+
+ if (!FrameCaptureRequested(aTime)) {
+ return;
+ }
+
+ mLastFrameTime = aTime;
+ mExplicitCaptureRequested = false;
+ SetImage(std::move(image), aTime);
+ }
+
+ protected:
+ virtual ~TimerDriver() = default;
+
+ private:
+ const TimeDuration mFrameInterval;
+ bool mExplicitCaptureRequested = false;
+ TimeStamp mLastFrameTime;
+};
+
+// ----------------------------------------------------------------------
+
+class AutoDriver : public OutputStreamDriver {
+ public:
+ explicit AutoDriver(SourceMediaTrack* aSourceStream,
+ const PrincipalHandle& aPrincipalHandle)
+ : OutputStreamDriver(aSourceStream, aPrincipalHandle) {}
+
+ void RequestFrameCapture() override {}
+
+ bool FrameCaptureRequested(const TimeStamp& aTime) const override {
+ return true;
+ }
+
+ void NewFrame(already_AddRefed<Image> aImage,
+ const TimeStamp& aTime) override {
+ AUTO_PROFILER_MARKER_TEXT("Canvas CaptureStream", MEDIA_RT, {},
+ "AutoDriver taking frame"_ns);
+
+ RefPtr<Image> image = aImage;
+ SetImage(std::move(image), aTime);
+ }
+
+ protected:
+ virtual ~AutoDriver() = default;
+};
+
+// ----------------------------------------------------------------------
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(CanvasCaptureMediaStream, DOMMediaStream,
+ mCanvas)
+
+NS_IMPL_ADDREF_INHERITED(CanvasCaptureMediaStream, DOMMediaStream)
+NS_IMPL_RELEASE_INHERITED(CanvasCaptureMediaStream, DOMMediaStream)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CanvasCaptureMediaStream)
+NS_INTERFACE_MAP_END_INHERITING(DOMMediaStream)
+
+CanvasCaptureMediaStream::CanvasCaptureMediaStream(nsPIDOMWindowInner* aWindow,
+ HTMLCanvasElement* aCanvas)
+ : DOMMediaStream(aWindow), mCanvas(aCanvas) {}
+
+CanvasCaptureMediaStream::~CanvasCaptureMediaStream() = default;
+
+JSObject* CanvasCaptureMediaStream::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return dom::CanvasCaptureMediaStream_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void CanvasCaptureMediaStream::RequestFrame() {
+ if (mOutputStreamDriver) {
+ mOutputStreamDriver->RequestFrameCapture();
+ }
+}
+
+nsresult CanvasCaptureMediaStream::Init(const dom::Optional<double>& aFPS,
+ nsIPrincipal* aPrincipal) {
+ MediaTrackGraph* graph = MediaTrackGraph::GetInstance(
+ MediaTrackGraph::SYSTEM_THREAD_DRIVER, GetOwner(),
+ MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE,
+ MediaTrackGraph::DEFAULT_OUTPUT_DEVICE);
+ SourceMediaTrack* source = graph->CreateSourceTrack(MediaSegment::VIDEO);
+ PrincipalHandle principalHandle = MakePrincipalHandle(aPrincipal);
+ if (!aFPS.WasPassed()) {
+ mOutputStreamDriver = new AutoDriver(source, principalHandle);
+ } else if (aFPS.Value() < 0) {
+ return NS_ERROR_ILLEGAL_VALUE;
+ } else {
+ // Cap frame rate to 60 FPS for sanity
+ double fps = std::min(60.0, aFPS.Value());
+ mOutputStreamDriver = new TimerDriver(source, fps, principalHandle);
+ }
+ return NS_OK;
+}
+
+FrameCaptureListener* CanvasCaptureMediaStream::FrameCaptureListener() {
+ return mOutputStreamDriver;
+}
+
+void CanvasCaptureMediaStream::StopCapture() {
+ if (!mOutputStreamDriver) {
+ return;
+ }
+
+ mOutputStreamDriver->EndTrack();
+ mOutputStreamDriver = nullptr;
+}
+
+SourceMediaTrack* CanvasCaptureMediaStream::GetSourceStream() const {
+ if (!mOutputStreamDriver) {
+ return nullptr;
+ }
+ return mOutputStreamDriver->mSourceStream;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/CanvasCaptureMediaStream.h b/dom/media/CanvasCaptureMediaStream.h
new file mode 100644
index 0000000000..faa5972142
--- /dev/null
+++ b/dom/media/CanvasCaptureMediaStream.h
@@ -0,0 +1,132 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_CanvasCaptureMediaStream_h_
+#define mozilla_dom_CanvasCaptureMediaStream_h_
+
+#include "DOMMediaStream.h"
+#include "mozilla/dom/HTMLCanvasElement.h"
+#include "PrincipalHandle.h"
+
+class nsIPrincipal;
+
+namespace mozilla {
+class DOMMediaStream;
+class SourceMediaTrack;
+
+namespace layers {
+class Image;
+} // namespace layers
+
+namespace dom {
+class CanvasCaptureMediaStream;
+class HTMLCanvasElement;
+class OutputStreamFrameListener;
+
+/*
+ * The CanvasCaptureMediaStream is a MediaStream subclass that provides a video
+ * track containing frames from a canvas. See an architectural overview below.
+ *
+ * ----------------------------------------------------------------------------
+ * === Main Thread === __________________________
+ * | |
+ * | CanvasCaptureMediaStream |
+ * |__________________________|
+ * |
+ * | RequestFrame()
+ * v
+ * ________________________
+ * ________ FrameCaptureRequested? | |
+ * | | ------------------------> | OutputStreamDriver |
+ * | Canvas | SetFrameCapture() | (FrameCaptureListener) |
+ * |________| ------------------------> |________________________|
+ * |
+ * | SetImage() -
+ * | AppendToTrack()
+ * |
+ * v
+ * __________________________
+ * | |
+ * | MTG / SourceMediaTrack |
+ * |__________________________|
+ * ----------------------------------------------------------------------------
+ */
+
+/*
+ * Base class for drivers of the output stream.
+ * It is up to each sub class to implement the NewFrame() callback of
+ * FrameCaptureListener.
+ */
+class OutputStreamDriver : public FrameCaptureListener {
+ public:
+ OutputStreamDriver(SourceMediaTrack* aSourceStream,
+ const PrincipalHandle& aPrincipalHandle);
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(OutputStreamDriver);
+
+ /*
+ * Called from js' requestFrame() when it wants the next painted frame to be
+ * explicitly captured.
+ */
+ virtual void RequestFrameCapture() = 0;
+
+ /*
+ * Sub classes can SetImage() to update the image being appended to the
+ * output stream. It will be appended on the next NotifyPull from MTG.
+ */
+ void SetImage(RefPtr<layers::Image>&& aImage, const TimeStamp& aTime);
+
+ /*
+ * Ends the track in mSourceStream when we know there won't be any more images
+ * requested for it.
+ */
+ void EndTrack();
+
+ const RefPtr<SourceMediaTrack> mSourceStream;
+ const PrincipalHandle mPrincipalHandle;
+
+ protected:
+ virtual ~OutputStreamDriver();
+};
+
+class CanvasCaptureMediaStream : public DOMMediaStream {
+ public:
+ CanvasCaptureMediaStream(nsPIDOMWindowInner* aWindow,
+ HTMLCanvasElement* aCanvas);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(CanvasCaptureMediaStream,
+ DOMMediaStream)
+
+ nsresult Init(const dom::Optional<double>& aFPS, nsIPrincipal* aPrincipal);
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // WebIDL
+ HTMLCanvasElement* Canvas() const { return mCanvas; }
+ void RequestFrame();
+
+ dom::FrameCaptureListener* FrameCaptureListener();
+
+ /**
+ * Stops capturing for this stream at mCanvas.
+ */
+ void StopCapture();
+
+ SourceMediaTrack* GetSourceStream() const;
+
+ protected:
+ ~CanvasCaptureMediaStream();
+
+ private:
+ RefPtr<HTMLCanvasElement> mCanvas;
+ RefPtr<OutputStreamDriver> mOutputStreamDriver;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif /* mozilla_dom_CanvasCaptureMediaStream_h_ */
diff --git a/dom/media/ChannelMediaDecoder.cpp b/dom/media/ChannelMediaDecoder.cpp
new file mode 100644
index 0000000000..c6da221f94
--- /dev/null
+++ b/dom/media/ChannelMediaDecoder.cpp
@@ -0,0 +1,567 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ChannelMediaDecoder.h"
+#include "ChannelMediaResource.h"
+#include "DecoderTraits.h"
+#include "ExternalEngineStateMachine.h"
+#include "MediaDecoderStateMachine.h"
+#include "MediaFormatReader.h"
+#include "BaseMediaResource.h"
+#include "MediaShutdownManager.h"
+#include "base/process_util.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "VideoUtils.h"
+
+namespace mozilla {
+
+using TimeUnit = media::TimeUnit;
+
+extern LazyLogModule gMediaDecoderLog;
+#define LOG(x, ...) \
+ DDMOZ_LOG(gMediaDecoderLog, LogLevel::Debug, x, ##__VA_ARGS__)
+
+ChannelMediaDecoder::ResourceCallback::ResourceCallback(
+ AbstractThread* aMainThread)
+ : mAbstractMainThread(aMainThread) {
+ MOZ_ASSERT(aMainThread);
+ DecoderDoctorLogger::LogConstructionAndBase(
+ "ChannelMediaDecoder::ResourceCallback", this,
+ static_cast<const MediaResourceCallback*>(this));
+}
+
+ChannelMediaDecoder::ResourceCallback::~ResourceCallback() {
+ DecoderDoctorLogger::LogDestruction("ChannelMediaDecoder::ResourceCallback",
+ this);
+}
+
+void ChannelMediaDecoder::ResourceCallback::Connect(
+ ChannelMediaDecoder* aDecoder) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mDecoder = aDecoder;
+ DecoderDoctorLogger::LinkParentAndChild(
+ "ChannelMediaDecoder::ResourceCallback", this, "decoder", mDecoder);
+ mTimer = NS_NewTimer(mAbstractMainThread->AsEventTarget());
+}
+
+void ChannelMediaDecoder::ResourceCallback::Disconnect() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mDecoder) {
+ DecoderDoctorLogger::UnlinkParentAndChild(
+ "ChannelMediaDecoder::ResourceCallback", this, mDecoder);
+ mDecoder = nullptr;
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+}
+
+AbstractThread* ChannelMediaDecoder::ResourceCallback::AbstractMainThread()
+ const {
+ return mAbstractMainThread;
+}
+
+MediaDecoderOwner* ChannelMediaDecoder::ResourceCallback::GetMediaOwner()
+ const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mDecoder ? mDecoder->GetOwner() : nullptr;
+}
+
+void ChannelMediaDecoder::ResourceCallback::NotifyNetworkError(
+ const MediaResult& aError) {
+ MOZ_ASSERT(NS_IsMainThread());
+ DDLOGEX2("ChannelMediaDecoder::ResourceCallback", this, DDLogCategory::Log,
+ "network_error", aError);
+ if (mDecoder) {
+ mDecoder->NetworkError(aError);
+ }
+}
+
+/* static */
+void ChannelMediaDecoder::ResourceCallback::TimerCallback(nsITimer* aTimer,
+ void* aClosure) {
+ MOZ_ASSERT(NS_IsMainThread());
+ ResourceCallback* thiz = static_cast<ResourceCallback*>(aClosure);
+ MOZ_ASSERT(thiz->mDecoder);
+ thiz->mDecoder->NotifyReaderDataArrived();
+ thiz->mTimerArmed = false;
+}
+
+void ChannelMediaDecoder::ResourceCallback::NotifyDataArrived() {
+ MOZ_ASSERT(NS_IsMainThread());
+ DDLOGEX2("ChannelMediaDecoder::ResourceCallback", this, DDLogCategory::Log,
+ "data_arrived", true);
+
+ if (!mDecoder) {
+ return;
+ }
+
+ mDecoder->DownloadProgressed();
+
+ if (mTimerArmed) {
+ return;
+ }
+ // In situations where these notifications come from stochastic network
+ // activity, we can save significant computation by throttling the
+ // calls to MediaDecoder::NotifyDataArrived() which will update the buffer
+ // ranges of the reader.
+ mTimerArmed = true;
+ mTimer->InitWithNamedFuncCallback(
+ TimerCallback, this, sDelay, nsITimer::TYPE_ONE_SHOT,
+ "ChannelMediaDecoder::ResourceCallback::TimerCallback");
+}
+
+void ChannelMediaDecoder::ResourceCallback::NotifyDataEnded(nsresult aStatus) {
+ DDLOGEX2("ChannelMediaDecoder::ResourceCallback", this, DDLogCategory::Log,
+ "data_ended", aStatus);
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mDecoder) {
+ mDecoder->NotifyDownloadEnded(aStatus);
+ }
+}
+
+void ChannelMediaDecoder::ResourceCallback::NotifyPrincipalChanged() {
+ MOZ_ASSERT(NS_IsMainThread());
+ DDLOGEX2("ChannelMediaDecoder::ResourceCallback", this, DDLogCategory::Log,
+ "principal_changed", true);
+ if (mDecoder) {
+ mDecoder->NotifyPrincipalChanged();
+ }
+}
+
+void ChannelMediaDecoder::NotifyPrincipalChanged() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MediaDecoder::NotifyPrincipalChanged();
+ if (!mInitialChannelPrincipalKnown) {
+ // We'll receive one notification when the channel's initial principal
+ // is known, after all HTTP redirects have resolved. This isn't really a
+ // principal change, so return here to avoid the mSameOriginMedia check
+ // below.
+ mInitialChannelPrincipalKnown = true;
+ return;
+ }
+ if (!mSameOriginMedia) {
+ // Block mid-flight redirects to non CORS same origin destinations.
+ // See bugs 1441153, 1443942.
+ LOG("ChannnelMediaDecoder prohibited cross origin redirect blocked.");
+ NetworkError(MediaResult(NS_ERROR_DOM_BAD_URI,
+ "Prohibited cross origin redirect blocked"));
+ }
+}
+
+void ChannelMediaDecoder::ResourceCallback::NotifySuspendedStatusChanged(
+ bool aSuspendedByCache) {
+ MOZ_ASSERT(NS_IsMainThread());
+ DDLOGEX2("ChannelMediaDecoder::ResourceCallback", this, DDLogCategory::Log,
+ "suspended_status_changed", aSuspendedByCache);
+ MediaDecoderOwner* owner = GetMediaOwner();
+ if (owner) {
+ owner->NotifySuspendedByCache(aSuspendedByCache);
+ }
+}
+
+ChannelMediaDecoder::ChannelMediaDecoder(MediaDecoderInit& aInit)
+ : MediaDecoder(aInit),
+ mResourceCallback(
+ new ResourceCallback(aInit.mOwner->AbstractMainThread())) {
+ mResourceCallback->Connect(this);
+}
+
+/* static */
+already_AddRefed<ChannelMediaDecoder> ChannelMediaDecoder::Create(
+ MediaDecoderInit& aInit, DecoderDoctorDiagnostics* aDiagnostics) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RefPtr<ChannelMediaDecoder> decoder;
+ if (DecoderTraits::CanHandleContainerType(aInit.mContainerType,
+ aDiagnostics) != CANPLAY_NO) {
+ decoder = new ChannelMediaDecoder(aInit);
+ return decoder.forget();
+ }
+
+ return nullptr;
+}
+
+bool ChannelMediaDecoder::CanClone() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mResource && mResource->CanClone();
+}
+
+already_AddRefed<ChannelMediaDecoder> ChannelMediaDecoder::Clone(
+ MediaDecoderInit& aInit) {
+ if (!mResource || DecoderTraits::CanHandleContainerType(
+ aInit.mContainerType, nullptr) == CANPLAY_NO) {
+ return nullptr;
+ }
+ RefPtr<ChannelMediaDecoder> decoder = new ChannelMediaDecoder(aInit);
+ nsresult rv = decoder->Load(mResource);
+ if (NS_FAILED(rv)) {
+ decoder->Shutdown();
+ return nullptr;
+ }
+ return decoder.forget();
+}
+
+MediaDecoderStateMachineBase* ChannelMediaDecoder::CreateStateMachine(
+ bool aDisableExternalEngine) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MediaFormatReaderInit init;
+ init.mVideoFrameContainer = GetVideoFrameContainer();
+ init.mKnowsCompositor = GetCompositor();
+ init.mCrashHelper = GetOwner()->CreateGMPCrashHelper();
+ init.mFrameStats = mFrameStats;
+ init.mResource = mResource;
+ init.mMediaDecoderOwnerID = mOwner;
+ static Atomic<uint32_t> sTrackingIdCounter(0);
+ init.mTrackingId.emplace(TrackingId::Source::ChannelDecoder,
+ sTrackingIdCounter++,
+ TrackingId::TrackAcrossProcesses::Yes);
+ mReader = DecoderTraits::CreateReader(ContainerType(), init);
+
+#ifdef MOZ_WMF_MEDIA_ENGINE
+ // TODO : Only for testing development for now. In the future this should be
+ // used for encrypted content only.
+ if (StaticPrefs::media_wmf_media_engine_enabled() &&
+ StaticPrefs::media_wmf_media_engine_channel_decoder_enabled() &&
+ !aDisableExternalEngine) {
+ return new ExternalEngineStateMachine(this, mReader);
+ }
+#endif
+ return new MediaDecoderStateMachine(this, mReader);
+}
+
+void ChannelMediaDecoder::Shutdown() {
+ mResourceCallback->Disconnect();
+ MediaDecoder::Shutdown();
+
+ if (mResource) {
+ // Force any outstanding seek and byterange requests to complete
+ // to prevent shutdown from deadlocking.
+ mResourceClosePromise = mResource->Close();
+ }
+}
+
+void ChannelMediaDecoder::ShutdownInternal() {
+ if (!mResourceClosePromise) {
+ MediaShutdownManager::Instance().Unregister(this);
+ return;
+ }
+
+ mResourceClosePromise->Then(
+ AbstractMainThread(), __func__,
+ [self = RefPtr<ChannelMediaDecoder>(this)] {
+ MediaShutdownManager::Instance().Unregister(self);
+ });
+}
+
+nsresult ChannelMediaDecoder::Load(nsIChannel* aChannel,
+ bool aIsPrivateBrowsing,
+ nsIStreamListener** aStreamListener) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!mResource);
+ MOZ_ASSERT(aStreamListener);
+
+ mResource = BaseMediaResource::Create(mResourceCallback, aChannel,
+ aIsPrivateBrowsing);
+ if (!mResource) {
+ return NS_ERROR_FAILURE;
+ }
+ DDLINKCHILD("resource", mResource.get());
+
+ nsresult rv = MediaShutdownManager::Instance().Register(this);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = mResource->Open(aStreamListener);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return CreateAndInitStateMachine(mResource->IsLiveStream());
+}
+
+nsresult ChannelMediaDecoder::Load(BaseMediaResource* aOriginal) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!mResource);
+
+ mResource = aOriginal->CloneData(mResourceCallback);
+ if (!mResource) {
+ return NS_ERROR_FAILURE;
+ }
+ DDLINKCHILD("resource", mResource.get());
+
+ nsresult rv = MediaShutdownManager::Instance().Register(this);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ return CreateAndInitStateMachine(mResource->IsLiveStream());
+}
+
+void ChannelMediaDecoder::NotifyDownloadEnded(nsresult aStatus) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
+
+ LOG("NotifyDownloadEnded, status=%" PRIx32, static_cast<uint32_t>(aStatus));
+
+ if (NS_SUCCEEDED(aStatus)) {
+ // Download ends successfully. This is a stream with a finite length.
+ GetStateMachine()->DispatchIsLiveStream(false);
+ }
+
+ MediaDecoderOwner* owner = GetOwner();
+ if (NS_SUCCEEDED(aStatus) || aStatus == NS_BASE_STREAM_CLOSED) {
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ "ChannelMediaDecoder::UpdatePlaybackRate",
+ [stats = mPlaybackStatistics,
+ res = RefPtr<BaseMediaResource>(mResource), duration = mDuration]() {
+ auto rate = ComputePlaybackRate(stats, res,
+ duration.match(DurationToTimeUnit()));
+ UpdatePlaybackRate(rate, res);
+ });
+ nsresult rv = GetStateMachine()->OwnerThread()->Dispatch(r.forget());
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ owner->DownloadSuspended();
+ // NotifySuspendedStatusChanged will tell the element that download
+ // has been suspended "by the cache", which is true since we never
+ // download anything. The element can then transition to HAVE_ENOUGH_DATA.
+ owner->NotifySuspendedByCache(true);
+ } else if (aStatus == NS_BINDING_ABORTED) {
+ // Download has been cancelled by user.
+ owner->LoadAborted();
+ } else {
+ NetworkError(MediaResult(aStatus, "Download aborted"));
+ }
+}
+
+bool ChannelMediaDecoder::CanPlayThroughImpl() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mCanPlayThrough;
+}
+
+void ChannelMediaDecoder::OnPlaybackEvent(MediaPlaybackEvent&& aEvent) {
+ MOZ_ASSERT(NS_IsMainThread());
+ switch (aEvent.mType) {
+ case MediaPlaybackEvent::PlaybackStarted:
+ mPlaybackPosition = aEvent.mData.as<int64_t>();
+ mPlaybackStatistics.Start();
+ break;
+ case MediaPlaybackEvent::PlaybackProgressed: {
+ int64_t newPos = aEvent.mData.as<int64_t>();
+ mPlaybackStatistics.AddBytes(newPos - mPlaybackPosition);
+ mPlaybackPosition = newPos;
+ break;
+ }
+ case MediaPlaybackEvent::PlaybackStopped: {
+ int64_t newPos = aEvent.mData.as<int64_t>();
+ mPlaybackStatistics.AddBytes(newPos - mPlaybackPosition);
+ mPlaybackPosition = newPos;
+ mPlaybackStatistics.Stop();
+ break;
+ }
+ default:
+ break;
+ }
+ MediaDecoder::OnPlaybackEvent(std::move(aEvent));
+}
+
+void ChannelMediaDecoder::DurationChanged() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MediaDecoder::DurationChanged();
+ // Duration has changed so we should recompute playback rate
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ "ChannelMediaDecoder::UpdatePlaybackRate",
+ [stats = mPlaybackStatistics, res = RefPtr<BaseMediaResource>(mResource),
+ duration = mDuration]() {
+ auto rate = ComputePlaybackRate(stats, res,
+ duration.match(DurationToTimeUnit()));
+ UpdatePlaybackRate(rate, res);
+ });
+ nsresult rv = GetStateMachine()->OwnerThread()->Dispatch(r.forget());
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+}
+
+void ChannelMediaDecoder::DownloadProgressed() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
+
+ GetOwner()->DownloadProgressed();
+
+ using StatsPromise = MozPromise<MediaStatistics, bool, true>;
+ InvokeAsync(GetStateMachine()->OwnerThread(), __func__,
+ [playbackStats = mPlaybackStatistics,
+ res = RefPtr<BaseMediaResource>(mResource), duration = mDuration,
+ pos = mPlaybackPosition]() {
+ auto rate = ComputePlaybackRate(
+ playbackStats, res, duration.match(DurationToTimeUnit()));
+ UpdatePlaybackRate(rate, res);
+ MediaStatistics stats = GetStatistics(rate, res, pos);
+ return StatsPromise::CreateAndResolve(stats, __func__);
+ })
+ ->Then(
+ mAbstractMainThread, __func__,
+ [=,
+ self = RefPtr<ChannelMediaDecoder>(this)](MediaStatistics aStats) {
+ if (IsShutdown()) {
+ return;
+ }
+ mCanPlayThrough = aStats.CanPlayThrough();
+ GetStateMachine()->DispatchCanPlayThrough(mCanPlayThrough);
+ mResource->ThrottleReadahead(ShouldThrottleDownload(aStats));
+ // Update readyState since mCanPlayThrough might have changed.
+ GetOwner()->UpdateReadyState();
+ },
+ []() { MOZ_ASSERT_UNREACHABLE("Promise not resolved"); });
+}
+
+/* static */ ChannelMediaDecoder::PlaybackRateInfo
+ChannelMediaDecoder::ComputePlaybackRate(const MediaChannelStatistics& aStats,
+ BaseMediaResource* aResource,
+ const TimeUnit& aDuration) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ int64_t length = aResource->GetLength();
+ if (aDuration.IsValid() && !aDuration.IsInfinite() &&
+ aDuration.IsPositive() && length >= 0 &&
+ length / aDuration.ToSeconds() < UINT32_MAX) {
+ return {uint32_t(length / aDuration.ToSeconds()), true};
+ }
+
+ bool reliable = false;
+ uint32_t rate = aStats.GetRate(&reliable);
+ return {rate, reliable};
+}
+
+/* static */
+void ChannelMediaDecoder::UpdatePlaybackRate(const PlaybackRateInfo& aInfo,
+ BaseMediaResource* aResource) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ uint32_t rate = aInfo.mRate;
+
+ if (aInfo.mReliable) {
+ // Avoid passing a zero rate
+ rate = std::max(rate, 1u);
+ } else {
+ // Set a minimum rate of 10,000 bytes per second ... sometimes we just
+ // don't have good data
+ rate = std::max(rate, 10000u);
+ }
+
+ aResource->SetPlaybackRate(rate);
+}
+
+/* static */
+MediaStatistics ChannelMediaDecoder::GetStatistics(
+ const PlaybackRateInfo& aInfo, BaseMediaResource* aRes,
+ int64_t aPlaybackPosition) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ MediaStatistics result;
+ result.mDownloadRate = aRes->GetDownloadRate(&result.mDownloadRateReliable);
+ result.mDownloadPosition = aRes->GetCachedDataEnd(aPlaybackPosition);
+ result.mTotalBytes = aRes->GetLength();
+ result.mPlaybackRate = aInfo.mRate;
+ result.mPlaybackRateReliable = aInfo.mReliable;
+ result.mPlaybackPosition = aPlaybackPosition;
+ return result;
+}
+
+bool ChannelMediaDecoder::ShouldThrottleDownload(
+ const MediaStatistics& aStats) {
+ // We throttle the download if either the throttle override pref is set
+ // (so that we always throttle at the readahead limit on mobile if using
+ // a cellular network) or if the download is fast enough that there's no
+ // concern about playback being interrupted.
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_TRUE(GetStateMachine(), false);
+
+ int64_t length = aStats.mTotalBytes;
+ if (length > 0 &&
+ length <= int64_t(StaticPrefs::media_memory_cache_max_size()) * 1024) {
+ // Don't throttle the download of small resources. This is to speed
+ // up seeking, as seeks into unbuffered ranges would require starting
+ // up a new HTTP transaction, which adds latency.
+ return false;
+ }
+
+ if (OnCellularConnection() &&
+ Preferences::GetBool(
+ "media.throttle-cellular-regardless-of-download-rate", false)) {
+ return true;
+ }
+
+ if (!aStats.mDownloadRateReliable || !aStats.mPlaybackRateReliable) {
+ return false;
+ }
+ uint32_t factor =
+ std::max(2u, Preferences::GetUint("media.throttle-factor", 2));
+ return aStats.mDownloadRate > factor * aStats.mPlaybackRate;
+}
+
+void ChannelMediaDecoder::AddSizeOfResources(ResourceSizes* aSizes) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mResource) {
+ aSizes->mByteSize += mResource->SizeOfIncludingThis(aSizes->mMallocSizeOf);
+ }
+}
+
+already_AddRefed<nsIPrincipal> ChannelMediaDecoder::GetCurrentPrincipal() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mResource ? mResource->GetCurrentPrincipal() : nullptr;
+}
+
+bool ChannelMediaDecoder::HadCrossOriginRedirects() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mResource ? mResource->HadCrossOriginRedirects() : false;
+}
+
+bool ChannelMediaDecoder::IsTransportSeekable() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mResource->IsTransportSeekable();
+}
+
+void ChannelMediaDecoder::SetLoadInBackground(bool aLoadInBackground) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mResource) {
+ mResource->SetLoadInBackground(aLoadInBackground);
+ }
+}
+
+void ChannelMediaDecoder::Suspend() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mResource) {
+ mResource->Suspend(true);
+ }
+ MediaDecoder::Suspend();
+}
+
+void ChannelMediaDecoder::Resume() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mResource) {
+ mResource->Resume();
+ }
+ MediaDecoder::Resume();
+}
+
+void ChannelMediaDecoder::MetadataLoaded(
+ UniquePtr<MediaInfo> aInfo, UniquePtr<MetadataTags> aTags,
+ MediaDecoderEventVisibility aEventVisibility) {
+ MediaDecoder::MetadataLoaded(std::move(aInfo), std::move(aTags),
+ aEventVisibility);
+ // Set mode to PLAYBACK after reading metadata.
+ mResource->SetReadMode(MediaCacheStream::MODE_PLAYBACK);
+}
+
+void ChannelMediaDecoder::GetDebugInfo(dom::MediaDecoderDebugInfo& aInfo) {
+ MediaDecoder::GetDebugInfo(aInfo);
+ if (mResource) {
+ mResource->GetDebugInfo(aInfo.mResource);
+ }
+}
+
+} // namespace mozilla
+
+// avoid redefined macro in unified build
+#undef LOG
diff --git a/dom/media/ChannelMediaDecoder.h b/dom/media/ChannelMediaDecoder.h
new file mode 100644
index 0000000000..47bf5a08b9
--- /dev/null
+++ b/dom/media/ChannelMediaDecoder.h
@@ -0,0 +1,169 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef ChannelMediaDecoder_h_
+#define ChannelMediaDecoder_h_
+
+#include "MediaDecoder.h"
+#include "MediaResourceCallback.h"
+#include "MediaChannelStatistics.h"
+
+class nsIChannel;
+class nsIStreamListener;
+
+namespace mozilla {
+
+class BaseMediaResource;
+
+DDLoggedTypeDeclNameAndBase(ChannelMediaDecoder, MediaDecoder);
+
+class ChannelMediaDecoder
+ : public MediaDecoder,
+ public DecoderDoctorLifeLogger<ChannelMediaDecoder> {
+ // Used to register with MediaResource to receive notifications which will
+ // be forwarded to MediaDecoder.
+ class ResourceCallback : public MediaResourceCallback {
+ // Throttle calls to MediaDecoder::NotifyDataArrived()
+ // to be at most once per 500ms.
+ static const uint32_t sDelay = 500;
+
+ public:
+ explicit ResourceCallback(AbstractThread* aMainThread);
+ // Start to receive notifications from ResourceCallback.
+ void Connect(ChannelMediaDecoder* aDecoder);
+ // Called upon shutdown to stop receiving notifications.
+ void Disconnect();
+
+ private:
+ ~ResourceCallback();
+
+ /* MediaResourceCallback functions */
+ AbstractThread* AbstractMainThread() const override;
+ MediaDecoderOwner* GetMediaOwner() const override;
+ void NotifyNetworkError(const MediaResult& aError) override;
+ void NotifyDataArrived() override;
+ void NotifyDataEnded(nsresult aStatus) override;
+ void NotifyPrincipalChanged() override;
+ void NotifySuspendedStatusChanged(bool aSuspendedByCache) override;
+
+ static void TimerCallback(nsITimer* aTimer, void* aClosure);
+
+ // The decoder to send notifications. Main-thread only.
+ ChannelMediaDecoder* mDecoder = nullptr;
+ nsCOMPtr<nsITimer> mTimer;
+ bool mTimerArmed = false;
+ const RefPtr<AbstractThread> mAbstractMainThread;
+ };
+
+ protected:
+ void ShutdownInternal() override;
+ void OnPlaybackEvent(MediaPlaybackEvent&& aEvent) override;
+ void DurationChanged() override;
+ void MetadataLoaded(UniquePtr<MediaInfo> aInfo, UniquePtr<MetadataTags> aTags,
+ MediaDecoderEventVisibility aEventVisibility) override;
+ void NotifyPrincipalChanged() override;
+
+ RefPtr<ResourceCallback> mResourceCallback;
+ RefPtr<BaseMediaResource> mResource;
+
+ explicit ChannelMediaDecoder(MediaDecoderInit& aInit);
+
+ void GetDebugInfo(dom::MediaDecoderDebugInfo& aInfo);
+
+ public:
+ // Create a decoder for the given aType. Returns null if we were unable
+ // to create the decoder, for example because the requested MIME type in
+ // the init struct was unsupported.
+ static already_AddRefed<ChannelMediaDecoder> Create(
+ MediaDecoderInit& aInit, DecoderDoctorDiagnostics* aDiagnostics);
+
+ void Shutdown() override;
+
+ bool CanClone();
+
+ // Create a new decoder of the same type as this one.
+ already_AddRefed<ChannelMediaDecoder> Clone(MediaDecoderInit& aInit);
+
+ nsresult Load(nsIChannel* aChannel, bool aIsPrivateBrowsing,
+ nsIStreamListener** aStreamListener);
+
+ void AddSizeOfResources(ResourceSizes* aSizes) override;
+ already_AddRefed<nsIPrincipal> GetCurrentPrincipal() override;
+ bool HadCrossOriginRedirects() override;
+ bool IsTransportSeekable() override;
+ void SetLoadInBackground(bool aLoadInBackground) override;
+ void Suspend() override;
+ void Resume() override;
+
+ private:
+ void DownloadProgressed();
+
+ // Create a new state machine to run this decoder.
+ MediaDecoderStateMachineBase* CreateStateMachine(
+ bool aDisableExternalEngine) override;
+
+ nsresult Load(BaseMediaResource* aOriginal);
+
+ // Called by MediaResource when the download has ended.
+ // Called on the main thread only. aStatus is the result from OnStopRequest.
+ void NotifyDownloadEnded(nsresult aStatus);
+
+ // Called by the MediaResource to keep track of the number of bytes read
+ // from the resource. Called on the main by an event runner dispatched
+ // by the MediaResource read functions.
+ void NotifyBytesConsumed(int64_t aBytes, int64_t aOffset);
+
+ bool CanPlayThroughImpl() final;
+
+ struct PlaybackRateInfo {
+ uint32_t mRate; // Estimate of the current playback rate (bytes/second).
+ bool mReliable; // True if mRate is a reliable estimate.
+ };
+ // The actual playback rate computation.
+ static PlaybackRateInfo ComputePlaybackRate(
+ const MediaChannelStatistics& aStats, BaseMediaResource* aResource,
+ const media::TimeUnit& aDuration);
+
+ // Something has changed that could affect the computed playback rate,
+ // so recompute it.
+ static void UpdatePlaybackRate(const PlaybackRateInfo& aInfo,
+ BaseMediaResource* aResource);
+
+ // Return statistics. This is used for progress events and other things.
+ // This can be called from any thread. It's only a snapshot of the
+ // current state, since other threads might be changing the state
+ // at any time.
+ static MediaStatistics GetStatistics(const PlaybackRateInfo& aInfo,
+ BaseMediaResource* aRes,
+ int64_t aPlaybackPosition);
+
+ bool ShouldThrottleDownload(const MediaStatistics& aStats);
+
+ // Data needed to estimate playback data rate. The timeline used for
+ // this estimate is "decode time" (where the "current time" is the
+ // time of the last decoded video frame).
+ MediaChannelStatistics mPlaybackStatistics;
+
+ // Current playback position in the stream. This is (approximately)
+ // where we're up to playing back the stream. This is not adjusted
+ // during decoder seek operations, but it's updated at the end when we
+ // start playing back again.
+ int64_t mPlaybackPosition = 0;
+
+ bool mCanPlayThrough = false;
+
+ // True if we've been notified that the ChannelMediaResource has
+ // a principal.
+ bool mInitialChannelPrincipalKnown = false;
+
+ // Set in Shutdown() when we start closing mResource, if mResource is set.
+ // Must resolve before we unregister the shutdown blocker.
+ RefPtr<GenericPromise> mResourceClosePromise;
+};
+
+} // namespace mozilla
+
+#endif // ChannelMediaDecoder_h_
diff --git a/dom/media/ChannelMediaResource.cpp b/dom/media/ChannelMediaResource.cpp
new file mode 100644
index 0000000000..e249ec36db
--- /dev/null
+++ b/dom/media/ChannelMediaResource.cpp
@@ -0,0 +1,1053 @@
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ChannelMediaResource.h"
+
+#include "mozilla/Preferences.h"
+#include "mozilla/dom/HTMLMediaElement.h"
+#include "mozilla/net/OpaqueResponseUtils.h"
+#include "nsIAsyncVerifyRedirectCallback.h"
+#include "nsICachingChannel.h"
+#include "nsIClassOfService.h"
+#include "nsIHttpChannel.h"
+#include "nsIInputStream.h"
+#include "nsIThreadRetargetableRequest.h"
+#include "nsITimedChannel.h"
+#include "nsHttp.h"
+#include "nsNetUtil.h"
+
+static const uint32_t HTTP_PARTIAL_RESPONSE_CODE = 206;
+static const uint32_t HTTP_OK_CODE = 200;
+static const uint32_t HTTP_REQUESTED_RANGE_NOT_SATISFIABLE_CODE = 416;
+
+mozilla::LazyLogModule gMediaResourceLog("MediaResource");
+// Debug logging macro with object pointer and class name.
+#define LOG(msg, ...) \
+ DDMOZ_LOG(gMediaResourceLog, mozilla::LogLevel::Debug, msg, ##__VA_ARGS__)
+
+namespace mozilla {
+
+ChannelMediaResource::ChannelMediaResource(MediaResourceCallback* aCallback,
+ nsIChannel* aChannel, nsIURI* aURI,
+ int64_t aStreamLength,
+ bool aIsPrivateBrowsing)
+ : BaseMediaResource(aCallback, aChannel, aURI),
+ mCacheStream(this, aIsPrivateBrowsing),
+ mSuspendAgent(mCacheStream),
+ mKnownStreamLength(aStreamLength) {}
+
+ChannelMediaResource::~ChannelMediaResource() {
+ MOZ_ASSERT(mClosed);
+ MOZ_ASSERT(!mChannel);
+ MOZ_ASSERT(!mListener);
+ if (mSharedInfo) {
+ mSharedInfo->mResources.RemoveElement(this);
+ }
+}
+
+// ChannelMediaResource::Listener just observes the channel and
+// forwards notifications to the ChannelMediaResource. We use multiple
+// listener objects so that when we open a new stream for a seek we can
+// disconnect the old listener from the ChannelMediaResource and hook up
+// a new listener, so notifications from the old channel are discarded
+// and don't confuse us.
+NS_IMPL_ISUPPORTS(ChannelMediaResource::Listener, nsIRequestObserver,
+ nsIStreamListener, nsIChannelEventSink, nsIInterfaceRequestor,
+ nsIThreadRetargetableStreamListener)
+
+nsresult ChannelMediaResource::Listener::OnStartRequest(nsIRequest* aRequest) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!mResource) return NS_OK;
+ return mResource->OnStartRequest(aRequest, mOffset);
+}
+
+nsresult ChannelMediaResource::Listener::OnStopRequest(nsIRequest* aRequest,
+ nsresult aStatus) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!mResource) return NS_OK;
+ return mResource->OnStopRequest(aRequest, aStatus);
+}
+
+nsresult ChannelMediaResource::Listener::OnDataAvailable(
+ nsIRequest* aRequest, nsIInputStream* aStream, uint64_t aOffset,
+ uint32_t aCount) {
+ // This might happen off the main thread.
+ RefPtr<ChannelMediaResource> res;
+ {
+ MutexAutoLock lock(mMutex);
+ res = mResource;
+ }
+ // Note Rekove() might happen at the same time to reset mResource. We check
+ // the load ID to determine if the data is from an old channel.
+ return res ? res->OnDataAvailable(mLoadID, aStream, aCount) : NS_OK;
+}
+
+nsresult ChannelMediaResource::Listener::AsyncOnChannelRedirect(
+ nsIChannel* aOld, nsIChannel* aNew, uint32_t aFlags,
+ nsIAsyncVerifyRedirectCallback* cb) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsresult rv = NS_OK;
+ if (mResource) {
+ rv = mResource->OnChannelRedirect(aOld, aNew, aFlags, mOffset);
+ }
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ cb->OnRedirectVerifyCallback(NS_OK);
+ return NS_OK;
+}
+
+nsresult ChannelMediaResource::Listener::CheckListenerChain() { return NS_OK; }
+
+nsresult ChannelMediaResource::Listener::GetInterface(const nsIID& aIID,
+ void** aResult) {
+ return QueryInterface(aIID, aResult);
+}
+
+void ChannelMediaResource::Listener::Revoke() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MutexAutoLock lock(mMutex);
+ mResource = nullptr;
+}
+
+static bool IsPayloadCompressed(nsIHttpChannel* aChannel) {
+ nsAutoCString encoding;
+ Unused << aChannel->GetResponseHeader("Content-Encoding"_ns, encoding);
+ return encoding.Length() > 0;
+}
+
+nsresult ChannelMediaResource::OnStartRequest(nsIRequest* aRequest,
+ int64_t aRequestOffset) {
+ NS_ASSERTION(mChannel.get() == aRequest, "Wrong channel!");
+ MOZ_DIAGNOSTIC_ASSERT(!mClosed);
+
+ MediaDecoderOwner* owner = mCallback->GetMediaOwner();
+ MOZ_DIAGNOSTIC_ASSERT(owner);
+ dom::HTMLMediaElement* element = owner->GetMediaElement();
+ MOZ_DIAGNOSTIC_ASSERT(element);
+
+ nsresult status;
+ nsresult rv = aRequest->GetStatus(&status);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (status == NS_BINDING_ABORTED) {
+ // Request was aborted before we had a chance to receive any data, or
+ // even an OnStartRequest(). Close the channel. This is important, as
+ // we don't want to mess up our state, as if we're cloned that would
+ // cause the clone to copy incorrect metadata (like whether we're
+ // infinite for example).
+ CloseChannel();
+ return status;
+ }
+
+ if (element->ShouldCheckAllowOrigin()) {
+ // If the request was cancelled by nsCORSListenerProxy due to failing
+ // the CORS security check, send an error through to the media element.
+ if (status == NS_ERROR_DOM_BAD_URI) {
+ mCallback->NotifyNetworkError(MediaResult(status, "CORS not allowed"));
+ return NS_ERROR_DOM_BAD_URI;
+ }
+ }
+
+ nsCOMPtr<nsIHttpChannel> hc = do_QueryInterface(aRequest);
+ bool seekable = false;
+ int64_t length = -1;
+ int64_t startOffset = aRequestOffset;
+
+ if (hc) {
+ uint32_t responseStatus = 0;
+ Unused << hc->GetResponseStatus(&responseStatus);
+ bool succeeded = false;
+ Unused << hc->GetRequestSucceeded(&succeeded);
+
+ if (!succeeded && NS_SUCCEEDED(status)) {
+ // HTTP-level error (e.g. 4xx); treat this as a fatal network-level error.
+ // We might get this on a seek.
+ // (Note that lower-level errors indicated by NS_FAILED(status) are
+ // handled in OnStopRequest.)
+ // A 416 error should treated as EOF here... it's possible
+ // that we don't get Content-Length, we read N bytes, then we
+ // suspend and resume, the resume reopens the channel and we seek to
+ // offset N, but there are no more bytes, so we get a 416
+ // "Requested Range Not Satisfiable".
+ if (responseStatus == HTTP_REQUESTED_RANGE_NOT_SATISFIABLE_CODE) {
+ // OnStopRequest will not be fired, so we need to do some of its
+ // work here. Note we need to pass the load ID first so the following
+ // NotifyDataEnded() can pass the ID check.
+ mCacheStream.NotifyLoadID(mLoadID);
+ mCacheStream.NotifyDataEnded(mLoadID, status);
+ } else {
+ mCallback->NotifyNetworkError(
+ MediaResult(NS_ERROR_FAILURE, "HTTP error"));
+ }
+
+ // This disconnects our listener so we don't get any more data. We
+ // certainly don't want an error page to end up in our cache!
+ CloseChannel();
+ return NS_OK;
+ }
+
+ nsAutoCString ranges;
+ Unused << hc->GetResponseHeader("Accept-Ranges"_ns, ranges);
+ bool acceptsRanges =
+ net::nsHttp::FindToken(ranges.get(), "bytes", HTTP_HEADER_VALUE_SEPS);
+
+ int64_t contentLength = -1;
+ const bool isCompressed = IsPayloadCompressed(hc);
+ if (!isCompressed) {
+ hc->GetContentLength(&contentLength);
+ }
+
+ // Check response code for byte-range requests (seeking, chunk requests).
+ // We don't expect to get a 206 response for a compressed stream, but
+ // double check just to be sure.
+ if (!isCompressed && responseStatus == HTTP_PARTIAL_RESPONSE_CODE) {
+ // Parse Content-Range header.
+ int64_t rangeStart = 0;
+ int64_t rangeEnd = 0;
+ int64_t rangeTotal = 0;
+ rv = ParseContentRangeHeader(hc, rangeStart, rangeEnd, rangeTotal);
+
+ // We received 'Content-Range', so the server accepts range requests.
+ bool gotRangeHeader = NS_SUCCEEDED(rv);
+
+ if (gotRangeHeader) {
+ startOffset = rangeStart;
+ // We received 'Content-Range', so the server accepts range requests.
+ // Notify media cache about the length and start offset of data
+ // received. Note: If aRangeTotal == -1, then the total bytes is unknown
+ // at this stage.
+ // For now, tell the decoder that the stream is infinite.
+ if (rangeTotal != -1) {
+ contentLength = std::max(contentLength, rangeTotal);
+ }
+ }
+ acceptsRanges = gotRangeHeader;
+ } else if (responseStatus == HTTP_OK_CODE) {
+ // HTTP_OK_CODE means data will be sent from the start of the stream.
+ startOffset = 0;
+
+ if (aRequestOffset > 0) {
+ // If HTTP_OK_CODE is responded for a non-zero range request, we have
+ // to assume seeking doesn't work.
+ acceptsRanges = false;
+ }
+ }
+ if (aRequestOffset == 0 && contentLength >= 0 &&
+ (responseStatus == HTTP_OK_CODE ||
+ responseStatus == HTTP_PARTIAL_RESPONSE_CODE)) {
+ length = contentLength;
+ }
+ // XXX we probably should examine the Content-Range header in case
+ // the server gave us a range which is not quite what we asked for
+
+ // If we get an HTTP_OK_CODE response to our byte range request,
+ // and the server isn't sending Accept-Ranges:bytes then we don't
+ // support seeking. We also can't seek in compressed streams.
+ seekable = !isCompressed && acceptsRanges;
+ } else {
+ // Not an HTTP channel. Assume data will be sent from position zero.
+ startOffset = 0;
+ }
+
+ // Update principals before OnDataAvailable() putting the data in the cache.
+ // This is important, we want to make sure all principals are updated before
+ // any consumer can see the new data.
+ UpdatePrincipal();
+ if (owner->HasError()) {
+ // Updating the principal resulted in an error. Abort the load.
+ CloseChannel();
+ return NS_OK;
+ }
+
+ mCacheStream.NotifyDataStarted(mLoadID, startOffset, seekable, length);
+ mIsTransportSeekable = seekable;
+ if (mFirstReadLength < 0) {
+ mFirstReadLength = length;
+ }
+
+ mSuspendAgent.Delegate(mChannel);
+
+ // Fires an initial progress event.
+ owner->DownloadProgressed();
+
+ nsCOMPtr<nsIThreadRetargetableRequest> retarget;
+ if (Preferences::GetBool("media.omt_data_delivery.enabled", false) &&
+ (retarget = do_QueryInterface(aRequest))) {
+ // Note this will not always succeed. We need to handle the case where
+ // all resources sharing the same cache might run their data callbacks
+ // on different threads.
+ retarget->RetargetDeliveryTo(mCacheStream.OwnerThread());
+ }
+
+ return NS_OK;
+}
+
+bool ChannelMediaResource::IsTransportSeekable() {
+ MOZ_ASSERT(NS_IsMainThread());
+ // We Report the transport as seekable if we know we will never seek into
+ // the underlying transport. As the MediaCache reads content by block of
+ // BLOCK_SIZE bytes, so the content length is less it will always be fully
+ // read from offset = 0 and we can then always successfully seek within this
+ // buffered content.
+ return mIsTransportSeekable ||
+ (mFirstReadLength > 0 &&
+ mFirstReadLength < MediaCacheStream::BLOCK_SIZE);
+}
+
+nsresult ChannelMediaResource::ParseContentRangeHeader(
+ nsIHttpChannel* aHttpChan, int64_t& aRangeStart, int64_t& aRangeEnd,
+ int64_t& aRangeTotal) const {
+ NS_ENSURE_ARG(aHttpChan);
+
+ nsAutoCString rangeStr;
+ nsresult rv = aHttpChan->GetResponseHeader("Content-Range"_ns, rangeStr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_FALSE(rangeStr.IsEmpty(), NS_ERROR_ILLEGAL_VALUE);
+
+ auto rangeOrErr = net::ParseContentRangeHeaderString(rangeStr);
+ NS_ENSURE_FALSE(rangeOrErr.isErr(), rangeOrErr.unwrapErr());
+
+ aRangeStart = std::get<0>(rangeOrErr.inspect());
+ aRangeEnd = std::get<1>(rangeOrErr.inspect());
+ aRangeTotal = std::get<2>(rangeOrErr.inspect());
+
+ LOG("Received bytes [%" PRId64 "] to [%" PRId64 "] of [%" PRId64
+ "] for decoder[%p]",
+ aRangeStart, aRangeEnd, aRangeTotal, mCallback.get());
+
+ return NS_OK;
+}
+
+nsresult ChannelMediaResource::OnStopRequest(nsIRequest* aRequest,
+ nsresult aStatus) {
+ NS_ASSERTION(mChannel.get() == aRequest, "Wrong channel!");
+ NS_ASSERTION(!mSuspendAgent.IsSuspended(),
+ "How can OnStopRequest fire while we're suspended?");
+ MOZ_DIAGNOSTIC_ASSERT(!mClosed);
+
+ // Move this request back into the foreground. This is necessary for
+ // requests owned by video documents to ensure the load group fires
+ // OnStopRequest when restoring from session history.
+ nsLoadFlags loadFlags;
+ DebugOnly<nsresult> rv = mChannel->GetLoadFlags(&loadFlags);
+ NS_ASSERTION(NS_SUCCEEDED(rv), "GetLoadFlags() failed!");
+
+ if (loadFlags & nsIRequest::LOAD_BACKGROUND) {
+ Unused << NS_WARN_IF(
+ NS_FAILED(ModifyLoadFlags(loadFlags & ~nsIRequest::LOAD_BACKGROUND)));
+ }
+
+ // Note that aStatus might have succeeded --- this might be a normal close
+ // --- even in situations where the server cut us off because we were
+ // suspended. It is also possible that the server sends us fewer bytes than
+ // requested. So we need to "reopen on error" in that case too. The only
+ // cases where we don't need to reopen are when *we* closed the stream.
+ // But don't reopen if we need to seek and we don't think we can... that would
+ // cause us to just re-read the stream, which would be really bad.
+ /*
+ * | length | offset | reopen |
+ * +--------+-----------+----------+
+ * | -1 | 0 | yes |
+ * +--------+-----------+----------+
+ * | -1 | > 0 | seekable |
+ * +--------+-----------+----------+
+ * | 0 | X | no |
+ * +--------+-----------+----------+
+ * | > 0 | 0 | yes |
+ * +--------+-----------+----------+
+ * | > 0 | != length | seekable |
+ * +--------+-----------+----------+
+ * | > 0 | == length | no |
+ */
+ if (aStatus != NS_ERROR_PARSED_DATA_CACHED && aStatus != NS_BINDING_ABORTED) {
+ auto lengthAndOffset = mCacheStream.GetLengthAndOffset();
+ int64_t length = lengthAndOffset.mLength;
+ int64_t offset = lengthAndOffset.mOffset;
+ if ((offset == 0 || mIsTransportSeekable) && offset != length) {
+ // If the stream did close normally, restart the channel if we're either
+ // at the start of the resource, or if the server is seekable and we're
+ // not at the end of stream. We don't restart the stream if we're at the
+ // end because not all web servers handle this case consistently; see:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1373618#c36
+ nsresult rv = Seek(offset, false);
+ if (NS_SUCCEEDED(rv)) {
+ return rv;
+ }
+ // Close the streams that failed due to error. This will cause all
+ // client Read and Seek operations on those streams to fail. Blocked
+ // Reads will also be woken up.
+ Close();
+ }
+ }
+
+ mCacheStream.NotifyDataEnded(mLoadID, aStatus);
+ return NS_OK;
+}
+
+nsresult ChannelMediaResource::OnChannelRedirect(nsIChannel* aOld,
+ nsIChannel* aNew,
+ uint32_t aFlags,
+ int64_t aOffset) {
+ // OnChannelRedirect() is followed by OnStartRequest() where we will
+ // call mSuspendAgent.Delegate().
+ mChannel = aNew;
+ return SetupChannelHeaders(aOffset);
+}
+
+nsresult ChannelMediaResource::CopySegmentToCache(
+ nsIInputStream* aInStream, void* aClosure, const char* aFromSegment,
+ uint32_t aToOffset, uint32_t aCount, uint32_t* aWriteCount) {
+ *aWriteCount = aCount;
+ Closure* closure = static_cast<Closure*>(aClosure);
+ MediaCacheStream* cacheStream = &closure->mResource->mCacheStream;
+ if (cacheStream->OwnerThread()->IsOnCurrentThread()) {
+ cacheStream->NotifyDataReceived(
+ closure->mLoadID, aCount,
+ reinterpret_cast<const uint8_t*>(aFromSegment));
+ return NS_OK;
+ }
+
+ RefPtr<ChannelMediaResource> self = closure->mResource;
+ uint32_t loadID = closure->mLoadID;
+ UniquePtr<uint8_t[]> data = MakeUnique<uint8_t[]>(aCount);
+ memcpy(data.get(), aFromSegment, aCount);
+ cacheStream->OwnerThread()->Dispatch(NS_NewRunnableFunction(
+ "MediaCacheStream::NotifyDataReceived",
+ [self, loadID, data = std::move(data), aCount]() {
+ self->mCacheStream.NotifyDataReceived(loadID, aCount, data.get());
+ }));
+
+ return NS_OK;
+}
+
+nsresult ChannelMediaResource::OnDataAvailable(uint32_t aLoadID,
+ nsIInputStream* aStream,
+ uint32_t aCount) {
+ // This might happen off the main thread.
+ Closure closure{aLoadID, this};
+ uint32_t count = aCount;
+ while (count > 0) {
+ uint32_t read;
+ nsresult rv =
+ aStream->ReadSegments(CopySegmentToCache, &closure, count, &read);
+ if (NS_FAILED(rv)) return rv;
+ NS_ASSERTION(read > 0, "Read 0 bytes while data was available?");
+ count -= read;
+ }
+
+ return NS_OK;
+}
+
+int64_t ChannelMediaResource::CalculateStreamLength() const {
+ if (!mChannel) {
+ return -1;
+ }
+
+ nsCOMPtr<nsIHttpChannel> hc = do_QueryInterface(mChannel);
+ if (!hc) {
+ return -1;
+ }
+
+ bool succeeded = false;
+ Unused << hc->GetRequestSucceeded(&succeeded);
+ if (!succeeded) {
+ return -1;
+ }
+
+ // We can't determine the length of uncompressed payload.
+ const bool isCompressed = IsPayloadCompressed(hc);
+ if (isCompressed) {
+ return -1;
+ }
+
+ int64_t contentLength = -1;
+ if (NS_FAILED(hc->GetContentLength(&contentLength))) {
+ return -1;
+ }
+
+ uint32_t responseStatus = 0;
+ Unused << hc->GetResponseStatus(&responseStatus);
+ if (responseStatus != HTTP_PARTIAL_RESPONSE_CODE) {
+ return contentLength;
+ }
+
+ // We have an HTTP Byte Range response. The Content-Length is the length
+ // of the response, not the resource. We need to parse the Content-Range
+ // header and extract the range total in order to get the stream length.
+ int64_t rangeStart = 0;
+ int64_t rangeEnd = 0;
+ int64_t rangeTotal = 0;
+ bool gotRangeHeader = NS_SUCCEEDED(
+ ParseContentRangeHeader(hc, rangeStart, rangeEnd, rangeTotal));
+ if (gotRangeHeader && rangeTotal != -1) {
+ contentLength = std::max(contentLength, rangeTotal);
+ }
+ return contentLength;
+}
+
+nsresult ChannelMediaResource::Open(nsIStreamListener** aStreamListener) {
+ NS_ASSERTION(NS_IsMainThread(), "Only call on main thread");
+ MOZ_ASSERT(aStreamListener);
+ MOZ_ASSERT(mChannel);
+
+ int64_t streamLength =
+ mKnownStreamLength < 0 ? CalculateStreamLength() : mKnownStreamLength;
+ nsresult rv = mCacheStream.Init(streamLength);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ mSharedInfo = new SharedInfo;
+ mSharedInfo->mResources.AppendElement(this);
+
+ mIsLiveStream = streamLength < 0;
+ mListener = new Listener(this, 0, ++mLoadID);
+ *aStreamListener = mListener;
+ NS_ADDREF(*aStreamListener);
+ return NS_OK;
+}
+
+dom::HTMLMediaElement* ChannelMediaResource::MediaElement() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ MediaDecoderOwner* owner = mCallback->GetMediaOwner();
+ MOZ_DIAGNOSTIC_ASSERT(owner);
+ dom::HTMLMediaElement* element = owner->GetMediaElement();
+ MOZ_DIAGNOSTIC_ASSERT(element);
+ return element;
+}
+
+nsresult ChannelMediaResource::OpenChannel(int64_t aOffset) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(!mClosed);
+ MOZ_ASSERT(mChannel);
+ MOZ_ASSERT(!mListener, "Listener should have been removed by now");
+
+ mListener = new Listener(this, aOffset, ++mLoadID);
+ nsresult rv = mChannel->SetNotificationCallbacks(mListener.get());
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = SetupChannelHeaders(aOffset);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = mChannel->AsyncOpen(mListener);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Tell the media element that we are fetching data from a channel.
+ MediaElement()->DownloadResumed();
+
+ return NS_OK;
+}
+
+nsresult ChannelMediaResource::SetupChannelHeaders(int64_t aOffset) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(!mClosed);
+
+ // Always use a byte range request even if we're reading from the start
+ // of the resource.
+ // This enables us to detect if the stream supports byte range
+ // requests, and therefore seeking, early.
+ nsCOMPtr<nsIHttpChannel> hc = do_QueryInterface(mChannel);
+ if (hc) {
+ // Use |mOffset| if seeking in a complete file download.
+ nsAutoCString rangeString("bytes=");
+ rangeString.AppendInt(aOffset);
+ rangeString.Append('-');
+ nsresult rv = hc->SetRequestHeader("Range"_ns, rangeString, false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Send Accept header for video and audio types only (Bug 489071)
+ MediaElement()->SetRequestHeaders(hc);
+ } else {
+ NS_ASSERTION(aOffset == 0, "Don't know how to seek on this channel type");
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+RefPtr<GenericPromise> ChannelMediaResource::Close() {
+ NS_ASSERTION(NS_IsMainThread(), "Only call on main thread");
+
+ if (!mClosed) {
+ CloseChannel();
+ mClosed = true;
+ return mCacheStream.Close();
+ }
+ return GenericPromise::CreateAndResolve(true, __func__);
+}
+
+already_AddRefed<nsIPrincipal> ChannelMediaResource::GetCurrentPrincipal() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return do_AddRef(mSharedInfo->mPrincipal);
+}
+
+bool ChannelMediaResource::HadCrossOriginRedirects() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mSharedInfo->mHadCrossOriginRedirects;
+}
+
+bool ChannelMediaResource::CanClone() {
+ return !mClosed && mCacheStream.IsAvailableForSharing();
+}
+
+already_AddRefed<BaseMediaResource> ChannelMediaResource::CloneData(
+ MediaResourceCallback* aCallback) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(CanClone(), "Stream can't be cloned");
+
+ RefPtr<ChannelMediaResource> resource =
+ new ChannelMediaResource(aCallback, nullptr, mURI, mKnownStreamLength);
+
+ resource->mIsLiveStream = mIsLiveStream;
+ resource->mIsTransportSeekable = mIsTransportSeekable;
+ resource->mSharedInfo = mSharedInfo;
+ mSharedInfo->mResources.AppendElement(resource.get());
+
+ // Initially the clone is treated as suspended by the cache, because
+ // we don't have a channel. If the cache needs to read data from the clone
+ // it will call CacheClientResume (or CacheClientSeek with aResume true)
+ // which will recreate the channel. This way, if all of the media data
+ // is already in the cache we don't create an unnecessary HTTP channel
+ // and perform a useless HTTP transaction.
+ resource->mCacheStream.InitAsClone(&mCacheStream);
+ return resource.forget();
+}
+
+void ChannelMediaResource::CloseChannel() {
+ NS_ASSERTION(NS_IsMainThread(), "Only call on main thread");
+
+ // Revoking listener should be done before canceling the channel, because
+ // canceling the channel might cause the input stream to release its buffer.
+ // If we don't do revoke first, it's possible that `OnDataAvailable` would be
+ // called later and then incorrectly access that released buffer.
+ if (mListener) {
+ mListener->Revoke();
+ mListener = nullptr;
+ }
+
+ if (mChannel) {
+ mSuspendAgent.Revoke();
+ // The status we use here won't be passed to the decoder, since
+ // we've already revoked the listener. It can however be passed
+ // to nsDocumentViewer::LoadComplete if our channel is the one
+ // that kicked off creation of a video document. We don't want that
+ // document load to think there was an error.
+ // NS_ERROR_PARSED_DATA_CACHED is the best thing we have for that
+ // at the moment.
+ mChannel->Cancel(NS_ERROR_PARSED_DATA_CACHED);
+ mChannel = nullptr;
+ }
+}
+
+nsresult ChannelMediaResource::ReadFromCache(char* aBuffer, int64_t aOffset,
+ uint32_t aCount) {
+ return mCacheStream.ReadFromCache(aBuffer, aOffset, aCount);
+}
+
+nsresult ChannelMediaResource::ReadAt(int64_t aOffset, char* aBuffer,
+ uint32_t aCount, uint32_t* aBytes) {
+ NS_ASSERTION(!NS_IsMainThread(), "Don't call on main thread");
+ return mCacheStream.ReadAt(aOffset, aBuffer, aCount, aBytes);
+}
+
+void ChannelMediaResource::ThrottleReadahead(bool bThrottle) {
+ mCacheStream.ThrottleReadahead(bThrottle);
+}
+
+nsresult ChannelMediaResource::GetCachedRanges(MediaByteRangeSet& aRanges) {
+ return mCacheStream.GetCachedRanges(aRanges);
+}
+
+void ChannelMediaResource::Suspend(bool aCloseImmediately) {
+ NS_ASSERTION(NS_IsMainThread(), "Don't call on non-main thread");
+
+ if (mClosed) {
+ // Nothing to do when we are closed.
+ return;
+ }
+
+ dom::HTMLMediaElement* element = MediaElement();
+
+ if (mChannel && aCloseImmediately && mIsTransportSeekable) {
+ CloseChannel();
+ }
+
+ if (mSuspendAgent.Suspend()) {
+ element->DownloadSuspended();
+ }
+}
+
+void ChannelMediaResource::Resume() {
+ NS_ASSERTION(NS_IsMainThread(), "Don't call on non-main thread");
+
+ if (mClosed) {
+ // Nothing to do when we are closed.
+ return;
+ }
+
+ dom::HTMLMediaElement* element = MediaElement();
+
+ if (mSuspendAgent.Resume()) {
+ if (mChannel) {
+ // Just wake up our existing channel
+ element->DownloadResumed();
+ } else {
+ mCacheStream.NotifyResume();
+ }
+ }
+}
+
+nsresult ChannelMediaResource::RecreateChannel() {
+ MOZ_DIAGNOSTIC_ASSERT(!mClosed);
+
+ nsLoadFlags loadFlags = nsICachingChannel::LOAD_BYPASS_LOCAL_CACHE_IF_BUSY |
+ (mLoadInBackground ? nsIRequest::LOAD_BACKGROUND : 0);
+
+ dom::HTMLMediaElement* element = MediaElement();
+
+ nsCOMPtr<nsILoadGroup> loadGroup = element->GetDocumentLoadGroup();
+ NS_ENSURE_TRUE(loadGroup, NS_ERROR_NULL_POINTER);
+
+ nsSecurityFlags securityFlags =
+ element->ShouldCheckAllowOrigin()
+ ? nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT
+ : nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT;
+
+ if (element->GetCORSMode() == CORS_USE_CREDENTIALS) {
+ securityFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE;
+ }
+
+ MOZ_ASSERT(element->IsAnyOfHTMLElements(nsGkAtoms::audio, nsGkAtoms::video));
+ nsContentPolicyType contentPolicyType =
+ element->IsHTMLElement(nsGkAtoms::audio)
+ ? nsIContentPolicy::TYPE_INTERNAL_AUDIO
+ : nsIContentPolicy::TYPE_INTERNAL_VIDEO;
+
+ // If element has 'triggeringprincipal' attribute, we will use the value as
+ // triggeringPrincipal for the channel, otherwise it will default to use
+ // aElement->NodePrincipal().
+ // This function returns true when element has 'triggeringprincipal', so if
+ // setAttrs is true we will override the origin attributes on the channel
+ // later.
+ nsCOMPtr<nsIPrincipal> triggeringPrincipal;
+ bool setAttrs = nsContentUtils::QueryTriggeringPrincipal(
+ element, getter_AddRefs(triggeringPrincipal));
+
+ nsresult rv = NS_NewChannelWithTriggeringPrincipal(
+ getter_AddRefs(mChannel), mURI, element, triggeringPrincipal,
+ securityFlags, contentPolicyType,
+ nullptr, // aPerformanceStorage
+ loadGroup,
+ nullptr, // aCallbacks
+ loadFlags);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsCOMPtr<nsILoadInfo> loadInfo = mChannel->LoadInfo();
+ if (setAttrs) {
+ // The function simply returns NS_OK, so we ignore the return value.
+ Unused << loadInfo->SetOriginAttributes(
+ triggeringPrincipal->OriginAttributesRef());
+ }
+
+ Unused << loadInfo->SetIsMediaRequest(true);
+
+ nsCOMPtr<nsIClassOfService> cos(do_QueryInterface(mChannel));
+ if (cos) {
+ // Unconditionally disable throttling since we want the media to fluently
+ // play even when we switch the tab to background.
+ cos->AddClassFlags(nsIClassOfService::DontThrottle);
+ }
+
+ return rv;
+}
+
+void ChannelMediaResource::CacheClientNotifyDataReceived() {
+ mCallback->AbstractMainThread()->Dispatch(NewRunnableMethod(
+ "MediaResourceCallback::NotifyDataArrived", mCallback.get(),
+ &MediaResourceCallback::NotifyDataArrived));
+}
+
+void ChannelMediaResource::CacheClientNotifyDataEnded(nsresult aStatus) {
+ mCallback->AbstractMainThread()->Dispatch(NS_NewRunnableFunction(
+ "ChannelMediaResource::CacheClientNotifyDataEnded",
+ [self = RefPtr<ChannelMediaResource>(this), aStatus]() {
+ if (NS_SUCCEEDED(aStatus)) {
+ self->mIsLiveStream = false;
+ }
+ self->mCallback->NotifyDataEnded(aStatus);
+ }));
+}
+
+void ChannelMediaResource::CacheClientNotifyPrincipalChanged() {
+ NS_ASSERTION(NS_IsMainThread(), "Don't call on non-main thread");
+
+ mCallback->NotifyPrincipalChanged();
+}
+
+void ChannelMediaResource::UpdatePrincipal() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mChannel);
+ nsIScriptSecurityManager* secMan = nsContentUtils::GetSecurityManager();
+ if (!secMan) {
+ return;
+ }
+ bool hadData = mSharedInfo->mPrincipal != nullptr;
+ // Channels created from a media element (in RecreateChannel() or
+ // HTMLMediaElement::ChannelLoader) do not have SANDBOXED_ORIGIN set in the
+ // LoadInfo. Document loads for a sandboxed iframe, however, may have
+ // SANDBOXED_ORIGIN set. Ignore sandboxing so that on such loads the result
+ // principal is not replaced with a null principal but describes the source
+ // of the data and is the same as would be obtained from a load from the
+ // media host element.
+ nsCOMPtr<nsIPrincipal> principal;
+ secMan->GetChannelResultPrincipalIfNotSandboxed(mChannel,
+ getter_AddRefs(principal));
+ if (nsContentUtils::CombineResourcePrincipals(&mSharedInfo->mPrincipal,
+ principal)) {
+ for (auto* r : mSharedInfo->mResources) {
+ r->CacheClientNotifyPrincipalChanged();
+ }
+ if (!mChannel) { // Sometimes cleared during NotifyPrincipalChanged()
+ return;
+ }
+ }
+ nsCOMPtr<nsILoadInfo> loadInfo = mChannel->LoadInfo();
+ auto mode = loadInfo->GetSecurityMode();
+ if (mode != nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT) {
+ MOZ_ASSERT(
+ mode == nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT ||
+ mode == nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ "no-cors request");
+ MOZ_ASSERT(!hadData || !mChannel->IsDocument(),
+ "Only the initial load may be a document load");
+ bool finalResponseIsOpaque =
+ // NS_GetFinalChannelURI() and GetChannelResultPrincipal() return the
+ // original request URI for null-origin Responses from ServiceWorker,
+ // in which case the URI does not necessarily indicate the real source
+ // of data. Such null-origin Responses have Basic LoadTainting, and
+ // so can be distinguished from true cross-origin responses when the
+ // channel is not a document load.
+ //
+ // When the channel is a document load, LoadTainting indicates opacity
+ // wrt the parent document and so does not indicate whether the
+ // response is cross-origin wrt to the media element. However,
+ // ServiceWorkers for document loads are always same-origin with the
+ // channel URI and so there is no need to distinguish null-origin
+ // ServiceWorker responses to document loads.
+ //
+ // CORS filtered Responses from ServiceWorker also cannot be mixed
+ // with no-cors cross-origin responses.
+ (mChannel->IsDocument() ||
+ loadInfo->GetTainting() == LoadTainting::Opaque) &&
+ // Although intermediate cross-origin redirects back to URIs with
+ // loadingPrincipal will have LoadTainting::Opaque and will taint the
+ // media element, they are not considered opaque when verifying
+ // network responses; they can be mixed with non-opaque responses from
+ // subsequent loads on the same-origin finalURI.
+ !nsContentUtils::CheckMayLoad(MediaElement()->NodePrincipal(), mChannel,
+ /*allowIfInheritsPrincipal*/ true);
+ if (!hadData) { // First response with data
+ mSharedInfo->mFinalResponsesAreOpaque = finalResponseIsOpaque;
+ } else if (mSharedInfo->mFinalResponsesAreOpaque != finalResponseIsOpaque) {
+ for (auto* r : mSharedInfo->mResources) {
+ r->mCallback->NotifyNetworkError(MediaResult(
+ NS_ERROR_CONTENT_BLOCKED, "opaque and non-opaque responses"));
+ }
+ // Our caller, OnStartRequest() will CloseChannel() on discovering the
+ // error, so no data will be read from the channel.
+ return;
+ }
+ }
+ // ChannelMediaResource can recreate the channel. When this happens, we don't
+ // want to overwrite mHadCrossOriginRedirects because the new channel could
+ // skip intermediate redirects.
+ if (!mSharedInfo->mHadCrossOriginRedirects) {
+ nsCOMPtr<nsITimedChannel> timedChannel = do_QueryInterface(mChannel);
+ if (timedChannel) {
+ bool allRedirectsSameOrigin = false;
+ mSharedInfo->mHadCrossOriginRedirects =
+ NS_SUCCEEDED(timedChannel->GetAllRedirectsSameOrigin(
+ &allRedirectsSameOrigin)) &&
+ !allRedirectsSameOrigin;
+ }
+ }
+}
+
+void ChannelMediaResource::CacheClientNotifySuspendedStatusChanged(
+ bool aSuspended) {
+ mCallback->AbstractMainThread()->Dispatch(NewRunnableMethod<bool>(
+ "MediaResourceCallback::NotifySuspendedStatusChanged", mCallback.get(),
+ &MediaResourceCallback::NotifySuspendedStatusChanged, aSuspended));
+}
+
+nsresult ChannelMediaResource::Seek(int64_t aOffset, bool aResume) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mClosed) {
+ // Nothing to do when we are closed.
+ return NS_OK;
+ }
+
+ LOG("Seek requested for aOffset [%" PRId64 "]", aOffset);
+
+ CloseChannel();
+
+ if (aResume) {
+ mSuspendAgent.Resume();
+ }
+
+ // Don't create a new channel if we are still suspended. The channel will
+ // be recreated when we are resumed.
+ if (mSuspendAgent.IsSuspended()) {
+ return NS_OK;
+ }
+
+ nsresult rv = RecreateChannel();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return OpenChannel(aOffset);
+}
+
+void ChannelMediaResource::CacheClientSeek(int64_t aOffset, bool aResume) {
+ RefPtr<ChannelMediaResource> self = this;
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ "ChannelMediaResource::Seek", [self, aOffset, aResume]() {
+ nsresult rv = self->Seek(aOffset, aResume);
+ if (NS_FAILED(rv)) {
+ // Close the streams that failed due to error. This will cause all
+ // client Read and Seek operations on those streams to fail. Blocked
+ // Reads will also be woken up.
+ self->Close();
+ }
+ });
+ mCallback->AbstractMainThread()->Dispatch(r.forget());
+}
+
+void ChannelMediaResource::CacheClientSuspend() {
+ mCallback->AbstractMainThread()->Dispatch(
+ NewRunnableMethod<bool>("ChannelMediaResource::Suspend", this,
+ &ChannelMediaResource::Suspend, false));
+}
+
+void ChannelMediaResource::CacheClientResume() {
+ mCallback->AbstractMainThread()->Dispatch(NewRunnableMethod(
+ "ChannelMediaResource::Resume", this, &ChannelMediaResource::Resume));
+}
+
+int64_t ChannelMediaResource::GetNextCachedData(int64_t aOffset) {
+ return mCacheStream.GetNextCachedData(aOffset);
+}
+
+int64_t ChannelMediaResource::GetCachedDataEnd(int64_t aOffset) {
+ return mCacheStream.GetCachedDataEnd(aOffset);
+}
+
+bool ChannelMediaResource::IsDataCachedToEndOfResource(int64_t aOffset) {
+ return mCacheStream.IsDataCachedToEndOfStream(aOffset);
+}
+
+bool ChannelMediaResource::IsSuspended() { return mSuspendAgent.IsSuspended(); }
+
+void ChannelMediaResource::SetReadMode(MediaCacheStream::ReadMode aMode) {
+ mCacheStream.SetReadMode(aMode);
+}
+
+void ChannelMediaResource::SetPlaybackRate(uint32_t aBytesPerSecond) {
+ mCacheStream.SetPlaybackRate(aBytesPerSecond);
+}
+
+void ChannelMediaResource::Pin() { mCacheStream.Pin(); }
+
+void ChannelMediaResource::Unpin() { mCacheStream.Unpin(); }
+
+double ChannelMediaResource::GetDownloadRate(bool* aIsReliable) {
+ return mCacheStream.GetDownloadRate(aIsReliable);
+}
+
+int64_t ChannelMediaResource::GetLength() { return mCacheStream.GetLength(); }
+
+void ChannelMediaResource::GetDebugInfo(dom::MediaResourceDebugInfo& aInfo) {
+ mCacheStream.GetDebugInfo(aInfo.mCacheStream);
+}
+
+// ChannelSuspendAgent
+
+bool ChannelSuspendAgent::Suspend() {
+ MOZ_ASSERT(NS_IsMainThread());
+ SuspendInternal();
+ if (++mSuspendCount == 1) {
+ mCacheStream.NotifyClientSuspended(true);
+ return true;
+ }
+ return false;
+}
+
+void ChannelSuspendAgent::SuspendInternal() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mChannel) {
+ bool isPending = false;
+ nsresult rv = mChannel->IsPending(&isPending);
+ if (NS_SUCCEEDED(rv) && isPending && !mIsChannelSuspended) {
+ mChannel->Suspend();
+ mIsChannelSuspended = true;
+ }
+ }
+}
+
+bool ChannelSuspendAgent::Resume() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(IsSuspended(), "Resume without suspend!");
+
+ if (--mSuspendCount == 0) {
+ if (mChannel && mIsChannelSuspended) {
+ mChannel->Resume();
+ mIsChannelSuspended = false;
+ }
+ mCacheStream.NotifyClientSuspended(false);
+ return true;
+ }
+ return false;
+}
+
+void ChannelSuspendAgent::Delegate(nsIChannel* aChannel) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aChannel);
+ MOZ_ASSERT(!mChannel, "The previous channel not closed.");
+ MOZ_ASSERT(!mIsChannelSuspended);
+
+ mChannel = aChannel;
+ // Ensure the suspend status of the channel matches our suspend count.
+ if (IsSuspended()) {
+ SuspendInternal();
+ }
+}
+
+void ChannelSuspendAgent::Revoke() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mChannel) {
+ // Channel already revoked. Nothing to do.
+ return;
+ }
+
+ // Before closing the channel, it needs to be resumed to make sure its
+ // internal state is correct. Besides, We need to suspend the channel after
+ // recreating.
+ if (mIsChannelSuspended) {
+ mChannel->Resume();
+ mIsChannelSuspended = false;
+ }
+ mChannel = nullptr;
+}
+
+bool ChannelSuspendAgent::IsSuspended() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return (mSuspendCount > 0);
+}
+
+} // namespace mozilla
diff --git a/dom/media/ChannelMediaResource.h b/dom/media/ChannelMediaResource.h
new file mode 100644
index 0000000000..892dca1de0
--- /dev/null
+++ b/dom/media/ChannelMediaResource.h
@@ -0,0 +1,273 @@
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_media_ChannelMediaResource_h
+#define mozilla_dom_media_ChannelMediaResource_h
+
+#include "BaseMediaResource.h"
+#include "MediaCache.h"
+#include "mozilla/Mutex.h"
+#include "nsIChannelEventSink.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIThreadRetargetableStreamListener.h"
+
+class nsIHttpChannel;
+
+namespace mozilla {
+
+/**
+ * This class is responsible for managing the suspend count and report suspend
+ * status of channel.
+ **/
+class ChannelSuspendAgent {
+ public:
+ explicit ChannelSuspendAgent(MediaCacheStream& aCacheStream)
+ : mCacheStream(aCacheStream) {}
+
+ // True when the channel has been suspended or needs to be suspended.
+ bool IsSuspended();
+
+ // Return true when the channel is logically suspended, i.e. the suspend
+ // count goes from 0 to 1.
+ bool Suspend();
+
+ // Return true only when the suspend count is equal to zero.
+ bool Resume();
+
+ // Tell the agent to manage the suspend status of the channel.
+ void Delegate(nsIChannel* aChannel);
+ // Stop the management of the suspend status of the channel.
+ void Revoke();
+
+ private:
+ // Only suspends channel but not changes the suspend count.
+ void SuspendInternal();
+
+ nsIChannel* mChannel = nullptr;
+ MediaCacheStream& mCacheStream;
+ uint32_t mSuspendCount = 0;
+ bool mIsChannelSuspended = false;
+};
+
+DDLoggedTypeDeclNameAndBase(ChannelMediaResource, BaseMediaResource);
+
+/**
+ * This is the MediaResource implementation that wraps Necko channels.
+ * Much of its functionality is actually delegated to MediaCache via
+ * an underlying MediaCacheStream.
+ *
+ * All synchronization is performed by MediaCacheStream; all off-main-
+ * thread operations are delegated directly to that object.
+ */
+class ChannelMediaResource
+ : public BaseMediaResource,
+ public DecoderDoctorLifeLogger<ChannelMediaResource> {
+ // Store information shared among resources. Main thread only.
+ struct SharedInfo {
+ NS_INLINE_DECL_REFCOUNTING(SharedInfo);
+
+ nsTArray<ChannelMediaResource*> mResources;
+ // Null if there is not yet any data from any origin.
+ nsCOMPtr<nsIPrincipal> mPrincipal;
+ // Meaningful only when mPrincipal is non-null,
+ // unaffected by intermediate cross-origin redirects.
+ bool mFinalResponsesAreOpaque = false;
+
+ bool mHadCrossOriginRedirects = false;
+
+ private:
+ ~SharedInfo() = default;
+ };
+ RefPtr<SharedInfo> mSharedInfo;
+
+ public:
+ ChannelMediaResource(MediaResourceCallback* aDecoder, nsIChannel* aChannel,
+ nsIURI* aURI, int64_t aStreamLength,
+ bool aIsPrivateBrowsing = false);
+ ~ChannelMediaResource();
+
+ // These are called on the main thread by MediaCache. These must
+ // not block or grab locks, because the media cache is holding its lock.
+ // Notify that data is available from the cache. This can happen even
+ // if this stream didn't read any data, since another stream might have
+ // received data for the same resource.
+ void CacheClientNotifyDataReceived();
+ // Notify that we reached the end of the stream. This can happen even
+ // if this stream didn't read any data, since another stream might have
+ // received data for the same resource.
+ void CacheClientNotifyDataEnded(nsresult aStatus);
+ // Notify that the principal for the cached resource changed.
+ void CacheClientNotifyPrincipalChanged();
+ // Notify the decoder that the cache suspended status changed.
+ void CacheClientNotifySuspendedStatusChanged(bool aSuspended);
+
+ // These are called on the main thread by MediaCache. These shouldn't block,
+ // but they may grab locks --- the media cache is not holding its lock
+ // when these are called.
+ // Start a new load at the given aOffset. The old load is cancelled
+ // and no more data from the old load will be notified via
+ // MediaCacheStream::NotifyDataReceived/Ended.
+ void CacheClientSeek(int64_t aOffset, bool aResume);
+ // Suspend the current load since data is currently not wanted
+ void CacheClientSuspend();
+ // Resume the current load since data is wanted again
+ void CacheClientResume();
+
+ bool IsSuspended();
+
+ void ThrottleReadahead(bool bThrottle) override;
+
+ // Main thread
+ nsresult Open(nsIStreamListener** aStreamListener) override;
+ RefPtr<GenericPromise> Close() override;
+ void Suspend(bool aCloseImmediately) override;
+ void Resume() override;
+ already_AddRefed<nsIPrincipal> GetCurrentPrincipal() override;
+ bool HadCrossOriginRedirects() override;
+ bool CanClone() override;
+ already_AddRefed<BaseMediaResource> CloneData(
+ MediaResourceCallback* aDecoder) override;
+ nsresult ReadFromCache(char* aBuffer, int64_t aOffset,
+ uint32_t aCount) override;
+
+ // Other thread
+ void SetReadMode(MediaCacheStream::ReadMode aMode) override;
+ void SetPlaybackRate(uint32_t aBytesPerSecond) override;
+ nsresult ReadAt(int64_t offset, char* aBuffer, uint32_t aCount,
+ uint32_t* aBytes) override;
+ // Data stored in IO&lock-encumbered MediaCacheStream, caching recommended.
+ bool ShouldCacheReads() override { return true; }
+
+ // Any thread
+ void Pin() override;
+ void Unpin() override;
+ double GetDownloadRate(bool* aIsReliable) override;
+ int64_t GetLength() override;
+ int64_t GetNextCachedData(int64_t aOffset) override;
+ int64_t GetCachedDataEnd(int64_t aOffset) override;
+ bool IsDataCachedToEndOfResource(int64_t aOffset) override;
+ bool IsTransportSeekable() override;
+ bool IsLiveStream() const override { return mIsLiveStream; }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override {
+ // Might be useful to track in the future:
+ // - mListener (seems minor)
+ size_t size = BaseMediaResource::SizeOfExcludingThis(aMallocSizeOf);
+ size += mCacheStream.SizeOfExcludingThis(aMallocSizeOf);
+
+ return size;
+ }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ void GetDebugInfo(dom::MediaResourceDebugInfo& aInfo) override;
+
+ class Listener final : public nsIStreamListener,
+ public nsIInterfaceRequestor,
+ public nsIChannelEventSink,
+ public nsIThreadRetargetableStreamListener {
+ ~Listener() = default;
+
+ public:
+ Listener(ChannelMediaResource* aResource, int64_t aOffset, uint32_t aLoadID)
+ : mMutex("Listener.mMutex"),
+ mResource(aResource),
+ mOffset(aOffset),
+ mLoadID(aLoadID) {}
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_NSICHANNELEVENTSINK
+ NS_DECL_NSIINTERFACEREQUESTOR
+ NS_DECL_NSITHREADRETARGETABLESTREAMLISTENER
+
+ void Revoke();
+
+ private:
+ Mutex mMutex MOZ_UNANNOTATED;
+ // mResource should only be modified on the main thread with the lock.
+ // So it can be read without lock on the main thread or on other threads
+ // with the lock.
+ RefPtr<ChannelMediaResource> mResource;
+
+ const int64_t mOffset;
+ const uint32_t mLoadID;
+ };
+ friend class Listener;
+
+ nsresult GetCachedRanges(MediaByteRangeSet& aRanges) override;
+
+ protected:
+ nsresult Seek(int64_t aOffset, bool aResume);
+
+ // These are called on the main thread by Listener.
+ nsresult OnStartRequest(nsIRequest* aRequest, int64_t aRequestOffset);
+ nsresult OnStopRequest(nsIRequest* aRequest, nsresult aStatus);
+ nsresult OnDataAvailable(uint32_t aLoadID, nsIInputStream* aStream,
+ uint32_t aCount);
+ nsresult OnChannelRedirect(nsIChannel* aOld, nsIChannel* aNew,
+ uint32_t aFlags, int64_t aOffset);
+
+ // Use only before MediaDecoder shutdown. Main thread only.
+ dom::HTMLMediaElement* MediaElement() const;
+ // Opens the channel, using an HTTP byte range request to start at aOffset
+ // if possible. Main thread only.
+ nsresult OpenChannel(int64_t aOffset);
+ nsresult RecreateChannel();
+ // Add headers to HTTP request. Main thread only.
+ nsresult SetupChannelHeaders(int64_t aOffset);
+ // Closes the channel. Main thread only.
+ void CloseChannel();
+ // Update the principal for the resource. Main thread only.
+ void UpdatePrincipal();
+
+ // Parses 'Content-Range' header and returns results via parameters.
+ // Returns error if header is not available, values are not parse-able or
+ // values are out of range.
+ nsresult ParseContentRangeHeader(nsIHttpChannel* aHttpChan,
+ int64_t& aRangeStart, int64_t& aRangeEnd,
+ int64_t& aRangeTotal) const;
+
+ // Calculates the length of the resource using HTTP headers, if this
+ // is an HTTP channel. Returns -1 on failure, or for non HTTP channels.
+ int64_t CalculateStreamLength() const;
+
+ struct Closure {
+ uint32_t mLoadID;
+ ChannelMediaResource* mResource;
+ };
+
+ static nsresult CopySegmentToCache(nsIInputStream* aInStream, void* aClosure,
+ const char* aFromSegment,
+ uint32_t aToOffset, uint32_t aCount,
+ uint32_t* aWriteCount);
+
+ // Main thread access only
+ // True if Close() has been called.
+ bool mClosed = false;
+ // The last reported seekability state for the underlying channel
+ bool mIsTransportSeekable = false;
+ // Length of the content first reported.
+ int64_t mFirstReadLength = -1;
+ RefPtr<Listener> mListener;
+ // A mono-increasing integer to uniquely identify the channel we are loading.
+ uint32_t mLoadID = 0;
+ bool mIsLiveStream = false;
+
+ // Any thread access
+ MediaCacheStream mCacheStream;
+
+ ChannelSuspendAgent mSuspendAgent;
+
+ // The size of the stream if known at construction time (such as with blob)
+ const int64_t mKnownStreamLength;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_dom_media_ChannelMediaResource_h
diff --git a/dom/media/CloneableWithRangeMediaResource.cpp b/dom/media/CloneableWithRangeMediaResource.cpp
new file mode 100644
index 0000000000..4faac6125f
--- /dev/null
+++ b/dom/media/CloneableWithRangeMediaResource.cpp
@@ -0,0 +1,228 @@
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CloneableWithRangeMediaResource.h"
+
+#include "mozilla/AbstractThread.h"
+#include "mozilla/Monitor.h"
+#include "nsContentUtils.h"
+#include "nsIAsyncInputStream.h"
+#include "nsITimedChannel.h"
+#include "nsNetCID.h"
+#include "nsServiceManagerUtils.h"
+
+namespace mozilla {
+
+namespace {
+
+class InputStreamReader final : public nsIInputStreamCallback {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ static already_AddRefed<InputStreamReader> Create(
+ nsICloneableInputStreamWithRange* aStream, int64_t aStart,
+ uint32_t aLength) {
+ MOZ_ASSERT(aStream);
+
+ nsCOMPtr<nsIInputStream> stream;
+ nsresult rv =
+ aStream->CloneWithRange(aStart, aLength, getter_AddRefs(stream));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ RefPtr<InputStreamReader> reader = new InputStreamReader(stream);
+ return reader.forget();
+ }
+
+ nsresult Read(char* aBuffer, uint32_t aSize, uint32_t* aRead) {
+ uint32_t done = 0;
+ do {
+ uint32_t read;
+ nsresult rv = SyncRead(aBuffer + done, aSize - done, &read);
+ if (NS_SUCCEEDED(rv) && read == 0) {
+ break;
+ }
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ done += read;
+ } while (done != aSize);
+
+ *aRead = done;
+ return NS_OK;
+ }
+
+ NS_IMETHOD
+ OnInputStreamReady(nsIAsyncInputStream* aStream) override {
+ // Let's continue with SyncRead().
+ MonitorAutoLock lock(mMonitor);
+ lock.Notify();
+ return NS_OK;
+ }
+
+ private:
+ explicit InputStreamReader(nsIInputStream* aStream)
+ : mStream(aStream), mMonitor("InputStreamReader::mMonitor") {
+ MOZ_ASSERT(aStream);
+ }
+
+ ~InputStreamReader() = default;
+
+ nsresult SyncRead(char* aBuffer, uint32_t aSize, uint32_t* aRead) {
+ while (1) {
+ nsresult rv = mStream->Read(aBuffer, aSize, aRead);
+ // All good.
+ if (rv == NS_BASE_STREAM_CLOSED || NS_SUCCEEDED(rv)) {
+ return NS_OK;
+ }
+
+ // An error.
+ if (NS_FAILED(rv) && rv != NS_BASE_STREAM_WOULD_BLOCK) {
+ return rv;
+ }
+
+ // We need to proceed async.
+ if (!mAsyncStream) {
+ mAsyncStream = do_QueryInterface(mStream);
+ }
+
+ if (!mAsyncStream) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIEventTarget> target =
+ do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
+ MOZ_ASSERT(target);
+
+ {
+ // We wait for ::OnInputStreamReady() to be called.
+ MonitorAutoLock lock(mMonitor);
+
+ rv = mAsyncStream->AsyncWait(this, 0, aSize, target);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ lock.Wait();
+ }
+ }
+ }
+
+ nsCOMPtr<nsIInputStream> mStream;
+ nsCOMPtr<nsIAsyncInputStream> mAsyncStream;
+ Monitor mMonitor MOZ_UNANNOTATED;
+};
+
+NS_IMPL_ADDREF(InputStreamReader);
+NS_IMPL_RELEASE(InputStreamReader);
+
+NS_INTERFACE_MAP_BEGIN(InputStreamReader)
+ NS_INTERFACE_MAP_ENTRY(nsIInputStreamCallback)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIInputStreamCallback)
+NS_INTERFACE_MAP_END
+
+} // namespace
+
+void CloneableWithRangeMediaResource::MaybeInitialize() {
+ if (!mInitialized) {
+ mInitialized = true;
+ mCallback->AbstractMainThread()->Dispatch(NewRunnableMethod<nsresult>(
+ "MediaResourceCallback::NotifyDataEnded", mCallback.get(),
+ &MediaResourceCallback::NotifyDataEnded, NS_OK));
+ }
+}
+
+nsresult CloneableWithRangeMediaResource::GetCachedRanges(
+ MediaByteRangeSet& aRanges) {
+ MaybeInitialize();
+ aRanges += MediaByteRange(0, (int64_t)mSize);
+ return NS_OK;
+}
+
+nsresult CloneableWithRangeMediaResource::Open(
+ nsIStreamListener** aStreamListener) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aStreamListener);
+
+ *aStreamListener = nullptr;
+ return NS_OK;
+}
+
+RefPtr<GenericPromise> CloneableWithRangeMediaResource::Close() {
+ return GenericPromise::CreateAndResolve(true, __func__);
+}
+
+already_AddRefed<nsIPrincipal>
+CloneableWithRangeMediaResource::GetCurrentPrincipal() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIPrincipal> principal;
+ nsIScriptSecurityManager* secMan = nsContentUtils::GetSecurityManager();
+ if (!secMan || !mChannel) {
+ return nullptr;
+ }
+
+ secMan->GetChannelResultPrincipal(mChannel, getter_AddRefs(principal));
+ return principal.forget();
+}
+
+bool CloneableWithRangeMediaResource::HadCrossOriginRedirects() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsITimedChannel> timedChannel = do_QueryInterface(mChannel);
+ if (!timedChannel) {
+ return false;
+ }
+
+ bool allRedirectsSameOrigin = false;
+ return NS_SUCCEEDED(timedChannel->GetAllRedirectsSameOrigin(
+ &allRedirectsSameOrigin)) &&
+ !allRedirectsSameOrigin;
+}
+
+nsresult CloneableWithRangeMediaResource::ReadFromCache(char* aBuffer,
+ int64_t aOffset,
+ uint32_t aCount) {
+ MaybeInitialize();
+ if (!aCount) {
+ return NS_OK;
+ }
+
+ RefPtr<InputStreamReader> reader =
+ InputStreamReader::Create(mStream, aOffset, aCount);
+ if (!reader) {
+ return NS_ERROR_FAILURE;
+ }
+
+ uint32_t bytes = 0;
+ nsresult rv = reader->Read(aBuffer, aCount, &bytes);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return bytes == aCount ? NS_OK : NS_ERROR_FAILURE;
+}
+
+nsresult CloneableWithRangeMediaResource::ReadAt(int64_t aOffset, char* aBuffer,
+ uint32_t aCount,
+ uint32_t* aBytes) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ RefPtr<InputStreamReader> reader =
+ InputStreamReader::Create(mStream, aOffset, aCount);
+ if (!reader) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv = reader->Read(aBuffer, aCount, aBytes);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/dom/media/CloneableWithRangeMediaResource.h b/dom/media/CloneableWithRangeMediaResource.h
new file mode 100644
index 0000000000..1c0bab3c1b
--- /dev/null
+++ b/dom/media/CloneableWithRangeMediaResource.h
@@ -0,0 +1,101 @@
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_media_CloneableWithRangeMediaResource_h
+#define mozilla_dom_media_CloneableWithRangeMediaResource_h
+
+#include "BaseMediaResource.h"
+#include "nsICloneableInputStream.h"
+
+namespace mozilla {
+
+class CloneableWithRangeMediaResource : public BaseMediaResource {
+ public:
+ CloneableWithRangeMediaResource(MediaResourceCallback* aCallback,
+ nsIChannel* aChannel, nsIURI* aURI,
+ nsIInputStream* aStream, uint64_t aSize)
+ : BaseMediaResource(aCallback, aChannel, aURI),
+ mStream(do_QueryInterface(aStream)),
+ mSize(aSize),
+ mInitialized(false) {
+ MOZ_ASSERT(mStream);
+ }
+
+ ~CloneableWithRangeMediaResource() = default;
+
+ // Main thread
+ nsresult Open(nsIStreamListener** aStreamListener) override;
+ RefPtr<GenericPromise> Close() override;
+ void Suspend(bool aCloseImmediately) override {}
+ void Resume() override {}
+ already_AddRefed<nsIPrincipal> GetCurrentPrincipal() override;
+ bool HadCrossOriginRedirects() override;
+ nsresult ReadFromCache(char* aBuffer, int64_t aOffset,
+ uint32_t aCount) override;
+
+ // These methods are called off the main thread.
+
+ // Other thread
+ void SetReadMode(MediaCacheStream::ReadMode aMode) override {}
+ void SetPlaybackRate(uint32_t aBytesPerSecond) override {}
+ nsresult ReadAt(int64_t aOffset, char* aBuffer, uint32_t aCount,
+ uint32_t* aBytes) override;
+ // (Probably) file-based, caching recommended.
+ bool ShouldCacheReads() override { return true; }
+
+ // Any thread
+ void Pin() override {}
+ void Unpin() override {}
+
+ double GetDownloadRate(bool* aIsReliable) override {
+ // The data's all already here
+ *aIsReliable = true;
+ return 100 * 1024 * 1024; // arbitray, use 100MB/s
+ }
+
+ int64_t GetLength() override {
+ MaybeInitialize();
+ return mSize;
+ }
+
+ int64_t GetNextCachedData(int64_t aOffset) override {
+ MaybeInitialize();
+ return (aOffset < (int64_t)mSize) ? aOffset : -1;
+ }
+
+ int64_t GetCachedDataEnd(int64_t aOffset) override {
+ MaybeInitialize();
+ return std::max(aOffset, (int64_t)mSize);
+ }
+
+ bool IsDataCachedToEndOfResource(int64_t aOffset) override { return true; }
+ bool IsTransportSeekable() override { return true; }
+
+ nsresult GetCachedRanges(MediaByteRangeSet& aRanges) override;
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return BaseMediaResource::SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ private:
+ void MaybeInitialize();
+
+ // Input stream for the media data. This can be used from any
+ // thread.
+ nsCOMPtr<nsICloneableInputStreamWithRange> mStream;
+
+ // The stream size.
+ uint64_t mSize;
+
+ bool mInitialized;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_dom_media_CloneableWithRangeMediaResource_h
diff --git a/dom/media/CrossGraphPort.cpp b/dom/media/CrossGraphPort.cpp
new file mode 100644
index 0000000000..da8de15551
--- /dev/null
+++ b/dom/media/CrossGraphPort.cpp
@@ -0,0 +1,205 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CrossGraphPort.h"
+
+#include "AudioDeviceInfo.h"
+#include "AudioStreamTrack.h"
+#include "MediaTrackGraphImpl.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Preferences.h"
+
+namespace mozilla {
+
+#ifdef LOG
+# undef LOG
+#endif
+#ifdef LOG_TEST
+# undef LOG_TEST
+#endif
+
+extern LazyLogModule gMediaTrackGraphLog;
+#define LOG(type, msg) MOZ_LOG(gMediaTrackGraphLog, type, msg)
+#define LOG_TEST(type) MOZ_LOG_TEST(gMediaTrackGraphLog, type)
+
+UniquePtr<CrossGraphPort> CrossGraphPort::Connect(
+ const RefPtr<dom::AudioStreamTrack>& aStreamTrack, AudioDeviceInfo* aSink,
+ nsPIDOMWindowInner* aWindow) {
+ MOZ_ASSERT(aSink);
+ MOZ_ASSERT(aStreamTrack);
+ uint32_t defaultRate;
+ aSink->GetDefaultRate(&defaultRate);
+ LOG(LogLevel::Debug,
+ ("CrossGraphPort::Connect: sink id: %p at rate %u, primary rate %d",
+ aSink->DeviceID(), defaultRate, aStreamTrack->Graph()->GraphRate()));
+
+ if (!aSink->DeviceID()) {
+ return nullptr;
+ }
+
+ MediaTrackGraph* newGraph =
+ MediaTrackGraph::GetInstance(MediaTrackGraph::AUDIO_THREAD_DRIVER,
+ aWindow, defaultRate, aSink->DeviceID());
+
+ return CrossGraphPort::Connect(aStreamTrack, newGraph);
+}
+
+UniquePtr<CrossGraphPort> CrossGraphPort::Connect(
+ const RefPtr<dom::AudioStreamTrack>& aStreamTrack,
+ MediaTrackGraph* aPartnerGraph) {
+ MOZ_ASSERT(aStreamTrack);
+ MOZ_ASSERT(aPartnerGraph);
+ if (aStreamTrack->Graph() == aPartnerGraph) {
+ // Primary graph the same as partner graph, just remove the existing cross
+ // graph connection
+ return nullptr;
+ }
+
+ RefPtr<CrossGraphReceiver> receiver = aPartnerGraph->CreateCrossGraphReceiver(
+ aStreamTrack->Graph()->GraphRate());
+
+ RefPtr<CrossGraphTransmitter> transmitter =
+ aStreamTrack->Graph()->CreateCrossGraphTransmitter(receiver);
+
+ RefPtr<MediaInputPort> port =
+ aStreamTrack->ForwardTrackContentsTo(transmitter);
+
+ return WrapUnique(new CrossGraphPort(std::move(port), std::move(transmitter),
+ std::move(receiver)));
+}
+
+void CrossGraphPort::AddAudioOutput(void* aKey) {
+ mReceiver->AddAudioOutput(aKey);
+}
+
+void CrossGraphPort::RemoveAudioOutput(void* aKey) {
+ mReceiver->RemoveAudioOutput(aKey);
+}
+
+void CrossGraphPort::SetAudioOutputVolume(void* aKey, float aVolume) {
+ mReceiver->SetAudioOutputVolume(aKey, aVolume);
+}
+
+CrossGraphPort::~CrossGraphPort() {
+ mTransmitter->Destroy();
+ mReceiver->Destroy();
+ mTransmitterPort->Destroy();
+}
+
+RefPtr<GenericPromise> CrossGraphPort::EnsureConnected() {
+ // The primary graph is already working check the partner (receiver's) graph.
+ return mReceiver->Graph()->NotifyWhenDeviceStarted(mReceiver.get());
+}
+
+/** CrossGraphTransmitter **/
+
+CrossGraphTransmitter::CrossGraphTransmitter(
+ TrackRate aSampleRate, RefPtr<CrossGraphReceiver> aReceiver)
+ : ProcessedMediaTrack(aSampleRate, MediaSegment::AUDIO,
+ nullptr /* aSegment */),
+ mReceiver(std::move(aReceiver)) {}
+
+void CrossGraphTransmitter::ProcessInput(GraphTime aFrom, GraphTime aTo,
+ uint32_t aFlags) {
+ MOZ_ASSERT(!mInputs.IsEmpty());
+ MOZ_ASSERT(mDisabledMode == DisabledTrackMode::ENABLED);
+
+ MediaTrack* input = mInputs[0]->GetSource();
+
+ if (input->Ended() &&
+ (input->GetEnd() <= input->GraphTimeToTrackTimeWithBlocking(aFrom))) {
+ mEnded = true;
+ return;
+ }
+
+ LOG(LogLevel::Verbose,
+ ("Transmitter (%p) mSegment: duration: %" PRId64 ", from %" PRId64
+ ", to %" PRId64 ", ticks %" PRId64 "",
+ this, mSegment->GetDuration(), aFrom, aTo, aTo - aFrom));
+
+ AudioSegment audio;
+ GraphTime next;
+ for (GraphTime t = aFrom; t < aTo; t = next) {
+ MediaInputPort::InputInterval interval =
+ MediaInputPort::GetNextInputInterval(mInputs[0], t);
+ interval.mEnd = std::min(interval.mEnd, aTo);
+
+ TrackTime ticks = interval.mEnd - interval.mStart;
+ next = interval.mEnd;
+
+ if (interval.mStart >= interval.mEnd) {
+ break;
+ }
+
+ if (interval.mInputIsBlocked) {
+ audio.AppendNullData(ticks);
+ } else if (input->IsSuspended()) {
+ audio.AppendNullData(ticks);
+ } else {
+ MOZ_ASSERT(GetEnd() == GraphTimeToTrackTimeWithBlocking(interval.mStart),
+ "Samples missing");
+ TrackTime inputStart =
+ input->GraphTimeToTrackTimeWithBlocking(interval.mStart);
+ TrackTime inputEnd =
+ input->GraphTimeToTrackTimeWithBlocking(interval.mEnd);
+ audio.AppendSlice(*input->GetData(), inputStart, inputEnd);
+ }
+ }
+
+ mStartTime = aTo;
+
+ for (AudioSegment::ChunkIterator iter(audio); !iter.IsEnded(); iter.Next()) {
+ Unused << mReceiver->EnqueueAudio(*iter);
+ }
+}
+
+/** CrossGraphReceiver **/
+
+CrossGraphReceiver::CrossGraphReceiver(TrackRate aSampleRate,
+ TrackRate aTransmitterRate)
+ : ProcessedMediaTrack(aSampleRate, MediaSegment::AUDIO,
+ static_cast<MediaSegment*>(new AudioSegment())),
+ mDriftCorrection(aTransmitterRate, aSampleRate,
+ Preferences::GetInt("media.clockdrift.buffering", 50),
+ PRINCIPAL_HANDLE_NONE) {}
+
+uint32_t CrossGraphReceiver::NumberOfChannels() const {
+ return GetData<AudioSegment>()->MaxChannelCount();
+}
+
+void CrossGraphReceiver::ProcessInput(GraphTime aFrom, GraphTime aTo,
+ uint32_t aFlags) {
+ LOG(LogLevel::Verbose,
+ ("Receiver (%p) mSegment: duration: %" PRId64 ", from %" PRId64
+ ", to %" PRId64 ", ticks %" PRId64 "",
+ this, mSegment->GetDuration(), aFrom, aTo, aTo - aFrom));
+
+ AudioSegment transmittedAudio;
+ while (mCrossThreadFIFO.AvailableRead()) {
+ AudioChunk chunk;
+ Unused << mCrossThreadFIFO.Dequeue(&chunk, 1);
+ transmittedAudio.AppendAndConsumeChunk(std::move(chunk));
+ mTransmitterHasStarted = true;
+ }
+
+ if (mTransmitterHasStarted) {
+ // If it does not have enough frames the result will be silence.
+ AudioSegment audioCorrected =
+ mDriftCorrection.RequestFrames(transmittedAudio, aTo - aFrom);
+ if (LOG_TEST(LogLevel::Verbose) && audioCorrected.IsNull()) {
+ LOG(LogLevel::Verbose,
+ ("Receiver(%p): Silence has been added, not enough input", this));
+ }
+ mSegment->AppendFrom(&audioCorrected);
+ } else {
+ mSegment->AppendNullData(aTo - aFrom);
+ }
+}
+
+int CrossGraphReceiver::EnqueueAudio(AudioChunk& aChunk) {
+ // This will take place on transmitter graph thread only.
+ return mCrossThreadFIFO.Enqueue(aChunk);
+}
+
+} // namespace mozilla
diff --git a/dom/media/CrossGraphPort.h b/dom/media/CrossGraphPort.h
new file mode 100644
index 0000000000..98d2a095d0
--- /dev/null
+++ b/dom/media/CrossGraphPort.h
@@ -0,0 +1,105 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_CROSS_GRAPH_TRACK_H_
+#define MOZILLA_CROSS_GRAPH_TRACK_H_
+
+#include "AudioDriftCorrection.h"
+#include "AudioSegment.h"
+#include "ForwardedInputTrack.h"
+#include "mozilla/SPSCQueue.h"
+#include "mozilla/UniquePtr.h"
+
+namespace mozilla {
+class CrossGraphReceiver;
+}
+
+namespace mozilla::dom {
+class AudioStreamTrack;
+}
+
+namespace mozilla {
+
+/**
+ * See MediaTrackGraph::CreateCrossGraphTransmitter()
+ */
+class CrossGraphTransmitter : public ProcessedMediaTrack {
+ public:
+ CrossGraphTransmitter(TrackRate aSampleRate,
+ RefPtr<CrossGraphReceiver> aReceiver);
+ CrossGraphTransmitter* AsCrossGraphTransmitter() override { return this; }
+
+ uint32_t NumberOfChannels() const override {
+ MOZ_CRASH("CrossGraphTransmitter has no segment. It cannot be played out.");
+ }
+ void ProcessInput(GraphTime aFrom, GraphTime aTo, uint32_t aFlags) override;
+
+ private:
+ const RefPtr<CrossGraphReceiver> mReceiver;
+};
+
+/**
+ * See MediaTrackGraph::CreateCrossGraphReceiver()
+ */
+class CrossGraphReceiver : public ProcessedMediaTrack {
+ public:
+ CrossGraphReceiver(TrackRate aSampleRate, TrackRate aTransmitterRate);
+ CrossGraphReceiver* AsCrossGraphReceiver() override { return this; }
+
+ uint32_t NumberOfChannels() const override;
+ void ProcessInput(GraphTime aFrom, GraphTime aTo, uint32_t aFlags) override;
+
+ int EnqueueAudio(AudioChunk& aChunk);
+
+ private:
+ SPSCQueue<AudioChunk> mCrossThreadFIFO{30};
+ // Indicates that tre CrossGraphTransmitter has started sending frames. It
+ // is false untill the point, transmitter has sent the first valid frame.
+ // Accessed in GraphThread only.
+ bool mTransmitterHasStarted = false;
+ // Correct the drift between transmitter and receiver. Reciever (this class)
+ // is considered as the master clock.
+ // Accessed in GraphThread only.
+ AudioDriftCorrection mDriftCorrection;
+};
+
+class CrossGraphPort final {
+ public:
+ static UniquePtr<CrossGraphPort> Connect(
+ const RefPtr<dom::AudioStreamTrack>& aStreamTrack,
+ MediaTrackGraph* aPartnerGraph);
+ static UniquePtr<CrossGraphPort> Connect(
+ const RefPtr<dom::AudioStreamTrack>& aStreamTrack, AudioDeviceInfo* aSink,
+ nsPIDOMWindowInner* aWindow);
+ ~CrossGraphPort();
+
+ void AddAudioOutput(void* aKey);
+ void RemoveAudioOutput(void* aKey);
+ void SetAudioOutputVolume(void* aKey, float aVolume);
+
+ RefPtr<GenericPromise> EnsureConnected();
+
+ const RefPtr<CrossGraphTransmitter> mTransmitter;
+ const RefPtr<CrossGraphReceiver> mReceiver;
+
+ private:
+ explicit CrossGraphPort(RefPtr<MediaInputPort> aTransmitterPort,
+ RefPtr<CrossGraphTransmitter> aTransmitter,
+ RefPtr<CrossGraphReceiver> aReceiver)
+ : mTransmitter(std::move(aTransmitter)),
+ mReceiver(std::move(aReceiver)),
+ mTransmitterPort(std::move(aTransmitterPort)) {
+ MOZ_ASSERT(mTransmitter);
+ MOZ_ASSERT(mReceiver);
+ MOZ_ASSERT(mTransmitterPort);
+ }
+
+ // The port that connects the input track to the transmitter.
+ const RefPtr<MediaInputPort> mTransmitterPort;
+};
+
+} // namespace mozilla
+
+#endif /* MOZILLA_CROSS_GRAPH_TRACK_H_ */
diff --git a/dom/media/CubebInputStream.cpp b/dom/media/CubebInputStream.cpp
new file mode 100644
index 0000000000..60eef92fe1
--- /dev/null
+++ b/dom/media/CubebInputStream.cpp
@@ -0,0 +1,172 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#include "CubebInputStream.h"
+
+#include "AudioSampleFormat.h"
+#include "mozilla/Logging.h"
+
+namespace mozilla {
+
+extern mozilla::LazyLogModule gMediaTrackGraphLog;
+
+#ifdef LOG_INTERNAL
+# undef LOG_INTERNAL
+#endif // LOG_INTERNAL
+#define LOG_INTERNAL(level, msg, ...) \
+ MOZ_LOG(gMediaTrackGraphLog, LogLevel::level, (msg, ##__VA_ARGS__))
+
+#ifdef LOG
+# undef LOG
+#endif // LOG
+#define LOG(msg, ...) LOG_INTERNAL(Debug, msg, ##__VA_ARGS__)
+
+#ifdef LOGE
+# undef LOGE
+#endif // LOGE
+#define LOGE(msg, ...) LOG_INTERNAL(Error, msg, ##__VA_ARGS__)
+
+#define InvokeCubebWithLog(func, ...) \
+ ({ \
+ int _retval; \
+ _retval = InvokeCubeb(func, ##__VA_ARGS__); \
+ if (_retval == CUBEB_OK) { \
+ LOG("CubebInputStream %p: %s for stream %p was successful", this, #func, \
+ mStream.get()); \
+ } else { \
+ LOGE("CubebInputStream %p: %s for stream %p was failed. Error %d", this, \
+ #func, mStream.get(), _retval); \
+ } \
+ _retval; \
+ })
+
+static cubeb_stream_params CreateStreamInitParams(uint32_t aChannels,
+ uint32_t aRate,
+ bool aIsVoice) {
+ cubeb_stream_params params;
+ params.format = CubebUtils::ToCubebFormat<AUDIO_OUTPUT_FORMAT>::value;
+ params.rate = aRate;
+ params.channels = aChannels;
+ params.layout = CUBEB_LAYOUT_UNDEFINED;
+ params.prefs = CubebUtils::GetDefaultStreamPrefs(CUBEB_DEVICE_TYPE_INPUT);
+
+ if (aIsVoice) {
+ params.prefs |= static_cast<cubeb_stream_prefs>(CUBEB_STREAM_PREF_VOICE);
+ }
+
+ return params;
+}
+
+void CubebInputStream::CubebDestroyPolicy::operator()(
+ cubeb_stream* aStream) const {
+ int r = cubeb_stream_register_device_changed_callback(aStream, nullptr);
+ if (r == CUBEB_OK) {
+ LOG("Unregister device changed callback for %p successfully", aStream);
+ } else {
+ LOGE("Fail to unregister device changed callback for %p. Error %d", aStream,
+ r);
+ }
+ cubeb_stream_destroy(aStream);
+}
+
+/* static */
+UniquePtr<CubebInputStream> CubebInputStream::Create(cubeb_devid aDeviceId,
+ uint32_t aChannels,
+ uint32_t aRate,
+ bool aIsVoice,
+ Listener* aListener) {
+ if (!aListener) {
+ LOGE("No available listener");
+ return nullptr;
+ }
+
+ cubeb* context = CubebUtils::GetCubebContext();
+ if (!context) {
+ LOGE("No valid cubeb context");
+ CubebUtils::ReportCubebStreamInitFailure(CubebUtils::GetFirstStream());
+ return nullptr;
+ }
+
+ cubeb_stream_params params =
+ CreateStreamInitParams(aChannels, aRate, aIsVoice);
+ uint32_t latencyFrames = CubebUtils::GetCubebMTGLatencyInFrames(&params);
+
+ cubeb_stream* cubebStream = nullptr;
+
+ RefPtr<Listener> listener(aListener);
+ if (int r = CubebUtils::CubebStreamInit(
+ context, &cubebStream, "input-only stream", aDeviceId, &params,
+ nullptr, nullptr, latencyFrames, DataCallback_s, StateCallback_s,
+ listener.get());
+ r != CUBEB_OK) {
+ CubebUtils::ReportCubebStreamInitFailure(CubebUtils::GetFirstStream());
+ LOGE("Fail to create a cubeb stream. Error %d", r);
+ return nullptr;
+ }
+
+ UniquePtr<cubeb_stream, CubebDestroyPolicy> inputStream(cubebStream);
+
+ LOG("Create a cubeb stream %p successfully", inputStream.get());
+
+ UniquePtr<CubebInputStream> stream(
+ new CubebInputStream(listener.forget(), std::move(inputStream)));
+ stream->Init();
+ return stream;
+}
+
+CubebInputStream::CubebInputStream(
+ already_AddRefed<Listener>&& aListener,
+ UniquePtr<cubeb_stream, CubebDestroyPolicy>&& aStream)
+ : mListener(aListener), mStream(std::move(aStream)) {
+ MOZ_ASSERT(mListener);
+ MOZ_ASSERT(mStream);
+}
+
+void CubebInputStream::Init() {
+ // cubeb_stream_register_device_changed_callback is only supported on macOS
+ // platform and MockCubebfor now.
+ InvokeCubebWithLog(cubeb_stream_register_device_changed_callback,
+ CubebInputStream::DeviceChangedCallback_s);
+}
+
+int CubebInputStream::Start() { return InvokeCubebWithLog(cubeb_stream_start); }
+
+int CubebInputStream::Stop() { return InvokeCubebWithLog(cubeb_stream_stop); }
+
+template <typename Function, typename... Args>
+int CubebInputStream::InvokeCubeb(Function aFunction, Args&&... aArgs) {
+ MOZ_ASSERT(mStream);
+ return aFunction(mStream.get(), std::forward<Args>(aArgs)...);
+}
+
+/* static */
+long CubebInputStream::DataCallback_s(cubeb_stream* aStream, void* aUser,
+ const void* aInputBuffer,
+ void* aOutputBuffer, long aFrames) {
+ MOZ_ASSERT(aUser);
+ MOZ_ASSERT(aInputBuffer);
+ MOZ_ASSERT(!aOutputBuffer);
+ return static_cast<Listener*>(aUser)->DataCallback(aInputBuffer, aFrames);
+}
+
+/* static */
+void CubebInputStream::StateCallback_s(cubeb_stream* aStream, void* aUser,
+ cubeb_state aState) {
+ MOZ_ASSERT(aUser);
+ static_cast<Listener*>(aUser)->StateCallback(aState);
+}
+
+/* static */
+void CubebInputStream::DeviceChangedCallback_s(void* aUser) {
+ MOZ_ASSERT(aUser);
+ static_cast<Listener*>(aUser)->DeviceChangedCallback();
+}
+
+#undef LOG_INTERNAL
+#undef LOG
+#undef LOGE
+
+} // namespace mozilla
diff --git a/dom/media/CubebInputStream.h b/dom/media/CubebInputStream.h
new file mode 100644
index 0000000000..a83d212896
--- /dev/null
+++ b/dom/media/CubebInputStream.h
@@ -0,0 +1,81 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_CUBEBINPUTSTREAM_H_
+#define DOM_MEDIA_CUBEBINPUTSTREAM_H_
+
+#include "CubebUtils.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "nsISupportsImpl.h"
+
+namespace mozilla {
+
+// A light-weight wrapper to operate the C style Cubeb APIs for an input-only
+// audio stream in a C++-friendly way.
+// Limitation: Do not call these APIs in an audio callback thread. Otherwise we
+// may get a deadlock.
+class CubebInputStream final {
+ public:
+ ~CubebInputStream() = default;
+
+ class Listener {
+ public:
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING;
+
+ // This will be fired on audio callback thread.
+ virtual long DataCallback(const void* aBuffer, long aFrames) = 0;
+ // This can be fired on any thread.
+ virtual void StateCallback(cubeb_state aState) = 0;
+ // This can be fired on any thread.
+ virtual void DeviceChangedCallback() = 0;
+
+ protected:
+ Listener() = default;
+ virtual ~Listener() = default;
+ };
+
+ // Return a non-null pointer if the stream has been initialized
+ // successfully. Otherwise return a null pointer.
+ static UniquePtr<CubebInputStream> Create(cubeb_devid aDeviceId,
+ uint32_t aChannels, uint32_t aRate,
+ bool aIsVoice, Listener* aListener);
+
+ // Start producing audio data.
+ int Start();
+
+ // Stop producing audio data.
+ int Stop();
+
+ private:
+ struct CubebDestroyPolicy {
+ void operator()(cubeb_stream* aStream) const;
+ };
+ CubebInputStream(already_AddRefed<Listener>&& aListener,
+ UniquePtr<cubeb_stream, CubebDestroyPolicy>&& aStream);
+
+ void Init();
+
+ template <typename Function, typename... Args>
+ int InvokeCubeb(Function aFunction, Args&&... aArgs);
+
+ // Static wrapper function cubeb callbacks.
+ static long DataCallback_s(cubeb_stream* aStream, void* aUser,
+ const void* aInputBuffer, void* aOutputBuffer,
+ long aFrames);
+ static void StateCallback_s(cubeb_stream* aStream, void* aUser,
+ cubeb_state aState);
+ static void DeviceChangedCallback_s(void* aUser);
+
+ // mListener must outlive the life time of the mStream.
+ const RefPtr<Listener> mListener;
+ const UniquePtr<cubeb_stream, CubebDestroyPolicy> mStream;
+};
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_CUBEBINPUTSTREAM_H_
diff --git a/dom/media/CubebUtils.cpp b/dom/media/CubebUtils.cpp
new file mode 100644
index 0000000000..4e7310e6f3
--- /dev/null
+++ b/dom/media/CubebUtils.cpp
@@ -0,0 +1,851 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CubebUtils.h"
+
+#include "audio_thread_priority.h"
+#include "mozilla/AbstractThread.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/ipc/FileDescriptor.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Components.h"
+#include "mozilla/Sprintf.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/UnderrunHandler.h"
+#include "nsContentUtils.h"
+#include "nsDebug.h"
+#include "nsIStringBundle.h"
+#include "nsString.h"
+#include "nsThreadUtils.h"
+#include "prdtoa.h"
+#include <algorithm>
+#include <stdint.h>
+#ifdef MOZ_WIDGET_ANDROID
+# include "mozilla/java/GeckoAppShellWrappers.h"
+#endif
+#ifdef XP_WIN
+# include "mozilla/mscom/EnsureMTA.h"
+#endif
+#include "audioipc2_server_ffi_generated.h"
+#include "audioipc2_client_ffi_generated.h"
+#include <cmath>
+#include <thread>
+#include "CallbackThreadRegistry.h"
+#include "mozilla/StaticPrefs_media.h"
+
+#define AUDIOIPC_STACK_SIZE_DEFAULT (64 * 4096)
+
+#define PREF_VOLUME_SCALE "media.volume_scale"
+#define PREF_CUBEB_BACKEND "media.cubeb.backend"
+#define PREF_CUBEB_OUTPUT_DEVICE "media.cubeb.output_device"
+#define PREF_CUBEB_LATENCY_PLAYBACK "media.cubeb_latency_playback_ms"
+#define PREF_CUBEB_LATENCY_MTG "media.cubeb_latency_mtg_frames"
+// Allows to get something non-default for the preferred sample-rate, to allow
+// troubleshooting in the field and testing.
+#define PREF_CUBEB_FORCE_SAMPLE_RATE "media.cubeb.force_sample_rate"
+#define PREF_CUBEB_LOGGING_LEVEL "logging.cubeb"
+// Hidden pref used by tests to force failure to obtain cubeb context
+#define PREF_CUBEB_FORCE_NULL_CONTEXT "media.cubeb.force_null_context"
+#define PREF_CUBEB_OUTPUT_VOICE_ROUTING "media.cubeb.output_voice_routing"
+#define PREF_CUBEB_SANDBOX "media.cubeb.sandbox"
+#define PREF_AUDIOIPC_STACK_SIZE "media.audioipc.stack_size"
+#define PREF_AUDIOIPC_SHM_AREA_SIZE "media.audioipc.shm_area_size"
+
+#if (defined(XP_LINUX) && !defined(MOZ_WIDGET_ANDROID)) || \
+ defined(XP_MACOSX) || defined(XP_WIN)
+# define MOZ_CUBEB_REMOTING
+#endif
+
+namespace mozilla {
+
+namespace {
+
+using Telemetry::LABELS_MEDIA_AUDIO_BACKEND;
+using Telemetry::LABELS_MEDIA_AUDIO_INIT_FAILURE;
+
+LazyLogModule gCubebLog("cubeb");
+
+void CubebLogCallback(const char* aFmt, ...) {
+ char buffer[1024];
+
+ va_list arglist;
+ va_start(arglist, aFmt);
+ VsprintfLiteral(buffer, aFmt, arglist);
+ MOZ_LOG(gCubebLog, LogLevel::Error, ("%s", buffer));
+ va_end(arglist);
+}
+
+// This mutex protects the variables below.
+StaticMutex sMutex;
+enum class CubebState {
+ Uninitialized = 0,
+ Initialized,
+ Shutdown
+} sCubebState = CubebState::Uninitialized;
+cubeb* sCubebContext;
+double sVolumeScale = 1.0;
+uint32_t sCubebPlaybackLatencyInMilliseconds = 100;
+uint32_t sCubebMTGLatencyInFrames = 512;
+// If sCubebForcedSampleRate is zero, PreferredSampleRate will return the
+// preferred sample-rate for the audio backend in use. Otherwise, it will be
+// used as the preferred sample-rate.
+Atomic<uint32_t> sCubebForcedSampleRate{0};
+bool sCubebPlaybackLatencyPrefSet = false;
+bool sCubebMTGLatencyPrefSet = false;
+bool sAudioStreamInitEverSucceeded = false;
+bool sCubebForceNullContext = false;
+bool sRouteOutputAsVoice = false;
+#ifdef MOZ_CUBEB_REMOTING
+bool sCubebSandbox = false;
+size_t sAudioIPCStackSize;
+size_t sAudioIPCShmAreaSize;
+#endif
+StaticAutoPtr<char> sBrandName;
+StaticAutoPtr<char> sCubebBackendName;
+StaticAutoPtr<char> sCubebOutputDeviceName;
+#ifdef MOZ_WIDGET_ANDROID
+// Counts the number of time a request for switching to global "communication
+// mode" has been received. If this is > 0, global communication mode is to be
+// enabled. If it is 0, the global communication mode is to be disabled.
+// This allows to correctly track the global behaviour to adopt accross
+// asynchronous GraphDriver changes, on Android.
+int sInCommunicationCount = 0;
+#endif
+
+const char kBrandBundleURL[] = "chrome://branding/locale/brand.properties";
+
+std::unordered_map<std::string, LABELS_MEDIA_AUDIO_BACKEND>
+ kTelemetryBackendLabel = {
+ {"audiounit", LABELS_MEDIA_AUDIO_BACKEND::audiounit},
+ {"audiounit-rust", LABELS_MEDIA_AUDIO_BACKEND::audiounit_rust},
+ {"aaudio", LABELS_MEDIA_AUDIO_BACKEND::aaudio},
+ {"opensl", LABELS_MEDIA_AUDIO_BACKEND::opensl},
+ {"wasapi", LABELS_MEDIA_AUDIO_BACKEND::wasapi},
+ {"winmm", LABELS_MEDIA_AUDIO_BACKEND::winmm},
+ {"alsa", LABELS_MEDIA_AUDIO_BACKEND::alsa},
+ {"jack", LABELS_MEDIA_AUDIO_BACKEND::jack},
+ {"oss", LABELS_MEDIA_AUDIO_BACKEND::oss},
+ {"pulse", LABELS_MEDIA_AUDIO_BACKEND::pulse},
+ {"pulse-rust", LABELS_MEDIA_AUDIO_BACKEND::pulse_rust},
+ {"sndio", LABELS_MEDIA_AUDIO_BACKEND::sndio},
+ {"sun", LABELS_MEDIA_AUDIO_BACKEND::sunaudio},
+};
+
+// Prefered samplerate, in Hz (characteristic of the hardware, mixer, platform,
+// and API used).
+//
+// sMutex protects *initialization* of this, which must be performed from each
+// thread before fetching, after which it is safe to fetch without holding the
+// mutex because it is only written once per process execution (by the first
+// initialization to complete). Since the init must have been called on a
+// given thread before fetching the value, it's guaranteed (via the mutex) that
+// sufficient memory barriers have occurred to ensure the correct value is
+// visible on the querying thread/CPU.
+static Atomic<uint32_t> sPreferredSampleRate{0};
+
+#ifdef MOZ_CUBEB_REMOTING
+// AudioIPC server handle
+void* sServerHandle = nullptr;
+
+// Initialized during early startup, protected by sMutex.
+StaticAutoPtr<ipc::FileDescriptor> sIPCConnection;
+
+static bool StartAudioIPCServer() {
+ if (sCubebSandbox) {
+ audioipc2::AudioIpcServerInitParams initParams{};
+ initParams.mThreadCreateCallback = [](const char* aName) {
+ PROFILER_REGISTER_THREAD(aName);
+ };
+ initParams.mThreadDestroyCallback = []() { PROFILER_UNREGISTER_THREAD(); };
+
+ sServerHandle = audioipc2::audioipc2_server_start(
+ sBrandName, sCubebBackendName, &initParams);
+ }
+ return sServerHandle != nullptr;
+}
+
+static void ShutdownAudioIPCServer() {
+ if (!sServerHandle) {
+ return;
+ }
+
+ audioipc2::audioipc2_server_stop(sServerHandle);
+ sServerHandle = nullptr;
+}
+#endif // MOZ_CUBEB_REMOTING
+} // namespace
+
+static const uint32_t CUBEB_NORMAL_LATENCY_MS = 100;
+// Consevative default that can work on all platforms.
+static const uint32_t CUBEB_NORMAL_LATENCY_FRAMES = 1024;
+
+namespace CubebUtils {
+cubeb* GetCubebContextUnlocked();
+
+void GetPrefAndSetString(const char* aPref, StaticAutoPtr<char>& aStorage) {
+ nsAutoCString value;
+ Preferences::GetCString(aPref, value);
+ if (value.IsEmpty()) {
+ aStorage = nullptr;
+ } else {
+ aStorage = new char[value.Length() + 1];
+ PodCopy(aStorage.get(), value.get(), value.Length());
+ aStorage[value.Length()] = 0;
+ }
+}
+
+void PrefChanged(const char* aPref, void* aClosure) {
+ if (strcmp(aPref, PREF_VOLUME_SCALE) == 0) {
+ nsAutoCString value;
+ Preferences::GetCString(aPref, value);
+ StaticMutexAutoLock lock(sMutex);
+ if (value.IsEmpty()) {
+ sVolumeScale = 1.0;
+ } else {
+ sVolumeScale = std::max<double>(0, PR_strtod(value.get(), nullptr));
+ }
+ } else if (strcmp(aPref, PREF_CUBEB_LATENCY_PLAYBACK) == 0) {
+ StaticMutexAutoLock lock(sMutex);
+ // Arbitrary default stream latency of 100ms. The higher this
+ // value, the longer stream volume changes will take to become
+ // audible.
+ sCubebPlaybackLatencyPrefSet = Preferences::HasUserValue(aPref);
+ uint32_t value = Preferences::GetUint(aPref, CUBEB_NORMAL_LATENCY_MS);
+ sCubebPlaybackLatencyInMilliseconds =
+ std::min<uint32_t>(std::max<uint32_t>(value, 1), 1000);
+ } else if (strcmp(aPref, PREF_CUBEB_LATENCY_MTG) == 0) {
+ StaticMutexAutoLock lock(sMutex);
+ sCubebMTGLatencyPrefSet = Preferences::HasUserValue(aPref);
+ uint32_t value = Preferences::GetUint(aPref, CUBEB_NORMAL_LATENCY_FRAMES);
+ // 128 is the block size for the Web Audio API, which limits how low the
+ // latency can be here.
+ // We don't want to limit the upper limit too much, so that people can
+ // experiment.
+ sCubebMTGLatencyInFrames =
+ std::min<uint32_t>(std::max<uint32_t>(value, 128), 1e6);
+ } else if (strcmp(aPref, PREF_CUBEB_FORCE_SAMPLE_RATE) == 0) {
+ StaticMutexAutoLock lock(sMutex);
+ sCubebForcedSampleRate = Preferences::GetUint(aPref);
+ } else if (strcmp(aPref, PREF_CUBEB_LOGGING_LEVEL) == 0) {
+ LogLevel value =
+ ToLogLevel(Preferences::GetInt(aPref, 0 /* LogLevel::Disabled */));
+ if (value == LogLevel::Verbose) {
+ cubeb_set_log_callback(CUBEB_LOG_VERBOSE, CubebLogCallback);
+ } else if (value == LogLevel::Debug) {
+ cubeb_set_log_callback(CUBEB_LOG_NORMAL, CubebLogCallback);
+ } else if (value == LogLevel::Disabled) {
+ cubeb_set_log_callback(CUBEB_LOG_DISABLED, nullptr);
+ }
+ } else if (strcmp(aPref, PREF_CUBEB_BACKEND) == 0) {
+ StaticMutexAutoLock lock(sMutex);
+ GetPrefAndSetString(aPref, sCubebBackendName);
+ } else if (strcmp(aPref, PREF_CUBEB_OUTPUT_DEVICE) == 0) {
+ StaticMutexAutoLock lock(sMutex);
+ GetPrefAndSetString(aPref, sCubebOutputDeviceName);
+ } else if (strcmp(aPref, PREF_CUBEB_FORCE_NULL_CONTEXT) == 0) {
+ StaticMutexAutoLock lock(sMutex);
+ sCubebForceNullContext = Preferences::GetBool(aPref, false);
+ MOZ_LOG(gCubebLog, LogLevel::Verbose,
+ ("%s: %s", PREF_CUBEB_FORCE_NULL_CONTEXT,
+ sCubebForceNullContext ? "true" : "false"));
+ }
+#ifdef MOZ_CUBEB_REMOTING
+ else if (strcmp(aPref, PREF_CUBEB_SANDBOX) == 0) {
+ StaticMutexAutoLock lock(sMutex);
+ sCubebSandbox = Preferences::GetBool(aPref);
+ MOZ_LOG(gCubebLog, LogLevel::Verbose,
+ ("%s: %s", PREF_CUBEB_SANDBOX, sCubebSandbox ? "true" : "false"));
+ } else if (strcmp(aPref, PREF_AUDIOIPC_STACK_SIZE) == 0) {
+ StaticMutexAutoLock lock(sMutex);
+ sAudioIPCStackSize = Preferences::GetUint(PREF_AUDIOIPC_STACK_SIZE,
+ AUDIOIPC_STACK_SIZE_DEFAULT);
+ } else if (strcmp(aPref, PREF_AUDIOIPC_SHM_AREA_SIZE) == 0) {
+ StaticMutexAutoLock lock(sMutex);
+ sAudioIPCShmAreaSize = Preferences::GetUint(PREF_AUDIOIPC_SHM_AREA_SIZE);
+ }
+#endif
+ else if (strcmp(aPref, PREF_CUBEB_OUTPUT_VOICE_ROUTING) == 0) {
+ StaticMutexAutoLock lock(sMutex);
+ sRouteOutputAsVoice = Preferences::GetBool(aPref);
+ MOZ_LOG(gCubebLog, LogLevel::Verbose,
+ ("%s: %s", PREF_CUBEB_OUTPUT_VOICE_ROUTING,
+ sRouteOutputAsVoice ? "true" : "false"));
+ }
+}
+
+bool GetFirstStream() {
+ static bool sFirstStream = true;
+
+ StaticMutexAutoLock lock(sMutex);
+ bool result = sFirstStream;
+ sFirstStream = false;
+ return result;
+}
+
+double GetVolumeScale() {
+ StaticMutexAutoLock lock(sMutex);
+ return sVolumeScale;
+}
+
+cubeb* GetCubebContext() {
+ StaticMutexAutoLock lock(sMutex);
+ return GetCubebContextUnlocked();
+}
+
+// This is only exported when running tests.
+void ForceSetCubebContext(cubeb* aCubebContext) {
+ StaticMutexAutoLock lock(sMutex);
+ sCubebContext = aCubebContext;
+ sCubebState = CubebState::Initialized;
+}
+
+void SetInCommunication(bool aInCommunication) {
+#ifdef MOZ_WIDGET_ANDROID
+ StaticMutexAutoLock lock(sMutex);
+ if (aInCommunication) {
+ sInCommunicationCount++;
+ } else {
+ MOZ_ASSERT(sInCommunicationCount > 0);
+ sInCommunicationCount--;
+ }
+
+ if (sInCommunicationCount == 1) {
+ java::GeckoAppShell::SetCommunicationAudioModeOn(true);
+ } else if (sInCommunicationCount == 0) {
+ java::GeckoAppShell::SetCommunicationAudioModeOn(false);
+ }
+#endif
+}
+
+bool InitPreferredSampleRate() {
+ StaticMutexAutoLock lock(sMutex);
+ if (sPreferredSampleRate != 0) {
+ return true;
+ }
+#ifdef MOZ_WIDGET_ANDROID
+ int rate = AndroidGetAudioOutputSampleRate();
+ if (rate > 0) {
+ sPreferredSampleRate = rate;
+ return true;
+ } else {
+ return false;
+ }
+#else
+ cubeb* context = GetCubebContextUnlocked();
+ if (!context) {
+ return false;
+ }
+ uint32_t rate;
+ if (cubeb_get_preferred_sample_rate(context, &rate) != CUBEB_OK) {
+ return false;
+ }
+ sPreferredSampleRate = rate;
+#endif
+ MOZ_ASSERT(sPreferredSampleRate);
+ return true;
+}
+
+uint32_t PreferredSampleRate(bool aShouldResistFingerprinting) {
+ if (sCubebForcedSampleRate) {
+ return sCubebForcedSampleRate;
+ }
+ if (aShouldResistFingerprinting) {
+ return 44100;
+ }
+ if (!InitPreferredSampleRate()) {
+ return 44100;
+ }
+ MOZ_ASSERT(sPreferredSampleRate);
+ return sPreferredSampleRate;
+}
+
+int CubebStreamInit(cubeb* context, cubeb_stream** stream,
+ char const* stream_name, cubeb_devid input_device,
+ cubeb_stream_params* input_stream_params,
+ cubeb_devid output_device,
+ cubeb_stream_params* output_stream_params,
+ uint32_t latency_frames, cubeb_data_callback data_callback,
+ cubeb_state_callback state_callback, void* user_ptr) {
+ uint32_t ms = StaticPrefs::media_cubeb_slow_stream_init_ms();
+ if (ms) {
+ std::this_thread::sleep_for(std::chrono::milliseconds(ms));
+ }
+ return cubeb_stream_init(context, stream, stream_name, input_device,
+ input_stream_params, output_device,
+ output_stream_params, latency_frames, data_callback,
+ state_callback, user_ptr);
+}
+
+void InitBrandName() {
+ if (sBrandName) {
+ return;
+ }
+ nsAutoString brandName;
+ nsCOMPtr<nsIStringBundleService> stringBundleService =
+ mozilla::components::StringBundle::Service();
+ if (stringBundleService) {
+ nsCOMPtr<nsIStringBundle> brandBundle;
+ nsresult rv = stringBundleService->CreateBundle(
+ kBrandBundleURL, getter_AddRefs(brandBundle));
+ if (NS_SUCCEEDED(rv)) {
+ rv = brandBundle->GetStringFromName("brandShortName", brandName);
+ NS_WARNING_ASSERTION(
+ NS_SUCCEEDED(rv),
+ "Could not get the program name for a cubeb stream.");
+ }
+ }
+ NS_LossyConvertUTF16toASCII ascii(brandName);
+ sBrandName = new char[ascii.Length() + 1];
+ PodCopy(sBrandName.get(), ascii.get(), ascii.Length());
+ sBrandName[ascii.Length()] = 0;
+}
+
+#ifdef MOZ_CUBEB_REMOTING
+void InitAudioIPCConnection() {
+ MOZ_ASSERT(NS_IsMainThread());
+ auto contentChild = dom::ContentChild::GetSingleton();
+ auto promise = contentChild->SendCreateAudioIPCConnection();
+ promise->Then(
+ AbstractThread::MainThread(), __func__,
+ [](dom::FileDescOrError&& aFD) {
+ StaticMutexAutoLock lock(sMutex);
+ MOZ_ASSERT(!sIPCConnection);
+ if (aFD.type() == dom::FileDescOrError::Type::TFileDescriptor) {
+ sIPCConnection = new ipc::FileDescriptor(std::move(aFD));
+ } else {
+ MOZ_LOG(gCubebLog, LogLevel::Error,
+ ("SendCreateAudioIPCConnection failed: invalid FD"));
+ }
+ },
+ [](mozilla::ipc::ResponseRejectReason&& aReason) {
+ MOZ_LOG(gCubebLog, LogLevel::Error,
+ ("SendCreateAudioIPCConnection rejected: %d", int(aReason)));
+ });
+}
+#endif
+
+#ifdef MOZ_CUBEB_REMOTING
+ipc::FileDescriptor CreateAudioIPCConnectionUnlocked() {
+ MOZ_ASSERT(sCubebSandbox && XRE_IsParentProcess());
+ if (!sServerHandle) {
+ MOZ_LOG(gCubebLog, LogLevel::Debug, ("Starting cubeb server..."));
+ if (!StartAudioIPCServer()) {
+ MOZ_LOG(gCubebLog, LogLevel::Error, ("audioipc_server_start failed"));
+ return ipc::FileDescriptor();
+ }
+ }
+ MOZ_LOG(gCubebLog, LogLevel::Debug,
+ ("%s: %d", PREF_AUDIOIPC_SHM_AREA_SIZE, (int)sAudioIPCShmAreaSize));
+ MOZ_ASSERT(sServerHandle);
+ ipc::FileDescriptor::PlatformHandleType rawFD;
+ rawFD = audioipc2::audioipc2_server_new_client(sServerHandle,
+ sAudioIPCShmAreaSize);
+ ipc::FileDescriptor fd(rawFD);
+ if (!fd.IsValid()) {
+ MOZ_LOG(gCubebLog, LogLevel::Error, ("audioipc_server_new_client failed"));
+ return ipc::FileDescriptor();
+ }
+ // Close rawFD since FileDescriptor's ctor cloned it.
+ // TODO: Find cleaner cross-platform way to close rawFD.
+# ifdef XP_WIN
+ CloseHandle(rawFD);
+# else
+ close(rawFD);
+# endif
+ return fd;
+}
+#endif
+
+ipc::FileDescriptor CreateAudioIPCConnection() {
+#ifdef MOZ_CUBEB_REMOTING
+ StaticMutexAutoLock lock(sMutex);
+ return CreateAudioIPCConnectionUnlocked();
+#else
+ return ipc::FileDescriptor();
+#endif
+}
+
+cubeb* GetCubebContextUnlocked() {
+ sMutex.AssertCurrentThreadOwns();
+ if (sCubebForceNullContext) {
+ // Pref set such that we should return a null context
+ MOZ_LOG(gCubebLog, LogLevel::Debug,
+ ("%s: returning null context due to %s!", __func__,
+ PREF_CUBEB_FORCE_NULL_CONTEXT));
+ return nullptr;
+ }
+ if (sCubebState != CubebState::Uninitialized) {
+ // If we have already passed the initialization point (below), just return
+ // the current context, which may be null (e.g., after error or shutdown.)
+ return sCubebContext;
+ }
+
+ if (!sBrandName && NS_IsMainThread()) {
+ InitBrandName();
+ } else {
+ NS_WARNING_ASSERTION(
+ sBrandName,
+ "Did not initialize sbrandName, and not on the main thread?");
+ }
+
+ int rv = CUBEB_ERROR;
+#ifdef MOZ_CUBEB_REMOTING
+ MOZ_LOG(gCubebLog, LogLevel::Info,
+ ("%s: %s", PREF_CUBEB_SANDBOX, sCubebSandbox ? "true" : "false"));
+
+ if (sCubebSandbox) {
+ if (XRE_IsParentProcess() && !sIPCConnection) {
+ // TODO: Don't use audio IPC when within the same process.
+ auto fd = CreateAudioIPCConnectionUnlocked();
+ if (fd.IsValid()) {
+ sIPCConnection = new ipc::FileDescriptor(fd);
+ }
+ }
+ if (NS_WARN_IF(!sIPCConnection)) {
+ // Either the IPC connection failed to init or we're still waiting for
+ // InitAudioIPCConnection to complete (bug 1454782).
+ return nullptr;
+ }
+
+ MOZ_LOG(gCubebLog, LogLevel::Debug,
+ ("%s: %d", PREF_AUDIOIPC_STACK_SIZE, (int)sAudioIPCStackSize));
+
+ audioipc2::AudioIpcInitParams initParams{};
+ initParams.mStackSize = sAudioIPCStackSize;
+ initParams.mServerConnection =
+ sIPCConnection->ClonePlatformHandle().release();
+ initParams.mThreadCreateCallback = [](const char* aName) {
+ PROFILER_REGISTER_THREAD(aName);
+ };
+ initParams.mThreadDestroyCallback = []() { PROFILER_UNREGISTER_THREAD(); };
+
+ rv = audioipc2::audioipc2_client_init(&sCubebContext, sBrandName,
+ &initParams);
+ } else {
+#endif // MOZ_CUBEB_REMOTING
+#ifdef XP_WIN
+ mozilla::mscom::EnsureMTA([&]() -> void {
+#endif
+ rv = cubeb_init(&sCubebContext, sBrandName, sCubebBackendName);
+#ifdef XP_WIN
+ });
+#endif
+#ifdef MOZ_CUBEB_REMOTING
+ }
+ sIPCConnection = nullptr;
+#endif // MOZ_CUBEB_REMOTING
+ NS_WARNING_ASSERTION(rv == CUBEB_OK, "Could not get a cubeb context.");
+ sCubebState =
+ (rv == CUBEB_OK) ? CubebState::Initialized : CubebState::Uninitialized;
+
+ return sCubebContext;
+}
+
+void ReportCubebBackendUsed() {
+ StaticMutexAutoLock lock(sMutex);
+
+ sAudioStreamInitEverSucceeded = true;
+
+ LABELS_MEDIA_AUDIO_BACKEND label = LABELS_MEDIA_AUDIO_BACKEND::unknown;
+ auto backend =
+ kTelemetryBackendLabel.find(cubeb_get_backend_id(sCubebContext));
+ if (backend != kTelemetryBackendLabel.end()) {
+ label = backend->second;
+ }
+ AccumulateCategorical(label);
+}
+
+void ReportCubebStreamInitFailure(bool aIsFirst) {
+ StaticMutexAutoLock lock(sMutex);
+ if (!aIsFirst && !sAudioStreamInitEverSucceeded) {
+ // This machine has no audio hardware, or it's in really bad shape, don't
+ // send this info, since we want CUBEB_BACKEND_INIT_FAILURE_OTHER to detect
+ // failures to open multiple streams in a process over time.
+ return;
+ }
+ AccumulateCategorical(aIsFirst ? LABELS_MEDIA_AUDIO_INIT_FAILURE::first
+ : LABELS_MEDIA_AUDIO_INIT_FAILURE::other);
+}
+
+uint32_t GetCubebPlaybackLatencyInMilliseconds() {
+ StaticMutexAutoLock lock(sMutex);
+ return sCubebPlaybackLatencyInMilliseconds;
+}
+
+bool CubebPlaybackLatencyPrefSet() {
+ StaticMutexAutoLock lock(sMutex);
+ return sCubebPlaybackLatencyPrefSet;
+}
+
+bool CubebMTGLatencyPrefSet() {
+ StaticMutexAutoLock lock(sMutex);
+ return sCubebMTGLatencyPrefSet;
+}
+
+uint32_t GetCubebMTGLatencyInFrames(cubeb_stream_params* params) {
+ StaticMutexAutoLock lock(sMutex);
+ if (sCubebMTGLatencyPrefSet) {
+ MOZ_ASSERT(sCubebMTGLatencyInFrames > 0);
+ return sCubebMTGLatencyInFrames;
+ }
+
+#ifdef MOZ_WIDGET_ANDROID
+ int frames = AndroidGetAudioOutputFramesPerBuffer();
+ if (frames > 0) {
+ return frames;
+ } else {
+ return 512;
+ }
+#else
+ cubeb* context = GetCubebContextUnlocked();
+ if (!context) {
+ return sCubebMTGLatencyInFrames; // default 512
+ }
+ uint32_t latency_frames = 0;
+ if (cubeb_get_min_latency(context, params, &latency_frames) != CUBEB_OK) {
+ NS_WARNING("Could not get minimal latency from cubeb.");
+ return sCubebMTGLatencyInFrames; // default 512
+ }
+ return latency_frames;
+#endif
+}
+
+static const char* gInitCallbackPrefs[] = {
+ PREF_VOLUME_SCALE, PREF_CUBEB_OUTPUT_DEVICE,
+ PREF_CUBEB_LATENCY_PLAYBACK, PREF_CUBEB_LATENCY_MTG,
+ PREF_CUBEB_BACKEND, PREF_CUBEB_FORCE_NULL_CONTEXT,
+ PREF_CUBEB_SANDBOX, PREF_AUDIOIPC_STACK_SIZE,
+ PREF_AUDIOIPC_SHM_AREA_SIZE, nullptr,
+};
+
+static const char* gCallbackPrefs[] = {
+ PREF_CUBEB_FORCE_SAMPLE_RATE,
+ // We don't want to call the callback on startup, because the pref is the
+ // empty string by default ("", which means "logging disabled"). Because the
+ // logging can be enabled via environment variables (MOZ_LOG="module:5"),
+ // calling this callback on init would immediately re-disable the logging.
+ PREF_CUBEB_LOGGING_LEVEL,
+ nullptr,
+};
+
+void InitLibrary() {
+ Preferences::RegisterCallbacksAndCall(PrefChanged, gInitCallbackPrefs);
+ Preferences::RegisterCallbacks(PrefChanged, gCallbackPrefs);
+
+ if (MOZ_LOG_TEST(gCubebLog, LogLevel::Verbose)) {
+ cubeb_set_log_callback(CUBEB_LOG_VERBOSE, CubebLogCallback);
+ } else if (MOZ_LOG_TEST(gCubebLog, LogLevel::Error)) {
+ cubeb_set_log_callback(CUBEB_LOG_NORMAL, CubebLogCallback);
+ }
+
+#ifndef MOZ_WIDGET_ANDROID
+ NS_DispatchToMainThread(
+ NS_NewRunnableFunction("CubebUtils::InitLibrary", &InitBrandName));
+#endif
+#ifdef MOZ_CUBEB_REMOTING
+ if (sCubebSandbox && XRE_IsContentProcess()) {
+# ifdef XP_LINUX
+ if (atp_set_real_time_limit(0, 48000)) {
+ NS_WARNING("could not set real-time limit in CubebUtils::InitLibrary");
+ }
+ InstallSoftRealTimeLimitHandler();
+# endif
+ InitAudioIPCConnection();
+ }
+#endif
+
+ // Ensure the CallbackThreadRegistry is not created in an audio callback by
+ // creating it now.
+ Unused << CallbackThreadRegistry::Get();
+}
+
+void ShutdownLibrary() {
+ Preferences::UnregisterCallbacks(PrefChanged, gInitCallbackPrefs);
+ Preferences::UnregisterCallbacks(PrefChanged, gCallbackPrefs);
+
+ StaticMutexAutoLock lock(sMutex);
+ cubeb_set_log_callback(CUBEB_LOG_DISABLED, nullptr);
+ if (sCubebContext) {
+ cubeb_destroy(sCubebContext);
+ sCubebContext = nullptr;
+ }
+ sBrandName = nullptr;
+ sCubebBackendName = nullptr;
+ // This will ensure we don't try to re-create a context.
+ sCubebState = CubebState::Shutdown;
+
+#ifdef MOZ_CUBEB_REMOTING
+ sIPCConnection = nullptr;
+ ShutdownAudioIPCServer();
+#endif
+}
+
+bool SandboxEnabled() {
+#ifdef MOZ_CUBEB_REMOTING
+ StaticMutexAutoLock lock(sMutex);
+ return !!sCubebSandbox;
+#else
+ return false;
+#endif
+}
+
+uint32_t MaxNumberOfChannels() {
+ cubeb* cubebContext = GetCubebContext();
+ uint32_t maxNumberOfChannels;
+ if (cubebContext && cubeb_get_max_channel_count(
+ cubebContext, &maxNumberOfChannels) == CUBEB_OK) {
+ return maxNumberOfChannels;
+ }
+
+ return 0;
+}
+
+void GetCurrentBackend(nsAString& aBackend) {
+ cubeb* cubebContext = GetCubebContext();
+ if (cubebContext) {
+ const char* backend = cubeb_get_backend_id(cubebContext);
+ if (backend) {
+ aBackend.AssignASCII(backend);
+ return;
+ }
+ }
+ aBackend.AssignLiteral("unknown");
+}
+
+char* GetForcedOutputDevice() {
+ StaticMutexAutoLock lock(sMutex);
+ return sCubebOutputDeviceName;
+}
+
+cubeb_stream_prefs GetDefaultStreamPrefs(cubeb_device_type aType) {
+ cubeb_stream_prefs prefs = CUBEB_STREAM_PREF_NONE;
+#ifdef XP_WIN
+ if (StaticPrefs::media_cubeb_wasapi_raw() & static_cast<uint32_t>(aType)) {
+ prefs |= CUBEB_STREAM_PREF_RAW;
+ }
+#endif
+ return prefs;
+}
+
+bool RouteOutputAsVoice() { return sRouteOutputAsVoice; }
+
+long datacb(cubeb_stream*, void*, const void*, void* out_buffer, long nframes) {
+ PodZero(static_cast<float*>(out_buffer), nframes * 2);
+ return nframes;
+}
+
+void statecb(cubeb_stream*, void*, cubeb_state) {}
+
+bool EstimatedRoundTripLatencyDefaultDevices(double* aMean, double* aStdDev) {
+ nsTArray<double> roundtripLatencies;
+ // Create a cubeb stream with the correct latency and default input/output
+ // devices (mono/stereo channels). Wait for two seconds, get the latency a few
+ // times.
+ int rv;
+ uint32_t rate;
+ uint32_t latencyFrames;
+ rv = cubeb_get_preferred_sample_rate(GetCubebContext(), &rate);
+ if (rv != CUBEB_OK) {
+ MOZ_LOG(gCubebLog, LogLevel::Error, ("Could not get preferred rate"));
+ return false;
+ }
+
+ cubeb_stream_params output_params;
+ output_params.format = CUBEB_SAMPLE_FLOAT32NE;
+ output_params.rate = rate;
+ output_params.channels = 2;
+ output_params.layout = CUBEB_LAYOUT_UNDEFINED;
+ output_params.prefs = GetDefaultStreamPrefs(CUBEB_DEVICE_TYPE_OUTPUT);
+
+ latencyFrames = GetCubebMTGLatencyInFrames(&output_params);
+
+ cubeb_stream_params input_params;
+ input_params.format = CUBEB_SAMPLE_FLOAT32NE;
+ input_params.rate = rate;
+ input_params.channels = 1;
+ input_params.layout = CUBEB_LAYOUT_UNDEFINED;
+ input_params.prefs = GetDefaultStreamPrefs(CUBEB_DEVICE_TYPE_INPUT);
+
+ cubeb_stream* stm;
+ rv = cubeb_stream_init(GetCubebContext(), &stm,
+ "about:support latency estimation", NULL,
+ &input_params, NULL, &output_params, latencyFrames,
+ datacb, statecb, NULL);
+ if (rv != CUBEB_OK) {
+ MOZ_LOG(gCubebLog, LogLevel::Error, ("Could not get init stream"));
+ return false;
+ }
+
+ rv = cubeb_stream_start(stm);
+ if (rv != CUBEB_OK) {
+ MOZ_LOG(gCubebLog, LogLevel::Error, ("Could not start stream"));
+ return false;
+ }
+ // +-2s
+ for (uint32_t i = 0; i < 40; i++) {
+ std::this_thread::sleep_for(std::chrono::milliseconds(50));
+ uint32_t inputLatency, outputLatency, rvIn, rvOut;
+ rvOut = cubeb_stream_get_latency(stm, &outputLatency);
+ if (rvOut) {
+ MOZ_LOG(gCubebLog, LogLevel::Error, ("Could not get output latency"));
+ }
+ rvIn = cubeb_stream_get_input_latency(stm, &inputLatency);
+ if (rvIn) {
+ MOZ_LOG(gCubebLog, LogLevel::Error, ("Could not get input latency"));
+ }
+ if (rvIn != CUBEB_OK || rvOut != CUBEB_OK) {
+ continue;
+ }
+
+ double roundTrip = static_cast<double>(outputLatency + inputLatency) / rate;
+ roundtripLatencies.AppendElement(roundTrip);
+ }
+ rv = cubeb_stream_stop(stm);
+ if (rv != CUBEB_OK) {
+ MOZ_LOG(gCubebLog, LogLevel::Error, ("Could not stop the stream"));
+ }
+
+ *aMean = 0.0;
+ *aStdDev = 0.0;
+ double variance = 0.0;
+ for (uint32_t i = 0; i < roundtripLatencies.Length(); i++) {
+ *aMean += roundtripLatencies[i];
+ }
+
+ *aMean /= roundtripLatencies.Length();
+
+ for (uint32_t i = 0; i < roundtripLatencies.Length(); i++) {
+ variance += pow(roundtripLatencies[i] - *aMean, 2.);
+ }
+ variance /= roundtripLatencies.Length();
+
+ *aStdDev = sqrt(variance);
+
+ MOZ_LOG(gCubebLog, LogLevel::Debug,
+ ("Default device roundtrip latency in seconds %lf (stddev: %lf)",
+ *aMean, *aStdDev));
+
+ cubeb_stream_destroy(stm);
+
+ return true;
+}
+
+#ifdef MOZ_WIDGET_ANDROID
+int32_t AndroidGetAudioOutputSampleRate() {
+ int32_t sample_rate = java::GeckoAppShell::GetAudioOutputSampleRate();
+ return sample_rate;
+}
+int32_t AndroidGetAudioOutputFramesPerBuffer() {
+ int32_t frames = java::GeckoAppShell::GetAudioOutputFramesPerBuffer();
+ return frames;
+}
+#endif
+
+} // namespace CubebUtils
+} // namespace mozilla
diff --git a/dom/media/CubebUtils.h b/dom/media/CubebUtils.h
new file mode 100644
index 0000000000..03512ec5f5
--- /dev/null
+++ b/dom/media/CubebUtils.h
@@ -0,0 +1,102 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(CubebUtils_h_)
+# define CubebUtils_h_
+
+# include "cubeb/cubeb.h"
+
+# include "AudioSampleFormat.h"
+# include "nsString.h"
+
+class AudioDeviceInfo;
+
+MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(cubeb_stream_prefs)
+
+namespace mozilla {
+
+class CallbackThreadRegistry;
+
+namespace CubebUtils {
+
+typedef cubeb_devid AudioDeviceID;
+
+template <AudioSampleFormat N>
+struct ToCubebFormat {
+ static const cubeb_sample_format value = CUBEB_SAMPLE_FLOAT32NE;
+};
+
+template <>
+struct ToCubebFormat<AUDIO_FORMAT_S16> {
+ static const cubeb_sample_format value = CUBEB_SAMPLE_S16NE;
+};
+
+// Initialize Audio Library. Some Audio backends require initializing the
+// library before using it.
+void InitLibrary();
+
+// Shutdown Audio Library. Some Audio backends require shutting down the
+// library after using it.
+void ShutdownLibrary();
+
+bool SandboxEnabled();
+
+// Returns the maximum number of channels supported by the audio hardware.
+uint32_t MaxNumberOfChannels();
+
+// Get the sample rate the hardware/mixer runs at. Thread safe.
+uint32_t PreferredSampleRate(bool aShouldResistFingerprinting);
+
+// Initialize a cubeb stream. A pass through wrapper for cubeb_stream_init,
+// that can simulate streams that are very slow to start, by setting the pref
+// media.cubeb.slow_stream_init_ms.
+int CubebStreamInit(cubeb* context, cubeb_stream** stream,
+ char const* stream_name, cubeb_devid input_device,
+ cubeb_stream_params* input_stream_params,
+ cubeb_devid output_device,
+ cubeb_stream_params* output_stream_params,
+ uint32_t latency_frames, cubeb_data_callback data_callback,
+ cubeb_state_callback state_callback, void* user_ptr);
+
+enum Side { Input, Output };
+
+double GetVolumeScale();
+bool GetFirstStream();
+cubeb* GetCubebContext();
+void ReportCubebStreamInitFailure(bool aIsFirstStream);
+void ReportCubebBackendUsed();
+uint32_t GetCubebPlaybackLatencyInMilliseconds();
+uint32_t GetCubebMTGLatencyInFrames(cubeb_stream_params* params);
+bool CubebLatencyPrefSet();
+void GetCurrentBackend(nsAString& aBackend);
+cubeb_stream_prefs GetDefaultStreamPrefs(cubeb_device_type aType);
+char* GetForcedOutputDevice();
+// No-op on all platforms but Android, where it tells the device's AudioManager
+// to switch to "communication mode", which might change audio routing,
+// bluetooth communication type, etc.
+void SetInCommunication(bool aInCommunication);
+// Returns true if the output streams should be routed like a stream containing
+// voice data, and not generic audio. This can influence audio processing and
+// device selection.
+bool RouteOutputAsVoice();
+// Returns, in seconds, the roundtrip latency Gecko thinks there is between the
+// default input and output devices. This is for diagnosing purposes, the
+// latency figures are best used directly from the cubeb streams themselves, as
+// the devices being used matter. This is blocking.
+bool EstimatedRoundTripLatencyDefaultDevices(double* aMean, double* aStdDev);
+
+# ifdef MOZ_WIDGET_ANDROID
+int32_t AndroidGetAudioOutputSampleRate();
+int32_t AndroidGetAudioOutputFramesPerBuffer();
+# endif
+
+# ifdef ENABLE_SET_CUBEB_BACKEND
+void ForceSetCubebContext(cubeb* aCubebContext);
+# endif
+} // namespace CubebUtils
+} // namespace mozilla
+
+#endif // CubebUtils_h_
diff --git a/dom/media/DOMMediaStream.cpp b/dom/media/DOMMediaStream.cpp
new file mode 100644
index 0000000000..81f7019e3a
--- /dev/null
+++ b/dom/media/DOMMediaStream.cpp
@@ -0,0 +1,537 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DOMMediaStream.h"
+
+#include "AudioCaptureTrack.h"
+#include "AudioChannelAgent.h"
+#include "AudioStreamTrack.h"
+#include "MediaTrackGraph.h"
+#include "MediaTrackGraphImpl.h"
+#include "MediaTrackListener.h"
+#include "Tracing.h"
+#include "VideoStreamTrack.h"
+#include "mozilla/dom/AudioTrack.h"
+#include "mozilla/dom/AudioTrackList.h"
+#include "mozilla/dom/DocGroup.h"
+#include "mozilla/dom/HTMLCanvasElement.h"
+#include "mozilla/dom/MediaStreamBinding.h"
+#include "mozilla/dom/MediaStreamTrackEvent.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/VideoTrack.h"
+#include "mozilla/dom/VideoTrackList.h"
+#include "mozilla/media/MediaUtils.h"
+#include "nsContentUtils.h"
+#include "nsGlobalWindowInner.h"
+#include "nsIUUIDGenerator.h"
+#include "nsPIDOMWindow.h"
+#include "nsProxyRelease.h"
+#include "nsRFPService.h"
+#include "nsServiceManagerUtils.h"
+
+#ifdef LOG
+# undef LOG
+#endif
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using namespace mozilla::layers;
+using namespace mozilla::media;
+
+static LazyLogModule gMediaStreamLog("MediaStream");
+#define LOG(type, msg) MOZ_LOG(gMediaStreamLog, type, msg)
+
+static bool ContainsLiveTracks(
+ const nsTArray<RefPtr<MediaStreamTrack>>& aTracks) {
+ for (const auto& track : aTracks) {
+ if (track->ReadyState() == MediaStreamTrackState::Live) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+static bool ContainsLiveAudioTracks(
+ const nsTArray<RefPtr<MediaStreamTrack>>& aTracks) {
+ for (const auto& track : aTracks) {
+ if (track->AsAudioStreamTrack() &&
+ track->ReadyState() == MediaStreamTrackState::Live) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+class DOMMediaStream::PlaybackTrackListener : public MediaStreamTrackConsumer {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(PlaybackTrackListener)
+
+ explicit PlaybackTrackListener(DOMMediaStream* aStream) : mStream(aStream) {}
+
+ void NotifyEnded(MediaStreamTrack* aTrack) override {
+ if (!mStream) {
+ return;
+ }
+
+ if (!aTrack) {
+ MOZ_ASSERT(false);
+ return;
+ }
+
+ MOZ_ASSERT(mStream->HasTrack(*aTrack));
+ mStream->NotifyTrackRemoved(aTrack);
+ }
+
+ protected:
+ virtual ~PlaybackTrackListener() = default;
+
+ WeakPtr<DOMMediaStream> mStream;
+};
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(DOMMediaStream)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(DOMMediaStream,
+ DOMEventTargetHelper)
+ tmp->Destroy();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mTracks)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mConsumersToKeepAlive)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_PTR
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(DOMMediaStream,
+ DOMEventTargetHelper)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTracks)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mConsumersToKeepAlive)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_ADDREF_INHERITED(DOMMediaStream, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(DOMMediaStream, DOMEventTargetHelper)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DOMMediaStream)
+ NS_INTERFACE_MAP_ENTRY_CONCRETE(DOMMediaStream)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+DOMMediaStream::DOMMediaStream(nsPIDOMWindowInner* aWindow)
+ : DOMEventTargetHelper(aWindow),
+ mPlaybackTrackListener(MakeAndAddRef<PlaybackTrackListener>(this)) {
+ nsresult rv;
+ nsCOMPtr<nsIUUIDGenerator> uuidgen =
+ do_GetService("@mozilla.org/uuid-generator;1", &rv);
+
+ if (NS_SUCCEEDED(rv) && uuidgen) {
+ nsID uuid;
+ memset(&uuid, 0, sizeof(uuid));
+ rv = uuidgen->GenerateUUIDInPlace(&uuid);
+ if (NS_SUCCEEDED(rv)) {
+ char buffer[NSID_LENGTH];
+ uuid.ToProvidedString(buffer);
+ mID = NS_ConvertASCIItoUTF16(buffer);
+ }
+ }
+}
+
+DOMMediaStream::~DOMMediaStream() { Destroy(); }
+
+void DOMMediaStream::Destroy() {
+ LOG(LogLevel::Debug, ("DOMMediaStream %p Being destroyed.", this));
+ for (const auto& track : mTracks) {
+ // We must remove ourselves from each track's principal change observer list
+ // before we die.
+ if (!track->Ended()) {
+ track->RemoveConsumer(mPlaybackTrackListener);
+ }
+ }
+ mTrackListeners.Clear();
+}
+
+JSObject* DOMMediaStream::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::MediaStream_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+/* static */
+already_AddRefed<DOMMediaStream> DOMMediaStream::Constructor(
+ const GlobalObject& aGlobal, ErrorResult& aRv) {
+ Sequence<OwningNonNull<MediaStreamTrack>> emptyTrackSeq;
+ return Constructor(aGlobal, emptyTrackSeq, aRv);
+}
+
+/* static */
+already_AddRefed<DOMMediaStream> DOMMediaStream::Constructor(
+ const GlobalObject& aGlobal, const DOMMediaStream& aStream,
+ ErrorResult& aRv) {
+ nsTArray<RefPtr<MediaStreamTrack>> tracks;
+ aStream.GetTracks(tracks);
+
+ Sequence<OwningNonNull<MediaStreamTrack>> nonNullTrackSeq;
+ if (!nonNullTrackSeq.SetLength(tracks.Length(), fallible)) {
+ MOZ_ASSERT(false);
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return nullptr;
+ }
+
+ for (size_t i = 0; i < tracks.Length(); ++i) {
+ nonNullTrackSeq[i] = tracks[i];
+ }
+
+ return Constructor(aGlobal, nonNullTrackSeq, aRv);
+}
+
+/* static */
+already_AddRefed<DOMMediaStream> DOMMediaStream::Constructor(
+ const GlobalObject& aGlobal,
+ const Sequence<OwningNonNull<MediaStreamTrack>>& aTracks,
+ ErrorResult& aRv) {
+ nsCOMPtr<nsPIDOMWindowInner> ownerWindow =
+ do_QueryInterface(aGlobal.GetAsSupports());
+ if (!ownerWindow) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ auto newStream = MakeRefPtr<DOMMediaStream>(ownerWindow);
+ for (MediaStreamTrack& track : aTracks) {
+ newStream->AddTrack(track);
+ }
+ return newStream.forget();
+}
+
+already_AddRefed<Promise> DOMMediaStream::CountUnderlyingStreams(
+ const GlobalObject& aGlobal, ErrorResult& aRv) {
+ nsCOMPtr<nsPIDOMWindowInner> window =
+ do_QueryInterface(aGlobal.GetAsSupports());
+ if (!window) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIGlobalObject> go = do_QueryInterface(aGlobal.GetAsSupports());
+ if (!go) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ RefPtr<Promise> p = Promise::Create(go, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ MediaTrackGraph* graph = MediaTrackGraph::GetInstanceIfExists(
+ window, MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE,
+ MediaTrackGraph::DEFAULT_OUTPUT_DEVICE);
+ if (!graph) {
+ p->MaybeResolve(0);
+ return p.forget();
+ }
+
+ auto* graphImpl = static_cast<MediaTrackGraphImpl*>(graph);
+
+ class Counter : public ControlMessage {
+ public:
+ Counter(MediaTrackGraphImpl* aGraph, const RefPtr<Promise>& aPromise)
+ : ControlMessage(nullptr), mGraph(aGraph), mPromise(aPromise) {
+ MOZ_ASSERT(NS_IsMainThread());
+ }
+
+ void Run() override {
+ TRACE("DOMMediaStream::Counter")
+ uint32_t streams =
+ mGraph->mTracks.Length() + mGraph->mSuspendedTracks.Length();
+ mGraph->DispatchToMainThreadStableState(NS_NewRunnableFunction(
+ "DOMMediaStream::CountUnderlyingStreams (stable state)",
+ [promise = std::move(mPromise), streams]() mutable {
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "DOMMediaStream::CountUnderlyingStreams",
+ [promise = std::move(promise), streams]() {
+ promise->MaybeResolve(streams);
+ }));
+ }));
+ }
+
+ // mPromise can only be AddRefed/Released on main thread.
+ // In case of shutdown, Run() does not run, so we dispatch mPromise to be
+ // released on main thread here.
+ void RunDuringShutdown() override {
+ NS_ReleaseOnMainThread(
+ "DOMMediaStream::CountUnderlyingStreams::Counter::RunDuringShutdown",
+ mPromise.forget());
+ }
+
+ private:
+ // mGraph owns this Counter instance and decides its lifetime.
+ MediaTrackGraphImpl* mGraph;
+ RefPtr<Promise> mPromise;
+ };
+ graphImpl->AppendMessage(MakeUnique<Counter>(graphImpl, p));
+
+ return p.forget();
+}
+
+void DOMMediaStream::GetId(nsAString& aID) const { aID = mID; }
+
+void DOMMediaStream::GetAudioTracks(
+ nsTArray<RefPtr<AudioStreamTrack>>& aTracks) const {
+ for (const auto& track : mTracks) {
+ if (AudioStreamTrack* t = track->AsAudioStreamTrack()) {
+ aTracks.AppendElement(t);
+ }
+ }
+}
+
+void DOMMediaStream::GetAudioTracks(
+ nsTArray<RefPtr<MediaStreamTrack>>& aTracks) const {
+ for (const auto& track : mTracks) {
+ if (track->AsAudioStreamTrack()) {
+ aTracks.AppendElement(track);
+ }
+ }
+}
+
+void DOMMediaStream::GetVideoTracks(
+ nsTArray<RefPtr<VideoStreamTrack>>& aTracks) const {
+ for (const auto& track : mTracks) {
+ if (VideoStreamTrack* t = track->AsVideoStreamTrack()) {
+ aTracks.AppendElement(t);
+ }
+ }
+}
+
+void DOMMediaStream::GetVideoTracks(
+ nsTArray<RefPtr<MediaStreamTrack>>& aTracks) const {
+ for (const auto& track : mTracks) {
+ if (track->AsVideoStreamTrack()) {
+ aTracks.AppendElement(track);
+ }
+ }
+}
+
+void DOMMediaStream::GetTracks(
+ nsTArray<RefPtr<MediaStreamTrack>>& aTracks) const {
+ for (const auto& track : mTracks) {
+ aTracks.AppendElement(track);
+ }
+}
+
+void DOMMediaStream::AddTrack(MediaStreamTrack& aTrack) {
+ LOG(LogLevel::Info, ("DOMMediaStream %p Adding track %p (from track %p)",
+ this, &aTrack, aTrack.GetTrack()));
+
+ if (HasTrack(aTrack)) {
+ LOG(LogLevel::Debug,
+ ("DOMMediaStream %p already contains track %p", this, &aTrack));
+ return;
+ }
+
+ mTracks.AppendElement(&aTrack);
+
+ if (!aTrack.Ended()) {
+ NotifyTrackAdded(&aTrack);
+ }
+}
+
+void DOMMediaStream::RemoveTrack(MediaStreamTrack& aTrack) {
+ LOG(LogLevel::Info, ("DOMMediaStream %p Removing track %p (from track %p)",
+ this, &aTrack, aTrack.GetTrack()));
+
+ if (!mTracks.RemoveElement(&aTrack)) {
+ LOG(LogLevel::Debug,
+ ("DOMMediaStream %p does not contain track %p", this, &aTrack));
+ return;
+ }
+
+ if (!aTrack.Ended()) {
+ NotifyTrackRemoved(&aTrack);
+ }
+}
+
+already_AddRefed<DOMMediaStream> DOMMediaStream::Clone() {
+ auto newStream = MakeRefPtr<DOMMediaStream>(GetOwner());
+
+ LOG(LogLevel::Info,
+ ("DOMMediaStream %p created clone %p", this, newStream.get()));
+
+ for (const auto& track : mTracks) {
+ LOG(LogLevel::Debug,
+ ("DOMMediaStream %p forwarding external track %p to clone %p", this,
+ track.get(), newStream.get()));
+ RefPtr<MediaStreamTrack> clone = track->Clone();
+ newStream->AddTrack(*clone);
+ }
+
+ return newStream.forget();
+}
+
+bool DOMMediaStream::Active() const { return mActive; }
+bool DOMMediaStream::Audible() const { return mAudible; }
+
+MediaStreamTrack* DOMMediaStream::GetTrackById(const nsAString& aId) const {
+ for (const auto& track : mTracks) {
+ nsString id;
+ track->GetId(id);
+ if (id == aId) {
+ return track;
+ }
+ }
+ return nullptr;
+}
+
+bool DOMMediaStream::HasTrack(const MediaStreamTrack& aTrack) const {
+ return mTracks.Contains(&aTrack);
+}
+
+void DOMMediaStream::AddTrackInternal(MediaStreamTrack* aTrack) {
+ LOG(LogLevel::Debug,
+ ("DOMMediaStream %p Adding owned track %p", this, aTrack));
+ AddTrack(*aTrack);
+ DispatchTrackEvent(u"addtrack"_ns, aTrack);
+}
+
+void DOMMediaStream::RemoveTrackInternal(MediaStreamTrack* aTrack) {
+ LOG(LogLevel::Debug,
+ ("DOMMediaStream %p Removing owned track %p", this, aTrack));
+ if (!HasTrack(*aTrack)) {
+ return;
+ }
+ RemoveTrack(*aTrack);
+ DispatchTrackEvent(u"removetrack"_ns, aTrack);
+}
+
+already_AddRefed<nsIPrincipal> DOMMediaStream::GetPrincipal() {
+ if (!GetOwner()) {
+ return nullptr;
+ }
+ nsCOMPtr<nsIPrincipal> principal =
+ nsGlobalWindowInner::Cast(GetOwner())->GetPrincipal();
+ for (const auto& t : mTracks) {
+ if (t->Ended()) {
+ continue;
+ }
+ nsContentUtils::CombineResourcePrincipals(&principal, t->GetPrincipal());
+ }
+ return principal.forget();
+}
+
+void DOMMediaStream::NotifyActive() {
+ LOG(LogLevel::Info, ("DOMMediaStream %p NotifyActive(). ", this));
+
+ MOZ_ASSERT(mActive);
+ for (int32_t i = mTrackListeners.Length() - 1; i >= 0; --i) {
+ mTrackListeners[i]->NotifyActive();
+ }
+}
+
+void DOMMediaStream::NotifyInactive() {
+ LOG(LogLevel::Info, ("DOMMediaStream %p NotifyInactive(). ", this));
+
+ MOZ_ASSERT(!mActive);
+ for (int32_t i = mTrackListeners.Length() - 1; i >= 0; --i) {
+ mTrackListeners[i]->NotifyInactive();
+ }
+}
+
+void DOMMediaStream::NotifyAudible() {
+ LOG(LogLevel::Info, ("DOMMediaStream %p NotifyAudible(). ", this));
+
+ MOZ_ASSERT(mAudible);
+ for (int32_t i = mTrackListeners.Length() - 1; i >= 0; --i) {
+ mTrackListeners[i]->NotifyAudible();
+ }
+}
+
+void DOMMediaStream::NotifyInaudible() {
+ LOG(LogLevel::Info, ("DOMMediaStream %p NotifyInaudible(). ", this));
+
+ MOZ_ASSERT(!mAudible);
+ for (int32_t i = mTrackListeners.Length() - 1; i >= 0; --i) {
+ mTrackListeners[i]->NotifyInaudible();
+ }
+}
+
+void DOMMediaStream::RegisterTrackListener(TrackListener* aListener) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mTrackListeners.AppendElement(aListener);
+}
+
+void DOMMediaStream::UnregisterTrackListener(TrackListener* aListener) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mTrackListeners.RemoveElement(aListener);
+}
+
+void DOMMediaStream::NotifyTrackAdded(const RefPtr<MediaStreamTrack>& aTrack) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ aTrack->AddConsumer(mPlaybackTrackListener);
+
+ for (int32_t i = mTrackListeners.Length() - 1; i >= 0; --i) {
+ mTrackListeners[i]->NotifyTrackAdded(aTrack);
+ }
+
+ if (!mActive) {
+ // Check if we became active.
+ if (ContainsLiveTracks(mTracks)) {
+ mActive = true;
+ NotifyActive();
+ }
+ }
+
+ if (!mAudible) {
+ // Check if we became audible.
+ if (ContainsLiveAudioTracks(mTracks)) {
+ mAudible = true;
+ NotifyAudible();
+ }
+ }
+}
+
+void DOMMediaStream::NotifyTrackRemoved(
+ const RefPtr<MediaStreamTrack>& aTrack) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (aTrack) {
+ // aTrack may be null to allow HTMLMediaElement::MozCaptureStream streams
+ // to be played until the source media element has ended. The source media
+ // element will then call NotifyTrackRemoved(nullptr) to signal that we can
+ // go inactive, regardless of the timing of the last track ending.
+
+ aTrack->RemoveConsumer(mPlaybackTrackListener);
+
+ for (int32_t i = mTrackListeners.Length() - 1; i >= 0; --i) {
+ mTrackListeners[i]->NotifyTrackRemoved(aTrack);
+ }
+
+ if (!mActive) {
+ NS_ASSERTION(false, "Shouldn't remove a live track if already inactive");
+ return;
+ }
+ }
+
+ if (mAudible) {
+ // Check if we became inaudible.
+ if (!ContainsLiveAudioTracks(mTracks)) {
+ mAudible = false;
+ NotifyInaudible();
+ }
+ }
+
+ // Check if we became inactive.
+ if (!ContainsLiveTracks(mTracks)) {
+ mActive = false;
+ NotifyInactive();
+ }
+}
+
+nsresult DOMMediaStream::DispatchTrackEvent(
+ const nsAString& aName, const RefPtr<MediaStreamTrack>& aTrack) {
+ MediaStreamTrackEventInit init;
+ init.mTrack = aTrack;
+
+ RefPtr<MediaStreamTrackEvent> event =
+ MediaStreamTrackEvent::Constructor(this, aName, init);
+
+ return DispatchTrustedEvent(event);
+}
diff --git a/dom/media/DOMMediaStream.h b/dom/media/DOMMediaStream.h
new file mode 100644
index 0000000000..b0a9f895bb
--- /dev/null
+++ b/dom/media/DOMMediaStream.h
@@ -0,0 +1,252 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef NSDOMMEDIASTREAM_H_
+#define NSDOMMEDIASTREAM_H_
+
+#include "ImageContainer.h"
+
+#include "nsCycleCollectionParticipant.h"
+#include "nsWrapperCache.h"
+#include "nsIPrincipal.h"
+#include "MediaTrackConstraints.h"
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/RelativeTimeline.h"
+#include "mozilla/WeakPtr.h"
+
+namespace mozilla {
+
+class AbstractThread;
+class DOMMediaStream;
+
+enum class BlockingMode;
+
+namespace dom {
+class HTMLCanvasElement;
+class MediaStreamTrack;
+class MediaStreamTrackSource;
+class AudioStreamTrack;
+class VideoStreamTrack;
+} // namespace dom
+
+namespace layers {
+class ImageContainer;
+class OverlayImage;
+} // namespace layers
+
+#define NS_DOMMEDIASTREAM_IID \
+ { \
+ 0x8cb65468, 0x66c0, 0x444e, { \
+ 0x89, 0x9f, 0x89, 0x1d, 0x9e, 0xd2, 0xbe, 0x7c \
+ } \
+ }
+
+/**
+ * DOMMediaStream is the implementation of the js-exposed MediaStream interface.
+ *
+ * This is a thin main-thread class grouping MediaStreamTracks together.
+ */
+class DOMMediaStream : public DOMEventTargetHelper,
+ public RelativeTimeline,
+ public SupportsWeakPtr {
+ typedef dom::MediaStreamTrack MediaStreamTrack;
+ typedef dom::AudioStreamTrack AudioStreamTrack;
+ typedef dom::VideoStreamTrack VideoStreamTrack;
+ typedef dom::MediaStreamTrackSource MediaStreamTrackSource;
+
+ public:
+ typedef dom::MediaTrackConstraints MediaTrackConstraints;
+
+ class TrackListener {
+ public:
+ virtual ~TrackListener() = default;
+
+ /**
+ * Called when the DOMMediaStream has a live track added, either by
+ * script (addTrack()) or the source creating one.
+ */
+ virtual void NotifyTrackAdded(const RefPtr<MediaStreamTrack>& aTrack){};
+
+ /**
+ * Called when the DOMMediaStream removes a live track from playback, either
+ * by script (removeTrack(), track.stop()) or the source ending it.
+ */
+ virtual void NotifyTrackRemoved(const RefPtr<MediaStreamTrack>& aTrack){};
+
+ /**
+ * Called when the DOMMediaStream has become active.
+ */
+ virtual void NotifyActive(){};
+
+ /**
+ * Called when the DOMMediaStream has become inactive.
+ */
+ virtual void NotifyInactive(){};
+
+ /**
+ * Called when the DOMMediaStream has become audible.
+ */
+ virtual void NotifyAudible(){};
+
+ /**
+ * Called when the DOMMediaStream has become inaudible.
+ */
+ virtual void NotifyInaudible(){};
+ };
+
+ explicit DOMMediaStream(nsPIDOMWindowInner* aWindow);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(DOMMediaStream, DOMEventTargetHelper)
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_DOMMEDIASTREAM_IID)
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // WebIDL
+
+ static already_AddRefed<DOMMediaStream> Constructor(
+ const dom::GlobalObject& aGlobal, ErrorResult& aRv);
+
+ static already_AddRefed<DOMMediaStream> Constructor(
+ const dom::GlobalObject& aGlobal, const DOMMediaStream& aStream,
+ ErrorResult& aRv);
+
+ static already_AddRefed<DOMMediaStream> Constructor(
+ const dom::GlobalObject& aGlobal,
+ const dom::Sequence<OwningNonNull<MediaStreamTrack>>& aTracks,
+ ErrorResult& aRv);
+
+ static already_AddRefed<dom::Promise> CountUnderlyingStreams(
+ const dom::GlobalObject& aGlobal, ErrorResult& aRv);
+
+ void GetId(nsAString& aID) const;
+
+ void GetAudioTracks(nsTArray<RefPtr<AudioStreamTrack>>& aTracks) const;
+ void GetAudioTracks(nsTArray<RefPtr<MediaStreamTrack>>& aTracks) const;
+ void GetVideoTracks(nsTArray<RefPtr<VideoStreamTrack>>& aTracks) const;
+ void GetVideoTracks(nsTArray<RefPtr<MediaStreamTrack>>& aTracks) const;
+ void GetTracks(nsTArray<RefPtr<MediaStreamTrack>>& aTracks) const;
+ MediaStreamTrack* GetTrackById(const nsAString& aId) const;
+ void AddTrack(MediaStreamTrack& aTrack);
+ void RemoveTrack(MediaStreamTrack& aTrack);
+ already_AddRefed<DOMMediaStream> Clone();
+
+ bool Active() const;
+
+ IMPL_EVENT_HANDLER(addtrack)
+ IMPL_EVENT_HANDLER(removetrack)
+
+ // NON-WebIDL
+
+ // Returns true if this stream contains a live audio track.
+ bool Audible() const;
+
+ /**
+ * Returns true if this DOMMediaStream has aTrack in mTracks.
+ */
+ bool HasTrack(const MediaStreamTrack& aTrack) const;
+
+ /**
+ * Returns a principal indicating who may access this stream. The stream
+ * contents can only be accessed by principals subsuming this principal.
+ */
+ already_AddRefed<nsIPrincipal> GetPrincipal();
+
+ // Webrtc allows the remote side to name a stream whatever it wants, and we
+ // need to surface this to content.
+ void AssignId(const nsAString& aID) { mID = aID; }
+
+ /**
+ * Adds a MediaStreamTrack to mTracks and raises "addtrack".
+ *
+ * Note that "addtrack" is raised synchronously and only has an effect if
+ * this MediaStream is already exposed to script. For spec compliance this is
+ * to be called from an async task.
+ */
+ void AddTrackInternal(MediaStreamTrack* aTrack);
+
+ /**
+ * Removes a MediaStreamTrack from mTracks and fires "removetrack" if it
+ * was removed.
+ *
+ * Note that "removetrack" is raised synchronously and only has an effect if
+ * this MediaStream is already exposed to script. For spec compliance this is
+ * to be called from an async task.
+ */
+ void RemoveTrackInternal(MediaStreamTrack* aTrack);
+
+ /**
+ * Add an nsISupports object that this stream will keep alive as long as
+ * the stream itself is alive.
+ */
+ void AddConsumerToKeepAlive(nsISupports* aConsumer) {
+ mConsumersToKeepAlive.AppendElement(aConsumer);
+ }
+
+ // Registers a track listener to this MediaStream, for listening to changes
+ // to our track set. The caller must call UnregisterTrackListener before
+ // being destroyed, so we don't hold on to a dead pointer. Main thread only.
+ void RegisterTrackListener(TrackListener* aListener);
+
+ // Unregisters a track listener from this MediaStream. The caller must call
+ // UnregisterTrackListener before being destroyed, so we don't hold on to
+ // a dead pointer. Main thread only.
+ void UnregisterTrackListener(TrackListener* aListener);
+
+ protected:
+ virtual ~DOMMediaStream();
+
+ void Destroy();
+
+ // Dispatches NotifyActive() to all registered track listeners.
+ void NotifyActive();
+
+ // Dispatches NotifyInactive() to all registered track listeners.
+ void NotifyInactive();
+
+ // Dispatches NotifyAudible() to all registered track listeners.
+ void NotifyAudible();
+
+ // Dispatches NotifyInaudible() to all registered track listeners.
+ void NotifyInaudible();
+
+ // Dispatches NotifyTrackAdded() to all registered track listeners.
+ void NotifyTrackAdded(const RefPtr<MediaStreamTrack>& aTrack);
+
+ // Dispatches NotifyTrackRemoved() to all registered track listeners.
+ void NotifyTrackRemoved(const RefPtr<MediaStreamTrack>& aTrack);
+
+ // Dispatches "addtrack" or "removetrack".
+ nsresult DispatchTrackEvent(const nsAString& aName,
+ const RefPtr<MediaStreamTrack>& aTrack);
+
+ // MediaStreamTracks contained by this DOMMediaStream.
+ nsTArray<RefPtr<MediaStreamTrack>> mTracks;
+
+ // Listener tracking when live MediaStreamTracks in mTracks end.
+ class PlaybackTrackListener;
+ RefPtr<PlaybackTrackListener> mPlaybackTrackListener;
+
+ nsString mID;
+
+ // Keep these alive while the stream is alive.
+ nsTArray<nsCOMPtr<nsISupports>> mConsumersToKeepAlive;
+
+ // The track listeners subscribe to changes in this stream's track set.
+ nsTArray<TrackListener*> mTrackListeners;
+
+ // True if this stream has live tracks.
+ bool mActive = false;
+
+ // True if this stream has live audio tracks.
+ bool mAudible = false;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(DOMMediaStream, NS_DOMMEDIASTREAM_IID)
+
+} // namespace mozilla
+
+#endif /* NSDOMMEDIASTREAM_H_ */
diff --git a/dom/media/DecoderTraits.cpp b/dom/media/DecoderTraits.cpp
new file mode 100644
index 0000000000..ad64dca729
--- /dev/null
+++ b/dom/media/DecoderTraits.cpp
@@ -0,0 +1,345 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DecoderTraits.h"
+#include "MediaContainerType.h"
+#include "mozilla/Preferences.h"
+
+#include "OggDecoder.h"
+#include "OggDemuxer.h"
+
+#include "WebMDecoder.h"
+#include "WebMDemuxer.h"
+
+#ifdef MOZ_ANDROID_HLS_SUPPORT
+# include "HLSDecoder.h"
+#endif
+#ifdef MOZ_FMP4
+# include "MP4Decoder.h"
+# include "MP4Demuxer.h"
+#endif
+#include "MediaFormatReader.h"
+
+#include "MP3Decoder.h"
+#include "MP3Demuxer.h"
+
+#include "WaveDecoder.h"
+#include "WaveDemuxer.h"
+
+#include "ADTSDecoder.h"
+#include "ADTSDemuxer.h"
+
+#include "FlacDecoder.h"
+#include "FlacDemuxer.h"
+
+#include "nsPluginHost.h"
+
+namespace mozilla {
+
+/* static */
+bool DecoderTraits::IsHttpLiveStreamingType(const MediaContainerType& aType) {
+ const auto& mimeType = aType.Type();
+ return // For m3u8.
+ // https://tools.ietf.org/html/draft-pantos-http-live-streaming-19#section-10
+ mimeType == MEDIAMIMETYPE("application/vnd.apple.mpegurl") ||
+ // Some sites serve these as the informal m3u type.
+ mimeType == MEDIAMIMETYPE("application/x-mpegurl") ||
+ mimeType == MEDIAMIMETYPE("audio/mpegurl") ||
+ mimeType == MEDIAMIMETYPE("audio/x-mpegurl");
+}
+
+/* static */
+bool DecoderTraits::IsMatroskaType(const MediaContainerType& aType) {
+ const auto& mimeType = aType.Type();
+ // https://matroska.org/technical/specs/notes.html
+ return mimeType == MEDIAMIMETYPE("audio/x-matroska") ||
+ mimeType == MEDIAMIMETYPE("video/x-matroska");
+}
+
+/* static */
+bool DecoderTraits::IsMP4SupportedType(const MediaContainerType& aType,
+ DecoderDoctorDiagnostics* aDiagnostics) {
+#ifdef MOZ_FMP4
+ return MP4Decoder::IsSupportedType(aType, aDiagnostics);
+#else
+ return false;
+#endif
+}
+
+static CanPlayStatus CanHandleCodecsType(
+ const MediaContainerType& aType, DecoderDoctorDiagnostics* aDiagnostics) {
+ // We should have been given a codecs string, though it may be empty.
+ MOZ_ASSERT(aType.ExtendedType().HaveCodecs());
+
+ // Container type with the MIME type, no codecs.
+ const MediaContainerType mimeType(aType.Type());
+
+ if (OggDecoder::IsSupportedType(mimeType)) {
+ if (OggDecoder::IsSupportedType(aType)) {
+ return CANPLAY_YES;
+ }
+ // We can only reach this position if a particular codec was requested,
+ // ogg is supported and working: the codec must be invalid.
+ return CANPLAY_NO;
+ }
+ if (WaveDecoder::IsSupportedType(MediaContainerType(mimeType))) {
+ if (WaveDecoder::IsSupportedType(aType)) {
+ return CANPLAY_YES;
+ }
+ // We can only reach this position if a particular codec was requested, wave
+ // is supported and working: the codec must be invalid or not supported.
+ return CANPLAY_NO;
+ }
+ if (WebMDecoder::IsSupportedType(mimeType)) {
+ if (WebMDecoder::IsSupportedType(aType)) {
+ return CANPLAY_YES;
+ }
+ // We can only reach this position if a particular codec was requested,
+ // webm is supported and working: the codec must be invalid.
+ return CANPLAY_NO;
+ }
+#ifdef MOZ_FMP4
+ if (MP4Decoder::IsSupportedType(mimeType,
+ /* DecoderDoctorDiagnostics* */ nullptr)) {
+ if (MP4Decoder::IsSupportedType(aType, aDiagnostics)) {
+ return CANPLAY_YES;
+ }
+ // We can only reach this position if a particular codec was requested,
+ // fmp4 is supported and working: the codec must be invalid.
+ return CANPLAY_NO;
+ }
+#endif
+ if (MP3Decoder::IsSupportedType(mimeType)) {
+ if (MP3Decoder::IsSupportedType(aType)) {
+ return CANPLAY_YES;
+ }
+ // We can only reach this position if a particular codec was requested,
+ // mp3 is supported and working: the codec must be invalid.
+ return CANPLAY_NO;
+ }
+ if (ADTSDecoder::IsSupportedType(mimeType)) {
+ if (ADTSDecoder::IsSupportedType(aType)) {
+ return CANPLAY_YES;
+ }
+ // We can only reach this position if a particular codec was requested,
+ // adts is supported and working: the codec must be invalid.
+ return CANPLAY_NO;
+ }
+ if (FlacDecoder::IsSupportedType(mimeType)) {
+ if (FlacDecoder::IsSupportedType(aType)) {
+ return CANPLAY_YES;
+ }
+ // We can only reach this position if a particular codec was requested,
+ // flac is supported and working: the codec must be invalid.
+ return CANPLAY_NO;
+ }
+
+ return CANPLAY_MAYBE;
+}
+
+static CanPlayStatus CanHandleMediaType(
+ const MediaContainerType& aType, DecoderDoctorDiagnostics* aDiagnostics) {
+ if (DecoderTraits::IsHttpLiveStreamingType(aType)) {
+ Telemetry::Accumulate(Telemetry::MEDIA_HLS_CANPLAY_REQUESTED, true);
+ }
+#ifdef MOZ_ANDROID_HLS_SUPPORT
+ if (HLSDecoder::IsSupportedType(aType)) {
+ Telemetry::Accumulate(Telemetry::MEDIA_HLS_CANPLAY_SUPPORTED, true);
+ return CANPLAY_MAYBE;
+ }
+#endif
+
+ if (DecoderTraits::IsMatroskaType(aType)) {
+ Telemetry::Accumulate(Telemetry::MEDIA_MKV_CANPLAY_REQUESTED, true);
+ }
+
+ if (aType.ExtendedType().HaveCodecs()) {
+ CanPlayStatus result = CanHandleCodecsType(aType, aDiagnostics);
+ if (result == CANPLAY_NO || result == CANPLAY_YES) {
+ return result;
+ }
+ }
+
+ // Container type with just the MIME type/subtype, no codecs.
+ const MediaContainerType mimeType(aType.Type());
+
+ if (OggDecoder::IsSupportedType(mimeType)) {
+ return CANPLAY_MAYBE;
+ }
+ if (WaveDecoder::IsSupportedType(mimeType)) {
+ return CANPLAY_MAYBE;
+ }
+#ifdef MOZ_FMP4
+ if (MP4Decoder::IsSupportedType(mimeType, aDiagnostics)) {
+ return CANPLAY_MAYBE;
+ }
+#endif
+ if (WebMDecoder::IsSupportedType(mimeType)) {
+ return CANPLAY_MAYBE;
+ }
+ if (MP3Decoder::IsSupportedType(mimeType)) {
+ return CANPLAY_MAYBE;
+ }
+ if (ADTSDecoder::IsSupportedType(mimeType)) {
+ return CANPLAY_MAYBE;
+ }
+ if (FlacDecoder::IsSupportedType(mimeType)) {
+ return CANPLAY_MAYBE;
+ }
+ return CANPLAY_NO;
+}
+
+/* static */
+CanPlayStatus DecoderTraits::CanHandleContainerType(
+ const MediaContainerType& aContainerType,
+ DecoderDoctorDiagnostics* aDiagnostics) {
+ return CanHandleMediaType(aContainerType, aDiagnostics);
+}
+
+/* static */
+bool DecoderTraits::ShouldHandleMediaType(
+ const char* aMIMEType, DecoderDoctorDiagnostics* aDiagnostics) {
+ Maybe<MediaContainerType> containerType = MakeMediaContainerType(aMIMEType);
+ if (!containerType) {
+ return false;
+ }
+
+ if (WaveDecoder::IsSupportedType(*containerType)) {
+ // We should not return true for Wave types, since there are some
+ // Wave codecs actually in use in the wild that we don't support, and
+ // we should allow those to be handled by plugins or helper apps.
+ // Furthermore people can play Wave files on most platforms by other
+ // means.
+ return false;
+ }
+
+ // If an external plugin which can handle quicktime video is available
+ // (and not disabled), prefer it over native playback as there several
+ // codecs found in the wild that we do not handle.
+ if (containerType->Type() == MEDIAMIMETYPE("video/quicktime")) {
+ RefPtr<nsPluginHost> pluginHost = nsPluginHost::GetInst();
+ if (pluginHost &&
+ pluginHost->HavePluginForType(containerType->Type().AsString())) {
+ return false;
+ }
+ }
+
+ return CanHandleMediaType(*containerType, aDiagnostics) != CANPLAY_NO;
+}
+
+/* static */
+already_AddRefed<MediaDataDemuxer> DecoderTraits::CreateDemuxer(
+ const MediaContainerType& aType, MediaResource* aResource) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RefPtr<MediaDataDemuxer> demuxer;
+
+#ifdef MOZ_FMP4
+ if (MP4Decoder::IsSupportedType(aType,
+ /* DecoderDoctorDiagnostics* */ nullptr)) {
+ demuxer = new MP4Demuxer(aResource);
+ } else
+#endif
+ if (MP3Decoder::IsSupportedType(aType)) {
+ demuxer = new MP3Demuxer(aResource);
+ } else if (ADTSDecoder::IsSupportedType(aType)) {
+ demuxer = new ADTSDemuxer(aResource);
+ } else if (WaveDecoder::IsSupportedType(aType)) {
+ demuxer = new WAVDemuxer(aResource);
+ } else if (FlacDecoder::IsSupportedType(aType)) {
+ demuxer = new FlacDemuxer(aResource);
+ } else if (OggDecoder::IsSupportedType(aType)) {
+ demuxer = new OggDemuxer(aResource);
+ } else if (WebMDecoder::IsSupportedType(aType)) {
+ demuxer = new WebMDemuxer(aResource);
+ }
+
+ return demuxer.forget();
+}
+
+/* static */
+MediaFormatReader* DecoderTraits::CreateReader(const MediaContainerType& aType,
+ MediaFormatReaderInit& aInit) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<MediaDataDemuxer> demuxer = CreateDemuxer(aType, aInit.mResource);
+ if (!demuxer) {
+ return nullptr;
+ }
+
+ MediaFormatReader* decoderReader = new MediaFormatReader(aInit, demuxer);
+
+ if (OggDecoder::IsSupportedType(aType)) {
+ static_cast<OggDemuxer*>(demuxer.get())
+ ->SetChainingEvents(&decoderReader->TimedMetadataProducer(),
+ &decoderReader->MediaNotSeekableProducer());
+ }
+
+ return decoderReader;
+}
+
+/* static */
+bool DecoderTraits::IsSupportedInVideoDocument(const nsACString& aType) {
+ // Forbid playing media in video documents if the user has opted
+ // not to, using either the legacy WMF specific pref, or the newer
+ // catch-all pref.
+ if (!Preferences::GetBool("media.wmf.play-stand-alone", true) ||
+ !Preferences::GetBool("media.play-stand-alone", true)) {
+ return false;
+ }
+
+ Maybe<MediaContainerType> type = MakeMediaContainerType(aType);
+ if (!type) {
+ return false;
+ }
+
+ return OggDecoder::IsSupportedType(*type) ||
+ WebMDecoder::IsSupportedType(*type) ||
+#ifdef MOZ_FMP4
+ MP4Decoder::IsSupportedType(*type,
+ /* DecoderDoctorDiagnostics* */ nullptr) ||
+#endif
+ MP3Decoder::IsSupportedType(*type) ||
+ ADTSDecoder::IsSupportedType(*type) ||
+ FlacDecoder::IsSupportedType(*type) ||
+#ifdef MOZ_ANDROID_HLS_SUPPORT
+ HLSDecoder::IsSupportedType(*type) ||
+#endif
+ false;
+}
+
+/* static */
+nsTArray<UniquePtr<TrackInfo>> DecoderTraits::GetTracksInfo(
+ const MediaContainerType& aType) {
+ // Container type with just the MIME type/subtype, no codecs.
+ const MediaContainerType mimeType(aType.Type());
+
+ if (OggDecoder::IsSupportedType(mimeType)) {
+ return OggDecoder::GetTracksInfo(aType);
+ }
+ if (WaveDecoder::IsSupportedType(mimeType)) {
+ return WaveDecoder::GetTracksInfo(aType);
+ }
+#ifdef MOZ_FMP4
+ if (MP4Decoder::IsSupportedType(mimeType, nullptr)) {
+ return MP4Decoder::GetTracksInfo(aType);
+ }
+#endif
+ if (WebMDecoder::IsSupportedType(mimeType)) {
+ return WebMDecoder::GetTracksInfo(aType);
+ }
+ if (MP3Decoder::IsSupportedType(mimeType)) {
+ return MP3Decoder::GetTracksInfo(aType);
+ }
+ if (ADTSDecoder::IsSupportedType(mimeType)) {
+ return ADTSDecoder::GetTracksInfo(aType);
+ }
+ if (FlacDecoder::IsSupportedType(mimeType)) {
+ return FlacDecoder::GetTracksInfo(aType);
+ }
+ return nsTArray<UniquePtr<TrackInfo>>();
+}
+
+} // namespace mozilla
diff --git a/dom/media/DecoderTraits.h b/dom/media/DecoderTraits.h
new file mode 100644
index 0000000000..291d3c1192
--- /dev/null
+++ b/dom/media/DecoderTraits.h
@@ -0,0 +1,73 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DecoderTraits_h_
+#define DecoderTraits_h_
+
+#include "mozilla/UniquePtr.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+
+class DecoderDoctorDiagnostics;
+class MediaContainerType;
+class MediaDataDemuxer;
+struct MediaFormatReaderInit;
+class MediaFormatReader;
+class MediaResource;
+class TrackInfo;
+
+enum CanPlayStatus { CANPLAY_NO, CANPLAY_MAYBE, CANPLAY_YES };
+
+class DecoderTraits {
+ public:
+ // Returns the CanPlayStatus indicating if we can handle this container type.
+ static CanPlayStatus CanHandleContainerType(
+ const MediaContainerType& aContainerType,
+ DecoderDoctorDiagnostics* aDiagnostics);
+
+ // Returns true if we should handle this MIME type when it appears
+ // as an <object> or as a toplevel page. If, in practice, our support
+ // for the type is more limited than appears in the wild, we should return
+ // false here even if CanHandleMediaType would return true.
+ static bool ShouldHandleMediaType(const char* aMIMEType,
+ DecoderDoctorDiagnostics* aDiagnostics);
+
+ // Create a demuxer for the given MIME type aType. Returns null if we
+ // were unable to create the demuxer.
+ static already_AddRefed<MediaDataDemuxer> CreateDemuxer(
+ const MediaContainerType& aType, MediaResource* aResource);
+
+ // Create a reader for thew given MIME type aType. Returns null
+ // if we were unable to create the reader.
+ static MediaFormatReader* CreateReader(const MediaContainerType& aType,
+ MediaFormatReaderInit& aInit);
+
+ // Returns true if MIME type aType is supported in video documents,
+ // or false otherwise. Not all platforms support all MIME types, and
+ // vice versa.
+ static bool IsSupportedInVideoDocument(const nsACString& aType);
+
+ // Convenience function that returns false if MOZ_FMP4 is not defined,
+ // otherwise defers to MP4Decoder::IsSupportedType().
+ static bool IsMP4SupportedType(const MediaContainerType& aType,
+ DecoderDoctorDiagnostics* aDiagnostics);
+
+ // Returns true if aType is MIME type of hls.
+ static bool IsHttpLiveStreamingType(const MediaContainerType& aType);
+
+ // Returns true if aType is matroska type.
+ static bool IsMatroskaType(const MediaContainerType& aType);
+
+ // Returns an array of all TrackInfo objects described by this type.
+ static nsTArray<UniquePtr<TrackInfo>> GetTracksInfo(
+ const MediaContainerType& aType);
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/DeviceInputTrack.cpp b/dom/media/DeviceInputTrack.cpp
new file mode 100644
index 0000000000..5e67cd9b6c
--- /dev/null
+++ b/dom/media/DeviceInputTrack.cpp
@@ -0,0 +1,694 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#include "DeviceInputTrack.h"
+
+#include "MediaTrackGraphImpl.h"
+#include "Tracing.h"
+
+namespace mozilla {
+
+#ifdef LOG_INTERNAL
+# undef LOG_INTERNAL
+#endif // LOG_INTERNAL
+#define LOG_INTERNAL(level, msg, ...) \
+ MOZ_LOG(gMediaTrackGraphLog, LogLevel::level, (msg, ##__VA_ARGS__))
+
+#ifdef LOG
+# undef LOG
+#endif // LOG
+#define LOG(msg, ...) LOG_INTERNAL(Debug, msg, ##__VA_ARGS__)
+
+#ifdef LOGE
+# undef LOGE
+#endif // LOGE
+#define LOGE(msg, ...) LOG_INTERNAL(Error, msg, ##__VA_ARGS__)
+
+// This can only be called in graph thread since mGraph->CurrentDriver() is
+// graph thread only
+#ifdef TRACK_GRAPH_LOG_INTERNAL
+# undef TRACK_GRAPH_LOG_INTERNAL
+#endif // TRACK_GRAPH_LOG_INTERNAL
+#define TRACK_GRAPH_LOG_INTERNAL(level, msg, ...) \
+ LOG_INTERNAL(level, "(Graph %p, Driver %p) DeviceInputTrack %p, " msg, \
+ this->mGraph, this->mGraph->CurrentDriver(), this, \
+ ##__VA_ARGS__)
+
+#ifdef TRACK_GRAPH_LOG
+# undef TRACK_GRAPH_LOG
+#endif // TRACK_GRAPH_LOG
+#define TRACK_GRAPH_LOG(msg, ...) \
+ TRACK_GRAPH_LOG_INTERNAL(Debug, msg, ##__VA_ARGS__)
+
+#ifdef TRACK_GRAPH_LOGV
+# undef TRACK_GRAPH_LOGV
+#endif // TRACK_GRAPH_LOGV
+#define TRACK_GRAPH_LOGV(msg, ...) \
+ TRACK_GRAPH_LOG_INTERNAL(Verbose, msg, ##__VA_ARGS__)
+
+#ifdef TRACK_GRAPH_LOGE
+# undef TRACK_GRAPH_LOGE
+#endif // TRACK_GRAPH_LOGE
+#define TRACK_GRAPH_LOGE(msg, ...) \
+ TRACK_GRAPH_LOG_INTERNAL(Error, msg, ##__VA_ARGS__)
+
+#ifdef CONSUMER_GRAPH_LOG_INTERNAL
+# undef CONSUMER_GRAPH_LOG_INTERNAL
+#endif // CONSUMER_GRAPH_LOG_INTERNAL
+#define CONSUMER_GRAPH_LOG_INTERNAL(level, msg, ...) \
+ LOG_INTERNAL( \
+ level, "(Graph %p, Driver %p) DeviceInputConsumerTrack %p, " msg, \
+ this->mGraph, this->mGraph->CurrentDriver(), this, ##__VA_ARGS__)
+
+#ifdef CONSUMER_GRAPH_LOGV
+# undef CONSUMER_GRAPH_LOGV
+#endif // CONSUMER_GRAPH_LOGV
+#define CONSUMER_GRAPH_LOGV(msg, ...) \
+ CONSUMER_GRAPH_LOG_INTERNAL(Verbose, msg, ##__VA_ARGS__)
+
+DeviceInputConsumerTrack::DeviceInputConsumerTrack(TrackRate aSampleRate)
+ : ProcessedMediaTrack(aSampleRate, MediaSegment::AUDIO,
+ new AudioSegment()) {}
+
+void DeviceInputConsumerTrack::ConnectDeviceInput(
+ CubebUtils::AudioDeviceID aId, AudioDataListener* aListener,
+ const PrincipalHandle& aPrincipal) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(GraphImpl());
+ MOZ_ASSERT(aListener);
+ MOZ_ASSERT(!mListener);
+ MOZ_ASSERT(!mDeviceInputTrack);
+ MOZ_ASSERT(mDeviceId.isNothing());
+ MOZ_ASSERT(!mDeviceInputTrack,
+ "Must disconnect a device input before connecting a new one");
+
+ mListener = aListener;
+ mDeviceId.emplace(aId);
+
+ mDeviceInputTrack =
+ DeviceInputTrack::OpenAudio(GraphImpl(), aId, aPrincipal, this);
+ LOG("Open device %p (DeviceInputTrack %p) for consumer %p", aId,
+ mDeviceInputTrack.get(), this);
+ mPort = AllocateInputPort(mDeviceInputTrack.get());
+}
+
+void DeviceInputConsumerTrack::DisconnectDeviceInput() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(GraphImpl());
+
+ if (!mListener) {
+ MOZ_ASSERT(mDeviceId.isNothing());
+ MOZ_ASSERT(!mDeviceInputTrack);
+ return;
+ }
+
+ MOZ_ASSERT(mPort);
+ MOZ_ASSERT(mDeviceInputTrack);
+ MOZ_ASSERT(mDeviceId.isSome());
+
+ LOG("Close device %p (DeviceInputTrack %p) for consumer %p ", *mDeviceId,
+ mDeviceInputTrack.get(), this);
+ mPort->Destroy();
+ DeviceInputTrack::CloseAudio(mDeviceInputTrack.forget(), this);
+ mListener = nullptr;
+ mDeviceId = Nothing();
+}
+
+Maybe<CubebUtils::AudioDeviceID> DeviceInputConsumerTrack::DeviceId() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mDeviceId;
+}
+
+NotNull<AudioDataListener*> DeviceInputConsumerTrack::GetAudioDataListener()
+ const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return WrapNotNull(mListener.get());
+}
+
+bool DeviceInputConsumerTrack::ConnectToNativeDevice() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mDeviceInputTrack && mDeviceInputTrack->AsNativeInputTrack();
+}
+
+bool DeviceInputConsumerTrack::ConnectToNonNativeDevice() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mDeviceInputTrack && mDeviceInputTrack->AsNonNativeInputTrack();
+}
+
+void DeviceInputConsumerTrack::GetInputSourceData(AudioSegment& aOutput,
+ const MediaInputPort* aPort,
+ GraphTime aFrom,
+ GraphTime aTo) const {
+ MOZ_ASSERT(mGraph->OnGraphThread());
+ MOZ_ASSERT(aOutput.IsEmpty());
+
+ MediaTrack* source = aPort->GetSource();
+ GraphTime next;
+ for (GraphTime t = aFrom; t < aTo; t = next) {
+ MediaInputPort::InputInterval interval =
+ MediaInputPort::GetNextInputInterval(aPort, t);
+ interval.mEnd = std::min(interval.mEnd, aTo);
+
+ const bool inputEnded =
+ source->Ended() &&
+ source->GetEnd() <=
+ source->GraphTimeToTrackTimeWithBlocking(interval.mStart);
+
+ TrackTime ticks = interval.mEnd - interval.mStart;
+ next = interval.mEnd;
+
+ if (interval.mStart >= interval.mEnd) {
+ break;
+ }
+
+ if (inputEnded) {
+ aOutput.AppendNullData(ticks);
+ CONSUMER_GRAPH_LOGV(
+ "Getting %" PRId64
+ " ticks of null data from input port source (ended input)",
+ ticks);
+ } else if (interval.mInputIsBlocked) {
+ aOutput.AppendNullData(ticks);
+ CONSUMER_GRAPH_LOGV(
+ "Getting %" PRId64
+ " ticks of null data from input port source (blocked input)",
+ ticks);
+ } else if (source->IsSuspended()) {
+ aOutput.AppendNullData(ticks);
+ CONSUMER_GRAPH_LOGV(
+ "Getting %" PRId64
+ " ticks of null data from input port source (source is suspended)",
+ ticks);
+ } else {
+ TrackTime start =
+ source->GraphTimeToTrackTimeWithBlocking(interval.mStart);
+ TrackTime end = source->GraphTimeToTrackTimeWithBlocking(interval.mEnd);
+ MOZ_ASSERT(source->GetData<AudioSegment>()->GetDuration() >= end);
+ aOutput.AppendSlice(*source->GetData<AudioSegment>(), start, end);
+ CONSUMER_GRAPH_LOGV("Getting %" PRId64
+ " ticks of real data from input port source %p",
+ end - start, source);
+ }
+ }
+}
+
+/* static */
+NotNull<RefPtr<DeviceInputTrack>> DeviceInputTrack::OpenAudio(
+ MediaTrackGraphImpl* aGraph, CubebUtils::AudioDeviceID aDeviceId,
+ const PrincipalHandle& aPrincipalHandle,
+ DeviceInputConsumerTrack* aConsumer) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aConsumer);
+ MOZ_ASSERT(aGraph == aConsumer->GraphImpl());
+
+ RefPtr<DeviceInputTrack> track =
+ aGraph->GetDeviceInputTrackMainThread(aDeviceId);
+ if (track) {
+ MOZ_ASSERT(!track->mConsumerTracks.IsEmpty());
+ track->AddDataListener(aConsumer->GetAudioDataListener());
+ } else {
+ // Create a NativeInputTrack or NonNativeInputTrack, depending on whether
+ // the given graph already has a native device or not.
+ if (aGraph->GetNativeInputTrackMainThread()) {
+ // A native device is already in use. This device will be a non-native
+ // device.
+ track = new NonNativeInputTrack(aGraph->GraphRate(), aDeviceId,
+ aPrincipalHandle);
+ } else {
+ // No native device is in use. This device will be the native device.
+ track = new NativeInputTrack(aGraph->GraphRate(), aDeviceId,
+ aPrincipalHandle);
+ }
+ LOG("Create %sNativeInputTrack %p in MTG %p for device %p",
+ (track->AsNativeInputTrack() ? "" : "Non"), track.get(), aGraph,
+ aDeviceId);
+ aGraph->AddTrack(track);
+ // Add the listener before opening the device so the device passed to
+ // OpenAudioInput always has a non-zero input channel count.
+ track->AddDataListener(aConsumer->GetAudioDataListener());
+ aGraph->OpenAudioInput(track);
+ }
+ MOZ_ASSERT(track->AsNativeInputTrack() || track->AsNonNativeInputTrack());
+ MOZ_ASSERT(track->mDeviceId == aDeviceId);
+
+ MOZ_ASSERT(!track->mConsumerTracks.Contains(aConsumer));
+ track->mConsumerTracks.AppendElement(aConsumer);
+
+ LOG("DeviceInputTrack %p (device %p: %snative) in MTG %p has %zu users now",
+ track.get(), track->mDeviceId,
+ (track->AsNativeInputTrack() ? "" : "non-"), aGraph,
+ track->mConsumerTracks.Length());
+ if (track->mConsumerTracks.Length() > 1) {
+ track->ReevaluateInputDevice();
+ }
+
+ return WrapNotNull(track);
+}
+
+/* static */
+void DeviceInputTrack::CloseAudio(already_AddRefed<DeviceInputTrack> aTrack,
+ DeviceInputConsumerTrack* aConsumer) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<DeviceInputTrack> track = aTrack;
+ MOZ_ASSERT(track);
+
+ track->RemoveDataListener(aConsumer->GetAudioDataListener());
+ DebugOnly<bool> removed = track->mConsumerTracks.RemoveElement(aConsumer);
+ MOZ_ASSERT(removed);
+ LOG("DeviceInputTrack %p (device %p) in MTG %p has %zu users now",
+ track.get(), track->mDeviceId, track->GraphImpl(),
+ track->mConsumerTracks.Length());
+ if (track->mConsumerTracks.IsEmpty()) {
+ track->GraphImpl()->CloseAudioInput(track);
+ track->Destroy();
+ } else {
+ track->ReevaluateInputDevice();
+ }
+}
+
+const nsTArray<RefPtr<DeviceInputConsumerTrack>>&
+DeviceInputTrack::GetConsumerTracks() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mConsumerTracks;
+}
+
+DeviceInputTrack::DeviceInputTrack(TrackRate aSampleRate,
+ CubebUtils::AudioDeviceID aDeviceId,
+ const PrincipalHandle& aPrincipalHandle)
+ : ProcessedMediaTrack(aSampleRate, MediaSegment::AUDIO, new AudioSegment()),
+ mDeviceId(aDeviceId),
+ mPrincipalHandle(aPrincipalHandle) {}
+
+uint32_t DeviceInputTrack::MaxRequestedInputChannels() const {
+ MOZ_ASSERT(mGraph->OnGraphThreadOrNotRunning());
+ uint32_t maxInputChannels = 0;
+ for (const auto& listener : mListeners) {
+ maxInputChannels = std::max(maxInputChannels,
+ listener->RequestedInputChannelCount(mGraph));
+ }
+ return maxInputChannels;
+}
+
+bool DeviceInputTrack::HasVoiceInput() const {
+ MOZ_ASSERT(mGraph->OnGraphThreadOrNotRunning());
+ for (const auto& listener : mListeners) {
+ if (listener->IsVoiceInput(mGraph)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void DeviceInputTrack::DeviceChanged(MediaTrackGraphImpl* aGraph) const {
+ MOZ_ASSERT(aGraph->OnGraphThreadOrNotRunning());
+ MOZ_ASSERT(aGraph == mGraph,
+ "Receive device changed signal from another graph");
+ TRACK_GRAPH_LOG("DeviceChanged");
+ for (const auto& listener : mListeners) {
+ listener->DeviceChanged(aGraph);
+ }
+}
+
+void DeviceInputTrack::ReevaluateInputDevice() {
+ MOZ_ASSERT(NS_IsMainThread());
+ class Message : public ControlMessage {
+ public:
+ explicit Message(MediaTrack* aTrack, CubebUtils::AudioDeviceID aDeviceId)
+ : ControlMessage(aTrack), mDeviceId(aDeviceId) {}
+ void Run() override {
+ TRACE("DeviceInputTrack::ReevaluateInputDevice ControlMessage");
+ mTrack->GraphImpl()->ReevaluateInputDevice(mDeviceId);
+ }
+ CubebUtils::AudioDeviceID mDeviceId;
+ };
+ mGraph->AppendMessage(MakeUnique<Message>(this, mDeviceId));
+}
+
+void DeviceInputTrack::AddDataListener(AudioDataListener* aListener) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ class Message : public ControlMessage {
+ public:
+ Message(DeviceInputTrack* aInputTrack, AudioDataListener* aListener)
+ : ControlMessage(nullptr),
+ mInputTrack(aInputTrack),
+ mListener(aListener) {}
+ void Run() override {
+ TRACE("DeviceInputTrack::AddDataListener ControlMessage");
+ MOZ_ASSERT(!mInputTrack->mListeners.Contains(mListener.get()),
+ "Don't add a listener twice.");
+ mInputTrack->mListeners.AppendElement(mListener.get());
+ }
+ RefPtr<DeviceInputTrack> mInputTrack;
+ RefPtr<AudioDataListener> mListener;
+ };
+
+ mGraph->AppendMessage(MakeUnique<Message>(this, aListener));
+}
+
+void DeviceInputTrack::RemoveDataListener(AudioDataListener* aListener) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ class Message : public ControlMessage {
+ public:
+ Message(DeviceInputTrack* aInputTrack, AudioDataListener* aListener)
+ : ControlMessage(nullptr),
+ mInputTrack(aInputTrack),
+ mListener(aListener) {}
+ void Run() override {
+ TRACE("DeviceInputTrack::RemoveDataListener ControlMessage");
+ DebugOnly<bool> wasPresent =
+ mInputTrack->mListeners.RemoveElement(mListener.get());
+ MOZ_ASSERT(wasPresent, "Remove an unknown listener");
+ mListener->Disconnect(mInputTrack->GraphImpl());
+ }
+ RefPtr<DeviceInputTrack> mInputTrack;
+ RefPtr<AudioDataListener> mListener;
+ };
+
+ mGraph->AppendMessage(MakeUnique<Message>(this, aListener));
+}
+
+NativeInputTrack::NativeInputTrack(TrackRate aSampleRate,
+ CubebUtils::AudioDeviceID aDeviceId,
+ const PrincipalHandle& aPrincipalHandle)
+ : DeviceInputTrack(aSampleRate, aDeviceId, aPrincipalHandle),
+ mIsBufferingAppended(false),
+ mInputChannels(0) {}
+
+void NativeInputTrack::DestroyImpl() {
+ MOZ_ASSERT(mGraph->OnGraphThreadOrNotRunning());
+ mPendingData.Clear();
+ ProcessedMediaTrack::DestroyImpl();
+}
+
+void NativeInputTrack::ProcessInput(GraphTime aFrom, GraphTime aTo,
+ uint32_t aFlags) {
+ MOZ_ASSERT(mGraph->OnGraphThread());
+ TRACE_COMMENT("NativeInputTrack::ProcessInput", "%p", this);
+
+ TRACK_GRAPH_LOGV("(Native) ProcessInput from %" PRId64 " to %" PRId64
+ ", needs %" PRId64 " frames",
+ aFrom, aTo, aTo - aFrom);
+
+ TrackTime from = GraphTimeToTrackTime(aFrom);
+ TrackTime to = GraphTimeToTrackTime(aTo);
+ if (from >= to) {
+ return;
+ }
+
+ MOZ_ASSERT_IF(!mIsBufferingAppended, mPendingData.IsEmpty());
+
+ TrackTime need = to - from;
+ TrackTime dataNeed = std::min(mPendingData.GetDuration(), need);
+ TrackTime silenceNeed = std::max(need - dataNeed, (TrackTime)0);
+
+ MOZ_ASSERT_IF(dataNeed > 0, silenceNeed == 0);
+
+ GetData<AudioSegment>()->AppendSlice(mPendingData, 0, dataNeed);
+ mPendingData.RemoveLeading(dataNeed);
+ GetData<AudioSegment>()->AppendNullData(silenceNeed);
+}
+
+uint32_t NativeInputTrack::NumberOfChannels() const {
+ MOZ_ASSERT(mGraph->OnGraphThreadOrNotRunning());
+ return mInputChannels;
+}
+
+void NativeInputTrack::NotifyInputStopped(MediaTrackGraphImpl* aGraph) {
+ MOZ_ASSERT(aGraph->OnGraphThreadOrNotRunning());
+ MOZ_ASSERT(aGraph == mGraph,
+ "Receive input stopped signal from another graph");
+ TRACK_GRAPH_LOG("(Native) NotifyInputStopped");
+ mInputChannels = 0;
+ mIsBufferingAppended = false;
+ mPendingData.Clear();
+}
+
+void NativeInputTrack::NotifyInputData(MediaTrackGraphImpl* aGraph,
+ const AudioDataValue* aBuffer,
+ size_t aFrames, TrackRate aRate,
+ uint32_t aChannels,
+ uint32_t aAlreadyBuffered) {
+ MOZ_ASSERT(aGraph->OnGraphThread());
+ MOZ_ASSERT(aGraph == mGraph, "Receive input data from another graph");
+ TRACK_GRAPH_LOGV(
+ "NotifyInputData: frames=%zu, rate=%d, channel=%u, alreadyBuffered=%u",
+ aFrames, aRate, aChannels, aAlreadyBuffered);
+
+ if (!mIsBufferingAppended) {
+ // First time we see live frames getting added. Use what's already buffered
+ // in the driver's scratch buffer as a starting point.
+ MOZ_ASSERT(mPendingData.IsEmpty());
+ constexpr TrackTime buffering = WEBAUDIO_BLOCK_SIZE;
+ const TrackTime remaining =
+ buffering - static_cast<TrackTime>(aAlreadyBuffered);
+ mPendingData.AppendNullData(remaining);
+ mIsBufferingAppended = true;
+ TRACK_GRAPH_LOG("Set mIsBufferingAppended by appending %" PRId64 " frames.",
+ remaining);
+ }
+
+ MOZ_ASSERT(aChannels);
+ if (!mInputChannels) {
+ mInputChannels = aChannels;
+ }
+ mPendingData.AppendFromInterleavedBuffer(aBuffer, aFrames, aChannels,
+ mPrincipalHandle);
+}
+
+NonNativeInputTrack::NonNativeInputTrack(
+ TrackRate aSampleRate, CubebUtils::AudioDeviceID aDeviceId,
+ const PrincipalHandle& aPrincipalHandle)
+ : DeviceInputTrack(aSampleRate, aDeviceId, aPrincipalHandle),
+ mAudioSource(nullptr),
+ mSourceIdNumber(0),
+ mGraphDriverThreadId(std::thread::id()) {}
+
+void NonNativeInputTrack::DestroyImpl() {
+ MOZ_ASSERT(mGraph->OnGraphThreadOrNotRunning());
+ if (mAudioSource) {
+ mAudioSource->Stop();
+ mAudioSource = nullptr;
+ }
+ ProcessedMediaTrack::DestroyImpl();
+}
+
+void NonNativeInputTrack::ProcessInput(GraphTime aFrom, GraphTime aTo,
+ uint32_t aFlags) {
+ MOZ_ASSERT(mGraph->OnGraphThread());
+ TRACE_COMMENT("NonNativeInputTrack::ProcessInput", "%p", this);
+
+ TRACK_GRAPH_LOGV("(NonNative) ProcessInput from %" PRId64 " to %" PRId64
+ ", needs %" PRId64 " frames",
+ aFrom, aTo, aTo - aFrom);
+
+ TrackTime from = GraphTimeToTrackTime(aFrom);
+ TrackTime to = GraphTimeToTrackTime(aTo);
+ if (from >= to) {
+ return;
+ }
+
+ TrackTime delta = to - from;
+ if (!mAudioSource) {
+ GetData<AudioSegment>()->AppendNullData(delta);
+ return;
+ }
+
+ // GetAudioSegment only checks the given reader if DEBUG is defined.
+ AudioInputSource::Consumer consumer =
+#ifdef DEBUG
+ // If we are on GraphRunner, we should always be on the same thread.
+ mGraph->mGraphRunner || !CheckGraphDriverChanged()
+ ? AudioInputSource::Consumer::Same
+ : AudioInputSource::Consumer::Changed;
+#else
+ AudioInputSource::Consumer::Same;
+#endif
+
+ AudioSegment data = mAudioSource->GetAudioSegment(delta, consumer);
+ MOZ_ASSERT(data.GetDuration() == delta);
+ GetData<AudioSegment>()->AppendFrom(&data);
+}
+
+uint32_t NonNativeInputTrack::NumberOfChannels() const {
+ MOZ_ASSERT(mGraph->OnGraphThreadOrNotRunning());
+ return mAudioSource ? mAudioSource->mChannelCount : 0;
+}
+
+void NonNativeInputTrack::StartAudio(
+ RefPtr<AudioInputSource>&& aAudioInputSource) {
+ MOZ_ASSERT(mGraph->OnGraphThread());
+ MOZ_ASSERT(aAudioInputSource->mPrincipalHandle == mPrincipalHandle);
+ MOZ_ASSERT(aAudioInputSource->mDeviceId == mDeviceId);
+
+ TRACK_GRAPH_LOG("StartAudio with source %p", aAudioInputSource.get());
+ mAudioSource = std::move(aAudioInputSource);
+ mAudioSource->Start();
+}
+
+void NonNativeInputTrack::StopAudio() {
+ MOZ_ASSERT(mGraph->OnGraphThread());
+
+ TRACK_GRAPH_LOG("StopAudio from source %p", mAudioSource.get());
+ if (!mAudioSource) {
+ return;
+ }
+ mAudioSource->Stop();
+ mAudioSource = nullptr;
+}
+
+AudioInputType NonNativeInputTrack::DevicePreference() const {
+ MOZ_ASSERT(mGraph->OnGraphThreadOrNotRunning());
+ return mAudioSource && mAudioSource->mIsVoice ? AudioInputType::Voice
+ : AudioInputType::Unknown;
+}
+
+void NonNativeInputTrack::NotifyDeviceChanged(uint32_t aSourceId) {
+ MOZ_ASSERT(mGraph->OnGraphThreadOrNotRunning());
+
+ // No need to forward the notification if the audio input has been stopped or
+ // restarted by it users.
+ if (!mAudioSource || mAudioSource->mId != aSourceId) {
+ TRACK_GRAPH_LOG("(NonNative) NotifyDeviceChanged: No need to forward");
+ return;
+ }
+
+ TRACK_GRAPH_LOG("(NonNative) NotifyDeviceChanged");
+ // Forward the notification.
+ DeviceInputTrack::DeviceChanged(mGraph);
+}
+
+void NonNativeInputTrack::NotifyInputStopped(uint32_t aSourceId) {
+ MOZ_ASSERT(mGraph->OnGraphThreadOrNotRunning());
+
+ // No need to forward the notification if the audio input has been stopped or
+ // restarted by it users.
+ if (!mAudioSource || mAudioSource->mId != aSourceId) {
+ TRACK_GRAPH_LOG("(NonNative) NotifyInputStopped: No need to forward");
+ return;
+ }
+
+ TRACK_GRAPH_LOGE(
+ "(NonNative) NotifyInputStopped: audio unexpectedly stopped");
+ // Destory the underlying audio stream if it's stopped unexpectedly.
+ mAudioSource->Stop();
+}
+
+AudioInputSource::Id NonNativeInputTrack::GenerateSourceId() {
+ MOZ_ASSERT(mGraph->OnGraphThread());
+ return mSourceIdNumber++;
+}
+
+bool NonNativeInputTrack::CheckGraphDriverChanged() {
+ MOZ_ASSERT(mGraph->CurrentDriver()->OnThread());
+
+ std::thread::id currentId = std::this_thread::get_id();
+ if (mGraphDriverThreadId == currentId) {
+ return false;
+ }
+ mGraphDriverThreadId = currentId;
+ return true;
+}
+
+AudioInputSourceListener::AudioInputSourceListener(NonNativeInputTrack* aOwner)
+ : mOwner(aOwner) {}
+
+void AudioInputSourceListener::AudioDeviceChanged(
+ AudioInputSource::Id aSourceId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mOwner);
+
+ if (mOwner->IsDestroyed()) {
+ LOG("NonNativeInputTrack %p has been destroyed. No need to forward the "
+ "audio device-changed notification",
+ mOwner.get());
+ return;
+ }
+
+ class DeviceChangedMessage : public ControlMessage {
+ public:
+ DeviceChangedMessage(NonNativeInputTrack* aInputTrack,
+ AudioInputSource::Id aSourceId)
+ : ControlMessage(nullptr),
+ mInputTrack(aInputTrack),
+ mSourceId(aSourceId) {
+ MOZ_ASSERT(mInputTrack);
+ }
+ void Run() override {
+ TRACE("NonNativeInputTrack::AudioDeviceChanged ControlMessage");
+ mInputTrack->NotifyDeviceChanged(mSourceId);
+ }
+ RefPtr<NonNativeInputTrack> mInputTrack;
+ AudioInputSource::Id mSourceId;
+ };
+
+ MOZ_DIAGNOSTIC_ASSERT(mOwner->GraphImpl());
+ mOwner->GraphImpl()->AppendMessage(
+ MakeUnique<DeviceChangedMessage>(mOwner.get(), aSourceId));
+}
+
+void AudioInputSourceListener::AudioStateCallback(
+ AudioInputSource::Id aSourceId,
+ AudioInputSource::EventListener::State aState) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mOwner);
+
+ const char* state =
+ aState == AudioInputSource::EventListener::State::Started ? "started"
+ : aState == AudioInputSource::EventListener::State::Stopped ? "stopped"
+ : aState == AudioInputSource::EventListener::State::Drained ? "drained"
+ : "error";
+
+ if (mOwner->IsDestroyed()) {
+ LOG("NonNativeInputTrack %p has been destroyed. No need to forward the "
+ "audio state-changed(%s) notification",
+ mOwner.get(), state);
+ return;
+ }
+
+ if (aState == AudioInputSource::EventListener::State::Started) {
+ LOG("We can ignore %s notification for NonNativeInputTrack %p", state,
+ mOwner.get());
+ return;
+ }
+
+ LOG("Notify audio stopped due to entering %s state", state);
+
+ class InputStoppedMessage : public ControlMessage {
+ public:
+ InputStoppedMessage(NonNativeInputTrack* aInputTrack,
+ AudioInputSource::Id aSourceId)
+ : ControlMessage(nullptr),
+ mInputTrack(aInputTrack),
+ mSourceId(aSourceId) {
+ MOZ_ASSERT(mInputTrack);
+ }
+ void Run() override {
+ TRACE("NonNativeInputTrack::AudioStateCallback ControlMessage");
+ mInputTrack->NotifyInputStopped(mSourceId);
+ }
+ RefPtr<NonNativeInputTrack> mInputTrack;
+ AudioInputSource::Id mSourceId;
+ };
+
+ MOZ_DIAGNOSTIC_ASSERT(mOwner->GraphImpl());
+ mOwner->GraphImpl()->AppendMessage(
+ MakeUnique<InputStoppedMessage>(mOwner.get(), aSourceId));
+}
+
+#undef LOG_INTERNAL
+#undef LOG
+#undef LOGE
+#undef TRACK_GRAPH_LOG_INTERNAL
+#undef TRACK_GRAPH_LOG
+#undef TRACK_GRAPH_LOGV
+#undef TRACK_GRAPH_LOGE
+#undef CONSUMER_GRAPH_LOG_INTERNAL
+#undef CONSUMER_GRAPH_LOGV
+
+} // namespace mozilla
diff --git a/dom/media/DeviceInputTrack.h b/dom/media/DeviceInputTrack.h
new file mode 100644
index 0000000000..4b1caeab2f
--- /dev/null
+++ b/dom/media/DeviceInputTrack.h
@@ -0,0 +1,302 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_DEVICEINPUTTRACK_H_
+#define DOM_MEDIA_DEVICEINPUTTRACK_H_
+
+#include <thread>
+
+#include "AudioDriftCorrection.h"
+#include "AudioSegment.h"
+#include "AudioInputSource.h"
+#include "MediaTrackGraph.h"
+#include "GraphDriver.h"
+#include "mozilla/NotNull.h"
+
+namespace mozilla {
+
+class NativeInputTrack;
+class NonNativeInputTrack;
+
+// Any MediaTrack that needs the audio data from the certain device should
+// inherit the this class and get the raw audio data on graph thread via
+// GetInputSourceData(), after calling ConnectDeviceInput() and before
+// DisconnectDeviceInput() on main thread. See more examples in
+// TestAudioTrackGraph.cpp
+//
+// Example:
+//
+// class RawAudioDataTrack : public DeviceInputConsumerTrack {
+// public:
+// ...
+//
+// void ProcessInput(GraphTime aFrom, GraphTime aTo, uint32_t aFlags) override
+// {
+// if (aFrom >= aTo) {
+// return;
+// }
+//
+// if (mInputs.IsEmpty()) {
+// GetData<AudioSegment>()->AppendNullData(aTo - aFrom);
+// } else {
+// MOZ_ASSERT(mInputs.Length() == 1);
+// AudioSegment data;
+// DeviceInputConsumerTrack::GetInputSourceData(data, mInputs[0], aFrom,
+// aTo);
+// // You can do audio data processing before appending to mSegment here.
+// GetData<AudioSegment>()->AppendFrom(&data);
+// }
+// };
+//
+// uint32_t NumberOfChannels() const override {
+// if (mInputs.IsEmpty()) {
+// return 0;
+// }
+// DeviceInputTrack* t = mInputs[0]->GetSource()->AsDeviceInputTrack();
+// MOZ_ASSERT(t);
+// return t->NumberOfChannels();
+// }
+//
+// ...
+//
+// private:
+// explicit RawAudioDataTrack(TrackRate aSampleRate)
+// : DeviceInputConsumerTrack(aSampleRate) {}
+// };
+class DeviceInputConsumerTrack : public ProcessedMediaTrack {
+ public:
+ explicit DeviceInputConsumerTrack(TrackRate aSampleRate);
+
+ // Main Thread APIs:
+ void ConnectDeviceInput(CubebUtils::AudioDeviceID aId,
+ AudioDataListener* aListener,
+ const PrincipalHandle& aPrincipal);
+ void DisconnectDeviceInput();
+ Maybe<CubebUtils::AudioDeviceID> DeviceId() const;
+ NotNull<AudioDataListener*> GetAudioDataListener() const;
+ bool ConnectToNativeDevice() const;
+ bool ConnectToNonNativeDevice() const;
+
+ // Any thread:
+ DeviceInputConsumerTrack* AsDeviceInputConsumerTrack() override {
+ return this;
+ }
+
+ protected:
+ // Graph thread API:
+ // Get the data in [aFrom, aTo) from aPort->GetSource() to aOutput. aOutput
+ // needs to be empty.
+ void GetInputSourceData(AudioSegment& aOutput, const MediaInputPort* aPort,
+ GraphTime aFrom, GraphTime aTo) const;
+
+ // Main Thread variables:
+ RefPtr<MediaInputPort> mPort;
+ RefPtr<DeviceInputTrack> mDeviceInputTrack;
+ RefPtr<AudioDataListener> mListener;
+ Maybe<CubebUtils::AudioDeviceID> mDeviceId;
+};
+
+class DeviceInputTrack : public ProcessedMediaTrack {
+ public:
+ // Main Thread APIs:
+ // Any MediaTrack that needs the audio data from the certain device should
+ // inherit the DeviceInputConsumerTrack class and call GetInputSourceData to
+ // get the data instead of using the below APIs.
+ //
+ // The following two APIs can create and destroy a DeviceInputTrack reference
+ // on main thread, then open and close the underlying audio device accordingly
+ // on the graph thread. The user who wants to read the audio input from a
+ // certain device should use these APIs to obtain a DeviceInputTrack reference
+ // and release the reference when the user no longer needs the audio data.
+ //
+ // Once the DeviceInputTrack is created on the main thread, the paired device
+ // will start producing data, so its users can read the data immediately on
+ // the graph thread, once they obtain the reference. The lifetime of
+ // DeviceInputTrack is managed by the MediaTrackGraph itself. When the
+ // DeviceInputTrack has no user any more, MediaTrackGraph will destroy it.
+ // This means, it occurs when the last reference has been released by the API
+ // below.
+ //
+ // The DeviceInputTrack is either a NativeInputTrack, or a
+ // NonNativeInputTrack. We can have only one NativeInputTrack per
+ // MediaTrackGraph, but multiple NonNativeInputTrack per graph. The audio
+ // device paired with the NativeInputTrack is called "native device", and the
+ // device paired with the NonNativeInputTrack is called "non-native device".
+ // In other words, we can have only one native device per MediaTrackGraph, but
+ // many non-native devices per graph.
+ //
+ // The native device is the first input device created in the MediaTrackGraph.
+ // All other devices created after it will be non-native devices. Once the
+ // native device is destroyed, the first non-native device will be promoted to
+ // the new native device. The switch will be started by the MediaTrackGraph.
+ // The MediaTrackGraph will force DeviceInputTrack's users to re-configure
+ // their DeviceInputTrack connections with the APIs below to execute the
+ // switching.
+ //
+ // The native device is also the audio input device serving the
+ // AudioCallbackDriver, which drives the MediaTrackGraph periodically from
+ // audio callback thread. The audio data produced by the native device and
+ // non-native device is stored in NativeInputTrack and NonNativeInputTrack
+ // respectively, and then accessed by their users. The only difference between
+ // these audio data is that the data from the non-native device is
+ // clock-drift-corrected since the non-native device may run on a different
+ // clock than the native device's one.
+ //
+ // Example:
+ // // On main thread
+ // RefPtr<DeviceInputTrack> track = DeviceInputTrack::OpenAudio(...);
+ // ...
+ // // On graph thread
+ // AudioSegmen* data = track->GetData<AudioSegment>();
+ // ...
+ // // On main thread
+ // DeviceInputTrack::CloseAudio(track.forget(), ...);
+ //
+ // Returns a reference of DeviceInputTrack, storing the input audio data from
+ // the given device, in the given MediaTrackGraph. The paired audio device
+ // will be opened accordingly. The DeviceInputTrack will access its user's
+ // audio settings via the attached AudioDataListener, and delivers the
+ // notifications when it needs.
+ static NotNull<RefPtr<DeviceInputTrack>> OpenAudio(
+ MediaTrackGraphImpl* aGraph, CubebUtils::AudioDeviceID aDeviceId,
+ const PrincipalHandle& aPrincipalHandle,
+ DeviceInputConsumerTrack* aConsumer);
+ // Destroy the DeviceInputTrack reference obtained by the above API. The
+ // paired audio device will be closed accordingly.
+ static void CloseAudio(already_AddRefed<DeviceInputTrack> aTrack,
+ DeviceInputConsumerTrack* aConsumer);
+
+ // Main thread API:
+ const nsTArray<RefPtr<DeviceInputConsumerTrack>>& GetConsumerTracks() const;
+
+ // Graph thread APIs:
+ // Query audio settings from its users.
+ uint32_t MaxRequestedInputChannels() const;
+ bool HasVoiceInput() const;
+ // Deliver notification to its users.
+ void DeviceChanged(MediaTrackGraphImpl* aGraph) const;
+
+ // Any thread:
+ DeviceInputTrack* AsDeviceInputTrack() override { return this; }
+ virtual NativeInputTrack* AsNativeInputTrack() { return nullptr; }
+ virtual NonNativeInputTrack* AsNonNativeInputTrack() { return nullptr; }
+
+ // Any thread:
+ const CubebUtils::AudioDeviceID mDeviceId;
+ const PrincipalHandle mPrincipalHandle;
+
+ protected:
+ DeviceInputTrack(TrackRate aSampleRate, CubebUtils::AudioDeviceID aDeviceId,
+ const PrincipalHandle& aPrincipalHandle);
+ ~DeviceInputTrack() = default;
+
+ private:
+ // Main thread APIs:
+ void ReevaluateInputDevice();
+ void AddDataListener(AudioDataListener* aListener);
+ void RemoveDataListener(AudioDataListener* aListener);
+
+ // Only accessed on the main thread.
+ // When this becomes empty, this DeviceInputTrack is no longer needed.
+ nsTArray<RefPtr<DeviceInputConsumerTrack>> mConsumerTracks;
+
+ // Only accessed on the graph thread.
+ nsTArray<RefPtr<AudioDataListener>> mListeners;
+};
+
+class NativeInputTrack final : public DeviceInputTrack {
+ public:
+ // Do not call this directly. This can only be called in DeviceInputTrack or
+ // tests.
+ NativeInputTrack(TrackRate aSampleRate, CubebUtils::AudioDeviceID aDeviceId,
+ const PrincipalHandle& aPrincipalHandle);
+
+ // Graph Thread APIs, for ProcessedMediaTrack.
+ void DestroyImpl() override;
+ void ProcessInput(GraphTime aFrom, GraphTime aTo, uint32_t aFlags) override;
+ uint32_t NumberOfChannels() const override;
+
+ // Graph thread APIs: Get input audio data and event from graph.
+ void NotifyInputStopped(MediaTrackGraphImpl* aGraph);
+ void NotifyInputData(MediaTrackGraphImpl* aGraph,
+ const AudioDataValue* aBuffer, size_t aFrames,
+ TrackRate aRate, uint32_t aChannels,
+ uint32_t aAlreadyBuffered);
+
+ // Any thread
+ NativeInputTrack* AsNativeInputTrack() override { return this; }
+
+ private:
+ ~NativeInputTrack() = default;
+
+ // Graph thread only members:
+ // Indicate whether we append extra frames in mPendingData. The extra number
+ // of frames is in [0, WEBAUDIO_BLOCK_SIZE] range.
+ bool mIsBufferingAppended = false;
+ // Queue the audio input data coming from NotifyInputData.
+ AudioSegment mPendingData;
+ // The input channel count for the audio data.
+ uint32_t mInputChannels = 0;
+};
+
+class NonNativeInputTrack final : public DeviceInputTrack {
+ public:
+ // Do not call this directly. This can only be called in DeviceInputTrack or
+ // tests.
+ NonNativeInputTrack(TrackRate aSampleRate,
+ CubebUtils::AudioDeviceID aDeviceId,
+ const PrincipalHandle& aPrincipalHandle);
+
+ // Graph Thread APIs, for ProcessedMediaTrack
+ void DestroyImpl() override;
+ void ProcessInput(GraphTime aFrom, GraphTime aTo, uint32_t aFlags) override;
+ uint32_t NumberOfChannels() const override;
+
+ // Any thread
+ NonNativeInputTrack* AsNonNativeInputTrack() override { return this; }
+
+ // Graph thread APIs:
+ void StartAudio(RefPtr<AudioInputSource>&& aAudioInputSource);
+ void StopAudio();
+ AudioInputType DevicePreference() const;
+ void NotifyDeviceChanged(AudioInputSource::Id aSourceId);
+ void NotifyInputStopped(AudioInputSource::Id aSourceId);
+ AudioInputSource::Id GenerateSourceId();
+
+ private:
+ ~NonNativeInputTrack() = default;
+
+ // Graph driver thread only.
+ bool CheckGraphDriverChanged();
+
+ // Graph thread only.
+ RefPtr<AudioInputSource> mAudioSource;
+ AudioInputSource::Id mSourceIdNumber;
+
+ // Graph driver thread only.
+ std::thread::id mGraphDriverThreadId;
+};
+
+class AudioInputSourceListener : public AudioInputSource::EventListener {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AudioInputSourceListener, override);
+
+ explicit AudioInputSourceListener(NonNativeInputTrack* aOwner);
+
+ // Main thread APIs:
+ void AudioDeviceChanged(AudioInputSource::Id aSourceId) override;
+ void AudioStateCallback(
+ AudioInputSource::Id aSourceId,
+ AudioInputSource::EventListener::State aState) override;
+
+ private:
+ ~AudioInputSourceListener() = default;
+ const RefPtr<NonNativeInputTrack> mOwner;
+};
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_DEVICEINPUTTRACK_H_
diff --git a/dom/media/DriftCompensation.h b/dom/media/DriftCompensation.h
new file mode 100644
index 0000000000..ef22f7106f
--- /dev/null
+++ b/dom/media/DriftCompensation.h
@@ -0,0 +1,137 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#ifndef DriftCompensation_h_
+#define DriftCompensation_h_
+
+#include "MediaSegment.h"
+#include "VideoUtils.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/Unused.h"
+
+namespace mozilla {
+
+static LazyLogModule gDriftCompensatorLog("DriftCompensator");
+#define LOG(type, ...) MOZ_LOG(gDriftCompensatorLog, type, (__VA_ARGS__))
+
+/**
+ * DriftCompensator can be used to handle drift between audio and video tracks
+ * from the MediaTrackGraph.
+ *
+ * Drift can occur because audio is driven by a MediaTrackGraph running off an
+ * audio callback, thus it's progressed by the clock of one the audio output
+ * devices on the user's machine. Video on the other hand is always expressed in
+ * wall-clock TimeStamps, i.e., it's progressed by the system clock. These
+ * clocks will, over time, drift apart.
+ *
+ * Do not use the DriftCompensator across multiple audio tracks, as it will
+ * automatically record the start time of the first audio samples, and all
+ * samples for the same audio track on the same audio clock will have to be
+ * processed to retain accuracy.
+ *
+ * DriftCompensator is designed to be used from two threads:
+ * - The audio thread for notifications of audio samples.
+ * - The video thread for compensating drift of video frames to match the audio
+ * clock.
+ */
+class DriftCompensator {
+ const RefPtr<nsIEventTarget> mVideoThread;
+ const TrackRate mAudioRate;
+
+ // Number of audio samples produced. Any thread.
+ Atomic<TrackTime> mAudioSamples{0};
+
+ // Time the first audio samples were added. mVideoThread only.
+ TimeStamp mAudioStartTime;
+
+ void SetAudioStartTime(TimeStamp aTime) {
+ MOZ_ASSERT(mVideoThread->IsOnCurrentThread());
+ MOZ_ASSERT(mAudioStartTime.IsNull());
+ mAudioStartTime = aTime;
+ }
+
+ protected:
+ virtual ~DriftCompensator() = default;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DriftCompensator)
+
+ DriftCompensator(RefPtr<nsIEventTarget> aVideoThread, TrackRate aAudioRate)
+ : mVideoThread(std::move(aVideoThread)), mAudioRate(aAudioRate) {
+ MOZ_ASSERT(mAudioRate > 0);
+ }
+
+ void NotifyAudioStart(TimeStamp aStart) {
+ MOZ_ASSERT(mAudioSamples == 0);
+ LOG(LogLevel::Info, "DriftCompensator %p at rate %d started", this,
+ mAudioRate);
+ nsresult rv = mVideoThread->Dispatch(NewRunnableMethod<TimeStamp>(
+ "DriftCompensator::SetAudioStartTime", this,
+ &DriftCompensator::SetAudioStartTime, aStart));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ }
+
+ /**
+ * aSamples is the number of samples fed by an AudioStream.
+ */
+ void NotifyAudio(TrackTime aSamples) {
+ MOZ_ASSERT(aSamples > 0);
+ mAudioSamples += aSamples;
+
+ LOG(LogLevel::Verbose,
+ "DriftCompensator %p Processed another %" PRId64
+ " samples; now %.3fs audio",
+ this, aSamples, static_cast<double>(mAudioSamples) / mAudioRate);
+ }
+
+ /**
+ * Drift compensates a video TimeStamp based on historical audio data.
+ */
+ virtual TimeStamp GetVideoTime(TimeStamp aNow, TimeStamp aTime) {
+ MOZ_ASSERT(mVideoThread->IsOnCurrentThread());
+ TrackTime samples = mAudioSamples;
+
+ if (samples / mAudioRate < 10) {
+ // We don't apply compensation for the first 10 seconds because of the
+ // higher inaccuracy during this time.
+ LOG(LogLevel::Debug, "DriftCompensator %p %" PRId64 "ms so far; ignoring",
+ this, samples * 1000 / mAudioRate);
+ return aTime;
+ }
+
+ if (aNow == mAudioStartTime) {
+ LOG(LogLevel::Warning,
+ "DriftCompensator %p video scale 0, assuming no drift", this);
+ return aTime;
+ }
+
+ double videoScaleUs = (aNow - mAudioStartTime).ToMicroseconds();
+ double audioScaleUs = FramesToUsecs(samples, mAudioRate).value();
+ double videoDurationUs = (aTime - mAudioStartTime).ToMicroseconds();
+
+ TimeStamp reclocked =
+ mAudioStartTime + TimeDuration::FromMicroseconds(
+ videoDurationUs * audioScaleUs / videoScaleUs);
+
+ LOG(LogLevel::Debug,
+ "DriftCompensator %p GetVideoTime, v-now: %.3fs, a-now: %.3fs; %.3fs "
+ "-> %.3fs (d %.3fms)",
+ this, (aNow - mAudioStartTime).ToSeconds(),
+ TimeDuration::FromMicroseconds(audioScaleUs).ToSeconds(),
+ (aTime - mAudioStartTime).ToSeconds(),
+ (reclocked - mAudioStartTime).ToSeconds(),
+ (reclocked - aTime).ToMilliseconds());
+
+ return reclocked;
+ }
+};
+
+#undef LOG
+
+} // namespace mozilla
+
+#endif /* DriftCompensation_h_ */
diff --git a/dom/media/DynamicResampler.cpp b/dom/media/DynamicResampler.cpp
new file mode 100644
index 0000000000..470bbfd418
--- /dev/null
+++ b/dom/media/DynamicResampler.cpp
@@ -0,0 +1,477 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DynamicResampler.h"
+
+namespace mozilla {
+
+DynamicResampler::DynamicResampler(uint32_t aInRate, uint32_t aOutRate,
+ uint32_t aPreBufferFrames)
+ : mInRate(aInRate), mPreBufferFrames(aPreBufferFrames), mOutRate(aOutRate) {
+ MOZ_ASSERT(aInRate);
+ MOZ_ASSERT(aOutRate);
+ UpdateResampler(mOutRate, STEREO);
+}
+
+DynamicResampler::~DynamicResampler() {
+ if (mResampler) {
+ speex_resampler_destroy(mResampler);
+ }
+}
+
+void DynamicResampler::SetSampleFormat(AudioSampleFormat aFormat) {
+ MOZ_ASSERT(mSampleFormat == AUDIO_FORMAT_SILENCE);
+ MOZ_ASSERT(aFormat == AUDIO_FORMAT_S16 || aFormat == AUDIO_FORMAT_FLOAT32);
+
+ mSampleFormat = aFormat;
+ for (AudioRingBuffer& b : mInternalInBuffer) {
+ b.SetSampleFormat(mSampleFormat);
+ }
+ if (mPreBufferFrames) {
+ AppendInputSilence(mPreBufferFrames);
+ }
+}
+
+bool DynamicResampler::Resample(float* aOutBuffer, uint32_t* aOutFrames,
+ uint32_t aChannelIndex) {
+ MOZ_ASSERT(mSampleFormat == AUDIO_FORMAT_FLOAT32);
+ return ResampleInternal(aOutBuffer, aOutFrames, aChannelIndex);
+}
+
+bool DynamicResampler::Resample(int16_t* aOutBuffer, uint32_t* aOutFrames,
+ uint32_t aChannelIndex) {
+ MOZ_ASSERT(mSampleFormat == AUDIO_FORMAT_S16);
+ return ResampleInternal(aOutBuffer, aOutFrames, aChannelIndex);
+}
+
+void DynamicResampler::ResampleInternal(const float* aInBuffer,
+ uint32_t* aInFrames, float* aOutBuffer,
+ uint32_t* aOutFrames,
+ uint32_t aChannelIndex) {
+ MOZ_ASSERT(mResampler);
+ MOZ_ASSERT(mChannels);
+ MOZ_ASSERT(mInRate);
+ MOZ_ASSERT(mOutRate);
+
+ MOZ_ASSERT(aInBuffer);
+ MOZ_ASSERT(aInFrames);
+ MOZ_ASSERT(*aInFrames > 0);
+ MOZ_ASSERT(aOutBuffer);
+ MOZ_ASSERT(aOutFrames);
+ MOZ_ASSERT(*aOutFrames > 0);
+
+ MOZ_ASSERT(aChannelIndex <= mChannels);
+
+#ifdef DEBUG
+ int rv =
+#endif
+ speex_resampler_process_float(mResampler, aChannelIndex, aInBuffer,
+ aInFrames, aOutBuffer, aOutFrames);
+ MOZ_ASSERT(rv == RESAMPLER_ERR_SUCCESS);
+}
+
+void DynamicResampler::ResampleInternal(const int16_t* aInBuffer,
+ uint32_t* aInFrames,
+ int16_t* aOutBuffer,
+ uint32_t* aOutFrames,
+ uint32_t aChannelIndex) {
+ MOZ_ASSERT(mResampler);
+ MOZ_ASSERT(mChannels);
+ MOZ_ASSERT(mInRate);
+ MOZ_ASSERT(mOutRate);
+
+ MOZ_ASSERT(aInBuffer);
+ MOZ_ASSERT(aInFrames);
+ MOZ_ASSERT(*aInFrames > 0);
+ MOZ_ASSERT(aOutBuffer);
+ MOZ_ASSERT(aOutFrames);
+ MOZ_ASSERT(*aOutFrames > 0);
+
+ MOZ_ASSERT(aChannelIndex <= mChannels);
+
+#ifdef DEBUG
+ int rv =
+#endif
+ speex_resampler_process_int(mResampler, aChannelIndex, aInBuffer,
+ aInFrames, aOutBuffer, aOutFrames);
+ MOZ_ASSERT(rv == RESAMPLER_ERR_SUCCESS);
+}
+
+void DynamicResampler::UpdateResampler(uint32_t aOutRate, uint32_t aChannels) {
+ MOZ_ASSERT(aOutRate);
+ MOZ_ASSERT(aChannels);
+
+ if (mChannels != aChannels) {
+ if (mResampler) {
+ speex_resampler_destroy(mResampler);
+ }
+ mResampler = speex_resampler_init(aChannels, mInRate, aOutRate,
+ SPEEX_RESAMPLER_QUALITY_MIN, nullptr);
+ MOZ_ASSERT(mResampler);
+ mChannels = aChannels;
+ mOutRate = aOutRate;
+ // Between mono and stereo changes, keep always allocated 2 channels to
+ // avoid reallocations in the most common case.
+ if ((mChannels == STEREO || mChannels == 1) &&
+ mInternalInBuffer.Length() == STEREO) {
+ // Don't worry if format is not set it will write silence then.
+ if ((mSampleFormat == AUDIO_FORMAT_S16 ||
+ mSampleFormat == AUDIO_FORMAT_FLOAT32) &&
+ mChannels == STEREO) {
+ // The mono channel is always up to date. When we are going from mono
+ // to stereo upmix the mono to stereo channel
+ uint32_t bufferedDuration = mInternalInBuffer[0].AvailableRead();
+ mInternalInBuffer[1].Clear();
+ if (bufferedDuration) {
+ mInternalInBuffer[1].Write(mInternalInBuffer[0], bufferedDuration);
+ }
+ }
+ // Maintain stereo size
+ mInputTail.SetLength(STEREO);
+ WarmUpResampler(false);
+ return;
+ }
+ // upmix or downmix, for now just clear but it has to be updated
+ // because allocates and this is executed in audio thread.
+ mInternalInBuffer.Clear();
+ for (uint32_t i = 0; i < mChannels; ++i) {
+ // Pre-allocate something big, twice the pre-buffer, or at least 100ms.
+ AudioRingBuffer* b = mInternalInBuffer.AppendElement(
+ sizeof(float) * std::max(2 * mPreBufferFrames, mInRate / 10));
+ if (mSampleFormat != AUDIO_FORMAT_SILENCE) {
+ // In ctor this update is not needed
+ b->SetSampleFormat(mSampleFormat);
+ }
+ }
+ mInputTail.SetLength(mChannels);
+ return;
+ }
+
+ if (mOutRate != aOutRate) {
+ // If the rates was the same the resampler was not being used so warm up.
+ if (mOutRate == mInRate) {
+ WarmUpResampler(true);
+ }
+
+#ifdef DEBUG
+ int rv =
+#endif
+ speex_resampler_set_rate(mResampler, mInRate, aOutRate);
+ MOZ_ASSERT(rv == RESAMPLER_ERR_SUCCESS);
+ mOutRate = aOutRate;
+ }
+}
+
+void DynamicResampler::WarmUpResampler(bool aSkipLatency) {
+ MOZ_ASSERT(mInputTail.Length());
+ for (uint32_t i = 0; i < mChannels; ++i) {
+ if (!mInputTail[i].Length()) {
+ continue;
+ }
+ uint32_t inFrames = mInputTail[i].Length();
+ uint32_t outFrames = 5 * TailBuffer::MAXSIZE; // something big
+ if (mSampleFormat == AUDIO_FORMAT_S16) {
+ short outBuffer[5 * TailBuffer::MAXSIZE] = {};
+ ResampleInternal(mInputTail[i].Buffer<short>(), &inFrames, outBuffer,
+ &outFrames, i);
+ MOZ_ASSERT(inFrames == (uint32_t)mInputTail[i].Length());
+ } else {
+ float outBuffer[100] = {};
+ ResampleInternal(mInputTail[i].Buffer<float>(), &inFrames, outBuffer,
+ &outFrames, i);
+ MOZ_ASSERT(inFrames == (uint32_t)mInputTail[i].Length());
+ }
+ }
+ if (aSkipLatency) {
+ int inputLatency = speex_resampler_get_input_latency(mResampler);
+ MOZ_ASSERT(inputLatency > 0);
+ uint32_t ratioNum, ratioDen;
+ speex_resampler_get_ratio(mResampler, &ratioNum, &ratioDen);
+ // Ratio at this point is one so only skip the input latency. No special
+ // calculations are needed.
+ speex_resampler_set_skip_frac_num(mResampler, inputLatency * ratioDen);
+ }
+}
+
+void DynamicResampler::AppendInput(const nsTArray<const float*>& aInBuffer,
+ uint32_t aInFrames) {
+ MOZ_ASSERT(mSampleFormat == AUDIO_FORMAT_FLOAT32);
+ AppendInputInternal(aInBuffer, aInFrames);
+}
+void DynamicResampler::AppendInput(const nsTArray<const int16_t*>& aInBuffer,
+ uint32_t aInFrames) {
+ MOZ_ASSERT(mSampleFormat == AUDIO_FORMAT_S16);
+ AppendInputInternal(aInBuffer, aInFrames);
+}
+
+bool DynamicResampler::EnoughInFrames(uint32_t aOutFrames,
+ uint32_t aChannelIndex) const {
+ if (mInRate == mOutRate) {
+ return InFramesBuffered(aChannelIndex) >= aOutFrames;
+ }
+ if (!(mOutRate % mInRate) && !(aOutFrames % mOutRate / mInRate)) {
+ return InFramesBuffered(aChannelIndex) >= aOutFrames / (mOutRate / mInRate);
+ }
+ if (!(mInRate % mOutRate) && !(aOutFrames % mOutRate / mInRate)) {
+ return InFramesBuffered(aChannelIndex) >= aOutFrames * mInRate / mOutRate;
+ }
+ return InFramesBuffered(aChannelIndex) > aOutFrames * mInRate / mOutRate;
+}
+
+bool DynamicResampler::CanResample(uint32_t aOutFrames) const {
+ for (uint32_t i = 0; i < mChannels; ++i) {
+ if (!EnoughInFrames(aOutFrames, i)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+void DynamicResampler::AppendInputSilence(const uint32_t aInFrames) {
+ MOZ_ASSERT(aInFrames);
+ MOZ_ASSERT(mChannels);
+ MOZ_ASSERT(mInternalInBuffer.Length() >= (uint32_t)mChannels);
+ for (uint32_t i = 0; i < mChannels; ++i) {
+ mInternalInBuffer[i].WriteSilence(aInFrames);
+ }
+}
+
+uint32_t DynamicResampler::InFramesBuffered(uint32_t aChannelIndex) const {
+ MOZ_ASSERT(mChannels);
+ MOZ_ASSERT(aChannelIndex <= mChannels);
+ MOZ_ASSERT(aChannelIndex <= mInternalInBuffer.Length());
+ return mInternalInBuffer[aChannelIndex].AvailableRead();
+}
+
+uint32_t DynamicResampler::InFramesLeftToBuffer(uint32_t aChannelIndex) const {
+ MOZ_ASSERT(mChannels);
+ MOZ_ASSERT(aChannelIndex <= mChannels);
+ MOZ_ASSERT(aChannelIndex <= mInternalInBuffer.Length());
+ return mInternalInBuffer[aChannelIndex].AvailableWrite();
+}
+
+AudioChunkList::AudioChunkList(uint32_t aTotalDuration, uint32_t aChannels,
+ const PrincipalHandle& aPrincipalHandle)
+ : mPrincipalHandle(aPrincipalHandle) {
+ uint32_t numOfChunks = aTotalDuration / mChunkCapacity;
+ if (aTotalDuration % mChunkCapacity) {
+ ++numOfChunks;
+ }
+ CreateChunks(numOfChunks, aChannels);
+}
+
+void AudioChunkList::CreateChunks(uint32_t aNumOfChunks, uint32_t aChannels) {
+ MOZ_ASSERT(!mChunks.Length());
+ MOZ_ASSERT(aNumOfChunks);
+ MOZ_ASSERT(aChannels);
+ mChunks.AppendElements(aNumOfChunks);
+
+ for (AudioChunk& chunk : mChunks) {
+ AutoTArray<nsTArray<float>, STEREO> buffer;
+ buffer.AppendElements(aChannels);
+
+ AutoTArray<const float*, STEREO> bufferPtrs;
+ bufferPtrs.AppendElements(aChannels);
+
+ for (uint32_t i = 0; i < aChannels; ++i) {
+ float* ptr = buffer[i].AppendElements(mChunkCapacity);
+ bufferPtrs[i] = ptr;
+ }
+
+ chunk.mBuffer = new mozilla::SharedChannelArrayBuffer(std::move(buffer));
+ chunk.mChannelData.AppendElements(aChannels);
+ for (uint32_t i = 0; i < aChannels; ++i) {
+ chunk.mChannelData[i] = bufferPtrs[i];
+ }
+ }
+}
+
+void AudioChunkList::UpdateToMonoOrStereo(uint32_t aChannels) {
+ MOZ_ASSERT(mChunks.Length());
+ MOZ_ASSERT(mSampleFormat == AUDIO_FORMAT_S16 ||
+ mSampleFormat == AUDIO_FORMAT_FLOAT32);
+ MOZ_ASSERT(aChannels == 1 || aChannels == 2);
+
+ for (AudioChunk& chunk : mChunks) {
+ MOZ_ASSERT(chunk.ChannelCount() != (uint32_t)aChannels);
+ MOZ_ASSERT(chunk.ChannelCount() == 1 || chunk.ChannelCount() == 2);
+ chunk.mChannelData.SetLengthAndRetainStorage(aChannels);
+ if (mSampleFormat == AUDIO_FORMAT_S16) {
+ SharedChannelArrayBuffer<short>* channelArray =
+ static_cast<SharedChannelArrayBuffer<short>*>(chunk.mBuffer.get());
+ channelArray->mBuffers.SetLengthAndRetainStorage(aChannels);
+ if (aChannels == 2) {
+ // This an indirect allocation, unfortunately.
+ channelArray->mBuffers[1].SetLength(mChunkCapacity);
+ chunk.mChannelData[1] = channelArray->mBuffers[1].Elements();
+ }
+ } else {
+ SharedChannelArrayBuffer<float>* channelArray =
+ static_cast<SharedChannelArrayBuffer<float>*>(chunk.mBuffer.get());
+ channelArray->mBuffers.SetLengthAndRetainStorage(aChannels);
+ if (aChannels == 2) {
+ // This an indirect allocation, unfortunately.
+ channelArray->mBuffers[1].SetLength(mChunkCapacity);
+ chunk.mChannelData[1] = channelArray->mBuffers[1].Elements();
+ }
+ }
+ }
+}
+
+void AudioChunkList::SetSampleFormat(AudioSampleFormat aFormat) {
+ MOZ_ASSERT(mSampleFormat == AUDIO_FORMAT_SILENCE);
+ MOZ_ASSERT(aFormat == AUDIO_FORMAT_S16 || aFormat == AUDIO_FORMAT_FLOAT32);
+ mSampleFormat = aFormat;
+ if (mSampleFormat == AUDIO_FORMAT_S16) {
+ mChunkCapacity = 2 * mChunkCapacity;
+ }
+}
+
+AudioChunk& AudioChunkList::GetNext() {
+ AudioChunk& chunk = mChunks[mIndex];
+ MOZ_ASSERT(!chunk.mChannelData.IsEmpty());
+ MOZ_ASSERT(chunk.mBuffer);
+ MOZ_ASSERT(!chunk.mBuffer->IsShared());
+ MOZ_ASSERT(mSampleFormat == AUDIO_FORMAT_S16 ||
+ mSampleFormat == AUDIO_FORMAT_FLOAT32);
+ chunk.mDuration = 0;
+ chunk.mVolume = 1.0f;
+ chunk.mPrincipalHandle = mPrincipalHandle;
+ chunk.mBufferFormat = mSampleFormat;
+ IncrementIndex();
+ return chunk;
+}
+
+void AudioChunkList::Update(uint32_t aChannels) {
+ MOZ_ASSERT(mChunks.Length());
+ if (mChunks[0].ChannelCount() == aChannels) {
+ return;
+ }
+
+ // Special handling between mono and stereo to avoid reallocations.
+ if (aChannels <= 2 && mChunks[0].ChannelCount() <= 2) {
+ UpdateToMonoOrStereo(aChannels);
+ return;
+ }
+
+ uint32_t numOfChunks = mChunks.Length();
+ mChunks.ClearAndRetainStorage();
+ CreateChunks(numOfChunks, aChannels);
+}
+
+AudioResampler::AudioResampler(uint32_t aInRate, uint32_t aOutRate,
+ uint32_t aPreBufferFrames,
+ const PrincipalHandle& aPrincipalHandle)
+ : mResampler(aInRate, aOutRate, aPreBufferFrames),
+ mOutputChunks(aOutRate / 10, STEREO, aPrincipalHandle) {}
+
+void AudioResampler::AppendInput(const AudioSegment& aInSegment) {
+ MOZ_ASSERT(aInSegment.GetDuration());
+ for (AudioSegment::ConstChunkIterator iter(aInSegment); !iter.IsEnded();
+ iter.Next()) {
+ const AudioChunk& chunk = *iter;
+ if (!mIsSampleFormatSet) {
+ // We don't know the format yet and all buffers are empty.
+ if (chunk.mBufferFormat == AUDIO_FORMAT_SILENCE) {
+ // Only silence has been received and the format is unkown. Igonre it,
+ // if Resampler() is called it will return silence too.
+ continue;
+ }
+ // First no silence data, set the format once for lifetime and let it
+ // continue the rest of the flow. We will not get in here again.
+ mOutputChunks.SetSampleFormat(chunk.mBufferFormat);
+ mResampler.SetSampleFormat(chunk.mBufferFormat);
+ mIsSampleFormatSet = true;
+ }
+ MOZ_ASSERT(mIsSampleFormatSet);
+ if (chunk.IsNull()) {
+ mResampler.AppendInputSilence(chunk.GetDuration());
+ continue;
+ }
+ // Make sure the channel is up to date. An AudioSegment can contain chunks
+ // with different channel count.
+ UpdateChannels(chunk.mChannelData.Length());
+ if (chunk.mBufferFormat == AUDIO_FORMAT_FLOAT32) {
+ mResampler.AppendInput(chunk.ChannelData<float>(), chunk.GetDuration());
+ } else {
+ mResampler.AppendInput(chunk.ChannelData<int16_t>(), chunk.GetDuration());
+ }
+ }
+}
+
+AudioSegment AudioResampler::Resample(uint32_t aOutFrames) {
+ AudioSegment segment;
+
+ // We don't know what to do yet and we only have received silence if any just
+ // return what they want and leave
+ if (!mIsSampleFormatSet) {
+ segment.AppendNullData(aOutFrames);
+ return segment;
+ }
+
+ // Not enough input frames abort. We check for the requested frames plus one.
+ // This is to make sure that the individual resample iteration that will
+ // follow up, will have enough frames even if one of them consume an extra
+ // frame.
+ if (!mResampler.CanResample(aOutFrames + 1)) {
+ return segment;
+ }
+
+ uint32_t totalFrames = aOutFrames;
+ while (totalFrames) {
+ MOZ_ASSERT(totalFrames > 0);
+ AudioChunk& chunk = mOutputChunks.GetNext();
+ uint32_t outFrames = std::min(totalFrames, mOutputChunks.ChunkCapacity());
+ totalFrames -= outFrames;
+
+ for (uint32_t i = 0; i < chunk.ChannelCount(); ++i) {
+ uint32_t outFramesUsed = outFrames;
+ if (chunk.mBufferFormat == AUDIO_FORMAT_FLOAT32) {
+#ifdef DEBUG
+ bool rv =
+#endif
+ mResampler.Resample(chunk.ChannelDataForWrite<float>(i),
+ &outFramesUsed, i);
+ MOZ_ASSERT(rv);
+ } else {
+#ifdef DEBUG
+ bool rv =
+#endif
+ mResampler.Resample(chunk.ChannelDataForWrite<int16_t>(i),
+ &outFramesUsed, i);
+ MOZ_ASSERT(rv);
+ }
+ MOZ_ASSERT(outFramesUsed == outFrames);
+ chunk.mDuration = outFrames;
+ }
+
+ // Create a copy in order to consume that copy and not the pre-allocated
+ // chunk
+ segment.AppendAndConsumeChunk(AudioChunk(chunk));
+ }
+
+ return segment;
+}
+
+void AudioResampler::Update(uint32_t aOutRate, uint32_t aChannels) {
+ mResampler.UpdateResampler(aOutRate, aChannels);
+ mOutputChunks.Update(aChannels);
+}
+
+uint32_t AudioResampler::InputReadableFrames() const {
+ if (!mIsSampleFormatSet) {
+ return mResampler.mPreBufferFrames;
+ }
+ return mResampler.InFramesBuffered(0);
+}
+
+uint32_t AudioResampler::InputWritableFrames() const {
+ if (!mIsSampleFormatSet) {
+ return mResampler.mPreBufferFrames;
+ }
+ return mResampler.InFramesLeftToBuffer(0);
+}
+
+} // namespace mozilla
diff --git a/dom/media/DynamicResampler.h b/dom/media/DynamicResampler.h
new file mode 100644
index 0000000000..f8c5aff0e4
--- /dev/null
+++ b/dom/media/DynamicResampler.h
@@ -0,0 +1,409 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_DYNAMIC_RESAMPLER_H_
+#define MOZILLA_DYNAMIC_RESAMPLER_H_
+
+#include "AudioRingBuffer.h"
+#include "AudioSegment.h"
+
+#include <speex/speex_resampler.h>
+
+namespace mozilla {
+
+const uint32_t STEREO = 2;
+
+/**
+ * DynamicResampler allows updating on the fly the output sample rate and the
+ * number of channels. In addition to that, it maintains an internal buffer for
+ * the input data and allows pre-buffering as well. The Resample() method
+ * strives to provide the requested number of output frames by using the input
+ * data including any pre-buffering. If this is not possible then it will not
+ * attempt to resample and it will return failure.
+ *
+ * Input data buffering makes use of the AudioRingBuffer. The capacity of the
+ * buffer is 100ms of float audio and it is pre-allocated at the constructor.
+ * No extra allocations take place when the input is appended. In addition to
+ * that, due to special feature of AudioRingBuffer, no extra copies take place
+ * when the input data is fed to the resampler.
+ *
+ * The sample format must be set before using any method. If the provided sample
+ * format is of type short the pre-allocated capacity of the input buffer
+ * becomes 200ms of short audio.
+ *
+ * The DynamicResampler is not thread-safe, so all the methods appart from the
+ * constructor must be called on the same thread.
+ */
+class DynamicResampler final {
+ public:
+ /**
+ * Provide the initial input and output rate and the amount of pre-buffering.
+ * The channel count will be set to stereo. Memory allocation will take
+ * place. The input buffer is non-interleaved.
+ */
+ DynamicResampler(uint32_t aInRate, uint32_t aOutRate,
+ uint32_t aPreBufferFrames = 0);
+ ~DynamicResampler();
+
+ /**
+ * Set the sample format type to float or short.
+ */
+ void SetSampleFormat(AudioSampleFormat aFormat);
+ uint32_t GetOutRate() const { return mOutRate; }
+ uint32_t GetChannels() const { return mChannels; }
+
+ /**
+ * Append `aInFrames` number of frames from `aInBuffer` to the internal input
+ * buffer. Memory copy/move takes place.
+ */
+ void AppendInput(const nsTArray<const float*>& aInBuffer, uint32_t aInFrames);
+ void AppendInput(const nsTArray<const int16_t*>& aInBuffer,
+ uint32_t aInFrames);
+ /**
+ * Append `aInFrames` number of frames of silence to the internal input
+ * buffer. Memory copy/move takes place.
+ */
+ void AppendInputSilence(const uint32_t aInFrames);
+ /**
+ * Return the number of frames stored in the internal input buffer.
+ */
+ uint32_t InFramesBuffered(uint32_t aChannelIndex) const;
+ /**
+ * Return the number of frames left to store in the internal input buffer.
+ */
+ uint32_t InFramesLeftToBuffer(uint32_t aChannelIndex) const;
+
+ /*
+ * Resampler as much frame is needed from the internal input buffer to the
+ * `aOutBuffer` in order to provide all `aOutFrames` and return true. If there
+ * not enough input frames to provide the requested output frames no
+ * resampling is attempted and false is returned.
+ */
+ bool Resample(float* aOutBuffer, uint32_t* aOutFrames,
+ uint32_t aChannelIndex);
+ bool Resample(int16_t* aOutBuffer, uint32_t* aOutFrames,
+ uint32_t aChannelIndex);
+
+ /**
+ * Update the output rate or/and the channel count. If a value is not updated
+ * compared to the current one nothing happens. Changing the `aOutRate`
+ * results in recalculation in the resampler. Changing `aChannels` results in
+ * the reallocation of the internal input buffer with the exception of
+ * changes between mono to stereo and vice versa where no reallocation takes
+ * place. A stereo internal input buffer is always maintained even if the
+ * sound is mono.
+ */
+ void UpdateResampler(uint32_t aOutRate, uint32_t aChannels);
+
+ /**
+ * Returns true if the resampler has enough input data to provide to the
+ * output of the `Resample()` method `aOutFrames` number of frames. This is a
+ * way to know in advance if the `Resampler` method will return true or false
+ * given that nothing changes in between.
+ */
+ bool CanResample(uint32_t aOutFrames) const;
+
+ private:
+ template <typename T>
+ void AppendInputInternal(const nsTArray<const T*>& aInBuffer,
+ uint32_t aInFrames) {
+ MOZ_ASSERT(aInBuffer.Length() == (uint32_t)mChannels);
+ for (uint32_t i = 0; i < mChannels; ++i) {
+ PushInFrames(aInBuffer[i], aInFrames, i);
+ }
+ }
+
+ void ResampleInternal(const float* aInBuffer, uint32_t* aInFrames,
+ float* aOutBuffer, uint32_t* aOutFrames,
+ uint32_t aChannelIndex);
+ void ResampleInternal(const int16_t* aInBuffer, uint32_t* aInFrames,
+ int16_t* aOutBuffer, uint32_t* aOutFrames,
+ uint32_t aChannelIndex);
+
+ template <typename T>
+ bool ResampleInternal(T* aOutBuffer, uint32_t* aOutFrames,
+ uint32_t aChannelIndex) {
+ MOZ_ASSERT(mInRate);
+ MOZ_ASSERT(mOutRate);
+ MOZ_ASSERT(mChannels);
+ MOZ_ASSERT(aChannelIndex <= mChannels);
+ MOZ_ASSERT(aChannelIndex <= mInternalInBuffer.Length());
+ MOZ_ASSERT(aOutFrames);
+ MOZ_ASSERT(*aOutFrames);
+
+ // Not enough input, don't do anything
+ if (!EnoughInFrames(*aOutFrames, aChannelIndex)) {
+ *aOutFrames = 0;
+ return false;
+ }
+
+ if (mInRate == mOutRate) {
+ mInternalInBuffer[aChannelIndex].Read(Span(aOutBuffer, *aOutFrames));
+ // Workaround to avoid discontinuity when the speex resampler operates
+ // again. Feed it with the last 20 frames to warm up the internal memory
+ // of the resampler and then skip memory equals to resampler's input
+ // latency.
+ mInputTail[aChannelIndex].StoreTail<T>(aOutBuffer, *aOutFrames);
+ return true;
+ }
+
+ uint32_t totalOutFramesNeeded = *aOutFrames;
+
+ mInternalInBuffer[aChannelIndex].ReadNoCopy(
+ [this, &aOutBuffer, &totalOutFramesNeeded,
+ aChannelIndex](const Span<const T>& aInBuffer) -> uint32_t {
+ if (!totalOutFramesNeeded) {
+ return 0;
+ }
+ uint32_t outFramesResampled = totalOutFramesNeeded;
+ uint32_t inFrames = aInBuffer.Length();
+ ResampleInternal(aInBuffer.data(), &inFrames, aOutBuffer,
+ &outFramesResampled, aChannelIndex);
+ aOutBuffer += outFramesResampled;
+ totalOutFramesNeeded -= outFramesResampled;
+ mInputTail[aChannelIndex].StoreTail<T>(aInBuffer);
+ return inFrames;
+ });
+
+ MOZ_ASSERT(totalOutFramesNeeded == 0);
+ return true;
+ }
+
+ bool EnoughInFrames(uint32_t aOutFrames, uint32_t aChannelIndex) const;
+
+ template <typename T>
+ void PushInFrames(const T* aInBuffer, const uint32_t aInFrames,
+ uint32_t aChannelIndex) {
+ MOZ_ASSERT(aInBuffer);
+ MOZ_ASSERT(aInFrames);
+ MOZ_ASSERT(mChannels);
+ MOZ_ASSERT(aChannelIndex <= mChannels);
+ MOZ_ASSERT(aChannelIndex <= mInternalInBuffer.Length());
+ mInternalInBuffer[aChannelIndex].Write(Span(aInBuffer, aInFrames));
+ }
+
+ void WarmUpResampler(bool aSkipLatency);
+
+ public:
+ const uint32_t mInRate;
+ const uint32_t mPreBufferFrames;
+
+ private:
+ uint32_t mChannels = 0;
+ uint32_t mOutRate;
+
+ AutoTArray<AudioRingBuffer, STEREO> mInternalInBuffer;
+
+ SpeexResamplerState* mResampler = nullptr;
+ AudioSampleFormat mSampleFormat = AUDIO_FORMAT_SILENCE;
+
+ class TailBuffer {
+ public:
+ template <typename T>
+ T* Buffer() {
+ return reinterpret_cast<T*>(mBuffer);
+ }
+ /* Store the MAXSIZE last elements of the buffer. */
+ template <typename T>
+ void StoreTail(const Span<const T>& aInBuffer) {
+ StoreTail(aInBuffer.data(), aInBuffer.size());
+ }
+ template <typename T>
+ void StoreTail(const T* aInBuffer, uint32_t aInFrames) {
+ if (aInFrames >= MAXSIZE) {
+ PodCopy(Buffer<T>(), aInBuffer + aInFrames - MAXSIZE, MAXSIZE);
+ mSize = MAXSIZE;
+ } else {
+ PodCopy(Buffer<T>(), aInBuffer, aInFrames);
+ mSize = aInFrames;
+ }
+ }
+ uint32_t Length() { return mSize; }
+ static const uint32_t MAXSIZE = 20;
+
+ private:
+ float mBuffer[MAXSIZE] = {};
+ uint32_t mSize = 0;
+ };
+ AutoTArray<TailBuffer, STEREO> mInputTail;
+};
+
+/**
+ * AudioChunkList provides a way to have preallocated audio buffers in
+ * AudioSegment. The idea is that the amount of AudioChunks is created in
+ * advance. Each AudioChunk is able to hold a specific amount of audio
+ * (capacity). The total capacity of AudioChunkList is specified by the number
+ * of AudioChunks. The important aspect of the AudioChunkList is that
+ * preallocates everything and reuse the same chunks similar to a ring buffer.
+ *
+ * Why the whole AudioChunk is preallocated and not some raw memory buffer? This
+ * is due to the limitations of MediaTrackGraph. The way that MTG works depends
+ * on `AudioSegment`s to convey the actual audio data. An AudioSegment consists
+ * of AudioChunks. The AudioChunk is built in a way, that owns and allocates the
+ * audio buffers. Thus, since the use of AudioSegment is mandatory if the audio
+ * data was in a different form, the only way to use it from the audio thread
+ * would be to create the AudioChunk there. That would result in a copy
+ * operation (not very important) and most of all an allocation of the audio
+ * buffer in the audio thread. This happens in many places inside MTG it's a bad
+ * practice, though, and it has been avoided due to the AudioChunkList.
+ *
+ * After construction the sample format must be set, when it is available. It
+ * can be set in the audio thread. Before setting the sample format is not
+ * possible to use any method of AudioChunkList.
+ *
+ * Every AudioChunk in the AudioChunkList is preallocated with a capacity of 128
+ * frames of float audio. Nevertheless, the sample format is not available at
+ * that point. Thus if the sample format is set to short, the capacity of each
+ * chunk changes to 256 number of frames, and the total duration becomes twice
+ * big. There are methods to get the chunk capacity and total capacity in frames
+ * and must always be used.
+ *
+ * Two things to note. First, when the channel count changes everything is
+ * recreated which means reallocations. Second, the total capacity might differs
+ * from the requested total capacity for two reasons. First, if the sample
+ * format is set to short and second because the number of chunks in the list
+ * divides exactly the final total capacity. The corresponding method must
+ * always be used to query the total capacity.
+ */
+class AudioChunkList {
+ public:
+ /**
+ * Constructor, the final total duration might be different from the requested
+ * `aTotalDuration`. Memory allocation takes place.
+ */
+ AudioChunkList(uint32_t aTotalDuration, uint32_t aChannels,
+ const PrincipalHandle& aPrincipalHandle);
+ AudioChunkList(const AudioChunkList&) = delete;
+ AudioChunkList(AudioChunkList&&) = delete;
+ ~AudioChunkList() = default;
+
+ /**
+ * Set sample format. It must be done before any other method being used.
+ */
+ void SetSampleFormat(AudioSampleFormat aFormat);
+ /**
+ * Get the next available AudioChunk. The duration of the chunk will be zero
+ * and the volume 1.0. However, the buffers will be there ready to be written.
+ * Please note, that a reference of the preallocated chunk is returned. Thus
+ * it _must not be consumed_ directly. If the chunk needs to be consumed it
+ * must be copied to a temporary chunk first. For example:
+ * ```
+ * AudioChunk& chunk = audioChunklist.GetNext();
+ * // Set up the chunk
+ * AudioChunk tmp = chunk;
+ * audioSegment.AppendAndConsumeChunk(std::move(tmp));
+ * ```
+ * This way no memory allocation or copy, takes place.
+ */
+ AudioChunk& GetNext();
+
+ /**
+ * Get the capacity of each individual AudioChunk in the list.
+ */
+ uint32_t ChunkCapacity() const {
+ MOZ_ASSERT(mSampleFormat == AUDIO_FORMAT_S16 ||
+ mSampleFormat == AUDIO_FORMAT_FLOAT32);
+ return mChunkCapacity;
+ }
+ /**
+ * Get the total capacity of AudioChunkList.
+ */
+ uint32_t TotalCapacity() const {
+ MOZ_ASSERT(mSampleFormat == AUDIO_FORMAT_S16 ||
+ mSampleFormat == AUDIO_FORMAT_FLOAT32);
+ return CheckedInt<uint32_t>(mChunkCapacity * mChunks.Length()).value();
+ }
+
+ /**
+ * Update the channel count of the AudioChunkList. Memory allocation is
+ * taking place.
+ */
+ void Update(uint32_t aChannels);
+
+ private:
+ void IncrementIndex() {
+ ++mIndex;
+ mIndex = CheckedInt<uint32_t>(mIndex % mChunks.Length()).value();
+ }
+ void CreateChunks(uint32_t aNumOfChunks, uint32_t aChannels);
+ void UpdateToMonoOrStereo(uint32_t aChannels);
+
+ private:
+ const PrincipalHandle mPrincipalHandle;
+ nsTArray<AudioChunk> mChunks;
+ uint32_t mIndex = 0;
+ uint32_t mChunkCapacity = WEBAUDIO_BLOCK_SIZE;
+ AudioSampleFormat mSampleFormat = AUDIO_FORMAT_SILENCE;
+};
+
+/**
+ * Audio Resampler is a resampler able to change the output rate and channels
+ * count on the fly. The API is simple and it is based in AudioSegment in order
+ * to be used MTG. All memory allocations, for input and output buffers, happen
+ * in the constructor and when channel count changes. The memory is recycled in
+ * order to avoid reallocations. It also supports prebuffering of silence. It
+ * consists of DynamicResampler and AudioChunkList so please read their
+ * documentation if you are interested in more details.
+ *
+ * The output buffer is preallocated and returned in the form of AudioSegment.
+ * The intention is to be used directly in a MediaTrack. Since an AudioChunk
+ * must no be "shared" in order to be written, the AudioSegment returned by
+ * resampler method must be cleaned up in order to be able for the `AudioChunk`s
+ * that it consists of to be reused. For `MediaTrack::mSegment` this happens
+ * every ~50ms (look at MediaTrack::AdvanceTimeVaryingValuesToCurrentTime). Thus
+ * memory capacity of 100ms has been preallocated for internal input and output
+ * buffering.
+ */
+class AudioResampler final {
+ public:
+ AudioResampler(uint32_t aInRate, uint32_t aOutRate, uint32_t aPreBufferFrames,
+ const PrincipalHandle& aPrincipalHandle);
+
+ /**
+ * Append input data into the resampler internal buffer. Copy/move of the
+ * memory is taking place. Also, the channel count will change according to
+ * the channel count of the chunks.
+ */
+ void AppendInput(const AudioSegment& aInSegment);
+ /**
+ * Get the number of frames that can be read from the internal input buffer
+ * before it becomes empty.
+ */
+ uint32_t InputReadableFrames() const;
+ /**
+ * Get the number of frames that can be written to the internal input buffer
+ * before it becomes full.
+ */
+ uint32_t InputWritableFrames() const;
+
+ /*
+ * Reguest `aOutFrames` of audio in the output sample rate. The internal
+ * buffered input is used. If there is no enough input for that amount of
+ * output and empty AudioSegment is returned
+ */
+ AudioSegment Resample(uint32_t aOutFrames);
+
+ /*
+ * Updates the output rate that will be used by the resampler.
+ */
+ void UpdateOutRate(uint32_t aOutRate) {
+ Update(aOutRate, mResampler.GetChannels());
+ }
+
+ private:
+ void UpdateChannels(uint32_t aChannels) {
+ Update(mResampler.GetOutRate(), aChannels);
+ }
+ void Update(uint32_t aOutRate, uint32_t aChannels);
+
+ private:
+ DynamicResampler mResampler;
+ AudioChunkList mOutputChunks;
+ bool mIsSampleFormatSet = false;
+};
+
+} // namespace mozilla
+
+#endif // MOZILLA_DYNAMIC_RESAMPLER_H_
diff --git a/dom/media/ExternalEngineStateMachine.cpp b/dom/media/ExternalEngineStateMachine.cpp
new file mode 100644
index 0000000000..9b568caa53
--- /dev/null
+++ b/dom/media/ExternalEngineStateMachine.cpp
@@ -0,0 +1,1196 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ExternalEngineStateMachine.h"
+
+#include "PerformanceRecorder.h"
+#ifdef MOZ_WMF_MEDIA_ENGINE
+# include "MFMediaEngineDecoderModule.h"
+# include "mozilla/MFMediaEngineChild.h"
+# include "mozilla/StaticPrefs_media.h"
+#endif
+#include "mozilla/Atomics.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/StaticMutex.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+
+extern LazyLogModule gMediaDecoderLog;
+
+#define FMT(x, ...) \
+ "Decoder=%p, State=%s, " x, mDecoderID, GetStateStr(), ##__VA_ARGS__
+#define LOG(x, ...) \
+ DDMOZ_LOG(gMediaDecoderLog, LogLevel::Debug, "Decoder=%p, State=%s, " x, \
+ mDecoderID, GetStateStr(), ##__VA_ARGS__)
+#define LOGV(x, ...) \
+ DDMOZ_LOG(gMediaDecoderLog, LogLevel::Verbose, "Decoder=%p, State=%s, " x, \
+ mDecoderID, GetStateStr(), ##__VA_ARGS__)
+#define LOGW(x, ...) NS_WARNING(nsPrintfCString(FMT(x, ##__VA_ARGS__)).get())
+#define LOGE(x, ...) \
+ NS_DebugBreak(NS_DEBUG_WARNING, \
+ nsPrintfCString(FMT(x, ##__VA_ARGS__)).get(), nullptr, \
+ __FILE__, __LINE__)
+
+const char* ExternalEngineEventToStr(ExternalEngineEvent aEvent) {
+#define EVENT_TO_STR(event) \
+ case ExternalEngineEvent::event: \
+ return #event
+ switch (aEvent) {
+ EVENT_TO_STR(LoadedMetaData);
+ EVENT_TO_STR(LoadedFirstFrame);
+ EVENT_TO_STR(LoadedData);
+ EVENT_TO_STR(Waiting);
+ EVENT_TO_STR(Playing);
+ EVENT_TO_STR(Seeked);
+ EVENT_TO_STR(BufferingStarted);
+ EVENT_TO_STR(BufferingEnded);
+ EVENT_TO_STR(Timeupdate);
+ EVENT_TO_STR(Ended);
+ EVENT_TO_STR(RequestForAudio);
+ EVENT_TO_STR(RequestForVideo);
+ EVENT_TO_STR(AudioEnough);
+ EVENT_TO_STR(VideoEnough);
+ default:
+ MOZ_ASSERT_UNREACHABLE("Undefined event!");
+ return "Undefined";
+ }
+#undef EVENT_TO_STR
+}
+
+/**
+ * This class monitors the amount of crash happened for a remote engine
+ * process. It the amount of crash of the remote process exceeds the defined
+ * threshold, then `ShouldRecoverProcess()` will return false to indicate that
+ * we should not keep spawning that remote process because it's too easy to
+ * crash.
+ *
+ * In addition, we also have another mechanism in the media format reader
+ * (MFR) to detect crash amount of remote processes, but that would only
+ * happen during the decoding process. The main reason to choose using this
+ * simple monitor, instead of the mechanism in the MFR is because that
+ * mechanism can't detect every crash happening in the remote process, such as
+ * crash happening during initializing the remote engine, or setting the CDM
+ * pipepline, which can happen prior to decoding.
+ */
+class ProcessCrashMonitor final {
+ public:
+ static void NotifyCrash() {
+ StaticMutexAutoLock lock(sMutex);
+ auto* monitor = ProcessCrashMonitor::EnsureInstance();
+ if (!monitor) {
+ return;
+ }
+ monitor->mCrashNums++;
+ }
+ static bool ShouldRecoverProcess() {
+ StaticMutexAutoLock lock(sMutex);
+ auto* monitor = ProcessCrashMonitor::EnsureInstance();
+ if (!monitor) {
+ return false;
+ }
+ return monitor->mCrashNums <= monitor->mMaxCrashes;
+ }
+
+ private:
+ ProcessCrashMonitor() : mCrashNums(0) {
+#ifdef MOZ_WMF_MEDIA_ENGINE
+ mMaxCrashes = StaticPrefs::media_wmf_media_engine_max_crashes();
+#else
+ mMaxCrashes = 0;
+#endif
+ };
+ ProcessCrashMonitor(const ProcessCrashMonitor&) = delete;
+ ProcessCrashMonitor& operator=(const ProcessCrashMonitor&) = delete;
+
+ static ProcessCrashMonitor* EnsureInstance() {
+ if (sIsShutdown) {
+ return nullptr;
+ }
+ if (!sCrashMonitor) {
+ sCrashMonitor.reset(new ProcessCrashMonitor());
+ GetMainThreadSerialEventTarget()->Dispatch(
+ NS_NewRunnableFunction("ProcessCrashMonitor::EnsureInstance", [&] {
+ RunOnShutdown(
+ [&] {
+ StaticMutexAutoLock lock(sMutex);
+ sCrashMonitor.reset();
+ sIsShutdown = true;
+ },
+ ShutdownPhase::XPCOMShutdown);
+ }));
+ }
+ return sCrashMonitor.get();
+ }
+
+ static inline StaticMutex sMutex;
+ static inline UniquePtr<ProcessCrashMonitor> sCrashMonitor;
+ static inline Atomic<bool> sIsShutdown{false};
+
+ uint32_t mCrashNums;
+ uint32_t mMaxCrashes;
+};
+
+/* static */
+const char* ExternalEngineStateMachine::StateToStr(State aNextState) {
+#define STATE_TO_STR(state) \
+ case State::state: \
+ return #state
+ switch (aNextState) {
+ STATE_TO_STR(InitEngine);
+ STATE_TO_STR(ReadingMetadata);
+ STATE_TO_STR(RunningEngine);
+ STATE_TO_STR(SeekingData);
+ STATE_TO_STR(ShutdownEngine);
+ STATE_TO_STR(RecoverEngine);
+ default:
+ MOZ_ASSERT_UNREACHABLE("Undefined state!");
+ return "Undefined";
+ }
+#undef STATE_TO_STR
+}
+
+const char* ExternalEngineStateMachine::GetStateStr() const {
+ return StateToStr(mState.mName);
+}
+
+void ExternalEngineStateMachine::ChangeStateTo(State aNextState) {
+ LOG("Change state : '%s' -> '%s' (play-state=%d)", StateToStr(mState.mName),
+ StateToStr(aNextState), mPlayState.Ref());
+ // Assert the possible state transitions.
+ MOZ_ASSERT_IF(mState.IsInitEngine(), aNextState == State::ReadingMetadata ||
+ aNextState == State::ShutdownEngine);
+ MOZ_ASSERT_IF(mState.IsReadingMetadata(),
+ aNextState == State::RunningEngine ||
+ aNextState == State::ShutdownEngine);
+ MOZ_ASSERT_IF(mState.IsRunningEngine(),
+ aNextState == State::SeekingData ||
+ aNextState == State::ShutdownEngine ||
+ aNextState == State::RecoverEngine);
+ MOZ_ASSERT_IF(mState.IsSeekingData(),
+ aNextState == State::RunningEngine ||
+ aNextState == State::ShutdownEngine ||
+ aNextState == State::RecoverEngine);
+ MOZ_ASSERT_IF(mState.IsShutdownEngine(), aNextState == State::ShutdownEngine);
+ MOZ_ASSERT_IF(
+ mState.IsRecoverEngine(),
+ aNextState == State::SeekingData || aNextState == State::ShutdownEngine);
+ if (aNextState == State::SeekingData) {
+ mState = StateObject({StateObject::SeekingData()});
+ } else if (aNextState == State::ReadingMetadata) {
+ mState = StateObject({StateObject::ReadingMetadata()});
+ } else if (aNextState == State::RunningEngine) {
+ mState = StateObject({StateObject::RunningEngine()});
+ } else if (aNextState == State::ShutdownEngine) {
+ mState = StateObject({StateObject::ShutdownEngine()});
+ } else if (aNextState == State::RecoverEngine) {
+ mState = StateObject({StateObject::RecoverEngine()});
+ } else {
+ MOZ_ASSERT_UNREACHABLE("Wrong state!");
+ }
+}
+
+ExternalEngineStateMachine::ExternalEngineStateMachine(
+ MediaDecoder* aDecoder, MediaFormatReader* aReader)
+ : MediaDecoderStateMachineBase(aDecoder, aReader) {
+ LOG("Created ExternalEngineStateMachine");
+ MOZ_ASSERT(mState.IsInitEngine());
+ InitEngine();
+}
+
+void ExternalEngineStateMachine::InitEngine() {
+ MOZ_ASSERT(mState.IsInitEngine() || mState.IsRecoverEngine());
+#ifdef MOZ_WMF_MEDIA_ENGINE
+ mEngine.reset(new MFMediaEngineWrapper(this, mFrameStats));
+#endif
+ if (mEngine) {
+ auto* state = mState.AsInitEngine();
+ state->mInitPromise = mEngine->Init(!mMinimizePreroll);
+ state->mInitPromise
+ ->Then(OwnerThread(), __func__, this,
+ &ExternalEngineStateMachine::OnEngineInitSuccess,
+ &ExternalEngineStateMachine::OnEngineInitFailure)
+ ->Track(state->mEngineInitRequest);
+ }
+}
+
+void ExternalEngineStateMachine::OnEngineInitSuccess() {
+ AssertOnTaskQueue();
+ AUTO_PROFILER_LABEL("ExternalEngineStateMachine::OnEngineInitSuccess",
+ MEDIA_PLAYBACK);
+ MOZ_ASSERT(mState.IsInitEngine() || mState.IsRecoverEngine());
+ LOG("Initialized the external playback engine %" PRIu64, mEngine->Id());
+ auto* state = mState.AsInitEngine();
+ state->mEngineInitRequest.Complete();
+ mReader->UpdateMediaEngineId(mEngine->Id());
+ state->mInitPromise = nullptr;
+ if (mState.IsInitEngine()) {
+ ChangeStateTo(State::ReadingMetadata);
+ ReadMetadata();
+ return;
+ }
+ // We just recovered from CDM process crash, so we need to update the media
+ // info to the new CDM process.
+ MOZ_ASSERT(mInfo);
+ mEngine->SetMediaInfo(*mInfo);
+ SeekTarget target(mCurrentPosition.Ref(), SeekTarget::Type::Accurate);
+ Seek(target);
+}
+
+void ExternalEngineStateMachine::OnEngineInitFailure() {
+ AssertOnTaskQueue();
+ MOZ_ASSERT(mState.IsInitEngine() || mState.IsRecoverEngine());
+ LOGE("Failed to initialize the external playback engine");
+ auto* state = mState.AsInitEngine();
+ state->mEngineInitRequest.Complete();
+ state->mInitPromise = nullptr;
+ // TODO : Should fallback to the normal playback with media engine.
+ DecodeError(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__));
+}
+
+void ExternalEngineStateMachine::ReadMetadata() {
+ AssertOnTaskQueue();
+ MOZ_ASSERT(mState.IsReadingMetadata());
+ mReader->ReadMetadata()
+ ->Then(OwnerThread(), __func__, this,
+ &ExternalEngineStateMachine::OnMetadataRead,
+ &ExternalEngineStateMachine::OnMetadataNotRead)
+ ->Track(mState.AsReadingMetadata()->mMetadataRequest);
+}
+
+void ExternalEngineStateMachine::OnMetadataRead(MetadataHolder&& aMetadata) {
+ AssertOnTaskQueue();
+ AUTO_PROFILER_LABEL("ExternalEngineStateMachine::OnMetadataRead",
+ MEDIA_PLAYBACK);
+ MOZ_ASSERT(mState.IsReadingMetadata());
+ LOG("OnMetadataRead");
+
+ mState.AsReadingMetadata()->mMetadataRequest.Complete();
+ mInfo.emplace(*aMetadata.mInfo);
+ mMediaSeekable = Info().mMediaSeekable;
+ mMediaSeekableOnlyInBufferedRanges =
+ Info().mMediaSeekableOnlyInBufferedRanges;
+
+ if (!IsFormatSupportedByExternalEngine(*mInfo)) {
+ // The external engine doesn't support the type, try to notify the decoder
+ // to use our own state machine again.
+ DecodeError(
+ MediaResult(NS_ERROR_DOM_MEDIA_EXTERNAL_ENGINE_NOT_SUPPORTED_ERR));
+ return;
+ }
+
+#ifdef MOZ_WMF_MEDIA_ENGINE
+ // Only support encrypted playback.
+ if (!mInfo->IsEncrypted() &&
+ StaticPrefs::media_wmf_media_engine_enabled() == 2) {
+ LOG("External engine only supports encrypted playback by the pref");
+ DecodeError(
+ MediaResult(NS_ERROR_DOM_MEDIA_EXTERNAL_ENGINE_NOT_SUPPORTED_ERR));
+ return;
+ }
+#endif
+
+ mEngine->SetMediaInfo(*mInfo);
+
+ if (Info().mMetadataDuration.isSome()) {
+ mDuration = Info().mMetadataDuration;
+ } else if (Info().mUnadjustedMetadataEndTime.isSome()) {
+ const media::TimeUnit unadjusted = Info().mUnadjustedMetadataEndTime.ref();
+ const media::TimeUnit adjustment = Info().mStartTime;
+ mInfo->mMetadataDuration.emplace(unadjusted - adjustment);
+ mDuration = Info().mMetadataDuration;
+ }
+
+ // If we don't know the duration by this point, we assume infinity, per spec.
+ if (mDuration.Ref().isNothing()) {
+ mDuration = Some(media::TimeUnit::FromInfinity());
+ }
+ MOZ_ASSERT(mDuration.Ref().isSome());
+
+ mMetadataLoadedEvent.Notify(std::move(aMetadata.mInfo),
+ std::move(aMetadata.mTags),
+ MediaDecoderEventVisibility::Observable);
+ StartRunningEngine();
+}
+
+void ExternalEngineStateMachine::OnMetadataNotRead(const MediaResult& aError) {
+ AssertOnTaskQueue();
+ MOZ_ASSERT(mState.IsReadingMetadata());
+ LOGE("Decode metadata failed, shutting down decoder");
+ mState.AsReadingMetadata()->mMetadataRequest.Complete();
+ DecodeError(aError);
+}
+
+bool ExternalEngineStateMachine::IsFormatSupportedByExternalEngine(
+ const MediaInfo& aInfo) {
+ AssertOnTaskQueue();
+ MOZ_ASSERT(mState.IsReadingMetadata());
+#ifdef MOZ_WMF_MEDIA_ENGINE
+ const bool audioSupported =
+ !aInfo.HasAudio() ||
+ MFMediaEngineDecoderModule::SupportsConfig(aInfo.mAudio);
+ const bool videoSupported =
+ !aInfo.HasVideo() ||
+ MFMediaEngineDecoderModule::SupportsConfig(aInfo.mVideo);
+ LOG("audio=%s (supported=%d), video=%s(supported=%d)",
+ aInfo.HasAudio() ? aInfo.mAudio.mMimeType.get() : "none", audioSupported,
+ aInfo.HasVideo() ? aInfo.mVideo.mMimeType.get() : "none", videoSupported);
+ return audioSupported && videoSupported;
+#else
+ return false;
+#endif
+}
+
+RefPtr<MediaDecoder::SeekPromise> ExternalEngineStateMachine::Seek(
+ const SeekTarget& aTarget) {
+ AssertOnTaskQueue();
+ if (!mState.IsRunningEngine() && !mState.IsSeekingData() &&
+ !mState.IsRecoverEngine()) {
+ MOZ_ASSERT(false, "Can't seek due to unsupported state.");
+ return MediaDecoder::SeekPromise::CreateAndReject(true, __func__);
+ }
+ // We don't support these type of seek, because they're depending on the
+ // implementation of the external engine, which might not be supported.
+ if (aTarget.IsNextFrame() || aTarget.IsVideoOnly()) {
+ return MediaDecoder::SeekPromise::CreateAndReject(true, __func__);
+ }
+
+ LOG("Start seeking to %" PRId64, aTarget.GetTime().ToMicroseconds());
+ auto* state = mState.AsSeekingData();
+ if (!state) {
+ // We're in other states, so change the state to seeking.
+ ChangeStateTo(State::SeekingData);
+ state = mState.AsSeekingData();
+ }
+ state->SetTarget(aTarget);
+
+ // Update related status.
+ mSentPlaybackEndedEvent = false;
+ mOnPlaybackEvent.Notify(MediaPlaybackEvent::SeekStarted);
+ mOnNextFrameStatus.Notify(MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE_SEEKING);
+
+ // Notify the external playback engine about seeking. After the engine changes
+ // its current time, it would send `seeked` event.
+ mEngine->Seek(aTarget.GetTime());
+ state->mWaitingEngineSeeked = true;
+ SeekReader();
+ return state->mSeekJob.mPromise.Ensure(__func__);
+}
+
+void ExternalEngineStateMachine::SeekReader() {
+ AssertOnTaskQueue();
+ MOZ_ASSERT(mState.IsSeekingData());
+ auto* state = mState.AsSeekingData();
+
+ // Reset the reader first and ask it to perform a demuxer seek.
+ ResetDecode();
+ state->mWaitingReaderSeeked = true;
+ LOG("Seek reader to %" PRId64, state->GetTargetTime().ToMicroseconds());
+ mReader->Seek(state->mSeekJob.mTarget.ref())
+ ->Then(OwnerThread(), __func__, this,
+ &ExternalEngineStateMachine::OnSeekResolved,
+ &ExternalEngineStateMachine::OnSeekRejected)
+ ->Track(state->mSeekRequest);
+}
+
+void ExternalEngineStateMachine::OnSeekResolved(const media::TimeUnit& aUnit) {
+ AUTO_PROFILER_LABEL("ExternalEngineStateMachine::OnSeekResolved",
+ MEDIA_PLAYBACK);
+ AssertOnTaskQueue();
+ MOZ_ASSERT(mState.IsSeekingData());
+ auto* state = mState.AsSeekingData();
+
+ LOG("OnReaderSeekResolved");
+ state->mSeekRequest.Complete();
+ state->mWaitingReaderSeeked = false;
+
+ // Start sending new data to the external playback engine.
+ if (HasAudio()) {
+ mHasEnoughAudio = false;
+ OnRequestAudio();
+ }
+ if (HasVideo()) {
+ mHasEnoughVideo = false;
+ OnRequestVideo();
+ }
+ CheckIfSeekCompleted();
+}
+
+void ExternalEngineStateMachine::OnSeekRejected(
+ const SeekRejectValue& aReject) {
+ AUTO_PROFILER_LABEL("ExternalEngineStateMachine::OnSeekRejected",
+ MEDIA_PLAYBACK);
+ AssertOnTaskQueue();
+ MOZ_ASSERT(mState.IsSeekingData());
+ auto* state = mState.AsSeekingData();
+
+ LOG("OnReaderSeekRejected");
+ state->mSeekRequest.Complete();
+ if (aReject.mError == NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA) {
+ LOG("OnSeekRejected reason=WAITING_FOR_DATA type=%s",
+ MediaData::TypeToStr(aReject.mType));
+ MOZ_ASSERT_IF(aReject.mType == MediaData::Type::AUDIO_DATA,
+ !IsRequestingAudioData());
+ MOZ_ASSERT_IF(aReject.mType == MediaData::Type::VIDEO_DATA,
+ !IsRequestingVideoData());
+ MOZ_ASSERT_IF(aReject.mType == MediaData::Type::AUDIO_DATA,
+ !IsWaitingAudioData());
+ MOZ_ASSERT_IF(aReject.mType == MediaData::Type::VIDEO_DATA,
+ !IsWaitingVideoData());
+
+ // Fire 'waiting' to notify the player that we are waiting for data.
+ mOnNextFrameStatus.Notify(
+ MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE_SEEKING);
+ WaitForData(aReject.mType);
+ return;
+ }
+
+ if (aReject.mError == NS_ERROR_DOM_MEDIA_END_OF_STREAM) {
+ EndOfStream(aReject.mType);
+ return;
+ }
+
+ MOZ_ASSERT(NS_FAILED(aReject.mError),
+ "Cancels should also disconnect mSeekRequest");
+ state->RejectIfExists(__func__);
+ DecodeError(aReject.mError);
+}
+
+bool ExternalEngineStateMachine::IsSeeking() {
+ AssertOnTaskQueue();
+ const auto* state = mState.AsSeekingData();
+ return state && state->IsSeeking();
+}
+
+void ExternalEngineStateMachine::CheckIfSeekCompleted() {
+ AssertOnTaskQueue();
+ MOZ_ASSERT(mState.IsSeekingData());
+ auto* state = mState.AsSeekingData();
+ if (state->mWaitingEngineSeeked || state->mWaitingReaderSeeked) {
+ LOG("Seek hasn't been completed yet, waitEngineSeeked=%d, "
+ "waitReaderSeeked=%d",
+ state->mWaitingEngineSeeked, state->mWaitingReaderSeeked);
+ return;
+ }
+
+ LOG("Seek completed");
+ state->Resolve(__func__);
+ mOnPlaybackEvent.Notify(MediaPlaybackEvent::Invalidate);
+ mOnNextFrameStatus.Notify(MediaDecoderOwner::NEXT_FRAME_AVAILABLE);
+ StartRunningEngine();
+}
+
+void ExternalEngineStateMachine::ResetDecode() {
+ AssertOnTaskQueue();
+ if (!mInfo) {
+ return;
+ }
+
+ LOG("ResetDecode");
+ MediaFormatReader::TrackSet tracks;
+ if (HasVideo()) {
+ mVideoDataRequest.DisconnectIfExists();
+ mVideoWaitRequest.DisconnectIfExists();
+ tracks += TrackInfo::kVideoTrack;
+ }
+ if (HasAudio()) {
+ mAudioDataRequest.DisconnectIfExists();
+ mAudioWaitRequest.DisconnectIfExists();
+ tracks += TrackInfo::kAudioTrack;
+ }
+ mReader->ResetDecode(tracks);
+}
+
+RefPtr<GenericPromise> ExternalEngineStateMachine::InvokeSetSink(
+ const RefPtr<AudioDeviceInfo>& aSink) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // TODO : can media engine support this?
+ return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+}
+
+RefPtr<ShutdownPromise> ExternalEngineStateMachine::Shutdown() {
+ AssertOnTaskQueue();
+ if (mState.IsShutdownEngine()) {
+ LOG("Already shutdown");
+ return mState.AsShutdownEngine()->mShutdown;
+ }
+
+ LOG("Shutdown");
+ ChangeStateTo(State::ShutdownEngine);
+ ResetDecode();
+
+ mAudioDataRequest.DisconnectIfExists();
+ mVideoDataRequest.DisconnectIfExists();
+ mAudioWaitRequest.DisconnectIfExists();
+ mVideoWaitRequest.DisconnectIfExists();
+
+ mDuration.DisconnectAll();
+ mCurrentPosition.DisconnectAll();
+ // TODO : implement audible check
+ mIsAudioDataAudible.DisconnectAll();
+
+ mMetadataManager.Disconnect();
+
+ mSetCDMProxyPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_ABORT_ERR, __func__);
+ mSetCDMProxyRequest.DisconnectIfExists();
+
+ mEngine->Shutdown();
+
+ auto* state = mState.AsShutdownEngine();
+ state->mShutdown = mReader->Shutdown()->Then(
+ OwnerThread(), __func__, [self = RefPtr{this}, this]() {
+ LOG("Shutting down state machine task queue");
+ return OwnerThread()->BeginShutdown();
+ });
+ return state->mShutdown;
+}
+
+void ExternalEngineStateMachine::BufferedRangeUpdated() {
+ AssertOnTaskQueue();
+ AUTO_PROFILER_LABEL("ExternalEngineStateMachine::BufferedRangeUpdated",
+ MEDIA_PLAYBACK);
+
+ // While playing an unseekable stream of unknown duration, mDuration
+ // is updated as we play. But if data is being downloaded
+ // faster than played, mDuration won't reflect the end of playable data
+ // since we haven't played the frame at the end of buffered data. So update
+ // mDuration here as new data is downloaded to prevent such a lag.
+ if (mBuffered.Ref().IsInvalid()) {
+ return;
+ }
+
+ bool exists;
+ media::TimeUnit end{mBuffered.Ref().GetEnd(&exists)};
+ if (!exists) {
+ return;
+ }
+
+ // Use estimated duration from buffer ranges when mDuration is unknown or
+ // the estimated duration is larger.
+ if (mDuration.Ref().isNothing() || mDuration.Ref()->IsInfinite() ||
+ end > mDuration.Ref().ref()) {
+ mDuration = Some(end);
+ DDLOG(DDLogCategory::Property, "duration_us",
+ mDuration.Ref()->ToMicroseconds());
+ }
+}
+
+// Note: the variadic only supports passing member variables.
+#define PERFORM_WHEN_ALLOW(Func, ...) \
+ do { \
+ /* Initialzation is not done yet, postpone the operation */ \
+ if ((mState.IsInitEngine() || mState.IsRecoverEngine()) && \
+ mState.AsInitEngine()->mInitPromise) { \
+ LOG("%s is called before init", __func__); \
+ mState.AsInitEngine()->mInitPromise->Then( \
+ OwnerThread(), __func__, \
+ [self = RefPtr{this}, this]( \
+ const GenericNonExclusivePromise::ResolveOrRejectValue& aVal) { \
+ if (aVal.IsResolve()) { \
+ Func(__VA_ARGS__); \
+ } \
+ }); \
+ return; \
+ } else if (mState.IsShutdownEngine()) { \
+ return; \
+ } \
+ } while (false)
+
+void ExternalEngineStateMachine::SetPlaybackRate(double aPlaybackRate) {
+ AssertOnTaskQueue();
+ mPlaybackRate = aPlaybackRate;
+ PERFORM_WHEN_ALLOW(SetPlaybackRate, mPlaybackRate);
+ mEngine->SetPlaybackRate(aPlaybackRate);
+}
+
+void ExternalEngineStateMachine::VolumeChanged() {
+ AssertOnTaskQueue();
+ PERFORM_WHEN_ALLOW(VolumeChanged);
+ mEngine->SetVolume(mVolume);
+}
+
+void ExternalEngineStateMachine::PreservesPitchChanged() {
+ AssertOnTaskQueue();
+ PERFORM_WHEN_ALLOW(PreservesPitchChanged);
+ mEngine->SetPreservesPitch(mPreservesPitch);
+}
+
+void ExternalEngineStateMachine::PlayStateChanged() {
+ AssertOnTaskQueue();
+ PERFORM_WHEN_ALLOW(PlayStateChanged);
+ if (mPlayState == MediaDecoder::PLAY_STATE_PLAYING) {
+ mEngine->Play();
+ } else if (mPlayState == MediaDecoder::PLAY_STATE_PAUSED) {
+ mEngine->Pause();
+ }
+}
+
+void ExternalEngineStateMachine::LoopingChanged() {
+ AssertOnTaskQueue();
+ PERFORM_WHEN_ALLOW(LoopingChanged);
+ mEngine->SetLooping(mLooping);
+}
+
+#undef PERFORM_WHEN_ALLOW
+
+void ExternalEngineStateMachine::EndOfStream(MediaData::Type aType) {
+ AssertOnTaskQueue();
+ MOZ_ASSERT(mState.IsRunningEngine() || mState.IsSeekingData());
+ static auto DataTypeToTrackType = [](const MediaData::Type& aType) {
+ if (aType == MediaData::Type::VIDEO_DATA) {
+ return TrackInfo::TrackType::kVideoTrack;
+ }
+ if (aType == MediaData::Type::AUDIO_DATA) {
+ return TrackInfo::TrackType::kAudioTrack;
+ }
+ return TrackInfo::TrackType::kUndefinedTrack;
+ };
+ mEngine->NotifyEndOfStream(DataTypeToTrackType(aType));
+}
+
+void ExternalEngineStateMachine::WaitForData(MediaData::Type aType) {
+ AssertOnTaskQueue();
+ MOZ_ASSERT(mState.IsRunningEngine() || mState.IsSeekingData());
+ AUTO_PROFILER_LABEL("ExternalEngineStateMachine::WaitForData",
+ MEDIA_PLAYBACK);
+ MOZ_ASSERT(aType == MediaData::Type::AUDIO_DATA ||
+ aType == MediaData::Type::VIDEO_DATA);
+
+ LOG("WaitForData");
+ RefPtr<ExternalEngineStateMachine> self = this;
+ if (aType == MediaData::Type::AUDIO_DATA) {
+ MOZ_ASSERT(HasAudio());
+ mReader->WaitForData(MediaData::Type::AUDIO_DATA)
+ ->Then(
+ OwnerThread(), __func__,
+ [self, this](MediaData::Type aType) {
+ AUTO_PROFILER_LABEL(
+ "ExternalEngineStateMachine::WaitForData:AudioResolved",
+ MEDIA_PLAYBACK);
+ MOZ_ASSERT(aType == MediaData::Type::AUDIO_DATA);
+ LOG("Done waiting for audio data");
+ mAudioWaitRequest.Complete();
+ MaybeFinishWaitForData();
+ },
+ [self, this](const WaitForDataRejectValue& aRejection) {
+ AUTO_PROFILER_LABEL(
+ "ExternalEngineStateMachine::WaitForData:AudioRejected",
+ MEDIA_PLAYBACK);
+ mAudioWaitRequest.Complete();
+ DecodeError(NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA);
+ })
+ ->Track(mAudioWaitRequest);
+ } else {
+ MOZ_ASSERT(HasVideo());
+ mReader->WaitForData(MediaData::Type::VIDEO_DATA)
+ ->Then(
+ OwnerThread(), __func__,
+ [self, this](MediaData::Type aType) {
+ AUTO_PROFILER_LABEL(
+ "ExternalEngineStateMachine::WaitForData:VideoResolved",
+ MEDIA_PLAYBACK);
+ MOZ_ASSERT(aType == MediaData::Type::VIDEO_DATA);
+ LOG("Done waiting for video data");
+ mVideoWaitRequest.Complete();
+ MaybeFinishWaitForData();
+ },
+ [self, this](const WaitForDataRejectValue& aRejection) {
+ AUTO_PROFILER_LABEL(
+ "ExternalEngineStateMachine::WaitForData:VideoRejected",
+ MEDIA_PLAYBACK);
+ mVideoWaitRequest.Complete();
+ DecodeError(NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA);
+ })
+ ->Track(mVideoWaitRequest);
+ }
+}
+
+void ExternalEngineStateMachine::MaybeFinishWaitForData() {
+ AssertOnTaskQueue();
+ MOZ_ASSERT(mState.IsRunningEngine() || mState.IsSeekingData());
+
+ bool isWaitingForAudio = HasAudio() && mAudioWaitRequest.Exists();
+ bool isWaitingForVideo = HasVideo() && mVideoWaitRequest.Exists();
+ if (isWaitingForAudio || isWaitingForVideo) {
+ LOG("Still waiting for data (waitAudio=%d, waitVideo=%d)",
+ isWaitingForAudio, isWaitingForVideo);
+ return;
+ }
+
+ LOG("Finished waiting for data");
+ if (mState.IsSeekingData()) {
+ SeekReader();
+ return;
+ }
+ if (HasAudio()) {
+ RunningEngineUpdate(MediaData::Type::AUDIO_DATA);
+ }
+ if (HasVideo()) {
+ RunningEngineUpdate(MediaData::Type::VIDEO_DATA);
+ }
+}
+
+void ExternalEngineStateMachine::StartRunningEngine() {
+ ChangeStateTo(State::RunningEngine);
+ // Manually check the play state because the engine might be recovered from
+ // crash or just get recreated, so PlayStateChanged() won't be triggered.
+ if (mPlayState == MediaDecoder::PLAY_STATE_PLAYING) {
+ mEngine->Play();
+ }
+ if (HasAudio()) {
+ RunningEngineUpdate(MediaData::Type::AUDIO_DATA);
+ }
+ if (HasVideo()) {
+ RunningEngineUpdate(MediaData::Type::VIDEO_DATA);
+ }
+}
+
+void ExternalEngineStateMachine::RunningEngineUpdate(MediaData::Type aType) {
+ AssertOnTaskQueue();
+ MOZ_ASSERT(mState.IsRunningEngine() || mState.IsSeekingData());
+ if (aType == MediaData::Type::AUDIO_DATA && !mHasEnoughAudio) {
+ OnRequestAudio();
+ }
+ if (aType == MediaData::Type::VIDEO_DATA && !mHasEnoughVideo) {
+ OnRequestVideo();
+ }
+}
+
+void ExternalEngineStateMachine::OnRequestAudio() {
+ AssertOnTaskQueue();
+ MOZ_ASSERT(mState.IsRunningEngine() || mState.IsSeekingData());
+ LOGV("OnRequestAudio");
+
+ if (!HasAudio()) {
+ return;
+ }
+
+ if (IsRequestingAudioData() || mAudioWaitRequest.Exists() || IsSeeking()) {
+ LOGV(
+ "No need to request audio, isRequesting=%d, waitingAudio=%d, "
+ "isSeeking=%d",
+ IsRequestingAudioData(), mAudioWaitRequest.Exists(), IsSeeking());
+ return;
+ }
+
+ LOGV("Start requesting audio");
+ PerformanceRecorder<PlaybackStage> perfRecorder(MediaStage::RequestData);
+ RefPtr<ExternalEngineStateMachine> self = this;
+ mReader->RequestAudioData()
+ ->Then(
+ OwnerThread(), __func__,
+ [this, self, perfRecorder(std::move(perfRecorder))](
+ const RefPtr<AudioData>& aAudio) mutable {
+ perfRecorder.Record();
+ mAudioDataRequest.Complete();
+ LOGV("Completed requesting audio");
+ AUTO_PROFILER_LABEL(
+ "ExternalEngineStateMachine::OnRequestAudio:Resolved",
+ MEDIA_PLAYBACK);
+ MOZ_ASSERT(aAudio);
+ RunningEngineUpdate(MediaData::Type::AUDIO_DATA);
+ },
+ [this, self](const MediaResult& aError) {
+ mAudioDataRequest.Complete();
+ AUTO_PROFILER_LABEL(
+ "ExternalEngineStateMachine::OnRequestAudio:Rejected",
+ MEDIA_PLAYBACK);
+ LOG("OnRequestAudio ErrorName=%s Message=%s",
+ aError.ErrorName().get(), aError.Message().get());
+ switch (aError.Code()) {
+ case NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA:
+ WaitForData(MediaData::Type::AUDIO_DATA);
+ break;
+ case NS_ERROR_DOM_MEDIA_CANCELED:
+ OnRequestAudio();
+ break;
+ case NS_ERROR_DOM_MEDIA_END_OF_STREAM:
+ LOG("Reach to the end, no more audio data");
+ EndOfStream(MediaData::Type::AUDIO_DATA);
+ break;
+ case NS_ERROR_DOM_MEDIA_REMOTE_DECODER_CRASHED_MF_CDM_ERR:
+ // We will handle the process crash in `NotifyErrorInternal()`
+ // so here just silently ignore this.
+ break;
+ default:
+ DecodeError(aError);
+ }
+ })
+ ->Track(mAudioDataRequest);
+}
+
+void ExternalEngineStateMachine::OnRequestVideo() {
+ AssertOnTaskQueue();
+ MOZ_ASSERT(mState.IsRunningEngine() || mState.IsSeekingData());
+ LOGV("OnRequestVideo");
+
+ if (!HasVideo()) {
+ return;
+ }
+
+ if (IsRequestingVideoData() || mVideoWaitRequest.Exists() || IsSeeking()) {
+ LOGV(
+ "No need to request video, isRequesting=%d, waitingVideo=%d, "
+ "isSeeking=%d",
+ IsRequestingVideoData(), mVideoWaitRequest.Exists(), IsSeeking());
+ return;
+ }
+
+ LOGV("Start requesting video");
+ PerformanceRecorder<PlaybackStage> perfRecorder(MediaStage::RequestData,
+ Info().mVideo.mImage.height);
+ RefPtr<ExternalEngineStateMachine> self = this;
+ mReader->RequestVideoData(GetVideoThreshold(), false)
+ ->Then(
+ OwnerThread(), __func__,
+ [this, self, perfRecorder(std::move(perfRecorder))](
+ const RefPtr<VideoData>& aVideo) mutable {
+ perfRecorder.Record();
+ mVideoDataRequest.Complete();
+ LOGV("Completed requesting video");
+ AUTO_PROFILER_LABEL(
+ "ExternalEngineStateMachine::OnRequestVideo:Resolved",
+ MEDIA_PLAYBACK);
+ MOZ_ASSERT(aVideo);
+ if (!mHasReceivedFirstDecodedVideoFrame) {
+ mHasReceivedFirstDecodedVideoFrame = true;
+ OnLoadedFirstFrame();
+ }
+ RunningEngineUpdate(MediaData::Type::VIDEO_DATA);
+ // Send image to PIP window.
+ if (mSecondaryVideoContainer.Ref()) {
+ mSecondaryVideoContainer.Ref()->SetCurrentFrame(
+ mInfo->mVideo.mDisplay, aVideo->mImage, TimeStamp::Now());
+ } else {
+ mVideoFrameContainer->SetCurrentFrame(
+ mInfo->mVideo.mDisplay, aVideo->mImage, TimeStamp::Now());
+ }
+ },
+ [this, self](const MediaResult& aError) {
+ mVideoDataRequest.Complete();
+ AUTO_PROFILER_LABEL(
+ "ExternalEngineStateMachine::OnRequestVideo:Rejected",
+ MEDIA_PLAYBACK);
+ LOG("OnRequestVideo ErrorName=%s Message=%s",
+ aError.ErrorName().get(), aError.Message().get());
+ switch (aError.Code()) {
+ case NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA:
+ WaitForData(MediaData::Type::VIDEO_DATA);
+ break;
+ case NS_ERROR_DOM_MEDIA_CANCELED:
+ OnRequestVideo();
+ break;
+ case NS_ERROR_DOM_MEDIA_END_OF_STREAM:
+ LOG("Reach to the end, no more video data");
+ EndOfStream(MediaData::Type::VIDEO_DATA);
+ break;
+ case NS_ERROR_DOM_MEDIA_REMOTE_DECODER_CRASHED_MF_CDM_ERR:
+ // We will handle the process crash in `NotifyErrorInternal()`
+ // so here just silently ignore this.
+ break;
+ default:
+ DecodeError(aError);
+ }
+ })
+ ->Track(mVideoDataRequest);
+}
+
+void ExternalEngineStateMachine::OnLoadedFirstFrame() {
+ AssertOnTaskQueue();
+ // We will wait until receive the first video frame.
+ if (mInfo->HasVideo() && !mHasReceivedFirstDecodedVideoFrame) {
+ LOGV("Hasn't received first decoded video frame");
+ return;
+ }
+ LOGV("OnLoadedFirstFrame");
+ MediaDecoderEventVisibility visibility =
+ mSentFirstFrameLoadedEvent ? MediaDecoderEventVisibility::Suppressed
+ : MediaDecoderEventVisibility::Observable;
+ mSentFirstFrameLoadedEvent = true;
+ mFirstFrameLoadedEvent.Notify(UniquePtr<MediaInfo>(new MediaInfo(Info())),
+ visibility);
+ mOnNextFrameStatus.Notify(MediaDecoderOwner::NEXT_FRAME_AVAILABLE);
+}
+
+void ExternalEngineStateMachine::OnLoadedData() {
+ AssertOnTaskQueue();
+ // In case the external engine doesn't send the first frame loaded event
+ // correctly.
+ if (!mSentFirstFrameLoadedEvent) {
+ OnLoadedFirstFrame();
+ }
+ mOnNextFrameStatus.Notify(MediaDecoderOwner::NEXT_FRAME_AVAILABLE);
+}
+
+void ExternalEngineStateMachine::OnWaiting() {
+ AssertOnTaskQueue();
+ mOnNextFrameStatus.Notify(
+ MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE_BUFFERING);
+}
+
+void ExternalEngineStateMachine::OnPlaying() {
+ AssertOnTaskQueue();
+ mOnNextFrameStatus.Notify(MediaDecoderOwner::NEXT_FRAME_AVAILABLE);
+}
+
+void ExternalEngineStateMachine::OnSeeked() {
+ AssertOnTaskQueue();
+ if (!mState.IsSeekingData()) {
+ LOG("Engine Seeking has been completed, ignore the event");
+ return;
+ }
+ MOZ_ASSERT(mState.IsSeekingData());
+
+ const auto currentTime = mEngine->GetCurrentPosition();
+ auto* state = mState.AsSeekingData();
+ LOG("OnEngineSeeked, target=%" PRId64 ", currentTime=%" PRId64,
+ state->GetTargetTime().ToMicroseconds(), currentTime.ToMicroseconds());
+ // It's possible to receive multiple seeked event if we seek the engine
+ // before the previous seeking finishes, so we would wait until the last
+ // seeking is finished.
+ if (currentTime >= state->GetTargetTime()) {
+ state->mWaitingEngineSeeked = false;
+ CheckIfSeekCompleted();
+ }
+}
+
+void ExternalEngineStateMachine::OnBufferingStarted() {
+ AssertOnTaskQueue();
+ mOnNextFrameStatus.Notify(
+ MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE_BUFFERING);
+ if (HasAudio()) {
+ WaitForData(MediaData::Type::AUDIO_DATA);
+ }
+ if (HasVideo()) {
+ WaitForData(MediaData::Type::VIDEO_DATA);
+ }
+}
+
+void ExternalEngineStateMachine::OnBufferingEnded() {
+ AssertOnTaskQueue();
+ mOnNextFrameStatus.Notify(MediaDecoderOwner::NEXT_FRAME_AVAILABLE);
+}
+
+void ExternalEngineStateMachine::OnEnded() {
+ AssertOnTaskQueue();
+ if (mSentPlaybackEndedEvent) {
+ return;
+ }
+ LOG("Playback is ended");
+ mOnNextFrameStatus.Notify(MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE);
+ mOnPlaybackEvent.Notify(MediaPlaybackEvent::PlaybackEnded);
+ mSentPlaybackEndedEvent = true;
+}
+
+void ExternalEngineStateMachine::OnTimeupdate() {
+ AssertOnTaskQueue();
+ if (IsSeeking()) {
+ return;
+ }
+ mCurrentPosition = mEngine->GetCurrentPosition();
+ if (mDuration.Ref().ref() < mCurrentPosition.Ref()) {
+ mDuration = Some(mCurrentPosition.Ref());
+ }
+}
+
+void ExternalEngineStateMachine::NotifyEventInternal(
+ ExternalEngineEvent aEvent) {
+ AssertOnTaskQueue();
+ AUTO_PROFILER_LABEL("ExternalEngineStateMachine::NotifyEventInternal",
+ MEDIA_PLAYBACK);
+ LOG("Receive event %s", ExternalEngineEventToStr(aEvent));
+ if (mState.IsShutdownEngine()) {
+ return;
+ }
+ switch (aEvent) {
+ case ExternalEngineEvent::LoadedMetaData:
+ // We read metadata by ourselves, ignore this if there is any.
+ MOZ_ASSERT(mInfo);
+ break;
+ case ExternalEngineEvent::LoadedFirstFrame:
+ OnLoadedFirstFrame();
+ break;
+ case ExternalEngineEvent::LoadedData:
+ OnLoadedData();
+ break;
+ case ExternalEngineEvent::Waiting:
+ OnWaiting();
+ break;
+ case ExternalEngineEvent::Playing:
+ OnPlaying();
+ break;
+ case ExternalEngineEvent::Seeked:
+ OnSeeked();
+ break;
+ case ExternalEngineEvent::BufferingStarted:
+ OnBufferingStarted();
+ break;
+ case ExternalEngineEvent::BufferingEnded:
+ OnBufferingEnded();
+ break;
+ case ExternalEngineEvent::Timeupdate:
+ OnTimeupdate();
+ break;
+ case ExternalEngineEvent::Ended:
+ OnEnded();
+ break;
+ case ExternalEngineEvent::RequestForAudio:
+ mHasEnoughAudio = false;
+ if (ShouldRunEngineUpdateForRequest()) {
+ RunningEngineUpdate(MediaData::Type::AUDIO_DATA);
+ }
+ break;
+ case ExternalEngineEvent::RequestForVideo:
+ mHasEnoughVideo = false;
+ if (ShouldRunEngineUpdateForRequest()) {
+ RunningEngineUpdate(MediaData::Type::VIDEO_DATA);
+ }
+ break;
+ case ExternalEngineEvent::AudioEnough:
+ mHasEnoughAudio = true;
+ break;
+ case ExternalEngineEvent::VideoEnough:
+ mHasEnoughVideo = true;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Undefined event!");
+ break;
+ }
+}
+
+bool ExternalEngineStateMachine::ShouldRunEngineUpdateForRequest() {
+ // Running engine update will request new data, which could be run on
+ // `RunningEngine` or `SeekingData` state. However, in `SeekingData` we should
+ // only request new data after finishing reader seek, otherwise the reader
+ // would start requesting data from a wrong position.
+ return mState.IsRunningEngine() ||
+ (mState.AsSeekingData() &&
+ !mState.AsSeekingData()->mWaitingReaderSeeked);
+}
+
+void ExternalEngineStateMachine::NotifyErrorInternal(
+ const MediaResult& aError) {
+ AssertOnTaskQueue();
+ LOG("Engine error: %s", aError.Description().get());
+ if (aError == NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR) {
+ // The external engine doesn't support the type, try to notify the decoder
+ // to use our own state machine again.
+ DecodeError(
+ MediaResult(NS_ERROR_DOM_MEDIA_EXTERNAL_ENGINE_NOT_SUPPORTED_ERR));
+ } else if (aError == NS_ERROR_DOM_MEDIA_REMOTE_DECODER_CRASHED_MF_CDM_ERR) {
+ RecoverFromCDMProcessCrashIfNeeded();
+ } else {
+ DecodeError(aError);
+ }
+}
+
+void ExternalEngineStateMachine::RecoverFromCDMProcessCrashIfNeeded() {
+ AssertOnTaskQueue();
+ if (mState.IsRecoverEngine()) {
+ return;
+ }
+ ProcessCrashMonitor::NotifyCrash();
+ if (!ProcessCrashMonitor::ShouldRecoverProcess()) {
+ LOG("CDM process has crashed too many times, abort recovery");
+ DecodeError(
+ MediaResult(NS_ERROR_DOM_MEDIA_EXTERNAL_ENGINE_NOT_SUPPORTED_ERR));
+ return;
+ }
+
+ LOG("CDM process crashed, recover the engine again (last time=%" PRId64 ")",
+ mCurrentPosition.Ref().ToMicroseconds());
+ ChangeStateTo(State::RecoverEngine);
+ if (HasVideo()) {
+ mVideoDataRequest.DisconnectIfExists();
+ mVideoWaitRequest.DisconnectIfExists();
+ }
+ if (HasAudio()) {
+ mAudioDataRequest.DisconnectIfExists();
+ mAudioWaitRequest.DisconnectIfExists();
+ }
+ // Ask the reader to shutdown current decoders which are no longer available
+ // due to the remote process crash.
+ mReader->ReleaseResources();
+ InitEngine();
+}
+
+media::TimeUnit ExternalEngineStateMachine::GetVideoThreshold() {
+ AssertOnTaskQueue();
+ if (auto* state = mState.AsSeekingData()) {
+ return state->GetTargetTime();
+ }
+ return mCurrentPosition.Ref();
+}
+
+void ExternalEngineStateMachine::UpdateSecondaryVideoContainer() {
+ AssertOnTaskQueue();
+ LOG("UpdateSecondaryVideoContainer=%p", mSecondaryVideoContainer.Ref().get());
+ mOnSecondaryVideoContainerInstalled.Notify(mSecondaryVideoContainer.Ref());
+}
+
+RefPtr<SetCDMPromise> ExternalEngineStateMachine::SetCDMProxy(
+ CDMProxy* aProxy) {
+ if (mState.IsShutdownEngine()) {
+ return SetCDMPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+
+ if (mState.IsInitEngine() && mState.AsInitEngine()->mInitPromise) {
+ LOG("SetCDMProxy is called before init");
+ mState.AsInitEngine()->mInitPromise->Then(
+ OwnerThread(), __func__,
+ [self = RefPtr{this}, proxy = RefPtr{aProxy},
+ this](const GenericNonExclusivePromise::ResolveOrRejectValue& aVal) {
+ SetCDMProxy(proxy)
+ ->Then(OwnerThread(), __func__,
+ [self = RefPtr{this},
+ this](const SetCDMPromise::ResolveOrRejectValue& aVal) {
+ mSetCDMProxyRequest.Complete();
+ if (aVal.IsResolve()) {
+ mSetCDMProxyPromise.Resolve(true, __func__);
+ } else {
+ mSetCDMProxyPromise.Reject(NS_ERROR_DOM_MEDIA_CDM_ERR,
+ __func__);
+ }
+ })
+ ->Track(mSetCDMProxyRequest);
+ });
+ return mSetCDMProxyPromise.Ensure(__func__);
+ }
+
+ // TODO : set CDM proxy again if we recreate the media engine after crash.
+ LOG("SetCDMProxy=%p", aProxy);
+ MOZ_DIAGNOSTIC_ASSERT(mEngine);
+ if (!mEngine->SetCDMProxy(aProxy)) {
+ LOG("Failed to set CDM proxy on the engine");
+ return SetCDMPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_CDM_ERR, __func__);
+ }
+ return MediaDecoderStateMachineBase::SetCDMProxy(aProxy);
+}
+
+bool ExternalEngineStateMachine::IsCDMProxySupported(CDMProxy* aProxy) {
+#ifdef MOZ_WMF_CDM
+ MOZ_ASSERT(aProxy);
+ // 1=enabled encrypted and clear, 2=enabled encrytped
+ if (StaticPrefs::media_wmf_media_engine_enabled() != 1 &&
+ StaticPrefs::media_wmf_media_engine_enabled() != 2) {
+ return false;
+ }
+
+ // The CDM needs to be hosted in the same process of the external engine, and
+ // only WMFCDM meets this requirement.
+ return aProxy->AsWMFCDMProxy();
+#else
+ return false;
+#endif
+}
+
+#undef FMT
+#undef LOG
+#undef LOGV
+#undef LOGW
+#undef LOGE
+
+} // namespace mozilla
diff --git a/dom/media/ExternalEngineStateMachine.h b/dom/media/ExternalEngineStateMachine.h
new file mode 100644
index 0000000000..31ea9e92cd
--- /dev/null
+++ b/dom/media/ExternalEngineStateMachine.h
@@ -0,0 +1,328 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_EXTERNALENGINESTATEMACHINE_H_
+#define DOM_MEDIA_EXTERNALENGINESTATEMACHINE_H_
+
+#include "MediaDecoderStateMachineBase.h"
+#include "SeekJob.h"
+#include "mozilla/Variant.h"
+
+namespace mozilla {
+
+/**
+ * ExternalPlaybackEngine represents a media engine which is responsible for
+ * decoding and playback, which are not controlled by Gecko.
+ */
+class ExternalPlaybackEngine;
+
+enum class ExternalEngineEvent {
+ LoadedMetaData,
+ LoadedFirstFrame,
+ LoadedData,
+ Waiting,
+ Playing,
+ Seeked,
+ BufferingStarted,
+ BufferingEnded,
+ Timeupdate,
+ Ended,
+ RequestForAudio,
+ RequestForVideo,
+ AudioEnough,
+ VideoEnough,
+};
+const char* ExternalEngineEventToStr(ExternalEngineEvent aEvent);
+
+/**
+ * When using ExternalEngineStateMachine, that means we use an external engine
+ * to control decoding and playback (including A/V sync). Eg. Media Foundation
+ * Media Engine on Windows.
+ *
+ * The external engine does most of playback works, and uses ExternalEngineEvent
+ * to tell us its internal state. Therefore, this state machine is responsible
+ * to address those events from the engine and coordinate the format reader in
+ * order to provide data to the engine correctly.
+ */
+DDLoggedTypeDeclName(ExternalEngineStateMachine);
+
+class ExternalEngineStateMachine final
+ : public MediaDecoderStateMachineBase,
+ public DecoderDoctorLifeLogger<ExternalEngineStateMachine> {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ExternalEngineStateMachine, override)
+
+ ExternalEngineStateMachine(MediaDecoder* aDecoder,
+ MediaFormatReader* aReader);
+
+ RefPtr<GenericPromise> InvokeSetSink(
+ const RefPtr<AudioDeviceInfo>& aSink) override;
+
+ // The media sample would be managed by the external engine so we won't store
+ // any samples in our side.
+ size_t SizeOfVideoQueue() const override { return 0; }
+ size_t SizeOfAudioQueue() const override { return 0; }
+
+ // Not supported.
+ void SetVideoDecodeMode(VideoDecodeMode aMode) override {}
+ void InvokeSuspendMediaSink() override {}
+ void InvokeResumeMediaSink() override {}
+ RefPtr<GenericPromise> RequestDebugInfo(
+ dom::MediaDecoderStateMachineDebugInfo& aInfo) override {
+ // This debug info doesn't fit in this scenario because most decoding
+ // details are only visible inside the external engine.
+ return GenericPromise::CreateAndResolve(true, __func__);
+ }
+
+ void NotifyEvent(ExternalEngineEvent aEvent) {
+ // On the engine manager thread.
+ Unused << OwnerThread()->Dispatch(NS_NewRunnableFunction(
+ "ExternalEngineStateMachine::NotifyEvent",
+ [self = RefPtr{this}, aEvent] { self->NotifyEventInternal(aEvent); }));
+ }
+ void NotifyError(const MediaResult& aError) {
+ // On the engine manager thread.
+ Unused << OwnerThread()->Dispatch(NS_NewRunnableFunction(
+ "ExternalEngineStateMachine::NotifyError",
+ [self = RefPtr{this}, aError] { self->NotifyErrorInternal(aError); }));
+ }
+
+ const char* GetStateStr() const;
+
+ RefPtr<SetCDMPromise> SetCDMProxy(CDMProxy* aProxy) override;
+
+ bool IsCDMProxySupported(CDMProxy* aProxy) override;
+
+ private:
+ ~ExternalEngineStateMachine() = default;
+
+ void AssertOnTaskQueue() const { MOZ_ASSERT(OnTaskQueue()); }
+
+ // A light-weight state object that helps to store some variables which would
+ // only be used in a certain state. Also be able to do the cleaning for the
+ // state transition. Only modify on the task queue.
+ struct StateObject final {
+ enum class State {
+ InitEngine,
+ ReadingMetadata,
+ RunningEngine,
+ SeekingData,
+ ShutdownEngine,
+ RecoverEngine,
+ };
+ struct InitEngine {
+ InitEngine() = default;
+ ~InitEngine() { mEngineInitRequest.DisconnectIfExists(); }
+ MozPromiseRequestHolder<GenericNonExclusivePromise> mEngineInitRequest;
+ RefPtr<GenericNonExclusivePromise> mInitPromise;
+ };
+ struct ReadingMetadata {
+ ReadingMetadata() = default;
+ ~ReadingMetadata() { mMetadataRequest.DisconnectIfExists(); }
+ MozPromiseRequestHolder<MediaFormatReader::MetadataPromise>
+ mMetadataRequest;
+ };
+ struct RunningEngine {};
+ struct SeekingData {
+ SeekingData() = default;
+ SeekingData(SeekingData&&) = default;
+ SeekingData(const SeekingData&) = delete;
+ SeekingData& operator=(const SeekingData&) = delete;
+ ~SeekingData() {
+ mSeekJob.RejectIfExists(__func__);
+ mSeekRequest.DisconnectIfExists();
+ }
+ void SetTarget(const SeekTarget& aTarget) {
+ // If there is any promise for previous seeking, reject it first.
+ mSeekJob.RejectIfExists(__func__);
+ mSeekRequest.DisconnectIfExists();
+ // Then create a new seek job.
+ mSeekJob = SeekJob();
+ mSeekJob.mTarget = Some(aTarget);
+ }
+ void Resolve(const char* aCallSite) {
+ MOZ_ASSERT(mSeekJob.Exists());
+ mSeekJob.Resolve(aCallSite);
+ mSeekJob = SeekJob();
+ }
+ void RejectIfExists(const char* aCallSite) {
+ mSeekJob.RejectIfExists(aCallSite);
+ }
+ bool IsSeeking() const { return mSeekRequest.Exists(); }
+ media::TimeUnit GetTargetTime() const {
+ return mSeekJob.mTarget ? mSeekJob.mTarget->GetTime()
+ : media::TimeUnit::Invalid();
+ }
+ // Set it to true when starting seeking, and would be set to false after
+ // receiving engine's `seeked` event. Used on thhe task queue only.
+ bool mWaitingEngineSeeked = false;
+ bool mWaitingReaderSeeked = false;
+ MozPromiseRequestHolder<MediaFormatReader::SeekPromise> mSeekRequest;
+ SeekJob mSeekJob;
+ };
+ struct ShutdownEngine {
+ RefPtr<ShutdownPromise> mShutdown;
+ };
+ // This state is used to recover the media engine after the MF CDM process
+ // crashes.
+ struct RecoverEngine : public InitEngine {};
+
+ StateObject() : mData(InitEngine()), mName(State::InitEngine){};
+ explicit StateObject(ReadingMetadata&& aArg)
+ : mData(std::move(aArg)), mName(State::ReadingMetadata){};
+ explicit StateObject(RunningEngine&& aArg)
+ : mData(std::move(aArg)), mName(State::RunningEngine){};
+ explicit StateObject(SeekingData&& aArg)
+ : mData(std::move(aArg)), mName(State::SeekingData){};
+ explicit StateObject(ShutdownEngine&& aArg)
+ : mData(std::move(aArg)), mName(State::ShutdownEngine){};
+ explicit StateObject(RecoverEngine&& aArg)
+ : mData(std::move(aArg)), mName(State::RecoverEngine){};
+
+ bool IsInitEngine() const { return mData.is<InitEngine>(); }
+ bool IsReadingMetadata() const { return mData.is<ReadingMetadata>(); }
+ bool IsRunningEngine() const { return mData.is<RunningEngine>(); }
+ bool IsSeekingData() const { return mData.is<SeekingData>(); }
+ bool IsShutdownEngine() const { return mData.is<ShutdownEngine>(); }
+ bool IsRecoverEngine() const { return mData.is<RecoverEngine>(); }
+
+ InitEngine* AsInitEngine() {
+ if (IsInitEngine()) {
+ return &mData.as<InitEngine>();
+ }
+ if (IsRecoverEngine()) {
+ return &mData.as<RecoverEngine>();
+ }
+ return nullptr;
+ }
+ ReadingMetadata* AsReadingMetadata() {
+ return IsReadingMetadata() ? &mData.as<ReadingMetadata>() : nullptr;
+ }
+ SeekingData* AsSeekingData() {
+ return IsSeekingData() ? &mData.as<SeekingData>() : nullptr;
+ }
+ ShutdownEngine* AsShutdownEngine() {
+ return IsShutdownEngine() ? &mData.as<ShutdownEngine>() : nullptr;
+ }
+
+ Variant<InitEngine, ReadingMetadata, RunningEngine, SeekingData,
+ ShutdownEngine, RecoverEngine>
+ mData;
+ State mName;
+ } mState;
+ using State = StateObject::State;
+
+ void NotifyEventInternal(ExternalEngineEvent aEvent);
+ void NotifyErrorInternal(const MediaResult& aError);
+
+ RefPtr<ShutdownPromise> Shutdown() override;
+
+ void SetPlaybackRate(double aPlaybackRate) override;
+ void BufferedRangeUpdated() override;
+ void VolumeChanged() override;
+ void PreservesPitchChanged() override;
+ void PlayStateChanged() override;
+ void LoopingChanged() override;
+
+ // Not supported.
+ void SetIsLiveStream(bool aIsLiveStream) override {}
+ void SetCanPlayThrough(bool aCanPlayThrough) override {}
+ void SetFragmentEndTime(const media::TimeUnit& aFragmentEndTime) override {}
+
+ void InitEngine();
+ void OnEngineInitSuccess();
+ void OnEngineInitFailure();
+
+ void ReadMetadata();
+ void OnMetadataRead(MetadataHolder&& aMetadata);
+ void OnMetadataNotRead(const MediaResult& aError);
+ bool IsFormatSupportedByExternalEngine(const MediaInfo& aInfo);
+
+ // Functions for handling external engine event.
+ void OnLoadedFirstFrame();
+ void OnLoadedData();
+ void OnWaiting();
+ void OnPlaying();
+ void OnSeeked();
+ void OnBufferingStarted();
+ void OnBufferingEnded();
+ void OnTimeupdate();
+ void OnEnded();
+ void OnRequestAudio();
+ void OnRequestVideo();
+
+ void ResetDecode();
+
+ void EndOfStream(MediaData::Type aType);
+ void WaitForData(MediaData::Type aType);
+
+ void StartRunningEngine();
+ void RunningEngineUpdate(MediaData::Type aType);
+
+ void ChangeStateTo(State aNextState);
+ static const char* StateToStr(State aState);
+
+ RefPtr<MediaDecoder::SeekPromise> Seek(const SeekTarget& aTarget) override;
+ void SeekReader();
+ void OnSeekResolved(const media::TimeUnit& aUnit);
+ void OnSeekRejected(const SeekRejectValue& aReject);
+ bool IsSeeking();
+ void CheckIfSeekCompleted();
+
+ void MaybeFinishWaitForData();
+
+ void SetBlankVideoToVideoContainer();
+
+ media::TimeUnit GetVideoThreshold();
+
+ bool ShouldRunEngineUpdateForRequest();
+
+ void UpdateSecondaryVideoContainer() override;
+
+ void RecoverFromCDMProcessCrashIfNeeded();
+
+ UniquePtr<ExternalPlaybackEngine> mEngine;
+
+ bool mHasEnoughAudio = false;
+ bool mHasEnoughVideo = false;
+ bool mSentPlaybackEndedEvent = false;
+ bool mHasReceivedFirstDecodedVideoFrame = false;
+
+ // Only used if setting CDM happens before the engine finishes initialization.
+ MozPromiseHolder<SetCDMPromise> mSetCDMProxyPromise;
+ MozPromiseRequestHolder<SetCDMPromise> mSetCDMProxyRequest;
+};
+
+class ExternalPlaybackEngine {
+ public:
+ explicit ExternalPlaybackEngine(ExternalEngineStateMachine* aOwner)
+ : mOwner(aOwner) {}
+
+ virtual ~ExternalPlaybackEngine() = default;
+
+ // Init the engine and specify the preload request.
+ virtual RefPtr<GenericNonExclusivePromise> Init(bool aShouldPreload) = 0;
+ virtual void Shutdown() = 0;
+ virtual uint64_t Id() const = 0;
+
+ // Following methods should only be called after successfully initialize the
+ // external engine.
+ virtual void Play() = 0;
+ virtual void Pause() = 0;
+ virtual void Seek(const media::TimeUnit& aTargetTime) = 0;
+ virtual void SetPlaybackRate(double aPlaybackRate) = 0;
+ virtual void SetVolume(double aVolume) = 0;
+ virtual void SetLooping(bool aLooping) = 0;
+ virtual void SetPreservesPitch(bool aPreservesPitch) = 0;
+ virtual media::TimeUnit GetCurrentPosition() = 0;
+ virtual void NotifyEndOfStream(TrackInfo::TrackType aType) = 0;
+ virtual void SetMediaInfo(const MediaInfo& aInfo) = 0;
+ virtual bool SetCDMProxy(CDMProxy* aProxy) = 0;
+
+ ExternalEngineStateMachine* const MOZ_NON_OWNING_REF mOwner;
+};
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_EXTERNALENGINESTATEMACHINE_H_
diff --git a/dom/media/FileBlockCache.cpp b/dom/media/FileBlockCache.cpp
new file mode 100644
index 0000000000..3989c05833
--- /dev/null
+++ b/dom/media/FileBlockCache.cpp
@@ -0,0 +1,506 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FileBlockCache.h"
+#include "MediaCache.h"
+#include "VideoUtils.h"
+#include "prio.h"
+#include <algorithm>
+#include "nsAnonymousTemporaryFile.h"
+#include "nsIThreadManager.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/ScopeExit.h"
+#include "nsXULAppAPI.h"
+
+namespace mozilla {
+
+#undef LOG
+LazyLogModule gFileBlockCacheLog("FileBlockCache");
+#define LOG(x, ...) \
+ MOZ_LOG(gFileBlockCacheLog, LogLevel::Debug, ("%p " x, this, ##__VA_ARGS__))
+
+static void CloseFD(PRFileDesc* aFD) {
+ PRStatus prrc;
+ prrc = PR_Close(aFD);
+ if (prrc != PR_SUCCESS) {
+ NS_WARNING("PR_Close() failed.");
+ }
+}
+
+void FileBlockCache::SetCacheFile(PRFileDesc* aFD) {
+ LOG("SetCacheFile aFD=%p", aFD);
+ if (!aFD) {
+ // Failed to get a temporary file. Shutdown.
+ Close();
+ return;
+ }
+ {
+ MutexAutoLock lock(mFileMutex);
+ mFD = aFD;
+ }
+ {
+ MutexAutoLock lock(mDataMutex);
+ LOG("SetFileCache mBackgroundET=%p, mIsWriteScheduled %d",
+ mBackgroundET.get(), mIsWriteScheduled);
+ if (mBackgroundET) {
+ // Still open, complete the initialization.
+ mInitialized = true;
+ if (mIsWriteScheduled) {
+ // A write was scheduled while waiting for FD. We need to run/dispatch a
+ // task to service the request.
+ nsCOMPtr<nsIRunnable> event = mozilla::NewRunnableMethod(
+ "FileBlockCache::SetCacheFile -> PerformBlockIOs", this,
+ &FileBlockCache::PerformBlockIOs);
+ mBackgroundET->Dispatch(event.forget(), NS_DISPATCH_EVENT_MAY_BLOCK);
+ }
+ return;
+ }
+ }
+ // We've been closed while waiting for the file descriptor.
+ // Close the file descriptor we've just received, if still there.
+ MutexAutoLock lock(mFileMutex);
+ if (mFD) {
+ CloseFD(mFD);
+ mFD = nullptr;
+ }
+}
+
+nsresult FileBlockCache::Init() {
+ LOG("Init()");
+ MutexAutoLock mon(mDataMutex);
+ MOZ_ASSERT(!mBackgroundET);
+ nsresult rv = NS_CreateBackgroundTaskQueue("FileBlockCache",
+ getter_AddRefs(mBackgroundET));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (XRE_IsParentProcess()) {
+ RefPtr<FileBlockCache> self = this;
+ rv = mBackgroundET->Dispatch(
+ NS_NewRunnableFunction("FileBlockCache::Init",
+ [self] {
+ PRFileDesc* fd = nullptr;
+ nsresult rv =
+ NS_OpenAnonymousTemporaryFile(&fd);
+ if (NS_SUCCEEDED(rv)) {
+ self->SetCacheFile(fd);
+ } else {
+ self->Close();
+ }
+ }),
+ NS_DISPATCH_EVENT_MAY_BLOCK);
+ } else {
+ // We must request a temporary file descriptor from the parent process.
+ RefPtr<FileBlockCache> self = this;
+ rv = dom::ContentChild::GetSingleton()->AsyncOpenAnonymousTemporaryFile(
+ [self](PRFileDesc* aFD) { self->SetCacheFile(aFD); });
+ }
+
+ if (NS_FAILED(rv)) {
+ Close();
+ }
+
+ return rv;
+}
+
+void FileBlockCache::Flush() {
+ LOG("Flush()");
+ MutexAutoLock mon(mDataMutex);
+ MOZ_ASSERT(mBackgroundET);
+
+ // Dispatch a task so we won't clear the arrays while PerformBlockIOs() is
+ // dropping the data lock and cause InvalidArrayIndex.
+ RefPtr<FileBlockCache> self = this;
+ mBackgroundET->Dispatch(
+ NS_NewRunnableFunction("FileBlockCache::Flush", [self]() {
+ MutexAutoLock mon(self->mDataMutex);
+ // Just discard pending changes, assume MediaCache won't read from
+ // blocks it hasn't written to.
+ self->mChangeIndexList.clear();
+ self->mBlockChanges.Clear();
+ }));
+}
+
+size_t FileBlockCache::GetMaxBlocks(size_t aCacheSizeInKB) const {
+ // We look up the cache size every time. This means dynamic changes
+ // to the pref are applied.
+ // Ensure we can divide BLOCK_SIZE by 1024.
+ static_assert(MediaCacheStream::BLOCK_SIZE % 1024 == 0,
+ "BLOCK_SIZE should be a multiple of 1024");
+ // Ensure BLOCK_SIZE/1024 is at least 2.
+ static_assert(MediaCacheStream::BLOCK_SIZE / 1024 >= 2,
+ "BLOCK_SIZE / 1024 should be at least 2");
+ // Ensure we can convert BLOCK_SIZE/1024 to a uint32_t without truncation.
+ static_assert(MediaCacheStream::BLOCK_SIZE / 1024 <= int64_t(UINT32_MAX),
+ "BLOCK_SIZE / 1024 should be at most UINT32_MAX");
+ // Since BLOCK_SIZE is a strict multiple of 1024,
+ // aCacheSizeInKB * 1024 / BLOCK_SIZE == aCacheSizeInKB / (BLOCK_SIZE /
+ // 1024), but the latter formula avoids a potential overflow from `* 1024`.
+ // And because BLOCK_SIZE/1024 is at least 2, the maximum cache size
+ // INT32_MAX*2 will give a maxBlocks that can fit in an int32_t.
+ constexpr size_t blockSizeKb = size_t(MediaCacheStream::BLOCK_SIZE / 1024);
+ const size_t maxBlocks = aCacheSizeInKB / blockSizeKb;
+ return std::max(maxBlocks, size_t(1));
+}
+
+FileBlockCache::FileBlockCache()
+ : mFileMutex("MediaCache.Writer.IO.Mutex"),
+ mFD(nullptr),
+ mFDCurrentPos(0),
+ mDataMutex("MediaCache.Writer.Data.Mutex"),
+ mIsWriteScheduled(false),
+ mIsReading(false) {}
+
+FileBlockCache::~FileBlockCache() { Close(); }
+
+void FileBlockCache::Close() {
+ LOG("Close()");
+
+ nsCOMPtr<nsISerialEventTarget> thread;
+ {
+ MutexAutoLock mon(mDataMutex);
+ if (!mBackgroundET) {
+ return;
+ }
+ thread.swap(mBackgroundET);
+ }
+
+ PRFileDesc* fd;
+ {
+ MutexAutoLock lock(mFileMutex);
+ fd = mFD;
+ mFD = nullptr;
+ }
+
+ // Let the thread close the FD, and then trigger its own shutdown.
+ // Note that mBackgroundET is now empty, so no other task will be posted
+ // there. Also mBackgroundET and mFD are empty and therefore can be reused
+ // immediately.
+ nsresult rv = thread->Dispatch(NS_NewRunnableFunction("FileBlockCache::Close",
+ [thread, fd] {
+ if (fd) {
+ CloseFD(fd);
+ }
+ // No need to shutdown
+ // background task
+ // queues.
+ }),
+ NS_DISPATCH_EVENT_MAY_BLOCK);
+ NS_ENSURE_SUCCESS_VOID(rv);
+}
+
+template <typename Container, typename Value>
+bool ContainerContains(const Container& aContainer, const Value& value) {
+ return std::find(aContainer.begin(), aContainer.end(), value) !=
+ aContainer.end();
+}
+
+nsresult FileBlockCache::WriteBlock(uint32_t aBlockIndex,
+ Span<const uint8_t> aData1,
+ Span<const uint8_t> aData2) {
+ MutexAutoLock mon(mDataMutex);
+
+ if (!mBackgroundET) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Check if we've already got a pending write scheduled for this block.
+ mBlockChanges.EnsureLengthAtLeast(aBlockIndex + 1);
+ bool blockAlreadyHadPendingChange = mBlockChanges[aBlockIndex] != nullptr;
+ mBlockChanges[aBlockIndex] = new BlockChange(aData1, aData2);
+
+ if (!blockAlreadyHadPendingChange ||
+ !ContainerContains(mChangeIndexList, aBlockIndex)) {
+ // We either didn't already have a pending change for this block, or we
+ // did but we didn't have an entry for it in mChangeIndexList (we're in the
+ // process of writing it and have removed the block's index out of
+ // mChangeIndexList in Run() but not finished writing the block to file
+ // yet). Add the blocks index to the end of mChangeIndexList to ensure the
+ // block is written as as soon as possible.
+ mChangeIndexList.push_back(aBlockIndex);
+ }
+ NS_ASSERTION(ContainerContains(mChangeIndexList, aBlockIndex),
+ "Must have entry for new block");
+
+ EnsureWriteScheduled();
+
+ return NS_OK;
+}
+
+void FileBlockCache::EnsureWriteScheduled() {
+ mDataMutex.AssertCurrentThreadOwns();
+ MOZ_ASSERT(mBackgroundET);
+
+ if (mIsWriteScheduled || mIsReading) {
+ return;
+ }
+ mIsWriteScheduled = true;
+ if (!mInitialized) {
+ // We're still waiting on a file descriptor. When it arrives,
+ // the write will be scheduled.
+ return;
+ }
+ nsCOMPtr<nsIRunnable> event = mozilla::NewRunnableMethod(
+ "FileBlockCache::EnsureWriteScheduled -> PerformBlockIOs", this,
+ &FileBlockCache::PerformBlockIOs);
+ mBackgroundET->Dispatch(event.forget(), NS_DISPATCH_EVENT_MAY_BLOCK);
+}
+
+nsresult FileBlockCache::Seek(int64_t aOffset) {
+ mFileMutex.AssertCurrentThreadOwns();
+
+ if (mFDCurrentPos != aOffset) {
+ MOZ_ASSERT(mFD);
+ int64_t result = PR_Seek64(mFD, aOffset, PR_SEEK_SET);
+ if (result != aOffset) {
+ NS_WARNING("Failed to seek media cache file");
+ return NS_ERROR_FAILURE;
+ }
+ mFDCurrentPos = result;
+ }
+ return NS_OK;
+}
+
+nsresult FileBlockCache::ReadFromFile(int64_t aOffset, uint8_t* aDest,
+ int32_t aBytesToRead,
+ int32_t& aBytesRead) {
+ LOG("ReadFromFile(offset=%" PRIu64 ", len=%u)", aOffset, aBytesToRead);
+ mFileMutex.AssertCurrentThreadOwns();
+ MOZ_ASSERT(mFD);
+
+ nsresult res = Seek(aOffset);
+ if (NS_FAILED(res)) return res;
+
+ aBytesRead = PR_Read(mFD, aDest, aBytesToRead);
+ if (aBytesRead <= 0) return NS_ERROR_FAILURE;
+ mFDCurrentPos += aBytesRead;
+
+ return NS_OK;
+}
+
+nsresult FileBlockCache::WriteBlockToFile(int32_t aBlockIndex,
+ const uint8_t* aBlockData) {
+ LOG("WriteBlockToFile(index=%u)", aBlockIndex);
+
+ mFileMutex.AssertCurrentThreadOwns();
+ MOZ_ASSERT(mFD);
+
+ nsresult rv = Seek(BlockIndexToOffset(aBlockIndex));
+ if (NS_FAILED(rv)) return rv;
+
+ int32_t amount = PR_Write(mFD, aBlockData, BLOCK_SIZE);
+ if (amount < BLOCK_SIZE) {
+ NS_WARNING("Failed to write media cache block!");
+ return NS_ERROR_FAILURE;
+ }
+ mFDCurrentPos += BLOCK_SIZE;
+
+ return NS_OK;
+}
+
+nsresult FileBlockCache::MoveBlockInFile(int32_t aSourceBlockIndex,
+ int32_t aDestBlockIndex) {
+ LOG("MoveBlockInFile(src=%u, dest=%u)", aSourceBlockIndex, aDestBlockIndex);
+
+ mFileMutex.AssertCurrentThreadOwns();
+
+ uint8_t buf[BLOCK_SIZE];
+ int32_t bytesRead = 0;
+ if (NS_FAILED(ReadFromFile(BlockIndexToOffset(aSourceBlockIndex), buf,
+ BLOCK_SIZE, bytesRead))) {
+ return NS_ERROR_FAILURE;
+ }
+ return WriteBlockToFile(aDestBlockIndex, buf);
+}
+
+void FileBlockCache::PerformBlockIOs() {
+ MutexAutoLock mon(mDataMutex);
+ MOZ_ASSERT(mBackgroundET->IsOnCurrentThread());
+ NS_ASSERTION(mIsWriteScheduled, "Should report write running or scheduled.");
+
+ LOG("Run() mFD=%p mBackgroundET=%p", mFD, mBackgroundET.get());
+
+ while (!mChangeIndexList.empty()) {
+ if (!mBackgroundET) {
+ // We've been closed, abort, discarding unwritten changes.
+ mIsWriteScheduled = false;
+ return;
+ }
+
+ if (mIsReading) {
+ // We're trying to read; postpone all writes. (Reader will resume writes.)
+ mIsWriteScheduled = false;
+ return;
+ }
+
+ // Process each pending change. We pop the index out of the change
+ // list, but leave the BlockChange in mBlockChanges until the change
+ // is written to file. This is so that any read which happens while
+ // we drop mDataMutex to write will refer to the data's source in
+ // memory, rather than the not-yet up to date data written to file.
+ // This also ensures we will insert a new index into mChangeIndexList
+ // when this happens.
+
+ // Hold a reference to the change, in case another change
+ // overwrites the mBlockChanges entry for this block while we drop
+ // mDataMutex to take mFileMutex.
+ int32_t blockIndex = mChangeIndexList.front();
+ RefPtr<BlockChange> change = mBlockChanges[blockIndex];
+ MOZ_ASSERT(change,
+ "Change index list should only contain entries for blocks "
+ "with changes");
+ {
+ MutexAutoUnlock unlock(mDataMutex);
+ MutexAutoLock lock(mFileMutex);
+ if (!mFD) {
+ // We may be here if mFD has been reset because we're closing, so we
+ // don't care anymore about writes.
+ return;
+ }
+ if (change->IsWrite()) {
+ WriteBlockToFile(blockIndex, change->mData.get());
+ } else if (change->IsMove()) {
+ MoveBlockInFile(change->mSourceBlockIndex, blockIndex);
+ }
+ }
+ mChangeIndexList.pop_front(); // MonitorAutoUnlock above
+ // If a new change has not been made to the block while we dropped
+ // mDataMutex, clear reference to the old change. Otherwise, the old
+ // reference has been cleared already.
+ if (mBlockChanges[blockIndex] == change) { // MonitorAutoUnlock above
+ mBlockChanges[blockIndex] = nullptr; // MonitorAutoUnlock above
+ }
+ }
+
+ mIsWriteScheduled = false;
+}
+
+nsresult FileBlockCache::Read(int64_t aOffset, uint8_t* aData, int32_t aLength,
+ int32_t* aBytes) {
+ MutexAutoLock mon(mDataMutex);
+
+ if (!mBackgroundET || (aOffset / BLOCK_SIZE) > INT32_MAX) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mIsReading = true;
+ auto exitRead = MakeScopeExit([&] {
+ mDataMutex.AssertCurrentThreadOwns();
+ mIsReading = false;
+ if (!mChangeIndexList.empty()) {
+ // mReading has stopped or prevented pending writes, resume them.
+ EnsureWriteScheduled();
+ }
+ });
+
+ int32_t bytesToRead = aLength;
+ int64_t offset = aOffset;
+ uint8_t* dst = aData;
+ while (bytesToRead > 0) {
+ int32_t blockIndex = static_cast<int32_t>(offset / BLOCK_SIZE);
+ int32_t start = offset % BLOCK_SIZE;
+ int32_t amount = std::min(BLOCK_SIZE - start, bytesToRead);
+
+ // If the block is not yet written to file, we can just read from
+ // the memory buffer, otherwise we need to read from file.
+ int32_t bytesRead = 0;
+ MOZ_ASSERT(!mBlockChanges.IsEmpty());
+ MOZ_ASSERT(blockIndex >= 0 &&
+ static_cast<uint32_t>(blockIndex) < mBlockChanges.Length());
+ RefPtr<BlockChange> change = mBlockChanges.SafeElementAt(blockIndex);
+ if (change && change->IsWrite()) {
+ // Block isn't yet written to file. Read from memory buffer.
+ const uint8_t* blockData = change->mData.get();
+ memcpy(dst, blockData + start, amount);
+ bytesRead = amount;
+ } else {
+ if (change && change->IsMove()) {
+ // The target block is the destination of a not-yet-completed move
+ // action, so read from the move's source block from file. Note we
+ // *don't* follow a chain of moves here, as a move's source index
+ // is resolved when MoveBlock() is called, and the move's source's
+ // block could be have itself been subject to a move (or write)
+ // which happened *after* this move was recorded.
+ blockIndex = change->mSourceBlockIndex;
+ }
+ // Block has been written to file, either as the source block of a move,
+ // or as a stable (all changes made) block. Read the data directly
+ // from file.
+ nsresult res;
+ {
+ MutexAutoUnlock unlock(mDataMutex);
+ MutexAutoLock lock(mFileMutex);
+ if (!mFD) {
+ // Not initialized yet, or closed.
+ return NS_ERROR_FAILURE;
+ }
+ res = ReadFromFile(BlockIndexToOffset(blockIndex) + start, dst, amount,
+ bytesRead);
+ }
+ NS_ENSURE_SUCCESS(res, res);
+ }
+ dst += bytesRead;
+ offset += bytesRead;
+ bytesToRead -= bytesRead;
+ }
+ *aBytes = aLength - bytesToRead;
+ return NS_OK;
+}
+
+nsresult FileBlockCache::MoveBlock(int32_t aSourceBlockIndex,
+ int32_t aDestBlockIndex) {
+ MutexAutoLock mon(mDataMutex);
+
+ if (!mBackgroundET) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mBlockChanges.EnsureLengthAtLeast(
+ std::max(aSourceBlockIndex, aDestBlockIndex) + 1);
+
+ // The source block's contents may be the destination of another pending
+ // move, which in turn can be the destination of another pending move,
+ // etc. Resolve the final source block, so that if one of the blocks in
+ // the chain of moves is overwritten, we don't lose the reference to the
+ // contents of the destination block.
+ int32_t sourceIndex = aSourceBlockIndex;
+ BlockChange* sourceBlock = nullptr;
+ while ((sourceBlock = mBlockChanges[sourceIndex]) && sourceBlock->IsMove()) {
+ sourceIndex = sourceBlock->mSourceBlockIndex;
+ }
+
+ if (mBlockChanges[aDestBlockIndex] == nullptr ||
+ !ContainerContains(mChangeIndexList, aDestBlockIndex)) {
+ // Only add another entry to the change index list if we don't already
+ // have one for this block. We won't have an entry when either there's
+ // no pending change for this block, or if there is a pending change for
+ // this block and we're in the process of writing it (we've popped the
+ // block's index out of mChangeIndexList in Run() but not finished writing
+ // the block to file yet.
+ mChangeIndexList.push_back(aDestBlockIndex);
+ }
+
+ // If the source block hasn't yet been written to file then the dest block
+ // simply contains that same write. Resolve this as a write instead.
+ if (sourceBlock && sourceBlock->IsWrite()) {
+ mBlockChanges[aDestBlockIndex] = new BlockChange(sourceBlock->mData.get());
+ } else {
+ mBlockChanges[aDestBlockIndex] = new BlockChange(sourceIndex);
+ }
+
+ EnsureWriteScheduled();
+
+ NS_ASSERTION(ContainerContains(mChangeIndexList, aDestBlockIndex),
+ "Should have scheduled block for change");
+
+ return NS_OK;
+}
+
+} // End namespace mozilla.
+
+// avoid redefined macro in unified build
+#undef LOG
diff --git a/dom/media/FileBlockCache.h b/dom/media/FileBlockCache.h
new file mode 100644
index 0000000000..3a1daf0794
--- /dev/null
+++ b/dom/media/FileBlockCache.h
@@ -0,0 +1,193 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef FILE_BLOCK_CACHE_H_
+#define FILE_BLOCK_CACHE_H_
+
+#include "mozilla/Attributes.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/AbstractThread.h"
+#include "nsTArray.h"
+#include "MediaBlockCacheBase.h"
+#include "nsDeque.h"
+#include "nsThreadUtils.h"
+#include <deque>
+
+struct PRFileDesc;
+
+namespace mozilla {
+
+// Manages file I/O for the media cache. Data comes in over the network
+// via callbacks on the main thread, however we don't want to write the
+// incoming data to the media cache on the main thread, as this could block
+// causing UI jank.
+//
+// So FileBlockCache provides an abstraction for a temporary file accessible
+// as an array of blocks, which supports a block move operation, and
+// allows synchronous reading and writing from any thread, with writes being
+// buffered so as not to block.
+//
+// Writes and cache block moves (which require reading) are deferred to
+// their own non-main thread. This object also ensures that data which has
+// been scheduled to be written, but hasn't actually *been* written, is read
+// as if it had, i.e. pending writes are cached in readable memory until
+// they're flushed to file.
+//
+// To improve efficiency, writes can only be done at block granularity,
+// whereas reads can be done with byte granularity.
+//
+// Note it's also recommended not to read from the media cache from the main
+// thread to prevent jank.
+//
+// When WriteBlock() or MoveBlock() are called, data about how to complete
+// the block change is added to mBlockChanges, indexed by block index, and
+// the block index is appended to the mChangeIndexList. This enables
+// us to quickly tell if a block has been changed, and ensures we can perform
+// the changes in the correct order. An event is dispatched to perform the
+// changes listed in mBlockChanges to file. Read() checks mBlockChanges and
+// determines the current data to return, reading from file or from
+// mBlockChanges as necessary.
+class FileBlockCache : public MediaBlockCacheBase {
+ public:
+ FileBlockCache();
+
+ protected:
+ virtual ~FileBlockCache();
+
+ public:
+ // Launch thread and open temporary file.
+ nsresult Init() override;
+
+ // Will discard pending changes if any.
+ void Flush() override;
+
+ // Maximum number of blocks allowed in this block cache.
+ // Calculated from "media.cache_size" pref.
+ size_t GetMaxBlocks(size_t aCacheSizeInKB) const override;
+
+ // Can be called on any thread. This defers to a non-main thread.
+ nsresult WriteBlock(uint32_t aBlockIndex, Span<const uint8_t> aData1,
+ Span<const uint8_t> aData2) override;
+
+ // Synchronously reads data from file. May read from file or memory
+ // depending on whether written blocks have been flushed to file yet.
+ // Not recommended to be called from the main thread, as can cause jank.
+ nsresult Read(int64_t aOffset, uint8_t* aData, int32_t aLength,
+ int32_t* aBytes) override;
+
+ // Moves a block asynchronously. Can be called on any thread.
+ // This defers file I/O to a non-main thread.
+ nsresult MoveBlock(int32_t aSourceBlockIndex,
+ int32_t aDestBlockIndex) override;
+
+ // Represents a change yet to be made to a block in the file. The change
+ // is either a write (and the data to be written is stored in this struct)
+ // or a move (and the index of the source block is stored instead).
+ struct BlockChange final {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(BlockChange)
+
+ // This block is waiting in memory to be written.
+ // Stores a copy of the block, so we can write it asynchronously.
+ explicit BlockChange(const uint8_t* aData) : mSourceBlockIndex(-1) {
+ mData = MakeUnique<uint8_t[]>(BLOCK_SIZE);
+ memcpy(mData.get(), aData, BLOCK_SIZE);
+ }
+
+ BlockChange(Span<const uint8_t> aData1, Span<const uint8_t> aData2)
+ : mSourceBlockIndex(-1) {
+ MOZ_ASSERT(aData1.Length() + aData2.Length() == BLOCK_SIZE);
+ mData = MakeUnique<uint8_t[]>(BLOCK_SIZE);
+ memcpy(mData.get(), aData1.Elements(), aData1.Length());
+ memcpy(mData.get() + aData1.Length(), aData2.Elements(), aData2.Length());
+ }
+
+ // This block's contents are located in another file
+ // block, i.e. this block has been moved.
+ explicit BlockChange(int32_t aSourceBlockIndex)
+ : mSourceBlockIndex(aSourceBlockIndex) {}
+
+ UniquePtr<uint8_t[]> mData;
+ const int32_t mSourceBlockIndex;
+
+ bool IsMove() const { return mSourceBlockIndex != -1; }
+ bool IsWrite() const {
+ return mSourceBlockIndex == -1 && mData.get() != nullptr;
+ }
+
+ private:
+ // Private destructor, to discourage deletion outside of Release():
+ ~BlockChange() = default;
+ };
+
+ private:
+ int64_t BlockIndexToOffset(int32_t aBlockIndex) {
+ return static_cast<int64_t>(aBlockIndex) * BLOCK_SIZE;
+ }
+
+ void SetCacheFile(PRFileDesc* aFD);
+
+ // Close file in thread and terminate thread.
+ void Close();
+
+ // Performs block writes and block moves on its own thread.
+ void PerformBlockIOs();
+
+ // Mutex which controls access to mFD and mFDCurrentPos. Don't hold
+ // mDataMutex while holding mFileMutex! mFileMutex must be owned
+ // while accessing any of the following data fields or methods.
+ Mutex mFileMutex;
+ // Moves a block already committed to file.
+ nsresult MoveBlockInFile(int32_t aSourceBlockIndex, int32_t aDestBlockIndex);
+ // Seeks file pointer.
+ nsresult Seek(int64_t aOffset);
+ // Reads data from file offset.
+ nsresult ReadFromFile(int64_t aOffset, uint8_t* aDest, int32_t aBytesToRead,
+ int32_t& aBytesRead);
+ nsresult WriteBlockToFile(int32_t aBlockIndex, const uint8_t* aBlockData);
+ // File descriptor we're writing to. This is created externally, but
+ // shutdown by us.
+ PRFileDesc* mFD MOZ_PT_GUARDED_BY(mFileMutex);
+ // The current file offset in the file.
+ int64_t mFDCurrentPos MOZ_GUARDED_BY(mFileMutex);
+
+ // Mutex which controls access to all data in this class, except mFD
+ // and mFDCurrentPos. Don't hold mDataMutex while holding mFileMutex!
+ // mDataMutex must be owned while accessing any of the following data
+ // fields or methods.
+ Mutex mDataMutex;
+ // Ensures we either are running the event to preform IO, or an event
+ // has been dispatched to preform the IO.
+ // mDataMutex must be owned while calling this.
+ void EnsureWriteScheduled();
+
+ // Array of block changes to made. If mBlockChanges[offset/BLOCK_SIZE] ==
+ // nullptr, then the block has no pending changes to be written, but if
+ // mBlockChanges[offset/BLOCK_SIZE] != nullptr, then either there's a block
+ // cached in memory waiting to be written, or this block is the target of a
+ // block move.
+ nsTArray<RefPtr<BlockChange> > mBlockChanges MOZ_GUARDED_BY(mDataMutex);
+ // Event target upon which block writes and block moves are performed. This is
+ // created upon open, and dropped on close.
+ nsCOMPtr<nsISerialEventTarget> mBackgroundET MOZ_GUARDED_BY(mDataMutex);
+ // Queue of pending block indexes that need to be written or moved.
+ std::deque<int32_t> mChangeIndexList MOZ_GUARDED_BY(mDataMutex);
+ // True if we've dispatched an event to commit all pending block changes
+ // to file on mBackgroundET.
+ bool mIsWriteScheduled MOZ_GUARDED_BY(mDataMutex);
+ // True when a read is happening. Pending writes may be postponed, to give
+ // higher priority to reads (which may be blocking the caller).
+ bool mIsReading MOZ_GUARDED_BY(mDataMutex);
+ // True if we've got a temporary file descriptor. Note: we don't use mFD
+ // directly as that's synchronized via mFileMutex and we need to make
+ // decisions about whether we can write while holding mDataMutex.
+ bool mInitialized MOZ_GUARDED_BY(mDataMutex) = false;
+};
+
+} // End namespace mozilla.
+
+#endif /* FILE_BLOCK_CACHE_H_ */
diff --git a/dom/media/FileMediaResource.cpp b/dom/media/FileMediaResource.cpp
new file mode 100644
index 0000000000..16bcc5c3de
--- /dev/null
+++ b/dom/media/FileMediaResource.cpp
@@ -0,0 +1,223 @@
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FileMediaResource.h"
+
+#include "mozilla/AbstractThread.h"
+#include "mozilla/dom/BlobImpl.h"
+#include "mozilla/dom/BlobURLProtocolHandler.h"
+#include "nsContentUtils.h"
+#include "nsIFile.h"
+#include "nsIFileChannel.h"
+#include "nsIFileStreams.h"
+#include "nsITimedChannel.h"
+#include "nsNetUtil.h"
+
+namespace mozilla {
+
+void FileMediaResource::EnsureSizeInitialized() {
+ mLock.AssertCurrentThreadOwns();
+ NS_ASSERTION(mInput, "Must have file input stream");
+ if (mSizeInitialized && mNotifyDataEndedProcessed) {
+ return;
+ }
+
+ if (!mSizeInitialized) {
+ // Get the file size and inform the decoder.
+ uint64_t size;
+ nsresult res = mInput->Available(&size);
+ if (NS_SUCCEEDED(res) && size <= INT64_MAX) {
+ mSize = (int64_t)size;
+ }
+ }
+ mSizeInitialized = true;
+ if (!mNotifyDataEndedProcessed && mSize >= 0) {
+ mCallback->AbstractMainThread()->Dispatch(NewRunnableMethod<nsresult>(
+ "MediaResourceCallback::NotifyDataEnded", mCallback.get(),
+ &MediaResourceCallback::NotifyDataEnded, NS_OK));
+ }
+ mNotifyDataEndedProcessed = true;
+}
+
+nsresult FileMediaResource::GetCachedRanges(MediaByteRangeSet& aRanges) {
+ MutexAutoLock lock(mLock);
+
+ EnsureSizeInitialized();
+ if (mSize == -1) {
+ return NS_ERROR_FAILURE;
+ }
+ aRanges += MediaByteRange(0, mSize);
+ return NS_OK;
+}
+
+nsresult FileMediaResource::Open(nsIStreamListener** aStreamListener) {
+ NS_ASSERTION(NS_IsMainThread(), "Only call on main thread");
+ MOZ_ASSERT(aStreamListener);
+
+ *aStreamListener = nullptr;
+ nsresult rv = NS_OK;
+
+ MutexAutoLock lock(mLock);
+ // The channel is already open. We need a synchronous stream that
+ // implements nsISeekableStream, so we have to find the underlying
+ // file and reopen it
+ nsCOMPtr<nsIFileChannel> fc(do_QueryInterface(mChannel));
+ if (fc) {
+ nsCOMPtr<nsIFile> file;
+ rv = fc->GetFile(getter_AddRefs(file));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(mInput), file, -1, -1,
+ nsIFileInputStream::SHARE_DELETE);
+ NS_ENSURE_SUCCESS(rv, rv);
+ } else if (dom::IsBlobURI(mURI)) {
+ RefPtr<dom::BlobImpl> blobImpl;
+ rv = NS_GetBlobForBlobURI(mURI, getter_AddRefs(blobImpl));
+ NS_ENSURE_SUCCESS(rv, rv);
+ MOZ_ASSERT(blobImpl);
+
+ ErrorResult err;
+ blobImpl->CreateInputStream(getter_AddRefs(mInput), err);
+ if (NS_WARN_IF(err.Failed())) {
+ return err.StealNSResult();
+ }
+ }
+
+ mSeekable = do_QueryInterface(mInput);
+ if (!mSeekable) {
+ // XXX The file may just be a .url or similar
+ // shortcut that points to a Web site. We need to fix this by
+ // doing an async open and waiting until we locate the real resource,
+ // then using that (if it's still a file!).
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+RefPtr<GenericPromise> FileMediaResource::Close() {
+ NS_ASSERTION(NS_IsMainThread(), "Only call on main thread");
+
+ // Since mChennel is only accessed by main thread, there is no necessary to
+ // take the lock.
+ if (mChannel) {
+ mChannel->Cancel(NS_ERROR_PARSED_DATA_CACHED);
+ mChannel = nullptr;
+ }
+
+ return GenericPromise::CreateAndResolve(true, __func__);
+}
+
+already_AddRefed<nsIPrincipal> FileMediaResource::GetCurrentPrincipal() {
+ NS_ASSERTION(NS_IsMainThread(), "Only call on main thread");
+
+ nsCOMPtr<nsIPrincipal> principal;
+ nsIScriptSecurityManager* secMan = nsContentUtils::GetSecurityManager();
+ if (!secMan || !mChannel) return nullptr;
+ secMan->GetChannelResultPrincipal(mChannel, getter_AddRefs(principal));
+ return principal.forget();
+}
+
+bool FileMediaResource::HadCrossOriginRedirects() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsITimedChannel> timedChannel = do_QueryInterface(mChannel);
+ if (!timedChannel) {
+ return false;
+ }
+
+ bool allRedirectsSameOrigin = false;
+ return NS_SUCCEEDED(timedChannel->GetAllRedirectsSameOrigin(
+ &allRedirectsSameOrigin)) &&
+ !allRedirectsSameOrigin;
+}
+
+nsresult FileMediaResource::ReadFromCache(char* aBuffer, int64_t aOffset,
+ uint32_t aCount) {
+ MutexAutoLock lock(mLock);
+
+ EnsureSizeInitialized();
+ if (!aCount) {
+ return NS_OK;
+ }
+ int64_t offset = 0;
+ nsresult res = mSeekable->Tell(&offset);
+ NS_ENSURE_SUCCESS(res, res);
+ res = mSeekable->Seek(nsISeekableStream::NS_SEEK_SET, aOffset);
+ NS_ENSURE_SUCCESS(res, res);
+ uint32_t bytesRead = 0;
+ do {
+ uint32_t x = 0;
+ uint32_t bytesToRead = aCount - bytesRead;
+ res = mInput->Read(aBuffer, bytesToRead, &x);
+ bytesRead += x;
+ if (!x) {
+ res = NS_ERROR_FAILURE;
+ }
+ } while (bytesRead != aCount && res == NS_OK);
+
+ // Reset read head to original position so we don't disturb any other
+ // reading thread.
+ nsresult seekres = mSeekable->Seek(nsISeekableStream::NS_SEEK_SET, offset);
+
+ // If a read failed in the loop above, we want to return its failure code.
+ NS_ENSURE_SUCCESS(res, res);
+
+ // Else we succeed if the reset-seek succeeds.
+ return seekres;
+}
+
+nsresult FileMediaResource::UnsafeRead(char* aBuffer, uint32_t aCount,
+ uint32_t* aBytes) {
+ EnsureSizeInitialized();
+ return mInput->Read(aBuffer, aCount, aBytes);
+}
+
+nsresult FileMediaResource::ReadAt(int64_t aOffset, char* aBuffer,
+ uint32_t aCount, uint32_t* aBytes) {
+ NS_ASSERTION(!NS_IsMainThread(), "Don't call on main thread");
+
+ nsresult rv;
+ {
+ MutexAutoLock lock(mLock);
+ rv = UnsafeSeek(nsISeekableStream::NS_SEEK_SET, aOffset);
+ if (NS_FAILED(rv)) return rv;
+ rv = UnsafeRead(aBuffer, aCount, aBytes);
+ }
+ return rv;
+}
+
+already_AddRefed<MediaByteBuffer> FileMediaResource::UnsafeMediaReadAt(
+ int64_t aOffset, uint32_t aCount) {
+ RefPtr<MediaByteBuffer> bytes = new MediaByteBuffer();
+ bool ok = bytes->SetLength(aCount, fallible);
+ NS_ENSURE_TRUE(ok, nullptr);
+ nsresult rv = UnsafeSeek(nsISeekableStream::NS_SEEK_SET, aOffset);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ char* curr = reinterpret_cast<char*>(bytes->Elements());
+ const char* start = curr;
+ while (aCount > 0) {
+ uint32_t bytesRead;
+ rv = UnsafeRead(curr, aCount, &bytesRead);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ if (!bytesRead) {
+ break;
+ }
+ aCount -= bytesRead;
+ curr += bytesRead;
+ }
+ bytes->SetLength(curr - start);
+ return bytes.forget();
+}
+
+nsresult FileMediaResource::UnsafeSeek(int32_t aWhence, int64_t aOffset) {
+ NS_ASSERTION(!NS_IsMainThread(), "Don't call on main thread");
+
+ if (!mSeekable) return NS_ERROR_FAILURE;
+ EnsureSizeInitialized();
+ return mSeekable->Seek(aWhence, aOffset);
+}
+
+} // namespace mozilla
diff --git a/dom/media/FileMediaResource.h b/dom/media/FileMediaResource.h
new file mode 100644
index 0000000000..7373a6fd37
--- /dev/null
+++ b/dom/media/FileMediaResource.h
@@ -0,0 +1,136 @@
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_media_FileMediaResource_h
+#define mozilla_dom_media_FileMediaResource_h
+
+#include "BaseMediaResource.h"
+#include "mozilla/Mutex.h"
+
+namespace mozilla {
+
+class FileMediaResource : public BaseMediaResource {
+ public:
+ FileMediaResource(MediaResourceCallback* aCallback, nsIChannel* aChannel,
+ nsIURI* aURI, int64_t aSize = -1 /* unknown size */)
+ : BaseMediaResource(aCallback, aChannel, aURI),
+ mSize(aSize),
+ mLock("FileMediaResource.mLock"),
+ mSizeInitialized(aSize != -1) {}
+ ~FileMediaResource() = default;
+
+ // Main thread
+ nsresult Open(nsIStreamListener** aStreamListener) override;
+ RefPtr<GenericPromise> Close() override;
+ void Suspend(bool aCloseImmediately) override {}
+ void Resume() override {}
+ already_AddRefed<nsIPrincipal> GetCurrentPrincipal() override;
+ bool HadCrossOriginRedirects() override;
+ nsresult ReadFromCache(char* aBuffer, int64_t aOffset,
+ uint32_t aCount) override;
+
+ // These methods are called off the main thread.
+
+ // Other thread
+ void SetReadMode(MediaCacheStream::ReadMode aMode) override {}
+ void SetPlaybackRate(uint32_t aBytesPerSecond) override {}
+ nsresult ReadAt(int64_t aOffset, char* aBuffer, uint32_t aCount,
+ uint32_t* aBytes) override;
+ // (Probably) file-based, caching recommended.
+ bool ShouldCacheReads() override { return true; }
+
+ // Any thread
+ void Pin() override {}
+ void Unpin() override {}
+ double GetDownloadRate(bool* aIsReliable) override {
+ // The data's all already here
+ *aIsReliable = true;
+ return 100 * 1024 * 1024; // arbitray, use 100MB/s
+ }
+
+ int64_t GetLength() override {
+ MutexAutoLock lock(mLock);
+
+ EnsureSizeInitialized();
+ return mSizeInitialized ? mSize : 0;
+ }
+
+ int64_t GetNextCachedData(int64_t aOffset) override {
+ MutexAutoLock lock(mLock);
+
+ EnsureSizeInitialized();
+ return (aOffset < mSize) ? aOffset : -1;
+ }
+
+ int64_t GetCachedDataEnd(int64_t aOffset) override {
+ MutexAutoLock lock(mLock);
+
+ EnsureSizeInitialized();
+ return std::max(aOffset, mSize);
+ }
+ bool IsDataCachedToEndOfResource(int64_t aOffset) override { return true; }
+ bool IsTransportSeekable() override { return true; }
+
+ nsresult GetCachedRanges(MediaByteRangeSet& aRanges) override;
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override {
+ // Might be useful to track in the future:
+ // - mInput
+ return BaseMediaResource::SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ protected:
+ // These Unsafe variants of Read and Seek perform their operations
+ // without acquiring mLock. The caller must obtain the lock before
+ // calling. The implmentation of Read, Seek and ReadAt obtains the
+ // lock before calling these Unsafe variants to read or seek.
+ nsresult UnsafeRead(char* aBuffer, uint32_t aCount, uint32_t* aBytes)
+ MOZ_REQUIRES(mLock);
+ nsresult UnsafeSeek(int32_t aWhence, int64_t aOffset) MOZ_REQUIRES(mLock);
+
+ private:
+ // Ensures mSize is initialized, if it can be.
+ // mLock must be held when this is called, and mInput must be non-null.
+ void EnsureSizeInitialized() MOZ_REQUIRES(mLock);
+ already_AddRefed<MediaByteBuffer> UnsafeMediaReadAt(int64_t aOffset,
+ uint32_t aCount)
+ MOZ_REQUIRES(mLock);
+
+ // The file size, or -1 if not known. Immutable after Open().
+ // Can be used from any thread.
+ // XXX FIX? is this under mLock? comments are contradictory
+ int64_t mSize MOZ_GUARDED_BY(mLock);
+
+ // This lock handles synchronisation between calls to Close() and
+ // the Read, Seek, etc calls. Close must not be called while a
+ // Read or Seek is in progress since it resets various internal
+ // values to null.
+ // This lock protects mSeekable, mInput, mSize, and mSizeInitialized.
+ Mutex mLock;
+
+ // Seekable stream interface to file. This can be used from any
+ // thread.
+ nsCOMPtr<nsISeekableStream> mSeekable MOZ_GUARDED_BY(mLock);
+
+ // Input stream for the media data. This can be used from any
+ // thread.
+ nsCOMPtr<nsIInputStream> mInput MOZ_GUARDED_BY(mLock);
+
+ // Whether we've attempted to initialize mSize. Note that mSize can be -1
+ // when mSizeInitialized is true if we tried and failed to get the size
+ // of the file.
+ bool mSizeInitialized MOZ_GUARDED_BY(mLock);
+ // Set to true if NotifyDataEnded callback has been processed (which only
+ // occurs if resource size is known)
+ bool mNotifyDataEndedProcessed = false;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_dom_media_FileMediaResource_h
diff --git a/dom/media/ForwardedInputTrack.cpp b/dom/media/ForwardedInputTrack.cpp
new file mode 100644
index 0000000000..8859fc6332
--- /dev/null
+++ b/dom/media/ForwardedInputTrack.cpp
@@ -0,0 +1,291 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ForwardedInputTrack.h"
+
+#include <algorithm>
+#include "AudioChannelService.h"
+#include "AudioNodeEngine.h"
+#include "AudioNodeExternalInputTrack.h"
+#include "AudioNodeTrack.h"
+#include "AudioSegment.h"
+#include "DOMMediaStream.h"
+#include "GeckoProfiler.h"
+#include "ImageContainer.h"
+#include "MediaTrackGraphImpl.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/Logging.h"
+#include "mozilla/MathAlgorithms.h"
+#include "mozilla/Unused.h"
+#include "nsContentUtils.h"
+#include "nsPrintfCString.h"
+#include "nsServiceManagerUtils.h"
+#include "nsWidgetsCID.h"
+#include "prerror.h"
+#include "Tracing.h"
+#include "VideoSegment.h"
+#include "webaudio/MediaStreamAudioDestinationNode.h"
+
+using namespace mozilla::layers;
+using namespace mozilla::dom;
+using namespace mozilla::gfx;
+
+namespace mozilla {
+
+#ifdef TRACK_LOG
+# undef TRACK_LOG
+#endif
+
+LazyLogModule gForwardedInputTrackLog("ForwardedInputTrack");
+#define TRACK_LOG(type, msg) MOZ_LOG(gForwardedInputTrackLog, type, msg)
+
+ForwardedInputTrack::ForwardedInputTrack(TrackRate aSampleRate,
+ MediaSegment::Type aType)
+ : ProcessedMediaTrack(
+ aSampleRate, aType,
+ aType == MediaSegment::AUDIO
+ ? static_cast<MediaSegment*>(new AudioSegment())
+ : static_cast<MediaSegment*>(new VideoSegment())) {}
+
+void ForwardedInputTrack::AddInput(MediaInputPort* aPort) {
+ SetInput(aPort);
+ ProcessedMediaTrack::AddInput(aPort);
+}
+
+void ForwardedInputTrack::RemoveInput(MediaInputPort* aPort) {
+ TRACK_LOG(LogLevel::Debug,
+ ("ForwardedInputTrack %p removing input %p", this, aPort));
+ MOZ_ASSERT(aPort == mInputPort);
+
+ for (const auto& listener : mOwnedDirectListeners) {
+ MediaTrack* source = mInputPort->GetSource();
+ TRACK_LOG(LogLevel::Debug,
+ ("ForwardedInputTrack %p removing direct listener "
+ "%p. Forwarding to input track %p.",
+ this, listener.get(), aPort->GetSource()));
+ source->RemoveDirectListenerImpl(listener);
+ }
+
+ DisabledTrackMode oldMode = CombinedDisabledMode();
+ mInputDisabledMode = DisabledTrackMode::ENABLED;
+ NotifyIfDisabledModeChangedFrom(oldMode);
+
+ mInputPort = nullptr;
+ ProcessedMediaTrack::RemoveInput(aPort);
+}
+
+void ForwardedInputTrack::SetInput(MediaInputPort* aPort) {
+ MOZ_ASSERT(aPort);
+ MOZ_ASSERT(aPort->GetSource());
+ MOZ_ASSERT(aPort->GetSource()->GetData());
+ MOZ_ASSERT(!mInputPort);
+ MOZ_ASSERT(mInputDisabledMode == DisabledTrackMode::ENABLED);
+
+ mInputPort = aPort;
+
+ for (const auto& listener : mOwnedDirectListeners) {
+ MediaTrack* source = mInputPort->GetSource();
+ TRACK_LOG(LogLevel::Debug, ("ForwardedInputTrack %p adding direct listener "
+ "%p. Forwarding to input track %p.",
+ this, listener.get(), aPort->GetSource()));
+ source->AddDirectListenerImpl(do_AddRef(listener));
+ }
+
+ DisabledTrackMode oldMode = CombinedDisabledMode();
+ mInputDisabledMode = mInputPort->GetSource()->CombinedDisabledMode();
+ NotifyIfDisabledModeChangedFrom(oldMode);
+}
+
+void ForwardedInputTrack::ProcessInputImpl(MediaTrack* aSource,
+ MediaSegment* aSegment,
+ GraphTime aFrom, GraphTime aTo,
+ uint32_t aFlags) {
+ GraphTime next;
+ for (GraphTime t = aFrom; t < aTo; t = next) {
+ MediaInputPort::InputInterval interval =
+ MediaInputPort::GetNextInputInterval(mInputPort, t);
+ interval.mEnd = std::min(interval.mEnd, aTo);
+
+ const bool inputEnded =
+ !aSource ||
+ (aSource->Ended() &&
+ aSource->GetEnd() <=
+ aSource->GraphTimeToTrackTimeWithBlocking(interval.mEnd));
+
+ TrackTime ticks = interval.mEnd - interval.mStart;
+ next = interval.mEnd;
+
+ if (interval.mStart >= interval.mEnd) {
+ break;
+ }
+
+ if (inputEnded) {
+ if (mAutoend && (aFlags & ALLOW_END)) {
+ mEnded = true;
+ break;
+ }
+ aSegment->AppendNullData(ticks);
+ TRACK_LOG(LogLevel::Verbose,
+ ("ForwardedInputTrack %p appending %lld ticks "
+ "of null data (ended input)",
+ this, (long long)ticks));
+ } else if (interval.mInputIsBlocked) {
+ aSegment->AppendNullData(ticks);
+ TRACK_LOG(LogLevel::Verbose,
+ ("ForwardedInputTrack %p appending %lld ticks "
+ "of null data (blocked input)",
+ this, (long long)ticks));
+ } else if (InMutedCycle()) {
+ aSegment->AppendNullData(ticks);
+ } else if (aSource->IsSuspended()) {
+ aSegment->AppendNullData(ticks);
+ } else {
+ MOZ_ASSERT(GetEnd() == GraphTimeToTrackTimeWithBlocking(interval.mStart),
+ "Samples missing");
+ TrackTime inputStart =
+ aSource->GraphTimeToTrackTimeWithBlocking(interval.mStart);
+ TrackTime inputEnd =
+ aSource->GraphTimeToTrackTimeWithBlocking(interval.mEnd);
+ aSegment->AppendSlice(*aSource->GetData(), inputStart, inputEnd);
+ }
+ ApplyTrackDisabling(aSegment);
+ for (const auto& listener : mTrackListeners) {
+ listener->NotifyQueuedChanges(Graph(), GetEnd(), *aSegment);
+ }
+ mSegment->AppendFrom(aSegment);
+ }
+}
+
+void ForwardedInputTrack::ProcessInput(GraphTime aFrom, GraphTime aTo,
+ uint32_t aFlags) {
+ TRACE_COMMENT("ForwardedInputTrack::ProcessInput", "ForwardedInputTrack %p",
+ this);
+ if (mEnded) {
+ return;
+ }
+
+ MediaInputPort* input = mInputPort;
+ MediaTrack* source = input ? input->GetSource() : nullptr;
+ if (mType == MediaSegment::AUDIO) {
+ AudioSegment audio;
+ ProcessInputImpl(source, &audio, aFrom, aTo, aFlags);
+ } else if (mType == MediaSegment::VIDEO) {
+ VideoSegment video;
+ ProcessInputImpl(source, &video, aFrom, aTo, aFlags);
+ } else {
+ MOZ_CRASH("Unknown segment type");
+ }
+
+ if (mEnded) {
+ RemoveAllDirectListenersImpl();
+ }
+}
+
+DisabledTrackMode ForwardedInputTrack::CombinedDisabledMode() const {
+ if (mDisabledMode == DisabledTrackMode::SILENCE_BLACK ||
+ mInputDisabledMode == DisabledTrackMode::SILENCE_BLACK) {
+ return DisabledTrackMode::SILENCE_BLACK;
+ }
+ if (mDisabledMode == DisabledTrackMode::SILENCE_FREEZE ||
+ mInputDisabledMode == DisabledTrackMode::SILENCE_FREEZE) {
+ return DisabledTrackMode::SILENCE_FREEZE;
+ }
+ return DisabledTrackMode::ENABLED;
+}
+
+void ForwardedInputTrack::SetDisabledTrackModeImpl(DisabledTrackMode aMode) {
+ bool enabled = aMode == DisabledTrackMode::ENABLED;
+ TRACK_LOG(LogLevel::Info, ("ForwardedInputTrack %p was explicitly %s", this,
+ enabled ? "enabled" : "disabled"));
+ for (DirectMediaTrackListener* listener : mOwnedDirectListeners) {
+ DisabledTrackMode oldMode = mDisabledMode;
+ bool oldEnabled = oldMode == DisabledTrackMode::ENABLED;
+ if (!oldEnabled && enabled) {
+ TRACK_LOG(LogLevel::Debug, ("ForwardedInputTrack %p setting "
+ "direct listener enabled",
+ this));
+ listener->DecreaseDisabled(oldMode);
+ } else if (oldEnabled && !enabled) {
+ TRACK_LOG(LogLevel::Debug, ("ForwardedInputTrack %p setting "
+ "direct listener disabled",
+ this));
+ listener->IncreaseDisabled(aMode);
+ }
+ }
+ MediaTrack::SetDisabledTrackModeImpl(aMode);
+}
+
+void ForwardedInputTrack::OnInputDisabledModeChanged(
+ DisabledTrackMode aInputMode) {
+ MOZ_ASSERT(mInputs.Length() == 1);
+ MOZ_ASSERT(mInputs[0]->GetSource());
+ DisabledTrackMode oldMode = CombinedDisabledMode();
+ if (mInputDisabledMode == DisabledTrackMode::SILENCE_BLACK &&
+ aInputMode == DisabledTrackMode::SILENCE_FREEZE) {
+ // Don't allow demoting from SILENCE_BLACK to SILENCE_FREEZE. Frames will
+ // remain black so we shouldn't notify that the track got enabled.
+ aInputMode = DisabledTrackMode::SILENCE_BLACK;
+ }
+ mInputDisabledMode = aInputMode;
+ NotifyIfDisabledModeChangedFrom(oldMode);
+}
+
+uint32_t ForwardedInputTrack::NumberOfChannels() const {
+ MOZ_DIAGNOSTIC_ASSERT(mSegment->GetType() == MediaSegment::AUDIO);
+ if (!mInputPort || !mInputPort->GetSource()) {
+ return GetData<AudioSegment>()->MaxChannelCount();
+ }
+ return mInputPort->GetSource()->NumberOfChannels();
+}
+
+void ForwardedInputTrack::AddDirectListenerImpl(
+ already_AddRefed<DirectMediaTrackListener> aListener) {
+ RefPtr<DirectMediaTrackListener> listener = aListener;
+ mOwnedDirectListeners.AppendElement(listener);
+
+ DisabledTrackMode currentMode = mDisabledMode;
+ if (currentMode != DisabledTrackMode::ENABLED) {
+ listener->IncreaseDisabled(currentMode);
+ }
+
+ if (mInputPort) {
+ MediaTrack* source = mInputPort->GetSource();
+ TRACK_LOG(LogLevel::Debug, ("ForwardedInputTrack %p adding direct listener "
+ "%p. Forwarding to input track %p.",
+ this, listener.get(), source));
+ source->AddDirectListenerImpl(listener.forget());
+ }
+}
+
+void ForwardedInputTrack::RemoveDirectListenerImpl(
+ DirectMediaTrackListener* aListener) {
+ for (size_t i = 0; i < mOwnedDirectListeners.Length(); ++i) {
+ if (mOwnedDirectListeners[i] == aListener) {
+ TRACK_LOG(LogLevel::Debug,
+ ("ForwardedInputTrack %p removing direct listener %p", this,
+ aListener));
+ DisabledTrackMode currentMode = mDisabledMode;
+ if (currentMode != DisabledTrackMode::ENABLED) {
+ // Reset the listener's state.
+ aListener->DecreaseDisabled(currentMode);
+ }
+ mOwnedDirectListeners.RemoveElementAt(i);
+ break;
+ }
+ }
+ if (mInputPort) {
+ // Forward to the input
+ MediaTrack* source = mInputPort->GetSource();
+ source->RemoveDirectListenerImpl(aListener);
+ }
+}
+
+void ForwardedInputTrack::RemoveAllDirectListenersImpl() {
+ for (const auto& listener : mOwnedDirectListeners.Clone()) {
+ RemoveDirectListenerImpl(listener);
+ }
+ MOZ_DIAGNOSTIC_ASSERT(mOwnedDirectListeners.IsEmpty());
+}
+
+} // namespace mozilla
diff --git a/dom/media/ForwardedInputTrack.h b/dom/media/ForwardedInputTrack.h
new file mode 100644
index 0000000000..2aaa30ca8f
--- /dev/null
+++ b/dom/media/ForwardedInputTrack.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_FORWARDEDINPUTTRACK_H_
+#define MOZILLA_FORWARDEDINPUTTRACK_H_
+
+#include "MediaTrackGraph.h"
+#include "MediaTrackListener.h"
+#include <algorithm>
+
+namespace mozilla {
+
+/**
+ * See MediaTrackGraph::CreateForwardedInputTrack.
+ */
+class ForwardedInputTrack : public ProcessedMediaTrack {
+ public:
+ ForwardedInputTrack(TrackRate aSampleRate, MediaSegment::Type aType);
+
+ virtual ForwardedInputTrack* AsForwardedInputTrack() override { return this; }
+ friend class DOMMediaStream;
+
+ void AddInput(MediaInputPort* aPort) override;
+ void RemoveInput(MediaInputPort* aPort) override;
+ void ProcessInput(GraphTime aFrom, GraphTime aTo, uint32_t aFlags) override;
+
+ DisabledTrackMode CombinedDisabledMode() const override;
+ void SetDisabledTrackModeImpl(DisabledTrackMode aMode) override;
+ void OnInputDisabledModeChanged(DisabledTrackMode aInputMode) override;
+
+ uint32_t NumberOfChannels() const override;
+
+ friend class MediaTrackGraphImpl;
+
+ protected:
+ // Set up this track from a specific input.
+ void SetInput(MediaInputPort* aPort);
+
+ // MediaSegment-agnostic ProcessInput.
+ void ProcessInputImpl(MediaTrack* aSource, MediaSegment* aSegment,
+ GraphTime aFrom, GraphTime aTo, uint32_t aFlags);
+
+ void AddDirectListenerImpl(
+ already_AddRefed<DirectMediaTrackListener> aListener) override;
+ void RemoveDirectListenerImpl(DirectMediaTrackListener* aListener) override;
+ void RemoveAllDirectListenersImpl() override;
+
+ // These are direct track listeners that have been added to this
+ // ForwardedInputTrack-track. While an input is set, these are forwarded to
+ // the input track. We will update these when this track's disabled status
+ // changes.
+ nsTArray<RefPtr<DirectMediaTrackListener>> mOwnedDirectListeners;
+
+ // Set if an input has been added, nullptr otherwise. Adding more than one
+ // input is an error.
+ MediaInputPort* mInputPort = nullptr;
+
+ // This track's input's associated disabled mode. ENABLED if there is no
+ // input. This is used with MediaTrackListener::NotifyEnabledStateChanged(),
+ // which affects only video tracks. This is set only on ForwardedInputTracks.
+ DisabledTrackMode mInputDisabledMode = DisabledTrackMode::ENABLED;
+};
+
+} // namespace mozilla
+
+#endif /* MOZILLA_FORWARDEDINPUTTRACK_H_ */
diff --git a/dom/media/FrameStatistics.h b/dom/media/FrameStatistics.h
new file mode 100644
index 0000000000..c0063bd1bc
--- /dev/null
+++ b/dom/media/FrameStatistics.h
@@ -0,0 +1,196 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef FrameStatistics_h_
+#define FrameStatistics_h_
+
+#include "mozilla/ReentrantMonitor.h"
+
+namespace mozilla {
+
+struct FrameStatisticsData {
+ // Number of frames parsed and demuxed from media.
+ // Access protected by mReentrantMonitor.
+ uint64_t mParsedFrames = 0;
+
+ // Number of parsed frames which were actually decoded.
+ // Access protected by mReentrantMonitor.
+ uint64_t mDecodedFrames = 0;
+
+ // Number of parsed frames which were dropped in the decoder.
+ // Access protected by mReentrantMonitor.
+ uint64_t mDroppedDecodedFrames = 0;
+
+ // Number of decoded frames which were dropped in the sink
+ // Access protected by mReentrantMonitor.
+ uint64_t mDroppedSinkFrames = 0;
+
+ // Number of sinked frames which were dropped in the compositor
+ // Access protected by mReentrantMonitor.
+ uint64_t mDroppedCompositorFrames = 0;
+
+ // Number of decoded frames which were actually sent down the rendering
+ // pipeline to be painted ("presented"). Access protected by
+ // mReentrantMonitor.
+ uint64_t mPresentedFrames = 0;
+
+ // Sum of all inter-keyframe segment durations, in microseconds.
+ // Dividing by count will give the average inter-keyframe time.
+ uint64_t mInterKeyframeSum_us = 0;
+ // Number of inter-keyframe segments summed so far.
+ size_t mInterKeyframeCount = 0;
+
+ // Maximum inter-keyframe segment duration, in microseconds.
+ uint64_t mInterKeyFrameMax_us = 0;
+
+ FrameStatisticsData() = default;
+ FrameStatisticsData(uint64_t aParsed, uint64_t aDecoded, uint64_t aPresented,
+ uint64_t aDroppedDecodedFrames,
+ uint64_t aDroppedSinkFrames,
+ uint64_t aDroppedCompositorFrames)
+ : mParsedFrames(aParsed),
+ mDecodedFrames(aDecoded),
+ mDroppedDecodedFrames(aDroppedDecodedFrames),
+ mDroppedSinkFrames(aDroppedSinkFrames),
+ mDroppedCompositorFrames(aDroppedCompositorFrames),
+ mPresentedFrames(aPresented) {}
+
+ void Accumulate(const FrameStatisticsData& aStats) {
+ mParsedFrames += aStats.mParsedFrames;
+ mDecodedFrames += aStats.mDecodedFrames;
+ mPresentedFrames += aStats.mPresentedFrames;
+ mDroppedDecodedFrames += aStats.mDroppedDecodedFrames;
+ mDroppedSinkFrames += aStats.mDroppedSinkFrames;
+ mDroppedCompositorFrames += aStats.mDroppedCompositorFrames;
+ mInterKeyframeSum_us += aStats.mInterKeyframeSum_us;
+ mInterKeyframeCount += aStats.mInterKeyframeCount;
+ // It doesn't make sense to add max numbers, instead keep the bigger one.
+ if (mInterKeyFrameMax_us < aStats.mInterKeyFrameMax_us) {
+ mInterKeyFrameMax_us = aStats.mInterKeyFrameMax_us;
+ }
+ }
+};
+
+// Frame decoding/painting related performance counters.
+// Threadsafe.
+class FrameStatistics {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FrameStatistics);
+
+ FrameStatistics() : mReentrantMonitor("FrameStats") {}
+
+ // Returns a copy of all frame statistics data.
+ // Can be called on any thread.
+ FrameStatisticsData GetFrameStatisticsData() const {
+ ReentrantMonitorAutoEnter mon(mReentrantMonitor);
+ return mFrameStatisticsData;
+ }
+
+ // Returns number of frames which have been parsed from the media.
+ // Can be called on any thread.
+ uint64_t GetParsedFrames() const {
+ ReentrantMonitorAutoEnter mon(mReentrantMonitor);
+ return mFrameStatisticsData.mParsedFrames;
+ }
+
+ // Returns the number of parsed frames which have been decoded.
+ // Can be called on any thread.
+ uint64_t GetDecodedFrames() const {
+ ReentrantMonitorAutoEnter mon(mReentrantMonitor);
+ return mFrameStatisticsData.mDecodedFrames;
+ }
+
+ // Returns the number of decoded frames which have been sent to the rendering
+ // pipeline for painting ("presented").
+ // Can be called on any thread.
+ uint64_t GetPresentedFrames() const {
+ ReentrantMonitorAutoEnter mon(mReentrantMonitor);
+ return mFrameStatisticsData.mPresentedFrames;
+ }
+
+ // Returns the number of presented and dropped frames
+ // Can be called on any thread.
+ uint64_t GetTotalFrames() const {
+ ReentrantMonitorAutoEnter mon(mReentrantMonitor);
+ return GetTotalFrames(mFrameStatisticsData);
+ }
+
+ static uint64_t GetTotalFrames(const FrameStatisticsData& aData) {
+ return aData.mPresentedFrames + GetDroppedFrames(aData);
+ }
+
+ // Returns the number of frames that have been skipped because they have
+ // missed their composition deadline.
+ uint64_t GetDroppedFrames() const {
+ ReentrantMonitorAutoEnter mon(mReentrantMonitor);
+ return GetDroppedFrames(mFrameStatisticsData);
+ }
+
+ static uint64_t GetDroppedFrames(const FrameStatisticsData& aData) {
+ return aData.mDroppedDecodedFrames + aData.mDroppedSinkFrames +
+ aData.mDroppedCompositorFrames;
+ }
+
+ uint64_t GetDroppedDecodedFrames() const {
+ ReentrantMonitorAutoEnter mon(mReentrantMonitor);
+ return mFrameStatisticsData.mDroppedDecodedFrames;
+ }
+
+ uint64_t GetDroppedSinkFrames() const {
+ ReentrantMonitorAutoEnter mon(mReentrantMonitor);
+ return mFrameStatisticsData.mDroppedSinkFrames;
+ }
+
+ uint64_t GetDroppedCompositorFrames() const {
+ ReentrantMonitorAutoEnter mon(mReentrantMonitor);
+ return mFrameStatisticsData.mDroppedCompositorFrames;
+ }
+
+ // Increments the parsed and decoded frame counters by the passed in counts.
+ // Can be called on any thread.
+ void Accumulate(const FrameStatisticsData& aStats) {
+ ReentrantMonitorAutoEnter mon(mReentrantMonitor);
+ mFrameStatisticsData.Accumulate(aStats);
+ }
+
+ // Increments the presented frame counters.
+ // Can be called on any thread.
+ void NotifyPresentedFrame() {
+ ReentrantMonitorAutoEnter mon(mReentrantMonitor);
+ ++mFrameStatisticsData.mPresentedFrames;
+ }
+
+ // Stack based class to assist in notifying the frame statistics of
+ // parsed and decoded frames. Use inside video demux & decode functions
+ // to ensure all parsed and decoded frames are reported on all return paths.
+ class AutoNotifyDecoded {
+ public:
+ explicit AutoNotifyDecoded(FrameStatistics* aFrameStats)
+ : mFrameStats(aFrameStats) {}
+ ~AutoNotifyDecoded() {
+ if (mFrameStats) {
+ mFrameStats->Accumulate(mStats);
+ }
+ }
+
+ FrameStatisticsData mStats;
+
+ private:
+ FrameStatistics* mFrameStats;
+ };
+
+ private:
+ ~FrameStatistics() = default;
+
+ // ReentrantMonitor to protect access of playback statistics.
+ mutable ReentrantMonitor mReentrantMonitor MOZ_UNANNOTATED;
+
+ FrameStatisticsData mFrameStatisticsData;
+};
+
+} // namespace mozilla
+
+#endif // FrameStatistics_h_
diff --git a/dom/media/GetUserMediaRequest.cpp b/dom/media/GetUserMediaRequest.cpp
new file mode 100644
index 0000000000..84c9eba32a
--- /dev/null
+++ b/dom/media/GetUserMediaRequest.cpp
@@ -0,0 +1,127 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GetUserMediaRequest.h"
+
+#include "base/basictypes.h"
+#include "MediaManager.h"
+#include "mozilla/dom/MediaDevicesBinding.h"
+#include "mozilla/dom/MediaStreamBinding.h"
+#include "mozilla/dom/GetUserMediaRequestBinding.h"
+#include "nsIMediaDevice.h"
+#include "nsIScriptGlobalObject.h"
+#include "nsPIDOMWindow.h"
+
+namespace mozilla::dom {
+
+GetUserMediaRequest::GetUserMediaRequest(
+ nsPIDOMWindowInner* aInnerWindow, const nsAString& aCallID,
+ RefPtr<LocalMediaDeviceSetRefCnt> aMediaDeviceSet,
+ const MediaStreamConstraints& aConstraints, bool aIsSecure,
+ bool aIsHandlingUserInput)
+ : mInnerWindowID(aInnerWindow->WindowID()),
+ mOuterWindowID(aInnerWindow->GetOuterWindow()->WindowID()),
+ mCallID(aCallID),
+ mMediaDeviceSet(std::move(aMediaDeviceSet)),
+ mConstraints(new MediaStreamConstraints(aConstraints)),
+ mType(GetUserMediaRequestType::Getusermedia),
+ mIsSecure(aIsSecure),
+ mIsHandlingUserInput(aIsHandlingUserInput) {}
+
+GetUserMediaRequest::GetUserMediaRequest(
+ nsPIDOMWindowInner* aInnerWindow, const nsAString& aCallID,
+ RefPtr<LocalMediaDeviceSetRefCnt> aMediaDeviceSet,
+ const AudioOutputOptions& aAudioOutputOptions, bool aIsSecure,
+ bool aIsHandlingUserInput)
+ : mInnerWindowID(aInnerWindow->WindowID()),
+ mOuterWindowID(aInnerWindow->GetOuterWindow()->WindowID()),
+ mCallID(aCallID),
+ mMediaDeviceSet(std::move(aMediaDeviceSet)),
+ mAudioOutputOptions(new AudioOutputOptions(aAudioOutputOptions)),
+ mType(GetUserMediaRequestType::Selectaudiooutput),
+ mIsSecure(aIsSecure),
+ mIsHandlingUserInput(aIsHandlingUserInput) {}
+
+GetUserMediaRequest::GetUserMediaRequest(nsPIDOMWindowInner* aInnerWindow,
+ const nsAString& aRawId,
+ const nsAString& aMediaSource,
+ bool aIsHandlingUserInput)
+ : mInnerWindowID(0),
+ mOuterWindowID(0),
+ mRawID(aRawId),
+ mMediaSource(aMediaSource),
+ mType(GetUserMediaRequestType::Recording_device_stopped),
+ mIsSecure(false),
+ mIsHandlingUserInput(aIsHandlingUserInput) {
+ if (aInnerWindow && aInnerWindow->GetOuterWindow()) {
+ mOuterWindowID = aInnerWindow->GetOuterWindow()->WindowID();
+ }
+}
+
+GetUserMediaRequest::~GetUserMediaRequest() = default;
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_0(GetUserMediaRequest)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(GetUserMediaRequest)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(GetUserMediaRequest)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(GetUserMediaRequest)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+JSObject* GetUserMediaRequest::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return GetUserMediaRequest_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsISupports* GetUserMediaRequest::GetParentObject() { return nullptr; }
+
+GetUserMediaRequestType GetUserMediaRequest::Type() { return mType; }
+
+void GetUserMediaRequest::GetCallID(nsString& retval) { retval = mCallID; }
+
+void GetUserMediaRequest::GetRawID(nsString& retval) { retval = mRawID; }
+
+void GetUserMediaRequest::GetMediaSource(nsString& retval) {
+ retval = mMediaSource;
+}
+
+uint64_t GetUserMediaRequest::WindowID() { return mOuterWindowID; }
+
+uint64_t GetUserMediaRequest::InnerWindowID() { return mInnerWindowID; }
+
+bool GetUserMediaRequest::IsSecure() { return mIsSecure; }
+
+bool GetUserMediaRequest::IsHandlingUserInput() const {
+ return mIsHandlingUserInput;
+}
+
+void GetUserMediaRequest::GetDevices(
+ nsTArray<RefPtr<nsIMediaDevice>>& retval) const {
+ MOZ_ASSERT(retval.Length() == 0);
+ if (!mMediaDeviceSet) {
+ return;
+ }
+ for (const auto& device : *mMediaDeviceSet) {
+ retval.AppendElement(device);
+ }
+}
+
+void GetUserMediaRequest::GetConstraints(MediaStreamConstraints& result) {
+ MOZ_ASSERT(result.mAudio.IsBoolean() && !result.mAudio.GetAsBoolean() &&
+ result.mVideo.IsBoolean() && !result.mVideo.GetAsBoolean(),
+ "result should be default initialized");
+ if (mConstraints) {
+ result = *mConstraints;
+ }
+}
+
+void GetUserMediaRequest::GetAudioOutputOptions(AudioOutputOptions& result) {
+ MOZ_ASSERT(result.mDeviceId.IsEmpty(),
+ "result should be default initialized");
+ if (mAudioOutputOptions) {
+ result = *mAudioOutputOptions;
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/GetUserMediaRequest.h b/dom/media/GetUserMediaRequest.h
new file mode 100644
index 0000000000..a2b69bf1d6
--- /dev/null
+++ b/dom/media/GetUserMediaRequest.h
@@ -0,0 +1,93 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GetUserMediaRequest_h__
+#define GetUserMediaRequest_h__
+
+#include <cstdint>
+#include "js/TypeDecls.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/UniquePtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupports.h"
+#include "nsString.h"
+#include "nsWrapperCache.h"
+
+class nsIMediaDevice;
+class nsPIDOMWindowInner;
+
+namespace mozilla {
+
+class LocalMediaDevice;
+
+namespace media {
+template <typename T>
+class Refcountable;
+}
+
+namespace dom {
+
+struct AudioOutputOptions;
+struct MediaStreamConstraints;
+enum class GetUserMediaRequestType : uint8_t;
+
+class GetUserMediaRequest : public nsISupports, public nsWrapperCache {
+ public:
+ using LocalMediaDeviceSetRefCnt =
+ media::Refcountable<nsTArray<RefPtr<LocalMediaDevice>>>;
+
+ // For getUserMedia "getUserMedia:request"
+ GetUserMediaRequest(nsPIDOMWindowInner* aInnerWindow,
+ const nsAString& aCallID,
+ RefPtr<LocalMediaDeviceSetRefCnt> aMediaDeviceSet,
+ const MediaStreamConstraints& aConstraints,
+ bool aIsSecure, bool aIsHandlingUserInput);
+ // For selectAudioOutput "getUserMedia:request"
+ GetUserMediaRequest(nsPIDOMWindowInner* aInnerWindow,
+ const nsAString& aCallID,
+ RefPtr<LocalMediaDeviceSetRefCnt> aMediaDeviceSet,
+ const AudioOutputOptions& aAudioOutputOptions,
+ bool aIsSecure, bool aIsHandlingUserInput);
+ // For "recording-device-stopped"
+ GetUserMediaRequest(nsPIDOMWindowInner* aInnerWindow, const nsAString& aRawId,
+ const nsAString& aMediaSource, bool aIsHandlingUserInput);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(GetUserMediaRequest)
+
+ JSObject* WrapObject(JSContext* cx,
+ JS::Handle<JSObject*> aGivenProto) override;
+ nsISupports* GetParentObject();
+
+ GetUserMediaRequestType Type();
+ uint64_t WindowID();
+ uint64_t InnerWindowID();
+ bool IsSecure();
+ bool IsHandlingUserInput() const;
+ void GetCallID(nsString& retval);
+ void GetRawID(nsString& retval);
+ void GetMediaSource(nsString& retval);
+ void GetDevices(nsTArray<RefPtr<nsIMediaDevice>>& retval) const;
+ void GetConstraints(MediaStreamConstraints& result);
+ void GetAudioOutputOptions(AudioOutputOptions& result);
+
+ private:
+ virtual ~GetUserMediaRequest();
+
+ uint64_t mInnerWindowID, mOuterWindowID;
+ const nsString mCallID;
+ const nsString mRawID;
+ const nsString mMediaSource;
+ const RefPtr<LocalMediaDeviceSetRefCnt> mMediaDeviceSet;
+ UniquePtr<MediaStreamConstraints> mConstraints;
+ UniquePtr<AudioOutputOptions> mAudioOutputOptions;
+ GetUserMediaRequestType mType;
+ bool mIsSecure;
+ bool mIsHandlingUserInput;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // GetUserMediaRequest_h__
diff --git a/dom/media/GraphDriver.cpp b/dom/media/GraphDriver.cpp
new file mode 100644
index 0000000000..745a4785c5
--- /dev/null
+++ b/dom/media/GraphDriver.cpp
@@ -0,0 +1,1296 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GraphDriver.h"
+
+#include "AudioNodeEngine.h"
+#include "mozilla/dom/AudioContext.h"
+#include "mozilla/dom/AudioDeviceInfo.h"
+#include "mozilla/dom/BaseAudioContextBinding.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/SharedThreadPool.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Unused.h"
+#include "mozilla/MathAlgorithms.h"
+#include "CubebDeviceEnumerator.h"
+#include "MediaTrackGraphImpl.h"
+#include "CallbackThreadRegistry.h"
+#include "Tracing.h"
+
+#ifdef MOZ_WEBRTC
+# include "webrtc/MediaEngineWebRTC.h"
+#endif
+
+#ifdef XP_MACOSX
+# include <sys/sysctl.h>
+#endif
+
+extern mozilla::LazyLogModule gMediaTrackGraphLog;
+#ifdef LOG
+# undef LOG
+#endif // LOG
+#define LOG(type, msg) MOZ_LOG(gMediaTrackGraphLog, type, msg)
+
+namespace mozilla {
+
+GraphDriver::GraphDriver(GraphInterface* aGraphInterface,
+ GraphDriver* aPreviousDriver, uint32_t aSampleRate)
+ : mGraphInterface(aGraphInterface),
+ mSampleRate(aSampleRate),
+ mPreviousDriver(aPreviousDriver) {}
+
+void GraphDriver::SetState(GraphTime aIterationStart, GraphTime aIterationEnd,
+ GraphTime aStateComputedTime) {
+ MOZ_ASSERT(InIteration() || !ThreadRunning());
+
+ mIterationStart = aIterationStart;
+ mIterationEnd = aIterationEnd;
+ mStateComputedTime = aStateComputedTime;
+}
+
+#ifdef DEBUG
+bool GraphDriver::InIteration() const {
+ return OnThread() || Graph()->InDriverIteration(this);
+}
+#endif
+
+GraphDriver* GraphDriver::PreviousDriver() {
+ MOZ_ASSERT(InIteration() || !ThreadRunning());
+ return mPreviousDriver;
+}
+
+void GraphDriver::SetPreviousDriver(GraphDriver* aPreviousDriver) {
+ MOZ_ASSERT(InIteration() || !ThreadRunning());
+ mPreviousDriver = aPreviousDriver;
+}
+
+ThreadedDriver::ThreadedDriver(GraphInterface* aGraphInterface,
+ GraphDriver* aPreviousDriver,
+ uint32_t aSampleRate)
+ : GraphDriver(aGraphInterface, aPreviousDriver, aSampleRate),
+ mThreadRunning(false) {}
+
+class MediaTrackGraphShutdownThreadRunnable : public Runnable {
+ public:
+ explicit MediaTrackGraphShutdownThreadRunnable(
+ already_AddRefed<nsIThread> aThread)
+ : Runnable("MediaTrackGraphShutdownThreadRunnable"), mThread(aThread) {}
+ NS_IMETHOD Run() override {
+ TRACE("MediaTrackGraphShutdownThreadRunnable");
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mThread);
+
+ mThread->AsyncShutdown();
+ mThread = nullptr;
+ return NS_OK;
+ }
+
+ private:
+ nsCOMPtr<nsIThread> mThread;
+};
+
+ThreadedDriver::~ThreadedDriver() {
+ if (mThread) {
+ nsCOMPtr<nsIRunnable> event =
+ new MediaTrackGraphShutdownThreadRunnable(mThread.forget());
+ SchedulerGroup::Dispatch(TaskCategory::Other, event.forget());
+ }
+}
+
+class MediaTrackGraphInitThreadRunnable : public Runnable {
+ public:
+ explicit MediaTrackGraphInitThreadRunnable(ThreadedDriver* aDriver)
+ : Runnable("MediaTrackGraphInitThreadRunnable"), mDriver(aDriver) {}
+ NS_IMETHOD Run() override {
+ TRACE("MediaTrackGraphInitThreadRunnable");
+ MOZ_ASSERT(!mDriver->ThreadRunning());
+ LOG(LogLevel::Debug, ("Starting a new system driver for graph %p",
+ mDriver->mGraphInterface.get()));
+
+ if (GraphDriver* previousDriver = mDriver->PreviousDriver()) {
+ LOG(LogLevel::Debug,
+ ("%p releasing an AudioCallbackDriver(%p), for graph %p",
+ mDriver.get(), previousDriver, mDriver->Graph()));
+ MOZ_ASSERT(!mDriver->AsAudioCallbackDriver());
+ RefPtr<AsyncCubebTask> releaseEvent =
+ new AsyncCubebTask(previousDriver->AsAudioCallbackDriver(),
+ AsyncCubebOperation::SHUTDOWN);
+ releaseEvent->Dispatch();
+ mDriver->SetPreviousDriver(nullptr);
+ }
+
+ mDriver->RunThread();
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<ThreadedDriver> mDriver;
+};
+
+void ThreadedDriver::Start() {
+ MOZ_ASSERT(!ThreadRunning());
+ LOG(LogLevel::Debug,
+ ("Starting thread for a SystemClockDriver %p", mGraphInterface.get()));
+ Unused << NS_WARN_IF(mThread);
+ MOZ_ASSERT(!mThread); // Ensure we haven't already started it
+
+ nsCOMPtr<nsIRunnable> event = new MediaTrackGraphInitThreadRunnable(this);
+ // Note: mThread may be null during event->Run() if we pass to NewNamedThread!
+ // See AudioInitTask
+ nsresult rv = NS_NewNamedThread("MediaTrackGrph", getter_AddRefs(mThread));
+ if (NS_SUCCEEDED(rv)) {
+ mThread->Dispatch(event.forget(), NS_DISPATCH_NORMAL);
+ }
+}
+
+void ThreadedDriver::Shutdown() {
+ NS_ASSERTION(NS_IsMainThread(), "Must be called on main thread");
+ // mGraph's thread is not running so it's OK to do whatever here
+ LOG(LogLevel::Debug, ("Stopping threads for MediaTrackGraph %p", this));
+
+ if (mThread) {
+ LOG(LogLevel::Debug,
+ ("%p: Stopping ThreadedDriver's %p thread", Graph(), this));
+ mThread->AsyncShutdown();
+ mThread = nullptr;
+ }
+}
+
+SystemClockDriver::SystemClockDriver(GraphInterface* aGraphInterface,
+ GraphDriver* aPreviousDriver,
+ uint32_t aSampleRate)
+ : ThreadedDriver(aGraphInterface, aPreviousDriver, aSampleRate),
+ mInitialTimeStamp(TimeStamp::Now()),
+ mCurrentTimeStamp(TimeStamp::Now()),
+ mLastTimeStamp(TimeStamp::Now()) {}
+
+SystemClockDriver::~SystemClockDriver() = default;
+
+void ThreadedDriver::RunThread() {
+ mThreadRunning = true;
+ while (true) {
+ mIterationStart = mIterationEnd;
+ mIterationEnd += GetIntervalForIteration();
+
+ if (mStateComputedTime < mIterationEnd) {
+ LOG(LogLevel::Warning, ("%p: Global underrun detected", Graph()));
+ mIterationEnd = mStateComputedTime;
+ }
+
+ if (mIterationStart >= mIterationEnd) {
+ NS_ASSERTION(mIterationStart == mIterationEnd,
+ "Time can't go backwards!");
+ // This could happen due to low clock resolution, maybe?
+ LOG(LogLevel::Debug, ("%p: Time did not advance", Graph()));
+ }
+
+ GraphTime nextStateComputedTime =
+ MediaTrackGraphImpl::RoundUpToEndOfAudioBlock(
+ mIterationEnd + MillisecondsToMediaTime(AUDIO_TARGET_MS));
+ if (nextStateComputedTime < mStateComputedTime) {
+ // A previous driver may have been processing further ahead of
+ // iterationEnd.
+ LOG(LogLevel::Warning,
+ ("%p: Prevent state from going backwards. interval[%ld; %ld] "
+ "state[%ld; "
+ "%ld]",
+ Graph(), (long)mIterationStart, (long)mIterationEnd,
+ (long)mStateComputedTime, (long)nextStateComputedTime));
+ nextStateComputedTime = mStateComputedTime;
+ }
+ LOG(LogLevel::Verbose,
+ ("%p: interval[%ld; %ld] state[%ld; %ld]", Graph(),
+ (long)mIterationStart, (long)mIterationEnd, (long)mStateComputedTime,
+ (long)nextStateComputedTime));
+
+ mStateComputedTime = nextStateComputedTime;
+ IterationResult result =
+ Graph()->OneIteration(mStateComputedTime, mIterationEnd, nullptr);
+
+ if (result.IsStop()) {
+ // Signal that we're done stopping.
+ result.Stopped();
+ break;
+ }
+ WaitForNextIteration();
+ if (GraphDriver* nextDriver = result.NextDriver()) {
+ LOG(LogLevel::Debug, ("%p: Switching to AudioCallbackDriver", Graph()));
+ result.Switched();
+ nextDriver->SetState(mIterationStart, mIterationEnd, mStateComputedTime);
+ nextDriver->Start();
+ break;
+ }
+ MOZ_ASSERT(result.IsStillProcessing());
+ }
+ mThreadRunning = false;
+}
+
+MediaTime SystemClockDriver::GetIntervalForIteration() {
+ TimeStamp now = TimeStamp::Now();
+ MediaTime interval =
+ SecondsToMediaTime((now - mCurrentTimeStamp).ToSeconds());
+ mCurrentTimeStamp = now;
+
+ MOZ_LOG(gMediaTrackGraphLog, LogLevel::Verbose,
+ ("%p: Updating current time to %f (real %f, StateComputedTime() %f)",
+ Graph(), MediaTimeToSeconds(mIterationEnd + interval),
+ (now - mInitialTimeStamp).ToSeconds(),
+ MediaTimeToSeconds(mStateComputedTime)));
+
+ return interval;
+}
+
+void ThreadedDriver::EnsureNextIteration() {
+ mWaitHelper.EnsureNextIteration();
+}
+
+void ThreadedDriver::WaitForNextIteration() {
+ MOZ_ASSERT(mThread);
+ MOZ_ASSERT(OnThread());
+ mWaitHelper.WaitForNextIterationAtLeast(WaitInterval());
+}
+
+TimeDuration SystemClockDriver::WaitInterval() {
+ MOZ_ASSERT(mThread);
+ MOZ_ASSERT(OnThread());
+ TimeStamp now = TimeStamp::Now();
+ int64_t timeoutMS = MEDIA_GRAPH_TARGET_PERIOD_MS -
+ int64_t((now - mCurrentTimeStamp).ToMilliseconds());
+ // Make sure timeoutMS doesn't overflow 32 bits by waking up at
+ // least once a minute, if we need to wake up at all
+ timeoutMS = std::max<int64_t>(0, std::min<int64_t>(timeoutMS, 60 * 1000));
+ LOG(LogLevel::Verbose,
+ ("%p: Waiting for next iteration; at %f, timeout=%f", Graph(),
+ (now - mInitialTimeStamp).ToSeconds(), timeoutMS / 1000.0));
+
+ return TimeDuration::FromMilliseconds(timeoutMS);
+}
+
+OfflineClockDriver::OfflineClockDriver(GraphInterface* aGraphInterface,
+ uint32_t aSampleRate, GraphTime aSlice)
+ : ThreadedDriver(aGraphInterface, nullptr, aSampleRate), mSlice(aSlice) {}
+
+OfflineClockDriver::~OfflineClockDriver() = default;
+
+void OfflineClockDriver::RunThread() {
+ nsCOMPtr<nsIThreadInternal> threadInternal = do_QueryInterface(mThread);
+ nsCOMPtr<nsIThreadObserver> observer = do_QueryInterface(Graph());
+ threadInternal->SetObserver(observer);
+
+ ThreadedDriver::RunThread();
+}
+
+MediaTime OfflineClockDriver::GetIntervalForIteration() {
+ return MillisecondsToMediaTime(mSlice);
+}
+
+AsyncCubebTask::AsyncCubebTask(AudioCallbackDriver* aDriver,
+ AsyncCubebOperation aOperation)
+ : Runnable("AsyncCubebTask"),
+ mDriver(aDriver),
+ mOperation(aOperation),
+ mShutdownGrip(aDriver->Graph()) {
+ MOZ_ASSERT(mDriver->mAudioStreamState ==
+ AudioCallbackDriver::AudioStreamState::Pending ||
+ aOperation == AsyncCubebOperation::SHUTDOWN,
+ "Replacing active stream!");
+}
+
+AsyncCubebTask::~AsyncCubebTask() = default;
+
+NS_IMETHODIMP
+AsyncCubebTask::Run() {
+ MOZ_ASSERT(mDriver);
+
+ switch (mOperation) {
+ case AsyncCubebOperation::INIT: {
+ LOG(LogLevel::Debug, ("%p: AsyncCubebOperation::INIT driver=%p",
+ mDriver->Graph(), mDriver.get()));
+ mDriver->Init();
+ break;
+ }
+ case AsyncCubebOperation::SHUTDOWN: {
+ LOG(LogLevel::Debug, ("%p: AsyncCubebOperation::SHUTDOWN driver=%p",
+ mDriver->Graph(), mDriver.get()));
+ mDriver->Stop();
+ mDriver = nullptr;
+ mShutdownGrip = nullptr;
+ break;
+ }
+ default:
+ MOZ_CRASH("Operation not implemented.");
+ }
+
+ // The thread will kill itself after a bit
+ return NS_OK;
+}
+
+TrackAndPromiseForOperation::TrackAndPromiseForOperation(
+ MediaTrack* aTrack, dom::AudioContextOperation aOperation,
+ AbstractThread* aMainThread,
+ MozPromiseHolder<MediaTrackGraph::AudioContextOperationPromise>&& aHolder)
+ : mTrack(aTrack),
+ mOperation(aOperation),
+ mMainThread(aMainThread),
+ mHolder(std::move(aHolder)) {}
+
+TrackAndPromiseForOperation::TrackAndPromiseForOperation(
+ TrackAndPromiseForOperation&& aOther) noexcept
+ : mTrack(std::move(aOther.mTrack)),
+ mOperation(aOther.mOperation),
+ mMainThread(std::move(aOther.mMainThread)),
+ mHolder(std::move(aOther.mHolder)) {}
+
+/* Helper to proxy the GraphInterface methods used by a running
+ * mFallbackDriver. */
+class AudioCallbackDriver::FallbackWrapper : public GraphInterface {
+ public:
+ FallbackWrapper(RefPtr<GraphInterface> aGraph,
+ RefPtr<AudioCallbackDriver> aOwner, uint32_t aSampleRate,
+ GraphTime aIterationStart, GraphTime aIterationEnd,
+ GraphTime aStateComputedTime)
+ : mGraph(std::move(aGraph)),
+ mOwner(std::move(aOwner)),
+ mFallbackDriver(
+ MakeRefPtr<SystemClockDriver>(this, nullptr, aSampleRate)),
+ mIterationStart(aIterationStart),
+ mIterationEnd(aIterationEnd),
+ mStateComputedTime(aStateComputedTime) {
+ mFallbackDriver->SetState(mIterationStart, mIterationEnd,
+ mStateComputedTime);
+ }
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ /* Proxied SystemClockDriver methods */
+ void SetState(GraphTime aIterationStart, GraphTime aIterationEnd,
+ GraphTime aStateComputedTime) {
+ mIterationStart = aIterationStart;
+ mIterationEnd = aIterationEnd;
+ mStateComputedTime = aStateComputedTime;
+ mFallbackDriver->SetState(aIterationStart, aIterationEnd,
+ aStateComputedTime);
+ }
+ void Start() { mFallbackDriver->Start(); }
+ MOZ_CAN_RUN_SCRIPT void Shutdown() {
+ RefPtr<SystemClockDriver> driver = mFallbackDriver;
+ driver->Shutdown();
+ }
+ void EnsureNextIteration() { mFallbackDriver->EnsureNextIteration(); }
+#ifdef DEBUG
+ bool InIteration() { return mFallbackDriver->InIteration(); }
+#endif
+ bool OnThread() { return mFallbackDriver->OnThread(); }
+
+ /* GraphInterface methods */
+ void NotifyOutputData(AudioDataValue* aBuffer, size_t aFrames,
+ TrackRate aRate, uint32_t aChannels) override {
+ MOZ_CRASH("Unexpected NotifyOutputData from fallback SystemClockDriver");
+ }
+ void NotifyInputStopped() override {
+ MOZ_CRASH("Unexpected NotifyInputStopped from fallback SystemClockDriver");
+ }
+ void NotifyInputData(const AudioDataValue* aBuffer, size_t aFrames,
+ TrackRate aRate, uint32_t aChannels,
+ uint32_t aAlreadyBuffered) override {
+ MOZ_CRASH("Unexpected NotifyInputData from fallback SystemClockDriver");
+ }
+ void DeviceChanged() override {
+ MOZ_CRASH("Unexpected DeviceChanged from fallback SystemClockDriver");
+ }
+#ifdef DEBUG
+ bool InDriverIteration(const GraphDriver* aDriver) const override {
+ return !mOwner->ThreadRunning() && mOwner->InIteration();
+ }
+#endif
+ IterationResult OneIteration(GraphTime aStateComputedEnd,
+ GraphTime aIterationEnd,
+ AudioMixer* aMixer) override {
+ MOZ_ASSERT(!aMixer);
+
+#ifdef DEBUG
+ AutoInCallback aic(mOwner);
+#endif
+
+ mIterationStart = mIterationEnd;
+ mIterationEnd = aIterationEnd;
+ mStateComputedTime = aStateComputedEnd;
+ IterationResult result =
+ mGraph->OneIteration(aStateComputedEnd, aIterationEnd, aMixer);
+
+ AudioStreamState audioState = mOwner->mAudioStreamState;
+
+ MOZ_ASSERT(audioState != AudioStreamState::Stopping,
+ "The audio driver can only enter stopping if it iterated the "
+ "graph, which it can only do if there's no fallback driver");
+ if (audioState != AudioStreamState::Running && result.IsStillProcessing()) {
+ mOwner->MaybeStartAudioStream();
+ return result;
+ }
+
+ MOZ_ASSERT(result.IsStillProcessing() || result.IsStop() ||
+ result.IsSwitchDriver());
+
+ // Proxy the release of the fallback driver to a background thread, so it
+ // doesn't perform unexpected suicide.
+ IterationResult stopFallback =
+ IterationResult::CreateStop(NS_NewRunnableFunction(
+ "AudioCallbackDriver::FallbackDriverStopped",
+ [self = RefPtr<FallbackWrapper>(this), this,
+ result = std::move(result)]() mutable {
+ FallbackDriverState fallbackState =
+ result.IsStillProcessing() ? FallbackDriverState::None
+ : FallbackDriverState::Stopped;
+ mOwner->FallbackDriverStopped(mIterationStart, mIterationEnd,
+ mStateComputedTime, fallbackState);
+
+ if (fallbackState == FallbackDriverState::Stopped) {
+#ifdef DEBUG
+ // The AudioCallbackDriver may not iterate the graph, but we'll
+ // call into it so we need to be regarded as "in iteration".
+ AutoInCallback aic(mOwner);
+#endif
+ if (GraphDriver* nextDriver = result.NextDriver()) {
+ LOG(LogLevel::Debug,
+ ("%p: Switching from fallback to other driver.",
+ mGraph.get()));
+ result.Switched();
+ nextDriver->SetState(mIterationStart, mIterationEnd,
+ mStateComputedTime);
+ nextDriver->Start();
+ } else if (result.IsStop()) {
+ LOG(LogLevel::Debug,
+ ("%p: Stopping fallback driver.", mGraph.get()));
+ result.Stopped();
+ }
+ }
+ mOwner = nullptr;
+ NS_DispatchBackgroundTask(NS_NewRunnableFunction(
+ "AudioCallbackDriver::FallbackDriverStopped::Release",
+ [fallback = std::move(self->mFallbackDriver)] {}));
+ }));
+
+ return stopFallback;
+ }
+
+ private:
+ virtual ~FallbackWrapper() = default;
+
+ const RefPtr<GraphInterface> mGraph;
+ // Valid until mFallbackDriver has finished its last iteration.
+ RefPtr<AudioCallbackDriver> mOwner;
+ RefPtr<SystemClockDriver> mFallbackDriver;
+
+ GraphTime mIterationStart;
+ GraphTime mIterationEnd;
+ GraphTime mStateComputedTime;
+};
+
+NS_IMPL_ISUPPORTS0(AudioCallbackDriver::FallbackWrapper)
+
+AudioCallbackDriver::AudioCallbackDriver(
+ GraphInterface* aGraphInterface, GraphDriver* aPreviousDriver,
+ uint32_t aSampleRate, uint32_t aOutputChannelCount,
+ uint32_t aInputChannelCount, CubebUtils::AudioDeviceID aOutputDeviceID,
+ CubebUtils::AudioDeviceID aInputDeviceID, AudioInputType aAudioInputType)
+ : GraphDriver(aGraphInterface, aPreviousDriver, aSampleRate),
+ mOutputChannelCount(aOutputChannelCount),
+ mInputChannelCount(aInputChannelCount),
+ mOutputDeviceID(aOutputDeviceID),
+ mInputDeviceID(aInputDeviceID),
+ mIterationDurationMS(MEDIA_GRAPH_TARGET_PERIOD_MS),
+ mInitShutdownThread(CUBEB_TASK_THREAD),
+ mAudioThreadId(ProfilerThreadId{}),
+ mAudioThreadIdInCb(std::thread::id()),
+ mFallback("AudioCallbackDriver::mFallback"),
+ mSandboxed(CubebUtils::SandboxEnabled()) {
+ LOG(LogLevel::Debug, ("%p: AudioCallbackDriver %p ctor - input: device %p, "
+ "channel %d, output: device %p, channel %d",
+ Graph(), this, mInputDeviceID, mInputChannelCount,
+ mOutputDeviceID, mOutputChannelCount));
+
+ NS_WARNING_ASSERTION(mOutputChannelCount != 0,
+ "Invalid output channel count");
+ MOZ_ASSERT(mOutputChannelCount <= 8);
+
+ const uint32_t kIdleThreadTimeoutMs = 2000;
+ mInitShutdownThread->SetIdleThreadTimeout(
+ PR_MillisecondsToInterval(kIdleThreadTimeoutMs));
+
+ if (aAudioInputType == AudioInputType::Voice) {
+ LOG(LogLevel::Debug, ("VOICE."));
+ mInputDevicePreference = CUBEB_DEVICE_PREF_VOICE;
+ CubebUtils::SetInCommunication(true);
+ } else {
+ mInputDevicePreference = CUBEB_DEVICE_PREF_ALL;
+ }
+
+ mMixer.AddCallback(WrapNotNull(this));
+}
+
+AudioCallbackDriver::~AudioCallbackDriver() {
+ if (mInputDevicePreference == CUBEB_DEVICE_PREF_VOICE) {
+ CubebUtils::SetInCommunication(false);
+ }
+}
+
+bool IsMacbookOrMacbookAir() {
+#ifdef XP_MACOSX
+ size_t len = 0;
+ sysctlbyname("hw.model", NULL, &len, NULL, 0);
+ if (len) {
+ UniquePtr<char[]> model(new char[len]);
+ // This string can be
+ // MacBook%d,%d for a normal MacBook
+ // MacBookAir%d,%d for a Macbook Air
+ sysctlbyname("hw.model", model.get(), &len, NULL, 0);
+ char* substring = strstr(model.get(), "MacBook");
+ if (substring) {
+ const size_t offset = strlen("MacBook");
+ if (!strncmp(model.get() + offset, "Air", 3) ||
+ isdigit(model[offset + 1])) {
+ return true;
+ }
+ }
+ }
+#endif
+ return false;
+}
+
+void AudioCallbackDriver::Init() {
+ TRACE("AudioCallbackDriver::Init");
+ MOZ_ASSERT(OnCubebOperationThread());
+ MOZ_ASSERT(mAudioStreamState == AudioStreamState::Pending);
+ FallbackDriverState fallbackState = mFallbackDriverState;
+ if (fallbackState == FallbackDriverState::Stopped) {
+ // The graph has already stopped us.
+ return;
+ }
+ bool fromFallback = fallbackState == FallbackDriverState::Running;
+ cubeb* cubebContext = CubebUtils::GetCubebContext();
+ if (!cubebContext) {
+ NS_WARNING("Could not get cubeb context.");
+ LOG(LogLevel::Warning, ("%s: Could not get cubeb context", __func__));
+ mAudioStreamState = AudioStreamState::None;
+ if (!fromFallback) {
+ CubebUtils::ReportCubebStreamInitFailure(true);
+ FallbackToSystemClockDriver();
+ }
+ return;
+ }
+
+ cubeb_stream_params output;
+ cubeb_stream_params input;
+ bool firstStream = CubebUtils::GetFirstStream();
+
+ MOZ_ASSERT(!NS_IsMainThread(),
+ "This is blocking and should never run on the main thread.");
+
+ output.rate = mSampleRate;
+
+#ifdef MOZ_SAMPLE_TYPE_S16
+ MOZ_ASSERT(AUDIO_OUTPUT_FORMAT == AUDIO_FORMAT_S16);
+ output.format = CUBEB_SAMPLE_S16NE;
+#else
+ MOZ_ASSERT(AUDIO_OUTPUT_FORMAT == AUDIO_FORMAT_FLOAT32);
+ output.format = CUBEB_SAMPLE_FLOAT32NE;
+#endif
+
+ if (!mOutputChannelCount) {
+ LOG(LogLevel::Warning, ("Output number of channels is 0."));
+ mAudioStreamState = AudioStreamState::None;
+ if (!fromFallback) {
+ CubebUtils::ReportCubebStreamInitFailure(firstStream);
+ FallbackToSystemClockDriver();
+ }
+ return;
+ }
+
+ CubebUtils::AudioDeviceID forcedOutputDeviceId = nullptr;
+
+ char* forcedOutputDeviceName = CubebUtils::GetForcedOutputDevice();
+ if (forcedOutputDeviceName) {
+ RefPtr<CubebDeviceEnumerator> enumerator = Enumerator::GetInstance();
+ RefPtr<AudioDeviceInfo> device = enumerator->DeviceInfoFromName(
+ NS_ConvertUTF8toUTF16(forcedOutputDeviceName), EnumeratorSide::OUTPUT);
+ if (device && device->DeviceID()) {
+ forcedOutputDeviceId = device->DeviceID();
+ }
+ }
+
+ mBuffer = AudioCallbackBufferWrapper<AudioDataValue>(mOutputChannelCount);
+ mScratchBuffer =
+ SpillBuffer<AudioDataValue, WEBAUDIO_BLOCK_SIZE * 2>(mOutputChannelCount);
+
+ output.channels = mOutputChannelCount;
+ AudioConfig::ChannelLayout::ChannelMap channelMap =
+ AudioConfig::ChannelLayout(mOutputChannelCount).Map();
+
+ output.layout = static_cast<uint32_t>(channelMap);
+ output.prefs = CubebUtils::GetDefaultStreamPrefs(CUBEB_DEVICE_TYPE_OUTPUT);
+ if (mInputDevicePreference == CUBEB_DEVICE_PREF_VOICE &&
+ CubebUtils::RouteOutputAsVoice()) {
+ output.prefs |= static_cast<cubeb_stream_prefs>(CUBEB_STREAM_PREF_VOICE);
+ }
+
+ uint32_t latencyFrames = CubebUtils::GetCubebMTGLatencyInFrames(&output);
+
+ // Macbook and MacBook air don't have enough CPU to run very low latency
+ // MediaTrackGraphs, cap the minimal latency to 512 frames int this case.
+ if (IsMacbookOrMacbookAir()) {
+ latencyFrames = std::max((uint32_t)512, latencyFrames);
+ }
+
+ // On OSX, having a latency that is lower than 10ms is very common. It's
+ // not very useful when doing voice, because all the WebRTC code deal in 10ms
+ // chunks of audio. Take the first power of two above 10ms at the current
+ // rate in this case. It's probably 512, for common rates.
+#if defined(XP_MACOSX)
+ if (mInputDevicePreference == CUBEB_DEVICE_PREF_VOICE) {
+ if (latencyFrames < mSampleRate / 100) {
+ latencyFrames = mozilla::RoundUpPow2(mSampleRate / 100);
+ }
+ }
+#endif
+ LOG(LogLevel::Debug, ("Effective latency in frames: %d", latencyFrames));
+
+ input = output;
+ input.channels = mInputChannelCount;
+ input.layout = CUBEB_LAYOUT_UNDEFINED;
+ input.prefs = CubebUtils::GetDefaultStreamPrefs(CUBEB_DEVICE_TYPE_INPUT);
+ if (mInputDevicePreference == CUBEB_DEVICE_PREF_VOICE) {
+ input.prefs |= static_cast<cubeb_stream_prefs>(CUBEB_STREAM_PREF_VOICE);
+ }
+
+ cubeb_stream* stream = nullptr;
+ bool inputWanted = mInputChannelCount > 0;
+ CubebUtils::AudioDeviceID outputId = mOutputDeviceID;
+ CubebUtils::AudioDeviceID inputId = mInputDeviceID;
+
+ if (CubebUtils::CubebStreamInit(
+ cubebContext, &stream, "AudioCallbackDriver", inputId,
+ inputWanted ? &input : nullptr,
+ forcedOutputDeviceId ? forcedOutputDeviceId : outputId, &output,
+ latencyFrames, DataCallback_s, StateCallback_s, this) == CUBEB_OK) {
+ mAudioStream.own(stream);
+ DebugOnly<int> rv =
+ cubeb_stream_set_volume(mAudioStream, CubebUtils::GetVolumeScale());
+ NS_WARNING_ASSERTION(
+ rv == CUBEB_OK,
+ "Could not set the audio stream volume in GraphDriver.cpp");
+ CubebUtils::ReportCubebBackendUsed();
+ } else {
+ NS_WARNING(
+ "Could not create a cubeb stream for MediaTrackGraph, falling "
+ "back to a SystemClockDriver");
+ mAudioStreamState = AudioStreamState::None;
+ // Only report failures when we're not coming from a driver that was
+ // created itself as a fallback driver because of a previous audio driver
+ // failure.
+ if (!fromFallback) {
+ CubebUtils::ReportCubebStreamInitFailure(firstStream);
+ FallbackToSystemClockDriver();
+ }
+ return;
+ }
+
+#ifdef XP_MACOSX
+ PanOutputIfNeeded(inputWanted);
+#endif
+
+ cubeb_stream_register_device_changed_callback(
+ mAudioStream, AudioCallbackDriver::DeviceChangedCallback_s);
+
+ // No-op if MOZ_DUMP_AUDIO is not defined as an environment variable. This
+ // is intended for diagnosing issues, and only works if the content sandbox is
+ // disabled.
+ mInputStreamFile.Open("GraphDriverInput", input.channels, input.rate);
+ mOutputStreamFile.Open("GraphDriverOutput", output.channels, output.rate);
+
+ if (NS_WARN_IF(!StartStream())) {
+ LOG(LogLevel::Warning,
+ ("%p: AudioCallbackDriver couldn't start a cubeb stream.", Graph()));
+ return;
+ }
+
+ LOG(LogLevel::Debug, ("%p: AudioCallbackDriver started.", Graph()));
+}
+
+void AudioCallbackDriver::Start() {
+ MOZ_ASSERT(!IsStarted());
+ MOZ_ASSERT(mAudioStreamState == AudioStreamState::None);
+ MOZ_ASSERT_IF(PreviousDriver(), PreviousDriver()->InIteration());
+ mAudioStreamState = AudioStreamState::Pending;
+
+ if (mFallbackDriverState == FallbackDriverState::None) {
+ // Starting an audio driver could take a while. We start a system driver in
+ // the meantime so that the graph is kept running.
+ FallbackToSystemClockDriver();
+ }
+
+ if (mPreviousDriver) {
+ if (mPreviousDriver->AsAudioCallbackDriver()) {
+ LOG(LogLevel::Debug, ("Releasing audio driver off main thread."));
+ RefPtr<AsyncCubebTask> releaseEvent =
+ new AsyncCubebTask(mPreviousDriver->AsAudioCallbackDriver(),
+ AsyncCubebOperation::SHUTDOWN);
+ releaseEvent->Dispatch();
+ } else {
+ LOG(LogLevel::Debug,
+ ("Dropping driver reference for SystemClockDriver."));
+ MOZ_ASSERT(mPreviousDriver->AsSystemClockDriver());
+ }
+ mPreviousDriver = nullptr;
+ }
+
+ LOG(LogLevel::Debug, ("Starting new audio driver off main thread, "
+ "to ensure it runs after previous shutdown."));
+ RefPtr<AsyncCubebTask> initEvent =
+ new AsyncCubebTask(AsAudioCallbackDriver(), AsyncCubebOperation::INIT);
+ initEvent->Dispatch();
+}
+
+bool AudioCallbackDriver::StartStream() {
+ TRACE("AudioCallbackDriver::StartStream");
+ MOZ_ASSERT(!IsStarted() && OnCubebOperationThread());
+ // Set STARTING before cubeb_stream_start, since starting the cubeb stream
+ // can result in a callback (that may read mAudioStreamState) before
+ // mAudioStreamState would otherwise be set.
+ mAudioStreamState = AudioStreamState::Starting;
+ if (cubeb_stream_start(mAudioStream) != CUBEB_OK) {
+ NS_WARNING("Could not start cubeb stream for MTG.");
+ return false;
+ }
+
+ return true;
+}
+
+void AudioCallbackDriver::Stop() {
+ TRACE("AudioCallbackDriver::Stop");
+ MOZ_ASSERT(OnCubebOperationThread());
+ cubeb_stream_register_device_changed_callback(mAudioStream, nullptr);
+ if (cubeb_stream_stop(mAudioStream) != CUBEB_OK) {
+ NS_WARNING("Could not stop cubeb stream for MTG.");
+ } else {
+ mAudioStreamState = AudioStreamState::None;
+ }
+}
+
+void AudioCallbackDriver::Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ RefPtr<FallbackWrapper> fallback;
+ {
+ auto fallbackLock = mFallback.Lock();
+ fallback = fallbackLock.ref();
+ fallbackLock.ref() = nullptr;
+ }
+ if (fallback) {
+ LOG(LogLevel::Debug,
+ ("%p: Releasing fallback driver %p.", Graph(), fallback.get()));
+ fallback->Shutdown();
+ }
+
+ LOG(LogLevel::Debug,
+ ("%p: Releasing audio driver off main thread (GraphDriver::Shutdown).",
+ Graph()));
+
+ RefPtr<AsyncCubebTask> releaseEvent =
+ new AsyncCubebTask(this, AsyncCubebOperation::SHUTDOWN);
+ releaseEvent->DispatchAndSpinEventLoopUntilComplete(
+ "AudioCallbackDriver::Shutdown"_ns);
+}
+
+/* static */
+long AudioCallbackDriver::DataCallback_s(cubeb_stream* aStream, void* aUser,
+ const void* aInputBuffer,
+ void* aOutputBuffer, long aFrames) {
+ AudioCallbackDriver* driver = reinterpret_cast<AudioCallbackDriver*>(aUser);
+ return driver->DataCallback(static_cast<const AudioDataValue*>(aInputBuffer),
+ static_cast<AudioDataValue*>(aOutputBuffer),
+ aFrames);
+}
+
+/* static */
+void AudioCallbackDriver::StateCallback_s(cubeb_stream* aStream, void* aUser,
+ cubeb_state aState) {
+ AudioCallbackDriver* driver = reinterpret_cast<AudioCallbackDriver*>(aUser);
+ driver->StateCallback(aState);
+}
+
+/* static */
+void AudioCallbackDriver::DeviceChangedCallback_s(void* aUser) {
+ AudioCallbackDriver* driver = reinterpret_cast<AudioCallbackDriver*>(aUser);
+ driver->DeviceChangedCallback();
+}
+
+AudioCallbackDriver::AutoInCallback::AutoInCallback(
+ AudioCallbackDriver* aDriver)
+ : mDriver(aDriver) {
+ MOZ_ASSERT(mDriver->mAudioThreadIdInCb == std::thread::id());
+ mDriver->mAudioThreadIdInCb = std::this_thread::get_id();
+}
+
+AudioCallbackDriver::AutoInCallback::~AutoInCallback() {
+ MOZ_ASSERT(mDriver->mAudioThreadIdInCb == std::this_thread::get_id());
+ mDriver->mAudioThreadIdInCb = std::thread::id();
+}
+
+bool AudioCallbackDriver::CheckThreadIdChanged() {
+ ProfilerThreadId id = profiler_current_thread_id();
+ if (id != mAudioThreadId) {
+ mAudioThreadId = id;
+ return true;
+ }
+ return false;
+}
+
+long AudioCallbackDriver::DataCallback(const AudioDataValue* aInputBuffer,
+ AudioDataValue* aOutputBuffer,
+ long aFrames) {
+ if (!mSandboxed && CheckThreadIdChanged()) {
+ CallbackThreadRegistry::Get()->Register(mAudioThreadId,
+ "NativeAudioCallback");
+ }
+
+ if (mAudioStreamState.compareExchange(AudioStreamState::Starting,
+ AudioStreamState::Running)) {
+ LOG(LogLevel::Verbose, ("%p: AudioCallbackDriver %p First audio callback "
+ "close the Fallback driver",
+ Graph(), this));
+ }
+
+ FallbackDriverState fallbackState = mFallbackDriverState;
+ if (MOZ_UNLIKELY(fallbackState == FallbackDriverState::Running)) {
+ // Wait for the fallback driver to stop. Wake it up so it can stop if it's
+ // sleeping.
+ LOG(LogLevel::Verbose,
+ ("%p: AudioCallbackDriver %p Waiting for the Fallback driver to stop",
+ Graph(), this));
+ EnsureNextIteration();
+ PodZero(aOutputBuffer, aFrames * mOutputChannelCount);
+ return aFrames;
+ }
+
+ if (MOZ_UNLIKELY(fallbackState == FallbackDriverState::Stopped)) {
+ // We're supposed to stop.
+ PodZero(aOutputBuffer, aFrames * mOutputChannelCount);
+ if (!mSandboxed) {
+ CallbackThreadRegistry::Get()->Unregister(mAudioThreadId);
+ }
+ return aFrames - 1;
+ }
+
+ MOZ_ASSERT(ThreadRunning());
+ TRACE_AUDIO_CALLBACK_BUDGET(aFrames, mSampleRate);
+ TRACE("AudioCallbackDriver::DataCallback");
+
+#ifdef DEBUG
+ AutoInCallback aic(this);
+#endif
+
+ uint32_t durationMS = aFrames * 1000 / mSampleRate;
+
+ // For now, simply average the duration with the previous
+ // duration so there is some damping against sudden changes.
+ if (!mIterationDurationMS) {
+ mIterationDurationMS = durationMS;
+ } else {
+ mIterationDurationMS = (mIterationDurationMS * 3) + durationMS;
+ mIterationDurationMS /= 4;
+ }
+
+ mBuffer.SetBuffer(aOutputBuffer, aFrames);
+ // fill part or all with leftover data from last iteration (since we
+ // align to Audio blocks)
+ uint32_t alreadyBuffered = mScratchBuffer.Empty(mBuffer);
+
+ // State computed time is decided by the audio callback's buffer length. We
+ // compute the iteration start and end from there, trying to keep the amount
+ // of buffering in the graph constant.
+ GraphTime nextStateComputedTime =
+ MediaTrackGraphImpl::RoundUpToEndOfAudioBlock(mStateComputedTime +
+ mBuffer.Available());
+
+ mIterationStart = mIterationEnd;
+ // inGraph is the number of audio frames there is between the state time and
+ // the current time, i.e. the maximum theoretical length of the interval we
+ // could use as [mIterationStart; mIterationEnd].
+ GraphTime inGraph = mStateComputedTime - mIterationStart;
+ // We want the interval [mIterationStart; mIterationEnd] to be before the
+ // interval [mStateComputedTime; nextStateComputedTime]. We also want
+ // the distance between these intervals to be roughly equivalent each time, to
+ // ensure there is no clock drift between current time and state time. Since
+ // we can't act on the state time because we have to fill the audio buffer, we
+ // reclock the current time against the state time, here.
+ mIterationEnd = mIterationStart + 0.8 * inGraph;
+
+ LOG(LogLevel::Verbose,
+ ("%p: interval[%ld; %ld] state[%ld; %ld] (frames: %ld) (durationMS: %u) "
+ "(duration ticks: %ld)",
+ Graph(), (long)mIterationStart, (long)mIterationEnd,
+ (long)mStateComputedTime, (long)nextStateComputedTime, (long)aFrames,
+ (uint32_t)durationMS,
+ (long)(nextStateComputedTime - mStateComputedTime)));
+
+ if (mStateComputedTime < mIterationEnd) {
+ LOG(LogLevel::Error, ("%p: Media graph global underrun detected", Graph()));
+ MOZ_ASSERT_UNREACHABLE("We should not underrun in full duplex");
+ mIterationEnd = mStateComputedTime;
+ }
+
+ // Process mic data if any/needed
+ if (aInputBuffer && mInputChannelCount > 0) {
+ Graph()->NotifyInputData(aInputBuffer, static_cast<size_t>(aFrames),
+ mSampleRate, mInputChannelCount, alreadyBuffered);
+ }
+
+ IterationResult result =
+ Graph()->OneIteration(nextStateComputedTime, mIterationEnd, &mMixer);
+
+ mStateComputedTime = nextStateComputedTime;
+
+ MOZ_ASSERT(mBuffer.Available() == 0,
+ "The graph should have filled the buffer");
+
+ mBuffer.BufferFilled();
+
+#ifdef MOZ_SAMPLE_TYPE_FLOAT32
+ // Prevent returning NaN to the OS mixer, and propagating NaN into the reverse
+ // stream of the AEC.
+ NaNToZeroInPlace(aOutputBuffer, aFrames * mOutputChannelCount);
+#endif
+
+ // Callback any observers for the AEC speaker data. Note that one
+ // (maybe) of these will be full-duplex, the others will get their input
+ // data off separate cubeb callbacks. Take care with how stuff is
+ // removed/added to this list and TSAN issues, but input and output will
+ // use separate callback methods.
+ Graph()->NotifyOutputData(aOutputBuffer, static_cast<size_t>(aFrames),
+ mSampleRate, mOutputChannelCount);
+
+#ifdef XP_MACOSX
+ // This only happens when the output is on a macbookpro's external speaker,
+ // that are stereo, but let's just be safe.
+ if (mNeedsPanning && mOutputChannelCount == 2) {
+ // hard pan to the right
+ for (uint32_t i = 0; i < aFrames * 2; i += 2) {
+ aOutputBuffer[i + 1] += aOutputBuffer[i];
+ aOutputBuffer[i] = 0.0;
+ }
+ }
+#endif
+
+ // No-op if MOZ_DUMP_AUDIO is not defined as an environment variable
+ if (aInputBuffer) {
+ mInputStreamFile.Write(static_cast<const AudioDataValue*>(aInputBuffer),
+ aFrames * mInputChannelCount);
+ }
+ mOutputStreamFile.Write(static_cast<const AudioDataValue*>(aOutputBuffer),
+ aFrames * mOutputChannelCount);
+
+ if (result.IsStop()) {
+ if (mInputDeviceID) {
+ mGraphInterface->NotifyInputStopped();
+ }
+ // Signal that we have stopped.
+ result.Stopped();
+ // Update the flag before handing over the graph and going to drain.
+ mAudioStreamState = AudioStreamState::Stopping;
+ if (!mSandboxed) {
+ CallbackThreadRegistry::Get()->Unregister(mAudioThreadId);
+ }
+ return aFrames - 1;
+ }
+
+ if (GraphDriver* nextDriver = result.NextDriver()) {
+ LOG(LogLevel::Debug,
+ ("%p: Switching to %s driver.", Graph(),
+ nextDriver->AsAudioCallbackDriver() ? "audio" : "system"));
+ if (mInputDeviceID) {
+ mGraphInterface->NotifyInputStopped();
+ }
+ result.Switched();
+ mAudioStreamState = AudioStreamState::Stopping;
+ nextDriver->SetState(mIterationStart, mIterationEnd, mStateComputedTime);
+ nextDriver->Start();
+ if (!mSandboxed) {
+ CallbackThreadRegistry::Get()->Unregister(mAudioThreadId);
+ }
+ // Returning less than aFrames starts the draining and eventually stops the
+ // audio thread. This function will never get called again.
+ return aFrames - 1;
+ }
+
+ MOZ_ASSERT(result.IsStillProcessing());
+ return aFrames;
+}
+
+static const char* StateToString(cubeb_state aState) {
+ switch (aState) {
+ case CUBEB_STATE_STARTED:
+ return "STARTED";
+ case CUBEB_STATE_STOPPED:
+ return "STOPPED";
+ case CUBEB_STATE_DRAINED:
+ return "DRAINED";
+ case CUBEB_STATE_ERROR:
+ return "ERROR";
+ default:
+ MOZ_CRASH("Unexpected state!");
+ }
+}
+
+void AudioCallbackDriver::StateCallback(cubeb_state aState) {
+ MOZ_ASSERT(!InIteration());
+ LOG(LogLevel::Debug,
+ ("AudioCallbackDriver(%p) State: %s", this, StateToString(aState)));
+
+ if (aState == CUBEB_STATE_STARTED || aState == CUBEB_STATE_STOPPED) {
+ // Nothing to do for STARTED.
+ //
+ // For STOPPED, don't reset mAudioStreamState until after
+ // cubeb_stream_stop() returns, as wasapi_stream_stop() dispatches
+ // CUBEB_STATE_STOPPED before ensuring that data callbacks have finished.
+ // https://searchfox.org/mozilla-central/rev/f9beb753a84aa297713d1565dcd0c5e3c66e4174/media/libcubeb/src/cubeb_wasapi.cpp#3009,3012
+ return;
+ }
+
+ AudioStreamState streamState = mAudioStreamState;
+ if (streamState < AudioStreamState::Starting) {
+ // mAudioStream has already entered STOPPED, DRAINED, or ERROR.
+ // Don't reset a Pending state indicating that a task to destroy
+ // mAudioStream and init a new cubeb_stream has already been triggered.
+ return;
+ }
+
+ // Reset for DRAINED or ERROR.
+ streamState = mAudioStreamState.exchange(AudioStreamState::None);
+
+ if (aState == CUBEB_STATE_ERROR) {
+ // About to hand over control of the graph. Do not start a new driver if
+ // StateCallback() receives an error for this stream while the main thread
+ // or another driver has control of the graph.
+ if (streamState == AudioStreamState::Running) {
+ MOZ_ASSERT(!ThreadRunning());
+ if (mFallbackDriverState == FallbackDriverState::None) {
+ // Only switch to fallback if it's not already running. It could be
+ // running with the callback driver having started but not seen a single
+ // callback yet. I.e., handover from fallback to callback is not done.
+ if (mInputDeviceID) {
+#ifdef DEBUG
+ // No audio callback after an error. We're calling into the graph here
+ // so we need to be regarded as "in iteration".
+ AutoInCallback aic(this);
+#endif
+ mGraphInterface->NotifyInputStopped();
+ }
+ FallbackToSystemClockDriver();
+ }
+ }
+ }
+}
+
+void AudioCallbackDriver::MixerCallback(AudioDataValue* aMixedBuffer,
+ AudioSampleFormat aFormat,
+ uint32_t aChannels, uint32_t aFrames,
+ uint32_t aSampleRate) {
+ MOZ_ASSERT(InIteration());
+ uint32_t toWrite = mBuffer.Available();
+
+ if (!mBuffer.Available() && aFrames > 0) {
+ NS_WARNING("DataCallback buffer full, expect frame drops.");
+ }
+
+ MOZ_ASSERT(mBuffer.Available() <= aFrames);
+
+ mBuffer.WriteFrames(aMixedBuffer, mBuffer.Available());
+ MOZ_ASSERT(mBuffer.Available() == 0,
+ "Missing frames to fill audio callback's buffer.");
+
+ DebugOnly<uint32_t> written = mScratchBuffer.Fill(
+ aMixedBuffer + toWrite * aChannels, aFrames - toWrite);
+ NS_WARNING_ASSERTION(written == aFrames - toWrite, "Dropping frames.");
+};
+
+void AudioCallbackDriver::PanOutputIfNeeded(bool aMicrophoneActive) {
+#ifdef XP_MACOSX
+ TRACE("AudioCallbackDriver::PanOutputIfNeeded");
+ cubeb_device* out = nullptr;
+ int rv;
+ char name[128];
+ size_t length = sizeof(name);
+
+ rv = sysctlbyname("hw.model", name, &length, NULL, 0);
+ if (rv) {
+ return;
+ }
+
+ int major, minor;
+ for (uint32_t i = 0; i < length; i++) {
+ // skip the model name
+ if (isalpha(name[i])) {
+ continue;
+ }
+ sscanf(name + i, "%d,%d", &major, &minor);
+ break;
+ }
+
+ enum MacbookModel { MacBook, MacBookPro, MacBookAir, NotAMacbook };
+
+ MacbookModel model;
+
+ if (!strncmp(name, "MacBookPro", length)) {
+ model = MacBookPro;
+ } else if (strncmp(name, "MacBookAir", length)) {
+ model = MacBookAir;
+ } else if (strncmp(name, "MacBook", length)) {
+ model = MacBook;
+ } else {
+ model = NotAMacbook;
+ }
+ // For macbook pro before 2016 model (change of chassis), hard pan the audio
+ // to the right if the speakers are in use to avoid feedback.
+ if (model == MacBookPro && major <= 12) {
+ if (cubeb_stream_get_current_device(mAudioStream, &out) == CUBEB_OK) {
+ MOZ_ASSERT(out);
+ // Check if we are currently outputing sound on external speakers.
+ if (out->output_name && !strcmp(out->output_name, "ispk")) {
+ // Pan everything to the right speaker.
+ LOG(LogLevel::Debug, ("Using the built-in speakers, with%s audio input",
+ aMicrophoneActive ? "" : "out"));
+ mNeedsPanning = aMicrophoneActive;
+ } else {
+ LOG(LogLevel::Debug, ("Using an external output device"));
+ mNeedsPanning = false;
+ }
+ cubeb_stream_device_destroy(mAudioStream, out);
+ }
+ }
+#endif
+}
+
+void AudioCallbackDriver::DeviceChangedCallback() {
+ MOZ_ASSERT(!InIteration());
+ // Tell the audio engine the device has changed, it might want to reset some
+ // state.
+ Graph()->DeviceChanged();
+#ifdef XP_MACOSX
+ RefPtr<AudioCallbackDriver> self(this);
+ bool hasInput = mInputChannelCount;
+ NS_DispatchBackgroundTask(NS_NewRunnableFunction(
+ "PanOutputIfNeeded", [self{std::move(self)}, hasInput]() {
+ self->PanOutputIfNeeded(hasInput);
+ }));
+#endif
+}
+
+uint32_t AudioCallbackDriver::IterationDuration() {
+ MOZ_ASSERT(InIteration());
+ // The real fix would be to have an API in cubeb to give us the number. Short
+ // of that, we approximate it here. bug 1019507
+ return mIterationDurationMS;
+}
+
+void AudioCallbackDriver::EnsureNextIteration() {
+ if (mFallbackDriverState == FallbackDriverState::Running) {
+ auto fallback = mFallback.Lock();
+ if (fallback.ref()) {
+ fallback.ref()->EnsureNextIteration();
+ }
+ }
+}
+
+TimeDuration AudioCallbackDriver::AudioOutputLatency() {
+ TRACE("AudioCallbackDriver::AudioOutputLatency");
+ uint32_t latencyFrames;
+ int rv = cubeb_stream_get_latency(mAudioStream, &latencyFrames);
+ if (rv || mSampleRate == 0) {
+ return TimeDuration::FromSeconds(0.0);
+ }
+
+ return TimeDuration::FromSeconds(static_cast<double>(latencyFrames) /
+ mSampleRate);
+}
+
+bool AudioCallbackDriver::OnFallback() const {
+ MOZ_ASSERT(InIteration());
+ return mFallbackDriverState == FallbackDriverState::Running;
+}
+
+void AudioCallbackDriver::FallbackToSystemClockDriver() {
+ MOZ_ASSERT(!ThreadRunning());
+ MOZ_ASSERT(mAudioStreamState == AudioStreamState::None ||
+ mAudioStreamState == AudioStreamState::Pending);
+ MOZ_ASSERT(mFallbackDriverState == FallbackDriverState::None);
+ LOG(LogLevel::Debug,
+ ("%p: AudioCallbackDriver %p Falling back to SystemClockDriver.", Graph(),
+ this));
+ mFallbackDriverState = FallbackDriverState::Running;
+ mNextReInitBackoffStep =
+ TimeDuration::FromMilliseconds(AUDIO_INITIAL_FALLBACK_BACKOFF_STEP_MS);
+ mNextReInitAttempt = TimeStamp::Now() + mNextReInitBackoffStep;
+ auto fallback =
+ MakeRefPtr<FallbackWrapper>(Graph(), this, mSampleRate, mIterationStart,
+ mIterationEnd, mStateComputedTime);
+ {
+ auto driver = mFallback.Lock();
+ driver.ref() = fallback;
+ }
+ fallback->Start();
+}
+
+void AudioCallbackDriver::FallbackDriverStopped(GraphTime aIterationStart,
+ GraphTime aIterationEnd,
+ GraphTime aStateComputedTime,
+ FallbackDriverState aState) {
+ mIterationStart = aIterationStart;
+ mIterationEnd = aIterationEnd;
+ mStateComputedTime = aStateComputedTime;
+ mNextReInitAttempt = TimeStamp();
+ mNextReInitBackoffStep = TimeDuration();
+ {
+ auto fallback = mFallback.Lock();
+ MOZ_ASSERT(fallback.ref()->OnThread());
+ fallback.ref() = nullptr;
+ }
+
+ MOZ_ASSERT(aState == FallbackDriverState::None ||
+ aState == FallbackDriverState::Stopped);
+ MOZ_ASSERT_IF(aState == FallbackDriverState::None,
+ mAudioStreamState == AudioStreamState::Running);
+ mFallbackDriverState = aState;
+}
+
+void AudioCallbackDriver::MaybeStartAudioStream() {
+ AudioStreamState streamState = mAudioStreamState;
+ if (streamState != AudioStreamState::None) {
+ LOG(LogLevel::Verbose,
+ ("%p: AudioCallbackDriver %p Cannot re-init.", Graph(), this));
+ return;
+ }
+
+ TimeStamp now = TimeStamp::Now();
+ if (now < mNextReInitAttempt) {
+ LOG(LogLevel::Verbose,
+ ("%p: AudioCallbackDriver %p Not time to re-init yet. %.3fs left.",
+ Graph(), this, (mNextReInitAttempt - now).ToSeconds()));
+ return;
+ }
+
+ LOG(LogLevel::Debug, ("%p: AudioCallbackDriver %p Attempting to re-init "
+ "audio stream from fallback driver.",
+ Graph(), this));
+ mNextReInitBackoffStep = std::min(
+ mNextReInitBackoffStep * 2,
+ TimeDuration::FromMilliseconds(AUDIO_MAX_FALLBACK_BACKOFF_STEP_MS));
+ mNextReInitAttempt = now + mNextReInitBackoffStep;
+ Start();
+}
+
+} // namespace mozilla
+
+// avoid redefined macro in unified build
+#undef LOG
diff --git a/dom/media/GraphDriver.h b/dom/media/GraphDriver.h
new file mode 100644
index 0000000000..7c985eeca0
--- /dev/null
+++ b/dom/media/GraphDriver.h
@@ -0,0 +1,821 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GRAPHDRIVER_H_
+#define GRAPHDRIVER_H_
+
+#include "nsAutoRef.h"
+#include "nsIThread.h"
+#include "AudioBufferUtils.h"
+#include "AudioMixer.h"
+#include "AudioSegment.h"
+#include "SelfRef.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/dom/AudioContext.h"
+#include "mozilla/DataMutex.h"
+#include "mozilla/SharedThreadPool.h"
+#include "mozilla/StaticPtr.h"
+#include "WavDumper.h"
+
+#include <thread>
+
+struct cubeb_stream;
+
+template <>
+class nsAutoRefTraits<cubeb_stream> : public nsPointerRefTraits<cubeb_stream> {
+ public:
+ static void Release(cubeb_stream* aStream) { cubeb_stream_destroy(aStream); }
+};
+
+namespace mozilla {
+
+// A thread pool containing only one thread to execute the cubeb operations. We
+// should always use this thread to init, destroy, start, or stop cubeb streams,
+// to avoid data racing or deadlock issues across platforms.
+#define CUBEB_TASK_THREAD SharedThreadPool::Get("CubebOperation"_ns, 1)
+
+/**
+ * Assume we can run an iteration of the MediaTrackGraph loop in this much time
+ * or less.
+ * We try to run the control loop at this rate.
+ */
+static const int MEDIA_GRAPH_TARGET_PERIOD_MS = 10;
+
+/**
+ * Assume that we might miss our scheduled wakeup of the MediaTrackGraph by
+ * this much.
+ */
+static const int SCHEDULE_SAFETY_MARGIN_MS = 10;
+
+/**
+ * Try have this much audio buffered in streams and queued to the hardware.
+ * The maximum delay to the end of the next control loop
+ * is 2*MEDIA_GRAPH_TARGET_PERIOD_MS + SCHEDULE_SAFETY_MARGIN_MS.
+ * There is no point in buffering more audio than this in a stream at any
+ * given time (until we add processing).
+ * This is not optimal yet.
+ */
+static const int AUDIO_TARGET_MS =
+ 2 * MEDIA_GRAPH_TARGET_PERIOD_MS + SCHEDULE_SAFETY_MARGIN_MS;
+
+/**
+ * After starting a fallback driver, wait this long before attempting to re-init
+ * the audio stream the first time.
+ */
+static const int AUDIO_INITIAL_FALLBACK_BACKOFF_STEP_MS = 10;
+
+/**
+ * The backoff step duration for when to next attempt to re-init the audio
+ * stream is capped at this value.
+ */
+static const int AUDIO_MAX_FALLBACK_BACKOFF_STEP_MS = 1000;
+
+class AudioCallbackDriver;
+class GraphDriver;
+class MediaTrack;
+class OfflineClockDriver;
+class SystemClockDriver;
+
+namespace dom {
+enum class AudioContextOperation : uint8_t;
+}
+
+struct GraphInterface : public nsISupports {
+ /**
+ * Object returned from OneIteration() instructing the iterating GraphDriver
+ * what to do.
+ *
+ * - If the result is StillProcessing: keep the iterations coming.
+ * - If the result is Stop: the driver potentially updates its internal state
+ * and interacts with the graph (e.g., NotifyOutputData), then it must call
+ * Stopped() exactly once.
+ * - If the result is SwitchDriver: the driver updates internal state as for
+ * the Stop result, then it must call Switched() exactly once and start
+ * NextDriver().
+ */
+ class IterationResult final {
+ struct Undefined {};
+ struct StillProcessing {};
+ struct Stop {
+ explicit Stop(RefPtr<Runnable> aStoppedRunnable)
+ : mStoppedRunnable(std::move(aStoppedRunnable)) {}
+ Stop(const Stop&) = delete;
+ Stop(Stop&& aOther) noexcept
+ : mStoppedRunnable(std::move(aOther.mStoppedRunnable)) {}
+ ~Stop() { MOZ_ASSERT(!mStoppedRunnable); }
+ RefPtr<Runnable> mStoppedRunnable;
+ void Stopped() {
+ mStoppedRunnable->Run();
+ mStoppedRunnable = nullptr;
+ }
+ };
+ struct SwitchDriver {
+ SwitchDriver(RefPtr<GraphDriver> aDriver,
+ RefPtr<Runnable> aSwitchedRunnable)
+ : mDriver(std::move(aDriver)),
+ mSwitchedRunnable(std::move(aSwitchedRunnable)) {}
+ SwitchDriver(const SwitchDriver&) = delete;
+ SwitchDriver(SwitchDriver&& aOther) noexcept
+ : mDriver(std::move(aOther.mDriver)),
+ mSwitchedRunnable(std::move(aOther.mSwitchedRunnable)) {}
+ ~SwitchDriver() { MOZ_ASSERT(!mSwitchedRunnable); }
+ RefPtr<GraphDriver> mDriver;
+ RefPtr<Runnable> mSwitchedRunnable;
+ void Switched() {
+ mSwitchedRunnable->Run();
+ mSwitchedRunnable = nullptr;
+ }
+ };
+ Variant<Undefined, StillProcessing, Stop, SwitchDriver> mResult;
+
+ explicit IterationResult(StillProcessing&& aArg)
+ : mResult(std::move(aArg)) {}
+ explicit IterationResult(Stop&& aArg) : mResult(std::move(aArg)) {}
+ explicit IterationResult(SwitchDriver&& aArg) : mResult(std::move(aArg)) {}
+
+ public:
+ IterationResult() : mResult(Undefined()) {}
+ IterationResult(const IterationResult&) = delete;
+ IterationResult(IterationResult&&) = default;
+
+ IterationResult& operator=(const IterationResult&) = delete;
+ IterationResult& operator=(IterationResult&&) = default;
+
+ static IterationResult CreateStillProcessing() {
+ return IterationResult(StillProcessing());
+ }
+ static IterationResult CreateStop(RefPtr<Runnable> aStoppedRunnable) {
+ return IterationResult(Stop(std::move(aStoppedRunnable)));
+ }
+ static IterationResult CreateSwitchDriver(
+ RefPtr<GraphDriver> aDriver, RefPtr<Runnable> aSwitchedRunnable) {
+ return IterationResult(
+ SwitchDriver(std::move(aDriver), std::move(aSwitchedRunnable)));
+ }
+
+ bool IsStillProcessing() const { return mResult.is<StillProcessing>(); }
+ bool IsStop() const { return mResult.is<Stop>(); }
+ bool IsSwitchDriver() const { return mResult.is<SwitchDriver>(); }
+
+ void Stopped() {
+ MOZ_ASSERT(IsStop());
+ mResult.as<Stop>().Stopped();
+ }
+
+ GraphDriver* NextDriver() const {
+ if (!IsSwitchDriver()) {
+ return nullptr;
+ }
+ return mResult.as<SwitchDriver>().mDriver;
+ }
+
+ void Switched() {
+ MOZ_ASSERT(IsSwitchDriver());
+ mResult.as<SwitchDriver>().Switched();
+ }
+ };
+
+ /* Called on the graph thread when there is new output data for listeners.
+ * This is the mixed audio output of this MediaTrackGraph. */
+ virtual void NotifyOutputData(AudioDataValue* aBuffer, size_t aFrames,
+ TrackRate aRate, uint32_t aChannels) = 0;
+ /* Called on the graph thread after an AudioCallbackDriver with an input
+ * stream has stopped. */
+ virtual void NotifyInputStopped() = 0;
+ /* Called on the graph thread when there is new input data for listeners. This
+ * is the raw audio input for this MediaTrackGraph. */
+ virtual void NotifyInputData(const AudioDataValue* aBuffer, size_t aFrames,
+ TrackRate aRate, uint32_t aChannels,
+ uint32_t aAlreadyBuffered) = 0;
+ /* Called every time there are changes to input/output audio devices like
+ * plug/unplug etc. This can be called on any thread, and posts a message to
+ * the main thread so that it can post a message to the graph thread. */
+ virtual void DeviceChanged() = 0;
+ /* Called by GraphDriver to iterate the graph. Output from the graph gets
+ * mixed into aMixer, if it is non-null. */
+ virtual IterationResult OneIteration(GraphTime aStateComputedEnd,
+ GraphTime aIterationEnd,
+ AudioMixer* aMixer) = 0;
+#ifdef DEBUG
+ /* True if we're on aDriver's thread, or if we're on mGraphRunner's thread
+ * and mGraphRunner is currently run by aDriver. */
+ virtual bool InDriverIteration(const GraphDriver* aDriver) const = 0;
+#endif
+};
+
+/**
+ * A driver is responsible for the scheduling of the processing, the thread
+ * management, and give the different clocks to a MediaTrackGraph. This is an
+ * abstract base class. A MediaTrackGraph can be driven by an
+ * OfflineClockDriver, if the graph is offline, or a SystemClockDriver or an
+ * AudioCallbackDriver, if the graph is real time.
+ * A MediaTrackGraph holds an owning reference to its driver.
+ *
+ * The lifetime of drivers is a complicated affair. Here are the different
+ * scenarii that can happen:
+ *
+ * Starting a MediaTrackGraph with an AudioCallbackDriver
+ * - A new thread T is created, from the main thread.
+ * - On this thread T, cubeb is initialized if needed, and a cubeb_stream is
+ * created and started
+ * - The thread T posts a message to the main thread to terminate itself.
+ * - The graph runs off the audio thread
+ *
+ * Starting a MediaTrackGraph with a SystemClockDriver:
+ * - A new thread T is created from the main thread.
+ * - The graph runs off this thread.
+ *
+ * Switching from a SystemClockDriver to an AudioCallbackDriver:
+ * - At the end of the MTG iteration, the graph tells the current driver to
+ * switch to an AudioCallbackDriver, which is created and initialized on the
+ * graph thread.
+ * - At the end of the MTG iteration, the SystemClockDriver transfers its timing
+ * info and a reference to itself to the AudioCallbackDriver. It then starts
+ * the AudioCallbackDriver.
+ * - When the AudioCallbackDriver starts, it:
+ * - Starts a fallback SystemClockDriver that runs until the
+ * AudioCallbackDriver is running, in case it takes a long time to start (it
+ * could block on I/O, e.g., negotiating a bluetooth connection).
+ * - Checks if it has been switched from a SystemClockDriver, and if that is
+ * the case, sends a message to the main thread to shut the
+ * SystemClockDriver thread down.
+ * - When the AudioCallbackDriver is running, data callbacks are blocked. The
+ * fallback driver detects this in its callback and stops itself. The first
+ * DataCallback after the fallback driver had stopped goes through.
+ * - The graph now runs off an audio callback.
+ *
+ * Switching from an AudioCallbackDriver to a SystemClockDriver:
+ * - At the end of the MTG iteration, the graph tells the current driver to
+ * switch to a SystemClockDriver.
+ * - the AudioCallbackDriver transfers its timing info and a reference to itself
+ * to the SystemClockDriver. A new SystemClockDriver is started from the
+ * current audio thread.
+ * - When starting, the SystemClockDriver checks if it has been switched from an
+ * AudioCallbackDriver. If yes, it creates a new temporary thread to release
+ * the cubeb_streams. This temporary thread closes the cubeb_stream, and then
+ * dispatches a message to the main thread to be terminated.
+ * - The graph now runs off a normal thread.
+ *
+ * Two drivers cannot run at the same time for the same graph. The thread safety
+ * of the different members of drivers, and their access pattern is documented
+ * next to the members themselves.
+ */
+class GraphDriver {
+ public:
+ using IterationResult = GraphInterface::IterationResult;
+
+ GraphDriver(GraphInterface* aGraphInterface, GraphDriver* aPreviousDriver,
+ uint32_t aSampleRate);
+
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+ /* Start the graph, init the driver, start the thread.
+ * A driver cannot be started twice, it must be shutdown
+ * before being started again. */
+ virtual void Start() = 0;
+ /* Shutdown GraphDriver */
+ MOZ_CAN_RUN_SCRIPT virtual void Shutdown() = 0;
+ /* Rate at which the GraphDriver runs, in ms. This can either be user
+ * controlled (because we are using a {System,Offline}ClockDriver, and decide
+ * how often we want to wakeup/how much we want to process per iteration), or
+ * it can be indirectly set by the latency of the audio backend, and the
+ * number of buffers of this audio backend: say we have four buffers, and 40ms
+ * latency, we will get a callback approximately every 10ms. */
+ virtual uint32_t IterationDuration() = 0;
+ /*
+ * Signaled by the graph when it needs another iteration. Goes unhandled for
+ * GraphDrivers that are not able to sleep indefinitely (i.e., all drivers but
+ * ThreadedDriver). Can be called on any thread.
+ */
+ virtual void EnsureNextIteration() = 0;
+
+ /* Implement the switching of the driver and the necessary updates */
+ void SwitchToDriver(GraphDriver* aDriver);
+
+ // Those are simply for accessing the associated pointer. Graph thread only,
+ // or if one is not running, main thread.
+ GraphDriver* PreviousDriver();
+ void SetPreviousDriver(GraphDriver* aPreviousDriver);
+
+ virtual AudioCallbackDriver* AsAudioCallbackDriver() { return nullptr; }
+ virtual const AudioCallbackDriver* AsAudioCallbackDriver() const {
+ return nullptr;
+ }
+
+ virtual OfflineClockDriver* AsOfflineClockDriver() { return nullptr; }
+ virtual const OfflineClockDriver* AsOfflineClockDriver() const {
+ return nullptr;
+ }
+
+ virtual SystemClockDriver* AsSystemClockDriver() { return nullptr; }
+ virtual const SystemClockDriver* AsSystemClockDriver() const {
+ return nullptr;
+ }
+
+ /**
+ * Set the state of the driver so it can start at the right point in time,
+ * after switching from another driver.
+ */
+ void SetState(GraphTime aIterationStart, GraphTime aIterationEnd,
+ GraphTime aStateComputedTime);
+
+ GraphInterface* Graph() const { return mGraphInterface; }
+
+#ifdef DEBUG
+ // True if the current thread is currently iterating the MTG.
+ bool InIteration() const;
+#endif
+ // True if the current thread is the GraphDriver's thread.
+ virtual bool OnThread() const = 0;
+ // GraphDriver's thread has started and the thread is running.
+ virtual bool ThreadRunning() const = 0;
+
+ double MediaTimeToSeconds(GraphTime aTime) const {
+ NS_ASSERTION(aTime > -TRACK_TIME_MAX && aTime <= TRACK_TIME_MAX,
+ "Bad time");
+ return static_cast<double>(aTime) / mSampleRate;
+ }
+
+ GraphTime SecondsToMediaTime(double aS) const {
+ NS_ASSERTION(0 <= aS && aS <= TRACK_TICKS_MAX / TRACK_RATE_MAX,
+ "Bad seconds");
+ return mSampleRate * aS;
+ }
+
+ GraphTime MillisecondsToMediaTime(int32_t aMS) const {
+ return RateConvertTicksRoundDown(mSampleRate, 1000, aMS);
+ }
+
+ protected:
+ // Time of the start of this graph iteration.
+ GraphTime mIterationStart = 0;
+ // Time of the end of this graph iteration.
+ GraphTime mIterationEnd = 0;
+ // Time until which the graph has processed data.
+ GraphTime mStateComputedTime = 0;
+ // The GraphInterface this driver is currently iterating.
+ const RefPtr<GraphInterface> mGraphInterface;
+ // The sample rate for the graph, and in case of an audio driver, also for the
+ // cubeb stream.
+ const uint32_t mSampleRate;
+
+ // This is non-null only when this driver has recently switched from an other
+ // driver, and has not cleaned it up yet (for example because the audio stream
+ // is currently calling the callback during initialization).
+ //
+ // This is written to when changing driver, from the previous driver's thread,
+ // or a thread created for the occasion. This is read each time we need to
+ // check whether we're changing driver (in Switching()), from the graph
+ // thread.
+ // This must be accessed using the {Set,Get}PreviousDriver methods.
+ RefPtr<GraphDriver> mPreviousDriver;
+
+ virtual ~GraphDriver() = default;
+};
+
+class MediaTrackGraphInitThreadRunnable;
+
+/**
+ * This class is a driver that manages its own thread.
+ */
+class ThreadedDriver : public GraphDriver {
+ class IterationWaitHelper {
+ Monitor mMonitor MOZ_UNANNOTATED;
+ // The below members are guarded by mMonitor.
+ bool mNeedAnotherIteration = false;
+ TimeStamp mWakeTime;
+
+ public:
+ IterationWaitHelper() : mMonitor("IterationWaitHelper::mMonitor") {}
+
+ /**
+ * If another iteration is needed we wait for aDuration, otherwise we wait
+ * for a wake-up. If a wake-up occurs before aDuration time has passed, we
+ * wait for aDuration nonetheless.
+ */
+ void WaitForNextIterationAtLeast(TimeDuration aDuration) {
+ MonitorAutoLock lock(mMonitor);
+ TimeStamp now = TimeStamp::Now();
+ mWakeTime = now + aDuration;
+ while (true) {
+ if (mNeedAnotherIteration && now >= mWakeTime) {
+ break;
+ }
+ if (mNeedAnotherIteration) {
+ lock.Wait(mWakeTime - now);
+ } else {
+ lock.Wait(TimeDuration::Forever());
+ }
+ now = TimeStamp::Now();
+ }
+ mWakeTime = TimeStamp();
+ mNeedAnotherIteration = false;
+ }
+
+ /**
+ * Sets mNeedAnotherIteration to true and notifies the monitor, in case a
+ * driver is currently waiting.
+ */
+ void EnsureNextIteration() {
+ MonitorAutoLock lock(mMonitor);
+ mNeedAnotherIteration = true;
+ lock.Notify();
+ }
+ };
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ThreadedDriver, override);
+
+ ThreadedDriver(GraphInterface* aGraphInterface, GraphDriver* aPreviousDriver,
+ uint32_t aSampleRate);
+
+ void EnsureNextIteration() override;
+ void Start() override;
+ MOZ_CAN_RUN_SCRIPT void Shutdown() override;
+ /**
+ * Runs main control loop on the graph thread. Normally a single invocation
+ * of this runs for the entire lifetime of the graph thread.
+ */
+ virtual void RunThread();
+ friend class MediaTrackGraphInitThreadRunnable;
+ uint32_t IterationDuration() override { return MEDIA_GRAPH_TARGET_PERIOD_MS; }
+
+ nsIThread* Thread() const { return mThread; }
+
+ bool OnThread() const override {
+ return !mThread || mThread->IsOnCurrentThread();
+ }
+
+ bool ThreadRunning() const override { return mThreadRunning; }
+
+ protected:
+ /* Waits until it's time to process more data. */
+ void WaitForNextIteration();
+ /* Implementation dependent time the ThreadedDriver should wait between
+ * iterations. */
+ virtual TimeDuration WaitInterval() = 0;
+ /* When the graph wakes up to do an iteration, implementations return the
+ * range of time that will be processed. This is called only once per
+ * iteration; it may determine the interval from state in a previous
+ * call. */
+ virtual MediaTime GetIntervalForIteration() = 0;
+
+ virtual ~ThreadedDriver();
+
+ nsCOMPtr<nsIThread> mThread;
+
+ private:
+ // This is true if the thread is running. It is false
+ // before starting the thread and after stopping it.
+ Atomic<bool> mThreadRunning;
+
+ // Any thread.
+ IterationWaitHelper mWaitHelper;
+};
+
+/**
+ * A SystemClockDriver drives a GraphInterface using a system clock, and waits
+ * using a monitor, between each iteration.
+ */
+class SystemClockDriver : public ThreadedDriver {
+ public:
+ SystemClockDriver(GraphInterface* aGraphInterface,
+ GraphDriver* aPreviousDriver, uint32_t aSampleRate);
+ virtual ~SystemClockDriver();
+ SystemClockDriver* AsSystemClockDriver() override { return this; }
+ const SystemClockDriver* AsSystemClockDriver() const override { return this; }
+
+ protected:
+ /* Return the TimeDuration to wait before the next rendering iteration. */
+ TimeDuration WaitInterval() override;
+ MediaTime GetIntervalForIteration() override;
+
+ private:
+ // Those are only modified (after initialization) on the graph thread. The
+ // graph thread does not run during the initialization.
+ TimeStamp mInitialTimeStamp;
+ TimeStamp mCurrentTimeStamp;
+ TimeStamp mLastTimeStamp;
+};
+
+/**
+ * An OfflineClockDriver runs the graph as fast as possible, without waiting
+ * between iteration.
+ */
+class OfflineClockDriver : public ThreadedDriver {
+ public:
+ OfflineClockDriver(GraphInterface* aGraphInterface, uint32_t aSampleRate,
+ GraphTime aSlice);
+ virtual ~OfflineClockDriver();
+ OfflineClockDriver* AsOfflineClockDriver() override { return this; }
+ const OfflineClockDriver* AsOfflineClockDriver() const override {
+ return this;
+ }
+
+ void RunThread() override;
+
+ protected:
+ TimeDuration WaitInterval() override { return TimeDuration(); }
+ MediaTime GetIntervalForIteration() override;
+
+ private:
+ // Time, in GraphTime, for each iteration
+ GraphTime mSlice;
+};
+
+struct TrackAndPromiseForOperation {
+ TrackAndPromiseForOperation(
+ MediaTrack* aTrack, dom::AudioContextOperation aOperation,
+ AbstractThread* aMainThread,
+ MozPromiseHolder<MediaTrackGraph::AudioContextOperationPromise>&&
+ aHolder);
+ TrackAndPromiseForOperation(TrackAndPromiseForOperation&& aOther) noexcept;
+ RefPtr<MediaTrack> mTrack;
+ dom::AudioContextOperation mOperation;
+ RefPtr<AbstractThread> mMainThread;
+ MozPromiseHolder<MediaTrackGraph::AudioContextOperationPromise> mHolder;
+};
+
+enum class AsyncCubebOperation { INIT, SHUTDOWN };
+enum class AudioInputType { Unknown, Voice };
+
+/**
+ * This is a graph driver that is based on callback functions called by the
+ * audio api. This ensures minimal audio latency, because it means there is no
+ * buffering happening: the audio is generated inside the callback.
+ *
+ * This design is less flexible than running our own thread:
+ * - We have no control over the thread:
+ * - It cannot block, and it has to run for a shorter amount of time than the
+ * buffer it is going to fill, or an under-run is going to occur (short burst
+ * of silence in the final audio output).
+ * - We can't know for sure when the callback function is going to be called
+ * (although we compute an estimation so we can schedule video frames)
+ * - Creating and shutting the thread down is a blocking operation, that can
+ * take _seconds_ in some cases (because IPC has to be set up, and
+ * sometimes hardware components are involved and need to be warmed up)
+ * - We have no control on how much audio we generate, we have to return exactly
+ * the number of frames asked for by the callback. Since for the Web Audio
+ * API, we have to do block processing at 128 frames per block, we need to
+ * keep a little spill buffer to store the extra frames.
+ */
+class AudioCallbackDriver : public GraphDriver, public MixerCallbackReceiver {
+ using IterationResult = GraphInterface::IterationResult;
+ enum class FallbackDriverState;
+ class FallbackWrapper;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AudioCallbackDriver, override);
+
+ /** If aInputChannelCount is zero, then this driver is output-only. */
+ AudioCallbackDriver(GraphInterface* aGraphInterface,
+ GraphDriver* aPreviousDriver, uint32_t aSampleRate,
+ uint32_t aOutputChannelCount, uint32_t aInputChannelCount,
+ CubebUtils::AudioDeviceID aOutputDeviceID,
+ CubebUtils::AudioDeviceID aInputDeviceID,
+ AudioInputType aAudioInputType);
+
+ void Start() override;
+ MOZ_CAN_RUN_SCRIPT void Shutdown() override;
+
+ /* Static wrapper function cubeb calls back. */
+ static long DataCallback_s(cubeb_stream* aStream, void* aUser,
+ const void* aInputBuffer, void* aOutputBuffer,
+ long aFrames);
+ static void StateCallback_s(cubeb_stream* aStream, void* aUser,
+ cubeb_state aState);
+ static void DeviceChangedCallback_s(void* aUser);
+
+ /* This function is called by the underlying audio backend when a refill is
+ * needed. This is what drives the whole graph when it is used to output
+ * audio. If the return value is exactly aFrames, this function will get
+ * called again. If it is less than aFrames, the stream will go in draining
+ * mode, and this function will not be called again. */
+ long DataCallback(const AudioDataValue* aInputBuffer,
+ AudioDataValue* aOutputBuffer, long aFrames);
+ /* This function is called by the underlying audio backend, but is only used
+ * for informational purposes at the moment. */
+ void StateCallback(cubeb_state aState);
+ /* This is an approximation of the number of millisecond there are between two
+ * iterations of the graph. */
+ uint32_t IterationDuration() override;
+ /* If the audio stream has started, this does nothing. There will be another
+ * iteration. If there is an active fallback driver, we forward the call so it
+ * can wake up. */
+ void EnsureNextIteration() override;
+
+ /* This function gets called when the graph has produced the audio frames for
+ * this iteration. */
+ void MixerCallback(AudioDataValue* aMixedBuffer, AudioSampleFormat aFormat,
+ uint32_t aChannels, uint32_t aFrames,
+ uint32_t aSampleRate) override;
+
+ AudioCallbackDriver* AsAudioCallbackDriver() override { return this; }
+ const AudioCallbackDriver* AsAudioCallbackDriver() const override {
+ return this;
+ }
+
+ uint32_t OutputChannelCount() { return mOutputChannelCount; }
+
+ uint32_t InputChannelCount() { return mInputChannelCount; }
+
+ AudioInputType InputDevicePreference() {
+ if (mInputDevicePreference == CUBEB_DEVICE_PREF_VOICE) {
+ return AudioInputType::Voice;
+ }
+ return AudioInputType::Unknown;
+ }
+
+ std::thread::id ThreadId() const { return mAudioThreadIdInCb.load(); }
+
+ /* Called when the thread servicing the callback has changed. This can be
+ * fairly expensive */
+ void OnThreadIdChanged();
+ /* Called at the beginning of the audio callback to check if the thread id has
+ * changed. */
+ bool CheckThreadIdChanged();
+
+ bool OnThread() const override {
+ return mAudioThreadIdInCb.load() == std::this_thread::get_id();
+ }
+
+ /* Returns true if this audio callback driver has successfully started and not
+ * yet stopped. If the fallback driver is active, this returns false. */
+ bool ThreadRunning() const override {
+ return mAudioStreamState == AudioStreamState::Running;
+ }
+
+ /* Whether the underlying cubeb stream has been started and has not stopped
+ * or errored. */
+ bool IsStarted() { return mAudioStreamState > AudioStreamState::Starting; };
+
+ // Returns the output latency for the current audio output stream.
+ TimeDuration AudioOutputLatency();
+
+ /* Returns true if this driver is currently driven by the fallback driver. */
+ bool OnFallback() const;
+
+ private:
+ /**
+ * On certain MacBookPro, the microphone is located near the left speaker.
+ * We need to pan the sound output to the right speaker if we are using the
+ * mic and the built-in speaker, or we will have terrible echo. */
+ void PanOutputIfNeeded(bool aMicrophoneActive);
+ /**
+ * This is called when the output device used by the cubeb stream changes. */
+ void DeviceChangedCallback();
+ /* Start the cubeb stream */
+ bool StartStream();
+ friend class AsyncCubebTask;
+ void Init();
+ void Stop();
+ /**
+ * Fall back to a SystemClockDriver using a normal thread. If needed,
+ * the graph will try to re-open an audio stream later. */
+ void FallbackToSystemClockDriver();
+ /* Called by the fallback driver when it has fully stopped, after finishing
+ * its last iteration. If it stopped after the audio stream started, aState
+ * will be None. If it stopped after the graph told it to stop, or switch,
+ * aState will be Stopped. Hands over state to the audio driver that may
+ * iterate the graph after this has been called. */
+ void FallbackDriverStopped(GraphTime aIterationStart, GraphTime aIterationEnd,
+ GraphTime aStateComputedTime,
+ FallbackDriverState aState);
+
+ /* Called at the end of the fallback driver's iteration to see whether we
+ * should attempt to start the AudioStream again. */
+ void MaybeStartAudioStream();
+
+ /* This is true when the method is executed on CubebOperation thread pool. */
+ bool OnCubebOperationThread() {
+ return mInitShutdownThread->IsOnCurrentThreadInfallible();
+ }
+
+ /* MediaTrackGraphs are always down/up mixed to output channels. */
+ const uint32_t mOutputChannelCount;
+ /* The size of this buffer comes from the fact that some audio backends can
+ * call back with a number of frames lower than one block (128 frames), so we
+ * need to keep at most two block in the SpillBuffer, because we always round
+ * up to block boundaries during an iteration.
+ * This is only ever accessed on the audio callback thread. */
+ SpillBuffer<AudioDataValue, WEBAUDIO_BLOCK_SIZE * 2> mScratchBuffer;
+ /* Wrapper to ensure we write exactly the number of frames we need in the
+ * audio buffer cubeb passes us. This is only ever accessed on the audio
+ * callback thread. */
+ AudioCallbackBufferWrapper<AudioDataValue> mBuffer;
+ /* cubeb stream for this graph. This is non-null after a successful
+ * cubeb_stream_init(). CubebOperation thread only. */
+ nsAutoRef<cubeb_stream> mAudioStream;
+ /* The number of input channels from cubeb. Set before opening cubeb. If it is
+ * zero then the driver is output-only. */
+ const uint32_t mInputChannelCount;
+ /**
+ * Devices to use for cubeb input & output, or nullptr for default device.
+ */
+ const CubebUtils::AudioDeviceID mOutputDeviceID;
+ const CubebUtils::AudioDeviceID mInputDeviceID;
+ /* Approximation of the time between two callbacks. This is used to schedule
+ * video frames. This is in milliseconds. Only even used (after
+ * inizatialization) on the audio callback thread. */
+ uint32_t mIterationDurationMS;
+
+ struct AutoInCallback {
+ explicit AutoInCallback(AudioCallbackDriver* aDriver);
+ ~AutoInCallback();
+ AudioCallbackDriver* mDriver;
+ };
+
+ /* Shared thread pool with up to one thread for off-main-thread
+ * initialization and shutdown of the audio stream via AsyncCubebTask. */
+ const RefPtr<SharedThreadPool> mInitShutdownThread;
+ cubeb_device_pref mInputDevicePreference;
+ /* The mixer that the graph mixes into during an iteration. Audio thread only.
+ */
+ AudioMixer mMixer;
+ /* Contains the id of the audio thread, from profiler_current_thread_id. */
+ std::atomic<ProfilerThreadId> mAudioThreadId;
+ /* This allows implementing AutoInCallback. This is equal to the current
+ * thread id when in an audio callback, and is an invalid thread id otherwise.
+ */
+ std::atomic<std::thread::id> mAudioThreadIdInCb;
+ /* State of the audio stream, see inline comments. */
+ enum class AudioStreamState {
+ /* There is no cubeb_stream or mAudioStream is in CUBEB_STATE_ERROR or
+ * CUBEB_STATE_STOPPED and no pending AsyncCubebTask exists to INIT a new
+ * cubeb_stream. */
+ None,
+ /* An AsyncCubebTask to INIT a new cubeb_stream is pending. */
+ Pending,
+ /* cubeb_start_stream() is about to be or has been called on mAudioStream.
+ * Any previous cubeb_streams have been destroyed. */
+ Starting,
+ /* mAudioStream is running. */
+ Running,
+ /* mAudioStream is draining, and will soon stop. */
+ Stopping
+ };
+ Atomic<AudioStreamState> mAudioStreamState{AudioStreamState::None};
+ /* State of the fallback driver, see inline comments. */
+ enum class FallbackDriverState {
+ /* There is no fallback driver. */
+ None,
+ /* There is a fallback driver trying to iterate us. */
+ Running,
+ /* There was a fallback driver and the graph stopped it. No audio callback
+ may iterate the graph. */
+ Stopped,
+ };
+ Atomic<FallbackDriverState> mFallbackDriverState{FallbackDriverState::None};
+ /* SystemClockDriver used as fallback if this AudioCallbackDriver fails to
+ * init or start. */
+ DataMutex<RefPtr<FallbackWrapper>> mFallback;
+ /* If using a fallback driver, this is the duration to wait after failing to
+ * start it before attempting to start it again. */
+ TimeDuration mNextReInitBackoffStep;
+ /* If using a fallback driver, this is the next time we'll try to start the
+ * audio stream. */
+ TimeStamp mNextReInitAttempt;
+#ifdef XP_MACOSX
+ /* When using the built-in speakers on macbook pro (13 and 15, all models),
+ * it's best to hard pan the audio on the right, to avoid feedback into the
+ * microphone that is located next to the left speaker. */
+ Atomic<bool> mNeedsPanning;
+#endif
+
+ WavDumper mInputStreamFile;
+ WavDumper mOutputStreamFile;
+
+ virtual ~AudioCallbackDriver();
+ const bool mSandboxed = false;
+};
+
+class AsyncCubebTask : public Runnable {
+ public:
+ AsyncCubebTask(AudioCallbackDriver* aDriver, AsyncCubebOperation aOperation);
+
+ nsresult Dispatch(uint32_t aFlags = NS_DISPATCH_NORMAL) {
+ return mDriver->mInitShutdownThread->Dispatch(this, aFlags);
+ }
+
+ nsresult DispatchAndSpinEventLoopUntilComplete(
+ const nsACString& aVeryGoodReasonToDoThis) {
+ return NS_DispatchAndSpinEventLoopUntilComplete(
+ aVeryGoodReasonToDoThis, mDriver->mInitShutdownThread, do_AddRef(this));
+ }
+
+ protected:
+ virtual ~AsyncCubebTask();
+
+ private:
+ NS_IMETHOD Run() final;
+
+ RefPtr<AudioCallbackDriver> mDriver;
+ AsyncCubebOperation mOperation;
+ RefPtr<GraphInterface> mShutdownGrip;
+};
+
+} // namespace mozilla
+
+#endif // GRAPHDRIVER_H_
diff --git a/dom/media/GraphRunner.cpp b/dom/media/GraphRunner.cpp
new file mode 100644
index 0000000000..850f423790
--- /dev/null
+++ b/dom/media/GraphRunner.cpp
@@ -0,0 +1,176 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#include "GraphRunner.h"
+
+#include "GraphDriver.h"
+#include "MediaTrackGraph.h"
+#include "MediaTrackGraphImpl.h"
+#include "nsISupportsImpl.h"
+#include "nsISupportsPriority.h"
+#include "prthread.h"
+#include "Tracing.h"
+#include "audio_thread_priority.h"
+#ifdef MOZ_WIDGET_ANDROID
+# include "AndroidProcess.h"
+#endif // MOZ_WIDGET_ANDROID
+
+namespace mozilla {
+
+GraphRunner::GraphRunner(MediaTrackGraphImpl* aGraph,
+ already_AddRefed<nsIThread> aThread)
+ : Runnable("GraphRunner"),
+ mMonitor("GraphRunner::mMonitor"),
+ mGraph(aGraph),
+ mThreadState(ThreadState::Wait),
+ mThread(aThread) {
+ mThread->Dispatch(do_AddRef(this));
+}
+
+GraphRunner::~GraphRunner() {
+ MOZ_ASSERT(mThreadState == ThreadState::Shutdown);
+}
+
+/* static */
+already_AddRefed<GraphRunner> GraphRunner::Create(MediaTrackGraphImpl* aGraph) {
+ nsCOMPtr<nsIThread> thread;
+ if (NS_WARN_IF(NS_FAILED(
+ NS_NewNamedThread("GraphRunner", getter_AddRefs(thread))))) {
+ return nullptr;
+ }
+ nsCOMPtr<nsISupportsPriority> supportsPriority = do_QueryInterface(thread);
+ MOZ_ASSERT(supportsPriority);
+ MOZ_ALWAYS_SUCCEEDS(
+ supportsPriority->SetPriority(nsISupportsPriority::PRIORITY_HIGHEST));
+
+ return do_AddRef(new GraphRunner(aGraph, thread.forget()));
+}
+
+void GraphRunner::Shutdown() {
+ {
+ MonitorAutoLock lock(mMonitor);
+ MOZ_ASSERT(mThreadState == ThreadState::Wait);
+ mThreadState = ThreadState::Shutdown;
+ mMonitor.Notify();
+ }
+ mThread->Shutdown();
+}
+
+auto GraphRunner::OneIteration(GraphTime aStateTime, GraphTime aIterationEnd,
+ AudioMixer* aMixer) -> IterationResult {
+ TRACE("GraphRunner::OneIteration");
+
+ MonitorAutoLock lock(mMonitor);
+ MOZ_ASSERT(mThreadState == ThreadState::Wait);
+ mIterationState = Some(IterationState(aStateTime, aIterationEnd, aMixer));
+
+#ifdef DEBUG
+ if (const auto* audioDriver =
+ mGraph->CurrentDriver()->AsAudioCallbackDriver()) {
+ mAudioDriverThreadId = audioDriver->ThreadId();
+ } else if (const auto* clockDriver =
+ mGraph->CurrentDriver()->AsSystemClockDriver()) {
+ mClockDriverThread = clockDriver->Thread();
+ } else {
+ MOZ_CRASH("Unknown GraphDriver");
+ }
+#endif
+ // Signal that mIterationState was updated
+ mThreadState = ThreadState::Run;
+ mMonitor.Notify();
+ // Wait for mIterationResult to update
+ do {
+ mMonitor.Wait();
+ } while (mThreadState == ThreadState::Run);
+
+#ifdef DEBUG
+ mAudioDriverThreadId = std::thread::id();
+ mClockDriverThread = nullptr;
+#endif
+
+ mIterationState = Nothing();
+
+ IterationResult result = std::move(mIterationResult);
+ mIterationResult = IterationResult();
+ return result;
+}
+
+#ifdef MOZ_WIDGET_ANDROID
+namespace {
+void PromoteRenderingThreadAndroid() {
+ MOZ_LOG(gMediaTrackGraphLog, LogLevel::Debug,
+ ("GraphRunner default thread priority: %d",
+ java::sdk::Process::GetThreadPriority(java::sdk::Process::MyTid())));
+ java::sdk::Process::SetThreadPriority(
+ java::sdk::Process::THREAD_PRIORITY_URGENT_AUDIO);
+ MOZ_LOG(gMediaTrackGraphLog, LogLevel::Debug,
+ ("GraphRunner promoted thread priority: %d",
+ java::sdk::Process::GetThreadPriority(java::sdk::Process::MyTid())));
+}
+}; // namespace
+#endif // MOZ_WIDGET_ANDROID
+
+NS_IMETHODIMP GraphRunner::Run() {
+#ifndef XP_LINUX
+ atp_handle* handle =
+ atp_promote_current_thread_to_real_time(0, mGraph->GraphRate());
+#endif
+
+#ifdef MOZ_WIDGET_ANDROID
+ PromoteRenderingThreadAndroid();
+#endif // MOZ_WIDGET_ANDROID
+
+ nsCOMPtr<nsIThreadInternal> threadInternal = do_QueryInterface(mThread);
+ threadInternal->SetObserver(mGraph);
+
+ MonitorAutoLock lock(mMonitor);
+ while (true) {
+ while (mThreadState == ThreadState::Wait) {
+ mMonitor.Wait(); // Wait for mIterationState to update or for shutdown
+ }
+ if (mThreadState == ThreadState::Shutdown) {
+ break;
+ }
+ MOZ_DIAGNOSTIC_ASSERT(mIterationState.isSome());
+ TRACE("GraphRunner::Run");
+ mIterationResult = mGraph->OneIterationImpl(mIterationState->StateTime(),
+ mIterationState->IterationEnd(),
+ mIterationState->Mixer());
+ // Signal that mIterationResult was updated
+ mThreadState = ThreadState::Wait;
+ mMonitor.Notify();
+ }
+
+#ifndef XP_LINUX
+ if (handle) {
+ atp_demote_current_thread_from_real_time(handle);
+ }
+#endif
+
+ return NS_OK;
+}
+
+bool GraphRunner::OnThread() const { return mThread->IsOnCurrentThread(); }
+
+#ifdef DEBUG
+bool GraphRunner::InDriverIteration(const GraphDriver* aDriver) const {
+ if (!OnThread()) {
+ return false;
+ }
+
+ if (const auto* audioDriver = aDriver->AsAudioCallbackDriver()) {
+ return audioDriver->ThreadId() == mAudioDriverThreadId;
+ }
+
+ if (const auto* clockDriver = aDriver->AsSystemClockDriver()) {
+ return clockDriver->Thread() == mClockDriverThread;
+ }
+
+ MOZ_CRASH("Unknown driver");
+}
+#endif
+
+} // namespace mozilla
diff --git a/dom/media/GraphRunner.h b/dom/media/GraphRunner.h
new file mode 100644
index 0000000000..9d98ef07b2
--- /dev/null
+++ b/dom/media/GraphRunner.h
@@ -0,0 +1,121 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_GraphRunner_h
+#define mozilla_GraphRunner_h
+
+#include "GraphDriver.h"
+#include "MediaSegment.h"
+#include "mozilla/Monitor.h"
+
+#include <thread>
+
+struct PRThread;
+
+namespace mozilla {
+
+class AudioMixer;
+class MediaTrackGraphImpl;
+
+class GraphRunner final : public Runnable {
+ using IterationResult = GraphInterface::IterationResult;
+
+ public:
+ static already_AddRefed<GraphRunner> Create(MediaTrackGraphImpl* aGraph);
+
+ /**
+ * Marks us as shut down and signals mThread, so that it runs until the end.
+ */
+ MOZ_CAN_RUN_SCRIPT void Shutdown();
+
+ /**
+ * Signals one iteration of mGraph. Hands state over to mThread and runs
+ * the iteration there.
+ */
+ IterationResult OneIteration(GraphTime aStateTime, GraphTime aIterationEnd,
+ AudioMixer* aMixer);
+
+ /**
+ * Runs mGraph until it shuts down.
+ */
+ NS_IMETHOD Run() override;
+
+ /**
+ * Returns true if called on mThread.
+ */
+ bool OnThread() const;
+
+#ifdef DEBUG
+ /**
+ * Returns true if called on mThread, and aDriver was the driver that called
+ * OneIteration() last.
+ */
+ bool InDriverIteration(const GraphDriver* aDriver) const;
+#endif
+
+ private:
+ explicit GraphRunner(MediaTrackGraphImpl* aGraph,
+ already_AddRefed<nsIThread> aThread);
+ ~GraphRunner();
+
+ class IterationState {
+ GraphTime mStateTime;
+ GraphTime mIterationEnd;
+ AudioMixer* MOZ_NON_OWNING_REF mMixer;
+
+ public:
+ IterationState(GraphTime aStateTime, GraphTime aIterationEnd,
+ AudioMixer* aMixer)
+ : mStateTime(aStateTime),
+ mIterationEnd(aIterationEnd),
+ mMixer(aMixer) {}
+ IterationState& operator=(const IterationState& aOther) = default;
+ GraphTime StateTime() const { return mStateTime; }
+ GraphTime IterationEnd() const { return mIterationEnd; }
+ AudioMixer* Mixer() const { return mMixer; }
+ };
+
+ // Monitor used for yielding mThread through Wait(), and scheduling mThread
+ // through Signal() from a GraphDriver.
+ Monitor mMonitor;
+ // The MediaTrackGraph we're running. Weakptr beecause this graph owns us and
+ // guarantees that our lifetime will not go beyond that of itself.
+ MediaTrackGraphImpl* const mGraph;
+ // State being handed over to the graph through OneIteration. Protected by
+ // mMonitor.
+ Maybe<IterationState> mIterationState MOZ_GUARDED_BY(mMonitor);
+ // Result from mGraph's OneIteration. Protected by mMonitor.
+ IterationResult mIterationResult MOZ_GUARDED_BY(mMonitor);
+
+ enum class ThreadState {
+ Wait, // Waiting for a message. This is the initial state.
+ // A transition from Run back to Wait occurs on the runner thread
+ // after it processes as far as mIterationState->mStateTime
+ // and sets mIterationResult.
+ Run, // Set on driver thread after each mIterationState update.
+ Shutdown, // Set when Shutdown() is called on main thread.
+ };
+ // Protected by mMonitor until set to Shutdown, after which this is not
+ // modified.
+ ThreadState mThreadState MOZ_GUARDED_BY(mMonitor);
+
+ // The thread running mGraph. Set on construction, after other members are
+ // initialized. Cleared at the end of Shutdown().
+ const nsCOMPtr<nsIThread> mThread;
+
+#ifdef DEBUG
+ // Set to mGraph's audio callback driver's thread id, if run by an
+ // AudioCallbackDriver, while OneIteration() is running.
+ std::thread::id mAudioDriverThreadId = std::thread::id();
+ // Set to mGraph's system clock driver's thread, if run by a
+ // SystemClockDriver, while OneIteration() is running.
+ nsIThread* mClockDriverThread = nullptr;
+#endif
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/IdpSandbox.sys.mjs b/dom/media/IdpSandbox.sys.mjs
new file mode 100644
index 0000000000..bd864b2fab
--- /dev/null
+++ b/dom/media/IdpSandbox.sys.mjs
@@ -0,0 +1,284 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const { NetUtil } = ChromeUtils.import("resource://gre/modules/NetUtil.jsm");
+
+/** This little class ensures that redirects maintain an https:// origin */
+function RedirectHttpsOnly() {}
+
+RedirectHttpsOnly.prototype = {
+ asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
+ if (newChannel.URI.scheme !== "https") {
+ callback.onRedirectVerifyCallback(Cr.NS_ERROR_ABORT);
+ } else {
+ callback.onRedirectVerifyCallback(Cr.NS_OK);
+ }
+ },
+
+ getInterface(iid) {
+ return this.QueryInterface(iid);
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIChannelEventSink"]),
+};
+
+/** This class loads a resource into a single string. ResourceLoader.load() is
+ * the entry point. */
+function ResourceLoader(res, rej) {
+ this.resolve = res;
+ this.reject = rej;
+ this.data = "";
+}
+
+/** Loads the identified https:// URL. */
+ResourceLoader.load = function (uri, doc) {
+ return new Promise((resolve, reject) => {
+ let listener = new ResourceLoader(resolve, reject);
+ let ioChannel = NetUtil.newChannel({
+ uri,
+ loadingNode: doc,
+ securityFlags: Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_INTERNAL_SCRIPT,
+ });
+
+ ioChannel.loadGroup = doc.documentLoadGroup.QueryInterface(Ci.nsILoadGroup);
+ ioChannel.notificationCallbacks = new RedirectHttpsOnly();
+ ioChannel.asyncOpen(listener);
+ });
+};
+
+ResourceLoader.prototype = {
+ onDataAvailable(request, input, offset, count) {
+ let stream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ stream.init(input);
+ this.data += stream.read(count);
+ },
+
+ onStartRequest(request) {},
+
+ onStopRequest(request, status) {
+ if (Components.isSuccessCode(status)) {
+ var statusCode = request.QueryInterface(Ci.nsIHttpChannel).responseStatus;
+ if (statusCode === 200) {
+ this.resolve({ request, data: this.data });
+ } else {
+ this.reject(new Error("Non-200 response from server: " + statusCode));
+ }
+ } else {
+ this.reject(new Error("Load failed: " + status));
+ }
+ },
+
+ getInterface(iid) {
+ return this.QueryInterface(iid);
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIStreamListener"]),
+};
+
+/**
+ * A simple implementation of the WorkerLocation interface.
+ */
+function createLocationFromURI(uri) {
+ return {
+ href: uri.spec,
+ protocol: uri.scheme + ":",
+ host: uri.host + (uri.port >= 0 ? ":" + uri.port : ""),
+ port: uri.port,
+ hostname: uri.host,
+ pathname: uri.pathQueryRef.replace(/[#\?].*/, ""),
+ search: uri.pathQueryRef.replace(/^[^\?]*/, "").replace(/#.*/, ""),
+ hash: uri.hasRef ? "#" + uri.ref : "",
+ origin: uri.prePath,
+ toString() {
+ return uri.spec;
+ },
+ };
+}
+
+/**
+ * A javascript sandbox for running an IdP.
+ *
+ * @param domain (string) the domain of the IdP
+ * @param protocol (string?) the protocol of the IdP [default: 'default']
+ * @param win (obj) the current window
+ * @throws if the domain or protocol aren't valid
+ */
+export function IdpSandbox(domain, protocol, win) {
+ this.source = IdpSandbox.createIdpUri(domain, protocol || "default");
+ this.active = null;
+ this.sandbox = null;
+ this.window = win;
+}
+
+IdpSandbox.checkDomain = function (domain) {
+ if (!domain || typeof domain !== "string") {
+ throw new Error(
+ "Invalid domain for identity provider: " +
+ "must be a non-zero length string"
+ );
+ }
+};
+
+/**
+ * Checks that the IdP protocol is superficially sane. In particular, we don't
+ * want someone adding relative paths (e.g., '../../myuri'), which could be used
+ * to move outside of /.well-known/ and into space that they control.
+ */
+IdpSandbox.checkProtocol = function (protocol) {
+ let message = "Invalid protocol for identity provider: ";
+ if (!protocol || typeof protocol !== "string") {
+ throw new Error(message + "must be a non-zero length string");
+ }
+ if (decodeURIComponent(protocol).match(/[\/\\]/)) {
+ throw new Error(message + "must not include '/' or '\\'");
+ }
+};
+
+/**
+ * Turns a domain and protocol into a URI. This does some aggressive checking
+ * to make sure that we aren't being fooled somehow. Throws on fooling.
+ */
+IdpSandbox.createIdpUri = function (domain, protocol) {
+ IdpSandbox.checkDomain(domain);
+ IdpSandbox.checkProtocol(protocol);
+
+ let message = "Invalid IdP parameters: ";
+ try {
+ let wkIdp = "https://" + domain + "/.well-known/idp-proxy/" + protocol;
+ let uri = Services.io.newURI(wkIdp);
+
+ if (uri.hostPort !== domain) {
+ throw new Error(message + "domain is invalid");
+ }
+ if (uri.pathQueryRef.indexOf("/.well-known/idp-proxy/") !== 0) {
+ throw new Error(message + "must produce a /.well-known/idp-proxy/ URI");
+ }
+
+ return uri;
+ } catch (e) {
+ if (
+ typeof e.result !== "undefined" &&
+ e.result === Cr.NS_ERROR_MALFORMED_URI
+ ) {
+ throw new Error(message + "must produce a valid URI");
+ }
+ throw e;
+ }
+};
+
+IdpSandbox.prototype = {
+ isSame(domain, protocol) {
+ return this.source.spec === IdpSandbox.createIdpUri(domain, protocol).spec;
+ },
+
+ start() {
+ if (!this.active) {
+ this.active = ResourceLoader.load(this.source, this.window.document).then(
+ result => this._createSandbox(result)
+ );
+ }
+ return this.active;
+ },
+
+ // Provides the sandbox with some useful facilities. Initially, this is only
+ // a minimal set; it is far easier to add more as the need arises, than to
+ // take them back if we discover a mistake.
+ _populateSandbox(uri) {
+ this.sandbox.location = Cu.cloneInto(
+ createLocationFromURI(uri),
+ this.sandbox,
+ { cloneFunctions: true }
+ );
+ },
+
+ _createSandbox(result) {
+ let principal = Services.scriptSecurityManager.getChannelResultPrincipal(
+ result.request
+ );
+
+ this.sandbox = Cu.Sandbox(principal, {
+ sandboxName: "IdP-" + this.source.host,
+ wantComponents: false,
+ wantExportHelpers: false,
+ wantGlobalProperties: [
+ "indexedDB",
+ "XMLHttpRequest",
+ "TextEncoder",
+ "TextDecoder",
+ "URL",
+ "URLSearchParams",
+ "atob",
+ "btoa",
+ "Blob",
+ "crypto",
+ "rtcIdentityProvider",
+ "fetch",
+ ],
+ });
+ let registrar = this.sandbox.rtcIdentityProvider;
+ if (!Cu.isXrayWrapper(registrar)) {
+ throw new Error("IdP setup failed");
+ }
+
+ // have to use the ultimate URI, not the starting one to avoid
+ // that origin stealing from the one that redirected to it
+ this._populateSandbox(result.request.URI);
+ try {
+ Cu.evalInSandbox(
+ result.data,
+ this.sandbox,
+ "latest",
+ result.request.URI.spec,
+ 1
+ );
+ } catch (e) {
+ // These can be passed straight on, because they are explicitly labelled
+ // as being IdP errors by the IdP and we drop line numbers as a result.
+ if (e.name === "IdpError" || e.name === "IdpLoginError") {
+ throw e;
+ }
+ this._logError(e);
+ throw new Error("Error in IdP, check console for details");
+ }
+
+ if (!registrar.hasIdp) {
+ throw new Error("IdP failed to call rtcIdentityProvider.register()");
+ }
+ return registrar;
+ },
+
+ // Capture all the details from the error and log them to the console. This
+ // can't rethrow anything else because that could leak information about the
+ // internal workings of the IdP across origins.
+ _logError(e) {
+ let winID = this.window.windowGlobalChild.innerWindowId;
+ let scriptError = Cc["@mozilla.org/scripterror;1"].createInstance(
+ Ci.nsIScriptError
+ );
+ scriptError.initWithWindowID(
+ e.message,
+ e.fileName,
+ null,
+ e.lineNumber,
+ e.columnNumber,
+ Ci.nsIScriptError.errorFlag,
+ "content javascript",
+ winID
+ );
+ Services.console.logMessage(scriptError);
+ },
+
+ stop() {
+ if (this.sandbox) {
+ Cu.nukeSandbox(this.sandbox);
+ }
+ this.sandbox = null;
+ this.active = null;
+ },
+
+ toString() {
+ return this.source.spec;
+ },
+};
diff --git a/dom/media/ImageToI420.cpp b/dom/media/ImageToI420.cpp
new file mode 100644
index 0000000000..8fc5198b4a
--- /dev/null
+++ b/dom/media/ImageToI420.cpp
@@ -0,0 +1,148 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ImageToI420.h"
+
+#include "ImageContainer.h"
+#include "libyuv/convert.h"
+#include "mozilla/dom/ImageBitmapBinding.h"
+#include "mozilla/dom/ImageUtils.h"
+#include "mozilla/gfx/Point.h"
+#include "mozilla/RefPtr.h"
+#include "nsThreadUtils.h"
+
+using mozilla::ImageFormat;
+using mozilla::dom::ImageBitmapFormat;
+using mozilla::dom::ImageUtils;
+using mozilla::gfx::DataSourceSurface;
+using mozilla::gfx::SourceSurface;
+using mozilla::gfx::SurfaceFormat;
+using mozilla::layers::Image;
+using mozilla::layers::PlanarYCbCrData;
+using mozilla::layers::PlanarYCbCrImage;
+
+static const PlanarYCbCrData* GetPlanarYCbCrData(Image* aImage) {
+ switch (aImage->GetFormat()) {
+ case ImageFormat::PLANAR_YCBCR:
+ return aImage->AsPlanarYCbCrImage()->GetData();
+ case ImageFormat::NV_IMAGE:
+ return aImage->AsNVImage()->GetData();
+ default:
+ return nullptr;
+ }
+}
+
+static already_AddRefed<SourceSurface> GetSourceSurface(Image* aImage) {
+ if (!aImage->AsGLImage() || NS_IsMainThread()) {
+ return aImage->GetAsSourceSurface();
+ }
+
+ // GLImage::GetAsSourceSurface() only supports main thread
+ RefPtr<SourceSurface> surf;
+ NS_DispatchAndSpinEventLoopUntilComplete(
+ "ImageToI420::GLImage::GetSourceSurface"_ns,
+ mozilla::GetMainThreadSerialEventTarget(),
+ NS_NewRunnableFunction(
+ "ImageToI420::GLImage::GetSourceSurface",
+ [&aImage, &surf]() { surf = aImage->GetAsSourceSurface(); }));
+
+ return surf.forget();
+}
+
+static nsresult MapRv(int aRv) {
+ // Docs for libyuv::ConvertToI420 say:
+ // Returns 0 for successful; -1 for invalid parameter. Non-zero for failure.
+ switch (aRv) {
+ case 0:
+ return NS_OK;
+ case -1:
+ return NS_ERROR_INVALID_ARG;
+ default:
+ return NS_ERROR_FAILURE;
+ }
+}
+
+namespace mozilla {
+
+nsresult ConvertToI420(Image* aImage, uint8_t* aDestY, int aDestStrideY,
+ uint8_t* aDestU, int aDestStrideU, uint8_t* aDestV,
+ int aDestStrideV) {
+ if (!aImage->IsValid()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (const PlanarYCbCrData* data = GetPlanarYCbCrData(aImage)) {
+ const ImageUtils imageUtils(aImage);
+ switch (imageUtils.GetFormat()) {
+ case ImageBitmapFormat::YUV420P:
+ return MapRv(libyuv::I420ToI420(
+ data->mYChannel, data->mYStride, data->mCbChannel,
+ data->mCbCrStride, data->mCrChannel, data->mCbCrStride, aDestY,
+ aDestStrideY, aDestU, aDestStrideU, aDestV, aDestStrideV,
+ aImage->GetSize().width, aImage->GetSize().height));
+ case ImageBitmapFormat::YUV422P:
+ return MapRv(libyuv::I422ToI420(
+ data->mYChannel, data->mYStride, data->mCbChannel,
+ data->mCbCrStride, data->mCrChannel, data->mCbCrStride, aDestY,
+ aDestStrideY, aDestU, aDestStrideU, aDestV, aDestStrideV,
+ aImage->GetSize().width, aImage->GetSize().height));
+ case ImageBitmapFormat::YUV444P:
+ return MapRv(libyuv::I444ToI420(
+ data->mYChannel, data->mYStride, data->mCbChannel,
+ data->mCbCrStride, data->mCrChannel, data->mCbCrStride, aDestY,
+ aDestStrideY, aDestU, aDestStrideU, aDestV, aDestStrideV,
+ aImage->GetSize().width, aImage->GetSize().height));
+ case ImageBitmapFormat::YUV420SP_NV12:
+ return MapRv(libyuv::NV12ToI420(
+ data->mYChannel, data->mYStride, data->mCbChannel,
+ data->mCbCrStride, aDestY, aDestStrideY, aDestU, aDestStrideU,
+ aDestV, aDestStrideV, aImage->GetSize().width,
+ aImage->GetSize().height));
+ case ImageBitmapFormat::YUV420SP_NV21:
+ return MapRv(libyuv::NV21ToI420(
+ data->mYChannel, data->mYStride, data->mCrChannel,
+ data->mCbCrStride, aDestY, aDestStrideY, aDestU, aDestStrideU,
+ aDestV, aDestStrideV, aImage->GetSize().width,
+ aImage->GetSize().height));
+ default:
+ MOZ_ASSERT_UNREACHABLE("YUV format conversion not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+ }
+
+ RefPtr<SourceSurface> surf = GetSourceSurface(aImage);
+ if (!surf) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<DataSourceSurface> data = surf->GetDataSurface();
+ if (!data) {
+ return NS_ERROR_FAILURE;
+ }
+
+ DataSourceSurface::ScopedMap map(data, DataSourceSurface::READ);
+ if (!map.IsMapped()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ switch (surf->GetFormat()) {
+ case SurfaceFormat::B8G8R8A8:
+ case SurfaceFormat::B8G8R8X8:
+ return MapRv(libyuv::ARGBToI420(
+ static_cast<uint8_t*>(map.GetData()), map.GetStride(), aDestY,
+ aDestStrideY, aDestU, aDestStrideU, aDestV, aDestStrideV,
+ aImage->GetSize().width, aImage->GetSize().height));
+ case SurfaceFormat::R5G6B5_UINT16:
+ return MapRv(libyuv::RGB565ToI420(
+ static_cast<uint8_t*>(map.GetData()), map.GetStride(), aDestY,
+ aDestStrideY, aDestU, aDestStrideU, aDestV, aDestStrideV,
+ aImage->GetSize().width, aImage->GetSize().height));
+ default:
+ MOZ_ASSERT_UNREACHABLE("Surface format conversion not implemented");
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/ImageToI420.h b/dom/media/ImageToI420.h
new file mode 100644
index 0000000000..24a66ebc9f
--- /dev/null
+++ b/dom/media/ImageToI420.h
@@ -0,0 +1,26 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef ImageToI420Converter_h
+#define ImageToI420Converter_h
+
+#include "nsError.h"
+
+namespace mozilla {
+
+namespace layers {
+class Image;
+} // namespace layers
+
+/**
+ * Converts aImage to an I420 image and writes it to the given buffers.
+ */
+nsresult ConvertToI420(layers::Image* aImage, uint8_t* aDestY, int aDestStrideY,
+ uint8_t* aDestU, int aDestStrideU, uint8_t* aDestV,
+ int aDestStrideV);
+
+} // namespace mozilla
+
+#endif /* ImageToI420Converter_h */
diff --git a/dom/media/Intervals.h b/dom/media/Intervals.h
new file mode 100644
index 0000000000..ecda90d350
--- /dev/null
+++ b/dom/media/Intervals.h
@@ -0,0 +1,762 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_INTERVALS_H_
+#define DOM_MEDIA_INTERVALS_H_
+
+#include <algorithm>
+#include <type_traits>
+
+#include "nsTArray.h"
+#include "nsString.h"
+#include "nsPrintfCString.h"
+
+// Specialization for nsTArray CopyChooser.
+namespace mozilla::media {
+template <class T>
+class IntervalSet;
+class TimeUnit;
+} // namespace mozilla::media
+
+template <class E>
+struct nsTArray_RelocationStrategy<mozilla::media::IntervalSet<E>> {
+ typedef nsTArray_RelocateUsingMoveConstructor<mozilla::media::IntervalSet<E>>
+ Type;
+};
+
+namespace mozilla::media {
+
+/* Interval defines an interval between two points. Unlike a traditional
+ interval [A,B] where A <= x <= B, the upper boundary B is exclusive: A <= x <
+ B (e.g [A,B[ or [A,B) depending on where you're living) It provides basic
+ interval arithmetic and fuzzy edges. The type T must provides a default
+ constructor and +, -, <, <= and == operators.
+ */
+template <typename T>
+class Interval {
+ public:
+ typedef Interval<T> SelfType;
+
+ Interval() : mStart(T()), mEnd(T()), mFuzz(T()) {}
+
+ template <typename StartArg, typename EndArg>
+ Interval(StartArg&& aStart, EndArg&& aEnd)
+ : mStart(aStart), mEnd(aEnd), mFuzz() {
+ MOZ_DIAGNOSTIC_ASSERT(mStart <= mEnd, "Invalid Interval");
+ }
+
+ template <typename StartArg, typename EndArg, typename FuzzArg>
+ Interval(StartArg&& aStart, EndArg&& aEnd, FuzzArg&& aFuzz)
+ : mStart(aStart), mEnd(aEnd), mFuzz(aFuzz) {
+ MOZ_DIAGNOSTIC_ASSERT(mStart <= mEnd, "Invalid Interval");
+ }
+
+ Interval(const SelfType& aOther)
+ : mStart(aOther.mStart), mEnd(aOther.mEnd), mFuzz(aOther.mFuzz) {}
+
+ Interval(SelfType&& aOther)
+ : mStart(std::move(aOther.mStart)),
+ mEnd(std::move(aOther.mEnd)),
+ mFuzz(std::move(aOther.mFuzz)) {}
+
+ SelfType& operator=(const SelfType& aOther) {
+ mStart = aOther.mStart;
+ mEnd = aOther.mEnd;
+ mFuzz = aOther.mFuzz;
+ return *this;
+ }
+
+ SelfType& operator=(SelfType&& aOther) {
+ MOZ_ASSERT(&aOther != this, "self-moves are prohibited");
+ this->~Interval();
+ new (this) Interval(std::move(aOther));
+ return *this;
+ }
+
+ // Basic interval arithmetic operator definition.
+ SelfType operator+(const SelfType& aOther) const {
+ return SelfType(mStart + aOther.mStart, mEnd + aOther.mEnd,
+ mFuzz + aOther.mFuzz);
+ }
+
+ SelfType operator+(const T& aVal) const {
+ return SelfType(mStart + aVal, mEnd + aVal, mFuzz);
+ }
+
+ SelfType operator-(const SelfType& aOther) const {
+ return SelfType(mStart - aOther.mEnd, mEnd - aOther.mStart,
+ mFuzz + aOther.mFuzz);
+ }
+
+ SelfType operator-(const T& aVal) const {
+ return SelfType(mStart - aVal, mEnd - aVal, mFuzz);
+ }
+
+ SelfType& operator+=(const SelfType& aOther) {
+ mStart += aOther.mStart;
+ mEnd += aOther.mEnd;
+ mFuzz += aOther.mFuzz;
+ return *this;
+ }
+
+ SelfType& operator+=(const T& aVal) {
+ mStart += aVal;
+ mEnd += aVal;
+ return *this;
+ }
+
+ SelfType& operator-=(const SelfType& aOther) {
+ mStart -= aOther.mStart;
+ mEnd -= aOther.mEnd;
+ mFuzz += aOther.mFuzz;
+ return *this;
+ }
+
+ SelfType& operator-=(const T& aVal) {
+ mStart -= aVal;
+ mEnd -= aVal;
+ return *this;
+ }
+
+ bool operator==(const SelfType& aOther) const {
+ return mStart == aOther.mStart && mEnd == aOther.mEnd;
+ }
+
+ bool operator!=(const SelfType& aOther) const { return !(*this == aOther); }
+
+ bool Contains(const T& aX) const {
+ return mStart - mFuzz <= aX && aX < mEnd + mFuzz;
+ }
+
+ bool ContainsStrict(const T& aX) const { return mStart <= aX && aX < mEnd; }
+
+ bool ContainsWithStrictEnd(const T& aX) const {
+ return mStart - mFuzz <= aX && aX < mEnd;
+ }
+
+ bool Contains(const SelfType& aOther) const {
+ return (mStart - mFuzz <= aOther.mStart + aOther.mFuzz) &&
+ (aOther.mEnd - aOther.mFuzz <= mEnd + mFuzz);
+ }
+
+ bool ContainsStrict(const SelfType& aOther) const {
+ return mStart <= aOther.mStart && aOther.mEnd <= mEnd;
+ }
+
+ bool ContainsWithStrictEnd(const SelfType& aOther) const {
+ return (mStart - mFuzz <= aOther.mStart + aOther.mFuzz) &&
+ aOther.mEnd <= mEnd;
+ }
+
+ bool Intersects(const SelfType& aOther) const {
+ return (mStart - mFuzz < aOther.mEnd + aOther.mFuzz) &&
+ (aOther.mStart - aOther.mFuzz < mEnd + mFuzz);
+ }
+
+ bool IntersectsStrict(const SelfType& aOther) const {
+ return mStart < aOther.mEnd && aOther.mStart < mEnd;
+ }
+
+ // Same as Intersects, but including the boundaries.
+ bool Touches(const SelfType& aOther) const {
+ return (mStart - mFuzz <= aOther.mEnd + aOther.mFuzz) &&
+ (aOther.mStart - aOther.mFuzz <= mEnd + mFuzz);
+ }
+
+ // Returns true if aOther is strictly to the right of this and contiguous.
+ // This operation isn't commutative.
+ bool Contiguous(const SelfType& aOther) const {
+ return mEnd <= aOther.mStart &&
+ aOther.mStart - mEnd <= mFuzz + aOther.mFuzz;
+ }
+
+ bool RightOf(const SelfType& aOther) const {
+ return aOther.mEnd - aOther.mFuzz <= mStart + mFuzz;
+ }
+
+ bool LeftOf(const SelfType& aOther) const {
+ return mEnd - mFuzz <= aOther.mStart + aOther.mFuzz;
+ }
+
+ SelfType Span(const SelfType& aOther) const {
+ if (IsEmpty()) {
+ return aOther;
+ }
+ SelfType result(*this);
+ if (aOther.mStart < mStart) {
+ result.mStart = aOther.mStart;
+ }
+ if (mEnd < aOther.mEnd) {
+ result.mEnd = aOther.mEnd;
+ }
+ if (mFuzz < aOther.mFuzz) {
+ result.mFuzz = aOther.mFuzz;
+ }
+ return result;
+ }
+
+ SelfType Intersection(const SelfType& aOther) const {
+ const T& s = std::max(mStart, aOther.mStart);
+ const T& e = std::min(mEnd, aOther.mEnd);
+ const T& f = std::max(mFuzz, aOther.mFuzz);
+ if (s < e) {
+ return SelfType(s, e, f);
+ }
+ // Return an empty interval.
+ return SelfType();
+ }
+
+ T Length() const { return mEnd - mStart; }
+
+ bool IsEmpty() const { return mStart == mEnd; }
+
+ void SetFuzz(const T& aFuzz) { mFuzz = aFuzz; }
+
+ // Returns true if the two intervals intersect with this being on the right
+ // of aOther
+ bool TouchesOnRight(const SelfType& aOther) const {
+ return aOther.mStart <= mStart &&
+ (mStart - mFuzz <= aOther.mEnd + aOther.mFuzz) &&
+ (aOther.mStart - aOther.mFuzz <= mEnd + mFuzz);
+ }
+
+ // Returns true if the two intervals intersect with this being on the right
+ // of aOther, ignoring fuzz.
+ bool TouchesOnRightStrict(const SelfType& aOther) const {
+ return aOther.mStart <= mStart && mStart <= aOther.mEnd;
+ }
+
+ nsCString ToString() const {
+ if constexpr (std::is_same_v<T, TimeUnit>) {
+ return nsPrintfCString("[%s, %s](%s)", mStart.ToString().get(),
+ mEnd.ToString().get(), mFuzz.ToString().get());
+ } else if constexpr (std::is_same_v<T, double>) {
+ return nsPrintfCString("[%lf, %lf](%lf)", mStart, mEnd, mFuzz);
+ }
+ }
+
+ T mStart;
+ T mEnd;
+ T mFuzz;
+};
+
+// An IntervalSet in a collection of Intervals. The IntervalSet is always
+// normalized.
+template <typename T>
+class IntervalSet {
+ public:
+ typedef IntervalSet<T> SelfType;
+ typedef Interval<T> ElemType;
+ typedef AutoTArray<ElemType, 4> ContainerType;
+ typedef typename ContainerType::index_type IndexType;
+
+ IntervalSet() = default;
+ virtual ~IntervalSet() = default;
+
+ IntervalSet(const SelfType& aOther) : mIntervals(aOther.mIntervals.Clone()) {}
+
+ IntervalSet(SelfType&& aOther) {
+ mIntervals.AppendElements(std::move(aOther.mIntervals));
+ }
+
+ explicit IntervalSet(const ElemType& aOther) {
+ if (!aOther.IsEmpty()) {
+ mIntervals.AppendElement(aOther);
+ }
+ }
+
+ explicit IntervalSet(ElemType&& aOther) {
+ if (!aOther.IsEmpty()) {
+ mIntervals.AppendElement(std::move(aOther));
+ }
+ }
+
+ bool operator==(const SelfType& aOther) const {
+ return mIntervals == aOther.mIntervals;
+ }
+
+ bool operator!=(const SelfType& aOther) const {
+ return mIntervals != aOther.mIntervals;
+ }
+
+ SelfType& operator=(const SelfType& aOther) {
+ mIntervals = aOther.mIntervals.Clone();
+ return *this;
+ }
+
+ SelfType& operator=(SelfType&& aOther) {
+ MOZ_ASSERT(&aOther != this, "self-moves are prohibited");
+ this->~IntervalSet();
+ new (this) IntervalSet(std::move(aOther));
+ return *this;
+ }
+
+ SelfType& operator=(const ElemType& aInterval) {
+ mIntervals.Clear();
+ if (!aInterval.IsEmpty()) {
+ mIntervals.AppendElement(aInterval);
+ }
+ return *this;
+ }
+
+ SelfType& operator=(ElemType&& aInterval) {
+ mIntervals.Clear();
+ if (!aInterval.IsEmpty()) {
+ mIntervals.AppendElement(std::move(aInterval));
+ }
+ return *this;
+ }
+
+ SelfType& Add(const SelfType& aIntervals) {
+ if (aIntervals.mIntervals.Length() == 1) {
+ Add(aIntervals.mIntervals[0]);
+ } else {
+ mIntervals.AppendElements(aIntervals.mIntervals);
+ Normalize();
+ }
+ return *this;
+ }
+
+ SelfType& Add(const ElemType& aInterval) {
+ if (aInterval.IsEmpty()) {
+ return *this;
+ }
+ if (mIntervals.IsEmpty()) {
+ mIntervals.AppendElement(aInterval);
+ return *this;
+ }
+ ElemType& last = mIntervals.LastElement();
+ if (aInterval.TouchesOnRight(last)) {
+ last = last.Span(aInterval);
+ return *this;
+ }
+ // Most of our actual usage is adding an interval that will be outside the
+ // range. We can speed up normalization here.
+ if (aInterval.RightOf(last)) {
+ mIntervals.AppendElement(aInterval);
+ return *this;
+ }
+
+ ContainerType normalized;
+ ElemType current(aInterval);
+ IndexType i = 0;
+ for (; i < mIntervals.Length(); i++) {
+ ElemType& interval = mIntervals[i];
+ if (current.Touches(interval)) {
+ current = current.Span(interval);
+ } else if (current.LeftOf(interval)) {
+ break;
+ } else {
+ normalized.AppendElement(std::move(interval));
+ }
+ }
+ normalized.AppendElement(std::move(current));
+ for (; i < mIntervals.Length(); i++) {
+ normalized.AppendElement(std::move(mIntervals[i]));
+ }
+ mIntervals.Clear();
+ mIntervals.AppendElements(std::move(normalized));
+
+ return *this;
+ }
+
+ SelfType& operator+=(const SelfType& aIntervals) {
+ Add(aIntervals);
+ return *this;
+ }
+
+ SelfType& operator+=(const ElemType& aInterval) {
+ Add(aInterval);
+ return *this;
+ }
+
+ SelfType operator+(const SelfType& aIntervals) const {
+ SelfType intervals(*this);
+ intervals.Add(aIntervals);
+ return intervals;
+ }
+
+ SelfType operator+(const ElemType& aInterval) const {
+ SelfType intervals(*this);
+ intervals.Add(aInterval);
+ return intervals;
+ }
+
+ friend SelfType operator+(const ElemType& aInterval,
+ const SelfType& aIntervals) {
+ SelfType intervals;
+ intervals.Add(aInterval);
+ intervals.Add(aIntervals);
+ return intervals;
+ }
+
+ // Excludes an interval from an IntervalSet.
+ SelfType& operator-=(const ElemType& aInterval) {
+ if (aInterval.IsEmpty() || mIntervals.IsEmpty()) {
+ return *this;
+ }
+ if (mIntervals.Length() == 1 &&
+ mIntervals[0].TouchesOnRightStrict(aInterval)) {
+ // Fast path when we're removing from the front of a set with a
+ // single interval. This is common for the buffered time ranges
+ // we see on Twitch.
+ if (aInterval.mEnd >= mIntervals[0].mEnd) {
+ mIntervals.RemoveElementAt(0);
+ } else {
+ mIntervals[0].mStart = aInterval.mEnd;
+ mIntervals[0].mFuzz = std::max(mIntervals[0].mFuzz, aInterval.mFuzz);
+ }
+ return *this;
+ }
+
+ // General case performed by inverting aInterval within the bounds of
+ // mIntervals and then doing the intersection.
+ T firstEnd = std::max(mIntervals[0].mStart, aInterval.mStart);
+ T secondStart = std::min(mIntervals.LastElement().mEnd, aInterval.mEnd);
+ ElemType startInterval(mIntervals[0].mStart, firstEnd);
+ ElemType endInterval(secondStart, mIntervals.LastElement().mEnd);
+ SelfType intervals(std::move(startInterval));
+ intervals += std::move(endInterval);
+ return Intersection(intervals);
+ }
+
+ SelfType& operator-=(const SelfType& aIntervals) {
+ for (const auto& interval : aIntervals.mIntervals) {
+ *this -= interval;
+ }
+ return *this;
+ }
+
+ SelfType operator-(const SelfType& aInterval) const {
+ SelfType intervals(*this);
+ intervals -= aInterval;
+ return intervals;
+ }
+
+ SelfType operator-(const ElemType& aInterval) const {
+ SelfType intervals(*this);
+ intervals -= aInterval;
+ return intervals;
+ }
+
+ // Mutate this IntervalSet to be the union of this and aOther.
+ SelfType& Union(const SelfType& aOther) {
+ Add(aOther);
+ return *this;
+ }
+
+ SelfType& Union(const ElemType& aInterval) {
+ Add(aInterval);
+ return *this;
+ }
+
+ // Mutate this TimeRange to be the intersection of this and aOther.
+ SelfType& Intersection(const SelfType& aOther) {
+ ContainerType intersection;
+
+ // Ensure the intersection has enough capacity to store the upper bound on
+ // the intersection size. This ensures that we don't spend time reallocating
+ // the storage as we append, at the expense of extra memory.
+ intersection.SetCapacity(std::max(aOther.Length(), mIntervals.Length()));
+
+ const ContainerType& other = aOther.mIntervals;
+ IndexType i = 0, j = 0;
+ for (; i < mIntervals.Length() && j < other.Length();) {
+ if (mIntervals[i].IntersectsStrict(other[j])) {
+ intersection.AppendElement(mIntervals[i].Intersection(other[j]));
+ }
+ if (mIntervals[i].mEnd < other[j].mEnd) {
+ i++;
+ } else {
+ j++;
+ }
+ }
+ mIntervals = std::move(intersection);
+ return *this;
+ }
+
+ SelfType& Intersection(const ElemType& aInterval) {
+ SelfType intervals(aInterval);
+ return Intersection(intervals);
+ }
+
+ const ElemType& operator[](IndexType aIndex) const {
+ return mIntervals[aIndex];
+ }
+
+ // Returns the start boundary of the first interval. Or a default constructed
+ // T if IntervalSet is empty (and aExists if provided will be set to false).
+ T GetStart(bool* aExists = nullptr) const {
+ bool exists = !mIntervals.IsEmpty();
+
+ if (aExists) {
+ *aExists = exists;
+ }
+
+ if (exists) {
+ return mIntervals[0].mStart;
+ } else {
+ return T();
+ }
+ }
+
+ // Returns the end boundary of the last interval. Or a default constructed T
+ // if IntervalSet is empty (and aExists if provided will be set to false).
+ T GetEnd(bool* aExists = nullptr) const {
+ bool exists = !mIntervals.IsEmpty();
+ if (aExists) {
+ *aExists = exists;
+ }
+
+ if (exists) {
+ return mIntervals.LastElement().mEnd;
+ } else {
+ return T();
+ }
+ }
+
+ IndexType Length() const { return mIntervals.Length(); }
+
+ bool IsEmpty() const { return mIntervals.IsEmpty(); }
+
+ T Start(IndexType aIndex) const { return mIntervals[aIndex].mStart; }
+
+ T Start(IndexType aIndex, bool& aExists) const {
+ aExists = aIndex < mIntervals.Length();
+
+ if (aExists) {
+ return mIntervals[aIndex].mStart;
+ } else {
+ return T();
+ }
+ }
+
+ T End(IndexType aIndex) const { return mIntervals[aIndex].mEnd; }
+
+ T End(IndexType aIndex, bool& aExists) const {
+ aExists = aIndex < mIntervals.Length();
+
+ if (aExists) {
+ return mIntervals[aIndex].mEnd;
+ } else {
+ return T();
+ }
+ }
+
+ bool Contains(const ElemType& aInterval) const {
+ for (const auto& interval : mIntervals) {
+ if (interval.Contains(aInterval)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ bool ContainsStrict(const ElemType& aInterval) const {
+ for (const auto& interval : mIntervals) {
+ if (interval.ContainsStrict(aInterval)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ bool Contains(const T& aX) const {
+ for (const auto& interval : mIntervals) {
+ if (interval.Contains(aX)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ bool ContainsStrict(const T& aX) const {
+ for (const auto& interval : mIntervals) {
+ if (interval.ContainsStrict(aX)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ bool ContainsWithStrictEnd(const T& aX) const {
+ for (const auto& interval : mIntervals) {
+ if (interval.ContainsWithStrictEnd(aX)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ bool ContainsWithStrictEnd(const ElemType& aInterval) const {
+ for (const auto& interval : mIntervals) {
+ if (interval.ContainsWithStrictEnd(aInterval)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ bool Intersects(const ElemType& aInterval) const {
+ for (const auto& interval : mIntervals) {
+ if (interval.Intersects(aInterval)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ bool IntersectsStrict(const ElemType& aInterval) const {
+ for (const auto& interval : mIntervals) {
+ if (interval.IntersectsStrict(aInterval)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // Returns if there's any intersection between this and aOther.
+ bool IntersectsStrict(const SelfType& aOther) const {
+ const ContainerType& other = aOther.mIntervals;
+ IndexType i = 0, j = 0;
+ for (; i < mIntervals.Length() && j < other.Length();) {
+ if (mIntervals[i].IntersectsStrict(other[j])) {
+ return true;
+ }
+ if (mIntervals[i].mEnd < other[j].mEnd) {
+ i++;
+ } else {
+ j++;
+ }
+ }
+ return false;
+ }
+
+ bool IntersectsWithStrictEnd(const ElemType& aInterval) const {
+ for (const auto& interval : mIntervals) {
+ if (interval.IntersectsWithStrictEnd(aInterval)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // Shift all values by aOffset.
+ SelfType& Shift(const T& aOffset) {
+ for (auto& interval : mIntervals) {
+ interval.mStart = interval.mStart + aOffset;
+ interval.mEnd = interval.mEnd + aOffset;
+ }
+ return *this;
+ }
+
+ void SetFuzz(const T& aFuzz) {
+ for (auto& interval : mIntervals) {
+ interval.SetFuzz(aFuzz);
+ }
+ MergeOverlappingIntervals();
+ }
+
+ static const IndexType NoIndex = IndexType(-1);
+
+ IndexType Find(const T& aValue) const {
+ for (IndexType i = 0; i < mIntervals.Length(); i++) {
+ if (mIntervals[i].Contains(aValue)) {
+ return i;
+ }
+ }
+ return NoIndex;
+ }
+
+ // Methods for range-based for loops.
+ typename ContainerType::iterator begin() { return mIntervals.begin(); }
+
+ typename ContainerType::const_iterator begin() const {
+ return mIntervals.begin();
+ }
+
+ typename ContainerType::iterator end() { return mIntervals.end(); }
+
+ typename ContainerType::const_iterator end() const {
+ return mIntervals.end();
+ }
+
+ ElemType& LastInterval() {
+ MOZ_ASSERT(!mIntervals.IsEmpty());
+ return mIntervals.LastElement();
+ }
+
+ const ElemType& LastInterval() const {
+ MOZ_ASSERT(!mIntervals.IsEmpty());
+ return mIntervals.LastElement();
+ }
+
+ void Clear() { mIntervals.Clear(); }
+
+ protected:
+ ContainerType mIntervals;
+
+ private:
+ void Normalize() {
+ if (mIntervals.Length() < 2) {
+ return;
+ }
+ mIntervals.Sort(CompareIntervals());
+ MergeOverlappingIntervals();
+ }
+
+ void MergeOverlappingIntervals() {
+ if (mIntervals.Length() < 2) {
+ return;
+ }
+
+ // This merges the intervals in place.
+ IndexType read = 0;
+ IndexType write = 0;
+ while (read < mIntervals.Length()) {
+ ElemType current(mIntervals[read]);
+ read++;
+ while (read < mIntervals.Length() && current.Touches(mIntervals[read])) {
+ current = current.Span(mIntervals[read]);
+ read++;
+ }
+ mIntervals[write] = current;
+ write++;
+ }
+ mIntervals.SetLength(write);
+ }
+
+ struct CompareIntervals {
+ bool Equals(const ElemType& aT1, const ElemType& aT2) const {
+ return aT1.mStart == aT2.mStart && aT1.mEnd == aT2.mEnd;
+ }
+
+ bool LessThan(const ElemType& aT1, const ElemType& aT2) const {
+ return aT1.mStart - aT1.mFuzz < aT2.mStart + aT2.mFuzz;
+ }
+ };
+};
+
+// clang doesn't allow for this to be defined inline of IntervalSet.
+template <typename T>
+IntervalSet<T> Union(const IntervalSet<T>& aIntervals1,
+ const IntervalSet<T>& aIntervals2) {
+ IntervalSet<T> intervals(aIntervals1);
+ intervals.Union(aIntervals2);
+ return intervals;
+}
+
+template <typename T>
+IntervalSet<T> Intersection(const IntervalSet<T>& aIntervals1,
+ const IntervalSet<T>& aIntervals2) {
+ IntervalSet<T> intersection(aIntervals1);
+ intersection.Intersection(aIntervals2);
+ return intersection;
+}
+
+} // namespace mozilla::media
+
+#endif // DOM_MEDIA_INTERVALS_H_
diff --git a/dom/media/MPSCQueue.h b/dom/media/MPSCQueue.h
new file mode 100644
index 0000000000..ea7848154f
--- /dev/null
+++ b/dom/media/MPSCQueue.h
@@ -0,0 +1,132 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_MPSCQueue_h
+#define mozilla_dom_MPSCQueue_h
+
+namespace mozilla {
+
+// This class implements a lock-free multiple producer single consumer queue of
+// fixed size log messages, with the following characteristics:
+// - Unbounded (uses a intrinsic linked list)
+// - Allocates on Push. Push can be called on any thread.
+// - Deallocates on Pop. Pop MUST always be called on the same thread for the
+// life-time of the queue.
+//
+// In our scenario, the producer threads are real-time, they can't block. The
+// consummer thread runs every now and then and empties the queue to a log
+// file, on disk.
+const size_t MPSC_MSG_RESERVED = sizeof(std::atomic<void*>);
+
+template <typename T>
+class MPSCQueue {
+ public:
+ struct Message {
+ Message() { mNext.store(nullptr, std::memory_order_relaxed); }
+ Message(const Message& aMessage) = delete;
+ void operator=(const Message& aMessage) = delete;
+
+ std::atomic<Message*> mNext;
+ T data;
+ };
+
+ // Creates a new MPSCQueue. Initially, the queue has a single sentinel node,
+ // pointed to by both mHead and mTail.
+ MPSCQueue()
+ // At construction, the initial message points to nullptr (it has no
+ // successor). It is a sentinel node, that does not contain meaningful
+ // data.
+ : mHead(new Message()), mTail(mHead.load(std::memory_order_relaxed)) {}
+
+ ~MPSCQueue() {
+ Message dummy;
+ while (Pop(&dummy.data)) {
+ }
+ Message* front = mHead.load(std::memory_order_relaxed);
+ delete front;
+ }
+
+ void Push(MPSCQueue<T>::Message* aMessage) {
+ // The next two non-commented line are called A and B in this paragraph.
+ // Producer threads i, i-1, etc. are numbered in the order they reached
+ // A in time, thread i being the thread that has reached A first.
+ // Atomically, on line A the new `mHead` is set to be the node that was
+ // just allocated, with strong memory order. From now on, any thread
+ // that reaches A will see that the node just allocated is
+ // effectively the head of the list, and will make itself the new head
+ // of the list.
+ // In a bad case (when thread i executes A and then
+ // is not scheduled for a long time), it is possible that thread i-1 and
+ // subsequent threads create a seemingly disconnected set of nodes, but
+ // they all have the correct value for the next node to set as their
+ // mNext member on their respective stacks (in `prev`), and this is
+ // always correct. When the scheduler resumes, and line B is executed,
+ // the correct linkage is resumed.
+ // Before line B, since mNext for the node was the last element of
+ // the queue still has an mNext of nullptr, Pop will not see the node
+ // added.
+ // For line A, it's critical to have strong ordering both ways (since
+ // it's going to possibly be read and write repeatidly by multiple
+ // threads)
+ // Line B can have weaker guarantees, it's only going to be written by a
+ // single thread, and we just need to ensure it's read properly by a
+ // single other one.
+ Message* prev = mHead.exchange(aMessage, std::memory_order_acq_rel);
+ prev->mNext.store(aMessage, std::memory_order_release);
+ }
+
+ // Copy the content of the first message of the queue to aOutput, and
+ // frees the message. Returns true if there was a message, in which case
+ // `aOutput` contains a valid value. If the queue was empty, returns false,
+ // in which case `aOutput` is left untouched.
+ bool Pop(T* aOutput) {
+ // Similarly, in this paragraph, the two following lines are called A
+ // and B, and threads are called thread i, i-1, etc. in order of
+ // execution of line A.
+ // On line A, the first element of the queue is acquired. It is simply a
+ // sentinel node.
+ // On line B, we acquire the node that has the data we want. If B is
+ // null, then only the sentinel node was present in the queue, we can
+ // safely return false.
+ // mTail can be loaded with relaxed ordering, since it's not written nor
+ // read by any other thread (this queue is single consumer).
+ // mNext can be written to by one of the producer, so it's necessary to
+ // ensure those writes are seen, hence the stricter ordering.
+ Message* tail = mTail.load(std::memory_order_relaxed);
+ Message* next = tail->mNext.load(std::memory_order_acquire);
+
+ if (next == nullptr) {
+ return false;
+ }
+
+ *aOutput = next->data;
+
+ // Simply shift the queue one node further, so that the sentinel node is
+ // now pointing to the correct most ancient node. It contains stale data,
+ // but this data will never be read again.
+ // It's only necessary to ensure the previous load on this thread is not
+ // reordered past this line, so release ordering is sufficient here.
+ mTail.store(next, std::memory_order_release);
+
+ // This thread is now the only thing that points to `tail`, it can be
+ // safely deleted.
+ delete tail;
+
+ return true;
+ }
+
+ private:
+ // An atomic pointer to the most recent message in the queue.
+ std::atomic<Message*> mHead;
+ // An atomic pointer to a sentinel node, that points to the oldest message
+ // in the queue.
+ std::atomic<Message*> mTail;
+
+ MPSCQueue(const MPSCQueue&) = delete;
+ void operator=(const MPSCQueue&) = delete;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_dom_MPSCQueue_h
diff --git a/dom/media/MediaBlockCacheBase.h b/dom/media/MediaBlockCacheBase.h
new file mode 100644
index 0000000000..d3cadf3dea
--- /dev/null
+++ b/dom/media/MediaBlockCacheBase.h
@@ -0,0 +1,81 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MEDIA_BLOCK_CACHE_BASE_H_
+#define MEDIA_BLOCK_CACHE_BASE_H_
+
+#include "MediaCache.h"
+#include "mozilla/Span.h"
+
+namespace mozilla {
+
+// Manages block management for the media cache. Data comes in over the network
+// via callbacks on the main thread, however we don't want to write the
+// incoming data to the media cache on the main thread, as this could block
+// causing UI jank.
+//
+// So MediaBlockCacheBase provides an abstraction for a temporary memory buffer
+// or file accessible as an array of blocks, which supports a block move
+// operation, and allows synchronous reading and writing from any thread, with
+// writes being buffered as needed so as not to block.
+//
+// Writes and cache block moves (which require reading) may be deferred to
+// their own non-main thread. This object also ensures that data which has
+// been scheduled to be written, but hasn't actually *been* written, is read
+// as if it had, i.e. pending writes are cached in readable memory until
+// they're flushed to file.
+//
+// To improve efficiency, writes can only be done at block granularity,
+// whereas reads can be done with byte granularity.
+//
+// Note it's also recommended not to read from the media cache from the main
+// thread to prevent jank.
+class MediaBlockCacheBase {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaBlockCacheBase)
+
+ static_assert(
+ MediaCacheStream::BLOCK_SIZE <
+ static_cast<
+ std::remove_const<decltype(MediaCacheStream::BLOCK_SIZE)>::type>(
+ INT32_MAX),
+ "MediaCacheStream::BLOCK_SIZE should fit in 31 bits");
+ static const int32_t BLOCK_SIZE = MediaCacheStream::BLOCK_SIZE;
+
+ protected:
+ virtual ~MediaBlockCacheBase() = default;
+
+ public:
+ // Initialize this cache.
+ virtual nsresult Init() = 0;
+
+ // Erase data and discard pending changes to reset the cache to its pristine
+ // state as it was after Init().
+ virtual void Flush() = 0;
+
+ // Maximum number of blocks expected in this block cache. (But allow overflow
+ // to accomodate incoming traffic before MediaCache can handle it.)
+ virtual size_t GetMaxBlocks(size_t aCacheSizeInKiB) const = 0;
+
+ // Can be called on any thread. This defers to a non-main thread.
+ virtual nsresult WriteBlock(uint32_t aBlockIndex, Span<const uint8_t> aData1,
+ Span<const uint8_t> aData2) = 0;
+
+ // Synchronously reads data from file. May read from file or memory
+ // depending on whether written blocks have been flushed to file yet.
+ // Not recommended to be called from the main thread, as can cause jank.
+ virtual nsresult Read(int64_t aOffset, uint8_t* aData, int32_t aLength,
+ int32_t* aBytes) = 0;
+
+ // Moves a block asynchronously. Can be called on any thread.
+ // This defers file I/O to a non-main thread.
+ virtual nsresult MoveBlock(int32_t aSourceBlockIndex,
+ int32_t aDestBlockIndex) = 0;
+};
+
+} // End namespace mozilla.
+
+#endif /* MEDIA_BLOCK_CACHE_BASE_H_ */
diff --git a/dom/media/MediaCache.cpp b/dom/media/MediaCache.cpp
new file mode 100644
index 0000000000..41d51a49cc
--- /dev/null
+++ b/dom/media/MediaCache.cpp
@@ -0,0 +1,2816 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaCache.h"
+
+#include "ChannelMediaResource.h"
+#include "FileBlockCache.h"
+#include "MediaBlockCacheBase.h"
+#include "MediaResource.h"
+#include "MemoryBlockCache.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/ErrorNames.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/StaticPrefs_browser.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/Telemetry.h"
+#include "nsContentUtils.h"
+#include "nsINetworkLinkService.h"
+#include "nsIObserverService.h"
+#include "nsPrintfCString.h"
+#include "nsProxyRelease.h"
+#include "nsTHashSet.h"
+#include "nsThreadUtils.h"
+#include "prio.h"
+#include "VideoUtils.h"
+#include <algorithm>
+
+namespace mozilla {
+
+#undef LOG
+#undef LOGI
+#undef LOGE
+LazyLogModule gMediaCacheLog("MediaCache");
+#define LOG(...) MOZ_LOG(gMediaCacheLog, LogLevel::Debug, (__VA_ARGS__))
+#define LOGI(...) MOZ_LOG(gMediaCacheLog, LogLevel::Info, (__VA_ARGS__))
+#define LOGE(...) \
+ NS_DebugBreak(NS_DEBUG_WARNING, nsPrintfCString(__VA_ARGS__).get(), nullptr, \
+ __FILE__, __LINE__)
+
+// For HTTP seeking, if number of bytes needing to be
+// seeked forward is less than this value then a read is
+// done rather than a byte range request.
+//
+// If we assume a 100Mbit connection, and assume reissuing an HTTP seek causes
+// a delay of 200ms, then in that 200ms we could have simply read ahead 2MB. So
+// setting SEEK_VS_READ_THRESHOLD to 1MB sounds reasonable.
+static const int64_t SEEK_VS_READ_THRESHOLD = 1 * 1024 * 1024;
+
+// Readahead blocks for non-seekable streams will be limited to this
+// fraction of the cache space. We don't normally evict such blocks
+// because replacing them requires a seek, but we need to make sure
+// they don't monopolize the cache.
+static const double NONSEEKABLE_READAHEAD_MAX = 0.5;
+
+// Data N seconds before the current playback position is given the same
+// priority as data REPLAY_PENALTY_FACTOR*N seconds ahead of the current
+// playback position. REPLAY_PENALTY_FACTOR is greater than 1 to reflect that
+// data in the past is less likely to be played again than data in the future.
+// We want to give data just behind the current playback position reasonably
+// high priority in case codecs need to retrieve that data (e.g. because
+// tracks haven't been muxed well or are being decoded at uneven rates).
+// 1/REPLAY_PENALTY_FACTOR as much data will be kept behind the
+// current playback position as will be kept ahead of the current playback
+// position.
+static const uint32_t REPLAY_PENALTY_FACTOR = 3;
+
+// When looking for a reusable block, scan forward this many blocks
+// from the desired "best" block location to look for free blocks,
+// before we resort to scanning the whole cache. The idea is to try to
+// store runs of stream blocks close-to-consecutively in the cache if we
+// can.
+static const uint32_t FREE_BLOCK_SCAN_LIMIT = 16;
+
+#ifdef DEBUG
+// Turn this on to do very expensive cache state validation
+// #define DEBUG_VERIFY_CACHE
+#endif
+
+class MediaCacheFlusher final : public nsIObserver,
+ public nsSupportsWeakReference {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+ static void RegisterMediaCache(MediaCache* aMediaCache);
+ static void UnregisterMediaCache(MediaCache* aMediaCache);
+
+ private:
+ MediaCacheFlusher() = default;
+ ~MediaCacheFlusher() = default;
+
+ // Singleton instance created when a first MediaCache is registered, and
+ // released when the last MediaCache is unregistered.
+ // The observer service will keep a weak reference to it, for notifications.
+ static StaticRefPtr<MediaCacheFlusher> gMediaCacheFlusher;
+
+ nsTArray<MediaCache*> mMediaCaches;
+};
+
+/* static */
+StaticRefPtr<MediaCacheFlusher> MediaCacheFlusher::gMediaCacheFlusher;
+
+NS_IMPL_ISUPPORTS(MediaCacheFlusher, nsIObserver, nsISupportsWeakReference)
+
+/* static */
+void MediaCacheFlusher::RegisterMediaCache(MediaCache* aMediaCache) {
+ NS_ASSERTION(NS_IsMainThread(), "Only call on main thread");
+
+ if (!gMediaCacheFlusher) {
+ gMediaCacheFlusher = new MediaCacheFlusher();
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService) {
+ observerService->AddObserver(gMediaCacheFlusher, "last-pb-context-exited",
+ true);
+ observerService->AddObserver(gMediaCacheFlusher,
+ "cacheservice:empty-cache", true);
+ observerService->AddObserver(
+ gMediaCacheFlusher, "contentchild:network-link-type-changed", true);
+ observerService->AddObserver(gMediaCacheFlusher,
+ NS_NETWORK_LINK_TYPE_TOPIC, true);
+ }
+ }
+
+ gMediaCacheFlusher->mMediaCaches.AppendElement(aMediaCache);
+}
+
+/* static */
+void MediaCacheFlusher::UnregisterMediaCache(MediaCache* aMediaCache) {
+ NS_ASSERTION(NS_IsMainThread(), "Only call on main thread");
+
+ gMediaCacheFlusher->mMediaCaches.RemoveElement(aMediaCache);
+
+ if (gMediaCacheFlusher->mMediaCaches.Length() == 0) {
+ gMediaCacheFlusher = nullptr;
+ }
+}
+
+class MediaCache {
+ using AutoLock = MonitorAutoLock;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaCache)
+
+ friend class MediaCacheStream::BlockList;
+ typedef MediaCacheStream::BlockList BlockList;
+ static const int64_t BLOCK_SIZE = MediaCacheStream::BLOCK_SIZE;
+
+ // Get an instance of a MediaCache (or nullptr if initialization failed).
+ // aContentLength is the content length if known already, otherwise -1.
+ // If the length is known and considered small enough, a discrete MediaCache
+ // with memory backing will be given. Otherwise the one MediaCache with
+ // file backing will be provided.
+ // If aIsPrivateBrowsing is true, only initialization of a memory backed
+ // MediaCache will be attempted, returning nullptr if that fails.
+ static RefPtr<MediaCache> GetMediaCache(int64_t aContentLength,
+ bool aIsPrivateBrowsing);
+
+ nsISerialEventTarget* OwnerThread() const { return sThread; }
+
+ // Brutally flush the cache contents. Main thread only.
+ void Flush();
+
+ // Close all streams associated with private browsing windows. This will
+ // also remove the blocks from the cache since we don't want to leave any
+ // traces when PB is done.
+ void CloseStreamsForPrivateBrowsing();
+
+ // Cache-file access methods. These are the lowest-level cache methods.
+ // mMonitor must be held; these can be called on any thread.
+ // This can return partial reads.
+ // Note mMonitor will be dropped while doing IO. The caller need
+ // to handle changes happening when the monitor is not held.
+ nsresult ReadCacheFile(AutoLock&, int64_t aOffset, void* aData,
+ int32_t aLength, int32_t* aBytes);
+
+ // The generated IDs are always positive.
+ int64_t AllocateResourceID(AutoLock&) { return ++mNextResourceID; }
+
+ // mMonitor must be held, called on main thread.
+ // These methods are used by the stream to set up and tear down streams,
+ // and to handle reads and writes.
+ // Add aStream to the list of streams.
+ void OpenStream(AutoLock&, MediaCacheStream* aStream, bool aIsClone = false);
+ // Remove aStream from the list of streams.
+ void ReleaseStream(AutoLock&, MediaCacheStream* aStream);
+ // Free all blocks belonging to aStream.
+ void ReleaseStreamBlocks(AutoLock&, MediaCacheStream* aStream);
+ // Find a cache entry for this data, and write the data into it
+ void AllocateAndWriteBlock(
+ AutoLock&, MediaCacheStream* aStream, int32_t aStreamBlockIndex,
+ Span<const uint8_t> aData1,
+ Span<const uint8_t> aData2 = Span<const uint8_t>());
+
+ // mMonitor must be held; can be called on any thread
+ // Notify the cache that a seek has been requested. Some blocks may
+ // need to change their class between PLAYED_BLOCK and READAHEAD_BLOCK.
+ // This does not trigger channel seeks directly, the next Update()
+ // will do that if necessary. The caller will call QueueUpdate().
+ void NoteSeek(AutoLock&, MediaCacheStream* aStream, int64_t aOldOffset);
+ // Notify the cache that a block has been read from. This is used
+ // to update last-use times. The block may not actually have a
+ // cache entry yet since Read can read data from a stream's
+ // in-memory mPartialBlockBuffer while the block is only partly full,
+ // and thus hasn't yet been committed to the cache. The caller will
+ // call QueueUpdate().
+ void NoteBlockUsage(AutoLock&, MediaCacheStream* aStream, int32_t aBlockIndex,
+ int64_t aStreamOffset, MediaCacheStream::ReadMode aMode,
+ TimeStamp aNow);
+ // Mark aStream as having the block, adding it as an owner.
+ void AddBlockOwnerAsReadahead(AutoLock&, int32_t aBlockIndex,
+ MediaCacheStream* aStream,
+ int32_t aStreamBlockIndex);
+
+ // This queues a call to Update() on the media cache thread.
+ void QueueUpdate(AutoLock&);
+
+ // Notify all streams for the resource ID that the suspended status changed
+ // at the end of MediaCache::Update.
+ void QueueSuspendedStatusUpdate(AutoLock&, int64_t aResourceID);
+
+ // Updates the cache state asynchronously on the media cache thread:
+ // -- try to trim the cache back to its desired size, if necessary
+ // -- suspend channels that are going to read data that's lower priority
+ // than anything currently cached
+ // -- resume channels that are going to read data that's higher priority
+ // than something currently cached
+ // -- seek channels that need to seek to a new location
+ void Update();
+
+#ifdef DEBUG_VERIFY_CACHE
+ // Verify invariants, especially block list invariants
+ void Verify(AutoLock&);
+#else
+ void Verify(AutoLock&) {}
+#endif
+
+ mozilla::Monitor& Monitor() {
+ // This method should only be called outside the main thread.
+ // The MOZ_DIAGNOSTIC_ASSERT(!NS_IsMainThread()) assertion should be
+ // re-added as part of bug 1464045
+ return mMonitor;
+ }
+
+ // Polls whether we're on a cellular network connection, and posts a task
+ // to the MediaCache thread to set the value of MediaCache::sOnCellular.
+ // Call on main thread only.
+ static void UpdateOnCellular();
+
+ /**
+ * An iterator that makes it easy to iterate through all streams that
+ * have a given resource ID and are not closed.
+ * Must be used while holding the media cache lock.
+ */
+ class ResourceStreamIterator {
+ public:
+ ResourceStreamIterator(MediaCache* aMediaCache, int64_t aResourceID)
+ : mMediaCache(aMediaCache), mResourceID(aResourceID), mNext(0) {
+ aMediaCache->mMonitor.AssertCurrentThreadOwns();
+ }
+ MediaCacheStream* Next(AutoLock& aLock) {
+ while (mNext < mMediaCache->mStreams.Length()) {
+ MediaCacheStream* stream = mMediaCache->mStreams[mNext];
+ ++mNext;
+ if (stream->GetResourceID() == mResourceID && !stream->IsClosed(aLock))
+ return stream;
+ }
+ return nullptr;
+ }
+
+ private:
+ MediaCache* mMediaCache;
+ int64_t mResourceID;
+ uint32_t mNext;
+ };
+
+ protected:
+ explicit MediaCache(MediaBlockCacheBase* aCache)
+ : mMonitor("MediaCache.mMonitor"),
+ mBlockCache(aCache),
+ mUpdateQueued(false)
+#ifdef DEBUG
+ ,
+ mInUpdate(false)
+#endif
+ {
+ NS_ASSERTION(NS_IsMainThread(), "Only construct MediaCache on main thread");
+ MOZ_COUNT_CTOR(MediaCache);
+ MediaCacheFlusher::RegisterMediaCache(this);
+ UpdateOnCellular();
+ }
+
+ ~MediaCache() {
+ NS_ASSERTION(NS_IsMainThread(), "Only destroy MediaCache on main thread");
+ if (this == gMediaCache) {
+ LOG("~MediaCache(Global file-backed MediaCache)");
+ // This is the file-backed MediaCache, reset the global pointer.
+ gMediaCache = nullptr;
+ } else {
+ LOG("~MediaCache(Memory-backed MediaCache %p)", this);
+ }
+ MediaCacheFlusher::UnregisterMediaCache(this);
+ NS_ASSERTION(mStreams.IsEmpty(), "Stream(s) still open!");
+ Truncate();
+ NS_ASSERTION(mIndex.Length() == 0, "Blocks leaked?");
+
+ MOZ_COUNT_DTOR(MediaCache);
+ }
+
+ static size_t CacheSize() {
+ MOZ_ASSERT(sThread->IsOnCurrentThread());
+ return sOnCellular ? StaticPrefs::media_cache_size_cellular()
+ : StaticPrefs::media_cache_size();
+ }
+
+ static size_t ReadaheadLimit() {
+ MOZ_ASSERT(sThread->IsOnCurrentThread());
+ return sOnCellular ? StaticPrefs::media_cache_readahead_limit_cellular()
+ : StaticPrefs::media_cache_readahead_limit();
+ }
+
+ static size_t ResumeThreshold() {
+ return sOnCellular ? StaticPrefs::media_cache_resume_threshold_cellular()
+ : StaticPrefs::media_cache_resume_threshold();
+ }
+
+ // Find a free or reusable block and return its index. If there are no
+ // free blocks and no reusable blocks, add a new block to the cache
+ // and return it. Can return -1 on OOM.
+ int32_t FindBlockForIncomingData(AutoLock&, TimeStamp aNow,
+ MediaCacheStream* aStream,
+ int32_t aStreamBlockIndex);
+ // Find a reusable block --- a free block, if there is one, otherwise
+ // the reusable block with the latest predicted-next-use, or -1 if
+ // there aren't any freeable blocks. Only block indices less than
+ // aMaxSearchBlockIndex are considered. If aForStream is non-null,
+ // then aForStream and aForStreamBlock indicate what media data will
+ // be placed; FindReusableBlock will favour returning free blocks
+ // near other blocks for that point in the stream.
+ int32_t FindReusableBlock(AutoLock&, TimeStamp aNow,
+ MediaCacheStream* aForStream,
+ int32_t aForStreamBlock,
+ int32_t aMaxSearchBlockIndex);
+ bool BlockIsReusable(AutoLock&, int32_t aBlockIndex);
+ // Given a list of blocks sorted with the most reusable blocks at the
+ // end, find the last block whose stream is not pinned (if any)
+ // and whose cache entry index is less than aBlockIndexLimit
+ // and append it to aResult.
+ void AppendMostReusableBlock(AutoLock&, BlockList* aBlockList,
+ nsTArray<uint32_t>* aResult,
+ int32_t aBlockIndexLimit);
+
+ enum BlockClass {
+ // block belongs to mMetadataBlockList because data has been consumed
+ // from it in "metadata mode" --- in particular blocks read during
+ // Ogg seeks go into this class. These blocks may have played data
+ // in them too.
+ METADATA_BLOCK,
+ // block belongs to mPlayedBlockList because its offset is
+ // less than the stream's current reader position
+ PLAYED_BLOCK,
+ // block belongs to the stream's mReadaheadBlockList because its
+ // offset is greater than or equal to the stream's current
+ // reader position
+ READAHEAD_BLOCK
+ };
+
+ struct BlockOwner {
+ constexpr BlockOwner() = default;
+
+ // The stream that owns this block, or null if the block is free.
+ MediaCacheStream* mStream = nullptr;
+ // The block index in the stream. Valid only if mStream is non-null.
+ // Initialized to an insane value to highlight misuse.
+ uint32_t mStreamBlock = UINT32_MAX;
+ // Time at which this block was last used. Valid only if
+ // mClass is METADATA_BLOCK or PLAYED_BLOCK.
+ TimeStamp mLastUseTime;
+ BlockClass mClass = READAHEAD_BLOCK;
+ };
+
+ struct Block {
+ // Free blocks have an empty mOwners array
+ nsTArray<BlockOwner> mOwners;
+ };
+
+ // Get the BlockList that the block should belong to given its
+ // current owner
+ BlockList* GetListForBlock(AutoLock&, BlockOwner* aBlock);
+ // Get the BlockOwner for the given block index and owning stream
+ // (returns null if the stream does not own the block)
+ BlockOwner* GetBlockOwner(AutoLock&, int32_t aBlockIndex,
+ MediaCacheStream* aStream);
+ // Returns true iff the block is free
+ bool IsBlockFree(int32_t aBlockIndex) {
+ return mIndex[aBlockIndex].mOwners.IsEmpty();
+ }
+ // Add the block to the free list and mark its streams as not having
+ // the block in cache
+ void FreeBlock(AutoLock&, int32_t aBlock);
+ // Mark aStream as not having the block, removing it as an owner. If
+ // the block has no more owners it's added to the free list.
+ void RemoveBlockOwner(AutoLock&, int32_t aBlockIndex,
+ MediaCacheStream* aStream);
+ // Swap all metadata associated with the two blocks. The caller
+ // is responsible for swapping up any cache file state.
+ void SwapBlocks(AutoLock&, int32_t aBlockIndex1, int32_t aBlockIndex2);
+ // Insert the block into the readahead block list for the stream
+ // at the right point in the list.
+ void InsertReadaheadBlock(AutoLock&, BlockOwner* aBlockOwner,
+ int32_t aBlockIndex);
+
+ // Guess the duration until block aBlock will be next used
+ TimeDuration PredictNextUse(AutoLock&, TimeStamp aNow, int32_t aBlock);
+ // Guess the duration until the next incoming data on aStream will be used
+ TimeDuration PredictNextUseForIncomingData(AutoLock&,
+ MediaCacheStream* aStream);
+
+ // Truncate the file and index array if there are free blocks at the
+ // end
+ void Truncate();
+
+ void FlushInternal(AutoLock&);
+
+ // There is at most one file-backed media cache.
+ // It is owned by all MediaCacheStreams that use it.
+ // This is a raw pointer set by GetMediaCache(), and reset by ~MediaCache(),
+ // both on the main thread; and is not accessed anywhere else.
+ static inline MediaCache* gMediaCache = nullptr;
+
+ // This member is main-thread only. It's used to allocate unique
+ // resource IDs to streams.
+ int64_t mNextResourceID = 0;
+
+ // The monitor protects all the data members here. Also, off-main-thread
+ // readers that need to block will Wait() on this monitor. When new
+ // data becomes available in the cache, we NotifyAll() on this monitor.
+ mozilla::Monitor mMonitor MOZ_UNANNOTATED;
+ // This must always be accessed when the monitor is held.
+ nsTArray<MediaCacheStream*> mStreams;
+ // The Blocks describing the cache entries.
+ nsTArray<Block> mIndex;
+
+ RefPtr<MediaBlockCacheBase> mBlockCache;
+ // The list of free blocks; they are not ordered.
+ BlockList mFreeBlocks;
+ // True if an event to run Update() has been queued but not processed
+ bool mUpdateQueued;
+#ifdef DEBUG
+ bool mInUpdate;
+#endif
+ // A list of resource IDs to notify about the change in suspended status.
+ nsTArray<int64_t> mSuspendedStatusToNotify;
+ // The thread on which we will run data callbacks from the channels.
+ // Note this thread is shared among all MediaCache instances.
+ static inline StaticRefPtr<nsIThread> sThread;
+ // True if we've tried to init sThread. Note we try once only so it is safe
+ // to access sThread on all threads.
+ static inline bool sThreadInit = false;
+
+ private:
+ // MediaCache thread only. True if we're on a cellular network connection.
+ static inline bool sOnCellular = false;
+
+ // Try to trim the cache back to its desired size, if necessary. Return the
+ // amount of free block counts after trimming.
+ int32_t TrimCacheIfNeeded(AutoLock& aLock, const TimeStamp& aNow);
+
+ struct StreamAction {
+ enum { NONE, SEEK, RESUME, SUSPEND } mTag = NONE;
+ // Members for 'SEEK' only.
+ bool mResume = false;
+ int64_t mSeekTarget = -1;
+ };
+ // In each update, media cache would determine an action for each stream,
+ // possible actions are: keeping the stream unchanged, seeking to the new
+ // position, resuming its channel or suspending its channel. The action would
+ // be determined by considering a lot of different factors, eg. stream's data
+ // offset and length, how many free or reusable blocks are avaliable, the
+ // predicted time for the next block...e.t.c. This function will write the
+ // corresponding action for each stream in `mStreams` into `aActions`.
+ void DetermineActionsForStreams(AutoLock& aLock, const TimeStamp& aNow,
+ nsTArray<StreamAction>& aActions,
+ int32_t aFreeBlockCount);
+
+ // Used by MediaCacheStream::GetDebugInfo() only for debugging.
+ // Don't add new callers to this function.
+ friend void MediaCacheStream::GetDebugInfo(
+ dom::MediaCacheStreamDebugInfo& aInfo);
+ mozilla::Monitor& GetMonitorOnTheMainThread() {
+ MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
+ return mMonitor;
+ }
+};
+
+void MediaCache::UpdateOnCellular() {
+ NS_ASSERTION(NS_IsMainThread(),
+ "Only call on main thread"); // JNI required on Android...
+ bool onCellular = OnCellularConnection();
+ LOG("MediaCache::UpdateOnCellular() onCellular=%d", onCellular);
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ "MediaCache::UpdateOnCellular", [=]() { sOnCellular = onCellular; });
+ sThread->Dispatch(r.forget());
+}
+
+NS_IMETHODIMP
+MediaCacheFlusher::Observe(nsISupports* aSubject, char const* aTopic,
+ char16_t const* aData) {
+ NS_ASSERTION(NS_IsMainThread(), "Only call on main thread");
+
+ if (strcmp(aTopic, "last-pb-context-exited") == 0) {
+ for (MediaCache* mc : mMediaCaches) {
+ mc->CloseStreamsForPrivateBrowsing();
+ }
+ return NS_OK;
+ }
+ if (strcmp(aTopic, "cacheservice:empty-cache") == 0) {
+ for (MediaCache* mc : mMediaCaches) {
+ mc->Flush();
+ }
+ return NS_OK;
+ }
+ if (strcmp(aTopic, "contentchild:network-link-type-changed") == 0 ||
+ strcmp(aTopic, NS_NETWORK_LINK_TYPE_TOPIC) == 0) {
+ MediaCache::UpdateOnCellular();
+ }
+ return NS_OK;
+}
+
+MediaCacheStream::MediaCacheStream(ChannelMediaResource* aClient,
+ bool aIsPrivateBrowsing)
+ : mMediaCache(nullptr),
+ mClient(aClient),
+ mIsTransportSeekable(false),
+ mCacheSuspended(false),
+ mChannelEnded(false),
+ mStreamOffset(0),
+ mPlaybackBytesPerSecond(10000),
+ mPinCount(0),
+ mNotifyDataEndedStatus(NS_ERROR_NOT_INITIALIZED),
+ mIsPrivateBrowsing(aIsPrivateBrowsing) {}
+
+size_t MediaCacheStream::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ AutoLock lock(mMediaCache->Monitor());
+
+ // Looks like these are not owned:
+ // - mClient
+ size_t size = mBlocks.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ size += mReadaheadBlocks.SizeOfExcludingThis(aMallocSizeOf);
+ size += mMetadataBlocks.SizeOfExcludingThis(aMallocSizeOf);
+ size += mPlayedBlocks.SizeOfExcludingThis(aMallocSizeOf);
+ size += aMallocSizeOf(mPartialBlockBuffer.get());
+
+ return size;
+}
+
+size_t MediaCacheStream::BlockList::SizeOfExcludingThis(
+ MallocSizeOf aMallocSizeOf) const {
+ return mEntries.ShallowSizeOfExcludingThis(aMallocSizeOf);
+}
+
+void MediaCacheStream::BlockList::AddFirstBlock(int32_t aBlock) {
+ NS_ASSERTION(!mEntries.GetEntry(aBlock), "Block already in list");
+ Entry* entry = mEntries.PutEntry(aBlock);
+
+ if (mFirstBlock < 0) {
+ entry->mNextBlock = entry->mPrevBlock = aBlock;
+ } else {
+ entry->mNextBlock = mFirstBlock;
+ entry->mPrevBlock = mEntries.GetEntry(mFirstBlock)->mPrevBlock;
+ mEntries.GetEntry(entry->mNextBlock)->mPrevBlock = aBlock;
+ mEntries.GetEntry(entry->mPrevBlock)->mNextBlock = aBlock;
+ }
+ mFirstBlock = aBlock;
+ ++mCount;
+}
+
+void MediaCacheStream::BlockList::AddAfter(int32_t aBlock, int32_t aBefore) {
+ NS_ASSERTION(!mEntries.GetEntry(aBlock), "Block already in list");
+ Entry* entry = mEntries.PutEntry(aBlock);
+
+ Entry* addAfter = mEntries.GetEntry(aBefore);
+ NS_ASSERTION(addAfter, "aBefore not in list");
+
+ entry->mNextBlock = addAfter->mNextBlock;
+ entry->mPrevBlock = aBefore;
+ mEntries.GetEntry(entry->mNextBlock)->mPrevBlock = aBlock;
+ mEntries.GetEntry(entry->mPrevBlock)->mNextBlock = aBlock;
+ ++mCount;
+}
+
+void MediaCacheStream::BlockList::RemoveBlock(int32_t aBlock) {
+ Entry* entry = mEntries.GetEntry(aBlock);
+ MOZ_DIAGNOSTIC_ASSERT(entry, "Block not in list");
+
+ if (entry->mNextBlock == aBlock) {
+ MOZ_DIAGNOSTIC_ASSERT(entry->mPrevBlock == aBlock,
+ "Linked list inconsistency");
+ MOZ_DIAGNOSTIC_ASSERT(mFirstBlock == aBlock, "Linked list inconsistency");
+ mFirstBlock = -1;
+ } else {
+ if (mFirstBlock == aBlock) {
+ mFirstBlock = entry->mNextBlock;
+ }
+ mEntries.GetEntry(entry->mNextBlock)->mPrevBlock = entry->mPrevBlock;
+ mEntries.GetEntry(entry->mPrevBlock)->mNextBlock = entry->mNextBlock;
+ }
+ mEntries.RemoveEntry(entry);
+ --mCount;
+}
+
+int32_t MediaCacheStream::BlockList::GetLastBlock() const {
+ if (mFirstBlock < 0) return -1;
+ return mEntries.GetEntry(mFirstBlock)->mPrevBlock;
+}
+
+int32_t MediaCacheStream::BlockList::GetNextBlock(int32_t aBlock) const {
+ int32_t block = mEntries.GetEntry(aBlock)->mNextBlock;
+ if (block == mFirstBlock) return -1;
+ return block;
+}
+
+int32_t MediaCacheStream::BlockList::GetPrevBlock(int32_t aBlock) const {
+ if (aBlock == mFirstBlock) return -1;
+ return mEntries.GetEntry(aBlock)->mPrevBlock;
+}
+
+#ifdef DEBUG
+void MediaCacheStream::BlockList::Verify() {
+ int32_t count = 0;
+ if (mFirstBlock >= 0) {
+ int32_t block = mFirstBlock;
+ do {
+ Entry* entry = mEntries.GetEntry(block);
+ NS_ASSERTION(mEntries.GetEntry(entry->mNextBlock)->mPrevBlock == block,
+ "Bad prev link");
+ NS_ASSERTION(mEntries.GetEntry(entry->mPrevBlock)->mNextBlock == block,
+ "Bad next link");
+ block = entry->mNextBlock;
+ ++count;
+ } while (block != mFirstBlock);
+ }
+ NS_ASSERTION(count == mCount, "Bad count");
+}
+#endif
+
+static void UpdateSwappedBlockIndex(int32_t* aBlockIndex, int32_t aBlock1Index,
+ int32_t aBlock2Index) {
+ int32_t index = *aBlockIndex;
+ if (index == aBlock1Index) {
+ *aBlockIndex = aBlock2Index;
+ } else if (index == aBlock2Index) {
+ *aBlockIndex = aBlock1Index;
+ }
+}
+
+void MediaCacheStream::BlockList::NotifyBlockSwapped(int32_t aBlockIndex1,
+ int32_t aBlockIndex2) {
+ Entry* e1 = mEntries.GetEntry(aBlockIndex1);
+ Entry* e2 = mEntries.GetEntry(aBlockIndex2);
+ int32_t e1Prev = -1, e1Next = -1, e2Prev = -1, e2Next = -1;
+
+ // Fix mFirstBlock
+ UpdateSwappedBlockIndex(&mFirstBlock, aBlockIndex1, aBlockIndex2);
+
+ // Fix mNextBlock/mPrevBlock links. First capture previous/next links
+ // so we don't get confused due to aliasing.
+ if (e1) {
+ e1Prev = e1->mPrevBlock;
+ e1Next = e1->mNextBlock;
+ }
+ if (e2) {
+ e2Prev = e2->mPrevBlock;
+ e2Next = e2->mNextBlock;
+ }
+ // Update the entries.
+ if (e1) {
+ mEntries.GetEntry(e1Prev)->mNextBlock = aBlockIndex2;
+ mEntries.GetEntry(e1Next)->mPrevBlock = aBlockIndex2;
+ }
+ if (e2) {
+ mEntries.GetEntry(e2Prev)->mNextBlock = aBlockIndex1;
+ mEntries.GetEntry(e2Next)->mPrevBlock = aBlockIndex1;
+ }
+
+ // Fix hashtable keys. First remove stale entries.
+ if (e1) {
+ e1Prev = e1->mPrevBlock;
+ e1Next = e1->mNextBlock;
+ mEntries.RemoveEntry(e1);
+ // Refresh pointer after hashtable mutation.
+ e2 = mEntries.GetEntry(aBlockIndex2);
+ }
+ if (e2) {
+ e2Prev = e2->mPrevBlock;
+ e2Next = e2->mNextBlock;
+ mEntries.RemoveEntry(e2);
+ }
+ // Put new entries back.
+ if (e1) {
+ e1 = mEntries.PutEntry(aBlockIndex2);
+ e1->mNextBlock = e1Next;
+ e1->mPrevBlock = e1Prev;
+ }
+ if (e2) {
+ e2 = mEntries.PutEntry(aBlockIndex1);
+ e2->mNextBlock = e2Next;
+ e2->mPrevBlock = e2Prev;
+ }
+}
+
+void MediaCache::FlushInternal(AutoLock& aLock) {
+ for (uint32_t blockIndex = 0; blockIndex < mIndex.Length(); ++blockIndex) {
+ FreeBlock(aLock, blockIndex);
+ }
+
+ // Truncate index array.
+ Truncate();
+ NS_ASSERTION(mIndex.Length() == 0, "Blocks leaked?");
+ // Reset block cache to its pristine state.
+ mBlockCache->Flush();
+}
+
+void MediaCache::Flush() {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ "MediaCache::Flush", [self = RefPtr<MediaCache>(this)]() mutable {
+ AutoLock lock(self->mMonitor);
+ self->FlushInternal(lock);
+ // Ensure MediaCache is deleted on the main thread.
+ NS_ReleaseOnMainThread("MediaCache::Flush", self.forget());
+ });
+ sThread->Dispatch(r.forget());
+}
+
+void MediaCache::CloseStreamsForPrivateBrowsing() {
+ MOZ_ASSERT(NS_IsMainThread());
+ sThread->Dispatch(NS_NewRunnableFunction(
+ "MediaCache::CloseStreamsForPrivateBrowsing",
+ [self = RefPtr<MediaCache>(this)]() mutable {
+ AutoLock lock(self->mMonitor);
+ // Copy mStreams since CloseInternal() will change the array.
+ for (MediaCacheStream* s : self->mStreams.Clone()) {
+ if (s->mIsPrivateBrowsing) {
+ s->CloseInternal(lock);
+ }
+ }
+ // Ensure MediaCache is deleted on the main thread.
+ NS_ReleaseOnMainThread("MediaCache::CloseStreamsForPrivateBrowsing",
+ self.forget());
+ }));
+}
+
+/* static */
+RefPtr<MediaCache> MediaCache::GetMediaCache(int64_t aContentLength,
+ bool aIsPrivateBrowsing) {
+ NS_ASSERTION(NS_IsMainThread(), "Only call on main thread");
+
+ if (!sThreadInit) {
+ sThreadInit = true;
+ nsCOMPtr<nsIThread> thread;
+ nsresult rv = NS_NewNamedThread("MediaCache", getter_AddRefs(thread));
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to create a thread for MediaCache.");
+ return nullptr;
+ }
+ sThread = ToRefPtr(std::move(thread));
+
+ static struct ClearThread {
+ // Called during shutdown to clear sThread.
+ void operator=(std::nullptr_t) {
+ MOZ_ASSERT(sThread, "We should only clear sThread once.");
+ sThread->Shutdown();
+ sThread = nullptr;
+ }
+ } sClearThread;
+ ClearOnShutdown(&sClearThread, ShutdownPhase::XPCOMShutdownThreads);
+ }
+
+ if (!sThread) {
+ return nullptr;
+ }
+
+ const int64_t mediaMemoryCacheMaxSize =
+ static_cast<int64_t>(StaticPrefs::media_memory_cache_max_size()) * 1024;
+
+ // Force usage of in-memory cache if we are in private browsing mode
+ // and the forceMediaMemoryCache pref is set
+ // We will not attempt to create an on-disk cache if this is the case
+ const bool forceMediaMemoryCache =
+ aIsPrivateBrowsing &&
+ StaticPrefs::browser_privatebrowsing_forceMediaMemoryCache();
+
+ // Alternatively, use an in-memory cache if the media will fit entirely
+ // in memory
+ // aContentLength < 0 indicates we do not know content's actual size
+ const bool contentFitsInMediaMemoryCache =
+ (aContentLength > 0) && (aContentLength <= mediaMemoryCacheMaxSize);
+
+ // Try to allocate a memory cache for our content
+ if (contentFitsInMediaMemoryCache || forceMediaMemoryCache) {
+ // Figure out how large our cache should be
+ int64_t cacheSize = 0;
+ if (contentFitsInMediaMemoryCache) {
+ cacheSize = aContentLength;
+ } else if (forceMediaMemoryCache) {
+ // Unknown content length, we'll give the maximum allowed cache size
+ // just to be sure.
+ if (aContentLength < 0) {
+ cacheSize = mediaMemoryCacheMaxSize;
+ } else {
+ // If the content length is less than the maximum allowed cache size,
+ // use that, otherwise we cap it to max size.
+ cacheSize = std::min(aContentLength, mediaMemoryCacheMaxSize);
+ }
+ }
+
+ RefPtr<MediaBlockCacheBase> bc = new MemoryBlockCache(cacheSize);
+ nsresult rv = bc->Init();
+ if (NS_SUCCEEDED(rv)) {
+ RefPtr<MediaCache> mc = new MediaCache(bc);
+ LOG("GetMediaCache(%" PRIi64 ") -> Memory MediaCache %p", aContentLength,
+ mc.get());
+ return mc;
+ }
+
+ // MemoryBlockCache initialization failed.
+ // If we require use of a memory media cache, we will bail here.
+ // Otherwise use a file-backed MediaCache below.
+ if (forceMediaMemoryCache) {
+ return nullptr;
+ }
+ }
+
+ if (gMediaCache) {
+ LOG("GetMediaCache(%" PRIi64 ") -> Existing file-backed MediaCache",
+ aContentLength);
+ return gMediaCache;
+ }
+
+ RefPtr<MediaBlockCacheBase> bc = new FileBlockCache();
+ nsresult rv = bc->Init();
+ if (NS_SUCCEEDED(rv)) {
+ gMediaCache = new MediaCache(bc);
+ LOG("GetMediaCache(%" PRIi64 ") -> Created file-backed MediaCache",
+ aContentLength);
+ } else {
+ LOG("GetMediaCache(%" PRIi64 ") -> Failed to create file-backed MediaCache",
+ aContentLength);
+ }
+
+ return gMediaCache;
+}
+
+nsresult MediaCache::ReadCacheFile(AutoLock&, int64_t aOffset, void* aData,
+ int32_t aLength, int32_t* aBytes) {
+ if (!mBlockCache) {
+ return NS_ERROR_FAILURE;
+ }
+ return mBlockCache->Read(aOffset, reinterpret_cast<uint8_t*>(aData), aLength,
+ aBytes);
+}
+
+// Allowed range is whatever can be accessed with an int32_t block index.
+static bool IsOffsetAllowed(int64_t aOffset) {
+ return aOffset < (int64_t(INT32_MAX) + 1) * MediaCache::BLOCK_SIZE &&
+ aOffset >= 0;
+}
+
+// Convert 64-bit offset to 32-bit block index.
+// Assumes offset range-check was already done.
+static int32_t OffsetToBlockIndexUnchecked(int64_t aOffset) {
+ // Still check for allowed range in debug builds, to catch out-of-range
+ // issues early during development.
+ MOZ_ASSERT(IsOffsetAllowed(aOffset));
+ return int32_t(aOffset / MediaCache::BLOCK_SIZE);
+}
+
+// Convert 64-bit offset to 32-bit block index. -1 if out of allowed range.
+static int32_t OffsetToBlockIndex(int64_t aOffset) {
+ return IsOffsetAllowed(aOffset) ? OffsetToBlockIndexUnchecked(aOffset) : -1;
+}
+
+// Convert 64-bit offset to 32-bit offset inside a block.
+// Will not fail (even if offset is outside allowed range), so there is no
+// need to check for errors.
+static int32_t OffsetInBlock(int64_t aOffset) {
+ // Still check for allowed range in debug builds, to catch out-of-range
+ // issues early during development.
+ MOZ_ASSERT(IsOffsetAllowed(aOffset));
+ return int32_t(aOffset % MediaCache::BLOCK_SIZE);
+}
+
+int32_t MediaCache::FindBlockForIncomingData(AutoLock& aLock, TimeStamp aNow,
+ MediaCacheStream* aStream,
+ int32_t aStreamBlockIndex) {
+ MOZ_ASSERT(sThread->IsOnCurrentThread());
+
+ int32_t blockIndex =
+ FindReusableBlock(aLock, aNow, aStream, aStreamBlockIndex, INT32_MAX);
+
+ if (blockIndex < 0 || !IsBlockFree(blockIndex)) {
+ // The block returned is already allocated.
+ // Don't reuse it if a) there's room to expand the cache or
+ // b) the data we're going to store in the free block is not higher
+ // priority than the data already stored in the free block.
+ // The latter can lead us to go over the cache limit a bit.
+ if ((mIndex.Length() <
+ uint32_t(mBlockCache->GetMaxBlocks(MediaCache::CacheSize())) ||
+ blockIndex < 0 ||
+ PredictNextUseForIncomingData(aLock, aStream) >=
+ PredictNextUse(aLock, aNow, blockIndex))) {
+ blockIndex = mIndex.Length();
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier.
+ mIndex.AppendElement();
+ mFreeBlocks.AddFirstBlock(blockIndex);
+ return blockIndex;
+ }
+ }
+
+ return blockIndex;
+}
+
+bool MediaCache::BlockIsReusable(AutoLock&, int32_t aBlockIndex) {
+ Block* block = &mIndex[aBlockIndex];
+ for (uint32_t i = 0; i < block->mOwners.Length(); ++i) {
+ MediaCacheStream* stream = block->mOwners[i].mStream;
+ if (stream->mPinCount > 0 ||
+ uint32_t(OffsetToBlockIndex(stream->mStreamOffset)) ==
+ block->mOwners[i].mStreamBlock) {
+ return false;
+ }
+ }
+ return true;
+}
+
+void MediaCache::AppendMostReusableBlock(AutoLock& aLock, BlockList* aBlockList,
+ nsTArray<uint32_t>* aResult,
+ int32_t aBlockIndexLimit) {
+ int32_t blockIndex = aBlockList->GetLastBlock();
+ if (blockIndex < 0) return;
+ do {
+ // Don't consider blocks for pinned streams, or blocks that are
+ // beyond the specified limit, or a block that contains a stream's
+ // current read position (such a block contains both played data
+ // and readahead data)
+ if (blockIndex < aBlockIndexLimit && BlockIsReusable(aLock, blockIndex)) {
+ aResult->AppendElement(blockIndex);
+ return;
+ }
+ blockIndex = aBlockList->GetPrevBlock(blockIndex);
+ } while (blockIndex >= 0);
+}
+
+int32_t MediaCache::FindReusableBlock(AutoLock& aLock, TimeStamp aNow,
+ MediaCacheStream* aForStream,
+ int32_t aForStreamBlock,
+ int32_t aMaxSearchBlockIndex) {
+ MOZ_ASSERT(sThread->IsOnCurrentThread());
+
+ uint32_t length =
+ std::min(uint32_t(aMaxSearchBlockIndex), uint32_t(mIndex.Length()));
+
+ if (aForStream && aForStreamBlock > 0 &&
+ uint32_t(aForStreamBlock) <= aForStream->mBlocks.Length()) {
+ int32_t prevCacheBlock = aForStream->mBlocks[aForStreamBlock - 1];
+ if (prevCacheBlock >= 0) {
+ uint32_t freeBlockScanEnd =
+ std::min(length, prevCacheBlock + FREE_BLOCK_SCAN_LIMIT);
+ for (uint32_t i = prevCacheBlock; i < freeBlockScanEnd; ++i) {
+ if (IsBlockFree(i)) return i;
+ }
+ }
+ }
+
+ if (!mFreeBlocks.IsEmpty()) {
+ int32_t blockIndex = mFreeBlocks.GetFirstBlock();
+ do {
+ if (blockIndex < aMaxSearchBlockIndex) return blockIndex;
+ blockIndex = mFreeBlocks.GetNextBlock(blockIndex);
+ } while (blockIndex >= 0);
+ }
+
+ // Build a list of the blocks we should consider for the "latest
+ // predicted time of next use". We can exploit the fact that the block
+ // linked lists are ordered by increasing time of next use. This is
+ // actually the whole point of having the linked lists.
+ AutoTArray<uint32_t, 8> candidates;
+ for (uint32_t i = 0; i < mStreams.Length(); ++i) {
+ MediaCacheStream* stream = mStreams[i];
+ if (stream->mPinCount > 0) {
+ // No point in even looking at this stream's blocks
+ continue;
+ }
+
+ AppendMostReusableBlock(aLock, &stream->mMetadataBlocks, &candidates,
+ length);
+ AppendMostReusableBlock(aLock, &stream->mPlayedBlocks, &candidates, length);
+
+ // Don't consider readahead blocks in non-seekable streams. If we
+ // remove the block we won't be able to seek back to read it later.
+ if (stream->mIsTransportSeekable) {
+ AppendMostReusableBlock(aLock, &stream->mReadaheadBlocks, &candidates,
+ length);
+ }
+ }
+
+ TimeDuration latestUse;
+ int32_t latestUseBlock = -1;
+ for (uint32_t i = 0; i < candidates.Length(); ++i) {
+ TimeDuration nextUse = PredictNextUse(aLock, aNow, candidates[i]);
+ if (nextUse > latestUse) {
+ latestUse = nextUse;
+ latestUseBlock = candidates[i];
+ }
+ }
+
+ return latestUseBlock;
+}
+
+MediaCache::BlockList* MediaCache::GetListForBlock(AutoLock&,
+ BlockOwner* aBlock) {
+ switch (aBlock->mClass) {
+ case METADATA_BLOCK:
+ NS_ASSERTION(aBlock->mStream, "Metadata block has no stream?");
+ return &aBlock->mStream->mMetadataBlocks;
+ case PLAYED_BLOCK:
+ NS_ASSERTION(aBlock->mStream, "Metadata block has no stream?");
+ return &aBlock->mStream->mPlayedBlocks;
+ case READAHEAD_BLOCK:
+ NS_ASSERTION(aBlock->mStream, "Readahead block has no stream?");
+ return &aBlock->mStream->mReadaheadBlocks;
+ default:
+ NS_ERROR("Invalid block class");
+ return nullptr;
+ }
+}
+
+MediaCache::BlockOwner* MediaCache::GetBlockOwner(AutoLock&,
+ int32_t aBlockIndex,
+ MediaCacheStream* aStream) {
+ Block* block = &mIndex[aBlockIndex];
+ for (uint32_t i = 0; i < block->mOwners.Length(); ++i) {
+ if (block->mOwners[i].mStream == aStream) return &block->mOwners[i];
+ }
+ return nullptr;
+}
+
+void MediaCache::SwapBlocks(AutoLock& aLock, int32_t aBlockIndex1,
+ int32_t aBlockIndex2) {
+ Block* block1 = &mIndex[aBlockIndex1];
+ Block* block2 = &mIndex[aBlockIndex2];
+
+ block1->mOwners.SwapElements(block2->mOwners);
+
+ // Now all references to block1 have to be replaced with block2 and
+ // vice versa.
+ // First update stream references to blocks via mBlocks.
+ const Block* blocks[] = {block1, block2};
+ int32_t blockIndices[] = {aBlockIndex1, aBlockIndex2};
+ for (int32_t i = 0; i < 2; ++i) {
+ for (uint32_t j = 0; j < blocks[i]->mOwners.Length(); ++j) {
+ const BlockOwner* b = &blocks[i]->mOwners[j];
+ b->mStream->mBlocks[b->mStreamBlock] = blockIndices[i];
+ }
+ }
+
+ // Now update references to blocks in block lists.
+ mFreeBlocks.NotifyBlockSwapped(aBlockIndex1, aBlockIndex2);
+
+ nsTHashSet<MediaCacheStream*> visitedStreams;
+
+ for (int32_t i = 0; i < 2; ++i) {
+ for (uint32_t j = 0; j < blocks[i]->mOwners.Length(); ++j) {
+ MediaCacheStream* stream = blocks[i]->mOwners[j].mStream;
+ // Make sure that we don't update the same stream twice --- that
+ // would result in swapping the block references back again!
+ if (!visitedStreams.EnsureInserted(stream)) continue;
+ stream->mReadaheadBlocks.NotifyBlockSwapped(aBlockIndex1, aBlockIndex2);
+ stream->mPlayedBlocks.NotifyBlockSwapped(aBlockIndex1, aBlockIndex2);
+ stream->mMetadataBlocks.NotifyBlockSwapped(aBlockIndex1, aBlockIndex2);
+ }
+ }
+
+ Verify(aLock);
+}
+
+void MediaCache::RemoveBlockOwner(AutoLock& aLock, int32_t aBlockIndex,
+ MediaCacheStream* aStream) {
+ Block* block = &mIndex[aBlockIndex];
+ for (uint32_t i = 0; i < block->mOwners.Length(); ++i) {
+ BlockOwner* bo = &block->mOwners[i];
+ if (bo->mStream == aStream) {
+ GetListForBlock(aLock, bo)->RemoveBlock(aBlockIndex);
+ bo->mStream->mBlocks[bo->mStreamBlock] = -1;
+ block->mOwners.RemoveElementAt(i);
+ if (block->mOwners.IsEmpty()) {
+ mFreeBlocks.AddFirstBlock(aBlockIndex);
+ }
+ return;
+ }
+ }
+}
+
+void MediaCache::AddBlockOwnerAsReadahead(AutoLock& aLock, int32_t aBlockIndex,
+ MediaCacheStream* aStream,
+ int32_t aStreamBlockIndex) {
+ Block* block = &mIndex[aBlockIndex];
+ if (block->mOwners.IsEmpty()) {
+ mFreeBlocks.RemoveBlock(aBlockIndex);
+ }
+ BlockOwner* bo = block->mOwners.AppendElement();
+ bo->mStream = aStream;
+ bo->mStreamBlock = aStreamBlockIndex;
+ aStream->mBlocks[aStreamBlockIndex] = aBlockIndex;
+ bo->mClass = READAHEAD_BLOCK;
+ InsertReadaheadBlock(aLock, bo, aBlockIndex);
+}
+
+void MediaCache::FreeBlock(AutoLock& aLock, int32_t aBlock) {
+ Block* block = &mIndex[aBlock];
+ if (block->mOwners.IsEmpty()) {
+ // already free
+ return;
+ }
+
+ LOG("Released block %d", aBlock);
+
+ for (uint32_t i = 0; i < block->mOwners.Length(); ++i) {
+ BlockOwner* bo = &block->mOwners[i];
+ GetListForBlock(aLock, bo)->RemoveBlock(aBlock);
+ bo->mStream->mBlocks[bo->mStreamBlock] = -1;
+ }
+ block->mOwners.Clear();
+ mFreeBlocks.AddFirstBlock(aBlock);
+ Verify(aLock);
+}
+
+TimeDuration MediaCache::PredictNextUse(AutoLock&, TimeStamp aNow,
+ int32_t aBlock) {
+ MOZ_ASSERT(sThread->IsOnCurrentThread());
+ NS_ASSERTION(!IsBlockFree(aBlock), "aBlock is free");
+
+ Block* block = &mIndex[aBlock];
+ // Blocks can be belong to multiple streams. The predicted next use
+ // time is the earliest time predicted by any of the streams.
+ TimeDuration result;
+ for (uint32_t i = 0; i < block->mOwners.Length(); ++i) {
+ BlockOwner* bo = &block->mOwners[i];
+ TimeDuration prediction;
+ switch (bo->mClass) {
+ case METADATA_BLOCK:
+ // This block should be managed in LRU mode. For metadata we predict
+ // that the time until the next use is the time since the last use.
+ prediction = aNow - bo->mLastUseTime;
+ break;
+ case PLAYED_BLOCK: {
+ // This block should be managed in LRU mode, and we should impose
+ // a "replay delay" to reflect the likelihood of replay happening
+ NS_ASSERTION(static_cast<int64_t>(bo->mStreamBlock) * BLOCK_SIZE <
+ bo->mStream->mStreamOffset,
+ "Played block after the current stream position?");
+ int64_t bytesBehind =
+ bo->mStream->mStreamOffset -
+ static_cast<int64_t>(bo->mStreamBlock) * BLOCK_SIZE;
+ int64_t millisecondsBehind =
+ bytesBehind * 1000 / bo->mStream->mPlaybackBytesPerSecond;
+ prediction = TimeDuration::FromMilliseconds(std::min<int64_t>(
+ millisecondsBehind * REPLAY_PENALTY_FACTOR, INT32_MAX));
+ break;
+ }
+ case READAHEAD_BLOCK: {
+ int64_t bytesAhead =
+ static_cast<int64_t>(bo->mStreamBlock) * BLOCK_SIZE -
+ bo->mStream->mStreamOffset;
+ NS_ASSERTION(bytesAhead >= 0,
+ "Readahead block before the current stream position?");
+ int64_t millisecondsAhead =
+ bytesAhead * 1000 / bo->mStream->mPlaybackBytesPerSecond;
+ prediction = TimeDuration::FromMilliseconds(
+ std::min<int64_t>(millisecondsAhead, INT32_MAX));
+ break;
+ }
+ default:
+ NS_ERROR("Invalid class for predicting next use");
+ return TimeDuration(0);
+ }
+ if (i == 0 || prediction < result) {
+ result = prediction;
+ }
+ }
+ return result;
+}
+
+TimeDuration MediaCache::PredictNextUseForIncomingData(
+ AutoLock&, MediaCacheStream* aStream) {
+ MOZ_ASSERT(sThread->IsOnCurrentThread());
+
+ int64_t bytesAhead = aStream->mChannelOffset - aStream->mStreamOffset;
+ if (bytesAhead <= -BLOCK_SIZE) {
+ // Hmm, no idea when data behind us will be used. Guess 24 hours.
+ return TimeDuration::FromSeconds(24 * 60 * 60);
+ }
+ if (bytesAhead <= 0) return TimeDuration(0);
+ int64_t millisecondsAhead =
+ bytesAhead * 1000 / aStream->mPlaybackBytesPerSecond;
+ return TimeDuration::FromMilliseconds(
+ std::min<int64_t>(millisecondsAhead, INT32_MAX));
+}
+
+void MediaCache::Update() {
+ MOZ_ASSERT(sThread->IsOnCurrentThread());
+
+ AutoLock lock(mMonitor);
+
+ mUpdateQueued = false;
+#ifdef DEBUG
+ mInUpdate = true;
+#endif
+ const TimeStamp now = TimeStamp::Now();
+ const int32_t freeBlockCount = TrimCacheIfNeeded(lock, now);
+
+ // The action to use for each stream. We store these so we can make
+ // decisions while holding the cache lock but implement those decisions
+ // without holding the cache lock, since we need to call out to
+ // stream, decoder and element code.
+ AutoTArray<StreamAction, 10> actions;
+ DetermineActionsForStreams(lock, now, actions, freeBlockCount);
+
+#ifdef DEBUG
+ mInUpdate = false;
+#endif
+
+ // First, update the mCacheSuspended/mCacheEnded flags so that they're all
+ // correct when we fire our CacheClient commands below. Those commands can
+ // rely on these flags being set correctly for all streams.
+ for (uint32_t i = 0; i < mStreams.Length(); ++i) {
+ MediaCacheStream* stream = mStreams[i];
+ switch (actions[i].mTag) {
+ case StreamAction::SEEK:
+ stream->mCacheSuspended = false;
+ stream->mChannelEnded = false;
+ break;
+ case StreamAction::RESUME:
+ stream->mCacheSuspended = false;
+ break;
+ case StreamAction::SUSPEND:
+ stream->mCacheSuspended = true;
+ break;
+ default:
+ break;
+ }
+ }
+
+ for (uint32_t i = 0; i < mStreams.Length(); ++i) {
+ MediaCacheStream* stream = mStreams[i];
+ switch (actions[i].mTag) {
+ case StreamAction::SEEK:
+ LOG("Stream %p CacheSeek to %" PRId64 " (resume=%d)", stream,
+ actions[i].mSeekTarget, actions[i].mResume);
+ stream->mClient->CacheClientSeek(actions[i].mSeekTarget,
+ actions[i].mResume);
+ break;
+ case StreamAction::RESUME:
+ LOG("Stream %p Resumed", stream);
+ stream->mClient->CacheClientResume();
+ QueueSuspendedStatusUpdate(lock, stream->mResourceID);
+ break;
+ case StreamAction::SUSPEND:
+ LOG("Stream %p Suspended", stream);
+ stream->mClient->CacheClientSuspend();
+ QueueSuspendedStatusUpdate(lock, stream->mResourceID);
+ break;
+ default:
+ break;
+ }
+ }
+
+ // Notify streams about the suspended status changes.
+ for (uint32_t i = 0; i < mSuspendedStatusToNotify.Length(); ++i) {
+ MediaCache::ResourceStreamIterator iter(this, mSuspendedStatusToNotify[i]);
+ while (MediaCacheStream* stream = iter.Next(lock)) {
+ stream->mClient->CacheClientNotifySuspendedStatusChanged(
+ stream->AreAllStreamsForResourceSuspended(lock));
+ }
+ }
+ mSuspendedStatusToNotify.Clear();
+}
+
+int32_t MediaCache::TrimCacheIfNeeded(AutoLock& aLock, const TimeStamp& aNow) {
+ MOZ_ASSERT(sThread->IsOnCurrentThread());
+
+ const int32_t maxBlocks = mBlockCache->GetMaxBlocks(MediaCache::CacheSize());
+
+ int32_t freeBlockCount = mFreeBlocks.GetCount();
+ TimeDuration latestPredictedUseForOverflow = 0;
+ if (mIndex.Length() > uint32_t(maxBlocks)) {
+ // Try to trim back the cache to its desired maximum size. The cache may
+ // have overflowed simply due to data being received when we have
+ // no blocks in the main part of the cache that are free or lower
+ // priority than the new data. The cache can also be overflowing because
+ // the media.cache_size preference was reduced.
+ // First, figure out what the least valuable block in the cache overflow
+ // is. We don't want to replace any blocks in the main part of the
+ // cache whose expected time of next use is earlier or equal to that.
+ // If we allow that, we can effectively end up discarding overflowing
+ // blocks (by moving an overflowing block to the main part of the cache,
+ // and then overwriting it with another overflowing block), and we try
+ // to avoid that since it requires HTTP seeks.
+ // We also use this loop to eliminate overflowing blocks from
+ // freeBlockCount.
+ for (int32_t blockIndex = mIndex.Length() - 1; blockIndex >= maxBlocks;
+ --blockIndex) {
+ if (IsBlockFree(blockIndex)) {
+ // Don't count overflowing free blocks in our free block count
+ --freeBlockCount;
+ continue;
+ }
+ TimeDuration predictedUse = PredictNextUse(aLock, aNow, blockIndex);
+ latestPredictedUseForOverflow =
+ std::max(latestPredictedUseForOverflow, predictedUse);
+ }
+ } else {
+ freeBlockCount += maxBlocks - mIndex.Length();
+ }
+
+ // Now try to move overflowing blocks to the main part of the cache.
+ for (int32_t blockIndex = mIndex.Length() - 1; blockIndex >= maxBlocks;
+ --blockIndex) {
+ if (IsBlockFree(blockIndex)) continue;
+
+ Block* block = &mIndex[blockIndex];
+ // Try to relocate the block close to other blocks for the first stream.
+ // There is no point in trying to make it close to other blocks in
+ // *all* the streams it might belong to.
+ int32_t destinationBlockIndex =
+ FindReusableBlock(aLock, aNow, block->mOwners[0].mStream,
+ block->mOwners[0].mStreamBlock, maxBlocks);
+ if (destinationBlockIndex < 0) {
+ // Nowhere to place this overflow block. We won't be able to
+ // place any more overflow blocks.
+ break;
+ }
+
+ // Don't evict |destinationBlockIndex| if it is within [cur, end) otherwise
+ // a new channel will be opened to download this block again which is bad.
+ bool inCurrentCachedRange = false;
+ for (BlockOwner& owner : mIndex[destinationBlockIndex].mOwners) {
+ MediaCacheStream* stream = owner.mStream;
+ int64_t end = OffsetToBlockIndexUnchecked(
+ stream->GetCachedDataEndInternal(aLock, stream->mStreamOffset));
+ int64_t cur = OffsetToBlockIndexUnchecked(stream->mStreamOffset);
+ if (cur <= owner.mStreamBlock && owner.mStreamBlock < end) {
+ inCurrentCachedRange = true;
+ break;
+ }
+ }
+ if (inCurrentCachedRange) {
+ continue;
+ }
+
+ if (IsBlockFree(destinationBlockIndex) ||
+ PredictNextUse(aLock, aNow, destinationBlockIndex) >
+ latestPredictedUseForOverflow) {
+ // Reuse blocks in the main part of the cache that are less useful than
+ // the least useful overflow blocks
+
+ nsresult rv = mBlockCache->MoveBlock(blockIndex, destinationBlockIndex);
+
+ if (NS_SUCCEEDED(rv)) {
+ // We successfully copied the file data.
+ LOG("Swapping blocks %d and %d (trimming cache)", blockIndex,
+ destinationBlockIndex);
+ // Swapping the block metadata here lets us maintain the
+ // correct positions in the linked lists
+ SwapBlocks(aLock, blockIndex, destinationBlockIndex);
+ // Free the overflowing block even if the copy failed.
+ LOG("Released block %d (trimming cache)", blockIndex);
+ FreeBlock(aLock, blockIndex);
+ }
+ } else {
+ LOG("Could not trim cache block %d (destination %d, "
+ "predicted next use %f, latest predicted use for overflow %f",
+ blockIndex, destinationBlockIndex,
+ PredictNextUse(aLock, aNow, destinationBlockIndex).ToSeconds(),
+ latestPredictedUseForOverflow.ToSeconds());
+ }
+ }
+ // Try chopping back the array of cache entries and the cache file.
+ Truncate();
+ return freeBlockCount;
+}
+
+void MediaCache::DetermineActionsForStreams(AutoLock& aLock,
+ const TimeStamp& aNow,
+ nsTArray<StreamAction>& aActions,
+ int32_t aFreeBlockCount) {
+ MOZ_ASSERT(sThread->IsOnCurrentThread());
+
+ // Count the blocks allocated for readahead of non-seekable streams
+ // (these blocks can't be freed but we don't want them to monopolize the
+ // cache)
+ int32_t nonSeekableReadaheadBlockCount = 0;
+ for (uint32_t i = 0; i < mStreams.Length(); ++i) {
+ MediaCacheStream* stream = mStreams[i];
+ if (!stream->mIsTransportSeekable) {
+ nonSeekableReadaheadBlockCount += stream->mReadaheadBlocks.GetCount();
+ }
+ }
+
+ // If freeBlockCount is zero, then compute the latest of
+ // the predicted next-uses for all blocks
+ TimeDuration latestNextUse;
+ const int32_t maxBlocks = mBlockCache->GetMaxBlocks(MediaCache::CacheSize());
+ if (aFreeBlockCount == 0) {
+ const int32_t reusableBlock =
+ FindReusableBlock(aLock, aNow, nullptr, 0, maxBlocks);
+ if (reusableBlock >= 0) {
+ latestNextUse = PredictNextUse(aLock, aNow, reusableBlock);
+ }
+ }
+
+ for (uint32_t i = 0; i < mStreams.Length(); ++i) {
+ aActions.AppendElement(StreamAction{});
+
+ MediaCacheStream* stream = mStreams[i];
+ if (stream->mClosed) {
+ LOG("Stream %p closed", stream);
+ continue;
+ }
+
+ // We make decisions based on mSeekTarget when there is a pending seek.
+ // Otherwise we will keep issuing seek requests until mChannelOffset
+ // is changed by NotifyDataStarted() which is bad.
+ const int64_t channelOffset = stream->mSeekTarget != -1
+ ? stream->mSeekTarget
+ : stream->mChannelOffset;
+
+ // Figure out where we should be reading from. It's the first
+ // uncached byte after the current mStreamOffset.
+ const int64_t dataOffset =
+ stream->GetCachedDataEndInternal(aLock, stream->mStreamOffset);
+ MOZ_ASSERT(dataOffset >= 0);
+
+ // Compute where we'd actually seek to to read at readOffset
+ int64_t desiredOffset = dataOffset;
+ if (stream->mIsTransportSeekable) {
+ if (desiredOffset > channelOffset &&
+ desiredOffset <= channelOffset + SEEK_VS_READ_THRESHOLD) {
+ // Assume it's more efficient to just keep reading up to the
+ // desired position instead of trying to seek
+ desiredOffset = channelOffset;
+ }
+ } else {
+ // We can't seek directly to the desired offset...
+ if (channelOffset > desiredOffset) {
+ // Reading forward won't get us anywhere, we need to go backwards.
+ // Seek back to 0 (the client will reopen the stream) and then
+ // read forward.
+ NS_WARNING("Can't seek backwards, so seeking to 0");
+ desiredOffset = 0;
+ // Flush cached blocks out, since if this is a live stream
+ // the cached data may be completely different next time we
+ // read it. We have to assume that live streams don't
+ // advertise themselves as being seekable...
+ ReleaseStreamBlocks(aLock, stream);
+ } else {
+ // otherwise reading forward is looking good, so just stay where we
+ // are and don't trigger a channel seek!
+ desiredOffset = channelOffset;
+ }
+ }
+
+ // Figure out if we should be reading data now or not. It's amazing
+ // how complex this is, but each decision is simple enough.
+ bool enableReading;
+ if (stream->mStreamLength >= 0 && dataOffset >= stream->mStreamLength) {
+ // We want data at the end of the stream, where there's nothing to
+ // read. We don't want to try to read if we're suspended, because that
+ // might create a new channel and seek unnecessarily (and incorrectly,
+ // since HTTP doesn't allow seeking to the actual EOF), and we don't want
+ // to suspend if we're not suspended and already reading at the end of
+ // the stream, since there just might be more data than the server
+ // advertised with Content-Length, and we may as well keep reading.
+ // But we don't want to seek to the end of the stream if we're not
+ // already there.
+ LOG("Stream %p at end of stream", stream);
+ enableReading =
+ !stream->mCacheSuspended && stream->mStreamLength == channelOffset;
+ } else if (desiredOffset < stream->mStreamOffset) {
+ // We're reading to try to catch up to where the current stream
+ // reader wants to be. Better not stop.
+ LOG("Stream %p catching up", stream);
+ enableReading = true;
+ } else if (desiredOffset < stream->mStreamOffset + BLOCK_SIZE) {
+ // The stream reader is waiting for us, or nearly so. Better feed it.
+ LOG("Stream %p feeding reader", stream);
+ enableReading = true;
+ } else if (!stream->mIsTransportSeekable &&
+ nonSeekableReadaheadBlockCount >=
+ maxBlocks * NONSEEKABLE_READAHEAD_MAX) {
+ // This stream is not seekable and there are already too many blocks
+ // being cached for readahead for nonseekable streams (which we can't
+ // free). So stop reading ahead now.
+ LOG("Stream %p throttling non-seekable readahead", stream);
+ enableReading = false;
+ } else if (mIndex.Length() > uint32_t(maxBlocks)) {
+ // We're in the process of bringing the cache size back to the
+ // desired limit, so don't bring in more data yet
+ LOG("Stream %p throttling to reduce cache size", stream);
+ enableReading = false;
+ } else {
+ TimeDuration predictedNewDataUse =
+ PredictNextUseForIncomingData(aLock, stream);
+
+ if (stream->mThrottleReadahead && stream->mCacheSuspended &&
+ predictedNewDataUse.ToSeconds() > MediaCache::ResumeThreshold()) {
+ // Don't need data for a while, so don't bother waking up the stream
+ LOG("Stream %p avoiding wakeup since more data is not needed", stream);
+ enableReading = false;
+ } else if (stream->mThrottleReadahead &&
+ predictedNewDataUse.ToSeconds() >
+ MediaCache::ReadaheadLimit()) {
+ // Don't read ahead more than this much
+ LOG("Stream %p throttling to avoid reading ahead too far", stream);
+ enableReading = false;
+ } else if (aFreeBlockCount > 0) {
+ // Free blocks in the cache, so keep reading
+ LOG("Stream %p reading since there are free blocks", stream);
+ enableReading = true;
+ } else if (latestNextUse <= TimeDuration(0)) {
+ // No reusable blocks, so can't read anything
+ LOG("Stream %p throttling due to no reusable blocks", stream);
+ enableReading = false;
+ } else {
+ // Read ahead if the data we expect to read is more valuable than
+ // the least valuable block in the main part of the cache
+ LOG("Stream %p predict next data in %f, current worst block is %f",
+ stream, predictedNewDataUse.ToSeconds(), latestNextUse.ToSeconds());
+ enableReading = predictedNewDataUse < latestNextUse;
+ }
+ }
+
+ if (enableReading) {
+ for (uint32_t j = 0; j < i; ++j) {
+ MediaCacheStream* other = mStreams[j];
+ if (other->mResourceID == stream->mResourceID && !other->mClosed &&
+ !other->mClientSuspended && !other->mChannelEnded &&
+ OffsetToBlockIndexUnchecked(other->mSeekTarget != -1
+ ? other->mSeekTarget
+ : other->mChannelOffset) ==
+ OffsetToBlockIndexUnchecked(desiredOffset)) {
+ // This block is already going to be read by the other stream.
+ // So don't try to read it from this stream as well.
+ enableReading = false;
+ LOG("Stream %p waiting on same block (%" PRId32 ") from stream %p",
+ stream, OffsetToBlockIndexUnchecked(desiredOffset), other);
+ break;
+ }
+ }
+ }
+
+ if (channelOffset != desiredOffset && enableReading) {
+ // We need to seek now.
+ NS_ASSERTION(stream->mIsTransportSeekable || desiredOffset == 0,
+ "Trying to seek in a non-seekable stream!");
+ // Round seek offset down to the start of the block. This is essential
+ // because we don't want to think we have part of a block already
+ // in mPartialBlockBuffer.
+ stream->mSeekTarget =
+ OffsetToBlockIndexUnchecked(desiredOffset) * BLOCK_SIZE;
+ aActions[i].mTag = StreamAction::SEEK;
+ aActions[i].mResume = stream->mCacheSuspended;
+ aActions[i].mSeekTarget = stream->mSeekTarget;
+ } else if (enableReading && stream->mCacheSuspended) {
+ aActions[i].mTag = StreamAction::RESUME;
+ } else if (!enableReading && !stream->mCacheSuspended) {
+ aActions[i].mTag = StreamAction::SUSPEND;
+ }
+ LOG("Stream %p, mCacheSuspended=%d, enableReading=%d, action=%s", stream,
+ stream->mCacheSuspended, enableReading,
+ aActions[i].mTag == StreamAction::SEEK ? "SEEK"
+ : aActions[i].mTag == StreamAction::RESUME ? "RESUME"
+ : aActions[i].mTag == StreamAction::SUSPEND ? "SUSPEND"
+ : "NONE");
+ }
+}
+
+void MediaCache::QueueUpdate(AutoLock&) {
+ // Queuing an update while we're in an update raises a high risk of
+ // triggering endless events
+ NS_ASSERTION(!mInUpdate, "Queuing an update while we're in an update");
+ if (mUpdateQueued) {
+ return;
+ }
+ mUpdateQueued = true;
+ sThread->Dispatch(NS_NewRunnableFunction(
+ "MediaCache::QueueUpdate", [self = RefPtr<MediaCache>(this)]() mutable {
+ self->Update();
+ // Ensure MediaCache is deleted on the main thread.
+ NS_ReleaseOnMainThread("UpdateEvent::mMediaCache", self.forget());
+ }));
+}
+
+void MediaCache::QueueSuspendedStatusUpdate(AutoLock&, int64_t aResourceID) {
+ if (!mSuspendedStatusToNotify.Contains(aResourceID)) {
+ mSuspendedStatusToNotify.AppendElement(aResourceID);
+ }
+}
+
+#ifdef DEBUG_VERIFY_CACHE
+void MediaCache::Verify(AutoLock&) {
+ mFreeBlocks.Verify();
+ for (uint32_t i = 0; i < mStreams.Length(); ++i) {
+ MediaCacheStream* stream = mStreams[i];
+ stream->mReadaheadBlocks.Verify();
+ stream->mPlayedBlocks.Verify();
+ stream->mMetadataBlocks.Verify();
+
+ // Verify that the readahead blocks are listed in stream block order
+ int32_t block = stream->mReadaheadBlocks.GetFirstBlock();
+ int32_t lastStreamBlock = -1;
+ while (block >= 0) {
+ uint32_t j = 0;
+ while (mIndex[block].mOwners[j].mStream != stream) {
+ ++j;
+ }
+ int32_t nextStreamBlock = int32_t(mIndex[block].mOwners[j].mStreamBlock);
+ NS_ASSERTION(lastStreamBlock < nextStreamBlock,
+ "Blocks not increasing in readahead stream");
+ lastStreamBlock = nextStreamBlock;
+ block = stream->mReadaheadBlocks.GetNextBlock(block);
+ }
+ }
+}
+#endif
+
+void MediaCache::InsertReadaheadBlock(AutoLock& aLock, BlockOwner* aBlockOwner,
+ int32_t aBlockIndex) {
+ // Find the last block whose stream block is before aBlockIndex's
+ // stream block, and insert after it
+ MediaCacheStream* stream = aBlockOwner->mStream;
+ int32_t readaheadIndex = stream->mReadaheadBlocks.GetLastBlock();
+ while (readaheadIndex >= 0) {
+ BlockOwner* bo = GetBlockOwner(aLock, readaheadIndex, stream);
+ NS_ASSERTION(bo, "stream must own its blocks");
+ if (bo->mStreamBlock < aBlockOwner->mStreamBlock) {
+ stream->mReadaheadBlocks.AddAfter(aBlockIndex, readaheadIndex);
+ return;
+ }
+ NS_ASSERTION(bo->mStreamBlock > aBlockOwner->mStreamBlock,
+ "Duplicated blocks??");
+ readaheadIndex = stream->mReadaheadBlocks.GetPrevBlock(readaheadIndex);
+ }
+
+ stream->mReadaheadBlocks.AddFirstBlock(aBlockIndex);
+ Verify(aLock);
+}
+
+void MediaCache::AllocateAndWriteBlock(AutoLock& aLock,
+ MediaCacheStream* aStream,
+ int32_t aStreamBlockIndex,
+ Span<const uint8_t> aData1,
+ Span<const uint8_t> aData2) {
+ MOZ_ASSERT(sThread->IsOnCurrentThread());
+
+ // Remove all cached copies of this block
+ ResourceStreamIterator iter(this, aStream->mResourceID);
+ while (MediaCacheStream* stream = iter.Next(aLock)) {
+ while (aStreamBlockIndex >= int32_t(stream->mBlocks.Length())) {
+ stream->mBlocks.AppendElement(-1);
+ }
+ if (stream->mBlocks[aStreamBlockIndex] >= 0) {
+ // We no longer want to own this block
+ int32_t globalBlockIndex = stream->mBlocks[aStreamBlockIndex];
+ LOG("Released block %d from stream %p block %d(%" PRId64 ")",
+ globalBlockIndex, stream, aStreamBlockIndex,
+ aStreamBlockIndex * BLOCK_SIZE);
+ RemoveBlockOwner(aLock, globalBlockIndex, stream);
+ }
+ }
+
+ // Extend the mBlocks array as necessary
+
+ TimeStamp now = TimeStamp::Now();
+ int32_t blockIndex =
+ FindBlockForIncomingData(aLock, now, aStream, aStreamBlockIndex);
+ if (blockIndex >= 0) {
+ FreeBlock(aLock, blockIndex);
+
+ Block* block = &mIndex[blockIndex];
+ LOG("Allocated block %d to stream %p block %d(%" PRId64 ")", blockIndex,
+ aStream, aStreamBlockIndex, aStreamBlockIndex * BLOCK_SIZE);
+
+ ResourceStreamIterator iter(this, aStream->mResourceID);
+ while (MediaCacheStream* stream = iter.Next(aLock)) {
+ BlockOwner* bo = block->mOwners.AppendElement();
+ if (!bo) {
+ // Roll back mOwners if any allocation fails.
+ block->mOwners.Clear();
+ return;
+ }
+ bo->mStream = stream;
+ }
+
+ if (block->mOwners.IsEmpty()) {
+ // This happens when all streams with the resource id are closed. We can
+ // just return here now and discard the data.
+ return;
+ }
+
+ // Tell each stream using this resource about the new block.
+ for (auto& bo : block->mOwners) {
+ bo.mStreamBlock = aStreamBlockIndex;
+ bo.mLastUseTime = now;
+ bo.mStream->mBlocks[aStreamBlockIndex] = blockIndex;
+ if (aStreamBlockIndex * BLOCK_SIZE < bo.mStream->mStreamOffset) {
+ bo.mClass = PLAYED_BLOCK;
+ // This must be the most-recently-used block, since we
+ // marked it as used now (which may be slightly bogus, but we'll
+ // treat it as used for simplicity).
+ GetListForBlock(aLock, &bo)->AddFirstBlock(blockIndex);
+ Verify(aLock);
+ } else {
+ // This may not be the latest readahead block, although it usually
+ // will be. We may have to scan for the right place to insert
+ // the block in the list.
+ bo.mClass = READAHEAD_BLOCK;
+ InsertReadaheadBlock(aLock, &bo, blockIndex);
+ }
+ }
+
+ // Invariant: block->mOwners.IsEmpty() iff we can find an entry
+ // in mFreeBlocks for a given blockIndex.
+ MOZ_DIAGNOSTIC_ASSERT(!block->mOwners.IsEmpty());
+ mFreeBlocks.RemoveBlock(blockIndex);
+
+ nsresult rv = mBlockCache->WriteBlock(blockIndex, aData1, aData2);
+ if (NS_FAILED(rv)) {
+ LOG("Released block %d from stream %p block %d(%" PRId64 ")", blockIndex,
+ aStream, aStreamBlockIndex, aStreamBlockIndex * BLOCK_SIZE);
+ FreeBlock(aLock, blockIndex);
+ }
+ }
+
+ // Queue an Update since the cache state has changed (for example
+ // we might want to stop loading because the cache is full)
+ QueueUpdate(aLock);
+}
+
+void MediaCache::OpenStream(AutoLock& aLock, MediaCacheStream* aStream,
+ bool aIsClone) {
+ LOG("Stream %p opened, aIsClone=%d, mCacheSuspended=%d, "
+ "mDidNotifyDataEnded=%d",
+ aStream, aIsClone, aStream->mCacheSuspended,
+ aStream->mDidNotifyDataEnded);
+ mStreams.AppendElement(aStream);
+
+ // A cloned stream should've got the ID from its original.
+ if (!aIsClone) {
+ MOZ_ASSERT(aStream->mResourceID == 0, "mResourceID has been initialized.");
+ aStream->mResourceID = AllocateResourceID(aLock);
+ }
+
+ // We should have a valid ID now no matter it is cloned or not.
+ MOZ_ASSERT(aStream->mResourceID > 0, "mResourceID is invalid");
+
+ // Queue an update since a new stream has been opened.
+ QueueUpdate(aLock);
+}
+
+void MediaCache::ReleaseStream(AutoLock&, MediaCacheStream* aStream) {
+ MOZ_ASSERT(OwnerThread()->IsOnCurrentThread());
+ LOG("Stream %p closed", aStream);
+ mStreams.RemoveElement(aStream);
+ // The caller needs to call QueueUpdate() to re-run Update().
+}
+
+void MediaCache::ReleaseStreamBlocks(AutoLock& aLock,
+ MediaCacheStream* aStream) {
+ // XXX scanning the entire stream doesn't seem great, if not much of it
+ // is cached, but the only easy alternative is to scan the entire cache
+ // which isn't better
+ uint32_t length = aStream->mBlocks.Length();
+ for (uint32_t i = 0; i < length; ++i) {
+ int32_t blockIndex = aStream->mBlocks[i];
+ if (blockIndex >= 0) {
+ LOG("Released block %d from stream %p block %d(%" PRId64 ")", blockIndex,
+ aStream, i, i * BLOCK_SIZE);
+ RemoveBlockOwner(aLock, blockIndex, aStream);
+ }
+ }
+}
+
+void MediaCache::Truncate() {
+ uint32_t end;
+ for (end = mIndex.Length(); end > 0; --end) {
+ if (!IsBlockFree(end - 1)) break;
+ mFreeBlocks.RemoveBlock(end - 1);
+ }
+
+ if (end < mIndex.Length()) {
+ mIndex.TruncateLength(end);
+ // XXX We could truncate the cache file here, but we don't seem
+ // to have a cross-platform API for doing that. At least when all
+ // streams are closed we shut down the cache, which erases the
+ // file at that point.
+ }
+}
+
+void MediaCache::NoteBlockUsage(AutoLock& aLock, MediaCacheStream* aStream,
+ int32_t aBlockIndex, int64_t aStreamOffset,
+ MediaCacheStream::ReadMode aMode,
+ TimeStamp aNow) {
+ if (aBlockIndex < 0) {
+ // this block is not in the cache yet
+ return;
+ }
+
+ BlockOwner* bo = GetBlockOwner(aLock, aBlockIndex, aStream);
+ if (!bo) {
+ // this block is not in the cache yet
+ return;
+ }
+
+ // The following check has to be <= because the stream offset has
+ // not yet been updated for the data read from this block
+ NS_ASSERTION(bo->mStreamBlock * BLOCK_SIZE <= aStreamOffset,
+ "Using a block that's behind the read position?");
+
+ GetListForBlock(aLock, bo)->RemoveBlock(aBlockIndex);
+ bo->mClass =
+ (aMode == MediaCacheStream::MODE_METADATA || bo->mClass == METADATA_BLOCK)
+ ? METADATA_BLOCK
+ : PLAYED_BLOCK;
+ // Since this is just being used now, it can definitely be at the front
+ // of mMetadataBlocks or mPlayedBlocks
+ GetListForBlock(aLock, bo)->AddFirstBlock(aBlockIndex);
+ bo->mLastUseTime = aNow;
+ Verify(aLock);
+}
+
+void MediaCache::NoteSeek(AutoLock& aLock, MediaCacheStream* aStream,
+ int64_t aOldOffset) {
+ if (aOldOffset < aStream->mStreamOffset) {
+ // We seeked forward. Convert blocks from readahead to played.
+ // Any readahead block that intersects the seeked-over range must
+ // be converted.
+ int32_t blockIndex = OffsetToBlockIndex(aOldOffset);
+ if (blockIndex < 0) {
+ return;
+ }
+ int32_t endIndex =
+ std::min(OffsetToBlockIndex(aStream->mStreamOffset + (BLOCK_SIZE - 1)),
+ int32_t(aStream->mBlocks.Length()));
+ if (endIndex < 0) {
+ return;
+ }
+ TimeStamp now = TimeStamp::Now();
+ while (blockIndex < endIndex) {
+ int32_t cacheBlockIndex = aStream->mBlocks[blockIndex];
+ if (cacheBlockIndex >= 0) {
+ // Marking the block used may not be exactly what we want but
+ // it's simple
+ NoteBlockUsage(aLock, aStream, cacheBlockIndex, aStream->mStreamOffset,
+ MediaCacheStream::MODE_PLAYBACK, now);
+ }
+ ++blockIndex;
+ }
+ } else {
+ // We seeked backward. Convert from played to readahead.
+ // Any played block that is entirely after the start of the seeked-over
+ // range must be converted.
+ int32_t blockIndex =
+ OffsetToBlockIndex(aStream->mStreamOffset + (BLOCK_SIZE - 1));
+ if (blockIndex < 0) {
+ return;
+ }
+ int32_t endIndex =
+ std::min(OffsetToBlockIndex(aOldOffset + (BLOCK_SIZE - 1)),
+ int32_t(aStream->mBlocks.Length()));
+ if (endIndex < 0) {
+ return;
+ }
+ while (blockIndex < endIndex) {
+ MOZ_ASSERT(endIndex > 0);
+ int32_t cacheBlockIndex = aStream->mBlocks[endIndex - 1];
+ if (cacheBlockIndex >= 0) {
+ BlockOwner* bo = GetBlockOwner(aLock, cacheBlockIndex, aStream);
+ NS_ASSERTION(bo, "Stream doesn't own its blocks?");
+ if (bo->mClass == PLAYED_BLOCK) {
+ aStream->mPlayedBlocks.RemoveBlock(cacheBlockIndex);
+ bo->mClass = READAHEAD_BLOCK;
+ // Adding this as the first block is sure to be OK since
+ // this must currently be the earliest readahead block
+ // (that's why we're proceeding backwards from the end of
+ // the seeked range to the start)
+ aStream->mReadaheadBlocks.AddFirstBlock(cacheBlockIndex);
+ Verify(aLock);
+ }
+ }
+ --endIndex;
+ }
+ }
+}
+
+void MediaCacheStream::NotifyLoadID(uint32_t aLoadID) {
+ MOZ_ASSERT(aLoadID > 0);
+
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ "MediaCacheStream::NotifyLoadID",
+ [client = RefPtr<ChannelMediaResource>(mClient), this, aLoadID]() {
+ AutoLock lock(mMediaCache->Monitor());
+ mLoadID = aLoadID;
+ });
+ OwnerThread()->Dispatch(r.forget());
+}
+
+void MediaCacheStream::NotifyDataStartedInternal(uint32_t aLoadID,
+ int64_t aOffset,
+ bool aSeekable,
+ int64_t aLength) {
+ MOZ_ASSERT(OwnerThread()->IsOnCurrentThread());
+ MOZ_ASSERT(aLoadID > 0);
+ LOG("Stream %p DataStarted: %" PRId64 " aLoadID=%u aLength=%" PRId64, this,
+ aOffset, aLoadID, aLength);
+
+ AutoLock lock(mMediaCache->Monitor());
+ NS_WARNING_ASSERTION(aOffset == mSeekTarget || aOffset == mChannelOffset,
+ "Server is giving us unexpected offset");
+ MOZ_ASSERT(aOffset >= 0);
+ if (aLength >= 0) {
+ mStreamLength = aLength;
+ }
+ mChannelOffset = aOffset;
+ if (mStreamLength >= 0) {
+ // If we started reading at a certain offset, then for sure
+ // the stream is at least that long.
+ mStreamLength = std::max(mStreamLength, mChannelOffset);
+ }
+ mLoadID = aLoadID;
+
+ MOZ_ASSERT(aOffset == 0 || aSeekable,
+ "channel offset must be zero when we become non-seekable");
+ mIsTransportSeekable = aSeekable;
+ // Queue an Update since we may change our strategy for dealing
+ // with this stream
+ mMediaCache->QueueUpdate(lock);
+
+ // Reset mSeekTarget since the seek is completed so MediaCache::Update() will
+ // make decisions based on mChannelOffset instead of mSeekTarget.
+ mSeekTarget = -1;
+
+ // Reset these flags since a new load has begun.
+ mChannelEnded = false;
+ mDidNotifyDataEnded = false;
+
+ UpdateDownloadStatistics(lock);
+}
+
+void MediaCacheStream::NotifyDataStarted(uint32_t aLoadID, int64_t aOffset,
+ bool aSeekable, int64_t aLength) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aLoadID > 0);
+
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ "MediaCacheStream::NotifyDataStarted",
+ [=, client = RefPtr<ChannelMediaResource>(mClient)]() {
+ NotifyDataStartedInternal(aLoadID, aOffset, aSeekable, aLength);
+ });
+ OwnerThread()->Dispatch(r.forget());
+}
+
+void MediaCacheStream::NotifyDataReceived(uint32_t aLoadID, uint32_t aCount,
+ const uint8_t* aData) {
+ MOZ_ASSERT(OwnerThread()->IsOnCurrentThread());
+ MOZ_ASSERT(aLoadID > 0);
+
+ AutoLock lock(mMediaCache->Monitor());
+ if (mClosed) {
+ // Nothing to do if the stream is closed.
+ return;
+ }
+
+ LOG("Stream %p DataReceived at %" PRId64 " count=%u aLoadID=%u", this,
+ mChannelOffset, aCount, aLoadID);
+
+ if (mLoadID != aLoadID) {
+ // mChannelOffset is updated to a new position when loading a new channel.
+ // We should discard the data coming from the old channel so it won't be
+ // stored to the wrong positoin.
+ return;
+ }
+
+ mDownloadStatistics.AddBytes(aCount);
+
+ // True if we commit any blocks to the cache.
+ bool cacheUpdated = false;
+
+ auto source = Span<const uint8_t>(aData, aCount);
+
+ // We process the data one block (or part of a block) at a time
+ while (!source.IsEmpty()) {
+ // The data we've collected so far in the partial block.
+ auto partial = Span<const uint8_t>(mPartialBlockBuffer.get(),
+ OffsetInBlock(mChannelOffset));
+
+ // The number of bytes needed to complete the partial block.
+ size_t remaining = BLOCK_SIZE - partial.Length();
+
+ if (source.Length() >= remaining) {
+ // We have a whole block now to write it out.
+ mMediaCache->AllocateAndWriteBlock(
+ lock, this, OffsetToBlockIndexUnchecked(mChannelOffset), partial,
+ source.First(remaining));
+ source = source.From(remaining);
+ mChannelOffset += remaining;
+ cacheUpdated = true;
+ } else {
+ // The buffer to be filled in the partial block.
+ auto buf = Span<uint8_t>(mPartialBlockBuffer.get() + partial.Length(),
+ remaining);
+ memcpy(buf.Elements(), source.Elements(), source.Length());
+ mChannelOffset += source.Length();
+ break;
+ }
+ }
+
+ MediaCache::ResourceStreamIterator iter(mMediaCache, mResourceID);
+ while (MediaCacheStream* stream = iter.Next(lock)) {
+ if (stream->mStreamLength >= 0) {
+ // The stream is at least as long as what we've read
+ stream->mStreamLength = std::max(stream->mStreamLength, mChannelOffset);
+ }
+ stream->mClient->CacheClientNotifyDataReceived();
+ }
+
+ // XXX it would be fairly easy to optimize things a lot more to
+ // avoid waking up reader threads unnecessarily
+ if (cacheUpdated) {
+ // Wake up the reader who is waiting for the committed blocks.
+ lock.NotifyAll();
+ }
+}
+
+void MediaCacheStream::FlushPartialBlockInternal(AutoLock& aLock,
+ bool aNotifyAll) {
+ MOZ_ASSERT(OwnerThread()->IsOnCurrentThread());
+
+ int32_t blockIndex = OffsetToBlockIndexUnchecked(mChannelOffset);
+ int32_t blockOffset = OffsetInBlock(mChannelOffset);
+ if (blockOffset > 0) {
+ LOG("Stream %p writing partial block: [%d] bytes; "
+ "mStreamOffset [%" PRId64 "] mChannelOffset[%" PRId64
+ "] mStreamLength [%" PRId64 "] notifying: [%s]",
+ this, blockOffset, mStreamOffset, mChannelOffset, mStreamLength,
+ aNotifyAll ? "yes" : "no");
+
+ // Write back the partial block
+ memset(mPartialBlockBuffer.get() + blockOffset, 0,
+ BLOCK_SIZE - blockOffset);
+ auto data = Span<const uint8_t>(mPartialBlockBuffer.get(), BLOCK_SIZE);
+ mMediaCache->AllocateAndWriteBlock(aLock, this, blockIndex, data);
+ }
+
+ // |mChannelOffset == 0| means download ends with no bytes received.
+ // We should also wake up those readers who are waiting for data
+ // that will never come.
+ if ((blockOffset > 0 || mChannelOffset == 0) && aNotifyAll) {
+ // Wake up readers who may be waiting for this data
+ aLock.NotifyAll();
+ }
+}
+
+void MediaCacheStream::UpdateDownloadStatistics(AutoLock&) {
+ if (mChannelEnded || mClientSuspended) {
+ mDownloadStatistics.Stop();
+ } else {
+ mDownloadStatistics.Start();
+ }
+}
+
+void MediaCacheStream::NotifyDataEndedInternal(uint32_t aLoadID,
+ nsresult aStatus) {
+ MOZ_ASSERT(OwnerThread()->IsOnCurrentThread());
+ AutoLock lock(mMediaCache->Monitor());
+
+ if (mClosed || aLoadID != mLoadID) {
+ // Nothing to do if the stream is closed or a new load has begun.
+ return;
+ }
+
+ // It is prudent to update channel/cache status before calling
+ // CacheClientNotifyDataEnded() which will read |mChannelEnded|.
+ mChannelEnded = true;
+ mMediaCache->QueueUpdate(lock);
+
+ UpdateDownloadStatistics(lock);
+
+ if (NS_FAILED(aStatus)) {
+ // Notify the client about this network error.
+ mDidNotifyDataEnded = true;
+ mNotifyDataEndedStatus = aStatus;
+ mClient->CacheClientNotifyDataEnded(aStatus);
+ // Wake up the readers so they can fail gracefully.
+ lock.NotifyAll();
+ return;
+ }
+
+ // Note we don't flush the partial block when download ends abnormally for
+ // the padding zeros will give wrong data to other streams.
+ FlushPartialBlockInternal(lock, true);
+
+ MediaCache::ResourceStreamIterator iter(mMediaCache, mResourceID);
+ while (MediaCacheStream* stream = iter.Next(lock)) {
+ // We read the whole stream, so remember the true length
+ stream->mStreamLength = mChannelOffset;
+ if (!stream->mDidNotifyDataEnded) {
+ stream->mDidNotifyDataEnded = true;
+ stream->mNotifyDataEndedStatus = aStatus;
+ stream->mClient->CacheClientNotifyDataEnded(aStatus);
+ }
+ }
+}
+
+void MediaCacheStream::NotifyDataEnded(uint32_t aLoadID, nsresult aStatus) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aLoadID > 0);
+
+ RefPtr<ChannelMediaResource> client = mClient;
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ "MediaCacheStream::NotifyDataEnded", [client, this, aLoadID, aStatus]() {
+ NotifyDataEndedInternal(aLoadID, aStatus);
+ });
+ OwnerThread()->Dispatch(r.forget());
+}
+
+void MediaCacheStream::NotifyClientSuspended(bool aSuspended) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<ChannelMediaResource> client = mClient;
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ "MediaCacheStream::NotifyClientSuspended", [client, this, aSuspended]() {
+ AutoLock lock(mMediaCache->Monitor());
+ if (!mClosed && mClientSuspended != aSuspended) {
+ mClientSuspended = aSuspended;
+ // mClientSuspended changes the decision of reading streams.
+ mMediaCache->QueueUpdate(lock);
+ UpdateDownloadStatistics(lock);
+ if (mClientSuspended) {
+ // Download is suspended. Wake up the readers that might be able to
+ // get data from the partial block.
+ lock.NotifyAll();
+ }
+ }
+ });
+ OwnerThread()->Dispatch(r.forget());
+}
+
+void MediaCacheStream::NotifyResume() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ "MediaCacheStream::NotifyResume",
+ [this, client = RefPtr<ChannelMediaResource>(mClient)]() {
+ AutoLock lock(mMediaCache->Monitor());
+ if (mClosed) {
+ return;
+ }
+ // Don't resume download if we are already at the end of the stream for
+ // seek will fail and be wasted anyway.
+ int64_t offset = mSeekTarget != -1 ? mSeekTarget : mChannelOffset;
+ if (mStreamLength < 0 || offset < mStreamLength) {
+ mClient->CacheClientSeek(offset, false);
+ // DownloadResumed() will be notified when a new channel is opened.
+ }
+ // The channel remains dead. If we want to read some other data in the
+ // future, CacheClientSeek() will be called to reopen the channel.
+ });
+ OwnerThread()->Dispatch(r.forget());
+}
+
+MediaCacheStream::~MediaCacheStream() {
+ MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");
+ MOZ_ASSERT(!mPinCount, "Unbalanced Pin");
+ MOZ_ASSERT(!mMediaCache || mClosed);
+
+ uint32_t lengthKb = uint32_t(std::min(
+ std::max(mStreamLength, int64_t(0)) / 1024, int64_t(UINT32_MAX)));
+ LOG("MediaCacheStream::~MediaCacheStream(this=%p) "
+ "MEDIACACHESTREAM_LENGTH_KB=%" PRIu32,
+ this, lengthKb);
+}
+
+bool MediaCacheStream::AreAllStreamsForResourceSuspended(AutoLock& aLock) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ MediaCache::ResourceStreamIterator iter(mMediaCache, mResourceID);
+ // Look for a stream that's able to read the data we need
+ int64_t dataOffset = -1;
+ while (MediaCacheStream* stream = iter.Next(aLock)) {
+ if (stream->mCacheSuspended || stream->mChannelEnded || stream->mClosed) {
+ continue;
+ }
+ if (dataOffset < 0) {
+ dataOffset = GetCachedDataEndInternal(aLock, mStreamOffset);
+ }
+ // Ignore streams that are reading beyond the data we need
+ if (stream->mChannelOffset > dataOffset) {
+ continue;
+ }
+ return false;
+ }
+
+ return true;
+}
+
+RefPtr<GenericPromise> MediaCacheStream::Close() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!mMediaCache) {
+ return GenericPromise::CreateAndResolve(true, __func__);
+ }
+
+ return InvokeAsync(OwnerThread(), "MediaCacheStream::Close",
+ [this, client = RefPtr<ChannelMediaResource>(mClient)] {
+ AutoLock lock(mMediaCache->Monitor());
+ CloseInternal(lock);
+ return GenericPromise::CreateAndResolve(true, __func__);
+ });
+}
+
+void MediaCacheStream::CloseInternal(AutoLock& aLock) {
+ MOZ_ASSERT(OwnerThread()->IsOnCurrentThread());
+
+ if (mClosed) {
+ return;
+ }
+
+ // Closing a stream will change the return value of
+ // MediaCacheStream::AreAllStreamsForResourceSuspended as well as
+ // ChannelMediaResource::IsSuspendedByCache. Let's notify it.
+ mMediaCache->QueueSuspendedStatusUpdate(aLock, mResourceID);
+
+ mClosed = true;
+ mMediaCache->ReleaseStreamBlocks(aLock, this);
+ mMediaCache->ReleaseStream(aLock, this);
+ // Wake up any blocked readers
+ aLock.NotifyAll();
+
+ // Queue an Update since we may have created more free space.
+ mMediaCache->QueueUpdate(aLock);
+}
+
+void MediaCacheStream::Pin() {
+ MOZ_ASSERT(!NS_IsMainThread());
+ AutoLock lock(mMediaCache->Monitor());
+ ++mPinCount;
+ // Queue an Update since we may no longer want to read more into the
+ // cache, if this stream's block have become non-evictable
+ mMediaCache->QueueUpdate(lock);
+}
+
+void MediaCacheStream::Unpin() {
+ MOZ_ASSERT(!NS_IsMainThread());
+ AutoLock lock(mMediaCache->Monitor());
+ NS_ASSERTION(mPinCount > 0, "Unbalanced Unpin");
+ --mPinCount;
+ // Queue an Update since we may be able to read more into the
+ // cache, if this stream's block have become evictable
+ mMediaCache->QueueUpdate(lock);
+}
+
+int64_t MediaCacheStream::GetLength() const {
+ MOZ_ASSERT(!NS_IsMainThread());
+ AutoLock lock(mMediaCache->Monitor());
+ return mStreamLength;
+}
+
+MediaCacheStream::LengthAndOffset MediaCacheStream::GetLengthAndOffset() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ AutoLock lock(mMediaCache->Monitor());
+ return {mStreamLength, mChannelOffset};
+}
+
+int64_t MediaCacheStream::GetNextCachedData(int64_t aOffset) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ AutoLock lock(mMediaCache->Monitor());
+ return GetNextCachedDataInternal(lock, aOffset);
+}
+
+int64_t MediaCacheStream::GetCachedDataEnd(int64_t aOffset) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ AutoLock lock(mMediaCache->Monitor());
+ return GetCachedDataEndInternal(lock, aOffset);
+}
+
+bool MediaCacheStream::IsDataCachedToEndOfStream(int64_t aOffset) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ AutoLock lock(mMediaCache->Monitor());
+ if (mStreamLength < 0) return false;
+ return GetCachedDataEndInternal(lock, aOffset) >= mStreamLength;
+}
+
+int64_t MediaCacheStream::GetCachedDataEndInternal(AutoLock&, int64_t aOffset) {
+ int32_t blockIndex = OffsetToBlockIndex(aOffset);
+ if (blockIndex < 0) {
+ return aOffset;
+ }
+ while (size_t(blockIndex) < mBlocks.Length() && mBlocks[blockIndex] != -1) {
+ ++blockIndex;
+ }
+ int64_t result = blockIndex * BLOCK_SIZE;
+ if (blockIndex == OffsetToBlockIndexUnchecked(mChannelOffset)) {
+ // The block containing mChannelOffset may be partially read but not
+ // yet committed to the main cache
+ result = mChannelOffset;
+ }
+ if (mStreamLength >= 0) {
+ // The last block in the cache may only be partially valid, so limit
+ // the cached range to the stream length
+ result = std::min(result, mStreamLength);
+ }
+ return std::max(result, aOffset);
+}
+
+int64_t MediaCacheStream::GetNextCachedDataInternal(AutoLock&,
+ int64_t aOffset) {
+ if (aOffset == mStreamLength) return -1;
+
+ int32_t startBlockIndex = OffsetToBlockIndex(aOffset);
+ if (startBlockIndex < 0) {
+ return -1;
+ }
+ int32_t channelBlockIndex = OffsetToBlockIndexUnchecked(mChannelOffset);
+
+ if (startBlockIndex == channelBlockIndex && aOffset < mChannelOffset) {
+ // The block containing mChannelOffset is partially read, but not
+ // yet committed to the main cache. aOffset lies in the partially
+ // read portion, thus it is effectively cached.
+ return aOffset;
+ }
+
+ if (size_t(startBlockIndex) >= mBlocks.Length()) return -1;
+
+ // Is the current block cached?
+ if (mBlocks[startBlockIndex] != -1) return aOffset;
+
+ // Count the number of uncached blocks
+ bool hasPartialBlock = OffsetInBlock(mChannelOffset) != 0;
+ int32_t blockIndex = startBlockIndex + 1;
+ while (true) {
+ if ((hasPartialBlock && blockIndex == channelBlockIndex) ||
+ (size_t(blockIndex) < mBlocks.Length() && mBlocks[blockIndex] != -1)) {
+ // We at the incoming channel block, which has has data in it,
+ // or are we at a cached block. Return index of block start.
+ return blockIndex * BLOCK_SIZE;
+ }
+
+ // No more cached blocks?
+ if (size_t(blockIndex) >= mBlocks.Length()) return -1;
+
+ ++blockIndex;
+ }
+
+ MOZ_ASSERT_UNREACHABLE("Should return in loop");
+ return -1;
+}
+
+void MediaCacheStream::SetReadMode(ReadMode aMode) {
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ "MediaCacheStream::SetReadMode",
+ [this, client = RefPtr<ChannelMediaResource>(mClient), aMode]() {
+ AutoLock lock(mMediaCache->Monitor());
+ if (!mClosed && mCurrentMode != aMode) {
+ mCurrentMode = aMode;
+ mMediaCache->QueueUpdate(lock);
+ }
+ });
+ OwnerThread()->Dispatch(r.forget());
+}
+
+void MediaCacheStream::SetPlaybackRate(uint32_t aBytesPerSecond) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(aBytesPerSecond > 0, "Zero playback rate not allowed");
+
+ AutoLock lock(mMediaCache->Monitor());
+ if (!mClosed && mPlaybackBytesPerSecond != aBytesPerSecond) {
+ mPlaybackBytesPerSecond = aBytesPerSecond;
+ mMediaCache->QueueUpdate(lock);
+ }
+}
+
+nsresult MediaCacheStream::Seek(AutoLock& aLock, int64_t aOffset) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ if (!IsOffsetAllowed(aOffset)) {
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+ if (mClosed) {
+ return NS_ERROR_ABORT;
+ }
+
+ int64_t oldOffset = mStreamOffset;
+ mStreamOffset = aOffset;
+ LOG("Stream %p Seek to %" PRId64, this, mStreamOffset);
+ mMediaCache->NoteSeek(aLock, this, oldOffset);
+ mMediaCache->QueueUpdate(aLock);
+ return NS_OK;
+}
+
+void MediaCacheStream::ThrottleReadahead(bool bThrottle) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ "MediaCacheStream::ThrottleReadahead",
+ [client = RefPtr<ChannelMediaResource>(mClient), this, bThrottle]() {
+ AutoLock lock(mMediaCache->Monitor());
+ if (!mClosed && mThrottleReadahead != bThrottle) {
+ LOGI("Stream %p ThrottleReadahead %d", this, bThrottle);
+ mThrottleReadahead = bThrottle;
+ mMediaCache->QueueUpdate(lock);
+ }
+ });
+ OwnerThread()->Dispatch(r.forget());
+}
+
+uint32_t MediaCacheStream::ReadPartialBlock(AutoLock&, int64_t aOffset,
+ Span<char> aBuffer) {
+ MOZ_ASSERT(IsOffsetAllowed(aOffset));
+
+ if (OffsetToBlockIndexUnchecked(mChannelOffset) !=
+ OffsetToBlockIndexUnchecked(aOffset) ||
+ aOffset >= mChannelOffset) {
+ // Not in the partial block or no data to read.
+ return 0;
+ }
+
+ auto source = Span<const uint8_t>(
+ mPartialBlockBuffer.get() + OffsetInBlock(aOffset),
+ OffsetInBlock(mChannelOffset) - OffsetInBlock(aOffset));
+ // We have |source.Length() <= BLOCK_SIZE < INT32_MAX| to guarantee
+ // that |bytesToRead| can fit into a uint32_t.
+ uint32_t bytesToRead = std::min(aBuffer.Length(), source.Length());
+ memcpy(aBuffer.Elements(), source.Elements(), bytesToRead);
+ return bytesToRead;
+}
+
+Result<uint32_t, nsresult> MediaCacheStream::ReadBlockFromCache(
+ AutoLock& aLock, int64_t aOffset, Span<char> aBuffer,
+ bool aNoteBlockUsage) {
+ MOZ_ASSERT(IsOffsetAllowed(aOffset));
+
+ // OffsetToBlockIndexUnchecked() is always non-negative.
+ uint32_t index = OffsetToBlockIndexUnchecked(aOffset);
+ int32_t cacheBlock = index < mBlocks.Length() ? mBlocks[index] : -1;
+ if (cacheBlock < 0 || (mStreamLength >= 0 && aOffset >= mStreamLength)) {
+ // Not in the cache.
+ return 0;
+ }
+
+ if (aBuffer.Length() > size_t(BLOCK_SIZE)) {
+ // Clamp the buffer to avoid overflow below since we will read at most
+ // BLOCK_SIZE bytes.
+ aBuffer = aBuffer.First(BLOCK_SIZE);
+ }
+
+ if (mStreamLength >= 0 &&
+ int64_t(aBuffer.Length()) > mStreamLength - aOffset) {
+ // Clamp reads to stream's length
+ aBuffer = aBuffer.First(mStreamLength - aOffset);
+ }
+
+ // |BLOCK_SIZE - OffsetInBlock(aOffset)| <= BLOCK_SIZE
+ int32_t bytesToRead =
+ std::min<int32_t>(BLOCK_SIZE - OffsetInBlock(aOffset), aBuffer.Length());
+ int32_t bytesRead = 0;
+ nsresult rv = mMediaCache->ReadCacheFile(
+ aLock, cacheBlock * BLOCK_SIZE + OffsetInBlock(aOffset),
+ aBuffer.Elements(), bytesToRead, &bytesRead);
+
+ // Ensure |cacheBlock * BLOCK_SIZE + OffsetInBlock(aOffset)| won't overflow.
+ static_assert(INT64_MAX >= BLOCK_SIZE * (uint32_t(INT32_MAX) + 1),
+ "BLOCK_SIZE too large!");
+
+ if (NS_FAILED(rv)) {
+ nsCString name;
+ GetErrorName(rv, name);
+ LOGE("Stream %p ReadCacheFile failed, rv=%s", this, name.Data());
+ return mozilla::Err(rv);
+ }
+
+ if (aNoteBlockUsage) {
+ mMediaCache->NoteBlockUsage(aLock, this, cacheBlock, aOffset, mCurrentMode,
+ TimeStamp::Now());
+ }
+
+ return bytesRead;
+}
+
+nsresult MediaCacheStream::Read(AutoLock& aLock, char* aBuffer, uint32_t aCount,
+ uint32_t* aBytes) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ // Cache the offset in case it is changed again when we are waiting for the
+ // monitor to be notified to avoid reading at the wrong position.
+ auto streamOffset = mStreamOffset;
+
+ // The buffer we are about to fill.
+ auto buffer = Span<char>(aBuffer, aCount);
+
+ // Read one block (or part of a block) at a time
+ while (!buffer.IsEmpty()) {
+ if (mClosed) {
+ return NS_ERROR_ABORT;
+ }
+
+ if (!IsOffsetAllowed(streamOffset)) {
+ LOGE("Stream %p invalid offset=%" PRId64, this, streamOffset);
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ if (mStreamLength >= 0 && streamOffset >= mStreamLength) {
+ // Don't try to read beyond the end of the stream
+ break;
+ }
+
+ Result<uint32_t, nsresult> rv = ReadBlockFromCache(
+ aLock, streamOffset, buffer, true /* aNoteBlockUsage */);
+ if (rv.isErr()) {
+ return rv.unwrapErr();
+ }
+
+ uint32_t bytes = rv.unwrap();
+ if (bytes > 0) {
+ // Got data from the cache successfully. Read next block.
+ streamOffset += bytes;
+ buffer = buffer.From(bytes);
+ continue;
+ }
+
+ // See if we can use the data in the partial block of any stream reading
+ // this resource. Note we use the partial block only when it is completed,
+ // that is reaching EOS.
+ bool foundDataInPartialBlock = false;
+ MediaCache::ResourceStreamIterator iter(mMediaCache, mResourceID);
+ while (MediaCacheStream* stream = iter.Next(aLock)) {
+ if (OffsetToBlockIndexUnchecked(stream->mChannelOffset) ==
+ OffsetToBlockIndexUnchecked(streamOffset) &&
+ stream->mChannelOffset == stream->mStreamLength) {
+ uint32_t bytes = stream->ReadPartialBlock(aLock, streamOffset, buffer);
+ streamOffset += bytes;
+ buffer = buffer.From(bytes);
+ foundDataInPartialBlock = true;
+ break;
+ }
+ }
+ if (foundDataInPartialBlock) {
+ // Break for we've reached EOS.
+ break;
+ }
+
+ if (mDidNotifyDataEnded && NS_FAILED(mNotifyDataEndedStatus)) {
+ // Since download ends abnormally, there is no point in waiting for new
+ // data to come. We will check the partial block to read as many bytes as
+ // possible before exiting this function.
+ bytes = ReadPartialBlock(aLock, streamOffset, buffer);
+ streamOffset += bytes;
+ buffer = buffer.From(bytes);
+ break;
+ }
+
+ if (mStreamOffset != streamOffset) {
+ // Update mStreamOffset before we drop the lock. We need to run
+ // Update() again since stream reading strategy might have changed.
+ mStreamOffset = streamOffset;
+ mMediaCache->QueueUpdate(aLock);
+ }
+
+ // No data to read, so block
+ aLock.Wait();
+ }
+
+ uint32_t count = buffer.Elements() - aBuffer;
+ *aBytes = count;
+ if (count == 0) {
+ return NS_OK;
+ }
+
+ // Some data was read, so queue an update since block priorities may
+ // have changed
+ mMediaCache->QueueUpdate(aLock);
+
+ LOG("Stream %p Read at %" PRId64 " count=%d", this, streamOffset - count,
+ count);
+ mStreamOffset = streamOffset;
+ return NS_OK;
+}
+
+nsresult MediaCacheStream::ReadAt(int64_t aOffset, char* aBuffer,
+ uint32_t aCount, uint32_t* aBytes) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ AutoLock lock(mMediaCache->Monitor());
+ nsresult rv = Seek(lock, aOffset);
+ if (NS_FAILED(rv)) return rv;
+ return Read(lock, aBuffer, aCount, aBytes);
+}
+
+nsresult MediaCacheStream::ReadFromCache(char* aBuffer, int64_t aOffset,
+ uint32_t aCount) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ AutoLock lock(mMediaCache->Monitor());
+
+ // The buffer we are about to fill.
+ auto buffer = Span<char>(aBuffer, aCount);
+
+ // Read one block (or part of a block) at a time
+ int64_t streamOffset = aOffset;
+ while (!buffer.IsEmpty()) {
+ if (mClosed) {
+ // We need to check |mClosed| in each iteration which might be changed
+ // after calling |mMediaCache->ReadCacheFile|.
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!IsOffsetAllowed(streamOffset)) {
+ LOGE("Stream %p invalid offset=%" PRId64, this, streamOffset);
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ Result<uint32_t, nsresult> rv =
+ ReadBlockFromCache(lock, streamOffset, buffer);
+ if (rv.isErr()) {
+ return rv.unwrapErr();
+ }
+
+ uint32_t bytes = rv.unwrap();
+ if (bytes > 0) {
+ // Read data from the cache successfully. Let's try next block.
+ streamOffset += bytes;
+ buffer = buffer.From(bytes);
+ continue;
+ }
+
+ // The partial block is our last chance to get data.
+ bytes = ReadPartialBlock(lock, streamOffset, buffer);
+ if (bytes < buffer.Length()) {
+ // Not enough data to read.
+ return NS_ERROR_FAILURE;
+ }
+
+ // Return for we've got all the requested bytes.
+ return NS_OK;
+ }
+
+ return NS_OK;
+}
+
+nsresult MediaCacheStream::Init(int64_t aContentLength) {
+ NS_ASSERTION(NS_IsMainThread(), "Only call on main thread");
+ MOZ_ASSERT(!mMediaCache, "Has been initialized.");
+
+ if (aContentLength > 0) {
+ uint32_t length = uint32_t(std::min(aContentLength, int64_t(UINT32_MAX)));
+ LOG("MediaCacheStream::Init(this=%p) "
+ "MEDIACACHESTREAM_NOTIFIED_LENGTH=%" PRIu32,
+ this, length);
+
+ mStreamLength = aContentLength;
+ }
+
+ mMediaCache = MediaCache::GetMediaCache(aContentLength, mIsPrivateBrowsing);
+ if (!mMediaCache) {
+ return NS_ERROR_FAILURE;
+ }
+
+ OwnerThread()->Dispatch(NS_NewRunnableFunction(
+ "MediaCacheStream::Init",
+ [this, res = RefPtr<ChannelMediaResource>(mClient)]() {
+ AutoLock lock(mMediaCache->Monitor());
+ mMediaCache->OpenStream(lock, this);
+ }));
+
+ return NS_OK;
+}
+
+void MediaCacheStream::InitAsClone(MediaCacheStream* aOriginal) {
+ MOZ_ASSERT(!mMediaCache, "Has been initialized.");
+ MOZ_ASSERT(aOriginal->mMediaCache, "Don't clone an uninitialized stream.");
+
+ // Use the same MediaCache as our clone.
+ mMediaCache = aOriginal->mMediaCache;
+ OwnerThread()->Dispatch(NS_NewRunnableFunction(
+ "MediaCacheStream::InitAsClone",
+ [this, aOriginal, r1 = RefPtr<ChannelMediaResource>(mClient),
+ r2 = RefPtr<ChannelMediaResource>(aOriginal->mClient)]() {
+ InitAsCloneInternal(aOriginal);
+ }));
+}
+
+void MediaCacheStream::InitAsCloneInternal(MediaCacheStream* aOriginal) {
+ MOZ_ASSERT(OwnerThread()->IsOnCurrentThread());
+ AutoLock lock(mMediaCache->Monitor());
+ LOG("MediaCacheStream::InitAsCloneInternal(this=%p, original=%p)", this,
+ aOriginal);
+
+ // Download data and notify events if necessary. Note the order is important
+ // in order to mimic the behavior of data being downloaded from the channel.
+
+ // Step 1: copy/download data from the original stream.
+ mResourceID = aOriginal->mResourceID;
+ mStreamLength = aOriginal->mStreamLength;
+ mIsTransportSeekable = aOriginal->mIsTransportSeekable;
+ mDownloadStatistics = aOriginal->mDownloadStatistics;
+ mDownloadStatistics.Stop();
+
+ // Grab cache blocks from aOriginal as readahead blocks for our stream
+ for (uint32_t i = 0; i < aOriginal->mBlocks.Length(); ++i) {
+ int32_t cacheBlockIndex = aOriginal->mBlocks[i];
+ if (cacheBlockIndex < 0) continue;
+
+ while (i >= mBlocks.Length()) {
+ mBlocks.AppendElement(-1);
+ }
+ // Every block is a readahead block for the clone because the clone's
+ // initial stream offset is zero
+ mMediaCache->AddBlockOwnerAsReadahead(lock, cacheBlockIndex, this, i);
+ }
+
+ // Copy the partial block.
+ mChannelOffset = aOriginal->mChannelOffset;
+ memcpy(mPartialBlockBuffer.get(), aOriginal->mPartialBlockBuffer.get(),
+ BLOCK_SIZE);
+
+ // Step 2: notify the client that we have new data so the decoder has a chance
+ // to compute 'canplaythrough' and buffer ranges.
+ mClient->CacheClientNotifyDataReceived();
+
+ // Step 3: notify download ended if necessary.
+ if (aOriginal->mDidNotifyDataEnded &&
+ NS_SUCCEEDED(aOriginal->mNotifyDataEndedStatus)) {
+ mNotifyDataEndedStatus = aOriginal->mNotifyDataEndedStatus;
+ mDidNotifyDataEnded = true;
+ mClient->CacheClientNotifyDataEnded(mNotifyDataEndedStatus);
+ }
+
+ // Step 4: notify download is suspended by the cache.
+ mClientSuspended = true;
+ mCacheSuspended = true;
+ mChannelEnded = true;
+ mClient->CacheClientSuspend();
+ mMediaCache->QueueSuspendedStatusUpdate(lock, mResourceID);
+
+ // Step 5: add the stream to be managed by the cache.
+ mMediaCache->OpenStream(lock, this, true /* aIsClone */);
+ // Wake up the reader which is waiting for the cloned data.
+ lock.NotifyAll();
+}
+
+nsISerialEventTarget* MediaCacheStream::OwnerThread() const {
+ return mMediaCache->OwnerThread();
+}
+
+nsresult MediaCacheStream::GetCachedRanges(MediaByteRangeSet& aRanges) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ // Take the monitor, so that the cached data ranges can't grow while we're
+ // trying to loop over them.
+ AutoLock lock(mMediaCache->Monitor());
+
+ // We must be pinned while running this, otherwise the cached data ranges may
+ // shrink while we're trying to loop over them.
+ NS_ASSERTION(mPinCount > 0, "Must be pinned");
+
+ int64_t startOffset = GetNextCachedDataInternal(lock, 0);
+ while (startOffset >= 0) {
+ int64_t endOffset = GetCachedDataEndInternal(lock, startOffset);
+ NS_ASSERTION(startOffset < endOffset,
+ "Buffered range must end after its start");
+ // Bytes [startOffset..endOffset] are cached.
+ aRanges += MediaByteRange(startOffset, endOffset);
+ startOffset = GetNextCachedDataInternal(lock, endOffset);
+ NS_ASSERTION(
+ startOffset == -1 || startOffset > endOffset,
+ "Must have advanced to start of next range, or hit end of stream");
+ }
+ return NS_OK;
+}
+
+double MediaCacheStream::GetDownloadRate(bool* aIsReliable) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ AutoLock lock(mMediaCache->Monitor());
+ return mDownloadStatistics.GetRate(aIsReliable);
+}
+
+void MediaCacheStream::GetDebugInfo(dom::MediaCacheStreamDebugInfo& aInfo) {
+ AutoLock lock(mMediaCache->GetMonitorOnTheMainThread());
+ aInfo.mStreamLength = mStreamLength;
+ aInfo.mChannelOffset = mChannelOffset;
+ aInfo.mCacheSuspended = mCacheSuspended;
+ aInfo.mChannelEnded = mChannelEnded;
+ aInfo.mLoadID = mLoadID;
+}
+
+} // namespace mozilla
+
+// avoid redefined macro in unified build
+#undef LOG
+#undef LOGI
diff --git a/dom/media/MediaCache.h b/dom/media/MediaCache.h
new file mode 100644
index 0000000000..b4559c1fd1
--- /dev/null
+++ b/dom/media/MediaCache.h
@@ -0,0 +1,557 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MediaCache_h_
+#define MediaCache_h_
+
+#include "DecoderDoctorLogger.h"
+#include "Intervals.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/Result.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/MediaDebugInfoBinding.h"
+#include "nsCOMPtr.h"
+#include "nsHashKeys.h"
+#include "nsTArray.h"
+#include "nsTHashtable.h"
+
+#include "MediaChannelStatistics.h"
+
+class nsIEventTarget;
+class nsIPrincipal;
+
+namespace mozilla {
+// defined in MediaResource.h
+class ChannelMediaResource;
+typedef media::IntervalSet<int64_t> MediaByteRangeSet;
+class MediaResource;
+
+/**
+ * Media applications want fast, "on demand" random access to media data,
+ * for pausing, seeking, etc. But we are primarily interested
+ * in transporting media data using HTTP over the Internet, which has
+ * high latency to open a connection, requires a new connection for every
+ * seek, may not even support seeking on some connections (especially
+ * live streams), and uses a push model --- data comes from the server
+ * and you don't have much control over the rate. Also, transferring data
+ * over the Internet can be slow and/or unpredictable, so we want to read
+ * ahead to buffer and cache as much data as possible.
+ *
+ * The job of the media cache is to resolve this impedance mismatch.
+ * The media cache reads data from Necko channels into file-backed storage,
+ * and offers a random-access file-like API to the stream data
+ * (MediaCacheStream). Along the way it solves several problems:
+ * -- The cache intelligently reads ahead to prefetch data that may be
+ * needed in the future
+ * -- The size of the cache is bounded so that we don't fill up
+ * storage with read-ahead data
+ * -- Cache replacement is managed globally so that the most valuable
+ * data (across all streams) is retained
+ * -- The cache can suspend Necko channels temporarily when their data is
+ * not wanted (yet)
+ * -- The cache translates file-like seek requests to HTTP seeks,
+ * including optimizations like not triggering a new seek if it would
+ * be faster to just keep reading until we reach the seek point. The
+ * "seek to EOF" idiom to determine file size is also handled efficiently
+ * (seeking to EOF and then seeking back to the previous offset does not
+ * trigger any Necko activity)
+ * -- The cache also handles the case where the server does not support
+ * seeking
+ * -- Necko can only send data to the main thread, but MediaCacheStream
+ * can distribute data to any thread
+ * -- The cache exposes APIs so clients can detect what data is
+ * currently held
+ *
+ * Note that although HTTP is the most important transport and we only
+ * support transport-level seeking via HTTP byte-ranges, the media cache
+ * works with any kind of Necko channels and provides random access to
+ * cached data even for, e.g., FTP streams.
+ *
+ * The media cache is not persistent. It does not currently allow
+ * data from one load to be used by other loads, either within the same
+ * browser session or across browser sessions. The media cache file
+ * is marked "delete on close" so it will automatically disappear in the
+ * event of a browser crash or shutdown.
+ *
+ * The media cache is block-based. Streams are divided into blocks of a
+ * fixed size (currently 4K) and we cache blocks. A single cache contains
+ * blocks for all streams.
+ *
+ * The cache size is controlled by the media.cache_size preference
+ * (which is in KB). The default size is 500MB.
+ *
+ * The replacement policy predicts a "time of next use" for each block
+ * in the cache. When we need to free a block, the block with the latest
+ * "time of next use" will be evicted. Blocks are divided into
+ * different classes, each class having its own predictor:
+ * FREE_BLOCK: these blocks are effectively infinitely far in the future;
+ * a free block will always be chosen for replacement before other classes
+ * of blocks.
+ * METADATA_BLOCK: these are blocks that contain data that has been read
+ * by the decoder in "metadata mode", e.g. while the decoder is searching
+ * the stream during a seek operation. These blocks are managed with an
+ * LRU policy; the "time of next use" is predicted to be as far in the
+ * future as the last use was in the past.
+ * PLAYED_BLOCK: these are blocks that have not been read in "metadata
+ * mode", and contain data behind the current decoder read point. (They
+ * may not actually have been read by the decoder, if the decoder seeked
+ * forward.) These blocks are managed with an LRU policy except that we add
+ * REPLAY_DELAY seconds of penalty to their predicted "time of next use",
+ * to reflect the uncertainty about whether replay will actually happen
+ * or not.
+ * READAHEAD_BLOCK: these are blocks that have not been read in
+ * "metadata mode" and that are entirely ahead of the current decoder
+ * read point. (They may actually have been read by the decoder in the
+ * past if the decoder has since seeked backward.) We predict the
+ * time of next use for these blocks by assuming steady playback and
+ * dividing the number of bytes between the block and the current decoder
+ * read point by the decoder's estimate of its playback rate in bytes
+ * per second. This ensures that the blocks farthest ahead are considered
+ * least valuable.
+ * For efficient prediction of the "latest time of next use", we maintain
+ * linked lists of blocks in each class, ordering blocks by time of
+ * next use. READAHEAD_BLOCKS have one linked list per stream, since their
+ * time of next use depends on stream parameters, but the other lists
+ * are global.
+ *
+ * A block containing a current decoder read point can contain data
+ * both behind and ahead of the read point. It will be classified as a
+ * PLAYED_BLOCK but we will give it special treatment so it is never
+ * evicted --- it actually contains the highest-priority readahead data
+ * as well as played data.
+ *
+ * "Time of next use" estimates are also used for flow control. When
+ * reading ahead we can predict the time of next use for the data that
+ * will be read. If the predicted time of next use is later then the
+ * prediction for all currently cached blocks, and the cache is full, then
+ * we should suspend reading from the Necko channel.
+ *
+ * Unfortunately suspending the Necko channel can't immediately stop the
+ * flow of data from the server. First our desire to suspend has to be
+ * transmitted to the server (in practice, Necko stops reading from the
+ * socket, which causes the kernel to shrink its advertised TCP receive
+ * window size to zero). Then the server can stop sending the data, but
+ * we will receive data roughly corresponding to the product of the link
+ * bandwidth multiplied by the round-trip latency. We deal with this by
+ * letting the cache overflow temporarily and then trimming it back by
+ * moving overflowing blocks back into the body of the cache, replacing
+ * less valuable blocks as they become available. We try to avoid simply
+ * discarding overflowing readahead data.
+ *
+ * All changes to the actual contents of the cache happen on the main
+ * thread, since that's where Necko's notifications happen.
+ *
+ * The media cache maintains at most one Necko channel for each stream.
+ * (In the future it might be advantageous to relax this, e.g. so that a
+ * seek to near the end of the file can happen without disturbing
+ * the loading of data from the beginning of the file.) The Necko channel
+ * is managed through ChannelMediaResource; MediaCache does not
+ * depend on Necko directly.
+ *
+ * Every time something changes that might affect whether we want to
+ * read from a Necko channel, or whether we want to seek on the Necko
+ * channel --- such as data arriving or data being consumed by the
+ * decoder --- we asynchronously trigger MediaCache::Update on the main
+ * thread. That method implements most cache policy. It evaluates for
+ * each stream whether we want to suspend or resume the stream and what
+ * offset we should seek to, if any. It is also responsible for trimming
+ * back the cache size to its desired limit by moving overflowing blocks
+ * into the main part of the cache.
+ *
+ * Streams can be opened in non-seekable mode. In non-seekable mode,
+ * the cache will only call ChannelMediaResource::CacheClientSeek with
+ * a 0 offset. The cache tries hard not to discard readahead data
+ * for non-seekable streams, since that could trigger a potentially
+ * disastrous re-read of the entire stream. It's up to cache clients
+ * to try to avoid requesting seeks on such streams.
+ *
+ * MediaCache has a single internal monitor for all synchronization.
+ * This is treated as the lowest level monitor in the media code. So,
+ * we must not acquire any MediaDecoder locks or MediaResource locks
+ * while holding the MediaCache lock. But it's OK to hold those locks
+ * and then get the MediaCache lock.
+ *
+ * MediaCache associates a principal with each stream. CacheClientSeek
+ * can trigger new HTTP requests; due to redirects to other domains,
+ * each HTTP load can return data with a different principal. This
+ * principal must be passed to NotifyDataReceived, and MediaCache
+ * will detect when different principals are associated with data in the
+ * same stream, and replace them with a null principal.
+ */
+class MediaCache;
+
+DDLoggedTypeDeclName(MediaCacheStream);
+
+/**
+ * If the cache fails to initialize then Init will fail, so nonstatic
+ * methods of this class can assume gMediaCache is non-null.
+ *
+ * This class can be directly embedded as a value.
+ */
+class MediaCacheStream : public DecoderDoctorLifeLogger<MediaCacheStream> {
+ using AutoLock = MonitorAutoLock;
+
+ public:
+ // This needs to be a power of two
+ static const int64_t BLOCK_SIZE = 32768;
+
+ enum ReadMode { MODE_METADATA, MODE_PLAYBACK };
+
+ // aClient provides the underlying transport that cache will use to read
+ // data for this stream.
+ MediaCacheStream(ChannelMediaResource* aClient, bool aIsPrivateBrowsing);
+ ~MediaCacheStream();
+
+ // Set up this stream with the cache. Can fail on OOM.
+ // aContentLength is the content length if known, otherwise -1.
+ // Exactly one of InitAsClone or Init must be called before any other method
+ // on this class. Does nothing if already initialized.
+ nsresult Init(int64_t aContentLength);
+
+ // Set up this stream with the cache, assuming it's for the same data
+ // as the aOriginal stream.
+ // Exactly one of InitAsClone or Init must be called before any other method
+ // on this class.
+ void InitAsClone(MediaCacheStream* aOriginal);
+
+ nsISerialEventTarget* OwnerThread() const;
+
+ // These are called on the main thread.
+ // This must be called (and resolve) before the ChannelMediaResource
+ // used to create this MediaCacheStream is deleted.
+ RefPtr<GenericPromise> Close();
+ // This returns true when the stream has been closed.
+ bool IsClosed(AutoLock&) const { return mClosed; }
+ // Returns true when this stream is can be shared by a new resource load.
+ // Called on the main thread only.
+ bool IsAvailableForSharing() const { return !mIsPrivateBrowsing; }
+
+ // These callbacks are called on the main thread by the client
+ // when data has been received via the channel.
+
+ // Notifies the cache that a load has begun. We pass the offset
+ // because in some cases the offset might not be what the cache
+ // requested. In particular we might unexpectedly start providing
+ // data at offset 0. This need not be called if the offset is the
+ // offset that the cache requested in
+ // ChannelMediaResource::CacheClientSeek. This can be called at any
+ // time by the client, not just after a CacheClientSeek.
+ //
+ // aSeekable tells us whether the stream is seekable or not. Non-seekable
+ // streams will always pass 0 for aOffset to CacheClientSeek. This should only
+ // be called while the stream is at channel offset 0. Seekability can
+ // change during the lifetime of the MediaCacheStream --- every time
+ // we do an HTTP load the seekability may be different (and sometimes
+ // is, in practice, due to the effects of caching proxies).
+ //
+ // aLength tells the cache what the server said the data length is going to
+ // be. The actual data length may be greater (we receive more data than
+ // specified) or smaller (the stream ends before we reach the given
+ // length), because servers can lie. The server's reported data length
+ // *and* the actual data length can even vary over time because a
+ // misbehaving server may feed us a different stream after each seek
+ // operation. So this is really just a hint. The cache may however
+ // stop reading (suspend the channel) when it thinks we've read all the
+ // data available based on an incorrect reported length. Seeks relative
+ // EOF also depend on the reported length if we haven't managed to
+ // read the whole stream yet.
+ void NotifyDataStarted(uint32_t aLoadID, int64_t aOffset, bool aSeekable,
+ int64_t aLength);
+ // Notifies the cache that data has been received. The stream already
+ // knows the offset because data is received in sequence and
+ // the starting offset is known via NotifyDataStarted or because
+ // the cache requested the offset in
+ // ChannelMediaResource::CacheClientSeek, or because it defaulted to 0.
+ void NotifyDataReceived(uint32_t aLoadID, uint32_t aCount,
+ const uint8_t* aData);
+
+ // Set the load ID so the following NotifyDataEnded() call can work properly.
+ // Used in some rare cases where NotifyDataEnded() is called without the
+ // preceding NotifyDataStarted().
+ void NotifyLoadID(uint32_t aLoadID);
+
+ // Notifies the cache that the channel has closed with the given status.
+ void NotifyDataEnded(uint32_t aLoadID, nsresult aStatus);
+
+ // Notifies the stream that the suspend status of the client has changed.
+ // Main thread only.
+ void NotifyClientSuspended(bool aSuspended);
+
+ // Notifies the stream to resume download at the current offset.
+ void NotifyResume();
+
+ // These methods can be called on any thread.
+ // Cached blocks associated with this stream will not be evicted
+ // while the stream is pinned.
+ void Pin();
+ void Unpin();
+ // See comments above for NotifyDataStarted about how the length
+ // can vary over time. Returns -1 if no length is known. Returns the
+ // reported length if we haven't got any better information. If
+ // the stream ended normally we return the length we actually got.
+ // If we've successfully read data beyond the originally reported length,
+ // we return the end of the data we've read.
+ int64_t GetLength() const;
+ // Return the length and offset where next channel data will write to. Main
+ // thread only.
+ // This method should be removed as part of bug 1464045.
+ struct LengthAndOffset {
+ int64_t mLength;
+ int64_t mOffset;
+ };
+ LengthAndOffset GetLengthAndOffset() const;
+ // Returns the unique resource ID. Call only on the main thread or while
+ // holding the media cache lock.
+ int64_t GetResourceID() { return mResourceID; }
+ // Returns the end of the bytes starting at the given offset
+ // which are in cache.
+ int64_t GetCachedDataEnd(int64_t aOffset);
+ // Returns the offset of the first byte of cached data at or after aOffset,
+ // or -1 if there is no such cached data.
+ int64_t GetNextCachedData(int64_t aOffset);
+ // Fills aRanges with the ByteRanges representing the data which is currently
+ // cached. Locks the media cache while running, to prevent any ranges
+ // growing. The stream should be pinned while this runs and while its results
+ // are used, to ensure no data is evicted.
+ nsresult GetCachedRanges(MediaByteRangeSet& aRanges);
+
+ double GetDownloadRate(bool* aIsReliable);
+
+ // Reads from buffered data only. Will fail if not all data to be read is
+ // in the cache. Will not mark blocks as read. Can be called from the main
+ // thread. It's the caller's responsibility to wrap the call in a pin/unpin,
+ // and also to check that the range they want is cached before calling this.
+ nsresult ReadFromCache(char* aBuffer, int64_t aOffset, uint32_t aCount);
+
+ // IsDataCachedToEndOfStream returns true if all the data from
+ // aOffset to the end of the stream (the server-reported end, if the
+ // real end is not known) is in cache. If we know nothing about the
+ // end of the stream, this returns false.
+ bool IsDataCachedToEndOfStream(int64_t aOffset);
+ // The mode is initially MODE_METADATA.
+ void SetReadMode(ReadMode aMode);
+ // This is the client's estimate of the playback rate assuming
+ // the media plays continuously. The cache can't guess this itself
+ // because it doesn't know when the decoder was paused, buffering, etc.
+ // Do not pass zero.
+ void SetPlaybackRate(uint32_t aBytesPerSecond);
+
+ // Returns true when all streams for this resource are suspended or their
+ // channel has ended.
+ bool AreAllStreamsForResourceSuspended(AutoLock&);
+
+ // These methods must be called on a different thread from the main
+ // thread. They should always be called on the same thread for a given
+ // stream.
+ // *aBytes gets the number of bytes that were actually read. This can
+ // be less than aCount. If the first byte of data is not in the cache,
+ // this will block until the data is available or the stream is
+ // closed, otherwise it won't block.
+ nsresult Read(AutoLock&, char* aBuffer, uint32_t aCount, uint32_t* aBytes);
+ // Seeks to aOffset in the stream then performs a Read operation. See
+ // 'Read' for argument and return details.
+ nsresult ReadAt(int64_t aOffset, char* aBuffer, uint32_t aCount,
+ uint32_t* aBytes);
+
+ void ThrottleReadahead(bool bThrottle);
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const;
+
+ void GetDebugInfo(dom::MediaCacheStreamDebugInfo& aInfo);
+
+ private:
+ friend class MediaCache;
+
+ /**
+ * A doubly-linked list of blocks. Add/Remove/Get methods are all
+ * constant time. We declare this here so that a stream can contain a
+ * BlockList of its read-ahead blocks. Blocks are referred to by index
+ * into the MediaCache::mIndex array.
+ *
+ * Blocks can belong to more than one list at the same time, because
+ * the next/prev pointers are not stored in the block.
+ */
+ class BlockList {
+ public:
+ BlockList() : mFirstBlock(-1), mCount(0) {}
+ ~BlockList() {
+ NS_ASSERTION(mFirstBlock == -1 && mCount == 0,
+ "Destroying non-empty block list");
+ }
+ void AddFirstBlock(int32_t aBlock);
+ void AddAfter(int32_t aBlock, int32_t aBefore);
+ void RemoveBlock(int32_t aBlock);
+ // Returns the first block in the list, or -1 if empty
+ int32_t GetFirstBlock() const { return mFirstBlock; }
+ // Returns the last block in the list, or -1 if empty
+ int32_t GetLastBlock() const;
+ // Returns the next block in the list after aBlock or -1 if
+ // aBlock is the last block
+ int32_t GetNextBlock(int32_t aBlock) const;
+ // Returns the previous block in the list before aBlock or -1 if
+ // aBlock is the first block
+ int32_t GetPrevBlock(int32_t aBlock) const;
+ bool IsEmpty() const { return mFirstBlock < 0; }
+ int32_t GetCount() const { return mCount; }
+ // The contents of aBlockIndex1 and aBlockIndex2 have been swapped
+ void NotifyBlockSwapped(int32_t aBlockIndex1, int32_t aBlockIndex2);
+#ifdef DEBUG
+ // Verify linked-list invariants
+ void Verify();
+#else
+ void Verify() {}
+#endif
+
+ size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ private:
+ struct Entry : public nsUint32HashKey {
+ explicit Entry(KeyTypePointer aKey)
+ : nsUint32HashKey(aKey), mNextBlock(0), mPrevBlock(0) {}
+ Entry(const Entry& toCopy)
+ : nsUint32HashKey(&toCopy.GetKey()),
+ mNextBlock(toCopy.mNextBlock),
+ mPrevBlock(toCopy.mPrevBlock) {}
+
+ int32_t mNextBlock;
+ int32_t mPrevBlock;
+ };
+ nsTHashtable<Entry> mEntries;
+
+ // The index of the first block in the list, or -1 if the list is empty.
+ int32_t mFirstBlock;
+ // The number of blocks in the list.
+ int32_t mCount;
+ };
+
+ // Read data from the partial block and return the number of bytes read
+ // successfully. 0 if aOffset is not an offset in the partial block or there
+ // is nothing to read.
+ uint32_t ReadPartialBlock(AutoLock&, int64_t aOffset, Span<char> aBuffer);
+
+ // Read data from the cache block specified by aOffset. Return the number of
+ // bytes read successfully or an error code if any failure.
+ Result<uint32_t, nsresult> ReadBlockFromCache(AutoLock&, int64_t aOffset,
+ Span<char> aBuffer,
+ bool aNoteBlockUsage = false);
+
+ // Non-main thread only.
+ nsresult Seek(AutoLock&, int64_t aOffset);
+
+ // Returns the end of the bytes starting at the given offset
+ // which are in cache.
+ // This method assumes that the cache monitor is held and can be called on
+ // any thread.
+ int64_t GetCachedDataEndInternal(AutoLock&, int64_t aOffset);
+ // Returns the offset of the first byte of cached data at or after aOffset,
+ // or -1 if there is no such cached data.
+ // This method assumes that the cache monitor is held and can be called on
+ // any thread.
+ int64_t GetNextCachedDataInternal(AutoLock&, int64_t aOffset);
+ // Used by |NotifyDataEnded| to write |mPartialBlock| to disk.
+ // If |aNotifyAll| is true, this function will wake up readers who may be
+ // waiting on the media cache monitor. Called on the media cache thread only.
+ void FlushPartialBlockInternal(AutoLock&, bool aNotifyAll);
+
+ void NotifyDataStartedInternal(uint32_t aLoadID, int64_t aOffset,
+ bool aSeekable, int64_t aLength);
+
+ void NotifyDataEndedInternal(uint32_t aLoadID, nsresult aStatus);
+
+ void UpdateDownloadStatistics(AutoLock&);
+
+ void CloseInternal(AutoLock&);
+ void InitAsCloneInternal(MediaCacheStream* aOriginal);
+
+ // Instance of MediaCache to use with this MediaCacheStream.
+ RefPtr<MediaCache> mMediaCache;
+
+ ChannelMediaResource* const mClient;
+
+ // The following fields must be written holding the cache's monitor and
+ // only on the main thread, thus can be read either on the main thread
+ // or while holding the cache's monitor.
+
+ // Set to true when the stream has been closed either explicitly or
+ // due to an internal cache error
+ bool mClosed = false;
+ // This is a unique ID representing the resource we're loading.
+ // All streams with the same mResourceID are loading the same
+ // underlying resource and should share data.
+ // Initialized to 0 as invalid. Will be allocated a valid ID (always positive)
+ // from the cache.
+ int64_t mResourceID = 0;
+ // The last reported seekability state for the underlying channel
+ bool mIsTransportSeekable;
+ // True if the cache has suspended our channel because the cache is
+ // full and the priority of the data that would be received is lower
+ // than the priority of the data already in the cache
+ bool mCacheSuspended;
+ // True if the channel ended and we haven't seeked it again.
+ bool mChannelEnded;
+
+ // The following fields are protected by the cache's monitor and can be
+ // written by any thread.
+
+ // The reported or discovered length of the data, or -1 if nothing is known
+ int64_t mStreamLength = -1;
+ // The offset where the next data from the channel will arrive
+ int64_t mChannelOffset = 0;
+ // The offset where the reader is positioned in the stream
+ int64_t mStreamOffset;
+ // For each block in the stream data, maps to the cache entry for the
+ // block, or -1 if the block is not cached.
+ nsTArray<int32_t> mBlocks;
+ // The list of read-ahead blocks, ordered by stream offset; the first
+ // block is the earliest in the stream (so the last block will be the
+ // least valuable).
+ BlockList mReadaheadBlocks;
+ // The list of metadata blocks; the first block is the most recently used
+ BlockList mMetadataBlocks;
+ // The list of played-back blocks; the first block is the most recently used
+ BlockList mPlayedBlocks;
+ // The last reported estimate of the decoder's playback rate
+ uint32_t mPlaybackBytesPerSecond;
+ // The number of times this stream has been Pinned without a
+ // corresponding Unpin
+ uint32_t mPinCount;
+ // True if CacheClientNotifyDataEnded has been called for this stream.
+ bool mDidNotifyDataEnded = false;
+ // The status used when we did CacheClientNotifyDataEnded. Only valid
+ // when mDidNotifyDataEnded is true.
+ nsresult mNotifyDataEndedStatus;
+ // The last reported read mode
+ ReadMode mCurrentMode = MODE_METADATA;
+ // The load ID of the current channel. Used to check whether the data is
+ // coming from an old channel and should be discarded.
+ uint32_t mLoadID = 0;
+ // The seek target initiated by MediaCache. -1 if no seek is going on.
+ int64_t mSeekTarget = -1;
+
+ bool mThrottleReadahead = false;
+
+ // Data received for the block containing mChannelOffset. Data needs
+ // to wait here so we can write back a complete block. The first
+ // mChannelOffset%BLOCK_SIZE bytes have been filled in with good data,
+ // the rest are garbage.
+ // Heap allocate this buffer since the exact power-of-2 will cause allocation
+ // slop when combined with the rest of the object members.
+ // This partial buffer should always be read/write within the cache's monitor.
+ const UniquePtr<uint8_t[]> mPartialBlockBuffer =
+ MakeUnique<uint8_t[]>(BLOCK_SIZE);
+
+ // True if associated with a private browsing window.
+ const bool mIsPrivateBrowsing;
+
+ // True if the client is suspended. Accessed on the owner thread only.
+ bool mClientSuspended = false;
+
+ MediaChannelStatistics mDownloadStatistics;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/MediaChannelStatistics.h b/dom/media/MediaChannelStatistics.h
new file mode 100644
index 0000000000..9a3636cef2
--- /dev/null
+++ b/dom/media/MediaChannelStatistics.h
@@ -0,0 +1,89 @@
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(MediaChannelStatistics_h_)
+# define MediaChannelStatistics_h_
+
+# include "mozilla/TimeStamp.h"
+
+namespace mozilla {
+
+// Number of bytes we have accumulated before we assume the connection download
+// rate can be reliably calculated. 57 Segments at IW=3 allows slow start to
+// reach a CWND of 30 (See bug 831998)
+static const int64_t RELIABLE_DATA_THRESHOLD = 57 * 1460;
+
+/**
+ * This class is useful for estimating rates of data passing through
+ * some channel. The idea is that activity on the channel "starts"
+ * and "stops" over time. At certain times data passes through the
+ * channel (usually while the channel is active; data passing through
+ * an inactive channel is ignored). The GetRate() function computes
+ * an estimate of the "current rate" of the channel, which is some
+ * kind of average of the data passing through over the time the
+ * channel is active.
+ *
+ * All methods take "now" as a parameter so the user of this class can
+ * control the timeline used.
+ */
+class MediaChannelStatistics {
+ public:
+ MediaChannelStatistics() = default;
+ MediaChannelStatistics(const MediaChannelStatistics&) = default;
+ MediaChannelStatistics& operator=(const MediaChannelStatistics&) = default;
+
+ void Reset() {
+ mLastStartTime = TimeStamp();
+ mAccumulatedTime = TimeDuration(0);
+ mAccumulatedBytes = 0;
+ mIsStarted = false;
+ }
+ void Start() {
+ if (mIsStarted) return;
+ mLastStartTime = TimeStamp::Now();
+ mIsStarted = true;
+ }
+ void Stop() {
+ if (!mIsStarted) return;
+ mAccumulatedTime += TimeStamp::Now() - mLastStartTime;
+ mIsStarted = false;
+ }
+ void AddBytes(int64_t aBytes) {
+ if (!mIsStarted) {
+ // ignore this data, it may be related to seeking or some other
+ // operation we don't care about
+ return;
+ }
+ mAccumulatedBytes += aBytes;
+ }
+ double GetRateAtLastStop(bool* aReliable) const {
+ double seconds = mAccumulatedTime.ToSeconds();
+ *aReliable =
+ (seconds >= 1.0) || (mAccumulatedBytes >= RELIABLE_DATA_THRESHOLD);
+ if (seconds <= 0.0) return 0.0;
+ return static_cast<double>(mAccumulatedBytes) / seconds;
+ }
+ double GetRate(bool* aReliable) const {
+ TimeDuration time = mAccumulatedTime;
+ if (mIsStarted) {
+ time += TimeStamp::Now() - mLastStartTime;
+ }
+ double seconds = time.ToSeconds();
+ *aReliable =
+ (seconds >= 3.0) || (mAccumulatedBytes >= RELIABLE_DATA_THRESHOLD);
+ if (seconds <= 0.0) return 0.0;
+ return static_cast<double>(mAccumulatedBytes) / seconds;
+ }
+
+ private:
+ int64_t mAccumulatedBytes = 0;
+ TimeDuration mAccumulatedTime;
+ TimeStamp mLastStartTime;
+ bool mIsStarted = false;
+};
+
+} // namespace mozilla
+
+#endif // MediaChannelStatistics_h_
diff --git a/dom/media/MediaContainerType.cpp b/dom/media/MediaContainerType.cpp
new file mode 100644
index 0000000000..c657110e02
--- /dev/null
+++ b/dom/media/MediaContainerType.cpp
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaContainerType.h"
+
+namespace mozilla {
+
+size_t MediaContainerType::SizeOfExcludingThis(
+ MallocSizeOf aMallocSizeOf) const {
+ return mExtendedMIMEType.SizeOfExcludingThis(aMallocSizeOf);
+}
+
+Maybe<MediaContainerType> MakeMediaContainerType(const nsAString& aType) {
+ Maybe<MediaExtendedMIMEType> mime = MakeMediaExtendedMIMEType(aType);
+ if (mime) {
+ return Some(MediaContainerType(std::move(*mime)));
+ }
+ return Nothing();
+}
+
+Maybe<MediaContainerType> MakeMediaContainerType(const nsACString& aType) {
+ return MakeMediaContainerType(NS_ConvertUTF8toUTF16(aType));
+}
+
+Maybe<MediaContainerType> MakeMediaContainerType(const char* aType) {
+ if (!aType) {
+ return Nothing();
+ }
+ return MakeMediaContainerType(nsDependentCString(aType));
+}
+
+} // namespace mozilla
diff --git a/dom/media/MediaContainerType.h b/dom/media/MediaContainerType.h
new file mode 100644
index 0000000000..3b4d50f0be
--- /dev/null
+++ b/dom/media/MediaContainerType.h
@@ -0,0 +1,51 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MediaContainerType_h_
+#define MediaContainerType_h_
+
+#include "MediaMIMETypes.h"
+#include "mozilla/Maybe.h"
+#include "nsString.h"
+
+namespace mozilla {
+
+// Class containing media type information for containers.
+class MediaContainerType {
+ public:
+ explicit MediaContainerType(const MediaMIMEType& aType)
+ : mExtendedMIMEType(aType) {}
+ explicit MediaContainerType(MediaMIMEType&& aType)
+ : mExtendedMIMEType(std::move(aType)) {}
+ explicit MediaContainerType(const MediaExtendedMIMEType& aType)
+ : mExtendedMIMEType(aType) {}
+ explicit MediaContainerType(MediaExtendedMIMEType&& aType)
+ : mExtendedMIMEType(std::move(aType)) {}
+
+ const MediaMIMEType& Type() const { return mExtendedMIMEType.Type(); }
+ const MediaExtendedMIMEType& ExtendedType() const {
+ return mExtendedMIMEType;
+ }
+
+ // Original string. Note that "type/subtype" may not be lowercase,
+ // use Type().AsString() instead to get the normalized "type/subtype".
+ const nsCString& OriginalString() const {
+ return mExtendedMIMEType.OriginalString();
+ }
+
+ size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ private:
+ MediaExtendedMIMEType mExtendedMIMEType;
+};
+
+Maybe<MediaContainerType> MakeMediaContainerType(const nsAString& aType);
+Maybe<MediaContainerType> MakeMediaContainerType(const nsACString& aType);
+Maybe<MediaContainerType> MakeMediaContainerType(const char* aType);
+
+} // namespace mozilla
+
+#endif // MediaContainerType_h_
diff --git a/dom/media/MediaData.cpp b/dom/media/MediaData.cpp
new file mode 100644
index 0000000000..d1cfca35a9
--- /dev/null
+++ b/dom/media/MediaData.cpp
@@ -0,0 +1,599 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaData.h"
+
+#include "ImageContainer.h"
+#include "MediaInfo.h"
+#include "PerformanceRecorder.h"
+#include "VideoUtils.h"
+#include "YCbCrUtils.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/layers/ImageBridgeChild.h"
+#include "mozilla/layers/KnowsCompositor.h"
+#include "mozilla/layers/SharedRGBImage.h"
+
+#include <stdint.h>
+
+#ifdef XP_WIN
+# include "mozilla/WindowsVersion.h"
+# include "mozilla/gfx/DeviceManagerDx.h"
+# include "mozilla/layers/D3D11ShareHandleImage.h"
+# include "mozilla/layers/D3D11YCbCrImage.h"
+#elif XP_MACOSX
+# include "MacIOSurfaceImage.h"
+# include "mozilla/gfx/gfxVars.h"
+#endif
+
+namespace mozilla {
+
+using namespace mozilla::gfx;
+using layers::PlanarYCbCrData;
+using layers::PlanarYCbCrImage;
+using media::TimeUnit;
+
+const char* AudioData::sTypeName = "audio";
+const char* VideoData::sTypeName = "video";
+
+AudioData::AudioData(int64_t aOffset, const media::TimeUnit& aTime,
+ AlignedAudioBuffer&& aData, uint32_t aChannels,
+ uint32_t aRate, uint32_t aChannelMap)
+ // Passing TimeUnit::Zero() here because we can't pass the result of an
+ // arithmetic operation to the CheckedInt ctor. We set the duration in the
+ // ctor body below.
+ : MediaData(sType, aOffset, aTime, TimeUnit::Zero()),
+ mChannels(aChannels),
+ mChannelMap(aChannelMap),
+ mRate(aRate),
+ mOriginalTime(aTime),
+ mAudioData(std::move(aData)),
+ mFrames(mAudioData.Length() / aChannels) {
+ MOZ_RELEASE_ASSERT(aChannels != 0,
+ "Can't create an AudioData with 0 channels.");
+ MOZ_RELEASE_ASSERT(aRate != 0,
+ "Can't create an AudioData with a sample-rate of 0.");
+ mDuration = TimeUnit(mFrames, aRate);
+}
+
+Span<AudioDataValue> AudioData::Data() const {
+ return Span{GetAdjustedData(), mFrames * mChannels};
+}
+
+void AudioData::SetOriginalStartTime(const media::TimeUnit& aStartTime) {
+ MOZ_ASSERT(mTime == mOriginalTime,
+ "Do not call this if data has been trimmed!");
+ mTime = aStartTime;
+ mOriginalTime = aStartTime;
+}
+
+bool AudioData::AdjustForStartTime(const media::TimeUnit& aStartTime) {
+ mOriginalTime -= aStartTime;
+ mTime -= aStartTime;
+ if (mTrimWindow) {
+ *mTrimWindow -= aStartTime;
+ }
+ if (mTime.IsNegative()) {
+ NS_WARNING("Negative audio start time after time-adjustment!");
+ }
+ return mTime.IsValid() && mOriginalTime.IsValid();
+}
+
+bool AudioData::SetTrimWindow(const media::TimeInterval& aTrim) {
+ MOZ_DIAGNOSTIC_ASSERT(aTrim.mStart.IsValid() && aTrim.mEnd.IsValid(),
+ "An overflow occurred on the provided TimeInterval");
+ if (!mAudioData) {
+ // MoveableData got called. Can no longer work on it.
+ return false;
+ }
+ if (aTrim.mStart < mOriginalTime || aTrim.mEnd > GetEndTime()) {
+ return false;
+ }
+
+ auto trimBefore = aTrim.mStart - mOriginalTime;
+ auto trimAfter = aTrim.mEnd - mOriginalTime;
+ if (!trimBefore.IsValid() || !trimAfter.IsValid()) {
+ // Overflow.
+ return false;
+ }
+ if (!mTrimWindow && trimBefore.IsZero() && trimAfter == mDuration) {
+ // Nothing to change, abort early to prevent rounding errors.
+ return true;
+ }
+
+ size_t frameOffset = trimBefore.ToTicksAtRate(mRate);
+ mTrimWindow = Some(aTrim);
+ mDataOffset = frameOffset * mChannels;
+ MOZ_DIAGNOSTIC_ASSERT(mDataOffset <= mAudioData.Length(),
+ "Data offset outside original buffer");
+ mFrames = (trimAfter - trimBefore).ToTicksAtRate(mRate);
+ MOZ_DIAGNOSTIC_ASSERT(mFrames <= mAudioData.Length() / mChannels,
+ "More frames than found in container");
+ mTime = mOriginalTime + trimBefore;
+ mDuration = TimeUnit(mFrames, mRate);
+
+ return true;
+}
+
+AudioDataValue* AudioData::GetAdjustedData() const {
+ if (!mAudioData) {
+ return nullptr;
+ }
+ return mAudioData.Data() + mDataOffset;
+}
+
+void AudioData::EnsureAudioBuffer() {
+ if (mAudioBuffer || !mAudioData) {
+ return;
+ }
+ const AudioDataValue* srcData = GetAdjustedData();
+ CheckedInt<size_t> bufferSize(sizeof(AudioDataValue));
+ bufferSize *= mFrames;
+ bufferSize *= mChannels;
+ mAudioBuffer = SharedBuffer::Create(bufferSize);
+
+ AudioDataValue* destData = static_cast<AudioDataValue*>(mAudioBuffer->Data());
+ for (uint32_t i = 0; i < mFrames; ++i) {
+ for (uint32_t j = 0; j < mChannels; ++j) {
+ destData[j * mFrames + i] = srcData[i * mChannels + j];
+ }
+ }
+}
+
+size_t AudioData::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t size =
+ aMallocSizeOf(this) + mAudioData.SizeOfExcludingThis(aMallocSizeOf);
+ if (mAudioBuffer) {
+ size += mAudioBuffer->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ return size;
+}
+
+AlignedAudioBuffer AudioData::MoveableData() {
+ // Trim buffer according to trimming mask.
+ mAudioData.PopFront(mDataOffset);
+ mAudioData.SetLength(mFrames * mChannels);
+ mDataOffset = 0;
+ mFrames = 0;
+ mTrimWindow.reset();
+ return std::move(mAudioData);
+}
+
+static bool ValidatePlane(const VideoData::YCbCrBuffer::Plane& aPlane) {
+ return aPlane.mWidth <= PlanarYCbCrImage::MAX_DIMENSION &&
+ aPlane.mHeight <= PlanarYCbCrImage::MAX_DIMENSION &&
+ aPlane.mWidth * aPlane.mHeight < MAX_VIDEO_WIDTH * MAX_VIDEO_HEIGHT &&
+ aPlane.mStride > 0 && aPlane.mWidth <= aPlane.mStride;
+}
+
+static bool ValidateBufferAndPicture(const VideoData::YCbCrBuffer& aBuffer,
+ const IntRect& aPicture) {
+ // The following situation should never happen unless there is a bug
+ // in the decoder
+ if (aBuffer.mPlanes[1].mWidth != aBuffer.mPlanes[2].mWidth ||
+ aBuffer.mPlanes[1].mHeight != aBuffer.mPlanes[2].mHeight) {
+ NS_ERROR("C planes with different sizes");
+ return false;
+ }
+
+ // The following situations could be triggered by invalid input
+ if (aPicture.width <= 0 || aPicture.height <= 0) {
+ NS_WARNING("Empty picture rect");
+ return false;
+ }
+ if (!ValidatePlane(aBuffer.mPlanes[0]) ||
+ !ValidatePlane(aBuffer.mPlanes[1]) ||
+ !ValidatePlane(aBuffer.mPlanes[2])) {
+ NS_WARNING("Invalid plane size");
+ return false;
+ }
+
+ // Ensure the picture size specified in the headers can be extracted out of
+ // the frame we've been supplied without indexing out of bounds.
+ CheckedUint32 xLimit = aPicture.x + CheckedUint32(aPicture.width);
+ CheckedUint32 yLimit = aPicture.y + CheckedUint32(aPicture.height);
+ if (!xLimit.isValid() || xLimit.value() > aBuffer.mPlanes[0].mStride ||
+ !yLimit.isValid() || yLimit.value() > aBuffer.mPlanes[0].mHeight) {
+ // The specified picture dimensions can't be contained inside the video
+ // frame, we'll stomp memory if we try to copy it. Fail.
+ NS_WARNING("Overflowing picture rect");
+ return false;
+ }
+ return true;
+}
+
+VideoData::VideoData(int64_t aOffset, const TimeUnit& aTime,
+ const TimeUnit& aDuration, bool aKeyframe,
+ const TimeUnit& aTimecode, IntSize aDisplay,
+ layers::ImageContainer::FrameID aFrameID)
+ : MediaData(Type::VIDEO_DATA, aOffset, aTime, aDuration),
+ mDisplay(aDisplay),
+ mFrameID(aFrameID),
+ mSentToCompositor(false),
+ mNextKeyFrameTime(TimeUnit::Invalid()) {
+ MOZ_ASSERT(!mDuration.IsNegative(), "Frame must have non-negative duration.");
+ mKeyframe = aKeyframe;
+ mTimecode = aTimecode;
+}
+
+VideoData::~VideoData() = default;
+
+size_t VideoData::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t size = aMallocSizeOf(this);
+
+ // Currently only PLANAR_YCBCR has a well defined function for determining
+ // it's size, so reporting is limited to that type.
+ if (mImage && mImage->GetFormat() == ImageFormat::PLANAR_YCBCR) {
+ const mozilla::layers::PlanarYCbCrImage* img =
+ static_cast<const mozilla::layers::PlanarYCbCrImage*>(mImage.get());
+ size += img->SizeOfIncludingThis(aMallocSizeOf);
+ }
+
+ return size;
+}
+
+ColorDepth VideoData::GetColorDepth() const {
+ if (!mImage) {
+ return ColorDepth::COLOR_8;
+ }
+
+ return mImage->GetColorDepth();
+}
+
+void VideoData::UpdateDuration(const TimeUnit& aDuration) {
+ MOZ_ASSERT(!aDuration.IsNegative());
+ mDuration = aDuration;
+}
+
+void VideoData::UpdateTimestamp(const TimeUnit& aTimestamp) {
+ MOZ_ASSERT(!aTimestamp.IsNegative());
+
+ auto updatedDuration = GetEndTime() - aTimestamp;
+ MOZ_ASSERT(!updatedDuration.IsNegative());
+
+ mTime = aTimestamp;
+ mDuration = updatedDuration;
+}
+
+bool VideoData::AdjustForStartTime(const media::TimeUnit& aStartTime) {
+ mTime -= aStartTime;
+ if (mTime.IsNegative()) {
+ NS_WARNING("Negative video start time after time-adjustment!");
+ }
+ return mTime.IsValid();
+}
+
+PlanarYCbCrData ConstructPlanarYCbCrData(const VideoInfo& aInfo,
+ const VideoData::YCbCrBuffer& aBuffer,
+ const IntRect& aPicture) {
+ const VideoData::YCbCrBuffer::Plane& Y = aBuffer.mPlanes[0];
+ const VideoData::YCbCrBuffer::Plane& Cb = aBuffer.mPlanes[1];
+ const VideoData::YCbCrBuffer::Plane& Cr = aBuffer.mPlanes[2];
+
+ PlanarYCbCrData data;
+ data.mYChannel = Y.mData;
+ data.mYStride = AssertedCast<int32_t>(Y.mStride);
+ data.mYSkip = AssertedCast<int32_t>(Y.mSkip);
+ data.mCbChannel = Cb.mData;
+ data.mCrChannel = Cr.mData;
+ data.mCbCrStride = AssertedCast<int32_t>(Cb.mStride);
+ data.mCbSkip = AssertedCast<int32_t>(Cb.mSkip);
+ data.mCrSkip = AssertedCast<int32_t>(Cr.mSkip);
+ data.mPictureRect = aPicture;
+ data.mStereoMode = aInfo.mStereoMode;
+ data.mYUVColorSpace = aBuffer.mYUVColorSpace;
+ data.mColorPrimaries = aBuffer.mColorPrimaries;
+ data.mColorDepth = aBuffer.mColorDepth;
+ if (aInfo.mTransferFunction) {
+ data.mTransferFunction = *aInfo.mTransferFunction;
+ }
+ data.mColorRange = aBuffer.mColorRange;
+ data.mChromaSubsampling = aBuffer.mChromaSubsampling;
+ return data;
+}
+
+/* static */
+bool VideoData::SetVideoDataToImage(PlanarYCbCrImage* aVideoImage,
+ const VideoInfo& aInfo,
+ const YCbCrBuffer& aBuffer,
+ const IntRect& aPicture, bool aCopyData) {
+ if (!aVideoImage) {
+ return false;
+ }
+
+ PlanarYCbCrData data = ConstructPlanarYCbCrData(aInfo, aBuffer, aPicture);
+
+ if (aCopyData) {
+ return aVideoImage->CopyData(data);
+ }
+ return aVideoImage->AdoptData(data);
+}
+
+/* static */
+bool VideoData::UseUseNV12ForSoftwareDecodedVideoIfPossible(
+ layers::KnowsCompositor* aAllocator) {
+#if XP_WIN
+ if (!aAllocator) {
+ return false;
+ }
+
+ if (StaticPrefs::gfx_video_convert_i420_to_nv12_force_enabled_AtStartup() ||
+ (gfx::DeviceManagerDx::Get()->CanUseNV12() &&
+ gfx::gfxVars::UseWebRenderDCompSwVideoOverlayWin() &&
+ aAllocator->GetCompositorUseDComp())) {
+ return true;
+ }
+#endif
+ return false;
+}
+
+/* static */
+already_AddRefed<VideoData> VideoData::CreateAndCopyData(
+ const VideoInfo& aInfo, ImageContainer* aContainer, int64_t aOffset,
+ const TimeUnit& aTime, const TimeUnit& aDuration,
+ const YCbCrBuffer& aBuffer, bool aKeyframe, const TimeUnit& aTimecode,
+ const IntRect& aPicture, layers::KnowsCompositor* aAllocator) {
+ if (!aContainer) {
+ // Create a dummy VideoData with no image. This gives us something to
+ // send to media streams if necessary.
+ RefPtr<VideoData> v(new VideoData(aOffset, aTime, aDuration, aKeyframe,
+ aTimecode, aInfo.mDisplay, 0));
+ return v.forget();
+ }
+
+ if (!ValidateBufferAndPicture(aBuffer, aPicture)) {
+ return nullptr;
+ }
+
+ PerformanceRecorder<PlaybackStage> perfRecorder(MediaStage::CopyDecodedVideo,
+ aInfo.mImage.height);
+ RefPtr<VideoData> v(new VideoData(aOffset, aTime, aDuration, aKeyframe,
+ aTimecode, aInfo.mDisplay, 0));
+
+ // Currently our decoder only knows how to output to ImageFormat::PLANAR_YCBCR
+ // format.
+#if XP_WIN
+ // Copy to NV12 format D3D11ShareHandleImage when video overlay could be used
+ // with the video frame
+ if (UseUseNV12ForSoftwareDecodedVideoIfPossible(aAllocator)) {
+ PlanarYCbCrData data = ConstructPlanarYCbCrData(aInfo, aBuffer, aPicture);
+ RefPtr<layers::D3D11ShareHandleImage> d3d11Image =
+ layers::D3D11ShareHandleImage::MaybeCreateNV12ImageAndSetData(
+ aAllocator, aContainer, data);
+ if (d3d11Image) {
+ v->mImage = d3d11Image;
+ perfRecorder.Record();
+ return v.forget();
+ }
+ }
+
+ // We disable this code path on Windows version earlier of Windows 8 due to
+ // intermittent crashes with old drivers. See bug 1405110.
+ // D3D11YCbCrImage can only handle YCbCr images using 3 non-interleaved planes
+ // non-zero mSkip value indicates that one of the plane would be interleaved.
+ if (IsWin8OrLater() && !XRE_IsParentProcess() && aAllocator &&
+ aAllocator->SupportsD3D11() && aBuffer.mPlanes[0].mSkip == 0 &&
+ aBuffer.mPlanes[1].mSkip == 0 && aBuffer.mPlanes[2].mSkip == 0) {
+ RefPtr<layers::D3D11YCbCrImage> d3d11Image = new layers::D3D11YCbCrImage();
+ PlanarYCbCrData data = ConstructPlanarYCbCrData(aInfo, aBuffer, aPicture);
+ if (d3d11Image->SetData(layers::ImageBridgeChild::GetSingleton()
+ ? layers::ImageBridgeChild::GetSingleton().get()
+ : aAllocator,
+ aContainer, data)) {
+ v->mImage = d3d11Image;
+ perfRecorder.Record();
+ return v.forget();
+ }
+ }
+#elif XP_MACOSX
+ if (aAllocator && aAllocator->GetWebRenderCompositorType() !=
+ layers::WebRenderCompositor::SOFTWARE) {
+ RefPtr<layers::MacIOSurfaceImage> ioImage =
+ new layers::MacIOSurfaceImage(nullptr);
+ PlanarYCbCrData data = ConstructPlanarYCbCrData(aInfo, aBuffer, aPicture);
+ if (ioImage->SetData(aContainer, data)) {
+ v->mImage = ioImage;
+ perfRecorder.Record();
+ return v.forget();
+ }
+ }
+#endif
+ if (!v->mImage) {
+ v->mImage = aContainer->CreatePlanarYCbCrImage();
+ }
+
+ if (!v->mImage) {
+ return nullptr;
+ }
+ NS_ASSERTION(v->mImage->GetFormat() == ImageFormat::PLANAR_YCBCR,
+ "Wrong format?");
+ PlanarYCbCrImage* videoImage = v->mImage->AsPlanarYCbCrImage();
+ MOZ_ASSERT(videoImage);
+
+ if (!VideoData::SetVideoDataToImage(videoImage, aInfo, aBuffer, aPicture,
+ true /* aCopyData */)) {
+ return nullptr;
+ }
+
+ perfRecorder.Record();
+ return v.forget();
+}
+
+/* static */
+already_AddRefed<VideoData> VideoData::CreateAndCopyData(
+ const VideoInfo& aInfo, ImageContainer* aContainer, int64_t aOffset,
+ const TimeUnit& aTime, const TimeUnit& aDuration,
+ const YCbCrBuffer& aBuffer, const YCbCrBuffer::Plane& aAlphaPlane,
+ bool aKeyframe, const TimeUnit& aTimecode, const IntRect& aPicture) {
+ if (!aContainer) {
+ // Create a dummy VideoData with no image. This gives us something to
+ // send to media streams if necessary.
+ RefPtr<VideoData> v(new VideoData(aOffset, aTime, aDuration, aKeyframe,
+ aTimecode, aInfo.mDisplay, 0));
+ return v.forget();
+ }
+
+ if (!ValidateBufferAndPicture(aBuffer, aPicture)) {
+ return nullptr;
+ }
+
+ RefPtr<VideoData> v(new VideoData(aOffset, aTime, aDuration, aKeyframe,
+ aTimecode, aInfo.mDisplay, 0));
+
+ // Convert from YUVA to BGRA format on the software side.
+ RefPtr<layers::SharedRGBImage> videoImage =
+ aContainer->CreateSharedRGBImage();
+ v->mImage = videoImage;
+
+ if (!v->mImage) {
+ return nullptr;
+ }
+ if (!videoImage->Allocate(
+ IntSize(aBuffer.mPlanes[0].mWidth, aBuffer.mPlanes[0].mHeight),
+ SurfaceFormat::B8G8R8A8)) {
+ return nullptr;
+ }
+
+ RefPtr<layers::TextureClient> texture =
+ videoImage->GetTextureClient(/* aKnowsCompositor */ nullptr);
+ if (!texture) {
+ NS_WARNING("Failed to allocate TextureClient");
+ return nullptr;
+ }
+
+ layers::TextureClientAutoLock autoLock(texture,
+ layers::OpenMode::OPEN_WRITE_ONLY);
+ if (!autoLock.Succeeded()) {
+ NS_WARNING("Failed to lock TextureClient");
+ return nullptr;
+ }
+
+ layers::MappedTextureData buffer;
+ if (!texture->BorrowMappedData(buffer)) {
+ NS_WARNING("Failed to borrow mapped data");
+ return nullptr;
+ }
+
+ // The naming convention for libyuv and associated utils is word-order.
+ // The naming convention in the gfx stack is byte-order.
+ ConvertI420AlphaToARGB(aBuffer.mPlanes[0].mData, aBuffer.mPlanes[1].mData,
+ aBuffer.mPlanes[2].mData, aAlphaPlane.mData,
+ AssertedCast<int>(aBuffer.mPlanes[0].mStride),
+ AssertedCast<int>(aBuffer.mPlanes[1].mStride),
+ buffer.data, buffer.stride, buffer.size.width,
+ buffer.size.height);
+
+ return v.forget();
+}
+
+/* static */
+already_AddRefed<VideoData> VideoData::CreateFromImage(
+ const IntSize& aDisplay, int64_t aOffset, const TimeUnit& aTime,
+ const TimeUnit& aDuration, const RefPtr<Image>& aImage, bool aKeyframe,
+ const TimeUnit& aTimecode) {
+ RefPtr<VideoData> v(new VideoData(aOffset, aTime, aDuration, aKeyframe,
+ aTimecode, aDisplay, 0));
+ v->mImage = aImage;
+ return v.forget();
+}
+
+MediaRawData::MediaRawData()
+ : MediaData(Type::RAW_DATA), mCrypto(mCryptoInternal) {}
+
+MediaRawData::MediaRawData(const uint8_t* aData, size_t aSize)
+ : MediaData(Type::RAW_DATA),
+ mCrypto(mCryptoInternal),
+ mBuffer(aData, aSize) {}
+
+MediaRawData::MediaRawData(const uint8_t* aData, size_t aSize,
+ const uint8_t* aAlphaData, size_t aAlphaSize)
+ : MediaData(Type::RAW_DATA),
+ mCrypto(mCryptoInternal),
+ mBuffer(aData, aSize),
+ mAlphaBuffer(aAlphaData, aAlphaSize) {}
+
+MediaRawData::MediaRawData(AlignedByteBuffer&& aData)
+ : MediaData(Type::RAW_DATA),
+ mCrypto(mCryptoInternal),
+ mBuffer(std::move(aData)) {}
+
+MediaRawData::MediaRawData(AlignedByteBuffer&& aData,
+ AlignedByteBuffer&& aAlphaData)
+ : MediaData(Type::RAW_DATA),
+ mCrypto(mCryptoInternal),
+ mBuffer(std::move(aData)),
+ mAlphaBuffer(std::move(aAlphaData)) {}
+
+already_AddRefed<MediaRawData> MediaRawData::Clone() const {
+ int32_t sampleHeight = 0;
+ if (mTrackInfo && mTrackInfo->GetAsVideoInfo()) {
+ sampleHeight = mTrackInfo->GetAsVideoInfo()->mImage.height;
+ }
+ PerformanceRecorder<PlaybackStage> perfRecorder(MediaStage::CopyDemuxedData,
+ sampleHeight);
+ RefPtr<MediaRawData> s = new MediaRawData;
+ s->mTimecode = mTimecode;
+ s->mTime = mTime;
+ s->mDuration = mDuration;
+ s->mOffset = mOffset;
+ s->mKeyframe = mKeyframe;
+ s->mExtraData = mExtraData;
+ s->mCryptoInternal = mCryptoInternal;
+ s->mTrackInfo = mTrackInfo;
+ s->mEOS = mEOS;
+ s->mOriginalPresentationWindow = mOriginalPresentationWindow;
+ if (!s->mBuffer.Append(mBuffer.Data(), mBuffer.Length())) {
+ return nullptr;
+ }
+ if (!s->mAlphaBuffer.Append(mAlphaBuffer.Data(), mAlphaBuffer.Length())) {
+ return nullptr;
+ }
+ perfRecorder.Record();
+ return s.forget();
+}
+
+MediaRawData::~MediaRawData() = default;
+
+size_t MediaRawData::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t size = aMallocSizeOf(this);
+ size += mBuffer.SizeOfExcludingThis(aMallocSizeOf);
+ return size;
+}
+
+UniquePtr<MediaRawDataWriter> MediaRawData::CreateWriter() {
+ UniquePtr<MediaRawDataWriter> p(new MediaRawDataWriter(this));
+ return p;
+}
+
+MediaRawDataWriter::MediaRawDataWriter(MediaRawData* aMediaRawData)
+ : mCrypto(aMediaRawData->mCryptoInternal), mTarget(aMediaRawData) {}
+
+bool MediaRawDataWriter::SetSize(size_t aSize) {
+ return mTarget->mBuffer.SetLength(aSize);
+}
+
+bool MediaRawDataWriter::Prepend(const uint8_t* aData, size_t aSize) {
+ return mTarget->mBuffer.Prepend(aData, aSize);
+}
+
+bool MediaRawDataWriter::Append(const uint8_t* aData, size_t aSize) {
+ return mTarget->mBuffer.Append(aData, aSize);
+}
+
+bool MediaRawDataWriter::Replace(const uint8_t* aData, size_t aSize) {
+ return mTarget->mBuffer.Replace(aData, aSize);
+}
+
+void MediaRawDataWriter::Clear() { mTarget->mBuffer.Clear(); }
+
+uint8_t* MediaRawDataWriter::Data() { return mTarget->mBuffer.Data(); }
+
+size_t MediaRawDataWriter::Size() { return mTarget->Size(); }
+
+void MediaRawDataWriter::PopFront(size_t aSize) {
+ mTarget->mBuffer.PopFront(aSize);
+}
+
+} // namespace mozilla
diff --git a/dom/media/MediaData.h b/dom/media/MediaData.h
new file mode 100644
index 0000000000..4040f368ba
--- /dev/null
+++ b/dom/media/MediaData.h
@@ -0,0 +1,712 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#if !defined(MediaData_h)
+# define MediaData_h
+
+# include "AudioConfig.h"
+# include "AudioSampleFormat.h"
+# include "ImageTypes.h"
+# include "SharedBuffer.h"
+# include "TimeUnits.h"
+# include "mozilla/CheckedInt.h"
+# include "mozilla/Maybe.h"
+# include "mozilla/PodOperations.h"
+# include "mozilla/RefPtr.h"
+# include "mozilla/Span.h"
+# include "mozilla/UniquePtr.h"
+# include "mozilla/UniquePtrExtensions.h"
+# include "mozilla/gfx/Rect.h"
+# include "nsString.h"
+# include "nsTArray.h"
+
+namespace mozilla {
+
+namespace layers {
+class Image;
+class ImageContainer;
+class KnowsCompositor;
+} // namespace layers
+
+class MediaByteBuffer;
+class TrackInfoSharedPtr;
+
+// AlignedBuffer:
+// Memory allocations are fallibles. Methods return a boolean indicating if
+// memory allocations were successful. Return values should always be checked.
+// AlignedBuffer::mData will be nullptr if no memory has been allocated or if
+// an error occurred during construction.
+// Existing data is only ever modified if new memory allocation has succeeded
+// and preserved if not.
+//
+// The memory referenced by mData will always be Alignment bytes aligned and the
+// underlying buffer will always have a size such that Alignment bytes blocks
+// can be used to read the content, regardless of the mSize value. Buffer is
+// zeroed on creation, elements are not individually constructed.
+// An Alignment value of 0 means that the data isn't aligned.
+//
+// Type must be trivially copyable.
+//
+// AlignedBuffer can typically be used in place of UniquePtr<Type[]> however
+// care must be taken as all memory allocations are fallible.
+// Example:
+// auto buffer = MakeUniqueFallible<float[]>(samples)
+// becomes: AlignedFloatBuffer buffer(samples)
+//
+// auto buffer = MakeUnique<float[]>(samples)
+// becomes:
+// AlignedFloatBuffer buffer(samples);
+// if (!buffer) { return NS_ERROR_OUT_OF_MEMORY; }
+
+template <typename Type, int Alignment = 32>
+class AlignedBuffer {
+ public:
+ AlignedBuffer()
+ : mData(nullptr), mLength(0), mBuffer(nullptr), mCapacity(0) {}
+
+ explicit AlignedBuffer(size_t aLength)
+ : mData(nullptr), mLength(0), mBuffer(nullptr), mCapacity(0) {
+ if (EnsureCapacity(aLength)) {
+ mLength = aLength;
+ }
+ }
+
+ AlignedBuffer(const Type* aData, size_t aLength) : AlignedBuffer(aLength) {
+ if (!mData) {
+ return;
+ }
+ PodCopy(mData, aData, aLength);
+ }
+
+ AlignedBuffer(const AlignedBuffer& aOther)
+ : AlignedBuffer(aOther.Data(), aOther.Length()) {}
+
+ AlignedBuffer(AlignedBuffer&& aOther)
+ : mData(aOther.mData),
+ mLength(aOther.mLength),
+ mBuffer(std::move(aOther.mBuffer)),
+ mCapacity(aOther.mCapacity) {
+ aOther.mData = nullptr;
+ aOther.mLength = 0;
+ aOther.mCapacity = 0;
+ }
+
+ AlignedBuffer& operator=(AlignedBuffer&& aOther) {
+ this->~AlignedBuffer();
+ new (this) AlignedBuffer(std::move(aOther));
+ return *this;
+ }
+
+ Type* Data() const { return mData; }
+ size_t Length() const { return mLength; }
+ size_t Size() const { return mLength * sizeof(Type); }
+ Type& operator[](size_t aIndex) {
+ MOZ_ASSERT(aIndex < mLength);
+ return mData[aIndex];
+ }
+ const Type& operator[](size_t aIndex) const {
+ MOZ_ASSERT(aIndex < mLength);
+ return mData[aIndex];
+ }
+ // Set length of buffer, allocating memory as required.
+ // If length is increased, new buffer area is filled with 0.
+ bool SetLength(size_t aLength) {
+ if (aLength > mLength && !EnsureCapacity(aLength)) {
+ return false;
+ }
+ mLength = aLength;
+ return true;
+ }
+ // Add aData at the beginning of buffer.
+ bool Prepend(const Type* aData, size_t aLength) {
+ if (!EnsureCapacity(aLength + mLength)) {
+ return false;
+ }
+
+ // Shift the data to the right by aLength to leave room for the new data.
+ PodMove(mData + aLength, mData, mLength);
+ PodCopy(mData, aData, aLength);
+
+ mLength += aLength;
+ return true;
+ }
+ // Add aData at the end of buffer.
+ bool Append(const Type* aData, size_t aLength) {
+ if (!EnsureCapacity(aLength + mLength)) {
+ return false;
+ }
+
+ PodCopy(mData + mLength, aData, aLength);
+
+ mLength += aLength;
+ return true;
+ }
+ // Replace current content with aData.
+ bool Replace(const Type* aData, size_t aLength) {
+ // If aLength is smaller than our current length, we leave the buffer as is,
+ // only adjusting the reported length.
+ if (!EnsureCapacity(aLength)) {
+ return false;
+ }
+
+ PodCopy(mData, aData, aLength);
+ mLength = aLength;
+ return true;
+ }
+ // Clear the memory buffer. Will set target mData and mLength to 0.
+ void Clear() {
+ mLength = 0;
+ mData = nullptr;
+ }
+
+ // Methods for reporting memory.
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t size = aMallocSizeOf(this);
+ size += aMallocSizeOf(mBuffer.get());
+ return size;
+ }
+ // AlignedBuffer is typically allocated on the stack. As such, you likely
+ // want to use SizeOfExcludingThis
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(mBuffer.get());
+ }
+ size_t ComputedSizeOfExcludingThis() const { return mCapacity; }
+
+ // For backward compatibility with UniquePtr<Type[]>
+ Type* get() const { return mData; }
+ explicit operator bool() const { return mData != nullptr; }
+
+ // Size in bytes of extra space allocated for padding.
+ static size_t AlignmentPaddingSize() { return AlignmentOffset() * 2; }
+
+ void PopFront(size_t aCount) {
+ MOZ_DIAGNOSTIC_ASSERT(mLength >= aCount, "Popping too many elements.");
+ PodMove(mData, mData + aCount, mLength - aCount);
+ mLength -= aCount;
+ }
+
+ void PopBack(size_t aCount) {
+ MOZ_DIAGNOSTIC_ASSERT(mLength >= aCount, "Popping too many elements.");
+ mLength -= aCount;
+ }
+
+ private:
+ static size_t AlignmentOffset() { return Alignment ? Alignment - 1 : 0; }
+
+ // Ensure that the backend buffer can hold aLength data. Will update mData.
+ // Will enforce that the start of allocated data is always Alignment bytes
+ // aligned and that it has sufficient end padding to allow for Alignment bytes
+ // block read as required by some data decoders.
+ // Returns false if memory couldn't be allocated.
+ bool EnsureCapacity(size_t aLength) {
+ if (!aLength) {
+ // No need to allocate a buffer yet.
+ return true;
+ }
+ const CheckedInt<size_t> sizeNeeded =
+ CheckedInt<size_t>(aLength) * sizeof(Type) + AlignmentPaddingSize();
+
+ if (!sizeNeeded.isValid() || sizeNeeded.value() >= INT32_MAX) {
+ // overflow or over an acceptable size.
+ return false;
+ }
+ if (mData && mCapacity >= sizeNeeded.value()) {
+ return true;
+ }
+ auto newBuffer = MakeUniqueFallible<uint8_t[]>(sizeNeeded.value());
+ if (!newBuffer) {
+ return false;
+ }
+
+ // Find alignment address.
+ const uintptr_t alignmask = AlignmentOffset();
+ Type* newData = reinterpret_cast<Type*>(
+ (reinterpret_cast<uintptr_t>(newBuffer.get()) + alignmask) &
+ ~alignmask);
+ MOZ_ASSERT(uintptr_t(newData) % (AlignmentOffset() + 1) == 0);
+
+ MOZ_ASSERT(!mLength || mData);
+
+ PodZero(newData + mLength, aLength - mLength);
+ if (mLength) {
+ PodCopy(newData, mData, mLength);
+ }
+
+ mBuffer = std::move(newBuffer);
+ mCapacity = sizeNeeded.value();
+ mData = newData;
+
+ return true;
+ }
+ Type* mData;
+ size_t mLength{}; // number of elements
+ UniquePtr<uint8_t[]> mBuffer;
+ size_t mCapacity{}; // in bytes
+};
+
+typedef AlignedBuffer<uint8_t> AlignedByteBuffer;
+typedef AlignedBuffer<float> AlignedFloatBuffer;
+typedef AlignedBuffer<int16_t> AlignedShortBuffer;
+typedef AlignedBuffer<AudioDataValue> AlignedAudioBuffer;
+
+// Container that holds media samples.
+class MediaData {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaData)
+
+ enum class Type : uint8_t { AUDIO_DATA = 0, VIDEO_DATA, RAW_DATA, NULL_DATA };
+ static const char* TypeToStr(Type aType) {
+ switch (aType) {
+ case Type::AUDIO_DATA:
+ return "AUDIO_DATA";
+ case Type::VIDEO_DATA:
+ return "VIDEO_DATA";
+ case Type::RAW_DATA:
+ return "RAW_DATA";
+ case Type::NULL_DATA:
+ return "NULL_DATA";
+ default:
+ MOZ_CRASH("bad value");
+ }
+ }
+
+ MediaData(Type aType, int64_t aOffset, const media::TimeUnit& aTimestamp,
+ const media::TimeUnit& aDuration)
+ : mType(aType),
+ mOffset(aOffset),
+ mTime(aTimestamp),
+ mTimecode(aTimestamp),
+ mDuration(aDuration),
+ mKeyframe(false) {}
+
+ // Type of contained data.
+ const Type mType;
+
+ // Approximate byte offset where this data was demuxed from its media.
+ int64_t mOffset;
+
+ // Start time of sample.
+ media::TimeUnit mTime;
+
+ // Codec specific internal time code. For Ogg based codecs this is the
+ // granulepos.
+ media::TimeUnit mTimecode;
+
+ // Duration of sample, in microseconds.
+ media::TimeUnit mDuration;
+
+ bool mKeyframe;
+
+ media::TimeUnit GetEndTime() const { return mTime + mDuration; }
+
+ media::TimeUnit GetEndTimecode() const { return mTimecode + mDuration; }
+
+ bool HasValidTime() const {
+ return mTime.IsValid() && mTimecode.IsValid() && mDuration.IsValid() &&
+ GetEndTime().IsValid() && GetEndTimecode().IsValid();
+ }
+
+ template <typename ReturnType>
+ const ReturnType* As() const {
+ MOZ_ASSERT(this->mType == ReturnType::sType);
+ return static_cast<const ReturnType*>(this);
+ }
+
+ template <typename ReturnType>
+ ReturnType* As() {
+ MOZ_ASSERT(this->mType == ReturnType::sType);
+ return static_cast<ReturnType*>(this);
+ }
+
+ protected:
+ explicit MediaData(Type aType) : mType(aType), mOffset(0), mKeyframe(false) {}
+
+ virtual ~MediaData() = default;
+};
+
+// NullData is for decoder generating a sample which doesn't need to be
+// rendered.
+class NullData : public MediaData {
+ public:
+ NullData(int64_t aOffset, const media::TimeUnit& aTime,
+ const media::TimeUnit& aDuration)
+ : MediaData(Type::NULL_DATA, aOffset, aTime, aDuration) {}
+
+ static const Type sType = Type::NULL_DATA;
+};
+
+// Holds chunk a decoded audio frames.
+class AudioData : public MediaData {
+ public:
+ AudioData(int64_t aOffset, const media::TimeUnit& aTime,
+ AlignedAudioBuffer&& aData, uint32_t aChannels, uint32_t aRate,
+ uint32_t aChannelMap = AudioConfig::ChannelLayout::UNKNOWN_MAP);
+
+ static const Type sType = Type::AUDIO_DATA;
+ static const char* sTypeName;
+
+ // Access the buffer as a Span.
+ Span<AudioDataValue> Data() const;
+
+ // Amount of frames for contained data.
+ uint32_t Frames() const { return mFrames; }
+
+ // Trim the audio buffer such that its apparent content fits within the aTrim
+ // interval. The actual data isn't removed from the buffer and a followup call
+ // to SetTrimWindow could restore the content. mDuration, mTime and mFrames
+ // will be adjusted accordingly.
+ // Warning: rounding may occurs, in which case the new start time of the audio
+ // sample may still be lesser than aTrim.mStart.
+ bool SetTrimWindow(const media::TimeInterval& aTrim);
+
+ // Get the internal audio buffer to be moved. After this call the original
+ // AudioData will be emptied and can't be used again.
+ AlignedAudioBuffer MoveableData();
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const;
+
+ // If mAudioBuffer is null, creates it from mAudioData.
+ void EnsureAudioBuffer();
+
+ // Return true if the adjusted time is valid. Caller should handle error when
+ // the result is invalid.
+ bool AdjustForStartTime(const media::TimeUnit& aStartTime);
+
+ // This method is used to adjust the original start time, which would change
+ // `mTime` and `mOriginalTime` together, and should only be used for data
+ // which hasn't been trimmed before.
+ void SetOriginalStartTime(const media::TimeUnit& aStartTime);
+
+ const uint32_t mChannels;
+ // The AudioConfig::ChannelLayout map. Channels are ordered as per SMPTE
+ // definition. A value of UNKNOWN_MAP indicates unknown layout.
+ // ChannelMap is an unsigned bitmap compatible with Windows' WAVE and FFmpeg
+ // channel map.
+ const AudioConfig::ChannelLayout::ChannelMap mChannelMap;
+ const uint32_t mRate;
+
+ // At least one of mAudioBuffer/mAudioData must be non-null.
+ // mChannels channels, each with mFrames frames
+ RefPtr<SharedBuffer> mAudioBuffer;
+
+ protected:
+ ~AudioData() = default;
+
+ private:
+ friend class ArrayOfRemoteAudioData;
+ AudioDataValue* GetAdjustedData() const;
+ media::TimeUnit mOriginalTime;
+ // mFrames frames, each with mChannels values
+ AlignedAudioBuffer mAudioData;
+ Maybe<media::TimeInterval> mTrimWindow;
+ // Amount of frames for contained data.
+ uint32_t mFrames;
+ size_t mDataOffset = 0;
+};
+
+namespace layers {
+class TextureClient;
+class PlanarYCbCrImage;
+} // namespace layers
+
+class VideoInfo;
+
+// Holds a decoded video frame, in YCbCr format. These are queued in the reader.
+class VideoData : public MediaData {
+ public:
+ typedef gfx::IntRect IntRect;
+ typedef gfx::IntSize IntSize;
+ typedef gfx::ColorDepth ColorDepth;
+ typedef gfx::ColorRange ColorRange;
+ typedef gfx::YUVColorSpace YUVColorSpace;
+ typedef gfx::ColorSpace2 ColorSpace2;
+ typedef gfx::ChromaSubsampling ChromaSubsampling;
+ typedef layers::ImageContainer ImageContainer;
+ typedef layers::Image Image;
+ typedef layers::PlanarYCbCrImage PlanarYCbCrImage;
+
+ static const Type sType = Type::VIDEO_DATA;
+ static const char* sTypeName;
+
+ // YCbCr data obtained from decoding the video. The index's are:
+ // 0 = Y
+ // 1 = Cb
+ // 2 = Cr
+ struct YCbCrBuffer {
+ struct Plane {
+ uint8_t* mData;
+ uint32_t mWidth;
+ uint32_t mHeight;
+ uint32_t mStride;
+ uint32_t mSkip;
+ };
+
+ Plane mPlanes[3];
+ YUVColorSpace mYUVColorSpace = YUVColorSpace::Identity;
+ ColorSpace2 mColorPrimaries = ColorSpace2::UNKNOWN;
+ ColorDepth mColorDepth = ColorDepth::COLOR_8;
+ ColorRange mColorRange = ColorRange::LIMITED;
+ ChromaSubsampling mChromaSubsampling = ChromaSubsampling::FULL;
+ };
+
+ // Constructs a VideoData object. If aImage is nullptr, creates a new Image
+ // holding a copy of the YCbCr data passed in aBuffer. If aImage is not
+ // nullptr, it's stored as the underlying video image and aBuffer is assumed
+ // to point to memory within aImage so no copy is made. aTimecode is a codec
+ // specific number representing the timestamp of the frame of video data.
+ // Returns nsnull if an error occurs. This may indicate that memory couldn't
+ // be allocated to create the VideoData object, or it may indicate some
+ // problem with the input data (e.g. negative stride).
+
+ static bool UseUseNV12ForSoftwareDecodedVideoIfPossible(
+ layers::KnowsCompositor* aAllocator);
+
+ // Creates a new VideoData containing a deep copy of aBuffer. May use
+ // aContainer to allocate an Image to hold the copied data.
+ static already_AddRefed<VideoData> CreateAndCopyData(
+ const VideoInfo& aInfo, ImageContainer* aContainer, int64_t aOffset,
+ const media::TimeUnit& aTime, const media::TimeUnit& aDuration,
+ const YCbCrBuffer& aBuffer, bool aKeyframe,
+ const media::TimeUnit& aTimecode, const IntRect& aPicture,
+ layers::KnowsCompositor* aAllocator);
+
+ static already_AddRefed<VideoData> CreateAndCopyData(
+ const VideoInfo& aInfo, ImageContainer* aContainer, int64_t aOffset,
+ const media::TimeUnit& aTime, const media::TimeUnit& aDuration,
+ const YCbCrBuffer& aBuffer, const YCbCrBuffer::Plane& aAlphaPlane,
+ bool aKeyframe, const media::TimeUnit& aTimecode,
+ const IntRect& aPicture);
+
+ static already_AddRefed<VideoData> CreateFromImage(
+ const IntSize& aDisplay, int64_t aOffset, const media::TimeUnit& aTime,
+ const media::TimeUnit& aDuration, const RefPtr<Image>& aImage,
+ bool aKeyframe, const media::TimeUnit& aTimecode);
+
+ // Initialize PlanarYCbCrImage. Only When aCopyData is true,
+ // video data is copied to PlanarYCbCrImage.
+ static bool SetVideoDataToImage(PlanarYCbCrImage* aVideoImage,
+ const VideoInfo& aInfo,
+ const YCbCrBuffer& aBuffer,
+ const IntRect& aPicture, bool aCopyData);
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const;
+
+ // Dimensions at which to display the video frame. The picture region
+ // will be scaled to this size. This is should be the picture region's
+ // dimensions scaled with respect to its aspect ratio.
+ const IntSize mDisplay;
+
+ // This frame's image.
+ RefPtr<Image> mImage;
+
+ ColorDepth GetColorDepth() const;
+
+ uint32_t mFrameID;
+
+ VideoData(int64_t aOffset, const media::TimeUnit& aTime,
+ const media::TimeUnit& aDuration, bool aKeyframe,
+ const media::TimeUnit& aTimecode, IntSize aDisplay,
+ uint32_t aFrameID);
+
+ void MarkSentToCompositor() { mSentToCompositor = true; }
+ bool IsSentToCompositor() { return mSentToCompositor; }
+
+ void UpdateDuration(const media::TimeUnit& aDuration);
+ void UpdateTimestamp(const media::TimeUnit& aTimestamp);
+
+ // Return true if the adjusted time is valid. Caller should handle error when
+ // the result is invalid.
+ bool AdjustForStartTime(const media::TimeUnit& aStartTime);
+
+ void SetNextKeyFrameTime(const media::TimeUnit& aTime) {
+ mNextKeyFrameTime = aTime;
+ }
+
+ const media::TimeUnit& NextKeyFrameTime() const { return mNextKeyFrameTime; }
+
+ protected:
+ ~VideoData();
+
+ bool mSentToCompositor;
+ media::TimeUnit mNextKeyFrameTime;
+};
+
+enum class CryptoScheme : uint8_t {
+ None,
+ Cenc,
+ Cbcs,
+};
+
+class CryptoTrack {
+ public:
+ CryptoTrack()
+ : mCryptoScheme(CryptoScheme::None),
+ mIVSize(0),
+ mCryptByteBlock(0),
+ mSkipByteBlock(0) {}
+ CryptoScheme mCryptoScheme;
+ int32_t mIVSize;
+ CopyableTArray<uint8_t> mKeyId;
+ uint8_t mCryptByteBlock;
+ uint8_t mSkipByteBlock;
+ CopyableTArray<uint8_t> mConstantIV;
+
+ bool IsEncrypted() const { return mCryptoScheme != CryptoScheme::None; }
+};
+
+class CryptoSample : public CryptoTrack {
+ public:
+ // The num clear bytes in each subsample. The nth element in the array is the
+ // number of clear bytes at the start of the nth subsample.
+ // Clear sizes are stored as uint16_t in containers per ISO/IEC
+ // 23001-7, but we store them as uint32_t for 2 reasons
+ // - The Widevine CDM accepts clear sizes as uint32_t.
+ // - When converting samples to Annex B we modify the clear sizes and
+ // clear sizes near UINT16_MAX can overflow if stored in a uint16_t.
+ CopyableTArray<uint32_t> mPlainSizes;
+ // The num encrypted bytes in each subsample. The nth element in the array is
+ // the number of encrypted bytes at the start of the nth subsample.
+ CopyableTArray<uint32_t> mEncryptedSizes;
+ CopyableTArray<uint8_t> mIV;
+ CopyableTArray<CopyableTArray<uint8_t>> mInitDatas;
+ nsString mInitDataType;
+};
+
+// MediaRawData is a MediaData container used to store demuxed, still compressed
+// samples.
+// Use MediaRawData::CreateWriter() to obtain a MediaRawDataWriter object that
+// provides methods to modify and manipulate the data.
+// Memory allocations are fallible. Methods return a boolean indicating if
+// memory allocations were successful. Return values should always be checked.
+// MediaRawData::mData will be nullptr if no memory has been allocated or if
+// an error occurred during construction.
+// Existing data is only ever modified if new memory allocation has succeeded
+// and preserved if not.
+//
+// The memory referenced by mData will always be 32 bytes aligned and the
+// underlying buffer will always have a size such that 32 bytes blocks can be
+// used to read the content, regardless of the mSize value. Buffer is zeroed
+// on creation.
+//
+// Typical usage: create new MediaRawData; create the associated
+// MediaRawDataWriter, call SetSize() to allocate memory, write to mData,
+// up to mSize bytes.
+
+class MediaRawData;
+
+class MediaRawDataWriter {
+ public:
+ // Pointer to data or null if not-yet allocated
+ uint8_t* Data();
+ // Writeable size of buffer.
+ size_t Size();
+ // Writeable reference to MediaRawData::mCryptoInternal
+ CryptoSample& mCrypto;
+
+ // Data manipulation methods. mData and mSize may be updated accordingly.
+
+ // Set size of buffer, allocating memory as required.
+ // If size is increased, new buffer area is filled with 0.
+ [[nodiscard]] bool SetSize(size_t aSize);
+ // Add aData at the beginning of buffer.
+ [[nodiscard]] bool Prepend(const uint8_t* aData, size_t aSize);
+ [[nodiscard]] bool Append(const uint8_t* aData, size_t aSize);
+ // Replace current content with aData.
+ [[nodiscard]] bool Replace(const uint8_t* aData, size_t aSize);
+ // Clear the memory buffer. Will set target mData and mSize to 0.
+ void Clear();
+ // Remove aSize bytes from the front of the sample.
+ void PopFront(size_t aSize);
+
+ private:
+ friend class MediaRawData;
+ explicit MediaRawDataWriter(MediaRawData* aMediaRawData);
+ [[nodiscard]] bool EnsureSize(size_t aSize);
+ MediaRawData* mTarget;
+};
+
+class MediaRawData final : public MediaData {
+ public:
+ MediaRawData();
+ MediaRawData(const uint8_t* aData, size_t aSize);
+ MediaRawData(const uint8_t* aData, size_t aSize, const uint8_t* aAlphaData,
+ size_t aAlphaSize);
+ explicit MediaRawData(AlignedByteBuffer&& aData);
+ MediaRawData(AlignedByteBuffer&& aData, AlignedByteBuffer&& aAlphaData);
+
+ // Pointer to data or null if not-yet allocated
+ const uint8_t* Data() const { return mBuffer.Data(); }
+ // Pointer to alpha data or null if not-yet allocated
+ const uint8_t* AlphaData() const { return mAlphaBuffer.Data(); }
+ // Size of buffer.
+ size_t Size() const { return mBuffer.Length(); }
+ size_t AlphaSize() const { return mAlphaBuffer.Length(); }
+ size_t ComputedSizeOfIncludingThis() const {
+ return sizeof(*this) + mBuffer.ComputedSizeOfExcludingThis() +
+ mAlphaBuffer.ComputedSizeOfExcludingThis();
+ }
+ // Access the buffer as a Span.
+ operator Span<const uint8_t>() { return Span{Data(), Size()}; }
+
+ const CryptoSample& mCrypto;
+ RefPtr<MediaByteBuffer> mExtraData;
+
+ // Used by the Vorbis decoder and Ogg demuxer.
+ // Indicates that this is the last packet of the stream.
+ bool mEOS = false;
+
+ // Indicate to the audio decoder that mDiscardPadding frames should be
+ // trimmed.
+ uint32_t mDiscardPadding = 0;
+
+ RefPtr<TrackInfoSharedPtr> mTrackInfo;
+
+ // May contain the original start time and duration of the frames.
+ // mOriginalPresentationWindow.mStart would always be less or equal to mTime
+ // and mOriginalPresentationWindow.mEnd equal or greater to mTime + mDuration.
+ // This is used when the sample should get cropped so that its content will
+ // actually start on mTime and go for mDuration. If this interval is set, then
+ // the decoder should crop the content accordingly.
+ Maybe<media::TimeInterval> mOriginalPresentationWindow;
+
+ // If it's true, the `mCrypto` should be copied into the remote data as well.
+ // Currently this is only used for the media engine DRM playback.
+ bool mShouldCopyCryptoToRemoteRawData = false;
+
+ // It's only used when the remote decoder reconstructs the media raw data.
+ CryptoSample& GetWritableCrypto() { return mCryptoInternal; }
+
+ // Return a deep copy or nullptr if out of memory.
+ already_AddRefed<MediaRawData> Clone() const;
+ // Create a MediaRawDataWriter for this MediaRawData. The writer is not
+ // thread-safe.
+ UniquePtr<MediaRawDataWriter> CreateWriter();
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const;
+
+ protected:
+ ~MediaRawData();
+
+ private:
+ friend class MediaRawDataWriter;
+ friend class ArrayOfRemoteMediaRawData;
+ AlignedByteBuffer mBuffer;
+ AlignedByteBuffer mAlphaBuffer;
+ CryptoSample mCryptoInternal;
+ MediaRawData(const MediaRawData&); // Not implemented
+};
+
+// MediaByteBuffer is a ref counted infallible TArray.
+class MediaByteBuffer : public nsTArray<uint8_t> {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaByteBuffer);
+ MediaByteBuffer() = default;
+ explicit MediaByteBuffer(size_t aCapacity) : nsTArray<uint8_t>(aCapacity) {}
+
+ private:
+ ~MediaByteBuffer() = default;
+};
+
+} // namespace mozilla
+
+#endif // MediaData_h
diff --git a/dom/media/MediaDataDemuxer.h b/dom/media/MediaDataDemuxer.h
new file mode 100644
index 0000000000..79ef5f5f0a
--- /dev/null
+++ b/dom/media/MediaDataDemuxer.h
@@ -0,0 +1,213 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(MediaDataDemuxer_h)
+# define MediaDataDemuxer_h
+
+# include "DecoderDoctorLogger.h"
+# include "mozilla/MozPromise.h"
+# include "mozilla/UniquePtr.h"
+
+# include "MediaData.h"
+# include "MediaInfo.h"
+# include "MediaResult.h"
+# include "TimeUnits.h"
+# include "nsISupportsImpl.h"
+# include "mozilla/RefPtr.h"
+# include "nsTArray.h"
+
+namespace mozilla {
+
+class MediaTrackDemuxer;
+class TrackMetadataHolder;
+
+DDLoggedTypeDeclName(MediaDataDemuxer);
+DDLoggedTypeName(MediaTrackDemuxer);
+
+// Allows reading the media data: to retrieve the metadata and demux samples.
+// MediaDataDemuxer isn't designed to be thread safe.
+// When used by the MediaFormatDecoder, care is taken to ensure that the demuxer
+// will never be called from more than one thread at once.
+class MediaDataDemuxer : public DecoderDoctorLifeLogger<MediaDataDemuxer> {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaDataDemuxer)
+
+ typedef MozPromise<MediaResult, MediaResult, /* IsExclusive = */ false>
+ InitPromise;
+
+ // Initializes the demuxer. Other methods cannot be called unless
+ // initialization has completed and succeeded.
+ // Typically a demuxer will wait to parse the metadata before resolving the
+ // promise. The promise must not be resolved until sufficient data is
+ // supplied. For example, an incomplete metadata would cause the promise to be
+ // rejected should no more data be coming, while the demuxer would wait
+ // otherwise.
+ virtual RefPtr<InitPromise> Init() = 0;
+
+ // Returns the number of tracks of aType type available. A value of
+ // 0 indicates that no such type is available.
+ virtual uint32_t GetNumberTracks(TrackInfo::TrackType aType) const = 0;
+
+ // Returns the MediaTrackDemuxer associated with aTrackNumber aType track.
+ // aTrackNumber is not to be confused with the Track ID.
+ // aTrackNumber must be constrained between 0 and GetNumberTracks(aType) - 1
+ // The actual Track ID is to be retrieved by calling
+ // MediaTrackDemuxer::TrackInfo.
+ virtual already_AddRefed<MediaTrackDemuxer> GetTrackDemuxer(
+ TrackInfo::TrackType aType, uint32_t aTrackNumber) = 0;
+
+ // Returns true if the underlying resource allows seeking.
+ virtual bool IsSeekable() const = 0;
+
+ // Returns true if the underlying resource can only seek within buffered
+ // ranges.
+ virtual bool IsSeekableOnlyInBufferedRanges() const { return false; }
+
+ // Returns the media's crypto information, or nullptr if media isn't
+ // encrypted.
+ virtual UniquePtr<EncryptionInfo> GetCrypto() { return nullptr; }
+
+ // Notifies the demuxer that the underlying resource has received more data
+ // since the demuxer was initialized.
+ // The demuxer can use this mechanism to inform all track demuxers that new
+ // data is available and to refresh its buffered range.
+ virtual void NotifyDataArrived() {}
+
+ // Notifies the demuxer that the underlying resource has had data removed
+ // since the demuxer was initialized.
+ // The demuxer can use this mechanism to inform all track demuxers to update
+ // its buffered range.
+ // This will be called should the demuxer be used with MediaSource.
+ virtual void NotifyDataRemoved() {}
+
+ // Indicate to MediaFormatReader if it should compute the start time
+ // of the demuxed data. If true (default) the first sample returned will be
+ // used as reference time base.
+ virtual bool ShouldComputeStartTime() const { return true; }
+
+ protected:
+ virtual ~MediaDataDemuxer() = default;
+};
+
+class MediaTrackDemuxer : public DecoderDoctorLifeLogger<MediaTrackDemuxer> {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaTrackDemuxer)
+
+ class SamplesHolder {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SamplesHolder)
+
+ void AppendSample(RefPtr<MediaRawData>& aSample) {
+ MOZ_DIAGNOSTIC_ASSERT(aSample->HasValidTime());
+ mSamples.AppendElement(aSample);
+ }
+
+ const nsTArray<RefPtr<MediaRawData>>& GetSamples() const {
+ return mSamples;
+ }
+
+ // This method is only used to do the move semantic for mSamples, do not
+ // append any element to the samples we returns. We should always append new
+ // sample to mSamples via `AppendSample()`.
+ nsTArray<RefPtr<MediaRawData>>& GetMovableSamples() { return mSamples; }
+
+ private:
+ ~SamplesHolder() = default;
+ nsTArray<RefPtr<MediaRawData>> mSamples;
+ };
+
+ class SkipFailureHolder {
+ public:
+ SkipFailureHolder(const MediaResult& aFailure, uint32_t aSkipped)
+ : mFailure(aFailure), mSkipped(aSkipped) {}
+ MediaResult mFailure;
+ uint32_t mSkipped;
+ };
+
+ typedef MozPromise<media::TimeUnit, MediaResult, /* IsExclusive = */ true>
+ SeekPromise;
+ typedef MozPromise<RefPtr<SamplesHolder>, MediaResult,
+ /* IsExclusive = */ true>
+ SamplesPromise;
+ typedef MozPromise<uint32_t, SkipFailureHolder, /* IsExclusive = */ true>
+ SkipAccessPointPromise;
+
+ // Returns the TrackInfo (a.k.a Track Description) for this track.
+ // The TrackInfo returned will be:
+ // TrackInfo::kVideoTrack -> VideoInfo.
+ // TrackInfo::kAudioTrack -> AudioInfo.
+ // respectively.
+ virtual UniquePtr<TrackInfo> GetInfo() const = 0;
+
+ // Seeks to aTime. Upon success, SeekPromise will be resolved with the
+ // actual time seeked to. Typically the random access point time
+ virtual RefPtr<SeekPromise> Seek(const media::TimeUnit& aTime) = 0;
+
+ // Returns the next aNumSamples sample(s) available.
+ // If only a lesser amount of samples is available, only those will be
+ // returned.
+ // A aNumSamples value of -1 indicates to return all remaining samples.
+ // A video sample is typically made of a single video frame while an audio
+ // sample will contains multiple audio frames.
+ virtual RefPtr<SamplesPromise> GetSamples(int32_t aNumSamples = 1) = 0;
+
+ // Returns true if a call to GetSamples() may block while waiting on the
+ // underlying resource to return the data.
+ // This is used by the MediaFormatReader to determine if buffering heuristics
+ // should be used.
+ virtual bool GetSamplesMayBlock() const { return true; }
+
+ // Cancel all pending actions (Seek, GetSamples) and reset current state
+ // All pending promises are to be rejected with CANCEL.
+ // The next call to GetSamples would return the first sample available in the
+ // track.
+ virtual void Reset() = 0;
+
+ // Returns timestamp of next random access point or an error if the demuxer
+ // can't report this.
+ virtual nsresult GetNextRandomAccessPoint(media::TimeUnit* aTime) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ // Returns timestamp of previous random access point or an error if the
+ // demuxer can't report this.
+ virtual nsresult GetPreviousRandomAccessPoint(media::TimeUnit* aTime) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ // Skip frames until the next Random Access Point located after
+ // aTimeThreshold.
+ // The first frame returned by the next call to GetSamples() will be the
+ // first random access point found after aTimeThreshold.
+ // Upon success, returns the number of frames skipped.
+ virtual RefPtr<SkipAccessPointPromise> SkipToNextRandomAccessPoint(
+ const media::TimeUnit& aTimeThreshold) = 0;
+
+ // Gets the resource's offset used for the last Seek() or GetSample().
+ // A negative value indicates that this functionality isn't supported.
+ virtual int64_t GetResourceOffset() const { return -1; }
+
+ virtual TrackInfo::TrackType GetType() const { return GetInfo()->GetType(); }
+
+ virtual media::TimeIntervals GetBuffered() = 0;
+
+ // By default, it is assumed that the entire resource can be evicted once
+ // all samples have been demuxed.
+ virtual int64_t GetEvictionOffset(const media::TimeUnit& aTime) {
+ return INT64_MAX;
+ }
+
+ // If the MediaTrackDemuxer and MediaDataDemuxer hold cross references.
+ // BreakCycles must be overridden.
+ virtual void BreakCycles() {}
+
+ protected:
+ virtual ~MediaTrackDemuxer() = default;
+};
+
+} // namespace mozilla
+
+#endif // MediaDataDemuxer_h
diff --git a/dom/media/MediaDecoder.cpp b/dom/media/MediaDecoder.cpp
new file mode 100644
index 0000000000..f7a8d4e644
--- /dev/null
+++ b/dom/media/MediaDecoder.cpp
@@ -0,0 +1,1681 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaDecoder.h"
+
+#include "AudioDeviceInfo.h"
+#include "DOMMediaStream.h"
+#include "DecoderBenchmark.h"
+#include "ImageContainer.h"
+#include "MediaDecoderStateMachineBase.h"
+#include "MediaFormatReader.h"
+#include "MediaResource.h"
+#include "MediaShutdownManager.h"
+#include "MediaTrackGraph.h"
+#include "TelemetryProbesReporter.h"
+#include "VideoFrameContainer.h"
+#include "VideoUtils.h"
+#include "mozilla/AbstractThread.h"
+#include "mozilla/dom/DOMTypes.h"
+#include "mozilla/FloatingPoint.h"
+#include "mozilla/MathAlgorithms.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/Unused.h"
+#include "nsComponentManagerUtils.h"
+#include "nsContentUtils.h"
+#include "nsError.h"
+#include "nsIMemoryReporter.h"
+#include "nsPrintfCString.h"
+#include "nsServiceManagerUtils.h"
+#include "nsTArray.h"
+#include "WindowRenderer.h"
+#include <algorithm>
+#include <cmath>
+#include <limits>
+
+using namespace mozilla::dom;
+using namespace mozilla::layers;
+using namespace mozilla::media;
+
+namespace mozilla {
+
+// avoid redefined macro in unified build
+#undef LOG
+#undef DUMP
+
+LazyLogModule gMediaDecoderLog("MediaDecoder");
+
+#define LOG(x, ...) \
+ DDMOZ_LOG(gMediaDecoderLog, LogLevel::Debug, x, ##__VA_ARGS__)
+
+#define DUMP(x, ...) printf_stderr(x "\n", ##__VA_ARGS__)
+
+#define NS_DispatchToMainThread(...) CompileError_UseAbstractMainThreadInstead
+
+static const char* ToPlayStateStr(MediaDecoder::PlayState aState) {
+ switch (aState) {
+ case MediaDecoder::PLAY_STATE_LOADING:
+ return "LOADING";
+ case MediaDecoder::PLAY_STATE_PAUSED:
+ return "PAUSED";
+ case MediaDecoder::PLAY_STATE_PLAYING:
+ return "PLAYING";
+ case MediaDecoder::PLAY_STATE_ENDED:
+ return "ENDED";
+ case MediaDecoder::PLAY_STATE_SHUTDOWN:
+ return "SHUTDOWN";
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid playState.");
+ }
+ return "UNKNOWN";
+}
+
+class MediaMemoryTracker : public nsIMemoryReporter {
+ virtual ~MediaMemoryTracker();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIMEMORYREPORTER
+
+ MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf);
+
+ MediaMemoryTracker();
+ void InitMemoryReporter();
+
+ static StaticRefPtr<MediaMemoryTracker> sUniqueInstance;
+
+ static MediaMemoryTracker* UniqueInstance() {
+ if (!sUniqueInstance) {
+ sUniqueInstance = new MediaMemoryTracker();
+ sUniqueInstance->InitMemoryReporter();
+ }
+ return sUniqueInstance;
+ }
+
+ using DecodersArray = nsTArray<MediaDecoder*>;
+ static DecodersArray& Decoders() { return UniqueInstance()->mDecoders; }
+
+ DecodersArray mDecoders;
+
+ public:
+ static void AddMediaDecoder(MediaDecoder* aDecoder) {
+ Decoders().AppendElement(aDecoder);
+ }
+
+ static void RemoveMediaDecoder(MediaDecoder* aDecoder) {
+ DecodersArray& decoders = Decoders();
+ decoders.RemoveElement(aDecoder);
+ if (decoders.IsEmpty()) {
+ sUniqueInstance = nullptr;
+ }
+ }
+
+ static RefPtr<MediaMemoryPromise> GetSizes(dom::Document* aDoc) {
+ MOZ_ASSERT(NS_IsMainThread());
+ DecodersArray& decoders = Decoders();
+
+ // if we don't have any decoder, we can bail
+ if (decoders.IsEmpty()) {
+ // and release the instance that was created by calling Decoders()
+ sUniqueInstance = nullptr;
+ return MediaMemoryPromise::CreateAndResolve(MediaMemoryInfo(), __func__);
+ }
+
+ RefPtr<MediaDecoder::ResourceSizes> resourceSizes =
+ new MediaDecoder::ResourceSizes(MediaMemoryTracker::MallocSizeOf);
+
+ size_t videoSize = 0;
+ size_t audioSize = 0;
+
+ for (auto&& decoder : decoders) {
+ if (decoder->GetOwner() && decoder->GetOwner()->GetDocument() == aDoc) {
+ videoSize += decoder->SizeOfVideoQueue();
+ audioSize += decoder->SizeOfAudioQueue();
+ decoder->AddSizeOfResources(resourceSizes);
+ }
+ }
+
+ return resourceSizes->Promise()->Then(
+ AbstractThread::MainThread(), __func__,
+ [videoSize, audioSize](size_t resourceSize) {
+ return MediaMemoryPromise::CreateAndResolve(
+ MediaMemoryInfo(videoSize, audioSize, resourceSize), __func__);
+ },
+ [](size_t) {
+ return MediaMemoryPromise::CreateAndReject(NS_ERROR_FAILURE,
+ __func__);
+ });
+ }
+};
+
+StaticRefPtr<MediaMemoryTracker> MediaMemoryTracker::sUniqueInstance;
+
+RefPtr<MediaMemoryPromise> GetMediaMemorySizes(dom::Document* aDoc) {
+ return MediaMemoryTracker::GetSizes(aDoc);
+}
+
+LazyLogModule gMediaTimerLog("MediaTimer");
+
+constexpr TimeUnit MediaDecoder::DEFAULT_NEXT_FRAME_AVAILABLE_BUFFERED;
+
+void MediaDecoder::InitStatics() {
+ MOZ_ASSERT(NS_IsMainThread());
+ // Eagerly init gMediaDecoderLog to work around bug 1415441.
+ MOZ_LOG(gMediaDecoderLog, LogLevel::Info, ("MediaDecoder::InitStatics"));
+}
+
+NS_IMPL_ISUPPORTS(MediaMemoryTracker, nsIMemoryReporter)
+
+void MediaDecoder::NotifyOwnerActivityChanged(bool aIsOwnerInvisible,
+ bool aIsOwnerConnected) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
+ SetElementVisibility(aIsOwnerInvisible, aIsOwnerConnected);
+
+ NotifyCompositor();
+}
+
+void MediaDecoder::Pause() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
+ LOG("Pause");
+ if (mPlayState == PLAY_STATE_LOADING || IsEnded()) {
+ mNextState = PLAY_STATE_PAUSED;
+ return;
+ }
+ ChangeState(PLAY_STATE_PAUSED);
+}
+
+void MediaDecoder::SetVolume(double aVolume) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mVolume = aVolume;
+}
+
+RefPtr<GenericPromise> MediaDecoder::SetSink(AudioDeviceInfo* aSinkDevice) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mSinkDevice = aSinkDevice;
+ return GetStateMachine()->InvokeSetSink(aSinkDevice);
+}
+
+void MediaDecoder::SetOutputCaptureState(OutputCaptureState aState,
+ SharedDummyTrack* aDummyTrack) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mDecoderStateMachine, "Must be called after Load().");
+ MOZ_ASSERT_IF(aState == OutputCaptureState::Capture, aDummyTrack);
+ mOutputCaptureState = aState;
+ if (mOutputDummyTrack.Ref().get() != aDummyTrack) {
+ mOutputDummyTrack = nsMainThreadPtrHandle<SharedDummyTrack>(
+ MakeAndAddRef<nsMainThreadPtrHolder<SharedDummyTrack>>(
+ "MediaDecoder::mOutputDummyTrack", aDummyTrack));
+ }
+}
+
+void MediaDecoder::AddOutputTrack(RefPtr<ProcessedMediaTrack> aTrack) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mDecoderStateMachine, "Must be called after Load().");
+ CopyableTArray<RefPtr<ProcessedMediaTrack>> tracks = mOutputTracks;
+ tracks.AppendElement(std::move(aTrack));
+ mOutputTracks = tracks;
+}
+
+void MediaDecoder::RemoveOutputTrack(
+ const RefPtr<ProcessedMediaTrack>& aTrack) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mDecoderStateMachine, "Must be called after Load().");
+ CopyableTArray<RefPtr<ProcessedMediaTrack>> tracks = mOutputTracks;
+ if (tracks.RemoveElement(aTrack)) {
+ mOutputTracks = tracks;
+ }
+}
+
+void MediaDecoder::SetOutputTracksPrincipal(
+ const RefPtr<nsIPrincipal>& aPrincipal) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mDecoderStateMachine, "Must be called after Load().");
+ mOutputPrincipal = MakePrincipalHandle(aPrincipal);
+}
+
+double MediaDecoder::GetDuration() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return ToMicrosecondResolution(mDuration.match(DurationToDouble()));
+}
+
+bool MediaDecoder::IsInfinite() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return std::isinf(mDuration.match(DurationToDouble()));
+}
+
+#define INIT_MIRROR(name, val) \
+ name(mOwner->AbstractMainThread(), val, "MediaDecoder::" #name " (Mirror)")
+#define INIT_CANONICAL(name, val) \
+ name(mOwner->AbstractMainThread(), val, "MediaDecoder::" #name " (Canonical)")
+
+MediaDecoder::MediaDecoder(MediaDecoderInit& aInit)
+ : mWatchManager(this, aInit.mOwner->AbstractMainThread()),
+ mLogicalPosition(0.0),
+ mDuration(TimeUnit::Invalid()),
+ mOwner(aInit.mOwner),
+ mAbstractMainThread(aInit.mOwner->AbstractMainThread()),
+ mFrameStats(new FrameStatistics()),
+ mDecoderBenchmark(new DecoderBenchmark()),
+ mVideoFrameContainer(aInit.mOwner->GetVideoFrameContainer()),
+ mMinimizePreroll(aInit.mMinimizePreroll),
+ mFiredMetadataLoaded(false),
+ mIsOwnerInvisible(false),
+ mIsOwnerConnected(false),
+ mForcedHidden(false),
+ mHasSuspendTaint(aInit.mHasSuspendTaint),
+ mShouldResistFingerprinting(
+ aInit.mOwner->ShouldResistFingerprinting(RFPTarget::Unknown)),
+ mPlaybackRate(aInit.mPlaybackRate),
+ mLogicallySeeking(false, "MediaDecoder::mLogicallySeeking"),
+ INIT_MIRROR(mBuffered, TimeIntervals()),
+ INIT_MIRROR(mCurrentPosition, TimeUnit::Zero()),
+ INIT_MIRROR(mStateMachineDuration, NullableTimeUnit()),
+ INIT_MIRROR(mIsAudioDataAudible, false),
+ INIT_CANONICAL(mVolume, aInit.mVolume),
+ INIT_CANONICAL(mPreservesPitch, aInit.mPreservesPitch),
+ INIT_CANONICAL(mLooping, aInit.mLooping),
+ INIT_CANONICAL(mStreamName, aInit.mStreamName),
+ INIT_CANONICAL(mSinkDevice, nullptr),
+ INIT_CANONICAL(mSecondaryVideoContainer, nullptr),
+ INIT_CANONICAL(mOutputCaptureState, OutputCaptureState::None),
+ INIT_CANONICAL(mOutputDummyTrack, nullptr),
+ INIT_CANONICAL(mOutputTracks, nsTArray<RefPtr<ProcessedMediaTrack>>()),
+ INIT_CANONICAL(mOutputPrincipal, PRINCIPAL_HANDLE_NONE),
+ INIT_CANONICAL(mPlayState, PLAY_STATE_LOADING),
+ mSameOriginMedia(false),
+ mVideoDecodingOberver(
+ new BackgroundVideoDecodingPermissionObserver(this)),
+ mIsBackgroundVideoDecodingAllowed(false),
+ mTelemetryReported(false),
+ mContainerType(aInit.mContainerType),
+ mTelemetryProbesReporter(
+ new TelemetryProbesReporter(aInit.mReporterOwner)) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mAbstractMainThread);
+ MediaMemoryTracker::AddMediaDecoder(this);
+
+ //
+ // Initialize watchers.
+ //
+
+ // mDuration
+ mWatchManager.Watch(mStateMachineDuration, &MediaDecoder::DurationChanged);
+
+ // readyState
+ mWatchManager.Watch(mPlayState, &MediaDecoder::UpdateReadyState);
+ // ReadyState computation depends on MediaDecoder::CanPlayThrough, which
+ // depends on the download rate.
+ mWatchManager.Watch(mBuffered, &MediaDecoder::UpdateReadyState);
+
+ // mLogicalPosition
+ mWatchManager.Watch(mCurrentPosition, &MediaDecoder::UpdateLogicalPosition);
+ mWatchManager.Watch(mPlayState, &MediaDecoder::UpdateLogicalPosition);
+ mWatchManager.Watch(mLogicallySeeking, &MediaDecoder::UpdateLogicalPosition);
+
+ mWatchManager.Watch(mIsAudioDataAudible,
+ &MediaDecoder::NotifyAudibleStateChanged);
+
+ mWatchManager.Watch(mVolume, &MediaDecoder::NotifyVolumeChanged);
+
+ mVideoDecodingOberver->RegisterEvent();
+}
+
+#undef INIT_MIRROR
+#undef INIT_CANONICAL
+
+void MediaDecoder::Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
+
+ // Unwatch all watch targets to prevent further notifications.
+ mWatchManager.Shutdown();
+
+ DiscardOngoingSeekIfExists();
+
+ // This changes the decoder state to SHUTDOWN and does other things
+ // necessary to unblock the state machine thread if it's blocked, so
+ // the asynchronous shutdown in nsDestroyStateMachine won't deadlock.
+ if (mDecoderStateMachine) {
+ ShutdownStateMachine()->Then(mAbstractMainThread, __func__, this,
+ &MediaDecoder::FinishShutdown,
+ &MediaDecoder::FinishShutdown);
+ } else {
+ // Ensure we always unregister asynchronously in order not to disrupt
+ // the hashtable iterating in MediaShutdownManager::Shutdown().
+ RefPtr<MediaDecoder> self = this;
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ "MediaDecoder::Shutdown", [self]() { self->ShutdownInternal(); });
+ mAbstractMainThread->Dispatch(r.forget());
+ }
+
+ ChangeState(PLAY_STATE_SHUTDOWN);
+ mVideoDecodingOberver->UnregisterEvent();
+ mVideoDecodingOberver = nullptr;
+ mOwner = nullptr;
+}
+
+void MediaDecoder::NotifyXPCOMShutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ // NotifyXPCOMShutdown will clear its reference to mDecoder. So we must ensure
+ // that this MediaDecoder stays alive until completion.
+ RefPtr<MediaDecoder> kungFuDeathGrip = this;
+ if (auto* owner = GetOwner()) {
+ owner->NotifyXPCOMShutdown();
+ } else if (!IsShutdown()) {
+ Shutdown();
+ }
+ MOZ_DIAGNOSTIC_ASSERT(IsShutdown());
+}
+
+MediaDecoder::~MediaDecoder() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(IsShutdown());
+ MediaMemoryTracker::RemoveMediaDecoder(this);
+}
+
+void MediaDecoder::OnPlaybackEvent(MediaPlaybackEvent&& aEvent) {
+ switch (aEvent.mType) {
+ case MediaPlaybackEvent::PlaybackEnded:
+ PlaybackEnded();
+ break;
+ case MediaPlaybackEvent::SeekStarted:
+ SeekingStarted();
+ break;
+ case MediaPlaybackEvent::Invalidate:
+ Invalidate();
+ break;
+ case MediaPlaybackEvent::EnterVideoSuspend:
+ GetOwner()->DispatchAsyncEvent(u"mozentervideosuspend"_ns);
+ mTelemetryProbesReporter->OnDecodeSuspended();
+ mIsVideoDecodingSuspended = true;
+ break;
+ case MediaPlaybackEvent::ExitVideoSuspend:
+ GetOwner()->DispatchAsyncEvent(u"mozexitvideosuspend"_ns);
+ mTelemetryProbesReporter->OnDecodeResumed();
+ mIsVideoDecodingSuspended = false;
+ break;
+ case MediaPlaybackEvent::StartVideoSuspendTimer:
+ GetOwner()->DispatchAsyncEvent(u"mozstartvideosuspendtimer"_ns);
+ break;
+ case MediaPlaybackEvent::CancelVideoSuspendTimer:
+ GetOwner()->DispatchAsyncEvent(u"mozcancelvideosuspendtimer"_ns);
+ break;
+ case MediaPlaybackEvent::VideoOnlySeekBegin:
+ GetOwner()->DispatchAsyncEvent(u"mozvideoonlyseekbegin"_ns);
+ break;
+ case MediaPlaybackEvent::VideoOnlySeekCompleted:
+ GetOwner()->DispatchAsyncEvent(u"mozvideoonlyseekcompleted"_ns);
+ break;
+ default:
+ break;
+ }
+}
+
+bool MediaDecoder::IsVideoDecodingSuspended() const {
+ return mIsVideoDecodingSuspended;
+}
+
+void MediaDecoder::OnPlaybackErrorEvent(const MediaResult& aError) {
+ MOZ_ASSERT(NS_IsMainThread());
+#ifndef MOZ_WMF_MEDIA_ENGINE
+ DecodeError(aError);
+#else
+ if (aError != NS_ERROR_DOM_MEDIA_EXTERNAL_ENGINE_NOT_SUPPORTED_ERR &&
+ aError != NS_ERROR_DOM_MEDIA_CDM_PROXY_NOT_SUPPORTED_ERR) {
+ DecodeError(aError);
+ return;
+ }
+
+ // Already in shutting down decoder, no need to create another state machine.
+ if (mPlayState == PLAY_STATE_SHUTDOWN) {
+ return;
+ }
+
+ // External engine can't play the resource or we intentionally disable it, try
+ // to use our own state machine again. Here we will create a new state machine
+ // immediately and asynchrously shutdown the old one because we don't want to
+ // dispatch any task to the old state machine. Therefore, we will disconnect
+ // anything related with the old state machine, create a new state machine and
+ // setup events/mirror/etc, then shutdown the old one and release its
+ // reference once it finishes shutdown.
+ RefPtr<MediaDecoderStateMachineBase> discardStateMachine =
+ mDecoderStateMachine;
+
+ // Disconnect mirror and events first.
+ SetStateMachine(nullptr);
+ DisconnectEvents();
+
+ // Recreate a state machine and shutdown the old one.
+ bool needExternalEngine = false;
+ if (aError == NS_ERROR_DOM_MEDIA_CDM_PROXY_NOT_SUPPORTED_ERR) {
+# ifdef MOZ_WMF_CDM
+ if (aError.GetCDMProxy()->AsWMFCDMProxy()) {
+ needExternalEngine = true;
+ }
+# endif
+ }
+ LOG("Need to create a new %s state machine",
+ needExternalEngine ? "external engine" : "normal");
+
+ nsresult rv = CreateAndInitStateMachine(
+ false /* live stream */,
+ !needExternalEngine /* disable external engine */);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ LOG("Failed to create a new state machine!");
+ }
+
+ // Some attributes might have been set on the destroyed state machine, and
+ // won't be reflected on the new MDSM by the state mirroring. We need to
+ // update them manually later, after MDSM finished reading the
+ // metadata because the MDSM might not be ready to perform the operations yet.
+ mPendingStatusUpdateForNewlyCreatedStateMachine = true;
+
+ // If there is ongoing seek performed on the old MDSM, cancel it because we
+ // will perform seeking later again and don't want the old seeking affecting
+ // us.
+ DiscardOngoingSeekIfExists();
+
+ discardStateMachine->BeginShutdown()->Then(
+ AbstractThread::MainThread(), __func__, [discardStateMachine] {});
+#endif
+}
+
+void MediaDecoder::OnDecoderDoctorEvent(DecoderDoctorEvent aEvent) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // OnDecoderDoctorEvent is disconnected at shutdown time.
+ MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
+ Document* doc = GetOwner()->GetDocument();
+ if (!doc) {
+ return;
+ }
+ DecoderDoctorDiagnostics diags;
+ diags.StoreEvent(doc, aEvent, __func__);
+}
+
+static const char* NextFrameStatusToStr(
+ MediaDecoderOwner::NextFrameStatus aStatus) {
+ switch (aStatus) {
+ case MediaDecoderOwner::NEXT_FRAME_AVAILABLE:
+ return "NEXT_FRAME_AVAILABLE";
+ case MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE:
+ return "NEXT_FRAME_UNAVAILABLE";
+ case MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE_BUFFERING:
+ return "NEXT_FRAME_UNAVAILABLE_BUFFERING";
+ case MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE_SEEKING:
+ return "NEXT_FRAME_UNAVAILABLE_SEEKING";
+ case MediaDecoderOwner::NEXT_FRAME_UNINITIALIZED:
+ return "NEXT_FRAME_UNINITIALIZED";
+ }
+ return "UNKNOWN";
+}
+
+void MediaDecoder::OnNextFrameStatus(
+ MediaDecoderOwner::NextFrameStatus aStatus) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
+ if (mNextFrameStatus != aStatus) {
+ LOG("Changed mNextFrameStatus to %s", NextFrameStatusToStr(aStatus));
+ mNextFrameStatus = aStatus;
+ UpdateReadyState();
+ }
+}
+
+void MediaDecoder::OnTrackInfoUpdated(const VideoInfo& aVideoInfo,
+ const AudioInfo& aAudioInfo) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
+
+ // Note that we don't check HasVideo() or HasAudio() here, because
+ // those are checks for existing validity. If we always set the values
+ // to what we receive, then we can go from not-video to video, for
+ // example.
+ mInfo->mVideo = aVideoInfo;
+ mInfo->mAudio = aAudioInfo;
+
+ Invalidate();
+
+ EnsureTelemetryReported();
+}
+
+void MediaDecoder::OnSecondaryVideoContainerInstalled(
+ const RefPtr<VideoFrameContainer>& aSecondaryVideoContainer) {
+ MOZ_ASSERT(NS_IsMainThread());
+ GetOwner()->OnSecondaryVideoContainerInstalled(aSecondaryVideoContainer);
+}
+
+void MediaDecoder::OnStoreDecoderBenchmark(const VideoInfo& aInfo) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ int32_t videoFrameRate = aInfo.GetFrameRate().ref();
+
+ if (mFrameStats && videoFrameRate) {
+ DecoderBenchmarkInfo benchmarkInfo{
+ aInfo.mMimeType,
+ aInfo.mDisplay.width,
+ aInfo.mDisplay.height,
+ videoFrameRate,
+ BitDepthForColorDepth(aInfo.mColorDepth),
+ };
+
+ LOG("Store benchmark: Video width=%d, height=%d, frameRate=%d, content "
+ "type = %s\n",
+ benchmarkInfo.mWidth, benchmarkInfo.mHeight, benchmarkInfo.mFrameRate,
+ benchmarkInfo.mContentType.BeginReading());
+
+ mDecoderBenchmark->Store(benchmarkInfo, mFrameStats);
+ }
+}
+
+void MediaDecoder::ShutdownInternal() {
+ MOZ_ASSERT(NS_IsMainThread());
+ mVideoFrameContainer = nullptr;
+ mSecondaryVideoContainer = nullptr;
+ MediaShutdownManager::Instance().Unregister(this);
+}
+
+void MediaDecoder::FinishShutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ SetStateMachine(nullptr);
+ ShutdownInternal();
+}
+
+nsresult MediaDecoder::CreateAndInitStateMachine(bool aIsLiveStream,
+ bool aDisableExternalEngine) {
+ MOZ_ASSERT(NS_IsMainThread());
+ SetStateMachine(CreateStateMachine(aDisableExternalEngine));
+
+ NS_ENSURE_TRUE(GetStateMachine(), NS_ERROR_FAILURE);
+ GetStateMachine()->DispatchIsLiveStream(aIsLiveStream);
+
+ nsresult rv = mDecoderStateMachine->Init(this);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // If some parameters got set before the state machine got created,
+ // set them now
+ SetStateMachineParameters();
+
+ return NS_OK;
+}
+
+void MediaDecoder::SetStateMachineParameters() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mPlaybackRate != 1 && mPlaybackRate != 0) {
+ mDecoderStateMachine->DispatchSetPlaybackRate(mPlaybackRate);
+ }
+ mTimedMetadataListener = mDecoderStateMachine->TimedMetadataEvent().Connect(
+ mAbstractMainThread, this, &MediaDecoder::OnMetadataUpdate);
+ mMetadataLoadedListener = mDecoderStateMachine->MetadataLoadedEvent().Connect(
+ mAbstractMainThread, this, &MediaDecoder::MetadataLoaded);
+ mFirstFrameLoadedListener =
+ mDecoderStateMachine->FirstFrameLoadedEvent().Connect(
+ mAbstractMainThread, this, &MediaDecoder::FirstFrameLoaded);
+
+ mOnPlaybackEvent = mDecoderStateMachine->OnPlaybackEvent().Connect(
+ mAbstractMainThread, this, &MediaDecoder::OnPlaybackEvent);
+ mOnPlaybackErrorEvent = mDecoderStateMachine->OnPlaybackErrorEvent().Connect(
+ mAbstractMainThread, this, &MediaDecoder::OnPlaybackErrorEvent);
+ mOnDecoderDoctorEvent = mDecoderStateMachine->OnDecoderDoctorEvent().Connect(
+ mAbstractMainThread, this, &MediaDecoder::OnDecoderDoctorEvent);
+ mOnMediaNotSeekable = mDecoderStateMachine->OnMediaNotSeekable().Connect(
+ mAbstractMainThread, this, &MediaDecoder::OnMediaNotSeekable);
+ mOnNextFrameStatus = mDecoderStateMachine->OnNextFrameStatus().Connect(
+ mAbstractMainThread, this, &MediaDecoder::OnNextFrameStatus);
+ mOnTrackInfoUpdated = mDecoderStateMachine->OnTrackInfoUpdatedEvent().Connect(
+ mAbstractMainThread, this, &MediaDecoder::OnTrackInfoUpdated);
+ mOnSecondaryVideoContainerInstalled =
+ mDecoderStateMachine->OnSecondaryVideoContainerInstalled().Connect(
+ mAbstractMainThread, this,
+ &MediaDecoder::OnSecondaryVideoContainerInstalled);
+ mOnStoreDecoderBenchmark = mReader->OnStoreDecoderBenchmark().Connect(
+ mAbstractMainThread, this, &MediaDecoder::OnStoreDecoderBenchmark);
+
+ mOnEncrypted = mReader->OnEncrypted().Connect(
+ mAbstractMainThread, GetOwner(), &MediaDecoderOwner::DispatchEncrypted);
+ mOnWaitingForKey = mReader->OnWaitingForKey().Connect(
+ mAbstractMainThread, GetOwner(), &MediaDecoderOwner::NotifyWaitingForKey);
+ mOnDecodeWarning = mReader->OnDecodeWarning().Connect(
+ mAbstractMainThread, GetOwner(), &MediaDecoderOwner::DecodeWarning);
+}
+
+void MediaDecoder::DisconnectEvents() {
+ MOZ_ASSERT(NS_IsMainThread());
+ mTimedMetadataListener.Disconnect();
+ mMetadataLoadedListener.Disconnect();
+ mFirstFrameLoadedListener.Disconnect();
+ mOnPlaybackEvent.Disconnect();
+ mOnPlaybackErrorEvent.Disconnect();
+ mOnDecoderDoctorEvent.Disconnect();
+ mOnMediaNotSeekable.Disconnect();
+ mOnEncrypted.Disconnect();
+ mOnWaitingForKey.Disconnect();
+ mOnDecodeWarning.Disconnect();
+ mOnNextFrameStatus.Disconnect();
+ mOnTrackInfoUpdated.Disconnect();
+ mOnSecondaryVideoContainerInstalled.Disconnect();
+ mOnStoreDecoderBenchmark.Disconnect();
+}
+
+RefPtr<ShutdownPromise> MediaDecoder::ShutdownStateMachine() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(GetStateMachine());
+ DisconnectEvents();
+ return mDecoderStateMachine->BeginShutdown();
+}
+
+void MediaDecoder::Play() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ NS_ASSERTION(mDecoderStateMachine != nullptr, "Should have state machine.");
+ LOG("Play");
+ if (mPlaybackRate == 0) {
+ return;
+ }
+
+ if (IsEnded()) {
+ Seek(0, SeekTarget::PrevSyncPoint);
+ return;
+ }
+
+ if (mPlayState == PLAY_STATE_LOADING) {
+ mNextState = PLAY_STATE_PLAYING;
+ return;
+ }
+
+ ChangeState(PLAY_STATE_PLAYING);
+}
+
+void MediaDecoder::Seek(double aTime, SeekTarget::Type aSeekType) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
+
+ MOZ_ASSERT(aTime >= 0.0, "Cannot seek to a negative value.");
+
+ LOG("Seek");
+ auto time = TimeUnit::FromSeconds(aTime);
+
+ mLogicalPosition = aTime;
+ mLogicallySeeking = true;
+ SeekTarget target = SeekTarget(time, aSeekType);
+ CallSeek(target);
+
+ if (mPlayState == PLAY_STATE_ENDED) {
+ ChangeState(GetOwner()->GetPaused() ? PLAY_STATE_PAUSED
+ : PLAY_STATE_PLAYING);
+ }
+}
+
+void MediaDecoder::SetDelaySeekMode(bool aShouldDelaySeek) {
+ MOZ_ASSERT(NS_IsMainThread());
+ LOG("SetDelaySeekMode, shouldDelaySeek=%d", aShouldDelaySeek);
+ if (mShouldDelaySeek == aShouldDelaySeek) {
+ return;
+ }
+ mShouldDelaySeek = aShouldDelaySeek;
+ if (!mShouldDelaySeek && mDelayedSeekTarget) {
+ Seek(mDelayedSeekTarget->GetTime().ToSeconds(),
+ mDelayedSeekTarget->GetType());
+ mDelayedSeekTarget.reset();
+ }
+}
+
+void MediaDecoder::DiscardOngoingSeekIfExists() {
+ MOZ_ASSERT(NS_IsMainThread());
+ mSeekRequest.DisconnectIfExists();
+}
+
+void MediaDecoder::CallSeek(const SeekTarget& aTarget) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mShouldDelaySeek) {
+ LOG("Delay seek to %f and store it to delayed seek target",
+ mDelayedSeekTarget->GetTime().ToSeconds());
+ mDelayedSeekTarget = Some(aTarget);
+ return;
+ }
+ DiscardOngoingSeekIfExists();
+ mDecoderStateMachine->InvokeSeek(aTarget)
+ ->Then(mAbstractMainThread, __func__, this, &MediaDecoder::OnSeekResolved,
+ &MediaDecoder::OnSeekRejected)
+ ->Track(mSeekRequest);
+}
+
+double MediaDecoder::GetCurrentTime() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mLogicalPosition;
+}
+
+void MediaDecoder::OnMetadataUpdate(TimedMetadata&& aMetadata) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MetadataLoaded(MakeUnique<MediaInfo>(*aMetadata.mInfo),
+ UniquePtr<MetadataTags>(std::move(aMetadata.mTags)),
+ MediaDecoderEventVisibility::Observable);
+ FirstFrameLoaded(std::move(aMetadata.mInfo),
+ MediaDecoderEventVisibility::Observable);
+}
+
+void MediaDecoder::MetadataLoaded(
+ UniquePtr<MediaInfo> aInfo, UniquePtr<MetadataTags> aTags,
+ MediaDecoderEventVisibility aEventVisibility) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
+
+ LOG("MetadataLoaded, channels=%u rate=%u hasAudio=%d hasVideo=%d",
+ aInfo->mAudio.mChannels, aInfo->mAudio.mRate, aInfo->HasAudio(),
+ aInfo->HasVideo());
+
+ mMediaSeekable = aInfo->mMediaSeekable;
+ mMediaSeekableOnlyInBufferedRanges =
+ aInfo->mMediaSeekableOnlyInBufferedRanges;
+ mInfo = std::move(aInfo);
+
+ mTelemetryProbesReporter->OnMediaContentChanged(
+ TelemetryProbesReporter::MediaInfoToMediaContent(*mInfo));
+
+ // Make sure the element and the frame (if any) are told about
+ // our new size.
+ if (aEventVisibility != MediaDecoderEventVisibility::Suppressed) {
+ mFiredMetadataLoaded = true;
+ GetOwner()->MetadataLoaded(mInfo.get(), std::move(aTags));
+ }
+ // Invalidate() will end up calling GetOwner()->UpdateMediaSize with the last
+ // dimensions retrieved from the video frame container. The video frame
+ // container contains more up to date dimensions than aInfo.
+ // So we call Invalidate() after calling GetOwner()->MetadataLoaded to ensure
+ // the media element has the latest dimensions.
+ Invalidate();
+
+#ifdef MOZ_WMF_MEDIA_ENGINE
+ if (mPendingStatusUpdateForNewlyCreatedStateMachine) {
+ mPendingStatusUpdateForNewlyCreatedStateMachine = false;
+ LOG("Set pending statuses if necessary (mLogicallySeeking=%d, "
+ "mLogicalPosition=%f, mPlaybackRate=%f)",
+ mLogicallySeeking.Ref(), mLogicalPosition, mPlaybackRate);
+ if (mLogicalPosition != 0) {
+ Seek(mLogicalPosition, SeekTarget::Accurate);
+ }
+ if (mPlaybackRate != 0 && mPlaybackRate != 1.0) {
+ mDecoderStateMachine->DispatchSetPlaybackRate(mPlaybackRate);
+ }
+ }
+#endif
+
+ EnsureTelemetryReported();
+}
+
+void MediaDecoder::EnsureTelemetryReported() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mTelemetryReported || !mInfo) {
+ // Note: sometimes we get multiple MetadataLoaded calls (for example
+ // for chained ogg). So we ensure we don't report duplicate results for
+ // these resources.
+ return;
+ }
+
+ nsTArray<nsCString> codecs;
+ if (mInfo->HasAudio() &&
+ !mInfo->mAudio.GetAsAudioInfo()->mMimeType.IsEmpty()) {
+ codecs.AppendElement(mInfo->mAudio.GetAsAudioInfo()->mMimeType);
+ }
+ if (mInfo->HasVideo() &&
+ !mInfo->mVideo.GetAsVideoInfo()->mMimeType.IsEmpty()) {
+ codecs.AppendElement(mInfo->mVideo.GetAsVideoInfo()->mMimeType);
+ }
+ if (codecs.IsEmpty()) {
+ codecs.AppendElement(nsPrintfCString(
+ "resource; %s", ContainerType().OriginalString().Data()));
+ }
+ for (const nsCString& codec : codecs) {
+ LOG("Telemetry MEDIA_CODEC_USED= '%s'", codec.get());
+ Telemetry::Accumulate(Telemetry::HistogramID::MEDIA_CODEC_USED, codec);
+ }
+
+ mTelemetryReported = true;
+}
+
+const char* MediaDecoder::PlayStateStr() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return ToPlayStateStr(mPlayState);
+}
+
+void MediaDecoder::FirstFrameLoaded(
+ UniquePtr<MediaInfo> aInfo, MediaDecoderEventVisibility aEventVisibility) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
+
+ LOG("FirstFrameLoaded, channels=%u rate=%u hasAudio=%d hasVideo=%d "
+ "mPlayState=%s transportSeekable=%d",
+ aInfo->mAudio.mChannels, aInfo->mAudio.mRate, aInfo->HasAudio(),
+ aInfo->HasVideo(), PlayStateStr(), IsTransportSeekable());
+
+ mInfo = std::move(aInfo);
+ mTelemetryProbesReporter->OnMediaContentChanged(
+ TelemetryProbesReporter::MediaInfoToMediaContent(*mInfo));
+
+ Invalidate();
+
+ // The element can run javascript via events
+ // before reaching here, so only change the
+ // state if we're still set to the original
+ // loading state.
+ if (mPlayState == PLAY_STATE_LOADING) {
+ ChangeState(mNextState);
+ }
+
+ // GetOwner()->FirstFrameLoaded() might call us back. Put it at the bottom of
+ // this function to avoid unexpected shutdown from reentrant calls.
+ if (aEventVisibility != MediaDecoderEventVisibility::Suppressed) {
+ GetOwner()->FirstFrameLoaded();
+ }
+}
+
+void MediaDecoder::NetworkError(const MediaResult& aError) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
+ GetOwner()->NetworkError(aError);
+}
+
+void MediaDecoder::DecodeError(const MediaResult& aError) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
+ GetOwner()->DecodeError(aError);
+}
+
+void MediaDecoder::UpdateSameOriginStatus(bool aSameOrigin) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mSameOriginMedia = aSameOrigin;
+}
+
+bool MediaDecoder::IsSeeking() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mLogicallySeeking;
+}
+
+bool MediaDecoder::OwnerHasError() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
+ return GetOwner()->HasError();
+}
+
+bool MediaDecoder::IsEnded() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mPlayState == PLAY_STATE_ENDED;
+}
+
+bool MediaDecoder::IsShutdown() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mPlayState == PLAY_STATE_SHUTDOWN;
+}
+
+void MediaDecoder::PlaybackEnded() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
+
+ if (mLogicallySeeking || mPlayState == PLAY_STATE_LOADING ||
+ mPlayState == PLAY_STATE_ENDED) {
+ LOG("MediaDecoder::PlaybackEnded bailed out, "
+ "mLogicallySeeking=%d mPlayState=%s",
+ mLogicallySeeking.Ref(), ToPlayStateStr(mPlayState));
+ return;
+ }
+
+ LOG("MediaDecoder::PlaybackEnded");
+
+ ChangeState(PLAY_STATE_ENDED);
+ InvalidateWithFlags(VideoFrameContainer::INVALIDATE_FORCE);
+ GetOwner()->PlaybackEnded();
+}
+
+void MediaDecoder::NotifyPrincipalChanged() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
+ GetOwner()->NotifyDecoderPrincipalChanged();
+}
+
+void MediaDecoder::OnSeekResolved() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
+ LOG("MediaDecoder::OnSeekResolved");
+ mLogicallySeeking = false;
+
+ // Ensure logical position is updated after seek.
+ UpdateLogicalPositionInternal();
+ mSeekRequest.Complete();
+
+ GetOwner()->SeekCompleted();
+}
+
+void MediaDecoder::OnSeekRejected() {
+ MOZ_ASSERT(NS_IsMainThread());
+ LOG("MediaDecoder::OnSeekRejected");
+ mSeekRequest.Complete();
+ mLogicallySeeking = false;
+
+ GetOwner()->SeekAborted();
+}
+
+void MediaDecoder::SeekingStarted() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
+ GetOwner()->SeekStarted();
+}
+
+void MediaDecoder::ChangeState(PlayState aState) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!IsShutdown(), "SHUTDOWN is the final state.");
+
+ if (mNextState == aState) {
+ mNextState = PLAY_STATE_PAUSED;
+ }
+
+ if (mPlayState != aState) {
+ DDLOG(DDLogCategory::Property, "play_state", ToPlayStateStr(aState));
+ LOG("Play state changes from %s to %s", ToPlayStateStr(mPlayState),
+ ToPlayStateStr(aState));
+ mPlayState = aState;
+ UpdateTelemetryHelperBasedOnPlayState(aState);
+ }
+}
+
+TelemetryProbesReporter::Visibility MediaDecoder::OwnerVisibility() const {
+ return GetOwner()->IsActuallyInvisible() || mForcedHidden
+ ? TelemetryProbesReporter::Visibility::eInvisible
+ : TelemetryProbesReporter::Visibility::eVisible;
+}
+
+void MediaDecoder::UpdateTelemetryHelperBasedOnPlayState(
+ PlayState aState) const {
+ if (aState == PlayState::PLAY_STATE_PLAYING) {
+ mTelemetryProbesReporter->OnPlay(
+ OwnerVisibility(),
+ TelemetryProbesReporter::MediaInfoToMediaContent(*mInfo),
+ mVolume == 0.f);
+ } else if (aState == PlayState::PLAY_STATE_PAUSED ||
+ aState == PlayState::PLAY_STATE_ENDED) {
+ mTelemetryProbesReporter->OnPause(OwnerVisibility());
+ } else if (aState == PLAY_STATE_SHUTDOWN) {
+ mTelemetryProbesReporter->OnShutdown();
+ }
+}
+
+MediaDecoder::PositionUpdate MediaDecoder::GetPositionUpdateReason(
+ double aPrevPos, const TimeUnit& aCurPos) const {
+ MOZ_ASSERT(NS_IsMainThread());
+ // If current position is earlier than previous position and we didn't do
+ // seek, that means we looped back to the start position.
+ const bool notSeeking = !mSeekRequest.Exists();
+ if (mLooping && notSeeking && aCurPos.ToSeconds() < aPrevPos) {
+ return PositionUpdate::eSeamlessLoopingSeeking;
+ }
+ return aPrevPos != aCurPos.ToSeconds() && notSeeking
+ ? PositionUpdate::ePeriodicUpdate
+ : PositionUpdate::eOther;
+}
+
+void MediaDecoder::UpdateLogicalPositionInternal() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
+
+ TimeUnit currentPosition = CurrentPosition();
+ if (mPlayState == PLAY_STATE_ENDED) {
+ currentPosition =
+ std::max(currentPosition, mDuration.match(DurationToTimeUnit()));
+ }
+
+ const PositionUpdate reason =
+ GetPositionUpdateReason(mLogicalPosition, currentPosition);
+ switch (reason) {
+ case PositionUpdate::ePeriodicUpdate:
+ SetLogicalPosition(currentPosition);
+ // This is actually defined in `TimeMarchesOn`, but we do that in decoder.
+ // https://html.spec.whatwg.org/multipage/media.html#playing-the-media-resource:event-media-timeupdate-7
+ // TODO (bug 1688137): should we move it back to `TimeMarchesOn`?
+ GetOwner()->MaybeQueueTimeupdateEvent();
+ break;
+ case PositionUpdate::eSeamlessLoopingSeeking:
+ // When seamless seeking occurs, seeking was performed on the demuxer so
+ // the decoder doesn't know. That means decoder still thinks it's in
+ // playing. Therefore, we have to manually call those methods to notify
+ // the owner about seeking.
+ GetOwner()->SeekStarted();
+ SetLogicalPosition(currentPosition);
+ GetOwner()->SeekCompleted();
+ break;
+ default:
+ MOZ_ASSERT(reason == PositionUpdate::eOther);
+ SetLogicalPosition(currentPosition);
+ break;
+ }
+
+ // Invalidate the frame so any video data is displayed.
+ // Do this before the timeupdate event so that if that
+ // event runs JavaScript that queries the media size, the
+ // frame has reflowed and the size updated beforehand.
+ Invalidate();
+}
+
+void MediaDecoder::SetLogicalPosition(const TimeUnit& aNewPosition) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (TimeUnit::FromSeconds(mLogicalPosition) == aNewPosition ||
+ mLogicalPosition == aNewPosition.ToSeconds()) {
+ return;
+ }
+ mLogicalPosition = aNewPosition.ToSeconds();
+ DDLOG(DDLogCategory::Property, "currentTime", mLogicalPosition);
+}
+
+void MediaDecoder::DurationChanged() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
+
+ Variant<TimeUnit, double> oldDuration = mDuration;
+
+ // Use the explicit duration if we have one.
+ // Otherwise use the duration mirrored from MDSM.
+ if (mExplicitDuration.isSome()) {
+ mDuration.emplace<double>(mExplicitDuration.ref());
+ } else if (mStateMachineDuration.Ref().isSome()) {
+ MOZ_ASSERT(mStateMachineDuration.Ref().ref().IsValid());
+ mDuration.emplace<TimeUnit>(mStateMachineDuration.Ref().ref());
+ }
+
+ LOG("New duration: %s",
+ mDuration.match(DurationToTimeUnit()).ToString().get());
+ if (oldDuration.is<TimeUnit>() && oldDuration.as<TimeUnit>().IsValid()) {
+ LOG("Old Duration %s",
+ oldDuration.match(DurationToTimeUnit()).ToString().get());
+ }
+
+ if ((oldDuration.is<double>() || oldDuration.as<TimeUnit>().IsValid())) {
+ if (mDuration.match(DurationToDouble()) ==
+ oldDuration.match(DurationToDouble())) {
+ return;
+ }
+ }
+
+ LOG("Duration changed to %s",
+ mDuration.match(DurationToTimeUnit()).ToString().get());
+
+ // See https://www.w3.org/Bugs/Public/show_bug.cgi?id=28822 for a discussion
+ // of whether we should fire durationchange on explicit infinity.
+ if (mFiredMetadataLoaded &&
+ (!std::isinf(mDuration.match(DurationToDouble())) ||
+ mExplicitDuration.isSome())) {
+ GetOwner()->DispatchAsyncEvent(u"durationchange"_ns);
+ }
+
+ if (CurrentPosition().ToSeconds() > mDuration.match(DurationToDouble())) {
+ Seek(mDuration.match(DurationToDouble()), SeekTarget::Accurate);
+ }
+}
+
+already_AddRefed<KnowsCompositor> MediaDecoder::GetCompositor() {
+ MediaDecoderOwner* owner = GetOwner();
+ Document* ownerDoc = owner ? owner->GetDocument() : nullptr;
+ WindowRenderer* renderer =
+ ownerDoc ? nsContentUtils::WindowRendererForDocument(ownerDoc) : nullptr;
+ RefPtr<KnowsCompositor> knows =
+ renderer ? renderer->AsKnowsCompositor() : nullptr;
+ return knows ? knows->GetForMedia().forget() : nullptr;
+}
+
+void MediaDecoder::NotifyCompositor() {
+ RefPtr<KnowsCompositor> knowsCompositor = GetCompositor();
+ if (knowsCompositor) {
+ nsCOMPtr<nsIRunnable> r =
+ NewRunnableMethod<already_AddRefed<KnowsCompositor>&&>(
+ "MediaFormatReader::UpdateCompositor", mReader,
+ &MediaFormatReader::UpdateCompositor, knowsCompositor.forget());
+ Unused << mReader->OwnerThread()->Dispatch(r.forget());
+ }
+}
+
+void MediaDecoder::SetElementVisibility(bool aIsOwnerInvisible,
+ bool aIsOwnerConnected) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mIsOwnerInvisible = aIsOwnerInvisible;
+ mIsOwnerConnected = aIsOwnerConnected;
+ mTelemetryProbesReporter->OnVisibilityChanged(OwnerVisibility());
+ UpdateVideoDecodeMode();
+}
+
+void MediaDecoder::SetForcedHidden(bool aForcedHidden) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mForcedHidden = aForcedHidden;
+ mTelemetryProbesReporter->OnVisibilityChanged(OwnerVisibility());
+ UpdateVideoDecodeMode();
+}
+
+void MediaDecoder::SetSuspendTaint(bool aTainted) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mHasSuspendTaint = aTainted;
+ UpdateVideoDecodeMode();
+}
+
+void MediaDecoder::UpdateVideoDecodeMode() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // The MDSM may yet be set.
+ if (!mDecoderStateMachine) {
+ LOG("UpdateVideoDecodeMode(), early return because we don't have MDSM.");
+ return;
+ }
+
+ // Seeking is required when leaving suspend mode.
+ if (!mMediaSeekable) {
+ LOG("UpdateVideoDecodeMode(), set Normal because the media is not "
+ "seekable");
+ mDecoderStateMachine->SetVideoDecodeMode(VideoDecodeMode::Normal);
+ return;
+ }
+
+ // If mHasSuspendTaint is set, never suspend the video decoder.
+ if (mHasSuspendTaint) {
+ LOG("UpdateVideoDecodeMode(), set Normal because the element has been "
+ "tainted.");
+ mDecoderStateMachine->SetVideoDecodeMode(VideoDecodeMode::Normal);
+ return;
+ }
+
+ // If mSecondaryVideoContainer is set, never suspend the video decoder.
+ if (mSecondaryVideoContainer.Ref()) {
+ LOG("UpdateVideoDecodeMode(), set Normal because the element is cloning "
+ "itself visually to another video container.");
+ mDecoderStateMachine->SetVideoDecodeMode(VideoDecodeMode::Normal);
+ return;
+ }
+
+ // Don't suspend elements that is not in a connected tree.
+ if (!mIsOwnerConnected) {
+ LOG("UpdateVideoDecodeMode(), set Normal because the element is not in "
+ "tree.");
+ mDecoderStateMachine->SetVideoDecodeMode(VideoDecodeMode::Normal);
+ return;
+ }
+
+ // If mForcedHidden is set, suspend the video decoder anyway.
+ if (mForcedHidden) {
+ LOG("UpdateVideoDecodeMode(), set Suspend because the element is forced to "
+ "be suspended.");
+ mDecoderStateMachine->SetVideoDecodeMode(VideoDecodeMode::Suspend);
+ return;
+ }
+
+ // Resume decoding in the advance, even the element is in the background.
+ if (mIsBackgroundVideoDecodingAllowed) {
+ LOG("UpdateVideoDecodeMode(), set Normal because the tab is in background "
+ "and hovered.");
+ mDecoderStateMachine->SetVideoDecodeMode(VideoDecodeMode::Normal);
+ return;
+ }
+
+ if (mIsOwnerInvisible) {
+ LOG("UpdateVideoDecodeMode(), set Suspend because of invisible element.");
+ mDecoderStateMachine->SetVideoDecodeMode(VideoDecodeMode::Suspend);
+ } else {
+ LOG("UpdateVideoDecodeMode(), set Normal because of visible element.");
+ mDecoderStateMachine->SetVideoDecodeMode(VideoDecodeMode::Normal);
+ }
+}
+
+void MediaDecoder::SetIsBackgroundVideoDecodingAllowed(bool aAllowed) {
+ mIsBackgroundVideoDecodingAllowed = aAllowed;
+ UpdateVideoDecodeMode();
+}
+
+bool MediaDecoder::HasSuspendTaint() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mHasSuspendTaint;
+}
+
+void MediaDecoder::SetSecondaryVideoContainer(
+ const RefPtr<VideoFrameContainer>& aSecondaryVideoContainer) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mSecondaryVideoContainer.Ref() == aSecondaryVideoContainer) {
+ return;
+ }
+ mSecondaryVideoContainer = aSecondaryVideoContainer;
+ UpdateVideoDecodeMode();
+}
+
+bool MediaDecoder::IsMediaSeekable() {
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_TRUE(GetStateMachine(), false);
+ return mMediaSeekable;
+}
+
+namespace {
+
+// Returns zero, either as a TimeUnit or as a double.
+template <typename T>
+constexpr T Zero() {
+ if constexpr (std::is_same<T, double>::value) {
+ return 0.0;
+ } else if constexpr (std::is_same<T, TimeUnit>::value) {
+ return TimeUnit::Zero();
+ }
+ MOZ_RELEASE_ASSERT(false);
+};
+
+// Returns Infinity either as a TimeUnit or as a double.
+template <typename T>
+constexpr T Infinity() {
+ if constexpr (std::is_same<T, double>::value) {
+ return std::numeric_limits<double>::infinity();
+ } else if constexpr (std::is_same<T, TimeUnit>::value) {
+ return TimeUnit::FromInfinity();
+ }
+ MOZ_RELEASE_ASSERT(false);
+};
+
+}; // namespace
+
+// This method can be made to return either TimeIntervals, that is a set of
+// interval that are delimited with TimeUnit, or TimeRanges, that is a set of
+// intervals that are delimited by seconds, as doubles.
+// seekable often depends on the duration of a media, in the very common case
+// where the seekable range is [0, duration]. When playing a MediaSource, the
+// duration of a media element can be set as an arbitrary number, that are
+// 64-bits floating point values.
+// This allows returning an interval that is [0, duration], with duration being
+// a double that cannot be represented as a TimeUnit, either because it has too
+// many significant digits, or because it's outside of the int64_t range that
+// TimeUnit internally uses.
+template <typename IntervalType>
+IntervalType MediaDecoder::GetSeekableImpl() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (std::isnan(GetDuration())) {
+ // We do not have a duration yet, we can't determine the seekable range.
+ return IntervalType();
+ }
+
+ // Compute [0, duration] -- When dealing with doubles, use ::GetDuration to
+ // avoid rounding the value differently. When dealing with TimeUnit, it's
+ // returned directly.
+ typename IntervalType::InnerType duration;
+ if constexpr (std::is_same<typename IntervalType::InnerType, double>::value) {
+ duration = GetDuration();
+ } else {
+ duration = mDuration.as<TimeUnit>();
+ }
+ typename IntervalType::ElemType zeroToDuration =
+ typename IntervalType::ElemType(
+ Zero<typename IntervalType::InnerType>(),
+ IsInfinite() ? Infinity<typename IntervalType::InnerType>()
+ : duration);
+ auto buffered = IntervalType(GetBuffered());
+ // Remove any negative range in the interval -- seeking to a non-positive
+ // position isn't possible.
+ auto positiveBuffered = buffered.Intersection(zeroToDuration);
+
+ // We can seek in buffered range if the media is seekable. Also, we can seek
+ // in unbuffered ranges if the transport level is seekable (local file or the
+ // server supports range requests, etc.) or in cue-less WebMs
+ if (mMediaSeekableOnlyInBufferedRanges) {
+ return IntervalType(positiveBuffered);
+ }
+ if (!IsMediaSeekable()) {
+ return IntervalType();
+ }
+ if (!IsTransportSeekable()) {
+ return IntervalType(positiveBuffered);
+ }
+
+ // Common case: seeking is possible at any point of the stream.
+ return IntervalType(zeroToDuration);
+}
+
+media::TimeIntervals MediaDecoder::GetSeekable() {
+ return GetSeekableImpl<media::TimeIntervals>();
+}
+
+media::TimeRanges MediaDecoder::GetSeekableTimeRanges() {
+ return GetSeekableImpl<media::TimeRanges>();
+}
+
+void MediaDecoder::SetFragmentEndTime(double aTime) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mDecoderStateMachine) {
+ mDecoderStateMachine->DispatchSetFragmentEndTime(
+ TimeUnit::FromSeconds(aTime));
+ }
+}
+
+void MediaDecoder::SetPlaybackRate(double aPlaybackRate) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ double oldRate = mPlaybackRate;
+ mPlaybackRate = aPlaybackRate;
+ if (aPlaybackRate == 0) {
+ Pause();
+ return;
+ }
+
+ if (oldRate == 0 && !GetOwner()->GetPaused()) {
+ // PlaybackRate is no longer null.
+ // Restart the playback if the media was playing.
+ Play();
+ }
+
+ if (mDecoderStateMachine) {
+ mDecoderStateMachine->DispatchSetPlaybackRate(aPlaybackRate);
+ }
+}
+
+void MediaDecoder::SetPreservesPitch(bool aPreservesPitch) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mPreservesPitch = aPreservesPitch;
+}
+
+void MediaDecoder::SetLooping(bool aLooping) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mLooping = aLooping;
+}
+
+void MediaDecoder::SetStreamName(const nsAutoString& aStreamName) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mStreamName = aStreamName;
+}
+
+void MediaDecoder::ConnectMirrors(MediaDecoderStateMachineBase* aObject) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aObject);
+ mStateMachineDuration.Connect(aObject->CanonicalDuration());
+ mBuffered.Connect(aObject->CanonicalBuffered());
+ mCurrentPosition.Connect(aObject->CanonicalCurrentPosition());
+ mIsAudioDataAudible.Connect(aObject->CanonicalIsAudioDataAudible());
+}
+
+void MediaDecoder::DisconnectMirrors() {
+ MOZ_ASSERT(NS_IsMainThread());
+ mStateMachineDuration.DisconnectIfConnected();
+ mBuffered.DisconnectIfConnected();
+ mCurrentPosition.DisconnectIfConnected();
+ mIsAudioDataAudible.DisconnectIfConnected();
+}
+
+void MediaDecoder::SetStateMachine(
+ MediaDecoderStateMachineBase* aStateMachine) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT_IF(aStateMachine, !mDecoderStateMachine);
+ if (aStateMachine) {
+ mDecoderStateMachine = aStateMachine;
+ LOG("set state machine %p", mDecoderStateMachine.get());
+ ConnectMirrors(aStateMachine);
+ UpdateVideoDecodeMode();
+ } else if (mDecoderStateMachine) {
+ LOG("null out state machine %p", mDecoderStateMachine.get());
+ mDecoderStateMachine = nullptr;
+ DisconnectMirrors();
+ }
+}
+
+ImageContainer* MediaDecoder::GetImageContainer() {
+ return mVideoFrameContainer ? mVideoFrameContainer->GetImageContainer()
+ : nullptr;
+}
+
+void MediaDecoder::InvalidateWithFlags(uint32_t aFlags) {
+ if (mVideoFrameContainer) {
+ mVideoFrameContainer->InvalidateWithFlags(aFlags);
+ }
+}
+
+void MediaDecoder::Invalidate() {
+ if (mVideoFrameContainer) {
+ mVideoFrameContainer->Invalidate();
+ }
+}
+
+void MediaDecoder::Suspend() {
+ MOZ_ASSERT(NS_IsMainThread());
+ GetStateMachine()->InvokeSuspendMediaSink();
+}
+
+void MediaDecoder::Resume() {
+ MOZ_ASSERT(NS_IsMainThread());
+ GetStateMachine()->InvokeResumeMediaSink();
+}
+
+// Constructs the time ranges representing what segments of the media
+// are buffered and playable.
+media::TimeIntervals MediaDecoder::GetBuffered() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mBuffered.Ref();
+}
+
+size_t MediaDecoder::SizeOfVideoQueue() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mDecoderStateMachine) {
+ return mDecoderStateMachine->SizeOfVideoQueue();
+ }
+ return 0;
+}
+
+size_t MediaDecoder::SizeOfAudioQueue() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mDecoderStateMachine) {
+ return mDecoderStateMachine->SizeOfAudioQueue();
+ }
+ return 0;
+}
+
+void MediaDecoder::NotifyReaderDataArrived() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
+
+ nsresult rv = mReader->OwnerThread()->Dispatch(
+ NewRunnableMethod("MediaFormatReader::NotifyDataArrived", mReader.get(),
+ &MediaFormatReader::NotifyDataArrived));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+}
+
+// Provide access to the state machine object
+MediaDecoderStateMachineBase* MediaDecoder::GetStateMachine() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mDecoderStateMachine;
+}
+
+bool MediaDecoder::CanPlayThrough() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
+ return CanPlayThroughImpl();
+}
+
+RefPtr<SetCDMPromise> MediaDecoder::SetCDMProxy(CDMProxy* aProxy) {
+ MOZ_ASSERT(NS_IsMainThread());
+#ifdef MOZ_WMF_MEDIA_ENGINE
+ // Switch to another state machine if the current one doesn't support the
+ // given CDM proxy.
+ if (aProxy && !GetStateMachine()->IsCDMProxySupported(aProxy)) {
+ LOG("CDM proxy not supported! Switch to another state machine.");
+ OnPlaybackErrorEvent(
+ MediaResult{NS_ERROR_DOM_MEDIA_CDM_PROXY_NOT_SUPPORTED_ERR, aProxy});
+ }
+#endif
+ MOZ_DIAGNOSTIC_ASSERT_IF(aProxy,
+ GetStateMachine()->IsCDMProxySupported(aProxy));
+ return GetStateMachine()->SetCDMProxy(aProxy);
+}
+
+bool MediaDecoder::IsOpusEnabled() { return StaticPrefs::media_opus_enabled(); }
+
+bool MediaDecoder::IsOggEnabled() { return StaticPrefs::media_ogg_enabled(); }
+
+bool MediaDecoder::IsWaveEnabled() { return StaticPrefs::media_wave_enabled(); }
+
+bool MediaDecoder::IsWebMEnabled() { return StaticPrefs::media_webm_enabled(); }
+
+NS_IMETHODIMP
+MediaMemoryTracker::CollectReports(nsIHandleReportCallback* aHandleReport,
+ nsISupports* aData, bool aAnonymize) {
+ // NB: When resourceSizes' ref count goes to 0 the promise will report the
+ // resources memory and finish the asynchronous memory report.
+ RefPtr<MediaDecoder::ResourceSizes> resourceSizes =
+ new MediaDecoder::ResourceSizes(MediaMemoryTracker::MallocSizeOf);
+
+ nsCOMPtr<nsIHandleReportCallback> handleReport = aHandleReport;
+ nsCOMPtr<nsISupports> data = aData;
+
+ resourceSizes->Promise()->Then(
+ AbstractThread::MainThread(), __func__,
+ [handleReport, data](size_t size) {
+ handleReport->Callback(
+ ""_ns, "explicit/media/resources"_ns, KIND_HEAP, UNITS_BYTES,
+ static_cast<int64_t>(size),
+ nsLiteralCString("Memory used by media resources including "
+ "streaming buffers, caches, etc."),
+ data);
+
+ nsCOMPtr<nsIMemoryReporterManager> imgr =
+ do_GetService("@mozilla.org/memory-reporter-manager;1");
+
+ if (imgr) {
+ imgr->EndReport();
+ }
+ },
+ [](size_t) { /* unused reject function */ });
+
+ int64_t video = 0;
+ int64_t audio = 0;
+ DecodersArray& decoders = Decoders();
+ for (size_t i = 0; i < decoders.Length(); ++i) {
+ MediaDecoder* decoder = decoders[i];
+ video += static_cast<int64_t>(decoder->SizeOfVideoQueue());
+ audio += static_cast<int64_t>(decoder->SizeOfAudioQueue());
+ decoder->AddSizeOfResources(resourceSizes);
+ }
+
+ MOZ_COLLECT_REPORT("explicit/media/decoded/video", KIND_HEAP, UNITS_BYTES,
+ video, "Memory used by decoded video frames.");
+
+ MOZ_COLLECT_REPORT("explicit/media/decoded/audio", KIND_HEAP, UNITS_BYTES,
+ audio, "Memory used by decoded audio chunks.");
+
+ return NS_OK;
+}
+
+MediaDecoderOwner* MediaDecoder::GetOwner() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ // mOwner is valid until shutdown.
+ return mOwner;
+}
+
+MediaDecoderOwner::NextFrameStatus MediaDecoder::NextFrameBufferedStatus() {
+ MOZ_ASSERT(NS_IsMainThread());
+ // Next frame hasn't been decoded yet.
+ // Use the buffered range to consider if we have the next frame available.
+ auto currentPosition = CurrentPosition();
+ media::TimeInterval interval(
+ currentPosition, currentPosition + DEFAULT_NEXT_FRAME_AVAILABLE_BUFFERED);
+ return GetBuffered().Contains(interval)
+ ? MediaDecoderOwner::NEXT_FRAME_AVAILABLE
+ : MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE;
+}
+
+void MediaDecoder::GetDebugInfo(dom::MediaDecoderDebugInfo& aInfo) {
+ MOZ_ASSERT(NS_IsMainThread());
+ CopyUTF8toUTF16(nsPrintfCString("%p", this), aInfo.mInstance);
+ aInfo.mChannels = mInfo ? mInfo->mAudio.mChannels : 0;
+ aInfo.mRate = mInfo ? mInfo->mAudio.mRate : 0;
+ aInfo.mHasAudio = mInfo ? mInfo->HasAudio() : false;
+ aInfo.mHasVideo = mInfo ? mInfo->HasVideo() : false;
+ CopyUTF8toUTF16(MakeStringSpan(PlayStateStr()), aInfo.mPlayState);
+ aInfo.mContainerType =
+ NS_ConvertUTF8toUTF16(ContainerType().Type().AsString());
+}
+
+RefPtr<GenericPromise> MediaDecoder::RequestDebugInfo(
+ MediaDecoderDebugInfo& aInfo) {
+ MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
+ if (!NS_IsMainThread()) {
+ // Run the request on the main thread if it's not already.
+ return InvokeAsync(AbstractThread::MainThread(), __func__,
+ [this, self = RefPtr{this}, &aInfo]() {
+ return RequestDebugInfo(aInfo);
+ });
+ }
+ GetDebugInfo(aInfo);
+
+ return mReader->RequestDebugInfo(aInfo.mReader)
+ ->Then(AbstractThread::MainThread(), __func__,
+ [this, self = RefPtr{this}, &aInfo] {
+ if (!GetStateMachine()) {
+ return GenericPromise::CreateAndResolve(true, __func__);
+ }
+ return GetStateMachine()->RequestDebugInfo(aInfo.mStateMachine);
+ });
+}
+
+void MediaDecoder::NotifyAudibleStateChanged() {
+ MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
+ GetOwner()->SetAudibleState(mIsAudioDataAudible);
+ mTelemetryProbesReporter->OnAudibleChanged(
+ mIsAudioDataAudible ? TelemetryProbesReporter::AudibleState::eAudible
+ : TelemetryProbesReporter::AudibleState::eNotAudible);
+}
+
+void MediaDecoder::NotifyVolumeChanged() {
+ MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
+ mTelemetryProbesReporter->OnMutedChanged(mVolume == 0.f);
+}
+
+double MediaDecoder::GetTotalVideoPlayTimeInSeconds() const {
+ return mTelemetryProbesReporter->GetTotalVideoPlayTimeInSeconds();
+}
+
+double MediaDecoder::GetTotalVideoHDRPlayTimeInSeconds() const {
+ return mTelemetryProbesReporter->GetTotalVideoHDRPlayTimeInSeconds();
+}
+
+double MediaDecoder::GetVisibleVideoPlayTimeInSeconds() const {
+ return mTelemetryProbesReporter->GetVisibleVideoPlayTimeInSeconds();
+}
+
+double MediaDecoder::GetInvisibleVideoPlayTimeInSeconds() const {
+ return mTelemetryProbesReporter->GetInvisibleVideoPlayTimeInSeconds();
+}
+
+double MediaDecoder::GetVideoDecodeSuspendedTimeInSeconds() const {
+ return mTelemetryProbesReporter->GetVideoDecodeSuspendedTimeInSeconds();
+}
+
+double MediaDecoder::GetTotalAudioPlayTimeInSeconds() const {
+ return mTelemetryProbesReporter->GetTotalAudioPlayTimeInSeconds();
+}
+
+double MediaDecoder::GetAudiblePlayTimeInSeconds() const {
+ return mTelemetryProbesReporter->GetAudiblePlayTimeInSeconds();
+}
+
+double MediaDecoder::GetInaudiblePlayTimeInSeconds() const {
+ return mTelemetryProbesReporter->GetInaudiblePlayTimeInSeconds();
+}
+
+double MediaDecoder::GetMutedPlayTimeInSeconds() const {
+ return mTelemetryProbesReporter->GetMutedPlayTimeInSeconds();
+}
+
+MediaMemoryTracker::MediaMemoryTracker() = default;
+
+void MediaMemoryTracker::InitMemoryReporter() {
+ RegisterWeakAsyncMemoryReporter(this);
+}
+
+MediaMemoryTracker::~MediaMemoryTracker() {
+ UnregisterWeakMemoryReporter(this);
+}
+
+} // namespace mozilla
+
+// avoid redefined macro in unified build
+#undef DUMP
+#undef LOG
+#undef NS_DispatchToMainThread
diff --git a/dom/media/MediaDecoder.h b/dom/media/MediaDecoder.h
new file mode 100644
index 0000000000..899be81da4
--- /dev/null
+++ b/dom/media/MediaDecoder.h
@@ -0,0 +1,836 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(MediaDecoder_h_)
+# define MediaDecoder_h_
+
+# include "BackgroundVideoDecodingPermissionObserver.h"
+# include "DecoderDoctorDiagnostics.h"
+# include "MediaContainerType.h"
+# include "MediaDecoderOwner.h"
+# include "MediaEventSource.h"
+# include "MediaMetadataManager.h"
+# include "MediaPromiseDefs.h"
+# include "MediaResource.h"
+# include "MediaStatistics.h"
+# include "SeekTarget.h"
+# include "TelemetryProbesReporter.h"
+# include "TimeUnits.h"
+# include "mozilla/Atomics.h"
+# include "mozilla/CDMProxy.h"
+# include "mozilla/MozPromise.h"
+# include "mozilla/ReentrantMonitor.h"
+# include "mozilla/StateMirroring.h"
+# include "mozilla/StateWatching.h"
+# include "mozilla/dom/MediaDebugInfoBinding.h"
+# include "nsCOMPtr.h"
+# include "nsIObserver.h"
+# include "nsISupports.h"
+# include "nsITimer.h"
+
+class AudioDeviceInfo;
+class nsIPrincipal;
+
+namespace mozilla {
+
+namespace dom {
+class MediaMemoryInfo;
+}
+
+class AbstractThread;
+class DOMMediaStream;
+class DecoderBenchmark;
+class ProcessedMediaTrack;
+class FrameStatistics;
+class VideoFrameContainer;
+class MediaFormatReader;
+class MediaDecoderStateMachineBase;
+struct MediaPlaybackEvent;
+struct SharedDummyTrack;
+
+template <typename T>
+struct DurationToType {
+ double operator()(double aDouble);
+ double operator()(const media::TimeUnit& aTimeUnit);
+};
+
+template <>
+struct DurationToType<double> {
+ double operator()(double aDouble) { return aDouble; }
+ double operator()(const media::TimeUnit& aTimeUnit) {
+ if (aTimeUnit.IsValid()) {
+ if (aTimeUnit.IsPosInf()) {
+ return std::numeric_limits<double>::infinity();
+ }
+ if (aTimeUnit.IsNegInf()) {
+ return -std::numeric_limits<double>::infinity();
+ }
+ return aTimeUnit.ToSeconds();
+ }
+ return std::numeric_limits<double>::quiet_NaN();
+ }
+};
+
+using DurationToDouble = DurationToType<double>;
+
+template <>
+struct DurationToType<media::TimeUnit> {
+ media::TimeUnit operator()(double aDouble) {
+ return media::TimeUnit::FromSeconds(aDouble);
+ }
+ media::TimeUnit operator()(const media::TimeUnit& aTimeUnit) {
+ return aTimeUnit;
+ }
+};
+
+using DurationToTimeUnit = DurationToType<media::TimeUnit>;
+
+struct MOZ_STACK_CLASS MediaDecoderInit {
+ MediaDecoderOwner* const mOwner;
+ TelemetryProbesReporterOwner* const mReporterOwner;
+ const double mVolume;
+ const bool mPreservesPitch;
+ const double mPlaybackRate;
+ const bool mMinimizePreroll;
+ const bool mHasSuspendTaint;
+ const bool mLooping;
+ const MediaContainerType mContainerType;
+ const nsAutoString mStreamName;
+
+ MediaDecoderInit(MediaDecoderOwner* aOwner,
+ TelemetryProbesReporterOwner* aReporterOwner, double aVolume,
+ bool aPreservesPitch, double aPlaybackRate,
+ bool aMinimizePreroll, bool aHasSuspendTaint, bool aLooping,
+ const MediaContainerType& aContainerType)
+ : mOwner(aOwner),
+ mReporterOwner(aReporterOwner),
+ mVolume(aVolume),
+ mPreservesPitch(aPreservesPitch),
+ mPlaybackRate(aPlaybackRate),
+ mMinimizePreroll(aMinimizePreroll),
+ mHasSuspendTaint(aHasSuspendTaint),
+ mLooping(aLooping),
+ mContainerType(aContainerType) {}
+};
+
+DDLoggedTypeDeclName(MediaDecoder);
+
+class MediaDecoder : public DecoderDoctorLifeLogger<MediaDecoder> {
+ public:
+ typedef MozPromise<bool /* aIgnored */, bool /* aIgnored */,
+ /* IsExclusive = */ true>
+ SeekPromise;
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaDecoder)
+
+ // Enumeration for the valid play states (see mPlayState)
+ enum PlayState {
+ PLAY_STATE_LOADING,
+ PLAY_STATE_PAUSED,
+ PLAY_STATE_PLAYING,
+ PLAY_STATE_ENDED,
+ PLAY_STATE_SHUTDOWN
+ };
+
+ // Must be called exactly once, on the main thread, during startup.
+ static void InitStatics();
+
+ explicit MediaDecoder(MediaDecoderInit& aInit);
+
+ // Returns the container content type of the resource.
+ // Safe to call from any thread.
+ const MediaContainerType& ContainerType() const { return mContainerType; }
+
+ // Cleanup internal data structures. Must be called on the main
+ // thread by the owning object before that object disposes of this object.
+ virtual void Shutdown();
+
+ // Notified by the shutdown manager that XPCOM shutdown has begun.
+ // The decoder should notify its owner to drop the reference to the decoder
+ // to prevent further calls into the decoder.
+ void NotifyXPCOMShutdown();
+
+ // Called if the media file encounters a network error.
+ void NetworkError(const MediaResult& aError);
+
+ // Return the principal of the current URI being played or downloaded.
+ virtual already_AddRefed<nsIPrincipal> GetCurrentPrincipal() = 0;
+
+ // Return true if the loading of this resource required cross-origin
+ // redirects.
+ virtual bool HadCrossOriginRedirects() = 0;
+
+ // Return the time position in the video stream being
+ // played measured in seconds.
+ virtual double GetCurrentTime();
+
+ // Seek to the time position in (seconds) from the start of the video.
+ // If aDoFastSeek is true, we'll seek to the sync point/keyframe preceeding
+ // the seek target.
+ void Seek(double aTime, SeekTarget::Type aSeekType);
+
+ // Start playback of a video. 'Load' must have previously been
+ // called.
+ virtual void Play();
+
+ // Notify activity of the decoder owner is changed.
+ virtual void NotifyOwnerActivityChanged(bool aIsOwnerInvisible,
+ bool aIsOwnerConnected);
+
+ // Pause video playback.
+ virtual void Pause();
+ // Adjust the speed of the playback, optionally with pitch correction,
+ void SetVolume(double aVolume);
+
+ void SetPlaybackRate(double aPlaybackRate);
+ void SetPreservesPitch(bool aPreservesPitch);
+ void SetLooping(bool aLooping);
+ void SetStreamName(const nsAutoString& aStreamName);
+
+ // Set the given device as the output device.
+ RefPtr<GenericPromise> SetSink(AudioDeviceInfo* aSinkDevice);
+
+ bool GetMinimizePreroll() const { return mMinimizePreroll; }
+
+ // When we enable delay seek mode, media decoder won't actually ask MDSM to do
+ // seeking. During this period, we would store the latest seeking target and
+ // perform the seek to that target when we leave the mode. If we have any
+ // delayed seeks stored `IsSeeking()` will return true. E.g. During delay
+ // seeking mode, if we get seek target to 5s, 10s, 7s. When we stop delaying
+ // seeking, we would only seek to 7s.
+ void SetDelaySeekMode(bool aShouldDelaySeek);
+
+ // All MediaStream-related data is protected by mReentrantMonitor.
+ // We have at most one DecodedStreamData per MediaDecoder. Its stream
+ // is used as the input for each ProcessedMediaTrack created by calls to
+ // captureStream(UntilEnded). Seeking creates a new source stream, as does
+ // replaying after the input as ended. In the latter case, the new source is
+ // not connected to streams created by captureStreamUntilEnded.
+
+ enum class OutputCaptureState { Capture, Halt, None };
+ // Set the output capture state of this decoder.
+ // @param aState Capture: Output is captured into output tracks, and
+ // aDummyTrack must be provided.
+ // Halt: A capturing media sink is used, but capture is
+ // halted.
+ // None: Output is not captured.
+ // @param aDummyTrack A SharedDummyTrack the capturing media sink can use to
+ // access a MediaTrackGraph, so it can create tracks even
+ // when there are no output tracks available.
+ void SetOutputCaptureState(OutputCaptureState aState,
+ SharedDummyTrack* aDummyTrack = nullptr);
+ // Add an output track. All decoder output for the track's media type will be
+ // sent to the track.
+ // Note that only one audio track and one video track is supported by
+ // MediaDecoder at this time. Passing in more of one type, or passing in a
+ // type that metadata says we are not decoding, is an error.
+ void AddOutputTrack(RefPtr<ProcessedMediaTrack> aTrack);
+ // Remove an output track added with AddOutputTrack.
+ void RemoveOutputTrack(const RefPtr<ProcessedMediaTrack>& aTrack);
+ // Update the principal for any output tracks.
+ void SetOutputTracksPrincipal(const RefPtr<nsIPrincipal>& aPrincipal);
+
+ // Return the duration of the video in seconds.
+ virtual double GetDuration();
+
+ // Return true if the stream is infinite.
+ bool IsInfinite() const;
+
+ // Return true if we are currently seeking in the media resource.
+ // Call on the main thread only.
+ bool IsSeeking() const;
+
+ // Return true if the decoder has reached the end of playback.
+ bool IsEnded() const;
+
+ // True if we are playing a MediaSource object.
+ virtual bool IsMSE() const { return false; }
+
+ // Return true if the MediaDecoderOwner's error attribute is not null.
+ // Must be called before Shutdown().
+ bool OwnerHasError() const;
+
+ // Returns true if this media supports random seeking. False for example with
+ // chained ogg files.
+ bool IsMediaSeekable();
+ // Returns true if seeking is supported on a transport level (e.g. the server
+ // supports range requests, we are playing a file, etc.).
+ virtual bool IsTransportSeekable() = 0;
+
+ // Return the time ranges that can be seeked into, in TimeUnits.
+ virtual media::TimeIntervals GetSeekable();
+ // Return the time ranges that can be seeked into, in seconds, double
+ // precision.
+ virtual media::TimeRanges GetSeekableTimeRanges();
+
+ template <typename T>
+ T GetSeekableImpl();
+
+ // Set the end time of the media resource. When playback reaches
+ // this point the media pauses. aTime is in seconds.
+ virtual void SetFragmentEndTime(double aTime);
+
+ // Invalidate the frame.
+ void Invalidate();
+ void InvalidateWithFlags(uint32_t aFlags);
+
+ // Suspend any media downloads that are in progress. Called by the
+ // media element when it is sent to the bfcache, or when we need
+ // to throttle the download. Call on the main thread only. This can
+ // be called multiple times, there's an internal "suspend count".
+ // When it is called the internal system audio resource are cleaned up.
+ virtual void Suspend();
+
+ // Resume any media downloads that have been suspended. Called by the
+ // media element when it is restored from the bfcache, or when we need
+ // to stop throttling the download. Call on the main thread only.
+ // The download will only actually resume once as many Resume calls
+ // have been made as Suspend calls.
+ virtual void Resume();
+
+ // Moves any existing channel loads into or out of background. Background
+ // loads don't block the load event. This is called when we stop or restart
+ // delaying the load event. This also determines whether any new loads
+ // initiated (for example to seek) will be in the background. This calls
+ // SetLoadInBackground() on mResource.
+ virtual void SetLoadInBackground(bool aLoadInBackground) {}
+
+ MediaDecoderStateMachineBase* GetStateMachine() const;
+ void SetStateMachine(MediaDecoderStateMachineBase* aStateMachine);
+
+ // Constructs the time ranges representing what segments of the media
+ // are buffered and playable.
+ virtual media::TimeIntervals GetBuffered();
+
+ // Returns the size, in bytes, of the heap memory used by the currently
+ // queued decoded video and audio data.
+ size_t SizeOfVideoQueue();
+ size_t SizeOfAudioQueue();
+
+ // Helper struct for accumulating resource sizes that need to be measured
+ // asynchronously. Once all references are dropped the callback will be
+ // invoked.
+ struct ResourceSizes {
+ typedef MozPromise<size_t, size_t, true> SizeOfPromise;
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ResourceSizes)
+ explicit ResourceSizes(MallocSizeOf aMallocSizeOf)
+ : mMallocSizeOf(aMallocSizeOf), mByteSize(0), mCallback() {}
+
+ mozilla::MallocSizeOf mMallocSizeOf;
+ mozilla::Atomic<size_t> mByteSize;
+
+ RefPtr<SizeOfPromise> Promise() { return mCallback.Ensure(__func__); }
+
+ private:
+ ~ResourceSizes() { mCallback.ResolveIfExists(mByteSize, __func__); }
+
+ MozPromiseHolder<SizeOfPromise> mCallback;
+ };
+
+ virtual void AddSizeOfResources(ResourceSizes* aSizes) = 0;
+
+ VideoFrameContainer* GetVideoFrameContainer() { return mVideoFrameContainer; }
+
+ layers::ImageContainer* GetImageContainer();
+
+ // Returns true if we can play the entire media through without stopping
+ // to buffer, given the current download and playback rates.
+ bool CanPlayThrough();
+
+ // Called from HTMLMediaElement when owner document activity changes
+ virtual void SetElementVisibility(bool aIsOwnerInvisible,
+ bool aIsOwnerConnected);
+
+ // Force override the visible state to hidden.
+ // Called from HTMLMediaElement when testing of video decode suspend from
+ // mochitests.
+ void SetForcedHidden(bool aForcedHidden);
+
+ // Mark the decoder as tainted, meaning suspend-video-decoder is disabled.
+ void SetSuspendTaint(bool aTaint);
+
+ // Returns true if the decoder can't participate in suspend-video-decoder.
+ bool HasSuspendTaint() const;
+
+ void UpdateVideoDecodeMode();
+
+ void SetSecondaryVideoContainer(
+ const RefPtr<VideoFrameContainer>& aSecondaryVideoContainer);
+
+ void SetIsBackgroundVideoDecodingAllowed(bool aAllowed);
+
+ bool IsVideoDecodingSuspended() const;
+
+ // The MediaDecoderOwner of this decoder wants to resist fingerprinting.
+ bool ShouldResistFingerprinting() const {
+ return mShouldResistFingerprinting;
+ }
+
+ /******
+ * The following methods must only be called on the main
+ * thread.
+ ******/
+
+ // Change to a new play state. This updates the mState variable and
+ // notifies any thread blocking on this object's monitor of the
+ // change. Call on the main thread only.
+ virtual void ChangeState(PlayState aState);
+
+ // Called when the video has completed playing.
+ // Call on the main thread only.
+ void PlaybackEnded();
+
+ void OnSeekRejected();
+ void OnSeekResolved();
+
+ // Seeking has started. Inform the element on the main thread.
+ void SeekingStarted();
+
+ void UpdateLogicalPositionInternal();
+ void UpdateLogicalPosition() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
+ // Per spec, offical position remains stable during pause and seek.
+ if (mPlayState == PLAY_STATE_PAUSED || IsSeeking()) {
+ return;
+ }
+ UpdateLogicalPositionInternal();
+ }
+
+ // Find the end of the cached data starting at the current decoder
+ // position.
+ int64_t GetDownloadPosition();
+
+ // Notifies the element that decoding has failed.
+ void DecodeError(const MediaResult& aError);
+
+ // Indicate whether the media is same-origin with the element.
+ void UpdateSameOriginStatus(bool aSameOrigin);
+
+ MediaDecoderOwner* GetOwner() const;
+
+ AbstractThread* AbstractMainThread() const { return mAbstractMainThread; }
+
+ RefPtr<SetCDMPromise> SetCDMProxy(CDMProxy* aProxy);
+
+ void EnsureTelemetryReported();
+
+ static bool IsOggEnabled();
+ static bool IsOpusEnabled();
+ static bool IsWaveEnabled();
+ static bool IsWebMEnabled();
+
+ // Return the frame decode/paint related statistics.
+ FrameStatistics& GetFrameStatistics() { return *mFrameStats; }
+
+ void UpdateReadyState() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
+ GetOwner()->UpdateReadyState();
+ }
+
+ MediaDecoderOwner::NextFrameStatus NextFrameStatus() const {
+ return mNextFrameStatus;
+ }
+
+ virtual MediaDecoderOwner::NextFrameStatus NextFrameBufferedStatus();
+
+ RefPtr<GenericPromise> RequestDebugInfo(dom::MediaDecoderDebugInfo& aInfo);
+
+ void GetDebugInfo(dom::MediaDecoderDebugInfo& aInfo);
+
+ protected:
+ virtual ~MediaDecoder();
+
+ // Called when the first audio and/or video from the media file has been
+ // loaded by the state machine. Call on the main thread only.
+ virtual void FirstFrameLoaded(UniquePtr<MediaInfo> aInfo,
+ MediaDecoderEventVisibility aEventVisibility);
+
+ // Return error if fail to init the state machine.
+ nsresult CreateAndInitStateMachine(bool aIsLiveStream,
+ bool aDisableExternalEngine = false);
+
+ // Always return a state machine. If the decoder supports using external
+ // engine, `aDisableExternalEngine` can disable the external engine if needed.
+ virtual MediaDecoderStateMachineBase* CreateStateMachine(
+ bool aDisableExternalEngine) MOZ_NONNULL_RETURN = 0;
+
+ void SetStateMachineParameters();
+
+ // Disconnect any events before shutting down the state machine.
+ void DisconnectEvents();
+ RefPtr<ShutdownPromise> ShutdownStateMachine();
+
+ // Called when MediaDecoder shutdown is finished. Subclasses use this to clean
+ // up internal structures, and unregister potential shutdown blockers when
+ // they're done.
+ virtual void ShutdownInternal();
+
+ bool IsShutdown() const;
+
+ // Called to notify the decoder that the duration has changed.
+ virtual void DurationChanged();
+
+ // State-watching manager.
+ WatchManager<MediaDecoder> mWatchManager;
+
+ double ExplicitDuration() { return mExplicitDuration.ref(); }
+
+ void SetExplicitDuration(double aValue) {
+ MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
+ mExplicitDuration = Some(aValue);
+
+ // We Invoke DurationChanged explicitly, rather than using a watcher, so
+ // that it takes effect immediately, rather than at the end of the current
+ // task.
+ DurationChanged();
+ }
+
+ virtual void OnPlaybackEvent(MediaPlaybackEvent&& aEvent);
+
+ // Called when the metadata from the media file has been loaded by the
+ // state machine. Call on the main thread only.
+ virtual void MetadataLoaded(UniquePtr<MediaInfo> aInfo,
+ UniquePtr<MetadataTags> aTags,
+ MediaDecoderEventVisibility aEventVisibility);
+
+ void SetLogicalPosition(const media::TimeUnit& aNewPosition);
+
+ /******
+ * The following members should be accessed with the decoder lock held.
+ ******/
+
+ // The logical playback position of the media resource in units of
+ // seconds. This corresponds to the "official position" in HTML5. Note that
+ // we need to store this as a double, rather than an int64_t (like
+ // mCurrentPosition), so that |v.currentTime = foo; v.currentTime == foo|
+ // returns true without being affected by rounding errors.
+ double mLogicalPosition;
+
+ // The current playback position of the underlying playback infrastructure.
+ // This corresponds to the "current position" in HTML5.
+ // We allow omx subclasses to substitute an alternative current position for
+ // usage with the audio offload player.
+ virtual media::TimeUnit CurrentPosition() { return mCurrentPosition.Ref(); }
+
+ already_AddRefed<layers::KnowsCompositor> GetCompositor();
+
+ // Official duration of the media resource as observed by script.
+ // This can be a TimeUnit representing the exact duration found by demuxing,
+ // as a TimeUnit. This can also be a duration set explicitly by script, as a
+ // double.
+ Variant<media::TimeUnit, double> mDuration;
+
+ /******
+ * The following member variables can be accessed from any thread.
+ ******/
+
+ RefPtr<MediaFormatReader> mReader;
+
+ // Amount of buffered data ahead of current time required to consider that
+ // the next frame is available.
+ // An arbitrary value of 250ms is used.
+ static constexpr auto DEFAULT_NEXT_FRAME_AVAILABLE_BUFFERED =
+ media::TimeUnit::FromMicroseconds(250000);
+
+ private:
+ // Called when the owner's activity changed.
+ void NotifyCompositor();
+
+ void OnPlaybackErrorEvent(const MediaResult& aError);
+
+ void OnDecoderDoctorEvent(DecoderDoctorEvent aEvent);
+
+ void OnMediaNotSeekable() { mMediaSeekable = false; }
+
+ void OnNextFrameStatus(MediaDecoderOwner::NextFrameStatus);
+
+ void OnTrackInfoUpdated(const VideoInfo& aVideoInfo,
+ const AudioInfo& aAudioInfo);
+
+ void OnSecondaryVideoContainerInstalled(
+ const RefPtr<VideoFrameContainer>& aSecondaryVideoContainer);
+
+ void OnStoreDecoderBenchmark(const VideoInfo& aInfo);
+
+ void FinishShutdown();
+
+ void ConnectMirrors(MediaDecoderStateMachineBase* aObject);
+ void DisconnectMirrors();
+
+ virtual bool CanPlayThroughImpl() = 0;
+
+ // The state machine object for handling the decoding. It is safe to
+ // call methods of this object from other threads. Its internal data
+ // is synchronised on a monitor. The lifetime of this object is
+ // after mPlayState is LOADING and before mPlayState is SHUTDOWN. It
+ // is safe to access it during this period.
+ //
+ // Explicitly prievate to force access via accessors.
+ RefPtr<MediaDecoderStateMachineBase> mDecoderStateMachine;
+
+ protected:
+ void NotifyReaderDataArrived();
+ void DiscardOngoingSeekIfExists();
+ void CallSeek(const SeekTarget& aTarget);
+
+ // Called by MediaResource when the principal of the resource has
+ // changed. Called on main thread only.
+ virtual void NotifyPrincipalChanged();
+
+ MozPromiseRequestHolder<SeekPromise> mSeekRequest;
+
+ const char* PlayStateStr();
+
+ void OnMetadataUpdate(TimedMetadata&& aMetadata);
+
+ // This should only ever be accessed from the main thread.
+ // It is set in the constructor and cleared in Shutdown when the element goes
+ // away. The decoder does not add a reference the element.
+ MediaDecoderOwner* mOwner;
+
+ // The AbstractThread from mOwner.
+ const RefPtr<AbstractThread> mAbstractMainThread;
+
+ // Counters related to decode and presentation of frames.
+ const RefPtr<FrameStatistics> mFrameStats;
+
+ // Store a benchmark of the decoder based on FrameStatistics.
+ RefPtr<DecoderBenchmark> mDecoderBenchmark;
+
+ RefPtr<VideoFrameContainer> mVideoFrameContainer;
+
+ // True if the decoder has been directed to minimize its preroll before
+ // playback starts. After the first time playback starts, we don't attempt
+ // to minimize preroll, as we assume the user is likely to keep playing,
+ // or play the media again.
+ const bool mMinimizePreroll;
+
+ // True if we've already fired metadataloaded.
+ bool mFiredMetadataLoaded;
+
+ // True if the media is seekable (i.e. supports random access).
+ bool mMediaSeekable = true;
+
+ // True if the media is only seekable within its buffered ranges
+ // like WebMs with no cues.
+ bool mMediaSeekableOnlyInBufferedRanges = false;
+
+ // Stores media info, including info of audio tracks and video tracks, should
+ // only be accessed from main thread.
+ UniquePtr<MediaInfo> mInfo;
+
+ // True if the owner element is actually visible to users.
+ bool mIsOwnerInvisible;
+
+ // True if the owner element is connected to a document tree.
+ // https://dom.spec.whatwg.org/#connected
+ bool mIsOwnerConnected;
+
+ // If true, forces the decoder to be considered hidden.
+ bool mForcedHidden;
+
+ // True if the decoder has a suspend taint - meaning suspend-video-decoder is
+ // disabled.
+ bool mHasSuspendTaint;
+
+ // If true, the decoder should resist fingerprinting.
+ const bool mShouldResistFingerprinting;
+
+ MediaDecoderOwner::NextFrameStatus mNextFrameStatus =
+ MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE;
+
+ // A listener to receive metadata updates from MDSM.
+ MediaEventListener mTimedMetadataListener;
+
+ MediaEventListener mMetadataLoadedListener;
+ MediaEventListener mFirstFrameLoadedListener;
+
+ MediaEventListener mOnPlaybackEvent;
+ MediaEventListener mOnPlaybackErrorEvent;
+ MediaEventListener mOnDecoderDoctorEvent;
+ MediaEventListener mOnMediaNotSeekable;
+ MediaEventListener mOnEncrypted;
+ MediaEventListener mOnWaitingForKey;
+ MediaEventListener mOnDecodeWarning;
+ MediaEventListener mOnNextFrameStatus;
+ MediaEventListener mOnTrackInfoUpdated;
+ MediaEventListener mOnSecondaryVideoContainerInstalled;
+ MediaEventListener mOnStoreDecoderBenchmark;
+
+ // True if we have suspended video decoding.
+ bool mIsVideoDecodingSuspended = false;
+
+ protected:
+ // PlaybackRate and pitch preservation status we should start at.
+ double mPlaybackRate;
+
+ // True if the decoder is seeking.
+ Watchable<bool> mLogicallySeeking;
+
+ // Buffered range, mirrored from the reader.
+ Mirror<media::TimeIntervals> mBuffered;
+
+ // NB: Don't use mCurrentPosition directly, but rather CurrentPosition().
+ Mirror<media::TimeUnit> mCurrentPosition;
+
+ // Duration of the media resource according to the state machine.
+ Mirror<media::NullableTimeUnit> mStateMachineDuration;
+
+ // Used to distinguish whether the audio is producing sound.
+ Mirror<bool> mIsAudioDataAudible;
+
+ // Volume of playback. 0.0 = muted. 1.0 = full volume.
+ Canonical<double> mVolume;
+
+ Canonical<bool> mPreservesPitch;
+
+ Canonical<bool> mLooping;
+
+ Canonical<nsAutoString> mStreamName;
+
+ // The device used with SetSink, or nullptr if no explicit device has been
+ // set.
+ Canonical<RefPtr<AudioDeviceInfo>> mSinkDevice;
+
+ // Set if the decoder is sending video to a secondary container. While set we
+ // should not suspend the decoder.
+ Canonical<RefPtr<VideoFrameContainer>> mSecondaryVideoContainer;
+
+ // Whether this MediaDecoder's output is captured, halted or not captured.
+ // When captured, all decoded data must be played out through mOutputTracks.
+ Canonical<OutputCaptureState> mOutputCaptureState;
+
+ // A dummy track used to access the right MediaTrackGraph instance. Needed
+ // since there's no guarantee that output tracks are present.
+ Canonical<nsMainThreadPtrHandle<SharedDummyTrack>> mOutputDummyTrack;
+
+ // Tracks that, if set, will get data routed through them.
+ Canonical<CopyableTArray<RefPtr<ProcessedMediaTrack>>> mOutputTracks;
+
+ // PrincipalHandle to be used when feeding data into mOutputTracks.
+ Canonical<PrincipalHandle> mOutputPrincipal;
+
+ // Media duration set explicitly by JS. At present, this is only ever present
+ // for MSE.
+ Maybe<double> mExplicitDuration;
+
+ // Set to one of the valid play states.
+ // This can only be changed on the main thread while holding the decoder
+ // monitor. Thus, it can be safely read while holding the decoder monitor
+ // OR on the main thread.
+ Canonical<PlayState> mPlayState;
+
+ // This can only be changed on the main thread.
+ PlayState mNextState = PLAY_STATE_PAUSED;
+
+ // True if the media is same-origin with the element. Data can only be
+ // passed to MediaStreams when this is true.
+ bool mSameOriginMedia;
+
+ // We can allow video decoding in background when we match some special
+ // conditions, eg. when the cursor is hovering over the tab. This observer is
+ // used to listen the related events.
+ RefPtr<BackgroundVideoDecodingPermissionObserver> mVideoDecodingOberver;
+
+ // True if we want to resume video decoding even the media element is in the
+ // background.
+ bool mIsBackgroundVideoDecodingAllowed;
+
+ // True if we want to delay seeking, and and save the latest seeking target to
+ // resume to when we stop delaying seeking.
+ bool mShouldDelaySeek = false;
+ Maybe<SeekTarget> mDelayedSeekTarget;
+
+ public:
+ AbstractCanonical<double>* CanonicalVolume() { return &mVolume; }
+ AbstractCanonical<bool>* CanonicalPreservesPitch() {
+ return &mPreservesPitch;
+ }
+ AbstractCanonical<bool>* CanonicalLooping() { return &mLooping; }
+ AbstractCanonical<nsAutoString>* CanonicalStreamName() {
+ return &mStreamName;
+ }
+ AbstractCanonical<RefPtr<AudioDeviceInfo>>* CanonicalSinkDevice() {
+ return &mSinkDevice;
+ }
+ AbstractCanonical<RefPtr<VideoFrameContainer>>*
+ CanonicalSecondaryVideoContainer() {
+ return &mSecondaryVideoContainer;
+ }
+ AbstractCanonical<OutputCaptureState>* CanonicalOutputCaptureState() {
+ return &mOutputCaptureState;
+ }
+ AbstractCanonical<nsMainThreadPtrHandle<SharedDummyTrack>>*
+ CanonicalOutputDummyTrack() {
+ return &mOutputDummyTrack;
+ }
+ AbstractCanonical<CopyableTArray<RefPtr<ProcessedMediaTrack>>>*
+ CanonicalOutputTracks() {
+ return &mOutputTracks;
+ }
+ AbstractCanonical<PrincipalHandle>* CanonicalOutputPrincipal() {
+ return &mOutputPrincipal;
+ }
+ AbstractCanonical<PlayState>* CanonicalPlayState() { return &mPlayState; }
+
+ void UpdateTelemetryHelperBasedOnPlayState(PlayState aState) const;
+
+ TelemetryProbesReporter::Visibility OwnerVisibility() const;
+
+ // Those methods exist to report telemetry related metrics.
+ double GetTotalVideoPlayTimeInSeconds() const;
+ double GetTotalVideoHDRPlayTimeInSeconds() const;
+ double GetVisibleVideoPlayTimeInSeconds() const;
+ double GetInvisibleVideoPlayTimeInSeconds() const;
+ double GetVideoDecodeSuspendedTimeInSeconds() const;
+ double GetTotalAudioPlayTimeInSeconds() const;
+ double GetAudiblePlayTimeInSeconds() const;
+ double GetInaudiblePlayTimeInSeconds() const;
+ double GetMutedPlayTimeInSeconds() const;
+
+ private:
+ /**
+ * This enum describes the reason why we need to update the logical position.
+ * ePeriodicUpdate : the position grows periodically during playback
+ * eSeamlessLoopingSeeking : the position changes due to demuxer level seek.
+ * eOther : due to normal seeking or other attributes changes, eg. playstate
+ */
+ enum class PositionUpdate {
+ ePeriodicUpdate,
+ eSeamlessLoopingSeeking,
+ eOther,
+ };
+ PositionUpdate GetPositionUpdateReason(double aPrevPos,
+ const media::TimeUnit& aCurPos) const;
+
+ // Notify owner when the audible state changed
+ void NotifyAudibleStateChanged();
+
+ void NotifyVolumeChanged();
+
+ bool mTelemetryReported;
+ const MediaContainerType mContainerType;
+ bool mCanPlayThrough = false;
+
+ UniquePtr<TelemetryProbesReporter> mTelemetryProbesReporter;
+
+# ifdef MOZ_WMF_MEDIA_ENGINE
+ // True when we need to update the newly created MDSM's status to make it
+ // consistent with the previous destroyed one.
+ bool mPendingStatusUpdateForNewlyCreatedStateMachine = false;
+# endif
+};
+
+typedef MozPromise<mozilla::dom::MediaMemoryInfo, nsresult, true>
+ MediaMemoryPromise;
+
+RefPtr<MediaMemoryPromise> GetMediaMemorySizes(dom::Document* aDoc);
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/MediaDecoderOwner.h b/dom/media/MediaDecoderOwner.h
new file mode 100644
index 0000000000..388c0241a2
--- /dev/null
+++ b/dom/media/MediaDecoderOwner.h
@@ -0,0 +1,200 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef MediaDecoderOwner_h_
+#define MediaDecoderOwner_h_
+
+#include "mozilla/UniquePtr.h"
+#include "MediaInfo.h"
+#include "MediaSegment.h"
+#include "nsSize.h"
+
+namespace mozilla {
+
+class AbstractThread;
+class GMPCrashHelper;
+class VideoFrameContainer;
+class MediaInfo;
+class MediaResult;
+enum class RFPTarget : uint32_t;
+
+namespace dom {
+class Document;
+class HTMLMediaElement;
+} // namespace dom
+
+class MediaDecoderOwner {
+ public:
+ // Called by the media decoder to indicate that the download is progressing.
+ virtual void DownloadProgressed() = 0;
+
+ // Dispatch an asynchronous event to the decoder owner
+ virtual void DispatchAsyncEvent(const nsAString& aName) = 0;
+
+ // Triggers a recomputation of readyState.
+ virtual void UpdateReadyState() = 0;
+
+ // Called by the decoder object to notify owner might need to dispatch the
+ // `timeupdate` event due to current time changes.
+ virtual void MaybeQueueTimeupdateEvent() = 0;
+
+ // Return true if decoding should be paused
+ virtual bool GetPaused() = 0;
+
+ // Called by the video decoder object, on the main thread,
+ // when it has read the metadata containing video dimensions,
+ // etc.
+ // Must take ownership of MetadataTags aTags argument.
+ virtual void MetadataLoaded(const MediaInfo* aInfo,
+ UniquePtr<const MetadataTags> aTags) = 0;
+
+ // Called by the decoder object, on the main thread,
+ // when it has read the first frame of the video or audio.
+ virtual void FirstFrameLoaded() = 0;
+
+ // Called by the decoder object, on the main thread,
+ // when the resource has a network error during loading.
+ // The decoder owner should call Shutdown() on the decoder and drop the
+ // reference to the decoder to prevent further calls into the decoder.
+ virtual void NetworkError(const MediaResult& aError) = 0;
+
+ // Called by the decoder object, on the main thread, when the
+ // resource has a decode error during metadata loading or decoding.
+ // The decoder owner should call Shutdown() on the decoder and drop the
+ // reference to the decoder to prevent further calls into the decoder.
+ virtual void DecodeError(const MediaResult& aError) = 0;
+
+ // Called by the decoder object, on the main thread, when the
+ // resource has a decode issue during metadata loading or decoding, but can
+ // continue decoding.
+ virtual void DecodeWarning(const MediaResult& aError) = 0;
+
+ // Return true if media element error attribute is not null.
+ virtual bool HasError() const = 0;
+
+ // Called by the video decoder object, on the main thread, when the
+ // resource load has been cancelled.
+ virtual void LoadAborted() = 0;
+
+ // Called by the video decoder object, on the main thread,
+ // when the video playback has ended.
+ virtual void PlaybackEnded() = 0;
+
+ // Called by the video decoder object, on the main thread,
+ // when the resource has started seeking.
+ virtual void SeekStarted() = 0;
+
+ // Called by the video decoder object, on the main thread,
+ // when the resource has completed seeking.
+ virtual void SeekCompleted() = 0;
+
+ // Called by the video decoder object, on the main thread,
+ // when the resource has aborted seeking.
+ virtual void SeekAborted() = 0;
+
+ // Called by the media stream, on the main thread, when the download
+ // has been suspended by the cache or because the element itself
+ // asked the decoder to suspend the download.
+ virtual void DownloadSuspended() = 0;
+
+ // Called by the media decoder to indicate whether the media cache has
+ // suspended the channel.
+ virtual void NotifySuspendedByCache(bool aSuspendedByCache) = 0;
+
+ // called to notify that the principal of the decoder's media resource has
+ // changed.
+ virtual void NotifyDecoderPrincipalChanged() = 0;
+
+ // The status of the next frame which might be available from the decoder
+ enum NextFrameStatus {
+ // The next frame of audio/video is available
+ NEXT_FRAME_AVAILABLE,
+ // The next frame of audio/video is unavailable because the decoder
+ // is paused while it buffers up data
+ NEXT_FRAME_UNAVAILABLE_BUFFERING,
+ // The next frame of audio/video is unavailable for the decoder is seeking.
+ NEXT_FRAME_UNAVAILABLE_SEEKING,
+ // The next frame of audio/video is unavailable for some other reasons
+ NEXT_FRAME_UNAVAILABLE,
+ // Sentinel value
+ NEXT_FRAME_UNINITIALIZED
+ };
+
+ // Called by media decoder when the audible state changed
+ virtual void SetAudibleState(bool aAudible) = 0;
+
+ // Notified by the decoder that XPCOM shutdown has begun.
+ // The decoder owner should call Shutdown() on the decoder and drop the
+ // reference to the decoder to prevent further calls into the decoder.
+ virtual void NotifyXPCOMShutdown() = 0;
+
+ // Dispatches a "encrypted" event to the HTMLMediaElement, with the
+ // provided init data. Actual dispatch may be delayed until HAVE_METADATA.
+ // Main thread only.
+ virtual void DispatchEncrypted(const nsTArray<uint8_t>& aInitData,
+ const nsAString& aInitDataType) = 0;
+
+ // Notified by the decoder that a decryption key is required before emitting
+ // further output.
+ virtual void NotifyWaitingForKey() {}
+
+ /*
+ * Methods that are used only in Gecko go here. We provide defaul
+ * implementations so they can compile in Servo without modification.
+ */
+ // Return an abstract thread on which to run main thread runnables.
+ virtual AbstractThread* AbstractMainThread() const { return nullptr; }
+
+ // Get the HTMLMediaElement object if the decoder is being used from an
+ // HTML media element, and null otherwise.
+ virtual dom::HTMLMediaElement* GetMediaElement() { return nullptr; }
+
+ // Called by the media decoder and the video frame to get the
+ // ImageContainer containing the video data.
+ virtual VideoFrameContainer* GetVideoFrameContainer() { return nullptr; }
+
+ // Return the decoder owner's owner document.
+ virtual mozilla::dom::Document* GetDocument() const { return nullptr; }
+
+ // Called by the media decoder to create a GMPCrashHelper.
+ virtual already_AddRefed<GMPCrashHelper> CreateGMPCrashHelper() {
+ return nullptr;
+ }
+
+ // Called by the frame container to notify the layout engine that the
+ // size of the image has changed, or the video needs to be be repainted
+ // for some other reason.
+ enum class ImageSizeChanged { No, Yes };
+ enum class ForceInvalidate { No, Yes };
+ virtual void Invalidate(ImageSizeChanged aImageSizeChanged,
+ const Maybe<nsIntSize>& aNewIntrinsicSize,
+ ForceInvalidate aForceInvalidate) {}
+
+ // Called after the MediaStream we're playing rendered a frame to aContainer
+ // with a different principalHandle than the previous frame.
+ virtual void PrincipalHandleChangedForVideoFrameContainer(
+ VideoFrameContainer* aContainer,
+ const PrincipalHandle& aNewPrincipalHandle) {}
+
+ // Called after the MediaDecoder has installed the given secondary video
+ // container and render potential frames to it.
+ virtual void OnSecondaryVideoContainerInstalled(
+ const RefPtr<VideoFrameContainer>& aSecondaryContainer) {}
+
+ // Return true is the owner is actually invisible to users.
+ virtual bool IsActuallyInvisible() const = 0;
+
+ // Returns true if the owner should resist fingerprinting.
+ virtual bool ShouldResistFingerprinting(RFPTarget aTarget) const = 0;
+
+ /*
+ * Servo only methods go here. Please provide default implementations so they
+ * can build in Gecko without any modification.
+ */
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/MediaDecoderStateMachine.cpp b/dom/media/MediaDecoderStateMachine.cpp
new file mode 100644
index 0000000000..de7fb2c18d
--- /dev/null
+++ b/dom/media/MediaDecoderStateMachine.cpp
@@ -0,0 +1,4824 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <algorithm>
+#include <stdint.h>
+#include <utility>
+
+#include "mediasink/AudioSink.h"
+#include "mediasink/AudioSinkWrapper.h"
+#include "mediasink/DecodedStream.h"
+#include "mediasink/VideoSink.h"
+#include "mozilla/Logging.h"
+#include "mozilla/MathAlgorithms.h"
+#include "mozilla/NotNull.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/ProfilerMarkers.h"
+#include "mozilla/ProfilerMarkerTypes.h"
+#include "mozilla/SharedThreadPool.h"
+#include "mozilla/Sprintf.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/TaskQueue.h"
+
+#include "nsIMemoryReporter.h"
+#include "nsPrintfCString.h"
+#include "nsTArray.h"
+#include "AudioSegment.h"
+#include "DOMMediaStream.h"
+#include "ImageContainer.h"
+#include "MediaDecoder.h"
+#include "MediaDecoderStateMachine.h"
+#include "MediaShutdownManager.h"
+#include "MediaTrackGraph.h"
+#include "MediaTimer.h"
+#include "PerformanceRecorder.h"
+#include "ReaderProxy.h"
+#include "TimeUnits.h"
+#include "VideoSegment.h"
+#include "VideoUtils.h"
+
+namespace mozilla {
+
+using namespace mozilla::media;
+
+#define NS_DispatchToMainThread(...) \
+ CompileError_UseAbstractThreadDispatchInstead
+
+// avoid redefined macro in unified build
+#undef FMT
+#undef LOG
+#undef LOGV
+#undef LOGW
+#undef LOGE
+#undef SFMT
+#undef SLOG
+#undef SLOGW
+#undef SLOGE
+
+#define FMT(x, ...) "Decoder=%p " x, mDecoderID, ##__VA_ARGS__
+#define LOG(x, ...) \
+ DDMOZ_LOG(gMediaDecoderLog, LogLevel::Debug, "Decoder=%p " x, mDecoderID, \
+ ##__VA_ARGS__)
+#define LOGV(x, ...) \
+ DDMOZ_LOG(gMediaDecoderLog, LogLevel::Verbose, "Decoder=%p " x, mDecoderID, \
+ ##__VA_ARGS__)
+#define LOGW(x, ...) NS_WARNING(nsPrintfCString(FMT(x, ##__VA_ARGS__)).get())
+#define LOGE(x, ...) \
+ NS_DebugBreak(NS_DEBUG_WARNING, \
+ nsPrintfCString(FMT(x, ##__VA_ARGS__)).get(), nullptr, \
+ __FILE__, __LINE__)
+
+// Used by StateObject and its sub-classes
+#define SFMT(x, ...) \
+ "Decoder=%p state=%s " x, mMaster->mDecoderID, ToStateStr(GetState()), \
+ ##__VA_ARGS__
+#define SLOG(x, ...) \
+ DDMOZ_LOGEX(mMaster, gMediaDecoderLog, LogLevel::Debug, "state=%s " x, \
+ ToStateStr(GetState()), ##__VA_ARGS__)
+#define SLOGW(x, ...) NS_WARNING(nsPrintfCString(SFMT(x, ##__VA_ARGS__)).get())
+#define SLOGE(x, ...) \
+ NS_DebugBreak(NS_DEBUG_WARNING, \
+ nsPrintfCString(SFMT(x, ##__VA_ARGS__)).get(), nullptr, \
+ __FILE__, __LINE__)
+
+// Certain constants get stored as member variables and then adjusted by various
+// scale factors on a per-decoder basis. We want to make sure to avoid using
+// these constants directly, so we put them in a namespace.
+namespace detail {
+
+// Resume a suspended video decoder to the current playback position plus this
+// time premium for compensating the seeking delay.
+static constexpr auto RESUME_VIDEO_PREMIUM = TimeUnit::FromMicroseconds(125000);
+
+static const int64_t AMPLE_AUDIO_USECS = 2000000;
+
+// If more than this much decoded audio is queued, we'll hold off
+// decoding more audio.
+static constexpr auto AMPLE_AUDIO_THRESHOLD =
+ TimeUnit::FromMicroseconds(AMPLE_AUDIO_USECS);
+
+} // namespace detail
+
+// If we have fewer than LOW_VIDEO_FRAMES decoded frames, and
+// we're not "prerolling video", we'll skip the video up to the next keyframe
+// which is at or after the current playback position.
+static const uint32_t LOW_VIDEO_FRAMES = 2;
+
+// Arbitrary "frame duration" when playing only audio.
+static const uint32_t AUDIO_DURATION_USECS = 40000;
+
+namespace detail {
+
+// If we have less than this much buffered data available, we'll consider
+// ourselves to be running low on buffered data. We determine how much
+// buffered data we have remaining using the reader's GetBuffered()
+// implementation.
+static const int64_t LOW_BUFFER_THRESHOLD_USECS = 5000000;
+
+static constexpr auto LOW_BUFFER_THRESHOLD =
+ TimeUnit::FromMicroseconds(LOW_BUFFER_THRESHOLD_USECS);
+
+// LOW_BUFFER_THRESHOLD_USECS needs to be greater than AMPLE_AUDIO_USECS,
+// otherwise the skip-to-keyframe logic can activate when we're running low on
+// data.
+static_assert(LOW_BUFFER_THRESHOLD_USECS > AMPLE_AUDIO_USECS,
+ "LOW_BUFFER_THRESHOLD_USECS is too small");
+
+} // namespace detail
+
+// Amount of excess data to add in to the "should we buffer" calculation.
+static constexpr auto EXHAUSTED_DATA_MARGIN =
+ TimeUnit::FromMicroseconds(100000);
+
+static const uint32_t MIN_VIDEO_QUEUE_SIZE = 3;
+static const uint32_t MAX_VIDEO_QUEUE_SIZE = 10;
+#ifdef MOZ_APPLEMEDIA
+static const uint32_t HW_VIDEO_QUEUE_SIZE = 10;
+#else
+static const uint32_t HW_VIDEO_QUEUE_SIZE = 3;
+#endif
+static const uint32_t VIDEO_QUEUE_SEND_TO_COMPOSITOR_SIZE = 9999;
+
+static uint32_t sVideoQueueDefaultSize = MAX_VIDEO_QUEUE_SIZE;
+static uint32_t sVideoQueueHWAccelSize = HW_VIDEO_QUEUE_SIZE;
+static uint32_t sVideoQueueSendToCompositorSize =
+ VIDEO_QUEUE_SEND_TO_COMPOSITOR_SIZE;
+
+static void InitVideoQueuePrefs() {
+ MOZ_ASSERT(NS_IsMainThread());
+ static bool sPrefInit = false;
+ if (!sPrefInit) {
+ sPrefInit = true;
+ sVideoQueueDefaultSize = Preferences::GetUint(
+ "media.video-queue.default-size", MAX_VIDEO_QUEUE_SIZE);
+ sVideoQueueHWAccelSize = Preferences::GetUint(
+ "media.video-queue.hw-accel-size", HW_VIDEO_QUEUE_SIZE);
+ sVideoQueueSendToCompositorSize =
+ Preferences::GetUint("media.video-queue.send-to-compositor-size",
+ VIDEO_QUEUE_SEND_TO_COMPOSITOR_SIZE);
+ }
+}
+
+template <typename Type, typename Function>
+static void DiscardFramesFromTail(MediaQueue<Type>& aQueue,
+ const Function&& aTest) {
+ while (aQueue.GetSize()) {
+ if (aTest(aQueue.PeekBack()->mTime.ToMicroseconds())) {
+ RefPtr<Type> releaseMe = aQueue.PopBack();
+ continue;
+ }
+ break;
+ }
+}
+
+// Delay, in milliseconds, that tabs needs to be in background before video
+// decoding is suspended.
+static TimeDuration SuspendBackgroundVideoDelay() {
+ return TimeDuration::FromMilliseconds(
+ StaticPrefs::media_suspend_bkgnd_video_delay_ms());
+}
+
+class MediaDecoderStateMachine::StateObject {
+ public:
+ virtual ~StateObject() = default;
+ virtual void Exit() {} // Exit action.
+ virtual void Step() {} // Perform a 'cycle' of this state object.
+ virtual State GetState() const = 0;
+
+ // Event handlers for various events.
+ virtual void HandleAudioCaptured() {}
+ virtual void HandleAudioDecoded(AudioData* aAudio) {
+ Crash("Unexpected event!", __func__);
+ }
+ virtual void HandleVideoDecoded(VideoData* aVideo) {
+ Crash("Unexpected event!", __func__);
+ }
+ virtual void HandleAudioWaited(MediaData::Type aType) {
+ Crash("Unexpected event!", __func__);
+ }
+ virtual void HandleVideoWaited(MediaData::Type aType) {
+ Crash("Unexpected event!", __func__);
+ }
+ virtual void HandleWaitingForAudio() { Crash("Unexpected event!", __func__); }
+ virtual void HandleAudioCanceled() { Crash("Unexpected event!", __func__); }
+ virtual void HandleEndOfAudio() { Crash("Unexpected event!", __func__); }
+ virtual void HandleWaitingForVideo() { Crash("Unexpected event!", __func__); }
+ virtual void HandleVideoCanceled() { Crash("Unexpected event!", __func__); }
+ virtual void HandleEndOfVideo() { Crash("Unexpected event!", __func__); }
+
+ virtual RefPtr<MediaDecoder::SeekPromise> HandleSeek(
+ const SeekTarget& aTarget);
+
+ virtual RefPtr<ShutdownPromise> HandleShutdown();
+
+ virtual void HandleVideoSuspendTimeout() = 0;
+
+ virtual void HandleResumeVideoDecoding(const TimeUnit& aTarget);
+
+ virtual void HandlePlayStateChanged(MediaDecoder::PlayState aPlayState) {}
+
+ virtual void GetDebugInfo(
+ dom::MediaDecoderStateMachineDecodingStateDebugInfo& aInfo) {}
+
+ virtual void HandleLoopingChanged() {}
+
+ private:
+ template <class S, typename R, typename... As>
+ auto ReturnTypeHelper(R (S::*)(As...)) -> R;
+
+ void Crash(const char* aReason, const char* aSite) {
+ char buf[1024];
+ SprintfLiteral(buf, "%s state=%s callsite=%s", aReason,
+ ToStateStr(GetState()), aSite);
+ MOZ_ReportAssertionFailure(buf, __FILE__, __LINE__);
+ MOZ_CRASH();
+ }
+
+ protected:
+ enum class EventVisibility : int8_t { Observable, Suppressed };
+
+ using Master = MediaDecoderStateMachine;
+ explicit StateObject(Master* aPtr) : mMaster(aPtr) {}
+ TaskQueue* OwnerThread() const { return mMaster->mTaskQueue; }
+ ReaderProxy* Reader() const { return mMaster->mReader; }
+ const MediaInfo& Info() const { return mMaster->Info(); }
+ MediaQueue<AudioData>& AudioQueue() const { return mMaster->mAudioQueue; }
+ MediaQueue<VideoData>& VideoQueue() const { return mMaster->mVideoQueue; }
+
+ template <class S, typename... Args, size_t... Indexes>
+ auto CallEnterMemberFunction(S* aS, std::tuple<Args...>& aTuple,
+ std::index_sequence<Indexes...>)
+ -> decltype(ReturnTypeHelper(&S::Enter)) {
+ AUTO_PROFILER_LABEL("StateObject::CallEnterMemberFunction", MEDIA_PLAYBACK);
+ return aS->Enter(std::move(std::get<Indexes>(aTuple))...);
+ }
+
+ // Note this function will delete the current state object.
+ // Don't access members to avoid UAF after this call.
+ template <class S, typename... Ts>
+ auto SetState(Ts&&... aArgs) -> decltype(ReturnTypeHelper(&S::Enter)) {
+ // |aArgs| must be passed by reference to avoid passing MOZ_NON_PARAM class
+ // SeekJob by value. See bug 1287006 and bug 1338374. But we still *must*
+ // copy the parameters, because |Exit()| can modify them. See bug 1312321.
+ // So we 1) pass the parameters by reference, but then 2) immediately copy
+ // them into a Tuple to be safe against modification, and finally 3) move
+ // the elements of the Tuple into the final function call.
+ auto copiedArgs = std::make_tuple(std::forward<Ts>(aArgs)...);
+
+ // Copy mMaster which will reset to null.
+ auto* master = mMaster;
+
+ auto* s = new S(master);
+
+ // It's possible to seek again during seeking, otherwise the new state
+ // should always be different from the original one.
+ MOZ_ASSERT(GetState() != s->GetState() ||
+ GetState() == DECODER_STATE_SEEKING_ACCURATE ||
+ GetState() == DECODER_STATE_SEEKING_FROMDORMANT ||
+ GetState() == DECODER_STATE_SEEKING_NEXTFRAMESEEKING ||
+ GetState() == DECODER_STATE_SEEKING_VIDEOONLY);
+
+ SLOG("change state to: %s", ToStateStr(s->GetState()));
+ PROFILER_MARKER_TEXT("MDSM::StateChange", MEDIA_PLAYBACK, {},
+ nsPrintfCString("%s", ToStateStr(s->GetState())));
+
+ Exit();
+
+ // Delete the old state asynchronously to avoid UAF if the caller tries to
+ // access its members after SetState() returns.
+ master->OwnerThread()->DispatchDirectTask(
+ NS_NewRunnableFunction("MDSM::StateObject::DeleteOldState",
+ [toDelete = std::move(master->mStateObj)]() {}));
+ // Also reset mMaster to catch potentail UAF.
+ mMaster = nullptr;
+
+ master->mStateObj.reset(s);
+ return CallEnterMemberFunction(s, copiedArgs,
+ std::index_sequence_for<Ts...>{});
+ }
+
+ RefPtr<MediaDecoder::SeekPromise> SetSeekingState(
+ SeekJob&& aSeekJob, EventVisibility aVisibility);
+
+ void SetDecodingState();
+
+ // Take a raw pointer in order not to change the life cycle of MDSM.
+ // It is guaranteed to be valid by MDSM.
+ Master* mMaster;
+};
+
+/**
+ * Purpose: decode metadata like duration and dimensions of the media resource.
+ *
+ * Transition to other states when decoding metadata is done:
+ * SHUTDOWN if failing to decode metadata.
+ * DECODING_FIRSTFRAME otherwise.
+ */
+class MediaDecoderStateMachine::DecodeMetadataState
+ : public MediaDecoderStateMachine::StateObject {
+ public:
+ explicit DecodeMetadataState(Master* aPtr) : StateObject(aPtr) {}
+
+ void Enter() {
+ MOZ_ASSERT(!mMaster->mVideoDecodeSuspended);
+ MOZ_ASSERT(!mMetadataRequest.Exists());
+ SLOG("Dispatching AsyncReadMetadata");
+
+ // We disconnect mMetadataRequest in Exit() so it is fine to capture
+ // a raw pointer here.
+ Reader()
+ ->ReadMetadata()
+ ->Then(
+ OwnerThread(), __func__,
+ [this](MetadataHolder&& aMetadata) {
+ OnMetadataRead(std::move(aMetadata));
+ },
+ [this](const MediaResult& aError) { OnMetadataNotRead(aError); })
+ ->Track(mMetadataRequest);
+ }
+
+ void Exit() override { mMetadataRequest.DisconnectIfExists(); }
+
+ State GetState() const override { return DECODER_STATE_DECODING_METADATA; }
+
+ RefPtr<MediaDecoder::SeekPromise> HandleSeek(
+ const SeekTarget& aTarget) override {
+ MOZ_DIAGNOSTIC_ASSERT(false, "Can't seek while decoding metadata.");
+ return MediaDecoder::SeekPromise::CreateAndReject(true, __func__);
+ }
+
+ void HandleVideoSuspendTimeout() override {
+ // Do nothing since no decoders are created yet.
+ }
+
+ void HandleResumeVideoDecoding(const TimeUnit&) override {
+ // We never suspend video decoding in this state.
+ MOZ_ASSERT(false, "Shouldn't have suspended video decoding.");
+ }
+
+ private:
+ void OnMetadataRead(MetadataHolder&& aMetadata);
+
+ void OnMetadataNotRead(const MediaResult& aError) {
+ AUTO_PROFILER_LABEL("DecodeMetadataState::OnMetadataNotRead",
+ MEDIA_PLAYBACK);
+
+ mMetadataRequest.Complete();
+ SLOGE("Decode metadata failed, shutting down decoder");
+ mMaster->DecodeError(aError);
+ }
+
+ MozPromiseRequestHolder<MediaFormatReader::MetadataPromise> mMetadataRequest;
+};
+
+/**
+ * Purpose: release decoder resources to save memory and hardware resources.
+ *
+ * Transition to:
+ * SEEKING if any seek request or play state changes to PLAYING.
+ */
+class MediaDecoderStateMachine::DormantState
+ : public MediaDecoderStateMachine::StateObject {
+ public:
+ explicit DormantState(Master* aPtr) : StateObject(aPtr) {}
+
+ void Enter() {
+ if (mMaster->IsPlaying()) {
+ mMaster->StopPlayback();
+ }
+
+ // Calculate the position to seek to when exiting dormant.
+ auto t = mMaster->mMediaSink->IsStarted() ? mMaster->GetClock()
+ : mMaster->GetMediaTime();
+ mMaster->AdjustByLooping(t);
+ mPendingSeek.mTarget.emplace(t, SeekTarget::Accurate);
+ // SeekJob asserts |mTarget.IsValid() == !mPromise.IsEmpty()| so we
+ // need to create the promise even it is not used at all.
+ // The promise may be used when coming out of DormantState into
+ // SeekingState.
+ RefPtr<MediaDecoder::SeekPromise> x =
+ mPendingSeek.mPromise.Ensure(__func__);
+
+ // Reset the decoding state to ensure that any queued video frames are
+ // released and don't consume video memory.
+ mMaster->ResetDecode();
+
+ // No need to call StopMediaSink() here.
+ // We will do it during seeking when exiting dormant.
+
+ // Ignore WAIT_FOR_DATA since we won't decode in dormant.
+ mMaster->mAudioWaitRequest.DisconnectIfExists();
+ mMaster->mVideoWaitRequest.DisconnectIfExists();
+
+ MaybeReleaseResources();
+ }
+
+ void Exit() override {
+ // mPendingSeek is either moved when exiting dormant or
+ // should be rejected here before transition to SHUTDOWN.
+ mPendingSeek.RejectIfExists(__func__);
+ }
+
+ State GetState() const override { return DECODER_STATE_DORMANT; }
+
+ RefPtr<MediaDecoder::SeekPromise> HandleSeek(
+ const SeekTarget& aTarget) override;
+
+ void HandleVideoSuspendTimeout() override {
+ // Do nothing since we've released decoders in Enter().
+ }
+
+ void HandleResumeVideoDecoding(const TimeUnit&) override {
+ // Do nothing since we won't resume decoding until exiting dormant.
+ }
+
+ void HandlePlayStateChanged(MediaDecoder::PlayState aPlayState) override;
+
+ void HandleAudioDecoded(AudioData*) override { MaybeReleaseResources(); }
+ void HandleVideoDecoded(VideoData*) override { MaybeReleaseResources(); }
+ void HandleWaitingForAudio() override { MaybeReleaseResources(); }
+ void HandleWaitingForVideo() override { MaybeReleaseResources(); }
+ void HandleAudioCanceled() override { MaybeReleaseResources(); }
+ void HandleVideoCanceled() override { MaybeReleaseResources(); }
+ void HandleEndOfAudio() override { MaybeReleaseResources(); }
+ void HandleEndOfVideo() override { MaybeReleaseResources(); }
+
+ private:
+ void MaybeReleaseResources() {
+ if (!mMaster->mAudioDataRequest.Exists() &&
+ !mMaster->mVideoDataRequest.Exists()) {
+ // Release decoders only when they are idle. Otherwise it might cause
+ // decode error later when resetting decoders during seeking.
+ mMaster->mReader->ReleaseResources();
+ }
+ }
+
+ SeekJob mPendingSeek;
+};
+
+/**
+ * Purpose: decode the 1st audio and video frames to fire the 'loadeddata'
+ * event.
+ *
+ * Transition to:
+ * SHUTDOWN if any decode error.
+ * SEEKING if any seek request.
+ * DECODING/LOOPING_DECODING when the 'loadeddata' event is fired.
+ */
+class MediaDecoderStateMachine::DecodingFirstFrameState
+ : public MediaDecoderStateMachine::StateObject {
+ public:
+ explicit DecodingFirstFrameState(Master* aPtr) : StateObject(aPtr) {}
+
+ void Enter();
+
+ void Exit() override {
+ // mPendingSeek is either moved in MaybeFinishDecodeFirstFrame()
+ // or should be rejected here before transition to SHUTDOWN.
+ mPendingSeek.RejectIfExists(__func__);
+ }
+
+ State GetState() const override { return DECODER_STATE_DECODING_FIRSTFRAME; }
+
+ void HandleAudioDecoded(AudioData* aAudio) override {
+ mMaster->PushAudio(aAudio);
+ MaybeFinishDecodeFirstFrame();
+ }
+
+ void HandleVideoDecoded(VideoData* aVideo) override {
+ mMaster->PushVideo(aVideo);
+ MaybeFinishDecodeFirstFrame();
+ }
+
+ void HandleWaitingForAudio() override {
+ mMaster->WaitForData(MediaData::Type::AUDIO_DATA);
+ }
+
+ void HandleAudioCanceled() override { mMaster->RequestAudioData(); }
+
+ void HandleEndOfAudio() override {
+ AudioQueue().Finish();
+ MaybeFinishDecodeFirstFrame();
+ }
+
+ void HandleWaitingForVideo() override {
+ mMaster->WaitForData(MediaData::Type::VIDEO_DATA);
+ }
+
+ void HandleVideoCanceled() override {
+ mMaster->RequestVideoData(media::TimeUnit());
+ }
+
+ void HandleEndOfVideo() override {
+ VideoQueue().Finish();
+ MaybeFinishDecodeFirstFrame();
+ }
+
+ void HandleAudioWaited(MediaData::Type aType) override {
+ mMaster->RequestAudioData();
+ }
+
+ void HandleVideoWaited(MediaData::Type aType) override {
+ mMaster->RequestVideoData(media::TimeUnit());
+ }
+
+ void HandleVideoSuspendTimeout() override {
+ // Do nothing for we need to decode the 1st video frame to get the
+ // dimensions.
+ }
+
+ void HandleResumeVideoDecoding(const TimeUnit&) override {
+ // We never suspend video decoding in this state.
+ MOZ_ASSERT(false, "Shouldn't have suspended video decoding.");
+ }
+
+ RefPtr<MediaDecoder::SeekPromise> HandleSeek(
+ const SeekTarget& aTarget) override {
+ if (mMaster->mIsMSE) {
+ return StateObject::HandleSeek(aTarget);
+ }
+ // Delay seek request until decoding first frames for non-MSE media.
+ SLOG("Not Enough Data to seek at this stage, queuing seek");
+ mPendingSeek.RejectIfExists(__func__);
+ mPendingSeek.mTarget.emplace(aTarget);
+ return mPendingSeek.mPromise.Ensure(__func__);
+ }
+
+ private:
+ // Notify FirstFrameLoaded if having decoded first frames and
+ // transition to SEEKING if there is any pending seek, or DECODING otherwise.
+ void MaybeFinishDecodeFirstFrame();
+
+ SeekJob mPendingSeek;
+};
+
+/**
+ * Purpose: decode audio/video data for playback.
+ *
+ * Transition to:
+ * DORMANT if playback is paused for a while.
+ * SEEKING if any seek request.
+ * SHUTDOWN if any decode error.
+ * BUFFERING if playback can't continue due to lack of decoded data.
+ * COMPLETED when having decoded all audio/video data.
+ * LOOPING_DECODING when media start seamless looping
+ */
+class MediaDecoderStateMachine::DecodingState
+ : public MediaDecoderStateMachine::StateObject {
+ public:
+ explicit DecodingState(Master* aPtr)
+ : StateObject(aPtr), mDormantTimer(OwnerThread()) {}
+
+ void Enter();
+
+ void Exit() override {
+ if (!mDecodeStartTime.IsNull()) {
+ TimeDuration decodeDuration = TimeStamp::Now() - mDecodeStartTime;
+ SLOG("Exiting DECODING, decoded for %.3lfs", decodeDuration.ToSeconds());
+ }
+ mDormantTimer.Reset();
+ mOnAudioPopped.DisconnectIfExists();
+ mOnVideoPopped.DisconnectIfExists();
+ }
+
+ void Step() override;
+
+ State GetState() const override { return DECODER_STATE_DECODING; }
+
+ void HandleAudioDecoded(AudioData* aAudio) override {
+ mMaster->PushAudio(aAudio);
+ DispatchDecodeTasksIfNeeded();
+ MaybeStopPrerolling();
+ }
+
+ void HandleVideoDecoded(VideoData* aVideo) override {
+ // We only do this check when we're not looping, which can be known by
+ // checking the queue's offset.
+ const auto currentTime = mMaster->GetMediaTime();
+ if (aVideo->GetEndTime() < currentTime &&
+ VideoQueue().GetOffset() == media::TimeUnit::Zero()) {
+ if (!mVideoFirstLateTime) {
+ mVideoFirstLateTime = Some(TimeStamp::Now());
+ }
+ PROFILER_MARKER("Video falling behind", MEDIA_PLAYBACK, {},
+ VideoFallingBehindMarker, aVideo->mTime.ToMicroseconds(),
+ currentTime.ToMicroseconds());
+ SLOG("video %" PRId64 " starts being late (current=%" PRId64 ")",
+ aVideo->mTime.ToMicroseconds(), currentTime.ToMicroseconds());
+ } else {
+ mVideoFirstLateTime.reset();
+ }
+ mMaster->PushVideo(aVideo);
+ DispatchDecodeTasksIfNeeded();
+ MaybeStopPrerolling();
+ }
+
+ void HandleAudioCanceled() override { mMaster->RequestAudioData(); }
+
+ void HandleVideoCanceled() override {
+ mMaster->RequestVideoData(mMaster->GetMediaTime(),
+ ShouldRequestNextKeyFrame());
+ }
+
+ void HandleEndOfAudio() override;
+ void HandleEndOfVideo() override;
+
+ void HandleWaitingForAudio() override {
+ mMaster->WaitForData(MediaData::Type::AUDIO_DATA);
+ MaybeStopPrerolling();
+ }
+
+ void HandleWaitingForVideo() override {
+ mMaster->WaitForData(MediaData::Type::VIDEO_DATA);
+ MaybeStopPrerolling();
+ }
+
+ void HandleAudioWaited(MediaData::Type aType) override {
+ mMaster->RequestAudioData();
+ }
+
+ void HandleVideoWaited(MediaData::Type aType) override {
+ mMaster->RequestVideoData(mMaster->GetMediaTime(),
+ ShouldRequestNextKeyFrame());
+ }
+
+ void HandleAudioCaptured() override {
+ MaybeStopPrerolling();
+ // MediaSink is changed. Schedule Step() to check if we can start playback.
+ mMaster->ScheduleStateMachine();
+ }
+
+ void HandleVideoSuspendTimeout() override {
+ // No video, so nothing to suspend.
+ if (!mMaster->HasVideo()) {
+ return;
+ }
+
+ PROFILER_MARKER_UNTYPED("MDSM::EnterVideoSuspend", MEDIA_PLAYBACK);
+ mMaster->mVideoDecodeSuspended = true;
+ mMaster->mOnPlaybackEvent.Notify(MediaPlaybackEvent::EnterVideoSuspend);
+ Reader()->SetVideoBlankDecode(true);
+ }
+
+ void HandlePlayStateChanged(MediaDecoder::PlayState aPlayState) override {
+ if (aPlayState == MediaDecoder::PLAY_STATE_PLAYING) {
+ // Schedule Step() to check if we can start playback.
+ mMaster->ScheduleStateMachine();
+ // Try to dispatch decoding tasks for mMinimizePreroll might be reset.
+ DispatchDecodeTasksIfNeeded();
+ }
+
+ if (aPlayState == MediaDecoder::PLAY_STATE_PAUSED) {
+ StartDormantTimer();
+ mVideoFirstLateTime.reset();
+ } else {
+ mDormantTimer.Reset();
+ }
+ }
+
+ void GetDebugInfo(
+ dom::MediaDecoderStateMachineDecodingStateDebugInfo& aInfo) override {
+ aInfo.mIsPrerolling = mIsPrerolling;
+ }
+
+ void HandleLoopingChanged() override { SetDecodingState(); }
+
+ protected:
+ virtual void EnsureAudioDecodeTaskQueued();
+ virtual void EnsureVideoDecodeTaskQueued();
+
+ virtual bool ShouldStopPrerolling() const {
+ return mIsPrerolling &&
+ (DonePrerollingAudio() ||
+ IsWaitingData(MediaData::Type::AUDIO_DATA)) &&
+ (DonePrerollingVideo() ||
+ IsWaitingData(MediaData::Type::VIDEO_DATA));
+ }
+
+ virtual bool IsWaitingData(MediaData::Type aType) const {
+ if (aType == MediaData::Type::AUDIO_DATA) {
+ return mMaster->IsWaitingAudioData();
+ }
+ MOZ_ASSERT(aType == MediaData::Type::VIDEO_DATA);
+ return mMaster->IsWaitingVideoData();
+ }
+
+ void MaybeStopPrerolling() {
+ if (ShouldStopPrerolling()) {
+ mIsPrerolling = false;
+ // Check if we can start playback.
+ mMaster->ScheduleStateMachine();
+ }
+ }
+
+ bool ShouldRequestNextKeyFrame() const {
+ if (!mVideoFirstLateTime) {
+ return false;
+ }
+ const double elapsedTimeMs =
+ (TimeStamp::Now() - *mVideoFirstLateTime).ToMilliseconds();
+ const bool rv = elapsedTimeMs >=
+ StaticPrefs::media_decoder_skip_when_video_too_slow_ms();
+ if (rv) {
+ PROFILER_MARKER_UNTYPED("Skipping to next keyframe", MEDIA_PLAYBACK);
+ SLOG(
+ "video has been late behind media time for %f ms, should skip to "
+ "next key frame",
+ elapsedTimeMs);
+ }
+ return rv;
+ }
+
+ virtual bool IsBufferingAllowed() const { return true; }
+
+ private:
+ void DispatchDecodeTasksIfNeeded();
+ void MaybeStartBuffering();
+
+ // At the start of decoding we want to "preroll" the decode until we've
+ // got a few frames decoded before we consider whether decode is falling
+ // behind. Otherwise our "we're falling behind" logic will trigger
+ // unnecessarily if we start playing as soon as the first sample is
+ // decoded. These two fields store how many video frames and audio
+ // samples we must consume before are considered to be finished prerolling.
+ TimeUnit AudioPrerollThreshold() const {
+ return (mMaster->mAmpleAudioThreshold / 2)
+ .MultDouble(mMaster->mPlaybackRate);
+ }
+
+ uint32_t VideoPrerollFrames() const {
+ return std::min(
+ static_cast<uint32_t>(
+ mMaster->GetAmpleVideoFrames() / 2. * mMaster->mPlaybackRate + 1),
+ sVideoQueueDefaultSize);
+ }
+
+ bool DonePrerollingAudio() const {
+ return !mMaster->IsAudioDecoding() ||
+ mMaster->GetDecodedAudioDuration() >= AudioPrerollThreshold();
+ }
+
+ bool DonePrerollingVideo() const {
+ return !mMaster->IsVideoDecoding() ||
+ static_cast<uint32_t>(mMaster->VideoQueue().GetSize()) >=
+ VideoPrerollFrames();
+ }
+
+ void StartDormantTimer() {
+ if (!mMaster->mMediaSeekable) {
+ // Don't enter dormant if the media is not seekable because we need to
+ // seek when exiting dormant.
+ return;
+ }
+
+ auto timeout = StaticPrefs::media_dormant_on_pause_timeout_ms();
+ if (timeout < 0) {
+ // Disabled when timeout is negative.
+ return;
+ }
+
+ if (timeout == 0) {
+ // Enter dormant immediately without scheduling a timer.
+ SetState<DormantState>();
+ return;
+ }
+
+ if (mMaster->mMinimizePreroll) {
+ SetState<DormantState>();
+ return;
+ }
+
+ TimeStamp target =
+ TimeStamp::Now() + TimeDuration::FromMilliseconds(timeout);
+
+ mDormantTimer.Ensure(
+ target,
+ [this]() {
+ AUTO_PROFILER_LABEL("DecodingState::StartDormantTimer:SetDormant",
+ MEDIA_PLAYBACK);
+ mDormantTimer.CompleteRequest();
+ SetState<DormantState>();
+ },
+ [this]() { mDormantTimer.CompleteRequest(); });
+ }
+
+ // Time at which we started decoding.
+ TimeStamp mDecodeStartTime;
+
+ // When we start decoding (either for the first time, or after a pause)
+ // we may be low on decoded data. We don't want our "low data" logic to
+ // kick in and decide that we're low on decoded data because the download
+ // can't keep up with the decode, and cause us to pause playback. So we
+ // have a "preroll" stage, where we ignore the results of our "low data"
+ // logic during the first few frames of our decode. This occurs during
+ // playback.
+ bool mIsPrerolling = true;
+
+ // Fired when playback is paused for a while to enter dormant.
+ DelayedScheduler mDormantTimer;
+
+ MediaEventListener mOnAudioPopped;
+ MediaEventListener mOnVideoPopped;
+
+ // If video has been later than the media time, this will records when the
+ // video started being late. It will be reset once video catches up with the
+ // media time.
+ Maybe<TimeStamp> mVideoFirstLateTime;
+};
+
+/**
+ * Purpose: decode audio data for playback when media is in seamless
+ * looping, we will adjust media time to make samples time monotonically
+ * increasing. All its methods runs on its owner thread (MDSM thread).
+ *
+ * Transition to:
+ * DORMANT if playback is paused for a while.
+ * SEEKING if any seek request.
+ * SHUTDOWN if any decode error.
+ * BUFFERING if playback can't continue due to lack of decoded data.
+ * COMPLETED when the media resource is closed and no data is available
+ * anymore.
+ * DECODING when media stops seamless looping.
+ */
+class MediaDecoderStateMachine::LoopingDecodingState
+ : public MediaDecoderStateMachine::DecodingState {
+ public:
+ explicit LoopingDecodingState(Master* aPtr)
+ : DecodingState(aPtr),
+ mIsReachingAudioEOS(!mMaster->IsAudioDecoding()),
+ mIsReachingVideoEOS(!mMaster->IsVideoDecoding()),
+ mAudioEndedBeforeEnteringStateWithoutDuration(false),
+ mVideoEndedBeforeEnteringStateWithoutDuration(false) {
+ MOZ_ASSERT(mMaster->mLooping);
+ // If the track has reached EOS and we already have its last data, then we
+ // can know its duration. But if playback starts from EOS (due to seeking),
+ // the decoded end time would be zero because none of data gets decoded yet.
+ if (mIsReachingAudioEOS) {
+ if (mMaster->HasLastDecodedData(MediaData::Type::AUDIO_DATA) &&
+ !mMaster->mAudioTrackDecodedDuration) {
+ mMaster->mAudioTrackDecodedDuration.emplace(
+ mMaster->mDecodedAudioEndTime);
+ } else {
+ mAudioEndedBeforeEnteringStateWithoutDuration = true;
+ }
+ }
+
+ if (mIsReachingVideoEOS) {
+ if (mMaster->HasLastDecodedData(MediaData::Type::VIDEO_DATA) &&
+ !mMaster->mVideoTrackDecodedDuration) {
+ mMaster->mVideoTrackDecodedDuration.emplace(
+ mMaster->mDecodedVideoEndTime);
+ } else {
+ mVideoEndedBeforeEnteringStateWithoutDuration = true;
+ }
+ }
+
+ // If we've looped at least once before, the master's media queues have
+ // already stored some adjusted data. If a track has reached EOS, we need to
+ // update queue offset correctly. Otherwise, it would cause a/v unsync.
+ if (mMaster->mOriginalDecodedDuration != media::TimeUnit::Zero()) {
+ if (mIsReachingAudioEOS && mMaster->HasAudio()) {
+ AudioQueue().SetOffset(AudioQueue().GetOffset() +
+ mMaster->mOriginalDecodedDuration);
+ }
+ if (mIsReachingVideoEOS && mMaster->HasVideo()) {
+ VideoQueue().SetOffset(VideoQueue().GetOffset() +
+ mMaster->mOriginalDecodedDuration);
+ }
+ }
+ }
+
+ void Enter() {
+ UpdatePlaybackPositionToZeroIfNeeded();
+ if (mMaster->HasAudio() && mIsReachingAudioEOS) {
+ SLOG("audio has ended, request the data again.");
+ RequestDataFromStartPosition(TrackInfo::TrackType::kAudioTrack);
+ }
+ if (mMaster->HasVideo() && mIsReachingVideoEOS) {
+ SLOG("video has ended, request the data again.");
+ RequestDataFromStartPosition(TrackInfo::TrackType::kVideoTrack);
+ }
+ DecodingState::Enter();
+ }
+
+ void Exit() override {
+ MOZ_DIAGNOSTIC_ASSERT(mMaster->OnTaskQueue());
+ SLOG("Leaving looping state, offset [a=%" PRId64 ",v=%" PRId64
+ "], endtime [a=%" PRId64 ",v=%" PRId64 "], track duration [a=%" PRId64
+ ",v=%" PRId64 "], waiting=%s",
+ AudioQueue().GetOffset().ToMicroseconds(),
+ VideoQueue().GetOffset().ToMicroseconds(),
+ mMaster->mDecodedAudioEndTime.ToMicroseconds(),
+ mMaster->mDecodedVideoEndTime.ToMicroseconds(),
+ mMaster->mAudioTrackDecodedDuration
+ ? mMaster->mAudioTrackDecodedDuration->ToMicroseconds()
+ : 0,
+ mMaster->mVideoTrackDecodedDuration
+ ? mMaster->mVideoTrackDecodedDuration->ToMicroseconds()
+ : 0,
+ mDataWaitingTimestampAdjustment
+ ? MediaData::TypeToStr(mDataWaitingTimestampAdjustment->mType)
+ : "none");
+ if (ShouldDiscardLoopedData(MediaData::Type::AUDIO_DATA)) {
+ DiscardLoopedData(MediaData::Type::AUDIO_DATA);
+ }
+ if (ShouldDiscardLoopedData(MediaData::Type::VIDEO_DATA)) {
+ DiscardLoopedData(MediaData::Type::VIDEO_DATA);
+ }
+
+ if (mMaster->HasAudio() && HasDecodedLastAudioFrame()) {
+ SLOG("Mark audio queue as finished");
+ mMaster->mAudioDataRequest.DisconnectIfExists();
+ mMaster->mAudioWaitRequest.DisconnectIfExists();
+ AudioQueue().Finish();
+ }
+ if (mMaster->HasVideo() && HasDecodedLastVideoFrame()) {
+ SLOG("Mark video queue as finished");
+ mMaster->mVideoDataRequest.DisconnectIfExists();
+ mMaster->mVideoWaitRequest.DisconnectIfExists();
+ VideoQueue().Finish();
+ }
+
+ if (mWaitingAudioDataFromStart) {
+ mMaster->mMediaSink->EnableTreatAudioUnderrunAsSilence(false);
+ }
+
+ // Clear waiting data should be done after marking queue as finished.
+ mDataWaitingTimestampAdjustment = nullptr;
+
+ mAudioDataRequest.DisconnectIfExists();
+ mVideoDataRequest.DisconnectIfExists();
+ mAudioSeekRequest.DisconnectIfExists();
+ mVideoSeekRequest.DisconnectIfExists();
+ DecodingState::Exit();
+ }
+
+ ~LoopingDecodingState() {
+ MOZ_DIAGNOSTIC_ASSERT(!mAudioDataRequest.Exists());
+ MOZ_DIAGNOSTIC_ASSERT(!mVideoDataRequest.Exists());
+ MOZ_DIAGNOSTIC_ASSERT(!mAudioSeekRequest.Exists());
+ MOZ_DIAGNOSTIC_ASSERT(!mVideoSeekRequest.Exists());
+ }
+
+ State GetState() const override { return DECODER_STATE_LOOPING_DECODING; }
+
+ void HandleAudioDecoded(AudioData* aAudio) override {
+ // TODO : check if we need to update mOriginalDecodedDuration
+
+ if (mWaitingAudioDataFromStart) {
+ mMaster->mMediaSink->EnableTreatAudioUnderrunAsSilence(false);
+ mWaitingAudioDataFromStart = false;
+ }
+
+ // After pushing data to the queue, timestamp might be adjusted.
+ DecodingState::HandleAudioDecoded(aAudio);
+ mMaster->mDecodedAudioEndTime =
+ std::max(aAudio->GetEndTime(), mMaster->mDecodedAudioEndTime);
+ SLOG("audio sample after time-adjustment [%" PRId64 ",%" PRId64 "]",
+ aAudio->mTime.ToMicroseconds(), aAudio->GetEndTime().ToMicroseconds());
+ }
+
+ void HandleVideoDecoded(VideoData* aVideo) override {
+ // TODO : check if we need to update mOriginalDecodedDuration
+
+ // After pushing data to the queue, timestamp might be adjusted.
+ DecodingState::HandleVideoDecoded(aVideo);
+ mMaster->mDecodedVideoEndTime =
+ std::max(aVideo->GetEndTime(), mMaster->mDecodedVideoEndTime);
+ SLOG("video sample after time-adjustment [%" PRId64 ",%" PRId64 "]",
+ aVideo->mTime.ToMicroseconds(), aVideo->GetEndTime().ToMicroseconds());
+ }
+
+ void HandleEndOfAudio() override {
+ mIsReachingAudioEOS = true;
+ if (!mMaster->mAudioTrackDecodedDuration &&
+ mMaster->HasLastDecodedData(MediaData::Type::AUDIO_DATA)) {
+ mMaster->mAudioTrackDecodedDuration.emplace(
+ mMaster->mDecodedAudioEndTime);
+ }
+ if (DetermineOriginalDecodedDurationIfNeeded()) {
+ AudioQueue().SetOffset(AudioQueue().GetOffset() +
+ mMaster->mOriginalDecodedDuration);
+ }
+
+ SLOG(
+ "received audio EOS when seamless looping, starts seeking, "
+ "audioLoopingOffset=[%" PRId64 "], mAudioTrackDecodedDuration=[%" PRId64
+ "]",
+ AudioQueue().GetOffset().ToMicroseconds(),
+ mMaster->mAudioTrackDecodedDuration->ToMicroseconds());
+ if (!IsRequestingDataFromStartPosition(MediaData::Type::AUDIO_DATA)) {
+ RequestDataFromStartPosition(TrackInfo::TrackType::kAudioTrack);
+ }
+ ProcessSamplesWaitingAdjustmentIfAny();
+ }
+
+ void HandleEndOfVideo() override {
+ mIsReachingVideoEOS = true;
+ if (!mMaster->mVideoTrackDecodedDuration &&
+ mMaster->HasLastDecodedData(MediaData::Type::VIDEO_DATA)) {
+ mMaster->mVideoTrackDecodedDuration.emplace(
+ mMaster->mDecodedVideoEndTime);
+ }
+ if (DetermineOriginalDecodedDurationIfNeeded()) {
+ VideoQueue().SetOffset(VideoQueue().GetOffset() +
+ mMaster->mOriginalDecodedDuration);
+ }
+
+ SLOG(
+ "received video EOS when seamless looping, starts seeking, "
+ "videoLoopingOffset=[%" PRId64 "], mVideoTrackDecodedDuration=[%" PRId64
+ "]",
+ VideoQueue().GetOffset().ToMicroseconds(),
+ mMaster->mVideoTrackDecodedDuration->ToMicroseconds());
+ if (!IsRequestingDataFromStartPosition(MediaData::Type::VIDEO_DATA)) {
+ RequestDataFromStartPosition(TrackInfo::TrackType::kVideoTrack);
+ }
+ ProcessSamplesWaitingAdjustmentIfAny();
+ }
+
+ private:
+ void RequestDataFromStartPosition(TrackInfo::TrackType aType) {
+ MOZ_DIAGNOSTIC_ASSERT(aType == TrackInfo::TrackType::kAudioTrack ||
+ aType == TrackInfo::TrackType::kVideoTrack);
+
+ const bool isAudio = aType == TrackInfo::TrackType::kAudioTrack;
+ MOZ_ASSERT_IF(isAudio, mMaster->HasAudio());
+ MOZ_ASSERT_IF(!isAudio, mMaster->HasVideo());
+
+ if (IsReaderSeeking()) {
+ MOZ_ASSERT(!mPendingSeekingType);
+ mPendingSeekingType = Some(aType);
+ SLOG("Delay %s seeking until the reader finishes current seeking",
+ isAudio ? "audio" : "video");
+ return;
+ }
+
+ auto& seekRequest = isAudio ? mAudioSeekRequest : mVideoSeekRequest;
+ Reader()->ResetDecode(aType);
+ Reader()
+ ->Seek(SeekTarget(media::TimeUnit::Zero(), SeekTarget::Type::Accurate,
+ isAudio ? SeekTarget::Track::AudioOnly
+ : SeekTarget::Track::VideoOnly))
+ ->Then(
+ OwnerThread(), __func__,
+ [this, isAudio, master = RefPtr{mMaster}]() mutable -> void {
+ AUTO_PROFILER_LABEL(
+ nsPrintfCString(
+ "LoopingDecodingState::RequestDataFromStartPosition(%s)::"
+ "SeekResolved",
+ isAudio ? "audio" : "video")
+ .get(),
+ MEDIA_PLAYBACK);
+ if (auto& state = master->mStateObj;
+ state &&
+ state->GetState() != DECODER_STATE_LOOPING_DECODING) {
+ MOZ_RELEASE_ASSERT(false, "This shouldn't happen!");
+ return;
+ }
+ if (isAudio) {
+ mAudioSeekRequest.Complete();
+ } else {
+ mVideoSeekRequest.Complete();
+ }
+ SLOG(
+ "seeking completed, start to request first %s sample "
+ "(queued=%zu, decoder-queued=%zu)",
+ isAudio ? "audio" : "video",
+ isAudio ? AudioQueue().GetSize() : VideoQueue().GetSize(),
+ isAudio ? Reader()->SizeOfAudioQueueInFrames()
+ : Reader()->SizeOfVideoQueueInFrames());
+ if (isAudio) {
+ RequestAudioDataFromReaderAfterEOS();
+ } else {
+ RequestVideoDataFromReaderAfterEOS();
+ }
+ if (mPendingSeekingType) {
+ auto seekingType = *mPendingSeekingType;
+ mPendingSeekingType.reset();
+ SLOG("Perform pending %s seeking", TrackTypeToStr(seekingType));
+ RequestDataFromStartPosition(seekingType);
+ }
+ },
+ [this, isAudio, master = RefPtr{mMaster}](
+ const SeekRejectValue& aReject) mutable -> void {
+ AUTO_PROFILER_LABEL(
+ nsPrintfCString("LoopingDecodingState::"
+ "RequestDataFromStartPosition(%s)::"
+ "SeekRejected",
+ isAudio ? "audio" : "video")
+ .get(),
+ MEDIA_PLAYBACK);
+ if (auto& state = master->mStateObj;
+ state &&
+ state->GetState() != DECODER_STATE_LOOPING_DECODING) {
+ MOZ_RELEASE_ASSERT(false, "This shouldn't happen!");
+ return;
+ }
+ if (isAudio) {
+ mAudioSeekRequest.Complete();
+ } else {
+ mVideoSeekRequest.Complete();
+ }
+ HandleError(aReject.mError, isAudio);
+ })
+ ->Track(seekRequest);
+ }
+
+ void RequestAudioDataFromReaderAfterEOS() {
+ MOZ_ASSERT(mMaster->HasAudio());
+ Reader()
+ ->RequestAudioData()
+ ->Then(
+ OwnerThread(), __func__,
+ [this, master = RefPtr{mMaster}](const RefPtr<AudioData>& aAudio) {
+ AUTO_PROFILER_LABEL(
+ "LoopingDecodingState::"
+ "RequestAudioDataFromReader::"
+ "RequestDataResolved",
+ MEDIA_PLAYBACK);
+ if (auto& state = master->mStateObj;
+ state &&
+ state->GetState() != DECODER_STATE_LOOPING_DECODING) {
+ MOZ_RELEASE_ASSERT(false, "This shouldn't happen!");
+ return;
+ }
+ mIsReachingAudioEOS = false;
+ mAudioDataRequest.Complete();
+ SLOG(
+ "got audio decoded sample "
+ "[%" PRId64 ",%" PRId64 "]",
+ aAudio->mTime.ToMicroseconds(),
+ aAudio->GetEndTime().ToMicroseconds());
+ if (ShouldPutDataOnWaiting(MediaData::Type::AUDIO_DATA)) {
+ SLOG(
+ "decoded audio sample needs to wait for timestamp "
+ "adjustment after EOS");
+ PutDataOnWaiting(aAudio);
+ return;
+ }
+ HandleAudioDecoded(aAudio);
+ ProcessSamplesWaitingAdjustmentIfAny();
+ },
+ [this, master = RefPtr{mMaster}](const MediaResult& aError) {
+ AUTO_PROFILER_LABEL(
+ "LoopingDecodingState::"
+ "RequestAudioDataFromReader::"
+ "RequestDataRejected",
+ MEDIA_PLAYBACK);
+ if (auto& state = master->mStateObj;
+ state &&
+ state->GetState() != DECODER_STATE_LOOPING_DECODING) {
+ MOZ_RELEASE_ASSERT(false, "This shouldn't happen!");
+ return;
+ }
+ mAudioDataRequest.Complete();
+ HandleError(aError, true /* isAudio */);
+ })
+ ->Track(mAudioDataRequest);
+ }
+
+ void RequestVideoDataFromReaderAfterEOS() {
+ MOZ_ASSERT(mMaster->HasVideo());
+ Reader()
+ ->RequestVideoData(media::TimeUnit(),
+ false /* aRequestNextVideoKeyFrame */)
+ ->Then(
+ OwnerThread(), __func__,
+ [this, master = RefPtr{mMaster}](const RefPtr<VideoData>& aVideo) {
+ AUTO_PROFILER_LABEL(
+ "LoopingDecodingState::"
+ "RequestVideoDataFromReaderAfterEOS()::"
+ "RequestDataResolved",
+ MEDIA_PLAYBACK);
+ if (auto& state = master->mStateObj;
+ state &&
+ state->GetState() != DECODER_STATE_LOOPING_DECODING) {
+ MOZ_RELEASE_ASSERT(false, "This shouldn't happen!");
+ return;
+ }
+ mIsReachingVideoEOS = false;
+ mVideoDataRequest.Complete();
+ SLOG(
+ "got video decoded sample "
+ "[%" PRId64 ",%" PRId64 "]",
+ aVideo->mTime.ToMicroseconds(),
+ aVideo->GetEndTime().ToMicroseconds());
+ if (ShouldPutDataOnWaiting(MediaData::Type::VIDEO_DATA)) {
+ SLOG(
+ "decoded video sample needs to wait for timestamp "
+ "adjustment after EOS");
+ PutDataOnWaiting(aVideo);
+ return;
+ }
+ mMaster->mBypassingSkipToNextKeyFrameCheck = true;
+ HandleVideoDecoded(aVideo);
+ ProcessSamplesWaitingAdjustmentIfAny();
+ },
+ [this, master = RefPtr{mMaster}](const MediaResult& aError) {
+ AUTO_PROFILER_LABEL(
+ "LoopingDecodingState::"
+ "RequestVideoDataFromReaderAfterEOS()::"
+ "RequestDataRejected",
+ MEDIA_PLAYBACK);
+ if (auto& state = master->mStateObj;
+ state &&
+ state->GetState() != DECODER_STATE_LOOPING_DECODING) {
+ MOZ_RELEASE_ASSERT(false, "This shouldn't happen!");
+ return;
+ }
+ mVideoDataRequest.Complete();
+ HandleError(aError, false /* isAudio */);
+ })
+ ->Track(mVideoDataRequest);
+ }
+
+ void UpdatePlaybackPositionToZeroIfNeeded() {
+ // Hasn't reached EOS, no need to adjust playback position.
+ if (!mIsReachingAudioEOS || !mIsReachingVideoEOS) {
+ return;
+ }
+
+ // If we have already reached EOS before starting media sink, the sink
+ // has not started yet and the current position is larger than last decoded
+ // end time, that means we directly seeked to EOS and playback would start
+ // from the start position soon. Therefore, we should reset the position to
+ // 0s so that when media sink starts we can make it start from 0s, not from
+ // EOS position which would result in wrong estimation of decoded audio
+ // duration because decoded data's time which can't be adjusted as offset is
+ // zero would be always less than media sink time.
+ if (!mMaster->mMediaSink->IsStarted() &&
+ (mMaster->mCurrentPosition.Ref() > mMaster->mDecodedAudioEndTime ||
+ mMaster->mCurrentPosition.Ref() > mMaster->mDecodedVideoEndTime)) {
+ mMaster->UpdatePlaybackPositionInternal(TimeUnit::Zero());
+ }
+ }
+
+ void HandleError(const MediaResult& aError, bool aIsAudio);
+
+ bool ShouldRequestData(MediaData::Type aType) const {
+ MOZ_DIAGNOSTIC_ASSERT(aType == MediaData::Type::AUDIO_DATA ||
+ aType == MediaData::Type::VIDEO_DATA);
+ if (aType == MediaData::Type::AUDIO_DATA &&
+ (mAudioSeekRequest.Exists() || mAudioDataRequest.Exists() ||
+ IsDataWaitingForTimestampAdjustment(MediaData::Type::AUDIO_DATA))) {
+ return false;
+ }
+ if (aType == MediaData::Type::VIDEO_DATA &&
+ (mVideoSeekRequest.Exists() || mVideoDataRequest.Exists() ||
+ IsDataWaitingForTimestampAdjustment(MediaData::Type::VIDEO_DATA))) {
+ return false;
+ }
+ return true;
+ }
+
+ void HandleAudioCanceled() override {
+ if (ShouldRequestData(MediaData::Type::AUDIO_DATA)) {
+ mMaster->RequestAudioData();
+ }
+ }
+
+ void HandleAudioWaited(MediaData::Type aType) override {
+ if (ShouldRequestData(MediaData::Type::AUDIO_DATA)) {
+ mMaster->RequestAudioData();
+ }
+ }
+
+ void HandleVideoCanceled() override {
+ if (ShouldRequestData(MediaData::Type::VIDEO_DATA)) {
+ mMaster->RequestVideoData(mMaster->GetMediaTime(),
+ ShouldRequestNextKeyFrame());
+ };
+ }
+
+ void HandleVideoWaited(MediaData::Type aType) override {
+ if (ShouldRequestData(MediaData::Type::VIDEO_DATA)) {
+ mMaster->RequestVideoData(mMaster->GetMediaTime(),
+ ShouldRequestNextKeyFrame());
+ };
+ }
+
+ void EnsureAudioDecodeTaskQueued() override {
+ if (!ShouldRequestData(MediaData::Type::AUDIO_DATA)) {
+ return;
+ }
+ DecodingState::EnsureAudioDecodeTaskQueued();
+ }
+
+ void EnsureVideoDecodeTaskQueued() override {
+ if (!ShouldRequestData(MediaData::Type::VIDEO_DATA)) {
+ return;
+ }
+ DecodingState::EnsureVideoDecodeTaskQueued();
+ }
+
+ bool DetermineOriginalDecodedDurationIfNeeded() {
+ // Duration would only need to be set once, unless we get more data which is
+ // larger than the duration. That can happen on MSE (reopen stream).
+ if (mMaster->mOriginalDecodedDuration != media::TimeUnit::Zero()) {
+ return true;
+ }
+
+ // Single track situations
+ if (mMaster->HasAudio() && !mMaster->HasVideo()) {
+ MOZ_ASSERT(mMaster->mAudioTrackDecodedDuration);
+ mMaster->mOriginalDecodedDuration = *mMaster->mAudioTrackDecodedDuration;
+ SLOG("audio only, duration=%" PRId64,
+ mMaster->mOriginalDecodedDuration.ToMicroseconds());
+ return true;
+ }
+ if (mMaster->HasVideo() && !mMaster->HasAudio()) {
+ MOZ_ASSERT(mMaster->mVideoTrackDecodedDuration);
+ mMaster->mOriginalDecodedDuration = *mMaster->mVideoTrackDecodedDuration;
+ SLOG("video only, duration=%" PRId64,
+ mMaster->mOriginalDecodedDuration.ToMicroseconds());
+ return true;
+ }
+
+ MOZ_ASSERT(mMaster->HasAudio() && mMaster->HasVideo());
+
+ // Both tracks have ended so that we can check which track is longer.
+ if (mMaster->mAudioTrackDecodedDuration &&
+ mMaster->mVideoTrackDecodedDuration) {
+ mMaster->mOriginalDecodedDuration =
+ std::max(*mMaster->mVideoTrackDecodedDuration,
+ *mMaster->mAudioTrackDecodedDuration);
+ SLOG("Both tracks ended, original duration=%" PRId64 " (a=%" PRId64
+ ", v=%" PRId64 ")",
+ mMaster->mOriginalDecodedDuration.ToMicroseconds(),
+ mMaster->mAudioTrackDecodedDuration->ToMicroseconds(),
+ mMaster->mVideoTrackDecodedDuration->ToMicroseconds());
+ return true;
+ }
+ // When entering the state, video has ended but audio hasn't, which means
+ // audio is longer.
+ if (mMaster->mAudioTrackDecodedDuration &&
+ mVideoEndedBeforeEnteringStateWithoutDuration) {
+ mMaster->mOriginalDecodedDuration = *mMaster->mAudioTrackDecodedDuration;
+ mVideoEndedBeforeEnteringStateWithoutDuration = false;
+ SLOG("audio is longer, duration=%" PRId64,
+ mMaster->mOriginalDecodedDuration.ToMicroseconds());
+ return true;
+ }
+ // When entering the state, audio has ended but video hasn't, which means
+ // video is longer.
+ if (mMaster->mVideoTrackDecodedDuration &&
+ mAudioEndedBeforeEnteringStateWithoutDuration) {
+ mMaster->mOriginalDecodedDuration = *mMaster->mVideoTrackDecodedDuration;
+ mAudioEndedBeforeEnteringStateWithoutDuration = false;
+ SLOG("video is longer, duration=%" PRId64,
+ mMaster->mOriginalDecodedDuration.ToMicroseconds());
+ return true;
+ }
+
+ SLOG("Still waiting for another track ends...");
+ MOZ_ASSERT(!mMaster->mAudioTrackDecodedDuration ||
+ !mMaster->mVideoTrackDecodedDuration);
+ MOZ_ASSERT(mMaster->mOriginalDecodedDuration == media::TimeUnit::Zero());
+ return false;
+ }
+
+ void ProcessSamplesWaitingAdjustmentIfAny() {
+ if (!mDataWaitingTimestampAdjustment) {
+ return;
+ }
+
+ RefPtr<MediaData> data = mDataWaitingTimestampAdjustment;
+ mDataWaitingTimestampAdjustment = nullptr;
+ const bool isAudio = data->mType == MediaData::Type::AUDIO_DATA;
+ SLOG("process %s sample waiting for timestamp adjustment",
+ isAudio ? "audio" : "video");
+ if (isAudio) {
+ // Waiting sample is for next round of looping, so the queue offset
+ // shouldn't be zero. This happens when the track has reached EOS before
+ // entering the state (and looping never happens before). Same for below
+ // video case.
+ if (AudioQueue().GetOffset() == media::TimeUnit::Zero()) {
+ AudioQueue().SetOffset(mMaster->mOriginalDecodedDuration);
+ }
+ HandleAudioDecoded(data->As<AudioData>());
+ } else {
+ MOZ_DIAGNOSTIC_ASSERT(data->mType == MediaData::Type::VIDEO_DATA);
+ if (VideoQueue().GetOffset() == media::TimeUnit::Zero()) {
+ VideoQueue().SetOffset(mMaster->mOriginalDecodedDuration);
+ }
+ HandleVideoDecoded(data->As<VideoData>());
+ }
+ }
+
+ bool IsDataWaitingForTimestampAdjustment(MediaData::Type aType) const {
+ return mDataWaitingTimestampAdjustment &&
+ mDataWaitingTimestampAdjustment->mType == aType;
+ }
+
+ bool ShouldPutDataOnWaiting(MediaData::Type aType) const {
+ // If another track is already waiting, this track shouldn't be waiting.
+ // This case only happens when both tracks reached EOS before entering the
+ // looping decoding state, so we don't know the decoded duration yet (used
+ // to adjust timestamp) But this is fine, because both tracks will start
+ // from 0 so we don't need to adjust them now.
+ if (mDataWaitingTimestampAdjustment &&
+ !IsDataWaitingForTimestampAdjustment(aType)) {
+ return false;
+ }
+
+ // Only have one track, no need to wait.
+ if ((aType == MediaData::Type::AUDIO_DATA && !mMaster->HasVideo()) ||
+ (aType == MediaData::Type::VIDEO_DATA && !mMaster->HasAudio())) {
+ return false;
+ }
+
+ // We don't know the duration yet, so we can't calculate the looping offset.
+ return mMaster->mOriginalDecodedDuration == media::TimeUnit::Zero();
+ }
+
+ void PutDataOnWaiting(MediaData* aData) {
+ MOZ_ASSERT(!mDataWaitingTimestampAdjustment);
+ mDataWaitingTimestampAdjustment = aData;
+ SLOG("put %s [%" PRId64 ",%" PRId64 "] on waiting",
+ MediaData::TypeToStr(aData->mType), aData->mTime.ToMicroseconds(),
+ aData->GetEndTime().ToMicroseconds());
+ MaybeStopPrerolling();
+ }
+
+ bool ShouldDiscardLoopedData(MediaData::Type aType) const {
+ if (!mMaster->mMediaSink->IsStarted()) {
+ return false;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(aType == MediaData::Type::AUDIO_DATA ||
+ aType == MediaData::Type::VIDEO_DATA);
+ const bool isAudio = aType == MediaData::Type::AUDIO_DATA;
+ if (isAudio && !mMaster->HasAudio()) {
+ return false;
+ }
+ if (!isAudio && !mMaster->HasVideo()) {
+ return false;
+ }
+
+ /**
+ * If media cancels looping, we should check whether there is media data
+ * whose time is later than EOS. If so, we should discard them because we
+ * won't have a chance to play them.
+ *
+ * playback last decoded
+ * position EOS data time
+ * ----|---------------|------------|---------> (Increasing timeline)
+ * mCurrent looping mMaster's
+ * ClockTime offset mDecodedXXXEndTime
+ *
+ */
+ const auto offset =
+ isAudio ? AudioQueue().GetOffset() : VideoQueue().GetOffset();
+ const auto endTime =
+ isAudio ? mMaster->mDecodedAudioEndTime : mMaster->mDecodedVideoEndTime;
+ const auto clockTime = mMaster->GetClock();
+ return (offset != media::TimeUnit::Zero() && clockTime < offset &&
+ offset < endTime);
+ }
+
+ void DiscardLoopedData(MediaData::Type aType) {
+ MOZ_DIAGNOSTIC_ASSERT(aType == MediaData::Type::AUDIO_DATA ||
+ aType == MediaData::Type::VIDEO_DATA);
+ const bool isAudio = aType == MediaData::Type::AUDIO_DATA;
+ const auto offset =
+ isAudio ? AudioQueue().GetOffset() : VideoQueue().GetOffset();
+ if (offset == media::TimeUnit::Zero()) {
+ return;
+ }
+
+ SLOG("Discard %s frames after the time=%" PRId64,
+ isAudio ? "audio" : "video", offset.ToMicroseconds());
+ if (isAudio) {
+ DiscardFramesFromTail(AudioQueue(), [&](int64_t aSampleTime) {
+ return aSampleTime > offset.ToMicroseconds();
+ });
+ } else {
+ DiscardFramesFromTail(VideoQueue(), [&](int64_t aSampleTime) {
+ return aSampleTime > offset.ToMicroseconds();
+ });
+ }
+ }
+
+ bool HasDecodedLastAudioFrame() const {
+ // when we're going to leave looping state and have got EOS before, we
+ // should mark audio queue as ended because we have got all data we need.
+ return mAudioDataRequest.Exists() || mAudioSeekRequest.Exists() ||
+ ShouldDiscardLoopedData(MediaData::Type::AUDIO_DATA) ||
+ IsDataWaitingForTimestampAdjustment(MediaData::Type::AUDIO_DATA) ||
+ mIsReachingAudioEOS;
+ }
+
+ bool HasDecodedLastVideoFrame() const {
+ // when we're going to leave looping state and have got EOS before, we
+ // should mark video queue as ended because we have got all data we need.
+ return mVideoDataRequest.Exists() || mVideoSeekRequest.Exists() ||
+ ShouldDiscardLoopedData(MediaData::Type::VIDEO_DATA) ||
+ IsDataWaitingForTimestampAdjustment(MediaData::Type::VIDEO_DATA) ||
+ mIsReachingVideoEOS;
+ }
+
+ bool ShouldStopPrerolling() const override {
+ // These checks is used to handle the media queue aren't opened correctly
+ // because they've been close before entering the looping state. Therefore,
+ // we need to preroll data in order to let new data to reopen the queue
+ // automatically. Otherwise, playback can't start successfully.
+ bool isWaitingForNewData = false;
+ if (mMaster->HasAudio()) {
+ isWaitingForNewData |= (mIsReachingAudioEOS && AudioQueue().IsFinished());
+ }
+ if (mMaster->HasVideo()) {
+ isWaitingForNewData |= (mIsReachingVideoEOS && VideoQueue().IsFinished());
+ }
+ return !isWaitingForNewData && DecodingState::ShouldStopPrerolling();
+ }
+
+ bool IsReaderSeeking() const {
+ return mAudioSeekRequest.Exists() || mVideoSeekRequest.Exists();
+ }
+
+ bool IsWaitingData(MediaData::Type aType) const override {
+ if (aType == MediaData::Type::AUDIO_DATA) {
+ return mMaster->IsWaitingAudioData() ||
+ IsDataWaitingForTimestampAdjustment(MediaData::Type::AUDIO_DATA);
+ }
+ MOZ_DIAGNOSTIC_ASSERT(aType == MediaData::Type::VIDEO_DATA);
+ return mMaster->IsWaitingVideoData() ||
+ IsDataWaitingForTimestampAdjustment(MediaData::Type::VIDEO_DATA);
+ }
+
+ bool IsRequestingDataFromStartPosition(MediaData::Type aType) const {
+ MOZ_DIAGNOSTIC_ASSERT(aType == MediaData::Type::AUDIO_DATA ||
+ aType == MediaData::Type::VIDEO_DATA);
+ if (aType == MediaData::Type::AUDIO_DATA) {
+ return mAudioSeekRequest.Exists() || mAudioDataRequest.Exists();
+ }
+ return mVideoSeekRequest.Exists() || mVideoDataRequest.Exists();
+ }
+
+ bool IsBufferingAllowed() const override {
+ return !mIsReachingAudioEOS && !mIsReachingVideoEOS;
+ }
+
+ bool mIsReachingAudioEOS;
+ bool mIsReachingVideoEOS;
+
+ /**
+ * If we have both tracks which have different length, when one track ends
+ * first, we can't adjust new data from that track if another longer track
+ * hasn't ended yet. The adjusted timestamp needs to be based off the longer
+ * track's last data's timestamp, because otherwise it would cause a deviation
+ * and eventually a/v unsync. Those sample needs to be stored and we will
+ * adjust their timestamp later.
+ *
+ * Following graph explains the situation in details.
+ * o : decoded data with timestamp adjusted or no adjustment (not looping yet)
+ * x : decoded data without timestamp adjustment.
+ * - : stop decoding and nothing happens
+ * EOS : the track reaches to the end. We now know the offset of the track.
+ *
+ * Timeline ----------------------------------->
+ * Track1 : o EOS x - - o
+ * Track2 : o o o EOS o o
+ *
+ * Before reaching track2's EOS, we can't adjust samples from track1 because
+ * track2 might have longer duration than track1. The sample X would be
+ * stored in `mDataWaitingTimestampAdjustment` and we would also stop decoding
+ * for track1.
+ *
+ * After reaching track2's EOS, now we know another track's offset, and the
+ * larger one would be used for `mOriginalDecodedDuration`. Once that duration
+ * has been determined, we will no longer need to put samples on waiting
+ * because we already know how to adjust timestamp.
+ */
+ RefPtr<MediaData> mDataWaitingTimestampAdjustment;
+
+ MozPromiseRequestHolder<MediaFormatReader::SeekPromise> mAudioSeekRequest;
+ MozPromiseRequestHolder<MediaFormatReader::SeekPromise> mVideoSeekRequest;
+ MozPromiseRequestHolder<AudioDataPromise> mAudioDataRequest;
+ MozPromiseRequestHolder<VideoDataPromise> mVideoDataRequest;
+
+ // The media format reader only allows seeking a track at a time, if we're
+ // already in seeking, then delay the new seek until the current one finishes.
+ Maybe<TrackInfo::TrackType> mPendingSeekingType;
+
+ // These are used to track a special case where the playback starts from EOS
+ // position via seeking. So even if EOS has reached, none of data has been
+ // decoded yet. They will be reset when `mOriginalDecodedDuration` is
+ // determined.
+ bool mAudioEndedBeforeEnteringStateWithoutDuration;
+ bool mVideoEndedBeforeEnteringStateWithoutDuration;
+
+ // True if the audio has reached EOS, but the data from the start position is
+ // not avalible yet. We use this to determine whether we should enable
+ // appending silence audio frames into audio backend while audio underrun in
+ // order to keep audio clock running.
+ bool mWaitingAudioDataFromStart = false;
+};
+
+/**
+ * Purpose: seek to a particular new playback position.
+ *
+ * Transition to:
+ * SEEKING if any new seek request.
+ * SHUTDOWN if seek failed.
+ * COMPLETED if the new playback position is the end of the media resource.
+ * NextFrameSeekingState if completing a NextFrameSeekingFromDormantState.
+ * DECODING/LOOPING_DECODING otherwise.
+ */
+class MediaDecoderStateMachine::SeekingState
+ : public MediaDecoderStateMachine::StateObject {
+ public:
+ explicit SeekingState(Master* aPtr)
+ : StateObject(aPtr), mVisibility(static_cast<EventVisibility>(0)) {}
+
+ RefPtr<MediaDecoder::SeekPromise> Enter(SeekJob&& aSeekJob,
+ EventVisibility aVisibility) {
+ mSeekJob = std::move(aSeekJob);
+ mVisibility = aVisibility;
+
+ // Suppressed visibility comes from two cases: (1) leaving dormant state,
+ // and (2) resuming suspended video decoder. We want both cases to be
+ // transparent to the user. So we only notify the change when the seek
+ // request is from the user.
+ if (mVisibility == EventVisibility::Observable) {
+ // Don't stop playback for a video-only seek since we want to keep playing
+ // audio and we don't need to stop playback while leaving dormant for the
+ // playback should has been stopped.
+ mMaster->StopPlayback();
+ mMaster->UpdatePlaybackPositionInternal(mSeekJob.mTarget->GetTime());
+ mMaster->mOnPlaybackEvent.Notify(MediaPlaybackEvent::SeekStarted);
+ mMaster->mOnNextFrameStatus.Notify(
+ MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE_SEEKING);
+ }
+
+ RefPtr<MediaDecoder::SeekPromise> p = mSeekJob.mPromise.Ensure(__func__);
+
+ DoSeek();
+
+ return p;
+ }
+
+ virtual void Exit() override = 0;
+
+ State GetState() const override = 0;
+
+ void HandleAudioDecoded(AudioData* aAudio) override = 0;
+ void HandleVideoDecoded(VideoData* aVideo) override = 0;
+ void HandleAudioWaited(MediaData::Type aType) override = 0;
+ void HandleVideoWaited(MediaData::Type aType) override = 0;
+
+ void HandleVideoSuspendTimeout() override {
+ // Do nothing since we want a valid video frame to show when seek is done.
+ }
+
+ void HandleResumeVideoDecoding(const TimeUnit&) override {
+ // Do nothing. We will resume video decoding in the decoding state.
+ }
+
+ // We specially handle next frame seeks by ignoring them if we're already
+ // seeking.
+ RefPtr<MediaDecoder::SeekPromise> HandleSeek(
+ const SeekTarget& aTarget) override {
+ if (aTarget.IsNextFrame()) {
+ // We ignore next frame seeks if we already have a seek pending
+ SLOG("Already SEEKING, ignoring seekToNextFrame");
+ MOZ_ASSERT(!mSeekJob.mPromise.IsEmpty(), "Seek shouldn't be finished");
+ return MediaDecoder::SeekPromise::CreateAndReject(
+ /* aRejectValue = */ true, __func__);
+ }
+
+ return StateObject::HandleSeek(aTarget);
+ }
+
+ protected:
+ SeekJob mSeekJob;
+ EventVisibility mVisibility;
+
+ virtual void DoSeek() = 0;
+ // Transition to the next state (defined by the subclass) when seek is
+ // completed.
+ virtual void GoToNextState() { SetDecodingState(); }
+ void SeekCompleted();
+ virtual TimeUnit CalculateNewCurrentTime() const = 0;
+};
+
+class MediaDecoderStateMachine::AccurateSeekingState
+ : public MediaDecoderStateMachine::SeekingState {
+ public:
+ explicit AccurateSeekingState(Master* aPtr) : SeekingState(aPtr) {}
+
+ State GetState() const override { return DECODER_STATE_SEEKING_ACCURATE; }
+
+ RefPtr<MediaDecoder::SeekPromise> Enter(SeekJob&& aSeekJob,
+ EventVisibility aVisibility) {
+ MOZ_ASSERT(aSeekJob.mTarget->IsAccurate() || aSeekJob.mTarget->IsFast());
+ mCurrentTimeBeforeSeek = mMaster->GetMediaTime();
+ return SeekingState::Enter(std::move(aSeekJob), aVisibility);
+ }
+
+ void Exit() override {
+ // Disconnect MediaDecoder.
+ mSeekJob.RejectIfExists(__func__);
+
+ // Disconnect ReaderProxy.
+ mSeekRequest.DisconnectIfExists();
+
+ mWaitRequest.DisconnectIfExists();
+ }
+
+ void HandleAudioDecoded(AudioData* aAudio) override {
+ MOZ_ASSERT(!mDoneAudioSeeking || !mDoneVideoSeeking,
+ "Seek shouldn't be finished");
+ MOZ_ASSERT(aAudio);
+
+ AdjustFastSeekIfNeeded(aAudio);
+
+ if (mSeekJob.mTarget->IsFast()) {
+ // Non-precise seek; we can stop the seek at the first sample.
+ mMaster->PushAudio(aAudio);
+ mDoneAudioSeeking = true;
+ } else {
+ nsresult rv = DropAudioUpToSeekTarget(aAudio);
+ if (NS_FAILED(rv)) {
+ mMaster->DecodeError(rv);
+ return;
+ }
+ }
+
+ if (!mDoneAudioSeeking) {
+ RequestAudioData();
+ return;
+ }
+ MaybeFinishSeek();
+ }
+
+ void HandleVideoDecoded(VideoData* aVideo) override {
+ MOZ_ASSERT(!mDoneAudioSeeking || !mDoneVideoSeeking,
+ "Seek shouldn't be finished");
+ MOZ_ASSERT(aVideo);
+
+ AdjustFastSeekIfNeeded(aVideo);
+
+ if (mSeekJob.mTarget->IsFast()) {
+ // Non-precise seek. We can stop the seek at the first sample.
+ mMaster->PushVideo(aVideo);
+ mDoneVideoSeeking = true;
+ } else {
+ nsresult rv = DropVideoUpToSeekTarget(aVideo);
+ if (NS_FAILED(rv)) {
+ mMaster->DecodeError(rv);
+ return;
+ }
+ }
+
+ if (!mDoneVideoSeeking) {
+ RequestVideoData();
+ return;
+ }
+ MaybeFinishSeek();
+ }
+
+ void HandleWaitingForAudio() override {
+ MOZ_ASSERT(!mDoneAudioSeeking);
+ mMaster->WaitForData(MediaData::Type::AUDIO_DATA);
+ }
+
+ void HandleAudioCanceled() override {
+ MOZ_ASSERT(!mDoneAudioSeeking);
+ RequestAudioData();
+ }
+
+ void HandleEndOfAudio() override {
+ HandleEndOfAudioInternal();
+ MaybeFinishSeek();
+ }
+
+ void HandleWaitingForVideo() override {
+ MOZ_ASSERT(!mDoneVideoSeeking);
+ mMaster->WaitForData(MediaData::Type::VIDEO_DATA);
+ }
+
+ void HandleVideoCanceled() override {
+ MOZ_ASSERT(!mDoneVideoSeeking);
+ RequestVideoData();
+ }
+
+ void HandleEndOfVideo() override {
+ HandleEndOfVideoInternal();
+ MaybeFinishSeek();
+ }
+
+ void HandleAudioWaited(MediaData::Type aType) override {
+ MOZ_ASSERT(!mDoneAudioSeeking || !mDoneVideoSeeking,
+ "Seek shouldn't be finished");
+
+ RequestAudioData();
+ }
+
+ void HandleVideoWaited(MediaData::Type aType) override {
+ MOZ_ASSERT(!mDoneAudioSeeking || !mDoneVideoSeeking,
+ "Seek shouldn't be finished");
+
+ RequestVideoData();
+ }
+
+ void DoSeek() override {
+ mDoneAudioSeeking = !Info().HasAudio();
+ mDoneVideoSeeking = !Info().HasVideo();
+
+ // Resetting decode should be called after stopping media sink, which can
+ // ensure that we have an empty media queue before seeking the demuxer.
+ mMaster->StopMediaSink();
+ mMaster->ResetDecode();
+
+ DemuxerSeek();
+ }
+
+ TimeUnit CalculateNewCurrentTime() const override {
+ const auto seekTime = mSeekJob.mTarget->GetTime();
+
+ // For the accurate seek, we always set the newCurrentTime = seekTime so
+ // that the updated HTMLMediaElement.currentTime will always be the seek
+ // target; we rely on the MediaSink to handles the gap between the
+ // newCurrentTime and the real decoded samples' start time.
+ if (mSeekJob.mTarget->IsAccurate()) {
+ return seekTime;
+ }
+
+ // For the fast seek, we update the newCurrentTime with the decoded audio
+ // and video samples, set it to be the one which is closet to the seekTime.
+ if (mSeekJob.mTarget->IsFast()) {
+ RefPtr<AudioData> audio = AudioQueue().PeekFront();
+ RefPtr<VideoData> video = VideoQueue().PeekFront();
+
+ // A situation that both audio and video approaches the end.
+ if (!audio && !video) {
+ return seekTime;
+ }
+
+ const int64_t audioStart =
+ audio ? audio->mTime.ToMicroseconds() : INT64_MAX;
+ const int64_t videoStart =
+ video ? video->mTime.ToMicroseconds() : INT64_MAX;
+ const int64_t audioGap = std::abs(audioStart - seekTime.ToMicroseconds());
+ const int64_t videoGap = std::abs(videoStart - seekTime.ToMicroseconds());
+ return TimeUnit::FromMicroseconds(audioGap <= videoGap ? audioStart
+ : videoStart);
+ }
+
+ MOZ_ASSERT(false, "AccurateSeekTask doesn't handle other seek types.");
+ return TimeUnit::Zero();
+ }
+
+ protected:
+ void DemuxerSeek() {
+ // Request the demuxer to perform seek.
+ Reader()
+ ->Seek(mSeekJob.mTarget.ref())
+ ->Then(
+ OwnerThread(), __func__,
+ [this](const media::TimeUnit& aUnit) { OnSeekResolved(aUnit); },
+ [this](const SeekRejectValue& aReject) { OnSeekRejected(aReject); })
+ ->Track(mSeekRequest);
+ }
+
+ void OnSeekResolved(media::TimeUnit) {
+ AUTO_PROFILER_LABEL("AccurateSeekingState::OnSeekResolved", MEDIA_PLAYBACK);
+ mSeekRequest.Complete();
+
+ // We must decode the first samples of active streams, so we can determine
+ // the new stream time. So dispatch tasks to do that.
+ if (!mDoneVideoSeeking) {
+ RequestVideoData();
+ }
+ if (!mDoneAudioSeeking) {
+ RequestAudioData();
+ }
+ }
+
+ void OnSeekRejected(const SeekRejectValue& aReject) {
+ AUTO_PROFILER_LABEL("AccurateSeekingState::OnSeekRejected", MEDIA_PLAYBACK);
+ mSeekRequest.Complete();
+
+ if (aReject.mError == NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA) {
+ SLOG("OnSeekRejected reason=WAITING_FOR_DATA type=%s",
+ MediaData::TypeToStr(aReject.mType));
+ MOZ_ASSERT_IF(aReject.mType == MediaData::Type::AUDIO_DATA,
+ !mMaster->IsRequestingAudioData());
+ MOZ_ASSERT_IF(aReject.mType == MediaData::Type::VIDEO_DATA,
+ !mMaster->IsRequestingVideoData());
+ MOZ_ASSERT_IF(aReject.mType == MediaData::Type::AUDIO_DATA,
+ !mMaster->IsWaitingAudioData());
+ MOZ_ASSERT_IF(aReject.mType == MediaData::Type::VIDEO_DATA,
+ !mMaster->IsWaitingVideoData());
+
+ // Fire 'waiting' to notify the player that we are waiting for data.
+ mMaster->mOnNextFrameStatus.Notify(
+ MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE_SEEKING);
+
+ Reader()
+ ->WaitForData(aReject.mType)
+ ->Then(
+ OwnerThread(), __func__,
+ [this](MediaData::Type aType) {
+ AUTO_PROFILER_LABEL(
+ "AccurateSeekingState::OnSeekRejected:WaitDataResolved",
+ MEDIA_PLAYBACK);
+ SLOG("OnSeekRejected wait promise resolved");
+ mWaitRequest.Complete();
+ DemuxerSeek();
+ },
+ [this](const WaitForDataRejectValue& aRejection) {
+ AUTO_PROFILER_LABEL(
+ "AccurateSeekingState::OnSeekRejected:WaitDataRejected",
+ MEDIA_PLAYBACK);
+ SLOG("OnSeekRejected wait promise rejected");
+ mWaitRequest.Complete();
+ mMaster->DecodeError(NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA);
+ })
+ ->Track(mWaitRequest);
+ return;
+ }
+
+ if (aReject.mError == NS_ERROR_DOM_MEDIA_END_OF_STREAM) {
+ if (!mDoneAudioSeeking) {
+ HandleEndOfAudioInternal();
+ }
+ if (!mDoneVideoSeeking) {
+ HandleEndOfVideoInternal();
+ }
+ MaybeFinishSeek();
+ return;
+ }
+
+ MOZ_ASSERT(NS_FAILED(aReject.mError),
+ "Cancels should also disconnect mSeekRequest");
+ mMaster->DecodeError(aReject.mError);
+ }
+
+ void RequestAudioData() {
+ MOZ_ASSERT(!mDoneAudioSeeking);
+ mMaster->RequestAudioData();
+ }
+
+ virtual void RequestVideoData() {
+ MOZ_ASSERT(!mDoneVideoSeeking);
+ mMaster->RequestVideoData(media::TimeUnit());
+ }
+
+ void AdjustFastSeekIfNeeded(MediaData* aSample) {
+ if (mSeekJob.mTarget->IsFast() &&
+ mSeekJob.mTarget->GetTime() > mCurrentTimeBeforeSeek &&
+ aSample->mTime < mCurrentTimeBeforeSeek) {
+ // We are doing a fastSeek, but we ended up *before* the previous
+ // playback position. This is surprising UX, so switch to an accurate
+ // seek and decode to the seek target. This is not conformant to the
+ // spec, fastSeek should always be fast, but until we get the time to
+ // change all Readers to seek to the keyframe after the currentTime
+ // in this case, we'll just decode forward. Bug 1026330.
+ mSeekJob.mTarget->SetType(SeekTarget::Accurate);
+ }
+ }
+
+ nsresult DropAudioUpToSeekTarget(AudioData* aAudio) {
+ MOZ_ASSERT(aAudio && mSeekJob.mTarget->IsAccurate());
+
+ if (mSeekJob.mTarget->GetTime() >= aAudio->GetEndTime()) {
+ // Our seek target lies after the frames in this AudioData. Don't
+ // push it onto the audio queue, and keep decoding forwards.
+ return NS_OK;
+ }
+
+ if (aAudio->mTime > mSeekJob.mTarget->GetTime()) {
+ // The seek target doesn't lie in the audio block just after the last
+ // audio frames we've seen which were before the seek target. This
+ // could have been the first audio data we've seen after seek, i.e. the
+ // seek terminated after the seek target in the audio stream. Just
+ // abort the audio decode-to-target, the state machine will play
+ // silence to cover the gap. Typically this happens in poorly muxed
+ // files.
+ SLOGW("Audio not synced after seek, maybe a poorly muxed file?");
+ mMaster->PushAudio(aAudio);
+ mDoneAudioSeeking = true;
+ return NS_OK;
+ }
+
+ bool ok = aAudio->SetTrimWindow(
+ {mSeekJob.mTarget->GetTime(), aAudio->GetEndTime()});
+ if (!ok) {
+ return NS_ERROR_DOM_MEDIA_OVERFLOW_ERR;
+ }
+
+ MOZ_ASSERT(AudioQueue().GetSize() == 0,
+ "Should be the 1st sample after seeking");
+ mMaster->PushAudio(aAudio);
+ mDoneAudioSeeking = true;
+
+ return NS_OK;
+ }
+
+ nsresult DropVideoUpToSeekTarget(VideoData* aVideo) {
+ MOZ_ASSERT(aVideo);
+ SLOG("DropVideoUpToSeekTarget() frame [%" PRId64 ", %" PRId64 "]",
+ aVideo->mTime.ToMicroseconds(), aVideo->GetEndTime().ToMicroseconds());
+ const auto target = GetSeekTarget();
+
+ // If the frame end time is less than the seek target, we won't want
+ // to display this frame after the seek, so discard it.
+ if (target >= aVideo->GetEndTime()) {
+ SLOG("DropVideoUpToSeekTarget() pop video frame [%" PRId64 ", %" PRId64
+ "] target=%" PRId64,
+ aVideo->mTime.ToMicroseconds(),
+ aVideo->GetEndTime().ToMicroseconds(), target.ToMicroseconds());
+ PROFILER_MARKER_UNTYPED("MDSM::DropVideoUpToSeekTarget", MEDIA_PLAYBACK);
+ mFirstVideoFrameAfterSeek = aVideo;
+ } else {
+ if (target >= aVideo->mTime && aVideo->GetEndTime() >= target) {
+ // The seek target lies inside this frame's time slice. Adjust the
+ // frame's start time to match the seek target.
+ aVideo->UpdateTimestamp(target);
+ }
+ mFirstVideoFrameAfterSeek = nullptr;
+
+ SLOG("DropVideoUpToSeekTarget() found video frame [%" PRId64 ", %" PRId64
+ "] containing target=%" PRId64,
+ aVideo->mTime.ToMicroseconds(),
+ aVideo->GetEndTime().ToMicroseconds(), target.ToMicroseconds());
+
+ MOZ_ASSERT(VideoQueue().GetSize() == 0,
+ "Should be the 1st sample after seeking");
+ mMaster->PushVideo(aVideo);
+ mDoneVideoSeeking = true;
+ }
+
+ return NS_OK;
+ }
+
+ void HandleEndOfAudioInternal() {
+ MOZ_ASSERT(!mDoneAudioSeeking);
+ AudioQueue().Finish();
+ mDoneAudioSeeking = true;
+ }
+
+ void HandleEndOfVideoInternal() {
+ MOZ_ASSERT(!mDoneVideoSeeking);
+ if (mFirstVideoFrameAfterSeek) {
+ // Hit the end of stream. Move mFirstVideoFrameAfterSeek into
+ // mSeekedVideoData so we have something to display after seeking.
+ mMaster->PushVideo(mFirstVideoFrameAfterSeek);
+ }
+ VideoQueue().Finish();
+ mDoneVideoSeeking = true;
+ }
+
+ void MaybeFinishSeek() {
+ if (mDoneAudioSeeking && mDoneVideoSeeking) {
+ SeekCompleted();
+ }
+ }
+
+ /*
+ * Track the current seek promise made by the reader.
+ */
+ MozPromiseRequestHolder<MediaFormatReader::SeekPromise> mSeekRequest;
+
+ /*
+ * Internal state.
+ */
+ media::TimeUnit mCurrentTimeBeforeSeek;
+ bool mDoneAudioSeeking = false;
+ bool mDoneVideoSeeking = false;
+ MozPromiseRequestHolder<WaitForDataPromise> mWaitRequest;
+
+ // This temporarily stores the first frame we decode after we seek.
+ // This is so that if we hit end of stream while we're decoding to reach
+ // the seek target, we will still have a frame that we can display as the
+ // last frame in the media.
+ RefPtr<VideoData> mFirstVideoFrameAfterSeek;
+
+ private:
+ virtual media::TimeUnit GetSeekTarget() const {
+ return mSeekJob.mTarget->GetTime();
+ }
+};
+
+/*
+ * Remove samples from the queue until aCompare() returns false.
+ * aCompare A function object with the signature bool(int64_t) which returns
+ * true for samples that should be removed.
+ */
+template <typename Type, typename Function>
+static void DiscardFrames(MediaQueue<Type>& aQueue, const Function& aCompare) {
+ while (aQueue.GetSize() > 0) {
+ if (aCompare(aQueue.PeekFront()->mTime.ToMicroseconds())) {
+ RefPtr<Type> releaseMe = aQueue.PopFront();
+ continue;
+ }
+ break;
+ }
+}
+
+class MediaDecoderStateMachine::NextFrameSeekingState
+ : public MediaDecoderStateMachine::SeekingState {
+ public:
+ explicit NextFrameSeekingState(Master* aPtr) : SeekingState(aPtr) {}
+
+ State GetState() const override {
+ return DECODER_STATE_SEEKING_NEXTFRAMESEEKING;
+ }
+
+ RefPtr<MediaDecoder::SeekPromise> Enter(SeekJob&& aSeekJob,
+ EventVisibility aVisibility) {
+ MOZ_ASSERT(aSeekJob.mTarget->IsNextFrame());
+ mCurrentTime = mMaster->GetMediaTime();
+ mDuration = mMaster->Duration();
+ return SeekingState::Enter(std::move(aSeekJob), aVisibility);
+ }
+
+ void Exit() override {
+ // Disconnect my async seek operation.
+ if (mAsyncSeekTask) {
+ mAsyncSeekTask->Cancel();
+ }
+
+ // Disconnect MediaDecoder.
+ mSeekJob.RejectIfExists(__func__);
+ }
+
+ void HandleAudioDecoded(AudioData* aAudio) override {
+ mMaster->PushAudio(aAudio);
+ }
+
+ void HandleVideoDecoded(VideoData* aVideo) override {
+ MOZ_ASSERT(aVideo);
+ MOZ_ASSERT(!mSeekJob.mPromise.IsEmpty(), "Seek shouldn't be finished");
+ MOZ_ASSERT(NeedMoreVideo());
+
+ if (aVideo->mTime > mCurrentTime) {
+ mMaster->PushVideo(aVideo);
+ FinishSeek();
+ } else {
+ RequestVideoData();
+ }
+ }
+
+ void HandleWaitingForAudio() override {
+ MOZ_ASSERT(!mSeekJob.mPromise.IsEmpty(), "Seek shouldn't be finished");
+ // We don't care about audio decode errors in this state which will be
+ // handled by other states after seeking.
+ }
+
+ void HandleAudioCanceled() override {
+ MOZ_ASSERT(!mSeekJob.mPromise.IsEmpty(), "Seek shouldn't be finished");
+ // We don't care about audio decode errors in this state which will be
+ // handled by other states after seeking.
+ }
+
+ void HandleEndOfAudio() override {
+ MOZ_ASSERT(!mSeekJob.mPromise.IsEmpty(), "Seek shouldn't be finished");
+ // We don't care about audio decode errors in this state which will be
+ // handled by other states after seeking.
+ }
+
+ void HandleWaitingForVideo() override {
+ MOZ_ASSERT(!mSeekJob.mPromise.IsEmpty(), "Seek shouldn't be finished");
+ MOZ_ASSERT(NeedMoreVideo());
+ mMaster->WaitForData(MediaData::Type::VIDEO_DATA);
+ }
+
+ void HandleVideoCanceled() override {
+ MOZ_ASSERT(!mSeekJob.mPromise.IsEmpty(), "Seek shouldn't be finished");
+ MOZ_ASSERT(NeedMoreVideo());
+ RequestVideoData();
+ }
+
+ void HandleEndOfVideo() override {
+ MOZ_ASSERT(!mSeekJob.mPromise.IsEmpty(), "Seek shouldn't be finished");
+ MOZ_ASSERT(NeedMoreVideo());
+ VideoQueue().Finish();
+ FinishSeek();
+ }
+
+ void HandleAudioWaited(MediaData::Type aType) override {
+ // We don't care about audio in this state.
+ }
+
+ void HandleVideoWaited(MediaData::Type aType) override {
+ MOZ_ASSERT(!mSeekJob.mPromise.IsEmpty(), "Seek shouldn't be finished");
+ MOZ_ASSERT(NeedMoreVideo());
+ RequestVideoData();
+ }
+
+ TimeUnit CalculateNewCurrentTime() const override {
+ // The HTMLMediaElement.currentTime should be updated to the seek target
+ // which has been updated to the next frame's time.
+ return mSeekJob.mTarget->GetTime();
+ }
+
+ void DoSeek() override {
+ mMaster->StopMediaSink();
+
+ auto currentTime = mCurrentTime;
+ DiscardFrames(VideoQueue(), [currentTime](int64_t aSampleTime) {
+ return aSampleTime <= currentTime.ToMicroseconds();
+ });
+
+ // If there is a pending video request, finish the seeking if we don't need
+ // more data, or wait for HandleVideoDecoded() to finish seeking.
+ if (mMaster->IsRequestingVideoData()) {
+ if (!NeedMoreVideo()) {
+ FinishSeek();
+ }
+ return;
+ }
+
+ // Otherwise, we need to do the seek operation asynchronously for a special
+ // case (bug504613.ogv) which has no data at all, the 1st seekToNextFrame()
+ // operation reaches the end of the media. If we did the seek operation
+ // synchronously, we immediately resolve the SeekPromise in mSeekJob and
+ // then switch to the CompletedState which dispatches an "ended" event.
+ // However, the ThenValue of the SeekPromise has not yet been set, so the
+ // promise resolving is postponed and then the JS developer receives the
+ // "ended" event before the seek promise is resolved.
+ // An asynchronous seek operation helps to solve this issue since while the
+ // seek is actually performed, the ThenValue of SeekPromise has already
+ // been set so that it won't be postponed.
+ RefPtr<Runnable> r = mAsyncSeekTask = new AysncNextFrameSeekTask(this);
+ nsresult rv = OwnerThread()->Dispatch(r.forget());
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ }
+
+ private:
+ void DoSeekInternal() {
+ // We don't need to discard frames to the mCurrentTime here because we have
+ // done it at DoSeek() and any video data received in between either
+ // finishes the seek operation or be discarded, see HandleVideoDecoded().
+
+ if (!NeedMoreVideo()) {
+ FinishSeek();
+ } else if (!mMaster->IsRequestingVideoData() &&
+ !mMaster->IsWaitingVideoData()) {
+ RequestVideoData();
+ }
+ }
+
+ class AysncNextFrameSeekTask : public Runnable {
+ public:
+ explicit AysncNextFrameSeekTask(NextFrameSeekingState* aStateObject)
+ : Runnable(
+ "MediaDecoderStateMachine::NextFrameSeekingState::"
+ "AysncNextFrameSeekTask"),
+ mStateObj(aStateObject) {}
+
+ void Cancel() { mStateObj = nullptr; }
+
+ NS_IMETHOD Run() override {
+ if (mStateObj) {
+ AUTO_PROFILER_LABEL("AysncNextFrameSeekTask::Run", MEDIA_PLAYBACK);
+ mStateObj->DoSeekInternal();
+ }
+ return NS_OK;
+ }
+
+ private:
+ NextFrameSeekingState* mStateObj;
+ };
+
+ void RequestVideoData() { mMaster->RequestVideoData(media::TimeUnit()); }
+
+ bool NeedMoreVideo() const {
+ // Need to request video when we have none and video queue is not finished.
+ return VideoQueue().GetSize() == 0 && !VideoQueue().IsFinished();
+ }
+
+ // Update the seek target's time before resolving this seek task, the updated
+ // time will be used in the MDSM::SeekCompleted() to update the MDSM's
+ // position.
+ void UpdateSeekTargetTime() {
+ RefPtr<VideoData> data = VideoQueue().PeekFront();
+ if (data) {
+ mSeekJob.mTarget->SetTime(data->mTime);
+ } else {
+ MOZ_ASSERT(VideoQueue().AtEndOfStream());
+ mSeekJob.mTarget->SetTime(mDuration);
+ }
+ }
+
+ void FinishSeek() {
+ MOZ_ASSERT(!NeedMoreVideo());
+ UpdateSeekTargetTime();
+ auto time = mSeekJob.mTarget->GetTime().ToMicroseconds();
+ DiscardFrames(AudioQueue(),
+ [time](int64_t aSampleTime) { return aSampleTime < time; });
+ SeekCompleted();
+ }
+
+ /*
+ * Internal state.
+ */
+ TimeUnit mCurrentTime;
+ TimeUnit mDuration;
+ RefPtr<AysncNextFrameSeekTask> mAsyncSeekTask;
+};
+
+class MediaDecoderStateMachine::NextFrameSeekingFromDormantState
+ : public MediaDecoderStateMachine::AccurateSeekingState {
+ public:
+ explicit NextFrameSeekingFromDormantState(Master* aPtr)
+ : AccurateSeekingState(aPtr) {}
+
+ State GetState() const override { return DECODER_STATE_SEEKING_FROMDORMANT; }
+
+ RefPtr<MediaDecoder::SeekPromise> Enter(SeekJob&& aCurrentSeekJob,
+ SeekJob&& aFutureSeekJob) {
+ mFutureSeekJob = std::move(aFutureSeekJob);
+
+ AccurateSeekingState::Enter(std::move(aCurrentSeekJob),
+ EventVisibility::Suppressed);
+
+ // Once seekToNextFrame() is called, we assume the user is likely to keep
+ // calling seekToNextFrame() repeatedly, and so, we should prevent the MDSM
+ // from getting into Dormant state.
+ mMaster->mMinimizePreroll = false;
+
+ return mFutureSeekJob.mPromise.Ensure(__func__);
+ }
+
+ void Exit() override {
+ mFutureSeekJob.RejectIfExists(__func__);
+ AccurateSeekingState::Exit();
+ }
+
+ private:
+ SeekJob mFutureSeekJob;
+
+ // We don't want to transition to DecodingState once this seek completes,
+ // instead, we transition to NextFrameSeekingState.
+ void GoToNextState() override {
+ SetState<NextFrameSeekingState>(std::move(mFutureSeekJob),
+ EventVisibility::Observable);
+ }
+};
+
+class MediaDecoderStateMachine::VideoOnlySeekingState
+ : public MediaDecoderStateMachine::AccurateSeekingState {
+ public:
+ explicit VideoOnlySeekingState(Master* aPtr) : AccurateSeekingState(aPtr) {}
+
+ State GetState() const override { return DECODER_STATE_SEEKING_VIDEOONLY; }
+
+ RefPtr<MediaDecoder::SeekPromise> Enter(SeekJob&& aSeekJob,
+ EventVisibility aVisibility) {
+ MOZ_ASSERT(aSeekJob.mTarget->IsVideoOnly());
+ MOZ_ASSERT(aVisibility == EventVisibility::Suppressed);
+
+ RefPtr<MediaDecoder::SeekPromise> p =
+ AccurateSeekingState::Enter(std::move(aSeekJob), aVisibility);
+
+ // Dispatch a mozvideoonlyseekbegin event to indicate UI for corresponding
+ // changes.
+ mMaster->mOnPlaybackEvent.Notify(MediaPlaybackEvent::VideoOnlySeekBegin);
+
+ return p;
+ }
+
+ void Exit() override {
+ // We are completing or discarding this video-only seek operation now,
+ // dispatch an event so that the UI can change in response to the end
+ // of video-only seek.
+ mMaster->mOnPlaybackEvent.Notify(
+ MediaPlaybackEvent::VideoOnlySeekCompleted);
+
+ AccurateSeekingState::Exit();
+ }
+
+ void HandleAudioDecoded(AudioData* aAudio) override {
+ MOZ_ASSERT(mDoneAudioSeeking && !mDoneVideoSeeking,
+ "Seek shouldn't be finished");
+ MOZ_ASSERT(aAudio);
+
+ // Video-only seek doesn't reset audio decoder. There might be pending audio
+ // requests when AccurateSeekTask::Seek() begins. We will just store the
+ // data without checking |mDiscontinuity| or calling
+ // DropAudioUpToSeekTarget().
+ mMaster->PushAudio(aAudio);
+ }
+
+ void HandleWaitingForAudio() override {}
+
+ void HandleAudioCanceled() override {}
+
+ void HandleEndOfAudio() override {}
+
+ void HandleAudioWaited(MediaData::Type aType) override {
+ MOZ_ASSERT(!mDoneAudioSeeking || !mDoneVideoSeeking,
+ "Seek shouldn't be finished");
+
+ // Ignore pending requests from video-only seek.
+ }
+
+ void DoSeek() override {
+ // TODO: keep decoding audio.
+ mDoneAudioSeeking = true;
+ mDoneVideoSeeking = !Info().HasVideo();
+
+ const auto offset = VideoQueue().GetOffset();
+ mMaster->ResetDecode(TrackInfo::kVideoTrack);
+
+ // Entering video-only state and we've looped at least once before, so we
+ // need to set offset in order to let new video frames catch up with the
+ // clock time.
+ if (offset != media::TimeUnit::Zero()) {
+ VideoQueue().SetOffset(offset);
+ }
+
+ DemuxerSeek();
+ }
+
+ protected:
+ // Allow skip-to-next-key-frame to kick in if we fall behind the current
+ // playback position so decoding has a better chance to catch up.
+ void RequestVideoData() override {
+ MOZ_ASSERT(!mDoneVideoSeeking);
+
+ auto clock = mMaster->mMediaSink->IsStarted() ? mMaster->GetClock()
+ : mMaster->GetMediaTime();
+ mMaster->AdjustByLooping(clock);
+ const auto& nextKeyFrameTime = GetNextKeyFrameTime();
+
+ auto threshold = clock;
+
+ if (nextKeyFrameTime.IsValid() &&
+ clock >= (nextKeyFrameTime - sSkipToNextKeyFrameThreshold)) {
+ threshold = nextKeyFrameTime;
+ }
+
+ mMaster->RequestVideoData(threshold);
+ }
+
+ private:
+ // Trigger skip to next key frame if the current playback position is very
+ // close the next key frame's time.
+ static constexpr TimeUnit sSkipToNextKeyFrameThreshold =
+ TimeUnit::FromMicroseconds(5000);
+
+ // If the media is playing, drop video until catch up playback position.
+ media::TimeUnit GetSeekTarget() const override {
+ auto target = mMaster->mMediaSink->IsStarted()
+ ? mMaster->GetClock()
+ : mSeekJob.mTarget->GetTime();
+ mMaster->AdjustByLooping(target);
+ return target;
+ }
+
+ media::TimeUnit GetNextKeyFrameTime() const {
+ // We only call this method in RequestVideoData() and we only request video
+ // data if we haven't done video seeking.
+ MOZ_DIAGNOSTIC_ASSERT(!mDoneVideoSeeking);
+ MOZ_DIAGNOSTIC_ASSERT(mMaster->VideoQueue().GetSize() == 0);
+
+ if (mFirstVideoFrameAfterSeek) {
+ return mFirstVideoFrameAfterSeek->NextKeyFrameTime();
+ }
+
+ return TimeUnit::Invalid();
+ }
+};
+
+constexpr TimeUnit MediaDecoderStateMachine::VideoOnlySeekingState::
+ sSkipToNextKeyFrameThreshold;
+
+RefPtr<MediaDecoder::SeekPromise>
+MediaDecoderStateMachine::DormantState::HandleSeek(const SeekTarget& aTarget) {
+ if (aTarget.IsNextFrame()) {
+ // NextFrameSeekingState doesn't reset the decoder unlike
+ // AccurateSeekingState. So we first must come out of dormant by seeking to
+ // mPendingSeek and continue later with the NextFrameSeek
+ SLOG("Changed state to SEEKING (to %" PRId64 ")",
+ aTarget.GetTime().ToMicroseconds());
+ SeekJob seekJob;
+ seekJob.mTarget = Some(aTarget);
+ return StateObject::SetState<NextFrameSeekingFromDormantState>(
+ std::move(mPendingSeek), std::move(seekJob));
+ }
+
+ return StateObject::HandleSeek(aTarget);
+}
+
+/**
+ * Purpose: stop playback until enough data is decoded to continue playback.
+ *
+ * Transition to:
+ * SEEKING if any seek request.
+ * SHUTDOWN if any decode error.
+ * COMPLETED when having decoded all audio/video data.
+ * DECODING/LOOPING_DECODING when having decoded enough data to continue
+ * playback.
+ */
+class MediaDecoderStateMachine::BufferingState
+ : public MediaDecoderStateMachine::StateObject {
+ public:
+ explicit BufferingState(Master* aPtr) : StateObject(aPtr) {}
+
+ void Enter() {
+ if (mMaster->IsPlaying()) {
+ mMaster->StopPlayback();
+ }
+
+ mBufferingStart = TimeStamp::Now();
+ mMaster->ScheduleStateMachineIn(TimeUnit::FromMicroseconds(USECS_PER_S));
+ mMaster->mOnNextFrameStatus.Notify(
+ MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE_BUFFERING);
+ }
+
+ void Step() override;
+
+ State GetState() const override { return DECODER_STATE_BUFFERING; }
+
+ void HandleAudioDecoded(AudioData* aAudio) override {
+ mMaster->PushAudio(aAudio);
+ if (!mMaster->HaveEnoughDecodedAudio()) {
+ mMaster->RequestAudioData();
+ }
+ // This might be the sample we need to exit buffering.
+ // Schedule Step() to check it.
+ mMaster->ScheduleStateMachine();
+ }
+
+ void HandleVideoDecoded(VideoData* aVideo) override {
+ mMaster->PushVideo(aVideo);
+ if (!mMaster->HaveEnoughDecodedVideo()) {
+ mMaster->RequestVideoData(media::TimeUnit());
+ }
+ // This might be the sample we need to exit buffering.
+ // Schedule Step() to check it.
+ mMaster->ScheduleStateMachine();
+ }
+
+ void HandleAudioCanceled() override { mMaster->RequestAudioData(); }
+
+ void HandleVideoCanceled() override {
+ mMaster->RequestVideoData(media::TimeUnit());
+ }
+
+ void HandleWaitingForAudio() override {
+ mMaster->WaitForData(MediaData::Type::AUDIO_DATA);
+ }
+
+ void HandleWaitingForVideo() override {
+ mMaster->WaitForData(MediaData::Type::VIDEO_DATA);
+ }
+
+ void HandleAudioWaited(MediaData::Type aType) override {
+ mMaster->RequestAudioData();
+ }
+
+ void HandleVideoWaited(MediaData::Type aType) override {
+ mMaster->RequestVideoData(media::TimeUnit());
+ }
+
+ void HandleEndOfAudio() override;
+ void HandleEndOfVideo() override;
+
+ void HandleVideoSuspendTimeout() override {
+ // No video, so nothing to suspend.
+ if (!mMaster->HasVideo()) {
+ return;
+ }
+
+ mMaster->mVideoDecodeSuspended = true;
+ mMaster->mOnPlaybackEvent.Notify(MediaPlaybackEvent::EnterVideoSuspend);
+ Reader()->SetVideoBlankDecode(true);
+ }
+
+ private:
+ TimeStamp mBufferingStart;
+
+ // The maximum number of second we spend buffering when we are short on
+ // unbuffered data.
+ const uint32_t mBufferingWait = 15;
+};
+
+/**
+ * Purpose: play all the decoded data and fire the 'ended' event.
+ *
+ * Transition to:
+ * SEEKING if any seek request.
+ * LOOPING_DECODING if MDSM enable looping.
+ */
+class MediaDecoderStateMachine::CompletedState
+ : public MediaDecoderStateMachine::StateObject {
+ public:
+ explicit CompletedState(Master* aPtr) : StateObject(aPtr) {}
+
+ void Enter() {
+ // On Android, the life cycle of graphic buffer is equal to Android's codec,
+ // we couldn't release it if we still need to render the frame.
+#ifndef MOZ_WIDGET_ANDROID
+ if (!mMaster->mLooping) {
+ // We've decoded all samples.
+ // We don't need decoders anymore if not looping.
+ Reader()->ReleaseResources();
+ }
+#endif
+ bool hasNextFrame = (!mMaster->HasAudio() || !mMaster->mAudioCompleted) &&
+ (!mMaster->HasVideo() || !mMaster->mVideoCompleted);
+
+ mMaster->mOnNextFrameStatus.Notify(
+ hasNextFrame ? MediaDecoderOwner::NEXT_FRAME_AVAILABLE
+ : MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE);
+
+ Step();
+ }
+
+ void Exit() override { mSentPlaybackEndedEvent = false; }
+
+ void Step() override {
+ if (mMaster->mPlayState != MediaDecoder::PLAY_STATE_PLAYING &&
+ mMaster->IsPlaying()) {
+ mMaster->StopPlayback();
+ }
+
+ // Play the remaining media. We want to run AdvanceFrame() at least
+ // once to ensure the current playback position is advanced to the
+ // end of the media, and so that we update the readyState.
+ if ((mMaster->HasVideo() && !mMaster->mVideoCompleted) ||
+ (mMaster->HasAudio() && !mMaster->mAudioCompleted)) {
+ // Start playback if necessary to play the remaining media.
+ mMaster->MaybeStartPlayback();
+ mMaster->UpdatePlaybackPositionPeriodically();
+ MOZ_ASSERT(!mMaster->IsPlaying() || mMaster->IsStateMachineScheduled(),
+ "Must have timer scheduled");
+ return;
+ }
+
+ // StopPlayback in order to reset the IsPlaying() state so audio
+ // is restarted correctly.
+ mMaster->StopPlayback();
+
+ if (!mSentPlaybackEndedEvent) {
+ auto clockTime =
+ std::max(mMaster->AudioEndTime(), mMaster->VideoEndTime());
+ // Correct the time over the end once looping was turned on.
+ mMaster->AdjustByLooping(clockTime);
+ if (mMaster->mDuration.Ref()->IsInfinite()) {
+ // We have a finite duration when playback reaches the end.
+ mMaster->mDuration = Some(clockTime);
+ DDLOGEX(mMaster, DDLogCategory::Property, "duration_us",
+ mMaster->mDuration.Ref()->ToMicroseconds());
+ }
+ mMaster->UpdatePlaybackPosition(clockTime);
+
+ // Ensure readyState is updated before firing the 'ended' event.
+ mMaster->mOnNextFrameStatus.Notify(
+ MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE);
+
+ mMaster->mOnPlaybackEvent.Notify(MediaPlaybackEvent::PlaybackEnded);
+
+ mSentPlaybackEndedEvent = true;
+
+ // MediaSink::GetEndTime() must be called before stopping playback.
+ mMaster->StopMediaSink();
+ }
+ }
+
+ State GetState() const override { return DECODER_STATE_COMPLETED; }
+
+ void HandleLoopingChanged() override {
+ if (mMaster->mLooping) {
+ SetDecodingState();
+ }
+ }
+
+ void HandleAudioCaptured() override {
+ // MediaSink is changed. Schedule Step() to check if we can start playback.
+ mMaster->ScheduleStateMachine();
+ }
+
+ void HandleVideoSuspendTimeout() override {
+ // Do nothing since no decoding is going on.
+ }
+
+ void HandleResumeVideoDecoding(const TimeUnit&) override {
+ // Resume the video decoder and seek to the last video frame.
+ // This triggers a video-only seek which won't update the playback position.
+ auto target = mMaster->mDecodedVideoEndTime;
+ mMaster->AdjustByLooping(target);
+ StateObject::HandleResumeVideoDecoding(target);
+ }
+
+ void HandlePlayStateChanged(MediaDecoder::PlayState aPlayState) override {
+ if (aPlayState == MediaDecoder::PLAY_STATE_PLAYING) {
+ // Schedule Step() to check if we can start playback.
+ mMaster->ScheduleStateMachine();
+ }
+ }
+
+ private:
+ bool mSentPlaybackEndedEvent = false;
+};
+
+/**
+ * Purpose: release all resources allocated by MDSM.
+ *
+ * Transition to:
+ * None since this is the final state.
+ *
+ * Transition from:
+ * Any states other than SHUTDOWN.
+ */
+class MediaDecoderStateMachine::ShutdownState
+ : public MediaDecoderStateMachine::StateObject {
+ public:
+ explicit ShutdownState(Master* aPtr) : StateObject(aPtr) {}
+
+ RefPtr<ShutdownPromise> Enter();
+
+ void Exit() override {
+ MOZ_DIAGNOSTIC_ASSERT(false, "Shouldn't escape the SHUTDOWN state.");
+ }
+
+ State GetState() const override { return DECODER_STATE_SHUTDOWN; }
+
+ RefPtr<MediaDecoder::SeekPromise> HandleSeek(
+ const SeekTarget& aTarget) override {
+ MOZ_DIAGNOSTIC_ASSERT(false, "Can't seek in shutdown state.");
+ return MediaDecoder::SeekPromise::CreateAndReject(true, __func__);
+ }
+
+ RefPtr<ShutdownPromise> HandleShutdown() override {
+ MOZ_DIAGNOSTIC_ASSERT(false, "Already shutting down.");
+ return nullptr;
+ }
+
+ void HandleVideoSuspendTimeout() override {
+ MOZ_DIAGNOSTIC_ASSERT(false, "Already shutting down.");
+ }
+
+ void HandleResumeVideoDecoding(const TimeUnit&) override {
+ MOZ_DIAGNOSTIC_ASSERT(false, "Already shutting down.");
+ }
+};
+
+RefPtr<MediaDecoder::SeekPromise>
+MediaDecoderStateMachine::StateObject::HandleSeek(const SeekTarget& aTarget) {
+ SLOG("Changed state to SEEKING (to %" PRId64 ")",
+ aTarget.GetTime().ToMicroseconds());
+ SeekJob seekJob;
+ seekJob.mTarget = Some(aTarget);
+ return SetSeekingState(std::move(seekJob), EventVisibility::Observable);
+}
+
+RefPtr<ShutdownPromise>
+MediaDecoderStateMachine::StateObject::HandleShutdown() {
+ return SetState<ShutdownState>();
+}
+
+static void ReportRecoveryTelemetry(const TimeStamp& aRecoveryStart,
+ const MediaInfo& aMediaInfo,
+ bool aIsHardwareAccelerated) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!aMediaInfo.HasVideo()) {
+ return;
+ }
+
+ // Keyed by audio+video or video alone, hardware acceleration,
+ // and by a resolution range.
+ nsCString key(aMediaInfo.HasAudio() ? "AV" : "V");
+ key.AppendASCII(aIsHardwareAccelerated ? "(hw)," : ",");
+ static const struct {
+ int32_t mH;
+ const char* mRes;
+ } sResolutions[] = {{240, "0-240"},
+ {480, "241-480"},
+ {720, "481-720"},
+ {1080, "721-1080"},
+ {2160, "1081-2160"}};
+ const char* resolution = "2161+";
+ int32_t height = aMediaInfo.mVideo.mImage.height;
+ for (const auto& res : sResolutions) {
+ if (height <= res.mH) {
+ resolution = res.mRes;
+ break;
+ }
+ }
+ key.AppendASCII(resolution);
+
+ TimeDuration duration = TimeStamp::Now() - aRecoveryStart;
+ double duration_ms = duration.ToMilliseconds();
+ Telemetry::Accumulate(Telemetry::VIDEO_SUSPEND_RECOVERY_TIME_MS, key,
+ static_cast<uint32_t>(lround(duration_ms)));
+ Telemetry::Accumulate(Telemetry::VIDEO_SUSPEND_RECOVERY_TIME_MS, "All"_ns,
+ static_cast<uint32_t>(lround(duration_ms)));
+}
+
+void MediaDecoderStateMachine::StateObject::HandleResumeVideoDecoding(
+ const TimeUnit& aTarget) {
+ MOZ_ASSERT(mMaster->mVideoDecodeSuspended);
+
+ mMaster->mVideoDecodeSuspended = false;
+ mMaster->mOnPlaybackEvent.Notify(MediaPlaybackEvent::ExitVideoSuspend);
+ Reader()->SetVideoBlankDecode(false);
+
+ // Start counting recovery time from right now.
+ TimeStamp start = TimeStamp::Now();
+
+ // Local reference to mInfo, so that it will be copied in the lambda below.
+ const auto& info = Info();
+ bool hw = Reader()->VideoIsHardwareAccelerated();
+
+ // Start video-only seek to the current time.
+ SeekJob seekJob;
+
+ // We use fastseek to optimize the resuming time.
+ // FastSeek is only used for video-only media since we don't need to worry
+ // about A/V sync.
+ // Don't use fastSeek if we want to seek to the end because it might seek to a
+ // keyframe before the last frame (if the last frame itself is not a keyframe)
+ // and we always want to present the final frame to the user when seeking to
+ // the end.
+ const auto type = mMaster->HasAudio() || aTarget == mMaster->Duration()
+ ? SeekTarget::Type::Accurate
+ : SeekTarget::Type::PrevSyncPoint;
+
+ seekJob.mTarget.emplace(aTarget, type, SeekTarget::Track::VideoOnly);
+ SLOG("video-only seek target=%" PRId64 ", current time=%" PRId64,
+ aTarget.ToMicroseconds(), mMaster->GetMediaTime().ToMicroseconds());
+
+ // Hold mMaster->mAbstractMainThread here because this->mMaster will be
+ // invalid after the current state object is deleted in SetState();
+ RefPtr<AbstractThread> mainThread = mMaster->mAbstractMainThread;
+
+ SetSeekingState(std::move(seekJob), EventVisibility::Suppressed)
+ ->Then(
+ mainThread, __func__,
+ [start, info, hw]() { ReportRecoveryTelemetry(start, info, hw); },
+ []() {});
+}
+
+RefPtr<MediaDecoder::SeekPromise>
+MediaDecoderStateMachine::StateObject::SetSeekingState(
+ SeekJob&& aSeekJob, EventVisibility aVisibility) {
+ if (aSeekJob.mTarget->IsAccurate() || aSeekJob.mTarget->IsFast()) {
+ if (aSeekJob.mTarget->IsVideoOnly()) {
+ return SetState<VideoOnlySeekingState>(std::move(aSeekJob), aVisibility);
+ }
+ return SetState<AccurateSeekingState>(std::move(aSeekJob), aVisibility);
+ }
+
+ if (aSeekJob.mTarget->IsNextFrame()) {
+ return SetState<NextFrameSeekingState>(std::move(aSeekJob), aVisibility);
+ }
+
+ MOZ_ASSERT_UNREACHABLE("Unknown SeekTarget::Type.");
+ return nullptr;
+}
+
+void MediaDecoderStateMachine::StateObject::SetDecodingState() {
+ if (mMaster->IsInSeamlessLooping()) {
+ SetState<LoopingDecodingState>();
+ return;
+ }
+ SetState<DecodingState>();
+}
+
+void MediaDecoderStateMachine::DecodeMetadataState::OnMetadataRead(
+ MetadataHolder&& aMetadata) {
+ mMetadataRequest.Complete();
+
+ AUTO_PROFILER_LABEL("DecodeMetadataState::OnMetadataRead", MEDIA_PLAYBACK);
+ mMaster->mInfo.emplace(*aMetadata.mInfo);
+ mMaster->mMediaSeekable = Info().mMediaSeekable;
+ mMaster->mMediaSeekableOnlyInBufferedRanges =
+ Info().mMediaSeekableOnlyInBufferedRanges;
+
+ if (Info().mMetadataDuration.isSome()) {
+ mMaster->mDuration = Info().mMetadataDuration;
+ } else if (Info().mUnadjustedMetadataEndTime.isSome()) {
+ const TimeUnit unadjusted = Info().mUnadjustedMetadataEndTime.ref();
+ const TimeUnit adjustment = Info().mStartTime;
+ mMaster->mInfo->mMetadataDuration.emplace(unadjusted - adjustment);
+ mMaster->mDuration = Info().mMetadataDuration;
+ }
+
+ // If we don't know the duration by this point, we assume infinity, per spec.
+ if (mMaster->mDuration.Ref().isNothing()) {
+ mMaster->mDuration = Some(TimeUnit::FromInfinity());
+ }
+
+ DDLOGEX(mMaster, DDLogCategory::Property, "duration_us",
+ mMaster->mDuration.Ref()->ToMicroseconds());
+
+ if (mMaster->HasVideo()) {
+ SLOG("Video decode HWAccel=%d videoQueueSize=%d",
+ Reader()->VideoIsHardwareAccelerated(),
+ mMaster->GetAmpleVideoFrames());
+ }
+
+ MOZ_ASSERT(mMaster->mDuration.Ref().isSome());
+
+ mMaster->mMetadataLoadedEvent.Notify(std::move(aMetadata.mInfo),
+ std::move(aMetadata.mTags),
+ MediaDecoderEventVisibility::Observable);
+
+ // Check whether the media satisfies the requirement of seamless looping.
+ // TODO : after we ensure video seamless looping is stable enough, then we can
+ // remove this to make the condition always true.
+ mMaster->mSeamlessLoopingAllowed = StaticPrefs::media_seamless_looping();
+ if (mMaster->HasVideo()) {
+ mMaster->mSeamlessLoopingAllowed =
+ StaticPrefs::media_seamless_looping_video();
+ }
+
+ SetState<DecodingFirstFrameState>();
+}
+
+void MediaDecoderStateMachine::DormantState::HandlePlayStateChanged(
+ MediaDecoder::PlayState aPlayState) {
+ if (aPlayState == MediaDecoder::PLAY_STATE_PLAYING) {
+ // Exit dormant when the user wants to play.
+ MOZ_ASSERT(mMaster->mSentFirstFrameLoadedEvent);
+ SetSeekingState(std::move(mPendingSeek), EventVisibility::Suppressed);
+ }
+}
+
+void MediaDecoderStateMachine::DecodingFirstFrameState::Enter() {
+ // Transition to DECODING if we've decoded first frames.
+ if (mMaster->mSentFirstFrameLoadedEvent) {
+ SetDecodingState();
+ return;
+ }
+
+ MOZ_ASSERT(!mMaster->mVideoDecodeSuspended);
+
+ // Dispatch tasks to decode first frames.
+ if (mMaster->HasAudio()) {
+ mMaster->RequestAudioData();
+ }
+ if (mMaster->HasVideo()) {
+ mMaster->RequestVideoData(media::TimeUnit());
+ }
+}
+
+void MediaDecoderStateMachine::DecodingFirstFrameState::
+ MaybeFinishDecodeFirstFrame() {
+ MOZ_ASSERT(!mMaster->mSentFirstFrameLoadedEvent);
+
+ if ((mMaster->IsAudioDecoding() && AudioQueue().GetSize() == 0) ||
+ (mMaster->IsVideoDecoding() && VideoQueue().GetSize() == 0)) {
+ return;
+ }
+
+ mMaster->FinishDecodeFirstFrame();
+ if (mPendingSeek.Exists()) {
+ SetSeekingState(std::move(mPendingSeek), EventVisibility::Observable);
+ } else {
+ SetDecodingState();
+ }
+}
+
+void MediaDecoderStateMachine::DecodingState::Enter() {
+ MOZ_ASSERT(mMaster->mSentFirstFrameLoadedEvent);
+
+ if (mMaster->mVideoDecodeSuspended &&
+ mMaster->mVideoDecodeMode == VideoDecodeMode::Normal) {
+ StateObject::HandleResumeVideoDecoding(mMaster->GetMediaTime());
+ return;
+ }
+
+ if (mMaster->mVideoDecodeMode == VideoDecodeMode::Suspend &&
+ !mMaster->mVideoDecodeSuspendTimer.IsScheduled() &&
+ !mMaster->mVideoDecodeSuspended) {
+ // If the VideoDecodeMode is Suspend and the timer is not schedule, it means
+ // the timer has timed out and we should suspend video decoding now if
+ // necessary.
+ HandleVideoSuspendTimeout();
+ }
+
+ // If we're in the normal decoding mode and the decoding has finished, then we
+ // should go to `completed` state because we don't need to decode anything
+ // later. However, if we're in the saemless decoding mode, we will restart
+ // decoding ASAP so we can still stay in `decoding` state.
+ if (!mMaster->IsVideoDecoding() && !mMaster->IsAudioDecoding() &&
+ !mMaster->IsInSeamlessLooping()) {
+ SetState<CompletedState>();
+ return;
+ }
+
+ mOnAudioPopped =
+ AudioQueue().PopFrontEvent().Connect(OwnerThread(), [this]() {
+ AUTO_PROFILER_LABEL("MediaDecoderStateMachine::OnAudioPopped",
+ MEDIA_PLAYBACK);
+ if (mMaster->IsAudioDecoding() && !mMaster->HaveEnoughDecodedAudio()) {
+ EnsureAudioDecodeTaskQueued();
+ }
+ });
+ mOnVideoPopped =
+ VideoQueue().PopFrontEvent().Connect(OwnerThread(), [this]() {
+ AUTO_PROFILER_LABEL("MediaDecoderStateMachine::OnVideoPopped",
+ MEDIA_PLAYBACK);
+ if (mMaster->IsVideoDecoding() && !mMaster->HaveEnoughDecodedVideo()) {
+ EnsureVideoDecodeTaskQueued();
+ }
+ });
+
+ mMaster->mOnNextFrameStatus.Notify(MediaDecoderOwner::NEXT_FRAME_AVAILABLE);
+
+ mDecodeStartTime = TimeStamp::Now();
+
+ MaybeStopPrerolling();
+
+ // Ensure that we've got tasks enqueued to decode data if we need to.
+ DispatchDecodeTasksIfNeeded();
+
+ mMaster->ScheduleStateMachine();
+
+ // Will enter dormant when playback is paused for a while.
+ if (mMaster->mPlayState == MediaDecoder::PLAY_STATE_PAUSED) {
+ StartDormantTimer();
+ }
+}
+
+void MediaDecoderStateMachine::DecodingState::Step() {
+ if (mMaster->mPlayState != MediaDecoder::PLAY_STATE_PLAYING &&
+ mMaster->IsPlaying()) {
+ // We're playing, but the element/decoder is in paused state. Stop
+ // playing!
+ mMaster->StopPlayback();
+ }
+
+ // Start playback if necessary so that the clock can be properly queried.
+ if (!mIsPrerolling) {
+ mMaster->MaybeStartPlayback();
+ }
+
+ mMaster->UpdatePlaybackPositionPeriodically();
+ MOZ_ASSERT(!mMaster->IsPlaying() || mMaster->IsStateMachineScheduled(),
+ "Must have timer scheduled");
+ if (IsBufferingAllowed()) {
+ MaybeStartBuffering();
+ }
+}
+
+void MediaDecoderStateMachine::DecodingState::HandleEndOfAudio() {
+ AudioQueue().Finish();
+ if (!mMaster->IsVideoDecoding()) {
+ SetState<CompletedState>();
+ } else {
+ MaybeStopPrerolling();
+ }
+}
+
+void MediaDecoderStateMachine::DecodingState::HandleEndOfVideo() {
+ VideoQueue().Finish();
+ if (!mMaster->IsAudioDecoding()) {
+ SetState<CompletedState>();
+ } else {
+ MaybeStopPrerolling();
+ }
+}
+
+void MediaDecoderStateMachine::DecodingState::DispatchDecodeTasksIfNeeded() {
+ if (mMaster->IsAudioDecoding() && !mMaster->mMinimizePreroll &&
+ !mMaster->HaveEnoughDecodedAudio()) {
+ EnsureAudioDecodeTaskQueued();
+ }
+
+ if (mMaster->IsVideoDecoding() && !mMaster->mMinimizePreroll &&
+ !mMaster->HaveEnoughDecodedVideo()) {
+ EnsureVideoDecodeTaskQueued();
+ }
+}
+
+void MediaDecoderStateMachine::DecodingState::EnsureAudioDecodeTaskQueued() {
+ if (!mMaster->IsAudioDecoding() || mMaster->IsRequestingAudioData() ||
+ mMaster->IsWaitingAudioData()) {
+ return;
+ }
+ mMaster->RequestAudioData();
+}
+
+void MediaDecoderStateMachine::DecodingState::EnsureVideoDecodeTaskQueued() {
+ if (!mMaster->IsVideoDecoding() || mMaster->IsRequestingVideoData() ||
+ mMaster->IsWaitingVideoData()) {
+ return;
+ }
+ mMaster->RequestVideoData(mMaster->GetMediaTime(),
+ ShouldRequestNextKeyFrame());
+}
+
+void MediaDecoderStateMachine::DecodingState::MaybeStartBuffering() {
+ // Buffering makes senses only after decoding first frames.
+ MOZ_ASSERT(mMaster->mSentFirstFrameLoadedEvent);
+
+ // Don't enter buffering when MediaDecoder is not playing.
+ if (mMaster->mPlayState != MediaDecoder::PLAY_STATE_PLAYING) {
+ return;
+ }
+
+ // Don't enter buffering while prerolling so that the decoder has a chance to
+ // enqueue some decoded data before we give up and start buffering.
+ if (!mMaster->IsPlaying()) {
+ return;
+ }
+
+ // Note we could have a wait promise pending when playing non-MSE EME.
+ if (mMaster->OutOfDecodedAudio() && mMaster->IsWaitingAudioData()) {
+ PROFILER_MARKER_TEXT("MDSM::StartBuffering", MEDIA_PLAYBACK, {},
+ "OutOfDecodedAudio");
+ SLOG("Enter buffering due to out of decoded audio");
+ SetState<BufferingState>();
+ return;
+ }
+ if (mMaster->OutOfDecodedVideo() && mMaster->IsWaitingVideoData()) {
+ PROFILER_MARKER_TEXT("MDSM::StartBuffering", MEDIA_PLAYBACK, {},
+ "OutOfDecodedVideo");
+ SLOG("Enter buffering due to out of decoded video");
+ SetState<BufferingState>();
+ return;
+ }
+
+ if (Reader()->UseBufferingHeuristics() && mMaster->HasLowDecodedData() &&
+ mMaster->HasLowBufferedData() && !mMaster->mCanPlayThrough) {
+ PROFILER_MARKER_TEXT("MDSM::StartBuffering", MEDIA_PLAYBACK, {},
+ "BufferingHeuristics");
+ SLOG("Enter buffering due to buffering heruistics");
+ SetState<BufferingState>();
+ }
+}
+
+void MediaDecoderStateMachine::LoopingDecodingState::HandleError(
+ const MediaResult& aError, bool aIsAudio) {
+ SLOG("%s looping failed, aError=%s", aIsAudio ? "audio" : "video",
+ aError.ErrorName().get());
+ switch (aError.Code()) {
+ case NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA:
+ if (aIsAudio) {
+ HandleWaitingForAudio();
+ // Now we won't be able to get new audio from the start position.
+ // This could happen for MSE, because data for the start hasn't been
+ // appended yet. If we can get the data before all queued audio has been
+ // consumed, then nothing special would happen. If not, then the audio
+ // underrun would happen and the audio clock stalls, which means video
+ // playback would also stall. Therefore, in this special situation, we
+ // can treat those audio underrun as silent frames in order to keep
+ // driving the clock. But we would cancel this behavior once the new
+ // audio data comes, or we fallback to the non-seamless looping.
+ mWaitingAudioDataFromStart = true;
+ mMaster->mMediaSink->EnableTreatAudioUnderrunAsSilence(true);
+ } else {
+ HandleWaitingForVideo();
+ }
+ [[fallthrough]];
+ case NS_ERROR_DOM_MEDIA_END_OF_STREAM:
+ // This could happen after either the resource has been close, or the data
+ // hasn't been appended in MSE, so that we won't be able to get any
+ // sample and need to fallback to normal looping.
+ if (mIsReachingAudioEOS && mIsReachingVideoEOS) {
+ SetState<CompletedState>();
+ }
+ break;
+ default:
+ mMaster->DecodeError(aError);
+ break;
+ }
+}
+
+void MediaDecoderStateMachine::SeekingState::SeekCompleted() {
+ const auto newCurrentTime = CalculateNewCurrentTime();
+
+ if ((newCurrentTime == mMaster->Duration() ||
+ newCurrentTime.EqualsAtLowestResolution(
+ mMaster->Duration().ToBase(USECS_PER_S))) &&
+ !mMaster->mIsLiveStream) {
+ SLOG("Seek completed, seeked to end: %s", newCurrentTime.ToString().get());
+ // will transition to COMPLETED immediately. Note we don't do
+ // this when playing a live stream, since the end of media will advance
+ // once we download more data!
+ AudioQueue().Finish();
+ VideoQueue().Finish();
+
+ // We won't start MediaSink when paused. m{Audio,Video}Completed will
+ // remain false and 'playbackEnded' won't be notified. Therefore we
+ // need to set these flags explicitly when seeking to the end.
+ mMaster->mAudioCompleted = true;
+ mMaster->mVideoCompleted = true;
+
+ // There might still be a pending audio request when doing video-only or
+ // next-frame seek. Discard it so we won't break the invariants of the
+ // COMPLETED state by adding audio samples to a finished queue.
+ mMaster->mAudioDataRequest.DisconnectIfExists();
+ }
+
+ // We want to resolve the seek request prior finishing the first frame
+ // to ensure that the seeked event is fired prior loadeded.
+ // Note: SeekJob.Resolve() resets SeekJob.mTarget. Don't use mSeekJob anymore
+ // hereafter.
+ mSeekJob.Resolve(__func__);
+
+ // Notify FirstFrameLoaded now if we haven't since we've decoded some data
+ // for readyState to transition to HAVE_CURRENT_DATA and fire 'loadeddata'.
+ if (!mMaster->mSentFirstFrameLoadedEvent) {
+ mMaster->FinishDecodeFirstFrame();
+ }
+
+ // Ensure timestamps are up to date.
+ // Suppressed visibility comes from two cases: (1) leaving dormant state,
+ // and (2) resuming suspended video decoder. We want both cases to be
+ // transparent to the user. So we only notify the change when the seek
+ // request is from the user.
+ if (mVisibility == EventVisibility::Observable) {
+ // Don't update playback position for video-only seek.
+ // Otherwise we might have |newCurrentTime > mMediaSink->GetPosition()|
+ // and fail the assertion in GetClock() since we didn't stop MediaSink.
+ mMaster->UpdatePlaybackPositionInternal(newCurrentTime);
+ }
+
+ // Try to decode another frame to detect if we're at the end...
+ SLOG("Seek completed, mCurrentPosition=%" PRId64,
+ mMaster->mCurrentPosition.Ref().ToMicroseconds());
+
+ if (mMaster->VideoQueue().PeekFront()) {
+ mMaster->mMediaSink->Redraw(Info().mVideo);
+ mMaster->mOnPlaybackEvent.Notify(MediaPlaybackEvent::Invalidate);
+ }
+
+ GoToNextState();
+}
+
+void MediaDecoderStateMachine::BufferingState::Step() {
+ TimeStamp now = TimeStamp::Now();
+ MOZ_ASSERT(!mBufferingStart.IsNull(), "Must know buffering start time.");
+
+ if (Reader()->UseBufferingHeuristics()) {
+ if (mMaster->IsWaitingAudioData() || mMaster->IsWaitingVideoData()) {
+ // Can't exit buffering when we are still waiting for data.
+ // Note we don't schedule next loop for we will do that when the wait
+ // promise is resolved.
+ return;
+ }
+ // With buffering heuristics, we exit buffering state when we:
+ // 1. can play through or
+ // 2. time out (specified by mBufferingWait) or
+ // 3. have enough buffered data.
+ TimeDuration elapsed = now - mBufferingStart;
+ TimeDuration timeout =
+ TimeDuration::FromSeconds(mBufferingWait * mMaster->mPlaybackRate);
+ bool stopBuffering =
+ mMaster->mCanPlayThrough || elapsed >= timeout ||
+ !mMaster->HasLowBufferedData(TimeUnit::FromSeconds(mBufferingWait));
+ if (!stopBuffering) {
+ SLOG("Buffering: wait %ds, timeout in %.3lfs", mBufferingWait,
+ mBufferingWait - elapsed.ToSeconds());
+ mMaster->ScheduleStateMachineIn(TimeUnit::FromMicroseconds(USECS_PER_S));
+ return;
+ }
+ } else if (mMaster->OutOfDecodedAudio() || mMaster->OutOfDecodedVideo()) {
+ MOZ_ASSERT(!mMaster->OutOfDecodedAudio() ||
+ mMaster->IsRequestingAudioData() ||
+ mMaster->IsWaitingAudioData());
+ MOZ_ASSERT(!mMaster->OutOfDecodedVideo() ||
+ mMaster->IsRequestingVideoData() ||
+ mMaster->IsWaitingVideoData());
+ SLOG(
+ "In buffering mode, waiting to be notified: outOfAudio: %d, "
+ "mAudioStatus: %s, outOfVideo: %d, mVideoStatus: %s",
+ mMaster->OutOfDecodedAudio(), mMaster->AudioRequestStatus(),
+ mMaster->OutOfDecodedVideo(), mMaster->VideoRequestStatus());
+ return;
+ }
+
+ SLOG("Buffered for %.3lfs", (now - mBufferingStart).ToSeconds());
+ SetDecodingState();
+}
+
+void MediaDecoderStateMachine::BufferingState::HandleEndOfAudio() {
+ AudioQueue().Finish();
+ if (!mMaster->IsVideoDecoding()) {
+ SetState<CompletedState>();
+ } else {
+ // Check if we can exit buffering.
+ mMaster->ScheduleStateMachine();
+ }
+}
+
+void MediaDecoderStateMachine::BufferingState::HandleEndOfVideo() {
+ VideoQueue().Finish();
+ if (!mMaster->IsAudioDecoding()) {
+ SetState<CompletedState>();
+ } else {
+ // Check if we can exit buffering.
+ mMaster->ScheduleStateMachine();
+ }
+}
+
+RefPtr<ShutdownPromise> MediaDecoderStateMachine::ShutdownState::Enter() {
+ auto* master = mMaster;
+
+ master->mDelayedScheduler.Reset();
+
+ // Shutdown happens while decode timer is active, we need to disconnect and
+ // dispose of the timer.
+ master->CancelSuspendTimer();
+
+ if (master->IsPlaying()) {
+ master->StopPlayback();
+ }
+
+ master->mAudioDataRequest.DisconnectIfExists();
+ master->mVideoDataRequest.DisconnectIfExists();
+ master->mAudioWaitRequest.DisconnectIfExists();
+ master->mVideoWaitRequest.DisconnectIfExists();
+
+ // Resetting decode should be called after stopping media sink, which can
+ // ensure that we have an empty media queue before seeking the demuxer.
+ master->StopMediaSink();
+ master->ResetDecode();
+ master->mMediaSink->Shutdown();
+
+ // Prevent dangling pointers by disconnecting the listeners.
+ master->mAudioQueueListener.Disconnect();
+ master->mVideoQueueListener.Disconnect();
+ master->mMetadataManager.Disconnect();
+ master->mOnMediaNotSeekable.Disconnect();
+ master->mAudibleListener.DisconnectIfExists();
+
+ // Disconnect canonicals and mirrors before shutting down our task queue.
+ master->mStreamName.DisconnectIfConnected();
+ master->mSinkDevice.DisconnectIfConnected();
+ master->mOutputCaptureState.DisconnectIfConnected();
+ master->mOutputDummyTrack.DisconnectIfConnected();
+ master->mOutputTracks.DisconnectIfConnected();
+ master->mOutputPrincipal.DisconnectIfConnected();
+
+ master->mDuration.DisconnectAll();
+ master->mCurrentPosition.DisconnectAll();
+ master->mIsAudioDataAudible.DisconnectAll();
+
+ // Shut down the watch manager to stop further notifications.
+ master->mWatchManager.Shutdown();
+
+ return Reader()->Shutdown()->Then(OwnerThread(), __func__, master,
+ &MediaDecoderStateMachine::FinishShutdown,
+ &MediaDecoderStateMachine::FinishShutdown);
+}
+
+#define INIT_WATCHABLE(name, val) name(val, "MediaDecoderStateMachine::" #name)
+#define INIT_MIRROR(name, val) \
+ name(mTaskQueue, val, "MediaDecoderStateMachine::" #name " (Mirror)")
+#define INIT_CANONICAL(name, val) \
+ name(mTaskQueue, val, "MediaDecoderStateMachine::" #name " (Canonical)")
+
+MediaDecoderStateMachine::MediaDecoderStateMachine(MediaDecoder* aDecoder,
+ MediaFormatReader* aReader)
+ : MediaDecoderStateMachineBase(aDecoder, aReader),
+ mWatchManager(this, mTaskQueue),
+ mDispatchedStateMachine(false),
+ mDelayedScheduler(mTaskQueue, true /*aFuzzy*/),
+ mCurrentFrameID(0),
+ mAmpleAudioThreshold(detail::AMPLE_AUDIO_THRESHOLD),
+ mVideoDecodeSuspended(false),
+ mVideoDecodeSuspendTimer(mTaskQueue),
+ mVideoDecodeMode(VideoDecodeMode::Normal),
+ mIsMSE(aDecoder->IsMSE()),
+ mShouldResistFingerprinting(aDecoder->ShouldResistFingerprinting()),
+ mSeamlessLoopingAllowed(false),
+ INIT_MIRROR(mStreamName, nsAutoString()),
+ INIT_MIRROR(mSinkDevice, nullptr),
+ INIT_MIRROR(mOutputCaptureState, MediaDecoder::OutputCaptureState::None),
+ INIT_MIRROR(mOutputDummyTrack, nullptr),
+ INIT_MIRROR(mOutputTracks, nsTArray<RefPtr<ProcessedMediaTrack>>()),
+ INIT_MIRROR(mOutputPrincipal, PRINCIPAL_HANDLE_NONE),
+ INIT_CANONICAL(mCanonicalOutputTracks,
+ nsTArray<RefPtr<ProcessedMediaTrack>>()),
+ INIT_CANONICAL(mCanonicalOutputPrincipal, PRINCIPAL_HANDLE_NONE) {
+ MOZ_COUNT_CTOR(MediaDecoderStateMachine);
+ NS_ASSERTION(NS_IsMainThread(), "Should be on main thread.");
+
+ InitVideoQueuePrefs();
+
+ DDLINKCHILD("reader", aReader);
+}
+
+#undef INIT_WATCHABLE
+#undef INIT_MIRROR
+#undef INIT_CANONICAL
+
+MediaDecoderStateMachine::~MediaDecoderStateMachine() {
+ MOZ_ASSERT(NS_IsMainThread(), "Should be on main thread.");
+ MOZ_COUNT_DTOR(MediaDecoderStateMachine);
+}
+
+void MediaDecoderStateMachine::InitializationTask(MediaDecoder* aDecoder) {
+ AUTO_PROFILER_LABEL("MediaDecoderStateMachine::InitializationTask",
+ MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+
+ MediaDecoderStateMachineBase::InitializationTask(aDecoder);
+
+ // Connect mirrors.
+ mStreamName.Connect(aDecoder->CanonicalStreamName());
+ mSinkDevice.Connect(aDecoder->CanonicalSinkDevice());
+ mOutputCaptureState.Connect(aDecoder->CanonicalOutputCaptureState());
+ mOutputDummyTrack.Connect(aDecoder->CanonicalOutputDummyTrack());
+ mOutputTracks.Connect(aDecoder->CanonicalOutputTracks());
+ mOutputPrincipal.Connect(aDecoder->CanonicalOutputPrincipal());
+
+ // Initialize watchers.
+ mWatchManager.Watch(mStreamName,
+ &MediaDecoderStateMachine::StreamNameChanged);
+ mWatchManager.Watch(mOutputCaptureState,
+ &MediaDecoderStateMachine::UpdateOutputCaptured);
+ mWatchManager.Watch(mOutputDummyTrack,
+ &MediaDecoderStateMachine::UpdateOutputCaptured);
+ mWatchManager.Watch(mOutputTracks,
+ &MediaDecoderStateMachine::UpdateOutputCaptured);
+ mWatchManager.Watch(mOutputTracks,
+ &MediaDecoderStateMachine::OutputTracksChanged);
+ mWatchManager.Watch(mOutputPrincipal,
+ &MediaDecoderStateMachine::OutputPrincipalChanged);
+
+ mMediaSink = CreateMediaSink();
+
+ MOZ_ASSERT(!mStateObj);
+ auto* s = new DecodeMetadataState(this);
+ mStateObj.reset(s);
+ s->Enter();
+}
+
+void MediaDecoderStateMachine::AudioAudibleChanged(bool aAudible) {
+ mIsAudioDataAudible = aAudible;
+}
+
+MediaSink* MediaDecoderStateMachine::CreateAudioSink() {
+ if (mOutputCaptureState != MediaDecoder::OutputCaptureState::None) {
+ DecodedStream* stream = new DecodedStream(
+ this,
+ mOutputCaptureState == MediaDecoder::OutputCaptureState::Capture
+ ? mOutputDummyTrack.Ref()
+ : nullptr,
+ mOutputTracks, mVolume, mPlaybackRate, mPreservesPitch, mAudioQueue,
+ mVideoQueue, mSinkDevice.Ref());
+ mAudibleListener.DisconnectIfExists();
+ mAudibleListener = stream->AudibleEvent().Connect(
+ OwnerThread(), this, &MediaDecoderStateMachine::AudioAudibleChanged);
+ return stream;
+ }
+
+ auto audioSinkCreator = [s = RefPtr<MediaDecoderStateMachine>(this), this]() {
+ MOZ_ASSERT(OnTaskQueue());
+ AudioSink* audioSink = new AudioSink(mTaskQueue, mAudioQueue, Info().mAudio,
+ mShouldResistFingerprinting);
+ mAudibleListener.DisconnectIfExists();
+ mAudibleListener = audioSink->AudibleEvent().Connect(
+ mTaskQueue, this, &MediaDecoderStateMachine::AudioAudibleChanged);
+ return audioSink;
+ };
+ return new AudioSinkWrapper(mTaskQueue, mAudioQueue, audioSinkCreator,
+ mVolume, mPlaybackRate, mPreservesPitch,
+ mSinkDevice.Ref());
+}
+
+already_AddRefed<MediaSink> MediaDecoderStateMachine::CreateMediaSink() {
+ MOZ_ASSERT(OnTaskQueue());
+ RefPtr<MediaSink> audioSink = CreateAudioSink();
+ RefPtr<MediaSink> mediaSink =
+ new VideoSink(mTaskQueue, audioSink, mVideoQueue, mVideoFrameContainer,
+ *mFrameStats, sVideoQueueSendToCompositorSize);
+ if (mSecondaryVideoContainer.Ref()) {
+ mediaSink->SetSecondaryVideoContainer(mSecondaryVideoContainer.Ref());
+ }
+ return mediaSink.forget();
+}
+
+TimeUnit MediaDecoderStateMachine::GetDecodedAudioDuration() const {
+ MOZ_ASSERT(OnTaskQueue());
+ if (mMediaSink->IsStarted()) {
+ return mMediaSink->UnplayedDuration(TrackInfo::kAudioTrack) +
+ TimeUnit::FromMicroseconds(AudioQueue().Duration());
+ }
+ // MediaSink not started. All audio samples are in the queue.
+ return TimeUnit::FromMicroseconds(AudioQueue().Duration());
+}
+
+bool MediaDecoderStateMachine::HaveEnoughDecodedAudio() const {
+ MOZ_ASSERT(OnTaskQueue());
+ auto ampleAudio = mAmpleAudioThreshold.MultDouble(mPlaybackRate);
+ return AudioQueue().GetSize() > 0 && GetDecodedAudioDuration() >= ampleAudio;
+}
+
+bool MediaDecoderStateMachine::HaveEnoughDecodedVideo() const {
+ MOZ_ASSERT(OnTaskQueue());
+ return static_cast<double>(VideoQueue().GetSize()) >=
+ GetAmpleVideoFrames() * mPlaybackRate + 1 &&
+ IsVideoDataEnoughComparedWithAudio();
+}
+
+bool MediaDecoderStateMachine::IsVideoDataEnoughComparedWithAudio() const {
+ // HW decoding is usually fast enough and we don't need to worry about its
+ // speed.
+ // TODO : we can consider whether we need to enable this on other HW decoding
+ // except VAAPI. When enabling VAAPI on Linux, ffmpeg is not able to store too
+ // many frames because it has a limitation of amount of stored video frames.
+ // See bug1716638 and 1718309.
+ if (mReader->VideoIsHardwareAccelerated()) {
+ return true;
+ }
+ // In extreme situations (e.g. 4k+ video without hardware acceleration), the
+ // video decoding will be much slower than audio. So for 4K+ video, we want to
+ // consider audio decoding speed as well in order to reduce frame drops. This
+ // check tries to keep the decoded video buffered as much as audio.
+ if (HasAudio() && Info().mVideo.mImage.width >= 3840 &&
+ Info().mVideo.mImage.height >= 2160) {
+ return VideoQueue().Duration() >= AudioQueue().Duration();
+ }
+ // For non-4k video, the video decoding is usually really fast so we won't
+ // need to consider audio decoding speed to store extra frames.
+ return true;
+}
+
+void MediaDecoderStateMachine::PushAudio(AudioData* aSample) {
+ MOZ_ASSERT(OnTaskQueue());
+ MOZ_ASSERT(aSample);
+ AudioQueue().Push(aSample);
+ PROFILER_MARKER("MDSM::PushAudio", MEDIA_PLAYBACK, {}, MediaSampleMarker,
+ aSample->mTime.ToMicroseconds(),
+ aSample->GetEndTime().ToMicroseconds(),
+ AudioQueue().GetSize());
+}
+
+void MediaDecoderStateMachine::PushVideo(VideoData* aSample) {
+ MOZ_ASSERT(OnTaskQueue());
+ MOZ_ASSERT(aSample);
+ aSample->mFrameID = ++mCurrentFrameID;
+ VideoQueue().Push(aSample);
+ PROFILER_MARKER("MDSM::PushVideo", MEDIA_PLAYBACK, {}, MediaSampleMarker,
+ aSample->mTime.ToMicroseconds(),
+ aSample->GetEndTime().ToMicroseconds(),
+ VideoQueue().GetSize());
+}
+
+void MediaDecoderStateMachine::OnAudioPopped(const RefPtr<AudioData>& aSample) {
+ MOZ_ASSERT(OnTaskQueue());
+ mPlaybackOffset = std::max(mPlaybackOffset, aSample->mOffset);
+}
+
+void MediaDecoderStateMachine::OnVideoPopped(const RefPtr<VideoData>& aSample) {
+ MOZ_ASSERT(OnTaskQueue());
+ mPlaybackOffset = std::max(mPlaybackOffset, aSample->mOffset);
+}
+
+bool MediaDecoderStateMachine::IsAudioDecoding() {
+ MOZ_ASSERT(OnTaskQueue());
+ return HasAudio() && !AudioQueue().IsFinished();
+}
+
+bool MediaDecoderStateMachine::IsVideoDecoding() {
+ MOZ_ASSERT(OnTaskQueue());
+ return HasVideo() && !VideoQueue().IsFinished();
+}
+
+bool MediaDecoderStateMachine::IsPlaying() const {
+ MOZ_ASSERT(OnTaskQueue());
+ return mMediaSink->IsPlaying();
+}
+
+void MediaDecoderStateMachine::SetMediaNotSeekable() { mMediaSeekable = false; }
+
+nsresult MediaDecoderStateMachine::Init(MediaDecoder* aDecoder) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsresult rv = MediaDecoderStateMachineBase::Init(aDecoder);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mAudioQueueListener = AudioQueue().PopFrontEvent().Connect(
+ mTaskQueue, this, &MediaDecoderStateMachine::OnAudioPopped);
+ mVideoQueueListener = VideoQueue().PopFrontEvent().Connect(
+ mTaskQueue, this, &MediaDecoderStateMachine::OnVideoPopped);
+ mOnMediaNotSeekable = mReader->OnMediaNotSeekable().Connect(
+ OwnerThread(), this, &MediaDecoderStateMachine::SetMediaNotSeekable);
+
+ return NS_OK;
+}
+
+void MediaDecoderStateMachine::StopPlayback() {
+ MOZ_ASSERT(OnTaskQueue());
+ LOG("StopPlayback()");
+
+ if (IsPlaying()) {
+ mOnPlaybackEvent.Notify(MediaPlaybackEvent{
+ MediaPlaybackEvent::PlaybackStopped, mPlaybackOffset});
+ mMediaSink->SetPlaying(false);
+ MOZ_ASSERT(!IsPlaying());
+ }
+}
+
+void MediaDecoderStateMachine::MaybeStartPlayback() {
+ MOZ_ASSERT(OnTaskQueue());
+ // Should try to start playback only after decoding first frames.
+ if (!mSentFirstFrameLoadedEvent) {
+ LOG("MaybeStartPlayback: Not starting playback before loading first frame");
+ return;
+ }
+
+ if (IsPlaying()) {
+ // Logging this case is really spammy - don't do it.
+ return;
+ }
+
+ if (mIsMediaSinkSuspended) {
+ LOG("MaybeStartPlayback: Not starting playback when sink is suspended");
+ return;
+ }
+
+ if (mPlayState != MediaDecoder::PLAY_STATE_PLAYING) {
+ LOG("MaybeStartPlayback: Not starting playback [mPlayState=%d]",
+ mPlayState.Ref());
+ return;
+ }
+
+ LOG("MaybeStartPlayback() starting playback");
+ StartMediaSink();
+
+ if (!IsPlaying()) {
+ mMediaSink->SetPlaying(true);
+ MOZ_ASSERT(IsPlaying());
+ }
+
+ mOnPlaybackEvent.Notify(
+ MediaPlaybackEvent{MediaPlaybackEvent::PlaybackStarted, mPlaybackOffset});
+}
+
+void MediaDecoderStateMachine::UpdatePlaybackPositionInternal(
+ const TimeUnit& aTime) {
+ MOZ_ASSERT(OnTaskQueue());
+ LOGV("UpdatePlaybackPositionInternal(%" PRId64 ")", aTime.ToMicroseconds());
+
+ mCurrentPosition = aTime;
+ NS_ASSERTION(mCurrentPosition.Ref() >= TimeUnit::Zero(),
+ "CurrentTime should be positive!");
+ if (mDuration.Ref().ref() < mCurrentPosition.Ref()) {
+ mDuration = Some(mCurrentPosition.Ref());
+ DDLOG(DDLogCategory::Property, "duration_us",
+ mDuration.Ref()->ToMicroseconds());
+ }
+}
+
+void MediaDecoderStateMachine::UpdatePlaybackPosition(const TimeUnit& aTime) {
+ MOZ_ASSERT(OnTaskQueue());
+ UpdatePlaybackPositionInternal(aTime);
+
+ bool fragmentEnded =
+ mFragmentEndTime.IsValid() && GetMediaTime() >= mFragmentEndTime;
+ mMetadataManager.DispatchMetadataIfNeeded(aTime);
+
+ if (fragmentEnded) {
+ StopPlayback();
+ }
+}
+
+/* static */ const char* MediaDecoderStateMachine::ToStateStr(State aState) {
+ switch (aState) {
+ case DECODER_STATE_DECODING_METADATA:
+ return "DECODING_METADATA";
+ case DECODER_STATE_DORMANT:
+ return "DORMANT";
+ case DECODER_STATE_DECODING_FIRSTFRAME:
+ return "DECODING_FIRSTFRAME";
+ case DECODER_STATE_DECODING:
+ return "DECODING";
+ case DECODER_STATE_SEEKING_ACCURATE:
+ return "SEEKING_ACCURATE";
+ case DECODER_STATE_SEEKING_FROMDORMANT:
+ return "SEEKING_FROMDORMANT";
+ case DECODER_STATE_SEEKING_NEXTFRAMESEEKING:
+ return "DECODER_STATE_SEEKING_NEXTFRAMESEEKING";
+ case DECODER_STATE_SEEKING_VIDEOONLY:
+ return "SEEKING_VIDEOONLY";
+ case DECODER_STATE_BUFFERING:
+ return "BUFFERING";
+ case DECODER_STATE_COMPLETED:
+ return "COMPLETED";
+ case DECODER_STATE_SHUTDOWN:
+ return "SHUTDOWN";
+ case DECODER_STATE_LOOPING_DECODING:
+ return "LOOPING_DECODING";
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid state.");
+ }
+ return "UNKNOWN";
+}
+
+const char* MediaDecoderStateMachine::ToStateStr() {
+ MOZ_ASSERT(OnTaskQueue());
+ return ToStateStr(mStateObj->GetState());
+}
+
+void MediaDecoderStateMachine::VolumeChanged() {
+ AUTO_PROFILER_LABEL("MediaDecoderStateMachine::VolumeChanged",
+ MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+ mMediaSink->SetVolume(mVolume);
+}
+
+RefPtr<ShutdownPromise> MediaDecoderStateMachine::Shutdown() {
+ AUTO_PROFILER_LABEL("MediaDecoderStateMachine::Shutdown", MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+ return mStateObj->HandleShutdown();
+}
+
+void MediaDecoderStateMachine::PlayStateChanged() {
+ AUTO_PROFILER_LABEL("MediaDecoderStateMachine::PlayStateChanged",
+ MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+
+ if (mPlayState != MediaDecoder::PLAY_STATE_PLAYING) {
+ CancelSuspendTimer();
+ } else if (mMinimizePreroll) {
+ // Once we start playing, we don't want to minimize our prerolling, as we
+ // assume the user is likely to want to keep playing in future. This needs
+ // to happen before we invoke StartDecoding().
+ mMinimizePreroll = false;
+ }
+
+ mStateObj->HandlePlayStateChanged(mPlayState);
+}
+
+void MediaDecoderStateMachine::SetVideoDecodeMode(VideoDecodeMode aMode) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCOMPtr<nsIRunnable> r = NewRunnableMethod<VideoDecodeMode>(
+ "MediaDecoderStateMachine::SetVideoDecodeModeInternal", this,
+ &MediaDecoderStateMachine::SetVideoDecodeModeInternal, aMode);
+ OwnerThread()->DispatchStateChange(r.forget());
+}
+
+void MediaDecoderStateMachine::SetVideoDecodeModeInternal(
+ VideoDecodeMode aMode) {
+ MOZ_ASSERT(OnTaskQueue());
+
+ LOG("SetVideoDecodeModeInternal(), VideoDecodeMode=(%s->%s), "
+ "mVideoDecodeSuspended=%c",
+ mVideoDecodeMode == VideoDecodeMode::Normal ? "Normal" : "Suspend",
+ aMode == VideoDecodeMode::Normal ? "Normal" : "Suspend",
+ mVideoDecodeSuspended ? 'T' : 'F');
+
+ // Should not suspend decoding if we don't turn on the pref.
+ if (!StaticPrefs::media_suspend_bkgnd_video_enabled() &&
+ aMode == VideoDecodeMode::Suspend) {
+ LOG("SetVideoDecodeModeInternal(), early return because preference off and "
+ "set to Suspend");
+ return;
+ }
+
+ if (aMode == mVideoDecodeMode) {
+ LOG("SetVideoDecodeModeInternal(), early return because the mode does not "
+ "change");
+ return;
+ }
+
+ // Set new video decode mode.
+ mVideoDecodeMode = aMode;
+
+ // Start timer to trigger suspended video decoding.
+ if (mVideoDecodeMode == VideoDecodeMode::Suspend) {
+ TimeStamp target = TimeStamp::Now() + SuspendBackgroundVideoDelay();
+
+ RefPtr<MediaDecoderStateMachine> self = this;
+ mVideoDecodeSuspendTimer.Ensure(
+ target, [=]() { self->OnSuspendTimerResolved(); },
+ []() { MOZ_DIAGNOSTIC_ASSERT(false); });
+ mOnPlaybackEvent.Notify(MediaPlaybackEvent::StartVideoSuspendTimer);
+ return;
+ }
+
+ // Resuming from suspended decoding
+
+ // If suspend timer exists, destroy it.
+ CancelSuspendTimer();
+
+ if (mVideoDecodeSuspended) {
+ auto target = mMediaSink->IsStarted() ? GetClock() : GetMediaTime();
+ AdjustByLooping(target);
+ mStateObj->HandleResumeVideoDecoding(target + detail::RESUME_VIDEO_PREMIUM);
+ }
+}
+
+void MediaDecoderStateMachine::BufferedRangeUpdated() {
+ AUTO_PROFILER_LABEL("MediaDecoderStateMachine::BufferedRangeUpdated",
+ MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+
+ // While playing an unseekable stream of unknown duration, mDuration
+ // is updated as we play. But if data is being downloaded
+ // faster than played, mDuration won't reflect the end of playable data
+ // since we haven't played the frame at the end of buffered data. So update
+ // mDuration here as new data is downloaded to prevent such a lag.
+ if (mBuffered.Ref().IsInvalid()) {
+ return;
+ }
+
+ bool exists;
+ media::TimeUnit end{mBuffered.Ref().GetEnd(&exists)};
+ if (!exists) {
+ return;
+ }
+
+ // Use estimated duration from buffer ranges when mDuration is unknown or
+ // the estimated duration is larger.
+ if (mDuration.Ref().isNothing() || mDuration.Ref()->IsInfinite() ||
+ end > mDuration.Ref().ref()) {
+ mDuration = Some(end);
+ DDLOG(DDLogCategory::Property, "duration_us",
+ mDuration.Ref()->ToMicroseconds());
+ }
+}
+
+RefPtr<MediaDecoder::SeekPromise> MediaDecoderStateMachine::Seek(
+ const SeekTarget& aTarget) {
+ AUTO_PROFILER_LABEL("MediaDecoderStateMachine::Seek", MEDIA_PLAYBACK);
+ PROFILER_MARKER_UNTYPED("MDSM::Seek", MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+
+ // We need to be able to seek in some way
+ if (!mMediaSeekable && !mMediaSeekableOnlyInBufferedRanges) {
+ LOGW("Seek() should not be called on a non-seekable media");
+ return MediaDecoder::SeekPromise::CreateAndReject(/* aRejectValue = */ true,
+ __func__);
+ }
+
+ if (aTarget.IsNextFrame() && !HasVideo()) {
+ LOGW("Ignore a NextFrameSeekTask on a media file without video track.");
+ return MediaDecoder::SeekPromise::CreateAndReject(/* aRejectValue = */ true,
+ __func__);
+ }
+
+ MOZ_ASSERT(mDuration.Ref().isSome(), "We should have got duration already");
+
+ return mStateObj->HandleSeek(aTarget);
+}
+
+void MediaDecoderStateMachine::StopMediaSink() {
+ MOZ_ASSERT(OnTaskQueue());
+ if (mMediaSink->IsStarted()) {
+ LOG("Stop MediaSink");
+ mMediaSink->Stop();
+ mMediaSinkAudioEndedPromise.DisconnectIfExists();
+ mMediaSinkVideoEndedPromise.DisconnectIfExists();
+ }
+}
+
+void MediaDecoderStateMachine::RequestAudioData() {
+ AUTO_PROFILER_LABEL("MediaDecoderStateMachine::RequestAudioData",
+ MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+ MOZ_ASSERT(IsAudioDecoding());
+ MOZ_ASSERT(!IsRequestingAudioData());
+ MOZ_ASSERT(!IsWaitingAudioData());
+ LOGV("Queueing audio task - queued=%zu, decoder-queued=%zu",
+ AudioQueue().GetSize(), mReader->SizeOfAudioQueueInFrames());
+
+ PerformanceRecorder<PlaybackStage> perfRecorder(MediaStage::RequestData);
+ RefPtr<MediaDecoderStateMachine> self = this;
+ mReader->RequestAudioData()
+ ->Then(
+ OwnerThread(), __func__,
+ [this, self, perfRecorder(std::move(perfRecorder))](
+ const RefPtr<AudioData>& aAudio) mutable {
+ perfRecorder.Record();
+ AUTO_PROFILER_LABEL(
+ "MediaDecoderStateMachine::RequestAudioData:Resolved",
+ MEDIA_PLAYBACK);
+ MOZ_ASSERT(aAudio);
+ mAudioDataRequest.Complete();
+ // audio->GetEndTime() is not always mono-increasing in chained
+ // ogg.
+ mDecodedAudioEndTime =
+ std::max(aAudio->GetEndTime(), mDecodedAudioEndTime);
+ LOGV("OnAudioDecoded [%" PRId64 ",%" PRId64 "]",
+ aAudio->mTime.ToMicroseconds(),
+ aAudio->GetEndTime().ToMicroseconds());
+ mStateObj->HandleAudioDecoded(aAudio);
+ },
+ [this, self](const MediaResult& aError) {
+ AUTO_PROFILER_LABEL(
+ "MediaDecoderStateMachine::RequestAudioData:Rejected",
+ MEDIA_PLAYBACK);
+ LOGV("OnAudioNotDecoded ErrorName=%s Message=%s",
+ aError.ErrorName().get(), aError.Message().get());
+ mAudioDataRequest.Complete();
+ switch (aError.Code()) {
+ case NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA:
+ mStateObj->HandleWaitingForAudio();
+ break;
+ case NS_ERROR_DOM_MEDIA_CANCELED:
+ mStateObj->HandleAudioCanceled();
+ break;
+ case NS_ERROR_DOM_MEDIA_END_OF_STREAM:
+ mStateObj->HandleEndOfAudio();
+ break;
+ default:
+ DecodeError(aError);
+ }
+ })
+ ->Track(mAudioDataRequest);
+}
+
+void MediaDecoderStateMachine::RequestVideoData(
+ const media::TimeUnit& aCurrentTime, bool aRequestNextKeyFrame) {
+ AUTO_PROFILER_LABEL("MediaDecoderStateMachine::RequestVideoData",
+ MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+ MOZ_ASSERT(IsVideoDecoding());
+ MOZ_ASSERT(!IsRequestingVideoData());
+ MOZ_ASSERT(!IsWaitingVideoData());
+ LOGV(
+ "Queueing video task - queued=%zu, decoder-queued=%zo"
+ ", stime=%" PRId64 ", by-pass-skip=%d",
+ VideoQueue().GetSize(), mReader->SizeOfVideoQueueInFrames(),
+ aCurrentTime.ToMicroseconds(), mBypassingSkipToNextKeyFrameCheck);
+
+ PerformanceRecorder<PlaybackStage> perfRecorder(MediaStage::RequestData,
+ Info().mVideo.mImage.height);
+ RefPtr<MediaDecoderStateMachine> self = this;
+ mReader
+ ->RequestVideoData(
+ mBypassingSkipToNextKeyFrameCheck ? media::TimeUnit() : aCurrentTime,
+ mBypassingSkipToNextKeyFrameCheck ? false : aRequestNextKeyFrame)
+ ->Then(
+ OwnerThread(), __func__,
+ [this, self, perfRecorder(std::move(perfRecorder))](
+ const RefPtr<VideoData>& aVideo) mutable {
+ perfRecorder.Record();
+ AUTO_PROFILER_LABEL(
+ "MediaDecoderStateMachine::RequestVideoData:Resolved",
+ MEDIA_PLAYBACK);
+ MOZ_ASSERT(aVideo);
+ mVideoDataRequest.Complete();
+ // Handle abnormal or negative timestamps.
+ mDecodedVideoEndTime =
+ std::max(mDecodedVideoEndTime, aVideo->GetEndTime());
+ LOGV("OnVideoDecoded [%" PRId64 ",%" PRId64 "]",
+ aVideo->mTime.ToMicroseconds(),
+ aVideo->GetEndTime().ToMicroseconds());
+ mStateObj->HandleVideoDecoded(aVideo);
+ },
+ [this, self](const MediaResult& aError) {
+ AUTO_PROFILER_LABEL(
+ "MediaDecoderStateMachine::RequestVideoData:Rejected",
+ MEDIA_PLAYBACK);
+ LOGV("OnVideoNotDecoded ErrorName=%s Message=%s",
+ aError.ErrorName().get(), aError.Message().get());
+ mVideoDataRequest.Complete();
+ switch (aError.Code()) {
+ case NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA:
+ mStateObj->HandleWaitingForVideo();
+ break;
+ case NS_ERROR_DOM_MEDIA_CANCELED:
+ mStateObj->HandleVideoCanceled();
+ break;
+ case NS_ERROR_DOM_MEDIA_END_OF_STREAM:
+ mStateObj->HandleEndOfVideo();
+ break;
+ default:
+ DecodeError(aError);
+ }
+ })
+ ->Track(mVideoDataRequest);
+}
+
+void MediaDecoderStateMachine::WaitForData(MediaData::Type aType) {
+ MOZ_ASSERT(OnTaskQueue());
+ MOZ_ASSERT(aType == MediaData::Type::AUDIO_DATA ||
+ aType == MediaData::Type::VIDEO_DATA);
+ RefPtr<MediaDecoderStateMachine> self = this;
+ if (aType == MediaData::Type::AUDIO_DATA) {
+ mReader->WaitForData(MediaData::Type::AUDIO_DATA)
+ ->Then(
+ OwnerThread(), __func__,
+ [self](MediaData::Type aType) {
+ AUTO_PROFILER_LABEL(
+ "MediaDecoderStateMachine::WaitForData:AudioResolved",
+ MEDIA_PLAYBACK);
+ self->mAudioWaitRequest.Complete();
+ MOZ_ASSERT(aType == MediaData::Type::AUDIO_DATA);
+ self->mStateObj->HandleAudioWaited(aType);
+ },
+ [self](const WaitForDataRejectValue& aRejection) {
+ AUTO_PROFILER_LABEL(
+ "MediaDecoderStateMachine::WaitForData:AudioRejected",
+ MEDIA_PLAYBACK);
+ self->mAudioWaitRequest.Complete();
+ self->DecodeError(NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA);
+ })
+ ->Track(mAudioWaitRequest);
+ } else {
+ mReader->WaitForData(MediaData::Type::VIDEO_DATA)
+ ->Then(
+ OwnerThread(), __func__,
+ [self](MediaData::Type aType) {
+ AUTO_PROFILER_LABEL(
+ "MediaDecoderStateMachine::WaitForData:VideoResolved",
+ MEDIA_PLAYBACK);
+ self->mVideoWaitRequest.Complete();
+ MOZ_ASSERT(aType == MediaData::Type::VIDEO_DATA);
+ self->mStateObj->HandleVideoWaited(aType);
+ },
+ [self](const WaitForDataRejectValue& aRejection) {
+ AUTO_PROFILER_LABEL(
+ "MediaDecoderStateMachine::WaitForData:VideoRejected",
+ MEDIA_PLAYBACK);
+ self->mVideoWaitRequest.Complete();
+ self->DecodeError(NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA);
+ })
+ ->Track(mVideoWaitRequest);
+ }
+}
+
+nsresult MediaDecoderStateMachine::StartMediaSink() {
+ MOZ_ASSERT(OnTaskQueue());
+
+ if (mMediaSink->IsStarted()) {
+ return NS_OK;
+ }
+
+ mAudioCompleted = false;
+ nsresult rv = mMediaSink->Start(GetMediaTime(), Info());
+ StreamNameChanged();
+
+ auto videoPromise = mMediaSink->OnEnded(TrackInfo::kVideoTrack);
+ auto audioPromise = mMediaSink->OnEnded(TrackInfo::kAudioTrack);
+
+ if (audioPromise) {
+ audioPromise
+ ->Then(OwnerThread(), __func__, this,
+ &MediaDecoderStateMachine::OnMediaSinkAudioComplete,
+ &MediaDecoderStateMachine::OnMediaSinkAudioError)
+ ->Track(mMediaSinkAudioEndedPromise);
+ }
+ if (videoPromise) {
+ videoPromise
+ ->Then(OwnerThread(), __func__, this,
+ &MediaDecoderStateMachine::OnMediaSinkVideoComplete,
+ &MediaDecoderStateMachine::OnMediaSinkVideoError)
+ ->Track(mMediaSinkVideoEndedPromise);
+ }
+ // Remember the initial offset when playback starts. This will be used
+ // to calculate the rate at which bytes are consumed as playback moves on.
+ RefPtr<MediaData> sample = mAudioQueue.PeekFront();
+ mPlaybackOffset = sample ? sample->mOffset : 0;
+ sample = mVideoQueue.PeekFront();
+ if (sample && sample->mOffset > mPlaybackOffset) {
+ mPlaybackOffset = sample->mOffset;
+ }
+ return rv;
+}
+
+bool MediaDecoderStateMachine::HasLowDecodedAudio() {
+ MOZ_ASSERT(OnTaskQueue());
+ return IsAudioDecoding() &&
+ GetDecodedAudioDuration() <
+ EXHAUSTED_DATA_MARGIN.MultDouble(mPlaybackRate);
+}
+
+bool MediaDecoderStateMachine::HasLowDecodedVideo() {
+ MOZ_ASSERT(OnTaskQueue());
+ return IsVideoDecoding() &&
+ VideoQueue().GetSize() <
+ static_cast<size_t>(floorl(LOW_VIDEO_FRAMES * mPlaybackRate));
+}
+
+bool MediaDecoderStateMachine::HasLowDecodedData() {
+ MOZ_ASSERT(OnTaskQueue());
+ MOZ_ASSERT(mReader->UseBufferingHeuristics());
+ return HasLowDecodedAudio() || HasLowDecodedVideo();
+}
+
+bool MediaDecoderStateMachine::OutOfDecodedAudio() {
+ MOZ_ASSERT(OnTaskQueue());
+ return IsAudioDecoding() && !AudioQueue().IsFinished() &&
+ AudioQueue().GetSize() == 0 &&
+ !mMediaSink->HasUnplayedFrames(TrackInfo::kAudioTrack);
+}
+
+bool MediaDecoderStateMachine::HasLowBufferedData() {
+ MOZ_ASSERT(OnTaskQueue());
+ return HasLowBufferedData(detail::LOW_BUFFER_THRESHOLD);
+}
+
+bool MediaDecoderStateMachine::HasLowBufferedData(const TimeUnit& aThreshold) {
+ MOZ_ASSERT(OnTaskQueue());
+
+ // If we don't have a duration, mBuffered is probably not going to have
+ // a useful buffered range. Return false here so that we don't get stuck in
+ // buffering mode for live streams.
+ if (Duration().IsInfinite()) {
+ return false;
+ }
+
+ if (mBuffered.Ref().IsInvalid()) {
+ return false;
+ }
+
+ // We are never low in decoded data when we don't have audio/video or have
+ // decoded all audio/video samples.
+ TimeUnit endOfDecodedVideo = (HasVideo() && !VideoQueue().IsFinished())
+ ? mDecodedVideoEndTime
+ : TimeUnit::FromNegativeInfinity();
+ TimeUnit endOfDecodedAudio = (HasAudio() && !AudioQueue().IsFinished())
+ ? mDecodedAudioEndTime
+ : TimeUnit::FromNegativeInfinity();
+
+ auto endOfDecodedData = std::max(endOfDecodedVideo, endOfDecodedAudio);
+ if (Duration() < endOfDecodedData) {
+ // Our duration is not up to date. No point buffering.
+ return false;
+ }
+
+ if (endOfDecodedData.IsInfinite()) {
+ // Have decoded all samples. No point buffering.
+ return false;
+ }
+
+ auto start = endOfDecodedData;
+ auto end = std::min(GetMediaTime() + aThreshold, Duration());
+ if (start >= end) {
+ // Duration of decoded samples is greater than our threshold.
+ return false;
+ }
+ media::TimeInterval interval(start, end);
+ return !mBuffered.Ref().Contains(interval);
+}
+
+void MediaDecoderStateMachine::EnqueueFirstFrameLoadedEvent() {
+ MOZ_ASSERT(OnTaskQueue());
+ // Track value of mSentFirstFrameLoadedEvent from before updating it
+ bool firstFrameBeenLoaded = mSentFirstFrameLoadedEvent;
+ mSentFirstFrameLoadedEvent = true;
+ MediaDecoderEventVisibility visibility =
+ firstFrameBeenLoaded ? MediaDecoderEventVisibility::Suppressed
+ : MediaDecoderEventVisibility::Observable;
+ mFirstFrameLoadedEvent.Notify(UniquePtr<MediaInfo>(new MediaInfo(Info())),
+ visibility);
+}
+
+void MediaDecoderStateMachine::FinishDecodeFirstFrame() {
+ MOZ_ASSERT(OnTaskQueue());
+ MOZ_ASSERT(!mSentFirstFrameLoadedEvent);
+ LOG("FinishDecodeFirstFrame");
+
+ mMediaSink->Redraw(Info().mVideo);
+
+ LOG("Media duration %" PRId64 ", mediaSeekable=%d",
+ Duration().ToMicroseconds(), mMediaSeekable);
+
+ // Get potentially updated metadata
+ mReader->ReadUpdatedMetadata(mInfo.ptr());
+
+ EnqueueFirstFrameLoadedEvent();
+}
+
+RefPtr<ShutdownPromise> MediaDecoderStateMachine::FinishShutdown() {
+ AUTO_PROFILER_LABEL("MediaDecoderStateMachine::FinishShutdown",
+ MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+ LOG("Shutting down state machine task queue");
+ return OwnerThread()->BeginShutdown();
+}
+
+void MediaDecoderStateMachine::RunStateMachine() {
+ MOZ_ASSERT(OnTaskQueue());
+ AUTO_PROFILER_LABEL("MediaDecoderStateMachine::RunStateMachine",
+ MEDIA_PLAYBACK);
+ mDelayedScheduler.Reset(); // Must happen on state machine task queue.
+ mDispatchedStateMachine = false;
+ mStateObj->Step();
+}
+
+void MediaDecoderStateMachine::ResetDecode(const TrackSet& aTracks) {
+ MOZ_ASSERT(OnTaskQueue());
+ LOG("MediaDecoderStateMachine::Reset");
+
+ // Assert that aTracks specifies to reset the video track because we
+ // don't currently support resetting just the audio track.
+ MOZ_ASSERT(aTracks.contains(TrackInfo::kVideoTrack));
+
+ if (aTracks.contains(TrackInfo::kVideoTrack)) {
+ mDecodedVideoEndTime = TimeUnit::Zero();
+ mVideoCompleted = false;
+ VideoQueue().Reset();
+ mVideoDataRequest.DisconnectIfExists();
+ mVideoWaitRequest.DisconnectIfExists();
+ }
+
+ if (aTracks.contains(TrackInfo::kAudioTrack)) {
+ mDecodedAudioEndTime = TimeUnit::Zero();
+ mAudioCompleted = false;
+ AudioQueue().Reset();
+ mAudioDataRequest.DisconnectIfExists();
+ mAudioWaitRequest.DisconnectIfExists();
+ }
+
+ mReader->ResetDecode(aTracks);
+}
+
+media::TimeUnit MediaDecoderStateMachine::GetClock(
+ TimeStamp* aTimeStamp) const {
+ MOZ_ASSERT(OnTaskQueue());
+ auto clockTime = mMediaSink->GetPosition(aTimeStamp);
+ // This fails on Windows some times, see 1765563
+#if defined(XP_WIN)
+ NS_ASSERTION(GetMediaTime() <= clockTime, "Clock should go forwards.");
+#else
+ MOZ_ASSERT(GetMediaTime() <= clockTime, "Clock should go forwards.");
+#endif
+ return clockTime;
+}
+
+void MediaDecoderStateMachine::UpdatePlaybackPositionPeriodically() {
+ MOZ_ASSERT(OnTaskQueue());
+
+ if (!IsPlaying()) {
+ return;
+ }
+
+ // Cap the current time to the larger of the audio and video end time.
+ // This ensures that if we're running off the system clock, we don't
+ // advance the clock to after the media end time.
+ if (VideoEndTime() > TimeUnit::Zero() || AudioEndTime() > TimeUnit::Zero()) {
+ auto clockTime = GetClock();
+ // Once looping was turned on, the time is probably larger than the duration
+ // of the media track, so the time over the end should be corrected.
+ AdjustByLooping(clockTime);
+ bool loopback = clockTime < GetMediaTime() && mLooping;
+ if (loopback && mBypassingSkipToNextKeyFrameCheck) {
+ LOG("media has looped back, no longer bypassing skip-to-next-key-frame");
+ mBypassingSkipToNextKeyFrameCheck = false;
+ }
+
+ // Skip frames up to the frame at the playback position, and figure out
+ // the time remaining until it's time to display the next frame and drop
+ // the current frame.
+ NS_ASSERTION(clockTime >= TimeUnit::Zero(),
+ "Should have positive clock time.");
+
+ // These will be non -1 if we've displayed a video frame, or played an audio
+ // frame.
+ auto maxEndTime = std::max(VideoEndTime(), AudioEndTime());
+ auto t = std::min(clockTime, maxEndTime);
+ // FIXME: Bug 1091422 - chained ogg files hit this assertion.
+ // MOZ_ASSERT(t >= GetMediaTime());
+ if (loopback || t > GetMediaTime()) {
+ UpdatePlaybackPosition(t);
+ }
+ }
+ // Note we have to update playback position before releasing the monitor.
+ // Otherwise, MediaDecoder::AddOutputTrack could kick in when we are outside
+ // the monitor and get a staled value from GetCurrentTimeUs() which hits the
+ // assertion in GetClock().
+
+ int64_t delay = std::max<int64_t>(
+ 1, static_cast<int64_t>(AUDIO_DURATION_USECS / mPlaybackRate));
+ ScheduleStateMachineIn(TimeUnit::FromMicroseconds(delay));
+
+ // Notify the listener as we progress in the playback offset. Note it would
+ // be too intensive to send notifications for each popped audio/video sample.
+ // It is good enough to send 'PlaybackProgressed' events every 40us (defined
+ // by AUDIO_DURATION_USECS), and we ensure 'PlaybackProgressed' events are
+ // always sent after 'PlaybackStarted' and before 'PlaybackStopped'.
+ mOnPlaybackEvent.Notify(MediaPlaybackEvent{
+ MediaPlaybackEvent::PlaybackProgressed, mPlaybackOffset});
+}
+
+void MediaDecoderStateMachine::ScheduleStateMachine() {
+ MOZ_ASSERT(OnTaskQueue());
+ if (mDispatchedStateMachine) {
+ return;
+ }
+ mDispatchedStateMachine = true;
+
+ nsresult rv = OwnerThread()->Dispatch(
+ NewRunnableMethod("MediaDecoderStateMachine::RunStateMachine", this,
+ &MediaDecoderStateMachine::RunStateMachine));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+}
+
+void MediaDecoderStateMachine::ScheduleStateMachineIn(const TimeUnit& aTime) {
+ AUTO_PROFILER_LABEL("MediaDecoderStateMachine::ScheduleStateMachineIn",
+ MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue()); // mDelayedScheduler.Ensure() may Disconnect()
+ // the promise, which must happen on the state
+ // machine task queue.
+ MOZ_ASSERT(aTime > TimeUnit::Zero());
+ if (mDispatchedStateMachine) {
+ return;
+ }
+
+ TimeStamp target = TimeStamp::Now() + aTime.ToTimeDuration();
+
+ // It is OK to capture 'this' without causing UAF because the callback
+ // always happens before shutdown.
+ RefPtr<MediaDecoderStateMachine> self = this;
+ mDelayedScheduler.Ensure(
+ target,
+ [self]() {
+ self->mDelayedScheduler.CompleteRequest();
+ self->RunStateMachine();
+ },
+ []() { MOZ_DIAGNOSTIC_ASSERT(false); });
+}
+
+bool MediaDecoderStateMachine::IsStateMachineScheduled() const {
+ MOZ_ASSERT(OnTaskQueue());
+ return mDispatchedStateMachine || mDelayedScheduler.IsScheduled();
+}
+
+void MediaDecoderStateMachine::SetPlaybackRate(double aPlaybackRate) {
+ MOZ_ASSERT(OnTaskQueue());
+ MOZ_ASSERT(aPlaybackRate != 0, "Should be handled by MediaDecoder::Pause()");
+
+ mPlaybackRate = aPlaybackRate;
+ mMediaSink->SetPlaybackRate(mPlaybackRate);
+
+ // Schedule next cycle to check if we can stop prerolling.
+ ScheduleStateMachine();
+}
+
+void MediaDecoderStateMachine::PreservesPitchChanged() {
+ AUTO_PROFILER_LABEL("MediaDecoderStateMachine::PreservesPitchChanged",
+ MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+ mMediaSink->SetPreservesPitch(mPreservesPitch);
+}
+
+void MediaDecoderStateMachine::LoopingChanged() {
+ AUTO_PROFILER_LABEL("MediaDecoderStateMachine::LoopingChanged",
+ MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+ LOGV("LoopingChanged, looping=%d", mLooping.Ref());
+ PROFILER_MARKER_TEXT("MDSM::LoopingChanged", MEDIA_PLAYBACK, {},
+ mLooping ? "true"_ns : "false"_ns);
+ if (mSeamlessLoopingAllowed) {
+ mStateObj->HandleLoopingChanged();
+ }
+}
+
+void MediaDecoderStateMachine::StreamNameChanged() {
+ AUTO_PROFILER_LABEL("MediaDecoderStateMachine::StreamNameChanged",
+ MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+
+ mMediaSink->SetStreamName(mStreamName);
+}
+
+void MediaDecoderStateMachine::UpdateOutputCaptured() {
+ AUTO_PROFILER_LABEL("MediaDecoderStateMachine::UpdateOutputCaptured",
+ MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+ MOZ_ASSERT_IF(
+ mOutputCaptureState == MediaDecoder::OutputCaptureState::Capture,
+ mOutputDummyTrack.Ref());
+
+ // Reset these flags so they are consistent with the status of the sink.
+ // TODO: Move these flags into MediaSink to improve cohesion so we don't need
+ // to reset these flags when switching MediaSinks.
+ mAudioCompleted = false;
+ mVideoCompleted = false;
+
+ // Don't create a new media sink if we're still suspending media sink.
+ if (!mIsMediaSinkSuspended) {
+ const bool wasPlaying = IsPlaying();
+ // Stop and shut down the existing sink.
+ StopMediaSink();
+ mMediaSink->Shutdown();
+
+ // Create a new sink according to whether output is captured.
+ mMediaSink = CreateMediaSink();
+ if (wasPlaying) {
+ DebugOnly<nsresult> rv = StartMediaSink();
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+ }
+
+ // Don't buffer as much when audio is captured because we don't need to worry
+ // about high latency audio devices.
+ mAmpleAudioThreshold =
+ mOutputCaptureState != MediaDecoder::OutputCaptureState::None
+ ? detail::AMPLE_AUDIO_THRESHOLD / 2
+ : detail::AMPLE_AUDIO_THRESHOLD;
+
+ mStateObj->HandleAudioCaptured();
+}
+
+void MediaDecoderStateMachine::OutputTracksChanged() {
+ MOZ_ASSERT(OnTaskQueue());
+ LOG("OutputTracksChanged, tracks=%zu", mOutputTracks.Ref().Length());
+ mCanonicalOutputTracks = mOutputTracks;
+}
+
+void MediaDecoderStateMachine::OutputPrincipalChanged() {
+ MOZ_ASSERT(OnTaskQueue());
+ mCanonicalOutputPrincipal = mOutputPrincipal;
+}
+
+RefPtr<GenericPromise> MediaDecoderStateMachine::InvokeSetSink(
+ const RefPtr<AudioDeviceInfo>& aSink) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aSink);
+
+ return InvokeAsync(OwnerThread(), this, __func__,
+ &MediaDecoderStateMachine::SetSink, aSink);
+}
+
+RefPtr<GenericPromise> MediaDecoderStateMachine::SetSink(
+ const RefPtr<AudioDeviceInfo>& aDevice) {
+ MOZ_ASSERT(OnTaskQueue());
+ if (mIsMediaSinkSuspended) {
+ // Don't create a new media sink when suspended.
+ return GenericPromise::CreateAndResolve(false, __func__);
+ }
+
+ if (mOutputCaptureState != MediaDecoder::OutputCaptureState::None) {
+ // Not supported yet.
+ return GenericPromise::CreateAndReject(NS_ERROR_ABORT, __func__);
+ }
+
+ if (mSinkDevice.Ref() != aDevice) {
+ // A new sink was set before this ran.
+ return GenericPromise::CreateAndResolve(IsPlaying(), __func__);
+ }
+
+ if (mMediaSink->AudioDevice() == aDevice) {
+ // The sink has not changed.
+ return GenericPromise::CreateAndResolve(IsPlaying(), __func__);
+ }
+
+ const bool wasPlaying = IsPlaying();
+
+ // Stop and shutdown the existing sink.
+ StopMediaSink();
+ mMediaSink->Shutdown();
+ // Create a new sink according to whether audio is captured.
+ mMediaSink = CreateMediaSink();
+ // Start the new sink
+ if (wasPlaying) {
+ nsresult rv = StartMediaSink();
+ if (NS_FAILED(rv)) {
+ return GenericPromise::CreateAndReject(NS_ERROR_ABORT, __func__);
+ }
+ }
+ return GenericPromise::CreateAndResolve(wasPlaying, __func__);
+}
+
+void MediaDecoderStateMachine::InvokeSuspendMediaSink() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsresult rv = OwnerThread()->Dispatch(
+ NewRunnableMethod("MediaDecoderStateMachine::SuspendMediaSink", this,
+ &MediaDecoderStateMachine::SuspendMediaSink));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+}
+
+void MediaDecoderStateMachine::SuspendMediaSink() {
+ AUTO_PROFILER_LABEL("MediaDecoderStateMachine::SuspendMediaSink",
+ MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+ if (mIsMediaSinkSuspended) {
+ return;
+ }
+ LOG("SuspendMediaSink");
+ mIsMediaSinkSuspended = true;
+ StopMediaSink();
+ mMediaSink->Shutdown();
+}
+
+void MediaDecoderStateMachine::InvokeResumeMediaSink() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsresult rv = OwnerThread()->Dispatch(
+ NewRunnableMethod("MediaDecoderStateMachine::ResumeMediaSink", this,
+ &MediaDecoderStateMachine::ResumeMediaSink));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+}
+
+void MediaDecoderStateMachine::ResumeMediaSink() {
+ AUTO_PROFILER_LABEL("MediaDecoderStateMachine::ResumeMediaSink",
+ MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+ if (!mIsMediaSinkSuspended) {
+ return;
+ }
+ LOG("ResumeMediaSink");
+ mIsMediaSinkSuspended = false;
+ if (!mMediaSink->IsStarted()) {
+ mMediaSink = CreateMediaSink();
+ MaybeStartPlayback();
+ }
+}
+
+void MediaDecoderStateMachine::UpdateSecondaryVideoContainer() {
+ AUTO_PROFILER_LABEL("MediaDecoderStateMachine::UpdateSecondaryVideoContainer",
+ MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+ MOZ_DIAGNOSTIC_ASSERT(mMediaSink);
+ mMediaSink->SetSecondaryVideoContainer(mSecondaryVideoContainer.Ref());
+ mOnSecondaryVideoContainerInstalled.Notify(mSecondaryVideoContainer.Ref());
+}
+
+TimeUnit MediaDecoderStateMachine::AudioEndTime() const {
+ MOZ_ASSERT(OnTaskQueue());
+ if (mMediaSink->IsStarted()) {
+ return mMediaSink->GetEndTime(TrackInfo::kAudioTrack);
+ }
+ return GetMediaTime();
+}
+
+TimeUnit MediaDecoderStateMachine::VideoEndTime() const {
+ MOZ_ASSERT(OnTaskQueue());
+ if (mMediaSink->IsStarted()) {
+ return mMediaSink->GetEndTime(TrackInfo::kVideoTrack);
+ }
+ return GetMediaTime();
+}
+
+void MediaDecoderStateMachine::OnMediaSinkVideoComplete() {
+ MOZ_ASSERT(OnTaskQueue());
+ MOZ_ASSERT(HasVideo());
+ AUTO_PROFILER_LABEL("MediaDecoderStateMachine::OnMediaSinkVideoComplete",
+ MEDIA_PLAYBACK);
+ LOG("[%s]", __func__);
+
+ mMediaSinkVideoEndedPromise.Complete();
+ mVideoCompleted = true;
+ ScheduleStateMachine();
+}
+
+void MediaDecoderStateMachine::OnMediaSinkVideoError() {
+ MOZ_ASSERT(OnTaskQueue());
+ MOZ_ASSERT(HasVideo());
+ AUTO_PROFILER_LABEL("MediaDecoderStateMachine::OnMediaSinkVideoError",
+ MEDIA_PLAYBACK);
+ LOGE("[%s]", __func__);
+
+ mMediaSinkVideoEndedPromise.Complete();
+ mVideoCompleted = true;
+ if (HasAudio()) {
+ return;
+ }
+ DecodeError(MediaResult(NS_ERROR_DOM_MEDIA_MEDIASINK_ERR, __func__));
+}
+
+void MediaDecoderStateMachine::OnMediaSinkAudioComplete() {
+ MOZ_ASSERT(OnTaskQueue());
+ MOZ_ASSERT(HasAudio());
+ AUTO_PROFILER_LABEL("MediaDecoderStateMachine::OnMediaSinkAudioComplete",
+ MEDIA_PLAYBACK);
+ LOG("[%s]", __func__);
+
+ mMediaSinkAudioEndedPromise.Complete();
+ mAudioCompleted = true;
+ // To notify PlaybackEnded as soon as possible.
+ ScheduleStateMachine();
+
+ // Report OK to Decoder Doctor (to know if issue may have been resolved).
+ mOnDecoderDoctorEvent.Notify(
+ DecoderDoctorEvent{DecoderDoctorEvent::eAudioSinkStartup, NS_OK});
+}
+
+void MediaDecoderStateMachine::OnMediaSinkAudioError(nsresult aResult) {
+ MOZ_ASSERT(OnTaskQueue());
+ MOZ_ASSERT(HasAudio());
+ AUTO_PROFILER_LABEL("MediaDecoderStateMachine::OnMediaSinkAudioError",
+ MEDIA_PLAYBACK);
+ LOGE("[%s]", __func__);
+
+ mMediaSinkAudioEndedPromise.Complete();
+ mAudioCompleted = true;
+
+ // Result should never be NS_OK in this *error* handler. Report to Dec-Doc.
+ MOZ_ASSERT(NS_FAILED(aResult));
+ mOnDecoderDoctorEvent.Notify(
+ DecoderDoctorEvent{DecoderDoctorEvent::eAudioSinkStartup, aResult});
+
+ // Make the best effort to continue playback when there is video.
+ if (HasVideo()) {
+ return;
+ }
+
+ // Otherwise notify media decoder/element about this error for it makes
+ // no sense to play an audio-only file without sound output.
+ DecodeError(MediaResult(NS_ERROR_DOM_MEDIA_MEDIASINK_ERR, __func__));
+}
+
+uint32_t MediaDecoderStateMachine::GetAmpleVideoFrames() const {
+ MOZ_ASSERT(OnTaskQueue());
+ return mReader->VideoIsHardwareAccelerated()
+ ? std::max<uint32_t>(sVideoQueueHWAccelSize, MIN_VIDEO_QUEUE_SIZE)
+ : std::max<uint32_t>(sVideoQueueDefaultSize, MIN_VIDEO_QUEUE_SIZE);
+}
+
+void MediaDecoderStateMachine::GetDebugInfo(
+ dom::MediaDecoderStateMachineDebugInfo& aInfo) {
+ MOZ_ASSERT(OnTaskQueue());
+ aInfo.mDuration =
+ mDuration.Ref() ? mDuration.Ref().ref().ToMicroseconds() : -1;
+ aInfo.mMediaTime = GetMediaTime().ToMicroseconds();
+ aInfo.mClock = mMediaSink->IsStarted() ? GetClock().ToMicroseconds() : -1;
+ aInfo.mPlayState = int32_t(mPlayState.Ref());
+ aInfo.mSentFirstFrameLoadedEvent = mSentFirstFrameLoadedEvent;
+ aInfo.mIsPlaying = IsPlaying();
+ CopyUTF8toUTF16(MakeStringSpan(AudioRequestStatus()),
+ aInfo.mAudioRequestStatus);
+ CopyUTF8toUTF16(MakeStringSpan(VideoRequestStatus()),
+ aInfo.mVideoRequestStatus);
+ aInfo.mDecodedAudioEndTime = mDecodedAudioEndTime.ToMicroseconds();
+ aInfo.mDecodedVideoEndTime = mDecodedVideoEndTime.ToMicroseconds();
+ aInfo.mAudioCompleted = mAudioCompleted;
+ aInfo.mVideoCompleted = mVideoCompleted;
+ mStateObj->GetDebugInfo(aInfo.mStateObj);
+ mMediaSink->GetDebugInfo(aInfo.mMediaSink);
+}
+
+RefPtr<GenericPromise> MediaDecoderStateMachine::RequestDebugInfo(
+ dom::MediaDecoderStateMachineDebugInfo& aInfo) {
+ RefPtr<GenericPromise::Private> p = new GenericPromise::Private(__func__);
+ RefPtr<MediaDecoderStateMachine> self = this;
+ nsresult rv = OwnerThread()->Dispatch(
+ NS_NewRunnableFunction("MediaDecoderStateMachine::RequestDebugInfo",
+ [self, p, &aInfo]() {
+ self->GetDebugInfo(aInfo);
+ p->Resolve(true, __func__);
+ }),
+ AbstractThread::TailDispatch);
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ return p;
+}
+
+class VideoQueueMemoryFunctor : public nsDequeFunctor<VideoData> {
+ public:
+ VideoQueueMemoryFunctor() : mSize(0) {}
+
+ MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf);
+
+ virtual void operator()(VideoData* aObject) override {
+ mSize += aObject->SizeOfIncludingThis(MallocSizeOf);
+ }
+
+ size_t mSize;
+};
+
+class AudioQueueMemoryFunctor : public nsDequeFunctor<AudioData> {
+ public:
+ AudioQueueMemoryFunctor() : mSize(0) {}
+
+ MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf);
+
+ virtual void operator()(AudioData* aObject) override {
+ mSize += aObject->SizeOfIncludingThis(MallocSizeOf);
+ }
+
+ size_t mSize;
+};
+
+size_t MediaDecoderStateMachine::SizeOfVideoQueue() const {
+ VideoQueueMemoryFunctor functor;
+ mVideoQueue.LockedForEach(functor);
+ return functor.mSize;
+}
+
+size_t MediaDecoderStateMachine::SizeOfAudioQueue() const {
+ AudioQueueMemoryFunctor functor;
+ mAudioQueue.LockedForEach(functor);
+ return functor.mSize;
+}
+
+const char* MediaDecoderStateMachine::AudioRequestStatus() const {
+ MOZ_ASSERT(OnTaskQueue());
+ if (IsRequestingAudioData()) {
+ MOZ_DIAGNOSTIC_ASSERT(!IsWaitingAudioData());
+ return "pending";
+ }
+
+ if (IsWaitingAudioData()) {
+ return "waiting";
+ }
+ return "idle";
+}
+
+const char* MediaDecoderStateMachine::VideoRequestStatus() const {
+ MOZ_ASSERT(OnTaskQueue());
+ if (IsRequestingVideoData()) {
+ MOZ_DIAGNOSTIC_ASSERT(!IsWaitingVideoData());
+ return "pending";
+ }
+
+ if (IsWaitingVideoData()) {
+ return "waiting";
+ }
+ return "idle";
+}
+
+void MediaDecoderStateMachine::OnSuspendTimerResolved() {
+ LOG("OnSuspendTimerResolved");
+ mVideoDecodeSuspendTimer.CompleteRequest();
+ mStateObj->HandleVideoSuspendTimeout();
+}
+
+void MediaDecoderStateMachine::CancelSuspendTimer() {
+ LOG("CancelSuspendTimer: State: %s, Timer.IsScheduled: %c",
+ ToStateStr(mStateObj->GetState()),
+ mVideoDecodeSuspendTimer.IsScheduled() ? 'T' : 'F');
+ MOZ_ASSERT(OnTaskQueue());
+ if (mVideoDecodeSuspendTimer.IsScheduled()) {
+ mOnPlaybackEvent.Notify(MediaPlaybackEvent::CancelVideoSuspendTimer);
+ }
+ mVideoDecodeSuspendTimer.Reset();
+}
+
+void MediaDecoderStateMachine::AdjustByLooping(media::TimeUnit& aTime) const {
+ MOZ_ASSERT(OnTaskQueue());
+
+ // No need to adjust time.
+ if (mOriginalDecodedDuration == media::TimeUnit::Zero()) {
+ return;
+ }
+
+ // There are situations where we need to perform subtraction instead of modulo
+ // to accurately adjust the clock. When we are not in a state of seamless
+ // looping, it is usually necessary to normalize the clock time within the
+ // range of [0, duration]. However, if the current clock time is greater than
+ // the duration (i.e., duration+1) and not in looping, we should not adjust it
+ // to 1 as we are not looping back to the starting position. Instead, we
+ // should leave the clock time unchanged and trim it later to match the
+ // maximum duration time.
+ if (mStateObj->GetState() != DECODER_STATE_LOOPING_DECODING) {
+ // Use the smaller offset rather than the larger one, as the larger offset
+ // indicates the next round of looping. For example, if the duration is X
+ // and the playback is currently in the third round of looping, both
+ // queues will have an offset of 3X. However, if the audio decoding is
+ // faster and the fourth round of data has already been added to the audio
+ // queue, the audio offset will become 4X. Since playback is still in the
+ // third round, we should use the smaller offset of 3X to adjust the time.
+ TimeUnit offset = TimeUnit::FromInfinity();
+ if (HasAudio()) {
+ offset = std::min(AudioQueue().GetOffset(), offset);
+ }
+ if (HasVideo()) {
+ offset = std::min(VideoQueue().GetOffset(), offset);
+ }
+ if (aTime > offset) {
+ aTime -= offset;
+ return;
+ }
+ }
+
+ // When seamless looping happens at least once, it doesn't matter if we're
+ // looping or not.
+ aTime = aTime % mOriginalDecodedDuration;
+}
+
+bool MediaDecoderStateMachine::IsInSeamlessLooping() const {
+ return mLooping && mSeamlessLoopingAllowed;
+}
+
+bool MediaDecoderStateMachine::HasLastDecodedData(MediaData::Type aType) {
+ MOZ_DIAGNOSTIC_ASSERT(aType == MediaData::Type::AUDIO_DATA ||
+ aType == MediaData::Type::VIDEO_DATA);
+ if (aType == MediaData::Type::AUDIO_DATA) {
+ return mDecodedAudioEndTime != TimeUnit::Zero();
+ }
+ return mDecodedVideoEndTime != TimeUnit::Zero();
+}
+
+bool MediaDecoderStateMachine::IsCDMProxySupported(CDMProxy* aProxy) {
+#ifdef MOZ_WMF_CDM
+ MOZ_ASSERT(aProxy);
+ // This proxy only works with the external state machine.
+ return !aProxy->AsWMFCDMProxy();
+#else
+ return true;
+#endif
+}
+
+} // namespace mozilla
+
+// avoid redefined macro in unified build
+#undef LOG
+#undef LOGV
+#undef LOGW
+#undef LOGE
+#undef SLOGW
+#undef SLOGE
+#undef NS_DispatchToMainThread
diff --git a/dom/media/MediaDecoderStateMachine.h b/dom/media/MediaDecoderStateMachine.h
new file mode 100644
index 0000000000..758feb7539
--- /dev/null
+++ b/dom/media/MediaDecoderStateMachine.h
@@ -0,0 +1,586 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#if !defined(MediaDecoderStateMachine_h__)
+# define MediaDecoderStateMachine_h__
+
+# include "AudioDeviceInfo.h"
+# include "ImageContainer.h"
+# include "MediaDecoder.h"
+# include "MediaDecoderOwner.h"
+# include "MediaDecoderStateMachineBase.h"
+# include "MediaFormatReader.h"
+# include "MediaQueue.h"
+# include "MediaSink.h"
+# include "MediaStatistics.h"
+# include "MediaTimer.h"
+# include "SeekJob.h"
+# include "mozilla/Attributes.h"
+# include "mozilla/ReentrantMonitor.h"
+# include "mozilla/StateMirroring.h"
+# include "nsThreadUtils.h"
+
+namespace mozilla {
+
+class AbstractThread;
+class AudioSegment;
+class DecodedStream;
+class DOMMediaStream;
+class ReaderProxy;
+class TaskQueue;
+
+extern LazyLogModule gMediaDecoderLog;
+
+DDLoggedTypeDeclName(MediaDecoderStateMachine);
+
+/*
+
+Each media element for a media file has one thread called the "audio thread".
+
+The audio thread writes the decoded audio data to the audio
+hardware. This is done in a separate thread to ensure that the
+audio hardware gets a constant stream of data without
+interruption due to decoding or display. At some point
+AudioStream will be refactored to have a callback interface
+where it asks for data and this thread will no longer be
+needed.
+
+The element/state machine also has a TaskQueue which runs in a
+SharedThreadPool that is shared with all other elements/decoders. The state
+machine dispatches tasks to this to call into the MediaDecoderReader to
+request decoded audio or video data. The Reader will callback with decoded
+sampled when it has them available, and the state machine places the decoded
+samples into its queues for the consuming threads to pull from.
+
+The MediaDecoderReader can choose to decode asynchronously, or synchronously
+and return requested samples synchronously inside it's Request*Data()
+functions via callback. Asynchronous decoding is preferred, and should be
+used for any new readers.
+
+Synchronisation of state between the thread is done via a monitor owned
+by MediaDecoder.
+
+The lifetime of the audio thread is controlled by the state machine when
+it runs on the shared state machine thread. When playback needs to occur
+the audio thread is created and an event dispatched to run it. The audio
+thread exits when audio playback is completed or no longer required.
+
+A/V synchronisation is handled by the state machine. It examines the audio
+playback time and compares this to the next frame in the queue of video
+frames. If it is time to play the video frame it is then displayed, otherwise
+it schedules the state machine to run again at the time of the next frame.
+
+Frame skipping is done in the following ways:
+
+ 1) The state machine will skip all frames in the video queue whose
+ display time is less than the current audio time. This ensures
+ the correct frame for the current time is always displayed.
+
+ 2) The decode tasks will stop decoding interframes and read to the
+ next keyframe if it determines that decoding the remaining
+ interframes will cause playback issues. It detects this by:
+ a) If the amount of audio data in the audio queue drops
+ below a threshold whereby audio may start to skip.
+ b) If the video queue drops below a threshold where it
+ will be decoding video data that won't be displayed due
+ to the decode thread dropping the frame immediately.
+ TODO: In future we should only do this when the Reader is decoding
+ synchronously.
+
+When hardware accelerated graphics is not available, YCbCr conversion
+is done on the decode task queue when video frames are decoded.
+
+The decode task queue pushes decoded audio and videos frames into two
+separate queues - one for audio and one for video. These are kept
+separate to make it easy to constantly feed audio data to the audio
+hardware while allowing frame skipping of video data. These queues are
+threadsafe, and neither the decode, audio, or state machine should
+be able to monopolize them, and cause starvation of the other threads.
+
+Both queues are bounded by a maximum size. When this size is reached
+the decode tasks will no longer request video or audio depending on the
+queue that has reached the threshold. If both queues are full, no more
+decode tasks will be dispatched to the decode task queue, so other
+decoders will have an opportunity to run.
+
+During playback the audio thread will be idle (via a Wait() on the
+monitor) if the audio queue is empty. Otherwise it constantly pops
+audio data off the queue and plays it with a blocking write to the audio
+hardware (via AudioStream).
+
+*/
+class MediaDecoderStateMachine
+ : public MediaDecoderStateMachineBase,
+ public DecoderDoctorLifeLogger<MediaDecoderStateMachine> {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaDecoderStateMachine, override)
+
+ using TrackSet = MediaFormatReader::TrackSet;
+
+ public:
+ using FrameID = mozilla::layers::ImageContainer::FrameID;
+ MediaDecoderStateMachine(MediaDecoder* aDecoder, MediaFormatReader* aReader);
+
+ nsresult Init(MediaDecoder* aDecoder) override;
+
+ // Enumeration for the valid decoding states
+ enum State {
+ DECODER_STATE_DECODING_METADATA,
+ DECODER_STATE_DORMANT,
+ DECODER_STATE_DECODING_FIRSTFRAME,
+ DECODER_STATE_DECODING,
+ DECODER_STATE_LOOPING_DECODING,
+ DECODER_STATE_SEEKING_ACCURATE,
+ DECODER_STATE_SEEKING_FROMDORMANT,
+ DECODER_STATE_SEEKING_NEXTFRAMESEEKING,
+ DECODER_STATE_SEEKING_VIDEOONLY,
+ DECODER_STATE_BUFFERING,
+ DECODER_STATE_COMPLETED,
+ DECODER_STATE_SHUTDOWN
+ };
+
+ RefPtr<GenericPromise> RequestDebugInfo(
+ dom::MediaDecoderStateMachineDebugInfo& aInfo) override;
+
+ size_t SizeOfVideoQueue() const override;
+
+ size_t SizeOfAudioQueue() const override;
+
+ // Sets the video decode mode. Used by the suspend-video-decoder feature.
+ void SetVideoDecodeMode(VideoDecodeMode aMode) override;
+
+ RefPtr<GenericPromise> InvokeSetSink(
+ const RefPtr<AudioDeviceInfo>& aSink) override;
+
+ void InvokeSuspendMediaSink() override;
+ void InvokeResumeMediaSink() override;
+
+ bool IsCDMProxySupported(CDMProxy* aProxy) override;
+
+ private:
+ class StateObject;
+ class DecodeMetadataState;
+ class DormantState;
+ class DecodingFirstFrameState;
+ class DecodingState;
+ class LoopingDecodingState;
+ class SeekingState;
+ class AccurateSeekingState;
+ class NextFrameSeekingState;
+ class NextFrameSeekingFromDormantState;
+ class VideoOnlySeekingState;
+ class BufferingState;
+ class CompletedState;
+ class ShutdownState;
+
+ static const char* ToStateStr(State aState);
+ const char* ToStateStr();
+
+ void GetDebugInfo(dom::MediaDecoderStateMachineDebugInfo& aInfo);
+
+ // Initialization that needs to happen on the task queue. This is the first
+ // task that gets run on the task queue, and is dispatched from the MDSM
+ // constructor immediately after the task queue is created.
+ void InitializationTask(MediaDecoder* aDecoder) override;
+
+ RefPtr<MediaDecoder::SeekPromise> Seek(const SeekTarget& aTarget) override;
+
+ RefPtr<ShutdownPromise> Shutdown() override;
+
+ RefPtr<ShutdownPromise> FinishShutdown();
+
+ // Update the playback position. This can result in a timeupdate event
+ // and an invalidate of the frame being dispatched asynchronously if
+ // there is no such event currently queued.
+ // Only called on the decoder thread. Must be called with
+ // the decode monitor held.
+ void UpdatePlaybackPosition(const media::TimeUnit& aTime);
+
+ // Schedules the shared state machine thread to run the state machine.
+ void ScheduleStateMachine();
+
+ // Invokes ScheduleStateMachine to run in |aTime|,
+ // unless it's already scheduled to run earlier, in which case the
+ // request is discarded.
+ void ScheduleStateMachineIn(const media::TimeUnit& aTime);
+
+ bool HaveEnoughDecodedAudio() const;
+ bool HaveEnoughDecodedVideo() const;
+
+ // The check is used to store more video frames than usual when playing 4K+
+ // video.
+ bool IsVideoDataEnoughComparedWithAudio() const;
+
+ // Returns true if we're currently playing. The decoder monitor must
+ // be held.
+ bool IsPlaying() const;
+
+ // Sets mMediaSeekable to false.
+ void SetMediaNotSeekable();
+
+ // Resets all states related to decoding and aborts all pending requests
+ // to the decoders.
+ void ResetDecode(const TrackSet& aTracks = TrackSet(TrackInfo::kAudioTrack,
+ TrackInfo::kVideoTrack));
+
+ void SetVideoDecodeModeInternal(VideoDecodeMode aMode);
+
+ // Set new sink device and restart MediaSink if playback is started.
+ // Returned promise will be resolved with true if the playback is
+ // started and false if playback is stopped after setting the new sink.
+ // Returned promise will be rejected with value NS_ERROR_ABORT
+ // if the action fails or it is not supported.
+ // If there are multiple pending requests only the last one will be
+ // executed, for all previous requests the promise will be resolved
+ // with true or false similar to above.
+ RefPtr<GenericPromise> SetSink(const RefPtr<AudioDeviceInfo>& aDevice);
+
+ // Shutdown MediaSink on suspend to clean up resources.
+ void SuspendMediaSink();
+ // Create a new MediaSink, it must have been stopped first.
+ void ResumeMediaSink();
+
+ protected:
+ virtual ~MediaDecoderStateMachine();
+
+ void BufferedRangeUpdated() override;
+ void VolumeChanged() override;
+ void PreservesPitchChanged() override;
+ void PlayStateChanged() override;
+ void LoopingChanged() override;
+ void UpdateSecondaryVideoContainer() override;
+
+ void ReaderSuspendedChanged();
+
+ // Inserts a sample into the Audio/Video queue.
+ // aSample must not be null.
+ void PushAudio(AudioData* aSample);
+ void PushVideo(VideoData* aSample);
+
+ void OnAudioPopped(const RefPtr<AudioData>& aSample);
+ void OnVideoPopped(const RefPtr<VideoData>& aSample);
+
+ void AudioAudibleChanged(bool aAudible);
+
+ void SetPlaybackRate(double aPlaybackRate) override;
+ void SetIsLiveStream(bool aIsLiveStream) override {
+ mIsLiveStream = aIsLiveStream;
+ }
+ void SetCanPlayThrough(bool aCanPlayThrough) override {
+ mCanPlayThrough = aCanPlayThrough;
+ }
+ void SetFragmentEndTime(const media::TimeUnit& aEndTime) override {
+ // A negative number means we don't have a fragment end time at all.
+ mFragmentEndTime = aEndTime >= media::TimeUnit::Zero()
+ ? aEndTime
+ : media::TimeUnit::Invalid();
+ }
+
+ void StreamNameChanged();
+ void UpdateOutputCaptured();
+ void OutputTracksChanged();
+ void OutputPrincipalChanged();
+
+ MediaQueue<AudioData>& AudioQueue() { return mAudioQueue; }
+ MediaQueue<VideoData>& VideoQueue() { return mVideoQueue; }
+
+ const MediaQueue<AudioData>& AudioQueue() const { return mAudioQueue; }
+ const MediaQueue<VideoData>& VideoQueue() const { return mVideoQueue; }
+
+ // True if we are low in decoded audio/video data.
+ // May not be invoked when mReader->UseBufferingHeuristics() is false.
+ bool HasLowDecodedData();
+
+ bool HasLowDecodedAudio();
+
+ bool HasLowDecodedVideo();
+
+ bool OutOfDecodedAudio();
+
+ bool OutOfDecodedVideo() {
+ MOZ_ASSERT(OnTaskQueue());
+ return IsVideoDecoding() && VideoQueue().GetSize() <= 1;
+ }
+
+ // Returns true if we're running low on buffered data.
+ bool HasLowBufferedData();
+
+ // Returns true if we have less than aThreshold of buffered data available.
+ bool HasLowBufferedData(const media::TimeUnit& aThreshold);
+
+ // Return the current time, either the audio clock if available (if the media
+ // has audio, and the playback is possible), or a clock for the video.
+ // Called on the state machine thread.
+ // If aTimeStamp is non-null, set *aTimeStamp to the TimeStamp corresponding
+ // to the returned stream time.
+ media::TimeUnit GetClock(TimeStamp* aTimeStamp = nullptr) const;
+
+ // Update only the state machine's current playback position (and duration,
+ // if unknown). Does not update the playback position on the decoder or
+ // media element -- use UpdatePlaybackPosition for that. Called on the state
+ // machine thread, caller must hold the decoder lock.
+ void UpdatePlaybackPositionInternal(const media::TimeUnit& aTime);
+
+ // Update playback position and trigger next update by default time period.
+ // Called on the state machine thread.
+ void UpdatePlaybackPositionPeriodically();
+
+ MediaSink* CreateAudioSink();
+
+ // Always create mediasink which contains an AudioSink or DecodedStream
+ // inside.
+ already_AddRefed<MediaSink> CreateMediaSink();
+
+ // Stops the media sink and shut it down.
+ // The decoder monitor must be held with exactly one lock count.
+ // Called on the state machine thread.
+ void StopMediaSink();
+
+ // Create and start the media sink.
+ // The decoder monitor must be held with exactly one lock count.
+ // Called on the state machine thread.
+ // If start fails an NS_ERROR_FAILURE is returned.
+ nsresult StartMediaSink();
+
+ // Notification method invoked when mIsVisible changes.
+ void VisibilityChanged();
+
+ // Sets internal state which causes playback of media to pause.
+ // The decoder monitor must be held.
+ void StopPlayback();
+
+ // If the conditions are right, sets internal state which causes playback
+ // of media to begin or resume.
+ // Must be called with the decode monitor held.
+ void MaybeStartPlayback();
+
+ void EnqueueFirstFrameLoadedEvent();
+
+ // Start a task to decode audio.
+ void RequestAudioData();
+
+ // Start a task to decode video.
+ // @param aRequestNextVideoKeyFrame
+ // If aRequestNextKeyFrame is true, will request data for the next keyframe
+ // after aCurrentTime.
+ void RequestVideoData(const media::TimeUnit& aCurrentTime,
+ bool aRequestNextKeyFrame = false);
+
+ void WaitForData(MediaData::Type aType);
+
+ // Returns the "media time". This is the absolute time which the media
+ // playback has reached. i.e. this returns values in the range
+ // [mStartTime, mEndTime], and mStartTime will not be 0 if the media does
+ // not start at 0. Note this is different than the "current playback
+ // position", which is in the range [0,duration].
+ media::TimeUnit GetMediaTime() const {
+ MOZ_ASSERT(OnTaskQueue());
+ return mCurrentPosition;
+ }
+
+ // Returns an upper bound on the number of microseconds of audio that is
+ // decoded and playable. This is the sum of the number of usecs of audio which
+ // is decoded and in the reader's audio queue, and the usecs of unplayed audio
+ // which has been pushed to the audio hardware for playback. Note that after
+ // calling this, the audio hardware may play some of the audio pushed to
+ // hardware, so this can only be used as a upper bound. The decoder monitor
+ // must be held when calling this. Called on the decode thread.
+ media::TimeUnit GetDecodedAudioDuration() const;
+
+ void FinishDecodeFirstFrame();
+
+ // Performs one "cycle" of the state machine.
+ void RunStateMachine();
+
+ bool IsStateMachineScheduled() const;
+
+ // These return true if the respective stream's decode has not yet reached
+ // the end of stream.
+ bool IsAudioDecoding();
+ bool IsVideoDecoding();
+
+ private:
+ // Resolved by the MediaSink to signal that all audio/video outstanding
+ // work is complete and identify which part(a/v) of the sink is shutting down.
+ void OnMediaSinkAudioComplete();
+ void OnMediaSinkVideoComplete();
+
+ // Rejected by the MediaSink to signal errors for audio/video.
+ void OnMediaSinkAudioError(nsresult aResult);
+ void OnMediaSinkVideoError();
+
+ // State-watching manager.
+ WatchManager<MediaDecoderStateMachine> mWatchManager;
+
+ // True if we've dispatched a task to run the state machine but the task has
+ // yet to run.
+ bool mDispatchedStateMachine;
+
+ // Used to dispatch another round schedule with specific target time.
+ DelayedScheduler mDelayedScheduler;
+
+ // Queue of audio frames. This queue is threadsafe, and is accessed from
+ // the audio, decoder, state machine, and main threads.
+ MediaQueue<AudioData> mAudioQueue;
+ // Queue of video frames. This queue is threadsafe, and is accessed from
+ // the decoder, state machine, and main threads.
+ MediaQueue<VideoData> mVideoQueue;
+
+ UniquePtr<StateObject> mStateObj;
+
+ media::TimeUnit Duration() const {
+ MOZ_ASSERT(OnTaskQueue());
+ return mDuration.Ref().ref();
+ }
+
+ // FrameID which increments every time a frame is pushed to our queue.
+ FrameID mCurrentFrameID;
+
+ // Media Fragment end time.
+ media::TimeUnit mFragmentEndTime = media::TimeUnit::Invalid();
+
+ // The media sink resource. Used on the state machine thread.
+ RefPtr<MediaSink> mMediaSink;
+
+ // The end time of the last audio frame that's been pushed onto the media sink
+ // in microseconds. This will approximately be the end time
+ // of the audio stream, unless another frame is pushed to the hardware.
+ media::TimeUnit AudioEndTime() const;
+
+ // The end time of the last rendered video frame that's been sent to
+ // compositor.
+ media::TimeUnit VideoEndTime() const;
+
+ // The end time of the last decoded audio frame. This signifies the end of
+ // decoded audio data. Used to check if we are low in decoded data.
+ media::TimeUnit mDecodedAudioEndTime;
+
+ // The end time of the last decoded video frame. Used to check if we are low
+ // on decoded video data.
+ media::TimeUnit mDecodedVideoEndTime;
+
+ // If we've got more than this number of decoded video frames waiting in
+ // the video queue, we will not decode any more video frames until some have
+ // been consumed by the play state machine thread.
+ // Must hold monitor.
+ uint32_t GetAmpleVideoFrames() const;
+
+ // Our "ample" audio threshold. Once we've this much audio decoded, we
+ // pause decoding.
+ media::TimeUnit mAmpleAudioThreshold;
+
+ const char* AudioRequestStatus() const;
+ const char* VideoRequestStatus() const;
+
+ void OnSuspendTimerResolved();
+ void CancelSuspendTimer();
+
+ bool IsInSeamlessLooping() const;
+
+ bool mCanPlayThrough = false;
+
+ bool mIsLiveStream = false;
+
+ // True if all audio frames are already rendered.
+ bool mAudioCompleted = false;
+
+ // True if all video frames are already rendered.
+ bool mVideoCompleted = false;
+
+ // True if video decoding is suspended.
+ bool mVideoDecodeSuspended;
+
+ // Track enabling video decode suspension via timer
+ DelayedScheduler mVideoDecodeSuspendTimer;
+
+ // Track the current video decode mode.
+ VideoDecodeMode mVideoDecodeMode;
+
+ // Track the complete & error for audio/video separately
+ MozPromiseRequestHolder<MediaSink::EndedPromise> mMediaSinkAudioEndedPromise;
+ MozPromiseRequestHolder<MediaSink::EndedPromise> mMediaSinkVideoEndedPromise;
+
+ MediaEventListener mAudioQueueListener;
+ MediaEventListener mVideoQueueListener;
+ MediaEventListener mAudibleListener;
+ MediaEventListener mOnMediaNotSeekable;
+
+ const bool mIsMSE;
+
+ const bool mShouldResistFingerprinting;
+
+ bool mSeamlessLoopingAllowed;
+
+ // If media was in looping and had reached to the end before, then we need
+ // to adjust sample time from clock time to media time.
+ void AdjustByLooping(media::TimeUnit& aTime) const;
+
+ // These are used for seamless looping. When looping has been enable at least
+ // once, `mOriginalDecodedDuration` would be set to the larger duration
+ // between two tracks.
+ media::TimeUnit mOriginalDecodedDuration;
+ Maybe<media::TimeUnit> mAudioTrackDecodedDuration;
+ Maybe<media::TimeUnit> mVideoTrackDecodedDuration;
+
+ bool HasLastDecodedData(MediaData::Type aType);
+
+ // Current playback position in the stream in bytes.
+ int64_t mPlaybackOffset = 0;
+
+ // For seamless looping video, we don't want to trigger skip-to-next-keyframe
+ // after reaching video EOS. Because we've reset the demuxer to 0, and are
+ // going to request data from start. If playback hasn't looped back, the media
+ // time would still be too large, which makes the reader think the playback is
+ // way behind and performs unnecessary skipping. Eg. Media is 10s long,
+ // reaching EOS at 8s, requesting data at 9s. Assume media's keyframe interval
+ // is 3s, which means keyframes will appear on 0s, 3s, 6s and 9s. If we use
+ // current time as a threshold, the reader sees the next key frame is 3s but
+ // the threashold is 9s, which usually happens when the decoding is too slow.
+ // But that is not the case for us, we should by pass thskip-to-next-keyframe
+ // logic until the media loops back.
+ bool mBypassingSkipToNextKeyFrameCheck = false;
+
+ private:
+ // Audio stream name
+ Mirror<nsAutoString> mStreamName;
+
+ // The device used with SetSink, or nullptr if no explicit device has been
+ // set.
+ Mirror<RefPtr<AudioDeviceInfo>> mSinkDevice;
+
+ // Whether all output should be captured into mOutputTracks, halted, or not
+ // captured.
+ Mirror<MediaDecoder::OutputCaptureState> mOutputCaptureState;
+
+ // A dummy track used to access the right MediaTrackGraph instance. Needed
+ // since there's no guarantee that output tracks are present.
+ Mirror<nsMainThreadPtrHandle<SharedDummyTrack>> mOutputDummyTrack;
+
+ // Tracks to capture data into.
+ Mirror<CopyableTArray<RefPtr<ProcessedMediaTrack>>> mOutputTracks;
+
+ // PrincipalHandle to feed with data captured into mOutputTracks.
+ Mirror<PrincipalHandle> mOutputPrincipal;
+
+ Canonical<CopyableTArray<RefPtr<ProcessedMediaTrack>>> mCanonicalOutputTracks;
+ Canonical<PrincipalHandle> mCanonicalOutputPrincipal;
+
+ // Track when MediaSink is supsended. When that happens some actions are
+ // restricted like starting the sink or changing sink id. The flag is valid
+ // after Initialization. TaskQueue thread only.
+ bool mIsMediaSinkSuspended = false;
+
+ public:
+ AbstractCanonical<CopyableTArray<RefPtr<ProcessedMediaTrack>>>*
+ CanonicalOutputTracks() {
+ return &mCanonicalOutputTracks;
+ }
+ AbstractCanonical<PrincipalHandle>* CanonicalOutputPrincipal() {
+ return &mCanonicalOutputPrincipal;
+ }
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/MediaDecoderStateMachineBase.cpp b/dom/media/MediaDecoderStateMachineBase.cpp
new file mode 100644
index 0000000000..081ed09e6b
--- /dev/null
+++ b/dom/media/MediaDecoderStateMachineBase.cpp
@@ -0,0 +1,184 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaDecoderStateMachineBase.h"
+
+#include "MediaDecoder.h"
+#include "mozilla/ProfilerMarkers.h"
+#include "mozilla/TaskQueue.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+
+#define INIT_MIRROR(name, val) \
+ name(mTaskQueue, val, "MediaDecoderStateMachineBase::" #name " (Mirror)")
+#define INIT_CANONICAL(name, val) \
+ name(mTaskQueue, val, "MediaDecoderStateMachineBase::" #name " (Canonical)")
+#define FMT(x, ...) "Decoder=%p " x, mDecoderID, ##__VA_ARGS__
+#define LOG(x, ...) \
+ DDMOZ_LOG(gMediaDecoderLog, LogLevel::Debug, "Decoder=%p " x, mDecoderID, \
+ ##__VA_ARGS__)
+#define LOGV(x, ...) \
+ DDMOZ_LOG(gMediaDecoderLog, LogLevel::Verbose, "Decoder=%p " x, mDecoderID, \
+ ##__VA_ARGS__)
+#define LOGW(x, ...) NS_WARNING(nsPrintfCString(FMT(x, ##__VA_ARGS__)).get())
+#define LOGE(x, ...) \
+ NS_DebugBreak(NS_DEBUG_WARNING, \
+ nsPrintfCString(FMT(x, ##__VA_ARGS__)).get(), nullptr, \
+ __FILE__, __LINE__)
+
+MediaDecoderStateMachineBase::MediaDecoderStateMachineBase(
+ MediaDecoder* aDecoder, MediaFormatReader* aReader)
+ : mDecoderID(aDecoder),
+ mAbstractMainThread(aDecoder->AbstractMainThread()),
+ mFrameStats(&aDecoder->GetFrameStatistics()),
+ mVideoFrameContainer(aDecoder->GetVideoFrameContainer()),
+ mTaskQueue(TaskQueue::Create(GetMediaThreadPool(MediaThreadType::MDSM),
+ "MDSM::mTaskQueue",
+ /* aSupportsTailDispatch = */ true)),
+ mReader(new ReaderProxy(mTaskQueue, aReader)),
+ mPlaybackRate(1.0),
+ INIT_MIRROR(mBuffered, media::TimeIntervals()),
+ INIT_MIRROR(mPlayState, MediaDecoder::PLAY_STATE_LOADING),
+ INIT_MIRROR(mVolume, 1.0),
+ INIT_MIRROR(mPreservesPitch, true),
+ INIT_MIRROR(mLooping, false),
+ INIT_MIRROR(mSecondaryVideoContainer, nullptr),
+ INIT_CANONICAL(mDuration, media::NullableTimeUnit()),
+ INIT_CANONICAL(mCurrentPosition, media::TimeUnit::Zero()),
+ INIT_CANONICAL(mIsAudioDataAudible, false),
+ mMinimizePreroll(aDecoder->GetMinimizePreroll()),
+ mWatchManager(this, mTaskQueue) {}
+
+MediaEventSource<void>& MediaDecoderStateMachineBase::OnMediaNotSeekable()
+ const {
+ return mReader->OnMediaNotSeekable();
+}
+
+AbstractCanonical<media::TimeIntervals>*
+MediaDecoderStateMachineBase::CanonicalBuffered() const {
+ return mReader->CanonicalBuffered();
+}
+
+void MediaDecoderStateMachineBase::DispatchSetFragmentEndTime(
+ const media::TimeUnit& aEndTime) {
+ OwnerThread()->DispatchStateChange(NewRunnableMethod<media::TimeUnit>(
+ "MediaDecoderStateMachineBase::SetFragmentEndTime", this,
+ &MediaDecoderStateMachineBase::SetFragmentEndTime, aEndTime));
+}
+
+void MediaDecoderStateMachineBase::DispatchCanPlayThrough(
+ bool aCanPlayThrough) {
+ OwnerThread()->DispatchStateChange(NewRunnableMethod<bool>(
+ "MediaDecoderStateMachineBase::SetCanPlayThrough", this,
+ &MediaDecoderStateMachineBase::SetCanPlayThrough, aCanPlayThrough));
+}
+
+void MediaDecoderStateMachineBase::DispatchIsLiveStream(bool aIsLiveStream) {
+ OwnerThread()->DispatchStateChange(NewRunnableMethod<bool>(
+ "MediaDecoderStateMachineBase::SetIsLiveStream", this,
+ &MediaDecoderStateMachineBase::SetIsLiveStream, aIsLiveStream));
+}
+
+void MediaDecoderStateMachineBase::DispatchSetPlaybackRate(
+ double aPlaybackRate) {
+ OwnerThread()->DispatchStateChange(NewRunnableMethod<double>(
+ "MediaDecoderStateMachineBase::SetPlaybackRate", this,
+ &MediaDecoderStateMachineBase::SetPlaybackRate, aPlaybackRate));
+}
+
+nsresult MediaDecoderStateMachineBase::Init(MediaDecoder* aDecoder) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Dispatch initialization that needs to happen on that task queue.
+ nsCOMPtr<nsIRunnable> r = NewRunnableMethod<RefPtr<MediaDecoder>>(
+ "MediaDecoderStateMachineBase::InitializationTask", this,
+ &MediaDecoderStateMachineBase::InitializationTask, aDecoder);
+ mTaskQueue->DispatchStateChange(r.forget());
+ ;
+ nsresult rv = mReader->Init();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mMetadataManager.Connect(mReader->TimedMetadataEvent(), OwnerThread());
+ mReader->SetCanonicalDuration(&mDuration);
+
+ return NS_OK;
+}
+
+void MediaDecoderStateMachineBase::InitializationTask(MediaDecoder* aDecoder) {
+ MOZ_ASSERT(OnTaskQueue());
+
+ // Connect mirrors.
+ mBuffered.Connect(mReader->CanonicalBuffered());
+ mPlayState.Connect(aDecoder->CanonicalPlayState());
+ mVolume.Connect(aDecoder->CanonicalVolume());
+ mPreservesPitch.Connect(aDecoder->CanonicalPreservesPitch());
+ mLooping.Connect(aDecoder->CanonicalLooping());
+ mSecondaryVideoContainer.Connect(
+ aDecoder->CanonicalSecondaryVideoContainer());
+
+ // Initialize watchers.
+ mWatchManager.Watch(mBuffered,
+ &MediaDecoderStateMachineBase::BufferedRangeUpdated);
+ mWatchManager.Watch(mVolume, &MediaDecoderStateMachineBase::VolumeChanged);
+ mWatchManager.Watch(mPreservesPitch,
+ &MediaDecoderStateMachineBase::PreservesPitchChanged);
+ mWatchManager.Watch(mPlayState,
+ &MediaDecoderStateMachineBase::PlayStateChanged);
+ mWatchManager.Watch(mLooping, &MediaDecoderStateMachineBase::LoopingChanged);
+ mWatchManager.Watch(
+ mSecondaryVideoContainer,
+ &MediaDecoderStateMachineBase::UpdateSecondaryVideoContainer);
+}
+
+RefPtr<ShutdownPromise> MediaDecoderStateMachineBase::BeginShutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return InvokeAsync(
+ OwnerThread(), __func__,
+ [self = RefPtr<MediaDecoderStateMachineBase>(this), this]() {
+ mWatchManager.Shutdown();
+ mBuffered.DisconnectIfConnected();
+ mPlayState.DisconnectIfConnected();
+ mVolume.DisconnectIfConnected();
+ mPreservesPitch.DisconnectIfConnected();
+ mLooping.DisconnectIfConnected();
+ mSecondaryVideoContainer.DisconnectIfConnected();
+ return Shutdown();
+ });
+}
+
+RefPtr<MediaDecoder::SeekPromise> MediaDecoderStateMachineBase::InvokeSeek(
+ const SeekTarget& aTarget) {
+ return InvokeAsync(OwnerThread(), __func__,
+ [self = RefPtr<MediaDecoderStateMachineBase>(this),
+ target = aTarget]() { return self->Seek(target); });
+}
+
+bool MediaDecoderStateMachineBase::OnTaskQueue() const {
+ return OwnerThread()->IsCurrentThreadIn();
+}
+
+void MediaDecoderStateMachineBase::DecodeError(const MediaResult& aError) {
+ MOZ_ASSERT(OnTaskQueue());
+ LOGE("Decode error: %s", aError.Description().get());
+ PROFILER_MARKER_TEXT("MDSMBase::DecodeError", MEDIA_PLAYBACK, {},
+ aError.Description());
+ // Notify the decode error and MediaDecoder will shut down MDSM.
+ mOnPlaybackErrorEvent.Notify(aError);
+}
+
+RefPtr<SetCDMPromise> MediaDecoderStateMachineBase::SetCDMProxy(
+ CDMProxy* aProxy) {
+ return mReader->SetCDMProxy(aProxy);
+}
+
+#undef INIT_MIRROR
+#undef INIT_CANONICAL
+#undef FMT
+#undef LOG
+#undef LOGV
+#undef LOGW
+#undef LOGE
+
+} // namespace mozilla
diff --git a/dom/media/MediaDecoderStateMachineBase.h b/dom/media/MediaDecoderStateMachineBase.h
new file mode 100644
index 0000000000..5836b6f2f5
--- /dev/null
+++ b/dom/media/MediaDecoderStateMachineBase.h
@@ -0,0 +1,299 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_MEDIADECODERSTATEMACHINEBASE_H_
+#define DOM_MEDIA_MEDIADECODERSTATEMACHINEBASE_H_
+
+#include "DecoderDoctorDiagnostics.h"
+#include "MediaDecoder.h"
+#include "MediaDecoderOwner.h"
+#include "MediaEventSource.h"
+#include "MediaInfo.h"
+#include "MediaMetadataManager.h"
+#include "MediaPromiseDefs.h"
+#include "ReaderProxy.h"
+#include "VideoFrameContainer.h"
+#include "mozilla/dom/MediaDebugInfoBinding.h"
+#include "mozilla/Variant.h"
+#include "nsISupportsImpl.h"
+
+class AudioDeviceInfo;
+
+namespace mozilla {
+
+class AbstractThread;
+class CDMProxy;
+class FrameStatistics;
+class MediaFormatReader;
+class TaskQueue;
+
+struct MediaPlaybackEvent {
+ enum EventType {
+ PlaybackStarted,
+ PlaybackStopped,
+ PlaybackProgressed,
+ PlaybackEnded,
+ SeekStarted,
+ Invalidate,
+ EnterVideoSuspend,
+ ExitVideoSuspend,
+ StartVideoSuspendTimer,
+ CancelVideoSuspendTimer,
+ VideoOnlySeekBegin,
+ VideoOnlySeekCompleted,
+ } mType;
+
+ using DataType = Variant<Nothing, int64_t>;
+ DataType mData;
+
+ MOZ_IMPLICIT MediaPlaybackEvent(EventType aType)
+ : mType(aType), mData(Nothing{}) {}
+
+ template <typename T>
+ MediaPlaybackEvent(EventType aType, T&& aArg)
+ : mType(aType), mData(std::forward<T>(aArg)) {}
+};
+
+enum class VideoDecodeMode : uint8_t { Normal, Suspend };
+
+/**
+ * The state machine class. This manages the decoding and seeking in the
+ * MediaDecoderReader on the decode task queue, and A/V sync on the shared
+ * state machine thread, and controls the audio "push" thread.
+ *
+ * All internal state is synchronised via the decoder monitor. State changes
+ * are propagated by scheduling the state machine to run another cycle on the
+ * shared state machine thread.
+ */
+class MediaDecoderStateMachineBase {
+ public:
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+ using FirstFrameEventSourceExc =
+ MediaEventSourceExc<UniquePtr<MediaInfo>, MediaDecoderEventVisibility>;
+ using MetadataEventSourceExc =
+ MediaEventSourceExc<UniquePtr<MediaInfo>, UniquePtr<MetadataTags>,
+ MediaDecoderEventVisibility>;
+ using NextFrameStatus = MediaDecoderOwner::NextFrameStatus;
+
+ MediaDecoderStateMachineBase(MediaDecoder* aDecoder,
+ MediaFormatReader* aReader);
+
+ virtual nsresult Init(MediaDecoder* aDecoder);
+
+ RefPtr<ShutdownPromise> BeginShutdown();
+
+ // Seeks to the decoder to aTarget asynchronously.
+ RefPtr<MediaDecoder::SeekPromise> InvokeSeek(const SeekTarget& aTarget);
+
+ virtual size_t SizeOfVideoQueue() const = 0;
+ virtual size_t SizeOfAudioQueue() const = 0;
+
+ // Sets the video decode mode. Used by the suspend-video-decoder feature.
+ virtual void SetVideoDecodeMode(VideoDecodeMode aMode) = 0;
+
+ virtual RefPtr<GenericPromise> InvokeSetSink(
+ const RefPtr<AudioDeviceInfo>& aSink) = 0;
+ virtual void InvokeSuspendMediaSink() = 0;
+ virtual void InvokeResumeMediaSink() = 0;
+
+ virtual RefPtr<GenericPromise> RequestDebugInfo(
+ dom::MediaDecoderStateMachineDebugInfo& aInfo) = 0;
+
+ // Returns the state machine task queue.
+ TaskQueue* OwnerThread() const { return mTaskQueue; }
+
+ MetadataEventSourceExc& MetadataLoadedEvent() { return mMetadataLoadedEvent; }
+
+ FirstFrameEventSourceExc& FirstFrameLoadedEvent() {
+ return mFirstFrameLoadedEvent;
+ }
+
+ MediaEventSourceExc<RefPtr<VideoFrameContainer>>&
+ OnSecondaryVideoContainerInstalled() {
+ return mOnSecondaryVideoContainerInstalled;
+ }
+
+ TimedMetadataEventSource& TimedMetadataEvent() {
+ return mMetadataManager.TimedMetadataEvent();
+ }
+
+ MediaEventSource<MediaPlaybackEvent>& OnPlaybackEvent() {
+ return mOnPlaybackEvent;
+ }
+ MediaEventSource<MediaResult>& OnPlaybackErrorEvent() {
+ return mOnPlaybackErrorEvent;
+ }
+
+ MediaEventSource<DecoderDoctorEvent>& OnDecoderDoctorEvent() {
+ return mOnDecoderDoctorEvent;
+ }
+
+ MediaEventSource<NextFrameStatus>& OnNextFrameStatus() {
+ return mOnNextFrameStatus;
+ }
+
+ MediaEventProducer<VideoInfo, AudioInfo>& OnTrackInfoUpdatedEvent() {
+ return mReader->OnTrackInfoUpdatedEvent();
+ }
+
+ MediaEventSource<void>& OnMediaNotSeekable() const;
+
+ AbstractCanonical<media::NullableTimeUnit>* CanonicalDuration() {
+ return &mDuration;
+ }
+ AbstractCanonical<media::TimeUnit>* CanonicalCurrentPosition() {
+ return &mCurrentPosition;
+ }
+ AbstractCanonical<bool>* CanonicalIsAudioDataAudible() {
+ return &mIsAudioDataAudible;
+ }
+ AbstractCanonical<media::TimeIntervals>* CanonicalBuffered() const;
+
+ void DispatchSetFragmentEndTime(const media::TimeUnit& aEndTime);
+ void DispatchCanPlayThrough(bool aCanPlayThrough);
+ void DispatchIsLiveStream(bool aIsLiveStream);
+ void DispatchSetPlaybackRate(double aPlaybackRate);
+
+ virtual RefPtr<SetCDMPromise> SetCDMProxy(CDMProxy* aProxy);
+
+ virtual bool IsCDMProxySupported(CDMProxy* aProxy) = 0;
+
+ protected:
+ virtual ~MediaDecoderStateMachineBase() = default;
+
+ bool HasAudio() const { return mInfo.ref().HasAudio(); }
+ bool HasVideo() const { return mInfo.ref().HasVideo(); }
+ const MediaInfo& Info() const { return mInfo.ref(); }
+
+ virtual void SetPlaybackRate(double aPlaybackRate) = 0;
+ virtual void SetIsLiveStream(bool aIsLiveStream) = 0;
+ virtual void SetCanPlayThrough(bool aCanPlayThrough) = 0;
+ virtual void SetFragmentEndTime(const media::TimeUnit& aFragmentEndTime) = 0;
+
+ virtual void BufferedRangeUpdated() = 0;
+ virtual void VolumeChanged() = 0;
+ virtual void PreservesPitchChanged() = 0;
+ virtual void PlayStateChanged() = 0;
+ virtual void LoopingChanged() = 0;
+ virtual void UpdateSecondaryVideoContainer() = 0;
+
+ // Init tasks which should be done on the task queue.
+ virtual void InitializationTask(MediaDecoder* aDecoder);
+
+ virtual RefPtr<ShutdownPromise> Shutdown() = 0;
+
+ virtual RefPtr<MediaDecoder::SeekPromise> Seek(const SeekTarget& aTarget) = 0;
+
+ void DecodeError(const MediaResult& aError);
+
+ // Functions used by assertions to ensure we're calling things
+ // on the appropriate threads.
+ bool OnTaskQueue() const;
+
+ bool IsRequestingAudioData() const { return mAudioDataRequest.Exists(); }
+ bool IsRequestingVideoData() const { return mVideoDataRequest.Exists(); }
+ bool IsWaitingAudioData() const { return mAudioWaitRequest.Exists(); }
+ bool IsWaitingVideoData() const { return mVideoWaitRequest.Exists(); }
+
+ void* const mDecoderID;
+ const RefPtr<AbstractThread> mAbstractMainThread;
+ const RefPtr<FrameStatistics> mFrameStats;
+ const RefPtr<VideoFrameContainer> mVideoFrameContainer;
+ const RefPtr<TaskQueue> mTaskQueue;
+ const RefPtr<ReaderProxy> mReader;
+ mozilla::MediaMetadataManager mMetadataManager;
+
+ // Playback rate. 1.0 : normal speed, 0.5 : two times slower.
+ double mPlaybackRate;
+
+ // Event producers
+ MediaEventProducerExc<UniquePtr<MediaInfo>, UniquePtr<MetadataTags>,
+ MediaDecoderEventVisibility>
+ mMetadataLoadedEvent;
+ MediaEventProducerExc<UniquePtr<MediaInfo>, MediaDecoderEventVisibility>
+ mFirstFrameLoadedEvent;
+ MediaEventProducerExc<RefPtr<VideoFrameContainer>>
+ mOnSecondaryVideoContainerInstalled;
+ MediaEventProducer<MediaPlaybackEvent> mOnPlaybackEvent;
+ MediaEventProducer<MediaResult> mOnPlaybackErrorEvent;
+ MediaEventProducer<DecoderDoctorEvent> mOnDecoderDoctorEvent;
+ MediaEventProducer<NextFrameStatus> mOnNextFrameStatus;
+
+ // The buffered range. Mirrored from the decoder thread.
+ Mirror<media::TimeIntervals> mBuffered;
+
+ // The current play state, mirrored from the main thread.
+ Mirror<MediaDecoder::PlayState> mPlayState;
+
+ // Volume of playback. 0.0 = muted. 1.0 = full volume.
+ Mirror<double> mVolume;
+
+ // Pitch preservation for the playback rate.
+ Mirror<bool> mPreservesPitch;
+
+ // Whether to seek back to the start of the media resource
+ // upon reaching the end.
+ Mirror<bool> mLooping;
+
+ // Set if the decoder is sending video to a secondary container. While set we
+ // should not suspend the decoder.
+ Mirror<RefPtr<VideoFrameContainer>> mSecondaryVideoContainer;
+
+ // Duration of the media. This is guaranteed to be non-null after we finish
+ // decoding the first frame.
+ Canonical<media::NullableTimeUnit> mDuration;
+
+ // The time of the current frame, corresponding to the "current
+ // playback position" in HTML5. This is referenced from 0, which is the
+ // initial playback position.
+ Canonical<media::TimeUnit> mCurrentPosition;
+
+ // Used to distinguish whether the audio is producing sound.
+ Canonical<bool> mIsAudioDataAudible;
+
+ // Stores presentation info required for playback.
+ Maybe<MediaInfo> mInfo;
+
+ // True if the media is seekable (i.e. supports random access).
+ bool mMediaSeekable = true;
+
+ // True if the media is seekable only in buffered ranges.
+ bool mMediaSeekableOnlyInBufferedRanges = false;
+
+ // True if we've decoded first frames (thus having the start time) and
+ // notified the FirstFrameLoaded event. Note we can't initiate seek until the
+ // start time is known which happens when the first frames are decoded or we
+ // are playing an MSE stream (the start time is always assumed 0).
+ bool mSentFirstFrameLoadedEvent = false;
+
+ // True if we should not decode/preroll unnecessary samples, unless we're
+ // played. "Prerolling" in this context refers to when we decode and
+ // buffer decoded samples in advance of when they're needed for playback.
+ // This flag is set for preload=metadata media, and means we won't
+ // decode more than the first video frame and first block of audio samples
+ // for that media when we startup, or after a seek. When Play() is called,
+ // we reset this flag, as we assume the user is playing the media, so
+ // prerolling is appropriate then. This flag is used to reduce the overhead
+ // of prerolling samples for media elements that may not play, both
+ // memory and CPU overhead.
+ bool mMinimizePreroll;
+
+ // Only one of a given pair of ({Audio,Video}DataPromise, WaitForDataPromise)
+ // should exist at any given moment.
+ using AudioDataPromise = MediaFormatReader::AudioDataPromise;
+ using VideoDataPromise = MediaFormatReader::VideoDataPromise;
+ using WaitForDataPromise = MediaFormatReader::WaitForDataPromise;
+ MozPromiseRequestHolder<AudioDataPromise> mAudioDataRequest;
+ MozPromiseRequestHolder<VideoDataPromise> mVideoDataRequest;
+ MozPromiseRequestHolder<WaitForDataPromise> mAudioWaitRequest;
+ MozPromiseRequestHolder<WaitForDataPromise> mVideoWaitRequest;
+
+ private:
+ WatchManager<MediaDecoderStateMachineBase> mWatchManager;
+};
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_MEDIADECODERSTATEMACHINEBASE_H_
diff --git a/dom/media/MediaDeviceInfo.cpp b/dom/media/MediaDeviceInfo.cpp
new file mode 100644
index 0000000000..4183da2865
--- /dev/null
+++ b/dom/media/MediaDeviceInfo.cpp
@@ -0,0 +1,42 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/MediaDeviceInfo.h"
+#include "mozilla/dom/MediaStreamBinding.h"
+#include "mozilla/MediaManager.h"
+#include "nsIScriptGlobalObject.h"
+
+namespace mozilla::dom {
+
+MediaDeviceInfo::MediaDeviceInfo(const nsAString& aDeviceId,
+ MediaDeviceKind aKind, const nsAString& aLabel,
+ const nsAString& aGroupId)
+ : mKind(aKind), mDeviceId(aDeviceId), mLabel(aLabel), mGroupId(aGroupId) {}
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_0(MediaDeviceInfo)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaDeviceInfo)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaDeviceInfo)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaDeviceInfo)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+JSObject* MediaDeviceInfo::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return MediaDeviceInfo_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsISupports* MediaDeviceInfo::GetParentObject() { return nullptr; }
+
+void MediaDeviceInfo::GetDeviceId(nsString& retval) { retval = mDeviceId; }
+
+MediaDeviceKind MediaDeviceInfo::Kind() { return mKind; }
+
+void MediaDeviceInfo::GetGroupId(nsString& retval) { retval = mGroupId; }
+
+void MediaDeviceInfo::GetLabel(nsString& retval) { retval = mLabel; }
+
+MediaDeviceKind Kind();
+
+} // namespace mozilla::dom
diff --git a/dom/media/MediaDeviceInfo.h b/dom/media/MediaDeviceInfo.h
new file mode 100644
index 0000000000..e68ffba9dc
--- /dev/null
+++ b/dom/media/MediaDeviceInfo.h
@@ -0,0 +1,59 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_MediaDeviceInfo_h
+#define mozilla_dom_MediaDeviceInfo_h
+
+#include "js/RootingAPI.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/dom/MediaDeviceInfoBinding.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsID.h"
+#include "nsISupports.h"
+#include "nsStringFwd.h"
+#include "nsWrapperCache.h"
+
+namespace mozilla::dom {
+
+#define MOZILLA_DOM_MEDIADEVICEINFO_IMPLEMENTATION_IID \
+ { \
+ 0x25091870, 0x84d6, 0x4acf, { \
+ 0xaf, 0x97, 0x6e, 0xd5, 0x5b, 0xe0, 0x47, 0xb2 \
+ } \
+ }
+
+class MediaDeviceInfo final : public nsISupports, public nsWrapperCache {
+ public:
+ explicit MediaDeviceInfo(const nsAString& aDeviceId, MediaDeviceKind aKind,
+ const nsAString& aLabel, const nsAString& aGroupId);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(MediaDeviceInfo)
+ NS_DECLARE_STATIC_IID_ACCESSOR(MOZILLA_DOM_MEDIADEVICEINFO_IMPLEMENTATION_IID)
+
+ JSObject* WrapObject(JSContext* cx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ nsISupports* GetParentObject();
+
+ void GetDeviceId(nsString& retval);
+ MediaDeviceKind Kind();
+ void GetLabel(nsString& retval);
+ void GetGroupId(nsString& retval);
+
+ private:
+ MediaDeviceKind mKind;
+ nsString mDeviceId;
+ nsString mLabel;
+ nsString mGroupId;
+
+ virtual ~MediaDeviceInfo() = default;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(MediaDeviceInfo,
+ MOZILLA_DOM_MEDIADEVICEINFO_IMPLEMENTATION_IID)
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_MediaDeviceInfo_h
diff --git a/dom/media/MediaDevices.cpp b/dom/media/MediaDevices.cpp
new file mode 100644
index 0000000000..581eec5247
--- /dev/null
+++ b/dom/media/MediaDevices.cpp
@@ -0,0 +1,804 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/MediaDevices.h"
+
+#include "AudioDeviceInfo.h"
+#include "MediaEngine.h"
+#include "MediaEngineFake.h"
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/FeaturePolicyUtils.h"
+#include "mozilla/dom/MediaStreamBinding.h"
+#include "mozilla/dom/MediaDeviceInfo.h"
+#include "mozilla/dom/MediaDevicesBinding.h"
+#include "mozilla/dom/NavigatorBinding.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/WindowContext.h"
+#include "mozilla/intl/Localization.h"
+#include "mozilla/MediaManager.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "MediaTrackConstraints.h"
+#include "nsContentUtils.h"
+#include "nsINamed.h"
+#include "nsIScriptGlobalObject.h"
+#include "nsPIDOMWindow.h"
+#include "nsGlobalWindowInner.h"
+#include "nsQueryObject.h"
+
+namespace mozilla::dom {
+
+using ConstDeviceSetPromise = MediaManager::ConstDeviceSetPromise;
+using LocalDeviceSetPromise = MediaManager::LocalDeviceSetPromise;
+using LocalMediaDeviceSetRefCnt = MediaManager::LocalMediaDeviceSetRefCnt;
+using MediaDeviceSetRefCnt = MediaManager::MediaDeviceSetRefCnt;
+using mozilla::intl::Localization;
+
+MediaDevices::MediaDevices(nsPIDOMWindowInner* aWindow)
+ : DOMEventTargetHelper(aWindow), mDefaultOutputLabel(VoidString()) {}
+
+MediaDevices::~MediaDevices() {
+ MOZ_ASSERT(NS_IsMainThread());
+ mDeviceChangeListener.DisconnectIfExists();
+}
+
+already_AddRefed<Promise> MediaDevices::GetUserMedia(
+ const MediaStreamConstraints& aConstraints, CallerType aCallerType,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // Get the relevant global for the promise from the wrapper cache because
+ // DOMEventTargetHelper::GetOwner() returns null if the document is unloaded.
+ // We know the wrapper exists because it is being used for |this| from JS.
+ // See https://github.com/heycam/webidl/issues/932 for why the relevant
+ // global is used instead of the current global.
+ nsCOMPtr<nsIGlobalObject> global = xpc::NativeGlobal(GetWrapper());
+ // global is a window because MediaDevices is exposed only to Window.
+ nsCOMPtr<nsPIDOMWindowInner> owner = do_QueryInterface(global);
+ if (Document* doc = owner->GetExtantDoc()) {
+ if (!owner->IsSecureContext()) {
+ doc->SetUseCounter(eUseCounter_custom_GetUserMediaInsec);
+ }
+ Document* topDoc = doc->GetTopLevelContentDocumentIfSameProcess();
+ IgnoredErrorResult ignored;
+ if (topDoc && !topDoc->HasFocus(ignored)) {
+ doc->SetUseCounter(eUseCounter_custom_GetUserMediaUnfocused);
+ }
+ }
+ RefPtr<Promise> p = Promise::Create(global, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+ /* If requestedMediaTypes is the empty set, return a promise rejected with a
+ * TypeError. */
+ if (!MediaManager::IsOn(aConstraints.mVideo) &&
+ !MediaManager::IsOn(aConstraints.mAudio)) {
+ p->MaybeRejectWithTypeError("audio and/or video is required");
+ return p.forget();
+ }
+ /* If the relevant settings object's responsible document is NOT fully
+ * active, return a promise rejected with a DOMException object whose name
+ * attribute has the value "InvalidStateError". */
+ if (!owner->IsFullyActive()) {
+ p->MaybeRejectWithInvalidStateError("The document is not fully active.");
+ return p.forget();
+ }
+ const OwningBooleanOrMediaTrackConstraints& video = aConstraints.mVideo;
+ if (aCallerType != CallerType::System && video.IsMediaTrackConstraints()) {
+ const Optional<nsString>& mediaSource =
+ video.GetAsMediaTrackConstraints().mMediaSource;
+ if (mediaSource.WasPassed() &&
+ !mediaSource.Value().EqualsLiteral("camera")) {
+ WindowContext* wc = owner->GetWindowContext();
+ if (!wc || !wc->HasValidTransientUserGestureActivation()) {
+ p->MaybeRejectWithInvalidStateError(
+ "Display capture requires transient activation "
+ "from a user gesture.");
+ return p.forget();
+ }
+ }
+ }
+ RefPtr<MediaDevices> self(this);
+ GetUserMedia(owner, aConstraints, aCallerType)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [this, self, p](RefPtr<DOMMediaStream>&& aStream) {
+ if (!GetWindowIfCurrent()) {
+ return; // Leave Promise pending after navigation by design.
+ }
+ p->MaybeResolve(std::move(aStream));
+ },
+ [this, self, p](const RefPtr<MediaMgrError>& error) {
+ nsPIDOMWindowInner* window = GetWindowIfCurrent();
+ if (!window) {
+ return; // Leave Promise pending after navigation by design.
+ }
+ error->Reject(p);
+ });
+ return p.forget();
+}
+
+RefPtr<MediaDevices::StreamPromise> MediaDevices::GetUserMedia(
+ nsPIDOMWindowInner* aWindow, const MediaStreamConstraints& aConstraints,
+ CallerType aCallerType) {
+ MOZ_ASSERT(NS_IsMainThread());
+ bool haveFake = aConstraints.mFake.WasPassed() && aConstraints.mFake.Value();
+ const OwningBooleanOrMediaTrackConstraints& video = aConstraints.mVideo;
+ const OwningBooleanOrMediaTrackConstraints& audio = aConstraints.mAudio;
+ bool isMicrophone =
+ !haveFake &&
+ (audio.IsBoolean()
+ ? audio.GetAsBoolean()
+ : !audio.GetAsMediaTrackConstraints().mMediaSource.WasPassed());
+ bool isCamera =
+ !haveFake &&
+ (video.IsBoolean()
+ ? video.GetAsBoolean()
+ : !video.GetAsMediaTrackConstraints().mMediaSource.WasPassed());
+
+ RefPtr<MediaDevices> self(this);
+ return MediaManager::Get()
+ ->GetUserMedia(aWindow, aConstraints, aCallerType)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [this, self, isMicrophone,
+ isCamera](RefPtr<DOMMediaStream>&& aStream) {
+ if (isMicrophone) {
+ mCanExposeMicrophoneInfo = true;
+ }
+ if (isCamera) {
+ mCanExposeCameraInfo = true;
+ }
+ return StreamPromise::CreateAndResolve(std::move(aStream),
+ __func__);
+ },
+ [](RefPtr<MediaMgrError>&& aError) {
+ return StreamPromise::CreateAndReject(std::move(aError), __func__);
+ });
+}
+
+already_AddRefed<Promise> MediaDevices::EnumerateDevices(ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCOMPtr<nsIGlobalObject> global = xpc::NativeGlobal(GetWrapper());
+ nsCOMPtr<nsPIDOMWindowInner> owner = do_QueryInterface(global);
+ if (Document* doc = owner->GetExtantDoc()) {
+ if (!owner->IsSecureContext()) {
+ doc->SetUseCounter(eUseCounter_custom_EnumerateDevicesInsec);
+ }
+ Document* topDoc = doc->GetTopLevelContentDocumentIfSameProcess();
+ IgnoredErrorResult ignored;
+ if (topDoc && !topDoc->HasFocus(ignored)) {
+ doc->SetUseCounter(eUseCounter_custom_EnumerateDevicesUnfocused);
+ }
+ }
+ RefPtr<Promise> p = Promise::Create(global, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+ mPendingEnumerateDevicesPromises.AppendElement(p);
+ MaybeResumeDeviceExposure();
+ return p.forget();
+}
+
+void MediaDevices::MaybeResumeDeviceExposure() {
+ if (mPendingEnumerateDevicesPromises.IsEmpty() &&
+ !mHaveUnprocessedDeviceListChange) {
+ return;
+ }
+ nsPIDOMWindowInner* window = GetOwner();
+ if (!window || !window->IsFullyActive()) {
+ return;
+ }
+ if (!StaticPrefs::media_devices_unfocused_enabled()) {
+ // Device list changes are not exposed to unfocused contexts because the
+ // timing information would allow fingerprinting for content to identify
+ // concurrent browsing, even when pages are in different containers.
+ BrowsingContext* bc = window->GetBrowsingContext();
+ if (!bc->IsActive() || // background tab or browser window fully obscured
+ !bc->GetIsActiveBrowserWindow()) { // browser window without focus
+ return;
+ }
+ }
+ MediaManager::Get()->GetPhysicalDevices()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr(this), this,
+ haveDeviceListChange = mHaveUnprocessedDeviceListChange,
+ enumerateDevicesPromises = std::move(mPendingEnumerateDevicesPromises)](
+ RefPtr<const MediaDeviceSetRefCnt> aAllDevices) mutable {
+ RefPtr<MediaDeviceSetRefCnt> exposedDevices =
+ FilterExposedDevices(*aAllDevices);
+ if (haveDeviceListChange) {
+ if (ShouldQueueDeviceChange(*exposedDevices)) {
+ NS_DispatchToCurrentThread(NS_NewRunnableFunction(
+ "devicechange", [self = RefPtr(this), this] {
+ DispatchTrustedEvent(u"devicechange"_ns);
+ }));
+ }
+ mLastPhysicalDevices = std::move(aAllDevices);
+ }
+ if (!enumerateDevicesPromises.IsEmpty()) {
+ ResumeEnumerateDevices(std::move(enumerateDevicesPromises),
+ std::move(exposedDevices));
+ }
+ },
+ [](RefPtr<MediaMgrError>&&) {
+ MOZ_ASSERT_UNREACHABLE("GetPhysicalDevices does not reject");
+ });
+ mHaveUnprocessedDeviceListChange = false;
+}
+
+RefPtr<MediaDeviceSetRefCnt> MediaDevices::FilterExposedDevices(
+ const MediaDeviceSet& aDevices) const {
+ nsPIDOMWindowInner* window = GetOwner();
+ RefPtr exposed = new MediaDeviceSetRefCnt();
+ if (!window) {
+ return exposed; // Promises will be left pending
+ }
+ Document* doc = window->GetExtantDoc();
+ if (!doc) {
+ return exposed;
+ }
+ // Only expose devices which are allowed to use:
+ // https://w3c.github.io/mediacapture-main/#dom-mediadevices-enumeratedevices
+ bool dropMics = !FeaturePolicyUtils::IsFeatureAllowed(doc, u"microphone"_ns);
+ bool dropCams = !FeaturePolicyUtils::IsFeatureAllowed(doc, u"camera"_ns);
+ bool dropSpeakers =
+ !Preferences::GetBool("media.setsinkid.enabled") ||
+ !FeaturePolicyUtils::IsFeatureAllowed(doc, u"speaker-selection"_ns);
+
+ if (doc->ShouldResistFingerprinting(RFPTarget::Unknown)) {
+ RefPtr fakeEngine = new MediaEngineFake();
+ fakeEngine->EnumerateDevices(MediaSourceEnum::Microphone,
+ MediaSinkEnum::Other, exposed);
+ fakeEngine->EnumerateDevices(MediaSourceEnum::Camera, MediaSinkEnum::Other,
+ exposed);
+ dropMics = dropCams = true;
+ // Speakers are not handled specially with resistFingerprinting because
+ // they are exposed only when explicitly and individually allowed by the
+ // user.
+ }
+ bool legacy = StaticPrefs::media_devices_enumerate_legacy_enabled();
+ bool outputIsDefault = true; // First output is the default.
+ bool haveDefaultOutput = false;
+ nsTHashSet<nsString> exposedMicrophoneGroupIds;
+ for (const auto& device : aDevices) {
+ switch (device->mKind) {
+ case MediaDeviceKind::Audioinput:
+ if (dropMics) {
+ continue;
+ }
+ if (mCanExposeMicrophoneInfo) {
+ exposedMicrophoneGroupIds.Insert(device->mRawGroupID);
+ }
+ if (!mCanExposeMicrophoneInfo && !legacy) {
+ dropMics = true;
+ }
+ break;
+ case MediaDeviceKind::Videoinput:
+ if (dropCams) {
+ continue;
+ }
+ if (!mCanExposeCameraInfo && !legacy) {
+ dropCams = true;
+ }
+ break;
+ case MediaDeviceKind::Audiooutput:
+ if (dropSpeakers ||
+ (!mExplicitlyGrantedAudioOutputRawIds.Contains(device->mRawID) &&
+ // Assumes aDevices order has microphones before speakers.
+ !exposedMicrophoneGroupIds.Contains(device->mRawGroupID))) {
+ outputIsDefault = false;
+ continue;
+ }
+ if (!haveDefaultOutput && !outputIsDefault) {
+ // Insert a virtual default device so that the first enumerated
+ // device is the default output.
+ if (mDefaultOutputLabel.IsVoid()) {
+ mDefaultOutputLabel.SetIsVoid(false);
+ AutoTArray<nsCString, 1> resourceIds{"dom/media.ftl"_ns};
+ RefPtr l10n = Localization::Create(resourceIds, /*sync*/ true);
+ nsAutoCString translation;
+ IgnoredErrorResult rv;
+ l10n->FormatValueSync("default-audio-output-device-label"_ns, {},
+ translation, rv);
+ if (!rv.Failed()) {
+ AppendUTF8toUTF16(translation, mDefaultOutputLabel);
+ }
+ }
+ RefPtr info = new AudioDeviceInfo(
+ nullptr, mDefaultOutputLabel, u""_ns, u""_ns,
+ CUBEB_DEVICE_TYPE_OUTPUT, CUBEB_DEVICE_STATE_ENABLED,
+ CUBEB_DEVICE_PREF_ALL, CUBEB_DEVICE_FMT_ALL,
+ CUBEB_DEVICE_FMT_S16NE, 2, 44100, 44100, 44100, 128, 128);
+ exposed->AppendElement(
+ new MediaDevice(new MediaEngineFake(), info, u""_ns));
+ }
+ haveDefaultOutput = true;
+ break;
+ case MediaDeviceKind::EndGuard_:
+ continue;
+ // Avoid `default:` so that `-Wswitch` catches missing
+ // enumerators at compile time.
+ }
+ exposed->AppendElement(device);
+ }
+ return exposed;
+}
+
+bool MediaDevices::CanExposeInfo(MediaDeviceKind aKind) const {
+ switch (aKind) {
+ case MediaDeviceKind::Audioinput:
+ return mCanExposeMicrophoneInfo;
+ case MediaDeviceKind::Videoinput:
+ return mCanExposeCameraInfo;
+ case MediaDeviceKind::Audiooutput:
+ // Assumes caller has used FilterExposedDevices()
+ return true;
+ case MediaDeviceKind::EndGuard_:
+ break;
+ // Avoid `default:` so that `-Wswitch` catches missing enumerators at
+ // compile time.
+ }
+ MOZ_ASSERT_UNREACHABLE("unexpected MediaDeviceKind");
+ return false;
+}
+
+bool MediaDevices::ShouldQueueDeviceChange(
+ const MediaDeviceSet& aExposedDevices) const {
+ if (!mLastPhysicalDevices) { // SetupDeviceChangeListener not complete
+ return false;
+ }
+ RefPtr<MediaDeviceSetRefCnt> lastExposedDevices =
+ FilterExposedDevices(*mLastPhysicalDevices);
+ auto exposed = aExposedDevices.begin();
+ auto exposedEnd = aExposedDevices.end();
+ auto last = lastExposedDevices->begin();
+ auto lastEnd = lastExposedDevices->end();
+ // Lists from FilterExposedDevices may have multiple devices of the same
+ // kind even when only a single anonymous device of that kind should be
+ // exposed by enumerateDevices() (but multiple devices are currently exposed
+ // - bug 1528042). "devicechange" events are not queued when the number
+ // of such devices changes but remains non-zero.
+ while (exposed < exposedEnd && last < lastEnd) {
+ // First determine whether there is at least one device of the same kind
+ // in both `aExposedDevices` and `lastExposedDevices`.
+ // A change between zero and non-zero numbers of microphone or camera
+ // devices triggers a devicechange event even if that kind of device is
+ // not yet exposed.
+ MediaDeviceKind kind = (*exposed)->mKind;
+ if (kind != (*last)->mKind) {
+ return true;
+ }
+ // `exposed` and `last` have matching kind.
+ if (CanExposeInfo(kind)) {
+ // Queue "devicechange" if there has been any change in devices of this
+ // exposed kind. ID and kind uniquely identify a device.
+ if ((*exposed)->mRawID != (*last)->mRawID) {
+ return true;
+ }
+ ++exposed;
+ ++last;
+ continue;
+ }
+ // `aExposedDevices` and `lastExposedDevices` both have non-zero numbers
+ // of devices of this unexposed kind.
+ // Skip remaining devices of this kind because all devices of this kind
+ // should be exposed as a single anonymous device.
+ do {
+ ++exposed;
+ } while (exposed != exposedEnd && (*exposed)->mKind == kind);
+ do {
+ ++last;
+ } while (last != lastEnd && (*last)->mKind == kind);
+ }
+ // Queue "devicechange" if the number of exposed devices differs.
+ return exposed < exposedEnd || last < lastEnd;
+}
+
+void MediaDevices::ResumeEnumerateDevices(
+ nsTArray<RefPtr<Promise>>&& aPromises,
+ RefPtr<const MediaDeviceSetRefCnt> aExposedDevices) const {
+ nsCOMPtr<nsPIDOMWindowInner> window = GetOwner();
+ if (!window) {
+ return; // Leave Promise pending after navigation by design.
+ }
+ MediaManager::Get()
+ ->AnonymizeDevices(window, std::move(aExposedDevices))
+ ->Then(GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr(this), this, promises = std::move(aPromises)](
+ const LocalDeviceSetPromise::ResolveOrRejectValue&
+ aLocalDevices) {
+ nsPIDOMWindowInner* window = GetWindowIfCurrent();
+ if (!window) {
+ return; // Leave Promises pending after navigation by design.
+ }
+ for (const RefPtr<Promise>& promise : promises) {
+ if (aLocalDevices.IsReject()) {
+ aLocalDevices.RejectValue()->Reject(promise);
+ } else {
+ ResolveEnumerateDevicesPromise(
+ promise, *aLocalDevices.ResolveValue());
+ }
+ }
+ });
+}
+
+void MediaDevices::ResolveEnumerateDevicesPromise(
+ Promise* aPromise, const LocalMediaDeviceSet& aDevices) const {
+ nsCOMPtr<nsPIDOMWindowInner> window = GetOwner();
+ auto windowId = window->WindowID();
+ nsTArray<RefPtr<MediaDeviceInfo>> infos;
+ bool legacy = StaticPrefs::media_devices_enumerate_legacy_enabled();
+ bool capturePermitted =
+ legacy &&
+ MediaManager::Get()->IsActivelyCapturingOrHasAPermission(windowId);
+
+ for (const RefPtr<LocalMediaDevice>& device : aDevices) {
+ bool exposeInfo = CanExposeInfo(device->Kind()) || legacy;
+ bool exposeLabel = legacy ? capturePermitted : exposeInfo;
+ infos.AppendElement(MakeRefPtr<MediaDeviceInfo>(
+ exposeInfo ? device->mID : u""_ns, device->Kind(),
+ exposeLabel ? device->mName : u""_ns,
+ exposeInfo ? device->mGroupID : u""_ns));
+ }
+ aPromise->MaybeResolve(std::move(infos));
+}
+
+already_AddRefed<Promise> MediaDevices::GetDisplayMedia(
+ const DisplayMediaStreamConstraints& aConstraints, CallerType aCallerType,
+ ErrorResult& aRv) {
+ nsCOMPtr<nsIGlobalObject> global = xpc::NativeGlobal(GetWrapper());
+ RefPtr<Promise> p = Promise::Create(global, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+ nsCOMPtr<nsPIDOMWindowInner> owner = do_QueryInterface(global);
+ /* If the relevant global object of this does not have transient activation,
+ * return a promise rejected with a DOMException object whose name attribute
+ * has the value InvalidStateError. */
+ WindowContext* wc = owner->GetWindowContext();
+ if (!wc || !wc->HasValidTransientUserGestureActivation()) {
+ p->MaybeRejectWithInvalidStateError(
+ "getDisplayMedia requires transient activation from a user gesture.");
+ return p.forget();
+ }
+ /* If constraints.video is false, return a promise rejected with a newly
+ * created TypeError. */
+ if (!MediaManager::IsOn(aConstraints.mVideo)) {
+ p->MaybeRejectWithTypeError("video is required");
+ return p.forget();
+ }
+ MediaStreamConstraints c;
+ auto& vc = c.mVideo.SetAsMediaTrackConstraints();
+
+ if (aConstraints.mVideo.IsMediaTrackConstraints()) {
+ vc = aConstraints.mVideo.GetAsMediaTrackConstraints();
+ /* If CS contains a member named advanced, return a promise rejected with
+ * a newly created TypeError. */
+ if (vc.mAdvanced.WasPassed()) {
+ p->MaybeRejectWithTypeError("advanced not allowed");
+ return p.forget();
+ }
+ auto getCLR = [](const auto& aCon) -> const ConstrainLongRange& {
+ static ConstrainLongRange empty;
+ return (aCon.WasPassed() && !aCon.Value().IsLong())
+ ? aCon.Value().GetAsConstrainLongRange()
+ : empty;
+ };
+ auto getCDR = [](auto&& aCon) -> const ConstrainDoubleRange& {
+ static ConstrainDoubleRange empty;
+ return (aCon.WasPassed() && !aCon.Value().IsDouble())
+ ? aCon.Value().GetAsConstrainDoubleRange()
+ : empty;
+ };
+ const auto& w = getCLR(vc.mWidth);
+ const auto& h = getCLR(vc.mHeight);
+ const auto& f = getCDR(vc.mFrameRate);
+ /* If CS contains a member whose name specifies a constrainable property
+ * applicable to display surfaces, and whose value in turn is a dictionary
+ * containing a member named either min or exact, return a promise
+ * rejected with a newly created TypeError. */
+ if (w.mMin.WasPassed() || h.mMin.WasPassed() || f.mMin.WasPassed()) {
+ p->MaybeRejectWithTypeError("min not allowed");
+ return p.forget();
+ }
+ if (w.mExact.WasPassed() || h.mExact.WasPassed() || f.mExact.WasPassed()) {
+ p->MaybeRejectWithTypeError("exact not allowed");
+ return p.forget();
+ }
+ /* If CS contains a member whose name, failedConstraint specifies a
+ * constrainable property, constraint, applicable to display surfaces, and
+ * whose value in turn is a dictionary containing a member named max, and
+ * that member's value in turn is less than the constrainable property's
+ * floor value, then let failedConstraint be the name of the constraint,
+ * let message be either undefined or an informative human-readable
+ * message, and return a promise rejected with a new OverconstrainedError
+ * created by calling OverconstrainedError(failedConstraint, message). */
+ // We fail early without incurring a prompt, on known-to-fail constraint
+ // values that don't reveal anything about the user's system.
+ const char* badConstraint = nullptr;
+ if (w.mMax.WasPassed() && w.mMax.Value() < 1) {
+ badConstraint = "width";
+ }
+ if (h.mMax.WasPassed() && h.mMax.Value() < 1) {
+ badConstraint = "height";
+ }
+ if (f.mMax.WasPassed() && f.mMax.Value() < 1) {
+ badConstraint = "frameRate";
+ }
+ if (badConstraint) {
+ p->MaybeReject(MakeRefPtr<dom::MediaStreamError>(
+ owner, *MakeRefPtr<MediaMgrError>(
+ MediaMgrError::Name::OverconstrainedError, "",
+ NS_ConvertASCIItoUTF16(badConstraint))));
+ return p.forget();
+ }
+ }
+ /* If the relevant settings object's responsible document is NOT fully
+ * active, return a promise rejected with a DOMException object whose name
+ * attribute has the value "InvalidStateError". */
+ if (!owner->IsFullyActive()) {
+ p->MaybeRejectWithInvalidStateError("The document is not fully active.");
+ return p.forget();
+ }
+ // We ask for "screen" sharing.
+ //
+ // If this is a privileged call or permission is disabled, this gives us full
+ // screen sharing by default, which is useful for internal testing.
+ //
+ // If this is a non-priviliged call, GetUserMedia() will change it to "window"
+ // for us.
+ vc.mMediaSource.Reset();
+ vc.mMediaSource.Construct().AssignASCII(
+ dom::MediaSourceEnumValues::GetString(MediaSourceEnum::Screen));
+
+ RefPtr<MediaDevices> self(this);
+ MediaManager::Get()
+ ->GetUserMedia(owner, c, aCallerType)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [this, self, p](RefPtr<DOMMediaStream>&& aStream) {
+ if (!GetWindowIfCurrent()) {
+ return; // leave promise pending after navigation.
+ }
+ p->MaybeResolve(std::move(aStream));
+ },
+ [this, self, p](RefPtr<MediaMgrError>&& error) {
+ nsPIDOMWindowInner* window = GetWindowIfCurrent();
+ if (!window) {
+ return; // leave promise pending after navigation.
+ }
+ error->Reject(p);
+ });
+ return p.forget();
+}
+
+already_AddRefed<Promise> MediaDevices::SelectAudioOutput(
+ const AudioOutputOptions& aOptions, CallerType aCallerType,
+ ErrorResult& aRv) {
+ nsCOMPtr<nsIGlobalObject> global = xpc::NativeGlobal(GetWrapper());
+ RefPtr<Promise> p = Promise::Create(global, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+ /* (This includes the expected user activation update of
+ * https://github.com/w3c/mediacapture-output/issues/107)
+ * If the relevant global object of this does not have transient activation,
+ * return a promise rejected with a DOMException object whose name attribute
+ * has the value InvalidStateError. */
+ nsCOMPtr<nsPIDOMWindowInner> owner = do_QueryInterface(global);
+ WindowContext* wc = owner->GetWindowContext();
+ if (!wc || !wc->HasValidTransientUserGestureActivation()) {
+ p->MaybeRejectWithInvalidStateError(
+ "selectAudioOutput requires transient user activation.");
+ return p.forget();
+ }
+ RefPtr<MediaDevices> self(this);
+ MediaManager::Get()
+ ->SelectAudioOutput(owner, aOptions, aCallerType)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [this, self, p](RefPtr<LocalMediaDevice> aDevice) {
+ nsPIDOMWindowInner* window = GetWindowIfCurrent();
+ if (!window) {
+ return; // Leave Promise pending after navigation by design.
+ }
+ MOZ_ASSERT(aDevice->Kind() == dom::MediaDeviceKind::Audiooutput);
+ mExplicitlyGrantedAudioOutputRawIds.Insert(aDevice->RawID());
+ p->MaybeResolve(
+ MakeRefPtr<MediaDeviceInfo>(aDevice->mID, aDevice->Kind(),
+ aDevice->mName, aDevice->mGroupID));
+ },
+ [this, self, p](const RefPtr<MediaMgrError>& error) {
+ nsPIDOMWindowInner* window = GetWindowIfCurrent();
+ if (!window) {
+ return; // Leave Promise pending after navigation by design.
+ }
+ error->Reject(p);
+ });
+ return p.forget();
+}
+
+static RefPtr<AudioDeviceInfo> CopyWithNullDeviceId(
+ AudioDeviceInfo* aDeviceInfo) {
+ MOZ_ASSERT(aDeviceInfo->Preferred());
+
+ nsString vendor;
+ aDeviceInfo->GetVendor(vendor);
+ uint16_t type;
+ aDeviceInfo->GetType(&type);
+ uint16_t state;
+ aDeviceInfo->GetState(&state);
+ uint16_t pref;
+ aDeviceInfo->GetPreferred(&pref);
+ uint16_t supportedFormat;
+ aDeviceInfo->GetSupportedFormat(&supportedFormat);
+ uint16_t defaultFormat;
+ aDeviceInfo->GetDefaultFormat(&defaultFormat);
+ uint32_t maxChannels;
+ aDeviceInfo->GetMaxChannels(&maxChannels);
+ uint32_t defaultRate;
+ aDeviceInfo->GetDefaultRate(&defaultRate);
+ uint32_t maxRate;
+ aDeviceInfo->GetMaxRate(&maxRate);
+ uint32_t minRate;
+ aDeviceInfo->GetMinRate(&minRate);
+ uint32_t maxLatency;
+ aDeviceInfo->GetMaxLatency(&maxLatency);
+ uint32_t minLatency;
+ aDeviceInfo->GetMinLatency(&minLatency);
+
+ return MakeRefPtr<AudioDeviceInfo>(
+ nullptr, aDeviceInfo->Name(), aDeviceInfo->GroupID(), vendor, type, state,
+ pref, supportedFormat, defaultFormat, maxChannels, defaultRate, maxRate,
+ minRate, maxLatency, minLatency);
+}
+
+RefPtr<MediaDevices::SinkInfoPromise> MediaDevices::GetSinkDevice(
+ const nsString& aDeviceId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ return MediaManager::Get()
+ ->GetPhysicalDevices()
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr(this), this,
+ aDeviceId](RefPtr<const MediaDeviceSetRefCnt> aRawDevices) {
+ nsCOMPtr<nsPIDOMWindowInner> window = GetOwner();
+ if (!window) {
+ return LocalDeviceSetPromise::CreateAndReject(
+ new MediaMgrError(MediaMgrError::Name::AbortError), __func__);
+ }
+ // Don't filter if matching the preferred device, because that may
+ // not be exposed.
+ RefPtr devices = aDeviceId.IsEmpty()
+ ? std::move(aRawDevices)
+ : FilterExposedDevices(*aRawDevices);
+ return MediaManager::Get()->AnonymizeDevices(window,
+ std::move(devices));
+ },
+ [](RefPtr<MediaMgrError>&& reason) {
+ MOZ_ASSERT_UNREACHABLE("GetPhysicalDevices does not reject");
+ return RefPtr<LocalDeviceSetPromise>();
+ })
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [aDeviceId](RefPtr<LocalMediaDeviceSetRefCnt> aDevices) {
+ RefPtr<AudioDeviceInfo> outputInfo;
+ // Check for a matching device.
+ for (const RefPtr<LocalMediaDevice>& device : *aDevices) {
+ if (device->Kind() != dom::MediaDeviceKind::Audiooutput) {
+ continue;
+ }
+ if (aDeviceId.IsEmpty()) {
+ MOZ_ASSERT(device->GetAudioDeviceInfo()->Preferred(),
+ "First Audiooutput should be preferred");
+ return SinkInfoPromise::CreateAndResolve(
+ CopyWithNullDeviceId(device->GetAudioDeviceInfo()),
+ __func__);
+ } else if (aDeviceId.Equals(device->mID)) {
+ return SinkInfoPromise::CreateAndResolve(
+ device->GetAudioDeviceInfo(), __func__);
+ }
+ }
+ /* If sinkId is not the empty string and does not match any audio
+ * output device identified by the result that would be provided
+ * by enumerateDevices(), reject p with a new DOMException whose
+ * name is NotFoundError and abort these substeps. */
+ return SinkInfoPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE,
+ __func__);
+ },
+ // aRejectMethod =
+ [](RefPtr<MediaMgrError>&& aError) {
+ return SinkInfoPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE,
+ __func__);
+ });
+}
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(MediaDevices,
+ DOMEventTargetHelper)
+NS_IMPL_CYCLE_COLLECTION_INHERITED(MediaDevices, DOMEventTargetHelper,
+ mPendingEnumerateDevicesPromises)
+
+void MediaDevices::OnDeviceChange() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (NS_FAILED(CheckCurrentGlobalCorrectness())) {
+ // This is a ghost window, don't do anything.
+ return;
+ }
+
+ // Do not fire event to content script when
+ // privacy.resistFingerprinting is true.
+
+ if (nsContentUtils::ShouldResistFingerprinting(
+ "Guarding the more expensive RFP check with a simple one",
+ RFPTarget::Unknown)) {
+ nsCOMPtr<nsPIDOMWindowInner> window = GetOwner();
+ auto* wrapper = GetWrapper();
+ if (!window && wrapper) {
+ nsCOMPtr<nsIGlobalObject> global = xpc::NativeGlobal(wrapper);
+ window = do_QueryInterface(global);
+ }
+ if (!window) {
+ return;
+ }
+
+ if (nsGlobalWindowInner::Cast(window)->ShouldResistFingerprinting(
+ RFPTarget::Unknown)) {
+ return;
+ }
+ }
+
+ mHaveUnprocessedDeviceListChange = true;
+ MaybeResumeDeviceExposure();
+}
+
+mozilla::dom::EventHandlerNonNull* MediaDevices::GetOndevicechange() {
+ return GetEventHandler(nsGkAtoms::ondevicechange);
+}
+
+void MediaDevices::SetupDeviceChangeListener() {
+ if (mIsDeviceChangeListenerSetUp) {
+ return;
+ }
+
+ nsPIDOMWindowInner* window = GetOwner();
+ if (!window) {
+ return;
+ }
+
+ nsISerialEventTarget* mainThread =
+ window->EventTargetFor(TaskCategory::Other);
+ if (!mainThread) {
+ return;
+ }
+
+ mDeviceChangeListener = MediaManager::Get()->DeviceListChangeEvent().Connect(
+ mainThread, this, &MediaDevices::OnDeviceChange);
+ mIsDeviceChangeListenerSetUp = true;
+
+ MediaManager::Get()->GetPhysicalDevices()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr(this), this](RefPtr<const MediaDeviceSetRefCnt> aDevices) {
+ mLastPhysicalDevices = std::move(aDevices);
+ },
+ [](RefPtr<MediaMgrError>&& reason) {
+ MOZ_ASSERT_UNREACHABLE("GetPhysicalDevices does not reject");
+ });
+}
+
+void MediaDevices::SetOndevicechange(
+ mozilla::dom::EventHandlerNonNull* aCallback) {
+ SetEventHandler(nsGkAtoms::ondevicechange, aCallback);
+}
+
+void MediaDevices::EventListenerAdded(nsAtom* aType) {
+ DOMEventTargetHelper::EventListenerAdded(aType);
+ SetupDeviceChangeListener();
+}
+
+JSObject* MediaDevices::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return MediaDevices_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/MediaDevices.h b/dom/media/MediaDevices.h
new file mode 100644
index 0000000000..5390a583b3
--- /dev/null
+++ b/dom/media/MediaDevices.h
@@ -0,0 +1,140 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_MediaDevices_h
+#define mozilla_dom_MediaDevices_h
+
+#include "MediaEventSource.h"
+#include "js/RootingAPI.h"
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/UseCounter.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/MediaDeviceInfoBinding.h"
+#include "nsCOMPtr.h"
+#include "nsID.h"
+#include "nsISupports.h"
+#include "nsTHashSet.h"
+
+class AudioDeviceInfo;
+
+namespace mozilla {
+
+class LocalMediaDevice;
+class MediaDevice;
+class MediaMgrError;
+class DOMMediaStream;
+template <typename ResolveValueT, typename RejectValueT, bool IsExclusive>
+class MozPromise;
+
+namespace media {
+template <typename T>
+class Refcountable;
+} // namespace media
+
+namespace dom {
+
+class Promise;
+struct MediaStreamConstraints;
+struct DisplayMediaStreamConstraints;
+struct MediaTrackSupportedConstraints;
+struct AudioOutputOptions;
+
+class MediaDevices final : public DOMEventTargetHelper {
+ public:
+ using StreamPromise =
+ MozPromise<RefPtr<DOMMediaStream>, RefPtr<MediaMgrError>, true>;
+ using SinkInfoPromise = MozPromise<RefPtr<AudioDeviceInfo>, nsresult, true>;
+
+ explicit MediaDevices(nsPIDOMWindowInner* aWindow);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MediaDevices, DOMEventTargetHelper)
+
+ JSObject* WrapObject(JSContext* cx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // No code needed, as MediaTrackSupportedConstraints members default to true.
+ void GetSupportedConstraints(MediaTrackSupportedConstraints& aResult){};
+
+ already_AddRefed<Promise> GetUserMedia(
+ const MediaStreamConstraints& aConstraints, CallerType aCallerType,
+ ErrorResult& aRv);
+
+ RefPtr<StreamPromise> GetUserMedia(nsPIDOMWindowInner* aWindow,
+ const MediaStreamConstraints& aConstraints,
+ CallerType aCallerType);
+
+ already_AddRefed<Promise> EnumerateDevices(ErrorResult& aRv);
+
+ already_AddRefed<Promise> GetDisplayMedia(
+ const DisplayMediaStreamConstraints& aConstraints, CallerType aCallerType,
+ ErrorResult& aRv);
+
+ already_AddRefed<Promise> SelectAudioOutput(
+ const AudioOutputOptions& aOptions, CallerType aCallerType,
+ ErrorResult& aRv);
+
+ // Get the sink that corresponds to the given device id.
+ // The returned promise will be resolved with the device
+ // information if the aDeviceId matches a device that would be exposed by
+ // enumerateDevices().
+ // The promise will be rejected with NS_ERROR_NOT_AVAILABLE if aDeviceId
+ // does not match any exposed device.
+ RefPtr<SinkInfoPromise> GetSinkDevice(const nsString& aDeviceId);
+
+ // Called when MediaManager encountered a change in its device lists.
+ void OnDeviceChange();
+
+ void SetupDeviceChangeListener();
+
+ mozilla::dom::EventHandlerNonNull* GetOndevicechange();
+ void SetOndevicechange(mozilla::dom::EventHandlerNonNull* aCallback);
+
+ void EventListenerAdded(nsAtom* aType) override;
+ using DOMEventTargetHelper::EventListenerAdded;
+
+ void BackgroundStateChanged() { MaybeResumeDeviceExposure(); }
+ void WindowResumed() { MaybeResumeDeviceExposure(); }
+ void BrowserWindowBecameActive() { MaybeResumeDeviceExposure(); }
+
+ private:
+ using MediaDeviceSet = nsTArray<RefPtr<MediaDevice>>;
+ using MediaDeviceSetRefCnt = media::Refcountable<MediaDeviceSet>;
+ using LocalMediaDeviceSet = nsTArray<RefPtr<LocalMediaDevice>>;
+
+ virtual ~MediaDevices();
+ void MaybeResumeDeviceExposure();
+ void ResumeEnumerateDevices(
+ nsTArray<RefPtr<Promise>>&& aPromises,
+ RefPtr<const MediaDeviceSetRefCnt> aExposedDevices) const;
+ RefPtr<MediaDeviceSetRefCnt> FilterExposedDevices(
+ const MediaDeviceSet& aDevices) const;
+ bool CanExposeInfo(MediaDeviceKind aKind) const;
+ bool ShouldQueueDeviceChange(const MediaDeviceSet& aExposedDevices) const;
+ void ResolveEnumerateDevicesPromise(
+ Promise* aPromise, const LocalMediaDeviceSet& aDevices) const;
+
+ nsTHashSet<nsString> mExplicitlyGrantedAudioOutputRawIds;
+ nsTArray<RefPtr<Promise>> mPendingEnumerateDevicesPromises;
+ // Set only once, if and when required.
+ mutable nsString mDefaultOutputLabel;
+
+ // Connect/Disconnect on main thread only
+ MediaEventListener mDeviceChangeListener;
+ // Ordered set of the system physical devices when devicechange event
+ // decisions were last performed.
+ RefPtr<const MediaDeviceSetRefCnt> mLastPhysicalDevices;
+ bool mIsDeviceChangeListenerSetUp = false;
+ bool mHaveUnprocessedDeviceListChange = false;
+ bool mCanExposeMicrophoneInfo = false;
+ bool mCanExposeCameraInfo = false;
+
+ void RecordAccessTelemetry(const UseCounter counter) const;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_MediaDevices_h
diff --git a/dom/media/MediaEventSource.h b/dom/media/MediaEventSource.h
new file mode 100644
index 0000000000..cb0a851ffe
--- /dev/null
+++ b/dom/media/MediaEventSource.h
@@ -0,0 +1,594 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MediaEventSource_h_
+#define MediaEventSource_h_
+
+#include <type_traits>
+#include <utility>
+
+#include "mozilla/AbstractThread.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/DataMutex.h"
+#include "mozilla/Mutex.h"
+
+#include "mozilla/Unused.h"
+
+#include "nsISupportsImpl.h"
+#include "nsTArray.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+
+/**
+ * A thread-safe tool to communicate "revocation" across threads. It is used to
+ * disconnect a listener from the event source to prevent future notifications
+ * from coming. Revoke() can be called on any thread. However, it is recommended
+ * to be called on the target thread to avoid race condition.
+ *
+ * RevocableToken is not exposed to the client code directly.
+ * Use MediaEventListener below to do the job.
+ */
+class RevocableToken {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RevocableToken);
+
+ public:
+ RevocableToken() = default;
+
+ virtual void Revoke() = 0;
+ virtual bool IsRevoked() const = 0;
+
+ protected:
+ // Virtual destructor is required since we might delete a Listener object
+ // through its base type pointer.
+ virtual ~RevocableToken() = default;
+};
+
+enum class ListenerPolicy : int8_t {
+ // Allow at most one listener. Move will be used when possible
+ // to pass the event data to save copy.
+ Exclusive,
+ // Allow multiple listeners. Event data will always be copied when passed
+ // to the listeners.
+ NonExclusive
+};
+
+namespace detail {
+
+/**
+ * Define how an event type is passed internally in MediaEventSource and to the
+ * listeners. Specialized for the void type to pass a dummy bool instead.
+ */
+template <typename T>
+struct EventTypeTraits {
+ typedef T ArgType;
+};
+
+template <>
+struct EventTypeTraits<void> {
+ typedef bool ArgType;
+};
+
+/**
+ * Test if a method function or lambda accepts one or more arguments.
+ */
+template <typename T>
+class TakeArgsHelper {
+ template <typename C>
+ static std::false_type test(void (C::*)(), int);
+ template <typename C>
+ static std::false_type test(void (C::*)() const, int);
+ template <typename C>
+ static std::false_type test(void (C::*)() volatile, int);
+ template <typename C>
+ static std::false_type test(void (C::*)() const volatile, int);
+ template <typename F>
+ static std::false_type test(F&&, decltype(std::declval<F>()(), 0));
+ static std::true_type test(...);
+
+ public:
+ typedef decltype(test(std::declval<T>(), 0)) type;
+};
+
+template <typename T>
+struct TakeArgs : public TakeArgsHelper<T>::type {};
+
+template <typename T>
+struct EventTarget;
+
+template <>
+struct EventTarget<nsIEventTarget> {
+ static void Dispatch(nsIEventTarget* aTarget,
+ already_AddRefed<nsIRunnable> aTask) {
+ aTarget->Dispatch(std::move(aTask), NS_DISPATCH_NORMAL);
+ }
+ static bool IsOnTargetThread(nsIEventTarget* aTarget) {
+ bool rv;
+ aTarget->IsOnCurrentThread(&rv);
+ return rv;
+ }
+};
+
+template <>
+struct EventTarget<AbstractThread> {
+ static void Dispatch(AbstractThread* aTarget,
+ already_AddRefed<nsIRunnable> aTask) {
+ aTarget->Dispatch(std::move(aTask));
+ }
+ static bool IsOnTargetThread(AbstractThread* aTarget) {
+ bool rv;
+ aTarget->IsOnCurrentThread(&rv);
+ return rv;
+ }
+};
+
+/**
+ * Encapsulate a raw pointer to be captured by a lambda without causing
+ * static-analysis errors.
+ */
+template <typename T>
+class RawPtr {
+ public:
+ explicit RawPtr(T* aPtr) : mPtr(aPtr) {}
+ T* get() const { return mPtr; }
+
+ private:
+ T* const mPtr;
+};
+
+template <typename... As>
+class Listener : public RevocableToken {
+ public:
+ template <typename... Ts>
+ void Dispatch(Ts&&... aEvents) {
+ if (CanTakeArgs()) {
+ DispatchTask(NewRunnableMethod<std::decay_t<Ts>&&...>(
+ "detail::Listener::ApplyWithArgs", this, &Listener::ApplyWithArgs,
+ std::forward<Ts>(aEvents)...));
+ } else {
+ DispatchTask(NewRunnableMethod("detail::Listener::ApplyWithNoArgs", this,
+ &Listener::ApplyWithNoArgs));
+ }
+ }
+
+ private:
+ virtual void DispatchTask(already_AddRefed<nsIRunnable> aTask) = 0;
+
+ // True if the underlying listener function takes non-zero arguments.
+ virtual bool CanTakeArgs() const = 0;
+ // Pass the event data to the underlying listener function. Should be called
+ // only when CanTakeArgs() returns true.
+ virtual void ApplyWithArgs(As&&... aEvents) = 0;
+ // Invoke the underlying listener function. Should be called only when
+ // CanTakeArgs() returns false.
+ virtual void ApplyWithNoArgs() = 0;
+};
+
+/**
+ * Store the registered target thread and function so it knows where and to
+ * whom to send the event data.
+ */
+template <typename Target, typename Function, typename... As>
+class ListenerImpl : public Listener<As...> {
+ // Strip CV and reference from Function.
+ using FunctionStorage = std::decay_t<Function>;
+ using SelfType = ListenerImpl<Target, Function, As...>;
+
+ public:
+ ListenerImpl(Target* aTarget, Function&& aFunction)
+ : mData(MakeRefPtr<Data>(aTarget, std::forward<Function>(aFunction)),
+ "MediaEvent ListenerImpl::mData") {
+ MOZ_DIAGNOSTIC_ASSERT(aTarget);
+ }
+
+ protected:
+ virtual ~ListenerImpl() {
+ MOZ_ASSERT(IsRevoked(), "Must disconnect the listener.");
+ }
+
+ private:
+ void DispatchTask(already_AddRefed<nsIRunnable> aTask) override {
+ RefPtr<Data> data;
+ {
+ auto d = mData.Lock();
+ data = *d;
+ }
+ if (NS_WARN_IF(!data)) {
+ // already_AddRefed doesn't allow releasing the ref, so transfer it first.
+ RefPtr<nsIRunnable> temp(aTask);
+ return;
+ }
+ EventTarget<Target>::Dispatch(data->mTarget, std::move(aTask));
+ }
+
+ bool CanTakeArgs() const override { return TakeArgs<FunctionStorage>::value; }
+
+ // |F| takes one or more arguments.
+ template <typename F>
+ std::enable_if_t<TakeArgs<F>::value, void> ApplyWithArgsImpl(
+ Target* aTarget, const F& aFunc, As&&... aEvents) {
+ MOZ_DIAGNOSTIC_ASSERT(EventTarget<Target>::IsOnTargetThread(aTarget));
+ aFunc(std::move(aEvents)...);
+ }
+
+ // |F| takes no arguments.
+ template <typename F>
+ std::enable_if_t<!TakeArgs<F>::value, void> ApplyWithArgsImpl(
+ Target* aTarget, const F& aFunc, As&&... aEvents) {
+ MOZ_CRASH("Call ApplyWithNoArgs instead.");
+ }
+
+ void ApplyWithArgs(As&&... aEvents) override {
+ MOZ_RELEASE_ASSERT(TakeArgs<Function>::value);
+ // Don't call the listener if it is disconnected.
+ RefPtr<Data> data;
+ {
+ auto d = mData.Lock();
+ data = *d;
+ }
+ if (!data) {
+ return;
+ }
+ MOZ_DIAGNOSTIC_ASSERT(EventTarget<Target>::IsOnTargetThread(data->mTarget));
+ ApplyWithArgsImpl(data->mTarget, data->mFunction, std::move(aEvents)...);
+ }
+
+ // |F| takes one or more arguments.
+ template <typename F>
+ std::enable_if_t<TakeArgs<F>::value, void> ApplyWithNoArgsImpl(
+ Target* aTarget, const F& aFunc) {
+ MOZ_CRASH("Call ApplyWithArgs instead.");
+ }
+
+ // |F| takes no arguments.
+ template <typename F>
+ std::enable_if_t<!TakeArgs<F>::value, void> ApplyWithNoArgsImpl(
+ Target* aTarget, const F& aFunc) {
+ MOZ_DIAGNOSTIC_ASSERT(EventTarget<Target>::IsOnTargetThread(aTarget));
+ aFunc();
+ }
+
+ void ApplyWithNoArgs() override {
+ MOZ_RELEASE_ASSERT(!TakeArgs<Function>::value);
+ // Don't call the listener if it is disconnected.
+ RefPtr<Data> data;
+ {
+ auto d = mData.Lock();
+ data = *d;
+ }
+ if (!data) {
+ return;
+ }
+ MOZ_DIAGNOSTIC_ASSERT(EventTarget<Target>::IsOnTargetThread(data->mTarget));
+ ApplyWithNoArgsImpl(data->mTarget, data->mFunction);
+ }
+
+ void Revoke() override {
+ {
+ auto data = mData.Lock();
+ *data = nullptr;
+ }
+ }
+
+ bool IsRevoked() const override {
+ auto data = mData.Lock();
+ return !*data;
+ }
+
+ struct RefCountedMediaEventListenerData {
+ // Keep ref-counting here since Data holds a template member, leading to
+ // instances of varying size, which the memory leak logging system dislikes.
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RefCountedMediaEventListenerData)
+ protected:
+ virtual ~RefCountedMediaEventListenerData() = default;
+ };
+ struct Data : public RefCountedMediaEventListenerData {
+ Data(RefPtr<Target> aTarget, Function&& aFunction)
+ : mTarget(std::move(aTarget)),
+ mFunction(std::forward<Function>(aFunction)) {}
+ const RefPtr<Target> mTarget;
+ FunctionStorage mFunction;
+ };
+
+ // Storage for target and function. Also used to track revocation.
+ mutable DataMutex<RefPtr<Data>> mData;
+};
+
+/**
+ * Return true if any type is a reference type.
+ */
+template <typename Head, typename... Tails>
+struct IsAnyReference {
+ static const bool value =
+ std::is_reference_v<Head> || IsAnyReference<Tails...>::value;
+};
+
+template <typename T>
+struct IsAnyReference<T> {
+ static const bool value = std::is_reference_v<T>;
+};
+
+} // namespace detail
+
+template <ListenerPolicy, typename... Ts>
+class MediaEventSourceImpl;
+
+/**
+ * Not thread-safe since this is not meant to be shared and therefore only
+ * move constructor is provided. Used to hold the result of
+ * MediaEventSource<T>::Connect() and call Disconnect() to disconnect the
+ * listener from an event source.
+ */
+class MediaEventListener {
+ template <ListenerPolicy, typename... Ts>
+ friend class MediaEventSourceImpl;
+
+ public:
+ MediaEventListener() = default;
+
+ MediaEventListener(MediaEventListener&& aOther)
+ : mToken(std::move(aOther.mToken)) {}
+
+ MediaEventListener& operator=(MediaEventListener&& aOther) {
+ MOZ_ASSERT(!mToken, "Must disconnect the listener.");
+ mToken = std::move(aOther.mToken);
+ return *this;
+ }
+
+ ~MediaEventListener() {
+ MOZ_ASSERT(!mToken, "Must disconnect the listener.");
+ }
+
+ void Disconnect() {
+ mToken->Revoke();
+ mToken = nullptr;
+ }
+
+ void DisconnectIfExists() {
+ if (mToken) {
+ Disconnect();
+ }
+ }
+
+ private:
+ // Avoid exposing RevocableToken directly to the client code so that
+ // listeners can be disconnected in a controlled manner.
+ explicit MediaEventListener(RevocableToken* aToken) : mToken(aToken) {}
+ RefPtr<RevocableToken> mToken;
+};
+
+/**
+ * A generic and thread-safe class to implement the observer pattern.
+ */
+template <ListenerPolicy Lp, typename... Es>
+class MediaEventSourceImpl {
+ static_assert(!detail::IsAnyReference<Es...>::value,
+ "Ref-type not supported!");
+
+ template <typename T>
+ using ArgType = typename detail::EventTypeTraits<T>::ArgType;
+
+ typedef detail::Listener<ArgType<Es>...> Listener;
+
+ template <typename Target, typename Func>
+ using ListenerImpl = detail::ListenerImpl<Target, Func, ArgType<Es>...>;
+
+ template <typename Method>
+ using TakeArgs = detail::TakeArgs<Method>;
+
+ void PruneListeners() {
+ mListeners.RemoveElementsBy(
+ [](const auto& listener) { return listener->IsRevoked(); });
+ }
+
+ template <typename Target, typename Function>
+ MediaEventListener ConnectInternal(Target* aTarget, Function&& aFunction) {
+ MutexAutoLock lock(mMutex);
+ PruneListeners();
+ MOZ_ASSERT(Lp == ListenerPolicy::NonExclusive || mListeners.IsEmpty());
+ auto l = mListeners.AppendElement();
+ *l = new ListenerImpl<Target, Function>(aTarget,
+ std::forward<Function>(aFunction));
+ return MediaEventListener(*l);
+ }
+
+ // |Method| takes one or more arguments.
+ template <typename Target, typename This, typename Method>
+ std::enable_if_t<TakeArgs<Method>::value, MediaEventListener> ConnectInternal(
+ Target* aTarget, This* aThis, Method aMethod) {
+ detail::RawPtr<This> thiz(aThis);
+ return ConnectInternal(aTarget, [=](ArgType<Es>&&... aEvents) {
+ (thiz.get()->*aMethod)(std::move(aEvents)...);
+ });
+ }
+
+ // |Method| takes no arguments. Don't bother passing the event data.
+ template <typename Target, typename This, typename Method>
+ std::enable_if_t<!TakeArgs<Method>::value, MediaEventListener>
+ ConnectInternal(Target* aTarget, This* aThis, Method aMethod) {
+ detail::RawPtr<This> thiz(aThis);
+ return ConnectInternal(aTarget, [=]() { (thiz.get()->*aMethod)(); });
+ }
+
+ public:
+ /**
+ * Register a function to receive notifications from the event source.
+ *
+ * @param aTarget The target thread on which the function will run.
+ * @param aFunction A function to be called on the target thread. The function
+ * parameter must be convertible from |EventType|.
+ * @return An object used to disconnect from the event source.
+ */
+ template <typename Function>
+ MediaEventListener Connect(AbstractThread* aTarget, Function&& aFunction) {
+ return ConnectInternal(aTarget, std::forward<Function>(aFunction));
+ }
+
+ template <typename Function>
+ MediaEventListener Connect(nsIEventTarget* aTarget, Function&& aFunction) {
+ return ConnectInternal(aTarget, std::forward<Function>(aFunction));
+ }
+
+ /**
+ * As above.
+ *
+ * Note we deliberately keep a weak reference to |aThis| in order not to
+ * change its lifetime. This is because notifications are dispatched
+ * asynchronously and removing a listener doesn't always break the reference
+ * cycle for the pending event could still hold a reference to |aThis|.
+ *
+ * The caller must call MediaEventListener::Disconnect() to avoid dangling
+ * pointers.
+ */
+ template <typename This, typename Method>
+ MediaEventListener Connect(AbstractThread* aTarget, This* aThis,
+ Method aMethod) {
+ return ConnectInternal(aTarget, aThis, aMethod);
+ }
+
+ template <typename This, typename Method>
+ MediaEventListener Connect(nsIEventTarget* aTarget, This* aThis,
+ Method aMethod) {
+ return ConnectInternal(aTarget, aThis, aMethod);
+ }
+
+ protected:
+ MediaEventSourceImpl() : mMutex("MediaEventSourceImpl::mMutex") {}
+
+ template <typename... Ts>
+ void NotifyInternal(Ts&&... aEvents) {
+ MutexAutoLock lock(mMutex);
+ int32_t last = static_cast<int32_t>(mListeners.Length()) - 1;
+ for (int32_t i = last; i >= 0; --i) {
+ auto&& l = mListeners[i];
+ // Remove disconnected listeners.
+ // It is not optimal but is simple and works well.
+ if (l->IsRevoked()) {
+ mListeners.RemoveElementAt(i);
+ continue;
+ }
+ l->Dispatch(std::forward<Ts>(aEvents)...);
+ }
+ }
+
+ private:
+ Mutex mMutex MOZ_UNANNOTATED;
+ nsTArray<RefPtr<Listener>> mListeners;
+};
+
+template <typename... Es>
+using MediaEventSource =
+ MediaEventSourceImpl<ListenerPolicy::NonExclusive, Es...>;
+
+template <typename... Es>
+using MediaEventSourceExc =
+ MediaEventSourceImpl<ListenerPolicy::Exclusive, Es...>;
+
+/**
+ * A class to separate the interface of event subject (MediaEventSource)
+ * and event publisher. Mostly used as a member variable to publish events
+ * to the listeners.
+ */
+template <typename... Es>
+class MediaEventProducer : public MediaEventSource<Es...> {
+ public:
+ template <typename... Ts>
+ void Notify(Ts&&... aEvents) {
+ // Pass lvalues to prevent move in NonExclusive mode.
+ this->NotifyInternal(aEvents...);
+ }
+};
+
+/**
+ * Specialization for void type. A dummy bool is passed to NotifyInternal
+ * since there is no way to pass a void value.
+ */
+template <>
+class MediaEventProducer<void> : public MediaEventSource<void> {
+ public:
+ void Notify() { this->NotifyInternal(true /* dummy */); }
+};
+
+/**
+ * A producer allowing at most one listener.
+ */
+template <typename... Es>
+class MediaEventProducerExc : public MediaEventSourceExc<Es...> {
+ public:
+ template <typename... Ts>
+ void Notify(Ts&&... aEvents) {
+ this->NotifyInternal(std::forward<Ts>(aEvents)...);
+ }
+};
+
+/**
+ * A class that facilitates forwarding MediaEvents from multiple sources of the
+ * same type into a single source.
+ *
+ * Lifetimes are convenient. A forwarded source is disconnected either by
+ * the source itself going away, or the forwarder being destroyed.
+ *
+ * Not threadsafe. The caller is responsible for calling Forward() in a
+ * threadsafe manner.
+ */
+template <typename... Es>
+class MediaEventForwarder : public MediaEventSource<Es...> {
+ public:
+ template <typename T>
+ using ArgType = typename detail::EventTypeTraits<T>::ArgType;
+
+ explicit MediaEventForwarder(nsCOMPtr<nsISerialEventTarget> aEventTarget)
+ : mEventTarget(std::move(aEventTarget)) {}
+
+ MediaEventForwarder(MediaEventForwarder&& aOther)
+ : mEventTarget(aOther.mEventTarget),
+ mListeners(std::move(aOther.mListeners)) {}
+
+ ~MediaEventForwarder() { MOZ_ASSERT(mListeners.IsEmpty()); }
+
+ MediaEventForwarder& operator=(MediaEventForwarder&& aOther) {
+ MOZ_RELEASE_ASSERT(mEventTarget == aOther.mEventTarget);
+ MOZ_ASSERT(mListeners.IsEmpty());
+ mListeners = std::move(aOther.mListeners);
+ }
+
+ void Forward(MediaEventSource<Es...>& aSource) {
+ // Forwarding a rawptr `this` here is fine, since DisconnectAll disconnect
+ // all mListeners synchronously and prevents this handler from running.
+ mListeners.AppendElement(
+ aSource.Connect(mEventTarget, [this](ArgType<Es>&&... aEvents) {
+ this->NotifyInternal(std::forward<ArgType<Es>...>(aEvents)...);
+ }));
+ }
+
+ template <typename Function>
+ void ForwardIf(MediaEventSource<Es...>& aSource, Function&& aFunction) {
+ // Forwarding a rawptr `this` here is fine, since DisconnectAll disconnect
+ // all mListeners synchronously and prevents this handler from running.
+ mListeners.AppendElement(aSource.Connect(
+ mEventTarget, [this, func = aFunction](ArgType<Es>&&... aEvents) {
+ if (!func()) {
+ return;
+ }
+ this->NotifyInternal(std::forward<ArgType<Es>...>(aEvents)...);
+ }));
+ }
+
+ void DisconnectAll() {
+ for (auto& l : mListeners) {
+ l.Disconnect();
+ }
+ mListeners.Clear();
+ }
+
+ private:
+ const nsCOMPtr<nsISerialEventTarget> mEventTarget;
+ nsTArray<MediaEventListener> mListeners;
+};
+
+} // namespace mozilla
+
+#endif // MediaEventSource_h_
diff --git a/dom/media/MediaFormatReader.cpp b/dom/media/MediaFormatReader.cpp
new file mode 100644
index 0000000000..5af5027012
--- /dev/null
+++ b/dom/media/MediaFormatReader.cpp
@@ -0,0 +1,3432 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaFormatReader.h"
+
+#include <algorithm>
+#include <map>
+#include <queue>
+
+#include "AllocationPolicy.h"
+#ifdef MOZ_AV1
+# include "AOMDecoder.h"
+#endif
+#include "DecoderBenchmark.h"
+#include "MediaData.h"
+#include "MediaDataDecoderProxy.h"
+#include "MediaInfo.h"
+#include "MP4Decoder.h"
+#include "PDMFactory.h"
+#include "PerformanceRecorder.h"
+#include "VideoFrameContainer.h"
+#include "VideoUtils.h"
+#include "VPXDecoder.h"
+#include "mozilla/AbstractThread.h"
+#include "mozilla/CDMProxy.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/NotNull.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/ProfilerMarkers.h"
+#include "mozilla/SharedThreadPool.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/TaskQueue.h"
+#include "mozilla/Unused.h"
+#include "nsContentUtils.h"
+#include "nsPrintfCString.h"
+#include "nsTHashSet.h"
+
+using namespace mozilla::media;
+
+static mozilla::LazyLogModule sFormatDecoderLog("MediaFormatReader");
+mozilla::LazyLogModule gMediaDemuxerLog("MediaDemuxer");
+
+#define LOG(arg, ...) \
+ DDMOZ_LOG(sFormatDecoderLog, mozilla::LogLevel::Debug, "::%s: " arg, \
+ __func__, ##__VA_ARGS__)
+#define LOGV(arg, ...) \
+ DDMOZ_LOG(sFormatDecoderLog, mozilla::LogLevel::Verbose, "::%s: " arg, \
+ __func__, ##__VA_ARGS__)
+
+#define NS_DispatchToMainThread(...) CompileError_UseAbstractMainThreadInstead
+
+namespace mozilla {
+
+typedef void* MediaDataDecoderID;
+
+/**
+ * This class tracks shutdown promises to ensure all decoders are shut down
+ * completely before MFR continues the rest of the shutdown procedure.
+ */
+class MediaFormatReader::ShutdownPromisePool {
+ public:
+ ShutdownPromisePool()
+ : mOnShutdownComplete(new ShutdownPromise::Private(__func__)) {}
+
+ // Return a promise which will be resolved when all the tracking promises
+ // are resolved. Note no more promises should be added for tracking once
+ // this function is called.
+ RefPtr<ShutdownPromise> Shutdown();
+
+ // Track a shutdown promise.
+ void Track(RefPtr<ShutdownPromise> aPromise);
+
+ // Shut down a decoder and track its shutdown promise.
+ void ShutdownDecoder(already_AddRefed<MediaDataDecoder> aDecoder) {
+ Track(RefPtr<MediaDataDecoder>(aDecoder)->Shutdown());
+ }
+
+ private:
+ bool mShutdown = false;
+ const RefPtr<ShutdownPromise::Private> mOnShutdownComplete;
+ nsTHashSet<RefPtr<ShutdownPromise>> mPromises;
+};
+
+RefPtr<ShutdownPromise> MediaFormatReader::ShutdownPromisePool::Shutdown() {
+ MOZ_DIAGNOSTIC_ASSERT(!mShutdown);
+ mShutdown = true;
+ if (mPromises.Count() == 0) {
+ mOnShutdownComplete->Resolve(true, __func__);
+ }
+ return mOnShutdownComplete;
+}
+
+void MediaFormatReader::ShutdownPromisePool::Track(
+ RefPtr<ShutdownPromise> aPromise) {
+ MOZ_DIAGNOSTIC_ASSERT(!mShutdown);
+ MOZ_DIAGNOSTIC_ASSERT(!mPromises.Contains(aPromise));
+ mPromises.Insert(aPromise);
+ aPromise->Then(AbstractThread::GetCurrent(), __func__, [aPromise, this]() {
+ MOZ_DIAGNOSTIC_ASSERT(mPromises.Contains(aPromise));
+ mPromises.Remove(aPromise);
+ if (mShutdown && mPromises.Count() == 0) {
+ mOnShutdownComplete->Resolve(true, __func__);
+ }
+ });
+}
+
+void MediaFormatReader::DecoderData::ShutdownDecoder() {
+ MOZ_ASSERT(mOwner->OnTaskQueue());
+
+ MutexAutoLock lock(mMutex);
+
+ if (!mDecoder) {
+ // No decoder to shut down.
+ return;
+ }
+
+ if (mFlushing) {
+ // Flush is is in action. Shutdown will be initiated after flush completes.
+ MOZ_DIAGNOSTIC_ASSERT(mShutdownPromise);
+ mOwner->mShutdownPromisePool->Track(mShutdownPromise->Ensure(__func__));
+ // The order of decoder creation and shutdown is handled by LocalAllocPolicy
+ // and ShutdownPromisePool. MFR can now reset these members to a fresh state
+ // and be ready to create new decoders again without explicitly waiting for
+ // flush/shutdown to complete.
+ mShutdownPromise = nullptr;
+ mFlushing = false;
+ } else {
+ // No flush is in action. We can shut down the decoder now.
+ mOwner->mShutdownPromisePool->Track(mDecoder->Shutdown());
+ }
+
+ // mShutdownPromisePool will handle the order of decoder shutdown so
+ // we can forget mDecoder and be ready to create a new one.
+ mDecoder = nullptr;
+ mDescription = "shutdown"_ns;
+ mHasReportedVideoHardwareSupportTelemtry = false;
+ mOwner->ScheduleUpdate(mType == MediaData::Type::AUDIO_DATA
+ ? TrackType::kAudioTrack
+ : TrackType::kVideoTrack);
+}
+
+void MediaFormatReader::DecoderData::Flush() {
+ AUTO_PROFILER_LABEL("MediaFormatReader::Flush", MEDIA_PLAYBACK);
+ MOZ_ASSERT(mOwner->OnTaskQueue());
+
+ if (mFlushing || mFlushed) {
+ // Flush still pending or already flushed, nothing more to do.
+ return;
+ }
+ mDecodeRequest.DisconnectIfExists();
+ mDrainRequest.DisconnectIfExists();
+ mDrainState = DrainState::None;
+ CancelWaitingForKey();
+ mOutput.Clear();
+ mNumSamplesInput = 0;
+ mNumSamplesOutput = 0;
+ mSizeOfQueue = 0;
+ if (mDecoder) {
+ TrackType type = mType == MediaData::Type::AUDIO_DATA
+ ? TrackType::kAudioTrack
+ : TrackType::kVideoTrack;
+ mFlushing = true;
+ MOZ_DIAGNOSTIC_ASSERT(!mShutdownPromise);
+ mShutdownPromise = new SharedShutdownPromiseHolder();
+ RefPtr<SharedShutdownPromiseHolder> p = mShutdownPromise;
+ RefPtr<MediaDataDecoder> d = mDecoder;
+ DDLOGEX2("MediaFormatReader::DecoderData", this, DDLogCategory::Log,
+ "flushing", DDNoValue{});
+ mDecoder->Flush()->Then(
+ mOwner->OwnerThread(), __func__,
+ [type, this, p, d]() {
+ AUTO_PROFILER_LABEL("MediaFormatReader::Flush:Resolved",
+ MEDIA_PLAYBACK);
+ DDLOGEX2("MediaFormatReader::DecoderData", this, DDLogCategory::Log,
+ "flushed", DDNoValue{});
+ if (!p->IsEmpty()) {
+ // Shutdown happened before flush completes.
+ // Let's continue to shut down the decoder. Note
+ // we don't access |this| because this decoder
+ // is no longer managed by MFR::DecoderData.
+ d->Shutdown()->ChainTo(p->Steal(), __func__);
+ return;
+ }
+ mFlushing = false;
+ mShutdownPromise = nullptr;
+ mOwner->ScheduleUpdate(type);
+ },
+ [type, this, p, d](const MediaResult& aError) {
+ AUTO_PROFILER_LABEL("MediaFormatReader::Flush:Rejected",
+ MEDIA_PLAYBACK);
+ DDLOGEX2("MediaFormatReader::DecoderData", this, DDLogCategory::Log,
+ "flush_error", aError);
+ if (!p->IsEmpty()) {
+ d->Shutdown()->ChainTo(p->Steal(), __func__);
+ return;
+ }
+ mFlushing = false;
+ mShutdownPromise = nullptr;
+ mOwner->NotifyError(type, aError);
+ });
+ }
+ mFlushed = true;
+}
+
+class MediaFormatReader::DecoderFactory {
+ using InitPromise = MediaDataDecoder::InitPromise;
+ using TokenPromise = AllocPolicy::Promise;
+ using Token = AllocPolicy::Token;
+ using CreateDecoderPromise = PlatformDecoderModule::CreateDecoderPromise;
+
+ public:
+ explicit DecoderFactory(MediaFormatReader* aOwner)
+ : mAudio(aOwner->mAudio, TrackInfo::kAudioTrack, aOwner->OwnerThread()),
+ mVideo(aOwner->mVideo, TrackInfo::kVideoTrack, aOwner->OwnerThread()),
+ mOwner(WrapNotNull(aOwner)) {
+ DecoderDoctorLogger::LogConstruction("MediaFormatReader::DecoderFactory",
+ this);
+ DecoderDoctorLogger::LinkParentAndChild(
+ aOwner, "decoder factory", "MediaFormatReader::DecoderFactory", this);
+ }
+
+ ~DecoderFactory() {
+ DecoderDoctorLogger::LogDestruction("MediaFormatReader::DecoderFactory",
+ this);
+ }
+
+ void CreateDecoder(TrackType aTrack);
+
+ // Shutdown any decoder pending initialization and reset mAudio/mVideo to its
+ // pristine state so CreateDecoder() is ready to be called again immediately.
+ void ShutdownDecoder(TrackType aTrack) {
+ MOZ_ASSERT(aTrack == TrackInfo::kAudioTrack ||
+ aTrack == TrackInfo::kVideoTrack);
+ auto& data = aTrack == TrackInfo::kAudioTrack ? mAudio : mVideo;
+ data.mPolicy->Cancel();
+ data.mTokenRequest.DisconnectIfExists();
+ if (data.mLiveToken) {
+ // We haven't completed creation of the decoder, and it hasn't been
+ // initialised yet.
+ data.mLiveToken = nullptr;
+ // The decoder will be shutdown as soon as it's available and tracked by
+ // the ShutdownPromisePool.
+ mOwner->mShutdownPromisePool->Track(data.mCreateDecoderPromise->Then(
+ mOwner->mTaskQueue, __func__,
+ [](CreateDecoderPromise::ResolveOrRejectValue&& aResult) {
+ if (aResult.IsReject()) {
+ return ShutdownPromise::CreateAndResolve(true, __func__);
+ }
+ return aResult.ResolveValue()->Shutdown();
+ }));
+ // Free the token to leave room for a new decoder.
+ data.mToken = nullptr;
+ }
+ data.mInitRequest.DisconnectIfExists();
+ if (data.mDecoder) {
+ mOwner->mShutdownPromisePool->ShutdownDecoder(data.mDecoder.forget());
+ }
+ data.mStage = Stage::None;
+ MOZ_ASSERT(!data.mToken);
+ }
+
+ private:
+ enum class Stage : int8_t { None, WaitForToken, CreateDecoder, WaitForInit };
+
+ struct Data {
+ Data(DecoderData& aOwnerData, TrackType aTrack, TaskQueue* aThread)
+ : mOwnerData(aOwnerData),
+ mTrack(aTrack),
+ mPolicy(new SingleAllocPolicy(aTrack, aThread)) {}
+ DecoderData& mOwnerData;
+ const TrackType mTrack;
+ RefPtr<SingleAllocPolicy> mPolicy;
+ Stage mStage = Stage::None;
+ RefPtr<Token> mToken;
+ RefPtr<MediaDataDecoder> mDecoder;
+ MozPromiseRequestHolder<TokenPromise> mTokenRequest;
+ struct DecoderCancelled : public SupportsWeakPtr {
+ NS_INLINE_DECL_REFCOUNTING_ONEVENTTARGET(DecoderCancelled)
+ private:
+ ~DecoderCancelled() = default;
+ };
+ // Set when decoder is about to be created. If cleared before the decoder
+ // creation promise is resolved; it indicates that Shutdown() was called and
+ // further processing such as initialization should stop.
+ RefPtr<DecoderCancelled> mLiveToken;
+ RefPtr<CreateDecoderPromise> mCreateDecoderPromise;
+ MozPromiseRequestHolder<InitPromise> mInitRequest;
+ } mAudio, mVideo;
+
+ void RunStage(Data& aData);
+ void DoCreateDecoder(Data& aData);
+ void DoInitDecoder(Data& aData);
+
+ // guaranteed to be valid by the owner.
+ const NotNull<MediaFormatReader*> mOwner;
+};
+
+void MediaFormatReader::DecoderFactory::CreateDecoder(TrackType aTrack) {
+ MOZ_ASSERT(aTrack == TrackInfo::kAudioTrack ||
+ aTrack == TrackInfo::kVideoTrack);
+ RunStage(aTrack == TrackInfo::kAudioTrack ? mAudio : mVideo);
+}
+
+void MediaFormatReader::DecoderFactory::RunStage(Data& aData) {
+ switch (aData.mStage) {
+ case Stage::None: {
+ MOZ_ASSERT(!aData.mToken);
+ aData.mPolicy->Alloc()
+ ->Then(
+ mOwner->OwnerThread(), __func__,
+ [this, &aData](RefPtr<Token> aToken) {
+ aData.mTokenRequest.Complete();
+ aData.mToken = std::move(aToken);
+ aData.mStage = Stage::CreateDecoder;
+ RunStage(aData);
+ },
+ [&aData]() {
+ aData.mTokenRequest.Complete();
+ aData.mStage = Stage::None;
+ })
+ ->Track(aData.mTokenRequest);
+ aData.mStage = Stage::WaitForToken;
+ break;
+ }
+
+ case Stage::WaitForToken: {
+ MOZ_ASSERT(!aData.mToken);
+ MOZ_ASSERT(aData.mTokenRequest.Exists());
+ break;
+ }
+
+ case Stage::CreateDecoder: {
+ MOZ_ASSERT(aData.mToken);
+ MOZ_ASSERT(!aData.mDecoder);
+ MOZ_ASSERT(!aData.mInitRequest.Exists());
+
+ DoCreateDecoder(aData);
+ aData.mStage = Stage::WaitForInit;
+ break;
+ }
+
+ case Stage::WaitForInit: {
+ MOZ_ASSERT((aData.mDecoder && aData.mInitRequest.Exists()) ||
+ aData.mLiveToken);
+ break;
+ }
+ }
+}
+
+void MediaFormatReader::DecoderFactory::DoCreateDecoder(Data& aData) {
+ AUTO_PROFILER_LABEL("DecoderFactory::DoCreateDecoder", MEDIA_PLAYBACK);
+ auto& ownerData = aData.mOwnerData;
+ auto& decoder = mOwner->GetDecoderData(aData.mTrack);
+
+ RefPtr<PDMFactory> platform = new PDMFactory();
+ if (decoder.IsEncrypted()) {
+ MOZ_ASSERT(mOwner->mCDMProxy);
+ platform->SetCDMProxy(mOwner->mCDMProxy);
+ }
+
+ RefPtr<PlatformDecoderModule::CreateDecoderPromise> p;
+ MediaFormatReader* owner = mOwner;
+ auto onWaitingForKeyEvent =
+ [owner = ThreadSafeWeakPtr<MediaFormatReader>(owner)]() {
+ RefPtr<MediaFormatReader> mfr(owner);
+ MOZ_DIAGNOSTIC_ASSERT(mfr, "The MediaFormatReader didn't wait for us");
+ return mfr ? &mfr->OnTrackWaitingForKeyProducer() : nullptr;
+ };
+
+ switch (aData.mTrack) {
+ case TrackInfo::kAudioTrack: {
+ p = platform->CreateDecoder(
+ {*ownerData.GetCurrentInfo()->GetAsAudioInfo(), mOwner->mCrashHelper,
+ CreateDecoderParams::UseNullDecoder(ownerData.mIsNullDecode),
+ TrackInfo::kAudioTrack, std::move(onWaitingForKeyEvent),
+ mOwner->mMediaEngineId, mOwner->mTrackingId});
+ break;
+ }
+
+ case TrackType::kVideoTrack: {
+ // Decoders use the layers backend to decide if they can use hardware
+ // decoding, so specify LAYERS_NONE if we want to forcibly disable it.
+ using Option = CreateDecoderParams::Option;
+ using OptionSet = CreateDecoderParams::OptionSet;
+
+ p = platform->CreateDecoder(
+ {*ownerData.GetCurrentInfo()->GetAsVideoInfo(),
+ mOwner->mKnowsCompositor, mOwner->GetImageContainer(),
+ mOwner->mCrashHelper,
+ CreateDecoderParams::UseNullDecoder(ownerData.mIsNullDecode),
+ TrackType::kVideoTrack, std::move(onWaitingForKeyEvent),
+ CreateDecoderParams::VideoFrameRate(ownerData.mMeanRate.Mean()),
+ OptionSet(ownerData.mHardwareDecodingDisabled
+ ? Option::HardwareDecoderNotAllowed
+ : Option::Default),
+ mOwner->mMediaEngineId, mOwner->mTrackingId});
+ break;
+ }
+
+ default:
+ p = PlatformDecoderModule::CreateDecoderPromise::CreateAndReject(
+ NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
+ }
+
+ aData.mLiveToken = MakeRefPtr<Data::DecoderCancelled>();
+
+ aData.mCreateDecoderPromise = p->Then(
+ mOwner->OwnerThread(), __func__,
+ [this, &aData, &ownerData, live = WeakPtr{aData.mLiveToken},
+ owner = ThreadSafeWeakPtr<MediaFormatReader>(owner)](
+ RefPtr<MediaDataDecoder>&& aDecoder) {
+ if (!live) {
+ return CreateDecoderPromise::CreateAndResolve(std::move(aDecoder),
+ __func__);
+ }
+ aData.mLiveToken = nullptr;
+ aData.mDecoder = new MediaDataDecoderProxy(
+ aDecoder.forget(), do_AddRef(ownerData.mTaskQueue.get()));
+ aData.mDecoder = new AllocationWrapper(aData.mDecoder.forget(),
+ aData.mToken.forget());
+ DecoderDoctorLogger::LinkParentAndChild(
+ aData.mDecoder.get(), "decoder",
+ "MediaFormatReader::DecoderFactory", this);
+
+ DoInitDecoder(aData);
+
+ return CreateDecoderPromise::CreateAndResolve(aData.mDecoder, __func__);
+ },
+ [this, &aData,
+ live = WeakPtr{aData.mLiveToken}](const MediaResult& aError) {
+ NS_WARNING("Error constructing decoders");
+ if (!live) {
+ return CreateDecoderPromise::CreateAndReject(aError, __func__);
+ }
+ aData.mLiveToken = nullptr;
+ aData.mToken = nullptr;
+ aData.mStage = Stage::None;
+ aData.mOwnerData.mDescription = aError.Description();
+ DDLOGEX2("MediaFormatReader::DecoderFactory", this, DDLogCategory::Log,
+ "create_decoder_error", aError);
+ mOwner->NotifyError(aData.mTrack, aError);
+
+ return CreateDecoderPromise::CreateAndReject(aError, __func__);
+ });
+}
+
+void MediaFormatReader::DecoderFactory::DoInitDecoder(Data& aData) {
+ AUTO_PROFILER_LABEL("DecoderFactory::DoInitDecoder", MEDIA_PLAYBACK);
+ auto& ownerData = aData.mOwnerData;
+
+ DDLOGEX2("MediaFormatReader::DecoderFactory", this, DDLogCategory::Log,
+ "initialize_decoder", DDNoValue{});
+ aData.mDecoder->Init()
+ ->Then(
+ mOwner->OwnerThread(), __func__,
+ [this, &aData, &ownerData](TrackType aTrack) {
+ AUTO_PROFILER_LABEL("DecoderFactory::DoInitDecoder:Resolved",
+ MEDIA_PLAYBACK);
+ aData.mInitRequest.Complete();
+ aData.mStage = Stage::None;
+ MutexAutoLock lock(ownerData.mMutex);
+ ownerData.mDecoder = std::move(aData.mDecoder);
+ ownerData.mDescription = ownerData.mDecoder->GetDescriptionName();
+ DDLOGEX2("MediaFormatReader::DecoderFactory", this,
+ DDLogCategory::Log, "decoder_initialized", DDNoValue{});
+ DecoderDoctorLogger::LinkParentAndChild(
+ "MediaFormatReader::DecoderData", &ownerData, "decoder",
+ ownerData.mDecoder.get());
+ mOwner->SetVideoDecodeThreshold();
+ mOwner->ScheduleUpdate(aTrack);
+ if (aTrack == TrackInfo::kVideoTrack) {
+ DecoderBenchmark::CheckVersion(
+ ownerData.GetCurrentInfo()->mMimeType);
+ }
+ if (aTrack == TrackInfo::kAudioTrack) {
+ ownerData.mProcessName = ownerData.mDecoder->GetProcessName();
+ ownerData.mCodecName = ownerData.mDecoder->GetCodecName();
+ }
+ },
+ [this, &aData, &ownerData](const MediaResult& aError) {
+ AUTO_PROFILER_LABEL("DecoderFactory::DoInitDecoder:Rejected",
+ MEDIA_PLAYBACK);
+ aData.mInitRequest.Complete();
+ MOZ_RELEASE_ASSERT(!ownerData.mDecoder,
+ "Can't have a decoder already set");
+ aData.mStage = Stage::None;
+ mOwner->mShutdownPromisePool->ShutdownDecoder(
+ aData.mDecoder.forget());
+ DDLOGEX2("MediaFormatReader::DecoderFactory", this,
+ DDLogCategory::Log, "initialize_decoder_error", aError);
+ mOwner->NotifyError(aData.mTrack, aError);
+ })
+ ->Track(aData.mInitRequest);
+}
+
+// DemuxerProxy ensures that the original main demuxer is only ever accessed
+// via its own dedicated task queue.
+// This ensure that the reader's taskqueue will never blocked while a demuxer
+// is itself blocked attempting to access the MediaCache or the MediaResource.
+class MediaFormatReader::DemuxerProxy {
+ using TrackType = TrackInfo::TrackType;
+ class Wrapper;
+
+ public:
+ explicit DemuxerProxy(MediaDataDemuxer* aDemuxer)
+ : mTaskQueue(TaskQueue::Create(
+ GetMediaThreadPool(MediaThreadType::PLATFORM_DECODER),
+ "DemuxerProxy::mTaskQueue")),
+ mData(new Data(aDemuxer)) {
+ MOZ_COUNT_CTOR(DemuxerProxy);
+ }
+
+ MOZ_COUNTED_DTOR(DemuxerProxy)
+
+ RefPtr<ShutdownPromise> Shutdown() {
+ RefPtr<Data> data = std::move(mData);
+ return InvokeAsync(mTaskQueue, __func__, [data]() {
+ // We need to clear our reference to the demuxer now. So that in the event
+ // the init promise wasn't resolved, such as what can happen with the
+ // mediasource demuxer that is waiting on more data, it will force the
+ // init promise to be rejected.
+ data->mDemuxer = nullptr;
+ data->mAudioDemuxer = nullptr;
+ data->mVideoDemuxer = nullptr;
+ return ShutdownPromise::CreateAndResolve(true, __func__);
+ });
+ }
+
+ RefPtr<MediaDataDemuxer::InitPromise> Init();
+
+ Wrapper* GetTrackDemuxer(TrackType aTrack, uint32_t aTrackNumber) {
+ MOZ_RELEASE_ASSERT(mData && mData->mInitDone);
+
+ switch (aTrack) {
+ case TrackInfo::kAudioTrack:
+ return mData->mAudioDemuxer;
+ case TrackInfo::kVideoTrack:
+ return mData->mVideoDemuxer;
+ default:
+ return nullptr;
+ }
+ }
+
+ uint32_t GetNumberTracks(TrackType aTrack) const {
+ MOZ_RELEASE_ASSERT(mData && mData->mInitDone);
+
+ switch (aTrack) {
+ case TrackInfo::kAudioTrack:
+ return mData->mNumAudioTrack;
+ case TrackInfo::kVideoTrack:
+ return mData->mNumVideoTrack;
+ default:
+ return 0;
+ }
+ }
+
+ bool IsSeekable() const {
+ MOZ_RELEASE_ASSERT(mData && mData->mInitDone);
+
+ return mData->mSeekable;
+ }
+
+ bool IsSeekableOnlyInBufferedRanges() const {
+ MOZ_RELEASE_ASSERT(mData && mData->mInitDone);
+
+ return mData->mSeekableOnlyInBufferedRange;
+ }
+
+ UniquePtr<EncryptionInfo> GetCrypto() const {
+ MOZ_RELEASE_ASSERT(mData && mData->mInitDone);
+
+ if (!mData->mCrypto) {
+ return nullptr;
+ }
+ auto crypto = MakeUnique<EncryptionInfo>();
+ *crypto = *mData->mCrypto;
+ return crypto;
+ }
+
+ RefPtr<NotifyDataArrivedPromise> NotifyDataArrived();
+
+ bool ShouldComputeStartTime() const {
+ MOZ_RELEASE_ASSERT(mData && mData->mInitDone);
+
+ return mData->mShouldComputeStartTime;
+ }
+
+ private:
+ const RefPtr<TaskQueue> mTaskQueue;
+ struct Data {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(Data)
+
+ explicit Data(MediaDataDemuxer* aDemuxer)
+ : mInitDone(false), mDemuxer(aDemuxer) {}
+
+ Atomic<bool> mInitDone;
+ // Only ever accessed over mTaskQueue once.
+ RefPtr<MediaDataDemuxer> mDemuxer;
+ // Only accessed once InitPromise has been resolved and immutable after.
+ // So we can safely access them without the use of the mutex.
+ uint32_t mNumAudioTrack = 0;
+ RefPtr<Wrapper> mAudioDemuxer;
+ uint32_t mNumVideoTrack = 0;
+ RefPtr<Wrapper> mVideoDemuxer;
+ bool mSeekable = false;
+ bool mSeekableOnlyInBufferedRange = false;
+ bool mShouldComputeStartTime = true;
+ UniquePtr<EncryptionInfo> mCrypto;
+
+ private:
+ ~Data() = default;
+ };
+ RefPtr<Data> mData;
+};
+
+class MediaFormatReader::DemuxerProxy::Wrapper : public MediaTrackDemuxer {
+ public:
+ Wrapper(MediaTrackDemuxer* aTrackDemuxer, TaskQueue* aTaskQueue)
+ : mMutex("TrackDemuxer Mutex"),
+ mTaskQueue(aTaskQueue),
+ mGetSamplesMayBlock(aTrackDemuxer->GetSamplesMayBlock()),
+ mInfo(aTrackDemuxer->GetInfo()),
+ mTrackDemuxer(aTrackDemuxer) {
+ DecoderDoctorLogger::LogConstructionAndBase(
+ "MediaFormatReader::DemuxerProxy::Wrapper", this,
+ static_cast<const MediaTrackDemuxer*>(this));
+ DecoderDoctorLogger::LinkParentAndChild(
+ "MediaFormatReader::DemuxerProxy::Wrapper", this, "track demuxer",
+ aTrackDemuxer);
+ }
+
+ UniquePtr<TrackInfo> GetInfo() const override {
+ if (!mInfo) {
+ return nullptr;
+ }
+ return mInfo->Clone();
+ }
+
+ RefPtr<SeekPromise> Seek(const TimeUnit& aTime) override {
+ RefPtr<Wrapper> self = this;
+ return InvokeAsync(
+ mTaskQueue, __func__,
+ [self, aTime]() { return self->mTrackDemuxer->Seek(aTime); })
+ ->Then(
+ mTaskQueue, __func__,
+ [self](const TimeUnit& aTime) {
+ self->UpdateRandomAccessPoint();
+ return SeekPromise::CreateAndResolve(aTime, __func__);
+ },
+ [self](const MediaResult& aError) {
+ self->UpdateRandomAccessPoint();
+ return SeekPromise::CreateAndReject(aError, __func__);
+ });
+ }
+
+ RefPtr<SamplesPromise> GetSamples(int32_t aNumSamples) override {
+ RefPtr<Wrapper> self = this;
+ return InvokeAsync(mTaskQueue, __func__,
+ [self, aNumSamples]() {
+ return self->mTrackDemuxer->GetSamples(aNumSamples);
+ })
+ ->Then(
+ mTaskQueue, __func__,
+ [self](RefPtr<SamplesHolder> aSamples) {
+ self->UpdateRandomAccessPoint();
+ return SamplesPromise::CreateAndResolve(aSamples.forget(),
+ __func__);
+ },
+ [self](const MediaResult& aError) {
+ self->UpdateRandomAccessPoint();
+ return SamplesPromise::CreateAndReject(aError, __func__);
+ });
+ }
+
+ bool GetSamplesMayBlock() const override { return mGetSamplesMayBlock; }
+
+ void Reset() override {
+ RefPtr<Wrapper> self = this;
+ nsresult rv = mTaskQueue->Dispatch(NS_NewRunnableFunction(
+ "MediaFormatReader::DemuxerProxy::Wrapper::Reset",
+ [self]() { self->mTrackDemuxer->Reset(); }));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ }
+
+ nsresult GetNextRandomAccessPoint(TimeUnit* aTime) override {
+ MutexAutoLock lock(mMutex);
+ if (NS_SUCCEEDED(mNextRandomAccessPointResult)) {
+ *aTime = mNextRandomAccessPoint;
+ }
+ return mNextRandomAccessPointResult;
+ }
+
+ RefPtr<SkipAccessPointPromise> SkipToNextRandomAccessPoint(
+ const TimeUnit& aTimeThreshold) override {
+ RefPtr<Wrapper> self = this;
+ return InvokeAsync(
+ mTaskQueue, __func__,
+ [self, aTimeThreshold]() {
+ return self->mTrackDemuxer->SkipToNextRandomAccessPoint(
+ aTimeThreshold);
+ })
+ ->Then(
+ mTaskQueue, __func__,
+ [self](uint32_t aVal) {
+ self->UpdateRandomAccessPoint();
+ return SkipAccessPointPromise::CreateAndResolve(aVal, __func__);
+ },
+ [self](const SkipFailureHolder& aError) {
+ self->UpdateRandomAccessPoint();
+ return SkipAccessPointPromise::CreateAndReject(aError, __func__);
+ });
+ }
+
+ TimeIntervals GetBuffered() override {
+ MutexAutoLock lock(mMutex);
+ return mBuffered;
+ }
+
+ void BreakCycles() override {}
+
+ private:
+ Mutex mMutex MOZ_UNANNOTATED;
+ const RefPtr<TaskQueue> mTaskQueue;
+ const bool mGetSamplesMayBlock;
+ const UniquePtr<TrackInfo> mInfo;
+ // mTrackDemuxer is only ever accessed on demuxer's task queue.
+ RefPtr<MediaTrackDemuxer> mTrackDemuxer;
+ // All following members are protected by mMutex
+ nsresult mNextRandomAccessPointResult = NS_OK;
+ TimeUnit mNextRandomAccessPoint;
+ TimeIntervals mBuffered;
+ friend class DemuxerProxy;
+
+ ~Wrapper() {
+ RefPtr<MediaTrackDemuxer> trackDemuxer = std::move(mTrackDemuxer);
+ nsresult rv = mTaskQueue->Dispatch(NS_NewRunnableFunction(
+ "MediaFormatReader::DemuxerProxy::Wrapper::~Wrapper",
+ [trackDemuxer]() { trackDemuxer->BreakCycles(); }));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ DecoderDoctorLogger::LogDestruction(
+ "MediaFormatReader::DemuxerProxy::Wrapper", this);
+ }
+
+ void UpdateRandomAccessPoint() {
+ MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
+ if (!mTrackDemuxer) {
+ // Detached.
+ return;
+ }
+ MutexAutoLock lock(mMutex);
+ mNextRandomAccessPointResult =
+ mTrackDemuxer->GetNextRandomAccessPoint(&mNextRandomAccessPoint);
+ }
+
+ void UpdateBuffered() {
+ MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
+ if (!mTrackDemuxer) {
+ // Detached.
+ return;
+ }
+ MutexAutoLock lock(mMutex);
+ mBuffered = mTrackDemuxer->GetBuffered();
+ }
+};
+
+RefPtr<MediaDataDemuxer::InitPromise> MediaFormatReader::DemuxerProxy::Init() {
+ AUTO_PROFILER_LABEL("DemuxerProxy::Init", MEDIA_PLAYBACK);
+ using InitPromise = MediaDataDemuxer::InitPromise;
+
+ RefPtr<Data> data = mData;
+ RefPtr<TaskQueue> taskQueue = mTaskQueue;
+ return InvokeAsync(mTaskQueue, __func__,
+ [data, taskQueue]() {
+ if (!data->mDemuxer) {
+ return InitPromise::CreateAndReject(
+ NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ }
+ return data->mDemuxer->Init();
+ })
+ ->Then(
+ taskQueue, __func__,
+ [data, taskQueue]() {
+ AUTO_PROFILER_LABEL("DemuxerProxy::Init:Resolved", MEDIA_PLAYBACK);
+ if (!data->mDemuxer) { // Was shutdown.
+ return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_CANCELED,
+ __func__);
+ }
+ data->mNumAudioTrack =
+ data->mDemuxer->GetNumberTracks(TrackInfo::kAudioTrack);
+ if (data->mNumAudioTrack) {
+ RefPtr<MediaTrackDemuxer> d =
+ data->mDemuxer->GetTrackDemuxer(TrackInfo::kAudioTrack, 0);
+ if (d) {
+ RefPtr<Wrapper> wrapper =
+ new DemuxerProxy::Wrapper(d, taskQueue);
+ wrapper->UpdateBuffered();
+ data->mAudioDemuxer = wrapper;
+ DecoderDoctorLogger::LinkParentAndChild(
+ data->mDemuxer.get(), "decoder factory wrapper",
+ "MediaFormatReader::DecoderFactory::Wrapper",
+ wrapper.get());
+ }
+ }
+ data->mNumVideoTrack =
+ data->mDemuxer->GetNumberTracks(TrackInfo::kVideoTrack);
+ if (data->mNumVideoTrack) {
+ RefPtr<MediaTrackDemuxer> d =
+ data->mDemuxer->GetTrackDemuxer(TrackInfo::kVideoTrack, 0);
+ if (d) {
+ RefPtr<Wrapper> wrapper =
+ new DemuxerProxy::Wrapper(d, taskQueue);
+ wrapper->UpdateBuffered();
+ data->mVideoDemuxer = wrapper;
+ DecoderDoctorLogger::LinkParentAndChild(
+ data->mDemuxer.get(), "decoder factory wrapper",
+ "MediaFormatReader::DecoderFactory::Wrapper",
+ wrapper.get());
+ }
+ }
+ data->mCrypto = data->mDemuxer->GetCrypto();
+ data->mSeekable = data->mDemuxer->IsSeekable();
+ data->mSeekableOnlyInBufferedRange =
+ data->mDemuxer->IsSeekableOnlyInBufferedRanges();
+ data->mShouldComputeStartTime =
+ data->mDemuxer->ShouldComputeStartTime();
+ data->mInitDone = true;
+ return InitPromise::CreateAndResolve(NS_OK, __func__);
+ },
+ [](const MediaResult& aError) {
+ return InitPromise::CreateAndReject(aError, __func__);
+ });
+}
+
+RefPtr<MediaFormatReader::NotifyDataArrivedPromise>
+MediaFormatReader::DemuxerProxy::NotifyDataArrived() {
+ RefPtr<Data> data = mData;
+ return InvokeAsync(mTaskQueue, __func__, [data]() {
+ if (!data->mDemuxer) {
+ // Was shutdown.
+ return NotifyDataArrivedPromise::CreateAndReject(
+ NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ }
+ data->mDemuxer->NotifyDataArrived();
+ if (data->mAudioDemuxer) {
+ data->mAudioDemuxer->UpdateBuffered();
+ }
+ if (data->mVideoDemuxer) {
+ data->mVideoDemuxer->UpdateBuffered();
+ }
+ return NotifyDataArrivedPromise::CreateAndResolve(true, __func__);
+ });
+}
+
+MediaFormatReader::MediaFormatReader(MediaFormatReaderInit& aInit,
+ MediaDataDemuxer* aDemuxer)
+ : mTaskQueue(
+ TaskQueue::Create(GetMediaThreadPool(MediaThreadType::SUPERVISOR),
+ "MediaFormatReader::mTaskQueue",
+ /* aSupportsTailDispatch = */ true)),
+ mAudio(this, MediaData::Type::AUDIO_DATA,
+ StaticPrefs::media_audio_max_decode_error()),
+ mVideo(this, MediaData::Type::VIDEO_DATA,
+ StaticPrefs::media_video_max_decode_error()),
+ mWorkingInfoChanged(false, "MediaFormatReader::mWorkingInfoChanged"),
+ mWatchManager(this, OwnerThread()),
+ mIsWatchingWorkingInfo(false),
+ mDemuxer(new DemuxerProxy(aDemuxer)),
+ mDemuxerInitDone(false),
+ mPendingNotifyDataArrived(false),
+ mLastReportedNumDecodedFrames(0),
+ mPreviousDecodedKeyframeTime_us(sNoPreviousDecodedKeyframe),
+ mKnowsCompositor(aInit.mKnowsCompositor),
+ mInitDone(false),
+ mTrackDemuxersMayBlock(false),
+ mSeekScheduled(false),
+ mVideoFrameContainer(aInit.mVideoFrameContainer),
+ mCrashHelper(aInit.mCrashHelper),
+ mDecoderFactory(new DecoderFactory(this)),
+ mShutdownPromisePool(new ShutdownPromisePool()),
+ mBuffered(mTaskQueue, TimeIntervals(),
+ "MediaFormatReader::mBuffered (Canonical)"),
+ mFrameStats(aInit.mFrameStats),
+ mMediaDecoderOwnerID(aInit.mMediaDecoderOwnerID),
+ mTrackingId(std::move(aInit.mTrackingId)) {
+ MOZ_ASSERT(aDemuxer);
+ MOZ_COUNT_CTOR(MediaFormatReader);
+ DDLINKCHILD("audio decoder data", "MediaFormatReader::DecoderDataWithPromise",
+ &mAudio);
+ DDLINKCHILD("video decoder data", "MediaFormatReader::DecoderDataWithPromise",
+ &mVideo);
+ DDLINKCHILD("demuxer", aDemuxer);
+ mOnTrackWaitingForKeyListener = OnTrackWaitingForKey().Connect(
+ mTaskQueue, this, &MediaFormatReader::NotifyWaitingForKey);
+}
+
+MediaFormatReader::~MediaFormatReader() {
+ MOZ_COUNT_DTOR(MediaFormatReader);
+ MOZ_ASSERT(mShutdown);
+}
+
+RefPtr<ShutdownPromise> MediaFormatReader::Shutdown() {
+ MOZ_ASSERT(OnTaskQueue());
+ LOG("");
+
+ mDemuxerInitRequest.DisconnectIfExists();
+ mNotifyDataArrivedPromise.DisconnectIfExists();
+ mMetadataPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ mSeekPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ mSkipRequest.DisconnectIfExists();
+ mSetCDMPromise.RejectIfExists(
+ MediaResult(NS_ERROR_DOM_INVALID_STATE_ERR,
+ "MediaFormatReader is shutting down"),
+ __func__);
+
+ if (mIsWatchingWorkingInfo) {
+ mWatchManager.Unwatch(mWorkingInfoChanged,
+ &MediaFormatReader::NotifyTrackInfoUpdated);
+ }
+ mWatchManager.Shutdown();
+
+ if (mAudio.HasPromise()) {
+ mAudio.RejectPromise(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ }
+ if (mVideo.HasPromise()) {
+ mVideo.RejectPromise(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ }
+
+ if (HasAudio()) {
+ mAudio.ResetDemuxer();
+ mAudio.mTrackDemuxer->BreakCycles();
+ {
+ MutexAutoLock lock(mAudio.mMutex);
+ mAudio.mTrackDemuxer = nullptr;
+ }
+ mAudio.ResetState();
+ ShutdownDecoder(TrackInfo::kAudioTrack);
+ }
+
+ if (HasVideo()) {
+ mVideo.ResetDemuxer();
+ mVideo.mTrackDemuxer->BreakCycles();
+ {
+ MutexAutoLock lock(mVideo.mMutex);
+ mVideo.mTrackDemuxer = nullptr;
+ }
+ mVideo.ResetState();
+ ShutdownDecoder(TrackInfo::kVideoTrack);
+ }
+
+ mShutdownPromisePool->Track(mDemuxer->Shutdown());
+ mDemuxer = nullptr;
+
+ mOnTrackWaitingForKeyListener.Disconnect();
+
+ mShutdown = true;
+ return mShutdownPromisePool->Shutdown()->Then(
+ OwnerThread(), __func__, this, &MediaFormatReader::TearDownDecoders,
+ &MediaFormatReader::TearDownDecoders);
+}
+
+void MediaFormatReader::ShutdownDecoder(TrackType aTrack) {
+ LOGV("%s", TrackTypeToStr(aTrack));
+
+ // Shut down the pending decoder if any.
+ mDecoderFactory->ShutdownDecoder(aTrack);
+
+ auto& decoder = GetDecoderData(aTrack);
+ // Flush the decoder if necessary.
+ decoder.Flush();
+
+ // Shut down the decoder if any.
+ decoder.ShutdownDecoder();
+}
+
+void MediaFormatReader::NotifyDecoderBenchmarkStore() {
+ MOZ_ASSERT(OnTaskQueue());
+ if (!StaticPrefs::media_mediacapabilities_from_database()) {
+ return;
+ }
+ auto& decoder = GetDecoderData(TrackInfo::kVideoTrack);
+ if (decoder.GetCurrentInfo() && decoder.GetCurrentInfo()->GetAsVideoInfo()) {
+ VideoInfo info = *(decoder.GetCurrentInfo()->GetAsVideoInfo());
+ info.SetFrameRate(static_cast<int32_t>(ceil(decoder.mMeanRate.Mean())));
+ mOnStoreDecoderBenchmark.Notify(std::move(info));
+ }
+}
+
+void MediaFormatReader::NotifyTrackInfoUpdated() {
+ MOZ_ASSERT(OnTaskQueue());
+ if (mWorkingInfoChanged) {
+ mWorkingInfoChanged = false;
+
+ VideoInfo videoInfo;
+ AudioInfo audioInfo;
+ {
+ MutexAutoLock lock(mVideo.mMutex);
+ if (HasVideo()) {
+ videoInfo = *mVideo.GetWorkingInfo()->GetAsVideoInfo();
+ }
+ }
+ {
+ MutexAutoLock lock(mAudio.mMutex);
+ if (HasAudio()) {
+ audioInfo = *mAudio.GetWorkingInfo()->GetAsAudioInfo();
+ }
+ }
+
+ mTrackInfoUpdatedEvent.Notify(videoInfo, audioInfo);
+ }
+}
+
+RefPtr<ShutdownPromise> MediaFormatReader::TearDownDecoders() {
+ if (mAudio.mTaskQueue) {
+ mAudio.mTaskQueue->BeginShutdown();
+ mAudio.mTaskQueue->AwaitShutdownAndIdle();
+ mAudio.mTaskQueue = nullptr;
+ }
+ if (mVideo.mTaskQueue) {
+ mVideo.mTaskQueue->BeginShutdown();
+ mVideo.mTaskQueue->AwaitShutdownAndIdle();
+ mVideo.mTaskQueue = nullptr;
+ }
+
+ mDecoderFactory = nullptr;
+ mVideoFrameContainer = nullptr;
+
+ ReleaseResources();
+ mBuffered.DisconnectAll();
+ return mTaskQueue->BeginShutdown();
+}
+
+nsresult MediaFormatReader::Init() {
+ MOZ_ASSERT(NS_IsMainThread(), "Must be on main thread.");
+
+ mAudio.mTaskQueue =
+ TaskQueue::Create(GetMediaThreadPool(MediaThreadType::PLATFORM_DECODER),
+ "MFR::mAudio::mTaskQueue");
+
+ mVideo.mTaskQueue =
+ TaskQueue::Create(GetMediaThreadPool(MediaThreadType::PLATFORM_DECODER),
+ "MFR::mVideo::mTaskQueue");
+
+ return NS_OK;
+}
+
+bool MediaFormatReader::ResolveSetCDMPromiseIfDone(TrackType aTrack) {
+ // When a CDM proxy is set, MFR would shutdown the existing MediaDataDecoder
+ // and would create new one for specific track in the next Update.
+ MOZ_ASSERT(OnTaskQueue());
+
+ if (mSetCDMPromise.IsEmpty()) {
+ return true;
+ }
+
+ MOZ_ASSERT(mCDMProxy);
+ if (mSetCDMForTracks.contains(aTrack)) {
+ mSetCDMForTracks -= aTrack;
+ }
+
+ if (mSetCDMForTracks.isEmpty()) {
+ LOGV("%s : Done ", __func__);
+ mSetCDMPromise.Resolve(/* aIgnored = */ true, __func__);
+ if (HasAudio()) {
+ ScheduleUpdate(TrackInfo::kAudioTrack);
+ }
+ if (HasVideo()) {
+ ScheduleUpdate(TrackInfo::kVideoTrack);
+ }
+ return true;
+ }
+ LOGV("%s : %s track is ready.", __func__, TrackTypeToStr(aTrack));
+ return false;
+}
+
+void MediaFormatReader::PrepareToSetCDMForTrack(TrackType aTrack) {
+ MOZ_ASSERT(OnTaskQueue());
+ LOGV("%s : %s", __func__, TrackTypeToStr(aTrack));
+
+ mSetCDMForTracks += aTrack;
+ if (mCDMProxy) {
+ // An old cdm proxy exists, so detaching old cdm proxy by shutting down
+ // MediaDataDecoder.
+ ShutdownDecoder(aTrack);
+ }
+ ScheduleUpdate(aTrack);
+}
+
+bool MediaFormatReader::IsDecoderWaitingForCDM(TrackType aTrack) {
+ MOZ_ASSERT(OnTaskQueue());
+ return GetDecoderData(aTrack).IsEncrypted() &&
+ mSetCDMForTracks.contains(aTrack) && !mCDMProxy;
+}
+
+RefPtr<SetCDMPromise> MediaFormatReader::SetCDMProxy(CDMProxy* aProxy) {
+ MOZ_ASSERT(OnTaskQueue());
+ LOGV("SetCDMProxy (%p)", aProxy);
+
+ if (mShutdown) {
+ return SetCDMPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_INVALID_STATE_ERR,
+ "MediaFormatReader is shutting down"),
+ __func__);
+ }
+
+ mSetCDMPromise.RejectIfExists(
+ MediaResult(NS_ERROR_DOM_INVALID_STATE_ERR,
+ "Another new CDM proxy is being set."),
+ __func__);
+
+ // Shutdown all decoders as switching CDM proxy indicates that it's
+ // inappropriate for the existing decoders to continue decoding via the old
+ // CDM proxy.
+ if (HasAudio()) {
+ PrepareToSetCDMForTrack(TrackInfo::kAudioTrack);
+ }
+ if (HasVideo()) {
+ PrepareToSetCDMForTrack(TrackInfo::kVideoTrack);
+ }
+
+ mCDMProxy = aProxy;
+
+ if (!mInitDone || mSetCDMForTracks.isEmpty() || !mCDMProxy) {
+ // 1) MFR is not initialized yet or
+ // 2) Demuxer is initialized without active audio and video or
+ // 3) A null cdm proxy is set
+ // the promise can be resolved directly.
+ mSetCDMForTracks.clear();
+ return SetCDMPromise::CreateAndResolve(/* aIgnored = */ true, __func__);
+ }
+
+ RefPtr<SetCDMPromise> p = mSetCDMPromise.Ensure(__func__);
+ return p;
+}
+
+bool MediaFormatReader::IsWaitingOnCDMResource() {
+ MOZ_ASSERT(OnTaskQueue());
+ return IsEncrypted() && !mCDMProxy;
+}
+
+RefPtr<MediaFormatReader::MetadataPromise>
+MediaFormatReader::AsyncReadMetadata() {
+ AUTO_PROFILER_LABEL("MediaFormatReader::AsyncReadMetadata", MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+
+ MOZ_DIAGNOSTIC_ASSERT(mMetadataPromise.IsEmpty());
+
+ if (mInitDone) {
+ // We are returning from dormant.
+ MetadataHolder metadata;
+ metadata.mInfo = MakeUnique<MediaInfo>(mInfo);
+ return MetadataPromise::CreateAndResolve(std::move(metadata), __func__);
+ }
+
+ RefPtr<MetadataPromise> p = mMetadataPromise.Ensure(__func__);
+
+ mDemuxer->Init()
+ ->Then(OwnerThread(), __func__, this,
+ &MediaFormatReader::OnDemuxerInitDone,
+ &MediaFormatReader::OnDemuxerInitFailed)
+ ->Track(mDemuxerInitRequest);
+ return p;
+}
+
+void MediaFormatReader::OnDemuxerInitDone(const MediaResult& aResult) {
+ AUTO_PROFILER_LABEL("MediaFormatReader::OnDemuxerInitDone", MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+ mDemuxerInitRequest.Complete();
+
+ if (NS_FAILED(aResult) && StaticPrefs::media_playback_warnings_as_errors()) {
+ mMetadataPromise.Reject(aResult, __func__);
+ return;
+ }
+
+ mDemuxerInitDone = true;
+
+ UniquePtr<MetadataTags> tags(MakeUnique<MetadataTags>());
+
+ RefPtr<PDMFactory> platform;
+ if (!IsWaitingOnCDMResource()) {
+ platform = new PDMFactory();
+ }
+
+ // To decode, we need valid video and a place to put it.
+ bool videoActive = !!mDemuxer->GetNumberTracks(TrackInfo::kVideoTrack) &&
+ GetImageContainer();
+
+ if (videoActive) {
+ // We currently only handle the first video track.
+ MutexAutoLock lock(mVideo.mMutex);
+ mVideo.mTrackDemuxer = mDemuxer->GetTrackDemuxer(TrackInfo::kVideoTrack, 0);
+ if (!mVideo.mTrackDemuxer) {
+ mMetadataPromise.Reject(NS_ERROR_DOM_MEDIA_METADATA_ERR, __func__);
+ return;
+ }
+
+ UniquePtr<TrackInfo> videoInfo = mVideo.mTrackDemuxer->GetInfo();
+ videoActive = videoInfo && videoInfo->IsValid();
+ if (videoActive) {
+ if (platform && platform->SupportsMimeType(videoInfo->mMimeType) ==
+ media::DecodeSupport::Unsupported) {
+ // We have no decoder for this track. Error.
+ mMetadataPromise.Reject(NS_ERROR_DOM_MEDIA_METADATA_ERR, __func__);
+ return;
+ }
+ mInfo.mVideo = *videoInfo->GetAsVideoInfo();
+ mVideo.mWorkingInfo = MakeUnique<VideoInfo>(mInfo.mVideo);
+ for (const MetadataTag& tag : videoInfo->mTags) {
+ tags->InsertOrUpdate(tag.mKey, tag.mValue);
+ }
+ mWorkingInfoChanged = true;
+ mVideo.mOriginalInfo = std::move(videoInfo);
+ mTrackDemuxersMayBlock |= mVideo.mTrackDemuxer->GetSamplesMayBlock();
+ } else {
+ mVideo.mTrackDemuxer->BreakCycles();
+ mVideo.mTrackDemuxer = nullptr;
+ }
+ }
+
+ bool audioActive = !!mDemuxer->GetNumberTracks(TrackInfo::kAudioTrack);
+ if (audioActive) {
+ MutexAutoLock lock(mAudio.mMutex);
+ mAudio.mTrackDemuxer = mDemuxer->GetTrackDemuxer(TrackInfo::kAudioTrack, 0);
+ if (!mAudio.mTrackDemuxer) {
+ mMetadataPromise.Reject(NS_ERROR_DOM_MEDIA_METADATA_ERR, __func__);
+ return;
+ }
+
+ UniquePtr<TrackInfo> audioInfo = mAudio.mTrackDemuxer->GetInfo();
+ // We actively ignore audio tracks that we know we can't play.
+ audioActive =
+ audioInfo && audioInfo->IsValid() &&
+ (!platform || platform->SupportsMimeType(audioInfo->mMimeType) !=
+ media::DecodeSupport::Unsupported);
+
+ if (audioActive) {
+ mInfo.mAudio = *audioInfo->GetAsAudioInfo();
+ mAudio.mWorkingInfo = MakeUnique<AudioInfo>(mInfo.mAudio);
+ for (const MetadataTag& tag : audioInfo->mTags) {
+ tags->InsertOrUpdate(tag.mKey, tag.mValue);
+ }
+ mWorkingInfoChanged = true;
+ mAudio.mOriginalInfo = std::move(audioInfo);
+ mTrackDemuxersMayBlock |= mAudio.mTrackDemuxer->GetSamplesMayBlock();
+ } else {
+ mAudio.mTrackDemuxer->BreakCycles();
+ mAudio.mTrackDemuxer = nullptr;
+ }
+ }
+
+ UniquePtr<EncryptionInfo> crypto = mDemuxer->GetCrypto();
+ if (crypto && crypto->IsEncrypted()) {
+ // Try and dispatch 'encrypted'. Won't go if ready state still HAVE_NOTHING.
+ for (uint32_t i = 0; i < crypto->mInitDatas.Length(); i++) {
+ mOnEncrypted.Notify(crypto->mInitDatas[i].mInitData,
+ crypto->mInitDatas[i].mType);
+ }
+ mInfo.mCrypto = *crypto;
+ }
+
+ auto videoDuration = HasVideo() ? mInfo.mVideo.mDuration : TimeUnit::Zero();
+ auto audioDuration = HasAudio() ? mInfo.mAudio.mDuration : TimeUnit::Zero();
+
+ // If the duration is 0 on both audio and video, it mMetadataDuration is to be
+ // Nothing(). Duration will use buffered ranges.
+ if (videoDuration.IsPositive() || audioDuration.IsPositive()) {
+ auto duration = std::max(videoDuration, audioDuration);
+ mInfo.mMetadataDuration = Some(duration);
+ }
+
+ mInfo.mMediaSeekable = mDemuxer->IsSeekable();
+ mInfo.mMediaSeekableOnlyInBufferedRanges =
+ mDemuxer->IsSeekableOnlyInBufferedRanges();
+
+ if (!videoActive && !audioActive) {
+ mMetadataPromise.Reject(NS_ERROR_DOM_MEDIA_METADATA_ERR, __func__);
+ return;
+ }
+
+ mTags = std::move(tags);
+ mInitDone = true;
+
+ // Try to get the start time.
+ // For MSE case, the start time of each track is assumed to be 0.
+ // For others, we must demux the first sample to know the start time for each
+ // track.
+ if (!mDemuxer->ShouldComputeStartTime()) {
+ mAudio.mFirstDemuxedSampleTime.emplace(TimeUnit::Zero());
+ mVideo.mFirstDemuxedSampleTime.emplace(TimeUnit::Zero());
+ } else {
+ if (HasAudio()) {
+ RequestDemuxSamples(TrackInfo::kAudioTrack);
+ }
+
+ if (HasVideo()) {
+ RequestDemuxSamples(TrackInfo::kVideoTrack);
+ }
+ }
+
+ if (aResult != NS_OK) {
+ mOnDecodeWarning.Notify(aResult);
+ }
+
+ MaybeResolveMetadataPromise();
+}
+
+void MediaFormatReader::MaybeResolveMetadataPromise() {
+ MOZ_ASSERT(OnTaskQueue());
+
+ if ((HasAudio() && mAudio.mFirstDemuxedSampleTime.isNothing()) ||
+ (HasVideo() && mVideo.mFirstDemuxedSampleTime.isNothing())) {
+ return;
+ }
+
+ TimeUnit startTime =
+ std::min(mAudio.mFirstDemuxedSampleTime.refOr(TimeUnit::FromInfinity()),
+ mVideo.mFirstDemuxedSampleTime.refOr(TimeUnit::FromInfinity()));
+
+ if (!startTime.IsInfinite()) {
+ mInfo.mStartTime = startTime; // mInfo.mStartTime is initialized to 0.
+ }
+
+ MetadataHolder metadata;
+ metadata.mInfo = MakeUnique<MediaInfo>(mInfo);
+ metadata.mTags = mTags->Count() ? std::move(mTags) : nullptr;
+
+ // We now have all the informations required to calculate the initial buffered
+ // range.
+ mHasStartTime = true;
+ UpdateBuffered();
+
+ mWatchManager.Watch(mWorkingInfoChanged,
+ &MediaFormatReader::NotifyTrackInfoUpdated);
+ mIsWatchingWorkingInfo = true;
+
+ mMetadataPromise.Resolve(std::move(metadata), __func__);
+}
+
+bool MediaFormatReader::IsEncrypted() const {
+ return (HasAudio() && mAudio.GetCurrentInfo()->mCrypto.IsEncrypted()) ||
+ (HasVideo() && mVideo.GetCurrentInfo()->mCrypto.IsEncrypted());
+}
+
+void MediaFormatReader::OnDemuxerInitFailed(const MediaResult& aError) {
+ mDemuxerInitRequest.Complete();
+ mMetadataPromise.Reject(aError, __func__);
+}
+
+void MediaFormatReader::ReadUpdatedMetadata(MediaInfo* aInfo) {
+ // Called on the MDSM's TaskQueue.
+ {
+ MutexAutoLock lock(mVideo.mMutex);
+ if (HasVideo()) {
+ aInfo->mVideo = *mVideo.GetWorkingInfo()->GetAsVideoInfo();
+ }
+ }
+ {
+ MutexAutoLock lock(mAudio.mMutex);
+ if (HasAudio()) {
+ aInfo->mAudio = *mAudio.GetWorkingInfo()->GetAsAudioInfo();
+ Maybe<nsCString> audioProcessPerCodecName = GetAudioProcessPerCodec();
+ if (audioProcessPerCodecName.isSome()) {
+ Telemetry::ScalarAdd(
+ Telemetry::ScalarID::MEDIA_AUDIO_PROCESS_PER_CODEC_NAME,
+ NS_ConvertUTF8toUTF16(*audioProcessPerCodecName), 1);
+ }
+ }
+ }
+}
+
+MediaFormatReader::DecoderData& MediaFormatReader::GetDecoderData(
+ TrackType aTrack) {
+ MOZ_ASSERT(aTrack == TrackInfo::kAudioTrack ||
+ aTrack == TrackInfo::kVideoTrack);
+ if (aTrack == TrackInfo::kAudioTrack) {
+ return mAudio;
+ }
+ return mVideo;
+}
+
+Maybe<TimeUnit> MediaFormatReader::ShouldSkip(TimeUnit aTimeThreshold,
+ bool aRequestNextVideoKeyFrame) {
+ MOZ_ASSERT(OnTaskQueue());
+ MOZ_ASSERT(HasVideo());
+
+ if (!StaticPrefs::media_decoder_skip_to_next_key_frame_enabled()) {
+ return Nothing();
+ }
+
+ // Ensure we have no pending seek going as skip-to-keyframe could return out
+ // of date information.
+ if (mVideo.HasInternalSeekPending()) {
+ return Nothing();
+ }
+
+ TimeUnit nextKeyframe;
+ nsresult rv = mVideo.mTrackDemuxer->GetNextRandomAccessPoint(&nextKeyframe);
+ if (NS_FAILED(rv)) {
+ // Only OggTrackDemuxer with video type gets into here.
+ // We don't support skip-to-next-frame for this case.
+ return Nothing();
+ }
+
+ const bool isNextKeyframeValid =
+ nextKeyframe.ToMicroseconds() >= 0 && !nextKeyframe.IsInfinite();
+ // If we request the next keyframe, only return times greater than
+ // aTimeThreshold. Otherwise, data will be already behind the threshold and
+ // will be eventually discarded somewhere in the media pipeline.
+ if (aRequestNextVideoKeyFrame && isNextKeyframeValid &&
+ nextKeyframe > aTimeThreshold) {
+ return Some(nextKeyframe);
+ }
+
+ const bool isNextVideoBehindTheThreshold =
+ (isNextKeyframeValid && nextKeyframe <= aTimeThreshold) ||
+ GetInternalSeekTargetEndTime() < aTimeThreshold;
+ return isNextVideoBehindTheThreshold ? Some(aTimeThreshold) : Nothing();
+}
+
+RefPtr<MediaFormatReader::VideoDataPromise> MediaFormatReader::RequestVideoData(
+ const TimeUnit& aTimeThreshold, bool aRequestNextVideoKeyFrame) {
+ MOZ_ASSERT(OnTaskQueue());
+ MOZ_DIAGNOSTIC_ASSERT(!mVideo.HasPromise(), "No duplicate sample requests");
+ // Requesting video can be done independently from audio, even during audio
+ // seeking. But it shouldn't happen if we're doing video seek.
+ if (!IsAudioOnlySeeking()) {
+ MOZ_DIAGNOSTIC_ASSERT(mSeekPromise.IsEmpty(),
+ "No sample requests allowed while seeking");
+ MOZ_DIAGNOSTIC_ASSERT(!mVideo.mSeekRequest.Exists() ||
+ mVideo.mTimeThreshold.isSome());
+ MOZ_DIAGNOSTIC_ASSERT(!IsSeeking(), "called mid-seek");
+ }
+ LOGV("RequestVideoData(%" PRId64 "), requestNextKeyFrame=%d",
+ aTimeThreshold.ToMicroseconds(), aRequestNextVideoKeyFrame);
+
+ if (!HasVideo()) {
+ LOG("called with no video track");
+ return VideoDataPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ __func__);
+ }
+
+ if (IsSeeking()) {
+ LOG("called mid-seek. Rejecting.");
+ return VideoDataPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_CANCELED,
+ __func__);
+ }
+
+ if (mShutdown) {
+ NS_WARNING("RequestVideoData on shutdown MediaFormatReader!");
+ return VideoDataPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_CANCELED,
+ __func__);
+ }
+
+ if (Maybe<TimeUnit> target =
+ ShouldSkip(aTimeThreshold, aRequestNextVideoKeyFrame)) {
+ PROFILER_MARKER_UNTYPED("RequestVideoData SkipVideoDemuxToNextKeyFrame",
+ MEDIA_PLAYBACK);
+ RefPtr<VideoDataPromise> p = mVideo.EnsurePromise(__func__);
+ SkipVideoDemuxToNextKeyFrame(*target);
+ return p;
+ }
+
+ RefPtr<VideoDataPromise> p = mVideo.EnsurePromise(__func__);
+ ScheduleUpdate(TrackInfo::kVideoTrack);
+
+ return p;
+}
+
+void MediaFormatReader::OnDemuxFailed(TrackType aTrack,
+ const MediaResult& aError) {
+ AUTO_PROFILER_LABEL("MediaFormatReader::OnDemuxFailed", MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+ LOG("Failed to demux %s, failure:%s",
+ aTrack == TrackType::kVideoTrack ? "video" : "audio",
+ aError.ErrorName().get());
+ auto& decoder = GetDecoderData(aTrack);
+ decoder.mDemuxRequest.Complete();
+ switch (aError.Code()) {
+ case NS_ERROR_DOM_MEDIA_END_OF_STREAM:
+ DDLOG(DDLogCategory::Log,
+ aTrack == TrackType::kVideoTrack ? "video_demux_interruption"
+ : "audio_demux_interruption",
+ aError);
+ if (!decoder.mWaitingForData) {
+ decoder.RequestDrain();
+ }
+ NotifyEndOfStream(aTrack);
+ break;
+ case NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA:
+ DDLOG(DDLogCategory::Log,
+ aTrack == TrackType::kVideoTrack ? "video_demux_interruption"
+ : "audio_demux_interruption",
+ aError);
+ if (!decoder.mWaitingForData) {
+ decoder.RequestDrain();
+ }
+ NotifyWaitingForData(aTrack);
+ break;
+ case NS_ERROR_DOM_MEDIA_CANCELED:
+ DDLOG(DDLogCategory::Log,
+ aTrack == TrackType::kVideoTrack ? "video_demux_interruption"
+ : "audio_demux_interruption",
+ aError);
+ if (decoder.HasPromise()) {
+ decoder.RejectPromise(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ }
+ break;
+ default:
+ DDLOG(DDLogCategory::Log,
+ aTrack == TrackType::kVideoTrack ? "video_demux_error"
+ : "audio_demux_error",
+ aError);
+ NotifyError(aTrack, aError);
+ break;
+ }
+}
+
+void MediaFormatReader::DoDemuxVideo() {
+ AUTO_PROFILER_LABEL("MediaFormatReader::DoDemuxVideo", MEDIA_PLAYBACK);
+ using SamplesPromise = MediaTrackDemuxer::SamplesPromise;
+
+ DDLOG(DDLogCategory::Log, "video_demuxing", DDNoValue{});
+ PerformanceRecorder<PlaybackStage> perfRecorder(
+ MediaStage::RequestDemux,
+ mVideo.GetCurrentInfo()->GetAsVideoInfo()->mImage.height);
+ auto p = mVideo.mTrackDemuxer->GetSamples(1);
+
+ RefPtr<MediaFormatReader> self = this;
+ if (mVideo.mFirstDemuxedSampleTime.isNothing()) {
+ p = p->Then(
+ OwnerThread(), __func__,
+ [self](RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples) {
+ AUTO_PROFILER_LABEL("MediaFormatReader::DoDemuxVideo:Resolved",
+ MEDIA_PLAYBACK);
+ DDLOGEX(self.get(), DDLogCategory::Log, "video_first_demuxed",
+ DDNoValue{});
+ self->OnFirstDemuxCompleted(TrackInfo::kVideoTrack, aSamples);
+ return SamplesPromise::CreateAndResolve(aSamples.forget(), __func__);
+ },
+ [self](const MediaResult& aError) {
+ AUTO_PROFILER_LABEL("MediaFormatReader::DoDemuxVideo:Rejected",
+ MEDIA_PLAYBACK);
+ DDLOGEX(self.get(), DDLogCategory::Log, "video_first_demuxing_error",
+ aError);
+ self->OnFirstDemuxFailed(TrackInfo::kVideoTrack, aError);
+ return SamplesPromise::CreateAndReject(aError, __func__);
+ });
+ }
+
+ p->Then(
+ OwnerThread(), __func__,
+ [self, perfRecorder(std::move(perfRecorder))](
+ RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples) mutable {
+ perfRecorder.Record();
+ self->OnVideoDemuxCompleted(std::move(aSamples));
+ },
+ [self](const MediaResult& aError) { self->OnVideoDemuxFailed(aError); })
+ ->Track(mVideo.mDemuxRequest);
+}
+
+void MediaFormatReader::OnVideoDemuxCompleted(
+ RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples) {
+ AUTO_PROFILER_LABEL("MediaFormatReader::OnVideoDemuxCompleted",
+ MEDIA_PLAYBACK);
+ LOGV("%zu video samples demuxed (sid:%d)", aSamples->GetSamples().Length(),
+ aSamples->GetSamples()[0]->mTrackInfo
+ ? aSamples->GetSamples()[0]->mTrackInfo->GetID()
+ : 0);
+ DDLOG(DDLogCategory::Log, "video_demuxed_samples",
+ uint64_t(aSamples->GetSamples().Length()));
+ mVideo.mDemuxRequest.Complete();
+ mVideo.mQueuedSamples.AppendElements(aSamples->GetSamples());
+ ScheduleUpdate(TrackInfo::kVideoTrack);
+}
+
+RefPtr<MediaFormatReader::AudioDataPromise>
+MediaFormatReader::RequestAudioData() {
+ MOZ_ASSERT(OnTaskQueue());
+ MOZ_DIAGNOSTIC_ASSERT(!mAudio.HasPromise(), "No duplicate sample requests");
+ // Requesting audio can be done independently from video, even during video
+ // seeking. But it shouldn't happen if we're doing audio seek.
+ if (!IsVideoOnlySeeking()) {
+ MOZ_DIAGNOSTIC_ASSERT(mSeekPromise.IsEmpty(),
+ "No sample requests allowed while seeking");
+ MOZ_DIAGNOSTIC_ASSERT(!mAudio.mSeekRequest.Exists() ||
+ mAudio.mTimeThreshold.isSome());
+ MOZ_DIAGNOSTIC_ASSERT(!IsSeeking(), "called mid-seek");
+ }
+ LOGV("");
+
+ if (!HasAudio()) {
+ LOG("called with no audio track");
+ return AudioDataPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ __func__);
+ }
+
+ if (IsSeeking()) {
+ LOG("called mid-seek. Rejecting.");
+ return AudioDataPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_CANCELED,
+ __func__);
+ }
+
+ if (mShutdown) {
+ NS_WARNING("RequestAudioData on shutdown MediaFormatReader!");
+ return AudioDataPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_CANCELED,
+ __func__);
+ }
+
+ RefPtr<AudioDataPromise> p = mAudio.EnsurePromise(__func__);
+ ScheduleUpdate(TrackInfo::kAudioTrack);
+
+ return p;
+}
+
+void MediaFormatReader::DoDemuxAudio() {
+ AUTO_PROFILER_LABEL("MediaFormatReader::DoDemuxAudio", MEDIA_PLAYBACK);
+ using SamplesPromise = MediaTrackDemuxer::SamplesPromise;
+
+ DDLOG(DDLogCategory::Log, "audio_demuxing", DDNoValue{});
+ PerformanceRecorder<PlaybackStage> perfRecorder(MediaStage::RequestDemux);
+ auto p = mAudio.mTrackDemuxer->GetSamples(1);
+
+ RefPtr<MediaFormatReader> self = this;
+ if (mAudio.mFirstDemuxedSampleTime.isNothing()) {
+ p = p->Then(
+ OwnerThread(), __func__,
+ [self](RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples) {
+ AUTO_PROFILER_LABEL("MediaFormatReader::DoDemuxAudio:Resolved",
+ MEDIA_PLAYBACK);
+ DDLOGEX(self.get(), DDLogCategory::Log, "audio_first_demuxed",
+ DDNoValue{});
+ self->OnFirstDemuxCompleted(TrackInfo::kAudioTrack, aSamples);
+ return SamplesPromise::CreateAndResolve(aSamples.forget(), __func__);
+ },
+ [self](const MediaResult& aError) {
+ AUTO_PROFILER_LABEL("MediaFormatReader::DoDemuxAudio:Rejected",
+ MEDIA_PLAYBACK);
+ DDLOGEX(self.get(), DDLogCategory::Log, "audio_first_demuxing_error",
+ aError);
+ self->OnFirstDemuxFailed(TrackInfo::kAudioTrack, aError);
+ return SamplesPromise::CreateAndReject(aError, __func__);
+ });
+ }
+
+ p->Then(
+ OwnerThread(), __func__,
+ [self, perfRecorder(std::move(perfRecorder))](
+ RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples) mutable {
+ perfRecorder.Record();
+ self->OnAudioDemuxCompleted(std::move(aSamples));
+ },
+ [self](const MediaResult& aError) { self->OnAudioDemuxFailed(aError); })
+ ->Track(mAudio.mDemuxRequest);
+}
+
+void MediaFormatReader::OnAudioDemuxCompleted(
+ RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples) {
+ LOGV("%zu audio samples demuxed (sid:%d)", aSamples->GetSamples().Length(),
+ aSamples->GetSamples()[0]->mTrackInfo
+ ? aSamples->GetSamples()[0]->mTrackInfo->GetID()
+ : 0);
+ DDLOG(DDLogCategory::Log, "audio_demuxed_samples",
+ uint64_t(aSamples->GetSamples().Length()));
+ mAudio.mDemuxRequest.Complete();
+ mAudio.mQueuedSamples.AppendElements(aSamples->GetSamples());
+ ScheduleUpdate(TrackInfo::kAudioTrack);
+}
+
+void MediaFormatReader::NotifyNewOutput(
+ TrackType aTrack, MediaDataDecoder::DecodedData&& aResults) {
+ AUTO_PROFILER_LABEL("MediaFormatReader::NotifyNewOutput", MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+ auto& decoder = GetDecoderData(aTrack);
+ if (aResults.IsEmpty()) {
+ DDLOG(DDLogCategory::Log,
+ aTrack == TrackInfo::kAudioTrack ? "decoded_audio" : "decoded_video",
+ "no output samples");
+ } else
+ for (auto&& sample : aResults) {
+ if (DecoderDoctorLogger::IsDDLoggingEnabled()) {
+ switch (sample->mType) {
+ case MediaData::Type::AUDIO_DATA:
+ DDLOGPR(DDLogCategory::Log,
+ aTrack == TrackInfo::kAudioTrack ? "decoded_audio"
+ : "decoded_got_audio!?",
+ "{\"type\":\"AudioData\", \"offset\":%" PRIi64
+ ", \"time_us\":%" PRIi64 ", \"timecode_us\":%" PRIi64
+ ", \"duration_us\":%" PRIi64 ", \"frames\":%" PRIu32
+ ", \"channels\":%" PRIu32 ", \"rate\":%" PRIu32
+ ", \"bytes\":%zu}",
+ sample->mOffset, sample->mTime.ToMicroseconds(),
+ sample->mTimecode.ToMicroseconds(),
+ sample->mDuration.ToMicroseconds(),
+ sample->As<AudioData>()->Frames(),
+ sample->As<AudioData>()->mChannels,
+ sample->As<AudioData>()->mRate,
+ sample->As<AudioData>()->Data().Length());
+ break;
+ case MediaData::Type::VIDEO_DATA:
+ DDLOGPR(DDLogCategory::Log,
+ aTrack == TrackInfo::kVideoTrack ? "decoded_video"
+ : "decoded_got_video!?",
+ "{\"type\":\"VideoData\", \"offset\":%" PRIi64
+ ", \"time_us\":%" PRIi64 ", \"timecode_us\":%" PRIi64
+ ", \"duration_us\":%" PRIi64
+ ", \"kf\":%s, \"size\":[%" PRIi32 ",%" PRIi32 "]}",
+ sample->mOffset, sample->mTime.ToMicroseconds(),
+ sample->mTimecode.ToMicroseconds(),
+ sample->mDuration.ToMicroseconds(),
+ sample->mKeyframe ? "true" : "false",
+ sample->As<VideoData>()->mDisplay.width,
+ sample->As<VideoData>()->mDisplay.height);
+ break;
+ case MediaData::Type::RAW_DATA:
+ DDLOGPR(DDLogCategory::Log,
+ aTrack == TrackInfo::kAudioTrack ? "decoded_audio"
+ : aTrack == TrackInfo::kVideoTrack ? "decoded_video"
+ : "decoded_?",
+ "{\"type\":\"RawData\", \"offset\":%" PRIi64
+ " \"time_us\":%" PRIi64 ", \"timecode_us\":%" PRIi64
+ ", \"duration_us\":%" PRIi64 ", \"kf\":%s}",
+ sample->mOffset, sample->mTime.ToMicroseconds(),
+ sample->mTimecode.ToMicroseconds(),
+ sample->mDuration.ToMicroseconds(),
+ sample->mKeyframe ? "true" : "false");
+ break;
+ case MediaData::Type::NULL_DATA:
+ DDLOGPR(DDLogCategory::Log,
+ aTrack == TrackInfo::kAudioTrack ? "decoded_audio"
+ : aTrack == TrackInfo::kVideoTrack ? "decoded_video"
+ : "decoded_?",
+ "{\"type\":\"NullData\", \"offset\":%" PRIi64
+ " \"time_us\":%" PRIi64 ", \"timecode_us\":%" PRIi64
+ ", \"duration_us\":%" PRIi64 ", \"kf\":%s}",
+ sample->mOffset, sample->mTime.ToMicroseconds(),
+ sample->mTimecode.ToMicroseconds(),
+ sample->mDuration.ToMicroseconds(),
+ sample->mKeyframe ? "true" : "false");
+ break;
+ }
+ }
+ LOGV("Received new %s sample time:%" PRId64 " duration:%" PRId64,
+ TrackTypeToStr(aTrack), sample->mTime.ToMicroseconds(),
+ sample->mDuration.ToMicroseconds());
+ decoder.mOutput.AppendElement(sample);
+ decoder.mNumSamplesOutput++;
+ decoder.mNumOfConsecutiveDecodingError = 0;
+ decoder.mNumOfConsecutiveRDDOrGPUCrashes = 0;
+ if (aTrack == TrackInfo::kAudioTrack) {
+ decoder.mNumOfConsecutiveUtilityCrashes = 0;
+ }
+ }
+ LOG("Done processing new %s samples", TrackTypeToStr(aTrack));
+
+ if (!aResults.IsEmpty()) {
+ // We have decoded our first frame, we can now starts to skip future errors.
+ decoder.mFirstFrameTime.reset();
+ }
+ ScheduleUpdate(aTrack);
+}
+
+void MediaFormatReader::NotifyError(TrackType aTrack,
+ const MediaResult& aError) {
+ MOZ_ASSERT(OnTaskQueue());
+ NS_WARNING(aError.Description().get());
+ LOGV("%s Decoding error", TrackTypeToStr(aTrack));
+ auto& decoder = GetDecoderData(aTrack);
+ decoder.mError = decoder.HasFatalError() ? decoder.mError : Some(aError);
+
+ ScheduleUpdate(aTrack);
+}
+
+void MediaFormatReader::NotifyWaitingForData(TrackType aTrack) {
+ MOZ_ASSERT(OnTaskQueue());
+ auto& decoder = GetDecoderData(aTrack);
+ decoder.mWaitingForData = true;
+ if (decoder.mTimeThreshold) {
+ decoder.mTimeThreshold.ref().mWaiting = true;
+ }
+ ScheduleUpdate(aTrack);
+}
+
+void MediaFormatReader::NotifyWaitingForKey(TrackType aTrack) {
+ MOZ_ASSERT(OnTaskQueue());
+ auto& decoder = GetDecoderData(aTrack);
+ mOnWaitingForKey.Notify();
+ if (!decoder.mDecodeRequest.Exists()) {
+ LOGV("WaitingForKey received while no pending decode. Ignoring");
+ return;
+ }
+ decoder.mWaitingForKey = true;
+ ScheduleUpdate(aTrack);
+}
+
+void MediaFormatReader::NotifyEndOfStream(TrackType aTrack) {
+ MOZ_ASSERT(OnTaskQueue());
+ auto& decoder = GetDecoderData(aTrack);
+ decoder.mDemuxEOS = true;
+ ScheduleUpdate(aTrack);
+}
+
+bool MediaFormatReader::NeedInput(DecoderData& aDecoder) {
+ // The decoder will not be fed a new raw sample until the current decoding
+ // requests has completed.
+ return (aDecoder.HasPromise() || aDecoder.mTimeThreshold.isSome()) &&
+ !aDecoder.HasPendingDrain() && !aDecoder.HasFatalError() &&
+ !aDecoder.mDemuxRequest.Exists() && !aDecoder.mOutput.Length() &&
+ !aDecoder.HasInternalSeekPending() &&
+ !aDecoder.mDecodeRequest.Exists();
+}
+
+void MediaFormatReader::ScheduleUpdate(TrackType aTrack) {
+ MOZ_ASSERT(OnTaskQueue());
+ if (mShutdown) {
+ return;
+ }
+ auto& decoder = GetDecoderData(aTrack);
+ MOZ_RELEASE_ASSERT(decoder.GetCurrentInfo(),
+ "Can only schedule update when track exists");
+
+ if (decoder.mUpdateScheduled) {
+ return;
+ }
+ LOGV("SchedulingUpdate(%s)", TrackTypeToStr(aTrack));
+ decoder.mUpdateScheduled = true;
+ RefPtr<nsIRunnable> task(NewRunnableMethod<TrackType>(
+ "MediaFormatReader::Update", this, &MediaFormatReader::Update, aTrack));
+ nsresult rv = OwnerThread()->Dispatch(task.forget());
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+}
+
+bool MediaFormatReader::UpdateReceivedNewData(TrackType aTrack) {
+ MOZ_ASSERT(OnTaskQueue());
+ auto& decoder = GetDecoderData(aTrack);
+
+ if (!decoder.mReceivedNewData) {
+ return false;
+ }
+
+ // We do not want to clear mWaitingForData while there are pending
+ // demuxing or seeking operations that could affect the value of this flag.
+ // This is in order to ensure that we will retry once they complete as we may
+ // now have new data that could potentially allow those operations to
+ // successfully complete if tried again.
+ if (decoder.mSeekRequest.Exists()) {
+ // Nothing more to do until this operation complete.
+ return true;
+ }
+
+ if (aTrack == TrackType::kVideoTrack && mSkipRequest.Exists()) {
+ LOGV("Skipping in progress, nothing more to do");
+ return true;
+ }
+
+ if (decoder.mDemuxRequest.Exists()) {
+ // We may have pending operations to process, so we want to continue
+ // after UpdateReceivedNewData returns.
+ return false;
+ }
+
+ if (decoder.HasPendingDrain()) {
+ // We do not want to clear mWaitingForData or mDemuxEOS while
+ // a drain is in progress in order to properly complete the operation.
+ return false;
+ }
+
+ decoder.mReceivedNewData = false;
+ if (decoder.mTimeThreshold) {
+ decoder.mTimeThreshold.ref().mWaiting = false;
+ }
+ decoder.mWaitingForData = false;
+
+ if (decoder.HasFatalError()) {
+ return false;
+ }
+
+ if (!mSeekPromise.IsEmpty() &&
+ (!IsVideoOnlySeeking() || aTrack == TrackInfo::kVideoTrack)) {
+ MOZ_ASSERT(!decoder.HasPromise());
+ MOZ_DIAGNOSTIC_ASSERT(
+ (IsVideoOnlySeeking() || !mAudio.mTimeThreshold) &&
+ !mVideo.mTimeThreshold,
+ "InternalSeek must have been aborted when Seek was first called");
+ MOZ_DIAGNOSTIC_ASSERT(
+ (IsVideoOnlySeeking() || !mAudio.HasWaitingPromise()) &&
+ !mVideo.HasWaitingPromise(),
+ "Waiting promises must have been rejected when Seek was first called");
+ if (mVideo.mSeekRequest.Exists() ||
+ (!IsVideoOnlySeeking() && mAudio.mSeekRequest.Exists())) {
+ // Already waiting for a seek to complete. Nothing more to do.
+ return true;
+ }
+ LOG("Attempting Seek");
+ ScheduleSeek();
+ return true;
+ }
+ if (decoder.HasInternalSeekPending() || decoder.HasWaitingPromise()) {
+ if (decoder.HasInternalSeekPending()) {
+ LOG("Attempting Internal Seek");
+ InternalSeek(aTrack, decoder.mTimeThreshold.ref());
+ }
+ if (decoder.HasWaitingPromise() && !decoder.IsWaitingForKey() &&
+ !decoder.IsWaitingForData()) {
+ MOZ_ASSERT(!decoder.HasPromise());
+ LOG("We have new data. Resolving WaitingPromise");
+ decoder.mWaitingPromise.Resolve(decoder.mType, __func__);
+ }
+ return true;
+ }
+ return false;
+}
+
+void MediaFormatReader::RequestDemuxSamples(TrackType aTrack) {
+ MOZ_ASSERT(OnTaskQueue());
+ auto& decoder = GetDecoderData(aTrack);
+ MOZ_ASSERT(!decoder.mDemuxRequest.Exists());
+
+ if (!decoder.mQueuedSamples.IsEmpty()) {
+ // No need to demux new samples.
+ return;
+ }
+
+ if (decoder.mDemuxEOS) {
+ // Nothing left to demux.
+ // We do not want to attempt to demux while in waiting for data mode
+ // as it would retrigger an unnecessary drain.
+ return;
+ }
+
+ LOGV("Requesting extra demux %s", TrackTypeToStr(aTrack));
+ if (aTrack == TrackInfo::kVideoTrack) {
+ DoDemuxVideo();
+ } else {
+ DoDemuxAudio();
+ }
+}
+
+void MediaFormatReader::DecodeDemuxedSamples(TrackType aTrack,
+ MediaRawData* aSample) {
+ MOZ_ASSERT(OnTaskQueue());
+ auto& decoder = GetDecoderData(aTrack);
+ RefPtr<MediaFormatReader> self = this;
+ decoder.mFlushed = false;
+ DDLOGPR(DDLogCategory::Log,
+ aTrack == TrackInfo::kAudioTrack ? "decode_audio"
+ : aTrack == TrackInfo::kVideoTrack ? "decode_video"
+ : "decode_?",
+ "{\"type\":\"MediaRawData\", \"offset\":%" PRIi64
+ ", \"bytes\":%zu, \"time_us\":%" PRIi64 ", \"timecode_us\":%" PRIi64
+ ", \"duration_us\":%" PRIi64 ",%s%s}",
+ aSample->mOffset, aSample->Size(), aSample->mTime.ToMicroseconds(),
+ aSample->mTimecode.ToMicroseconds(),
+ aSample->mDuration.ToMicroseconds(), aSample->mKeyframe ? " kf" : "",
+ aSample->mEOS ? " eos" : "");
+
+ const int32_t height =
+ aTrack == TrackInfo::kVideoTrack
+ ? decoder.GetCurrentInfo()->GetAsVideoInfo()->mImage.height
+ : 0;
+ MediaInfoFlag flag = MediaInfoFlag::None;
+ flag |=
+ aSample->mKeyframe ? MediaInfoFlag::KeyFrame : MediaInfoFlag::NonKeyFrame;
+ if (aTrack == TrackInfo::kVideoTrack) {
+ flag |= VideoIsHardwareAccelerated() ? MediaInfoFlag::HardwareDecoding
+ : MediaInfoFlag::SoftwareDecoding;
+ const nsCString& mimeType = decoder.GetCurrentInfo()->mMimeType;
+ if (MP4Decoder::IsH264(mimeType)) {
+ flag |= MediaInfoFlag::VIDEO_H264;
+ } else if (VPXDecoder::IsVPX(mimeType, VPXDecoder::VP8)) {
+ flag |= MediaInfoFlag::VIDEO_VP8;
+ } else if (VPXDecoder::IsVPX(mimeType, VPXDecoder::VP9)) {
+ flag |= MediaInfoFlag::VIDEO_VP9;
+ }
+#ifdef MOZ_AV1
+ else if (AOMDecoder::IsAV1(mimeType)) {
+ flag |= MediaInfoFlag::VIDEO_AV1;
+ }
+#endif
+ }
+ PerformanceRecorder<PlaybackStage> perfRecorder(MediaStage::RequestDecode,
+ height, flag);
+ if (mMediaEngineId && aSample->mCrypto.IsEncrypted()) {
+ aSample->mShouldCopyCryptoToRemoteRawData = true;
+ }
+ decoder.mDecoder->Decode(aSample)
+ ->Then(
+ mTaskQueue, __func__,
+ [self, aTrack, &decoder, perfRecorder(std::move(perfRecorder))](
+ MediaDataDecoder::DecodedData&& aResults) mutable {
+ perfRecorder.Record();
+ decoder.mDecodeRequest.Complete();
+ self->NotifyNewOutput(aTrack, std::move(aResults));
+ },
+ [self, aTrack, &decoder](const MediaResult& aError) {
+ decoder.mDecodeRequest.Complete();
+ self->NotifyError(aTrack, aError);
+ })
+ ->Track(decoder.mDecodeRequest);
+}
+
+void MediaFormatReader::HandleDemuxedSamples(
+ TrackType aTrack, FrameStatistics::AutoNotifyDecoded& aA) {
+ MOZ_ASSERT(OnTaskQueue());
+
+ auto& decoder = GetDecoderData(aTrack);
+
+ if (decoder.mFlushing) {
+ LOGV("Decoder operation in progress, let it complete.");
+ return;
+ }
+
+ if (decoder.mQueuedSamples.IsEmpty()) {
+ return;
+ }
+
+ RefPtr<MediaRawData> sample = decoder.mQueuedSamples[0];
+ const RefPtr<TrackInfoSharedPtr> info = sample->mTrackInfo;
+
+ if (info && decoder.mLastStreamSourceID != info->GetID()) {
+ nsTArray<RefPtr<MediaRawData>> samples;
+ if (decoder.mDecoder) {
+ bool recyclable =
+ StaticPrefs::media_decoder_recycle_enabled() &&
+ decoder.mDecoder->SupportDecoderRecycling() &&
+ (*info)->mCrypto.mCryptoScheme ==
+ decoder.GetCurrentInfo()->mCrypto.mCryptoScheme &&
+ (*info)->mMimeType == decoder.GetCurrentInfo()->mMimeType;
+ if (!recyclable && decoder.mTimeThreshold.isNothing() &&
+ (decoder.mNextStreamSourceID.isNothing() ||
+ decoder.mNextStreamSourceID.ref() != info->GetID())) {
+ LOG("%s stream id has changed from:%d to:%d, draining decoder.",
+ TrackTypeToStr(aTrack), decoder.mLastStreamSourceID, info->GetID());
+ decoder.RequestDrain();
+ decoder.mNextStreamSourceID = Some(info->GetID());
+ ScheduleUpdate(aTrack);
+ return;
+ }
+
+ // If flushing is required, it will clear our array of queued samples.
+ // So we may need to make a copy.
+ samples = decoder.mQueuedSamples.Clone();
+ if (!recyclable) {
+ LOG("Decoder does not support recycling, recreate decoder.");
+ ShutdownDecoder(aTrack);
+ // We're going to be using a new decoder following the change of content
+ // We can attempt to use hardware decoding again.
+ decoder.mHardwareDecodingDisabled = false;
+ } else if (decoder.HasWaitingPromise()) {
+ decoder.Flush();
+ }
+ }
+
+ nsPrintfCString markerString(
+ "%s stream id changed from:%" PRIu32 " to:%" PRIu32,
+ TrackTypeToStr(aTrack), decoder.mLastStreamSourceID, info->GetID());
+ PROFILER_MARKER_TEXT("StreamID Change", MEDIA_PLAYBACK, {}, markerString);
+ LOG("%s", markerString.get());
+
+ if (aTrack == TrackInfo::kVideoTrack) {
+ // We are about to create a new decoder thus the benchmark,
+ // up to this point, is stored.
+ NotifyDecoderBenchmarkStore();
+ }
+ decoder.mNextStreamSourceID.reset();
+ decoder.mLastStreamSourceID = info->GetID();
+ decoder.mInfo = info;
+ {
+ MutexAutoLock lock(decoder.mMutex);
+ if (aTrack == TrackInfo::kAudioTrack) {
+ decoder.mWorkingInfo = MakeUnique<AudioInfo>(*info->GetAsAudioInfo());
+ } else if (aTrack == TrackInfo::kVideoTrack) {
+ decoder.mWorkingInfo = MakeUnique<VideoInfo>(*info->GetAsVideoInfo());
+ }
+ mWorkingInfoChanged = true;
+ }
+
+ decoder.mMeanRate.Reset();
+
+ if (sample->mKeyframe) {
+ if (samples.Length()) {
+ decoder.mQueuedSamples = std::move(samples);
+ }
+ } else {
+ auto time = TimeInterval(sample->mTime, sample->GetEndTime());
+ InternalSeekTarget seekTarget =
+ decoder.mTimeThreshold.refOr(InternalSeekTarget(time, false));
+ LOG("Stream change occurred on a non-keyframe. Seeking to:%" PRId64,
+ sample->mTime.ToMicroseconds());
+ InternalSeek(aTrack, seekTarget);
+ return;
+ }
+ }
+
+ // Calculate the average frame rate. The first frame will be accounted
+ // for twice.
+ decoder.mMeanRate.Update(sample->mDuration);
+
+ if (!decoder.mDecoder) {
+ // In Clear Lead situation, the `mInfo` could change from unencrypted to
+ // encrypted so we need to ensure the CDM proxy is ready before creating a
+ // decoder.
+ if (decoder.IsEncrypted() &&
+ (IsWaitingOnCDMResource() || !ResolveSetCDMPromiseIfDone(aTrack))) {
+ return;
+ }
+ mDecoderFactory->CreateDecoder(aTrack);
+ return;
+ }
+
+ LOGV("Input:%" PRId64 " (dts:%" PRId64 " kf:%d)",
+ sample->mTime.ToMicroseconds(), sample->mTimecode.ToMicroseconds(),
+ sample->mKeyframe);
+ decoder.mNumSamplesInput++;
+ decoder.mSizeOfQueue++;
+ if (aTrack == TrackInfo::kVideoTrack) {
+ aA.mStats.mParsedFrames++;
+ }
+
+ DecodeDemuxedSamples(aTrack, sample);
+
+ decoder.mQueuedSamples.RemoveElementAt(0);
+}
+
+media::TimeUnit MediaFormatReader::GetInternalSeekTargetEndTime() const {
+ MOZ_ASSERT(OnTaskQueue());
+ return mVideo.mTimeThreshold ? mVideo.mTimeThreshold->EndTime()
+ : TimeUnit::FromInfinity();
+}
+
+void MediaFormatReader::InternalSeek(TrackType aTrack,
+ const InternalSeekTarget& aTarget) {
+ MOZ_ASSERT(OnTaskQueue());
+ LOG("%s internal seek to %f", TrackTypeToStr(aTrack),
+ aTarget.Time().ToSeconds());
+
+ auto& decoder = GetDecoderData(aTrack);
+ decoder.Flush();
+ decoder.ResetDemuxer();
+ decoder.mTimeThreshold = Some(aTarget);
+ DDLOG(DDLogCategory::Log, "seeking", DDNoValue{});
+ RefPtr<MediaFormatReader> self = this;
+ decoder.mTrackDemuxer->Seek(decoder.mTimeThreshold.ref().Time())
+ ->Then(
+ OwnerThread(), __func__,
+ [self, aTrack](TimeUnit aTime) {
+ DDLOGEX(self.get(), DDLogCategory::Log, "seeked", DDNoValue{});
+ auto& decoder = self->GetDecoderData(aTrack);
+ decoder.mSeekRequest.Complete();
+ MOZ_ASSERT(decoder.mTimeThreshold,
+ "Seek promise must be disconnected when "
+ "timethreshold is reset");
+ decoder.mTimeThreshold.ref().mHasSeeked = true;
+ self->SetVideoDecodeThreshold();
+ self->ScheduleUpdate(aTrack);
+ },
+ [self, aTrack](const MediaResult& aError) {
+ auto& decoder = self->GetDecoderData(aTrack);
+ decoder.mSeekRequest.Complete();
+ switch (aError.Code()) {
+ case NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA:
+ DDLOGEX(self.get(), DDLogCategory::Log, "seeking_interrupted",
+ aError);
+ self->NotifyWaitingForData(aTrack);
+ break;
+ case NS_ERROR_DOM_MEDIA_END_OF_STREAM:
+ DDLOGEX(self.get(), DDLogCategory::Log, "seeking_interrupted",
+ aError);
+ decoder.mTimeThreshold.reset();
+ self->NotifyEndOfStream(aTrack);
+ break;
+ case NS_ERROR_DOM_MEDIA_CANCELED:
+ DDLOGEX(self.get(), DDLogCategory::Log, "seeking_interrupted",
+ aError);
+ decoder.mTimeThreshold.reset();
+ break;
+ default:
+ DDLOGEX(self.get(), DDLogCategory::Log, "seeking_error",
+ aError);
+ decoder.mTimeThreshold.reset();
+ self->NotifyError(aTrack, aError);
+ break;
+ }
+ })
+ ->Track(decoder.mSeekRequest);
+}
+
+void MediaFormatReader::DrainDecoder(TrackType aTrack) {
+ AUTO_PROFILER_LABEL("MediaFormatReader::DrainDecoder", MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+
+ auto& decoder = GetDecoderData(aTrack);
+ if (decoder.mDrainState == DrainState::Draining) {
+ return;
+ }
+ if (!decoder.mDecoder ||
+ (decoder.mDrainState != DrainState::PartialDrainPending &&
+ decoder.mNumSamplesInput == decoder.mNumSamplesOutput)) {
+ // No frames to drain.
+ LOGV("Draining %s with nothing to drain", TrackTypeToStr(aTrack));
+ decoder.mDrainState = DrainState::DrainAborted;
+ ScheduleUpdate(aTrack);
+ return;
+ }
+
+ decoder.mDrainState = DrainState::Draining;
+
+ DDLOG(DDLogCategory::Log, "draining", DDNoValue{});
+ RefPtr<MediaFormatReader> self = this;
+ decoder.mDecoder->Drain()
+ ->Then(
+ mTaskQueue, __func__,
+ [self, aTrack, &decoder](MediaDataDecoder::DecodedData&& aResults) {
+ decoder.mDrainRequest.Complete();
+ DDLOGEX(self.get(), DDLogCategory::Log, "drained", DDNoValue{});
+ if (aResults.IsEmpty()) {
+ decoder.mDrainState = DrainState::DrainCompleted;
+ } else {
+ self->NotifyNewOutput(aTrack, std::move(aResults));
+ // Let's see if we have any more data available to drain.
+ decoder.mDrainState = DrainState::PartialDrainPending;
+ }
+ self->ScheduleUpdate(aTrack);
+ },
+ [self, aTrack, &decoder](const MediaResult& aError) {
+ decoder.mDrainRequest.Complete();
+ DDLOGEX(self.get(), DDLogCategory::Log, "draining_error", aError);
+ self->NotifyError(aTrack, aError);
+ })
+ ->Track(decoder.mDrainRequest);
+ LOG("Requesting %s decoder to drain", TrackTypeToStr(aTrack));
+}
+
+void MediaFormatReader::Update(TrackType aTrack) {
+ AUTO_PROFILER_LABEL("MediaFormatReader::Update", MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+
+ if (mShutdown) {
+ return;
+ }
+
+ LOGV("Processing update for %s", TrackTypeToStr(aTrack));
+
+ bool needOutput = false;
+ auto& decoder = GetDecoderData(aTrack);
+ decoder.mUpdateScheduled = false;
+
+ if (!mInitDone) {
+ return;
+ }
+
+ if (aTrack == TrackType::kVideoTrack && mSkipRequest.Exists()) {
+ LOGV("Skipping in progress, nothing more to do");
+ return;
+ }
+
+ if (UpdateReceivedNewData(aTrack)) {
+ LOGV("Nothing more to do");
+ return;
+ }
+
+ if (decoder.mSeekRequest.Exists()) {
+ LOGV("Seeking hasn't completed, nothing more to do");
+ return;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(
+ !decoder.HasInternalSeekPending() ||
+ (!decoder.mOutput.Length() && !decoder.mQueuedSamples.Length()),
+ "No frames can be demuxed or decoded while an internal seek is pending");
+
+ // Record number of frames decoded and parsed. Automatically update the
+ // stats counters using the AutoNotifyDecoded stack-based class.
+ FrameStatistics::AutoNotifyDecoded a(mFrameStats);
+
+ // Drop any frames found prior our internal seek target.
+ while (decoder.mTimeThreshold && decoder.mOutput.Length()) {
+ RefPtr<MediaData>& output = decoder.mOutput[0];
+ InternalSeekTarget target = decoder.mTimeThreshold.ref();
+ auto time = output->mTime;
+ if (time >= target.Time()) {
+ // We have reached our internal seek target.
+ decoder.mTimeThreshold.reset();
+ // We might have dropped some keyframes.
+ mPreviousDecodedKeyframeTime_us = sNoPreviousDecodedKeyframe;
+ }
+ if (time < target.Time() || (target.mDropTarget && target.Contains(time))) {
+ LOGV("Internal Seeking: Dropping %s frame time:%f wanted:%f (kf:%d)",
+ TrackTypeToStr(aTrack), output->mTime.ToSeconds(),
+ target.Time().ToSeconds(), output->mKeyframe);
+ decoder.mOutput.RemoveElementAt(0);
+ decoder.mSizeOfQueue -= 1;
+ }
+ }
+
+ while (decoder.mOutput.Length() &&
+ decoder.mOutput[0]->mType == MediaData::Type::NULL_DATA) {
+ LOGV("Dropping null data. Time: %" PRId64,
+ decoder.mOutput[0]->mTime.ToMicroseconds());
+ decoder.mOutput.RemoveElementAt(0);
+ decoder.mSizeOfQueue -= 1;
+ }
+
+ if (decoder.HasPromise()) {
+ needOutput = true;
+ if (decoder.mOutput.Length()) {
+ RefPtr<MediaData> output = decoder.mOutput[0];
+ decoder.mOutput.RemoveElementAt(0);
+ decoder.mSizeOfQueue -= 1;
+ decoder.mLastDecodedSampleTime =
+ Some(TimeInterval(output->mTime, output->GetEndTime()));
+ decoder.mNumSamplesOutputTotal++;
+ ReturnOutput(output, aTrack);
+ // We have a decoded sample ready to be returned.
+ if (aTrack == TrackType::kVideoTrack) {
+ uint64_t delta =
+ decoder.mNumSamplesOutputTotal - mLastReportedNumDecodedFrames;
+ a.mStats.mDecodedFrames = static_cast<uint32_t>(delta);
+ mLastReportedNumDecodedFrames = decoder.mNumSamplesOutputTotal;
+ if (output->mKeyframe) {
+ if (mPreviousDecodedKeyframeTime_us <
+ output->mTime.ToMicroseconds()) {
+ // There is a previous keyframe -> Record inter-keyframe stats.
+ uint64_t segment_us = output->mTime.ToMicroseconds() -
+ mPreviousDecodedKeyframeTime_us;
+ a.mStats.mInterKeyframeSum_us += segment_us;
+ a.mStats.mInterKeyframeCount += 1;
+ if (a.mStats.mInterKeyFrameMax_us < segment_us) {
+ a.mStats.mInterKeyFrameMax_us = segment_us;
+ }
+ }
+ mPreviousDecodedKeyframeTime_us = output->mTime.ToMicroseconds();
+ }
+ bool wasHardwareAccelerated = mVideo.mIsHardwareAccelerated;
+ nsCString error;
+ mVideo.mIsHardwareAccelerated =
+ mVideo.mDecoder && mVideo.mDecoder->IsHardwareAccelerated(error);
+ VideoData* videoData = output->As<VideoData>();
+ if (!mVideo.mHasReportedVideoHardwareSupportTelemtry ||
+ wasHardwareAccelerated != mVideo.mIsHardwareAccelerated) {
+ mVideo.mHasReportedVideoHardwareSupportTelemtry = true;
+ Telemetry::ScalarSet(
+ Telemetry::ScalarID::MEDIA_VIDEO_HARDWARE_DECODING_SUPPORT,
+ NS_ConvertUTF8toUTF16(mVideo.GetCurrentInfo()->mMimeType),
+ !!mVideo.mIsHardwareAccelerated);
+ static constexpr gfx::IntSize HD_VIDEO_SIZE{1280, 720};
+ if (videoData->mDisplay.width >= HD_VIDEO_SIZE.Width() &&
+ videoData->mDisplay.height >= HD_VIDEO_SIZE.Height()) {
+ Telemetry::ScalarSet(
+ Telemetry::ScalarID::MEDIA_VIDEO_HD_HARDWARE_DECODING_SUPPORT,
+ NS_ConvertUTF8toUTF16(mVideo.GetCurrentInfo()->mMimeType),
+ !!mVideo.mIsHardwareAccelerated);
+ }
+ }
+#ifdef XP_WIN
+ // D3D11_YCBCR_IMAGE images are GPU based, we try to limit the amount
+ // of GPU RAM used.
+ mVideo.mIsHardwareAccelerated =
+ mVideo.mIsHardwareAccelerated ||
+ (videoData->mImage &&
+ videoData->mImage->GetFormat() == ImageFormat::D3D11_YCBCR_IMAGE);
+#endif
+ }
+ } else if (decoder.HasFatalError()) {
+ nsCString mimeType = decoder.GetCurrentInfo()->mMimeType;
+ if (!mimeType.IsEmpty()) {
+ Telemetry::ScalarAdd(
+ Telemetry::ScalarID::MEDIA_DECODE_ERROR_PER_MIME_TYPE,
+ NS_ConvertUTF8toUTF16(mimeType), 1 /* error count */);
+ }
+ LOG("Rejecting %s promise for %s : DECODE_ERROR", TrackTypeToStr(aTrack),
+ mimeType.get());
+ decoder.RejectPromise(decoder.mError.ref(), __func__);
+ return;
+ } else if (decoder.HasCompletedDrain()) {
+ if (decoder.mDemuxEOS) {
+ LOG("Rejecting %s promise: EOS", TrackTypeToStr(aTrack));
+ if (aTrack == TrackInfo::kVideoTrack) {
+ // End of video, store the benchmark of the decoder.
+ NotifyDecoderBenchmarkStore();
+ }
+ decoder.RejectPromise(NS_ERROR_DOM_MEDIA_END_OF_STREAM, __func__);
+ } else if (decoder.mWaitingForData) {
+ if (decoder.mDrainState == DrainState::DrainCompleted &&
+ decoder.mLastDecodedSampleTime && !decoder.mNextStreamSourceID) {
+ // We have completed draining the decoder following WaitingForData.
+ // Set up the internal seek machinery to be able to resume from the
+ // last sample decoded.
+ LOG("Seeking to last sample time: %" PRId64,
+ decoder.mLastDecodedSampleTime.ref().mStart.ToMicroseconds());
+ InternalSeek(aTrack, InternalSeekTarget(
+ decoder.mLastDecodedSampleTime.ref(), true));
+ }
+ if (!decoder.mReceivedNewData) {
+ LOG("Rejecting %s promise: WAITING_FOR_DATA", TrackTypeToStr(aTrack));
+ decoder.RejectPromise(NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA, __func__);
+ }
+ }
+
+ decoder.mDrainState = DrainState::None;
+
+ // Now that draining has completed, we check if we have received
+ // new data again as the result may now be different from the earlier
+ // run.
+ if (UpdateReceivedNewData(aTrack) || decoder.mSeekRequest.Exists()) {
+ LOGV("Nothing more to do");
+ return;
+ }
+ } else if (decoder.mDemuxEOS && !decoder.HasPendingDrain() &&
+ decoder.mQueuedSamples.IsEmpty()) {
+ // It is possible to transition from WAITING_FOR_DATA directly to EOS
+ // state during the internal seek; in which case no draining would occur.
+ // There is no more samples left to be decoded and we are already in
+ // EOS state. We can immediately reject the data promise.
+ LOG("Rejecting %s promise: EOS", TrackTypeToStr(aTrack));
+ decoder.RejectPromise(NS_ERROR_DOM_MEDIA_END_OF_STREAM, __func__);
+ } else if (decoder.mWaitingForKey) {
+ LOG("Rejecting %s promise: WAITING_FOR_DATA due to waiting for key",
+ TrackTypeToStr(aTrack));
+ decoder.RejectPromise(NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA, __func__);
+ } else if (IsDecoderWaitingForCDM(aTrack)) {
+ // Rejecting the promise could lead to entering buffering state for MDSM,
+ // once a qualified(with the same key system and sessions created by the
+ // same InitData) new cdm proxy is set, decoding can be resumed.
+ LOG("Rejecting %s promise: WAITING_FOR_DATA due to waiting for CDM",
+ TrackTypeToStr(aTrack));
+ decoder.RejectPromise(NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA, __func__);
+ }
+ }
+
+ if (decoder.mDrainState == DrainState::DrainRequested ||
+ decoder.mDrainState == DrainState::PartialDrainPending) {
+ if (decoder.mOutput.IsEmpty()) {
+ DrainDecoder(aTrack);
+ }
+ return;
+ }
+
+ if (decoder.mError && !decoder.HasFatalError()) {
+ MOZ_RELEASE_ASSERT(!decoder.HasInternalSeekPending(),
+ "No error can occur while an internal seek is pending");
+
+ nsCString error;
+ bool firstFrameDecodingFailedWithHardware =
+ decoder.mFirstFrameTime &&
+ decoder.mError.ref() == NS_ERROR_DOM_MEDIA_DECODE_ERR &&
+ decoder.mDecoder && decoder.mDecoder->IsHardwareAccelerated(error) &&
+ !decoder.mHardwareDecodingDisabled;
+ bool needsNewDecoder =
+ decoder.mError.ref() == NS_ERROR_DOM_MEDIA_NEED_NEW_DECODER ||
+ firstFrameDecodingFailedWithHardware;
+ // Limit number of process restarts after crash
+ if ((decoder.mError.ref() ==
+ NS_ERROR_DOM_MEDIA_REMOTE_DECODER_CRASHED_RDD_OR_GPU_ERR &&
+ decoder.mNumOfConsecutiveRDDOrGPUCrashes++ <
+ decoder.mMaxConsecutiveRDDOrGPUCrashes) ||
+ (decoder.mError.ref() ==
+ NS_ERROR_DOM_MEDIA_REMOTE_DECODER_CRASHED_UTILITY_ERR &&
+ decoder.mNumOfConsecutiveUtilityCrashes++ <
+ decoder.mMaxConsecutiveUtilityCrashes)) {
+ needsNewDecoder = true;
+ }
+ // For MF CDM crash, it needs to be handled differently. We need to shutdown
+ // current decoder and report that error to the state machine in order to
+ // let it to determine if playback can keep going or not.
+ if (decoder.mError.ref() ==
+ NS_ERROR_DOM_MEDIA_REMOTE_DECODER_CRASHED_MF_CDM_ERR) {
+ LOG("Error: notify MF CDM crash and shutdown %s decoder",
+ TrackTypeToStr(aTrack));
+ ShutdownDecoder(aTrack);
+ decoder.RejectPromise(decoder.mError.ref(), __func__);
+ decoder.mError.reset();
+ return;
+ }
+#ifdef XP_LINUX
+ // We failed to decode on Linux with HW decoder,
+ // give it another try without HW decoder.
+ if (decoder.mError.ref() == NS_ERROR_DOM_MEDIA_DECODE_ERR &&
+ decoder.mDecoder->IsHardwareAccelerated(error)) {
+ LOG("Error: %s decode error, disable HW acceleration",
+ TrackTypeToStr(aTrack));
+ needsNewDecoder = true;
+ decoder.mHardwareDecodingDisabled = true;
+ }
+ // RDD process crashed on Linux, give it another try without HW decoder.
+ if (decoder.mError.ref() ==
+ NS_ERROR_DOM_MEDIA_REMOTE_DECODER_CRASHED_RDD_OR_GPU_ERR) {
+ LOG("Error: %s remote decoder crashed, disable HW acceleration",
+ TrackTypeToStr(aTrack));
+ decoder.mHardwareDecodingDisabled = true;
+ }
+#endif
+ // We don't want to expose crash error so switch to
+ // NS_ERROR_DOM_MEDIA_DECODE_ERR.
+ if (decoder.mError.ref() ==
+ NS_ERROR_DOM_MEDIA_REMOTE_DECODER_CRASHED_RDD_OR_GPU_ERR ||
+ decoder.mError.ref() ==
+ NS_ERROR_DOM_MEDIA_REMOTE_DECODER_CRASHED_UTILITY_ERR) {
+ decoder.mError = Some(MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ RESULT_DETAIL("Unable to decode")));
+ }
+ if (!needsNewDecoder && ++decoder.mNumOfConsecutiveDecodingError >
+ decoder.mMaxConsecutiveDecodingError) {
+ DDLOG(DDLogCategory::Log, "too_many_decode_errors", decoder.mError.ref());
+ NotifyError(aTrack, decoder.mError.ref());
+ return;
+ }
+
+ if (firstFrameDecodingFailedWithHardware) {
+ decoder.mHardwareDecodingDisabled = true;
+ }
+ decoder.mError.reset();
+
+ LOG("%s decoded error count %d RDD crashes count %d",
+ TrackTypeToStr(aTrack), decoder.mNumOfConsecutiveDecodingError,
+ decoder.mNumOfConsecutiveRDDOrGPUCrashes);
+
+ if (needsNewDecoder) {
+ LOG("Error: %s needs a new decoder", TrackTypeToStr(aTrack));
+ ShutdownDecoder(aTrack);
+ }
+ if (decoder.mFirstFrameTime) {
+ TimeInterval seekInterval = TimeInterval(decoder.mFirstFrameTime.ref(),
+ decoder.mFirstFrameTime.ref());
+ InternalSeek(aTrack, InternalSeekTarget(seekInterval, false));
+ return;
+ }
+
+ TimeUnit nextKeyframe;
+ if (aTrack == TrackType::kVideoTrack &&
+ NS_SUCCEEDED(
+ decoder.mTrackDemuxer->GetNextRandomAccessPoint(&nextKeyframe)) &&
+ !nextKeyframe.IsInfinite()) {
+ SkipVideoDemuxToNextKeyFrame(
+ decoder.mLastDecodedSampleTime.refOr(TimeInterval()).Length());
+ } else if (aTrack == TrackType::kAudioTrack) {
+ decoder.Flush();
+ } else {
+ DDLOG(DDLogCategory::Log, "no_keyframe", NS_ERROR_DOM_MEDIA_FATAL_ERR);
+ // We can't recover from this error.
+ NotifyError(aTrack, NS_ERROR_DOM_MEDIA_FATAL_ERR);
+ }
+ return;
+ }
+
+ bool needInput = NeedInput(decoder);
+
+ LOGV("Update(%s) ni=%d no=%d in:%" PRIu64 " out:%" PRIu64
+ " qs=%u decoding:%d flushing:%d desc:%s pending:%u waiting:%d eos:%d "
+ "ds:%d sid:%u waitcdm:%d",
+ TrackTypeToStr(aTrack), needInput, needOutput, decoder.mNumSamplesInput,
+ decoder.mNumSamplesOutput, uint32_t(size_t(decoder.mSizeOfQueue)),
+ decoder.mDecodeRequest.Exists(), decoder.mFlushing,
+ decoder.mDescription.get(), uint32_t(decoder.mOutput.Length()),
+ decoder.mWaitingForData, decoder.mDemuxEOS, int32_t(decoder.mDrainState),
+ decoder.mLastStreamSourceID, IsDecoderWaitingForCDM(aTrack));
+
+ if (IsWaitingOnCDMResource() || !ResolveSetCDMPromiseIfDone(aTrack)) {
+ // If the content is encrypted, MFR won't start to create decoder until
+ // CDMProxy is set.
+ return;
+ }
+
+ if ((decoder.IsWaitingForData() &&
+ (!decoder.mTimeThreshold || decoder.mTimeThreshold.ref().mWaiting)) ||
+ (decoder.IsWaitingForKey())) {
+ // Nothing more we can do at present.
+ LOGV("Still waiting for data or key. data(%d)/key(%d)",
+ decoder.mWaitingForData, decoder.mWaitingForKey);
+ return;
+ }
+
+ if (decoder.CancelWaitingForKey()) {
+ LOGV("No longer waiting for key. Resolving waiting promise");
+ return;
+ }
+
+ if (!needInput) {
+ LOGV("No need for additional input (pending:%u)",
+ uint32_t(decoder.mOutput.Length()));
+ return;
+ }
+
+ // Demux samples if we don't have some.
+ RequestDemuxSamples(aTrack);
+
+ HandleDemuxedSamples(aTrack, a);
+}
+
+void MediaFormatReader::ReturnOutput(MediaData* aData, TrackType aTrack) {
+ AUTO_PROFILER_LABEL("MediaFormatReader::ReturnOutput", MEDIA_PLAYBACK);
+ MOZ_ASSERT(GetDecoderData(aTrack).HasPromise());
+ MOZ_DIAGNOSTIC_ASSERT(aData->mType != MediaData::Type::NULL_DATA);
+ LOG("Resolved data promise for %s [%" PRId64 ", %" PRId64 "]",
+ TrackTypeToStr(aTrack), aData->mTime.ToMicroseconds(),
+ aData->GetEndTime().ToMicroseconds());
+
+ if (aTrack == TrackInfo::kAudioTrack) {
+ AudioData* audioData = aData->As<AudioData>();
+
+ if (audioData->mChannels != mInfo.mAudio.mChannels ||
+ audioData->mRate != mInfo.mAudio.mRate) {
+ LOG("change of audio format (rate:%d->%d). "
+ "This is an unsupported configuration",
+ mInfo.mAudio.mRate, audioData->mRate);
+ mInfo.mAudio.mRate = audioData->mRate;
+ mInfo.mAudio.mChannels = audioData->mChannels;
+ MutexAutoLock lock(mAudio.mMutex);
+ mAudio.mWorkingInfo->GetAsAudioInfo()->mRate = audioData->mRate;
+ mAudio.mWorkingInfo->GetAsAudioInfo()->mChannels = audioData->mChannels;
+ mWorkingInfoChanged = true;
+ }
+ mAudio.ResolvePromise(audioData, __func__);
+ } else if (aTrack == TrackInfo::kVideoTrack) {
+ VideoData* videoData = aData->As<VideoData>();
+
+ if (videoData->mDisplay != mInfo.mVideo.mDisplay) {
+ LOG("change of video display size (%dx%d->%dx%d)",
+ mInfo.mVideo.mDisplay.width, mInfo.mVideo.mDisplay.height,
+ videoData->mDisplay.width, videoData->mDisplay.height);
+ mInfo.mVideo.mDisplay = videoData->mDisplay;
+ MutexAutoLock lock(mVideo.mMutex);
+ mVideo.mWorkingInfo->GetAsVideoInfo()->mDisplay = videoData->mDisplay;
+ mWorkingInfoChanged = true;
+ }
+
+ mozilla::gfx::ColorDepth colorDepth = videoData->GetColorDepth();
+ if (colorDepth != mInfo.mVideo.mColorDepth) {
+ LOG("change of video color depth (enum %u -> enum %u)",
+ (unsigned)mInfo.mVideo.mColorDepth, (unsigned)colorDepth);
+ mInfo.mVideo.mColorDepth = colorDepth;
+ MutexAutoLock lock(mVideo.mMutex);
+ mVideo.mWorkingInfo->GetAsVideoInfo()->mColorDepth = colorDepth;
+ mWorkingInfoChanged = true;
+ }
+
+ TimeUnit nextKeyframe;
+ if (!mVideo.HasInternalSeekPending() &&
+ NS_SUCCEEDED(
+ mVideo.mTrackDemuxer->GetNextRandomAccessPoint(&nextKeyframe))) {
+ videoData->SetNextKeyFrameTime(nextKeyframe);
+ }
+
+ mVideo.ResolvePromise(videoData, __func__);
+ }
+}
+
+size_t MediaFormatReader::SizeOfVideoQueueInFrames() {
+ return SizeOfQueue(TrackInfo::kVideoTrack);
+}
+
+size_t MediaFormatReader::SizeOfAudioQueueInFrames() {
+ return SizeOfQueue(TrackInfo::kAudioTrack);
+}
+
+size_t MediaFormatReader::SizeOfQueue(TrackType aTrack) {
+ auto& decoder = GetDecoderData(aTrack);
+ return decoder.mSizeOfQueue;
+}
+
+RefPtr<MediaFormatReader::WaitForDataPromise> MediaFormatReader::WaitForData(
+ MediaData::Type aType) {
+ MOZ_ASSERT(OnTaskQueue());
+ TrackType trackType = aType == MediaData::Type::VIDEO_DATA
+ ? TrackType::kVideoTrack
+ : TrackType::kAudioTrack;
+ auto& decoder = GetDecoderData(trackType);
+ if (!decoder.IsWaitingForData() && !decoder.IsWaitingForKey()) {
+ // We aren't waiting for anything.
+ return WaitForDataPromise::CreateAndResolve(decoder.mType, __func__);
+ }
+ RefPtr<WaitForDataPromise> p = decoder.mWaitingPromise.Ensure(__func__);
+ ScheduleUpdate(trackType);
+ return p;
+}
+
+nsresult MediaFormatReader::ResetDecode(TrackSet aTracks) {
+ AUTO_PROFILER_LABEL("MediaFormatReader::ResetDecode", MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+ LOGV("");
+
+ mSeekPromise.RejectIfExists(NS_OK, __func__);
+ mSkipRequest.DisconnectIfExists();
+
+ // Do the same for any data wait promises.
+ if (aTracks.contains(TrackInfo::kAudioTrack)) {
+ mAudio.mWaitingPromise.RejectIfExists(
+ WaitForDataRejectValue(MediaData::Type::AUDIO_DATA,
+ WaitForDataRejectValue::CANCELED),
+ __func__);
+ }
+
+ if (aTracks.contains(TrackInfo::kVideoTrack)) {
+ mVideo.mWaitingPromise.RejectIfExists(
+ WaitForDataRejectValue(MediaData::Type::VIDEO_DATA,
+ WaitForDataRejectValue::CANCELED),
+ __func__);
+ }
+
+ // Reset miscellaneous seeking state.
+ mPendingSeekTime.reset();
+
+ if (HasVideo() && aTracks.contains(TrackInfo::kVideoTrack)) {
+ mVideo.ResetDemuxer();
+ mVideo.mFirstFrameTime = Some(media::TimeUnit::Zero());
+ Reset(TrackInfo::kVideoTrack);
+ if (mVideo.HasPromise()) {
+ mVideo.RejectPromise(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ }
+ }
+
+ if (HasAudio() && aTracks.contains(TrackInfo::kAudioTrack)) {
+ mAudio.ResetDemuxer();
+ mVideo.mFirstFrameTime = Some(media::TimeUnit::Zero());
+ Reset(TrackInfo::kAudioTrack);
+ if (mAudio.HasPromise()) {
+ mAudio.RejectPromise(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ }
+ }
+
+ return NS_OK;
+}
+
+void MediaFormatReader::Reset(TrackType aTrack) {
+ MOZ_ASSERT(OnTaskQueue());
+ LOG("Reset(%s) BEGIN", TrackTypeToStr(aTrack));
+
+ auto& decoder = GetDecoderData(aTrack);
+
+ decoder.ResetState();
+ decoder.Flush();
+
+ LOG("Reset(%s) END", TrackTypeToStr(aTrack));
+}
+
+void MediaFormatReader::DropDecodedSamples(TrackType aTrack) {
+ MOZ_ASSERT(OnTaskQueue());
+ auto& decoder = GetDecoderData(aTrack);
+ size_t lengthDecodedQueue = decoder.mOutput.Length();
+ if (lengthDecodedQueue && decoder.mTimeThreshold.isSome()) {
+ auto time = decoder.mOutput.LastElement()->mTime;
+ if (time >= decoder.mTimeThreshold.ref().Time()) {
+ // We would have reached our internal seek target.
+ decoder.mTimeThreshold.reset();
+ }
+ }
+ decoder.mOutput.Clear();
+ decoder.mSizeOfQueue -= lengthDecodedQueue;
+ if (aTrack == TrackInfo::kVideoTrack && mFrameStats) {
+ mFrameStats->Accumulate({0, 0, 0, lengthDecodedQueue, 0, 0});
+ }
+}
+
+void MediaFormatReader::SkipVideoDemuxToNextKeyFrame(TimeUnit aTimeThreshold) {
+ AUTO_PROFILER_LABEL("MediaFormatReader::SkipVideoDemuxToNextKeyFrame",
+ MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+ LOG("Skipping up to %" PRId64, aTimeThreshold.ToMicroseconds());
+
+ // We've reached SkipVideoDemuxToNextKeyFrame when our decoding is late.
+ // As such we can drop all already decoded samples and discard all pending
+ // samples.
+ DropDecodedSamples(TrackInfo::kVideoTrack);
+
+ mVideo.mTrackDemuxer->SkipToNextRandomAccessPoint(aTimeThreshold)
+ ->Then(OwnerThread(), __func__, this,
+ &MediaFormatReader::OnVideoSkipCompleted,
+ &MediaFormatReader::OnVideoSkipFailed)
+ ->Track(mSkipRequest);
+}
+
+void MediaFormatReader::VideoSkipReset(uint32_t aSkipped) {
+ PROFILER_MARKER_UNTYPED("SkippedVideoDecode", MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+
+ // Some frames may have been output by the decoder since we initiated the
+ // videoskip process and we know they would be late.
+ DropDecodedSamples(TrackInfo::kVideoTrack);
+ // Report the pending frames as dropped.
+ if (mFrameStats) {
+ uint32_t droppedDecoderCount = SizeOfVideoQueueInFrames();
+ mFrameStats->Accumulate({0, 0, 0, droppedDecoderCount, 0, 0});
+ }
+
+ // Cancel any pending demux request and pending demuxed samples.
+ mVideo.mDemuxRequest.DisconnectIfExists();
+ Reset(TrackType::kVideoTrack);
+
+ if (mFrameStats) {
+ mFrameStats->Accumulate({aSkipped, 0, 0, aSkipped, 0, 0});
+ }
+
+ mVideo.mNumSamplesSkippedTotal += aSkipped;
+}
+
+void MediaFormatReader::OnVideoSkipCompleted(uint32_t aSkipped) {
+ AUTO_PROFILER_LABEL("MediaFormatReader::OnVideoSkipCompleted",
+ MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+ LOG("Skipping succeeded, skipped %u frames", aSkipped);
+ mSkipRequest.Complete();
+
+ DDLOG(DDLogCategory::Log, "video_skipped", DDNoValue());
+
+ VideoSkipReset(aSkipped);
+
+ ScheduleUpdate(TrackInfo::kVideoTrack);
+}
+
+void MediaFormatReader::OnVideoSkipFailed(
+ MediaTrackDemuxer::SkipFailureHolder aFailure) {
+ AUTO_PROFILER_LABEL("MediaFormatReader::OnVideoSkipFailed", MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+ LOG("Skipping failed, skipped %u frames", aFailure.mSkipped);
+ mSkipRequest.Complete();
+
+ switch (aFailure.mFailure.Code()) {
+ case NS_ERROR_DOM_MEDIA_END_OF_STREAM:
+ case NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA:
+ DDLOG(DDLogCategory::Log, "video_skipping_interruption",
+ aFailure.mFailure);
+ // Some frames may have been output by the decoder since we initiated the
+ // videoskip process and we know they would be late.
+ DropDecodedSamples(TrackInfo::kVideoTrack);
+ // We can't complete the skip operation, will just service a video frame
+ // normally.
+ ScheduleUpdate(TrackInfo::kVideoTrack);
+ break;
+ case NS_ERROR_DOM_MEDIA_CANCELED:
+ DDLOG(DDLogCategory::Log, "video_skipping_interruption",
+ aFailure.mFailure);
+ if (mVideo.HasPromise()) {
+ mVideo.RejectPromise(aFailure.mFailure, __func__);
+ }
+ break;
+ default:
+ DDLOG(DDLogCategory::Log, "video_skipping_error", aFailure.mFailure);
+ NotifyError(TrackType::kVideoTrack, aFailure.mFailure);
+ break;
+ }
+}
+
+RefPtr<MediaFormatReader::SeekPromise> MediaFormatReader::Seek(
+ const SeekTarget& aTarget) {
+ AUTO_PROFILER_LABEL("MediaFormatReader::Seek", MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+
+ LOG("aTarget=(%" PRId64 "), track=%s", aTarget.GetTime().ToMicroseconds(),
+ SeekTarget::TrackToStr(aTarget.GetTrack()));
+
+ MOZ_DIAGNOSTIC_ASSERT(mSeekPromise.IsEmpty());
+ MOZ_DIAGNOSTIC_ASSERT(mPendingSeekTime.isNothing());
+ // Should reset data request, and no pending internal seek.
+ if (aTarget.IsAllTracks()) {
+ MOZ_DIAGNOSTIC_ASSERT(!mVideo.HasPromise());
+ MOZ_DIAGNOSTIC_ASSERT(!mAudio.HasPromise());
+ MOZ_DIAGNOSTIC_ASSERT(mVideo.mTimeThreshold.isNothing());
+ MOZ_DIAGNOSTIC_ASSERT(mAudio.mTimeThreshold.isNothing());
+ } else if (aTarget.IsVideoOnly()) {
+ MOZ_DIAGNOSTIC_ASSERT(!mVideo.HasPromise());
+ MOZ_DIAGNOSTIC_ASSERT(mVideo.mTimeThreshold.isNothing());
+ } else if (aTarget.IsAudioOnly()) {
+ MOZ_DIAGNOSTIC_ASSERT(!mAudio.HasPromise());
+ MOZ_DIAGNOSTIC_ASSERT(mAudio.mTimeThreshold.isNothing());
+ }
+
+ if (!mInfo.mMediaSeekable && !mInfo.mMediaSeekableOnlyInBufferedRanges) {
+ LOG("Seek() END (Unseekable)");
+ return SeekPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+
+ if (mShutdown) {
+ return SeekPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+
+ SetSeekTarget(aTarget);
+
+ RefPtr<SeekPromise> p = mSeekPromise.Ensure(__func__);
+
+ ScheduleSeek();
+
+ return p;
+}
+
+void MediaFormatReader::SetSeekTarget(const SeekTarget& aTarget) {
+ MOZ_ASSERT(OnTaskQueue());
+
+ mOriginalSeekTarget = aTarget;
+ mFallbackSeekTime = mPendingSeekTime = Some(aTarget.GetTime());
+}
+
+void MediaFormatReader::ScheduleSeek() {
+ if (mSeekScheduled) {
+ return;
+ }
+ mSeekScheduled = true;
+ nsresult rv = OwnerThread()->Dispatch(NewRunnableMethod(
+ "MediaFormatReader::AttemptSeek", this, &MediaFormatReader::AttemptSeek));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+}
+
+void MediaFormatReader::AttemptSeek() {
+ AUTO_PROFILER_LABEL("MediaFormatReader::AttemptSeek", MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+
+ mSeekScheduled = false;
+
+ if (mPendingSeekTime.isNothing()) {
+ LOGV("AttemptSeek, no pending seek time?");
+ return;
+ }
+
+ // Only reset the demuxers targeted by this SeekTarget, to avoid A/V sync
+ // issues.
+ const bool isSeekingAudio = HasAudio() && !mOriginalSeekTarget.IsVideoOnly();
+ const bool isSeekingVideo = HasVideo() && !mOriginalSeekTarget.IsAudioOnly();
+ LOG("AttemptSeek, seekingAudio=%d, seekingVideo=%d", isSeekingAudio,
+ isSeekingVideo);
+ if (isSeekingVideo) {
+ mVideo.ResetDemuxer();
+ mVideo.ResetState();
+ }
+ if (isSeekingAudio) {
+ mAudio.ResetDemuxer();
+ mAudio.ResetState();
+ }
+
+ // If seeking both tracks, seek the video track, and then the audio track when
+ // the video track seek has completed. Otherwise, only seek a specific track.
+ if (isSeekingVideo) {
+ DoVideoSeek();
+ } else if (isSeekingAudio) {
+ DoAudioSeek();
+ } else {
+ MOZ_CRASH();
+ }
+}
+
+void MediaFormatReader::OnSeekFailed(TrackType aTrack,
+ const MediaResult& aError) {
+ AUTO_PROFILER_LABEL("MediaFormatReader::OnSeekFailed", MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+ LOGV("%s failure:%s", TrackTypeToStr(aTrack), aError.ErrorName().get());
+ if (aTrack == TrackType::kVideoTrack) {
+ mVideo.mSeekRequest.Complete();
+ } else {
+ mAudio.mSeekRequest.Complete();
+ }
+
+ if (aError == NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA) {
+ if (HasVideo() && aTrack == TrackType::kAudioTrack &&
+ mFallbackSeekTime.isSome() &&
+ mPendingSeekTime.ref() != mFallbackSeekTime.ref()) {
+ // We have failed to seek audio where video seeked to earlier.
+ // Attempt to seek instead to the closest point that we know we have in
+ // order to limit A/V sync discrepency.
+
+ // Ensure we have the most up to date buffered ranges.
+ UpdateReceivedNewData(TrackType::kAudioTrack);
+ Maybe<TimeUnit> nextSeekTime;
+ // Find closest buffered time found after video seeked time.
+ for (const auto& timeRange : mAudio.mTimeRanges) {
+ if (timeRange.mStart >= mPendingSeekTime.ref()) {
+ nextSeekTime.emplace(timeRange.mStart);
+ break;
+ }
+ }
+ if (nextSeekTime.isNothing() ||
+ nextSeekTime.ref() > mFallbackSeekTime.ref()) {
+ nextSeekTime = Some(mFallbackSeekTime.ref());
+ LOG("Unable to seek audio to video seek time. A/V sync may be broken");
+ } else {
+ mFallbackSeekTime.reset();
+ }
+ mPendingSeekTime = nextSeekTime;
+ DoAudioSeek();
+ return;
+ }
+ NotifyWaitingForData(aTrack);
+ }
+ MOZ_ASSERT(!mVideo.mSeekRequest.Exists() && !mAudio.mSeekRequest.Exists());
+ mPendingSeekTime.reset();
+
+ auto type = aTrack == TrackType::kAudioTrack ? MediaData::Type::AUDIO_DATA
+ : MediaData::Type::VIDEO_DATA;
+ mSeekPromise.RejectIfExists(SeekRejectValue(type, aError), __func__);
+}
+
+void MediaFormatReader::DoVideoSeek() {
+ AUTO_PROFILER_LABEL("MediaFormatReader::DoVideoSeek", MEDIA_PLAYBACK);
+ MOZ_ASSERT(mPendingSeekTime.isSome());
+ LOGV("Seeking video to %" PRId64, mPendingSeekTime.ref().ToMicroseconds());
+ MOZ_DIAGNOSTIC_ASSERT(!IsAudioOnlySeeking());
+ MOZ_DIAGNOSTIC_ASSERT(!mVideo.mSeekRequest.Exists());
+ auto seekTime = mPendingSeekTime.ref();
+ mVideo.mTrackDemuxer->Seek(seekTime)
+ ->Then(OwnerThread(), __func__, this,
+ &MediaFormatReader::OnVideoSeekCompleted,
+ &MediaFormatReader::OnVideoSeekFailed)
+ ->Track(mVideo.mSeekRequest);
+}
+
+void MediaFormatReader::OnVideoSeekCompleted(TimeUnit aTime) {
+ AUTO_PROFILER_LABEL("MediaFormatReader::OnVideoSeekCompleted",
+ MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+ LOGV("Video seeked to %" PRId64, aTime.ToMicroseconds());
+ mVideo.mSeekRequest.Complete();
+
+ mVideo.mFirstFrameTime = Some(aTime);
+ mPreviousDecodedKeyframeTime_us = sNoPreviousDecodedKeyframe;
+
+ SetVideoDecodeThreshold();
+
+ if (HasAudio() && !mOriginalSeekTarget.IsVideoOnly()) {
+ MOZ_ASSERT(mPendingSeekTime.isSome());
+ if (mOriginalSeekTarget.IsFast()) {
+ // We are performing a fast seek. We need to seek audio to where the
+ // video seeked to, to ensure proper A/V sync once playback resume.
+ mPendingSeekTime = Some(aTime);
+ }
+ DoAudioSeek();
+ } else {
+ mPendingSeekTime.reset();
+ mSeekPromise.ResolveIfExists(aTime, __func__);
+ }
+}
+
+void MediaFormatReader::OnVideoSeekFailed(const MediaResult& aError) {
+ AUTO_PROFILER_LABEL("MediaFormatReader::OnVideoSeekFailed", MEDIA_PLAYBACK);
+ mPreviousDecodedKeyframeTime_us = sNoPreviousDecodedKeyframe;
+ OnSeekFailed(TrackType::kVideoTrack, aError);
+}
+
+void MediaFormatReader::SetVideoDecodeThreshold() {
+ MOZ_ASSERT(OnTaskQueue());
+
+ if (!HasVideo() || !mVideo.mDecoder) {
+ return;
+ }
+
+ if (!mVideo.mTimeThreshold && !IsSeeking()) {
+ return;
+ }
+
+ TimeUnit threshold;
+ if (mVideo.mTimeThreshold) {
+ // For internalSeek.
+ threshold = mVideo.mTimeThreshold.ref().Time();
+ } else if (IsSeeking()) {
+ // If IsSeeking() is true, then video seek must have completed already.
+ TimeUnit keyframe;
+ if (NS_FAILED(mVideo.mTrackDemuxer->GetNextRandomAccessPoint(&keyframe))) {
+ return;
+ }
+
+ // If the key frame is invalid/infinite, it means the target position is
+ // closing to end of stream. We don't want to skip any frame at this point.
+ threshold = keyframe.IsValid() && !keyframe.IsInfinite()
+ ? mOriginalSeekTarget.GetTime()
+ : TimeUnit::Invalid();
+ } else {
+ return;
+ }
+
+ if (threshold.IsValid()) {
+ LOG("Set seek threshold to %" PRId64, threshold.ToMicroseconds());
+ } else {
+ LOG("Resetting seek threshold");
+ }
+ mVideo.mDecoder->SetSeekThreshold(threshold);
+}
+
+void MediaFormatReader::DoAudioSeek() {
+ AUTO_PROFILER_LABEL("MediaFormatReader::DoAudioSeek", MEDIA_PLAYBACK);
+ MOZ_ASSERT(mPendingSeekTime.isSome());
+ LOGV("Seeking audio to %" PRId64, mPendingSeekTime.ref().ToMicroseconds());
+ MOZ_DIAGNOSTIC_ASSERT(!IsVideoOnlySeeking());
+ MOZ_DIAGNOSTIC_ASSERT(!mAudio.mSeekRequest.Exists());
+ auto seekTime = mPendingSeekTime.ref();
+ mAudio.mTrackDemuxer->Seek(seekTime)
+ ->Then(OwnerThread(), __func__, this,
+ &MediaFormatReader::OnAudioSeekCompleted,
+ &MediaFormatReader::OnAudioSeekFailed)
+ ->Track(mAudio.mSeekRequest);
+}
+
+void MediaFormatReader::OnAudioSeekCompleted(TimeUnit aTime) {
+ MOZ_ASSERT(OnTaskQueue());
+ AUTO_PROFILER_LABEL("MediaFormatReader::OnAudioSeekCompleted",
+ MEDIA_PLAYBACK);
+ LOGV("Audio seeked to %" PRId64, aTime.ToMicroseconds());
+ mAudio.mSeekRequest.Complete();
+ mAudio.mFirstFrameTime = Some(aTime);
+ mPendingSeekTime.reset();
+ mSeekPromise.ResolveIfExists(aTime, __func__);
+}
+
+void MediaFormatReader::OnAudioSeekFailed(const MediaResult& aError) {
+ AUTO_PROFILER_LABEL("MediaFormatReader::OnAudioSeekFailed", MEDIA_PLAYBACK);
+ OnSeekFailed(TrackType::kAudioTrack, aError);
+}
+
+void MediaFormatReader::ReleaseResources() {
+ LOGV("");
+ if (mShutdown) {
+ return;
+ }
+ ShutdownDecoder(TrackInfo::kAudioTrack);
+ ShutdownDecoder(TrackInfo::kVideoTrack);
+}
+
+bool MediaFormatReader::VideoIsHardwareAccelerated() const {
+ return mVideo.mIsHardwareAccelerated;
+}
+
+void MediaFormatReader::NotifyTrackDemuxers() {
+ MOZ_ASSERT(OnTaskQueue());
+
+ LOGV("");
+
+ if (!mInitDone) {
+ return;
+ }
+
+ if (HasVideo()) {
+ mVideo.mReceivedNewData = true;
+ ScheduleUpdate(TrackType::kVideoTrack);
+ }
+ if (HasAudio()) {
+ mAudio.mReceivedNewData = true;
+ ScheduleUpdate(TrackType::kAudioTrack);
+ }
+}
+
+void MediaFormatReader::NotifyDataArrived() {
+ AUTO_PROFILER_LABEL("MediaFormatReader::NotifyDataArrived", MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+
+ if (mShutdown || !mDemuxer || !mDemuxerInitDone) {
+ return;
+ }
+
+ if (mNotifyDataArrivedPromise.Exists()) {
+ // Already one in progress. Set the dirty flag so we can process it later.
+ mPendingNotifyDataArrived = true;
+ return;
+ }
+
+ RefPtr<MediaFormatReader> self = this;
+ mDemuxer->NotifyDataArrived()
+ ->Then(
+ OwnerThread(), __func__,
+ [self]() {
+ AUTO_PROFILER_LABEL("MediaFormatReader::NotifyDataArrived:Resolved",
+ MEDIA_PLAYBACK);
+ self->mNotifyDataArrivedPromise.Complete();
+ self->UpdateBuffered();
+ self->NotifyTrackDemuxers();
+ if (self->mPendingNotifyDataArrived) {
+ self->mPendingNotifyDataArrived = false;
+ self->NotifyDataArrived();
+ }
+ },
+ [self]() { self->mNotifyDataArrivedPromise.Complete(); })
+ ->Track(mNotifyDataArrivedPromise);
+}
+
+void MediaFormatReader::UpdateMediaEngineId(uint64_t aMediaEngineId) {
+ LOG("Update external media engine Id %" PRIu64, aMediaEngineId);
+ mMediaEngineId = Some(aMediaEngineId);
+}
+
+void MediaFormatReader::UpdateBuffered() {
+ AUTO_PROFILER_LABEL("MediaFormatReader::UpdateBuffered", MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+
+ if (mShutdown) {
+ return;
+ }
+
+ if (!mInitDone || !mHasStartTime) {
+ mBuffered = TimeIntervals();
+ return;
+ }
+
+ if (HasVideo()) {
+ mVideo.mTimeRanges = mVideo.mTrackDemuxer->GetBuffered();
+ bool hasLastEnd;
+ auto lastEnd = mVideo.mTimeRanges.GetEnd(&hasLastEnd);
+ if (hasLastEnd) {
+ if (mVideo.mLastTimeRangesEnd &&
+ mVideo.mLastTimeRangesEnd.ref() < lastEnd) {
+ // New data was added after our previous end, we can clear the EOS flag.
+ mVideo.mDemuxEOS = false;
+ ScheduleUpdate(TrackInfo::kVideoTrack);
+ }
+ mVideo.mLastTimeRangesEnd = Some(lastEnd);
+ }
+ }
+ if (HasAudio()) {
+ mAudio.mTimeRanges = mAudio.mTrackDemuxer->GetBuffered();
+ bool hasLastEnd;
+ auto lastEnd = mAudio.mTimeRanges.GetEnd(&hasLastEnd);
+ if (hasLastEnd) {
+ if (mAudio.mLastTimeRangesEnd &&
+ mAudio.mLastTimeRangesEnd.ref() < lastEnd) {
+ // New data was added after our previous end, we can clear the EOS flag.
+ mAudio.mDemuxEOS = false;
+ ScheduleUpdate(TrackInfo::kAudioTrack);
+ }
+ mAudio.mLastTimeRangesEnd = Some(lastEnd);
+ }
+ }
+
+ media::TimeIntervals intervals;
+ if (HasAudio() && HasVideo()) {
+ intervals = media::Intersection(mVideo.mTimeRanges, mAudio.mTimeRanges);
+ } else if (HasAudio()) {
+ intervals = mAudio.mTimeRanges;
+ } else if (HasVideo()) {
+ intervals = mVideo.mTimeRanges;
+ }
+
+ if (intervals.IsEmpty() || intervals.GetStart() == TimeUnit::Zero()) {
+ // IntervalSet already starts at 0 or is empty, nothing to shift.
+ mBuffered = intervals;
+ } else {
+ mBuffered = intervals.Shift(TimeUnit::Zero() - mInfo.mStartTime);
+ }
+}
+
+layers::ImageContainer* MediaFormatReader::GetImageContainer() {
+ return mVideoFrameContainer ? mVideoFrameContainer->GetImageContainer()
+ : nullptr;
+}
+
+RefPtr<GenericPromise> MediaFormatReader::RequestDebugInfo(
+ dom::MediaFormatReaderDebugInfo& aInfo) {
+ if (!OnTaskQueue()) {
+ // Run the request on the task queue if it's not already.
+ return InvokeAsync(mTaskQueue, __func__,
+ [this, self = RefPtr{this}, &aInfo] {
+ return RequestDebugInfo(aInfo);
+ });
+ }
+ GetDebugInfo(aInfo);
+ return GenericPromise::CreateAndResolve(true, __func__);
+}
+
+Maybe<nsCString> MediaFormatReader::GetAudioProcessPerCodec() {
+ if (mAudio.mDescription == "uninitialized"_ns) {
+ return Nothing();
+ }
+
+ MOZ_ASSERT(mAudio.mProcessName.Length() > 0,
+ "Should have had a process name");
+ MOZ_ASSERT(mAudio.mCodecName.Length() > 0, "Should have had a codec name");
+
+ nsCString processName = mAudio.mProcessName;
+ nsCString audioProcessPerCodecName(processName + ","_ns + mAudio.mCodecName);
+ if (processName != "utility"_ns) {
+ if (!StaticPrefs::media_rdd_process_enabled()) {
+ audioProcessPerCodecName += ",rdd-disabled"_ns;
+ }
+ if (!StaticPrefs::media_utility_process_enabled()) {
+ audioProcessPerCodecName += ",utility-disabled"_ns;
+ }
+ }
+ return Some(audioProcessPerCodecName);
+}
+
+void MediaFormatReader::GetDebugInfo(dom::MediaFormatReaderDebugInfo& aInfo) {
+ MOZ_ASSERT(OnTaskQueue(),
+ "Don't call this off the task queue, it's going to touch a lot of "
+ "data members");
+ nsCString result;
+ nsAutoCString audioDecoderName("unavailable");
+ nsAutoCString videoDecoderName = audioDecoderName;
+ nsAutoCString audioType("none");
+ nsAutoCString videoType("none");
+
+ AudioInfo audioInfo;
+ if (HasAudio()) {
+ audioInfo = *mAudio.GetWorkingInfo()->GetAsAudioInfo();
+ audioDecoderName = mAudio.mDecoder ? mAudio.mDecoder->GetDescriptionName()
+ : mAudio.mDescription;
+ audioType = audioInfo.mMimeType;
+ aInfo.mAudioState.mNeedInput = NeedInput(mAudio);
+ aInfo.mAudioState.mHasPromise = mAudio.HasPromise();
+ aInfo.mAudioState.mWaitingPromise = !mAudio.mWaitingPromise.IsEmpty();
+ aInfo.mAudioState.mHasDemuxRequest = mAudio.mDemuxRequest.Exists();
+ aInfo.mAudioState.mDemuxQueueSize =
+ uint32_t(mAudio.mQueuedSamples.Length());
+ aInfo.mAudioState.mHasDecoder = mAudio.mDecodeRequest.Exists();
+ aInfo.mAudioState.mTimeTreshold =
+ mAudio.mTimeThreshold ? mAudio.mTimeThreshold.ref().Time().ToSeconds()
+ : -1.0;
+ aInfo.mAudioState.mTimeTresholdHasSeeked =
+ mAudio.mTimeThreshold ? mAudio.mTimeThreshold.ref().mHasSeeked : false;
+ aInfo.mAudioState.mNumSamplesInput = mAudio.mNumSamplesInput;
+ aInfo.mAudioState.mNumSamplesOutput = mAudio.mNumSamplesOutput;
+ aInfo.mAudioState.mQueueSize = size_t(mAudio.mSizeOfQueue);
+ aInfo.mAudioState.mPending = mAudio.mOutput.Length();
+ aInfo.mAudioState.mWaitingForData = mAudio.mWaitingForData;
+ aInfo.mAudioState.mDemuxEOS = mAudio.mDemuxEOS;
+ aInfo.mAudioState.mDrainState = int32_t(mAudio.mDrainState);
+ aInfo.mAudioState.mWaitingForKey = mAudio.mWaitingForKey;
+ aInfo.mAudioState.mLastStreamSourceID = mAudio.mLastStreamSourceID;
+ }
+
+ CopyUTF8toUTF16(audioDecoderName, aInfo.mAudioDecoderName);
+ CopyUTF8toUTF16(audioType, aInfo.mAudioType);
+ aInfo.mAudioChannels = audioInfo.mChannels;
+ aInfo.mAudioRate = audioInfo.mRate;
+ aInfo.mAudioFramesDecoded = mAudio.mNumSamplesOutputTotal;
+
+ VideoInfo videoInfo;
+ if (HasVideo()) {
+ videoInfo = *mVideo.GetWorkingInfo()->GetAsVideoInfo();
+ videoDecoderName = mVideo.mDecoder ? mVideo.mDecoder->GetDescriptionName()
+ : mVideo.mDescription;
+ videoType = videoInfo.mMimeType;
+ aInfo.mVideoState.mNeedInput = NeedInput(mVideo);
+ aInfo.mVideoState.mHasPromise = mVideo.HasPromise();
+ aInfo.mVideoState.mWaitingPromise = !mVideo.mWaitingPromise.IsEmpty();
+ aInfo.mVideoState.mHasDemuxRequest = mVideo.mDemuxRequest.Exists();
+ aInfo.mVideoState.mDemuxQueueSize =
+ uint32_t(mVideo.mQueuedSamples.Length());
+ aInfo.mVideoState.mHasDecoder = mVideo.mDecodeRequest.Exists();
+ aInfo.mVideoState.mTimeTreshold =
+ mVideo.mTimeThreshold ? mVideo.mTimeThreshold.ref().Time().ToSeconds()
+ : -1.0;
+ aInfo.mVideoState.mTimeTresholdHasSeeked =
+ mVideo.mTimeThreshold ? mVideo.mTimeThreshold.ref().mHasSeeked : false;
+ aInfo.mVideoState.mNumSamplesInput = mVideo.mNumSamplesInput;
+ aInfo.mVideoState.mNumSamplesOutput = mVideo.mNumSamplesOutput;
+ aInfo.mVideoState.mQueueSize = size_t(mVideo.mSizeOfQueue);
+ aInfo.mVideoState.mPending = mVideo.mOutput.Length();
+ aInfo.mVideoState.mWaitingForData = mVideo.mWaitingForData;
+ aInfo.mVideoState.mDemuxEOS = mVideo.mDemuxEOS;
+ aInfo.mVideoState.mDrainState = int32_t(mVideo.mDrainState);
+ aInfo.mVideoState.mWaitingForKey = mVideo.mWaitingForKey;
+ aInfo.mVideoState.mLastStreamSourceID = mVideo.mLastStreamSourceID;
+ }
+
+ CopyUTF8toUTF16(videoDecoderName, aInfo.mVideoDecoderName);
+ CopyUTF8toUTF16(videoType, aInfo.mVideoType);
+ aInfo.mVideoWidth =
+ videoInfo.mDisplay.width < 0 ? 0 : videoInfo.mDisplay.width;
+ aInfo.mVideoHeight =
+ videoInfo.mDisplay.height < 0 ? 0 : videoInfo.mDisplay.height;
+ aInfo.mVideoRate = mVideo.mMeanRate.Mean();
+ aInfo.mVideoHardwareAccelerated = VideoIsHardwareAccelerated();
+ aInfo.mVideoNumSamplesOutputTotal = mVideo.mNumSamplesOutputTotal;
+ aInfo.mVideoNumSamplesSkippedTotal = mVideo.mNumSamplesSkippedTotal;
+
+ // Looking at dropped frames
+ FrameStatisticsData stats = mFrameStats->GetFrameStatisticsData();
+ aInfo.mFrameStats.mDroppedDecodedFrames = stats.mDroppedDecodedFrames;
+ aInfo.mFrameStats.mDroppedSinkFrames = stats.mDroppedSinkFrames;
+ aInfo.mFrameStats.mDroppedCompositorFrames = stats.mDroppedCompositorFrames;
+}
+
+void MediaFormatReader::SetVideoNullDecode(bool aIsNullDecode) {
+ MOZ_ASSERT(OnTaskQueue());
+ return SetNullDecode(TrackType::kVideoTrack, aIsNullDecode);
+}
+
+void MediaFormatReader::UpdateCompositor(
+ already_AddRefed<layers::KnowsCompositor> aCompositor) {
+ MOZ_ASSERT(OnTaskQueue());
+ mKnowsCompositor = aCompositor;
+}
+
+void MediaFormatReader::SetNullDecode(TrackType aTrack, bool aIsNullDecode) {
+ MOZ_ASSERT(OnTaskQueue());
+
+ auto& decoder = GetDecoderData(aTrack);
+ if (decoder.mIsNullDecode == aIsNullDecode) {
+ return;
+ }
+
+ LOG("%s, decoder.mIsNullDecode = %d => aIsNullDecode = %d",
+ TrackTypeToStr(aTrack), decoder.mIsNullDecode, aIsNullDecode);
+
+ decoder.mIsNullDecode = aIsNullDecode;
+ ShutdownDecoder(aTrack);
+}
+
+void MediaFormatReader::OnFirstDemuxCompleted(
+ TrackInfo::TrackType aType,
+ RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples) {
+ AUTO_PROFILER_LABEL("MediaFormatReader::OnFirstDemuxCompleted",
+ MEDIA_PLAYBACK);
+ MOZ_ASSERT(OnTaskQueue());
+
+ if (mShutdown) {
+ return;
+ }
+
+ auto& decoder = GetDecoderData(aType);
+ MOZ_ASSERT(decoder.mFirstDemuxedSampleTime.isNothing());
+ decoder.mFirstDemuxedSampleTime.emplace(aSamples->GetSamples()[0]->mTime);
+ MaybeResolveMetadataPromise();
+}
+
+void MediaFormatReader::OnFirstDemuxFailed(TrackInfo::TrackType aType,
+ const MediaResult& aError) {
+ MOZ_ASSERT(OnTaskQueue());
+
+ if (mShutdown) {
+ return;
+ }
+
+ auto& decoder = GetDecoderData(aType);
+ MOZ_ASSERT(decoder.mFirstDemuxedSampleTime.isNothing());
+ decoder.mFirstDemuxedSampleTime.emplace(TimeUnit::FromInfinity());
+ MaybeResolveMetadataPromise();
+}
+
+} // namespace mozilla
+
+#undef NS_DispatchToMainThread
+#undef LOGV
+#undef LOG
diff --git a/dom/media/MediaFormatReader.h b/dom/media/MediaFormatReader.h
new file mode 100644
index 0000000000..ebbca0e179
--- /dev/null
+++ b/dom/media/MediaFormatReader.h
@@ -0,0 +1,885 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(MediaFormatReader_h_)
+# define MediaFormatReader_h_
+
+# include "FrameStatistics.h"
+# include "MediaDataDemuxer.h"
+# include "MediaEventSource.h"
+# include "MediaMetadataManager.h"
+# include "MediaPromiseDefs.h"
+# include "PlatformDecoderModule.h"
+# include "SeekTarget.h"
+# include "mozilla/Atomics.h"
+# include "mozilla/Maybe.h"
+# include "mozilla/MozPromise.h"
+# include "mozilla/Mutex.h"
+# include "mozilla/StateMirroring.h"
+# include "mozilla/StaticPrefs_media.h"
+# include "mozilla/TaskQueue.h"
+# include "mozilla/ThreadSafeWeakPtr.h"
+# include "mozilla/dom/MediaDebugInfoBinding.h"
+
+namespace mozilla {
+
+class CDMProxy;
+class GMPCrashHelper;
+class MediaResource;
+class VideoFrameContainer;
+
+struct WaitForDataRejectValue {
+ enum Reason { SHUTDOWN, CANCELED };
+
+ WaitForDataRejectValue(MediaData::Type aType, Reason aReason)
+ : mType(aType), mReason(aReason) {}
+ MediaData::Type mType;
+ Reason mReason;
+};
+
+struct SeekRejectValue {
+ MOZ_IMPLICIT SeekRejectValue(const MediaResult& aError)
+ : mType(MediaData::Type::NULL_DATA), mError(aError) {}
+ MOZ_IMPLICIT SeekRejectValue(nsresult aResult)
+ : mType(MediaData::Type::NULL_DATA), mError(aResult) {}
+ SeekRejectValue(MediaData::Type aType, const MediaResult& aError)
+ : mType(aType), mError(aError) {}
+ MediaData::Type mType;
+ MediaResult mError;
+};
+
+struct MetadataHolder {
+ UniquePtr<MediaInfo> mInfo;
+ UniquePtr<MetadataTags> mTags;
+};
+
+typedef void* MediaDecoderOwnerID;
+
+struct MOZ_STACK_CLASS MediaFormatReaderInit {
+ MediaResource* mResource = nullptr;
+ VideoFrameContainer* mVideoFrameContainer = nullptr;
+ FrameStatistics* mFrameStats = nullptr;
+ already_AddRefed<layers::KnowsCompositor> mKnowsCompositor;
+ already_AddRefed<GMPCrashHelper> mCrashHelper;
+ // Used in bug 1393399 for temporary telemetry.
+ MediaDecoderOwnerID mMediaDecoderOwnerID = nullptr;
+ Maybe<TrackingId> mTrackingId;
+};
+
+DDLoggedTypeDeclName(MediaFormatReader);
+
+class MediaFormatReader final
+ : public SupportsThreadSafeWeakPtr<MediaFormatReader>,
+ public DecoderDoctorLifeLogger<MediaFormatReader> {
+ static const bool IsExclusive = true;
+ typedef TrackInfo::TrackType TrackType;
+ typedef MozPromise<bool, MediaResult, IsExclusive> NotifyDataArrivedPromise;
+
+ public:
+ MOZ_DECLARE_REFCOUNTED_TYPENAME(MediaFormatReader)
+
+ using TrackSet = EnumSet<TrackInfo::TrackType>;
+ using MetadataPromise = MozPromise<MetadataHolder, MediaResult, IsExclusive>;
+
+ template <typename Type>
+ using DataPromise = MozPromise<RefPtr<Type>, MediaResult, IsExclusive>;
+ using AudioDataPromise = DataPromise<AudioData>;
+ using VideoDataPromise = DataPromise<VideoData>;
+
+ using SeekPromise = MozPromise<media::TimeUnit, SeekRejectValue, IsExclusive>;
+
+ // Note that, conceptually, WaitForData makes sense in a non-exclusive sense.
+ // But in the current architecture it's only ever used exclusively (by MDSM),
+ // so we mark it that way to verify our assumptions. If you have a use-case
+ // for multiple WaitForData consumers, feel free to flip the exclusivity here.
+ using WaitForDataPromise =
+ MozPromise<MediaData::Type, WaitForDataRejectValue, IsExclusive>;
+
+ MediaFormatReader(MediaFormatReaderInit& aInit, MediaDataDemuxer* aDemuxer);
+ virtual ~MediaFormatReader();
+
+ // Initializes the reader, returns NS_OK on success, or NS_ERROR_FAILURE
+ // on failure.
+ nsresult Init();
+
+ size_t SizeOfVideoQueueInFrames();
+ size_t SizeOfAudioQueueInFrames();
+
+ // Requests one video sample from the reader.
+ RefPtr<VideoDataPromise> RequestVideoData(
+ const media::TimeUnit& aTimeThreshold,
+ bool aRequestNextVideoKeyFrame = false);
+
+ // Requests one audio sample from the reader.
+ //
+ // The decode should be performed asynchronously, and the promise should
+ // be resolved when it is complete.
+ RefPtr<AudioDataPromise> RequestAudioData();
+
+ // The default implementation of AsyncReadMetadata is implemented in terms of
+ // synchronous ReadMetadata() calls. Implementations may also
+ // override AsyncReadMetadata to create a more proper async implementation.
+ RefPtr<MetadataPromise> AsyncReadMetadata();
+
+ // Fills aInfo with the latest cached data required to present the media,
+ // ReadUpdatedMetadata will always be called once ReadMetadata has succeeded.
+ void ReadUpdatedMetadata(MediaInfo* aInfo);
+
+ RefPtr<SeekPromise> Seek(const SeekTarget& aTarget);
+
+ // Called once new data has been cached by the MediaResource.
+ // mBuffered should be recalculated and updated accordingly.
+ void NotifyDataArrived();
+
+ // Update ID for the external playback engine. Currently it's only used on
+ // Windows when the media engine playback is enabled.
+ void UpdateMediaEngineId(uint64_t aMediaEngineId);
+
+ protected:
+ // Recomputes mBuffered.
+ void UpdateBuffered();
+
+ public:
+ // Called by MDSM in dormant state to release resources allocated by this
+ // reader. The reader can resume decoding by calling Seek() to a specific
+ // position.
+ void ReleaseResources();
+
+ bool OnTaskQueue() const { return OwnerThread()->IsCurrentThreadIn(); }
+
+ // Resets all state related to decoding, emptying all buffers etc.
+ // Cancels all pending Request*Data() request callbacks, rejects any
+ // outstanding seek promises, and flushes the decode pipeline. The
+ // decoder must not call any of the callbacks for outstanding
+ // Request*Data() calls after this is called. Calls to Request*Data()
+ // made after this should be processed as usual.
+ //
+ // Normally this call preceedes a Seek() call, or shutdown.
+ //
+ // aParam is a set of TrackInfo::TrackType enums specifying which
+ // queues need to be reset, defaulting to both audio and video tracks.
+ nsresult ResetDecode(TrackSet aTracks);
+
+ // Destroys the decoding state. The reader cannot be made usable again.
+ // This is different from ReleaseMediaResources() as it is irreversable,
+ // whereas ReleaseMediaResources() is. Must be called on the decode
+ // thread.
+ RefPtr<ShutdownPromise> Shutdown();
+
+ // Returns true if this decoder reader uses hardware accelerated video
+ // decoding.
+ bool VideoIsHardwareAccelerated() const;
+
+ // By default, the state machine polls the reader once per second when it's
+ // in buffering mode. Some readers support a promise-based mechanism by which
+ // they notify the state machine when the data arrives.
+ bool IsWaitForDataSupported() const { return true; }
+
+ RefPtr<WaitForDataPromise> WaitForData(MediaData::Type aType);
+
+ // The MediaDecoderStateMachine uses various heuristics that assume that
+ // raw media data is arriving sequentially from a network channel. This
+ // makes sense in the <video src="foo"> case, but not for more advanced use
+ // cases like MSE.
+ bool UseBufferingHeuristics() const { return mTrackDemuxersMayBlock; }
+
+ RefPtr<SetCDMPromise> SetCDMProxy(CDMProxy* aProxy);
+
+ // Requests that the MediaFormatReader populates aInfo with debug information.
+ // This may be done asynchronously, and aInfo should *not* be accessed by the
+ // caller until the returned promise is resolved or rejected.
+ RefPtr<GenericPromise> RequestDebugInfo(
+ dom::MediaFormatReaderDebugInfo& aInfo);
+
+ Maybe<nsCString> GetAudioProcessPerCodec();
+
+ // Switch the video decoder to NullDecoderModule. It might takes effective
+ // since a few samples later depends on how much demuxed samples are already
+ // queued in the original video decoder.
+ void SetVideoNullDecode(bool aIsNullDecode);
+
+ void UpdateCompositor(already_AddRefed<layers::KnowsCompositor>);
+
+ void UpdateDuration(const media::TimeUnit& aDuration) {
+ MOZ_ASSERT(OnTaskQueue());
+ UpdateBuffered();
+ }
+
+ AbstractCanonical<media::TimeIntervals>* CanonicalBuffered() {
+ return &mBuffered;
+ }
+
+ TaskQueue* OwnerThread() const { return mTaskQueue; }
+
+ TimedMetadataEventSource& TimedMetadataEvent() { return mTimedMetadataEvent; }
+
+ // Notified by the OggDemuxer during playback when chained ogg is detected.
+ MediaEventSource<void>& OnMediaNotSeekable() { return mOnMediaNotSeekable; }
+
+ TimedMetadataEventProducer& TimedMetadataProducer() {
+ return mTimedMetadataEvent;
+ }
+
+ MediaEventProducer<void>& MediaNotSeekableProducer() {
+ return mOnMediaNotSeekable;
+ }
+
+ // Notified if the reader can't decode a sample due to a missing decryption
+ // key.
+ MediaEventSource<TrackInfo::TrackType>& OnTrackWaitingForKey() {
+ return mOnTrackWaitingForKey;
+ }
+
+ MediaEventProducer<TrackInfo::TrackType>& OnTrackWaitingForKeyProducer() {
+ return mOnTrackWaitingForKey;
+ }
+
+ MediaEventSource<nsTArray<uint8_t>, nsString>& OnEncrypted() {
+ return mOnEncrypted;
+ }
+
+ MediaEventSource<void>& OnWaitingForKey() { return mOnWaitingForKey; }
+
+ MediaEventSource<MediaResult>& OnDecodeWarning() { return mOnDecodeWarning; }
+
+ MediaEventSource<VideoInfo>& OnStoreDecoderBenchmark() {
+ return mOnStoreDecoderBenchmark;
+ }
+
+ MediaEventProducer<VideoInfo, AudioInfo>& OnTrackInfoUpdatedEvent() {
+ return mTrackInfoUpdatedEvent;
+ }
+
+ private:
+ bool HasVideo() const { return mVideo.mTrackDemuxer; }
+ bool HasAudio() const { return mAudio.mTrackDemuxer; }
+
+ bool IsWaitingOnCDMResource();
+
+ bool InitDemuxer();
+ // Notify the track demuxers that new data has been received.
+ void NotifyTrackDemuxers();
+ void ReturnOutput(MediaData* aData, TrackType aTrack);
+
+ // Enqueues a task to call Update(aTrack) on the decoder task queue.
+ // Lock for corresponding track must be held.
+ void ScheduleUpdate(TrackType aTrack);
+ void Update(TrackType aTrack);
+ // Handle actions should more data be received.
+ // Returns true if no more action is required.
+ bool UpdateReceivedNewData(TrackType aTrack);
+ // Called when new samples need to be demuxed.
+ void RequestDemuxSamples(TrackType aTrack);
+ // Handle demuxed samples by the input behavior.
+ void HandleDemuxedSamples(TrackType aTrack,
+ FrameStatistics::AutoNotifyDecoded& aA);
+ // Decode any pending already demuxed samples.
+ void DecodeDemuxedSamples(TrackType aTrack, MediaRawData* aSample);
+
+ struct InternalSeekTarget {
+ InternalSeekTarget(const media::TimeInterval& aTime, bool aDropTarget)
+ : mTime(aTime),
+ mDropTarget(aDropTarget),
+ mWaiting(false),
+ mHasSeeked(false) {}
+
+ media::TimeUnit Time() const { return mTime.mStart; }
+ media::TimeUnit EndTime() const { return mTime.mEnd; }
+ bool Contains(const media::TimeUnit& aTime) const {
+ return mTime.Contains(aTime);
+ }
+
+ media::TimeInterval mTime;
+ bool mDropTarget;
+ bool mWaiting;
+ bool mHasSeeked;
+ };
+
+ // Perform an internal seek to aTime. If aDropTarget is true then
+ // the first sample past the target will be dropped.
+ void InternalSeek(TrackType aTrack, const InternalSeekTarget& aTarget);
+ // Return the end time of the internal seek target if it exists. Otherwise,
+ // return infinity.
+ media::TimeUnit GetInternalSeekTargetEndTime() const;
+
+ // Drain the current decoder.
+ void DrainDecoder(TrackType aTrack);
+ void NotifyNewOutput(TrackType aTrack,
+ MediaDataDecoder::DecodedData&& aResults);
+ void NotifyError(TrackType aTrack, const MediaResult& aError);
+ void NotifyWaitingForData(TrackType aTrack);
+ void NotifyWaitingForKey(TrackType aTrack);
+ void NotifyEndOfStream(TrackType aTrack);
+
+ void ExtractCryptoInitData(nsTArray<uint8_t>& aInitData);
+
+ // Initializes mLayersBackendType if possible.
+ void InitLayersBackendType();
+
+ void Reset(TrackType aTrack);
+ void DropDecodedSamples(TrackType aTrack);
+
+ // Return a target timeunit which the reader should skip to, this would be
+ // either the timethreshold we pass, or the time of the next keyframe. Return
+ // nothing if we don't need to skip.
+ // @param aTimeThreshold
+ // The time that we expect the time of next video frame should be or go beyond
+ // @param aRequestNextVideoKeyFrame
+ // If true and the next keyframe's time is larger than aTimeThreshold, skip to
+ // the next keyframe time instead of aTimeThreshold.
+ Maybe<media::TimeUnit> ShouldSkip(media::TimeUnit aTimeThreshold,
+ bool aRequestNextVideoKeyFrame);
+
+ void SetVideoDecodeThreshold();
+
+ size_t SizeOfQueue(TrackType aTrack);
+
+ // Fire a new OnStoreDecoderBenchmark event that will create new
+ // storage of the decoder benchmark.
+ // This is called only on TaskQueue.
+ void NotifyDecoderBenchmarkStore();
+
+ void NotifyTrackInfoUpdated();
+
+ enum class DrainState {
+ None,
+ DrainRequested,
+ Draining,
+ PartialDrainPending,
+ DrainCompleted,
+ DrainAborted,
+ };
+
+ class SharedShutdownPromiseHolder : public MozPromiseHolder<ShutdownPromise> {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SharedShutdownPromiseHolder)
+ private:
+ ~SharedShutdownPromiseHolder() = default;
+ };
+
+ struct DecoderData {
+ DecoderData(MediaFormatReader* aOwner, MediaData::Type aType,
+ uint32_t aNumOfMaxError)
+ : mOwner(aOwner),
+ mType(aType),
+ mMutex("DecoderData"),
+ mDescription("uninitialized"),
+ mProcessName(""),
+ mCodecName(""),
+ mUpdateScheduled(false),
+ mDemuxEOS(false),
+ mWaitingForData(false),
+ mWaitingForKey(false),
+ mReceivedNewData(false),
+ mFlushing(false),
+ mFlushed(true),
+ mDrainState(DrainState::None),
+ mNumOfConsecutiveDecodingError(0),
+ mMaxConsecutiveDecodingError(aNumOfMaxError),
+ mNumOfConsecutiveRDDOrGPUCrashes(0),
+ mMaxConsecutiveRDDOrGPUCrashes(
+ StaticPrefs::media_rdd_process_max_crashes()),
+ mNumOfConsecutiveUtilityCrashes(0),
+ mMaxConsecutiveUtilityCrashes(
+ StaticPrefs::media_utility_process_max_crashes()),
+ mFirstFrameTime(Some(media::TimeUnit::Zero())),
+ mNumSamplesInput(0),
+ mNumSamplesOutput(0),
+ mNumSamplesOutputTotal(0),
+ mNumSamplesSkippedTotal(0),
+ mSizeOfQueue(0),
+ mIsHardwareAccelerated(false),
+ mLastStreamSourceID(UINT32_MAX),
+ mIsNullDecode(false),
+ mHardwareDecodingDisabled(false) {
+ DecoderDoctorLogger::LogConstruction("MediaFormatReader::DecoderData",
+ this);
+ }
+
+ ~DecoderData() {
+ DecoderDoctorLogger::LogDestruction("MediaFormatReader::DecoderData",
+ this);
+ }
+
+ MediaFormatReader* mOwner;
+ // Disambiguate Audio vs Video.
+ MediaData::Type mType;
+ RefPtr<MediaTrackDemuxer> mTrackDemuxer;
+ // TaskQueue on which decoder can choose to decode.
+ // Only non-null up until the decoder is created.
+ RefPtr<TaskQueue> mTaskQueue;
+
+ // Mutex protecting mDescription, mDecoder, mTrackDemuxer, mWorkingInfo,
+ // mProcessName and mCodecName as those can be read outside the TaskQueue.
+ // They are only written on the TaskQueue however, as such mMutex doesn't
+ // need to be held when those members are read on the TaskQueue.
+ Mutex mMutex MOZ_UNANNOTATED;
+ // The platform decoder.
+ RefPtr<MediaDataDecoder> mDecoder;
+ nsCString mDescription;
+ nsCString mProcessName;
+ nsCString mCodecName;
+ void ShutdownDecoder();
+
+ // Only accessed from reader's task queue.
+ bool mUpdateScheduled;
+ bool mDemuxEOS;
+ bool mWaitingForData;
+ bool mWaitingForKey;
+ bool mReceivedNewData;
+
+ // Pending seek.
+ MozPromiseRequestHolder<MediaTrackDemuxer::SeekPromise> mSeekRequest;
+
+ // Queued demux samples waiting to be decoded.
+ nsTArray<RefPtr<MediaRawData>> mQueuedSamples;
+ MozPromiseRequestHolder<MediaTrackDemuxer::SamplesPromise> mDemuxRequest;
+ // A WaitingPromise is pending if the demuxer is waiting for data or
+ // if the decoder is waiting for a key.
+ MozPromiseHolder<WaitForDataPromise> mWaitingPromise;
+ bool HasWaitingPromise() const {
+ MOZ_ASSERT(mOwner->OnTaskQueue());
+ return !mWaitingPromise.IsEmpty();
+ }
+
+ bool IsWaitingForData() const {
+ MOZ_ASSERT(mOwner->OnTaskQueue());
+ return mWaitingForData;
+ }
+
+ bool IsWaitingForKey() const {
+ MOZ_ASSERT(mOwner->OnTaskQueue());
+ return mWaitingForKey && mDecodeRequest.Exists();
+ }
+
+ // MediaDataDecoder handler's variables.
+ MozPromiseRequestHolder<MediaDataDecoder::DecodePromise> mDecodeRequest;
+ bool mFlushing; // True if flush is in action.
+ // Set to true if the last operation run on the decoder was a flush.
+ bool mFlushed;
+ RefPtr<SharedShutdownPromiseHolder> mShutdownPromise;
+
+ MozPromiseRequestHolder<MediaDataDecoder::DecodePromise> mDrainRequest;
+ DrainState mDrainState;
+ bool HasPendingDrain() const { return mDrainState != DrainState::None; }
+ bool HasCompletedDrain() const {
+ return mDrainState == DrainState::DrainCompleted ||
+ mDrainState == DrainState::DrainAborted;
+ }
+ void RequestDrain() {
+ MOZ_RELEASE_ASSERT(mDrainState == DrainState::None);
+ mDrainState = DrainState::DrainRequested;
+ }
+
+ // Track decoding error and fail when we hit the limit.
+ uint32_t mNumOfConsecutiveDecodingError;
+ uint32_t mMaxConsecutiveDecodingError;
+
+ // Track RDD or GPU process crashes and fail when we hit the limit.
+ uint32_t mNumOfConsecutiveRDDOrGPUCrashes;
+ uint32_t mMaxConsecutiveRDDOrGPUCrashes;
+
+ // Track Utility process crashes and fail when we hit the limit.
+ uint32_t mNumOfConsecutiveUtilityCrashes;
+ uint32_t mMaxConsecutiveUtilityCrashes;
+
+ // Set when we haven't yet decoded the first frame.
+ // Cleared once the first frame has been decoded.
+ // This is used to determine, upon error, if we should try again to decode
+ // the frame, or skip to the next keyframe.
+ Maybe<media::TimeUnit> mFirstFrameTime;
+
+ Maybe<MediaResult> mError;
+ bool HasFatalError() const {
+ if (!mError.isSome()) {
+ return false;
+ }
+ if (mError.ref() == NS_ERROR_DOM_MEDIA_DECODE_ERR) {
+ // Allow decode errors to be non-fatal, but give up
+ // if we have too many, or if warnings should be treated as errors.
+ return mNumOfConsecutiveDecodingError > mMaxConsecutiveDecodingError ||
+ StaticPrefs::media_playback_warnings_as_errors();
+ } else if (mError.ref() == NS_ERROR_DOM_MEDIA_NEED_NEW_DECODER) {
+ // If the caller asked for a new decoder we shouldn't treat
+ // it as fatal.
+ return false;
+ } else if (mError.ref() ==
+ NS_ERROR_DOM_MEDIA_REMOTE_DECODER_CRASHED_RDD_OR_GPU_ERR) {
+ // Allow RDD crashes to be non-fatal, but give up
+ // if we have too many, or if warnings should be treated as errors.
+ return mNumOfConsecutiveRDDOrGPUCrashes >
+ mMaxConsecutiveRDDOrGPUCrashes ||
+ StaticPrefs::media_playback_warnings_as_errors();
+ } else if (mError.ref() ==
+ NS_ERROR_DOM_MEDIA_REMOTE_DECODER_CRASHED_UTILITY_ERR) {
+ bool tooManyConsecutiveCrashes =
+ mNumOfConsecutiveUtilityCrashes > mMaxConsecutiveUtilityCrashes;
+ // TODO: Telemetry?
+ return tooManyConsecutiveCrashes ||
+ StaticPrefs::media_playback_warnings_as_errors();
+ } else if (mError.ref() ==
+ NS_ERROR_DOM_MEDIA_REMOTE_DECODER_CRASHED_MF_CDM_ERR) {
+ return false;
+ } else {
+ // All other error types are fatal
+ return true;
+ }
+ }
+
+ // If set, all decoded samples prior mTimeThreshold will be dropped.
+ // Used for internal seeking when a change of stream is detected or when
+ // encountering data discontinuity.
+ Maybe<InternalSeekTarget> mTimeThreshold;
+ // Time of last decoded sample returned.
+ Maybe<media::TimeInterval> mLastDecodedSampleTime;
+
+ // Decoded samples returned my mDecoder awaiting being returned to
+ // state machine upon request.
+ nsTArray<RefPtr<MediaData>> mOutput;
+ uint64_t mNumSamplesInput;
+ uint64_t mNumSamplesOutput;
+ uint64_t mNumSamplesOutputTotal;
+ uint64_t mNumSamplesSkippedTotal;
+
+ // These get overridden in the templated concrete class.
+ // Indicate if we have a pending promise for decoded frame.
+ // Rejecting the promise will stop the reader from decoding ahead.
+ virtual bool HasPromise() const = 0;
+ virtual void RejectPromise(const MediaResult& aError,
+ const char* aMethodName) = 0;
+
+ // Clear track demuxer related data.
+ void ResetDemuxer() {
+ mDemuxRequest.DisconnectIfExists();
+ mSeekRequest.DisconnectIfExists();
+ mTrackDemuxer->Reset();
+ mQueuedSamples.Clear();
+ }
+
+ // Flush the decoder if present and reset decoding related data.
+ // Following a flush, the decoder is ready to accept any new data.
+ void Flush();
+
+ bool CancelWaitingForKey() {
+ if (!mWaitingForKey) {
+ return false;
+ }
+ mWaitingForKey = false;
+ if (IsWaitingForData() || !HasWaitingPromise()) {
+ return false;
+ }
+ mWaitingPromise.Resolve(mType, __func__);
+ return true;
+ }
+
+ // Reset the state of the DecoderData, clearing all queued frames
+ // (pending demuxed and decoded).
+ // The track demuxer is *not* reset.
+ void ResetState() {
+ MOZ_ASSERT(mOwner->OnTaskQueue());
+ mDemuxEOS = false;
+ mWaitingForData = false;
+ mQueuedSamples.Clear();
+ mDecodeRequest.DisconnectIfExists();
+ mDrainRequest.DisconnectIfExists();
+ mDrainState = DrainState::None;
+ CancelWaitingForKey();
+ mTimeThreshold.reset();
+ mLastDecodedSampleTime.reset();
+ mOutput.Clear();
+ mNumSamplesInput = 0;
+ mNumSamplesOutput = 0;
+ mSizeOfQueue = 0;
+ mNextStreamSourceID.reset();
+ if (!HasFatalError()) {
+ mError.reset();
+ }
+ }
+
+ bool HasInternalSeekPending() const {
+ return mTimeThreshold && !mTimeThreshold.ref().mHasSeeked;
+ }
+
+ // Return the current TrackInfo in the stream. If the stream content never
+ // changed since AsyncReadMetadata was called then the TrackInfo used is
+ // mOriginalInfo, other it will be mInfo. The later case is only ever true
+ // with MSE or the WebMDemuxer.
+ const TrackInfo* GetCurrentInfo() const {
+ if (mInfo) {
+ return *mInfo;
+ }
+ return mOriginalInfo.get();
+ }
+ // Return the current TrackInfo updated as per the decoder output.
+ // Typically for audio, the number of channels and/or sampling rate can vary
+ // between what was found in the metadata and what the decoder returned.
+ const TrackInfo* GetWorkingInfo() const { return mWorkingInfo.get(); }
+ bool IsEncrypted() const { return GetCurrentInfo()->mCrypto.IsEncrypted(); }
+
+ // Used by the MDSM for logging purposes.
+ Atomic<size_t> mSizeOfQueue;
+ // Used by the MDSM to determine if video decoding is hardware accelerated.
+ // This value is updated after a frame is successfully decoded.
+ Atomic<bool> mIsHardwareAccelerated;
+ // Sample format monitoring.
+ uint32_t mLastStreamSourceID;
+ Maybe<uint32_t> mNextStreamSourceID;
+ media::TimeIntervals mTimeRanges;
+ Maybe<media::TimeUnit> mLastTimeRangesEnd;
+ // TrackInfo as first discovered during ReadMetadata.
+ UniquePtr<TrackInfo> mOriginalInfo;
+ // Written exclusively on the TaskQueue, can be read on MDSM's TaskQueue.
+ // Must be read with parent's mutex held.
+ UniquePtr<TrackInfo> mWorkingInfo;
+ RefPtr<TrackInfoSharedPtr> mInfo;
+ Maybe<media::TimeUnit> mFirstDemuxedSampleTime;
+ // Use NullDecoderModule or not.
+ bool mIsNullDecode;
+ bool mHardwareDecodingDisabled;
+ // Whether we have reported hardware decoding support for video. Used only
+ // on reader's task queue,
+ bool mHasReportedVideoHardwareSupportTelemtry = false;
+
+ class {
+ public:
+ float Mean() const { return mMean; }
+
+ void Update(const media::TimeUnit& aValue) {
+ if (aValue == media::TimeUnit::Zero()) {
+ return;
+ }
+ mMean += (1.0f / aValue.ToSeconds() - mMean) / ++mCount;
+ }
+
+ void Reset() {
+ mMean = 0;
+ mCount = 0;
+ }
+
+ private:
+ float mMean = 0;
+ uint64_t mCount = 0;
+ } mMeanRate;
+ };
+
+ template <typename Type>
+ class DecoderDataWithPromise : public DecoderData {
+ public:
+ DecoderDataWithPromise(MediaFormatReader* aOwner, MediaData::Type aType,
+ uint32_t aNumOfMaxError)
+ : DecoderData(aOwner, aType, aNumOfMaxError), mHasPromise(false) {
+ DecoderDoctorLogger::LogConstructionAndBase(
+ "MediaFormatReader::DecoderDataWithPromise", this,
+ "MediaFormatReader::DecoderData",
+ static_cast<const MediaFormatReader::DecoderData*>(this));
+ }
+
+ ~DecoderDataWithPromise() {
+ DecoderDoctorLogger::LogDestruction(
+ "MediaFormatReader::DecoderDataWithPromise", this);
+ }
+
+ bool HasPromise() const override { return mHasPromise; }
+
+ RefPtr<DataPromise<Type>> EnsurePromise(const char* aMethodName) {
+ MOZ_ASSERT(mOwner->OnTaskQueue());
+ mHasPromise = true;
+ return mPromise.Ensure(aMethodName);
+ }
+
+ void ResolvePromise(Type* aData, const char* aMethodName) {
+ MOZ_ASSERT(mOwner->OnTaskQueue());
+ mPromise.Resolve(aData, aMethodName);
+ mHasPromise = false;
+ }
+
+ void RejectPromise(const MediaResult& aError,
+ const char* aMethodName) override {
+ MOZ_ASSERT(mOwner->OnTaskQueue());
+ mPromise.Reject(aError, aMethodName);
+ mHasPromise = false;
+ }
+
+ private:
+ MozPromiseHolder<DataPromise<Type>> mPromise;
+ Atomic<bool> mHasPromise;
+ };
+
+ // Decode task queue.
+ RefPtr<TaskQueue> mTaskQueue;
+
+ DecoderDataWithPromise<AudioData> mAudio;
+ DecoderDataWithPromise<VideoData> mVideo;
+
+ Watchable<bool> mWorkingInfoChanged;
+ WatchManager<MediaFormatReader> mWatchManager;
+ bool mIsWatchingWorkingInfo;
+
+ // Returns true when the decoder for this track needs input.
+ bool NeedInput(DecoderData& aDecoder);
+
+ DecoderData& GetDecoderData(TrackType aTrack);
+
+ // Demuxer objects.
+ class DemuxerProxy;
+ UniquePtr<DemuxerProxy> mDemuxer;
+ bool mDemuxerInitDone;
+ void OnDemuxerInitDone(const MediaResult& aResult);
+ void OnDemuxerInitFailed(const MediaResult& aError);
+ MozPromiseRequestHolder<MediaDataDemuxer::InitPromise> mDemuxerInitRequest;
+ MozPromiseRequestHolder<NotifyDataArrivedPromise> mNotifyDataArrivedPromise;
+ bool mPendingNotifyDataArrived;
+ void OnDemuxFailed(TrackType aTrack, const MediaResult& aError);
+
+ void DoDemuxVideo();
+ void OnVideoDemuxCompleted(RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples);
+ void OnVideoDemuxFailed(const MediaResult& aError) {
+ OnDemuxFailed(TrackType::kVideoTrack, aError);
+ }
+
+ void DoDemuxAudio();
+ void OnAudioDemuxCompleted(RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples);
+ void OnAudioDemuxFailed(const MediaResult& aError) {
+ OnDemuxFailed(TrackType::kAudioTrack, aError);
+ }
+
+ void SkipVideoDemuxToNextKeyFrame(media::TimeUnit aTimeThreshold);
+ MozPromiseRequestHolder<MediaTrackDemuxer::SkipAccessPointPromise>
+ mSkipRequest;
+ void VideoSkipReset(uint32_t aSkipped);
+ void OnVideoSkipCompleted(uint32_t aSkipped);
+ void OnVideoSkipFailed(MediaTrackDemuxer::SkipFailureHolder aFailure);
+
+ // The last number of decoded output frames that we've reported to
+ // MediaDecoder::NotifyDecoded(). We diff the number of output video
+ // frames every time that DecodeVideoData() is called, and report the
+ // delta there.
+ uint64_t mLastReportedNumDecodedFrames;
+
+ // Timestamp of the previous decoded keyframe, in microseconds.
+ int64_t mPreviousDecodedKeyframeTime_us;
+ // Default mLastDecodedKeyframeTime_us value, must be bigger than anything.
+ static const int64_t sNoPreviousDecodedKeyframe = INT64_MAX;
+
+ RefPtr<layers::KnowsCompositor> mKnowsCompositor;
+
+ // Metadata objects
+ // True if we've read the streams' metadata.
+ bool mInitDone;
+ MozPromiseHolder<MetadataPromise> mMetadataPromise;
+ bool IsEncrypted() const;
+
+ // Set to true if any of our track buffers may be blocking.
+ bool mTrackDemuxersMayBlock;
+
+ // Seeking objects.
+ void SetSeekTarget(const SeekTarget& aTarget);
+ bool IsSeeking() const { return mPendingSeekTime.isSome(); }
+ bool IsVideoOnlySeeking() const {
+ return IsSeeking() && mOriginalSeekTarget.IsVideoOnly();
+ }
+ bool IsAudioOnlySeeking() const {
+ return IsSeeking() && mOriginalSeekTarget.IsAudioOnly();
+ }
+ void ScheduleSeek();
+ void AttemptSeek();
+ void OnSeekFailed(TrackType aTrack, const MediaResult& aError);
+ void DoVideoSeek();
+ void OnVideoSeekCompleted(media::TimeUnit aTime);
+ void OnVideoSeekFailed(const MediaResult& aError);
+ bool mSeekScheduled;
+
+ void DoAudioSeek();
+ void OnAudioSeekCompleted(media::TimeUnit aTime);
+ void OnAudioSeekFailed(const MediaResult& aError);
+ // The SeekTarget that was last given to Seek()
+ SeekTarget mOriginalSeekTarget;
+ // Temporary seek information while we wait for the data
+ Maybe<media::TimeUnit> mFallbackSeekTime;
+ Maybe<media::TimeUnit> mPendingSeekTime;
+ MozPromiseHolder<SeekPromise> mSeekPromise;
+
+ RefPtr<VideoFrameContainer> mVideoFrameContainer;
+ layers::ImageContainer* GetImageContainer();
+
+ RefPtr<CDMProxy> mCDMProxy;
+
+ RefPtr<GMPCrashHelper> mCrashHelper;
+
+ void SetNullDecode(TrackType aTrack, bool aIsNullDecode);
+
+ class DecoderFactory;
+ UniquePtr<DecoderFactory> mDecoderFactory;
+
+ class ShutdownPromisePool;
+ UniquePtr<ShutdownPromisePool> mShutdownPromisePool;
+
+ MediaEventListener mOnTrackWaitingForKeyListener;
+
+ void OnFirstDemuxCompleted(TrackInfo::TrackType aType,
+ RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples);
+
+ void OnFirstDemuxFailed(TrackInfo::TrackType aType,
+ const MediaResult& aError);
+
+ void MaybeResolveMetadataPromise();
+
+ // Stores presentation info required for playback.
+ MediaInfo mInfo;
+
+ UniquePtr<MetadataTags> mTags;
+
+ // A flag indicating if the start time is known or not.
+ bool mHasStartTime = false;
+
+ void ShutdownDecoder(TrackType aTrack);
+ RefPtr<ShutdownPromise> TearDownDecoders();
+
+ bool mShutdown = false;
+
+ // Buffered range.
+ Canonical<media::TimeIntervals> mBuffered;
+
+ // Used to send TimedMetadata to the listener.
+ TimedMetadataEventProducer mTimedMetadataEvent;
+
+ // Notify if this media is not seekable.
+ MediaEventProducer<void> mOnMediaNotSeekable;
+
+ // Notify if we are waiting for a decryption key.
+ MediaEventProducer<TrackInfo::TrackType> mOnTrackWaitingForKey;
+
+ MediaEventProducer<nsTArray<uint8_t>, nsString> mOnEncrypted;
+
+ MediaEventProducer<void> mOnWaitingForKey;
+
+ MediaEventProducer<MediaResult> mOnDecodeWarning;
+
+ MediaEventProducer<VideoInfo> mOnStoreDecoderBenchmark;
+
+ MediaEventProducer<VideoInfo, AudioInfo> mTrackInfoUpdatedEvent;
+
+ RefPtr<FrameStatistics> mFrameStats;
+
+ // Used in bug 1393399 for telemetry.
+ const MediaDecoderOwnerID mMediaDecoderOwnerID;
+
+ bool ResolveSetCDMPromiseIfDone(TrackType aTrack);
+ void PrepareToSetCDMForTrack(TrackType aTrack);
+ MozPromiseHolder<SetCDMPromise> mSetCDMPromise;
+ TrackSet mSetCDMForTracks{};
+ bool IsDecoderWaitingForCDM(TrackType aTrack);
+
+ void GetDebugInfo(dom::MediaFormatReaderDebugInfo& aInfo);
+
+ // Only be used on Windows when the media engine playback is enabled.
+ Maybe<uint64_t> mMediaEngineId;
+
+ const Maybe<TrackingId> mTrackingId;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/MediaInfo.cpp b/dom/media/MediaInfo.cpp
new file mode 100644
index 0000000000..04599819f6
--- /dev/null
+++ b/dom/media/MediaInfo.cpp
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaInfo.h"
+
+namespace mozilla {
+
+const char* TrackTypeToStr(TrackInfo::TrackType aTrack) {
+ switch (aTrack) {
+ case TrackInfo::kUndefinedTrack:
+ return "Undefined";
+ case TrackInfo::kAudioTrack:
+ return "Audio";
+ case TrackInfo::kVideoTrack:
+ return "Video";
+ case TrackInfo::kTextTrack:
+ return "Text";
+ default:
+ return "Unknown";
+ }
+}
+
+bool TrackInfo::IsEqualTo(const TrackInfo& rhs) const {
+ return (
+ mId == rhs.mId && mKind == rhs.mKind && mLabel == rhs.mLabel &&
+ mLanguage == rhs.mLanguage && mEnabled == rhs.mEnabled &&
+ mTrackId == rhs.mTrackId && mMimeType == rhs.mMimeType &&
+ mDuration == rhs.mDuration && mMediaTime == rhs.mMediaTime &&
+ mCrypto.mCryptoScheme == rhs.mCrypto.mCryptoScheme &&
+ mCrypto.mIVSize == rhs.mCrypto.mIVSize &&
+ mCrypto.mKeyId == rhs.mCrypto.mKeyId &&
+ mCrypto.mCryptByteBlock == rhs.mCrypto.mCryptByteBlock &&
+ mCrypto.mSkipByteBlock == rhs.mCrypto.mSkipByteBlock &&
+ mCrypto.mConstantIV == rhs.mCrypto.mConstantIV && mTags == rhs.mTags &&
+ mIsRenderedExternally == rhs.mIsRenderedExternally && mType == rhs.mType);
+}
+
+bool VideoInfo::operator==(const VideoInfo& rhs) const {
+ return (TrackInfo::IsEqualTo(rhs) && mDisplay == rhs.mDisplay &&
+ mStereoMode == rhs.mStereoMode && mImage == rhs.mImage &&
+ *mCodecSpecificConfig == *rhs.mCodecSpecificConfig &&
+ *mExtraData == *rhs.mExtraData && mRotation == rhs.mRotation &&
+ mColorDepth == rhs.mColorDepth && mImageRect == rhs.mImageRect &&
+ mAlphaPresent == rhs.mAlphaPresent);
+}
+
+bool AudioInfo::operator==(const AudioInfo& rhs) const {
+ return (TrackInfo::IsEqualTo(rhs) && mRate == rhs.mRate &&
+ mChannels == rhs.mChannels && mChannelMap == rhs.mChannelMap &&
+ mBitDepth == rhs.mBitDepth && mProfile == rhs.mProfile &&
+ mExtendedProfile == rhs.mExtendedProfile &&
+ mCodecSpecificConfig == rhs.mCodecSpecificConfig);
+}
+
+} // namespace mozilla
diff --git a/dom/media/MediaInfo.h b/dom/media/MediaInfo.h
new file mode 100644
index 0000000000..c692b94d64
--- /dev/null
+++ b/dom/media/MediaInfo.h
@@ -0,0 +1,673 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#if !defined(MediaInfo_h)
+# define MediaInfo_h
+
+# include "mozilla/UniquePtr.h"
+# include "mozilla/RefPtr.h"
+# include "mozilla/Variant.h"
+# include "nsTHashMap.h"
+# include "nsString.h"
+# include "nsTArray.h"
+# include "AudioConfig.h"
+# include "ImageTypes.h"
+# include "MediaData.h"
+# include "TimeUnits.h"
+# include "mozilla/gfx/Point.h" // for gfx::IntSize
+# include "mozilla/gfx/Rect.h" // for gfx::IntRect
+# include "mozilla/gfx/Types.h" // for gfx::ColorDepth
+
+namespace mozilla {
+
+class AudioInfo;
+class VideoInfo;
+class TextInfo;
+
+class MetadataTag {
+ public:
+ MetadataTag(const nsACString& aKey, const nsACString& aValue)
+ : mKey(aKey), mValue(aValue) {}
+ nsCString mKey;
+ nsCString mValue;
+ bool operator==(const MetadataTag& rhs) const {
+ return mKey == rhs.mKey && mValue == rhs.mValue;
+ }
+};
+
+typedef nsTHashMap<nsCStringHashKey, nsCString> MetadataTags;
+
+// Start codec specific data structs. If modifying these remember to also
+// modify the MediaIPCUtils so that any new members are sent across IPC.
+
+// Generic types, we should prefer a specific type when we can.
+
+// Generic empty type. Prefer to use a specific type but not populate members
+// if possible, as that helps with type checking.
+struct NoCodecSpecificData {
+ bool operator==(const NoCodecSpecificData& rhs) const { return true; }
+};
+
+// Generic binary blob type. Prefer not to use this structure. It's here to ease
+// the transition to codec specific structures in the code.
+struct AudioCodecSpecificBinaryBlob {
+ bool operator==(const AudioCodecSpecificBinaryBlob& rhs) const {
+ return *mBinaryBlob == *rhs.mBinaryBlob;
+ }
+
+ RefPtr<MediaByteBuffer> mBinaryBlob{new MediaByteBuffer};
+};
+
+// End generic types.
+
+// Audio codec specific data types.
+
+struct AacCodecSpecificData {
+ bool operator==(const AacCodecSpecificData& rhs) const {
+ return *mEsDescriptorBinaryBlob == *rhs.mEsDescriptorBinaryBlob &&
+ *mDecoderConfigDescriptorBinaryBlob ==
+ *rhs.mDecoderConfigDescriptorBinaryBlob;
+ }
+ // An explanation for the necessity of handling the encoder delay and the
+ // padding is available here:
+ // https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFAppenG/QTFFAppenG.html
+
+ // The number of frames that should be skipped from the beginning of the
+ // decoded stream.
+ uint32_t mEncoderDelayFrames{0};
+
+ // The total number of frames of the media, that is, excluding the encoder
+ // delay and the padding of the last packet, that must be discarded.
+ uint64_t mMediaFrameCount{0};
+
+ // The bytes of the ES_Descriptor field parsed out of esds box. We store
+ // this as a blob as some decoders want this.
+ RefPtr<MediaByteBuffer> mEsDescriptorBinaryBlob{new MediaByteBuffer};
+
+ // The bytes of the DecoderConfigDescriptor field within the parsed
+ // ES_Descriptor. This is a subset of the ES_Descriptor, so it is technically
+ // redundant to store both. However, some decoders expect this binary blob
+ // instead of the whole ES_Descriptor, so both are stored for convenience
+ // and clarity (rather than reparsing the ES_Descriptor).
+ // TODO(bug 1768562): use a Span to track this rather than duplicating data.
+ RefPtr<MediaByteBuffer> mDecoderConfigDescriptorBinaryBlob{
+ new MediaByteBuffer};
+};
+
+struct FlacCodecSpecificData {
+ bool operator==(const FlacCodecSpecificData& rhs) const {
+ return *mStreamInfoBinaryBlob == *rhs.mStreamInfoBinaryBlob;
+ }
+
+ // A binary blob of the data from the METADATA_BLOCK_STREAMINFO block
+ // in the flac header.
+ // See https://xiph.org/flac/format.html#metadata_block_streaminfo
+ // Consumers of this data (ffmpeg) take a blob, so we don't parse the data,
+ // just store the blob. For headerless flac files this will be left empty.
+ RefPtr<MediaByteBuffer> mStreamInfoBinaryBlob{new MediaByteBuffer};
+};
+
+struct Mp3CodecSpecificData {
+ bool operator==(const Mp3CodecSpecificData& rhs) const {
+ return mEncoderDelayFrames == rhs.mEncoderDelayFrames &&
+ mEncoderPaddingFrames == rhs.mEncoderPaddingFrames;
+ }
+
+ // The number of frames that should be skipped from the beginning of the
+ // decoded stream.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1566389 for more info.
+ uint32_t mEncoderDelayFrames{0};
+
+ // The number of frames that should be skipped from the end of the decoded
+ // stream.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1566389 for more info.
+ uint32_t mEncoderPaddingFrames{0};
+};
+
+struct OpusCodecSpecificData {
+ bool operator==(const OpusCodecSpecificData& rhs) const {
+ return mContainerCodecDelayMicroSeconds ==
+ rhs.mContainerCodecDelayMicroSeconds &&
+ *mHeadersBinaryBlob == *rhs.mHeadersBinaryBlob;
+ }
+ // The codec delay (aka pre-skip) in microseconds.
+ // See https://tools.ietf.org/html/rfc7845#section-4.2 for more info.
+ // This member should store the codec delay parsed from the container file.
+ // In some cases (such as the ogg container), this information is derived
+ // from the same headers stored in the header blob, making storing this
+ // separately redundant. However, other containers store the delay in
+ // addition to the header blob, in which case we can check this container
+ // delay against the header delay to ensure they're consistent.
+ int64_t mContainerCodecDelayMicroSeconds{-1};
+
+ // A binary blob of opus header data, specifically the Identification Header.
+ // See https://datatracker.ietf.org/doc/html/rfc7845.html#section-5.1
+ RefPtr<MediaByteBuffer> mHeadersBinaryBlob{new MediaByteBuffer};
+};
+
+struct VorbisCodecSpecificData {
+ bool operator==(const VorbisCodecSpecificData& rhs) const {
+ return *mHeadersBinaryBlob == *rhs.mHeadersBinaryBlob;
+ }
+
+ // A binary blob of headers in the 'extradata' format (the format ffmpeg
+ // expects for packing the extradata field). This is also the format some
+ // containers use for storing the data. Specifically, this format consists of
+ // the page_segments field, followed by the segment_table field, followed by
+ // the three Vorbis header packets, respectively the identification header,
+ // the comments header, and the setup header, in that order.
+ // See also https://xiph.org/vorbis/doc/framing.html and
+ // https://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-610004.2
+ RefPtr<MediaByteBuffer> mHeadersBinaryBlob{new MediaByteBuffer};
+};
+
+struct WaveCodecSpecificData {
+ bool operator==(const WaveCodecSpecificData& rhs) const { return true; }
+ // Intentionally empty. We don't store any wave specific data, but this
+ // variant is useful for type checking.
+};
+
+using AudioCodecSpecificVariant =
+ mozilla::Variant<NoCodecSpecificData, AudioCodecSpecificBinaryBlob,
+ AacCodecSpecificData, FlacCodecSpecificData,
+ Mp3CodecSpecificData, OpusCodecSpecificData,
+ VorbisCodecSpecificData, WaveCodecSpecificData>;
+
+// Returns a binary blob representation of the AudioCodecSpecificVariant. This
+// does not guarantee that a binary representation exists. Will return an empty
+// buffer if no representation exists. Prefer `GetAudioCodecSpecificBlob` which
+// asserts if getting a blob is unexpected for a given codec config.
+inline already_AddRefed<MediaByteBuffer> ForceGetAudioCodecSpecificBlob(
+ const AudioCodecSpecificVariant& v) {
+ return v.match(
+ [](const NoCodecSpecificData&) {
+ return RefPtr<MediaByteBuffer>(new MediaByteBuffer).forget();
+ },
+ [](const AudioCodecSpecificBinaryBlob& binaryBlob) {
+ return RefPtr<MediaByteBuffer>(binaryBlob.mBinaryBlob).forget();
+ },
+ [](const AacCodecSpecificData& aacData) {
+ // We return the mDecoderConfigDescriptor blob here, as it is more
+ // commonly used by decoders at time of writing than the
+ // ES_Descriptor data. However, consumers of this data should
+ // prefer getting one or the other specifically, rather than
+ // calling this.
+ return RefPtr<MediaByteBuffer>(
+ aacData.mDecoderConfigDescriptorBinaryBlob)
+ .forget();
+ },
+ [](const FlacCodecSpecificData& flacData) {
+ return RefPtr<MediaByteBuffer>(flacData.mStreamInfoBinaryBlob).forget();
+ },
+ [](const Mp3CodecSpecificData&) {
+ return RefPtr<MediaByteBuffer>(new MediaByteBuffer).forget();
+ },
+ [](const OpusCodecSpecificData& opusData) {
+ return RefPtr<MediaByteBuffer>(opusData.mHeadersBinaryBlob).forget();
+ },
+ [](const VorbisCodecSpecificData& vorbisData) {
+ return RefPtr<MediaByteBuffer>(vorbisData.mHeadersBinaryBlob).forget();
+ },
+ [](const WaveCodecSpecificData&) {
+ return RefPtr<MediaByteBuffer>(new MediaByteBuffer).forget();
+ });
+}
+
+// Same as `ForceGetAudioCodecSpecificBlob` but with extra asserts to ensure
+// we're not trying to get a binary blob from codecs where we don't store the
+// information as a blob or where a blob is ambiguous.
+inline already_AddRefed<MediaByteBuffer> GetAudioCodecSpecificBlob(
+ const AudioCodecSpecificVariant& v) {
+ MOZ_ASSERT(!v.is<NoCodecSpecificData>(),
+ "NoCodecSpecificData shouldn't be used as a blob");
+ MOZ_ASSERT(!v.is<AacCodecSpecificData>(),
+ "AacCodecSpecificData has 2 blobs internally, one should "
+ "explicitly be selected");
+ MOZ_ASSERT(!v.is<Mp3CodecSpecificData>(),
+ "Mp3CodecSpecificData shouldn't be used as a blob");
+
+ return ForceGetAudioCodecSpecificBlob(v);
+}
+
+// End audio codec specific data types.
+
+// End codec specific data structs.
+
+class TrackInfo {
+ public:
+ enum TrackType { kUndefinedTrack, kAudioTrack, kVideoTrack, kTextTrack };
+ TrackInfo(TrackType aType, const nsAString& aId, const nsAString& aKind,
+ const nsAString& aLabel, const nsAString& aLanguage, bool aEnabled,
+ uint32_t aTrackId)
+ : mId(aId),
+ mKind(aKind),
+ mLabel(aLabel),
+ mLanguage(aLanguage),
+ mEnabled(aEnabled),
+ mTrackId(aTrackId),
+ mIsRenderedExternally(false),
+ mType(aType) {
+ MOZ_COUNT_CTOR(TrackInfo);
+ }
+
+ // Only used for backward compatibility. Do not use in new code.
+ void Init(const nsAString& aId, const nsAString& aKind,
+ const nsAString& aLabel, const nsAString& aLanguage,
+ bool aEnabled) {
+ mId = aId;
+ mKind = aKind;
+ mLabel = aLabel;
+ mLanguage = aLanguage;
+ mEnabled = aEnabled;
+ }
+
+ // Fields common with MediaTrack object.
+ nsString mId;
+ nsString mKind;
+ nsString mLabel;
+ nsString mLanguage;
+ bool mEnabled;
+
+ uint32_t mTrackId;
+
+ nsCString mMimeType;
+ media::TimeUnit mDuration;
+ media::TimeUnit mMediaTime;
+ uint32_t mTimeScale = 0;
+ CryptoTrack mCrypto;
+
+ CopyableTArray<MetadataTag> mTags;
+
+ // True if the track is gonna be (decrypted)/decoded and
+ // rendered directly by non-gecko components.
+ bool mIsRenderedExternally;
+
+ virtual AudioInfo* GetAsAudioInfo() { return nullptr; }
+ virtual VideoInfo* GetAsVideoInfo() { return nullptr; }
+ virtual TextInfo* GetAsTextInfo() { return nullptr; }
+ virtual const AudioInfo* GetAsAudioInfo() const { return nullptr; }
+ virtual const VideoInfo* GetAsVideoInfo() const { return nullptr; }
+ virtual const TextInfo* GetAsTextInfo() const { return nullptr; }
+
+ bool IsAudio() const { return !!GetAsAudioInfo(); }
+ bool IsVideo() const { return !!GetAsVideoInfo(); }
+ bool IsText() const { return !!GetAsTextInfo(); }
+ TrackType GetType() const { return mType; }
+
+ bool virtual IsValid() const = 0;
+
+ virtual UniquePtr<TrackInfo> Clone() const = 0;
+
+ MOZ_COUNTED_DTOR_VIRTUAL(TrackInfo)
+
+ protected:
+ TrackInfo(const TrackInfo& aOther) {
+ mId = aOther.mId;
+ mKind = aOther.mKind;
+ mLabel = aOther.mLabel;
+ mLanguage = aOther.mLanguage;
+ mEnabled = aOther.mEnabled;
+ mTrackId = aOther.mTrackId;
+ mMimeType = aOther.mMimeType;
+ mDuration = aOther.mDuration;
+ mMediaTime = aOther.mMediaTime;
+ mCrypto = aOther.mCrypto;
+ mIsRenderedExternally = aOther.mIsRenderedExternally;
+ mType = aOther.mType;
+ mTags = aOther.mTags.Clone();
+ MOZ_COUNT_CTOR(TrackInfo);
+ }
+ bool IsEqualTo(const TrackInfo& rhs) const;
+
+ private:
+ TrackType mType;
+};
+
+// String version of track type.
+const char* TrackTypeToStr(TrackInfo::TrackType aTrack);
+
+// Stores info relevant to presenting media frames.
+class VideoInfo : public TrackInfo {
+ public:
+ enum Rotation {
+ kDegree_0 = 0,
+ kDegree_90 = 90,
+ kDegree_180 = 180,
+ kDegree_270 = 270,
+ };
+ VideoInfo() : VideoInfo(-1, -1) {}
+
+ VideoInfo(int32_t aWidth, int32_t aHeight)
+ : VideoInfo(gfx::IntSize(aWidth, aHeight)) {}
+
+ explicit VideoInfo(const gfx::IntSize& aSize)
+ : TrackInfo(kVideoTrack, u"2"_ns, u"main"_ns, u""_ns, u""_ns, true, 2),
+ mDisplay(aSize),
+ mStereoMode(StereoMode::MONO),
+ mImage(aSize),
+ mCodecSpecificConfig(new MediaByteBuffer),
+ mExtraData(new MediaByteBuffer),
+ mRotation(kDegree_0) {}
+
+ VideoInfo(const VideoInfo& aOther) = default;
+
+ bool operator==(const VideoInfo& rhs) const;
+
+ bool IsValid() const override {
+ return mDisplay.width > 0 && mDisplay.height > 0;
+ }
+
+ VideoInfo* GetAsVideoInfo() override { return this; }
+
+ const VideoInfo* GetAsVideoInfo() const override { return this; }
+
+ UniquePtr<TrackInfo> Clone() const override {
+ return MakeUnique<VideoInfo>(*this);
+ }
+
+ void SetAlpha(bool aAlphaPresent) { mAlphaPresent = aAlphaPresent; }
+
+ bool HasAlpha() const { return mAlphaPresent; }
+
+ gfx::IntRect ImageRect() const {
+ if (!mImageRect) {
+ return gfx::IntRect(0, 0, mImage.width, mImage.height);
+ }
+ return *mImageRect;
+ }
+
+ void SetImageRect(const gfx::IntRect& aRect) { mImageRect = Some(aRect); }
+ void ResetImageRect() { mImageRect.reset(); }
+
+ // Returned the crop rectangle scaled to aWidth/aHeight size relative to
+ // mImage size.
+ // If aWidth and aHeight are identical to the original
+ // mImage.width/mImage.height then the scaling ratio will be 1. This is used
+ // for when the frame size is different from what the container reports. This
+ // is legal in WebM, and we will preserve the ratio of the crop rectangle as
+ // it was reported relative to the picture size reported by the container.
+ gfx::IntRect ScaledImageRect(int64_t aWidth, int64_t aHeight) const {
+ if ((aWidth == mImage.width && aHeight == mImage.height) || !mImage.width ||
+ !mImage.height) {
+ return ImageRect();
+ }
+
+ gfx::IntRect imageRect = ImageRect();
+ int64_t w = (aWidth * imageRect.Width()) / mImage.width;
+ int64_t h = (aHeight * imageRect.Height()) / mImage.height;
+ if (!w || !h) {
+ return imageRect;
+ }
+
+ imageRect.x = (imageRect.x * aWidth) / mImage.width;
+ imageRect.y = (imageRect.y * aHeight) / mImage.height;
+ imageRect.SetWidth(w);
+ imageRect.SetHeight(h);
+ return imageRect;
+ }
+
+ Rotation ToSupportedRotation(int32_t aDegree) const {
+ switch (aDegree) {
+ case 90:
+ return kDegree_90;
+ case 180:
+ return kDegree_180;
+ case 270:
+ return kDegree_270;
+ default:
+ NS_WARNING_ASSERTION(aDegree == 0, "Invalid rotation degree, ignored");
+ return kDegree_0;
+ }
+ }
+
+ // Size in pixels at which the video is rendered. This is after it has
+ // been scaled by its aspect ratio.
+ gfx::IntSize mDisplay;
+
+ // Indicates the frame layout for single track stereo videos.
+ StereoMode mStereoMode;
+
+ // Size of the decoded video's image.
+ gfx::IntSize mImage;
+
+ RefPtr<MediaByteBuffer> mCodecSpecificConfig;
+ RefPtr<MediaByteBuffer> mExtraData;
+
+ // Describing how many degrees video frames should be rotated in clock-wise to
+ // get correct view.
+ Rotation mRotation;
+
+ // Should be 8, 10 or 12. Default value is 8.
+ gfx::ColorDepth mColorDepth = gfx::ColorDepth::COLOR_8;
+
+ // Matrix coefficients (if specified by the video) imply a colorspace.
+ Maybe<gfx::YUVColorSpace> mColorSpace;
+
+ // Color primaries are independent from the coefficients.
+ Maybe<gfx::ColorSpace2> mColorPrimaries;
+
+ // Transfer functions get their own member, which may not be strongly
+ // correlated to the colorspace.
+ Maybe<gfx::TransferFunction> mTransferFunction;
+
+ // True indicates no restriction on Y, U, V values (otherwise 16-235 for 8
+ // bits etc)
+ gfx::ColorRange mColorRange = gfx::ColorRange::LIMITED;
+
+ Maybe<int32_t> GetFrameRate() const { return mFrameRate; }
+ void SetFrameRate(int32_t aRate) { mFrameRate = Some(aRate); }
+
+ private:
+ friend struct IPC::ParamTraits<VideoInfo>;
+
+ // mImage may be cropped; currently only used with the WebM container.
+ // If unset, no cropping is to occur.
+ Maybe<gfx::IntRect> mImageRect;
+
+ // Indicates whether or not frames may contain alpha information.
+ bool mAlphaPresent = false;
+
+ Maybe<int32_t> mFrameRate;
+};
+
+class AudioInfo : public TrackInfo {
+ public:
+ AudioInfo()
+ : TrackInfo(kAudioTrack, u"1"_ns, u"main"_ns, u""_ns, u""_ns, true, 1),
+ mRate(0),
+ mChannels(0),
+ mChannelMap(AudioConfig::ChannelLayout::UNKNOWN_MAP),
+ mBitDepth(0),
+ mProfile(0),
+ mExtendedProfile(0) {}
+
+ AudioInfo(const AudioInfo& aOther) = default;
+
+ bool operator==(const AudioInfo& rhs) const;
+
+ static const uint32_t MAX_RATE = 640000;
+ static const uint32_t MAX_CHANNEL_COUNT = 256;
+
+ bool IsValid() const override {
+ return mChannels > 0 && mChannels <= MAX_CHANNEL_COUNT && mRate > 0 &&
+ mRate <= MAX_RATE;
+ }
+
+ AudioInfo* GetAsAudioInfo() override { return this; }
+
+ const AudioInfo* GetAsAudioInfo() const override { return this; }
+
+ UniquePtr<TrackInfo> Clone() const override {
+ return MakeUnique<AudioInfo>(*this);
+ }
+
+ // Sample rate.
+ uint32_t mRate;
+
+ // Number of audio channels.
+ uint32_t mChannels;
+ // The AudioConfig::ChannelLayout map. Channels are ordered as per SMPTE
+ // definition. A value of UNKNOWN_MAP indicates unknown layout.
+ // ChannelMap is an unsigned bitmap compatible with Windows' WAVE and FFmpeg
+ // channel map.
+ AudioConfig::ChannelLayout::ChannelMap mChannelMap;
+
+ // Bits per sample.
+ uint32_t mBitDepth;
+
+ // Codec profile.
+ int8_t mProfile;
+
+ // Extended codec profile.
+ int8_t mExtendedProfile;
+
+ AudioCodecSpecificVariant mCodecSpecificConfig{NoCodecSpecificData{}};
+};
+
+class EncryptionInfo {
+ public:
+ EncryptionInfo() : mEncrypted(false) {}
+
+ struct InitData {
+ template <typename AInitDatas>
+ InitData(const nsAString& aType, AInitDatas&& aInitData)
+ : mType(aType), mInitData(std::forward<AInitDatas>(aInitData)) {}
+
+ // Encryption type to be passed to JS. Usually `cenc'.
+ nsString mType;
+
+ // Encryption data.
+ CopyableTArray<uint8_t> mInitData;
+ };
+ typedef CopyableTArray<InitData> InitDatas;
+
+ // True if the stream has encryption metadata
+ bool IsEncrypted() const { return mEncrypted; }
+
+ void Reset() {
+ mEncrypted = false;
+ mInitDatas.Clear();
+ }
+
+ template <typename AInitDatas>
+ void AddInitData(const nsAString& aType, AInitDatas&& aInitData) {
+ mInitDatas.AppendElement(
+ InitData(aType, std::forward<AInitDatas>(aInitData)));
+ mEncrypted = true;
+ }
+
+ void AddInitData(const EncryptionInfo& aInfo) {
+ mInitDatas.AppendElements(aInfo.mInitDatas);
+ mEncrypted = !!mInitDatas.Length();
+ }
+
+ // One 'InitData' per encrypted buffer.
+ InitDatas mInitDatas;
+
+ private:
+ bool mEncrypted;
+};
+
+class MediaInfo {
+ public:
+ bool HasVideo() const { return mVideo.IsValid(); }
+
+ void EnableVideo() {
+ if (HasVideo()) {
+ return;
+ }
+ // Set dummy values so that HasVideo() will return true;
+ // See VideoInfo::IsValid()
+ mVideo.mDisplay = gfx::IntSize(1, 1);
+ }
+
+ bool HasAudio() const { return mAudio.IsValid(); }
+
+ void EnableAudio() {
+ if (HasAudio()) {
+ return;
+ }
+ // Set dummy values so that HasAudio() will return true;
+ // See AudioInfo::IsValid()
+ mAudio.mChannels = 2;
+ mAudio.mRate = 44100;
+ }
+
+ bool IsEncrypted() const {
+ return (HasAudio() && mAudio.mCrypto.IsEncrypted()) ||
+ (HasVideo() && mVideo.mCrypto.IsEncrypted());
+ }
+
+ bool HasValidMedia() const { return HasVideo() || HasAudio(); }
+
+ // TODO: Store VideoInfo and AudioIndo in arrays to support multi-tracks.
+ VideoInfo mVideo;
+ AudioInfo mAudio;
+
+ // If the metadata includes a duration, we store it here.
+ media::NullableTimeUnit mMetadataDuration;
+
+ // The Ogg reader tries to kinda-sorta compute the duration by seeking to the
+ // end and determining the timestamp of the last frame. This isn't useful as
+ // a duration until we know the start time, so we need to track it separately.
+ media::NullableTimeUnit mUnadjustedMetadataEndTime;
+
+ // True if the media is seekable (i.e. supports random access).
+ bool mMediaSeekable = true;
+
+ // True if the media is only seekable within its buffered ranges.
+ bool mMediaSeekableOnlyInBufferedRanges = false;
+
+ EncryptionInfo mCrypto;
+
+ // The minimum of start times of audio and video tracks.
+ // Use to map the zero time on the media timeline to the first frame.
+ media::TimeUnit mStartTime;
+};
+
+class TrackInfoSharedPtr {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(TrackInfoSharedPtr)
+ public:
+ TrackInfoSharedPtr(const TrackInfo& aOriginal, uint32_t aStreamID)
+ : mInfo(aOriginal.Clone()),
+ mStreamSourceID(aStreamID),
+ mMimeType(mInfo->mMimeType) {}
+
+ uint32_t GetID() const { return mStreamSourceID; }
+
+ operator const TrackInfo*() const { return mInfo.get(); }
+
+ const TrackInfo* operator*() const { return mInfo.get(); }
+
+ const TrackInfo* operator->() const {
+ MOZ_ASSERT(mInfo.get(), "dereferencing a UniquePtr containing nullptr");
+ return mInfo.get();
+ }
+
+ const AudioInfo* GetAsAudioInfo() const {
+ return mInfo ? mInfo->GetAsAudioInfo() : nullptr;
+ }
+
+ const VideoInfo* GetAsVideoInfo() const {
+ return mInfo ? mInfo->GetAsVideoInfo() : nullptr;
+ }
+
+ const TextInfo* GetAsTextInfo() const {
+ return mInfo ? mInfo->GetAsTextInfo() : nullptr;
+ }
+
+ private:
+ ~TrackInfoSharedPtr() = default;
+ UniquePtr<TrackInfo> mInfo;
+ // A unique ID, guaranteed to change when changing streams.
+ uint32_t mStreamSourceID;
+
+ public:
+ const nsCString& mMimeType;
+};
+
+} // namespace mozilla
+
+#endif // MediaInfo_h
diff --git a/dom/media/MediaMIMETypes.cpp b/dom/media/MediaMIMETypes.cpp
new file mode 100644
index 0000000000..a5da17ea44
--- /dev/null
+++ b/dom/media/MediaMIMETypes.cpp
@@ -0,0 +1,267 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaMIMETypes.h"
+
+#include "nsContentTypeParser.h"
+#include "mozilla/dom/MediaCapabilitiesBinding.h"
+
+namespace mozilla {
+
+template <int N>
+static bool StartsWith(const nsACString& string, const char (&prefix)[N]) {
+ if (N - 1 > string.Length()) {
+ return false;
+ }
+ return memcmp(string.Data(), prefix, N - 1) == 0;
+}
+
+bool MediaMIMEType::HasApplicationMajorType() const {
+ return StartsWith(mMIMEType, "application/");
+}
+
+bool MediaMIMEType::HasAudioMajorType() const {
+ return StartsWith(mMIMEType, "audio/");
+}
+
+bool MediaMIMEType::HasVideoMajorType() const {
+ return StartsWith(mMIMEType, "video/");
+}
+
+size_t MediaMIMEType::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ return mMIMEType.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+}
+
+MediaMIMEType::MediaMIMEType(const nsACString& aType) : mMIMEType(aType) {}
+
+Maybe<MediaMIMEType> MakeMediaMIMEType(const nsAString& aType) {
+ nsContentTypeParser parser(aType);
+ nsAutoString mime;
+ nsresult rv = parser.GetType(mime);
+ if (!NS_SUCCEEDED(rv) || mime.IsEmpty()) {
+ return Nothing();
+ }
+
+ NS_ConvertUTF16toUTF8 mime8{mime};
+ if (!IsMediaMIMEType(mime8)) {
+ return Nothing();
+ }
+
+ return Some(MediaMIMEType(mime8));
+}
+
+Maybe<MediaMIMEType> MakeMediaMIMEType(const nsACString& aType) {
+ return MakeMediaMIMEType(NS_ConvertUTF8toUTF16(aType));
+}
+
+Maybe<MediaMIMEType> MakeMediaMIMEType(const char* aType) {
+ if (!aType) {
+ return Nothing();
+ }
+ return MakeMediaMIMEType(nsDependentCString(aType));
+}
+
+bool MediaCodecs::Contains(const nsAString& aCodec) const {
+ for (const auto& myCodec : Range()) {
+ if (myCodec == aCodec) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool MediaCodecs::ContainsAll(const MediaCodecs& aCodecs) const {
+ const auto& codecsToTest = aCodecs.Range();
+ for (const auto& codecToTest : codecsToTest) {
+ if (!Contains(codecToTest)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool MediaCodecs::ContainsPrefix(const nsAString& aCodecPrefix) const {
+ const size_t prefixLength = aCodecPrefix.Length();
+ for (const auto& myCodec : Range()) {
+ if (myCodec.Length() >= prefixLength &&
+ memcmp(myCodec.Data(), aCodecPrefix.Data(),
+ prefixLength * sizeof(char16_t)) == 0) {
+ return true;
+ }
+ }
+ return false;
+}
+
+size_t MediaCodecs::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ return mCodecs.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+}
+
+static int32_t GetParameterAsNumber(const nsContentTypeParser& aParser,
+ const char* aParameter,
+ const int32_t aErrorReturn) {
+ nsAutoString parameterString;
+ nsresult rv = aParser.GetParameter(aParameter, parameterString);
+ if (NS_FAILED_impl(rv)) {
+ return aErrorReturn;
+ }
+ int32_t number = parameterString.ToInteger(&rv);
+ if (MOZ_UNLIKELY(NS_FAILED_impl(rv))) {
+ return aErrorReturn;
+ }
+ return number;
+}
+
+MediaExtendedMIMEType::MediaExtendedMIMEType(
+ const nsACString& aOriginalString, const nsACString& aMIMEType,
+ bool aHaveCodecs, const nsAString& aCodecs, int32_t aWidth, int32_t aHeight,
+ double aFramerate, int32_t aBitrate)
+ : mOriginalString(aOriginalString),
+ mMIMEType(aMIMEType),
+ mHaveCodecs(aHaveCodecs),
+ mCodecs(aCodecs),
+ mWidth(aWidth),
+ mHeight(aHeight),
+ mFramerate(aFramerate),
+ mBitrate(aBitrate) {}
+
+MediaExtendedMIMEType::MediaExtendedMIMEType(
+ const nsACString& aOriginalString, const nsACString& aMIMEType,
+ bool aHaveCodecs, const nsAString& aCodecs, int32_t aChannels,
+ int32_t aSamplerate, int32_t aBitrate)
+ : mOriginalString(aOriginalString),
+ mMIMEType(aMIMEType),
+ mHaveCodecs(aHaveCodecs),
+ mCodecs(aCodecs),
+ mChannels(aChannels),
+ mSamplerate(aSamplerate),
+ mBitrate(aBitrate) {}
+
+MediaExtendedMIMEType::MediaExtendedMIMEType(const MediaMIMEType& aType)
+ : mOriginalString(aType.AsString()), mMIMEType(aType) {}
+
+MediaExtendedMIMEType::MediaExtendedMIMEType(MediaMIMEType&& aType)
+ : mOriginalString(aType.AsString()), mMIMEType(std::move(aType)) {}
+
+Maybe<MediaExtendedMIMEType> MakeMediaExtendedMIMEType(const nsAString& aType) {
+ nsContentTypeParser parser(aType);
+ nsAutoString mime;
+ nsresult rv = parser.GetType(mime);
+ if (!NS_SUCCEEDED(rv) || mime.IsEmpty()) {
+ return Nothing();
+ }
+
+ NS_ConvertUTF16toUTF8 mime8{mime};
+ if (!IsMediaMIMEType(mime8)) {
+ return Nothing();
+ }
+
+ nsAutoString codecs;
+ rv = parser.GetParameter("codecs", codecs);
+ bool haveCodecs = NS_SUCCEEDED(rv);
+
+ int32_t width = GetParameterAsNumber(parser, "width", -1);
+ int32_t height = GetParameterAsNumber(parser, "height", -1);
+ double framerate = GetParameterAsNumber(parser, "framerate", -1);
+ int32_t bitrate = GetParameterAsNumber(parser, "bitrate", -1);
+
+ return Some(MediaExtendedMIMEType(NS_ConvertUTF16toUTF8(aType), mime8,
+ haveCodecs, codecs, width, height,
+ framerate, bitrate));
+}
+
+Maybe<MediaExtendedMIMEType> MakeMediaExtendedMIMEType(
+ const dom::VideoConfiguration& aConfig) {
+ if (aConfig.mContentType.IsEmpty()) {
+ return Nothing();
+ }
+ nsContentTypeParser parser(aConfig.mContentType);
+ nsAutoString mime;
+ nsresult rv = parser.GetType(mime);
+ if (!NS_SUCCEEDED(rv) || mime.IsEmpty()) {
+ return Nothing();
+ }
+
+ NS_ConvertUTF16toUTF8 mime8{mime};
+ if (!IsMediaMIMEType(mime8)) {
+ return Nothing();
+ }
+
+ nsAutoString codecs;
+ rv = parser.GetParameter("codecs", codecs);
+ bool haveCodecs = NS_SUCCEEDED(rv);
+
+ if (!std::isfinite(aConfig.mFramerate) || aConfig.mFramerate <= 0.0) {
+ return Nothing();
+ }
+
+ return Some(MediaExtendedMIMEType(
+ NS_ConvertUTF16toUTF8(aConfig.mContentType), mime8, haveCodecs, codecs,
+ aConfig.mWidth, aConfig.mHeight, aConfig.mFramerate, aConfig.mBitrate));
+}
+
+Maybe<MediaExtendedMIMEType> MakeMediaExtendedMIMEType(
+ const dom::AudioConfiguration& aConfig) {
+ if (aConfig.mContentType.IsEmpty()) {
+ return Nothing();
+ }
+ nsContentTypeParser parser(aConfig.mContentType);
+ nsAutoString mime;
+ nsresult rv = parser.GetType(mime);
+ if (!NS_SUCCEEDED(rv) || mime.IsEmpty()) {
+ return Nothing();
+ }
+
+ NS_ConvertUTF16toUTF8 mime8{mime};
+ if (!IsMediaMIMEType(mime8)) {
+ return Nothing();
+ }
+
+ nsAutoString codecs;
+ rv = parser.GetParameter("codecs", codecs);
+ bool haveCodecs = NS_SUCCEEDED(rv);
+
+ int32_t channels = 2; // use a stereo config if not known.
+ if (aConfig.mChannels.WasPassed()) {
+ // A channels string was passed. Make sure it is valid.
+ nsresult error;
+ double value = aConfig.mChannels.Value().ToDouble(&error);
+ if (NS_FAILED(error)) {
+ return Nothing();
+ }
+ // Value is a channel configuration such as 5.1. We want to treat this as 6.
+ channels = value;
+ double fp = value - channels;
+ // round up as .1 and .2 aren't exactly expressible in binary.
+ channels += (fp * 10) + .5;
+ }
+
+ return Some(MediaExtendedMIMEType(
+ NS_ConvertUTF16toUTF8(aConfig.mContentType), mime8, haveCodecs, codecs,
+ channels,
+ aConfig.mSamplerate.WasPassed() ? aConfig.mSamplerate.Value() : 48000,
+ aConfig.mBitrate.WasPassed() ? aConfig.mBitrate.Value() : 131072));
+}
+
+size_t MediaExtendedMIMEType::SizeOfExcludingThis(
+ MallocSizeOf aMallocSizeOf) const {
+ return mOriginalString.SizeOfExcludingThisIfUnshared(aMallocSizeOf) +
+ mMIMEType.SizeOfExcludingThis(aMallocSizeOf) +
+ mCodecs.SizeOfExcludingThis(aMallocSizeOf);
+}
+
+Maybe<MediaExtendedMIMEType> MakeMediaExtendedMIMEType(
+ const nsACString& aType) {
+ return MakeMediaExtendedMIMEType(NS_ConvertUTF8toUTF16(aType));
+}
+
+Maybe<MediaExtendedMIMEType> MakeMediaExtendedMIMEType(const char* aType) {
+ if (!aType) {
+ return Nothing();
+ }
+ return MakeMediaExtendedMIMEType(nsDependentCString(aType));
+}
+
+} // namespace mozilla
diff --git a/dom/media/MediaMIMETypes.h b/dom/media/MediaMIMETypes.h
new file mode 100644
index 0000000000..ff1397df72
--- /dev/null
+++ b/dom/media/MediaMIMETypes.h
@@ -0,0 +1,222 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MediaMIMETypes_h_
+#define MediaMIMETypes_h_
+
+#include "VideoUtils.h"
+#include "mozilla/Maybe.h"
+#include "nsString.h"
+
+namespace mozilla {
+
+namespace dom {
+struct AudioConfiguration;
+struct VideoConfiguration;
+} // namespace dom
+
+// Class containing pointing at a media MIME "type/subtype" string literal.
+// See IsMediaMIMEType for restrictions.
+// Mainly used to help construct a MediaMIMEType through the statically-checked
+// MEDIAMIMETYPE macro, or to compare a MediaMIMEType to a literal.
+class DependentMediaMIMEType {
+ public:
+ // Construction from a literal. Checked in debug builds.
+ // Use MEDIAMIMETYPE macro instead, for static checking.
+ template <size_t N>
+ explicit DependentMediaMIMEType(const char (&aType)[N])
+ : mMIMEType(aType, N - 1) {
+ MOZ_ASSERT(IsMediaMIMEType(aType, N - 1), "Invalid media MIME type");
+ }
+
+ // MIME "type/subtype".
+ const nsDependentCString& AsDependentString() const { return mMIMEType; }
+
+ private:
+ nsDependentCString mMIMEType;
+};
+
+// Instantiate a DependentMediaMIMEType from a literal. Statically checked.
+#define MEDIAMIMETYPE(LIT) \
+ static_cast<const DependentMediaMIMEType&>([]() { \
+ static_assert(IsMediaMIMEType(LIT), "Invalid media MIME type"); \
+ return DependentMediaMIMEType(LIT); \
+ }())
+
+// Class containing only pre-parsed lowercase media MIME type/subtype.
+class MediaMIMEType {
+ public:
+ // Construction from a DependentMediaMIMEType, with its inherent checks.
+ // Implicit so MEDIAMIMETYPE can be used wherever a MediaMIMEType is expected.
+ MOZ_IMPLICIT MediaMIMEType(const DependentMediaMIMEType& aType)
+ : mMIMEType(aType.AsDependentString()) {}
+
+ // MIME "type/subtype", always lowercase.
+ const nsCString& AsString() const { return mMIMEType; }
+
+ // Comparison with DependentMediaMIMEType.
+ // Useful to compare to MEDIAMIMETYPE literals.
+ bool operator==(const DependentMediaMIMEType& aOther) const {
+ return mMIMEType.Equals(aOther.AsDependentString());
+ }
+ bool operator!=(const DependentMediaMIMEType& aOther) const {
+ return !mMIMEType.Equals(aOther.AsDependentString());
+ }
+
+ bool operator==(const MediaMIMEType& aOther) const {
+ return mMIMEType.Equals(aOther.mMIMEType);
+ }
+ bool operator!=(const MediaMIMEType& aOther) const {
+ return !mMIMEType.Equals(aOther.mMIMEType);
+ }
+
+ // True if type starts with "application/".
+ bool HasApplicationMajorType() const;
+ // True if type starts with "audio/".
+ // Note that some audio content could be stored in a "video/..." container!
+ bool HasAudioMajorType() const;
+ // True if type starts with "video/".
+ // Note that this does not guarantee 100% that the content is actually video!
+ // (e.g., "video/webm" could contain a vorbis audio track.)
+ bool HasVideoMajorType() const;
+
+ size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ private:
+ friend Maybe<MediaMIMEType> MakeMediaMIMEType(const nsAString& aType);
+ friend class MediaExtendedMIMEType;
+ explicit MediaMIMEType(const nsACString& aType);
+
+ nsCString mMIMEType; // UTF8 MIME "type/subtype".
+};
+
+Maybe<MediaMIMEType> MakeMediaMIMEType(const nsAString& aType);
+Maybe<MediaMIMEType> MakeMediaMIMEType(const nsACString& aType);
+Maybe<MediaMIMEType> MakeMediaMIMEType(const char* aType);
+
+// A list of case-sensitive codecs attached to a MediaExtendedMIMEType.
+class MediaCodecs {
+ public:
+ MediaCodecs() = default;
+ // Construction from a comma-separated list of codecs. Unchecked.
+ explicit MediaCodecs(const nsAString& aCodecs) : mCodecs(aCodecs) {}
+ // Construction from a literal comma-separated list of codecs. Unchecked.
+ template <size_t N>
+ explicit MediaCodecs(const char (&aCodecs)[N])
+ : mCodecs(NS_ConvertUTF8toUTF16(aCodecs, N - 1)) {}
+
+ bool IsEmpty() const { return mCodecs.IsEmpty(); }
+ const nsString& AsString() const { return mCodecs; }
+
+ using RangeType =
+ const StringListRange<nsString,
+ StringListRangeEmptyItems::ProcessEmptyItems>;
+
+ // Produces a range object with begin()&end(), can be used in range-for loops.
+ // This will iterate through all codecs, even empty ones (except if the
+ // original list was an empty string). Iterators dereference to
+ // 'const nsDependentString', valid for as long as this MediaCodecs object.
+ RangeType Range() const { return RangeType(mCodecs); };
+
+ // Does this list of codecs contain the given aCodec?
+ bool Contains(const nsAString& aCodec) const;
+ // Does this list of codecs contain *all* the codecs in the given list?
+ bool ContainsAll(const MediaCodecs& aCodecs) const;
+
+ // Does this list of codecs contain a codec starting with the given prefix?
+ bool ContainsPrefix(const nsAString& aCodecPrefix) const;
+
+ template <size_t N>
+ bool operator==(const char (&aType)[N]) const {
+ return mCodecs.EqualsASCII(aType, N - 1);
+ }
+
+ size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ private:
+ // UTF16 comma-separated list of codecs.
+ // See http://www.rfc-editor.org/rfc/rfc4281.txt for the description
+ // of the 'codecs' parameter.
+ nsString mCodecs;
+};
+
+// Class containing pre-parsed media MIME type parameters, e.g.:
+// MIME type/subtype, optional codecs, etc.
+class MediaExtendedMIMEType {
+ public:
+ explicit MediaExtendedMIMEType(const MediaMIMEType& aType);
+ explicit MediaExtendedMIMEType(MediaMIMEType&& aType);
+
+ // MIME "type/subtype".
+ const MediaMIMEType& Type() const { return mMIMEType; }
+
+ // Was there an explicit 'codecs' parameter provided?
+ bool HaveCodecs() const { return mHaveCodecs; }
+ // Codecs. May be empty if not provided or explicitly provided as empty.
+ const MediaCodecs& Codecs() const { return mCodecs; }
+
+ // Sizes and rates.
+ Maybe<int32_t> GetWidth() const { return GetMaybeNumber(mWidth); }
+ Maybe<int32_t> GetHeight() const { return GetMaybeNumber(mHeight); }
+ Maybe<double> GetFramerate() const { return GetMaybeNumber(mFramerate); }
+ Maybe<int32_t> GetBitrate() const { return GetMaybeNumber(mBitrate); }
+ Maybe<int32_t> GetChannels() const { return GetMaybeNumber(mChannels); }
+ Maybe<int32_t> GetSamplerate() const { return GetMaybeNumber(mSamplerate); }
+
+ // Original string. Note that "type/subtype" may not be lowercase,
+ // use Type().AsString() instead to get the normalized "type/subtype".
+ const nsCString& OriginalString() const { return mOriginalString; }
+
+ size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ private:
+ friend Maybe<MediaExtendedMIMEType> MakeMediaExtendedMIMEType(
+ const nsAString& aType);
+ friend Maybe<MediaExtendedMIMEType> MakeMediaExtendedMIMEType(
+ const dom::VideoConfiguration& aConfig);
+ friend Maybe<MediaExtendedMIMEType> MakeMediaExtendedMIMEType(
+ const dom::AudioConfiguration& aConfig);
+
+ MediaExtendedMIMEType(const nsACString& aOriginalString,
+ const nsACString& aMIMEType, bool aHaveCodecs,
+ const nsAString& aCodecs, int32_t aWidth,
+ int32_t aHeight, double aFramerate, int32_t aBitrate);
+ MediaExtendedMIMEType(const nsACString& aOriginalString,
+ const nsACString& aMIMEType, bool aHaveCodecs,
+ const nsAString& aCodecs, int32_t aChannels,
+ int32_t aSamplerate, int32_t aBitrate);
+
+ template <typename T>
+ Maybe<T> GetMaybeNumber(T aNumber) const {
+ return (aNumber < 0) ? Maybe<T>(Nothing()) : Some(T(aNumber));
+ }
+
+ nsCString mOriginalString; // Original full string.
+ MediaMIMEType mMIMEType; // MIME type/subtype.
+ bool mHaveCodecs = false; // If false, mCodecs must be empty.
+ MediaCodecs mCodecs;
+ // For video
+ int32_t mWidth = -1; // -1 if not provided.
+ int32_t mHeight = -1; // -1 if not provided.
+ double mFramerate = -1; // -1 if not provided.
+ // For audio
+ int32_t mChannels = -1; // -1 if not provided.
+ int32_t mSamplerate = -1; // -1 if not provided.
+ // For both audio and video.
+ int32_t mBitrate = -1; // -1 if not provided.
+};
+
+Maybe<MediaExtendedMIMEType> MakeMediaExtendedMIMEType(const nsAString& aType);
+Maybe<MediaExtendedMIMEType> MakeMediaExtendedMIMEType(const nsACString& aType);
+Maybe<MediaExtendedMIMEType> MakeMediaExtendedMIMEType(const char* aType);
+Maybe<MediaExtendedMIMEType> MakeMediaExtendedMIMEType(
+ const dom::VideoConfiguration& aConfig);
+Maybe<MediaExtendedMIMEType> MakeMediaExtendedMIMEType(
+ const dom::AudioConfiguration& aConfig);
+
+} // namespace mozilla
+
+#endif // MediaMIMETypes_h_
diff --git a/dom/media/MediaManager.cpp b/dom/media/MediaManager.cpp
new file mode 100644
index 0000000000..ec7315ae22
--- /dev/null
+++ b/dom/media/MediaManager.cpp
@@ -0,0 +1,4404 @@
+/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaManager.h"
+
+#include "AudioCaptureTrack.h"
+#include "AudioDeviceInfo.h"
+#include "AudioStreamTrack.h"
+#include "CubebDeviceEnumerator.h"
+#include "MediaTimer.h"
+#include "MediaTrackConstraints.h"
+#include "MediaTrackGraphImpl.h"
+#include "MediaTrackListener.h"
+#include "VideoStreamTrack.h"
+#include "VideoUtils.h"
+#include "mozilla/Base64.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/NullPrincipal.h"
+#include "mozilla/PeerIdentity.h"
+#include "mozilla/PermissionDelegateHandler.h"
+#include "mozilla/Sprintf.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/Types.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/FeaturePolicyUtils.h"
+#include "mozilla/dom/File.h"
+#include "mozilla/dom/GetUserMediaRequestBinding.h"
+#include "mozilla/dom/MediaDeviceInfo.h"
+#include "mozilla/dom/MediaDevices.h"
+#include "mozilla/dom/MediaDevicesBinding.h"
+#include "mozilla/dom/MediaStreamBinding.h"
+#include "mozilla/dom/MediaStreamTrackBinding.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/UserActivation.h"
+#include "mozilla/dom/WindowContext.h"
+#include "mozilla/dom/WindowGlobalChild.h"
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/media/MediaChild.h"
+#include "mozilla/media/MediaTaskUtils.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsArray.h"
+#include "nsContentUtils.h"
+#include "nsGlobalWindow.h"
+#include "nsHashPropertyBag.h"
+#include "nsIEventTarget.h"
+#include "nsIPermissionManager.h"
+#include "nsIUUIDGenerator.h"
+#include "nsJSUtils.h"
+#include "nsNetCID.h"
+#include "nsNetUtil.h"
+#include "nsPIDOMWindow.h"
+#include "nsProxyRelease.h"
+#include "nspr.h"
+#include "nss.h"
+#include "pk11pub.h"
+
+/* Using WebRTC backend on Desktops (Mac, Windows, Linux), otherwise default */
+#include "MediaEngineFake.h"
+#include "MediaEngineSource.h"
+#if defined(MOZ_WEBRTC)
+# include "MediaEngineWebRTC.h"
+# include "MediaEngineWebRTCAudio.h"
+# include "browser_logging/WebRtcLog.h"
+# include "modules/audio_processing/include/audio_processing.h"
+#endif
+
+#if defined(XP_WIN)
+# include <iphlpapi.h>
+# include <objbase.h>
+# include <tchar.h>
+# include <winsock2.h>
+
+# include "mozilla/WindowsVersion.h"
+#endif
+
+// A specialization of nsMainThreadPtrHolder for
+// mozilla::dom::CallbackObjectHolder. See documentation for
+// nsMainThreadPtrHolder in nsProxyRelease.h. This specialization lets us avoid
+// wrapping the CallbackObjectHolder into a separate refcounted object.
+template <class WebIDLCallbackT, class XPCOMCallbackT>
+class nsMainThreadPtrHolder<
+ mozilla::dom::CallbackObjectHolder<WebIDLCallbackT, XPCOMCallbackT>>
+ final {
+ typedef mozilla::dom::CallbackObjectHolder<WebIDLCallbackT, XPCOMCallbackT>
+ Holder;
+
+ public:
+ nsMainThreadPtrHolder(const char* aName, Holder&& aHolder)
+ : mHolder(std::move(aHolder))
+#ifndef RELEASE_OR_BETA
+ ,
+ mName(aName)
+#endif
+ {
+ MOZ_ASSERT(NS_IsMainThread());
+ }
+
+ private:
+ // We can be released on any thread.
+ ~nsMainThreadPtrHolder() {
+ if (NS_IsMainThread()) {
+ mHolder.Reset();
+ } else if (mHolder.GetISupports()) {
+ nsCOMPtr<nsIEventTarget> target = do_GetMainThread();
+ MOZ_ASSERT(target);
+ NS_ProxyRelease(
+#ifdef RELEASE_OR_BETA
+ nullptr,
+#else
+ mName,
+#endif
+ target, mHolder.Forget());
+ }
+ }
+
+ public:
+ Holder* get() {
+ // Nobody should be touching the raw pointer off-main-thread.
+ if (MOZ_UNLIKELY(!NS_IsMainThread())) {
+ NS_ERROR("Can't dereference nsMainThreadPtrHolder off main thread");
+ MOZ_CRASH();
+ }
+ return &mHolder;
+ }
+
+ bool operator!() const { return !mHolder; }
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(nsMainThreadPtrHolder<Holder>)
+
+ private:
+ // Our holder.
+ Holder mHolder;
+
+#ifndef RELEASE_OR_BETA
+ const char* mName = nullptr;
+#endif
+
+ // Copy constructor and operator= not implemented. Once constructed, the
+ // holder is immutable.
+ Holder& operator=(const nsMainThreadPtrHolder& aOther) = delete;
+ nsMainThreadPtrHolder(const nsMainThreadPtrHolder& aOther) = delete;
+};
+
+namespace mozilla {
+
+LazyLogModule gMediaManagerLog("MediaManager");
+#define LOG(...) MOZ_LOG(gMediaManagerLog, LogLevel::Debug, (__VA_ARGS__))
+
+class GetUserMediaStreamTask;
+class LocalTrackSource;
+class SelectAudioOutputTask;
+
+using dom::BFCacheStatus;
+using dom::CallerType;
+using dom::ConstrainDOMStringParameters;
+using dom::ConstrainDoubleRange;
+using dom::ConstrainLongRange;
+using dom::DisplayMediaStreamConstraints;
+using dom::Document;
+using dom::Element;
+using dom::FeaturePolicyUtils;
+using dom::File;
+using dom::GetUserMediaRequest;
+using dom::MediaDeviceKind;
+using dom::MediaDevices;
+using dom::MediaSourceEnum;
+using dom::MediaStreamConstraints;
+using dom::MediaStreamError;
+using dom::MediaStreamTrack;
+using dom::MediaStreamTrackSource;
+using dom::MediaTrackConstraints;
+using dom::MediaTrackConstraintSet;
+using dom::MediaTrackSettings;
+using dom::OwningBooleanOrMediaTrackConstraints;
+using dom::OwningStringOrStringSequence;
+using dom::OwningStringOrStringSequenceOrConstrainDOMStringParameters;
+using dom::Promise;
+using dom::Sequence;
+using dom::UserActivation;
+using dom::WindowGlobalChild;
+using ConstDeviceSetPromise = MediaManager::ConstDeviceSetPromise;
+using LocalDevicePromise = MediaManager::LocalDevicePromise;
+using LocalDeviceSetPromise = MediaManager::LocalDeviceSetPromise;
+using LocalMediaDeviceSetRefCnt = MediaManager::LocalMediaDeviceSetRefCnt;
+using media::NewRunnableFrom;
+using media::NewTaskFrom;
+using media::Refcountable;
+
+// Whether main thread actions of MediaManager shutdown (except for clearing
+// of sSingleton) have completed.
+static bool sHasMainThreadShutdown;
+
+struct DeviceState {
+ DeviceState(RefPtr<LocalMediaDevice> aDevice,
+ RefPtr<LocalTrackSource> aTrackSource, bool aOffWhileDisabled)
+ : mOffWhileDisabled(aOffWhileDisabled),
+ mDevice(std::move(aDevice)),
+ mTrackSource(std::move(aTrackSource)) {
+ MOZ_ASSERT(mDevice);
+ MOZ_ASSERT(mTrackSource);
+ }
+
+ // true if we have stopped mDevice, this is a terminal state.
+ // MainThread only.
+ bool mStopped = false;
+
+ // true if mDevice is currently enabled.
+ // A device must be both enabled and unmuted to be turned on and capturing.
+ // MainThread only.
+ bool mDeviceEnabled = false;
+
+ // true if mDevice is currently muted.
+ // A device that is either muted or disabled is turned off and not capturing.
+ // MainThread only.
+ bool mDeviceMuted;
+
+ // true if the application has currently enabled mDevice.
+ // MainThread only.
+ bool mTrackEnabled = false;
+
+ // Time when the application last enabled mDevice.
+ // MainThread only.
+ TimeStamp mTrackEnabledTime;
+
+ // true if an operation to Start() or Stop() mDevice has been dispatched to
+ // the media thread and is not finished yet.
+ // MainThread only.
+ bool mOperationInProgress = false;
+
+ // true if we are allowed to turn off the underlying source while all tracks
+ // are disabled. Only affects disabling; always turns off on user-agent mute.
+ // MainThread only.
+ bool mOffWhileDisabled = false;
+
+ // Timer triggered by a MediaStreamTrackSource signaling that all tracks got
+ // disabled. When the timer fires we initiate Stop()ing mDevice.
+ // If set we allow dynamically stopping and starting mDevice.
+ // Any thread.
+ const RefPtr<MediaTimer> mDisableTimer = new MediaTimer();
+
+ // The underlying device we keep state for. Always non-null.
+ // Threadsafe access, but see method declarations for individual constraints.
+ const RefPtr<LocalMediaDevice> mDevice;
+
+ // The MediaStreamTrackSource for any tracks (original and clones) originating
+ // from this device. Always non-null. Threadsafe access, but see method
+ // declarations for individual constraints.
+ const RefPtr<LocalTrackSource> mTrackSource;
+};
+
+/**
+ * This mimics the capture state from nsIMediaManagerService.
+ */
+enum class CaptureState : uint16_t {
+ Off = nsIMediaManagerService::STATE_NOCAPTURE,
+ Enabled = nsIMediaManagerService::STATE_CAPTURE_ENABLED,
+ Disabled = nsIMediaManagerService::STATE_CAPTURE_DISABLED,
+};
+
+static CaptureState CombineCaptureState(CaptureState aFirst,
+ CaptureState aSecond) {
+ if (aFirst == CaptureState::Enabled || aSecond == CaptureState::Enabled) {
+ return CaptureState::Enabled;
+ }
+ if (aFirst == CaptureState::Disabled || aSecond == CaptureState::Disabled) {
+ return CaptureState::Disabled;
+ }
+ MOZ_ASSERT(aFirst == CaptureState::Off);
+ MOZ_ASSERT(aSecond == CaptureState::Off);
+ return CaptureState::Off;
+}
+
+static uint16_t FromCaptureState(CaptureState aState) {
+ MOZ_ASSERT(aState == CaptureState::Off || aState == CaptureState::Enabled ||
+ aState == CaptureState::Disabled);
+ return static_cast<uint16_t>(aState);
+}
+
+void MediaManager::CallOnError(GetUserMediaErrorCallback& aCallback,
+ MediaStreamError& aError) {
+ aCallback.Call(aError);
+}
+
+void MediaManager::CallOnSuccess(GetUserMediaSuccessCallback& aCallback,
+ DOMMediaStream& aStream) {
+ aCallback.Call(aStream);
+}
+
+/**
+ * DeviceListener has threadsafe refcounting for use across the main, media and
+ * MTG threads. But it has a non-threadsafe SupportsWeakPtr for WeakPtr usage
+ * only from main thread, to ensure that garbage- and cycle-collected objects
+ * don't hold a reference to it during late shutdown.
+ */
+class DeviceListener : public SupportsWeakPtr {
+ public:
+ typedef MozPromise<bool /* aIgnored */, RefPtr<MediaMgrError>, true>
+ DeviceListenerPromise;
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING_WITH_DELETE_ON_MAIN_THREAD(
+ DeviceListener)
+
+ DeviceListener();
+
+ /**
+ * Registers this device listener as belonging to the given window listener.
+ * Stop() must be called on registered DeviceListeners before destruction.
+ */
+ void Register(GetUserMediaWindowListener* aListener);
+
+ /**
+ * Marks this listener as active and creates the internal device state.
+ */
+ void Activate(RefPtr<LocalMediaDevice> aDevice,
+ RefPtr<LocalTrackSource> aTrackSource, bool aStartMuted);
+
+ /**
+ * Posts a task to initialize and start the associated device.
+ */
+ RefPtr<DeviceListenerPromise> InitializeAsync();
+
+ /**
+ * Posts a task to stop the device associated with this DeviceListener and
+ * notifies the associated window listener that a track was stopped.
+ *
+ * This will also clean up the weak reference to the associated window
+ * listener, and tell the window listener to remove its hard reference to this
+ * DeviceListener, so any caller will need to keep its own hard ref.
+ */
+ void Stop();
+
+ /**
+ * Gets the main thread MediaTrackSettings from the MediaEngineSource
+ * associated with aTrack.
+ */
+ void GetSettings(MediaTrackSettings& aOutSettings) const;
+
+ /**
+ * Posts a task to set the enabled state of the device associated with this
+ * DeviceListener to aEnabled and notifies the associated window listener that
+ * a track's state has changed.
+ *
+ * Turning the hardware off while the device is disabled is supported for:
+ * - Camera (enabled by default, controlled by pref
+ * "media.getusermedia.camera.off_while_disabled.enabled")
+ * - Microphone (disabled by default, controlled by pref
+ * "media.getusermedia.microphone.off_while_disabled.enabled")
+ * Screen-, app-, or windowsharing is not supported at this time.
+ *
+ * The behavior is also different between disabling and enabling a device.
+ * While enabling is immediate, disabling only happens after a delay.
+ * This is now defaulting to 3 seconds but can be overriden by prefs:
+ * - "media.getusermedia.camera.off_while_disabled.delay_ms" and
+ * - "media.getusermedia.microphone.off_while_disabled.delay_ms".
+ *
+ * The delay is in place to prevent misuse by malicious sites. If a track is
+ * re-enabled before the delay has passed, the device will not be touched
+ * until another disable followed by the full delay happens.
+ */
+ void SetDeviceEnabled(bool aEnabled);
+
+ /**
+ * Posts a task to set the muted state of the device associated with this
+ * DeviceListener to aMuted and notifies the associated window listener that a
+ * track's state has changed.
+ *
+ * Turning the hardware off while the device is muted is supported for:
+ * - Camera (enabled by default, controlled by pref
+ * "media.getusermedia.camera.off_while_disabled.enabled")
+ * - Microphone (disabled by default, controlled by pref
+ * "media.getusermedia.microphone.off_while_disabled.enabled")
+ * Screen-, app-, or windowsharing is not supported at this time.
+ */
+ void SetDeviceMuted(bool aMuted);
+
+ /**
+ * Mutes or unmutes the associated video device if it is a camera.
+ */
+ void MuteOrUnmuteCamera(bool aMute);
+ void MuteOrUnmuteMicrophone(bool aMute);
+
+ LocalMediaDevice* GetDevice() const {
+ return mDeviceState ? mDeviceState->mDevice.get() : nullptr;
+ }
+
+ bool Activated() const { return static_cast<bool>(mDeviceState); }
+
+ bool Stopped() const { return mStopped; }
+
+ bool CapturingVideo() const;
+
+ bool CapturingAudio() const;
+
+ CaptureState CapturingSource(MediaSourceEnum aSource) const;
+
+ RefPtr<DeviceListenerPromise> ApplyConstraints(
+ const MediaTrackConstraints& aConstraints, CallerType aCallerType);
+
+ PrincipalHandle GetPrincipalHandle() const;
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t amount = aMallocSizeOf(this);
+ // Assume mPrincipalHandle refers to a principal owned elsewhere.
+ // DeviceState does not have support for memory accounting.
+ return amount;
+ }
+
+ private:
+ virtual ~DeviceListener() {
+ MOZ_ASSERT(mStopped);
+ MOZ_ASSERT(!mWindowListener);
+ }
+
+ using DeviceOperationPromise =
+ MozPromise<nsresult, bool, /* IsExclusive = */ true>;
+
+ /**
+ * Posts a task to start or stop the device associated with aTrack, based on
+ * a passed-in boolean. Private method used by SetDeviceEnabled and
+ * SetDeviceMuted.
+ */
+ RefPtr<DeviceOperationPromise> UpdateDevice(bool aOn);
+
+ // true after this listener has had all devices stopped. MainThread only.
+ bool mStopped;
+
+ // never ever indirect off this; just for assertions
+ PRThread* mMainThreadCheck;
+
+ // Set in Register() on main thread, then read from any thread.
+ PrincipalHandle mPrincipalHandle;
+
+ // Weak pointer to the window listener that owns us. MainThread only.
+ GetUserMediaWindowListener* mWindowListener;
+
+ // Accessed from MediaTrackGraph thread, MediaManager thread, and MainThread
+ // No locking needed as it's set on Activate() and never assigned to again.
+ UniquePtr<DeviceState> mDeviceState;
+};
+
+/**
+ * This class represents a WindowID and handles all MediaTrackListeners
+ * (here subclassed as DeviceListeners) used to feed GetUserMedia tracks.
+ * It proxies feedback from them into messages for browser chrome.
+ * The DeviceListeners are used to Start() and Stop() the underlying
+ * MediaEngineSource when MediaStreams are assigned and deassigned in content.
+ */
+class GetUserMediaWindowListener {
+ friend MediaManager;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GetUserMediaWindowListener)
+
+ // Create in an inactive state
+ GetUserMediaWindowListener(uint64_t aWindowID,
+ const PrincipalHandle& aPrincipalHandle)
+ : mWindowID(aWindowID),
+ mPrincipalHandle(aPrincipalHandle),
+ mChromeNotificationTaskPosted(false) {}
+
+ /**
+ * Registers an inactive gUM device listener for this WindowListener.
+ */
+ void Register(RefPtr<DeviceListener> aListener) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aListener);
+ MOZ_ASSERT(!aListener->Activated());
+ MOZ_ASSERT(!mInactiveListeners.Contains(aListener), "Already registered");
+ MOZ_ASSERT(!mActiveListeners.Contains(aListener), "Already activated");
+
+ aListener->Register(this);
+ mInactiveListeners.AppendElement(std::move(aListener));
+ }
+
+ /**
+ * Activates an already registered and inactive gUM device listener for this
+ * WindowListener.
+ */
+ void Activate(RefPtr<DeviceListener> aListener,
+ RefPtr<LocalMediaDevice> aDevice,
+ RefPtr<LocalTrackSource> aTrackSource) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aListener);
+ MOZ_ASSERT(!aListener->Activated());
+ MOZ_ASSERT(mInactiveListeners.Contains(aListener),
+ "Must be registered to activate");
+ MOZ_ASSERT(!mActiveListeners.Contains(aListener), "Already activated");
+
+ bool muted = false;
+ if (aDevice->Kind() == MediaDeviceKind::Videoinput) {
+ muted = mCamerasAreMuted;
+ } else if (aDevice->Kind() == MediaDeviceKind::Audioinput) {
+ muted = mMicrophonesAreMuted;
+ } else {
+ MOZ_CRASH("Unexpected device kind");
+ }
+
+ mInactiveListeners.RemoveElement(aListener);
+ aListener->Activate(std::move(aDevice), std::move(aTrackSource), muted);
+ mActiveListeners.AppendElement(std::move(aListener));
+ }
+
+ /**
+ * Removes all DeviceListeners from this window listener.
+ * Removes this window listener from the list of active windows, so callers
+ * need to make sure to hold a strong reference.
+ */
+ void RemoveAll() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ for (auto& l : mInactiveListeners.Clone()) {
+ Remove(l);
+ }
+ for (auto& l : mActiveListeners.Clone()) {
+ Remove(l);
+ }
+ MOZ_ASSERT(mInactiveListeners.Length() == 0);
+ MOZ_ASSERT(mActiveListeners.Length() == 0);
+
+ MediaManager* mgr = MediaManager::GetIfExists();
+ if (!mgr) {
+ MOZ_ASSERT(false, "MediaManager should stay until everything is removed");
+ return;
+ }
+ GetUserMediaWindowListener* windowListener =
+ mgr->GetWindowListener(mWindowID);
+
+ if (!windowListener) {
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ auto* globalWindow = nsGlobalWindowInner::GetInnerWindowWithId(mWindowID);
+ if (globalWindow) {
+ auto req = MakeRefPtr<GetUserMediaRequest>(
+ globalWindow, VoidString(), VoidString(),
+ UserActivation::IsHandlingUserInput());
+ obs->NotifyWhenScriptSafe(req, "recording-device-stopped", nullptr);
+ }
+ return;
+ }
+
+ MOZ_ASSERT(windowListener == this,
+ "There should only be one window listener per window ID");
+
+ LOG("GUMWindowListener %p removing windowID %" PRIu64, this, mWindowID);
+ mgr->RemoveWindowID(mWindowID);
+ }
+
+ /**
+ * Removes a listener from our lists. Safe to call without holding a hard
+ * reference. That said, you'll still want to iterate on a copy of said lists,
+ * if you end up calling this method (or methods that may call this method) in
+ * the loop, to avoid inadvertently skipping members.
+ *
+ * For use only from GetUserMediaWindowListener and DeviceListener.
+ */
+ bool Remove(RefPtr<DeviceListener> aListener) {
+ // We refcount aListener on entry since we're going to proxy-release it
+ // below to prevent the refcount going to zero on callers who might be
+ // inside the listener, but operating without a hard reference to self.
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mInactiveListeners.RemoveElement(aListener) &&
+ !mActiveListeners.RemoveElement(aListener)) {
+ return false;
+ }
+ MOZ_ASSERT(!mInactiveListeners.Contains(aListener),
+ "A DeviceListener should only be once in one of "
+ "mInactiveListeners and mActiveListeners");
+ MOZ_ASSERT(!mActiveListeners.Contains(aListener),
+ "A DeviceListener should only be once in one of "
+ "mInactiveListeners and mActiveListeners");
+
+ LOG("GUMWindowListener %p stopping DeviceListener %p.", this,
+ aListener.get());
+ aListener->Stop();
+
+ if (LocalMediaDevice* removedDevice = aListener->GetDevice()) {
+ bool revokePermission = true;
+ nsString removedRawId;
+ nsString removedSourceType;
+ removedDevice->GetRawId(removedRawId);
+ removedDevice->GetMediaSource(removedSourceType);
+
+ for (const auto& l : mActiveListeners) {
+ if (LocalMediaDevice* device = l->GetDevice()) {
+ nsString rawId;
+ device->GetRawId(rawId);
+ if (removedRawId.Equals(rawId)) {
+ revokePermission = false;
+ break;
+ }
+ }
+ }
+
+ if (revokePermission) {
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ auto* window = nsGlobalWindowInner::GetInnerWindowWithId(mWindowID);
+ auto req = MakeRefPtr<GetUserMediaRequest>(
+ window, removedRawId, removedSourceType,
+ UserActivation::IsHandlingUserInput());
+ obs->NotifyWhenScriptSafe(req, "recording-device-stopped", nullptr);
+ }
+ }
+
+ if (mInactiveListeners.Length() == 0 && mActiveListeners.Length() == 0) {
+ LOG("GUMWindowListener %p Removed last DeviceListener. Cleaning up.",
+ this);
+ RemoveAll();
+ }
+
+ nsCOMPtr<nsIEventTarget> mainTarget = do_GetMainThread();
+ // To allow being invoked by callers not holding a strong reference to self,
+ // hold the listener alive until the stack has unwound, by always
+ // dispatching a runnable (aAlwaysProxy = true)
+ NS_ProxyRelease(__func__, mainTarget, aListener.forget(), true);
+ return true;
+ }
+
+ /**
+ * Stops all screen/window/audioCapture sharing, but not camera or microphone.
+ */
+ void StopSharing();
+
+ void StopRawID(const nsString& removedDeviceID);
+
+ void MuteOrUnmuteCameras(bool aMute);
+ void MuteOrUnmuteMicrophones(bool aMute);
+
+ /**
+ * Called by one of our DeviceListeners when one of its tracks has changed so
+ * that chrome state is affected.
+ * Schedules an event for the next stable state to update chrome.
+ */
+ void ChromeAffectingStateChanged();
+
+ /**
+ * Called in stable state to send a notification to update chrome.
+ */
+ void NotifyChrome();
+
+ bool CapturingVideo() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ for (auto& l : mActiveListeners) {
+ if (l->CapturingVideo()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ bool CapturingAudio() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ for (auto& l : mActiveListeners) {
+ if (l->CapturingAudio()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ CaptureState CapturingSource(MediaSourceEnum aSource) const {
+ MOZ_ASSERT(NS_IsMainThread());
+ CaptureState result = CaptureState::Off;
+ for (auto& l : mActiveListeners) {
+ result = CombineCaptureState(result, l->CapturingSource(aSource));
+ }
+ return result;
+ }
+
+ RefPtr<LocalMediaDeviceSetRefCnt> GetDevices() {
+ RefPtr devices = new LocalMediaDeviceSetRefCnt();
+ for (auto& l : mActiveListeners) {
+ devices->AppendElement(l->GetDevice());
+ }
+ return devices;
+ }
+
+ uint64_t WindowID() const { return mWindowID; }
+
+ PrincipalHandle GetPrincipalHandle() const { return mPrincipalHandle; }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t amount = aMallocSizeOf(this);
+ // Assume mPrincipalHandle refers to a principal owned elsewhere.
+ amount += mInactiveListeners.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (const RefPtr<DeviceListener>& listener : mInactiveListeners) {
+ amount += listener->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ amount += mActiveListeners.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (const RefPtr<DeviceListener>& listener : mActiveListeners) {
+ amount += listener->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ return amount;
+ }
+
+ private:
+ ~GetUserMediaWindowListener() {
+ MOZ_ASSERT(mInactiveListeners.Length() == 0,
+ "Inactive listeners should already be removed");
+ MOZ_ASSERT(mActiveListeners.Length() == 0,
+ "Active listeners should already be removed");
+ }
+
+ uint64_t mWindowID;
+ const PrincipalHandle mPrincipalHandle;
+
+ // true if we have scheduled a task to notify chrome in the next stable state.
+ // The task will reset this to false. MainThread only.
+ bool mChromeNotificationTaskPosted;
+
+ nsTArray<RefPtr<DeviceListener>> mInactiveListeners;
+ nsTArray<RefPtr<DeviceListener>> mActiveListeners;
+
+ // Whether camera and microphone access in this window are currently
+ // User Agent (UA) muted. When true, new and cloned tracks must start
+ // out muted, to avoid JS circumventing UA mute. Per-camera and
+ // per-microphone UA muting is not supported.
+ bool mCamerasAreMuted = false;
+ bool mMicrophonesAreMuted = false;
+};
+
+class LocalTrackSource : public MediaStreamTrackSource {
+ public:
+ LocalTrackSource(nsIPrincipal* aPrincipal, const nsString& aLabel,
+ const RefPtr<DeviceListener>& aListener,
+ MediaSourceEnum aSource, MediaTrack* aTrack,
+ RefPtr<PeerIdentity> aPeerIdentity,
+ TrackingId aTrackingId = TrackingId())
+ : MediaStreamTrackSource(aPrincipal, aLabel, std::move(aTrackingId)),
+ mSource(aSource),
+ mTrack(aTrack),
+ mPeerIdentity(std::move(aPeerIdentity)),
+ mListener(aListener.get()) {}
+
+ MediaSourceEnum GetMediaSource() const override { return mSource; }
+
+ const PeerIdentity* GetPeerIdentity() const override { return mPeerIdentity; }
+
+ RefPtr<MediaStreamTrackSource::ApplyConstraintsPromise> ApplyConstraints(
+ const MediaTrackConstraints& aConstraints,
+ CallerType aCallerType) override {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (sHasMainThreadShutdown || !mListener) {
+ // Track has been stopped, or we are in shutdown. In either case
+ // there's no observable outcome, so pretend we succeeded.
+ return MediaStreamTrackSource::ApplyConstraintsPromise::CreateAndResolve(
+ false, __func__);
+ }
+ return mListener->ApplyConstraints(aConstraints, aCallerType);
+ }
+
+ void GetSettings(MediaTrackSettings& aOutSettings) override {
+ if (mListener) {
+ mListener->GetSettings(aOutSettings);
+ }
+ }
+
+ void Stop() override {
+ if (mListener) {
+ mListener->Stop();
+ mListener = nullptr;
+ }
+ if (!mTrack->IsDestroyed()) {
+ mTrack->Destroy();
+ }
+ }
+
+ void Disable() override {
+ if (mListener) {
+ mListener->SetDeviceEnabled(false);
+ }
+ }
+
+ void Enable() override {
+ if (mListener) {
+ mListener->SetDeviceEnabled(true);
+ }
+ }
+
+ void Mute() {
+ MutedChanged(true);
+ mTrack->SetDisabledTrackMode(DisabledTrackMode::SILENCE_BLACK);
+ }
+
+ void Unmute() {
+ MutedChanged(false);
+ mTrack->SetDisabledTrackMode(DisabledTrackMode::ENABLED);
+ }
+
+ const MediaSourceEnum mSource;
+ const RefPtr<MediaTrack> mTrack;
+ const RefPtr<const PeerIdentity> mPeerIdentity;
+
+ protected:
+ ~LocalTrackSource() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mTrack->IsDestroyed());
+ }
+
+ // This is a weak pointer to avoid having the DeviceListener (which may
+ // have references to threads and threadpools) kept alive by DOM-objects
+ // that may have ref-cycles and thus are released very late during
+ // shutdown, even after xpcom-shutdown-threads. See bug 1351655 for what
+ // can happen.
+ WeakPtr<DeviceListener> mListener;
+};
+
+class AudioCaptureTrackSource : public LocalTrackSource {
+ public:
+ AudioCaptureTrackSource(nsIPrincipal* aPrincipal, nsPIDOMWindowInner* aWindow,
+ const nsString& aLabel,
+ AudioCaptureTrack* aAudioCaptureTrack,
+ RefPtr<PeerIdentity> aPeerIdentity)
+ : LocalTrackSource(aPrincipal, aLabel, nullptr,
+ MediaSourceEnum::AudioCapture, aAudioCaptureTrack,
+ std::move(aPeerIdentity)),
+ mWindow(aWindow),
+ mAudioCaptureTrack(aAudioCaptureTrack) {
+ mAudioCaptureTrack->Start();
+ mAudioCaptureTrack->Graph()->RegisterCaptureTrackForWindow(
+ mWindow->WindowID(), mAudioCaptureTrack);
+ mWindow->SetAudioCapture(true);
+ }
+
+ void Stop() override {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!mAudioCaptureTrack->IsDestroyed()) {
+ MOZ_ASSERT(mWindow);
+ mWindow->SetAudioCapture(false);
+ mAudioCaptureTrack->Graph()->UnregisterCaptureTrackForWindow(
+ mWindow->WindowID());
+ mWindow = nullptr;
+ }
+ // LocalTrackSource destroys the track.
+ LocalTrackSource::Stop();
+ MOZ_ASSERT(mAudioCaptureTrack->IsDestroyed());
+ }
+
+ ProcessedMediaTrack* InputTrack() const { return mAudioCaptureTrack.get(); }
+
+ protected:
+ ~AudioCaptureTrackSource() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mAudioCaptureTrack->IsDestroyed());
+ }
+
+ RefPtr<nsPIDOMWindowInner> mWindow;
+ const RefPtr<AudioCaptureTrack> mAudioCaptureTrack;
+};
+
+/**
+ * nsIMediaDevice implementation.
+ */
+NS_IMPL_ISUPPORTS(LocalMediaDevice, nsIMediaDevice)
+
+MediaDevice::MediaDevice(MediaEngine* aEngine, MediaSourceEnum aMediaSource,
+ const nsString& aRawName, const nsString& aRawID,
+ const nsString& aRawGroupID, IsScary aIsScary,
+ const OsPromptable canRequestOsLevelPrompt)
+ : mEngine(aEngine),
+ mAudioDeviceInfo(nullptr),
+ mMediaSource(aMediaSource),
+ mKind(MediaEngineSource::IsVideo(aMediaSource)
+ ? MediaDeviceKind::Videoinput
+ : MediaDeviceKind::Audioinput),
+ mScary(aIsScary == IsScary::Yes),
+ mCanRequestOsLevelPrompt(canRequestOsLevelPrompt == OsPromptable::Yes),
+ mIsFake(mEngine->IsFake()),
+ mType(
+ NS_ConvertASCIItoUTF16(dom::MediaDeviceKindValues::GetString(mKind))),
+ mRawID(aRawID),
+ mRawGroupID(aRawGroupID),
+ mRawName(aRawName) {
+ MOZ_ASSERT(mEngine);
+}
+
+MediaDevice::MediaDevice(MediaEngine* aEngine,
+ const RefPtr<AudioDeviceInfo>& aAudioDeviceInfo,
+ const nsString& aRawID)
+ : mEngine(aEngine),
+ mAudioDeviceInfo(aAudioDeviceInfo),
+ mMediaSource(mAudioDeviceInfo->Type() == AudioDeviceInfo::TYPE_INPUT
+ ? MediaSourceEnum::Microphone
+ : MediaSourceEnum::Other),
+ mKind(mMediaSource == MediaSourceEnum::Microphone
+ ? MediaDeviceKind::Audioinput
+ : MediaDeviceKind::Audiooutput),
+ mScary(false),
+ mCanRequestOsLevelPrompt(false),
+ mIsFake(false),
+ mType(
+ NS_ConvertASCIItoUTF16(dom::MediaDeviceKindValues::GetString(mKind))),
+ mRawID(aRawID),
+ mRawGroupID(mAudioDeviceInfo->GroupID()),
+ mRawName(mAudioDeviceInfo->Name()) {}
+
+/* static */
+RefPtr<MediaDevice> MediaDevice::CopyWithNewRawGroupId(
+ const RefPtr<MediaDevice>& aOther, const nsString& aRawGroupID) {
+ MOZ_ASSERT(!aOther->mAudioDeviceInfo, "device not supported");
+ return new MediaDevice(aOther->mEngine, aOther->mMediaSource,
+ aOther->mRawName, aOther->mRawID, aRawGroupID,
+ IsScary(aOther->mScary),
+ OsPromptable(aOther->mCanRequestOsLevelPrompt));
+}
+
+MediaDevice::~MediaDevice() = default;
+
+LocalMediaDevice::LocalMediaDevice(RefPtr<const MediaDevice> aRawDevice,
+ const nsString& aID,
+ const nsString& aGroupID,
+ const nsString& aName)
+ : mRawDevice(std::move(aRawDevice)),
+ mName(aName),
+ mID(aID),
+ mGroupID(aGroupID) {
+ MOZ_ASSERT(mRawDevice);
+}
+
+/**
+ * Helper functions that implement the constraints algorithm from
+ * http://dev.w3.org/2011/webrtc/editor/getusermedia.html#methods-5
+ */
+
+/* static */
+bool LocalMediaDevice::StringsContain(
+ const OwningStringOrStringSequence& aStrings, nsString aN) {
+ return aStrings.IsString() ? aStrings.GetAsString() == aN
+ : aStrings.GetAsStringSequence().Contains(aN);
+}
+
+/* static */
+uint32_t LocalMediaDevice::FitnessDistance(
+ nsString aN, const ConstrainDOMStringParameters& aParams) {
+ if (aParams.mExact.WasPassed() &&
+ !StringsContain(aParams.mExact.Value(), aN)) {
+ return UINT32_MAX;
+ }
+ if (aParams.mIdeal.WasPassed() &&
+ !StringsContain(aParams.mIdeal.Value(), aN)) {
+ return 1;
+ }
+ return 0;
+}
+
+// Binding code doesn't templatize well...
+
+/* static */
+uint32_t LocalMediaDevice::FitnessDistance(
+ nsString aN,
+ const OwningStringOrStringSequenceOrConstrainDOMStringParameters&
+ aConstraint) {
+ if (aConstraint.IsString()) {
+ ConstrainDOMStringParameters params;
+ params.mIdeal.Construct();
+ params.mIdeal.Value().SetAsString() = aConstraint.GetAsString();
+ return FitnessDistance(aN, params);
+ } else if (aConstraint.IsStringSequence()) {
+ ConstrainDOMStringParameters params;
+ params.mIdeal.Construct();
+ params.mIdeal.Value().SetAsStringSequence() =
+ aConstraint.GetAsStringSequence();
+ return FitnessDistance(aN, params);
+ } else {
+ return FitnessDistance(aN, aConstraint.GetAsConstrainDOMStringParameters());
+ }
+}
+
+uint32_t LocalMediaDevice::GetBestFitnessDistance(
+ const nsTArray<const NormalizedConstraintSet*>& aConstraintSets,
+ CallerType aCallerType) {
+ MOZ_ASSERT(MediaManager::IsInMediaThread());
+ MOZ_ASSERT(GetMediaSource() != MediaSourceEnum::Other);
+
+ bool isChrome = aCallerType == CallerType::System;
+ const nsString& id = isChrome ? RawID() : mID;
+ auto type = GetMediaSource();
+ uint64_t distance = 0;
+ if (!aConstraintSets.IsEmpty()) {
+ if (isChrome /* For the screen/window sharing preview */ ||
+ type == MediaSourceEnum::Camera ||
+ type == MediaSourceEnum::Microphone) {
+ distance += uint64_t(MediaConstraintsHelper::FitnessDistance(
+ Some(id), aConstraintSets[0]->mDeviceId)) +
+ uint64_t(MediaConstraintsHelper::FitnessDistance(
+ Some(mGroupID), aConstraintSets[0]->mGroupId));
+ }
+ }
+ if (distance < UINT32_MAX) {
+ // Forward request to underlying object to interrogate per-mode
+ // capabilities.
+ distance += Source()->GetBestFitnessDistance(aConstraintSets);
+ }
+ return std::min<uint64_t>(distance, UINT32_MAX);
+}
+
+NS_IMETHODIMP
+LocalMediaDevice::GetRawName(nsAString& aName) {
+ MOZ_ASSERT(NS_IsMainThread());
+ aName.Assign(mRawDevice->mRawName);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LocalMediaDevice::GetType(nsAString& aType) {
+ MOZ_ASSERT(NS_IsMainThread());
+ aType.Assign(mRawDevice->mType);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LocalMediaDevice::GetRawId(nsAString& aID) {
+ MOZ_ASSERT(NS_IsMainThread());
+ aID.Assign(RawID());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LocalMediaDevice::GetId(nsAString& aID) {
+ MOZ_ASSERT(NS_IsMainThread());
+ aID.Assign(mID);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LocalMediaDevice::GetScary(bool* aScary) {
+ *aScary = mRawDevice->mScary;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+LocalMediaDevice::GetCanRequestOsLevelPrompt(bool* aCanRequestOsLevelPrompt) {
+ *aCanRequestOsLevelPrompt = mRawDevice->mCanRequestOsLevelPrompt;
+ return NS_OK;
+}
+
+void LocalMediaDevice::GetSettings(MediaTrackSettings& aOutSettings) {
+ MOZ_ASSERT(NS_IsMainThread());
+ Source()->GetSettings(aOutSettings);
+}
+
+MediaEngineSource* LocalMediaDevice::Source() {
+ if (!mSource) {
+ mSource = mRawDevice->mEngine->CreateSource(mRawDevice);
+ }
+ return mSource;
+}
+
+const TrackingId& LocalMediaDevice::GetTrackingId() const {
+ return mSource->GetTrackingId();
+}
+
+// Threadsafe since mKind and mSource are const.
+NS_IMETHODIMP
+LocalMediaDevice::GetMediaSource(nsAString& aMediaSource) {
+ if (Kind() == MediaDeviceKind::Audiooutput) {
+ aMediaSource.Truncate();
+ } else {
+ aMediaSource.AssignASCII(
+ dom::MediaSourceEnumValues::GetString(GetMediaSource()));
+ }
+ return NS_OK;
+}
+
+nsresult LocalMediaDevice::Allocate(const MediaTrackConstraints& aConstraints,
+ const MediaEnginePrefs& aPrefs,
+ uint64_t aWindowID,
+ const char** aOutBadConstraint) {
+ MOZ_ASSERT(MediaManager::IsInMediaThread());
+
+ // Mock failure for automated tests.
+ if (IsFake() && aConstraints.mDeviceId.WasPassed() &&
+ aConstraints.mDeviceId.Value().IsString() &&
+ aConstraints.mDeviceId.Value().GetAsString().EqualsASCII("bad device")) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return Source()->Allocate(aConstraints, aPrefs, aWindowID, aOutBadConstraint);
+}
+
+void LocalMediaDevice::SetTrack(const RefPtr<MediaTrack>& aTrack,
+ const PrincipalHandle& aPrincipalHandle) {
+ MOZ_ASSERT(MediaManager::IsInMediaThread());
+ Source()->SetTrack(aTrack, aPrincipalHandle);
+}
+
+nsresult LocalMediaDevice::Start() {
+ MOZ_ASSERT(MediaManager::IsInMediaThread());
+ MOZ_ASSERT(Source());
+ return Source()->Start();
+}
+
+nsresult LocalMediaDevice::Reconfigure(
+ const MediaTrackConstraints& aConstraints, const MediaEnginePrefs& aPrefs,
+ const char** aOutBadConstraint) {
+ MOZ_ASSERT(MediaManager::IsInMediaThread());
+ auto type = GetMediaSource();
+ if (type == MediaSourceEnum::Camera || type == MediaSourceEnum::Microphone) {
+ NormalizedConstraints c(aConstraints);
+ if (MediaConstraintsHelper::FitnessDistance(Some(mID), c.mDeviceId) ==
+ UINT32_MAX) {
+ *aOutBadConstraint = "deviceId";
+ return NS_ERROR_INVALID_ARG;
+ }
+ if (MediaConstraintsHelper::FitnessDistance(Some(mGroupID), c.mGroupId) ==
+ UINT32_MAX) {
+ *aOutBadConstraint = "groupId";
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+ return Source()->Reconfigure(aConstraints, aPrefs, aOutBadConstraint);
+}
+
+nsresult LocalMediaDevice::FocusOnSelectedSource() {
+ MOZ_ASSERT(MediaManager::IsInMediaThread());
+ return Source()->FocusOnSelectedSource();
+}
+
+nsresult LocalMediaDevice::Stop() {
+ MOZ_ASSERT(MediaManager::IsInMediaThread());
+ MOZ_ASSERT(mSource);
+ return mSource->Stop();
+}
+
+nsresult LocalMediaDevice::Deallocate() {
+ MOZ_ASSERT(MediaManager::IsInMediaThread());
+ MOZ_ASSERT(mSource);
+ return mSource->Deallocate();
+}
+
+MediaSourceEnum MediaDevice::GetMediaSource() const { return mMediaSource; }
+
+static const MediaTrackConstraints& GetInvariant(
+ const OwningBooleanOrMediaTrackConstraints& aUnion) {
+ static const MediaTrackConstraints empty;
+ return aUnion.IsMediaTrackConstraints() ? aUnion.GetAsMediaTrackConstraints()
+ : empty;
+}
+
+// Source getter returning full list
+
+static void GetMediaDevices(MediaEngine* aEngine, MediaSourceEnum aSrcType,
+ MediaManager::MediaDeviceSet& aResult,
+ const char* aMediaDeviceName = nullptr) {
+ MOZ_ASSERT(MediaManager::IsInMediaThread());
+
+ LOG("%s: aEngine=%p, aSrcType=%" PRIu8 ", aMediaDeviceName=%s", __func__,
+ aEngine, static_cast<uint8_t>(aSrcType),
+ aMediaDeviceName ? aMediaDeviceName : "null");
+ nsTArray<RefPtr<MediaDevice>> devices;
+ aEngine->EnumerateDevices(aSrcType, MediaSinkEnum::Other, &devices);
+
+ /*
+ * We're allowing multiple tabs to access the same camera for parity
+ * with Chrome. See bug 811757 for some of the issues surrounding
+ * this decision. To disallow, we'd filter by IsAvailable() as we used
+ * to.
+ */
+ if (aMediaDeviceName && *aMediaDeviceName) {
+ for (auto& device : devices) {
+ if (device->mRawName.EqualsASCII(aMediaDeviceName)) {
+ aResult.AppendElement(device);
+ LOG("%s: found aMediaDeviceName=%s", __func__, aMediaDeviceName);
+ break;
+ }
+ }
+ } else {
+ aResult = std::move(devices);
+ if (MOZ_LOG_TEST(gMediaManagerLog, mozilla::LogLevel::Debug)) {
+ for (auto& device : aResult) {
+ LOG("%s: appending device=%s", __func__,
+ NS_ConvertUTF16toUTF8(device->mRawName).get());
+ }
+ }
+ }
+}
+
+RefPtr<LocalDeviceSetPromise> MediaManager::SelectSettings(
+ const MediaStreamConstraints& aConstraints, CallerType aCallerType,
+ RefPtr<LocalMediaDeviceSetRefCnt> aDevices) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Algorithm accesses device capabilities code and must run on media thread.
+ // Modifies passed-in aDevices.
+
+ return MediaManager::Dispatch<LocalDeviceSetPromise>(
+ __func__, [aConstraints, devices = std::move(aDevices),
+ aCallerType](MozPromiseHolder<LocalDeviceSetPromise>& holder) {
+ auto& devicesRef = *devices;
+
+ // Since the advanced part of the constraints algorithm needs to know
+ // when a candidate set is overconstrained (zero members), we must split
+ // up the list into videos and audios, and put it back together again at
+ // the end.
+
+ nsTArray<RefPtr<LocalMediaDevice>> videos;
+ nsTArray<RefPtr<LocalMediaDevice>> audios;
+
+ for (const auto& device : devicesRef) {
+ MOZ_ASSERT(device->Kind() == MediaDeviceKind::Videoinput ||
+ device->Kind() == MediaDeviceKind::Audioinput);
+ if (device->Kind() == MediaDeviceKind::Videoinput) {
+ videos.AppendElement(device);
+ } else if (device->Kind() == MediaDeviceKind::Audioinput) {
+ audios.AppendElement(device);
+ }
+ }
+ devicesRef.Clear();
+ const char* badConstraint = nullptr;
+ bool needVideo = IsOn(aConstraints.mVideo);
+ bool needAudio = IsOn(aConstraints.mAudio);
+
+ if (needVideo && videos.Length()) {
+ badConstraint = MediaConstraintsHelper::SelectSettings(
+ NormalizedConstraints(GetInvariant(aConstraints.mVideo)), videos,
+ aCallerType);
+ }
+ if (!badConstraint && needAudio && audios.Length()) {
+ badConstraint = MediaConstraintsHelper::SelectSettings(
+ NormalizedConstraints(GetInvariant(aConstraints.mAudio)), audios,
+ aCallerType);
+ }
+ if (badConstraint) {
+ LOG("SelectSettings: bad constraint found! Calling error handler!");
+ nsString constraint;
+ constraint.AssignASCII(badConstraint);
+ holder.Reject(
+ new MediaMgrError(MediaMgrError::Name::OverconstrainedError, "",
+ constraint),
+ __func__);
+ return;
+ }
+ if (!needVideo == !videos.Length() && !needAudio == !audios.Length()) {
+ for (auto& video : videos) {
+ devicesRef.AppendElement(video);
+ }
+ for (auto& audio : audios) {
+ devicesRef.AppendElement(audio);
+ }
+ }
+ holder.Resolve(devices, __func__);
+ });
+}
+
+/**
+ * Describes a requested task that handles response from the UI and sends
+ * results back to the DOM.
+ */
+class GetUserMediaTask {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GetUserMediaTask)
+ GetUserMediaTask(uint64_t aWindowID, const ipc::PrincipalInfo& aPrincipalInfo,
+ CallerType aCallerType)
+ : mPrincipalInfo(aPrincipalInfo),
+ mWindowID(aWindowID),
+ mCallerType(aCallerType) {}
+
+ virtual void Denied(MediaMgrError::Name aName,
+ const nsCString& aMessage = ""_ns) = 0;
+
+ virtual GetUserMediaStreamTask* AsGetUserMediaStreamTask() { return nullptr; }
+ virtual SelectAudioOutputTask* AsSelectAudioOutputTask() { return nullptr; }
+
+ uint64_t GetWindowID() const { return mWindowID; }
+ enum CallerType CallerType() const { return mCallerType; }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t amount = aMallocSizeOf(this);
+ // Assume mWindowListener is owned by MediaManager.
+ // Assume mAudioDeviceListener and mVideoDeviceListener are owned by
+ // mWindowListener.
+ // Assume PrincipalInfo string buffers are shared.
+ // Member types without support for accounting of pointees:
+ // MozPromiseHolder, RefPtr<LocalMediaDevice>.
+ // We don't have a good way to account for lambda captures for MozPromise
+ // callbacks.
+ return amount;
+ }
+
+ protected:
+ virtual ~GetUserMediaTask() = default;
+
+ // Call GetPrincipalKey again, if not private browing, this time with
+ // persist = true, to promote deviceIds to persistent, in case they're not
+ // already. Fire'n'forget.
+ void PersistPrincipalKey() {
+ if (IsPrincipalInfoPrivate(mPrincipalInfo)) {
+ return;
+ }
+ media::GetPrincipalKey(mPrincipalInfo, true)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [](const media::PrincipalKeyPromise::ResolveOrRejectValue& aValue) {
+ if (aValue.IsReject()) {
+ LOG("Failed get Principal key. Persisting of deviceIds "
+ "will be broken");
+ }
+ });
+ }
+
+ private:
+ // Thread-safe (object) principal of Window with ID mWindowID
+ const ipc::PrincipalInfo mPrincipalInfo;
+
+ protected:
+ // The ID of the not-necessarily-toplevel inner Window relevant global
+ // object of the MediaDevices on which getUserMedia() was called
+ const uint64_t mWindowID;
+ // Whether the JS caller of getUserMedia() has system (subject) principal
+ const enum CallerType mCallerType;
+};
+
+/**
+ * Describes a requested task that handles response from the UI to a
+ * getUserMedia() request and sends results back to content. If the request
+ * is allowed and device initialization succeeds, then the MozPromise is
+ * resolved with a DOMMediaStream having a track or tracks for the approved
+ * device or devices.
+ */
+class GetUserMediaStreamTask final : public GetUserMediaTask {
+ public:
+ GetUserMediaStreamTask(
+ const MediaStreamConstraints& aConstraints,
+ MozPromiseHolder<MediaManager::StreamPromise>&& aHolder,
+ uint64_t aWindowID, RefPtr<GetUserMediaWindowListener> aWindowListener,
+ RefPtr<DeviceListener> aAudioDeviceListener,
+ RefPtr<DeviceListener> aVideoDeviceListener,
+ const MediaEnginePrefs& aPrefs, const ipc::PrincipalInfo& aPrincipalInfo,
+ enum CallerType aCallerType, bool aShouldFocusSource)
+ : GetUserMediaTask(aWindowID, aPrincipalInfo, aCallerType),
+ mConstraints(aConstraints),
+ mHolder(std::move(aHolder)),
+ mWindowListener(std::move(aWindowListener)),
+ mAudioDeviceListener(std::move(aAudioDeviceListener)),
+ mVideoDeviceListener(std::move(aVideoDeviceListener)),
+ mPrefs(aPrefs),
+ mShouldFocusSource(aShouldFocusSource),
+ mManager(MediaManager::GetInstance()) {}
+
+ void Allowed(RefPtr<LocalMediaDevice> aAudioDevice,
+ RefPtr<LocalMediaDevice> aVideoDevice) {
+ MOZ_ASSERT(aAudioDevice || aVideoDevice);
+ mAudioDevice = std::move(aAudioDevice);
+ mVideoDevice = std::move(aVideoDevice);
+ // Reuse the same thread to save memory.
+ MediaManager::Dispatch(
+ NewRunnableMethod("GetUserMediaStreamTask::AllocateDevices", this,
+ &GetUserMediaStreamTask::AllocateDevices));
+ }
+
+ GetUserMediaStreamTask* AsGetUserMediaStreamTask() override { return this; }
+
+ private:
+ ~GetUserMediaStreamTask() override {
+ if (!mHolder.IsEmpty()) {
+ Fail(MediaMgrError::Name::NotAllowedError);
+ }
+ }
+
+ void Fail(MediaMgrError::Name aName, const nsCString& aMessage = ""_ns,
+ const nsString& aConstraint = u""_ns) {
+ mHolder.Reject(MakeRefPtr<MediaMgrError>(aName, aMessage, aConstraint),
+ __func__);
+ // We add a disabled listener to the StreamListeners array until accepted
+ // If this was the only active MediaStream, remove the window from the list.
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "DeviceListener::Stop",
+ [audio = mAudioDeviceListener, video = mVideoDeviceListener] {
+ if (audio) {
+ audio->Stop();
+ }
+ if (video) {
+ video->Stop();
+ }
+ }));
+ }
+
+ /**
+ * Runs on a separate thread and is responsible for allocating devices.
+ *
+ * Do not run this on the main thread.
+ */
+ void AllocateDevices() {
+ MOZ_ASSERT(!NS_IsMainThread());
+ LOG("GetUserMediaStreamTask::AllocateDevices()");
+
+ // Allocate a video or audio device and return a MediaStream via
+ // PrepareDOMStream().
+
+ nsresult rv;
+ const char* errorMsg = nullptr;
+ const char* badConstraint = nullptr;
+
+ if (mAudioDevice) {
+ auto& constraints = GetInvariant(mConstraints.mAudio);
+ rv = mAudioDevice->Allocate(constraints, mPrefs, mWindowID,
+ &badConstraint);
+ if (NS_FAILED(rv)) {
+ errorMsg = "Failed to allocate audiosource";
+ if (rv == NS_ERROR_NOT_AVAILABLE && !badConstraint) {
+ nsTArray<RefPtr<LocalMediaDevice>> devices;
+ devices.AppendElement(mAudioDevice);
+ badConstraint = MediaConstraintsHelper::SelectSettings(
+ NormalizedConstraints(constraints), devices, mCallerType);
+ }
+ }
+ }
+ if (!errorMsg && mVideoDevice) {
+ auto& constraints = GetInvariant(mConstraints.mVideo);
+ rv = mVideoDevice->Allocate(constraints, mPrefs, mWindowID,
+ &badConstraint);
+ if (NS_FAILED(rv)) {
+ errorMsg = "Failed to allocate videosource";
+ if (rv == NS_ERROR_NOT_AVAILABLE && !badConstraint) {
+ nsTArray<RefPtr<LocalMediaDevice>> devices;
+ devices.AppendElement(mVideoDevice);
+ badConstraint = MediaConstraintsHelper::SelectSettings(
+ NormalizedConstraints(constraints), devices, mCallerType);
+ }
+ if (mAudioDevice) {
+ mAudioDevice->Deallocate();
+ }
+ } else {
+ mVideoTrackingId.emplace(mVideoDevice->GetTrackingId());
+ }
+ }
+ if (errorMsg) {
+ LOG("%s %" PRIu32, errorMsg, static_cast<uint32_t>(rv));
+ if (badConstraint) {
+ Fail(MediaMgrError::Name::OverconstrainedError, ""_ns,
+ NS_ConvertUTF8toUTF16(badConstraint));
+ } else {
+ Fail(MediaMgrError::Name::NotReadableError, nsCString(errorMsg));
+ }
+ NS_DispatchToMainThread(
+ NS_NewRunnableFunction("MediaManager::SendPendingGUMRequest", []() {
+ if (MediaManager* manager = MediaManager::GetIfExists()) {
+ manager->SendPendingGUMRequest();
+ }
+ }));
+ return;
+ }
+ NS_DispatchToMainThread(
+ NewRunnableMethod("GetUserMediaStreamTask::PrepareDOMStream", this,
+ &GetUserMediaStreamTask::PrepareDOMStream));
+ }
+
+ public:
+ void Denied(MediaMgrError::Name aName, const nsCString& aMessage) override {
+ MOZ_ASSERT(NS_IsMainThread());
+ Fail(aName, aMessage);
+ }
+
+ const MediaStreamConstraints& GetConstraints() { return mConstraints; }
+
+ private:
+ void PrepareDOMStream();
+
+ // Constraints derived from those passed to getUserMedia() but adjusted for
+ // preferences, defaults, and security
+ const MediaStreamConstraints mConstraints;
+
+ MozPromiseHolder<MediaManager::StreamPromise> mHolder;
+ // GetUserMediaWindowListener with which DeviceListeners are registered
+ const RefPtr<GetUserMediaWindowListener> mWindowListener;
+ const RefPtr<DeviceListener> mAudioDeviceListener;
+ const RefPtr<DeviceListener> mVideoDeviceListener;
+ // MediaDevices are set when selected and Allowed() by the UI.
+ RefPtr<LocalMediaDevice> mAudioDevice;
+ RefPtr<LocalMediaDevice> mVideoDevice;
+ // Tracking id unique for a video frame source. Set when the corresponding
+ // device has been allocated.
+ Maybe<TrackingId> mVideoTrackingId;
+ // Copy of MediaManager::mPrefs
+ const MediaEnginePrefs mPrefs;
+ // media.getusermedia.window.focus_source.enabled
+ const bool mShouldFocusSource;
+ // The MediaManager is referenced at construction so that it won't be
+ // created after its ShutdownBlocker would run.
+ const RefPtr<MediaManager> mManager;
+};
+
+/**
+ * Creates a MediaTrack, attaches a listener and resolves a MozPromise to
+ * provide the stream to the DOM.
+ *
+ * All of this must be done on the main thread!
+ */
+void GetUserMediaStreamTask::PrepareDOMStream() {
+ MOZ_ASSERT(NS_IsMainThread());
+ LOG("GetUserMediaStreamTask::PrepareDOMStream()");
+ nsGlobalWindowInner* window =
+ nsGlobalWindowInner::GetInnerWindowWithId(mWindowID);
+
+ // We're on main-thread, and the windowlist can only
+ // be invalidated from the main-thread (see OnNavigation)
+ if (!mManager->IsWindowListenerStillActive(mWindowListener)) {
+ // This window is no longer live. mListener has already been removed.
+ return;
+ }
+
+ MediaTrackGraph::GraphDriverType graphDriverType =
+ mAudioDevice ? MediaTrackGraph::AUDIO_THREAD_DRIVER
+ : MediaTrackGraph::SYSTEM_THREAD_DRIVER;
+ MediaTrackGraph* mtg = MediaTrackGraph::GetInstance(
+ graphDriverType, window, MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE,
+ MediaTrackGraph::DEFAULT_OUTPUT_DEVICE);
+
+ auto domStream = MakeRefPtr<DOMMediaStream>(window);
+ RefPtr<LocalTrackSource> audioTrackSource;
+ RefPtr<LocalTrackSource> videoTrackSource;
+ nsCOMPtr<nsIPrincipal> principal;
+ RefPtr<PeerIdentity> peerIdentity = nullptr;
+ if (!mConstraints.mPeerIdentity.IsEmpty()) {
+ peerIdentity = new PeerIdentity(mConstraints.mPeerIdentity);
+ principal = NullPrincipal::CreateWithInheritedAttributes(
+ window->GetExtantDoc()->NodePrincipal());
+ } else {
+ principal = window->GetExtantDoc()->NodePrincipal();
+ }
+ RefPtr<GenericNonExclusivePromise> firstFramePromise;
+ if (mAudioDevice) {
+ if (mAudioDevice->GetMediaSource() == MediaSourceEnum::AudioCapture) {
+ // AudioCapture is a special case, here, in the sense that we're not
+ // really using the audio source and the SourceMediaTrack, which acts
+ // as placeholders. We re-route a number of tracks internally in the
+ // MTG and mix them down instead.
+ NS_WARNING(
+ "MediaCaptureWindowState doesn't handle "
+ "MediaSourceEnum::AudioCapture. This must be fixed with UX "
+ "before shipping.");
+ auto audioCaptureSource = MakeRefPtr<AudioCaptureTrackSource>(
+ principal, window, u"Window audio capture"_ns,
+ mtg->CreateAudioCaptureTrack(), peerIdentity);
+ audioTrackSource = audioCaptureSource;
+ RefPtr<MediaStreamTrack> track = new dom::AudioStreamTrack(
+ window, audioCaptureSource->InputTrack(), audioCaptureSource);
+ domStream->AddTrackInternal(track);
+ } else {
+ const nsString& audioDeviceName = mAudioDevice->mName;
+ RefPtr<MediaTrack> track;
+#ifdef MOZ_WEBRTC
+ if (mAudioDevice->IsFake()) {
+ track = mtg->CreateSourceTrack(MediaSegment::AUDIO);
+ } else {
+ track = AudioProcessingTrack::Create(mtg);
+ track->Suspend(); // Microphone source resumes in SetTrack
+ }
+#else
+ track = mtg->CreateSourceTrack(MediaSegment::AUDIO);
+#endif
+ audioTrackSource = new LocalTrackSource(
+ principal, audioDeviceName, mAudioDeviceListener,
+ mAudioDevice->GetMediaSource(), track, peerIdentity);
+ MOZ_ASSERT(MediaManager::IsOn(mConstraints.mAudio));
+ RefPtr<MediaStreamTrack> domTrack = new dom::AudioStreamTrack(
+ window, track, audioTrackSource, dom::MediaStreamTrackState::Live,
+ false, GetInvariant(mConstraints.mAudio));
+ domStream->AddTrackInternal(domTrack);
+ }
+ }
+ if (mVideoDevice) {
+ const nsString& videoDeviceName = mVideoDevice->mName;
+ RefPtr<MediaTrack> track = mtg->CreateSourceTrack(MediaSegment::VIDEO);
+ videoTrackSource = new LocalTrackSource(
+ principal, videoDeviceName, mVideoDeviceListener,
+ mVideoDevice->GetMediaSource(), track, peerIdentity, *mVideoTrackingId);
+ MOZ_ASSERT(MediaManager::IsOn(mConstraints.mVideo));
+ RefPtr<MediaStreamTrack> domTrack = new dom::VideoStreamTrack(
+ window, track, videoTrackSource, dom::MediaStreamTrackState::Live,
+ false, GetInvariant(mConstraints.mVideo));
+ domStream->AddTrackInternal(domTrack);
+ switch (mVideoDevice->GetMediaSource()) {
+ case MediaSourceEnum::Browser:
+ case MediaSourceEnum::Screen:
+ case MediaSourceEnum::Window:
+ // Wait for first frame for screen-sharing devices, to ensure
+ // with and height settings are available immediately, to pass wpt.
+ firstFramePromise = mVideoDevice->Source()->GetFirstFramePromise();
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (!domStream || (!audioTrackSource && !videoTrackSource) ||
+ sHasMainThreadShutdown) {
+ LOG("Returning error for getUserMedia() - no stream");
+
+ mHolder.Reject(
+ MakeRefPtr<MediaMgrError>(
+ MediaMgrError::Name::AbortError,
+ sHasMainThreadShutdown ? "In shutdown"_ns : "No stream."_ns),
+ __func__);
+ return;
+ }
+
+ // Activate our device listeners. We'll call Start() on the source when we
+ // get a callback that the MediaStream has started consuming. The listener
+ // is freed when the page is invalidated (on navigation or close).
+ if (mAudioDeviceListener) {
+ mWindowListener->Activate(mAudioDeviceListener, mAudioDevice,
+ std::move(audioTrackSource));
+ }
+ if (mVideoDeviceListener) {
+ mWindowListener->Activate(mVideoDeviceListener, mVideoDevice,
+ std::move(videoTrackSource));
+ }
+
+ // Dispatch to the media thread to ask it to start the sources, because that
+ // can take a while.
+ typedef DeviceListener::DeviceListenerPromise PromiseType;
+ AutoTArray<RefPtr<PromiseType>, 2> promises;
+ if (mAudioDeviceListener) {
+ promises.AppendElement(mAudioDeviceListener->InitializeAsync());
+ }
+ if (mVideoDeviceListener) {
+ promises.AppendElement(mVideoDeviceListener->InitializeAsync());
+ }
+ PromiseType::All(GetMainThreadSerialEventTarget(), promises)
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [manager = mManager, windowListener = mWindowListener,
+ firstFramePromise] {
+ LOG("GetUserMediaStreamTask::PrepareDOMStream: starting success "
+ "callback following InitializeAsync()");
+ // Initiating and starting devices succeeded.
+ windowListener->ChromeAffectingStateChanged();
+ manager->SendPendingGUMRequest();
+ if (!firstFramePromise) {
+ return DeviceListener::DeviceListenerPromise::CreateAndResolve(
+ true, __func__);
+ }
+ RefPtr<DeviceListener::DeviceListenerPromise> resolvePromise =
+ firstFramePromise->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [] {
+ return DeviceListener::DeviceListenerPromise::
+ CreateAndResolve(true, __func__);
+ },
+ [] {
+ return DeviceListener::DeviceListenerPromise::
+ CreateAndReject(MakeRefPtr<MediaMgrError>(
+ MediaMgrError::Name::AbortError,
+ "In shutdown"),
+ __func__);
+ });
+ return resolvePromise;
+ },
+ [audio = mAudioDeviceListener,
+ video = mVideoDeviceListener](RefPtr<MediaMgrError>&& aError) {
+ LOG("GetUserMediaStreamTask::PrepareDOMStream: starting failure "
+ "callback following InitializeAsync()");
+ if (audio) {
+ audio->Stop();
+ }
+ if (video) {
+ video->Stop();
+ }
+ return DeviceListener::DeviceListenerPromise::CreateAndReject(
+ aError, __func__);
+ })
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [holder = std::move(mHolder), domStream, callerType = mCallerType,
+ shouldFocus = mShouldFocusSource, videoDevice = mVideoDevice](
+ const DeviceListener::DeviceListenerPromise::ResolveOrRejectValue&
+ aValue) mutable {
+ if (aValue.IsResolve()) {
+ if (auto* mgr = MediaManager::GetIfExists();
+ mgr && !sHasMainThreadShutdown && videoDevice &&
+ callerType == CallerType::NonSystem && shouldFocus) {
+ // Device was successfully started. Attempt to focus the
+ // source.
+ MOZ_ALWAYS_SUCCEEDS(
+ mgr->mMediaThread->Dispatch(NS_NewRunnableFunction(
+ "GetUserMediaStreamTask::FocusOnSelectedSource",
+ [videoDevice = std::move(videoDevice)] {
+ nsresult rv = videoDevice->FocusOnSelectedSource();
+ if (NS_FAILED(rv)) {
+ LOG("FocusOnSelectedSource failed");
+ }
+ })));
+ }
+
+ holder.Resolve(domStream, __func__);
+ } else {
+ holder.Reject(aValue.RejectValue(), __func__);
+ }
+ });
+
+ PersistPrincipalKey();
+}
+
+/**
+ * Describes a requested task that handles response from the UI to a
+ * selectAudioOutput() request and sends results back to content. If the
+ * request is allowed, then the MozPromise is resolved with a MediaDevice
+ * for the approved device.
+ */
+class SelectAudioOutputTask final : public GetUserMediaTask {
+ public:
+ SelectAudioOutputTask(MozPromiseHolder<LocalDevicePromise>&& aHolder,
+ uint64_t aWindowID, enum CallerType aCallerType,
+ const ipc::PrincipalInfo& aPrincipalInfo)
+ : GetUserMediaTask(aWindowID, aPrincipalInfo, aCallerType),
+ mHolder(std::move(aHolder)) {}
+
+ void Allowed(RefPtr<LocalMediaDevice> aAudioOutput) {
+ MOZ_ASSERT(aAudioOutput);
+ mHolder.Resolve(std::move(aAudioOutput), __func__);
+ PersistPrincipalKey();
+ }
+
+ void Denied(MediaMgrError::Name aName, const nsCString& aMessage) override {
+ MOZ_ASSERT(NS_IsMainThread());
+ Fail(aName, aMessage);
+ }
+
+ SelectAudioOutputTask* AsSelectAudioOutputTask() override { return this; }
+
+ private:
+ ~SelectAudioOutputTask() override {
+ if (!mHolder.IsEmpty()) {
+ Fail(MediaMgrError::Name::NotAllowedError);
+ }
+ }
+
+ void Fail(MediaMgrError::Name aName, const nsCString& aMessage = ""_ns) {
+ mHolder.Reject(MakeRefPtr<MediaMgrError>(aName, aMessage), __func__);
+ }
+
+ private:
+ MozPromiseHolder<LocalDevicePromise> mHolder;
+};
+
+/* static */
+void MediaManager::GuessVideoDeviceGroupIDs(MediaDeviceSet& aDevices,
+ const MediaDeviceSet& aAudios) {
+ // Run the logic in a lambda to avoid duplication.
+ auto updateGroupIdIfNeeded = [&](RefPtr<MediaDevice>& aVideo,
+ const MediaDeviceKind aKind) -> bool {
+ MOZ_ASSERT(aVideo->mKind == MediaDeviceKind::Videoinput);
+ MOZ_ASSERT(aKind == MediaDeviceKind::Audioinput ||
+ aKind == MediaDeviceKind::Audiooutput);
+ // This will store the new group id if a match is found.
+ nsString newVideoGroupID;
+ // If the group id needs to be updated this will become true. It is
+ // necessary when the new group id is an empty string. Without this extra
+ // variable to signal the update, we would resort to test if
+ // `newVideoGroupId` is empty. However,
+ // that check does not work when the new group id is an empty string.
+ bool updateGroupId = false;
+ for (const RefPtr<MediaDevice>& dev : aAudios) {
+ if (dev->mKind != aKind) {
+ continue;
+ }
+ if (!FindInReadable(aVideo->mRawName, dev->mRawName)) {
+ continue;
+ }
+ if (newVideoGroupID.IsEmpty()) {
+ // This is only expected on first match. If that's the only match group
+ // id will be updated to this one at the end of the loop.
+ updateGroupId = true;
+ newVideoGroupID = dev->mRawGroupID;
+ } else {
+ // More than one device found, it is impossible to know which group id
+ // is the correct one.
+ updateGroupId = false;
+ newVideoGroupID = u""_ns;
+ break;
+ }
+ }
+ if (updateGroupId) {
+ aVideo = MediaDevice::CopyWithNewRawGroupId(aVideo, newVideoGroupID);
+ return true;
+ }
+ return false;
+ };
+
+ for (RefPtr<MediaDevice>& video : aDevices) {
+ if (video->mKind != MediaDeviceKind::Videoinput) {
+ continue;
+ }
+ if (updateGroupIdIfNeeded(video, MediaDeviceKind::Audioinput)) {
+ // GroupId has been updated, continue to the next video device
+ continue;
+ }
+ // GroupId has not been updated, check among the outputs
+ updateGroupIdIfNeeded(video, MediaDeviceKind::Audiooutput);
+ }
+}
+
+/**
+ * EnumerateRawDevices - Enumerate a list of audio & video devices that
+ * satisfy passed-in constraints. List contains raw id's.
+ */
+
+RefPtr<MediaManager::DeviceSetPromise> MediaManager::EnumerateRawDevices(
+ MediaSourceEnum aVideoInputType, MediaSourceEnum aAudioInputType,
+ EnumerationFlags aFlags) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aVideoInputType != MediaSourceEnum::Other ||
+ aAudioInputType != MediaSourceEnum::Other ||
+ aFlags.contains(EnumerationFlag::EnumerateAudioOutputs));
+
+ LOG("%s: aVideoInputType=%" PRIu8 ", aAudioInputType=%" PRIu8, __func__,
+ static_cast<uint8_t>(aVideoInputType),
+ static_cast<uint8_t>(aAudioInputType));
+
+ MozPromiseHolder<DeviceSetPromise> holder;
+ RefPtr<DeviceSetPromise> promise = holder.Ensure(__func__);
+ if (sHasMainThreadShutdown) {
+ // The media thread is no longer available but the result will not be
+ // observable. Resolve with an empty set, so that callers do not need to
+ // handle rejection.
+ holder.Resolve(new MediaDeviceSetRefCnt(), __func__);
+ return promise;
+ }
+
+ const bool hasVideo = aVideoInputType != MediaSourceEnum::Other;
+ const bool hasAudio = aAudioInputType != MediaSourceEnum::Other;
+ const bool hasAudioOutput =
+ aFlags.contains(EnumerationFlag::EnumerateAudioOutputs);
+ const bool forceFakes = aFlags.contains(EnumerationFlag::ForceFakes);
+ const bool fakeByPref = Preferences::GetBool("media.navigator.streams.fake");
+ // Fake and loopback devices are supported for only Camera and Microphone.
+ nsAutoCString videoLoopDev, audioLoopDev;
+ bool hasFakeCams = false;
+ bool hasFakeMics = false;
+ if (aVideoInputType == MediaSourceEnum::Camera) {
+ if (forceFakes) {
+ hasFakeCams = true;
+ } else {
+ Preferences::GetCString("media.video_loopback_dev", videoLoopDev);
+ // Loopback prefs take precedence over fake prefs
+ hasFakeCams = fakeByPref && videoLoopDev.IsEmpty();
+ }
+ }
+ if (aAudioInputType == MediaSourceEnum::Microphone) {
+ if (forceFakes) {
+ hasFakeMics = true;
+ } else {
+ Preferences::GetCString("media.audio_loopback_dev", audioLoopDev);
+ // Loopback prefs take precedence over fake prefs
+ hasFakeMics = fakeByPref && audioLoopDev.IsEmpty();
+ }
+ }
+ // True if at least one of video input or audio input is a real device
+ // or there is audio output.
+ const bool realDeviceRequested = (!hasFakeCams && hasVideo) ||
+ (!hasFakeMics && hasAudio) || hasAudioOutput;
+
+ RefPtr<Runnable> task = NewTaskFrom(
+ [holder = std::move(holder), aVideoInputType, aAudioInputType,
+ hasFakeCams, hasFakeMics, videoLoopDev, audioLoopDev, hasVideo, hasAudio,
+ hasAudioOutput, realDeviceRequested]() mutable {
+ // Only enumerate what's asked for, and only fake cams and mics.
+ RefPtr<MediaEngine> fakeBackend, realBackend;
+ if (hasFakeCams || hasFakeMics) {
+ fakeBackend = new MediaEngineFake();
+ }
+ if (realDeviceRequested) {
+ MediaManager* manager = MediaManager::GetIfExists();
+ MOZ_RELEASE_ASSERT(manager, "Must exist while media thread is alive");
+ realBackend = manager->GetBackend();
+ }
+
+ RefPtr<MediaEngine> videoBackend;
+ RefPtr<MediaEngine> audioBackend;
+ Maybe<MediaDeviceSet> micsOfVideoBackend;
+ Maybe<MediaDeviceSet> speakers;
+ RefPtr devices = new MediaDeviceSetRefCnt();
+
+ // Enumerate microphones first, then cameras, then speakers, since the
+ // enumerateDevices() algorithm expects them listed in that order.
+ if (hasAudio) {
+ audioBackend = hasFakeMics ? fakeBackend : realBackend;
+ MediaDeviceSet audios;
+ LOG("EnumerateRawDevices Task: Getting audio sources with %s backend",
+ audioBackend == fakeBackend ? "fake" : "real");
+ GetMediaDevices(audioBackend, aAudioInputType, audios,
+ audioLoopDev.get());
+ if (aAudioInputType == MediaSourceEnum::Microphone &&
+ audioBackend == videoBackend) {
+ micsOfVideoBackend = Some(MediaDeviceSet());
+ micsOfVideoBackend->AppendElements(audios);
+ }
+ devices->AppendElements(audios);
+ }
+ if (hasVideo) {
+ videoBackend = hasFakeCams ? fakeBackend : realBackend;
+ MediaDeviceSet videos;
+ LOG("EnumerateRawDevices Task: Getting video sources with %s backend",
+ videoBackend == fakeBackend ? "fake" : "real");
+ GetMediaDevices(videoBackend, aVideoInputType, videos,
+ videoLoopDev.get());
+ devices->AppendElements(videos);
+ }
+ if (hasAudioOutput) {
+ MediaDeviceSet outputs;
+ MOZ_ASSERT(realBackend);
+ realBackend->EnumerateDevices(MediaSourceEnum::Other,
+ MediaSinkEnum::Speaker, &outputs);
+ speakers = Some(MediaDeviceSet());
+ speakers->AppendElements(outputs);
+ devices->AppendElements(outputs);
+ }
+ if (hasVideo && aVideoInputType == MediaSourceEnum::Camera) {
+ MediaDeviceSet audios;
+ LOG("EnumerateRawDevices Task: Getting audio sources with %s backend "
+ "for "
+ "groupId correlation",
+ videoBackend == fakeBackend ? "fake" : "real");
+ // We need to correlate cameras with audio groupIds. We use the
+ // backend of the camera to always do correlation on devices in the
+ // same scope. If we don't do this, video-only getUserMedia will not
+ // apply groupId constraints to the same set of groupIds as gets
+ // returned by enumerateDevices.
+ if (micsOfVideoBackend.isSome()) {
+ // Microphones from the same backend used for the cameras have
+ // already been enumerated. Avoid doing it again.
+ audios.AppendElements(*micsOfVideoBackend);
+ } else {
+ GetMediaDevices(videoBackend, MediaSourceEnum::Microphone, audios,
+ audioLoopDev.get());
+ }
+ if (videoBackend == realBackend) {
+ // When using the real backend for video, there could also be
+ // speakers to correlate with. There are no fake speakers.
+ if (speakers.isSome()) {
+ // Speakers have already been enumerated. Avoid doing it again.
+ audios.AppendElements(*speakers);
+ } else {
+ realBackend->EnumerateDevices(MediaSourceEnum::Other,
+ MediaSinkEnum::Speaker, &audios);
+ }
+ }
+ GuessVideoDeviceGroupIDs(*devices, audios);
+ }
+
+ holder.Resolve(std::move(devices), __func__);
+ });
+
+ if (realDeviceRequested &&
+ aFlags.contains(EnumerationFlag::AllowPermissionRequest) &&
+ Preferences::GetBool("media.navigator.permission.device", false)) {
+ // Need to ask permission to retrieve list of all devices;
+ // notify frontend observer and wait for callback notification to post task.
+ const char16_t* const type =
+ (aVideoInputType != MediaSourceEnum::Camera) ? u"audio"
+ : (aAudioInputType != MediaSourceEnum::Microphone) ? u"video"
+ : u"all";
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ obs->NotifyObservers(static_cast<nsIRunnable*>(task),
+ "getUserMedia:ask-device-permission", type);
+ } else {
+ // Don't need to ask permission to retrieve list of all devices;
+ // post the retrieval task immediately.
+ MediaManager::Dispatch(task.forget());
+ }
+
+ return promise;
+}
+
+RefPtr<ConstDeviceSetPromise> MediaManager::GetPhysicalDevices() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mPhysicalDevices) {
+ return ConstDeviceSetPromise::CreateAndResolve(mPhysicalDevices, __func__);
+ }
+ if (mPendingDevicesPromises) {
+ // Enumeration is already in progress.
+ return mPendingDevicesPromises->AppendElement()->Ensure(__func__);
+ }
+ mPendingDevicesPromises =
+ new Refcountable<nsTArray<MozPromiseHolder<ConstDeviceSetPromise>>>;
+ EnumerateRawDevices(MediaSourceEnum::Camera, MediaSourceEnum::Microphone,
+ EnumerationFlag::EnumerateAudioOutputs)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr(this), this, promises = mPendingDevicesPromises](
+ RefPtr<MediaDeviceSetRefCnt> aDevices) mutable {
+ for (auto& promiseHolder : *promises) {
+ promiseHolder.Resolve(aDevices, __func__);
+ }
+ // mPendingDevicesPromises may have changed if devices have changed.
+ if (promises == mPendingDevicesPromises) {
+ mPendingDevicesPromises = nullptr;
+ mPhysicalDevices = std::move(aDevices);
+ }
+ },
+ [](RefPtr<MediaMgrError>&& reason) {
+ MOZ_ASSERT_UNREACHABLE("EnumerateRawDevices does not reject");
+ });
+
+ return mPendingDevicesPromises->AppendElement()->Ensure(__func__);
+}
+
+MediaManager::MediaManager(already_AddRefed<TaskQueue> aMediaThread)
+ : mMediaThread(aMediaThread), mBackend(nullptr) {
+ mPrefs.mFreq = 1000; // 1KHz test tone
+ mPrefs.mWidth = 0; // adaptive default
+ mPrefs.mHeight = 0; // adaptive default
+ mPrefs.mFPS = MediaEnginePrefs::DEFAULT_VIDEO_FPS;
+ mPrefs.mAecOn = false;
+ mPrefs.mUseAecMobile = false;
+ mPrefs.mAgcOn = false;
+ mPrefs.mHPFOn = false;
+ mPrefs.mNoiseOn = false;
+ mPrefs.mTransientOn = false;
+ mPrefs.mResidualEchoOn = false;
+ mPrefs.mAgc2Forced = false;
+#ifdef MOZ_WEBRTC
+ mPrefs.mAgc =
+ webrtc::AudioProcessing::Config::GainController1::Mode::kAdaptiveDigital;
+ mPrefs.mNoise =
+ webrtc::AudioProcessing::Config::NoiseSuppression::Level::kModerate;
+#else
+ mPrefs.mAgc = 0;
+ mPrefs.mNoise = 0;
+#endif
+ mPrefs.mChannels = 0; // max channels default
+ nsresult rv;
+ nsCOMPtr<nsIPrefService> prefs =
+ do_GetService("@mozilla.org/preferences-service;1", &rv);
+ if (NS_SUCCEEDED(rv)) {
+ nsCOMPtr<nsIPrefBranch> branch = do_QueryInterface(prefs);
+ if (branch) {
+ GetPrefs(branch, nullptr);
+ }
+ }
+ LOG("%s: default prefs: %dx%d @%dfps, %dHz test tones, aec: %s,"
+ "agc: %s, hpf: %s, noise: %s, agc level: %d, agc version: %s, noise "
+ "level: %d, transient: %s, residual echo: %s, channels %d",
+ __FUNCTION__, mPrefs.mWidth, mPrefs.mHeight, mPrefs.mFPS, mPrefs.mFreq,
+ mPrefs.mAecOn ? "on" : "off", mPrefs.mAgcOn ? "on" : "off",
+ mPrefs.mHPFOn ? "on" : "off", mPrefs.mNoiseOn ? "on" : "off", mPrefs.mAgc,
+ mPrefs.mAgc2Forced ? "2" : "1", mPrefs.mNoise,
+ mPrefs.mTransientOn ? "on" : "off", mPrefs.mResidualEchoOn ? "on" : "off",
+ mPrefs.mChannels);
+}
+
+NS_IMPL_ISUPPORTS(MediaManager, nsIMediaManagerService, nsIMemoryReporter,
+ nsIObserver)
+
+/* static */
+StaticRefPtr<MediaManager> MediaManager::sSingleton;
+
+#ifdef DEBUG
+/* static */
+bool MediaManager::IsInMediaThread() {
+ return sSingleton && sSingleton->mMediaThread->IsOnCurrentThread();
+}
+#endif
+
+template <typename Function>
+static void ForeachObservedPref(const Function& aFunction) {
+ aFunction("media.navigator.video.default_width"_ns);
+ aFunction("media.navigator.video.default_height"_ns);
+ aFunction("media.navigator.video.default_fps"_ns);
+ aFunction("media.navigator.audio.fake_frequency"_ns);
+ aFunction("media.audio_loopback_dev"_ns);
+ aFunction("media.video_loopback_dev"_ns);
+ aFunction("media.getusermedia.fake-camera-name"_ns);
+#ifdef MOZ_WEBRTC
+ aFunction("media.getusermedia.aec_enabled"_ns);
+ aFunction("media.getusermedia.aec"_ns);
+ aFunction("media.getusermedia.agc_enabled"_ns);
+ aFunction("media.getusermedia.agc"_ns);
+ aFunction("media.getusermedia.hpf_enabled"_ns);
+ aFunction("media.getusermedia.noise_enabled"_ns);
+ aFunction("media.getusermedia.noise"_ns);
+ aFunction("media.getusermedia.channels"_ns);
+ aFunction("media.navigator.streams.fake"_ns);
+#endif
+}
+
+// NOTE: never NS_DispatchAndSpinEventLoopUntilComplete to the MediaManager
+// thread from the MainThread, as we NS_DispatchAndSpinEventLoopUntilComplete to
+// MainThread from MediaManager thread.
+
+// Guaranteed never to return nullptr.
+/* static */
+MediaManager* MediaManager::Get() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!sSingleton) {
+ static int timesCreated = 0;
+ timesCreated++;
+ MOZ_RELEASE_ASSERT(timesCreated == 1);
+
+ RefPtr<TaskQueue> mediaThread = TaskQueue::Create(
+ GetMediaThreadPool(MediaThreadType::SUPERVISOR), "MediaManager");
+ LOG("New Media thread for gum");
+
+ sSingleton = new MediaManager(mediaThread.forget());
+
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ if (obs) {
+ obs->AddObserver(sSingleton, "last-pb-context-exited", false);
+ obs->AddObserver(sSingleton, "getUserMedia:got-device-permission", false);
+ obs->AddObserver(sSingleton, "getUserMedia:privileged:allow", false);
+ obs->AddObserver(sSingleton, "getUserMedia:response:allow", false);
+ obs->AddObserver(sSingleton, "getUserMedia:response:deny", false);
+ obs->AddObserver(sSingleton, "getUserMedia:response:noOSPermission",
+ false);
+ obs->AddObserver(sSingleton, "getUserMedia:revoke", false);
+ obs->AddObserver(sSingleton, "getUserMedia:muteVideo", false);
+ obs->AddObserver(sSingleton, "getUserMedia:unmuteVideo", false);
+ obs->AddObserver(sSingleton, "getUserMedia:muteAudio", false);
+ obs->AddObserver(sSingleton, "getUserMedia:unmuteAudio", false);
+ obs->AddObserver(sSingleton, "application-background", false);
+ obs->AddObserver(sSingleton, "application-foreground", false);
+ }
+ // else MediaManager won't work properly and will leak (see bug 837874)
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ if (prefs) {
+ ForeachObservedPref([&](const nsLiteralCString& aPrefName) {
+ prefs->AddObserver(aPrefName, sSingleton, false);
+ });
+ }
+ RegisterStrongMemoryReporter(sSingleton);
+
+ // Prepare async shutdown
+
+ class Blocker : public media::ShutdownBlocker {
+ public:
+ Blocker()
+ : media::ShutdownBlocker(
+ u"Media shutdown: blocking on media thread"_ns) {}
+
+ NS_IMETHOD BlockShutdown(nsIAsyncShutdownClient*) override {
+ MOZ_RELEASE_ASSERT(MediaManager::GetIfExists());
+ MediaManager::GetIfExists()->Shutdown();
+ return NS_OK;
+ }
+ };
+
+ sSingleton->mShutdownBlocker = new Blocker();
+ nsresult rv = media::MustGetShutdownBarrier()->AddBlocker(
+ sSingleton->mShutdownBlocker, NS_LITERAL_STRING_FROM_CSTRING(__FILE__),
+ __LINE__, u""_ns);
+ MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
+ }
+ return sSingleton;
+}
+
+/* static */
+MediaManager* MediaManager::GetIfExists() {
+ MOZ_ASSERT(NS_IsMainThread() || IsInMediaThread());
+ return sSingleton;
+}
+
+/* static */
+already_AddRefed<MediaManager> MediaManager::GetInstance() {
+ // so we can have non-refcounted getters
+ RefPtr<MediaManager> service = MediaManager::Get();
+ return service.forget();
+}
+
+media::Parent<media::NonE10s>* MediaManager::GetNonE10sParent() {
+ if (!mNonE10sParent) {
+ mNonE10sParent = new media::Parent<media::NonE10s>();
+ }
+ return mNonE10sParent;
+}
+
+/* static */
+void MediaManager::StartupInit() {
+#ifdef WIN32
+ if (!IsWin8OrLater()) {
+ // Bug 1107702 - Older Windows fail in GetAdaptersInfo (and others) if the
+ // first(?) call occurs after the process size is over 2GB (kb/2588507).
+ // Attempt to 'prime' the pump by making a call at startup.
+ unsigned long out_buf_len = sizeof(IP_ADAPTER_INFO);
+ PIP_ADAPTER_INFO pAdapterInfo = (IP_ADAPTER_INFO*)moz_xmalloc(out_buf_len);
+ if (GetAdaptersInfo(pAdapterInfo, &out_buf_len) == ERROR_BUFFER_OVERFLOW) {
+ free(pAdapterInfo);
+ pAdapterInfo = (IP_ADAPTER_INFO*)moz_xmalloc(out_buf_len);
+ GetAdaptersInfo(pAdapterInfo, &out_buf_len);
+ }
+ if (pAdapterInfo) {
+ free(pAdapterInfo);
+ }
+ }
+#endif
+}
+
+/* static */
+void MediaManager::Dispatch(already_AddRefed<Runnable> task) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (sHasMainThreadShutdown) {
+ // Can't safely delete task here since it may have items with specific
+ // thread-release requirements.
+ // XXXkhuey well then who is supposed to delete it?! We don't signal
+ // that we failed ...
+ MOZ_CRASH();
+ return;
+ }
+ NS_ASSERTION(Get(), "MediaManager singleton?");
+ NS_ASSERTION(Get()->mMediaThread, "No thread yet");
+ MOZ_ALWAYS_SUCCEEDS(Get()->mMediaThread->Dispatch(std::move(task)));
+}
+
+template <typename MozPromiseType, typename FunctionType>
+/* static */
+RefPtr<MozPromiseType> MediaManager::Dispatch(const char* aName,
+ FunctionType&& aFunction) {
+ MozPromiseHolder<MozPromiseType> holder;
+ RefPtr<MozPromiseType> promise = holder.Ensure(aName);
+ MediaManager::Dispatch(NS_NewRunnableFunction(
+ aName, [h = std::move(holder), func = std::forward<FunctionType>(
+ aFunction)]() mutable { func(h); }));
+ return promise;
+}
+
+/* static */
+nsresult MediaManager::NotifyRecordingStatusChange(
+ nsPIDOMWindowInner* aWindow) {
+ NS_ENSURE_ARG(aWindow);
+
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ if (!obs) {
+ NS_WARNING(
+ "Could not get the Observer service for GetUserMedia recording "
+ "notification.");
+ return NS_ERROR_FAILURE;
+ }
+
+ auto props = MakeRefPtr<nsHashPropertyBag>();
+
+ nsCString pageURL;
+ nsCOMPtr<nsIURI> docURI = aWindow->GetDocumentURI();
+ NS_ENSURE_TRUE(docURI, NS_ERROR_FAILURE);
+
+ nsresult rv = docURI->GetSpec(pageURL);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NS_ConvertUTF8toUTF16 requestURL(pageURL);
+
+ props->SetPropertyAsAString(u"requestURL"_ns, requestURL);
+ props->SetPropertyAsInterface(u"window"_ns, aWindow);
+
+ obs->NotifyObservers(static_cast<nsIPropertyBag2*>(props),
+ "recording-device-events", nullptr);
+ LOG("Sent recording-device-events for url '%s'", pageURL.get());
+
+ return NS_OK;
+}
+
+void MediaManager::DeviceListChanged() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (sHasMainThreadShutdown) {
+ return;
+ }
+ // Invalidate immediately to provide an up-to-date device list for future
+ // enumerations on platforms with sane device-list-changed events.
+ InvalidateDeviceCache();
+
+ // Wait 200 ms, because
+ // A) on some Windows machines, if we call EnumerateRawDevices immediately
+ // after receiving devicechange event, we'd get an outdated devices list.
+ // B) Waiting helps coalesce multiple calls on us into one, which can happen
+ // if a device with both audio input and output is attached or removed.
+ // We want to react & fire a devicechange event only once in that case.
+
+ // The wait is extended if another hardware device-list-changed notification
+ // is received to provide the full 200ms for EnumerateRawDevices().
+ if (mDeviceChangeTimer) {
+ mDeviceChangeTimer->Cancel();
+ } else {
+ mDeviceChangeTimer = MakeRefPtr<MediaTimer>();
+ }
+ // However, if this would cause a delay of over 1000ms in handling the
+ // oldest unhandled event, then respond now and set the timer to run
+ // EnumerateRawDevices() again in 200ms.
+ auto now = TimeStamp::NowLoRes();
+ auto enumerateDelay = TimeDuration::FromMilliseconds(200);
+ auto coalescenceLimit = TimeDuration::FromMilliseconds(1000) - enumerateDelay;
+ if (!mUnhandledDeviceChangeTime) {
+ mUnhandledDeviceChangeTime = now;
+ } else if (now - mUnhandledDeviceChangeTime > coalescenceLimit) {
+ HandleDeviceListChanged();
+ mUnhandledDeviceChangeTime = now;
+ }
+ RefPtr<MediaManager> self = this;
+ mDeviceChangeTimer->WaitFor(enumerateDelay, __func__)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self, this] {
+ // Invalidate again for the sake of platforms with inconsistent
+ // timing between device-list-changed notification and enumeration.
+ InvalidateDeviceCache();
+
+ mUnhandledDeviceChangeTime = TimeStamp();
+ HandleDeviceListChanged();
+ },
+ [] { /* Timer was canceled by us, or we're in shutdown. */ });
+}
+
+void MediaManager::InvalidateDeviceCache() {
+ mPhysicalDevices = nullptr;
+ // Disconnect any in-progress enumeration, which may now be out of date,
+ // from updating mPhysicalDevices or resolving future device request
+ // promises.
+ mPendingDevicesPromises = nullptr;
+}
+
+void MediaManager::HandleDeviceListChanged() {
+ mDeviceListChangeEvent.Notify();
+
+ GetPhysicalDevices()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr(this), this](RefPtr<const MediaDeviceSetRefCnt> aDevices) {
+ if (!MediaManager::GetIfExists()) {
+ return;
+ }
+
+ nsTHashSet<nsString> deviceIDs;
+ for (const auto& device : *aDevices) {
+ deviceIDs.Insert(device->mRawID);
+ }
+ // For any real removed cameras or microphones, notify their
+ // listeners cleanly that the source has stopped, so JS knows and
+ // usage indicators update.
+ // First collect the listeners in an array to stop them after
+ // iterating the hashtable. The StopRawID() method indirectly
+ // modifies the mActiveWindows and would assert-crash if the
+ // iterator were active while the table is being enumerated.
+ const auto windowListeners = ToArray(mActiveWindows.Values());
+ for (const RefPtr<GetUserMediaWindowListener>& l : windowListeners) {
+ const auto activeDevices = l->GetDevices();
+ for (const RefPtr<LocalMediaDevice>& device : *activeDevices) {
+ if (device->IsFake()) {
+ continue;
+ }
+ MediaSourceEnum mediaSource = device->GetMediaSource();
+ if (mediaSource != MediaSourceEnum::Microphone &&
+ mediaSource != MediaSourceEnum::Camera) {
+ continue;
+ }
+ if (!deviceIDs.Contains(device->RawID())) {
+ // Device has been removed
+ l->StopRawID(device->RawID());
+ }
+ }
+ }
+ },
+ [](RefPtr<MediaMgrError>&& reason) {
+ MOZ_ASSERT_UNREACHABLE("EnumerateRawDevices does not reject");
+ });
+}
+
+size_t MediaManager::AddTaskAndGetCount(uint64_t aWindowID,
+ const nsAString& aCallID,
+ RefPtr<GetUserMediaTask> aTask) {
+ // Store the task w/callbacks.
+ mActiveCallbacks.InsertOrUpdate(aCallID, std::move(aTask));
+
+ // Add a WindowID cross-reference so OnNavigation can tear things down
+ nsTArray<nsString>* const array = mCallIds.GetOrInsertNew(aWindowID);
+ array->AppendElement(aCallID);
+
+ return array->Length();
+}
+
+RefPtr<GetUserMediaTask> MediaManager::TakeGetUserMediaTask(
+ const nsAString& aCallID) {
+ RefPtr<GetUserMediaTask> task;
+ mActiveCallbacks.Remove(aCallID, getter_AddRefs(task));
+ if (!task) {
+ return nullptr;
+ }
+ nsTArray<nsString>* array;
+ mCallIds.Get(task->GetWindowID(), &array);
+ MOZ_ASSERT(array);
+ array->RemoveElement(aCallID);
+ return task;
+}
+
+void MediaManager::NotifyAllowed(const nsString& aCallID,
+ const LocalMediaDeviceSet& aDevices) {
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ nsCOMPtr<nsIMutableArray> devicesCopy = nsArray::Create();
+ for (const auto& device : aDevices) {
+ nsresult rv = devicesCopy->AppendElement(device);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ obs->NotifyObservers(nullptr, "getUserMedia:response:deny",
+ aCallID.get());
+ return;
+ }
+ }
+ obs->NotifyObservers(devicesCopy, "getUserMedia:privileged:allow",
+ aCallID.get());
+}
+
+nsresult MediaManager::GenerateUUID(nsAString& aResult) {
+ nsresult rv;
+ nsCOMPtr<nsIUUIDGenerator> uuidgen =
+ do_GetService("@mozilla.org/uuid-generator;1", &rv);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Generate a call ID.
+ nsID id;
+ rv = uuidgen->GenerateUUIDInPlace(&id);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ char buffer[NSID_LENGTH];
+ id.ToProvidedString(buffer);
+ aResult.Assign(NS_ConvertUTF8toUTF16(buffer));
+ return NS_OK;
+}
+
+enum class GetUserMediaSecurityState {
+ Other = 0,
+ HTTPS = 1,
+ File = 2,
+ App = 3,
+ Localhost = 4,
+ Loop = 5,
+ Privileged = 6
+};
+
+/**
+ * This function is used in getUserMedia when privacy.resistFingerprinting is
+ * true. Only mediaSource of audio/video constraint will be kept.
+ */
+static void ReduceConstraint(
+ OwningBooleanOrMediaTrackConstraints& aConstraint) {
+ // Not requesting stream.
+ if (!MediaManager::IsOn(aConstraint)) {
+ return;
+ }
+
+ // It looks like {audio: true}, do nothing.
+ if (!aConstraint.IsMediaTrackConstraints()) {
+ return;
+ }
+
+ // Keep mediaSource, ignore all other constraints.
+ Maybe<nsString> mediaSource;
+ if (aConstraint.GetAsMediaTrackConstraints().mMediaSource.WasPassed()) {
+ mediaSource =
+ Some(aConstraint.GetAsMediaTrackConstraints().mMediaSource.Value());
+ }
+ aConstraint.Uninit();
+ if (mediaSource) {
+ aConstraint.SetAsMediaTrackConstraints().mMediaSource.Construct(
+ *mediaSource);
+ } else {
+ aConstraint.SetAsMediaTrackConstraints();
+ }
+}
+
+/**
+ * The entry point for this file. A call from Navigator::mozGetUserMedia
+ * will end up here. MediaManager is a singleton that is responsible
+ * for handling all incoming getUserMedia calls from every window.
+ */
+RefPtr<MediaManager::StreamPromise> MediaManager::GetUserMedia(
+ nsPIDOMWindowInner* aWindow,
+ const MediaStreamConstraints& aConstraintsPassedIn,
+ CallerType aCallerType) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aWindow);
+ uint64_t windowID = aWindow->WindowID();
+
+ MediaStreamConstraints c(aConstraintsPassedIn); // use a modifiable copy
+
+ if (sHasMainThreadShutdown) {
+ return StreamPromise::CreateAndReject(
+ MakeRefPtr<MediaMgrError>(MediaMgrError::Name::AbortError,
+ "In shutdown"),
+ __func__);
+ }
+
+ // Determine permissions early (while we still have a stack).
+
+ nsIURI* docURI = aWindow->GetDocumentURI();
+ if (!docURI) {
+ return StreamPromise::CreateAndReject(
+ MakeRefPtr<MediaMgrError>(MediaMgrError::Name::AbortError), __func__);
+ }
+ bool isChrome = (aCallerType == CallerType::System);
+ bool privileged =
+ isChrome ||
+ Preferences::GetBool("media.navigator.permission.disabled", false);
+ bool isSecure = aWindow->IsSecureContext();
+ bool isHandlingUserInput = UserActivation::IsHandlingUserInput();
+ nsCString host;
+ nsresult rv = docURI->GetHost(host);
+
+ nsCOMPtr<nsIPrincipal> principal =
+ nsGlobalWindowInner::Cast(aWindow)->GetPrincipal();
+ if (NS_WARN_IF(!principal)) {
+ return StreamPromise::CreateAndReject(
+ MakeRefPtr<MediaMgrError>(MediaMgrError::Name::SecurityError),
+ __func__);
+ }
+
+ Document* doc = aWindow->GetExtantDoc();
+ if (NS_WARN_IF(!doc)) {
+ return StreamPromise::CreateAndReject(
+ MakeRefPtr<MediaMgrError>(MediaMgrError::Name::SecurityError),
+ __func__);
+ }
+
+ // Disallow access to null principal pages and http pages (unless pref)
+ if (principal->GetIsNullPrincipal() ||
+ !(isSecure || StaticPrefs::media_getusermedia_insecure_enabled())) {
+ return StreamPromise::CreateAndReject(
+ MakeRefPtr<MediaMgrError>(MediaMgrError::Name::NotAllowedError),
+ __func__);
+ }
+
+ // This principal needs to be sent to different threads and so via IPC.
+ // For this reason it's better to convert it to PrincipalInfo right now.
+ ipc::PrincipalInfo principalInfo;
+ rv = PrincipalToPrincipalInfo(principal, &principalInfo);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return StreamPromise::CreateAndReject(
+ MakeRefPtr<MediaMgrError>(MediaMgrError::Name::SecurityError),
+ __func__);
+ }
+
+ const bool resistFingerprinting =
+ !isChrome && doc->ShouldResistFingerprinting(RFPTarget::Unknown);
+ if (resistFingerprinting) {
+ ReduceConstraint(c.mVideo);
+ ReduceConstraint(c.mAudio);
+ }
+
+ if (!Preferences::GetBool("media.navigator.video.enabled", true)) {
+ c.mVideo.SetAsBoolean() = false;
+ }
+
+ MediaSourceEnum videoType = MediaSourceEnum::Other; // none
+ MediaSourceEnum audioType = MediaSourceEnum::Other; // none
+
+ if (c.mVideo.IsMediaTrackConstraints()) {
+ auto& vc = c.mVideo.GetAsMediaTrackConstraints();
+ if (!vc.mMediaSource.WasPassed()) {
+ vc.mMediaSource.Construct().AssignASCII(
+ dom::MediaSourceEnumValues::GetString(MediaSourceEnum::Camera));
+ }
+ videoType = StringToEnum(dom::MediaSourceEnumValues::strings,
+ vc.mMediaSource.Value(), MediaSourceEnum::Other);
+ Telemetry::Accumulate(Telemetry::WEBRTC_GET_USER_MEDIA_TYPE,
+ (uint32_t)videoType);
+ switch (videoType) {
+ case MediaSourceEnum::Camera:
+ break;
+
+ case MediaSourceEnum::Browser:
+ // If no window id is passed in then default to the caller's window.
+ // Functional defaults are helpful in tests, but also a natural outcome
+ // of the constraints API's limited semantics for requiring input.
+ if (!vc.mBrowserWindow.WasPassed()) {
+ nsPIDOMWindowOuter* outer = aWindow->GetOuterWindow();
+ vc.mBrowserWindow.Construct(outer->WindowID());
+ }
+ [[fallthrough]];
+ case MediaSourceEnum::Screen:
+ case MediaSourceEnum::Window:
+ // Deny screensharing request if support is disabled, or
+ // the requesting document is not from a host on the whitelist.
+ if (!Preferences::GetBool(
+ ((videoType == MediaSourceEnum::Browser)
+ ? "media.getusermedia.browser.enabled"
+ : "media.getusermedia.screensharing.enabled"),
+ false) ||
+ (!privileged && !aWindow->IsSecureContext())) {
+ return StreamPromise::CreateAndReject(
+ MakeRefPtr<MediaMgrError>(MediaMgrError::Name::NotAllowedError),
+ __func__);
+ }
+ break;
+
+ case MediaSourceEnum::Microphone:
+ case MediaSourceEnum::Other:
+ default: {
+ return StreamPromise::CreateAndReject(
+ MakeRefPtr<MediaMgrError>(MediaMgrError::Name::OverconstrainedError,
+ "", u"mediaSource"_ns),
+ __func__);
+ }
+ }
+
+ if (!privileged) {
+ // Only allow privileged content to explicitly pick full-screen,
+ // application or tabsharing, since these modes are still available for
+ // testing. All others get "Window" (*) sharing.
+ //
+ // *) We overload "Window" with the new default getDisplayMedia spec-
+ // mandated behavior of not influencing user-choice, which we currently
+ // implement as a list containing BOTH windows AND screen(s).
+ //
+ // Notes on why we chose "Window" as the one to overload. Two reasons:
+ //
+ // 1. It's the closest logically & behaviorally (multi-choice, no default)
+ // 2. Screen is still useful in tests (implicit default is entire screen)
+ //
+ // For UX reasons we don't want "Entire Screen" to be the first/default
+ // choice (in our code first=default). It's a "scary" source that comes
+ // with complicated warnings on-top that would be confusing as the first
+ // thing people see, and also deserves to be listed as last resort for
+ // privacy reasons.
+
+ if (videoType == MediaSourceEnum::Screen ||
+ videoType == MediaSourceEnum::Browser) {
+ videoType = MediaSourceEnum::Window;
+ vc.mMediaSource.Value().AssignASCII(
+ dom::MediaSourceEnumValues::GetString(videoType));
+ }
+ // only allow privileged content to set the window id
+ if (vc.mBrowserWindow.WasPassed()) {
+ vc.mBrowserWindow.Value() = -1;
+ }
+ if (vc.mAdvanced.WasPassed()) {
+ for (MediaTrackConstraintSet& cs : vc.mAdvanced.Value()) {
+ if (cs.mBrowserWindow.WasPassed()) {
+ cs.mBrowserWindow.Value() = -1;
+ }
+ }
+ }
+ }
+ } else if (IsOn(c.mVideo)) {
+ videoType = MediaSourceEnum::Camera;
+ Telemetry::Accumulate(Telemetry::WEBRTC_GET_USER_MEDIA_TYPE,
+ (uint32_t)videoType);
+ }
+
+ if (c.mAudio.IsMediaTrackConstraints()) {
+ auto& ac = c.mAudio.GetAsMediaTrackConstraints();
+ if (!ac.mMediaSource.WasPassed()) {
+ ac.mMediaSource.Construct(NS_ConvertASCIItoUTF16(
+ dom::MediaSourceEnumValues::GetString(MediaSourceEnum::Microphone)));
+ }
+ audioType = StringToEnum(dom::MediaSourceEnumValues::strings,
+ ac.mMediaSource.Value(), MediaSourceEnum::Other);
+ Telemetry::Accumulate(Telemetry::WEBRTC_GET_USER_MEDIA_TYPE,
+ (uint32_t)audioType);
+
+ switch (audioType) {
+ case MediaSourceEnum::Microphone:
+ break;
+
+ case MediaSourceEnum::AudioCapture:
+ // Only enable AudioCapture if the pref is enabled. If it's not, we can
+ // deny right away.
+ if (!Preferences::GetBool("media.getusermedia.audiocapture.enabled")) {
+ return StreamPromise::CreateAndReject(
+ MakeRefPtr<MediaMgrError>(MediaMgrError::Name::NotAllowedError),
+ __func__);
+ }
+ break;
+
+ case MediaSourceEnum::Other:
+ default: {
+ return StreamPromise::CreateAndReject(
+ MakeRefPtr<MediaMgrError>(MediaMgrError::Name::OverconstrainedError,
+ "", u"mediaSource"_ns),
+ __func__);
+ }
+ }
+ } else if (IsOn(c.mAudio)) {
+ audioType = MediaSourceEnum::Microphone;
+ Telemetry::Accumulate(Telemetry::WEBRTC_GET_USER_MEDIA_TYPE,
+ (uint32_t)audioType);
+ }
+
+ // Create a window listener if it doesn't already exist.
+ RefPtr<GetUserMediaWindowListener> windowListener =
+ GetOrMakeWindowListener(aWindow);
+ MOZ_ASSERT(windowListener);
+ // Create an inactive DeviceListener to act as a placeholder, so the
+ // window listener doesn't clean itself up until we're done.
+ auto placeholderListener = MakeRefPtr<DeviceListener>();
+ windowListener->Register(placeholderListener);
+
+ { // Check Permissions Policy. Reject if a requested feature is disabled.
+ bool disabled = !IsOn(c.mAudio) && !IsOn(c.mVideo);
+ if (IsOn(c.mAudio)) {
+ if (audioType == MediaSourceEnum::Microphone) {
+ if (Preferences::GetBool("media.getusermedia.microphone.deny", false) ||
+ !FeaturePolicyUtils::IsFeatureAllowed(doc, u"microphone"_ns)) {
+ disabled = true;
+ }
+ } else if (!FeaturePolicyUtils::IsFeatureAllowed(doc,
+ u"display-capture"_ns)) {
+ disabled = true;
+ }
+ }
+ if (IsOn(c.mVideo)) {
+ if (videoType == MediaSourceEnum::Camera) {
+ if (Preferences::GetBool("media.getusermedia.camera.deny", false) ||
+ !FeaturePolicyUtils::IsFeatureAllowed(doc, u"camera"_ns)) {
+ disabled = true;
+ }
+ } else if (!FeaturePolicyUtils::IsFeatureAllowed(doc,
+ u"display-capture"_ns)) {
+ disabled = true;
+ }
+ }
+
+ if (disabled) {
+ placeholderListener->Stop();
+ return StreamPromise::CreateAndReject(
+ MakeRefPtr<MediaMgrError>(MediaMgrError::Name::NotAllowedError),
+ __func__);
+ }
+ }
+
+ // Get list of all devices, with origin-specific device ids.
+
+ MediaEnginePrefs prefs = mPrefs;
+
+ nsString callID;
+ rv = GenerateUUID(callID);
+ MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
+
+ bool hasVideo = videoType != MediaSourceEnum::Other;
+ bool hasAudio = audioType != MediaSourceEnum::Other;
+
+ // Handle fake requests from content. For gUM we don't consider resist
+ // fingerprinting as users should be prompted anyway.
+ bool forceFakes = c.mFake.WasPassed() && c.mFake.Value();
+ // fake:true is effective only for microphone and camera devices, so
+ // permission must be requested for screen capture even if fake:true is set.
+ bool hasOnlyForcedFakes =
+ forceFakes && (!hasVideo || videoType == MediaSourceEnum::Camera) &&
+ (!hasAudio || audioType == MediaSourceEnum::Microphone);
+ bool askPermission =
+ (!privileged ||
+ Preferences::GetBool("media.navigator.permission.force")) &&
+ (!hasOnlyForcedFakes ||
+ Preferences::GetBool("media.navigator.permission.fake"));
+
+ LOG("%s: Preparing to enumerate devices. windowId=%" PRIu64
+ ", videoType=%" PRIu8 ", audioType=%" PRIu8
+ ", forceFakes=%s, askPermission=%s",
+ __func__, windowID, static_cast<uint8_t>(videoType),
+ static_cast<uint8_t>(audioType), forceFakes ? "true" : "false",
+ askPermission ? "true" : "false");
+
+ EnumerationFlags flags = EnumerationFlag::AllowPermissionRequest;
+ if (forceFakes) {
+ flags += EnumerationFlag::ForceFakes;
+ }
+ RefPtr<MediaManager> self = this;
+ return EnumerateDevicesImpl(aWindow, videoType, audioType, flags)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self, windowID, c, windowListener,
+ aCallerType](RefPtr<LocalMediaDeviceSetRefCnt> aDevices) {
+ LOG("GetUserMedia: post enumeration promise success callback "
+ "starting");
+ // Ensure that our windowID is still good.
+ RefPtr<nsPIDOMWindowInner> window =
+ nsGlobalWindowInner::GetInnerWindowWithId(windowID);
+ if (!window || !self->IsWindowListenerStillActive(windowListener)) {
+ LOG("GetUserMedia: bad window (%" PRIu64
+ ") in post enumeration success callback!",
+ windowID);
+ return LocalDeviceSetPromise::CreateAndReject(
+ MakeRefPtr<MediaMgrError>(MediaMgrError::Name::AbortError),
+ __func__);
+ }
+ // Apply any constraints. This modifies the passed-in list.
+ return self->SelectSettings(c, aCallerType, std::move(aDevices));
+ },
+ [](RefPtr<MediaMgrError>&& aError) {
+ LOG("GetUserMedia: post enumeration EnumerateDevicesImpl "
+ "failure callback called!");
+ return LocalDeviceSetPromise::CreateAndReject(std::move(aError),
+ __func__);
+ })
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self, windowID, c, windowListener, placeholderListener, hasAudio,
+ hasVideo, askPermission, prefs, isSecure, isHandlingUserInput,
+ callID, principalInfo, aCallerType, resistFingerprinting](
+ RefPtr<LocalMediaDeviceSetRefCnt> aDevices) mutable {
+ LOG("GetUserMedia: starting post enumeration promise2 success "
+ "callback!");
+
+ // Ensure that the window is still good.
+ RefPtr<nsPIDOMWindowInner> window =
+ nsGlobalWindowInner::GetInnerWindowWithId(windowID);
+ if (!window || !self->IsWindowListenerStillActive(windowListener)) {
+ LOG("GetUserMedia: bad window (%" PRIu64
+ ") in post enumeration success callback 2!",
+ windowID);
+ placeholderListener->Stop();
+ return StreamPromise::CreateAndReject(
+ MakeRefPtr<MediaMgrError>(MediaMgrError::Name::AbortError),
+ __func__);
+ }
+ if (!aDevices->Length()) {
+ LOG("GetUserMedia: no devices found in post enumeration promise2 "
+ "success callback! Calling error handler!");
+ placeholderListener->Stop();
+ // When privacy.resistFingerprinting = true, no
+ // available device implies content script is requesting
+ // a fake device, so report NotAllowedError.
+ auto error = resistFingerprinting
+ ? MediaMgrError::Name::NotAllowedError
+ : MediaMgrError::Name::NotFoundError;
+ return StreamPromise::CreateAndReject(
+ MakeRefPtr<MediaMgrError>(error), __func__);
+ }
+
+ // Time to start devices. Create the necessary device listeners and
+ // remove the placeholder.
+ RefPtr<DeviceListener> audioListener;
+ RefPtr<DeviceListener> videoListener;
+ if (hasAudio) {
+ audioListener = MakeRefPtr<DeviceListener>();
+ windowListener->Register(audioListener);
+ }
+ if (hasVideo) {
+ videoListener = MakeRefPtr<DeviceListener>();
+ windowListener->Register(videoListener);
+ }
+ placeholderListener->Stop();
+
+ bool focusSource = mozilla::Preferences::GetBool(
+ "media.getusermedia.window.focus_source.enabled", true);
+
+ // Incremental hack to compile. To be replaced by deeper
+ // refactoring. MediaManager allows
+ // "neither-resolve-nor-reject" semantics, so we cannot
+ // use MozPromiseHolder here.
+ MozPromiseHolder<StreamPromise> holder;
+ RefPtr<StreamPromise> p = holder.Ensure(__func__);
+
+ // Pass callbacks and listeners along to GetUserMediaStreamTask.
+ auto task = MakeRefPtr<GetUserMediaStreamTask>(
+ c, std::move(holder), windowID, std::move(windowListener),
+ std::move(audioListener), std::move(videoListener), prefs,
+ principalInfo, aCallerType, focusSource);
+
+ size_t taskCount =
+ self->AddTaskAndGetCount(windowID, callID, std::move(task));
+
+ if (!askPermission) {
+ self->NotifyAllowed(callID, *aDevices);
+ } else {
+ auto req = MakeRefPtr<GetUserMediaRequest>(
+ window, callID, std::move(aDevices), c, isSecure,
+ isHandlingUserInput);
+ if (!Preferences::GetBool("media.navigator.permission.force") &&
+ taskCount > 1) {
+ // there is at least 1 pending gUM request
+ // For the scarySources test case, always send the
+ // request
+ self->mPendingGUMRequest.AppendElement(req.forget());
+ } else {
+ nsCOMPtr<nsIObserverService> obs =
+ services::GetObserverService();
+ obs->NotifyObservers(req, "getUserMedia:request", nullptr);
+ }
+ }
+#ifdef MOZ_WEBRTC
+ EnableWebRtcLog();
+#endif
+ return p;
+ },
+ [placeholderListener](RefPtr<MediaMgrError>&& aError) {
+ LOG("GetUserMedia: post enumeration SelectSettings failure "
+ "callback called!");
+ placeholderListener->Stop();
+ return StreamPromise::CreateAndReject(std::move(aError), __func__);
+ });
+};
+
+RefPtr<LocalDeviceSetPromise> MediaManager::AnonymizeDevices(
+ nsPIDOMWindowInner* aWindow, RefPtr<const MediaDeviceSetRefCnt> aDevices) {
+ // Get an origin-key (for either regular or private browsing).
+ MOZ_ASSERT(NS_IsMainThread());
+ uint64_t windowId = aWindow->WindowID();
+ nsCOMPtr<nsIPrincipal> principal =
+ nsGlobalWindowInner::Cast(aWindow)->GetPrincipal();
+ MOZ_ASSERT(principal);
+ ipc::PrincipalInfo principalInfo;
+ nsresult rv = PrincipalToPrincipalInfo(principal, &principalInfo);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return LocalDeviceSetPromise::CreateAndReject(
+ MakeRefPtr<MediaMgrError>(MediaMgrError::Name::NotAllowedError),
+ __func__);
+ }
+ bool persist = IsActivelyCapturingOrHasAPermission(windowId);
+ return media::GetPrincipalKey(principalInfo, persist)
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [rawDevices = std::move(aDevices),
+ windowId](const nsCString& aOriginKey) {
+ MOZ_ASSERT(!aOriginKey.IsEmpty());
+ RefPtr anonymized = new LocalMediaDeviceSetRefCnt();
+ for (const RefPtr<MediaDevice>& device : *rawDevices) {
+ nsString id = device->mRawID;
+ // An empty id represents a virtual default device, for which
+ // the exposed deviceId is the empty string.
+ if (!id.IsEmpty()) {
+ nsContentUtils::AnonymizeId(id, aOriginKey);
+ }
+ nsString groupId = device->mRawGroupID;
+ // Use window id to salt group id in order to make it session
+ // based as required by the spec. This does not provide unique
+ // group ids through out a browser restart. However, this is not
+ // against the spec. Furthermore, since device ids are the same
+ // after a browser restart the fingerprint is not bigger.
+ groupId.AppendInt(windowId);
+ nsContentUtils::AnonymizeId(groupId, aOriginKey);
+
+ nsString name = device->mRawName;
+ if (name.Find(u"AirPods"_ns) != -1) {
+ name = u"AirPods"_ns;
+ }
+ anonymized->EmplaceBack(
+ new LocalMediaDevice(device, id, groupId, name));
+ }
+ return LocalDeviceSetPromise::CreateAndResolve(anonymized,
+ __func__);
+ },
+ [](nsresult rs) {
+ NS_WARNING("AnonymizeDevices failed to get Principal Key");
+ return LocalDeviceSetPromise::CreateAndReject(
+ MakeRefPtr<MediaMgrError>(MediaMgrError::Name::AbortError),
+ __func__);
+ });
+}
+
+RefPtr<LocalDeviceSetPromise> MediaManager::EnumerateDevicesImpl(
+ nsPIDOMWindowInner* aWindow, MediaSourceEnum aVideoInputType,
+ MediaSourceEnum aAudioInputType, EnumerationFlags aFlags) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ uint64_t windowId = aWindow->WindowID();
+ LOG("%s: windowId=%" PRIu64 ", aVideoInputType=%" PRIu8
+ ", aAudioInputType=%" PRIu8,
+ __func__, windowId, static_cast<uint8_t>(aVideoInputType),
+ static_cast<uint8_t>(aAudioInputType));
+
+ // To get a device list anonymized for a particular origin, we must:
+ // 1. Get the raw devices list
+ // 2. Anonymize the raw list with an origin-key.
+
+ // Add the window id here to check for that and abort silently if no longer
+ // exists.
+ RefPtr<GetUserMediaWindowListener> windowListener =
+ GetOrMakeWindowListener(aWindow);
+ MOZ_ASSERT(windowListener);
+ // Create an inactive DeviceListener to act as a placeholder, so the
+ // window listener doesn't clean itself up until we're done.
+ auto placeholderListener = MakeRefPtr<DeviceListener>();
+ windowListener->Register(placeholderListener);
+
+ return EnumerateRawDevices(aVideoInputType, aAudioInputType, aFlags)
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [self = RefPtr(this), this, window = nsCOMPtr(aWindow),
+ placeholderListener](RefPtr<MediaDeviceSetRefCnt> aDevices) mutable {
+ // Only run if window is still on our active list.
+ MediaManager* mgr = MediaManager::GetIfExists();
+ if (!mgr || placeholderListener->Stopped()) {
+ // The listener has already been removed if the window is no
+ // longer active.
+ return LocalDeviceSetPromise::CreateAndReject(
+ MakeRefPtr<MediaMgrError>(MediaMgrError::Name::AbortError),
+ __func__);
+ }
+ MOZ_ASSERT(mgr->IsWindowStillActive(window->WindowID()));
+ placeholderListener->Stop();
+ return AnonymizeDevices(window, aDevices);
+ },
+ [placeholderListener](RefPtr<MediaMgrError>&& aError) {
+ // EnumerateDevicesImpl may fail if a new doc has been set, in which
+ // case the OnNavigation() method should have removed all previous
+ // active listeners.
+ MOZ_ASSERT(placeholderListener->Stopped());
+ return LocalDeviceSetPromise::CreateAndReject(std::move(aError),
+ __func__);
+ });
+}
+
+RefPtr<LocalDevicePromise> MediaManager::SelectAudioOutput(
+ nsPIDOMWindowInner* aWindow, const dom::AudioOutputOptions& aOptions,
+ CallerType aCallerType) {
+ bool isHandlingUserInput = UserActivation::IsHandlingUserInput();
+ nsCOMPtr<nsIPrincipal> principal =
+ nsGlobalWindowInner::Cast(aWindow)->GetPrincipal();
+ if (!FeaturePolicyUtils::IsFeatureAllowed(aWindow->GetExtantDoc(),
+ u"speaker-selection"_ns)) {
+ return LocalDevicePromise::CreateAndReject(
+ MakeRefPtr<MediaMgrError>(
+ MediaMgrError::Name::NotAllowedError,
+ "Document's Permissions Policy does not allow selectAudioOutput()"),
+ __func__);
+ }
+ if (NS_WARN_IF(!principal)) {
+ return LocalDevicePromise::CreateAndReject(
+ MakeRefPtr<MediaMgrError>(MediaMgrError::Name::SecurityError),
+ __func__);
+ }
+ // Disallow access to null principal.
+ if (principal->GetIsNullPrincipal()) {
+ return LocalDevicePromise::CreateAndReject(
+ MakeRefPtr<MediaMgrError>(MediaMgrError::Name::NotAllowedError),
+ __func__);
+ }
+ ipc::PrincipalInfo principalInfo;
+ nsresult rv = PrincipalToPrincipalInfo(principal, &principalInfo);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return LocalDevicePromise::CreateAndReject(
+ MakeRefPtr<MediaMgrError>(MediaMgrError::Name::SecurityError),
+ __func__);
+ }
+ uint64_t windowID = aWindow->WindowID();
+ const bool resistFingerprinting =
+ aWindow->AsGlobal()->ShouldResistFingerprinting(aCallerType,
+ RFPTarget::Unknown);
+ return EnumerateDevicesImpl(aWindow, MediaSourceEnum::Other,
+ MediaSourceEnum::Other,
+ {EnumerationFlag::EnumerateAudioOutputs,
+ EnumerationFlag::AllowPermissionRequest})
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr<MediaManager>(this), windowID, aOptions, aCallerType,
+ resistFingerprinting, isHandlingUserInput,
+ principalInfo](RefPtr<LocalMediaDeviceSetRefCnt> aDevices) mutable {
+ // Ensure that the window is still good.
+ RefPtr<nsPIDOMWindowInner> window =
+ nsGlobalWindowInner::GetInnerWindowWithId(windowID);
+ if (!window) {
+ LOG("SelectAudioOutput: bad window (%" PRIu64
+ ") in post enumeration success callback!",
+ windowID);
+ return LocalDevicePromise::CreateAndReject(
+ MakeRefPtr<MediaMgrError>(MediaMgrError::Name::AbortError),
+ __func__);
+ }
+ if (aDevices->IsEmpty()) {
+ LOG("SelectAudioOutput: no devices found");
+ auto error = resistFingerprinting
+ ? MediaMgrError::Name::NotAllowedError
+ : MediaMgrError::Name::NotFoundError;
+ return LocalDevicePromise::CreateAndReject(
+ MakeRefPtr<MediaMgrError>(error), __func__);
+ }
+ MozPromiseHolder<LocalDevicePromise> holder;
+ RefPtr<LocalDevicePromise> p = holder.Ensure(__func__);
+ auto task = MakeRefPtr<SelectAudioOutputTask>(
+ std::move(holder), windowID, aCallerType, principalInfo);
+ nsString callID;
+ nsresult rv = GenerateUUID(callID);
+ MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
+ size_t taskCount =
+ self->AddTaskAndGetCount(windowID, callID, std::move(task));
+ bool askPermission =
+ !Preferences::GetBool("media.navigator.permission.disabled") ||
+ Preferences::GetBool("media.navigator.permission.force");
+ if (!askPermission) {
+ self->NotifyAllowed(callID, *aDevices);
+ } else {
+ MOZ_ASSERT(window->IsSecureContext());
+ auto req = MakeRefPtr<GetUserMediaRequest>(
+ window, callID, std::move(aDevices), aOptions, true,
+ isHandlingUserInput);
+ if (taskCount > 1) {
+ // there is at least 1 pending gUM request
+ self->mPendingGUMRequest.AppendElement(req.forget());
+ } else {
+ nsCOMPtr<nsIObserverService> obs =
+ services::GetObserverService();
+ obs->NotifyObservers(req, "getUserMedia:request", nullptr);
+ }
+ }
+ return p;
+ },
+ [](RefPtr<MediaMgrError> aError) {
+ LOG("SelectAudioOutput: EnumerateDevicesImpl "
+ "failure callback called!");
+ return LocalDevicePromise::CreateAndReject(std::move(aError),
+ __func__);
+ });
+}
+
+MediaEngine* MediaManager::GetBackend() {
+ MOZ_ASSERT(MediaManager::IsInMediaThread());
+ // Plugin backends as appropriate. The default engine also currently
+ // includes picture support for Android.
+ // This IS called off main-thread.
+ if (!mBackend) {
+#if defined(MOZ_WEBRTC)
+ mBackend = new MediaEngineWebRTC();
+#else
+ mBackend = new MediaEngineFake();
+#endif
+ mDeviceListChangeListener = mBackend->DeviceListChangeEvent().Connect(
+ AbstractThread::MainThread(), this, &MediaManager::DeviceListChanged);
+ }
+ return mBackend;
+}
+
+void MediaManager::OnNavigation(uint64_t aWindowID) {
+ MOZ_ASSERT(NS_IsMainThread());
+ LOG("OnNavigation for %" PRIu64, aWindowID);
+
+ // Stop the streams for this window. The runnables check this value before
+ // making a call to content.
+
+ nsTArray<nsString>* callIDs;
+ if (mCallIds.Get(aWindowID, &callIDs)) {
+ for (auto& callID : *callIDs) {
+ mActiveCallbacks.Remove(callID);
+ for (auto& request : mPendingGUMRequest.Clone()) {
+ nsString id;
+ request->GetCallID(id);
+ if (id == callID) {
+ mPendingGUMRequest.RemoveElement(request);
+ }
+ }
+ }
+ mCallIds.Remove(aWindowID);
+ }
+
+ if (RefPtr<GetUserMediaWindowListener> listener =
+ GetWindowListener(aWindowID)) {
+ listener->RemoveAll();
+ }
+ MOZ_ASSERT(!GetWindowListener(aWindowID));
+}
+
+void MediaManager::OnCameraMute(bool aMute) {
+ MOZ_ASSERT(NS_IsMainThread());
+ LOG("OnCameraMute for all windows");
+ mCamerasMuted = aMute;
+ // This is safe since we're on main-thread, and the windowlist can only
+ // be added to from the main-thread
+ for (const auto& window : mActiveWindows.Values()) {
+ window->MuteOrUnmuteCameras(aMute);
+ }
+}
+
+void MediaManager::OnMicrophoneMute(bool aMute) {
+ MOZ_ASSERT(NS_IsMainThread());
+ LOG("OnMicrophoneMute for all windows");
+ mMicrophonesMuted = aMute;
+ // This is safe since we're on main-thread, and the windowlist can only
+ // be added to from the main-thread
+ for (const auto& window : mActiveWindows.Values()) {
+ window->MuteOrUnmuteMicrophones(aMute);
+ }
+}
+
+RefPtr<GetUserMediaWindowListener> MediaManager::GetOrMakeWindowListener(
+ nsPIDOMWindowInner* aWindow) {
+ Document* doc = aWindow->GetExtantDoc();
+ if (!doc) {
+ // The window has been destroyed. Destroyed windows don't have listeners.
+ return nullptr;
+ }
+ nsIPrincipal* principal = doc->NodePrincipal();
+ uint64_t windowId = aWindow->WindowID();
+ RefPtr<GetUserMediaWindowListener> windowListener =
+ GetWindowListener(windowId);
+ if (windowListener) {
+ MOZ_ASSERT(PrincipalHandleMatches(windowListener->GetPrincipalHandle(),
+ principal));
+ } else {
+ windowListener = new GetUserMediaWindowListener(
+ windowId, MakePrincipalHandle(principal));
+ AddWindowID(windowId, windowListener);
+ }
+ return windowListener;
+}
+
+void MediaManager::AddWindowID(uint64_t aWindowId,
+ RefPtr<GetUserMediaWindowListener> aListener) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // Store the WindowID in a hash table and mark as active. The entry is removed
+ // when this window is closed or navigated away from.
+ // This is safe since we're on main-thread, and the windowlist can only
+ // be invalidated from the main-thread (see OnNavigation)
+ if (IsWindowStillActive(aWindowId)) {
+ MOZ_ASSERT(false, "Window already added");
+ return;
+ }
+
+ aListener->MuteOrUnmuteCameras(mCamerasMuted);
+ aListener->MuteOrUnmuteMicrophones(mMicrophonesMuted);
+ GetActiveWindows()->InsertOrUpdate(aWindowId, std::move(aListener));
+
+ RefPtr<WindowGlobalChild> wgc =
+ WindowGlobalChild::GetByInnerWindowId(aWindowId);
+ if (wgc) {
+ wgc->BlockBFCacheFor(BFCacheStatus::ACTIVE_GET_USER_MEDIA);
+ }
+}
+
+void MediaManager::RemoveWindowID(uint64_t aWindowId) {
+ RefPtr<WindowGlobalChild> wgc =
+ WindowGlobalChild::GetByInnerWindowId(aWindowId);
+ if (wgc) {
+ wgc->UnblockBFCacheFor(BFCacheStatus::ACTIVE_GET_USER_MEDIA);
+ }
+
+ mActiveWindows.Remove(aWindowId);
+
+ // get outer windowID
+ auto* window = nsGlobalWindowInner::GetInnerWindowWithId(aWindowId);
+ if (!window) {
+ LOG("No inner window for %" PRIu64, aWindowId);
+ return;
+ }
+
+ auto* outer = window->GetOuterWindow();
+ if (!outer) {
+ LOG("No outer window for inner %" PRIu64, aWindowId);
+ return;
+ }
+
+ uint64_t outerID = outer->WindowID();
+
+ // Notify the UI that this window no longer has gUM active
+ char windowBuffer[32];
+ SprintfLiteral(windowBuffer, "%" PRIu64, outerID);
+ nsString data = NS_ConvertUTF8toUTF16(windowBuffer);
+
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ obs->NotifyWhenScriptSafe(nullptr, "recording-window-ended", data.get());
+ LOG("Sent recording-window-ended for window %" PRIu64 " (outer %" PRIu64 ")",
+ aWindowId, outerID);
+}
+
+bool MediaManager::IsWindowListenerStillActive(
+ const RefPtr<GetUserMediaWindowListener>& aListener) {
+ MOZ_DIAGNOSTIC_ASSERT(aListener);
+ return aListener && aListener == GetWindowListener(aListener->WindowID());
+}
+
+void MediaManager::GetPref(nsIPrefBranch* aBranch, const char* aPref,
+ const char* aData, int32_t* aVal) {
+ int32_t temp;
+ if (aData == nullptr || strcmp(aPref, aData) == 0) {
+ if (NS_SUCCEEDED(aBranch->GetIntPref(aPref, &temp))) {
+ *aVal = temp;
+ }
+ }
+}
+
+void MediaManager::GetPrefBool(nsIPrefBranch* aBranch, const char* aPref,
+ const char* aData, bool* aVal) {
+ bool temp;
+ if (aData == nullptr || strcmp(aPref, aData) == 0) {
+ if (NS_SUCCEEDED(aBranch->GetBoolPref(aPref, &temp))) {
+ *aVal = temp;
+ }
+ }
+}
+
+void MediaManager::GetPrefs(nsIPrefBranch* aBranch, const char* aData) {
+ GetPref(aBranch, "media.navigator.video.default_width", aData,
+ &mPrefs.mWidth);
+ GetPref(aBranch, "media.navigator.video.default_height", aData,
+ &mPrefs.mHeight);
+ GetPref(aBranch, "media.navigator.video.default_fps", aData, &mPrefs.mFPS);
+ GetPref(aBranch, "media.navigator.audio.fake_frequency", aData,
+ &mPrefs.mFreq);
+#ifdef MOZ_WEBRTC
+ GetPrefBool(aBranch, "media.getusermedia.aec_enabled", aData, &mPrefs.mAecOn);
+ GetPrefBool(aBranch, "media.getusermedia.agc_enabled", aData, &mPrefs.mAgcOn);
+ GetPrefBool(aBranch, "media.getusermedia.hpf_enabled", aData, &mPrefs.mHPFOn);
+ GetPrefBool(aBranch, "media.getusermedia.noise_enabled", aData,
+ &mPrefs.mNoiseOn);
+ GetPrefBool(aBranch, "media.getusermedia.transient_enabled", aData,
+ &mPrefs.mTransientOn);
+ GetPrefBool(aBranch, "media.getusermedia.residual_echo_enabled", aData,
+ &mPrefs.mResidualEchoOn);
+ GetPrefBool(aBranch, "media.getusermedia.agc2_forced", aData,
+ &mPrefs.mAgc2Forced);
+ GetPref(aBranch, "media.getusermedia.agc", aData, &mPrefs.mAgc);
+ GetPref(aBranch, "media.getusermedia.noise", aData, &mPrefs.mNoise);
+ GetPref(aBranch, "media.getusermedia.channels", aData, &mPrefs.mChannels);
+#endif
+}
+
+void MediaManager::Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (sHasMainThreadShutdown) {
+ return;
+ }
+
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+
+ obs->RemoveObserver(this, "last-pb-context-exited");
+ obs->RemoveObserver(this, "getUserMedia:privileged:allow");
+ obs->RemoveObserver(this, "getUserMedia:response:allow");
+ obs->RemoveObserver(this, "getUserMedia:response:deny");
+ obs->RemoveObserver(this, "getUserMedia:response:noOSPermission");
+ obs->RemoveObserver(this, "getUserMedia:revoke");
+ obs->RemoveObserver(this, "getUserMedia:muteVideo");
+ obs->RemoveObserver(this, "getUserMedia:unmuteVideo");
+ obs->RemoveObserver(this, "getUserMedia:muteAudio");
+ obs->RemoveObserver(this, "getUserMedia:unmuteAudio");
+ obs->RemoveObserver(this, "application-background");
+ obs->RemoveObserver(this, "application-foreground");
+
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ if (prefs) {
+ ForeachObservedPref([&](const nsLiteralCString& aPrefName) {
+ prefs->RemoveObserver(aPrefName, this);
+ });
+ }
+
+ if (mDeviceChangeTimer) {
+ mDeviceChangeTimer->Cancel();
+ // Drop ref to MediaTimer early to avoid blocking SharedThreadPool shutdown
+ mDeviceChangeTimer = nullptr;
+ }
+
+ {
+ // Close off any remaining active windows.
+
+ // Live capture at this point is rare but can happen. Stopping it will make
+ // the window listeners attempt to remove themselves from the active windows
+ // table. We cannot touch the table at point so we grab a copy of the window
+ // listeners first.
+ const auto listeners = ToArray(GetActiveWindows()->Values());
+ for (const auto& listener : listeners) {
+ listener->RemoveAll();
+ }
+ }
+ MOZ_ASSERT(GetActiveWindows()->Count() == 0);
+
+ GetActiveWindows()->Clear();
+ mActiveCallbacks.Clear();
+ mCallIds.Clear();
+ mPendingGUMRequest.Clear();
+#ifdef MOZ_WEBRTC
+ StopWebRtcLog();
+#endif
+
+ // From main thread's point of view, shutdown is now done.
+ // All that remains is shutting down the media thread.
+ sHasMainThreadShutdown = true;
+
+ // Release the backend (and call Shutdown()) from within mMediaThread.
+ // Don't use MediaManager::Dispatch() because we're
+ // sHasMainThreadShutdown == true here!
+ MOZ_ALWAYS_SUCCEEDS(mMediaThread->Dispatch(
+ NS_NewRunnableFunction(__func__, [self = RefPtr(this), this]() {
+ LOG("MediaManager Thread Shutdown");
+ MOZ_ASSERT(IsInMediaThread());
+ // Must shutdown backend on MediaManager thread, since that's
+ // where we started it from!
+ if (mBackend) {
+ mBackend->Shutdown(); // idempotent
+ mDeviceListChangeListener.DisconnectIfExists();
+ }
+ // last reference, will invoke Shutdown() again
+ mBackend = nullptr;
+ })));
+
+ // note that this == sSingleton
+ MOZ_ASSERT(this == sSingleton);
+
+ // Explicitly shut down the TaskQueue so that it releases its
+ // SharedThreadPool when all tasks have completed. SharedThreadPool blocks
+ // XPCOM shutdown from proceeding beyond "xpcom-shutdown-threads" until all
+ // SharedThreadPools are released, but the nsComponentManager keeps a
+ // reference to the MediaManager for the nsIMediaManagerService until much
+ // later in shutdown. This also provides additional assurance that no
+ // further tasks will be queued.
+ mMediaThread->BeginShutdown()->Then(
+ GetMainThreadSerialEventTarget(), __func__, [] {
+ LOG("MediaManager shutdown lambda running, releasing MediaManager "
+ "singleton");
+ // Remove async shutdown blocker
+ media::MustGetShutdownBarrier()->RemoveBlocker(
+ sSingleton->mShutdownBlocker);
+
+ sSingleton = nullptr;
+ });
+}
+
+void MediaManager::SendPendingGUMRequest() {
+ if (mPendingGUMRequest.Length() > 0) {
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ obs->NotifyObservers(mPendingGUMRequest[0], "getUserMedia:request",
+ nullptr);
+ mPendingGUMRequest.RemoveElementAt(0);
+ }
+}
+
+bool IsGUMResponseNoAccess(const char* aTopic,
+ MediaMgrError::Name& aErrorName) {
+ if (!strcmp(aTopic, "getUserMedia:response:deny")) {
+ aErrorName = MediaMgrError::Name::NotAllowedError;
+ return true;
+ }
+
+ if (!strcmp(aTopic, "getUserMedia:response:noOSPermission")) {
+ aErrorName = MediaMgrError::Name::NotFoundError;
+ return true;
+ }
+
+ return false;
+}
+
+static MediaSourceEnum ParseScreenColonWindowID(const char16_t* aData,
+ uint64_t* aWindowIDOut) {
+ MOZ_ASSERT(aWindowIDOut);
+ // may be windowid or screen:windowid
+ const nsDependentString data(aData);
+ if (Substring(data, 0, strlen("screen:")).EqualsLiteral("screen:")) {
+ nsresult rv;
+ *aWindowIDOut = Substring(data, strlen("screen:")).ToInteger64(&rv);
+ MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
+ return MediaSourceEnum::Screen;
+ }
+ nsresult rv;
+ *aWindowIDOut = data.ToInteger64(&rv);
+ MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
+ return MediaSourceEnum::Camera;
+}
+
+nsresult MediaManager::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ MediaMgrError::Name gumNoAccessError = MediaMgrError::Name::NotAllowedError;
+
+ if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) {
+ nsCOMPtr<nsIPrefBranch> branch(do_QueryInterface(aSubject));
+ if (branch) {
+ GetPrefs(branch, NS_ConvertUTF16toUTF8(aData).get());
+ LOG("%s: %dx%d @%dfps", __FUNCTION__, mPrefs.mWidth, mPrefs.mHeight,
+ mPrefs.mFPS);
+ DeviceListChanged();
+ }
+ } else if (!strcmp(aTopic, "last-pb-context-exited")) {
+ // Clear memory of private-browsing-specific deviceIds. Fire and forget.
+ media::SanitizeOriginKeys(0, true);
+ return NS_OK;
+ } else if (!strcmp(aTopic, "getUserMedia:got-device-permission")) {
+ MOZ_ASSERT(aSubject);
+ nsCOMPtr<nsIRunnable> task = do_QueryInterface(aSubject);
+ MediaManager::Dispatch(NewTaskFrom([task] { task->Run(); }));
+ return NS_OK;
+ } else if (!strcmp(aTopic, "getUserMedia:privileged:allow") ||
+ !strcmp(aTopic, "getUserMedia:response:allow")) {
+ nsString key(aData);
+ RefPtr<GetUserMediaTask> task = TakeGetUserMediaTask(key);
+ if (!task) {
+ return NS_OK;
+ }
+
+ if (sHasMainThreadShutdown) {
+ task->Denied(MediaMgrError::Name::AbortError, "In shutdown"_ns);
+ return NS_OK;
+ }
+ if (NS_WARN_IF(!aSubject)) {
+ return NS_ERROR_FAILURE; // ignored
+ }
+ // Permission has been granted. aSubject contains the particular device
+ // or devices selected and approved by the user, if any.
+ nsCOMPtr<nsIArray> array(do_QueryInterface(aSubject));
+ MOZ_ASSERT(array);
+ uint32_t len = 0;
+ array->GetLength(&len);
+ RefPtr<LocalMediaDevice> audioInput;
+ RefPtr<LocalMediaDevice> videoInput;
+ RefPtr<LocalMediaDevice> audioOutput;
+ for (uint32_t i = 0; i < len; i++) {
+ nsCOMPtr<nsIMediaDevice> device;
+ array->QueryElementAt(i, NS_GET_IID(nsIMediaDevice),
+ getter_AddRefs(device));
+ MOZ_ASSERT(device); // shouldn't be returning anything else...
+ if (!device) {
+ continue;
+ }
+
+ // Casting here is safe because a LocalMediaDevice is created
+ // only in Gecko side, JS can only query for an instance.
+ auto* dev = static_cast<LocalMediaDevice*>(device.get());
+ switch (dev->Kind()) {
+ case MediaDeviceKind::Videoinput:
+ if (!videoInput) {
+ videoInput = dev;
+ }
+ break;
+ case MediaDeviceKind::Audioinput:
+ if (!audioInput) {
+ audioInput = dev;
+ }
+ break;
+ case MediaDeviceKind::Audiooutput:
+ if (!audioOutput) {
+ audioOutput = dev;
+ }
+ break;
+ default:
+ MOZ_CRASH("Unexpected device kind");
+ }
+ }
+
+ if (GetUserMediaStreamTask* streamTask = task->AsGetUserMediaStreamTask()) {
+ bool needVideo = IsOn(streamTask->GetConstraints().mVideo);
+ bool needAudio = IsOn(streamTask->GetConstraints().mAudio);
+ MOZ_ASSERT(needVideo || needAudio);
+
+ if ((needVideo && !videoInput) || (needAudio && !audioInput)) {
+ task->Denied(MediaMgrError::Name::NotAllowedError);
+ return NS_OK;
+ }
+ streamTask->Allowed(std::move(audioInput), std::move(videoInput));
+ return NS_OK;
+ }
+ if (SelectAudioOutputTask* outputTask = task->AsSelectAudioOutputTask()) {
+ if (!audioOutput) {
+ task->Denied(MediaMgrError::Name::NotAllowedError);
+ return NS_OK;
+ }
+ outputTask->Allowed(std::move(audioOutput));
+ return NS_OK;
+ }
+
+ NS_WARNING("Unknown task type in getUserMedia");
+ return NS_ERROR_FAILURE;
+
+ } else if (IsGUMResponseNoAccess(aTopic, gumNoAccessError)) {
+ nsString key(aData);
+ RefPtr<GetUserMediaTask> task = TakeGetUserMediaTask(key);
+ if (task) {
+ task->Denied(gumNoAccessError);
+ SendPendingGUMRequest();
+ }
+ return NS_OK;
+
+ } else if (!strcmp(aTopic, "getUserMedia:revoke")) {
+ uint64_t windowID;
+ if (ParseScreenColonWindowID(aData, &windowID) == MediaSourceEnum::Screen) {
+ LOG("Revoking ScreenCapture access for window %" PRIu64, windowID);
+ StopScreensharing(windowID);
+ } else {
+ LOG("Revoking MediaCapture access for window %" PRIu64, windowID);
+ OnNavigation(windowID);
+ }
+ return NS_OK;
+ } else if (!strcmp(aTopic, "getUserMedia:muteVideo") ||
+ !strcmp(aTopic, "getUserMedia:unmuteVideo")) {
+ OnCameraMute(!strcmp(aTopic, "getUserMedia:muteVideo"));
+ return NS_OK;
+ } else if (!strcmp(aTopic, "getUserMedia:muteAudio") ||
+ !strcmp(aTopic, "getUserMedia:unmuteAudio")) {
+ OnMicrophoneMute(!strcmp(aTopic, "getUserMedia:muteAudio"));
+ return NS_OK;
+ } else if ((!strcmp(aTopic, "application-background") ||
+ !strcmp(aTopic, "application-foreground")) &&
+ StaticPrefs::media_getusermedia_camera_background_mute_enabled()) {
+ // On mobile we turn off any cameras (but not mics) while in the background.
+ // Keeping things simple for now by duplicating test-covered code above.
+ //
+ // NOTE: If a mobile device ever wants to implement "getUserMedia:muteVideo"
+ // as well, it'd need to update this code to handle & test the combinations.
+ OnCameraMute(!strcmp(aTopic, "application-background"));
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MediaManager::CollectReports(nsIHandleReportCallback* aHandleReport,
+ nsISupports* aData, bool aAnonymize) {
+ size_t amount = 0;
+ amount += mActiveWindows.ShallowSizeOfExcludingThis(MallocSizeOf);
+ for (const GetUserMediaWindowListener* listener : mActiveWindows.Values()) {
+ amount += listener->SizeOfIncludingThis(MallocSizeOf);
+ }
+ amount += mActiveCallbacks.ShallowSizeOfExcludingThis(MallocSizeOf);
+ for (const GetUserMediaTask* task : mActiveCallbacks.Values()) {
+ // Assume nsString buffers for keys are accounted in mCallIds.
+ amount += task->SizeOfIncludingThis(MallocSizeOf);
+ }
+ amount += mCallIds.ShallowSizeOfExcludingThis(MallocSizeOf);
+ for (const auto& array : mCallIds.Values()) {
+ amount += array->ShallowSizeOfExcludingThis(MallocSizeOf);
+ for (const nsString& callID : *array) {
+ amount += callID.SizeOfExcludingThisEvenIfShared(MallocSizeOf);
+ }
+ }
+ amount += mPendingGUMRequest.ShallowSizeOfExcludingThis(MallocSizeOf);
+ // GetUserMediaRequest pointees of mPendingGUMRequest do not have support
+ // for memory accounting. mPendingGUMRequest logic should probably be moved
+ // to the front end (bug 1691625).
+ MOZ_COLLECT_REPORT("explicit/media/media-manager-aggregates", KIND_HEAP,
+ UNITS_BYTES, amount,
+ "Memory used by MediaManager variable length members.");
+ return NS_OK;
+}
+
+nsresult MediaManager::GetActiveMediaCaptureWindows(nsIArray** aArray) {
+ MOZ_ASSERT(aArray);
+
+ nsCOMPtr<nsIMutableArray> array = nsArray::Create();
+
+ for (const auto& entry : mActiveWindows) {
+ const uint64_t& id = entry.GetKey();
+ RefPtr<GetUserMediaWindowListener> winListener = entry.GetData();
+ if (!winListener) {
+ continue;
+ }
+
+ auto* window = nsGlobalWindowInner::GetInnerWindowWithId(id);
+ MOZ_ASSERT(window);
+ // XXXkhuey ...
+ if (!window) {
+ continue;
+ }
+
+ if (winListener->CapturingVideo() || winListener->CapturingAudio()) {
+ array->AppendElement(ToSupports(window));
+ }
+ }
+
+ array.forget(aArray);
+ return NS_OK;
+}
+
+struct CaptureWindowStateData {
+ uint16_t* mCamera;
+ uint16_t* mMicrophone;
+ uint16_t* mScreenShare;
+ uint16_t* mWindowShare;
+ uint16_t* mAppShare;
+ uint16_t* mBrowserShare;
+};
+
+NS_IMETHODIMP
+MediaManager::MediaCaptureWindowState(
+ nsIDOMWindow* aCapturedWindow, uint16_t* aCamera, uint16_t* aMicrophone,
+ uint16_t* aScreen, uint16_t* aWindow, uint16_t* aBrowser,
+ nsTArray<RefPtr<nsIMediaDevice>>& aDevices) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ CaptureState camera = CaptureState::Off;
+ CaptureState microphone = CaptureState::Off;
+ CaptureState screen = CaptureState::Off;
+ CaptureState window = CaptureState::Off;
+ CaptureState browser = CaptureState::Off;
+ RefPtr<LocalMediaDeviceSetRefCnt> devices;
+
+ nsCOMPtr<nsPIDOMWindowInner> piWin = do_QueryInterface(aCapturedWindow);
+ if (piWin) {
+ if (RefPtr<GetUserMediaWindowListener> listener =
+ GetWindowListener(piWin->WindowID())) {
+ camera = listener->CapturingSource(MediaSourceEnum::Camera);
+ microphone = listener->CapturingSource(MediaSourceEnum::Microphone);
+ screen = listener->CapturingSource(MediaSourceEnum::Screen);
+ window = listener->CapturingSource(MediaSourceEnum::Window);
+ browser = listener->CapturingSource(MediaSourceEnum::Browser);
+ devices = listener->GetDevices();
+ }
+ }
+
+ *aCamera = FromCaptureState(camera);
+ *aMicrophone = FromCaptureState(microphone);
+ *aScreen = FromCaptureState(screen);
+ *aWindow = FromCaptureState(window);
+ *aBrowser = FromCaptureState(browser);
+ if (devices) {
+ for (auto& device : *devices) {
+ aDevices.AppendElement(device);
+ }
+ }
+
+ LOG("%s: window %" PRIu64 " capturing %s %s %s %s %s", __FUNCTION__,
+ piWin ? piWin->WindowID() : -1,
+ *aCamera == nsIMediaManagerService::STATE_CAPTURE_ENABLED
+ ? "camera (enabled)"
+ : (*aCamera == nsIMediaManagerService::STATE_CAPTURE_DISABLED
+ ? "camera (disabled)"
+ : ""),
+ *aMicrophone == nsIMediaManagerService::STATE_CAPTURE_ENABLED
+ ? "microphone (enabled)"
+ : (*aMicrophone == nsIMediaManagerService::STATE_CAPTURE_DISABLED
+ ? "microphone (disabled)"
+ : ""),
+ *aScreen ? "screenshare" : "", *aWindow ? "windowshare" : "",
+ *aBrowser ? "browsershare" : "");
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MediaManager::SanitizeDeviceIds(int64_t aSinceWhen) {
+ MOZ_ASSERT(NS_IsMainThread());
+ LOG("%s: sinceWhen = %" PRId64, __FUNCTION__, aSinceWhen);
+
+ media::SanitizeOriginKeys(aSinceWhen, false); // we fire and forget
+ return NS_OK;
+}
+
+void MediaManager::StopScreensharing(uint64_t aWindowID) {
+ // We need to stop window/screensharing for all streams in this innerwindow.
+
+ if (RefPtr<GetUserMediaWindowListener> listener =
+ GetWindowListener(aWindowID)) {
+ listener->StopSharing();
+ }
+}
+
+bool MediaManager::IsActivelyCapturingOrHasAPermission(uint64_t aWindowId) {
+ // Does page currently have a gUM stream active?
+
+ nsCOMPtr<nsIArray> array;
+ GetActiveMediaCaptureWindows(getter_AddRefs(array));
+ uint32_t len;
+ array->GetLength(&len);
+ for (uint32_t i = 0; i < len; i++) {
+ nsCOMPtr<nsPIDOMWindowInner> win;
+ array->QueryElementAt(i, NS_GET_IID(nsPIDOMWindowInner),
+ getter_AddRefs(win));
+ if (win && win->WindowID() == aWindowId) {
+ return true;
+ }
+ }
+
+ // Or are persistent permissions (audio or video) granted?
+
+ auto* window = nsGlobalWindowInner::GetInnerWindowWithId(aWindowId);
+ if (NS_WARN_IF(!window) || NS_WARN_IF(!window->GetPrincipal())) {
+ return false;
+ }
+
+ Document* doc = window->GetExtantDoc();
+ if (NS_WARN_IF(!doc)) {
+ return false;
+ }
+
+ nsIPrincipal* principal = window->GetPrincipal();
+ if (NS_WARN_IF(!principal)) {
+ return false;
+ }
+
+ // Check if this site has persistent permissions.
+ nsresult rv;
+ RefPtr<PermissionDelegateHandler> permDelegate =
+ doc->GetPermissionDelegateHandler();
+ if (NS_WARN_IF(!permDelegate)) {
+ return false;
+ }
+
+ uint32_t audio = nsIPermissionManager::UNKNOWN_ACTION;
+ uint32_t video = nsIPermissionManager::UNKNOWN_ACTION;
+ {
+ rv = permDelegate->GetPermission("microphone"_ns, &audio, true);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+ rv = permDelegate->GetPermission("camera"_ns, &video, true);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+ }
+ return audio == nsIPermissionManager::ALLOW_ACTION ||
+ video == nsIPermissionManager::ALLOW_ACTION;
+}
+
+DeviceListener::DeviceListener()
+ : mStopped(false),
+ mMainThreadCheck(nullptr),
+ mPrincipalHandle(PRINCIPAL_HANDLE_NONE),
+ mWindowListener(nullptr) {}
+
+void DeviceListener::Register(GetUserMediaWindowListener* aListener) {
+ LOG("DeviceListener %p registering with window listener %p", this, aListener);
+
+ MOZ_ASSERT(aListener, "No listener");
+ MOZ_ASSERT(!mWindowListener, "Already registered");
+ MOZ_ASSERT(!Activated(), "Already activated");
+
+ mPrincipalHandle = aListener->GetPrincipalHandle();
+ mWindowListener = aListener;
+}
+
+void DeviceListener::Activate(RefPtr<LocalMediaDevice> aDevice,
+ RefPtr<LocalTrackSource> aTrackSource,
+ bool aStartMuted) {
+ MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");
+
+ LOG("DeviceListener %p activating %s device %p", this,
+ nsCString(dom::MediaDeviceKindValues::GetString(aDevice->Kind())).get(),
+ aDevice.get());
+
+ MOZ_ASSERT(!mStopped, "Cannot activate stopped device listener");
+ MOZ_ASSERT(!Activated(), "Already activated");
+
+ mMainThreadCheck = PR_GetCurrentThread();
+ bool offWhileDisabled =
+ (aDevice->GetMediaSource() == MediaSourceEnum::Microphone &&
+ Preferences::GetBool(
+ "media.getusermedia.microphone.off_while_disabled.enabled", true)) ||
+ (aDevice->GetMediaSource() == MediaSourceEnum::Camera &&
+ Preferences::GetBool(
+ "media.getusermedia.camera.off_while_disabled.enabled", true));
+ mDeviceState = MakeUnique<DeviceState>(
+ std::move(aDevice), std::move(aTrackSource), offWhileDisabled);
+ mDeviceState->mDeviceMuted = aStartMuted;
+ if (aStartMuted) {
+ mDeviceState->mTrackSource->Mute();
+ }
+}
+
+RefPtr<DeviceListener::DeviceListenerPromise>
+DeviceListener::InitializeAsync() {
+ MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");
+ MOZ_DIAGNOSTIC_ASSERT(!mStopped);
+
+ return MediaManager::Dispatch<DeviceListenerPromise>(
+ __func__,
+ [principal = GetPrincipalHandle(), device = mDeviceState->mDevice,
+ track = mDeviceState->mTrackSource->mTrack,
+ deviceMuted = mDeviceState->mDeviceMuted](
+ MozPromiseHolder<DeviceListenerPromise>& aHolder) {
+ auto kind = device->Kind();
+ device->SetTrack(track, principal);
+ nsresult rv = deviceMuted ? NS_OK : device->Start();
+ if (kind == MediaDeviceKind::Audioinput ||
+ kind == MediaDeviceKind::Videoinput) {
+ if ((rv == NS_ERROR_NOT_AVAILABLE &&
+ kind == MediaDeviceKind::Audioinput) ||
+ (NS_FAILED(rv) && kind == MediaDeviceKind::Videoinput)) {
+ PR_Sleep(200);
+ rv = device->Start();
+ }
+ if (rv == NS_ERROR_NOT_AVAILABLE &&
+ kind == MediaDeviceKind::Audioinput) {
+ nsCString log;
+ log.AssignLiteral("Concurrent mic process limit.");
+ aHolder.Reject(MakeRefPtr<MediaMgrError>(
+ MediaMgrError::Name::NotReadableError,
+ std::move(log)),
+ __func__);
+ return;
+ }
+ }
+ if (NS_FAILED(rv)) {
+ nsCString log;
+ log.AppendPrintf(
+ "Starting %s failed",
+ nsCString(dom::MediaDeviceKindValues::GetString(kind))
+ .get());
+ aHolder.Reject(
+ MakeRefPtr<MediaMgrError>(MediaMgrError::Name::AbortError,
+ std::move(log)),
+ __func__);
+ return;
+ }
+ LOG("started %s device %p",
+ nsCString(dom::MediaDeviceKindValues::GetString(kind)).get(),
+ device.get());
+ aHolder.Resolve(true, __func__);
+ })
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [self = RefPtr<DeviceListener>(this), this]() {
+ if (mStopped) {
+ // We were shut down during the async init
+ return DeviceListenerPromise::CreateAndResolve(true, __func__);
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(!mDeviceState->mTrackEnabled);
+ MOZ_DIAGNOSTIC_ASSERT(!mDeviceState->mDeviceEnabled);
+ MOZ_DIAGNOSTIC_ASSERT(!mDeviceState->mStopped);
+
+ mDeviceState->mDeviceEnabled = true;
+ mDeviceState->mTrackEnabled = true;
+ mDeviceState->mTrackEnabledTime = TimeStamp::Now();
+ return DeviceListenerPromise::CreateAndResolve(true, __func__);
+ },
+ [self = RefPtr<DeviceListener>(this),
+ this](RefPtr<MediaMgrError>&& aResult) {
+ if (mStopped) {
+ return DeviceListenerPromise::CreateAndReject(std::move(aResult),
+ __func__);
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(!mDeviceState->mTrackEnabled);
+ MOZ_DIAGNOSTIC_ASSERT(!mDeviceState->mDeviceEnabled);
+ MOZ_DIAGNOSTIC_ASSERT(!mDeviceState->mStopped);
+
+ Stop();
+ return DeviceListenerPromise::CreateAndReject(std::move(aResult),
+ __func__);
+ });
+}
+
+void DeviceListener::Stop() {
+ MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");
+
+ if (mStopped) {
+ return;
+ }
+ mStopped = true;
+
+ LOG("DeviceListener %p stopping", this);
+
+ if (mDeviceState) {
+ mDeviceState->mDisableTimer->Cancel();
+
+ if (mDeviceState->mStopped) {
+ // device already stopped.
+ return;
+ }
+ mDeviceState->mStopped = true;
+
+ mDeviceState->mTrackSource->Stop();
+
+ MediaManager::Dispatch(NewTaskFrom([device = mDeviceState->mDevice]() {
+ device->Stop();
+ device->Deallocate();
+ }));
+
+ mWindowListener->ChromeAffectingStateChanged();
+ }
+
+ // Keep a strong ref to the removed window listener.
+ RefPtr<GetUserMediaWindowListener> windowListener = mWindowListener;
+ mWindowListener = nullptr;
+ windowListener->Remove(this);
+}
+
+void DeviceListener::GetSettings(MediaTrackSettings& aOutSettings) const {
+ MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");
+ LocalMediaDevice* device = GetDevice();
+ device->GetSettings(aOutSettings);
+
+ MediaSourceEnum mediaSource = device->GetMediaSource();
+ if (mediaSource == MediaSourceEnum::Camera ||
+ mediaSource == MediaSourceEnum::Microphone) {
+ aOutSettings.mDeviceId.Construct(device->mID);
+ aOutSettings.mGroupId.Construct(device->mGroupID);
+ }
+}
+
+auto DeviceListener::UpdateDevice(bool aOn) -> RefPtr<DeviceOperationPromise> {
+ MOZ_ASSERT(NS_IsMainThread());
+ RefPtr<DeviceListener> self = this;
+ DeviceState& state = *mDeviceState;
+ return MediaManager::Dispatch<DeviceOperationPromise>(
+ __func__,
+ [self, device = state.mDevice,
+ aOn](MozPromiseHolder<DeviceOperationPromise>& h) {
+ LOG("Turning %s device (%s)", aOn ? "on" : "off",
+ NS_ConvertUTF16toUTF8(device->mName).get());
+ h.Resolve(aOn ? device->Start() : device->Stop(), __func__);
+ })
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [self, this, &state, aOn](nsresult aResult) {
+ if (state.mStopped) {
+ // Device was stopped on main thread during the operation. Done.
+ return DeviceOperationPromise::CreateAndResolve(aResult,
+ __func__);
+ }
+ LOG("DeviceListener %p turning %s %s input device %s", this,
+ aOn ? "on" : "off",
+ nsCString(
+ dom::MediaDeviceKindValues::GetString(GetDevice()->Kind()))
+ .get(),
+ NS_SUCCEEDED(aResult) ? "succeeded" : "failed");
+
+ if (NS_FAILED(aResult) && aResult != NS_ERROR_ABORT) {
+ // This path handles errors from starting or stopping the
+ // device. NS_ERROR_ABORT are for cases where *we* aborted. They
+ // need graceful handling.
+ if (aOn) {
+ // Starting the device failed. Stopping the track here will
+ // make the MediaStreamTrack end after a pass through the
+ // MediaTrackGraph.
+ Stop();
+ } else {
+ // Stopping the device failed. This is odd, but not fatal.
+ MOZ_ASSERT_UNREACHABLE("The device should be stoppable");
+ }
+ }
+ return DeviceOperationPromise::CreateAndResolve(aResult, __func__);
+ },
+ []() {
+ MOZ_ASSERT_UNREACHABLE("Unexpected and unhandled reject");
+ return DeviceOperationPromise::CreateAndReject(false, __func__);
+ });
+}
+
+void DeviceListener::SetDeviceEnabled(bool aEnable) {
+ MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");
+ MOZ_ASSERT(Activated(), "No device to set enabled state for");
+
+ DeviceState& state = *mDeviceState;
+
+ LOG("DeviceListener %p %s %s device", this,
+ aEnable ? "enabling" : "disabling",
+ nsCString(dom::MediaDeviceKindValues::GetString(GetDevice()->Kind()))
+ .get());
+
+ state.mTrackEnabled = aEnable;
+
+ if (state.mStopped) {
+ // Device terminally stopped. Updating device state is pointless.
+ return;
+ }
+
+ if (state.mOperationInProgress) {
+ // If a timer is in progress, it needs to be canceled now so the next
+ // DisableTrack() gets a fresh start. Canceling will trigger another
+ // operation.
+ state.mDisableTimer->Cancel();
+ return;
+ }
+
+ if (state.mDeviceEnabled == aEnable) {
+ // Device is already in the desired state.
+ return;
+ }
+
+ // All paths from here on must end in setting
+ // `state.mOperationInProgress` to false.
+ state.mOperationInProgress = true;
+
+ RefPtr<MediaTimerPromise> timerPromise;
+ if (aEnable) {
+ timerPromise = MediaTimerPromise::CreateAndResolve(true, __func__);
+ state.mTrackEnabledTime = TimeStamp::Now();
+ } else {
+ const TimeDuration maxDelay =
+ TimeDuration::FromMilliseconds(Preferences::GetUint(
+ GetDevice()->Kind() == MediaDeviceKind::Audioinput
+ ? "media.getusermedia.microphone.off_while_disabled.delay_ms"
+ : "media.getusermedia.camera.off_while_disabled.delay_ms",
+ 3000));
+ const TimeDuration durationEnabled =
+ TimeStamp::Now() - state.mTrackEnabledTime;
+ const TimeDuration delay = TimeDuration::Max(
+ TimeDuration::FromMilliseconds(0), maxDelay - durationEnabled);
+ timerPromise = state.mDisableTimer->WaitFor(delay, __func__);
+ }
+
+ RefPtr<DeviceListener> self = this;
+ timerPromise
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [self, this, &state, aEnable]() mutable {
+ MOZ_ASSERT(state.mDeviceEnabled != aEnable,
+ "Device operation hasn't started");
+ MOZ_ASSERT(state.mOperationInProgress,
+ "It's our responsibility to reset the inProgress state");
+
+ LOG("DeviceListener %p %s %s device - starting device operation",
+ this, aEnable ? "enabling" : "disabling",
+ nsCString(
+ dom::MediaDeviceKindValues::GetString(GetDevice()->Kind()))
+ .get());
+
+ if (state.mStopped) {
+ // Source was stopped between timer resolving and this runnable.
+ return DeviceOperationPromise::CreateAndResolve(NS_ERROR_ABORT,
+ __func__);
+ }
+
+ state.mDeviceEnabled = aEnable;
+
+ if (mWindowListener) {
+ mWindowListener->ChromeAffectingStateChanged();
+ }
+ if (!state.mOffWhileDisabled || state.mDeviceMuted) {
+ // If the feature to turn a device off while disabled is itself
+ // disabled, or the device is currently user agent muted, then
+ // we shortcut the device operation and tell the
+ // ux-updating code that everything went fine.
+ return DeviceOperationPromise::CreateAndResolve(NS_OK, __func__);
+ }
+ return UpdateDevice(aEnable);
+ },
+ []() {
+ // Timer was canceled by us. We signal this with NS_ERROR_ABORT.
+ return DeviceOperationPromise::CreateAndResolve(NS_ERROR_ABORT,
+ __func__);
+ })
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [self, this, &state, aEnable](nsresult aResult) mutable {
+ MOZ_ASSERT_IF(aResult != NS_ERROR_ABORT,
+ state.mDeviceEnabled == aEnable);
+ MOZ_ASSERT(state.mOperationInProgress);
+ state.mOperationInProgress = false;
+
+ if (state.mStopped) {
+ // Device was stopped on main thread during the operation.
+ // Nothing to do.
+ return;
+ }
+
+ if (NS_FAILED(aResult) && aResult != NS_ERROR_ABORT && !aEnable) {
+ // To keep our internal state sane in this case, we disallow
+ // future stops due to disable.
+ state.mOffWhileDisabled = false;
+ return;
+ }
+
+ // This path is for a device operation aResult that was success or
+ // NS_ERROR_ABORT (*we* canceled the operation).
+ // At this point we have to follow up on the intended state, i.e.,
+ // update the device state if the track state changed in the
+ // meantime.
+
+ if (state.mTrackEnabled != state.mDeviceEnabled) {
+ // Track state changed during this operation. We'll start over.
+ SetDeviceEnabled(state.mTrackEnabled);
+ }
+ },
+ []() { MOZ_ASSERT_UNREACHABLE("Unexpected and unhandled reject"); });
+}
+
+void DeviceListener::SetDeviceMuted(bool aMute) {
+ MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");
+ MOZ_ASSERT(Activated(), "No device to set muted state for");
+
+ DeviceState& state = *mDeviceState;
+
+ LOG("DeviceListener %p %s %s device", this, aMute ? "muting" : "unmuting",
+ nsCString(dom::MediaDeviceKindValues::GetString(GetDevice()->Kind()))
+ .get());
+
+ if (state.mStopped) {
+ // Device terminally stopped. Updating device state is pointless.
+ return;
+ }
+
+ if (state.mDeviceMuted == aMute) {
+ // Device is already in the desired state.
+ return;
+ }
+
+ LOG("DeviceListener %p %s %s device - starting device operation", this,
+ aMute ? "muting" : "unmuting",
+ nsCString(dom::MediaDeviceKindValues::GetString(GetDevice()->Kind()))
+ .get());
+
+ state.mDeviceMuted = aMute;
+
+ if (mWindowListener) {
+ mWindowListener->ChromeAffectingStateChanged();
+ }
+ // Update trackSource to fire mute/unmute events on all its tracks
+ if (aMute) {
+ state.mTrackSource->Mute();
+ } else {
+ state.mTrackSource->Unmute();
+ }
+ if (!state.mOffWhileDisabled || !state.mDeviceEnabled) {
+ // If the pref to turn the underlying device is itself off, or the device
+ // is already off, it's unecessary to do anything else.
+ return;
+ }
+ UpdateDevice(!aMute);
+}
+
+void DeviceListener::MuteOrUnmuteCamera(bool aMute) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mStopped) {
+ return;
+ }
+
+ MOZ_RELEASE_ASSERT(mWindowListener);
+ LOG("DeviceListener %p MuteOrUnmuteCamera: %s", this,
+ aMute ? "mute" : "unmute");
+
+ if (GetDevice() &&
+ (GetDevice()->GetMediaSource() == MediaSourceEnum::Camera)) {
+ SetDeviceMuted(aMute);
+ }
+}
+
+void DeviceListener::MuteOrUnmuteMicrophone(bool aMute) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mStopped) {
+ return;
+ }
+
+ MOZ_RELEASE_ASSERT(mWindowListener);
+ LOG("DeviceListener %p MuteOrUnmuteMicrophone: %s", this,
+ aMute ? "mute" : "unmute");
+
+ if (GetDevice() &&
+ (GetDevice()->GetMediaSource() == MediaSourceEnum::Microphone)) {
+ SetDeviceMuted(aMute);
+ }
+}
+
+bool DeviceListener::CapturingVideo() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return Activated() && mDeviceState && !mDeviceState->mStopped &&
+ MediaEngineSource::IsVideo(GetDevice()->GetMediaSource()) &&
+ (!GetDevice()->IsFake() ||
+ Preferences::GetBool("media.navigator.permission.fake"));
+}
+
+bool DeviceListener::CapturingAudio() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return Activated() && mDeviceState && !mDeviceState->mStopped &&
+ MediaEngineSource::IsAudio(GetDevice()->GetMediaSource()) &&
+ (!GetDevice()->IsFake() ||
+ Preferences::GetBool("media.navigator.permission.fake"));
+}
+
+CaptureState DeviceListener::CapturingSource(MediaSourceEnum aSource) const {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (GetDevice()->GetMediaSource() != aSource) {
+ // This DeviceListener doesn't capture a matching source
+ return CaptureState::Off;
+ }
+
+ if (mDeviceState->mStopped) {
+ // The source is a match but has been permanently stopped
+ return CaptureState::Off;
+ }
+
+ if ((aSource == MediaSourceEnum::Camera ||
+ aSource == MediaSourceEnum::Microphone) &&
+ GetDevice()->IsFake() &&
+ !Preferences::GetBool("media.navigator.permission.fake")) {
+ // Fake Camera and Microphone only count if there is no fake permission
+ return CaptureState::Off;
+ }
+
+ // Source is a match and is active and unmuted
+
+ if (mDeviceState->mDeviceEnabled && !mDeviceState->mDeviceMuted) {
+ return CaptureState::Enabled;
+ }
+
+ return CaptureState::Disabled;
+}
+
+RefPtr<DeviceListener::DeviceListenerPromise> DeviceListener::ApplyConstraints(
+ const MediaTrackConstraints& aConstraints, CallerType aCallerType) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mStopped || mDeviceState->mStopped) {
+ LOG("DeviceListener %p %s device applyConstraints, but device is stopped",
+ this,
+ nsCString(dom::MediaDeviceKindValues::GetString(GetDevice()->Kind()))
+ .get());
+ return DeviceListenerPromise::CreateAndResolve(false, __func__);
+ }
+
+ MediaManager* mgr = MediaManager::GetIfExists();
+ if (!mgr) {
+ return DeviceListenerPromise::CreateAndResolve(false, __func__);
+ }
+
+ return MediaManager::Dispatch<DeviceListenerPromise>(
+ __func__, [device = mDeviceState->mDevice, aConstraints, aCallerType](
+ MozPromiseHolder<DeviceListenerPromise>& aHolder) mutable {
+ MOZ_ASSERT(MediaManager::IsInMediaThread());
+ MediaManager* mgr = MediaManager::GetIfExists();
+ MOZ_RELEASE_ASSERT(mgr); // Must exist while media thread is alive
+ const char* badConstraint = nullptr;
+ nsresult rv =
+ device->Reconfigure(aConstraints, mgr->mPrefs, &badConstraint);
+ if (NS_FAILED(rv)) {
+ if (rv == NS_ERROR_INVALID_ARG) {
+ // Reconfigure failed due to constraints
+ if (!badConstraint) {
+ nsTArray<RefPtr<LocalMediaDevice>> devices;
+ devices.AppendElement(device);
+ badConstraint = MediaConstraintsHelper::SelectSettings(
+ NormalizedConstraints(aConstraints), devices, aCallerType);
+ }
+ } else {
+ // Unexpected. ApplyConstraints* cannot fail with any other error.
+ badConstraint = "";
+ LOG("ApplyConstraints-Task: Unexpected fail %" PRIx32,
+ static_cast<uint32_t>(rv));
+ }
+
+ aHolder.Reject(MakeRefPtr<MediaMgrError>(
+ MediaMgrError::Name::OverconstrainedError, "",
+ NS_ConvertASCIItoUTF16(badConstraint)),
+ __func__);
+ return;
+ }
+ // Reconfigure was successful
+ aHolder.Resolve(false, __func__);
+ });
+}
+
+PrincipalHandle DeviceListener::GetPrincipalHandle() const {
+ return mPrincipalHandle;
+}
+
+void GetUserMediaWindowListener::StopSharing() {
+ MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");
+
+ for (auto& l : mActiveListeners.Clone()) {
+ MediaSourceEnum source = l->GetDevice()->GetMediaSource();
+ if (source == MediaSourceEnum::Screen ||
+ source == MediaSourceEnum::Window ||
+ source == MediaSourceEnum::AudioCapture) {
+ l->Stop();
+ }
+ }
+}
+
+void GetUserMediaWindowListener::StopRawID(const nsString& removedDeviceID) {
+ MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");
+
+ for (auto& l : mActiveListeners.Clone()) {
+ if (removedDeviceID.Equals(l->GetDevice()->RawID())) {
+ l->Stop();
+ }
+ }
+}
+
+void GetUserMediaWindowListener::MuteOrUnmuteCameras(bool aMute) {
+ MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");
+
+ if (mCamerasAreMuted == aMute) {
+ return;
+ }
+ mCamerasAreMuted = aMute;
+
+ for (auto& l : mActiveListeners) {
+ if (l->GetDevice()->Kind() == MediaDeviceKind::Videoinput) {
+ l->MuteOrUnmuteCamera(aMute);
+ }
+ }
+}
+
+void GetUserMediaWindowListener::MuteOrUnmuteMicrophones(bool aMute) {
+ MOZ_ASSERT(NS_IsMainThread(), "Only call on main thread");
+
+ if (mMicrophonesAreMuted == aMute) {
+ return;
+ }
+ mMicrophonesAreMuted = aMute;
+
+ for (auto& l : mActiveListeners) {
+ if (l->GetDevice()->Kind() == MediaDeviceKind::Audioinput) {
+ l->MuteOrUnmuteMicrophone(aMute);
+ }
+ }
+}
+
+void GetUserMediaWindowListener::ChromeAffectingStateChanged() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // We wait until stable state before notifying chrome so chrome only does
+ // one update if more updates happen in this event loop.
+
+ if (mChromeNotificationTaskPosted) {
+ return;
+ }
+
+ nsCOMPtr<nsIRunnable> runnable =
+ NewRunnableMethod("GetUserMediaWindowListener::NotifyChrome", this,
+ &GetUserMediaWindowListener::NotifyChrome);
+ nsContentUtils::RunInStableState(runnable.forget());
+ mChromeNotificationTaskPosted = true;
+}
+
+void GetUserMediaWindowListener::NotifyChrome() {
+ MOZ_ASSERT(mChromeNotificationTaskPosted);
+ mChromeNotificationTaskPosted = false;
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "MediaManager::NotifyChrome", [windowID = mWindowID]() {
+ auto* window = nsGlobalWindowInner::GetInnerWindowWithId(windowID);
+ if (!window) {
+ MOZ_ASSERT_UNREACHABLE("Should have window");
+ return;
+ }
+
+ nsresult rv = MediaManager::NotifyRecordingStatusChange(window);
+ if (NS_FAILED(rv)) {
+ MOZ_ASSERT_UNREACHABLE("Should be able to notify chrome");
+ return;
+ }
+ }));
+}
+
+#undef LOG
+
+} // namespace mozilla
diff --git a/dom/media/MediaManager.h b/dom/media/MediaManager.h
new file mode 100644
index 0000000000..5a1679deec
--- /dev/null
+++ b/dom/media/MediaManager.h
@@ -0,0 +1,422 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_MEDIAMANAGER_H
+#define MOZILLA_MEDIAMANAGER_H
+
+#include "MediaEnginePrefs.h"
+#include "MediaEventSource.h"
+#include "mozilla/dom/GetUserMediaRequest.h"
+#include "mozilla/Unused.h"
+#include "nsIMediaDevice.h"
+#include "nsIMediaManager.h"
+
+#include "nsHashKeys.h"
+#include "nsClassHashtable.h"
+#include "nsRefPtrHashtable.h"
+#include "nsIMemoryReporter.h"
+#include "nsIObserver.h"
+
+#include "nsXULAppAPI.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/dom/MediaStreamBinding.h"
+#include "mozilla/dom/MediaStreamTrackBinding.h"
+#include "mozilla/dom/MediaStreamError.h"
+#include "mozilla/dom/NavigatorBinding.h"
+#include "mozilla/media/MediaChild.h"
+#include "mozilla/media/MediaParent.h"
+#include "mozilla/Logging.h"
+#include "mozilla/UniquePtr.h"
+#include "DOMMediaStream.h"
+#include "PerformanceRecorder.h"
+
+#ifdef MOZ_WEBRTC
+# include "transport/runnable_utils.h"
+#endif
+
+class AudioDeviceInfo;
+class nsIPrefBranch;
+
+namespace mozilla {
+class MediaEngine;
+class MediaEngineSource;
+class TaskQueue;
+class MediaTimer;
+class MediaTrack;
+namespace dom {
+struct AudioOutputOptions;
+struct MediaStreamConstraints;
+struct MediaTrackConstraints;
+struct MediaTrackConstraintSet;
+struct MediaTrackSettings;
+enum class CallerType : uint32_t;
+enum class MediaDeviceKind : uint8_t;
+} // namespace dom
+
+namespace ipc {
+class PrincipalInfo;
+}
+
+class GetUserMediaTask;
+class GetUserMediaWindowListener;
+class MediaManager;
+class DeviceListener;
+
+/**
+ * Device info that is independent of any Window.
+ * MediaDevices can be shared, unlike LocalMediaDevices.
+ */
+class MediaDevice final {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaDevice)
+
+ /**
+ * Whether source device does end-run around cross origin restrictions.
+ */
+ enum class IsScary { No, Yes };
+
+ /**
+ * Whether source device can use OS level selection prompt
+ */
+ enum class OsPromptable { No, Yes };
+
+ MediaDevice(MediaEngine* aEngine, dom::MediaSourceEnum aMediaSource,
+ const nsString& aRawName, const nsString& aRawID,
+ const nsString& aRawGroupID, IsScary aIsScary,
+ const OsPromptable canRequestOsLevelPrompt);
+
+ MediaDevice(MediaEngine* aEngine,
+ const RefPtr<AudioDeviceInfo>& aAudioDeviceInfo,
+ const nsString& aRawID);
+
+ static RefPtr<MediaDevice> CopyWithNewRawGroupId(
+ const RefPtr<MediaDevice>& aOther, const nsString& aRawGroupID);
+
+ dom::MediaSourceEnum GetMediaSource() const;
+
+ protected:
+ ~MediaDevice();
+
+ public:
+ const RefPtr<MediaEngine> mEngine;
+ const RefPtr<AudioDeviceInfo> mAudioDeviceInfo;
+ const dom::MediaSourceEnum mMediaSource;
+ const dom::MediaDeviceKind mKind;
+ const bool mScary;
+ const bool mCanRequestOsLevelPrompt;
+ const bool mIsFake;
+ const nsString mType;
+ const nsString mRawID;
+ const nsString mRawGroupID;
+ const nsString mRawName;
+};
+
+/**
+ * Device info that is specific to a particular Window. If the device is a
+ * source device, then a single corresponding MediaEngineSource is provided,
+ * which can provide a maximum of one capture stream. LocalMediaDevices are
+ * not shared, but APIs returning LocalMediaDevices return a new object each
+ * call.
+ */
+class LocalMediaDevice final : public nsIMediaDevice {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIMEDIADEVICE
+
+ LocalMediaDevice(RefPtr<const MediaDevice> aRawDevice, const nsString& aID,
+ const nsString& aGroupID, const nsString& aName);
+
+ uint32_t GetBestFitnessDistance(
+ const nsTArray<const NormalizedConstraintSet*>& aConstraintSets,
+ dom::CallerType aCallerType);
+
+ nsresult Allocate(const dom::MediaTrackConstraints& aConstraints,
+ const MediaEnginePrefs& aPrefs, uint64_t aWindowId,
+ const char** aOutBadConstraint);
+ void SetTrack(const RefPtr<MediaTrack>& aTrack,
+ const nsMainThreadPtrHandle<nsIPrincipal>& aPrincipal);
+ nsresult Start();
+ nsresult Reconfigure(const dom::MediaTrackConstraints& aConstraints,
+ const MediaEnginePrefs& aPrefs,
+ const char** aOutBadConstraint);
+ nsresult FocusOnSelectedSource();
+ nsresult Stop();
+ nsresult Deallocate();
+
+ void GetSettings(dom::MediaTrackSettings& aOutSettings);
+ MediaEngineSource* Source();
+ const TrackingId& GetTrackingId() const;
+ // Returns null if not a physical audio device.
+ AudioDeviceInfo* GetAudioDeviceInfo() const {
+ return mRawDevice->mAudioDeviceInfo;
+ }
+ dom::MediaSourceEnum GetMediaSource() const {
+ return mRawDevice->GetMediaSource();
+ }
+ dom::MediaDeviceKind Kind() const { return mRawDevice->mKind; }
+ bool IsFake() const { return mRawDevice->mIsFake; }
+ const nsString& RawID() { return mRawDevice->mRawID; }
+
+ private:
+ virtual ~LocalMediaDevice() = default;
+
+ static uint32_t FitnessDistance(
+ nsString aN,
+ const dom::OwningStringOrStringSequenceOrConstrainDOMStringParameters&
+ aConstraint);
+
+ static bool StringsContain(const dom::OwningStringOrStringSequence& aStrings,
+ nsString aN);
+ static uint32_t FitnessDistance(
+ nsString aN, const dom::ConstrainDOMStringParameters& aParams);
+
+ public:
+ const RefPtr<const MediaDevice> mRawDevice;
+ const nsString mName;
+ const nsString mID;
+ const nsString mGroupID;
+
+ private:
+ RefPtr<MediaEngineSource> mSource;
+};
+
+typedef nsRefPtrHashtable<nsUint64HashKey, GetUserMediaWindowListener>
+ WindowTable;
+
+class MediaManager final : public nsIMediaManagerService,
+ public nsIMemoryReporter,
+ public nsIObserver {
+ friend DeviceListener;
+
+ public:
+ static already_AddRefed<MediaManager> GetInstance();
+
+ // NOTE: never NS_DispatchAndSpinEventLoopUntilComplete to the MediaManager
+ // thread from the MainThread, as we NS_DispatchAndSpinEventLoopUntilComplete
+ // to MainThread from MediaManager thread.
+ static MediaManager* Get();
+ static MediaManager* GetIfExists();
+ static void StartupInit();
+ static void Dispatch(already_AddRefed<Runnable> task);
+
+ /**
+ * Posts an async operation to the media manager thread.
+ * FunctionType must be a function that takes a `MozPromiseHolder&`.
+ *
+ * The returned promise is resolved or rejected by aFunction on the media
+ * manager thread.
+ */
+ template <typename MozPromiseType, typename FunctionType>
+ static RefPtr<MozPromiseType> Dispatch(const char* aName,
+ FunctionType&& aFunction);
+
+#ifdef DEBUG
+ static bool IsInMediaThread();
+#endif
+
+ static bool Exists() { return !!GetIfExists(); }
+
+ static nsresult NotifyRecordingStatusChange(nsPIDOMWindowInner* aWindow);
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSIMEMORYREPORTER
+ NS_DECL_NSIMEDIAMANAGERSERVICE
+
+ media::Parent<media::NonE10s>* GetNonE10sParent();
+
+ // If the window has not been destroyed, then return the
+ // GetUserMediaWindowListener for this window.
+ // If the window has been destroyed, then return null.
+ RefPtr<GetUserMediaWindowListener> GetOrMakeWindowListener(
+ nsPIDOMWindowInner* aWindow);
+ WindowTable* GetActiveWindows() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return &mActiveWindows;
+ }
+ GetUserMediaWindowListener* GetWindowListener(uint64_t aWindowId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mActiveWindows.GetWeak(aWindowId);
+ }
+ void AddWindowID(uint64_t aWindowId,
+ RefPtr<GetUserMediaWindowListener> aListener);
+ void RemoveWindowID(uint64_t aWindowId);
+ void SendPendingGUMRequest();
+ bool IsWindowStillActive(uint64_t aWindowId) {
+ return !!GetWindowListener(aWindowId);
+ }
+ bool IsWindowListenerStillActive(
+ const RefPtr<GetUserMediaWindowListener>& aListener);
+
+ static bool IsOn(const dom::OwningBooleanOrMediaTrackConstraints& aUnion) {
+ return !aUnion.IsBoolean() || aUnion.GetAsBoolean();
+ }
+ using GetUserMediaSuccessCallback = dom::NavigatorUserMediaSuccessCallback;
+ using GetUserMediaErrorCallback = dom::NavigatorUserMediaErrorCallback;
+
+ MOZ_CAN_RUN_SCRIPT
+ static void CallOnError(GetUserMediaErrorCallback& aCallback,
+ dom::MediaStreamError& aError);
+ MOZ_CAN_RUN_SCRIPT
+ static void CallOnSuccess(GetUserMediaSuccessCallback& aCallback,
+ DOMMediaStream& aTrack);
+
+ using MediaDeviceSet = nsTArray<RefPtr<MediaDevice>>;
+ using MediaDeviceSetRefCnt = media::Refcountable<MediaDeviceSet>;
+ using LocalMediaDeviceSet = nsTArray<RefPtr<LocalMediaDevice>>;
+ using LocalMediaDeviceSetRefCnt = media::Refcountable<LocalMediaDeviceSet>;
+
+ using StreamPromise =
+ MozPromise<RefPtr<DOMMediaStream>, RefPtr<MediaMgrError>, true>;
+ using DeviceSetPromise =
+ MozPromise<RefPtr<MediaDeviceSetRefCnt>, RefPtr<MediaMgrError>, true>;
+ using ConstDeviceSetPromise = MozPromise<RefPtr<const MediaDeviceSetRefCnt>,
+ RefPtr<MediaMgrError>, true>;
+ using LocalDevicePromise =
+ MozPromise<RefPtr<LocalMediaDevice>, RefPtr<MediaMgrError>, true>;
+ using LocalDeviceSetPromise = MozPromise<RefPtr<LocalMediaDeviceSetRefCnt>,
+ RefPtr<MediaMgrError>, true>;
+ using MgrPromise = MozPromise<bool, RefPtr<MediaMgrError>, true>;
+
+ RefPtr<StreamPromise> GetUserMedia(
+ nsPIDOMWindowInner* aWindow,
+ const dom::MediaStreamConstraints& aConstraints,
+ dom::CallerType aCallerType);
+
+ RefPtr<LocalDevicePromise> SelectAudioOutput(
+ nsPIDOMWindowInner* aWindow, const dom::AudioOutputOptions& aOptions,
+ dom::CallerType aCallerType);
+
+ // Return the list of microphone, camera, and speaker devices.
+ // MediaDeviceSets provided on promise resolution are shared between
+ // callers and so cannot be modified.
+ RefPtr<ConstDeviceSetPromise> GetPhysicalDevices();
+
+ void OnNavigation(uint64_t aWindowID);
+ void OnCameraMute(bool aMute);
+ void OnMicrophoneMute(bool aMute);
+ bool IsActivelyCapturingOrHasAPermission(uint64_t aWindowId);
+
+ MediaEventSource<void>& DeviceListChangeEvent() {
+ return mDeviceListChangeEvent;
+ }
+ RefPtr<LocalDeviceSetPromise> AnonymizeDevices(
+ nsPIDOMWindowInner* aWindow, RefPtr<const MediaDeviceSetRefCnt> aDevices);
+
+ MediaEnginePrefs mPrefs;
+
+ private:
+ static nsresult GenerateUUID(nsAString& aResult);
+
+ public:
+ /**
+ * This function tries to guess the group id for a video device in aDevices
+ * based on the device name. If the name of only one audio device in aAudios
+ * contains the name of the video device, then, this video device will take
+ * the group id of the audio device. Since this is a guess we try to minimize
+ * the probability of false positive. If we fail to find a correlation we
+ * leave the video group id untouched. In that case the group id will be the
+ * video device name.
+ */
+ static void GuessVideoDeviceGroupIDs(MediaDeviceSet& aDevices,
+ const MediaDeviceSet& aAudios);
+
+ private:
+ enum class EnumerationFlag {
+ AllowPermissionRequest,
+ EnumerateAudioOutputs,
+ ForceFakes,
+ };
+ using EnumerationFlags = EnumSet<EnumerationFlag>;
+ RefPtr<LocalDeviceSetPromise> EnumerateDevicesImpl(
+ nsPIDOMWindowInner* aWindow, dom::MediaSourceEnum aVideoInputType,
+ dom::MediaSourceEnum aAudioInputType, EnumerationFlags aFlags);
+
+ RefPtr<DeviceSetPromise> EnumerateRawDevices(
+ dom::MediaSourceEnum aVideoInputType,
+ dom::MediaSourceEnum aAudioInputType, EnumerationFlags aFlags);
+
+ RefPtr<LocalDeviceSetPromise> SelectSettings(
+ const dom::MediaStreamConstraints& aConstraints,
+ dom::CallerType aCallerType, RefPtr<LocalMediaDeviceSetRefCnt> aDevices);
+
+ void GetPref(nsIPrefBranch* aBranch, const char* aPref, const char* aData,
+ int32_t* aVal);
+ void GetPrefBool(nsIPrefBranch* aBranch, const char* aPref, const char* aData,
+ bool* aVal);
+ void GetPrefs(nsIPrefBranch* aBranch, const char* aData);
+
+ // Make private because we want only one instance of this class
+ explicit MediaManager(already_AddRefed<TaskQueue> aMediaThread);
+
+ ~MediaManager() = default;
+ void Shutdown();
+
+ void StopScreensharing(uint64_t aWindowID);
+
+ void RemoveMediaDevicesCallback(uint64_t aWindowID);
+ void DeviceListChanged();
+ void InvalidateDeviceCache();
+ void HandleDeviceListChanged();
+
+ // Returns the number of incomplete tasks associated with this window,
+ // including the newly added task.
+ size_t AddTaskAndGetCount(uint64_t aWindowID, const nsAString& aCallID,
+ RefPtr<GetUserMediaTask> aTask);
+ // Finds the task corresponding to aCallID and removes it from tracking.
+ RefPtr<GetUserMediaTask> TakeGetUserMediaTask(const nsAString& aCallID);
+ // Intended for use with "media.navigator.permission.disabled" to bypass the
+ // permission prompt and use the first appropriate device.
+ void NotifyAllowed(const nsString& aCallID,
+ const LocalMediaDeviceSet& aDevices);
+
+ // Media thread only
+ MediaEngine* GetBackend();
+
+ MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf);
+
+ // ONLY access from MainThread so we don't need to lock
+ WindowTable mActiveWindows;
+ nsRefPtrHashtable<nsStringHashKey, GetUserMediaTask> mActiveCallbacks;
+ nsClassHashtable<nsUint64HashKey, nsTArray<nsString>> mCallIds;
+ nsTArray<RefPtr<dom::GetUserMediaRequest>> mPendingGUMRequest;
+ // non-null if a device enumeration is in progress and was started after the
+ // last device-change invalidation
+ RefPtr<media::Refcountable<nsTArray<MozPromiseHolder<ConstDeviceSetPromise>>>>
+ mPendingDevicesPromises;
+ RefPtr<MediaDeviceSetRefCnt> mPhysicalDevices;
+ TimeStamp mUnhandledDeviceChangeTime;
+ RefPtr<MediaTimer> mDeviceChangeTimer;
+ bool mCamerasMuted = false;
+ bool mMicrophonesMuted = false;
+
+ public:
+ // Always exists
+ const RefPtr<TaskQueue> mMediaThread;
+
+ private:
+ nsCOMPtr<nsIAsyncShutdownBlocker> mShutdownBlocker;
+
+ // ONLY accessed from MediaManagerThread
+ RefPtr<MediaEngine> mBackend;
+
+ // Accessed only on main thread and mMediaThread.
+ // Set before mMediaThread is created, and cleared on main thread after last
+ // mMediaThread task is run.
+ static StaticRefPtr<MediaManager> sSingleton;
+
+ // Connect/Disconnect on media thread only
+ MediaEventListener mDeviceListChangeListener;
+
+ MediaEventProducer<void> mDeviceListChangeEvent;
+
+ public:
+ RefPtr<media::Parent<media::NonE10s>> mNonE10sParent;
+};
+
+} // namespace mozilla
+
+#endif // MOZILLA_MEDIAMANAGER_H
diff --git a/dom/media/MediaMetadataManager.h b/dom/media/MediaMetadataManager.h
new file mode 100644
index 0000000000..026c343fcc
--- /dev/null
+++ b/dom/media/MediaMetadataManager.h
@@ -0,0 +1,97 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(MediaMetadataManager_h__)
+# define MediaMetadataManager_h__
+
+# include "mozilla/AbstractThread.h"
+# include "mozilla/LinkedList.h"
+
+# include "MediaEventSource.h"
+# include "TimeUnits.h"
+# include "VideoUtils.h"
+
+namespace mozilla {
+
+class TimedMetadata;
+typedef MediaEventProducerExc<TimedMetadata> TimedMetadataEventProducer;
+typedef MediaEventSourceExc<TimedMetadata> TimedMetadataEventSource;
+
+// A struct that contains the metadata of a media, and the time at which those
+// metadata should start to be reported.
+class TimedMetadata : public LinkedListElement<TimedMetadata> {
+ public:
+ TimedMetadata(const media::TimeUnit& aPublishTime,
+ UniquePtr<MetadataTags>&& aTags, UniquePtr<MediaInfo>&& aInfo)
+ : mPublishTime(aPublishTime),
+ mTags(std::move(aTags)),
+ mInfo(std::move(aInfo)) {}
+
+ // Define our move constructor because we don't want to move the members of
+ // LinkedListElement to change the list.
+ TimedMetadata(TimedMetadata&& aOther)
+ : mPublishTime(aOther.mPublishTime),
+ mTags(std::move(aOther.mTags)),
+ mInfo(std::move(aOther.mInfo)) {}
+
+ // The time, in microseconds, at which those metadata should be available.
+ media::TimeUnit mPublishTime;
+ // The metadata. The ownership is transfered to the element when dispatching
+ // to the main threads.
+ UniquePtr<MetadataTags> mTags;
+ // The media info, including the info of audio tracks and video tracks.
+ // The ownership is transfered to MediaDecoder when dispatching to the
+ // main thread.
+ UniquePtr<MediaInfo> mInfo;
+};
+
+// This class encapsulate the logic to give the metadata from the reader to
+// the content, at the right time.
+class MediaMetadataManager {
+ public:
+ ~MediaMetadataManager() {
+ TimedMetadata* element;
+ while ((element = mMetadataQueue.popFirst()) != nullptr) {
+ delete element;
+ }
+ }
+
+ // Connect to an event source to receive TimedMetadata events.
+ void Connect(TimedMetadataEventSource& aEvent, AbstractThread* aThread) {
+ mListener =
+ aEvent.Connect(aThread, this, &MediaMetadataManager::OnMetadataQueued);
+ }
+
+ // Stop receiving TimedMetadata events.
+ void Disconnect() { mListener.Disconnect(); }
+
+ // Return an event source through which we will send TimedMetadata events
+ // when playback position reaches the publish time.
+ TimedMetadataEventSource& TimedMetadataEvent() { return mTimedMetadataEvent; }
+
+ void DispatchMetadataIfNeeded(const media::TimeUnit& aCurrentTime) {
+ TimedMetadata* metadata = mMetadataQueue.getFirst();
+ while (metadata && aCurrentTime >= metadata->mPublishTime) {
+ // Our listener will figure out what to do with TimedMetadata.
+ mTimedMetadataEvent.Notify(std::move(*metadata));
+ delete mMetadataQueue.popFirst();
+ metadata = mMetadataQueue.getFirst();
+ }
+ }
+
+ protected:
+ void OnMetadataQueued(TimedMetadata&& aMetadata) {
+ mMetadataQueue.insertBack(new TimedMetadata(std::move(aMetadata)));
+ }
+
+ LinkedList<TimedMetadata> mMetadataQueue;
+ MediaEventListener mListener;
+ TimedMetadataEventProducer mTimedMetadataEvent;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/MediaPlaybackDelayPolicy.cpp b/dom/media/MediaPlaybackDelayPolicy.cpp
new file mode 100644
index 0000000000..1ac326db7c
--- /dev/null
+++ b/dom/media/MediaPlaybackDelayPolicy.cpp
@@ -0,0 +1,166 @@
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaPlaybackDelayPolicy.h"
+
+#include "nsPIDOMWindow.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/HTMLMediaElement.h"
+#include "mozilla/StaticPrefs_media.h"
+
+namespace mozilla::dom {
+
+using AudibleState = AudioChannelService::AudibleState;
+
+static AudibleState DetermineMediaAudibleState(const HTMLMediaElement* aElement,
+ bool aIsAudible) {
+ MOZ_ASSERT(aElement);
+ if (!aElement->HasAudio()) {
+ return AudibleState::eNotAudible;
+ }
+ // `eMaybeAudible` is used to distinguish if the media has audio track or not,
+ // because we would only show the delayed media playback icon for media with
+ // an audio track.
+ return aIsAudible ? AudibleState::eAudible : AudibleState::eMaybeAudible;
+}
+
+ResumeDelayedPlaybackAgent::~ResumeDelayedPlaybackAgent() {
+ if (mDelegate) {
+ mDelegate->Clear();
+ mDelegate = nullptr;
+ }
+}
+
+bool ResumeDelayedPlaybackAgent::InitDelegate(const HTMLMediaElement* aElement,
+ bool aIsAudible) {
+ MOZ_ASSERT(!mDelegate, "Delegate has been initialized!");
+ mDelegate = new ResumePlayDelegate();
+ if (!mDelegate->Init(aElement, aIsAudible)) {
+ mDelegate->Clear();
+ mDelegate = nullptr;
+ return false;
+ }
+ return true;
+}
+
+RefPtr<ResumeDelayedPlaybackAgent::ResumePromise>
+ResumeDelayedPlaybackAgent::GetResumePromise() {
+ MOZ_ASSERT(mDelegate);
+ return mDelegate->GetResumePromise();
+}
+
+void ResumeDelayedPlaybackAgent::UpdateAudibleState(
+ const HTMLMediaElement* aElement, bool aIsAudible) {
+ MOZ_ASSERT(aElement);
+ MOZ_ASSERT(mDelegate);
+ mDelegate->UpdateAudibleState(aElement, aIsAudible);
+}
+
+NS_IMPL_ISUPPORTS(ResumeDelayedPlaybackAgent::ResumePlayDelegate,
+ nsIAudioChannelAgentCallback)
+
+ResumeDelayedPlaybackAgent::ResumePlayDelegate::~ResumePlayDelegate() {
+ MOZ_ASSERT(!mAudioChannelAgent);
+}
+
+bool ResumeDelayedPlaybackAgent::ResumePlayDelegate::Init(
+ const HTMLMediaElement* aElement, bool aIsAudible) {
+ MOZ_ASSERT(aElement);
+ if (!aElement->OwnerDoc()->GetInnerWindow()) {
+ return false;
+ }
+
+ MOZ_ASSERT(!mAudioChannelAgent);
+ mAudioChannelAgent = new AudioChannelAgent();
+ nsresult rv =
+ mAudioChannelAgent->Init(aElement->OwnerDoc()->GetInnerWindow(), this);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ Clear();
+ return false;
+ }
+
+ // Start AudioChannelAgent in order to wait the suspended state change when we
+ // are able to resume delayed playback.
+ AudibleState audibleState = DetermineMediaAudibleState(aElement, aIsAudible);
+ rv = mAudioChannelAgent->NotifyStartedPlaying(audibleState);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ Clear();
+ return false;
+ }
+
+ return true;
+}
+
+void ResumeDelayedPlaybackAgent::ResumePlayDelegate::Clear() {
+ if (mAudioChannelAgent) {
+ mAudioChannelAgent->NotifyStoppedPlaying();
+ mAudioChannelAgent = nullptr;
+ }
+ mPromise.RejectIfExists(true, __func__);
+}
+
+RefPtr<ResumeDelayedPlaybackAgent::ResumePromise>
+ResumeDelayedPlaybackAgent::ResumePlayDelegate::GetResumePromise() {
+ return mPromise.Ensure(__func__);
+}
+
+void ResumeDelayedPlaybackAgent::ResumePlayDelegate::UpdateAudibleState(
+ const HTMLMediaElement* aElement, bool aIsAudible) {
+ MOZ_ASSERT(aElement);
+ // It's possible for the owner of `ResumeDelayedPlaybackAgent` to call
+ // `UpdateAudibleState()` after we resolve the promise and clean resource.
+ if (!mAudioChannelAgent) {
+ return;
+ }
+ AudibleState audibleState = DetermineMediaAudibleState(aElement, aIsAudible);
+ mAudioChannelAgent->NotifyStartedAudible(
+ audibleState,
+ AudioChannelService::AudibleChangedReasons::eDataAudibleChanged);
+}
+
+NS_IMETHODIMP
+ResumeDelayedPlaybackAgent::ResumePlayDelegate::WindowVolumeChanged(
+ float aVolume, bool aMuted) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ResumeDelayedPlaybackAgent::ResumePlayDelegate::WindowAudioCaptureChanged(
+ bool aCapture) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+ResumeDelayedPlaybackAgent::ResumePlayDelegate::WindowSuspendChanged(
+ SuspendTypes aSuspend) {
+ if (aSuspend == nsISuspendedTypes::NONE_SUSPENDED) {
+ mPromise.ResolveIfExists(true, __func__);
+ Clear();
+ }
+ return NS_OK;
+}
+
+bool MediaPlaybackDelayPolicy::ShouldDelayPlayback(
+ const HTMLMediaElement* aElement) {
+ MOZ_ASSERT(aElement);
+ if (!StaticPrefs::media_block_autoplay_until_in_foreground()) {
+ return false;
+ }
+
+ const Document* doc = aElement->OwnerDoc();
+ nsPIDOMWindowInner* inner = nsPIDOMWindowInner::From(doc->GetInnerWindow());
+ nsPIDOMWindowOuter* outer = nsPIDOMWindowOuter::GetFromCurrentInner(inner);
+ return outer && outer->ShouldDelayMediaFromStart();
+}
+
+RefPtr<ResumeDelayedPlaybackAgent>
+MediaPlaybackDelayPolicy::CreateResumeDelayedPlaybackAgent(
+ const HTMLMediaElement* aElement, bool aIsAudible) {
+ MOZ_ASSERT(aElement);
+ RefPtr<ResumeDelayedPlaybackAgent> agent = new ResumeDelayedPlaybackAgent();
+ return agent->InitDelegate(aElement, aIsAudible) ? agent : nullptr;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/MediaPlaybackDelayPolicy.h b/dom/media/MediaPlaybackDelayPolicy.h
new file mode 100644
index 0000000000..17d9164d3b
--- /dev/null
+++ b/dom/media/MediaPlaybackDelayPolicy.h
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_mediaplaybackdelaypolicy_h__
+#define mozilla_dom_mediaplaybackdelaypolicy_h__
+
+#include "AudioChannelAgent.h"
+#include "AudioChannelService.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/RefPtr.h"
+#include "nsISupportsImpl.h"
+
+typedef uint32_t SuspendTypes;
+
+namespace mozilla::dom {
+
+class HTMLMediaElement;
+/**
+ * We usaually start AudioChannelAgent when media starts and stop it when media
+ * stops. However, when we decide to delay media playback for unvisited tab, we
+ * would start AudioChannelAgent even if media doesn't start in order to
+ * register the agent to AudioChannelService, so that the service could notify
+ * us when we are able to resume media playback. Therefore,
+ * ResumeDelayedPlaybackAgent is used to handle this special use case of
+ * AudioChannelAgent.
+ * - Use `GetResumePromise()` to require resume-promise and then do follow-up
+ * resume behavior when promise is resolved.
+ * - Use `UpdateAudibleState()` to update audible state only when media info
+ * changes. As having audio track or not is the only thing for us to decide
+ * whether we would show the delayed media playback icon on the tab bar.
+ */
+class ResumeDelayedPlaybackAgent {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ResumeDelayedPlaybackAgent);
+ ResumeDelayedPlaybackAgent() = default;
+
+ using ResumePromise = MozPromise<bool, bool, true /* IsExclusive */>;
+ RefPtr<ResumePromise> GetResumePromise();
+ void UpdateAudibleState(const HTMLMediaElement* aElement, bool aIsAudible);
+
+ private:
+ friend class MediaPlaybackDelayPolicy;
+
+ ~ResumeDelayedPlaybackAgent();
+ bool InitDelegate(const HTMLMediaElement* aElement, bool aIsAudible);
+
+ class ResumePlayDelegate final : public nsIAudioChannelAgentCallback {
+ public:
+ NS_DECL_ISUPPORTS
+
+ ResumePlayDelegate() = default;
+
+ bool Init(const HTMLMediaElement* aElement, bool aIsAudible);
+ void UpdateAudibleState(const HTMLMediaElement* aElement, bool aIsAudible);
+ RefPtr<ResumePromise> GetResumePromise();
+ void Clear();
+
+ NS_IMETHODIMP WindowVolumeChanged(float aVolume, bool aMuted) override;
+ NS_IMETHODIMP WindowAudioCaptureChanged(bool aCapture) override;
+ NS_IMETHODIMP WindowSuspendChanged(SuspendTypes aSuspend) override;
+
+ private:
+ virtual ~ResumePlayDelegate();
+
+ MozPromiseHolder<ResumePromise> mPromise;
+ RefPtr<AudioChannelAgent> mAudioChannelAgent;
+ };
+
+ RefPtr<ResumePlayDelegate> mDelegate;
+};
+
+class MediaPlaybackDelayPolicy {
+ public:
+ static bool ShouldDelayPlayback(const HTMLMediaElement* aElement);
+ static RefPtr<ResumeDelayedPlaybackAgent> CreateResumeDelayedPlaybackAgent(
+ const HTMLMediaElement* aElement, bool aIsAudible);
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/MediaPromiseDefs.h b/dom/media/MediaPromiseDefs.h
new file mode 100644
index 0000000000..a979ef19a5
--- /dev/null
+++ b/dom/media/MediaPromiseDefs.h
@@ -0,0 +1,19 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef MediaPromiseDefs_h_
+#define MediaPromiseDefs_h_
+
+#include "MediaResult.h"
+#include "mozilla/MozPromise.h"
+
+namespace mozilla {
+
+using SetCDMPromise =
+ MozPromise<bool /* aIgnored */, MediaResult, /* IsExclusive */ true>;
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/MediaQueue.h b/dom/media/MediaQueue.h
new file mode 100644
index 0000000000..4609493339
--- /dev/null
+++ b/dom/media/MediaQueue.h
@@ -0,0 +1,277 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#if !defined(MediaQueue_h_)
+# define MediaQueue_h_
+
+# include <type_traits>
+
+# include "mozilla/RecursiveMutex.h"
+# include "mozilla/TaskQueue.h"
+
+# include "nsDeque.h"
+# include "MediaEventSource.h"
+# include "TimeUnits.h"
+
+namespace mozilla {
+
+extern LazyLogModule gMediaDecoderLog;
+
+# define QLOG(msg, ...) \
+ MOZ_LOG(gMediaDecoderLog, LogLevel::Debug, \
+ ("MediaQueue=%p " msg, this, ##__VA_ARGS__))
+
+class AudioData;
+class VideoData;
+
+template <typename T>
+struct TimestampAdjustmentTrait {
+ static const bool mValue = false;
+};
+
+template <>
+struct TimestampAdjustmentTrait<AudioData> {
+ static const bool mValue = true;
+};
+
+template <>
+struct TimestampAdjustmentTrait<VideoData> {
+ static const bool mValue = true;
+};
+
+template <typename T>
+struct NonTimestampAdjustmentTrait {
+ static const bool mValue = !TimestampAdjustmentTrait<T>::mValue;
+};
+
+template <class T>
+class MediaQueue : private nsRefPtrDeque<T> {
+ public:
+ MediaQueue()
+ : nsRefPtrDeque<T>(),
+ mRecursiveMutex("mediaqueue"),
+ mEndOfStream(false) {}
+
+ ~MediaQueue() { Reset(); }
+
+ inline size_t GetSize() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return nsRefPtrDeque<T>::GetSize();
+ }
+
+ template <typename U,
+ std::enable_if_t<TimestampAdjustmentTrait<U>::mValue, bool> = true>
+ inline void AdjustTimeStampIfNeeded(U* aItem) {
+ static_assert(std::is_same_v<U, AudioData> || std::is_same_v<U, VideoData>);
+ if (mOffset != media::TimeUnit::Zero()) {
+ const auto prev = aItem->mTime, prevEndTime = aItem->GetEndTime();
+ aItem->mTime += mOffset;
+ if (!aItem->mTime.IsValid()) {
+ NS_WARNING("Reverting timestamp adjustment due to sample overflow!");
+ aItem->mTime = prev;
+ } else {
+ QLOG("adjusted %s sample [%" PRId64 ",%" PRId64 "] -> [%" PRId64
+ ",%" PRId64 "]",
+ std::is_same_v<U, AudioData> ? "audio" : "video",
+ prev.ToMicroseconds(), prevEndTime.ToMicroseconds(),
+ aItem->mTime.ToMicroseconds(),
+ aItem->GetEndTime().ToMicroseconds());
+ }
+ }
+ }
+
+ template <typename U, std::enable_if_t<NonTimestampAdjustmentTrait<U>::mValue,
+ bool> = true>
+ inline void AdjustTimeStampIfNeeded(U* aItem) {}
+
+ enum class TimestampAdjustment {
+ Enable,
+ Disable,
+ };
+ inline void PushFront(
+ T* aItem, TimestampAdjustment aIsEnabled = TimestampAdjustment::Enable) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ if (aIsEnabled == TimestampAdjustment::Enable) {
+ AdjustTimeStampIfNeeded(aItem);
+ }
+ nsRefPtrDeque<T>::PushFront(aItem);
+ }
+
+ inline void Push(T* aItem) {
+ MOZ_DIAGNOSTIC_ASSERT(aItem);
+ Push(do_AddRef(aItem));
+ }
+
+ inline void Push(already_AddRefed<T> aItem) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ T* item = aItem.take();
+
+ MOZ_DIAGNOSTIC_ASSERT(item);
+ MOZ_DIAGNOSTIC_ASSERT(item->GetEndTime() >= item->mTime);
+ AdjustTimeStampIfNeeded(item);
+ nsRefPtrDeque<T>::Push(dont_AddRef(item));
+ mPushEvent.Notify(RefPtr<T>(item));
+
+ // Pushing new data after queue has ended means that the stream is active
+ // again, so we should not mark it as ended.
+ if (mEndOfStream) {
+ mEndOfStream = false;
+ }
+ }
+
+ inline already_AddRefed<T> PopFront() {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ RefPtr<T> rv = nsRefPtrDeque<T>::PopFront();
+ if (rv) {
+ MOZ_DIAGNOSTIC_ASSERT(rv->GetEndTime() >= rv->mTime);
+ mPopFrontEvent.Notify(RefPtr<T>(rv));
+ }
+ return rv.forget();
+ }
+
+ inline already_AddRefed<T> PopBack() {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return nsRefPtrDeque<T>::Pop();
+ }
+
+ inline RefPtr<T> PeekFront() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return nsRefPtrDeque<T>::PeekFront();
+ }
+
+ inline RefPtr<T> PeekBack() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return nsRefPtrDeque<T>::Peek();
+ }
+
+ void Reset() {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ nsRefPtrDeque<T>::Erase();
+ SetOffset(media::TimeUnit::Zero());
+ mEndOfStream = false;
+ }
+
+ bool AtEndOfStream() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return GetSize() == 0 && mEndOfStream;
+ }
+
+ // Returns true if the media queue has had its last item added to it.
+ // This happens when the media stream has been completely decoded. Note this
+ // does not mean that the corresponding stream has finished playback.
+ bool IsFinished() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mEndOfStream;
+ }
+
+ // Informs the media queue that it won't be receiving any more items.
+ void Finish() {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ if (!mEndOfStream) {
+ mEndOfStream = true;
+ mFinishEvent.Notify();
+ }
+ }
+
+ // Returns the approximate number of microseconds of items in the queue.
+ int64_t Duration() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ if (GetSize() == 0) {
+ return 0;
+ }
+ T* last = nsRefPtrDeque<T>::Peek();
+ T* first = nsRefPtrDeque<T>::PeekFront();
+ return (last->GetEndTime() - first->mTime).ToMicroseconds();
+ }
+
+ void LockedForEach(nsDequeFunctor<T>& aFunctor) const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ nsRefPtrDeque<T>::ForEach(aFunctor);
+ }
+
+ // Fill aResult with the elements which end later than the given time aTime.
+ void GetElementsAfter(const media::TimeUnit& aTime,
+ nsTArray<RefPtr<T>>* aResult) {
+ GetElementsAfterStrict(aTime.ToMicroseconds(), aResult);
+ }
+
+ void GetFirstElements(uint32_t aMaxElements, nsTArray<RefPtr<T>>* aResult) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ for (size_t i = 0; i < aMaxElements && i < GetSize(); ++i) {
+ *aResult->AppendElement() = nsRefPtrDeque<T>::ObjectAt(i);
+ }
+ }
+
+ uint32_t AudioFramesCount() {
+ static_assert(std::is_same_v<T, AudioData>,
+ "Only usable with MediaQueue<AudioData>");
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ uint32_t frames = 0;
+ for (size_t i = 0; i < GetSize(); ++i) {
+ T* v = nsRefPtrDeque<T>::ObjectAt(i);
+ frames += v->Frames();
+ }
+ return frames;
+ }
+
+ bool SetOffset(const media::TimeUnit& aOffset) {
+ if (!aOffset.IsValid()) {
+ QLOG("Invalid offset!");
+ return false;
+ }
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ mOffset = aOffset;
+ QLOG("Set media queue offset %" PRId64, mOffset.ToMicroseconds());
+ return true;
+ }
+
+ media::TimeUnit GetOffset() const {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ return mOffset;
+ }
+
+ MediaEventSource<RefPtr<T>>& PopFrontEvent() { return mPopFrontEvent; }
+
+ MediaEventSource<RefPtr<T>>& PushEvent() { return mPushEvent; }
+
+ MediaEventSource<void>& FinishEvent() { return mFinishEvent; }
+
+ private:
+ // Extracts elements from the queue into aResult, in order.
+ // Elements whose end time is before or equal to aTime are ignored.
+ void GetElementsAfterStrict(int64_t aTime, nsTArray<RefPtr<T>>* aResult) {
+ RecursiveMutexAutoLock lock(mRecursiveMutex);
+ if (GetSize() == 0) return;
+ size_t i;
+ for (i = GetSize() - 1; i > 0; --i) {
+ T* v = nsRefPtrDeque<T>::ObjectAt(i);
+ if (v->GetEndTime().ToMicroseconds() < aTime) break;
+ }
+ for (; i < GetSize(); ++i) {
+ RefPtr<T> elem = nsRefPtrDeque<T>::ObjectAt(i);
+ if (elem->GetEndTime().ToMicroseconds() > aTime) {
+ aResult->AppendElement(elem);
+ }
+ }
+ }
+
+ mutable RecursiveMutex mRecursiveMutex MOZ_UNANNOTATED;
+ MediaEventProducer<RefPtr<T>> mPopFrontEvent;
+ MediaEventProducer<RefPtr<T>> mPushEvent;
+ MediaEventProducer<void> mFinishEvent;
+ // True when we've decoded the last frame of data in the
+ // bitstream for which we're queueing frame data.
+ bool mEndOfStream;
+ // This offset will be added to any data pushed into the queue. We use it when
+ // the media queue starts receiving looped data, which timestamp needs to be
+ // modified.
+ media::TimeUnit mOffset;
+};
+
+} // namespace mozilla
+
+# undef QLOG
+
+#endif
diff --git a/dom/media/MediaRecorder.cpp b/dom/media/MediaRecorder.cpp
new file mode 100644
index 0000000000..9f8c2ad425
--- /dev/null
+++ b/dom/media/MediaRecorder.cpp
@@ -0,0 +1,1894 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaRecorder.h"
+
+#include "AudioNodeEngine.h"
+#include "AudioNodeTrack.h"
+#include "DOMMediaStream.h"
+#include "MediaDecoder.h"
+#include "MediaEncoder.h"
+#include "MediaTrackGraphImpl.h"
+#include "VideoUtils.h"
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/dom/AudioStreamTrack.h"
+#include "mozilla/dom/BlobEvent.h"
+#include "mozilla/dom/EmptyBlobImpl.h"
+#include "mozilla/dom/File.h"
+#include "mozilla/dom/MediaRecorderErrorEvent.h"
+#include "mozilla/dom/VideoStreamTrack.h"
+#include "mozilla/media/MediaUtils.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/TaskQueue.h"
+#include "nsContentTypeParser.h"
+#include "nsContentUtils.h"
+#include "nsDocShell.h"
+#include "nsError.h"
+#include "mozilla/dom/Document.h"
+#include "nsIPrincipal.h"
+#include "nsIScriptError.h"
+#include "nsMimeTypes.h"
+#include "nsProxyRelease.h"
+#include "nsServiceManagerUtils.h"
+#include "nsTArray.h"
+
+mozilla::LazyLogModule gMediaRecorderLog("MediaRecorder");
+#define LOG(type, msg) MOZ_LOG(gMediaRecorderLog, type, msg)
+
+constexpr int MIN_VIDEO_BITRATE_BPS = 10e3; // 10kbps
+constexpr int DEFAULT_VIDEO_BITRATE_BPS = 2500e3; // 2.5Mbps
+constexpr int MAX_VIDEO_BITRATE_BPS = 100e6; // 100Mbps
+
+constexpr int MIN_AUDIO_BITRATE_BPS = 500; // 500bps
+constexpr int DEFAULT_AUDIO_BITRATE_BPS = 128e3; // 128kbps
+constexpr int MAX_AUDIO_BITRATE_BPS = 512e3; // 512kbps
+
+namespace mozilla::dom {
+
+using namespace mozilla::media;
+
+/**
+ * MediaRecorderReporter measures memory being used by the Media Recorder.
+ *
+ * It is a singleton reporter and the single class object lives as long as at
+ * least one Recorder is registered. In MediaRecorder, the reporter is
+ * unregistered when it is destroyed.
+ */
+class MediaRecorderReporter final : public nsIMemoryReporter {
+ public:
+ static void AddMediaRecorder(MediaRecorder* aRecorder) {
+ if (!sUniqueInstance) {
+ sUniqueInstance = MakeAndAddRef<MediaRecorderReporter>();
+ RegisterWeakAsyncMemoryReporter(sUniqueInstance);
+ }
+ sUniqueInstance->mRecorders.AppendElement(aRecorder);
+ }
+
+ static void RemoveMediaRecorder(MediaRecorder* aRecorder) {
+ if (!sUniqueInstance) {
+ return;
+ }
+
+ sUniqueInstance->mRecorders.RemoveElement(aRecorder);
+ if (sUniqueInstance->mRecorders.IsEmpty()) {
+ UnregisterWeakMemoryReporter(sUniqueInstance);
+ sUniqueInstance = nullptr;
+ }
+ }
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ MediaRecorderReporter() = default;
+
+ NS_IMETHOD
+ CollectReports(nsIHandleReportCallback* aHandleReport, nsISupports* aData,
+ bool aAnonymize) override {
+ nsTArray<RefPtr<MediaRecorder::SizeOfPromise>> promises;
+ for (const RefPtr<MediaRecorder>& recorder : mRecorders) {
+ promises.AppendElement(recorder->SizeOfExcludingThis(MallocSizeOf));
+ }
+
+ nsCOMPtr<nsIHandleReportCallback> handleReport = aHandleReport;
+ nsCOMPtr<nsISupports> data = aData;
+ MediaRecorder::SizeOfPromise::All(GetCurrentSerialEventTarget(), promises)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [handleReport, data](const nsTArray<size_t>& sizes) {
+ nsCOMPtr<nsIMemoryReporterManager> manager =
+ do_GetService("@mozilla.org/memory-reporter-manager;1");
+ if (!manager) {
+ return;
+ }
+
+ size_t sum = 0;
+ for (const size_t& size : sizes) {
+ sum += size;
+ }
+
+ handleReport->Callback(""_ns, "explicit/media/recorder"_ns,
+ KIND_HEAP, UNITS_BYTES, sum,
+ "Memory used by media recorder."_ns, data);
+
+ manager->EndReport();
+ },
+ [](size_t) { MOZ_CRASH("Unexpected reject"); });
+
+ return NS_OK;
+ }
+
+ private:
+ MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf)
+
+ virtual ~MediaRecorderReporter() {
+ MOZ_ASSERT(mRecorders.IsEmpty(), "All recorders must have been removed");
+ }
+
+ static StaticRefPtr<MediaRecorderReporter> sUniqueInstance;
+
+ nsTArray<RefPtr<MediaRecorder>> mRecorders;
+};
+NS_IMPL_ISUPPORTS(MediaRecorderReporter, nsIMemoryReporter);
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(MediaRecorder)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(MediaRecorder,
+ DOMEventTargetHelper)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mStream)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAudioNode)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOtherDomException)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSecurityDomException)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mUnknownDomException)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(MediaRecorder,
+ DOMEventTargetHelper)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mStream)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mAudioNode)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mOtherDomException)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mSecurityDomException)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mUnknownDomException)
+ tmp->UnRegisterActivityObserver();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocument)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaRecorder)
+ NS_INTERFACE_MAP_ENTRY(nsIDocumentActivity)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+NS_IMPL_ADDREF_INHERITED(MediaRecorder, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(MediaRecorder, DOMEventTargetHelper)
+
+namespace {
+bool PrincipalSubsumes(MediaRecorder* aRecorder, nsIPrincipal* aPrincipal) {
+ if (!aRecorder->GetOwner()) {
+ return false;
+ }
+ nsCOMPtr<Document> doc = aRecorder->GetOwner()->GetExtantDoc();
+ if (!doc) {
+ return false;
+ }
+ if (!aPrincipal) {
+ return false;
+ }
+ bool subsumes;
+ if (NS_FAILED(doc->NodePrincipal()->Subsumes(aPrincipal, &subsumes))) {
+ return false;
+ }
+ return subsumes;
+}
+
+bool MediaStreamTracksPrincipalSubsumes(
+ MediaRecorder* aRecorder,
+ const nsTArray<RefPtr<MediaStreamTrack>>& aTracks) {
+ nsCOMPtr<nsIPrincipal> principal = nullptr;
+ for (const auto& track : aTracks) {
+ nsContentUtils::CombineResourcePrincipals(&principal,
+ track->GetPrincipal());
+ }
+ return PrincipalSubsumes(aRecorder, principal);
+}
+
+bool AudioNodePrincipalSubsumes(MediaRecorder* aRecorder,
+ AudioNode* aAudioNode) {
+ MOZ_ASSERT(aAudioNode);
+ Document* doc =
+ aAudioNode->GetOwner() ? aAudioNode->GetOwner()->GetExtantDoc() : nullptr;
+ nsCOMPtr<nsIPrincipal> principal = doc ? doc->NodePrincipal() : nullptr;
+ return PrincipalSubsumes(aRecorder, principal);
+}
+
+// This list is sorted so that lesser failures are later, so that
+// IsTypeSupportedImpl() can report the error from audio or video types that
+// is closer to being supported.
+enum class TypeSupport {
+ MediaTypeInvalid,
+ NoVideoWithAudioType,
+ ContainersDisabled,
+ CodecsDisabled,
+ ContainerUnsupported,
+ CodecUnsupported,
+ CodecDuplicated,
+ Supported,
+};
+
+nsCString TypeSupportToCString(TypeSupport aSupport,
+ const nsAString& aMimeType) {
+ nsAutoCString mime = NS_ConvertUTF16toUTF8(aMimeType);
+ switch (aSupport) {
+ case TypeSupport::Supported:
+ return nsPrintfCString("%s is supported", mime.get());
+ case TypeSupport::MediaTypeInvalid:
+ return nsPrintfCString("%s is not a valid media type", mime.get());
+ case TypeSupport::NoVideoWithAudioType:
+ return nsPrintfCString(
+ "Video cannot be recorded with %s as it is an audio type",
+ mime.get());
+ case TypeSupport::ContainersDisabled:
+ return "All containers are disabled"_ns;
+ case TypeSupport::CodecsDisabled:
+ return "All codecs are disabled"_ns;
+ case TypeSupport::ContainerUnsupported:
+ return nsPrintfCString("%s indicates an unsupported container",
+ mime.get());
+ case TypeSupport::CodecUnsupported:
+ return nsPrintfCString("%s indicates an unsupported codec", mime.get());
+ case TypeSupport::CodecDuplicated:
+ return nsPrintfCString("%s contains the same codec multiple times",
+ mime.get());
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unknown TypeSupport");
+ return "Unknown error"_ns;
+ }
+}
+
+TypeSupport CanRecordAudioTrackWith(const Maybe<MediaContainerType>& aMimeType,
+ const nsAString& aMimeTypeString) {
+ if (aMimeTypeString.IsEmpty()) {
+ // For the empty string we just need to check whether we have support for an
+ // audio container and an audio codec.
+ if (!MediaEncoder::IsWebMEncoderEnabled() &&
+ !MediaDecoder::IsOggEnabled()) {
+ // No container support for audio.
+ return TypeSupport::ContainersDisabled;
+ }
+
+ if (!MediaDecoder::IsOpusEnabled()) {
+ // No codec support for audio.
+ return TypeSupport::CodecsDisabled;
+ }
+
+ return TypeSupport::Supported;
+ }
+
+ if (!aMimeType) {
+ // A mime type string was set, but it couldn't be parsed to a valid
+ // MediaContainerType.
+ return TypeSupport::MediaTypeInvalid;
+ }
+
+ if (aMimeType->Type() != MEDIAMIMETYPE(VIDEO_WEBM) &&
+ aMimeType->Type() != MEDIAMIMETYPE(AUDIO_WEBM) &&
+ aMimeType->Type() != MEDIAMIMETYPE(AUDIO_OGG)) {
+ // Any currently supported container can record audio.
+ return TypeSupport::ContainerUnsupported;
+ }
+
+ if (aMimeType->Type() == MEDIAMIMETYPE(VIDEO_WEBM) &&
+ !MediaEncoder::IsWebMEncoderEnabled()) {
+ return TypeSupport::ContainerUnsupported;
+ }
+
+ if (aMimeType->Type() == MEDIAMIMETYPE(AUDIO_WEBM) &&
+ !MediaEncoder::IsWebMEncoderEnabled()) {
+ return TypeSupport::ContainerUnsupported;
+ }
+
+ if (aMimeType->Type() == MEDIAMIMETYPE(AUDIO_OGG) &&
+ !MediaDecoder::IsOggEnabled()) {
+ return TypeSupport::ContainerUnsupported;
+ }
+
+ if (!MediaDecoder::IsOpusEnabled()) {
+ return TypeSupport::CodecUnsupported;
+ }
+
+ if (!aMimeType->ExtendedType().HaveCodecs()) {
+ // No codecs constrained, we can pick opus.
+ return TypeSupport::Supported;
+ }
+
+ size_t opus = 0;
+ size_t unknown = 0;
+ for (const auto& codec : aMimeType->ExtendedType().Codecs().Range()) {
+ // Ignore video codecs.
+ if (codec.EqualsLiteral("vp8")) {
+ continue;
+ }
+ if (codec.EqualsLiteral("vp8.0")) {
+ continue;
+ }
+ if (codec.EqualsLiteral("opus")) {
+ // All containers support opus
+ opus++;
+ continue;
+ }
+ unknown++;
+ }
+
+ if (unknown > 0) {
+ // Unsupported codec.
+ return TypeSupport::CodecUnsupported;
+ }
+
+ if (opus == 0) {
+ // Codecs specified but not opus. Unsupported for audio.
+ return TypeSupport::CodecUnsupported;
+ }
+
+ if (opus > 1) {
+ // Opus specified more than once. Bad form.
+ return TypeSupport::CodecDuplicated;
+ }
+
+ return TypeSupport::Supported;
+}
+
+TypeSupport CanRecordVideoTrackWith(const Maybe<MediaContainerType>& aMimeType,
+ const nsAString& aMimeTypeString) {
+ if (aMimeTypeString.IsEmpty()) {
+ // For the empty string we just need to check whether we have support for a
+ // video container and a video codec. The VP8 encoder is always available.
+ if (!MediaEncoder::IsWebMEncoderEnabled()) {
+ // No container support for video.
+ return TypeSupport::ContainersDisabled;
+ }
+
+ return TypeSupport::Supported;
+ }
+
+ if (!aMimeType) {
+ // A mime type string was set, but it couldn't be parsed to a valid
+ // MediaContainerType.
+ return TypeSupport::MediaTypeInvalid;
+ }
+
+ if (!aMimeType->Type().HasVideoMajorType()) {
+ return TypeSupport::NoVideoWithAudioType;
+ }
+
+ if (aMimeType->Type() != MEDIAMIMETYPE(VIDEO_WEBM)) {
+ return TypeSupport::ContainerUnsupported;
+ }
+
+ if (!MediaEncoder::IsWebMEncoderEnabled()) {
+ return TypeSupport::ContainerUnsupported;
+ }
+
+ if (!aMimeType->ExtendedType().HaveCodecs()) {
+ // No codecs constrained, we can pick vp8.
+ return TypeSupport::Supported;
+ }
+
+ size_t vp8 = 0;
+ size_t unknown = 0;
+ for (const auto& codec : aMimeType->ExtendedType().Codecs().Range()) {
+ if (codec.EqualsLiteral("opus")) {
+ // Ignore audio codecs.
+ continue;
+ }
+ if (codec.EqualsLiteral("vp8")) {
+ vp8++;
+ continue;
+ }
+ if (codec.EqualsLiteral("vp8.0")) {
+ vp8++;
+ continue;
+ }
+ unknown++;
+ }
+
+ if (unknown > 0) {
+ // Unsupported codec.
+ return TypeSupport::CodecUnsupported;
+ }
+
+ if (vp8 == 0) {
+ // Codecs specified but not vp8. Unsupported for video.
+ return TypeSupport::CodecUnsupported;
+ }
+
+ if (vp8 > 1) {
+ // Vp8 specified more than once. Bad form.
+ return TypeSupport::CodecDuplicated;
+ }
+
+ return TypeSupport::Supported;
+}
+
+TypeSupport CanRecordWith(MediaStreamTrack* aTrack,
+ const Maybe<MediaContainerType>& aMimeType,
+ const nsAString& aMimeTypeString) {
+ if (aTrack->AsAudioStreamTrack()) {
+ return CanRecordAudioTrackWith(aMimeType, aMimeTypeString);
+ }
+
+ if (aTrack->AsVideoStreamTrack()) {
+ return CanRecordVideoTrackWith(aMimeType, aMimeTypeString);
+ }
+
+ MOZ_CRASH("Unexpected track type");
+}
+
+TypeSupport IsTypeSupportedImpl(const nsAString& aMIMEType) {
+ if (aMIMEType.IsEmpty()) {
+ // Lie and return true even if no container/codec support is enabled,
+ // because the spec mandates it.
+ return TypeSupport::Supported;
+ }
+ Maybe<MediaContainerType> mime = MakeMediaContainerType(aMIMEType);
+ TypeSupport audioSupport = CanRecordAudioTrackWith(mime, aMIMEType);
+ TypeSupport videoSupport = CanRecordVideoTrackWith(mime, aMIMEType);
+ return std::max(audioSupport, videoSupport);
+}
+
+nsString SelectMimeType(bool aHasVideo, bool aHasAudio,
+ const nsString& aConstrainedMimeType) {
+ MOZ_ASSERT(aHasVideo || aHasAudio);
+
+ Maybe<MediaContainerType> constrainedType =
+ MakeMediaContainerType(aConstrainedMimeType);
+
+ // If we are recording video, Start() should have rejected any non-video mime
+ // types.
+ MOZ_ASSERT_IF(constrainedType && aHasVideo,
+ constrainedType->Type().HasVideoMajorType());
+ // IsTypeSupported() rejects application mime types.
+ MOZ_ASSERT_IF(constrainedType,
+ !constrainedType->Type().HasApplicationMajorType());
+
+ nsString result;
+ if (constrainedType && constrainedType->ExtendedType().HaveCodecs()) {
+ // The constrained mime type is fully defined (it has codecs!). No need to
+ // select anything.
+ CopyUTF8toUTF16(constrainedType->OriginalString(), result);
+ } else {
+ // There is no constrained mime type, or there is and it is not fully
+ // defined but still valid. Select what's missing, so that we have major
+ // type, container and codecs.
+
+ // If there is a constrained mime type it should not have codecs defined,
+ // because then it is fully defined and used unchanged (covered earlier).
+ MOZ_ASSERT_IF(constrainedType,
+ !constrainedType->ExtendedType().HaveCodecs());
+
+ nsCString majorType;
+ {
+ if (constrainedType) {
+ // There is a constrained type. It has both major type and container in
+ // order to be valid. Use them as is.
+ majorType = constrainedType->Type().AsString();
+ } else if (aHasVideo) {
+ majorType = nsLiteralCString(VIDEO_WEBM);
+ } else {
+ majorType = nsLiteralCString(AUDIO_OGG);
+ }
+ }
+
+ nsCString codecs;
+ {
+ if (aHasVideo && aHasAudio) {
+ codecs = "\"vp8, opus\""_ns;
+ } else if (aHasVideo) {
+ codecs = "vp8"_ns;
+ } else {
+ codecs = "opus"_ns;
+ }
+ }
+ result = NS_ConvertUTF8toUTF16(
+ nsPrintfCString("%s; codecs=%s", majorType.get(), codecs.get()));
+ }
+
+ MOZ_ASSERT_IF(aHasAudio,
+ CanRecordAudioTrackWith(MakeMediaContainerType(result),
+ result) == TypeSupport::Supported);
+ MOZ_ASSERT_IF(aHasVideo,
+ CanRecordVideoTrackWith(MakeMediaContainerType(result),
+ result) == TypeSupport::Supported);
+ return result;
+}
+
+void SelectBitrates(uint32_t aBitsPerSecond, uint8_t aNumVideoTracks,
+ uint32_t* aOutVideoBps, uint8_t aNumAudioTracks,
+ uint32_t* aOutAudioBps) {
+ uint32_t vbps = 0;
+ uint32_t abps = 0;
+
+ const uint32_t minVideoBps = MIN_VIDEO_BITRATE_BPS * aNumVideoTracks;
+ const uint32_t maxVideoBps = MAX_VIDEO_BITRATE_BPS * aNumVideoTracks;
+
+ const uint32_t minAudioBps = MIN_AUDIO_BITRATE_BPS * aNumAudioTracks;
+ const uint32_t maxAudioBps = MAX_AUDIO_BITRATE_BPS * aNumAudioTracks;
+
+ if (aNumVideoTracks == 0) {
+ MOZ_DIAGNOSTIC_ASSERT(aNumAudioTracks > 0);
+ abps = std::min(maxAudioBps, std::max(minAudioBps, aBitsPerSecond));
+ } else if (aNumAudioTracks == 0) {
+ vbps = std::min(maxVideoBps, std::max(minVideoBps, aBitsPerSecond));
+ } else {
+ // Scale the bits so that video gets 20 times the bits of audio.
+ // Since we must account for varying number of tracks of each type we weight
+ // them by type; video = weight 20, audio = weight 1.
+ const uint32_t videoWeight = aNumVideoTracks * 20;
+ const uint32_t audioWeight = aNumAudioTracks;
+ const uint32_t totalWeights = audioWeight + videoWeight;
+ const uint32_t videoBitrate =
+ uint64_t(aBitsPerSecond) * videoWeight / totalWeights;
+ const uint32_t audioBitrate =
+ uint64_t(aBitsPerSecond) * audioWeight / totalWeights;
+ vbps = std::min(maxVideoBps, std::max(minVideoBps, videoBitrate));
+ abps = std::min(maxAudioBps, std::max(minAudioBps, audioBitrate));
+ }
+
+ *aOutVideoBps = vbps;
+ *aOutAudioBps = abps;
+}
+} // namespace
+
+/**
+ * Session is an object to represent a single recording event.
+ * In original design, all recording context is stored in MediaRecorder, which
+ * causes a problem if someone calls MediaRecorder::Stop and
+ * MediaRecorder::Start quickly. To prevent blocking main thread, media encoding
+ * is executed in a second thread, named encoder thread. For the same reason, we
+ * do not await encoder thread shutdown in MediaRecorder::Stop.
+ * If someone calls MediaRecorder::Start before encoder thread shutdown, the
+ * same recording context in MediaRecorder might be accessed by two distinct
+ * encoder threads, which would be racy. With the recording context, including
+ * the encoder thread, in a Session object the problem is solved.
+ *
+ * Lifetime of MediaRecorder and Session objects.
+ * 1) MediaRecorder creates a Session in MediaRecorder::Start() and holds
+ * a reference to it. Then the Session registers itself to a ShutdownBlocker
+ * and also holds a reference to MediaRecorder.
+ * Therefore, the reference dependency in gecko is:
+ * ShutdownBlocker -> Session <-> MediaRecorder, note that there is a cycle
+ * reference between Session and MediaRecorder.
+ * 2) A Session is destroyed after Session::DoSessionEndTask() has been called
+ * _and_ all encoded media data has been passed to OnDataAvailable handler.
+ * In some cases the encoded media can be discarded before being passed to
+ * the OnDataAvailable handler.
+ * 3) Session::DoSessionEndTask is called by an application through
+ * MediaRecorder::Stop(), from a MediaEncoder Shutdown notification, from the
+ * document going inactive or invisible, or from the ShutdownBlocker.
+ */
+class MediaRecorder::Session : public PrincipalChangeObserver<MediaStreamTrack>,
+ public DOMMediaStream::TrackListener {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(Session)
+
+ struct TrackTypeComparator {
+ enum Type {
+ AUDIO,
+ VIDEO,
+ };
+ static bool Equals(const RefPtr<MediaStreamTrack>& aTrack, Type aType) {
+ return (aType == AUDIO && aTrack->AsAudioStreamTrack()) ||
+ (aType == VIDEO && aTrack->AsVideoStreamTrack());
+ }
+ };
+
+ public:
+ Session(MediaRecorder* aRecorder,
+ nsTArray<RefPtr<MediaStreamTrack>> aMediaStreamTracks,
+ uint32_t aVideoBitsPerSecond, uint32_t aAudioBitsPerSecond)
+ : mRecorder(aRecorder),
+ mMediaStreamTracks(std::move(aMediaStreamTracks)),
+ mMainThread(mRecorder->GetOwner()->EventTargetFor(TaskCategory::Other)),
+ mMimeType(SelectMimeType(
+ mMediaStreamTracks.Contains(TrackTypeComparator::VIDEO,
+ TrackTypeComparator()),
+ mRecorder->mAudioNode ||
+ mMediaStreamTracks.Contains(TrackTypeComparator::AUDIO,
+ TrackTypeComparator()),
+ mRecorder->mConstrainedMimeType)),
+ mVideoBitsPerSecond(aVideoBitsPerSecond),
+ mAudioBitsPerSecond(aAudioBitsPerSecond),
+ mStartTime(TimeStamp::Now()),
+ mRunningState(RunningState::Idling) {
+ MOZ_ASSERT(NS_IsMainThread());
+ Telemetry::ScalarAdd(Telemetry::ScalarID::MEDIARECORDER_RECORDING_COUNT, 1);
+ }
+
+ void PrincipalChanged(MediaStreamTrack* aTrack) override {
+ NS_ASSERTION(mMediaStreamTracks.Contains(aTrack),
+ "Principal changed for unrecorded track");
+ if (!MediaStreamTracksPrincipalSubsumes(mRecorder, mMediaStreamTracks)) {
+ DoSessionEndTask(NS_ERROR_DOM_SECURITY_ERR);
+ }
+ }
+
+ void NotifyTrackAdded(const RefPtr<MediaStreamTrack>& aTrack) override {
+ LOG(LogLevel::Warning,
+ ("Session.NotifyTrackAdded %p Raising error due to track set change",
+ this));
+ // There's a chance we have a sensible JS stack here.
+ if (!mRecorder->mOtherDomException) {
+ mRecorder->mOtherDomException = DOMException::Create(
+ NS_ERROR_DOM_INVALID_MODIFICATION_ERR,
+ "An attempt was made to add a track to the recorded MediaStream "
+ "during the recording"_ns);
+ }
+ DoSessionEndTask(NS_ERROR_DOM_INVALID_MODIFICATION_ERR);
+ }
+
+ void NotifyTrackRemoved(const RefPtr<MediaStreamTrack>& aTrack) override {
+ if (aTrack->Ended()) {
+ // TrackEncoder will pickup tracks that end itself.
+ return;
+ }
+ LOG(LogLevel::Warning,
+ ("Session.NotifyTrackRemoved %p Raising error due to track set change",
+ this));
+ // There's a chance we have a sensible JS stack here.
+ if (!mRecorder->mOtherDomException) {
+ mRecorder->mOtherDomException = DOMException::Create(
+ NS_ERROR_DOM_INVALID_MODIFICATION_ERR,
+ "An attempt was made to remove a track from the recorded MediaStream "
+ "during the recording"_ns);
+ }
+ DoSessionEndTask(NS_ERROR_DOM_INVALID_MODIFICATION_ERR);
+ }
+
+ void Start(TimeDuration aTimeslice) {
+ LOG(LogLevel::Debug, ("Session.Start %p", this));
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mRecorder->mStream) {
+ // The TrackListener reports back when tracks are added or removed from
+ // the MediaStream.
+ mMediaStream = mRecorder->mStream;
+ mMediaStream->RegisterTrackListener(this);
+
+ uint8_t trackTypes = 0;
+ for (const auto& track : mMediaStreamTracks) {
+ if (track->AsAudioStreamTrack()) {
+ trackTypes |= ContainerWriter::CREATE_AUDIO_TRACK;
+ } else if (track->AsVideoStreamTrack()) {
+ trackTypes |= ContainerWriter::CREATE_VIDEO_TRACK;
+ } else {
+ MOZ_CRASH("Unexpected track type");
+ }
+ }
+
+ for (const auto& t : mMediaStreamTracks) {
+ t->AddPrincipalChangeObserver(this);
+ }
+
+ LOG(LogLevel::Debug, ("Session.Start track types = (%d)", trackTypes));
+ InitEncoder(trackTypes, mMediaStreamTracks[0]->Graph()->GraphRate(),
+ aTimeslice);
+ return;
+ }
+
+ if (mRecorder->mAudioNode) {
+ TrackRate trackRate =
+ mRecorder->mAudioNode->Context()->Graph()->GraphRate();
+
+ // Web Audio node has only audio.
+ InitEncoder(ContainerWriter::CREATE_AUDIO_TRACK, trackRate, aTimeslice);
+ return;
+ }
+
+ MOZ_ASSERT(false, "Unknown source");
+ }
+
+ void Stop() {
+ LOG(LogLevel::Debug, ("Session.Stop %p", this));
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mEncoder) {
+ mEncoder->DisconnectTracks();
+ }
+
+ // Remove main thread state added in Start().
+ if (mMediaStream) {
+ mMediaStream->UnregisterTrackListener(this);
+ mMediaStream = nullptr;
+ }
+
+ {
+ for (const auto& track : mMediaStreamTracks) {
+ track->RemovePrincipalChangeObserver(this);
+ }
+ }
+
+ if (mRunningState.isOk() &&
+ mRunningState.inspect() == RunningState::Idling) {
+ LOG(LogLevel::Debug, ("Session.Stop Explicit end task %p", this));
+ // End the Session directly if there is no encoder.
+ DoSessionEndTask(NS_OK);
+ } else if (mRunningState.isOk() &&
+ (mRunningState.inspect() == RunningState::Starting ||
+ mRunningState.inspect() == RunningState::Running)) {
+ if (mRunningState.inspect() == RunningState::Starting) {
+ // The MediaEncoder might not report started, but by spec we must fire
+ // "start".
+ mStartedListener.DisconnectIfExists();
+ NS_DispatchToMainThread(NewRunnableMethod(
+ "MediaRecorder::Session::Stop", this, &Session::OnStarted));
+ }
+ mRunningState = RunningState::Stopping;
+ }
+ }
+
+ void Pause() {
+ LOG(LogLevel::Debug, ("Session.Pause"));
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT_IF(mRunningState.isOk(),
+ mRunningState.unwrap() != RunningState::Idling);
+ if (mRunningState.isErr() ||
+ mRunningState.unwrap() == RunningState::Stopping ||
+ mRunningState.unwrap() == RunningState::Stopped) {
+ return;
+ }
+ MOZ_ASSERT(mEncoder);
+ mEncoder->Suspend();
+ }
+
+ void Resume() {
+ LOG(LogLevel::Debug, ("Session.Resume"));
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT_IF(mRunningState.isOk(),
+ mRunningState.unwrap() != RunningState::Idling);
+ if (mRunningState.isErr() ||
+ mRunningState.unwrap() == RunningState::Stopping ||
+ mRunningState.unwrap() == RunningState::Stopped) {
+ return;
+ }
+ MOZ_ASSERT(mEncoder);
+ mEncoder->Resume();
+ }
+
+ void RequestData() {
+ LOG(LogLevel::Debug, ("Session.RequestData"));
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mEncoder);
+
+ InvokeAsync(mEncoderThread, mEncoder.get(), __func__,
+ &MediaEncoder::RequestData)
+ ->Then(
+ mMainThread, __func__,
+ [this, self = RefPtr<Session>(this)](
+ const MediaEncoder::BlobPromise::ResolveOrRejectValue& aRrv) {
+ if (aRrv.IsReject()) {
+ LOG(LogLevel::Warning, ("RequestData failed"));
+ DoSessionEndTask(aRrv.RejectValue());
+ return;
+ }
+
+ nsresult rv =
+ mRecorder->CreateAndDispatchBlobEvent(aRrv.ResolveValue());
+ if (NS_FAILED(rv)) {
+ DoSessionEndTask(NS_OK);
+ }
+ });
+ }
+
+ public:
+ RefPtr<SizeOfPromise> SizeOfExcludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!mEncoder) {
+ return SizeOfPromise::CreateAndResolve(0, __func__);
+ }
+
+ return mEncoder->SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ private:
+ virtual ~Session() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mShutdownPromise);
+ MOZ_ASSERT(!mShutdownBlocker);
+ LOG(LogLevel::Debug, ("Session.~Session (%p)", this));
+ }
+
+ void InitEncoder(uint8_t aTrackTypes, TrackRate aTrackRate,
+ TimeDuration aTimeslice) {
+ LOG(LogLevel::Debug, ("Session.InitEncoder %p", this));
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mRunningState.isOk() ||
+ mRunningState.inspect() != RunningState::Idling) {
+ MOZ_ASSERT_UNREACHABLE("Double-init");
+ return;
+ }
+
+ // Create a TaskQueue to read encode media data from MediaEncoder.
+ MOZ_RELEASE_ASSERT(!mEncoderThread);
+ RefPtr<SharedThreadPool> pool =
+ GetMediaThreadPool(MediaThreadType::WEBRTC_WORKER);
+ if (!pool) {
+ LOG(LogLevel::Debug, ("Session.InitEncoder %p Failed to create "
+ "MediaRecorderReadThread thread pool",
+ this));
+ DoSessionEndTask(NS_ERROR_FAILURE);
+ return;
+ }
+
+ mEncoderThread =
+ TaskQueue::Create(pool.forget(), "MediaRecorderReadThread");
+
+ MOZ_DIAGNOSTIC_ASSERT(!mShutdownBlocker);
+ // Add a shutdown blocker so mEncoderThread can be shutdown async.
+ class Blocker : public ShutdownBlocker {
+ const RefPtr<Session> mSession;
+
+ public:
+ Blocker(RefPtr<Session> aSession, const nsString& aName)
+ : ShutdownBlocker(aName), mSession(std::move(aSession)) {}
+
+ NS_IMETHOD BlockShutdown(nsIAsyncShutdownClient*) override {
+ mSession->DoSessionEndTask(NS_ERROR_ABORT);
+ return NS_OK;
+ }
+ };
+
+ nsCOMPtr<nsIAsyncShutdownClient> barrier = GetShutdownBarrier();
+ if (!barrier) {
+ LOG(LogLevel::Error,
+ ("Session.InitEncoder %p Failed to get shutdown barrier", this));
+ DoSessionEndTask(NS_ERROR_FAILURE);
+ return;
+ }
+
+ nsString name;
+ name.AppendPrintf("MediaRecorder::Session %p shutdown", this);
+ mShutdownBlocker = MakeAndAddRef<Blocker>(this, name);
+ nsresult rv = barrier->AddBlocker(
+ mShutdownBlocker, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__,
+ u"MediaRecorder::Session: shutdown"_ns);
+ MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
+
+ uint32_t maxMemory = Preferences::GetUint("media.recorder.max_memory",
+ MAX_ALLOW_MEMORY_BUFFER);
+
+ mEncoder = MediaEncoder::CreateEncoder(
+ mEncoderThread, mMimeType, mAudioBitsPerSecond, mVideoBitsPerSecond,
+ aTrackTypes, aTrackRate, maxMemory, aTimeslice);
+
+ if (!mEncoder) {
+ LOG(LogLevel::Error, ("Session.InitEncoder !mEncoder %p", this));
+ DoSessionEndTask(NS_ERROR_ABORT);
+ return;
+ }
+
+ mStartedListener = mEncoder->StartedEvent().Connect(mMainThread, this,
+ &Session::OnStarted);
+ mDataAvailableListener = mEncoder->DataAvailableEvent().Connect(
+ mMainThread, this, &Session::OnDataAvailable);
+ mErrorListener =
+ mEncoder->ErrorEvent().Connect(mMainThread, this, &Session::OnError);
+ mShutdownListener = mEncoder->ShutdownEvent().Connect(mMainThread, this,
+ &Session::OnShutdown);
+
+ if (mRecorder->mAudioNode) {
+ mEncoder->ConnectAudioNode(mRecorder->mAudioNode,
+ mRecorder->mAudioNodeOutput);
+ }
+
+ for (const auto& track : mMediaStreamTracks) {
+ mEncoder->ConnectMediaStreamTrack(track);
+ }
+
+ // Set mRunningState to Running so that DoSessionEndTask will
+ // take the responsibility to end the session.
+ mRunningState = RunningState::Starting;
+ }
+
+ // This is the task that will stop recording per spec:
+ // - If rv is NS_ERROR_ABORT or NS_ERROR_DOM_SECURITY_ERR, cancel the encoders
+ // - Otherwise, stop the encoders gracefully, this still encodes buffered data
+ // - Set state to "inactive"
+ // - Fire an error event, if NS_FAILED(rv)
+ // - Discard blob data if rv is NS_ERROR_DOM_SECURITY_ERR
+ // - Fire a Blob event
+ // - Fire an event named stop
+ void DoSessionEndTask(nsresult rv) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mRunningState.isErr()) {
+ // We have already ended with an error.
+ return;
+ }
+
+ if (mRunningState.isOk() &&
+ mRunningState.inspect() == RunningState::Stopped) {
+ // We have already ended gracefully.
+ return;
+ }
+
+ bool needsStartEvent = false;
+ if (mRunningState.isOk() &&
+ (mRunningState.inspect() == RunningState::Idling ||
+ mRunningState.inspect() == RunningState::Starting)) {
+ needsStartEvent = true;
+ }
+
+ // Set a terminated running state. Future DoSessionEnd tasks will exit
+ // early.
+ if (rv == NS_OK) {
+ mRunningState = RunningState::Stopped;
+ } else {
+ mRunningState = Err(rv);
+ }
+
+ RefPtr<MediaEncoder::BlobPromise> blobPromise;
+ if (!mEncoder) {
+ blobPromise = MediaEncoder::BlobPromise::CreateAndReject(NS_OK, __func__);
+ } else {
+ blobPromise =
+ (rv == NS_ERROR_ABORT || rv == NS_ERROR_DOM_SECURITY_ERR
+ ? mEncoder->Cancel()
+ : mEncoder->Stop())
+ ->Then(mEncoderThread, __func__,
+ [encoder = mEncoder](
+ const GenericNonExclusivePromise::ResolveOrRejectValue&
+ aValue) {
+ MOZ_DIAGNOSTIC_ASSERT(aValue.IsResolve());
+ return encoder->RequestData();
+ });
+ }
+
+ blobPromise
+ ->Then(
+ mMainThread, __func__,
+ [this, self = RefPtr<Session>(this), rv, needsStartEvent](
+ const MediaEncoder::BlobPromise::ResolveOrRejectValue& aRv) {
+ if (mRecorder->mSessions.LastElement() == this) {
+ // Set state to inactive, but only if the recorder is not
+ // controlled by another session already.
+ mRecorder->Inactivate();
+ }
+
+ if (needsStartEvent) {
+ mRecorder->DispatchSimpleEvent(u"start"_ns);
+ }
+
+ // If there was an error, Fire the appropriate one
+ if (NS_FAILED(rv)) {
+ mRecorder->NotifyError(rv);
+ }
+
+ // Fire a blob event named dataavailable
+ RefPtr<BlobImpl> blobImpl;
+ if (rv == NS_ERROR_DOM_SECURITY_ERR || aRv.IsReject()) {
+ // In case of SecurityError, the blob data must be discarded.
+ // We create a new empty one and throw the blob with its data
+ // away.
+ // In case we failed to gather blob data, we create an empty
+ // memory blob instead.
+ blobImpl = new EmptyBlobImpl(mMimeType);
+ } else {
+ blobImpl = aRv.ResolveValue();
+ }
+ if (NS_FAILED(mRecorder->CreateAndDispatchBlobEvent(blobImpl))) {
+ // Failed to dispatch blob event. That's unexpected. It's
+ // probably all right to fire an error event if we haven't
+ // already.
+ if (NS_SUCCEEDED(rv)) {
+ mRecorder->NotifyError(NS_ERROR_FAILURE);
+ }
+ }
+
+ // Fire an event named stop
+ mRecorder->DispatchSimpleEvent(u"stop"_ns);
+
+ // And finally, Shutdown and destroy the Session
+ return Shutdown();
+ })
+ ->Then(mMainThread, __func__, [this, self = RefPtr<Session>(this)] {
+ // Guard against the case where we fail to add a blocker due to being
+ // in XPCOM shutdown. If we're in this state we shouldn't try and get
+ // a shutdown barrier as we'll fail.
+ if (!mShutdownBlocker) {
+ return;
+ }
+ MustGetShutdownBarrier()->RemoveBlocker(mShutdownBlocker);
+ mShutdownBlocker = nullptr;
+ });
+ }
+
+ void OnStarted() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mRunningState.isErr()) {
+ return;
+ }
+ RunningState state = mRunningState.inspect();
+ if (state == RunningState::Starting || state == RunningState::Stopping) {
+ if (state == RunningState::Starting) {
+ // We set it to Running in the runnable since we can only assign
+ // mRunningState on main thread. We set it before running the start
+ // event runnable since that dispatches synchronously (and may cause
+ // js calls to methods depending on mRunningState).
+ mRunningState = RunningState::Running;
+
+ mRecorder->mMimeType = mEncoder->mMimeType;
+ }
+ mRecorder->DispatchSimpleEvent(u"start"_ns);
+ }
+ }
+
+ void OnDataAvailable(const RefPtr<BlobImpl>& aBlob) {
+ if (mRunningState.isErr() &&
+ mRunningState.unwrapErr() == NS_ERROR_DOM_SECURITY_ERR) {
+ return;
+ }
+ if (NS_WARN_IF(NS_FAILED(mRecorder->CreateAndDispatchBlobEvent(aBlob)))) {
+ LOG(LogLevel::Warning,
+ ("MediaRecorder %p Creating or dispatching BlobEvent failed", this));
+ DoSessionEndTask(NS_OK);
+ }
+ }
+
+ void OnError() {
+ MOZ_ASSERT(NS_IsMainThread());
+ DoSessionEndTask(NS_ERROR_FAILURE);
+ }
+
+ void OnShutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ DoSessionEndTask(NS_OK);
+ }
+
+ RefPtr<ShutdownPromise> Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ LOG(LogLevel::Debug, ("Session Shutdown %p", this));
+
+ if (mShutdownPromise) {
+ return mShutdownPromise;
+ }
+
+ // This is a coarse calculation and does not reflect the duration of the
+ // final recording for reasons such as pauses. However it allows us an
+ // idea of how long people are running their recorders for.
+ TimeDuration timeDelta = TimeStamp::Now() - mStartTime;
+ Telemetry::Accumulate(Telemetry::MEDIA_RECORDER_RECORDING_DURATION,
+ timeDelta.ToSeconds());
+
+ mShutdownPromise = ShutdownPromise::CreateAndResolve(true, __func__);
+
+ if (mEncoder) {
+ mShutdownPromise =
+ mShutdownPromise
+ ->Then(mMainThread, __func__,
+ [this, self = RefPtr<Session>(this)] {
+ mStartedListener.DisconnectIfExists();
+ mDataAvailableListener.DisconnectIfExists();
+ mErrorListener.DisconnectIfExists();
+ mShutdownListener.DisconnectIfExists();
+ return mEncoder->Cancel();
+ })
+ ->Then(mEncoderThread, __func__, [] {
+ // Meh, this is just to convert the promise type to match
+ // mShutdownPromise.
+ return ShutdownPromise::CreateAndResolve(true, __func__);
+ });
+ }
+
+ // Remove main thread state. This could be needed if Stop() wasn't called.
+ if (mMediaStream) {
+ mMediaStream->UnregisterTrackListener(this);
+ mMediaStream = nullptr;
+ }
+
+ {
+ auto tracks(std::move(mMediaStreamTracks));
+ for (RefPtr<MediaStreamTrack>& track : tracks) {
+ track->RemovePrincipalChangeObserver(this);
+ }
+ }
+
+ // Break the cycle reference between Session and MediaRecorder.
+ mShutdownPromise = mShutdownPromise->Then(
+ mMainThread, __func__,
+ [self = RefPtr<Session>(this)]() {
+ self->mRecorder->RemoveSession(self);
+ return ShutdownPromise::CreateAndResolve(true, __func__);
+ },
+ []() {
+ MOZ_ASSERT_UNREACHABLE("Unexpected reject");
+ return ShutdownPromise::CreateAndReject(false, __func__);
+ });
+
+ if (mEncoderThread) {
+ mShutdownPromise = mShutdownPromise->Then(
+ mMainThread, __func__,
+ [encoderThread = mEncoderThread]() {
+ return encoderThread->BeginShutdown();
+ },
+ []() {
+ MOZ_ASSERT_UNREACHABLE("Unexpected reject");
+ return ShutdownPromise::CreateAndReject(false, __func__);
+ });
+ }
+
+ return mShutdownPromise;
+ }
+
+ private:
+ enum class RunningState {
+ Idling, // Session has been created
+ Starting, // MediaEncoder started, waiting for data
+ Running, // MediaEncoder has received data
+ Stopping, // Stop() has been called
+ Stopped, // Session has stopped without any error
+ };
+
+ // Our associated MediaRecorder.
+ const RefPtr<MediaRecorder> mRecorder;
+
+ // Stream currently recorded.
+ RefPtr<DOMMediaStream> mMediaStream;
+
+ // Tracks currently recorded. This should be a subset of mMediaStream's track
+ // set.
+ nsTArray<RefPtr<MediaStreamTrack>> mMediaStreamTracks;
+
+ // Main thread used for MozPromise operations.
+ const RefPtr<nsISerialEventTarget> mMainThread;
+ // Runnable thread for reading data from MediaEncoder.
+ RefPtr<TaskQueue> mEncoderThread;
+ // MediaEncoder pipeline.
+ RefPtr<MediaEncoder> mEncoder;
+ // Listener connected to mMediaEncoder::StartedEvent().
+ MediaEventListener mStartedListener;
+ // Listener connected to mMediaEncoder::DataAvailableEvent().
+ MediaEventListener mDataAvailableListener;
+ // Listener connected to mMediaEncoder::ErrorEvent().
+ MediaEventListener mErrorListener;
+ // Listener connected to mMediaEncoder::ShutdownEvent().
+ MediaEventListener mShutdownListener;
+ // Set in Shutdown() and resolved when shutdown is complete.
+ RefPtr<ShutdownPromise> mShutdownPromise;
+ // Session mimeType
+ const nsString mMimeType;
+ // The video bitrate the recorder was configured with.
+ const uint32_t mVideoBitsPerSecond;
+ // The audio bitrate the recorder was configured with.
+ const uint32_t mAudioBitsPerSecond;
+ // The time this session started, for telemetry.
+ const TimeStamp mStartTime;
+ // The session's current main thread state. The error type gets set when
+ // ending a recording with an error. An NS_OK error is invalid.
+ // Main thread only.
+ Result<RunningState, nsresult> mRunningState;
+ // Shutdown blocker unique for this Session. Main thread only.
+ RefPtr<ShutdownBlocker> mShutdownBlocker;
+};
+
+MediaRecorder::~MediaRecorder() {
+ LOG(LogLevel::Debug, ("~MediaRecorder (%p)", this));
+ UnRegisterActivityObserver();
+}
+
+MediaRecorder::MediaRecorder(nsPIDOMWindowInner* aOwnerWindow)
+ : DOMEventTargetHelper(aOwnerWindow) {
+ MOZ_ASSERT(aOwnerWindow);
+ RegisterActivityObserver();
+}
+
+void MediaRecorder::RegisterActivityObserver() {
+ if (nsPIDOMWindowInner* window = GetOwner()) {
+ mDocument = window->GetExtantDoc();
+ if (mDocument) {
+ mDocument->RegisterActivityObserver(
+ NS_ISUPPORTS_CAST(nsIDocumentActivity*, this));
+ }
+ }
+}
+
+void MediaRecorder::UnRegisterActivityObserver() {
+ if (mDocument) {
+ mDocument->UnregisterActivityObserver(
+ NS_ISUPPORTS_CAST(nsIDocumentActivity*, this));
+ }
+}
+
+void MediaRecorder::GetMimeType(nsString& aMimeType) { aMimeType = mMimeType; }
+
+void MediaRecorder::Start(const Optional<uint32_t>& aTimeslice,
+ ErrorResult& aResult) {
+ LOG(LogLevel::Debug, ("MediaRecorder.Start %p", this));
+
+ InitializeDomExceptions();
+
+ // When a MediaRecorder object’s start() method is invoked, the UA MUST run
+ // the following steps:
+
+ // 1. Let recorder be the MediaRecorder object on which the method was
+ // invoked.
+
+ // 2. Let timeslice be the method’s first argument, if provided, or undefined.
+ TimeDuration timeslice =
+ aTimeslice.WasPassed()
+ ? TimeDuration::FromMilliseconds(aTimeslice.Value())
+ : TimeDuration::Forever();
+
+ // 3. Let stream be the value of recorder’s stream attribute.
+
+ // 4. Let tracks be the set of live tracks in stream’s track set.
+ nsTArray<RefPtr<MediaStreamTrack>> tracks;
+ if (mStream) {
+ mStream->GetTracks(tracks);
+ }
+ tracks.RemoveLastElements(
+ tracks.end() - std::remove_if(tracks.begin(), tracks.end(),
+ [](const auto& t) { return t->Ended(); }));
+
+ // 5. If the value of recorder’s state attribute is not inactive, throw an
+ // InvalidStateError DOMException and abort these steps.
+ if (mState != RecordingState::Inactive) {
+ aResult.ThrowInvalidStateError(
+ "The MediaRecorder has already been started");
+ return;
+ }
+
+ // 6. If the isolation properties of stream disallow access from recorder,
+ // throw a SecurityError DOMException and abort these steps.
+ if (mStream) {
+ RefPtr<nsIPrincipal> streamPrincipal = mStream->GetPrincipal();
+ if (!streamPrincipal) {
+ // This is more or less part of the step 7, see below.
+ aResult.ThrowNotSupportedError("The MediaStream is inactive");
+ return;
+ }
+
+ if (!PrincipalSubsumes(this, streamPrincipal)) {
+ aResult.ThrowSecurityError(
+ "The MediaStream's isolation properties disallow access from "
+ "MediaRecorder");
+ return;
+ }
+ }
+ if (mAudioNode && !AudioNodePrincipalSubsumes(this, mAudioNode)) {
+ LOG(LogLevel::Warning,
+ ("MediaRecorder %p Start AudioNode principal check failed", this));
+ aResult.ThrowSecurityError(
+ "The AudioNode's isolation properties disallow access from "
+ "MediaRecorder");
+ return;
+ }
+
+ // 7. If stream is inactive, throw a NotSupportedError DOMException and abort
+ // these steps.
+ if (mStream && !mStream->Active()) {
+ aResult.ThrowNotSupportedError("The MediaStream is inactive");
+ return;
+ }
+
+ // 8. If the [[ConstrainedMimeType]] slot specifies a media type, container,
+ // or codec, then run the following sub steps:
+ // 1. Constrain the configuration of recorder to the media type, container,
+ // and codec specified in the [[ConstrainedMimeType]] slot.
+ // 2. For each track in tracks, if the User Agent cannot record the track
+ // using the current configuration, then throw a NotSupportedError
+ // DOMException and abort all steps.
+ Maybe<MediaContainerType> mime;
+ if (mConstrainedMimeType.Length() > 0) {
+ mime = MakeMediaContainerType(mConstrainedMimeType);
+ MOZ_DIAGNOSTIC_ASSERT(
+ mime,
+ "Invalid media MIME type should have been caught by IsTypeSupported");
+ }
+ for (const auto& track : tracks) {
+ TypeSupport support = CanRecordWith(track, mime, mConstrainedMimeType);
+ if (support != TypeSupport::Supported) {
+ nsString id;
+ track->GetId(id);
+ aResult.ThrowNotSupportedError(nsPrintfCString(
+ "%s track cannot be recorded: %s",
+ track->AsAudioStreamTrack() ? "An audio" : "A video",
+ TypeSupportToCString(support, mConstrainedMimeType).get()));
+ return;
+ }
+ }
+ if (mAudioNode) {
+ TypeSupport support = CanRecordAudioTrackWith(mime, mConstrainedMimeType);
+ if (support != TypeSupport::Supported) {
+ aResult.ThrowNotSupportedError(nsPrintfCString(
+ "An AudioNode cannot be recorded: %s",
+ TypeSupportToCString(support, mConstrainedMimeType).get()));
+ return;
+ }
+ }
+
+ // 9. If recorder’s [[ConstrainedBitsPerSecond]] slot is not undefined, set
+ // recorder’s videoBitsPerSecond and audioBitsPerSecond attributes to
+ // values the User Agent deems reasonable for the respective media types,
+ // for recording all tracks in tracks, such that the sum of
+ // videoBitsPerSecond and audioBitsPerSecond is close to the value of
+ // recorder’s
+ // [[ConstrainedBitsPerSecond]] slot.
+ uint8_t numVideoTracks = 0;
+ uint8_t numAudioTracks = 0;
+ for (const auto& t : tracks) {
+ if (t->AsVideoStreamTrack() && numVideoTracks < UINT8_MAX) {
+ ++numVideoTracks;
+ } else if (t->AsAudioStreamTrack() && numAudioTracks < UINT8_MAX) {
+ ++numAudioTracks;
+ }
+ }
+ if (mAudioNode) {
+ MOZ_DIAGNOSTIC_ASSERT(!mStream);
+ ++numAudioTracks;
+ }
+ if (mConstrainedBitsPerSecond) {
+ SelectBitrates(*mConstrainedBitsPerSecond, numVideoTracks,
+ &mVideoBitsPerSecond, numAudioTracks, &mAudioBitsPerSecond);
+ }
+
+ // 10. Let videoBitrate be the value of recorder’s videoBitsPerSecond
+ // attribute, and constrain the configuration of recorder to target an
+ // aggregate bitrate of videoBitrate bits per second for all video tracks
+ // recorder will be recording. videoBitrate is a hint for the encoder and
+ // the value might be surpassed, not achieved, or only be achieved over a
+ // long period of time.
+ const uint32_t videoBitrate = mVideoBitsPerSecond;
+
+ // 11. Let audioBitrate be the value of recorder’s audioBitsPerSecond
+ // attribute, and constrain the configuration of recorder to target an
+ // aggregate bitrate of audioBitrate bits per second for all audio tracks
+ // recorder will be recording. audioBitrate is a hint for the encoder and
+ // the value might be surpassed, not achieved, or only be achieved over a
+ // long period of time.
+ const uint32_t audioBitrate = mAudioBitsPerSecond;
+
+ // 12. Constrain the configuration of recorder to encode using the BitrateMode
+ // specified by the value of recorder’s audioBitrateMode attribute for all
+ // audio tracks recorder will be recording.
+ // -- NOT IMPLEMENTED
+
+ // 13. For each track in tracks, if the User Agent cannot record the track
+ // using the current configuration, then throw a NotSupportedError
+ // DOMException and abort these steps.
+ if (numVideoTracks > 1) {
+ aResult.ThrowNotSupportedError(
+ "MediaRecorder does not support recording more than one video track"_ns);
+ return;
+ }
+ if (numAudioTracks > 1) {
+ aResult.ThrowNotSupportedError(
+ "MediaRecorder does not support recording more than one audio track"_ns);
+ return;
+ }
+
+ // 14. Set recorder’s state to recording
+ mState = RecordingState::Recording;
+
+ MediaRecorderReporter::AddMediaRecorder(this);
+ // Start a session.
+ mSessions.AppendElement();
+ mSessions.LastElement() =
+ new Session(this, std::move(tracks), videoBitrate, audioBitrate);
+ mSessions.LastElement()->Start(timeslice);
+}
+
+void MediaRecorder::Stop(ErrorResult& aResult) {
+ LOG(LogLevel::Debug, ("MediaRecorder.Stop %p", this));
+ MediaRecorderReporter::RemoveMediaRecorder(this);
+
+ // When a MediaRecorder object’s stop() method is invoked, the UA MUST run the
+ // following steps:
+
+ // 1. Let recorder be the MediaRecorder object on which the method was
+ // invoked.
+
+ // 2. If recorder’s state attribute is inactive, abort these steps.
+ if (mState == RecordingState::Inactive) {
+ return;
+ }
+
+ // 3. Inactivate the recorder with recorder.
+ Inactivate();
+
+ // 4. Queue a task, using the DOM manipulation task source, that runs the
+ // following steps:
+ // 1. Stop gathering data.
+ // 2. Let blob be the Blob of collected data so far, then fire a blob event
+ // named dataavailable at recorder with blob.
+ // 3. Fire an event named stop at recorder.
+ MOZ_ASSERT(mSessions.Length() > 0);
+ mSessions.LastElement()->Stop();
+
+ // 5. return undefined.
+}
+
+void MediaRecorder::Pause(ErrorResult& aResult) {
+ LOG(LogLevel::Debug, ("MediaRecorder.Pause %p", this));
+
+ // When a MediaRecorder object’s pause() method is invoked, the UA MUST run
+ // the following steps:
+
+ // 1. If state is inactive, throw an InvalidStateError DOMException and abort
+ // these steps.
+ if (mState == RecordingState::Inactive) {
+ aResult.ThrowInvalidStateError("The MediaRecorder is inactive");
+ return;
+ }
+
+ // 2. If state is paused, abort these steps.
+ if (mState == RecordingState::Paused) {
+ return;
+ }
+
+ // 3. Set state to paused, and queue a task, using the DOM manipulation task
+ // source, that runs the following steps:
+ mState = RecordingState::Paused;
+
+ // XXX - We pause synchronously pending spec issue
+ // https://github.com/w3c/mediacapture-record/issues/131
+ // 1. Stop gathering data into blob (but keep it available so that
+ // recording can be resumed in the future).
+ MOZ_ASSERT(!mSessions.IsEmpty());
+ mSessions.LastElement()->Pause();
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "MediaRecorder::Pause", [recorder = RefPtr<MediaRecorder>(this)] {
+ // 2. Let target be the MediaRecorder context object. Fire an event
+ // named pause at target.
+ recorder->DispatchSimpleEvent(u"pause"_ns);
+ }));
+
+ // 4. return undefined.
+}
+
+void MediaRecorder::Resume(ErrorResult& aResult) {
+ LOG(LogLevel::Debug, ("MediaRecorder.Resume %p", this));
+
+ // When a MediaRecorder object’s resume() method is invoked, the UA MUST run
+ // the following steps:
+
+ // 1. If state is inactive, throw an InvalidStateError DOMException and abort
+ // these steps.
+ if (mState == RecordingState::Inactive) {
+ aResult.ThrowInvalidStateError("The MediaRecorder is inactive");
+ return;
+ }
+
+ // 2. If state is recording, abort these steps.
+ if (mState == RecordingState::Recording) {
+ return;
+ }
+
+ // 3. Set state to recording, and queue a task, using the DOM manipulation
+ // task source, that runs the following steps:
+ mState = RecordingState::Recording;
+
+ // XXX - We resume synchronously pending spec issue
+ // https://github.com/w3c/mediacapture-record/issues/131
+ // 1. Resume (or continue) gathering data into the current blob.
+ MOZ_ASSERT(!mSessions.IsEmpty());
+ mSessions.LastElement()->Resume();
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "MediaRecorder::Resume", [recorder = RefPtr<MediaRecorder>(this)] {
+ // 2. Let target be the MediaRecorder context object. Fire an event
+ // named resume at target.
+ recorder->DispatchSimpleEvent(u"resume"_ns);
+ }));
+
+ // 4. return undefined.
+}
+
+void MediaRecorder::RequestData(ErrorResult& aResult) {
+ LOG(LogLevel::Debug, ("MediaRecorder.RequestData %p", this));
+
+ // When a MediaRecorder object’s requestData() method is invoked, the UA MUST
+ // run the following steps:
+
+ // 1. If state is inactive throw an InvalidStateError DOMException and
+ // terminate these steps. Otherwise the UA MUST queue a task, using the DOM
+ // manipulation task source, that runs the following steps:
+ // 1. Let blob be the Blob of collected data so far and let target be the
+ // MediaRecorder context object, then fire a blob event named
+ // dataavailable at target with blob. (Note that blob will be empty if no
+ // data has been gathered yet.)
+ // 2. Create a new Blob and gather subsequent data into it.
+ if (mState == RecordingState::Inactive) {
+ aResult.ThrowInvalidStateError("The MediaRecorder is inactive");
+ return;
+ }
+ MOZ_ASSERT(mSessions.Length() > 0);
+ mSessions.LastElement()->RequestData();
+
+ // 2. return undefined.
+}
+
+JSObject* MediaRecorder::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return MediaRecorder_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+/* static */
+already_AddRefed<MediaRecorder> MediaRecorder::Constructor(
+ const GlobalObject& aGlobal, DOMMediaStream& aStream,
+ const MediaRecorderOptions& aOptions, ErrorResult& aRv) {
+ nsCOMPtr<nsPIDOMWindowInner> ownerWindow =
+ do_QueryInterface(aGlobal.GetAsSupports());
+ if (!ownerWindow) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ // When the MediaRecorder() constructor is invoked, the User Agent MUST run
+ // the following steps:
+
+ // 1. Let stream be the constructor’s first argument.
+
+ // 2. Let options be the constructor’s second argument.
+
+ // 3. If invoking is type supported with options’ mimeType member as its
+ // argument returns false, throw a NotSupportedError DOMException and abort
+ // these steps.
+ TypeSupport support = IsTypeSupportedImpl(aOptions.mMimeType);
+ if (support != TypeSupport::Supported) {
+ // This catches also the empty string mimeType when support for any encoders
+ // has been disabled.
+ aRv.ThrowNotSupportedError(
+ TypeSupportToCString(support, aOptions.mMimeType));
+ return nullptr;
+ }
+
+ // 4. Let recorder be a newly constructed MediaRecorder object.
+ RefPtr<MediaRecorder> recorder = new MediaRecorder(ownerWindow);
+
+ // 5. Let recorder have a [[ConstrainedMimeType]] internal slot, initialized
+ // to the value of options' mimeType member.
+ recorder->mConstrainedMimeType = aOptions.mMimeType;
+
+ // 6. Let recorder have a [[ConstrainedBitsPerSecond]] internal slot,
+ // initialized to the value of options’ bitsPerSecond member, if it is
+ // present, otherwise undefined.
+ recorder->mConstrainedBitsPerSecond =
+ aOptions.mBitsPerSecond.WasPassed()
+ ? Some(aOptions.mBitsPerSecond.Value())
+ : Nothing();
+
+ // 7. Initialize recorder’s stream attribute to stream.
+ recorder->mStream = &aStream;
+
+ // 8. Initialize recorder’s mimeType attribute to the value of recorder’s
+ // [[ConstrainedMimeType]] slot.
+ recorder->mMimeType = recorder->mConstrainedMimeType;
+
+ // 9. Initialize recorder’s state attribute to inactive.
+ recorder->mState = RecordingState::Inactive;
+
+ // 10. Initialize recorder’s videoBitsPerSecond attribute to the value of
+ // options’ videoBitsPerSecond member, if it is present. Otherwise, choose
+ // a target value the User Agent deems reasonable for video.
+ recorder->mVideoBitsPerSecond = aOptions.mVideoBitsPerSecond.WasPassed()
+ ? aOptions.mVideoBitsPerSecond.Value()
+ : DEFAULT_VIDEO_BITRATE_BPS;
+
+ // 11. Initialize recorder’s audioBitsPerSecond attribute to the value of
+ // options’ audioBitsPerSecond member, if it is present. Otherwise, choose
+ // a target value the User Agent deems reasonable for audio.
+ recorder->mAudioBitsPerSecond = aOptions.mAudioBitsPerSecond.WasPassed()
+ ? aOptions.mAudioBitsPerSecond.Value()
+ : DEFAULT_AUDIO_BITRATE_BPS;
+
+ // 12. If recorder’s [[ConstrainedBitsPerSecond]] slot is not undefined, set
+ // recorder’s videoBitsPerSecond and audioBitsPerSecond attributes to
+ // values the User Agent deems reasonable for the respective media types,
+ // such that the sum of videoBitsPerSecond and audioBitsPerSecond is close
+ // to the value of recorder’s [[ConstrainedBitsPerSecond]] slot.
+ if (recorder->mConstrainedBitsPerSecond) {
+ SelectBitrates(*recorder->mConstrainedBitsPerSecond, 1,
+ &recorder->mVideoBitsPerSecond, 1,
+ &recorder->mAudioBitsPerSecond);
+ }
+
+ // 13. Return recorder.
+ return recorder.forget();
+}
+
+/* static */
+already_AddRefed<MediaRecorder> MediaRecorder::Constructor(
+ const GlobalObject& aGlobal, AudioNode& aAudioNode,
+ uint32_t aAudioNodeOutput, const MediaRecorderOptions& aOptions,
+ ErrorResult& aRv) {
+ // Allow recording from audio node only when pref is on.
+ if (!Preferences::GetBool("media.recorder.audio_node.enabled", false)) {
+ // Pretending that this constructor is not defined.
+ aRv.ThrowTypeError<MSG_DOES_NOT_IMPLEMENT_INTERFACE>("Argument 1",
+ "MediaStream");
+ return nullptr;
+ }
+
+ nsCOMPtr<nsPIDOMWindowInner> ownerWindow =
+ do_QueryInterface(aGlobal.GetAsSupports());
+ if (!ownerWindow) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ // aAudioNodeOutput doesn't matter to destination node because it has no
+ // output.
+ if (aAudioNode.NumberOfOutputs() > 0 &&
+ aAudioNodeOutput >= aAudioNode.NumberOfOutputs()) {
+ aRv.ThrowIndexSizeError("Invalid AudioNode output index");
+ return nullptr;
+ }
+
+ // When the MediaRecorder() constructor is invoked, the User Agent MUST run
+ // the following steps:
+
+ // 1. Let stream be the constructor’s first argument. (we'll let audioNode be
+ // the first arg, and audioNodeOutput the second)
+
+ // 2. Let options be the constructor’s second argument. (we'll let options be
+ // the third arg)
+
+ // 3. If invoking is type supported with options’ mimeType member as its
+ // argument returns false, throw a NotSupportedError DOMException and abort
+ // these steps.
+ TypeSupport support = IsTypeSupportedImpl(aOptions.mMimeType);
+ if (support != TypeSupport::Supported) {
+ // This catches also the empty string mimeType when support for any encoders
+ // has been disabled.
+ aRv.ThrowNotSupportedError(
+ TypeSupportToCString(support, aOptions.mMimeType));
+ return nullptr;
+ }
+
+ // 4. Let recorder be a newly constructed MediaRecorder object.
+ RefPtr<MediaRecorder> recorder = new MediaRecorder(ownerWindow);
+
+ // 5. Let recorder have a [[ConstrainedMimeType]] internal slot, initialized
+ // to the value of options' mimeType member.
+ recorder->mConstrainedMimeType = aOptions.mMimeType;
+
+ // 6. Let recorder have a [[ConstrainedBitsPerSecond]] internal slot,
+ // initialized to the value of options’ bitsPerSecond member, if it is
+ // present, otherwise undefined.
+ recorder->mConstrainedBitsPerSecond =
+ aOptions.mBitsPerSecond.WasPassed()
+ ? Some(aOptions.mBitsPerSecond.Value())
+ : Nothing();
+
+ // 7. Initialize recorder’s stream attribute to stream. (make that the
+ // audioNode and audioNodeOutput equivalents)
+ recorder->mAudioNode = &aAudioNode;
+ recorder->mAudioNodeOutput = aAudioNodeOutput;
+
+ // 8. Initialize recorder’s mimeType attribute to the value of recorder’s
+ // [[ConstrainedMimeType]] slot.
+ recorder->mMimeType = recorder->mConstrainedMimeType;
+
+ // 9. Initialize recorder’s state attribute to inactive.
+ recorder->mState = RecordingState::Inactive;
+
+ // 10. Initialize recorder’s videoBitsPerSecond attribute to the value of
+ // options’ videoBitsPerSecond member, if it is present. Otherwise, choose
+ // a target value the User Agent deems reasonable for video.
+ recorder->mVideoBitsPerSecond = aOptions.mVideoBitsPerSecond.WasPassed()
+ ? aOptions.mVideoBitsPerSecond.Value()
+ : DEFAULT_VIDEO_BITRATE_BPS;
+
+ // 11. Initialize recorder’s audioBitsPerSecond attribute to the value of
+ // options’ audioBitsPerSecond member, if it is present. Otherwise, choose
+ // a target value the User Agent deems reasonable for audio.
+ recorder->mAudioBitsPerSecond = aOptions.mAudioBitsPerSecond.WasPassed()
+ ? aOptions.mAudioBitsPerSecond.Value()
+ : DEFAULT_AUDIO_BITRATE_BPS;
+
+ // 12. If recorder’s [[ConstrainedBitsPerSecond]] slot is not undefined, set
+ // recorder’s videoBitsPerSecond and audioBitsPerSecond attributes to
+ // values the User Agent deems reasonable for the respective media types,
+ // such that the sum of videoBitsPerSecond and audioBitsPerSecond is close
+ // to the value of recorder’s [[ConstrainedBitsPerSecond]] slot.
+ if (recorder->mConstrainedBitsPerSecond) {
+ SelectBitrates(*recorder->mConstrainedBitsPerSecond, 1,
+ &recorder->mVideoBitsPerSecond, 1,
+ &recorder->mAudioBitsPerSecond);
+ }
+
+ // 13. Return recorder.
+ return recorder.forget();
+}
+
+/* static */
+bool MediaRecorder::IsTypeSupported(GlobalObject& aGlobal,
+ const nsAString& aMIMEType) {
+ return MediaRecorder::IsTypeSupported(aMIMEType);
+}
+
+/* static */
+bool MediaRecorder::IsTypeSupported(const nsAString& aMIMEType) {
+ return IsTypeSupportedImpl(aMIMEType) == TypeSupport::Supported;
+}
+
+nsresult MediaRecorder::CreateAndDispatchBlobEvent(BlobImpl* aBlobImpl) {
+ MOZ_ASSERT(NS_IsMainThread(), "Not running on main thread");
+
+ if (!GetOwnerGlobal()) {
+ // This MediaRecorder has been disconnected in the meantime.
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<Blob> blob = Blob::Create(GetOwnerGlobal(), aBlobImpl);
+ if (NS_WARN_IF(!blob)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ BlobEventInit init;
+ init.mBubbles = false;
+ init.mCancelable = false;
+ init.mData = blob;
+
+ RefPtr<BlobEvent> event =
+ BlobEvent::Constructor(this, u"dataavailable"_ns, init);
+ event->SetTrusted(true);
+ ErrorResult rv;
+ DispatchEvent(*event, rv);
+ return rv.StealNSResult();
+}
+
+void MediaRecorder::DispatchSimpleEvent(const nsAString& aStr) {
+ MOZ_ASSERT(NS_IsMainThread(), "Not running on main thread");
+ nsresult rv = CheckCurrentGlobalCorrectness();
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ rv = DOMEventTargetHelper::DispatchTrustedEvent(aStr);
+ if (NS_FAILED(rv)) {
+ LOG(LogLevel::Error,
+ ("MediaRecorder.DispatchSimpleEvent: DispatchTrustedEvent failed %p",
+ this));
+ NS_ERROR("Failed to dispatch the event!!!");
+ }
+}
+
+void MediaRecorder::NotifyError(nsresult aRv) {
+ MOZ_ASSERT(NS_IsMainThread(), "Not running on main thread");
+ nsresult rv = CheckCurrentGlobalCorrectness();
+ if (NS_FAILED(rv)) {
+ return;
+ }
+ MediaRecorderErrorEventInit init;
+ init.mBubbles = false;
+ init.mCancelable = false;
+ // These DOMExceptions have been created earlier so they can contain stack
+ // traces. We attach the appropriate one here to be fired. We should have
+ // exceptions here, but defensively check.
+ switch (aRv) {
+ case NS_ERROR_DOM_SECURITY_ERR:
+ if (!mSecurityDomException) {
+ LOG(LogLevel::Debug, ("MediaRecorder.NotifyError: "
+ "mSecurityDomException was not initialized"));
+ mSecurityDomException = DOMException::Create(NS_ERROR_DOM_SECURITY_ERR);
+ }
+ init.mError = std::move(mSecurityDomException);
+ break;
+ default:
+ if (mOtherDomException && aRv == mOtherDomException->GetResult()) {
+ LOG(LogLevel::Debug, ("MediaRecorder.NotifyError: "
+ "mOtherDomException being fired for aRv: %X",
+ uint32_t(aRv)));
+ init.mError = std::move(mOtherDomException);
+ break;
+ }
+ if (!mUnknownDomException) {
+ LOG(LogLevel::Debug, ("MediaRecorder.NotifyError: "
+ "mUnknownDomException was not initialized"));
+ mUnknownDomException = DOMException::Create(NS_ERROR_DOM_UNKNOWN_ERR);
+ }
+ LOG(LogLevel::Debug, ("MediaRecorder.NotifyError: "
+ "mUnknownDomException being fired for aRv: %X",
+ uint32_t(aRv)));
+ init.mError = std::move(mUnknownDomException);
+ break;
+ }
+
+ RefPtr<MediaRecorderErrorEvent> event =
+ MediaRecorderErrorEvent::Constructor(this, u"error"_ns, init);
+ event->SetTrusted(true);
+
+ IgnoredErrorResult res;
+ DispatchEvent(*event, res);
+ if (res.Failed()) {
+ NS_ERROR("Failed to dispatch the error event!!!");
+ }
+}
+
+void MediaRecorder::RemoveSession(Session* aSession) {
+ LOG(LogLevel::Debug, ("MediaRecorder.RemoveSession (%p)", aSession));
+ mSessions.RemoveElement(aSession);
+}
+
+void MediaRecorder::NotifyOwnerDocumentActivityChanged() {
+ nsPIDOMWindowInner* window = GetOwner();
+ NS_ENSURE_TRUE_VOID(window);
+ Document* doc = window->GetExtantDoc();
+ NS_ENSURE_TRUE_VOID(doc);
+
+ LOG(LogLevel::Debug, ("MediaRecorder %p NotifyOwnerDocumentActivityChanged "
+ "IsActive=%d, "
+ "IsVisible=%d, ",
+ this, doc->IsActive(), doc->IsVisible()));
+ if (!doc->IsActive() || !doc->IsVisible()) {
+ // Stop the session.
+ ErrorResult result;
+ Stop(result);
+ result.SuppressException();
+ }
+}
+
+void MediaRecorder::Inactivate() {
+ LOG(LogLevel::Debug, ("MediaRecorder.Inactivate %p", this));
+ // The Inactivate the recorder algorithm given a recorder, is as follows:
+
+ // 1. Set recorder’s mimeType attribute to the value of the
+ // [[ConstrainedMimeType]] slot.
+ mMimeType = mConstrainedMimeType;
+
+ // 2. Set recorder’s state attribute to inactive.
+ mState = RecordingState::Inactive;
+
+ // 3. If recorder’s [[ConstrainedBitsPerSecond]] slot is not undefined, set
+ // recorder’s videoBitsPerSecond and audioBitsPerSecond attributes to
+ // values the User Agent deems reasonable for the respective media types,
+ // such that the sum of videoBitsPerSecond and audioBitsPerSecond is close
+ // to the value of recorder’s [[ConstrainedBitsPerSecond]] slot.
+ if (mConstrainedBitsPerSecond) {
+ SelectBitrates(*mConstrainedBitsPerSecond, 1, &mVideoBitsPerSecond, 1,
+ &mAudioBitsPerSecond);
+ }
+}
+
+void MediaRecorder::InitializeDomExceptions() {
+ mSecurityDomException = DOMException::Create(NS_ERROR_DOM_SECURITY_ERR);
+ mUnknownDomException = DOMException::Create(NS_ERROR_DOM_UNKNOWN_ERR);
+}
+
+RefPtr<MediaRecorder::SizeOfPromise> MediaRecorder::SizeOfExcludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // The return type of a chained MozPromise cannot be changed, so we create a
+ // holder for our desired return type and resolve that from All()->Then().
+ auto holder = MakeRefPtr<Refcountable<MozPromiseHolder<SizeOfPromise>>>();
+ RefPtr<SizeOfPromise> promise = holder->Ensure(__func__);
+
+ nsTArray<RefPtr<SizeOfPromise>> promises(mSessions.Length());
+ for (const RefPtr<Session>& session : mSessions) {
+ promises.AppendElement(session->SizeOfExcludingThis(aMallocSizeOf));
+ }
+
+ SizeOfPromise::All(GetCurrentSerialEventTarget(), promises)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [holder](const nsTArray<size_t>& sizes) {
+ size_t total = 0;
+ for (const size_t& size : sizes) {
+ total += size;
+ }
+ holder->Resolve(total, __func__);
+ },
+ []() { MOZ_CRASH("Unexpected reject"); });
+
+ return promise;
+}
+
+StaticRefPtr<MediaRecorderReporter> MediaRecorderReporter::sUniqueInstance;
+
+} // namespace mozilla::dom
+
+#undef LOG
diff --git a/dom/media/MediaRecorder.h b/dom/media/MediaRecorder.h
new file mode 100644
index 0000000000..77e8ca069b
--- /dev/null
+++ b/dom/media/MediaRecorder.h
@@ -0,0 +1,187 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MediaRecorder_h
+#define MediaRecorder_h
+
+#include "mozilla/dom/MediaRecorderBinding.h"
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/MozPromise.h"
+#include "nsIDocumentActivity.h"
+
+// Max size for allowing queue encoded data in memory
+#define MAX_ALLOW_MEMORY_BUFFER 1024000
+namespace mozilla {
+
+class AudioNodeTrack;
+class DOMMediaStream;
+class ErrorResult;
+struct MediaRecorderOptions;
+class GlobalObject;
+
+namespace dom {
+
+class AudioNode;
+class BlobImpl;
+class Document;
+class DOMException;
+
+/**
+ * Implementation of
+ * https://w3c.github.io/mediacapture-record/MediaRecorder.html
+ *
+ * The MediaRecorder accepts a MediaStream as input passed from an application.
+ * When the MediaRecorder starts, a MediaEncoder will be created and accepts the
+ * MediaStreamTracks in the MediaStream as input source. For each track it
+ * creates a TrackEncoder.
+ *
+ * The MediaEncoder automatically encodes and muxes data from the tracks by the
+ * given MIME type, then it stores this data into a MutableBlobStorage object.
+ * When a timeslice is set and the MediaEncoder has stored enough data to fill
+ * the timeslice, it extracts a Blob from the storage and passes it to
+ * MediaRecorder. On RequestData() or Stop(), the MediaEncoder extracts the blob
+ * from the storage and returns it to MediaRecorder through a MozPromise.
+ *
+ * Thread model: When the recorder starts, it creates a worker thread (called
+ * the encoder thread) that does all the heavy lifting - encoding, time keeping,
+ * muxing.
+ */
+
+class MediaRecorder final : public DOMEventTargetHelper,
+ public nsIDocumentActivity {
+ public:
+ class Session;
+
+ explicit MediaRecorder(nsPIDOMWindowInner* aOwnerWindow);
+
+ static nsTArray<RefPtr<Session>> GetSessions();
+
+ // nsWrapperCache
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MediaRecorder, DOMEventTargetHelper)
+
+ // WebIDL
+ // Start recording.
+ void Start(const Optional<uint32_t>& timeSlice, ErrorResult& aResult);
+ // Stop recording.
+ void Stop(ErrorResult& aResult);
+ // Pause a recording.
+ void Pause(ErrorResult& aResult);
+ // Resume a paused recording.
+ void Resume(ErrorResult& aResult);
+ // Extracts buffered data and fires the dataavailable event.
+ void RequestData(ErrorResult& aResult);
+ // Return the The DOMMediaStream passed from UA.
+ DOMMediaStream* Stream() const { return mStream; }
+ // Return the current encoding MIME type selected by the MediaEncoder.
+ void GetMimeType(nsString& aMimeType);
+ // The current state of the MediaRecorder object.
+ RecordingState State() const { return mState; }
+
+ static bool IsTypeSupported(GlobalObject& aGlobal,
+ const nsAString& aMIMEType);
+ static bool IsTypeSupported(const nsAString& aMIMEType);
+
+ // Construct a recorder with a DOM media stream object as its source.
+ static already_AddRefed<MediaRecorder> Constructor(
+ const GlobalObject& aGlobal, DOMMediaStream& aStream,
+ const MediaRecorderOptions& aOptions, ErrorResult& aRv);
+ // Construct a recorder with a Web Audio destination node as its source.
+ static already_AddRefed<MediaRecorder> Constructor(
+ const GlobalObject& aGlobal, AudioNode& aAudioNode,
+ uint32_t aAudioNodeOutput, const MediaRecorderOptions& aOptions,
+ ErrorResult& aRv);
+
+ /*
+ * Measure the size of the buffer, and heap memory in bytes occupied by
+ * mAudioEncoder and mVideoEncoder.
+ */
+ typedef MozPromise<size_t, size_t, true> SizeOfPromise;
+ RefPtr<SizeOfPromise> SizeOfExcludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf);
+ // EventHandler
+ IMPL_EVENT_HANDLER(start)
+ IMPL_EVENT_HANDLER(stop)
+ IMPL_EVENT_HANDLER(dataavailable)
+ IMPL_EVENT_HANDLER(pause)
+ IMPL_EVENT_HANDLER(resume)
+ IMPL_EVENT_HANDLER(error)
+
+ NS_DECL_NSIDOCUMENTACTIVITY
+
+ uint32_t AudioBitsPerSecond() const { return mAudioBitsPerSecond; }
+ uint32_t VideoBitsPerSecond() const { return mVideoBitsPerSecond; }
+
+ protected:
+ virtual ~MediaRecorder();
+
+ MediaRecorder& operator=(const MediaRecorder& x) = delete;
+ // Create dataavailable event with Blob data and it runs in main thread
+ nsresult CreateAndDispatchBlobEvent(BlobImpl* aBlobImpl);
+ // Creating a simple event to notify UA simple event.
+ void DispatchSimpleEvent(const nsAString& aStr);
+ // Creating a error event with message.
+ void NotifyError(nsresult aRv);
+
+ MediaRecorder(const MediaRecorder& x) = delete; // prevent bad usage
+ // Remove session pointer.
+ void RemoveSession(Session* aSession);
+ // Create DOMExceptions capturing the JS stack for async errors. These are
+ // created ahead of time rather than on demand when firing an error as the JS
+ // stack of the operation that started the async behavior will not be
+ // available at the time the error event is fired. Note, depending on when
+ // this is called there may not be a JS stack to capture.
+ void InitializeDomExceptions();
+ // Runs the "Inactivate the recorder" algorithm.
+ void Inactivate();
+ // Stop the recorder and its internal session. This should be used by
+ // sessions that are in the process of being destroyed.
+ void StopForSessionDestruction();
+ // DOM wrapper for source media stream. Will be null when input is audio node.
+ RefPtr<DOMMediaStream> mStream;
+ // Source audio node. Will be null when input is a media stream.
+ RefPtr<AudioNode> mAudioNode;
+ // Source audio node's output index. Will be zero when input is a media
+ // stream.
+ uint32_t mAudioNodeOutput = 0;
+
+ // The current state of the MediaRecorder object.
+ RecordingState mState = RecordingState::Inactive;
+ // Hold the sessions reference and clean it when the DestroyRunnable for a
+ // session is running.
+ nsTArray<RefPtr<Session>> mSessions;
+
+ RefPtr<Document> mDocument;
+
+ nsString mMimeType;
+ nsString mConstrainedMimeType;
+
+ uint32_t mAudioBitsPerSecond = 0;
+ uint32_t mVideoBitsPerSecond = 0;
+ Maybe<uint32_t> mConstrainedBitsPerSecond;
+
+ // DOMExceptions that are created early and possibly thrown in NotifyError.
+ // Creating them early allows us to capture the JS stack for which cannot be
+ // done at the time the error event is fired.
+ RefPtr<DOMException> mOtherDomException;
+ RefPtr<DOMException> mSecurityDomException;
+ RefPtr<DOMException> mUnknownDomException;
+
+ private:
+ // Register MediaRecorder into Document to listen the activity changes.
+ void RegisterActivityObserver();
+ void UnRegisterActivityObserver();
+
+ bool CheckPermission(const nsString& aType);
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/MediaResource.cpp b/dom/media/MediaResource.cpp
new file mode 100644
index 0000000000..feb0b130bf
--- /dev/null
+++ b/dom/media/MediaResource.cpp
@@ -0,0 +1,425 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaResource.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/Logging.h"
+#include "mozilla/MathAlgorithms.h"
+#include "mozilla/ErrorNames.h"
+#include "mozilla/SchedulerGroup.h"
+
+using mozilla::media::TimeUnit;
+
+#undef ILOG
+
+mozilla::LazyLogModule gMediaResourceIndexLog("MediaResourceIndex");
+// Debug logging macro with object pointer and class name.
+#define ILOG(msg, ...) \
+ DDMOZ_LOG(gMediaResourceIndexLog, mozilla::LogLevel::Debug, msg, \
+ ##__VA_ARGS__)
+
+namespace mozilla {
+
+static const uint32_t kMediaResourceIndexCacheSize = 8192;
+static_assert(IsPowerOfTwo(kMediaResourceIndexCacheSize),
+ "kMediaResourceIndexCacheSize cache size must be a power of 2");
+
+MediaResourceIndex::MediaResourceIndex(MediaResource* aResource)
+ : mResource(aResource),
+ mOffset(0),
+ mCacheBlockSize(
+ aResource->ShouldCacheReads() ? kMediaResourceIndexCacheSize : 0),
+ mCachedOffset(0),
+ mCachedBytes(0),
+ mCachedBlock(MakeUnique<char[]>(mCacheBlockSize)) {
+ DDLINKCHILD("resource", aResource);
+}
+
+nsresult MediaResourceIndex::Read(char* aBuffer, uint32_t aCount,
+ uint32_t* aBytes) {
+ NS_ASSERTION(!NS_IsMainThread(), "Don't call on main thread");
+
+ // We purposefuly don't check that we may attempt to read past
+ // mResource->GetLength() as the resource's length may change over time.
+
+ nsresult rv = ReadAt(mOffset, aBuffer, aCount, aBytes);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ mOffset += *aBytes;
+ if (mOffset < 0) {
+ // Very unlikely overflow; just return to position 0.
+ mOffset = 0;
+ }
+ return NS_OK;
+}
+
+static nsCString ResultName(nsresult aResult) {
+ nsCString name;
+ GetErrorName(aResult, name);
+ return name;
+}
+
+nsresult MediaResourceIndex::ReadAt(int64_t aOffset, char* aBuffer,
+ uint32_t aCount, uint32_t* aBytes) {
+ if (mCacheBlockSize == 0) {
+ return UncachedReadAt(aOffset, aBuffer, aCount, aBytes);
+ }
+
+ *aBytes = 0;
+
+ if (aCount == 0) {
+ return NS_OK;
+ }
+
+ const int64_t endOffset = aOffset + aCount;
+ if (aOffset < 0 || endOffset < aOffset) {
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ const int64_t lastBlockOffset = CacheOffsetContaining(endOffset - 1);
+
+ if (mCachedBytes != 0 && mCachedOffset + mCachedBytes >= aOffset &&
+ mCachedOffset < endOffset) {
+ // There is data in the cache that is not completely before aOffset and not
+ // completely after endOffset, so it could be usable (with potential
+ // top-up).
+ if (aOffset < mCachedOffset) {
+ // We need to read before the cached data.
+ const uint32_t toRead = uint32_t(mCachedOffset - aOffset);
+ MOZ_ASSERT(toRead > 0);
+ MOZ_ASSERT(toRead < aCount);
+ uint32_t read = 0;
+ nsresult rv = UncachedReadAt(aOffset, aBuffer, toRead, &read);
+ if (NS_FAILED(rv)) {
+ ILOG("ReadAt(%" PRIu32 "@%" PRId64
+ ") uncached read before cache -> %s, %" PRIu32,
+ aCount, aOffset, ResultName(rv).get(), *aBytes);
+ return rv;
+ }
+ *aBytes = read;
+ if (read < toRead) {
+ // Could not read everything we wanted, we're done.
+ ILOG("ReadAt(%" PRIu32 "@%" PRId64
+ ") uncached read before cache, incomplete -> OK, %" PRIu32,
+ aCount, aOffset, *aBytes);
+ return NS_OK;
+ }
+ ILOG("ReadAt(%" PRIu32 "@%" PRId64
+ ") uncached read before cache: %" PRIu32 ", remaining: %" PRIu32
+ "@%" PRId64 "...",
+ aCount, aOffset, read, aCount - read, aOffset + read);
+ aOffset += read;
+ aBuffer += read;
+ aCount -= read;
+ // We should have reached the cache.
+ MOZ_ASSERT(aOffset == mCachedOffset);
+ }
+ MOZ_ASSERT(aOffset >= mCachedOffset);
+
+ // We've reached our cache.
+ const uint32_t toCopy =
+ std::min(aCount, uint32_t(mCachedOffset + mCachedBytes - aOffset));
+ // Note that we could in fact be just after the last byte of the cache, in
+ // which case we can't actually read from it! (But we will top-up next.)
+ if (toCopy != 0) {
+ memcpy(aBuffer, &mCachedBlock[IndexInCache(aOffset)], toCopy);
+ *aBytes += toCopy;
+ aCount -= toCopy;
+ if (aCount == 0) {
+ // All done!
+ ILOG("ReadAt(%" PRIu32 "@%" PRId64 ") copied everything (%" PRIu32
+ ") from cache(%" PRIu32 "@%" PRId64 ") :-D -> OK, %" PRIu32,
+ aCount, aOffset, toCopy, mCachedBytes, mCachedOffset, *aBytes);
+ return NS_OK;
+ }
+ aOffset += toCopy;
+ aBuffer += toCopy;
+ ILOG("ReadAt(%" PRIu32 "@%" PRId64 ") copied %" PRIu32
+ " from cache(%" PRIu32 "@%" PRId64 ") :-), remaining: %" PRIu32
+ "@%" PRId64 "...",
+ aCount + toCopy, aOffset - toCopy, toCopy, mCachedBytes,
+ mCachedOffset, aCount, aOffset);
+ }
+
+ if (aOffset - 1 >= lastBlockOffset) {
+ // We were already reading cached data from the last block, we need more
+ // from it -> try to top-up, read what we can, and we'll be done.
+ MOZ_ASSERT(aOffset == mCachedOffset + mCachedBytes);
+ MOZ_ASSERT(endOffset <= lastBlockOffset + mCacheBlockSize);
+ return CacheOrReadAt(aOffset, aBuffer, aCount, aBytes);
+ }
+
+ // We were not in the last block (but we may just have crossed the line now)
+ MOZ_ASSERT(aOffset <= lastBlockOffset);
+ // Continue below...
+ } else if (aOffset >= lastBlockOffset) {
+ // There was nothing we could get from the cache.
+ // But we're already in the last block -> Cache or read what we can.
+ // Make sure to invalidate the cache first.
+ mCachedBytes = 0;
+ return CacheOrReadAt(aOffset, aBuffer, aCount, aBytes);
+ }
+
+ // If we're here, either there was nothing usable in the cache, or we've just
+ // read what was in the cache but there's still more to read.
+
+ if (aOffset < lastBlockOffset) {
+ // We need to read before the last block.
+ // Start with an uncached read up to the last block.
+ const uint32_t toRead = uint32_t(lastBlockOffset - aOffset);
+ MOZ_ASSERT(toRead > 0);
+ MOZ_ASSERT(toRead < aCount);
+ uint32_t read = 0;
+ nsresult rv = UncachedReadAt(aOffset, aBuffer, toRead, &read);
+ if (NS_FAILED(rv)) {
+ ILOG("ReadAt(%" PRIu32 "@%" PRId64
+ ") uncached read before last block failed -> %s, %" PRIu32,
+ aCount, aOffset, ResultName(rv).get(), *aBytes);
+ return rv;
+ }
+ if (read == 0) {
+ ILOG("ReadAt(%" PRIu32 "@%" PRId64
+ ") uncached read 0 before last block -> OK, %" PRIu32,
+ aCount, aOffset, *aBytes);
+ return NS_OK;
+ }
+ *aBytes += read;
+ if (read < toRead) {
+ // Could not read everything we wanted, we're done.
+ ILOG("ReadAt(%" PRIu32 "@%" PRId64
+ ") uncached read before last block, incomplete -> OK, %" PRIu32,
+ aCount, aOffset, *aBytes);
+ return NS_OK;
+ }
+ ILOG("ReadAt(%" PRIu32 "@%" PRId64 ") read %" PRIu32
+ " before last block, remaining: %" PRIu32 "@%" PRId64 "...",
+ aCount, aOffset, read, aCount - read, aOffset + read);
+ aOffset += read;
+ aBuffer += read;
+ aCount -= read;
+ }
+
+ // We should just have reached the start of the last block.
+ MOZ_ASSERT(aOffset == lastBlockOffset);
+ MOZ_ASSERT(aCount <= mCacheBlockSize);
+ // Make sure to invalidate the cache first.
+ mCachedBytes = 0;
+ return CacheOrReadAt(aOffset, aBuffer, aCount, aBytes);
+}
+
+nsresult MediaResourceIndex::CacheOrReadAt(int64_t aOffset, char* aBuffer,
+ uint32_t aCount, uint32_t* aBytes) {
+ // We should be here because there is more data to read.
+ MOZ_ASSERT(aCount > 0);
+ // We should be in the last block, so we shouldn't try to read past it.
+ MOZ_ASSERT(IndexInCache(aOffset) + aCount <= mCacheBlockSize);
+
+ const int64_t length = GetLength();
+ // If length is unknown (-1), look at resource-cached data.
+ // If length is known and equal or greater than requested, also look at
+ // resource-cached data.
+ // Otherwise, if length is known but same, or less than(!?), requested, don't
+ // attempt to access resource-cached data, as we're not expecting it to ever
+ // be greater than the length.
+ if (length < 0 || length >= aOffset + aCount) {
+ // Is there cached data covering at least the requested range?
+ const int64_t cachedDataEnd = mResource->GetCachedDataEnd(aOffset);
+ if (cachedDataEnd >= aOffset + aCount) {
+ // Try to read as much resource-cached data as can fill our local cache.
+ // Assume we can read as much as is cached without blocking.
+ const uint32_t cacheIndex = IndexInCache(aOffset);
+ const uint32_t toRead = uint32_t(std::min(
+ cachedDataEnd - aOffset, int64_t(mCacheBlockSize - cacheIndex)));
+ MOZ_ASSERT(toRead >= aCount);
+ uint32_t read = 0;
+ // We would like `toRead` if possible, but ok with at least `aCount`.
+ nsresult rv = UncachedRangedReadAt(aOffset, &mCachedBlock[cacheIndex],
+ aCount, toRead - aCount, &read);
+ if (NS_SUCCEEDED(rv)) {
+ if (read == 0) {
+ ILOG("ReadAt(%" PRIu32 "@%" PRId64 ") - UncachedRangedReadAt(%" PRIu32
+ "..%" PRIu32 "@%" PRId64
+ ") to top-up succeeded but read nothing -> OK anyway",
+ aCount, aOffset, aCount, toRead, aOffset);
+ // Couldn't actually read anything, but didn't error out, so count
+ // that as success.
+ return NS_OK;
+ }
+ if (mCachedOffset + mCachedBytes == aOffset) {
+ // We were topping-up the cache, just update its size.
+ ILOG("ReadAt(%" PRIu32 "@%" PRId64 ") - UncachedRangedReadAt(%" PRIu32
+ "..%" PRIu32 "@%" PRId64 ") to top-up succeeded to read %" PRIu32
+ "...",
+ aCount, aOffset, aCount, toRead, aOffset, read);
+ mCachedBytes += read;
+ } else {
+ // We were filling the cache from scratch, save new cache information.
+ ILOG("ReadAt(%" PRIu32 "@%" PRId64 ") - UncachedRangedReadAt(%" PRIu32
+ "..%" PRIu32 "@%" PRId64
+ ") to fill cache succeeded to read %" PRIu32 "...",
+ aCount, aOffset, aCount, toRead, aOffset, read);
+ mCachedOffset = aOffset;
+ mCachedBytes = read;
+ }
+ // Copy relevant part into output.
+ uint32_t toCopy = std::min(aCount, read);
+ memcpy(aBuffer, &mCachedBlock[cacheIndex], toCopy);
+ *aBytes += toCopy;
+ ILOG("ReadAt(%" PRIu32 "@%" PRId64 ") - copied %" PRIu32 "@%" PRId64
+ " -> OK, %" PRIu32,
+ aCount, aOffset, toCopy, aOffset, *aBytes);
+ // We may not have read all that was requested, but we got everything
+ // we could get, so we're done.
+ return NS_OK;
+ }
+ ILOG("ReadAt(%" PRIu32 "@%" PRId64 ") - UncachedRangedReadAt(%" PRIu32
+ "..%" PRIu32 "@%" PRId64
+ ") failed: %s, will fallback to blocking read...",
+ aCount, aOffset, aCount, toRead, aOffset, ResultName(rv).get());
+ // Failure during reading. Note that this may be due to the cache
+ // changing between `GetCachedDataEnd` and `ReadAt`, so it's not
+ // totally unexpected, just hopefully rare; but we do need to handle it.
+
+ // Invalidate part of cache that may have been partially overridden.
+ if (mCachedOffset + mCachedBytes == aOffset) {
+ // We were topping-up the cache, just keep the old untouched data.
+ // (i.e., nothing to do here.)
+ } else {
+ // We were filling the cache from scratch, invalidate cache.
+ mCachedBytes = 0;
+ }
+ } else {
+ ILOG("ReadAt(%" PRIu32 "@%" PRId64
+ ") - no cached data, will fallback to blocking read...",
+ aCount, aOffset);
+ }
+ } else {
+ ILOG("ReadAt(%" PRIu32 "@%" PRId64 ") - length is %" PRId64
+ " (%s), will fallback to blocking read as the caller requested...",
+ aCount, aOffset, length, length < 0 ? "unknown" : "too short!");
+ }
+ uint32_t read = 0;
+ nsresult rv = UncachedReadAt(aOffset, aBuffer, aCount, &read);
+ if (NS_SUCCEEDED(rv)) {
+ *aBytes += read;
+ ILOG("ReadAt(%" PRIu32 "@%" PRId64 ") - fallback uncached read got %" PRIu32
+ " bytes -> %s, %" PRIu32,
+ aCount, aOffset, read, ResultName(rv).get(), *aBytes);
+ } else {
+ ILOG("ReadAt(%" PRIu32 "@%" PRId64
+ ") - fallback uncached read failed -> %s, %" PRIu32,
+ aCount, aOffset, ResultName(rv).get(), *aBytes);
+ }
+ return rv;
+}
+
+nsresult MediaResourceIndex::UncachedReadAt(int64_t aOffset, char* aBuffer,
+ uint32_t aCount,
+ uint32_t* aBytes) const {
+ if (aOffset < 0) {
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+ if (aCount == 0) {
+ *aBytes = 0;
+ return NS_OK;
+ }
+ return mResource->ReadAt(aOffset, aBuffer, aCount, aBytes);
+}
+
+nsresult MediaResourceIndex::UncachedRangedReadAt(int64_t aOffset,
+ char* aBuffer,
+ uint32_t aRequestedCount,
+ uint32_t aExtraCount,
+ uint32_t* aBytes) const {
+ uint32_t count = aRequestedCount + aExtraCount;
+ if (aOffset < 0 || count < aRequestedCount) {
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+ if (count == 0) {
+ *aBytes = 0;
+ return NS_OK;
+ }
+ return mResource->ReadAt(aOffset, aBuffer, count, aBytes);
+}
+
+nsresult MediaResourceIndex::Seek(int32_t aWhence, int64_t aOffset) {
+ switch (aWhence) {
+ case SEEK_SET:
+ break;
+ case SEEK_CUR:
+ aOffset += mOffset;
+ break;
+ case SEEK_END: {
+ int64_t length = mResource->GetLength();
+ if (length == -1 || length - aOffset < 0) {
+ return NS_ERROR_FAILURE;
+ }
+ aOffset = mResource->GetLength() - aOffset;
+ } break;
+ default:
+ return NS_ERROR_FAILURE;
+ }
+
+ if (aOffset < 0) {
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+ mOffset = aOffset;
+
+ return NS_OK;
+}
+
+already_AddRefed<MediaByteBuffer> MediaResourceIndex::MediaReadAt(
+ int64_t aOffset, uint32_t aCount) const {
+ NS_ENSURE_TRUE(aOffset >= 0, nullptr);
+ RefPtr<MediaByteBuffer> bytes = new MediaByteBuffer();
+ bool ok = bytes->SetLength(aCount, fallible);
+ NS_ENSURE_TRUE(ok, nullptr);
+
+ uint32_t bytesRead = 0;
+ nsresult rv = mResource->ReadAt(
+ aOffset, reinterpret_cast<char*>(bytes->Elements()), aCount, &bytesRead);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+
+ bytes->SetLength(bytesRead);
+ return bytes.forget();
+}
+
+already_AddRefed<MediaByteBuffer> MediaResourceIndex::CachedMediaReadAt(
+ int64_t aOffset, uint32_t aCount) const {
+ RefPtr<MediaByteBuffer> bytes = new MediaByteBuffer();
+ bool ok = bytes->SetLength(aCount, fallible);
+ NS_ENSURE_TRUE(ok, nullptr);
+ char* curr = reinterpret_cast<char*>(bytes->Elements());
+ nsresult rv = mResource->ReadFromCache(curr, aOffset, aCount);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ return bytes.forget();
+}
+
+// Get the length of the stream in bytes. Returns -1 if not known.
+// This can change over time; after a seek operation, a misbehaving
+// server may give us a resource of a different length to what it had
+// reported previously --- or it may just lie in its Content-Length
+// header and give us more or less data than it reported. We will adjust
+// the result of GetLength to reflect the data that's actually arriving.
+int64_t MediaResourceIndex::GetLength() const { return mResource->GetLength(); }
+
+uint32_t MediaResourceIndex::IndexInCache(int64_t aOffsetInFile) const {
+ const uint32_t index = uint32_t(aOffsetInFile) & (mCacheBlockSize - 1);
+ MOZ_ASSERT(index == aOffsetInFile % mCacheBlockSize);
+ return index;
+}
+
+int64_t MediaResourceIndex::CacheOffsetContaining(int64_t aOffsetInFile) const {
+ const int64_t offset = aOffsetInFile & ~(int64_t(mCacheBlockSize) - 1);
+ MOZ_ASSERT(offset == aOffsetInFile - IndexInCache(aOffsetInFile));
+ return offset;
+}
+
+} // namespace mozilla
+
+// avoid redefined macro in unified build
+#undef ILOG
diff --git a/dom/media/MediaResource.h b/dom/media/MediaResource.h
new file mode 100644
index 0000000000..223340b876
--- /dev/null
+++ b/dom/media/MediaResource.h
@@ -0,0 +1,283 @@
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(MediaResource_h_)
+# define MediaResource_h_
+
+# include "DecoderDoctorLogger.h"
+# include "Intervals.h"
+# include "MediaData.h"
+# include "mozilla/Attributes.h"
+# include "mozilla/UniquePtr.h"
+# include "nsISeekableStream.h"
+# include "nsThreadUtils.h"
+
+namespace mozilla {
+
+// Represents a section of contiguous media, with a start and end offset.
+// Used to denote ranges of data which are cached.
+
+typedef media::Interval<int64_t> MediaByteRange;
+typedef media::IntervalSet<int64_t> MediaByteRangeSet;
+
+DDLoggedTypeDeclName(MediaResource);
+
+/**
+ * Provides a thread-safe, seek/read interface to resources
+ * loaded from a URI. Uses MediaCache to cache data received over
+ * Necko's async channel API, thus resolving the mismatch between clients
+ * that need efficient random access to the data and protocols that do not
+ * support efficient random access, such as HTTP.
+ *
+ * Instances of this class must be created on the main thread.
+ * Most methods must be called on the main thread only. Read, Seek and
+ * Tell must only be called on non-main threads. In the case of the Ogg
+ * Decoder they are called on the Decode thread for example. You must
+ * ensure that no threads are calling these methods once Close is called.
+ *
+ * Instances of this class are reference counted. Use nsRefPtr for
+ * managing the lifetime of instances of this class.
+ *
+ * The generic implementation of this class is ChannelMediaResource, which can
+ * handle any URI for which Necko supports AsyncOpen.
+ * The 'file:' protocol can be implemented efficiently with direct random
+ * access, so the FileMediaResource implementation class bypasses the cache.
+ * For cross-process blob URL, CloneableWithRangeMediaResource is used.
+ * MediaResource::Create automatically chooses the best implementation class.
+ */
+class MediaResource : public DecoderDoctorLifeLogger<MediaResource> {
+ public:
+ // Our refcounting is threadsafe, and when our refcount drops to zero
+ // we dispatch an event to the main thread to delete the MediaResource.
+ // Note that this means it's safe for references to this object to be
+ // released on a non main thread, but the destructor will always run on
+ // the main thread.
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING_WITH_DELETE_ON_MAIN_THREAD(
+ MediaResource)
+
+ // Close the resource, stop any listeners, channels, etc.
+ // Cancels any currently blocking Read request and forces that request to
+ // return an error. This must be called (and resolve) before the MediaResource
+ // is deleted.
+ virtual RefPtr<GenericPromise> Close() {
+ return GenericPromise::CreateAndResolve(true, __func__);
+ }
+
+ // These methods are called off the main thread.
+ // Read up to aCount bytes from the stream. The read starts at
+ // aOffset in the stream, seeking to that location initially if
+ // it is not the current stream offset. The remaining arguments,
+ // results and requirements are the same as per the Read method.
+ virtual nsresult ReadAt(int64_t aOffset, char* aBuffer, uint32_t aCount,
+ uint32_t* aBytes) = 0;
+ // Indicate whether caching data in advance of reads is worth it.
+ // E.g. Caching lockless and memory-based MediaResource subclasses would be a
+ // waste, but caching lock/IO-bound resources means reducing the impact of
+ // each read.
+ virtual bool ShouldCacheReads() = 0;
+
+ // These can be called on any thread.
+ // Cached blocks associated with this stream will not be evicted
+ // while the stream is pinned.
+ virtual void Pin() = 0;
+ virtual void Unpin() = 0;
+ // Get the length of the stream in bytes. Returns -1 if not known.
+ // This can change over time; after a seek operation, a misbehaving
+ // server may give us a resource of a different length to what it had
+ // reported previously --- or it may just lie in its Content-Length
+ // header and give us more or less data than it reported. We will adjust
+ // the result of GetLength to reflect the data that's actually arriving.
+ virtual int64_t GetLength() = 0;
+ // Returns the offset of the first byte of cached data at or after aOffset,
+ // or -1 if there is no such cached data.
+ virtual int64_t GetNextCachedData(int64_t aOffset) = 0;
+ // Returns the end of the bytes starting at the given offset which are in
+ // cache. Returns aOffset itself if there are zero bytes available there.
+ virtual int64_t GetCachedDataEnd(int64_t aOffset) = 0;
+ // Returns true if all the data from aOffset to the end of the stream
+ // is in cache. If the end of the stream is not known, we return false.
+ virtual bool IsDataCachedToEndOfResource(int64_t aOffset) = 0;
+ // Reads only data which is cached in the media cache. If you try to read
+ // any data which overlaps uncached data, or if aCount bytes otherwise can't
+ // be read, this function will return failure. This function be called from
+ // any thread, and it is the only read operation which is safe to call on
+ // the main thread, since it's guaranteed to be non blocking.
+ virtual nsresult ReadFromCache(char* aBuffer, int64_t aOffset,
+ uint32_t aCount) = 0;
+
+ /**
+ * Fills aRanges with MediaByteRanges representing the data which is cached
+ * in the media cache. Stream should be pinned during call and while
+ * aRanges is being used.
+ */
+ virtual nsresult GetCachedRanges(MediaByteRangeSet& aRanges) = 0;
+
+ protected:
+ virtual ~MediaResource() = default;
+};
+
+/**
+ * RAII class that handles pinning and unpinning for MediaResource and derived.
+ * This should be used when making calculations that involve potentially-cached
+ * MediaResource data, so that the state of the world can't change out from
+ * under us.
+ */
+template <class T>
+class MOZ_RAII AutoPinned {
+ public:
+ explicit AutoPinned(T* aResource) : mResource(aResource) {
+ MOZ_ASSERT(mResource);
+ mResource->Pin();
+ }
+
+ ~AutoPinned() { mResource->Unpin(); }
+
+ operator T*() const { return mResource; }
+ T* operator->() const MOZ_NO_ADDREF_RELEASE_ON_RETURN { return mResource; }
+
+ private:
+ T* mResource;
+};
+
+DDLoggedTypeDeclName(MediaResourceIndex);
+
+/*
+ * MediaResourceIndex provides a way to access MediaResource objects.
+ * Read, Seek and Tell must only be called on non-main threads.
+ * In the case of the Ogg Decoder they are called on the Decode thread for
+ * example. You must ensure that no threads are calling these methods once
+ * the MediaResource has been Closed.
+ */
+class MediaResourceIndex : public DecoderDoctorLifeLogger<MediaResourceIndex> {
+ public:
+ explicit MediaResourceIndex(MediaResource* aResource);
+
+ // Read up to aCount bytes from the stream. The buffer must have
+ // enough room for at least aCount bytes. Stores the number of
+ // actual bytes read in aBytes (0 on end of file).
+ // May read less than aCount bytes if the number of
+ // available bytes is less than aCount. Always check *aBytes after
+ // read, and call again if necessary.
+ nsresult Read(char* aBuffer, uint32_t aCount, uint32_t* aBytes);
+ // Seek to the given bytes offset in the stream. aWhence can be
+ // one of:
+ // nsISeekableStream::NS_SEEK_SET
+ // nsISeekableStream::NS_SEEK_CUR
+ // nsISeekableStream::NS_SEEK_END
+ //
+ // In the Http strategy case the cancel will cause the http
+ // channel's listener to close the pipe, forcing an i/o error on any
+ // blocked read. This will allow the decode thread to complete the
+ // event.
+ //
+ // In the case of a seek in progress, the byte range request creates
+ // a new listener. This is done on the main thread via seek
+ // synchronously dispatching an event. This avoids the issue of us
+ // closing the listener but an outstanding byte range request
+ // creating a new one. They run on the same thread so no explicit
+ // synchronisation is required. The byte range request checks for
+ // the cancel flag and does not create a new channel or listener if
+ // we are cancelling.
+ //
+ // The default strategy does not do any seeking - the only issue is
+ // a blocked read which it handles by causing the listener to close
+ // the pipe, as per the http case.
+ //
+ // The file strategy doesn't block for any great length of time so
+ // is fine for a no-op cancel.
+ nsresult Seek(int32_t aWhence, int64_t aOffset);
+ // Report the current offset in bytes from the start of the stream.
+ int64_t Tell() const { return mOffset; }
+
+ // Return the underlying MediaResource.
+ MediaResource* GetResource() const { return mResource; }
+
+ // Read up to aCount bytes from the stream. The read starts at
+ // aOffset in the stream, seeking to that location initially if
+ // it is not the current stream offset.
+ // Unlike MediaResource::ReadAt, ReadAt only returns fewer bytes than
+ // requested if end of stream or an error is encountered. There is no need to
+ // call it again to get more data.
+ // If the resource has cached data past the end of the request, it will be
+ // used to fill a local cache, which should speed up consecutive ReadAt's
+ // (mostly by avoiding using the resource's IOs and locks.)
+ // *aBytes will contain the number of bytes copied, even if an error occurred.
+ // ReadAt doesn't have an impact on the offset returned by Tell().
+ nsresult ReadAt(int64_t aOffset, char* aBuffer, uint32_t aCount,
+ uint32_t* aBytes);
+
+ // Same as ReadAt, but doesn't try to cache around the read.
+ // Useful if you know that you will not read again from the same area.
+ nsresult UncachedReadAt(int64_t aOffset, char* aBuffer, uint32_t aCount,
+ uint32_t* aBytes) const;
+
+ // Similar to ReadAt, but doesn't try to cache around the read.
+ // Useful if you know that you will not read again from the same area.
+ // Will attempt to read aRequestedCount+aExtraCount, repeatedly calling
+ // MediaResource/ ReadAt()'s until a read returns 0 bytes (so we may actually
+ // get less than aRequestedCount bytes), or until we get at least
+ // aRequestedCount bytes (so we may not get any/all of the aExtraCount bytes.)
+ nsresult UncachedRangedReadAt(int64_t aOffset, char* aBuffer,
+ uint32_t aRequestedCount, uint32_t aExtraCount,
+ uint32_t* aBytes) const;
+
+ // This method returns nullptr if anything fails.
+ // Otherwise, it returns an owned buffer.
+ // MediaReadAt may return fewer bytes than requested if end of stream is
+ // encountered. There is no need to call it again to get more data.
+ // Note this method will not update mOffset.
+ already_AddRefed<MediaByteBuffer> MediaReadAt(int64_t aOffset,
+ uint32_t aCount) const;
+
+ already_AddRefed<MediaByteBuffer> CachedMediaReadAt(int64_t aOffset,
+ uint32_t aCount) const;
+
+ // Get the length of the stream in bytes. Returns -1 if not known.
+ // This can change over time; after a seek operation, a misbehaving
+ // server may give us a resource of a different length to what it had
+ // reported previously --- or it may just lie in its Content-Length
+ // header and give us more or less data than it reported. We will adjust
+ // the result of GetLength to reflect the data that's actually arriving.
+ int64_t GetLength() const;
+
+ private:
+ // If the resource has cached data past the requested range, try to grab it
+ // into our local cache.
+ // If there is no cached data, or attempting to read it fails, fallback on
+ // a (potentially-blocking) read of just what was requested, so that we don't
+ // get unexpected side-effects by trying to read more than intended.
+ nsresult CacheOrReadAt(int64_t aOffset, char* aBuffer, uint32_t aCount,
+ uint32_t* aBytes);
+
+ // Maps a file offset to a mCachedBlock index.
+ uint32_t IndexInCache(int64_t aOffsetInFile) const;
+
+ // Starting file offset of the cache block that contains a given file offset.
+ int64_t CacheOffsetContaining(int64_t aOffsetInFile) const;
+
+ RefPtr<MediaResource> mResource;
+ int64_t mOffset;
+
+ // Local cache used by ReadAt().
+ // mCachedBlock is valid when mCachedBytes != 0, in which case it contains
+ // data of length mCachedBytes, starting at offset `mCachedOffset` in the
+ // resource, located at index `IndexInCache(mCachedOffset)` in mCachedBlock.
+ //
+ // resource: |------------------------------------------------------|
+ // <----------> mCacheBlockSize
+ // <---------------------------------> mCachedOffset
+ // <--> mCachedBytes
+ // mCachedBlock: |..----....|
+ // CacheOffsetContaining(mCachedOffset) <--> IndexInCache(mCachedOffset)
+ // <------------------------------>
+ const uint32_t mCacheBlockSize;
+ int64_t mCachedOffset;
+ uint32_t mCachedBytes;
+ UniquePtr<char[]> mCachedBlock;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/MediaResourceCallback.h b/dom/media/MediaResourceCallback.h
new file mode 100644
index 0000000000..6c4bb08c07
--- /dev/null
+++ b/dom/media/MediaResourceCallback.h
@@ -0,0 +1,66 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MediaResourceCallback_h_
+#define MediaResourceCallback_h_
+
+#include "DecoderDoctorLogger.h"
+#include "nsError.h"
+#include "nsISupportsImpl.h"
+#include "MediaResult.h"
+
+namespace mozilla {
+
+class AbstractThread;
+class MediaDecoderOwner;
+class MediaResource;
+
+DDLoggedTypeDeclName(MediaResourceCallback);
+
+/**
+ * A callback used by MediaResource (sub-classes like FileMediaResource,
+ * RtspMediaResource, and ChannelMediaResource) to notify various events.
+ * Currently this is implemented by MediaDecoder only.
+ *
+ * Since this class has no pure virtual function, it is convenient to write
+ * gtests for the readers without using a mock MediaResource when you don't
+ * care about the events notified by the MediaResource.
+ */
+class MediaResourceCallback
+ : public DecoderDoctorLifeLogger<MediaResourceCallback> {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaResourceCallback);
+
+ // Return an abstract thread on which to run main thread runnables.
+ virtual AbstractThread* AbstractMainThread() const { return nullptr; }
+
+ // Returns a weak reference to the media decoder owner.
+ virtual MediaDecoderOwner* GetMediaOwner() const { return nullptr; }
+
+ // Notify that a network error is encountered.
+ virtual void NotifyNetworkError(const MediaResult& aError) {}
+
+ // Notify that data arrives on the stream and is read into the cache.
+ virtual void NotifyDataArrived() {}
+
+ // Notify download is ended.
+ // NOTE: this can be called with the media cache lock held, so don't
+ // block or do anything which might try to acquire a lock!
+ virtual void NotifyDataEnded(nsresult aStatus) {}
+
+ // Notify that the principal of MediaResource has changed.
+ virtual void NotifyPrincipalChanged() {}
+
+ // Notify that the "cache suspended" status of MediaResource changes.
+ virtual void NotifySuspendedStatusChanged(bool aSuspendedByCache) {}
+
+ protected:
+ virtual ~MediaResourceCallback() = default;
+};
+
+} // namespace mozilla
+
+#endif // MediaResourceCallback_h_
diff --git a/dom/media/MediaResult.h b/dom/media/MediaResult.h
new file mode 100644
index 0000000000..5504dcda4c
--- /dev/null
+++ b/dom/media/MediaResult.h
@@ -0,0 +1,82 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MediaResult_h_
+#define MediaResult_h_
+
+#include "nsString.h" // Required before 'mozilla/ErrorNames.h'!?
+#include "mozilla/ErrorNames.h"
+#include "mozilla/TimeStamp.h"
+#include "nsError.h"
+#include "nsPrintfCString.h"
+
+// MediaResult can be used interchangeably with nsresult.
+// It allows to store extra information such as where the error occurred.
+// While nsresult is typically passed by value; due to its potential size, using
+// MediaResult const references is recommended.
+namespace mozilla {
+
+class CDMProxy;
+
+class MediaResult {
+ public:
+ MediaResult() : mCode(NS_OK) {}
+ MOZ_IMPLICIT MediaResult(nsresult aResult) : mCode(aResult) {}
+ MediaResult(nsresult aResult, const nsACString& aMessage)
+ : mCode(aResult), mMessage(aMessage) {}
+ MediaResult(nsresult aResult, const char* aMessage)
+ : mCode(aResult), mMessage(aMessage) {}
+ MediaResult(nsresult aResult, CDMProxy* aCDMProxy)
+ : mCode(aResult), mCDMProxy(aCDMProxy) {
+ MOZ_ASSERT(aResult == NS_ERROR_DOM_MEDIA_CDM_PROXY_NOT_SUPPORTED_ERR);
+ }
+ MediaResult(const MediaResult& aOther) = default;
+ MediaResult(MediaResult&& aOther) = default;
+ MediaResult& operator=(const MediaResult& aOther) = default;
+ MediaResult& operator=(MediaResult&& aOther) = default;
+
+ nsresult Code() const { return mCode; }
+ nsCString ErrorName() const {
+ nsCString name;
+ GetErrorName(mCode, name);
+ return name;
+ }
+
+ const nsCString& Message() const { return mMessage; }
+
+ // Interoperations with nsresult.
+ bool operator==(nsresult aResult) const { return aResult == mCode; }
+ bool operator!=(nsresult aResult) const { return aResult != mCode; }
+ operator nsresult() const { return mCode; }
+
+ nsCString Description() const {
+ if (NS_SUCCEEDED(mCode)) {
+ return nsCString();
+ }
+ return nsPrintfCString("%s (0x%08" PRIx32 ")%s%s", ErrorName().get(),
+ static_cast<uint32_t>(mCode),
+ mMessage.IsEmpty() ? "" : " - ", mMessage.get());
+ }
+
+ CDMProxy* GetCDMProxy() const { return mCDMProxy; }
+
+ private:
+ nsresult mCode;
+ nsCString mMessage;
+ // It's used when the error is NS_ERROR_DOM_MEDIA_CDM_PROXY_NOT_SUPPORTED_ERR.
+ CDMProxy* mCDMProxy;
+};
+
+#ifdef _MSC_VER
+# define RESULT_DETAIL(arg, ...) \
+ nsPrintfCString("%s: " arg, __FUNCSIG__, ##__VA_ARGS__)
+#else
+# define RESULT_DETAIL(arg, ...) \
+ nsPrintfCString("%s: " arg, __PRETTY_FUNCTION__, ##__VA_ARGS__)
+#endif
+
+} // namespace mozilla
+#endif // MediaResult_h_
diff --git a/dom/media/MediaSegment.h b/dom/media/MediaSegment.h
new file mode 100644
index 0000000000..181a726c74
--- /dev/null
+++ b/dom/media/MediaSegment.h
@@ -0,0 +1,501 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_MEDIASEGMENT_H_
+#define MOZILLA_MEDIASEGMENT_H_
+
+#include "PrincipalHandle.h"
+#include "nsTArray.h"
+#ifdef MOZILLA_INTERNAL_API
+# include "mozilla/TimeStamp.h"
+#endif
+#include <algorithm>
+
+namespace mozilla {
+
+/**
+ * Track or graph rate in Hz. Maximum 1 << TRACK_RATE_MAX_BITS Hz. This
+ * maximum avoids overflow in conversions between track rates and conversions
+ * from seconds.
+ */
+typedef int32_t TrackRate;
+const int64_t TRACK_RATE_MAX_BITS = 20;
+const TrackRate TRACK_RATE_MAX = 1 << TRACK_RATE_MAX_BITS;
+
+/**
+ * A number of ticks at a rate determined by some underlying track (e.g., audio
+ * sample rate). We want to make sure that multiplying TrackTicks by a TrackRate
+ * doesn't overflow, so we set its max accordingly.
+ * TrackTime should be used instead when we're working with MediaTrackGraph's
+ * rate, but TrackTicks can be used outside MediaTracks when we have data at a
+ * different rate.
+ */
+typedef int64_t TrackTicks;
+const int64_t TRACK_TICKS_MAX = INT64_MAX >> TRACK_RATE_MAX_BITS;
+
+/**
+ * We represent media times in 64-bit audio frame counts or ticks.
+ * All tracks in a MediaTrackGraph have the same rate.
+ */
+typedef int64_t MediaTime;
+const int64_t MEDIA_TIME_MAX = TRACK_TICKS_MAX;
+
+/**
+ * Media time relative to the start of a MediaTrack.
+ */
+typedef MediaTime TrackTime;
+const TrackTime TRACK_TIME_MAX = MEDIA_TIME_MAX;
+
+/**
+ * Media time relative to the start of the graph timeline.
+ */
+typedef MediaTime GraphTime;
+const GraphTime GRAPH_TIME_MAX = MEDIA_TIME_MAX;
+
+/* Time conversion helper functions */
+inline TrackTicks RateConvertTicksRoundDown(TrackRate aOutRate,
+ TrackRate aInRate,
+ TrackTicks aTicks) {
+ MOZ_ASSERT(0 < aOutRate && aOutRate <= TRACK_RATE_MAX, "Bad out rate");
+ MOZ_ASSERT(0 < aInRate && aInRate <= TRACK_RATE_MAX, "Bad in rate");
+ MOZ_ASSERT(0 <= aTicks && aTicks <= TRACK_TICKS_MAX, "Bad ticks");
+ return (aTicks * aOutRate) / aInRate;
+}
+
+inline TrackTicks RateConvertTicksRoundUp(TrackRate aOutRate, TrackRate aInRate,
+ TrackTicks aTicks) {
+ MOZ_ASSERT(0 < aOutRate && aOutRate <= TRACK_RATE_MAX, "Bad out rate");
+ MOZ_ASSERT(0 < aInRate && aInRate <= TRACK_RATE_MAX, "Bad in rate");
+ MOZ_ASSERT(0 <= aTicks && aTicks <= TRACK_TICKS_MAX, "Bad ticks");
+ return (aTicks * aOutRate + aInRate - 1) / aInRate;
+}
+
+/**
+ * The number of chunks allocated by default for a MediaSegment.
+ * Appending more chunks than this will cause further allocations.
+ *
+ * 16 is an arbitrary number intended to cover the most common cases in the
+ * MediaTrackGraph (1 with silence and 1-2 with data for a realtime track)
+ * with some margin.
+ */
+const size_t DEFAULT_SEGMENT_CAPACITY = 16;
+
+/**
+ * A MediaSegment is a chunk of media data sequential in time. Different
+ * types of data have different subclasses of MediaSegment, all inheriting
+ * from MediaSegmentBase.
+ * All MediaSegment data is timed using TrackTime. The actual tick rate
+ * is defined on a per-track basis. For some track types, this can be
+ * a fixed constant for all tracks of that type (e.g. 1MHz for video).
+ *
+ * Each media segment defines a concept of "null media data" (e.g. silence
+ * for audio or "no video frame" for video), which can be efficiently
+ * represented. This is used for padding.
+ */
+class MediaSegment {
+ public:
+ MediaSegment(const MediaSegment&) = delete;
+ MediaSegment& operator=(const MediaSegment&) = delete;
+
+ MOZ_COUNTED_DTOR_VIRTUAL(MediaSegment)
+
+ enum Type { AUDIO, VIDEO, TYPE_COUNT };
+
+ /**
+ * Gets the total duration of the segment.
+ */
+ TrackTime GetDuration() const { return mDuration; }
+ Type GetType() const { return mType; }
+
+ /**
+ * Gets the last principal id that was appended to this segment.
+ */
+ const PrincipalHandle& GetLastPrincipalHandle() const {
+ return mLastPrincipalHandle;
+ }
+ /**
+ * Called by the MediaTrackGraph as it appends a chunk with a different
+ * principal id than the current one.
+ */
+ void SetLastPrincipalHandle(PrincipalHandle aLastPrincipalHandle) {
+ mLastPrincipalHandle = std::forward<PrincipalHandle>(aLastPrincipalHandle);
+ }
+
+ /**
+ * Returns true if all chunks in this segment are null.
+ */
+ virtual bool IsNull() const = 0;
+
+ /**
+ * Returns true if this segment contains no chunks.
+ */
+ virtual bool IsEmpty() const = 0;
+
+ /**
+ * Create a MediaSegment of the same type.
+ */
+ virtual MediaSegment* CreateEmptyClone() const = 0;
+ /**
+ * Moves contents of aSource to the end of this segment.
+ */
+ virtual void AppendFrom(MediaSegment* aSource) = 0;
+ /**
+ * Append a slice of aSource to this segment.
+ */
+ virtual void AppendSlice(const MediaSegment& aSource, TrackTime aStart,
+ TrackTime aEnd) = 0;
+ /**
+ * Replace all contents up to aDuration with null data.
+ */
+ virtual void ForgetUpTo(TrackTime aDuration) = 0;
+ /**
+ * Forget all data buffered after a given point
+ */
+ virtual void FlushAfter(TrackTime aNewEnd) = 0;
+ /**
+ * Insert aDuration of null data at the start of the segment.
+ */
+ virtual void InsertNullDataAtStart(TrackTime aDuration) = 0;
+ /**
+ * Insert aDuration of null data at the end of the segment.
+ */
+ virtual void AppendNullData(TrackTime aDuration) = 0;
+ /**
+ * Replace contents with disabled (silence/black) data of the same duration
+ */
+ virtual void ReplaceWithDisabled() = 0;
+ /**
+ * Replace contents with null data of the same duration
+ */
+ virtual void ReplaceWithNull() = 0;
+ /**
+ * Remove all contents, setting duration to 0.
+ */
+ virtual void Clear() = 0;
+
+ virtual size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ return 0;
+ }
+
+ virtual size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ protected:
+ explicit MediaSegment(Type aType)
+ : mDuration(0),
+ mType(aType),
+ mLastPrincipalHandle(PRINCIPAL_HANDLE_NONE) {
+ MOZ_COUNT_CTOR(MediaSegment);
+ }
+
+ MediaSegment(MediaSegment&& aSegment)
+ : mDuration(std::move(aSegment.mDuration)),
+ mType(std::move(aSegment.mType)),
+ mLastPrincipalHandle(std::move(aSegment.mLastPrincipalHandle)) {
+ MOZ_COUNT_CTOR(MediaSegment);
+ }
+
+ TrackTime mDuration; // total of mDurations of all chunks
+ Type mType;
+
+ // The latest principal handle that the MediaTrackGraph has processed for
+ // this segment.
+ PrincipalHandle mLastPrincipalHandle;
+};
+
+/**
+ * C is the implementation class subclassed from MediaSegmentBase.
+ * C must contain a Chunk class.
+ */
+template <class C, class Chunk>
+class MediaSegmentBase : public MediaSegment {
+ public:
+ bool IsNull() const override {
+ for (typename C::ConstChunkIterator iter(*this); !iter.IsEnded();
+ iter.Next()) {
+ if (!iter->IsNull()) {
+ return false;
+ }
+ }
+ return true;
+ }
+ bool IsEmpty() const override { return mChunks.IsEmpty(); }
+ MediaSegment* CreateEmptyClone() const override { return new C(); }
+ void AppendFrom(MediaSegment* aSource) override {
+ NS_ASSERTION(aSource->GetType() == C::StaticType(), "Wrong type");
+ AppendFromInternal(static_cast<C*>(aSource));
+ }
+ void AppendFrom(C* aSource) { AppendFromInternal(aSource); }
+ void AppendSlice(const MediaSegment& aSource, TrackTime aStart,
+ TrackTime aEnd) override {
+ NS_ASSERTION(aSource.GetType() == C::StaticType(), "Wrong type");
+ AppendSliceInternal(static_cast<const C&>(aSource), aStart, aEnd);
+ }
+ void AppendSlice(const C& aOther, TrackTime aStart, TrackTime aEnd) {
+ AppendSliceInternal(aOther, aStart, aEnd);
+ }
+ /**
+ * Replace the first aDuration ticks with null media data, because the data
+ * will not be required again.
+ */
+ void ForgetUpTo(TrackTime aDuration) override {
+ if (mChunks.IsEmpty() || aDuration <= 0) {
+ return;
+ }
+ if (mChunks[0].IsNull()) {
+ TrackTime extraToForget =
+ std::min(aDuration, mDuration) - mChunks[0].GetDuration();
+ if (extraToForget > 0) {
+ RemoveLeading(extraToForget, 1);
+ mChunks[0].mDuration += extraToForget;
+ mDuration += extraToForget;
+ }
+ return;
+ }
+ RemoveLeading(aDuration, 0);
+ mChunks.InsertElementAt(0)->SetNull(aDuration);
+ mDuration += aDuration;
+ }
+ void FlushAfter(TrackTime aNewEnd) override {
+ if (mChunks.IsEmpty()) {
+ return;
+ }
+
+ if (!aNewEnd) {
+ Clear();
+ } else if (mChunks[0].IsNull()) {
+ TrackTime extraToKeep = aNewEnd - mChunks[0].GetDuration();
+ if (extraToKeep < 0) {
+ // reduce the size of the Null, get rid of everthing else
+ mChunks[0].SetNull(aNewEnd);
+ extraToKeep = 0;
+ }
+ RemoveTrailing(extraToKeep, 1);
+ } else {
+ if (aNewEnd > mDuration) {
+ NS_ASSERTION(aNewEnd <= mDuration, "can't add data in FlushAfter");
+ return;
+ }
+ RemoveTrailing(aNewEnd, 0);
+ }
+ mDuration = aNewEnd;
+ }
+ void InsertNullDataAtStart(TrackTime aDuration) override {
+ if (aDuration <= 0) {
+ return;
+ }
+ if (!mChunks.IsEmpty() && mChunks[0].IsNull()) {
+ mChunks[0].mDuration += aDuration;
+ } else {
+ mChunks.InsertElementAt(0)->SetNull(aDuration);
+ }
+ mDuration += aDuration;
+ }
+ void AppendNullData(TrackTime aDuration) override {
+ if (aDuration <= 0) {
+ return;
+ }
+ if (!mChunks.IsEmpty() && mChunks[mChunks.Length() - 1].IsNull()) {
+ mChunks[mChunks.Length() - 1].mDuration += aDuration;
+ } else {
+ mChunks.AppendElement()->SetNull(aDuration);
+ }
+ mDuration += aDuration;
+ }
+ void ReplaceWithDisabled() override {
+ if (GetType() != AUDIO) {
+ MOZ_CRASH("Disabling unknown segment type");
+ }
+ ReplaceWithNull();
+ }
+ void ReplaceWithNull() override {
+ TrackTime duration = GetDuration();
+ Clear();
+ AppendNullData(duration);
+ }
+ void Clear() override {
+ mDuration = 0;
+ mChunks.ClearAndRetainStorage();
+ mChunks.SetCapacity(DEFAULT_SEGMENT_CAPACITY);
+ }
+
+ class ChunkIterator {
+ public:
+ explicit ChunkIterator(MediaSegmentBase<C, Chunk>& aSegment)
+ : mSegment(aSegment), mIndex(0) {}
+ bool IsEnded() { return mIndex >= mSegment.mChunks.Length(); }
+ void Next() { ++mIndex; }
+ Chunk& operator*() { return mSegment.mChunks[mIndex]; }
+ Chunk* operator->() { return &mSegment.mChunks[mIndex]; }
+
+ private:
+ MediaSegmentBase<C, Chunk>& mSegment;
+ uint32_t mIndex;
+ };
+ class ConstChunkIterator {
+ public:
+ explicit ConstChunkIterator(const MediaSegmentBase<C, Chunk>& aSegment)
+ : mSegment(aSegment), mIndex(0) {}
+ bool IsEnded() { return mIndex >= mSegment.mChunks.Length(); }
+ void Next() { ++mIndex; }
+ const Chunk& operator*() { return mSegment.mChunks[mIndex]; }
+ const Chunk* operator->() { return &mSegment.mChunks[mIndex]; }
+
+ private:
+ const MediaSegmentBase<C, Chunk>& mSegment;
+ uint32_t mIndex;
+ };
+
+ void RemoveLeading(TrackTime aDuration) { RemoveLeading(aDuration, 0); }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override {
+ size_t amount = mChunks.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (size_t i = 0; i < mChunks.Length(); i++) {
+ amount += mChunks[i].SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ }
+ return amount;
+ }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ Chunk* GetLastChunk() {
+ if (mChunks.IsEmpty()) {
+ return nullptr;
+ }
+ return &mChunks[mChunks.Length() - 1];
+ }
+
+ const Chunk* GetLastChunk() const {
+ if (mChunks.IsEmpty()) {
+ return nullptr;
+ }
+ return &mChunks[mChunks.Length() - 1];
+ }
+
+ protected:
+ explicit MediaSegmentBase(Type aType) : MediaSegment(aType), mChunks() {}
+
+ MediaSegmentBase(MediaSegmentBase&& aSegment)
+ : MediaSegment(std::move(aSegment)),
+ mChunks(std::move(aSegment.mChunks)) {
+ MOZ_ASSERT(mChunks.Capacity() >= DEFAULT_SEGMENT_CAPACITY,
+ "Capacity must be retained in self after swap");
+ MOZ_ASSERT(aSegment.mChunks.Capacity() >= DEFAULT_SEGMENT_CAPACITY,
+ "Capacity must be retained in other after swap");
+ }
+
+ /**
+ * Appends the contents of aSource to this segment, clearing aSource.
+ */
+ void AppendFromInternal(MediaSegmentBase<C, Chunk>* aSource) {
+ MOZ_ASSERT(aSource->mDuration >= 0);
+ mDuration += aSource->mDuration;
+ aSource->mDuration = 0;
+ size_t offset = 0;
+ if (!mChunks.IsEmpty() && !aSource->mChunks.IsEmpty() &&
+ mChunks[mChunks.Length() - 1].CanCombineWithFollowing(
+ aSource->mChunks[0])) {
+ mChunks[mChunks.Length() - 1].mDuration += aSource->mChunks[0].mDuration;
+ offset = 1;
+ }
+
+ for (; offset < aSource->mChunks.Length(); ++offset) {
+ mChunks.AppendElement(std::move(aSource->mChunks[offset]));
+ }
+
+ aSource->mChunks.ClearAndRetainStorage();
+ MOZ_ASSERT(aSource->mChunks.Capacity() >= DEFAULT_SEGMENT_CAPACITY,
+ "Capacity must be retained after appending from aSource");
+ }
+
+ void AppendSliceInternal(const MediaSegmentBase<C, Chunk>& aSource,
+ TrackTime aStart, TrackTime aEnd) {
+ MOZ_ASSERT(aStart <= aEnd, "Endpoints inverted");
+ NS_ASSERTION(aStart >= 0 && aEnd <= aSource.mDuration,
+ "Slice out of range");
+ mDuration += aEnd - aStart;
+ TrackTime offset = 0;
+ for (uint32_t i = 0; i < aSource.mChunks.Length() && offset < aEnd; ++i) {
+ const Chunk& c = aSource.mChunks[i];
+ TrackTime start = std::max(aStart, offset);
+ TrackTime nextOffset = offset + c.GetDuration();
+ TrackTime end = std::min(aEnd, nextOffset);
+ if (start < end) {
+ if (!mChunks.IsEmpty() &&
+ mChunks[mChunks.Length() - 1].CanCombineWithFollowing(c)) {
+ MOZ_ASSERT(start - offset >= 0 && end - offset <= aSource.mDuration,
+ "Slice out of bounds");
+ mChunks[mChunks.Length() - 1].mDuration += end - start;
+ } else {
+ mChunks.AppendElement(c)->SliceTo(start - offset, end - offset);
+ }
+ }
+ offset = nextOffset;
+ }
+ }
+
+ Chunk* AppendChunk(TrackTime aDuration) {
+ MOZ_ASSERT(aDuration >= 0);
+ Chunk* c = mChunks.AppendElement();
+ c->mDuration = aDuration;
+ mDuration += aDuration;
+ return c;
+ }
+
+ void RemoveLeading(TrackTime aDuration, uint32_t aStartIndex) {
+ NS_ASSERTION(aDuration >= 0, "Can't remove negative duration");
+ TrackTime t = aDuration;
+ uint32_t chunksToRemove = 0;
+ for (uint32_t i = aStartIndex; i < mChunks.Length() && t > 0; ++i) {
+ Chunk* c = &mChunks[i];
+ if (c->GetDuration() > t) {
+ c->SliceTo(t, c->GetDuration());
+ t = 0;
+ break;
+ }
+ t -= c->GetDuration();
+ chunksToRemove = i + 1 - aStartIndex;
+ }
+ if (aStartIndex == 0 && chunksToRemove == mChunks.Length()) {
+ mChunks.ClearAndRetainStorage();
+ } else {
+ mChunks.RemoveElementsAt(aStartIndex, chunksToRemove);
+ }
+ mDuration -= aDuration - t;
+
+ MOZ_ASSERT(mChunks.Capacity() >= DEFAULT_SEGMENT_CAPACITY,
+ "Capacity must be retained after removing chunks");
+ }
+
+ void RemoveTrailing(TrackTime aKeep, uint32_t aStartIndex) {
+ NS_ASSERTION(aKeep >= 0, "Can't keep negative duration");
+ TrackTime t = aKeep;
+ uint32_t i;
+ for (i = aStartIndex; i < mChunks.Length() && t; ++i) {
+ Chunk* c = &mChunks[i];
+ if (c->GetDuration() > t) {
+ c->SliceTo(0, t);
+ break;
+ }
+ t -= c->GetDuration();
+ }
+ // At this point `i` is already advanced due to last check in the loop.
+ if (i < mChunks.Length()) {
+ mChunks.RemoveLastElements(mChunks.Length() - i);
+ }
+ MOZ_ASSERT(mChunks.Capacity() >= DEFAULT_SEGMENT_CAPACITY,
+ "Capacity must be retained after removing chunks");
+ // Caller must adjust mDuration
+ }
+
+ AutoTArray<Chunk, DEFAULT_SEGMENT_CAPACITY> mChunks;
+};
+
+} // namespace mozilla
+
+#endif /* MOZILLA_MEDIASEGMENT_H_ */
diff --git a/dom/media/MediaShutdownManager.cpp b/dom/media/MediaShutdownManager.cpp
new file mode 100644
index 0000000000..4a3c3a4357
--- /dev/null
+++ b/dom/media/MediaShutdownManager.cpp
@@ -0,0 +1,202 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaShutdownManager.h"
+
+#include "MediaDecoder.h"
+#include "mozilla/Logging.h"
+#include "mozilla/media/MediaUtils.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPtr.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIWritablePropertyBag2.h"
+
+namespace mozilla {
+
+#undef LOGW
+
+extern LazyLogModule gMediaDecoderLog;
+#define DECODER_LOG(type, msg) MOZ_LOG(gMediaDecoderLog, type, msg)
+#define LOGW(...) NS_WARNING(nsPrintfCString(__VA_ARGS__).get())
+
+NS_IMPL_ISUPPORTS(MediaShutdownManager, nsIAsyncShutdownBlocker)
+
+MediaShutdownManager::MediaShutdownManager() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(sInitPhase == NotInited);
+}
+
+MediaShutdownManager::~MediaShutdownManager() { MOZ_ASSERT(NS_IsMainThread()); }
+
+// Note that we don't use ClearOnShutdown() on this StaticRefPtr, as that
+// may interfere with our shutdown listener.
+StaticRefPtr<MediaShutdownManager> MediaShutdownManager::sInstance;
+
+MediaShutdownManager::InitPhase MediaShutdownManager::sInitPhase =
+ MediaShutdownManager::NotInited;
+
+MediaShutdownManager& MediaShutdownManager::Instance() {
+ MOZ_ASSERT(NS_IsMainThread());
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ if (!sInstance) {
+ MOZ_CRASH_UNSAFE_PRINTF("sInstance is null. sInitPhase=%d",
+ int(sInitPhase));
+ }
+#endif
+ return *sInstance;
+}
+
+void MediaShutdownManager::InitStatics() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (sInitPhase != NotInited) {
+ return;
+ }
+
+ sInstance = new MediaShutdownManager();
+ MOZ_DIAGNOSTIC_ASSERT(sInstance);
+
+ nsCOMPtr<nsIAsyncShutdownClient> barrier = media::GetShutdownBarrier();
+
+ if (!barrier) {
+ LOGW("Failed to get barrier, cannot add shutdown blocker!");
+ sInitPhase = InitFailed;
+ return;
+ }
+
+ nsresult rv =
+ barrier->AddBlocker(sInstance, NS_LITERAL_STRING_FROM_CSTRING(__FILE__),
+ __LINE__, u"MediaShutdownManager shutdown"_ns);
+ if (NS_FAILED(rv)) {
+ LOGW("Failed to add shutdown blocker! rv=%x", uint32_t(rv));
+ sInitPhase = InitFailed;
+ return;
+ }
+ sInitPhase = InitSucceeded;
+}
+
+void MediaShutdownManager::RemoveBlocker() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(sInitPhase == XPCOMShutdownStarted);
+ MOZ_ASSERT(mDecoders.Count() == 0);
+ nsCOMPtr<nsIAsyncShutdownClient> barrier = media::GetShutdownBarrier();
+ // xpcom should still be available because we blocked shutdown by having a
+ // blocker. Until it completely shuts down we should still be able to get
+ // the barrier.
+ MOZ_RELEASE_ASSERT(
+ barrier,
+ "Failed to get shutdown barrier, cannot remove shutdown blocker!");
+ barrier->RemoveBlocker(this);
+ // Clear our singleton reference. This will probably delete
+ // this instance, so don't deref |this| clearing sInstance.
+ sInitPhase = XPCOMShutdownEnded;
+ sInstance = nullptr;
+ DECODER_LOG(LogLevel::Debug, ("MediaShutdownManager::BlockShutdown() end."));
+}
+
+nsresult MediaShutdownManager::Register(MediaDecoder* aDecoder) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (sInitPhase == InitFailed) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ if (sInitPhase == XPCOMShutdownStarted) {
+ return NS_ERROR_ABORT;
+ }
+ // Don't call Register() after you've Unregistered() all the decoders,
+ // that's not going to work.
+ MOZ_ASSERT(!mDecoders.Contains(aDecoder));
+ mDecoders.Insert(aDecoder);
+ MOZ_ASSERT(mDecoders.Contains(aDecoder));
+ MOZ_ASSERT(mDecoders.Count() > 0);
+ return NS_OK;
+}
+
+void MediaShutdownManager::Unregister(MediaDecoder* aDecoder) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!mDecoders.EnsureRemoved(aDecoder)) {
+ return;
+ }
+ if (sInitPhase == XPCOMShutdownStarted && mDecoders.Count() == 0) {
+ RemoveBlocker();
+ }
+}
+
+NS_IMETHODIMP
+MediaShutdownManager::GetName(nsAString& aName) {
+ aName = u"MediaShutdownManager: shutdown"_ns;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MediaShutdownManager::GetState(nsIPropertyBag** aBagOut) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aBagOut);
+
+ nsCOMPtr<nsIWritablePropertyBag2> propertyBag =
+ do_CreateInstance("@mozilla.org/hash-property-bag;1");
+
+ if (NS_WARN_IF(!propertyBag)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ nsresult rv = propertyBag->SetPropertyAsInt32(
+ u"sInitPhase"_ns, static_cast<int32_t>(sInitPhase));
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsAutoCString decoderInfo;
+ for (const auto& key : mDecoders) {
+ // Grab the full extended type for the decoder. This can be used to help
+ // indicate problems with specific decoders by associating type -> decoder.
+ decoderInfo.Append(key->ContainerType().ExtendedType().OriginalString());
+ decoderInfo.Append(", ");
+ }
+
+ rv = propertyBag->SetPropertyAsACString(u"decoderInfo"_ns, decoderInfo);
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ propertyBag.forget(aBagOut);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MediaShutdownManager::BlockShutdown(nsIAsyncShutdownClient*) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(sInitPhase == InitSucceeded);
+ MOZ_DIAGNOSTIC_ASSERT(sInstance);
+
+ DECODER_LOG(LogLevel::Debug,
+ ("MediaShutdownManager::BlockShutdown() start..."));
+
+ // Set this flag to ensure no Register() is allowed when Shutdown() begins.
+ sInitPhase = XPCOMShutdownStarted;
+
+ auto oldCount = mDecoders.Count();
+ if (oldCount == 0) {
+ RemoveBlocker();
+ return NS_OK;
+ }
+
+ // Iterate over the decoders and shut them down.
+ for (const auto& key : mDecoders) {
+ key->NotifyXPCOMShutdown();
+ // Check MediaDecoder::Shutdown doesn't call Unregister() synchronously in
+ // order not to corrupt our hashtable traversal.
+ MOZ_ASSERT(mDecoders.Count() == oldCount);
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla
+
+// avoid redefined macro in unified build
+#undef LOGW
diff --git a/dom/media/MediaShutdownManager.h b/dom/media/MediaShutdownManager.h
new file mode 100644
index 0000000000..f393f5667d
--- /dev/null
+++ b/dom/media/MediaShutdownManager.h
@@ -0,0 +1,96 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(MediaShutdownManager_h_)
+# define MediaShutdownManager_h_
+
+# include "mozilla/Monitor.h"
+# include "mozilla/RefPtr.h"
+# include "mozilla/StaticPtr.h"
+# include "nsCOMPtr.h"
+# include "nsIAsyncShutdown.h"
+# include "nsIThread.h"
+# include "nsTHashSet.h"
+
+namespace mozilla {
+
+class MediaDecoder;
+
+// The MediaShutdownManager manages shutting down the MediaDecoder
+// infrastructure in response to an xpcom-shutdown notification.
+// This happens when Gecko is shutting down in the middle of operation.
+// This is tricky, as there are a number of moving parts that must
+// be shutdown in a particular order. The MediaShutdownManager
+// encapsulates all these dependencies to ensure that no shutdown
+// order dependencies leak out of the MediaDecoder stack.
+// The MediaShutdownManager is a singleton.
+//
+// The MediaShutdownManager ensures that the MediaDecoder stack
+// is shutdown before exiting xpcom-shutdown stage by registering
+// itself with nsIAsyncShutdownService to receive notification
+// when the stage of shutdown has started and then calls Shutdown()
+// on every MediaDecoder. Shutdown will not proceed until all
+// MediaDecoders finish shutdown and MediaShutdownManager unregisters
+// itself from the async shutdown service.
+//
+// Note that calling the Unregister() functions may result in the singleton
+// being deleted, so don't store references to the singleton, always use the
+// singleton by derefing the referenced returned by
+// MediaShutdownManager::Instance(), which ensures that the singleton is
+// created when needed.
+// i.e. like this:
+// MediaShutdownManager::Instance()::Unregister(someDecoder);
+// MediaShutdownManager::Instance()::Register(someOtherDecoder);
+// Not like this:
+// MediaShutdownManager& instance = MediaShutdownManager::Instance();
+// instance.Unregister(someDecoder); // Warning! May delete instance!
+// instance.Register(someOtherDecoder); // BAD! instance may be dangling!
+class MediaShutdownManager : public nsIAsyncShutdownBlocker {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIASYNCSHUTDOWNBLOCKER
+
+ static void InitStatics();
+
+ // The MediaShutdownManager is a singleton, access its instance with
+ // this accessor.
+ static MediaShutdownManager& Instance();
+
+ // Notifies the MediaShutdownManager that it needs to track the shutdown
+ // of this MediaDecoder.
+ nsresult Register(MediaDecoder* aDecoder);
+
+ // Notifies the MediaShutdownManager that a MediaDecoder that it was
+ // tracking has shutdown, and it no longer needs to be shutdown in the
+ // xpcom-shutdown listener.
+ void Unregister(MediaDecoder* aDecoder);
+
+ private:
+ enum InitPhase {
+ NotInited,
+ InitSucceeded,
+ InitFailed,
+ XPCOMShutdownStarted,
+ XPCOMShutdownEnded
+ };
+
+ static InitPhase sInitPhase;
+
+ MediaShutdownManager();
+ virtual ~MediaShutdownManager();
+ void RemoveBlocker();
+
+ static StaticRefPtr<MediaShutdownManager> sInstance;
+
+ // References to the MediaDecoder. The decoders unregister themselves
+ // in their Shutdown() method, so we'll drop the reference naturally when
+ // we're shutting down (in the non xpcom-shutdown case).
+ nsTHashSet<RefPtr<MediaDecoder>> mDecoders;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/MediaSpan.h b/dom/media/MediaSpan.h
new file mode 100644
index 0000000000..4bb512a7dc
--- /dev/null
+++ b/dom/media/MediaSpan.h
@@ -0,0 +1,131 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#if !defined(MediaSpan_h)
+# define MediaSpan_h
+
+# include "MediaData.h"
+# include "mozilla/RefPtr.h"
+# include "mozilla/Span.h"
+
+namespace mozilla {
+
+// A MediaSpan wraps a MediaByteBuffer and exposes a slice/span, or subregion,
+// of the buffer. This allows you to slice off a logical subregion without
+// needing to reallocate a new buffer to hold it. You can also append to a
+// MediaSpan without affecting other MediaSpans referencing the same buffer
+// (the MediaSpan receiving the append will allocate a new buffer to store its
+// result if necessary, to ensure other MediaSpans referencing its original
+// buffer are unaffected). Note there are no protections here that something
+// other than MediaSpans doesn't modify the underlying MediaByteBuffer while
+// a MediaSpan is alive.
+class MediaSpan {
+ public:
+ ~MediaSpan() = default;
+
+ explicit MediaSpan(const MediaSpan& aOther) = default;
+
+ MediaSpan(MediaSpan&& aOther) = default;
+
+ explicit MediaSpan(const RefPtr<MediaByteBuffer>& aBuffer)
+ : mBuffer(aBuffer), mStart(0), mLength(aBuffer ? aBuffer->Length() : 0) {
+ MOZ_DIAGNOSTIC_ASSERT(mBuffer);
+ }
+
+ explicit MediaSpan(MediaByteBuffer* aBuffer)
+ : mBuffer(aBuffer), mStart(0), mLength(aBuffer ? aBuffer->Length() : 0) {
+ MOZ_DIAGNOSTIC_ASSERT(mBuffer);
+ }
+
+ MediaSpan& operator=(const MediaSpan& aOther) = default;
+
+ static MediaSpan WithCopyOf(const RefPtr<MediaByteBuffer>& aBuffer) {
+ RefPtr<MediaByteBuffer> buffer = new MediaByteBuffer(aBuffer->Length());
+ buffer->AppendElements(*aBuffer);
+ return MediaSpan(buffer);
+ }
+
+ bool IsEmpty() const { return Length() == 0; }
+
+ // Note: It's unsafe to store the pointer returned by this function, as an
+ // append operation could cause the wrapped MediaByteBuffer to be
+ // reallocated, invalidating pointers previously returned by this function.
+ const uint8_t* Elements() const {
+ MOZ_DIAGNOSTIC_ASSERT(mStart < mBuffer->Length());
+ return mBuffer->Elements() + mStart;
+ }
+
+ size_t Length() const { return mLength; }
+
+ uint8_t operator[](size_t aIndex) const {
+ MOZ_DIAGNOSTIC_ASSERT(aIndex < Length());
+ return (*mBuffer)[mStart + aIndex];
+ }
+
+ bool Append(const MediaSpan& aBuffer) { return Append(aBuffer.Buffer()); }
+
+ bool Append(MediaByteBuffer* aBuffer) {
+ if (!aBuffer) {
+ return true;
+ }
+ if (mStart + mLength < mBuffer->Length()) {
+ // This MediaSpan finishes before the end of its buffer. The buffer
+ // could be shared with another MediaSpan. So we can't just append to
+ // the underlying buffer without risking damaging other MediaSpans' data.
+ // So we must reallocate a new buffer, copy our old data into it, and
+ // append the new data into it.
+ RefPtr<MediaByteBuffer> buffer =
+ new MediaByteBuffer(mLength + aBuffer->Length());
+ if (!buffer->AppendElements(Elements(), Length(), fallible) ||
+ !buffer->AppendElements(*aBuffer, fallible)) {
+ return false;
+ }
+ mBuffer = buffer;
+ mLength += aBuffer->Length();
+ return true;
+ }
+ if (!mBuffer->AppendElements(*aBuffer, fallible)) {
+ return false;
+ }
+ mLength += aBuffer->Length();
+ return true;
+ }
+
+ // Returns a new MediaSpan, spanning from the start of this span,
+ // up until aEnd.
+ MediaSpan To(size_t aEnd) const {
+ MOZ_DIAGNOSTIC_ASSERT(aEnd <= Length());
+ return MediaSpan(mBuffer, mStart, aEnd);
+ }
+
+ // Returns a new MediaSpan, spanning from aStart bytes offset from
+ // the start of this span, until the end of this span.
+ MediaSpan From(size_t aStart) const {
+ MOZ_DIAGNOSTIC_ASSERT(aStart <= Length());
+ return MediaSpan(mBuffer, mStart + aStart, Length() - aStart);
+ }
+
+ void RemoveFront(size_t aNumBytes) {
+ MOZ_DIAGNOSTIC_ASSERT(aNumBytes <= Length());
+ mStart += aNumBytes;
+ mLength -= aNumBytes;
+ }
+
+ MediaByteBuffer* Buffer() const { return mBuffer; }
+
+ private:
+ MediaSpan(MediaByteBuffer* aBuffer, size_t aStart, size_t aLength)
+ : mBuffer(aBuffer), mStart(aStart), mLength(aLength) {
+ MOZ_DIAGNOSTIC_ASSERT(mStart + mLength <= mBuffer->Length());
+ }
+
+ RefPtr<MediaByteBuffer> mBuffer;
+ size_t mStart = 0;
+ size_t mLength = 0;
+};
+
+} // namespace mozilla
+
+#endif // MediaSpan_h
diff --git a/dom/media/MediaStatistics.h b/dom/media/MediaStatistics.h
new file mode 100644
index 0000000000..08b516b25a
--- /dev/null
+++ b/dom/media/MediaStatistics.h
@@ -0,0 +1,78 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MediaStatistics_h_
+#define MediaStatistics_h_
+
+namespace mozilla {
+
+struct MediaStatistics {
+ // Estimate of the current playback rate (bytes/second).
+ double mPlaybackRate;
+ // Estimate of the current download rate (bytes/second). This
+ // ignores time that the channel was paused by Gecko.
+ double mDownloadRate;
+ // Total length of media stream in bytes; -1 if not known
+ int64_t mTotalBytes;
+ // Current position of the download, in bytes. This is the offset of
+ // the first uncached byte after the decoder position.
+ int64_t mDownloadPosition;
+ // Current position of playback, in bytes
+ int64_t mPlaybackPosition;
+ // If false, then mDownloadRate cannot be considered a reliable
+ // estimate (probably because the download has only been running
+ // a short time).
+ bool mDownloadRateReliable;
+ // If false, then mPlaybackRate cannot be considered a reliable
+ // estimate (probably because playback has only been running
+ // a short time).
+ bool mPlaybackRateReliable;
+
+ bool CanPlayThrough() {
+ // Number of estimated seconds worth of data we need to have buffered
+ // ahead of the current playback position before we allow the media decoder
+ // to report that it can play through the entire media without the decode
+ // catching up with the download. Having this margin make the
+ // CanPlayThrough() calculation more stable in the case of
+ // fluctuating bitrates.
+ static const int64_t CAN_PLAY_THROUGH_MARGIN = 1;
+
+ if ((mTotalBytes < 0 && mDownloadRateReliable) ||
+ (mTotalBytes >= 0 && mTotalBytes == mDownloadPosition)) {
+ return true;
+ }
+
+ if (!mDownloadRateReliable || !mPlaybackRateReliable) {
+ return false;
+ }
+
+ int64_t bytesToDownload = mTotalBytes - mDownloadPosition;
+ int64_t bytesToPlayback = mTotalBytes - mPlaybackPosition;
+ double timeToDownload = bytesToDownload / mDownloadRate;
+ double timeToPlay = bytesToPlayback / mPlaybackRate;
+
+ if (timeToDownload > timeToPlay) {
+ // Estimated time to download is greater than the estimated time to play.
+ // We probably can't play through without having to stop to buffer.
+ return false;
+ }
+
+ // Estimated time to download is less than the estimated time to play.
+ // We can probably play through without having to buffer, but ensure that
+ // we've got a reasonable amount of data buffered after the current
+ // playback position, so that if the bitrate of the media fluctuates, or if
+ // our download rate or decode rate estimation is otherwise inaccurate,
+ // we don't suddenly discover that we need to buffer. This is particularly
+ // required near the start of the media, when not much data is downloaded.
+ int64_t readAheadMargin =
+ static_cast<int64_t>(mPlaybackRate * CAN_PLAY_THROUGH_MARGIN);
+ return mDownloadPosition > mPlaybackPosition + readAheadMargin;
+ }
+};
+
+} // namespace mozilla
+
+#endif // MediaStatistics_h_
diff --git a/dom/media/MediaStreamError.cpp b/dom/media/MediaStreamError.cpp
new file mode 100644
index 0000000000..27f1201261
--- /dev/null
+++ b/dom/media/MediaStreamError.cpp
@@ -0,0 +1,112 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaStreamError.h"
+#include "mozilla/dom/MediaStreamErrorBinding.h"
+#include "mozilla/dom/Promise.h"
+#include "nsContentUtils.h"
+
+namespace mozilla {
+
+BaseMediaMgrError::BaseMediaMgrError(Name aName, const nsACString& aMessage,
+ const nsAString& aConstraint)
+ : mMessage(aMessage), mConstraint(aConstraint), mName(aName) {
+#define MAP_MEDIAERR(name, msg) \
+ { Name::name, #name, msg }
+
+ static struct {
+ Name mName;
+ const char* mNameString;
+ const char* mMessage;
+ } map[] = {
+ MAP_MEDIAERR(AbortError, "The operation was aborted."),
+ MAP_MEDIAERR(InvalidStateError, "The object is in an invalid state."),
+ MAP_MEDIAERR(NotAllowedError,
+ "The request is not allowed by the user agent "
+ "or the platform in the current context."),
+ MAP_MEDIAERR(NotFoundError, "The object can not be found here."),
+ MAP_MEDIAERR(NotReadableError, "The I/O read operation failed."),
+ MAP_MEDIAERR(OverconstrainedError, "Constraints could be not satisfied."),
+ MAP_MEDIAERR(SecurityError, "The operation is insecure."),
+ MAP_MEDIAERR(TypeError, ""),
+ };
+ for (auto& entry : map) {
+ if (entry.mName == mName) {
+ mNameString.AssignASCII(entry.mNameString);
+ if (mMessage.IsEmpty()) {
+ mMessage.AssignASCII(entry.mMessage);
+ }
+ return;
+ }
+ }
+ MOZ_ASSERT_UNREACHABLE("Unknown error type");
+}
+
+NS_IMPL_ISUPPORTS0(MediaMgrError)
+
+void MediaMgrError::Reject(dom::Promise* aPromise) const {
+ switch (mName) {
+ case Name::AbortError:
+ aPromise->MaybeRejectWithAbortError(mMessage);
+ return;
+ case Name::InvalidStateError:
+ aPromise->MaybeRejectWithInvalidStateError(mMessage);
+ return;
+ case Name::NotAllowedError:
+ aPromise->MaybeRejectWithNotAllowedError(mMessage);
+ return;
+ case Name::NotFoundError:
+ aPromise->MaybeRejectWithNotFoundError(mMessage);
+ return;
+ case Name::NotReadableError:
+ aPromise->MaybeRejectWithNotReadableError(mMessage);
+ return;
+ case Name::OverconstrainedError: {
+ // TODO: Add OverconstrainedError type.
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1453013
+ nsCOMPtr<nsPIDOMWindowInner> window =
+ do_QueryInterface(aPromise->GetGlobalObject());
+ aPromise->MaybeReject(MakeRefPtr<dom::MediaStreamError>(window, *this));
+ return;
+ }
+ case Name::SecurityError:
+ aPromise->MaybeRejectWithSecurityError(mMessage);
+ return;
+ case Name::TypeError:
+ aPromise->MaybeRejectWithTypeError(mMessage);
+ return;
+ // -Wswitch ensures all cases are covered so don't add default:.
+ }
+}
+
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(MediaStreamError, mParent)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaStreamError)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaStreamError)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaStreamError)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+ NS_INTERFACE_MAP_ENTRY(MediaStreamError)
+NS_INTERFACE_MAP_END
+
+JSObject* MediaStreamError::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return MediaStreamError_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void MediaStreamError::GetName(nsAString& aName) const { aName = mNameString; }
+
+void MediaStreamError::GetMessage(nsAString& aMessage) const {
+ CopyUTF8toUTF16(mMessage, aMessage);
+}
+
+void MediaStreamError::GetConstraint(nsAString& aConstraint) const {
+ aConstraint = mConstraint;
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/media/MediaStreamError.h b/dom/media/MediaStreamError.h
new file mode 100644
index 0000000000..3e84cf6790
--- /dev/null
+++ b/dom/media/MediaStreamError.h
@@ -0,0 +1,113 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_MediaStreamError_h
+#define mozilla_dom_MediaStreamError_h
+
+#include "mozilla/Attributes.h"
+#include "nsWrapperCache.h"
+#include "js/TypeDecls.h"
+#include "nsPIDOMWindow.h"
+#include "mozilla/RefPtr.h"
+
+#if defined(XP_WIN) && defined(GetMessage)
+# undef GetMessage
+#endif
+
+namespace mozilla {
+
+namespace dom {
+
+#define MOZILLA_DOM_MEDIASTREAMERROR_IMPLEMENTATION_IID \
+ { \
+ 0x95fa29aa, 0x0cc2, 0x4698, { \
+ 0x9d, 0xa9, 0xf2, 0xeb, 0x03, 0x91, 0x0b, 0xd1 \
+ } \
+ }
+
+class MediaStreamError;
+} // namespace dom
+
+class BaseMediaMgrError {
+ friend class dom::MediaStreamError;
+
+ public:
+ enum class Name {
+ AbortError,
+ InvalidStateError,
+ NotAllowedError,
+ NotFoundError,
+ NotReadableError,
+ OverconstrainedError,
+ SecurityError,
+ TypeError,
+ };
+
+ protected:
+ BaseMediaMgrError(Name aName, const nsACString& aMessage,
+ const nsAString& aConstraint);
+
+ public:
+ nsString mNameString;
+ nsCString mMessage;
+ const nsString mConstraint;
+ const Name mName;
+};
+
+class MediaMgrError final : public nsISupports, public BaseMediaMgrError {
+ public:
+ // aMessage should be valid UTF-8, but invalid UTF-8 byte sequences are
+ // replaced with the REPLACEMENT CHARACTER on conversion to UTF-16.
+ explicit MediaMgrError(Name aName, const nsACString& aMessage = ""_ns,
+ const nsAString& aConstraint = u""_ns)
+ : BaseMediaMgrError(aName, aMessage, aConstraint) {}
+ template <int N>
+ explicit MediaMgrError(Name aName, const char (&aMessage)[N],
+ const nsAString& aConstraint = u""_ns)
+ : BaseMediaMgrError(aName, nsLiteralCString(aMessage), aConstraint) {}
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ void Reject(dom::Promise* aPromise) const;
+
+ private:
+ ~MediaMgrError() = default;
+};
+
+namespace dom {
+class MediaStreamError final : public nsISupports,
+ public BaseMediaMgrError,
+ public nsWrapperCache {
+ public:
+ MediaStreamError(nsPIDOMWindowInner* aParent, const BaseMediaMgrError& aOther)
+ : BaseMediaMgrError(aOther.mName, aOther.mMessage, aOther.mConstraint),
+ mParent(aParent) {}
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(MediaStreamError)
+ NS_DECLARE_STATIC_IID_ACCESSOR(
+ MOZILLA_DOM_MEDIASTREAMERROR_IMPLEMENTATION_IID)
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ nsPIDOMWindowInner* GetParentObject() const { return mParent; }
+ void GetName(nsAString& aName) const;
+ void GetMessage(nsAString& aMessage) const;
+ void GetConstraint(nsAString& aConstraint) const;
+
+ private:
+ virtual ~MediaStreamError() = default;
+
+ RefPtr<nsPIDOMWindowInner> mParent;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(MediaStreamError,
+ MOZILLA_DOM_MEDIASTREAMERROR_IMPLEMENTATION_IID)
+} // namespace dom
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/MediaStreamTrack.cpp b/dom/media/MediaStreamTrack.cpp
new file mode 100644
index 0000000000..58b44cb308
--- /dev/null
+++ b/dom/media/MediaStreamTrack.cpp
@@ -0,0 +1,638 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaStreamTrack.h"
+
+#include "DOMMediaStream.h"
+#include "MediaSegment.h"
+#include "MediaStreamError.h"
+#include "MediaTrackGraphImpl.h"
+#include "MediaTrackListener.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/dom/Promise.h"
+#include "nsContentUtils.h"
+#include "nsGlobalWindowInner.h"
+#include "nsIUUIDGenerator.h"
+#include "nsServiceManagerUtils.h"
+#include "systemservices/MediaUtils.h"
+
+#ifdef LOG
+# undef LOG
+#endif
+
+static mozilla::LazyLogModule gMediaStreamTrackLog("MediaStreamTrack");
+#define LOG(type, msg) MOZ_LOG(gMediaStreamTrackLog, type, msg)
+
+using namespace mozilla::media;
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaStreamTrackSource)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaStreamTrackSource)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaStreamTrackSource)
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(MediaStreamTrackSource)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(MediaStreamTrackSource)
+ tmp->Destroy();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mPrincipal)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(MediaStreamTrackSource)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPrincipal)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+auto MediaStreamTrackSource::ApplyConstraints(
+ const dom::MediaTrackConstraints& aConstraints, CallerType aCallerType)
+ -> RefPtr<ApplyConstraintsPromise> {
+ return ApplyConstraintsPromise::CreateAndReject(
+ MakeRefPtr<MediaMgrError>(MediaMgrError::Name::OverconstrainedError, ""),
+ __func__);
+}
+
+/**
+ * MTGListener monitors state changes of the media flowing through the
+ * MediaTrackGraph.
+ *
+ *
+ * For changes to PrincipalHandle the following applies:
+ *
+ * When the main thread principal for a MediaStreamTrack changes, its principal
+ * will be set to the combination of the previous principal and the new one.
+ *
+ * As a PrincipalHandle change later happens on the MediaTrackGraph thread, we
+ * will be notified. If the latest principal on main thread matches the
+ * PrincipalHandle we just saw on MTG thread, we will set the track's principal
+ * to the new one.
+ *
+ * We know at this point that the old principal has been flushed out and data
+ * under it cannot leak to consumers.
+ *
+ * In case of multiple changes to the main thread state, the track's principal
+ * will be a combination of its old principal and all the new ones until the
+ * latest main thread principal matches the PrincipalHandle on the MTG thread.
+ */
+class MediaStreamTrack::MTGListener : public MediaTrackListener {
+ public:
+ explicit MTGListener(MediaStreamTrack* aTrack) : mTrack(aTrack) {}
+
+ void DoNotifyPrincipalHandleChanged(
+ const PrincipalHandle& aNewPrincipalHandle) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mTrack) {
+ return;
+ }
+
+ mTrack->NotifyPrincipalHandleChanged(aNewPrincipalHandle);
+ }
+
+ void NotifyPrincipalHandleChanged(
+ MediaTrackGraph* aGraph,
+ const PrincipalHandle& aNewPrincipalHandle) override {
+ aGraph->DispatchToMainThreadStableState(
+ NewRunnableMethod<StoreCopyPassByConstLRef<PrincipalHandle>>(
+ "dom::MediaStreamTrack::MTGListener::"
+ "DoNotifyPrincipalHandleChanged",
+ this, &MTGListener::DoNotifyPrincipalHandleChanged,
+ aNewPrincipalHandle));
+ }
+
+ void NotifyRemoved(MediaTrackGraph* aGraph) override {
+ // `mTrack` is a WeakPtr and must be destroyed on main thread.
+ // We dispatch ourselves to main thread here in case the MediaTrackGraph
+ // is holding the last reference to us.
+ aGraph->DispatchToMainThreadStableState(
+ NS_NewRunnableFunction("MediaStreamTrack::MTGListener::mTrackReleaser",
+ [self = RefPtr<MTGListener>(this)]() {}));
+ }
+
+ void DoNotifyEnded() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mTrack) {
+ return;
+ }
+
+ if (!mTrack->GetParentObject()) {
+ return;
+ }
+
+ AbstractThread* mainThread =
+ nsGlobalWindowInner::Cast(mTrack->GetParentObject())
+ ->AbstractMainThreadFor(TaskCategory::Other);
+ mainThread->Dispatch(NewRunnableMethod("MediaStreamTrack::OverrideEnded",
+ mTrack.get(),
+ &MediaStreamTrack::OverrideEnded));
+ }
+
+ void NotifyEnded(MediaTrackGraph* aGraph) override {
+ aGraph->DispatchToMainThreadStableState(
+ NewRunnableMethod("MediaStreamTrack::MTGListener::DoNotifyEnded", this,
+ &MTGListener::DoNotifyEnded));
+ }
+
+ protected:
+ // Main thread only.
+ WeakPtr<MediaStreamTrack> mTrack;
+};
+
+class MediaStreamTrack::TrackSink : public MediaStreamTrackSource::Sink {
+ public:
+ explicit TrackSink(MediaStreamTrack* aTrack) : mTrack(aTrack) {}
+
+ /**
+ * Keep the track source alive. This track and any clones are controlling the
+ * lifetime of the source by being registered as its sinks.
+ */
+ bool KeepsSourceAlive() const override { return true; }
+
+ bool Enabled() const override {
+ if (!mTrack) {
+ return false;
+ }
+ return mTrack->Enabled();
+ }
+
+ void PrincipalChanged() override {
+ if (mTrack) {
+ mTrack->PrincipalChanged();
+ }
+ }
+
+ void MutedChanged(bool aNewState) override {
+ if (mTrack) {
+ mTrack->MutedChanged(aNewState);
+ }
+ }
+
+ void OverrideEnded() override {
+ if (mTrack) {
+ mTrack->OverrideEnded();
+ }
+ }
+
+ private:
+ WeakPtr<MediaStreamTrack> mTrack;
+};
+
+MediaStreamTrack::MediaStreamTrack(nsPIDOMWindowInner* aWindow,
+ mozilla::MediaTrack* aInputTrack,
+ MediaStreamTrackSource* aSource,
+ MediaStreamTrackState aReadyState,
+ bool aMuted,
+ const MediaTrackConstraints& aConstraints)
+ : mWindow(aWindow),
+ mInputTrack(aInputTrack),
+ mSource(aSource),
+ mSink(MakeUnique<TrackSink>(this)),
+ mPrincipal(aSource->GetPrincipal()),
+ mReadyState(aReadyState),
+ mEnabled(true),
+ mMuted(aMuted),
+ mConstraints(aConstraints) {
+ if (!Ended()) {
+ GetSource().RegisterSink(mSink.get());
+
+ // Even if the input track is destroyed we need mTrack so that methods
+ // like AddListener still work. Keeping the number of paths to a minimum
+ // also helps prevent bugs elsewhere. We'll be ended through the
+ // MediaStreamTrackSource soon enough.
+ auto graph = mInputTrack->IsDestroyed()
+ ? MediaTrackGraph::GetInstanceIfExists(
+ mWindow, mInputTrack->mSampleRate,
+ MediaTrackGraph::DEFAULT_OUTPUT_DEVICE)
+ : mInputTrack->Graph();
+ MOZ_DIAGNOSTIC_ASSERT(graph,
+ "A destroyed input track is only expected when "
+ "cloning, but since we're live there must be another "
+ "live track that is keeping the graph alive");
+
+ mTrack = graph->CreateForwardedInputTrack(mInputTrack->mType);
+ mPort = mTrack->AllocateInputPort(mInputTrack);
+ mMTGListener = new MTGListener(this);
+ AddListener(mMTGListener);
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIUUIDGenerator> uuidgen =
+ do_GetService("@mozilla.org/uuid-generator;1", &rv);
+
+ nsID uuid;
+ memset(&uuid, 0, sizeof(uuid));
+ if (uuidgen) {
+ uuidgen->GenerateUUIDInPlace(&uuid);
+ }
+
+ char chars[NSID_LENGTH];
+ uuid.ToProvidedString(chars);
+ mID = NS_ConvertASCIItoUTF16(chars);
+}
+
+MediaStreamTrack::~MediaStreamTrack() { Destroy(); }
+
+void MediaStreamTrack::Destroy() {
+ SetReadyState(MediaStreamTrackState::Ended);
+ // Remove all listeners -- avoid iterating over the list we're removing from
+ for (const auto& listener : mTrackListeners.Clone()) {
+ RemoveListener(listener);
+ }
+ // Do the same as above for direct listeners
+ for (const auto& listener : mDirectTrackListeners.Clone()) {
+ RemoveDirectListener(listener);
+ }
+}
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(MediaStreamTrack)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(MediaStreamTrack,
+ DOMEventTargetHelper)
+ tmp->Destroy();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mSource)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mPrincipal)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingPrincipal)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_PTR
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(MediaStreamTrack,
+ DOMEventTargetHelper)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSource)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPrincipal)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingPrincipal)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_ADDREF_INHERITED(MediaStreamTrack, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(MediaStreamTrack, DOMEventTargetHelper)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaStreamTrack)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+JSObject* MediaStreamTrack::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return MediaStreamTrack_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void MediaStreamTrack::GetId(nsAString& aID) const { aID = mID; }
+
+void MediaStreamTrack::SetEnabled(bool aEnabled) {
+ LOG(LogLevel::Info,
+ ("MediaStreamTrack %p %s", this, aEnabled ? "Enabled" : "Disabled"));
+
+ if (mEnabled == aEnabled) {
+ return;
+ }
+
+ mEnabled = aEnabled;
+
+ if (Ended()) {
+ return;
+ }
+
+ mTrack->SetDisabledTrackMode(mEnabled ? DisabledTrackMode::ENABLED
+ : DisabledTrackMode::SILENCE_BLACK);
+ NotifyEnabledChanged();
+}
+
+void MediaStreamTrack::Stop() {
+ LOG(LogLevel::Info, ("MediaStreamTrack %p Stop()", this));
+
+ if (Ended()) {
+ LOG(LogLevel::Warning, ("MediaStreamTrack %p Already ended", this));
+ return;
+ }
+
+ SetReadyState(MediaStreamTrackState::Ended);
+
+ NotifyEnded();
+}
+
+void MediaStreamTrack::GetConstraints(dom::MediaTrackConstraints& aResult) {
+ aResult = mConstraints;
+}
+
+void MediaStreamTrack::GetSettings(dom::MediaTrackSettings& aResult,
+ CallerType aCallerType) {
+ GetSource().GetSettings(aResult);
+
+ // Spoof values when privacy.resistFingerprinting is true.
+ nsIGlobalObject* global = mWindow ? mWindow->AsGlobal() : nullptr;
+ if (!nsContentUtils::ShouldResistFingerprinting(
+ aCallerType, global, RFPTarget::StreamVideoFacingMode)) {
+ return;
+ }
+ if (aResult.mFacingMode.WasPassed()) {
+ aResult.mFacingMode.Value().AssignASCII(
+ VideoFacingModeEnumValues::GetString(VideoFacingModeEnum::User));
+ }
+}
+
+already_AddRefed<Promise> MediaStreamTrack::ApplyConstraints(
+ const MediaTrackConstraints& aConstraints, CallerType aCallerType,
+ ErrorResult& aRv) {
+ if (MOZ_LOG_TEST(gMediaStreamTrackLog, LogLevel::Info)) {
+ nsString str;
+ aConstraints.ToJSON(str);
+
+ LOG(LogLevel::Info, ("MediaStreamTrack %p ApplyConstraints() with "
+ "constraints %s",
+ this, NS_ConvertUTF16toUTF8(str).get()));
+ }
+
+ nsIGlobalObject* go = mWindow ? mWindow->AsGlobal() : nullptr;
+
+ RefPtr<Promise> promise = Promise::Create(go, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ // Forward constraints to the source.
+ //
+ // After GetSource().ApplyConstraints succeeds (after it's been to
+ // media-thread and back), and no sooner, do we set mConstraints to the newly
+ // applied values.
+
+ // Keep a reference to this, to make sure it's still here when we get back.
+ RefPtr<MediaStreamTrack> self(this);
+ GetSource()
+ .ApplyConstraints(aConstraints, aCallerType)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [this, self, promise, aConstraints](bool aDummy) {
+ if (!mWindow || !mWindow->IsCurrentInnerWindow()) {
+ return; // Leave Promise pending after navigation by design.
+ }
+ mConstraints = aConstraints;
+ promise->MaybeResolve(false);
+ },
+ [this, self, promise](const RefPtr<MediaMgrError>& aError) {
+ if (!mWindow || !mWindow->IsCurrentInnerWindow()) {
+ return; // Leave Promise pending after navigation by design.
+ }
+ promise->MaybeReject(
+ MakeRefPtr<MediaStreamError>(mWindow, *aError));
+ });
+ return promise.forget();
+}
+
+ProcessedMediaTrack* MediaStreamTrack::GetTrack() const {
+ MOZ_DIAGNOSTIC_ASSERT(!Ended());
+ return mTrack;
+}
+
+MediaTrackGraph* MediaStreamTrack::Graph() const {
+ MOZ_DIAGNOSTIC_ASSERT(!Ended());
+ return mTrack->Graph();
+}
+
+MediaTrackGraphImpl* MediaStreamTrack::GraphImpl() const {
+ MOZ_DIAGNOSTIC_ASSERT(!Ended());
+ return mTrack->GraphImpl();
+}
+
+void MediaStreamTrack::SetPrincipal(nsIPrincipal* aPrincipal) {
+ if (aPrincipal == mPrincipal) {
+ return;
+ }
+ mPrincipal = aPrincipal;
+
+ LOG(LogLevel::Info,
+ ("MediaStreamTrack %p principal changed to %p. Now: "
+ "null=%d, codebase=%d, expanded=%d, system=%d",
+ this, mPrincipal.get(), mPrincipal->GetIsNullPrincipal(),
+ mPrincipal->GetIsContentPrincipal(),
+ mPrincipal->GetIsExpandedPrincipal(), mPrincipal->IsSystemPrincipal()));
+ for (PrincipalChangeObserver<MediaStreamTrack>* observer :
+ mPrincipalChangeObservers) {
+ observer->PrincipalChanged(this);
+ }
+}
+
+void MediaStreamTrack::PrincipalChanged() {
+ mPendingPrincipal = GetSource().GetPrincipal();
+ nsCOMPtr<nsIPrincipal> newPrincipal = mPrincipal;
+ LOG(LogLevel::Info, ("MediaStreamTrack %p Principal changed on main thread "
+ "to %p (pending). Combining with existing principal %p.",
+ this, mPendingPrincipal.get(), mPrincipal.get()));
+ if (nsContentUtils::CombineResourcePrincipals(&newPrincipal,
+ mPendingPrincipal)) {
+ SetPrincipal(newPrincipal);
+ }
+}
+
+void MediaStreamTrack::NotifyPrincipalHandleChanged(
+ const PrincipalHandle& aNewPrincipalHandle) {
+ PrincipalHandle handle(aNewPrincipalHandle);
+ LOG(LogLevel::Info, ("MediaStreamTrack %p principalHandle changed on "
+ "MediaTrackGraph thread to %p. Current principal: %p, "
+ "pending: %p",
+ this, GetPrincipalFromHandle(handle), mPrincipal.get(),
+ mPendingPrincipal.get()));
+ if (PrincipalHandleMatches(handle, mPendingPrincipal)) {
+ SetPrincipal(mPendingPrincipal);
+ mPendingPrincipal = nullptr;
+ }
+}
+
+void MediaStreamTrack::MutedChanged(bool aNewState) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ /**
+ * 4.3.1 Life-cycle and Media flow - Media flow
+ * To set a track's muted state to newState, the User Agent MUST run the
+ * following steps:
+ * 1. Let track be the MediaStreamTrack in question.
+ * 2. Set track's muted attribute to newState.
+ * 3. If newState is true let eventName be mute, otherwise unmute.
+ * 4. Fire a simple event named eventName on track.
+ */
+
+ if (mMuted == aNewState) {
+ return;
+ }
+
+ LOG(LogLevel::Info,
+ ("MediaStreamTrack %p became %s", this, aNewState ? "muted" : "unmuted"));
+
+ mMuted = aNewState;
+
+ if (Ended()) {
+ return;
+ }
+
+ nsString eventName = aNewState ? u"mute"_ns : u"unmute"_ns;
+ DispatchTrustedEvent(eventName);
+}
+
+void MediaStreamTrack::NotifyEnded() {
+ MOZ_ASSERT(mReadyState == MediaStreamTrackState::Ended);
+
+ for (const auto& consumer : mConsumers.Clone()) {
+ if (consumer) {
+ consumer->NotifyEnded(this);
+ } else {
+ MOZ_ASSERT_UNREACHABLE("A consumer was not explicitly removed");
+ mConsumers.RemoveElement(consumer);
+ }
+ }
+}
+
+void MediaStreamTrack::NotifyEnabledChanged() {
+ GetSource().SinkEnabledStateChanged();
+
+ for (const auto& consumer : mConsumers.Clone()) {
+ if (consumer) {
+ consumer->NotifyEnabledChanged(this, Enabled());
+ } else {
+ MOZ_ASSERT_UNREACHABLE("A consumer was not explicitly removed");
+ mConsumers.RemoveElement(consumer);
+ }
+ }
+}
+
+bool MediaStreamTrack::AddPrincipalChangeObserver(
+ PrincipalChangeObserver<MediaStreamTrack>* aObserver) {
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier.
+ mPrincipalChangeObservers.AppendElement(aObserver);
+ return true;
+}
+
+bool MediaStreamTrack::RemovePrincipalChangeObserver(
+ PrincipalChangeObserver<MediaStreamTrack>* aObserver) {
+ return mPrincipalChangeObservers.RemoveElement(aObserver);
+}
+
+void MediaStreamTrack::AddConsumer(MediaStreamTrackConsumer* aConsumer) {
+ MOZ_ASSERT(!mConsumers.Contains(aConsumer));
+ mConsumers.AppendElement(aConsumer);
+
+ // Remove destroyed consumers for cleanliness
+ while (mConsumers.RemoveElement(nullptr)) {
+ MOZ_ASSERT_UNREACHABLE("A consumer was not explicitly removed");
+ }
+}
+
+void MediaStreamTrack::RemoveConsumer(MediaStreamTrackConsumer* aConsumer) {
+ mConsumers.RemoveElement(aConsumer);
+
+ // Remove destroyed consumers for cleanliness
+ while (mConsumers.RemoveElement(nullptr)) {
+ MOZ_ASSERT_UNREACHABLE("A consumer was not explicitly removed");
+ }
+}
+
+already_AddRefed<MediaStreamTrack> MediaStreamTrack::Clone() {
+ RefPtr<MediaStreamTrack> newTrack = CloneInternal();
+ newTrack->SetEnabled(Enabled());
+ newTrack->SetMuted(Muted());
+ return newTrack.forget();
+}
+
+void MediaStreamTrack::SetReadyState(MediaStreamTrackState aState) {
+ MOZ_ASSERT(!(mReadyState == MediaStreamTrackState::Ended &&
+ aState == MediaStreamTrackState::Live),
+ "We don't support overriding the ready state from ended to live");
+
+ if (Ended()) {
+ return;
+ }
+
+ if (mReadyState == MediaStreamTrackState::Live &&
+ aState == MediaStreamTrackState::Ended) {
+ if (mSource) {
+ mSource->UnregisterSink(mSink.get());
+ }
+ if (mMTGListener) {
+ RemoveListener(mMTGListener);
+ }
+ if (mPort) {
+ mPort->Destroy();
+ }
+ if (mTrack) {
+ mTrack->Destroy();
+ }
+ mPort = nullptr;
+ mTrack = nullptr;
+ mMTGListener = nullptr;
+ }
+
+ mReadyState = aState;
+}
+
+void MediaStreamTrack::OverrideEnded() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (Ended()) {
+ return;
+ }
+
+ LOG(LogLevel::Info, ("MediaStreamTrack %p ended", this));
+
+ SetReadyState(MediaStreamTrackState::Ended);
+
+ NotifyEnded();
+
+ DispatchTrustedEvent(u"ended"_ns);
+}
+
+void MediaStreamTrack::AddListener(MediaTrackListener* aListener) {
+ LOG(LogLevel::Debug,
+ ("MediaStreamTrack %p adding listener %p", this, aListener));
+ mTrackListeners.AppendElement(aListener);
+
+ if (Ended()) {
+ return;
+ }
+ mTrack->AddListener(aListener);
+}
+
+void MediaStreamTrack::RemoveListener(MediaTrackListener* aListener) {
+ LOG(LogLevel::Debug,
+ ("MediaStreamTrack %p removing listener %p", this, aListener));
+ mTrackListeners.RemoveElement(aListener);
+
+ if (Ended()) {
+ return;
+ }
+ mTrack->RemoveListener(aListener);
+}
+
+void MediaStreamTrack::AddDirectListener(DirectMediaTrackListener* aListener) {
+ LOG(LogLevel::Debug, ("MediaStreamTrack %p (%s) adding direct listener %p to "
+ "track %p",
+ this, AsAudioStreamTrack() ? "audio" : "video",
+ aListener, mTrack.get()));
+ mDirectTrackListeners.AppendElement(aListener);
+
+ if (Ended()) {
+ return;
+ }
+ mTrack->AddDirectListener(aListener);
+}
+
+void MediaStreamTrack::RemoveDirectListener(
+ DirectMediaTrackListener* aListener) {
+ LOG(LogLevel::Debug,
+ ("MediaStreamTrack %p removing direct listener %p from track %p", this,
+ aListener, mTrack.get()));
+ mDirectTrackListeners.RemoveElement(aListener);
+
+ if (Ended()) {
+ return;
+ }
+ mTrack->RemoveDirectListener(aListener);
+}
+
+already_AddRefed<MediaInputPort> MediaStreamTrack::ForwardTrackContentsTo(
+ ProcessedMediaTrack* aTrack) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_RELEASE_ASSERT(aTrack);
+ return aTrack->AllocateInputPort(mTrack);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/MediaStreamTrack.h b/dom/media/MediaStreamTrack.h
new file mode 100644
index 0000000000..30ef7a3d93
--- /dev/null
+++ b/dom/media/MediaStreamTrack.h
@@ -0,0 +1,638 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MEDIASTREAMTRACK_H_
+#define MEDIASTREAMTRACK_H_
+
+#include "MediaTrackConstraints.h"
+#include "PrincipalChangeObserver.h"
+#include "PrincipalHandle.h"
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/dom/MediaStreamTrackBinding.h"
+#include "mozilla/dom/MediaTrackSettingsBinding.h"
+#include "mozilla/media/MediaUtils.h"
+#include "mozilla/WeakPtr.h"
+#include "nsError.h"
+#include "nsID.h"
+#include "nsIPrincipal.h"
+#include "PerformanceRecorder.h"
+
+namespace mozilla {
+
+class DOMMediaStream;
+class MediaEnginePhotoCallback;
+class MediaInputPort;
+class MediaTrack;
+class MediaTrackGraph;
+class MediaTrackGraphImpl;
+class MediaTrackListener;
+class DirectMediaTrackListener;
+class PeerConnectionImpl;
+class PeerIdentity;
+class ProcessedMediaTrack;
+class RemoteSourceStreamInfo;
+class SourceStreamInfo;
+class MediaMgrError;
+
+namespace dom {
+
+class AudioStreamTrack;
+class VideoStreamTrack;
+enum class CallerType : uint32_t;
+
+/**
+ * Common interface through which a MediaStreamTrack can communicate with its
+ * producer on the main thread.
+ *
+ * Kept alive by a strong ref in all MediaStreamTracks (original and clones)
+ * sharing this source.
+ */
+class MediaStreamTrackSource : public nsISupports {
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(MediaStreamTrackSource)
+
+ public:
+ class Sink : public SupportsWeakPtr {
+ public:
+ /**
+ * Must be constant throughout the Sink's lifetime.
+ *
+ * Return true to keep the MediaStreamTrackSource where this sink is
+ * registered alive.
+ * Return false to allow the source to stop.
+ *
+ * Typically MediaStreamTrack::Sink returns true and other Sinks
+ * (like HTMLMediaElement::StreamCaptureTrackSource) return false.
+ */
+ virtual bool KeepsSourceAlive() const = 0;
+
+ /**
+ * Return true to ensure that the MediaStreamTrackSource where this Sink is
+ * registered is kept turned on and active.
+ * Return false to allow the source to pause, and any underlying devices to
+ * temporarily stop.
+ *
+ * When the underlying enabled state of the sink changes,
+ * call MediaStreamTrackSource::SinkEnabledStateChanged().
+ *
+ * Typically MediaStreamTrack returns the track's enabled state and other
+ * Sinks (like HTMLMediaElement::StreamCaptureTrackSource) return false so
+ * control over device state remains with tracks and their enabled state.
+ */
+ virtual bool Enabled() const = 0;
+
+ /**
+ * Called when the principal of the MediaStreamTrackSource where this sink
+ * is registered has changed.
+ */
+ virtual void PrincipalChanged() = 0;
+
+ /**
+ * Called when the muted state of the MediaStreamTrackSource where this sink
+ * is registered has changed.
+ */
+ virtual void MutedChanged(bool aNewState) = 0;
+
+ /**
+ * Called when the MediaStreamTrackSource where this sink is registered has
+ * stopped producing data for good, i.e., it has ended.
+ */
+ virtual void OverrideEnded() = 0;
+
+ protected:
+ virtual ~Sink() = default;
+ };
+
+ MediaStreamTrackSource(nsIPrincipal* aPrincipal, const nsString& aLabel,
+ TrackingId aTrackingId)
+ : mPrincipal(aPrincipal),
+ mLabel(aLabel),
+ mTrackingId(std::move(aTrackingId)),
+ mStopped(false) {}
+
+ /**
+ * Use to clean up any resources that have to be cleaned before the
+ * destructor is called. It is often too late in the destructor because
+ * of garbage collection having removed the members already.
+ */
+ virtual void Destroy() {}
+
+ /**
+ * Gets the source's MediaSourceEnum for usage by PeerConnections.
+ */
+ virtual MediaSourceEnum GetMediaSource() const = 0;
+
+ /**
+ * Get this TrackSource's principal.
+ */
+ nsIPrincipal* GetPrincipal() const { return mPrincipal; }
+
+ /**
+ * This is used in WebRTC. A peerIdentity constrained MediaStreamTrack cannot
+ * be sent across the network to anything other than a peer with the provided
+ * identity. If this is set, then GetPrincipal() should return an instance of
+ * NullPrincipal.
+ *
+ * A track's PeerIdentity is immutable and will not change during the track's
+ * lifetime.
+ */
+ virtual const PeerIdentity* GetPeerIdentity() const { return nullptr; }
+
+ /**
+ * MediaStreamTrack::GetLabel (see spec) calls through to here.
+ */
+ void GetLabel(nsAString& aLabel) { aLabel.Assign(mLabel); }
+
+ /**
+ * Whether this TrackSource provides video frames with an alpha channel. Only
+ * applies to video sources. Used by HTMLVideoElement.
+ */
+ virtual bool HasAlpha() const { return false; }
+
+ /**
+ * Forwards a photo request to backends that support it. Other backends return
+ * NS_ERROR_NOT_IMPLEMENTED to indicate that a MediaTrackGraph-based fallback
+ * should be used.
+ */
+ virtual nsresult TakePhoto(MediaEnginePhotoCallback*) const {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ typedef MozPromise<bool /* aIgnored */, RefPtr<MediaMgrError>, true>
+ ApplyConstraintsPromise;
+
+ /**
+ * We provide a fallback solution to ApplyConstraints() here.
+ * Sources that support ApplyConstraints() will have to override it.
+ */
+ virtual RefPtr<ApplyConstraintsPromise> ApplyConstraints(
+ const dom::MediaTrackConstraints& aConstraints, CallerType aCallerType);
+
+ /**
+ * Same for GetSettings (no-op).
+ */
+ virtual void GetSettings(dom::MediaTrackSettings& aResult){};
+
+ /**
+ * Called by the source interface when all registered sinks with
+ * KeepsSourceAlive() == true have unregistered.
+ */
+ virtual void Stop() = 0;
+
+ /**
+ * Called by the source interface when all registered sinks with
+ * KeepsSourceAlive() == true become disabled.
+ */
+ virtual void Disable() = 0;
+
+ /**
+ * Called by the source interface when at least one registered sink with
+ * KeepsSourceAlive() == true become enabled.
+ */
+ virtual void Enable() = 0;
+
+ /**
+ * Called when a Sink's Enabled() state changed. Will iterate through all
+ * sinks and notify the source of the aggregated enabled state.
+ *
+ * Note that a Sink with KeepsSourceAlive() == false counts as disabled.
+ */
+ void SinkEnabledStateChanged() {
+ if (IsEnabled()) {
+ Enable();
+ } else {
+ Disable();
+ }
+ }
+
+ /**
+ * Called by each MediaStreamTrack clone on initialization.
+ */
+ void RegisterSink(Sink* aSink) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mStopped) {
+ return;
+ }
+ mSinks.AppendElement(aSink);
+ while (mSinks.RemoveElement(nullptr)) {
+ MOZ_ASSERT_UNREACHABLE("Sink was not explicitly removed");
+ }
+ }
+
+ /**
+ * Called by each MediaStreamTrack clone on Stop() if supported by the
+ * source (us) or destruction.
+ */
+ void UnregisterSink(Sink* aSink) {
+ MOZ_ASSERT(NS_IsMainThread());
+ while (mSinks.RemoveElement(nullptr)) {
+ MOZ_ASSERT_UNREACHABLE("Sink was not explicitly removed");
+ }
+ if (mSinks.RemoveElement(aSink) && !IsActive()) {
+ MOZ_ASSERT(!aSink->KeepsSourceAlive() || !mStopped,
+ "When the last sink keeping the source alive is removed, "
+ "we should still be live");
+ Stop();
+ mStopped = true;
+ }
+ if (!mStopped) {
+ SinkEnabledStateChanged();
+ }
+ }
+
+ protected:
+ virtual ~MediaStreamTrackSource() = default;
+
+ bool IsActive() {
+ for (const WeakPtr<Sink>& sink : mSinks) {
+ if (sink && sink->KeepsSourceAlive()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ bool IsEnabled() {
+ for (const WeakPtr<Sink>& sink : mSinks) {
+ if (sink && sink->KeepsSourceAlive() && sink->Enabled()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * Called by a sub class when the principal has changed.
+ * Notifies all sinks.
+ */
+ void PrincipalChanged() {
+ MOZ_ASSERT(NS_IsMainThread());
+ for (auto& sink : mSinks.Clone()) {
+ if (!sink) {
+ DebugOnly<bool> removed = mSinks.RemoveElement(sink);
+ MOZ_ASSERT(!removed, "Sink was not explicitly removed");
+ continue;
+ }
+ sink->PrincipalChanged();
+ }
+ }
+
+ /**
+ * Called by a sub class when the source's muted state has changed. Note that
+ * the source is responsible for making the content black/silent during mute.
+ * Notifies all sinks.
+ */
+ void MutedChanged(bool aNewState) {
+ MOZ_ASSERT(NS_IsMainThread());
+ for (auto& sink : mSinks.Clone()) {
+ if (!sink) {
+ DebugOnly<bool> removed = mSinks.RemoveElement(sink);
+ MOZ_ASSERT(!removed, "Sink was not explicitly removed");
+ continue;
+ }
+ sink->MutedChanged(aNewState);
+ }
+ }
+
+ /**
+ * Called by a sub class when the source has stopped producing data for good,
+ * i.e., it has ended. Notifies all sinks.
+ */
+ void OverrideEnded() {
+ MOZ_ASSERT(NS_IsMainThread());
+ for (auto& sink : mSinks.Clone()) {
+ if (!sink) {
+ DebugOnly<bool> removed = mSinks.RemoveElement(sink);
+ MOZ_ASSERT(!removed, "Sink was not explicitly removed");
+ continue;
+ }
+ sink->OverrideEnded();
+ }
+ }
+
+ // Principal identifying who may access the contents of this source.
+ RefPtr<nsIPrincipal> mPrincipal;
+
+ // Currently registered sinks.
+ nsTArray<WeakPtr<Sink>> mSinks;
+
+ public:
+ // The label of the track we are the source of per the MediaStreamTrack spec.
+ const nsString mLabel;
+
+ // Set for all video sources; an id for tracking the source of the video
+ // frames for this track.
+ const TrackingId mTrackingId;
+
+ protected:
+ // True if all MediaStreamTrack users have unregistered from this source and
+ // Stop() has been called.
+ bool mStopped;
+};
+
+/**
+ * Base class that consumers of a MediaStreamTrack can use to get notifications
+ * about state changes in the track.
+ */
+class MediaStreamTrackConsumer : public SupportsWeakPtr {
+ public:
+ /**
+ * Called when the track's readyState transitions to "ended".
+ * Unlike the "ended" event exposed to script this is called for any reason,
+ * including MediaStreamTrack::Stop().
+ */
+ virtual void NotifyEnded(MediaStreamTrack* aTrack){};
+
+ /**
+ * Called when the track's enabled state changes.
+ */
+ virtual void NotifyEnabledChanged(MediaStreamTrack* aTrack, bool aEnabled){};
+};
+
+// clang-format off
+/**
+ * DOM wrapper for MediaTrackGraph-MediaTracks.
+ *
+ * To account for cloning, a MediaStreamTrack wraps two internal (and chained)
+ * MediaTracks:
+ * 1. mInputTrack
+ * - Controlled by the producer of the data in the track. The producer
+ * decides on lifetime of the MediaTrack and the track inside it.
+ * - It can be any type of MediaTrack.
+ * - Contains one track only.
+ * 2. mTrack
+ * - A ForwardedInputTrack representing this MediaStreamTrack.
+ * - Its data is piped from mInputTrack through mPort.
+ * - Contains one track only.
+ * - When this MediaStreamTrack is enabled/disabled this is reflected in
+ * the chunks in the track in mTrack.
+ * - When this MediaStreamTrack has ended, mTrack gets destroyed.
+ * Note that mInputTrack is unaffected, such that any clones of mTrack
+ * can live on. When all clones are ended, this is signaled to the
+ * producer via our MediaStreamTrackSource. It is then likely to destroy
+ * mInputTrack.
+ *
+ * A graphical representation of how tracks are connected when cloned follows:
+ *
+ * MediaStreamTrack A
+ * mInputTrack mTrack
+ * t1 ---------> t1
+ * \
+ * -----
+ * MediaStreamTrack B \ (clone of A)
+ * mInputTrack \ mTrack
+ * * -> t1
+ *
+ * (*) is a copy of A's mInputTrack
+ */
+// clang-format on
+class MediaStreamTrack : public DOMEventTargetHelper, public SupportsWeakPtr {
+ // PeerConnection and friends need to know our owning DOMStream and track id.
+ friend class mozilla::PeerConnectionImpl;
+ friend class mozilla::SourceStreamInfo;
+ friend class mozilla::RemoteSourceStreamInfo;
+
+ class MTGListener;
+ class TrackSink;
+
+ public:
+ MediaStreamTrack(
+ nsPIDOMWindowInner* aWindow, mozilla::MediaTrack* aInputTrack,
+ MediaStreamTrackSource* aSource,
+ MediaStreamTrackState aReadyState = MediaStreamTrackState::Live,
+ bool aMuted = false,
+ const MediaTrackConstraints& aConstraints = MediaTrackConstraints());
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MediaStreamTrack,
+ DOMEventTargetHelper)
+
+ nsPIDOMWindowInner* GetParentObject() const { return mWindow; }
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ virtual AudioStreamTrack* AsAudioStreamTrack() { return nullptr; }
+ virtual VideoStreamTrack* AsVideoStreamTrack() { return nullptr; }
+
+ virtual const AudioStreamTrack* AsAudioStreamTrack() const { return nullptr; }
+ virtual const VideoStreamTrack* AsVideoStreamTrack() const { return nullptr; }
+
+ // WebIDL
+ virtual void GetKind(nsAString& aKind) = 0;
+ void GetId(nsAString& aID) const;
+ virtual void GetLabel(nsAString& aLabel, CallerType /* aCallerType */) {
+ GetSource().GetLabel(aLabel);
+ }
+ bool Enabled() const { return mEnabled; }
+ void SetEnabled(bool aEnabled);
+ bool Muted() { return mMuted; }
+ void Stop();
+ void GetConstraints(dom::MediaTrackConstraints& aResult);
+ void GetSettings(dom::MediaTrackSettings& aResult, CallerType aCallerType);
+
+ already_AddRefed<Promise> ApplyConstraints(
+ const dom::MediaTrackConstraints& aConstraints, CallerType aCallerType,
+ ErrorResult& aRv);
+ already_AddRefed<MediaStreamTrack> Clone();
+ MediaStreamTrackState ReadyState() { return mReadyState; }
+
+ IMPL_EVENT_HANDLER(mute)
+ IMPL_EVENT_HANDLER(unmute)
+ IMPL_EVENT_HANDLER(ended)
+
+ /**
+ * Convenience (and legacy) method for when ready state is "ended".
+ */
+ bool Ended() const { return mReadyState == MediaStreamTrackState::Ended; }
+
+ /**
+ * Get this track's principal.
+ */
+ nsIPrincipal* GetPrincipal() const { return mPrincipal; }
+
+ /**
+ * Get this track's PeerIdentity.
+ */
+ const PeerIdentity* GetPeerIdentity() const {
+ return GetSource().GetPeerIdentity();
+ }
+
+ ProcessedMediaTrack* GetTrack() const;
+ MediaTrackGraph* Graph() const;
+ MediaTrackGraphImpl* GraphImpl() const;
+
+ MediaStreamTrackSource& GetSource() const {
+ MOZ_RELEASE_ASSERT(mSource,
+ "The track source is only removed on destruction");
+ return *mSource;
+ }
+
+ // Webrtc allows the remote side to name tracks whatever it wants, and we
+ // need to surface this to content.
+ void AssignId(const nsAString& aID) { mID = aID; }
+
+ /**
+ * Add a PrincipalChangeObserver to this track.
+ *
+ * Returns true if it was successfully added.
+ *
+ * Ownership of the PrincipalChangeObserver remains with the caller, and it's
+ * the caller's responsibility to remove the observer before it dies.
+ */
+ bool AddPrincipalChangeObserver(
+ PrincipalChangeObserver<MediaStreamTrack>* aObserver);
+
+ /**
+ * Remove an added PrincipalChangeObserver from this track.
+ *
+ * Returns true if it was successfully removed.
+ */
+ bool RemovePrincipalChangeObserver(
+ PrincipalChangeObserver<MediaStreamTrack>* aObserver);
+
+ /**
+ * Add a MediaStreamTrackConsumer to this track.
+ *
+ * Adding the same consumer multiple times is prohibited.
+ */
+ void AddConsumer(MediaStreamTrackConsumer* aConsumer);
+
+ /**
+ * Remove an added MediaStreamTrackConsumer from this track.
+ */
+ void RemoveConsumer(MediaStreamTrackConsumer* aConsumer);
+
+ /**
+ * Adds a MediaTrackListener to the MediaTrackGraph representation of
+ * this track.
+ */
+ virtual void AddListener(MediaTrackListener* aListener);
+
+ /**
+ * Removes a MediaTrackListener from the MediaTrackGraph representation
+ * of this track.
+ */
+ void RemoveListener(MediaTrackListener* aListener);
+
+ /**
+ * Attempts to add a direct track listener to this track.
+ * Callers must listen to the NotifyInstalled event to know if installing
+ * the listener succeeded (tracks originating from SourceMediaTracks) or
+ * failed (e.g., WebAudio originated tracks).
+ */
+ virtual void AddDirectListener(DirectMediaTrackListener* aListener);
+ void RemoveDirectListener(DirectMediaTrackListener* aListener);
+
+ /**
+ * Sets up a MediaInputPort from the underlying track that this
+ * MediaStreamTrack represents, to aTrack, and returns it.
+ */
+ already_AddRefed<MediaInputPort> ForwardTrackContentsTo(
+ ProcessedMediaTrack* aTrack);
+
+ protected:
+ virtual ~MediaStreamTrack();
+
+ /**
+ * Forces the ready state to a particular value, for instance when we're
+ * cloning an already ended track.
+ */
+ virtual void SetReadyState(MediaStreamTrackState aState);
+
+ /**
+ * Notified by the MediaTrackGraph, through our owning MediaStream on the
+ * main thread.
+ *
+ * Note that this sets the track to ended and raises the "ended" event
+ * synchronously.
+ */
+ void OverrideEnded();
+
+ /**
+ * Called by the MTGListener when this track's PrincipalHandle changes on
+ * the MediaTrackGraph thread. When the PrincipalHandle matches the pending
+ * principal we know that the principal change has propagated to consumers.
+ */
+ void NotifyPrincipalHandleChanged(const PrincipalHandle& aNewPrincipalHandle);
+
+ /**
+ * Called when this track's readyState transitions to "ended".
+ * Notifies all MediaStreamTrackConsumers that this track ended.
+ */
+ void NotifyEnded();
+
+ /**
+ * Called when this track's enabled state has changed.
+ * Notifies all MediaStreamTrackConsumers.
+ */
+ void NotifyEnabledChanged();
+
+ /**
+ * Called when mSource's principal has changed.
+ */
+ void PrincipalChanged();
+
+ /**
+ * Called when mSource's muted state has changed.
+ */
+ void MutedChanged(bool aNewState);
+
+ /**
+ * Sets this track's muted state without raising any events.
+ * Only really set by cloning. See MutedChanged for runtime changes.
+ */
+ void SetMuted(bool aMuted) { mMuted = aMuted; }
+
+ virtual void Destroy();
+
+ /**
+ * Sets the principal and notifies PrincipalChangeObservers if it changes.
+ */
+ void SetPrincipal(nsIPrincipal* aPrincipal);
+
+ /**
+ * Creates a new MediaStreamTrack with the same kind, input track, input
+ * track ID and source as this MediaStreamTrack.
+ */
+ virtual already_AddRefed<MediaStreamTrack> CloneInternal() = 0;
+
+ nsTArray<PrincipalChangeObserver<MediaStreamTrack>*>
+ mPrincipalChangeObservers;
+
+ nsTArray<WeakPtr<MediaStreamTrackConsumer>> mConsumers;
+
+ // We need this to track our parent object.
+ nsCOMPtr<nsPIDOMWindowInner> mWindow;
+
+ // The input MediaTrack assigned us by the data producer.
+ // Owned by the producer.
+ const RefPtr<mozilla::MediaTrack> mInputTrack;
+ // The MediaTrack representing this MediaStreamTrack in the MediaTrackGraph.
+ // Set on construction if we're live. Valid until we end. Owned by us.
+ RefPtr<ProcessedMediaTrack> mTrack;
+ // The MediaInputPort connecting mInputTrack to mTrack. Set on construction
+ // if mInputTrack is non-destroyed and we're live. Valid until we end. Owned
+ // by us.
+ RefPtr<MediaInputPort> mPort;
+ RefPtr<MediaStreamTrackSource> mSource;
+ const UniquePtr<TrackSink> mSink;
+ nsCOMPtr<nsIPrincipal> mPrincipal;
+ nsCOMPtr<nsIPrincipal> mPendingPrincipal;
+ RefPtr<MTGListener> mMTGListener;
+ // Keep tracking MediaTrackListener and DirectMediaTrackListener,
+ // so we can remove them in |Destory|.
+ nsTArray<RefPtr<MediaTrackListener>> mTrackListeners;
+ nsTArray<RefPtr<DirectMediaTrackListener>> mDirectTrackListeners;
+ nsString mID;
+ MediaStreamTrackState mReadyState;
+ bool mEnabled;
+ bool mMuted;
+ dom::MediaTrackConstraints mConstraints;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif /* MEDIASTREAMTRACK_H_ */
diff --git a/dom/media/MediaStreamWindowCapturer.cpp b/dom/media/MediaStreamWindowCapturer.cpp
new file mode 100644
index 0000000000..142242eff0
--- /dev/null
+++ b/dom/media/MediaStreamWindowCapturer.cpp
@@ -0,0 +1,77 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaStreamWindowCapturer.h"
+
+#include "AudioStreamTrack.h"
+#include "DOMMediaStream.h"
+#include "MediaTrackGraph.h"
+
+namespace mozilla {
+using dom::AudioStreamTrack;
+using dom::MediaStreamTrack;
+
+MediaStreamWindowCapturer::CapturedTrack::CapturedTrack(
+ MediaStreamTrack* aTrack, uint64_t aWindowID)
+ : mTrack(aTrack),
+ mPort(aTrack->Graph()->ConnectToCaptureTrack(aWindowID,
+ aTrack->GetTrack())) {}
+
+MediaStreamWindowCapturer::CapturedTrack::~CapturedTrack() {
+ if (mPort) {
+ mPort->Destroy();
+ }
+}
+
+MediaStreamWindowCapturer::MediaStreamWindowCapturer(DOMMediaStream* aStream,
+ uint64_t aWindowId)
+ : mStream(aStream), mWindowId(aWindowId) {
+ mStream->RegisterTrackListener(this);
+ nsTArray<RefPtr<AudioStreamTrack>> tracks;
+ mStream->GetAudioTracks(tracks);
+ for (const auto& t : tracks) {
+ if (t->Ended()) {
+ continue;
+ }
+ AddTrack(t);
+ }
+}
+
+MediaStreamWindowCapturer::~MediaStreamWindowCapturer() {
+ if (mStream) {
+ mStream->UnregisterTrackListener(this);
+ }
+}
+
+void MediaStreamWindowCapturer::NotifyTrackAdded(
+ const RefPtr<MediaStreamTrack>& aTrack) {
+ if (AudioStreamTrack* at = aTrack->AsAudioStreamTrack()) {
+ AddTrack(at);
+ }
+}
+
+void MediaStreamWindowCapturer::NotifyTrackRemoved(
+ const RefPtr<MediaStreamTrack>& aTrack) {
+ if (AudioStreamTrack* at = aTrack->AsAudioStreamTrack()) {
+ RemoveTrack(at);
+ }
+}
+
+void MediaStreamWindowCapturer::AddTrack(AudioStreamTrack* aTrack) {
+ if (aTrack->Ended()) {
+ return;
+ }
+ mTracks.AppendElement(MakeUnique<CapturedTrack>(aTrack, mWindowId));
+}
+
+void MediaStreamWindowCapturer::RemoveTrack(AudioStreamTrack* aTrack) {
+ for (size_t i = mTracks.Length(); i > 0; --i) {
+ if (mTracks[i - 1]->mTrack == aTrack) {
+ mTracks.RemoveElementAt(i - 1);
+ break;
+ }
+ }
+}
+} // namespace mozilla
diff --git a/dom/media/MediaStreamWindowCapturer.h b/dom/media/MediaStreamWindowCapturer.h
new file mode 100644
index 0000000000..8a6695ed43
--- /dev/null
+++ b/dom/media/MediaStreamWindowCapturer.h
@@ -0,0 +1,51 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MediaStreamWindowCapturer_h
+#define MediaStreamWindowCapturer_h
+
+#include "DOMMediaStream.h"
+
+namespace mozilla {
+namespace dom {
+class AudioStreamTrack;
+class MediaStreamTrack;
+} // namespace dom
+
+class MediaInputPort;
+
+/**
+ * Given a DOMMediaStream and a window id, this class will pipe the audio from
+ * all live audio tracks in the stream to the MediaTrackGraph's window capture
+ * mechanism.
+ */
+class MediaStreamWindowCapturer : public DOMMediaStream::TrackListener {
+ public:
+ MediaStreamWindowCapturer(DOMMediaStream* aStream, uint64_t aWindowId);
+ ~MediaStreamWindowCapturer();
+
+ void NotifyTrackAdded(const RefPtr<dom::MediaStreamTrack>& aTrack) override;
+ void NotifyTrackRemoved(const RefPtr<dom::MediaStreamTrack>& aTrack) override;
+
+ struct CapturedTrack {
+ CapturedTrack(dom::MediaStreamTrack* aTrack, uint64_t aWindowID);
+ ~CapturedTrack();
+
+ const WeakPtr<dom::MediaStreamTrack> mTrack;
+ const RefPtr<MediaInputPort> mPort;
+ };
+
+ const WeakPtr<DOMMediaStream> mStream;
+ const uint64_t mWindowId;
+
+ protected:
+ void AddTrack(dom::AudioStreamTrack* aTrack);
+ void RemoveTrack(dom::AudioStreamTrack* aTrack);
+
+ nsTArray<UniquePtr<CapturedTrack>> mTracks;
+};
+} // namespace mozilla
+
+#endif /* MediaStreamWindowCapturer_h */
diff --git a/dom/media/MediaTimer.cpp b/dom/media/MediaTimer.cpp
new file mode 100644
index 0000000000..c34eb816fa
--- /dev/null
+++ b/dom/media/MediaTimer.cpp
@@ -0,0 +1,192 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaTimer.h"
+
+#include "mozilla/DebugOnly.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/SharedThreadPool.h"
+#include "mozilla/Unused.h"
+#include "nsComponentManagerUtils.h"
+#include "nsThreadUtils.h"
+#include <math.h>
+
+namespace mozilla {
+
+MediaTimer::MediaTimer(bool aFuzzy)
+ : mMonitor("MediaTimer Monitor"),
+ mCreationTimeStamp(TimeStamp::Now()),
+ mUpdateScheduled(false),
+ mFuzzy(aFuzzy) {
+ TIMER_LOG("MediaTimer::MediaTimer");
+
+ // Use the SharedThreadPool to create an nsIThreadPool with a maximum of one
+ // thread, which is equivalent to an nsIThread for our purposes.
+ RefPtr<SharedThreadPool> threadPool(
+ SharedThreadPool::Get("MediaTimer"_ns, 1));
+ mThread = threadPool.get();
+ mTimer = NS_NewTimer(mThread);
+}
+
+void MediaTimer::DispatchDestroy() {
+ // Hold a strong reference to the thread so that it doesn't get deleted in
+ // Destroy(), which may run completely before the stack if Dispatch() begins
+ // to unwind.
+ nsCOMPtr<nsIEventTarget> thread = mThread;
+ nsresult rv =
+ thread->Dispatch(NewNonOwningRunnableMethod("MediaTimer::Destroy", this,
+ &MediaTimer::Destroy),
+ NS_DISPATCH_NORMAL);
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ (void)rv;
+}
+
+void MediaTimer::Destroy() {
+ MOZ_ASSERT(OnMediaTimerThread());
+ TIMER_LOG("MediaTimer::Destroy");
+
+ // Reject any outstanding entries.
+ {
+ MonitorAutoLock lock(mMonitor);
+ Reject();
+
+ // Cancel the timer if necessary.
+ CancelTimerIfArmed();
+ }
+
+ delete this;
+}
+
+bool MediaTimer::OnMediaTimerThread() {
+ bool rv = false;
+ mThread->IsOnCurrentThread(&rv);
+ return rv;
+}
+
+RefPtr<MediaTimerPromise> MediaTimer::WaitFor(const TimeDuration& aDuration,
+ const char* aCallSite) {
+ return WaitUntil(TimeStamp::Now() + aDuration, aCallSite);
+}
+
+RefPtr<MediaTimerPromise> MediaTimer::WaitUntil(const TimeStamp& aTimeStamp,
+ const char* aCallSite) {
+ MonitorAutoLock mon(mMonitor);
+ TIMER_LOG("MediaTimer::WaitUntil %" PRId64, RelativeMicroseconds(aTimeStamp));
+ Entry e(aTimeStamp, aCallSite);
+ RefPtr<MediaTimerPromise> p = e.mPromise.get();
+ mEntries.push(e);
+ ScheduleUpdate();
+ return p;
+}
+
+void MediaTimer::Cancel() {
+ MonitorAutoLock mon(mMonitor);
+ TIMER_LOG("MediaTimer::Cancel");
+ Reject();
+}
+
+void MediaTimer::ScheduleUpdate() {
+ mMonitor.AssertCurrentThreadOwns();
+ if (mUpdateScheduled) {
+ return;
+ }
+ mUpdateScheduled = true;
+
+ nsresult rv = mThread->Dispatch(
+ NewRunnableMethod("MediaTimer::Update", this, &MediaTimer::Update),
+ NS_DISPATCH_NORMAL);
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ (void)rv;
+}
+
+void MediaTimer::Update() {
+ MonitorAutoLock mon(mMonitor);
+ UpdateLocked();
+}
+
+bool MediaTimer::IsExpired(const TimeStamp& aTarget, const TimeStamp& aNow) {
+ MOZ_ASSERT(OnMediaTimerThread());
+ mMonitor.AssertCurrentThreadOwns();
+ // Treat this timer as expired in fuzzy mode even if it is fired
+ // slightly (< 1ms) before the schedule target. So we don't need to schedule a
+ // timer with very small timeout again when the client doesn't need a high-res
+ // timer.
+ TimeStamp t = mFuzzy ? aTarget - TimeDuration::FromMilliseconds(1) : aTarget;
+ return t <= aNow;
+}
+
+void MediaTimer::UpdateLocked() {
+ MOZ_ASSERT(OnMediaTimerThread());
+ mMonitor.AssertCurrentThreadOwns();
+ mUpdateScheduled = false;
+
+ TIMER_LOG("MediaTimer::UpdateLocked");
+
+ // Resolve all the promises whose time is up.
+ TimeStamp now = TimeStamp::Now();
+ while (!mEntries.empty() && IsExpired(mEntries.top().mTimeStamp, now)) {
+ mEntries.top().mPromise->Resolve(true, __func__);
+ DebugOnly<TimeStamp> poppedTimeStamp = mEntries.top().mTimeStamp;
+ mEntries.pop();
+ MOZ_ASSERT_IF(!mEntries.empty(),
+ *&poppedTimeStamp <= mEntries.top().mTimeStamp);
+ }
+
+ // If we've got no more entries, cancel any pending timer and bail out.
+ if (mEntries.empty()) {
+ CancelTimerIfArmed();
+ return;
+ }
+
+ // We've got more entries - (re)arm the timer for the soonest one.
+ if (!TimerIsArmed() || mEntries.top().mTimeStamp < mCurrentTimerTarget) {
+ CancelTimerIfArmed();
+ ArmTimer(mEntries.top().mTimeStamp, now);
+ }
+}
+
+void MediaTimer::Reject() {
+ mMonitor.AssertCurrentThreadOwns();
+ while (!mEntries.empty()) {
+ mEntries.top().mPromise->Reject(false, __func__);
+ mEntries.pop();
+ }
+}
+
+/*
+ * We use a callback function, rather than a callback method, to ensure that
+ * the nsITimer does not artifically keep the refcount of the MediaTimer above
+ * zero. When the MediaTimer is destroyed, it safely cancels the nsITimer so
+ * that we never fire against a dangling closure.
+ */
+
+/* static */
+void MediaTimer::TimerCallback(nsITimer* aTimer, void* aClosure) {
+ static_cast<MediaTimer*>(aClosure)->TimerFired();
+}
+
+void MediaTimer::TimerFired() {
+ MonitorAutoLock mon(mMonitor);
+ MOZ_ASSERT(OnMediaTimerThread());
+ mCurrentTimerTarget = TimeStamp();
+ UpdateLocked();
+}
+
+void MediaTimer::ArmTimer(const TimeStamp& aTarget, const TimeStamp& aNow) {
+ MOZ_DIAGNOSTIC_ASSERT(!TimerIsArmed());
+ MOZ_DIAGNOSTIC_ASSERT(aTarget > aNow);
+
+ const TimeDuration delay = aTarget - aNow;
+ TIMER_LOG("MediaTimer::ArmTimer delay=%.3fms", delay.ToMilliseconds());
+ mCurrentTimerTarget = aTarget;
+ MOZ_ALWAYS_SUCCEEDS(mTimer->InitHighResolutionWithNamedFuncCallback(
+ &TimerCallback, this, delay, nsITimer::TYPE_ONE_SHOT,
+ "MediaTimer::TimerCallback"));
+}
+
+} // namespace mozilla
diff --git a/dom/media/MediaTimer.h b/dom/media/MediaTimer.h
new file mode 100644
index 0000000000..837a1591b3
--- /dev/null
+++ b/dom/media/MediaTimer.h
@@ -0,0 +1,161 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(MediaTimer_h_)
+# define MediaTimer_h_
+
+# include <queue>
+
+# include "mozilla/AbstractThread.h"
+# include "mozilla/IntegerPrintfMacros.h"
+# include "mozilla/Monitor.h"
+# include "mozilla/MozPromise.h"
+# include "mozilla/RefPtr.h"
+# include "mozilla/TimeStamp.h"
+# include "mozilla/Unused.h"
+# include "nsITimer.h"
+
+namespace mozilla {
+
+extern LazyLogModule gMediaTimerLog;
+
+# define TIMER_LOG(x, ...) \
+ MOZ_ASSERT(gMediaTimerLog); \
+ MOZ_LOG(gMediaTimerLog, LogLevel::Debug, \
+ ("[MediaTimer=%p relative_t=%" PRId64 "]" x, this, \
+ RelativeMicroseconds(TimeStamp::Now()), ##__VA_ARGS__))
+
+// This promise type is only exclusive because so far there isn't a reason for
+// it not to be. Feel free to change that.
+typedef MozPromise<bool, bool, /* IsExclusive = */ true> MediaTimerPromise;
+
+// Timers only know how to fire at a given thread, which creates an impedence
+// mismatch with code that operates with TaskQueues. This class solves
+// that mismatch with a dedicated (but shared) thread and a nice MozPromise-y
+// interface.
+class MediaTimer {
+ public:
+ explicit MediaTimer(bool aFuzzy = false);
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING_WITH_DESTROY(MediaTimer,
+ DispatchDestroy());
+
+ RefPtr<MediaTimerPromise> WaitFor(const TimeDuration& aDuration,
+ const char* aCallSite);
+ RefPtr<MediaTimerPromise> WaitUntil(const TimeStamp& aTimeStamp,
+ const char* aCallSite);
+ void Cancel(); // Cancel and reject any unresolved promises with false.
+
+ private:
+ virtual ~MediaTimer() { MOZ_ASSERT(OnMediaTimerThread()); }
+
+ void DispatchDestroy(); // Invoked by Release on an arbitrary thread.
+ void Destroy(); // Runs on the timer thread.
+
+ bool OnMediaTimerThread();
+ void ScheduleUpdate();
+ void Update();
+ void UpdateLocked();
+ bool IsExpired(const TimeStamp& aTarget, const TimeStamp& aNow);
+ void Reject();
+
+ static void TimerCallback(nsITimer* aTimer, void* aClosure);
+ void TimerFired();
+ void ArmTimer(const TimeStamp& aTarget, const TimeStamp& aNow);
+
+ bool TimerIsArmed() { return !mCurrentTimerTarget.IsNull(); }
+
+ void CancelTimerIfArmed() {
+ MOZ_ASSERT(OnMediaTimerThread());
+ if (TimerIsArmed()) {
+ TIMER_LOG("MediaTimer::CancelTimerIfArmed canceling timer");
+ mTimer->Cancel();
+ mCurrentTimerTarget = TimeStamp();
+ }
+ }
+
+ struct Entry {
+ TimeStamp mTimeStamp;
+ RefPtr<MediaTimerPromise::Private> mPromise;
+
+ explicit Entry(const TimeStamp& aTimeStamp, const char* aCallSite)
+ : mTimeStamp(aTimeStamp),
+ mPromise(new MediaTimerPromise::Private(aCallSite)) {}
+
+ // Define a < overload that reverses ordering because std::priority_queue
+ // provides access to the largest element, and we want the smallest
+ // (i.e. the soonest).
+ bool operator<(const Entry& aOther) const {
+ return mTimeStamp > aOther.mTimeStamp;
+ }
+ };
+
+ nsCOMPtr<nsIEventTarget> mThread;
+ std::priority_queue<Entry> mEntries;
+ Monitor mMonitor MOZ_UNANNOTATED;
+ nsCOMPtr<nsITimer> mTimer;
+ TimeStamp mCurrentTimerTarget;
+
+ // Timestamps only have relative meaning, so we need a base timestamp for
+ // logging purposes.
+ TimeStamp mCreationTimeStamp;
+ int64_t RelativeMicroseconds(const TimeStamp& aTimeStamp) {
+ return (int64_t)(aTimeStamp - mCreationTimeStamp).ToMicroseconds();
+ }
+
+ bool mUpdateScheduled;
+ const bool mFuzzy;
+};
+
+// Class for managing delayed dispatches on target thread.
+class DelayedScheduler {
+ public:
+ explicit DelayedScheduler(nsISerialEventTarget* aTargetThread,
+ bool aFuzzy = false)
+ : mTargetThread(aTargetThread), mMediaTimer(new MediaTimer(aFuzzy)) {
+ MOZ_ASSERT(mTargetThread);
+ }
+
+ bool IsScheduled() const { return !mTarget.IsNull(); }
+
+ void Reset() {
+ MOZ_ASSERT(mTargetThread->IsOnCurrentThread(),
+ "Must be on target thread to disconnect");
+ mRequest.DisconnectIfExists();
+ mTarget = TimeStamp();
+ }
+
+ template <typename ResolveFunc, typename RejectFunc>
+ void Ensure(mozilla::TimeStamp& aTarget, ResolveFunc&& aResolver,
+ RejectFunc&& aRejector) {
+ MOZ_ASSERT(mTargetThread->IsOnCurrentThread());
+ if (IsScheduled() && mTarget <= aTarget) {
+ return;
+ }
+ Reset();
+ mTarget = aTarget;
+ mMediaTimer->WaitUntil(mTarget, __func__)
+ ->Then(mTargetThread, __func__, std::forward<ResolveFunc>(aResolver),
+ std::forward<RejectFunc>(aRejector))
+ ->Track(mRequest);
+ }
+
+ void CompleteRequest() {
+ MOZ_ASSERT(mTargetThread->IsOnCurrentThread());
+ mRequest.Complete();
+ mTarget = TimeStamp();
+ }
+
+ private:
+ nsCOMPtr<nsISerialEventTarget> mTargetThread;
+ RefPtr<MediaTimer> mMediaTimer;
+ MozPromiseRequestHolder<mozilla::MediaTimerPromise> mRequest;
+ TimeStamp mTarget;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/MediaTrack.cpp b/dom/media/MediaTrack.cpp
new file mode 100644
index 0000000000..a382fcd739
--- /dev/null
+++ b/dom/media/MediaTrack.cpp
@@ -0,0 +1,34 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 et tw=78: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaTrack.h"
+#include "AudioTrack.h"
+#include "MediaTrackList.h"
+#include "VideoTrack.h"
+
+namespace mozilla::dom {
+
+MediaTrack::MediaTrack(nsIGlobalObject* aOwnerGlobal, const nsAString& aId,
+ const nsAString& aKind, const nsAString& aLabel,
+ const nsAString& aLanguage)
+ : DOMEventTargetHelper(aOwnerGlobal),
+ mId(aId),
+ mKind(aKind),
+ mLabel(aLabel),
+ mLanguage(aLanguage) {}
+
+MediaTrack::~MediaTrack() = default;
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(dom::MediaTrack, DOMEventTargetHelper, mList)
+
+NS_IMPL_ADDREF_INHERITED(dom::MediaTrack, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(dom::MediaTrack, DOMEventTargetHelper)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(dom::MediaTrack)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+void MediaTrack::SetTrackList(MediaTrackList* aList) { mList = aList; }
+
+} // namespace mozilla::dom
diff --git a/dom/media/MediaTrack.h b/dom/media/MediaTrack.h
new file mode 100644
index 0000000000..8b030c8562
--- /dev/null
+++ b/dom/media/MediaTrack.h
@@ -0,0 +1,79 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 et tw=78: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_MediaTrack_h
+#define mozilla_dom_MediaTrack_h
+
+#include "mozilla/DOMEventTargetHelper.h"
+
+namespace mozilla::dom {
+
+class MediaTrackList;
+class VideoTrack;
+class AudioTrack;
+
+/**
+ * Base class of AudioTrack and VideoTrack. The AudioTrack and VideoTrack
+ * objects represent specific tracks of a media resource. Each track has aspects
+ * of an identifier, category, label, and language, even if a track is removed
+ * from its corresponding track list, those aspects do not change.
+ *
+ * When fetching the media resource, an audio/video track is created if the
+ * media resource is found to have an audio/video track. When the UA has learned
+ * that an audio/video track has ended, this audio/video track will be removed
+ * from its corresponding track list.
+ *
+ * Although AudioTrack and VideoTrack are not EventTargets, TextTrack is, and
+ * TextTrack inherits from MediaTrack as well (or is going to).
+ */
+class MediaTrack : public DOMEventTargetHelper {
+ public:
+ MediaTrack(nsIGlobalObject* aOwnerGlobal, const nsAString& aId,
+ const nsAString& aKind, const nsAString& aLabel,
+ const nsAString& aLanguage);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(dom::MediaTrack,
+ DOMEventTargetHelper)
+
+ enum {
+ DEFAULT = 0,
+ FIRE_NO_EVENTS = 1 << 0,
+ };
+ // The default behavior of enabling an audio track or selecting a video track
+ // fires a change event and notifies its media resource about the changes.
+ // It should not fire any events when fetching media resource.
+ virtual void SetEnabledInternal(bool aEnabled, int aFlags) = 0;
+
+ virtual AudioTrack* AsAudioTrack() { return nullptr; }
+
+ virtual VideoTrack* AsVideoTrack() { return nullptr; }
+
+ const nsString& GetId() const { return mId; }
+
+ // WebIDL
+ void GetId(nsAString& aId) const { aId = mId; }
+ void GetKind(nsAString& aKind) const { aKind = mKind; }
+ void GetLabel(nsAString& aLabel) const { aLabel = mLabel; }
+ void GetLanguage(nsAString& aLanguage) const { aLanguage = mLanguage; }
+
+ friend class MediaTrackList;
+
+ protected:
+ virtual ~MediaTrack();
+
+ void SetTrackList(MediaTrackList* aList);
+
+ RefPtr<MediaTrackList> mList;
+ nsString mId;
+ nsString mKind;
+ nsString mLabel;
+ nsString mLanguage;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_MediaTrack_h
diff --git a/dom/media/MediaTrackGraph.cpp b/dom/media/MediaTrackGraph.cpp
new file mode 100644
index 0000000000..d60015a68d
--- /dev/null
+++ b/dom/media/MediaTrackGraph.cpp
@@ -0,0 +1,4129 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaTrackGraphImpl.h"
+#include "mozilla/MathAlgorithms.h"
+#include "mozilla/Unused.h"
+
+#include "AudioSegment.h"
+#include "CrossGraphPort.h"
+#include "VideoSegment.h"
+#include "nsContentUtils.h"
+#include "nsPrintfCString.h"
+#include "nsServiceManagerUtils.h"
+#include "prerror.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Attributes.h"
+#include "ForwardedInputTrack.h"
+#include "ImageContainer.h"
+#include "AudioCaptureTrack.h"
+#include "AudioNodeTrack.h"
+#include "AudioNodeExternalInputTrack.h"
+#if defined(MOZ_WEBRTC)
+# include "MediaEngineWebRTCAudio.h"
+#endif // MOZ_WEBRTC
+#include "MediaTrackListener.h"
+#include "mozilla/dom/BaseAudioContextBinding.h"
+#include "mozilla/dom/WorkletThread.h"
+#include "mozilla/media/MediaUtils.h"
+#include <algorithm>
+#include "GeckoProfiler.h"
+#include "VideoFrameContainer.h"
+#include "mozilla/AbstractThread.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "transport/runnable_utils.h"
+#include "VideoUtils.h"
+#include "GraphRunner.h"
+#include "Tracing.h"
+#include "UnderrunHandler.h"
+#include "mozilla/CycleCollectedJSRuntime.h"
+#include "mozilla/Preferences.h"
+
+#include "webaudio/blink/DenormalDisabler.h"
+#include "webaudio/blink/HRTFDatabaseLoader.h"
+
+using namespace mozilla::layers;
+using namespace mozilla::dom;
+using namespace mozilla::gfx;
+using namespace mozilla::media;
+
+namespace mozilla {
+
+LazyLogModule gMediaTrackGraphLog("MediaTrackGraph");
+#ifdef LOG
+# undef LOG
+#endif // LOG
+#define LOG(type, msg) MOZ_LOG(gMediaTrackGraphLog, type, msg)
+
+NativeInputTrack* DeviceInputTrackManager::GetNativeInputTrack() {
+ return mNativeInputTrack.get();
+}
+
+DeviceInputTrack* DeviceInputTrackManager::GetDeviceInputTrack(
+ CubebUtils::AudioDeviceID aID) {
+ if (mNativeInputTrack && mNativeInputTrack->mDeviceId == aID) {
+ return mNativeInputTrack.get();
+ }
+ for (const RefPtr<NonNativeInputTrack>& t : mNonNativeInputTracks) {
+ if (t->mDeviceId == aID) {
+ return t.get();
+ }
+ }
+ return nullptr;
+}
+
+NonNativeInputTrack* DeviceInputTrackManager::GetFirstNonNativeInputTrack() {
+ if (mNonNativeInputTracks.IsEmpty()) {
+ return nullptr;
+ }
+ return mNonNativeInputTracks[0].get();
+}
+
+void DeviceInputTrackManager::Add(DeviceInputTrack* aTrack) {
+ if (NativeInputTrack* native = aTrack->AsNativeInputTrack()) {
+ MOZ_ASSERT(!mNativeInputTrack);
+ mNativeInputTrack = native;
+ } else {
+ NonNativeInputTrack* nonNative = aTrack->AsNonNativeInputTrack();
+ MOZ_ASSERT(nonNative);
+ struct DeviceTrackComparator {
+ public:
+ bool Equals(const RefPtr<NonNativeInputTrack>& aTrack,
+ CubebUtils::AudioDeviceID aDeviceId) const {
+ return aTrack->mDeviceId == aDeviceId;
+ }
+ };
+ MOZ_ASSERT(!mNonNativeInputTracks.Contains(aTrack->mDeviceId,
+ DeviceTrackComparator()));
+ mNonNativeInputTracks.AppendElement(nonNative);
+ }
+}
+
+void DeviceInputTrackManager::Remove(DeviceInputTrack* aTrack) {
+ if (aTrack->AsNativeInputTrack()) {
+ MOZ_ASSERT(mNativeInputTrack);
+ MOZ_ASSERT(mNativeInputTrack.get() == aTrack->AsNativeInputTrack());
+ mNativeInputTrack = nullptr;
+ } else {
+ NonNativeInputTrack* nonNative = aTrack->AsNonNativeInputTrack();
+ MOZ_ASSERT(nonNative);
+ DebugOnly<bool> removed = mNonNativeInputTracks.RemoveElement(nonNative);
+ MOZ_ASSERT(removed);
+ }
+}
+
+namespace {
+/**
+ * A hash table containing the graph instances, one per Window ID,
+ * sample rate, and device ID combination.
+ */
+class GraphKey final {
+ public:
+ GraphKey(uint64_t aWindowID, TrackRate aSampleRate,
+ CubebUtils::AudioDeviceID aOutputDeviceID)
+ : mWindowID(aWindowID),
+ mSampleRate(aSampleRate),
+ mOutputDeviceID(aOutputDeviceID) {}
+ GraphKey(const GraphKey&) = default;
+ ~GraphKey() = default;
+ bool operator==(const GraphKey& b) const {
+ return mWindowID == b.mWindowID && mSampleRate == b.mSampleRate &&
+ mOutputDeviceID == b.mOutputDeviceID;
+ }
+ PLDHashNumber Hash() const {
+ return HashGeneric(mWindowID, mSampleRate, mOutputDeviceID);
+ }
+
+ private:
+ uint64_t mWindowID;
+ TrackRate mSampleRate;
+ CubebUtils::AudioDeviceID mOutputDeviceID;
+};
+nsTHashMap<nsGenericHashKey<GraphKey>, MediaTrackGraphImpl*> gGraphs;
+} // anonymous namespace
+
+MediaTrackGraphImpl::~MediaTrackGraphImpl() {
+ MOZ_ASSERT(mTracks.IsEmpty() && mSuspendedTracks.IsEmpty(),
+ "All tracks should have been destroyed by messages from the main "
+ "thread");
+ LOG(LogLevel::Debug, ("MediaTrackGraph %p destroyed", this));
+ LOG(LogLevel::Debug, ("MediaTrackGraphImpl::~MediaTrackGraphImpl"));
+}
+
+void MediaTrackGraphImpl::AddTrackGraphThread(MediaTrack* aTrack) {
+ MOZ_ASSERT(OnGraphThreadOrNotRunning());
+ aTrack->mStartTime = mProcessedTime;
+
+ if (aTrack->IsSuspended()) {
+ mSuspendedTracks.AppendElement(aTrack);
+ LOG(LogLevel::Debug,
+ ("%p: Adding media track %p, in the suspended track array", this,
+ aTrack));
+ } else {
+ mTracks.AppendElement(aTrack);
+ LOG(LogLevel::Debug, ("%p: Adding media track %p, count %zu", this, aTrack,
+ mTracks.Length()));
+ }
+
+ SetTrackOrderDirty();
+}
+
+void MediaTrackGraphImpl::RemoveTrackGraphThread(MediaTrack* aTrack) {
+ MOZ_ASSERT(OnGraphThreadOrNotRunning());
+ // Remove references in mTrackUpdates before we allow aTrack to die.
+ // Pending updates are not needed (since the main thread has already given
+ // up the track) so we will just drop them.
+ {
+ MonitorAutoLock lock(mMonitor);
+ for (uint32_t i = 0; i < mTrackUpdates.Length(); ++i) {
+ if (mTrackUpdates[i].mTrack == aTrack) {
+ mTrackUpdates[i].mTrack = nullptr;
+ }
+ }
+ }
+
+ // Ensure that mFirstCycleBreaker is updated when necessary.
+ SetTrackOrderDirty();
+
+ UnregisterAllAudioOutputs(aTrack);
+
+ if (aTrack->IsSuspended()) {
+ mSuspendedTracks.RemoveElement(aTrack);
+ } else {
+ mTracks.RemoveElement(aTrack);
+ }
+
+ LOG(LogLevel::Debug, ("%p: Removed media track %p, count %zu", this, aTrack,
+ mTracks.Length()));
+
+ NS_RELEASE(aTrack); // probably destroying it
+}
+
+TrackTime MediaTrackGraphImpl::GraphTimeToTrackTimeWithBlocking(
+ const MediaTrack* aTrack, GraphTime aTime) const {
+ MOZ_ASSERT(
+ aTime <= mStateComputedTime,
+ "Don't ask about times where we haven't made blocking decisions yet");
+ return std::max<TrackTime>(
+ 0, std::min(aTime, aTrack->mStartBlocking) - aTrack->mStartTime);
+}
+
+GraphTime MediaTrackGraphImpl::IterationEnd() const {
+ MOZ_ASSERT(OnGraphThread());
+ return mIterationEndTime;
+}
+
+void MediaTrackGraphImpl::UpdateCurrentTimeForTracks(
+ GraphTime aPrevCurrentTime) {
+ MOZ_ASSERT(OnGraphThread());
+ for (MediaTrack* track : AllTracks()) {
+ // Shouldn't have already notified of ended *and* have output!
+ MOZ_ASSERT_IF(track->mStartBlocking > aPrevCurrentTime,
+ !track->mNotifiedEnded);
+
+ // Calculate blocked time and fire Blocked/Unblocked events
+ GraphTime blockedTime = mStateComputedTime - track->mStartBlocking;
+ NS_ASSERTION(blockedTime >= 0, "Error in blocking time");
+ track->AdvanceTimeVaryingValuesToCurrentTime(mStateComputedTime,
+ blockedTime);
+ LOG(LogLevel::Verbose,
+ ("%p: MediaTrack %p bufferStartTime=%f blockedTime=%f", this, track,
+ MediaTimeToSeconds(track->mStartTime),
+ MediaTimeToSeconds(blockedTime)));
+ track->mStartBlocking = mStateComputedTime;
+
+ TrackTime trackCurrentTime =
+ track->GraphTimeToTrackTime(mStateComputedTime);
+ if (track->mEnded) {
+ MOZ_ASSERT(track->GetEnd() <= trackCurrentTime);
+ if (!track->mNotifiedEnded) {
+ // Playout of this track ended and listeners have not been notified.
+ track->mNotifiedEnded = true;
+ SetTrackOrderDirty();
+ for (const auto& listener : track->mTrackListeners) {
+ listener->NotifyOutput(this, track->GetEnd());
+ listener->NotifyEnded(this);
+ }
+ }
+ } else {
+ for (const auto& listener : track->mTrackListeners) {
+ listener->NotifyOutput(this, trackCurrentTime);
+ }
+ }
+ }
+}
+
+template <typename C, typename Chunk>
+void MediaTrackGraphImpl::ProcessChunkMetadataForInterval(MediaTrack* aTrack,
+ C& aSegment,
+ TrackTime aStart,
+ TrackTime aEnd) {
+ MOZ_ASSERT(OnGraphThreadOrNotRunning());
+ MOZ_ASSERT(aTrack);
+
+ TrackTime offset = 0;
+ for (typename C::ConstChunkIterator chunk(aSegment); !chunk.IsEnded();
+ chunk.Next()) {
+ if (offset >= aEnd) {
+ break;
+ }
+ offset += chunk->GetDuration();
+ if (chunk->IsNull() || offset < aStart) {
+ continue;
+ }
+ const PrincipalHandle& principalHandle = chunk->GetPrincipalHandle();
+ if (principalHandle != aSegment.GetLastPrincipalHandle()) {
+ aSegment.SetLastPrincipalHandle(principalHandle);
+ LOG(LogLevel::Debug,
+ ("%p: MediaTrack %p, principalHandle "
+ "changed in %sChunk with duration %lld",
+ this, aTrack,
+ aSegment.GetType() == MediaSegment::AUDIO ? "Audio" : "Video",
+ (long long)chunk->GetDuration()));
+ for (const auto& listener : aTrack->mTrackListeners) {
+ listener->NotifyPrincipalHandleChanged(this, principalHandle);
+ }
+ }
+ }
+}
+
+void MediaTrackGraphImpl::ProcessChunkMetadata(GraphTime aPrevCurrentTime) {
+ MOZ_ASSERT(OnGraphThreadOrNotRunning());
+ for (MediaTrack* track : AllTracks()) {
+ TrackTime iterationStart = track->GraphTimeToTrackTime(aPrevCurrentTime);
+ TrackTime iterationEnd = track->GraphTimeToTrackTime(mProcessedTime);
+ if (!track->mSegment) {
+ continue;
+ }
+ if (track->mType == MediaSegment::AUDIO) {
+ ProcessChunkMetadataForInterval<AudioSegment, AudioChunk>(
+ track, *track->GetData<AudioSegment>(), iterationStart, iterationEnd);
+ } else if (track->mType == MediaSegment::VIDEO) {
+ ProcessChunkMetadataForInterval<VideoSegment, VideoChunk>(
+ track, *track->GetData<VideoSegment>(), iterationStart, iterationEnd);
+ } else {
+ MOZ_CRASH("Unknown track type");
+ }
+ }
+}
+
+GraphTime MediaTrackGraphImpl::WillUnderrun(MediaTrack* aTrack,
+ GraphTime aEndBlockingDecisions) {
+ // Ended tracks can't underrun. ProcessedMediaTracks also can't cause
+ // underrun currently, since we'll always be able to produce data for them
+ // unless they block on some other track.
+ if (aTrack->mEnded || aTrack->AsProcessedTrack()) {
+ return aEndBlockingDecisions;
+ }
+ // This track isn't ended or suspended. We don't need to call
+ // TrackTimeToGraphTime since an underrun is the only thing that can block
+ // it.
+ GraphTime bufferEnd = aTrack->GetEnd() + aTrack->mStartTime;
+#ifdef DEBUG
+ if (bufferEnd < mProcessedTime) {
+ LOG(LogLevel::Error, ("%p: MediaTrack %p underrun, "
+ "bufferEnd %f < mProcessedTime %f (%" PRId64
+ " < %" PRId64 "), TrackTime %" PRId64,
+ this, aTrack, MediaTimeToSeconds(bufferEnd),
+ MediaTimeToSeconds(mProcessedTime), bufferEnd,
+ mProcessedTime, aTrack->GetEnd()));
+ NS_ASSERTION(bufferEnd >= mProcessedTime, "Buffer underran");
+ }
+#endif
+ return std::min(bufferEnd, aEndBlockingDecisions);
+}
+
+namespace {
+// Value of mCycleMarker for unvisited tracks in cycle detection.
+const uint32_t NOT_VISITED = UINT32_MAX;
+// Value of mCycleMarker for ordered tracks in muted cycles.
+const uint32_t IN_MUTED_CYCLE = 1;
+} // namespace
+
+bool MediaTrackGraphImpl::AudioTrackPresent() {
+ MOZ_ASSERT(OnGraphThreadOrNotRunning());
+
+ bool audioTrackPresent = false;
+ for (MediaTrack* track : mTracks) {
+ if (track->AsAudioNodeTrack()) {
+ audioTrackPresent = true;
+ break;
+ }
+
+ if (track->mType == MediaSegment::AUDIO && !track->mNotifiedEnded) {
+ audioTrackPresent = true;
+ break;
+ }
+ }
+
+ // We may not have audio input device when we only have AudioNodeTracks. But
+ // if audioTrackPresent is false, we must have no input device.
+ MOZ_DIAGNOSTIC_ASSERT_IF(
+ !audioTrackPresent,
+ !mDeviceInputTrackManagerGraphThread.GetNativeInputTrack());
+
+ return audioTrackPresent;
+}
+
+void MediaTrackGraphImpl::CheckDriver() {
+ MOZ_ASSERT(OnGraphThread());
+ // An offline graph has only one driver.
+ // Otherwise, if a switch is already pending, let that happen.
+ if (!mRealtime || Switching()) {
+ return;
+ }
+
+ AudioCallbackDriver* audioCallbackDriver =
+ CurrentDriver()->AsAudioCallbackDriver();
+ if (audioCallbackDriver && !audioCallbackDriver->OnFallback()) {
+ for (PendingResumeOperation& op : mPendingResumeOperations) {
+ op.Apply(this);
+ }
+ mPendingResumeOperations.Clear();
+ }
+
+ // Note that this looks for any audio tracks, input or output, and switches
+ // to a SystemClockDriver if there are none active or no resume operations
+ // to make any active.
+ bool needAudioCallbackDriver =
+ !mPendingResumeOperations.IsEmpty() || AudioTrackPresent();
+ if (!needAudioCallbackDriver) {
+ if (audioCallbackDriver && audioCallbackDriver->IsStarted()) {
+ SwitchAtNextIteration(
+ new SystemClockDriver(this, CurrentDriver(), mSampleRate));
+ }
+ return;
+ }
+
+ NativeInputTrack* native =
+ mDeviceInputTrackManagerGraphThread.GetNativeInputTrack();
+ CubebUtils::AudioDeviceID inputDevice = native ? native->mDeviceId : nullptr;
+ uint32_t inputChannelCount =
+ native ? AudioInputChannelCount(native->mDeviceId) : 0;
+ AudioInputType inputPreference =
+ native ? AudioInputDevicePreference(native->mDeviceId)
+ : AudioInputType::Unknown;
+
+ uint32_t graphOutputChannelCount = AudioOutputChannelCount();
+ if (!audioCallbackDriver) {
+ if (graphOutputChannelCount > 0) {
+ AudioCallbackDriver* driver = new AudioCallbackDriver(
+ this, CurrentDriver(), mSampleRate, graphOutputChannelCount,
+ inputChannelCount, mOutputDeviceID, inputDevice, inputPreference);
+ SwitchAtNextIteration(driver);
+ }
+ return;
+ }
+
+ // Check if this graph should switch to a different number of output channels.
+ // Generally, a driver switch is explicitly made by an event (e.g., setting
+ // the AudioDestinationNode channelCount), but if an HTMLMediaElement is
+ // directly playing back via another HTMLMediaElement, the number of channels
+ // of the media determines how many channels to output, and it can change
+ // dynamically.
+ if (graphOutputChannelCount != audioCallbackDriver->OutputChannelCount()) {
+ AudioCallbackDriver* driver = new AudioCallbackDriver(
+ this, CurrentDriver(), mSampleRate, graphOutputChannelCount,
+ inputChannelCount, mOutputDeviceID, inputDevice, inputPreference);
+ SwitchAtNextIteration(driver);
+ }
+}
+
+void MediaTrackGraphImpl::UpdateTrackOrder() {
+ if (!mTrackOrderDirty) {
+ return;
+ }
+
+ mTrackOrderDirty = false;
+
+ // The algorithm for finding cycles is based on Tim Leslie's iterative
+ // implementation [1][2] of Pearce's variant [3] of Tarjan's strongly
+ // connected components (SCC) algorithm. There are variations (a) to
+ // distinguish whether tracks in SCCs of size 1 are in a cycle and (b) to
+ // re-run the algorithm over SCCs with breaks at DelayNodes.
+ //
+ // [1] http://www.timl.id.au/?p=327
+ // [2]
+ // https://github.com/scipy/scipy/blob/e2c502fca/scipy/sparse/csgraph/_traversal.pyx#L582
+ // [3] http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.102.1707
+ //
+ // There are two stacks. One for the depth-first search (DFS),
+ mozilla::LinkedList<MediaTrack> dfsStack;
+ // and another for tracks popped from the DFS stack, but still being
+ // considered as part of SCCs involving tracks on the stack.
+ mozilla::LinkedList<MediaTrack> sccStack;
+
+ // An index into mTracks for the next track found with no unsatisfied
+ // upstream dependencies.
+ uint32_t orderedTrackCount = 0;
+
+ for (uint32_t i = 0; i < mTracks.Length(); ++i) {
+ MediaTrack* t = mTracks[i];
+ ProcessedMediaTrack* pt = t->AsProcessedTrack();
+ if (pt) {
+ // The dfsStack initially contains a list of all processed tracks in
+ // unchanged order.
+ dfsStack.insertBack(t);
+ pt->mCycleMarker = NOT_VISITED;
+ } else {
+ // SourceMediaTracks have no inputs and so can be ordered now.
+ mTracks[orderedTrackCount] = t;
+ ++orderedTrackCount;
+ }
+ }
+
+ // mNextStackMarker corresponds to "index" in Tarjan's algorithm. It is a
+ // counter to label mCycleMarker on the next visited track in the DFS
+ // uniquely in the set of visited tracks that are still being considered.
+ //
+ // In this implementation, the counter descends so that the values are
+ // strictly greater than the values that mCycleMarker takes when the track
+ // has been ordered (0 or IN_MUTED_CYCLE).
+ //
+ // Each new track labelled, as the DFS searches upstream, receives a value
+ // less than those used for all other tracks being considered.
+ uint32_t nextStackMarker = NOT_VISITED - 1;
+ // Reset list of DelayNodes in cycles stored at the tail of mTracks.
+ mFirstCycleBreaker = mTracks.Length();
+
+ // Rearrange dfsStack order as required to DFS upstream and pop tracks
+ // in processing order to place in mTracks.
+ while (auto pt = static_cast<ProcessedMediaTrack*>(dfsStack.getFirst())) {
+ const auto& inputs = pt->mInputs;
+ MOZ_ASSERT(pt->AsProcessedTrack());
+ if (pt->mCycleMarker == NOT_VISITED) {
+ // Record the position on the visited stack, so that any searches
+ // finding this track again know how much of the stack is in the cycle.
+ pt->mCycleMarker = nextStackMarker;
+ --nextStackMarker;
+ // Not-visited input tracks should be processed first.
+ // SourceMediaTracks have already been ordered.
+ for (uint32_t i = inputs.Length(); i--;) {
+ if (inputs[i]->GetSource()->IsSuspended()) {
+ continue;
+ }
+ auto input = inputs[i]->GetSource()->AsProcessedTrack();
+ if (input && input->mCycleMarker == NOT_VISITED) {
+ // It can be that this track has an input which is from a suspended
+ // AudioContext.
+ if (input->isInList()) {
+ input->remove();
+ dfsStack.insertFront(input);
+ }
+ }
+ }
+ continue;
+ }
+
+ // Returning from DFS. Pop from dfsStack.
+ pt->remove();
+
+ // cycleStackMarker keeps track of the highest marker value on any
+ // upstream track, if any, found receiving input, directly or indirectly,
+ // from the visited stack (and so from |ps|, making a cycle). In a
+ // variation from Tarjan's SCC algorithm, this does not include |ps|
+ // unless it is part of the cycle.
+ uint32_t cycleStackMarker = 0;
+ for (uint32_t i = inputs.Length(); i--;) {
+ if (inputs[i]->GetSource()->IsSuspended()) {
+ continue;
+ }
+ auto input = inputs[i]->GetSource()->AsProcessedTrack();
+ if (input) {
+ cycleStackMarker = std::max(cycleStackMarker, input->mCycleMarker);
+ }
+ }
+
+ if (cycleStackMarker <= IN_MUTED_CYCLE) {
+ // All inputs have been ordered and their stack markers have been removed.
+ // This track is not part of a cycle. It can be processed next.
+ pt->mCycleMarker = 0;
+ mTracks[orderedTrackCount] = pt;
+ ++orderedTrackCount;
+ continue;
+ }
+
+ // A cycle has been found. Record this track for ordering when all
+ // tracks in this SCC have been popped from the DFS stack.
+ sccStack.insertFront(pt);
+
+ if (cycleStackMarker > pt->mCycleMarker) {
+ // Cycles have been found that involve tracks that remain on the stack.
+ // Leave mCycleMarker indicating the most downstream (last) track on
+ // the stack known to be part of this SCC. In this way, any searches on
+ // other paths that find |ps| will know (without having to traverse from
+ // this track again) that they are part of this SCC (i.e. part of an
+ // intersecting cycle).
+ pt->mCycleMarker = cycleStackMarker;
+ continue;
+ }
+
+ // |pit| is the root of an SCC involving no other tracks on dfsStack, the
+ // complete SCC has been recorded, and tracks in this SCC are part of at
+ // least one cycle.
+ MOZ_ASSERT(cycleStackMarker == pt->mCycleMarker);
+ // If there are DelayNodes in this SCC, then they may break the cycles.
+ bool haveDelayNode = false;
+ auto next = sccStack.getFirst();
+ // Tracks in this SCC are identified by mCycleMarker <= cycleStackMarker.
+ // (There may be other tracks later in sccStack from other incompletely
+ // searched SCCs, involving tracks still on dfsStack.)
+ //
+ // DelayNodes in cycles must behave differently from those not in cycles,
+ // so all DelayNodes in the SCC must be identified.
+ while (next && static_cast<ProcessedMediaTrack*>(next)->mCycleMarker <=
+ cycleStackMarker) {
+ auto nt = next->AsAudioNodeTrack();
+ // Get next before perhaps removing from list below.
+ next = next->getNext();
+ if (nt && nt->Engine()->AsDelayNodeEngine()) {
+ haveDelayNode = true;
+ // DelayNodes break cycles by producing their output in a
+ // preprocessing phase; they do not need to be ordered before their
+ // consumers. Order them at the tail of mTracks so that they can be
+ // handled specially. Do so now, so that DFS ignores them.
+ nt->remove();
+ nt->mCycleMarker = 0;
+ --mFirstCycleBreaker;
+ mTracks[mFirstCycleBreaker] = nt;
+ }
+ }
+ auto after_scc = next;
+ while ((next = sccStack.getFirst()) != after_scc) {
+ next->remove();
+ auto removed = static_cast<ProcessedMediaTrack*>(next);
+ if (haveDelayNode) {
+ // Return tracks to the DFS stack again (to order and detect cycles
+ // without delayNodes). Any of these tracks that are still inputs
+ // for tracks on the visited stack must be returned to the front of
+ // the stack to be ordered before their dependents. We know that none
+ // of these tracks need input from tracks on the visited stack, so
+ // they can all be searched and ordered before the current stack head
+ // is popped.
+ removed->mCycleMarker = NOT_VISITED;
+ dfsStack.insertFront(removed);
+ } else {
+ // Tracks in cycles without any DelayNodes must be muted, and so do
+ // not need input and can be ordered now. They must be ordered before
+ // their consumers so that their muted output is available.
+ removed->mCycleMarker = IN_MUTED_CYCLE;
+ mTracks[orderedTrackCount] = removed;
+ ++orderedTrackCount;
+ }
+ }
+ }
+
+ MOZ_ASSERT(orderedTrackCount == mFirstCycleBreaker);
+}
+
+TrackTime MediaTrackGraphImpl::PlayAudio(AudioMixer* aMixer,
+ const TrackKeyAndVolume& aTkv,
+ GraphTime aPlayedTime) {
+ MOZ_ASSERT(OnGraphThread());
+ MOZ_ASSERT(mRealtime, "Should only attempt to play audio in realtime mode");
+ MOZ_ASSERT(aMixer, "Can only play audio if there's a mixer");
+
+ TrackTime ticksWritten = 0;
+
+ ticksWritten = 0;
+ MediaTrack* track = aTkv.mTrack;
+ AudioSegment* audio = track->GetData<AudioSegment>();
+ AudioSegment output;
+
+ TrackTime offset = track->GraphTimeToTrackTime(aPlayedTime);
+
+ // We don't update Track->mTracksStartTime here to account for time spent
+ // blocked. Instead, we'll update it in UpdateCurrentTimeForTracks after
+ // the blocked period has completed. But we do need to make sure we play
+ // from the right offsets in the track buffer, even if we've already
+ // written silence for some amount of blocked time after the current time.
+ GraphTime t = aPlayedTime;
+ while (t < mStateComputedTime) {
+ bool blocked = t >= track->mStartBlocking;
+ GraphTime end = blocked ? mStateComputedTime : track->mStartBlocking;
+ NS_ASSERTION(end <= mStateComputedTime, "mStartBlocking is wrong!");
+
+ // Check how many ticks of sound we can provide if we are blocked some
+ // time in the middle of this cycle.
+ TrackTime toWrite = end - t;
+
+ if (blocked) {
+ output.InsertNullDataAtStart(toWrite);
+ ticksWritten += toWrite;
+ LOG(LogLevel::Verbose,
+ ("%p: MediaTrack %p writing %" PRId64 " blocking-silence samples for "
+ "%f to %f (%" PRId64 " to %" PRId64 ")",
+ this, track, toWrite, MediaTimeToSeconds(t), MediaTimeToSeconds(end),
+ offset, offset + toWrite));
+ } else {
+ TrackTime endTicksNeeded = offset + toWrite;
+ TrackTime endTicksAvailable = audio->GetDuration();
+
+ if (endTicksNeeded <= endTicksAvailable) {
+ LOG(LogLevel::Verbose,
+ ("%p: MediaTrack %p writing %" PRId64 " samples for %f to %f "
+ "(samples %" PRId64 " to %" PRId64 ")",
+ this, track, toWrite, MediaTimeToSeconds(t),
+ MediaTimeToSeconds(end), offset, endTicksNeeded));
+ output.AppendSlice(*audio, offset, endTicksNeeded);
+ ticksWritten += toWrite;
+ offset = endTicksNeeded;
+ } else {
+ // MOZ_ASSERT(track->IsEnded(), "Not enough data, and track not
+ // ended."); If we are at the end of the track, maybe write the
+ // remaining samples, and pad with/output silence.
+ if (endTicksNeeded > endTicksAvailable && offset < endTicksAvailable) {
+ output.AppendSlice(*audio, offset, endTicksAvailable);
+
+ LOG(LogLevel::Verbose,
+ ("%p: MediaTrack %p writing %" PRId64 " samples for %f to %f "
+ "(samples %" PRId64 " to %" PRId64 ")",
+ this, track, toWrite, MediaTimeToSeconds(t),
+ MediaTimeToSeconds(end), offset, endTicksNeeded));
+ uint32_t available = endTicksAvailable - offset;
+ ticksWritten += available;
+ toWrite -= available;
+ offset = endTicksAvailable;
+ }
+ output.AppendNullData(toWrite);
+ LOG(LogLevel::Verbose,
+ ("%p MediaTrack %p writing %" PRId64 " padding slsamples for %f to "
+ "%f (samples %" PRId64 " to %" PRId64 ")",
+ this, track, toWrite, MediaTimeToSeconds(t),
+ MediaTimeToSeconds(end), offset, endTicksNeeded));
+ ticksWritten += toWrite;
+ }
+ output.ApplyVolume(mGlobalVolume * aTkv.mVolume);
+ }
+ t = end;
+
+ uint32_t outputChannels;
+ // Use the number of channel the driver expects: this is the number of
+ // channel that can be output by the underlying system level audio stream.
+ // Fall back to something sensible if this graph is being driven by a normal
+ // thread (this can happen when there are no output devices, etc.).
+ if (CurrentDriver()->AsAudioCallbackDriver()) {
+ outputChannels =
+ CurrentDriver()->AsAudioCallbackDriver()->OutputChannelCount();
+ } else {
+ outputChannels = AudioOutputChannelCount();
+ }
+ output.WriteTo(*aMixer, outputChannels, mSampleRate);
+ }
+ return ticksWritten;
+}
+
+DeviceInputTrack* MediaTrackGraphImpl::GetDeviceInputTrackMainThread(
+ CubebUtils::AudioDeviceID aID) {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mDeviceInputTrackManagerMainThread.GetDeviceInputTrack(aID);
+}
+
+NativeInputTrack* MediaTrackGraphImpl::GetNativeInputTrackMainThread() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mDeviceInputTrackManagerMainThread.GetNativeInputTrack();
+}
+
+void MediaTrackGraphImpl::OpenAudioInputImpl(DeviceInputTrack* aTrack) {
+ MOZ_ASSERT(OnGraphThread());
+ LOG(LogLevel::Debug,
+ ("%p OpenAudioInputImpl: device %p", this, aTrack->mDeviceId));
+
+ mDeviceInputTrackManagerGraphThread.Add(aTrack);
+
+ if (aTrack->AsNativeInputTrack()) {
+ // Switch Drivers since we're adding input (to input-only or full-duplex)
+ AudioCallbackDriver* driver = new AudioCallbackDriver(
+ this, CurrentDriver(), mSampleRate, AudioOutputChannelCount(),
+ AudioInputChannelCount(aTrack->mDeviceId), mOutputDeviceID,
+ aTrack->mDeviceId, AudioInputDevicePreference(aTrack->mDeviceId));
+ LOG(LogLevel::Debug,
+ ("%p OpenAudioInputImpl: starting new AudioCallbackDriver(input) %p",
+ this, driver));
+ SwitchAtNextIteration(driver);
+ } else {
+ NonNativeInputTrack* nonNative = aTrack->AsNonNativeInputTrack();
+ MOZ_ASSERT(nonNative);
+ // Start non-native input right away.
+ nonNative->StartAudio(MakeRefPtr<AudioInputSource>(
+ MakeRefPtr<AudioInputSourceListener>(nonNative),
+ nonNative->GenerateSourceId(), nonNative->mDeviceId,
+ AudioInputChannelCount(nonNative->mDeviceId),
+ AudioInputDevicePreference(nonNative->mDeviceId) ==
+ AudioInputType::Voice,
+ nonNative->mPrincipalHandle, nonNative->mSampleRate, GraphRate(),
+ StaticPrefs::media_clockdrift_buffering()));
+ }
+}
+
+void MediaTrackGraphImpl::OpenAudioInput(DeviceInputTrack* aTrack) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aTrack);
+
+ LOG(LogLevel::Debug, ("%p OpenInput: DeviceInputTrack %p for device %p", this,
+ aTrack, aTrack->mDeviceId));
+
+ class Message : public ControlMessage {
+ public:
+ Message(MediaTrackGraphImpl* aGraph, DeviceInputTrack* aInputTrack)
+ : ControlMessage(nullptr), mGraph(aGraph), mInputTrack(aInputTrack) {}
+ void Run() override {
+ TRACE("MTG::OpenAudioInputImpl ControlMessage");
+ mGraph->OpenAudioInputImpl(mInputTrack);
+ }
+ MediaTrackGraphImpl* mGraph;
+ DeviceInputTrack* mInputTrack;
+ };
+
+ mDeviceInputTrackManagerMainThread.Add(aTrack);
+
+ this->AppendMessage(MakeUnique<Message>(this, aTrack));
+}
+
+void MediaTrackGraphImpl::CloseAudioInputImpl(DeviceInputTrack* aTrack) {
+ MOZ_ASSERT(OnGraphThread());
+
+ LOG(LogLevel::Debug,
+ ("%p CloseAudioInputImpl: device %p", this, aTrack->mDeviceId));
+
+ if (NonNativeInputTrack* nonNative = aTrack->AsNonNativeInputTrack()) {
+ nonNative->StopAudio();
+ mDeviceInputTrackManagerGraphThread.Remove(aTrack);
+ return;
+ }
+
+ MOZ_ASSERT(aTrack->AsNativeInputTrack());
+
+ mDeviceInputTrackManagerGraphThread.Remove(aTrack);
+
+ // Switch Drivers since we're adding or removing an input (to nothing/system
+ // or output only)
+ bool audioTrackPresent = AudioTrackPresent();
+
+ GraphDriver* driver;
+ if (audioTrackPresent) {
+ // We still have audio output
+ LOG(LogLevel::Debug,
+ ("%p: CloseInput: output present (AudioCallback)", this));
+
+ driver = new AudioCallbackDriver(
+ this, CurrentDriver(), mSampleRate, AudioOutputChannelCount(),
+ AudioInputChannelCount(aTrack->mDeviceId), mOutputDeviceID, nullptr,
+ AudioInputDevicePreference(aTrack->mDeviceId));
+ SwitchAtNextIteration(driver);
+ } else if (CurrentDriver()->AsAudioCallbackDriver()) {
+ LOG(LogLevel::Debug,
+ ("%p: CloseInput: no output present (SystemClockCallback)", this));
+
+ driver = new SystemClockDriver(this, CurrentDriver(), mSampleRate);
+ SwitchAtNextIteration(driver);
+ } // else SystemClockDriver->SystemClockDriver, no switch
+}
+
+void MediaTrackGraphImpl::RegisterAudioOutput(MediaTrack* aTrack, void* aKey) {
+ MOZ_ASSERT(OnGraphThread());
+ MOZ_ASSERT(!mAudioOutputs.Contains(TrackAndKey{aTrack, aKey}));
+
+ TrackKeyAndVolume* tkv = mAudioOutputs.AppendElement();
+ tkv->mTrack = aTrack;
+ tkv->mKey = aKey;
+ tkv->mVolume = 1.0;
+
+ if (!CurrentDriver()->AsAudioCallbackDriver() && !Switching()) {
+ NativeInputTrack* native =
+ mDeviceInputTrackManagerGraphThread.GetNativeInputTrack();
+ CubebUtils::AudioDeviceID inputDevice =
+ native ? native->mDeviceId : nullptr;
+ uint32_t inputChannelCount =
+ native ? AudioInputChannelCount(native->mDeviceId) : 0;
+ AudioInputType inputPreference =
+ native ? AudioInputDevicePreference(native->mDeviceId)
+ : AudioInputType::Unknown;
+
+ AudioCallbackDriver* driver = new AudioCallbackDriver(
+ this, CurrentDriver(), mSampleRate, AudioOutputChannelCount(),
+ inputChannelCount, mOutputDeviceID, inputDevice, inputPreference);
+ SwitchAtNextIteration(driver);
+ }
+}
+
+void MediaTrackGraphImpl::UnregisterAllAudioOutputs(MediaTrack* aTrack) {
+ MOZ_ASSERT(OnGraphThreadOrNotRunning());
+
+ mAudioOutputs.RemoveElementsBy([aTrack](const TrackKeyAndVolume& aTkv) {
+ return aTkv.mTrack == aTrack;
+ });
+}
+
+void MediaTrackGraphImpl::UnregisterAudioOutput(MediaTrack* aTrack,
+ void* aKey) {
+ MOZ_ASSERT(OnGraphThreadOrNotRunning());
+
+ DebugOnly<bool> removed =
+ mAudioOutputs.RemoveElement(TrackAndKey{aTrack, aKey});
+ MOZ_ASSERT(removed, "Audio output not found");
+}
+
+void MediaTrackGraphImpl::CloseAudioInput(DeviceInputTrack* aTrack) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aTrack);
+
+ LOG(LogLevel::Debug, ("%p CloseInput: DeviceInputTrack %p for device %p",
+ this, aTrack, aTrack->mDeviceId));
+
+ class Message : public ControlMessage {
+ public:
+ Message(MediaTrackGraphImpl* aGraph, DeviceInputTrack* aInputTrack)
+ : ControlMessage(nullptr), mGraph(aGraph), mInputTrack(aInputTrack) {}
+ void Run() override {
+ TRACE("MTG::CloseAudioInputImpl ControlMessage");
+ mGraph->CloseAudioInputImpl(mInputTrack);
+ }
+ MediaTrackGraphImpl* mGraph;
+ DeviceInputTrack* mInputTrack;
+ };
+
+ // DeviceInputTrack is still alive (in mTracks) even we remove it here, since
+ // aTrack->Destroy() is called after this. See DeviceInputTrack::CloseAudio
+ // for more details.
+ mDeviceInputTrackManagerMainThread.Remove(aTrack);
+
+ this->AppendMessage(MakeUnique<Message>(this, aTrack));
+
+ if (aTrack->AsNativeInputTrack()) {
+ LOG(LogLevel::Debug,
+ ("%p Native input device %p is closed!", this, aTrack->mDeviceId));
+ SetNewNativeInput();
+ }
+}
+
+// All AudioInput listeners get the same speaker data (at least for now).
+void MediaTrackGraphImpl::NotifyOutputData(AudioDataValue* aBuffer,
+ size_t aFrames, TrackRate aRate,
+ uint32_t aChannels) {
+ if (!mDeviceInputTrackManagerGraphThread.GetNativeInputTrack()) {
+ return;
+ }
+
+#if defined(MOZ_WEBRTC)
+ for (const auto& track : mTracks) {
+ if (const auto& t = track->AsAudioProcessingTrack()) {
+ t->NotifyOutputData(this, aBuffer, aFrames, aRate, aChannels);
+ }
+ }
+#endif
+}
+
+void MediaTrackGraphImpl::NotifyInputStopped() {
+ NativeInputTrack* native =
+ mDeviceInputTrackManagerGraphThread.GetNativeInputTrack();
+ if (!native) {
+ return;
+ }
+ native->NotifyInputStopped(this);
+}
+
+void MediaTrackGraphImpl::NotifyInputData(const AudioDataValue* aBuffer,
+ size_t aFrames, TrackRate aRate,
+ uint32_t aChannels,
+ uint32_t aAlreadyBuffered) {
+ // Either we have an audio input device, or we just removed the audio input
+ // this iteration, and we're switching back to an output-only driver next
+ // iteration.
+ NativeInputTrack* native =
+ mDeviceInputTrackManagerGraphThread.GetNativeInputTrack();
+ MOZ_ASSERT(native || Switching());
+ if (!native) {
+ return;
+ }
+ native->NotifyInputData(this, aBuffer, aFrames, aRate, aChannels,
+ aAlreadyBuffered);
+}
+
+void MediaTrackGraphImpl::DeviceChangedImpl() {
+ MOZ_ASSERT(OnGraphThread());
+ NativeInputTrack* native =
+ mDeviceInputTrackManagerGraphThread.GetNativeInputTrack();
+ if (!native) {
+ return;
+ }
+ native->DeviceChanged(this);
+}
+
+void MediaTrackGraphImpl::SetMaxOutputChannelCount(uint32_t aMaxChannelCount) {
+ MOZ_ASSERT(OnGraphThread());
+ mMaxOutputChannelCount = aMaxChannelCount;
+}
+
+void MediaTrackGraphImpl::DeviceChanged() {
+ // This is safe to be called from any thread: this message comes from an
+ // underlying platform API, and we don't have much guarantees. If it is not
+ // called from the main thread (and it probably will rarely be), it will post
+ // itself to the main thread, and the actual device change message will be ran
+ // and acted upon on the graph thread.
+ if (!NS_IsMainThread()) {
+ RefPtr<nsIRunnable> runnable = WrapRunnable(
+ RefPtr<MediaTrackGraphImpl>(this), &MediaTrackGraphImpl::DeviceChanged);
+ mMainThread->Dispatch(runnable.forget());
+ return;
+ }
+
+ class Message : public ControlMessage {
+ public:
+ explicit Message(MediaTrackGraph* aGraph)
+ : ControlMessage(nullptr),
+ mGraphImpl(static_cast<MediaTrackGraphImpl*>(aGraph)) {}
+ void Run() override {
+ TRACE("MTG::DeviceChangeImpl ControlMessage");
+ mGraphImpl->DeviceChangedImpl();
+ }
+ // We know that this is valid, because the graph can't shutdown if it has
+ // messages.
+ MediaTrackGraphImpl* mGraphImpl;
+ };
+
+ if (mMainThreadTrackCount == 0 && mMainThreadPortCount == 0) {
+ // This is a special case where the origin of this event cannot control the
+ // lifetime of the graph, because the graph is controling the lifetime of
+ // the AudioCallbackDriver where the event originated.
+ // We know the graph is soon going away, so there's no need to notify about
+ // this device change.
+ return;
+ }
+
+ // Reset the latency, it will get fetched again next time it's queried.
+ MOZ_ASSERT(NS_IsMainThread());
+ mAudioOutputLatency = 0.0;
+
+ // Dispatch to the bg thread to do the (potentially expensive) query of the
+ // maximum channel count, and then dispatch back to the main thread, then to
+ // the graph, with the new info.
+ RefPtr<MediaTrackGraphImpl> self = this;
+ NS_DispatchBackgroundTask(NS_NewRunnableFunction(
+ "MaxChannelCountUpdateOnBgThread", [self{std::move(self)}]() {
+ uint32_t maxChannelCount = CubebUtils::MaxNumberOfChannels();
+ self->Dispatch(NS_NewRunnableFunction(
+ "MaxChannelCountUpdateToMainThread",
+ [self{self}, maxChannelCount]() {
+ class MessageToGraph : public ControlMessage {
+ public:
+ explicit MessageToGraph(MediaTrackGraph* aGraph,
+ uint32_t aMaxChannelCount)
+ : ControlMessage(nullptr),
+ mGraphImpl(static_cast<MediaTrackGraphImpl*>(aGraph)),
+ mMaxChannelCount(aMaxChannelCount) {}
+ void Run() override {
+ TRACE("MTG::SetMaxOutputChannelCount ControlMessage")
+ mGraphImpl->SetMaxOutputChannelCount(mMaxChannelCount);
+ }
+ MediaTrackGraphImpl* mGraphImpl;
+ uint32_t mMaxChannelCount;
+ };
+ self->AppendMessage(
+ MakeUnique<MessageToGraph>(self, maxChannelCount));
+ }));
+ }));
+
+ AppendMessage(MakeUnique<Message>(this));
+}
+
+static const char* GetAudioInputTypeString(const AudioInputType& aType) {
+ return aType == AudioInputType::Voice ? "Voice" : "Unknown";
+}
+
+void MediaTrackGraphImpl::ReevaluateInputDevice(CubebUtils::AudioDeviceID aID) {
+ MOZ_ASSERT(OnGraphThread());
+
+ LOG(LogLevel::Debug, ("%p: ReevaluateInputDevice: device %p", this, aID));
+
+ DeviceInputTrack* track =
+ mDeviceInputTrackManagerGraphThread.GetDeviceInputTrack(aID);
+ if (!track) {
+ LOG(LogLevel::Debug,
+ ("%p: No DeviceInputTrack for this device. Ignore", this));
+ return;
+ }
+
+ bool needToSwitch = false;
+
+ if (NonNativeInputTrack* nonNative = track->AsNonNativeInputTrack()) {
+ if (nonNative->NumberOfChannels() != AudioInputChannelCount(aID)) {
+ LOG(LogLevel::Debug,
+ ("%p: %u-channel non-native input device %p (track %p) is "
+ "re-configured to %d-channel",
+ this, nonNative->NumberOfChannels(), aID, track,
+ AudioInputChannelCount(aID)));
+ needToSwitch = true;
+ }
+ if (nonNative->DevicePreference() != AudioInputDevicePreference(aID)) {
+ LOG(LogLevel::Debug,
+ ("%p: %s-type non-native input device %p (track %p) is re-configured "
+ "to %s-type",
+ this, GetAudioInputTypeString(nonNative->DevicePreference()), aID,
+ track, GetAudioInputTypeString(AudioInputDevicePreference(aID))));
+ needToSwitch = true;
+ }
+
+ if (needToSwitch) {
+ nonNative->StopAudio();
+ nonNative->StartAudio(MakeRefPtr<AudioInputSource>(
+ MakeRefPtr<AudioInputSourceListener>(nonNative),
+ nonNative->GenerateSourceId(), aID, AudioInputChannelCount(aID),
+ AudioInputDevicePreference(aID) == AudioInputType::Voice,
+ nonNative->mPrincipalHandle, nonNative->mSampleRate, GraphRate(),
+ StaticPrefs::media_clockdrift_buffering()));
+ }
+
+ return;
+ }
+
+ MOZ_ASSERT(track->AsNativeInputTrack());
+
+ if (AudioCallbackDriver* audioCallbackDriver =
+ CurrentDriver()->AsAudioCallbackDriver()) {
+ if (audioCallbackDriver->InputChannelCount() !=
+ AudioInputChannelCount(aID)) {
+ LOG(LogLevel::Debug,
+ ("%p: ReevaluateInputDevice: %u-channel AudioCallbackDriver %p is "
+ "re-configured to %d-channel",
+ this, audioCallbackDriver->InputChannelCount(), audioCallbackDriver,
+ AudioInputChannelCount(aID)));
+ needToSwitch = true;
+ }
+ if (audioCallbackDriver->InputDevicePreference() !=
+ AudioInputDevicePreference(aID)) {
+ LOG(LogLevel::Debug,
+ ("%p: ReevaluateInputDevice: %s-type AudioCallbackDriver %p is "
+ "re-configured to %s-type",
+ this,
+ GetAudioInputTypeString(
+ audioCallbackDriver->InputDevicePreference()),
+ audioCallbackDriver,
+ GetAudioInputTypeString(AudioInputDevicePreference(aID))));
+ needToSwitch = true;
+ }
+ } else if (Switching() && NextDriver()->AsAudioCallbackDriver()) {
+ // We're already in the process of switching to a audio callback driver,
+ // which will happen at the next iteration.
+ // However, maybe it's not the correct number of channels. Re-query the
+ // correct channel amount at this time.
+ needToSwitch = true;
+ }
+
+ if (needToSwitch) {
+ AudioCallbackDriver* newDriver = new AudioCallbackDriver(
+ this, CurrentDriver(), mSampleRate, AudioOutputChannelCount(),
+ AudioInputChannelCount(aID), mOutputDeviceID, aID,
+ AudioInputDevicePreference(aID));
+ SwitchAtNextIteration(newDriver);
+ }
+}
+
+bool MediaTrackGraphImpl::OnGraphThreadOrNotRunning() const {
+ // either we're on the right thread (and calling CurrentDriver() is safe),
+ // or we're going to fail the assert anyway, so don't cross-check
+ // via CurrentDriver().
+ return mGraphDriverRunning ? OnGraphThread() : NS_IsMainThread();
+}
+
+bool MediaTrackGraphImpl::OnGraphThread() const {
+ // we're on the right thread (and calling mDriver is safe),
+ MOZ_ASSERT(mDriver);
+ if (mGraphRunner && mGraphRunner->OnThread()) {
+ return true;
+ }
+ return mDriver->OnThread();
+}
+
+bool MediaTrackGraphImpl::Destroyed() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return !mSelfRef;
+}
+
+bool MediaTrackGraphImpl::ShouldUpdateMainThread() {
+ MOZ_ASSERT(OnGraphThreadOrNotRunning());
+ if (mRealtime) {
+ return true;
+ }
+
+ TimeStamp now = TimeStamp::Now();
+ // For offline graphs, update now if it has been long enough since the last
+ // update, or if it has reached the end.
+ if ((now - mLastMainThreadUpdate).ToMilliseconds() >
+ CurrentDriver()->IterationDuration() ||
+ mStateComputedTime >= mEndTime) {
+ mLastMainThreadUpdate = now;
+ return true;
+ }
+ return false;
+}
+
+void MediaTrackGraphImpl::PrepareUpdatesToMainThreadState(bool aFinalUpdate) {
+ MOZ_ASSERT(OnGraphThreadOrNotRunning());
+ mMonitor.AssertCurrentThreadOwns();
+
+ // We don't want to frequently update the main thread about timing update
+ // when we are not running in realtime.
+ if (aFinalUpdate || ShouldUpdateMainThread()) {
+ // Strip updates that will be obsoleted below, so as to keep the length of
+ // mTrackUpdates sane.
+ size_t keptUpdateCount = 0;
+ for (size_t i = 0; i < mTrackUpdates.Length(); ++i) {
+ MediaTrack* track = mTrackUpdates[i].mTrack;
+ // RemoveTrackGraphThread() clears mTrack in updates for
+ // tracks that are removed from the graph.
+ MOZ_ASSERT(!track || track->GraphImpl() == this);
+ if (!track || track->MainThreadNeedsUpdates()) {
+ // Discard this update as it has either been cleared when the track
+ // was destroyed or there will be a newer update below.
+ continue;
+ }
+ if (keptUpdateCount != i) {
+ mTrackUpdates[keptUpdateCount] = std::move(mTrackUpdates[i]);
+ MOZ_ASSERT(!mTrackUpdates[i].mTrack);
+ }
+ ++keptUpdateCount;
+ }
+ mTrackUpdates.TruncateLength(keptUpdateCount);
+
+ mTrackUpdates.SetCapacity(mTrackUpdates.Length() + mTracks.Length() +
+ mSuspendedTracks.Length());
+ for (MediaTrack* track : AllTracks()) {
+ if (!track->MainThreadNeedsUpdates()) {
+ continue;
+ }
+ TrackUpdate* update = mTrackUpdates.AppendElement();
+ update->mTrack = track;
+ // No blocking to worry about here, since we've passed
+ // UpdateCurrentTimeForTracks.
+ update->mNextMainThreadCurrentTime =
+ track->GraphTimeToTrackTime(mProcessedTime);
+ update->mNextMainThreadEnded = track->mNotifiedEnded;
+ }
+ mNextMainThreadGraphTime = mProcessedTime;
+ if (!mPendingUpdateRunnables.IsEmpty()) {
+ mUpdateRunnables.AppendElements(std::move(mPendingUpdateRunnables));
+ }
+ }
+
+ // If this is the final update, then a stable state event will soon be
+ // posted just before this thread finishes, and so there is no need to also
+ // post here.
+ if (!aFinalUpdate &&
+ // Don't send the message to the main thread if it's not going to have
+ // any work to do.
+ !(mUpdateRunnables.IsEmpty() && mTrackUpdates.IsEmpty())) {
+ EnsureStableStateEventPosted();
+ }
+}
+
+GraphTime MediaTrackGraphImpl::RoundUpToEndOfAudioBlock(GraphTime aTime) {
+ if (aTime % WEBAUDIO_BLOCK_SIZE == 0) {
+ return aTime;
+ }
+ return RoundUpToNextAudioBlock(aTime);
+}
+
+GraphTime MediaTrackGraphImpl::RoundUpToNextAudioBlock(GraphTime aTime) {
+ uint64_t block = aTime >> WEBAUDIO_BLOCK_SIZE_BITS;
+ uint64_t nextBlock = block + 1;
+ GraphTime nextTime = nextBlock << WEBAUDIO_BLOCK_SIZE_BITS;
+ return nextTime;
+}
+
+void MediaTrackGraphImpl::ProduceDataForTracksBlockByBlock(
+ uint32_t aTrackIndex, TrackRate aSampleRate) {
+ MOZ_ASSERT(OnGraphThread());
+ MOZ_ASSERT(aTrackIndex <= mFirstCycleBreaker,
+ "Cycle breaker is not AudioNodeTrack?");
+
+ while (mProcessedTime < mStateComputedTime) {
+ // Microtask checkpoints are in between render quanta.
+ nsAutoMicroTask mt;
+
+ GraphTime next = RoundUpToNextAudioBlock(mProcessedTime);
+ for (uint32_t i = mFirstCycleBreaker; i < mTracks.Length(); ++i) {
+ auto nt = static_cast<AudioNodeTrack*>(mTracks[i]);
+ MOZ_ASSERT(nt->AsAudioNodeTrack());
+ nt->ProduceOutputBeforeInput(mProcessedTime);
+ }
+ for (uint32_t i = aTrackIndex; i < mTracks.Length(); ++i) {
+ ProcessedMediaTrack* pt = mTracks[i]->AsProcessedTrack();
+ if (pt) {
+ pt->ProcessInput(
+ mProcessedTime, next,
+ (next == mStateComputedTime) ? ProcessedMediaTrack::ALLOW_END : 0);
+ }
+ }
+ mProcessedTime = next;
+ }
+ NS_ASSERTION(mProcessedTime == mStateComputedTime,
+ "Something went wrong with rounding to block boundaries");
+}
+
+void MediaTrackGraphImpl::RunMessageAfterProcessing(
+ UniquePtr<ControlMessage> aMessage) {
+ MOZ_ASSERT(OnGraphThread());
+
+ if (mFrontMessageQueue.IsEmpty()) {
+ mFrontMessageQueue.AppendElement();
+ }
+
+ // Only one block is used for messages from the graph thread.
+ MOZ_ASSERT(mFrontMessageQueue.Length() == 1);
+ mFrontMessageQueue[0].mMessages.AppendElement(std::move(aMessage));
+}
+
+void MediaTrackGraphImpl::RunMessagesInQueue() {
+ TRACE("MTG::RunMessagesInQueue");
+ MOZ_ASSERT(OnGraphThread());
+ // Calculate independent action times for each batch of messages (each
+ // batch corresponding to an event loop task). This isolates the performance
+ // of different scripts to some extent.
+ for (uint32_t i = 0; i < mFrontMessageQueue.Length(); ++i) {
+ nsTArray<UniquePtr<ControlMessage>>& messages =
+ mFrontMessageQueue[i].mMessages;
+
+ for (uint32_t j = 0; j < messages.Length(); ++j) {
+ TRACE("ControlMessage::Run");
+ messages[j]->Run();
+ }
+ }
+ mFrontMessageQueue.Clear();
+}
+
+void MediaTrackGraphImpl::UpdateGraph(GraphTime aEndBlockingDecisions) {
+ TRACE("MTG::UpdateGraph");
+ MOZ_ASSERT(OnGraphThread());
+ MOZ_ASSERT(aEndBlockingDecisions >= mProcessedTime);
+ // The next state computed time can be the same as the previous: it
+ // means the driver would have been blocking indefinitly, but the graph has
+ // been woken up right after having been to sleep.
+ MOZ_ASSERT(aEndBlockingDecisions >= mStateComputedTime);
+
+ CheckDriver();
+ UpdateTrackOrder();
+
+ // Always do another iteration if there are tracks waiting to resume.
+ bool ensureNextIteration = !mPendingResumeOperations.IsEmpty();
+
+ for (MediaTrack* track : mTracks) {
+ if (SourceMediaTrack* is = track->AsSourceTrack()) {
+ ensureNextIteration |= is->PullNewData(aEndBlockingDecisions);
+ is->ExtractPendingInput(mStateComputedTime, aEndBlockingDecisions);
+ }
+ if (track->mEnded) {
+ // The track's not suspended, and since it's ended, underruns won't
+ // stop it playing out. So there's no blocking other than what we impose
+ // here.
+ GraphTime endTime = track->GetEnd() + track->mStartTime;
+ if (endTime <= mStateComputedTime) {
+ LOG(LogLevel::Verbose,
+ ("%p: MediaTrack %p is blocked due to being ended", this, track));
+ track->mStartBlocking = mStateComputedTime;
+ } else {
+ LOG(LogLevel::Verbose,
+ ("%p: MediaTrack %p has ended, but is not blocked yet (current "
+ "time %f, end at %f)",
+ this, track, MediaTimeToSeconds(mStateComputedTime),
+ MediaTimeToSeconds(endTime)));
+ // Data can't be added to a ended track, so underruns are irrelevant.
+ MOZ_ASSERT(endTime <= aEndBlockingDecisions);
+ track->mStartBlocking = endTime;
+ }
+ } else {
+ track->mStartBlocking = WillUnderrun(track, aEndBlockingDecisions);
+
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ if (SourceMediaTrack* s = track->AsSourceTrack()) {
+ if (s->Ended()) {
+ continue;
+ }
+ {
+ MutexAutoLock lock(s->mMutex);
+ if (!s->mUpdateTrack->mPullingEnabled) {
+ // The invariant that data must be provided is only enforced when
+ // pulling.
+ continue;
+ }
+ }
+ if (track->GetEnd() <
+ track->GraphTimeToTrackTime(aEndBlockingDecisions)) {
+ LOG(LogLevel::Error,
+ ("%p: SourceMediaTrack %p (%s) is live and pulled, "
+ "but wasn't fed "
+ "enough data. TrackListeners=%zu. Track-end=%f, "
+ "Iteration-end=%f",
+ this, track,
+ (track->mType == MediaSegment::AUDIO ? "audio" : "video"),
+ track->mTrackListeners.Length(),
+ MediaTimeToSeconds(track->GetEnd()),
+ MediaTimeToSeconds(
+ track->GraphTimeToTrackTime(aEndBlockingDecisions))));
+ MOZ_DIAGNOSTIC_ASSERT(false,
+ "A non-ended SourceMediaTrack wasn't fed "
+ "enough data by NotifyPull");
+ }
+ }
+#endif /* MOZ_DIAGNOSTIC_ASSERT_ENABLED */
+ }
+ }
+
+ for (MediaTrack* track : mSuspendedTracks) {
+ track->mStartBlocking = mStateComputedTime;
+ }
+
+ // If the loop is woken up so soon that IterationEnd() barely advances or
+ // if an offline graph is not currently rendering, we end up having
+ // aEndBlockingDecisions == mStateComputedTime.
+ // Since the process interval [mStateComputedTime, aEndBlockingDecision) is
+ // empty, Process() will not find any unblocked track and so will not
+ // ensure another iteration. If the graph should be rendering, then ensure
+ // another iteration to render.
+ if (ensureNextIteration || (aEndBlockingDecisions == mStateComputedTime &&
+ mStateComputedTime < mEndTime)) {
+ EnsureNextIteration();
+ }
+}
+
+void MediaTrackGraphImpl::Process(AudioMixer* aMixer) {
+ TRACE("MTG::Process");
+ MOZ_ASSERT(OnGraphThread());
+ // Play track contents.
+ bool allBlockedForever = true;
+ // True when we've done ProcessInput for all processed tracks.
+ bool doneAllProducing = false;
+ const GraphTime oldProcessedTime = mProcessedTime;
+
+ // Figure out what each track wants to do
+ for (uint32_t i = 0; i < mTracks.Length(); ++i) {
+ MediaTrack* track = mTracks[i];
+ if (!doneAllProducing) {
+ ProcessedMediaTrack* pt = track->AsProcessedTrack();
+ if (pt) {
+ AudioNodeTrack* n = track->AsAudioNodeTrack();
+ if (n) {
+#ifdef DEBUG
+ // Verify that the sampling rate for all of the following tracks is
+ // the same
+ for (uint32_t j = i + 1; j < mTracks.Length(); ++j) {
+ AudioNodeTrack* nextTrack = mTracks[j]->AsAudioNodeTrack();
+ if (nextTrack) {
+ MOZ_ASSERT(n->mSampleRate == nextTrack->mSampleRate,
+ "All AudioNodeTracks in the graph must have the same "
+ "sampling rate");
+ }
+ }
+#endif
+ // Since an AudioNodeTrack is present, go ahead and
+ // produce audio block by block for all the rest of the tracks.
+ ProduceDataForTracksBlockByBlock(i, n->mSampleRate);
+ doneAllProducing = true;
+ } else {
+ pt->ProcessInput(mProcessedTime, mStateComputedTime,
+ ProcessedMediaTrack::ALLOW_END);
+ // Assert that a live track produced enough data
+ MOZ_ASSERT_IF(!track->mEnded,
+ track->GetEnd() >= GraphTimeToTrackTimeWithBlocking(
+ track, mStateComputedTime));
+ }
+ }
+ }
+ if (track->mStartBlocking > oldProcessedTime) {
+ allBlockedForever = false;
+ }
+ }
+ mProcessedTime = mStateComputedTime;
+
+ if (aMixer) {
+ MOZ_ASSERT(mRealtime, "If there's a mixer, this graph must be realtime");
+ aMixer->StartMixing();
+ // This is the number of frames that are written to the output buffer, for
+ // this iteration.
+ TrackTime ticksPlayed = 0;
+ for (auto& t : mAudioOutputs) {
+ TrackTime ticksPlayedForThisTrack =
+ PlayAudio(aMixer, t, oldProcessedTime);
+ if (ticksPlayed == 0) {
+ ticksPlayed = ticksPlayedForThisTrack;
+ } else {
+ MOZ_ASSERT(
+ !ticksPlayedForThisTrack || ticksPlayedForThisTrack == ticksPlayed,
+ "Each track should have the same number of frames.");
+ }
+ }
+
+ if (ticksPlayed == 0) {
+ // Nothing was played, so the mixer doesn't know how many frames were
+ // processed. We still tell it so AudioCallbackDriver knows how much has
+ // been processed. (bug 1406027)
+ aMixer->Mix(
+ nullptr,
+ CurrentDriver()->AsAudioCallbackDriver()->OutputChannelCount(),
+ mStateComputedTime - oldProcessedTime, mSampleRate);
+ }
+ aMixer->FinishMixing();
+ }
+
+ if (!allBlockedForever) {
+ EnsureNextIteration();
+ }
+}
+
+bool MediaTrackGraphImpl::UpdateMainThreadState() {
+ MOZ_ASSERT(OnGraphThread());
+ if (mForceShutDownReceived) {
+ for (MediaTrack* track : AllTracks()) {
+ track->OnGraphThreadDone();
+ }
+ }
+ {
+ MonitorAutoLock lock(mMonitor);
+ bool finalUpdate =
+ mForceShutDownReceived || (IsEmpty() && mBackMessageQueue.IsEmpty());
+ PrepareUpdatesToMainThreadState(finalUpdate);
+ if (!finalUpdate) {
+ SwapMessageQueues();
+ return true;
+ }
+ // The JSContext will not be used again.
+ // Clear main thread access while under monitor.
+ mJSContext = nullptr;
+ }
+ dom::WorkletThread::DeleteCycleCollectedJSContext();
+ // Enter shutdown mode when this iteration is completed.
+ // No need to Destroy tracks here. The main-thread owner of each
+ // track is responsible for calling Destroy on them.
+ return false;
+}
+
+auto MediaTrackGraphImpl::OneIteration(GraphTime aStateTime,
+ GraphTime aIterationEnd,
+ AudioMixer* aMixer) -> IterationResult {
+ if (mGraphRunner) {
+ return mGraphRunner->OneIteration(aStateTime, aIterationEnd, aMixer);
+ }
+
+ return OneIterationImpl(aStateTime, aIterationEnd, aMixer);
+}
+
+auto MediaTrackGraphImpl::OneIterationImpl(GraphTime aStateTime,
+ GraphTime aIterationEnd,
+ AudioMixer* aMixer)
+ -> IterationResult {
+ TRACE("MTG::OneIterationImpl");
+
+ mIterationEndTime = aIterationEnd;
+
+ if (SoftRealTimeLimitReached()) {
+ TRACE("MTG::Demoting real-time thread!");
+ DemoteThreadFromRealTime();
+ }
+
+ // Changes to LIFECYCLE_RUNNING occur before starting or reviving the graph
+ // thread, and so the monitor need not be held to check mLifecycleState.
+ // LIFECYCLE_THREAD_NOT_STARTED is possible when shutting down offline
+ // graphs that have not started.
+
+ // While changes occur on mainthread, this assert confirms that
+ // this code shouldn't run if mainthread might be changing the state (to
+ // > LIFECYCLE_RUNNING)
+
+ // Ignore mutex warning: static during execution of the graph
+ MOZ_PUSH_IGNORE_THREAD_SAFETY
+ MOZ_DIAGNOSTIC_ASSERT(mLifecycleState <= LIFECYCLE_RUNNING);
+ MOZ_POP_THREAD_SAFETY
+
+ MOZ_ASSERT(OnGraphThread());
+
+ WebCore::DenormalDisabler disabler;
+
+ // Process graph message from the main thread for this iteration.
+ RunMessagesInQueue();
+
+ // Process MessagePort events.
+ // These require a single thread, which has an nsThread with an event queue.
+ if (mGraphRunner || !mRealtime) {
+ TRACE("MTG::MessagePort events");
+ NS_ProcessPendingEvents(nullptr);
+ }
+
+ GraphTime stateTime = std::min(aStateTime, GraphTime(mEndTime));
+ UpdateGraph(stateTime);
+
+ mStateComputedTime = stateTime;
+
+ GraphTime oldProcessedTime = mProcessedTime;
+ Process(aMixer);
+ MOZ_ASSERT(mProcessedTime == stateTime);
+
+ UpdateCurrentTimeForTracks(oldProcessedTime);
+
+ ProcessChunkMetadata(oldProcessedTime);
+
+ // Process graph messages queued from RunMessageAfterProcessing() on this
+ // thread during the iteration.
+ RunMessagesInQueue();
+
+ if (!UpdateMainThreadState()) {
+ if (Switching()) {
+ // We'll never get to do this switch. Clear mNextDriver to break the
+ // ref-cycle graph->nextDriver->currentDriver->graph.
+ SwitchAtNextIteration(nullptr);
+ }
+ return IterationResult::CreateStop(
+ NewRunnableMethod("MediaTrackGraphImpl::SignalMainThreadCleanup", this,
+ &MediaTrackGraphImpl::SignalMainThreadCleanup));
+ }
+
+ if (Switching()) {
+ RefPtr<GraphDriver> nextDriver = std::move(mNextDriver);
+ return IterationResult::CreateSwitchDriver(
+ nextDriver, NewRunnableMethod<StoreRefPtrPassByPtr<GraphDriver>>(
+ "MediaTrackGraphImpl::SetCurrentDriver", this,
+ &MediaTrackGraphImpl::SetCurrentDriver, nextDriver));
+ }
+
+ return IterationResult::CreateStillProcessing();
+}
+
+void MediaTrackGraphImpl::ApplyTrackUpdate(TrackUpdate* aUpdate) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mMonitor.AssertCurrentThreadOwns();
+
+ MediaTrack* track = aUpdate->mTrack;
+ if (!track) return;
+ track->mMainThreadCurrentTime = aUpdate->mNextMainThreadCurrentTime;
+ track->mMainThreadEnded = aUpdate->mNextMainThreadEnded;
+
+ if (track->ShouldNotifyTrackEnded()) {
+ track->NotifyMainThreadListeners();
+ }
+}
+
+void MediaTrackGraphImpl::ForceShutDown() {
+ MOZ_ASSERT(NS_IsMainThread(), "Must be called on main thread");
+ LOG(LogLevel::Debug, ("%p: MediaTrackGraph::ForceShutdown", this));
+
+ if (mShutdownBlocker) {
+ // Avoid waiting forever for a graph to shut down
+ // synchronously. Reports are that some 3rd-party audio drivers
+ // occasionally hang in shutdown (both for us and Chrome).
+ NS_NewTimerWithCallback(
+ getter_AddRefs(mShutdownTimer), this,
+ MediaTrackGraph::AUDIO_CALLBACK_DRIVER_SHUTDOWN_TIMEOUT,
+ nsITimer::TYPE_ONE_SHOT);
+ }
+
+ class Message final : public ControlMessage {
+ public:
+ explicit Message(MediaTrackGraphImpl* aGraph)
+ : ControlMessage(nullptr), mGraph(aGraph) {}
+ void Run() override {
+ TRACE("MTG::ForceShutdown ControlMessage");
+ mGraph->mForceShutDownReceived = true;
+ }
+ // The graph owns this message.
+ MediaTrackGraphImpl* MOZ_NON_OWNING_REF mGraph;
+ };
+
+ if (mMainThreadTrackCount > 0 || mMainThreadPortCount > 0) {
+ // If both the track and port counts are zero, the regular shutdown
+ // sequence will progress shortly to shutdown threads and destroy the graph.
+ AppendMessage(MakeUnique<Message>(this));
+ InterruptJS();
+ }
+}
+
+NS_IMETHODIMP
+MediaTrackGraphImpl::Notify(nsITimer* aTimer) {
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ASSERTION(!mShutdownBlocker,
+ "MediaTrackGraph took too long to shut down!");
+ // Sigh, graph took too long to shut down. Stop blocking system
+ // shutdown and hope all is well.
+ RemoveShutdownBlocker();
+ return NS_OK;
+}
+
+bool MediaTrackGraphImpl::AddShutdownBlocker() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!mShutdownBlocker);
+
+ class Blocker : public media::ShutdownBlocker {
+ const RefPtr<MediaTrackGraphImpl> mGraph;
+
+ public:
+ Blocker(MediaTrackGraphImpl* aGraph, const nsString& aName)
+ : media::ShutdownBlocker(aName), mGraph(aGraph) {}
+
+ NS_IMETHOD
+ BlockShutdown(nsIAsyncShutdownClient* aProfileBeforeChange) override {
+ mGraph->ForceShutDown();
+ return NS_OK;
+ }
+ };
+
+ nsCOMPtr<nsIAsyncShutdownClient> barrier = media::GetShutdownBarrier();
+ if (!barrier) {
+ // We're already shutting down, we won't be able to add a blocker, bail.
+ LOG(LogLevel::Error,
+ ("%p: Couldn't get shutdown barrier, won't add shutdown blocker",
+ this));
+ return false;
+ }
+
+ // Blocker names must be distinct.
+ nsString blockerName;
+ blockerName.AppendPrintf("MediaTrackGraph %p shutdown", this);
+ mShutdownBlocker = MakeAndAddRef<Blocker>(this, blockerName);
+ nsresult rv = barrier->AddBlocker(mShutdownBlocker,
+ NS_LITERAL_STRING_FROM_CSTRING(__FILE__),
+ __LINE__, u"MediaTrackGraph shutdown"_ns);
+ MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
+ return true;
+}
+
+void MediaTrackGraphImpl::RemoveShutdownBlocker() {
+ if (!mShutdownBlocker) {
+ return;
+ }
+ media::MustGetShutdownBarrier()->RemoveBlocker(mShutdownBlocker);
+ mShutdownBlocker = nullptr;
+}
+
+NS_IMETHODIMP
+MediaTrackGraphImpl::GetName(nsACString& aName) {
+ aName.AssignLiteral("MediaTrackGraphImpl");
+ return NS_OK;
+}
+
+namespace {
+
+class MediaTrackGraphShutDownRunnable : public Runnable {
+ public:
+ explicit MediaTrackGraphShutDownRunnable(MediaTrackGraphImpl* aGraph)
+ : Runnable("MediaTrackGraphShutDownRunnable"), mGraph(aGraph) {}
+ // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT.
+ // See bug 1535398.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override {
+ TRACE("MTG::MediaTrackGraphShutDownRunnable runnable");
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!mGraph->mGraphDriverRunning && mGraph->mDriver,
+ "We should know the graph thread control loop isn't running!");
+
+ LOG(LogLevel::Debug, ("%p: Shutting down graph", mGraph.get()));
+
+ // We've asserted the graph isn't running. Use mDriver instead of
+ // CurrentDriver to avoid thread-safety checks
+#if 0 // AudioCallbackDrivers are released asynchronously anyways
+ // XXX a better test would be have setting mGraphDriverRunning make sure
+ // any current callback has finished and block future ones -- or just
+ // handle it all in Shutdown()!
+ if (mGraph->mDriver->AsAudioCallbackDriver()) {
+ MOZ_ASSERT(!mGraph->mDriver->AsAudioCallbackDriver()->InCallback());
+ }
+#endif
+
+ for (MediaTrackGraphImpl::PendingResumeOperation& op :
+ mGraph->mPendingResumeOperations) {
+ op.Abort();
+ }
+
+ if (mGraph->mGraphRunner) {
+ RefPtr<GraphRunner>(mGraph->mGraphRunner)->Shutdown();
+ }
+
+ RefPtr<GraphDriver>(mGraph->mDriver)->Shutdown();
+
+ // Release the driver now so that an AudioCallbackDriver will release its
+ // SharedThreadPool reference. Each SharedThreadPool reference must be
+ // released before SharedThreadPool::SpinUntilEmpty() runs on
+ // xpcom-shutdown-threads. Don't wait for GC/CC to release references to
+ // objects owning tracks, or for expiration of mGraph->mShutdownTimer,
+ // which won't otherwise release its reference on the graph until
+ // nsTimerImpl::Shutdown(), which runs after xpcom-shutdown-threads.
+ mGraph->SetCurrentDriver(nullptr);
+
+ // Safe to access these without the monitor since the graph isn't running.
+ // We may be one of several graphs. Drop ticket to eventually unblock
+ // shutdown.
+ if (mGraph->mShutdownTimer && !mGraph->mShutdownBlocker) {
+ MOZ_ASSERT(
+ false,
+ "AudioCallbackDriver took too long to shut down and we let shutdown"
+ " continue - freezing and leaking");
+
+ // The timer fired, so we may be deeper in shutdown now. Block any
+ // further teardown and just leak, for safety.
+ return NS_OK;
+ }
+
+ // mGraph's thread is not running so it's OK to do whatever here
+ for (MediaTrack* track : mGraph->AllTracks()) {
+ // Clean up all MediaSegments since we cannot release Images too
+ // late during shutdown. Also notify listeners that they were removed
+ // so they can clean up any gfx resources.
+ track->RemoveAllResourcesAndListenersImpl();
+ }
+
+#ifdef DEBUG
+ {
+ MonitorAutoLock lock(mGraph->mMonitor);
+ MOZ_ASSERT(mGraph->mUpdateRunnables.IsEmpty());
+ }
+#endif
+ mGraph->mPendingUpdateRunnables.Clear();
+
+ mGraph->RemoveShutdownBlocker();
+
+ // We can't block past the final LIFECYCLE_WAITING_FOR_TRACK_DESTRUCTION
+ // stage, since completion of that stage requires all tracks to be freed,
+ // which requires shutdown to proceed.
+
+ if (mGraph->IsEmpty()) {
+ // mGraph is no longer needed, so delete it.
+ mGraph->Destroy();
+ } else {
+ // The graph is not empty. We must be in a forced shutdown.
+ // Some later AppendMessage will detect that the graph has
+ // been emptied, and delete it.
+ NS_ASSERTION(mGraph->mForceShutDownReceived, "Not in forced shutdown?");
+ mGraph->LifecycleStateRef() =
+ MediaTrackGraphImpl::LIFECYCLE_WAITING_FOR_TRACK_DESTRUCTION;
+ }
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<MediaTrackGraphImpl> mGraph;
+};
+
+class MediaTrackGraphStableStateRunnable : public Runnable {
+ public:
+ explicit MediaTrackGraphStableStateRunnable(MediaTrackGraphImpl* aGraph,
+ bool aSourceIsMTG)
+ : Runnable("MediaTrackGraphStableStateRunnable"),
+ mGraph(aGraph),
+ mSourceIsMTG(aSourceIsMTG) {}
+ NS_IMETHOD Run() override {
+ TRACE("MTG::MediaTrackGraphStableStateRunnable ControlMessage");
+ if (mGraph) {
+ mGraph->RunInStableState(mSourceIsMTG);
+ }
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<MediaTrackGraphImpl> mGraph;
+ bool mSourceIsMTG;
+};
+
+/*
+ * Control messages forwarded from main thread to graph manager thread
+ */
+class CreateMessage : public ControlMessage {
+ public:
+ explicit CreateMessage(MediaTrack* aTrack) : ControlMessage(aTrack) {}
+ void Run() override {
+ TRACE("MTG::AddTrackGraphThread ControlMessage");
+ mTrack->GraphImpl()->AddTrackGraphThread(mTrack);
+ }
+ void RunDuringShutdown() override {
+ // Make sure to run this message during shutdown too, to make sure
+ // that we balance the number of tracks registered with the graph
+ // as they're destroyed during shutdown.
+ Run();
+ }
+};
+
+} // namespace
+
+void MediaTrackGraphImpl::RunInStableState(bool aSourceIsMTG) {
+ MOZ_ASSERT(NS_IsMainThread(), "Must be called on main thread");
+
+ nsTArray<nsCOMPtr<nsIRunnable>> runnables;
+ // When we're doing a forced shutdown, pending control messages may be
+ // run on the main thread via RunDuringShutdown. Those messages must
+ // run without the graph monitor being held. So, we collect them here.
+ nsTArray<UniquePtr<ControlMessage>> controlMessagesToRunDuringShutdown;
+
+ {
+ MonitorAutoLock lock(mMonitor);
+ if (aSourceIsMTG) {
+ MOZ_ASSERT(mPostedRunInStableStateEvent);
+ mPostedRunInStableStateEvent = false;
+ }
+
+ // This should be kept in sync with the LifecycleState enum in
+ // MediaTrackGraphImpl.h
+ const char* LifecycleState_str[] = {
+ "LIFECYCLE_THREAD_NOT_STARTED", "LIFECYCLE_RUNNING",
+ "LIFECYCLE_WAITING_FOR_MAIN_THREAD_CLEANUP",
+ "LIFECYCLE_WAITING_FOR_THREAD_SHUTDOWN",
+ "LIFECYCLE_WAITING_FOR_TRACK_DESTRUCTION"};
+
+ if (LifecycleStateRef() != LIFECYCLE_RUNNING) {
+ LOG(LogLevel::Debug,
+ ("%p: Running stable state callback. Current state: %s", this,
+ LifecycleState_str[LifecycleStateRef()]));
+ }
+
+ runnables = std::move(mUpdateRunnables);
+ for (uint32_t i = 0; i < mTrackUpdates.Length(); ++i) {
+ TrackUpdate* update = &mTrackUpdates[i];
+ if (update->mTrack) {
+ ApplyTrackUpdate(update);
+ }
+ }
+ mTrackUpdates.Clear();
+
+ mMainThreadGraphTime = mNextMainThreadGraphTime;
+
+ if (mCurrentTaskMessageQueue.IsEmpty()) {
+ if (LifecycleStateRef() == LIFECYCLE_WAITING_FOR_MAIN_THREAD_CLEANUP &&
+ IsEmpty()) {
+ // Complete shutdown. First, ensure that this graph is no longer used.
+ // A new graph graph will be created if one is needed.
+ // Asynchronously clean up old graph. We don't want to do this
+ // synchronously because it spins the event loop waiting for threads
+ // to shut down, and we don't want to do that in a stable state handler.
+ LifecycleStateRef() = LIFECYCLE_WAITING_FOR_THREAD_SHUTDOWN;
+ LOG(LogLevel::Debug,
+ ("%p: Sending MediaTrackGraphShutDownRunnable", this));
+ nsCOMPtr<nsIRunnable> event = new MediaTrackGraphShutDownRunnable(this);
+ mMainThread->Dispatch(event.forget());
+ }
+ } else {
+ if (LifecycleStateRef() <= LIFECYCLE_WAITING_FOR_MAIN_THREAD_CLEANUP) {
+ MessageBlock* block = mBackMessageQueue.AppendElement();
+ block->mMessages = std::move(mCurrentTaskMessageQueue);
+ EnsureNextIteration();
+ }
+
+ // If this MediaTrackGraph has entered regular (non-forced) shutdown it
+ // is not able to process any more messages. Those messages being added to
+ // the graph in the first place is an error.
+ MOZ_DIAGNOSTIC_ASSERT(LifecycleStateRef() <
+ LIFECYCLE_WAITING_FOR_MAIN_THREAD_CLEANUP ||
+ mForceShutDownReceived);
+ }
+
+ if (LifecycleStateRef() == LIFECYCLE_THREAD_NOT_STARTED) {
+ // Start the driver now. We couldn't start it earlier because the graph
+ // might exit immediately on finding it has no tracks. The first message
+ // for a new graph must create a track. Ensure that his message runs on
+ // the first iteration.
+ MOZ_ASSERT(MessagesQueued());
+ SwapMessageQueues();
+
+ LOG(LogLevel::Debug,
+ ("%p: Starting a graph with a %s", this,
+ CurrentDriver()->AsAudioCallbackDriver() ? "AudioCallbackDriver"
+ : "SystemClockDriver"));
+ LifecycleStateRef() = LIFECYCLE_RUNNING;
+ mGraphDriverRunning = true;
+ RefPtr<GraphDriver> driver = CurrentDriver();
+ driver->Start();
+ // It's not safe to Shutdown() a thread from StableState, and
+ // releasing this may shutdown a SystemClockDriver thread.
+ // Proxy the release to outside of StableState.
+ NS_ReleaseOnMainThread("MediaTrackGraphImpl::CurrentDriver",
+ driver.forget(),
+ true); // always proxy
+ }
+
+ if (LifecycleStateRef() == LIFECYCLE_WAITING_FOR_MAIN_THREAD_CLEANUP &&
+ mForceShutDownReceived) {
+ // Defer calls to RunDuringShutdown() to happen while mMonitor is not
+ // held.
+ for (uint32_t i = 0; i < mBackMessageQueue.Length(); ++i) {
+ MessageBlock& mb = mBackMessageQueue[i];
+ controlMessagesToRunDuringShutdown.AppendElements(
+ std::move(mb.mMessages));
+ }
+ mBackMessageQueue.Clear();
+ MOZ_ASSERT(mCurrentTaskMessageQueue.IsEmpty());
+ // Stop MediaTrackGraph threads.
+ LifecycleStateRef() = LIFECYCLE_WAITING_FOR_THREAD_SHUTDOWN;
+ nsCOMPtr<nsIRunnable> event = new MediaTrackGraphShutDownRunnable(this);
+ mMainThread->Dispatch(event.forget());
+ }
+
+ mGraphDriverRunning = LifecycleStateRef() == LIFECYCLE_RUNNING;
+ }
+
+ // Make sure we get a new current time in the next event loop task
+ if (!aSourceIsMTG) {
+ MOZ_ASSERT(mPostedRunInStableState);
+ mPostedRunInStableState = false;
+ }
+
+ for (uint32_t i = 0; i < controlMessagesToRunDuringShutdown.Length(); ++i) {
+ controlMessagesToRunDuringShutdown[i]->RunDuringShutdown();
+ }
+
+#ifdef DEBUG
+ mCanRunMessagesSynchronously =
+ !mGraphDriverRunning &&
+ LifecycleStateRef() >= LIFECYCLE_WAITING_FOR_THREAD_SHUTDOWN;
+#endif
+
+ for (uint32_t i = 0; i < runnables.Length(); ++i) {
+ runnables[i]->Run();
+ }
+}
+
+void MediaTrackGraphImpl::EnsureRunInStableState() {
+ MOZ_ASSERT(NS_IsMainThread(), "main thread only");
+
+ if (mPostedRunInStableState) return;
+ mPostedRunInStableState = true;
+ nsCOMPtr<nsIRunnable> event =
+ new MediaTrackGraphStableStateRunnable(this, false);
+ nsContentUtils::RunInStableState(event.forget());
+}
+
+void MediaTrackGraphImpl::EnsureStableStateEventPosted() {
+ MOZ_ASSERT(OnGraphThread());
+ mMonitor.AssertCurrentThreadOwns();
+
+ if (mPostedRunInStableStateEvent) return;
+ mPostedRunInStableStateEvent = true;
+ nsCOMPtr<nsIRunnable> event =
+ new MediaTrackGraphStableStateRunnable(this, true);
+ mMainThread->Dispatch(event.forget());
+}
+
+void MediaTrackGraphImpl::SignalMainThreadCleanup() {
+ MOZ_ASSERT(mDriver->OnThread());
+
+ MonitorAutoLock lock(mMonitor);
+ // LIFECYCLE_THREAD_NOT_STARTED is possible when shutting down offline
+ // graphs that have not started.
+ MOZ_DIAGNOSTIC_ASSERT(mLifecycleState <= LIFECYCLE_RUNNING);
+ LOG(LogLevel::Debug,
+ ("%p: MediaTrackGraph waiting for main thread cleanup", this));
+ LifecycleStateRef() =
+ MediaTrackGraphImpl::LIFECYCLE_WAITING_FOR_MAIN_THREAD_CLEANUP;
+ EnsureStableStateEventPosted();
+}
+
+void MediaTrackGraphImpl::AppendMessage(UniquePtr<ControlMessage> aMessage) {
+ MOZ_ASSERT(NS_IsMainThread(), "main thread only");
+ MOZ_RELEASE_ASSERT(!aMessage->GetTrack() ||
+ !aMessage->GetTrack()->IsDestroyed());
+ MOZ_DIAGNOSTIC_ASSERT(mMainThreadTrackCount > 0 || mMainThreadPortCount > 0);
+
+ if (!mGraphDriverRunning &&
+ LifecycleStateRef() > LIFECYCLE_WAITING_FOR_MAIN_THREAD_CLEANUP) {
+ // The graph control loop is not running and main thread cleanup has
+ // happened. From now on we can't append messages to
+ // mCurrentTaskMessageQueue, because that will never be processed again, so
+ // just RunDuringShutdown this message. This should only happen during
+ // forced shutdown, or after a non-realtime graph has finished processing.
+#ifdef DEBUG
+ MOZ_ASSERT(mCanRunMessagesSynchronously);
+ mCanRunMessagesSynchronously = false;
+#endif
+ aMessage->RunDuringShutdown();
+#ifdef DEBUG
+ mCanRunMessagesSynchronously = true;
+#endif
+ if (IsEmpty() &&
+ LifecycleStateRef() >= LIFECYCLE_WAITING_FOR_TRACK_DESTRUCTION) {
+ Destroy();
+ }
+ return;
+ }
+
+ mCurrentTaskMessageQueue.AppendElement(std::move(aMessage));
+ EnsureRunInStableState();
+}
+
+void MediaTrackGraphImpl::Dispatch(already_AddRefed<nsIRunnable>&& aRunnable) {
+ mMainThread->Dispatch(std::move(aRunnable));
+}
+
+MediaTrack::MediaTrack(TrackRate aSampleRate, MediaSegment::Type aType,
+ MediaSegment* aSegment)
+ : mSampleRate(aSampleRate),
+ mType(aType),
+ mSegment(aSegment),
+ mStartTime(0),
+ mForgottenTime(0),
+ mEnded(false),
+ mNotifiedEnded(false),
+ mDisabledMode(DisabledTrackMode::ENABLED),
+ mStartBlocking(GRAPH_TIME_MAX),
+ mSuspendedCount(0),
+ mMainThreadCurrentTime(0),
+ mMainThreadEnded(false),
+ mEndedNotificationSent(false),
+ mMainThreadDestroyed(false),
+ mGraph(nullptr) {
+ MOZ_COUNT_CTOR(MediaTrack);
+ MOZ_ASSERT_IF(mSegment, mSegment->GetType() == aType);
+}
+
+MediaTrack::~MediaTrack() {
+ MOZ_COUNT_DTOR(MediaTrack);
+ NS_ASSERTION(mMainThreadDestroyed, "Should have been destroyed already");
+ NS_ASSERTION(mMainThreadListeners.IsEmpty(),
+ "All main thread listeners should have been removed");
+}
+
+size_t MediaTrack::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t amount = 0;
+
+ // Not owned:
+ // - mGraph - Not reported here
+ // - mConsumers - elements
+ // Future:
+ // - mLastPlayedVideoFrame
+ // - mTrackListeners - elements
+
+ amount += mTrackListeners.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ amount += mMainThreadListeners.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ amount += mConsumers.ShallowSizeOfExcludingThis(aMallocSizeOf);
+
+ return amount;
+}
+
+size_t MediaTrack::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
+
+void MediaTrack::IncrementSuspendCount() {
+ ++mSuspendedCount;
+ if (mSuspendedCount != 1 || !mGraph) {
+ MOZ_ASSERT(mGraph || mConsumers.IsEmpty());
+ return;
+ }
+ MOZ_ASSERT(mGraph->OnGraphThreadOrNotRunning());
+ for (uint32_t i = 0; i < mConsumers.Length(); ++i) {
+ mConsumers[i]->Suspended();
+ }
+ MOZ_ASSERT(mGraph->mTracks.Contains(this));
+ mGraph->mTracks.RemoveElement(this);
+ mGraph->mSuspendedTracks.AppendElement(this);
+ mGraph->SetTrackOrderDirty();
+}
+
+void MediaTrack::DecrementSuspendCount() {
+ MOZ_ASSERT(mSuspendedCount > 0, "Suspend count underrun");
+ --mSuspendedCount;
+ if (mSuspendedCount != 0 || !mGraph) {
+ MOZ_ASSERT(mGraph || mConsumers.IsEmpty());
+ return;
+ }
+ MOZ_ASSERT(mGraph->OnGraphThreadOrNotRunning());
+ for (uint32_t i = 0; i < mConsumers.Length(); ++i) {
+ mConsumers[i]->Resumed();
+ }
+ MOZ_ASSERT(mGraph->mSuspendedTracks.Contains(this));
+ mGraph->mSuspendedTracks.RemoveElement(this);
+ mGraph->mTracks.AppendElement(this);
+ mGraph->SetTrackOrderDirty();
+}
+
+void ProcessedMediaTrack::DecrementSuspendCount() {
+ mCycleMarker = NOT_VISITED;
+ MediaTrack::DecrementSuspendCount();
+}
+
+MediaTrackGraphImpl* MediaTrack::GraphImpl() { return mGraph; }
+
+const MediaTrackGraphImpl* MediaTrack::GraphImpl() const { return mGraph; }
+
+MediaTrackGraph* MediaTrack::Graph() { return mGraph; }
+
+const MediaTrackGraph* MediaTrack::Graph() const { return mGraph; }
+
+void MediaTrack::SetGraphImpl(MediaTrackGraphImpl* aGraph) {
+ MOZ_ASSERT(!mGraph, "Should only be called once");
+ MOZ_ASSERT(mSampleRate == aGraph->GraphRate());
+ mGraph = aGraph;
+}
+
+void MediaTrack::SetGraphImpl(MediaTrackGraph* aGraph) {
+ MediaTrackGraphImpl* graph = static_cast<MediaTrackGraphImpl*>(aGraph);
+ SetGraphImpl(graph);
+}
+
+TrackTime MediaTrack::GraphTimeToTrackTime(GraphTime aTime) const {
+ NS_ASSERTION(mStartBlocking == GraphImpl()->mStateComputedTime ||
+ aTime <= mStartBlocking,
+ "Incorrectly ignoring blocking!");
+ return aTime - mStartTime;
+}
+
+GraphTime MediaTrack::TrackTimeToGraphTime(TrackTime aTime) const {
+ NS_ASSERTION(mStartBlocking == GraphImpl()->mStateComputedTime ||
+ aTime + mStartTime <= mStartBlocking,
+ "Incorrectly ignoring blocking!");
+ return aTime + mStartTime;
+}
+
+TrackTime MediaTrack::GraphTimeToTrackTimeWithBlocking(GraphTime aTime) const {
+ return GraphImpl()->GraphTimeToTrackTimeWithBlocking(this, aTime);
+}
+
+void MediaTrack::RemoveAllResourcesAndListenersImpl() {
+ GraphImpl()->AssertOnGraphThreadOrNotRunning();
+
+ for (auto& l : mTrackListeners.Clone()) {
+ l->NotifyRemoved(Graph());
+ }
+ mTrackListeners.Clear();
+
+ RemoveAllDirectListenersImpl();
+
+ if (mSegment) {
+ mSegment->Clear();
+ }
+}
+
+void MediaTrack::DestroyImpl() {
+ for (int32_t i = mConsumers.Length() - 1; i >= 0; --i) {
+ mConsumers[i]->Disconnect();
+ }
+ if (mSegment) {
+ mSegment->Clear();
+ }
+ mGraph = nullptr;
+}
+
+void MediaTrack::Destroy() {
+ // Keep this track alive until we leave this method
+ RefPtr<MediaTrack> kungFuDeathGrip = this;
+
+ class Message : public ControlMessage {
+ public:
+ explicit Message(MediaTrack* aTrack) : ControlMessage(aTrack) {}
+ void RunDuringShutdown() override {
+ TRACE("MediaTrack::Destroy ControlMessage");
+ mTrack->RemoveAllResourcesAndListenersImpl();
+ auto graph = mTrack->GraphImpl();
+ mTrack->DestroyImpl();
+ graph->RemoveTrackGraphThread(mTrack);
+ }
+ void Run() override {
+ mTrack->OnGraphThreadDone();
+ RunDuringShutdown();
+ }
+ };
+ // Keep a reference to the graph, since Message might RunDuringShutdown()
+ // synchronously and make GraphImpl() invalid.
+ RefPtr<MediaTrackGraphImpl> graph = GraphImpl();
+ graph->AppendMessage(MakeUnique<Message>(this));
+ graph->RemoveTrack(this);
+ // Message::RunDuringShutdown may have removed this track from the graph,
+ // but our kungFuDeathGrip above will have kept this track alive if
+ // necessary.
+ mMainThreadDestroyed = true;
+}
+
+TrackTime MediaTrack::GetEnd() const {
+ return mSegment ? mSegment->GetDuration() : 0;
+}
+
+void MediaTrack::AddAudioOutput(void* aKey) {
+ class Message : public ControlMessage {
+ public:
+ Message(MediaTrack* aTrack, void* aKey)
+ : ControlMessage(aTrack), mKey(aKey) {}
+ void Run() override {
+ TRACE("MediaTrack::AddAudioOutputImpl ControlMessage");
+ mTrack->AddAudioOutputImpl(mKey);
+ }
+ void* mKey;
+ };
+ if (mMainThreadDestroyed) {
+ return;
+ }
+ GraphImpl()->AppendMessage(MakeUnique<Message>(this, aKey));
+}
+
+void MediaTrackGraphImpl::SetAudioOutputVolume(MediaTrack* aTrack, void* aKey,
+ float aVolume) {
+ for (auto& tkv : mAudioOutputs) {
+ if (tkv.mKey == aKey && aTrack == tkv.mTrack) {
+ tkv.mVolume = aVolume;
+ return;
+ }
+ }
+ MOZ_CRASH("Audio stream key not found when setting the volume.");
+}
+
+void MediaTrack::SetAudioOutputVolumeImpl(void* aKey, float aVolume) {
+ MOZ_ASSERT(GraphImpl()->OnGraphThread());
+ GraphImpl()->SetAudioOutputVolume(this, aKey, aVolume);
+}
+
+void MediaTrack::SetAudioOutputVolume(void* aKey, float aVolume) {
+ class Message : public ControlMessage {
+ public:
+ Message(MediaTrack* aTrack, void* aKey, float aVolume)
+ : ControlMessage(aTrack), mKey(aKey), mVolume(aVolume) {}
+ void Run() override {
+ TRACE("MediaTrack::SetAudioOutputVolumeImpl ControlMessage");
+ mTrack->SetAudioOutputVolumeImpl(mKey, mVolume);
+ }
+ void* mKey;
+ float mVolume;
+ };
+ if (mMainThreadDestroyed) {
+ return;
+ }
+ GraphImpl()->AppendMessage(MakeUnique<Message>(this, aKey, aVolume));
+}
+
+void MediaTrack::AddAudioOutputImpl(void* aKey) {
+ LOG(LogLevel::Info, ("MediaTrack %p adding AudioOutput", this));
+ GraphImpl()->RegisterAudioOutput(this, aKey);
+}
+
+void MediaTrack::RemoveAudioOutputImpl(void* aKey) {
+ LOG(LogLevel::Info, ("MediaTrack %p removing AudioOutput", this));
+ GraphImpl()->UnregisterAudioOutput(this, aKey);
+}
+
+void MediaTrack::RemoveAudioOutput(void* aKey) {
+ class Message : public ControlMessage {
+ public:
+ explicit Message(MediaTrack* aTrack, void* aKey)
+ : ControlMessage(aTrack), mKey(aKey) {}
+ void Run() override {
+ TRACE("MediaTrack::RemoveAudioOutputImpl ControlMessage");
+ mTrack->RemoveAudioOutputImpl(mKey);
+ }
+ void* mKey;
+ };
+ if (mMainThreadDestroyed) {
+ return;
+ }
+ GraphImpl()->AppendMessage(MakeUnique<Message>(this, aKey));
+}
+
+void MediaTrack::Suspend() {
+ class Message : public ControlMessage {
+ public:
+ explicit Message(MediaTrack* aTrack) : ControlMessage(aTrack) {}
+ void Run() override {
+ TRACE("MediaTrack::IncrementSuspendCount ControlMessage");
+ mTrack->IncrementSuspendCount();
+ }
+ };
+
+ // This can happen if this method has been called asynchronously, and the
+ // track has been destroyed since then.
+ if (mMainThreadDestroyed) {
+ return;
+ }
+ GraphImpl()->AppendMessage(MakeUnique<Message>(this));
+}
+
+void MediaTrack::Resume() {
+ class Message : public ControlMessage {
+ public:
+ explicit Message(MediaTrack* aTrack) : ControlMessage(aTrack) {}
+ void Run() override {
+ TRACE("MediaTrack::DecrementSuspendCount ControlMessage");
+ mTrack->DecrementSuspendCount();
+ }
+ };
+
+ // This can happen if this method has been called asynchronously, and the
+ // track has been destroyed since then.
+ if (mMainThreadDestroyed) {
+ return;
+ }
+ GraphImpl()->AppendMessage(MakeUnique<Message>(this));
+}
+
+void MediaTrack::AddListenerImpl(
+ already_AddRefed<MediaTrackListener> aListener) {
+ RefPtr<MediaTrackListener> l(aListener);
+ mTrackListeners.AppendElement(std::move(l));
+
+ PrincipalHandle lastPrincipalHandle = mSegment->GetLastPrincipalHandle();
+ mTrackListeners.LastElement()->NotifyPrincipalHandleChanged(
+ Graph(), lastPrincipalHandle);
+ if (mNotifiedEnded) {
+ mTrackListeners.LastElement()->NotifyEnded(Graph());
+ }
+ if (CombinedDisabledMode() == DisabledTrackMode::SILENCE_BLACK) {
+ mTrackListeners.LastElement()->NotifyEnabledStateChanged(Graph(), false);
+ }
+}
+
+void MediaTrack::AddListener(MediaTrackListener* aListener) {
+ class Message : public ControlMessage {
+ public:
+ Message(MediaTrack* aTrack, MediaTrackListener* aListener)
+ : ControlMessage(aTrack), mListener(aListener) {}
+ void Run() override {
+ TRACE("MediaTrack::AddListenerImpl ControlMessage");
+ mTrack->AddListenerImpl(mListener.forget());
+ }
+ RefPtr<MediaTrackListener> mListener;
+ };
+ MOZ_ASSERT(mSegment, "Segment-less tracks do not support listeners");
+ if (mMainThreadDestroyed) {
+ return;
+ }
+ GraphImpl()->AppendMessage(MakeUnique<Message>(this, aListener));
+}
+
+void MediaTrack::RemoveListenerImpl(MediaTrackListener* aListener) {
+ for (size_t i = 0; i < mTrackListeners.Length(); ++i) {
+ if (mTrackListeners[i] == aListener) {
+ mTrackListeners[i]->NotifyRemoved(Graph());
+ mTrackListeners.RemoveElementAt(i);
+ return;
+ }
+ }
+}
+
+RefPtr<GenericPromise> MediaTrack::RemoveListener(
+ MediaTrackListener* aListener) {
+ class Message : public ControlMessage {
+ public:
+ Message(MediaTrack* aTrack, MediaTrackListener* aListener)
+ : ControlMessage(aTrack), mListener(aListener) {}
+ void Run() override {
+ TRACE("MediaTrack::RemoveListenerImpl ControlMessage");
+ mTrack->RemoveListenerImpl(mListener);
+ mRemovedPromise.Resolve(true, __func__);
+ }
+ void RunDuringShutdown() override {
+ // During shutdown we still want the listener's NotifyRemoved to be
+ // called, since not doing that might block shutdown of other modules.
+ Run();
+ }
+ RefPtr<MediaTrackListener> mListener;
+ MozPromiseHolder<GenericPromise> mRemovedPromise;
+ };
+
+ UniquePtr<Message> message = MakeUnique<Message>(this, aListener);
+ RefPtr<GenericPromise> p = message->mRemovedPromise.Ensure(__func__);
+ if (mMainThreadDestroyed) {
+ message->mRemovedPromise.Reject(NS_ERROR_FAILURE, __func__);
+ return p;
+ }
+ GraphImpl()->AppendMessage(std::move(message));
+ return p;
+}
+
+void MediaTrack::AddDirectListenerImpl(
+ already_AddRefed<DirectMediaTrackListener> aListener) {
+ // Base implementation, for tracks that don't support direct track listeners.
+ RefPtr<DirectMediaTrackListener> listener = aListener;
+ listener->NotifyDirectListenerInstalled(
+ DirectMediaTrackListener::InstallationResult::TRACK_NOT_SUPPORTED);
+}
+
+void MediaTrack::AddDirectListener(DirectMediaTrackListener* aListener) {
+ class Message : public ControlMessage {
+ public:
+ Message(MediaTrack* aTrack, DirectMediaTrackListener* aListener)
+ : ControlMessage(aTrack), mListener(aListener) {}
+ void Run() override {
+ TRACE("MediaTrack::AddDirectListenerImpl ControlMessage");
+ mTrack->AddDirectListenerImpl(mListener.forget());
+ }
+ RefPtr<DirectMediaTrackListener> mListener;
+ };
+ if (mMainThreadDestroyed) {
+ return;
+ }
+ GraphImpl()->AppendMessage(MakeUnique<Message>(this, aListener));
+}
+
+void MediaTrack::RemoveDirectListenerImpl(DirectMediaTrackListener* aListener) {
+ // Base implementation, the listener was never added so nothing to do.
+}
+
+void MediaTrack::RemoveDirectListener(DirectMediaTrackListener* aListener) {
+ class Message : public ControlMessage {
+ public:
+ Message(MediaTrack* aTrack, DirectMediaTrackListener* aListener)
+ : ControlMessage(aTrack), mListener(aListener) {}
+ void Run() override {
+ TRACE("MediaTrack::RemoveDirectListenerImpl ControlMessage");
+ mTrack->RemoveDirectListenerImpl(mListener);
+ }
+ void RunDuringShutdown() override {
+ // During shutdown we still want the listener's
+ // NotifyDirectListenerUninstalled to be called, since not doing that
+ // might block shutdown of other modules.
+ Run();
+ }
+ RefPtr<DirectMediaTrackListener> mListener;
+ };
+ if (mMainThreadDestroyed) {
+ return;
+ }
+ GraphImpl()->AppendMessage(MakeUnique<Message>(this, aListener));
+}
+
+void MediaTrack::RunAfterPendingUpdates(
+ already_AddRefed<nsIRunnable> aRunnable) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MediaTrackGraphImpl* graph = GraphImpl();
+ nsCOMPtr<nsIRunnable> runnable(aRunnable);
+
+ class Message : public ControlMessage {
+ public:
+ Message(MediaTrack* aTrack, already_AddRefed<nsIRunnable> aRunnable)
+ : ControlMessage(aTrack), mRunnable(aRunnable) {}
+ void Run() override {
+ TRACE("MediaTrack::DispatchToMainThreadStableState ControlMessage");
+ mTrack->Graph()->DispatchToMainThreadStableState(mRunnable.forget());
+ }
+ void RunDuringShutdown() override {
+ // Don't run mRunnable now as it may call AppendMessage() which would
+ // assume that there are no remaining controlMessagesToRunDuringShutdown.
+ MOZ_ASSERT(NS_IsMainThread());
+ mTrack->GraphImpl()->Dispatch(mRunnable.forget());
+ }
+
+ private:
+ nsCOMPtr<nsIRunnable> mRunnable;
+ };
+
+ if (mMainThreadDestroyed) {
+ return;
+ }
+ graph->AppendMessage(MakeUnique<Message>(this, runnable.forget()));
+}
+
+void MediaTrack::SetDisabledTrackModeImpl(DisabledTrackMode aMode) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ aMode == DisabledTrackMode::ENABLED ||
+ mDisabledMode == DisabledTrackMode::ENABLED,
+ "Changing disabled track mode for a track is not allowed");
+ DisabledTrackMode oldMode = CombinedDisabledMode();
+ mDisabledMode = aMode;
+ NotifyIfDisabledModeChangedFrom(oldMode);
+}
+
+void MediaTrack::SetDisabledTrackMode(DisabledTrackMode aMode) {
+ class Message : public ControlMessage {
+ public:
+ Message(MediaTrack* aTrack, DisabledTrackMode aMode)
+ : ControlMessage(aTrack), mMode(aMode) {}
+ void Run() override {
+ TRACE("MediaTrack::SetDisabledTrackModeImpl ControlMessage");
+ mTrack->SetDisabledTrackModeImpl(mMode);
+ }
+ DisabledTrackMode mMode;
+ };
+ if (mMainThreadDestroyed) {
+ return;
+ }
+ GraphImpl()->AppendMessage(MakeUnique<Message>(this, aMode));
+}
+
+void MediaTrack::ApplyTrackDisabling(MediaSegment* aSegment,
+ MediaSegment* aRawSegment) {
+ if (mDisabledMode == DisabledTrackMode::ENABLED) {
+ return;
+ }
+ if (mDisabledMode == DisabledTrackMode::SILENCE_BLACK) {
+ aSegment->ReplaceWithDisabled();
+ if (aRawSegment) {
+ aRawSegment->ReplaceWithDisabled();
+ }
+ } else if (mDisabledMode == DisabledTrackMode::SILENCE_FREEZE) {
+ aSegment->ReplaceWithNull();
+ if (aRawSegment) {
+ aRawSegment->ReplaceWithNull();
+ }
+ } else {
+ MOZ_CRASH("Unsupported mode");
+ }
+}
+
+void MediaTrack::AddMainThreadListener(
+ MainThreadMediaTrackListener* aListener) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aListener);
+ MOZ_ASSERT(!mMainThreadListeners.Contains(aListener));
+
+ mMainThreadListeners.AppendElement(aListener);
+
+ // If it is not yet time to send the notification, then exit here.
+ if (!mEndedNotificationSent) {
+ return;
+ }
+
+ class NotifyRunnable final : public Runnable {
+ public:
+ explicit NotifyRunnable(MediaTrack* aTrack)
+ : Runnable("MediaTrack::NotifyRunnable"), mTrack(aTrack) {}
+
+ NS_IMETHOD Run() override {
+ TRACE("MediaTrack::NotifyMainThreadListeners Runnable");
+ MOZ_ASSERT(NS_IsMainThread());
+ mTrack->NotifyMainThreadListeners();
+ return NS_OK;
+ }
+
+ private:
+ ~NotifyRunnable() = default;
+
+ RefPtr<MediaTrack> mTrack;
+ };
+
+ nsCOMPtr<nsIRunnable> runnable = new NotifyRunnable(this);
+ GraphImpl()->Dispatch(runnable.forget());
+}
+
+void MediaTrack::AdvanceTimeVaryingValuesToCurrentTime(GraphTime aCurrentTime,
+ GraphTime aBlockedTime) {
+ mStartTime += aBlockedTime;
+
+ if (!mSegment) {
+ // No data to be forgotten.
+ return;
+ }
+
+ TrackTime time = aCurrentTime - mStartTime;
+ // Only prune if there is a reasonable chunk (50ms) to forget, so we don't
+ // spend too much time pruning segments.
+ const TrackTime minChunkSize = mSampleRate * 50 / 1000;
+ if (time < mForgottenTime + minChunkSize) {
+ return;
+ }
+
+ mForgottenTime = std::min(GetEnd() - 1, time);
+ mSegment->ForgetUpTo(mForgottenTime);
+}
+
+void MediaTrack::NotifyIfDisabledModeChangedFrom(DisabledTrackMode aOldMode) {
+ DisabledTrackMode mode = CombinedDisabledMode();
+ if (aOldMode == mode) {
+ return;
+ }
+
+ for (const auto& listener : mTrackListeners) {
+ listener->NotifyEnabledStateChanged(
+ Graph(), mode != DisabledTrackMode::SILENCE_BLACK);
+ }
+
+ for (const auto& c : mConsumers) {
+ if (c->GetDestination()) {
+ c->GetDestination()->OnInputDisabledModeChanged(mode);
+ }
+ }
+}
+
+SourceMediaTrack::SourceMediaTrack(MediaSegment::Type aType,
+ TrackRate aSampleRate)
+ : MediaTrack(aSampleRate, aType,
+ aType == MediaSegment::AUDIO
+ ? static_cast<MediaSegment*>(new AudioSegment())
+ : static_cast<MediaSegment*>(new VideoSegment())),
+ mMutex("mozilla::media::SourceMediaTrack") {
+ mUpdateTrack = MakeUnique<TrackData>();
+ mUpdateTrack->mInputRate = aSampleRate;
+ mUpdateTrack->mResamplerChannelCount = 0;
+ mUpdateTrack->mData = UniquePtr<MediaSegment>(mSegment->CreateEmptyClone());
+ mUpdateTrack->mEnded = false;
+ mUpdateTrack->mPullingEnabled = false;
+ mUpdateTrack->mGraphThreadDone = false;
+}
+
+void SourceMediaTrack::DestroyImpl() {
+ GraphImpl()->AssertOnGraphThreadOrNotRunning();
+ for (int32_t i = mConsumers.Length() - 1; i >= 0; --i) {
+ // Disconnect before we come under mMutex's lock since it can call back
+ // through RemoveDirectListenerImpl() and deadlock.
+ mConsumers[i]->Disconnect();
+ }
+
+ // Hold mMutex while mGraph is reset so that other threads holding mMutex
+ // can null-check know that the graph will not destroyed.
+ MutexAutoLock lock(mMutex);
+ mUpdateTrack = nullptr;
+ MediaTrack::DestroyImpl();
+}
+
+void SourceMediaTrack::SetPullingEnabled(bool aEnabled) {
+ class Message : public ControlMessage {
+ public:
+ Message(SourceMediaTrack* aTrack, bool aEnabled)
+ : ControlMessage(nullptr), mTrack(aTrack), mEnabled(aEnabled) {}
+ void Run() override {
+ TRACE("SourceMediaTrack::SetPullingEnabled ControlMessage");
+ MutexAutoLock lock(mTrack->mMutex);
+ if (!mTrack->mUpdateTrack) {
+ // We can't enable pulling for a track that has ended. We ignore
+ // this if we're disabling pulling, since shutdown sequences are
+ // complex. If there's truly an issue we'll have issues enabling anyway.
+ MOZ_ASSERT_IF(mEnabled, mTrack->mEnded);
+ return;
+ }
+ MOZ_ASSERT(mTrack->mType == MediaSegment::AUDIO,
+ "Pulling is not allowed for video");
+ mTrack->mUpdateTrack->mPullingEnabled = mEnabled;
+ }
+ SourceMediaTrack* mTrack;
+ bool mEnabled;
+ };
+ GraphImpl()->AppendMessage(MakeUnique<Message>(this, aEnabled));
+}
+
+bool SourceMediaTrack::PullNewData(GraphTime aDesiredUpToTime) {
+ TRACE_COMMENT("SourceMediaTrack::PullNewData", "%p", this);
+ TrackTime t;
+ TrackTime current;
+ {
+ if (mEnded) {
+ return false;
+ }
+ MutexAutoLock lock(mMutex);
+ if (mUpdateTrack->mEnded) {
+ return false;
+ }
+ if (!mUpdateTrack->mPullingEnabled) {
+ return false;
+ }
+ // Compute how much track time we'll need assuming we don't block
+ // the track at all.
+ t = GraphTimeToTrackTime(aDesiredUpToTime);
+ current = GetEnd() + mUpdateTrack->mData->GetDuration();
+ }
+ if (t <= current) {
+ return false;
+ }
+ LOG(LogLevel::Verbose, ("%p: Calling NotifyPull track=%p t=%f current end=%f",
+ GraphImpl(), this, GraphImpl()->MediaTimeToSeconds(t),
+ GraphImpl()->MediaTimeToSeconds(current)));
+ for (auto& l : mTrackListeners) {
+ l->NotifyPull(Graph(), current, t);
+ }
+ return true;
+}
+
+/**
+ * This moves chunks from aIn to aOut. For audio this is simple. For video
+ * we carry durations over if present, or extend up to aDesiredUpToTime if not.
+ *
+ * We also handle "resetters" from captured media elements. This type of source
+ * pushes future frames into the track, and should it need to remove some, e.g.,
+ * because of a seek or pause, it tells us by letting time go backwards. Without
+ * this, tracks would be live for too long after a seek or pause.
+ */
+static void MoveToSegment(SourceMediaTrack* aTrack, MediaSegment* aIn,
+ MediaSegment* aOut, TrackTime aCurrentTime,
+ TrackTime aDesiredUpToTime)
+ MOZ_REQUIRES(aTrack->GetMutex()) {
+ MOZ_ASSERT(aIn->GetType() == aOut->GetType());
+ MOZ_ASSERT(aOut->GetDuration() >= aCurrentTime);
+ MOZ_ASSERT(aDesiredUpToTime >= aCurrentTime);
+ if (aIn->GetType() == MediaSegment::AUDIO) {
+ AudioSegment* in = static_cast<AudioSegment*>(aIn);
+ AudioSegment* out = static_cast<AudioSegment*>(aOut);
+ TrackTime desiredDurationToMove = aDesiredUpToTime - aCurrentTime;
+ TrackTime end = std::min(in->GetDuration(), desiredDurationToMove);
+
+ out->AppendSlice(*in, 0, end);
+ in->RemoveLeading(end);
+
+ aTrack->GetMutex().AssertCurrentThreadOwns();
+ out->ApplyVolume(aTrack->GetVolumeLocked());
+ } else {
+ VideoSegment* in = static_cast<VideoSegment*>(aIn);
+ VideoSegment* out = static_cast<VideoSegment*>(aOut);
+ for (VideoSegment::ConstChunkIterator c(*in); !c.IsEnded(); c.Next()) {
+ MOZ_ASSERT(!c->mTimeStamp.IsNull());
+ VideoChunk* last = out->GetLastChunk();
+ if (!last || last->mTimeStamp.IsNull()) {
+ // This is the first frame, or the last frame pushed to `out` has been
+ // all consumed. Just append and we deal with its duration later.
+ out->AppendFrame(do_AddRef(c->mFrame.GetImage()),
+ c->mFrame.GetIntrinsicSize(),
+ c->mFrame.GetPrincipalHandle(),
+ c->mFrame.GetForceBlack(), c->mTimeStamp);
+ if (c->GetDuration() > 0) {
+ out->ExtendLastFrameBy(c->GetDuration());
+ }
+ continue;
+ }
+
+ // We now know when this frame starts, aka when the last frame ends.
+
+ if (c->mTimeStamp < last->mTimeStamp) {
+ // Time is going backwards. This is a resetting frame from
+ // DecodedStream. Clear everything up to currentTime.
+ out->Clear();
+ out->AppendNullData(aCurrentTime);
+ }
+
+ // Append the current frame (will have duration 0).
+ out->AppendFrame(do_AddRef(c->mFrame.GetImage()),
+ c->mFrame.GetIntrinsicSize(),
+ c->mFrame.GetPrincipalHandle(),
+ c->mFrame.GetForceBlack(), c->mTimeStamp);
+ if (c->GetDuration() > 0) {
+ out->ExtendLastFrameBy(c->GetDuration());
+ }
+ }
+ if (out->GetDuration() < aDesiredUpToTime) {
+ out->ExtendLastFrameBy(aDesiredUpToTime - out->GetDuration());
+ }
+ in->Clear();
+ MOZ_ASSERT(aIn->GetDuration() == 0, "aIn must be consumed");
+ }
+}
+
+void SourceMediaTrack::ExtractPendingInput(GraphTime aCurrentTime,
+ GraphTime aDesiredUpToTime) {
+ MutexAutoLock lock(mMutex);
+
+ if (!mUpdateTrack) {
+ MOZ_ASSERT(mEnded);
+ return;
+ }
+
+ TrackTime trackCurrentTime = GraphTimeToTrackTime(aCurrentTime);
+
+ ApplyTrackDisabling(mUpdateTrack->mData.get());
+
+ if (!mUpdateTrack->mData->IsEmpty()) {
+ for (const auto& l : mTrackListeners) {
+ l->NotifyQueuedChanges(GraphImpl(), GetEnd(), *mUpdateTrack->mData);
+ }
+ }
+ TrackTime trackDesiredUpToTime = GraphTimeToTrackTime(aDesiredUpToTime);
+ TrackTime endTime = trackDesiredUpToTime;
+ if (mUpdateTrack->mEnded) {
+ endTime = std::min(trackDesiredUpToTime,
+ GetEnd() + mUpdateTrack->mData->GetDuration());
+ }
+ LOG(LogLevel::Verbose,
+ ("%p: SourceMediaTrack %p advancing end from %" PRId64 " to %" PRId64,
+ GraphImpl(), this, int64_t(trackCurrentTime), int64_t(endTime)));
+ MoveToSegment(this, mUpdateTrack->mData.get(), mSegment.get(),
+ trackCurrentTime, endTime);
+ if (mUpdateTrack->mEnded && GetEnd() < trackDesiredUpToTime) {
+ mEnded = true;
+ mUpdateTrack = nullptr;
+ }
+}
+
+void SourceMediaTrack::ResampleAudioToGraphSampleRate(MediaSegment* aSegment) {
+ mMutex.AssertCurrentThreadOwns();
+ if (aSegment->GetType() != MediaSegment::AUDIO ||
+ mUpdateTrack->mInputRate == GraphImpl()->GraphRate()) {
+ return;
+ }
+ AudioSegment* segment = static_cast<AudioSegment*>(aSegment);
+ segment->ResampleChunks(mUpdateTrack->mResampler,
+ &mUpdateTrack->mResamplerChannelCount,
+ mUpdateTrack->mInputRate, GraphImpl()->GraphRate());
+}
+
+void SourceMediaTrack::AdvanceTimeVaryingValuesToCurrentTime(
+ GraphTime aCurrentTime, GraphTime aBlockedTime) {
+ MutexAutoLock lock(mMutex);
+ MediaTrack::AdvanceTimeVaryingValuesToCurrentTime(aCurrentTime, aBlockedTime);
+}
+
+void SourceMediaTrack::SetAppendDataSourceRate(TrackRate aRate) {
+ MutexAutoLock lock(mMutex);
+ if (!mUpdateTrack) {
+ return;
+ }
+ MOZ_DIAGNOSTIC_ASSERT(mSegment->GetType() == MediaSegment::AUDIO);
+ // Set the new input rate and reset the resampler.
+ mUpdateTrack->mInputRate = aRate;
+ mUpdateTrack->mResampler.own(nullptr);
+ mUpdateTrack->mResamplerChannelCount = 0;
+}
+
+TrackTime SourceMediaTrack::AppendData(MediaSegment* aSegment,
+ MediaSegment* aRawSegment) {
+ MutexAutoLock lock(mMutex);
+ MOZ_DIAGNOSTIC_ASSERT(aSegment->GetType() == mType);
+ TrackTime appended = 0;
+ if (!mUpdateTrack || mUpdateTrack->mEnded || mUpdateTrack->mGraphThreadDone) {
+ aSegment->Clear();
+ return appended;
+ }
+
+ // Data goes into mData, and on the next iteration of the MTG moves
+ // into the track's segment after NotifyQueuedTrackChanges(). This adds
+ // 0-10ms of delay before data gets to direct listeners.
+ // Indirect listeners (via subsequent TrackUnion nodes) are synced to
+ // playout time, and so can be delayed by buffering.
+
+ // Apply track disabling before notifying any consumers directly
+ // or inserting into the graph
+ ApplyTrackDisabling(aSegment, aRawSegment);
+
+ ResampleAudioToGraphSampleRate(aSegment);
+
+ // Must notify first, since AppendFrom() will empty out aSegment
+ NotifyDirectConsumers(aRawSegment ? aRawSegment : aSegment);
+ appended = aSegment->GetDuration();
+ mUpdateTrack->mData->AppendFrom(aSegment); // note: aSegment is now dead
+ {
+ auto graph = GraphImpl();
+ MonitorAutoLock lock(graph->GetMonitor());
+ if (graph->CurrentDriver()) { // graph has not completed forced shutdown
+ graph->EnsureNextIteration();
+ }
+ }
+
+ return appended;
+}
+
+TrackTime SourceMediaTrack::ClearFutureData() {
+ MutexAutoLock lock(mMutex);
+ auto graph = GraphImpl();
+ if (!mUpdateTrack || !graph) {
+ return 0;
+ }
+
+ TrackTime duration = mUpdateTrack->mData->GetDuration();
+ mUpdateTrack->mData->Clear();
+ return duration;
+}
+
+void SourceMediaTrack::NotifyDirectConsumers(MediaSegment* aSegment) {
+ mMutex.AssertCurrentThreadOwns();
+
+ for (const auto& l : mDirectTrackListeners) {
+ TrackTime offset = 0; // FIX! need a separate TrackTime.... or the end of
+ // the internal buffer
+ l->NotifyRealtimeTrackDataAndApplyTrackDisabling(Graph(), offset,
+ *aSegment);
+ }
+}
+
+void SourceMediaTrack::AddDirectListenerImpl(
+ already_AddRefed<DirectMediaTrackListener> aListener) {
+ MutexAutoLock lock(mMutex);
+
+ RefPtr<DirectMediaTrackListener> listener = aListener;
+ LOG(LogLevel::Debug,
+ ("%p: Adding direct track listener %p to source track %p", GraphImpl(),
+ listener.get(), this));
+
+ MOZ_ASSERT(mType == MediaSegment::VIDEO);
+ for (const auto& l : mDirectTrackListeners) {
+ if (l == listener) {
+ listener->NotifyDirectListenerInstalled(
+ DirectMediaTrackListener::InstallationResult::ALREADY_EXISTS);
+ return;
+ }
+ }
+
+ mDirectTrackListeners.AppendElement(listener);
+
+ LOG(LogLevel::Debug,
+ ("%p: Added direct track listener %p", GraphImpl(), listener.get()));
+ listener->NotifyDirectListenerInstalled(
+ DirectMediaTrackListener::InstallationResult::SUCCESS);
+
+ if (mDisabledMode != DisabledTrackMode::ENABLED) {
+ listener->IncreaseDisabled(mDisabledMode);
+ }
+
+ if (mEnded) {
+ return;
+ }
+
+ // Pass buffered data to the listener
+ VideoSegment bufferedData;
+ size_t videoFrames = 0;
+ VideoSegment& segment = *GetData<VideoSegment>();
+ for (VideoSegment::ConstChunkIterator iter(segment); !iter.IsEnded();
+ iter.Next()) {
+ if (iter->mTimeStamp.IsNull()) {
+ // No timestamp means this is only for the graph's internal book-keeping,
+ // denoting a late start of the track.
+ continue;
+ }
+ ++videoFrames;
+ bufferedData.AppendFrame(do_AddRef(iter->mFrame.GetImage()),
+ iter->mFrame.GetIntrinsicSize(),
+ iter->mFrame.GetPrincipalHandle(),
+ iter->mFrame.GetForceBlack(), iter->mTimeStamp);
+ }
+
+ VideoSegment& video = static_cast<VideoSegment&>(*mUpdateTrack->mData);
+ for (VideoSegment::ConstChunkIterator iter(video); !iter.IsEnded();
+ iter.Next()) {
+ ++videoFrames;
+ MOZ_ASSERT(!iter->mTimeStamp.IsNull());
+ bufferedData.AppendFrame(do_AddRef(iter->mFrame.GetImage()),
+ iter->mFrame.GetIntrinsicSize(),
+ iter->mFrame.GetPrincipalHandle(),
+ iter->mFrame.GetForceBlack(), iter->mTimeStamp);
+ }
+
+ LOG(LogLevel::Info,
+ ("%p: Notifying direct listener %p of %zu video frames and duration "
+ "%" PRId64,
+ GraphImpl(), listener.get(), videoFrames, bufferedData.GetDuration()));
+ listener->NotifyRealtimeTrackData(Graph(), 0, bufferedData);
+}
+
+void SourceMediaTrack::RemoveDirectListenerImpl(
+ DirectMediaTrackListener* aListener) {
+ MutexAutoLock lock(mMutex);
+ for (int32_t i = mDirectTrackListeners.Length() - 1; i >= 0; --i) {
+ const RefPtr<DirectMediaTrackListener>& l = mDirectTrackListeners[i];
+ if (l == aListener) {
+ if (mDisabledMode != DisabledTrackMode::ENABLED) {
+ aListener->DecreaseDisabled(mDisabledMode);
+ }
+ aListener->NotifyDirectListenerUninstalled();
+ mDirectTrackListeners.RemoveElementAt(i);
+ }
+ }
+}
+
+void SourceMediaTrack::End() {
+ MutexAutoLock lock(mMutex);
+ if (!mUpdateTrack) {
+ // Already ended
+ return;
+ }
+ mUpdateTrack->mEnded = true;
+ if (auto graph = GraphImpl()) {
+ MonitorAutoLock lock(graph->GetMonitor());
+ if (graph->CurrentDriver()) { // graph has not completed forced shutdown
+ graph->EnsureNextIteration();
+ }
+ }
+}
+
+void SourceMediaTrack::SetDisabledTrackModeImpl(DisabledTrackMode aMode) {
+ {
+ MutexAutoLock lock(mMutex);
+ for (const auto& l : mDirectTrackListeners) {
+ DisabledTrackMode oldMode = mDisabledMode;
+ bool oldEnabled = oldMode == DisabledTrackMode::ENABLED;
+ if (!oldEnabled && aMode == DisabledTrackMode::ENABLED) {
+ LOG(LogLevel::Debug, ("%p: SourceMediaTrack %p setting "
+ "direct listener enabled",
+ GraphImpl(), this));
+ l->DecreaseDisabled(oldMode);
+ } else if (oldEnabled && aMode != DisabledTrackMode::ENABLED) {
+ LOG(LogLevel::Debug, ("%p: SourceMediaTrack %p setting "
+ "direct listener disabled",
+ GraphImpl(), this));
+ l->IncreaseDisabled(aMode);
+ }
+ }
+ }
+ MediaTrack::SetDisabledTrackModeImpl(aMode);
+}
+
+uint32_t SourceMediaTrack::NumberOfChannels() const {
+ AudioSegment* audio = GetData<AudioSegment>();
+ MOZ_DIAGNOSTIC_ASSERT(audio);
+ if (!audio) {
+ return 0;
+ }
+ return audio->MaxChannelCount();
+}
+
+void SourceMediaTrack::RemoveAllDirectListenersImpl() {
+ GraphImpl()->AssertOnGraphThreadOrNotRunning();
+ MutexAutoLock lock(mMutex);
+
+ for (auto& l : mDirectTrackListeners.Clone()) {
+ l->NotifyDirectListenerUninstalled();
+ }
+ mDirectTrackListeners.Clear();
+}
+
+void SourceMediaTrack::SetVolume(float aVolume) {
+ MutexAutoLock lock(mMutex);
+ mVolume = aVolume;
+}
+
+float SourceMediaTrack::GetVolumeLocked() {
+ mMutex.AssertCurrentThreadOwns();
+ return mVolume;
+}
+
+SourceMediaTrack::~SourceMediaTrack() = default;
+
+void MediaInputPort::Init() {
+ mGraph->AssertOnGraphThreadOrNotRunning();
+ LOG(LogLevel::Debug, ("%p: Adding MediaInputPort %p (from %p to %p)",
+ mSource->GraphImpl(), this, mSource, mDest));
+ // Only connect the port if it wasn't disconnected on allocation.
+ if (mSource) {
+ mSource->AddConsumer(this);
+ mDest->AddInput(this);
+ }
+ // mPortCount decremented via MediaInputPort::Destroy's message
+ ++mGraph->mPortCount;
+}
+
+void MediaInputPort::Disconnect() {
+ mGraph->AssertOnGraphThreadOrNotRunning();
+ NS_ASSERTION(!mSource == !mDest,
+ "mSource and mDest must either both be null or both non-null");
+
+ if (!mSource) {
+ return;
+ }
+
+ mSource->RemoveConsumer(this);
+ mDest->RemoveInput(this);
+ mSource = nullptr;
+ mDest = nullptr;
+
+ mGraph->SetTrackOrderDirty();
+}
+
+MediaTrack* MediaInputPort::GetSource() const {
+ mGraph->AssertOnGraphThreadOrNotRunning();
+ return mSource;
+}
+
+ProcessedMediaTrack* MediaInputPort::GetDestination() const {
+ mGraph->AssertOnGraphThreadOrNotRunning();
+ return mDest;
+}
+
+MediaInputPort::InputInterval MediaInputPort::GetNextInputInterval(
+ MediaInputPort const* aPort, GraphTime aTime) {
+ InputInterval result = {GRAPH_TIME_MAX, GRAPH_TIME_MAX, false};
+ if (!aPort) {
+ result.mStart = aTime;
+ result.mInputIsBlocked = true;
+ return result;
+ }
+ aPort->mGraph->AssertOnGraphThreadOrNotRunning();
+ if (aTime >= aPort->mDest->mStartBlocking) {
+ return result;
+ }
+ result.mStart = aTime;
+ result.mEnd = aPort->mDest->mStartBlocking;
+ result.mInputIsBlocked = aTime >= aPort->mSource->mStartBlocking;
+ if (!result.mInputIsBlocked) {
+ result.mEnd = std::min(result.mEnd, aPort->mSource->mStartBlocking);
+ }
+ return result;
+}
+
+void MediaInputPort::Suspended() {
+ mGraph->AssertOnGraphThreadOrNotRunning();
+ mDest->InputSuspended(this);
+}
+
+void MediaInputPort::Resumed() {
+ mGraph->AssertOnGraphThreadOrNotRunning();
+ mDest->InputResumed(this);
+}
+
+void MediaInputPort::Destroy() {
+ class Message : public ControlMessage {
+ public:
+ explicit Message(MediaInputPort* aPort)
+ : ControlMessage(nullptr), mPort(aPort) {}
+ void Run() override {
+ TRACE("MediaInputPort::Destroy ControlMessage");
+ mPort->Disconnect();
+ --mPort->GraphImpl()->mPortCount;
+ mPort->SetGraphImpl(nullptr);
+ NS_RELEASE(mPort);
+ }
+ void RunDuringShutdown() override { Run(); }
+ MediaInputPort* mPort;
+ };
+ // Keep a reference to the graph, since Message might RunDuringShutdown()
+ // synchronously and make GraphImpl() invalid.
+ RefPtr<MediaTrackGraphImpl> graph = mGraph;
+ graph->AppendMessage(MakeUnique<Message>(this));
+ --graph->mMainThreadPortCount;
+}
+
+MediaTrackGraphImpl* MediaInputPort::GraphImpl() const {
+ mGraph->AssertOnGraphThreadOrNotRunning();
+ return mGraph;
+}
+
+MediaTrackGraph* MediaInputPort::Graph() const {
+ mGraph->AssertOnGraphThreadOrNotRunning();
+ return mGraph;
+}
+
+void MediaInputPort::SetGraphImpl(MediaTrackGraphImpl* aGraph) {
+ MOZ_ASSERT(!mGraph || !aGraph, "Should only be set once");
+ DebugOnly<MediaTrackGraphImpl*> graph = mGraph ? mGraph : aGraph;
+ MOZ_ASSERT(graph->OnGraphThreadOrNotRunning());
+ mGraph = aGraph;
+}
+
+already_AddRefed<MediaInputPort> ProcessedMediaTrack::AllocateInputPort(
+ MediaTrack* aTrack, uint16_t aInputNumber, uint16_t aOutputNumber) {
+ // This method creates two references to the MediaInputPort: one for
+ // the main thread, and one for the MediaTrackGraph.
+ class Message : public ControlMessage {
+ public:
+ explicit Message(MediaInputPort* aPort)
+ : ControlMessage(aPort->mDest), mPort(aPort) {}
+ void Run() override {
+ TRACE("ProcessedMediaTrack::AllocateInputPort ControlMessage");
+ mPort->Init();
+ // The graph holds its reference implicitly
+ mPort->GraphImpl()->SetTrackOrderDirty();
+ Unused << mPort.forget();
+ }
+ void RunDuringShutdown() override { Run(); }
+ RefPtr<MediaInputPort> mPort;
+ };
+
+ MOZ_DIAGNOSTIC_ASSERT(aTrack->mType == mType);
+ RefPtr<MediaInputPort> port;
+ if (aTrack->IsDestroyed()) {
+ // Create a port that's disconnected, which is what it'd be after its source
+ // track is Destroy()ed normally. Disconnect() is idempotent so destroying
+ // this later is fine.
+ port = new MediaInputPort(GraphImpl(), nullptr, nullptr, aInputNumber,
+ aOutputNumber);
+ } else {
+ MOZ_ASSERT(aTrack->GraphImpl() == GraphImpl());
+ port = new MediaInputPort(GraphImpl(), aTrack, this, aInputNumber,
+ aOutputNumber);
+ }
+ ++GraphImpl()->mMainThreadPortCount;
+ GraphImpl()->AppendMessage(MakeUnique<Message>(port));
+ return port.forget();
+}
+
+void ProcessedMediaTrack::QueueSetAutoend(bool aAutoend) {
+ class Message : public ControlMessage {
+ public:
+ Message(ProcessedMediaTrack* aTrack, bool aAutoend)
+ : ControlMessage(aTrack), mAutoend(aAutoend) {}
+ void Run() override {
+ TRACE("ProcessedMediaTrack::SetAutoendImpl ControlMessage");
+ static_cast<ProcessedMediaTrack*>(mTrack)->SetAutoendImpl(mAutoend);
+ }
+ bool mAutoend;
+ };
+ if (mMainThreadDestroyed) {
+ return;
+ }
+ GraphImpl()->AppendMessage(MakeUnique<Message>(this, aAutoend));
+}
+
+void ProcessedMediaTrack::DestroyImpl() {
+ for (int32_t i = mInputs.Length() - 1; i >= 0; --i) {
+ mInputs[i]->Disconnect();
+ }
+
+ for (int32_t i = mSuspendedInputs.Length() - 1; i >= 0; --i) {
+ mSuspendedInputs[i]->Disconnect();
+ }
+
+ MediaTrack::DestroyImpl();
+ // The track order is only important if there are connections, in which
+ // case MediaInputPort::Disconnect() called SetTrackOrderDirty().
+ // MediaTrackGraphImpl::RemoveTrackGraphThread() will also call
+ // SetTrackOrderDirty(), for other reasons.
+}
+
+MediaTrackGraphImpl::MediaTrackGraphImpl(
+ GraphDriverType aDriverRequested, GraphRunType aRunTypeRequested,
+ TrackRate aSampleRate, uint32_t aChannelCount,
+ CubebUtils::AudioDeviceID aOutputDeviceID,
+ nsISerialEventTarget* aMainThread)
+ : MediaTrackGraph(aSampleRate),
+ mGraphRunner(aRunTypeRequested == SINGLE_THREAD
+ ? GraphRunner::Create(this)
+ : already_AddRefed<GraphRunner>(nullptr)),
+ mFirstCycleBreaker(0)
+ // An offline graph is not initially processing.
+ ,
+ mEndTime(aDriverRequested == OFFLINE_THREAD_DRIVER ? 0 : GRAPH_TIME_MAX),
+ mPortCount(0),
+ mOutputDeviceID(aOutputDeviceID),
+ mMonitor("MediaTrackGraphImpl"),
+ mLifecycleState(LIFECYCLE_THREAD_NOT_STARTED),
+ mPostedRunInStableStateEvent(false),
+ mGraphDriverRunning(false),
+ mPostedRunInStableState(false),
+ mRealtime(aDriverRequested != OFFLINE_THREAD_DRIVER),
+ mTrackOrderDirty(false),
+ mMainThread(aMainThread),
+ mSelfRef(this),
+ mGlobalVolume(CubebUtils::GetVolumeScale())
+#ifdef DEBUG
+ ,
+ mCanRunMessagesSynchronously(false)
+#endif
+ ,
+ mMainThreadGraphTime(0, "MediaTrackGraphImpl::mMainThreadGraphTime"),
+ mAudioOutputLatency(0.0),
+ mMaxOutputChannelCount(std::min(8u, CubebUtils::MaxNumberOfChannels())) {
+ bool failedToGetShutdownBlocker = false;
+ if (!IsNonRealtime()) {
+ failedToGetShutdownBlocker = !AddShutdownBlocker();
+ }
+
+ if ((aRunTypeRequested == SINGLE_THREAD && !mGraphRunner) ||
+ failedToGetShutdownBlocker) {
+ // At least one of the following happened
+ // - Failed to create thread.
+ // - Failed to install a shutdown blocker when one is needed.
+ // Because we have a fail state, jump to last phase of the lifecycle.
+ mLifecycleState = LIFECYCLE_WAITING_FOR_TRACK_DESTRUCTION;
+ RemoveShutdownBlocker(); // No-op if blocker wasn't added.
+#ifdef DEBUG
+ mCanRunMessagesSynchronously = true;
+#endif
+ return;
+ }
+ if (mRealtime) {
+ if (aDriverRequested == AUDIO_THREAD_DRIVER) {
+ // Always start with zero input channels, and no particular preferences
+ // for the input channel.
+ mDriver = new AudioCallbackDriver(this, nullptr, mSampleRate,
+ aChannelCount, 0, mOutputDeviceID,
+ nullptr, AudioInputType::Unknown);
+ } else {
+ mDriver = new SystemClockDriver(this, nullptr, mSampleRate);
+ }
+ } else {
+ mDriver =
+ new OfflineClockDriver(this, mSampleRate, MEDIA_GRAPH_TARGET_PERIOD_MS);
+ }
+
+ mLastMainThreadUpdate = TimeStamp::Now();
+
+ RegisterWeakAsyncMemoryReporter(this);
+}
+
+#ifdef DEBUG
+bool MediaTrackGraphImpl::InDriverIteration(const GraphDriver* aDriver) const {
+ return aDriver->OnThread() ||
+ (mGraphRunner && mGraphRunner->InDriverIteration(aDriver));
+}
+#endif
+
+void MediaTrackGraphImpl::Destroy() {
+ // First unregister from memory reporting.
+ UnregisterWeakMemoryReporter(this);
+
+ // Clear the self reference which will destroy this instance if all
+ // associated GraphDrivers are destroyed.
+ mSelfRef = nullptr;
+}
+
+// Internal method has a Window ID parameter so that TestAudioTrackGraph
+// GTests can create a graph without a window.
+/* static */
+MediaTrackGraphImpl* MediaTrackGraphImpl::GetInstanceIfExists(
+ uint64_t aWindowID, bool aShouldResistFingerprinting, TrackRate aSampleRate,
+ CubebUtils::AudioDeviceID aOutputDeviceID) {
+ MOZ_ASSERT(NS_IsMainThread(), "Main thread only");
+
+ TrackRate sampleRate =
+ aSampleRate
+ ? aSampleRate
+ : CubebUtils::PreferredSampleRate(aShouldResistFingerprinting);
+ GraphKey key(aWindowID, sampleRate, aOutputDeviceID);
+
+ return gGraphs.Get(key);
+}
+
+// Public method has an nsPIDOMWindowInner* parameter to ensure that the
+// window is a real inner Window, not a WindowProxy.
+/* static */
+MediaTrackGraph* MediaTrackGraph::GetInstanceIfExists(
+ nsPIDOMWindowInner* aWindow, TrackRate aSampleRate,
+ CubebUtils::AudioDeviceID aOutputDeviceID) {
+ return MediaTrackGraphImpl::GetInstanceIfExists(
+ aWindow->WindowID(),
+ aWindow->AsGlobal()->ShouldResistFingerprinting(RFPTarget::Unknown),
+ aSampleRate, aOutputDeviceID);
+}
+
+/* static */
+MediaTrackGraphImpl* MediaTrackGraphImpl::GetInstance(
+ GraphDriverType aGraphDriverRequested, uint64_t aWindowID,
+ bool aShouldResistFingerprinting, TrackRate aSampleRate,
+ CubebUtils::AudioDeviceID aOutputDeviceID,
+ nsISerialEventTarget* aMainThread) {
+ MOZ_ASSERT(NS_IsMainThread(), "Main thread only");
+
+ TrackRate sampleRate =
+ aSampleRate
+ ? aSampleRate
+ : CubebUtils::PreferredSampleRate(aShouldResistFingerprinting);
+ MediaTrackGraphImpl* graph = GetInstanceIfExists(
+ aWindowID, aShouldResistFingerprinting, sampleRate, aOutputDeviceID);
+
+ if (!graph) {
+ GraphRunType runType = DIRECT_DRIVER;
+ if (aGraphDriverRequested != OFFLINE_THREAD_DRIVER &&
+ (StaticPrefs::dom_audioworklet_enabled() ||
+ Preferences::GetBool("media.audiograph.single_thread.enabled",
+ false))) {
+ runType = SINGLE_THREAD;
+ }
+
+ // In a real time graph, the number of output channels is determined by
+ // the underlying number of channel of the default audio output device, and
+ // capped to 8.
+ uint32_t channelCount =
+ std::min<uint32_t>(8, CubebUtils::MaxNumberOfChannels());
+ graph = new MediaTrackGraphImpl(aGraphDriverRequested, runType, sampleRate,
+ channelCount, aOutputDeviceID, aMainThread);
+ GraphKey key(aWindowID, sampleRate, aOutputDeviceID);
+ gGraphs.InsertOrUpdate(key, graph);
+
+ LOG(LogLevel::Debug,
+ ("Starting up MediaTrackGraph %p for window 0x%" PRIx64, graph,
+ aWindowID));
+ }
+
+ return graph;
+}
+
+/* static */
+MediaTrackGraph* MediaTrackGraph::GetInstance(
+ GraphDriverType aGraphDriverRequested, nsPIDOMWindowInner* aWindow,
+ TrackRate aSampleRate, CubebUtils::AudioDeviceID aOutputDeviceID) {
+ return MediaTrackGraphImpl::GetInstance(
+ aGraphDriverRequested, aWindow->WindowID(),
+ aWindow->AsGlobal()->ShouldResistFingerprinting(RFPTarget::Unknown),
+ aSampleRate, aOutputDeviceID,
+ aWindow->EventTargetFor(TaskCategory::Other));
+}
+
+MediaTrackGraph* MediaTrackGraph::CreateNonRealtimeInstance(
+ TrackRate aSampleRate, nsPIDOMWindowInner* aWindow) {
+ MOZ_ASSERT(NS_IsMainThread(), "Main thread only");
+
+ nsISerialEventTarget* mainThread = GetMainThreadSerialEventTarget();
+ // aWindow can be null when the document is being unlinked, so this works when
+ // with a generic main thread if that's the case.
+ if (aWindow) {
+ mainThread =
+ aWindow->AsGlobal()->AbstractMainThreadFor(TaskCategory::Other);
+ }
+
+ // Offline graphs have 0 output channel count: they write the output to a
+ // buffer, not an audio output track.
+ MediaTrackGraphImpl* graph =
+ new MediaTrackGraphImpl(OFFLINE_THREAD_DRIVER, DIRECT_DRIVER, aSampleRate,
+ 0, DEFAULT_OUTPUT_DEVICE, mainThread);
+
+ LOG(LogLevel::Debug, ("Starting up Offline MediaTrackGraph %p", graph));
+
+ return graph;
+}
+
+void MediaTrackGraph::ForceShutDown() {
+ MOZ_ASSERT(NS_IsMainThread(), "Main thread only");
+
+ MediaTrackGraphImpl* graph = static_cast<MediaTrackGraphImpl*>(this);
+
+ graph->ForceShutDown();
+}
+
+NS_IMPL_ISUPPORTS(MediaTrackGraphImpl, nsIMemoryReporter, nsIThreadObserver,
+ nsITimerCallback, nsINamed)
+
+NS_IMETHODIMP
+MediaTrackGraphImpl::CollectReports(nsIHandleReportCallback* aHandleReport,
+ nsISupports* aData, bool aAnonymize) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mMainThreadTrackCount == 0) {
+ // No tracks to report.
+ FinishCollectReports(aHandleReport, aData, nsTArray<AudioNodeSizes>());
+ return NS_OK;
+ }
+
+ class Message final : public ControlMessage {
+ public:
+ Message(MediaTrackGraphImpl* aGraph, nsIHandleReportCallback* aHandleReport,
+ nsISupports* aHandlerData)
+ : ControlMessage(nullptr),
+ mGraph(aGraph),
+ mHandleReport(aHandleReport),
+ mHandlerData(aHandlerData) {}
+ void Run() override {
+ TRACE("MTG::CollectSizesForMemoryReport ControlMessage");
+ mGraph->CollectSizesForMemoryReport(mHandleReport.forget(),
+ mHandlerData.forget());
+ }
+ void RunDuringShutdown() override {
+ // Run this message during shutdown too, so that endReports is called.
+ Run();
+ }
+ MediaTrackGraphImpl* mGraph;
+ // nsMemoryReporterManager keeps the callback and data alive only if it
+ // does not time out.
+ nsCOMPtr<nsIHandleReportCallback> mHandleReport;
+ nsCOMPtr<nsISupports> mHandlerData;
+ };
+
+ AppendMessage(MakeUnique<Message>(this, aHandleReport, aData));
+
+ return NS_OK;
+}
+
+void MediaTrackGraphImpl::CollectSizesForMemoryReport(
+ already_AddRefed<nsIHandleReportCallback> aHandleReport,
+ already_AddRefed<nsISupports> aHandlerData) {
+ class FinishCollectRunnable final : public Runnable {
+ public:
+ explicit FinishCollectRunnable(
+ already_AddRefed<nsIHandleReportCallback> aHandleReport,
+ already_AddRefed<nsISupports> aHandlerData)
+ : mozilla::Runnable("FinishCollectRunnable"),
+ mHandleReport(aHandleReport),
+ mHandlerData(aHandlerData) {}
+
+ NS_IMETHOD Run() override {
+ TRACE("MTG::FinishCollectReports ControlMessage");
+ MediaTrackGraphImpl::FinishCollectReports(mHandleReport, mHandlerData,
+ std::move(mAudioTrackSizes));
+ return NS_OK;
+ }
+
+ nsTArray<AudioNodeSizes> mAudioTrackSizes;
+
+ private:
+ ~FinishCollectRunnable() = default;
+
+ // Avoiding nsCOMPtr because NSCAP_ASSERT_NO_QUERY_NEEDED in its
+ // constructor modifies the ref-count, which cannot be done off main
+ // thread.
+ RefPtr<nsIHandleReportCallback> mHandleReport;
+ RefPtr<nsISupports> mHandlerData;
+ };
+
+ RefPtr<FinishCollectRunnable> runnable = new FinishCollectRunnable(
+ std::move(aHandleReport), std::move(aHandlerData));
+
+ auto audioTrackSizes = &runnable->mAudioTrackSizes;
+
+ for (MediaTrack* t : AllTracks()) {
+ AudioNodeTrack* track = t->AsAudioNodeTrack();
+ if (track) {
+ AudioNodeSizes* usage = audioTrackSizes->AppendElement();
+ track->SizeOfAudioNodesIncludingThis(MallocSizeOf, *usage);
+ }
+ }
+
+ mMainThread->Dispatch(runnable.forget());
+}
+
+void MediaTrackGraphImpl::FinishCollectReports(
+ nsIHandleReportCallback* aHandleReport, nsISupports* aData,
+ const nsTArray<AudioNodeSizes>& aAudioTrackSizes) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIMemoryReporterManager> manager =
+ do_GetService("@mozilla.org/memory-reporter-manager;1");
+
+ if (!manager) return;
+
+#define REPORT(_path, _amount, _desc) \
+ aHandleReport->Callback(""_ns, _path, KIND_HEAP, UNITS_BYTES, _amount, \
+ nsLiteralCString(_desc), aData);
+
+ for (size_t i = 0; i < aAudioTrackSizes.Length(); i++) {
+ const AudioNodeSizes& usage = aAudioTrackSizes[i];
+ const char* const nodeType =
+ usage.mNodeType ? usage.mNodeType : "<unknown>";
+
+ nsPrintfCString enginePath("explicit/webaudio/audio-node/%s/engine-objects",
+ nodeType);
+ REPORT(enginePath, usage.mEngine,
+ "Memory used by AudioNode engine objects (Web Audio).");
+
+ nsPrintfCString trackPath("explicit/webaudio/audio-node/%s/track-objects",
+ nodeType);
+ REPORT(trackPath, usage.mTrack,
+ "Memory used by AudioNode track objects (Web Audio).");
+ }
+
+ size_t hrtfLoaders = WebCore::HRTFDatabaseLoader::sizeOfLoaders(MallocSizeOf);
+ if (hrtfLoaders) {
+ REPORT(nsLiteralCString(
+ "explicit/webaudio/audio-node/PannerNode/hrtf-databases"),
+ hrtfLoaders, "Memory used by PannerNode databases (Web Audio).");
+ }
+
+#undef REPORT
+
+ manager->EndReport();
+}
+
+SourceMediaTrack* MediaTrackGraph::CreateSourceTrack(MediaSegment::Type aType) {
+ SourceMediaTrack* track = new SourceMediaTrack(aType, GraphRate());
+ AddTrack(track);
+ return track;
+}
+
+ProcessedMediaTrack* MediaTrackGraph::CreateForwardedInputTrack(
+ MediaSegment::Type aType) {
+ ForwardedInputTrack* track = new ForwardedInputTrack(GraphRate(), aType);
+ AddTrack(track);
+ return track;
+}
+
+AudioCaptureTrack* MediaTrackGraph::CreateAudioCaptureTrack() {
+ AudioCaptureTrack* track = new AudioCaptureTrack(GraphRate());
+ AddTrack(track);
+ return track;
+}
+
+CrossGraphTransmitter* MediaTrackGraph::CreateCrossGraphTransmitter(
+ CrossGraphReceiver* aReceiver) {
+ CrossGraphTransmitter* track =
+ new CrossGraphTransmitter(GraphRate(), aReceiver);
+ AddTrack(track);
+ return track;
+}
+
+CrossGraphReceiver* MediaTrackGraph::CreateCrossGraphReceiver(
+ TrackRate aTransmitterRate) {
+ CrossGraphReceiver* track =
+ new CrossGraphReceiver(GraphRate(), aTransmitterRate);
+ AddTrack(track);
+ return track;
+}
+
+void MediaTrackGraph::AddTrack(MediaTrack* aTrack) {
+ MediaTrackGraphImpl* graph = static_cast<MediaTrackGraphImpl*>(this);
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ if (graph->mRealtime) {
+ bool found = false;
+ for (const auto& currentGraph : gGraphs.Values()) {
+ if (currentGraph == graph) {
+ found = true;
+ break;
+ }
+ }
+ MOZ_DIAGNOSTIC_ASSERT(found, "Graph must not be shutting down");
+ }
+#endif
+ NS_ADDREF(aTrack);
+ aTrack->SetGraphImpl(graph);
+ ++graph->mMainThreadTrackCount;
+ graph->AppendMessage(MakeUnique<CreateMessage>(aTrack));
+}
+
+void MediaTrackGraphImpl::RemoveTrack(MediaTrack* aTrack) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(mMainThreadTrackCount > 0);
+ if (--mMainThreadTrackCount == 0) {
+ LOG(LogLevel::Info, ("MediaTrackGraph %p, last track %p removed from "
+ "main thread. Graph will shut down.",
+ this, aTrack));
+ // Find the graph in the hash table and remove it.
+ for (auto iter = gGraphs.Iter(); !iter.Done(); iter.Next()) {
+ if (iter.UserData() == this) {
+ iter.Remove();
+ break;
+ }
+ }
+ // The graph thread will shut itself down soon, but won't be able to do
+ // that if JS continues to run.
+ InterruptJS();
+ }
+}
+
+auto MediaTrackGraph::NotifyWhenDeviceStarted(MediaTrack* aTrack)
+ -> RefPtr<GraphStartedPromise> {
+ MOZ_ASSERT(NS_IsMainThread());
+ MozPromiseHolder<GraphStartedPromise> h;
+ RefPtr<GraphStartedPromise> p = h.Ensure(__func__);
+ aTrack->GraphImpl()->NotifyWhenGraphStarted(aTrack, std::move(h));
+ return p;
+}
+
+void MediaTrackGraphImpl::NotifyWhenGraphStarted(
+ RefPtr<MediaTrack> aTrack,
+ MozPromiseHolder<GraphStartedPromise>&& aHolder) {
+ class GraphStartedNotificationControlMessage : public ControlMessage {
+ RefPtr<MediaTrack> mMediaTrack;
+ MozPromiseHolder<GraphStartedPromise> mHolder;
+
+ public:
+ GraphStartedNotificationControlMessage(
+ RefPtr<MediaTrack> aTrack,
+ MozPromiseHolder<GraphStartedPromise>&& aHolder)
+ : ControlMessage(nullptr),
+ mMediaTrack(std::move(aTrack)),
+ mHolder(std::move(aHolder)) {}
+ void Run() override {
+ TRACE("MTG::GraphStartedNotificationControlMessage ControlMessage");
+ // This runs on the graph thread, so when this runs, and the current
+ // driver is an AudioCallbackDriver, we know the audio hardware is
+ // started. If not, we are going to switch soon, keep reposting this
+ // ControlMessage.
+ MediaTrackGraphImpl* graphImpl = mMediaTrack->GraphImpl();
+ if (graphImpl->CurrentDriver()->AsAudioCallbackDriver() &&
+ graphImpl->CurrentDriver()->ThreadRunning() &&
+ !graphImpl->CurrentDriver()->AsAudioCallbackDriver()->OnFallback()) {
+ // Avoid Resolve's locking on the graph thread by doing it on main.
+ graphImpl->Dispatch(NS_NewRunnableFunction(
+ "MediaTrackGraphImpl::NotifyWhenGraphStarted::Resolver",
+ [holder = std::move(mHolder)]() mutable {
+ holder.Resolve(true, __func__);
+ }));
+ } else {
+ graphImpl->DispatchToMainThreadStableState(
+ NewRunnableMethod<
+ StoreCopyPassByRRef<RefPtr<MediaTrack>>,
+ StoreCopyPassByRRef<MozPromiseHolder<GraphStartedPromise>>>(
+ "MediaTrackGraphImpl::NotifyWhenGraphStarted", graphImpl,
+ &MediaTrackGraphImpl::NotifyWhenGraphStarted,
+ std::move(mMediaTrack), std::move(mHolder)));
+ }
+ }
+ void RunDuringShutdown() override {
+ mHolder.Reject(NS_ERROR_ILLEGAL_DURING_SHUTDOWN, __func__);
+ }
+ };
+
+ if (aTrack->IsDestroyed()) {
+ aHolder.Reject(NS_ERROR_NOT_AVAILABLE, __func__);
+ return;
+ }
+
+ MediaTrackGraphImpl* graph = aTrack->GraphImpl();
+ graph->AppendMessage(MakeUnique<GraphStartedNotificationControlMessage>(
+ std::move(aTrack), std::move(aHolder)));
+}
+
+class AudioContextOperationControlMessage : public ControlMessage {
+ using AudioContextOperationPromise =
+ MediaTrackGraph::AudioContextOperationPromise;
+
+ public:
+ AudioContextOperationControlMessage(
+ MediaTrack* aDestinationTrack, nsTArray<RefPtr<MediaTrack>> aTracks,
+ AudioContextOperation aOperation,
+ MozPromiseHolder<AudioContextOperationPromise>&& aHolder)
+ : ControlMessage(aDestinationTrack),
+ mTracks(std::move(aTracks)),
+ mAudioContextOperation(aOperation),
+ mHolder(std::move(aHolder)) {}
+ void Run() override {
+ TRACE_COMMENT("MTG::ApplyAudioContextOperationImpl ControlMessage",
+ kAudioContextOptionsStrings[static_cast<uint8_t>(
+ mAudioContextOperation)]);
+ mTrack->GraphImpl()->ApplyAudioContextOperationImpl(this);
+ }
+ void RunDuringShutdown() override {
+ MOZ_ASSERT(mAudioContextOperation == AudioContextOperation::Close,
+ "We should be reviving the graph?");
+ mHolder.Reject(false, __func__);
+ }
+
+ nsTArray<RefPtr<MediaTrack>> mTracks;
+ AudioContextOperation mAudioContextOperation;
+ MozPromiseHolder<AudioContextOperationPromise> mHolder;
+};
+
+void MediaTrackGraphImpl::ApplyAudioContextOperationImpl(
+ AudioContextOperationControlMessage* aMessage) {
+ MOZ_ASSERT(OnGraphThread());
+ // Initialize state to zero. This silences a GCC warning about uninitialized
+ // values, because although the switch below initializes state for all valid
+ // enum values, the actual value could be any integer that fits in the enum.
+ AudioContextState state{0};
+ switch (aMessage->mAudioContextOperation) {
+ // Suspend and Close operations may be performed immediately because no
+ // specific kind of GraphDriver is required. CheckDriver() will schedule
+ // a change to a SystemCallbackDriver if all tracks are suspended.
+ case AudioContextOperation::Suspend:
+ state = AudioContextState::Suspended;
+ break;
+ case AudioContextOperation::Close:
+ state = AudioContextState::Closed;
+ break;
+ case AudioContextOperation::Resume:
+ // Resume operations require an AudioCallbackDriver. CheckDriver() will
+ // schedule an AudioCallbackDriver if necessary and process pending
+ // operations if and when an AudioCallbackDriver is running.
+ mPendingResumeOperations.EmplaceBack(aMessage);
+ return;
+ }
+ // First resolve any pending Resume promises for the same AudioContext so as
+ // to resolve its associated promises in the same order as they were
+ // created. These Resume operations are considered complete and immediately
+ // canceled by the Suspend or Close.
+ MediaTrack* destinationTrack = aMessage->GetTrack();
+ bool shrinking = false;
+ auto moveDest = mPendingResumeOperations.begin();
+ for (PendingResumeOperation& op : mPendingResumeOperations) {
+ if (op.DestinationTrack() == destinationTrack) {
+ op.Apply(this);
+ shrinking = true;
+ continue;
+ }
+ if (shrinking) { // Fill-in gaps in the array.
+ *moveDest = std::move(op);
+ }
+ ++moveDest;
+ }
+ mPendingResumeOperations.TruncateLength(moveDest -
+ mPendingResumeOperations.begin());
+
+ for (MediaTrack* track : aMessage->mTracks) {
+ track->IncrementSuspendCount();
+ }
+ // Resolve after main thread state is up to date with completed processing.
+ DispatchToMainThreadStableState(NS_NewRunnableFunction(
+ "MediaTrackGraphImpl::ApplyAudioContextOperationImpl",
+ [holder = std::move(aMessage->mHolder), state]() mutable {
+ holder.Resolve(state, __func__);
+ }));
+}
+
+MediaTrackGraphImpl::PendingResumeOperation::PendingResumeOperation(
+ AudioContextOperationControlMessage* aMessage)
+ : mDestinationTrack(aMessage->GetTrack()),
+ mTracks(std::move(aMessage->mTracks)),
+ mHolder(std::move(aMessage->mHolder)) {
+ MOZ_ASSERT(aMessage->mAudioContextOperation == AudioContextOperation::Resume);
+}
+
+void MediaTrackGraphImpl::PendingResumeOperation::Apply(
+ MediaTrackGraphImpl* aGraph) {
+ MOZ_ASSERT(aGraph->OnGraphThread());
+ for (MediaTrack* track : mTracks) {
+ track->DecrementSuspendCount();
+ }
+ // The graph is provided through the parameter so that it is available even
+ // when the track is destroyed.
+ aGraph->DispatchToMainThreadStableState(NS_NewRunnableFunction(
+ "PendingResumeOperation::Apply", [holder = std::move(mHolder)]() mutable {
+ holder.Resolve(AudioContextState::Running, __func__);
+ }));
+}
+
+void MediaTrackGraphImpl::PendingResumeOperation::Abort() {
+ // The graph is shutting down before the operation completed.
+ MOZ_ASSERT(!mDestinationTrack->GraphImpl() ||
+ mDestinationTrack->GraphImpl()->LifecycleStateRef() ==
+ MediaTrackGraphImpl::LIFECYCLE_WAITING_FOR_THREAD_SHUTDOWN);
+ mHolder.Reject(false, __func__);
+}
+
+auto MediaTrackGraph::ApplyAudioContextOperation(
+ MediaTrack* aDestinationTrack, nsTArray<RefPtr<MediaTrack>> aTracks,
+ AudioContextOperation aOperation) -> RefPtr<AudioContextOperationPromise> {
+ MozPromiseHolder<AudioContextOperationPromise> holder;
+ RefPtr<AudioContextOperationPromise> p = holder.Ensure(__func__);
+ MediaTrackGraphImpl* graphImpl = static_cast<MediaTrackGraphImpl*>(this);
+ graphImpl->AppendMessage(MakeUnique<AudioContextOperationControlMessage>(
+ aDestinationTrack, std::move(aTracks), aOperation, std::move(holder)));
+ return p;
+}
+
+uint32_t MediaTrackGraphImpl::AudioOutputChannelCount() const {
+ MOZ_ASSERT(OnGraphThread());
+ // The audio output channel count for a graph is the maximum of the output
+ // channel count of all the tracks that are in mAudioOutputs, or the max audio
+ // output channel count the machine can do, whichever is smaller.
+ uint32_t channelCount = 0;
+ for (auto& tkv : mAudioOutputs) {
+ channelCount = std::max(channelCount, tkv.mTrack->NumberOfChannels());
+ }
+ channelCount = std::min(channelCount, mMaxOutputChannelCount);
+ if (channelCount) {
+ return channelCount;
+ } else {
+ if (CurrentDriver()->AsAudioCallbackDriver()) {
+ return CurrentDriver()->AsAudioCallbackDriver()->OutputChannelCount();
+ }
+ return 2;
+ }
+}
+
+double MediaTrackGraph::AudioOutputLatency() {
+ return static_cast<MediaTrackGraphImpl*>(this)->AudioOutputLatency();
+}
+
+double MediaTrackGraphImpl::AudioOutputLatency() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mAudioOutputLatency != 0.0) {
+ return mAudioOutputLatency;
+ }
+ MonitorAutoLock lock(mMonitor);
+ if (CurrentDriver()->AsAudioCallbackDriver()) {
+ mAudioOutputLatency = CurrentDriver()
+ ->AsAudioCallbackDriver()
+ ->AudioOutputLatency()
+ .ToSeconds();
+ } else {
+ // Failure mode: return 0.0 if running on a normal thread.
+ mAudioOutputLatency = 0.0;
+ }
+
+ return mAudioOutputLatency;
+}
+
+bool MediaTrackGraph::IsNonRealtime() const {
+ return !static_cast<const MediaTrackGraphImpl*>(this)->mRealtime;
+}
+
+void MediaTrackGraph::StartNonRealtimeProcessing(uint32_t aTicksToProcess) {
+ MOZ_ASSERT(NS_IsMainThread(), "main thread only");
+
+ MediaTrackGraphImpl* graph = static_cast<MediaTrackGraphImpl*>(this);
+ NS_ASSERTION(!graph->mRealtime, "non-realtime only");
+
+ class Message : public ControlMessage {
+ public:
+ explicit Message(MediaTrackGraphImpl* aGraph, uint32_t aTicksToProcess)
+ : ControlMessage(nullptr),
+ mGraph(aGraph),
+ mTicksToProcess(aTicksToProcess) {}
+ void Run() override {
+ TRACE("MTG::StartNonRealtimeProcessing ControlMessage");
+ MOZ_ASSERT(mGraph->mEndTime == 0,
+ "StartNonRealtimeProcessing should be called only once");
+ mGraph->mEndTime = mGraph->RoundUpToEndOfAudioBlock(
+ mGraph->mStateComputedTime + mTicksToProcess);
+ }
+ // The graph owns this message.
+ MediaTrackGraphImpl* MOZ_NON_OWNING_REF mGraph;
+ uint32_t mTicksToProcess;
+ };
+
+ graph->AppendMessage(MakeUnique<Message>(graph, aTicksToProcess));
+}
+
+void MediaTrackGraphImpl::InterruptJS() {
+ MonitorAutoLock lock(mMonitor);
+ mInterruptJSCalled = true;
+ if (mJSContext) {
+ JS_RequestInterruptCallback(mJSContext);
+ }
+}
+
+static bool InterruptCallback(JSContext* aCx) {
+ // Interrupt future calls also.
+ JS_RequestInterruptCallback(aCx);
+ // Stop execution.
+ return false;
+}
+
+void MediaTrackGraph::NotifyJSContext(JSContext* aCx) {
+ MOZ_ASSERT(OnGraphThread());
+ MOZ_ASSERT(aCx);
+
+ auto* impl = static_cast<MediaTrackGraphImpl*>(this);
+ MonitorAutoLock lock(impl->mMonitor);
+ if (impl->mJSContext) {
+ MOZ_ASSERT(impl->mJSContext == aCx);
+ return;
+ }
+ JS_AddInterruptCallback(aCx, InterruptCallback);
+ impl->mJSContext = aCx;
+ if (impl->mInterruptJSCalled) {
+ JS_RequestInterruptCallback(aCx);
+ }
+}
+
+void ProcessedMediaTrack::AddInput(MediaInputPort* aPort) {
+ MediaTrack* t = aPort->GetSource();
+ if (!t->IsSuspended()) {
+ mInputs.AppendElement(aPort);
+ } else {
+ mSuspendedInputs.AppendElement(aPort);
+ }
+ GraphImpl()->SetTrackOrderDirty();
+}
+
+void ProcessedMediaTrack::InputSuspended(MediaInputPort* aPort) {
+ GraphImpl()->AssertOnGraphThreadOrNotRunning();
+ mInputs.RemoveElement(aPort);
+ mSuspendedInputs.AppendElement(aPort);
+ GraphImpl()->SetTrackOrderDirty();
+}
+
+void ProcessedMediaTrack::InputResumed(MediaInputPort* aPort) {
+ GraphImpl()->AssertOnGraphThreadOrNotRunning();
+ mSuspendedInputs.RemoveElement(aPort);
+ mInputs.AppendElement(aPort);
+ GraphImpl()->SetTrackOrderDirty();
+}
+
+void MediaTrackGraphImpl::SwitchAtNextIteration(GraphDriver* aNextDriver) {
+ MOZ_ASSERT(OnGraphThread());
+ LOG(LogLevel::Debug, ("%p: Switching to new driver: %p", this, aNextDriver));
+ if (GraphDriver* nextDriver = NextDriver()) {
+ if (nextDriver != CurrentDriver()) {
+ LOG(LogLevel::Debug,
+ ("%p: Discarding previous next driver: %p", this, nextDriver));
+ }
+ }
+ mNextDriver = aNextDriver;
+}
+
+void MediaTrackGraph::RegisterCaptureTrackForWindow(
+ uint64_t aWindowId, ProcessedMediaTrack* aCaptureTrack) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MediaTrackGraphImpl* graphImpl = static_cast<MediaTrackGraphImpl*>(this);
+ graphImpl->RegisterCaptureTrackForWindow(aWindowId, aCaptureTrack);
+}
+
+void MediaTrackGraphImpl::RegisterCaptureTrackForWindow(
+ uint64_t aWindowId, ProcessedMediaTrack* aCaptureTrack) {
+ MOZ_ASSERT(NS_IsMainThread());
+ WindowAndTrack winAndTrack;
+ winAndTrack.mWindowId = aWindowId;
+ winAndTrack.mCaptureTrackSink = aCaptureTrack;
+ mWindowCaptureTracks.AppendElement(winAndTrack);
+}
+
+void MediaTrackGraph::UnregisterCaptureTrackForWindow(uint64_t aWindowId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MediaTrackGraphImpl* graphImpl = static_cast<MediaTrackGraphImpl*>(this);
+ graphImpl->UnregisterCaptureTrackForWindow(aWindowId);
+}
+
+void MediaTrackGraphImpl::UnregisterCaptureTrackForWindow(uint64_t aWindowId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mWindowCaptureTracks.RemoveElementsBy(
+ [aWindowId](const auto& track) { return track.mWindowId == aWindowId; });
+}
+
+already_AddRefed<MediaInputPort> MediaTrackGraph::ConnectToCaptureTrack(
+ uint64_t aWindowId, MediaTrack* aMediaTrack) {
+ return aMediaTrack->GraphImpl()->ConnectToCaptureTrack(aWindowId,
+ aMediaTrack);
+}
+
+already_AddRefed<MediaInputPort> MediaTrackGraphImpl::ConnectToCaptureTrack(
+ uint64_t aWindowId, MediaTrack* aMediaTrack) {
+ MOZ_ASSERT(NS_IsMainThread());
+ for (uint32_t i = 0; i < mWindowCaptureTracks.Length(); i++) {
+ if (mWindowCaptureTracks[i].mWindowId == aWindowId) {
+ ProcessedMediaTrack* sink = mWindowCaptureTracks[i].mCaptureTrackSink;
+ return sink->AllocateInputPort(aMediaTrack);
+ }
+ }
+ return nullptr;
+}
+
+void MediaTrackGraph::DispatchToMainThreadStableState(
+ already_AddRefed<nsIRunnable> aRunnable) {
+ AssertOnGraphThreadOrNotRunning();
+ static_cast<MediaTrackGraphImpl*>(this)
+ ->mPendingUpdateRunnables.AppendElement(std::move(aRunnable));
+}
+
+Watchable<mozilla::GraphTime>& MediaTrackGraphImpl::CurrentTime() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mMainThreadGraphTime;
+}
+
+GraphTime MediaTrackGraph::ProcessedTime() const {
+ AssertOnGraphThreadOrNotRunning();
+ return static_cast<const MediaTrackGraphImpl*>(this)->mProcessedTime;
+}
+
+uint32_t MediaTrackGraphImpl::AudioInputChannelCount(
+ CubebUtils::AudioDeviceID aID) {
+ MOZ_ASSERT(OnGraphThreadOrNotRunning());
+ DeviceInputTrack* t =
+ mDeviceInputTrackManagerGraphThread.GetDeviceInputTrack(aID);
+ return t ? t->MaxRequestedInputChannels() : 0;
+}
+
+AudioInputType MediaTrackGraphImpl::AudioInputDevicePreference(
+ CubebUtils::AudioDeviceID aID) {
+ MOZ_ASSERT(OnGraphThreadOrNotRunning());
+ DeviceInputTrack* t =
+ mDeviceInputTrackManagerGraphThread.GetDeviceInputTrack(aID);
+ return t && t->HasVoiceInput() ? AudioInputType::Voice
+ : AudioInputType::Unknown;
+}
+
+void MediaTrackGraphImpl::SetNewNativeInput() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!mDeviceInputTrackManagerMainThread.GetNativeInputTrack());
+
+ LOG(LogLevel::Debug, ("%p SetNewNativeInput", this));
+
+ NonNativeInputTrack* track =
+ mDeviceInputTrackManagerMainThread.GetFirstNonNativeInputTrack();
+ if (!track) {
+ LOG(LogLevel::Debug, ("%p No other devices opened. Do nothing", this));
+ return;
+ }
+
+ const CubebUtils::AudioDeviceID deviceId = track->mDeviceId;
+ const PrincipalHandle principal = track->mPrincipalHandle;
+
+ LOG(LogLevel::Debug,
+ ("%p Select device %p as the new native input device", this, deviceId));
+
+ struct TrackListener {
+ DeviceInputConsumerTrack* track;
+ // Keep its reference so it won't be dropped when after
+ // DisconnectDeviceInput().
+ RefPtr<AudioDataListener> listener;
+ };
+ nsTArray<TrackListener> pairs;
+
+ for (const auto& t : track->GetConsumerTracks()) {
+ pairs.AppendElement(
+ TrackListener{t.get(), t->GetAudioDataListener().get()});
+ }
+
+ for (TrackListener& pair : pairs) {
+ pair.track->DisconnectDeviceInput();
+ }
+
+ for (TrackListener& pair : pairs) {
+ pair.track->ConnectDeviceInput(deviceId, pair.listener.get(), principal);
+ LOG(LogLevel::Debug,
+ ("%p: Reinitialize AudioProcessingTrack %p for device %p", this,
+ pair.track, deviceId));
+ }
+
+ LOG(LogLevel::Debug,
+ ("%p Native input device is set to device %p now", this, deviceId));
+
+ MOZ_ASSERT(mDeviceInputTrackManagerMainThread.GetNativeInputTrack());
+}
+
+// nsIThreadObserver methods
+
+NS_IMETHODIMP
+MediaTrackGraphImpl::OnDispatchedEvent() {
+ MonitorAutoLock lock(mMonitor);
+ EnsureNextIteration();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MediaTrackGraphImpl::OnProcessNextEvent(nsIThreadInternal*, bool) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MediaTrackGraphImpl::AfterProcessNextEvent(nsIThreadInternal*, bool) {
+ return NS_OK;
+}
+} // namespace mozilla
diff --git a/dom/media/MediaTrackGraph.h b/dom/media/MediaTrackGraph.h
new file mode 100644
index 0000000000..4973e743d0
--- /dev/null
+++ b/dom/media/MediaTrackGraph.h
@@ -0,0 +1,1185 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_MEDIATRACKGRAPH_H_
+#define MOZILLA_MEDIATRACKGRAPH_H_
+
+#include "AudioSampleFormat.h"
+#include "CubebUtils.h"
+#include "MainThreadUtils.h"
+#include "MediaSegment.h"
+#include "mozilla/LinkedList.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/StateWatching.h"
+#include "mozilla/TaskQueue.h"
+#include "nsAutoRef.h"
+#include "nsIRunnable.h"
+#include "nsTArray.h"
+#include <speex/speex_resampler.h>
+
+class nsIRunnable;
+class nsIGlobalObject;
+class nsPIDOMWindowInner;
+
+namespace mozilla {
+class AsyncLogger;
+class AudioCaptureTrack;
+class CrossGraphTransmitter;
+class CrossGraphReceiver;
+}; // namespace mozilla
+
+extern mozilla::AsyncLogger gMTGTraceLogger;
+
+template <>
+class nsAutoRefTraits<SpeexResamplerState>
+ : public nsPointerRefTraits<SpeexResamplerState> {
+ public:
+ static void Release(SpeexResamplerState* aState) {
+ speex_resampler_destroy(aState);
+ }
+};
+
+namespace mozilla {
+
+extern LazyLogModule gMediaTrackGraphLog;
+
+namespace dom {
+enum class AudioContextOperation : uint8_t;
+enum class AudioContextOperationFlags;
+enum class AudioContextState : uint8_t;
+} // namespace dom
+
+/*
+ * MediaTrackGraph is a framework for synchronized audio/video processing
+ * and playback. It is designed to be used by other browser components such as
+ * HTML media elements, media capture APIs, real-time media streaming APIs,
+ * multitrack media APIs, and advanced audio APIs.
+ *
+ * The MediaTrackGraph uses a dedicated thread to process media --- the media
+ * graph thread. This ensures that we can process media through the graph
+ * without blocking on main-thread activity. The media graph is only modified
+ * on the media graph thread, to ensure graph changes can be processed without
+ * interfering with media processing. All interaction with the media graph
+ * thread is done with message passing.
+ *
+ * APIs that modify the graph or its properties are described as "control APIs".
+ * These APIs are asynchronous; they queue graph changes internally and
+ * those changes are processed all-at-once by the MediaTrackGraph. The
+ * MediaTrackGraph monitors the main thread event loop via
+ * nsIAppShell::RunInStableState to ensure that graph changes from a single
+ * event loop task are always processed all together. Control APIs should only
+ * be used on the main thread, currently; we may be able to relax that later.
+ *
+ * To allow precise synchronization of times in the control API, the
+ * MediaTrackGraph maintains a "media timeline". Control APIs that take or
+ * return times use that timeline. Those times never advance during
+ * an event loop task. This time is returned by
+ * MediaTrackGraph::GetCurrentTime().
+ *
+ * Media decoding, audio processing and media playback use thread-safe APIs to
+ * the media graph to ensure they can continue while the main thread is blocked.
+ *
+ * When the graph is changed, we may need to throw out buffered data and
+ * reprocess it. This is triggered automatically by the MediaTrackGraph.
+ */
+
+class AudioProcessingTrack;
+class AudioNodeEngine;
+class AudioNodeExternalInputTrack;
+class AudioNodeTrack;
+class DirectMediaTrackListener;
+class ForwardedInputTrack;
+class MediaInputPort;
+class MediaTrack;
+class MediaTrackGraph;
+class MediaTrackGraphImpl;
+class MediaTrackListener;
+class DeviceInputConsumerTrack;
+class DeviceInputTrack;
+class ProcessedMediaTrack;
+class SourceMediaTrack;
+
+class AudioDataListenerInterface {
+ protected:
+ // Protected destructor, to discourage deletion outside of Release():
+ virtual ~AudioDataListenerInterface() = default;
+
+ public:
+ /**
+ * Number of audio input channels.
+ */
+ virtual uint32_t RequestedInputChannelCount(MediaTrackGraphImpl* aGraph) = 0;
+
+ /**
+ * Whether the underlying audio device is used for voice input.
+ */
+ virtual bool IsVoiceInput(MediaTrackGraphImpl* aGraph) const = 0;
+ /**
+ * Called when the underlying audio device has changed.
+ */
+ virtual void DeviceChanged(MediaTrackGraphImpl* aGraph) = 0;
+
+ /**
+ * Called when the underlying audio device is being closed.
+ */
+ virtual void Disconnect(MediaTrackGraphImpl* aGraph) = 0;
+};
+
+class AudioDataListener : public AudioDataListenerInterface {
+ protected:
+ // Protected destructor, to discourage deletion outside of Release():
+ virtual ~AudioDataListener() = default;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AudioDataListener)
+};
+
+/**
+ * This is a base class for main-thread listener callbacks.
+ * This callback is invoked on the main thread when the main-thread-visible
+ * state of a track has changed.
+ *
+ * These methods are called with the media graph monitor held, so
+ * reentry into general media graph methods is not possible.
+ * You should do something non-blocking and non-reentrant (e.g. dispatch an
+ * event) and return. NS_DispatchToCurrentThread would be a good choice.
+ * The listener is allowed to synchronously remove itself from the track, but
+ * not add or remove any other listeners.
+ */
+class MainThreadMediaTrackListener {
+ public:
+ virtual void NotifyMainThreadTrackEnded() = 0;
+};
+
+/**
+ * Helper struct used to keep track of memory usage by AudioNodes.
+ */
+struct AudioNodeSizes {
+ AudioNodeSizes() : mTrack(0), mEngine(0), mNodeType() {}
+ size_t mTrack;
+ size_t mEngine;
+ const char* mNodeType;
+};
+
+/**
+ * Describes how a track should be disabled.
+ *
+ * ENABLED Not disabled.
+ * SILENCE_BLACK Audio data is turned into silence, video frames are made
+ * black.
+ * SILENCE_FREEZE Audio data is turned into silence, video freezes at
+ * last frame.
+ */
+enum class DisabledTrackMode { ENABLED, SILENCE_BLACK, SILENCE_FREEZE };
+
+/**
+ * A track of audio or video data. The media type must be known at construction
+ * and cannot change. All tracks progress at the same rate --- "real time".
+ * Tracks cannot seek. The only operation readers can perform on a track is to
+ * read the next data.
+ *
+ * Consumers of a track can be reading from it at different offsets, but that
+ * should only happen due to the order in which consumers are being run.
+ * Those offsets must not diverge in the long term, otherwise we would require
+ * unbounded buffering.
+ *
+ * (DEPRECATED to be removed in bug 1581074)
+ * Tracks can be in a "blocked" state. While blocked, a track does not
+ * produce data. A track can be explicitly blocked via the control API,
+ * or implicitly blocked by whatever's generating it (e.g. an underrun in the
+ * source resource), or implicitly blocked because something consuming it
+ * blocks, or implicitly because it has ended.
+ *
+ * A track can be in an "ended" state. "Ended" tracks are permanently blocked.
+ * The "ended" state is terminal.
+ *
+ * Transitions into and out of the "blocked" and "ended" states are managed
+ * by the MediaTrackGraph on the media graph thread.
+ *
+ * We buffer media data ahead of the consumers' reading offsets. It is possible
+ * to have buffered data but still be blocked.
+ *
+ * Any track can have its audio or video playing when requested. The media
+ * track graph plays audio by constructing audio output tracks as necessary.
+ * Video is played through a DirectMediaTrackListener managed by
+ * VideoStreamTrack.
+ *
+ * The data in a track is managed by mSegment. The segment starts at GraphTime
+ * mStartTime and encodes its own TrackTime duration.
+ *
+ * Tracks are explicitly managed. The client creates them via
+ * MediaTrackGraph::Create{Source|ForwardedInput}Track, and releases them by
+ * calling Destroy() when no longer needed (actual destruction will be
+ * deferred). The actual object is owned by the MediaTrackGraph. The basic idea
+ * is that main thread objects will keep Tracks alive as long as necessary
+ * (using the cycle collector to clean up whenever needed).
+ *
+ * We make them refcounted only so that track-related messages with
+ * MediaTrack* pointers can be sent to the main thread safely.
+ *
+ * The lifetimes of MediaTracks are controlled from the main thread.
+ * For MediaTracks exposed to the DOM, the lifetime is controlled by the DOM
+ * wrapper; the DOM wrappers own their associated MediaTracks. When a DOM
+ * wrapper is destroyed, it sends a Destroy message for the associated
+ * MediaTrack and clears its reference (the last main-thread reference to
+ * the object). When the Destroy message is processed on the graph thread we
+ * immediately release the affected objects (disentangling them from other
+ * objects as necessary).
+ *
+ * This could cause problems for media processing if a MediaTrack is destroyed
+ * while a downstream MediaTrack is still using it. Therefore the DOM wrappers
+ * must keep upstream MediaTracks alive as long as they could be used in the
+ * media graph.
+ *
+ * At any time, however, a set of MediaTrack wrappers could be collected via
+ * cycle collection. Destroy messages will be sent for those objects in
+ * arbitrary order and the MediaTrackGraph has to be able to handle this.
+ */
+class MediaTrack : public mozilla::LinkedListElement<MediaTrack> {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaTrack)
+
+ MediaTrack(TrackRate aSampleRate, MediaSegment::Type aType,
+ MediaSegment* aSegment);
+
+ // The sample rate of the graph.
+ const TrackRate mSampleRate;
+ const MediaSegment::Type mType;
+
+ protected:
+ // Protected destructor, to discourage deletion outside of Release():
+ virtual ~MediaTrack();
+
+ public:
+ /**
+ * Returns the graph that owns this track.
+ */
+ MediaTrackGraphImpl* GraphImpl();
+ const MediaTrackGraphImpl* GraphImpl() const;
+ MediaTrackGraph* Graph();
+ const MediaTrackGraph* Graph() const;
+ /**
+ * Sets the graph that owns this track. Should only be called once.
+ */
+ void SetGraphImpl(MediaTrackGraphImpl* aGraph);
+ void SetGraphImpl(MediaTrackGraph* aGraph);
+
+ // Control API.
+ void AddAudioOutput(void* aKey);
+ void SetAudioOutputVolume(void* aKey, float aVolume);
+ void RemoveAudioOutput(void* aKey);
+ // Explicitly suspend. Useful for example if a media element is pausing
+ // and we need to stop its track emitting its buffered data. As soon as the
+ // Suspend message reaches the graph, the track stops processing. It
+ // ignores its inputs and produces silence/no video until Resumed. Its
+ // current time does not advance.
+ void Suspend();
+ void Resume();
+ // Events will be dispatched by calling methods of aListener.
+ virtual void AddListener(MediaTrackListener* aListener);
+ virtual RefPtr<GenericPromise> RemoveListener(MediaTrackListener* aListener);
+
+ /**
+ * Adds aListener to the source track of this track.
+ * When the MediaTrackGraph processes the added listener, it will traverse
+ * the graph and add it to the track's source track.
+ * Note that the listener will be notified on the MediaTrackGraph thread
+ * with whether the installation of it at the source was successful or not.
+ */
+ void AddDirectListener(DirectMediaTrackListener* aListener);
+
+ /**
+ * Removes aListener from the source track of this track.
+ * Note that the listener has already been removed if the link between the
+ * source and this track has been broken. The caller doesn't have to care
+ * about this, removing when the source cannot be found, or when the listener
+ * had already been removed does nothing.
+ */
+ void RemoveDirectListener(DirectMediaTrackListener* aListener);
+
+ // A disabled track has video replaced by black, and audio replaced by
+ // silence.
+ void SetDisabledTrackMode(DisabledTrackMode aMode);
+
+ // End event will be notified by calling methods of aListener. It is the
+ // responsibility of the caller to remove aListener before it is destroyed.
+ void AddMainThreadListener(MainThreadMediaTrackListener* aListener);
+ // It's safe to call this even if aListener is not currently a listener;
+ // the call will be ignored.
+ void RemoveMainThreadListener(MainThreadMediaTrackListener* aListener) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aListener);
+ mMainThreadListeners.RemoveElement(aListener);
+ }
+
+ /**
+ * Ensure a runnable will run on the main thread after running all pending
+ * updates that were sent from the graph thread or will be sent before the
+ * graph thread receives the next graph update.
+ *
+ * If the graph has been shut down or destroyed, then the runnable will be
+ * dispatched to the event queue immediately. (There are no pending updates
+ * in this situation.)
+ *
+ * Main thread only.
+ */
+ void RunAfterPendingUpdates(already_AddRefed<nsIRunnable> aRunnable);
+
+ // Signal that the client is done with this MediaTrack. It will be deleted
+ // later.
+ virtual void Destroy();
+
+ // Returns the main-thread's view of how much data has been processed by
+ // this track.
+ TrackTime GetCurrentTime() const {
+ NS_ASSERTION(NS_IsMainThread(), "Call only on main thread");
+ return mMainThreadCurrentTime;
+ }
+ // Return the main thread's view of whether this track has ended.
+ bool IsEnded() const {
+ NS_ASSERTION(NS_IsMainThread(), "Call only on main thread");
+ return mMainThreadEnded;
+ }
+
+ bool IsDestroyed() const {
+ NS_ASSERTION(NS_IsMainThread(), "Call only on main thread");
+ return mMainThreadDestroyed;
+ }
+
+ friend class MediaTrackGraphImpl;
+ friend class MediaInputPort;
+ friend class AudioNodeExternalInputTrack;
+
+ virtual AudioProcessingTrack* AsAudioProcessingTrack() { return nullptr; }
+ virtual SourceMediaTrack* AsSourceTrack() { return nullptr; }
+ virtual ProcessedMediaTrack* AsProcessedTrack() { return nullptr; }
+ virtual AudioNodeTrack* AsAudioNodeTrack() { return nullptr; }
+ virtual ForwardedInputTrack* AsForwardedInputTrack() { return nullptr; }
+ virtual CrossGraphTransmitter* AsCrossGraphTransmitter() { return nullptr; }
+ virtual CrossGraphReceiver* AsCrossGraphReceiver() { return nullptr; }
+ virtual DeviceInputTrack* AsDeviceInputTrack() { return nullptr; }
+ virtual DeviceInputConsumerTrack* AsDeviceInputConsumerTrack() {
+ return nullptr;
+ }
+
+ // These Impl methods perform the core functionality of the control methods
+ // above, on the media graph thread.
+ /**
+ * Stop all track activity and disconnect it from all inputs and outputs.
+ * This must be idempotent.
+ */
+ virtual void DestroyImpl();
+ TrackTime GetEnd() const;
+ void SetAudioOutputVolumeImpl(void* aKey, float aVolume);
+ void AddAudioOutputImpl(void* aKey);
+ void RemoveAudioOutputImpl(void* aKey);
+
+ /**
+ * Removes all direct listeners and signals to them that they have been
+ * uninstalled.
+ */
+ virtual void RemoveAllDirectListenersImpl() {}
+ void RemoveAllResourcesAndListenersImpl();
+
+ virtual void AddListenerImpl(already_AddRefed<MediaTrackListener> aListener);
+ virtual void RemoveListenerImpl(MediaTrackListener* aListener);
+ virtual void AddDirectListenerImpl(
+ already_AddRefed<DirectMediaTrackListener> aListener);
+ virtual void RemoveDirectListenerImpl(DirectMediaTrackListener* aListener);
+ virtual void SetDisabledTrackModeImpl(DisabledTrackMode aMode);
+
+ void AddConsumer(MediaInputPort* aPort) { mConsumers.AppendElement(aPort); }
+ void RemoveConsumer(MediaInputPort* aPort) {
+ mConsumers.RemoveElement(aPort);
+ }
+ GraphTime StartTime() const { return mStartTime; }
+ bool Ended() const { return mEnded; }
+
+ // Returns the current number of channels this track contains if it's an audio
+ // track. Calling this on a video track will trip assertions. Graph thread
+ // only.
+ virtual uint32_t NumberOfChannels() const = 0;
+
+ // The DisabledTrackMode after combining the explicit mode and that of the
+ // input, if any.
+ virtual DisabledTrackMode CombinedDisabledMode() const {
+ return mDisabledMode;
+ }
+
+ template <class SegmentType>
+ SegmentType* GetData() const {
+ if (!mSegment) {
+ return nullptr;
+ }
+ if (mSegment->GetType() != SegmentType::StaticType()) {
+ return nullptr;
+ }
+ return static_cast<SegmentType*>(mSegment.get());
+ }
+ MediaSegment* GetData() const { return mSegment.get(); }
+
+ double TrackTimeToSeconds(TrackTime aTime) const {
+ NS_ASSERTION(0 <= aTime && aTime <= TRACK_TIME_MAX, "Bad time");
+ return static_cast<double>(aTime) / mSampleRate;
+ }
+ int64_t TrackTimeToMicroseconds(TrackTime aTime) const {
+ NS_ASSERTION(0 <= aTime && aTime <= TRACK_TIME_MAX, "Bad time");
+ return (aTime * 1000000) / mSampleRate;
+ }
+ TrackTime SecondsToNearestTrackTime(double aSeconds) const {
+ NS_ASSERTION(0 <= aSeconds && aSeconds <= TRACK_TICKS_MAX / TRACK_RATE_MAX,
+ "Bad seconds");
+ return mSampleRate * aSeconds + 0.5;
+ }
+ TrackTime MicrosecondsToTrackTimeRoundDown(int64_t aMicroseconds) const {
+ return (aMicroseconds * mSampleRate) / 1000000;
+ }
+
+ TrackTicks TimeToTicksRoundUp(TrackRate aRate, TrackTime aTime) const {
+ return RateConvertTicksRoundUp(aRate, mSampleRate, aTime);
+ }
+ TrackTime TicksToTimeRoundDown(TrackRate aRate, TrackTicks aTicks) const {
+ return RateConvertTicksRoundDown(mSampleRate, aRate, aTicks);
+ }
+ /**
+ * Convert graph time to track time. aTime must be <= mStateComputedTime
+ * to ensure we know exactly how much time this track will be blocked during
+ * the interval.
+ */
+ TrackTime GraphTimeToTrackTimeWithBlocking(GraphTime aTime) const;
+ /**
+ * Convert graph time to track time. This assumes there is no blocking time
+ * to take account of, which is always true except between a track
+ * having its blocking time calculated in UpdateGraph and its blocking time
+ * taken account of in UpdateCurrentTimeForTracks.
+ */
+ TrackTime GraphTimeToTrackTime(GraphTime aTime) const;
+ /**
+ * Convert track time to graph time. This assumes there is no blocking time
+ * to take account of, which is always true except between a track
+ * having its blocking time calculated in UpdateGraph and its blocking time
+ * taken account of in UpdateCurrentTimeForTracks.
+ */
+ GraphTime TrackTimeToGraphTime(TrackTime aTime) const;
+
+ virtual void ApplyTrackDisabling(MediaSegment* aSegment,
+ MediaSegment* aRawSegment = nullptr);
+
+ // Return true if the main thread needs to observe updates from this track.
+ virtual bool MainThreadNeedsUpdates() const { return true; }
+
+ virtual size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const;
+ virtual size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const;
+
+ bool IsSuspended() const { return mSuspendedCount > 0; }
+ /**
+ * Increment suspend count and move it to mGraph->mSuspendedTracks if
+ * necessary. Graph thread.
+ */
+ void IncrementSuspendCount();
+ /**
+ * Increment suspend count on aTrack and move it to mGraph->mTracks if
+ * necessary. GraphThread.
+ */
+ virtual void DecrementSuspendCount();
+
+ protected:
+ // Called on graph thread either during destroy handling or before handing
+ // graph control to the main thread to release tracks.
+ virtual void OnGraphThreadDone() {}
+
+ // |AdvanceTimeVaryingValuesToCurrentTime| will be override in
+ // SourceMediaTrack.
+ virtual void AdvanceTimeVaryingValuesToCurrentTime(GraphTime aCurrentTime,
+ GraphTime aBlockedTime);
+
+ void NotifyMainThreadListeners() {
+ NS_ASSERTION(NS_IsMainThread(), "Call only on main thread");
+
+ for (int32_t i = mMainThreadListeners.Length() - 1; i >= 0; --i) {
+ mMainThreadListeners[i]->NotifyMainThreadTrackEnded();
+ }
+ mMainThreadListeners.Clear();
+ }
+
+ bool ShouldNotifyTrackEnded() {
+ NS_ASSERTION(NS_IsMainThread(), "Call only on main thread");
+ if (!mMainThreadEnded || mEndedNotificationSent) {
+ return false;
+ }
+
+ mEndedNotificationSent = true;
+ return true;
+ }
+
+ // Notifies listeners and consumers of the change in disabled mode when the
+ // current combined mode is different from aMode.
+ void NotifyIfDisabledModeChangedFrom(DisabledTrackMode aOldMode);
+
+ // This state is all initialized on the main thread but
+ // otherwise modified only on the media graph thread.
+
+ // Buffered data. The start of the buffer corresponds to mStartTime.
+ // Conceptually the buffer contains everything this track has ever played,
+ // but we forget some prefix of the buffered data to bound the space usage.
+ // Note that this may be null for tracks that never contain data, like
+ // non-external AudioNodeTracks.
+ const UniquePtr<MediaSegment> mSegment;
+
+ // The time when the buffered data could be considered to have started
+ // playing. This increases over time to account for time the track was
+ // blocked before mCurrentTime.
+ GraphTime mStartTime;
+
+ // The time until which we last called mSegment->ForgetUpTo().
+ TrackTime mForgottenTime;
+
+ // True once we've processed mSegment until the end and no more data will be
+ // added. Note that mSegment might still contain data for the current
+ // iteration.
+ bool mEnded;
+
+ // True after track listeners have been notified that this track has ended.
+ bool mNotifiedEnded;
+
+ // Client-set volume of this track
+ nsTArray<RefPtr<MediaTrackListener>> mTrackListeners;
+ nsTArray<MainThreadMediaTrackListener*> mMainThreadListeners;
+ // This track's associated disabled mode. It can either by disabled by frames
+ // being replaced by black, or by retaining the previous frame.
+ DisabledTrackMode mDisabledMode;
+
+ // GraphTime at which this track starts blocking.
+ // This is only valid up to mStateComputedTime. The track is considered to
+ // have not been blocked before mCurrentTime (its mStartTime is
+ // increased as necessary to account for that time instead).
+ GraphTime mStartBlocking;
+
+ // MediaInputPorts to which this is connected
+ nsTArray<MediaInputPort*> mConsumers;
+
+ /**
+ * Number of outstanding suspend operations on this track. Track is
+ * suspended when this is > 0.
+ */
+ int32_t mSuspendedCount;
+
+ // Main-thread views of state
+ TrackTime mMainThreadCurrentTime;
+ bool mMainThreadEnded;
+ bool mEndedNotificationSent;
+ bool mMainThreadDestroyed;
+
+ // Our media track graph. null if destroyed on the graph thread.
+ MediaTrackGraphImpl* mGraph;
+};
+
+/**
+ * This is a track into which a decoder can write audio or video.
+ *
+ * Audio or video can be written on any thread, but you probably want to
+ * always write from the same thread to avoid unexpected interleavings.
+ *
+ * For audio the sample rate of the written data can differ from the sample rate
+ * of the graph itself. Use SetAppendDataSourceRate to inform the track what
+ * rate written audio data will be sampled in.
+ */
+class SourceMediaTrack : public MediaTrack {
+ public:
+ SourceMediaTrack(MediaSegment::Type aType, TrackRate aSampleRate);
+
+ SourceMediaTrack* AsSourceTrack() override { return this; }
+
+ // Main thread only
+
+ /**
+ * Enable or disable pulling.
+ * When pulling is enabled, NotifyPull gets called on the
+ * MediaTrackListeners for this track during the MediaTrackGraph
+ * control loop. Pulling is initially disabled. Due to unavoidable race
+ * conditions, after a call to SetPullingEnabled(false) it is still possible
+ * for a NotifyPull to occur.
+ */
+ void SetPullingEnabled(bool aEnabled);
+
+ // MediaTrackGraph thread only
+ void DestroyImpl() override;
+
+ // Call these on any thread.
+ /**
+ * Call all MediaTrackListeners to request new data via the NotifyPull
+ * API (if enabled).
+ * aDesiredUpToTime (in): end time of new data requested.
+ *
+ * Returns true if new data is about to be added.
+ */
+ bool PullNewData(GraphTime aDesiredUpToTime);
+
+ /**
+ * Extract any state updates pending in the track, and apply them.
+ */
+ void ExtractPendingInput(GraphTime aCurrentTime, GraphTime aDesiredUpToTime);
+
+ /**
+ * All data appended with AppendData() from this point on will be resampled
+ * from aRate to the graph rate.
+ *
+ * Resampling for video does not make sense and is forbidden.
+ */
+ void SetAppendDataSourceRate(TrackRate aRate);
+
+ /**
+ * Append media data to this track. Ownership of aSegment remains with the
+ * caller, but aSegment is emptied. Returns 0 if the data was not appended
+ * because the stream has ended. Returns the duration of the appended data in
+ * the graph's track rate otherwise.
+ */
+ TrackTime AppendData(MediaSegment* aSegment,
+ MediaSegment* aRawSegment = nullptr);
+
+ /**
+ * Clear any data appended with AppendData() that hasn't entered the graph
+ * yet. Returns the duration of the cleared data in the graph's track rate.
+ */
+ TrackTime ClearFutureData();
+
+ /**
+ * Indicate that this track has ended. Do not do any more API calls affecting
+ * this track.
+ */
+ void End();
+
+ // Overriding allows us to hold the mMutex lock while changing the track
+ // enable status
+ void SetDisabledTrackModeImpl(DisabledTrackMode aMode) override;
+
+ // Overriding allows us to ensure mMutex is locked while changing the track
+ // enable status
+ void ApplyTrackDisabling(MediaSegment* aSegment,
+ MediaSegment* aRawSegment = nullptr) override {
+ mMutex.AssertCurrentThreadOwns();
+ MediaTrack::ApplyTrackDisabling(aSegment, aRawSegment);
+ }
+
+ uint32_t NumberOfChannels() const override;
+
+ void RemoveAllDirectListenersImpl() override;
+
+ // The value set here is applied in MoveToSegment so we can avoid the
+ // buffering delay in applying the change. See Bug 1443511.
+ void SetVolume(float aVolume);
+ float GetVolumeLocked() MOZ_REQUIRES(mMutex);
+
+ Mutex& GetMutex() MOZ_RETURN_CAPABILITY(mMutex) { return mMutex; }
+
+ friend class MediaTrackGraphImpl;
+
+ protected:
+ enum TrackCommands : uint32_t;
+
+ virtual ~SourceMediaTrack();
+
+ /**
+ * Data to cater for appending media data to this track.
+ */
+ struct TrackData {
+ // Sample rate of the input data.
+ TrackRate mInputRate;
+ // Resampler if the rate of the input track does not match the
+ // MediaTrackGraph's.
+ nsAutoRef<SpeexResamplerState> mResampler;
+ uint32_t mResamplerChannelCount;
+ // Each time the track updates are flushed to the media graph thread,
+ // the segment buffer is emptied.
+ UniquePtr<MediaSegment> mData;
+ // True once the producer has signaled that no more data is coming.
+ bool mEnded;
+ // True if the producer of this track is having data pulled by the graph.
+ bool mPullingEnabled;
+ // True if the graph has notified this track that it will not be used
+ // again on the graph thread.
+ bool mGraphThreadDone;
+ };
+
+ bool NeedsMixing();
+
+ void ResampleAudioToGraphSampleRate(MediaSegment* aSegment)
+ MOZ_REQUIRES(mMutex);
+
+ void AddDirectListenerImpl(
+ already_AddRefed<DirectMediaTrackListener> aListener) override;
+ void RemoveDirectListenerImpl(DirectMediaTrackListener* aListener) override;
+
+ /**
+ * Notify direct consumers of new data to this track.
+ * The data doesn't have to be resampled (though it may be). This is called
+ * from AppendData on the thread providing the data, and will call
+ * the Listeners on this thread.
+ */
+ void NotifyDirectConsumers(MediaSegment* aSegment) MOZ_REQUIRES(mMutex);
+
+ void OnGraphThreadDone() override {
+ MutexAutoLock lock(mMutex);
+ if (!mUpdateTrack) {
+ return;
+ }
+ mUpdateTrack->mGraphThreadDone = true;
+ if (!mUpdateTrack->mData) {
+ return;
+ }
+ mUpdateTrack->mData->Clear();
+ }
+
+ virtual void AdvanceTimeVaryingValuesToCurrentTime(
+ GraphTime aCurrentTime, GraphTime aBlockedTime) override;
+
+ // This must be acquired *before* MediaTrackGraphImpl's lock, if they are
+ // held together.
+ Mutex mMutex;
+ // protected by mMutex
+ float mVolume MOZ_GUARDED_BY(mMutex) = 1.0;
+ UniquePtr<TrackData> mUpdateTrack MOZ_GUARDED_BY(mMutex);
+ nsTArray<RefPtr<DirectMediaTrackListener>> mDirectTrackListeners
+ MOZ_GUARDED_BY(mMutex);
+};
+
+/**
+ * A ref-counted wrapper of a MediaTrack that allows multiple users to share a
+ * reference to the same MediaTrack with the purpose of being guaranteed that
+ * the graph it is in is kept alive.
+ *
+ * Automatically suspended on creation and destroyed on destruction. Main thread
+ * only.
+ */
+struct SharedDummyTrack {
+ NS_INLINE_DECL_REFCOUNTING(SharedDummyTrack)
+ explicit SharedDummyTrack(MediaTrack* aTrack) : mTrack(aTrack) {
+ mTrack->Suspend();
+ }
+ const RefPtr<MediaTrack> mTrack;
+
+ private:
+ ~SharedDummyTrack() { mTrack->Destroy(); }
+};
+
+/**
+ * Represents a connection between a ProcessedMediaTrack and one of its
+ * input tracks.
+ * We make these refcounted so that track-related messages with MediaInputPort*
+ * pointers can be sent to the main thread safely.
+ *
+ * When a port's source or destination track dies, the track's DestroyImpl
+ * calls MediaInputPort::Disconnect to disconnect the port from
+ * the source and destination tracks.
+ *
+ * The lifetimes of MediaInputPort are controlled from the main thread.
+ * The media graph adds a reference to the port. When a MediaInputPort is no
+ * longer needed, main-thread code sends a Destroy message for the port and
+ * clears its reference (the last main-thread reference to the object). When
+ * the Destroy message is processed on the graph manager thread we disconnect
+ * the port and drop the graph's reference, destroying the object.
+ */
+class MediaInputPort final {
+ private:
+ // Do not call this constructor directly. Instead call
+ // aDest->AllocateInputPort.
+ MediaInputPort(MediaTrackGraphImpl* aGraph, MediaTrack* aSource,
+ ProcessedMediaTrack* aDest, uint16_t aInputNumber,
+ uint16_t aOutputNumber)
+ : mSource(aSource),
+ mDest(aDest),
+ mInputNumber(aInputNumber),
+ mOutputNumber(aOutputNumber),
+ mGraph(aGraph) {
+ MOZ_COUNT_CTOR(MediaInputPort);
+ }
+
+ // Private destructor, to discourage deletion outside of Release():
+ MOZ_COUNTED_DTOR(MediaInputPort)
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaInputPort)
+
+ /**
+ * Disconnects and destroys the port. The caller must not reference this
+ * object again. Main thread.
+ */
+ void Destroy();
+
+ // The remaining methods and members must always be called on the graph thread
+ // from within MediaTrackGraph.cpp.
+
+ void Init();
+ // Called during message processing to trigger removal of this port's source
+ // and destination tracks.
+ void Disconnect();
+
+ MediaTrack* GetSource() const;
+ ProcessedMediaTrack* GetDestination() const;
+
+ uint16_t InputNumber() const { return mInputNumber; }
+ uint16_t OutputNumber() const { return mOutputNumber; }
+
+ struct InputInterval {
+ GraphTime mStart;
+ GraphTime mEnd;
+ bool mInputIsBlocked;
+ };
+ // Find the next time interval starting at or after aTime during which
+ // aPort->mDest is not blocked and aPort->mSource's blocking status does not
+ // change. A null aPort returns a blocked interval starting at aTime.
+ static InputInterval GetNextInputInterval(MediaInputPort const* aPort,
+ GraphTime aTime);
+
+ /**
+ * Returns the graph that owns this port.
+ */
+ MediaTrackGraphImpl* GraphImpl() const;
+ MediaTrackGraph* Graph() const;
+
+ /**
+ * Sets the graph that owns this track. Should only be called once.
+ */
+ void SetGraphImpl(MediaTrackGraphImpl* aGraph);
+
+ /**
+ * Notify the port that the source MediaTrack has been suspended.
+ */
+ void Suspended();
+
+ /**
+ * Notify the port that the source MediaTrack has been resumed.
+ */
+ void Resumed();
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t amount = 0;
+
+ // Not owned:
+ // - mSource
+ // - mDest
+ // - mGraph
+ return amount;
+ }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ private:
+ friend class ProcessedMediaTrack;
+ // Never modified after Init()
+ MediaTrack* mSource;
+ ProcessedMediaTrack* mDest;
+ // The input and output numbers are optional, and are currently only used by
+ // Web Audio.
+ const uint16_t mInputNumber;
+ const uint16_t mOutputNumber;
+
+ // Our media track graph
+ MediaTrackGraphImpl* mGraph;
+};
+
+/**
+ * This track processes zero or more input tracks in parallel to produce
+ * its output. The details of how the output is produced are handled by
+ * subclasses overriding the ProcessInput method.
+ */
+class ProcessedMediaTrack : public MediaTrack {
+ public:
+ ProcessedMediaTrack(TrackRate aSampleRate, MediaSegment::Type aType,
+ MediaSegment* aSegment)
+ : MediaTrack(aSampleRate, aType, aSegment),
+ mAutoend(true),
+ mCycleMarker(0) {}
+
+ // Control API.
+ /**
+ * Allocates a new input port attached to source aTrack.
+ * This port can be removed by calling MediaInputPort::Destroy().
+ */
+ already_AddRefed<MediaInputPort> AllocateInputPort(
+ MediaTrack* aTrack, uint16_t aInputNumber = 0,
+ uint16_t aOutputNumber = 0);
+ /**
+ * Queue a message to set the autoend flag on this track (defaults to
+ * true). When this flag is set, and the input track has ended playout
+ * (including if there is no input track), this track automatically
+ * enters the ended state.
+ */
+ virtual void QueueSetAutoend(bool aAutoend);
+
+ ProcessedMediaTrack* AsProcessedTrack() override { return this; }
+
+ friend class MediaTrackGraphImpl;
+
+ // Do not call these from outside MediaTrackGraph.cpp!
+ virtual void AddInput(MediaInputPort* aPort);
+ virtual void RemoveInput(MediaInputPort* aPort) {
+ mInputs.RemoveElement(aPort) || mSuspendedInputs.RemoveElement(aPort);
+ }
+ bool HasInputPort(MediaInputPort* aPort) const {
+ return mInputs.Contains(aPort) || mSuspendedInputs.Contains(aPort);
+ }
+ uint32_t InputPortCount() const {
+ return mInputs.Length() + mSuspendedInputs.Length();
+ }
+ void InputSuspended(MediaInputPort* aPort);
+ void InputResumed(MediaInputPort* aPort);
+ void DestroyImpl() override;
+ void DecrementSuspendCount() override;
+ /**
+ * This gets called after we've computed the blocking states for all
+ * tracks (mBlocked is up to date up to mStateComputedTime).
+ * Also, we've produced output for all tracks up to this one. If this track
+ * is not in a cycle, then all its source tracks have produced data.
+ * Generate output from aFrom to aTo.
+ * This will be called on tracks that have ended. Most track types should
+ * just return immediately if they're ended, but some may wish to update
+ * internal state (see AudioNodeTrack).
+ * ProcessInput is allowed to set mEnded only if ALLOW_END is in aFlags. (This
+ * flag will be set when aTo >= mStateComputedTime, i.e. when we've produced
+ * the last block of data we need to produce.) Otherwise we can get into a
+ * situation where we've determined the track should not block before
+ * mStateComputedTime, but the track ends before mStateComputedTime, violating
+ * the invariant that ended tracks are blocked.
+ */
+ enum { ALLOW_END = 0x01 };
+ virtual void ProcessInput(GraphTime aFrom, GraphTime aTo,
+ uint32_t aFlags) = 0;
+ void SetAutoendImpl(bool aAutoend) { mAutoend = aAutoend; }
+
+ // Only valid after MediaTrackGraphImpl::UpdateTrackOrder() has run.
+ // A DelayNode is considered to break a cycle and so this will not return
+ // true for echo loops, only for muted cycles.
+ bool InMutedCycle() const { return mCycleMarker; }
+
+ // Used by ForwardedInputTrack to propagate the disabled mode along the graph.
+ virtual void OnInputDisabledModeChanged(DisabledTrackMode aMode) {}
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override {
+ size_t amount = MediaTrack::SizeOfExcludingThis(aMallocSizeOf);
+ // Not owned:
+ // - mInputs elements
+ // - mSuspendedInputs elements
+ amount += mInputs.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ amount += mSuspendedInputs.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ return amount;
+ }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ protected:
+ // This state is all accessed only on the media graph thread.
+
+ // The list of all inputs that are not currently suspended.
+ nsTArray<MediaInputPort*> mInputs;
+ // The list of all inputs that are currently suspended.
+ nsTArray<MediaInputPort*> mSuspendedInputs;
+ bool mAutoend;
+ // After UpdateTrackOrder(), mCycleMarker is either 0 or 1 to indicate
+ // whether this track is in a muted cycle. During ordering it can contain
+ // other marker values - see MediaTrackGraphImpl::UpdateTrackOrder().
+ uint32_t mCycleMarker;
+};
+
+/**
+ * There is a single MediaTrackGraph per window.
+ * Additionaly, each OfflineAudioContext object creates its own MediaTrackGraph
+ * object too.
+ */
+class MediaTrackGraph {
+ public:
+ // We ensure that the graph current time advances in multiples of
+ // IdealAudioBlockSize()/AudioStream::PreferredSampleRate(). A track that
+ // never blocks and has the ideal audio rate will produce audio in multiples
+ // of the block size.
+
+ // Initializing a graph that outputs audio can take quite long on some
+ // platforms. Code that want to output audio at some point can express the
+ // fact that they will need an audio track at some point by passing
+ // AUDIO_THREAD_DRIVER when getting an instance of MediaTrackGraph, so that
+ // the graph starts with the right driver.
+ enum GraphDriverType {
+ AUDIO_THREAD_DRIVER,
+ SYSTEM_THREAD_DRIVER,
+ OFFLINE_THREAD_DRIVER
+ };
+ // A MediaTrackGraph running an AudioWorklet must always be run from the
+ // same thread, in order to run js. To acheive this, create the graph with
+ // a SINGLE_THREAD RunType. DIRECT_DRIVER will run the graph directly off
+ // the GraphDriver's thread.
+ enum GraphRunType {
+ DIRECT_DRIVER,
+ SINGLE_THREAD,
+ };
+ static const uint32_t AUDIO_CALLBACK_DRIVER_SHUTDOWN_TIMEOUT = 20 * 1000;
+ static const TrackRate REQUEST_DEFAULT_SAMPLE_RATE = 0;
+ constexpr static const CubebUtils::AudioDeviceID DEFAULT_OUTPUT_DEVICE =
+ nullptr;
+
+ // Main thread only
+ static MediaTrackGraph* GetInstanceIfExists(
+ nsPIDOMWindowInner* aWindow, TrackRate aSampleRate,
+ CubebUtils::AudioDeviceID aOutputDeviceID);
+ static MediaTrackGraph* GetInstance(
+ GraphDriverType aGraphDriverRequested, nsPIDOMWindowInner* aWindow,
+ TrackRate aSampleRate, CubebUtils::AudioDeviceID aOutputDeviceID);
+ static MediaTrackGraph* CreateNonRealtimeInstance(
+ TrackRate aSampleRate, nsPIDOMWindowInner* aWindowId);
+
+ // Idempotent
+ void ForceShutDown();
+
+ virtual void OpenAudioInput(DeviceInputTrack* aTrack) = 0;
+ virtual void CloseAudioInput(DeviceInputTrack* aTrack) = 0;
+
+ // Control API.
+ /**
+ * Create a track that a media decoder (or some other source of
+ * media data, such as a camera) can write to.
+ */
+ SourceMediaTrack* CreateSourceTrack(MediaSegment::Type aType);
+ /**
+ * Create a track that will forward data from its input track.
+ *
+ * A TrackUnionStream can have 0 or 1 input streams. Adding more than that is
+ * an error.
+ *
+ * A TrackUnionStream will end when autoending is enabled (default) and there
+ * is no input, or the input's source is ended. If there is no input and
+ * autoending is disabled, TrackUnionStream will continue to produce silence
+ * for audio or the last video frame for video.
+ */
+ ProcessedMediaTrack* CreateForwardedInputTrack(MediaSegment::Type aType);
+ /**
+ * Create a track that will mix all its audio inputs.
+ */
+ AudioCaptureTrack* CreateAudioCaptureTrack();
+
+ CrossGraphTransmitter* CreateCrossGraphTransmitter(
+ CrossGraphReceiver* aReceiver);
+ CrossGraphReceiver* CreateCrossGraphReceiver(TrackRate aTransmitterRate);
+
+ /**
+ * Add a new track to the graph. Main thread.
+ */
+ void AddTrack(MediaTrack* aTrack);
+
+ /* From the main thread, ask the MTG to resolve the returned promise when
+ * the device has started.
+ * The promise is rejected with NS_ERROR_NOT_AVAILABLE if aTrack
+ * is destroyed, or NS_ERROR_ILLEGAL_DURING_SHUTDOWN if the graph is shut
+ * down, before the promise could be resolved.
+ * (Audio is initially processed in the FallbackDriver's thread while the
+ * device is starting up.)
+ */
+ using GraphStartedPromise = GenericPromise;
+ RefPtr<GraphStartedPromise> NotifyWhenDeviceStarted(MediaTrack* aTrack);
+
+ /* From the main thread, suspend, resume or close an AudioContext. Calls
+ * are not counted. Even Resume calls can be more frequent than Suspend
+ * calls.
+ *
+ * aTracks are the tracks of all the AudioNodes of the AudioContext that
+ * need to be suspended or resumed. Suspend and Resume operations on these
+ * tracks are counted. Resume operations must not outnumber Suspends and a
+ * track will not resume until the number of Resume operations matches the
+ * number of Suspends. This array may be empty if, for example, this is a
+ * second consecutive suspend call and all the nodes are already suspended.
+ *
+ * This can possibly pause the graph thread, releasing system resources, if
+ * all tracks have been suspended/closed.
+ *
+ * When the operation is complete, the returned promise is resolved.
+ */
+ using AudioContextOperationPromise =
+ MozPromise<dom::AudioContextState, bool, true>;
+ RefPtr<AudioContextOperationPromise> ApplyAudioContextOperation(
+ MediaTrack* aDestinationTrack, nsTArray<RefPtr<MediaTrack>> aTracks,
+ dom::AudioContextOperation aOperation);
+
+ bool IsNonRealtime() const;
+ /**
+ * Start processing non-realtime for a specific number of ticks.
+ */
+ void StartNonRealtimeProcessing(uint32_t aTicksToProcess);
+
+ /**
+ * NotifyJSContext() is called on the graph thread before content script
+ * runs.
+ */
+ void NotifyJSContext(JSContext* aCx);
+
+ /**
+ * Media graph thread only.
+ * Dispatches a runnable that will run on the main thread after all
+ * main-thread track state has been updated, i.e., during stable state.
+ *
+ * Should only be called during MediaTrackListener callbacks or during
+ * ProcessedMediaTrack::ProcessInput().
+ *
+ * Note that if called during shutdown the runnable will be ignored and
+ * released on main thread.
+ */
+ void DispatchToMainThreadStableState(already_AddRefed<nsIRunnable> aRunnable);
+
+ /**
+ * Returns graph sample rate in Hz.
+ */
+ TrackRate GraphRate() const { return mSampleRate; }
+
+ double AudioOutputLatency();
+
+ void RegisterCaptureTrackForWindow(uint64_t aWindowId,
+ ProcessedMediaTrack* aCaptureTrack);
+ void UnregisterCaptureTrackForWindow(uint64_t aWindowId);
+ already_AddRefed<MediaInputPort> ConnectToCaptureTrack(
+ uint64_t aWindowId, MediaTrack* aMediaTrack);
+
+ void AssertOnGraphThreadOrNotRunning() const {
+ MOZ_ASSERT(OnGraphThreadOrNotRunning());
+ }
+
+ /**
+ * Returns a watchable of the graph's main-thread observable graph time.
+ * Main thread only.
+ */
+ virtual Watchable<GraphTime>& CurrentTime() = 0;
+
+ /**
+ * Graph thread function to return the time at which all processing has been
+ * completed. Some tracks may have performed processing beyond this time.
+ */
+ GraphTime ProcessedTime() const;
+
+ protected:
+ explicit MediaTrackGraph(TrackRate aSampleRate) : mSampleRate(aSampleRate) {
+ MOZ_COUNT_CTOR(MediaTrackGraph);
+ }
+ MOZ_COUNTED_DTOR_VIRTUAL(MediaTrackGraph)
+
+ // Intended only for assertions, either on graph thread or not running (in
+ // which case we must be on the main thread).
+ virtual bool OnGraphThreadOrNotRunning() const = 0;
+ virtual bool OnGraphThread() const = 0;
+
+ // Intended only for internal assertions. Main thread only.
+ virtual bool Destroyed() const = 0;
+
+ /**
+ * Sample rate at which this graph runs. For real time graphs, this is
+ * the rate of the audio mixer. For offline graphs, this is the rate specified
+ * at construction.
+ */
+ const TrackRate mSampleRate;
+};
+
+} // namespace mozilla
+
+#endif /* MOZILLA_MEDIATRACKGRAPH_H_ */
diff --git a/dom/media/MediaTrackGraphImpl.h b/dom/media/MediaTrackGraphImpl.h
new file mode 100644
index 0000000000..ffdadc7e5e
--- /dev/null
+++ b/dom/media/MediaTrackGraphImpl.h
@@ -0,0 +1,1066 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_MEDIATRACKGRAPHIMPL_H_
+#define MOZILLA_MEDIATRACKGRAPHIMPL_H_
+
+#include "MediaTrackGraph.h"
+
+#include "AudioMixer.h"
+#include "GraphDriver.h"
+#include "DeviceInputTrack.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/WeakPtr.h"
+#include "nsClassHashtable.h"
+#include "nsIMemoryReporter.h"
+#include "nsINamed.h"
+#include "nsIRunnable.h"
+#include "nsIThreadInternal.h"
+#include "nsITimer.h"
+#include "AsyncLogger.h"
+
+namespace mozilla {
+
+namespace media {
+class ShutdownBlocker;
+}
+
+class AudioContextOperationControlMessage;
+template <typename T>
+class LinkedList;
+class GraphRunner;
+
+class DeviceInputTrackManager {
+ public:
+ DeviceInputTrackManager() = default;
+
+ // Returns the current NativeInputTrack.
+ NativeInputTrack* GetNativeInputTrack();
+ // Returns the DeviceInputTrack paired with the device of aID if it exists.
+ // Otherwise, returns nullptr.
+ DeviceInputTrack* GetDeviceInputTrack(CubebUtils::AudioDeviceID aID);
+ // Returns the first added NonNativeInputTrack if any. Otherwise, returns
+ // nullptr.
+ NonNativeInputTrack* GetFirstNonNativeInputTrack();
+ // Adds DeviceInputTrack to the managing list.
+ void Add(DeviceInputTrack* aTrack);
+ // Removes DeviceInputTrack from the managing list.
+ void Remove(DeviceInputTrack* aTrack);
+
+ private:
+ RefPtr<NativeInputTrack> mNativeInputTrack;
+ nsTArray<RefPtr<NonNativeInputTrack>> mNonNativeInputTracks;
+};
+
+/**
+ * A per-track update message passed from the media graph thread to the
+ * main thread.
+ */
+struct TrackUpdate {
+ RefPtr<MediaTrack> mTrack;
+ TrackTime mNextMainThreadCurrentTime;
+ bool mNextMainThreadEnded;
+};
+
+/**
+ * This represents a message run on the graph thread to modify track or graph
+ * state. These are passed from main thread to graph thread through
+ * AppendMessage(), or scheduled on the graph thread with
+ * RunMessageAfterProcessing(). A ControlMessage
+ * always has a weak reference to a particular affected track.
+ */
+class ControlMessage {
+ public:
+ explicit ControlMessage(MediaTrack* aTrack) : mTrack(aTrack) {
+ MOZ_COUNT_CTOR(ControlMessage);
+ }
+ // All these run on the graph thread
+ MOZ_COUNTED_DTOR_VIRTUAL(ControlMessage)
+ // Do the action of this message on the MediaTrackGraph thread. Any actions
+ // affecting graph processing should take effect at mProcessedTime.
+ // All track data for times < mProcessedTime has already been
+ // computed.
+ virtual void Run() = 0;
+ // RunDuringShutdown() is only relevant to messages generated on the main
+ // thread (for AppendMessage()).
+ // When we're shutting down the application, most messages are ignored but
+ // some cleanup messages should still be processed (on the main thread).
+ // This must not add new control messages to the graph.
+ virtual void RunDuringShutdown() {}
+ MediaTrack* GetTrack() { return mTrack; }
+
+ protected:
+ // We do not hold a reference to mTrack. The graph will be holding a reference
+ // to the track until the Destroy message is processed. The last message
+ // referencing a track is the Destroy message for that track.
+ MediaTrack* mTrack;
+};
+
+class MessageBlock {
+ public:
+ nsTArray<UniquePtr<ControlMessage>> mMessages;
+};
+
+/**
+ * The implementation of a media track graph. This class is private to this
+ * file. It's not in the anonymous namespace because MediaTrack needs to
+ * be able to friend it.
+ *
+ * There can be multiple MediaTrackGraph per process: one per document.
+ * Additionaly, each OfflineAudioContext object creates its own MediaTrackGraph
+ * object too.
+ */
+class MediaTrackGraphImpl : public MediaTrackGraph,
+ public GraphInterface,
+ public nsIMemoryReporter,
+ public nsIThreadObserver,
+ public nsITimerCallback,
+ public nsINamed {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIMEMORYREPORTER
+ NS_DECL_NSITHREADOBSERVER
+ NS_DECL_NSITIMERCALLBACK
+ NS_DECL_NSINAMED
+
+ /**
+ * Use aGraphDriverRequested with SYSTEM_THREAD_DRIVER or AUDIO_THREAD_DRIVER
+ * to create a MediaTrackGraph which provides support for real-time audio
+ * and/or video. Set it to OFFLINE_THREAD_DRIVER in order to create a
+ * non-realtime instance which just churns through its inputs and produces
+ * output. Those objects currently only support audio, and are used to
+ * implement OfflineAudioContext. They do not support MediaTrack inputs.
+ */
+ explicit MediaTrackGraphImpl(GraphDriverType aGraphDriverRequested,
+ GraphRunType aRunTypeRequested,
+ TrackRate aSampleRate, uint32_t aChannelCount,
+ CubebUtils::AudioDeviceID aOutputDeviceID,
+ nsISerialEventTarget* aMainThread);
+ static MediaTrackGraphImpl* GetInstance(
+ GraphDriverType aGraphDriverRequested, uint64_t aWindowID,
+ bool aShouldResistFingerprinting, TrackRate aSampleRate,
+ CubebUtils::AudioDeviceID aOutputDeviceID,
+ nsISerialEventTarget* aMainThread);
+ static MediaTrackGraphImpl* GetInstanceIfExists(
+ uint64_t aWindowID, bool aShouldResistFingerprinting,
+ TrackRate aSampleRate, CubebUtils::AudioDeviceID aOutputDeviceID);
+
+ // Intended only for assertions, either on graph thread or not running (in
+ // which case we must be on the main thread).
+ bool OnGraphThreadOrNotRunning() const override;
+ bool OnGraphThread() const override;
+
+ bool Destroyed() const override;
+
+#ifdef DEBUG
+ /**
+ * True if we're on aDriver's thread, or if we're on mGraphRunner's thread
+ * and mGraphRunner is currently run by aDriver.
+ */
+ bool InDriverIteration(const GraphDriver* aDriver) const override;
+#endif
+
+ /**
+ * Unregisters memory reporting and deletes this instance. This should be
+ * called instead of calling the destructor directly.
+ */
+ void Destroy();
+
+ // Main thread only.
+ /**
+ * This runs every time we need to sync state from the media graph thread
+ * to the main thread while the main thread is not in the middle
+ * of a script. It runs during a "stable state" (per HTML5) or during
+ * an event posted to the main thread.
+ * The boolean affects which boolean controlling runnable dispatch is cleared
+ */
+ void RunInStableState(bool aSourceIsMTG);
+ /**
+ * Ensure a runnable to run RunInStableState is posted to the appshell to
+ * run at the next stable state (per HTML5).
+ * See EnsureStableStateEventPosted.
+ */
+ void EnsureRunInStableState();
+ /**
+ * Called to apply a TrackUpdate to its track.
+ */
+ void ApplyTrackUpdate(TrackUpdate* aUpdate) MOZ_REQUIRES(mMonitor);
+ /**
+ * Append a ControlMessage to the message queue. This queue is drained
+ * during RunInStableState; the messages will run on the graph thread.
+ */
+ virtual void AppendMessage(UniquePtr<ControlMessage> aMessage);
+
+ /**
+ * Dispatches a runnable from any thread to the correct main thread for this
+ * MediaTrackGraph.
+ */
+ void Dispatch(already_AddRefed<nsIRunnable>&& aRunnable);
+
+ /**
+ * Make this MediaTrackGraph enter forced-shutdown state. This state
+ * will be noticed by the media graph thread, which will shut down all tracks
+ * and other state controlled by the media graph thread.
+ * This is called during application shutdown, and on document unload if an
+ * AudioContext is using the graph.
+ */
+ void ForceShutDown();
+
+ /**
+ * Sets mShutdownBlocker and makes it block shutdown.
+ * Main thread only. Not idempotent. Returns true if a blocker was added,
+ * false if this failed.
+ */
+ bool AddShutdownBlocker();
+
+ /**
+ * Removes mShutdownBlocker and unblocks shutdown.
+ * Main thread only. Idempotent.
+ */
+ void RemoveShutdownBlocker();
+
+ /**
+ * Called before the thread runs.
+ */
+ void Init();
+
+ /**
+ * Respond to CollectReports with sizes collected on the graph thread.
+ */
+ static void FinishCollectReports(
+ nsIHandleReportCallback* aHandleReport, nsISupports* aData,
+ const nsTArray<AudioNodeSizes>& aAudioTrackSizes);
+
+ // The following methods run on the graph thread (or possibly the main thread
+ // if mLifecycleState > LIFECYCLE_RUNNING)
+ void CollectSizesForMemoryReport(
+ already_AddRefed<nsIHandleReportCallback> aHandleReport,
+ already_AddRefed<nsISupports> aHandlerData);
+
+ /**
+ * Returns true if this MediaTrackGraph should keep running
+ */
+ bool UpdateMainThreadState();
+
+ /**
+ * Proxy method called by GraphDriver to iterate the graph.
+ * If this graph was created with GraphRunType SINGLE_THREAD, mGraphRunner
+ * will take care of calling OneIterationImpl from its thread. Otherwise,
+ * OneIterationImpl is called directly. Output from the graph gets mixed into
+ * aMixer, if it is non-null.
+ */
+ IterationResult OneIteration(GraphTime aStateTime, GraphTime aIterationEnd,
+ AudioMixer* aMixer) override;
+
+ /**
+ * Returns true if this MediaTrackGraph should keep running
+ */
+ IterationResult OneIterationImpl(GraphTime aStateTime,
+ GraphTime aIterationEnd, AudioMixer* aMixer);
+
+ /**
+ * Called from the driver, when the graph thread is about to stop, to tell
+ * the main thread to attempt to begin cleanup. The main thread may either
+ * shutdown or revive the graph depending on whether it receives new
+ * messages.
+ */
+ void SignalMainThreadCleanup();
+
+ /* This is the end of the current iteration, that is, the current time of the
+ * graph. */
+ GraphTime IterationEnd() const;
+
+ /**
+ * Ensure there is an event posted to the main thread to run RunInStableState.
+ * mMonitor must be held.
+ * See EnsureRunInStableState
+ */
+ void EnsureStableStateEventPosted() MOZ_REQUIRES(mMonitor);
+ /**
+ * Generate messages to the main thread to update it for all state changes.
+ * mMonitor must be held.
+ */
+ void PrepareUpdatesToMainThreadState(bool aFinalUpdate)
+ MOZ_REQUIRES(mMonitor);
+ /**
+ * If we are rendering in non-realtime mode, we don't want to send messages to
+ * the main thread at each iteration for performance reasons. We instead
+ * notify the main thread at the same rate
+ */
+ bool ShouldUpdateMainThread();
+ // The following methods are the various stages of RunThread processing.
+ /**
+ * Advance all track state to mStateComputedTime.
+ */
+ void UpdateCurrentTimeForTracks(GraphTime aPrevCurrentTime);
+ /**
+ * Process chunks for all tracks and raise events for properties that have
+ * changed, such as principalId.
+ */
+ void ProcessChunkMetadata(GraphTime aPrevCurrentTime);
+ /**
+ * Process chunks for the given track and interval, and raise events for
+ * properties that have changed, such as principalHandle.
+ */
+ template <typename C, typename Chunk>
+ void ProcessChunkMetadataForInterval(MediaTrack* aTrack, C& aSegment,
+ TrackTime aStart, TrackTime aEnd);
+ /**
+ * Process graph messages in mFrontMessageQueue.
+ */
+ void RunMessagesInQueue();
+ /**
+ * Update track processing order and recompute track blocking until
+ * aEndBlockingDecisions.
+ */
+ void UpdateGraph(GraphTime aEndBlockingDecisions);
+
+ void SwapMessageQueues() MOZ_REQUIRES(mMonitor) {
+ MOZ_ASSERT(OnGraphThreadOrNotRunning());
+ mMonitor.AssertCurrentThreadOwns();
+ MOZ_ASSERT(mFrontMessageQueue.IsEmpty());
+ mFrontMessageQueue.SwapElements(mBackMessageQueue);
+ if (!mFrontMessageQueue.IsEmpty()) {
+ EnsureNextIteration();
+ }
+ }
+ /**
+ * Do all the processing and play the audio and video, from
+ * mProcessedTime to mStateComputedTime.
+ */
+ void Process(AudioMixer* aMixer);
+
+ /**
+ * For use during ProcessedMediaTrack::ProcessInput() or
+ * MediaTrackListener callbacks, when graph state cannot be changed.
+ * Schedules |aMessage| to run after processing, at a time when graph state
+ * can be changed. Graph thread.
+ */
+ void RunMessageAfterProcessing(UniquePtr<ControlMessage> aMessage);
+
+ /**
+ * Resolve the GraphStartedPromise when the driver has started processing on
+ * the audio thread after the device has started.
+ */
+ void NotifyWhenGraphStarted(RefPtr<MediaTrack> aTrack,
+ MozPromiseHolder<GraphStartedPromise>&& aHolder);
+
+ /**
+ * Apply an AudioContext operation (suspend/resume/close), on the graph
+ * thread.
+ */
+ void ApplyAudioContextOperationImpl(
+ AudioContextOperationControlMessage* aMessage);
+
+ /**
+ * Determine if we have any audio tracks, or are about to add any audiotracks.
+ */
+ bool AudioTrackPresent();
+
+ /**
+ * Schedules a replacement GraphDriver in mNextDriver, if necessary.
+ */
+ void CheckDriver();
+
+ /**
+ * Sort mTracks so that every track not in a cycle is after any tracks
+ * it depends on, and every track in a cycle is marked as being in a cycle.
+ */
+ void UpdateTrackOrder();
+
+ /**
+ * Returns smallest value of t such that t is a multiple of
+ * WEBAUDIO_BLOCK_SIZE and t >= aTime.
+ */
+ static GraphTime RoundUpToEndOfAudioBlock(GraphTime aTime);
+ /**
+ * Returns smallest value of t such that t is a multiple of
+ * WEBAUDIO_BLOCK_SIZE and t > aTime.
+ */
+ static GraphTime RoundUpToNextAudioBlock(GraphTime aTime);
+ /**
+ * Produce data for all tracks >= aTrackIndex for the current time interval.
+ * Advances block by block, each iteration producing data for all tracks
+ * for a single block.
+ * This is called whenever we have an AudioNodeTrack in the graph.
+ */
+ void ProduceDataForTracksBlockByBlock(uint32_t aTrackIndex,
+ TrackRate aSampleRate);
+ /**
+ * If aTrack will underrun between aTime, and aEndBlockingDecisions, returns
+ * the time at which the underrun will start. Otherwise return
+ * aEndBlockingDecisions.
+ */
+ GraphTime WillUnderrun(MediaTrack* aTrack, GraphTime aEndBlockingDecisions);
+
+ /**
+ * Given a graph time aTime, convert it to a track time taking into
+ * account the time during which aTrack is scheduled to be blocked.
+ */
+ TrackTime GraphTimeToTrackTimeWithBlocking(const MediaTrack* aTrack,
+ GraphTime aTime) const;
+
+ /**
+ * If aTrack needs an audio track but doesn't have one, create it.
+ * If aTrack doesn't need an audio track but has one, destroy it.
+ */
+ void CreateOrDestroyAudioTracks(MediaTrack* aTrack);
+ /**
+ * Queue audio (mix of track audio and silence for blocked intervals)
+ * to the audio output track. Returns the number of frames played.
+ */
+ struct TrackAndKey {
+ MediaTrack* mTrack;
+ void* mKey;
+ };
+ struct TrackKeyAndVolume {
+ MediaTrack* mTrack;
+ void* mKey;
+ float mVolume;
+ };
+ TrackTime PlayAudio(AudioMixer* aMixer, const TrackKeyAndVolume& aTkv,
+ GraphTime aPlayedTime);
+
+ /* Do not call this directly. For users who need to get a DeviceInputTrack,
+ * use DeviceInputTrack::OpenAudio() instead. This should only be used in
+ * DeviceInputTrack to get the existing DeviceInputTrack paired with the given
+ * device in this graph. Main thread only.*/
+ DeviceInputTrack* GetDeviceInputTrackMainThread(
+ CubebUtils::AudioDeviceID aID);
+
+ /* Do not call this directly. This should only be used in DeviceInputTrack to
+ * get the existing NativeInputTrackMain thread only.*/
+ NativeInputTrack* GetNativeInputTrackMainThread();
+
+ /* Runs off a message on the graph thread when something requests audio from
+ * an input audio device of ID aID, and delivers the input audio frames to
+ * aListener. */
+ void OpenAudioInputImpl(DeviceInputTrack* aTrack);
+ /* Called on the main thread when something requests audio from an input
+ * audio device aID. */
+ virtual void OpenAudioInput(DeviceInputTrack* aTrack) override;
+
+ /* Runs off a message on the graph when input audio from aID is not needed
+ * anymore, for a particular track. It can be that other tracks still need
+ * audio from this audio input device. */
+ void CloseAudioInputImpl(DeviceInputTrack* aTrack);
+ /* Called on the main thread when input audio from aID is not needed
+ * anymore, for a particular track. It can be that other tracks still need
+ * audio from this audio input device. */
+ virtual void CloseAudioInput(DeviceInputTrack* aTrack) override;
+
+ /* Add or remove an audio output for this track. All tracks that have an
+ * audio output are mixed and written to a single audio output stream. */
+ void RegisterAudioOutput(MediaTrack* aTrack, void* aKey);
+ void UnregisterAudioOutput(MediaTrack* aTrack, void* aKey);
+ void UnregisterAllAudioOutputs(MediaTrack* aTrack);
+ void SetAudioOutputVolume(MediaTrack* aTrack, void* aKey, float aVolume);
+
+ /* Called on the graph thread when the input device settings should be
+ * reevaluated, for example, if the channel count of the input track should
+ * be changed. */
+ void ReevaluateInputDevice(CubebUtils::AudioDeviceID aID);
+
+ /* Called on the graph thread when there is new output data for listeners.
+ * This is the mixed audio output of this MediaTrackGraph. */
+ void NotifyOutputData(AudioDataValue* aBuffer, size_t aFrames,
+ TrackRate aRate, uint32_t aChannels) override;
+ /* Called on the graph thread after an AudioCallbackDriver with an input
+ * stream has stopped. */
+ void NotifyInputStopped() override;
+ /* Called on the graph thread when there is new input data for listeners. This
+ * is the raw audio input for this MediaTrackGraph. */
+ void NotifyInputData(const AudioDataValue* aBuffer, size_t aFrames,
+ TrackRate aRate, uint32_t aChannels,
+ uint32_t aAlreadyBuffered) override;
+ /* Called every time there are changes to input/output audio devices like
+ * plug/unplug etc. This can be called on any thread, and posts a message to
+ * the main thread so that it can post a message to the graph thread. */
+ void DeviceChanged() override;
+ /* Called every time there are changes to input/output audio devices. This is
+ * called on the graph thread. */
+ void DeviceChangedImpl();
+
+ /**
+ * Compute how much track data we would like to buffer for aTrack.
+ */
+ TrackTime GetDesiredBufferEnd(MediaTrack* aTrack);
+ /**
+ * Returns true when there are no active tracks.
+ */
+ bool IsEmpty() const {
+ MOZ_ASSERT(
+ OnGraphThreadOrNotRunning() ||
+ (NS_IsMainThread() &&
+ LifecycleStateRef() >= LIFECYCLE_WAITING_FOR_MAIN_THREAD_CLEANUP));
+ return mTracks.IsEmpty() && mSuspendedTracks.IsEmpty() && mPortCount == 0;
+ }
+
+ /**
+ * Add aTrack to the graph and initializes its graph-specific state.
+ */
+ void AddTrackGraphThread(MediaTrack* aTrack);
+ /**
+ * Remove aTrack from the graph. Ensures that pending messages about the
+ * track back to the main thread are flushed.
+ */
+ void RemoveTrackGraphThread(MediaTrack* aTrack);
+ /**
+ * Remove a track from the graph. Main thread.
+ */
+ void RemoveTrack(MediaTrack* aTrack);
+ /**
+ * Remove aPort from the graph and release it.
+ */
+ void DestroyPort(MediaInputPort* aPort);
+ /**
+ * Mark the media track order as dirty.
+ */
+ void SetTrackOrderDirty() {
+ MOZ_ASSERT(OnGraphThreadOrNotRunning());
+ mTrackOrderDirty = true;
+ }
+
+ // Get the current maximum channel count. Graph thread only.
+ uint32_t AudioOutputChannelCount() const;
+ // Set a new maximum channel count. Graph thread only.
+ void SetMaxOutputChannelCount(uint32_t aMaxChannelCount);
+
+ double AudioOutputLatency();
+
+ /**
+ * The audio input channel count for a MediaTrackGraph is the max of all the
+ * channel counts requested by the listeners. The max channel count is
+ * delivered to the listeners themselves, and they take care of downmixing.
+ */
+ uint32_t AudioInputChannelCount(CubebUtils::AudioDeviceID aID);
+
+ AudioInputType AudioInputDevicePreference(CubebUtils::AudioDeviceID aID);
+
+ double MediaTimeToSeconds(GraphTime aTime) const {
+ NS_ASSERTION(aTime > -TRACK_TIME_MAX && aTime <= TRACK_TIME_MAX,
+ "Bad time");
+ return static_cast<double>(aTime) / GraphRate();
+ }
+
+ /**
+ * Signal to the graph that the thread has paused indefinitly,
+ * or resumed.
+ */
+ void PausedIndefinitly();
+ void ResumedFromPaused();
+
+ /**
+ * Not safe to call off the MediaTrackGraph thread unless monitor is held!
+ */
+ GraphDriver* CurrentDriver() const MOZ_NO_THREAD_SAFETY_ANALYSIS {
+#ifdef DEBUG
+ if (!OnGraphThreadOrNotRunning()) {
+ mMonitor.AssertCurrentThreadOwns();
+ }
+#endif
+ return mDriver;
+ }
+
+ /**
+ * Effectively set the new driver, while we are switching.
+ * It is only safe to call this at the very end of an iteration, when there
+ * has been a SwitchAtNextIteration call during the iteration. The driver
+ * should return and pass the control to the new driver shortly after.
+ * Monitor must be held.
+ */
+ void SetCurrentDriver(GraphDriver* aDriver) {
+ MOZ_ASSERT_IF(mGraphDriverRunning, InDriverIteration(mDriver));
+ MOZ_ASSERT_IF(!mGraphDriverRunning, NS_IsMainThread());
+ MonitorAutoLock lock(GetMonitor());
+ mDriver = aDriver;
+ }
+
+ GraphDriver* NextDriver() const {
+ MOZ_ASSERT(OnGraphThread());
+ return mNextDriver;
+ }
+
+ bool Switching() const { return NextDriver(); }
+
+ void SwitchAtNextIteration(GraphDriver* aNextDriver);
+
+ Monitor& GetMonitor() { return mMonitor; }
+
+ void EnsureNextIteration() { CurrentDriver()->EnsureNextIteration(); }
+
+ // Capture API. This allows to get a mixed-down output for a window.
+ void RegisterCaptureTrackForWindow(uint64_t aWindowId,
+ ProcessedMediaTrack* aCaptureTrack);
+ void UnregisterCaptureTrackForWindow(uint64_t aWindowId);
+ already_AddRefed<MediaInputPort> ConnectToCaptureTrack(
+ uint64_t aWindowId, MediaTrack* aMediaTrack);
+
+ Watchable<GraphTime>& CurrentTime() override;
+
+ /**
+ * Interrupt any JS running on the graph thread.
+ * Called on the main thread when shutting down the graph.
+ */
+ void InterruptJS();
+
+ class TrackSet {
+ public:
+ class iterator {
+ public:
+ explicit iterator(MediaTrackGraphImpl& aGraph)
+ : mGraph(&aGraph), mArrayNum(-1), mArrayIndex(0) {
+ ++(*this);
+ }
+ iterator() : mGraph(nullptr), mArrayNum(2), mArrayIndex(0) {}
+ MediaTrack* operator*() { return Array()->ElementAt(mArrayIndex); }
+ iterator operator++() {
+ ++mArrayIndex;
+ while (mArrayNum < 2 &&
+ (mArrayNum < 0 || mArrayIndex >= Array()->Length())) {
+ ++mArrayNum;
+ mArrayIndex = 0;
+ }
+ return *this;
+ }
+ bool operator==(const iterator& aOther) const {
+ return mArrayNum == aOther.mArrayNum &&
+ mArrayIndex == aOther.mArrayIndex;
+ }
+ bool operator!=(const iterator& aOther) const {
+ return !(*this == aOther);
+ }
+
+ private:
+ nsTArray<MediaTrack*>* Array() {
+ return mArrayNum == 0 ? &mGraph->mTracks : &mGraph->mSuspendedTracks;
+ }
+ MediaTrackGraphImpl* mGraph;
+ int mArrayNum;
+ uint32_t mArrayIndex;
+ };
+
+ explicit TrackSet(MediaTrackGraphImpl& aGraph) : mGraph(aGraph) {}
+ iterator begin() { return iterator(mGraph); }
+ iterator end() { return iterator(); }
+
+ private:
+ MediaTrackGraphImpl& mGraph;
+ };
+ TrackSet AllTracks() { return TrackSet(*this); }
+
+ // Data members
+
+ /*
+ * If set, the GraphRunner class handles handing over data from audio
+ * callbacks to a common single thread, shared across GraphDrivers.
+ */
+ const RefPtr<GraphRunner> mGraphRunner;
+
+ /**
+ * Main-thread view of the number of tracks in this graph, for lifetime
+ * management.
+ *
+ * When this becomes zero, the graph is marked as forbidden to add more
+ * tracks to. It will be shut down shortly after.
+ */
+ size_t mMainThreadTrackCount = 0;
+
+ /**
+ * Main-thread view of the number of ports in this graph, to catch bugs.
+ *
+ * When this becomes zero, and mMainThreadTrackCount is 0, the graph is
+ * marked as forbidden to add more ControlMessages to. It will be shut down
+ * shortly after.
+ */
+ size_t mMainThreadPortCount = 0;
+
+ /**
+ * Graphs own owning references to their driver, until shutdown. When a driver
+ * switch occur, previous driver is either deleted, or it's ownership is
+ * passed to a event that will take care of the asynchronous cleanup, as
+ * audio track can take some time to shut down.
+ * Accessed on both the main thread and the graph thread; both read and write.
+ * Must hold monitor to access it.
+ */
+ RefPtr<GraphDriver> mDriver;
+
+ // Set during an iteration to switch driver after the iteration has finished.
+ // Should the current iteration be the last iteration, the next driver will be
+ // discarded. Access through SwitchAtNextIteration()/NextDriver(). Graph
+ // thread only.
+ RefPtr<GraphDriver> mNextDriver;
+
+ // The following state is managed on the graph thread only, unless
+ // mLifecycleState > LIFECYCLE_RUNNING in which case the graph thread
+ // is not running and this state can be used from the main thread.
+
+ /**
+ * The graph keeps a reference to each track.
+ * References are maintained manually to simplify reordering without
+ * unnecessary thread-safe refcount changes.
+ * Must satisfy OnGraphThreadOrNotRunning().
+ */
+ nsTArray<MediaTrack*> mTracks;
+ /**
+ * This stores MediaTracks that are part of suspended AudioContexts.
+ * mTracks and mSuspendTracks are disjoint sets: a track is either suspended
+ * or not suspended. Suspended tracks are not ordered in UpdateTrackOrder,
+ * and are therefore not doing any processing.
+ * Must satisfy OnGraphThreadOrNotRunning().
+ */
+ nsTArray<MediaTrack*> mSuspendedTracks;
+ /**
+ * Tracks from mFirstCycleBreaker to the end of mTracks produce output before
+ * they receive input. They correspond to DelayNodes that are in cycles.
+ */
+ uint32_t mFirstCycleBreaker;
+ /**
+ * Blocking decisions have been computed up to this time.
+ * Between each iteration, this is the same as mProcessedTime.
+ */
+ GraphTime mStateComputedTime = 0;
+ /**
+ * All track contents have been computed up to this time.
+ * The next batch of updates from the main thread will be processed
+ * at this time. This is behind mStateComputedTime during processing.
+ */
+ GraphTime mProcessedTime = 0;
+ /**
+ * The end of the current iteration. Only access on the graph thread.
+ */
+ GraphTime mIterationEndTime = 0;
+ /**
+ * The graph should stop processing at this time.
+ */
+ GraphTime mEndTime;
+ /**
+ * Date of the last time we updated the main thread with the graph state.
+ */
+ TimeStamp mLastMainThreadUpdate;
+ /**
+ * Number of active MediaInputPorts
+ */
+ int32_t mPortCount;
+ /**
+ * Runnables to run after the next update to main thread state, but that are
+ * still waiting for the next iteration to finish.
+ */
+ nsTArray<nsCOMPtr<nsIRunnable>> mPendingUpdateRunnables;
+
+ /**
+ * Devices to use for cubeb output, or nullptr for default device.
+ * A MediaTrackGraph always has an output (even if silent).
+ *
+ * All mOutputDeviceID access is on the graph thread.
+ */
+ CubebUtils::AudioDeviceID mOutputDeviceID;
+
+ /**
+ * List of resume operations waiting for a switch to an AudioCallbackDriver.
+ */
+ class PendingResumeOperation {
+ public:
+ explicit PendingResumeOperation(
+ AudioContextOperationControlMessage* aMessage);
+ void Apply(MediaTrackGraphImpl* aGraph);
+ void Abort();
+ MediaTrack* DestinationTrack() const { return mDestinationTrack; }
+
+ private:
+ RefPtr<MediaTrack> mDestinationTrack;
+ nsTArray<RefPtr<MediaTrack>> mTracks;
+ MozPromiseHolder<AudioContextOperationPromise> mHolder;
+ };
+ AutoTArray<PendingResumeOperation, 1> mPendingResumeOperations;
+
+ // mMonitor guards the data below.
+ // MediaTrackGraph normally does its work without holding mMonitor, so it is
+ // not safe to just grab mMonitor from some thread and start monkeying with
+ // the graph. Instead, communicate with the graph thread using provided
+ // mechanisms such as the ControlMessage queue.
+ Monitor mMonitor;
+
+ // Data guarded by mMonitor (must always be accessed with mMonitor held,
+ // regardless of the value of mLifecycleState).
+
+ /**
+ * State to copy to main thread
+ */
+ nsTArray<TrackUpdate> mTrackUpdates MOZ_GUARDED_BY(mMonitor);
+ /**
+ * Runnables to run after the next update to main thread state.
+ */
+ nsTArray<nsCOMPtr<nsIRunnable>> mUpdateRunnables MOZ_GUARDED_BY(mMonitor);
+ /**
+ * A list of batches of messages to process. Each batch is processed
+ * as an atomic unit.
+ */
+ /*
+ * Message queue processed by the MTG thread during an iteration.
+ * Accessed on graph thread only.
+ */
+ nsTArray<MessageBlock> mFrontMessageQueue;
+ /*
+ * Message queue in which the main thread appends messages.
+ * Access guarded by mMonitor.
+ */
+ nsTArray<MessageBlock> mBackMessageQueue MOZ_GUARDED_BY(mMonitor);
+
+ /* True if there will messages to process if we swap the message queues. */
+ bool MessagesQueued() const MOZ_REQUIRES(mMonitor) {
+ mMonitor.AssertCurrentThreadOwns();
+ return !mBackMessageQueue.IsEmpty();
+ }
+ /**
+ * This enum specifies where this graph is in its lifecycle. This is used
+ * to control shutdown.
+ * Shutdown is tricky because it can happen in two different ways:
+ * 1) Shutdown due to inactivity. RunThread() detects that it has no
+ * pending messages and no tracks, and exits. The next RunInStableState()
+ * checks if there are new pending messages from the main thread (true only
+ * if new track creation raced with shutdown); if there are, it revives
+ * RunThread(), otherwise it commits to shutting down the graph. New track
+ * creation after this point will create a new graph. An async event is
+ * dispatched to Shutdown() the graph's threads and then delete the graph
+ * object.
+ * 2) Forced shutdown at application shutdown, completion of a non-realtime
+ * graph, or document unload. A flag is set, RunThread() detects the flag
+ * and exits, the next RunInStableState() detects the flag, and dispatches
+ * the async event to Shutdown() the graph's threads. However the graph
+ * object is not deleted. New messages for the graph are processed
+ * synchronously on the main thread if necessary. When the last track is
+ * destroyed, the graph object is deleted.
+ *
+ * This should be kept in sync with the LifecycleState_str array in
+ * MediaTrackGraph.cpp
+ */
+ enum LifecycleState {
+ // The graph thread hasn't started yet.
+ LIFECYCLE_THREAD_NOT_STARTED,
+ // RunThread() is running normally.
+ LIFECYCLE_RUNNING,
+ // In the following states, the graph thread is not running so
+ // all "graph thread only" state in this class can be used safely
+ // on the main thread.
+ // RunThread() has exited and we're waiting for the next
+ // RunInStableState(), at which point we can clean up the main-thread
+ // side of the graph.
+ LIFECYCLE_WAITING_FOR_MAIN_THREAD_CLEANUP,
+ // RunInStableState() posted a ShutdownRunnable, and we're waiting for it
+ // to shut down the graph thread(s).
+ LIFECYCLE_WAITING_FOR_THREAD_SHUTDOWN,
+ // Graph threads have shut down but we're waiting for remaining tracks
+ // to be destroyed. Only happens during application shutdown and on
+ // completed non-realtime graphs, since normally we'd only shut down a
+ // realtime graph when it has no tracks.
+ LIFECYCLE_WAITING_FOR_TRACK_DESTRUCTION
+ };
+
+ /**
+ * Modified only in mMonitor. Transitions to
+ * LIFECYCLE_WAITING_FOR_MAIN_THREAD_CLEANUP occur on the graph thread at
+ * the end of an iteration. All other transitions occur on the main thread.
+ */
+ LifecycleState mLifecycleState MOZ_GUARDED_BY(mMonitor);
+ LifecycleState& LifecycleStateRef() MOZ_NO_THREAD_SAFETY_ANALYSIS {
+#if DEBUG
+ if (mGraphDriverRunning) {
+ mMonitor.AssertCurrentThreadOwns();
+ } else {
+ MOZ_ASSERT(NS_IsMainThread());
+ }
+#endif
+ return mLifecycleState;
+ }
+ const LifecycleState& LifecycleStateRef() const
+ MOZ_NO_THREAD_SAFETY_ANALYSIS {
+#if DEBUG
+ if (mGraphDriverRunning) {
+ mMonitor.AssertCurrentThreadOwns();
+ } else {
+ MOZ_ASSERT(NS_IsMainThread());
+ }
+#endif
+ return mLifecycleState;
+ }
+
+ /**
+ * True once the graph thread has received the message from ForceShutDown().
+ * This is checked in the decision to shut down the
+ * graph thread so that control messages dispatched before forced shutdown
+ * are processed on the graph thread.
+ * Only set on the graph thread.
+ * Can be read safely on the thread currently owning the graph, as indicated
+ * by mLifecycleState.
+ */
+ bool mForceShutDownReceived = false;
+ /**
+ * true when InterruptJS() has been called, because shutdown (normal or
+ * forced) has commenced. Set on the main thread under mMonitor and read on
+ * the graph thread under mMonitor.
+ **/
+ bool mInterruptJSCalled MOZ_GUARDED_BY(mMonitor) = false;
+
+ /**
+ * Remove this blocker to unblock shutdown.
+ * Only accessed on the main thread.
+ **/
+ RefPtr<media::ShutdownBlocker> mShutdownBlocker;
+
+ /**
+ * True when we have posted an event to the main thread to run
+ * RunInStableState() and the event hasn't run yet.
+ * Accessed on both main and MTG thread, mMonitor must be held.
+ */
+ bool mPostedRunInStableStateEvent MOZ_GUARDED_BY(mMonitor);
+
+ /**
+ * The JSContext of the graph thread. Set under mMonitor on only the graph
+ * or GraphRunner thread. Once set this does not change until reset when
+ * the thread is about to exit. Read under mMonitor on the main thread to
+ * interrupt running JS for forced shutdown.
+ **/
+ JSContext* mJSContext MOZ_GUARDED_BY(mMonitor) = nullptr;
+
+ // Main thread only
+
+ /**
+ * Messages posted by the current event loop task. These are forwarded to
+ * the media graph thread during RunInStableState. We can't forward them
+ * immediately because we want all messages between stable states to be
+ * processed as an atomic batch.
+ */
+ nsTArray<UniquePtr<ControlMessage>> mCurrentTaskMessageQueue;
+ /**
+ * True from when RunInStableState sets mLifecycleState to LIFECYCLE_RUNNING,
+ * until RunInStableState has determined that mLifecycleState is >
+ * LIFECYCLE_RUNNING.
+ */
+ Atomic<bool> mGraphDriverRunning;
+ /**
+ * True when a stable state runner has been posted to the appshell to run
+ * RunInStableState at the next stable state.
+ * Only accessed on the main thread.
+ */
+ bool mPostedRunInStableState;
+ /**
+ * True when processing real-time audio/video. False when processing
+ * non-realtime audio.
+ */
+ const bool mRealtime;
+ /**
+ * True when a change has happened which requires us to recompute the track
+ * blocking order.
+ */
+ bool mTrackOrderDirty;
+ const RefPtr<nsISerialEventTarget> mMainThread;
+
+ // used to limit graph shutdown time
+ // Only accessed on the main thread.
+ nsCOMPtr<nsITimer> mShutdownTimer;
+
+ protected:
+ virtual ~MediaTrackGraphImpl();
+
+ private:
+ MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf)
+
+ // Set a new native iput device when the current native input device is close.
+ // Main thread only.
+ void SetNewNativeInput();
+
+ /**
+ * This class uses manual memory management, and all pointers to it are raw
+ * pointers. However, in order for it to implement nsIMemoryReporter, it needs
+ * to implement nsISupports and so be ref-counted. So it maintains a single
+ * nsRefPtr to itself, giving it a ref-count of 1 during its entire lifetime,
+ * and Destroy() nulls this self-reference in order to trigger self-deletion.
+ */
+ RefPtr<MediaTrackGraphImpl> mSelfRef;
+
+ struct WindowAndTrack {
+ uint64_t mWindowId;
+ RefPtr<ProcessedMediaTrack> mCaptureTrackSink;
+ };
+ /**
+ * Track for window audio capture.
+ */
+ nsTArray<WindowAndTrack> mWindowCaptureTracks;
+ /**
+ * Tracks that have their audio output mixed and written to an audio output
+ * device.
+ */
+ nsTArray<TrackKeyAndVolume> mAudioOutputs;
+
+ /**
+ * Global volume scale. Used when running tests so that the output is not too
+ * loud.
+ */
+ const float mGlobalVolume;
+
+#ifdef DEBUG
+ /**
+ * Used to assert when AppendMessage() runs ControlMessages synchronously.
+ */
+ bool mCanRunMessagesSynchronously;
+#endif
+
+ /**
+ * The graph's main-thread observable graph time.
+ * Updated by the stable state runnable after each iteration.
+ */
+ Watchable<GraphTime> mMainThreadGraphTime;
+
+ /**
+ * Set based on mProcessedTime at end of iteration.
+ * Read by stable state runnable on main thread. Protected by mMonitor.
+ */
+ GraphTime mNextMainThreadGraphTime MOZ_GUARDED_BY(mMonitor) = 0;
+
+ /**
+ * Cached audio output latency, in seconds. Main thread only. This is reset
+ * whenever the audio device running this MediaTrackGraph changes.
+ */
+ double mAudioOutputLatency;
+
+ /**
+ * The max audio output channel count the default audio output device
+ * supports. This is cached here because it can be expensive to query. The
+ * cache is invalidated when the device is changed. This is initialized in the
+ * ctor, and the read/write only on the graph thread.
+ */
+ uint32_t mMaxOutputChannelCount;
+
+ /**
+ * Manage the native or non-native input device in graph. Main thread only.
+ */
+ DeviceInputTrackManager mDeviceInputTrackManagerMainThread;
+
+ /**
+ * Manage the native or non-native input device in graph. Graph thread only.
+ */
+ DeviceInputTrackManager mDeviceInputTrackManagerGraphThread;
+};
+
+} // namespace mozilla
+
+template <>
+class nsDefaultComparator<mozilla::MediaTrackGraphImpl::TrackKeyAndVolume,
+ mozilla::MediaTrackGraphImpl::TrackAndKey> {
+ public:
+ bool Equals(
+ const mozilla::MediaTrackGraphImpl::TrackKeyAndVolume& aElement,
+ const mozilla::MediaTrackGraphImpl::TrackAndKey& aTrackAndKey) const {
+ return aElement.mTrack == aTrackAndKey.mTrack &&
+ aElement.mKey == aTrackAndKey.mKey;
+ }
+};
+
+#endif /* MEDIATRACKGRAPHIMPL_H_ */
diff --git a/dom/media/MediaTrackList.cpp b/dom/media/MediaTrackList.cpp
new file mode 100644
index 0000000000..97702f485d
--- /dev/null
+++ b/dom/media/MediaTrackList.cpp
@@ -0,0 +1,142 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 et tw=78: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaTrack.h"
+#include "MediaTrackList.h"
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/dom/HTMLMediaElement.h"
+#include "mozilla/dom/AudioTrack.h"
+#include "mozilla/dom/VideoTrack.h"
+#include "mozilla/dom/TrackEvent.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla::dom {
+
+MediaTrackList::MediaTrackList(nsIGlobalObject* aOwnerObject,
+ HTMLMediaElement* aMediaElement)
+ : DOMEventTargetHelper(aOwnerObject), mMediaElement(aMediaElement) {}
+
+MediaTrackList::~MediaTrackList() = default;
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(MediaTrackList, DOMEventTargetHelper,
+ mTracks, mMediaElement)
+
+NS_IMPL_ADDREF_INHERITED(MediaTrackList, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(MediaTrackList, DOMEventTargetHelper)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaTrackList)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+MediaTrack* MediaTrackList::operator[](uint32_t aIndex) {
+ return mTracks.ElementAt(aIndex);
+}
+
+MediaTrack* MediaTrackList::IndexedGetter(uint32_t aIndex, bool& aFound) {
+ aFound = aIndex < mTracks.Length();
+ if (!aFound) {
+ return nullptr;
+ }
+ return mTracks[aIndex];
+}
+
+MediaTrack* MediaTrackList::GetTrackById(const nsAString& aId) {
+ for (uint32_t i = 0; i < mTracks.Length(); ++i) {
+ if (aId.Equals(mTracks[i]->GetId())) {
+ return mTracks[i];
+ }
+ }
+ return nullptr;
+}
+
+void MediaTrackList::AddTrack(MediaTrack* aTrack) {
+ MOZ_ASSERT(aTrack->GetOwnerGlobal() == GetOwnerGlobal(),
+ "Where is this track from?");
+ mTracks.AppendElement(aTrack);
+ aTrack->SetTrackList(this);
+ CreateAndDispatchTrackEventRunner(aTrack, u"addtrack"_ns);
+
+ if (HTMLMediaElement* element = GetMediaElement()) {
+ element->NotifyMediaTrackAdded(aTrack);
+ }
+
+ if ((!aTrack->AsAudioTrack() || !aTrack->AsAudioTrack()->Enabled()) &&
+ (!aTrack->AsVideoTrack() || !aTrack->AsVideoTrack()->Selected())) {
+ // Track not enabled, no need to notify media element.
+ return;
+ }
+
+ if (HTMLMediaElement* element = GetMediaElement()) {
+ element->NotifyMediaTrackEnabled(aTrack);
+ }
+}
+
+void MediaTrackList::RemoveTrack(const RefPtr<MediaTrack>& aTrack) {
+ mTracks.RemoveElement(aTrack);
+ aTrack->SetEnabledInternal(false, MediaTrack::FIRE_NO_EVENTS);
+ aTrack->SetTrackList(nullptr);
+ CreateAndDispatchTrackEventRunner(aTrack, u"removetrack"_ns);
+ if (HTMLMediaElement* element = GetMediaElement()) {
+ element->NotifyMediaTrackRemoved(aTrack);
+ }
+}
+
+void MediaTrackList::RemoveTracks() {
+ while (!mTracks.IsEmpty()) {
+ RefPtr<MediaTrack> track = mTracks.LastElement();
+ RemoveTrack(track);
+ }
+}
+
+already_AddRefed<AudioTrack> MediaTrackList::CreateAudioTrack(
+ nsIGlobalObject* aOwnerGlobal, const nsAString& aId, const nsAString& aKind,
+ const nsAString& aLabel, const nsAString& aLanguage, bool aEnabled,
+ AudioStreamTrack* aAudioTrack) {
+ RefPtr<AudioTrack> track = new AudioTrack(aOwnerGlobal, aId, aKind, aLabel,
+ aLanguage, aEnabled, aAudioTrack);
+ return track.forget();
+}
+
+already_AddRefed<VideoTrack> MediaTrackList::CreateVideoTrack(
+ nsIGlobalObject* aOwnerGlobal, const nsAString& aId, const nsAString& aKind,
+ const nsAString& aLabel, const nsAString& aLanguage,
+ VideoStreamTrack* aVideoTrack) {
+ RefPtr<VideoTrack> track =
+ new VideoTrack(aOwnerGlobal, aId, aKind, aLabel, aLanguage, aVideoTrack);
+ return track.forget();
+}
+
+void MediaTrackList::EmptyTracks() {
+ for (uint32_t i = 0; i < mTracks.Length(); ++i) {
+ mTracks[i]->SetEnabledInternal(false, MediaTrack::FIRE_NO_EVENTS);
+ mTracks[i]->SetTrackList(nullptr);
+ }
+ mTracks.Clear();
+}
+
+void MediaTrackList::CreateAndDispatchChangeEvent() {
+ RefPtr<AsyncEventDispatcher> asyncDispatcher =
+ new AsyncEventDispatcher(this, u"change"_ns, CanBubble::eNo);
+ asyncDispatcher->PostDOMEvent();
+}
+
+void MediaTrackList::CreateAndDispatchTrackEventRunner(
+ MediaTrack* aTrack, const nsAString& aEventName) {
+ TrackEventInit eventInit;
+
+ if (aTrack->AsAudioTrack()) {
+ eventInit.mTrack.SetValue().SetAsAudioTrack() = aTrack->AsAudioTrack();
+ } else if (aTrack->AsVideoTrack()) {
+ eventInit.mTrack.SetValue().SetAsVideoTrack() = aTrack->AsVideoTrack();
+ }
+
+ RefPtr<TrackEvent> event =
+ TrackEvent::Constructor(this, aEventName, eventInit);
+
+ RefPtr<AsyncEventDispatcher> asyncDispatcher =
+ new AsyncEventDispatcher(this, event);
+ asyncDispatcher->PostDOMEvent();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/MediaTrackList.h b/dom/media/MediaTrackList.h
new file mode 100644
index 0000000000..5ad08b0f65
--- /dev/null
+++ b/dom/media/MediaTrackList.h
@@ -0,0 +1,111 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 et tw=78: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_MediaTrackList_h
+#define mozilla_dom_MediaTrackList_h
+
+#include "mozilla/DOMEventTargetHelper.h"
+
+namespace mozilla {
+class DOMMediaStream;
+
+namespace dom {
+
+class AudioStreamTrack;
+class AudioTrack;
+class AudioTrackList;
+class HTMLMediaElement;
+class MediaTrack;
+class VideoStreamTrack;
+class VideoTrack;
+class VideoTrackList;
+
+/**
+ * Base class of AudioTrackList and VideoTrackList. The AudioTrackList and
+ * VideoTrackList objects represent a dynamic list of zero or more audio and
+ * video tracks respectively.
+ *
+ * When a media element is to forget its media-resource-specific tracks, its
+ * audio track list and video track list will be emptied.
+ */
+class MediaTrackList : public DOMEventTargetHelper {
+ public:
+ MediaTrackList(nsIGlobalObject* aOwnerObject,
+ HTMLMediaElement* aMediaElement);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MediaTrackList, DOMEventTargetHelper)
+
+ using DOMEventTargetHelper::DispatchTrustedEvent;
+
+ // The return value is non-null, assert an error if aIndex is out of bound
+ // for array mTracks.
+ MediaTrack* operator[](uint32_t aIndex);
+
+ // The track must be from the same Window as this MediaTrackList.
+ void AddTrack(MediaTrack* aTrack);
+
+ // In remove track case, the VideoTrackList::mSelectedIndex should be updated
+ // due to mTracks changed. No need to take care this in add track case.
+ virtual void RemoveTrack(const RefPtr<MediaTrack>& aTrack);
+
+ void RemoveTracks();
+
+ // For the case of src of HTMLMediaElement is non-MediaStream, leave the
+ // aAudioTrack as default(nullptr).
+ static already_AddRefed<AudioTrack> CreateAudioTrack(
+ nsIGlobalObject* aOwnerGlobal, const nsAString& aId,
+ const nsAString& aKind, const nsAString& aLabel,
+ const nsAString& aLanguage, bool aEnabled,
+ AudioStreamTrack* aAudioTrack = nullptr);
+
+ // For the case of src of HTMLMediaElement is non-MediaStream, leave the
+ // aVideoTrack as default(nullptr).
+ static already_AddRefed<VideoTrack> CreateVideoTrack(
+ nsIGlobalObject* aOwnerGlobal, const nsAString& aId,
+ const nsAString& aKind, const nsAString& aLabel,
+ const nsAString& aLanguage, VideoStreamTrack* aVideoTrack = nullptr);
+
+ virtual void EmptyTracks();
+
+ void CreateAndDispatchChangeEvent();
+
+ // WebIDL
+ MediaTrack* IndexedGetter(uint32_t aIndex, bool& aFound);
+
+ MediaTrack* GetTrackById(const nsAString& aId);
+
+ bool IsEmpty() const { return mTracks.IsEmpty(); }
+
+ uint32_t Length() const { return mTracks.Length(); }
+
+ IMPL_EVENT_HANDLER(change)
+ IMPL_EVENT_HANDLER(addtrack)
+ IMPL_EVENT_HANDLER(removetrack)
+
+ friend class AudioTrack;
+ friend class VideoTrack;
+
+ protected:
+ virtual ~MediaTrackList();
+
+ void CreateAndDispatchTrackEventRunner(MediaTrack* aTrack,
+ const nsAString& aEventName);
+
+ virtual AudioTrackList* AsAudioTrackList() { return nullptr; }
+
+ virtual VideoTrackList* AsVideoTrackList() { return nullptr; }
+
+ HTMLMediaElement* GetMediaElement() { return mMediaElement; }
+
+ nsTArray<RefPtr<MediaTrack>> mTracks;
+ RefPtr<HTMLMediaElement> mMediaElement;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_MediaTrackList_h
diff --git a/dom/media/MediaTrackListener.cpp b/dom/media/MediaTrackListener.cpp
new file mode 100644
index 0000000000..d34ab17267
--- /dev/null
+++ b/dom/media/MediaTrackListener.cpp
@@ -0,0 +1,95 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaTrackListener.h"
+
+#include "AudioSegment.h"
+#include "VideoSegment.h"
+
+namespace mozilla {
+
+#ifdef LOG
+# undef LOG
+#endif
+
+#define LOG(type, msg) MOZ_LOG(gMediaTrackGraphLog, type, msg)
+
+void DirectMediaTrackListener::MirrorAndDisableSegment(AudioSegment& aFrom,
+ AudioSegment& aTo) {
+ aTo.AppendNullData(aFrom.GetDuration());
+}
+
+void DirectMediaTrackListener::MirrorAndDisableSegment(
+ VideoSegment& aFrom, VideoSegment& aTo, DisabledTrackMode aMode) {
+ if (aMode == DisabledTrackMode::SILENCE_BLACK) {
+ for (VideoSegment::ChunkIterator it(aFrom); !it.IsEnded(); it.Next()) {
+ aTo.AppendFrame(do_AddRef(it->mFrame.GetImage()),
+ it->mFrame.GetIntrinsicSize(), it->GetPrincipalHandle(),
+ true);
+ aTo.ExtendLastFrameBy(it->GetDuration());
+ }
+ } else if (aMode == DisabledTrackMode::SILENCE_FREEZE) {
+ aTo.AppendNullData(aFrom.GetDuration());
+ }
+}
+
+void DirectMediaTrackListener::NotifyRealtimeTrackDataAndApplyTrackDisabling(
+ MediaTrackGraph* aGraph, TrackTime aTrackOffset, MediaSegment& aMedia) {
+ if (mDisabledFreezeCount == 0 && mDisabledBlackCount == 0) {
+ NotifyRealtimeTrackData(aGraph, aTrackOffset, aMedia);
+ return;
+ }
+
+ DisabledTrackMode mode = mDisabledBlackCount > 0
+ ? DisabledTrackMode::SILENCE_BLACK
+ : DisabledTrackMode::SILENCE_FREEZE;
+ UniquePtr<MediaSegment> media(aMedia.CreateEmptyClone());
+ if (aMedia.GetType() == MediaSegment::AUDIO) {
+ MirrorAndDisableSegment(static_cast<AudioSegment&>(aMedia),
+ static_cast<AudioSegment&>(*media));
+ } else if (aMedia.GetType() == MediaSegment::VIDEO) {
+ MirrorAndDisableSegment(static_cast<VideoSegment&>(aMedia),
+ static_cast<VideoSegment&>(*media), mode);
+ } else {
+ MOZ_CRASH("Unsupported media type");
+ }
+ NotifyRealtimeTrackData(aGraph, aTrackOffset, *media);
+}
+
+void DirectMediaTrackListener::IncreaseDisabled(DisabledTrackMode aMode) {
+ if (aMode == DisabledTrackMode::SILENCE_FREEZE) {
+ ++mDisabledFreezeCount;
+ } else if (aMode == DisabledTrackMode::SILENCE_BLACK) {
+ ++mDisabledBlackCount;
+ } else {
+ MOZ_ASSERT(false, "Unknown disabled mode");
+ }
+
+ LOG(LogLevel::Debug,
+ ("DirectMediaTrackListener %p increased disabled "
+ "mode %s. Current counts are: freeze=%d, black=%d",
+ this, aMode == DisabledTrackMode::SILENCE_FREEZE ? "freeze" : "black",
+ int32_t(mDisabledFreezeCount), int32_t(mDisabledBlackCount)));
+}
+
+void DirectMediaTrackListener::DecreaseDisabled(DisabledTrackMode aMode) {
+ if (aMode == DisabledTrackMode::SILENCE_FREEZE) {
+ --mDisabledFreezeCount;
+ MOZ_ASSERT(mDisabledFreezeCount >= 0, "Double decrease");
+ } else if (aMode == DisabledTrackMode::SILENCE_BLACK) {
+ --mDisabledBlackCount;
+ MOZ_ASSERT(mDisabledBlackCount >= 0, "Double decrease");
+ } else {
+ MOZ_ASSERT(false, "Unknown disabled mode");
+ }
+
+ LOG(LogLevel::Debug,
+ ("DirectMediaTrackListener %p decreased disabled "
+ "mode %s. Current counts are: freeze=%d, black=%d",
+ this, aMode == DisabledTrackMode::SILENCE_FREEZE ? "freeze" : "black",
+ int32_t(mDisabledFreezeCount), int32_t(mDisabledBlackCount)));
+}
+
+} // namespace mozilla
diff --git a/dom/media/MediaTrackListener.h b/dom/media/MediaTrackListener.h
new file mode 100644
index 0000000000..162482f7ef
--- /dev/null
+++ b/dom/media/MediaTrackListener.h
@@ -0,0 +1,184 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_MEDIATRACKLISTENER_h_
+#define MOZILLA_MEDIATRACKLISTENER_h_
+
+#include "MediaTrackGraph.h"
+#include "PrincipalHandle.h"
+
+namespace mozilla {
+
+class AudioSegment;
+class MediaTrackGraph;
+class MediaStreamVideoSink;
+class VideoSegment;
+
+/**
+ * This is a base class for media graph thread listener callbacks locked to
+ * specific tracks. Override methods to be notified of audio or video data or
+ * changes in track state.
+ *
+ * All notification methods are called from the media graph thread. Overriders
+ * of these methods are responsible for all synchronization. Beware!
+ * These methods are called without the media graph monitor held, so
+ * reentry into media graph methods is possible, although very much discouraged!
+ * You should do something non-blocking and non-reentrant (e.g. dispatch an
+ * event to some thread) and return.
+ * The listener is not allowed to add/remove any listeners from the parent
+ * track.
+ *
+ * If a listener is attached to a track that has already ended, we guarantee
+ * to call NotifyEnded.
+ */
+class MediaTrackListener {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaTrackListener)
+
+ public:
+ /**
+ * When a SourceMediaTrack has pulling enabled, and the MediaTrackGraph
+ * control loop is ready to pull, this gets called for each track in the
+ * SourceMediaTrack that is lacking data for the current iteration.
+ * A NotifyPull implementation is allowed to call the SourceMediaTrack
+ * methods that alter track data.
+ *
+ * It is not allowed to make other MediaTrack API calls, including
+ * calls to add or remove MediaTrackListeners. It is not allowed to
+ * block for any length of time.
+ *
+ * aEndOfAppendedData is the duration of the data that has already been
+ * appended to this track, in track time.
+ *
+ * aDesiredTime is the track time we should append data up to. Data
+ * beyond this point will not be played until NotifyPull runs again, so
+ * there's not much point in providing it. Note that if the track is blocked
+ * for some reason, then data before aDesiredTime may not be played
+ * immediately.
+ */
+ virtual void NotifyPull(MediaTrackGraph* aGraph, TrackTime aEndOfAppendedData,
+ TrackTime aDesiredTime) {}
+
+ virtual void NotifyQueuedChanges(MediaTrackGraph* aGraph,
+ TrackTime aTrackOffset,
+ const MediaSegment& aQueuedMedia) {}
+
+ virtual void NotifyPrincipalHandleChanged(
+ MediaTrackGraph* aGraph, const PrincipalHandle& aNewPrincipalHandle) {}
+
+ /**
+ * Notify that the enabled state for the track this listener is attached to
+ * has changed.
+ *
+ * The enabled state here is referring to whether audio should be audible
+ * (enabled) or silent (not enabled); or whether video should be displayed as
+ * is (enabled), or black (not enabled).
+ */
+ virtual void NotifyEnabledStateChanged(MediaTrackGraph* aGraph,
+ bool aEnabled) {}
+
+ /**
+ * Notify that the track output is advancing. aCurrentTrackTime is the number
+ * of samples that has been played out for this track in track time.
+ */
+ virtual void NotifyOutput(MediaTrackGraph* aGraph,
+ TrackTime aCurrentTrackTime) {}
+
+ /**
+ * Notify that this track has been ended and all data has been played out.
+ */
+ virtual void NotifyEnded(MediaTrackGraph* aGraph) {}
+
+ /**
+ * Notify that this track listener has been removed from the graph, either
+ * after shutdown or through MediaTrack::RemoveListener().
+ */
+ virtual void NotifyRemoved(MediaTrackGraph* aGraph) {}
+
+ protected:
+ virtual ~MediaTrackListener() = default;
+};
+
+/**
+ * This is a base class for media graph thread listener direct callbacks from
+ * within AppendToTrack(). It is bound to a certain track and can only be
+ * installed on audio tracks. Once added to a track on any track in the graph,
+ * the graph will try to install it at that track's source of media data.
+ *
+ * This works for ForwardedInputTracks, which will forward the listener to the
+ * track's input track if it exists, or wait for it to be created before
+ * forwarding if it doesn't.
+ * Once it reaches a SourceMediaTrack, it can be successfully installed.
+ * Other types of tracks will fail installation since they are not supported.
+ *
+ * Note that this listener and others for the same track will still get
+ * NotifyQueuedChanges() callbacks from the MTG tread, so you must be careful
+ * to ignore them if this listener was successfully installed.
+ */
+class DirectMediaTrackListener : public MediaTrackListener {
+ friend class SourceMediaTrack;
+ friend class ForwardedInputTrack;
+
+ public:
+ /*
+ * This will be called on any DirectMediaTrackListener added to a
+ * SourceMediaTrack when AppendToTrack() is called for the listener's bound
+ * track, using the thread of the AppendToTrack() caller. The MediaSegment
+ * will be the RawSegment (unresampled) if available in AppendToTrack().
+ * If the track is enabled at the source but has been disabled in one of the
+ * tracks in between the source and where it was originally added, aMedia
+ * will be a disabled version of the one passed to AppendToTrack() as well.
+ * Note that NotifyQueuedTrackChanges() calls will also still occur.
+ */
+ virtual void NotifyRealtimeTrackData(MediaTrackGraph* aGraph,
+ TrackTime aTrackOffset,
+ const MediaSegment& aMedia) {}
+
+ /**
+ * When a direct listener is processed for installation by the
+ * MediaTrackGraph it will be notified with whether the installation was
+ * successful or not. The results of this installation are the following:
+ * TRACK_NOT_SUPPORTED
+ * While looking for the data source of this track, we found a MediaTrack
+ * that is not a SourceMediaTrack or a ForwardedInputTrack.
+ * ALREADY_EXISTS
+ * This DirectMediaTrackListener already exists in the
+ * SourceMediaTrack.
+ * SUCCESS
+ * Installation was successful and this listener will start receiving
+ * NotifyRealtimeData on the next AppendData().
+ */
+ enum class InstallationResult {
+ TRACK_NOT_SUPPORTED,
+ ALREADY_EXISTS,
+ SUCCESS
+ };
+ virtual void NotifyDirectListenerInstalled(InstallationResult aResult) {}
+ virtual void NotifyDirectListenerUninstalled() {}
+
+ protected:
+ virtual ~DirectMediaTrackListener() = default;
+
+ void MirrorAndDisableSegment(AudioSegment& aFrom, AudioSegment& aTo);
+ void MirrorAndDisableSegment(VideoSegment& aFrom, VideoSegment& aTo,
+ DisabledTrackMode aMode);
+ void NotifyRealtimeTrackDataAndApplyTrackDisabling(MediaTrackGraph* aGraph,
+ TrackTime aTrackOffset,
+ MediaSegment& aMedia);
+
+ void IncreaseDisabled(DisabledTrackMode aMode);
+ void DecreaseDisabled(DisabledTrackMode aMode);
+
+ // Matches the number of disabled tracks to which this listener is attached.
+ // The number of tracks are those between the track where the listener was
+ // added and the SourceMediaTrack that is the source of the data reaching
+ // this listener.
+ Atomic<int32_t> mDisabledFreezeCount;
+ Atomic<int32_t> mDisabledBlackCount;
+};
+
+} // namespace mozilla
+
+#endif // MOZILLA_MEDIATRACKLISTENER_h_
diff --git a/dom/media/MemoryBlockCache.cpp b/dom/media/MemoryBlockCache.cpp
new file mode 100644
index 0000000000..2848a3f381
--- /dev/null
+++ b/dom/media/MemoryBlockCache.cpp
@@ -0,0 +1,213 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MemoryBlockCache.h"
+
+#include "mozilla/Atomics.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "nsWeakReference.h"
+#include "prsystem.h"
+
+namespace mozilla {
+
+#undef LOG
+LazyLogModule gMemoryBlockCacheLog("MemoryBlockCache");
+#define LOG(x, ...) \
+ MOZ_LOG(gMemoryBlockCacheLog, LogLevel::Debug, ("%p " x, this, ##__VA_ARGS__))
+
+// Combined sizes of all MemoryBlockCache buffers.
+// Initialized to 0 by non-local static initialization.
+// Increases when a buffer grows (during initialization or unexpected OOB
+// writes), decreases when a MemoryBlockCache (with its buffer) is destroyed.
+static Atomic<size_t> gCombinedSizes;
+
+static int32_t CalculateMaxBlocks(int64_t aContentLength) {
+ int64_t maxSize = int64_t(StaticPrefs::media_memory_cache_max_size()) * 1024;
+ MOZ_ASSERT(aContentLength <= maxSize);
+ MOZ_ASSERT(maxSize % MediaBlockCacheBase::BLOCK_SIZE == 0);
+ // Note: It doesn't matter if calculations overflow, Init() would later fail.
+ // We want at least enough blocks to contain the original content length.
+ const int32_t requiredBlocks = maxSize / MediaBlockCacheBase::BLOCK_SIZE;
+ // Allow at least 1s of ultra HD (25Mbps).
+ const int32_t workableBlocks =
+ 25 * 1024 * 1024 / 8 / MediaBlockCacheBase::BLOCK_SIZE;
+ return std::max(requiredBlocks, workableBlocks);
+}
+
+MemoryBlockCache::MemoryBlockCache(int64_t aContentLength)
+ // Buffer whole blocks.
+ : mInitialContentLength((aContentLength >= 0) ? size_t(aContentLength) : 0),
+ mMaxBlocks(CalculateMaxBlocks(aContentLength)),
+ mMutex("MemoryBlockCache"),
+ mHasGrown(false) {
+ if (aContentLength <= 0) {
+ LOG("MemoryBlockCache() MEMORYBLOCKCACHE_ERRORS='InitUnderuse'");
+ }
+}
+
+MemoryBlockCache::~MemoryBlockCache() {
+ MOZ_ASSERT(gCombinedSizes >= mBuffer.Length());
+ size_t sizes = static_cast<size_t>(gCombinedSizes -= mBuffer.Length());
+ LOG("~MemoryBlockCache() - destroying buffer of size %zu; combined sizes now "
+ "%zu",
+ mBuffer.Length(), sizes);
+}
+
+bool MemoryBlockCache::EnsureBufferCanContain(size_t aContentLength) {
+ mMutex.AssertCurrentThreadOwns();
+ if (aContentLength == 0) {
+ return true;
+ }
+ const size_t initialLength = mBuffer.Length();
+ const size_t desiredLength =
+ ((aContentLength - 1) / BLOCK_SIZE + 1) * BLOCK_SIZE;
+ if (initialLength >= desiredLength) {
+ // Already large enough.
+ return true;
+ }
+ // Need larger buffer. If we are allowed more memory, attempt to re-allocate.
+ const size_t extra = desiredLength - initialLength;
+ // Only check the very first allocation against the combined MemoryBlockCache
+ // limit. Further growths will always be allowed, assuming MediaCache won't
+ // go over GetMaxBlocks() by too much.
+ if (initialLength == 0) {
+ // Note: There is a small race between testing `atomic + extra > limit` and
+ // committing to it with `atomic += extra` below; but this is acceptable, as
+ // in the worst case it may allow a small number of buffers to go past the
+ // limit.
+ // The alternative would have been to reserve the space first with
+ // `atomic += extra` and then undo it with `atomic -= extra` in case of
+ // failure; but this would have meant potentially preventing other (small
+ // but successful) allocations.
+ static const size_t sysmem =
+ std::max<size_t>(PR_GetPhysicalMemorySize(), 32 * 1024 * 1024);
+ const size_t limit = std::min(
+ size_t(StaticPrefs::media_memory_caches_combined_limit_kb()) * 1024,
+ sysmem * StaticPrefs::media_memory_caches_combined_limit_pc_sysmem() /
+ 100);
+ const size_t currentSizes = static_cast<size_t>(gCombinedSizes);
+ if (currentSizes + extra > limit) {
+ LOG("EnsureBufferCanContain(%zu) - buffer size %zu, wanted + %zu = %zu;"
+ " combined sizes %zu + %zu > limit %zu",
+ aContentLength, initialLength, extra, desiredLength, currentSizes,
+ extra, limit);
+ return false;
+ }
+ }
+ if (!mBuffer.SetLength(desiredLength, mozilla::fallible)) {
+ LOG("EnsureBufferCanContain(%zu) - buffer size %zu, wanted + %zu = %zu, "
+ "allocation failed",
+ aContentLength, initialLength, extra, desiredLength);
+ return false;
+ }
+ MOZ_ASSERT(mBuffer.Length() == desiredLength);
+ const size_t capacity = mBuffer.Capacity();
+ const size_t extraCapacity = capacity - desiredLength;
+ if (extraCapacity != 0) {
+ // Our buffer was given a larger capacity than the requested length, we may
+ // as well claim that extra capacity, both for our accounting, and to
+ // possibly bypass some future growths that would fit in this new capacity.
+ mBuffer.SetLength(capacity);
+ }
+ const size_t newSizes = gCombinedSizes += (extra + extraCapacity);
+ LOG("EnsureBufferCanContain(%zu) - buffer size %zu + requested %zu + bonus "
+ "%zu = %zu; combined sizes %zu",
+ aContentLength, initialLength, extra, extraCapacity, capacity, newSizes);
+ mHasGrown = true;
+ return true;
+}
+
+nsresult MemoryBlockCache::Init() {
+ LOG("Init()");
+ MutexAutoLock lock(mMutex);
+ MOZ_ASSERT(mBuffer.IsEmpty());
+ // Attempt to pre-allocate buffer for expected content length.
+ if (!EnsureBufferCanContain(mInitialContentLength)) {
+ LOG("Init() MEMORYBLOCKCACHE_ERRORS='InitAllocation'");
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+void MemoryBlockCache::Flush() {
+ LOG("Flush()");
+ MutexAutoLock lock(mMutex);
+ MOZ_ASSERT(mBuffer.Length() >= mInitialContentLength);
+ memset(mBuffer.Elements(), 0, mBuffer.Length());
+ mHasGrown = false;
+}
+
+nsresult MemoryBlockCache::WriteBlock(uint32_t aBlockIndex,
+ Span<const uint8_t> aData1,
+ Span<const uint8_t> aData2) {
+ MutexAutoLock lock(mMutex);
+
+ size_t offset = BlockIndexToOffset(aBlockIndex);
+ if (offset + aData1.Length() + aData2.Length() > mBuffer.Length() &&
+ !mHasGrown) {
+ LOG("WriteBlock() MEMORYBLOCKCACHE_ERRORS='WriteBlockOverflow'");
+ }
+ if (!EnsureBufferCanContain(offset + aData1.Length() + aData2.Length())) {
+ LOG("WriteBlock() MEMORYBLOCKCACHE_ERRORS='WriteBlockCannotGrow'");
+ return NS_ERROR_FAILURE;
+ }
+
+ memcpy(mBuffer.Elements() + offset, aData1.Elements(), aData1.Length());
+ if (aData2.Length() > 0) {
+ memcpy(mBuffer.Elements() + offset + aData1.Length(), aData2.Elements(),
+ aData2.Length());
+ }
+
+ return NS_OK;
+}
+
+nsresult MemoryBlockCache::Read(int64_t aOffset, uint8_t* aData,
+ int32_t aLength, int32_t* aBytes) {
+ MutexAutoLock lock(mMutex);
+
+ MOZ_ASSERT(aOffset >= 0);
+ if (aOffset + aLength > int64_t(mBuffer.Length())) {
+ LOG("Read() MEMORYBLOCKCACHE_ERRORS='ReadOverrun'");
+ return NS_ERROR_FAILURE;
+ }
+
+ memcpy(aData, mBuffer.Elements() + aOffset, aLength);
+ *aBytes = aLength;
+
+ return NS_OK;
+}
+
+nsresult MemoryBlockCache::MoveBlock(int32_t aSourceBlockIndex,
+ int32_t aDestBlockIndex) {
+ MutexAutoLock lock(mMutex);
+
+ size_t sourceOffset = BlockIndexToOffset(aSourceBlockIndex);
+ size_t destOffset = BlockIndexToOffset(aDestBlockIndex);
+ if (sourceOffset + BLOCK_SIZE > mBuffer.Length()) {
+ LOG("MoveBlock() MEMORYBLOCKCACHE_ERRORS='MoveBlockSourceOverrun'");
+ return NS_ERROR_FAILURE;
+ }
+ if (destOffset + BLOCK_SIZE > mBuffer.Length() && !mHasGrown) {
+ LOG("MoveBlock() MEMORYBLOCKCACHE_ERRORS='MoveBlockDestOverflow'");
+ }
+ if (!EnsureBufferCanContain(destOffset + BLOCK_SIZE)) {
+ LOG("MoveBlock() MEMORYBLOCKCACHE_ERRORS='MoveBlockCannotGrow'");
+ return NS_ERROR_FAILURE;
+ }
+
+ memcpy(mBuffer.Elements() + destOffset, mBuffer.Elements() + sourceOffset,
+ BLOCK_SIZE);
+
+ return NS_OK;
+}
+
+} // End namespace mozilla.
+
+// avoid redefined macro in unified build
+#undef LOG
diff --git a/dom/media/MemoryBlockCache.h b/dom/media/MemoryBlockCache.h
new file mode 100644
index 0000000000..32bf9615c6
--- /dev/null
+++ b/dom/media/MemoryBlockCache.h
@@ -0,0 +1,87 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MEMORY_BLOCK_CACHE_H_
+#define MEMORY_BLOCK_CACHE_H_
+
+#include "MediaBlockCacheBase.h"
+#include "mozilla/Mutex.h"
+
+namespace mozilla {
+
+// Manages block management for the media cache. Data comes in over the network
+// via callbacks on the main thread, however we don't want to write the
+// incoming data to the media cache on the main thread, as this could block
+// causing UI jank.
+//
+// So MediaBlockCacheBase provides an abstraction for a temporary memory buffer
+// as an array of blocks, which supports a block move operation, and
+// allows synchronous reading and writing from any thread.
+//
+// Writes and cache block moves (which require reading) may be deferred to
+// their own non-main thread. This object also ensures that data which has
+// been scheduled to be written, but hasn't actually *been* written, is read
+// as if it had, i.e. pending writes are cached in readable memory until
+// they're flushed to file.
+//
+// To improve efficiency, writes can only be done at block granularity,
+// whereas reads can be done with byte granularity.
+class MemoryBlockCache : public MediaBlockCacheBase {
+ public:
+ explicit MemoryBlockCache(int64_t aContentLength);
+
+ protected:
+ virtual ~MemoryBlockCache();
+
+ public:
+ // Allocate initial buffer.
+ // If re-initializing, clear buffer.
+ virtual nsresult Init() override;
+
+ void Flush() override;
+
+ // Maximum number of blocks allowed in this block cache.
+ // Based on initial content length, and minimum usable block cache.
+ size_t GetMaxBlocks(size_t) const override { return mMaxBlocks; }
+
+ // Can be called on any thread.
+ virtual nsresult WriteBlock(uint32_t aBlockIndex, Span<const uint8_t> aData1,
+ Span<const uint8_t> aData2) override;
+
+ // Synchronously reads data from buffer.
+ virtual nsresult Read(int64_t aOffset, uint8_t* aData, int32_t aLength,
+ int32_t* aBytes) override;
+
+ // Moves a block. Can be called on any thread.
+ virtual nsresult MoveBlock(int32_t aSourceBlockIndex,
+ int32_t aDestBlockIndex) override;
+
+ private:
+ static size_t BlockIndexToOffset(uint32_t aBlockIndex) {
+ return static_cast<size_t>(aBlockIndex) * BLOCK_SIZE;
+ }
+
+ // Ensure the buffer has at least a multiple of BLOCK_SIZE that can contain
+ // aContentLength bytes. Buffer size can only grow.
+ // Returns false if allocation failed.
+ bool EnsureBufferCanContain(size_t aContentLength);
+
+ // Initial content length.
+ const size_t mInitialContentLength;
+
+ // Maximum number of blocks that this MemoryBlockCache expects.
+ const size_t mMaxBlocks;
+
+ // Mutex which controls access to all members below.
+ Mutex mMutex MOZ_UNANNOTATED;
+
+ nsTArray<uint8_t> mBuffer;
+ bool mHasGrown = false;
+};
+
+} // End namespace mozilla.
+
+#endif /* MEMORY_BLOCK_CACHE_H_ */
diff --git a/dom/media/Pacer.h b/dom/media/Pacer.h
new file mode 100644
index 0000000000..cb95ac01dd
--- /dev/null
+++ b/dom/media/Pacer.h
@@ -0,0 +1,164 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaEventSource.h"
+#include "MediaTimer.h"
+#include "mozilla/TaskQueue.h"
+#include "nsDeque.h"
+
+#ifndef DOM_MEDIA_PACER_H_
+# define DOM_MEDIA_PACER_H_
+
+namespace mozilla {
+
+/**
+ * Pacer<T> takes a queue of Ts tied to timestamps, and emits PacedItemEvents
+ * for every T at its corresponding timestamp.
+ *
+ * The queue is ordered. Enqueing an item at time t will drop all items at times
+ * later than T. This is because of how video sources work (some send out frames
+ * in the future, some don't), and to allow swapping one source for another.
+ *
+ * It supports a duplication interval. If there is no new item enqueued within
+ * the duplication interval since the last enqueued item, the last enqueud item
+ * is emitted again.
+ */
+template <typename T>
+class Pacer {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(Pacer)
+
+ Pacer(RefPtr<TaskQueue> aTaskQueue, TimeDuration aDuplicationInterval)
+ : mTaskQueue(std::move(aTaskQueue)),
+ mDuplicationInterval(aDuplicationInterval),
+ mTimer(MakeAndAddRef<MediaTimer>()) {}
+
+ /**
+ * Enqueues an item and schedules a timer to pass it on to PacedItemEvent() at
+ * t=aTime. Already queued items with t>=aTime will be dropped.
+ */
+ void Enqueue(T aItem, TimeStamp aTime) {
+ MOZ_ALWAYS_SUCCEEDS(mTaskQueue->Dispatch(NS_NewRunnableFunction(
+ __func__,
+ [this, self = RefPtr<Pacer>(this), aItem = std::move(aItem), aTime] {
+ MOZ_DIAGNOSTIC_ASSERT(!mIsShutdown);
+ while (const auto* item = mQueue.Peek()) {
+ if (item->mTime < aTime) {
+ break;
+ }
+ RefPtr<QueueItem> dropping = mQueue.Pop();
+ }
+ mQueue.Push(MakeAndAddRef<QueueItem>(std::move(aItem), aTime));
+ EnsureTimerScheduled(aTime);
+ })));
+ }
+
+ RefPtr<GenericPromise> Shutdown() {
+ return InvokeAsync(
+ mTaskQueue, __func__, [this, self = RefPtr<Pacer>(this)] {
+ mIsShutdown = true;
+ mTimer->Cancel();
+ mQueue.Erase();
+ mCurrentTimerTarget = Nothing();
+ return GenericPromise::CreateAndResolve(true, "Pacer::Shutdown");
+ });
+ }
+
+ MediaEventSourceExc<T, TimeStamp>& PacedItemEvent() {
+ return mPacedItemEvent;
+ }
+
+ protected:
+ ~Pacer() = default;
+
+ void EnsureTimerScheduled(TimeStamp aTime) {
+ if (mCurrentTimerTarget && *mCurrentTimerTarget <= aTime) {
+ return;
+ }
+
+ if (mCurrentTimerTarget) {
+ mTimer->Cancel();
+ mCurrentTimerTarget = Nothing();
+ }
+
+ mTimer->WaitUntil(aTime, __func__)
+ ->Then(
+ mTaskQueue, __func__,
+ [this, self = RefPtr<Pacer>(this)] { OnTimerTick(); },
+ [] {
+ // Timer was rejected. This is fine.
+ });
+ mCurrentTimerTarget = Some(aTime);
+ }
+
+ void OnTimerTick() {
+ MOZ_ASSERT(mTaskQueue->IsOnCurrentThread());
+
+ mCurrentTimerTarget = Nothing();
+
+ while (RefPtr<QueueItem> item = mQueue.PopFront()) {
+ auto now = TimeStamp::Now();
+
+ if (item->mTime <= now) {
+ // It's time to process this item.
+ if (const auto& next = mQueue.PeekFront();
+ !next || next->mTime > (item->mTime + mDuplicationInterval)) {
+ // No future frame within the duplication interval exists. Schedule
+ // a copy.
+ mQueue.PushFront(MakeAndAddRef<QueueItem>(
+ item->mItem, item->mTime + mDuplicationInterval));
+ }
+ mPacedItemEvent.Notify(std::move(item->mItem), item->mTime);
+ continue;
+ }
+
+ // This item is in the future. Put it back.
+ mQueue.PushFront(item.forget());
+ break;
+ }
+
+ if (const auto& next = mQueue.PeekFront(); next) {
+ // The queue is not empty. Schedule the timer.
+ EnsureTimerScheduled(next->mTime);
+ }
+ }
+
+ public:
+ const RefPtr<TaskQueue> mTaskQueue;
+ const TimeDuration mDuplicationInterval;
+
+ protected:
+ struct QueueItem {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(QueueItem)
+
+ QueueItem(T aItem, TimeStamp aTime)
+ : mItem(std::forward<T>(aItem)), mTime(aTime) {}
+
+ T mItem;
+ TimeStamp mTime;
+
+ private:
+ ~QueueItem() = default;
+ };
+
+ // Accessed on mTaskQueue.
+ nsRefPtrDeque<QueueItem> mQueue;
+
+ // Accessed on mTaskQueue.
+ RefPtr<MediaTimer> mTimer;
+
+ // Accessed on mTaskQueue.
+ Maybe<TimeStamp> mCurrentTimerTarget;
+
+ // Accessed on mTaskQueue.
+ bool mIsShutdown = false;
+
+ MediaEventProducerExc<T, TimeStamp> mPacedItemEvent;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/PeerConnection.sys.mjs b/dom/media/PeerConnection.sys.mjs
new file mode 100644
index 0000000000..adb9cc3ba3
--- /dev/null
+++ b/dom/media/PeerConnection.sys.mjs
@@ -0,0 +1,2022 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ PeerConnectionIdp: "resource://gre/modules/media/PeerConnectionIdp.sys.mjs",
+});
+
+const PC_CONTRACT = "@mozilla.org/dom/peerconnection;1";
+const PC_OBS_CONTRACT = "@mozilla.org/dom/peerconnectionobserver;1";
+const PC_ICE_CONTRACT = "@mozilla.org/dom/rtcicecandidate;1";
+const PC_SESSION_CONTRACT = "@mozilla.org/dom/rtcsessiondescription;1";
+const PC_STATIC_CONTRACT = "@mozilla.org/dom/peerconnectionstatic;1";
+const PC_COREQUEST_CONTRACT = "@mozilla.org/dom/createofferrequest;1";
+
+const PC_CID = Components.ID("{bdc2e533-b308-4708-ac8e-a8bfade6d851}");
+const PC_OBS_CID = Components.ID("{d1748d4c-7f6a-4dc5-add6-d55b7678537e}");
+const PC_ICE_CID = Components.ID("{02b9970c-433d-4cc2-923d-f7028ac66073}");
+const PC_SESSION_CID = Components.ID("{1775081b-b62d-4954-8ffe-a067bbf508a7}");
+const PC_MANAGER_CID = Components.ID("{7293e901-2be3-4c02-b4bd-cbef6fc24f78}");
+const PC_STATIC_CID = Components.ID("{0fb47c47-a205-4583-a9fc-cbadf8c95880}");
+const PC_COREQUEST_CID = Components.ID(
+ "{74b2122d-65a8-4824-aa9e-3d664cb75dc2}"
+);
+
+function logMsg(msg, file, line, flag, winID) {
+ let scriptErrorClass = Cc["@mozilla.org/scripterror;1"];
+ let scriptError = scriptErrorClass.createInstance(Ci.nsIScriptError);
+ scriptError.initWithWindowID(
+ `WebRTC: ${msg}`,
+ file,
+ null,
+ line,
+ 0,
+ flag,
+ "content javascript",
+ winID
+ );
+ Services.console.logMessage(scriptError);
+}
+
+let setupPrototype = (_class, dict) => {
+ _class.prototype.classDescription = _class.name;
+ Object.assign(_class.prototype, dict);
+};
+
+// Global list of PeerConnection objects, so they can be cleaned up when
+// a page is torn down. (Maps inner window ID to an array of PC objects).
+export class GlobalPCList {
+ constructor() {
+ this._list = {};
+ this._networkdown = false; // XXX Need to query current state somehow
+ this._lifecycleobservers = {};
+ this._nextId = 1;
+ Services.obs.addObserver(this, "inner-window-destroyed", true);
+ Services.obs.addObserver(this, "profile-change-net-teardown", true);
+ Services.obs.addObserver(this, "network:offline-about-to-go-offline", true);
+ Services.obs.addObserver(this, "network:offline-status-changed", true);
+ Services.obs.addObserver(this, "gmp-plugin-crash", true);
+ Services.obs.addObserver(this, "PeerConnection:response:allow", true);
+ Services.obs.addObserver(this, "PeerConnection:response:deny", true);
+ if (Services.cpmm) {
+ Services.cpmm.addMessageListener("gmp-plugin-crash", this);
+ }
+ }
+
+ notifyLifecycleObservers(pc, type) {
+ for (var key of Object.keys(this._lifecycleobservers)) {
+ this._lifecycleobservers[key](pc, pc._winID, type);
+ }
+ }
+
+ addPC(pc) {
+ let winID = pc._winID;
+ if (this._list[winID]) {
+ this._list[winID].push(Cu.getWeakReference(pc));
+ } else {
+ this._list[winID] = [Cu.getWeakReference(pc)];
+ }
+ pc._globalPCListId = this._nextId++;
+ this.removeNullRefs(winID);
+ }
+
+ findPC(globalPCListId) {
+ for (let winId in this._list) {
+ if (this._list.hasOwnProperty(winId)) {
+ for (let pcref of this._list[winId]) {
+ let pc = pcref.get();
+ if (pc && pc._globalPCListId == globalPCListId) {
+ return pc;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ removeNullRefs(winID) {
+ if (this._list[winID] === undefined) {
+ return;
+ }
+ this._list[winID] = this._list[winID].filter(function (e, i, a) {
+ return e.get() !== null;
+ });
+
+ if (this._list[winID].length === 0) {
+ delete this._list[winID];
+ }
+ }
+
+ handleGMPCrash(data) {
+ let broadcastPluginCrash = function (list, winID, pluginID, pluginName) {
+ if (list.hasOwnProperty(winID)) {
+ list[winID].forEach(function (pcref) {
+ let pc = pcref.get();
+ if (pc) {
+ pc._pc.pluginCrash(pluginID, pluginName);
+ }
+ });
+ }
+ };
+
+ // a plugin crashed; if it's associated with any of our PCs, fire an
+ // event to the DOM window
+ for (let winId in this._list) {
+ broadcastPluginCrash(this._list, winId, data.pluginID, data.pluginName);
+ }
+ }
+
+ receiveMessage({ name, data }) {
+ if (name == "gmp-plugin-crash") {
+ this.handleGMPCrash(data);
+ }
+ }
+
+ observe(subject, topic, data) {
+ let cleanupPcRef = function (pcref) {
+ let pc = pcref.get();
+ if (pc) {
+ pc._suppressEvents = true;
+ pc.close();
+ }
+ };
+
+ let cleanupWinId = function (list, winID) {
+ if (list.hasOwnProperty(winID)) {
+ list[winID].forEach(cleanupPcRef);
+ delete list[winID];
+ }
+ };
+
+ if (topic == "inner-window-destroyed") {
+ let winID = subject.QueryInterface(Ci.nsISupportsPRUint64).data;
+ cleanupWinId(this._list, winID);
+
+ if (this._lifecycleobservers.hasOwnProperty(winID)) {
+ delete this._lifecycleobservers[winID];
+ }
+ } else if (
+ topic == "profile-change-net-teardown" ||
+ topic == "network:offline-about-to-go-offline"
+ ) {
+ // As Necko doesn't prevent us from accessing the network we still need to
+ // monitor the network offline/online state here. See bug 1326483
+ this._networkdown = true;
+ } else if (topic == "network:offline-status-changed") {
+ if (data == "offline") {
+ this._networkdown = true;
+ } else if (data == "online") {
+ this._networkdown = false;
+ }
+ } else if (topic == "gmp-plugin-crash") {
+ if (subject instanceof Ci.nsIWritablePropertyBag2) {
+ let pluginID = subject.getPropertyAsUint32("pluginID");
+ let pluginName = subject.getPropertyAsAString("pluginName");
+ let data = { pluginID, pluginName };
+ this.handleGMPCrash(data);
+ }
+ } else if (
+ topic == "PeerConnection:response:allow" ||
+ topic == "PeerConnection:response:deny"
+ ) {
+ var pc = this.findPC(data);
+ if (pc) {
+ if (topic == "PeerConnection:response:allow") {
+ pc._settlePermission.allow();
+ } else {
+ let err = new pc._win.DOMException(
+ "The request is not allowed by " +
+ "the user agent or the platform in the current context.",
+ "NotAllowedError"
+ );
+ pc._settlePermission.deny(err);
+ }
+ }
+ }
+ }
+
+ _registerPeerConnectionLifecycleCallback(winID, cb) {
+ this._lifecycleobservers[winID] = cb;
+ }
+}
+
+setupPrototype(GlobalPCList, {
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIObserver",
+ "nsISupportsWeakReference",
+ ]),
+ classID: PC_MANAGER_CID,
+});
+
+var _globalPCList = new GlobalPCList();
+
+export class RTCIceCandidate {
+ init(win) {
+ this._win = win;
+ }
+
+ __init(dict) {
+ if (dict.sdpMid == null && dict.sdpMLineIndex == null) {
+ throw new this._win.TypeError(
+ "Either sdpMid or sdpMLineIndex must be specified"
+ );
+ }
+ Object.assign(this, dict);
+ }
+}
+
+setupPrototype(RTCIceCandidate, {
+ classID: PC_ICE_CID,
+ contractID: PC_ICE_CONTRACT,
+ QueryInterface: ChromeUtils.generateQI(["nsIDOMGlobalPropertyInitializer"]),
+});
+
+export class RTCSessionDescription {
+ init(win) {
+ this._win = win;
+ this._winID = this._win.windowGlobalChild.innerWindowId;
+ }
+
+ __init({ type, sdp }) {
+ if (!type) {
+ throw new this._win.TypeError(
+ "Missing required 'type' member of RTCSessionDescriptionInit"
+ );
+ }
+ Object.assign(this, { _type: type, _sdp: sdp });
+ }
+
+ get type() {
+ return this._type;
+ }
+ set type(type) {
+ this.warn();
+ this._type = type;
+ }
+
+ get sdp() {
+ return this._sdp;
+ }
+ set sdp(sdp) {
+ this.warn();
+ this._sdp = sdp;
+ }
+
+ warn() {
+ if (!this._warned) {
+ // Warn once per RTCSessionDescription about deprecated writable usage.
+ this.logWarning(
+ "RTCSessionDescription's members are readonly! " +
+ "Writing to them is deprecated and will break soon!"
+ );
+ this._warned = true;
+ }
+ }
+
+ logWarning(msg) {
+ let err = this._win.Error();
+ logMsg(
+ msg,
+ err.fileName,
+ err.lineNumber,
+ Ci.nsIScriptError.warningFlag,
+ this._winID
+ );
+ }
+}
+
+setupPrototype(RTCSessionDescription, {
+ classID: PC_SESSION_CID,
+ contractID: PC_SESSION_CONTRACT,
+ QueryInterface: ChromeUtils.generateQI(["nsIDOMGlobalPropertyInitializer"]),
+});
+
+// Records PC related telemetry
+class PeerConnectionTelemetry {
+ // ICE connection state enters connected or completed.
+ recordConnected() {
+ Services.telemetry.scalarAdd("webrtc.peerconnection.connected", 1);
+ this.recordConnected = () => {};
+ }
+ // DataChannel is created
+ _recordDataChannelCreated() {
+ Services.telemetry.scalarAdd(
+ "webrtc.peerconnection.datachannel_created",
+ 1
+ );
+ this._recordDataChannelCreated = () => {};
+ }
+ // DataChannel initialized with maxRetransmitTime
+ _recordMaxRetransmitTime(maxRetransmitTime) {
+ if (maxRetransmitTime === undefined) {
+ return false;
+ }
+ Services.telemetry.scalarAdd(
+ "webrtc.peerconnection.datachannel_max_retx_used",
+ 1
+ );
+ this._recordMaxRetransmitTime = () => true;
+ return true;
+ }
+ // DataChannel initialized with maxPacketLifeTime
+ _recordMaxPacketLifeTime(maxPacketLifeTime) {
+ if (maxPacketLifeTime === undefined) {
+ return false;
+ }
+ Services.telemetry.scalarAdd(
+ "webrtc.peerconnection.datachannel_max_life_used",
+ 1
+ );
+ this._recordMaxPacketLifeTime = () => true;
+ return true;
+ }
+ // DataChannel initialized
+ recordDataChannelInit(maxRetransmitTime, maxPacketLifeTime) {
+ const retxUsed = this._recordMaxRetransmitTime(maxRetransmitTime);
+ if (this._recordMaxPacketLifeTime(maxPacketLifeTime) && retxUsed) {
+ Services.telemetry.scalarAdd(
+ "webrtc.peerconnection.datachannel_max_retx_and_life_used",
+ 1
+ );
+ this.recordDataChannelInit = () => {};
+ }
+ this._recordDataChannelCreated();
+ }
+}
+
+export class RTCPeerConnection {
+ constructor() {
+ this._pc = null;
+ this._closed = false;
+
+ // http://rtcweb-wg.github.io/jsep/#rfc.section.4.1.9
+ // canTrickle == null means unknown; when a remote description is received it
+ // is set to true or false based on the presence of the "trickle" ice-option
+ this._canTrickle = null;
+
+ // So we can record telemetry on state transitions
+ this._iceConnectionState = "new";
+
+ this._hasStunServer = this._hasTurnServer = false;
+ this._iceGatheredRelayCandidates = false;
+ // Records telemetry
+ this._pcTelemetry = new PeerConnectionTelemetry();
+ }
+
+ init(win) {
+ this._win = win;
+ }
+
+ // Pref-based overrides; will _not_ be reflected in getConfiguration
+ _applyPrefsToConfig(rtcConfig) {
+ if (
+ rtcConfig.iceTransportPolicy == "all" &&
+ Services.prefs.getBoolPref("media.peerconnection.ice.relay_only")
+ ) {
+ rtcConfig.iceTransportPolicy = "relay";
+ }
+
+ if (
+ !rtcConfig.iceServers ||
+ !Services.prefs.getBoolPref(
+ "media.peerconnection.use_document_iceservers"
+ )
+ ) {
+ try {
+ rtcConfig.iceServers = JSON.parse(
+ Services.prefs.getCharPref(
+ "media.peerconnection.default_iceservers"
+ ) || "[]"
+ );
+ } catch (e) {
+ this.logWarning(
+ "Ignoring invalid media.peerconnection.default_iceservers in about:config"
+ );
+ rtcConfig.iceServers = [];
+ }
+ try {
+ this._validateIceServers(
+ rtcConfig.iceServers,
+ "Ignoring invalid media.peerconnection.default_iceservers in about:config"
+ );
+ } catch (e) {
+ this.logWarning(e.message);
+ rtcConfig.iceServers = [];
+ }
+ }
+ }
+
+ _validateConfig(rtcConfig) {
+ if ("sdpSemantics" in rtcConfig) {
+ if (rtcConfig.sdpSemantics == "plan-b") {
+ this.logWarning(
+ `Outdated and non-standard {sdpSemantics: "plan-b"} is not ` +
+ `supported! WebRTC may be unreliable. Please update code to ` +
+ `follow standard "unified-plan".`
+ );
+ }
+ // Don't let it show up in getConfiguration.
+ delete rtcConfig.sdpSemantics;
+ }
+
+ if (this._config) {
+ // certificates must match
+ if (rtcConfig.certificates.length != this._config.certificates.length) {
+ throw new this._win.DOMException(
+ "Cannot change certificates with setConfiguration (length differs)",
+ "InvalidModificationError"
+ );
+ }
+ for (let i = 0; i < rtcConfig.certificates.length; i++) {
+ if (rtcConfig.certificates[i] != this._config.certificates[i]) {
+ throw new this._win.DOMException(
+ `Cannot change certificates with setConfiguration ` +
+ `(cert at index ${i} differs)`,
+ "InvalidModificationError"
+ );
+ }
+ }
+
+ // bundlePolicy must match
+ if (rtcConfig.bundlePolicy != this._config.bundlePolicy) {
+ throw new this._win.DOMException(
+ "Cannot change bundlePolicy with setConfiguration",
+ "InvalidModificationError"
+ );
+ }
+
+ // peerIdentity must match
+ if (
+ rtcConfig.peerIdentity &&
+ rtcConfig.peerIdentity != this._config.peerIdentity
+ ) {
+ throw new this._win.DOMException(
+ "Cannot change peerIdentity with setConfiguration",
+ "InvalidModificationError"
+ );
+ }
+
+ // TODO (bug 1339203): rtcpMuxPolicy must match
+ // TODO (bug 1529398): iceCandidatePoolSize must match if sLD has ever
+ // been called.
+ }
+
+ // This gets executed in the typical case when iceServers
+ // are passed in through the web page.
+ this._validateIceServers(
+ rtcConfig.iceServers,
+ "RTCPeerConnection constructor passed invalid RTCConfiguration"
+ );
+ }
+
+ _checkIfIceRestartRequired(rtcConfig) {
+ if (this._config) {
+ if (rtcConfig.iceTransportPolicy != this._config.iceTransportPolicy) {
+ this._pc.restartIceNoRenegotiationNeeded();
+ return;
+ }
+ if (
+ JSON.stringify(this._config.iceServers) !=
+ JSON.stringify(rtcConfig.iceServers)
+ ) {
+ this._pc.restartIceNoRenegotiationNeeded();
+ }
+ }
+ }
+
+ __init(rtcConfig) {
+ this._winID = this._win.windowGlobalChild.innerWindowId;
+ let certificates = rtcConfig.certificates || [];
+
+ if (certificates.some(c => c.expires <= Date.now())) {
+ throw new this._win.DOMException(
+ "Unable to create RTCPeerConnection with an expired certificate",
+ "InvalidAccessError"
+ );
+ }
+
+ // TODO(bug 1531875): Check origin of certs
+
+ // TODO(bug 1176518): Remove this code once we support multiple certs
+ let certificate;
+ if (certificates.length == 1) {
+ certificate = certificates[0];
+ } else if (certificates.length) {
+ throw new this._win.DOMException(
+ "RTCPeerConnection does not currently support multiple certificates",
+ "NotSupportedError"
+ );
+ }
+
+ this._documentPrincipal = Cu.getWebIDLCallerPrincipal();
+
+ if (_globalPCList._networkdown) {
+ throw new this._win.DOMException(
+ "Can't create RTCPeerConnections when the network is down",
+ "InvalidStateError"
+ );
+ }
+
+ this.makeGetterSetterEH("ontrack");
+ this.makeLegacyGetterSetterEH(
+ "onaddstream",
+ "Use peerConnection.ontrack instead."
+ );
+ this.makeLegacyGetterSetterEH(
+ "onaddtrack",
+ "Use peerConnection.ontrack instead."
+ );
+ this.makeGetterSetterEH("onicecandidate");
+ this.makeGetterSetterEH("onnegotiationneeded");
+ this.makeGetterSetterEH("onsignalingstatechange");
+ this.makeGetterSetterEH("ondatachannel");
+ this.makeGetterSetterEH("oniceconnectionstatechange");
+ this.makeGetterSetterEH("onicegatheringstatechange");
+ this.makeGetterSetterEH("onconnectionstatechange");
+ this.makeGetterSetterEH("onidentityresult");
+ this.makeGetterSetterEH("onpeeridentity");
+ this.makeGetterSetterEH("onidpassertionerror");
+ this.makeGetterSetterEH("onidpvalidationerror");
+
+ this._pc = new this._win.PeerConnectionImpl();
+
+ this.__DOM_IMPL__._innerObject = this;
+ const observer = new this._win.PeerConnectionObserver(this.__DOM_IMPL__);
+
+ // Add a reference to the PeerConnection to global list (before init).
+ _globalPCList.addPC(this);
+
+ this._pc.initialize(observer, this._win);
+
+ this.setConfiguration(rtcConfig);
+
+ this._certificateReady = this._initCertificate(certificate);
+ this._initIdp();
+ _globalPCList.notifyLifecycleObservers(this, "initialized");
+ }
+
+ getConfiguration() {
+ const config = Object.assign({}, this._config);
+ delete config.sdpSemantics;
+ return config;
+ }
+
+ setConfiguration(rtcConfig) {
+ this._checkClosed();
+ this._validateConfig(rtcConfig);
+ this._checkIfIceRestartRequired(rtcConfig);
+
+ // Allow prefs to tweak these settings before passing to c++, but hide all
+ // of that from JS.
+ const configWithPrefTweaks = Object.assign({}, rtcConfig);
+ this._applyPrefsToConfig(configWithPrefTweaks);
+ this._pc.setConfiguration(configWithPrefTweaks);
+
+ this._config = Object.assign({}, rtcConfig);
+ }
+
+ async _initCertificate(certificate) {
+ if (!certificate) {
+ certificate = await this._win.RTCPeerConnection.generateCertificate({
+ name: "ECDSA",
+ namedCurve: "P-256",
+ });
+ }
+ this._pc.certificate = certificate;
+ }
+
+ _resetPeerIdentityPromise() {
+ this._peerIdentity = new this._win.Promise((resolve, reject) => {
+ this._resolvePeerIdentity = resolve;
+ this._rejectPeerIdentity = reject;
+ });
+ }
+
+ _initIdp() {
+ this._resetPeerIdentityPromise();
+ this._lastIdentityValidation = this._win.Promise.resolve();
+
+ let prefName = "media.peerconnection.identity.timeout";
+ let idpTimeout = Services.prefs.getIntPref(prefName);
+ this._localIdp = new lazy.PeerConnectionIdp(this._win, idpTimeout);
+ this._remoteIdp = new lazy.PeerConnectionIdp(this._win, idpTimeout);
+ }
+
+ // Add a function to the internal operations chain.
+
+ _chain(operation) {
+ return this._pc.chain(operation);
+ }
+
+ // It's basically impossible to use async directly in JSImplemented code,
+ // because the implicit promise must be wrapped to the right type for content.
+ //
+ // The _async wrapper takes care of this. The _legacy wrapper implements
+ // legacy callbacks in a manner that produces correct line-numbers in errors,
+ // provided that methods validate their inputs before putting themselves on
+ // the pc's operations chain.
+ //
+ // These wrappers also serve as guards against settling promises past close().
+
+ _async(func) {
+ return this._win.Promise.resolve(this._closeWrapper(func));
+ }
+
+ _legacy(...args) {
+ return this._win.Promise.resolve(this._legacyCloseWrapper(...args));
+ }
+
+ _auto(onSucc, onErr, func) {
+ return typeof onSucc == "function"
+ ? this._legacy(onSucc, onErr, func)
+ : this._async(func);
+ }
+
+ async _closeWrapper(func) {
+ let closed = this._closed;
+ try {
+ let result = await func();
+ if (!closed && this._closed) {
+ await new Promise(() => {});
+ }
+ return result;
+ } catch (e) {
+ if (!closed && this._closed) {
+ await new Promise(() => {});
+ }
+ throw e;
+ }
+ }
+
+ async _legacyCloseWrapper(onSucc, onErr, func) {
+ let wrapCallback = cb => result => {
+ try {
+ cb && cb(result);
+ } catch (e) {
+ this.logErrorAndCallOnError(e);
+ }
+ };
+
+ try {
+ wrapCallback(onSucc)(await func());
+ } catch (e) {
+ wrapCallback(onErr)(e);
+ }
+ }
+
+ // This implements the fairly common "Queue a task" logic
+ async _queueTaskWithClosedCheck(func) {
+ const pc = this;
+ return new this._win.Promise((resolve, reject) => {
+ Services.tm.dispatchToMainThread({
+ run() {
+ try {
+ if (!pc._closed) {
+ func();
+ resolve();
+ }
+ } catch (e) {
+ reject(e);
+ }
+ },
+ });
+ });
+ }
+
+ /**
+ * An RTCConfiguration may look like this:
+ *
+ * { "iceServers": [ { urls: "stun:stun.example.org", },
+ * { url: "stun:stun.example.org", }, // deprecated version
+ * { urls: ["turn:turn1.x.org", "turn:turn2.x.org"],
+ * username:"jib", credential:"mypass"} ] }
+ *
+ * This function normalizes the structure of the input for rtcConfig.iceServers for us,
+ * so we test well-formed stun/turn urls before passing along to C++.
+ * msg - Error message to detail which array-entry failed, if any.
+ */
+ _validateIceServers(iceServers, msg) {
+ // Normalize iceServers input
+ iceServers.forEach(server => {
+ if (typeof server.urls === "string") {
+ server.urls = [server.urls];
+ } else if (!server.urls && server.url) {
+ // TODO: Remove support for legacy iceServer.url eventually (Bug 1116766)
+ server.urls = [server.url];
+ this.logWarning("RTCIceServer.url is deprecated! Use urls instead.");
+ }
+ });
+
+ let nicerNewURI = uriStr => {
+ try {
+ return Services.io.newURI(uriStr);
+ } catch (e) {
+ if (e.result == Cr.NS_ERROR_MALFORMED_URI) {
+ throw new this._win.DOMException(
+ `${msg} - malformed URI: ${uriStr}`,
+ "SyntaxError"
+ );
+ }
+ throw e;
+ }
+ };
+
+ var stunServers = 0;
+
+ iceServers.forEach(({ urls, username, credential, credentialType }) => {
+ if (!urls) {
+ // TODO: Remove once url is deprecated (Bug 1369563)
+ throw new this._win.TypeError(
+ "Missing required 'urls' member of RTCIceServer"
+ );
+ }
+ if (!urls.length) {
+ throw new this._win.DOMException(
+ `${msg} - urls is empty`,
+ "SyntaxError"
+ );
+ }
+ urls
+ .map(url => nicerNewURI(url))
+ .forEach(({ scheme, spec, query }) => {
+ if (scheme in { turn: 1, turns: 1 }) {
+ if (username == undefined) {
+ throw new this._win.DOMException(
+ `${msg} - missing username: ${spec}`,
+ "InvalidAccessError"
+ );
+ }
+ if (username.length > 512) {
+ throw new this._win.DOMException(
+ `${msg} - username longer then 512 bytes: ${username}`,
+ "InvalidAccessError"
+ );
+ }
+ if (credential == undefined) {
+ throw new this._win.DOMException(
+ `${msg} - missing credential: ${spec}`,
+ "InvalidAccessError"
+ );
+ }
+ if (credentialType != "password") {
+ this.logWarning(
+ `RTCConfiguration TURN credentialType \"${credentialType}\"` +
+ " is not yet implemented. Treating as password." +
+ " https://bugzil.la/1247616"
+ );
+ }
+ this._hasTurnServer = true;
+ // If this is not a TURN TCP/TLS server, it is also a STUN server
+ const parameters = query.split("&");
+ if (!parameters.includes("transport=tcp")) {
+ this._hasStunServer = true;
+ }
+ stunServers += 1;
+ } else if (scheme in { stun: 1, stuns: 1 }) {
+ this._hasStunServer = true;
+ stunServers += 1;
+ } else {
+ throw new this._win.DOMException(
+ `${msg} - improper scheme: ${scheme}`,
+ "SyntaxError"
+ );
+ }
+ if (scheme in { stuns: 1 }) {
+ this.logWarning(scheme.toUpperCase() + " is not yet supported.");
+ }
+ if (stunServers >= 5) {
+ this.logError(
+ "Using five or more STUN/TURN servers causes problems"
+ );
+ } else if (stunServers > 2) {
+ this.logWarning(
+ "Using more than two STUN/TURN servers slows down discovery"
+ );
+ }
+ });
+ });
+ }
+
+ // Ideally, this should be of the form _checkState(state),
+ // where the state is taken from an enumeration containing
+ // the valid peer connection states defined in the WebRTC
+ // spec. See Bug 831756.
+ _checkClosed() {
+ if (this._closed) {
+ throw new this._win.DOMException(
+ "Peer connection is closed",
+ "InvalidStateError"
+ );
+ }
+ }
+
+ dispatchEvent(event) {
+ // PC can close while events are firing if there is an async dispatch
+ // in c++ land. But let through "closed" signaling and ice connection events.
+ if (!this._suppressEvents) {
+ this.__DOM_IMPL__.dispatchEvent(event);
+ }
+ }
+
+ // Log error message to web console and window.onerror, if present.
+ logErrorAndCallOnError(e) {
+ this.logMsg(
+ e.message,
+ e.fileName,
+ e.lineNumber,
+ Ci.nsIScriptError.errorFlag
+ );
+
+ // Safely call onerror directly if present (necessary for testing)
+ try {
+ if (typeof this._win.onerror === "function") {
+ this._win.onerror(e.message, e.fileName, e.lineNumber);
+ }
+ } catch (e) {
+ // If onerror itself throws, service it.
+ try {
+ this.logMsg(
+ e.message,
+ e.fileName,
+ e.lineNumber,
+ Ci.nsIScriptError.errorFlag
+ );
+ } catch (e) {}
+ }
+ }
+
+ logError(msg) {
+ this.logStackMsg(msg, Ci.nsIScriptError.errorFlag);
+ }
+
+ logWarning(msg) {
+ this.logStackMsg(msg, Ci.nsIScriptError.warningFlag);
+ }
+
+ logStackMsg(msg, flag) {
+ let err = this._win.Error();
+ this.logMsg(msg, err.fileName, err.lineNumber, flag);
+ }
+
+ logMsg(msg, file, line, flag) {
+ return logMsg(msg, file, line, flag, this._winID);
+ }
+
+ getEH(type) {
+ return this.__DOM_IMPL__.getEventHandler(type);
+ }
+
+ setEH(type, handler) {
+ this.__DOM_IMPL__.setEventHandler(type, handler);
+ }
+
+ makeGetterSetterEH(name) {
+ Object.defineProperty(this, name, {
+ get() {
+ return this.getEH(name);
+ },
+ set(h) {
+ this.setEH(name, h);
+ },
+ });
+ }
+
+ makeLegacyGetterSetterEH(name, msg) {
+ Object.defineProperty(this, name, {
+ get() {
+ return this.getEH(name);
+ },
+ set(h) {
+ this.logWarning(name + " is deprecated! " + msg);
+ this.setEH(name, h);
+ },
+ });
+ }
+
+ createOffer(optionsOrOnSucc, onErr, options) {
+ let onSuccess = null;
+ if (typeof optionsOrOnSucc == "function") {
+ onSuccess = optionsOrOnSucc;
+ } else {
+ options = optionsOrOnSucc;
+ }
+ // This entry-point handles both new and legacy call sig. Decipher which one
+ if (onSuccess) {
+ return this._legacy(onSuccess, onErr, () => this._createOffer(options));
+ }
+ return this._async(() => this._createOffer(options));
+ }
+
+ // Ensures that we have at least one transceiver of |kind| that is
+ // configured to receive. It will create one if necessary.
+ _ensureOfferToReceive(kind) {
+ let hasRecv = this.getTransceivers().some(
+ transceiver =>
+ transceiver.getKind() == kind &&
+ (transceiver.direction == "sendrecv" ||
+ transceiver.direction == "recvonly") &&
+ !transceiver.stopped
+ );
+
+ if (!hasRecv) {
+ this._addTransceiverNoEvents(kind, { direction: "recvonly" });
+ }
+ }
+
+ // Handles offerToReceiveAudio/Video
+ _ensureTransceiversForOfferToReceive(options) {
+ if (options.offerToReceiveAudio) {
+ this._ensureOfferToReceive("audio");
+ }
+
+ if (options.offerToReceiveVideo) {
+ this._ensureOfferToReceive("video");
+ }
+
+ this.getTransceivers()
+ .filter(transceiver => {
+ return (
+ (options.offerToReceiveVideo === false &&
+ transceiver.receiver.track.kind == "video") ||
+ (options.offerToReceiveAudio === false &&
+ transceiver.receiver.track.kind == "audio")
+ );
+ })
+ .forEach(transceiver => {
+ if (transceiver.direction == "sendrecv") {
+ transceiver.setDirectionInternal("sendonly");
+ } else if (transceiver.direction == "recvonly") {
+ transceiver.setDirectionInternal("inactive");
+ }
+ });
+ }
+
+ _createOffer(options) {
+ this._checkClosed();
+ this._ensureTransceiversForOfferToReceive(options);
+ return this._chain(() => this._createAnOffer(options));
+ }
+
+ async _createAnOffer(options = {}) {
+ switch (this.signalingState) {
+ case "stable":
+ case "have-local-offer":
+ break;
+ default:
+ throw new this._win.DOMException(
+ `Cannot create offer in ${this.signalingState}`,
+ "InvalidStateError"
+ );
+ }
+ let haveAssertion;
+ if (this._localIdp.enabled) {
+ haveAssertion = this._getIdentityAssertion();
+ }
+ await this._getPermission();
+ await this._certificateReady;
+ let sdp = await new Promise((resolve, reject) => {
+ this._onCreateOfferSuccess = resolve;
+ this._onCreateOfferFailure = reject;
+ this._pc.createOffer(options);
+ });
+ if (haveAssertion) {
+ await haveAssertion;
+ sdp = this._localIdp.addIdentityAttribute(sdp);
+ }
+ return Cu.cloneInto({ type: "offer", sdp }, this._win);
+ }
+
+ createAnswer(optionsOrOnSucc, onErr) {
+ // This entry-point handles both new and legacy call sig. Decipher which one
+ if (typeof optionsOrOnSucc == "function") {
+ return this._legacy(optionsOrOnSucc, onErr, () => this._createAnswer({}));
+ }
+ return this._async(() => this._createAnswer(optionsOrOnSucc));
+ }
+
+ _createAnswer(options) {
+ this._checkClosed();
+ return this._chain(() => this._createAnAnswer());
+ }
+
+ async _createAnAnswer() {
+ if (this.signalingState != "have-remote-offer") {
+ throw new this._win.DOMException(
+ `Cannot create answer in ${this.signalingState}`,
+ "InvalidStateError"
+ );
+ }
+ let haveAssertion;
+ if (this._localIdp.enabled) {
+ haveAssertion = this._getIdentityAssertion();
+ }
+ await this._getPermission();
+ await this._certificateReady;
+ let sdp = await new Promise((resolve, reject) => {
+ this._onCreateAnswerSuccess = resolve;
+ this._onCreateAnswerFailure = reject;
+ this._pc.createAnswer();
+ });
+ if (haveAssertion) {
+ await haveAssertion;
+ sdp = this._localIdp.addIdentityAttribute(sdp);
+ }
+ return Cu.cloneInto({ type: "answer", sdp }, this._win);
+ }
+
+ async _getPermission() {
+ if (!this._havePermission) {
+ const privileged =
+ this._documentPrincipal.isSystemPrincipal ||
+ Services.prefs.getBoolPref("media.navigator.permission.disabled");
+
+ if (privileged) {
+ this._havePermission = Promise.resolve();
+ } else {
+ this._havePermission = new Promise((resolve, reject) => {
+ this._settlePermission = { allow: resolve, deny: reject };
+ let outerId = this._win.docShell.outerWindowID;
+
+ let chrome = new CreateOfferRequest(
+ outerId,
+ this._winID,
+ this._globalPCListId,
+ false
+ );
+ let request = this._win.CreateOfferRequest._create(this._win, chrome);
+ Services.obs.notifyObservers(request, "PeerConnection:request");
+ });
+ }
+ }
+ return this._havePermission;
+ }
+
+ _sanityCheckSdp(sdp) {
+ // The fippo butter finger filter AKA non-ASCII chars
+ // Note: SDP allows non-ASCII character in the subject (who cares?)
+ // eslint-disable-next-line no-control-regex
+ let pos = sdp.search(/[^\u0000-\u007f]/);
+ if (pos != -1) {
+ throw new this._win.DOMException(
+ "SDP contains non ASCII characters at position " + pos,
+ "InvalidParameterError"
+ );
+ }
+ }
+
+ setLocalDescription(desc, onSucc, onErr) {
+ return this._auto(onSucc, onErr, () => this._setLocalDescription(desc));
+ }
+
+ _setLocalDescription({ type, sdp }) {
+ if (type == "pranswer") {
+ throw new this._win.DOMException(
+ "pranswer not yet implemented",
+ "NotSupportedError"
+ );
+ }
+ this._checkClosed();
+ return this._chain(async () => {
+ // Avoid Promise.all ahead of synchronous part of spec algorithm, since it
+ // defers. NOTE: The spec says to return an already-rejected promise in
+ // some cases, which is difficult to achieve in practice from JS (would
+ // require avoiding await and then() entirely), but we want to come as
+ // close as we reasonably can.
+ const p = this._getPermission();
+ if (!type) {
+ switch (this.signalingState) {
+ case "stable":
+ case "have-local-offer":
+ case "have-remote-pranswer":
+ type = "offer";
+ break;
+ default:
+ type = "answer";
+ break;
+ }
+ }
+ if (!sdp) {
+ if (type == "offer") {
+ await this._createAnOffer();
+ } else if (type == "answer") {
+ await this._createAnAnswer();
+ }
+ } else {
+ this._sanityCheckSdp(sdp);
+ }
+
+ try {
+ await new Promise((resolve, reject) => {
+ this._onSetDescriptionSuccess = resolve;
+ this._onSetDescriptionFailure = reject;
+ this._pc.setLocalDescription(this._actions[type], sdp);
+ });
+ await p;
+ } catch (e) {
+ this._pc.onSetDescriptionError();
+ throw e;
+ }
+ await this._pc.onSetDescriptionSuccess(type, false);
+ });
+ }
+
+ async _validateIdentity(sdp, origin) {
+ // Only run a single identity verification at a time. We have to do this to
+ // avoid problems with the fact that identity validation doesn't block the
+ // resolution of setRemoteDescription().
+ const validate = async () => {
+ // Access this._pc synchronously in case pc is closed later
+ const identity = this._pc.peerIdentity;
+ await this._lastIdentityValidation;
+ const msg = await this._remoteIdp.verifyIdentityFromSDP(sdp, origin);
+ // If this pc has an identity already, then the identity in sdp must match
+ if (identity && (!msg || msg.identity !== identity)) {
+ throw new this._win.DOMException(
+ "Peer Identity mismatch, expected: " + identity,
+ "OperationError"
+ );
+ }
+ if (this._closed) {
+ return;
+ }
+ if (msg) {
+ // Set new identity and generate an event.
+ this._pc.peerIdentity = msg.identity;
+ this._resolvePeerIdentity(
+ Cu.cloneInto(
+ {
+ idp: this._remoteIdp.provider,
+ name: msg.identity,
+ },
+ this._win
+ )
+ );
+ }
+ };
+
+ const haveValidation = validate();
+
+ // Always eat errors on this chain
+ this._lastIdentityValidation = haveValidation.catch(() => {});
+
+ // If validation fails, we have some work to do. Fork it so it cannot
+ // interfere with the validation chain itself, even if the catch function
+ // throws.
+ haveValidation.catch(e => {
+ if (this._closed) {
+ return;
+ }
+ this._rejectPeerIdentity(e);
+
+ // If we don't expect a specific peer identity, failure to get a valid
+ // peer identity is not a terminal state, so replace the promise to
+ // allow another attempt.
+ if (!this._pc.peerIdentity) {
+ this._resetPeerIdentityPromise();
+ }
+ });
+
+ if (this._closed) {
+ return;
+ }
+ // Only wait for IdP validation if we need identity matching
+ if (this._pc.peerIdentity) {
+ await haveValidation;
+ }
+ }
+
+ setRemoteDescription(desc, onSucc, onErr) {
+ return this._auto(onSucc, onErr, () => this._setRemoteDescription(desc));
+ }
+
+ _setRemoteDescription({ type, sdp }) {
+ if (!type) {
+ throw new this._win.TypeError(
+ "Missing required 'type' member of RTCSessionDescriptionInit"
+ );
+ }
+ if (type == "pranswer") {
+ throw new this._win.DOMException(
+ "pranswer not yet implemented",
+ "NotSupportedError"
+ );
+ }
+ this._checkClosed();
+ return this._chain(async () => {
+ try {
+ if (type == "offer" && this.signalingState == "have-local-offer") {
+ await new Promise((resolve, reject) => {
+ this._onSetDescriptionSuccess = resolve;
+ this._onSetDescriptionFailure = reject;
+ this._pc.setLocalDescription(
+ Ci.IPeerConnection.kActionRollback,
+ ""
+ );
+ });
+ await this._pc.onSetDescriptionSuccess("rollback", false);
+ this._updateCanTrickle();
+ }
+
+ if (this._closed) {
+ return;
+ }
+
+ this._sanityCheckSdp(sdp);
+
+ const p = this._getPermission();
+
+ const haveSetRemote = new Promise((resolve, reject) => {
+ this._onSetDescriptionSuccess = resolve;
+ this._onSetDescriptionFailure = reject;
+ this._pc.setRemoteDescription(this._actions[type], sdp);
+ });
+
+ if (type != "rollback") {
+ // Do setRemoteDescription and identity validation in parallel
+ await this._validateIdentity(sdp);
+ }
+ await p;
+ await haveSetRemote;
+ } catch (e) {
+ this._pc.onSetDescriptionError();
+ throw e;
+ }
+
+ await this._pc.onSetDescriptionSuccess(type, true);
+ this._updateCanTrickle();
+ });
+ }
+
+ setIdentityProvider(provider, { protocol, usernameHint, peerIdentity } = {}) {
+ this._checkClosed();
+ peerIdentity = peerIdentity || this._pc.peerIdentity;
+ this._localIdp.setIdentityProvider(
+ provider,
+ protocol,
+ usernameHint,
+ peerIdentity
+ );
+ }
+
+ async _getIdentityAssertion() {
+ await this._certificateReady;
+ return this._localIdp.getIdentityAssertion(
+ this._pc.fingerprint,
+ this._documentPrincipal.origin
+ );
+ }
+
+ getIdentityAssertion() {
+ this._checkClosed();
+ return this._win.Promise.resolve(
+ this._chain(() => this._getIdentityAssertion())
+ );
+ }
+
+ get canTrickleIceCandidates() {
+ return this._canTrickle;
+ }
+
+ _updateCanTrickle() {
+ let containsTrickle = section => {
+ let lines = section.toLowerCase().split(/(?:\r\n?|\n)/);
+ return lines.some(line => {
+ let prefix = "a=ice-options:";
+ if (line.substring(0, prefix.length) !== prefix) {
+ return false;
+ }
+ let tokens = line.substring(prefix.length).split(" ");
+ return tokens.some(x => x === "trickle");
+ });
+ };
+
+ let desc = null;
+ try {
+ // The getter for remoteDescription can throw if the pc is closed.
+ desc = this.remoteDescription;
+ } catch (e) {}
+ if (!desc) {
+ this._canTrickle = null;
+ return;
+ }
+
+ let sections = desc.sdp.split(/(?:\r\n?|\n)m=/);
+ let topSection = sections.shift();
+ this._canTrickle =
+ containsTrickle(topSection) || sections.every(containsTrickle);
+ }
+
+ addIceCandidate(cand, onSucc, onErr) {
+ if (
+ cand.candidate != "" &&
+ cand.sdpMid == null &&
+ cand.sdpMLineIndex == null
+ ) {
+ throw new this._win.TypeError(
+ "Cannot add a candidate without specifying either sdpMid or sdpMLineIndex"
+ );
+ }
+ return this._auto(onSucc, onErr, () => this._addIceCandidate(cand));
+ }
+
+ async _addIceCandidate({
+ candidate,
+ sdpMid,
+ sdpMLineIndex,
+ usernameFragment,
+ }) {
+ this._checkClosed();
+ return this._chain(async () => {
+ if (
+ !this._pc.pendingRemoteDescription.length &&
+ !this._pc.currentRemoteDescription.length
+ ) {
+ throw new this._win.DOMException(
+ "No remoteDescription.",
+ "InvalidStateError"
+ );
+ }
+ return new Promise((resolve, reject) => {
+ this._onAddIceCandidateSuccess = resolve;
+ this._onAddIceCandidateError = reject;
+ this._pc.addIceCandidate(
+ candidate,
+ sdpMid || "",
+ usernameFragment || "",
+ sdpMLineIndex
+ );
+ });
+ });
+ }
+
+ restartIce() {
+ this._pc.restartIce();
+ }
+
+ addStream(stream) {
+ stream.getTracks().forEach(track => this.addTrack(track, stream));
+ }
+
+ addTrack(track, ...streams) {
+ this._checkClosed();
+
+ if (
+ this.getTransceivers().some(
+ transceiver => transceiver.sender.track == track
+ )
+ ) {
+ throw new this._win.DOMException(
+ "This track is already set on a sender.",
+ "InvalidAccessError"
+ );
+ }
+
+ let transceiver = this.getTransceivers().find(transceiver => {
+ return (
+ transceiver.sender.track == null &&
+ transceiver.getKind() == track.kind &&
+ !transceiver.stopped &&
+ !transceiver.hasBeenUsedToSend()
+ );
+ });
+
+ if (transceiver) {
+ transceiver.sender.setTrack(track);
+ transceiver.sender.setStreamsImpl(...streams);
+ if (transceiver.direction == "recvonly") {
+ transceiver.setDirectionInternal("sendrecv");
+ } else if (transceiver.direction == "inactive") {
+ transceiver.setDirectionInternal("sendonly");
+ }
+ } else {
+ transceiver = this._addTransceiverNoEvents(
+ track,
+ {
+ streams,
+ direction: "sendrecv",
+ },
+ true
+ );
+ }
+
+ this.updateNegotiationNeeded();
+ return transceiver.sender;
+ }
+
+ removeTrack(sender) {
+ this._checkClosed();
+
+ if (!this._pc.createdSender(sender)) {
+ throw new this._win.DOMException(
+ "This sender was not created by this PeerConnection",
+ "InvalidAccessError"
+ );
+ }
+
+ let transceiver = this.getTransceivers().find(
+ transceiver => !transceiver.stopped && transceiver.sender == sender
+ );
+
+ // If the transceiver was removed due to rollback, let it slide.
+ if (!transceiver || !sender.track) {
+ return;
+ }
+
+ sender.setTrack(null);
+ if (transceiver.direction == "sendrecv") {
+ transceiver.setDirectionInternal("recvonly");
+ } else if (transceiver.direction == "sendonly") {
+ transceiver.setDirectionInternal("inactive");
+ }
+
+ this.updateNegotiationNeeded();
+ }
+
+ _addTransceiverNoEvents(sendTrackOrKind, init, addTrackMagic) {
+ let sendTrack = null;
+ let kind;
+ if (typeof sendTrackOrKind == "string") {
+ kind = sendTrackOrKind;
+ switch (kind) {
+ case "audio":
+ case "video":
+ break;
+ default:
+ throw new this._win.TypeError("Invalid media kind");
+ }
+ } else {
+ sendTrack = sendTrackOrKind;
+ kind = sendTrack.kind;
+ }
+
+ try {
+ return this._pc.addTransceiver(init, kind, sendTrack, addTrackMagic);
+ } catch (e) {
+ // Exceptions thrown by c++ code do not propagate. In most cases, that's
+ // fine because we're using Promises, which can be copied. But this is
+ // not promise-based, so we have to do this sketchy stuff.
+ const holder = new StructuredCloneHolder(
+ "",
+ "",
+ new ClonedErrorHolder(e)
+ );
+ throw holder.deserialize(this._win);
+ }
+ }
+
+ addTransceiver(sendTrackOrKind, init) {
+ this._checkClosed();
+ let transceiver = this._addTransceiverNoEvents(sendTrackOrKind, init);
+ this.updateNegotiationNeeded();
+ return transceiver;
+ }
+
+ updateNegotiationNeeded() {
+ this._pc.updateNegotiationNeeded();
+ }
+
+ close() {
+ if (this._closed) {
+ return;
+ }
+ this._closed = true;
+ this.changeIceConnectionState("closed");
+ if (this._localIdp) {
+ this._localIdp.close();
+ }
+ if (this._remoteIdp) {
+ this._remoteIdp.close();
+ }
+ this._pc.close();
+ this._suppressEvents = true;
+ }
+
+ getLocalStreams() {
+ this._checkClosed();
+ let localStreams = new Set();
+ this.getTransceivers().forEach(transceiver => {
+ transceiver.sender.getStreams().forEach(stream => {
+ localStreams.add(stream);
+ });
+ });
+ return [...localStreams.values()];
+ }
+
+ getRemoteStreams() {
+ this._checkClosed();
+ return this._pc.getRemoteStreams();
+ }
+
+ getSenders() {
+ return this.getTransceivers()
+ .filter(transceiver => !transceiver.stopped)
+ .map(transceiver => transceiver.sender);
+ }
+
+ getReceivers() {
+ return this.getTransceivers()
+ .filter(transceiver => !transceiver.stopped)
+ .map(transceiver => transceiver.receiver);
+ }
+
+ mozSetPacketCallback(callback) {
+ this._onPacket = callback;
+ }
+
+ mozEnablePacketDump(level, type, sending) {
+ this._pc.enablePacketDump(level, type, sending);
+ }
+
+ mozDisablePacketDump(level, type, sending) {
+ this._pc.disablePacketDump(level, type, sending);
+ }
+
+ getTransceivers() {
+ return this._pc.getTransceivers();
+ }
+
+ get localDescription() {
+ return this.pendingLocalDescription || this.currentLocalDescription;
+ }
+
+ get currentLocalDescription() {
+ this._checkClosed();
+ const sdp = this._pc.currentLocalDescription;
+ if (!sdp.length) {
+ return null;
+ }
+ const type = this._pc.currentOfferer ? "offer" : "answer";
+ return new this._win.RTCSessionDescription({ type, sdp });
+ }
+
+ get pendingLocalDescription() {
+ this._checkClosed();
+ const sdp = this._pc.pendingLocalDescription;
+ if (!sdp.length) {
+ return null;
+ }
+ const type = this._pc.pendingOfferer ? "offer" : "answer";
+ return new this._win.RTCSessionDescription({ type, sdp });
+ }
+
+ get remoteDescription() {
+ return this.pendingRemoteDescription || this.currentRemoteDescription;
+ }
+
+ get currentRemoteDescription() {
+ this._checkClosed();
+ const sdp = this._pc.currentRemoteDescription;
+ if (!sdp.length) {
+ return null;
+ }
+ const type = this._pc.currentOfferer ? "answer" : "offer";
+ return new this._win.RTCSessionDescription({ type, sdp });
+ }
+
+ get pendingRemoteDescription() {
+ this._checkClosed();
+ const sdp = this._pc.pendingRemoteDescription;
+ if (!sdp.length) {
+ return null;
+ }
+ const type = this._pc.pendingOfferer ? "answer" : "offer";
+ return new this._win.RTCSessionDescription({ type, sdp });
+ }
+
+ get peerIdentity() {
+ return this._peerIdentity;
+ }
+ get idpLoginUrl() {
+ return this._localIdp.idpLoginUrl;
+ }
+ get id() {
+ return this._pc.id;
+ }
+ set id(s) {
+ this._pc.id = s;
+ }
+ get iceGatheringState() {
+ return this._pc.iceGatheringState;
+ }
+ get iceConnectionState() {
+ return this._iceConnectionState;
+ }
+ get connectionState() {
+ return this._pc.connectionState;
+ }
+
+ get signalingState() {
+ // checking for our local pc closed indication
+ // before invoking the pc methods.
+ if (this._closed) {
+ return "closed";
+ }
+ return this._pc.signalingState;
+ }
+
+ handleIceGatheringStateChange() {
+ _globalPCList.notifyLifecycleObservers(this, "icegatheringstatechange");
+ this.dispatchEvent(new this._win.Event("icegatheringstatechange"));
+ if (this.iceGatheringState === "complete") {
+ this.dispatchEvent(
+ new this._win.RTCPeerConnectionIceEvent("icecandidate", {
+ candidate: null,
+ })
+ );
+ }
+ }
+
+ changeIceConnectionState(state) {
+ if (state != this._iceConnectionState) {
+ this._iceConnectionState = state;
+ _globalPCList.notifyLifecycleObservers(this, "iceconnectionstatechange");
+ if (!this._closed) {
+ this.dispatchEvent(new this._win.Event("iceconnectionstatechange"));
+ }
+ }
+ }
+
+ getStats(selector, onSucc, onErr) {
+ if (selector !== null) {
+ let matchingSenders = this.getSenders().filter(s => s.track === selector);
+ let matchingReceivers = this.getReceivers().filter(
+ r => r.track === selector
+ );
+
+ if (matchingSenders.length + matchingReceivers.length != 1) {
+ throw new this._win.DOMException(
+ "track must be associated with a unique sender or receiver, but " +
+ " is associated with " +
+ matchingSenders.length +
+ " senders and " +
+ matchingReceivers.length +
+ " receivers.",
+ "InvalidAccessError"
+ );
+ }
+ }
+
+ return this._auto(onSucc, onErr, () => this._pc.getStats(selector));
+ }
+
+ get sctp() {
+ return this._pc.sctp;
+ }
+
+ createDataChannel(
+ label,
+ {
+ maxRetransmits,
+ ordered,
+ negotiated,
+ id = null,
+ maxRetransmitTime,
+ maxPacketLifeTime,
+ protocol,
+ } = {}
+ ) {
+ this._checkClosed();
+ this._pcTelemetry.recordDataChannelInit(
+ maxRetransmitTime,
+ maxPacketLifeTime
+ );
+
+ if (maxPacketLifeTime === undefined) {
+ maxPacketLifeTime = maxRetransmitTime;
+ }
+
+ if (maxRetransmitTime !== undefined) {
+ this.logWarning(
+ "Use maxPacketLifeTime instead of deprecated maxRetransmitTime which will stop working soon in createDataChannel!"
+ );
+ }
+
+ if (protocol.length > 32767) {
+ // At least 65536/2 UTF-16 characters. UTF-8 might be too long.
+ // Spec says to check how long |protocol| and |label| are in _bytes_. This
+ // is a little ambiguous. For now, examine the length of the utf-8 encoding.
+ const byteCounter = new TextEncoder();
+
+ if (byteCounter.encode(protocol).length > 65535) {
+ throw new this._win.TypeError(
+ "protocol cannot be longer than 65535 bytes"
+ );
+ }
+ }
+
+ if (label.length > 32767) {
+ const byteCounter = new TextEncoder();
+ if (byteCounter.encode(label).length > 65535) {
+ throw new this._win.TypeError(
+ "label cannot be longer than 65535 bytes"
+ );
+ }
+ }
+
+ if (!negotiated) {
+ id = null;
+ } else if (id === null) {
+ throw new this._win.TypeError("id is required when negotiated is true");
+ }
+ if (maxPacketLifeTime !== undefined && maxRetransmits !== undefined) {
+ throw new this._win.TypeError(
+ "Both maxPacketLifeTime and maxRetransmits cannot be provided"
+ );
+ }
+ if (id == 65535) {
+ throw new this._win.TypeError("id cannot be 65535");
+ }
+ // Must determine the type where we still know if entries are undefined.
+ let type;
+ if (maxPacketLifeTime !== undefined) {
+ type = Ci.IPeerConnection.kDataChannelPartialReliableTimed;
+ } else if (maxRetransmits !== undefined) {
+ type = Ci.IPeerConnection.kDataChannelPartialReliableRexmit;
+ } else {
+ type = Ci.IPeerConnection.kDataChannelReliable;
+ }
+ // Synchronous since it doesn't block.
+ let dataChannel;
+ try {
+ dataChannel = this._pc.createDataChannel(
+ label,
+ protocol,
+ type,
+ ordered,
+ maxPacketLifeTime,
+ maxRetransmits,
+ negotiated,
+ id
+ );
+ } catch (e) {
+ if (e.result != Cr.NS_ERROR_NOT_AVAILABLE) {
+ throw e;
+ }
+
+ const msg =
+ id === null ? "No available id could be generated" : "Id is in use";
+ throw new this._win.DOMException(msg, "OperationError");
+ }
+
+ // Spec says to only do this if this is the first DataChannel created,
+ // but the c++ code that does the "is negotiation needed" checking will
+ // only ever return true on the first one.
+ this.updateNegotiationNeeded();
+
+ return dataChannel;
+ }
+}
+
+setupPrototype(RTCPeerConnection, {
+ classID: PC_CID,
+ contractID: PC_CONTRACT,
+ QueryInterface: ChromeUtils.generateQI(["nsIDOMGlobalPropertyInitializer"]),
+ _actions: {
+ offer: Ci.IPeerConnection.kActionOffer,
+ answer: Ci.IPeerConnection.kActionAnswer,
+ pranswer: Ci.IPeerConnection.kActionPRAnswer,
+ rollback: Ci.IPeerConnection.kActionRollback,
+ },
+});
+
+// This is a separate class because we don't want to expose it to DOM.
+
+export class PeerConnectionObserver {
+ init(win) {
+ this._win = win;
+ }
+
+ __init(dompc) {
+ this._dompc = dompc._innerObject;
+ }
+
+ newError({ message, name }) {
+ return new this._dompc._win.DOMException(message, name);
+ }
+
+ dispatchEvent(event) {
+ this._dompc.dispatchEvent(event);
+ }
+
+ onCreateOfferSuccess(sdp) {
+ this._dompc._onCreateOfferSuccess(sdp);
+ }
+
+ onCreateOfferError(error) {
+ this._dompc._onCreateOfferFailure(this.newError(error));
+ }
+
+ onCreateAnswerSuccess(sdp) {
+ this._dompc._onCreateAnswerSuccess(sdp);
+ }
+
+ onCreateAnswerError(error) {
+ this._dompc._onCreateAnswerFailure(this.newError(error));
+ }
+
+ onSetDescriptionSuccess() {
+ this._dompc._onSetDescriptionSuccess();
+ }
+
+ onSetDescriptionError(error) {
+ this._dompc._onSetDescriptionFailure(this.newError(error));
+ }
+
+ onAddIceCandidateSuccess() {
+ this._dompc._onAddIceCandidateSuccess();
+ }
+
+ onAddIceCandidateError(error) {
+ this._dompc._onAddIceCandidateError(this.newError(error));
+ }
+
+ onIceCandidate(sdpMLineIndex, sdpMid, candidate, usernameFragment) {
+ let win = this._dompc._win;
+ if (candidate || sdpMid || usernameFragment) {
+ if (candidate.includes(" typ relay ")) {
+ this._dompc._iceGatheredRelayCandidates = true;
+ }
+ candidate = new win.RTCIceCandidate({
+ candidate,
+ sdpMid,
+ sdpMLineIndex,
+ usernameFragment,
+ });
+ }
+ this.dispatchEvent(
+ new win.RTCPeerConnectionIceEvent("icecandidate", { candidate })
+ );
+ }
+
+ // This method is primarily responsible for updating iceConnectionState.
+ // This state is defined in the WebRTC specification as follows:
+ //
+ // iceConnectionState:
+ // -------------------
+ // new Any of the RTCIceTransports are in the new state and none
+ // of them are in the checking, failed or disconnected state.
+ //
+ // checking Any of the RTCIceTransports are in the checking state and
+ // none of them are in the failed or disconnected state.
+ //
+ // connected All RTCIceTransports are in the connected, completed or
+ // closed state and at least one of them is in the connected
+ // state.
+ //
+ // completed All RTCIceTransports are in the completed or closed state
+ // and at least one of them is in the completed state.
+ //
+ // failed Any of the RTCIceTransports are in the failed state.
+ //
+ // disconnected Any of the RTCIceTransports are in the disconnected state
+ // and none of them are in the failed state.
+ //
+ // closed All of the RTCIceTransports are in the closed state.
+
+ handleIceConnectionStateChange(iceConnectionState) {
+ let pc = this._dompc;
+ if (pc.iceConnectionState === iceConnectionState) {
+ return;
+ }
+
+ if (iceConnectionState === "failed") {
+ if (!pc._hasStunServer) {
+ pc.logError(
+ "ICE failed, add a STUN server and see about:webrtc for more details"
+ );
+ } else if (!pc._hasTurnServer) {
+ pc.logError(
+ "ICE failed, add a TURN server and see about:webrtc for more details"
+ );
+ } else if (pc._hasTurnServer && !pc._iceGatheredRelayCandidates) {
+ pc.logError(
+ "ICE failed, your TURN server appears to be broken, see about:webrtc for more details"
+ );
+ } else {
+ pc.logError("ICE failed, see about:webrtc for more details");
+ }
+ }
+
+ pc.changeIceConnectionState(iceConnectionState);
+ }
+
+ onStateChange(state) {
+ if (!this._dompc) {
+ return;
+ }
+
+ if (state == "SignalingState") {
+ this.dispatchEvent(new this._win.Event("signalingstatechange"));
+ return;
+ }
+
+ if (!this._dompc._pc) {
+ return;
+ }
+
+ switch (state) {
+ case "IceConnectionState":
+ let connState = this._dompc._pc.iceConnectionState;
+ this._dompc._queueTaskWithClosedCheck(() => {
+ this.handleIceConnectionStateChange(connState);
+ });
+ break;
+
+ case "IceGatheringState":
+ this._dompc.handleIceGatheringStateChange();
+ break;
+
+ case "ConnectionState":
+ _globalPCList.notifyLifecycleObservers(this, "connectionstatechange");
+ this.dispatchEvent(new this._win.Event("connectionstatechange"));
+ break;
+
+ default:
+ this._dompc.logWarning("Unhandled state type: " + state);
+ break;
+ }
+ }
+
+ onTransceiverNeeded(kind, transceiverImpl) {
+ this._dompc._onTransceiverNeeded(kind, transceiverImpl);
+ }
+
+ notifyDataChannel(channel) {
+ this.dispatchEvent(
+ new this._dompc._win.RTCDataChannelEvent("datachannel", { channel })
+ );
+ }
+
+ fireTrackEvent(receiver, streams) {
+ const pc = this._dompc;
+ const transceiver = pc.getTransceivers().find(t => t.receiver == receiver);
+ if (!transceiver) {
+ return;
+ }
+ const track = receiver.track;
+ this.dispatchEvent(
+ new this._win.RTCTrackEvent("track", {
+ transceiver,
+ receiver,
+ track,
+ streams,
+ })
+ );
+ // Fire legacy event as well for a little bit.
+ this.dispatchEvent(
+ new this._win.MediaStreamTrackEvent("addtrack", { track })
+ );
+ }
+
+ fireStreamEvent(stream) {
+ const ev = new this._win.MediaStreamEvent("addstream", { stream });
+ this.dispatchEvent(ev);
+ }
+
+ fireNegotiationNeededEvent() {
+ this.dispatchEvent(new this._win.Event("negotiationneeded"));
+ }
+
+ onPacket(level, type, sending, packet) {
+ var pc = this._dompc;
+ if (pc._onPacket) {
+ pc._onPacket(level, type, sending, packet);
+ }
+ }
+}
+
+setupPrototype(PeerConnectionObserver, {
+ classID: PC_OBS_CID,
+ contractID: PC_OBS_CONTRACT,
+ QueryInterface: ChromeUtils.generateQI(["nsIDOMGlobalPropertyInitializer"]),
+});
+
+export class RTCPeerConnectionStatic {
+ init(win) {
+ this._winID = win.windowGlobalChild.innerWindowId;
+ }
+
+ registerPeerConnectionLifecycleCallback(cb) {
+ _globalPCList._registerPeerConnectionLifecycleCallback(this._winID, cb);
+ }
+}
+
+setupPrototype(RTCPeerConnectionStatic, {
+ classID: PC_STATIC_CID,
+ contractID: PC_STATIC_CONTRACT,
+ QueryInterface: ChromeUtils.generateQI(["nsIDOMGlobalPropertyInitializer"]),
+});
+
+export class CreateOfferRequest {
+ constructor(windowID, innerWindowID, callID, isSecure) {
+ Object.assign(this, { windowID, innerWindowID, callID, isSecure });
+ }
+}
+
+setupPrototype(CreateOfferRequest, {
+ classID: PC_COREQUEST_CID,
+ contractID: PC_COREQUEST_CONTRACT,
+ QueryInterface: ChromeUtils.generateQI([]),
+});
diff --git a/dom/media/PeerConnectionIdp.sys.mjs b/dom/media/PeerConnectionIdp.sys.mjs
new file mode 100644
index 0000000000..2be6643b06
--- /dev/null
+++ b/dom/media/PeerConnectionIdp.sys.mjs
@@ -0,0 +1,378 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ IdpSandbox: "resource://gre/modules/media/IdpSandbox.sys.mjs",
+});
+
+/**
+ * Creates an IdP helper.
+ *
+ * @param win (object) the window we are working for
+ * @param timeout (int) the timeout in milliseconds
+ */
+export function PeerConnectionIdp(win, timeout) {
+ this._win = win;
+ this._timeout = timeout || 5000;
+
+ this.provider = null;
+ this._resetAssertion();
+}
+
+(function () {
+ PeerConnectionIdp._mLinePattern = new RegExp("^m=", "m");
+ // attributes are funny, the 'a' is case sensitive, the name isn't
+ let pattern = "^a=[iI][dD][eE][nN][tT][iI][tT][yY]:(\\S+)";
+ PeerConnectionIdp._identityPattern = new RegExp(pattern, "m");
+ pattern = "^a=[fF][iI][nN][gG][eE][rR][pP][rR][iI][nN][tT]:(\\S+) (\\S+)";
+ PeerConnectionIdp._fingerprintPattern = new RegExp(pattern, "m");
+})();
+
+PeerConnectionIdp.prototype = {
+ get enabled() {
+ return !!this._idp;
+ },
+
+ _resetAssertion() {
+ this.assertion = null;
+ this.idpLoginUrl = null;
+ },
+
+ setIdentityProvider(provider, protocol, usernameHint, peerIdentity) {
+ this._resetAssertion();
+ this.provider = provider;
+ this.protocol = protocol;
+ this.username = usernameHint;
+ this.peeridentity = peerIdentity;
+ if (this._idp) {
+ if (this._idp.isSame(provider, protocol)) {
+ return; // noop
+ }
+ this._idp.stop();
+ }
+ this._idp = new lazy.IdpSandbox(provider, protocol, this._win);
+ },
+
+ // start the IdP and do some error fixup
+ start() {
+ return this._idp.start().catch(e => {
+ throw new this._win.DOMException(e.message, "IdpError");
+ });
+ },
+
+ close() {
+ this._resetAssertion();
+ this.provider = null;
+ this.protocol = null;
+ this.username = null;
+ this.peeridentity = null;
+ if (this._idp) {
+ this._idp.stop();
+ this._idp = null;
+ }
+ },
+
+ _getFingerprintsFromSdp(sdp) {
+ let fingerprints = {};
+ let m = sdp.match(PeerConnectionIdp._fingerprintPattern);
+ while (m) {
+ fingerprints[m[0]] = { algorithm: m[1], digest: m[2] };
+ sdp = sdp.substring(m.index + m[0].length);
+ m = sdp.match(PeerConnectionIdp._fingerprintPattern);
+ }
+
+ return Object.keys(fingerprints).map(k => fingerprints[k]);
+ },
+
+ _isValidAssertion(assertion) {
+ return (
+ assertion &&
+ assertion.idp &&
+ typeof assertion.idp.domain === "string" &&
+ (!assertion.idp.protocol || typeof assertion.idp.protocol === "string") &&
+ typeof assertion.assertion === "string"
+ );
+ },
+
+ _getSessionLevelEnd(sdp) {
+ const match = sdp.match(PeerConnectionIdp._mLinePattern);
+ if (!match) {
+ return sdp.length;
+ }
+ return match.index;
+ },
+
+ _getIdentityFromSdp(sdp) {
+ // a=identity is session level
+ let idMatch;
+ const index = this._getSessionLevelEnd(sdp);
+ const sessionLevel = sdp.substring(0, index);
+ idMatch = sessionLevel.match(PeerConnectionIdp._identityPattern);
+ if (!idMatch) {
+ return undefined; // undefined === no identity
+ }
+
+ let assertion;
+ try {
+ assertion = JSON.parse(atob(idMatch[1]));
+ } catch (e) {
+ throw new this._win.DOMException(
+ "invalid identity assertion: " + e,
+ "InvalidSessionDescriptionError"
+ );
+ }
+ if (!this._isValidAssertion(assertion)) {
+ throw new this._win.DOMException(
+ "assertion missing idp/idp.domain/assertion",
+ "InvalidSessionDescriptionError"
+ );
+ }
+ return assertion;
+ },
+
+ /**
+ * Verifies the a=identity line the given SDP contains, if any.
+ * If the verification succeeds callback is called with the message from the
+ * IdP proxy as parameter, else (verification failed OR no a=identity line in
+ * SDP at all) null is passed to callback.
+ *
+ * Note that this only verifies that the SDP is coherent. We still rely on
+ * the fact that the RTCPeerConnection won't connect to a peer if the
+ * fingerprint of the certificate they offer doesn't appear in the SDP.
+ */
+ verifyIdentityFromSDP(sdp, origin) {
+ let identity = this._getIdentityFromSdp(sdp);
+ let fingerprints = this._getFingerprintsFromSdp(sdp);
+ if (!identity || fingerprints.length <= 0) {
+ return this._win.Promise.resolve(); // undefined result = no identity
+ }
+
+ this.setIdentityProvider(identity.idp.domain, identity.idp.protocol);
+ return this._verifyIdentity(identity.assertion, fingerprints, origin);
+ },
+
+ /**
+ * Checks that the name in the identity provided by the IdP is OK.
+ *
+ * @param name (string) the name to validate
+ * @throws if the name isn't valid
+ */
+ _validateName(name) {
+ let error = msg => {
+ throw new this._win.DOMException(
+ "assertion name error: " + msg,
+ "IdpError"
+ );
+ };
+
+ if (typeof name !== "string") {
+ error("name not a string");
+ }
+ let atIdx = name.indexOf("@");
+ if (atIdx <= 0) {
+ error("missing authority in name from IdP");
+ }
+
+ // no third party assertions... for now
+ let tail = name.substring(atIdx + 1);
+
+ // strip the port number, if present
+ let provider = this.provider;
+ let providerPortIdx = provider.indexOf(":");
+ if (providerPortIdx > 0) {
+ provider = provider.substring(0, providerPortIdx);
+ }
+ let idnService = Cc["@mozilla.org/network/idn-service;1"].getService(
+ Ci.nsIIDNService
+ );
+ if (
+ idnService.convertUTF8toACE(tail) !==
+ idnService.convertUTF8toACE(provider)
+ ) {
+ error('name "' + name + '" doesn\'t match IdP: "' + this.provider + '"');
+ }
+ },
+
+ /**
+ * Check the validation response. We are very defensive here when handling
+ * the message from the IdP proxy. That way, broken IdPs aren't likely to
+ * cause catastrophic damage.
+ */
+ _checkValidation(validation, sdpFingerprints) {
+ let error = msg => {
+ throw new this._win.DOMException(
+ "IdP validation error: " + msg,
+ "IdpError"
+ );
+ };
+
+ if (!this.provider) {
+ error("IdP closed");
+ }
+
+ if (
+ typeof validation !== "object" ||
+ typeof validation.contents !== "string" ||
+ typeof validation.identity !== "string"
+ ) {
+ error("no payload in validation response");
+ }
+
+ let fingerprints;
+ try {
+ fingerprints = JSON.parse(validation.contents).fingerprint;
+ } catch (e) {
+ error("invalid JSON");
+ }
+
+ let isFingerprint = f =>
+ typeof f.digest === "string" && typeof f.algorithm === "string";
+ if (!Array.isArray(fingerprints) || !fingerprints.every(isFingerprint)) {
+ error(
+ "fingerprints must be an array of objects" +
+ " with digest and algorithm attributes"
+ );
+ }
+
+ // everything in `innerSet` is found in `outerSet`
+ let isSubsetOf = (outerSet, innerSet, comparator) => {
+ return innerSet.every(i => {
+ return outerSet.some(o => comparator(i, o));
+ });
+ };
+ let compareFingerprints = (a, b) => {
+ return a.digest === b.digest && a.algorithm === b.algorithm;
+ };
+ if (!isSubsetOf(fingerprints, sdpFingerprints, compareFingerprints)) {
+ error("the fingerprints must be covered by the assertion");
+ }
+ this._validateName(validation.identity);
+ return validation;
+ },
+
+ /**
+ * Asks the IdP proxy to verify an identity assertion.
+ */
+ _verifyIdentity(assertion, fingerprints, origin) {
+ let p = this.start()
+ .then(idp =>
+ this._wrapCrossCompartmentPromise(
+ idp.validateAssertion(assertion, origin)
+ )
+ )
+ .then(validation => this._checkValidation(validation, fingerprints));
+
+ return this._applyTimeout(p);
+ },
+
+ /**
+ * Enriches the given SDP with an `a=identity` line. getIdentityAssertion()
+ * must have already run successfully, otherwise this does nothing to the sdp.
+ */
+ addIdentityAttribute(sdp) {
+ if (!this.assertion) {
+ return sdp;
+ }
+
+ const index = this._getSessionLevelEnd(sdp);
+ return (
+ sdp.substring(0, index) +
+ "a=identity:" +
+ this.assertion +
+ "\r\n" +
+ sdp.substring(index)
+ );
+ },
+
+ /**
+ * Asks the IdP proxy for an identity assertion. Don't call this unless you
+ * have checked .enabled, or you really like exceptions. Also, don't call
+ * this when another call is still running, because it's not certain which
+ * call will finish first and the final state will be similarly uncertain.
+ */
+ getIdentityAssertion(fingerprint, origin) {
+ if (!this.enabled) {
+ throw new this._win.DOMException(
+ "no IdP set, call setIdentityProvider() to set one",
+ "InvalidStateError"
+ );
+ }
+
+ let [algorithm, digest] = fingerprint.split(" ", 2);
+ let content = {
+ fingerprint: [
+ {
+ algorithm,
+ digest,
+ },
+ ],
+ };
+
+ this._resetAssertion();
+ let p = this.start()
+ .then(idp => {
+ let options = {
+ protocol: this.protocol,
+ usernameHint: this.username,
+ peerIdentity: this.peeridentity,
+ };
+ return this._wrapCrossCompartmentPromise(
+ idp.generateAssertion(JSON.stringify(content), origin, options)
+ );
+ })
+ .then(assertion => {
+ if (!this._isValidAssertion(assertion)) {
+ throw new this._win.DOMException(
+ "IdP generated invalid assertion",
+ "IdpError"
+ );
+ }
+ // save the base64+JSON assertion, since that is all that is used
+ this.assertion = btoa(JSON.stringify(assertion));
+ return this.assertion;
+ });
+
+ return this._applyTimeout(p);
+ },
+
+ /**
+ * Promises generated by the sandbox need to be very carefully treated so that
+ * they can chain into promises in the `this._win` compartment. Results need
+ * to be cloned across; errors need to be converted.
+ */
+ _wrapCrossCompartmentPromise(sandboxPromise) {
+ return new this._win.Promise((resolve, reject) => {
+ sandboxPromise.then(
+ result => resolve(Cu.cloneInto(result, this._win)),
+ e => {
+ let message = "" + (e.message || JSON.stringify(e) || "IdP error");
+ if (e.name === "IdpLoginError") {
+ if (typeof e.loginUrl === "string") {
+ this.idpLoginUrl = e.loginUrl;
+ }
+ reject(new this._win.DOMException(message, "IdpLoginError"));
+ } else {
+ reject(new this._win.DOMException(message, "IdpError"));
+ }
+ }
+ );
+ });
+ },
+
+ /**
+ * Wraps a promise, adding a timeout guard on it so that it can't take longer
+ * than the specified time. Returns a promise that rejects if the timeout
+ * elapses before `p` resolves.
+ */
+ _applyTimeout(p) {
+ let timeout = new this._win.Promise(r =>
+ this._win.setTimeout(r, this._timeout)
+ ).then(() => {
+ throw new this._win.DOMException("IdP timed out", "IdpError");
+ });
+ return this._win.Promise.race([timeout, p]);
+ },
+};
diff --git a/dom/media/PrincipalChangeObserver.h b/dom/media/PrincipalChangeObserver.h
new file mode 100644
index 0000000000..e59c7b7492
--- /dev/null
+++ b/dom/media/PrincipalChangeObserver.h
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_PRINCIPALCHANGEOBSERVER_H_
+#define MOZILLA_PRINCIPALCHANGEOBSERVER_H_
+
+namespace mozilla::dom {
+
+/**
+ * A PrincipalChangeObserver for any type, but originating from DOMMediaStream,
+ * then expanded to MediaStreamTrack.
+ *
+ * Used to learn about dynamic changes to an object's principal.
+ * Operations relating to these observers must be confined to the main thread.
+ */
+template <typename T>
+class PrincipalChangeObserver {
+ public:
+ virtual void PrincipalChanged(T* aArg) = 0;
+};
+
+} // namespace mozilla::dom
+
+#endif /* MOZILLA_PRINCIPALCHANGEOBSERVER_H_ */
diff --git a/dom/media/PrincipalHandle.h b/dom/media/PrincipalHandle.h
new file mode 100644
index 0000000000..b4539c5e59
--- /dev/null
+++ b/dom/media/PrincipalHandle.h
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_PRINCIPALHANDLE_H_
+#define DOM_MEDIA_PRINCIPALHANDLE_H_
+
+#include "nsIPrincipal.h"
+#include "nsProxyRelease.h"
+
+namespace mozilla {
+/**
+ * The level of privacy of a principal as considered by RTCPeerConnection.
+ */
+enum class PrincipalPrivacy : uint8_t { Private, NonPrivate };
+
+/**
+ * We pass the principal through the MediaTrackGraph by wrapping it in a thread
+ * safe nsMainThreadPtrHandle, since it cannot be used directly off the main
+ * thread. We can compare two PrincipalHandles to each other on any thread, but
+ * they can only be created and converted back to nsIPrincipal* on main thread.
+ */
+typedef nsMainThreadPtrHandle<nsIPrincipal> PrincipalHandle;
+
+inline PrincipalHandle MakePrincipalHandle(nsIPrincipal* aPrincipal) {
+ RefPtr<nsMainThreadPtrHolder<nsIPrincipal>> holder =
+ new nsMainThreadPtrHolder<nsIPrincipal>(
+ "MakePrincipalHandle::nsIPrincipal", aPrincipal);
+ return PrincipalHandle(holder);
+}
+
+#define PRINCIPAL_HANDLE_NONE nullptr
+
+inline nsIPrincipal* GetPrincipalFromHandle(
+ const PrincipalHandle& aPrincipalHandle) {
+ MOZ_ASSERT(NS_IsMainThread());
+ return aPrincipalHandle.get();
+}
+
+inline bool PrincipalHandleMatches(const PrincipalHandle& aPrincipalHandle,
+ nsIPrincipal* aOther) {
+ if (!aOther) {
+ return false;
+ }
+
+ nsIPrincipal* principal = GetPrincipalFromHandle(aPrincipalHandle);
+ if (!principal) {
+ return false;
+ }
+
+ bool result;
+ if (NS_FAILED(principal->Equals(aOther, &result))) {
+ NS_ERROR("Principal check failed");
+ return false;
+ }
+
+ return result;
+}
+} // namespace mozilla
+
+#endif // DOM_MEDIA_PRINCIPALHANDLE_H_
diff --git a/dom/media/QueueObject.cpp b/dom/media/QueueObject.cpp
new file mode 100644
index 0000000000..f10cf4ce82
--- /dev/null
+++ b/dom/media/QueueObject.cpp
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "QueueObject.h"
+#include "mozilla/AbstractThread.h"
+#include "mozilla/TaskQueue.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+
+QueueObject::QueueObject(RefPtr<AbstractThread> aThread) : mThread(aThread) {}
+
+QueueObject::~QueueObject() = default;
+
+void QueueObject::Dispatch(nsIRunnable* aRunnable) {
+ Dispatch(do_AddRef(aRunnable));
+}
+
+void QueueObject::Dispatch(already_AddRefed<nsIRunnable> aRunnable) {
+ mThread->Dispatch(std::move(aRunnable));
+}
+
+bool QueueObject::OnThread() { return mThread->IsCurrentThreadIn(); }
+
+AbstractThread* QueueObject::Thread() { return mThread; }
+} // namespace mozilla
diff --git a/dom/media/QueueObject.h b/dom/media/QueueObject.h
new file mode 100644
index 0000000000..ab8b3967ec
--- /dev/null
+++ b/dom/media/QueueObject.h
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_QUEUE_OBJECT_H
+#define MOZILLA_QUEUE_OBJECT_H
+
+#include "mozilla/RefPtr.h"
+#include "nsIRunnable.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+
+class AbstractThread;
+
+class QueueObject {
+ public:
+ explicit QueueObject(RefPtr<AbstractThread> aThread);
+ ~QueueObject();
+ void Dispatch(nsIRunnable* aRunnable);
+ void Dispatch(already_AddRefed<nsIRunnable> aRunnable);
+ bool OnThread();
+ AbstractThread* Thread();
+
+ private:
+ RefPtr<AbstractThread> mThread;
+};
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/ReaderProxy.cpp b/dom/media/ReaderProxy.cpp
new file mode 100644
index 0000000000..fcdcd3376c
--- /dev/null
+++ b/dom/media/ReaderProxy.cpp
@@ -0,0 +1,216 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/CDMProxy.h"
+#include "mozilla/MozPromise.h"
+#include "MediaFormatReader.h"
+#include "ReaderProxy.h"
+#include "TimeUnits.h"
+
+namespace mozilla {
+
+ReaderProxy::ReaderProxy(AbstractThread* aOwnerThread,
+ MediaFormatReader* aReader)
+ : mOwnerThread(aOwnerThread),
+ mReader(aReader),
+ mWatchManager(this, aReader->OwnerThread()),
+ mDuration(aReader->OwnerThread(), media::NullableTimeUnit(),
+ "ReaderProxy::mDuration (Mirror)") {
+ // Must support either heuristic buffering or WaitForData().
+ MOZ_ASSERT(mReader->UseBufferingHeuristics() ||
+ mReader->IsWaitForDataSupported());
+}
+
+ReaderProxy::~ReaderProxy() = default;
+
+media::TimeUnit ReaderProxy::StartTime() const {
+ MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
+ return mStartTime.ref();
+}
+
+RefPtr<ReaderProxy::MetadataPromise> ReaderProxy::ReadMetadata() {
+ MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
+ MOZ_ASSERT(!mShutdown);
+ return InvokeAsync(mReader->OwnerThread(), mReader.get(), __func__,
+ &MediaFormatReader::AsyncReadMetadata)
+ ->Then(mOwnerThread, __func__, this, &ReaderProxy::OnMetadataRead,
+ &ReaderProxy::OnMetadataNotRead);
+}
+
+RefPtr<ReaderProxy::AudioDataPromise> ReaderProxy::OnAudioDataRequestCompleted(
+ RefPtr<AudioData> aAudio) {
+ MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
+
+ if (aAudio->AdjustForStartTime(StartTime())) {
+ return AudioDataPromise::CreateAndResolve(aAudio.forget(), __func__);
+ }
+ return AudioDataPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_OVERFLOW_ERR,
+ __func__);
+}
+
+RefPtr<ReaderProxy::AudioDataPromise> ReaderProxy::OnAudioDataRequestFailed(
+ const MediaResult& aError) {
+ MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
+ return AudioDataPromise::CreateAndReject(aError, __func__);
+}
+
+RefPtr<ReaderProxy::AudioDataPromise> ReaderProxy::RequestAudioData() {
+ MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
+ MOZ_ASSERT(!mShutdown);
+
+ return InvokeAsync(mReader->OwnerThread(), mReader.get(), __func__,
+ &MediaFormatReader::RequestAudioData)
+ ->Then(mOwnerThread, __func__, this,
+ &ReaderProxy::OnAudioDataRequestCompleted,
+ &ReaderProxy::OnAudioDataRequestFailed);
+}
+
+RefPtr<ReaderProxy::VideoDataPromise> ReaderProxy::RequestVideoData(
+ const media::TimeUnit& aTimeThreshold, bool aRequestNextVideoKeyFrame) {
+ MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
+ MOZ_ASSERT(!mShutdown);
+
+ const auto threshold = aTimeThreshold > media::TimeUnit::Zero()
+ ? aTimeThreshold + StartTime()
+ : aTimeThreshold;
+
+ auto startTime = StartTime();
+ return InvokeAsync(mReader->OwnerThread(), mReader.get(), __func__,
+ &MediaFormatReader::RequestVideoData, threshold,
+ aRequestNextVideoKeyFrame)
+ ->Then(
+ mOwnerThread, __func__,
+ [startTime](RefPtr<VideoData> aVideo) {
+ return aVideo->AdjustForStartTime(startTime)
+ ? VideoDataPromise::CreateAndResolve(aVideo.forget(),
+ __func__)
+ : VideoDataPromise::CreateAndReject(
+ NS_ERROR_DOM_MEDIA_OVERFLOW_ERR, __func__);
+ },
+ [](const MediaResult& aError) {
+ return VideoDataPromise::CreateAndReject(aError, __func__);
+ });
+}
+
+RefPtr<ReaderProxy::SeekPromise> ReaderProxy::Seek(const SeekTarget& aTarget) {
+ MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
+ return SeekInternal(aTarget);
+}
+
+RefPtr<ReaderProxy::SeekPromise> ReaderProxy::SeekInternal(
+ const SeekTarget& aTarget) {
+ MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
+ SeekTarget adjustedTarget = aTarget;
+ adjustedTarget.SetTime(adjustedTarget.GetTime() + StartTime());
+ return InvokeAsync(mReader->OwnerThread(), mReader.get(), __func__,
+ &MediaFormatReader::Seek, std::move(adjustedTarget));
+}
+
+RefPtr<ReaderProxy::WaitForDataPromise> ReaderProxy::WaitForData(
+ MediaData::Type aType) {
+ MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
+ MOZ_ASSERT(mReader->IsWaitForDataSupported());
+ return InvokeAsync(mReader->OwnerThread(), mReader.get(), __func__,
+ &MediaFormatReader::WaitForData, aType);
+}
+
+void ReaderProxy::ReleaseResources() {
+ MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
+ nsCOMPtr<nsIRunnable> r =
+ NewRunnableMethod("MediaFormatReader::ReleaseResources", mReader,
+ &MediaFormatReader::ReleaseResources);
+ nsresult rv = mReader->OwnerThread()->Dispatch(r.forget());
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+}
+
+void ReaderProxy::ResetDecode(TrackSet aTracks) {
+ MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
+ nsCOMPtr<nsIRunnable> r =
+ NewRunnableMethod<TrackSet>("MediaFormatReader::ResetDecode", mReader,
+ &MediaFormatReader::ResetDecode, aTracks);
+ nsresult rv = mReader->OwnerThread()->Dispatch(r.forget());
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+}
+
+RefPtr<ShutdownPromise> ReaderProxy::Shutdown() {
+ MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
+ mShutdown = true;
+ RefPtr<ReaderProxy> self = this;
+ return InvokeAsync(mReader->OwnerThread(), __func__, [self]() {
+ self->mDuration.DisconnectIfConnected();
+ self->mWatchManager.Shutdown();
+ return self->mReader->Shutdown();
+ });
+}
+
+RefPtr<ReaderProxy::MetadataPromise> ReaderProxy::OnMetadataRead(
+ MetadataHolder&& aMetadata) {
+ MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
+ if (mShutdown) {
+ return MetadataPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_ABORT_ERR,
+ __func__);
+ }
+
+ if (mStartTime.isNothing()) {
+ mStartTime.emplace(aMetadata.mInfo->mStartTime);
+ }
+ return MetadataPromise::CreateAndResolve(std::move(aMetadata), __func__);
+}
+
+RefPtr<ReaderProxy::MetadataPromise> ReaderProxy::OnMetadataNotRead(
+ const MediaResult& aError) {
+ return MetadataPromise::CreateAndReject(aError, __func__);
+}
+
+void ReaderProxy::SetVideoBlankDecode(bool aIsBlankDecode) {
+ MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
+ nsCOMPtr<nsIRunnable> r = NewRunnableMethod<bool>(
+ "MediaFormatReader::SetVideoNullDecode", mReader,
+ &MediaFormatReader::SetVideoNullDecode, aIsBlankDecode);
+ nsresult rv = mReader->OwnerThread()->Dispatch(r.forget());
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+}
+
+void ReaderProxy::UpdateDuration() {
+ MOZ_ASSERT(mReader->OwnerThread()->IsCurrentThreadIn());
+ mReader->UpdateDuration(mDuration.Ref().ref());
+}
+
+void ReaderProxy::SetCanonicalDuration(
+ AbstractCanonical<media::NullableTimeUnit>* aCanonical) {
+ using DurationT = AbstractCanonical<media::NullableTimeUnit>;
+ RefPtr<ReaderProxy> self = this;
+ RefPtr<DurationT> canonical = aCanonical;
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ "ReaderProxy::SetCanonicalDuration", [this, self, canonical]() {
+ mDuration.Connect(canonical);
+ mWatchManager.Watch(mDuration, &ReaderProxy::UpdateDuration);
+ });
+ nsresult rv = mReader->OwnerThread()->Dispatch(r.forget());
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+}
+
+void ReaderProxy::UpdateMediaEngineId(uint64_t aMediaEngineId) {
+ MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
+ nsCOMPtr<nsIRunnable> r = NewRunnableMethod<uint64_t>(
+ "MediaFormatReader::UpdateMediaEngineId", mReader,
+ &MediaFormatReader::UpdateMediaEngineId, aMediaEngineId);
+ nsresult rv = mReader->OwnerThread()->Dispatch(r.forget());
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+}
+
+RefPtr<SetCDMPromise> ReaderProxy::SetCDMProxy(CDMProxy* aProxy) {
+ return InvokeAsync<RefPtr<CDMProxy>>(mReader->OwnerThread(), mReader.get(),
+ __func__,
+ &MediaFormatReader::SetCDMProxy, aProxy);
+}
+
+} // namespace mozilla
diff --git a/dom/media/ReaderProxy.h b/dom/media/ReaderProxy.h
new file mode 100644
index 0000000000..2f578e8b0c
--- /dev/null
+++ b/dom/media/ReaderProxy.h
@@ -0,0 +1,120 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef ReaderProxy_h_
+#define ReaderProxy_h_
+
+#include "mozilla/AbstractThread.h"
+#include "mozilla/RefPtr.h"
+#include "nsISupportsImpl.h"
+
+#include "MediaEventSource.h"
+#include "MediaFormatReader.h"
+#include "MediaPromiseDefs.h"
+
+namespace mozilla {
+
+/**
+ * A wrapper around MediaFormatReader to offset the timestamps of Audio/Video
+ * samples by the start time to ensure MDSM can always assume zero start time.
+ * It also adjusts the seek target passed to Seek() to ensure correct seek time
+ * is passed to the underlying reader.
+ */
+class ReaderProxy {
+ using MetadataPromise = MediaFormatReader::MetadataPromise;
+ using AudioDataPromise = MediaFormatReader::AudioDataPromise;
+ using VideoDataPromise = MediaFormatReader::VideoDataPromise;
+ using SeekPromise = MediaFormatReader::SeekPromise;
+ using WaitForDataPromise = MediaFormatReader::WaitForDataPromise;
+ using TrackSet = MediaFormatReader::TrackSet;
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ReaderProxy);
+
+ public:
+ ReaderProxy(AbstractThread* aOwnerThread, MediaFormatReader* aReader);
+
+ media::TimeUnit StartTime() const;
+ RefPtr<MetadataPromise> ReadMetadata();
+
+ RefPtr<AudioDataPromise> RequestAudioData();
+
+ RefPtr<VideoDataPromise> RequestVideoData(
+ const media::TimeUnit& aTimeThreshold, bool aRequestNextVideoKeyFrame);
+
+ RefPtr<WaitForDataPromise> WaitForData(MediaData::Type aType);
+
+ RefPtr<SeekPromise> Seek(const SeekTarget& aTarget);
+ RefPtr<ShutdownPromise> Shutdown();
+
+ void ReleaseResources();
+ void ResetDecode(TrackSet aTracks);
+
+ nsresult Init() { return mReader->Init(); }
+ bool UseBufferingHeuristics() const {
+ return mReader->UseBufferingHeuristics();
+ }
+
+ bool VideoIsHardwareAccelerated() const {
+ return mReader->VideoIsHardwareAccelerated();
+ }
+ TimedMetadataEventSource& TimedMetadataEvent() {
+ return mReader->TimedMetadataEvent();
+ }
+ MediaEventSource<void>& OnMediaNotSeekable() {
+ return mReader->OnMediaNotSeekable();
+ }
+ MediaEventProducer<VideoInfo, AudioInfo>& OnTrackInfoUpdatedEvent() {
+ return mReader->OnTrackInfoUpdatedEvent();
+ }
+ size_t SizeOfAudioQueueInFrames() const {
+ return mReader->SizeOfAudioQueueInFrames();
+ }
+ size_t SizeOfVideoQueueInFrames() const {
+ return mReader->SizeOfVideoQueueInFrames();
+ }
+ void ReadUpdatedMetadata(MediaInfo* aInfo) {
+ mReader->ReadUpdatedMetadata(aInfo);
+ }
+ AbstractCanonical<media::TimeIntervals>* CanonicalBuffered() {
+ return mReader->CanonicalBuffered();
+ }
+
+ RefPtr<SetCDMPromise> SetCDMProxy(CDMProxy* aProxy);
+
+ void SetVideoBlankDecode(bool aIsBlankDecode);
+
+ void SetCanonicalDuration(
+ AbstractCanonical<media::NullableTimeUnit>* aCanonical);
+
+ void UpdateMediaEngineId(uint64_t aMediaEngineId);
+
+ private:
+ ~ReaderProxy();
+ RefPtr<MetadataPromise> OnMetadataRead(MetadataHolder&& aMetadata);
+ RefPtr<MetadataPromise> OnMetadataNotRead(const MediaResult& aError);
+ void UpdateDuration();
+ RefPtr<SeekPromise> SeekInternal(const SeekTarget& aTarget);
+
+ RefPtr<ReaderProxy::AudioDataPromise> OnAudioDataRequestCompleted(
+ RefPtr<AudioData> aAudio);
+ RefPtr<ReaderProxy::AudioDataPromise> OnAudioDataRequestFailed(
+ const MediaResult& aError);
+
+ const RefPtr<AbstractThread> mOwnerThread;
+ const RefPtr<MediaFormatReader> mReader;
+
+ bool mShutdown = false;
+ Maybe<media::TimeUnit> mStartTime;
+
+ // State-watching manager.
+ WatchManager<ReaderProxy> mWatchManager;
+
+ // Duration, mirrored from the state machine task queue.
+ Mirror<media::NullableTimeUnit> mDuration;
+};
+
+} // namespace mozilla
+
+#endif // ReaderProxy_h_
diff --git a/dom/media/SeekJob.cpp b/dom/media/SeekJob.cpp
new file mode 100644
index 0000000000..93911638ce
--- /dev/null
+++ b/dom/media/SeekJob.cpp
@@ -0,0 +1,31 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SeekJob.h"
+
+namespace mozilla {
+
+SeekJob::~SeekJob() {
+ MOZ_DIAGNOSTIC_ASSERT(mTarget.isNothing());
+ MOZ_DIAGNOSTIC_ASSERT(mPromise.IsEmpty());
+}
+
+bool SeekJob::Exists() const {
+ MOZ_ASSERT(mTarget.isSome() == !mPromise.IsEmpty());
+ return mTarget.isSome();
+}
+
+void SeekJob::Resolve(const char* aCallSite) {
+ mPromise.Resolve(true, aCallSite);
+ mTarget.reset();
+}
+
+void SeekJob::RejectIfExists(const char* aCallSite) {
+ mTarget.reset();
+ mPromise.RejectIfExists(true, aCallSite);
+}
+
+} // namespace mozilla
diff --git a/dom/media/SeekJob.h b/dom/media/SeekJob.h
new file mode 100644
index 0000000000..1bdd6913f3
--- /dev/null
+++ b/dom/media/SeekJob.h
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef SEEK_JOB_H
+#define SEEK_JOB_H
+
+#include "mozilla/MozPromise.h"
+#include "MediaDecoder.h"
+#include "SeekTarget.h"
+
+namespace mozilla {
+
+struct SeekJob {
+ SeekJob() = default;
+ SeekJob(SeekJob&& aOther) = default;
+ SeekJob& operator=(SeekJob&& aOther) = default;
+ ~SeekJob();
+
+ bool Exists() const;
+ void Resolve(const char* aCallSite);
+ void RejectIfExists(const char* aCallSite);
+
+ Maybe<SeekTarget> mTarget;
+ MozPromiseHolder<MediaDecoder::SeekPromise> mPromise;
+};
+
+} // namespace mozilla
+
+#endif /* SEEK_JOB_H */
diff --git a/dom/media/SeekTarget.h b/dom/media/SeekTarget.h
new file mode 100644
index 0000000000..7fffea8663
--- /dev/null
+++ b/dom/media/SeekTarget.h
@@ -0,0 +1,90 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef SEEK_TARGET_H
+#define SEEK_TARGET_H
+
+#include "TimeUnits.h"
+
+namespace mozilla {
+
+enum class MediaDecoderEventVisibility : int8_t { Observable, Suppressed };
+
+// Stores the seek target. It includes (1) the time to seek to (2) type of seek
+// (accurate, previous keyframe, next keyframe) (3) the type of track needs to
+// performs seeking.
+struct SeekTarget {
+ enum Type {
+ Invalid,
+ PrevSyncPoint,
+ Accurate,
+ NextFrame,
+ };
+ enum Track {
+ All,
+ AudioOnly,
+ VideoOnly,
+ };
+ SeekTarget()
+ : mTime(media::TimeUnit::Invalid()),
+ mType(SeekTarget::Invalid),
+ mTargetTrack(Track::All) {}
+ SeekTarget(const media::TimeUnit& aTime, Type aType,
+ Track aTrack = Track::All)
+ : mTime(aTime), mType(aType), mTargetTrack(aTrack) {
+ MOZ_ASSERT(mTime.IsValid());
+ }
+ SeekTarget(const SeekTarget& aOther)
+ : mTime(aOther.mTime),
+ mType(aOther.mType),
+ mTargetTrack(aOther.mTargetTrack) {
+ MOZ_ASSERT(mTime.IsValid());
+ }
+ media::TimeUnit GetTime() const {
+ MOZ_ASSERT(mTime.IsValid(), "Invalid SeekTarget");
+ return mTime;
+ }
+ void SetTime(const media::TimeUnit& aTime) {
+ MOZ_ASSERT(aTime.IsValid(), "Invalid SeekTarget destination");
+ mTime = aTime;
+ }
+ void SetType(Type aType) { mType = aType; }
+ bool IsFast() const { return mType == SeekTarget::Type::PrevSyncPoint; }
+ bool IsAccurate() const { return mType == SeekTarget::Type::Accurate; }
+ bool IsNextFrame() const { return mType == SeekTarget::Type::NextFrame; }
+ bool IsVideoOnly() const { return mTargetTrack == Track::VideoOnly; }
+ bool IsAudioOnly() const { return mTargetTrack == Track::AudioOnly; }
+ bool IsAllTracks() const { return mTargetTrack == Track::All; }
+ Type GetType() const { return mType; }
+ Track GetTrack() const { return mTargetTrack; }
+
+ static const char* TrackToStr(Track aTrack) {
+ switch (aTrack) {
+ case Track::All:
+ return "all tracks";
+ case Track::AudioOnly:
+ return "audio only";
+ case Track::VideoOnly:
+ return "video only";
+ default:
+ MOZ_ASSERT_UNREACHABLE("Not defined track!");
+ return "none";
+ }
+ }
+
+ private:
+ // Seek target time.
+ media::TimeUnit mTime;
+ // Whether we should seek "Fast", or "Accurate".
+ // "Fast" seeks to the seek point preceding mTime, whereas
+ // "Accurate" seeks as close as possible to mTime.
+ Type mType;
+ Track mTargetTrack;
+};
+
+} // namespace mozilla
+
+#endif /* SEEK_TARGET_H */
diff --git a/dom/media/SelfRef.h b/dom/media/SelfRef.h
new file mode 100644
index 0000000000..40410abb8f
--- /dev/null
+++ b/dom/media/SelfRef.h
@@ -0,0 +1,46 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef SELF_REF_H
+#define SELF_REF_H
+
+#include "mozilla/Attributes.h"
+
+namespace mozilla {
+
+template <class T>
+class SelfReference {
+ public:
+ SelfReference() : mHeld(false) {}
+ ~SelfReference() {
+ NS_ASSERTION(!mHeld, "Forgot to drop the self reference?");
+ }
+
+ void Take(T* t) {
+ if (!mHeld) {
+ mHeld = true;
+ t->AddRef();
+ }
+ }
+ void Drop(T* t) {
+ if (mHeld) {
+ mHeld = false;
+ t->Release();
+ }
+ }
+
+ MOZ_IMPLICIT operator bool() const { return mHeld; }
+
+ SelfReference(const SelfReference& aOther) = delete;
+ void operator=(const SelfReference& aOther) = delete;
+
+ private:
+ bool mHeld;
+};
+
+} // namespace mozilla
+
+#endif // SELF_REF_H
diff --git a/dom/media/SharedBuffer.h b/dom/media/SharedBuffer.h
new file mode 100644
index 0000000000..d8fa87f9ed
--- /dev/null
+++ b/dom/media/SharedBuffer.h
@@ -0,0 +1,116 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_SHAREDBUFFER_H_
+#define MOZILLA_SHAREDBUFFER_H_
+
+#include "mozilla/CheckedInt.h"
+#include "mozilla/mozalloc.h"
+#include "mozilla/MemoryReporting.h"
+#include "nsISupportsImpl.h"
+
+namespace mozilla {
+
+class AudioBlockBuffer;
+class ThreadSharedFloatArrayBufferList;
+
+/**
+ * Base class for objects with a thread-safe refcount and a virtual
+ * destructor.
+ */
+class ThreadSharedObject {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ThreadSharedObject)
+
+ bool IsShared() { return mRefCnt.get() > 1; }
+
+ virtual AudioBlockBuffer* AsAudioBlockBuffer() { return nullptr; };
+ virtual ThreadSharedFloatArrayBufferList*
+ AsThreadSharedFloatArrayBufferList() {
+ return nullptr;
+ };
+
+ virtual size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ return 0;
+ }
+
+ virtual size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ protected:
+ // Protected destructor, to discourage deletion outside of Release():
+ virtual ~ThreadSharedObject() = default;
+};
+
+/**
+ * Heap-allocated chunk of arbitrary data with threadsafe refcounting.
+ * Typically you would allocate one of these, fill it in, and then treat it as
+ * immutable while it's shared.
+ * This only guarantees 4-byte alignment of the data. For alignment we simply
+ * assume that the memory from malloc is at least 4-byte aligned and the
+ * refcount's size is large enough that SharedBuffer's size is divisible by 4.
+ */
+class SharedBuffer : public ThreadSharedObject {
+ public:
+ void* Data() { return this + 1; }
+
+ // Ensure that the caller has a CheckedInt. We have to take one by
+ // non-const reference to do that, because if we take one by const
+ // reference or value or rvalue reference the implicit constructor on
+ // CheckedInt will helpfully synthesize one for us at the callsite
+ // even if the caller passes a raw size_t.
+ static already_AddRefed<SharedBuffer> Create(CheckedInt<size_t>& aSize,
+ const fallible_t&) {
+ CheckedInt<size_t> allocSize = AllocSize(aSize, fallible);
+ if (!allocSize.isValid()) {
+ return nullptr;
+ }
+ void* m = operator new(allocSize.value(), fallible);
+ if (!m) {
+ return nullptr;
+ }
+ RefPtr<SharedBuffer> p = new (m) SharedBuffer();
+ return p.forget();
+ }
+
+ static already_AddRefed<SharedBuffer> Create(CheckedInt<size_t>& aSize) {
+ void* m = operator new(AllocSize(aSize));
+ RefPtr<SharedBuffer> p = new (m) SharedBuffer();
+ return p.forget();
+ }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ private:
+ static CheckedInt<size_t> AllocSize(CheckedInt<size_t> aDataSize,
+ const fallible_t&) {
+ CheckedInt<size_t> size = sizeof(SharedBuffer);
+ size += aDataSize;
+ return size;
+ }
+
+ static size_t AllocSize(CheckedInt<size_t> aDataSize) {
+ CheckedInt<size_t> size = AllocSize(aDataSize, fallible);
+ if (!size.isValid()) {
+ MOZ_CRASH();
+ }
+ return size.value();
+ }
+
+ SharedBuffer() {
+ NS_ASSERTION(
+ (reinterpret_cast<char*>(this + 1) - reinterpret_cast<char*>(this)) %
+ 4 ==
+ 0,
+ "SharedBuffers should be at least 4-byte aligned");
+ }
+};
+
+} // namespace mozilla
+
+#endif /* MOZILLA_SHAREDBUFFER_H_ */
diff --git a/dom/media/TimeUnits.cpp b/dom/media/TimeUnits.cpp
new file mode 100644
index 0000000000..346b3c48eb
--- /dev/null
+++ b/dom/media/TimeUnits.cpp
@@ -0,0 +1,430 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <cstdint>
+#include <cmath>
+#include <inttypes.h>
+#include <limits>
+#include <type_traits>
+
+#include "TimeUnits.h"
+#include "Intervals.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/FloatingPoint.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/IntegerPrintfMacros.h"
+#include "nsDebug.h"
+#include "nsPrintfCString.h"
+#include "nsStringFwd.h"
+
+namespace mozilla::media {
+class TimeIntervals;
+} // namespace mozilla::media
+
+namespace mozilla {
+
+namespace media {
+
+TimeUnit TimeUnit::FromSeconds(double aValue, int64_t aBase) {
+ MOZ_ASSERT(!std::isnan(aValue));
+ MOZ_ASSERT(aBase > 0);
+
+ if (std::isinf(aValue)) {
+ return aValue > 0 ? FromInfinity() : FromNegativeInfinity();
+ }
+ // Warn that a particular value won't be able to be roundtrip at the same
+ // base -- we can keep this for some time until we're confident this is
+ // stable.
+ double inBase = aValue * static_cast<double>(aBase);
+ if (std::abs(inBase) >
+ static_cast<double>(std::numeric_limits<int64_t>::max())) {
+ NS_WARNING(
+ nsPrintfCString("Warning: base %" PRId64
+ " is too high to represent %lfs, returning Infinity.",
+ aBase, aValue)
+ .get());
+ if (inBase > 0) {
+ return TimeUnit::FromInfinity();
+ }
+ return TimeUnit::FromNegativeInfinity();
+ }
+
+ // inBase can large enough that it doesn't map to an exact integer, warn in
+ // this case. This happens if aBase is large, and so the loss of precision is
+ // likely small.
+ if (inBase > std::pow(2, std::numeric_limits<double>::digits) - 1) {
+ NS_WARNING(nsPrintfCString("Warning: base %" PRId64
+ " is too high to represent %lfs accurately.",
+ aBase, aValue)
+ .get());
+ }
+ return TimeUnit(static_cast<int64_t>(inBase), aBase);
+}
+
+TimeUnit TimeUnit::FromInfinity() { return TimeUnit(INT64_MAX); }
+
+TimeUnit TimeUnit::FromNegativeInfinity() { return TimeUnit(INT64_MIN); }
+
+TimeUnit TimeUnit::FromTimeDuration(const TimeDuration& aDuration) {
+ // This could be made to choose the base
+ return TimeUnit(AssertedCast<int64_t>(aDuration.ToMicroseconds()),
+ USECS_PER_S);
+}
+
+TimeUnit TimeUnit::Invalid() {
+ TimeUnit ret;
+ ret.mTicks = CheckedInt64(INT64_MAX);
+ // Force an overflow to render the CheckedInt invalid.
+ ret.mTicks += 1;
+ return ret;
+}
+
+int64_t TimeUnit::ToMilliseconds() const { return ToCommonUnit(MSECS_PER_S); }
+
+int64_t TimeUnit::ToMicroseconds() const { return ToCommonUnit(USECS_PER_S); }
+
+int64_t TimeUnit::ToNanoseconds() const { return ToCommonUnit(NSECS_PER_S); }
+
+int64_t TimeUnit::ToTicksAtRate(int64_t aRate) const {
+ // Common case
+ if (aRate == mBase) {
+ return mTicks.value();
+ }
+ // Approximation
+ return mTicks.value() * aRate / mBase;
+}
+
+double TimeUnit::ToSeconds() const {
+ if (IsPosInf()) {
+ return PositiveInfinity<double>();
+ }
+ if (IsNegInf()) {
+ return NegativeInfinity<double>();
+ }
+ return static_cast<double>(mTicks.value()) / static_cast<double>(mBase);
+}
+
+nsCString TimeUnit::ToString() const {
+ nsCString dump;
+ if (mTicks.isValid()) {
+ dump += nsPrintfCString("{%" PRId64 ",%" PRId64 "}", mTicks.value(), mBase);
+ } else {
+ dump += nsLiteralCString("{invalid}"_ns);
+ }
+ return dump;
+}
+
+TimeDuration TimeUnit::ToTimeDuration() const {
+ return TimeDuration::FromSeconds(ToSeconds());
+}
+
+bool TimeUnit::IsInfinite() const { return IsPosInf() || IsNegInf(); }
+
+bool TimeUnit::IsPositive() const { return mTicks.value() > 0; }
+
+bool TimeUnit::IsPositiveOrZero() const { return mTicks.value() >= 0; }
+
+bool TimeUnit::IsZero() const { return mTicks.value() == 0; }
+
+bool TimeUnit::IsNegative() const { return mTicks.value() < 0; }
+
+// Returns true if the fractions are equal when converted to the smallest
+// base.
+bool TimeUnit::EqualsAtLowestResolution(const TimeUnit& aOther) const {
+ MOZ_ASSERT(IsValid() && aOther.IsValid());
+ if (aOther.mBase == mBase) {
+ return mTicks == aOther.mTicks;
+ }
+ if (mBase > aOther.mBase) {
+ TimeUnit thisInBase = ToBase(aOther.mBase);
+ return thisInBase.mTicks == aOther.mTicks;
+ }
+ TimeUnit otherInBase = aOther.ToBase(mBase);
+ return otherInBase.mTicks == mTicks;
+}
+
+// Strict equality -- the fractions must be exactly equal
+bool TimeUnit::operator==(const TimeUnit& aOther) const {
+ MOZ_ASSERT(IsValid() && aOther.IsValid());
+ if (aOther.mBase == mBase) {
+ return mTicks == aOther.mTicks;
+ }
+ // debatable mathematically
+ if ((IsPosInf() && aOther.IsPosInf()) || (IsNegInf() && aOther.IsNegInf())) {
+ return true;
+ }
+ if ((IsPosInf() && !aOther.IsPosInf()) ||
+ (IsNegInf() && !aOther.IsNegInf())) {
+ return false;
+ }
+ CheckedInt<int64_t> lhs = mTicks * aOther.mBase;
+ CheckedInt<int64_t> rhs = aOther.mTicks * mBase;
+ if (lhs.isValid() && rhs.isValid()) {
+ return lhs == rhs;
+ }
+ // Reduce the fractions and try again
+ const TimeUnit a = Reduced();
+ const TimeUnit b = aOther.Reduced();
+ lhs = a.mTicks * b.mBase;
+ rhs = b.mTicks * a.mBase;
+
+ if (lhs.isValid() && rhs.isValid()) {
+ return lhs.value() == rhs.value();
+ }
+ // last ditch, convert the reduced fractions to doubles
+ double lhsFloating =
+ static_cast<double>(a.mTicks.value()) * static_cast<double>(a.mBase);
+ double rhsFloating =
+ static_cast<double>(b.mTicks.value()) * static_cast<double>(b.mBase);
+
+ return lhsFloating == rhsFloating;
+}
+bool TimeUnit::operator!=(const TimeUnit& aOther) const {
+ MOZ_ASSERT(IsValid() && aOther.IsValid());
+ return !(aOther == *this);
+}
+bool TimeUnit::operator>=(const TimeUnit& aOther) const {
+ MOZ_ASSERT(IsValid() && aOther.IsValid());
+ if (aOther.mBase == mBase) {
+ return mTicks.value() >= aOther.mTicks.value();
+ }
+ if ((!IsPosInf() && aOther.IsPosInf()) ||
+ (IsNegInf() && !aOther.IsNegInf())) {
+ return false;
+ }
+ if ((IsPosInf() && !aOther.IsPosInf()) ||
+ (!IsNegInf() && aOther.IsNegInf())) {
+ return true;
+ }
+ CheckedInt<int64_t> lhs = mTicks * aOther.mBase;
+ CheckedInt<int64_t> rhs = aOther.mTicks * mBase;
+ if (lhs.isValid() && rhs.isValid()) {
+ return lhs.value() >= rhs.value();
+ }
+ // Reduce the fractions and try again
+ const TimeUnit a = Reduced();
+ const TimeUnit b = aOther.Reduced();
+ lhs = a.mTicks * b.mBase;
+ rhs = b.mTicks * a.mBase;
+
+ if (lhs.isValid() && rhs.isValid()) {
+ return lhs.value() >= rhs.value();
+ }
+ // last ditch, convert the reduced fractions to doubles
+ return ToSeconds() >= aOther.ToSeconds();
+}
+bool TimeUnit::operator>(const TimeUnit& aOther) const {
+ return !(*this <= aOther);
+}
+bool TimeUnit::operator<=(const TimeUnit& aOther) const {
+ MOZ_ASSERT(IsValid() && aOther.IsValid());
+ if (aOther.mBase == mBase) {
+ return mTicks.value() <= aOther.mTicks.value();
+ }
+ if ((!IsPosInf() && aOther.IsPosInf()) ||
+ (IsNegInf() && !aOther.IsNegInf())) {
+ return true;
+ }
+ if ((IsPosInf() && !aOther.IsPosInf()) ||
+ (!IsNegInf() && aOther.IsNegInf())) {
+ return false;
+ }
+ CheckedInt<int64_t> lhs = mTicks * aOther.mBase;
+ CheckedInt<int64_t> rhs = aOther.mTicks * mBase;
+ if (lhs.isValid() && rhs.isValid()) {
+ return lhs.value() <= rhs.value();
+ }
+ // Reduce the fractions and try again
+ const TimeUnit a = Reduced();
+ const TimeUnit b = aOther.Reduced();
+ lhs = a.mTicks * b.mBase;
+ rhs = b.mTicks * a.mBase;
+ if (lhs.isValid() && rhs.isValid()) {
+ return lhs.value() <= rhs.value();
+ }
+ // last ditch, convert the reduced fractions to doubles
+ return ToSeconds() <= aOther.ToSeconds();
+}
+bool TimeUnit::operator<(const TimeUnit& aOther) const {
+ return !(*this >= aOther);
+}
+
+TimeUnit TimeUnit::operator%(const TimeUnit& aOther) const {
+ MOZ_ASSERT(IsValid() && aOther.IsValid());
+ if (aOther.mBase == mBase) {
+ return TimeUnit(mTicks % aOther.mTicks, mBase);
+ }
+ // This path can be made better if need be.
+ double a = ToSeconds();
+ double b = aOther.ToSeconds();
+ return TimeUnit::FromSeconds(fmod(a, b), mBase);
+}
+
+TimeUnit TimeUnit::operator+(const TimeUnit& aOther) const {
+ if (IsInfinite() || aOther.IsInfinite()) {
+ // When adding at least one infinite value, the result is either
+ // +/-Inf, or NaN. So do the calculation in floating point for
+ // simplicity.
+ double result = ToSeconds() + aOther.ToSeconds();
+ return std::isnan(result) ? TimeUnit::Invalid() : FromSeconds(result);
+ }
+ if (aOther.mBase == mBase) {
+ return TimeUnit(mTicks + aOther.mTicks, mBase);
+ }
+ if (aOther.IsZero()) {
+ return *this;
+ }
+ if (IsZero()) {
+ return aOther;
+ }
+
+ double error;
+ TimeUnit inBase = aOther.ToBase(mBase, error);
+ if (error == 0.0) {
+ return *this + inBase;
+ }
+
+ // Last ditch: not exact
+ double a = ToSeconds();
+ double b = aOther.ToSeconds();
+ return TimeUnit::FromSeconds(a + b, mBase);
+}
+
+TimeUnit TimeUnit::operator-(const TimeUnit& aOther) const {
+ if (IsInfinite() || aOther.IsInfinite()) {
+ // When subtracting at least one infinite value, the result is either
+ // +/-Inf, or NaN. So do the calculation in floating point for
+ // simplicity.
+ double result = ToSeconds() - aOther.ToSeconds();
+ return std::isnan(result) ? TimeUnit::Invalid() : FromSeconds(result);
+ }
+ if (aOther.mBase == mBase) {
+ return TimeUnit(mTicks - aOther.mTicks, mBase);
+ }
+ if (aOther.IsZero()) {
+ return *this;
+ }
+
+ if (IsZero()) {
+ return TimeUnit(-aOther.mTicks, aOther.mBase);
+ }
+
+ double error = 0.0;
+ TimeUnit inBase = aOther.ToBase(mBase, error);
+ if (error == 0) {
+ return *this - inBase;
+ }
+
+ // Last ditch: not exact
+ double a = ToSeconds();
+ double b = aOther.ToSeconds();
+ return TimeUnit::FromSeconds(a - b, mBase);
+}
+TimeUnit& TimeUnit::operator+=(const TimeUnit& aOther) {
+ if (aOther.mBase == mBase) {
+ mTicks += aOther.mTicks;
+ return *this;
+ }
+ *this = *this + aOther;
+ return *this;
+}
+TimeUnit& TimeUnit::operator-=(const TimeUnit& aOther) {
+ if (aOther.mBase == mBase) {
+ mTicks -= aOther.mTicks;
+ return *this;
+ }
+ *this = *this - aOther;
+ return *this;
+}
+
+TimeUnit TimeUnit::MultDouble(double aVal) const {
+ double multiplied = AssertedCast<double>(mTicks.value()) * aVal;
+ // Check is the result of the multiplication can be represented exactly as
+ // an integer, in a double.
+ if (multiplied > std::pow(2, std::numeric_limits<double>::digits) - 1) {
+ printf_stderr("TimeUnit tick count after multiplication %" PRId64
+ " * %lf is too"
+ " high for the result to be exact",
+ mTicks.value(), aVal);
+ MOZ_CRASH();
+ }
+ // static_cast is ok, the magnitude of the number has been checked just above.
+ return TimeUnit(static_cast<int64_t>(multiplied), mBase);
+}
+
+bool TimeUnit::IsValid() const { return mTicks.isValid(); }
+
+bool TimeUnit::IsPosInf() const {
+ return mTicks.isValid() && mTicks.value() == INT64_MAX;
+}
+bool TimeUnit::IsNegInf() const {
+ return mTicks.isValid() && mTicks.value() == INT64_MIN;
+}
+
+int64_t TimeUnit::ToCommonUnit(int64_t aRatio) const {
+ CheckedInt<int64_t> rv = mTicks;
+ // Avoid the risk overflowing in common cases, e.g. converting a TimeUnit
+ // with a base of 1e9 back to nanoseconds.
+ if (mBase == aRatio) {
+ return rv.value();
+ }
+ // Avoid overflowing in other common cases, e.g. converting a TimeUnit with
+ // a base of 1e9 to microseconds: the denominator is divisible by the target
+ // unit so we can reorder the computation and keep the number within int64_t
+ // range.
+ if (aRatio < mBase && (mBase % aRatio) == 0) {
+ int64_t exactDivisor = mBase / aRatio;
+ rv /= exactDivisor;
+ return rv.value();
+ }
+ rv *= aRatio;
+ rv /= mBase;
+ if (rv.isValid()) {
+ return rv.value();
+ }
+ // Last ditch, perform the computation in floating point.
+ double ratioFloating = AssertedCast<double>(aRatio);
+ double baseFloating = AssertedCast<double>(mBase);
+ double ticksFloating = static_cast<double>(mTicks.value());
+ double approx = ticksFloating * (ratioFloating / baseFloating);
+ // Clamp to a valid range. If this is clamped it's outside any usable time
+ // value even in nanoseconds (thousands of years).
+ if (approx > static_cast<double>(std::numeric_limits<int64_t>::max())) {
+ return std::numeric_limits<int64_t>::max();
+ }
+ if (approx < static_cast<double>(std::numeric_limits<int64_t>::lowest())) {
+ return std::numeric_limits<int64_t>::lowest();
+ }
+ return static_cast<int64_t>(approx);
+}
+
+// Reduce a TimeUnit to the smallest possible ticks and base. This is useful
+// to comparison with big time values that can otherwise overflow.
+TimeUnit TimeUnit::Reduced() const {
+ int64_t gcd = GCD(mTicks.value(), mBase);
+ return TimeUnit(mTicks.value() / gcd, mBase / gcd);
+}
+
+double RoundToMicrosecondResolution(double aSeconds) {
+ return std::round(aSeconds * USECS_PER_S) / USECS_PER_S;
+}
+
+TimeRanges TimeRanges::ToMicrosecondResolution() const {
+ TimeRanges output;
+
+ for (const auto& interval : mIntervals) {
+ TimeRange reducedPrecision{RoundToMicrosecondResolution(interval.mStart),
+ RoundToMicrosecondResolution(interval.mEnd),
+ RoundToMicrosecondResolution(interval.mFuzz)};
+ output += reducedPrecision;
+ }
+ return output;
+}
+
+}; // namespace media
+
+} // namespace mozilla
diff --git a/dom/media/TimeUnits.h b/dom/media/TimeUnits.h
new file mode 100644
index 0000000000..bd8c84311d
--- /dev/null
+++ b/dom/media/TimeUnits.h
@@ -0,0 +1,342 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef TIME_UNITS_H
+#define TIME_UNITS_H
+
+#include <limits>
+#include <type_traits>
+
+#include "Intervals.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/FloatingPoint.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/IntegerPrintfMacros.h"
+#include "nsPrintfCString.h"
+
+namespace mozilla::media {
+class TimeIntervals;
+} // namespace mozilla::media
+// CopyChooser specialization for nsTArray
+template <>
+struct nsTArray_RelocationStrategy<mozilla::media::TimeIntervals> {
+ using Type =
+ nsTArray_RelocateUsingMoveConstructor<mozilla::media::TimeIntervals>;
+};
+
+namespace mozilla {
+
+// Number of milliseconds per second. 1e3.
+static const int64_t MSECS_PER_S = 1000;
+
+// Number of microseconds per second. 1e6.
+static const int64_t USECS_PER_S = 1000000;
+
+// Number of nanoseconds per second. 1e9.
+static const int64_t NSECS_PER_S = 1000000000;
+
+namespace media {
+
+#ifndef PROCESS_DECODE_LOG
+# define PROCESS_DECODE_LOG(sample) \
+ MOZ_LOG(sPDMLog, mozilla::LogLevel::Verbose, \
+ ("ProcessDecode: mDuration=%" PRIu64 "µs ; mTime=%" PRIu64 \
+ "µs ; mTimecode=%" PRIu64 "µs", \
+ (sample)->mDuration.ToMicroseconds(), \
+ (sample)->mTime.ToMicroseconds(), \
+ (sample)->mTimecode.ToMicroseconds()))
+#endif // PROCESS_DECODE_LOG
+
+// TimeUnit is a class that represents a time value, that can be negative or
+// positive.
+//
+// Internally, it works by storing a numerator (the tick numbers), that uses
+// checked arithmetics, and a denominator (the base), that is a regular integer
+// on which arithmetics is never performed, and is only set at construction, or
+// replaced.
+//
+// Dividing the tick count by the base always yields a value in seconds,
+// but it's very useful to have a base that is dependant on the context: it can
+// be the sample-rate of an audio stream, the time base of an mp4, that's often
+// 90000 because it's divisible by 24, 25 and 30.
+//
+// Keeping time like this allows performing calculations on time values with
+// maximum precision, without having to have to care about rounding errors or
+// precision loss.
+//
+// If not specified, the base is 1e6, representing microseconds, for historical
+// reasons. Users can gradually move to more precise representations when
+// needed.
+//
+// INT64_MAX has the special meaning of being +∞, and INT64_MIN means -∞. Any
+// other value corresponds to a valid time.
+//
+// If an overflow or other problem occurs, the underlying CheckedInt<int64_t> is
+// invalid and a crash is triggered.
+class TimeUnit final {
+ public:
+ constexpr TimeUnit(CheckedInt64 aTicks, int64_t aBase)
+ : mTicks(aTicks), mBase(aBase) {
+ MOZ_RELEASE_ASSERT(mBase > 0);
+ }
+
+ explicit constexpr TimeUnit(CheckedInt64 aTicks)
+ : mTicks(aTicks), mBase(USECS_PER_S) {}
+
+ // Return the maximum number of ticks that a TimeUnit can contain.
+ static constexpr int64_t MaxTicks() {
+ return std::numeric_limits<int64_t>::max() - 1;
+ }
+
+ // This is only precise up to a point, which is aValue * aBase <= 2^53 - 1
+ static TimeUnit FromSeconds(double aValue, int64_t aBase = USECS_PER_S);
+ static constexpr TimeUnit FromMicroseconds(int64_t aValue) {
+ return TimeUnit(aValue, USECS_PER_S);
+ }
+ static TimeUnit FromHns(int64_t aValue, int64_t aBase) {
+ // Truncating here would mean a loss of precision.
+ return TimeUnit::FromNanoseconds(aValue * 100).ToBase<RoundPolicy>(aBase);
+ }
+ static constexpr TimeUnit FromNanoseconds(int64_t aValue) {
+ return TimeUnit(aValue, NSECS_PER_S);
+ }
+ static TimeUnit FromInfinity();
+ static TimeUnit FromNegativeInfinity();
+ static TimeUnit FromTimeDuration(const TimeDuration& aDuration);
+ static constexpr TimeUnit Zero(int64_t aBase = USECS_PER_S) {
+ return TimeUnit(0, aBase);
+ }
+ static constexpr TimeUnit Zero(const TimeUnit& aOther) {
+ return TimeUnit(0, aOther.mBase);
+ }
+ static TimeUnit Invalid();
+ int64_t ToMilliseconds() const;
+ int64_t ToMicroseconds() const;
+ int64_t ToNanoseconds() const;
+ int64_t ToTicksAtRate(int64_t aRate) const;
+ double ToSeconds() const;
+ nsCString ToString() const;
+ TimeDuration ToTimeDuration() const;
+ bool IsInfinite() const;
+ bool IsPositive() const;
+ bool IsPositiveOrZero() const;
+ bool IsZero() const;
+ bool IsNegative() const;
+
+ // Returns true if the fractions are equal when converted to the smallest
+ // base.
+ bool EqualsAtLowestResolution(const TimeUnit& aOther) const;
+ // Strict equality -- the fractions must be exactly equal
+ bool operator==(const TimeUnit& aOther) const;
+ bool operator!=(const TimeUnit& aOther) const;
+ bool operator>=(const TimeUnit& aOther) const;
+ bool operator>(const TimeUnit& aOther) const;
+ bool operator<=(const TimeUnit& aOther) const;
+ bool operator<(const TimeUnit& aOther) const;
+ TimeUnit operator%(const TimeUnit& aOther) const;
+ TimeUnit operator+(const TimeUnit& aOther) const;
+ TimeUnit operator-(const TimeUnit& aOther) const;
+ TimeUnit& operator+=(const TimeUnit& aOther);
+ TimeUnit& operator-=(const TimeUnit& aOther);
+ template <typename T>
+ TimeUnit operator*(T aVal) const {
+ // See bug 853398 for the reason to block double multiplier.
+ // If required, use MultDouble below and with caution.
+ static_assert(std::is_integral_v<T>, "Must be an integral type");
+ return TimeUnit(mTicks * aVal, mBase);
+ }
+ TimeUnit MultDouble(double aVal) const;
+ friend TimeUnit operator/(const TimeUnit& aUnit, int64_t aVal) {
+ MOZ_DIAGNOSTIC_ASSERT(0 <= aVal && aVal <= UINT32_MAX);
+ return TimeUnit(aUnit.mTicks / aVal, aUnit.mBase);
+ }
+ friend TimeUnit operator%(const TimeUnit& aUnit, int64_t aVal) {
+ MOZ_DIAGNOSTIC_ASSERT(0 <= aVal && aVal <= UINT32_MAX);
+ return TimeUnit(aUnit.mTicks % aVal, aUnit.mBase);
+ }
+
+ struct TruncatePolicy {
+ template <typename T>
+ static T policy(T& aValue) {
+ return static_cast<T>(aValue);
+ }
+ };
+
+ struct RoundPolicy {
+ template <typename T>
+ static T policy(T& aValue) {
+ return std::round(aValue);
+ }
+ };
+
+ template <class RoundingPolicy = TruncatePolicy>
+ TimeUnit ToBase(int64_t aTargetBase) const {
+ double dummy = 0.0;
+ return ToBase<RoundingPolicy>(aTargetBase, dummy);
+ }
+
+ template <class RoundingPolicy = TruncatePolicy>
+ TimeUnit ToBase(const TimeUnit& aTimeUnit) const {
+ double dummy = 0.0;
+ return ToBase<RoundingPolicy>(aTimeUnit, dummy);
+ }
+
+ // Allow returning the same value, in a base that matches another TimeUnit.
+ template <class RoundingPolicy = TruncatePolicy>
+ TimeUnit ToBase(const TimeUnit& aTimeUnit, double& aOutError) const {
+ int64_t targetBase = aTimeUnit.mBase;
+ return ToBase<RoundingPolicy>(targetBase, aOutError);
+ }
+
+ template <class RoundingPolicy = TruncatePolicy>
+ TimeUnit ToBase(int64_t aTargetBase, double& aOutError) const {
+ aOutError = 0.0;
+ CheckedInt<int64_t> ticks = mTicks * aTargetBase;
+ if (ticks.isValid()) {
+ imaxdiv_t rv = imaxdiv(ticks.value(), mBase);
+ if (!rv.rem) {
+ return TimeUnit(rv.quot, aTargetBase);
+ }
+ }
+ double approx = static_cast<double>(mTicks.value()) *
+ static_cast<double>(aTargetBase) /
+ static_cast<double>(mBase);
+ double integer;
+ aOutError = modf(approx, &integer);
+ return TimeUnit(AssertedCast<int64_t>(RoundingPolicy::policy(approx)),
+ aTargetBase);
+ }
+
+ bool IsValid() const;
+
+ constexpr TimeUnit() = default;
+
+ TimeUnit(const TimeUnit&) = default;
+
+ TimeUnit& operator=(const TimeUnit&) = default;
+
+ bool IsPosInf() const;
+ bool IsNegInf() const;
+
+ // Allow serializing a TimeUnit via IPC
+ friend IPC::ParamTraits<mozilla::media::TimeUnit>;
+
+#ifndef VISIBLE_TIMEUNIT_INTERNALS
+ private:
+#endif
+ int64_t ToCommonUnit(int64_t aRatio) const;
+ // Reduce a TimeUnit to the smallest possible ticks and base. This is useful
+ // to comparison with big time values that can otherwise overflow.
+ TimeUnit Reduced() const;
+
+ CheckedInt64 mTicks{0};
+ // Default base is microseconds.
+ int64_t mBase{USECS_PER_S};
+};
+
+using NullableTimeUnit = Maybe<TimeUnit>;
+
+using TimeInterval = Interval<TimeUnit>;
+
+// A set of intervals, containing TimeUnit.
+class TimeIntervals : public IntervalSet<TimeUnit> {
+ public:
+ using BaseType = IntervalSet<TimeUnit>;
+ using InnerType = TimeUnit;
+
+ // We can't use inherited constructors yet. So we have to duplicate all the
+ // constructors found in IntervalSet base class.
+ // all this could be later replaced with:
+ // using IntervalSet<TimeUnit>::IntervalSet;
+
+ // MOZ_IMPLICIT as we want to enable initialization in the form:
+ // TimeIntervals i = ... like we would do with IntervalSet<T> i = ...
+ MOZ_IMPLICIT TimeIntervals(const BaseType& aOther) : BaseType(aOther) {}
+ MOZ_IMPLICIT TimeIntervals(BaseType&& aOther) : BaseType(std::move(aOther)) {}
+ explicit TimeIntervals(const BaseType::ElemType& aOther) : BaseType(aOther) {}
+ explicit TimeIntervals(BaseType::ElemType&& aOther)
+ : BaseType(std::move(aOther)) {}
+
+ static TimeIntervals Invalid() {
+ return TimeIntervals(TimeInterval(TimeUnit::FromNegativeInfinity(),
+ TimeUnit::FromNegativeInfinity()));
+ }
+ bool IsInvalid() const {
+ return Length() == 1 && Start(0).IsNegInf() && End(0).IsNegInf();
+ }
+
+ // Returns the same interval, with a microsecond resolution. This is used to
+ // compare TimeUnits internal to demuxers (that use a base from the container)
+ // to floating point numbers in seconds from content.
+ TimeIntervals ToMicrosecondResolution() const {
+ TimeIntervals output;
+
+ for (const auto& interval : mIntervals) {
+ TimeInterval reducedPrecision{interval.mStart.ToBase(USECS_PER_S),
+ interval.mEnd.ToBase(USECS_PER_S),
+ interval.mFuzz.ToBase(USECS_PER_S)};
+ output += reducedPrecision;
+ }
+ return output;
+ }
+
+ nsCString ToString() const {
+ nsCString dump;
+ for (const auto& interval : mIntervals) {
+ dump += nsPrintfCString("[%s],", interval.ToString().get());
+ }
+ return dump;
+ }
+
+ TimeIntervals() = default;
+};
+
+using TimeRange = Interval<double>;
+
+// A set of intervals, containing doubles that are seconds.
+class TimeRanges : public IntervalSet<double> {
+ public:
+ using BaseType = IntervalSet<double>;
+ using InnerType = double;
+ using nld = std::numeric_limits<double>;
+
+ // We can't use inherited constructors yet. So we have to duplicate all the
+ // constructors found in IntervalSet base class.
+ // all this could be later replaced with:
+ // using IntervalSet<TimeUnit>::IntervalSet;
+
+ // MOZ_IMPLICIT as we want to enable initialization in the form:
+ // TimeIntervals i = ... like we would do with IntervalSet<T> i = ...
+ MOZ_IMPLICIT TimeRanges(const BaseType& aOther) : BaseType(aOther) {}
+ MOZ_IMPLICIT TimeRanges(BaseType&& aOther) : BaseType(std::move(aOther)) {}
+ explicit TimeRanges(const BaseType::ElemType& aOther) : BaseType(aOther) {}
+ explicit TimeRanges(BaseType::ElemType&& aOther)
+ : BaseType(std::move(aOther)) {}
+
+ static TimeRanges Invalid() {
+ return TimeRanges(TimeRange(-nld::infinity(), nld::infinity()));
+ }
+ bool IsInvalid() const {
+ return Length() == 1 && Start(0) == -nld::infinity() &&
+ End(0) == nld::infinity();
+ }
+ // Convert from TimeUnit-based intervals to second-based TimeRanges.
+ explicit TimeRanges(const TimeIntervals& aIntervals) {
+ for (const auto& interval : aIntervals) {
+ Add(TimeRange(interval.mStart.ToSeconds(), interval.mEnd.ToSeconds()));
+ }
+ }
+
+ TimeRanges ToMicrosecondResolution() const;
+
+ TimeRanges() = default;
+};
+
+} // namespace media
+} // namespace mozilla
+
+#endif // TIME_UNITS_H
diff --git a/dom/media/Tracing.cpp b/dom/media/Tracing.cpp
new file mode 100644
index 0000000000..bbab95039c
--- /dev/null
+++ b/dom/media/Tracing.cpp
@@ -0,0 +1,86 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "Tracing.h"
+
+#include <inttypes.h>
+
+using namespace mozilla;
+
+using TracingPhase = mozilla::AsyncLogger::TracingPhase;
+
+mozilla::AsyncLogger gAudioCallbackTraceLogger;
+static std::atomic<int> gTracingStarted(0);
+
+void StartAudioCallbackTracing() {
+#ifdef MOZ_REAL_TIME_TRACING
+ int cnt = gTracingStarted.fetch_add(1, std::memory_order_seq_cst);
+ if (cnt == 0) {
+ // This is a noop if the logger has not been enabled.
+ gAudioCallbackTraceLogger.Start();
+ }
+#endif
+}
+
+void StopAudioCallbackTracing() {
+#ifdef MOZ_REAL_TIME_TRACING
+ int cnt = gTracingStarted.fetch_sub(1, std::memory_order_seq_cst);
+ if (cnt == 1) {
+ // This is a noop if the logger has not been enabled.
+ gAudioCallbackTraceLogger.Stop();
+ }
+#endif
+}
+
+void AutoTracer::PrintEvent(const char* aName, const char* aCategory,
+ const char* aComment, TracingPhase aPhase) {
+#ifdef MOZ_REAL_TIME_TRACING
+ mLogger.Log(aName, aCategory, aComment, aPhase);
+#endif
+}
+
+void AutoTracer::PrintBudget(const char* aName, const char* aCategory,
+ uint64_t aDuration, uint64_t aFrames,
+ uint64_t aSampleRate) {
+#ifdef MOZ_REAL_TIME_TRACING
+ mLogger.LogDuration(aName, aCategory, aDuration, aFrames, aSampleRate);
+#endif
+}
+
+AutoTracer::AutoTracer(AsyncLogger& aLogger, const char* aLocation,
+ EventType aEventType, uint64_t aFrames,
+ uint64_t aSampleRate)
+ : mLogger(aLogger),
+ mLocation(aLocation),
+ mComment(nullptr),
+ mEventType(aEventType) {
+ MOZ_ASSERT(aEventType == EventType::BUDGET);
+
+ if (aLogger.Enabled()) {
+ float durationUS = (static_cast<float>(aFrames) / aSampleRate) * 1e6;
+ PrintBudget(aLocation, "perf", durationUS, aFrames, aSampleRate);
+ }
+}
+
+AutoTracer::AutoTracer(AsyncLogger& aLogger, const char* aLocation,
+ EventType aEventType, const char* aComment)
+ : mLogger(aLogger),
+ mLocation(aLocation),
+ mComment(aComment),
+ mEventType(aEventType) {
+ MOZ_ASSERT(aEventType == EventType::DURATION);
+ if (aLogger.Enabled()) {
+ PrintEvent(aLocation, "perf", mComment, AsyncLogger::TracingPhase::BEGIN);
+ }
+}
+
+AutoTracer::~AutoTracer() {
+ if (mEventType == EventType::DURATION) {
+ if (mLogger.Enabled()) {
+ PrintEvent(mLocation, "perf", mComment, AsyncLogger::TracingPhase::END);
+ }
+ }
+}
diff --git a/dom/media/Tracing.h b/dom/media/Tracing.h
new file mode 100644
index 0000000000..75e74c63b5
--- /dev/null
+++ b/dom/media/Tracing.h
@@ -0,0 +1,102 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef TRACING_H
+#define TRACING_H
+
+#include <algorithm>
+#include <cstdint>
+#include <cstdio>
+
+#include "AsyncLogger.h"
+
+#include "mozilla/Attributes.h"
+#include "mozilla/UniquePtr.h"
+
+#if defined(_MSC_VER)
+// MSVC
+# define FUNCTION_SIGNATURE __FUNCSIG__
+#elif defined(__GNUC__)
+// gcc, clang
+# define FUNCTION_SIGNATURE __PRETTY_FUNCTION__
+#endif
+
+extern mozilla::AsyncLogger gAudioCallbackTraceLogger;
+
+// This is no-op if tracing is not enabled, and is idempotent.
+void StartAudioCallbackTracing();
+void StopAudioCallbackTracing();
+
+#ifdef MOZ_REAL_TIME_TRACING
+# define TRACE(aName) AutoTracer trace(gAudioCallbackTraceLogger, aName);
+# define TRACE_COMMENT(aName, aFmt, ...) \
+ AutoTracer trace(gAudioCallbackTraceLogger, aName, \
+ AutoTracer::EventType::DURATION, aFmt, ##__VA_ARGS__);
+# define TRACE_AUDIO_CALLBACK_BUDGET(aFrames, aSampleRate) \
+ AutoTracer budget(gAudioCallbackTraceLogger, "Real-time budget", \
+ AutoTracer::EventType::BUDGET, aFrames, aSampleRate);
+#else
+# define TRACE(aName)
+# define TRACE_COMMENT(aFmt, ...)
+# define TRACE_AUDIO_CALLBACK_BUDGET(aFrames, aSampleRate)
+#endif
+
+class MOZ_RAII AutoTracer {
+ public:
+ static const int32_t BUFFER_SIZE = 256;
+
+ enum class EventType { DURATION, BUDGET };
+
+ AutoTracer(mozilla::AsyncLogger& aLogger, const char* aLocation,
+ EventType aEventType = EventType::DURATION,
+ const char* aComment = nullptr);
+
+ template <typename... Args>
+ AutoTracer(mozilla::AsyncLogger& aLogger, const char* aLocation,
+ EventType aEventType, const char* aFormat, Args... aArgs)
+ : mLogger(aLogger),
+ mLocation(aLocation),
+ mComment(mBuffer),
+ mEventType(aEventType) {
+ MOZ_ASSERT(aEventType == EventType::DURATION);
+ if (aLogger.Enabled()) {
+ int32_t size = snprintf(mBuffer, BUFFER_SIZE, aFormat, aArgs...);
+ size = std::min(size, BUFFER_SIZE - 1);
+ mBuffer[size] = 0;
+ PrintEvent(aLocation, "perf", mComment,
+ mozilla::AsyncLogger::TracingPhase::BEGIN);
+ }
+ }
+
+ AutoTracer(mozilla::AsyncLogger& aLogger, const char* aLocation,
+ EventType aEventType, uint64_t aFrames, uint64_t aSampleRate);
+
+ ~AutoTracer();
+
+ private:
+ void PrintEvent(const char* aName, const char* aCategory,
+ const char* aComment,
+ mozilla::AsyncLogger::TracingPhase aPhase);
+
+ void PrintBudget(const char* aName, const char* aCategory, uint64_t aDuration,
+ uint64_t aFrames, uint64_t aSampleRate);
+
+ // The logger to use. It musdt have a lifetime longer than the block an
+ // instance of this class traces.
+ mozilla::AsyncLogger& mLogger;
+ // The location for this trace point, arbitrary string literal, often the
+ // name of the calling function, with a static lifetime.
+ const char* mLocation;
+ // A comment for this trace point, abitrary string literal with a static
+ // lifetime.
+ const char* mComment;
+ // A buffer used to hold string-formatted traces.
+ char mBuffer[BUFFER_SIZE];
+ // The event type, for now either a budget or a duration.
+ const EventType mEventType;
+};
+
+#endif /* TRACING_H */
diff --git a/dom/media/UnderrunHandler.h b/dom/media/UnderrunHandler.h
new file mode 100644
index 0000000000..a8d6bedbc7
--- /dev/null
+++ b/dom/media/UnderrunHandler.h
@@ -0,0 +1,22 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_UNDERRUNHANDLER_H_
+#define MOZILLA_UNDERRUNHANDLER_H_
+
+namespace mozilla {
+// Install an handler on SIGXPCU for the calling thread, that calls
+// `UnderrunHandler` when the soft real-time limit has been reached. If a
+// handler was already in place, this does nothing. No-op if not on Linux
+// Desktop.
+void InstallSoftRealTimeLimitHandler();
+// Query whether or not the soft-real-time limit has been reached. Always
+// false when not on Linux desktop. Can be called from any thread.
+bool SoftRealTimeLimitReached();
+// Set the calling thread to a normal scheduling class.
+void DemoteThreadFromRealTime();
+} // namespace mozilla
+
+#endif // MOZILLA_UNDERRUNHANDLER_H_
diff --git a/dom/media/UnderrunHandlerLinux.cpp b/dom/media/UnderrunHandlerLinux.cpp
new file mode 100644
index 0000000000..05646fbeb8
--- /dev/null
+++ b/dom/media/UnderrunHandlerLinux.cpp
@@ -0,0 +1,78 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <csignal>
+#include <cerrno>
+#include <pthread.h>
+
+#include <mozilla/Sprintf.h>
+#include <mozilla/Atomics.h>
+#include "audio_thread_priority.h"
+#include "nsDebug.h"
+
+namespace mozilla {
+
+Atomic<bool, MemoryOrdering::ReleaseAcquire> gRealtimeLimitReached;
+
+void UnderrunHandler(int signum) { gRealtimeLimitReached = true; }
+
+bool SoftRealTimeLimitReached() { return gRealtimeLimitReached; }
+
+void InstallSoftRealTimeLimitHandler() {
+ struct sigaction action, previous;
+
+ action.sa_handler = UnderrunHandler;
+ sigemptyset(&action.sa_mask);
+ action.sa_flags = 0;
+
+ int rv = sigaction(SIGXCPU, &action, &previous);
+ if (rv != 0) {
+ char buf[256];
+ SprintfLiteral(buf, "sigaction(SIGXCPU, ...): %s", strerror(errno));
+ NS_WARNING(buf);
+ return;
+ }
+
+ void* previous_handler = previous.sa_flags == SA_SIGINFO
+ ? reinterpret_cast<void*>(previous.sa_sigaction)
+ : reinterpret_cast<void*>(previous.sa_handler);
+
+ MOZ_ASSERT(previous_handler != UnderrunHandler,
+ "Only install the SIGXCPU handler once per process.");
+
+ if (previous_handler != SIG_DFL && previous_handler != UnderrunHandler) {
+ NS_WARNING(
+ "SIGXCPU handler was already set by something else, dropping real-time "
+ "priority.");
+ rv = sigaction(SIGXCPU, &previous, nullptr);
+ if (rv != 0) {
+ NS_WARNING("Could not restore previous handler for SIGXCPU.");
+ }
+ gRealtimeLimitReached = true;
+ return;
+ }
+
+ gRealtimeLimitReached = false;
+}
+
+void DemoteThreadFromRealTime() {
+ atp_thread_info* info = atp_get_current_thread_info();
+ if (!info) {
+ NS_WARNING("Could not get current thread info when demoting thread.");
+ return;
+ }
+ int rv = atp_demote_thread_from_real_time(info);
+ if (rv) {
+ NS_WARNING("Could not demote thread from real-time.");
+ return;
+ }
+ rv = atp_free_thread_info(info);
+ if (rv) {
+ NS_WARNING("Could not free atp_thread_info struct");
+ }
+ gRealtimeLimitReached = false;
+}
+
+} // namespace mozilla
diff --git a/dom/media/UnderrunHandlerNoop.cpp b/dom/media/UnderrunHandlerNoop.cpp
new file mode 100644
index 0000000000..8792dbf145
--- /dev/null
+++ b/dom/media/UnderrunHandlerNoop.cpp
@@ -0,0 +1,14 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+namespace mozilla {
+
+bool SoftRealTimeLimitReached() { return false; }
+
+void InstallSoftRealTimeLimitHandler() {}
+
+void DemoteThreadFromRealTime() {}
+
+} // namespace mozilla
diff --git a/dom/media/VideoFrameContainer.cpp b/dom/media/VideoFrameContainer.cpp
new file mode 100644
index 0000000000..8aff85fac0
--- /dev/null
+++ b/dom/media/VideoFrameContainer.cpp
@@ -0,0 +1,257 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "VideoFrameContainer.h"
+
+#ifdef MOZ_WIDGET_ANDROID
+# include "GLImages.h" // for SurfaceTextureImage
+#endif
+#include "MediaDecoderOwner.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/AbstractThread.h"
+
+using namespace mozilla::layers;
+
+namespace mozilla {
+#define NS_DispatchToMainThread(...) CompileError_UseAbstractMainThreadInstead
+
+namespace {
+template <Telemetry::HistogramID ID>
+class AutoTimer {
+ // Set a threshold to reduce performance overhead
+ // for we're measuring hot spots.
+ static const uint32_t sThresholdMS = 1000;
+
+ public:
+ ~AutoTimer() {
+ auto end = TimeStamp::Now();
+ auto diff = uint32_t((end - mStart).ToMilliseconds());
+ if (diff > sThresholdMS) {
+ Telemetry::Accumulate(ID, diff);
+ }
+ }
+
+ private:
+ const TimeStamp mStart = TimeStamp::Now();
+};
+} // namespace
+
+VideoFrameContainer::VideoFrameContainer(
+ MediaDecoderOwner* aOwner, already_AddRefed<ImageContainer> aContainer)
+ : mOwner(aOwner),
+ mImageContainer(aContainer),
+ mMutex("nsVideoFrameContainer"),
+ mFrameID(0),
+ mPendingPrincipalHandle(PRINCIPAL_HANDLE_NONE),
+ mFrameIDForPendingPrincipalHandle(0),
+ mMainThread(aOwner->AbstractMainThread()) {
+ NS_ASSERTION(aOwner, "aOwner must not be null");
+ NS_ASSERTION(mImageContainer, "aContainer must not be null");
+}
+
+VideoFrameContainer::~VideoFrameContainer() = default;
+
+PrincipalHandle VideoFrameContainer::GetLastPrincipalHandle() {
+ MutexAutoLock lock(mMutex);
+ return GetLastPrincipalHandleLocked();
+}
+
+PrincipalHandle VideoFrameContainer::GetLastPrincipalHandleLocked() {
+ return mLastPrincipalHandle;
+}
+
+void VideoFrameContainer::UpdatePrincipalHandleForFrameID(
+ const PrincipalHandle& aPrincipalHandle,
+ const ImageContainer::FrameID& aFrameID) {
+ MutexAutoLock lock(mMutex);
+ UpdatePrincipalHandleForFrameIDLocked(aPrincipalHandle, aFrameID);
+}
+
+void VideoFrameContainer::UpdatePrincipalHandleForFrameIDLocked(
+ const PrincipalHandle& aPrincipalHandle,
+ const ImageContainer::FrameID& aFrameID) {
+ if (mPendingPrincipalHandle == aPrincipalHandle) {
+ return;
+ }
+ mPendingPrincipalHandle = aPrincipalHandle;
+ mFrameIDForPendingPrincipalHandle = aFrameID;
+}
+
+#ifdef MOZ_WIDGET_ANDROID
+static void NotifySetCurrent(Image* aImage) {
+ if (aImage == nullptr) {
+ return;
+ }
+
+ SurfaceTextureImage* image = aImage->AsSurfaceTextureImage();
+ if (image == nullptr) {
+ return;
+ }
+
+ image->OnSetCurrent();
+}
+#endif
+
+void VideoFrameContainer::SetCurrentFrame(const gfx::IntSize& aIntrinsicSize,
+ Image* aImage,
+ const TimeStamp& aTargetTime) {
+#ifdef MOZ_WIDGET_ANDROID
+ NotifySetCurrent(aImage);
+#endif
+ if (aImage) {
+ MutexAutoLock lock(mMutex);
+ AutoTArray<ImageContainer::NonOwningImage, 1> imageList;
+ imageList.AppendElement(
+ ImageContainer::NonOwningImage(aImage, aTargetTime, ++mFrameID));
+ SetCurrentFramesLocked(aIntrinsicSize, imageList);
+ } else {
+ ClearCurrentFrame(aIntrinsicSize);
+ }
+}
+
+void VideoFrameContainer::SetCurrentFrames(
+ const gfx::IntSize& aIntrinsicSize,
+ const nsTArray<ImageContainer::NonOwningImage>& aImages) {
+#ifdef MOZ_WIDGET_ANDROID
+ // When there are multiple frames, only the last one is effective
+ // (see bug 1299068 comment 4). Here I just count on VideoSink and VideoOutput
+ // to send one frame at a time and warn if not.
+ Unused << NS_WARN_IF(aImages.Length() > 1);
+ for (auto& image : aImages) {
+ NotifySetCurrent(image.mImage);
+ }
+#endif
+ MutexAutoLock lock(mMutex);
+ SetCurrentFramesLocked(aIntrinsicSize, aImages);
+}
+
+void VideoFrameContainer::SetCurrentFramesLocked(
+ const gfx::IntSize& aIntrinsicSize,
+ const nsTArray<ImageContainer::NonOwningImage>& aImages) {
+ mMutex.AssertCurrentThreadOwns();
+
+ if (auto size = Some(aIntrinsicSize); size != mIntrinsicSize) {
+ mIntrinsicSize = size;
+ mMainThread->Dispatch(NS_NewRunnableFunction(
+ "IntrinsicSizeChanged", [this, self = RefPtr(this), size]() {
+ mMainThreadState.mNewIntrinsicSize = size;
+ }));
+ }
+
+ gfx::IntSize oldFrameSize = mImageContainer->GetCurrentSize();
+
+ // When using the OMX decoder, destruction of the current image can indirectly
+ // block on main thread I/O. If we let this happen while holding onto
+ // |mImageContainer|'s lock, then when the main thread then tries to
+ // composite it can then block on |mImageContainer|'s lock, causing a
+ // deadlock. We use this hack to defer the destruction of the current image
+ // until it is safe.
+ nsTArray<ImageContainer::OwningImage> oldImages;
+ mImageContainer->GetCurrentImages(&oldImages);
+
+ PrincipalHandle principalHandle = PRINCIPAL_HANDLE_NONE;
+ ImageContainer::FrameID lastFrameIDForOldPrincipalHandle =
+ mFrameIDForPendingPrincipalHandle - 1;
+ if (mPendingPrincipalHandle != PRINCIPAL_HANDLE_NONE &&
+ ((!oldImages.IsEmpty() &&
+ oldImages.LastElement().mFrameID >= lastFrameIDForOldPrincipalHandle) ||
+ (!aImages.IsEmpty() &&
+ aImages[0].mFrameID > lastFrameIDForOldPrincipalHandle))) {
+ // We are releasing the last FrameID prior to
+ // `lastFrameIDForOldPrincipalHandle` OR there are no FrameIDs prior to
+ // `lastFrameIDForOldPrincipalHandle` in the new set of images. This means
+ // that the old principal handle has been flushed out and we can notify our
+ // video element about this change.
+ principalHandle = mPendingPrincipalHandle;
+ mLastPrincipalHandle = mPendingPrincipalHandle;
+ mPendingPrincipalHandle = PRINCIPAL_HANDLE_NONE;
+ mFrameIDForPendingPrincipalHandle = 0;
+ }
+
+ if (aImages.IsEmpty()) {
+ mImageContainer->ClearAllImages();
+ } else {
+ mImageContainer->SetCurrentImages(aImages);
+ }
+ gfx::IntSize newFrameSize = mImageContainer->GetCurrentSize();
+ bool imageSizeChanged = (oldFrameSize != newFrameSize);
+
+ if (principalHandle != PRINCIPAL_HANDLE_NONE || imageSizeChanged) {
+ RefPtr<VideoFrameContainer> self = this;
+ mMainThread->Dispatch(NS_NewRunnableFunction(
+ "PrincipalHandleOrImageSizeChanged",
+ [this, self, principalHandle, imageSizeChanged]() {
+ mMainThreadState.mImageSizeChanged = imageSizeChanged;
+ if (mOwner && principalHandle != PRINCIPAL_HANDLE_NONE) {
+ mOwner->PrincipalHandleChangedForVideoFrameContainer(
+ this, principalHandle);
+ }
+ }));
+ }
+}
+
+void VideoFrameContainer::ClearFutureFrames(TimeStamp aNow) {
+ MutexAutoLock lock(mMutex);
+
+ // See comment in SetCurrentFrame for the reasoning behind
+ // using a kungFuDeathGrip here.
+ AutoTArray<ImageContainer::OwningImage, 10> kungFuDeathGrip;
+ mImageContainer->GetCurrentImages(&kungFuDeathGrip);
+
+ if (!kungFuDeathGrip.IsEmpty()) {
+ AutoTArray<ImageContainer::NonOwningImage, 1> currentFrame;
+ ImageContainer::OwningImage& img = kungFuDeathGrip[0];
+ // Find the current image in case there are several.
+ for (const auto& image : kungFuDeathGrip) {
+ if (image.mTimeStamp > aNow) {
+ break;
+ }
+ img = image;
+ }
+ currentFrame.AppendElement(ImageContainer::NonOwningImage(
+ img.mImage, img.mTimeStamp, img.mFrameID, img.mProducerID));
+ mImageContainer->SetCurrentImages(currentFrame);
+ }
+}
+
+void VideoFrameContainer::ClearCachedResources() {
+ MutexAutoLock lock(mMutex);
+ mImageContainer->ClearCachedResources();
+}
+
+ImageContainer* VideoFrameContainer::GetImageContainer() {
+ // Note - you'll need the lock to manipulate this. The pointer is not
+ // modified from multiple threads, just the data pointed to by it.
+ return mImageContainer;
+}
+
+double VideoFrameContainer::GetFrameDelay() {
+ MutexAutoLock lock(mMutex);
+ return mImageContainer->GetPaintDelay().ToSeconds();
+}
+
+void VideoFrameContainer::InvalidateWithFlags(uint32_t aFlags) {
+ NS_ASSERTION(NS_IsMainThread(), "Must call on main thread");
+
+ if (!mOwner) {
+ // Owner has been destroyed
+ return;
+ }
+
+ MediaDecoderOwner::ImageSizeChanged imageSizeChanged{
+ mMainThreadState.mImageSizeChanged};
+ mMainThreadState.mImageSizeChanged = false;
+
+ auto newIntrinsicSize = std::move(mMainThreadState.mNewIntrinsicSize);
+
+ MediaDecoderOwner::ForceInvalidate forceInvalidate{
+ (aFlags & INVALIDATE_FORCE) != 0};
+ mOwner->Invalidate(imageSizeChanged, newIntrinsicSize, forceInvalidate);
+}
+
+} // namespace mozilla
+
+#undef NS_DispatchToMainThread
diff --git a/dom/media/VideoFrameContainer.h b/dom/media/VideoFrameContainer.h
new file mode 100644
index 0000000000..80e1851d8f
--- /dev/null
+++ b/dom/media/VideoFrameContainer.h
@@ -0,0 +1,149 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef VIDEOFRAMECONTAINER_H_
+#define VIDEOFRAMECONTAINER_H_
+
+#include "mozilla/Mutex.h"
+#include "mozilla/TimeStamp.h"
+#include "gfxPoint.h"
+#include "nsCOMPtr.h"
+#include "ImageContainer.h"
+#include "MediaSegment.h"
+#include "VideoSegment.h"
+
+namespace mozilla {
+
+class MediaDecoderOwner;
+
+/**
+ * This object is used in the decoder backend threads and the main thread
+ * to manage the "current video frame" state. This state includes timing data
+ * and an intrinsic size (see below).
+ * This has to be a thread-safe object since it's accessed by resource decoders
+ * and other off-main-thread components. So we can't put this state in the media
+ * element itself ... well, maybe we could, but it could be risky and/or
+ * confusing.
+ */
+class VideoFrameContainer {
+ virtual ~VideoFrameContainer();
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(VideoFrameContainer)
+
+ public:
+ typedef layers::ImageContainer ImageContainer;
+ typedef layers::Image Image;
+
+ VideoFrameContainer(MediaDecoderOwner* aOwner,
+ already_AddRefed<ImageContainer> aContainer);
+
+ void SetCurrentFrame(const gfx::IntSize& aIntrinsicSize, Image* aImage,
+ const TimeStamp& aTargetTime);
+ // Returns the last principalHandle we notified mElement about.
+ PrincipalHandle GetLastPrincipalHandle();
+ PrincipalHandle GetLastPrincipalHandleLocked() MOZ_REQUIRES(mMutex);
+ // We will notify mElement that aPrincipalHandle has been applied when all
+ // FrameIDs prior to aFrameID have been flushed out.
+ // aFrameID is ignored if aPrincipalHandle already is our pending
+ // principalHandle.
+ void UpdatePrincipalHandleForFrameID(const PrincipalHandle& aPrincipalHandle,
+ const ImageContainer::FrameID& aFrameID);
+ void UpdatePrincipalHandleForFrameIDLocked(
+ const PrincipalHandle& aPrincipalHandle,
+ const ImageContainer::FrameID& aFrameID) MOZ_REQUIRES(mMutex);
+ void SetCurrentFrames(
+ const gfx::IntSize& aIntrinsicSize,
+ const nsTArray<ImageContainer::NonOwningImage>& aImages);
+ void ClearCurrentFrame(const gfx::IntSize& aIntrinsicSize) {
+ SetCurrentFrames(aIntrinsicSize,
+ nsTArray<ImageContainer::NonOwningImage>());
+ }
+
+ // Make the current frame the only frame in the container, i.e. discard
+ // all future frames.
+ void ClearFutureFrames(TimeStamp aNow = TimeStamp::Now());
+ // Time in seconds by which the last painted video frame was late by.
+ // E.g. if the last painted frame should have been painted at time t,
+ // but was actually painted at t+n, this returns n in seconds. Threadsafe.
+ double GetFrameDelay();
+
+ // Clear any resources that are not immediately necessary.
+ void ClearCachedResources();
+
+ // Returns a new frame ID for SetCurrentFrames(). The client must either
+ // call this on only one thread or provide barriers. Do not use together
+ // with SetCurrentFrame().
+ ImageContainer::FrameID NewFrameID() { return ++mFrameID; }
+
+ // Call on main thread
+ enum { INVALIDATE_DEFAULT, INVALIDATE_FORCE };
+ void Invalidate() { InvalidateWithFlags(INVALIDATE_DEFAULT); }
+ void InvalidateWithFlags(uint32_t aFlags);
+ ImageContainer* GetImageContainer();
+ void ForgetElement() { mOwner = nullptr; }
+
+ uint32_t GetDroppedImageCount() {
+ MutexAutoLock lock(mMutex);
+ return mImageContainer->GetDroppedImageCount();
+ }
+
+ Maybe<gfx::IntSize> CurrentIntrinsicSize() {
+ MutexAutoLock lock(mMutex);
+ return mIntrinsicSize;
+ }
+
+ protected:
+ void SetCurrentFramesLocked(
+ const gfx::IntSize& aIntrinsicSize,
+ const nsTArray<ImageContainer::NonOwningImage>& aImages)
+ MOZ_REQUIRES(mMutex);
+
+ // Non-addreffed pointer to the owner. The owner calls ForgetElement
+ // to clear this reference when the owner is destroyed.
+ MediaDecoderOwner* mOwner;
+ RefPtr<ImageContainer> mImageContainer;
+
+ struct {
+ // True when the Image size has changed since the last time Invalidate() was
+ // called. When set, the next call to Invalidate() will ensure that the
+ // frame is fully invalidated instead of just invalidating for the image
+ // change in the ImageLayer.
+ bool mImageSizeChanged = false;
+ // The main thread mirror of the member of the same name below, in case it
+ // has changed.
+ // Set to some size when the intrinsic size has been changed by
+ // SetCurrentFrame() since the last call to Invalidate().
+ // The next call to Invalidate() will recalculate and update the intrinsic
+ // size on the element, request a frame reflow and then reset this to
+ // Nothing.
+ Maybe<gfx::IntSize> mNewIntrinsicSize;
+ } mMainThreadState;
+
+ Mutex mMutex;
+ // The intrinsic size is the ideal size which we should render the
+ // ImageContainer's current Image at.
+ // This can differ from the Image's actual size when the media resource
+ // specifies that the Image should be stretched to have the correct aspect
+ // ratio.
+ Maybe<gfx::IntSize> mIntrinsicSize MOZ_GUARDED_BY(mMutex);
+ // We maintain our own mFrameID which is auto-incremented at every
+ // SetCurrentFrame() or NewFrameID() call.
+ ImageContainer::FrameID mFrameID;
+ // The last PrincipalHandle we notified mElement about.
+ PrincipalHandle mLastPrincipalHandle MOZ_GUARDED_BY(mMutex);
+ // The PrincipalHandle the client has notified us is changing with FrameID
+ // mFrameIDForPendingPrincipalHandle.
+ PrincipalHandle mPendingPrincipalHandle MOZ_GUARDED_BY(mMutex);
+ // The FrameID for which mPendingPrincipal is first valid.
+ ImageContainer::FrameID mFrameIDForPendingPrincipalHandle
+ MOZ_GUARDED_BY(mMutex);
+
+ const RefPtr<AbstractThread> mMainThread;
+};
+
+} // namespace mozilla
+
+#endif /* VIDEOFRAMECONTAINER_H_ */
diff --git a/dom/media/VideoFrameConverter.h b/dom/media/VideoFrameConverter.h
new file mode 100644
index 0000000000..a7f50b8220
--- /dev/null
+++ b/dom/media/VideoFrameConverter.h
@@ -0,0 +1,439 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef VideoFrameConverter_h
+#define VideoFrameConverter_h
+
+#include "ImageContainer.h"
+#include "ImageToI420.h"
+#include "Pacer.h"
+#include "PerformanceRecorder.h"
+#include "VideoSegment.h"
+#include "VideoUtils.h"
+#include "nsISupportsImpl.h"
+#include "nsThreadUtils.h"
+#include "jsapi/RTCStatsReport.h"
+#include "mozilla/TaskQueue.h"
+#include "mozilla/dom/ImageBitmapBinding.h"
+#include "mozilla/dom/ImageUtils.h"
+#include "api/video/video_frame.h"
+#include "common_video/include/video_frame_buffer_pool.h"
+#include "common_video/include/video_frame_buffer.h"
+
+// The number of frame buffers VideoFrameConverter may create before returning
+// errors.
+// Sometimes these are released synchronously but they can be forwarded all the
+// way to the encoder for asynchronous encoding. With a pool size of 5,
+// we allow 1 buffer for the current conversion, and 4 buffers to be queued at
+// the encoder.
+#define CONVERTER_BUFFER_POOL_SIZE 5
+
+namespace mozilla {
+
+static mozilla::LazyLogModule gVideoFrameConverterLog("VideoFrameConverter");
+
+// An async video frame format converter.
+//
+// Input is typically a MediaTrackListener driven by MediaTrackGraph.
+//
+// Output is passed through to VideoFrameConvertedEvent() whenever a frame is
+// converted.
+class VideoFrameConverter {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(VideoFrameConverter)
+
+ explicit VideoFrameConverter(
+ const dom::RTCStatsTimestampMaker& aTimestampMaker)
+ : mTimestampMaker(aTimestampMaker),
+ mTaskQueue(TaskQueue::Create(
+ GetMediaThreadPool(MediaThreadType::WEBRTC_WORKER),
+ "VideoFrameConverter")),
+ mPacer(MakeAndAddRef<Pacer<FrameToProcess>>(
+ mTaskQueue, TimeDuration::FromSeconds(1))),
+ mBufferPool(false, CONVERTER_BUFFER_POOL_SIZE) {
+ MOZ_COUNT_CTOR(VideoFrameConverter);
+
+ mPacingListener = mPacer->PacedItemEvent().Connect(
+ mTaskQueue, [self = RefPtr<VideoFrameConverter>(this), this](
+ FrameToProcess aFrame, TimeStamp aTime) {
+ QueueForProcessing(std::move(aFrame.mImage), aTime, aFrame.mSize,
+ aFrame.mForceBlack);
+ });
+ }
+
+ void QueueVideoChunk(const VideoChunk& aChunk, bool aForceBlack) {
+ gfx::IntSize size = aChunk.mFrame.GetIntrinsicSize();
+ if (size.width == 0 || size.height == 0) {
+ return;
+ }
+
+ TimeStamp t = aChunk.mTimeStamp;
+ MOZ_ASSERT(!t.IsNull());
+
+ mPacer->Enqueue(
+ FrameToProcess(aChunk.mFrame.GetImage(), t, size, aForceBlack), t);
+ }
+
+ /**
+ * An active VideoFrameConverter actively converts queued video frames.
+ * While inactive, we keep track of the frame most recently queued for
+ * processing, so it can be immediately sent out once activated.
+ */
+ void SetActive(bool aActive) {
+ MOZ_ALWAYS_SUCCEEDS(mTaskQueue->Dispatch(NS_NewRunnableFunction(
+ __func__, [self = RefPtr<VideoFrameConverter>(this), this, aActive,
+ time = TimeStamp::Now()] {
+ if (mActive == aActive) {
+ return;
+ }
+ MOZ_LOG(gVideoFrameConverterLog, LogLevel::Debug,
+ ("VideoFrameConverter %p is now %s", this,
+ aActive ? "active" : "inactive"));
+ mActive = aActive;
+ if (aActive && mLastFrameQueuedForProcessing.Serial() != -2) {
+ // After activating, we re-process the last image that was queued
+ // for processing so it can be immediately sent.
+ mLastFrameQueuedForProcessing.mTime = time;
+
+ MOZ_ALWAYS_SUCCEEDS(mTaskQueue->Dispatch(
+ NewRunnableMethod<StoreCopyPassByLRef<FrameToProcess>>(
+ "VideoFrameConverter::ProcessVideoFrame", this,
+ &VideoFrameConverter::ProcessVideoFrame,
+ mLastFrameQueuedForProcessing)));
+ }
+ })));
+ }
+
+ void SetTrackEnabled(bool aTrackEnabled) {
+ MOZ_ALWAYS_SUCCEEDS(mTaskQueue->Dispatch(NS_NewRunnableFunction(
+ __func__, [self = RefPtr<VideoFrameConverter>(this), this,
+ aTrackEnabled, time = TimeStamp::Now()] {
+ if (mTrackEnabled == aTrackEnabled) {
+ return;
+ }
+ MOZ_LOG(gVideoFrameConverterLog, LogLevel::Debug,
+ ("VideoFrameConverter %p Track is now %s", this,
+ aTrackEnabled ? "enabled" : "disabled"));
+ mTrackEnabled = aTrackEnabled;
+ if (!aTrackEnabled) {
+ // After disabling we immediately send a frame as black, so it can
+ // be seen quickly, even if no frames are flowing. If no frame has
+ // been queued for processing yet, we use the FrameToProcess default
+ // size (640x480).
+ mLastFrameQueuedForProcessing.mTime = time;
+ mLastFrameQueuedForProcessing.mForceBlack = true;
+ mLastFrameQueuedForProcessing.mImage = nullptr;
+
+ MOZ_ALWAYS_SUCCEEDS(mTaskQueue->Dispatch(
+ NewRunnableMethod<StoreCopyPassByLRef<FrameToProcess>>(
+ "VideoFrameConverter::ProcessVideoFrame", this,
+ &VideoFrameConverter::ProcessVideoFrame,
+ mLastFrameQueuedForProcessing)));
+ }
+ })));
+ }
+
+ void SetTrackingId(TrackingId aTrackingId) {
+ MOZ_ALWAYS_SUCCEEDS(mTaskQueue->Dispatch(NS_NewRunnableFunction(
+ __func__, [self = RefPtr<VideoFrameConverter>(this), this,
+ id = std::move(aTrackingId)]() mutable {
+ mTrackingId = Some(std::move(id));
+ })));
+ }
+
+ void Shutdown() {
+ mPacer->Shutdown()->Then(mTaskQueue, __func__,
+ [self = RefPtr<VideoFrameConverter>(this), this] {
+ mPacingListener.DisconnectIfExists();
+ mBufferPool.Release();
+ mLastFrameQueuedForProcessing = FrameToProcess();
+ mLastFrameConverted = Nothing();
+ });
+ }
+
+ MediaEventSourceExc<webrtc::VideoFrame>& VideoFrameConvertedEvent() {
+ return mVideoFrameConvertedEvent;
+ }
+
+ protected:
+ struct FrameToProcess {
+ FrameToProcess() = default;
+
+ FrameToProcess(RefPtr<layers::Image> aImage, TimeStamp aTime,
+ gfx::IntSize aSize, bool aForceBlack)
+ : mImage(std::move(aImage)),
+ mTime(aTime),
+ mSize(aSize),
+ mForceBlack(aForceBlack) {}
+
+ RefPtr<layers::Image> mImage;
+ TimeStamp mTime = TimeStamp::Now();
+ gfx::IntSize mSize = gfx::IntSize(640, 480);
+ bool mForceBlack = false;
+
+ int32_t Serial() const {
+ if (mForceBlack) {
+ // Set the last-img check to indicate black.
+ // -1 is not a guaranteed invalid serial. See bug 1262134.
+ return -1;
+ }
+ if (!mImage) {
+ // Set the last-img check to indicate reset.
+ // -2 is not a guaranteed invalid serial. See bug 1262134.
+ return -2;
+ }
+ return mImage->GetSerial();
+ }
+ };
+
+ struct FrameConverted {
+ FrameConverted(webrtc::VideoFrame aFrame, int32_t aSerial)
+ : mFrame(std::move(aFrame)), mSerial(aSerial) {}
+
+ webrtc::VideoFrame mFrame;
+ int32_t mSerial;
+ };
+
+ MOZ_COUNTED_DTOR_VIRTUAL(VideoFrameConverter)
+
+ void VideoFrameConverted(webrtc::VideoFrame aVideoFrame, int32_t aSerial) {
+ MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
+
+ MOZ_LOG(
+ gVideoFrameConverterLog, LogLevel::Verbose,
+ ("VideoFrameConverter %p: Converted a frame. Diff from last: %.3fms",
+ this,
+ static_cast<double>(aVideoFrame.timestamp_us() -
+ (mLastFrameConverted
+ ? mLastFrameConverted->mFrame.timestamp_us()
+ : aVideoFrame.timestamp_us())) /
+ 1000));
+
+ // Check that time doesn't go backwards
+ MOZ_ASSERT_IF(mLastFrameConverted,
+ aVideoFrame.timestamp_us() >
+ mLastFrameConverted->mFrame.timestamp_us());
+
+ mLastFrameConverted = Some(FrameConverted(aVideoFrame, aSerial));
+
+ mVideoFrameConvertedEvent.Notify(std::move(aVideoFrame));
+ }
+
+ void QueueForProcessing(RefPtr<layers::Image> aImage, TimeStamp aTime,
+ gfx::IntSize aSize, bool aForceBlack) {
+ MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
+
+ FrameToProcess frame{std::move(aImage), aTime, aSize,
+ aForceBlack || !mTrackEnabled};
+
+ if (frame.mTime <= mLastFrameQueuedForProcessing.mTime) {
+ MOZ_LOG(
+ gVideoFrameConverterLog, LogLevel::Debug,
+ ("VideoFrameConverter %p: Dropping a frame because time did not "
+ "progress (%.3fs)",
+ this,
+ (mLastFrameQueuedForProcessing.mTime - frame.mTime).ToSeconds()));
+ return;
+ }
+
+ if (frame.Serial() == mLastFrameQueuedForProcessing.Serial()) {
+ // This is the same frame as the last one. We limit the same-frame rate to
+ // 1 second, and rewrite the time so the frame-gap is in whole seconds.
+ //
+ // The pacer only starts duplicating frames every second if there is no
+ // flow of frames into it. There are other reasons the same frame could
+ // repeat here, and at a shorter interval than one second. For instance
+ // after the sender is disabled (SetTrackEnabled) but there is still a
+ // flow of frames into the pacer. All disabled frames have the same
+ // serial.
+ if (int32_t diffSec = static_cast<int32_t>(
+ (frame.mTime - mLastFrameQueuedForProcessing.mTime).ToSeconds());
+ diffSec != 0) {
+ MOZ_LOG(
+ gVideoFrameConverterLog, LogLevel::Verbose,
+ ("VideoFrameConverter %p: Rewrote time interval for a duplicate "
+ "frame from %.3fs to %.3fs",
+ this,
+ (frame.mTime - mLastFrameQueuedForProcessing.mTime).ToSeconds(),
+ static_cast<float>(diffSec)));
+ frame.mTime = mLastFrameQueuedForProcessing.mTime +
+ TimeDuration::FromSeconds(diffSec);
+ } else {
+ MOZ_LOG(
+ gVideoFrameConverterLog, LogLevel::Verbose,
+ ("VideoFrameConverter %p: Dropping a duplicate frame because a "
+ "second hasn't passed (%.3fs)",
+ this,
+ (frame.mTime - mLastFrameQueuedForProcessing.mTime).ToSeconds()));
+ return;
+ }
+ }
+
+ mLastFrameQueuedForProcessing = std::move(frame);
+
+ if (!mActive) {
+ MOZ_LOG(
+ gVideoFrameConverterLog, LogLevel::Debug,
+ ("VideoFrameConverter %p: Ignoring a frame because we're inactive",
+ this));
+ return;
+ }
+
+ MOZ_ALWAYS_SUCCEEDS(mTaskQueue->Dispatch(
+ NewRunnableMethod<StoreCopyPassByLRef<FrameToProcess>>(
+ "VideoFrameConverter::ProcessVideoFrame", this,
+ &VideoFrameConverter::ProcessVideoFrame,
+ mLastFrameQueuedForProcessing)));
+ }
+
+ void ProcessVideoFrame(const FrameToProcess& aFrame) {
+ MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
+
+ if (aFrame.mTime < mLastFrameQueuedForProcessing.mTime) {
+ MOZ_LOG(
+ gVideoFrameConverterLog, LogLevel::Debug,
+ ("VideoFrameConverter %p: Dropping a frame that is %.3f seconds "
+ "behind latest",
+ this,
+ (mLastFrameQueuedForProcessing.mTime - aFrame.mTime).ToSeconds()));
+ return;
+ }
+
+ const webrtc::Timestamp time =
+ dom::RTCStatsTimestamp::FromMozTime(mTimestampMaker, aFrame.mTime)
+ .ToRealtime();
+
+ if (mLastFrameConverted &&
+ aFrame.Serial() == mLastFrameConverted->mSerial) {
+ // This is the same input frame as last time. Avoid a conversion.
+ webrtc::VideoFrame frame = mLastFrameConverted->mFrame;
+ frame.set_timestamp_us(time.us());
+ VideoFrameConverted(std::move(frame), mLastFrameConverted->mSerial);
+ return;
+ }
+
+ if (aFrame.mForceBlack) {
+ // Send a black image.
+ rtc::scoped_refptr<webrtc::I420Buffer> buffer =
+ mBufferPool.CreateI420Buffer(aFrame.mSize.width, aFrame.mSize.height);
+ if (!buffer) {
+ MOZ_DIAGNOSTIC_ASSERT(false,
+ "Buffers not leaving scope except for "
+ "reconfig, should never leak");
+ MOZ_LOG(gVideoFrameConverterLog, LogLevel::Warning,
+ ("VideoFrameConverter %p: Creating a buffer for a black video "
+ "frame failed",
+ this));
+ return;
+ }
+
+ MOZ_LOG(gVideoFrameConverterLog, LogLevel::Verbose,
+ ("VideoFrameConverter %p: Sending a black video frame", this));
+ webrtc::I420Buffer::SetBlack(buffer.get());
+
+ VideoFrameConverted(webrtc::VideoFrame::Builder()
+ .set_video_frame_buffer(buffer)
+ .set_timestamp_us(time.us())
+ .build(),
+ aFrame.Serial());
+ return;
+ }
+
+ if (!aFrame.mImage) {
+ // Don't send anything for null images.
+ return;
+ }
+
+ MOZ_ASSERT(aFrame.mImage->GetSize() == aFrame.mSize);
+
+ RefPtr<layers::PlanarYCbCrImage> image =
+ aFrame.mImage->AsPlanarYCbCrImage();
+ if (image) {
+ dom::ImageUtils utils(image);
+ if (utils.GetFormat() == dom::ImageBitmapFormat::YUV420P &&
+ image->GetData()) {
+ const layers::PlanarYCbCrData* data = image->GetData();
+ rtc::scoped_refptr<webrtc::I420BufferInterface> video_frame_buffer =
+ webrtc::WrapI420Buffer(
+ aFrame.mImage->GetSize().width, aFrame.mImage->GetSize().height,
+ data->mYChannel, data->mYStride, data->mCbChannel,
+ data->mCbCrStride, data->mCrChannel, data->mCbCrStride,
+ [image] { /* keep reference alive*/ });
+
+ MOZ_LOG(gVideoFrameConverterLog, LogLevel::Verbose,
+ ("VideoFrameConverter %p: Sending an I420 video frame", this));
+ VideoFrameConverted(webrtc::VideoFrame::Builder()
+ .set_video_frame_buffer(video_frame_buffer)
+ .set_timestamp_us(time.us())
+ .build(),
+ aFrame.Serial());
+ return;
+ }
+ }
+
+ rtc::scoped_refptr<webrtc::I420Buffer> buffer =
+ mBufferPool.CreateI420Buffer(aFrame.mSize.width, aFrame.mSize.height);
+ if (!buffer) {
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ ++mFramesDropped;
+#endif
+ MOZ_DIAGNOSTIC_ASSERT(mFramesDropped <= 100, "Buffers must be leaking");
+ MOZ_LOG(gVideoFrameConverterLog, LogLevel::Warning,
+ ("VideoFrameConverter %p: Creating a buffer failed", this));
+ return;
+ }
+
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ mFramesDropped = 0;
+#endif
+ PerformanceRecorder<CopyVideoStage> rec(
+ "VideoFrameConverter::ConvertToI420"_ns, *mTrackingId, buffer->width(),
+ buffer->height());
+ nsresult rv =
+ ConvertToI420(aFrame.mImage, buffer->MutableDataY(), buffer->StrideY(),
+ buffer->MutableDataU(), buffer->StrideU(),
+ buffer->MutableDataV(), buffer->StrideV());
+
+ if (NS_FAILED(rv)) {
+ MOZ_LOG(gVideoFrameConverterLog, LogLevel::Warning,
+ ("VideoFrameConverter %p: Image conversion failed", this));
+ return;
+ }
+ rec.Record();
+
+ VideoFrameConverted(webrtc::VideoFrame::Builder()
+ .set_video_frame_buffer(buffer)
+ .set_timestamp_us(time.us())
+ .build(),
+ aFrame.Serial());
+ }
+
+ public:
+ const dom::RTCStatsTimestampMaker mTimestampMaker;
+
+ const RefPtr<TaskQueue> mTaskQueue;
+
+ protected:
+ // Used to pace future frames close to their rendering-time. Thread-safe.
+ const RefPtr<Pacer<FrameToProcess>> mPacer;
+
+ MediaEventProducerExc<webrtc::VideoFrame> mVideoFrameConvertedEvent;
+
+ // Accessed only from mTaskQueue.
+ MediaEventListener mPacingListener;
+ webrtc::VideoFrameBufferPool mBufferPool;
+ FrameToProcess mLastFrameQueuedForProcessing;
+ Maybe<FrameConverted> mLastFrameConverted;
+ bool mActive = false;
+ bool mTrackEnabled = true;
+ Maybe<TrackingId> mTrackingId;
+#ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ size_t mFramesDropped = 0;
+#endif
+};
+
+} // namespace mozilla
+
+#endif // VideoFrameConverter_h
diff --git a/dom/media/VideoLimits.h b/dom/media/VideoLimits.h
new file mode 100644
index 0000000000..aaa8753d4b
--- /dev/null
+++ b/dom/media/VideoLimits.h
@@ -0,0 +1,21 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef VideoLimits_h
+#define VideoLimits_h
+
+namespace mozilla {
+
+// The maximum height and width of the video. Used for
+// sanitizing the memory allocation of video frame buffers.
+// The maximum resolution we anticipate encountering in the
+// wild is 2160p (UHD "4K") or 4320p - 7680x4320 pixels for VR.
+static const int32_t MAX_VIDEO_WIDTH = 8192;
+static const int32_t MAX_VIDEO_HEIGHT = 4608;
+
+} // namespace mozilla
+
+#endif // VideoLimits_h
diff --git a/dom/media/VideoOutput.h b/dom/media/VideoOutput.h
new file mode 100644
index 0000000000..b07290fb3d
--- /dev/null
+++ b/dom/media/VideoOutput.h
@@ -0,0 +1,307 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef VideoOutput_h
+#define VideoOutput_h
+
+#include "MediaTrackListener.h"
+#include "VideoFrameContainer.h"
+
+namespace mozilla {
+
+static bool SetImageToBlackPixel(layers::PlanarYCbCrImage* aImage) {
+ uint8_t blackPixel[] = {0x10, 0x80, 0x80};
+
+ layers::PlanarYCbCrData data;
+ data.mYChannel = blackPixel;
+ data.mCbChannel = blackPixel + 1;
+ data.mCrChannel = blackPixel + 2;
+ data.mYStride = data.mCbCrStride = 1;
+ data.mPictureRect = gfx::IntRect(0, 0, 1, 1);
+ data.mYUVColorSpace = gfx::YUVColorSpace::BT601;
+ // This could be made FULL once bug 1568745 is complete. A black pixel being
+ // 0x00, 0x80, 0x80
+ data.mColorRange = gfx::ColorRange::LIMITED;
+
+ return aImage->CopyData(data);
+}
+
+class VideoOutput : public DirectMediaTrackListener {
+ protected:
+ typedef layers::Image Image;
+ typedef layers::ImageContainer ImageContainer;
+ typedef layers::ImageContainer::FrameID FrameID;
+ typedef layers::ImageContainer::ProducerID ProducerID;
+
+ virtual ~VideoOutput() = default;
+
+ void DropPastFrames() {
+ TimeStamp now = TimeStamp::Now();
+ size_t nrChunksInPast = 0;
+ for (const auto& idChunkPair : mFrames) {
+ const VideoChunk& chunk = idChunkPair.second;
+ if (chunk.mTimeStamp > now) {
+ break;
+ }
+ ++nrChunksInPast;
+ }
+ if (nrChunksInPast > 1) {
+ // We need to keep one frame that starts in the past, because it only ends
+ // when the next frame starts (which also needs to be in the past for it
+ // to drop).
+ mFrames.RemoveElementsAt(0, nrChunksInPast - 1);
+ }
+ }
+
+ void SendFramesEnsureLocked() {
+ mMutex.AssertCurrentThreadOwns();
+ SendFrames();
+ }
+
+ void SendFrames() {
+ DropPastFrames();
+
+ if (mFrames.IsEmpty()) {
+ return;
+ }
+
+ if (!mEnabled && mDisabledBlackImageSent) {
+ return;
+ }
+
+ // Collect any new frames produced in this iteration.
+ AutoTArray<ImageContainer::NonOwningImage, 16> images;
+ PrincipalHandle lastPrincipalHandle = PRINCIPAL_HANDLE_NONE;
+
+ for (const auto& idChunkPair : mFrames) {
+ ImageContainer::FrameID frameId = idChunkPair.first;
+ const VideoChunk& chunk = idChunkPair.second;
+ const VideoFrame& frame = chunk.mFrame;
+ Image* image = frame.GetImage();
+ if (frame.GetForceBlack() || !mEnabled) {
+ if (!mBlackImage) {
+ RefPtr<Image> blackImage = mVideoFrameContainer->GetImageContainer()
+ ->CreatePlanarYCbCrImage();
+ if (blackImage) {
+ // Sets the image to a single black pixel, which will be scaled to
+ // fill the rendered size.
+ if (SetImageToBlackPixel(blackImage->AsPlanarYCbCrImage())) {
+ mBlackImage = blackImage;
+ }
+ }
+ }
+ if (mBlackImage) {
+ image = mBlackImage;
+ }
+ }
+ if (!image) {
+ // We ignore null images.
+ continue;
+ }
+ images.AppendElement(ImageContainer::NonOwningImage(
+ image, chunk.mTimeStamp, frameId, mProducerID));
+
+ lastPrincipalHandle = chunk.GetPrincipalHandle();
+
+ if (!mEnabled && mBlackImage) {
+ MOZ_ASSERT(images.Length() == 1);
+ mDisabledBlackImageSent = true;
+ break;
+ }
+ }
+
+ if (images.IsEmpty()) {
+ // This could happen if the only images in mFrames are null. We leave the
+ // container at the current frame in this case.
+ mVideoFrameContainer->ClearFutureFrames();
+ return;
+ }
+
+ bool principalHandleChanged =
+ lastPrincipalHandle != PRINCIPAL_HANDLE_NONE &&
+ lastPrincipalHandle != mVideoFrameContainer->GetLastPrincipalHandle();
+
+ if (principalHandleChanged) {
+ mVideoFrameContainer->UpdatePrincipalHandleForFrameID(
+ lastPrincipalHandle, images.LastElement().mFrameID);
+ }
+
+ mVideoFrameContainer->SetCurrentFrames(
+ mFrames[0].second.mFrame.GetIntrinsicSize(), images);
+ mMainThread->Dispatch(NewRunnableMethod("VideoFrameContainer::Invalidate",
+ mVideoFrameContainer,
+ &VideoFrameContainer::Invalidate));
+ }
+
+ FrameID NewFrameID() {
+ mMutex.AssertCurrentThreadOwns();
+ return ++mFrameID;
+ }
+
+ public:
+ VideoOutput(VideoFrameContainer* aContainer, AbstractThread* aMainThread)
+ : mMutex("VideoOutput::mMutex"),
+ mVideoFrameContainer(aContainer),
+ mMainThread(aMainThread) {}
+ void NotifyRealtimeTrackData(MediaTrackGraph* aGraph, TrackTime aTrackOffset,
+ const MediaSegment& aMedia) override {
+ MOZ_ASSERT(aMedia.GetType() == MediaSegment::VIDEO);
+ const VideoSegment& video = static_cast<const VideoSegment&>(aMedia);
+ MutexAutoLock lock(mMutex);
+ for (VideoSegment::ConstChunkIterator i(video); !i.IsEnded(); i.Next()) {
+ if (!mLastFrameTime.IsNull() && i->mTimeStamp < mLastFrameTime) {
+ // Time can go backwards if the source is a captured MediaDecoder and
+ // it seeks, as the previously buffered frames would stretch into the
+ // future. If this happens, we clear the buffered frames and start over.
+ mFrames.ClearAndRetainStorage();
+ }
+ mFrames.AppendElement(std::make_pair(NewFrameID(), *i));
+ mLastFrameTime = i->mTimeStamp;
+ }
+
+ SendFramesEnsureLocked();
+ }
+ void NotifyRemoved(MediaTrackGraph* aGraph) override {
+ // Doesn't need locking by mMutex, since the direct listener is removed from
+ // the track before we get notified.
+ if (mFrames.Length() <= 1) {
+ // The compositor has already received the last frame.
+ mFrames.ClearAndRetainStorage();
+ mVideoFrameContainer->ClearFutureFrames();
+ return;
+ }
+
+ // The compositor has multiple frames. ClearFutureFrames() would only retain
+ // the first as that's normally the current one. We however stop doing
+ // SetCurrentFrames() once we've received the last frame in a track, so
+ // there might be old frames lingering. We'll find the current one and
+ // re-send that.
+ DropPastFrames();
+ mFrames.RemoveLastElements(mFrames.Length() - 1);
+ SendFrames();
+ mFrames.ClearAndRetainStorage();
+ }
+ void NotifyEnded(MediaTrackGraph* aGraph) override {
+ // Doesn't need locking by mMutex, since for the track to end, it must have
+ // been ended by the source, meaning that the source won't append more data.
+ if (mFrames.IsEmpty()) {
+ return;
+ }
+
+ // Re-send only the last one to the compositor.
+ mFrames.RemoveElementsAt(0, mFrames.Length() - 1);
+ SendFrames();
+ mFrames.ClearAndRetainStorage();
+ }
+ void NotifyEnabledStateChanged(MediaTrackGraph* aGraph,
+ bool aEnabled) override {
+ MutexAutoLock lock(mMutex);
+ mEnabled = aEnabled;
+ DropPastFrames();
+ if (mEnabled) {
+ mDisabledBlackImageSent = false;
+ }
+ if (!mEnabled || mFrames.Length() > 1) {
+ // Re-send frames when disabling, as new frames may not arrive. When
+ // enabling we keep them black until new frames arrive, or re-send if we
+ // already have frames in the future. If we're disabling and there are no
+ // frames available yet, we invent one. Unfortunately with a hardcoded
+ // size.
+ //
+ // Since mEnabled will affect whether
+ // frames are real, or black, we assign new FrameIDs whenever we re-send
+ // frames after an mEnabled change.
+ for (auto& idChunkPair : mFrames) {
+ idChunkPair.first = NewFrameID();
+ }
+ if (mFrames.IsEmpty()) {
+ VideoSegment v;
+ v.AppendFrame(nullptr, gfx::IntSize(640, 480), PRINCIPAL_HANDLE_NONE,
+ true, TimeStamp::Now());
+ mFrames.AppendElement(std::make_pair(NewFrameID(), *v.GetLastChunk()));
+ }
+ SendFramesEnsureLocked();
+ }
+ }
+
+ Mutex mMutex MOZ_UNANNOTATED;
+ TimeStamp mLastFrameTime;
+ // Once the frame is forced to black, we initialize mBlackImage for use in any
+ // following forced-black frames.
+ RefPtr<Image> mBlackImage;
+ // True once mBlackImage has been sent due to mEnabled being false.
+ bool mDisabledBlackImageSent = false;
+ bool mEnabled = true;
+ // This array is accessed from both the direct video thread, and the graph
+ // thread. Protected by mMutex.
+ nsTArray<std::pair<ImageContainer::FrameID, VideoChunk>> mFrames;
+ // Accessed from both the direct video thread, and the graph thread. Protected
+ // by mMutex.
+ FrameID mFrameID = 0;
+ const RefPtr<VideoFrameContainer> mVideoFrameContainer;
+ const RefPtr<AbstractThread> mMainThread;
+ const ProducerID mProducerID = ImageContainer::AllocateProducerID();
+};
+
+/**
+ * This listener observes the first video frame to arrive with a non-empty size,
+ * and renders it to its VideoFrameContainer.
+ */
+class FirstFrameVideoOutput : public VideoOutput {
+ public:
+ FirstFrameVideoOutput(VideoFrameContainer* aContainer,
+ AbstractThread* aMainThread)
+ : VideoOutput(aContainer, aMainThread) {
+ MOZ_ASSERT(NS_IsMainThread());
+ }
+
+ // NB that this overrides VideoOutput::NotifyRealtimeTrackData, so we can
+ // filter out all frames but the first one with a real size. This allows us to
+ // later re-use the logic in VideoOutput for rendering that frame.
+ void NotifyRealtimeTrackData(MediaTrackGraph* aGraph, TrackTime aTrackOffset,
+ const MediaSegment& aMedia) override {
+ MOZ_ASSERT(aMedia.GetType() == MediaSegment::VIDEO);
+
+ if (mInitialSizeFound) {
+ return;
+ }
+
+ const VideoSegment& video = static_cast<const VideoSegment&>(aMedia);
+ for (VideoSegment::ConstChunkIterator c(video); !c.IsEnded(); c.Next()) {
+ if (c->mFrame.GetIntrinsicSize() != gfx::IntSize(0, 0)) {
+ mInitialSizeFound = true;
+
+ mMainThread->Dispatch(NS_NewRunnableFunction(
+ "FirstFrameVideoOutput::FirstFrameRenderedSetter",
+ [self = RefPtr<FirstFrameVideoOutput>(this)] {
+ self->mFirstFrameRendered = true;
+ }));
+
+ // Pick the first frame and run it through the rendering code.
+ VideoSegment segment;
+ segment.AppendFrame(do_AddRef(c->mFrame.GetImage()),
+ c->mFrame.GetIntrinsicSize(),
+ c->mFrame.GetPrincipalHandle(),
+ c->mFrame.GetForceBlack(), c->mTimeStamp);
+ VideoOutput::NotifyRealtimeTrackData(aGraph, aTrackOffset, segment);
+ return;
+ }
+ }
+ }
+
+ // Main thread only.
+ Watchable<bool> mFirstFrameRendered = {
+ false, "FirstFrameVideoOutput::mFirstFrameRendered"};
+
+ private:
+ // Whether a frame with a concrete size has been received. May only be
+ // accessed on the MTG's appending thread. (this is a direct listener so we
+ // get called by whoever is producing this track's data)
+ bool mInitialSizeFound = false;
+};
+
+} // namespace mozilla
+
+#endif // VideoOutput_h
diff --git a/dom/media/VideoPlaybackQuality.cpp b/dom/media/VideoPlaybackQuality.cpp
new file mode 100644
index 0000000000..66136769c5
--- /dev/null
+++ b/dom/media/VideoPlaybackQuality.cpp
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "VideoPlaybackQuality.h"
+
+#include "mozilla/dom/HTMLMediaElement.h"
+#include "mozilla/dom/VideoPlaybackQualityBinding.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsWrapperCache.h"
+
+namespace mozilla::dom {
+
+VideoPlaybackQuality::VideoPlaybackQuality(HTMLMediaElement* aElement,
+ DOMHighResTimeStamp aCreationTime,
+ uint32_t aTotalFrames,
+ uint32_t aDroppedFrames)
+ : mElement(aElement),
+ mCreationTime(aCreationTime),
+ mTotalFrames(aTotalFrames),
+ mDroppedFrames(aDroppedFrames) {}
+
+HTMLMediaElement* VideoPlaybackQuality::GetParentObject() const {
+ return mElement;
+}
+
+JSObject* VideoPlaybackQuality::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return VideoPlaybackQuality_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(VideoPlaybackQuality, mElement)
+
+} // namespace mozilla::dom
diff --git a/dom/media/VideoPlaybackQuality.h b/dom/media/VideoPlaybackQuality.h
new file mode 100644
index 0000000000..76ca38de7b
--- /dev/null
+++ b/dom/media/VideoPlaybackQuality.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_VideoPlaybackQuality_h_
+#define mozilla_dom_VideoPlaybackQuality_h_
+
+#include "mozilla/dom/HTMLMediaElement.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsDOMNavigationTiming.h"
+#include "nsWrapperCache.h"
+
+namespace mozilla::dom {
+
+class VideoPlaybackQuality final : public nsWrapperCache {
+ public:
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(VideoPlaybackQuality)
+ NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(VideoPlaybackQuality)
+
+ VideoPlaybackQuality(HTMLMediaElement* aElement,
+ DOMHighResTimeStamp aCreationTime, uint32_t aTotalFrames,
+ uint32_t aDroppedFrames);
+
+ HTMLMediaElement* GetParentObject() const;
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ DOMHighResTimeStamp CreationTime() const { return mCreationTime; }
+
+ uint32_t TotalVideoFrames() const { return mTotalFrames; }
+
+ uint32_t DroppedVideoFrames() const { return mDroppedFrames; }
+
+ private:
+ ~VideoPlaybackQuality() = default;
+
+ RefPtr<HTMLMediaElement> mElement;
+ DOMHighResTimeStamp mCreationTime;
+ uint32_t mTotalFrames;
+ uint32_t mDroppedFrames;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_VideoPlaybackQuality_h_
diff --git a/dom/media/VideoSegment.cpp b/dom/media/VideoSegment.cpp
new file mode 100644
index 0000000000..c34a5fd553
--- /dev/null
+++ b/dom/media/VideoSegment.cpp
@@ -0,0 +1,108 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "VideoSegment.h"
+
+#include "gfx2DGlue.h"
+#include "ImageContainer.h"
+#include "VideoUtils.h"
+#include "mozilla/UniquePtr.h"
+
+namespace mozilla {
+
+using namespace layers;
+
+VideoFrame::VideoFrame(already_AddRefed<Image> aImage,
+ const gfx::IntSize& aIntrinsicSize)
+ : mImage(aImage),
+ mIntrinsicSize(aIntrinsicSize),
+ mForceBlack(false),
+ mPrincipalHandle(PRINCIPAL_HANDLE_NONE) {}
+
+VideoFrame::VideoFrame()
+ : mIntrinsicSize(0, 0),
+ mForceBlack(false),
+ mPrincipalHandle(PRINCIPAL_HANDLE_NONE) {}
+
+VideoFrame::~VideoFrame() = default;
+
+void VideoFrame::SetNull() {
+ mImage = nullptr;
+ mIntrinsicSize = gfx::IntSize(0, 0);
+ mPrincipalHandle = PRINCIPAL_HANDLE_NONE;
+}
+
+void VideoFrame::TakeFrom(VideoFrame* aFrame) {
+ mImage = std::move(aFrame->mImage);
+ mIntrinsicSize = aFrame->mIntrinsicSize;
+ mForceBlack = aFrame->GetForceBlack();
+ mPrincipalHandle = aFrame->mPrincipalHandle;
+}
+
+/* static */
+already_AddRefed<Image> VideoFrame::CreateBlackImage(
+ const gfx::IntSize& aSize) {
+ RefPtr<ImageContainer> container =
+ MakeAndAddRef<ImageContainer>(ImageContainer::ASYNCHRONOUS);
+ RefPtr<PlanarYCbCrImage> image = container->CreatePlanarYCbCrImage();
+ if (!image) {
+ return nullptr;
+ }
+
+ gfx::IntSize cbcrSize((aSize.width + 1) / 2, (aSize.height + 1) / 2);
+ int yLen = aSize.width * aSize.height;
+ int cbcrLen = cbcrSize.width * cbcrSize.height;
+
+ // Generate a black image.
+ auto frame = MakeUnique<uint8_t[]>(yLen + 2 * cbcrLen);
+ // Fill Y plane.
+ memset(frame.get(), 0x10, yLen);
+ // Fill Cb/Cr planes.
+ memset(frame.get() + yLen, 0x80, 2 * cbcrLen);
+
+ layers::PlanarYCbCrData data;
+ data.mYChannel = frame.get();
+ data.mYStride = aSize.width;
+ data.mCbCrStride = cbcrSize.width;
+ data.mCbChannel = frame.get() + yLen;
+ data.mCrChannel = data.mCbChannel + cbcrLen;
+ data.mPictureRect = gfx::IntRect(0, 0, aSize.width, aSize.height);
+ data.mStereoMode = StereoMode::MONO;
+ data.mYUVColorSpace = gfx::YUVColorSpace::BT601;
+ // This could be made FULL once bug 1568745 is complete. A black pixel being
+ // 0x00, 0x80, 0x80
+ data.mColorRange = gfx::ColorRange::LIMITED;
+ data.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT;
+
+ // Copies data, so we can free data.
+ if (!image->CopyData(data)) {
+ return nullptr;
+ }
+
+ return image.forget();
+}
+
+void VideoSegment::AppendFrame(already_AddRefed<Image>&& aImage,
+ const IntSize& aIntrinsicSize,
+ const PrincipalHandle& aPrincipalHandle,
+ bool aForceBlack, TimeStamp aTimeStamp) {
+ VideoChunk* chunk = AppendChunk(0);
+ chunk->mTimeStamp = aTimeStamp;
+ VideoFrame frame(std::move(aImage), aIntrinsicSize);
+ MOZ_ASSERT_IF(!IsNull(), !aTimeStamp.IsNull());
+ frame.SetForceBlack(aForceBlack);
+ frame.SetPrincipalHandle(aPrincipalHandle);
+ chunk->mFrame.TakeFrom(&frame);
+}
+
+VideoSegment::VideoSegment()
+ : MediaSegmentBase<VideoSegment, VideoChunk>(VIDEO) {}
+
+VideoSegment::VideoSegment(VideoSegment&& aSegment)
+ : MediaSegmentBase<VideoSegment, VideoChunk>(std::move(aSegment)) {}
+
+VideoSegment::~VideoSegment() = default;
+
+} // namespace mozilla
diff --git a/dom/media/VideoSegment.h b/dom/media/VideoSegment.h
new file mode 100644
index 0000000000..5421112c9f
--- /dev/null
+++ b/dom/media/VideoSegment.h
@@ -0,0 +1,187 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_VIDEOSEGMENT_H_
+#define MOZILLA_VIDEOSEGMENT_H_
+
+#include "MediaSegment.h"
+#include "nsCOMPtr.h"
+#include "gfxPoint.h"
+#include "ImageContainer.h"
+
+namespace mozilla {
+
+namespace layers {
+class Image;
+} // namespace layers
+
+class VideoFrame {
+ public:
+ typedef mozilla::layers::Image Image;
+
+ VideoFrame(already_AddRefed<Image> aImage,
+ const gfx::IntSize& aIntrinsicSize);
+ VideoFrame();
+ ~VideoFrame();
+
+ bool operator==(const VideoFrame& aFrame) const {
+ return mIntrinsicSize == aFrame.mIntrinsicSize &&
+ mForceBlack == aFrame.mForceBlack &&
+ ((mForceBlack && aFrame.mForceBlack) || mImage == aFrame.mImage) &&
+ mPrincipalHandle == aFrame.mPrincipalHandle;
+ }
+ bool operator!=(const VideoFrame& aFrame) const {
+ return !operator==(aFrame);
+ }
+
+ Image* GetImage() const { return mImage; }
+ void SetForceBlack(bool aForceBlack) { mForceBlack = aForceBlack; }
+ bool GetForceBlack() const { return mForceBlack; }
+ void SetPrincipalHandle(PrincipalHandle aPrincipalHandle) {
+ mPrincipalHandle = std::forward<PrincipalHandle>(aPrincipalHandle);
+ }
+ const PrincipalHandle& GetPrincipalHandle() const { return mPrincipalHandle; }
+ const gfx::IntSize& GetIntrinsicSize() const { return mIntrinsicSize; }
+ void SetNull();
+ void TakeFrom(VideoFrame* aFrame);
+
+ // Create a planar YCbCr black image.
+ static already_AddRefed<Image> CreateBlackImage(const gfx::IntSize& aSize);
+
+ protected:
+ // mImage can be null to indicate "no video" (aka "empty frame"). It can
+ // still have an intrinsic size in this case.
+ RefPtr<Image> mImage;
+ // The desired size to render the video frame at.
+ gfx::IntSize mIntrinsicSize;
+ bool mForceBlack;
+ // principalHandle for the image in this frame.
+ // This can be compared to an nsIPrincipal when back on main thread.
+ PrincipalHandle mPrincipalHandle;
+};
+
+struct VideoChunk {
+ void SliceTo(TrackTime aStart, TrackTime aEnd) {
+ NS_ASSERTION(aStart >= 0 && aStart < aEnd && aEnd <= mDuration,
+ "Slice out of bounds");
+ mDuration = aEnd - aStart;
+ }
+ TrackTime GetDuration() const { return mDuration; }
+ bool CanCombineWithFollowing(const VideoChunk& aOther) const {
+ return aOther.mFrame == mFrame;
+ }
+ bool IsNull() const { return !mFrame.GetImage(); }
+ void SetNull(TrackTime aDuration) {
+ mDuration = aDuration;
+ mFrame.SetNull();
+ mTimeStamp = TimeStamp();
+ }
+ void SetForceBlack(bool aForceBlack) { mFrame.SetForceBlack(aForceBlack); }
+
+ size_t SizeOfExcludingThisIfUnshared(MallocSizeOf aMallocSizeOf) const {
+ // Future:
+ // - mFrame
+ return 0;
+ }
+
+ const PrincipalHandle& GetPrincipalHandle() const {
+ return mFrame.GetPrincipalHandle();
+ }
+
+ TrackTime mDuration;
+ VideoFrame mFrame;
+ TimeStamp mTimeStamp;
+};
+
+class VideoSegment : public MediaSegmentBase<VideoSegment, VideoChunk> {
+ public:
+ typedef mozilla::layers::Image Image;
+ typedef mozilla::gfx::IntSize IntSize;
+
+ VideoSegment();
+ VideoSegment(VideoSegment&& aSegment);
+
+ VideoSegment(const VideoSegment&) = delete;
+ VideoSegment& operator=(const VideoSegment&) = delete;
+
+ ~VideoSegment();
+
+ void AppendFrame(already_AddRefed<Image>&& aImage,
+ const IntSize& aIntrinsicSize,
+ const PrincipalHandle& aPrincipalHandle,
+ bool aForceBlack = false,
+ TimeStamp aTimeStamp = TimeStamp::Now());
+ void ExtendLastFrameBy(TrackTime aDuration) {
+ if (aDuration <= 0) {
+ return;
+ }
+ if (mChunks.IsEmpty()) {
+ mChunks.AppendElement()->SetNull(aDuration);
+ } else {
+ mChunks[mChunks.Length() - 1].mDuration += aDuration;
+ }
+ mDuration += aDuration;
+ }
+ const VideoFrame* GetLastFrame(TrackTime* aStart = nullptr) {
+ VideoChunk* c = GetLastChunk();
+ if (!c) {
+ return nullptr;
+ }
+ if (aStart) {
+ *aStart = mDuration - c->mDuration;
+ }
+ return &c->mFrame;
+ }
+ VideoChunk* FindChunkContaining(const TimeStamp& aTime) {
+ VideoChunk* previousChunk = nullptr;
+ for (VideoChunk& c : mChunks) {
+ if (c.mTimeStamp.IsNull()) {
+ continue;
+ }
+ if (c.mTimeStamp > aTime) {
+ return previousChunk;
+ }
+ previousChunk = &c;
+ }
+ return previousChunk;
+ }
+ void ForgetUpToTime(const TimeStamp& aTime) {
+ VideoChunk* chunk = FindChunkContaining(aTime);
+ if (!chunk) {
+ return;
+ }
+ TrackTime duration = 0;
+ size_t chunksToRemove = 0;
+ for (const VideoChunk& c : mChunks) {
+ if (c.mTimeStamp >= chunk->mTimeStamp) {
+ break;
+ }
+ duration += c.GetDuration();
+ ++chunksToRemove;
+ }
+ mChunks.RemoveElementsAt(0, chunksToRemove);
+ mDuration -= duration;
+ MOZ_ASSERT(mChunks.Capacity() >= DEFAULT_SEGMENT_CAPACITY,
+ "Capacity must be retained after removing chunks");
+ }
+ // Override default impl
+ void ReplaceWithDisabled() override {
+ for (ChunkIterator i(*this); !i.IsEnded(); i.Next()) {
+ VideoChunk& chunk = *i;
+ chunk.SetForceBlack(true);
+ }
+ }
+
+ // Segment-generic methods not in MediaSegmentBase
+ static Type StaticType() { return VIDEO; }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+};
+
+} // namespace mozilla
+
+#endif /* MOZILLA_VIDEOSEGMENT_H_ */
diff --git a/dom/media/VideoStreamTrack.cpp b/dom/media/VideoStreamTrack.cpp
new file mode 100644
index 0000000000..ff02017dac
--- /dev/null
+++ b/dom/media/VideoStreamTrack.cpp
@@ -0,0 +1,91 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "VideoStreamTrack.h"
+
+#include "MediaTrackGraph.h"
+#include "MediaTrackListener.h"
+#include "nsContentUtils.h"
+#include "nsGlobalWindowInner.h"
+#include "VideoOutput.h"
+
+namespace mozilla::dom {
+
+VideoStreamTrack::VideoStreamTrack(nsPIDOMWindowInner* aWindow,
+ mozilla::MediaTrack* aInputTrack,
+ MediaStreamTrackSource* aSource,
+ MediaStreamTrackState aReadyState,
+ bool aMuted,
+ const MediaTrackConstraints& aConstraints)
+ : MediaStreamTrack(aWindow, aInputTrack, aSource, aReadyState, aMuted,
+ aConstraints) {}
+
+void VideoStreamTrack::Destroy() {
+ mVideoOutputs.Clear();
+ MediaStreamTrack::Destroy();
+}
+
+void VideoStreamTrack::AddVideoOutput(VideoFrameContainer* aSink) {
+ if (Ended()) {
+ return;
+ }
+ auto output = MakeRefPtr<VideoOutput>(
+ aSink, nsGlobalWindowInner::Cast(GetParentObject())
+ ->AbstractMainThreadFor(TaskCategory::Other));
+ AddVideoOutput(output);
+}
+
+void VideoStreamTrack::AddVideoOutput(VideoOutput* aOutput) {
+ if (Ended()) {
+ return;
+ }
+ for (const auto& output : mVideoOutputs) {
+ if (output == aOutput) {
+ MOZ_ASSERT_UNREACHABLE("A VideoOutput was already added");
+ return;
+ }
+ }
+ mVideoOutputs.AppendElement(aOutput);
+ AddDirectListener(aOutput);
+ AddListener(aOutput);
+}
+
+void VideoStreamTrack::RemoveVideoOutput(VideoFrameContainer* aSink) {
+ for (const auto& output : mVideoOutputs.Clone()) {
+ if (output->mVideoFrameContainer == aSink) {
+ mVideoOutputs.RemoveElement(output);
+ RemoveDirectListener(output);
+ RemoveListener(output);
+ }
+ }
+}
+
+void VideoStreamTrack::RemoveVideoOutput(VideoOutput* aOutput) {
+ for (const auto& output : mVideoOutputs.Clone()) {
+ if (output == aOutput) {
+ mVideoOutputs.RemoveElement(aOutput);
+ RemoveDirectListener(aOutput);
+ RemoveListener(aOutput);
+ }
+ }
+}
+
+void VideoStreamTrack::GetLabel(nsAString& aLabel, CallerType aCallerType) {
+ nsIGlobalObject* global =
+ GetParentObject() ? GetParentObject()->AsGlobal() : nullptr;
+ if (nsContentUtils::ShouldResistFingerprinting(aCallerType, global,
+ RFPTarget::StreamTrackLabel)) {
+ aLabel.AssignLiteral("Internal Camera");
+ return;
+ }
+ MediaStreamTrack::GetLabel(aLabel, aCallerType);
+}
+
+already_AddRefed<MediaStreamTrack> VideoStreamTrack::CloneInternal() {
+ return do_AddRef(new VideoStreamTrack(mWindow, mInputTrack, mSource,
+ ReadyState(), Muted(), mConstraints));
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/VideoStreamTrack.h b/dom/media/VideoStreamTrack.h
new file mode 100644
index 0000000000..3ed040c76a
--- /dev/null
+++ b/dom/media/VideoStreamTrack.h
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef VIDEOSTREAMTRACK_H_
+#define VIDEOSTREAMTRACK_H_
+
+#include "MediaStreamTrack.h"
+#include "DOMMediaStream.h"
+
+namespace mozilla {
+
+class VideoFrameContainer;
+class VideoOutput;
+
+namespace dom {
+
+class VideoStreamTrack : public MediaStreamTrack {
+ public:
+ VideoStreamTrack(
+ nsPIDOMWindowInner* aWindow, mozilla::MediaTrack* aInputTrack,
+ MediaStreamTrackSource* aSource,
+ MediaStreamTrackState aState = MediaStreamTrackState::Live,
+ bool aMuted = false,
+ const MediaTrackConstraints& aConstraints = MediaTrackConstraints());
+
+ void Destroy() override;
+
+ VideoStreamTrack* AsVideoStreamTrack() override { return this; }
+ const VideoStreamTrack* AsVideoStreamTrack() const override { return this; }
+
+ void AddVideoOutput(VideoFrameContainer* aSink);
+ void AddVideoOutput(VideoOutput* aOutput);
+ void RemoveVideoOutput(VideoFrameContainer* aSink);
+ void RemoveVideoOutput(VideoOutput* aOutput);
+
+ /**
+ * Whether this VideoStreamTrack's video frames will have an alpha channel.
+ */
+ bool HasAlpha() const { return GetSource().HasAlpha(); }
+
+ // WebIDL
+ void GetKind(nsAString& aKind) override { aKind.AssignLiteral("video"); }
+
+ void GetLabel(nsAString& aLabel, CallerType aCallerType) override;
+
+ protected:
+ already_AddRefed<MediaStreamTrack> CloneInternal() override;
+
+ private:
+ nsTArray<RefPtr<VideoOutput>> mVideoOutputs;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif /* VIDEOSTREAMTRACK_H_ */
diff --git a/dom/media/VideoTrack.cpp b/dom/media/VideoTrack.cpp
new file mode 100644
index 0000000000..57ed306857
--- /dev/null
+++ b/dom/media/VideoTrack.cpp
@@ -0,0 +1,92 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 et tw=78: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/HTMLMediaElement.h"
+#include "mozilla/dom/VideoStreamTrack.h"
+#include "mozilla/dom/VideoTrack.h"
+#include "mozilla/dom/VideoTrackBinding.h"
+#include "mozilla/dom/VideoTrackList.h"
+
+namespace mozilla::dom {
+
+VideoTrack::VideoTrack(nsIGlobalObject* aOwnerGlobal, const nsAString& aId,
+ const nsAString& aKind, const nsAString& aLabel,
+ const nsAString& aLanguage,
+ VideoStreamTrack* aStreamTrack)
+ : MediaTrack(aOwnerGlobal, aId, aKind, aLabel, aLanguage),
+ mSelected(false),
+ mVideoStreamTrack(aStreamTrack) {}
+
+VideoTrack::~VideoTrack() = default;
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(VideoTrack, MediaTrack, mVideoStreamTrack)
+
+NS_IMPL_ADDREF_INHERITED(VideoTrack, MediaTrack)
+NS_IMPL_RELEASE_INHERITED(VideoTrack, MediaTrack)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(VideoTrack)
+NS_INTERFACE_MAP_END_INHERITING(MediaTrack)
+
+JSObject* VideoTrack::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return VideoTrack_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void VideoTrack::SetSelected(bool aSelected) {
+ SetEnabledInternal(aSelected, MediaTrack::DEFAULT);
+}
+
+void VideoTrack::SetEnabledInternal(bool aEnabled, int aFlags) {
+ if (aEnabled == mSelected) {
+ return;
+ }
+
+ mSelected = aEnabled;
+
+ // If this VideoTrack is no longer in its original VideoTrackList, then
+ // whether it is selected or not has no effect on its original list.
+ if (!mList) {
+ return;
+ }
+
+ VideoTrackList& list = static_cast<VideoTrackList&>(*mList);
+ if (mSelected) {
+ uint32_t curIndex = 0;
+
+ // Unselect all video tracks except the current one.
+ for (uint32_t i = 0; i < list.Length(); ++i) {
+ if (list[i] == this) {
+ curIndex = i;
+ continue;
+ }
+
+ VideoTrack* track = list[i];
+ track->SetSelected(false);
+ }
+
+ // Set the index of selected video track to the current's index.
+ list.mSelectedIndex = curIndex;
+
+ HTMLMediaElement* element = mList->GetMediaElement();
+ if (element) {
+ element->NotifyMediaTrackEnabled(this);
+ }
+ } else {
+ list.mSelectedIndex = -1;
+
+ HTMLMediaElement* element = mList->GetMediaElement();
+ if (element) {
+ element->NotifyMediaTrackDisabled(this);
+ }
+ }
+
+ // Fire the change event at selection changes on this video track, shall
+ // propose a spec change later.
+ if (!(aFlags & MediaTrack::FIRE_NO_EVENTS)) {
+ list.CreateAndDispatchChangeEvent();
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/VideoTrack.h b/dom/media/VideoTrack.h
new file mode 100644
index 0000000000..af632090ee
--- /dev/null
+++ b/dom/media/VideoTrack.h
@@ -0,0 +1,61 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 et tw=78: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_VideoTrack_h
+#define mozilla_dom_VideoTrack_h
+
+#include "MediaTrack.h"
+
+namespace mozilla::dom {
+
+class VideoTrackList;
+class VideoStreamTrack;
+
+class VideoTrack : public MediaTrack {
+ public:
+ VideoTrack(nsIGlobalObject* aOwnerGlobal, const nsAString& aId,
+ const nsAString& aKind, const nsAString& aLabel,
+ const nsAString& aLanguage,
+ VideoStreamTrack* aStreamTrack = nullptr);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(VideoTrack, MediaTrack)
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ VideoTrack* AsVideoTrack() override { return this; }
+
+ // When fetching media resource, if no video track is selected by the media
+ // resource, then the first VideoTrack object in the list is set selected as
+ // default. If multiple video tracks are selected by its media resource at
+ // fetching phase, then the first enabled video track is set selected.
+ // aFlags contains FIRE_NO_EVENTS because no events are fired in such cases.
+ void SetEnabledInternal(bool aEnabled, int aFlags) override;
+
+ // Get associated video stream track when the video track comes from
+ // MediaStream. This might be nullptr when the src of owning HTMLMediaElement
+ // is not MediaStream.
+ VideoStreamTrack* GetVideoStreamTrack() { return mVideoStreamTrack; }
+
+ // WebIDL
+ bool Selected() const { return mSelected; }
+
+ // Either zero or one video track is selected in a list; If the selected track
+ // is in a VideoTrackList, then all the other VideoTrack objects in that list
+ // must be unselected.
+ void SetSelected(bool aSelected);
+
+ private:
+ virtual ~VideoTrack();
+
+ bool mSelected;
+ RefPtr<VideoStreamTrack> mVideoStreamTrack;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_VideoTrack_h
diff --git a/dom/media/VideoTrackList.cpp b/dom/media/VideoTrackList.cpp
new file mode 100644
index 0000000000..79ad1329ff
--- /dev/null
+++ b/dom/media/VideoTrackList.cpp
@@ -0,0 +1,80 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/VideoTrack.h"
+#include "mozilla/dom/VideoTrackList.h"
+#include "mozilla/dom/VideoTrackListBinding.h"
+
+namespace mozilla::dom {
+
+JSObject* VideoTrackList::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return VideoTrackList_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+VideoTrack* VideoTrackList::operator[](uint32_t aIndex) {
+ MediaTrack* track = MediaTrackList::operator[](aIndex);
+ return track->AsVideoTrack();
+}
+
+void VideoTrackList::RemoveTrack(const RefPtr<MediaTrack>& aTrack) {
+ // we need to find the video track before |MediaTrackList::RemoveTrack|. Or
+ // mSelectedIndex will not be valid. The check of mSelectedIndex == -1
+ // need to be done after RemoveTrack. Also the call of
+ // |MediaTrackList::RemoveTrack| is necessary even when mSelectedIndex = -1.
+ bool found;
+ VideoTrack* selectedVideoTrack = IndexedGetter(mSelectedIndex, found);
+ MediaTrackList::RemoveTrack(aTrack);
+ if (mSelectedIndex == -1) {
+ // There was no selected track and we don't select another track on removal.
+ return;
+ }
+ MOZ_ASSERT(found, "When mSelectedIndex is set it should point to a track");
+ MOZ_ASSERT(selectedVideoTrack,
+ "The mSelectedIndex should be set to video track only");
+
+ // Let the caller of RemoveTrack deal with choosing the new selected track if
+ // it removes the currently-selected track.
+ if (aTrack == selectedVideoTrack) {
+ mSelectedIndex = -1;
+ return;
+ }
+
+ // The removed track was not the selected track and there is a
+ // currently-selected video track. We need to find the new location of the
+ // selected track.
+ for (size_t ix = 0; ix < mTracks.Length(); ix++) {
+ if (mTracks[ix] == selectedVideoTrack) {
+ mSelectedIndex = ix;
+ return;
+ }
+ }
+}
+
+void VideoTrackList::EmptyTracks() {
+ mSelectedIndex = -1;
+ MediaTrackList::EmptyTracks();
+}
+
+VideoTrack* VideoTrackList::GetSelectedTrack() {
+ if (mSelectedIndex < 0 ||
+ static_cast<size_t>(mSelectedIndex) >= mTracks.Length()) {
+ return nullptr;
+ }
+
+ return operator[](mSelectedIndex);
+}
+
+VideoTrack* VideoTrackList::IndexedGetter(uint32_t aIndex, bool& aFound) {
+ MediaTrack* track = MediaTrackList::IndexedGetter(aIndex, aFound);
+ return track ? track->AsVideoTrack() : nullptr;
+}
+
+VideoTrack* VideoTrackList::GetTrackById(const nsAString& aId) {
+ MediaTrack* track = MediaTrackList::GetTrackById(aId);
+ return track ? track->AsVideoTrack() : nullptr;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/VideoTrackList.h b/dom/media/VideoTrackList.h
new file mode 100644
index 0000000000..370e281472
--- /dev/null
+++ b/dom/media/VideoTrackList.h
@@ -0,0 +1,51 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 et tw=78: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_VideoTrackList_h
+#define mozilla_dom_VideoTrackList_h
+
+#include "MediaTrack.h"
+#include "MediaTrackList.h"
+
+namespace mozilla::dom {
+
+class VideoTrack;
+
+class VideoTrackList : public MediaTrackList {
+ public:
+ VideoTrackList(nsIGlobalObject* aOwnerObject, HTMLMediaElement* aMediaElement)
+ : MediaTrackList(aOwnerObject, aMediaElement), mSelectedIndex(-1) {}
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ VideoTrack* operator[](uint32_t aIndex);
+
+ void RemoveTrack(const RefPtr<MediaTrack>& aTrack) override;
+
+ void EmptyTracks() override;
+
+ VideoTrack* GetSelectedTrack();
+
+ // WebIDL
+ int32_t SelectedIndex() const { return mSelectedIndex; }
+
+ VideoTrack* IndexedGetter(uint32_t aIndex, bool& aFound);
+
+ VideoTrack* GetTrackById(const nsAString& aId);
+
+ friend class VideoTrack;
+
+ protected:
+ VideoTrackList* AsVideoTrackList() override { return this; }
+
+ private:
+ int32_t mSelectedIndex;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_VideoTrackList_h
diff --git a/dom/media/VideoUtils.cpp b/dom/media/VideoUtils.cpp
new file mode 100644
index 0000000000..cacda40327
--- /dev/null
+++ b/dom/media/VideoUtils.cpp
@@ -0,0 +1,1046 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "VideoUtils.h"
+
+#include <functional>
+#include <stdint.h>
+
+#include "CubebUtils.h"
+#include "ImageContainer.h"
+#include "MediaContainerType.h"
+#include "MediaResource.h"
+#include "TimeUnits.h"
+#include "VorbisUtils.h"
+#include "mozilla/Base64.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/SharedThreadPool.h"
+#include "mozilla/StaticPrefs_accessibility.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/TaskCategory.h"
+#include "mozilla/TaskQueue.h"
+#include "mozilla/Telemetry.h"
+#include "nsCharSeparatedTokenizer.h"
+#include "nsContentTypeParser.h"
+#include "nsIConsoleService.h"
+#include "nsINetworkLinkService.h"
+#include "nsIRandomGenerator.h"
+#include "nsMathUtils.h"
+#include "nsNetCID.h"
+#include "nsServiceManagerUtils.h"
+#include "nsThreadUtils.h"
+#include "AudioStream.h"
+
+namespace mozilla {
+
+using gfx::ColorRange;
+using gfx::CICP::ColourPrimaries;
+using gfx::CICP::MatrixCoefficients;
+using gfx::CICP::TransferCharacteristics;
+using layers::PlanarYCbCrImage;
+using media::TimeUnit;
+
+double ToMicrosecondResolution(double aSeconds) {
+ double integer;
+ modf(aSeconds * USECS_PER_S, &integer);
+ return integer / USECS_PER_S;
+}
+
+CheckedInt64 SaferMultDiv(int64_t aValue, uint64_t aMul, uint64_t aDiv) {
+ if (aMul > INT64_MAX || aDiv > INT64_MAX) {
+ return CheckedInt64(INT64_MAX) + 1; // Return an invalid checked int.
+ }
+ int64_t mul = aMul;
+ int64_t div = aDiv;
+ int64_t major = aValue / div;
+ int64_t remainder = aValue % div;
+ return CheckedInt64(remainder) * mul / div + CheckedInt64(major) * mul;
+}
+
+// Converts from number of audio frames to microseconds, given the specified
+// audio rate.
+CheckedInt64 FramesToUsecs(int64_t aFrames, uint32_t aRate) {
+ return SaferMultDiv(aFrames, USECS_PER_S, aRate);
+}
+
+// Converts from microseconds to number of audio frames, given the specified
+// audio rate.
+CheckedInt64 UsecsToFrames(int64_t aUsecs, uint32_t aRate) {
+ return SaferMultDiv(aUsecs, aRate, USECS_PER_S);
+}
+
+// Format TimeUnit as number of frames at given rate.
+CheckedInt64 TimeUnitToFrames(const TimeUnit& aTime, uint32_t aRate) {
+ return aTime.IsValid() ? UsecsToFrames(aTime.ToMicroseconds(), aRate)
+ : CheckedInt64(INT64_MAX) + 1;
+}
+
+nsresult SecondsToUsecs(double aSeconds, int64_t& aOutUsecs) {
+ if (aSeconds * double(USECS_PER_S) > double(INT64_MAX)) {
+ return NS_ERROR_FAILURE;
+ }
+ aOutUsecs = int64_t(aSeconds * double(USECS_PER_S));
+ return NS_OK;
+}
+
+static int32_t ConditionDimension(float aValue) {
+ // This will exclude NaNs and too-big values.
+ if (aValue > 1.0 && aValue <= float(INT32_MAX)) {
+ return int32_t(NS_round(aValue));
+ }
+ return 0;
+}
+
+void ScaleDisplayByAspectRatio(gfx::IntSize& aDisplay, float aAspectRatio) {
+ if (aAspectRatio > 1.0) {
+ // Increase the intrinsic width
+ aDisplay.width = ConditionDimension(aAspectRatio * aDisplay.width);
+ } else {
+ // Increase the intrinsic height
+ aDisplay.height = ConditionDimension(aDisplay.height / aAspectRatio);
+ }
+}
+
+static int64_t BytesToTime(int64_t offset, int64_t length, int64_t durationUs) {
+ NS_ASSERTION(length > 0, "Must have positive length");
+ double r = double(offset) / double(length);
+ if (r > 1.0) {
+ r = 1.0;
+ }
+ return int64_t(double(durationUs) * r);
+}
+
+media::TimeIntervals GetEstimatedBufferedTimeRanges(
+ mozilla::MediaResource* aStream, int64_t aDurationUsecs) {
+ media::TimeIntervals buffered;
+ // Nothing to cache if the media takes 0us to play.
+ if (aDurationUsecs <= 0 || !aStream) {
+ return buffered;
+ }
+
+ // Special case completely cached files. This also handles local files.
+ if (aStream->IsDataCachedToEndOfResource(0)) {
+ buffered += media::TimeInterval(TimeUnit::Zero(),
+ TimeUnit::FromMicroseconds(aDurationUsecs));
+ return buffered;
+ }
+
+ int64_t totalBytes = aStream->GetLength();
+
+ // If we can't determine the total size, pretend that we have nothing
+ // buffered. This will put us in a state of eternally-low-on-undecoded-data
+ // which is not great, but about the best we can do.
+ if (totalBytes <= 0) {
+ return buffered;
+ }
+
+ int64_t startOffset = aStream->GetNextCachedData(0);
+ while (startOffset >= 0) {
+ int64_t endOffset = aStream->GetCachedDataEnd(startOffset);
+ // Bytes [startOffset..endOffset] are cached.
+ NS_ASSERTION(startOffset >= 0, "Integer underflow in GetBuffered");
+ NS_ASSERTION(endOffset >= 0, "Integer underflow in GetBuffered");
+
+ int64_t startUs = BytesToTime(startOffset, totalBytes, aDurationUsecs);
+ int64_t endUs = BytesToTime(endOffset, totalBytes, aDurationUsecs);
+ if (startUs != endUs) {
+ buffered += media::TimeInterval(TimeUnit::FromMicroseconds(startUs),
+ TimeUnit::FromMicroseconds(endUs));
+ }
+ startOffset = aStream->GetNextCachedData(endOffset);
+ }
+ return buffered;
+}
+
+void DownmixStereoToMono(mozilla::AudioDataValue* aBuffer, uint32_t aFrames) {
+ MOZ_ASSERT(aBuffer);
+ const int channels = 2;
+ for (uint32_t fIdx = 0; fIdx < aFrames; ++fIdx) {
+#ifdef MOZ_SAMPLE_TYPE_FLOAT32
+ float sample = 0.0;
+#else
+ int sample = 0;
+#endif
+ // The sample of the buffer would be interleaved.
+ sample = (aBuffer[fIdx * channels] + aBuffer[fIdx * channels + 1]) * 0.5f;
+ aBuffer[fIdx * channels] = aBuffer[fIdx * channels + 1] = sample;
+ }
+}
+
+uint32_t DecideAudioPlaybackChannels(const AudioInfo& info) {
+ if (StaticPrefs::accessibility_monoaudio_enable()) {
+ return 1;
+ }
+
+ if (StaticPrefs::media_forcestereo_enabled()) {
+ return 2;
+ }
+
+ return info.mChannels;
+}
+
+uint32_t DecideAudioPlaybackSampleRate(const AudioInfo& aInfo,
+ bool aShouldResistFingerprinting) {
+ bool resampling = StaticPrefs::media_resampling_enabled();
+
+ uint32_t rate = 0;
+
+ if (resampling) {
+ rate = 48000;
+ } else if (aInfo.mRate >= 44100) {
+ // The original rate is of good quality and we want to minimize unecessary
+ // resampling, so we let cubeb decide how to resample (if needed).
+ rate = aInfo.mRate;
+ } else {
+ // We will resample all data to match cubeb's preferred sampling rate.
+ rate = CubebUtils::PreferredSampleRate(aShouldResistFingerprinting);
+ if (rate > 384000) {
+ // bogus rate, fall back to something else;
+ rate = 48000;
+ }
+ }
+ MOZ_DIAGNOSTIC_ASSERT(rate, "output rate can't be 0.");
+
+ return rate;
+}
+
+bool IsDefaultPlaybackDeviceMono() {
+ return CubebUtils::MaxNumberOfChannels() == 1;
+}
+
+bool IsVideoContentType(const nsCString& aContentType) {
+ constexpr auto video = "video"_ns;
+ return FindInReadable(video, aContentType);
+}
+
+bool IsValidVideoRegion(const gfx::IntSize& aFrame,
+ const gfx::IntRect& aPicture,
+ const gfx::IntSize& aDisplay) {
+ return aFrame.width > 0 && aFrame.width <= PlanarYCbCrImage::MAX_DIMENSION &&
+ aFrame.height > 0 &&
+ aFrame.height <= PlanarYCbCrImage::MAX_DIMENSION &&
+ aFrame.width * aFrame.height <= MAX_VIDEO_WIDTH * MAX_VIDEO_HEIGHT &&
+ aPicture.width > 0 &&
+ aPicture.width <= PlanarYCbCrImage::MAX_DIMENSION &&
+ aPicture.x < PlanarYCbCrImage::MAX_DIMENSION &&
+ aPicture.x + aPicture.width < PlanarYCbCrImage::MAX_DIMENSION &&
+ aPicture.height > 0 &&
+ aPicture.height <= PlanarYCbCrImage::MAX_DIMENSION &&
+ aPicture.y < PlanarYCbCrImage::MAX_DIMENSION &&
+ aPicture.y + aPicture.height < PlanarYCbCrImage::MAX_DIMENSION &&
+ aPicture.width * aPicture.height <=
+ MAX_VIDEO_WIDTH * MAX_VIDEO_HEIGHT &&
+ aDisplay.width > 0 &&
+ aDisplay.width <= PlanarYCbCrImage::MAX_DIMENSION &&
+ aDisplay.height > 0 &&
+ aDisplay.height <= PlanarYCbCrImage::MAX_DIMENSION &&
+ aDisplay.width * aDisplay.height <= MAX_VIDEO_WIDTH * MAX_VIDEO_HEIGHT;
+}
+
+already_AddRefed<SharedThreadPool> GetMediaThreadPool(MediaThreadType aType) {
+ const char* name;
+ uint32_t threads = 4;
+ switch (aType) {
+ case MediaThreadType::PLATFORM_DECODER:
+ name = "MediaPDecoder";
+ break;
+ case MediaThreadType::WEBRTC_CALL_THREAD:
+ name = "WebrtcCallThread";
+ threads = 1;
+ break;
+ case MediaThreadType::WEBRTC_WORKER:
+ name = "WebrtcWorker";
+ break;
+ case MediaThreadType::MDSM:
+ name = "MediaDecoderStateMachine";
+ threads = 1;
+ break;
+ case MediaThreadType::PLATFORM_ENCODER:
+ name = "MediaPEncoder";
+ break;
+ default:
+ MOZ_FALLTHROUGH_ASSERT("Unexpected MediaThreadType");
+ case MediaThreadType::SUPERVISOR:
+ name = "MediaSupervisor";
+ break;
+ }
+
+ RefPtr<SharedThreadPool> pool =
+ SharedThreadPool::Get(nsDependentCString(name), threads);
+
+ // Ensure a larger stack for platform decoder threads
+ if (aType == MediaThreadType::PLATFORM_DECODER) {
+ const uint32_t minStackSize = 512 * 1024;
+ uint32_t stackSize;
+ MOZ_ALWAYS_SUCCEEDS(pool->GetThreadStackSize(&stackSize));
+ if (stackSize < minStackSize) {
+ MOZ_ALWAYS_SUCCEEDS(pool->SetThreadStackSize(minStackSize));
+ }
+ }
+
+ return pool.forget();
+}
+
+bool ExtractVPXCodecDetails(const nsAString& aCodec, uint8_t& aProfile,
+ uint8_t& aLevel, uint8_t& aBitDepth) {
+ uint8_t dummyChromaSubsampling = 1;
+ VideoColorSpace dummyColorspace;
+ return ExtractVPXCodecDetails(aCodec, aProfile, aLevel, aBitDepth,
+ dummyChromaSubsampling, dummyColorspace);
+}
+
+bool ExtractVPXCodecDetails(const nsAString& aCodec, uint8_t& aProfile,
+ uint8_t& aLevel, uint8_t& aBitDepth,
+ uint8_t& aChromaSubsampling,
+ VideoColorSpace& aColorSpace) {
+ // Assign default value.
+ aChromaSubsampling = 1;
+ auto splitter = aCodec.Split(u'.');
+ auto fieldsItr = splitter.begin();
+ auto fourCC = *fieldsItr;
+
+ if (!fourCC.EqualsLiteral("vp09") && !fourCC.EqualsLiteral("vp08")) {
+ // Invalid 4CC
+ return false;
+ }
+ ++fieldsItr;
+ uint8_t primary, transfer, matrix, range;
+ uint8_t* fields[] = {&aProfile, &aLevel, &aBitDepth, &aChromaSubsampling,
+ &primary, &transfer, &matrix, &range};
+ int fieldsCount = 0;
+ nsresult rv;
+ for (; fieldsItr != splitter.end(); ++fieldsItr, ++fieldsCount) {
+ if (fieldsCount > 7) {
+ // No more than 8 fields are expected.
+ return false;
+ }
+ *(fields[fieldsCount]) =
+ static_cast<uint8_t>((*fieldsItr).ToInteger(&rv, 10));
+ // We got invalid field value, parsing error.
+ NS_ENSURE_SUCCESS(rv, false);
+ }
+ // Mandatory Fields
+ // <sample entry 4CC>.<profile>.<level>.<bitDepth>.
+ // Optional Fields
+ // <chromaSubsampling>.<colourPrimaries>.<transferCharacteristics>.
+ // <matrixCoefficients>.<videoFullRangeFlag>
+ // First three fields are mandatory(we have parsed 4CC).
+ if (fieldsCount < 3) {
+ // Invalid number of fields.
+ return false;
+ }
+ // Start to validate the parsing value.
+
+ // profile should be 0,1,2 or 3.
+ // See https://www.webmproject.org/vp9/profiles/
+ if (aProfile > 3) {
+ // Invalid profile.
+ return false;
+ }
+
+ // level, See https://www.webmproject.org/vp9/mp4/#semantics_1
+ switch (aLevel) {
+ case 10:
+ case 11:
+ case 20:
+ case 21:
+ case 30:
+ case 31:
+ case 40:
+ case 41:
+ case 50:
+ case 51:
+ case 52:
+ case 60:
+ case 61:
+ case 62:
+ break;
+ default:
+ // Invalid level.
+ return false;
+ }
+
+ if (aBitDepth != 8 && aBitDepth != 10 && aBitDepth != 12) {
+ // Invalid bitDepth:
+ return false;
+ }
+
+ if (fieldsCount == 3) {
+ // No more options.
+ return true;
+ }
+
+ // chromaSubsampling should be 0,1,2,3...4~7 are reserved.
+ if (aChromaSubsampling > 3) {
+ return false;
+ }
+
+ if (fieldsCount == 4) {
+ // No more options.
+ return true;
+ }
+
+ // It is an integer that is defined by the "Colour primaries"
+ // section of ISO/IEC 23001-8:2016 Table 2.
+ // We treat reserved value as false case.
+ if (primary == 0 || primary == 3 || primary > 22) {
+ // reserved value.
+ return false;
+ }
+ if (primary > 12 && primary < 22) {
+ // 13~21 are reserved values.
+ return false;
+ }
+ aColorSpace.mPrimaries = static_cast<ColourPrimaries>(primary);
+
+ if (fieldsCount == 5) {
+ // No more options.
+ return true;
+ }
+
+ // It is an integer that is defined by the
+ // "Transfer characteristics" section of ISO/IEC 23001-8:2016 Table 3.
+ // We treat reserved value as false case.
+ if (transfer == 0 || transfer == 3 || transfer > 18) {
+ // reserved value.
+ return false;
+ }
+ aColorSpace.mTransfer = static_cast<TransferCharacteristics>(transfer);
+
+ if (fieldsCount == 6) {
+ // No more options.
+ return true;
+ }
+
+ // It is an integer that is defined by the
+ // "Matrix coefficients" section of ISO/IEC 23001-8:2016 Table 4.
+ // We treat reserved value as false case.
+ if (matrix == 3 || matrix > 11) {
+ return false;
+ }
+ aColorSpace.mMatrix = static_cast<MatrixCoefficients>(matrix);
+
+ // If matrixCoefficients is 0 (RGB), then chroma subsampling MUST be 3
+ // (4:4:4).
+ if (aColorSpace.mMatrix == MatrixCoefficients::MC_IDENTITY &&
+ aChromaSubsampling != 3) {
+ return false;
+ }
+
+ if (fieldsCount == 7) {
+ // No more options.
+ return true;
+ }
+
+ // videoFullRangeFlag indicates the black level and range of the luma and
+ // chroma signals. 0 = legal range (e.g. 16-235 for 8 bit sample depth);
+ // 1 = full range (e.g. 0-255 for 8-bit sample depth).
+ aColorSpace.mRange = static_cast<ColorRange>(range);
+ return range <= 1;
+}
+
+bool ExtractH264CodecDetails(const nsAString& aCodec, uint8_t& aProfile,
+ uint8_t& aConstraint, uint8_t& aLevel) {
+ // H.264 codecs parameters have a type defined as avcN.PPCCLL, where
+ // N = avc type. avc3 is avcc with SPS & PPS implicit (within stream)
+ // PP = profile_idc, CC = constraint_set flags, LL = level_idc.
+ // We ignore the constraint_set flags, as it's not clear from any
+ // documentation what constraints the platform decoders support.
+ // See
+ // http://blog.pearce.org.nz/2013/11/what-does-h264avc1-codecs-parameters.html
+ // for more details.
+ if (aCodec.Length() != strlen("avc1.PPCCLL")) {
+ return false;
+ }
+
+ // Verify the codec starts with "avc1." or "avc3.".
+ const nsAString& sample = Substring(aCodec, 0, 5);
+ if (!sample.EqualsASCII("avc1.") && !sample.EqualsASCII("avc3.")) {
+ return false;
+ }
+
+ // Extract the profile_idc, constraint_flags and level_idc.
+ nsresult rv = NS_OK;
+ aProfile = Substring(aCodec, 5, 2).ToInteger(&rv, 16);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ // Constraint flags are stored on the 6 most significant bits, first two bits
+ // are reserved_zero_2bits.
+ aConstraint = Substring(aCodec, 7, 2).ToInteger(&rv, 16);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ aLevel = Substring(aCodec, 9, 2).ToInteger(&rv, 16);
+ NS_ENSURE_SUCCESS(rv, false);
+
+ if (aLevel == 9) {
+ aLevel = H264_LEVEL_1_b;
+ } else if (aLevel <= 5) {
+ aLevel *= 10;
+ }
+
+ return true;
+}
+
+bool ExtractAV1CodecDetails(const nsAString& aCodec, uint8_t& aProfile,
+ uint8_t& aLevel, uint8_t& aTier, uint8_t& aBitDepth,
+ bool& aMonochrome, bool& aSubsamplingX,
+ bool& aSubsamplingY, uint8_t& aChromaSamplePosition,
+ VideoColorSpace& aColorSpace) {
+ auto fourCC = Substring(aCodec, 0, 4);
+
+ if (!fourCC.EqualsLiteral("av01")) {
+ // Invalid 4CC
+ return false;
+ }
+
+ // Format is:
+ // av01.N.NN[MH].NN.B.BBN.NN.NN.NN.B
+ // where
+ // N = decimal digit
+ // [] = single character
+ // B = binary digit
+ // Field order:
+ // <sample entry 4CC>.<profile>.<level><tier>.<bitDepth>
+ // [.<monochrome>.<chromaSubsampling>
+ // .<colorPrimaries>.<transferCharacteristics>.<matrixCoefficients>
+ // .<videoFullRangeFlag>]
+ //
+ // If any optional field is found, all the rest must be included.
+ //
+ // Parsing stops but does not fail upon encountering unexpected characters
+ // at the end of an otherwise well-formed string.
+ //
+ // See https://aomediacodec.github.io/av1-isobmff/#codecsparam
+
+ struct AV1Field {
+ uint8_t* field;
+ size_t length;
+ };
+ uint8_t monochrome;
+ uint8_t subsampling;
+ uint8_t primary;
+ uint8_t transfer;
+ uint8_t matrix;
+ uint8_t range;
+ AV1Field fields[] = {{&aProfile, 1},
+ {&aLevel, 2},
+ // parsing loop skips tier
+ {&aBitDepth, 2},
+ {&monochrome, 1},
+ {&subsampling, 3},
+ {&primary, 2},
+ {&transfer, 2},
+ {&matrix, 2},
+ {&range, 1}};
+
+ auto splitter = aCodec.Split(u'.');
+ auto iter = splitter.begin();
+ ++iter;
+ size_t fieldCount = 0;
+ while (iter != splitter.end()) {
+ // Exit if there are too many fields.
+ if (fieldCount >= 9) {
+ return false;
+ }
+
+ AV1Field& field = fields[fieldCount];
+ auto fieldStr = *iter;
+
+ if (field.field == &aLevel) {
+ // Parse tier and remove it from the level field.
+ if (fieldStr.Length() < 3) {
+ return false;
+ }
+ auto tier = fieldStr[2];
+ switch (tier) {
+ case 'M':
+ aTier = 0;
+ break;
+ case 'H':
+ aTier = 1;
+ break;
+ default:
+ return false;
+ }
+ fieldStr.SetLength(2);
+ }
+
+ if (fieldStr.Length() < field.length) {
+ return false;
+ }
+
+ // Manually parse values since nsString.ToInteger silently stops parsing
+ // upon encountering unknown characters.
+ uint8_t value = 0;
+ for (size_t i = 0; i < field.length; i++) {
+ uint8_t oldValue = value;
+ char16_t character = fieldStr[i];
+ if ('0' <= character && character <= '9') {
+ value = (value * 10) + (character - '0');
+ } else {
+ return false;
+ }
+ if (value < oldValue) {
+ // Overflow is possible on the 3-digit subsampling field.
+ return false;
+ }
+ }
+
+ *field.field = value;
+
+ ++fieldCount;
+ ++iter;
+
+ // Field had extra characters, exit early.
+ if (fieldStr.Length() > field.length) {
+ // Disallow numbers as unexpected characters.
+ char16_t character = fieldStr[field.length];
+ if ('0' <= character && character <= '9') {
+ return false;
+ }
+ break;
+ }
+ }
+
+ // Spec requires profile, level/tier, bitdepth, or for all possible fields to
+ // be present.
+ if (fieldCount != 3 && fieldCount != 9) {
+ return false;
+ }
+
+ // Valid profiles are: Main (0), High (1), Professional (2).
+ // Levels range from 0 to 23, or 31 to remove level restrictions.
+ if (aProfile > 2 || (aLevel > 23 && aLevel != 31)) {
+ return false;
+ }
+
+ if (fieldCount == 3) {
+ // If only required fields are included, set to the spec defaults for the
+ // rest and continue validating.
+ aMonochrome = false;
+ aSubsamplingX = true;
+ aSubsamplingY = true;
+ aChromaSamplePosition = 0;
+ aColorSpace.mPrimaries = ColourPrimaries::CP_BT709;
+ aColorSpace.mTransfer = TransferCharacteristics::TC_BT709;
+ aColorSpace.mMatrix = MatrixCoefficients::MC_BT709;
+ aColorSpace.mRange = ColorRange::LIMITED;
+ } else {
+ // Extract the individual values for the remaining fields, and check for
+ // valid values for each.
+
+ // Monochrome is a boolean.
+ if (monochrome > 1) {
+ return false;
+ }
+ aMonochrome = !!monochrome;
+
+ // Extract individual digits of the subsampling field.
+ // Subsampling is two binary digits for x and y
+ // and one enumerated sample position field of
+ // Unknown (0), Vertical (1), Colocated (2).
+ uint8_t subsamplingX = (subsampling / 100) % 10;
+ uint8_t subsamplingY = (subsampling / 10) % 10;
+ if (subsamplingX > 1 || subsamplingY > 1) {
+ return false;
+ }
+ aSubsamplingX = !!subsamplingX;
+ aSubsamplingY = !!subsamplingY;
+ aChromaSamplePosition = subsampling % 10;
+ if (aChromaSamplePosition > 2) {
+ return false;
+ }
+
+ // We can validate the color space values using CICP enums, as the values
+ // are standardized in Rec. ITU-T H.273.
+ aColorSpace.mPrimaries = static_cast<ColourPrimaries>(primary);
+ aColorSpace.mTransfer = static_cast<TransferCharacteristics>(transfer);
+ aColorSpace.mMatrix = static_cast<MatrixCoefficients>(matrix);
+ if (gfx::CICP::IsReserved(aColorSpace.mPrimaries) ||
+ gfx::CICP::IsReserved(aColorSpace.mTransfer) ||
+ gfx::CICP::IsReserved(aColorSpace.mMatrix)) {
+ return false;
+ }
+ // Range is a boolean, true meaning full and false meaning limited range.
+ if (range > 1) {
+ return false;
+ }
+ aColorSpace.mRange = static_cast<ColorRange>(range);
+ }
+
+ // Begin validating all parameter values:
+
+ // Only Levels 8 and above (4.0 and greater) can specify Tier.
+ // See: 5.5.1. General sequence header OBU syntax,
+ // if ( seq_level_idx[ i ] > 7 ) seq_tier[ i ] = f(1)
+ // https://aomediacodec.github.io/av1-spec/av1-spec.pdf#page=42
+ // Also: Annex A, A.3. Levels, columns MainMbps and HighMbps
+ // at https://aomediacodec.github.io/av1-spec/av1-spec.pdf#page=652
+ if (aLevel < 8 && aTier > 0) {
+ return false;
+ }
+
+ // Supported bit depths are 8, 10 and 12.
+ if (aBitDepth != 8 && aBitDepth != 10 && aBitDepth != 12) {
+ return false;
+ }
+ // Profiles 0 and 1 only support 8-bit and 10-bit.
+ if (aProfile < 2 && aBitDepth == 12) {
+ return false;
+ }
+
+ // x && y subsampling is used to specify monochrome 4:0:0 as well
+ bool is420or400 = aSubsamplingX && aSubsamplingY;
+ bool is422 = aSubsamplingX && !aSubsamplingY;
+ bool is444 = !aSubsamplingX && !aSubsamplingY;
+
+ // Profile 0 only supports 4:2:0.
+ if (aProfile == 0 && !is420or400) {
+ return false;
+ }
+ // Profile 1 only supports 4:4:4.
+ if (aProfile == 1 && !is444) {
+ return false;
+ }
+ // Profile 2 only allows 4:2:2 at 10 bits and below.
+ if (aProfile == 2 && aBitDepth < 12 && !is422) {
+ return false;
+ }
+ // Chroma sample position can only be specified with 4:2:0.
+ if (aChromaSamplePosition != 0 && !is420or400) {
+ return false;
+ }
+
+ // When video is monochrome, subsampling must be 4:0:0.
+ if (aMonochrome && (aChromaSamplePosition != 0 || !is420or400)) {
+ return false;
+ }
+ // Monochrome can only be signaled when profile is 0 or 2.
+ // Note: This check is redundant with the above subsampling check,
+ // as profile 1 only supports 4:4:4.
+ if (aMonochrome && aProfile != 0 && aProfile != 2) {
+ return false;
+ }
+
+ // Identity matrix requires 4:4:4 subsampling.
+ if (aColorSpace.mMatrix == MatrixCoefficients::MC_IDENTITY &&
+ (aSubsamplingX || aSubsamplingY ||
+ aColorSpace.mRange != gfx::ColorRange::FULL)) {
+ return false;
+ }
+
+ return true;
+}
+
+nsresult GenerateRandomName(nsCString& aOutSalt, uint32_t aLength) {
+ nsresult rv;
+ nsCOMPtr<nsIRandomGenerator> rg =
+ do_GetService("@mozilla.org/security/random-generator;1", &rv);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // For each three bytes of random data we will get four bytes of ASCII.
+ const uint32_t requiredBytesLength =
+ static_cast<uint32_t>((aLength + 3) / 4 * 3);
+
+ uint8_t* buffer;
+ rv = rg->GenerateRandomBytes(requiredBytesLength, &buffer);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ nsCString temp;
+ nsDependentCSubstring randomData(reinterpret_cast<const char*>(buffer),
+ requiredBytesLength);
+ rv = Base64Encode(randomData, temp);
+ free(buffer);
+ buffer = nullptr;
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ aOutSalt = std::move(temp);
+ return NS_OK;
+}
+
+nsresult GenerateRandomPathName(nsCString& aOutSalt, uint32_t aLength) {
+ nsresult rv = GenerateRandomName(aOutSalt, aLength);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Base64 characters are alphanumeric (a-zA-Z0-9) and '+' and '/', so we need
+ // to replace illegal characters -- notably '/'
+ aOutSalt.ReplaceChar(FILE_PATH_SEPARATOR FILE_ILLEGAL_CHARACTERS, '_');
+ return NS_OK;
+}
+
+already_AddRefed<TaskQueue> CreateMediaDecodeTaskQueue(const char* aName) {
+ RefPtr<TaskQueue> queue = TaskQueue::Create(
+ GetMediaThreadPool(MediaThreadType::PLATFORM_DECODER), aName);
+ return queue.forget();
+}
+
+void SimpleTimer::Cancel() {
+ if (mTimer) {
+#ifdef DEBUG
+ nsCOMPtr<nsIEventTarget> target;
+ mTimer->GetTarget(getter_AddRefs(target));
+ bool onCurrent;
+ nsresult rv = target->IsOnCurrentThread(&onCurrent);
+ MOZ_ASSERT(NS_SUCCEEDED(rv) && onCurrent);
+#endif
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+ mTask = nullptr;
+}
+
+NS_IMETHODIMP
+SimpleTimer::Notify(nsITimer* timer) {
+ RefPtr<SimpleTimer> deathGrip(this);
+ if (mTask) {
+ mTask->Run();
+ mTask = nullptr;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SimpleTimer::GetName(nsACString& aName) {
+ aName.AssignLiteral("SimpleTimer");
+ return NS_OK;
+}
+
+nsresult SimpleTimer::Init(nsIRunnable* aTask, uint32_t aTimeoutMs,
+ nsIEventTarget* aTarget) {
+ nsresult rv;
+
+ // Get target thread first, so we don't have to cancel the timer if it fails.
+ nsCOMPtr<nsIEventTarget> target;
+ if (aTarget) {
+ target = aTarget;
+ } else {
+ target = GetMainThreadSerialEventTarget();
+ if (!target) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ }
+
+ rv = NS_NewTimerWithCallback(getter_AddRefs(mTimer), this, aTimeoutMs,
+ nsITimer::TYPE_ONE_SHOT, target);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ mTask = aTask;
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(SimpleTimer, nsITimerCallback, nsINamed)
+
+already_AddRefed<SimpleTimer> SimpleTimer::Create(nsIRunnable* aTask,
+ uint32_t aTimeoutMs,
+ nsIEventTarget* aTarget) {
+ RefPtr<SimpleTimer> t(new SimpleTimer());
+ if (NS_FAILED(t->Init(aTask, aTimeoutMs, aTarget))) {
+ return nullptr;
+ }
+ return t.forget();
+}
+
+void LogToBrowserConsole(const nsAString& aMsg) {
+ if (!NS_IsMainThread()) {
+ nsString msg(aMsg);
+ nsCOMPtr<nsIRunnable> task = NS_NewRunnableFunction(
+ "LogToBrowserConsole", [msg]() { LogToBrowserConsole(msg); });
+ SchedulerGroup::Dispatch(TaskCategory::Other, task.forget());
+ return;
+ }
+ nsCOMPtr<nsIConsoleService> console(
+ do_GetService("@mozilla.org/consoleservice;1"));
+ if (!console) {
+ NS_WARNING("Failed to log message to console.");
+ return;
+ }
+ nsAutoString msg(aMsg);
+ console->LogStringMessage(msg.get());
+}
+
+bool ParseCodecsString(const nsAString& aCodecs,
+ nsTArray<nsString>& aOutCodecs) {
+ aOutCodecs.Clear();
+ bool expectMoreTokens = false;
+ nsCharSeparatedTokenizer tokenizer(aCodecs, ',');
+ while (tokenizer.hasMoreTokens()) {
+ const nsAString& token = tokenizer.nextToken();
+ expectMoreTokens = tokenizer.separatorAfterCurrentToken();
+ aOutCodecs.AppendElement(token);
+ }
+ if (expectMoreTokens) {
+ // Last codec name was empty
+ return false;
+ }
+ return true;
+}
+
+bool ParseMIMETypeString(const nsAString& aMIMEType,
+ nsString& aOutContainerType,
+ nsTArray<nsString>& aOutCodecs) {
+ nsContentTypeParser parser(aMIMEType);
+ nsresult rv = parser.GetType(aOutContainerType);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ nsString codecsStr;
+ parser.GetParameter("codecs", codecsStr);
+ return ParseCodecsString(codecsStr, aOutCodecs);
+}
+
+template <int N>
+static bool StartsWith(const nsACString& string, const char (&prefix)[N]) {
+ if (N - 1 > string.Length()) {
+ return false;
+ }
+ return memcmp(string.Data(), prefix, N - 1) == 0;
+}
+
+bool IsH264CodecString(const nsAString& aCodec) {
+ uint8_t profile = 0;
+ uint8_t constraint = 0;
+ uint8_t level = 0;
+ return ExtractH264CodecDetails(aCodec, profile, constraint, level);
+}
+
+bool IsAACCodecString(const nsAString& aCodec) {
+ return aCodec.EqualsLiteral("mp4a.40.2") || // MPEG4 AAC-LC
+ aCodec.EqualsLiteral(
+ "mp4a.40.02") || // MPEG4 AAC-LC(for compatibility)
+ aCodec.EqualsLiteral("mp4a.40.5") || // MPEG4 HE-AAC
+ aCodec.EqualsLiteral(
+ "mp4a.40.05") || // MPEG4 HE-AAC(for compatibility)
+ aCodec.EqualsLiteral("mp4a.67") || // MPEG2 AAC-LC
+ aCodec.EqualsLiteral("mp4a.40.29"); // MPEG4 HE-AACv2
+}
+
+bool IsVP8CodecString(const nsAString& aCodec) {
+ uint8_t profile = 0;
+ uint8_t level = 0;
+ uint8_t bitDepth = 0;
+ return aCodec.EqualsLiteral("vp8") || aCodec.EqualsLiteral("vp8.0") ||
+ (StartsWith(NS_ConvertUTF16toUTF8(aCodec), "vp08") &&
+ ExtractVPXCodecDetails(aCodec, profile, level, bitDepth));
+}
+
+bool IsVP9CodecString(const nsAString& aCodec) {
+ uint8_t profile = 0;
+ uint8_t level = 0;
+ uint8_t bitDepth = 0;
+ return aCodec.EqualsLiteral("vp9") || aCodec.EqualsLiteral("vp9.0") ||
+ (StartsWith(NS_ConvertUTF16toUTF8(aCodec), "vp09") &&
+ ExtractVPXCodecDetails(aCodec, profile, level, bitDepth));
+}
+
+bool IsAV1CodecString(const nsAString& aCodec) {
+ uint8_t profile, level, tier, bitDepth, chromaPosition;
+ bool monochrome, subsamplingX, subsamplingY;
+ VideoColorSpace colorSpace;
+ return aCodec.EqualsLiteral("av1") ||
+ (StartsWith(NS_ConvertUTF16toUTF8(aCodec), "av01") &&
+ ExtractAV1CodecDetails(aCodec, profile, level, tier, bitDepth,
+ monochrome, subsamplingX, subsamplingY,
+ chromaPosition, colorSpace));
+}
+
+UniquePtr<TrackInfo> CreateTrackInfoWithMIMEType(
+ const nsACString& aCodecMIMEType) {
+ UniquePtr<TrackInfo> trackInfo;
+ if (StartsWith(aCodecMIMEType, "audio/")) {
+ trackInfo.reset(new AudioInfo());
+ trackInfo->mMimeType = aCodecMIMEType;
+ } else if (StartsWith(aCodecMIMEType, "video/")) {
+ trackInfo.reset(new VideoInfo());
+ trackInfo->mMimeType = aCodecMIMEType;
+ }
+ return trackInfo;
+}
+
+UniquePtr<TrackInfo> CreateTrackInfoWithMIMETypeAndContainerTypeExtraParameters(
+ const nsACString& aCodecMIMEType,
+ const MediaContainerType& aContainerType) {
+ UniquePtr<TrackInfo> trackInfo = CreateTrackInfoWithMIMEType(aCodecMIMEType);
+ if (trackInfo) {
+ VideoInfo* videoInfo = trackInfo->GetAsVideoInfo();
+ if (videoInfo) {
+ Maybe<int32_t> maybeWidth = aContainerType.ExtendedType().GetWidth();
+ if (maybeWidth && *maybeWidth > 0) {
+ videoInfo->mImage.width = *maybeWidth;
+ videoInfo->mDisplay.width = *maybeWidth;
+ }
+ Maybe<int32_t> maybeHeight = aContainerType.ExtendedType().GetHeight();
+ if (maybeHeight && *maybeHeight > 0) {
+ videoInfo->mImage.height = *maybeHeight;
+ videoInfo->mDisplay.height = *maybeHeight;
+ }
+ } else if (trackInfo->GetAsAudioInfo()) {
+ AudioInfo* audioInfo = trackInfo->GetAsAudioInfo();
+ Maybe<int32_t> maybeChannels =
+ aContainerType.ExtendedType().GetChannels();
+ if (maybeChannels && *maybeChannels > 0) {
+ audioInfo->mChannels = *maybeChannels;
+ }
+ Maybe<int32_t> maybeSamplerate =
+ aContainerType.ExtendedType().GetSamplerate();
+ if (maybeSamplerate && *maybeSamplerate > 0) {
+ audioInfo->mRate = *maybeSamplerate;
+ }
+ }
+ }
+ return trackInfo;
+}
+
+bool OnCellularConnection() {
+ uint32_t linkType = nsINetworkLinkService::LINK_TYPE_UNKNOWN;
+ if (XRE_IsContentProcess()) {
+ mozilla::dom::ContentChild* cpc =
+ mozilla::dom::ContentChild::GetSingleton();
+ if (!cpc) {
+ NS_WARNING("Can't get ContentChild singleton in content process!");
+ return false;
+ }
+ linkType = cpc->NetworkLinkType();
+ } else {
+ nsresult rv;
+ nsCOMPtr<nsINetworkLinkService> nls =
+ do_GetService(NS_NETWORK_LINK_SERVICE_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Can't get nsINetworkLinkService.");
+ return false;
+ }
+
+ rv = nls->GetLinkType(&linkType);
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Can't get network link type.");
+ return false;
+ }
+ }
+
+ switch (linkType) {
+ case nsINetworkLinkService::LINK_TYPE_UNKNOWN:
+ case nsINetworkLinkService::LINK_TYPE_ETHERNET:
+ case nsINetworkLinkService::LINK_TYPE_USB:
+ case nsINetworkLinkService::LINK_TYPE_WIFI:
+ return false;
+ case nsINetworkLinkService::LINK_TYPE_WIMAX:
+ case nsINetworkLinkService::LINK_TYPE_MOBILE:
+ return true;
+ }
+
+ return false;
+}
+
+} // end namespace mozilla
diff --git a/dom/media/VideoUtils.h b/dom/media/VideoUtils.h
new file mode 100644
index 0000000000..98410286c8
--- /dev/null
+++ b/dom/media/VideoUtils.h
@@ -0,0 +1,579 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef VideoUtils_h
+#define VideoUtils_h
+
+#include "AudioSampleFormat.h"
+#include "MediaInfo.h"
+#include "VideoLimits.h"
+#include "mozilla/AbstractThread.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/ReentrantMonitor.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/SharedThreadPool.h"
+#include "mozilla/TaskQueue.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/gfx/Point.h" // for gfx::IntSize
+#include "mozilla/gfx/Types.h"
+#include "nsCOMPtr.h"
+#include "nsINamed.h"
+#include "nsIThread.h"
+#include "nsITimer.h"
+#include "nsThreadUtils.h"
+#include "prtime.h"
+
+using mozilla::CheckedInt32;
+using mozilla::CheckedInt64;
+using mozilla::CheckedUint32;
+using mozilla::CheckedUint64;
+
+// This file contains stuff we'd rather put elsewhere, but which is
+// dependent on other changes which we don't want to wait for. We plan to
+// remove this file in the near future.
+
+// This belongs in xpcom/monitor/Monitor.h, once we've made
+// mozilla::Monitor non-reentrant.
+namespace mozilla {
+
+class MediaContainerType;
+
+/**
+ * ReentrantMonitorConditionallyEnter
+ *
+ * Enters the supplied monitor only if the conditional value |aEnter| is true.
+ * E.g. Used to allow unmonitored read access on the decode thread,
+ * and monitored access on all other threads.
+ */
+class MOZ_STACK_CLASS ReentrantMonitorConditionallyEnter {
+ public:
+ ReentrantMonitorConditionallyEnter(bool aEnter,
+ ReentrantMonitor& aReentrantMonitor)
+ : mReentrantMonitor(nullptr) {
+ MOZ_COUNT_CTOR(ReentrantMonitorConditionallyEnter);
+ if (aEnter) {
+ mReentrantMonitor = &aReentrantMonitor;
+ NS_ASSERTION(mReentrantMonitor, "null monitor");
+ mReentrantMonitor->Enter();
+ }
+ }
+ ~ReentrantMonitorConditionallyEnter(void) {
+ if (mReentrantMonitor) {
+ mReentrantMonitor->Exit();
+ }
+ MOZ_COUNT_DTOR(ReentrantMonitorConditionallyEnter);
+ }
+
+ private:
+ // Restrict to constructor and destructor defined above.
+ ReentrantMonitorConditionallyEnter();
+ ReentrantMonitorConditionallyEnter(const ReentrantMonitorConditionallyEnter&);
+ ReentrantMonitorConditionallyEnter& operator=(
+ const ReentrantMonitorConditionallyEnter&);
+ static void* operator new(size_t) noexcept(true);
+ static void operator delete(void*);
+
+ ReentrantMonitor* mReentrantMonitor;
+};
+
+// Shuts down a thread asynchronously.
+class ShutdownThreadEvent : public Runnable {
+ public:
+ explicit ShutdownThreadEvent(nsIThread* aThread)
+ : Runnable("ShutdownThreadEvent"), mThread(aThread) {}
+ ~ShutdownThreadEvent() = default;
+ NS_IMETHOD Run() override {
+ mThread->Shutdown();
+ mThread = nullptr;
+ return NS_OK;
+ }
+
+ private:
+ nsCOMPtr<nsIThread> mThread;
+};
+
+class MediaResource;
+
+// Estimates the buffered ranges of a MediaResource using a simple
+// (byteOffset/length)*duration method. Probably inaccurate, but won't
+// do file I/O, and can be used when we don't have detailed knowledge
+// of the byte->time mapping of a resource. aDurationUsecs is the duration
+// of the media in microseconds. Estimated buffered ranges are stored in
+// aOutBuffered. Ranges are 0-normalized, i.e. in the range of (0,duration].
+media::TimeIntervals GetEstimatedBufferedTimeRanges(
+ mozilla::MediaResource* aStream, int64_t aDurationUsecs);
+
+double ToMicrosecondResolution(double aSeconds);
+// Converts from number of audio frames (aFrames) to microseconds, given
+// the specified audio rate (aRate).
+CheckedInt64 FramesToUsecs(int64_t aFrames, uint32_t aRate);
+// Converts from number of audio frames (aFrames) TimeUnit, given
+// the specified audio rate (aRate).
+media::TimeUnit FramesToTimeUnit(int64_t aFrames, uint32_t aRate);
+// Perform aValue * aMul / aDiv, reducing the possibility of overflow due to
+// aValue * aMul overflowing.
+CheckedInt64 SaferMultDiv(int64_t aValue, uint64_t aMul, uint64_t aDiv);
+
+// Converts from microseconds (aUsecs) to number of audio frames, given the
+// specified audio rate (aRate). Stores the result in aOutFrames. Returns
+// true if the operation succeeded, or false if there was an integer
+// overflow while calulating the conversion.
+CheckedInt64 UsecsToFrames(int64_t aUsecs, uint32_t aRate);
+
+// Format TimeUnit as number of frames at given rate.
+CheckedInt64 TimeUnitToFrames(const media::TimeUnit& aTime, uint32_t aRate);
+
+// Converts milliseconds to seconds.
+#define MS_TO_SECONDS(ms) ((double)(ms) / (PR_MSEC_PER_SEC))
+
+// Converts seconds to milliseconds.
+#define SECONDS_TO_MS(s) ((int)((s) * (PR_MSEC_PER_SEC)))
+
+// Converts from seconds to microseconds. Returns failure if the resulting
+// integer is too big to fit in an int64_t.
+nsresult SecondsToUsecs(double aSeconds, int64_t& aOutUsecs);
+
+// Scales the display rect aDisplay by aspect ratio aAspectRatio.
+// Note that aDisplay must be validated by IsValidVideoRegion()
+// before being used!
+void ScaleDisplayByAspectRatio(gfx::IntSize& aDisplay, float aAspectRatio);
+
+// Downmix Stereo audio samples to Mono.
+// Input are the buffer contains stereo data and the number of frames.
+void DownmixStereoToMono(mozilla::AudioDataValue* aBuffer, uint32_t aFrames);
+
+// Decide the number of playback channels according to the
+// given AudioInfo and the prefs that are being set.
+uint32_t DecideAudioPlaybackChannels(const AudioInfo& info);
+
+// Decide the sample-rate to use for audio output according to the
+// given AudioInfo and the prefs that are being set.
+uint32_t DecideAudioPlaybackSampleRate(const AudioInfo& info,
+ bool aShouldResistFingerprinting);
+
+bool IsDefaultPlaybackDeviceMono();
+
+bool IsVideoContentType(const nsCString& aContentType);
+
+// Returns true if it's safe to use aPicture as the picture to be
+// extracted inside a frame of size aFrame, and scaled up to and displayed
+// at a size of aDisplay. You should validate the frame, picture, and
+// display regions before using them to display video frames.
+bool IsValidVideoRegion(const gfx::IntSize& aFrame,
+ const gfx::IntRect& aPicture,
+ const gfx::IntSize& aDisplay);
+
+// Template to automatically set a variable to a value on scope exit.
+// Useful for unsetting flags, etc.
+template <typename T>
+class AutoSetOnScopeExit {
+ public:
+ AutoSetOnScopeExit(T& aVar, T aValue) : mVar(aVar), mValue(aValue) {}
+ ~AutoSetOnScopeExit() { mVar = mValue; }
+
+ private:
+ T& mVar;
+ const T mValue;
+};
+
+enum class MediaThreadType {
+ SUPERVISOR, // MediaFormatReader, RemoteDecoderManager, MediaDecodeTask and
+ // others
+ PLATFORM_DECODER, // MediaDataDecoder
+ PLATFORM_ENCODER, // MediaDataEncoder
+ WEBRTC_CALL_THREAD,
+ WEBRTC_WORKER,
+ MDSM, // MediaDecoderStateMachine
+};
+// Returns the thread pool that is shared amongst all decoder state machines
+// for decoding streams.
+already_AddRefed<SharedThreadPool> GetMediaThreadPool(MediaThreadType aType);
+
+enum H264_PROFILE {
+ H264_PROFILE_UNKNOWN = 0,
+ H264_PROFILE_BASE = 0x42,
+ H264_PROFILE_MAIN = 0x4D,
+ H264_PROFILE_EXTENDED = 0x58,
+ H264_PROFILE_HIGH = 0x64,
+};
+
+enum H264_LEVEL {
+ H264_LEVEL_1 = 10,
+ H264_LEVEL_1_b = 11,
+ H264_LEVEL_1_1 = 11,
+ H264_LEVEL_1_2 = 12,
+ H264_LEVEL_1_3 = 13,
+ H264_LEVEL_2 = 20,
+ H264_LEVEL_2_1 = 21,
+ H264_LEVEL_2_2 = 22,
+ H264_LEVEL_3 = 30,
+ H264_LEVEL_3_1 = 31,
+ H264_LEVEL_3_2 = 32,
+ H264_LEVEL_4 = 40,
+ H264_LEVEL_4_1 = 41,
+ H264_LEVEL_4_2 = 42,
+ H264_LEVEL_5 = 50,
+ H264_LEVEL_5_1 = 51,
+ H264_LEVEL_5_2 = 52
+};
+
+// Extracts the H.264/AVC profile and level from an H.264 codecs string.
+// H.264 codecs parameters have a type defined as avc1.PPCCLL, where
+// PP = profile_idc, CC = constraint_set flags, LL = level_idc.
+// See
+// http://blog.pearce.org.nz/2013/11/what-does-h264avc1-codecs-parameters.html
+// for more details.
+// Returns false on failure.
+bool ExtractH264CodecDetails(const nsAString& aCodecs, uint8_t& aProfile,
+ uint8_t& aConstraint, uint8_t& aLevel);
+
+struct VideoColorSpace {
+ // Default values are set according to
+ // https://www.webmproject.org/vp9/mp4/#optional-fields
+ // and https://aomediacodec.github.io/av1-isobmff/#codecsparam
+ gfx::CICP::ColourPrimaries mPrimaries = gfx::CICP::CP_BT709;
+ gfx::CICP::TransferCharacteristics mTransfer = gfx::CICP::TC_BT709;
+ gfx::CICP::MatrixCoefficients mMatrix = gfx::CICP::MC_BT709;
+ gfx::ColorRange mRange = gfx::ColorRange::LIMITED;
+
+ bool operator==(const VideoColorSpace& aOther) const {
+ return mPrimaries == aOther.mPrimaries && mTransfer == aOther.mTransfer &&
+ mMatrix == aOther.mMatrix && mRange == aOther.mRange;
+ }
+ bool operator!=(const VideoColorSpace& aOther) const {
+ return !(*this == aOther);
+ }
+};
+
+// Extracts the VPX codecs parameter string.
+// See https://www.webmproject.org/vp9/mp4/#codecs-parameter-string
+// for more details.
+// Returns false on failure.
+bool ExtractVPXCodecDetails(const nsAString& aCodec, uint8_t& aProfile,
+ uint8_t& aLevel, uint8_t& aBitDepth);
+bool ExtractVPXCodecDetails(const nsAString& aCodec, uint8_t& aProfile,
+ uint8_t& aLevel, uint8_t& aBitDepth,
+ uint8_t& aChromaSubsampling,
+ VideoColorSpace& aColorSpace);
+
+// Extracts AV1 codecs parameter string.
+// See https://aomediacodec.github.io/av1-isobmff/#codecsparam
+// Returns false if the codec is invalid.
+bool ExtractAV1CodecDetails(const nsAString& aCodec, uint8_t& aProfile,
+ uint8_t& aLevel, uint8_t& aTier, uint8_t& aBitDepth,
+ bool& aMonochrome, bool& aSubsamplingX,
+ bool& aSubsamplingY, uint8_t& aChromaSamplePosition,
+ VideoColorSpace& aColorSpace);
+
+// Use a cryptographic quality PRNG to generate raw random bytes
+// and convert that to a base64 string.
+nsresult GenerateRandomName(nsCString& aOutSalt, uint32_t aLength);
+
+// This version returns a string suitable for use as a file or URL
+// path. This is based on code from nsExternalAppHandler::SetUpTempFile.
+nsresult GenerateRandomPathName(nsCString& aOutSalt, uint32_t aLength);
+
+already_AddRefed<TaskQueue> CreateMediaDecodeTaskQueue(const char* aName);
+
+// Iteratively invokes aWork until aCondition returns true, or aWork returns
+// false. Use this rather than a while loop to avoid bogarting the task queue.
+template <class Work, class Condition>
+RefPtr<GenericPromise> InvokeUntil(Work aWork, Condition aCondition) {
+ RefPtr<GenericPromise::Private> p = new GenericPromise::Private(__func__);
+
+ if (aCondition()) {
+ p->Resolve(true, __func__);
+ }
+
+ struct Helper {
+ static void Iteration(const RefPtr<GenericPromise::Private>& aPromise,
+ Work aLocalWork, Condition aLocalCondition) {
+ if (!aLocalWork()) {
+ aPromise->Reject(NS_ERROR_FAILURE, __func__);
+ } else if (aLocalCondition()) {
+ aPromise->Resolve(true, __func__);
+ } else {
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ "InvokeUntil::Helper::Iteration",
+ [aPromise, aLocalWork, aLocalCondition]() {
+ Iteration(aPromise, aLocalWork, aLocalCondition);
+ });
+ AbstractThread::GetCurrent()->Dispatch(r.forget());
+ }
+ }
+ };
+
+ Helper::Iteration(p, aWork, aCondition);
+ return p;
+}
+
+// Simple timer to run a runnable after a timeout.
+class SimpleTimer : public nsITimerCallback, public nsINamed {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSINAMED
+
+ // Create a new timer to run aTask after aTimeoutMs milliseconds
+ // on thread aTarget. If aTarget is null, task is run on the main thread.
+ static already_AddRefed<SimpleTimer> Create(
+ nsIRunnable* aTask, uint32_t aTimeoutMs,
+ nsIEventTarget* aTarget = nullptr);
+ void Cancel();
+
+ NS_IMETHOD Notify(nsITimer* timer) override;
+
+ private:
+ virtual ~SimpleTimer() = default;
+ nsresult Init(nsIRunnable* aTask, uint32_t aTimeoutMs,
+ nsIEventTarget* aTarget);
+
+ RefPtr<nsIRunnable> mTask;
+ nsCOMPtr<nsITimer> mTimer;
+};
+
+void LogToBrowserConsole(const nsAString& aMsg);
+
+bool ParseMIMETypeString(const nsAString& aMIMEType,
+ nsString& aOutContainerType,
+ nsTArray<nsString>& aOutCodecs);
+
+bool ParseCodecsString(const nsAString& aCodecs,
+ nsTArray<nsString>& aOutCodecs);
+
+bool IsH264CodecString(const nsAString& aCodec);
+
+bool IsAACCodecString(const nsAString& aCodec);
+
+bool IsVP8CodecString(const nsAString& aCodec);
+
+bool IsVP9CodecString(const nsAString& aCodec);
+
+bool IsAV1CodecString(const nsAString& aCodec);
+
+// Try and create a TrackInfo with a given codec MIME type.
+UniquePtr<TrackInfo> CreateTrackInfoWithMIMEType(
+ const nsACString& aCodecMIMEType);
+
+// Try and create a TrackInfo with a given codec MIME type, and optional extra
+// parameters from a container type (its MIME type and codecs are ignored).
+UniquePtr<TrackInfo> CreateTrackInfoWithMIMETypeAndContainerTypeExtraParameters(
+ const nsACString& aCodecMIMEType, const MediaContainerType& aContainerType);
+
+namespace detail {
+
+// aString should start with aMajor + '/'.
+constexpr bool StartsWithMIMETypeMajor(const char* aString, const char* aMajor,
+ size_t aMajorRemaining) {
+ return (aMajorRemaining == 0 && *aString == '/') ||
+ (*aString == *aMajor &&
+ StartsWithMIMETypeMajor(aString + 1, aMajor + 1,
+ aMajorRemaining - 1));
+}
+
+// aString should only contain [a-z0-9\-\.] and a final '\0'.
+constexpr bool EndsWithMIMESubtype(const char* aString, size_t aRemaining) {
+ return aRemaining == 0 || (((*aString >= 'a' && *aString <= 'z') ||
+ (*aString >= '0' && *aString <= '9') ||
+ *aString == '-' || *aString == '.') &&
+ EndsWithMIMESubtype(aString + 1, aRemaining - 1));
+}
+
+// Simple MIME-type literal string checker with a given (major) type.
+// Only accepts "{aMajor}/[a-z0-9\-\.]+".
+template <size_t MajorLengthPlus1>
+constexpr bool IsMIMETypeWithMajor(const char* aString, size_t aLength,
+ const char (&aMajor)[MajorLengthPlus1]) {
+ return aLength > MajorLengthPlus1 && // Major + '/' + at least 1 char
+ StartsWithMIMETypeMajor(aString, aMajor, MajorLengthPlus1 - 1) &&
+ EndsWithMIMESubtype(aString + MajorLengthPlus1,
+ aLength - MajorLengthPlus1);
+}
+
+} // namespace detail
+
+// Simple MIME-type string checker.
+// Only accepts lowercase "{application,audio,video}/[a-z0-9\-\.]+".
+// Add more if necessary.
+constexpr bool IsMediaMIMEType(const char* aString, size_t aLength) {
+ return detail::IsMIMETypeWithMajor(aString, aLength, "application") ||
+ detail::IsMIMETypeWithMajor(aString, aLength, "audio") ||
+ detail::IsMIMETypeWithMajor(aString, aLength, "video");
+}
+
+// Simple MIME-type string literal checker.
+// Only accepts lowercase "{application,audio,video}/[a-z0-9\-\.]+".
+// Add more if necessary.
+template <size_t LengthPlus1>
+constexpr bool IsMediaMIMEType(const char (&aString)[LengthPlus1]) {
+ return IsMediaMIMEType(aString, LengthPlus1 - 1);
+}
+
+// Simple MIME-type string checker.
+// Only accepts lowercase "{application,audio,video}/[a-z0-9\-\.]+".
+// Add more if necessary.
+inline bool IsMediaMIMEType(const nsACString& aString) {
+ return IsMediaMIMEType(aString.Data(), aString.Length());
+}
+
+enum class StringListRangeEmptyItems {
+ // Skip all empty items (empty string will process nothing)
+ // E.g.: "a,,b" -> ["a", "b"], "" -> nothing
+ Skip,
+ // Process all, except if string is empty
+ // E.g.: "a,,b" -> ["a", "", "b"], "" -> nothing
+ ProcessEmptyItems,
+ // Process all, including 1 empty item in an empty string
+ // E.g.: "a,,b" -> ["a", "", "b"], "" -> [""]
+ ProcessAll
+};
+
+template <typename String,
+ StringListRangeEmptyItems empties = StringListRangeEmptyItems::Skip>
+class StringListRange {
+ typedef typename String::char_type CharType;
+ typedef const CharType* Pointer;
+
+ public:
+ // Iterator into range, trims items and optionally skips empty items.
+ class Iterator {
+ public:
+ bool operator!=(const Iterator& a) const {
+ return mStart != a.mStart || mEnd != a.mEnd;
+ }
+ Iterator& operator++() {
+ SearchItemAt(mComma + 1);
+ return *this;
+ }
+ // DereferencedType should be 'const nsDependent[C]String' pointing into
+ // mList (which is 'const ns[C]String&').
+ typedef decltype(Substring(Pointer(), Pointer())) DereferencedType;
+ DereferencedType operator*() { return Substring(mStart, mEnd); }
+
+ private:
+ friend class StringListRange;
+ Iterator(const CharType* aRangeStart, uint32_t aLength)
+ : mRangeEnd(aRangeStart + aLength),
+ mStart(nullptr),
+ mEnd(nullptr),
+ mComma(nullptr) {
+ SearchItemAt(aRangeStart);
+ }
+ void SearchItemAt(Pointer start) {
+ // First, skip leading whitespace.
+ for (Pointer p = start;; ++p) {
+ if (p >= mRangeEnd) {
+ if (p > mRangeEnd +
+ (empties != StringListRangeEmptyItems::Skip ? 1 : 0)) {
+ p = mRangeEnd +
+ (empties != StringListRangeEmptyItems::Skip ? 1 : 0);
+ }
+ mStart = mEnd = mComma = p;
+ return;
+ }
+ auto c = *p;
+ if (c == CharType(',')) {
+ // Comma -> Empty item -> Skip or process?
+ if (empties != StringListRangeEmptyItems::Skip) {
+ mStart = mEnd = mComma = p;
+ return;
+ }
+ } else if (c != CharType(' ')) {
+ mStart = p;
+ break;
+ }
+ }
+ // Find comma, recording start of trailing space.
+ Pointer trailingWhitespace = nullptr;
+ for (Pointer p = mStart + 1;; ++p) {
+ if (p >= mRangeEnd) {
+ mEnd = trailingWhitespace ? trailingWhitespace : p;
+ mComma = p;
+ return;
+ }
+ auto c = *p;
+ if (c == CharType(',')) {
+ mEnd = trailingWhitespace ? trailingWhitespace : p;
+ mComma = p;
+ return;
+ }
+ if (c == CharType(' ')) {
+ // Found a whitespace -> Record as trailing if not first one.
+ if (!trailingWhitespace) {
+ trailingWhitespace = p;
+ }
+ } else {
+ // Found a non-whitespace -> Reset trailing whitespace if needed.
+ if (trailingWhitespace) {
+ trailingWhitespace = nullptr;
+ }
+ }
+ }
+ }
+ const Pointer mRangeEnd;
+ Pointer mStart;
+ Pointer mEnd;
+ Pointer mComma;
+ };
+
+ explicit StringListRange(const String& aList) : mList(aList) {}
+ Iterator begin() const {
+ return Iterator(
+ mList.Data() +
+ ((empties == StringListRangeEmptyItems::ProcessEmptyItems &&
+ mList.Length() == 0)
+ ? 1
+ : 0),
+ mList.Length());
+ }
+ Iterator end() const {
+ return Iterator(mList.Data() + mList.Length() +
+ (empties != StringListRangeEmptyItems::Skip ? 1 : 0),
+ 0);
+ }
+
+ private:
+ const String& mList;
+};
+
+template <StringListRangeEmptyItems empties = StringListRangeEmptyItems::Skip,
+ typename String>
+StringListRange<String, empties> MakeStringListRange(const String& aList) {
+ return StringListRange<String, empties>(aList);
+}
+
+template <StringListRangeEmptyItems empties = StringListRangeEmptyItems::Skip,
+ typename ListString, typename ItemString>
+static bool StringListContains(const ListString& aList,
+ const ItemString& aItem) {
+ for (const auto& listItem : MakeStringListRange<empties>(aList)) {
+ if (listItem.Equals(aItem)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+inline void AppendStringIfNotEmpty(nsACString& aDest, nsACString&& aSrc) {
+ if (!aSrc.IsEmpty()) {
+ aDest.Append("\n"_ns);
+ aDest.Append(aSrc);
+ }
+}
+
+// Returns true if we're running on a cellular connection; 2G, 3G, etc.
+// Main thread only.
+bool OnCellularConnection();
+
+inline gfx::YUVColorSpace DefaultColorSpace(const gfx::IntSize& aSize) {
+ return aSize.height < 720 ? gfx::YUVColorSpace::BT601
+ : gfx::YUVColorSpace::BT709;
+}
+
+} // end namespace mozilla
+
+#endif
diff --git a/dom/media/VorbisUtils.h b/dom/media/VorbisUtils.h
new file mode 100644
index 0000000000..3d8ac74b00
--- /dev/null
+++ b/dom/media/VorbisUtils.h
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef VORBISUTILS_H_
+#define VORBISUTILS_H_
+
+#ifdef MOZ_SAMPLE_TYPE_S16
+# include <ogg/os_types.h>
+typedef ogg_int32_t VorbisPCMValue;
+
+# define MOZ_CLIP_TO_15(x) ((x) < -32768 ? -32768 : (x) <= 32767 ? (x) : 32767)
+// Convert the output of vorbis_synthesis_pcmout to a AudioDataValue
+# define MOZ_CONVERT_VORBIS_SAMPLE(x) \
+ (static_cast<AudioDataValue>(MOZ_CLIP_TO_15((x) >> 9)))
+
+#else /* MOZ_SAMPLE_TYPE_FLOAT32 */
+
+typedef float VorbisPCMValue;
+
+# define MOZ_CONVERT_VORBIS_SAMPLE(x) (x)
+
+#endif
+
+#endif /* VORBISUTILS_H_ */
diff --git a/dom/media/WavDumper.h b/dom/media/WavDumper.h
new file mode 100644
index 0000000000..de4195066a
--- /dev/null
+++ b/dom/media/WavDumper.h
@@ -0,0 +1,136 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(WavDumper_h_)
+# define WavDumper_h_
+# include <stdio.h>
+# include <stdint.h>
+# include <nsTArray.h>
+# include <nsString.h>
+# include <mozilla/Unused.h>
+# include <mozilla/Atomics.h>
+# include <mozilla/DebugOnly.h>
+# include <mozilla/Sprintf.h>
+# include <ByteWriter.h>
+
+/**
+ * If MOZ_DUMP_AUDIO is set, this dumps a file to disk containing the output of
+ * an audio stream, in 16bits integers.
+ *
+ * The sandbox needs to be disabled for this to work.
+ */
+class WavDumper {
+ public:
+ WavDumper() = default;
+ ~WavDumper() {
+ if (mFile) {
+ fclose(mFile);
+ }
+ }
+
+ void Open(const char* aBaseName, uint32_t aChannels, uint32_t aRate) {
+ using namespace mozilla;
+
+ if (!getenv("MOZ_DUMP_AUDIO")) {
+ return;
+ }
+
+ static mozilla::Atomic<int> sDumpedAudioCount(0);
+
+ char buf[100];
+ SprintfLiteral(buf, "%s-%d.wav", aBaseName, ++sDumpedAudioCount);
+ OpenExplicit(buf, aChannels, aRate);
+ }
+
+ void OpenExplicit(const char* aPath, uint32_t aChannels, uint32_t aRate) {
+# ifdef XP_WIN
+ nsAutoString widePath = NS_ConvertUTF8toUTF16(aPath);
+ mFile = _wfopen(widePath.get(), L"wb");
+# else
+ mFile = fopen(aPath, "wb");
+# endif
+ if (!mFile) {
+ NS_WARNING("Could not open file to DUMP a wav. Is sandboxing disabled?");
+ return;
+ }
+ const uint8_t riffHeader[] = {
+ // RIFF header
+ 0x52, 0x49, 0x46, 0x46, 0x00, 0x00, 0x00, 0x00, 0x57, 0x41, 0x56, 0x45,
+ // fmt chunk. We always write 16-bit samples.
+ 0x66, 0x6d, 0x74, 0x20, 0x10, 0x00, 0x00, 0x00, 0x01, 0x00, 0xFF, 0xFF,
+ 0xFF, 0xFF, 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0x10, 0x00,
+ // data chunk
+ 0x64, 0x61, 0x74, 0x61, 0xFE, 0xFF, 0xFF, 0x7F};
+ AutoTArray<uint8_t, sizeof(riffHeader)> header;
+ mozilla::ByteWriter<mozilla::LittleEndian> writer(header);
+ static const int CHANNEL_OFFSET = 22;
+ static const int SAMPLE_RATE_OFFSET = 24;
+ static const int BLOCK_ALIGN_OFFSET = 32;
+
+ mozilla::DebugOnly<bool> rv;
+ // Then number of bytes written in each iteration.
+ uint32_t written = 0;
+ for (size_t i = 0; i != sizeof(riffHeader);) {
+ switch (i) {
+ case CHANNEL_OFFSET:
+ rv = writer.WriteU16(aChannels);
+ written = 2;
+ MOZ_ASSERT(rv);
+ break;
+ case SAMPLE_RATE_OFFSET:
+ rv = writer.WriteU32(aRate);
+ written = 4;
+ MOZ_ASSERT(rv);
+ break;
+ case BLOCK_ALIGN_OFFSET:
+ rv = writer.WriteU16(aChannels * 2);
+ written = 2;
+ MOZ_ASSERT(rv);
+ break;
+ default:
+ // copy from the riffHeader struct above
+ rv = writer.WriteU8(riffHeader[i]);
+ written = 1;
+ MOZ_ASSERT(rv);
+ break;
+ }
+ i += written;
+ }
+ mozilla::Unused << fwrite(header.Elements(), header.Length(), 1, mFile);
+ }
+
+ template <typename T>
+ void Write(const T* aBuffer, uint32_t aSamples) {
+ if (!mFile) {
+ return;
+ }
+ WriteDumpFileHelper(aBuffer, aSamples);
+ }
+
+ private:
+ void WriteDumpFileHelper(const int16_t* aInput, size_t aSamples) {
+ mozilla::Unused << fwrite(aInput, sizeof(int16_t), aSamples, mFile);
+ fflush(mFile);
+ }
+
+ void WriteDumpFileHelper(const float* aInput, size_t aSamples) {
+ using namespace mozilla;
+
+ AutoTArray<uint8_t, 1024 * 2> buf;
+ mozilla::ByteWriter<mozilla::LittleEndian> writer(buf);
+ for (uint32_t i = 0; i < aSamples; ++i) {
+ mozilla::DebugOnly<bool> rv =
+ writer.WriteU16(int16_t(aInput[i] * 32767.0f));
+ MOZ_ASSERT(rv);
+ }
+ mozilla::Unused << fwrite(buf.Elements(), buf.Length(), 1, mFile);
+ fflush(mFile);
+ }
+
+ FILE* mFile = nullptr;
+};
+
+#endif // WavDumper_h_
diff --git a/dom/media/WebMSample.h b/dom/media/WebMSample.h
new file mode 100644
index 0000000000..ab06e7a68d
--- /dev/null
+++ b/dom/media/WebMSample.h
@@ -0,0 +1,22788 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+static const uint8_t sWebMSample[] = {
+ 0x1a, 0x45, 0xdf, 0xa3, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1f,
+ 0x42, 0x86, 0x81, 0x01, 0x42, 0xf7, 0x81, 0x01, 0x42, 0xf2, 0x81, 0x04,
+ 0x42, 0xf3, 0x81, 0x08, 0x42, 0x82, 0x84, 0x77, 0x65, 0x62, 0x6d, 0x42,
+ 0x87, 0x81, 0x02, 0x42, 0x85, 0x81, 0x02, 0x18, 0x53, 0x80, 0x67, 0x01,
+ 0x00, 0x00, 0x00, 0x00, 0x04, 0x2b, 0x9e, 0x11, 0x4d, 0x9b, 0x74, 0x40,
+ 0x2d, 0x4d, 0xbb, 0x8b, 0x53, 0xab, 0x84, 0x15, 0x49, 0xa9, 0x66, 0x53,
+ 0xac, 0x81, 0xdf, 0x4d, 0xbb, 0x8c, 0x53, 0xab, 0x84, 0x16, 0x54, 0xae,
+ 0x6b, 0x53, 0xac, 0x82, 0x01, 0x54, 0x4d, 0xbb, 0x8d, 0x53, 0xab, 0x84,
+ 0x1c, 0x53, 0xbb, 0x6b, 0x53, 0xac, 0x83, 0x04, 0x2b, 0x81, 0xec, 0x01,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xa3, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x15, 0x49, 0xa9, 0x66, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x69, 0x2a, 0xd7, 0xb1, 0x83, 0x0f, 0x42, 0x40, 0x7b, 0xa9, 0xa1,
+ 0x42, 0x69, 0x67, 0x20, 0x42, 0x75, 0x63, 0x6b, 0x20, 0x42, 0x75, 0x6e,
+ 0x6e, 0x79, 0x2c, 0x20, 0x53, 0x75, 0x6e, 0x66, 0x6c, 0x6f, 0x77, 0x65,
+ 0x72, 0x20, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x4d, 0x80, 0x8d,
+ 0x4c, 0x61, 0x76, 0x66, 0x35, 0x36, 0x2e, 0x33, 0x36, 0x2e, 0x31, 0x30,
+ 0x30, 0x57, 0x41, 0x8d, 0x4c, 0x61, 0x76, 0x66, 0x35, 0x36, 0x2e, 0x33,
+ 0x36, 0x2e, 0x31, 0x30, 0x30, 0x73, 0xa4, 0x90, 0xbc, 0x12, 0x68, 0xf6,
+ 0x3b, 0xee, 0xc2, 0x39, 0xb9, 0xe1, 0x53, 0xda, 0x25, 0xda, 0xfc, 0x3d,
+ 0x44, 0x89, 0x88, 0x40, 0x6e, 0xe0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16,
+ 0x54, 0xae, 0x6b, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x47, 0xae,
+ 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3e, 0xd7, 0x81, 0x01, 0x73,
+ 0xc5, 0x81, 0x01, 0x9c, 0x81, 0x00, 0x22, 0xb5, 0x9c, 0x83, 0x75, 0x6e,
+ 0x64, 0x86, 0x85, 0x56, 0x5f, 0x56, 0x50, 0x39, 0x83, 0x81, 0x01, 0x23,
+ 0xe3, 0x83, 0x84, 0x01, 0xfc, 0xa0, 0x55, 0xe0, 0x01, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x12, 0xb0, 0x82, 0x05, 0x00, 0xba, 0x82, 0x02, 0xd0,
+ 0x54, 0xb0, 0x82, 0x05, 0x00, 0x54, 0xba, 0x82, 0x02, 0xd0, 0x1f, 0x43,
+ 0xb6, 0x75, 0x01, 0x00, 0x00, 0x00, 0x00, 0x04, 0x29, 0xce, 0xe7, 0x81,
+ 0x0e, 0xa3, 0x22, 0xf7, 0x4e, 0x81, 0x00, 0x00, 0x80, 0x82, 0x49, 0x83,
+ 0x42, 0x00, 0x4f, 0xf0, 0x2c, 0xf6, 0x00, 0x38, 0x24, 0x1c, 0x18, 0x30,
+ 0x18, 0x06, 0x2c, 0x7f, 0xf9, 0xaf, 0xf9, 0x5f, 0xa0, 0xff, 0x33, 0xf4,
+ 0x7d, 0x1f, 0xa6, 0xf6, 0x9f, 0xcd, 0xfe, 0xf7, 0xfd, 0xe7, 0xf5, 0xff,
+ 0xaf, 0xff, 0x17, 0xf1, 0x3f, 0xe4, 0x3f, 0xc1, 0xff, 0x7b, 0xfe, 0x33,
+ 0xed, 0x9f, 0xf8, 0x1f, 0xe4, 0xff, 0x33, 0xfd, 0xef, 0xab, 0xff, 0x07,
+ 0xe9, 0xbd, 0xda, 0xff, 0x89, 0xfe, 0x6f, 0xf4, 0xff, 0xe3, 0x3f, 0x07,
+ 0xe2, 0xff, 0x81, 0xfc, 0x1f, 0xe9, 0xff, 0xb3, 0xf6, 0x87, 0xf9, 0x5e,
+ 0x63, 0x7f, 0x3f, 0xfa, 0x5f, 0xdb, 0xfd, 0xff, 0xec, 0x7e, 0x0f, 0xf0,
+ 0xff, 0xc9, 0xfe, 0x8f, 0xed, 0xfe, 0x6e, 0xae, 0xfa, 0xdf, 0xb4, 0x3f,
+ 0x9b, 0xba, 0x35, 0xc2, 0xfa, 0xd1, 0xfb, 0x19, 0xf0, 0x7f, 0xdf, 0xff,
+ 0x23, 0xfd, 0xff, 0xf2, 0x3f, 0xc8, 0xff, 0x89, 0xf8, 0x7f, 0xe7, 0xff,
+ 0x91, 0xf8, 0xfe, 0xc5, 0xff, 0x57, 0xec, 0x5f, 0xc5, 0xfe, 0x17, 0x1b,
+ 0x9f, 0x5f, 0xfe, 0x7f, 0xf8, 0x1f, 0xdf, 0x98, 0xb6, 0x17, 0x3c, 0x77,
+ 0xbb, 0x18, 0xa3, 0xd2, 0xf6, 0x42, 0x27, 0xdc, 0xf8, 0x07, 0xe1, 0xfe,
+ 0xa9, 0xfd, 0x11, 0xd3, 0xbd, 0xbf, 0xa8, 0xf5, 0xdf, 0xd7, 0xdc, 0xda,
+ 0x1d, 0x23, 0xf8, 0x1c, 0xc9, 0xde, 0xff, 0xbf, 0x74, 0xef, 0xea, 0x6e,
+ 0x91, 0xea, 0xfc, 0x60, 0xf5, 0x1c, 0xb3, 0x72, 0x4f, 0x92, 0xde, 0xaf,
+ 0xc9, 0x0f, 0xe1, 0x3c, 0x6c, 0xf5, 0x7e, 0x28, 0xff, 0x1b, 0x58, 0xbe,
+ 0xf7, 0xf4, 0x8f, 0x17, 0x7f, 0x6f, 0xf2, 0x07, 0xd5, 0xf8, 0xb5, 0xfa,
+ 0xd4, 0x81, 0xfa, 0x5f, 0x4b, 0xf7, 0x5f, 0xc6, 0xfe, 0xfd, 0xe4, 0x6f,
+ 0xe9, 0xec, 0xa5, 0xf7, 0x45, 0xfe, 0x47, 0xdc, 0x8f, 0xc3, 0xf8, 0xdf,
+ 0xef, 0x7a, 0x2d, 0x07, 0x74, 0x5a, 0xd7, 0x23, 0x36, 0x6f, 0x1d, 0x21,
+ 0xe5, 0xad, 0x91, 0x97, 0x3d, 0x6b, 0xe2, 0xa7, 0xa8, 0x96, 0x9b, 0xbb,
+ 0x96, 0x0b, 0xa3, 0xbc, 0x77, 0x47, 0x27, 0xc3, 0x39, 0xfc, 0xb7, 0x34,
+ 0x76, 0xe8, 0xe4, 0xfc, 0xdb, 0xff, 0x8a, 0xe9, 0x1f, 0xd4, 0x1d, 0x23,
+ 0xf9, 0xcf, 0x1b, 0x3d, 0x45, 0x41, 0xfe, 0x9d, 0xd1, 0x7f, 0x7e, 0xf2,
+ 0x37, 0xf9, 0xff, 0x18, 0x3f, 0x60, 0xbf, 0xb7, 0x40, 0x7e, 0xaf, 0xbe,
+ 0xc7, 0xe2, 0xfb, 0x9c, 0xf9, 0x1f, 0xa7, 0xf7, 0x7c, 0x7f, 0xcd, 0xfa,
+ 0x35, 0x35, 0xba, 0xfa, 0x3d, 0x4d, 0xfe, 0xdf, 0x2c, 0x1f, 0x1c, 0xe7,
+ 0xba, 0xfd, 0x33, 0xa4, 0x7b, 0xbd, 0xed, 0xbe, 0xa7, 0xd5, 0xfb, 0xd0,
+ 0x7b, 0xbd, 0xee, 0x67, 0x84, 0xec, 0x79, 0xef, 0x71, 0x8d, 0xed, 0xbe,
+ 0xe2, 0xf9, 0x47, 0xf4, 0x7f, 0x67, 0xf2, 0x7f, 0x9b, 0xfe, 0x8f, 0xc2,
+ 0xff, 0x5f, 0xeb, 0x7f, 0xc5, 0xea, 0x92, 0x55, 0x9b, 0xd2, 0xf7, 0x29,
+ 0xd6, 0xf1, 0xba, 0x2c, 0x39, 0x7a, 0x8f, 0x15, 0x3f, 0x58, 0x60, 0xe0,
+ 0x80, 0x00, 0x00, 0x00, 0xdb, 0xb2, 0x7f, 0x94, 0x28, 0xff, 0x8e, 0x3f,
+ 0xae, 0x8d, 0x9d, 0xf1, 0xce, 0x16, 0xb8, 0x14, 0x58, 0xd4, 0xcb, 0x27,
+ 0x3d, 0xc0, 0x71, 0x82, 0x61, 0xbe, 0xc3, 0xfa, 0xf3, 0x89, 0x79, 0x80,
+ 0x4c, 0x6a, 0x99, 0x53, 0xe2, 0x4e, 0xfa, 0x2c, 0xb8, 0x37, 0xff, 0xea,
+ 0xbc, 0xbd, 0xc2, 0xf8, 0x2c, 0xa6, 0x8f, 0xff, 0xa5, 0x21, 0x68, 0x67,
+ 0xbe, 0x10, 0x15, 0x6e, 0xfb, 0xae, 0xb7, 0x1a, 0x30, 0x12, 0x8d, 0x4a,
+ 0x0a, 0x4a, 0x47, 0xe4, 0xab, 0xd6, 0x8a, 0x35, 0x53, 0x4e, 0x82, 0xee,
+ 0x7c, 0x69, 0x0c, 0x65, 0x80, 0x59, 0x29, 0xbc, 0xd3, 0xdf, 0x20, 0xf6,
+ 0xa0, 0x8e, 0xfa, 0xb4, 0xea, 0xf4, 0x4c, 0x3d, 0x8f, 0x61, 0x8d, 0x35,
+ 0x05, 0x82, 0xe0, 0xfc, 0xc0, 0xec, 0x7e, 0x09, 0x52, 0xe0, 0xf4, 0xa8,
+ 0x48, 0xa8, 0x71, 0x8b, 0x0a, 0xcd, 0xa6, 0x7d, 0x9e, 0x44, 0x13, 0x34,
+ 0xb7, 0xd7, 0xdf, 0x22, 0xdf, 0xd0, 0xbc, 0xc1, 0x9c, 0xe0, 0x0e, 0x47,
+ 0x60, 0x9e, 0xce, 0x97, 0x48, 0x7c, 0x8a, 0x14, 0x3e, 0xa9, 0x17, 0xf6,
+ 0xc6, 0xf9, 0x3e, 0x3d, 0x3a, 0x32, 0xc6, 0x95, 0x35, 0x05, 0x61, 0x59,
+ 0xad, 0x7b, 0x30, 0xe8, 0xff, 0xbf, 0x1b, 0x81, 0x99, 0xd7, 0xc6, 0xb8,
+ 0x67, 0x9b, 0x50, 0x4e, 0xb3, 0xfc, 0x7c, 0x94, 0xc4, 0xa8, 0x92, 0x2b,
+ 0x43, 0x65, 0x01, 0xbb, 0xce, 0x6e, 0x2d, 0x82, 0xcd, 0x65, 0x6d, 0x16,
+ 0x70, 0x7c, 0x9a, 0xd0, 0xac, 0x7c, 0x56, 0xce, 0x10, 0xd3, 0x2f, 0x2a,
+ 0xc5, 0xc5, 0xcb, 0x3d, 0x40, 0x6b, 0x88, 0x35, 0x69, 0x8e, 0xaf, 0x61,
+ 0x73, 0x2c, 0xfa, 0x36, 0xa5, 0x2f, 0x75, 0xc2, 0x65, 0x37, 0x36, 0x05,
+ 0x08, 0x17, 0xec, 0x4a, 0x27, 0x81, 0xdf, 0x27, 0xdd, 0x7a, 0xa9, 0x45,
+ 0xc3, 0xa1, 0xb3, 0xa9, 0x69, 0xb2, 0x25, 0xf9, 0x5d, 0x49, 0xde, 0xf3,
+ 0xd6, 0x04, 0x0b, 0x8b, 0xde, 0x99, 0x6f, 0x93, 0x2d, 0x99, 0x08, 0x73,
+ 0x6b, 0x69, 0xbf, 0x70, 0x8b, 0x93, 0x1b, 0x78, 0x27, 0xcd, 0x62, 0xb8,
+ 0xff, 0x99, 0xa8, 0xdf, 0xc0, 0x89, 0xd1, 0xd9, 0x1a, 0x08, 0xb2, 0xf3,
+ 0x70, 0xef, 0xc6, 0xf3, 0xdc, 0xd3, 0xc4, 0x1e, 0x03, 0x38, 0xfe, 0xf0,
+ 0x82, 0x41, 0xe5, 0x9f, 0xee, 0xd4, 0xd6, 0x87, 0x95, 0x08, 0xec, 0x01,
+ 0x5d, 0x58, 0x07, 0x84, 0x9a, 0x78, 0x40, 0xa9, 0x44, 0x3b, 0x25, 0xf0,
+ 0xd6, 0x74, 0x66, 0xf5, 0x36, 0x9e, 0x33, 0x3d, 0x1d, 0xc4, 0x00, 0x7e,
+ 0x2a, 0x02, 0xd4, 0xc3, 0xfd, 0xfc, 0x09, 0x8f, 0x89, 0x04, 0x3d, 0xe0,
+ 0x34, 0x03, 0x9d, 0x7a, 0x68, 0x4e, 0x87, 0xbf, 0x19, 0x4f, 0x3f, 0x48,
+ 0xb2, 0xf7, 0x54, 0xc6, 0xc3, 0xe8, 0x9d, 0xc8, 0xf1, 0x05, 0x9c, 0x22,
+ 0xac, 0x43, 0xac, 0x0e, 0x34, 0x0e, 0x86, 0x8f, 0xfe, 0xdf, 0xde, 0x7d,
+ 0xb8, 0xe3, 0x7b, 0x28, 0x41, 0xa0, 0x4a, 0x62, 0x84, 0x4e, 0xe2, 0xa8,
+ 0x13, 0x0c, 0x74, 0x41, 0xe0, 0x52, 0xdc, 0x70, 0xc2, 0x43, 0xea, 0xc5,
+ 0xf6, 0xac, 0x67, 0xa2, 0x75, 0x59, 0x35, 0x4b, 0xda, 0xd9, 0x46, 0x82,
+ 0x6c, 0x56, 0xe9, 0x7f, 0x69, 0xdb, 0xf4, 0x7c, 0xf4, 0x61, 0x04, 0x84,
+ 0xc7, 0x5d, 0x6d, 0xbb, 0x75, 0x0a, 0xb6, 0x33, 0xe6, 0x04, 0x18, 0x41,
+ 0x9d, 0x30, 0x17, 0xd3, 0x8f, 0x01, 0x1d, 0xb1, 0xa7, 0x7e, 0x14, 0x6b,
+ 0x81, 0x51, 0xbe, 0x30, 0xfa, 0x79, 0x45, 0x8e, 0x87, 0x5f, 0x9e, 0x42,
+ 0xdd, 0x57, 0x6e, 0x7f, 0xb9, 0x8f, 0x54, 0xd3, 0x15, 0x78, 0xf5, 0x21,
+ 0x24, 0xb9, 0x38, 0xcb, 0xde, 0xf2, 0x3b, 0x5e, 0x9c, 0x80, 0x9b, 0x69,
+ 0x62, 0x06, 0x54, 0x77, 0x73, 0xb5, 0x15, 0xef, 0xc8, 0x91, 0x4d, 0x53,
+ 0x41, 0xd6, 0x7b, 0x83, 0x4d, 0x8b, 0x75, 0x5b, 0xc3, 0x94, 0x9a, 0xd2,
+ 0xdb, 0x28, 0x81, 0xd2, 0x23, 0x6a, 0x9e, 0xef, 0xea, 0xa2, 0x1c, 0x31,
+ 0xb5, 0x24, 0x34, 0xee, 0x05, 0xfd, 0xbf, 0x01, 0x97, 0xca, 0x26, 0xeb,
+ 0x8e, 0x2c, 0xb3, 0xcc, 0x9e, 0x4a, 0xc5, 0x12, 0x83, 0x65, 0x3c, 0x5a,
+ 0x67, 0xa8, 0x7c, 0x89, 0x71, 0x28, 0x98, 0x6c, 0x1f, 0x57, 0x6b, 0x28,
+ 0xdf, 0x67, 0xa4, 0x1e, 0x10, 0x7a, 0x87, 0x4c, 0x0a, 0x62, 0x3a, 0xd6,
+ 0x60, 0xe9, 0x16, 0xe3, 0x36, 0x4d, 0x0f, 0x05, 0x9d, 0x7d, 0xe6, 0x8f,
+ 0xa7, 0x22, 0x35, 0x2e, 0x1a, 0xd5, 0x2f, 0xf5, 0xf0, 0xe1, 0x0d, 0xd1,
+ 0x36, 0x80, 0xc8, 0x99, 0x70, 0x50, 0x81, 0x41, 0x0c, 0x50, 0x54, 0xe3,
+ 0x43, 0xaf, 0xfd, 0xed, 0xef, 0x90, 0x56, 0x66, 0x24, 0x47, 0x7f, 0x1d,
+ 0x4a, 0x3e, 0x0d, 0xc8, 0x51, 0x65, 0xbc, 0xdf, 0x79, 0x28, 0x25, 0xea,
+ 0xac, 0x88, 0x4d, 0x41, 0xe4, 0x81, 0x90, 0x64, 0x36, 0x99, 0x78, 0xec,
+ 0xae, 0xd0, 0xfe, 0x93, 0xf3, 0xba, 0x9e, 0x39, 0xb6, 0xa1, 0x8b, 0x90,
+ 0xcb, 0xf9, 0xf9, 0xd0, 0xf2, 0xf1, 0x78, 0xc4, 0x09, 0xe9, 0xba, 0x26,
+ 0x7f, 0xf5, 0x0e, 0x99, 0x6c, 0xab, 0x5e, 0x92, 0xf3, 0x19, 0xbb, 0xc9,
+ 0x4f, 0x24, 0x88, 0x11, 0xd9, 0xa7, 0x4a, 0x33, 0x8d, 0xdb, 0x90, 0xfb,
+ 0xb6, 0xff, 0x5b, 0xdf, 0x9f, 0xb1, 0xee, 0x5e, 0x90, 0x9c, 0xaf, 0x5e,
+ 0xaf, 0xc3, 0x7d, 0x4d, 0x93, 0x3d, 0x25, 0x89, 0x05, 0x38, 0x6e, 0x01,
+ 0x17, 0xa7, 0xac, 0x0f, 0x52, 0xc4, 0x3e, 0xfc, 0x79, 0x90, 0xbe, 0x63,
+ 0xf5, 0x97, 0x1b, 0xcd, 0xc7, 0x87, 0xeb, 0x6a, 0x20, 0xe5, 0x18, 0x66,
+ 0x43, 0x54, 0x82, 0xe0, 0x7b, 0x9a, 0x1b, 0xc2, 0xe9, 0x59, 0xb4, 0x4a,
+ 0x18, 0x5c, 0x6d, 0x7d, 0x7e, 0x7d, 0x21, 0xda, 0x90, 0x7a, 0x0f, 0x5a,
+ 0x89, 0xfd, 0x43, 0xc2, 0xc9, 0xb5, 0x67, 0x8e, 0x97, 0xef, 0x2e, 0xfd,
+ 0xda, 0x71, 0x74, 0xda, 0x00, 0x75, 0xed, 0x4a, 0xb1, 0x3d, 0x57, 0x99,
+ 0xe1, 0x3c, 0x29, 0x8f, 0x84, 0x18, 0xf7, 0x47, 0xe9, 0x2f, 0x74, 0x38,
+ 0xe6, 0xaf, 0xbf, 0xfd, 0x1f, 0x64, 0x17, 0x5d, 0x7f, 0xfa, 0x24, 0x10,
+ 0x58, 0xcf, 0x7d, 0x80, 0x5e, 0x39, 0x38, 0xa2, 0x11, 0x0f, 0xf6, 0x5b,
+ 0xdb, 0xd3, 0xbd, 0x9c, 0x18, 0x6b, 0x88, 0x5c, 0xf1, 0x08, 0x21, 0x98,
+ 0xd2, 0x81, 0x16, 0x94, 0xf1, 0x07, 0xfc, 0xcc, 0x92, 0xc5, 0x04, 0xb8,
+ 0x06, 0x6a, 0x79, 0x9a, 0xad, 0x01, 0xf0, 0xba, 0x5c, 0xf1, 0xe6, 0xe3,
+ 0xb7, 0x4e, 0xcd, 0x1c, 0x43, 0x89, 0x48, 0xab, 0x03, 0x4d, 0xe4, 0xd6,
+ 0x14, 0xad, 0xf7, 0xf3, 0xc0, 0xda, 0x51, 0xe2, 0xa9, 0x56, 0xf7, 0xcb,
+ 0x98, 0x70, 0x9e, 0x88, 0x6d, 0xe6, 0xa0, 0x06, 0xfd, 0x92, 0xf0, 0xb8,
+ 0x16, 0xe9, 0x8d, 0x79, 0x60, 0x1b, 0x4b, 0x0b, 0x0e, 0xa1, 0xd5, 0x51,
+ 0x98, 0x17, 0xc0, 0xa8, 0xf1, 0x6b, 0x19, 0xd1, 0x24, 0xed, 0x7f, 0xdf,
+ 0x57, 0xfc, 0xa7, 0xf5, 0xdc, 0xce, 0x7e, 0xc2, 0x00, 0x09, 0x6e, 0xf5,
+ 0x99, 0x10, 0x5f, 0x7f, 0x5c, 0x97, 0xe8, 0xb0, 0xd8, 0xd4, 0x78, 0x0e,
+ 0xd3, 0xa3, 0x4b, 0x69, 0xb1, 0xf9, 0x6a, 0x98, 0x86, 0x88, 0x5c, 0x61,
+ 0x2b, 0x47, 0x62, 0x53, 0x53, 0xb1, 0xe8, 0x29, 0x5e, 0xaa, 0x26, 0x29,
+ 0xdd, 0x6a, 0xe9, 0x2c, 0xd4, 0x80, 0xbc, 0xdf, 0x97, 0x5d, 0xf3, 0x3f,
+ 0xec, 0xd5, 0x5c, 0xa7, 0xdc, 0xa0, 0x78, 0xee, 0xaf, 0x37, 0x5e, 0x4c,
+ 0x6f, 0xce, 0x9b, 0x41, 0xbf, 0x51, 0x46, 0x68, 0x80, 0x27, 0x4c, 0x21,
+ 0x0e, 0xdc, 0xae, 0xb5, 0x2f, 0x01, 0x15, 0x1a, 0x03, 0x66, 0x6c, 0xd9,
+ 0x68, 0xb7, 0x9a, 0x7b, 0x56, 0xcf, 0xf4, 0xe0, 0xb7, 0xd9, 0xc4, 0x1e,
+ 0xa5, 0x49, 0x57, 0xa0, 0xa4, 0x1e, 0xfe, 0xbe, 0x8e, 0xb5, 0x02, 0x0c,
+ 0xb7, 0xaf, 0x2f, 0x88, 0x95, 0x90, 0xe1, 0x0b, 0xbb, 0x5e, 0xdb, 0x15,
+ 0x62, 0xa7, 0x4d, 0x16, 0x4a, 0x65, 0x92, 0xd9, 0x16, 0xeb, 0x65, 0x76,
+ 0x7f, 0xc6, 0x45, 0x63, 0xd0, 0x7d, 0xd3, 0x7a, 0xea, 0x65, 0x46, 0x90,
+ 0xf6, 0x35, 0x23, 0x61, 0xd3, 0xdf, 0x65, 0x85, 0x50, 0x53, 0x00, 0xcc,
+ 0xdb, 0x0c, 0x3f, 0xf6, 0x15, 0x71, 0xd6, 0x57, 0x02, 0xcb, 0xe9, 0x4b,
+ 0x93, 0x35, 0xe9, 0xef, 0xf9, 0x80, 0xac, 0xe9, 0xef, 0x88, 0x0b, 0x44,
+ 0x6e, 0xb5, 0xbf, 0x82, 0xc4, 0x01, 0xad, 0x0c, 0xb7, 0x19, 0x2b, 0xaa,
+ 0xcf, 0x4c, 0x2f, 0xf4, 0x46, 0xb9, 0x02, 0x75, 0xd0, 0x03, 0x3b, 0xd7,
+ 0x72, 0x33, 0x86, 0x38, 0xbf, 0x53, 0x22, 0x5c, 0xa6, 0x90, 0xe2, 0x07,
+ 0x57, 0x0b, 0x37, 0x8a, 0x6a, 0xa5, 0x3a, 0x36, 0x4f, 0x90, 0xfc, 0xfd,
+ 0x7a, 0x54, 0x58, 0x70, 0x8b, 0x28, 0xd7, 0xaa, 0x87, 0x3d, 0x6d, 0xc0,
+ 0xf3, 0xd7, 0xfe, 0xcf, 0x9f, 0x2b, 0x97, 0x65, 0x79, 0x93, 0xff, 0x40,
+ 0xcd, 0x6f, 0x23, 0x48, 0x0a, 0x84, 0xe4, 0x87, 0xb5, 0x8b, 0x71, 0xfc,
+ 0xf3, 0x8a, 0xbe, 0x5f, 0xf5, 0xf7, 0xa2, 0xf0, 0xd5, 0x73, 0xc9, 0x6e,
+ 0xfe, 0x08, 0xfb, 0x0e, 0xfe, 0x88, 0xe7, 0xce, 0x23, 0x26, 0x2c, 0x7c,
+ 0x47, 0x26, 0x85, 0x36, 0x65, 0x84, 0x67, 0xcb, 0xfd, 0x34, 0x03, 0x6c,
+ 0x4b, 0xf1, 0x86, 0x77, 0xa3, 0xbe, 0x93, 0x66, 0x09, 0x83, 0xfd, 0xae,
+ 0x3d, 0x35, 0x05, 0x14, 0x76, 0xb3, 0x87, 0x6a, 0x2d, 0x9f, 0x4a, 0x5c,
+ 0x0c, 0x17, 0x2f, 0xe1, 0xa1, 0xf1, 0xcd, 0xf9, 0x63, 0x44, 0x5f, 0xe8,
+ 0xbf, 0x5d, 0x09, 0x0a, 0xb0, 0x98, 0x2e, 0x7a, 0xcc, 0xb1, 0xbf, 0x58,
+ 0x20, 0x28, 0xfb, 0x41, 0xdc, 0x88, 0x28, 0xd1, 0xd4, 0x65, 0x83, 0xec,
+ 0xac, 0x79, 0x90, 0x4d, 0x0d, 0x4f, 0x6e, 0x16, 0x33, 0x31, 0x3e, 0x61,
+ 0xe3, 0x71, 0x5c, 0xdd, 0x32, 0x64, 0x32, 0xaf, 0x84, 0xe5, 0xe7, 0x66,
+ 0xd0, 0xcd, 0x57, 0xcd, 0x6a, 0x15, 0xb2, 0x3b, 0xd7, 0xde, 0x78, 0xdb,
+ 0x2d, 0xaf, 0x22, 0x53, 0x07, 0xf3, 0xff, 0x09, 0x83, 0x48, 0xde, 0xbc,
+ 0x4d, 0x31, 0x6a, 0xf2, 0x5f, 0x49, 0x59, 0x29, 0x76, 0xe5, 0xba, 0xa2,
+ 0x58, 0x55, 0x56, 0xca, 0x06, 0x3c, 0x91, 0xbd, 0xeb, 0x7f, 0xfb, 0xbc,
+ 0xf5, 0x02, 0x19, 0x5a, 0xff, 0x39, 0x3b, 0x8f, 0x0a, 0xf4, 0xc3, 0x3a,
+ 0x3c, 0x3b, 0x48, 0x3c, 0x6c, 0xaa, 0xe2, 0x9d, 0x91, 0xdd, 0xbe, 0xd9,
+ 0x9d, 0x29, 0x22, 0x62, 0x92, 0x00, 0xec, 0xad, 0xa0, 0x8e, 0x8d, 0xfa,
+ 0xa7, 0x9c, 0x07, 0xf2, 0xbe, 0x74, 0x07, 0xf0, 0x82, 0x8c, 0x0b, 0xfd,
+ 0x89, 0x85, 0xad, 0x3d, 0xd6, 0x7f, 0x62, 0xe7, 0xd4, 0xef, 0x9f, 0xca,
+ 0x74, 0x2c, 0xc8, 0xfa, 0xc2, 0xed, 0x75, 0x09, 0x5b, 0x70, 0xdb, 0x8b,
+ 0x3b, 0xbf, 0x20, 0xaa, 0x97, 0xe1, 0x06, 0x3c, 0x8b, 0x32, 0xaf, 0x7e,
+ 0x05, 0x46, 0xfe, 0x18, 0xf1, 0xf0, 0x8c, 0xf5, 0x4e, 0xf8, 0xde, 0xfa,
+ 0x22, 0xa6, 0xd1, 0x0c, 0x67, 0x13, 0x6e, 0xdc, 0xcd, 0x65, 0x50, 0x4f,
+ 0xdf, 0x40, 0xaf, 0xc5, 0xec, 0x1c, 0x78, 0x82, 0x45, 0x7e, 0x85, 0xb7,
+ 0xbb, 0xea, 0x11, 0x57, 0xf9, 0xfc, 0x01, 0x6f, 0xec, 0xf6, 0x96, 0x86,
+ 0x38, 0x4a, 0xbb, 0xe9, 0x70, 0x91, 0x07, 0x23, 0x9a, 0xe7, 0x2c, 0xba,
+ 0x40, 0x68, 0x91, 0x86, 0x8d, 0x9b, 0xd5, 0x5b, 0xe1, 0xd1, 0x1a, 0x6e,
+ 0x14, 0xa6, 0x87, 0x1f, 0x1e, 0xd1, 0x1e, 0x0f, 0xea, 0xd6, 0x0b, 0xc9,
+ 0xeb, 0x26, 0x33, 0x86, 0xad, 0x6f, 0xda, 0x43, 0x30, 0x40, 0xec, 0x65,
+ 0xa0, 0x63, 0xa3, 0xed, 0xb1, 0xb8, 0xa1, 0x3b, 0x7f, 0x56, 0xe4, 0x79,
+ 0x89, 0x06, 0x00, 0x26, 0xc1, 0xbf, 0x42, 0xc8, 0xea, 0x5e, 0xc5, 0x14,
+ 0x03, 0x66, 0x3c, 0x18, 0x61, 0xfb, 0x52, 0x95, 0x02, 0xd2, 0x9c, 0x51,
+ 0xef, 0xc9, 0xb7, 0xd1, 0xf0, 0x16, 0x59, 0xa6, 0x85, 0x20, 0x9c, 0x3c,
+ 0x57, 0x53, 0x8c, 0xe0, 0x2a, 0x0e, 0x54, 0x5c, 0x1c, 0x25, 0xd5, 0xb7,
+ 0xcf, 0x4a, 0x2c, 0xff, 0x59, 0xbf, 0xfe, 0x36, 0x76, 0x1a, 0x28, 0x5e,
+ 0x2a, 0xe6, 0x5d, 0x48, 0x0e, 0x2e, 0x68, 0x49, 0xcb, 0x02, 0xa9, 0xb2,
+ 0xa6, 0xbe, 0xd8, 0x80, 0x64, 0x84, 0x4e, 0x67, 0x46, 0x7b, 0x35, 0x5d,
+ 0xf3, 0xa9, 0x0f, 0xb8, 0x8f, 0x22, 0x48, 0x1f, 0x20, 0x78, 0x1e, 0x66,
+ 0x1c, 0xe3, 0x56, 0xff, 0xe8, 0xf1, 0x41, 0xf0, 0x10, 0xf5, 0xb4, 0x22,
+ 0xc4, 0xc0, 0xe1, 0xac, 0xe7, 0x79, 0xfb, 0xf9, 0x49, 0xe3, 0x2e, 0xa2,
+ 0x59, 0x80, 0xa0, 0xcb, 0xe3, 0xba, 0x00, 0x5e, 0xfe, 0x66, 0xc1, 0x1b,
+ 0x29, 0x34, 0xc1, 0x17, 0xa3, 0xa0, 0x82, 0x14, 0x46, 0x9c, 0x97, 0x82,
+ 0x33, 0x43, 0xbf, 0x89, 0x03, 0x9c, 0x1c, 0x9f, 0xa9, 0x10, 0x03, 0x83,
+ 0x2f, 0x4b, 0x6e, 0xff, 0x13, 0x8f, 0x91, 0xf7, 0x5d, 0x1e, 0x04, 0xfa,
+ 0x78, 0xf7, 0xdc, 0x2e, 0xe4, 0xd8, 0x34, 0xe1, 0xae, 0xb8, 0xc5, 0xb5,
+ 0xee, 0x7d, 0xc1, 0x73, 0x02, 0x73, 0x63, 0x8f, 0x7e, 0xb8, 0x9d, 0xb1,
+ 0x76, 0xdc, 0x17, 0x41, 0x37, 0xe1, 0x9c, 0x10, 0x59, 0x84, 0xff, 0x5a,
+ 0x45, 0x5f, 0x14, 0x82, 0x9d, 0xd1, 0xe6, 0x8b, 0x8a, 0x55, 0xad, 0xdf,
+ 0xa3, 0xa7, 0x87, 0x19, 0x51, 0x37, 0xba, 0x47, 0xbc, 0x21, 0x25, 0x32,
+ 0xba, 0x24, 0xf3, 0x92, 0x5e, 0x1b, 0x91, 0x89, 0x4c, 0x6c, 0x6a, 0x2e,
+ 0x37, 0x21, 0xee, 0x87, 0x01, 0x8b, 0x39, 0xb8, 0x39, 0xe6, 0x86, 0xf4,
+ 0x7f, 0x49, 0x14, 0x10, 0x45, 0x71, 0xc6, 0xf3, 0xca, 0x53, 0x7e, 0xec,
+ 0x82, 0xb4, 0xee, 0x25, 0x1d, 0x83, 0x2c, 0x23, 0x8f, 0x0c, 0x97, 0xd8,
+ 0xc2, 0xd7, 0x8f, 0xcc, 0x06, 0x61, 0xf7, 0xe7, 0x6d, 0x9e, 0xbc, 0x7b,
+ 0x99, 0x65, 0xef, 0x9b, 0x96, 0xee, 0x84, 0xc9, 0x10, 0xa0, 0x77, 0x4d,
+ 0xae, 0x57, 0x79, 0xa3, 0xdb, 0xa1, 0xd8, 0x17, 0xeb, 0xd1, 0xdd, 0x27,
+ 0x00, 0x4a, 0xa0, 0xaa, 0x83, 0x76, 0x26, 0xeb, 0x6b, 0xac, 0x37, 0xdf,
+ 0x4b, 0x0f, 0x30, 0xf7, 0x45, 0x7e, 0xeb, 0xd7, 0xb6, 0x17, 0xdf, 0x43,
+ 0x1f, 0x99, 0x2d, 0x26, 0x1c, 0x41, 0xc6, 0x92, 0x3a, 0x22, 0x45, 0x3e,
+ 0x18, 0x13, 0xd7, 0xa6, 0xe2, 0x7a, 0x9d, 0x1c, 0xa6, 0xd5, 0x1e, 0x62,
+ 0x3f, 0xeb, 0x7d, 0xff, 0xfb, 0xcd, 0x0f, 0xbf, 0xed, 0x50, 0xb0, 0xdc,
+ 0x0b, 0xfe, 0x57, 0xb7, 0x24, 0x1e, 0x8e, 0x4c, 0xd1, 0xd1, 0x18, 0xe2,
+ 0x95, 0xe6, 0x3f, 0x77, 0x8d, 0xab, 0x67, 0xb2, 0x96, 0x0d, 0xcd, 0x6f,
+ 0x98, 0x51, 0x37, 0x72, 0xd6, 0xe8, 0x98, 0x50, 0x5a, 0x15, 0x7d, 0x19,
+ 0x3d, 0xe2, 0xd3, 0xce, 0x0e, 0x81, 0x6a, 0xbc, 0x07, 0x40, 0xfe, 0xe7,
+ 0xb7, 0xd6, 0xda, 0xd6, 0x5b, 0x6a, 0x9e, 0x7f, 0xc5, 0x44, 0x2e, 0x73,
+ 0x2d, 0x92, 0x26, 0x9d, 0xfa, 0xbe, 0xe1, 0x01, 0x36, 0x9e, 0x59, 0xf3,
+ 0xb5, 0xbf, 0x01, 0x5f, 0xdc, 0x8c, 0x5b, 0xbd, 0x27, 0x17, 0x3d, 0xf4,
+ 0x72, 0xfd, 0x92, 0x79, 0x1a, 0x98, 0x54, 0x5c, 0x85, 0x0c, 0xa7, 0xc3,
+ 0x2f, 0x06, 0x89, 0xb9, 0x62, 0x59, 0x5d, 0x47, 0x72, 0x93, 0x20, 0x84,
+ 0xa6, 0x87, 0xaa, 0xf1, 0xa6, 0xf2, 0x42, 0xf2, 0x28, 0x78, 0x20, 0xe6,
+ 0x98, 0x4b, 0x87, 0xd3, 0x78, 0x4b, 0xba, 0x05, 0x4f, 0xba, 0x1b, 0x12,
+ 0x75, 0x5f, 0xc6, 0x5d, 0x68, 0x13, 0xee, 0xa3, 0xa5, 0x8b, 0x47, 0x1f,
+ 0x92, 0x61, 0x12, 0x6d, 0xfd, 0x8f, 0xe7, 0x1f, 0xbc, 0x77, 0x38, 0x11,
+ 0x59, 0xfd, 0xee, 0x2a, 0x50, 0x4e, 0xe0, 0x61, 0x59, 0xee, 0x89, 0xc3,
+ 0x7c, 0x68, 0x24, 0x65, 0xd4, 0x2b, 0xa4, 0x15, 0x6b, 0x84, 0x49, 0xec,
+ 0xef, 0xad, 0xb7, 0xab, 0x59, 0xad, 0xcf, 0xff, 0xc8, 0x03, 0xf9, 0x38,
+ 0xfd, 0x33, 0xe7, 0x59, 0x16, 0x41, 0x67, 0x3c, 0x86, 0x67, 0x05, 0x3c,
+ 0xd5, 0x93, 0x3e, 0x76, 0xa0, 0x4e, 0xda, 0xc9, 0xdb, 0x3f, 0x3a, 0x25,
+ 0x4a, 0x2b, 0x70, 0x34, 0xef, 0xb8, 0x5a, 0xb9, 0xef, 0x3f, 0xdc, 0x4b,
+ 0x81, 0x42, 0xa4, 0x14, 0xe0, 0x44, 0x94, 0x28, 0x74, 0x3b, 0x06, 0x50,
+ 0xc5, 0x89, 0x1e, 0xf8, 0x30, 0x94, 0x3f, 0x7f, 0x00, 0x64, 0xd1, 0x4c,
+ 0x71, 0x3b, 0xef, 0x96, 0x08, 0x92, 0x49, 0xff, 0xa8, 0x7c, 0x32, 0x19,
+ 0x29, 0x30, 0x37, 0x9c, 0x7b, 0xd6, 0x5b, 0x0c, 0xe1, 0xc2, 0x91, 0x3e,
+ 0x8e, 0x8a, 0xd5, 0xee, 0x3b, 0x50, 0x9a, 0x4f, 0x1e, 0x5f, 0xf3, 0x8c,
+ 0x3d, 0x8e, 0x92, 0x55, 0x4e, 0x28, 0xf5, 0xd8, 0x20, 0xe0, 0x2f, 0x8c,
+ 0x50, 0x1e, 0x5b, 0x4f, 0xad, 0x74, 0xb7, 0x1c, 0x15, 0x0f, 0xb6, 0xf6,
+ 0x80, 0x00, 0xe1, 0xab, 0x12, 0xbf, 0x07, 0x1f, 0xa2, 0x6a, 0x27, 0x1d,
+ 0xc0, 0xae, 0xcf, 0x51, 0x78, 0xe1, 0xdb, 0xc4, 0xc8, 0xe5, 0x5c, 0x95,
+ 0xb8, 0x20, 0x20, 0x6f, 0x0f, 0xee, 0xab, 0xe6, 0x20, 0xdb, 0x7e, 0x5c,
+ 0x11, 0xa7, 0x84, 0xb1, 0xf6, 0xd6, 0xb0, 0x5f, 0x22, 0xe3, 0x2b, 0x4b,
+ 0x8c, 0x29, 0xc4, 0x11, 0xe3, 0x2e, 0x7b, 0x10, 0x6d, 0xfd, 0xb2, 0xc9,
+ 0xc3, 0xc8, 0x59, 0xa8, 0x9e, 0xe0, 0x1c, 0x11, 0xec, 0xe6, 0x3d, 0xd4,
+ 0x3e, 0xd9, 0xcf, 0x4d, 0x75, 0x5a, 0x43, 0x9b, 0xf2, 0x31, 0x5c, 0xe0,
+ 0xaf, 0x09, 0xcc, 0xf5, 0x08, 0x7f, 0x73, 0x39, 0xe7, 0x24, 0x2b, 0xdd,
+ 0x2d, 0xe5, 0x94, 0xbf, 0x90, 0x9b, 0x85, 0xaf, 0x5e, 0x7b, 0x63, 0xf3,
+ 0xe7, 0x39, 0x32, 0x17, 0x81, 0xe0, 0xf0, 0x79, 0x7c, 0x5b, 0x19, 0xff,
+ 0xda, 0x31, 0xbd, 0xf6, 0xa1, 0x47, 0xbc, 0x99, 0xe7, 0x4b, 0xcd, 0x4f,
+ 0x56, 0xbc, 0xd2, 0xb8, 0xed, 0xc2, 0x8b, 0x1e, 0xf2, 0xd9, 0x71, 0x8e,
+ 0x19, 0x16, 0x49, 0xaa, 0x1e, 0xa4, 0x10, 0x9c, 0xf9, 0xcb, 0xc9, 0x94,
+ 0x5e, 0xfa, 0x4a, 0xbc, 0x90, 0xa4, 0xcc, 0xeb, 0xc4, 0x1f, 0x69, 0x8b,
+ 0xf0, 0x97, 0x25, 0x77, 0xd5, 0x1f, 0xa5, 0x8a, 0xd7, 0xa6, 0xb0, 0x78,
+ 0x79, 0xfa, 0xf6, 0x5d, 0x5e, 0xc5, 0xae, 0x2d, 0xb1, 0xe1, 0x68, 0x56,
+ 0x40, 0xb5, 0x00, 0x62, 0x8a, 0x17, 0x77, 0x34, 0xc0, 0xd0, 0x7c, 0xe0,
+ 0xac, 0x4b, 0xa8, 0x45, 0x6b, 0x68, 0xb9, 0x32, 0x70, 0xa9, 0xa2, 0x12,
+ 0x2e, 0x25, 0xff, 0x88, 0xc6, 0xa5, 0x15, 0xb7, 0x16, 0xac, 0x2c, 0x2e,
+ 0x44, 0xe0, 0x0a, 0x73, 0xf1, 0xc9, 0x7e, 0x52, 0x09, 0x6a, 0x19, 0x4f,
+ 0x07, 0x97, 0x5e, 0xdd, 0x33, 0xe6, 0x70, 0xea, 0xcf, 0xc7, 0x37, 0x7f,
+ 0xaa, 0x70, 0x9f, 0xcd, 0x0c, 0x86, 0x0c, 0xba, 0x40, 0x7a, 0x65, 0xd2,
+ 0xef, 0xc2, 0xaf, 0xde, 0x68, 0x4c, 0x9c, 0x2d, 0x98, 0x9b, 0x6c, 0xe9,
+ 0xe8, 0x26, 0xc9, 0x27, 0xf4, 0x6e, 0x56, 0x43, 0x2d, 0x7c, 0x98, 0xef,
+ 0x89, 0x89, 0x23, 0xd3, 0xf3, 0x6e, 0x8b, 0x41, 0xc3, 0x87, 0xd0, 0x87,
+ 0x01, 0xca, 0xf8, 0x44, 0x88, 0x14, 0x55, 0x71, 0xd0, 0x58, 0x7d, 0x36,
+ 0x51, 0x1c, 0x8d, 0x77, 0xa0, 0x1a, 0x72, 0x4a, 0x2b, 0xfc, 0x50, 0x2f,
+ 0x17, 0x56, 0x7c, 0xd6, 0xdc, 0xe9, 0x87, 0x12, 0xde, 0x24, 0x36, 0x6e,
+ 0x03, 0x68, 0xfd, 0x10, 0x60, 0x33, 0x7b, 0x41, 0x63, 0x5f, 0x66, 0x3f,
+ 0x25, 0x68, 0x77, 0x29, 0xcb, 0x3c, 0x61, 0x1d, 0xfa, 0x70, 0x40, 0x67,
+ 0x70, 0x8c, 0xab, 0xfc, 0xa7, 0xc3, 0xd3, 0xf3, 0xd7, 0x62, 0xa2, 0xf0,
+ 0x10, 0xe9, 0xb4, 0xe8, 0xb7, 0xb6, 0xff, 0xe3, 0xa9, 0x1e, 0x5a, 0x15,
+ 0x64, 0xa4, 0x50, 0x9b, 0x40, 0x5f, 0x57, 0x9c, 0xe4, 0xf8, 0x8d, 0x7b,
+ 0xa6, 0xba, 0xa3, 0x9a, 0x06, 0xd5, 0x10, 0xb4, 0x53, 0xdd, 0x2a, 0xda,
+ 0xe3, 0xcd, 0xf6, 0x39, 0xde, 0x26, 0x11, 0xfe, 0x98, 0x21, 0xc7, 0x41,
+ 0x80, 0xbd, 0xa6, 0x1b, 0xc9, 0x63, 0x6f, 0x81, 0x8a, 0x03, 0x2e, 0x1b,
+ 0xd8, 0xe6, 0x3c, 0xa0, 0xe3, 0xd0, 0xff, 0x65, 0x92, 0x3a, 0x63, 0xf8,
+ 0x9f, 0x73, 0xae, 0x6e, 0x9d, 0x9a, 0x61, 0xbd, 0x6d, 0x09, 0x21, 0x8a,
+ 0xb3, 0x24, 0x55, 0x58, 0x09, 0x25, 0xc0, 0x73, 0x03, 0x9e, 0x48, 0x62,
+ 0x65, 0x66, 0xea, 0x89, 0xf1, 0xd3, 0x8b, 0x56, 0x3d, 0xba, 0x39, 0xfa,
+ 0xb5, 0xa2, 0xcb, 0xf9, 0xc8, 0xc5, 0x52, 0xce, 0x7e, 0x0d, 0x6c, 0x38,
+ 0x4d, 0x5a, 0x6a, 0x54, 0xfb, 0xf4, 0xed, 0xb2, 0xfc, 0x17, 0xa8, 0x76,
+ 0xfa, 0x50, 0xa9, 0x44, 0x3a, 0xcc, 0x91, 0x8f, 0xae, 0x5a, 0x2c, 0x1c,
+ 0x1e, 0x5e, 0x83, 0xd4, 0x2e, 0x4f, 0xf3, 0xc0, 0x44, 0x65, 0xcb, 0x0c,
+ 0x3d, 0x48, 0x79, 0x80, 0xe1, 0x93, 0xd4, 0xb9, 0x8f, 0xd7, 0x58, 0x39,
+ 0x6b, 0x62, 0xb9, 0xa6, 0x65, 0x31, 0xe0, 0x57, 0x78, 0xa3, 0xae, 0x1d,
+ 0x96, 0x4f, 0xaa, 0x4d, 0x8b, 0x0f, 0x4f, 0x80, 0xe2, 0x51, 0x42, 0x37,
+ 0x5b, 0x76, 0xec, 0xca, 0x97, 0xfb, 0xbc, 0x44, 0x1f, 0x12, 0x94, 0x44,
+ 0x60, 0x83, 0xe2, 0x2c, 0xe2, 0xab, 0xad, 0x0b, 0x51, 0x42, 0x98, 0x49,
+ 0x8c, 0x6c, 0x63, 0x33, 0x3e, 0x34, 0xc5, 0xd3, 0x75, 0xa7, 0x12, 0x36,
+ 0x97, 0xf4, 0x0d, 0xac, 0xf4, 0x74, 0xfe, 0xfa, 0x24, 0xf4, 0x4c, 0x2b,
+ 0xac, 0x0a, 0x55, 0x4f, 0x6f, 0xa5, 0x58, 0xca, 0x4c, 0x31, 0x4e, 0x0c,
+ 0x0d, 0x7a, 0xde, 0xd0, 0x09, 0xdb, 0xce, 0xe6, 0x3e, 0xa9, 0xb6, 0xd8,
+ 0x07, 0x26, 0x3d, 0x1e, 0x22, 0xd2, 0xcf, 0xe4, 0xcf, 0xa2, 0xb0, 0x51,
+ 0x01, 0xe7, 0xda, 0xa7, 0x6b, 0x97, 0x2e, 0x93, 0x22, 0xda, 0x86, 0xdf,
+ 0x4e, 0xaa, 0x7b, 0x1c, 0x79, 0xd1, 0x20, 0x3c, 0x46, 0x8b, 0x36, 0x34,
+ 0x73, 0x44, 0xda, 0x6f, 0x2d, 0xcc, 0x7b, 0xae, 0x3e, 0xa1, 0x68, 0xb2,
+ 0xf7, 0x12, 0x6c, 0x23, 0xf5, 0xc7, 0xb9, 0x38, 0x5d, 0xbb, 0x48, 0x4d,
+ 0xab, 0x10, 0x40, 0x2f, 0xe4, 0x71, 0x16, 0x19, 0xf9, 0xdc, 0x9d, 0x32,
+ 0x5b, 0xe3, 0xc4, 0x50, 0xb6, 0x27, 0x9b, 0x61, 0xc6, 0xda, 0x0a, 0x11,
+ 0xfd, 0x54, 0x00, 0xa7, 0x74, 0x18, 0x51, 0x3a, 0x62, 0x83, 0x7f, 0x0d,
+ 0xaf, 0x31, 0xad, 0xa9, 0x9d, 0xc9, 0x5c, 0x74, 0x82, 0x86, 0xdc, 0x56,
+ 0xbd, 0x6e, 0x72, 0x47, 0x11, 0x45, 0x41, 0x64, 0xb2, 0x05, 0x9b, 0xc8,
+ 0x20, 0x90, 0x05, 0x79, 0x52, 0x7d, 0xea, 0x12, 0xcb, 0x5f, 0xce, 0xd9,
+ 0x98, 0x23, 0x8f, 0x66, 0x37, 0x0c, 0x4d, 0x14, 0x9f, 0x85, 0xeb, 0x8a,
+ 0xa6, 0x14, 0x38, 0x6a, 0x68, 0x34, 0xcf, 0x7c, 0x18, 0x6b, 0x3f, 0x5d,
+ 0xf2, 0x2b, 0xe0, 0x41, 0xb6, 0x24, 0x75, 0x05, 0x14, 0x9e, 0xf2, 0x8a,
+ 0xdb, 0x67, 0xd1, 0x05, 0xc2, 0x05, 0x17, 0xac, 0x4d, 0xb6, 0x8d, 0x54,
+ 0xe3, 0x38, 0x74, 0x31, 0x83, 0x7f, 0x61, 0x61, 0xda, 0xc9, 0xf8, 0x27,
+ 0x40, 0xe5, 0xc2, 0xb5, 0xbc, 0x5a, 0x77, 0xcc, 0x9c, 0xfb, 0x6f, 0xbe,
+ 0x82, 0x7e, 0xad, 0x94, 0x59, 0xb0, 0x77, 0x97, 0x93, 0x71, 0xaf, 0xa3,
+ 0x8a, 0x36, 0x52, 0x2f, 0x73, 0x55, 0xfd, 0x40, 0xbb, 0xb8, 0x59, 0x09,
+ 0x13, 0xe8, 0xfd, 0x69, 0x71, 0x2a, 0xec, 0x62, 0x98, 0x37, 0x44, 0x3d,
+ 0x6f, 0x6c, 0x50, 0xb9, 0x43, 0xfb, 0xf6, 0xb7, 0x89, 0xea, 0x8f, 0x2f,
+ 0xce, 0xdc, 0xf4, 0x8b, 0x8e, 0xa9, 0xd7, 0xe9, 0x6f, 0xa5, 0x08, 0x6e,
+ 0x11, 0x02, 0x08, 0x35, 0x52, 0x38, 0x6f, 0x5d, 0xff, 0x4c, 0xf7, 0xdd,
+ 0x86, 0xf0, 0xdc, 0x91, 0x2a, 0x65, 0x3a, 0x98, 0x14, 0x64, 0x8b, 0x41,
+ 0xee, 0xb0, 0xd6, 0xd9, 0x70, 0xde, 0xa0, 0xd5, 0x0c, 0x56, 0x6c, 0x07,
+ 0x41, 0x10, 0x18, 0xb2, 0xac, 0x81, 0xbc, 0xd6, 0xe6, 0x82, 0x1c, 0xaa,
+ 0xc1, 0xde, 0xc9, 0xc5, 0xe2, 0x2d, 0x94, 0x5b, 0xdc, 0xb5, 0xcd, 0x0d,
+ 0x86, 0x29, 0x1d, 0xb6, 0x17, 0x1d, 0xfd, 0x10, 0x76, 0x0b, 0xa9, 0xef,
+ 0x64, 0x72, 0xc3, 0x97, 0x4f, 0x2e, 0xae, 0x05, 0xd2, 0x2f, 0x1b, 0x43,
+ 0x33, 0xa9, 0x56, 0x2e, 0xa6, 0xe5, 0xe9, 0x9c, 0x76, 0x06, 0xf5, 0x9a,
+ 0x9e, 0x84, 0x0c, 0x73, 0x64, 0x60, 0x47, 0x51, 0xb5, 0x47, 0xbb, 0xe0,
+ 0xbb, 0xab, 0x32, 0xfc, 0x0f, 0xff, 0xcc, 0xd6, 0xef, 0x3e, 0x1a, 0xa6,
+ 0xd9, 0x41, 0xe6, 0xf0, 0x49, 0x26, 0x73, 0xec, 0xed, 0x7c, 0x49, 0x5d,
+ 0x62, 0xd5, 0xbc, 0x7c, 0x66, 0x87, 0x45, 0x8c, 0xeb, 0xb2, 0x7f, 0x37,
+ 0x8d, 0xb5, 0xc4, 0xd2, 0xa7, 0x1f, 0xe4, 0xfa, 0xfe, 0xbb, 0xa7, 0x50,
+ 0xe8, 0x59, 0x4c, 0xaa, 0x40, 0xca, 0x6d, 0x8f, 0x28, 0x79, 0x1a, 0x2d,
+ 0xcd, 0xb0, 0xc5, 0x1d, 0x66, 0xe3, 0x9a, 0xa6, 0x4b, 0x59, 0x72, 0x97,
+ 0x68, 0x62, 0x0c, 0xb3, 0xc6, 0x6e, 0xf4, 0x71, 0xfc, 0x38, 0x59, 0xf7,
+ 0x86, 0x0c, 0xd7, 0x46, 0x6d, 0x1e, 0x25, 0xee, 0xcb, 0x62, 0x65, 0xd5,
+ 0x9a, 0x3b, 0xc7, 0xf4, 0xb8, 0xe2, 0x22, 0x10, 0xa5, 0x4a, 0x84, 0x66,
+ 0x1d, 0x08, 0x30, 0xf3, 0x33, 0x4d, 0xac, 0xa3, 0x1d, 0xff, 0x99, 0xe7,
+ 0x04, 0xa1, 0xe8, 0x35, 0x47, 0x24, 0x16, 0xef, 0x85, 0xf9, 0xbe, 0x9e,
+ 0x13, 0x00, 0x3b, 0xba, 0xd1, 0xf9, 0xff, 0x80, 0xd8, 0xa3, 0xba, 0xf7,
+ 0x48, 0xaa, 0x25, 0x19, 0xaa, 0xf8, 0x56, 0xfa, 0x6c, 0x07, 0xed, 0xf0,
+ 0xa7, 0x91, 0x2f, 0x69, 0x9f, 0xd7, 0x8d, 0x90, 0x51, 0x78, 0xdd, 0xcf,
+ 0x7e, 0x47, 0x21, 0x07, 0x4b, 0xf3, 0x5a, 0x21, 0x1a, 0x99, 0x4c, 0x1f,
+ 0xcb, 0x07, 0x4b, 0x2e, 0x92, 0x20, 0xc3, 0x2f, 0xe4, 0x40, 0xe6, 0xe4,
+ 0xf3, 0xed, 0x06, 0xcd, 0x70, 0x61, 0xb1, 0x0f, 0x5b, 0x6f, 0x49, 0xf4,
+ 0x13, 0x77, 0x41, 0x72, 0x96, 0xd0, 0x60, 0x83, 0x23, 0x62, 0xb9, 0xc8,
+ 0x2b, 0x68, 0xf4, 0xc5, 0x53, 0xfe, 0x12, 0xaf, 0x43, 0xb5, 0x9a, 0x17,
+ 0x53, 0xf6, 0x92, 0x66, 0x27, 0xca, 0xdb, 0xd6, 0xbb, 0x66, 0xcc, 0xd6,
+ 0xe7, 0x66, 0x37, 0x95, 0x5f, 0x34, 0x32, 0xde, 0x45, 0xf6, 0x79, 0x2a,
+ 0x8c, 0x2c, 0xa9, 0xfa, 0xfc, 0xac, 0x05, 0xde, 0x0e, 0xd2, 0xb9, 0x7c,
+ 0x13, 0x77, 0x5e, 0xad, 0x80, 0x99, 0xc7, 0x23, 0x8a, 0x89, 0x79, 0xdc,
+ 0x9c, 0xce, 0xd7, 0x43, 0x5a, 0xd9, 0x9d, 0xe5, 0x77, 0xf7, 0x00, 0xb9,
+ 0x74, 0x36, 0x2f, 0xcb, 0xdc, 0x78, 0xcd, 0x52, 0x7c, 0xca, 0xb8, 0x3a,
+ 0xa0, 0xfd, 0xd8, 0x93, 0x19, 0x4d, 0xa8, 0x2d, 0x8c, 0x28, 0xf1, 0xdc,
+ 0xe8, 0xaf, 0x1b, 0xee, 0x1d, 0x2b, 0x79, 0xfc, 0x65, 0xe1, 0x2f, 0x70,
+ 0x0f, 0x4d, 0x43, 0x78, 0xa8, 0x55, 0x14, 0x54, 0x07, 0x78, 0xe6, 0x55,
+ 0xf0, 0x9f, 0xc0, 0x6b, 0x92, 0xe1, 0x8b, 0xb0, 0xae, 0x03, 0xb6, 0xe5,
+ 0x56, 0xd6, 0x1e, 0xd4, 0xbe, 0x01, 0x2f, 0x96, 0xe6, 0x29, 0xd6, 0x5a,
+ 0x01, 0x3a, 0xa7, 0xe1, 0x91, 0xbf, 0xb9, 0x05, 0xae, 0x40, 0xd6, 0x83,
+ 0x82, 0xb9, 0x83, 0x77, 0x53, 0xff, 0xa7, 0x69, 0x03, 0xb1, 0x55, 0x41,
+ 0x93, 0xc6, 0x94, 0xb6, 0x14, 0xc5, 0x9e, 0x51, 0x8f, 0xaa, 0xa8, 0xad,
+ 0x83, 0x34, 0xaf, 0x15, 0xde, 0xfb, 0x53, 0x73, 0xf9, 0x79, 0xb0, 0xed,
+ 0x8e, 0x32, 0x10, 0xe5, 0x2f, 0x36, 0xcb, 0x2d, 0x4e, 0xc1, 0x4c, 0x65,
+ 0xc5, 0x32, 0x7b, 0x50, 0xe8, 0x76, 0xc4, 0xa3, 0x72, 0x2f, 0xbc, 0x6c,
+ 0x4a, 0xa1, 0x1b, 0x2b, 0x84, 0x1f, 0x21, 0x02, 0xff, 0xb8, 0xeb, 0x0d,
+ 0xb8, 0x63, 0x9c, 0xdb, 0x87, 0x91, 0x95, 0x11, 0xaf, 0x53, 0x11, 0xa6,
+ 0xde, 0xf5, 0x9b, 0xc0, 0x4a, 0x37, 0x0a, 0xca, 0xe0, 0x96, 0xa1, 0x73,
+ 0xa3, 0x31, 0x41, 0xd5, 0xe7, 0xef, 0x94, 0xb3, 0x73, 0xc8, 0xe8, 0x5a,
+ 0x61, 0xc9, 0x98, 0xe7, 0xe8, 0xdd, 0xc6, 0x62, 0x3f, 0xfe, 0xda, 0x0e,
+ 0x8e, 0x01, 0x11, 0x4f, 0xeb, 0x0e, 0xdb, 0xb0, 0xe7, 0x11, 0xe2, 0xff,
+ 0xf1, 0x3d, 0x77, 0x4d, 0x85, 0x82, 0x4a, 0xb4, 0x63, 0x0b, 0x6d, 0xc1,
+ 0xf2, 0xcc, 0x73, 0x8e, 0xb1, 0x96, 0x09, 0x2d, 0x3b, 0x0a, 0x05, 0x7e,
+ 0x93, 0x64, 0x6e, 0xd1, 0x51, 0x5f, 0x0b, 0xdd, 0xbf, 0x00, 0x76, 0xaf,
+ 0x4b, 0xe9, 0x18, 0x14, 0x2f, 0xe0, 0x33, 0x79, 0xb2, 0x1f, 0xdf, 0x3f,
+ 0xec, 0xd5, 0xf5, 0xcf, 0x8a, 0x5c, 0xe1, 0xae, 0x75, 0xf6, 0x16, 0x14,
+ 0x5f, 0xab, 0x07, 0xdb, 0x71, 0x1b, 0x3c, 0xb8, 0xd7, 0x02, 0xf9, 0xb0,
+ 0x7f, 0xb0, 0x74, 0x4a, 0x13, 0x91, 0x19, 0x6d, 0x8e, 0x3e, 0x0b, 0x42,
+ 0x1a, 0x03, 0x1d, 0xcc, 0x23, 0x34, 0xf9, 0x60, 0x0c, 0x19, 0x60, 0x23,
+ 0x5a, 0xf5, 0xc0, 0x14, 0x20, 0xae, 0x56, 0xa3, 0x78, 0x19, 0x76, 0x55,
+ 0xdf, 0x33, 0xe3, 0x47, 0xb2, 0x56, 0xa8, 0xff, 0xe6, 0x70, 0xef, 0x00,
+ 0x7c, 0x38, 0x4f, 0xf4, 0x67, 0x41, 0x97, 0x42, 0x45, 0xe3, 0xdb, 0xbe,
+ 0xcd, 0x21, 0x04, 0x3b, 0xb3, 0x65, 0x73, 0x3f, 0x81, 0x6c, 0x2f, 0x91,
+ 0x39, 0x14, 0x0a, 0x76, 0x41, 0xf9, 0x96, 0xa7, 0xa1, 0x5d, 0x75, 0xf7,
+ 0xb4, 0x6e, 0x27, 0x81, 0x1b, 0xe2, 0xdf, 0xba, 0x23, 0xd9, 0x02, 0x1e,
+ 0xe2, 0x8f, 0x03, 0x10, 0x32, 0x97, 0x32, 0x9f, 0xbd, 0xce, 0xfc, 0xc4,
+ 0xa7, 0x01, 0x31, 0x2d, 0x8b, 0x4f, 0xd5, 0x5f, 0xe5, 0x87, 0xfb, 0xd4,
+ 0x25, 0x7b, 0x6f, 0xe4, 0x79, 0xdd, 0x92, 0xf0, 0x4c, 0x6c, 0x36, 0xaa,
+ 0xf6, 0xf9, 0xb2, 0x28, 0x4d, 0xa7, 0x9c, 0x44, 0xe9, 0x71, 0x45, 0x18,
+ 0x3a, 0x6a, 0xd7, 0x17, 0x54, 0x3e, 0xd9, 0x03, 0x7b, 0x64, 0x4a, 0xf7,
+ 0x33, 0xc0, 0xfb, 0xda, 0xaf, 0xf2, 0xf7, 0x57, 0xa7, 0xf1, 0xaa, 0xdb,
+ 0x26, 0x5e, 0x08, 0xe2, 0x4f, 0xd5, 0x0f, 0xea, 0x2b, 0x96, 0x36, 0x64,
+ 0x14, 0x97, 0x0c, 0x13, 0x45, 0xf1, 0x4e, 0x8d, 0x31, 0x3a, 0x99, 0x78,
+ 0xd8, 0x48, 0x86, 0x5d, 0x02, 0x47, 0xa7, 0xaf, 0xa3, 0x4f, 0x16, 0x9a,
+ 0xeb, 0x65, 0x17, 0xf8, 0x3b, 0x05, 0x49, 0xc7, 0xf0, 0x3f, 0xce, 0x46,
+ 0xc0, 0x42, 0x25, 0xab, 0x5d, 0xa0, 0x8f, 0x54, 0xd4, 0xb4, 0x89, 0x8b,
+ 0xf0, 0x5b, 0x39, 0x6e, 0xb5, 0xd6, 0xc3, 0x6c, 0x89, 0x39, 0x14, 0x2e,
+ 0x40, 0x8d, 0xac, 0x14, 0x35, 0x25, 0xb1, 0xa0, 0x05, 0xb2, 0x20, 0x18,
+ 0xcd, 0x2a, 0x8b, 0x9a, 0xe4, 0x31, 0x8c, 0x64, 0x63, 0x75, 0xa9, 0x73,
+ 0x89, 0x1d, 0x8f, 0xe7, 0x16, 0x60, 0x20, 0x9f, 0x36, 0xa6, 0x36, 0x1b,
+ 0x94, 0x89, 0x98, 0xc4, 0x54, 0x19, 0x1f, 0x9d, 0x6b, 0x19, 0x5f, 0xe5,
+ 0x74, 0x4a, 0x61, 0x7a, 0xdc, 0xe9, 0x7c, 0xfb, 0xa8, 0x05, 0x8f, 0x7a,
+ 0xfb, 0x09, 0xbc, 0x57, 0x57, 0x4d, 0x59, 0xaf, 0xdb, 0x2e, 0xbf, 0xf0,
+ 0x0e, 0xf3, 0xb0, 0x6b, 0x30, 0x9a, 0x81, 0x03, 0x9d, 0xcb, 0xa8, 0x5a,
+ 0xc9, 0xe3, 0x43, 0x09, 0xe9, 0xa5, 0x7a, 0xa5, 0x7c, 0xb7, 0x46, 0xd0,
+ 0xf7, 0x87, 0xf7, 0xf2, 0xfe, 0x9d, 0x68, 0x7c, 0xde, 0xa4, 0xbc, 0xd0,
+ 0x8d, 0x49, 0x86, 0xb8, 0x89, 0xd9, 0xae, 0x27, 0x46, 0x6c, 0xfd, 0x74,
+ 0xab, 0xfd, 0xb8, 0x86, 0x39, 0x98, 0xa8, 0x16, 0x9f, 0xa6, 0x89, 0x34,
+ 0x93, 0xd6, 0x5a, 0xb1, 0x17, 0xa1, 0x54, 0x4d, 0x3a, 0xa7, 0x48, 0x71,
+ 0x58, 0xe9, 0x3a, 0x61, 0xa1, 0xf4, 0x6f, 0x34, 0xaf, 0x9a, 0xbb, 0x73,
+ 0x49, 0x53, 0x3e, 0x1e, 0x11, 0x00, 0xb6, 0x1a, 0xa8, 0x9f, 0x11, 0x1f,
+ 0x47, 0x5f, 0xab, 0xe0, 0x53, 0xf8, 0x33, 0xfa, 0x69, 0x33, 0x3c, 0x90,
+ 0x3d, 0x30, 0x0d, 0x4d, 0xd9, 0xf3, 0x4e, 0x02, 0xf9, 0x8f, 0x92, 0x36,
+ 0xca, 0x7e, 0x5d, 0x8f, 0x37, 0x54, 0x88, 0x46, 0xa2, 0xa0, 0x38, 0xdb,
+ 0xed, 0xd7, 0xc7, 0x93, 0x7b, 0xcb, 0x9a, 0x8a, 0xd7, 0xa2, 0x8a, 0xf1,
+ 0x2c, 0x93, 0x8e, 0xb1, 0x5a, 0x4a, 0xf3, 0xad, 0x70, 0x7e, 0x25, 0xca,
+ 0x31, 0x85, 0x80, 0xdd, 0x47, 0xa7, 0x8f, 0x70, 0x9f, 0x4f, 0xe2, 0xf0,
+ 0xd7, 0xdb, 0xf0, 0xc1, 0xf7, 0x6a, 0x4f, 0xe1, 0x63, 0xe7, 0x86, 0xec,
+ 0x8d, 0xe7, 0xf9, 0x28, 0x7e, 0xcf, 0x42, 0x37, 0x24, 0x23, 0xb0, 0x15,
+ 0x49, 0x77, 0xb0, 0xee, 0x94, 0x10, 0xca, 0x72, 0x95, 0x5f, 0x9c, 0x84,
+ 0xd3, 0x56, 0x2d, 0x65, 0x92, 0xa9, 0x6d, 0xe4, 0x82, 0xa3, 0x82, 0x94,
+ 0x4e, 0x02, 0x42, 0xbe, 0x44, 0x07, 0x63, 0xec, 0x10, 0x93, 0x38, 0x35,
+ 0xc7, 0x7e, 0xfb, 0xba, 0x0c, 0x9d, 0x5c, 0x22, 0xc9, 0xfc, 0x93, 0xcb,
+ 0x78, 0x92, 0x9c, 0x23, 0x9d, 0xd8, 0x7d, 0xbf, 0x6b, 0xfe, 0x1d, 0xec,
+ 0x78, 0xc7, 0x2b, 0xe1, 0x75, 0xd6, 0x10, 0x81, 0x69, 0xfa, 0x47, 0xf1,
+ 0x0b, 0x41, 0x85, 0x8f, 0x68, 0x1e, 0xb2, 0x99, 0xb8, 0x7c, 0x21, 0x95,
+ 0x16, 0x92, 0xce, 0x41, 0xb5, 0x85, 0x8c, 0xdf, 0x62, 0xe8, 0xd6, 0x6f,
+ 0x0d, 0x4e, 0xde, 0x73, 0x6e, 0x85, 0x5d, 0xa0, 0x9b, 0x4d, 0xa7, 0x11,
+ 0x69, 0x9e, 0xf0, 0x69, 0x6e, 0x5c, 0x7c, 0x3b, 0x62, 0x01, 0xfa, 0x34,
+ 0x5b, 0xda, 0xb0, 0x7b, 0xfc, 0x7f, 0x98, 0x02, 0x8a, 0x5f, 0xb7, 0xf4,
+ 0x50, 0xee, 0x6d, 0xbf, 0x02, 0x01, 0x4e, 0x75, 0x1a, 0xdd, 0x55, 0xe9,
+ 0x15, 0xfa, 0x4c, 0x2b, 0xd4, 0x22, 0xad, 0x3e, 0xd5, 0xda, 0x52, 0x40,
+ 0x27, 0x06, 0x39, 0x03, 0x49, 0xf1, 0x73, 0x3c, 0x6e, 0xd0, 0x42, 0xb5,
+ 0xc4, 0x3d, 0x13, 0x6c, 0x38, 0x26, 0xad, 0xe0, 0xcd, 0x85, 0x28, 0xd8,
+ 0xde, 0xc2, 0x89, 0x64, 0xa2, 0xd1, 0xdd, 0x20, 0xb8, 0xa7, 0xf2, 0xdb,
+ 0x9c, 0xf8, 0x2d, 0x6a, 0x85, 0x2c, 0x28, 0x6b, 0xc9, 0xb0, 0xc6, 0xd8,
+ 0xe5, 0x75, 0xf1, 0x63, 0x8c, 0xf3, 0xb9, 0xb8, 0x86, 0x8d, 0xc7, 0x3c,
+ 0xbc, 0xf8, 0x10, 0xc1, 0xc1, 0x4d, 0xbd, 0x3c, 0xf1, 0x2d, 0x67, 0x0b,
+ 0xbc, 0x58, 0x0d, 0x23, 0x42, 0x25, 0xc8, 0xbf, 0x48, 0xab, 0xbc, 0xff,
+ 0xcc, 0xae, 0xc1, 0x69, 0x37, 0xc6, 0xb5, 0x9e, 0x9e, 0x6b, 0x20, 0xbb,
+ 0x80, 0x7d, 0x5b, 0x93, 0x32, 0x9c, 0x9b, 0x38, 0x90, 0xa0, 0xf2, 0x33,
+ 0x0c, 0x7d, 0x06, 0x64, 0xeb, 0xdd, 0xf3, 0x7b, 0x4c, 0xd0, 0xc0, 0x67,
+ 0x7b, 0x33, 0x78, 0x79, 0x39, 0xeb, 0xd3, 0x6a, 0x10, 0xff, 0x2a, 0xa4,
+ 0x24, 0x6d, 0x4a, 0x1e, 0xd6, 0xad, 0x7f, 0xba, 0xbe, 0x5e, 0x5e, 0xc5,
+ 0x3d, 0xe3, 0x11, 0xcd, 0x12, 0x5a, 0x5f, 0x93, 0xd6, 0xd6, 0x67, 0x30,
+ 0xa7, 0x28, 0xc5, 0x7d, 0x8e, 0x19, 0xec, 0x20, 0x6c, 0x33, 0xfc, 0x11,
+ 0x0e, 0x2b, 0x07, 0x60, 0x06, 0x3c, 0x8b, 0x7d, 0x21, 0x8e, 0xce, 0x3d,
+ 0xf6, 0xcd, 0x98, 0x1c, 0x7f, 0xed, 0x46, 0xac, 0xc1, 0xbf, 0xd7, 0x96,
+ 0x70, 0x9f, 0x5c, 0x64, 0xb4, 0xc8, 0x07, 0xf3, 0xed, 0xcd, 0xb2, 0xa4,
+ 0x81, 0x9f, 0x65, 0xf6, 0x16, 0x27, 0x5e, 0xaf, 0xa6, 0x20, 0xbe, 0xf4,
+ 0x4d, 0xac, 0x5a, 0xe8, 0xe6, 0x4a, 0x28, 0x0f, 0xf1, 0x4b, 0xb6, 0x64,
+ 0x8c, 0x98, 0x33, 0x4f, 0xc1, 0xf1, 0x37, 0xb7, 0x9b, 0x35, 0x5f, 0x1f,
+ 0xd3, 0xf5, 0x5c, 0xf8, 0xda, 0xf6, 0xb9, 0x05, 0xd8, 0xd1, 0xc4, 0xef,
+ 0xb6, 0xc1, 0xdb, 0x86, 0xd5, 0xb1, 0x22, 0x96, 0xa2, 0x6e, 0x5e, 0x5f,
+ 0xaa, 0xcd, 0x5e, 0x9b, 0x8a, 0x7a, 0x31, 0x5c, 0x77, 0xb0, 0xb0, 0x34,
+ 0xed, 0xde, 0xde, 0x60, 0xdc, 0xb7, 0x2a, 0x09, 0x88, 0x9e, 0x3a, 0xaa,
+ 0x3e, 0xa5, 0x55, 0x3c, 0xd0, 0xa9, 0x34, 0xc1, 0x06, 0x3d, 0x9d, 0xc5,
+ 0x95, 0x48, 0x46, 0xd7, 0x41, 0x73, 0x5d, 0x95, 0x1d, 0x07, 0x7a, 0x0a,
+ 0xb2, 0x45, 0xf2, 0x88, 0x99, 0x1c, 0x70, 0x3a, 0xc4, 0xda, 0xb1, 0xba,
+ 0x7a, 0x38, 0x0a, 0x43, 0x3a, 0x41, 0xe1, 0x09, 0xc7, 0x24, 0x50, 0x45,
+ 0x70, 0xe5, 0xdb, 0xe8, 0xa2, 0x33, 0xc6, 0xc7, 0xf7, 0xd2, 0x23, 0x01,
+ 0xbf, 0xe1, 0xdb, 0xac, 0x3a, 0x87, 0xe1, 0x0e, 0xab, 0x3d, 0xe0, 0x1f,
+ 0x75, 0x52, 0x9d, 0xdd, 0x9a, 0xed, 0xb5, 0xbb, 0xa4, 0x4c, 0x56, 0x34,
+ 0x07, 0xf2, 0x41, 0x49, 0x72, 0x43, 0x57, 0x8c, 0xb8, 0xca, 0xa6, 0xda,
+ 0x6d, 0x93, 0x29, 0xc3, 0x1b, 0x2f, 0x22, 0x6a, 0xd2, 0x62, 0xa9, 0xcf,
+ 0x59, 0x97, 0x51, 0x10, 0x2f, 0x0e, 0x08, 0xc8, 0xe0, 0xab, 0x97, 0xa3,
+ 0x4b, 0x96, 0xd5, 0x00, 0xf0, 0xba, 0xca, 0x59, 0xcf, 0x8d, 0xa4, 0x13,
+ 0x82, 0xb7, 0x50, 0xaf, 0x6c, 0xb6, 0x80, 0xd1, 0x32, 0x7d, 0x3a, 0x8b,
+ 0x2b, 0xbe, 0x8d, 0xcc, 0x6c, 0xe4, 0xcf, 0x5d, 0x3b, 0x01, 0xf7, 0xfc,
+ 0xf5, 0x51, 0xae, 0x1d, 0x6b, 0xc8, 0x85, 0xa4, 0x78, 0xce, 0x81, 0x7c,
+ 0xff, 0xa6, 0x29, 0x80, 0xac, 0x44, 0xa9, 0xa3, 0xf0, 0x87, 0x69, 0x6f,
+ 0x7e, 0xc9, 0x37, 0xe6, 0x56, 0x8d, 0x8d, 0x9d, 0xa5, 0x31, 0x5d, 0xf4,
+ 0xb3, 0x05, 0x48, 0xd8, 0x78, 0xf6, 0x45, 0xdd, 0x43, 0x3c, 0x45, 0x9b,
+ 0x8f, 0xf8, 0x38, 0x62, 0xda, 0x6a, 0x13, 0x9d, 0x14, 0x9f, 0xd2, 0x43,
+ 0x00, 0x35, 0x9c, 0xeb, 0x94, 0xe0, 0x1c, 0x83, 0x4b, 0x14, 0x0c, 0x82,
+ 0x11, 0x6a, 0x70, 0x8a, 0xd1, 0x0d, 0xc3, 0x87, 0x43, 0x53, 0xa2, 0xa4,
+ 0x75, 0x40, 0x97, 0x05, 0xc5, 0x4c, 0x00, 0xdc, 0x69, 0x35, 0xbc, 0xed,
+ 0xaa, 0x35, 0x68, 0xd8, 0x4a, 0xf5, 0xc8, 0x58, 0xd5, 0x0b, 0x85, 0x15,
+ 0xef, 0x58, 0xf4, 0xf3, 0xfb, 0x45, 0x3a, 0xc0, 0xb1, 0xfe, 0xd2, 0x2d,
+ 0x77, 0x26, 0x49, 0x3c, 0x10, 0xbf, 0xcd, 0xe6, 0x68, 0x9a, 0x02, 0xd0,
+ 0xc7, 0x15, 0xa0, 0xe6, 0xb5, 0xcf, 0x26, 0xaf, 0xc4, 0x44, 0xe1, 0xdf,
+ 0xfc, 0x15, 0x7f, 0xc6, 0x09, 0x6b, 0x4a, 0xfc, 0x74, 0x73, 0xb8, 0x6e,
+ 0x2e, 0xff, 0x57, 0xd6, 0x1f, 0xca, 0x2c, 0x73, 0x5e, 0x07, 0xf0, 0x06,
+ 0xc4, 0xa5, 0x36, 0x80, 0x5e, 0x65, 0x81, 0x6e, 0x09, 0x0f, 0x13, 0xba,
+ 0x34, 0x54, 0x7e, 0x09, 0x13, 0x85, 0x45, 0xb7, 0x19, 0x09, 0xe0, 0xd9,
+ 0x98, 0x84, 0xd9, 0x6e, 0x36, 0x7c, 0x8e, 0x03, 0x46, 0x4f, 0xd5, 0x74,
+ 0x2b, 0xfa, 0x0e, 0xe4, 0xdf, 0x82, 0xad, 0x1f, 0x0c, 0x5d, 0x80, 0x00,
+ 0x02, 0x8d, 0x30, 0x53, 0xf4, 0x44, 0xe2, 0x36, 0xad, 0x4b, 0x97, 0x8c,
+ 0x36, 0xf4, 0x4d, 0xb3, 0xc8, 0xf4, 0xb4, 0x8a, 0x0b, 0xe0, 0x5b, 0x8d,
+ 0x04, 0xcf, 0x66, 0xbf, 0x74, 0x8b, 0x4e, 0xfa, 0x0e, 0x4a, 0x50, 0x81,
+ 0x91, 0xff, 0xde, 0x43, 0xc1, 0x6b, 0x78, 0x63, 0x77, 0x21, 0xf3, 0x25,
+ 0xe1, 0x0d, 0x49, 0x49, 0x8a, 0xe7, 0xe9, 0x7a, 0xd2, 0xd6, 0x09, 0xd4,
+ 0x57, 0x05, 0x53, 0xae, 0xeb, 0xbf, 0x59, 0x98, 0xad, 0xfd, 0x55, 0xbb,
+ 0xac, 0xba, 0xa0, 0xdc, 0x11, 0x11, 0xaa, 0xba, 0x94, 0x9d, 0x26, 0x75,
+ 0x91, 0xce, 0x5a, 0x22, 0xe4, 0xa6, 0x09, 0x30, 0x79, 0xe2, 0xc9, 0xcb,
+ 0x5d, 0x13, 0x6e, 0x74, 0x95, 0x07, 0x98, 0xe7, 0x08, 0xe4, 0x32, 0xa4,
+ 0x2d, 0x76, 0x65, 0x7e, 0xe4, 0xfc, 0x72, 0xae, 0xb9, 0xc5, 0x91, 0x2d,
+ 0xbd, 0xe7, 0x02, 0x51, 0x8b, 0x29, 0xa5, 0x09, 0x5b, 0x46, 0x86, 0xba,
+ 0x55, 0xb2, 0xb0, 0x14, 0x59, 0x75, 0x41, 0x75, 0x6c, 0x09, 0x94, 0x02,
+ 0x70, 0x7d, 0xa0, 0xd6, 0x8c, 0x16, 0x79, 0x8f, 0x3a, 0x67, 0x74, 0xeb,
+ 0x9e, 0x18, 0xf9, 0xd0, 0x45, 0x72, 0xaa, 0xa1, 0x85, 0xfc, 0x5c, 0x83,
+ 0xf7, 0x59, 0x4b, 0x1f, 0x49, 0xd5, 0x5b, 0xf5, 0x87, 0xf5, 0xd7, 0xc6,
+ 0xc1, 0x4f, 0x72, 0xc6, 0x76, 0xdd, 0xdb, 0x47, 0x05, 0xaa, 0xed, 0xc1,
+ 0xd3, 0x51, 0xc3, 0xc9, 0x8a, 0xd4, 0x26, 0xff, 0x5e, 0x80, 0xda, 0x38,
+ 0xc6, 0x6d, 0xdf, 0x22, 0xa1, 0x8b, 0x0b, 0xe5, 0x1f, 0xb7, 0xbe, 0xf1,
+ 0xf9, 0xe0, 0x7e, 0x93, 0x7f, 0x1c, 0x7a, 0xbc, 0x5c, 0xaa, 0xe9, 0xf9,
+ 0xaa, 0x25, 0x38, 0x88, 0xd0, 0xf7, 0x4f, 0xc0, 0xb9, 0xb3, 0x40, 0xc9,
+ 0xf0, 0xf9, 0x13, 0x09, 0x86, 0x88, 0x58, 0x7e, 0x81, 0xa7, 0xaf, 0xeb,
+ 0x1b, 0x66, 0x39, 0x40, 0x2c, 0xeb, 0x11, 0x57, 0xdb, 0x8f, 0xb9, 0x12,
+ 0x54, 0x3d, 0x6e, 0x86, 0xb8, 0x7e, 0x37, 0xc0, 0xd3, 0x71, 0xc0, 0xe2,
+ 0xcd, 0x5f, 0x94, 0xaf, 0xfe, 0x55, 0x76, 0x57, 0x1e, 0x6d, 0x9e, 0xc5,
+ 0xef, 0x5e, 0x30, 0xcf, 0xa9, 0xcd, 0x8d, 0xd7, 0x84, 0x9a, 0x32, 0xcf,
+ 0x3e, 0xf4, 0x62, 0x77, 0x66, 0x49, 0xb9, 0xc4, 0xf5, 0xcf, 0x71, 0x94,
+ 0x1d, 0x61, 0xa7, 0x4f, 0x4f, 0xa9, 0x5e, 0xf5, 0x55, 0xa3, 0xe5, 0xb8,
+ 0x91, 0x47, 0x53, 0x12, 0x72, 0x2b, 0xae, 0x93, 0xe9, 0xee, 0x07, 0x8e,
+ 0x37, 0x27, 0x2b, 0x13, 0x31, 0xdb, 0xe5, 0xc5, 0x39, 0x73, 0x3c, 0x71,
+ 0xb9, 0xad, 0x04, 0x99, 0xfe, 0x00, 0x44, 0xc4, 0xb6, 0xc9, 0x12, 0xc3,
+ 0x15, 0xb6, 0x2c, 0x9c, 0xde, 0x5a, 0x7a, 0x39, 0xc2, 0x66, 0xf1, 0x7c,
+ 0x4c, 0x93, 0x0a, 0x1d, 0x2e, 0xae, 0x2d, 0x1f, 0x23, 0x09, 0x77, 0x4a,
+ 0x24, 0x62, 0x6b, 0x7d, 0x49, 0x4f, 0xc2, 0x31, 0x76, 0x78, 0x31, 0xfc,
+ 0xbc, 0x58, 0x0e, 0xce, 0x1a, 0x65, 0xbf, 0x29, 0xb8, 0x35, 0xf9, 0x28,
+ 0x75, 0xfe, 0xbd, 0x54, 0x8e, 0xe5, 0x6d, 0x1e, 0xa8, 0x48, 0x4a, 0xf0,
+ 0x1f, 0x51, 0xb5, 0x2f, 0xd9, 0xb1, 0x57, 0x6c, 0xc5, 0x6b, 0x57, 0xa1,
+ 0xf4, 0x10, 0xaf, 0x20, 0xfe, 0xad, 0x5f, 0x3f, 0xff, 0xf7, 0xef, 0x80,
+ 0x87, 0xdf, 0x77, 0x9d, 0xe6, 0x83, 0xa9, 0x4e, 0x26, 0x37, 0xbc, 0x7f,
+ 0xed, 0xe2, 0xd6, 0xc0, 0x7f, 0x32, 0x2c, 0x6a, 0xb5, 0x43, 0xad, 0xbe,
+ 0xfb, 0x47, 0x67, 0x50, 0x26, 0x67, 0xb9, 0xe9, 0x86, 0xdf, 0x27, 0x06,
+ 0x18, 0x66, 0x91, 0x9c, 0x5f, 0xec, 0x5c, 0x56, 0xcc, 0xcf, 0x05, 0x95,
+ 0xd8, 0xfa, 0xcc, 0x3e, 0x06, 0x3c, 0xd0, 0x53, 0x37, 0x83, 0x0d, 0x96,
+ 0x7b, 0xca, 0xd0, 0x58, 0x71, 0x6b, 0x9c, 0x2c, 0xef, 0x25, 0x9e, 0xce,
+ 0x29, 0xd0, 0xf1, 0x9f, 0xb3, 0x97, 0x67, 0x14, 0x78, 0xc5, 0xa5, 0xb6,
+ 0xdf, 0xe9, 0x8a, 0xb8, 0xc2, 0xf2, 0x05, 0x00, 0x4c, 0xd8, 0xfe, 0x64,
+ 0x44, 0xfc, 0x27, 0x2c, 0x96, 0xf6, 0x8d, 0x88, 0xb9, 0x01, 0x49, 0xbf,
+ 0xd3, 0xc3, 0x93, 0x6a, 0x0f, 0xed, 0x15, 0xbc, 0x0b, 0x9b, 0x11, 0xe0,
+ 0x9e, 0x61, 0xc6, 0xa2, 0x99, 0x6b, 0x33, 0x4a, 0xf4, 0x92, 0xef, 0xe7,
+ 0x92, 0xde, 0x37, 0x78, 0x6f, 0x73, 0x7a, 0x32, 0x51, 0xbb, 0xc8, 0xaf,
+ 0x53, 0xd5, 0x5c, 0xa9, 0xf2, 0x88, 0x70, 0x22, 0xb7, 0x38, 0xd6, 0xb8,
+ 0x82, 0x2b, 0x19, 0xa5, 0x6f, 0x99, 0x57, 0xb4, 0xe1, 0xf1, 0x7f, 0xf5,
+ 0x40, 0x0d, 0x47, 0x5c, 0xea, 0x75, 0xd0, 0x47, 0xbc, 0x9a, 0x4c, 0x1e,
+ 0xb6, 0x57, 0xfe, 0x81, 0x55, 0x67, 0x1f, 0xad, 0xae, 0x98, 0x66, 0x65,
+ 0x2b, 0x32, 0x7d, 0x37, 0xe4, 0x88, 0x1e, 0x33, 0x20, 0x1a, 0x30, 0x64,
+ 0x43, 0xfa, 0x4f, 0x0c, 0x17, 0xff, 0x1e, 0x68, 0x40, 0xe3, 0xa1, 0x67,
+ 0x69, 0x88, 0xa8, 0x45, 0x3e, 0x99, 0xaa, 0x7e, 0x51, 0x6f, 0xbf, 0xf8,
+ 0xb6, 0xaa, 0xf0, 0x11, 0xe2, 0x8d, 0xf8, 0xe5, 0x2a, 0xe2, 0x59, 0xe2,
+ 0xdd, 0x48, 0xf7, 0x0c, 0x17, 0x1d, 0x55, 0x7c, 0x1c, 0x52, 0xc1, 0xd0,
+ 0x42, 0xf4, 0xcc, 0x6d, 0x04, 0x7a, 0x2b, 0xf9, 0x50, 0xe2, 0x7a, 0xe8,
+ 0xc1, 0xa9, 0xc9, 0x9d, 0x91, 0x5e, 0x7d, 0x61, 0x93, 0x15, 0x29, 0x51,
+ 0x29, 0x18, 0x12, 0x1a, 0x24, 0xcd, 0xbf, 0xb7, 0x55, 0x1c, 0x36, 0xb8,
+ 0x3b, 0xa8, 0x35, 0x0d, 0x19, 0xa6, 0xb7, 0x8b, 0x65, 0xb6, 0x24, 0xf8,
+ 0x93, 0x26, 0x11, 0xb3, 0xa1, 0x9b, 0x92, 0x40, 0x92, 0x1b, 0xfc, 0x70,
+ 0x06, 0x3f, 0x1a, 0x4e, 0x4e, 0xe3, 0xc8, 0x79, 0xba, 0xfb, 0x14, 0x23,
+ 0xd0, 0x33, 0x9e, 0x9d, 0xfe, 0xf6, 0x6a, 0x58, 0x9a, 0x4c, 0x0c, 0x00,
+ 0x2a, 0xc9, 0x65, 0xce, 0xbf, 0xd5, 0x1e, 0xd4, 0x94, 0x05, 0xe1, 0x86,
+ 0xf2, 0x5e, 0x16, 0xc0, 0x90, 0x2e, 0x1a, 0x31, 0x27, 0xc4, 0xc9, 0xaa,
+ 0xf8, 0xe5, 0x16, 0x1d, 0x7d, 0x85, 0x73, 0x33, 0x60, 0x73, 0xc8, 0x12,
+ 0xe5, 0xf9, 0xbb, 0xbc, 0xc3, 0xb2, 0xb2, 0x6e, 0xa1, 0xa8, 0x53, 0x48,
+ 0x32, 0x83, 0xec, 0xe2, 0xef, 0x81, 0xdf, 0x9e, 0xb1, 0x1c, 0x09, 0xc8,
+ 0xc9, 0x28, 0x84, 0x8a, 0x49, 0xaa, 0x82, 0x31, 0x44, 0x2c, 0x42, 0x23,
+ 0xdb, 0x24, 0xca, 0xe4, 0x70, 0xc0, 0x08, 0x2c, 0xfa, 0xaa, 0xc9, 0xe5,
+ 0x97, 0x7b, 0xf3, 0x3c, 0x4f, 0xa0, 0x60, 0x70, 0xa6, 0xc6, 0x2d, 0x4c,
+ 0xf8, 0xf0, 0x30, 0x57, 0x26, 0x37, 0x3e, 0x70, 0x6c, 0x45, 0x08, 0xed,
+ 0xc3, 0xd1, 0x9b, 0xa7, 0x9a, 0x0c, 0xab, 0xc9, 0x91, 0x80, 0xfc, 0xff,
+ 0x97, 0x1e, 0x09, 0x07, 0x54, 0x45, 0xd9, 0x06, 0xbf, 0xed, 0xcc, 0xb6,
+ 0x6b, 0x1c, 0x3c, 0x5c, 0xff, 0xbc, 0x83, 0x92, 0xa5, 0x3f, 0x02, 0x1f,
+ 0x35, 0x77, 0xcf, 0x24, 0x59, 0xf8, 0x63, 0x78, 0x4d, 0x1e, 0x96, 0x44,
+ 0x63, 0x97, 0x48, 0x23, 0x49, 0x2c, 0x63, 0x3a, 0x48, 0x4f, 0x9c, 0x82,
+ 0x66, 0xea, 0x6c, 0x4c, 0xaf, 0x9e, 0x64, 0xcf, 0x3e, 0x81, 0x31, 0x2e,
+ 0xb0, 0x4b, 0x45, 0xf1, 0x8c, 0xde, 0x22, 0xea, 0xfb, 0x72, 0xe5, 0x2d,
+ 0x18, 0xdf, 0x2f, 0xd2, 0x69, 0x0d, 0xa1, 0xe8, 0x60, 0x53, 0x3b, 0x48,
+ 0xf2, 0x29, 0x16, 0x2c, 0x73, 0xf7, 0x9c, 0xdc, 0x13, 0x93, 0xb5, 0x15,
+ 0xb7, 0x3f, 0x79, 0x62, 0xe6, 0xfd, 0x86, 0x17, 0x4a, 0xb6, 0x88, 0xfa,
+ 0x17, 0x9a, 0x56, 0xe1, 0xe1, 0xa3, 0x3b, 0x42, 0x06, 0x5f, 0x87, 0x3d,
+ 0x93, 0xad, 0xf2, 0xb6, 0x45, 0xe9, 0xf0, 0xea, 0xb7, 0xb9, 0xdd, 0x67,
+ 0x6a, 0xac, 0x87, 0x43, 0x55, 0xc3, 0x1e, 0x1c, 0x5a, 0xed, 0xa7, 0x91,
+ 0xd8, 0x82, 0x27, 0xeb, 0x2a, 0x68, 0xb9, 0xa0, 0x3c, 0x86, 0x5c, 0xd7,
+ 0x6f, 0xb3, 0xb7, 0xa0, 0x6f, 0x91, 0x64, 0xc5, 0xa7, 0x45, 0xd9, 0x23,
+ 0xd8, 0xf6, 0x69, 0x9f, 0xc6, 0x22, 0x66, 0xba, 0x64, 0x07, 0xaa, 0x50,
+ 0xd7, 0x07, 0xf7, 0xe4, 0xd8, 0x78, 0x9b, 0x75, 0xd1, 0x1a, 0xe2, 0xf5,
+ 0x14, 0x59, 0x24, 0x96, 0xec, 0xdd, 0x14, 0xef, 0xa2, 0xbf, 0x14, 0x1b,
+ 0x6e, 0xfd, 0x2c, 0x1f, 0xc9, 0x38, 0x5e, 0x85, 0x65, 0x7e, 0xb7, 0xe3,
+ 0x51, 0x5f, 0x0b, 0x3d, 0x30, 0xf5, 0xcf, 0x30, 0x98, 0x72, 0xce, 0xec,
+ 0xef, 0x8b, 0x04, 0xba, 0xba, 0xc6, 0xf1, 0xb4, 0x56, 0x7d, 0x5f, 0x64,
+ 0x8b, 0x17, 0x06, 0x64, 0x55, 0x84, 0xbf, 0x2e, 0x78, 0xda, 0x5d, 0xfc,
+ 0x81, 0xd6, 0xe4, 0x8a, 0x7e, 0x7b, 0x3a, 0x6c, 0x59, 0xdb, 0xd3, 0xa3,
+ 0xe7, 0x91, 0x7a, 0x52, 0x73, 0x95, 0x2b, 0x3d, 0x17, 0xd8, 0x34, 0xea,
+ 0x99, 0x68, 0xbf, 0x53, 0xc1, 0x31, 0xd6, 0x3b, 0xb1, 0xaa, 0x78, 0xb5,
+ 0xb1, 0x55, 0x6d, 0x0c, 0x96, 0x86, 0xa2, 0x2c, 0x36, 0xa7, 0xab, 0x62,
+ 0x3f, 0xcb, 0x66, 0x54, 0x6d, 0xab, 0xf7, 0x18, 0xc5, 0xec, 0x1c, 0xd8,
+ 0x3e, 0x7c, 0x19, 0xb7, 0x96, 0xd2, 0xa1, 0x12, 0xfb, 0xad, 0x27, 0x22,
+ 0x3e, 0xc4, 0x37, 0x07, 0x55, 0xc6, 0x92, 0x13, 0xac, 0xa9, 0x32, 0x3e,
+ 0x83, 0x02, 0x31, 0x5e, 0x94, 0x52, 0x66, 0x60, 0x99, 0x55, 0x4c, 0x3c,
+ 0x54, 0xe2, 0xe1, 0x16, 0x95, 0x4e, 0x81, 0x85, 0x32, 0x80, 0xe0, 0x89,
+ 0xa3, 0x9f, 0x73, 0x16, 0x8d, 0x70, 0xbf, 0x17, 0x71, 0x27, 0x16, 0xeb,
+ 0x06, 0xbb, 0xb4, 0xc3, 0x9d, 0xbb, 0x2a, 0xc3, 0xd4, 0xfa, 0x00, 0x2b,
+ 0x75, 0x3b, 0xbb, 0x5b, 0x14, 0xf1, 0x97, 0x92, 0x5e, 0xda, 0x71, 0x93,
+ 0xaf, 0xcb, 0x22, 0xeb, 0x1d, 0xf5, 0xfe, 0x42, 0x42, 0xcd, 0x13, 0x84,
+ 0x9a, 0xef, 0xb8, 0x55, 0x67, 0x28, 0x74, 0xa8, 0xea, 0xa1, 0x70, 0x6f,
+ 0x0c, 0x02, 0xe7, 0xfd, 0xcd, 0x7f, 0x1f, 0x74, 0xcb, 0xf3, 0x0f, 0x80,
+ 0x64, 0x30, 0xfc, 0xd0, 0xb2, 0x45, 0xc9, 0x84, 0x2b, 0x7c, 0x88, 0xa9,
+ 0xc0, 0x2c, 0x3d, 0xfe, 0x50, 0x2e, 0x5b, 0xfe, 0xec, 0xc8, 0xf0, 0x4d,
+ 0xab, 0xcc, 0xb6, 0xab, 0x55, 0x7a, 0x56, 0x20, 0x48, 0xff, 0x9d, 0x00,
+ 0xdf, 0x4e, 0x88, 0x2c, 0x0d, 0x97, 0xe7, 0x42, 0xe2, 0x84, 0xdd, 0x5f,
+ 0x41, 0xe5, 0x2c, 0x32, 0xdf, 0x7b, 0x1b, 0x81, 0x9e, 0xd3, 0x44, 0x33,
+ 0x57, 0x39, 0xa0, 0xa6, 0xd0, 0x61, 0xd6, 0x29, 0x49, 0x55, 0xae, 0xb0,
+ 0x0d, 0x76, 0x44, 0xe0, 0x4c, 0xb1, 0x34, 0x9b, 0x09, 0x19, 0x9b, 0x8e,
+ 0xf3, 0x7c, 0x66, 0x58, 0xf8, 0x24, 0xd8, 0xd9, 0x85, 0x61, 0x2a, 0x0d,
+ 0xd0, 0xfe, 0xd8, 0xa8, 0xeb, 0xd8, 0x26, 0x7e, 0x2d, 0x2d, 0x51, 0x48,
+ 0xae, 0xa6, 0x10, 0xc8, 0x51, 0x5e, 0x02, 0x2e, 0xd4, 0x4b, 0xcc, 0x29,
+ 0x6b, 0x5d, 0x22, 0x5d, 0x34, 0x6d, 0x4a, 0xff, 0x99, 0x78, 0x39, 0x13,
+ 0xe0, 0x49, 0xc2, 0x37, 0xf2, 0x8c, 0xab, 0xe4, 0x00, 0x6d, 0x29, 0x31,
+ 0x9b, 0xba, 0x51, 0x32, 0xc5, 0x08, 0x37, 0xc5, 0xec, 0x52, 0xb0, 0xf8,
+ 0x00, 0xe4, 0x12, 0xfb, 0xf8, 0x72, 0x59, 0x32, 0xea, 0x17, 0x48, 0xc2,
+ 0x7c, 0x5b, 0xc0, 0xf5, 0xde, 0x3f, 0xe9, 0x1d, 0x1d, 0xe6, 0x40, 0xb3,
+ 0x57, 0x29, 0x8b, 0x8b, 0x77, 0xfd, 0x9b, 0xbe, 0x9b, 0x50, 0x04, 0xc7,
+ 0x30, 0x7b, 0x16, 0x0a, 0x6f, 0x90, 0xff, 0xf7, 0xa6, 0x0c, 0xf3, 0x9d,
+ 0x42, 0xdc, 0xd8, 0x97, 0x37, 0xe1, 0x10, 0xde, 0xa4, 0x88, 0x70, 0xd6,
+ 0xad, 0xd0, 0x11, 0x45, 0x25, 0xaf, 0x6b, 0x53, 0x66, 0x37, 0x54, 0xf5,
+ 0x61, 0x44, 0x5e, 0xeb, 0x99, 0x77, 0x0b, 0xe2, 0x63, 0x25, 0xe3, 0x38,
+ 0xd6, 0xcc, 0x37, 0x1b, 0x40, 0xa1, 0x67, 0xe2, 0x32, 0x5c, 0xc3, 0x59,
+ 0x4a, 0x89, 0xcf, 0x37, 0x5b, 0xba, 0xb0, 0xd2, 0x5a, 0xa8, 0xc9, 0x30,
+ 0x6f, 0x76, 0x55, 0x52, 0xb4, 0x6e, 0x61, 0xd1, 0x37, 0x3d, 0x0f, 0x4a,
+ 0xc7, 0x9a, 0xb3, 0x6f, 0x61, 0x8e, 0x07, 0x3e, 0x38, 0x5f, 0x80, 0xaf,
+ 0xf5, 0x75, 0x8c, 0xb5, 0x66, 0x42, 0x68, 0x87, 0x48, 0x01, 0x15, 0x95,
+ 0x17, 0x0a, 0x2e, 0x3e, 0xc8, 0xaa, 0x65, 0xe2, 0x64, 0x5a, 0x3c, 0x37,
+ 0xb7, 0x15, 0x10, 0x26, 0x04, 0x5f, 0x19, 0x58, 0x7a, 0x38, 0xf8, 0x0b,
+ 0x25, 0x94, 0x2d, 0x4d, 0x20, 0xbb, 0x0c, 0x64, 0xa1, 0x0c, 0xd8, 0x34,
+ 0x81, 0x2a, 0xb2, 0x6c, 0xd4, 0x36, 0x18, 0x30, 0x95, 0x0d, 0xa9, 0x2a,
+ 0x03, 0xea, 0x9f, 0xfc, 0xef, 0x5d, 0x90, 0x80, 0x53, 0x89, 0xb6, 0xde,
+ 0xbe, 0x25, 0xe3, 0x66, 0x3d, 0x33, 0x0c, 0x86, 0x2c, 0xb2, 0x57, 0x24,
+ 0x7d, 0xd5, 0x11, 0xc0, 0x0c, 0x0e, 0xf5, 0x6c, 0x29, 0xe8, 0x02, 0xae,
+ 0x39, 0x3c, 0xa1, 0x41, 0x3f, 0x67, 0x7c, 0x60, 0x98, 0x02, 0x55, 0xcb,
+ 0x3d, 0xcb, 0xfb, 0x54, 0xad, 0x24, 0x72, 0x65, 0xb7, 0x93, 0x53, 0x62,
+ 0x93, 0x43, 0x24, 0xca, 0x43, 0xbb, 0xc5, 0x07, 0x77, 0x09, 0xdf, 0x98,
+ 0x4e, 0x45, 0x30, 0x09, 0x82, 0x3f, 0xbe, 0xfc, 0xd4, 0x3b, 0x29, 0x34,
+ 0xb9, 0x8f, 0x67, 0x21, 0xc6, 0x65, 0xa6, 0xb2, 0x75, 0x66, 0x61, 0x78,
+ 0xfc, 0xed, 0xc1, 0xe2, 0xa0, 0xe9, 0x47, 0x05, 0xa0, 0x35, 0x2f, 0x42,
+ 0x67, 0xa1, 0x9b, 0xb8, 0xe7, 0xe7, 0xd3, 0xc0, 0x33, 0xb3, 0xc3, 0x3a,
+ 0x34, 0x47, 0xb2, 0x01, 0x83, 0xb3, 0x56, 0xaf, 0xe0, 0x82, 0x23, 0xe7,
+ 0x25, 0x9b, 0x30, 0xea, 0x78, 0x9c, 0x70, 0x55, 0x4d, 0x99, 0x19, 0x3a,
+ 0xda, 0x12, 0xc1, 0x4d, 0x42, 0xbe, 0xcc, 0x07, 0xd4, 0x71, 0x1d, 0x20,
+ 0xb2, 0xf6, 0xe9, 0xdf, 0x62, 0xf9, 0x66, 0x75, 0x34, 0xc0, 0x99, 0x16,
+ 0xb6, 0xa1, 0x75, 0x2c, 0x38, 0x50, 0xe0, 0xfc, 0x6c, 0x18, 0xd8, 0x43,
+ 0x2b, 0xdf, 0x2d, 0xf0, 0xd3, 0x4e, 0x8e, 0x3f, 0x35, 0xb7, 0x54, 0x84,
+ 0xc8, 0x38, 0x5f, 0xd7, 0x00, 0xc6, 0xc1, 0x64, 0xda, 0x46, 0x0c, 0x2f,
+ 0xfe, 0x69, 0x12, 0xb4, 0x6e, 0x64, 0xff, 0x80, 0x75, 0x7b, 0x98, 0xa2,
+ 0xb6, 0xd4, 0x23, 0x6e, 0xea, 0x75, 0x43, 0x6f, 0x5c, 0x5e, 0x03, 0x77,
+ 0x40, 0x81, 0x43, 0x26, 0x31, 0xd0, 0x97, 0xc5, 0xa9, 0xe4, 0x78, 0x75,
+ 0x3d, 0x66, 0x73, 0x41, 0xe6, 0x21, 0x9a, 0x3a, 0xde, 0x19, 0x36, 0x93,
+ 0x8d, 0x56, 0xe2, 0x17, 0x3f, 0x14, 0x54, 0xc5, 0x20, 0xbf, 0xb6, 0x43,
+ 0x7b, 0x6c, 0xac, 0xf9, 0xbe, 0xa9, 0xe5, 0xfc, 0x57, 0xd7, 0x38, 0x6a,
+ 0x92, 0x07, 0x73, 0x92, 0xb2, 0x2b, 0xdb, 0xb1, 0xdb, 0x5c, 0xae, 0x53,
+ 0xac, 0x8b, 0xa1, 0xad, 0xd2, 0xe5, 0xe0, 0xa4, 0x2d, 0xe0, 0xbf, 0x70,
+ 0x60, 0xbf, 0xef, 0xa9, 0x62, 0x9e, 0x20, 0x1f, 0x43, 0xc3, 0xae, 0x70,
+ 0xb7, 0x22, 0x77, 0xe7, 0x94, 0x13, 0x93, 0x44, 0x68, 0x2f, 0x22, 0x29,
+ 0xac, 0xf3, 0xff, 0xc8, 0xce, 0x60, 0x3b, 0xa8, 0x9a, 0x99, 0x54, 0xcb,
+ 0x04, 0x0c, 0xf7, 0x41, 0xf8, 0xed, 0x1a, 0x25, 0x8c, 0xbe, 0x2d, 0x2c,
+ 0x85, 0x3e, 0x22, 0xec, 0x85, 0x13, 0xe6, 0x1f, 0x65, 0x0a, 0xd4, 0xc5,
+ 0xcf, 0x35, 0x2f, 0xb0, 0xa9, 0x5a, 0xe2, 0x0e, 0x5d, 0xf5, 0x32, 0x84,
+ 0xd7, 0xcd, 0x98, 0x15, 0xb5, 0x65, 0x5d, 0xff, 0xf0, 0x9d, 0x9c, 0x87,
+ 0x2d, 0x26, 0x2a, 0xdc, 0x05, 0xfc, 0x72, 0xdc, 0x35, 0x97, 0xd6, 0x20,
+ 0xf7, 0x9b, 0x64, 0x06, 0x5a, 0x68, 0x5a, 0x26, 0x1a, 0x87, 0x93, 0x9d,
+ 0xe9, 0xe4, 0x50, 0x5e, 0x59, 0xbc, 0xdb, 0x85, 0x23, 0x20, 0x7b, 0x28,
+ 0x5a, 0xbc, 0x21, 0x5e, 0xad, 0xfd, 0xfd, 0x0b, 0xb4, 0xfa, 0xe1, 0x4d,
+ 0xcb, 0xa3, 0xfe, 0xd2, 0xa6, 0x99, 0xb1, 0xe5, 0x4f, 0x05, 0x0f, 0xe8,
+ 0x28, 0xbf, 0x58, 0x13, 0xd7, 0x31, 0xbb, 0x09, 0xad, 0x8a, 0x2a, 0x3e,
+ 0x85, 0x06, 0x81, 0x2a, 0xdf, 0xe2, 0xd9, 0xfd, 0xb2, 0x42, 0xc2, 0x5c,
+ 0x98, 0xe5, 0xbd, 0x17, 0x6a, 0xc8, 0x89, 0xa7, 0x5d, 0x2e, 0xed, 0x84,
+ 0xe1, 0x37, 0xe4, 0x9f, 0x61, 0x21, 0x47, 0x54, 0xa4, 0xaa, 0x46, 0xce,
+ 0x55, 0x38, 0xb2, 0xfb, 0xc0, 0xa6, 0x8b, 0xe7, 0xb6, 0xf3, 0xb0, 0x7d,
+ 0x62, 0x1b, 0x0e, 0x2a, 0xf3, 0x39, 0xa3, 0x34, 0xf7, 0x8a, 0xb5, 0x91,
+ 0xfe, 0x73, 0xb9, 0xa5, 0xc3, 0x34, 0xb3, 0x8b, 0x71, 0xe5, 0xe4, 0x6c,
+ 0x34, 0x09, 0xef, 0x5c, 0x4a, 0x0c, 0x0f, 0xdf, 0xed, 0xab, 0x6c, 0x0f,
+ 0x49, 0x92, 0x9b, 0xf4, 0x95, 0xe1, 0xbf, 0xf8, 0x9d, 0xf1, 0xd1, 0xe2,
+ 0xf8, 0x81, 0x43, 0xdc, 0x39, 0xd1, 0xba, 0x51, 0x5f, 0xc6, 0xe4, 0x60,
+ 0x21, 0x68, 0xb2, 0xf3, 0xeb, 0xda, 0xd1, 0xfd, 0xaf, 0xc8, 0xb1, 0x02,
+ 0x94, 0xa0, 0xe8, 0x31, 0x1a, 0xe1, 0x6e, 0xff, 0xff, 0xee, 0xb2, 0x9c,
+ 0xe3, 0x73, 0x54, 0x0d, 0x13, 0x67, 0xde, 0x18, 0x97, 0xad, 0xc2, 0xc5,
+ 0xf3, 0x4d, 0xf2, 0x4d, 0x9b, 0x8c, 0xcf, 0xfa, 0xdb, 0x88, 0xe4, 0x5e,
+ 0x5b, 0xf5, 0xe3, 0x92, 0x3f, 0xff, 0xc2, 0x6b, 0xfe, 0xae, 0xea, 0x9b,
+ 0x52, 0xfe, 0xc0, 0x4b, 0xcc, 0xf9, 0xfa, 0x12, 0x63, 0xf8, 0x7f, 0xf8,
+ 0xf9, 0x88, 0x30, 0xcf, 0x57, 0xa2, 0x7b, 0xd2, 0x2a, 0xb6, 0xe1, 0xb9,
+ 0x77, 0x6e, 0x06, 0x60, 0xb9, 0x1b, 0x9d, 0x65, 0x40, 0x1a, 0x90, 0x90,
+ 0x4f, 0x17, 0xfb, 0x9b, 0xf9, 0x1d, 0x68, 0xb3, 0x6d, 0x59, 0x51, 0xee,
+ 0x54, 0x74, 0xb1, 0xf1, 0xa7, 0x62, 0xb2, 0x4e, 0xd7, 0x02, 0x47, 0x11,
+ 0x7b, 0x1d, 0x0e, 0x37, 0x08, 0xb7, 0x57, 0x97, 0x3b, 0x32, 0xda, 0x89,
+ 0xb9, 0x86, 0x2e, 0x5a, 0xec, 0x81, 0x08, 0x75, 0xaa, 0x41, 0x53, 0xd7,
+ 0x97, 0x99, 0x67, 0x25, 0x30, 0x6f, 0x49, 0x59, 0xae, 0x91, 0xf2, 0xa4,
+ 0xb7, 0x3e, 0x1e, 0xd3, 0xb3, 0x39, 0x3e, 0x5a, 0x4e, 0xc4, 0x53, 0x07,
+ 0x29, 0x6e, 0x02, 0x75, 0x83, 0x20, 0x4d, 0x26, 0xa0, 0xe1, 0xcb, 0xdb,
+ 0x56, 0xed, 0xd8, 0x0e, 0xf0, 0x3e, 0x80, 0x91, 0xb4, 0x23, 0x7a, 0xe9,
+ 0xef, 0x11, 0x73, 0x6b, 0xc6, 0x73, 0xc5, 0x46, 0x6d, 0x04, 0x64, 0xde,
+ 0x58, 0xdf, 0x3e, 0xb3, 0x41, 0x9c, 0x9d, 0x44, 0x6f, 0x58, 0x12, 0x80,
+ 0x48, 0x05, 0x14, 0xe8, 0x9c, 0xf2, 0xbb, 0xbd, 0x7a, 0x13, 0xf5, 0x9a,
+ 0x48, 0x72, 0xc3, 0x7d, 0x6d, 0x54, 0x1a, 0x7c, 0x30, 0xcd, 0x78, 0x10,
+ 0xac, 0x38, 0x46, 0xd4, 0x85, 0xf6, 0xe5, 0x35, 0x6b, 0xa1, 0x51, 0xea,
+ 0x4d, 0x16, 0x58, 0xe9, 0x79, 0x14, 0x3f, 0x92, 0x97, 0x01, 0x0f, 0x6d,
+ 0x34, 0x90, 0x91, 0x1c, 0xb9, 0xdc, 0xa7, 0xad, 0x1f, 0x8f, 0x3f, 0x24,
+ 0x00, 0xfe, 0x5e, 0xcb, 0xad, 0xaa, 0x0d, 0x88, 0xd1, 0xe3, 0xb0, 0xa7,
+ 0xda, 0x11, 0xef, 0x68, 0x81, 0x95, 0x80, 0x9e, 0x81, 0xdb, 0x26, 0xcc,
+ 0x09, 0x78, 0x3e, 0x60, 0xee, 0x39, 0xc2, 0xb1, 0x8b, 0xa0, 0x5b, 0x63,
+ 0xd3, 0xc8, 0xa9, 0x41, 0x64, 0xbd, 0x06, 0x11, 0x2b, 0xf0, 0x43, 0x08,
+ 0xf0, 0x5d, 0x3b, 0x3c, 0x7d, 0xb5, 0x6f, 0xec, 0x6b, 0x65, 0xf4, 0x12,
+ 0xdf, 0xa3, 0x6e, 0xab, 0x11, 0xca, 0xc7, 0x10, 0x61, 0xd1, 0xdd, 0xf2,
+ 0xa3, 0x03, 0xd7, 0x82, 0xf0, 0xf8, 0x30, 0xfe, 0x01, 0x42, 0x87, 0x58,
+ 0xb9, 0x9d, 0x85, 0xa1, 0x03, 0x31, 0xa3, 0x31, 0xc3, 0xc1, 0xa9, 0xad,
+ 0x13, 0x69, 0xcb, 0x4c, 0x75, 0x07, 0xc8, 0x60, 0x5a, 0x8f, 0x38, 0x5c,
+ 0xa4, 0x4b, 0xc3, 0x4b, 0x9a, 0xb2, 0x19, 0xda, 0xfd, 0xbf, 0x22, 0x19,
+ 0xaa, 0x29, 0xaa, 0x23, 0xa5, 0x75, 0xb2, 0x69, 0x3c, 0xe7, 0x39, 0xc8,
+ 0x5c, 0xda, 0x4d, 0xd2, 0x51, 0x95, 0x95, 0x71, 0xb8, 0x17, 0x10, 0x71,
+ 0xf5, 0x95, 0xa0, 0xf4, 0x11, 0x41, 0x40, 0x1d, 0x8e, 0xff, 0xaf, 0xf2,
+ 0xa3, 0xdd, 0xa2, 0x85, 0x4c, 0x3e, 0x82, 0x29, 0x4e, 0x6a, 0x08, 0xd1,
+ 0x87, 0x30, 0xaa, 0xe6, 0x59, 0x96, 0xab, 0xf7, 0x70, 0x88, 0x36, 0x3d,
+ 0x0b, 0x4e, 0xf0, 0x4f, 0x85, 0xe9, 0xa6, 0x86, 0xc7, 0xff, 0xd0, 0xf7,
+ 0xc2, 0xed, 0xe6, 0x87, 0xd2, 0xfd, 0x72, 0x2b, 0x34, 0x76, 0x6b, 0xed,
+ 0x9a, 0xd1, 0x55, 0x89, 0x44, 0x3b, 0xc5, 0xb9, 0xc0, 0xa2, 0x0a, 0x14,
+ 0x57, 0x22, 0x50, 0x45, 0x2d, 0xea, 0xe2, 0x2c, 0x8f, 0x8e, 0x45, 0x57,
+ 0x8c, 0x05, 0x7b, 0xe6, 0x27, 0x86, 0xb0, 0xef, 0x4e, 0xaf, 0xb7, 0x6b,
+ 0xe9, 0x41, 0x04, 0x07, 0x53, 0xf6, 0x68, 0x30, 0xd0, 0xc5, 0x5f, 0xf0,
+ 0x0e, 0xda, 0xf5, 0xd9, 0x8f, 0x01, 0x00, 0xf0, 0xdb, 0x72, 0xa6, 0x78,
+ 0xc2, 0xad, 0x6d, 0x05, 0x11, 0xae, 0x7e, 0x7a, 0xcb, 0x25, 0x02, 0xd4,
+ 0x5f, 0x70, 0x95, 0xaf, 0xda, 0x88, 0x2d, 0xa6, 0xfe, 0x3c, 0xfa, 0x7b,
+ 0x16, 0xfb, 0xb3, 0x4f, 0x5d, 0x6b, 0x43, 0x97, 0x51, 0x3c, 0xbf, 0xab,
+ 0x9f, 0xd4, 0x0f, 0x9f, 0xb2, 0x9e, 0x6d, 0xea, 0xbf, 0x08, 0x0c, 0xf0,
+ 0xb2, 0xe9, 0xd5, 0x31, 0x6e, 0x95, 0x8d, 0x2f, 0x2a, 0xc4, 0x7a, 0x43,
+ 0x27, 0x55, 0x32, 0x7c, 0xc2, 0xf2, 0xb2, 0x97, 0xf1, 0xd8, 0x4c, 0xd6,
+ 0x0d, 0xb7, 0xc6, 0x80, 0xc6, 0x5e, 0x0a, 0xd4, 0xf8, 0x09, 0x5d, 0xc1,
+ 0x4a, 0xad, 0x2e, 0x9f, 0x2e, 0x3a, 0x66, 0x73, 0xb9, 0xa3, 0x66, 0x73,
+ 0x51, 0x2c, 0x3c, 0xcb, 0xac, 0xad, 0x74, 0xb4, 0xce, 0xb8, 0x68, 0x0b,
+ 0xa5, 0x43, 0xc6, 0xa6, 0xca, 0xb6, 0x58, 0xdc, 0x91, 0xaf, 0x55, 0x06,
+ 0xe9, 0xb4, 0x51, 0x27, 0x21, 0x8b, 0x93, 0xef, 0x4b, 0x19, 0xc8, 0xd1,
+ 0x5a, 0x24, 0x64, 0x46, 0x34, 0xde, 0x85, 0x58, 0xdf, 0x13, 0x4a, 0xa8,
+ 0x6b, 0x7e, 0x21, 0x27, 0xbc, 0x1d, 0xcf, 0xc9, 0xb4, 0xd7, 0xd8, 0x16,
+ 0xe4, 0xd3, 0xed, 0x29, 0xcf, 0x70, 0xc1, 0x64, 0x4c, 0xd3, 0xe7, 0xf9,
+ 0x91, 0x1b, 0x80, 0x29, 0xce, 0x25, 0x7e, 0xf0, 0x97, 0x0a, 0x59, 0x61,
+ 0x09, 0x2e, 0x29, 0x28, 0xcc, 0x8e, 0xc1, 0x61, 0x72, 0x29, 0x81, 0xe8,
+ 0x92, 0xd5, 0xc1, 0x50, 0xb6, 0x16, 0xc6, 0xff, 0xad, 0x2f, 0x26, 0x34,
+ 0x88, 0x5e, 0x19, 0x96, 0x00, 0x5b, 0x7b, 0x84, 0x18, 0x0f, 0xc2, 0xeb,
+ 0x2b, 0x1d, 0xc1, 0x59, 0xd2, 0x30, 0xcb, 0x14, 0xa8, 0xc4, 0xd1, 0x12,
+ 0xf2, 0x4e, 0x5b, 0xff, 0x1f, 0x23, 0xe8, 0x0c, 0xa7, 0xff, 0xd8, 0xba,
+ 0xe2, 0x2e, 0xfb, 0x19, 0x5b, 0xa3, 0x84, 0xb4, 0x9e, 0x17, 0xf1, 0x7b,
+ 0x0c, 0xb9, 0x90, 0xc8, 0x95, 0x7c, 0xee, 0x02, 0x77, 0x8a, 0x31, 0xc8,
+ 0xac, 0x80, 0xa5, 0xe1, 0x47, 0x48, 0x2c, 0x1d, 0xad, 0x4a, 0x52, 0x4b,
+ 0xfa, 0x1d, 0x02, 0xe9, 0x08, 0xf8, 0xb5, 0x9e, 0xae, 0x58, 0x08, 0xa3,
+ 0xcb, 0xda, 0x13, 0x22, 0x16, 0xbe, 0x3e, 0x08, 0xf6, 0x47, 0x83, 0xe6,
+ 0xba, 0x3d, 0xb3, 0x3a, 0x88, 0x84, 0x5c, 0x7e, 0x99, 0xd4, 0x1b, 0x17,
+ 0xed, 0x48, 0xe0, 0x52, 0xfa, 0x0c, 0x37, 0xf3, 0x2a, 0x45, 0xda, 0x72,
+ 0x75, 0xa3, 0x21, 0x1e, 0x28, 0x6c, 0xcf, 0xa3, 0xae, 0x39, 0xa3, 0x88,
+ 0xb1, 0xc0, 0xcc, 0x03, 0xd2, 0x8d, 0x38, 0xe3, 0x13, 0x84, 0x99, 0xa6,
+ 0x89, 0x17, 0x79, 0x72, 0x35, 0x50, 0x3c, 0xdd, 0x76, 0x5f, 0xaa, 0xee,
+ 0xaf, 0xfa, 0xb7, 0x3b, 0xd5, 0xe4, 0x99, 0x61, 0x99, 0x3c, 0x70, 0xfb,
+ 0x7e, 0xa2, 0x28, 0x63, 0x02, 0xf8, 0x8d, 0x8c, 0x60, 0xbc, 0xeb, 0xb5,
+ 0x9f, 0x8c, 0x71, 0x01, 0x55, 0x5e, 0xd6, 0x8a, 0x67, 0x94, 0x86, 0xcb,
+ 0xc0, 0x72, 0x18, 0x92, 0xc5, 0xde, 0x8c, 0x2f, 0x9c, 0x01, 0x9a, 0x9a,
+ 0x31, 0xc2, 0xf1, 0xa2, 0x10, 0x56, 0x45, 0xa8, 0x8c, 0x31, 0x25, 0xf4,
+ 0x0c, 0x1b, 0x69, 0x21, 0x54, 0x2e, 0x58, 0x57, 0xcb, 0xcb, 0x75, 0x94,
+ 0x45, 0x5c, 0xea, 0x65, 0x14, 0xe6, 0x19, 0x95, 0x78, 0x45, 0x64, 0x7a,
+ 0x91, 0xac, 0xd6, 0x28, 0x1b, 0xc6, 0xa4, 0x6b, 0xe4, 0x92, 0x2b, 0x6d,
+ 0x40, 0xcc, 0x8d, 0xeb, 0x50, 0xc6, 0x2f, 0x14, 0xcb, 0x4e, 0xf9, 0xd7,
+ 0xe6, 0x35, 0x3d, 0x24, 0x4d, 0x7e, 0x6d, 0x34, 0xe7, 0x47, 0x0f, 0x3f,
+ 0x36, 0x50, 0x2f, 0x56, 0x64, 0x59, 0x3d, 0xc5, 0xf0, 0xe7, 0xe4, 0x8a,
+ 0x76, 0x4c, 0xf4, 0x8f, 0x47, 0x5d, 0xe6, 0xfa, 0xe8, 0x2b, 0x40, 0x96,
+ 0x8d, 0x50, 0x89, 0x16, 0x57, 0x28, 0xee, 0x41, 0x68, 0xd9, 0xc9, 0x9c,
+ 0x42, 0xd7, 0xc7, 0x5b, 0xe4, 0x24, 0x98, 0xda, 0xca, 0x7a, 0x16, 0xf0,
+ 0x24, 0x8d, 0xfd, 0xf9, 0x6b, 0x55, 0x48, 0x49, 0x25, 0x0d, 0xb4, 0x1e,
+ 0xce, 0x9a, 0xbe, 0x3e, 0x7d, 0x7c, 0x5e, 0x3e, 0x8e, 0xf0, 0xbd, 0xc1,
+ 0xba, 0xb4, 0x0a, 0xeb, 0xa3, 0x15, 0x1c, 0x8d, 0xcb, 0x88, 0x11, 0xc2,
+ 0x53, 0x8d, 0xb9, 0xbf, 0xe9, 0xc6, 0x68, 0x1a, 0x15, 0x2f, 0xbf, 0x3c,
+ 0xa1, 0x1a, 0xf6, 0x7f, 0xbc, 0x2e, 0x5d, 0x15, 0x61, 0x6a, 0xcd, 0xdb,
+ 0x21, 0x1c, 0x9c, 0x77, 0x04, 0xde, 0x88, 0x15, 0x42, 0x1e, 0xd5, 0xf4,
+ 0x03, 0x7a, 0x78, 0x8a, 0x58, 0xf5, 0xb1, 0xd8, 0x6b, 0x68, 0x18, 0xf9,
+ 0x1a, 0x57, 0x0f, 0x2a, 0x82, 0xc1, 0xe4, 0xaf, 0x61, 0x10, 0xf0, 0x47,
+ 0xa6, 0x91, 0x62, 0x1d, 0x49, 0xf0, 0x01, 0xcf, 0xe1, 0x94, 0xd2, 0xce,
+ 0x0b, 0xb1, 0x45, 0x2d, 0x84, 0x14, 0x28, 0xf0, 0x5b, 0xd9, 0xc7, 0x14,
+ 0xb8, 0x9d, 0x72, 0x77, 0x82, 0x21, 0x4a, 0xc3, 0x8f, 0xdb, 0x44, 0x91,
+ 0xbe, 0x9e, 0xc0, 0xbb, 0x47, 0x98, 0x03, 0xa0, 0xdd, 0x79, 0x7d, 0xba,
+ 0x3d, 0x29, 0xdb, 0x37, 0xba, 0x20, 0x3d, 0xe2, 0x02, 0x2a, 0x93, 0xd6,
+ 0x1f, 0xf7, 0x87, 0xeb, 0x84, 0x85, 0xcf, 0xb7, 0x0d, 0x00, 0xbd, 0x50,
+ 0xd6, 0xae, 0xb7, 0x25, 0x7e, 0xd5, 0x66, 0x28, 0x39, 0x6b, 0x26, 0xfb,
+ 0x62, 0x1e, 0xd7, 0x73, 0xd1, 0x15, 0x96, 0x3c, 0x14, 0xd8, 0x13, 0xc7,
+ 0x98, 0x8f, 0xeb, 0xc1, 0x54, 0x7a, 0x55, 0xcd, 0xb1, 0x0a, 0x38, 0x80,
+ 0xbe, 0x80, 0x89, 0x4c, 0x4e, 0x97, 0xcf, 0x42, 0x7b, 0xc3, 0x27, 0x5f,
+ 0xdb, 0x22, 0xb1, 0x4f, 0xac, 0x4f, 0x9d, 0xc1, 0x46, 0x2d, 0xed, 0xa5,
+ 0x0e, 0xc0, 0xbf, 0x8d, 0xdd, 0x9a, 0x06, 0xba, 0x59, 0xa1, 0xcf, 0xc6,
+ 0x9e, 0xd0, 0xb6, 0x45, 0xba, 0x4f, 0x36, 0x69, 0xf0, 0x4b, 0xea, 0x62,
+ 0x5d, 0x4f, 0xd2, 0x21, 0xf9, 0xdd, 0xc4, 0x74, 0xf2, 0x42, 0x74, 0xf0,
+ 0x4c, 0x32, 0x9b, 0xae, 0x55, 0x5c, 0x52, 0xfa, 0xd1, 0xa1, 0x16, 0x91,
+ 0x8e, 0x4d, 0xe7, 0xdc, 0xcd, 0xe9, 0x4f, 0x25, 0x44, 0x65, 0x20, 0xfb,
+ 0x25, 0x6f, 0x42, 0x3b, 0xc7, 0x96, 0xae, 0x1d, 0x4d, 0x88, 0x37, 0x47,
+ 0xd7, 0x47, 0x22, 0x40, 0xd0, 0xfe, 0x4f, 0x19, 0x4a, 0xa1, 0x7b, 0xf7,
+ 0xb3, 0x97, 0x13, 0x72, 0x2f, 0x4a, 0x4f, 0x87, 0x66, 0x13, 0xc5, 0xb6,
+ 0x1d, 0xab, 0x37, 0x60, 0x7c, 0x68, 0xdf, 0x92, 0xd9, 0x16, 0xc1, 0x27,
+ 0xff, 0x19, 0xc7, 0x19, 0xc0, 0x14, 0xf0, 0xf0, 0xcf, 0xeb, 0xa2, 0xa1,
+ 0x79, 0xab, 0x7b, 0x84, 0xda, 0xce, 0x49, 0x91, 0xe5, 0xc7, 0x88, 0x47,
+ 0x95, 0x29, 0xf7, 0xaa, 0x55, 0xf6, 0x2b, 0x89, 0xcd, 0x91, 0xe0, 0x4b,
+ 0xa6, 0xe7, 0xae, 0xcc, 0xa7, 0x67, 0x1e, 0xaa, 0xf3, 0x55, 0xb7, 0xe9,
+ 0x10, 0x75, 0xe0, 0x83, 0x2b, 0x0f, 0xbc, 0xa6, 0x88, 0xf8, 0x32, 0x28,
+ 0xe7, 0x99, 0xbb, 0x12, 0xd3, 0x30, 0xf7, 0xea, 0x18, 0x9a, 0x49, 0x8f,
+ 0x32, 0x76, 0x98, 0x9a, 0x7e, 0x06, 0x14, 0x35, 0x73, 0x68, 0xc5, 0xf6,
+ 0x3a, 0x4a, 0x07, 0x8e, 0xe6, 0xe7, 0x13, 0x73, 0x19, 0x1a, 0x27, 0xa7,
+ 0xd1, 0xbc, 0xf3, 0x1d, 0xc1, 0xa2, 0x0b, 0x47, 0x41, 0x4e, 0x78, 0xbc,
+ 0x71, 0x76, 0x8a, 0xca, 0x60, 0x63, 0xd6, 0x1d, 0xe9, 0xbe, 0x39, 0x99,
+ 0xee, 0x19, 0xdb, 0x49, 0x09, 0x29, 0x02, 0xb8, 0xd4, 0x0b, 0x19, 0xda,
+ 0xb8, 0x87, 0x3e, 0x5f, 0x77, 0x45, 0xd8, 0x80, 0x10, 0x38, 0x93, 0xdb,
+ 0x09, 0x35, 0x88, 0xb1, 0x9b, 0xee, 0x42, 0xd8, 0xb2, 0x67, 0xd7, 0x86,
+ 0x13, 0xf3, 0xc6, 0x75, 0x77, 0x4c, 0xa5, 0x0c, 0x44, 0x9c, 0xfa, 0xe9,
+ 0x06, 0xae, 0x62, 0x6c, 0xc8, 0xf3, 0xd8, 0xbe, 0xde, 0x6b, 0xf2, 0x5d,
+ 0x0c, 0xd5, 0xe5, 0xa4, 0x25, 0x58, 0xd6, 0xa4, 0x1e, 0xb5, 0xb7, 0xa6,
+ 0x3f, 0xb0, 0x51, 0xef, 0x49, 0xcd, 0x3e, 0x70, 0xa7, 0xc8, 0x53, 0x99,
+ 0x1c, 0x7e, 0x21, 0xcd, 0xfa, 0x3f, 0x22, 0x2a, 0x35, 0xf7, 0x5b, 0xfc,
+ 0x33, 0xb6, 0x58, 0x86, 0xac, 0x64, 0x33, 0x43, 0xa1, 0x09, 0x39, 0x6b,
+ 0xb8, 0xa3, 0x32, 0x16, 0xb3, 0xbe, 0x30, 0x02, 0xa1, 0x66, 0xb1, 0x5a,
+ 0x7e, 0xc7, 0x12, 0xc3, 0x21, 0xf6, 0x27, 0x2e, 0xc4, 0x48, 0x17, 0xb8,
+ 0xfe, 0xa5, 0x48, 0x9f, 0x93, 0x5f, 0x6b, 0xaa, 0xd2, 0x1b, 0x68, 0xc1,
+ 0xaf, 0x8b, 0x69, 0xca, 0xe9, 0x60, 0xa6, 0xc6, 0x93, 0x3c, 0x42, 0xba,
+ 0xaa, 0x6e, 0x77, 0x9e, 0x4f, 0x35, 0xa4, 0xb1, 0x8c, 0x95, 0x15, 0x7a,
+ 0x80, 0xa9, 0x97, 0x3b, 0xb0, 0x66, 0x36, 0xf6, 0x3a, 0xe8, 0xcf, 0x99,
+ 0xe0, 0xaa, 0xaf, 0x2c, 0x3b, 0x60, 0x21, 0x24, 0xb7, 0x71, 0xa7, 0x96,
+ 0x0a, 0x27, 0xca, 0x88, 0x72, 0x4f, 0xda, 0x51, 0xda, 0x38, 0x97, 0x9f,
+ 0x9a, 0x27, 0x24, 0xac, 0x4b, 0x6a, 0x4c, 0x10, 0x6b, 0xe9, 0x30, 0x2d,
+ 0xcb, 0x64, 0xa6, 0xce, 0x89, 0x4e, 0xe7, 0x0a, 0xc8, 0x47, 0x5d, 0x2a,
+ 0x38, 0x54, 0xc4, 0x7e, 0x0c, 0xea, 0x2e, 0x73, 0xf5, 0x72, 0xe7, 0xf9,
+ 0x94, 0x2c, 0xf5, 0x99, 0x9a, 0x04, 0xc9, 0x92, 0x71, 0xc6, 0xd3, 0xd0,
+ 0xff, 0xd2, 0xcf, 0xbf, 0x8b, 0xdb, 0xc9, 0xe4, 0x10, 0xc1, 0xcc, 0x29,
+ 0x62, 0x4f, 0xb7, 0x4e, 0x6d, 0xf3, 0xb0, 0xaf, 0xb1, 0x9b, 0x34, 0xf9,
+ 0xd5, 0xf1, 0x4c, 0xf3, 0x0e, 0xc3, 0xca, 0xcb, 0xa5, 0x92, 0x41, 0xc5,
+ 0x0a, 0xcb, 0x95, 0x8b, 0xa9, 0xf6, 0x02, 0x86, 0x60, 0xbb, 0x72, 0xf6,
+ 0x94, 0xae, 0x40, 0xbc, 0xfb, 0x8d, 0xed, 0xfa, 0x61, 0xe2, 0x03, 0xe0,
+ 0x01, 0x1c, 0xa6, 0xb0, 0xaa, 0x78, 0x52, 0x67, 0x45, 0xd3, 0x7d, 0x07,
+ 0x5d, 0x9c, 0xdf, 0xc0, 0xca, 0x52, 0xa4, 0xf9, 0x8e, 0xb6, 0x3a, 0x1f,
+ 0x06, 0x54, 0xa3, 0x02, 0xab, 0x0f, 0xb7, 0xf7, 0x87, 0x06, 0xbc, 0x43,
+ 0x26, 0xec, 0x8e, 0x57, 0xf6, 0x67, 0xa7, 0x68, 0x98, 0xfc, 0x17, 0x98,
+ 0x75, 0xa9, 0xb1, 0x4f, 0xdd, 0x37, 0x21, 0xc6, 0xb2, 0x9d, 0xa2, 0x9b,
+ 0xb1, 0x9f, 0x3b, 0xd6, 0x3f, 0xab, 0xa4, 0x8c, 0x33, 0x2a, 0xb5, 0x3b,
+ 0x10, 0xc6, 0xa4, 0xf9, 0x10, 0xd8, 0x1b, 0xdf, 0x46, 0xfe, 0xea, 0xf8,
+ 0xfa, 0xf9, 0xd8, 0xe6, 0x45, 0x8d, 0x65, 0x89, 0xcf, 0x9e, 0x93, 0x0e,
+ 0xbb, 0xca, 0xc0, 0xc3, 0x33, 0xe9, 0xe8, 0x4c, 0x50, 0xe8, 0xcd, 0xc4,
+ 0x26, 0xe7, 0x3d, 0x96, 0x0f, 0xc7, 0x07, 0x30, 0xaa, 0x62, 0xd4, 0x17,
+ 0x17, 0x2f, 0x7c, 0x4c, 0x53, 0xc9, 0xa5, 0x52, 0x52, 0xa7, 0x19, 0xd7,
+ 0x34, 0x0e, 0xf1, 0x4c, 0x69, 0x17, 0xdf, 0x43, 0xdb, 0x5f, 0x20, 0xf8,
+ 0xe3, 0x48, 0xd5, 0x24, 0xbc, 0x08, 0x3d, 0xcb, 0x2b, 0x5e, 0xf7, 0x7c,
+ 0x5d, 0xa4, 0x2f, 0xdf, 0x8a, 0x4a, 0x27, 0x18, 0x51, 0x31, 0x2d, 0x0a,
+ 0xf8, 0xdc, 0x30, 0xe6, 0xfa, 0x7a, 0x00, 0x61, 0xcd, 0xdb, 0x93, 0x43,
+ 0xc3, 0xbd, 0xc6, 0x0b, 0x82, 0x57, 0xf7, 0x9f, 0x0c, 0xb0, 0x43, 0xf2,
+ 0xe9, 0x61, 0xc6, 0x3a, 0x2a, 0x57, 0x4c, 0x74, 0x56, 0x07, 0x57, 0x7c,
+ 0xf1, 0xc8, 0x60, 0x55, 0xf7, 0x17, 0x17, 0xc7, 0xc0, 0x76, 0x11, 0xee,
+ 0xb4, 0xa1, 0x1c, 0x47, 0xf8, 0xd5, 0xb8, 0x1f, 0xe4, 0x47, 0x93, 0x17,
+ 0x4e, 0x55, 0xf1, 0x9f, 0x58, 0xb3, 0x46, 0xf1, 0x20, 0xd0, 0xe0, 0x30,
+ 0x7e, 0x5d, 0x18, 0x61, 0x8d, 0xe2, 0xf9, 0x78, 0xb3, 0x30, 0x41, 0xe5,
+ 0xe0, 0x1f, 0xff, 0x76, 0x51, 0xc6, 0x2b, 0x9a, 0x20, 0xe7, 0x6b, 0x79,
+ 0xe2, 0xad, 0x0d, 0x2d, 0x9b, 0x48, 0x2f, 0xbf, 0x44, 0x4a, 0xcb, 0xb0,
+ 0xd3, 0x63, 0x48, 0xab, 0xfe, 0x32, 0xef, 0xf9, 0x55, 0x09, 0x84, 0xe3,
+ 0xfc, 0x09, 0x30, 0x23, 0xcf, 0x30, 0x34, 0x43, 0x1e, 0x59, 0x65, 0x44,
+ 0x8a, 0x89, 0x2c, 0x1e, 0x3a, 0x3a, 0x17, 0x49, 0x0c, 0x0b, 0x7d, 0xb3,
+ 0xfd, 0x26, 0x73, 0x5b, 0xd1, 0x98, 0x08, 0x1a, 0x2a, 0xfc, 0xe9, 0x9e,
+ 0x61, 0xfb, 0x06, 0x97, 0x84, 0x2c, 0xb7, 0xf5, 0xca, 0xd9, 0xdf, 0x6c,
+ 0x22, 0x23, 0x03, 0xb6, 0x0c, 0x0c, 0xdb, 0x75, 0x00, 0x47, 0xfc, 0x5c,
+ 0xd1, 0x6a, 0xcb, 0x68, 0xe9, 0x35, 0x4b, 0x26, 0x0a, 0xda, 0x09, 0x86,
+ 0xdf, 0xda, 0x5a, 0x97, 0xdc, 0x59, 0x0e, 0x74, 0x6c, 0x0c, 0xc8, 0xc7,
+ 0x58, 0x55, 0xf8, 0x63, 0x7c, 0x75, 0x19, 0x13, 0x7a, 0xa1, 0xb9, 0xe4,
+ 0x62, 0xf0, 0x59, 0x0c, 0x37, 0xa5, 0x5e, 0xce, 0x62, 0x52, 0x26, 0x2b,
+ 0x35, 0xae, 0x98, 0x0e, 0x3e, 0xc8, 0x90, 0x34, 0x24, 0xbb, 0xe9, 0xdf,
+ 0x53, 0x3b, 0x03, 0x47, 0x47, 0x03, 0x0a, 0xbc, 0xd1, 0x15, 0xc6, 0x27,
+ 0x82, 0x36, 0x4b, 0xd7, 0x77, 0x52, 0x7e, 0x85, 0x21, 0x83, 0x0a, 0xb2,
+ 0xdd, 0x93, 0xc6, 0x2a, 0x79, 0x17, 0x75, 0xe5, 0xbc, 0xdb, 0x29, 0xb5,
+ 0xd1, 0xc7, 0x0d, 0xdd, 0x45, 0xf2, 0x3f, 0xda, 0x7f, 0xba, 0x14, 0xe8,
+ 0x6f, 0xf7, 0x2d, 0x49, 0x07, 0x19, 0x02, 0x9d, 0x99, 0x96, 0xb4, 0x1e,
+ 0xc0, 0xf2, 0x29, 0x43, 0xfe, 0xc2, 0xb9, 0xd2, 0xec, 0x59, 0x13, 0x20,
+ 0xd7, 0xcd, 0x6e, 0x5b, 0xae, 0x67, 0x7b, 0xd0, 0xd9, 0x4c, 0xf4, 0x42,
+ 0xfa, 0xe5, 0xe1, 0x91, 0xe9, 0xac, 0xd8, 0xde, 0x25, 0x34, 0x13, 0x56,
+ 0x82, 0x74, 0xd7, 0xf2, 0x45, 0x8a, 0x7e, 0xfa, 0x6c, 0x56, 0x8c, 0x2c,
+ 0x94, 0x45, 0x29, 0x01, 0xc0, 0xbb, 0x82, 0x86, 0x63, 0xb4, 0x52, 0x3a,
+ 0xab, 0x92, 0x76, 0x81, 0xf7, 0xdb, 0xff, 0x64, 0x6b, 0x18, 0x27, 0xde,
+ 0xfa, 0x41, 0x04, 0x8f, 0xaa, 0xae, 0x3e, 0x74, 0x01, 0x95, 0x4f, 0xf9,
+ 0x5d, 0xba, 0x12, 0x0e, 0xd4, 0xee, 0x59, 0x85, 0xf4, 0x70, 0x19, 0x56,
+ 0xd9, 0x23, 0x35, 0x46, 0x83, 0xde, 0x6f, 0x3c, 0xd6, 0x3c, 0x8c, 0xfe,
+ 0x0b, 0x22, 0xab, 0x79, 0x17, 0xb2, 0x9c, 0x42, 0x75, 0xdb, 0x33, 0x23,
+ 0x04, 0x8a, 0x47, 0x4e, 0xaf, 0x46, 0x3e, 0x77, 0x1d, 0x1b, 0x52, 0xed,
+ 0xad, 0x50, 0x95, 0xd6, 0xd2, 0x87, 0x9d, 0x4b, 0x47, 0xfb, 0x05, 0xc2,
+ 0x79, 0xbb, 0x8e, 0x62, 0x7d, 0xa5, 0xa1, 0x0e, 0x8a, 0x21, 0xb9, 0x47,
+ 0x6e, 0xe7, 0x5c, 0x4a, 0x43, 0xc3, 0xa4, 0x68, 0xab, 0x04, 0x18, 0x87,
+ 0x53, 0xf8, 0x06, 0x2e, 0xb5, 0x15, 0x45, 0xcc, 0x76, 0x9d, 0x18, 0xb8,
+ 0x61, 0x59, 0x7d, 0x4e, 0x42, 0x3a, 0x07, 0x4c, 0xc7, 0x82, 0xfd, 0x2d,
+ 0x5f, 0x48, 0xc1, 0x68, 0xe6, 0x76, 0xaf, 0x36, 0x1b, 0x14, 0x18, 0xb7,
+ 0x50, 0x1c, 0x69, 0x69, 0x79, 0x1b, 0x30, 0xbc, 0x39, 0x9f, 0x90, 0x3f,
+ 0x50, 0x35, 0x1e, 0xd9, 0xab, 0x4b, 0x0b, 0xc0, 0xef, 0x8e, 0xd4, 0xce,
+ 0xef, 0xeb, 0x44, 0x3e, 0xc1, 0xb6, 0xc2, 0x03, 0x88, 0x25, 0x0c, 0xe7,
+ 0x76, 0x5b, 0xdb, 0xe4, 0x13, 0x06, 0xc8, 0x2c, 0xbc, 0xac, 0x31, 0x1f,
+ 0xe1, 0x35, 0x92, 0x38, 0xe4, 0x39, 0x28, 0x81, 0xa8, 0x02, 0x54, 0x3f,
+ 0x71, 0x82, 0x7a, 0x9c, 0xbd, 0xc3, 0x24, 0x2d, 0xd7, 0x18, 0x64, 0x9c,
+ 0xb7, 0x87, 0x22, 0xf3, 0x1a, 0xa4, 0x02, 0x29, 0x98, 0x5a, 0xbc, 0x76,
+ 0x41, 0xd7, 0x98, 0xbb, 0xc5, 0x7d, 0x93, 0x94, 0xa4, 0xe9, 0x81, 0x25,
+ 0xd4, 0x50, 0xe6, 0x70, 0x74, 0x18, 0xb1, 0xad, 0x89, 0xb6, 0xf6, 0xe1,
+ 0x4e, 0x66, 0x02, 0xeb, 0x1d, 0x24, 0xfa, 0x81, 0xc0, 0xcc, 0xb1, 0x74,
+ 0x2d, 0x97, 0x07, 0x8f, 0x79, 0x64, 0x18, 0x5a, 0x99, 0x4f, 0xde, 0x2b,
+ 0xcc, 0xc7, 0x2e, 0x28, 0xca, 0xf8, 0xdf, 0x11, 0xf6, 0xbf, 0xe1, 0xbf,
+ 0x6e, 0xc1, 0x20, 0x05, 0x96, 0x46, 0xb1, 0x56, 0xca, 0x2d, 0x49, 0x17,
+ 0xca, 0x28, 0x5b, 0x5f, 0xbe, 0x8d, 0xe0, 0xd8, 0x79, 0x97, 0x3f, 0xc0,
+ 0xc0, 0x33, 0x0a, 0xd0, 0x66, 0xf5, 0x34, 0xde, 0x75, 0x0a, 0x9d, 0xa5,
+ 0x71, 0xc6, 0x52, 0x1d, 0x68, 0x15, 0x35, 0x4e, 0xa9, 0xa3, 0xba, 0x9d,
+ 0x92, 0x5b, 0x16, 0xe3, 0x96, 0x65, 0x26, 0x22, 0x7e, 0x98, 0x23, 0x97,
+ 0xe0, 0x17, 0xc8, 0xb7, 0xbd, 0x09, 0x11, 0xb8, 0xd0, 0x45, 0xe2, 0x32,
+ 0x92, 0xf5, 0xd1, 0xdb, 0xc0, 0x7b, 0x12, 0x84, 0xde, 0x13, 0x0c, 0x44,
+ 0xb4, 0x80, 0x04, 0x2b, 0x8f, 0x20, 0x1f, 0x06, 0x38, 0x2f, 0x44, 0x92,
+ 0xe5, 0xfd, 0x70, 0x14, 0xdd, 0xbe, 0xe5, 0xff, 0x0a, 0x28, 0x9d, 0x42,
+ 0x75, 0x13, 0x03, 0xea, 0xa0, 0x9b, 0xdc, 0x97, 0xda, 0x13, 0xf2, 0x66,
+ 0x9d, 0x2f, 0x75, 0x28, 0xea, 0x12, 0xd3, 0x0b, 0x3a, 0x6b, 0x67, 0xb2,
+ 0x21, 0xd5, 0xb1, 0x47, 0xa9, 0xf6, 0x6a, 0x29, 0x01, 0xf7, 0x0b, 0x18,
+ 0x76, 0x19, 0xc0, 0xa0, 0xee, 0x43, 0xa8, 0x8a, 0x77, 0x02, 0x70, 0x57,
+ 0xea, 0xf0, 0x5d, 0xa5, 0xb5, 0x77, 0x51, 0x8c, 0x7d, 0xd6, 0xe7, 0xc1,
+ 0xf0, 0x4a, 0xcf, 0xc8, 0x87, 0x04, 0x57, 0xd4, 0x21, 0xc6, 0xe1, 0xc5,
+ 0x01, 0x98, 0xa9, 0xa1, 0xb2, 0x4b, 0x72, 0xa8, 0xaf, 0xe5, 0xb6, 0x16,
+ 0x63, 0xf0, 0xac, 0x05, 0x45, 0x51, 0x93, 0xfd, 0xa7, 0xc8, 0xbe, 0x6a,
+ 0x0e, 0x15, 0xa0, 0x94, 0x57, 0x72, 0x7f, 0x79, 0x14, 0x13, 0x09, 0x6c,
+ 0x97, 0x06, 0x72, 0x2a, 0xc2, 0x15, 0xf9, 0x9c, 0x09, 0x5e, 0xf6, 0x58,
+ 0xaf, 0x5d, 0x36, 0x1f, 0x3f, 0x38, 0xfe, 0xd5, 0x70, 0xa3, 0x91, 0x2e,
+ 0x0f, 0x2e, 0xba, 0xe1, 0xf6, 0x14, 0x85, 0x27, 0xc3, 0x73, 0x3b, 0x66,
+ 0xdd, 0xdf, 0xd0, 0xe3, 0x16, 0xc4, 0x91, 0x52, 0xc1, 0xb3, 0x49, 0x0d,
+ 0x96, 0xe1, 0x8d, 0xcd, 0xdd, 0xa6, 0x49, 0x64, 0xa1, 0xb4, 0xe6, 0xb5,
+ 0xc6, 0x47, 0x62, 0x2a, 0x0d, 0xec, 0xce, 0x65, 0xf9, 0x0d, 0x5d, 0x7b,
+ 0x46, 0x39, 0x26, 0x75, 0x3e, 0xee, 0x4f, 0xf0, 0x32, 0x49, 0x24, 0x07,
+ 0xac, 0xcf, 0x20, 0xbf, 0x28, 0x58, 0xee, 0x2a, 0x4b, 0x21, 0xfd, 0x28,
+ 0x21, 0x14, 0x71, 0x7e, 0x34, 0xfb, 0xc7, 0x2c, 0xa6, 0xec, 0xba, 0x5a,
+ 0xad, 0x1a, 0x60, 0x4d, 0x38, 0x81, 0x07, 0x12, 0x74, 0x1c, 0x59, 0x89,
+ 0x0b, 0x34, 0x77, 0xa2, 0xc0, 0x6a, 0x59, 0xbe, 0x99, 0x25, 0xca, 0x59,
+ 0x34, 0x70, 0x9f, 0xc6, 0x99, 0xd3, 0x44, 0x21, 0x98, 0x23, 0xe8, 0x1a,
+ 0xce, 0x0b, 0x6e, 0xee, 0x22, 0xfd, 0x56, 0xf4, 0x27, 0x6d, 0x0a, 0x01,
+ 0xb5, 0x52, 0xb2, 0xf7, 0xb4, 0x8d, 0x4d, 0x03, 0x6d, 0x5a, 0xd2, 0x2e,
+ 0xc9, 0xa9, 0xc6, 0x06, 0x42, 0xc5, 0x1b, 0xde, 0x50, 0x90, 0x7a, 0x77,
+ 0x53, 0xa2, 0x27, 0x67, 0x54, 0xd2, 0xd4, 0x33, 0x0c, 0x1a, 0xb7, 0x65,
+ 0xd0, 0x76, 0xb7, 0x54, 0xb0, 0x45, 0x1a, 0xb5, 0xb4, 0xad, 0xc2, 0x05,
+ 0xb5, 0x41, 0x5a, 0xd3, 0x64, 0xb6, 0x60, 0xf4, 0x36, 0x69, 0x71, 0xbd,
+ 0x71, 0x79, 0x6b, 0x1a, 0xae, 0xe8, 0xa5, 0x48, 0x51, 0x0f, 0x2d, 0x9d,
+ 0x74, 0xdd, 0xbb, 0xa0, 0x3f, 0x69, 0xff, 0x82, 0x13, 0x32, 0xfd, 0x60,
+ 0x89, 0x39, 0xc4, 0x3b, 0x81, 0x0c, 0x46, 0xdd, 0x56, 0x7b, 0x4c, 0xd4,
+ 0x3a, 0x83, 0x92, 0x81, 0x1b, 0xe7, 0xcc, 0xc1, 0xfc, 0xbe, 0x62, 0xac,
+ 0x7b, 0x5d, 0x61, 0x22, 0x86, 0x00, 0x8e, 0x25, 0x9f, 0xf4, 0x13, 0x00,
+ 0x57, 0x30, 0xed, 0xbc, 0x9d, 0xde, 0x8e, 0x87, 0xff, 0x51, 0x43, 0x50,
+ 0xcf, 0x5e, 0x48, 0x14, 0x2e, 0x19, 0xe9, 0xdd, 0xb9, 0xd2, 0xde, 0xcd,
+ 0xcb, 0xf1, 0x63, 0x6c, 0xb7, 0xc9, 0xd7, 0x2d, 0x3c, 0xe0, 0x53, 0x37,
+ 0x98, 0x61, 0x7d, 0x70, 0x4a, 0xb4, 0x34, 0x44, 0x39, 0xc3, 0x71, 0xd5,
+ 0xaf, 0xff, 0x8f, 0x73, 0xbf, 0xfa, 0x63, 0x4d, 0xbe, 0x2a, 0x8a, 0x4b,
+ 0x56, 0x3d, 0x4c, 0x4b, 0xe4, 0x7c, 0x2c, 0x66, 0xfc, 0x01, 0xab, 0x41,
+ 0xe6, 0x41, 0x1e, 0x5b, 0xf4, 0x5b, 0x33, 0x08, 0xea, 0xc4, 0x99, 0xcb,
+ 0x23, 0xe1, 0x36, 0xe1, 0x60, 0x6e, 0xad, 0xfa, 0xe1, 0xfc, 0x8d, 0x60,
+ 0x58, 0xc4, 0x23, 0x9c, 0x63, 0xe2, 0xcf, 0x21, 0xcf, 0x07, 0x44, 0x19,
+ 0x1f, 0x9b, 0x40, 0x3b, 0xc1, 0xe6, 0x46, 0x86, 0xd9, 0x53, 0xc2, 0x27,
+ 0x40, 0xa6, 0xb4, 0xed, 0xce, 0x98, 0x66, 0xe1, 0xc0, 0x01, 0x36, 0x63,
+ 0x07, 0x5b, 0x61, 0x79, 0xa5, 0x8c, 0x5f, 0xd7, 0x9f, 0x9b, 0xcd, 0x40,
+ 0xdc, 0x29, 0x69, 0x13, 0xb3, 0xa3, 0x80, 0x3f, 0xdb, 0x62, 0xbf, 0x90,
+ 0x7e, 0xf6, 0x94, 0x89, 0xea, 0x08, 0xd4, 0x70, 0xe6, 0x27, 0x1c, 0x5c,
+ 0xb4, 0x82, 0x8c, 0xff, 0x26, 0xcb, 0xad, 0x98, 0xe4, 0xef, 0x99, 0x5a,
+ 0xfb, 0x05, 0xc6, 0xe7, 0x91, 0xe2, 0xa4, 0x7c, 0x5b, 0x0b, 0xd9, 0x60,
+ 0x17, 0xab, 0x0e, 0x99, 0x5d, 0x7f, 0x02, 0x42, 0x2b, 0x32, 0x41, 0xfc,
+ 0xd1, 0x27, 0x63, 0xf4, 0x69, 0x54, 0x4b, 0x4f, 0xcc, 0x09, 0x20, 0xd3,
+ 0x4a, 0x8f, 0xa1, 0xa4, 0x66, 0x3f, 0x57, 0x44, 0x94, 0xff, 0xb3, 0x97,
+ 0x58, 0xd2, 0xc1, 0xa2, 0x67, 0x1f, 0x87, 0x1c, 0x6a, 0xe9, 0x10, 0x15,
+ 0xe2, 0xa4, 0xac, 0xf6, 0xc7, 0x87, 0xdc, 0x81, 0x02, 0x89, 0xa0, 0x57,
+ 0x21, 0x9c, 0xfc, 0xa0, 0x22, 0x94, 0x9f, 0x56, 0x81, 0xaa, 0xea, 0x8e,
+ 0x71, 0x25, 0xc6, 0x57, 0xb0, 0xc3, 0x9a, 0x08, 0xfc, 0x1d, 0xa9, 0xc0,
+ 0x68, 0xb1, 0x29, 0x05, 0x97, 0xe8, 0x6d, 0xc4, 0x5e, 0x1e, 0x64, 0x16,
+ 0xc1, 0x71, 0x4b, 0x23, 0xb9, 0x5b, 0x85, 0xd1, 0xc3, 0x18, 0xa2, 0x7e,
+ 0xcb, 0x0c, 0x6c, 0x2a, 0x43, 0x51, 0x58, 0x63, 0xbb, 0xaf, 0xdc, 0x9d,
+ 0xfc, 0x6e, 0x76, 0xa0, 0x46, 0x0c, 0x31, 0xca, 0x3e, 0xdb, 0x91, 0x0e,
+ 0x6d, 0xd0, 0x30, 0x64, 0x40, 0x6c, 0x82, 0x8f, 0xc9, 0x92, 0x41, 0xf5,
+ 0x74, 0x37, 0x43, 0xd7, 0x41, 0x55, 0xdd, 0xc2, 0xb2, 0xb9, 0x09, 0x6e,
+ 0xb3, 0xc0, 0x03, 0x11, 0x95, 0x2f, 0xbc, 0x0e, 0xef, 0x61, 0x6b, 0xb3,
+ 0xf9, 0xd4, 0xa6, 0xea, 0xf4, 0x1b, 0xf5, 0x04, 0x69, 0x84, 0xd4, 0x5b,
+ 0x06, 0xc5, 0x83, 0x71, 0xfd, 0x52, 0x56, 0x24, 0x0d, 0x4b, 0x0c, 0xd2,
+ 0x29, 0xdf, 0xda, 0xb6, 0x2d, 0xc1, 0x5e, 0x1c, 0x42, 0x37, 0xb5, 0x68,
+ 0x89, 0x4e, 0x67, 0x44, 0x85, 0x38, 0xab, 0xf3, 0x4d, 0x02, 0x17, 0xe8,
+ 0xb2, 0x0e, 0xf7, 0x1e, 0x8a, 0x5f, 0x42, 0x16, 0x97, 0xea, 0xa6, 0x2d,
+ 0x88, 0x84, 0xf3, 0x1e, 0x00, 0xb7, 0xf5, 0x0c, 0xbb, 0xcd, 0x09, 0xc5,
+ 0xcf, 0xdb, 0x1a, 0x5e, 0x55, 0xdb, 0x46, 0x42, 0xa3, 0x63, 0xac, 0x3f,
+ 0x06, 0x53, 0xba, 0xcf, 0xbb, 0x31, 0x54, 0x77, 0x99, 0xe1, 0x48, 0xd3,
+ 0xb4, 0x29, 0x1e, 0xad, 0x0d, 0x6f, 0xac, 0xd4, 0x4c, 0x1f, 0xb0, 0x92,
+ 0xe5, 0x35, 0x48, 0x69, 0x0a, 0xbe, 0x62, 0xe9, 0xb4, 0xb8, 0x68, 0x20,
+ 0x3c, 0x6d, 0x69, 0xbf, 0x30, 0x21, 0xc6, 0xe4, 0x9d, 0x08, 0x8b, 0xd0,
+ 0xf1, 0xb8, 0x84, 0x4e, 0xd5, 0x4c, 0xf1, 0xbb, 0x3a, 0x58, 0x11, 0x06,
+ 0xcd, 0x56, 0x4b, 0x5f, 0xda, 0xb9, 0x34, 0x91, 0xc2, 0x3b, 0xfc, 0xd1,
+ 0x1e, 0x96, 0x24, 0xd2, 0xb6, 0x76, 0xe2, 0xe4, 0xae, 0x9e, 0x30, 0x40,
+ 0x3e, 0xea, 0xd8, 0x73, 0x90, 0x31, 0xe1, 0xdd, 0xed, 0xcf, 0x85, 0xcf,
+ 0x2b, 0x3e, 0x64, 0x7f, 0x4d, 0x8a, 0x1d, 0x0e, 0x2c, 0xd5, 0xe2, 0x43,
+ 0xae, 0x76, 0x3f, 0x21, 0x53, 0x3c, 0xd1, 0x1b, 0xdb, 0x2a, 0x9f, 0x27,
+ 0xd1, 0x28, 0x1b, 0x46, 0x96, 0x07, 0xe5, 0xdc, 0xd7, 0xca, 0xf1, 0x93,
+ 0x1a, 0x24, 0xcf, 0x4d, 0xef, 0xaf, 0x40, 0x4e, 0x2d, 0x93, 0xb6, 0x07,
+ 0xe4, 0xef, 0xf5, 0xc1, 0xca, 0x98, 0x7c, 0xa8, 0x10, 0x20, 0x06, 0x88,
+ 0x06, 0xb2, 0x52, 0xd9, 0x86, 0xae, 0x9d, 0x1a, 0x62, 0x06, 0x53, 0xed,
+ 0x31, 0x53, 0xc9, 0xce, 0x67, 0x02, 0x48, 0x4d, 0x23, 0x37, 0x22, 0x7d,
+ 0x14, 0xe0, 0x9a, 0xfd, 0xe4, 0x02, 0xfe, 0x6b, 0x9f, 0xa9, 0x46, 0x59,
+ 0x54, 0x6c, 0x77, 0x9d, 0xee, 0x72, 0xd2, 0x18, 0x7f, 0x1c, 0x1d, 0xcc,
+ 0x30, 0x87, 0xcd, 0x49, 0xc4, 0x42, 0x0d, 0x83, 0x10, 0x1c, 0x10, 0xa3,
+ 0x9e, 0x4e, 0xfd, 0xfd, 0xd4, 0x9f, 0x5e, 0xca, 0xad, 0x34, 0x6c, 0x32,
+ 0x63, 0x6b, 0xf0, 0xe5, 0x0c, 0x46, 0x39, 0x65, 0x83, 0x66, 0x07, 0x3a,
+ 0xfe, 0x92, 0xae, 0x72, 0x47, 0x5e, 0x30, 0xee, 0x07, 0xc6, 0xd5, 0x20,
+ 0x4a, 0xb6, 0xa7, 0xb8, 0x79, 0xff, 0xbf, 0x8e, 0xf9, 0x57, 0xa8, 0x25,
+ 0x9b, 0x05, 0xe3, 0x48, 0xd3, 0x39, 0xd2, 0x6c, 0x70, 0xdc, 0x8a, 0xa3,
+ 0x49, 0x94, 0x43, 0xec, 0xea, 0xac, 0x67, 0xf5, 0xb0, 0x31, 0x06, 0x2a,
+ 0xcb, 0xb4, 0xb2, 0x5a, 0x59, 0x7d, 0xd3, 0x7c, 0x6a, 0xd2, 0x6b, 0xf3,
+ 0xb6, 0x79, 0x58, 0xc5, 0x0e, 0x0f, 0x34, 0x71, 0x43, 0x3c, 0xec, 0x16,
+ 0xd1, 0xc3, 0x0c, 0xf7, 0xb4, 0x59, 0xf7, 0x14, 0x25, 0x7c, 0x13, 0x5c,
+ 0x10, 0xb8, 0x63, 0x50, 0x6e, 0xf4, 0x9a, 0x6e, 0x2f, 0x16, 0x6a, 0x06,
+ 0x32, 0x4c, 0x07, 0x9a, 0x90, 0xf9, 0x63, 0x82, 0xea, 0xec, 0x92, 0xfd,
+ 0x92, 0x59, 0xee, 0xc1, 0xeb, 0x0a, 0xe8, 0x20, 0xc7, 0x0a, 0x71, 0x12,
+ 0xde, 0xfd, 0x89, 0xe2, 0xbe, 0xd0, 0x53, 0x03, 0x59, 0xaa, 0x08, 0xa9,
+ 0x26, 0x6b, 0x76, 0x99, 0xb1, 0x11, 0xb9, 0xf5, 0xd1, 0xbc, 0xd0, 0x01,
+ 0x28, 0x67, 0xf6, 0x55, 0x22, 0xdd, 0x9c, 0x2d, 0x3e, 0x51, 0xd3, 0xe8,
+ 0x57, 0xbd, 0x0d, 0x76, 0x4b, 0x96, 0x31, 0x99, 0xf0, 0xd6, 0x62, 0x8e,
+ 0xc3, 0xdc, 0xdb, 0xba, 0x2b, 0x4c, 0xa4, 0xe1, 0xc3, 0x9d, 0xbc, 0xe9,
+ 0xe5, 0x2b, 0x59, 0x64, 0xc9, 0xb9, 0x61, 0x26, 0x0c, 0x5d, 0xa0, 0x6a,
+ 0xd1, 0x3a, 0x17, 0x3d, 0x7f, 0x50, 0x63, 0xd2, 0x61, 0x1b, 0xb9, 0x2e,
+ 0x3d, 0x4e, 0x12, 0x6c, 0x6b, 0x5e, 0x94, 0x9d, 0x8e, 0x83, 0x2e, 0xe9,
+ 0xe6, 0x12, 0x2f, 0x49, 0xab, 0x15, 0xa2, 0x46, 0x62, 0x70, 0x72, 0xf5,
+ 0xdb, 0xc6, 0x2d, 0x71, 0xcc, 0x48, 0xae, 0x04, 0xe3, 0x5e, 0xd6, 0xa7,
+ 0x31, 0xd0, 0x03, 0xc8, 0x61, 0x8d, 0x10, 0x0d, 0xaf, 0x65, 0xea, 0x88,
+ 0xf3, 0x51, 0x7f, 0x4c, 0x99, 0xa2, 0x38, 0xeb, 0x8f, 0x06, 0x88, 0x20,
+ 0xfe, 0xf1, 0x4c, 0xd2, 0xe3, 0xb3, 0x44, 0x2d, 0x6f, 0x93, 0x29, 0x13,
+ 0xec, 0x16, 0x8b, 0x86, 0x74, 0xb7, 0x27, 0xe2, 0xf0, 0x30, 0x4d, 0x52,
+ 0x26, 0x5f, 0x1b, 0x9f, 0x52, 0x52, 0x6b, 0xdb, 0xb5, 0x74, 0xe6, 0x5f,
+ 0xa7, 0x4f, 0x76, 0xcb, 0xc5, 0x1e, 0xa8, 0x2a, 0x94, 0xfd, 0x06, 0x74,
+ 0xfb, 0xd6, 0x2a, 0x20, 0xfd, 0xa0, 0x45, 0xc6, 0x65, 0xd5, 0xa6, 0x83,
+ 0xa3, 0x20, 0x7e, 0x4d, 0x7e, 0xac, 0x90, 0x6b, 0x74, 0x5a, 0xd8, 0x41,
+ 0x86, 0x74, 0x53, 0x8d, 0x53, 0x5a, 0x88, 0x53, 0x2d, 0xa6, 0x47, 0xff,
+ 0x07, 0x83, 0x83, 0xfb, 0x36, 0x24, 0x3d, 0x88, 0xe3, 0x28, 0xa3, 0x97,
+ 0x43, 0x9b, 0x1b, 0x26, 0x16, 0xb4, 0xe9, 0xec, 0xa6, 0x6f, 0xc9, 0xef,
+ 0x22, 0x36, 0x98, 0x83, 0x71, 0xed, 0x5e, 0xf5, 0xef, 0xe2, 0xf9, 0x85,
+ 0xf3, 0x08, 0xe2, 0xda, 0x44, 0x76, 0x03, 0x2b, 0x87, 0x9b, 0xd2, 0x74,
+ 0x61, 0xb6, 0x65, 0x01, 0x6b, 0x20, 0x79, 0xde, 0xd4, 0x1c, 0x3c, 0x7e,
+ 0x75, 0xb4, 0x89, 0x1a, 0xf2, 0xa3, 0x0d, 0x6d, 0x4e, 0x6b, 0x04, 0xf6,
+ 0x98, 0xa2, 0x57, 0x8e, 0x22, 0xf5, 0xad, 0xd8, 0x60, 0xb1, 0x83, 0xdd,
+ 0x30, 0xb1, 0x82, 0xa2, 0x48, 0xa3, 0x2f, 0x19, 0x3d, 0x36, 0x45, 0xfe,
+ 0xb2, 0x6e, 0xde, 0xbc, 0x7f, 0x71, 0x55, 0x0a, 0x4d, 0x75, 0xad, 0x5e,
+ 0x4e, 0xcc, 0x8e, 0x62, 0x43, 0x3e, 0xbb, 0x08, 0xb4, 0x5a, 0xc8, 0xc2,
+ 0xd8, 0x4d, 0x2b, 0x51, 0x62, 0x0f, 0xac, 0x03, 0xcd, 0x6e, 0x54, 0x28,
+ 0xb0, 0x02, 0x51, 0x31, 0x99, 0xef, 0xc5, 0x09, 0x1c, 0x6f, 0xc2, 0x24,
+ 0xc6, 0x13, 0x5d, 0xd6, 0x01, 0xc2, 0x70, 0xc2, 0x5a, 0xfe, 0x95, 0x96,
+ 0x20, 0x98, 0xe8, 0x6a, 0xb8, 0x98, 0x32, 0x57, 0xe3, 0xb6, 0xdc, 0x4a,
+ 0x75, 0x61, 0xbd, 0x98, 0xdb, 0x0b, 0xf8, 0xbb, 0x52, 0xa5, 0x4a, 0x93,
+ 0x21, 0xc2, 0x43, 0xc2, 0x6f, 0x8f, 0x36, 0xf7, 0x64, 0x92, 0x25, 0x9f,
+ 0xae, 0x47, 0xc4, 0x48, 0xa8, 0xae, 0xa0, 0x83, 0x60, 0x9e, 0x80, 0xc5,
+ 0x39, 0x84, 0x33, 0x70, 0xb9, 0x51, 0x76, 0x1f, 0xb4, 0xb7, 0x44, 0xcc,
+ 0x19, 0xf3, 0xa0, 0xa7, 0xa6, 0xe6, 0x60, 0x1d, 0x18, 0xd7, 0x41, 0x55,
+ 0xa7, 0x68, 0xbc, 0x5e, 0x96, 0x7a, 0x8a, 0x96, 0x09, 0xed, 0x74, 0x71,
+ 0xed, 0x8c, 0x64, 0x67, 0x82, 0x6a, 0x50, 0x59, 0x95, 0x3e, 0xb5, 0x98,
+ 0x7e, 0xa1, 0xa8, 0x0b, 0x26, 0x33, 0xdb, 0x1e, 0x88, 0x9b, 0x7c, 0x26,
+ 0xd0, 0x08, 0x18, 0xd4, 0xcd, 0x88, 0xd4, 0x89, 0x20, 0xb2, 0x0c, 0xa5,
+ 0x33, 0xd2, 0x2d, 0x9e, 0xf0, 0xe4, 0x41, 0x81, 0xbb, 0x76, 0x10, 0xc1,
+ 0xda, 0xf2, 0x04, 0xb3, 0xf9, 0x51, 0x03, 0x45, 0x25, 0xe8, 0x36, 0x46,
+ 0x5c, 0x72, 0xc6, 0xf2, 0x77, 0x22, 0x8e, 0x81, 0x77, 0xcd, 0xe2, 0x12,
+ 0x6f, 0x23, 0x2a, 0xbc, 0x5f, 0xd8, 0x66, 0xa7, 0x14, 0xe0, 0xf9, 0x06,
+ 0x3f, 0x29, 0xf9, 0x86, 0x99, 0x00, 0xb2, 0x72, 0x82, 0x49, 0x67, 0x18,
+ 0x2d, 0xde, 0x12, 0x1f, 0xde, 0x96, 0xdf, 0xa1, 0x15, 0x76, 0x93, 0x91,
+ 0x5b, 0x67, 0xb6, 0x8a, 0x6b, 0xf3, 0x2a, 0x6b, 0xb5, 0x29, 0x7a, 0x10,
+ 0x81, 0x83, 0x8b, 0xa8, 0x3e, 0xb4, 0x65, 0x08, 0xa6, 0x81, 0x68, 0xfe,
+ 0xbe, 0x25, 0x2a, 0xe5, 0x45, 0x86, 0x13, 0xa4, 0xd9, 0xcc, 0x29, 0x57,
+ 0x1e, 0xed, 0x7e, 0xda, 0x4e, 0xf5, 0x93, 0x4f, 0x22, 0x72, 0x57, 0x2a,
+ 0x9f, 0x27, 0xd9, 0xa8, 0x7c, 0x68, 0xe9, 0x1b, 0x06, 0xf8, 0x8f, 0x55,
+ 0x4a, 0x8b, 0x8c, 0x2c, 0x9a, 0x57, 0xd7, 0xd7, 0x36, 0xe7, 0x59, 0x37,
+ 0x70, 0xf4, 0x98, 0xc9, 0x05, 0xac, 0x3f, 0x82, 0x8e, 0xb0, 0x7f, 0x81,
+ 0xe4, 0xcf, 0x0e, 0x65, 0x6c, 0x9f, 0xa5, 0xa8, 0x06, 0x91, 0x94, 0xd6,
+ 0x6f, 0xc6, 0x64, 0x42, 0x9e, 0x98, 0xc0, 0xb2, 0x1e, 0x33, 0x1a, 0x17,
+ 0xb2, 0xea, 0x75, 0xc5, 0x55, 0x37, 0x39, 0xf9, 0xc9, 0xa9, 0x31, 0xe2,
+ 0x2e, 0xec, 0x02, 0x22, 0x13, 0x88, 0xe2, 0x92, 0x8a, 0x4c, 0x1e, 0xce,
+ 0xeb, 0x07, 0xbd, 0x45, 0x9c, 0x6c, 0x59, 0x67, 0xa5, 0xe2, 0xa4, 0xab,
+ 0x23, 0x33, 0xa9, 0x25, 0x2d, 0x3f, 0xe9, 0x36, 0x4c, 0xf0, 0xc3, 0x22,
+ 0x25, 0x1b, 0xb4, 0xd9, 0x57, 0x14, 0xd1, 0x66, 0x86, 0x9e, 0x7c, 0x47,
+ 0xad, 0xbc, 0x6f, 0x82, 0xdd, 0x01, 0x14, 0x77, 0xaf, 0xa7, 0x8f, 0xad,
+ 0xef, 0x44, 0xd8, 0xe3, 0xb9, 0xc4, 0x9c, 0x02, 0x40, 0x24, 0xe8, 0x86,
+ 0xd2, 0x62, 0xc2, 0xd7, 0xb3, 0x65, 0x29, 0x70, 0xaa, 0xb7, 0x15, 0xe9,
+ 0x22, 0x5a, 0xf0, 0xc5, 0x57, 0xb3, 0x1d, 0xaf, 0x31, 0x89, 0x61, 0x4d,
+ 0x20, 0x7d, 0xee, 0xcc, 0xb5, 0x60, 0x2e, 0xab, 0x6c, 0x02, 0x79, 0xbf,
+ 0x88, 0x98, 0x78, 0x7e, 0x27, 0x0e, 0x91, 0x3a, 0xab, 0xf8, 0x81, 0xe9,
+ 0x82, 0x96, 0x86, 0x79, 0x51, 0x3b, 0xda, 0x24, 0x90, 0x69, 0xa5, 0x29,
+ 0x08, 0x78, 0xfe, 0x7d, 0xf1, 0xde, 0x75, 0x18, 0x8f, 0x8c, 0x1b, 0xf1,
+ 0xac, 0xe6, 0xce, 0x48, 0x8a, 0x4a, 0xd4, 0xc1, 0xdc, 0x5f, 0xf1, 0x82,
+ 0xd2, 0xa6, 0x24, 0x47, 0x26, 0x44, 0x39, 0x43, 0x91, 0xe0, 0x58, 0x51,
+ 0xdc, 0xb3, 0xd2, 0xdc, 0x7e, 0x9b, 0x06, 0x44, 0xa0, 0x50, 0x5c, 0x35,
+ 0x65, 0x39, 0xff, 0x29, 0x98, 0x48, 0xf0, 0x58, 0xb4, 0x91, 0xf1, 0xd8,
+ 0x5c, 0xdf, 0x4a, 0x55, 0x00, 0x5d, 0x2c, 0xc0, 0x91, 0xa2, 0x82, 0x67,
+ 0xa1, 0x06, 0xed, 0x67, 0x5e, 0xaa, 0xb2, 0xfa, 0x0a, 0x1f, 0x3e, 0xde,
+ 0xa1, 0xe9, 0x2d, 0xfb, 0xe8, 0x98, 0x2a, 0x8a, 0x32, 0x67, 0x4b, 0xd3,
+ 0xb0, 0xd9, 0x89, 0xba, 0x22, 0x73, 0xa3, 0x5f, 0x07, 0x41, 0xae, 0x4a,
+ 0x42, 0xe3, 0xfd, 0x05, 0x60, 0xff, 0x0f, 0xfe, 0xd9, 0xcf, 0xd6, 0x9b,
+ 0xa8, 0x39, 0x3c, 0x10, 0xab, 0x01, 0x46, 0xfb, 0xa5, 0x2a, 0x29, 0x3a,
+ 0xca, 0x5f, 0x39, 0x17, 0xff, 0x5e, 0x46, 0xd9, 0x4a, 0x5d, 0x3d, 0x14,
+ 0x25, 0xc8, 0x97, 0x9f, 0x3d, 0x53, 0x53, 0x2c, 0x97, 0x9a, 0x66, 0x35,
+ 0x8d, 0xef, 0xac, 0xb6, 0xee, 0x4c, 0xf1, 0xa8, 0xe4, 0x2d, 0xcc, 0xd2,
+ 0x02, 0x21, 0xc4, 0x7c, 0x23, 0x45, 0xe9, 0x15, 0xdf, 0x18, 0xaf, 0x9a,
+ 0xa7, 0xb6, 0xe9, 0x39, 0x9b, 0xaf, 0x0e, 0xd9, 0xaa, 0x7b, 0x9e, 0x0c,
+ 0x7e, 0x39, 0x69, 0xbb, 0x7a, 0x73, 0xbe, 0x36, 0x0c, 0xa7, 0x58, 0x75,
+ 0xfa, 0xd4, 0x21, 0x6a, 0x1c, 0x25, 0xc2, 0x25, 0x67, 0xe9, 0xff, 0xc3,
+ 0x84, 0x06, 0x28, 0x74, 0x64, 0x21, 0x6f, 0xc4, 0x44, 0xc6, 0x58, 0xac,
+ 0xf9, 0xf8, 0x57, 0xac, 0xf7, 0x0b, 0x58, 0x51, 0xdc, 0xeb, 0x52, 0xed,
+ 0x27, 0x36, 0x53, 0x4f, 0x1d, 0x47, 0x2c, 0xaa, 0x2c, 0x93, 0xb2, 0xcb,
+ 0x34, 0x88, 0x83, 0xf0, 0x2a, 0x4e, 0x03, 0x8a, 0x6d, 0x9a, 0xda, 0x60,
+ 0xbf, 0x37, 0x24, 0x32, 0x7e, 0x23, 0x57, 0xaf, 0x90, 0x48, 0xf1, 0x68,
+ 0xb6, 0xba, 0xb6, 0x49, 0x0a, 0x58, 0xb1, 0x8a, 0x4e, 0xd5, 0x99, 0x84,
+ 0x45, 0xe9, 0xe3, 0x43, 0xbf, 0x73, 0xd2, 0x19, 0x55, 0x44, 0x4c, 0x11,
+ 0xea, 0xed, 0x50, 0x33, 0x53, 0xbd, 0x35, 0xeb, 0x4d, 0xc6, 0x17, 0x6a,
+ 0x23, 0x2d, 0x5d, 0x63, 0x1e, 0xd2, 0x4d, 0x83, 0x0a, 0x96, 0x0a, 0x16,
+ 0x24, 0x68, 0xeb, 0x54, 0xa6, 0x56, 0xb0, 0xdf, 0x4d, 0xc2, 0x9f, 0x88,
+ 0x4a, 0x8f, 0xdc, 0x69, 0xe0, 0xfc, 0xde, 0x2a, 0x4d, 0xd3, 0x70, 0x00,
+ 0xec, 0x4d, 0xc5, 0xe2, 0x90, 0xeb, 0x38, 0xbd, 0x4b, 0xda, 0x0b, 0x41,
+ 0x5f, 0xa7, 0x8e, 0xaa, 0x30, 0x93, 0xd0, 0x49, 0x87, 0xb3, 0x13, 0xaa,
+ 0x51, 0x88, 0x55, 0x70, 0x1e, 0x02, 0x90, 0x10, 0xd9, 0xa5, 0x30, 0xa3,
+ 0xf9, 0xd0, 0x1a, 0x65, 0x18, 0x87, 0xb2, 0x60, 0xab, 0x00, 0xda, 0x65,
+ 0x8e, 0x7b, 0x07, 0x9d, 0x36, 0x1e, 0x3e, 0x19, 0xe9, 0x79, 0x8e, 0x58,
+ 0xf9, 0x19, 0xe2, 0xbc, 0x46, 0x3f, 0x0a, 0x55, 0x56, 0x23, 0x9e, 0xeb,
+ 0x80, 0x8e, 0x57, 0x01, 0x3e, 0x7e, 0xaf, 0x53, 0x4d, 0x0a, 0x4f, 0xde,
+ 0xf3, 0x10, 0x2b, 0xce, 0x9e, 0xc4, 0x78, 0x60, 0x40, 0x87, 0xd8, 0x1b,
+ 0x98, 0xc9, 0xc7, 0xd6, 0x0f, 0x92, 0xba, 0x0c, 0x5a, 0x13, 0xf5, 0x00,
+ 0x88, 0x28, 0xe6, 0x3e, 0x7f, 0x1b, 0x1b, 0x06, 0xa8, 0xcc, 0x53, 0xb7,
+ 0x47, 0xaf, 0x29, 0x0e, 0xea, 0x38, 0x87, 0xc3, 0x84, 0xe0, 0xc6, 0xa2,
+ 0x4a, 0x69, 0x5d, 0x42, 0xe0, 0x21, 0x7b, 0x66, 0xa6, 0xdf, 0x46, 0x8a,
+ 0x15, 0x04, 0xcf, 0x12, 0x0b, 0xcc, 0xc9, 0x2d, 0xae, 0x68, 0xdc, 0x40,
+ 0xb1, 0x9c, 0x59, 0x7f, 0x20, 0xb8, 0x2a, 0xb0, 0x5a, 0x81, 0x00, 0x69,
+ 0x2e, 0x0f, 0x77, 0xe7, 0xce, 0x64, 0x05, 0x14, 0x0f, 0xa1, 0xe1, 0xdc,
+ 0xb7, 0x42, 0x69, 0x2a, 0xd6, 0xf0, 0xba, 0xc6, 0x67, 0x2c, 0xbf, 0xce,
+ 0x0f, 0x41, 0x9b, 0x6f, 0xb8, 0xf8, 0x13, 0x72, 0x7e, 0x4a, 0xf9, 0x13,
+ 0xbb, 0xce, 0x4a, 0x14, 0x2c, 0xbd, 0xdf, 0x55, 0x06, 0x6a, 0x7e, 0x18,
+ 0x03, 0x39, 0xea, 0x78, 0x3e, 0x56, 0x28, 0x05, 0x41, 0xbe, 0x06, 0xd7,
+ 0xf0, 0xe0, 0x33, 0x21, 0x57, 0xfe, 0x9b, 0xd2, 0xb8, 0xe7, 0x29, 0x4c,
+ 0xb7, 0x72, 0xf1, 0xd3, 0x45, 0x91, 0x91, 0xa5, 0x9b, 0x97, 0x6c, 0x44,
+ 0xcc, 0xbd, 0xc0, 0x40, 0xee, 0x9c, 0x81, 0xc8, 0xe5, 0x97, 0xe5, 0x42,
+ 0x68, 0xed, 0x6b, 0x5c, 0xab, 0x8e, 0x39, 0x08, 0x76, 0xf6, 0xdd, 0x2f,
+ 0xbe, 0x7a, 0xd0, 0xfb, 0x6a, 0x27, 0x9f, 0xdc, 0xe7, 0x88, 0x3a, 0x33,
+ 0x17, 0x42, 0x10, 0xe8, 0xf6, 0x75, 0x37, 0x2c, 0x31, 0x37, 0x5a, 0x39,
+ 0x40, 0xe1, 0x05, 0xec, 0x00, 0xa6, 0x00, 0x32, 0xa7, 0x6b, 0x3d, 0xb3,
+ 0x8c, 0x34, 0xde, 0x09, 0xda, 0x3e, 0x64, 0xc2, 0xb5, 0x6f, 0xea, 0xa3,
+ 0x3b, 0xc3, 0x7c, 0xc6, 0x13, 0xc1, 0x0b, 0x82, 0x90, 0xb2, 0x7e, 0xea,
+ 0x1f, 0xc5, 0xeb, 0x7a, 0xbc, 0x13, 0x29, 0xcc, 0xa8, 0xd2, 0x65, 0xbc,
+ 0x1c, 0x36, 0xac, 0x14, 0xc7, 0xf6, 0x20, 0x36, 0xd6, 0x41, 0xe5, 0xa3,
+ 0x29, 0x15, 0x1d, 0x48, 0xd7, 0xd9, 0x6c, 0x9d, 0x12, 0x54, 0xb3, 0x84,
+ 0x18, 0xc7, 0x9b, 0x2e, 0xb1, 0xc6, 0x37, 0xd4, 0xe7, 0x37, 0x9c, 0xa2,
+ 0xae, 0x77, 0x6b, 0x7a, 0xdc, 0x34, 0x04, 0x04, 0x03, 0x55, 0x6f, 0xf4,
+ 0xb3, 0x9f, 0x16, 0xd7, 0xa9, 0x94, 0x12, 0xcb, 0x62, 0x4d, 0x0b, 0x91,
+ 0x90, 0x69, 0xe4, 0xcb, 0xc7, 0x03, 0xf0, 0x88, 0x78, 0xf3, 0x67, 0xb0,
+ 0x3b, 0x3e, 0xfc, 0x11, 0xc1, 0x8b, 0xee, 0x30, 0x5a, 0x8e, 0x4a, 0xee,
+ 0xc8, 0x67, 0xc7, 0x45, 0x16, 0xc7, 0x3f, 0xea, 0xc9, 0xc5, 0xba, 0x7a,
+ 0xa9, 0x20, 0x87, 0xb2, 0xc1, 0x90, 0xb8, 0xca, 0x0f, 0x81, 0xab, 0x27,
+ 0xb1, 0x27, 0x88, 0xde, 0x94, 0xce, 0x47, 0x11, 0x0f, 0x32, 0x15, 0xbc,
+ 0x6e, 0xea, 0x3a, 0x47, 0x4b, 0x27, 0x49, 0xa8, 0xf0, 0xc2, 0x45, 0x51,
+ 0xac, 0x6e, 0x5e, 0x15, 0xc0, 0xce, 0xdc, 0x88, 0x7a, 0xeb, 0x27, 0xe9,
+ 0xbb, 0x67, 0x56, 0x83, 0xc9, 0xe4, 0xa4, 0x73, 0xab, 0x1f, 0x04, 0xe7,
+ 0x79, 0x2f, 0xd5, 0x8b, 0xee, 0xf2, 0x9b, 0xea, 0xd9, 0x08, 0xfa, 0xa0,
+ 0xe2, 0x81, 0xf8, 0xf3, 0x5a, 0xce, 0xf6, 0x75, 0xe4, 0x60, 0xd0, 0x51,
+ 0xb6, 0x36, 0xbb, 0xbf, 0xcf, 0xa3, 0x4c, 0x55, 0x40, 0x07, 0xa1, 0xa4,
+ 0x3a, 0xfd, 0x16, 0x01, 0xbb, 0x8b, 0xbe, 0x24, 0x2b, 0xbb, 0xd7, 0x6c,
+ 0x34, 0x37, 0xbb, 0x18, 0x7c, 0x66, 0x10, 0xb7, 0x49, 0xbe, 0xfd, 0x67,
+ 0x78, 0x61, 0x08, 0x7e, 0x58, 0x56, 0x71, 0x00, 0x85, 0x0b, 0xd2, 0xe7,
+ 0x17, 0xff, 0xfa, 0xf8, 0x97, 0xd7, 0x42, 0xf1, 0xa0, 0x72, 0xaf, 0xa2,
+ 0x98, 0x4b, 0x6b, 0x65, 0x0f, 0xbb, 0x87, 0x8a, 0x44, 0xe0, 0xd3, 0xeb,
+ 0xf6, 0xa2, 0x33, 0x0e, 0x8b, 0x13, 0x7e, 0x85, 0x5f, 0xcb, 0x0d, 0x25,
+ 0x8e, 0x51, 0xc2, 0xfc, 0xfe, 0xf4, 0x89, 0xc6, 0x4f, 0x6b, 0x45, 0x1f,
+ 0x1a, 0xb2, 0x91, 0xf6, 0x34, 0xc8, 0xdd, 0xe0, 0xd5, 0xce, 0x88, 0xf3,
+ 0x3f, 0x04, 0x1b, 0x65, 0x15, 0x43, 0xa0, 0x18, 0x22, 0x4e, 0xbc, 0xf7,
+ 0xb5, 0x53, 0xde, 0x99, 0xc9, 0x72, 0x7b, 0x3e, 0xba, 0x32, 0x78, 0x2f,
+ 0x57, 0xb7, 0x05, 0x86, 0x49, 0x84, 0x4d, 0x91, 0x3e, 0x97, 0x99, 0xee,
+ 0x73, 0x39, 0xa9, 0xf7, 0x03, 0x0e, 0x4b, 0x87, 0x04, 0xa8, 0x7a, 0xf8,
+ 0x3a, 0xe0, 0x92, 0x6e, 0x4e, 0x36, 0xef, 0x14, 0x7e, 0x13, 0x57, 0xc5,
+ 0xe0, 0x5f, 0xd8, 0x73, 0x59, 0x63, 0x66, 0xd8, 0x9b, 0x36, 0x04, 0x59,
+ 0xeb, 0x27, 0x59, 0x43, 0x0b, 0xdd, 0x2e, 0x3f, 0x22, 0x44, 0x2c, 0x8e,
+ 0x01, 0x61, 0xd6, 0x31, 0x41, 0xf6, 0x92, 0xe3, 0xd5, 0x2e, 0xf5, 0x6c,
+ 0x79, 0x3b, 0xe3, 0x34, 0xd7, 0x5e, 0xe1, 0xec, 0xc2, 0xf6, 0xe2, 0x48,
+ 0x3d, 0x72, 0x68, 0x54, 0xee, 0xbb, 0xa1, 0xe6, 0xe1, 0x63, 0x9d, 0xd1,
+ 0x28, 0x06, 0x48, 0x00, 0xbb, 0xe1, 0x02, 0x5a, 0x55, 0x13, 0xbc, 0x37,
+ 0xc5, 0xe6, 0x77, 0x58, 0x5c, 0xfa, 0x6c, 0x9f, 0xeb, 0xe4, 0xc2, 0x5b,
+ 0x27, 0x33, 0x8f, 0xea, 0x49, 0x85, 0xf4, 0x21, 0xdd, 0xe4, 0x41, 0xb5,
+ 0xf4, 0x1e, 0xd7, 0x1a, 0xd3, 0x7a, 0xd7, 0xb2, 0xf0, 0x83, 0x8e, 0x6c,
+ 0x66, 0xf9, 0x9d, 0x97, 0xe0, 0xb3, 0xf5, 0x49, 0xed, 0xac, 0x8d, 0xbe,
+ 0x23, 0xcf, 0xe2, 0x8f, 0x79, 0xd7, 0x6b, 0x97, 0x62, 0x3a, 0x20, 0x62,
+ 0x88, 0x8e, 0x54, 0x94, 0x9d, 0x68, 0xa0, 0x99, 0x3c, 0x69, 0x57, 0x53,
+ 0x35, 0xb4, 0x19, 0x27, 0xa6, 0xd9, 0xec, 0x0c, 0xb8, 0x06, 0xef, 0xfc,
+ 0xe7, 0xad, 0x3e, 0x44, 0xe5, 0x6d, 0x2c, 0x74, 0x0b, 0xcf, 0xbe, 0x01,
+ 0x00, 0xee, 0xb1, 0x8e, 0x92, 0x20, 0x27, 0xea, 0xba, 0xa8, 0xb5, 0x72,
+ 0x21, 0x40, 0x4b, 0x62, 0x75, 0x1a, 0x29, 0x8f, 0xe2, 0x61, 0x40, 0xdd,
+ 0x55, 0x9e, 0xd9, 0x66, 0x14, 0x09, 0x5b, 0x90, 0xa2, 0x37, 0x57, 0xbb,
+ 0x70, 0x41, 0xce, 0xdc, 0x9a, 0x7b, 0xb3, 0x8d, 0xcc, 0xd4, 0xc8, 0x21,
+ 0x2f, 0xe6, 0xa7, 0x7f, 0x16, 0xdb, 0xa5, 0xf9, 0x36, 0x27, 0xe9, 0xfd,
+ 0x37, 0x2a, 0xf1, 0x07, 0x68, 0x71, 0x59, 0xf6, 0xb1, 0xa2, 0xf3, 0xbc,
+ 0x45, 0xac, 0x33, 0x2d, 0x3f, 0x2f, 0xd8, 0x8d, 0x7b, 0xe9, 0x74, 0x5c,
+ 0x54, 0xda, 0x2c, 0x2a, 0x86, 0x9f, 0xf3, 0xc3, 0xab, 0x48, 0x96, 0x6b,
+ 0x07, 0xcc, 0x31, 0xaf, 0x7c, 0xae, 0x35, 0x71, 0xe7, 0xdf, 0xc7, 0xd0,
+ 0x26, 0xea, 0x84, 0x25, 0x0a, 0xe4, 0x4d, 0x64, 0x45, 0x20, 0x35, 0xf7,
+ 0x6f, 0x80, 0xb6, 0xa3, 0xc9, 0x2c, 0xa8, 0x0e, 0xcc, 0xef, 0x91, 0x82,
+ 0x80, 0x4b, 0x78, 0x28, 0xfc, 0x9b, 0xf0, 0xaf, 0x24, 0x51, 0x4c, 0xfb,
+ 0x14, 0xdb, 0x5c, 0x66, 0xc0, 0x1c, 0x06, 0xdc, 0xbc, 0x16, 0xc8, 0xc2,
+ 0xac, 0xde, 0xd8, 0x9e, 0xc1, 0xec, 0xd4, 0x8a, 0xc5, 0xf7, 0x41, 0xd2,
+ 0xc3, 0x5c, 0x5c, 0x95, 0x5f, 0x36, 0x1e, 0x48, 0x58, 0x86, 0xd4, 0xd4,
+ 0xaa, 0x97, 0xc3, 0x34, 0xd9, 0x47, 0x3e, 0x8a, 0x79, 0x21, 0x31, 0xe1,
+ 0x06, 0x45, 0x41, 0xda, 0x02, 0xec, 0x17, 0x75, 0x5e, 0xa8, 0x31, 0x26,
+ 0xac, 0x4c, 0xe7, 0x4e, 0xb4, 0x34, 0x3d, 0xe0, 0xda, 0x68, 0x0f, 0x00,
+ 0x81, 0x5e, 0xba, 0x1c, 0x54, 0xa2, 0x1a, 0x36, 0x07, 0x56, 0x33, 0x1a,
+ 0x5b, 0x7d, 0x09, 0xe4, 0xce, 0x88, 0x4d, 0xc3, 0x08, 0x0e, 0x0e, 0x57,
+ 0x86, 0xa3, 0xb3, 0xd6, 0x26, 0x5e, 0x83, 0x70, 0x4b, 0xe4, 0x4e, 0x5f,
+ 0xfc, 0x9b, 0x9c, 0xf7, 0x6b, 0x03, 0x2f, 0x63, 0x83, 0x69, 0x52, 0xe5,
+ 0x9e, 0x48, 0x06, 0x3a, 0x16, 0xac, 0x45, 0x16, 0x0b, 0xdc, 0xe1, 0x31,
+ 0x8e, 0xfa, 0x8e, 0x4c, 0x38, 0x78, 0x11, 0xb5, 0x43, 0xeb, 0x1c, 0x24,
+ 0x4b, 0x4c, 0x1f, 0x39, 0x61, 0x71, 0x8e, 0x31, 0x87, 0x75, 0x5e, 0x2e,
+ 0x4c, 0x0e, 0x21, 0x6e, 0xff, 0xd5, 0x8a, 0xec, 0x30, 0x16, 0x24, 0x1d,
+ 0x85, 0x5a, 0xb6, 0xa8, 0x3a, 0x0a, 0x4c, 0x94, 0x0d, 0x9c, 0xff, 0x24,
+ 0x48, 0x79, 0xda, 0xc1, 0x92, 0x20, 0x58, 0x22, 0xe6, 0xa4, 0xd3, 0x1e,
+ 0x83, 0x05, 0x6f, 0x2c, 0xa6, 0x54, 0xb2, 0xbc, 0xf8, 0xe4, 0x17, 0x42,
+ 0xb2, 0x4c, 0xff, 0x27, 0x8b, 0x25, 0xbb, 0xd6, 0xa1, 0x04, 0x48, 0x7d,
+ 0xe5, 0x6f, 0x77, 0xf3, 0x4f, 0x06, 0x52, 0x17, 0xe8, 0x33, 0xf5, 0x72,
+ 0xcd, 0xab, 0xa7, 0x41, 0xcb, 0x0b, 0x6c, 0xe6, 0xe6, 0xa0, 0x18, 0x51,
+ 0x8e, 0xc9, 0xe1, 0xf3, 0x17, 0x20, 0x35, 0x6d, 0xd8, 0xe2, 0x93, 0x4c,
+ 0xd5, 0x07, 0x60, 0x1c, 0x02, 0x7c, 0x13, 0xe8, 0xcb, 0x1f, 0x17, 0xf2,
+ 0xcd, 0xdb, 0x7c, 0xc2, 0xf9, 0xa4, 0xc6, 0x4c, 0x17, 0xe3, 0x1d, 0x6f,
+ 0x24, 0x94, 0xcb, 0x6d, 0xe5, 0x5f, 0xef, 0x62, 0xa5, 0xca, 0x17, 0xd4,
+ 0x16, 0x95, 0x73, 0x78, 0x6f, 0x1b, 0x51, 0xeb, 0xc4, 0x2b, 0x94, 0x8f,
+ 0xed, 0xad, 0x97, 0x95, 0x4d, 0xd4, 0x9e, 0xc3, 0x92, 0xa4, 0xb8, 0x4c,
+ 0xe1, 0x15, 0x2a, 0x1b, 0x8c, 0xd9, 0x2f, 0x9d, 0x6d, 0x61, 0xb4, 0x24,
+ 0xf7, 0x68, 0x52, 0xb5, 0x81, 0xe2, 0xba, 0xf5, 0x0a, 0x3b, 0xdf, 0xef,
+ 0x0f, 0x6f, 0x9d, 0xda, 0x0e, 0x23, 0x94, 0xb0, 0x91, 0xe6, 0x00, 0xef,
+ 0x0c, 0xe2, 0xc7, 0x19, 0x2a, 0x0c, 0xdb, 0x12, 0xbd, 0x99, 0x69, 0xb9,
+ 0x48, 0x05, 0x06, 0xc7, 0xf0, 0x77, 0xb8, 0x72, 0x5c, 0x7d, 0x0c, 0xbd,
+ 0xc5, 0x30, 0x93, 0x8e, 0xa4, 0x62, 0x43, 0x14, 0x12, 0x57, 0x06, 0x15,
+ 0x02, 0x29, 0x52, 0x47, 0x99, 0x40, 0xfa, 0x9c, 0x13, 0x34, 0x0d, 0x54,
+ 0x37, 0x9d, 0xf2, 0xff, 0x44, 0x5c, 0x09, 0x7d, 0xfa, 0x7e, 0xf4, 0x02,
+ 0xdc, 0xb6, 0xe4, 0x59, 0xb8, 0x58, 0xd6, 0xb4, 0x73, 0x2e, 0x5a, 0x40,
+ 0xcd, 0x4f, 0xfe, 0xf7, 0x89, 0x01, 0xde, 0xe6, 0xcd, 0x77, 0x39, 0xbd,
+ 0x26, 0xe8, 0x44, 0x25, 0x6a, 0x78, 0xee, 0xcd, 0x7d, 0xb0, 0xe5, 0x2b,
+ 0x7c, 0x1a, 0xbf, 0xdc, 0x01, 0x92, 0x4a, 0xd9, 0x17, 0x80, 0xf3, 0x03,
+ 0xd5, 0xd7, 0x51, 0x65, 0xfe, 0x82, 0x4e, 0x7e, 0xfe, 0xae, 0x39, 0xfd,
+ 0x87, 0x57, 0x24, 0x16, 0x1b, 0x54, 0x01, 0x79, 0x22, 0xe9, 0x0f, 0x2c,
+ 0x2a, 0xba, 0x20, 0x5a, 0x57, 0x16, 0x36, 0x76, 0x2f, 0x47, 0x04, 0x05,
+ 0xb9, 0xd2, 0x19, 0x80, 0xb2, 0x49, 0x92, 0xe3, 0xd6, 0x0e, 0x8f, 0x03,
+ 0x49, 0x0d, 0xcf, 0xf7, 0xde, 0xe9, 0x41, 0x98, 0xe5, 0x6a, 0x21, 0x23,
+ 0xb9, 0x07, 0xfa, 0x07, 0x97, 0x69, 0x4e, 0xaf, 0x01, 0x32, 0x6e, 0xc4,
+ 0xde, 0x13, 0x1b, 0x27, 0x89, 0x1a, 0x57, 0xc1, 0x85, 0x15, 0x22, 0xac,
+ 0x78, 0xea, 0xbb, 0x8d, 0xec, 0x0c, 0xff, 0x58, 0x54, 0x00, 0xff, 0xa8,
+ 0x98, 0xd5, 0x7b, 0xcb, 0xae, 0x04, 0x70, 0xa4, 0x19, 0xee, 0x8d, 0xbc,
+ 0x38, 0xe8, 0xf1, 0xd3, 0x95, 0x2b, 0x0d, 0x5c, 0x67, 0xeb, 0x49, 0xe3,
+ 0xfc, 0x81, 0xa5, 0xfe, 0x79, 0xf6, 0x8a, 0x3e, 0xde, 0xa7, 0xca, 0x2e,
+ 0xae, 0x14, 0xe0, 0x32, 0x3e, 0x94, 0x66, 0x15, 0x02, 0xac, 0x4d, 0xe7,
+ 0xdd, 0xd4, 0x09, 0x9a, 0x16, 0x88, 0x13, 0x3e, 0xd4, 0x28, 0x60, 0xb5,
+ 0x09, 0x1a, 0x3e, 0xd5, 0x8b, 0x52, 0xad, 0x57, 0xa3, 0xa9, 0x2b, 0x34,
+ 0x04, 0xec, 0x90, 0x5c, 0x3a, 0xbd, 0x87, 0x0d, 0xbf, 0xfd, 0x2f, 0x99,
+ 0xd8, 0xc8, 0x71, 0xee, 0x69, 0x86, 0x41, 0xc1, 0x5f, 0x78, 0xc4, 0xbe,
+ 0x38, 0x5d, 0xa4, 0x49, 0x03, 0xb3, 0xef, 0xb8, 0x5f, 0x9f, 0xce, 0x4b,
+ 0x1d, 0x06, 0xd1, 0xdb, 0x29, 0x2f, 0x26, 0x43, 0x2e, 0x2f, 0xa3, 0x71,
+ 0x22, 0x80, 0xd8, 0xbe, 0x4d, 0xc3, 0xfb, 0x7b, 0x76, 0xef, 0x65, 0x0e,
+ 0x31, 0x03, 0xd2, 0x00, 0x29, 0x3e, 0x51, 0x53, 0x23, 0xa0, 0x90, 0xe4,
+ 0xef, 0x2c, 0xe6, 0x5e, 0xa6, 0xd7, 0xa9, 0x6b, 0x9a, 0xea, 0x04, 0xbd,
+ 0xfa, 0x19, 0x05, 0x1f, 0x67, 0xa5, 0xe0, 0xf0, 0xab, 0x5c, 0x2f, 0xb2,
+ 0x2f, 0xfc, 0xc9, 0x2f, 0xe8, 0x98, 0xb1, 0x04, 0xfd, 0x52, 0xce, 0xdf,
+ 0x8b, 0x7c, 0xd0, 0xe6, 0xfb, 0xad, 0xdf, 0x98, 0xa4, 0x67, 0xef, 0x9e,
+ 0xa5, 0x1a, 0x43, 0x4a, 0x91, 0x26, 0xe4, 0x71, 0x7a, 0x51, 0xb3, 0xc4,
+ 0xd6, 0x2d, 0x22, 0xef, 0xa5, 0xa5, 0xf2, 0x49, 0xaa, 0xe9, 0xfd, 0xf8,
+ 0x06, 0xfc, 0x58, 0x15, 0x2c, 0x9e, 0x79, 0x33, 0x44, 0xd7, 0x6e, 0x10,
+ 0x3c, 0xe8, 0x0d, 0xf8, 0x9f, 0xbb, 0x5f, 0x16, 0x19, 0xf4, 0xc9, 0x90,
+ 0xd0, 0x2b, 0xa6, 0x41, 0x50, 0xa8, 0x53, 0xf6, 0x36, 0x15, 0x7a, 0x82,
+ 0x86, 0x54, 0x9b, 0x77, 0x2f, 0x2d, 0xfa, 0x92, 0x0d, 0x44, 0x56, 0x52,
+ 0xd3, 0xea, 0x29, 0x3c, 0x8b, 0xae, 0xf0, 0xe2, 0x29, 0x9a, 0x53, 0xbe,
+ 0x46, 0x53, 0xaa, 0x24, 0x38, 0xe9, 0xbf, 0x85, 0x82, 0x14, 0xb0, 0x02,
+ 0xe8, 0x9f, 0xc1, 0xc1, 0x3e, 0x8b, 0xab, 0xf2, 0xce, 0xfc, 0x1f, 0x9a,
+ 0x9b, 0xd7, 0x15, 0x3f, 0x0f, 0x7c, 0x9d, 0x30, 0x8f, 0xd8, 0x5b, 0x16,
+ 0x5a, 0x56, 0x03, 0x19, 0xbb, 0x86, 0xe5, 0xca, 0x8b, 0xf9, 0x8e, 0x44,
+ 0x2e, 0x55, 0x65, 0xfd, 0xd2, 0x70, 0x14, 0x21, 0x9a, 0x69, 0x7e, 0xc0,
+ 0xab, 0x1c, 0x0d, 0x30, 0xee, 0xa6, 0xa2, 0xec, 0x36, 0x9b, 0x67, 0xc8,
+ 0x87, 0xfc, 0xa4, 0x05, 0x58, 0x3b, 0x8f, 0x6c, 0x6d, 0xfc, 0x26, 0x2c,
+ 0xb9, 0xad, 0x11, 0x47, 0x93, 0xbf, 0x27, 0x84, 0xa5, 0x30, 0x9f, 0xee,
+ 0xf9, 0x13, 0x96, 0xfe, 0x2b, 0xc4, 0x4c, 0x2d, 0x0b, 0x11, 0x15, 0xd8,
+ 0x22, 0x20, 0x3f, 0xb7, 0x47, 0xf3, 0xcc, 0x48, 0x85, 0x7f, 0x4f, 0xd7,
+ 0xe8, 0x7e, 0xb2, 0xd2, 0xe2, 0x45, 0x6e, 0xa4, 0x64, 0x0b, 0x61, 0xc3,
+ 0xc0, 0x9d, 0x56, 0x99, 0x20, 0x76, 0x3c, 0x30, 0x23, 0x97, 0x16, 0xfa,
+ 0x7f, 0xeb, 0x82, 0xc5, 0x95, 0xa3, 0x2f, 0x1b, 0x25, 0x11, 0x32, 0x7c,
+ 0xfd, 0x26, 0xdb, 0xaa, 0x8e, 0x07, 0x0b, 0x3d, 0xab, 0xbb, 0xe3, 0x4c,
+ 0xa0, 0x4e, 0x80, 0xed, 0xa7, 0x0c, 0x3a, 0x93, 0x59, 0xc2, 0x64, 0x48,
+ 0xf2, 0x57, 0xd2, 0x2e, 0xf4, 0x4b, 0x61, 0x91, 0x0a, 0x9e, 0x52, 0x7d,
+ 0x58, 0x6f, 0x8e, 0x4f, 0x8c, 0x9f, 0x68, 0x79, 0x40, 0x4d, 0x0d, 0x56,
+ 0xef, 0xd5, 0xb3, 0x75, 0xe8, 0x77, 0x1f, 0x7b, 0x9c, 0x1e, 0xbd, 0xc8,
+ 0xf1, 0xb6, 0x9b, 0xbb, 0xa6, 0x89, 0xf4, 0xb5, 0xf9, 0x09, 0x0c, 0x0c,
+ 0xe0, 0xb8, 0xc2, 0x4b, 0x6b, 0x39, 0x81, 0x3b, 0xcb, 0xa2, 0xda, 0x47,
+ 0xc8, 0x46, 0x46, 0x3e, 0x76, 0x9a, 0xa6, 0xc8, 0x80, 0xa3, 0x28, 0x8d,
+ 0x5a, 0x73, 0x79, 0x1f, 0x19, 0x2b, 0xb1, 0x0d, 0xe5, 0xff, 0x6c, 0x90,
+ 0x52, 0x28, 0xd2, 0x0f, 0x88, 0x63, 0x4c, 0xe0, 0x53, 0x20, 0x52, 0x7a,
+ 0xf3, 0xf2, 0xcf, 0x5f, 0x1f, 0xe5, 0xf5, 0xa0, 0xb4, 0x6f, 0x59, 0x8b,
+ 0xa0, 0x17, 0x07, 0x63, 0x19, 0x14, 0xde, 0x49, 0x3d, 0xec, 0xbf, 0xd0,
+ 0x91, 0xe1, 0xaa, 0x25, 0xb5, 0x6f, 0x02, 0x89, 0x12, 0x62, 0x72, 0x6e,
+ 0x0d, 0xe3, 0xdb, 0xe7, 0xfb, 0x53, 0xc7, 0x2c, 0xf5, 0x71, 0x6b, 0xee,
+ 0x38, 0x8f, 0x7a, 0xcf, 0x51, 0xb3, 0xf2, 0x88, 0x5d, 0x27, 0x0e, 0x3d,
+ 0xc8, 0x18, 0xf9, 0xf8, 0x5f, 0xe3, 0xaf, 0xd3, 0xa4, 0x6f, 0x56, 0xf7,
+ 0x95, 0xd0, 0xd6, 0xb3, 0x74, 0x32, 0x77, 0x39, 0x71, 0x58, 0x0f, 0xf9,
+ 0x18, 0xfd, 0x2c, 0x92, 0x1b, 0x88, 0x7e, 0x74, 0x74, 0x99, 0x3c, 0xf5,
+ 0x77, 0x5c, 0xd6, 0xb0, 0x84, 0x11, 0x5c, 0x39, 0xfc, 0xf2, 0x71, 0x2f,
+ 0x9b, 0x8c, 0x0e, 0x4d, 0x5c, 0xd4, 0xda, 0x97, 0x8a, 0xe5, 0x75, 0x13,
+ 0xfe, 0x7f, 0x10, 0x87, 0xad, 0x53, 0xfb, 0xaa, 0xfb, 0x0a, 0xae, 0xa5,
+ 0xc9, 0xd7, 0xbe, 0x99, 0x63, 0xaa, 0xd3, 0x34, 0xde, 0x45, 0x85, 0xcd,
+ 0xa7, 0x21, 0x6f, 0xde, 0x24, 0x5d, 0x56, 0x41, 0xb5, 0xa5, 0x68, 0x4d,
+ 0xf5, 0x77, 0x1b, 0x6c, 0x79, 0xe7, 0xd6, 0x74, 0x4f, 0x6e, 0x57, 0x13,
+ 0x5c, 0x2e, 0x7e, 0x37, 0xbf, 0xf7, 0x64, 0x44, 0x2a, 0x1a, 0x18, 0x25,
+ 0x7b, 0x36, 0x50, 0xdd, 0x5d, 0xc2, 0x6c, 0x14, 0xcc, 0xad, 0xc8, 0xe8,
+ 0xd1, 0x0b, 0x3b, 0x9c, 0xaf, 0xd4, 0x62, 0xcd, 0x38, 0xb9, 0xe4, 0xa1,
+ 0x0d, 0x61, 0x55, 0x9f, 0x39, 0x3c, 0x8d, 0x60, 0x46, 0x55, 0xbb, 0x86,
+ 0x6b, 0x20, 0xe7, 0xc3, 0x81, 0x2c, 0xbc, 0xe4, 0x54, 0xa4, 0x1f, 0xf5,
+ 0x2f, 0x0e, 0xf9, 0xda, 0x36, 0xab, 0x26, 0x60, 0xef, 0xc1, 0x79, 0xad,
+ 0x46, 0x34, 0x95, 0x39, 0x46, 0x03, 0xd6, 0x20, 0x65, 0x53, 0x37, 0xd3,
+ 0x7d, 0xde, 0xf2, 0xd3, 0x55, 0x5a, 0xdd, 0x86, 0x69, 0xa0, 0x9d, 0xb3,
+ 0xd4, 0x13, 0x8d, 0x49, 0x71, 0xa4, 0x99, 0x7a, 0x3d, 0x81, 0x19, 0xf6,
+ 0xe6, 0x0f, 0x4e, 0x72, 0xca, 0x05, 0x7b, 0x5f, 0xc1, 0xc1, 0x29, 0xa8,
+ 0xe8, 0x6b, 0x17, 0xb3, 0xf3, 0x12, 0xd4, 0xf2, 0x46, 0x78, 0xcf, 0xd2,
+ 0x69, 0xac, 0xfc, 0x91, 0x4d, 0xbc, 0x7b, 0x9c, 0x55, 0x4f, 0xa1, 0x9b,
+ 0xf4, 0x83, 0xdb, 0xee, 0x29, 0xe5, 0x7f, 0x59, 0x1d, 0xbb, 0x6b, 0x85,
+ 0x3d, 0x2d, 0x76, 0x10, 0xc7, 0x00, 0xe6, 0x1d, 0x19, 0x32, 0x81, 0xdb,
+ 0x02, 0x0e, 0x32, 0xa1, 0xd1, 0x33, 0x54, 0xae, 0xbb, 0x03, 0x89, 0xea,
+ 0x82, 0xa5, 0xd9, 0x37, 0x18, 0xed, 0xdb, 0xa5, 0x4a, 0xe4, 0x4f, 0xb4,
+ 0x3e, 0x49, 0xf1, 0x15, 0xbe, 0xe7, 0xad, 0x40, 0xf5, 0xf8, 0xba, 0xc6,
+ 0xf1, 0x2e, 0x08, 0xc4, 0xb1, 0xbe, 0x9b, 0x3f, 0x57, 0x3b, 0xd3, 0x9c,
+ 0x93, 0x00, 0x44, 0xb0, 0xee, 0x6d, 0x15, 0x44, 0x31, 0xc8, 0x6f, 0xdc,
+ 0xa1, 0xb1, 0xd2, 0x8c, 0x14, 0x69, 0x94, 0x9f, 0x42, 0xdb, 0x23, 0xe6,
+ 0x5a, 0x6b, 0x27, 0xb8, 0x1f, 0xe9, 0x7c, 0xa0, 0x57, 0x2f, 0xa9, 0x9b,
+ 0x4e, 0x94, 0x57, 0x2a, 0xe4, 0x36, 0xc0, 0xe6, 0xbc, 0x9e, 0x7f, 0xe5,
+ 0x68, 0xd3, 0xc1, 0xb0, 0x46, 0xc4, 0xf1, 0x30, 0x6c, 0x40, 0x43, 0xc2,
+ 0x09, 0x98, 0xd3, 0x74, 0xb4, 0x80, 0xa2, 0x88, 0x9b, 0xb6, 0xef, 0x73,
+ 0x6c, 0xa0, 0x2b, 0xf5, 0xb8, 0x63, 0x27, 0x54, 0xec, 0x7f, 0xe3, 0x04,
+ 0xb6, 0x88, 0xdc, 0xa4, 0xdb, 0xae, 0x52, 0x79, 0xaf, 0x1e, 0xd2, 0xe5,
+ 0x5e, 0x25, 0x1e, 0x94, 0x34, 0xb7, 0x9d, 0x67, 0x63, 0x28, 0x33, 0xbe,
+ 0xff, 0xb3, 0x9b, 0xb3, 0x8e, 0x35, 0xf6, 0x63, 0x77, 0x8d, 0xca, 0x0c,
+ 0xb6, 0x22, 0xf5, 0x8f, 0xa0, 0xd6, 0xa5, 0xc5, 0xd2, 0xaf, 0x26, 0xd1,
+ 0x81, 0x65, 0x87, 0x0c, 0x22, 0xd7, 0x13, 0x21, 0xda, 0xd5, 0x7e, 0x56,
+ 0xe9, 0xd2, 0xac, 0x13, 0xa7, 0x8a, 0x2d, 0xd7, 0x5a, 0x7f, 0x5b, 0x06,
+ 0x48, 0xad, 0xab, 0x59, 0x74, 0xc0, 0xda, 0xcc, 0x85, 0x94, 0xf9, 0xd2,
+ 0xf0, 0xf3, 0x80, 0xae, 0x3f, 0x93, 0x48, 0xc2, 0x32, 0x89, 0xe5, 0x37,
+ 0x24, 0xf5, 0xd0, 0x5c, 0x0a, 0xcc, 0x0c, 0xc5, 0x4e, 0x55, 0xad, 0xc4,
+ 0x2d, 0xa3, 0xff, 0xbb, 0x17, 0x4c, 0xfc, 0x1e, 0x7c, 0x33, 0x00, 0xba,
+ 0x75, 0x46, 0xd8, 0x25, 0x63, 0x71, 0x53, 0x0c, 0x54, 0x06, 0x81, 0x35,
+ 0xed, 0x9e, 0xb2, 0x96, 0x74, 0x6a, 0x98, 0xa4, 0x74, 0x2d, 0x87, 0x06,
+ 0x30, 0x6f, 0x38, 0x08, 0x28, 0x37, 0xe8, 0x9b, 0xac, 0xa3, 0x89, 0xa5,
+ 0xad, 0x20, 0x0f, 0x46, 0x8b, 0x9a, 0x6d, 0x0d, 0x75, 0x0c, 0xc6, 0x52,
+ 0x61, 0x12, 0xe0, 0x7d, 0xb7, 0xcf, 0xb6, 0xa9, 0x3c, 0x66, 0x47, 0xca,
+ 0x39, 0x75, 0x8c, 0xa2, 0x8b, 0x41, 0x5d, 0x38, 0x6f, 0x98, 0x1d, 0xab,
+ 0x07, 0x44, 0xd4, 0x42, 0xf7, 0xfa, 0xc2, 0x1a, 0x61, 0xf3, 0x1d, 0xa6,
+ 0xbf, 0x4c, 0xd0, 0x52, 0xc7, 0x6c, 0x6b, 0x30, 0x56, 0x7c, 0x07, 0xc1,
+ 0x13, 0x14, 0xcb, 0xfc, 0xc6, 0x76, 0x6d, 0x62, 0xff, 0xe7, 0x18, 0x14,
+ 0x45, 0x7a, 0x47, 0xe2, 0x81, 0x33, 0xfa, 0x78, 0x2c, 0xed, 0xfa, 0x1d,
+ 0x2c, 0xb1, 0x4e, 0xbf, 0xb2, 0xf1, 0x29, 0xfb, 0x18, 0x43, 0x97, 0x09,
+ 0x9f, 0x47, 0xa0, 0xb1, 0xdf, 0xe1, 0x41, 0x33, 0x79, 0x81, 0xf1, 0xf0,
+ 0x7e, 0x1b, 0xe8, 0x30, 0x15, 0xe0, 0xba, 0xe3, 0xd5, 0xe1, 0xf5, 0x2d,
+ 0xaf, 0x36, 0xb9, 0x85, 0xde, 0x73, 0xe2, 0xd6, 0x1b, 0x07, 0xe8, 0xd9,
+ 0x76, 0x0f, 0xee, 0x98, 0xbe, 0x75, 0x1b, 0xae, 0xa8, 0xf5, 0xa7, 0x34,
+ 0x18, 0x4f, 0x7a, 0x3f, 0xff, 0xf4, 0x3e, 0x0f, 0x50, 0xd9, 0xe3, 0xda,
+ 0xfe, 0x48, 0x38, 0x6e, 0x76, 0x7c, 0x9d, 0x08, 0x72, 0x38, 0x02, 0xe1,
+ 0x78, 0x8a, 0xc9, 0x42, 0x06, 0x48, 0x60, 0x88, 0xbe, 0x36, 0x90, 0xec,
+ 0xef, 0x7e, 0x50, 0x02, 0x75, 0x0a, 0x14, 0x59, 0x28, 0x03, 0xe9, 0x0a,
+ 0x7c, 0x08, 0x86, 0x48, 0x14, 0x45, 0xf2, 0xf2, 0xad, 0x14, 0xfe, 0x85,
+ 0x1c, 0xbe, 0xcb, 0x64, 0xcf, 0xd6, 0x94, 0x64, 0x8b, 0x4a, 0xb1, 0xce,
+ 0x0d, 0x17, 0x6c, 0x75, 0x91, 0xdc, 0x0d, 0x2d, 0x56, 0x11, 0x27, 0x94,
+ 0x89, 0xb5, 0xe2, 0xc2, 0x2a, 0x44, 0xb8, 0x98, 0x7b, 0x42, 0xfb, 0x77,
+ 0xf1, 0x0d, 0xb1, 0x82, 0x28, 0x02, 0x92, 0x2f, 0xd6, 0x27, 0x34, 0xd9,
+ 0xbd, 0xd1, 0x0e, 0x1d, 0x9e, 0xe4, 0xc4, 0xf2, 0xa0, 0x09, 0x50, 0x89,
+ 0xf8, 0x37, 0x5d, 0x81, 0x15, 0x5e, 0xe0, 0xc8, 0xa9, 0x4b, 0xc3, 0xc5,
+ 0x56, 0xaa, 0x22, 0x8e, 0x9d, 0xb5, 0x51, 0xeb, 0x61, 0x32, 0xfd, 0xb5,
+ 0xad, 0x55, 0x7c, 0x61, 0x3c, 0x00, 0xfb, 0x72, 0xe4, 0x46, 0x31, 0xfb,
+ 0x9f, 0x0b, 0x6a, 0xbf, 0xa7, 0x57, 0xfc, 0xbd, 0xa7, 0x00, 0xde, 0xc7,
+ 0x80, 0x5a, 0x20, 0xc8, 0x51, 0x9f, 0xa6, 0xa3, 0xc9, 0x12, 0x3a, 0x0f,
+ 0x0f, 0x62, 0x20, 0xad, 0x8a, 0x56, 0x85, 0xe8, 0x6f, 0xf0, 0xe3, 0xd6,
+ 0x63, 0x6e, 0x19, 0x0b, 0x98, 0xf1, 0x3d, 0x3f, 0xce, 0xe6, 0x9f, 0x3c,
+ 0x75, 0x26, 0xe2, 0x6c, 0x10, 0x8f, 0xee, 0x96, 0xaa, 0x93, 0x41, 0xd8,
+ 0x86, 0xb5, 0x72, 0x70, 0x93, 0xd4, 0x29, 0xbe, 0x9f, 0x57, 0x68, 0xc2,
+ 0x7d, 0x59, 0xbb, 0x8d, 0x3d, 0x70, 0xa7, 0x04, 0x23, 0x84, 0x37, 0x86,
+ 0x1d, 0xb0, 0x5e, 0x00, 0x11, 0x24, 0x9d, 0x65, 0xca, 0x84, 0x7f, 0x69,
+ 0xd0, 0xa9, 0x16, 0xab, 0xdb, 0xa4, 0x9f, 0x83, 0x1c, 0x6a, 0x07, 0x7b,
+ 0x6d, 0xb9, 0x32, 0xff, 0xed, 0x09, 0x24, 0x4f, 0x0a, 0x16, 0x41, 0x10,
+ 0xce, 0x32, 0xb4, 0xa1, 0x78, 0x7f, 0x8d, 0xf4, 0x59, 0xb6, 0xcb, 0x3b,
+ 0xb8, 0x80, 0x9b, 0x47, 0x39, 0xeb, 0xcc, 0x10, 0xff, 0x19, 0xdb, 0x8c,
+ 0x70, 0xfe, 0x93, 0x36, 0xd6, 0x66, 0xaa, 0x87, 0xbc, 0x01, 0xbf, 0xa6,
+ 0xac, 0x20, 0x90, 0x84, 0x38, 0x17, 0xdc, 0x35, 0x23, 0x41, 0xde, 0x40,
+ 0x70, 0x0f, 0x1b, 0x99, 0xa6, 0xf2, 0xf0, 0x55, 0x9c, 0x37, 0x39, 0x5b,
+ 0xb3, 0x33, 0xcb, 0x59, 0x53, 0x4f, 0x6f, 0xc5, 0x73, 0x50, 0xc5, 0xaa,
+ 0x82, 0x70, 0x96, 0x86, 0xd6, 0xf5, 0xdf, 0x46, 0xb4, 0x09, 0xf4, 0xb2,
+ 0xe0, 0x6f, 0x88, 0x0b, 0xd3, 0xe9, 0x9f, 0x9b, 0xa7, 0x2e, 0x21, 0xf8,
+ 0xe0, 0xc6, 0x1a, 0x4a, 0x6b, 0xc9, 0xe4, 0x71, 0x67, 0x75, 0x8c, 0xee,
+ 0x9f, 0xc8, 0x04, 0x21, 0x4e, 0x25, 0x08, 0xd0, 0x49, 0x3a, 0x31, 0x5b,
+ 0xa8, 0x7f, 0xc2, 0xd7, 0x10, 0x2f, 0x00, 0x7a, 0x90, 0x81, 0x40, 0x59,
+ 0x0b, 0x92, 0xd1, 0x37, 0xe3, 0x77, 0xb1, 0x9d, 0xff, 0x73, 0xb0, 0x5f,
+ 0xd3, 0x3b, 0x2a, 0xc5, 0x46, 0x66, 0x75, 0x21, 0x22, 0x55, 0xee, 0x70,
+ 0x69, 0x11, 0x11, 0x89, 0x57, 0x08, 0x97, 0x04, 0x38, 0xdc, 0x96, 0xaf,
+ 0xd3, 0x0c, 0x0d, 0x39, 0x37, 0xf2, 0x0a, 0x34, 0xe9, 0xbf, 0x7f, 0xdf,
+ 0xa3, 0xbf, 0x90, 0xb0, 0xf1, 0x8a, 0xe5, 0xa2, 0x46, 0xc9, 0xd2, 0xe3,
+ 0xa2, 0x3b, 0xd1, 0x39, 0x27, 0x2b, 0xac, 0xb3, 0x45, 0x03, 0xa4, 0x35,
+ 0x2b, 0xc8, 0x1e, 0x61, 0x9e, 0x68, 0x97, 0x15, 0x49, 0x09, 0x6a, 0xb5,
+ 0x3f, 0xde, 0x0f, 0xd1, 0xc0, 0x9e, 0x54, 0xe0, 0xbd, 0xfb, 0x61, 0x4d,
+ 0x68, 0x22, 0x75, 0xc5, 0x95, 0xa3, 0xe5, 0xbe, 0xd6, 0xd0, 0xf5, 0x4b,
+ 0x8c, 0x63, 0x0e, 0xc7, 0xee, 0x24, 0x85, 0x79, 0xdf, 0xd6, 0x98, 0x6e,
+ 0x62, 0xa3, 0xb7, 0xb3, 0xe2, 0x4d, 0xbf, 0xe3, 0xdb, 0x57, 0x1c, 0xbc,
+ 0xbe, 0x5b, 0x89, 0x07, 0xfc, 0x82, 0x5b, 0xcb, 0x3d, 0x1a, 0xb6, 0x2c,
+ 0xc8, 0x0c, 0xad, 0xa8, 0x79, 0xd8, 0x44, 0xba, 0x79, 0x5a, 0x56, 0x46,
+ 0xb1, 0x7b, 0xc4, 0x0a, 0x95, 0x1b, 0x05, 0x1d, 0x3a, 0xcd, 0x21, 0x3b,
+ 0x98, 0xe5, 0xc8, 0xf1, 0xf7, 0x43, 0x25, 0x91, 0xaf, 0xe3, 0x97, 0x45,
+ 0x85, 0x48, 0x90, 0x88, 0x54, 0x2f, 0x92, 0x40, 0x57, 0xac, 0x4b, 0xd8,
+ 0x4e, 0x45, 0x5a, 0x5b, 0xc3, 0x5b, 0x3c, 0x53, 0xd7, 0xb4, 0xd6, 0x52,
+ 0x7e, 0x2a, 0x0d, 0x37, 0x80, 0xf9, 0xdb, 0x82, 0xfe, 0x90, 0x39, 0x4a,
+ 0x4c, 0x73, 0x8e, 0x3b, 0xee, 0x11, 0xd2, 0x6c, 0x45, 0x48, 0x9f, 0x90,
+ 0xf3, 0x7b, 0xb5, 0x8c, 0x0d, 0x06, 0xeb, 0xd5, 0x6c, 0x62, 0x62, 0x57,
+ 0x20, 0xda, 0xe3, 0x55, 0x1e, 0x49, 0x14, 0xcc, 0x54, 0x30, 0xe2, 0x39,
+ 0xcc, 0x5e, 0x12, 0xea, 0xb4, 0x1c, 0xe6, 0x06, 0xc7, 0x86, 0x82, 0x85,
+ 0x01, 0x46, 0xc9, 0xcc, 0xe8, 0x6b, 0x8b, 0xf4, 0xba, 0xae, 0x96, 0x86,
+ 0x61, 0x15, 0x41, 0x49, 0x97, 0x0b, 0x9d, 0xb9, 0xd7, 0xa2, 0x6d, 0x27,
+ 0x2d, 0x31, 0x0a, 0x4e, 0xa9, 0xbe, 0x39, 0x12, 0x46, 0xda, 0x51, 0x2b,
+ 0xf2, 0x73, 0x33, 0x77, 0x77, 0xd3, 0x8e, 0xfa, 0x37, 0xc3, 0x82, 0xec,
+ 0x0c, 0x96, 0x1f, 0xa7, 0x3c, 0xa4, 0xff, 0xe5, 0x67, 0xc0, 0x61, 0xff,
+ 0x2f, 0x8f, 0x45, 0x83, 0xac, 0xd2, 0xe6, 0xc0, 0xd0, 0x83, 0xa9, 0x43,
+ 0x22, 0x36, 0xc1, 0xaa, 0x5f, 0x32, 0x0f, 0x38, 0x55, 0x3d, 0x05, 0xad,
+ 0x33, 0xcc, 0x6f, 0xf3, 0x2f, 0x26, 0x3e, 0x0d, 0xd9, 0x08, 0x97, 0xad,
+ 0xb6, 0xb5, 0xe2, 0xd5, 0xe7, 0x93, 0xbf, 0x4d, 0xad, 0xf8, 0x8a, 0x88,
+ 0xb4, 0xbf, 0xaa, 0x56, 0x97, 0xcb, 0x99, 0xb0, 0x09, 0xdc, 0x4f, 0x95,
+ 0x51, 0x7e, 0x92, 0xd5, 0x0e, 0xb8, 0x0a, 0xc7, 0xcc, 0x1d, 0xb5, 0x8c,
+ 0xfb, 0x02, 0xee, 0x81, 0x35, 0x3c, 0x57, 0x76, 0xe7, 0x9b, 0xba, 0x4d,
+ 0x01, 0x28, 0xe7, 0x23, 0x82, 0xad, 0xfa, 0x99, 0x52, 0xb8, 0xd3, 0x1b,
+ 0x59, 0x3a, 0x25, 0x5c, 0x03, 0x33, 0x0c, 0x47, 0x0d, 0x8e, 0x42, 0x3d,
+ 0x7e, 0x62, 0x17, 0x76, 0x4f, 0x8e, 0x0f, 0xd2, 0x2b, 0xb8, 0x70, 0x99,
+ 0x11, 0x51, 0x4c, 0x4c, 0x7e, 0x8c, 0x8c, 0x70, 0x73, 0xaa, 0x87, 0xa0,
+ 0x0d, 0x29, 0xb4, 0xb3, 0x5e, 0x7f, 0x03, 0x3c, 0xf8, 0x04, 0x2a, 0xb4,
+ 0x4e, 0x78, 0x6c, 0xfa, 0xab, 0xf8, 0xd7, 0x62, 0xa0, 0x0a, 0x89, 0xfd,
+ 0x8c, 0x6b, 0x82, 0xdb, 0x12, 0xc5, 0xa3, 0x8b, 0x80, 0xb9, 0x76, 0x7c,
+ 0x67, 0x8c, 0x08, 0xfc, 0xc1, 0x8f, 0x0d, 0xf9, 0xef, 0xdc, 0x28, 0x0e,
+ 0x82, 0x1c, 0x4f, 0xbf, 0x30, 0x0f, 0x37, 0xaa, 0xf9, 0x8a, 0x6d, 0xb3,
+ 0x4d, 0xd0, 0x60, 0xec, 0xec, 0xe7, 0x04, 0xee, 0xa3, 0x8e, 0x29, 0xfb,
+ 0xf3, 0x54, 0x98, 0x48, 0x62, 0xa6, 0x6b, 0x73, 0x27, 0xc0, 0xc4, 0xe2,
+ 0x52, 0xc5, 0x8c, 0xbd, 0xa3, 0xd7, 0xa4, 0x99, 0x0f, 0x9b, 0x2f, 0x86,
+ 0xef, 0x64, 0x4d, 0xa7, 0x2f, 0xcf, 0xad, 0x33, 0xfd, 0x62, 0x3a, 0x3a,
+ 0xa0, 0xec, 0x13, 0x2d, 0x1e, 0x9d, 0x0d, 0x3f, 0x24, 0x42, 0x9c, 0x88,
+ 0xd7, 0xb1, 0xf4, 0xf1, 0xd9, 0xa8, 0xa0, 0x4d, 0xb0, 0xa8, 0x12, 0x71,
+ 0x52, 0x06, 0x06, 0xc8, 0x10, 0xa8, 0xc6, 0x9b, 0x98, 0x8e, 0xb4, 0x67,
+ 0xea, 0x32, 0xcb, 0xef, 0x75, 0x55, 0xf2, 0x57, 0xad, 0x97, 0x6b, 0x72,
+ 0x69, 0xb9, 0x20, 0xc2, 0x30, 0xbb, 0x94, 0x9d, 0xf9, 0xf3, 0x08, 0x8b,
+ 0xe5, 0x3f, 0xcb, 0xc4, 0x8d, 0xe0, 0x16, 0xc4, 0xd7, 0x03, 0xf2, 0xea,
+ 0xc8, 0xcb, 0x95, 0xc8, 0x2a, 0x0b, 0x73, 0x67, 0xec, 0xc9, 0x8e, 0xe6,
+ 0x94, 0x44, 0x92, 0x6a, 0x76, 0xf1, 0xa6, 0x79, 0x2f, 0x1d, 0xde, 0x18,
+ 0x4a, 0x3e, 0x83, 0x35, 0x6b, 0xd9, 0xdb, 0x1d, 0x26, 0x0c, 0x72, 0xe1,
+ 0x4e, 0xba, 0xf1, 0xde, 0x19, 0xe6, 0x78, 0x1e, 0xaa, 0x76, 0x5b, 0x2a,
+ 0x58, 0x6b, 0x7d, 0x34, 0x38, 0xb8, 0x2a, 0x78, 0xa0, 0x9f, 0x42, 0xeb,
+ 0x92, 0xa6, 0xaa, 0xa2, 0xc1, 0xe4, 0xfd, 0x90, 0x2f, 0x04, 0x2f, 0x21,
+ 0xce, 0xd1, 0xf9, 0x98, 0x4d, 0xc5, 0x3e, 0x62, 0xd4, 0xc7, 0xf7, 0xd9,
+ 0x7f, 0x5d, 0xfc, 0x24, 0x3d, 0xec, 0x6c, 0xab, 0x74, 0x9c, 0x94, 0x07,
+ 0x5a, 0xb5, 0x20, 0x4b, 0xe7, 0x3e, 0x0a, 0x78, 0x13, 0x94, 0x17, 0x5d,
+ 0x43, 0x23, 0x25, 0x5a, 0x77, 0x73, 0x13, 0xbf, 0xee, 0xfd, 0xa1, 0x2a,
+ 0xe8, 0x2a, 0x38, 0xcf, 0xe8, 0xa6, 0x19, 0x2f, 0x35, 0xa3, 0xdc, 0xed,
+ 0x9d, 0xcb, 0xe3, 0x8b, 0x2c, 0x40, 0xf1, 0x70, 0xda, 0x67, 0x60, 0x8a,
+ 0xd0, 0x8e, 0x05, 0x9c, 0x14, 0x79, 0x80, 0x59, 0x16, 0xa4, 0xbb, 0xb9,
+ 0x62, 0xb4, 0x22, 0xa1, 0xc2, 0x73, 0xf9, 0x1b, 0xe1, 0x96, 0x95, 0xd9,
+ 0x0c, 0xa0, 0xe7, 0x88, 0x59, 0x15, 0xa9, 0xcd, 0x33, 0x44, 0xd7, 0xe1,
+ 0x59, 0xc4, 0x98, 0xe7, 0x0e, 0x75, 0x1b, 0xe9, 0xb6, 0x90, 0xaf, 0x6d,
+ 0xbd, 0x99, 0xc1, 0x2a, 0xb0, 0x00, 0xb0, 0x9e, 0x21, 0x8d, 0x03, 0xa9,
+ 0xb2, 0x33, 0xcf, 0x66, 0xf0, 0x20, 0xab, 0x16, 0xf7, 0x1c, 0xe6, 0x6d,
+ 0x47, 0x97, 0xf4, 0x06, 0x52, 0x5b, 0x4e, 0xca, 0x69, 0xbd, 0xb6, 0xc1,
+ 0xe7, 0xc0, 0x2c, 0x97, 0x7f, 0x22, 0xba, 0x48, 0xea, 0xef, 0xbe, 0xa5,
+ 0x38, 0x71, 0x74, 0x0e, 0x13, 0x44, 0x56, 0x04, 0x91, 0x9b, 0x55, 0x9a,
+ 0xfd, 0xd2, 0x42, 0xc2, 0x03, 0x07, 0xca, 0xd3, 0xf0, 0x0c, 0x04, 0xe1,
+ 0xd7, 0xfa, 0xbc, 0x3e, 0x1e, 0xed, 0x61, 0x2a, 0x7d, 0x21, 0x56, 0xe7,
+ 0x80, 0x27, 0x17, 0x2e, 0x3c, 0x11, 0x03, 0x63, 0x20, 0xc8, 0xb5, 0xb0,
+ 0x3e, 0x59, 0xae, 0xf6, 0xfe, 0x75, 0x59, 0x79, 0x7b, 0xb8, 0x1e, 0x4d,
+ 0xf0, 0x29, 0xbe, 0x84, 0x44, 0xde, 0x20, 0x3c, 0xb2, 0xb6, 0x9f, 0x25,
+ 0x44, 0x37, 0x1e, 0x42, 0x83, 0xe5, 0xd6, 0xdd, 0xbd, 0xbd, 0xdf, 0xf9,
+ 0x22, 0xd1, 0x3d, 0x90, 0x63, 0x66, 0x7a, 0xc7, 0xe7, 0x57, 0x35, 0x8f,
+ 0x2f, 0x99, 0xcb, 0xc3, 0xcb, 0xcc, 0x00, 0xb6, 0x85, 0xa1, 0xe0, 0x21,
+ 0x10, 0xb3, 0x35, 0xbf, 0xae, 0xb8, 0x28, 0xed, 0xc1, 0x1a, 0x59, 0xcf,
+ 0x74, 0x54, 0x6c, 0xcd, 0x9d, 0xc2, 0xb7, 0xb8, 0x94, 0xf2, 0xb1, 0x94,
+ 0xe8, 0x7c, 0xf2, 0x0e, 0x98, 0x16, 0x16, 0x76, 0xf0, 0xf9, 0x41, 0xdb,
+ 0xfe, 0xc8, 0x51, 0x50, 0x63, 0x36, 0xc0, 0x00, 0xf0, 0x78, 0xbd, 0x4b,
+ 0xb0, 0xd4, 0x77, 0xa5, 0x61, 0x91, 0x3a, 0x66, 0xe2, 0x97, 0xae, 0x75,
+ 0x57, 0x44, 0xd5, 0x7b, 0x07, 0x05, 0x35, 0xbd, 0x01, 0xa5, 0x8e, 0xf9,
+ 0x46, 0x66, 0x90, 0x08, 0xd8, 0x8d, 0xd6, 0xbe, 0x2a, 0xac, 0xe4, 0x9c,
+ 0x4c, 0x53, 0xfe, 0x6e, 0x64, 0xec, 0x4a, 0x1b, 0xbf, 0x16, 0x41, 0x07,
+ 0x64, 0x64, 0x49, 0x81, 0x67, 0xc6, 0xc3, 0xdc, 0xd4, 0x12, 0x8d, 0xed,
+ 0xcc, 0xb3, 0x56, 0xa9, 0xa7, 0x89, 0x68, 0xcf, 0x18, 0x6f, 0x78, 0x07,
+ 0x67, 0x1e, 0xd4, 0x50, 0xa8, 0x62, 0x58, 0xde, 0xab, 0xca, 0x1a, 0x71,
+ 0x66, 0x74, 0xba, 0x5b, 0x74, 0xe6, 0xab, 0x59, 0xa1, 0x90, 0x9a, 0x34,
+ 0x07, 0x3a, 0x60, 0x95, 0xd2, 0x52, 0xc6, 0x1c, 0x1c, 0xec, 0xf1, 0x7c,
+ 0x42, 0x10, 0x17, 0xa9, 0x6f, 0x18, 0xe9, 0xda, 0x09, 0x60, 0xf9, 0xcb,
+ 0xe0, 0xb3, 0x4c, 0x9b, 0x3c, 0x8a, 0xf5, 0x9d, 0x8d, 0xb4, 0xf1, 0x7d,
+ 0x5a, 0x24, 0x86, 0x3f, 0x04, 0xc2, 0x2e, 0xb4, 0x11, 0x36, 0xc1, 0x1a,
+ 0x8d, 0x08, 0x71, 0xd6, 0x95, 0x4c, 0xb6, 0x62, 0xec, 0x81, 0x1a, 0x35,
+ 0x28, 0x67, 0x86, 0xe8, 0xf6, 0xc4, 0xf7, 0xd2, 0xe1, 0x2b, 0x94, 0xf6,
+ 0xc6, 0x17, 0xcd, 0x7e, 0xbd, 0x65, 0x96, 0x19, 0x74, 0x1f, 0x32, 0xf6,
+ 0xc0, 0xdb, 0x40, 0x2d, 0xf8, 0xce, 0x89, 0x9d, 0x9b, 0x81, 0xb0, 0xfb,
+ 0xed, 0x8d, 0xac, 0xb4, 0xea, 0xa4, 0xe8, 0x0a, 0xf0, 0xed, 0xbf, 0xe4,
+ 0x60, 0x64, 0x27, 0x77, 0x5b, 0x95, 0xc9, 0xe3, 0x08, 0x3f, 0x89, 0x56,
+ 0xc4, 0xbe, 0x2a, 0x56, 0x14, 0xdc, 0x34, 0xa1, 0xca, 0x0f, 0x2a, 0xbc,
+ 0x95, 0x8d, 0xaf, 0x22, 0x63, 0xa5, 0x40, 0xa9, 0xaa, 0x08, 0x52, 0x88,
+ 0x18, 0x80, 0xe8, 0x70, 0x38, 0xe2, 0x85, 0xcf, 0x8f, 0x78, 0x5a, 0x4e,
+ 0x1c, 0x7f, 0x2a, 0x14, 0xdd, 0xec, 0xf5, 0xc8, 0x3d, 0xe0, 0xe2, 0x3a,
+ 0x0c, 0x92, 0x48, 0x98, 0xdd, 0x5a, 0xea, 0x37, 0xa1, 0x75, 0xd7, 0x5c,
+ 0x3c, 0x84, 0xf1, 0x87, 0x2a, 0xce, 0x48, 0x92, 0x24, 0x68, 0x1a, 0xb6,
+ 0xa1, 0xdd, 0x03, 0xc8, 0xf3, 0xa4, 0x07, 0xb5, 0xd4, 0x5a, 0xbc, 0xe0,
+ 0x04, 0x38, 0x58, 0x10, 0xf3, 0x72, 0x5e, 0x53, 0x7e, 0xb2, 0x0f, 0xb0,
+ 0x49, 0xcf, 0x3b, 0x89, 0x0a, 0x57, 0x14, 0x39, 0xbe, 0xb3, 0x59, 0x09,
+ 0xb5, 0xb1, 0x82, 0x0f, 0xd6, 0x77, 0xe9, 0x9a, 0xa4, 0x37, 0x40, 0x5f,
+ 0x98, 0x37, 0xae, 0x50, 0x81, 0x65, 0x25, 0xd1, 0xcd, 0x6e, 0x06, 0x1b,
+ 0xab, 0xf0, 0x31, 0x37, 0xa1, 0xfd, 0x8e, 0x53, 0xed, 0x3f, 0xf3, 0x5d,
+ 0x99, 0x62, 0xa9, 0x74, 0xee, 0x87, 0x92, 0x0f, 0x0e, 0x22, 0xf1, 0xc8,
+ 0x1c, 0x66, 0x55, 0x7c, 0x88, 0xe4, 0x81, 0xb9, 0xf1, 0xbb, 0x94, 0xc7,
+ 0x93, 0x16, 0x49, 0xe8, 0x3c, 0x4e, 0x48, 0x43, 0x48, 0x11, 0x64, 0x73,
+ 0x76, 0xfa, 0x8d, 0x2b, 0xf1, 0x98, 0x76, 0x27, 0x3e, 0xd2, 0x6b, 0xd5,
+ 0xa0, 0x46, 0x83, 0x70, 0xc4, 0x44, 0xaa, 0x7f, 0x68, 0x06, 0xa0, 0xa3,
+ 0xeb, 0x03, 0x9b, 0x16, 0x2a, 0x44, 0x39, 0x64, 0x4c, 0xc7, 0x75, 0x4e,
+ 0xab, 0x01, 0xf6, 0x92, 0x43, 0xe8, 0x4d, 0x92, 0xcb, 0x1d, 0xfa, 0x72,
+ 0x98, 0xf7, 0xd5, 0xaf, 0x7c, 0x71, 0xbc, 0x2b, 0xc0, 0xf0, 0xe8, 0x45,
+ 0xb8, 0x72, 0xe1, 0x36, 0xfe, 0xed, 0xb9, 0xa7, 0xf3, 0xdf, 0x35, 0x19,
+ 0xa6, 0xdf, 0x8d, 0x10, 0x54, 0x9d, 0xe7, 0xa2, 0x3b, 0xcf, 0x9d, 0x66,
+ 0x47, 0x49, 0xae, 0x8b, 0x1f, 0xe2, 0x6c, 0xa5, 0x6d, 0x29, 0x27, 0x00,
+ 0xb9, 0xb8, 0x69, 0xb4, 0x44, 0x6b, 0x79, 0x15, 0xb7, 0xa9, 0xb7, 0x9e,
+ 0xb5, 0x58, 0x9f, 0xf3, 0x4f, 0xd3, 0x9f, 0xd6, 0x32, 0x21, 0x5e, 0xdf,
+ 0x35, 0xba, 0x9b, 0x4d, 0xe5, 0x01, 0x2d, 0x26, 0xd6, 0xa2, 0x3c, 0xc6,
+ 0x6e, 0x85, 0x1c, 0xbc, 0xbb, 0x55, 0xa1, 0xeb, 0x88, 0x35, 0x0e, 0x5b,
+ 0xe4, 0x8a, 0x5d, 0xb4, 0x19, 0x2e, 0x7c, 0x3f, 0x61, 0xf4, 0x8e, 0xbd,
+ 0xa0, 0xa5, 0x6f, 0x58, 0xb0, 0x7d, 0x56, 0x50, 0xc7, 0xe9, 0x6e, 0xef,
+ 0xc2, 0xff, 0x2a, 0x4d, 0x9d, 0x3e, 0xca, 0xe6, 0xe4, 0x92, 0x6e, 0xbb,
+ 0x67, 0xcf, 0xa3, 0xc2, 0x97, 0xd8, 0xe1, 0x3e, 0x66, 0x25, 0xb0, 0x7d,
+ 0x07, 0xa4, 0x47, 0xf5, 0xfc, 0xbf, 0x1e, 0x0b, 0xe4, 0xc0, 0x49, 0x7f,
+ 0xb7, 0x96, 0x83, 0x75, 0x3d, 0xce, 0xf2, 0x7e, 0x56, 0xad, 0x20, 0x2d,
+ 0x7e, 0xd7, 0xea, 0x5a, 0x0a, 0x75, 0xcc, 0x85, 0xed, 0x3f, 0x4d, 0x8f,
+ 0x39, 0x3e, 0x9b, 0x29, 0x27, 0x8a, 0x72, 0x8a, 0x4b, 0xab, 0x99, 0x6d,
+ 0x7f, 0x29, 0x63, 0x94, 0x2c, 0x5f, 0x40, 0xbf, 0xe0, 0x30, 0x54, 0xc0,
+ 0x0d, 0x3c, 0x37, 0xfd, 0xc3, 0xfe, 0xf2, 0xba, 0x1f, 0xc6, 0x39, 0xd0,
+ 0x57, 0x1a, 0x4a, 0x92, 0xed, 0x36, 0x4b, 0x28, 0x4d, 0x3f, 0x2a, 0x1e,
+ 0xc1, 0x45, 0x19, 0x3e, 0xdb, 0x30, 0xf3, 0xc7, 0xd0, 0xa9, 0x52, 0x27,
+ 0xdf, 0x00, 0x1f, 0x43, 0xbd, 0x56, 0x30, 0x68, 0x9c, 0x37, 0xc6, 0x42,
+ 0x76, 0x7e, 0x11, 0x2b, 0x45, 0x01, 0xa9, 0xb8, 0x16, 0x4b, 0x25, 0x89,
+ 0x9b, 0x78, 0xdf, 0x21, 0x38, 0xe5, 0x6b, 0x9f, 0xe1, 0x6a, 0xa1, 0x7c,
+ 0xc6, 0xa5, 0xdd, 0xe2, 0x78, 0x11, 0x81, 0xbc, 0x13, 0x58, 0xbf, 0x32,
+ 0x21, 0x06, 0xa1, 0x8e, 0x3a, 0xcf, 0xc2, 0x0a, 0xde, 0x60, 0x40, 0x43,
+ 0xb4, 0xd0, 0x05, 0x1c, 0x09, 0x97, 0xe4, 0x23, 0x94, 0xe2, 0x88, 0xd9,
+ 0x03, 0x2a, 0x8a, 0x28, 0x5e, 0xb6, 0x09, 0xb2, 0x23, 0x97, 0xcc, 0x47,
+ 0x9c, 0xfc, 0xa0, 0x9c, 0xd6, 0x35, 0x07, 0x0f, 0x33, 0x1a, 0x4c, 0x0e,
+ 0x27, 0xf6, 0x20, 0x28, 0x97, 0xea, 0xa6, 0x0d, 0x94, 0x93, 0x4b, 0x9e,
+ 0x67, 0x1a, 0x01, 0x71, 0x0e, 0x75, 0x75, 0x6b, 0x2c, 0x4a, 0x92, 0x37,
+ 0xc2, 0x77, 0x30, 0x98, 0x07, 0x62, 0x3e, 0x8a, 0x47, 0x94, 0x0a, 0xd9,
+ 0x8c, 0x61, 0xcb, 0x08, 0x48, 0xd8, 0x07, 0x1a, 0x95, 0xc4, 0x24, 0x25,
+ 0xa9, 0x84, 0x39, 0x14, 0x83, 0x17, 0xbf, 0x01, 0x26, 0x79, 0x22, 0x34,
+ 0x2d, 0x18, 0xa9, 0xae, 0x91, 0x5f, 0x8c, 0xc5, 0x02, 0xa4, 0x39, 0x68,
+ 0xf2, 0xba, 0x80, 0xab, 0x59, 0x58, 0xa3, 0xa6, 0x05, 0x27, 0x79, 0x04,
+ 0xf4, 0x15, 0xbd, 0x5d, 0xb5, 0xb8, 0x08, 0xa6, 0xeb, 0x2c, 0x4e, 0x31,
+ 0x09, 0xfe, 0x5b, 0xb2, 0x70, 0x5d, 0xb1, 0xde, 0x84, 0x9e, 0x44, 0x15,
+ 0x94, 0xca, 0x87, 0x8d, 0xb4, 0xb5, 0x6c, 0xc8, 0x63, 0x7e, 0x96, 0xf0,
+ 0x4c, 0x40, 0x9c, 0xb1, 0xfd, 0x17, 0x47, 0x63, 0x9e, 0x41, 0x74, 0x40,
+ 0x1e, 0xa7, 0xa7, 0x57, 0x76, 0xf5, 0xcb, 0x6e, 0xf4, 0x95, 0x35, 0xb0,
+ 0xf6, 0xc9, 0x37, 0x3c, 0xc0, 0xc4, 0x28, 0x4d, 0xf2, 0x61, 0x28, 0xf6,
+ 0xf8, 0xff, 0x9d, 0xee, 0x16, 0x0a, 0xc1, 0x6a, 0x0c, 0xd2, 0x90, 0xc3,
+ 0xdd, 0xb9, 0xe6, 0x69, 0x22, 0x4a, 0xdf, 0xcf, 0x58, 0x66, 0xd2, 0xab,
+ 0x05, 0x9a, 0x59, 0x61, 0x42, 0xc9, 0xcd, 0xe3, 0x91, 0x0a, 0xd7, 0x0d,
+ 0xe4, 0xb1, 0x33, 0x22, 0x44, 0x0f, 0xd6, 0x0b, 0xba, 0x39, 0x7d, 0x42,
+ 0x44, 0xb8, 0xf7, 0x48, 0xdb, 0xf2, 0x84, 0x28, 0xce, 0x56, 0x24, 0x91,
+ 0xfc, 0xf0, 0xaa, 0x04, 0x52, 0x08, 0x49, 0xb9, 0x5e, 0xd1, 0x68, 0x76,
+ 0x0c, 0x97, 0x52, 0x0e, 0x3d, 0x09, 0xbd, 0xd5, 0x91, 0x57, 0xfa, 0x53,
+ 0xce, 0x26, 0x1d, 0x6a, 0xeb, 0x39, 0x28, 0xd5, 0x24, 0xc9, 0x2a, 0x33,
+ 0xb6, 0x3c, 0x95, 0x03, 0x08, 0x30, 0xfc, 0xea, 0x87, 0xa4, 0xe1, 0x0e,
+ 0x31, 0x38, 0x7c, 0xf7, 0xe0, 0x8b, 0x8d, 0x09, 0x4e, 0x89, 0x16, 0x02,
+ 0x23, 0x44, 0xc0, 0xbf, 0xd0, 0x51, 0xca, 0x77, 0x8e, 0x45, 0x75, 0xe8,
+ 0x6b, 0xa9, 0x30, 0x1e, 0xc7, 0xaa, 0x2d, 0xdf, 0xad, 0x81, 0x0c, 0xac,
+ 0x1f, 0x8a, 0x5a, 0xd9, 0x80, 0xcd, 0x1d, 0x04, 0x57, 0x02, 0x79, 0xc1,
+ 0xbf, 0x65, 0xe3, 0x89, 0xa5, 0x35, 0x51, 0x9a, 0xe2, 0x09, 0xdf, 0xa7,
+ 0x62, 0xc7, 0xdc, 0x42, 0xcb, 0x27, 0x5c, 0x48, 0xda, 0x39, 0x81, 0x1b,
+ 0x43, 0x4f, 0x35, 0x71, 0x09, 0x9d, 0x0a, 0x71, 0x66, 0x16, 0x6f, 0x2c,
+ 0x53, 0x14, 0x43, 0x0b, 0x86, 0x2b, 0xe6, 0xa1, 0x4d, 0x5a, 0x6e, 0x4b,
+ 0xb3, 0x6b, 0x5e, 0x1c, 0x16, 0xb1, 0x3f, 0x4b, 0x54, 0x71, 0x6f, 0x37,
+ 0x56, 0x61, 0x62, 0xcc, 0xaa, 0x22, 0x98, 0x76, 0x7f, 0xd8, 0xa5, 0xac,
+ 0x12, 0x53, 0x01, 0xc4, 0x40, 0x8f, 0x95, 0x69, 0x3a, 0x1a, 0xb7, 0x41,
+ 0x53, 0x55, 0xa2, 0x9e, 0x9d, 0x26, 0x00, 0x58, 0x1e, 0x4b, 0x24, 0xc6,
+ 0x68, 0xd2, 0x17, 0x07, 0xd6, 0x9c, 0x12, 0x60, 0x18, 0x28, 0xa3, 0x1c,
+ 0xc8, 0x00, 0x97, 0xe6, 0xfb, 0x4f, 0x2e, 0x64, 0x32, 0x76, 0x45, 0x90,
+ 0xfa, 0xfb, 0x2d, 0x38, 0x68, 0x02, 0x20, 0xf5, 0x02, 0x1f, 0x46, 0xcf,
+ 0x4c, 0x82, 0x70, 0x02, 0xce, 0x87, 0x5f, 0xdd, 0x2a, 0xab, 0x1c, 0xe8,
+ 0x4c, 0x97, 0x59, 0x7b, 0x0a, 0x7a, 0x18, 0x52, 0x15, 0x7c, 0xf9, 0x66,
+ 0x33, 0x1c, 0xc7, 0xdf, 0xfc, 0xb8, 0x57, 0xf1, 0x4e, 0x30, 0x78, 0xfe,
+ 0x81, 0x38, 0x7f, 0x96, 0xf8, 0x48, 0xb2, 0x7a, 0xfc, 0x8b, 0xa0, 0xc8,
+ 0xdf, 0xb7, 0xd7, 0x5e, 0x13, 0x0f, 0x77, 0xe8, 0xbe, 0xa0, 0x2b, 0xd0,
+ 0xf6, 0x29, 0x53, 0x4d, 0x7a, 0xc8, 0xfc, 0x3f, 0x34, 0x6e, 0xab, 0x53,
+ 0x99, 0x39, 0xb9, 0xa7, 0x88, 0x5a, 0x57, 0x13, 0xb1, 0x7d, 0xf5, 0xf8,
+ 0x6a, 0x69, 0x6e, 0x39, 0xc1, 0xfe, 0xa0, 0xe4, 0xf3, 0xe2, 0x2f, 0xe3,
+ 0x58, 0x3f, 0xfa, 0x3c, 0x3b, 0x9d, 0x18, 0xad, 0x46, 0x55, 0xcc, 0x0a,
+ 0x64, 0x99, 0x15, 0x28, 0xe9, 0xb5, 0xee, 0xbf, 0x2d, 0x7f, 0x23, 0x2d,
+ 0x0a, 0xa6, 0x93, 0x72, 0x9b, 0x81, 0x53, 0x3c, 0x6f, 0x0a, 0xa6, 0x24,
+ 0x59, 0x6b, 0x21, 0x24, 0x65, 0xe5, 0xdc, 0x1b, 0x7e, 0x31, 0x4a, 0x09,
+ 0x3e, 0x33, 0xe7, 0x41, 0xd5, 0x63, 0x02, 0xd7, 0x8a, 0x5f, 0x6a, 0x96,
+ 0x79, 0xf7, 0x3b, 0x37, 0x31, 0xbd, 0xac, 0xf3, 0xaa, 0x27, 0x26, 0xdc,
+ 0x54, 0xce, 0x1c, 0xe4, 0x8d, 0x28, 0x59, 0xe5, 0xfb, 0x70, 0x7a, 0xdf,
+ 0x32, 0x32, 0xd9, 0x4a, 0xad, 0xcb, 0xf9, 0x2c, 0x2b, 0xfa, 0x28, 0x75,
+ 0x24, 0xc8, 0xc4, 0xe4, 0x4a, 0x50, 0x5b, 0x00, 0x60, 0xed, 0xa5, 0xcb,
+ 0x83, 0x68, 0x17, 0xdb, 0x2b, 0x9a, 0xa3, 0xae, 0xb4, 0x29, 0xd5, 0x89,
+ 0x41, 0xe8, 0x73, 0x06, 0xef, 0x84, 0x26, 0x83, 0x09, 0x54, 0xc7, 0xdf,
+ 0xed, 0x10, 0x56, 0x5e, 0x40, 0x4c, 0xdf, 0x6b, 0xc3, 0x78, 0x1b, 0x8a,
+ 0xe8, 0xee, 0xce, 0x55, 0x8e, 0x55, 0x4e, 0x53, 0xa3, 0xa6, 0x7b, 0xc2,
+ 0x45, 0x43, 0x65, 0xcf, 0xf0, 0xdb, 0xe4, 0x9e, 0xe2, 0x0d, 0xc8, 0x59,
+ 0xb7, 0xae, 0x8c, 0xf9, 0x36, 0xd7, 0x54, 0x28, 0xbe, 0xae, 0x1a, 0x5f,
+ 0x9a, 0x06, 0x6c, 0xc1, 0x83, 0xec, 0xac, 0x62, 0x77, 0xfe, 0xce, 0x54,
+ 0x68, 0xb0, 0x92, 0xcf, 0xe0, 0xbd, 0x91, 0xbf, 0x99, 0x09, 0x33, 0xe3,
+ 0x8f, 0xa8, 0xfe, 0xd5, 0x96, 0xaa, 0x08, 0x54, 0x53, 0x84, 0xb3, 0x2b,
+ 0xc9, 0x31, 0x2d, 0x56, 0x61, 0x6c, 0xc7, 0x59, 0xa9, 0xc1, 0x41, 0x74,
+ 0x02, 0xa8, 0x4f, 0x60, 0x13, 0x87, 0x06, 0xe7, 0x79, 0xe7, 0xbe, 0x8f,
+ 0x4b, 0x89, 0x78, 0x11, 0xc0, 0x61, 0x0e, 0x1b, 0x60, 0x11, 0xc7, 0x01,
+ 0x20, 0x70, 0x9f, 0x85, 0x4c, 0xf2, 0x6b, 0x7f, 0x52, 0x0e, 0xa8, 0x3c,
+ 0xcd, 0xf7, 0x91, 0x00, 0x37, 0xc0, 0xb4, 0x43, 0x52, 0x37, 0xe3, 0x15,
+ 0x23, 0x71, 0x6c, 0x5e, 0x0d, 0x27, 0xa9, 0x7c, 0x5a, 0x44, 0xa6, 0x2e,
+ 0xc2, 0x4c, 0x88, 0xa0, 0x27, 0x93, 0x70, 0x53, 0xbf, 0x6e, 0x88, 0x29,
+ 0x33, 0x1c, 0x4d, 0x59, 0x54, 0x90, 0x8d, 0x31, 0x68, 0xe7, 0x61, 0xd6,
+ 0x37, 0xa4, 0x24, 0x32, 0x05, 0x36, 0xa4, 0xfa, 0x99, 0x97, 0x71, 0xf9,
+ 0xa1, 0xd8, 0xbe, 0xae, 0x65, 0x1a, 0x0e, 0x0c, 0xec, 0x43, 0x37, 0x48,
+ 0xd4, 0xa1, 0x44, 0x2a, 0x7e, 0xe4, 0x9d, 0x63, 0x09, 0xc7, 0x8b, 0x4b,
+ 0xf9, 0x20, 0x6c, 0x8b, 0x13, 0xa3, 0xe5, 0x04, 0x32, 0x09, 0x89, 0xf2,
+ 0x25, 0x89, 0x96, 0x34, 0x97, 0x80, 0xff, 0xa8, 0xc6, 0x97, 0x70, 0x7e,
+ 0x78, 0x83, 0xd7, 0x9f, 0xb4, 0xb6, 0xad, 0xbb, 0x08, 0x9c, 0xf8, 0x62,
+ 0xcf, 0xf0, 0x7e, 0x4a, 0xc9, 0xbf, 0x97, 0xf7, 0x6a, 0xe5, 0x47, 0x9d,
+ 0x16, 0xbc, 0x9a, 0xeb, 0x57, 0x40, 0xbd, 0x11, 0x77, 0xed, 0xfe, 0x83,
+ 0xef, 0xc1, 0x0b, 0x97, 0x36, 0x31, 0x2a, 0x71, 0xa6, 0x21, 0xa6, 0x9c,
+ 0xde, 0x5c, 0xe5, 0x9c, 0xb8, 0xdf, 0x51, 0x3e, 0xca, 0xcf, 0x46, 0x4f,
+ 0x0b, 0xde, 0xb0, 0xfc, 0x13, 0xb6, 0x8b, 0xe7, 0x46, 0xef, 0x5a, 0x44,
+ 0x34, 0x95, 0x0f, 0x7f, 0x75, 0x2e, 0xf7, 0x15, 0x08, 0x20, 0x45, 0x06,
+ 0xdd, 0xdb, 0x38, 0x03, 0x77, 0x36, 0x00, 0x2d, 0xf5, 0x0e, 0x43, 0xdc,
+ 0x7b, 0x05, 0xf8, 0xc1, 0xd2, 0x1f, 0xb8, 0x9b, 0x45, 0xc1, 0xbf, 0x08,
+ 0x71, 0xe9, 0xfd, 0xf7, 0xf3, 0x25, 0x5f, 0xaa, 0x73, 0x11, 0xd7, 0xe2,
+ 0x1c, 0x89, 0xe4, 0xae, 0xde, 0x60, 0x79, 0x16, 0x1e, 0x0f, 0x46, 0xd2,
+ 0x13, 0xe6, 0x49, 0xef, 0xa1, 0x4e, 0x27, 0x83, 0xed, 0x96, 0x85, 0x03,
+ 0xcb, 0x7c, 0x3c, 0xb7, 0x69, 0xa3, 0xf5, 0x6e, 0x65, 0xe1, 0x05, 0x90,
+ 0xcf, 0x04, 0x0d, 0x21, 0x4b, 0xf5, 0x86, 0x73, 0x1b, 0x52, 0x1b, 0xee,
+ 0xba, 0x73, 0xca, 0x58, 0x75, 0x4f, 0x98, 0x06, 0x5c, 0xa6, 0x97, 0x9f,
+ 0x2a, 0x78, 0xde, 0x61, 0x52, 0x9c, 0x13, 0x9c, 0x0d, 0x85, 0xf4, 0x97,
+ 0x96, 0xf1, 0x92, 0x12, 0x55, 0x01, 0xf7, 0x8c, 0x69, 0xa8, 0x32, 0xa4,
+ 0x67, 0x1c, 0x65, 0xd5, 0x88, 0x98, 0xe3, 0xe8, 0x29, 0x98, 0x2b, 0x0f,
+ 0x9b, 0x9f, 0x1a, 0x15, 0x6a, 0x13, 0xee, 0xd4, 0xac, 0x77, 0x76, 0x20,
+ 0xc1, 0x72, 0x51, 0xdd, 0xb7, 0xea, 0x73, 0xdf, 0xc5, 0x73, 0x8b, 0x98,
+ 0xc5, 0x7a, 0xe9, 0x8d, 0x90, 0xfb, 0x0c, 0xf2, 0x60, 0x3e, 0x97, 0xb2,
+ 0x2e, 0x30, 0xb6, 0x9a, 0xa9, 0x0d, 0x04, 0xc8, 0x95, 0xfc, 0x0c, 0x7a,
+ 0xb6, 0xcf, 0x2b, 0x7a, 0x63, 0x58, 0x22, 0x87, 0xbf, 0xee, 0x7e, 0x0e,
+ 0xaf, 0x15, 0xf1, 0x72, 0xc6, 0x5e, 0x9a, 0x05, 0xac, 0xe2, 0x06, 0xb7,
+ 0x48, 0x22, 0x72, 0x79, 0x7f, 0x26, 0xe9, 0x69, 0x6f, 0xbd, 0x39, 0x98,
+ 0x03, 0x69, 0x61, 0x5e, 0x19, 0x4e, 0x57, 0x2e, 0x6e, 0x93, 0x2a, 0xc6,
+ 0x39, 0xdd, 0xe0, 0xfa, 0x68, 0x24, 0xe6, 0x1d, 0x8f, 0x92, 0xd0, 0x0e,
+ 0x08, 0x35, 0x2e, 0x06, 0x32, 0xfa, 0xb9, 0xe7, 0xa3, 0x33, 0x1d, 0xb7,
+ 0x4a, 0xf0, 0x96, 0x6a, 0x70, 0x31, 0x00, 0xa5, 0x93, 0x6e, 0xb4, 0xa6,
+ 0x40, 0xea, 0xaa, 0x2e, 0x7a, 0x2d, 0x68, 0x4b, 0x94, 0x61, 0xc8, 0xcf,
+ 0x14, 0xf1, 0x02, 0xda, 0x08, 0xd9, 0x68, 0x4b, 0x7a, 0xca, 0x84, 0xfe,
+ 0x13, 0x26, 0x60, 0xb9, 0x07, 0xe6, 0xa7, 0xaf, 0xca, 0xe1, 0x1a, 0x5e,
+ 0x07, 0x44, 0xed, 0x3c, 0x55, 0x59, 0xf0, 0x52, 0x0b, 0x6b, 0x3e, 0x61,
+ 0xf4, 0x38, 0xc2, 0x61, 0x4e, 0x90, 0x52, 0xd0, 0xfd, 0xc3, 0x3f, 0xf8,
+ 0xbc, 0xf9, 0x36, 0xd0, 0x3f, 0xb1, 0x0e, 0x69, 0x09, 0x8e, 0x4d, 0x87,
+ 0x6e, 0x51, 0x2c, 0x7a, 0x5e, 0xa7, 0x03, 0x3c, 0xd5, 0xbf, 0xa8, 0x01,
+ 0x03, 0xee, 0x9e, 0xcc, 0xd6, 0xcd, 0xd3, 0x72, 0xe0, 0x20, 0x29, 0x3c,
+ 0x0d, 0x67, 0xd8, 0xb7, 0x23, 0xa5, 0xc6, 0xba, 0x00, 0xd9, 0xf1, 0xdf,
+ 0xda, 0x2e, 0x0d, 0x4b, 0xf4, 0xe3, 0xff, 0xd2, 0xe9, 0xb8, 0x97, 0x22,
+ 0xf4, 0x32, 0x83, 0x16, 0xec, 0x13, 0x36, 0xf3, 0x7c, 0xb3, 0x7d, 0x0e,
+ 0xa2, 0x93, 0xfe, 0x08, 0x10, 0x2b, 0x2f, 0xb8, 0x89, 0x31, 0xeb, 0xaa,
+ 0x74, 0x04, 0xa7, 0x6e, 0x45, 0x51, 0xa7, 0x5f, 0xfd, 0x59, 0x3a, 0x59,
+ 0x6e, 0x5b, 0xf4, 0xf3, 0xd9, 0x6c, 0xc7, 0xd6, 0xe8, 0xc2, 0xeb, 0xba,
+ 0xcb, 0x9d, 0x38, 0xa3, 0x1e, 0x2f, 0x4e, 0xa6, 0x32, 0x51, 0xbe, 0x1f,
+ 0xba, 0xdd, 0xa6, 0x84, 0xb1, 0x7b, 0xda, 0x58, 0x75, 0xd0, 0xec, 0x9c,
+ 0xd7, 0x35, 0xbe, 0x5d, 0x2b, 0x31, 0xaf, 0xbb, 0xdb, 0xce, 0xd2, 0x61,
+ 0x51, 0xcd, 0x4c, 0x8c, 0x3e, 0x62, 0x29, 0x05, 0xbd, 0x41, 0x8c, 0x5b,
+ 0x95, 0x37, 0xe4, 0x2e, 0xb8, 0xc0, 0x11, 0xd1, 0xd3, 0x9f, 0x48, 0x85,
+ 0x4b, 0xff, 0x48, 0xca, 0x5e, 0xf0, 0x6f, 0xf9, 0x3a, 0xb0, 0x92, 0xf8,
+ 0xc6, 0x9b, 0xdc, 0xb8, 0x87, 0xd4, 0xaa, 0x81, 0x1d, 0x1c, 0xa8, 0x1f,
+ 0xba, 0xcb, 0x56, 0xe3, 0xa4, 0x90, 0x4b, 0xd2, 0x64, 0x5f, 0x17, 0x4e,
+ 0xcb, 0x30, 0xf3, 0x95, 0xee, 0xec, 0xd0, 0x6f, 0xc6, 0xee, 0x7c, 0x30,
+ 0x9a, 0x24, 0x61, 0x84, 0x2d, 0x90, 0x90, 0x0e, 0xa3, 0xe9, 0x79, 0xdc,
+ 0x45, 0x86, 0x17, 0x7d, 0x3e, 0x84, 0x30, 0x7a, 0xfd, 0xb4, 0x56, 0xd3,
+ 0xc0, 0x15, 0xda, 0x2f, 0x7f, 0x88, 0xcb, 0x1a, 0x44, 0xff, 0x71, 0x95,
+ 0x05, 0xec, 0xee, 0xe2, 0x48, 0x8a, 0x81, 0xb4, 0x54, 0xa9, 0x1b, 0x9d,
+ 0x2d, 0x83, 0xb0, 0x97, 0x84, 0xd6, 0x19, 0xe1, 0xf4, 0xd8, 0xdb, 0x8e,
+ 0x37, 0xb3, 0xec, 0x5b, 0x46, 0xa6, 0x0d, 0x17, 0xa7, 0x9e, 0xfd, 0xeb,
+ 0xec, 0xeb, 0xaf, 0x40, 0x66, 0x52, 0x7d, 0x37, 0x59, 0x79, 0x7e, 0xc7,
+ 0x0e, 0xd5, 0xc9, 0x00, 0x53, 0x60, 0x31, 0x89, 0xb7, 0x1d, 0x69, 0xc4,
+ 0xf0, 0x89, 0x10, 0x7a, 0xed, 0x59, 0xff, 0xea, 0xd3, 0x3b, 0x2f, 0x31,
+ 0x68, 0xca, 0x77, 0x3a, 0x22, 0x8b, 0xf9, 0x08, 0xe1, 0xe0, 0x3a, 0x37,
+ 0x32, 0xd7, 0xff, 0xf7, 0x58, 0x68, 0x1c, 0xf7, 0xff, 0xac, 0x02, 0x49,
+ 0x3a, 0x3f, 0x3f, 0x0a, 0xb2, 0xc0, 0xef, 0xf8, 0x36, 0xf5, 0x94, 0x8b,
+ 0xf7, 0xe3, 0x1f, 0xb0, 0xd2, 0xa2, 0xc4, 0xd1, 0x14, 0x45, 0xda, 0x19,
+ 0x9f, 0x31, 0xdf, 0x70, 0x1c, 0x99, 0x9c, 0x0d, 0x91, 0xc9, 0xba, 0x32,
+ 0x8d, 0xef, 0x6b, 0xa3, 0xf9, 0x4b, 0xc6, 0xae, 0x68, 0x74, 0x5d, 0x94,
+ 0x9f, 0xf6, 0x1b, 0x8b, 0xfb, 0xd1, 0x48, 0x28, 0x5d, 0x19, 0x3c, 0xa3,
+ 0xaa, 0x31, 0x05, 0x6a, 0xb9, 0x6c, 0xf2, 0x8a, 0x02, 0x4e, 0xb0, 0xbf,
+ 0xae, 0x98, 0x2e, 0xeb, 0xeb, 0x0d, 0x87, 0xee, 0x1f, 0x54, 0x54, 0xbb,
+ 0xa8, 0x17, 0xe4, 0x7f, 0x46, 0x74, 0x0f, 0x56, 0x20, 0xcb, 0xcf, 0xa4,
+ 0xdc, 0xaa, 0xc4, 0xeb, 0x81, 0x83, 0x12, 0x28, 0x1c, 0x47, 0x5b, 0xd5,
+ 0x62, 0xe2, 0x49, 0x0a, 0x35, 0x09, 0xcf, 0x29, 0xe7, 0x80, 0xaa, 0x89,
+ 0xe8, 0xc1, 0x2f, 0x36, 0x6d, 0xef, 0xa1, 0xe4, 0xb4, 0x19, 0x4a, 0x7d,
+ 0x6f, 0x29, 0x03, 0xb5, 0x15, 0x33, 0x9a, 0x84, 0xdc, 0x22, 0x71, 0x04,
+ 0x02, 0xdc, 0x0a, 0x73, 0x20, 0x4c, 0xd3, 0x2c, 0x31, 0x0e, 0xa2, 0xf2,
+ 0x89, 0xe6, 0x99, 0x0c, 0x5c, 0x04, 0x77, 0xed, 0x53, 0x24, 0xcb, 0xb6,
+ 0xb5, 0x5c, 0x87, 0x49, 0xab, 0x74, 0x8f, 0xe8, 0xa4, 0xfa, 0x2f, 0x8e,
+ 0x82, 0x01, 0xbc, 0xf8, 0x1f, 0x79, 0x50, 0x12, 0xea, 0x25, 0x40, 0xa6,
+ 0x30, 0x8c, 0x0b, 0x00, 0xd9, 0x20, 0x52, 0xbe, 0xda, 0xb7, 0xf2, 0x39,
+ 0xb6, 0x70, 0xae, 0xe9, 0xf1, 0x3c, 0x0d, 0xe2, 0xda, 0x1d, 0xa2, 0xeb,
+ 0xc3, 0x68, 0xd7, 0x90, 0xc5, 0x54, 0xb7, 0xb7, 0x3e, 0x9f, 0x5c, 0x88,
+ 0x94, 0x4b, 0x29, 0x27, 0xfc, 0xa5, 0x2e, 0x1b, 0x14, 0x0a, 0x02, 0x63,
+ 0xc3, 0x15, 0x8c, 0xbe, 0xc2, 0x8b, 0xc0, 0x74, 0x3b, 0xd0, 0xef, 0x6c,
+ 0x01, 0x35, 0x51, 0x57, 0x98, 0x97, 0x48, 0xc7, 0xdd, 0xe7, 0xce, 0x25,
+ 0x2e, 0xfe, 0xe3, 0x68, 0xfb, 0xf1, 0x15, 0xbf, 0xbb, 0xd3, 0xcf, 0x16,
+ 0xaa, 0x69, 0x3f, 0xa4, 0x09, 0xfd, 0xb6, 0xe6, 0xf2, 0xaf, 0xa7, 0xeb,
+ 0x17, 0x19, 0xdb, 0x6b, 0x79, 0x70, 0x73, 0x8b, 0xb3, 0x02, 0x81, 0x9d,
+ 0xb2, 0xc0, 0xf2, 0xc2, 0x35, 0x56, 0xa5, 0x98, 0x4a, 0x3f, 0x31, 0xb7,
+ 0x68, 0x13, 0xa5, 0x85, 0x3c, 0xe9, 0xe6, 0x98, 0x06, 0x81, 0xf1, 0x7b,
+ 0x4f, 0x29, 0xf6, 0x24, 0xa2, 0xc9, 0x96, 0x45, 0xc7, 0x15, 0x40, 0xbf,
+ 0x32, 0x22, 0x6c, 0x2c, 0x70, 0x6a, 0x6d, 0xbb, 0x15, 0x3a, 0x50, 0x12,
+ 0xa4, 0x62, 0x33, 0x36, 0x89, 0x78, 0x7b, 0x57, 0xfc, 0xf6, 0x2b, 0x60,
+ 0x6d, 0x86, 0x9f, 0x86, 0x87, 0x5b, 0xe4, 0x03, 0xdb, 0x24, 0x74, 0x2d,
+ 0x61, 0xe0, 0xfd, 0xb4, 0x16, 0x46, 0x34, 0xf2, 0xee, 0x96, 0x7a, 0xff,
+ 0x95, 0x5b, 0x21, 0x5f, 0x32, 0x74, 0x6d, 0xc3, 0x55, 0x0b, 0xe9, 0xf8,
+ 0x15, 0x21, 0x97, 0x14, 0xf5, 0x45, 0x5d, 0x2f, 0xb2, 0xbc, 0xa8, 0x45,
+ 0x14, 0x65, 0x78, 0x1e, 0xad, 0x5d, 0x55, 0x97, 0x32, 0xa1, 0x83, 0xbf,
+ 0xcc, 0x9b, 0x76, 0xd0, 0xa8, 0x4d, 0x97, 0x91, 0xe0, 0x5c, 0x34, 0x72,
+ 0xc9, 0x54, 0x04, 0x96, 0xbf, 0xf2, 0x0f, 0x55, 0x72, 0x1e, 0x31, 0xdf,
+ 0xa0, 0x97, 0x19, 0x97, 0x08, 0xef, 0x7a, 0x6b, 0xa0, 0x65, 0x2c, 0xdc,
+ 0xcc, 0x45, 0x69, 0x5d, 0x4b, 0x2e, 0xb0, 0x64, 0x45, 0x1c, 0x57, 0xec,
+ 0xbb, 0xb4, 0x30, 0x6d, 0xf6, 0x98, 0x4e, 0xad, 0x26, 0x90, 0x1b, 0xb8,
+ 0x06, 0x1a, 0x36, 0x33, 0x1b, 0xc4, 0x20, 0x5c, 0x67, 0xb8, 0x36, 0xed,
+ 0x52, 0xca, 0x04, 0x19, 0xbe, 0x46, 0xdc, 0x34, 0x7a, 0x30, 0x30, 0x71,
+ 0x98, 0xf5, 0x50, 0xb2, 0x46, 0xc9, 0x85, 0x89, 0x74, 0xf7, 0xb1, 0x6f,
+ 0x7d, 0xc7, 0xee, 0x6d, 0x07, 0xf0, 0x87, 0x29, 0xc2, 0x3c, 0x06, 0x1f,
+ 0x4b, 0xc2, 0x38, 0xbc, 0x5a, 0x31, 0x7a, 0x1b, 0x22, 0x23, 0x56, 0x8e,
+ 0x38, 0x75, 0x88, 0x8e, 0x9b, 0x47, 0xe7, 0x7d, 0xd9, 0xee, 0x84, 0xec,
+ 0xf7, 0x31, 0x05, 0x68, 0xce, 0x3c, 0x53, 0x73, 0xaa, 0xc5, 0xfc, 0xfe,
+ 0x1d, 0x18, 0xef, 0x66, 0xe1, 0x87, 0x05, 0xf9, 0xbb, 0xd5, 0x02, 0x32,
+ 0x46, 0xfc, 0x49, 0xaf, 0xd5, 0xa3, 0x13, 0x48, 0xbc, 0x0d, 0x05, 0xce,
+ 0xd3, 0xb3, 0xed, 0xff, 0x04, 0xd1, 0xa0, 0xda, 0xf0, 0x8d, 0xcf, 0xb0,
+ 0x9f, 0xd3, 0x57, 0x6e, 0x8a, 0x9a, 0x48, 0x6c, 0x1c, 0x99, 0x3c, 0xf0,
+ 0xcf, 0x24, 0x93, 0x45, 0xcc, 0x56, 0x6e, 0x6b, 0x71, 0x73, 0x33, 0x37,
+ 0x19, 0x36, 0x93, 0xef, 0xea, 0x99, 0x64, 0x6a, 0x39, 0x9a, 0x8f, 0xdb,
+ 0xe6, 0xb8, 0x93, 0x2b, 0xdf, 0xdd, 0x96, 0x98, 0x39, 0x5c, 0x46, 0x3d,
+ 0x1c, 0x53, 0x93, 0xf4, 0x9f, 0x59, 0x36, 0x20, 0xad, 0x9f, 0x5e, 0x40,
+ 0x74, 0x98, 0x46, 0x6d, 0x17, 0x35, 0xf6, 0x6a, 0x91, 0x71, 0x55, 0xfd,
+ 0xac, 0x79, 0xce, 0x31, 0x95, 0x72, 0xa5, 0xb4, 0xf6, 0xd0, 0xa2, 0x44,
+ 0x38, 0xf0, 0x6a, 0xba, 0xfc, 0xcd, 0xef, 0x5d, 0x5e, 0xf0, 0x8e, 0xfa,
+ 0xfc, 0x4a, 0x23, 0x83, 0x3a, 0x49, 0x4a, 0xd4, 0x12, 0xce, 0x4e, 0x19,
+ 0xe9, 0x2e, 0x59, 0x31, 0xce, 0x00, 0x03, 0xe1, 0x93, 0xc9, 0x9e, 0x42,
+ 0x94, 0x21, 0x23, 0xb0, 0x2f, 0x76, 0x9b, 0x64, 0xb1, 0x6d, 0xd3, 0x44,
+ 0x64, 0x0f, 0x0f, 0xf6, 0xde, 0x08, 0xb2, 0xb1, 0xf8, 0xa4, 0x54, 0xdc,
+ 0x7f, 0xf3, 0xd2, 0xc0, 0x61, 0x5c, 0x62, 0x60, 0x82, 0xca, 0x14, 0x31,
+ 0x4a, 0x05, 0xa0, 0xb5, 0xc4, 0xb0, 0xef, 0xe6, 0x75, 0xd7, 0x68, 0x5b,
+ 0xe8, 0x79, 0x5f, 0xa9, 0xe6, 0xfa, 0x3e, 0x7f, 0x36, 0x74, 0x7a, 0x1b,
+ 0x84, 0x09, 0x89, 0xac, 0x1b, 0x99, 0x17, 0x7f, 0xf8, 0x5b, 0x40, 0x1d,
+ 0xb2, 0x82, 0xe4, 0x2d, 0xbf, 0x4b, 0x99, 0xa1, 0x5f, 0x39, 0x10, 0xf5,
+ 0x75, 0x90, 0xda, 0xfd, 0xa7, 0x37, 0x7a, 0x4a, 0x0e, 0xf8, 0xb8, 0xf3,
+ 0xc4, 0xaf, 0x3b, 0x00, 0xef, 0x6d, 0xf5, 0xcd, 0x91, 0x55, 0xe9, 0xf4,
+ 0x28, 0x69, 0x56, 0x27, 0xb5, 0x83, 0xbe, 0x0f, 0x0f, 0xce, 0x1d, 0x32,
+ 0xbd, 0x2c, 0x86, 0x86, 0x28, 0x0a, 0xee, 0x35, 0x30, 0xe5, 0xda, 0x0e,
+ 0xe6, 0xf9, 0x99, 0x53, 0x48, 0x53, 0x47, 0x9b, 0xb8, 0x29, 0x6e, 0xdd,
+ 0x50, 0x4e, 0xc7, 0x77, 0x97, 0x9b, 0x52, 0xf1, 0x33, 0xf1, 0x6f, 0x36,
+ 0x9a, 0x5c, 0x86, 0x77, 0x17, 0x6d, 0x9b, 0xd1, 0xa0, 0xc6, 0xcd, 0x90,
+ 0x45, 0xa3, 0xbc, 0x5f, 0x75, 0x81, 0xc5, 0x02, 0x6d, 0xbf, 0x6f, 0xca,
+ 0x90, 0x7e, 0x77, 0x5a, 0x62, 0x04, 0x52, 0x91, 0x03, 0xd8, 0x8c, 0x6e,
+ 0x0b, 0x79, 0x61, 0xb7, 0x30, 0xac, 0xab, 0xc1, 0xdb, 0x6a, 0x93, 0x33,
+ 0xc9, 0xb4, 0x11, 0x5a, 0x00, 0xdc, 0x74, 0xe9, 0x64, 0xf1, 0x90, 0xa4,
+ 0x54, 0x0b, 0xa1, 0xbe, 0xa1, 0xe6, 0x96, 0x66, 0xe5, 0x32, 0xfc, 0x1c,
+ 0xd7, 0x65, 0x58, 0xaf, 0x61, 0xeb, 0xe1, 0x3c, 0x9d, 0xec, 0xbe, 0xf1,
+ 0xb8, 0x4d, 0x17, 0xc6, 0x40, 0xed, 0x63, 0xad, 0x65, 0xe8, 0x83, 0x9f,
+ 0x71, 0xa4, 0x78, 0x5d, 0x7c, 0xee, 0x43, 0xfd, 0x0f, 0xb6, 0x7e, 0x27,
+ 0x5e, 0x91, 0x04, 0xc5, 0x81, 0x11, 0x26, 0x66, 0xc2, 0x10, 0xd1, 0xe7,
+ 0x74, 0x12, 0x2f, 0xcf, 0xfb, 0xc1, 0xfd, 0xc2, 0x44, 0x21, 0x63, 0xab,
+ 0xa3, 0xb5, 0x28, 0x74, 0xf5, 0xd7, 0x38, 0xfe, 0x5d, 0x64, 0x89, 0x48,
+ 0xbb, 0x84, 0x9f, 0x7e, 0x6b, 0xe4, 0x69, 0x3b, 0xf2, 0xf7, 0x7e, 0xae,
+ 0x4e, 0x1a, 0x05, 0xed, 0x62, 0xf0, 0xc7, 0xf6, 0xd3, 0x86, 0xf9, 0xb5,
+ 0x95, 0x5d, 0x2c, 0xfd, 0x94, 0x0e, 0xa7, 0x17, 0x96, 0x5b, 0xeb, 0x47,
+ 0x8d, 0x86, 0x1a, 0xd3, 0x1a, 0x16, 0x65, 0x56, 0x61, 0x80, 0x89, 0x44,
+ 0x39, 0x7a, 0xe9, 0x62, 0x56, 0x76, 0x5b, 0xe0, 0xc3, 0x71, 0xb0, 0xef,
+ 0x03, 0x3b, 0x17, 0x44, 0xa5, 0x29, 0xb8, 0x87, 0x55, 0xfe, 0x0a, 0x69,
+ 0x9e, 0x57, 0x71, 0xd5, 0x15, 0xdd, 0xd0, 0xc5, 0xa3, 0xfd, 0x5e, 0x42,
+ 0x4e, 0x03, 0x7c, 0xc6, 0x19, 0x85, 0xf2, 0x50, 0xbe, 0xd4, 0xbf, 0x82,
+ 0x3a, 0xba, 0x5c, 0xee, 0x06, 0xe2, 0xc1, 0x8b, 0xe6, 0x00, 0xed, 0x7a,
+ 0x67, 0x7e, 0x2a, 0xad, 0xc8, 0xaa, 0x1d, 0x9a, 0x18, 0x40, 0x84, 0x04,
+ 0x8b, 0x56, 0xa5, 0xed, 0x0a, 0xcc, 0xe5, 0x3e, 0xb8, 0x84, 0x1e, 0xb5,
+ 0x2e, 0x20, 0xe7, 0xb0, 0x45, 0x1e, 0x4d, 0xe9, 0xe0, 0x84, 0xf5, 0x80,
+ 0xdb, 0xcb, 0x1f, 0x5d, 0x49, 0xa1, 0x25, 0xf9, 0x62, 0x63, 0x4c, 0x72,
+ 0x0a, 0x71, 0x6f, 0x2b, 0x30, 0x15, 0x65, 0x2e, 0x2c, 0xa5, 0x34, 0x74,
+ 0xca, 0xe2, 0x0a, 0x54, 0x6f, 0x56, 0xdf, 0x42, 0x32, 0xe4, 0x53, 0x0c,
+ 0x3b, 0xa6, 0x6f, 0x1f, 0x9d, 0xd6, 0xf2, 0x59, 0x25, 0x11, 0x6a, 0xf8,
+ 0x1c, 0xbf, 0xff, 0x35, 0xe8, 0xdf, 0xdd, 0x4d, 0xf2, 0x49, 0x5b, 0x8d,
+ 0xee, 0xdd, 0x9e, 0xdb, 0xd1, 0x18, 0x6c, 0xd3, 0x86, 0xd0, 0x43, 0xf0,
+ 0x0d, 0xa0, 0x71, 0xe6, 0xb4, 0xbe, 0xdb, 0x92, 0xac, 0x92, 0xf7, 0xd0,
+ 0xa4, 0x30, 0x31, 0xbb, 0xe4, 0x04, 0x42, 0xf9, 0xd2, 0x86, 0x31, 0x4c,
+ 0xe4, 0xb9, 0xe1, 0x92, 0x99, 0x99, 0x28, 0xac, 0xc3, 0x91, 0xe1, 0x0e,
+ 0x82, 0xb3, 0x38, 0x76, 0x3f, 0x1c, 0x79, 0x7b, 0xcc, 0xf4, 0xaf, 0x46,
+ 0xc8, 0xc3, 0x36, 0xdb, 0x34, 0x30, 0x79, 0x6c, 0x64, 0xd4, 0xbb, 0x01,
+ 0xc4, 0x1f, 0x9e, 0xa2, 0x63, 0xa6, 0xf0, 0xf8, 0x74, 0x61, 0x99, 0xde,
+ 0xb5, 0x75, 0x02, 0x74, 0x94, 0x78, 0x38, 0xf2, 0xed, 0xe4, 0xf0, 0xa9,
+ 0x1c, 0x7e, 0x6d, 0x28, 0xf5, 0x0c, 0xb9, 0x4e, 0x54, 0x74, 0x04, 0x52,
+ 0xba, 0x05, 0x70, 0xae, 0x3b, 0x0d, 0x90, 0x44, 0x46, 0x9d, 0xba, 0x41,
+ 0xb6, 0x37, 0x7c, 0xe8, 0x32, 0x0b, 0xca, 0xe4, 0x0c, 0xb2, 0xc2, 0x3d,
+ 0x6e, 0x22, 0x2e, 0x74, 0x6f, 0x18, 0xc8, 0xa3, 0x05, 0x96, 0x9a, 0x65,
+ 0xc0, 0x4f, 0x90, 0xf9, 0xb7, 0x49, 0x8a, 0x85, 0x81, 0x88, 0x60, 0x71,
+ 0xa9, 0xdd, 0x19, 0xf6, 0x3e, 0xf0, 0x2e, 0xb0, 0x77, 0xdc, 0x88, 0xf8,
+ 0x6d, 0x49, 0x73, 0xf8, 0x64, 0xa3, 0x56, 0xcb, 0x75, 0x20, 0xfc, 0x72,
+ 0x7c, 0x0b, 0x59, 0x96, 0x27, 0x1c, 0xcb, 0x9a, 0x94, 0x4a, 0xb8, 0xc9,
+ 0xb2, 0x6e, 0x98, 0x80, 0xfc, 0xb2, 0x18, 0x67, 0x9a, 0xff, 0x98, 0x01,
+ 0x33, 0x47, 0x3f, 0x96, 0x08, 0xde, 0xe4, 0xd3, 0x38, 0x8f, 0x00, 0x03,
+ 0xd2, 0xb1, 0xed, 0xc5, 0x1e, 0x71, 0x09, 0x00, 0x3b, 0x1a, 0x5e, 0x21,
+ 0x61, 0x23, 0x43, 0x6c, 0x8b, 0xa2, 0xfa, 0x6a, 0xf9, 0x7b, 0xce, 0xd1,
+ 0x36, 0xc8, 0x48, 0x81, 0x65, 0xd9, 0x3a, 0x96, 0x15, 0x82, 0x11, 0xe2,
+ 0x11, 0xf8, 0x4e, 0x18, 0x1a, 0xfa, 0xd1, 0x55, 0x6c, 0x82, 0xd2, 0x3e,
+ 0x56, 0xea, 0x5f, 0xe6, 0xb6, 0x1c, 0x1b, 0xe1, 0x67, 0x79, 0x4e, 0x30,
+ 0x11, 0xac, 0xae, 0xe2, 0x27, 0x41, 0x27, 0x7d, 0x11, 0x4a, 0xe7, 0xc4,
+ 0xc5, 0xe9, 0x02, 0xba, 0x3b, 0x8b, 0x1f, 0xf0, 0xa0, 0xf8, 0x9c, 0x78,
+ 0x5c, 0xfb, 0xc0, 0x82, 0x12, 0xf5, 0xdb, 0x4f, 0xdd, 0xd4, 0x76, 0x88,
+ 0x7c, 0x4a, 0xe6, 0xe5, 0x1b, 0x07, 0x01, 0xe4, 0x08, 0xcc, 0x12, 0x09,
+ 0xfb, 0x9f, 0x78, 0x85, 0x82, 0x7a, 0xf8, 0x39, 0x03, 0x52, 0xa9, 0xc6,
+ 0x1c, 0x13, 0x58, 0xf1, 0x38, 0x53, 0x09, 0x98, 0x3a, 0xc6, 0xcd, 0xff,
+ 0xa1, 0x7b, 0xb7, 0x54, 0x0b, 0x93, 0x8e, 0x75, 0x83, 0x6b, 0x4e, 0xea,
+ 0x05, 0x7f, 0xf1, 0x03, 0x58, 0xde, 0x0e, 0x6b, 0xba, 0x1c, 0x58, 0x11,
+ 0xa0, 0x90, 0x6a, 0xde, 0xfc, 0x1a, 0x8c, 0xbf, 0x7b, 0xd2, 0x76, 0xef,
+ 0x79, 0xf3, 0xf9, 0x4e, 0x4c, 0xfd, 0xa2, 0x5b, 0xcf, 0xf7, 0xc5, 0x69,
+ 0x08, 0xec, 0xd4, 0x47, 0xbe, 0x7a, 0x05, 0x23, 0x1f, 0x45, 0x56, 0x9d,
+ 0x1a, 0x61, 0x9d, 0xa6, 0xe9, 0x93, 0x18, 0xc6, 0x4b, 0x84, 0x68, 0x28,
+ 0x7e, 0x15, 0xbc, 0x63, 0xb9, 0x5f, 0x8c, 0x82, 0x6b, 0x84, 0xf1, 0x48,
+ 0x30, 0xcf, 0x2b, 0xe1, 0xa0, 0x5b, 0x70, 0xb4, 0x07, 0xe6, 0x28, 0xc2,
+ 0x57, 0x9c, 0xf4, 0xe8, 0xc3, 0xc8, 0x9c, 0xd1, 0x37, 0x1f, 0xac, 0xfa,
+ 0x8e, 0x12, 0x9b, 0x87, 0x5b, 0x41, 0x86, 0x61, 0x41, 0xe9, 0x39, 0x2a,
+ 0x14, 0x1d, 0x23, 0xd1, 0x65, 0xc7, 0x18, 0x71, 0x0c, 0xff, 0x7a, 0x55,
+ 0xef, 0xc9, 0x55, 0xe6, 0xec, 0xe5, 0x4c, 0xbd, 0x97, 0xe3, 0xf3, 0x54,
+ 0x3e, 0x02, 0xce, 0xeb, 0xa1, 0xf6, 0x09, 0x16, 0x95, 0x81, 0x7c, 0xbc,
+ 0xf7, 0xa0, 0x6a, 0x8f, 0xd5, 0x80, 0xb0, 0xcd, 0x6d, 0x34, 0x63, 0xb2,
+ 0xb4, 0xe3, 0xd3, 0xd9, 0x77, 0x43, 0xf1, 0x7f, 0xdb, 0x4e, 0x3f, 0x5c,
+ 0xe0, 0x9e, 0x1f, 0x43, 0xff, 0x4d, 0x31, 0xf3, 0x96, 0xeb, 0x0f, 0xf0,
+ 0x61, 0xf3, 0x57, 0xa1, 0x5a, 0x55, 0xdc, 0xee, 0xef, 0x34, 0xef, 0x0d,
+ 0xa2, 0x50, 0xa5, 0x5a, 0xb8, 0x28, 0x02, 0x3e, 0x64, 0xb2, 0xa9, 0x22,
+ 0xc0, 0x23, 0x04, 0x0d, 0x6d, 0x1f, 0x26, 0x43, 0xe5, 0x1d, 0x1c, 0xe7,
+ 0x48, 0xfe, 0x15, 0x85, 0xc5, 0x6a, 0x5f, 0x64, 0x31, 0xff, 0x66, 0xdf,
+ 0x0b, 0x87, 0x84, 0xad, 0x7d, 0xb3, 0x31, 0x49, 0xa1, 0xb9, 0x9c, 0xc1,
+ 0x28, 0xa7, 0x43, 0xba, 0x1c, 0xcd, 0x1a, 0x20, 0x42, 0xd8, 0xd0, 0xba,
+ 0xc7, 0x0d, 0xa8, 0x34, 0x79, 0xc6, 0xd5, 0x2a, 0xa9, 0x8d, 0x16, 0xe0,
+ 0xfa, 0x75, 0xc1, 0xfb, 0x20, 0x10, 0xb7, 0xe4, 0x3f, 0xb4, 0xde, 0x16,
+ 0x43, 0xb1, 0x6f, 0xc9, 0x2f, 0xd8, 0xa2, 0xca, 0x3b, 0x45, 0xbe, 0x86,
+ 0x65, 0x76, 0x8e, 0x25, 0xdb, 0xa4, 0x52, 0xf1, 0xc3, 0xe5, 0x58, 0xef,
+ 0x82, 0x90, 0x7a, 0x37, 0xfc, 0x16, 0xcb, 0xb1, 0xa8, 0x10, 0x73, 0x6d,
+ 0x51, 0x98, 0x50, 0x86, 0x7d, 0xe5, 0x79, 0x86, 0x4c, 0x6f, 0xfb, 0x93,
+ 0xb9, 0xbd, 0x35, 0x50, 0x93, 0x22, 0x99, 0x32, 0x17, 0x75, 0x26, 0xbd,
+ 0x7b, 0x11, 0xff, 0xb1, 0x51, 0x6c, 0x74, 0xc5, 0x8b, 0xa6, 0xda, 0xe7,
+ 0x4b, 0x3f, 0x01, 0x36, 0x6e, 0x97, 0xf1, 0x73, 0x28, 0xcd, 0x4b, 0x3a,
+ 0xcf, 0xb5, 0xb6, 0xf7, 0xc1, 0xdb, 0xcb, 0xce, 0xf7, 0x59, 0x25, 0xc9,
+ 0x6a, 0x3a, 0x60, 0x9c, 0xd4, 0x7d, 0xe1, 0x33, 0x9f, 0x8f, 0xac, 0x62,
+ 0xa8, 0xaa, 0x18, 0xc5, 0xed, 0x2e, 0xba, 0x68, 0xf4, 0x45, 0xd8, 0xbf,
+ 0xed, 0x69, 0xd1, 0x00, 0x2d, 0x91, 0x74, 0x13, 0x41, 0xaa, 0xbc, 0xc3,
+ 0x32, 0xb4, 0x8f, 0xbd, 0x6e, 0x00, 0xe2, 0x5d, 0xa2, 0xfc, 0x6c, 0x79,
+ 0x3c, 0x5b, 0x54, 0x53, 0xba, 0x57, 0x13, 0xb9, 0x54, 0x79, 0x14, 0x71,
+ 0x34, 0x61, 0x94, 0x52, 0x5e, 0x68, 0x14, 0x00, 0x87, 0x20, 0x85, 0xa9,
+ 0x85, 0xc4, 0xc8, 0x27, 0x4f, 0x9f, 0x54, 0x1d, 0x59, 0xea, 0x41, 0xc0,
+ 0x86, 0x2a, 0x51, 0x10, 0x29, 0xe6, 0x7d, 0xcd, 0x61, 0xa9, 0x35, 0x26,
+ 0x6c, 0x67, 0xe6, 0xa0, 0x00, 0xff, 0xb4, 0x17, 0x3b, 0x4f, 0xf6, 0x4a,
+ 0xd4, 0x85, 0xbe, 0x4f, 0xf9, 0xdd, 0x70, 0x9d, 0x0d, 0xc8, 0x2a, 0x91,
+ 0x21, 0x6c, 0x2b, 0xa8, 0x44, 0x0a, 0x02, 0x7d, 0x9e, 0x70, 0xfc, 0xec,
+ 0xe6, 0xd4, 0x84, 0xda, 0x1e, 0x5e, 0xab, 0x79, 0xca, 0x94, 0x6f, 0x0c,
+ 0x43, 0x05, 0x9e, 0xe7, 0x78, 0xa5, 0x49, 0x80, 0xd1, 0xa1, 0x49, 0x31,
+ 0x0d, 0x64, 0xd0, 0x6a, 0x64, 0xfb, 0x9a, 0xce, 0xa4, 0x64, 0x4c, 0x48,
+ 0x97, 0xe4, 0x44, 0x74, 0x71, 0x36, 0xdc, 0x00, 0x46, 0x02, 0x78, 0xbc,
+ 0x24, 0x48, 0xc9, 0x86, 0x0c, 0xdb, 0xa0, 0x53, 0x8d, 0xd7, 0x45, 0xab,
+ 0x9c, 0xf4, 0x03, 0xcd, 0x23, 0xb4, 0x6e, 0x7f, 0xec, 0xf0, 0x37, 0xb1,
+ 0x7b, 0x2f, 0x11, 0x0c, 0xac, 0x8c, 0x65, 0x83, 0x14, 0x52, 0x42, 0xe4,
+ 0x23, 0x46, 0x56, 0xd6, 0x49, 0xe4, 0xd7, 0xff, 0xc4, 0x62, 0xf2, 0x42,
+ 0x4a, 0x34, 0xb1, 0xb3, 0xaa, 0x72, 0x02, 0xff, 0x62, 0x3d, 0x00, 0xa9,
+ 0x70, 0x15, 0xbc, 0x6b, 0xc4, 0x79, 0x37, 0xca, 0x81, 0xdb, 0xf2, 0x19,
+ 0x96, 0x94, 0xa0, 0x94, 0x29, 0x94, 0xff, 0xec, 0x08, 0x92, 0x05, 0x9d,
+ 0x92, 0x42, 0x7c, 0x78, 0xa2, 0x55, 0xf7, 0xee, 0xa3, 0x63, 0xef, 0x10,
+ 0xfd, 0x64, 0x15, 0xa7, 0xd3, 0xae, 0x29, 0x08, 0x2b, 0xd7, 0x5d, 0x72,
+ 0x90, 0xd7, 0x24, 0x6b, 0x78, 0xac, 0xa8, 0xf7, 0x1c, 0x66, 0xdb, 0x14,
+ 0x50, 0x17, 0xbd, 0xe0, 0x5c, 0xfd, 0x6a, 0x03, 0x96, 0x45, 0x03, 0xf1,
+ 0x34, 0x2c, 0x8e, 0x7b, 0xc2, 0x03, 0x6b, 0x44, 0xf6, 0x20, 0x87, 0x71,
+ 0x22, 0x89, 0x7a, 0xba, 0x14, 0x39, 0x9f, 0xe4, 0x01, 0x15, 0x9e, 0x97,
+ 0x6a, 0x28, 0x1e, 0x1a, 0x27, 0x15, 0x16, 0x40, 0x01, 0xf8, 0x21, 0xcf,
+ 0x75, 0x41, 0xfd, 0x48, 0x7c, 0xb6, 0xf1, 0xae, 0x75, 0x02, 0x59, 0xfd,
+ 0xf1, 0x29, 0xaf, 0x8b, 0xba, 0xd4, 0x1e, 0xe3, 0x92, 0xa3, 0xda, 0x52,
+ 0x09, 0xb1, 0x56, 0x79, 0x20, 0x24, 0x31, 0x2d, 0xf6, 0x54, 0x54, 0x39,
+ 0x1d, 0x7d, 0x6a, 0xb7, 0x23, 0x41, 0x0d, 0x00, 0x31, 0xbe, 0x7d, 0x04,
+ 0x57, 0x0f, 0x13, 0x4f, 0xf6, 0x60, 0xdd, 0xff, 0x7d, 0x28, 0x3a, 0xa9,
+ 0x37, 0xbc, 0x02, 0x5b, 0x6f, 0x0f, 0x3d, 0xdc, 0x4c, 0xe9, 0x30, 0x38,
+ 0x5a, 0x39, 0xbd, 0xd9, 0x2e, 0xc1, 0x69, 0x6b, 0xf3, 0x2d, 0xa1, 0xbd,
+ 0x59, 0x9a, 0x06, 0x6c, 0x7a, 0x70, 0x03, 0x0b, 0xbc, 0xbb, 0x71, 0xc1,
+ 0xa2, 0x59, 0x1c, 0x9d, 0x26, 0x8b, 0xfe, 0x10, 0x97, 0x0d, 0xf7, 0x50,
+ 0xb6, 0x9e, 0x03, 0x58, 0x20, 0xd6, 0x40, 0x60, 0xd2, 0x77, 0x83, 0x90,
+ 0x3d, 0xbe, 0xae, 0x6a, 0x53, 0x32, 0x72, 0x60, 0xde, 0xf8, 0x0f, 0x5f,
+ 0xb7, 0x98, 0xbf, 0x97, 0x87, 0x6f, 0x17, 0x80, 0x0d, 0x68, 0xb0, 0x66,
+ 0x18, 0x2a, 0x5c, 0x9a, 0x34, 0xcb, 0xe1, 0x7c, 0xc1, 0xea, 0xa7, 0xee,
+ 0xa4, 0x19, 0x9f, 0xd9, 0x49, 0x42, 0x75, 0x6b, 0x1d, 0x91, 0xef, 0x50,
+ 0x46, 0x6a, 0x90, 0x02, 0x4e, 0x35, 0xf2, 0x45, 0x04, 0xa7, 0xd1, 0x5c,
+ 0x9a, 0x2d, 0x09, 0x61, 0x01, 0xf7, 0x81, 0xdc, 0x06, 0xb8, 0xfd, 0x54,
+ 0x46, 0x93, 0xb6, 0x7c, 0xa0, 0x3e, 0x56, 0xa8, 0xbf, 0x0f, 0x7c, 0x82,
+ 0xf9, 0x5c, 0x01, 0xdd, 0x91, 0x66, 0xb4, 0x16, 0x6b, 0xd7, 0x67, 0xd4,
+ 0x29, 0xf9, 0xe2, 0x40, 0xd8, 0x6c, 0x8d, 0xd1, 0x5e, 0x27, 0x1d, 0xfc,
+ 0x70, 0x05, 0x0d, 0xf2, 0x5d, 0x84, 0x06, 0x28, 0x52, 0x01, 0x0a, 0x9c,
+ 0xfb, 0xb2, 0x76, 0x49, 0x85, 0x61, 0x59, 0xbc, 0x08, 0x59, 0xff, 0x1f,
+ 0x32, 0x40, 0xd8, 0x04, 0xae, 0x0f, 0xce, 0xdc, 0x63, 0x43, 0xf8, 0x86,
+ 0xfe, 0xac, 0x80, 0x26, 0xe5, 0x82, 0x0a, 0xb1, 0x9f, 0xc5, 0xb7, 0x4f,
+ 0xfb, 0x72, 0x74, 0x28, 0x68, 0x43, 0x1d, 0x09, 0x59, 0xab, 0xe2, 0x11,
+ 0x6f, 0x03, 0xbf, 0xd8, 0x43, 0xc8, 0x90, 0x56, 0xbd, 0x9d, 0x81, 0x92,
+ 0xe0, 0x7e, 0x18, 0x14, 0xb2, 0x54, 0xb3, 0x53, 0x8b, 0x98, 0x13, 0x0c,
+ 0x1e, 0x15, 0x04, 0x0e, 0x0b, 0x9b, 0x6c, 0xdc, 0x5b, 0x81, 0xae, 0xab,
+ 0xa4, 0x44, 0xc7, 0x2f, 0x4d, 0x58, 0xc1, 0x08, 0xaa, 0xb1, 0xd4, 0x2c,
+ 0x93, 0xc7, 0x2c, 0xa5, 0xaf, 0x70, 0x51, 0xac, 0x23, 0xd4, 0x5d, 0xe2,
+ 0x52, 0xc8, 0x9f, 0xaa, 0xc8, 0xfd, 0x2f, 0x87, 0xe2, 0xaf, 0x99, 0xa8,
+ 0x64, 0x0b, 0xb4, 0x40, 0x69, 0x1a, 0x1f, 0xfd, 0xbc, 0xb7, 0xd1, 0x2f,
+ 0xa8, 0x89, 0x1c, 0xb9, 0x16, 0x24, 0xb0, 0xd0, 0x01, 0xdd, 0x9f, 0xdc,
+ 0xcc, 0x82, 0x0f, 0x1e, 0x7d, 0xc4, 0x65, 0x2d, 0x42, 0x94, 0x33, 0xcf,
+ 0xe2, 0xbe, 0x56, 0x70, 0xdd, 0x18, 0x64, 0xa5, 0x2e, 0xb1, 0x60, 0x1d,
+ 0x76, 0xca, 0x2b, 0x52, 0x06, 0x28, 0x10, 0x00, 0x01, 0x5c, 0x78, 0x81,
+ 0x7a, 0x7b, 0x06, 0x0b, 0x1b, 0x0e, 0x28, 0x41, 0xde, 0x97, 0x22, 0xf3,
+ 0xfe, 0x50, 0x2c, 0xd3, 0xff, 0xc8, 0xc2, 0x10, 0xef, 0x20, 0x3d, 0x91,
+ 0x53, 0x6b, 0x50, 0x1d, 0x8f, 0x73, 0x1a, 0x77, 0x24, 0x55, 0x1b, 0xf3,
+ 0x2c, 0xc5, 0xea, 0xd2, 0x1e, 0x43, 0x2d, 0x1c, 0x55, 0x3c, 0xaf, 0xab,
+ 0x7a, 0xd3, 0xfb, 0x9c, 0xb3, 0xd9, 0x9a, 0x62, 0xb2, 0x15, 0x38, 0x2c,
+ 0x71, 0x68, 0x3b, 0xfc, 0x0e, 0x35, 0xf1, 0x4c, 0xe1, 0x00, 0x76, 0x8e,
+ 0x93, 0xf5, 0x3f, 0x5f, 0x8a, 0x1a, 0xed, 0xda, 0x19, 0x40, 0x83, 0xca,
+ 0x6e, 0xfe, 0xde, 0x65, 0x0d, 0x84, 0xa1, 0xdf, 0x4d, 0x87, 0x97, 0x00,
+ 0xcf, 0x3f, 0xd3, 0x64, 0xcf, 0x87, 0xad, 0xa8, 0xd4, 0xb0, 0x12, 0x9c,
+ 0xfd, 0x82, 0xaa, 0xa5, 0x10, 0xe8, 0xef, 0x94, 0xb9, 0x5c, 0x6b, 0x7e,
+ 0x4b, 0x28, 0x1f, 0x1a, 0x68, 0xcd, 0xe1, 0xb7, 0x6f, 0xa8, 0xce, 0x2f,
+ 0xcc, 0x99, 0x31, 0xc7, 0x59, 0xd7, 0x6b, 0x2b, 0x49, 0x51, 0xb3, 0x70,
+ 0x77, 0x32, 0xcf, 0xb4, 0x64, 0x84, 0xbd, 0xba, 0x3e, 0x78, 0xda, 0x0b,
+ 0x1d, 0x7d, 0xd6, 0x28, 0xbd, 0x68, 0x7a, 0x29, 0x1d, 0xbe, 0x2a, 0x1f,
+ 0xc5, 0xdf, 0x87, 0xa0, 0x0e, 0xfe, 0x0b, 0xed, 0x67, 0xf7, 0x80, 0xf5,
+ 0xcd, 0xd2, 0x6a, 0xf8, 0x10, 0x39, 0x87, 0x24, 0x9a, 0xa5, 0x2f, 0xcf,
+ 0x55, 0xed, 0xcb, 0x7d, 0xf4, 0xfe, 0xa6, 0x10, 0xb5, 0x6b, 0x4f, 0x1c,
+ 0x1f, 0xfd, 0xde, 0xfa, 0x66, 0x64, 0x9d, 0x3d, 0x21, 0xa1, 0x6f, 0xe1,
+ 0xd4, 0x28, 0x62, 0x9f, 0x43, 0xb2, 0xa8, 0xf0, 0xd7, 0xc1, 0xfe, 0xb9,
+ 0xc9, 0x30, 0x9a, 0xbb, 0x43, 0xa5, 0xb3, 0xd0, 0x48, 0x89, 0xa7, 0xdc,
+ 0x5f, 0x0e, 0xa3, 0xf0, 0xf8, 0x49, 0xcf, 0xf1, 0x26, 0xff, 0xdb, 0x10,
+ 0xe8, 0xc0, 0x08, 0x91, 0xd2, 0xb2, 0x4f, 0x0b, 0x3a, 0xb9, 0xd0, 0xf2,
+ 0x62, 0x86, 0xd5, 0xac, 0x1c, 0x3a, 0x0e, 0x01, 0xa7, 0x55, 0xfc, 0x99,
+ 0xa6, 0x3d, 0xb4, 0x2f, 0x1b, 0x0c, 0xbf, 0xe1, 0x47, 0x5b, 0xb6, 0x05,
+ 0x9e, 0x7d, 0xf2, 0x9f, 0x82, 0x6a, 0x3e, 0x67, 0x43, 0xe6, 0xa5, 0x82,
+ 0xf2, 0x75, 0x87, 0xd2, 0xf3, 0x1e, 0x9a, 0xd1, 0xd6, 0x6d, 0x85, 0x6c,
+ 0xb4, 0xd8, 0xfa, 0x53, 0xdc, 0x17, 0x96, 0x8c, 0xfd, 0xb5, 0xb8, 0xf4,
+ 0x21, 0x96, 0x0a, 0x4f, 0xf2, 0x03, 0xee, 0xee, 0x60, 0x30, 0xd0, 0x4d,
+ 0x23, 0xa2, 0xb7, 0x3e, 0xa9, 0xc5, 0x77, 0x82, 0x21, 0x08, 0x0f, 0x25,
+ 0x2f, 0xf0, 0xe0, 0xe0, 0x00, 0x00, 0x01, 0x14, 0x66, 0x66, 0xc6, 0x62,
+ 0x51, 0xf1, 0xbc, 0x6b, 0x72, 0xb6, 0xa1, 0xb9, 0x98, 0x19, 0x03, 0x79,
+ 0xde, 0xe6, 0x12, 0xab, 0x7b, 0xf8, 0x06, 0x43, 0x30, 0x03, 0xe3, 0x83,
+ 0xa3, 0x19, 0x5d, 0xde, 0xd8, 0x3f, 0x90, 0x81, 0x78, 0xc6, 0x74, 0x88,
+ 0xce, 0x8b, 0xbc, 0x1f, 0x95, 0xa1, 0x27, 0x6b, 0xfa, 0x4d, 0x0f, 0xe2,
+ 0xa8, 0xab, 0x03, 0xdc, 0x32, 0xeb, 0xed, 0x8e, 0xd2, 0x36, 0x51, 0x21,
+ 0x1e, 0x89, 0xb3, 0xfb, 0x87, 0x47, 0x34, 0xf0, 0x28, 0x62, 0x19, 0x26,
+ 0xb4, 0x5f, 0xfb, 0x51, 0x37, 0xe2, 0x64, 0xdf, 0x34, 0x56, 0xb6, 0x15,
+ 0x29, 0xb8, 0x9d, 0xef, 0xc1, 0x31, 0xa4, 0xc5, 0x84, 0x0e, 0x2a, 0x4d,
+ 0x46, 0x6a, 0xc0, 0xfd, 0xd4, 0xde, 0xae, 0xcb, 0x58, 0x30, 0xa2, 0xd2,
+ 0x2d, 0x87, 0xe9, 0x6d, 0xa4, 0xff, 0xe6, 0xeb, 0xd2, 0x90, 0x50, 0x27,
+ 0xbb, 0xf5, 0x93, 0xb8, 0x78, 0x6e, 0x59, 0x9e, 0xdf, 0x67, 0x4d, 0x67,
+ 0xa6, 0x7d, 0xe6, 0x27, 0x4b, 0x8b, 0x1e, 0x8e, 0x7b, 0xc3, 0x5e, 0xbb,
+ 0x0b, 0xb3, 0x0b, 0x7c, 0xa7, 0xea, 0x38, 0x63, 0x79, 0x0e, 0x3e, 0xf2,
+ 0x14, 0x23, 0xa8, 0xa0, 0x2a, 0x04, 0x6f, 0xac, 0x68, 0xe0, 0xe0, 0xbb,
+ 0x5a, 0xc3, 0x32, 0x91, 0xe8, 0x49, 0x08, 0x7b, 0x99, 0x55, 0x36, 0x00,
+ 0x11, 0x21, 0xea, 0x5c, 0xfe, 0x8f, 0xd7, 0x8d, 0x30, 0x91, 0x15, 0xbe,
+ 0x63, 0x19, 0x60, 0x45, 0xa2, 0xbc, 0xf5, 0xba, 0x1f, 0x57, 0xd9, 0x6e,
+ 0x5f, 0xaa, 0xbd, 0x11, 0x92, 0x6d, 0x9f, 0x97, 0x75, 0x86, 0xda, 0xbc,
+ 0x1b, 0x28, 0x8d, 0xf7, 0x05, 0xf5, 0xab, 0x99, 0xb1, 0xb5, 0xd6, 0xde,
+ 0xd0, 0x1a, 0xb3, 0xba, 0x29, 0xa6, 0x2e, 0xeb, 0xb7, 0x82, 0x77, 0x01,
+ 0xc4, 0x8d, 0x40, 0x64, 0xac, 0x36, 0x65, 0xba, 0x37, 0x99, 0xbd, 0x37,
+ 0x8d, 0xbf, 0x15, 0x8e, 0xd9, 0x41, 0x69, 0x32, 0xaf, 0x6e, 0x37, 0x35,
+ 0x6e, 0xb7, 0xa7, 0x62, 0x3b, 0x81, 0xc9, 0x4d, 0x67, 0x56, 0x19, 0x1b,
+ 0xba, 0xb7, 0x67, 0xf7, 0xe7, 0x5a, 0xa4, 0xdd, 0x78, 0xc0, 0xa8, 0x8b,
+ 0xc3, 0x46, 0x0e, 0x2d, 0x69, 0x65, 0x18, 0x50, 0xea, 0xe6, 0x24, 0x6d,
+ 0x32, 0xa3, 0x3b, 0x8d, 0x4a, 0x78, 0xa5, 0x46, 0x38, 0xfe, 0x6c, 0x35,
+ 0x20, 0x99, 0x57, 0x4b, 0xc3, 0xf7, 0xed, 0xf1, 0x5b, 0xfc, 0xa1, 0x7f,
+ 0xb5, 0xdf, 0xda, 0xc1, 0x0f, 0xa4, 0x1a, 0x74, 0x6f, 0x09, 0x07, 0x92,
+ 0x4d, 0x70, 0x89, 0x5d, 0x8f, 0xc9, 0x04, 0xf6, 0xa6, 0x52, 0xcb, 0xd3,
+ 0x23, 0xb3, 0x38, 0x82, 0xfa, 0x16, 0x6b, 0xfc, 0x47, 0x7b, 0x1c, 0x55,
+ 0xf0, 0xfb, 0x94, 0x63, 0x7b, 0x17, 0x79, 0x6f, 0x14, 0x44, 0x3c, 0x23,
+ 0x48, 0x63, 0x0c, 0x4c, 0x72, 0x06, 0x63, 0x0c, 0xdd, 0x0e, 0x62, 0x62,
+ 0x84, 0x66, 0x59, 0x08, 0xec, 0xb0, 0xdd, 0x0e, 0x6d, 0xae, 0x17, 0xa1,
+ 0x71, 0xd6, 0xcd, 0x41, 0x93, 0xfe, 0x3b, 0x36, 0x24, 0xfe, 0x18, 0x1a,
+ 0x3b, 0x39, 0x9b, 0xe0, 0xc1, 0x2d, 0xcd, 0xe7, 0x8a, 0x0f, 0xff, 0xe9,
+ 0x63, 0x56, 0xe0, 0x80, 0x6a, 0xd3, 0x4b, 0x90, 0x03, 0x47, 0x8a, 0xb1,
+ 0xd6, 0x39, 0x97, 0x34, 0x9c, 0x80, 0xc8, 0x44, 0xbe, 0x54, 0xea, 0x7d,
+ 0xa7, 0xb3, 0x79, 0xcc, 0xa0, 0xc9, 0x94, 0xdd, 0x2e, 0x93, 0xa9, 0x5c,
+ 0xe9, 0xf5, 0xe9, 0x49, 0xe6, 0x69, 0xd3, 0x8e, 0xe3, 0x69, 0x8d, 0xb1,
+ 0xdb, 0xdd, 0x88, 0x25, 0x48, 0x6a, 0x00, 0x97, 0xfa, 0x48, 0xf8, 0xbe,
+ 0x7b, 0x81, 0xb4, 0xe0, 0x9f, 0x76, 0x61, 0x7f, 0xa0, 0x57, 0xf9, 0x5d,
+ 0x5b, 0x6a, 0x43, 0x88, 0x8d, 0xd4, 0xe0, 0x43, 0x05, 0x0a, 0x5f, 0x83,
+ 0x61, 0x05, 0x12, 0xb1, 0x1f, 0x4f, 0x16, 0xa9, 0x5c, 0xa7, 0x68, 0x91,
+ 0x01, 0xdd, 0x9f, 0x85, 0x5e, 0xc2, 0x78, 0x38, 0x19, 0x27, 0x4e, 0xcc,
+ 0xaf, 0x85, 0x3a, 0x55, 0x64, 0x99, 0xd9, 0x13, 0xa8, 0x24, 0x5f, 0xfa,
+ 0xcf, 0xca, 0xf9, 0xff, 0xa0, 0xcc, 0xda, 0xe3, 0x0e, 0x98, 0xcc, 0x7c,
+ 0x1d, 0xe3, 0xad, 0xe6, 0x4f, 0xa0, 0x4b, 0xda, 0x07, 0xc9, 0xb5, 0x9d,
+ 0x68, 0x54, 0xab, 0xf5, 0xa6, 0x47, 0xcc, 0x28, 0xe0, 0x10, 0x89, 0x97,
+ 0xbf, 0xdc, 0x61, 0x92, 0x59, 0xfb, 0xad, 0x01, 0xa3, 0x85, 0x90, 0x72,
+ 0x9f, 0x3b, 0x78, 0x4f, 0x08, 0x1d, 0x25, 0x0f, 0x52, 0xdd, 0xa9, 0x0a,
+ 0x0b, 0x4d, 0x1d, 0xde, 0xbd, 0xdd, 0x8c, 0x38, 0xef, 0x5f, 0xf5, 0xbd,
+ 0x7e, 0xe3, 0xbc, 0x19, 0xf2, 0x61, 0x76, 0x46, 0xe0, 0xb9, 0xb7, 0x05,
+ 0x38, 0x60, 0x10, 0x2d, 0xb4, 0x60, 0x2d, 0x5d, 0x8b, 0x5e, 0xbb, 0x2b,
+ 0x6e, 0x68, 0x2c, 0x77, 0x25, 0xe1, 0x32, 0xac, 0x6b, 0x1b, 0xc6, 0x79,
+ 0xf1, 0x61, 0x1c, 0xa9, 0x69, 0xba, 0x54, 0x2c, 0x7a, 0x17, 0x0b, 0x54,
+ 0xa0, 0x6f, 0x69, 0x59, 0x50, 0x85, 0x54, 0x9c, 0xcd, 0x23, 0x7b, 0xb6,
+ 0x8b, 0xdf, 0xd4, 0xbb, 0xbf, 0x81, 0xe6, 0x43, 0x62, 0x9d, 0xa9, 0x1d,
+ 0x15, 0x60, 0xba, 0x3c, 0xdb, 0x8a, 0x20, 0x70, 0x04, 0xde, 0x71, 0x34,
+ 0xbc, 0xc0, 0x15, 0xd1, 0x5a, 0xbc, 0xa6, 0xba, 0xa8, 0x9e, 0x03, 0xb9,
+ 0x54, 0xa1, 0xb2, 0xac, 0xbe, 0x9b, 0x0d, 0x87, 0xfc, 0x79, 0xca, 0x07,
+ 0xc0, 0xcc, 0x10, 0x57, 0xba, 0x7e, 0x8a, 0x04, 0x41, 0x54, 0xb6, 0xa0,
+ 0x86, 0x46, 0x07, 0x4e, 0x28, 0x89, 0x68, 0xa6, 0xcd, 0x37, 0x91, 0x57,
+ 0x38, 0x2b, 0xbb, 0x6b, 0x97, 0x8f, 0xb5, 0xe3, 0xaa, 0x5c, 0xfe, 0xf7,
+ 0xa2, 0x83, 0xa0, 0x67, 0x67, 0x71, 0x19, 0x1a, 0xa3, 0x62, 0x85, 0x3a,
+ 0xce, 0x9d, 0x95, 0x7b, 0x96, 0xad, 0x2c, 0x57, 0x53, 0x5c, 0x73, 0x80,
+ 0xe8, 0xb5, 0xfc, 0xc4, 0x86, 0x87, 0x2e, 0xcd, 0x04, 0x41, 0xd5, 0x36,
+ 0xc7, 0x89, 0x70, 0x88, 0xde, 0x25, 0xb6, 0xfa, 0x07, 0xea, 0xe2, 0x0e,
+ 0x52, 0xae, 0x8d, 0x4f, 0xde, 0x8e, 0x8e, 0x2f, 0xf8, 0x13, 0x4f, 0x6c,
+ 0x83, 0xe0, 0x16, 0x54, 0x1b, 0x2b, 0x49, 0x40, 0xba, 0xf2, 0xee, 0x6c,
+ 0x38, 0x33, 0xec, 0x06, 0xab, 0x1c, 0x20, 0xf1, 0x1e, 0x18, 0x20, 0x7b,
+ 0x9f, 0xba, 0x16, 0x3a, 0x57, 0xcb, 0x06, 0xf2, 0xbd, 0x72, 0x1e, 0x74,
+ 0xee, 0x7b, 0x50, 0xd2, 0xbb, 0x22, 0xa4, 0x40, 0xfb, 0x0e, 0xa2, 0xb0,
+ 0x5d, 0xa7, 0x18, 0xfd, 0x64, 0x47, 0xec, 0x82, 0x6a, 0x3f, 0x36, 0x79,
+ 0xe6, 0x69, 0x80, 0x53, 0x6b, 0xd4, 0xa6, 0x1d, 0xd1, 0xef, 0x0d, 0xbb,
+ 0xd7, 0xc2, 0xaa, 0x10, 0x46, 0xa4, 0xf1, 0x60, 0x55, 0xb9, 0x01, 0x04,
+ 0x77, 0x65, 0x64, 0xfa, 0x29, 0x02, 0xf5, 0x3f, 0xfc, 0x3a, 0x1c, 0x9a,
+ 0xb0, 0xfc, 0x77, 0x23, 0x92, 0x76, 0x3e, 0xd2, 0xd6, 0xb3, 0x5b, 0x69,
+ 0xcf, 0x37, 0x1b, 0x0e, 0xbb, 0xe9, 0x75, 0xe4, 0x11, 0xf1, 0xaa, 0x56,
+ 0xa0, 0x62, 0xd3, 0x1b, 0x08, 0x1d, 0xa1, 0xe2, 0xce, 0xb6, 0xd4, 0xe3,
+ 0x27, 0x45, 0x3a, 0x51, 0xfa, 0xd0, 0xfd, 0xb2, 0xa3, 0xb9, 0x30, 0x46,
+ 0x5a, 0x4b, 0x2c, 0x0f, 0xa8, 0x58, 0xcd, 0x31, 0x9e, 0x5b, 0x76, 0x05,
+ 0x07, 0x94, 0xec, 0x5f, 0x75, 0xc5, 0xef, 0x46, 0x90, 0x79, 0xea, 0xd9,
+ 0xb5, 0x67, 0x89, 0x15, 0x98, 0x4c, 0x29, 0x4d, 0xdd, 0x25, 0x6b, 0x50,
+ 0x72, 0xbe, 0x07, 0x0a, 0xc4, 0x78, 0x04, 0x5f, 0x6a, 0x05, 0x82, 0xb4,
+ 0x5b, 0x16, 0xf3, 0x22, 0x2a, 0xef, 0x94, 0x01, 0xb9, 0x70, 0xb2, 0x86,
+ 0xdb, 0xe5, 0xe9, 0x12, 0x8e, 0x6a, 0x6f, 0xda, 0x49, 0xf3, 0xae, 0xf4,
+ 0x2a, 0x86, 0x1d, 0x90, 0xac, 0x40, 0xb9, 0x9b, 0xfd, 0x8c, 0x09, 0x72,
+ 0x76, 0x67, 0x14, 0x8f, 0x56, 0x16, 0x90, 0xbc, 0xb7, 0x83, 0x91, 0x01,
+ 0x9e, 0x85, 0xff, 0xba, 0x77, 0x5b, 0x34, 0xc0, 0x71, 0xfd, 0x91, 0xde,
+ 0x7d, 0xfa, 0x92, 0x96, 0xa6, 0x55, 0x7a, 0x55, 0x36, 0xcf, 0x7d, 0x33,
+ 0xf1, 0xa5, 0xc6, 0x6a, 0xda, 0x15, 0xfe, 0x9e, 0x20, 0x41, 0x99, 0xb7,
+ 0xc5, 0xf4, 0xee, 0x13, 0xe6, 0xd2, 0x75, 0xd8, 0xf9, 0x16, 0x25, 0xf2,
+ 0xab, 0xc2, 0x69, 0x08, 0x53, 0x99, 0x5f, 0x0f, 0x22, 0x25, 0x50, 0x4b,
+ 0x6d, 0xd2, 0x78, 0xd5, 0xca, 0xcc, 0x04, 0xc0, 0xcf, 0xab, 0x37, 0xc8,
+ 0x1d, 0x30, 0x1f, 0x22, 0xec, 0x08, 0xbe, 0xb6, 0x5f, 0x1a, 0xa4, 0xa7,
+ 0x48, 0x09, 0xc9, 0x41, 0x32, 0x88, 0x31, 0xbb, 0xe9, 0xd1, 0xac, 0xac,
+ 0xeb, 0x35, 0xdb, 0xf8, 0xcb, 0xc6, 0xff, 0x03, 0x0f, 0xce, 0x28, 0x91,
+ 0xd8, 0x1d, 0x8a, 0x58, 0xd1, 0x77, 0x4c, 0xc7, 0x3f, 0x8c, 0xbc, 0xbe,
+ 0x46, 0xd7, 0xc0, 0x26, 0x95, 0x2d, 0xdc, 0xce, 0xcd, 0x7c, 0x9a, 0xa1,
+ 0x1e, 0x84, 0xb8, 0x02, 0xed, 0xab, 0x34, 0x21, 0xc4, 0x8b, 0xb9, 0x47,
+ 0x79, 0x18, 0x53, 0x1c, 0xb5, 0x12, 0x24, 0xfc, 0x39, 0x94, 0x43, 0xfb,
+ 0xa8, 0x9d, 0x11, 0x24, 0x6e, 0xa9, 0x27, 0x8c, 0x2a, 0x36, 0x32, 0x79,
+ 0x19, 0xb4, 0xa5, 0x96, 0x10, 0x93, 0x81, 0x09, 0x31, 0x0c, 0x81, 0x3a,
+ 0x9d, 0xc1, 0x7f, 0xf9, 0x6e, 0xf4, 0x39, 0x30, 0x79, 0x5a, 0xc7, 0x36,
+ 0xe3, 0x2b, 0xa0, 0x94, 0x23, 0x63, 0xc0, 0x09, 0x30, 0x01, 0x60, 0xb0,
+ 0x49, 0x25, 0x3c, 0xdd, 0x5c, 0x73, 0x69, 0x8e, 0xbf, 0xd8, 0x71, 0xa3,
+ 0xbe, 0xe3, 0x42, 0x07, 0xf0, 0xca, 0x85, 0x2e, 0x93, 0xff, 0x86, 0x5f,
+ 0xd6, 0xeb, 0xa3, 0xf7, 0x60, 0x9e, 0xa9, 0x1b, 0x92, 0x33, 0xe3, 0x51,
+ 0x82, 0x6d, 0x36, 0x21, 0x61, 0xc8, 0xac, 0x27, 0xd8, 0xb3, 0x8b, 0xdf,
+ 0x24, 0xce, 0x05, 0xe5, 0x59, 0xdc, 0xd8, 0xb8, 0x55, 0x48, 0x9d, 0x1b,
+ 0x88, 0xcb, 0x19, 0x6c, 0xdc, 0x88, 0x69, 0x18, 0x49, 0xa8, 0xbb, 0x0a,
+ 0xb8, 0x02, 0x3e, 0xc4, 0xa5, 0x0e, 0xa8, 0x9e, 0x3f, 0x8f, 0xa4, 0xeb,
+ 0x4b, 0xb3, 0x32, 0x1e, 0x13, 0x33, 0xca, 0xc4, 0xa5, 0xf2, 0xdb, 0x10,
+ 0x5b, 0xd8, 0x58, 0x37, 0x3c, 0x60, 0x07, 0x45, 0x76, 0x0c, 0x56, 0xab,
+ 0x0e, 0xa4, 0x2e, 0xff, 0x89, 0x5b, 0x24, 0x31, 0x39, 0x19, 0xba, 0x3c,
+ 0x59, 0x43, 0x68, 0x90, 0xf8, 0xca, 0x9f, 0xf2, 0xe9, 0xb2, 0xf8, 0x8e,
+ 0xef, 0x74, 0x06, 0x42, 0x1b, 0x20, 0x0d, 0xa3, 0x75, 0xdc, 0x5a, 0xc1,
+ 0x01, 0x4a, 0x10, 0x9f, 0xaf, 0x2d, 0x0d, 0xce, 0xab, 0xea, 0x81, 0x1c,
+ 0x23, 0xfe, 0x34, 0x0d, 0xf8, 0xdb, 0x5e, 0x33, 0xd2, 0x89, 0xba, 0x08,
+ 0xa5, 0xb0, 0xbb, 0xc6, 0xbc, 0x60, 0x52, 0x94, 0xff, 0x6a, 0x86, 0xed,
+ 0x87, 0xb4, 0x04, 0x8a, 0x68, 0x38, 0xc2, 0x61, 0x64, 0x32, 0x8e, 0xf4,
+ 0x48, 0xe7, 0x44, 0xb4, 0xa3, 0xf1, 0x90, 0xde, 0xd6, 0x42, 0xc2, 0xb7,
+ 0x61, 0x88, 0xb0, 0x0f, 0x9d, 0xbf, 0x00, 0xf1, 0xf0, 0x04, 0x24, 0xc8,
+ 0x7b, 0x00, 0xe7, 0xee, 0x39, 0x12, 0xb4, 0x54, 0xbe, 0x32, 0xd9, 0x00,
+ 0xd0, 0x8a, 0xf3, 0xe5, 0xe4, 0x23, 0xcc, 0x12, 0x47, 0x1f, 0x44, 0x02,
+ 0x76, 0x6a, 0xdd, 0x2c, 0x50, 0x9c, 0x91, 0x0c, 0x5d, 0xc8, 0x56, 0x40,
+ 0x30, 0x34, 0x0c, 0x98, 0x3c, 0x7e, 0x83, 0x73, 0x8a, 0x34, 0xef, 0x67,
+ 0xd1, 0x68, 0x04, 0xca, 0xe5, 0x13, 0xfe, 0x8e, 0x07, 0x81, 0xa4, 0xb8,
+ 0xbe, 0x7c, 0xa0, 0xb8, 0x7f, 0x31, 0x4e, 0xf5, 0xe1, 0x35, 0xe9, 0xfb,
+ 0xf9, 0xee, 0xd3, 0xe4, 0x3c, 0x6a, 0xb6, 0xf5, 0x2b, 0x30, 0x91, 0xff,
+ 0x45, 0x34, 0xc8, 0xa0, 0x3b, 0x96, 0xcc, 0x19, 0xbf, 0x6f, 0x65, 0x43,
+ 0xf6, 0x5f, 0x98, 0x06, 0x29, 0xc9, 0xf0, 0xec, 0xfc, 0x21, 0xaa, 0x6d,
+ 0xdc, 0x69, 0xd5, 0xf1, 0x48, 0x3e, 0xda, 0xcd, 0xce, 0xb5, 0xb1, 0xda,
+ 0x63, 0xa8, 0xdb, 0x01, 0x77, 0xaf, 0x47, 0x13, 0xcf, 0xe3, 0x37, 0x0d,
+ 0x0a, 0x1d, 0xc3, 0xf2, 0xd8, 0x52, 0xe1, 0xb2, 0x57, 0x42, 0x9c, 0x01,
+ 0x9e, 0x26, 0x57, 0x54, 0xa1, 0x47, 0xfb, 0xe3, 0xc9, 0xd3, 0x77, 0xae,
+ 0x4e, 0x5f, 0x69, 0xd1, 0x64, 0x07, 0xad, 0x9e, 0xa1, 0xab, 0xdf, 0xd9,
+ 0x50, 0x84, 0x43, 0xd5, 0xf6, 0x8c, 0x8e, 0xd9, 0xb5, 0xcb, 0xfc, 0x45,
+ 0xd8, 0x22, 0xd6, 0x7f, 0xc7, 0x4e, 0x88, 0xd1, 0x7b, 0xe0, 0xd9, 0xc1,
+ 0x80, 0x11, 0x6e, 0x2e, 0x3b, 0xef, 0x2d, 0x8b, 0x89, 0xf2, 0x40, 0x4c,
+ 0x45, 0xc3, 0x2c, 0xbe, 0xad, 0x6f, 0x63, 0x27, 0x81, 0xd3, 0x9e, 0x8f,
+ 0x38, 0x4c, 0x98, 0x4b, 0xec, 0xb7, 0x2a, 0xdd, 0x27, 0xc0, 0xee, 0x80,
+ 0xd8, 0xb9, 0xfd, 0xed, 0x6e, 0xcf, 0x0b, 0x84, 0xdf, 0xfd, 0xda, 0x27,
+ 0xfd, 0x09, 0x01, 0x8f, 0x07, 0x4b, 0x98, 0x93, 0x86, 0x95, 0x4b, 0xe1,
+ 0x05, 0x0b, 0x0c, 0x74, 0xf7, 0x3d, 0x17, 0xdb, 0xbd, 0x92, 0xad, 0x06,
+ 0x07, 0x06, 0xdb, 0xdc, 0xa7, 0xe7, 0x88, 0xe5, 0x38, 0xaa, 0x94, 0xaf,
+ 0xc9, 0xc9, 0x3e, 0x41, 0xeb, 0x91, 0x7f, 0x48, 0xc5, 0xf9, 0xf9, 0x07,
+ 0x3c, 0xc3, 0xba, 0x81, 0xee, 0x61, 0x61, 0xd2, 0x89, 0x81, 0xa5, 0xb0,
+ 0x45, 0x2c, 0x60, 0xc1, 0x26, 0xb4, 0xc7, 0x0b, 0xcd, 0xae, 0x04, 0x23,
+ 0x8b, 0x76, 0xea, 0x7b, 0xd7, 0x85, 0x19, 0xbe, 0xc9, 0x33, 0xea, 0x63,
+ 0xe9, 0x6e, 0xf2, 0x53, 0xdf, 0x8e, 0xbd, 0xf5, 0xf5, 0x23, 0x18, 0x75,
+ 0x8f, 0x8a, 0xe6, 0x82, 0xe0, 0x3d, 0x16, 0xb0, 0x71, 0xcd, 0x95, 0x5e,
+ 0x8e, 0x6b, 0x75, 0xa6, 0x06, 0x0e, 0x8b, 0x58, 0x51, 0x22, 0x8e, 0xca,
+ 0x1b, 0x09, 0x3e, 0x73, 0xf9, 0x01, 0x21, 0x3b, 0x72, 0x9b, 0xa3, 0x4c,
+ 0xdc, 0x75, 0xee, 0xab, 0x78, 0x18, 0x95, 0x33, 0x94, 0xb5, 0x9b, 0x99,
+ 0xa1, 0x8a, 0x3e, 0xe5, 0x88, 0x28, 0x87, 0x2d, 0x42, 0x45, 0xcc, 0xc4,
+ 0x26, 0xf6, 0xd4, 0x92, 0xc2, 0x76, 0x34, 0x0a, 0x9b, 0x5b, 0xa8, 0x8f,
+ 0xbe, 0x74, 0x26, 0xf9, 0xf7, 0xd6, 0x8c, 0x2a, 0x55, 0x34, 0x36, 0xbf,
+ 0x7a, 0x73, 0x79, 0x2f, 0xb6, 0x71, 0x83, 0x09, 0x3a, 0x78, 0x32, 0x74,
+ 0x47, 0x5e, 0xa7, 0x1d, 0x3d, 0xf4, 0x8b, 0xe3, 0x50, 0xc0, 0xbb, 0x48,
+ 0x7f, 0xec, 0x8c, 0xca, 0x27, 0x2f, 0x28, 0x75, 0x36, 0x4d, 0x1d, 0x5c,
+ 0x92, 0x50, 0x6e, 0xf3, 0xe9, 0xab, 0x32, 0xa7, 0xf3, 0x04, 0xb1, 0x5d,
+ 0x7c, 0x08, 0x27, 0x0d, 0xac, 0xa3, 0x35, 0xc9, 0x32, 0xb5, 0x68, 0x10,
+ 0xcf, 0x6d, 0xba, 0xcf, 0x83, 0xde, 0xf9, 0xa3, 0x11, 0xf4, 0xe0, 0xfe,
+ 0x62, 0x20, 0x04, 0x80, 0x93, 0x74, 0x0d, 0x1c, 0x02, 0xcc, 0x5b, 0xcc,
+ 0x06, 0x2a, 0x8a, 0xdc, 0x5a, 0x68, 0x10, 0xd9, 0xce, 0xfd, 0xb7, 0x29,
+ 0xda, 0x18, 0x0a, 0x37, 0xbe, 0x43, 0x5c, 0xf9, 0x19, 0x53, 0xf9, 0xc9,
+ 0x1e, 0x18, 0xbe, 0x61, 0x60, 0x3b, 0x46, 0xa5, 0xfa, 0x79, 0x63, 0x9c,
+ 0x93, 0x6b, 0x8e, 0xcf, 0xb5, 0x5f, 0x97, 0x11, 0xad, 0xdc, 0x37, 0xe5,
+ 0x5a, 0x7e, 0x2b, 0x3a, 0x00, 0x33, 0x8c, 0xa5, 0xc5, 0x70, 0x2f, 0x22,
+ 0xec, 0x48, 0x18, 0x0b, 0x54, 0xe2, 0x99, 0x0d, 0x60, 0x28, 0x10, 0xbc,
+ 0x16, 0xa3, 0x3a, 0x80, 0x67, 0x47, 0x6c, 0x36, 0xd9, 0x6e, 0x5f, 0x44,
+ 0x24, 0x1c, 0x65, 0x47, 0xf1, 0x98, 0x97, 0x3a, 0x2a, 0x19, 0x75, 0x7b,
+ 0x12, 0x2d, 0x53, 0x0c, 0x44, 0x4d, 0x5b, 0x41, 0x46, 0x3f, 0xc3, 0x2d,
+ 0x91, 0x5a, 0x71, 0x8e, 0x6e, 0x84, 0x07, 0xc7, 0x62, 0xb7, 0xcb, 0x39,
+ 0x11, 0x99, 0xef, 0x43, 0xe7, 0x61, 0x3d, 0xe9, 0x91, 0x38, 0x40, 0xb8,
+ 0x8c, 0x80, 0x08, 0x81, 0x30, 0xb9, 0x5b, 0x52, 0x9f, 0x18, 0x0d, 0xf3,
+ 0xb8, 0x27, 0xa3, 0xf5, 0xba, 0xfc, 0x41, 0xa5, 0x10, 0xfe, 0x3a, 0xe6,
+ 0x53, 0x3b, 0x2f, 0x90, 0x64, 0x0d, 0x15, 0x9b, 0xe4, 0x10, 0x29, 0x84,
+ 0xff, 0xac, 0x83, 0xac, 0x13, 0x5e, 0x90, 0x87, 0x68, 0x17, 0x6f, 0xde,
+ 0x3a, 0x27, 0x25, 0x33, 0xb2, 0x3b, 0xca, 0xb6, 0xb3, 0x5d, 0x6d, 0x0b,
+ 0xf3, 0x6f, 0xe8, 0x10, 0xb3, 0x42, 0xb3, 0xf0, 0xd7, 0xf0, 0x0d, 0xb6,
+ 0xae, 0x13, 0x40, 0xef, 0x0d, 0x12, 0x99, 0xeb, 0xdb, 0x73, 0x87, 0xf8,
+ 0x19, 0xa3, 0xbd, 0xea, 0x5c, 0xa2, 0xec, 0x6a, 0xae, 0x3e, 0xc7, 0x43,
+ 0x73, 0xd6, 0xe9, 0x51, 0xa2, 0x8d, 0x36, 0x49, 0x43, 0x5d, 0x12, 0x6d,
+ 0xeb, 0x99, 0x79, 0x7f, 0x15, 0x74, 0x69, 0xaf, 0xa1, 0x97, 0x5a, 0x91,
+ 0xfd, 0xc6, 0x0f, 0xa5, 0x43, 0x90, 0x38, 0x54, 0xb7, 0xd7, 0x57, 0x4b,
+ 0x7c, 0xdd, 0xa2, 0x45, 0x8e, 0xbb, 0x56, 0xae, 0x1c, 0x03, 0xf4, 0xcf,
+ 0xca, 0xee, 0xc6, 0x10, 0x3e, 0xb8, 0x24, 0x3c, 0xbd, 0x4e, 0xd6, 0xac,
+ 0x9e, 0x98, 0x9b, 0x2f, 0x5d, 0x49, 0x6b, 0xb4, 0xfa, 0xe3, 0x10, 0x7f,
+ 0x33, 0xea, 0xf7, 0x37, 0x07, 0xae, 0xf3, 0xac, 0x04, 0x5a, 0x6a, 0xb2,
+ 0x02, 0x9f, 0x3d, 0x01, 0x08, 0x9b, 0xb6, 0x37, 0xe6, 0x11, 0xf5, 0x71,
+ 0xb0, 0xe2, 0x73, 0xba, 0x11, 0x22, 0x38, 0x58, 0xad, 0x03, 0x06, 0x8a,
+ 0x89, 0xf5, 0x53, 0xb6, 0xa0, 0x78, 0x91, 0x31, 0x2f, 0x29, 0xf8, 0x56,
+ 0x0b, 0xd0, 0xf4, 0x77, 0xbc, 0x3b, 0x9b, 0xdf, 0x87, 0x40, 0x60, 0x39,
+ 0xc1, 0xfb, 0x0c, 0xb0, 0xa1, 0x74, 0x36, 0xf5, 0xad, 0xf2, 0x66, 0x3f,
+ 0xe7, 0xae, 0x62, 0x01, 0xa6, 0x6c, 0xf3, 0x64, 0x95, 0x5e, 0x78, 0x5e,
+ 0x44, 0xba, 0x55, 0x12, 0xdc, 0xed, 0x25, 0xc6, 0x82, 0x04, 0xbc, 0x01,
+ 0x95, 0x98, 0x74, 0x69, 0x80, 0x57, 0x6a, 0xa6, 0xdc, 0xa5, 0x74, 0x46,
+ 0xd6, 0xea, 0x86, 0x29, 0x34, 0xcf, 0x48, 0xdf, 0x0c, 0xd9, 0xd8, 0x71,
+ 0xbd, 0x97, 0x8d, 0x8f, 0xfc, 0xef, 0xba, 0x7d, 0x70, 0x53, 0xf0, 0xc6,
+ 0xf7, 0xfb, 0x2d, 0x46, 0x99, 0x0d, 0x0c, 0x9d, 0xf2, 0x8b, 0xbf, 0xe5,
+ 0x12, 0x7f, 0x4d, 0x87, 0xc5, 0x1a, 0x1c, 0xbf, 0x32, 0x18, 0x8d, 0xb4,
+ 0x29, 0x94, 0xcb, 0x44, 0xf8, 0x1a, 0xa2, 0xf8, 0x58, 0x3a, 0xe6, 0x77,
+ 0xe0, 0x25, 0xf4, 0xdd, 0x21, 0x8e, 0x1b, 0x81, 0xfa, 0xaf, 0xf1, 0x49,
+ 0x32, 0x70, 0x24, 0x1c, 0xae, 0xde, 0x74, 0x14, 0xba, 0xbc, 0x4f, 0x71,
+ 0xb7, 0x76, 0x7f, 0x52, 0xf1, 0x6e, 0xb3, 0xab, 0x3a, 0x68, 0xf8, 0x85,
+ 0x89, 0xd8, 0x87, 0xb9, 0x30, 0x5a, 0xa8, 0xf5, 0x9f, 0xdf, 0x99, 0x54,
+ 0xec, 0x9d, 0x88, 0xfd, 0x4b, 0xbb, 0x84, 0x31, 0xe2, 0x42, 0xb1, 0xef,
+ 0x05, 0x58, 0x2f, 0xd7, 0x5b, 0xe6, 0xd9, 0xf9, 0x42, 0x34, 0x02, 0xde,
+ 0xdb, 0x63, 0xae, 0x6b, 0x52, 0xe4, 0x03, 0xc2, 0x0a, 0x9b, 0x99, 0xcc,
+ 0xe2, 0xac, 0xa4, 0x8b, 0xe2, 0x8e, 0x6d, 0xfe, 0x30, 0xa4, 0x6b, 0x08,
+ 0x11, 0xf6, 0xe1, 0x8b, 0xc7, 0xf7, 0xa3, 0x58, 0xcf, 0x86, 0x76, 0xcd,
+ 0xea, 0x34, 0x10, 0x31, 0xb7, 0x67, 0x03, 0x38, 0x46, 0x2e, 0x5f, 0xae,
+ 0x16, 0x15, 0x7e, 0xdc, 0xa4, 0xc0, 0x32, 0x3d, 0x8b, 0xde, 0xfb, 0x6a,
+ 0x99, 0x48, 0x76, 0xc6, 0x32, 0x3f, 0x17, 0x23, 0xde, 0xeb, 0x23, 0x76,
+ 0x28, 0xaa, 0xf2, 0x1c, 0x63, 0x97, 0x18, 0xb8, 0x40, 0x06, 0x5e, 0x0f,
+ 0x25, 0x28, 0xcc, 0xde, 0xf9, 0x8d, 0x08, 0xda, 0x55, 0x1d, 0xd2, 0x78,
+ 0xd0, 0x98, 0x08, 0x4b, 0x5c, 0x9c, 0x56, 0xa7, 0x9c, 0xa8, 0xd3, 0x8a,
+ 0x7b, 0x00, 0x02, 0x7d, 0xa8, 0x2e, 0x73, 0x80, 0x8b, 0xea, 0x2d, 0xd9,
+ 0x5a, 0xd4, 0x97, 0x82, 0xfa, 0x20, 0xad, 0xe7, 0x89, 0xe2, 0x08, 0x76,
+ 0xdd, 0x2d, 0x73, 0xab, 0xe8, 0xa5, 0xe3, 0x9e, 0xb2, 0x55, 0x85, 0x50,
+ 0xf5, 0x78, 0xed, 0x37, 0xeb, 0xde, 0x01, 0xeb, 0x8c, 0x4f, 0x55, 0x3e,
+ 0x1c, 0xf7, 0x93, 0xad, 0xc1, 0xf5, 0xcb, 0x8b, 0x78, 0x32, 0xd8, 0xfa,
+ 0xa1, 0x04, 0x42, 0xa5, 0x0d, 0x82, 0x30, 0x75, 0x85, 0xc3, 0xb3, 0x29,
+ 0x81, 0xba, 0x5d, 0x9e, 0x48, 0xea, 0xcb, 0xce, 0x83, 0x81, 0xd1, 0x32,
+ 0xd4, 0x97, 0x50, 0x4c, 0xc3, 0xc0, 0x03, 0x2e, 0xda, 0x73, 0x1e, 0xf7,
+ 0xe4, 0xa3, 0xea, 0xeb, 0x6d, 0xeb, 0xe1, 0xf9, 0x09, 0xe5, 0xee, 0x4a,
+ 0x1a, 0x9a, 0x07, 0xa0, 0x79, 0x23, 0x9f, 0x0b, 0x87, 0x23, 0x80, 0x9a,
+ 0x0e, 0x8f, 0xc7, 0x97, 0xb9, 0xdc, 0xf3, 0x64, 0x27, 0x0d, 0xda, 0x08,
+ 0xe1, 0x4a, 0xba, 0xae, 0x0f, 0xc8, 0x2f, 0xb0, 0x4c, 0x64, 0x96, 0x1d,
+ 0xa1, 0xd6, 0xe0, 0x27, 0xfb, 0x3b, 0x64, 0x12, 0xc0, 0xeb, 0x1f, 0x50,
+ 0xe0, 0x53, 0x5b, 0xe0, 0x4b, 0x17, 0x4b, 0x39, 0x9a, 0x3a, 0x7a, 0xe7,
+ 0x7d, 0xdf, 0xad, 0x30, 0x69, 0x21, 0x66, 0x19, 0x59, 0xcb, 0x0e, 0x41,
+ 0x88, 0xda, 0xed, 0x9d, 0x4b, 0xf5, 0xe8, 0xff, 0x58, 0x73, 0xd3, 0x4f,
+ 0x9a, 0x90, 0xe4, 0x59, 0xad, 0x82, 0xee, 0x15, 0xcc, 0x7f, 0x33, 0xcc,
+ 0xa2, 0xe2, 0x2a, 0xcd, 0x5c, 0xdd, 0xd0, 0x9d, 0x74, 0x1d, 0xb9, 0x9a,
+ 0x26, 0xf5, 0x9d, 0xd7, 0x09, 0xd3, 0x2b, 0x4c, 0x00, 0xa6, 0xbc, 0xca,
+ 0x25, 0xa0, 0x44, 0xc2, 0xb8, 0x9b, 0xba, 0x67, 0xc0, 0x78, 0x43, 0xf7,
+ 0x2b, 0x19, 0x53, 0xd6, 0x14, 0x94, 0x8a, 0xd5, 0xe0, 0x35, 0xe8, 0x91,
+ 0x68, 0x47, 0x29, 0x65, 0x9c, 0xea, 0xc8, 0x93, 0x3a, 0x03, 0xd8, 0xcd,
+ 0x59, 0x62, 0x87, 0x19, 0xe9, 0xc2, 0xb3, 0x87, 0x58, 0x5d, 0xa4, 0x79,
+ 0x9c, 0x53, 0x7e, 0x4d, 0x05, 0xb6, 0x67, 0x18, 0x4e, 0xdf, 0x56, 0x02,
+ 0xd8, 0xd5, 0x61, 0xb5, 0xb1, 0x29, 0xd8, 0x0f, 0xb9, 0x99, 0x48, 0x5b,
+ 0x65, 0x25, 0xc0, 0x55, 0xf2, 0xf5, 0x96, 0xe1, 0xd7, 0xf1, 0x81, 0x63,
+ 0x98, 0xc4, 0xd7, 0x5c, 0x21, 0xef, 0x82, 0x16, 0xc8, 0xe8, 0x47, 0x33,
+ 0xb9, 0xef, 0xa4, 0x3a, 0x42, 0x80, 0xc5, 0x9b, 0x60, 0xe7, 0x20, 0x00,
+ 0xa8, 0x85, 0x80, 0x06, 0x5e, 0x3b, 0x73, 0xf2, 0x3e, 0x28, 0x75, 0x82,
+ 0xce, 0x1c, 0x03, 0xb2, 0xa0, 0x67, 0xe1, 0x9e, 0x65, 0x98, 0x4e, 0x35,
+ 0xc7, 0xa4, 0xb5, 0xae, 0x62, 0x1c, 0xe7, 0x12, 0x0c, 0x01, 0xc7, 0x03,
+ 0x4e, 0xa6, 0x6c, 0x66, 0xd3, 0x2c, 0xa9, 0x12, 0x3a, 0x64, 0xde, 0x11,
+ 0x21, 0x7e, 0xa1, 0x94, 0x35, 0xc7, 0xbf, 0x8c, 0xcb, 0x62, 0x69, 0xe7,
+ 0xde, 0x9c, 0x89, 0x44, 0x4f, 0x5b, 0x7b, 0x39, 0x8c, 0xf4, 0x27, 0x60,
+ 0x79, 0x92, 0x96, 0xad, 0x10, 0xd5, 0x7e, 0x01, 0x8d, 0xd2, 0x03, 0x38,
+ 0xc0, 0x74, 0x6e, 0x2a, 0x0b, 0xf8, 0xc3, 0x8d, 0xad, 0xc1, 0x5e, 0x7b,
+ 0xe6, 0x26, 0xb7, 0x23, 0x70, 0x2f, 0x33, 0xcd, 0x35, 0x4a, 0xec, 0x60,
+ 0x6c, 0x6c, 0x63, 0x18, 0x9e, 0xf9, 0xc6, 0x47, 0x14, 0x86, 0x6c, 0x94,
+ 0xe7, 0x01, 0x52, 0x83, 0x7f, 0x48, 0x00, 0x6a, 0x4c, 0x05, 0x2d, 0x88,
+ 0x1c, 0x9f, 0x8a, 0xb6, 0x21, 0x2d, 0x7b, 0xe8, 0xe9, 0x28, 0x3b, 0xb1,
+ 0x38, 0x61, 0x9f, 0x78, 0xae, 0x8d, 0x53, 0xc2, 0x0f, 0x87, 0x8e, 0xbf,
+ 0x65, 0xec, 0x28, 0x2a, 0x84, 0x00, 0xf8, 0x5b, 0xbe, 0x76, 0x1c, 0xc7,
+ 0x34, 0xbf, 0x0b, 0x2c, 0xf8, 0xcf, 0x8c, 0x2a, 0x62, 0xc9, 0xad, 0xe2,
+ 0x58, 0x70, 0x7f, 0x5c, 0xfc, 0x6f, 0x93, 0xef, 0xb2, 0xc3, 0x57, 0xd3,
+ 0x77, 0xee, 0xde, 0xde, 0xe8, 0xb6, 0xb5, 0x70, 0x1f, 0xff, 0x13, 0x8c,
+ 0xa8, 0x2c, 0x36, 0xd1, 0x3f, 0x08, 0xb7, 0xff, 0xa7, 0x8e, 0xd4, 0x1b,
+ 0xd8, 0x20, 0x7a, 0x8c, 0x67, 0x9c, 0xd8, 0x1e, 0xe9, 0xb2, 0x54, 0x35,
+ 0x9c, 0xec, 0x1c, 0x64, 0xe0, 0xe4, 0xa7, 0x0d, 0xed, 0x61, 0x4c, 0x1d,
+ 0x1d, 0x3b, 0x0a, 0x03, 0x92, 0x44, 0xc7, 0xb0, 0x7d, 0xa9, 0x32, 0x1e,
+ 0xcf, 0x3a, 0x0b, 0x90, 0x74, 0x3b, 0x88, 0x6f, 0x72, 0xc6, 0xdc, 0xde,
+ 0xf8, 0x8f, 0x1d, 0xaf, 0x64, 0x28, 0x8a, 0x9e, 0x89, 0x9b, 0xdc, 0x13,
+ 0x01, 0x2f, 0x8f, 0xb7, 0x93, 0xdf, 0xe0, 0xf0, 0xb5, 0x7b, 0x0f, 0x84,
+ 0x87, 0xf6, 0xc7, 0xd9, 0xa9, 0xce, 0x43, 0x20, 0xe3, 0x23, 0xd3, 0x09,
+ 0xaa, 0x75, 0x5c, 0xcc, 0x89, 0x92, 0x47, 0xe8, 0x78, 0x71, 0x08, 0x20,
+ 0x69, 0x9f, 0xe3, 0x61, 0x72, 0xb7, 0x85, 0x0a, 0xee, 0x01, 0x77, 0x07,
+ 0x78, 0x7b, 0xfa, 0xdb, 0xe2, 0x84, 0xf0, 0xf1, 0x2a, 0x93, 0x2a, 0x6f,
+ 0x8e, 0x6d, 0x5c, 0xa1, 0x34, 0x1f, 0x65, 0x08, 0x6f, 0xf2, 0xed, 0xb0,
+ 0x1e, 0x1b, 0x0e, 0x12, 0x0c, 0x25, 0x3a, 0x18, 0x52, 0xa2, 0x0c, 0x35,
+ 0x73, 0xdc, 0x7f, 0x90, 0x17, 0x77, 0x5a, 0x3e, 0xe9, 0xf7, 0x28, 0x6e,
+ 0xd1, 0xe5, 0xde, 0x85, 0x2b, 0xab, 0x3b, 0xb3, 0x67, 0x15, 0xdc, 0xc3,
+ 0x25, 0x21, 0xe9, 0xb7, 0x93, 0x34, 0x9d, 0x57, 0x8a, 0x6d, 0xa4, 0x4e,
+ 0x17, 0xf1, 0x0a, 0x90, 0xf6, 0x22, 0xaa, 0x6a, 0x69, 0x4c, 0x64, 0x88,
+ 0xbb, 0x11, 0xb3, 0x83, 0xc4, 0xfb, 0x31, 0x61, 0x20, 0x1a, 0x00, 0x00,
+ 0x9c, 0x2d, 0xcf, 0x74, 0xca, 0x93, 0xc3, 0xea, 0x8f, 0x58, 0xcc, 0xf2,
+ 0x46, 0x54, 0x36, 0x1b, 0x9c, 0x0c, 0x27, 0x98, 0x19, 0xb7, 0x24, 0xe9,
+ 0xfb, 0xb6, 0xd8, 0xe4, 0xfc, 0x2c, 0x9b, 0xd0, 0x25, 0x3f, 0xb0, 0xa5,
+ 0x5b, 0x22, 0x31, 0xa2, 0x86, 0xd5, 0x8a, 0xb6, 0x7b, 0xe5, 0x08, 0xcc,
+ 0x10, 0xf7, 0x76, 0xd9, 0x5a, 0x50, 0xc0, 0x17, 0xcc, 0x02, 0xb1, 0x2b,
+ 0x39, 0x84, 0x24, 0x16, 0x5a, 0x17, 0x9f, 0x79, 0x5d, 0x1b, 0x15, 0xd9,
+ 0x95, 0x59, 0xbe, 0x7c, 0x93, 0x5e, 0x2a, 0xf1, 0x60, 0x07, 0xe1, 0xdf,
+ 0xcb, 0x19, 0xa5, 0xf9, 0x32, 0x9e, 0x55, 0x01, 0x65, 0x69, 0x63, 0x77,
+ 0x50, 0xbb, 0x14, 0x2b, 0x0f, 0xc4, 0x54, 0x38, 0x15, 0xac, 0x0d, 0x9f,
+ 0xc9, 0xfb, 0x8d, 0x0f, 0xa2, 0xec, 0xe6, 0x0d, 0x50, 0xf9, 0x68, 0xcb,
+ 0xae, 0xb3, 0xb9, 0xad, 0x95, 0xb1, 0x4a, 0x81, 0x63, 0xcd, 0xb1, 0xd5,
+ 0xa1, 0xda, 0x81, 0x2a, 0xde, 0x08, 0x10, 0x33, 0x66, 0x5f, 0xee, 0x5d,
+ 0xfe, 0x75, 0x27, 0x42, 0x55, 0xcb, 0xb8, 0xb0, 0xca, 0xb0, 0x34, 0x10,
+ 0x00, 0x00, 0x59, 0x2b, 0x97, 0x99, 0xd5, 0x46, 0xca, 0x45, 0x48, 0x39,
+ 0x52, 0x2e, 0xb0, 0xab, 0xbe, 0x89, 0x25, 0x9b, 0x32, 0x5e, 0x8b, 0x4d,
+ 0x09, 0xd4, 0xb4, 0x04, 0xe1, 0x79, 0xd3, 0x48, 0xef, 0x2b, 0x65, 0x8e,
+ 0xeb, 0x59, 0x57, 0x62, 0x93, 0xd3, 0xbb, 0x22, 0x82, 0x0c, 0x66, 0x79,
+ 0xf2, 0x14, 0x54, 0xc6, 0x7a, 0xbd, 0xbe, 0xab, 0x09, 0x02, 0xd9, 0x88,
+ 0xa8, 0x13, 0xc6, 0x8f, 0x30, 0xbf, 0x05, 0xf7, 0xd3, 0x39, 0xcc, 0x18,
+ 0xf6, 0x9c, 0x01, 0xc5, 0x06, 0x70, 0x92, 0x5b, 0x60, 0x1c, 0x09, 0x92,
+ 0xfc, 0xb6, 0xe5, 0x94, 0x04, 0x71, 0xe6, 0x5b, 0xfc, 0x72, 0x80, 0x31,
+ 0x6a, 0x8c, 0x9b, 0x20, 0x9d, 0x50, 0x04, 0xa1, 0xcb, 0xc3, 0xd3, 0x6c,
+ 0x1d, 0x30, 0x1f, 0xa9, 0xba, 0xe4, 0x8d, 0x24, 0xa9, 0xcb, 0x50, 0x01,
+ 0xb7, 0xac, 0xf3, 0x5a, 0x90, 0x5f, 0xae, 0x2a, 0xfe, 0x42, 0xea, 0x8a,
+ 0x86, 0x74, 0x36, 0x39, 0xf2, 0x9e, 0xca, 0x33, 0x26, 0x87, 0x05, 0xac,
+ 0xb6, 0x7d, 0xe8, 0xfa, 0xe5, 0x0d, 0xc2, 0x67, 0x2e, 0xaf, 0xf9, 0xa1,
+ 0x48, 0xe8, 0xbe, 0x82, 0xd1, 0x13, 0xf9, 0x00, 0x82, 0x32, 0x66, 0xe0,
+ 0xb9, 0x54, 0x8a, 0x3d, 0xd6, 0xbb, 0xe8, 0xbb, 0x44, 0xf7, 0x99, 0x5a,
+ 0x22, 0xca, 0x40, 0x71, 0xd8, 0xdb, 0xd6, 0xf3, 0xeb, 0x07, 0x70, 0x8b,
+ 0x72, 0xb1, 0x58, 0x17, 0xbb, 0xbf, 0x35, 0x37, 0x0f, 0x33, 0xcb, 0xb1,
+ 0x70, 0xa5, 0x79, 0x5c, 0x6e, 0xda, 0x1e, 0x29, 0x30, 0x7a, 0x00, 0x50,
+ 0x8e, 0xd8, 0xcb, 0xd6, 0x99, 0xa5, 0x4a, 0x25, 0x4f, 0x69, 0xd5, 0xf2,
+ 0xd7, 0x8c, 0x74, 0xa3, 0x79, 0x5a, 0x70, 0xbf, 0x5c, 0x4b, 0xa1, 0xad,
+ 0xb3, 0x0a, 0x59, 0xaa, 0x12, 0x39, 0x30, 0xb2, 0x1e, 0xd9, 0xde, 0x71,
+ 0x51, 0x3e, 0xde, 0x9f, 0xef, 0x22, 0x38, 0x13, 0xf3, 0x06, 0xcd, 0x69,
+ 0xeb, 0x0c, 0xdc, 0x35, 0x2f, 0x4b, 0x46, 0x13, 0xb4, 0xb7, 0x5e, 0xe4,
+ 0x5b, 0x8f, 0x07, 0x19, 0x50, 0x39, 0xff, 0x4c, 0x85, 0x67, 0xda, 0x37,
+ 0x08, 0x5c, 0x1e, 0x46, 0xd3, 0x18, 0xd6, 0x8e, 0x21, 0x61, 0x24, 0x81,
+ 0x3c, 0x8a, 0x9b, 0xc8, 0x5b, 0x76, 0xd6, 0x57, 0x98, 0x5e, 0xa1, 0xa3,
+ 0xe5, 0xa7, 0x3d, 0x35, 0x1f, 0xf4, 0x11, 0xe0, 0x22, 0x14, 0x0d, 0x69,
+ 0xe6, 0x8d, 0xe8, 0x35, 0x31, 0x4a, 0xcf, 0xe9, 0xa7, 0xb2, 0xd7, 0xf5,
+ 0x64, 0x7d, 0xb2, 0x18, 0x51, 0x76, 0x9a, 0xed, 0x87, 0xff, 0x0f, 0x13,
+ 0x12, 0x1d, 0x29, 0x50, 0x3d, 0x61, 0xc8, 0x24, 0x17, 0x12, 0x8b, 0x75,
+ 0xa3, 0xb5, 0x54, 0x8c, 0xb6, 0xa2, 0xf0, 0x8c, 0x6e, 0x11, 0x24, 0x59,
+ 0xa4, 0xaf, 0xa4, 0x2a, 0xd4, 0xf2, 0xf9, 0xc5, 0x78, 0xef, 0x60, 0x1f,
+ 0x0e, 0x14, 0xc4, 0xe9, 0xd6, 0x07, 0xc4, 0xb0, 0x75, 0xe9, 0xbf, 0xbc,
+ 0xa1, 0x4c, 0xa0, 0x9b, 0xdc, 0x51, 0xce, 0xc5, 0x97, 0x09, 0x79, 0x01,
+ 0x27, 0xf8, 0x1d, 0x38, 0x21, 0xb1, 0x61, 0xa3, 0xf9, 0x08, 0xdd, 0xbc,
+ 0xa1, 0xaf, 0x7a, 0xa7, 0x6d, 0x5f, 0x3a, 0x75, 0xbc, 0x1c, 0xf7, 0x3d,
+ 0xe3, 0xc9, 0x4a, 0xdc, 0x62, 0x42, 0xf3, 0x5d, 0xe5, 0xd5, 0xa8, 0x42,
+ 0x1f, 0xb2, 0x43, 0x67, 0x9c, 0xd4, 0xf1, 0x9b, 0xfc, 0x89, 0xca, 0x4f,
+ 0xd8, 0xe5, 0x12, 0x07, 0xeb, 0x88, 0x2e, 0x03, 0x49, 0x44, 0x6b, 0x4c,
+ 0xac, 0x21, 0xa4, 0x69, 0x42, 0x62, 0x1b, 0xd5, 0xb2, 0xe9, 0xbd, 0x33,
+ 0x1e, 0x89, 0x8f, 0xbb, 0xdd, 0x38, 0x53, 0x4b, 0x01, 0x66, 0x1b, 0x46,
+ 0x1a, 0xdb, 0x82, 0x66, 0x53, 0x93, 0xe4, 0xac, 0x22, 0x06, 0x33, 0x85,
+ 0xa3, 0x63, 0xb2, 0xaf, 0xac, 0x4b, 0xee, 0x3f, 0xab, 0xbe, 0x86, 0x09,
+ 0x3f, 0x94, 0xcd, 0x46, 0x11, 0x9a, 0xd2, 0x2c, 0xf3, 0xcf, 0xc5, 0xdf,
+ 0x29, 0xcc, 0xe6, 0xcc, 0xc3, 0x6d, 0x58, 0xa4, 0xaa, 0xf7, 0xa3, 0x40,
+ 0x14, 0x69, 0x03, 0xa4, 0xf8, 0x79, 0x68, 0xe4, 0x0b, 0x72, 0xcb, 0xd5,
+ 0xa0, 0x59, 0x63, 0xe6, 0x82, 0xe8, 0x46, 0xe6, 0x6c, 0xe3, 0x40, 0x76,
+ 0x66, 0x0d, 0x17, 0xa3, 0x9e, 0x68, 0x7d, 0xc4, 0x15, 0x7a, 0x91, 0xff,
+ 0xcd, 0xc5, 0xd7, 0x7e, 0x68, 0xb4, 0x24, 0x70, 0xd5, 0x10, 0x40, 0xdd,
+ 0xfc, 0x46, 0x67, 0xca, 0x12, 0x9d, 0x0a, 0x70, 0x12, 0xf8, 0x76, 0x82,
+ 0xb6, 0xb1, 0x11, 0xbf, 0x9c, 0xcb, 0x9b, 0xbc, 0x48, 0x70, 0x73, 0x7b,
+ 0x62, 0x96, 0xb6, 0x5e, 0xfc, 0xfd, 0x86, 0xf8, 0xe4, 0x30, 0xe6, 0x17,
+ 0x24, 0x4e, 0x16, 0x25, 0x35, 0xdb, 0xa0, 0xe1, 0x94, 0x98, 0x21, 0xc1,
+ 0xbd, 0x81, 0x7c, 0x28, 0xf9, 0x30, 0x5c, 0x65, 0x3f, 0xa0, 0x9d, 0x15,
+ 0xd1, 0xf7, 0x24, 0x34, 0x9a, 0xb7, 0x87, 0xf7, 0xb6, 0x41, 0x89, 0xaf,
+ 0x01, 0xdf, 0x6a, 0x25, 0x57, 0x9f, 0x8b, 0xd1, 0x84, 0x38, 0x79, 0x4a,
+ 0x6b, 0xb8, 0x0d, 0xec, 0xfe, 0xaf, 0xa3, 0xd9, 0x8a, 0x71, 0x22, 0x16,
+ 0xd6, 0xa2, 0x51, 0x70, 0xc7, 0x35, 0x2a, 0xfb, 0x49, 0x15, 0xa5, 0x85,
+ 0x24, 0xfd, 0xa6, 0x5a, 0x0a, 0x09, 0x1b, 0x62, 0xfc, 0xa4, 0x4a, 0xd1,
+ 0x42, 0xe4, 0x7a, 0x3c, 0xe0, 0xb9, 0x92, 0xda, 0x2b, 0x8e, 0xa4, 0x7b,
+ 0xe8, 0x48, 0x0f, 0x1f, 0x69, 0x41, 0xfe, 0xbb, 0xb8, 0x2c, 0xa0, 0x47,
+ 0xe9, 0x8a, 0x98, 0x32, 0x39, 0xb2, 0xba, 0xab, 0x6e, 0x70, 0xce, 0x0a,
+ 0x7a, 0x56, 0xd4, 0x6e, 0xee, 0x98, 0xb1, 0xb5, 0x0c, 0xed, 0x3d, 0x49,
+ 0xbd, 0x18, 0x26, 0x8b, 0x5c, 0x84, 0xef, 0x19, 0xbe, 0x45, 0xbb, 0x61,
+ 0xef, 0xd0, 0xc8, 0x9f, 0x36, 0xcc, 0x9d, 0xeb, 0xd4, 0x9d, 0x17, 0x28,
+ 0x73, 0x63, 0x82, 0xe4, 0xc2, 0xec, 0x77, 0x62, 0xe2, 0x80, 0x34, 0xfe,
+ 0x92, 0x89, 0xc6, 0xa1, 0x6f, 0x21, 0xce, 0x87, 0x83, 0x41, 0x67, 0x2b,
+ 0xdc, 0x98, 0x48, 0x65, 0x13, 0x06, 0x45, 0xc4, 0x15, 0x27, 0x52, 0x88,
+ 0x74, 0x9c, 0x60, 0x23, 0x2d, 0x6c, 0x5a, 0x40, 0x7f, 0xa1, 0x0f, 0x40,
+ 0x86, 0xa3, 0x22, 0x81, 0x6d, 0xd7, 0xff, 0x9a, 0x8c, 0x52, 0xd7, 0x51,
+ 0x0d, 0xc5, 0x1a, 0x64, 0x58, 0x9f, 0x16, 0x31, 0xb6, 0x95, 0x48, 0x59,
+ 0xe7, 0xa4, 0xe4, 0x87, 0xd6, 0x11, 0xb9, 0xc4, 0xd2, 0x76, 0xb8, 0xcf,
+ 0xcc, 0x5e, 0x93, 0xb2, 0x4d, 0x05, 0x1d, 0xff, 0x08, 0x94, 0xbc, 0x35,
+ 0xc8, 0x16, 0x1c, 0xf9, 0x8c, 0x2d, 0x79, 0xfc, 0xe3, 0x63, 0x67, 0xbe,
+ 0x99, 0xad, 0xfb, 0xce, 0xea, 0x02, 0x9d, 0xb8, 0xaa, 0x90, 0xc9, 0x28,
+ 0xd2, 0x08, 0xce, 0xc9, 0x2d, 0x85, 0x07, 0x9b, 0x6b, 0x6f, 0x28, 0x9f,
+ 0x9d, 0xe1, 0xae, 0x6e, 0x0f, 0x63, 0xaa, 0xd0, 0x79, 0xe6, 0x0b, 0xe9,
+ 0xbe, 0xea, 0x99, 0x3a, 0x48, 0x28, 0xe3, 0xe3, 0x3a, 0x09, 0x19, 0x29,
+ 0xd7, 0x89, 0xbe, 0xe9, 0x3d, 0x16, 0x7a, 0x58, 0xf7, 0x06, 0xbe, 0xff,
+ 0x90, 0x30, 0xf5, 0x12, 0xb0, 0xcb, 0xc1, 0xbe, 0x83, 0xcc, 0x10, 0xff,
+ 0x07, 0x35, 0x0f, 0x5b, 0x3e, 0x56, 0xcd, 0x29, 0x9f, 0x0e, 0xad, 0xb0,
+ 0x11, 0x58, 0xda, 0xb7, 0xe4, 0x3e, 0x7b, 0x57, 0xb6, 0x8c, 0x40, 0x63,
+ 0x38, 0x4f, 0xa3, 0xa1, 0x27, 0x86, 0xfe, 0xe0, 0x19, 0x15, 0x58, 0xac,
+ 0xae, 0x69, 0x4f, 0x17, 0x0e, 0x90, 0x7b, 0x68, 0xde, 0x58, 0x96, 0xac,
+ 0x33, 0x4a, 0x0d, 0x8f, 0xb0, 0x0c, 0x72, 0x43, 0xd5, 0x21, 0x15, 0x74,
+ 0x78, 0x8b, 0x90, 0x44, 0x74, 0xf6, 0xb5, 0x9f, 0x3f, 0xab, 0xfa, 0xb3,
+ 0x38, 0x9b, 0xe3, 0x87, 0x2d, 0xff, 0x97, 0xd3, 0x24, 0xf9, 0x1d, 0x3e,
+ 0xac, 0xb4, 0x3d, 0x07, 0xec, 0x23, 0x8e, 0x33, 0x06, 0x62, 0xa4, 0xad,
+ 0x37, 0x31, 0x07, 0x2e, 0xd5, 0x3e, 0x24, 0xb5, 0x53, 0xc5, 0x79, 0x03,
+ 0xe4, 0x91, 0x0e, 0xda, 0xae, 0x45, 0xb7, 0x9c, 0xb2, 0x11, 0x7e, 0xef,
+ 0x83, 0x5a, 0xea, 0xbc, 0x48, 0xba, 0xb5, 0x61, 0x7d, 0x67, 0x86, 0x75,
+ 0xbe, 0x78, 0x08, 0x2c, 0xb2, 0x65, 0x16, 0x6d, 0x03, 0xeb, 0x7b, 0x1b,
+ 0x24, 0xa3, 0xc6, 0xbb, 0x4d, 0xa8, 0xd7, 0x40, 0x72, 0xbe, 0x04, 0x3f,
+ 0x81, 0x03, 0xdb, 0x96, 0xdf, 0xaf, 0xe8, 0x33, 0x9c, 0xa2, 0xea, 0x4e,
+ 0x0e, 0x81, 0xb0, 0x10, 0x9f, 0x96, 0x32, 0xc4, 0x70, 0xd3, 0x03, 0x19,
+ 0x81, 0x62, 0x16, 0x8d, 0xa5, 0xe4, 0xc1, 0x68, 0x2d, 0xe7, 0xda, 0x00,
+ 0x3f, 0x3d, 0x8e, 0x54, 0x55, 0x88, 0xbe, 0x66, 0x13, 0x72, 0x9d, 0x17,
+ 0x59, 0xe5, 0x31, 0x60, 0x29, 0x15, 0xdd, 0x95, 0x5e, 0x42, 0x09, 0x11,
+ 0xb9, 0x27, 0x8b, 0x9f, 0xdc, 0xae, 0x45, 0xb7, 0x1b, 0xe1, 0x35, 0xf8,
+ 0xee, 0x2d, 0x1f, 0xef, 0x40, 0x56, 0x8b, 0xc1, 0xee, 0xca, 0x32, 0x33,
+ 0x9a, 0x9e, 0xc7, 0x7b, 0xa6, 0xd1, 0xb9, 0xc3, 0x2b, 0xa9, 0x10, 0xa2,
+ 0x3f, 0xc4, 0x85, 0x03, 0x49, 0x34, 0x48, 0x17, 0x0d, 0x02, 0x81, 0x83,
+ 0x19, 0x2f, 0x0a, 0xaa, 0x41, 0x05, 0x81, 0x53, 0xb6, 0xc6, 0xb4, 0x2c,
+ 0x6d, 0x52, 0x23, 0x15, 0x96, 0xd7, 0x45, 0xf3, 0xde, 0xcd, 0x1f, 0x5c,
+ 0xf2, 0xf0, 0x80, 0x88, 0xe8, 0x0a, 0x28, 0xd6, 0x9f, 0x55, 0xb8, 0x9a,
+ 0xce, 0x24, 0x24, 0x32, 0x67, 0xb4, 0x71, 0x93, 0xac, 0xf9, 0x4e, 0x1b,
+ 0xfc, 0x54, 0x78, 0xee, 0x62, 0x71, 0xa6, 0xd0, 0xb0, 0x3a, 0x8d, 0x5f,
+ 0x24, 0xc1, 0x32, 0x22, 0xe0, 0x98, 0xb1, 0x12, 0xe3, 0xbf, 0x33, 0xa3,
+ 0x31, 0xe3, 0xf1, 0xa4, 0x91, 0xae, 0xff, 0xc6, 0xdd, 0x6a, 0x94, 0x7b,
+ 0xd1, 0x72, 0xb4, 0x50, 0x83, 0x5e, 0xa9, 0x58, 0x12, 0x5b, 0x18, 0xa2,
+ 0x34, 0x93, 0xf7, 0x47, 0x93, 0x58, 0xd9, 0xe2, 0x76, 0x85, 0x21, 0x6f,
+ 0xb1, 0x10, 0xa0, 0x9e, 0x7e, 0x0c, 0x2b, 0x68, 0x72, 0x16, 0x18, 0x6d,
+ 0x69, 0x29, 0xf8, 0xa6, 0xba, 0x60, 0x91, 0xbc, 0x50, 0x98, 0x54, 0x69,
+ 0xd3, 0xba, 0x7c, 0x1d, 0x65, 0x0f, 0xbf, 0x26, 0xa1, 0x0d, 0x07, 0x05,
+ 0x21, 0xa0, 0x1d, 0xe1, 0xec, 0x53, 0x22, 0x00, 0x16, 0x82, 0xaa, 0x23,
+ 0x14, 0xd8, 0xb1, 0xea, 0x5f, 0x74, 0xe1, 0x0d, 0xf7, 0x32, 0x9b, 0x7f,
+ 0xd0, 0xc8, 0xa5, 0x5e, 0x24, 0xd1, 0x79, 0x58, 0x55, 0x56, 0x32, 0x5a,
+ 0xb1, 0xd9, 0x66, 0x21, 0x65, 0x17, 0x80, 0x15, 0x3b, 0x67, 0xa7, 0x95,
+ 0x6c, 0x92, 0x71, 0x3f, 0xc2, 0x2b, 0x0e, 0xc8, 0x1e, 0x62, 0xdc, 0x01,
+ 0x2a, 0xb6, 0xd7, 0x31, 0x80, 0x5a, 0x0e, 0xe2, 0x4d, 0x19, 0xd0, 0x89,
+ 0x8f, 0x31, 0xa9, 0x02, 0xf5, 0x68, 0xa5, 0x5f, 0x5d, 0xc4, 0x18, 0x53,
+ 0xd6, 0xad, 0x47, 0x52, 0xa9, 0xba, 0x56, 0xeb, 0xec, 0xe4, 0x0e, 0x8d,
+ 0x84, 0x1b, 0x2b, 0x81, 0xf2, 0xbf, 0x2f, 0xc5, 0x44, 0x75, 0xbb, 0x45,
+ 0x17, 0xbc, 0x6c, 0x06, 0x72, 0x19, 0xa8, 0xa8, 0x1d, 0x19, 0x61, 0x50,
+ 0x4e, 0xcc, 0xf9, 0x9d, 0x34, 0xbd, 0x5e, 0x47, 0x44, 0xf7, 0x7e, 0x2f,
+ 0x87, 0x39, 0x46, 0xb6, 0x1d, 0xf1, 0x2e, 0xc4, 0x82, 0x70, 0x42, 0x0e,
+ 0x0d, 0x98, 0x57, 0x19, 0xa1, 0x72, 0x2f, 0x70, 0xae, 0x3b, 0x9b, 0xc9,
+ 0x1e, 0xe2, 0x8f, 0x5f, 0x50, 0x0e, 0x53, 0xb6, 0x7c, 0x37, 0xc6, 0x4b,
+ 0xa5, 0xe4, 0xe5, 0x25, 0x3e, 0xac, 0xca, 0x43, 0xc5, 0x86, 0x53, 0x41,
+ 0xa1, 0x0f, 0x32, 0x66, 0x6d, 0xae, 0x4a, 0xa0, 0xc6, 0xe3, 0xc9, 0xe4,
+ 0x7e, 0xe9, 0xe3, 0xb8, 0xb1, 0xe1, 0xff, 0x7a, 0xb5, 0x2c, 0xf5, 0x43,
+ 0x35, 0xd3, 0x82, 0x33, 0xfd, 0x1a, 0x36, 0x9c, 0x77, 0xec, 0xc4, 0x4d,
+ 0xf6, 0xb6, 0x21, 0x67, 0xcc, 0xfc, 0xaf, 0x70, 0xf7, 0x5f, 0x41, 0x3b,
+ 0xff, 0x82, 0x24, 0x6e, 0x13, 0xcb, 0x17, 0xae, 0x74, 0x29, 0xa8, 0xea,
+ 0x62, 0xcd, 0xd1, 0xa3, 0xbc, 0x31, 0xfe, 0x03, 0x3c, 0x63, 0x97, 0xec,
+ 0x5a, 0x72, 0x15, 0x41, 0x63, 0x7c, 0xf5, 0xf7, 0x2f, 0x3f, 0x2d, 0x3a,
+ 0x69, 0x6b, 0x0c, 0xb0, 0x82, 0x90, 0xaf, 0x8e, 0x47, 0xa7, 0xab, 0x0a,
+ 0x96, 0xf9, 0x3d, 0x2f, 0xc5, 0xd3, 0x96, 0xb5, 0xe4, 0x8a, 0xe9, 0x20,
+ 0x12, 0x7a, 0x68, 0x36, 0x28, 0xaf, 0xa9, 0xd6, 0xb4, 0xd0, 0x03, 0x05,
+ 0xdb, 0x31, 0x4c, 0x71, 0xc0, 0xc2, 0x74, 0x39, 0x88, 0x0d, 0x78, 0x6c,
+ 0x15, 0xac, 0x4b, 0xf7, 0xe6, 0xe2, 0x8f, 0xc5, 0x1d, 0x6b, 0x1c, 0xbf,
+ 0x36, 0x4d, 0x06, 0xd6, 0x77, 0xc7, 0x2a, 0x0f, 0x07, 0xe3, 0x41, 0x28,
+ 0xef, 0x4c, 0xb4, 0x09, 0x4e, 0xb1, 0x53, 0xe1, 0x5b, 0xfc, 0x05, 0xd6,
+ 0xd1, 0x26, 0x71, 0x11, 0xd3, 0x26, 0x9f, 0x46, 0x97, 0x9f, 0x5c, 0x58,
+ 0x1b, 0x1d, 0x7a, 0x74, 0x66, 0x18, 0x5a, 0x6b, 0x58, 0x4e, 0xea, 0x74,
+ 0xfe, 0x16, 0x53, 0x62, 0xea, 0xfd, 0xdc, 0x22, 0x0a, 0x36, 0x5e, 0x7e,
+ 0xb5, 0x8e, 0xba, 0x57, 0xb3, 0x01, 0xb8, 0x32, 0xdd, 0x11, 0xb3, 0xdd,
+ 0x4a, 0x53, 0x17, 0xbb, 0x84, 0x5d, 0x50, 0xad, 0x48, 0x44, 0x6a, 0xfc,
+ 0x74, 0x0b, 0xca, 0xf5, 0x00, 0x47, 0x0d, 0x8a, 0x45, 0xed, 0x6e, 0x5b,
+ 0x9e, 0x16, 0x27, 0xee, 0x8a, 0x7b, 0x06, 0x1c, 0xfc, 0xec, 0x99, 0x85,
+ 0xda, 0x91, 0x88, 0xc4, 0x96, 0xf2, 0x02, 0xc2, 0xf7, 0x0e, 0x8b, 0xd1,
+ 0x86, 0x3b, 0x8d, 0x62, 0x52, 0xef, 0xc6, 0x42, 0xe0, 0x2d, 0x47, 0x83,
+ 0xea, 0xfd, 0x2e, 0xf4, 0x73, 0x39, 0xdd, 0xf7, 0x33, 0x9a, 0x9d, 0xca,
+ 0xc7, 0x63, 0x06, 0xf7, 0x84, 0x1a, 0x4a, 0xa8, 0xf2, 0x3c, 0x78, 0x40,
+ 0x67, 0xef, 0x14, 0x2c, 0xf4, 0x66, 0xad, 0x41, 0x08, 0x55, 0x2b, 0xf8,
+ 0xec, 0xab, 0xe5, 0x8b, 0xa1, 0x1d, 0x3d, 0x9a, 0xd7, 0x20, 0x95, 0x9d,
+ 0x28, 0xf0, 0x2f, 0x0f, 0x34, 0x61, 0xdb, 0xf2, 0xa2, 0x12, 0x8b, 0x65,
+ 0x3d, 0xe0, 0xf4, 0x09, 0x07, 0x11, 0xe0, 0x97, 0x45, 0xaa, 0x5e, 0x66,
+ 0xdc, 0x0e, 0x50, 0x19, 0x37, 0x6b, 0xeb, 0x35, 0x95, 0x06, 0x1d, 0x7d,
+ 0xf2, 0xc2, 0x2d, 0xa7, 0xee, 0x4a, 0x4e, 0x33, 0x99, 0xce, 0x0d, 0xa4,
+ 0x9b, 0x4a, 0xb6, 0xc8, 0x2c, 0x62, 0xb9, 0x22, 0x3f, 0x18, 0x8d, 0xc3,
+ 0x4a, 0xfb, 0xf0, 0x12, 0x6f, 0x36, 0xbd, 0xa5, 0x1c, 0x80, 0xc8, 0xe6,
+ 0xfa, 0x89, 0x20, 0x7d, 0xa7, 0xa1, 0xf7, 0x53, 0x32, 0xa4, 0x7e, 0xf5,
+ 0x90, 0xe1, 0xef, 0x81, 0xb8, 0x9d, 0xea, 0x6c, 0x3a, 0x6e, 0x31, 0xe5,
+ 0x1c, 0x03, 0xec, 0xd8, 0x5b, 0xbf, 0xdd, 0xbe, 0xbb, 0x78, 0x29, 0xd7,
+ 0xc3, 0x29, 0xfe, 0x23, 0x6c, 0x17, 0xbe, 0x5a, 0x5a, 0x94, 0x32, 0xbf,
+ 0xed, 0x0a, 0x1f, 0x40, 0x67, 0x68, 0x7a, 0x80, 0xe4, 0x9d, 0xd2, 0xed,
+ 0x64, 0x1e, 0x9c, 0xac, 0x91, 0x03, 0x5b, 0x62, 0xeb, 0x49, 0xed, 0x1c,
+ 0xcb, 0x3c, 0x4b, 0x8c, 0x40, 0xb6, 0xbc, 0x5f, 0x13, 0x53, 0x24, 0xb3,
+ 0x50, 0x6c, 0x36, 0xcf, 0xeb, 0xab, 0x8c, 0xfd, 0x65, 0x44, 0x68, 0x28,
+ 0x6b, 0x8c, 0x08, 0x73, 0x96, 0x89, 0x9d, 0x02, 0xe5, 0xa4, 0xaa, 0x36,
+ 0x6c, 0xfc, 0x5d, 0x19, 0x0a, 0xd5, 0x57, 0xc7, 0x1d, 0x6b, 0xc3, 0xf2,
+ 0x62, 0x12, 0xe0, 0x2a, 0xcf, 0xec, 0xcd, 0x10, 0x9b, 0x14, 0x00, 0xd2,
+ 0x4e, 0xb9, 0xe4, 0xef, 0x42, 0x3b, 0x56, 0x90, 0x59, 0x7c, 0x9e, 0xf8,
+ 0xbe, 0x58, 0xa8, 0x7a, 0x7a, 0x11, 0x0e, 0x8e, 0x85, 0xae, 0x81, 0x2b,
+ 0x93, 0xe7, 0xea, 0x34, 0xa0, 0xfb, 0x8d, 0x13, 0x7f, 0x35, 0x31, 0x95,
+ 0x70, 0x76, 0xb9, 0x67, 0x0b, 0x90, 0x32, 0xe0, 0xa8, 0xbb, 0x6e, 0x1c,
+ 0x1c, 0x8f, 0xe8, 0x04, 0xc3, 0x54, 0xca, 0xb7, 0xc4, 0xd0, 0x2a, 0x38,
+ 0xa8, 0x7d, 0xa6, 0x8f, 0x71, 0xe2, 0x54, 0xa1, 0xce, 0x8d, 0x03, 0xa4,
+ 0x02, 0x34, 0x6f, 0x8c, 0xed, 0x18, 0x45, 0x60, 0xbb, 0xc0, 0x8c, 0xed,
+ 0x12, 0xdf, 0xa5, 0xbe, 0x4d, 0xa5, 0x13, 0x23, 0x8c, 0x4f, 0xc9, 0xb6,
+ 0x61, 0xc3, 0xb6, 0x25, 0x25, 0xd5, 0x03, 0x35, 0x6a, 0x87, 0xe7, 0x99,
+ 0x2f, 0x52, 0xc5, 0x87, 0x40, 0x88, 0xb4, 0xec, 0x97, 0x1b, 0xf4, 0xa1,
+ 0x0f, 0xa9, 0x07, 0xe0, 0xb0, 0x86, 0x2c, 0x0a, 0x07, 0x85, 0xac, 0x40,
+ 0x9a, 0x2a, 0x17, 0xb8, 0xc2, 0x3f, 0xeb, 0x4c, 0x2c, 0xcd, 0xb4, 0x02,
+ 0x2f, 0xd1, 0xd3, 0x82, 0x5d, 0xb1, 0x79, 0xd6, 0xf0, 0x79, 0xd0, 0x28,
+ 0xfc, 0x0a, 0x8d, 0x19, 0x42, 0xfe, 0x68, 0x7b, 0xee, 0xc1, 0x64, 0x0c,
+ 0x33, 0x0a, 0x96, 0x76, 0xee, 0x8a, 0xb4, 0xbf, 0xed, 0x8f, 0x51, 0x1e,
+ 0x6b, 0xcb, 0xee, 0x9e, 0xba, 0x41, 0x1a, 0xba, 0x4f, 0x22, 0xdf, 0xda,
+ 0x92, 0xc1, 0x62, 0x05, 0x81, 0x45, 0x9b, 0x74, 0xdf, 0x4f, 0xd4, 0xe3,
+ 0x96, 0xcd, 0x55, 0x06, 0xf6, 0x37, 0x68, 0xae, 0xb9, 0x7d, 0x2c, 0xd9,
+ 0x53, 0xb6, 0x55, 0x0c, 0x59, 0x79, 0xdc, 0x18, 0x69, 0x02, 0x84, 0x2c,
+ 0x35, 0x75, 0x13, 0x83, 0xf6, 0x59, 0x31, 0x2d, 0xd5, 0xf9, 0x12, 0xfb,
+ 0x3f, 0xb9, 0x40, 0x4b, 0xac, 0x19, 0x2d, 0x0d, 0xac, 0x42, 0x4a, 0x31,
+ 0x7a, 0x10, 0xa5, 0xa7, 0x9b, 0xf6, 0x17, 0x78, 0x5b, 0x31, 0x9c, 0xec,
+ 0x46, 0x1f, 0xde, 0xf1, 0x83, 0x3f, 0x05, 0x15, 0x84, 0x84, 0xee, 0x02,
+ 0x35, 0x5e, 0x6a, 0x75, 0x3e, 0x31, 0x5d, 0x44, 0xd3, 0xe4, 0xfd, 0x5f,
+ 0xb6, 0xda, 0x77, 0x0c, 0x6e, 0x32, 0xaf, 0xea, 0x36, 0x34, 0x8e, 0x13,
+ 0x77, 0x7c, 0x6e, 0x39, 0xfe, 0xd3, 0x8a, 0x16, 0x2d, 0x88, 0xae, 0x41,
+ 0xc5, 0x54, 0x39, 0x56, 0x2f, 0xde, 0x65, 0x1b, 0x2d, 0x2b, 0x44, 0x27,
+ 0x6d, 0xb7, 0x79, 0xf9, 0x10, 0x7f, 0x39, 0xb4, 0x08, 0xca, 0x3f, 0xcc,
+ 0xf3, 0x6c, 0x21, 0xec, 0xd2, 0xdc, 0x48, 0x45, 0x34, 0xae, 0x5d, 0x67,
+ 0xd8, 0xb1, 0x7b, 0xd5, 0xad, 0xb1, 0x08, 0x0e, 0x2d, 0xc3, 0xbc, 0xd5,
+ 0x31, 0xb6, 0x52, 0x42, 0x67, 0x48, 0x82, 0x24, 0x60, 0xf5, 0x5b, 0x4b,
+ 0x77, 0xc7, 0x2e, 0xdd, 0x0a, 0xed, 0x39, 0x03, 0x03, 0x2a, 0xb3, 0x25,
+ 0xe3, 0xd8, 0x40, 0x28, 0x5e, 0x96, 0xd7, 0xae, 0x95, 0x14, 0x00, 0xe9,
+ 0x1f, 0xbe, 0x1b, 0x73, 0x6a, 0x93, 0xba, 0xb4, 0x96, 0xd8, 0x7d, 0x26,
+ 0x16, 0xdb, 0xb2, 0xed, 0x96, 0xdb, 0xc4, 0x7e, 0x9a, 0x9f, 0x4b, 0xc5,
+ 0x63, 0x0e, 0x38, 0x15, 0xa2, 0x00, 0x18, 0xaa, 0xdd, 0x80, 0x1d, 0xfc,
+ 0x3c, 0x1a, 0xe6, 0x00, 0x0d, 0x11, 0x4c, 0x47, 0x68, 0xb6, 0x0b, 0xb7,
+ 0x8e, 0x3a, 0x0a, 0x8b, 0xf2, 0x43, 0xec, 0xb7, 0xfe, 0xb9, 0xec, 0x37,
+ 0xab, 0xf8, 0x38, 0x47, 0x6e, 0xcd, 0xfb, 0x36, 0x7c, 0x05, 0xb2, 0xde,
+ 0x9d, 0xee, 0x47, 0x8a, 0x3f, 0xb6, 0x67, 0x43, 0x1f, 0xfa, 0xf6, 0x86,
+ 0x8e, 0xb4, 0x60, 0x01, 0x23, 0x1a, 0xd7, 0x99, 0x0e, 0xe7, 0xb6, 0x71,
+ 0x24, 0x77, 0x44, 0xd7, 0xe3, 0xac, 0x52, 0xb6, 0xad, 0x7c, 0xab, 0x08,
+ 0x1b, 0x78, 0x1e, 0x7f, 0x0a, 0xda, 0x3c, 0x64, 0xc5, 0xdd, 0x78, 0x7a,
+ 0x7a, 0x14, 0xf6, 0x7f, 0xe1, 0x47, 0x4e, 0x9d, 0xff, 0x51, 0x10, 0x22,
+ 0x56, 0x8e, 0xfe, 0x09, 0x03, 0xa0, 0x9e, 0x08, 0x40, 0x91, 0x7c, 0x41,
+ 0x63, 0xec, 0xb7, 0xe5, 0xb8, 0xdf, 0x2a, 0xbf, 0xc3, 0xf4, 0x0e, 0x03,
+ 0xa8, 0x1c, 0x4e, 0x8b, 0xbb, 0x5a, 0x08, 0x3c, 0x88, 0x6e, 0xb7, 0x3d,
+ 0x23, 0x1f, 0xc8, 0xec, 0x47, 0x8a, 0x88, 0x97, 0x8d, 0x9b, 0x5f, 0xa0,
+ 0xd1, 0x9e, 0x5e, 0xf9, 0x98, 0x22, 0xee, 0xdd, 0x28, 0xdb, 0x1e, 0x98,
+ 0xec, 0xc3, 0xd3, 0x5f, 0x8e, 0x58, 0xcf, 0x22, 0x4d, 0xc4, 0x03, 0xaa,
+ 0x3f, 0xbb, 0x10, 0x7f, 0x58, 0x61, 0xce, 0x01, 0xc6, 0x13, 0x94, 0x66,
+ 0x71, 0xaf, 0xa0, 0xbe, 0x3a, 0x02, 0xeb, 0x14, 0xb5, 0x77, 0xb3, 0xde,
+ 0xda, 0xfb, 0x9a, 0x7d, 0xcf, 0x0d, 0x09, 0xd4, 0x63, 0x69, 0xeb, 0x49,
+ 0x43, 0xf8, 0x4d, 0x06, 0x5d, 0x73, 0xed, 0xc9, 0xbe, 0x56, 0x13, 0x15,
+ 0xb2, 0x79, 0x5f, 0x1a, 0x58, 0x23, 0x9c, 0xcf, 0x54, 0x61, 0x23, 0xff,
+ 0x2e, 0x83, 0x87, 0xdb, 0xf6, 0x84, 0x2c, 0x84, 0xb1, 0x64, 0xcc, 0x7b,
+ 0x4c, 0x9d, 0x88, 0x67, 0xb3, 0x88, 0x26, 0x70, 0x23, 0xd2, 0x6a, 0x02,
+ 0x0f, 0xcf, 0xd9, 0xb9, 0x47, 0x6c, 0xee, 0x5a, 0x65, 0x5f, 0x6a, 0x66,
+ 0x5b, 0x15, 0x1d, 0xd7, 0x46, 0xe1, 0x84, 0x28, 0xef, 0x0b, 0xaa, 0xbf,
+ 0xc3, 0x63, 0x6c, 0x6e, 0x88, 0x14, 0x78, 0x14, 0x2b, 0x20, 0xac, 0x26,
+ 0x1a, 0x70, 0x0e, 0xd0, 0x0f, 0x9a, 0x65, 0x36, 0xb3, 0x9d, 0x30, 0x19,
+ 0x14, 0x4b, 0xae, 0x46, 0x2d, 0x82, 0x77, 0xea, 0x50, 0xb0, 0xb3, 0x24,
+ 0xb5, 0xba, 0x16, 0xf0, 0x95, 0x2c, 0x4e, 0xe8, 0xfc, 0xeb, 0xc1, 0x78,
+ 0xc4, 0x35, 0x50, 0x61, 0xcd, 0x29, 0x81, 0x1b, 0x16, 0x6d, 0xcd, 0xa5,
+ 0xd5, 0x3a, 0xeb, 0xf3, 0x9a, 0xf9, 0xc8, 0x18, 0xbe, 0xcd, 0xd6, 0x10,
+ 0x0f, 0x61, 0x38, 0xe0, 0x28, 0x47, 0xed, 0x53, 0x86, 0xd2, 0xf4, 0xf4,
+ 0xda, 0x45, 0x64, 0xb9, 0xbf, 0x84, 0x71, 0xd0, 0x13, 0x56, 0x7c, 0xe0,
+ 0x5b, 0xca, 0x9f, 0x18, 0x5a, 0x2b, 0xb4, 0x8a, 0x7b, 0xe7, 0xe8, 0xd5,
+ 0xde, 0x94, 0xf1, 0xe7, 0xd0, 0x9b, 0x0c, 0xb8, 0x99, 0x4c, 0x2b, 0x3c,
+ 0x97, 0xd4, 0x33, 0xed, 0xc7, 0x5d, 0x6c, 0xfa, 0x48, 0x34, 0x7c, 0x77,
+ 0x43, 0xbf, 0x53, 0x81, 0x70, 0x7b, 0x5e, 0x1e, 0x29, 0xb2, 0x02, 0x40,
+ 0x59, 0xa0, 0x01, 0x01, 0xa6, 0x5d, 0x5a, 0xef, 0xe5, 0x3a, 0xfa, 0xca,
+ 0xb8, 0x40, 0x41, 0x75, 0x50, 0xaa, 0x1b, 0x70, 0xae, 0x5c, 0xd4, 0x6f,
+ 0x25, 0x81, 0x14, 0x32, 0xc5, 0xe6, 0x8e, 0x10, 0x76, 0x42, 0xfb, 0xa1,
+ 0xd9, 0x7e, 0x38, 0x74, 0x07, 0x7a, 0xbe, 0x53, 0xf5, 0xb9, 0xd3, 0x4d,
+ 0x7a, 0xb7, 0x0d, 0xaa, 0xef, 0x49, 0x40, 0x39, 0x74, 0xf8, 0xc3, 0x93,
+ 0x62, 0xfd, 0x11, 0x0a, 0x5d, 0x27, 0x22, 0x34, 0x2c, 0xaf, 0x6f, 0x99,
+ 0x99, 0xed, 0xc3, 0x87, 0x50, 0xaa, 0xad, 0xf5, 0x01, 0x44, 0x5b, 0xeb,
+ 0x96, 0x4c, 0xea, 0x32, 0xae, 0xd8, 0x4b, 0x03, 0x45, 0x38, 0x44, 0x5e,
+ 0xad, 0x2f, 0xb1, 0xd2, 0x47, 0xe3, 0x99, 0xda, 0x18, 0x97, 0x07, 0x40,
+ 0x8c, 0xec, 0xe1, 0x42, 0x4e, 0x09, 0x3d, 0x44, 0x83, 0xd5, 0x93, 0xfd,
+ 0x29, 0x3d, 0x17, 0x2e, 0x99, 0x48, 0xa2, 0x13, 0x20, 0x3a, 0xca, 0x63,
+ 0x5e, 0x26, 0xa2, 0xaf, 0x23, 0x1c, 0x2e, 0x57, 0xc7, 0xc5, 0x04, 0x97,
+ 0x54, 0x5a, 0x3e, 0x7f, 0x6c, 0xe5, 0x4e, 0xff, 0xf0, 0x44, 0x93, 0x7e,
+ 0x72, 0x07, 0x12, 0xb6, 0x51, 0xfb, 0x9a, 0x2f, 0x32, 0x12, 0xa9, 0xc5,
+ 0xd9, 0x6a, 0x96, 0x65, 0x2b, 0x23, 0xd7, 0xe7, 0x16, 0x38, 0xde, 0xf2,
+ 0xf9, 0x96, 0xec, 0x0e, 0x8e, 0x4f, 0x3f, 0x19, 0xa1, 0xcf, 0x99, 0x28,
+ 0x87, 0x64, 0x9a, 0x90, 0xa8, 0x1b, 0xd4, 0x71, 0x97, 0x46, 0x25, 0x34,
+ 0x8c, 0xe7, 0xe4, 0xd9, 0x90, 0x5f, 0xd9, 0x25, 0x55, 0x19, 0xb8, 0x35,
+ 0xd0, 0x98, 0xe1, 0xa3, 0xcd, 0x0c, 0xcf, 0xca, 0xf0, 0x01, 0xe0, 0xea,
+ 0xa0, 0x3e, 0x1c, 0x2d, 0xb7, 0x63, 0x37, 0xf6, 0x73, 0x07, 0xf8, 0x84,
+ 0xed, 0x1e, 0xbc, 0xf8, 0xaf, 0x97, 0x6b, 0x71, 0x1f, 0x90, 0x32, 0xfa,
+ 0xd8, 0xf5, 0x88, 0xd9, 0x22, 0x00, 0x2d, 0x5b, 0x9c, 0x40, 0x9d, 0x94,
+ 0x32, 0xab, 0x11, 0xc8, 0xaa, 0xbc, 0x1c, 0xca, 0xa1, 0xb7, 0x88, 0x17,
+ 0x98, 0xed, 0x9e, 0x1b, 0x08, 0xf8, 0xc4, 0x11, 0xae, 0x18, 0x68, 0x3a,
+ 0x41, 0xf4, 0x86, 0x2f, 0xd9, 0x36, 0x0f, 0x43, 0x62, 0x3d, 0xba, 0x1f,
+ 0x74, 0x1d, 0x3f, 0x2a, 0x9a, 0x0a, 0x31, 0x29, 0x35, 0xf0, 0x49, 0x30,
+ 0x86, 0xcb, 0x77, 0xca, 0x45, 0xdf, 0x4a, 0x77, 0x35, 0xb5, 0xaf, 0x43,
+ 0xd4, 0x53, 0x73, 0x6b, 0xd0, 0xb4, 0xa9, 0x1d, 0x8f, 0x21, 0x14, 0x10,
+ 0xac, 0x28, 0xce, 0x6a, 0x65, 0x8c, 0xc6, 0xc7, 0x3e, 0x01, 0x8a, 0x51,
+ 0x3b, 0x40, 0x92, 0x51, 0x27, 0x88, 0xc7, 0x59, 0xc0, 0x02, 0x4d, 0xa6,
+ 0x4f, 0xd4, 0xba, 0xc8, 0xb2, 0x0c, 0x21, 0x1c, 0xf9, 0xb0, 0x7d, 0x28,
+ 0x6a, 0x08, 0xe5, 0xce, 0x75, 0x54, 0xf9, 0x7f, 0xa9, 0xfd, 0x42, 0xe6,
+ 0xb1, 0x72, 0xda, 0x2f, 0x3e, 0x00, 0x40, 0x00, 0x08, 0x1d, 0x55, 0x1e,
+ 0xc0, 0x06, 0xa2, 0xe7, 0x69, 0x03, 0xdd, 0xfe, 0xf3, 0xe6, 0x52, 0x8c,
+ 0xaf, 0xd3, 0x3e, 0x5b, 0x62, 0xdf, 0x79, 0x62, 0xf2, 0x02, 0x22, 0x70,
+ 0x3e, 0x6f, 0x0f, 0x23, 0xc5, 0xc1, 0xf8, 0x8c, 0x8d, 0x84, 0xa3, 0x72,
+ 0x94, 0xcd, 0x93, 0xdc, 0x31, 0x53, 0xc8, 0x4f, 0xe8, 0xa1, 0xd8, 0x52,
+ 0xef, 0x54, 0xbb, 0x46, 0x22, 0xc2, 0x31, 0x22, 0x40, 0xee, 0xad, 0x20,
+ 0x3c, 0x53, 0x89, 0xf8, 0x7e, 0x2c, 0x17, 0x84, 0xa3, 0x68, 0x49, 0x63,
+ 0x44, 0xa3, 0x84, 0x4e, 0x7d, 0xad, 0x35, 0x4f, 0xaf, 0x1e, 0xfa, 0x98,
+ 0xdd, 0x7b, 0x12, 0xab, 0x3d, 0xed, 0xaf, 0x04, 0x5d, 0x70, 0x19, 0x83,
+ 0xc4, 0x20, 0xd0, 0x1b, 0x38, 0x64, 0x1f, 0xf4, 0x2c, 0x89, 0x39, 0x78,
+ 0x2f, 0x28, 0xcf, 0x3c, 0x39, 0x8a, 0xb0, 0xe1, 0x3d, 0x13, 0x01, 0xd7,
+ 0x3b, 0xa1, 0xfc, 0x71, 0xa0, 0xed, 0x8e, 0xad, 0xe6, 0xb0, 0xaa, 0x01,
+ 0x39, 0x04, 0x31, 0x2b, 0xe8, 0x96, 0x18, 0x0d, 0x53, 0xf0, 0x26, 0xdf,
+ 0xa1, 0x48, 0x4f, 0x45, 0xa2, 0x37, 0x03, 0x81, 0x2a, 0x6a, 0x65, 0x51,
+ 0x79, 0xf3, 0x28, 0x70, 0xb9, 0x5a, 0x5e, 0xdd, 0xa1, 0xce, 0x59, 0xec,
+ 0x20, 0x93, 0x7e, 0x02, 0xaf, 0x06, 0xad, 0x20, 0x64, 0x56, 0x33, 0xd4,
+ 0x06, 0xe1, 0x6e, 0x20, 0xc6, 0xc4, 0xda, 0x1b, 0x8b, 0x72, 0xa9, 0xa8,
+ 0xb3, 0x35, 0x7d, 0x7d, 0x12, 0xdd, 0xd7, 0xda, 0x1b, 0x8a, 0x13, 0xa7,
+ 0x71, 0xd5, 0xc5, 0xf1, 0xbd, 0xd9, 0xcc, 0xa3, 0xe2, 0x72, 0xe1, 0xed,
+ 0xb2, 0x35, 0x5b, 0x7a, 0x10, 0xdc, 0x70, 0x55, 0x62, 0xb3, 0xbd, 0x08,
+ 0x27, 0xcf, 0x7e, 0x3e, 0xec, 0x7c, 0xb9, 0x12, 0xcb, 0x40, 0x92, 0xae,
+ 0x67, 0x54, 0x3c, 0x37, 0xb7, 0xfa, 0x4a, 0x7d, 0xc9, 0x13, 0x64, 0xbb,
+ 0x0f, 0xd0, 0x7e, 0xe2, 0x55, 0x03, 0xb5, 0x38, 0x6a, 0x4a, 0x41, 0xd6,
+ 0x77, 0x7e, 0x59, 0xfc, 0xcf, 0x14, 0x37, 0x01, 0x0c, 0x7f, 0xd0, 0x6c,
+ 0x17, 0x32, 0x66, 0xe3, 0x3d, 0xa2, 0x87, 0xbc, 0xa6, 0x28, 0x45, 0x3d,
+ 0x8f, 0x41, 0xdd, 0xea, 0x5b, 0x7c, 0xb6, 0xb7, 0x09, 0xc0, 0xb0, 0x5b,
+ 0xac, 0xf0, 0xa6, 0x95, 0xc5, 0x15, 0x1e, 0xeb, 0xce, 0x2e, 0xa8, 0x4b,
+ 0x34, 0xd7, 0xec, 0x25, 0x5f, 0x7d, 0x12, 0xbe, 0xf6, 0x69, 0x0c, 0xfa,
+ 0x39, 0xa2, 0x3c, 0xc5, 0xda, 0x4e, 0x8e, 0xf1, 0x0c, 0x46, 0xae, 0x12,
+ 0xd4, 0x2f, 0x19, 0x5a, 0x70, 0x09, 0x56, 0xd7, 0x84, 0x4b, 0x9a, 0x94,
+ 0x10, 0x1a, 0x22, 0x5b, 0xcc, 0xfc, 0x92, 0x77, 0x73, 0xed, 0x20, 0xd1,
+ 0x1f, 0x85, 0x7b, 0x79, 0x3c, 0x32, 0xc7, 0x60, 0xcb, 0x82, 0x18, 0xdd,
+ 0x17, 0xc1, 0x0a, 0x7a, 0x88, 0x62, 0xf0, 0x99, 0x3e, 0x18, 0x03, 0xc7,
+ 0x66, 0x94, 0xba, 0x72, 0x06, 0x58, 0x4f, 0x20, 0xce, 0xa8, 0xa3, 0x93,
+ 0x1f, 0x71, 0x5c, 0xf2, 0x2c, 0xa5, 0x8e, 0x57, 0x81, 0xb7, 0x4c, 0x75,
+ 0x84, 0xf2, 0x66, 0x48, 0x1b, 0x24, 0xf0, 0xf1, 0x7a, 0xa8, 0x56, 0xc5,
+ 0x5f, 0x0c, 0x57, 0x1e, 0x97, 0x2a, 0xbb, 0x06, 0xf6, 0xce, 0x08, 0x4a,
+ 0x91, 0xa8, 0xfe, 0x2a, 0x65, 0x60, 0x9f, 0x24, 0x6e, 0xec, 0xe5, 0xff,
+ 0xe1, 0x50, 0xd8, 0xdf, 0x4c, 0x2e, 0x7b, 0x11, 0xc2, 0x13, 0xe8, 0xbc,
+ 0x35, 0x13, 0x53, 0xa0, 0x2a, 0xa8, 0xe3, 0xda, 0x0d, 0x71, 0x67, 0x1c,
+ 0x17, 0x74, 0x24, 0xc6, 0xa3, 0xbe, 0xc5, 0xcf, 0x15, 0x5d, 0x95, 0xcb,
+ 0x67, 0x74, 0xfa, 0x77, 0xa1, 0x20, 0x87, 0x6f, 0x50, 0x07, 0xe9, 0x25,
+ 0xf3, 0x18, 0xad, 0x56, 0x8a, 0x80, 0x32, 0xd8, 0x72, 0x3b, 0x87, 0xfd,
+ 0x45, 0x6c, 0xf0, 0x98, 0x9d, 0xb5, 0x21, 0x96, 0x9f, 0xff, 0x7e, 0xde,
+ 0xa3, 0xbb, 0x9f, 0xf4, 0xaa, 0x1e, 0x7c, 0xfb, 0x9d, 0x94, 0xbb, 0x7b,
+ 0xc1, 0x08, 0x1e, 0x8e, 0x35, 0x80, 0x55, 0x03, 0x8c, 0x28, 0xa3, 0x94,
+ 0x8a, 0x7f, 0x3b, 0x79, 0x98, 0xf2, 0x75, 0xd3, 0xd6, 0x9f, 0xf6, 0x9b,
+ 0xca, 0x43, 0xdc, 0x5c, 0xaa, 0x49, 0x32, 0x71, 0x90, 0xb9, 0x52, 0x8f,
+ 0x63, 0x93, 0x27, 0x6c, 0xf3, 0xe4, 0xa0, 0x8a, 0xe9, 0xcd, 0xb1, 0x3c,
+ 0x5c, 0x36, 0xc1, 0xaf, 0x38, 0xb0, 0x37, 0xb9, 0x5c, 0x08, 0xe4, 0x3f,
+ 0x2e, 0x0d, 0xa4, 0x04, 0x5a, 0x3e, 0x2a, 0xa9, 0x7f, 0x1b, 0x9f, 0x5d,
+ 0x18, 0xa2, 0xc8, 0xbb, 0x28, 0x56, 0x2d, 0x60, 0xd7, 0xc7, 0x52, 0xa2,
+ 0xd7, 0x4f, 0x2d, 0xbe, 0x18, 0xc8, 0x98, 0x0a, 0xe9, 0x64, 0x11, 0xea,
+ 0xe4, 0x0f, 0x19, 0xad, 0x21, 0x37, 0x5e, 0x8e, 0x6b, 0x20, 0x7c, 0x35,
+ 0x3e, 0x8b, 0xa1, 0x3f, 0x44, 0x96, 0x0d, 0xcd, 0x00, 0xb9, 0x7f, 0xbf,
+ 0xda, 0x3d, 0x01, 0x35, 0x43, 0x60, 0x05, 0x71, 0x9e, 0x4c, 0x94, 0xdd,
+ 0x83, 0x32, 0xcf, 0x6b, 0xdd, 0x06, 0xb0, 0x87, 0xc9, 0x34, 0xef, 0x05,
+ 0xb0, 0x3a, 0x56, 0x9a, 0x4a, 0x65, 0xdf, 0x51, 0xea, 0x2a, 0x85, 0x17,
+ 0xc5, 0x0a, 0xac, 0x40, 0x45, 0x69, 0x67, 0x7f, 0xe8, 0x05, 0x20, 0xa7,
+ 0xf4, 0x25, 0x85, 0x49, 0x79, 0x28, 0xd8, 0xb4, 0xe7, 0xbb, 0xc8, 0xd5,
+ 0x1e, 0x8f, 0xcc, 0x33, 0x8b, 0xff, 0x01, 0x1c, 0xca, 0x8a, 0xc0, 0xf4,
+ 0x2d, 0x9d, 0x91, 0x7e, 0x77, 0x3c, 0x89, 0xe4, 0xe7, 0xa6, 0x4b, 0x06,
+ 0x94, 0x88, 0x05, 0x58, 0x09, 0x81, 0x44, 0x60, 0x54, 0xa5, 0x34, 0x2a,
+ 0xba, 0xea, 0xdb, 0xe6, 0x18, 0x82, 0x44, 0x6f, 0xe4, 0x1d, 0x47, 0x2f,
+ 0xe6, 0x8c, 0x4d, 0xcf, 0x50, 0x2d, 0xde, 0x28, 0x21, 0xb3, 0x05, 0x18,
+ 0xb5, 0x55, 0x09, 0x34, 0x56, 0xbf, 0x54, 0x40, 0xb3, 0x34, 0x58, 0x40,
+ 0x1f, 0x0d, 0xa3, 0xf6, 0xfa, 0xa7, 0xa5, 0x3d, 0x49, 0xdd, 0x99, 0x98,
+ 0x02, 0x34, 0xd9, 0x92, 0xb3, 0x77, 0x0c, 0x01, 0xc2, 0x9d, 0x47, 0xbe,
+ 0xe9, 0x6a, 0x0e, 0x73, 0xd1, 0xa2, 0x64, 0x6a, 0x2e, 0x3e, 0xc0, 0x1b,
+ 0x3e, 0x5f, 0x68, 0x85, 0x40, 0x50, 0xa3, 0xa5, 0x24, 0x4d, 0x85, 0x75,
+ 0x41, 0xbf, 0x75, 0x63, 0x9e, 0xcb, 0xbb, 0xbc, 0x6a, 0xe3, 0xe7, 0xb3,
+ 0x00, 0x15, 0xa1, 0xd0, 0x24, 0xbe, 0x58, 0xb6, 0x0b, 0xae, 0x4c, 0x0b,
+ 0x5e, 0x18, 0x32, 0xd4, 0xc3, 0x0a, 0xf1, 0xf7, 0x91, 0xde, 0xb2, 0x45,
+ 0xa0, 0x12, 0xba, 0x42, 0xb5, 0x62, 0x2b, 0x96, 0x1f, 0x2e, 0xcc, 0x64,
+ 0xb7, 0x34, 0x43, 0xa5, 0x64, 0xe7, 0x70, 0x8a, 0x8a, 0x7c, 0xe5, 0x37,
+ 0x22, 0x66, 0xb1, 0x93, 0x0b, 0xf9, 0x42, 0x55, 0x61, 0xa3, 0xb3, 0x4d,
+ 0x70, 0xf7, 0xf7, 0x68, 0xff, 0xa1, 0xc4, 0x00, 0xdd, 0xfc, 0x44, 0xf2,
+ 0x74, 0xc3, 0xec, 0x4b, 0x9a, 0xbb, 0xeb, 0x44, 0x73, 0x3e, 0x25, 0x26,
+ 0x37, 0xec, 0x3c, 0xae, 0x00, 0x1d, 0x1b, 0x64, 0x79, 0xb4, 0x29, 0x63,
+ 0x21, 0x65, 0xce, 0x6f, 0x1e, 0x6e, 0x68, 0xa6, 0xb4, 0xa1, 0xc7, 0x79,
+ 0xf0, 0xf2, 0x9a, 0x7b, 0x5e, 0x67, 0x3a, 0xa4, 0xf7, 0x6d, 0x11, 0x35,
+ 0xb9, 0x33, 0x87, 0xa6, 0x4b, 0xac, 0x34, 0x8b, 0x8d, 0xe0, 0x91, 0xd7,
+ 0xec, 0xc7, 0xa1, 0x6c, 0x50, 0x4c, 0x3a, 0xd1, 0x93, 0x20, 0xd2, 0x35,
+ 0x9b, 0x39, 0x4a, 0x32, 0x12, 0x10, 0x68, 0xd4, 0x84, 0x41, 0xa7, 0x8a,
+ 0xf7, 0x3d, 0x9d, 0x64, 0xc7, 0x02, 0x39, 0x94, 0x6c, 0xba, 0x56, 0xd4,
+ 0x5a, 0x26, 0xb4, 0x9c, 0x92, 0x65, 0xd4, 0x95, 0xb1, 0xa9, 0xfb, 0x02,
+ 0xdc, 0xed, 0xee, 0x7b, 0x3e, 0x62, 0x18, 0xbc, 0x03, 0xc7, 0xdc, 0x95,
+ 0xee, 0x1f, 0xa5, 0x92, 0x5b, 0xbd, 0x16, 0xac, 0xfd, 0x91, 0x0a, 0xa5,
+ 0xa8, 0xa3, 0xcc, 0xcc, 0xf3, 0xe1, 0xe4, 0x21, 0xa6, 0x26, 0xb5, 0xa0,
+ 0x6f, 0xbe, 0x23, 0x61, 0xe7, 0x64, 0x05, 0x9f, 0xa4, 0x55, 0xfc, 0xb3,
+ 0x5d, 0x3b, 0x4c, 0x58, 0x3e, 0x5c, 0xfc, 0xdd, 0xcf, 0xa0, 0x06, 0x9d,
+ 0x50, 0x35, 0x7a, 0x32, 0xd1, 0x7c, 0x33, 0x6d, 0x36, 0x78, 0x56, 0xb7,
+ 0x4c, 0xf3, 0x0e, 0x80, 0xbd, 0x66, 0xf0, 0xdd, 0x34, 0x9c, 0xec, 0xab,
+ 0xf2, 0x4c, 0x9a, 0xaf, 0xa9, 0x67, 0x58, 0x70, 0x4c, 0xde, 0xe5, 0x44,
+ 0xc8, 0x74, 0x72, 0xe2, 0x00, 0x5b, 0x08, 0xc0, 0x58, 0x99, 0x7e, 0x5c,
+ 0x5e, 0x05, 0xd3, 0x51, 0xb1, 0x7b, 0x82, 0x41, 0xff, 0xf4, 0x3e, 0xfe,
+ 0x4c, 0xe0, 0x9c, 0x08, 0x7e, 0x63, 0x6d, 0x9e, 0xc0, 0xdd, 0x9a, 0xb4,
+ 0xae, 0xcf, 0xb8, 0x9e, 0x4a, 0x6c, 0xd5, 0x87, 0xab, 0x7b, 0x15, 0xd2,
+ 0x1f, 0x6f, 0xcc, 0x7d, 0x54, 0x3c, 0x6f, 0x3c, 0xf3, 0xd6, 0xd7, 0x6f,
+ 0x1d, 0x85, 0x26, 0x7f, 0x2a, 0x79, 0x0c, 0x1f, 0xdf, 0xdf, 0xf6, 0x68,
+ 0x58, 0xfd, 0x51, 0x0d, 0x82, 0x4f, 0xa6, 0xc3, 0xc3, 0x08, 0x3d, 0x4a,
+ 0xec, 0xb6, 0x26, 0xc6, 0xdc, 0xe4, 0xc8, 0xa8, 0x9d, 0x74, 0x4a, 0x07,
+ 0xff, 0x72, 0xff, 0x03, 0xb0, 0x71, 0x1c, 0x47, 0xdf, 0xfb, 0xf8, 0x1e,
+ 0x54, 0xea, 0xa0, 0xe8, 0x21, 0x3f, 0xc4, 0xa7, 0xfd, 0xab, 0x70, 0x34,
+ 0xcc, 0x09, 0x4a, 0xb8, 0x4f, 0x1b, 0x9d, 0xa8, 0x05, 0x9e, 0x78, 0x8a,
+ 0x44, 0x05, 0x8e, 0x66, 0x4e, 0xf0, 0xdc, 0xe3, 0xbf, 0x4f, 0xd9, 0xe2,
+ 0x6f, 0x9b, 0x5c, 0x51, 0xfc, 0xb6, 0xc9, 0x4a, 0x09, 0x7d, 0x79, 0x2b,
+ 0x2f, 0x49, 0xd0, 0x11, 0xca, 0x57, 0x03, 0xd8, 0x86, 0x2c, 0xb9, 0xdb,
+ 0xfb, 0x52, 0x6b, 0xac, 0xf9, 0x7e, 0x94, 0x73, 0xc9, 0xd0, 0x52, 0xab,
+ 0x71, 0xec, 0x7e, 0xb2, 0x7f, 0xda, 0x4f, 0xe4, 0xde, 0xc7, 0xba, 0xcc,
+ 0x5e, 0x9f, 0x8a, 0x35, 0x99, 0x80, 0xb8, 0x9d, 0xbf, 0x80, 0x2b, 0xa0,
+ 0x75, 0x8a, 0x60, 0x74, 0x31, 0xe2, 0x71, 0x1f, 0xd9, 0xc5, 0x3a, 0xf9,
+ 0x2e, 0x3d, 0x18, 0xe4, 0xaa, 0xd3, 0x62, 0x97, 0x4a, 0xd7, 0x0c, 0xbb,
+ 0x33, 0xdc, 0x62, 0x09, 0x9b, 0x07, 0xd3, 0x9d, 0xc1, 0xb7, 0x6b, 0x00,
+ 0x2b, 0x32, 0x9a, 0x6b, 0x9e, 0xfe, 0xbb, 0xb0, 0xb0, 0x00, 0x72, 0x12,
+ 0xac, 0x72, 0xed, 0xa7, 0xaf, 0xb7, 0xf3, 0x75, 0x9c, 0xb8, 0x15, 0x7f,
+ 0x2d, 0x23, 0x26, 0x0d, 0x76, 0xf5, 0x5e, 0xbe, 0xa4, 0xb2, 0x82, 0xe8,
+ 0xd8, 0x97, 0x36, 0xdb, 0xba, 0xaa, 0x47, 0x06, 0x1f, 0x57, 0x7d, 0x70,
+ 0x09, 0xe5, 0x31, 0xf2, 0xb2, 0x0e, 0x00, 0xcb, 0x34, 0x74, 0x10, 0x0f,
+ 0x98, 0xa5, 0xfe, 0x75, 0x8a, 0xd5, 0x0c, 0x6c, 0xc6, 0x59, 0xa0, 0xf2,
+ 0x0f, 0x67, 0x01, 0xfe, 0xa4, 0x99, 0xb1, 0x04, 0xc8, 0xd6, 0xbe, 0xa8,
+ 0x90, 0x24, 0x69, 0xa4, 0x4b, 0x63, 0x45, 0xfd, 0x4f, 0x17, 0x85, 0x42,
+ 0xdb, 0x69, 0xc3, 0xbd, 0xd6, 0xf4, 0xab, 0x96, 0xa1, 0xbf, 0x8b, 0xa7,
+ 0x23, 0x16, 0xc1, 0x31, 0x88, 0x14, 0xd7, 0x60, 0xe0, 0xe3, 0x16, 0xd2,
+ 0xa0, 0x44, 0x0e, 0xdc, 0x53, 0x50, 0xfb, 0xa0, 0x83, 0xfd, 0x69, 0xa7,
+ 0x86, 0x58, 0x35, 0x74, 0xc2, 0xa2, 0x8e, 0x89, 0x31, 0x07, 0xf5, 0x3b,
+ 0xd5, 0xa4, 0xc0, 0xd7, 0x8a, 0x37, 0x98, 0x2f, 0x7e, 0x9e, 0xe8, 0x19,
+ 0xd7, 0x6a, 0x0b, 0x74, 0x3f, 0x38, 0xd0, 0x32, 0xeb, 0xd1, 0x59, 0xea,
+ 0x24, 0x35, 0x7e, 0x27, 0xd0, 0xcd, 0x2f, 0x4e, 0x49, 0x07, 0xd7, 0xab,
+ 0x48, 0xc7, 0x10, 0x9f, 0xa7, 0x90, 0x84, 0x43, 0x1e, 0x55, 0x2f, 0x04,
+ 0x97, 0x42, 0x4a, 0x42, 0x4f, 0xc7, 0x48, 0xd5, 0x40, 0x8b, 0x03, 0x5b,
+ 0x4a, 0xb6, 0x35, 0xf9, 0x18, 0x0a, 0xc4, 0x1f, 0x79, 0x5c, 0xe0, 0xf7,
+ 0x41, 0x9c, 0x07, 0x89, 0xd0, 0x47, 0x4f, 0x78, 0xc3, 0x36, 0x17, 0x23,
+ 0x88, 0xa9, 0x3e, 0x61, 0x71, 0xf4, 0xb5, 0x3c, 0x48, 0x8c, 0xee, 0x98,
+ 0x3d, 0x2f, 0x6f, 0x6c, 0x9d, 0x37, 0x07, 0xcd, 0xe3, 0xf4, 0x2f, 0x59,
+ 0x3b, 0x61, 0xfc, 0xcd, 0x0c, 0xcd, 0xbe, 0x16, 0xd5, 0xd8, 0x75, 0x66,
+ 0x7e, 0xe9, 0xea, 0x0a, 0xee, 0xe4, 0xb6, 0x5c, 0x80, 0xad, 0x76, 0x1f,
+ 0x6e, 0x97, 0x00, 0x78, 0xfc, 0xe2, 0x4c, 0x22, 0x12, 0x82, 0xfc, 0x85,
+ 0xd5, 0x0d, 0x4b, 0x99, 0x06, 0x84, 0xf9, 0x38, 0x0c, 0xa6, 0x42, 0x5e,
+ 0x58, 0xe6, 0x68, 0x1f, 0x86, 0x91, 0x2f, 0x71, 0x8c, 0x1f, 0x59, 0x8d,
+ 0xeb, 0xd8, 0xc8, 0x2a, 0xab, 0xd5, 0xad, 0xb2, 0x70, 0x66, 0x72, 0x52,
+ 0xbb, 0xc2, 0xb4, 0x2a, 0x3d, 0xae, 0x71, 0xb9, 0x29, 0xcc, 0x53, 0xc8,
+ 0x89, 0xf2, 0xf7, 0xfc, 0xec, 0x1a, 0x9d, 0xa1, 0xa9, 0xa4, 0x1d, 0xdd,
+ 0x92, 0x5b, 0x36, 0x39, 0x15, 0x57, 0xc1, 0xf2, 0xbb, 0x8c, 0x0b, 0x7f,
+ 0x2f, 0x11, 0x6c, 0x7a, 0x1b, 0xec, 0x69, 0x7f, 0xf9, 0xfb, 0xd0, 0xf9,
+ 0x23, 0xc8, 0x40, 0x64, 0xdc, 0x30, 0xcb, 0xc5, 0x62, 0xca, 0x49, 0x52,
+ 0x97, 0xf7, 0x90, 0x72, 0x10, 0xb5, 0xea, 0x68, 0xe8, 0x9e, 0x90, 0xd0,
+ 0x9b, 0xe1, 0xd4, 0x0b, 0xc6, 0xd7, 0xfe, 0x2b, 0xe0, 0x74, 0xe1, 0xb8,
+ 0x46, 0xd3, 0x75, 0x8a, 0x60, 0x91, 0x79, 0x08, 0x0b, 0x34, 0xd9, 0x6c,
+ 0x8a, 0x51, 0x94, 0x37, 0x74, 0x88, 0x96, 0x44, 0xb0, 0x26, 0x54, 0x2a,
+ 0x0b, 0xa9, 0xbc, 0xb7, 0x0d, 0xff, 0x15, 0x99, 0x1c, 0x02, 0xb7, 0x5d,
+ 0x4f, 0x16, 0x7c, 0xdd, 0x15, 0xa6, 0x72, 0x1e, 0x6c, 0x28, 0xd6, 0x43,
+ 0xcc, 0x75, 0xb3, 0x1e, 0x0e, 0x6b, 0xa3, 0xef, 0xd4, 0x01, 0xab, 0x00,
+ 0xda, 0xc9, 0xd4, 0x96, 0xa4, 0x85, 0x37, 0x96, 0x93, 0xbf, 0x44, 0x1f,
+ 0x36, 0x77, 0xba, 0x08, 0xf4, 0x58, 0x29, 0xf3, 0x6b, 0xd8, 0x11, 0x57,
+ 0xf2, 0xf6, 0x0e, 0xa8, 0x8e, 0x0b, 0xea, 0x1d, 0xaa, 0xe1, 0x09, 0x35,
+ 0x03, 0x65, 0x57, 0x0f, 0x66, 0x8d, 0xf0, 0xe9, 0x1e, 0xa3, 0xc3, 0x65,
+ 0x95, 0x0c, 0x4d, 0x42, 0x95, 0x89, 0x75, 0xd8, 0xa1, 0x03, 0xe8, 0x19,
+ 0x2a, 0xe0, 0x88, 0x90, 0xc8, 0x7a, 0x73, 0x32, 0xd8, 0xb1, 0x30, 0x6f,
+ 0xaf, 0x5f, 0x55, 0xf2, 0x44, 0x19, 0x8d, 0xfb, 0x4a, 0xfa, 0x95, 0x31,
+ 0xea, 0xa0, 0xf1, 0xe2, 0xed, 0xa2, 0xd0, 0xbb, 0x26, 0x57, 0x5f, 0x55,
+ 0x89, 0x97, 0xdc, 0x17, 0x88, 0x3e, 0x2c, 0xb7, 0xdc, 0xb6, 0x6b, 0xaa,
+ 0x1b, 0x6f, 0x34, 0x55, 0x1d, 0x0d, 0x51, 0xa0, 0xb3, 0x95, 0x84, 0x4a,
+ 0x64, 0x83, 0xc1, 0x91, 0x71, 0x49, 0x94, 0x3b, 0x41, 0x3d, 0xa4, 0x81,
+ 0xf0, 0x4a, 0x6a, 0xcf, 0xcf, 0x79, 0x3f, 0xdb, 0xce, 0x37, 0xd0, 0xab,
+ 0x05, 0x3f, 0xc7, 0x76, 0xf3, 0x64, 0x8d, 0x2a, 0x7e, 0xbd, 0x0f, 0xc6,
+ 0xc9, 0xd5, 0x7e, 0x5c, 0x9a, 0xd1, 0x22, 0x44, 0x28, 0x43, 0xd1, 0x7a,
+ 0x49, 0xe6, 0x3d, 0x2f, 0x55, 0xb6, 0x73, 0xfe, 0xb1, 0x53, 0x4b, 0x11,
+ 0x2f, 0x3d, 0x8c, 0x2f, 0xba, 0x8b, 0x80, 0x85, 0xb3, 0x34, 0xb1, 0x07,
+ 0x54, 0x43, 0xee, 0x4d, 0xdf, 0x95, 0x87, 0x8f, 0xe2, 0xc1, 0x30, 0xf1,
+ 0x62, 0xe6, 0x03, 0x7b, 0x8d, 0xa5, 0x66, 0xed, 0x5b, 0xb0, 0xf6, 0x7f,
+ 0x6c, 0xd1, 0xad, 0xf8, 0x7f, 0x4b, 0x51, 0x89, 0x11, 0xcb, 0x19, 0x8e,
+ 0x47, 0x0d, 0x21, 0x2f, 0x83, 0xa9, 0x12, 0x0b, 0x2e, 0xf8, 0x8b, 0x9b,
+ 0xe6, 0xb3, 0x27, 0xd5, 0xe8, 0x29, 0xae, 0x83, 0x2d, 0xb7, 0x34, 0xc3,
+ 0x07, 0x15, 0xb6, 0x20, 0xdf, 0x21, 0x62, 0x0d, 0xd8, 0x3c, 0x91, 0xd2,
+ 0x49, 0x39, 0xe5, 0x1f, 0x69, 0xf2, 0xfd, 0x6e, 0xdc, 0x33, 0xfc, 0xe2,
+ 0x04, 0x73, 0xdd, 0x50, 0x8e, 0x6b, 0x69, 0xe5, 0x19, 0x41, 0x77, 0xfc,
+ 0xb2, 0x85, 0x9a, 0x2c, 0xb8, 0xd1, 0x7b, 0xb5, 0x7b, 0xb2, 0xe8, 0x11,
+ 0x30, 0x89, 0x9d, 0x0f, 0x88, 0xc9, 0x78, 0xce, 0x2d, 0x6e, 0x4f, 0xb6,
+ 0xaa, 0xe1, 0xca, 0x7f, 0x4b, 0x85, 0xf5, 0x5b, 0xf2, 0x2c, 0xfe, 0x7f,
+ 0xd9, 0x25, 0xca, 0xc9, 0x6b, 0xae, 0x81, 0xa2, 0x0f, 0x82, 0xe4, 0x7d,
+ 0xcf, 0xd0, 0x92, 0x21, 0xdc, 0x45, 0xee, 0x7b, 0x67, 0x19, 0x73, 0x72,
+ 0x29, 0xa1, 0x6d, 0x67, 0xb3, 0x9f, 0xc8, 0x39, 0xbc, 0x7d, 0xeb, 0x96,
+ 0x96, 0xa1, 0xf8, 0xe1, 0xab, 0xf0, 0x9a, 0x18, 0xb3, 0xf7, 0x31, 0xd4,
+ 0x35, 0x5a, 0x82, 0xc6, 0x13, 0xe6, 0xb1, 0x1e, 0xdd, 0xc4, 0x70, 0xc6,
+ 0x54, 0x97, 0x6b, 0x9c, 0xda, 0xe1, 0x25, 0xa4, 0x87, 0x2d, 0xab, 0x78,
+ 0x0c, 0xb7, 0x99, 0x0f, 0x21, 0x0e, 0xac, 0xe1, 0x35, 0x76, 0xb6, 0x9e,
+ 0x1d, 0x60, 0x49, 0x05, 0x0c, 0xee, 0x17, 0x50, 0x34, 0x51, 0xa4, 0xff,
+ 0x17, 0xad, 0xcb, 0xe2, 0xf7, 0x09, 0x99, 0xf9, 0x54, 0xf8, 0xe9, 0xb9,
+ 0xa9, 0x72, 0x1a, 0xc7, 0x92, 0xdd, 0x54, 0x75, 0x72, 0x86, 0xbb, 0xff,
+ 0xd5, 0x8d, 0x6f, 0x30, 0xd3, 0xef, 0xd8, 0x0b, 0x1f, 0x38, 0x6c, 0x1e,
+ 0x4e, 0x54, 0x26, 0x57, 0x71, 0x6b, 0x6d, 0x82, 0x2d, 0x02, 0xac, 0xd8,
+ 0xd9, 0xa7, 0x67, 0x79, 0x39, 0x3c, 0x82, 0x2a, 0x8a, 0x6e, 0x76, 0x9e,
+ 0x67, 0xe8, 0x9b, 0xb7, 0x68, 0x1e, 0x50, 0xcf, 0x50, 0x62, 0x61, 0x84,
+ 0xe2, 0x2f, 0x10, 0xb7, 0xc9, 0x40, 0x90, 0x63, 0x68, 0x4b, 0x4d, 0x20,
+ 0x09, 0xdc, 0x7a, 0x1a, 0xb8, 0x64, 0x25, 0xaf, 0xac, 0x60, 0x49, 0x30,
+ 0xbb, 0xeb, 0xe3, 0xe9, 0x03, 0x66, 0x4e, 0xf1, 0x7f, 0xa3, 0x89, 0x21,
+ 0xff, 0xce, 0xd9, 0xc7, 0xb0, 0x2c, 0x9a, 0xb5, 0x97, 0x11, 0x45, 0xf9,
+ 0x59, 0xf0, 0xff, 0x7a, 0x75, 0xdf, 0x91, 0xc4, 0xa0, 0x24, 0x59, 0x52,
+ 0x77, 0x4a, 0x5c, 0x26, 0xbc, 0x03, 0xe8, 0x9b, 0x21, 0x38, 0xb8, 0xe0,
+ 0x0e, 0x95, 0xc1, 0x4d, 0x03, 0x07, 0xf5, 0xae, 0x7b, 0x6a, 0x28, 0xbf,
+ 0x3b, 0x76, 0x0f, 0x59, 0x72, 0x7d, 0xf0, 0x3c, 0xae, 0x46, 0xe9, 0xcd,
+ 0x48, 0x34, 0x8a, 0xa2, 0xe7, 0x29, 0x27, 0x9b, 0xed, 0x3e, 0x19, 0x59,
+ 0x7c, 0x49, 0x8e, 0xf3, 0x8b, 0x48, 0xbf, 0xd5, 0xa7, 0xf3, 0x5f, 0xff,
+ 0xda, 0x0d, 0xc2, 0x6f, 0x50, 0xf7, 0xc6, 0x13, 0x6b, 0x3d, 0x00, 0x69,
+ 0x23, 0x97, 0x6d, 0x12, 0xaa, 0xd2, 0xff, 0x8c, 0x58, 0x1e, 0x3d, 0x93,
+ 0x84, 0x7e, 0xeb, 0xd5, 0xec, 0xf2, 0x7a, 0x02, 0x00, 0x71, 0x18, 0x6d,
+ 0x4a, 0x7a, 0x54, 0x25, 0x8b, 0x4e, 0x8b, 0xad, 0xa1, 0xbd, 0x4a, 0xaa,
+ 0xbd, 0x54, 0x54, 0xdb, 0x24, 0x47, 0x18, 0xc9, 0x0c, 0xee, 0x8c, 0x69,
+ 0x99, 0x85, 0x99, 0x75, 0x1f, 0x05, 0xa7, 0xa8, 0x8b, 0xcc, 0xb1, 0xf6,
+ 0xe4, 0x00, 0xcb, 0xa8, 0x8d, 0xf3, 0xf0, 0xc9, 0x98, 0xbf, 0xf2, 0x91,
+ 0x5f, 0xbd, 0x2b, 0x41, 0x7a, 0x41, 0xe6, 0x69, 0x65, 0xd7, 0xfb, 0xf2,
+ 0xc2, 0x4b, 0x80, 0x2d, 0xa8, 0x3c, 0xed, 0x91, 0xca, 0x63, 0xb1, 0xf8,
+ 0xf6, 0x31, 0x95, 0xfe, 0xa0, 0xc6, 0xc2, 0xf6, 0xa3, 0xa9, 0x22, 0x90,
+ 0xd7, 0x0d, 0xa5, 0xdd, 0x08, 0xd6, 0xe2, 0x3d, 0x41, 0x0c, 0xb2, 0xfb,
+ 0x34, 0x3a, 0x67, 0x4b, 0x71, 0xe6, 0x56, 0x44, 0x52, 0x44, 0x74, 0xdb,
+ 0xff, 0x8b, 0x72, 0xbf, 0x2f, 0x1e, 0xb3, 0x8f, 0xd5, 0xaf, 0x7e, 0xd8,
+ 0x55, 0xd9, 0x52, 0xbe, 0xcf, 0x06, 0x0f, 0x55, 0xd5, 0x70, 0x29, 0x49,
+ 0x30, 0x07, 0x32, 0xa1, 0x0f, 0x78, 0x4b, 0x55, 0x8f, 0x54, 0xfd, 0x1f,
+ 0x08, 0xb3, 0x9c, 0xcc, 0xb6, 0xb7, 0xbc, 0x89, 0x54, 0xb3, 0x0e, 0x9b,
+ 0xbb, 0x39, 0x46, 0x59, 0x45, 0x94, 0x34, 0x17, 0x8f, 0x83, 0x28, 0x70,
+ 0x50, 0x8d, 0x0f, 0xc4, 0x7a, 0x5b, 0x43, 0x96, 0x3d, 0x0a, 0x7c, 0xeb,
+ 0x68, 0x70, 0xb9, 0x33, 0x0d, 0x49, 0x1f, 0x8c, 0x0c, 0x37, 0x69, 0x65,
+ 0xa6, 0x54, 0xf3, 0xba, 0xee, 0xe0, 0x0e, 0xc1, 0x4f, 0x30, 0xbe, 0x09,
+ 0xc6, 0x6d, 0x39, 0xea, 0x09, 0x01, 0x63, 0x53, 0xf5, 0xdd, 0x1b, 0x51,
+ 0x1a, 0x9b, 0x33, 0x1d, 0x19, 0x0d, 0x90, 0x1e, 0x29, 0x89, 0x0a, 0xe1,
+ 0xa4, 0xbe, 0x94, 0x67, 0xda, 0xfd, 0x24, 0x0d, 0xd8, 0x00, 0xa2, 0x04,
+ 0x67, 0x9c, 0x90, 0xaf, 0xb9, 0xf4, 0xbb, 0x8e, 0xdd, 0xc0, 0xbc, 0x61,
+ 0x18, 0xca, 0x7a, 0x20, 0xe7, 0xb2, 0x42, 0xa9, 0xb1, 0x18, 0xe4, 0x32,
+ 0x5c, 0xba, 0x60, 0x2c, 0x2c, 0xfc, 0x2c, 0x1f, 0x8b, 0x03, 0x9f, 0xaa,
+ 0x8a, 0x92, 0xa7, 0xb0, 0xbf, 0x0a, 0x3b, 0x73, 0x7d, 0xd3, 0x18, 0xf5,
+ 0xe1, 0xf5, 0xd6, 0xe7, 0x9d, 0x59, 0x9a, 0x45, 0xf4, 0x5e, 0xeb, 0x77,
+ 0xc1, 0x8c, 0x4d, 0x16, 0x73, 0xaa, 0x08, 0x9d, 0x11, 0x57, 0xc1, 0x06,
+ 0xce, 0xa3, 0xc4, 0xed, 0xfe, 0x91, 0x8c, 0xf6, 0x2d, 0x07, 0xaa, 0x16,
+ 0xd3, 0x84, 0x11, 0xe6, 0x6d, 0x06, 0x66, 0x33, 0xa4, 0xe5, 0xd9, 0xee,
+ 0xfd, 0xdc, 0x18, 0xd8, 0x21, 0x3c, 0xcb, 0xf7, 0xa8, 0x39, 0x5a, 0x91,
+ 0xd1, 0x4b, 0x31, 0x2d, 0x6b, 0xa1, 0x6f, 0x96, 0xa3, 0x83, 0x2a, 0x3b,
+ 0xc1, 0xa0, 0x20, 0xb6, 0xa3, 0x60, 0xce, 0x4d, 0xf2, 0xb3, 0xce, 0x77,
+ 0xfe, 0x43, 0x4b, 0x6a, 0xc9, 0x4a, 0xe5, 0xe9, 0x83, 0xb9, 0xb2, 0x17,
+ 0x10, 0x3a, 0xae, 0xaa, 0x00, 0xc2, 0x8d, 0x06, 0xf3, 0x43, 0x98, 0x53,
+ 0x23, 0x23, 0x5e, 0x5b, 0x4b, 0xb5, 0xa8, 0xd4, 0x90, 0x33, 0x55, 0x58,
+ 0xc0, 0xb8, 0x8e, 0x46, 0xca, 0x5c, 0x94, 0x15, 0x81, 0xa9, 0xf4, 0x2a,
+ 0xb9, 0xc5, 0xbe, 0x8e, 0x4d, 0x13, 0xd2, 0x5a, 0x4c, 0x1f, 0x59, 0x99,
+ 0xb3, 0x80, 0x2d, 0x69, 0xe9, 0x09, 0x2d, 0x77, 0xcd, 0xb6, 0x6a, 0x15,
+ 0xe1, 0x37, 0xb6, 0x25, 0xd3, 0x32, 0xef, 0xd2, 0x2b, 0xcc, 0x61, 0x69,
+ 0xc3, 0x46, 0x74, 0x53, 0xb4, 0x3c, 0x46, 0x1f, 0xfe, 0x95, 0x86, 0x70,
+ 0xe8, 0x2d, 0xcb, 0x9b, 0xb9, 0x28, 0x7c, 0x5b, 0xec, 0x68, 0x19, 0x58,
+ 0x8d, 0xd7, 0x51, 0x05, 0x1f, 0x17, 0x87, 0x4f, 0x97, 0xea, 0x7b, 0xfb,
+ 0xae, 0xe2, 0x86, 0x1d, 0x0d, 0x70, 0x43, 0x05, 0xe5, 0x9a, 0x86, 0x56,
+ 0xbb, 0x0d, 0x78, 0x3e, 0xa2, 0x75, 0xe4, 0x8a, 0x06, 0x1e, 0x7a, 0x56,
+ 0x9e, 0x85, 0x99, 0xe0, 0x41, 0xb9, 0x6b, 0xef, 0xee, 0x72, 0x82, 0x87,
+ 0xa4, 0xfd, 0x43, 0x0f, 0x5d, 0xb8, 0xc5, 0x1a, 0xe4, 0x7d, 0x5c, 0xce,
+ 0x76, 0x10, 0x05, 0x4a, 0x2a, 0x2c, 0x2c, 0x5b, 0xa0, 0x85, 0x13, 0xfa,
+ 0x55, 0x84, 0x02, 0x94, 0x0f, 0x78, 0x9d, 0x77, 0x8c, 0x36, 0x83, 0x94,
+ 0x63, 0xc2, 0xf4, 0xf7, 0x5b, 0x25, 0x10, 0xf8, 0x01, 0x67, 0x9e, 0x8c,
+ 0x07, 0xb0, 0xd2, 0xbe, 0x7a, 0x3c, 0x1a, 0x2e, 0x87, 0x0e, 0x2a, 0x9f,
+ 0xb2, 0x64, 0x9f, 0x8f, 0xbc, 0xe2, 0xe7, 0x00, 0xb2, 0xf4, 0x1e, 0xae,
+ 0xad, 0xd2, 0x79, 0xf7, 0x98, 0xdd, 0xa1, 0x73, 0xbc, 0x89, 0x31, 0xec,
+ 0x6b, 0x79, 0x97, 0x75, 0xd8, 0xd0, 0x68, 0x7b, 0xd5, 0xc9, 0x34, 0x41,
+ 0x6c, 0xb0, 0x73, 0x63, 0xbf, 0xda, 0xbb, 0x3c, 0xf9, 0xe4, 0xa8, 0x1b,
+ 0xeb, 0x6b, 0xd8, 0xcc, 0xb9, 0x03, 0xbb, 0xbd, 0xe1, 0x98, 0x81, 0x20,
+ 0x37, 0x47, 0x57, 0x56, 0x01, 0x7b, 0xb5, 0xc7, 0x4f, 0x82, 0x12, 0x56,
+ 0xaa, 0x09, 0x5c, 0x41, 0x21, 0x99, 0x05, 0x48, 0x9b, 0x4e, 0xe9, 0x75,
+ 0x5f, 0x77, 0x7c, 0x64, 0xb6, 0xee, 0x62, 0xb0, 0x62, 0x9b, 0x76, 0x2a,
+ 0xb5, 0x3d, 0x67, 0xbc, 0x77, 0x8a, 0xd5, 0x2c, 0x91, 0x46, 0x73, 0x96,
+ 0x0f, 0xf7, 0x60, 0x77, 0x29, 0x0c, 0xa1, 0x87, 0x47, 0x4f, 0x05, 0xe5,
+ 0xb3, 0xf0, 0x9a, 0x71, 0x03, 0x06, 0xeb, 0xba, 0xb7, 0x79, 0xae, 0x79,
+ 0x0e, 0x1a, 0x71, 0xd9, 0xcb, 0x98, 0x78, 0x11, 0x18, 0xd0, 0xd5, 0x8b,
+ 0x70, 0x9a, 0x84, 0x00, 0xbd, 0x75, 0x65, 0x92, 0xef, 0xa5, 0x16, 0x77,
+ 0x8f, 0xaf, 0x53, 0x4b, 0x11, 0x35, 0x03, 0x0f, 0xc6, 0xf8, 0x87, 0x79,
+ 0xd4, 0xb1, 0xc2, 0xc4, 0x8d, 0x66, 0x4c, 0xa7, 0x90, 0xc7, 0x7d, 0x61,
+ 0x35, 0x0b, 0x38, 0xe2, 0x78, 0x5f, 0xd9, 0xa3, 0x00, 0x72, 0x77, 0x71,
+ 0xc5, 0x0e, 0xbc, 0x42, 0x23, 0x77, 0x12, 0x05, 0xd1, 0xa6, 0xa0, 0x93,
+ 0x45, 0xe9, 0xf2, 0x55, 0x5d, 0x3d, 0x5a, 0x31, 0x5f, 0x41, 0xac, 0x52,
+ 0x6f, 0x2e, 0x2b, 0xa5, 0xf7, 0x48, 0x00, 0xa6, 0x95, 0x6f, 0x5e, 0x9a,
+ 0x37, 0x31, 0x15, 0x30, 0xbf, 0xe5, 0x24, 0xcb, 0x8b, 0x26, 0x35, 0x41,
+ 0x9e, 0x11, 0x2a, 0x5b, 0xc3, 0x74, 0xb6, 0xd5, 0x8c, 0xdc, 0x65, 0x9e,
+ 0x8c, 0x27, 0x01, 0x0f, 0x79, 0x7a, 0x12, 0xa8, 0x4e, 0x91, 0x77, 0xdc,
+ 0x78, 0x4b, 0xa3, 0xcc, 0xa3, 0xab, 0x71, 0x20, 0x18, 0x2a, 0xb3, 0xec,
+ 0xd6, 0xdc, 0x2f, 0x1e, 0x48, 0xbd, 0x3f, 0x09, 0x98, 0xb8, 0x77, 0x96,
+ 0xd7, 0x64, 0x9a, 0x11, 0x32, 0x25, 0x38, 0xe7, 0xfa, 0x6b, 0x96, 0x87,
+ 0xa2, 0x47, 0x4b, 0xe8, 0x98, 0xd0, 0x40, 0x00, 0x00, 0x09, 0x7a, 0x14,
+ 0xce, 0x46, 0x81, 0xce, 0x36, 0x5e, 0x30, 0xed, 0xbf, 0x3f, 0xbe, 0x3b,
+ 0x1d, 0xde, 0x70, 0x1a, 0x3f, 0xf2, 0x8c, 0x0b, 0x90, 0x90, 0xad, 0x64,
+ 0x6b, 0xbf, 0xff, 0xa2, 0x34, 0xb0, 0x9d, 0x24, 0xc1, 0x0e, 0x21, 0xa6,
+ 0xc7, 0xde, 0x89, 0x9c, 0x5d, 0x48, 0xc0, 0xd4, 0x5f, 0x19, 0x39, 0x60,
+ 0x4e, 0xea, 0xf5, 0xe1, 0xa9, 0xe4, 0xa3, 0x1b, 0xf2, 0x2b, 0xb8, 0x00,
+ 0x03, 0x99, 0x4b, 0xd7, 0x9a, 0xf1, 0x5b, 0x58, 0xbb, 0xc7, 0xb1, 0x70,
+ 0xb0, 0x08, 0x73, 0xe0, 0xd3, 0x17, 0xe7, 0x51, 0x1e, 0xa4, 0x05, 0x43,
+ 0x6f, 0x3d, 0x2a, 0x70, 0x88, 0x3f, 0xd8, 0xd9, 0x2e, 0x3d, 0x25, 0x8e,
+ 0x4d, 0x6c, 0x8c, 0xdb, 0x89, 0x2c, 0x0b, 0x6a, 0xea, 0x07, 0x41, 0xac,
+ 0x60, 0x62, 0x63, 0xfe, 0x4a, 0x5e, 0x1f, 0x70, 0x9f, 0x02, 0x40, 0x99,
+ 0x6d, 0x55, 0xef, 0x61, 0xe0, 0x8f, 0xae, 0x7b, 0x00, 0xea, 0x70, 0x62,
+ 0x3b, 0x17, 0x3a, 0x66, 0xe2, 0x16, 0xb2, 0xd6, 0x05, 0xc8, 0xb4, 0xe7,
+ 0xd6, 0x71, 0x33, 0x1b, 0x19, 0x93, 0xf9, 0x40, 0xff, 0x4f, 0x55, 0xb5,
+ 0x47, 0xce, 0x91, 0x47, 0x95, 0xd0, 0x69, 0x8f, 0x4d, 0xf3, 0x89, 0x7a,
+ 0x0a, 0xa7, 0x0f, 0x56, 0x41, 0x67, 0x64, 0x3a, 0x16, 0x83, 0xe3, 0xb0,
+ 0xd8, 0x04, 0x55, 0x54, 0x35, 0xae, 0x90, 0xcd, 0x93, 0x2e, 0x69, 0x56,
+ 0xfa, 0xa2, 0xdb, 0xa2, 0xb2, 0x56, 0xee, 0xf1, 0x22, 0x8a, 0xe9, 0x9b,
+ 0x33, 0x2d, 0xa4, 0x19, 0x29, 0x78, 0x8f, 0xad, 0xd2, 0xca, 0x8c, 0x96,
+ 0x5c, 0xdd, 0x06, 0x7a, 0xbf, 0x2e, 0x06, 0x25, 0xee, 0x63, 0x02, 0xb0,
+ 0x72, 0x48, 0xcc, 0x36, 0x94, 0x49, 0x07, 0x0b, 0x33, 0x4c, 0x32, 0x09,
+ 0xe7, 0xee, 0x61, 0xe0, 0x90, 0x8a, 0x0f, 0xc8, 0xf5, 0x10, 0x48, 0x93,
+ 0x12, 0x2d, 0x58, 0xe9, 0xdd, 0x04, 0x00, 0x00, 0x38, 0xbc, 0x24, 0x80,
+ 0xcb, 0xc8, 0xa8, 0x8f, 0xb2, 0x8b, 0x97, 0x76, 0x76, 0x15, 0x39, 0x6f,
+ 0x0e, 0xd5, 0xd5, 0x25, 0x4c, 0xb8, 0xb6, 0x61, 0xa2, 0x9e, 0xfe, 0xf5,
+ 0x74, 0xc2, 0x2f, 0x98, 0x51, 0x6f, 0x0b, 0x51, 0xa0, 0xf4, 0xa8, 0x4a,
+ 0xfb, 0x10, 0x5f, 0x5e, 0xf2, 0xdd, 0x90, 0xa6, 0x37, 0xa3, 0x01, 0xcc,
+ 0xbd, 0x91, 0x27, 0xaa, 0x57, 0xc2, 0xa0, 0x14, 0x00, 0x00, 0x04, 0x08,
+ 0xf0, 0x2d, 0x71, 0x3d, 0x2f, 0x78, 0x21, 0x9e, 0x7d, 0xde, 0x31, 0x21,
+ 0x1c, 0x47, 0xe0, 0xe5, 0x44, 0x82, 0x78, 0x20, 0xdc, 0x87, 0x5d, 0xd5,
+ 0x85, 0xd7, 0x0c, 0x14, 0x81, 0xd9, 0x93, 0x5d, 0xb8, 0x30, 0x7e, 0x79,
+ 0xf2, 0xd0, 0x86, 0xd5, 0xf7, 0x07, 0x9e, 0x44, 0xcb, 0xfd, 0xfb, 0xfd,
+ 0x01, 0xbb, 0x91, 0x16, 0x0b, 0x86, 0x45, 0xe5, 0xc0, 0x20, 0x1f, 0x18,
+ 0x02, 0xa1, 0x31, 0x0f, 0x6a, 0x05, 0x08, 0x92, 0x11, 0xcc, 0x7c, 0xfa,
+ 0xa3, 0xf5, 0x06, 0x49, 0x42, 0xfe, 0x0a, 0x71, 0xa9, 0xbe, 0x40, 0xc9,
+ 0x24, 0xe3, 0xec, 0x12, 0x5a, 0x24, 0xa9, 0xfa, 0x48, 0x65, 0xf5, 0x4f,
+ 0xdf, 0x1d, 0xaf, 0xf4, 0x0b, 0x65, 0x3f, 0x48, 0x85, 0x8d, 0x5f, 0x30,
+ 0x02, 0xb8, 0x06, 0x87, 0xbd, 0x91, 0x6f, 0x51, 0x17, 0xd5, 0xa7, 0x0b,
+ 0xe3, 0x36, 0x3c, 0xc1, 0x28, 0xb7, 0x85, 0x2a, 0x0b, 0x6f, 0x91, 0xbb,
+ 0x29, 0x88, 0x1c, 0x4c, 0x3e, 0x1c, 0x13, 0xca, 0x31, 0x90, 0xdf, 0x76,
+ 0x5b, 0x46, 0x73, 0xb7, 0x85, 0x3e, 0xa7, 0x2c, 0x92, 0xf0, 0x70, 0x50,
+ 0x5b, 0x6b, 0xf6, 0x05, 0x0d, 0x15, 0x94, 0x42, 0x27, 0x67, 0x62, 0xb4,
+ 0x1c, 0x80, 0x47, 0x57, 0xb4, 0x39, 0xa7, 0xc7, 0xb3, 0x04, 0x4e, 0xa9,
+ 0x05, 0x36, 0xce, 0x22, 0x89, 0x89, 0xe0, 0xba, 0x1a, 0xd9, 0x38, 0x88,
+ 0x29, 0x00, 0xc0, 0xff, 0x02, 0x67, 0xc8, 0x85, 0xfc, 0xda, 0xcf, 0x04,
+ 0x83, 0x45, 0xea, 0xfa, 0x6d, 0x31, 0x85, 0x9d, 0x45, 0xea, 0x75, 0x13,
+ 0xb3, 0x8c, 0x27, 0x01, 0x06, 0xf2, 0x90, 0x7a, 0x85, 0x50, 0x5d, 0x0a,
+ 0xae, 0x5e, 0x49, 0x13, 0x55, 0x01, 0xbb, 0xbd, 0x4b, 0x96, 0x62, 0x69,
+ 0x7c, 0x06, 0xd3, 0xac, 0x1b, 0xd7, 0x8b, 0x71, 0x61, 0x0f, 0x05, 0xfa,
+ 0x01, 0x04, 0x46, 0x13, 0xf7, 0xae, 0x00, 0x6c, 0x13, 0x20, 0x6f, 0x99,
+ 0xeb, 0x8f, 0x3e, 0xc9, 0x15, 0x41, 0x25, 0x72, 0x9f, 0x7f, 0x79, 0x80,
+ 0x45, 0x52, 0x58, 0x37, 0xda, 0xb3, 0x49, 0x21, 0x3f, 0x83, 0x01, 0x5b,
+ 0xba, 0xf2, 0x0a, 0x9d, 0x15, 0x0b, 0xba, 0x13, 0x7e, 0x51, 0xad, 0x3a,
+ 0xb5, 0xbc, 0x84, 0xee, 0xa8, 0x6b, 0x20, 0x20, 0x86, 0x68, 0x90, 0x83,
+ 0xd5, 0xc9, 0xa2, 0x7d, 0x78, 0x74, 0xdc, 0xff, 0xc1, 0xfe, 0x2c, 0x1e,
+ 0xb4, 0x09, 0x2d, 0x67, 0x9f, 0x6e, 0xd5, 0xfa, 0x18, 0x27, 0xf3, 0x20,
+ 0x1d, 0x02, 0x08, 0x20, 0xe6, 0x2f, 0xe2, 0x6d, 0x52, 0x0f, 0xc4, 0x04,
+ 0x00, 0x03, 0xfc, 0x05, 0x39, 0xf8, 0x81, 0x75, 0x81, 0x31, 0x82, 0x6b,
+ 0xb6, 0x0f, 0xb9, 0xfb, 0xb5, 0xb6, 0x05, 0x38, 0x6d, 0x02, 0x97, 0x27,
+ 0xa5, 0x34, 0x04, 0x3d, 0x5c, 0x18, 0x4f, 0x25, 0x01, 0x30, 0xaf, 0xec,
+ 0x5e, 0xf1, 0xef, 0x1b, 0xaa, 0x24, 0x40, 0x97, 0x3f, 0x65, 0x6a, 0x38,
+ 0xb5, 0x8c, 0xfc, 0x72, 0x2a, 0x37, 0x49, 0x2e, 0x0c, 0x57, 0x0f, 0x98,
+ 0x23, 0x63, 0x5c, 0x71, 0xa9, 0x5f, 0x59, 0x61, 0xa7, 0xd7, 0xd1, 0x1f,
+ 0x3a, 0xf6, 0x56, 0x51, 0xef, 0x53, 0x18, 0xc7, 0xf1, 0xd5, 0x74, 0x9d,
+ 0xba, 0xac, 0x62, 0xa3, 0x1e, 0xd4, 0xc6, 0xa3, 0xeb, 0x62, 0xfc, 0x0c,
+ 0xa1, 0xfb, 0xb9, 0xe2, 0xc3, 0x00, 0x21, 0xe0, 0x9a, 0x20, 0x8a, 0x5f,
+ 0x03, 0xfa, 0x0e, 0x51, 0x77, 0x7e, 0xc3, 0xdf, 0x57, 0x81, 0x66, 0xf8,
+ 0x90, 0x5a, 0x1a, 0xd5, 0x92, 0xce, 0x43, 0x89, 0x5b, 0xbd, 0xf2, 0x65,
+ 0x85, 0xe5, 0x65, 0x30, 0xfc, 0x09, 0x9f, 0xad, 0xa1, 0xbb, 0x88, 0xfd,
+ 0xde, 0x24, 0xc8, 0x58, 0x55, 0xd8, 0x98, 0x41, 0xe9, 0x99, 0x94, 0xcb,
+ 0x2c, 0xec, 0x67, 0xf8, 0x22, 0x07, 0x16, 0x20, 0x1c, 0x95, 0xcd, 0x12,
+ 0x6d, 0x7d, 0x55, 0x7a, 0xb0, 0xec, 0xee, 0xb6, 0x15, 0x7c, 0x13, 0x37,
+ 0xc5, 0xaf, 0x24, 0xe2, 0x96, 0x3c, 0x21, 0x38, 0x65, 0x97, 0xc4, 0xc6,
+ 0xe7, 0x64, 0x48, 0xc7, 0x54, 0x4b, 0xac, 0xce, 0xfb, 0xe9, 0xd6, 0x3e,
+ 0xec, 0x05, 0xfe, 0xb7, 0xda, 0x80, 0x5c, 0x2c, 0x13, 0x5b, 0x3f, 0x6f,
+ 0x26, 0xc0, 0x15, 0xf0, 0x24, 0x4f, 0x26, 0x97, 0x32, 0x55, 0xe6, 0xc2,
+ 0x67, 0x60, 0x14, 0x61, 0xc0, 0xb9, 0xa4, 0x59, 0x12, 0x7b, 0x98, 0xa6,
+ 0x46, 0x24, 0xb1, 0x50, 0xe4, 0x1a, 0x22, 0x38, 0x1e, 0x0b, 0x13, 0xe0,
+ 0xab, 0xc7, 0xdb, 0x27, 0x6f, 0x67, 0x58, 0x49, 0x8e, 0x01, 0x2e, 0x76,
+ 0xad, 0x12, 0xb6, 0x12, 0x98, 0x07, 0x86, 0x16, 0xfb, 0x14, 0xee, 0x00,
+ 0x67, 0x8d, 0x43, 0x6b, 0x9b, 0xf9, 0x07, 0x0a, 0xb4, 0xbd, 0xe6, 0xfb,
+ 0xbb, 0x9a, 0xef, 0x83, 0xbb, 0x3c, 0x2d, 0x44, 0xb0, 0xf9, 0xd3, 0xae,
+ 0x8d, 0x1d, 0x99, 0x4c, 0x95, 0x27, 0xdf, 0x19, 0x03, 0x16, 0xdf, 0x7b,
+ 0xca, 0x64, 0x73, 0x75, 0xa8, 0xb1, 0xf8, 0x38, 0x6a, 0x1c, 0x87, 0x1b,
+ 0xd1, 0xf5, 0x83, 0x72, 0x07, 0x98, 0x95, 0xba, 0x12, 0xdb, 0xf6, 0x5d,
+ 0x59, 0x74, 0xa3, 0x47, 0xe5, 0x3b, 0xed, 0x10, 0xc5, 0x65, 0x52, 0x2e,
+ 0x8e, 0x30, 0xa5, 0x28, 0x7d, 0x3c, 0x12, 0xde, 0xef, 0xcc, 0xbf, 0x67,
+ 0x1f, 0xcd, 0xa8, 0xaa, 0x1b, 0xa9, 0x21, 0x45, 0x08, 0x4a, 0x69, 0xd9,
+ 0x0a, 0x3f, 0x5e, 0x11, 0x76, 0x10, 0x55, 0xa0, 0xaa, 0x04, 0x16, 0x52,
+ 0x73, 0x02, 0x09, 0x5f, 0x31, 0x17, 0xb4, 0x7c, 0x7d, 0xc9, 0x70, 0x16,
+ 0x57, 0x10, 0xd9, 0x09, 0xda, 0xa3, 0x5f, 0x94, 0xaf, 0xca, 0x44, 0xe4,
+ 0xd5, 0xa9, 0xe6, 0x5f, 0x53, 0x3b, 0x18, 0xf7, 0xff, 0xf2, 0x70, 0x8b,
+ 0x14, 0x61, 0xe5, 0xaa, 0x39, 0xd7, 0x21, 0xad, 0x49, 0x14, 0x9f, 0x14,
+ 0x1e, 0x57, 0x9b, 0x78, 0x92, 0x85, 0x5e, 0x17, 0x49, 0x5a, 0xd3, 0x7c,
+ 0xdb, 0x30, 0x5b, 0xe9, 0x56, 0x2e, 0xd6, 0xe2, 0x09, 0xbd, 0xd0, 0x78,
+ 0xe1, 0xb4, 0x5b, 0x50, 0x4b, 0x50, 0x76, 0x29, 0xbc, 0x8b, 0x5f, 0x38,
+ 0x56, 0xac, 0xb5, 0xb5, 0x3f, 0xbe, 0x79, 0x68, 0x7b, 0x75, 0xa5, 0x68,
+ 0x56, 0x64, 0x2a, 0x47, 0xf4, 0x24, 0x6c, 0x5a, 0x30, 0x2e, 0xa4, 0xa7,
+ 0x34, 0x04, 0xc5, 0x36, 0xf7, 0x4b, 0x55, 0xed, 0x0c, 0x69, 0x93, 0x22,
+ 0x5d, 0xf9, 0x5f, 0xc5, 0x37, 0x77, 0xa2, 0x5f, 0xed, 0x9c, 0xe0, 0xef,
+ 0xa5, 0x6a, 0xfc, 0xf7, 0xe2, 0xcb, 0x82, 0x0a, 0x6d, 0xfd, 0xa5, 0x8c,
+ 0xef, 0x99, 0xb7, 0xcd, 0x61, 0x65, 0x35, 0xbe, 0x57, 0x72, 0x33, 0x10,
+ 0x69, 0xfd, 0x0a, 0x71, 0x93, 0x59, 0x1e, 0x83, 0x45, 0xb6, 0xd7, 0x07,
+ 0xdb, 0x24, 0xbf, 0x3c, 0x2b, 0x90, 0xfe, 0xa2, 0x15, 0x8f, 0x8f, 0x30,
+ 0xb1, 0x2f, 0xce, 0x22, 0x14, 0x83, 0x7d, 0xff, 0x87, 0x05, 0x70, 0xae,
+ 0x6e, 0xd9, 0x3a, 0xcc, 0x25, 0xd3, 0xe1, 0x60, 0xe4, 0x5f, 0x3c, 0x80,
+ 0x2e, 0xbc, 0x26, 0xd7, 0x38, 0xfd, 0x43, 0x1b, 0x7e, 0xba, 0x18, 0xb2,
+ 0xaa, 0x0e, 0x52, 0x02, 0x82, 0xd3, 0x14, 0x53, 0x81, 0x81, 0xd8, 0x6f,
+ 0xb2, 0x90, 0x7b, 0x71, 0xd4, 0x74, 0x71, 0xd7, 0x21, 0xba, 0xd9, 0xe3,
+ 0x5b, 0xd5, 0xaf, 0x59, 0x52, 0x8a, 0xd4, 0xe6, 0xe9, 0xc1, 0x14, 0x04,
+ 0x3f, 0x74, 0xbc, 0xa1, 0xb7, 0x98, 0x20, 0x36, 0xe1, 0xd9, 0x3d, 0x62,
+ 0xde, 0x16, 0x3a, 0x18, 0xa8, 0x69, 0x90, 0x25, 0x8c, 0x5e, 0xb7, 0x5f,
+ 0x58, 0xfb, 0x57, 0xe1, 0x42, 0x9c, 0xb7, 0xa5, 0xc3, 0xa3, 0xa4, 0x2e,
+ 0x6e, 0x0d, 0x00, 0xad, 0x45, 0x22, 0xf3, 0x7a, 0xec, 0x82, 0x5c, 0x05,
+ 0xf8, 0x0f, 0xb9, 0x8a, 0x7d, 0xbc, 0x98, 0xc7, 0x79, 0xf8, 0xe5, 0x02,
+ 0x35, 0xa1, 0x15, 0x13, 0xf0, 0x3e, 0x42, 0xf7, 0x43, 0xe7, 0xde, 0x91,
+ 0x0a, 0x9e, 0x36, 0xb5, 0xf1, 0xc6, 0x88, 0x0c, 0x5c, 0xf6, 0xef, 0x00,
+ 0xb1, 0x50, 0x32, 0xe7, 0x77, 0x25, 0xa9, 0x3e, 0x60, 0x49, 0xb3, 0x4d,
+ 0x0b, 0x4e, 0xd4, 0x9d, 0x72, 0x92, 0x52, 0x23, 0x7a, 0x86, 0x6a, 0x2d,
+ 0xfb, 0xb9, 0xe7, 0x36, 0x8c, 0x80, 0x01, 0x32, 0x76, 0xf7, 0x63, 0xb0,
+ 0x9f, 0xef, 0x35, 0x64, 0x89, 0x81, 0xaa, 0x4c, 0x78, 0xb3, 0xc6, 0x5c,
+ 0xcc, 0x02, 0x5f, 0xfd, 0xf2, 0x96, 0x50, 0xaa, 0xaa, 0xe2, 0x0a, 0xe0,
+ 0x0a, 0xf7, 0x61, 0x09, 0xb9, 0x55, 0x94, 0x72, 0xd3, 0xdb, 0xf0, 0x76,
+ 0xa5, 0x53, 0x7d, 0x7a, 0x77, 0x4e, 0x9a, 0x5e, 0x78, 0x96, 0xf0, 0xbe,
+ 0x6b, 0x8f, 0x36, 0x02, 0xfe, 0xa8, 0x3b, 0x51, 0xc8, 0x39, 0x39, 0x44,
+ 0xbd, 0xf4, 0x0e, 0xce, 0x1f, 0x9a, 0xde, 0xeb, 0xcf, 0xa9, 0x46, 0x03,
+ 0xed, 0x25, 0x96, 0xeb, 0xbd, 0xe2, 0x1b, 0xcf, 0xbf, 0xda, 0x3c, 0xdc,
+ 0xb1, 0x1c, 0xcf, 0xb4, 0x5d, 0xff, 0x19, 0x4c, 0x8d, 0x55, 0x57, 0xe8,
+ 0xe2, 0xf1, 0x62, 0xe6, 0x63, 0x0e, 0x70, 0xc9, 0x38, 0x40, 0xb1, 0xde,
+ 0x11, 0x9a, 0x73, 0xf3, 0x43, 0x65, 0x8b, 0xfa, 0x4f, 0x49, 0x9b, 0xe6,
+ 0x46, 0xed, 0x29, 0x39, 0x0d, 0xbb, 0x87, 0xc4, 0x8a, 0x45, 0xd6, 0x4d,
+ 0xec, 0x35, 0x8d, 0x02, 0xff, 0xc9, 0x7f, 0x06, 0x73, 0x27, 0xfb, 0x1f,
+ 0xc4, 0xd6, 0xc7, 0xeb, 0xfc, 0xd6, 0x9f, 0x01, 0xe6, 0x5c, 0x07, 0xec,
+ 0x9f, 0x2f, 0xca, 0xda, 0xcd, 0x00, 0x7e, 0x42, 0x78, 0xa0, 0x25, 0xd5,
+ 0x70, 0xb4, 0xaa, 0x70, 0xd2, 0x3e, 0x0d, 0xe4, 0x5d, 0x42, 0x60, 0x8a,
+ 0x79, 0x8a, 0x72, 0x5c, 0x02, 0xfe, 0xca, 0xb0, 0xdd, 0xc1, 0x5c, 0xa4,
+ 0x59, 0x45, 0x6f, 0x88, 0xa9, 0xc4, 0x65, 0x3a, 0xa8, 0x55, 0xd9, 0x28,
+ 0x42, 0xb4, 0xf2, 0x3f, 0x95, 0x6f, 0xbf, 0x8c, 0x57, 0x47, 0xb4, 0xfd,
+ 0xbc, 0xb2, 0x39, 0xec, 0x9d, 0x4d, 0x53, 0xdc, 0x82, 0xca, 0xec, 0x1a,
+ 0x73, 0x4f, 0xc0, 0x6e, 0xbb, 0x92, 0x7f, 0x7b, 0x37, 0x2b, 0x83, 0x88,
+ 0x11, 0x01, 0x9a, 0x80, 0xa7, 0x17, 0x9c, 0x7b, 0xad, 0xc0, 0x5b, 0xe0,
+ 0xec, 0x68, 0xca, 0xbc, 0x12, 0x5e, 0xad, 0x09, 0x03, 0xbc, 0x10, 0xa6,
+ 0xa3, 0x2a, 0xae, 0x0c, 0x96, 0x48, 0xda, 0x63, 0xb8, 0x54, 0xbf, 0xcb,
+ 0xa2, 0xbb, 0xa5, 0x69, 0x8a, 0x58, 0xd0, 0x2e, 0xbc, 0xe5, 0x17, 0x29,
+ 0x6a, 0xe8, 0x05, 0x3f, 0xf3, 0xdb, 0xef, 0x1e, 0x50, 0xf7, 0xe6, 0xb9,
+ 0x09, 0x77, 0x1d, 0x78, 0xc0, 0xa1, 0x12, 0xcd, 0x32, 0x1a, 0x0e, 0xc6,
+ 0x2c, 0xc3, 0x20, 0x23, 0x63, 0xf2, 0x62, 0x47, 0x61, 0x37, 0xcc, 0xb2,
+ 0x68, 0x02, 0xb5, 0x4a, 0x10, 0x4e, 0xc9, 0x69, 0x33, 0xe3, 0x38, 0x7c,
+ 0x1a, 0xbf, 0x3e, 0x9c, 0x87, 0xc2, 0xb1, 0x93, 0x1b, 0xfa, 0xf3, 0xf8,
+ 0x9b, 0xc7, 0xf0, 0xcd, 0x6f, 0x3c, 0x9e, 0xe0, 0xc1, 0xc7, 0x88, 0xe4,
+ 0x04, 0x78, 0xfb, 0x5c, 0xd3, 0xed, 0x5c, 0xb1, 0x07, 0x80, 0x34, 0x24,
+ 0x0b, 0x55, 0xa3, 0x4d, 0xfc, 0xf9, 0xe6, 0x2e, 0x7b, 0x0b, 0xf5, 0xdb,
+ 0xeb, 0x75, 0xfb, 0xb7, 0x3b, 0x8f, 0xc2, 0x00, 0xfb, 0xf1, 0xf0, 0x46,
+ 0x28, 0x95, 0x1a, 0x5f, 0x4d, 0x66, 0x98, 0xd8, 0x09, 0xb0, 0xe6, 0xbc,
+ 0xae, 0x42, 0x8f, 0x13, 0xda, 0x31, 0xd4, 0xb4, 0x6e, 0xa6, 0xc1, 0x68,
+ 0xa9, 0x11, 0x5f, 0x9d, 0xe5, 0x73, 0xbe, 0x2b, 0x64, 0xe5, 0x8d, 0x43,
+ 0x7c, 0x4a, 0x71, 0xcb, 0x40, 0x78, 0x3d, 0x51, 0x13, 0xe4, 0x2b, 0x81,
+ 0x5d, 0x61, 0x4d, 0xca, 0x4b, 0x9b, 0xa4, 0x30, 0x5f, 0x85, 0xe9, 0x2e,
+ 0xf9, 0x38, 0xea, 0x62, 0x17, 0xee, 0x47, 0x74, 0xe3, 0xa5, 0x3c, 0x44,
+ 0xc8, 0x90, 0xf0, 0x18, 0x86, 0x7c, 0xe2, 0x9f, 0xb4, 0x84, 0x9c, 0xd7,
+ 0x56, 0x2b, 0x0e, 0x8a, 0x0c, 0xbe, 0x31, 0x89, 0xdb, 0xe6, 0x1d, 0x2f,
+ 0x49, 0x74, 0x6f, 0x76, 0xca, 0x31, 0x92, 0xa9, 0x24, 0xae, 0x27, 0x1b,
+ 0xd5, 0x3b, 0x6c, 0xb4, 0xb2, 0xb1, 0x9e, 0xfa, 0x03, 0x73, 0x65, 0x8b,
+ 0xba, 0xa2, 0x73, 0x2d, 0x2f, 0xd2, 0x65, 0x33, 0x93, 0x6b, 0xed, 0x3d,
+ 0x07, 0x97, 0xd5, 0x07, 0x17, 0x6b, 0x04, 0xaf, 0x81, 0xd4, 0x69, 0x19,
+ 0x75, 0xe3, 0x0a, 0xfc, 0x85, 0xef, 0x57, 0x2f, 0xbf, 0xfb, 0x9d, 0xec,
+ 0x77, 0x92, 0x3b, 0x7a, 0x2d, 0x56, 0x55, 0xf2, 0xa8, 0xe4, 0xc3, 0x03,
+ 0x0e, 0xb6, 0xd7, 0xf2, 0x21, 0x5d, 0xfc, 0x14, 0xaa, 0xe5, 0xa9, 0xa4,
+ 0xd9, 0xdf, 0x59, 0xc8, 0xe8, 0x8f, 0x07, 0xee, 0x5c, 0x22, 0x92, 0x8a,
+ 0xd1, 0xf6, 0x6b, 0xa6, 0x32, 0xcf, 0x86, 0x72, 0xfa, 0xaf, 0xd1, 0xcb,
+ 0xb9, 0x6c, 0x4d, 0x60, 0x73, 0x6b, 0x00, 0x09, 0x36, 0xcf, 0xe4, 0x3e,
+ 0xc2, 0xd5, 0x76, 0xe9, 0x23, 0x67, 0x36, 0x6b, 0x3e, 0xff, 0xd4, 0xef,
+ 0x15, 0xf1, 0x86, 0x5f, 0x50, 0x82, 0x00, 0x89, 0xa5, 0x5c, 0xe0, 0x38,
+ 0x7a, 0x32, 0x62, 0xf6, 0xeb, 0x9a, 0xe0, 0x67, 0xbf, 0x42, 0xcf, 0x7e,
+ 0xc6, 0x9a, 0xdc, 0x37, 0x3f, 0x3e, 0x82, 0xfe, 0x4d, 0xcc, 0x35, 0x07,
+ 0x95, 0x20, 0xad, 0x71, 0xda, 0x8c, 0xc0, 0xa8, 0x5f, 0x75, 0xd9, 0x7a,
+ 0x7b, 0x99, 0xef, 0x69, 0x13, 0xe2, 0x0a, 0x2e, 0xa4, 0x2d, 0x7b, 0x54,
+ 0x94, 0xfa, 0x81, 0xbb, 0xea, 0x2d, 0x9e, 0x8a, 0xd6, 0x02, 0x23, 0x69,
+ 0xf2, 0x92, 0xa4, 0x86, 0x06, 0xdd, 0x41, 0x60, 0x26, 0x0b, 0x4d, 0x08,
+ 0x31, 0x73, 0xfb, 0xdb, 0x52, 0x96, 0x04, 0xc9, 0xa4, 0x5a, 0x33, 0x87,
+ 0xfe, 0x0c, 0x22, 0xb7, 0xc6, 0x56, 0x22, 0xf0, 0x94, 0xbd, 0x72, 0xba,
+ 0x2c, 0xb3, 0x66, 0xb5, 0xf5, 0x86, 0xc6, 0x4f, 0x17, 0x78, 0xe3, 0x61,
+ 0xcb, 0xf7, 0xed, 0xb7, 0x8b, 0x7c, 0x8c, 0xd6, 0x19, 0x60, 0x69, 0x24,
+ 0x14, 0x8b, 0x0c, 0x00, 0xd1, 0x32, 0x03, 0x71, 0xf9, 0x71, 0x7d, 0x4b,
+ 0x79, 0x68, 0x19, 0x2d, 0x79, 0x1e, 0x67, 0x1b, 0x8f, 0x2e, 0xac, 0x0f,
+ 0x4a, 0x5e, 0x9b, 0xc9, 0xbf, 0xe1, 0x3f, 0x37, 0xbb, 0xbe, 0x4f, 0x1a,
+ 0x7a, 0xfc, 0x28, 0x3a, 0xd6, 0xa9, 0x36, 0xe5, 0xfb, 0x46, 0xc7, 0x6e,
+ 0x20, 0x69, 0x11, 0xc8, 0x5a, 0x47, 0xe5, 0x25, 0x2c, 0xe3, 0x1d, 0xef,
+ 0xf3, 0x6b, 0x5a, 0xe8, 0xd5, 0xd4, 0xe1, 0x56, 0xd1, 0x67, 0xf6, 0x9f,
+ 0x2e, 0xc3, 0xe8, 0xa2, 0xf5, 0xdb, 0x89, 0x1a, 0x68, 0x9a, 0x67, 0xd0,
+ 0x2c, 0x00, 0x30, 0x72, 0xda, 0x96, 0x8b, 0xf7, 0xba, 0xaf, 0x90, 0x71,
+ 0xaf, 0x3c, 0xe7, 0xfd, 0x97, 0xc5, 0x7e, 0x0e, 0xec, 0xe9, 0x2d, 0xec,
+ 0x1c, 0x8f, 0x9e, 0x44, 0xaa, 0x7e, 0x67, 0x2d, 0x7a, 0x90, 0xf0, 0xbd,
+ 0x14, 0x7a, 0xe9, 0x53, 0x15, 0x98, 0x6c, 0x9d, 0x37, 0xfe, 0xa1, 0xae,
+ 0xba, 0xa8, 0x6c, 0x0e, 0xea, 0xdb, 0xc8, 0x86, 0x00, 0xd6, 0x8a, 0x05,
+ 0x69, 0x09, 0xba, 0xa2, 0x5f, 0x9d, 0x0e, 0xbd, 0x75, 0x69, 0x7b, 0x46,
+ 0x1b, 0xaa, 0x24, 0xca, 0x6b, 0x8b, 0x65, 0x51, 0x85, 0xcf, 0xe5, 0x23,
+ 0x1d, 0x9b, 0x3e, 0x09, 0x53, 0xdd, 0xe7, 0x96, 0x34, 0x07, 0xe1, 0xa1,
+ 0x43, 0x04, 0x5f, 0xdd, 0x57, 0x29, 0xd9, 0x81, 0xe9, 0xf0, 0x5b, 0x3f,
+ 0xea, 0x3f, 0xae, 0x2c, 0x61, 0x32, 0x4d, 0x2c, 0x22, 0x90, 0xa0, 0xc5,
+ 0xf8, 0xdd, 0xe9, 0xeb, 0x64, 0xb5, 0xfd, 0x02, 0x28, 0x85, 0xb4, 0x52,
+ 0x4c, 0x0e, 0x76, 0x21, 0x55, 0x09, 0xee, 0x93, 0x6d, 0xaa, 0x7c, 0x74,
+ 0x17, 0x7c, 0xb0, 0xeb, 0xab, 0x0b, 0xee, 0x21, 0x6d, 0xe1, 0x2b, 0x40,
+ 0x8c, 0x19, 0x81, 0x9a, 0xb8, 0x15, 0x0e, 0x58, 0x78, 0x3d, 0x12, 0x8f,
+ 0x35, 0x1e, 0x18, 0x26, 0x2e, 0xfc, 0x78, 0x60, 0x79, 0x60, 0x92, 0x1f,
+ 0xa1, 0x19, 0xeb, 0x72, 0x61, 0x4a, 0x4a, 0x49, 0x44, 0xc5, 0x65, 0x45,
+ 0x1b, 0x8a, 0x60, 0xa6, 0x7b, 0xf3, 0x2c, 0xfe, 0x3f, 0xa0, 0x1b, 0xfc,
+ 0x28, 0xb5, 0x59, 0x1b, 0x84, 0xa4, 0xe9, 0xcc, 0x6a, 0x50, 0x7f, 0x34,
+ 0x94, 0x52, 0x6c, 0x23, 0xea, 0xd7, 0x1e, 0x69, 0x4a, 0x27, 0x6d, 0x12,
+ 0xb7, 0xa8, 0x99, 0xc6, 0x7b, 0x1f, 0x6a, 0x18, 0x1d, 0x09, 0x6d, 0x25,
+ 0x8b, 0xb8, 0x6b, 0xee, 0x92, 0xec, 0xdb, 0x08, 0x70, 0x98, 0x2c, 0x3d,
+ 0x93, 0x72, 0xbd, 0xc1, 0xd5, 0x4d, 0x41, 0x1b, 0x04, 0x69, 0xa6, 0xda,
+ 0xc5, 0x32, 0xfc, 0x0c, 0x58, 0x3d, 0x3e, 0x78, 0x53, 0xce, 0x98, 0xd6,
+ 0x13, 0xfc, 0x05, 0x61, 0x00, 0xdb, 0x83, 0x58, 0xd6, 0x74, 0x45, 0x06,
+ 0x49, 0xc2, 0x58, 0x32, 0xd3, 0x52, 0x46, 0xc4, 0x5b, 0xde, 0x08, 0x11,
+ 0xee, 0x90, 0x62, 0x5d, 0x91, 0x2e, 0x27, 0xe7, 0x54, 0xe3, 0x9d, 0x12,
+ 0x92, 0x82, 0xf1, 0x48, 0xf7, 0x1c, 0x91, 0xaa, 0x79, 0x01, 0x36, 0x3e,
+ 0xde, 0x2a, 0xb5, 0xf0, 0xb1, 0xed, 0x6e, 0xe8, 0x14, 0xf5, 0x72, 0x4f,
+ 0xae, 0x5b, 0x6c, 0xc0, 0xf9, 0xa2, 0xef, 0x45, 0xcd, 0x25, 0x35, 0x7c,
+ 0x83, 0xa0, 0x8a, 0xce, 0x6e, 0xfa, 0xfa, 0xc0, 0xae, 0xed, 0x39, 0x6c,
+ 0xa9, 0x29, 0xbc, 0xe0, 0x18, 0x48, 0xfe, 0xde, 0xb8, 0x8f, 0xa7, 0xff,
+ 0x13, 0xbd, 0x35, 0x32, 0x1b, 0x8b, 0x36, 0x64, 0x6a, 0x04, 0x70, 0x79,
+ 0x91, 0x84, 0x84, 0x17, 0xf1, 0x3f, 0xe9, 0xde, 0xb3, 0xf7, 0x8d, 0x38,
+ 0x7f, 0xeb, 0x1a, 0x5f, 0x43, 0x09, 0xcb, 0x7f, 0x52, 0x51, 0x2d, 0x9a,
+ 0xa0, 0xbc, 0xbe, 0x20, 0x0d, 0xa3, 0x4c, 0xb0, 0xe1, 0x5e, 0xd6, 0x49,
+ 0x6b, 0x55, 0x11, 0xba, 0xa4, 0xe6, 0xb9, 0x99, 0x36, 0xa2, 0x1f, 0x1a,
+ 0xd6, 0x14, 0x0c, 0x8d, 0x07, 0x98, 0x70, 0xba, 0xcf, 0x29, 0x42, 0x4f,
+ 0x69, 0x60, 0x73, 0x1c, 0x5c, 0x62, 0xf0, 0x0d, 0xa1, 0xa3, 0xe6, 0xe6,
+ 0x11, 0x79, 0x4d, 0x2b, 0x07, 0x21, 0x24, 0x48, 0x19, 0xa7, 0xf2, 0xc9,
+ 0xf3, 0x35, 0x52, 0xec, 0xa5, 0x05, 0x70, 0x90, 0x78, 0xd5, 0x21, 0xef,
+ 0xce, 0xa1, 0x6d, 0x0d, 0x98, 0xc2, 0x70, 0xbd, 0x46, 0xdd, 0xec, 0xbf,
+ 0xca, 0x7f, 0xdc, 0x9f, 0x7b, 0x49, 0x42, 0x32, 0xc1, 0x2a, 0x4f, 0xd9,
+ 0xc3, 0xcb, 0x7a, 0x0e, 0xcd, 0x17, 0xa2, 0x7e, 0xa3, 0xb6, 0x1f, 0x18,
+ 0x05, 0x17, 0x1e, 0x31, 0x3e, 0x1e, 0xd7, 0x17, 0x86, 0xc7, 0xd0, 0x95,
+ 0x8b, 0xc3, 0xb2, 0xf3, 0x9c, 0x2f, 0x2f, 0xeb, 0xde, 0x39, 0x14, 0x0b,
+ 0xfe, 0x56, 0x6c, 0x7a, 0xdd, 0x98, 0x4e, 0x86, 0x3f, 0xec, 0x51, 0xc7,
+ 0xff, 0xfe, 0x2d, 0x3b, 0xce, 0x54, 0xb6, 0x2b, 0x95, 0x81, 0x31, 0xf6,
+ 0x95, 0x26, 0x41, 0x39, 0xf4, 0x0b, 0xcf, 0x1e, 0xf6, 0xd0, 0x34, 0xe6,
+ 0x5c, 0x68, 0x01, 0x05, 0x9a, 0x1e, 0xba, 0xbf, 0xc1, 0x9e, 0x6d, 0xf0,
+ 0xd0, 0x6c, 0x84, 0xa5, 0x56, 0xec, 0x47, 0xaa, 0x82, 0x7f, 0x84, 0xf7,
+ 0xac, 0x52, 0x50, 0x6c, 0x32, 0x24, 0x8b, 0x2e, 0x54, 0x66, 0x63, 0xe8,
+ 0x97, 0x94, 0x73, 0xbd, 0x69, 0x92, 0x10, 0xa7, 0x4e, 0x88, 0x0d, 0xdd,
+ 0xd4, 0xa6, 0x5e, 0x77, 0x7e, 0xa1, 0xb0, 0xda, 0xb2, 0xff, 0xf4, 0xec,
+ 0x34, 0xc5, 0x8d, 0xb1, 0x39, 0x42, 0x04, 0x7f, 0xdd, 0x74, 0xb3, 0xb5,
+ 0xe5, 0x5a, 0xa9, 0x65, 0xc5, 0x4f, 0xe6, 0x20, 0x35, 0x85, 0x38, 0xb8,
+ 0xa7, 0xdd, 0x35, 0xf8, 0xcf, 0x3f, 0x2e, 0x80, 0x7f, 0x5f, 0x67, 0x8b,
+ 0xbf, 0xbe, 0x51, 0x02, 0x23, 0xff, 0xac, 0x0d, 0xfb, 0x7c, 0xe2, 0x86,
+ 0xe9, 0xc5, 0x20, 0x6d, 0x03, 0x55, 0xfa, 0xa7, 0x0a, 0x1f, 0x74, 0x17,
+ 0x31, 0x75, 0x1c, 0x62, 0xa2, 0x51, 0x38, 0x76, 0x4a, 0xab, 0x75, 0x42,
+ 0xa8, 0x00, 0xdc, 0x46, 0xf2, 0xc7, 0xdf, 0x1c, 0xdf, 0x60, 0xad, 0x5c,
+ 0x02, 0xee, 0x24, 0xbc, 0x07, 0xf1, 0xf5, 0xdd, 0x03, 0x2a, 0xcc, 0xf1,
+ 0x3c, 0xef, 0xc4, 0x0e, 0xff, 0x14, 0xe3, 0x42, 0x0a, 0x44, 0xe0, 0x87,
+ 0x4d, 0x75, 0x46, 0x65, 0xdd, 0x5b, 0x9e, 0xde, 0x68, 0xf6, 0x0c, 0xc0,
+ 0xf7, 0x48, 0xfe, 0x28, 0x39, 0x4a, 0xc4, 0x80, 0x64, 0xe5, 0x13, 0xbb,
+ 0x62, 0x6f, 0x32, 0x20, 0x08, 0xe2, 0x4a, 0xda, 0xe5, 0x27, 0x39, 0xc7,
+ 0xbc, 0x46, 0x4e, 0x54, 0x2b, 0x69, 0xc4, 0xcc, 0xc0, 0x22, 0xa9, 0xbd,
+ 0xca, 0xd7, 0xf5, 0x8f, 0xfd, 0xdb, 0xf1, 0x70, 0x48, 0x80, 0x11, 0xf6,
+ 0x39, 0x0a, 0xaf, 0x58, 0x6e, 0x45, 0x1a, 0xfb, 0xb0, 0xbf, 0xf6, 0x44,
+ 0xbc, 0x3a, 0x0c, 0x0a, 0x2c, 0x07, 0x62, 0x0e, 0xf0, 0x1c, 0xe5, 0x42,
+ 0xe0, 0x19, 0xb8, 0xbd, 0xcf, 0x76, 0x10, 0xf7, 0x94, 0xc1, 0x0f, 0x58,
+ 0x31, 0xdd, 0xcb, 0x50, 0x86, 0x30, 0xa0, 0x3d, 0xfd, 0x4d, 0xe7, 0xb2,
+ 0xb7, 0xbb, 0xbc, 0x5a, 0x56, 0x7a, 0x57, 0x34, 0xcb, 0xeb, 0x6f, 0x11,
+ 0x67, 0x63, 0xd2, 0xf4, 0x00, 0xf0, 0x4c, 0xe3, 0xf1, 0x48, 0xd7, 0xac,
+ 0xfe, 0xdb, 0x2c, 0x25, 0x9a, 0x48, 0x07, 0xcb, 0x07, 0x94, 0x77, 0x07,
+ 0xe7, 0xbf, 0xd5, 0xcf, 0x47, 0x88, 0xad, 0x73, 0xf3, 0x4e, 0x2a, 0x31,
+ 0x10, 0x2f, 0x0b, 0xc7, 0x2d, 0x79, 0x72, 0xe4, 0xde, 0x48, 0xf5, 0xc7,
+ 0xd9, 0x38, 0x73, 0x0e, 0xb4, 0x4a, 0xb5, 0x65, 0x85, 0x69, 0xf6, 0x22,
+ 0x8e, 0xea, 0x72, 0xc1, 0x0b, 0x67, 0xbd, 0x86, 0x05, 0x0c, 0xf7, 0x21,
+ 0x47, 0x0c, 0x23, 0x96, 0x6d, 0xce, 0xf2, 0xf9, 0x4e, 0x2f, 0x3f, 0xb8,
+ 0x88, 0xc5, 0xed, 0x0b, 0x8e, 0xd9, 0x3b, 0xa3, 0xfc, 0x88, 0xa6, 0x23,
+ 0xd5, 0x84, 0xfc, 0x09, 0xd5, 0x37, 0x78, 0xb2, 0xa5, 0x31, 0x7f, 0xf7,
+ 0x18, 0x26, 0x59, 0xfc, 0x14, 0x5a, 0x31, 0xc4, 0x85, 0xc8, 0xa7, 0x78,
+ 0x88, 0x70, 0x6b, 0x97, 0xf0, 0xee, 0x17, 0x6d, 0x53, 0x2f, 0x4c, 0x91,
+ 0x69, 0x8c, 0x8d, 0x78, 0x3e, 0x15, 0x12, 0x8b, 0xd7, 0x2a, 0xb9, 0x22,
+ 0x72, 0xcd, 0x86, 0x34, 0x90, 0x5f, 0x8b, 0x59, 0x0d, 0xca, 0x19, 0xb8,
+ 0x93, 0x03, 0x0f, 0x9a, 0x3d, 0x61, 0x7c, 0xea, 0x7c, 0x15, 0x2a, 0x88,
+ 0x29, 0x8c, 0xc7, 0xd3, 0x1d, 0x22, 0x74, 0x1a, 0x41, 0xd0, 0xe9, 0x7e,
+ 0x54, 0x61, 0x7c, 0x91, 0x6e, 0x49, 0x6a, 0x14, 0xc9, 0x75, 0xd3, 0x93,
+ 0x9e, 0x02, 0x19, 0xb7, 0xf8, 0x1d, 0x52, 0xe4, 0xa2, 0xfc, 0x08, 0xde,
+ 0xc6, 0x2f, 0xab, 0x10, 0xb7, 0x84, 0x17, 0xea, 0x93, 0x54, 0x89, 0xb2,
+ 0x2f, 0x23, 0x11, 0x77, 0xcc, 0xc9, 0x55, 0x79, 0xeb, 0x78, 0xc3, 0x92,
+ 0xc8, 0xe0, 0xd3, 0x65, 0xad, 0xbc, 0x9b, 0x99, 0x7f, 0x3c, 0x84, 0x01,
+ 0xb1, 0xc6, 0x78, 0x75, 0xc7, 0x0c, 0xe6, 0x13, 0xe8, 0x07, 0x3e, 0x14,
+ 0x83, 0x1c, 0x9f, 0xf9, 0x6d, 0xf1, 0x20, 0x1b, 0x5c, 0x5d, 0xc1, 0x95,
+ 0x85, 0x92, 0x62, 0xce, 0x75, 0xb0, 0x14, 0xc8, 0x15, 0x41, 0x62, 0xf0,
+ 0x99, 0x8c, 0xba, 0x4f, 0xcd, 0xb4, 0x3e, 0x29, 0x60, 0x51, 0x04, 0x98,
+ 0x65, 0x82, 0xc6, 0x4d, 0xde, 0xab, 0x30, 0x6a, 0xc4, 0xd2, 0xee, 0x86,
+ 0xf7, 0x60, 0xaa, 0x86, 0x36, 0x1d, 0x36, 0xc9, 0xc6, 0x89, 0x72, 0x3a,
+ 0x73, 0x5f, 0xba, 0x79, 0xbb, 0xc8, 0xaa, 0x98, 0x99, 0xdd, 0xdf, 0x8b,
+ 0xab, 0x30, 0xd9, 0x64, 0x3e, 0x07, 0xb7, 0x9d, 0x1f, 0x84, 0xc1, 0xae,
+ 0xfa, 0x57, 0x2b, 0x95, 0x19, 0xdf, 0xe9, 0xe4, 0x04, 0x8d, 0x8e, 0x0b,
+ 0x38, 0xc8, 0xf8, 0x67, 0x1b, 0xb8, 0xea, 0xf3, 0xfa, 0x64, 0xe2, 0x98,
+ 0x10, 0x67, 0xb1, 0x0a, 0xb3, 0x8e, 0x30, 0x1c, 0x2c, 0xe8, 0x92, 0x15,
+ 0xa5, 0xef, 0xa0, 0x8d, 0x62, 0xff, 0xb6, 0x9a, 0x15, 0x0a, 0xd7, 0x8f,
+ 0x9d, 0x1c, 0x63, 0xe1, 0x75, 0x07, 0xe2, 0xb3, 0x01, 0xc0, 0x87, 0x50,
+ 0x45, 0x60, 0x7a, 0x92, 0x78, 0x41, 0xc7, 0xee, 0xb0, 0xe2, 0x50, 0x72,
+ 0xd5, 0x38, 0x6e, 0xdb, 0xd7, 0xc3, 0x6b, 0x2e, 0xa0, 0xc2, 0x6e, 0xcb,
+ 0x41, 0x34, 0xfb, 0x86, 0xf9, 0x00, 0x74, 0x1d, 0x30, 0x35, 0xf9, 0x0a,
+ 0xc8, 0x23, 0x2c, 0x5d, 0xf5, 0x0d, 0x98, 0x63, 0x20, 0xab, 0xb4, 0x71,
+ 0xe8, 0x80, 0xab, 0x59, 0xbe, 0xa5, 0x8a, 0x6a, 0x50, 0x36, 0x5b, 0xb8,
+ 0xfe, 0x6c, 0xdf, 0x15, 0x8c, 0x30, 0x5c, 0xa0, 0x22, 0x48, 0x04, 0xff,
+ 0xe3, 0x16, 0x00, 0xf3, 0xa4, 0x8b, 0x79, 0xcc, 0x1c, 0x60, 0x59, 0xc3,
+ 0x05, 0x2a, 0x74, 0xe2, 0xff, 0xee, 0xef, 0x1f, 0x01, 0xeb, 0x6c, 0xff,
+ 0x8c, 0x6e, 0x6b, 0x60, 0x9e, 0x1b, 0x75, 0x7b, 0x39, 0xcb, 0x03, 0xa2,
+ 0x3c, 0x71, 0x44, 0x9c, 0x5b, 0x81, 0xb3, 0x83, 0xac, 0x26, 0x59, 0xb4,
+ 0xaa, 0xb7, 0xfe, 0xa5, 0x24, 0xe3, 0xf7, 0x87, 0x19, 0xbd, 0x90, 0xe1,
+ 0xcc, 0x7c, 0xff, 0xa4, 0x32, 0xdc, 0xd1, 0x0d, 0x02, 0x5a, 0x6f, 0xf9,
+ 0x4f, 0x91, 0x7a, 0xd6, 0x87, 0x2e, 0xbd, 0xcd, 0x8e, 0x99, 0x99, 0xf6,
+ 0xb4, 0xb8, 0x90, 0x2d, 0x56, 0x24, 0x08, 0x4c, 0x14, 0xc4, 0x12, 0x0f,
+ 0xa3, 0x70, 0x84, 0x4e, 0x82, 0xb9, 0x31, 0x63, 0x45, 0x5b, 0x9d, 0x9a,
+ 0x45, 0xf7, 0x6c, 0x26, 0xad, 0x51, 0x5b, 0x68, 0xf2, 0x50, 0x24, 0x8f,
+ 0x7f, 0x8e, 0x78, 0xaa, 0x8e, 0xde, 0xe4, 0x50, 0xa1, 0x40, 0xe0, 0x2e,
+ 0xee, 0x8f, 0x47, 0x02, 0x82, 0x08, 0xb4, 0x3d, 0x58, 0x28, 0x97, 0x96,
+ 0x24, 0xcd, 0xa5, 0xb8, 0xe3, 0x92, 0xbd, 0xde, 0xd7, 0x38, 0x5b, 0x32,
+ 0x1d, 0x60, 0x96, 0xe0, 0xbe, 0xd8, 0x3e, 0x6b, 0xe6, 0x10, 0xf0, 0xb9,
+ 0x80, 0x4f, 0x76, 0x35, 0x17, 0xad, 0xb3, 0x7a, 0xda, 0x00, 0x63, 0xbd,
+ 0xb6, 0x5f, 0x38, 0x59, 0xd2, 0xa8, 0xab, 0x27, 0x8a, 0xbd, 0x9f, 0xb8,
+ 0x1c, 0x52, 0x0c, 0xd0, 0xe0, 0x9d, 0xd2, 0xbd, 0x00, 0x71, 0xdb, 0xa4,
+ 0xc1, 0x6d, 0xe0, 0x28, 0xe8, 0x8a, 0xc7, 0x09, 0x51, 0xf8, 0x3f, 0xd2,
+ 0xd6, 0x93, 0xdf, 0xef, 0x7f, 0x07, 0xdd, 0x00, 0x34, 0xcc, 0x1e, 0x19,
+ 0xf9, 0x89, 0x05, 0xee, 0x28, 0x87, 0x5a, 0x29, 0x33, 0x80, 0x9e, 0x1e,
+ 0xb9, 0x6b, 0xdb, 0x3a, 0xd3, 0xdb, 0x23, 0xd7, 0x6f, 0xc3, 0x48, 0xe6,
+ 0x35, 0x42, 0x23, 0x7a, 0x95, 0xbd, 0x8e, 0xb0, 0x79, 0xaa, 0x74, 0x5e,
+ 0x59, 0x97, 0xed, 0xf9, 0xf2, 0x0c, 0xa5, 0xc1, 0x33, 0xa5, 0xe9, 0xb9,
+ 0x9b, 0xec, 0x39, 0xa8, 0x41, 0xfe, 0x6f, 0x2b, 0xda, 0xd2, 0x8b, 0xaf,
+ 0xa8, 0x1f, 0x1d, 0x22, 0x94, 0x1c, 0x7c, 0x4b, 0xd7, 0xa9, 0x27, 0x60,
+ 0xda, 0x00, 0x4d, 0x46, 0x4d, 0xb0, 0xe3, 0x0e, 0xf1, 0x27, 0xf4, 0x07,
+ 0x9d, 0x42, 0x78, 0x78, 0x70, 0x15, 0xa5, 0x12, 0x63, 0x9c, 0x49, 0x26,
+ 0x6d, 0xc4, 0x93, 0x01, 0x51, 0xf7, 0x34, 0x8b, 0x2d, 0x3f, 0x84, 0x07,
+ 0x62, 0xaf, 0x16, 0x81, 0x7a, 0x65, 0xf1, 0xce, 0xb3, 0xde, 0x46, 0x0e,
+ 0x8b, 0xa2, 0x94, 0xa5, 0xb7, 0x04, 0x0e, 0x93, 0x63, 0xf2, 0x1b, 0xa5,
+ 0xdd, 0xbc, 0x52, 0xdc, 0x6c, 0xaa, 0xb7, 0x12, 0x39, 0x09, 0xf3, 0x3d,
+ 0x5f, 0x8f, 0x2d, 0xd4, 0x00, 0x08, 0x11, 0xe6, 0xbf, 0xbf, 0xfb, 0xd7,
+ 0xb2, 0xed, 0xb9, 0x0a, 0x41, 0x46, 0x03, 0xa0, 0x34, 0x09, 0x07, 0xf7,
+ 0xeb, 0x26, 0x3a, 0x14, 0x8c, 0xfb, 0xb8, 0xd9, 0x87, 0x6b, 0x5e, 0xa2,
+ 0x04, 0x59, 0x80, 0xff, 0x57, 0x56, 0xba, 0x94, 0xa5, 0xdc, 0xd6, 0xff,
+ 0x54, 0x4c, 0x2b, 0xf8, 0xa0, 0x60, 0x02, 0xeb, 0x7d, 0xac, 0x2b, 0x3d,
+ 0xd7, 0xc1, 0xfb, 0x53, 0xed, 0x06, 0xef, 0x60, 0x3f, 0x1e, 0x3a, 0xaf,
+ 0x58, 0xcf, 0xe4, 0x4e, 0x01, 0x31, 0x7d, 0xdd, 0x96, 0x94, 0x61, 0x8d,
+ 0x9c, 0x99, 0x17, 0x0f, 0xb4, 0x0e, 0xa3, 0x98, 0x85, 0x41, 0x63, 0x27,
+ 0x72, 0x6e, 0x91, 0xd3, 0xd1, 0x5f, 0xc3, 0xb7, 0x2a, 0xb4, 0x32, 0x47,
+ 0x85, 0xaa, 0x84, 0x83, 0x6e, 0x05, 0x23, 0x85, 0x38, 0x04, 0xf5, 0x70,
+ 0xbc, 0x0a, 0x56, 0x97, 0x41, 0xd7, 0xe8, 0x2c, 0x38, 0x72, 0x48, 0x9f,
+ 0xd7, 0xda, 0x59, 0x2d, 0x33, 0xfc, 0x06, 0x50, 0xa9, 0xc5, 0x3a, 0x4a,
+ 0xb2, 0xe9, 0x15, 0x97, 0xfe, 0x4f, 0xdd, 0xfb, 0xab, 0xa4, 0x7c, 0x78,
+ 0xb2, 0x40, 0x9f, 0x92, 0x58, 0xfb, 0xc6, 0xc8, 0x20, 0x98, 0x1e, 0x10,
+ 0x41, 0xcb, 0x73, 0x3c, 0x6c, 0x5d, 0xc1, 0x63, 0x6b, 0xa4, 0x0c, 0x7e,
+ 0xb3, 0x11, 0x03, 0x23, 0xc6, 0x36, 0x33, 0xe9, 0xe5, 0x5d, 0xa6, 0x0c,
+ 0x0b, 0xd7, 0x93, 0xdb, 0xd9, 0x06, 0xdf, 0x3c, 0x30, 0x75, 0x4f, 0xfd,
+ 0x20, 0xd5, 0x80, 0x7f, 0xbc, 0xca, 0x29, 0x29, 0x9b, 0xa3, 0x5f, 0xf6,
+ 0x46, 0x76, 0xe9, 0x07, 0x9d, 0x20, 0xfb, 0xb7, 0x46, 0xbe, 0x9d, 0xe3,
+ 0x2d, 0x3c, 0x58, 0x44, 0xf4, 0x56, 0x63, 0x54, 0x51, 0xda, 0xf5, 0x4a,
+ 0xc5, 0x03, 0xc4, 0xf1, 0x6b, 0xa9, 0xbc, 0xe0, 0x32, 0x30, 0xc4, 0xee,
+ 0x03, 0xbc, 0xf5, 0x07, 0x15, 0xa7, 0x16, 0xbe, 0x81, 0xd2, 0xf3, 0x6a,
+ 0x94, 0xff, 0x1c, 0x82, 0x96, 0x10, 0xfe, 0xe6, 0xcf, 0xc6, 0xac, 0xb2,
+ 0xe0, 0xf8, 0x00, 0xff, 0xce, 0x46, 0xcc, 0xf5, 0x5f, 0xce, 0xe1, 0xaf,
+ 0xd6, 0x26, 0x7a, 0xfb, 0x1f, 0xde, 0x7f, 0x54, 0xa9, 0x91, 0xf8, 0x5d,
+ 0x76, 0x10, 0x6a, 0x92, 0x11, 0xc5, 0x1a, 0x2c, 0x9d, 0x48, 0x0a, 0xfb,
+ 0xf6, 0x17, 0xfd, 0x85, 0xa1, 0x81, 0x79, 0x89, 0x4f, 0xfb, 0x56, 0x94,
+ 0x9e, 0x5a, 0x87, 0x9b, 0x6b, 0x72, 0x4a, 0xd1, 0x13, 0x20, 0xb4, 0xd7,
+ 0x6a, 0xc6, 0x58, 0xb0, 0x5f, 0x36, 0x2d, 0x76, 0xbc, 0xf3, 0x91, 0x60,
+ 0xaa, 0xf4, 0x8b, 0x4c, 0x9f, 0xb0, 0x80, 0x95, 0x36, 0x66, 0xb0, 0x83,
+ 0x9f, 0x90, 0xcb, 0x4e, 0xd8, 0xa1, 0x08, 0x88, 0x67, 0x10, 0x71, 0x04,
+ 0x85, 0x1f, 0x7f, 0x7d, 0xd6, 0x9f, 0x7b, 0xc3, 0xbf, 0x5e, 0x85, 0x0b,
+ 0x74, 0x24, 0x8d, 0x43, 0x0f, 0xae, 0x38, 0x9d, 0xaa, 0xa3, 0x6f, 0x7b,
+ 0x46, 0xce, 0x43, 0xfc, 0x71, 0x9a, 0x57, 0xb9, 0xf8, 0xaa, 0x6a, 0x5e,
+ 0x22, 0xf4, 0x57, 0x3d, 0xda, 0x6a, 0xe7, 0x88, 0x27, 0xe9, 0xe9, 0xca,
+ 0xce, 0xad, 0x98, 0x13, 0x93, 0xe5, 0x31, 0x71, 0x4f, 0x1a, 0xe3, 0x80,
+ 0x23, 0xaa, 0x12, 0xbb, 0x40, 0x04, 0xcb, 0xf6, 0x0c, 0x70, 0x22, 0x31,
+ 0xab, 0x4b, 0xf1, 0x2f, 0xd7, 0x77, 0x17, 0xf5, 0xdf, 0x6d, 0xed, 0x39,
+ 0x51, 0x77, 0xe1, 0x9c, 0x32, 0xef, 0xd8, 0x2d, 0x30, 0x60, 0x52, 0x3d,
+ 0x4b, 0x9a, 0x0b, 0x53, 0x6b, 0x7e, 0xee, 0x5c, 0x28, 0xd9, 0x8e, 0xe7,
+ 0xe2, 0x85, 0xe2, 0x81, 0x42, 0x0c, 0x23, 0x2d, 0x3f, 0x79, 0x52, 0xe9,
+ 0x53, 0x8b, 0xc2, 0x50, 0xca, 0x30, 0xe3, 0x4b, 0xca, 0xc5, 0xda, 0x33,
+ 0x36, 0x3e, 0xc6, 0xef, 0xd8, 0xd6, 0xa6, 0xf7, 0xeb, 0x91, 0xae, 0x61,
+ 0xb5, 0x44, 0xca, 0xd7, 0x20, 0x3e, 0x43, 0x03, 0xad, 0x1c, 0x52, 0xcf,
+ 0x0f, 0x0d, 0xad, 0x1b, 0x73, 0x24, 0x1f, 0x77, 0x58, 0xdc, 0x42, 0xd4,
+ 0x3c, 0x09, 0xfc, 0xdd, 0xcf, 0xf6, 0x2c, 0xdc, 0x39, 0xb8, 0x49, 0x64,
+ 0x0c, 0xc9, 0x05, 0xa0, 0x74, 0x1b, 0x60, 0x13, 0x4f, 0x67, 0xa3, 0x2a,
+ 0xf8, 0x3b, 0x7b, 0xaa, 0xdc, 0xda, 0x69, 0x9b, 0xc4, 0xc5, 0xec, 0x24,
+ 0x3e, 0xf9, 0x64, 0x31, 0x86, 0x39, 0x8f, 0x2b, 0x79, 0x16, 0x03, 0xff,
+ 0x32, 0x6b, 0xc4, 0xaa, 0x1c, 0xc5, 0x77, 0x79, 0xc4, 0x40, 0x97, 0x62,
+ 0xea, 0x41, 0xd0, 0xa1, 0x7b, 0x3d, 0xae, 0x7e, 0xde, 0x4d, 0xb3, 0xc9,
+ 0x21, 0xb6, 0xaf, 0x8b, 0x40, 0xea, 0xd5, 0x3d, 0x40, 0x4e, 0x5f, 0xd4,
+ 0xa3, 0xcc, 0x1a, 0x89, 0x15, 0xd5, 0x6e, 0x3e, 0xfc, 0x4e, 0xba, 0x93,
+ 0xcb, 0x95, 0x12, 0xb7, 0x59, 0x96, 0x77, 0x07, 0x40, 0x05, 0x87, 0xde,
+ 0xad, 0x6d, 0x81, 0xfe, 0x35, 0x45, 0xfa, 0x46, 0x1a, 0xa5, 0x16, 0xc3,
+ 0xb6, 0x44, 0x19, 0x21, 0xfb, 0x95, 0x23, 0xac, 0x6f, 0xa1, 0x40, 0xff,
+ 0x76, 0xb9, 0x19, 0xd4, 0x17, 0x44, 0x64, 0x00, 0x6f, 0x7e, 0xb6, 0x1a,
+ 0x55, 0x10, 0x24, 0x54, 0xe8, 0x4c, 0x8d, 0x19, 0x4f, 0x9d, 0xf4, 0xf7,
+ 0x4d, 0xc2, 0x58, 0x55, 0x92, 0x63, 0x01, 0xfe, 0xb6, 0xa7, 0x8c, 0xd6,
+ 0xbd, 0x15, 0x30, 0x91, 0x9c, 0x2e, 0xf8, 0x4c, 0x89, 0xfd, 0x43, 0xa7,
+ 0x69, 0x93, 0x8f, 0x0d, 0x5c, 0xb2, 0xaf, 0x36, 0x6d, 0x60, 0x29, 0x41,
+ 0xc2, 0x3b, 0x25, 0xb8, 0xfb, 0xf6, 0xc3, 0x26, 0x15, 0x2c, 0xf3, 0xf2,
+ 0x6e, 0xf9, 0xd0, 0x94, 0x1c, 0x45, 0x5c, 0xcf, 0xf6, 0x03, 0x59, 0xa7,
+ 0xad, 0x11, 0x15, 0x36, 0xa8, 0x7e, 0x77, 0xf7, 0x58, 0xe4, 0x68, 0x58,
+ 0x24, 0x0b, 0x16, 0xce, 0xe7, 0x7f, 0xd6, 0x06, 0x04, 0x1a, 0x92, 0x60,
+ 0xbc, 0xec, 0x7f, 0x83, 0x1b, 0xe0, 0xcc, 0x12, 0xd1, 0x82, 0x74, 0xce,
+ 0xb5, 0x64, 0xa1, 0x7e, 0x81, 0x58, 0xcd, 0xb2, 0xd1, 0x8f, 0x1c, 0xea,
+ 0xb6, 0x6e, 0x02, 0xa5, 0x88, 0x07, 0xaa, 0xfb, 0xe6, 0xbb, 0xea, 0xbd,
+ 0x0c, 0x15, 0x65, 0x9a, 0x23, 0x6d, 0x5b, 0x03, 0x1a, 0x3c, 0xc7, 0xf1,
+ 0xf3, 0x4a, 0x29, 0x38, 0xf8, 0x56, 0xcd, 0x4f, 0x42, 0x41, 0x0b, 0x2a,
+ 0x75, 0x2d, 0x52, 0x30, 0x0c, 0xcb, 0x3d, 0x54, 0xdd, 0xe2, 0x3f, 0xdd,
+ 0xf6, 0xcd, 0xb5, 0xf5, 0x24, 0xb1, 0xc0, 0x6b, 0x57, 0xe0, 0x6c, 0xbf,
+ 0x4a, 0x38, 0xc5, 0x12, 0x27, 0x4c, 0x69, 0x87, 0x31, 0x6a, 0x1f, 0xba,
+ 0xbe, 0xfc, 0x23, 0x34, 0xb5, 0x42, 0x1c, 0x99, 0x7e, 0xb3, 0x43, 0x2b,
+ 0x22, 0x4d, 0x03, 0xa0, 0x6d, 0x43, 0xa1, 0xcc, 0x27, 0x3a, 0x97, 0x18,
+ 0xe0, 0x75, 0x2a, 0x30, 0x5d, 0x9f, 0xc1, 0xfb, 0x0e, 0xdb, 0x20, 0xdf,
+ 0x98, 0xe9, 0xc0, 0xcf, 0x34, 0x38, 0x43, 0x62, 0x76, 0x0b, 0x69, 0xac,
+ 0x22, 0x3f, 0x3e, 0x8f, 0xb8, 0x9f, 0xa8, 0x52, 0x76, 0x4a, 0xe0, 0x76,
+ 0x00, 0xdd, 0x92, 0x02, 0x72, 0x05, 0x02, 0x18, 0xd9, 0x7c, 0xd5, 0x5d,
+ 0x47, 0x1b, 0xed, 0x80, 0xb8, 0xa4, 0x2a, 0xf8, 0x06, 0x66, 0x7d, 0x5f,
+ 0x23, 0xb9, 0x55, 0xa2, 0x68, 0x3a, 0x11, 0x51, 0xc7, 0xd3, 0xe4, 0x89,
+ 0x32, 0xd5, 0xf5, 0x53, 0xcd, 0xde, 0xbf, 0x26, 0x5b, 0xa0, 0xa6, 0x0d,
+ 0x42, 0x50, 0x80, 0xa0, 0x2f, 0x27, 0x13, 0x82, 0x0b, 0xfe, 0xc9, 0x49,
+ 0xbe, 0xde, 0x87, 0x8f, 0x47, 0x30, 0x22, 0x27, 0x6e, 0xef, 0xf5, 0x82,
+ 0x44, 0xa8, 0x4a, 0xf7, 0xf2, 0xe4, 0x04, 0x54, 0x4c, 0xf7, 0x83, 0x39,
+ 0x7d, 0xcb, 0xec, 0x8b, 0x89, 0x70, 0x41, 0xfc, 0x2b, 0xa3, 0x1e, 0x51,
+ 0xac, 0xa9, 0x11, 0x6a, 0x69, 0xcc, 0x06, 0x99, 0x9f, 0xbc, 0x90, 0x0f,
+ 0x61, 0x22, 0xa3, 0x61, 0x46, 0x42, 0xce, 0x11, 0x09, 0x31, 0x3a, 0xf8,
+ 0x9d, 0x05, 0xc7, 0x33, 0x8a, 0x5c, 0xe7, 0x19, 0x09, 0xeb, 0x68, 0x5f,
+ 0xc7, 0x95, 0x12, 0xe4, 0xd4, 0x38, 0x75, 0x47, 0xa1, 0x66, 0xf1, 0x31,
+ 0x23, 0xf1, 0x98, 0x16, 0xdd, 0x64, 0xe8, 0xf8, 0x28, 0x57, 0x3e, 0x79,
+ 0x5b, 0x77, 0x5c, 0x76, 0xce, 0x70, 0xf5, 0x56, 0xc9, 0x4c, 0xf7, 0xcb,
+ 0x00, 0xd4, 0xc8, 0xe3, 0xbc, 0xe6, 0x20, 0xdf, 0xa1, 0x39, 0x9f, 0x51,
+ 0x40, 0x96, 0x8f, 0xc1, 0x81, 0x6f, 0xa4, 0x18, 0xb8, 0xb6, 0x0c, 0x16,
+ 0x5d, 0x1c, 0xfb, 0x3a, 0x82, 0x94, 0x7d, 0x84, 0x55, 0xd0, 0x20, 0x62,
+ 0xd3, 0xb5, 0x66, 0x6c, 0xa5, 0x22, 0x14, 0x18, 0xed, 0xd9, 0x62, 0x39,
+ 0xc1, 0x35, 0xc9, 0x03, 0x52, 0x9b, 0x9c, 0x30, 0x2c, 0x9b, 0x5d, 0xd0,
+ 0xb6, 0xc5, 0x57, 0xda, 0x6c, 0x69, 0xb9, 0xc7, 0x33, 0x8a, 0x17, 0xd0,
+ 0xcd, 0x22, 0x62, 0xc3, 0x04, 0xc1, 0x79, 0x4c, 0x7f, 0xad, 0x3d, 0xf5,
+ 0xba, 0xd6, 0x7d, 0x35, 0x9b, 0x49, 0x97, 0xee, 0xaf, 0x2b, 0x5c, 0x01,
+ 0xf6, 0x88, 0xf1, 0x64, 0xe0, 0x60, 0x1a, 0x09, 0x20, 0x0c, 0x8b, 0x28,
+ 0x91, 0xc5, 0x5c, 0x0f, 0x2b, 0x11, 0xaf, 0x5f, 0x63, 0xf9, 0xba, 0x44,
+ 0x9a, 0x75, 0xd4, 0xb2, 0x7e, 0x6a, 0x42, 0xf5, 0x52, 0xfb, 0x65, 0x46,
+ 0x33, 0xe2, 0xde, 0x50, 0xb3, 0xc6, 0x82, 0x3a, 0x23, 0x3d, 0x06, 0x64,
+ 0xd9, 0xe5, 0x2b, 0xae, 0x30, 0xa9, 0x9d, 0x09, 0x38, 0x2e, 0xfa, 0xbe,
+ 0xda, 0xd0, 0x6e, 0xb0, 0xc9, 0xf5, 0xb0, 0xf2, 0xbe, 0x59, 0x7b, 0x32,
+ 0x7e, 0xaa, 0x6c, 0xf5, 0x68, 0xe4, 0x2c, 0x83, 0x2d, 0x9f, 0xc6, 0x3d,
+ 0x18, 0x8a, 0xaf, 0x6c, 0x9b, 0x0d, 0x3b, 0xd2, 0x64, 0x1a, 0xf4, 0x43,
+ 0x05, 0xe2, 0x02, 0x20, 0x06, 0xb8, 0x9b, 0x0e, 0x4a, 0x23, 0x95, 0x1c,
+ 0x3e, 0x14, 0x36, 0x3d, 0x15, 0x46, 0x98, 0x2a, 0xa5, 0xdf, 0xd9, 0x07,
+ 0x6a, 0x70, 0xd2, 0x33, 0x8c, 0xb2, 0x9a, 0x62, 0x30, 0x08, 0x06, 0xc7,
+ 0x96, 0x04, 0xc1, 0xfc, 0x1b, 0x11, 0xe9, 0x6c, 0x96, 0x36, 0xfe, 0x3a,
+ 0x66, 0x02, 0x97, 0xdd, 0x32, 0xab, 0x76, 0x78, 0xda, 0x28, 0x0e, 0x31,
+ 0x6c, 0xe9, 0x1e, 0x40, 0xbe, 0x6d, 0x18, 0x5c, 0xdf, 0x13, 0x4d, 0x83,
+ 0x90, 0xec, 0x50, 0xf7, 0xea, 0xad, 0x32, 0x3f, 0x42, 0x79, 0x08, 0x7d,
+ 0x70, 0x80, 0x8b, 0x47, 0x21, 0xec, 0x90, 0x7d, 0x6c, 0x74, 0xf7, 0x32,
+ 0x03, 0x3a, 0xf3, 0x93, 0xb4, 0x02, 0xf2, 0xe1, 0xcb, 0x05, 0xf5, 0xc5,
+ 0xd4, 0x92, 0x22, 0x6e, 0x80, 0xc5, 0x01, 0x10, 0x5c, 0x42, 0x58, 0xb0,
+ 0x00, 0x74, 0x40, 0x1f, 0x00, 0x01, 0x7d, 0x68, 0x68, 0x55, 0xe9, 0xb1,
+ 0x3b, 0x2b, 0xaa, 0xba, 0xeb, 0xc8, 0xc6, 0xaa, 0xdb, 0x85, 0x12, 0x9b,
+ 0x3a, 0x89, 0x97, 0x70, 0x30, 0x00, 0xcf, 0x13, 0x0c, 0x81, 0x93, 0x14,
+ 0x9e, 0x0a, 0xf6, 0x64, 0xb3, 0x1e, 0x83, 0xb7, 0x50, 0x2d, 0xd5, 0x39,
+ 0x74, 0xe5, 0xb1, 0x9e, 0x68, 0xae, 0xc4, 0xd5, 0xa8, 0x72, 0x11, 0xe7,
+ 0xfc, 0xc5, 0xa1, 0x73, 0x08, 0xc4, 0xb2, 0xd8, 0x72, 0x7c, 0x5e, 0xdc,
+ 0xc5, 0x94, 0x1a, 0xc2, 0xeb, 0x13, 0x00, 0xf1, 0xf0, 0xd4, 0x36, 0x9d,
+ 0x24, 0x3a, 0x2a, 0x6b, 0xbf, 0x03, 0xf2, 0xce, 0x9b, 0x72, 0x6a, 0x78,
+ 0x21, 0x6c, 0x3f, 0xf1, 0xcf, 0x66, 0xb6, 0x3b, 0x9d, 0x68, 0x07, 0x2d,
+ 0xe0, 0xca, 0x2a, 0x84, 0x5c, 0x1d, 0xd4, 0xde, 0x82, 0xad, 0xc0, 0x88,
+ 0x82, 0x58, 0xed, 0x72, 0x94, 0x18, 0x13, 0xd4, 0xc3, 0x35, 0x3e, 0xb4,
+ 0x34, 0x6f, 0x26, 0x51, 0xf2, 0x34, 0xde, 0xed, 0xb7, 0x52, 0xa0, 0x94,
+ 0x85, 0x52, 0xdc, 0x38, 0xd9, 0x73, 0xdd, 0xd7, 0xa0, 0x6f, 0xec, 0xcf,
+ 0xd9, 0x3d, 0xd2, 0xf4, 0xc1, 0xcc, 0x46, 0xc6, 0xf7, 0xb8, 0x8c, 0xaa,
+ 0x71, 0xbc, 0x48, 0xdb, 0xb1, 0xba, 0x5e, 0x4c, 0x71, 0x67, 0x22, 0x38,
+ 0x9b, 0x7c, 0x47, 0x73, 0xf4, 0xc1, 0x9d, 0x9e, 0x51, 0x93, 0xe7, 0x9b,
+ 0xee, 0x86, 0xaf, 0x1b, 0x6f, 0x1c, 0x5c, 0x6f, 0x0d, 0xca, 0x17, 0xc9,
+ 0xcc, 0x74, 0xdd, 0xaa, 0x40, 0xde, 0x63, 0xee, 0x46, 0x9c, 0x60, 0x49,
+ 0xee, 0x6e, 0x8d, 0xaf, 0xbe, 0x5b, 0x39, 0xc1, 0xdb, 0x2c, 0xaa, 0x2f,
+ 0xed, 0x0d, 0x52, 0xcd, 0x40, 0xd8, 0x7a, 0xd3, 0x81, 0x2d, 0x76, 0x70,
+ 0xee, 0x7e, 0xe9, 0xdd, 0xe7, 0x74, 0x08, 0xa0, 0x40, 0xab, 0x55, 0x1d,
+ 0x25, 0xe2, 0xc1, 0xd4, 0xe8, 0xf2, 0xdf, 0x35, 0x5b, 0x19, 0xd9, 0x18,
+ 0x18, 0x0f, 0x0e, 0xeb, 0x5f, 0xf4, 0x0b, 0xc3, 0x38, 0xb6, 0x0a, 0x16,
+ 0x45, 0xa3, 0xb7, 0x5a, 0x44, 0x2c, 0x45, 0x5f, 0x57, 0x69, 0xd9, 0x7d,
+ 0xe5, 0x92, 0xcc, 0x97, 0xd7, 0x35, 0x08, 0x28, 0x80, 0x1c, 0xab, 0x8d,
+ 0xc6, 0x58, 0xe6, 0xd1, 0x60, 0xb6, 0x09, 0x36, 0x40, 0x0c, 0x00, 0xf1,
+ 0x13, 0xd7, 0x58, 0xca, 0x58, 0x1f, 0x88, 0x52, 0xb4, 0x20, 0x64, 0x9d,
+ 0x3a, 0x22, 0xa9, 0xef, 0x83, 0x6c, 0x35, 0x34, 0x43, 0x55, 0x5f, 0x7c,
+ 0xfd, 0xb2, 0x1d, 0xa5, 0x22, 0x7a, 0xdc, 0xcf, 0xae, 0x1d, 0x09, 0xfb,
+ 0x72, 0xbf, 0xa0, 0x49, 0xe3, 0xe0, 0x47, 0xfe, 0xbf, 0xb4, 0xcf, 0x97,
+ 0xf3, 0x69, 0xa0, 0x1b, 0x59, 0x7f, 0x99, 0xea, 0xd2, 0x5b, 0x01, 0xa8,
+ 0xaf, 0xe3, 0x97, 0xfe, 0xa9, 0xe2, 0xd7, 0x18, 0x20, 0x65, 0xa9, 0x36,
+ 0x77, 0x7c, 0x5f, 0x46, 0x9f, 0xe8, 0x76, 0xfc, 0x17, 0x81, 0x92, 0x92,
+ 0xd9, 0x38, 0xcf, 0x88, 0x39, 0x53, 0xcd, 0x9a, 0x7d, 0x63, 0xba, 0x9c,
+ 0x2e, 0x97, 0x15, 0xbf, 0xd2, 0x76, 0x97, 0x8d, 0x15, 0x5a, 0x06, 0x13,
+ 0x1d, 0xfd, 0x4b, 0x38, 0xfb, 0x7c, 0x9e, 0x08, 0x10, 0xee, 0x95, 0x49,
+ 0x8e, 0x28, 0xe7, 0x9f, 0x92, 0x55, 0xee, 0xa8, 0xf5, 0x7c, 0x87, 0x81,
+ 0x7d, 0x6f, 0x96, 0x83, 0xc9, 0x49, 0x9a, 0xc0, 0x96, 0x9a, 0xfe, 0x05,
+ 0x04, 0xa5, 0x7b, 0xd5, 0xf2, 0x62, 0xf6, 0x29, 0xa6, 0x5e, 0x27, 0x2f,
+ 0xa7, 0x7c, 0x8a, 0x51, 0x35, 0xb8, 0x82, 0xc2, 0xc8, 0x35, 0xcd, 0x2d,
+ 0x9e, 0x2f, 0x16, 0xc6, 0x1d, 0x6a, 0xca, 0x33, 0x55, 0x75, 0x25, 0x1d,
+ 0x65, 0x9a, 0x9b, 0xbf, 0x05, 0x0b, 0xf5, 0x59, 0x70, 0x3b, 0x06, 0xc8,
+ 0x66, 0x89, 0xdb, 0x5c, 0x7e, 0xa0, 0x5d, 0x36, 0x9e, 0x79, 0xf9, 0xc7,
+ 0xab, 0xab, 0x88, 0x8d, 0xdf, 0xc4, 0xd7, 0x68, 0xa9, 0x11, 0x57, 0x16,
+ 0x74, 0xf6, 0x2f, 0x96, 0xce, 0x10, 0xa1, 0xdc, 0x4d, 0xaa, 0xef, 0x62,
+ 0xcb, 0xc6, 0x72, 0x19, 0xc9, 0xee, 0xbf, 0xe7, 0xf9, 0x5b, 0x39, 0x4d,
+ 0x1d, 0x02, 0x1c, 0xd6, 0xd0, 0xa5, 0x06, 0x97, 0x1a, 0xcd, 0xfb, 0x0b,
+ 0x7e, 0x7c, 0x9b, 0xa1, 0xab, 0xb9, 0x50, 0x0d, 0xce, 0x91, 0x1b, 0x95,
+ 0xc9, 0xfd, 0x39, 0x80, 0x7a, 0x14, 0x02, 0x63, 0x23, 0x7b, 0x14, 0x34,
+ 0x3d, 0x52, 0xcf, 0x7b, 0xa2, 0x9f, 0x84, 0x6d, 0xb4, 0x1f, 0xff, 0x41,
+ 0x28, 0x87, 0xbc, 0xfc, 0x59, 0xa4, 0x9e, 0xe6, 0xb0, 0x22, 0x2c, 0x57,
+ 0x68, 0xf4, 0x6d, 0xcf, 0xe5, 0x32, 0xcb, 0x7f, 0x25, 0xe5, 0xf2, 0xaf,
+ 0x7a, 0xb3, 0x67, 0xbb, 0xbc, 0x1a, 0xa9, 0xcc, 0xcd, 0x33, 0xfc, 0x21,
+ 0x09, 0xff, 0xbf, 0x32, 0xe7, 0xbe, 0xca, 0xe7, 0xbd, 0xca, 0xa9, 0x5c,
+ 0xb7, 0x6f, 0xe5, 0xb6, 0x6d, 0x9a, 0x75, 0xc7, 0x35, 0xa2, 0xf0, 0xa4,
+ 0x4a, 0x3e, 0x32, 0xb0, 0xc4, 0xf4, 0xfd, 0xad, 0x1f, 0xc9, 0x1c, 0xb9,
+ 0x53, 0x2e, 0xd3, 0xcc, 0x89, 0x7f, 0x89, 0x70, 0xa5, 0x46, 0xb7, 0x6b,
+ 0xca, 0x45, 0xe2, 0x82, 0x2f, 0x85, 0x56, 0x0b, 0x93, 0xbb, 0xa4, 0x69,
+ 0xba, 0xb5, 0x9e, 0x95, 0xe2, 0x0a, 0xa3, 0x90, 0x8d, 0xdb, 0x66, 0xba,
+ 0x72, 0xf3, 0x4a, 0xd1, 0xa1, 0xc5, 0xf8, 0xdf, 0xdc, 0xc0, 0x92, 0x98,
+ 0xce, 0x17, 0x4c, 0xfe, 0xf0, 0x5e, 0xc6, 0x82, 0xf8, 0x38, 0xe6, 0x4e,
+ 0x0e, 0x7b, 0x8f, 0x76, 0xd5, 0x14, 0xbd, 0xd6, 0xb5, 0x23, 0x09, 0xba,
+ 0xbe, 0xed, 0xf0, 0x2a, 0xe7, 0xdc, 0x2a, 0xfe, 0x9e, 0xcf, 0x1b, 0x2e,
+ 0x6e, 0x81, 0x53, 0x78, 0xdf, 0x4d, 0x62, 0x05, 0x99, 0x2e, 0x11, 0x96,
+ 0x7e, 0xd9, 0x0d, 0xe7, 0xd5, 0x73, 0x1f, 0x5c, 0xa3, 0xe4, 0x3b, 0xff,
+ 0xdd, 0xa3, 0x83, 0x6c, 0xf7, 0xd3, 0x0f, 0x9e, 0xce, 0x73, 0xd9, 0x52,
+ 0x68, 0xea, 0x99, 0xb7, 0xab, 0x88, 0xfd, 0x2f, 0x1e, 0xc9, 0x23, 0x6f,
+ 0x98, 0x54, 0xac, 0x00, 0x9b, 0x9b, 0x0f, 0xe1, 0x09, 0xa1, 0xb3, 0xe6,
+ 0x23, 0x73, 0xa2, 0x27, 0xe1, 0x1d, 0x06, 0x96, 0x61, 0x20, 0x96, 0x24,
+ 0x00, 0xb5, 0x22, 0x9b, 0xca, 0xd6, 0xbb, 0x2d, 0x72, 0x0d, 0xea, 0xa2,
+ 0xde, 0xfb, 0x8b, 0xfa, 0x41, 0xd6, 0xc8, 0x80, 0x8b, 0x33, 0x79, 0x6b,
+ 0xc0, 0xf3, 0xd9, 0x09, 0xe9, 0x6d, 0x86, 0xcd, 0x1d, 0x30, 0xb9, 0x98,
+ 0x85, 0xc4, 0x1f, 0x90, 0xcd, 0x07, 0x91, 0xf2, 0x50, 0xf8, 0xfc, 0x0f,
+ 0xd7, 0x58, 0xbb, 0x73, 0x65, 0x3e, 0x32, 0x22, 0x37, 0x91, 0x72, 0x20,
+ 0xee, 0x77, 0x2c, 0xc7, 0x76, 0x85, 0xf2, 0x6a, 0xcf, 0x9c, 0xe3, 0xe7,
+ 0x08, 0x3f, 0xa8, 0xee, 0x04, 0x02, 0x66, 0x8e, 0x08, 0xcc, 0x25, 0xde,
+ 0xca, 0xd4, 0x6b, 0xba, 0xd3, 0x0a, 0x49, 0x5b, 0x92, 0x46, 0x11, 0x5a,
+ 0x0c, 0x8c, 0x17, 0x7b, 0xbc, 0xad, 0xab, 0x20, 0x3b, 0xde, 0x0f, 0xe9,
+ 0x7d, 0x7d, 0xf3, 0x6b, 0x22, 0x90, 0xe1, 0x00, 0xd2, 0xa9, 0xbd, 0x78,
+ 0x4e, 0xda, 0x3b, 0x8d, 0xb5, 0xad, 0x8c, 0xf2, 0x23, 0x38, 0x7f, 0x67,
+ 0xc6, 0x87, 0x1e, 0xa6, 0x40, 0xe7, 0xd3, 0x35, 0x10, 0x0e, 0xa1, 0x61,
+ 0x60, 0x48, 0xa6, 0x1f, 0x61, 0xe2, 0x7f, 0x5e, 0x2b, 0xcf, 0xd5, 0x86,
+ 0x55, 0x7a, 0xa4, 0xd5, 0x58, 0x4b, 0x12, 0x30, 0x77, 0x15, 0x10, 0x00,
+ 0xc9, 0xe1, 0x70, 0x2e, 0x8a, 0x7c, 0xb6, 0x3a, 0x84, 0xef, 0x6c, 0x9f,
+ 0xef, 0xf1, 0x6c, 0xf2, 0x22, 0xa4, 0x36, 0xc2, 0x2d, 0xc5, 0x7e, 0xb1,
+ 0x9d, 0xdb, 0x5b, 0xb9, 0x3b, 0x06, 0x76, 0x21, 0x5d, 0xfc, 0x06, 0xb7,
+ 0x15, 0xc0, 0x0d, 0xf4, 0x07, 0x64, 0x26, 0x92, 0x3c, 0xbb, 0x99, 0xa3,
+ 0xdf, 0x8e, 0x67, 0x06, 0x17, 0xee, 0x3c, 0xa4, 0x97, 0x8e, 0x83, 0x4e,
+ 0x4b, 0x82, 0x4c, 0x40, 0x45, 0x63, 0x8a, 0xcc, 0xfd, 0x4d, 0x13, 0xb3,
+ 0x5d, 0x56, 0x5d, 0x55, 0x62, 0x69, 0x83, 0x5b, 0x7f, 0x62, 0x43, 0x71,
+ 0xc0, 0xf5, 0x0f, 0x2c, 0xc7, 0x5b, 0x1d, 0x50, 0xac, 0x3f, 0x9b, 0xfa,
+ 0x75, 0xd9, 0x77, 0xa9, 0x67, 0x83, 0x24, 0x9a, 0x48, 0xdd, 0xbe, 0x54,
+ 0x68, 0x05, 0x3b, 0xf4, 0x9b, 0xac, 0xce, 0x2e, 0x85, 0x06, 0xc5, 0x08,
+ 0xad, 0x2d, 0x7d, 0xd1, 0xd8, 0x40, 0x17, 0x57, 0x2c, 0x0e, 0xdc, 0x5c,
+ 0x21, 0xac, 0x55, 0xdd, 0x69, 0xe0, 0x4c, 0x13, 0x85, 0x29, 0x78, 0x43,
+ 0xdf, 0xea, 0xc5, 0x3f, 0x74, 0x0c, 0x8a, 0x12, 0xeb, 0xf7, 0x53, 0x2f,
+ 0x4f, 0x25, 0x20, 0x38, 0xb2, 0x88, 0xce, 0x16, 0x95, 0x98, 0xab, 0xa6,
+ 0xf8, 0x8a, 0x40, 0x67, 0x6d, 0x71, 0xb7, 0xa8, 0x9f, 0x33, 0x27, 0x19,
+ 0x69, 0x37, 0xb0, 0xea, 0xae, 0x1b, 0xe1, 0x7d, 0xad, 0xcb, 0x60, 0x5d,
+ 0xb6, 0x81, 0x8a, 0x1f, 0x66, 0x6f, 0x65, 0xa2, 0x1c, 0x8a, 0x8d, 0x77,
+ 0x37, 0x1f, 0x13, 0x22, 0x08, 0x09, 0xdb, 0x81, 0x70, 0x5f, 0x4b, 0xba,
+ 0x06, 0x14, 0x2d, 0x2c, 0xf9, 0xfd, 0x64, 0xdf, 0x76, 0x28, 0xfa, 0x98,
+ 0x16, 0x9f, 0x85, 0x38, 0x5a, 0x92, 0x0a, 0x7a, 0x1e, 0x38, 0xf6, 0x23,
+ 0x2e, 0xd0, 0xad, 0xb5, 0x68, 0x78, 0x67, 0x77, 0x95, 0x2a, 0x7e, 0x5a,
+ 0x22, 0x15, 0x93, 0x16, 0x91, 0xdd, 0x63, 0x22, 0x5d, 0x83, 0x58, 0xe2,
+ 0xe0, 0xe1, 0x45, 0x65, 0x3e, 0x16, 0xaa, 0x57, 0x5f, 0xbe, 0xc3, 0xdf,
+ 0xda, 0xdd, 0x0f, 0x2c, 0x15, 0x7d, 0x10, 0x68, 0xef, 0xe8, 0x94, 0x85,
+ 0xd7, 0xf2, 0x27, 0x3f, 0x14, 0x76, 0xef, 0xb9, 0x63, 0x30, 0x1b, 0x93,
+ 0x98, 0x75, 0x56, 0x5a, 0x3e, 0x04, 0x07, 0x1a, 0x65, 0xbf, 0x18, 0xc6,
+ 0x2c, 0x64, 0x33, 0x5c, 0x09, 0x21, 0x33, 0xe2, 0x57, 0x5a, 0xd1, 0xb5,
+ 0xa2, 0xa1, 0x5d, 0xae, 0xba, 0xc1, 0x8f, 0x48, 0x78, 0xbb, 0x98, 0x0a,
+ 0x31, 0xd7, 0x95, 0x7e, 0x2c, 0xf2, 0xd7, 0x41, 0x4e, 0x2b, 0x46, 0x4c,
+ 0xe2, 0x20, 0xbc, 0x78, 0x2f, 0x7a, 0xf5, 0x64, 0x75, 0x3f, 0x22, 0x9f,
+ 0x1d, 0xfc, 0xc7, 0xbc, 0x39, 0x99, 0x70, 0x3d, 0xe3, 0x94, 0x6d, 0xb1,
+ 0x1a, 0x80, 0x92, 0xbd, 0x9d, 0xd2, 0xb9, 0x0d, 0xc5, 0x61, 0xe4, 0x70,
+ 0x63, 0x27, 0x8b, 0x5c, 0x5b, 0xa8, 0x1d, 0x3b, 0xad, 0x29, 0x26, 0x4b,
+ 0x67, 0x9a, 0x27, 0x11, 0xfa, 0x42, 0xd9, 0x76, 0x11, 0x51, 0x47, 0x9f,
+ 0xed, 0xfa, 0x62, 0xc9, 0xd2, 0x7c, 0x87, 0xca, 0xc4, 0x67, 0x9c, 0xee,
+ 0xff, 0xcf, 0x2c, 0x83, 0xa9, 0x9f, 0xca, 0xf1, 0x6a, 0xe3, 0x6f, 0x4b,
+ 0x11, 0xd4, 0xa5, 0x42, 0x53, 0xa2, 0x4a, 0x11, 0xfc, 0xc6, 0xa8, 0x6a,
+ 0x68, 0xcb, 0x6e, 0xac, 0xa1, 0x47, 0xc2, 0xea, 0xd7, 0x45, 0xa2, 0xd9,
+ 0xeb, 0x89, 0x3e, 0x98, 0x24, 0x20, 0x92, 0x71, 0xec, 0xf0, 0xbc, 0xbd,
+ 0xcd, 0x46, 0x46, 0x47, 0x1d, 0x54, 0xc2, 0x11, 0xb0, 0x76, 0xe7, 0x6c,
+ 0xb7, 0xcd, 0xd2, 0x6d, 0xc5, 0x4f, 0x44, 0xf1, 0xe5, 0x19, 0x36, 0x89,
+ 0x9e, 0xf0, 0xbf, 0x3f, 0x07, 0x17, 0x58, 0xb3, 0x68, 0x44, 0x14, 0x44,
+ 0x3f, 0xf9, 0x01, 0x70, 0xdc, 0xf9, 0x6f, 0x5d, 0xa0, 0x27, 0xa6, 0x0b,
+ 0x41, 0x74, 0x4e, 0xcd, 0x77, 0xdb, 0x6c, 0x40, 0xd0, 0x21, 0x4a, 0xa2,
+ 0x01, 0x85, 0x8c, 0x34, 0x3b, 0xca, 0x31, 0xa6, 0x48, 0x5d, 0x37, 0xc8,
+ 0xa1, 0xd5, 0xe0, 0x7f, 0x3e, 0xeb, 0x22, 0xba, 0xca, 0xe6, 0xd6, 0x71,
+ 0x63, 0x06, 0x89, 0x46, 0xf0, 0xe5, 0x38, 0x92, 0x28, 0x5d, 0x4c, 0xb9,
+ 0xbb, 0x70, 0x9e, 0x89, 0xb1, 0xe7, 0xf5, 0x1a, 0x86, 0xe8, 0x01, 0x83,
+ 0x6e, 0xb6, 0x37, 0x3a, 0xfb, 0xb8, 0x2f, 0x11, 0x7d, 0xa2, 0x5d, 0xf8,
+ 0x2a, 0x8c, 0xb7, 0xac, 0xbe, 0x89, 0xb9, 0x59, 0x62, 0x09, 0xbf, 0x95,
+ 0xf8, 0x04, 0x28, 0xf2, 0xd0, 0xe1, 0x0f, 0x48, 0xb1, 0xc7, 0xfa, 0x5e,
+ 0xb1, 0xb7, 0x60, 0x74, 0x96, 0x53, 0xb0, 0xac, 0x14, 0xbf, 0xb2, 0xc2,
+ 0xbe, 0xc2, 0xb1, 0xcf, 0xfd, 0xbc, 0xd9, 0x35, 0xa6, 0x63, 0x09, 0x63,
+ 0x14, 0xc4, 0x8a, 0x5a, 0x1f, 0x45, 0x06, 0xa8, 0x6c, 0xe7, 0xdc, 0x5b,
+ 0x81, 0x2f, 0xa5, 0xd4, 0xcf, 0x71, 0x0c, 0x22, 0x5d, 0x80, 0x31, 0x49,
+ 0xbe, 0x4c, 0xe6, 0x1e, 0xe6, 0xad, 0x82, 0x78, 0xc2, 0xe5, 0xfd, 0x91,
+ 0x36, 0xc1, 0xc6, 0xf1, 0x1f, 0xbe, 0xf5, 0xa2, 0x8e, 0x86, 0xf9, 0x05,
+ 0xd3, 0xd2, 0x01, 0x3d, 0x2d, 0xb2, 0xd0, 0x35, 0x75, 0xbd, 0x3d, 0x28,
+ 0xc4, 0xc3, 0xa4, 0xf7, 0x64, 0x85, 0x9c, 0x1c, 0x84, 0x81, 0x40, 0x4f,
+ 0x69, 0x8e, 0xb8, 0x88, 0xb6, 0x0b, 0x35, 0x45, 0x1a, 0x34, 0xa3, 0x1f,
+ 0x8f, 0xc5, 0x62, 0x3f, 0xa9, 0x9e, 0xaa, 0xb0, 0x3c, 0xcf, 0x5d, 0x11,
+ 0xb8, 0xfe, 0xac, 0xfa, 0xfa, 0x0f, 0x7b, 0x97, 0x7e, 0x3e, 0x13, 0x59,
+ 0xc2, 0x65, 0x4a, 0x18, 0xe6, 0xa0, 0x2a, 0x9a, 0x9a, 0x07, 0x73, 0x0a,
+ 0x0d, 0x95, 0xa8, 0x3e, 0xb9, 0x08, 0x90, 0x71, 0xd0, 0x37, 0x76, 0x57,
+ 0xc9, 0x72, 0x68, 0x2e, 0x7b, 0x69, 0xa6, 0x56, 0xcd, 0xb4, 0x37, 0x18,
+ 0xba, 0x9c, 0xbb, 0x25, 0x7b, 0xde, 0xa8, 0xec, 0xd6, 0x31, 0xcb, 0x91,
+ 0x28, 0x38, 0x15, 0x33, 0x4b, 0xaf, 0x06, 0xb2, 0x3e, 0x39, 0x5b, 0x65,
+ 0x26, 0xb4, 0x9a, 0x32, 0xcf, 0x07, 0x4e, 0x4e, 0xd8, 0x8e, 0x70, 0x02,
+ 0x0a, 0xbf, 0xee, 0x9a, 0x65, 0xa3, 0xed, 0xb6, 0x62, 0xdd, 0x0d, 0xc2,
+ 0xa7, 0x45, 0x37, 0xb4, 0x6d, 0xfe, 0xdf, 0x05, 0x97, 0xb3, 0xe2, 0x0a,
+ 0x24, 0x63, 0x68, 0xe0, 0xf9, 0xff, 0xd0, 0xcb, 0x52, 0xbc, 0x4d, 0xe0,
+ 0xfc, 0x7f, 0xc2, 0x66, 0x23, 0xd1, 0x39, 0x73, 0xb9, 0x64, 0xd7, 0xb9,
+ 0x81, 0x32, 0x5b, 0xa3, 0xbd, 0xbd, 0x87, 0x5f, 0x90, 0x2a, 0x03, 0xfc,
+ 0xa6, 0xba, 0xa2, 0xd1, 0xbe, 0xc2, 0x02, 0x03, 0x56, 0xcb, 0xf7, 0x5f,
+ 0x50, 0x8f, 0x5e, 0x3c, 0xab, 0xb9, 0xae, 0x57, 0xf8, 0x73, 0xf3, 0xce,
+ 0xd6, 0x86, 0xe2, 0x88, 0x0f, 0x38, 0xfc, 0x3c, 0xc4, 0x42, 0xdd, 0x84,
+ 0xfc, 0xc9, 0x52, 0xd8, 0xd1, 0x5c, 0x98, 0x69, 0xbb, 0x51, 0xe9, 0x35,
+ 0xec, 0xb4, 0x28, 0x1c, 0xfe, 0x08, 0xe9, 0x9b, 0x89, 0x6d, 0x4c, 0x21,
+ 0x96, 0x2b, 0x87, 0x4d, 0xa4, 0xce, 0x5b, 0x97, 0xab, 0xc1, 0x1c, 0xa4,
+ 0xd5, 0xe4, 0x3b, 0x5c, 0x17, 0x20, 0xac, 0xa9, 0xe2, 0xe2, 0xeb, 0x5e,
+ 0x49, 0xd8, 0x6c, 0x92, 0xf6, 0x0d, 0x0c, 0x51, 0xce, 0xfc, 0xc7, 0x10,
+ 0x6a, 0xc2, 0xb3, 0xb5, 0xfd, 0xec, 0x4f, 0x5c, 0xf0, 0x94, 0x74, 0xe5,
+ 0xf5, 0x8f, 0xf2, 0xe1, 0xf6, 0x4f, 0x30, 0x03, 0xd9, 0x72, 0x17, 0x15,
+ 0xd0, 0x92, 0x27, 0xad, 0x54, 0x0f, 0x5f, 0x82, 0x75, 0x58, 0x50, 0xd3,
+ 0x75, 0x5d, 0x61, 0xde, 0xaf, 0x60, 0x06, 0x13, 0x57, 0x7d, 0x57, 0xa3,
+ 0x35, 0x5a, 0x4c, 0xb1, 0xcf, 0x04, 0x63, 0x2f, 0x6c, 0xd2, 0xfb, 0x9f,
+ 0x39, 0x74, 0x8d, 0x77, 0xd7, 0x0e, 0x6b, 0x79, 0x8f, 0x53, 0xb2, 0x84,
+ 0x6e, 0x03, 0xc9, 0x93, 0x68, 0x07, 0x9a, 0xf3, 0x66, 0x1e, 0x27, 0x55,
+ 0x63, 0x10, 0x3a, 0x17, 0x0c, 0x75, 0xb4, 0xae, 0xef, 0x2d, 0xcc, 0x1e,
+ 0x40, 0xed, 0x59, 0x73, 0xb5, 0xe4, 0xec, 0xb5, 0xf5, 0xab, 0x94, 0x8e,
+ 0x44, 0xfb, 0xa6, 0x0d, 0x56, 0xe0, 0xfb, 0x97, 0xdf, 0x16, 0x27, 0xa7,
+ 0x8a, 0x1c, 0x45, 0x16, 0xd4, 0xd3, 0xd8, 0x51, 0x3d, 0xe3, 0xf1, 0x22,
+ 0xe4, 0xd7, 0x35, 0xd6, 0x31, 0x81, 0x12, 0xc6, 0xcc, 0x46, 0x5d, 0x1b,
+ 0x69, 0x42, 0x7b, 0x29, 0x8a, 0x60, 0x4c, 0x80, 0x9c, 0xa1, 0xef, 0xd1,
+ 0x29, 0x5b, 0xf7, 0x83, 0xc9, 0x3f, 0xe8, 0x87, 0x30, 0xdf, 0x70, 0xbb,
+ 0xec, 0x01, 0xd9, 0xae, 0xa6, 0xd6, 0x74, 0x5d, 0xa0, 0x51, 0x98, 0x36,
+ 0x3e, 0x0d, 0x80, 0xfc, 0xa7, 0x5f, 0x67, 0x46, 0xb0, 0xe1, 0x6f, 0x66,
+ 0xbe, 0x7b, 0x44, 0x63, 0xa3, 0x21, 0xd6, 0x20, 0xf5, 0x0b, 0xb4, 0xd4,
+ 0x0a, 0x0b, 0x1e, 0xb3, 0x38, 0x97, 0xac, 0x5c, 0x29, 0x74, 0xa5, 0x2b,
+ 0xa7, 0xed, 0xe6, 0xcd, 0x08, 0xa3, 0x7f, 0x85, 0x45, 0x11, 0x12, 0x8d,
+ 0xe3, 0x79, 0xe7, 0xe7, 0x13, 0x76, 0xc1, 0xd8, 0x50, 0x6f, 0x9a, 0x9c,
+ 0xcd, 0xec, 0xe8, 0x19, 0x9a, 0x9d, 0xe1, 0x1a, 0xcc, 0x86, 0x15, 0x73,
+ 0x06, 0x87, 0x37, 0xea, 0x6e, 0xd9, 0x19, 0xdd, 0x3f, 0xea, 0xac, 0x88,
+ 0xad, 0xe9, 0x10, 0xe3, 0xf9, 0xd5, 0x55, 0x53, 0x62, 0x57, 0x1c, 0xa0,
+ 0x59, 0x2b, 0x05, 0xc7, 0xda, 0x85, 0x09, 0xdf, 0xc4, 0x1f, 0xb0, 0x26,
+ 0xe8, 0xa8, 0x70, 0x73, 0x0d, 0x78, 0xe4, 0x6f, 0x36, 0x54, 0x28, 0x9b,
+ 0x96, 0xf2, 0x78, 0x62, 0x4e, 0x44, 0xb7, 0x16, 0xca, 0xf5, 0xac, 0x55,
+ 0x12, 0x05, 0xe1, 0xbd, 0x6e, 0x75, 0x53, 0x64, 0x87, 0x44, 0xc7, 0x9a,
+ 0x2e, 0xd0, 0x8d, 0xd5, 0xe7, 0xef, 0x03, 0xa4, 0x72, 0x98, 0xa5, 0x27,
+ 0xd7, 0x90, 0xbe, 0x73, 0x7b, 0xf1, 0xc0, 0xba, 0x24, 0xef, 0x61, 0x28,
+ 0xad, 0x0d, 0xdb, 0xbf, 0x1c, 0x53, 0x9c, 0xf7, 0xac, 0x36, 0x63, 0xcd,
+ 0xf8, 0xcf, 0x1f, 0x88, 0x1e, 0xe6, 0x9c, 0x64, 0x3f, 0xea, 0xdc, 0x64,
+ 0x66, 0x22, 0xea, 0x69, 0xf8, 0xb7, 0x06, 0x01, 0x34, 0xf2, 0x16, 0xbf,
+ 0xf1, 0x0a, 0xc4, 0x00, 0xfb, 0xa8, 0x2a, 0xb2, 0x47, 0xea, 0x28, 0xe9,
+ 0x01, 0x9c, 0x6b, 0x3d, 0x68, 0xe8, 0x65, 0x47, 0x0f, 0x63, 0x9b, 0xb5,
+ 0xbd, 0x83, 0xf3, 0x91, 0xf4, 0xeb, 0x14, 0x46, 0xff, 0xef, 0xa5, 0xd5,
+ 0x20, 0xe6, 0x18, 0xfe, 0xaf, 0xa9, 0xc8, 0xc4, 0x5b, 0x5d, 0x93, 0x52,
+ 0x12, 0x78, 0x25, 0x17, 0x67, 0x71, 0xa2, 0x76, 0x79, 0xd1, 0xa5, 0x9c,
+ 0xa1, 0x8f, 0x2b, 0x31, 0x23, 0x13, 0x5d, 0x04, 0xe6, 0x5c, 0x74, 0xb3,
+ 0x6f, 0xfd, 0xf0, 0x02, 0xd7, 0x6e, 0xdf, 0x0e, 0xc2, 0x09, 0x05, 0xff,
+ 0x5b, 0xd1, 0xe7, 0xf3, 0x3f, 0xec, 0x59, 0x25, 0xe4, 0x49, 0xd4, 0x96,
+ 0x86, 0x64, 0xbc, 0x3f, 0x23, 0x07, 0x88, 0xab, 0x78, 0xb1, 0x8a, 0x67,
+ 0xf8, 0xe3, 0xdb, 0x0d, 0xcf, 0x4e, 0xbb, 0x4c, 0xf9, 0x03, 0x75, 0xe2,
+ 0x6d, 0xec, 0xc7, 0xaa, 0x4a, 0xf3, 0xe3, 0x71, 0x67, 0x9c, 0x23, 0xd7,
+ 0xa9, 0x95, 0x20, 0xee, 0x5d, 0xbf, 0xc8, 0x2a, 0xe3, 0xc1, 0xb6, 0x32,
+ 0x2e, 0xe7, 0xa9, 0xc0, 0x55, 0xea, 0xd7, 0x99, 0x5f, 0xb4, 0xbc, 0x31,
+ 0x9b, 0x3e, 0x5e, 0x0f, 0x77, 0x81, 0xdd, 0xa7, 0x64, 0x44, 0x3a, 0x11,
+ 0xb0, 0x78, 0x1b, 0x4a, 0x95, 0x31, 0x8d, 0x45, 0x99, 0xb1, 0xac, 0xe1,
+ 0x1f, 0x53, 0xb5, 0x3d, 0xbb, 0xdc, 0xe0, 0x05, 0x41, 0xac, 0xae, 0x1d,
+ 0x0c, 0x61, 0x85, 0x0f, 0xdd, 0x66, 0x18, 0xab, 0xf9, 0x01, 0xa4, 0x51,
+ 0xf9, 0xbb, 0x03, 0x1b, 0xd8, 0xc6, 0x1f, 0x38, 0x42, 0xe9, 0x08, 0x7d,
+ 0xc0, 0x38, 0x0b, 0x60, 0xd9, 0x2b, 0xc4, 0x9c, 0x0e, 0xf5, 0x44, 0x05,
+ 0x69, 0x65, 0xf5, 0x22, 0x0a, 0xc4, 0xbf, 0xf9, 0x74, 0xf5, 0x43, 0xb2,
+ 0x3b, 0xda, 0x8f, 0x8d, 0x1c, 0x0f, 0x0b, 0x28, 0x7d, 0x37, 0x8e, 0x44,
+ 0x1e, 0x35, 0x03, 0x98, 0xdd, 0x10, 0x60, 0x81, 0x85, 0x4c, 0xdd, 0xcc,
+ 0x13, 0xd2, 0x9a, 0xad, 0x15, 0xb2, 0x91, 0xd9, 0x7c, 0xb6, 0xff, 0xa4,
+ 0x1f, 0xa9, 0xf4, 0xb0, 0xb2, 0xbf, 0x57, 0x76, 0x86, 0x2f, 0xb5, 0x1a,
+ 0x6b, 0xf4, 0x29, 0xd8, 0x4a, 0xaa, 0x0a, 0xe3, 0x64, 0xcb, 0xc7, 0xef,
+ 0xbe, 0x12, 0xf7, 0x98, 0xca, 0x57, 0x01, 0x59, 0xa0, 0x7e, 0x27, 0x57,
+ 0x5f, 0x80, 0x3f, 0xa8, 0x01, 0x02, 0x9c, 0xbd, 0x69, 0x38, 0xf2, 0x92,
+ 0x13, 0xbf, 0x2f, 0x7c, 0xde, 0x23, 0x4e, 0x73, 0x3d, 0x6e, 0x5c, 0x59,
+ 0x36, 0x4e, 0x69, 0x77, 0x3f, 0xbd, 0x47, 0xf7, 0xcf, 0xb5, 0x2f, 0x6d,
+ 0x03, 0xd6, 0xb4, 0x25, 0xb3, 0x01, 0x46, 0x72, 0x69, 0xea, 0xd5, 0x88,
+ 0x07, 0xe6, 0x4e, 0xdb, 0xf6, 0x22, 0x77, 0x8e, 0x6a, 0x02, 0xf0, 0x5b,
+ 0xe1, 0xf5, 0x0d, 0xdc, 0xf0, 0x0a, 0x02, 0x1a, 0x0c, 0xb9, 0xaf, 0xb0,
+ 0x5e, 0x2b, 0x5d, 0x21, 0x2d, 0xb9, 0x26, 0x39, 0x4e, 0xad, 0x31, 0x8a,
+ 0x81, 0x5d, 0x4f, 0x66, 0x37, 0xb9, 0xd9, 0xe8, 0xdf, 0x1f, 0x8a, 0x42,
+ 0x80, 0x1b, 0x95, 0x82, 0xfa, 0x22, 0x97, 0x4d, 0xac, 0x9d, 0x2c, 0xae,
+ 0x36, 0xc5, 0x4b, 0x64, 0x5f, 0xc2, 0xc6, 0x66, 0xec, 0x5a, 0xd7, 0xe2,
+ 0x4a, 0x20, 0x9e, 0xfa, 0xb9, 0x74, 0x4f, 0x9f, 0xc4, 0xc6, 0xbc, 0xad,
+ 0x07, 0x30, 0x97, 0x2e, 0x58, 0x75, 0xce, 0x7b, 0xb9, 0xb1, 0x9c, 0x38,
+ 0x73, 0x6e, 0x7e, 0x77, 0x93, 0x4f, 0xc5, 0x0d, 0x88, 0x0b, 0x3d, 0x46,
+ 0x48, 0xda, 0x98, 0xf2, 0xc9, 0xf8, 0x55, 0x71, 0xde, 0x02, 0x19, 0x1b,
+ 0x47, 0xef, 0x9f, 0xff, 0x2f, 0x96, 0xdc, 0x9c, 0x4e, 0xef, 0xb4, 0x5d,
+ 0x0d, 0x08, 0x05, 0x5f, 0x41, 0xae, 0x0a, 0x42, 0xbc, 0x7f, 0x4d, 0x7f,
+ 0x66, 0x48, 0x34, 0x62, 0x23, 0xf6, 0xbb, 0x46, 0x63, 0x64, 0xa5, 0x04,
+ 0x81, 0x36, 0xad, 0xc0, 0xf2, 0xf6, 0x81, 0x7f, 0x3f, 0x00, 0x58, 0x25,
+ 0xc0, 0x1d, 0x9a, 0x2b, 0x88, 0xb8, 0x14, 0x05, 0x92, 0x31, 0x84, 0x04,
+ 0x13, 0x44, 0x8c, 0x3c, 0xe8, 0xc7, 0x92, 0xa6, 0x07, 0x06, 0xf6, 0xe3,
+ 0x8d, 0x64, 0x7a, 0x2e, 0xcf, 0x3c, 0x8b, 0x1d, 0xd3, 0x29, 0x3b, 0x85,
+ 0x45, 0x45, 0x67, 0xbb, 0x88, 0x1a, 0xdb, 0xe1, 0x08, 0x25, 0x22, 0xbd,
+ 0xbf, 0xd1, 0x86, 0x5d, 0x47, 0xd8, 0xf4, 0x18, 0x79, 0xaf, 0x5f, 0xea,
+ 0x10, 0x5f, 0x1a, 0x50, 0x13, 0xd8, 0xa8, 0x91, 0xd0, 0x37, 0xd4, 0xe7,
+ 0x2d, 0x59, 0xee, 0x38, 0x37, 0xc2, 0xd0, 0xc3, 0xcc, 0x98, 0x2a, 0xd5,
+ 0xc8, 0xf7, 0xc2, 0x8f, 0xf3, 0x66, 0xaa, 0x36, 0x45, 0xc7, 0x61, 0x69,
+ 0xbc, 0xa8, 0x99, 0xeb, 0x05, 0xb2, 0x83, 0xf7, 0x23, 0x4a, 0xf4, 0x6e,
+ 0x99, 0xd7, 0xdc, 0xf9, 0x77, 0xde, 0xb2, 0x5f, 0x93, 0x5c, 0x6d, 0xf3,
+ 0x54, 0x56, 0x13, 0xe9, 0x73, 0xdb, 0x4f, 0x87, 0xb3, 0x24, 0x59, 0x19,
+ 0x4b, 0x99, 0x40, 0x92, 0x27, 0x16, 0xae, 0x26, 0xfe, 0xac, 0xaf, 0x75,
+ 0x5c, 0x61, 0xf8, 0xac, 0x6e, 0x42, 0x3a, 0x4a, 0xef, 0xb7, 0xe2, 0x77,
+ 0x6b, 0x5d, 0xab, 0xc5, 0x76, 0xb0, 0xd4, 0xa1, 0x19, 0x9b, 0x2a, 0x2f,
+ 0xaa, 0xa0, 0x98, 0x84, 0xf9, 0x09, 0x16, 0xef, 0x12, 0x08, 0xa5, 0xdf,
+ 0x80, 0xdc, 0x29, 0xae, 0xba, 0xa7, 0x00, 0x53, 0xf6, 0x9a, 0xb9, 0xe8,
+ 0x94, 0x67, 0xf6, 0x38, 0x80, 0x3b, 0xfd, 0x19, 0xcb, 0x80, 0x5c, 0x50,
+ 0xe4, 0x31, 0xec, 0xfc, 0x5b, 0x30, 0x00, 0x24, 0xb2, 0x6b, 0x06, 0x5f,
+ 0x45, 0x3e, 0x94, 0x41, 0x3a, 0x1a, 0xf8, 0x64, 0x09, 0x04, 0x35, 0x52,
+ 0xca, 0x14, 0x62, 0xcf, 0xb0, 0x63, 0x12, 0xfa, 0x3f, 0x8b, 0x7a, 0x82,
+ 0x66, 0x47, 0x6a, 0x63, 0x00, 0x10, 0x9b, 0x2a, 0xf5, 0xcb, 0x7f, 0xa6,
+ 0x02, 0x43, 0x70, 0x5b, 0x3b, 0x96, 0xfb, 0x62, 0x22, 0x06, 0xdd, 0x97,
+ 0x9d, 0x6e, 0x2c, 0x7f, 0x04, 0xf6, 0x82, 0x29, 0x5b, 0x49, 0x6f, 0x2c,
+ 0xcd, 0x11, 0x21, 0x30, 0x65, 0xa6, 0x20, 0xb1, 0xce, 0x7d, 0x9b, 0xe7,
+ 0x43, 0xec, 0xc4, 0x13, 0x9d, 0x94, 0x69, 0x5c, 0x64, 0xd4, 0xda, 0x4e,
+ 0x24, 0x54, 0x03, 0xe9, 0x68, 0x16, 0xfc, 0xee, 0xb4, 0x25, 0xd8, 0x3e,
+ 0x60, 0x52, 0xb5, 0xb1, 0x35, 0xbd, 0x7b, 0xf9, 0x6d, 0xfd, 0xe8, 0xef,
+ 0x7a, 0xfe, 0x89, 0x37, 0x32, 0x57, 0x33, 0xa0, 0x05, 0xc1, 0xea, 0x59,
+ 0x56, 0xe8, 0x97, 0x74, 0x5f, 0x09, 0x8f, 0x44, 0x27, 0xd2, 0x12, 0x76,
+ 0xf3, 0x8f, 0x53, 0x94, 0xec, 0xfb, 0x64, 0xb0, 0x99, 0xb3, 0x67, 0xf7,
+ 0xb2, 0x9e, 0xe3, 0x8e, 0x83, 0x3d, 0xe7, 0x01, 0x9f, 0xe9, 0xed, 0xd2,
+ 0x13, 0xae, 0xf3, 0x78, 0x63, 0xfe, 0x05, 0xa7, 0x11, 0x10, 0xb4, 0xcc,
+ 0xb4, 0x12, 0xd7, 0xd4, 0x18, 0xa4, 0x26, 0x54, 0x26, 0xac, 0x78, 0x8a,
+ 0xb7, 0x11, 0x12, 0x90, 0xbc, 0xfe, 0x05, 0xde, 0xae, 0x36, 0x8f, 0xc1,
+ 0x0f, 0x62, 0x93, 0x4d, 0x74, 0xfe, 0xdd, 0x06, 0xa0, 0x10, 0x89, 0x8f,
+ 0x15, 0xdc, 0x4c, 0x52, 0xc3, 0x9d, 0x91, 0xec, 0x57, 0xe3, 0x51, 0x23,
+ 0x07, 0x40, 0x81, 0x47, 0xfe, 0xa4, 0x3b, 0xcb, 0x85, 0x6d, 0xf2, 0x7c,
+ 0xb7, 0xf2, 0x29, 0xff, 0x5e, 0x9b, 0x88, 0x2a, 0x65, 0xef, 0x44, 0xb8,
+ 0x46, 0x2b, 0x4e, 0xf3, 0x87, 0xc9, 0x3c, 0x11, 0x69, 0xc8, 0x11, 0x07,
+ 0xea, 0xc3, 0x59, 0x78, 0x5e, 0x8e, 0x15, 0x3d, 0x97, 0xa4, 0x12, 0xab,
+ 0x79, 0x8a, 0x70, 0xbf, 0x8f, 0x65, 0xc0, 0x82, 0x54, 0x78, 0xd8, 0x6c,
+ 0xf6, 0xd7, 0x0c, 0x3b, 0x22, 0xd8, 0xff, 0x07, 0xfc, 0xd1, 0x8a, 0x10,
+ 0x9a, 0x0f, 0xb6, 0x56, 0x50, 0x0f, 0x3f, 0xac, 0xc1, 0x3a, 0x33, 0x20,
+ 0x35, 0x04, 0xfe, 0xed, 0x9b, 0x65, 0x18, 0x92, 0x0f, 0x66, 0x93, 0x20,
+ 0x7c, 0xd3, 0xb4, 0x90, 0xd1, 0x1b, 0x52, 0x96, 0xfb, 0x0e, 0x9b, 0xd1,
+ 0x2b, 0x83, 0x72, 0x84, 0x59, 0xbc, 0x55, 0xc8, 0xdb, 0x2e, 0xd8, 0x52,
+ 0x8b, 0x57, 0x40, 0x03, 0xa0, 0x57, 0x9f, 0x35, 0xfa, 0x8c, 0xe9, 0x68,
+ 0x28, 0x5e, 0xcf, 0x77, 0xcb, 0xde, 0xf1, 0xe0, 0x80, 0x5a, 0xb1, 0x94,
+ 0xac, 0x00, 0x02, 0xe9, 0x46, 0x32, 0x26, 0xbc, 0x83, 0x28, 0x2a, 0x6d,
+ 0xe7, 0x2c, 0x16, 0x54, 0xff, 0xb4, 0xfb, 0x79, 0x1c, 0xb0, 0x0b, 0xf2,
+ 0xb8, 0x69, 0xe2, 0x46, 0xe2, 0x11, 0xeb, 0x35, 0xbe, 0x43, 0xfa, 0x0f,
+ 0xd2, 0x06, 0x99, 0x8a, 0x8b, 0x41, 0x06, 0xce, 0x99, 0x56, 0x47, 0x7f,
+ 0x89, 0x94, 0x45, 0x0b, 0x88, 0xed, 0x03, 0x12, 0xc4, 0xea, 0x05, 0xa0,
+ 0xfb, 0x8d, 0x29, 0x5a, 0xdb, 0x95, 0x5a, 0xd9, 0xcf, 0xce, 0xb2, 0x0c,
+ 0xd4, 0xea, 0x2b, 0xff, 0xfe, 0x83, 0x84, 0x67, 0x91, 0x3c, 0xf2, 0xf9,
+ 0x1b, 0xf1, 0x38, 0xb1, 0xec, 0xcf, 0x97, 0x84, 0xbb, 0x05, 0x90, 0xec,
+ 0x38, 0xc4, 0x81, 0x95, 0x76, 0xf0, 0x80, 0x21, 0xe2, 0x12, 0xb0, 0x79,
+ 0xbb, 0xdf, 0x61, 0x7c, 0x2c, 0xc0, 0xeb, 0x08, 0x66, 0x82, 0x6c, 0x86,
+ 0xaf, 0x2c, 0x20, 0xf9, 0x0f, 0x0b, 0x19, 0x43, 0xa3, 0xb8, 0x69, 0x26,
+ 0x5e, 0x19, 0x1a, 0x05, 0xb8, 0x4f, 0x5d, 0xe3, 0x47, 0xef, 0x4d, 0x29,
+ 0x00, 0x7d, 0x68, 0xb8, 0x7d, 0x88, 0xec, 0x32, 0xbc, 0xce, 0x71, 0xcb,
+ 0x56, 0x52, 0xbb, 0xea, 0x78, 0x8d, 0x59, 0xd9, 0x6c, 0x5d, 0x5a, 0xc1,
+ 0x5d, 0x0f, 0xbb, 0xec, 0x4e, 0xa1, 0x0d, 0xb2, 0x3a, 0x92, 0x90, 0x8f,
+ 0x49, 0x25, 0xbe, 0x87, 0xb8, 0xa7, 0x4d, 0x23, 0x3d, 0xf2, 0xf3, 0x1e,
+ 0x51, 0x4a, 0xb4, 0x95, 0x94, 0x45, 0x9f, 0x60, 0x81, 0x55, 0xe7, 0xc3,
+ 0x71, 0x36, 0x1c, 0x52, 0xec, 0x57, 0x56, 0x32, 0x36, 0xa4, 0x34, 0xbd,
+ 0xd7, 0xec, 0xe7, 0x0d, 0xb9, 0xd8, 0x2c, 0xbb, 0x0f, 0x18, 0x2c, 0xd4,
+ 0x50, 0x4a, 0x04, 0x13, 0x8d, 0xdf, 0x89, 0x41, 0x06, 0xf3, 0xce, 0x2b,
+ 0xe8, 0xa5, 0x60, 0xd2, 0xa6, 0x43, 0x51, 0x20, 0x0b, 0x47, 0x21, 0x8e,
+ 0x14, 0x06, 0x51, 0x7a, 0x8f, 0xd9, 0xb8, 0xbf, 0xb8, 0x63, 0xf7, 0x6e,
+ 0x5a, 0xd4, 0xcd, 0xd6, 0xb7, 0xf5, 0x65, 0x91, 0x7e, 0x95, 0x16, 0x6b,
+ 0x2c, 0x37, 0x26, 0x02, 0xe5, 0x76, 0xd3, 0x2e, 0xf0, 0xfe, 0x5c, 0xce,
+ 0x70, 0x22, 0x08, 0x36, 0x0a, 0x5f, 0x38, 0xdb, 0xfc, 0xf1, 0x5b, 0xe1,
+ 0x6e, 0x3a, 0x3e, 0xfb, 0xe1, 0x07, 0x02, 0x16, 0xce, 0xbd, 0x73, 0xfb,
+ 0xc0, 0x79, 0x4d, 0xc9, 0xe4, 0x85, 0xc6, 0x4e, 0x7f, 0x3c, 0xf2, 0x67,
+ 0x8f, 0x8c, 0x4f, 0x08, 0x6f, 0xac, 0x7f, 0x09, 0x41, 0x58, 0x88, 0xc5,
+ 0x95, 0xf9, 0x34, 0x81, 0xc1, 0x8b, 0xa7, 0xed, 0xf6, 0x6a, 0x4c, 0x83,
+ 0xa9, 0x95, 0xac, 0x2f, 0x6d, 0x81, 0x5f, 0x5f, 0x32, 0x7f, 0x42, 0x89,
+ 0x99, 0x7d, 0x9b, 0x0c, 0xde, 0xea, 0x25, 0x4f, 0xef, 0xa1, 0x1b, 0x94,
+ 0xc8, 0xd1, 0x0b, 0x69, 0x58, 0x2c, 0x21, 0x58, 0xa1, 0x8f, 0x00, 0x1c,
+ 0x3e, 0xa0, 0xbf, 0x06, 0x8e, 0x78, 0xe8, 0x68, 0x62, 0xee, 0x62, 0x8e,
+ 0xbe, 0xa6, 0x88, 0x13, 0xec, 0xca, 0x92, 0x7a, 0x95, 0x2f, 0x95, 0x30,
+ 0x2c, 0x03, 0xea, 0x74, 0xdd, 0x4f, 0x0e, 0x00, 0x24, 0xb0, 0x56, 0xaa,
+ 0x44, 0xdd, 0x78, 0x56, 0x68, 0x4d, 0x3a, 0xd2, 0xdb, 0xf6, 0x7a, 0x39,
+ 0xa5, 0x78, 0xcf, 0x87, 0x56, 0x52, 0x69, 0xc5, 0x4e, 0x56, 0xa1, 0x70,
+ 0x57, 0xe7, 0x73, 0x24, 0xe9, 0x73, 0xae, 0xcc, 0xcf, 0x49, 0xc9, 0x95,
+ 0x1f, 0xf9, 0x3a, 0xca, 0x89, 0x12, 0xf0, 0x4f, 0xf0, 0x4e, 0x95, 0xce,
+ 0x28, 0x60, 0x38, 0xce, 0x53, 0xe7, 0x0a, 0xba, 0xed, 0x46, 0x12, 0x2d,
+ 0x04, 0x4d, 0x1e, 0xe3, 0x00, 0x0b, 0xff, 0x2f, 0xf3, 0xd1, 0xad, 0x5a,
+ 0x49, 0xe3, 0x5d, 0xef, 0x8c, 0x6c, 0x29, 0x8e, 0xc4, 0x6b, 0x46, 0x82,
+ 0x49, 0x83, 0x69, 0xb8, 0xc3, 0x88, 0xc5, 0x6c, 0x86, 0xc8, 0xdf, 0xaf,
+ 0xae, 0xab, 0x30, 0x86, 0x95, 0x1a, 0x41, 0x16, 0xb4, 0x63, 0xda, 0x9b,
+ 0x12, 0x4e, 0x18, 0xe1, 0x1a, 0x81, 0xc1, 0xdc, 0xce, 0x2b, 0xa6, 0xb5,
+ 0x9c, 0x43, 0x42, 0x2b, 0xfb, 0xfa, 0x29, 0xfc, 0xba, 0x60, 0xce, 0xb5,
+ 0x12, 0x00, 0x91, 0xb9, 0x98, 0x7c, 0x82, 0x83, 0xd9, 0xd1, 0x7c, 0x87,
+ 0xf9, 0x2e, 0x3e, 0xa2, 0xbc, 0x9f, 0x59, 0x48, 0x95, 0x72, 0x60, 0x82,
+ 0xa8, 0x22, 0x37, 0x61, 0x15, 0x40, 0x6d, 0x5a, 0xcb, 0x30, 0xd2, 0x5a,
+ 0xc1, 0xe1, 0x7b, 0x57, 0x60, 0xbd, 0xfa, 0x2e, 0x98, 0x8b, 0xaf, 0x0b,
+ 0x24, 0xd9, 0x47, 0xf3, 0x2c, 0x74, 0x2e, 0xd2, 0xdd, 0x9d, 0x8f, 0x77,
+ 0x29, 0xdd, 0xeb, 0x93, 0xb6, 0xa2, 0xd8, 0x7b, 0x2a, 0x4d, 0x32, 0xf7,
+ 0x95, 0xe1, 0xf9, 0x0f, 0xdb, 0x61, 0xb5, 0x3a, 0xfa, 0xd7, 0x45, 0x02,
+ 0xab, 0x7d, 0x55, 0x3c, 0x97, 0xe6, 0x6a, 0x1a, 0x40, 0x80, 0x1c, 0x6d,
+ 0x1f, 0x2b, 0xdd, 0xc1, 0x3a, 0x6e, 0x12, 0x1a, 0x42, 0x6b, 0x2d, 0x01,
+ 0x5f, 0x01, 0xc6, 0x98, 0xee, 0x09, 0x6f, 0x53, 0xbc, 0x8f, 0x6f, 0x8c,
+ 0xd0, 0xb1, 0x60, 0xfd, 0x5e, 0x1d, 0xf8, 0xc4, 0x6a, 0x3a, 0x76, 0x61,
+ 0x5d, 0x20, 0x2f, 0xb7, 0xea, 0xda, 0x0a, 0x32, 0x31, 0x11, 0xde, 0xef,
+ 0xef, 0x23, 0xac, 0xf3, 0xd2, 0x95, 0x04, 0x17, 0x4b, 0x49, 0x86, 0xfd,
+ 0x76, 0x64, 0x47, 0x84, 0x9f, 0x3e, 0x81, 0x86, 0xa5, 0x46, 0xfc, 0xd5,
+ 0x93, 0x76, 0x3d, 0x83, 0x75, 0x56, 0x05, 0x38, 0xb4, 0x47, 0xb7, 0xd4,
+ 0xb4, 0xe2, 0x5b, 0xb4, 0xef, 0x2f, 0x07, 0xf1, 0x1c, 0x14, 0xbe, 0xee,
+ 0xb8, 0xd3, 0x45, 0x7b, 0x06, 0x9d, 0xb7, 0x22, 0xb1, 0x45, 0xe3, 0x7e,
+ 0x81, 0x02, 0x89, 0x44, 0xcb, 0x10, 0x67, 0x8e, 0xc1, 0xf3, 0xc4, 0x2b,
+ 0x0b, 0x1e, 0x08, 0x5d, 0xe5, 0x9a, 0x52, 0xce, 0xdd, 0xfc, 0xb7, 0xea,
+ 0xcb, 0x38, 0x62, 0x1b, 0x9a, 0xd1, 0x81, 0xf0, 0xf8, 0x5c, 0xdc, 0x25,
+ 0x96, 0x71, 0x80, 0xbb, 0x2d, 0x18, 0xf0, 0x5c, 0xe4, 0xc5, 0x59, 0xb4,
+ 0x5f, 0x41, 0x27, 0xb9, 0xee, 0x13, 0x49, 0xd4, 0xc3, 0x87, 0x14, 0xc7,
+ 0x6c, 0x21, 0xf0, 0xa1, 0xe8, 0xd8, 0xbf, 0xb7, 0x84, 0x2a, 0xee, 0x9b,
+ 0x10, 0xa3, 0xb0, 0x7f, 0x6e, 0x13, 0xf4, 0xf8, 0x4a, 0x2f, 0x79, 0xb9,
+ 0xa1, 0x23, 0xdc, 0xe5, 0xfb, 0x47, 0x49, 0xfd, 0x32, 0xe9, 0xdf, 0x0f,
+ 0x46, 0xd7, 0xe8, 0x6e, 0x56, 0x4e, 0x9c, 0x76, 0x65, 0x2d, 0xd3, 0x03,
+ 0x66, 0x8a, 0x46, 0x7e, 0x46, 0xbf, 0x76, 0x2d, 0x38, 0x7b, 0xb7, 0x6f,
+ 0xda, 0x94, 0xf7, 0x5b, 0xb6, 0x67, 0x00, 0x2d, 0x52, 0xee, 0xc8, 0x3b,
+ 0xb1, 0xd7, 0x05, 0xcc, 0xd3, 0xdf, 0xa4, 0xcd, 0x46, 0xd5, 0xaa, 0xff,
+ 0xc6, 0x81, 0x7a, 0xac, 0xbd, 0x6b, 0x6d, 0x06, 0xbf, 0xf0, 0x72, 0xa5,
+ 0xde, 0xb6, 0xd3, 0xd1, 0xa9, 0x5e, 0x10, 0x84, 0x7a, 0x71, 0x99, 0x6a,
+ 0x7d, 0xa4, 0x34, 0x54, 0x16, 0xda, 0xc9, 0x26, 0x99, 0xe2, 0x90, 0xf4,
+ 0xf8, 0x5b, 0x28, 0xc5, 0xe8, 0x52, 0x64, 0x85, 0xb3, 0xff, 0x7f, 0xb4,
+ 0xda, 0x5f, 0x36, 0x1f, 0xa6, 0x0d, 0xca, 0x70, 0xcb, 0x23, 0x2c, 0x3b,
+ 0x28, 0xce, 0xdc, 0x4e, 0x66, 0x06, 0x0e, 0x3c, 0xbc, 0xac, 0xa1, 0x60,
+ 0x90, 0xf8, 0x7a, 0x52, 0x8b, 0xbf, 0x19, 0x57, 0xcc, 0xe4, 0x0d, 0x03,
+ 0xb1, 0xfd, 0xf5, 0x9c, 0x21, 0x21, 0xfe, 0x5d, 0x76, 0x29, 0x6a, 0x7f,
+ 0xfa, 0xcd, 0xd1, 0x6e, 0x75, 0xd6, 0x88, 0x1a, 0x2e, 0x76, 0x7f, 0xd0,
+ 0xd1, 0xd9, 0x25, 0x2d, 0x5d, 0x50, 0x44, 0x86, 0xac, 0x0d, 0x33, 0x1b,
+ 0x6d, 0xd8, 0x92, 0x33, 0xed, 0x30, 0x9b, 0x1a, 0x73, 0xdf, 0xa8, 0x60,
+ 0x34, 0x01, 0x26, 0x7a, 0x54, 0x44, 0xfd, 0xb8, 0x69, 0x55, 0x50, 0x22,
+ 0xb3, 0x09, 0xfe, 0x4d, 0x12, 0x0f, 0xcc, 0x1e, 0x65, 0x6e, 0x70, 0x92,
+ 0xc7, 0x01, 0xcb, 0xc9, 0xe2, 0x79, 0x38, 0x95, 0x3a, 0x71, 0x0e, 0xfa,
+ 0x23, 0x80, 0x97, 0xaa, 0xd3, 0x7f, 0xe8, 0x51, 0xe2, 0x04, 0xe7, 0x4e,
+ 0x1e, 0x25, 0x98, 0xb2, 0xb2, 0xbe, 0x88, 0xed, 0x61, 0x8b, 0x03, 0xc5,
+ 0x59, 0xe7, 0x20, 0x5e, 0xb6, 0xb7, 0x18, 0xdc, 0x31, 0x63, 0x1a, 0x04,
+ 0xa3, 0xe6, 0x97, 0x29, 0xeb, 0x85, 0xd4, 0xa6, 0xf6, 0xf8, 0xbf, 0x98,
+ 0xca, 0x24, 0x39, 0xf9, 0x60, 0xf2, 0x2b, 0x51, 0x23, 0x59, 0x83, 0x99,
+ 0x34, 0x85, 0x34, 0x76, 0xbc, 0xd8, 0x90, 0xbf, 0x69, 0xc2, 0xe6, 0x65,
+ 0xdd, 0x69, 0x45, 0xa3, 0x44, 0xc8, 0x9a, 0x98, 0x75, 0x21, 0x02, 0xff,
+ 0xbf, 0x9b, 0x64, 0xf1, 0x3f, 0x9e, 0xc0, 0xb5, 0xc6, 0x75, 0x30, 0x63,
+ 0xd8, 0xda, 0xf2, 0xb8, 0x00, 0xfc, 0x76, 0x86, 0x1c, 0xe8, 0x8b, 0x7a,
+ 0x68, 0xbb, 0x1a, 0x38, 0x73, 0x43, 0xe7, 0x5b, 0x69, 0x5f, 0x07, 0x40,
+ 0x11, 0x6d, 0xeb, 0xf3, 0x7d, 0x73, 0xfa, 0x9a, 0xb0, 0xfb, 0x1a, 0x90,
+ 0xf8, 0x9a, 0xb2, 0x82, 0x5d, 0xb2, 0x43, 0xc9, 0xe1, 0x5f, 0x86, 0xab,
+ 0xbc, 0xb1, 0x6f, 0xa1, 0xf9, 0x53, 0x76, 0xd9, 0x1e, 0xfe, 0xaf, 0xc2,
+ 0xb4, 0xa9, 0x7d, 0xc6, 0x74, 0x56, 0x12, 0x78, 0xac, 0xb2, 0xae, 0xfc,
+ 0xa9, 0xd8, 0x3e, 0xaa, 0xb0, 0x23, 0xae, 0x1d, 0xf6, 0xd7, 0x58, 0x39,
+ 0xbb, 0xb3, 0xa2, 0xe4, 0x4c, 0x80, 0x0e, 0x88, 0x2c, 0x69, 0x59, 0x62,
+ 0xbd, 0x48, 0x60, 0x82, 0x14, 0xa1, 0xdd, 0x75, 0x92, 0x72, 0xe0, 0xa8,
+ 0xb1, 0xf2, 0x34, 0xd2, 0xcf, 0x6c, 0x16, 0xaa, 0x33, 0x86, 0x1b, 0x2d,
+ 0xba, 0x11, 0x1e, 0x85, 0x38, 0x3e, 0x14, 0x02, 0x4b, 0xda, 0x54, 0xdf,
+ 0xe9, 0x43, 0x55, 0xd7, 0xc8, 0x12, 0x2c, 0x52, 0xb6, 0x50, 0x52, 0x73,
+ 0x50, 0x43, 0x77, 0xa4, 0xed, 0xf6, 0x73, 0xf9, 0xff, 0x76, 0x6f, 0xc3,
+ 0xb2, 0x1e, 0x91, 0x6c, 0x5e, 0xe4, 0x2d, 0xbd, 0xc1, 0x6c, 0x98, 0x17,
+ 0x5c, 0xee, 0x34, 0x69, 0x7f, 0x75, 0xd1, 0xb9, 0x30, 0xe2, 0xe7, 0xcf,
+ 0x8a, 0xb1, 0x43, 0xf6, 0x68, 0xcb, 0xfa, 0xdb, 0x65, 0xff, 0x58, 0xcc,
+ 0x81, 0xcb, 0xa1, 0x7d, 0x38, 0x87, 0x22, 0x0d, 0xd4, 0xb1, 0xf3, 0x34,
+ 0x34, 0xc8, 0xb2, 0xe2, 0x03, 0x36, 0x57, 0x49, 0xa2, 0x58, 0x40, 0x11,
+ 0x2c, 0x11, 0x9d, 0x3f, 0x94, 0x5c, 0x07, 0xac, 0x91, 0x32, 0x62, 0x38,
+ 0x25, 0xa6, 0x88, 0x49, 0x12, 0xdf, 0x0a, 0xb2, 0xb9, 0xf7, 0xda, 0x4f,
+ 0xa4, 0x0d, 0x4c, 0x69, 0xe6, 0xf3, 0xcf, 0xff, 0x86, 0x1d, 0x0f, 0xb0,
+ 0x8f, 0xf0, 0x9b, 0xbf, 0xfc, 0xa3, 0x1a, 0x5f, 0x20, 0x07, 0xe6, 0x4f,
+ 0x36, 0xc8, 0xb0, 0xa1, 0x9a, 0xb7, 0x6b, 0x80, 0x23, 0xf7, 0x31, 0xb5,
+ 0x2a, 0xb8, 0x93, 0x26, 0x46, 0xc2, 0x4a, 0x55, 0x05, 0xa2, 0x20, 0xa4,
+ 0xd9, 0xd1, 0x5f, 0x81, 0xe1, 0xfe, 0x58, 0xf4, 0xe8, 0x98, 0x80, 0xcf,
+ 0xae, 0xcc, 0x14, 0x9e, 0xbb, 0x2a, 0xf4, 0x24, 0x7a, 0x68, 0xb8, 0x3b,
+ 0xcb, 0x48, 0xe5, 0x04, 0xec, 0xe6, 0x86, 0x0a, 0xe2, 0x39, 0x96, 0x69,
+ 0x3b, 0x8b, 0x06, 0xee, 0xb5, 0xaf, 0x57, 0x2b, 0x21, 0x38, 0xf8, 0xee,
+ 0xfe, 0xc6, 0x51, 0x74, 0x80, 0x14, 0x5e, 0xc4, 0xbe, 0x92, 0xf1, 0xea,
+ 0xe0, 0x67, 0x19, 0xe3, 0xb9, 0x7f, 0x33, 0x00, 0x52, 0x0d, 0x1e, 0x36,
+ 0x41, 0x87, 0xd5, 0x3e, 0x88, 0x30, 0x80, 0xcf, 0x8b, 0x73, 0x66, 0xa5,
+ 0xbd, 0xe3, 0x06, 0x25, 0x15, 0x2b, 0xed, 0xc4, 0x8a, 0xc7, 0x32, 0xa9,
+ 0x42, 0x7f, 0x63, 0x07, 0x38, 0x6d, 0x04, 0x64, 0x51, 0x6a, 0x0a, 0xbf,
+ 0x83, 0xb8, 0xb7, 0xfd, 0x19, 0x4f, 0xcc, 0xbb, 0xb6, 0xeb, 0x34, 0xc7,
+ 0xaf, 0x08, 0x62, 0x57, 0xb0, 0x3f, 0x25, 0x0e, 0xbf, 0x34, 0xda, 0xbf,
+ 0xb9, 0xaf, 0x43, 0x80, 0x63, 0xa5, 0xe9, 0x6f, 0x60, 0xd4, 0xfa, 0x9f,
+ 0xdb, 0xd9, 0xbb, 0x24, 0xef, 0xb8, 0x58, 0xe6, 0xc0, 0xa7, 0xc0, 0x0b,
+ 0x13, 0x28, 0xa3, 0xe3, 0xc6, 0x0c, 0x0c, 0x0d, 0x52, 0x25, 0xad, 0x84,
+ 0x29, 0x4a, 0xcd, 0xc3, 0x14, 0x48, 0xf6, 0xb9, 0xaa, 0x51, 0x14, 0x86,
+ 0x8e, 0xf2, 0x98, 0xc7, 0xcc, 0xd8, 0xbc, 0xa1, 0x7f, 0xab, 0x96, 0x8c,
+ 0x82, 0x8c, 0x00, 0x87, 0xbe, 0xb7, 0xf1, 0x68, 0x4d, 0xd6, 0xaa, 0x4e,
+ 0xab, 0x69, 0x03, 0xa5, 0x56, 0x3b, 0x7e, 0x69, 0xd2, 0x7d, 0x07, 0x87,
+ 0xee, 0x2e, 0xab, 0xa8, 0xd6, 0xeb, 0x3f, 0x5c, 0x83, 0x8a, 0x76, 0xdc,
+ 0xaa, 0x71, 0xac, 0x60, 0xa3, 0x1a, 0xd9, 0xff, 0x90, 0x72, 0x2f, 0x30,
+ 0xc1, 0x6d, 0x77, 0x23, 0xd7, 0xa8, 0x43, 0x8b, 0x1f, 0x84, 0x3b, 0x88,
+ 0x23, 0xeb, 0x41, 0xaa, 0x18, 0xd0, 0x80, 0x5f, 0xd4, 0x6d, 0x5f, 0xe4,
+ 0xe4, 0x6a, 0x06, 0xa6, 0xd5, 0xaf, 0x58, 0xdf, 0x8c, 0x97, 0x31, 0x98,
+ 0x7a, 0xd8, 0x84, 0xb3, 0x9f, 0xc6, 0x13, 0xeb, 0x21, 0xeb, 0x34, 0x44,
+ 0xd4, 0xe2, 0x6c, 0xee, 0xbf, 0xdf, 0xa0, 0x11, 0x4e, 0xee, 0xe3, 0xb8,
+ 0xb7, 0x21, 0x20, 0x10, 0xfa, 0x57, 0xdf, 0xfe, 0xcd, 0xcc, 0xf6, 0x08,
+ 0x7e, 0xe8, 0xd0, 0xe5, 0x70, 0x15, 0x49, 0x6f, 0xb0, 0xcd, 0x7d, 0x8c,
+ 0x11, 0x50, 0x3f, 0x1f, 0xb8, 0xec, 0x84, 0x06, 0x7f, 0xd5, 0xc5, 0xa5,
+ 0xfc, 0x96, 0xe6, 0x3d, 0x0f, 0x22, 0xe2, 0x68, 0x92, 0x5a, 0xd3, 0xa8,
+ 0x27, 0xdd, 0x3d, 0x23, 0x6d, 0x46, 0xa7, 0xae, 0xbd, 0xec, 0x04, 0xac,
+ 0x13, 0x70, 0x3a, 0x40, 0xae, 0xc0, 0x2d, 0xea, 0xb5, 0xf0, 0xaa, 0xfc,
+ 0xc1, 0x1f, 0x98, 0x28, 0x21, 0x71, 0x75, 0x44, 0x19, 0x8e, 0xb3, 0x0c,
+ 0x90, 0x55, 0xfb, 0x21, 0x32, 0xc0, 0x2b, 0x3b, 0x09, 0x3c, 0x5e, 0x80,
+ 0x4d, 0x54, 0xe5, 0xc1, 0xa4, 0xd2, 0xb2, 0x50, 0x9a, 0x91, 0xdf, 0xd1,
+ 0x16, 0xdd, 0x61, 0xa3, 0x0d, 0x12, 0xfd, 0xcd, 0x34, 0x55, 0x9e, 0x1c,
+ 0x07, 0xcf, 0xc9, 0xc2, 0xd1, 0x7f, 0xa8, 0x2d, 0x47, 0xfb, 0xfa, 0xb4,
+ 0x50, 0x20, 0x44, 0xef, 0xfa, 0x94, 0x1d, 0x94, 0x61, 0x8b, 0x14, 0x97,
+ 0x42, 0xf5, 0x0a, 0xb1, 0xbd, 0xa6, 0x45, 0xd8, 0x26, 0x5d, 0xcc, 0xc0,
+ 0xd9, 0x36, 0xa4, 0x45, 0x53, 0x2e, 0x0d, 0x51, 0x93, 0x28, 0x2e, 0x0e,
+ 0xe4, 0xfb, 0xae, 0x4c, 0x0b, 0x5a, 0x72, 0x76, 0xaf, 0xab, 0x43, 0x4f,
+ 0xdd, 0x58, 0x40, 0x63, 0xcc, 0xe2, 0xdc, 0x19, 0xa0, 0xb9, 0x3c, 0xa9,
+ 0xf2, 0x3e, 0x7e, 0x3f, 0xb1, 0xb1, 0xc5, 0x59, 0x0d, 0xdd, 0xd4, 0x68,
+ 0xda, 0x89, 0xf6, 0x0c, 0xea, 0x04, 0xca, 0xc3, 0xc1, 0xa8, 0x2b, 0x64,
+ 0xf8, 0x86, 0x6e, 0x5a, 0x32, 0xcf, 0xd1, 0x3b, 0xf9, 0x4b, 0x2d, 0xbe,
+ 0x12, 0xaf, 0xbc, 0x58, 0x74, 0x55, 0x0c, 0xa6, 0xdf, 0xc5, 0x30, 0x27,
+ 0x84, 0x22, 0x3c, 0xd8, 0x45, 0x59, 0x49, 0x65, 0xea, 0x2e, 0xf1, 0x02,
+ 0xfa, 0x11, 0x2a, 0x7b, 0xab, 0x27, 0xab, 0x59, 0x05, 0x8e, 0xe5, 0x25,
+ 0xdf, 0x18, 0xee, 0xc6, 0xd5, 0x52, 0xf3, 0xb5, 0x79, 0x89, 0x0b, 0x56,
+ 0x38, 0xf7, 0x91, 0x48, 0xc0, 0x02, 0xb8, 0x2c, 0x31, 0x12, 0x9c, 0x8d,
+ 0x32, 0x4e, 0x7d, 0xfd, 0x7a, 0x15, 0x95, 0x52, 0x2f, 0x04, 0x18, 0x30,
+ 0x4a, 0xac, 0x06, 0x5b, 0x16, 0xc6, 0x4a, 0x62, 0x90, 0x61, 0x40, 0x63,
+ 0x64, 0x14, 0x0c, 0x2c, 0xc2, 0xe3, 0x31, 0xc5, 0x73, 0x70, 0x12, 0x65,
+ 0xed, 0x66, 0x3e, 0x85, 0xc0, 0xa7, 0xbb, 0x14, 0xb0, 0xc9, 0x9f, 0x0c,
+ 0x06, 0xe7, 0x82, 0x4a, 0xac, 0xd4, 0xf8, 0x6e, 0x40, 0xb9, 0x15, 0x47,
+ 0x67, 0x85, 0x29, 0x54, 0xd5, 0xbc, 0x54, 0x79, 0x6e, 0x06, 0x78, 0xf8,
+ 0x30, 0x30, 0x18, 0x1b, 0x43, 0x2b, 0x35, 0x74, 0x4e, 0xba, 0x71, 0x35,
+ 0x9f, 0xaf, 0x9c, 0x50, 0xa4, 0x8d, 0xb8, 0x54, 0x39, 0x99, 0x99, 0x40,
+ 0x7e, 0xf0, 0xc4, 0x25, 0x67, 0x07, 0x61, 0x2c, 0xd1, 0x8f, 0x55, 0x13,
+ 0x40, 0x8d, 0x69, 0xa8, 0x10, 0x21, 0xff, 0xe2, 0xe1, 0xc9, 0xd4, 0xc1,
+ 0x11, 0xbe, 0x54, 0x1a, 0xf0, 0x3b, 0x16, 0xb3, 0xf9, 0x56, 0x59, 0x2d,
+ 0xdf, 0x49, 0x83, 0xb9, 0x1b, 0xdc, 0x3a, 0xd6, 0x70, 0x7b, 0x72, 0xec,
+ 0x22, 0x35, 0xd5, 0x0c, 0xe6, 0x85, 0x39, 0xd5, 0x56, 0x24, 0x70, 0x78,
+ 0x2e, 0x9c, 0x9c, 0xc7, 0x32, 0x68, 0xb2, 0xf8, 0xc9, 0xc0, 0xd8, 0xb3,
+ 0xe1, 0x71, 0x27, 0xf6, 0x21, 0x14, 0xd4, 0xd1, 0x66, 0x67, 0x94, 0xf5,
+ 0x9a, 0x36, 0x63, 0x74, 0x7b, 0xce, 0x92, 0x92, 0x0e, 0xbb, 0x42, 0x0a,
+ 0xf8, 0x77, 0x23, 0x81, 0xb9, 0x1a, 0x19, 0x9e, 0x6c, 0x30, 0xc2, 0x52,
+ 0xec, 0xd4, 0xf1, 0xf3, 0x7a, 0x69, 0x71, 0x00, 0xde, 0xf7, 0x5d, 0x3a,
+ 0x99, 0x91, 0x27, 0x74, 0x11, 0x70, 0x67, 0xca, 0x0a, 0x84, 0x16, 0xe2,
+ 0xbe, 0x6b, 0xee, 0x72, 0x37, 0xb4, 0x61, 0x75, 0x36, 0x39, 0xee, 0xc5,
+ 0xfc, 0x2b, 0xd5, 0x5b, 0x34, 0xe2, 0x40, 0xb7, 0xb5, 0xb1, 0x4f, 0x1a,
+ 0x0e, 0x39, 0x0a, 0x30, 0xa4, 0xc3, 0x7e, 0xfe, 0xb0, 0xb9, 0xda, 0xf7,
+ 0xc7, 0x10, 0x4d, 0x03, 0x6a, 0x3b, 0xf0, 0xa2, 0xe0, 0xdb, 0xd0, 0xef,
+ 0xd7, 0x89, 0xf0, 0xdd, 0xae, 0x62, 0x98, 0x14, 0xc3, 0x6b, 0xce, 0x0a,
+ 0x03, 0xdf, 0xf5, 0xb0, 0xa0, 0x97, 0x52, 0x5b, 0xf3, 0x11, 0x48, 0xdc,
+ 0x86, 0xa6, 0x98, 0x0a, 0x7d, 0xae, 0xa5, 0x79, 0x9d, 0xba, 0xf0, 0x89,
+ 0x11, 0x44, 0xe1, 0x1e, 0xcf, 0xa2, 0x1c, 0x3c, 0x22, 0xb3, 0x6a, 0x6c,
+ 0x3c, 0xd2, 0x5d, 0xfa, 0x3c, 0x32, 0x15, 0x0b, 0xdd, 0x63, 0x36, 0xb3,
+ 0x9e, 0x0f, 0x57, 0x4b, 0x1b, 0x69, 0xbb, 0x1c, 0x90, 0xf7, 0x23, 0x1a,
+ 0x8e, 0x01, 0x57, 0xbb, 0xbb, 0x92, 0xb7, 0x1b, 0x4e, 0xf2, 0x41, 0xa4,
+ 0x05, 0xed, 0x44, 0xf9, 0xa2, 0xaf, 0x32, 0x35, 0x25, 0x5f, 0xa5, 0x89,
+ 0xf6, 0xa9, 0x90, 0xcc, 0x76, 0x7d, 0xfc, 0x09, 0xb4, 0x8f, 0x80, 0x0d,
+ 0xc7, 0xc6, 0xe4, 0xc4, 0xc6, 0xcb, 0x58, 0xe0, 0xb4, 0xd5, 0x68, 0x5c,
+ 0x52, 0x7e, 0x9b, 0xbc, 0xcb, 0xdc, 0x2a, 0x9c, 0x32, 0x95, 0xb8, 0xfc,
+ 0x6b, 0x82, 0x2e, 0xbf, 0xa9, 0xeb, 0xd0, 0x0a, 0x16, 0x19, 0x1b, 0xef,
+ 0xe1, 0x74, 0x34, 0x61, 0x24, 0xbe, 0xd7, 0x98, 0x74, 0x8d, 0x19, 0x5a,
+ 0x72, 0x6f, 0x57, 0x7d, 0xad, 0x8a, 0xfa, 0xff, 0x9a, 0x59, 0x87, 0x16,
+ 0xa4, 0x48, 0xf6, 0xb4, 0x5c, 0x2e, 0xbc, 0xc6, 0xab, 0xd2, 0x08, 0x69,
+ 0x1a, 0xdf, 0x45, 0x66, 0x66, 0xa1, 0xcc, 0x10, 0x5f, 0x11, 0xaf, 0x7a,
+ 0x71, 0xfe, 0xb0, 0x47, 0x01, 0x96, 0xc5, 0xc4, 0xf6, 0x30, 0x7e, 0xb6,
+ 0xd8, 0x99, 0x56, 0x56, 0x84, 0xda, 0x9a, 0x1f, 0x7e, 0xfe, 0x60, 0xdb,
+ 0xdf, 0xf7, 0x62, 0x30, 0xe8, 0x61, 0xec, 0x81, 0x9c, 0x66, 0x67, 0xc4,
+ 0xde, 0x9a, 0xff, 0x40, 0x2e, 0xc5, 0x2d, 0x73, 0x6b, 0x65, 0xed, 0x6d,
+ 0x7b, 0xff, 0xcd, 0x67, 0xea, 0x03, 0xbf, 0xf6, 0xed, 0x3f, 0x66, 0xcd,
+ 0xa0, 0xac, 0x3c, 0xca, 0xcd, 0xee, 0xd5, 0x10, 0x83, 0x1c, 0x89, 0x2c,
+ 0x9e, 0x38, 0xbe, 0xb9, 0x2a, 0xc0, 0xa9, 0x0f, 0x03, 0x64, 0x50, 0xf5,
+ 0x75, 0x95, 0x66, 0x38, 0x4f, 0x9b, 0x63, 0xca, 0x83, 0xbd, 0x3a, 0x6f,
+ 0xdb, 0x35, 0x9d, 0x13, 0xe8, 0x92, 0x60, 0x36, 0x41, 0xdf, 0x88, 0xcd,
+ 0x13, 0x46, 0x0d, 0x2a, 0x87, 0xc0, 0x64, 0x1d, 0x6d, 0xdc, 0xd0, 0x9f,
+ 0xe8, 0x5e, 0xe6, 0x64, 0xf9, 0x58, 0x73, 0x49, 0x39, 0xc7, 0xd7, 0xaf,
+ 0xc8, 0x1d, 0xce, 0x43, 0xcb, 0x4d, 0x79, 0x1e, 0x7d, 0xb8, 0x65, 0x45,
+ 0xdc, 0xd7, 0x74, 0xf8, 0xc3, 0xa3, 0xe9, 0xf2, 0x12, 0x1e, 0xb1, 0xd9,
+ 0x13, 0x9d, 0xad, 0x7b, 0x38, 0x46, 0x50, 0xff, 0x2a, 0xd1, 0xfa, 0x1a,
+ 0x53, 0xbc, 0xce, 0xfd, 0x4e, 0x4b, 0xc4, 0x7b, 0x2d, 0xe7, 0xd1, 0x8c,
+ 0xa1, 0xd2, 0x7a, 0x9a, 0x02, 0x90, 0x70, 0x12, 0x61, 0xb3, 0x75, 0x11,
+ 0xe6, 0x39, 0x6f, 0x35, 0x30, 0x3a, 0xf3, 0x3c, 0x31, 0x94, 0x93, 0x21,
+ 0x6c, 0x21, 0xbb, 0xf1, 0xa6, 0x47, 0xb0, 0x3c, 0xec, 0x6d, 0xf7, 0x68,
+ 0x32, 0x6c, 0x33, 0x8d, 0x80, 0x7c, 0xbc, 0x80, 0xdf, 0x22, 0x2a, 0x5f,
+ 0x7d, 0x6a, 0x75, 0x08, 0x26, 0x22, 0x9a, 0x53, 0xb5, 0x99, 0xdc, 0x62,
+ 0x7a, 0x81, 0xc9, 0xb5, 0xe2, 0x7e, 0x37, 0x63, 0x88, 0xfc, 0xad, 0x66,
+ 0xc6, 0xe6, 0x97, 0xb4, 0xbd, 0xea, 0x63, 0xc9, 0x3c, 0xae, 0x07, 0x90,
+ 0x28, 0x5a, 0x0b, 0xa6, 0xef, 0xbe, 0xe9, 0x0c, 0xb3, 0x71, 0x49, 0x5d,
+ 0xbc, 0x1f, 0xd4, 0x4c, 0xa4, 0xd3, 0xa6, 0x05, 0x14, 0xd4, 0x09, 0xd2,
+ 0x33, 0x7c, 0x22, 0xe3, 0x9c, 0xec, 0xff, 0xae, 0x50, 0xa7, 0x85, 0xd4,
+ 0xce, 0x03, 0x52, 0x46, 0xd3, 0x3c, 0x55, 0x4e, 0x7f, 0x97, 0xa7, 0x32,
+ 0x97, 0x4c, 0xcd, 0xb2, 0x03, 0x01, 0x49, 0x17, 0x58, 0x28, 0xfd, 0xab,
+ 0x19, 0xbc, 0x1c, 0xa4, 0xe4, 0xf9, 0x7f, 0xb4, 0xc8, 0x97, 0x66, 0x93,
+ 0xc1, 0xc0, 0xad, 0x55, 0xd9, 0x4c, 0x71, 0xb4, 0xce, 0x00, 0x2e, 0xaf,
+ 0x97, 0x0f, 0x70, 0xbb, 0x47, 0xfb, 0x3b, 0x10, 0x46, 0x96, 0xd4, 0x8d,
+ 0x4a, 0xbb, 0xe0, 0x6c, 0x63, 0xa7, 0xa0, 0x32, 0xab, 0x69, 0x5b, 0x3e,
+ 0x11, 0x91, 0x0d, 0x91, 0xb8, 0xf7, 0xf8, 0xc5, 0x20, 0x54, 0x44, 0x7a,
+ 0xb7, 0xa4, 0x03, 0xe2, 0xf1, 0x9a, 0x4b, 0x30, 0xd7, 0x54, 0x59, 0x6d,
+ 0x94, 0xf0, 0xce, 0x19, 0xab, 0x03, 0x5c, 0xf9, 0x2d, 0xd7, 0x11, 0x03,
+ 0x87, 0xfb, 0xed, 0x90, 0xa1, 0xee, 0x5d, 0x56, 0xfd, 0xf9, 0xac, 0x35,
+ 0x0d, 0xcd, 0x4e, 0x01, 0xcb, 0x77, 0xc4, 0x1f, 0x86, 0x8c, 0x1d, 0x15,
+ 0x3b, 0x1e, 0x7e, 0xc8, 0x91, 0x49, 0x9b, 0x54, 0x7c, 0xe4, 0x13, 0x39,
+ 0xbf, 0xa9, 0x4d, 0xcb, 0x02, 0x02, 0xf0, 0x7a, 0x82, 0xe5, 0x49, 0x32,
+ 0xcb, 0x18, 0xe5, 0xa3, 0x3e, 0x3e, 0x3a, 0x97, 0xa0, 0x24, 0x49, 0x38,
+ 0x9c, 0x80, 0xde, 0x14, 0x56, 0xa2, 0x58, 0x5e, 0x4f, 0x1f, 0x27, 0x5d,
+ 0x66, 0x4f, 0x4d, 0x5e, 0xbf, 0x83, 0x36, 0xea, 0xa1, 0x80, 0x16, 0x80,
+ 0xee, 0x5c, 0x81, 0x8a, 0x4e, 0x8f, 0xac, 0xc3, 0x68, 0xff, 0xa3, 0x10,
+ 0x88, 0x99, 0x45, 0x25, 0xf1, 0xdf, 0x3b, 0x38, 0xf7, 0xa6, 0x17, 0xe4,
+ 0x12, 0xd4, 0x72, 0x2f, 0x4a, 0xf8, 0xa8, 0x7d, 0x8c, 0x65, 0x14, 0xa9,
+ 0x66, 0x95, 0x48, 0x04, 0x4e, 0x07, 0x7c, 0x0f, 0x5c, 0x5f, 0x61, 0x78,
+ 0x0a, 0xc9, 0x05, 0x49, 0x1b, 0x46, 0xeb, 0xa8, 0x3e, 0x7a, 0xd6, 0x99,
+ 0xff, 0xda, 0x35, 0x31, 0xdc, 0x04, 0x43, 0xf5, 0xe1, 0x3b, 0x20, 0x86,
+ 0x71, 0x8b, 0x83, 0x97, 0x49, 0x20, 0x15, 0x1e, 0xb6, 0xd8, 0x5e, 0x60,
+ 0x5c, 0x14, 0x6d, 0x55, 0xe4, 0x81, 0xb9, 0x31, 0xc2, 0x9b, 0x77, 0xc8,
+ 0x4f, 0x71, 0xe4, 0xd9, 0x45, 0xf8, 0x51, 0x32, 0xec, 0x7f, 0x86, 0x71,
+ 0xe0, 0xb6, 0x12, 0x0c, 0xc4, 0xea, 0xc9, 0x6d, 0x78, 0x46, 0x8b, 0x9b,
+ 0x6b, 0xa2, 0x43, 0x5f, 0xe4, 0xc2, 0x06, 0xaf, 0x8c, 0xca, 0xba, 0xf5,
+ 0xe6, 0x06, 0x13, 0x99, 0x05, 0xd7, 0x6d, 0x15, 0x4c, 0xcc, 0xa9, 0x05,
+ 0xfd, 0x83, 0x92, 0x51, 0xd6, 0x41, 0xef, 0x0f, 0xaa, 0x5a, 0x7d, 0x60,
+ 0x25, 0x81, 0x4c, 0xe2, 0xa2, 0xe7, 0x95, 0xc5, 0xe0, 0xd7, 0x37, 0xe4,
+ 0xa3, 0x80, 0x3c, 0x04, 0x87, 0xb3, 0xc6, 0xf4, 0x98, 0x6d, 0xbb, 0xbb,
+ 0xfe, 0xcd, 0x87, 0x52, 0x01, 0x36, 0xcc, 0xf8, 0x4a, 0x2e, 0x40, 0x0f,
+ 0x94, 0x51, 0x58, 0x34, 0xc5, 0xe8, 0x84, 0x76, 0xd1, 0x59, 0x48, 0xaa,
+ 0xfd, 0x92, 0xa4, 0x35, 0xee, 0x57, 0xa8, 0x72, 0x21, 0xeb, 0x3f, 0x43,
+ 0xbd, 0x47, 0x68, 0xe7, 0x21, 0x0b, 0x05, 0x22, 0x46, 0x43, 0x70, 0xf6,
+ 0x1f, 0x1f, 0xcf, 0xe5, 0xd4, 0xdf, 0x16, 0xb9, 0xaf, 0x23, 0x0a, 0x57,
+ 0xe0, 0xe0, 0xf2, 0xca, 0xca, 0x4c, 0xb8, 0xf0, 0x03, 0xda, 0x7d, 0x47,
+ 0x74, 0x7f, 0x61, 0xc6, 0x64, 0xf7, 0xe8, 0x77, 0xe3, 0xa2, 0xd9, 0x39,
+ 0xa7, 0xdc, 0x47, 0x8f, 0x91, 0x37, 0x98, 0x04, 0x80, 0x06, 0x0e, 0x28,
+ 0xde, 0x72, 0xf1, 0xda, 0xf7, 0x26, 0x89, 0x53, 0x15, 0x5a, 0xbf, 0xfd,
+ 0x62, 0x1f, 0x28, 0x3d, 0x8f, 0xf6, 0xbc, 0x3b, 0xdd, 0x31, 0x22, 0x33,
+ 0x88, 0xae, 0xe6, 0x11, 0x60, 0xc1, 0x0b, 0x6c, 0x36, 0xc5, 0x0d, 0x83,
+ 0xf7, 0x6e, 0xda, 0x12, 0xc1, 0xfa, 0x84, 0xaf, 0x34, 0xc3, 0xb0, 0x2b,
+ 0xe8, 0xc0, 0x22, 0x26, 0xed, 0x90, 0x55, 0x07, 0xd1, 0xe3, 0x48, 0xf2,
+ 0x32, 0xf2, 0x0b, 0x61, 0xdd, 0x1d, 0x85, 0x05, 0xb7, 0x6c, 0xd9, 0x0a,
+ 0x5c, 0x0e, 0x8d, 0x82, 0x8a, 0x52, 0x3f, 0x78, 0xea, 0x1b, 0xd9, 0xe6,
+ 0xac, 0x7f, 0x00, 0x00, 0x5a, 0xc7, 0xbd, 0x8a, 0x84, 0xe1, 0x61, 0xdb,
+ 0x34, 0xf0, 0xa6, 0xee, 0x2e, 0x2e, 0x8f, 0xd3, 0xe7, 0x32, 0xc6, 0xb2,
+ 0x39, 0xdf, 0xe0, 0x7d, 0x6a, 0xcd, 0x4c, 0xe1, 0xdc, 0x2d, 0xaa, 0x51,
+ 0xac, 0xd5, 0x15, 0xb0, 0x13, 0xc3, 0x98, 0x6e, 0x29, 0xb7, 0xb6, 0x10,
+ 0xf7, 0x0a, 0x98, 0x15, 0x02, 0x31, 0x15, 0x73, 0xe5, 0xea, 0x03, 0x2a,
+ 0x40, 0x5f, 0x69, 0x85, 0xf4, 0xcb, 0xc8, 0x52, 0xe4, 0xdb, 0x59, 0x37,
+ 0xc6, 0xc9, 0x58, 0x81, 0x1c, 0xdc, 0xc1, 0xb8, 0x81, 0x29, 0x82, 0xa3,
+ 0x85, 0x3e, 0xd8, 0xd0, 0x67, 0xea, 0x03, 0x3d, 0x0a, 0x45, 0xa1, 0x22,
+ 0x9f, 0xfd, 0x93, 0x64, 0xd6, 0xfa, 0xe4, 0x25, 0xc7, 0x4d, 0x69, 0xbd,
+ 0xa7, 0x37, 0x5f, 0x48, 0xef, 0xe4, 0xf4, 0x35, 0x98, 0x10, 0x0f, 0x65,
+ 0xc1, 0xf0, 0x61, 0x62, 0x6b, 0x13, 0xd3, 0xec, 0xc5, 0x5b, 0x03, 0x50,
+ 0x64, 0x79, 0x8c, 0x28, 0x99, 0xe9, 0xe4, 0x84, 0x79, 0x6d, 0x2d, 0x39,
+ 0xee, 0x2a, 0x12, 0xc8, 0xf7, 0x4f, 0x82, 0x6e, 0x9c, 0x94, 0x15, 0x20,
+ 0x19, 0xd5, 0x26, 0xc8, 0x22, 0x04, 0x38, 0xbb, 0xdb, 0x13, 0x9a, 0x7a,
+ 0x87, 0x21, 0x99, 0x49, 0x00, 0x46, 0x17, 0x2e, 0xdf, 0xed, 0xca, 0x83,
+ 0x17, 0xe3, 0x74, 0xb1, 0x45, 0x2e, 0xe6, 0xc1, 0x09, 0xe9, 0xcc, 0xe9,
+ 0x31, 0x56, 0x4e, 0x23, 0xf1, 0xef, 0x88, 0x36, 0x88, 0x14, 0x7e, 0x7d,
+ 0x4a, 0x82, 0x4f, 0xb6, 0x5a, 0x62, 0xda, 0x8c, 0xb3, 0xa0, 0xca, 0xaa,
+ 0x9b, 0x66, 0x18, 0xb9, 0xbe, 0x0d, 0x59, 0x75, 0x1b, 0x5b, 0xff, 0xf5,
+ 0x58, 0x13, 0x19, 0x2a, 0x48, 0xc6, 0x7f, 0xbe, 0x11, 0x21, 0x5a, 0x1b,
+ 0xf4, 0x49, 0xe7, 0xbc, 0xd6, 0xb2, 0xdf, 0xf9, 0x2a, 0xaf, 0xfb, 0x15,
+ 0xa8, 0xa2, 0x34, 0x24, 0x7e, 0x36, 0x86, 0x33, 0x9c, 0x3e, 0xe3, 0x46,
+ 0x7d, 0x8b, 0x1d, 0x37, 0xfc, 0x47, 0xbe, 0xd2, 0x3f, 0xb8, 0x4c, 0x9d,
+ 0xd4, 0xa9, 0xbb, 0xb6, 0x7f, 0x4a, 0x2f, 0xa0, 0x22, 0xa9, 0x1a, 0x1e,
+ 0xdd, 0xd0, 0x1e, 0xb6, 0x40, 0x98, 0x12, 0xc2, 0x5d, 0xff, 0xfb, 0x20,
+ 0xdc, 0x2c, 0xfd, 0xb9, 0x4c, 0x9a, 0xcd, 0x6b, 0x29, 0xab, 0x75, 0xb0,
+ 0xfd, 0xc4, 0xdd, 0xa0, 0x92, 0xe3, 0x8b, 0x2f, 0x18, 0xbb, 0x7d, 0x82,
+ 0xa5, 0x30, 0x88, 0x21, 0x94, 0x31, 0x44, 0xfd, 0x76, 0xdc, 0xc9, 0x58,
+ 0xb8, 0x0a, 0x8d, 0x14, 0x59, 0x81, 0x22, 0xa7, 0x84, 0xaf, 0x84, 0x1e,
+ 0xc6, 0x5e, 0x0c, 0x50, 0x24, 0xac, 0x8b, 0xa0, 0x5e, 0x45, 0x14, 0x06,
+ 0x52, 0x60, 0x35, 0xa2, 0x88, 0x08, 0x2c, 0xc1, 0x5e, 0xfd, 0xca, 0x57,
+ 0x2b, 0xbe, 0x4d, 0xee, 0x12, 0x8e, 0x60, 0x99, 0x12, 0x9d, 0xad, 0x8c,
+ 0x57, 0x3e, 0xf2, 0x7f, 0x69, 0x57, 0x65, 0x00, 0xa7, 0x48, 0x62, 0x5e,
+ 0xb3, 0x4f, 0x2c, 0x1e, 0x38, 0xb0, 0xc6, 0xbb, 0x8d, 0x77, 0x35, 0x3e,
+ 0xca, 0xd4, 0x36, 0x30, 0x46, 0x20, 0xd2, 0xde, 0xd4, 0x66, 0x50, 0xd9,
+ 0x7f, 0x12, 0x28, 0x28, 0x4d, 0x0c, 0xf0, 0x44, 0x95, 0x17, 0xbf, 0xa5,
+ 0xf9, 0xd5, 0x62, 0x97, 0xbc, 0xc0, 0x08, 0x64, 0x1d, 0x44, 0x69, 0x3f,
+ 0xb0, 0x60, 0xd0, 0xe0, 0x92, 0xd0, 0x87, 0x4f, 0x6d, 0x5f, 0x3a, 0xc2,
+ 0x8e, 0x9b, 0xfc, 0x37, 0x6d, 0x3c, 0xe8, 0xe2, 0xd8, 0x09, 0x0a, 0x04,
+ 0xe2, 0x00, 0xd0, 0x80, 0x13, 0xcd, 0xb9, 0x5f, 0x12, 0x7a, 0xa4, 0xce,
+ 0xc4, 0xa0, 0xee, 0xad, 0xe4, 0xc7, 0x5d, 0x11, 0xf4, 0xaa, 0x38, 0x89,
+ 0xa7, 0x2b, 0xb2, 0x34, 0x38, 0x7c, 0xff, 0x05, 0xe6, 0x3f, 0xc1, 0xb8,
+ 0x1d, 0x80, 0x1e, 0x9b, 0xb8, 0xa1, 0xe1, 0x4a, 0x8d, 0x3f, 0x82, 0x9b,
+ 0x4d, 0x08, 0xce, 0x89, 0x8d, 0x7e, 0xb9, 0x20, 0x1b, 0x71, 0xb3, 0x57,
+ 0x5a, 0x71, 0xdc, 0x45, 0x1b, 0xba, 0x3c, 0x95, 0x18, 0xfb, 0x08, 0xb0,
+ 0x6e, 0x90, 0x27, 0xd3, 0xae, 0x20, 0x39, 0xdb, 0x1f, 0x71, 0xdd, 0x97,
+ 0x6f, 0x7c, 0xfa, 0xd9, 0x12, 0xff, 0x41, 0x81, 0x14, 0x5c, 0xc7, 0xbc,
+ 0xe9, 0xb2, 0x60, 0xe3, 0x0b, 0xf4, 0x27, 0x32, 0x52, 0x7e, 0x08, 0x1d,
+ 0xa0, 0xc3, 0x4d, 0x0b, 0x91, 0x60, 0xba, 0x90, 0xfe, 0x26, 0x62, 0xb1,
+ 0x96, 0x1e, 0x47, 0xa0, 0x70, 0x2a, 0x29, 0xc5, 0xa9, 0xd2, 0x83, 0xff,
+ 0xb5, 0x89, 0x60, 0x7b, 0xc5, 0xe6, 0x67, 0xd2, 0xf6, 0x6d, 0x83, 0xfa,
+ 0xb3, 0xb3, 0x26, 0x48, 0x4e, 0xf2, 0x82, 0x67, 0xbf, 0xa6, 0x9b, 0xdb,
+ 0x84, 0xc9, 0xbd, 0xfd, 0xce, 0x52, 0x84, 0xa5, 0x3a, 0x87, 0x54, 0xea,
+ 0x0b, 0x97, 0xb6, 0xb0, 0x5e, 0x20, 0xde, 0x61, 0xce, 0xf4, 0x45, 0x3e,
+ 0x2f, 0xf5, 0x01, 0x9e, 0xef, 0x20, 0xe4, 0x80, 0x06, 0x84, 0xd4, 0x35,
+ 0x73, 0x20, 0xd2, 0xf8, 0xe5, 0xf4, 0x99, 0xd5, 0xc1, 0xbb, 0x94, 0x01,
+ 0x46, 0x17, 0xc4, 0x10, 0xa8, 0x0a, 0x0f, 0xea, 0xd3, 0x24, 0x4c, 0x01,
+ 0xc2, 0xd5, 0xcc, 0xa1, 0x73, 0x5e, 0xf7, 0x36, 0xd2, 0x79, 0x86, 0x88,
+ 0x3b, 0x91, 0xb9, 0x0f, 0xd3, 0xa8, 0xb3, 0x5b, 0x64, 0x02, 0xa0, 0xfb,
+ 0x15, 0x56, 0xb1, 0xac, 0x2f, 0x9d, 0xe2, 0x9e, 0x0f, 0x7e, 0xcf, 0xa6,
+ 0xc5, 0xf2, 0x58, 0xbe, 0xd1, 0x76, 0xf0, 0xec, 0x69, 0xf4, 0x91, 0xa9,
+ 0x50, 0x56, 0xa3, 0xdb, 0xba, 0x19, 0xd8, 0x9f, 0x77, 0x0f, 0x18, 0x26,
+ 0xb9, 0xfb, 0xcd, 0x4d, 0x76, 0x4c, 0xd2, 0x07, 0xc3, 0x04, 0x83, 0xc8,
+ 0xf1, 0x30, 0x79, 0x23, 0xd6, 0xd6, 0x56, 0xf6, 0x96, 0xe2, 0xa5, 0x89,
+ 0x86, 0xe0, 0x62, 0x8c, 0x02, 0x3a, 0x85, 0x5d, 0x58, 0xf5, 0xe2, 0xcc,
+ 0x6d, 0x8e, 0x9a, 0xe3, 0x6f, 0x83, 0x35, 0x4e, 0xc8, 0x2a, 0xb3, 0xb1,
+ 0xf8, 0x04, 0xae, 0xf3, 0xe0, 0xb7, 0x09, 0xce, 0x38, 0x13, 0x73, 0x8c,
+ 0x97, 0x6c, 0x90, 0xd4, 0x24, 0x53, 0xab, 0xec, 0xd5, 0x10, 0x8c, 0xc7,
+ 0x02, 0x3a, 0x77, 0x6e, 0x7e, 0x42, 0x50, 0x54, 0xf1, 0xbc, 0xd7, 0x2d,
+ 0x62, 0x7c, 0x33, 0x6d, 0x53, 0x45, 0xd2, 0x2e, 0x4e, 0x19, 0xf4, 0x5d,
+ 0xae, 0x34, 0x17, 0x85, 0x6c, 0x43, 0xd7, 0x0c, 0x91, 0x65, 0x90, 0xa5,
+ 0x5f, 0x38, 0x3d, 0xc1, 0xb0, 0xae, 0xf2, 0x50, 0x37, 0x2f, 0x8a, 0xe5,
+ 0x49, 0x66, 0x71, 0x53, 0xb7, 0x66, 0xa8, 0xb4, 0x6d, 0x87, 0x13, 0xa2,
+ 0x88, 0xc9, 0xe4, 0xb6, 0xd5, 0x0e, 0x7f, 0xad, 0xfe, 0x1a, 0xcf, 0x59,
+ 0xf3, 0xdf, 0xd9, 0xd3, 0x64, 0xd9, 0x0a, 0x3c, 0x26, 0xff, 0xa9, 0x92,
+ 0x64, 0xdc, 0xa6, 0xd5, 0xae, 0x77, 0x2f, 0x7e, 0xc1, 0x53, 0xfc, 0xff,
+ 0x75, 0xa9, 0xa5, 0x39, 0x6e, 0xe2, 0xd5, 0xff, 0xff, 0xe3, 0x43, 0xac,
+ 0x0b, 0x21, 0x26, 0xd0, 0xe1, 0xe7, 0x33, 0x59, 0xb0, 0x26, 0xed, 0xd8,
+ 0x9d, 0x67, 0x28, 0x09, 0x8a, 0x0f, 0xf0, 0x56, 0x72, 0xd5, 0x8f, 0x87,
+ 0x9b, 0x17, 0x62, 0xdf, 0x9e, 0x39, 0x11, 0x9a, 0x2f, 0x88, 0x88, 0x48,
+ 0xc4, 0xf6, 0x52, 0x11, 0xce, 0x96, 0x37, 0xaa, 0x04, 0xc1, 0x07, 0x5f,
+ 0x18, 0x77, 0x40, 0x1c, 0x75, 0xe3, 0x22, 0x65, 0x72, 0x50, 0xcc, 0xe5,
+ 0x53, 0xfe, 0x9a, 0xde, 0x7e, 0xfc, 0x52, 0x92, 0x36, 0x69, 0x5e, 0x4c,
+ 0x79, 0xd7, 0x51, 0x11, 0xec, 0x65, 0xd2, 0xab, 0x3f, 0xbe, 0x07, 0x89,
+ 0x13, 0x66, 0xc6, 0xa7, 0xfa, 0x7c, 0x99, 0x07, 0x6b, 0x05, 0xd8, 0x2d,
+ 0x1c, 0x52, 0xaa, 0x53, 0x84, 0x94, 0x62, 0x48, 0x23, 0x9a, 0xca, 0x33,
+ 0x1a, 0xd5, 0xba, 0x3e, 0xce, 0x43, 0x44, 0xb5, 0x78, 0x0e, 0x4b, 0x91,
+ 0x7e, 0xac, 0xf3, 0x8e, 0xc4, 0x16, 0x4f, 0xd1, 0x12, 0x05, 0x16, 0x60,
+ 0x35, 0x63, 0xf6, 0x21, 0x61, 0x19, 0x00, 0x2e, 0xa7, 0x84, 0x45, 0x26,
+ 0xbf, 0x98, 0x3f, 0xe8, 0x42, 0xf6, 0xf0, 0x46, 0x0e, 0xc6, 0xe0, 0xe3,
+ 0xcc, 0x22, 0x50, 0xe8, 0x76, 0x64, 0x33, 0x83, 0x8b, 0x9f, 0xad, 0xd1,
+ 0x72, 0x17, 0xff, 0x47, 0x57, 0x61, 0x67, 0x12, 0xc1, 0xda, 0x4c, 0x89,
+ 0x27, 0x1f, 0xe7, 0xf0, 0x4a, 0xe5, 0x05, 0x68, 0x51, 0x49, 0x16, 0x59,
+ 0x91, 0x6e, 0xcb, 0xfd, 0xb3, 0x03, 0xbe, 0x70, 0x7d, 0x2d, 0xfa, 0x2f,
+ 0xf0, 0xcc, 0xc3, 0x98, 0x09, 0x56, 0xa5, 0x64, 0x87, 0x0d, 0xac, 0xfe,
+ 0xbe, 0xbc, 0x16, 0xb4, 0x69, 0x0c, 0x66, 0x1e, 0x6e, 0x88, 0xc9, 0x88,
+ 0x47, 0xe0, 0xdd, 0x43, 0xe4, 0x64, 0xaf, 0x10, 0x4c, 0xac, 0x94, 0x58,
+ 0x32, 0x9c, 0x34, 0xb9, 0xa0, 0x86, 0x84, 0x7f, 0x77, 0xc5, 0x9a, 0x05,
+ 0x2d, 0x97, 0x7e, 0x69, 0xb2, 0x82, 0x9d, 0x64, 0x27, 0xc7, 0x06, 0x77,
+ 0x81, 0x32, 0x76, 0x36, 0xca, 0x05, 0x66, 0x37, 0xae, 0x3e, 0x5f, 0xff,
+ 0x09, 0xd1, 0x26, 0x24, 0x8f, 0x1a, 0x89, 0xc6, 0x36, 0xfa, 0x02, 0x11,
+ 0x59, 0x2b, 0xa1, 0x0f, 0xb9, 0x2a, 0xe6, 0xb3, 0xd4, 0x39, 0x07, 0xd8,
+ 0xc9, 0x79, 0x5b, 0x3f, 0xa0, 0xd7, 0x5d, 0x0c, 0x0d, 0x12, 0xe6, 0x58,
+ 0x2c, 0x5b, 0x6b, 0x4f, 0x45, 0x86, 0x19, 0x17, 0x72, 0x22, 0x49, 0x17,
+ 0x47, 0x4e, 0x93, 0x63, 0xa3, 0x04, 0x50, 0xa2, 0xb2, 0x8e, 0xb5, 0xc7,
+ 0x91, 0x46, 0x5e, 0xdf, 0x68, 0xb0, 0xf1, 0x8d, 0x9d, 0x4c, 0x76, 0xc3,
+ 0x5d, 0x10, 0x1f, 0x43, 0x01, 0x2e, 0x3d, 0xe6, 0xc2, 0x40, 0x6a, 0x80,
+ 0x12, 0x5e, 0x8e, 0x48, 0x6d, 0x38, 0x31, 0xf2, 0x98, 0x4b, 0xc7, 0x86,
+ 0x7d, 0xb0, 0xd3, 0x45, 0x31, 0x57, 0xce, 0x91, 0x64, 0xae, 0x62, 0x19,
+ 0xba, 0x70, 0x8a, 0x6f, 0xcc, 0xf9, 0x8b, 0x8d, 0x23, 0x5d, 0xe9, 0x13,
+ 0xa2, 0x69, 0x08, 0x67, 0x3b, 0xfd, 0x3f, 0xf6, 0x4d, 0x1d, 0x83, 0x82,
+ 0xa0, 0x32, 0x8f, 0x5b, 0x9a, 0xc6, 0x9b, 0xc7, 0x13, 0x03, 0xc2, 0x39,
+ 0xae, 0x77, 0x19, 0x89, 0xfc, 0xc5, 0xb3, 0x1f, 0xf8, 0x43, 0x46, 0x9c,
+ 0xd1, 0xe7, 0xb3, 0x72, 0x16, 0xdb, 0x6d, 0xad, 0xc9, 0xb7, 0xf3, 0xdf,
+ 0x9f, 0xb4, 0x4b, 0xc7, 0x42, 0x42, 0x49, 0xac, 0x1d, 0xc7, 0x48, 0x37,
+ 0x25, 0x81, 0x36, 0xf9, 0xe7, 0xc7, 0x9f, 0x24, 0xaf, 0x97, 0xb0, 0x92,
+ 0x89, 0x1c, 0xab, 0x5b, 0x67, 0x80, 0xf9, 0x93, 0x61, 0xff, 0x1d, 0x89,
+ 0x67, 0xc0, 0xc1, 0xeb, 0xbd, 0xd1, 0xd4, 0xc0, 0x87, 0xca, 0x49, 0x40,
+ 0xbe, 0xe6, 0xc2, 0xa2, 0x46, 0xd6, 0x4d, 0x93, 0x4b, 0x7b, 0x97, 0xb6,
+ 0x60, 0x4d, 0x9f, 0xe5, 0xed, 0xc2, 0x56, 0xc4, 0x35, 0x50, 0xc7, 0x90,
+ 0x1a, 0x23, 0x4e, 0x4a, 0x82, 0xa6, 0x20, 0x01, 0x55, 0x5c, 0x1a, 0xf2,
+ 0xe8, 0x61, 0x1d, 0x76, 0xe9, 0xde, 0x7d, 0x7f, 0x4d, 0x62, 0xb9, 0x51,
+ 0xe6, 0x05, 0x3b, 0xf7, 0x5a, 0x98, 0x6c, 0x86, 0x72, 0xfe, 0x38, 0xf9,
+ 0x38, 0x7b, 0xed, 0x26, 0xed, 0xe5, 0x1f, 0xed, 0x89, 0xf8, 0x37, 0x4d,
+ 0x27, 0x7b, 0x0d, 0x1d, 0x77, 0xc2, 0x59, 0x6a, 0xc9, 0x86, 0x46, 0xd7,
+ 0xc0, 0x9e, 0xd7, 0xaa, 0xd9, 0xe2, 0x0c, 0x07, 0xd1, 0x2f, 0xa0, 0xdb,
+ 0x15, 0xc8, 0x95, 0x8d, 0xe8, 0xaa, 0xfc, 0x7c, 0xef, 0xa5, 0xf8, 0xca,
+ 0xc0, 0x82, 0xf9, 0x1f, 0xf4, 0xda, 0x93, 0xe4, 0x2e, 0x46, 0xec, 0x85,
+ 0x1b, 0x48, 0xfb, 0x00, 0x28, 0x8f, 0xde, 0x6e, 0x86, 0xf7, 0xcd, 0x66,
+ 0x46, 0x43, 0x88, 0xd4, 0xac, 0x80, 0x79, 0x44, 0xc7, 0x92, 0xd1, 0x6f,
+ 0x64, 0xbd, 0x0f, 0x48, 0x54, 0x43, 0x06, 0xa7, 0x09, 0xd4, 0xc9, 0xe8,
+ 0xd1, 0x40, 0xaa, 0x58, 0x74, 0xb6, 0x15, 0xb9, 0x4f, 0x03, 0x87, 0x52,
+ 0x26, 0x25, 0x72, 0x78, 0xa9, 0x08, 0x3d, 0x86, 0x93, 0x8b, 0x6e, 0x3f,
+ 0x13, 0x02, 0x9c, 0xbd, 0x96, 0xbd, 0xb3, 0x38, 0xf3, 0xba, 0x4a, 0x8e,
+ 0x54, 0x4b, 0x05, 0xbb, 0xe7, 0x2f, 0x04, 0x16, 0x7a, 0x5d, 0xc7, 0x63,
+ 0xed, 0x6d, 0xc5, 0x67, 0x9c, 0x5a, 0xb3, 0x2a, 0xa4, 0x4e, 0x25, 0xae,
+ 0xa4, 0x87, 0x58, 0x3c, 0x25, 0xea, 0xdc, 0x14, 0xe4, 0x47, 0xbb, 0xed,
+ 0x80, 0x5e, 0x8c, 0x8c, 0xa8, 0x14, 0x9d, 0x60, 0xa7, 0xad, 0x93, 0x56,
+ 0x04, 0xe1, 0xf2, 0x43, 0xaa, 0xc7, 0xd5, 0xb6, 0x42, 0xf4, 0xf5, 0xf7,
+ 0x2e, 0x94, 0x97, 0x9d, 0xdb, 0x61, 0xba, 0x88, 0xa3, 0xd7, 0x80, 0x69,
+ 0x40, 0xea, 0xf2, 0x77, 0x45, 0xe0, 0xcb, 0xd7, 0xb4, 0x8a, 0x7c, 0xca,
+ 0xfe, 0x2f, 0x86, 0x5e, 0x9a, 0x21, 0xa2, 0xcf, 0x26, 0xf2, 0xa7, 0xfd,
+ 0x41, 0x7a, 0x66, 0x68, 0xcc, 0x31, 0xfb, 0x17, 0x76, 0x06, 0x06, 0xd2,
+ 0xd3, 0x90, 0xb1, 0x23, 0x03, 0x28, 0x2a, 0x54, 0x6f, 0xf5, 0xcd, 0x94,
+ 0x41, 0x30, 0x9f, 0x4b, 0x46, 0xc7, 0xbf, 0x13, 0xe6, 0x9d, 0x2d, 0x47,
+ 0xb5, 0xe1, 0x31, 0x82, 0xcb, 0x5a, 0x96, 0x76, 0x9e, 0x0d, 0x82, 0xa4,
+ 0x8f, 0x24, 0xb0, 0x7b, 0xc1, 0xc6, 0x62, 0xed, 0xfd, 0xf8, 0x35, 0xfe,
+ 0xfd, 0xc3, 0xe5, 0xa6, 0x49, 0xf0, 0xc7, 0xab, 0x15, 0x2b, 0x8c, 0x38,
+ 0x2e, 0xe7, 0x24, 0x7e, 0x01, 0x94, 0xef, 0xe1, 0x0a, 0x33, 0xfb, 0xef,
+ 0x07, 0xae, 0x2e, 0xae, 0xf6, 0xf9, 0xe9, 0x4c, 0x55, 0x22, 0x20, 0xa6,
+ 0xa9, 0xe1, 0x28, 0xad, 0xe4, 0x54, 0xe0, 0xcd, 0x76, 0x49, 0x5d, 0x1d,
+ 0x63, 0x1e, 0x9d, 0xc1, 0x52, 0x32, 0x70, 0x07, 0x50, 0xb9, 0x65, 0x40,
+ 0x31, 0xdf, 0x57, 0xa0, 0xb1, 0xf2, 0x0b, 0xaa, 0x63, 0xad, 0x7e, 0x48,
+ 0xde, 0x77, 0x66, 0x33, 0xdc, 0x0a, 0x6a, 0xe9, 0x51, 0xcf, 0xf7, 0x41,
+ 0x13, 0xf2, 0xb7, 0xab, 0x5d, 0x8f, 0x04, 0x8f, 0xbb, 0xec, 0x46, 0x1e,
+ 0xea, 0x64, 0x8d, 0x0f, 0x62, 0xb2, 0x8c, 0x4c, 0xc6, 0x29, 0x4e, 0x51,
+ 0x18, 0x40, 0xe5, 0xa5, 0xe7, 0x65, 0xd2, 0x05, 0xef, 0xe2, 0x6b, 0xed,
+ 0xe4, 0x37, 0x3a, 0x18, 0xb5, 0xbf, 0x6f, 0xff, 0x95, 0xb8, 0xc7, 0x26,
+ 0xb7, 0x04, 0xe5, 0xe1, 0xfd, 0xc0, 0xd3, 0xbb, 0x89, 0xf8, 0xe1, 0xb1,
+ 0x0c, 0x6e, 0x35, 0x57, 0xd1, 0xd9, 0xb2, 0x75, 0x80, 0x5f, 0xdf, 0x6e,
+ 0xad, 0xfa, 0xb9, 0x3d, 0x7e, 0xa7, 0xb7, 0xfa, 0xcd, 0xc9, 0x8e, 0x66,
+ 0xd7, 0xff, 0xa8, 0x6f, 0xe9, 0x5b, 0x5a, 0x30, 0x42, 0x08, 0x4a, 0xa6,
+ 0x59, 0xfa, 0xc2, 0x63, 0xf0, 0xf5, 0x18, 0x14, 0xc2, 0x24, 0x38, 0x86,
+ 0x70, 0x30, 0x4f, 0x49, 0x14, 0xfe, 0xbd, 0x88, 0x42, 0x33, 0x38, 0xf3,
+ 0x27, 0x7e, 0x41, 0x56, 0xe5, 0x6e, 0x27, 0x3b, 0x0d, 0x45, 0xb3, 0x64,
+ 0x4d, 0x1e, 0xe4, 0x9b, 0x7e, 0x82, 0x70, 0x5b, 0xa3, 0x4f, 0x70, 0x88,
+ 0x70, 0x21, 0xcd, 0x26, 0x49, 0xd1, 0xc0, 0x0d, 0x32, 0x9b, 0xf2, 0xd1,
+ 0x7c, 0x7a, 0xc5, 0x5c, 0xc4, 0xe5, 0x45, 0xe7, 0x00, 0xf4, 0x7e, 0x28,
+ 0xf4, 0xff, 0xde, 0x87, 0x6e, 0x25, 0xb7, 0x5f, 0x82, 0xa3, 0xdb, 0xad,
+ 0xcb, 0x4f, 0x92, 0x87, 0x72, 0xc2, 0x43, 0xdd, 0xfe, 0x70, 0xc7, 0x12,
+ 0xfd, 0x84, 0xad, 0x89, 0x0b, 0xcd, 0x19, 0xd9, 0xf5, 0x73, 0xad, 0x18,
+ 0x64, 0x68, 0xad, 0x33, 0xaa, 0x98, 0xb4, 0x03, 0x8b, 0x05, 0x3c, 0xbf,
+ 0x87, 0x1b, 0xa6, 0xdd, 0x83, 0x84, 0xfa, 0xf4, 0x40, 0x5c, 0x8f, 0xd0,
+ 0xe1, 0xaf, 0xe6, 0xcd, 0xfe, 0x68, 0x65, 0x9a, 0x66, 0x23, 0x03, 0x8f,
+ 0x37, 0x23, 0xf6, 0x89, 0x36, 0x70, 0x17, 0x82, 0x11, 0x88, 0x5f, 0x31,
+ 0xb2, 0x56, 0xbe, 0x32, 0x83, 0xf8, 0xaa, 0x89, 0xfa, 0xb1, 0x86, 0xc6,
+ 0x22, 0x05, 0x60, 0x45, 0x22, 0x3a, 0x98, 0xde, 0x45, 0xca, 0x7e, 0x83,
+ 0xe4, 0x43, 0xb9, 0x36, 0xf7, 0x52, 0xdb, 0x1b, 0x1b, 0xe6, 0xb4, 0x72,
+ 0x4f, 0x56, 0x48, 0xff, 0x7d, 0x6c, 0x7d, 0x0f, 0x40, 0xd4, 0x6f, 0xd1,
+ 0xd8, 0x41, 0x64, 0x1e, 0xe0, 0x76, 0x4e, 0x68, 0x58, 0x02, 0x3d, 0xaa,
+ 0x90, 0xc1, 0xda, 0x5d, 0xa8, 0x7e, 0xb7, 0x95, 0x8c, 0xd0, 0x88, 0x38,
+ 0xa8, 0x21, 0xe5, 0xc2, 0x46, 0x3e, 0x37, 0xbd, 0x8e, 0xc8, 0x74, 0x98,
+ 0xed, 0x6c, 0x88, 0x7f, 0xfd, 0xeb, 0x62, 0x2e, 0xf2, 0x6a, 0xd4, 0xeb,
+ 0xf2, 0xf0, 0x66, 0x21, 0x72, 0x4a, 0x24, 0x32, 0xed, 0x00, 0x0c, 0x56,
+ 0xfa, 0x49, 0x9d, 0xc7, 0x08, 0x22, 0xc9, 0x52, 0xdc, 0xf0, 0x26, 0xc1,
+ 0x69, 0x58, 0x2a, 0xbe, 0x27, 0x5d, 0x5e, 0x44, 0xb3, 0x85, 0x0f, 0x68,
+ 0x25, 0xc4, 0x59, 0x8a, 0x6d, 0xca, 0xb1, 0xec, 0x69, 0x6d, 0x52, 0xd1,
+ 0x54, 0x61, 0x8c, 0xff, 0xe9, 0xf7, 0x62, 0x9b, 0x8b, 0xe6, 0xe6, 0xc9,
+ 0x43, 0xa9, 0xc0, 0xad, 0x3a, 0xfa, 0x3a, 0x41, 0x1a, 0xed, 0xa4, 0xc8,
+ 0x42, 0xeb, 0xba, 0xbb, 0xec, 0xde, 0x72, 0xf5, 0x55, 0x78, 0xff, 0x8d,
+ 0xb9, 0x8a, 0x62, 0xa3, 0xfb, 0x94, 0x23, 0xdb, 0x90, 0xf2, 0x57, 0x28,
+ 0x7b, 0x17, 0x51, 0xb3, 0xdd, 0x8b, 0x4f, 0x6d, 0x5a, 0xeb, 0x21, 0x59,
+ 0x50, 0x5f, 0x71, 0xe5, 0x95, 0xd6, 0x03, 0xf5, 0xad, 0x1b, 0x7b, 0xbb,
+ 0xe1, 0xc4, 0xae, 0x2e, 0x71, 0x15, 0x6c, 0x66, 0xd0, 0xfb, 0x01, 0x35,
+ 0x9c, 0x64, 0xae, 0x55, 0xc8, 0x82, 0xb5, 0x6d, 0x83, 0x2b, 0x5d, 0xf6,
+ 0xde, 0x79, 0x5d, 0x93, 0xb1, 0xfe, 0x2e, 0x60, 0x4d, 0x46, 0xd3, 0xd9,
+ 0xcc, 0xd2, 0x30, 0x7c, 0xec, 0xd4, 0xcb, 0x75, 0x9f, 0x78, 0xfd, 0x44,
+ 0xd9, 0x1d, 0x58, 0xac, 0xef, 0x02, 0x28, 0xaa, 0x8d, 0xbe, 0x3c, 0xec,
+ 0x51, 0x7e, 0x44, 0x40, 0xd7, 0xec, 0x52, 0x25, 0xf1, 0x99, 0x53, 0x65,
+ 0xfb, 0xbc, 0x10, 0x75, 0xa4, 0x27, 0x1b, 0x65, 0xdb, 0xe9, 0x8d, 0x90,
+ 0xa5, 0x14, 0xdc, 0x38, 0x31, 0xf1, 0x06, 0xf0, 0xa8, 0xea, 0xd6, 0xfb,
+ 0x7e, 0xb0, 0x18, 0x0d, 0x06, 0xbe, 0x92, 0xbc, 0xa0, 0x21, 0x9b, 0xfa,
+ 0x72, 0x4f, 0x0a, 0x4c, 0xa7, 0x8b, 0xd0, 0x39, 0xf1, 0xc3, 0x7b, 0x40,
+ 0x22, 0x5a, 0x11, 0x96, 0xe5, 0x50, 0xb5, 0xb1, 0x4e, 0xdf, 0x95, 0x03,
+ 0xe1, 0x6e, 0x16, 0x9d, 0x1f, 0xb8, 0xb7, 0x4c, 0x93, 0x3c, 0x8c, 0x19,
+ 0xbd, 0x57, 0x8f, 0x7d, 0xa5, 0x4d, 0x04, 0x1f, 0x11, 0x22, 0xd5, 0x3b,
+ 0xb6, 0x8f, 0xbd, 0x6d, 0x21, 0x89, 0xa3, 0xc9, 0xc5, 0xf6, 0x09, 0xf1,
+ 0x4a, 0x1f, 0xe9, 0xff, 0x96, 0x8e, 0x71, 0x28, 0x95, 0x68, 0xcb, 0xd5,
+ 0xd9, 0x3c, 0xe0, 0xa6, 0xc4, 0x49, 0xf4, 0x84, 0xa7, 0x57, 0x69, 0x73,
+ 0xc5, 0xd9, 0xc9, 0x14, 0x05, 0xa9, 0x06, 0xa5, 0x95, 0x21, 0xb2, 0x69,
+ 0x1b, 0x25, 0xbe, 0x20, 0x56, 0xf0, 0xdd, 0x2d, 0xed, 0xea, 0xaf, 0xc2,
+ 0x79, 0xb1, 0xea, 0xeb, 0xed, 0x27, 0x87, 0xe6, 0xc3, 0x4e, 0x5b, 0x08,
+ 0xde, 0x44, 0xb3, 0x01, 0xf9, 0x71, 0x1d, 0x3e, 0x2d, 0x1a, 0xc6, 0xfe,
+ 0x87, 0xe9, 0xa4, 0xea, 0x4c, 0xc7, 0x83, 0x80, 0xae, 0x03, 0x9a, 0x43,
+ 0x2f, 0x26, 0xa3, 0x86, 0xa0, 0x8d, 0xc2, 0xf4, 0x26, 0x24, 0x31, 0xab,
+ 0x5b, 0xc6, 0x0d, 0xf6, 0xc6, 0x07, 0x56, 0x9a, 0x69, 0x04, 0xc4, 0x50,
+ 0x0d, 0x7e, 0x25, 0x18, 0x88, 0x8c, 0x01, 0x98, 0xd4, 0x07, 0x72, 0xa4,
+ 0x26, 0xe1, 0x70, 0x26, 0xc1, 0x74, 0x74, 0x9f, 0xda, 0xc3, 0x40, 0x48,
+ 0xc2, 0x03, 0x8e, 0x05, 0x2c, 0x9d, 0x27, 0xbf, 0x50, 0x28, 0x77, 0xc3,
+ 0xd8, 0x9f, 0xeb, 0x0a, 0x3e, 0x9e, 0xe8, 0x88, 0x8e, 0x14, 0x98, 0xff,
+ 0xbd, 0x50, 0x3e, 0x5d, 0xd2, 0x3a, 0x70, 0x95, 0xe8, 0x1e, 0x77, 0x5f,
+ 0x45, 0xd8, 0x57, 0x00, 0xcc, 0xa2, 0x9f, 0x1f, 0xcd, 0xec, 0xa8, 0x0b,
+ 0x1a, 0x25, 0x7f, 0x2c, 0x72, 0x8b, 0x05, 0xce, 0xc0, 0x56, 0x0e, 0xcc,
+ 0x8e, 0x98, 0x5c, 0xbd, 0xf4, 0x64, 0xf8, 0x14, 0xf3, 0x1e, 0x6a, 0xd5,
+ 0xa1, 0x3a, 0x35, 0x06, 0xae, 0x3b, 0x25, 0x4c, 0xf5, 0x76, 0x55, 0xbe,
+ 0xde, 0x0a, 0xe3, 0xa0, 0x87, 0xcf, 0xfd, 0xfc, 0xdf, 0x3a, 0x83, 0x7f,
+ 0x0f, 0x77, 0x1c, 0x33, 0x46, 0x0a, 0x3b, 0x17, 0x72, 0x4c, 0x9b, 0x1f,
+ 0xb6, 0xd7, 0x72, 0x00, 0x21, 0x4d, 0x17, 0x3c, 0xd8, 0x1e, 0x22, 0xf1,
+ 0xcd, 0xb5, 0x7d, 0xf9, 0xc1, 0xa5, 0xba, 0x77, 0x2a, 0x0a, 0xd2, 0x9b,
+ 0x6a, 0x4d, 0x6a, 0x23, 0x7f, 0xc7, 0xe1, 0xff, 0x31, 0x69, 0x3b, 0xb8,
+ 0x9a, 0xb5, 0x53, 0x83, 0xa5, 0xa1, 0xc0, 0xd7, 0x8a, 0x07, 0x9a, 0x47,
+ 0xd3, 0x87, 0x8c, 0x66, 0x9c, 0xa3, 0x08, 0xa4, 0x48, 0xde, 0xdf, 0xf8,
+ 0x6e, 0xc8, 0xca, 0x3f, 0xcb, 0x10, 0x7d, 0x4d, 0xa8, 0x53, 0x7b, 0xfa,
+ 0xe5, 0xac, 0x0a, 0x98, 0xf1, 0x53, 0x1f, 0x3d, 0x19, 0x49, 0x70, 0x4a,
+ 0x76, 0x6d, 0xb7, 0x9d, 0x40, 0x1f, 0x80, 0x98, 0x91, 0xa7, 0xa9, 0x13,
+ 0xe7, 0x54, 0xa7, 0xc4, 0xa1, 0x00, 0x4c, 0xa4, 0xa2, 0x54, 0xcf, 0xff,
+ 0xb9, 0x54, 0xcd, 0x43, 0xcd, 0xe0, 0x4c, 0x90, 0xc2, 0x9a, 0xe4, 0xbc,
+ 0x6c, 0xc9, 0x1e, 0x86, 0x35, 0x4a, 0x5e, 0xce, 0x18, 0x6c, 0x63, 0xd7,
+ 0x93, 0x0c, 0x0d, 0xc2, 0xfb, 0x08, 0xbc, 0x37, 0x9f, 0xe9, 0x3f, 0x03,
+ 0x9e, 0x47, 0xbc, 0x87, 0xa2, 0x16, 0xb8, 0xc4, 0x1e, 0x8c, 0x80, 0xe7,
+ 0xfb, 0xc2, 0x24, 0xbd, 0x3e, 0xc2, 0x51, 0xac, 0x54, 0x92, 0x89, 0x04,
+ 0x0d, 0xfb, 0xf2, 0x5f, 0xf0, 0x52, 0xa2, 0xf9, 0xf5, 0xcd, 0x38, 0x4d,
+ 0xb9, 0xec, 0x60, 0x02, 0x82, 0xe1, 0x5c, 0x97, 0xa5, 0x2b, 0x66, 0x91,
+ 0x23, 0xab, 0x10, 0x40, 0x8d, 0xcd, 0x43, 0x95, 0xa2, 0xef, 0x8c, 0xe3,
+ 0x45, 0x02, 0x4d, 0xc8, 0x51, 0xa2, 0x8f, 0x90, 0x82, 0x05, 0x00, 0xd5,
+ 0x58, 0xad, 0x1b, 0x72, 0xc8, 0x20, 0x82, 0x72, 0x0f, 0x51, 0xc0, 0x50,
+ 0x2c, 0x71, 0x5a, 0x20, 0xc3, 0x20, 0xab, 0x72, 0x2d, 0x35, 0x51, 0x3b,
+ 0xb3, 0x37, 0x27, 0x35, 0xa8, 0x93, 0x89, 0xf5, 0x5c, 0xbc, 0x16, 0xe9,
+ 0x69, 0x3f, 0xdb, 0x15, 0xa0, 0xc7, 0x9a, 0xbc, 0xf6, 0xfb, 0xe6, 0x23,
+ 0x5d, 0xb6, 0xf3, 0xc7, 0xdb, 0x30, 0x4b, 0x5a, 0x06, 0x85, 0x82, 0x4e,
+ 0x5a, 0xce, 0xb3, 0xa6, 0x49, 0xac, 0x83, 0x6f, 0x03, 0x27, 0x1f, 0x75,
+ 0xde, 0x8c, 0xa1, 0xd3, 0x5b, 0x2a, 0xb9, 0x91, 0xbf, 0xe3, 0x17, 0x8c,
+ 0x23, 0x49, 0xbc, 0x6a, 0xdf, 0xc5, 0x14, 0x93, 0x26, 0x63, 0x67, 0xd3,
+ 0x62, 0x3b, 0xc4, 0x93, 0xda, 0xeb, 0xf6, 0x77, 0xed, 0x97, 0x76, 0x5a,
+ 0x89, 0x23, 0x00, 0xc1, 0x04, 0xcf, 0xb9, 0x36, 0x55, 0x13, 0x6d, 0x7e,
+ 0x72, 0x64, 0xe6, 0x45, 0x26, 0x35, 0xdc, 0x00, 0x99, 0x67, 0xb9, 0x97,
+ 0x74, 0xe4, 0x9a, 0x68, 0x5a, 0xcc, 0xa0, 0x25, 0xd1, 0x6c, 0x11, 0xdb,
+ 0x4c, 0x82, 0x6b, 0x33, 0xda, 0xf9, 0xd5, 0xcf, 0x35, 0x60, 0x7e, 0xd1,
+ 0x1a, 0xa9, 0x08, 0xaf, 0xea, 0x62, 0x3d, 0xfd, 0x5d, 0x06, 0x8e, 0xbb,
+ 0x03, 0x44, 0x5c, 0x12, 0xbe, 0x35, 0xa2, 0xd8, 0x09, 0x51, 0xfd, 0x57,
+ 0x14, 0x89, 0xc4, 0x9f, 0x36, 0x89, 0xdd, 0x6e, 0x68, 0xab, 0xa1, 0x3f,
+ 0x77, 0x0f, 0x8e, 0xa6, 0xbd, 0x95, 0xba, 0xd4, 0x33, 0x1f, 0x78, 0x2f,
+ 0x48, 0x27, 0x9a, 0xe8, 0x82, 0x46, 0x37, 0x2e, 0x17, 0x59, 0xdd, 0xe7,
+ 0x31, 0x4f, 0x26, 0x41, 0x68, 0xaa, 0x1f, 0x55, 0x9d, 0x4e, 0x39, 0x83,
+ 0x03, 0x9b, 0x70, 0x0f, 0x7f, 0x67, 0x2a, 0x3f, 0xef, 0xc5, 0xdf, 0xfe,
+ 0x24, 0x49, 0x03, 0x86, 0x31, 0x79, 0x54, 0xfa, 0x9f, 0x73, 0xe2, 0x36,
+ 0x2c, 0x2b, 0xdb, 0x10, 0xaf, 0x95, 0xe0, 0x50, 0x51, 0x47, 0x6f, 0xea,
+ 0x3b, 0x77, 0x8b, 0x35, 0x8c, 0x4b, 0xd9, 0x98, 0x09, 0x6a, 0x4a, 0x92,
+ 0x75, 0x18, 0xe2, 0x94, 0x88, 0xcd, 0x31, 0xde, 0x07, 0xdb, 0xe4, 0x5f,
+ 0x15, 0x32, 0x7a, 0xc9, 0xac, 0xbf, 0x26, 0x45, 0x7f, 0xde, 0x54, 0x0c,
+ 0x95, 0xd3, 0xce, 0x7e, 0x6c, 0x09, 0x51, 0x7d, 0x1e, 0xbd, 0x40, 0xd6,
+ 0x05, 0x8d, 0xb4, 0xc2, 0xfc, 0xb8, 0x53, 0x54, 0xe3, 0x8c, 0x6d, 0xfa,
+ 0x88, 0xbf, 0x46, 0x8f, 0x2e, 0x5b, 0x78, 0x74, 0x38, 0x4c, 0xa9, 0x1e,
+ 0x0c, 0x8e, 0x51, 0xe5, 0x55, 0x6f, 0xa6, 0xb7, 0x99, 0xee, 0xf2, 0xa2,
+ 0x38, 0x62, 0xb9, 0xdd, 0xc1, 0x59, 0xe7, 0x73, 0xf9, 0x6c, 0x36, 0xe4,
+ 0xd0, 0x5d, 0x29, 0xa5, 0xfb, 0xec, 0x77, 0x79, 0xd0, 0x48, 0x03, 0x61,
+ 0x81, 0x1b, 0x9a, 0x91, 0xa9, 0x71, 0x27, 0x96, 0x67, 0x85, 0xa6, 0xdc,
+ 0xef, 0x1b, 0xc7, 0xe1, 0x80, 0x9d, 0x49, 0xfe, 0x2f, 0x08, 0xe4, 0x25,
+ 0x1d, 0x44, 0x78, 0x1e, 0xc9, 0xba, 0xfb, 0x79, 0xd9, 0x5d, 0x36, 0xd5,
+ 0xfa, 0x40, 0xb3, 0x6f, 0x2e, 0x91, 0x5a, 0xc5, 0x4a, 0x02, 0x30, 0x82,
+ 0xf0, 0x87, 0x0b, 0xfb, 0x62, 0x5c, 0x12, 0x0a, 0xd4, 0x7c, 0xe8, 0xac,
+ 0xae, 0xf7, 0x21, 0x33, 0xeb, 0xc0, 0x9a, 0x25, 0xeb, 0xf4, 0x6a, 0x48,
+ 0xc9, 0xd2, 0xd4, 0x8f, 0xa5, 0x42, 0xf8, 0x23, 0xdf, 0x89, 0xd9, 0xee,
+ 0x4b, 0xaf, 0xea, 0xc9, 0x3a, 0xe1, 0xc5, 0x62, 0xe4, 0x9c, 0x14, 0x9b,
+ 0xe7, 0xb1, 0xf6, 0x2d, 0xa0, 0x87, 0x11, 0xb7, 0xe5, 0x7a, 0x98, 0x03,
+ 0x17, 0xbc, 0x62, 0x45, 0xcb, 0x42, 0x6c, 0xa4, 0x06, 0xb7, 0x5c, 0x86,
+ 0xa6, 0xc5, 0xaf, 0x02, 0xea, 0xac, 0xfb, 0x6e, 0x0a, 0xa2, 0x3b, 0x8d,
+ 0x05, 0x5e, 0x89, 0x42, 0x83, 0xc3, 0x2f, 0xc7, 0x9c, 0x4e, 0x9d, 0x4c,
+ 0xe4, 0x4e, 0x3d, 0x59, 0x4c, 0xe2, 0xde, 0x27, 0x13, 0x0d, 0x96, 0xb2,
+ 0xae, 0xfc, 0xba, 0xd3, 0xf4, 0x7a, 0x2d, 0xc9, 0xde, 0x91, 0x58, 0xcf,
+ 0xe8, 0xed, 0x78, 0x74, 0x62, 0x16, 0x83, 0xca, 0x6d, 0xa6, 0xc8, 0x74,
+ 0x24, 0x92, 0xbe, 0x58, 0xa7, 0x06, 0x58, 0x2d, 0x10, 0x44, 0xcd, 0x22,
+ 0xe3, 0xa9, 0xf9, 0x3a, 0x12, 0x2b, 0x9d, 0x0f, 0xa1, 0xce, 0xaf, 0xd5,
+ 0xf3, 0x66, 0x7b, 0xcc, 0x66, 0xec, 0x82, 0x8b, 0x92, 0x4f, 0x3f, 0x8c,
+ 0xf3, 0x4f, 0x7d, 0x2c, 0x05, 0xf3, 0x80, 0x01, 0xe7, 0x00, 0xf2, 0xf5,
+ 0xbd, 0xcd, 0x3b, 0x23, 0xb5, 0x8b, 0x4c, 0x68, 0xa2, 0x81, 0xaa, 0xc7,
+ 0xf6, 0x11, 0xf8, 0x0b, 0x55, 0xaf, 0x69, 0xf1, 0x18, 0x78, 0x63, 0x3d,
+ 0xcd, 0x58, 0x3e, 0xc5, 0x7e, 0x8f, 0xf2, 0xed, 0x5f, 0xf3, 0xe2, 0x9c,
+ 0x94, 0xb6, 0x64, 0x9c, 0x0c, 0x8d, 0xc7, 0xfa, 0xc9, 0x1f, 0x64, 0xdf,
+ 0x12, 0x2c, 0x60, 0xa6, 0x4d, 0x94, 0xa9, 0x01, 0x48, 0x40, 0xb4, 0xb9,
+ 0x43, 0x65, 0xcf, 0x6d, 0x25, 0x1f, 0xf0, 0xa1, 0x05, 0xc8, 0x58, 0x1d,
+ 0x93, 0xac, 0x16, 0x2e, 0x83, 0x04, 0x39, 0xd5, 0xd2, 0xb6, 0xdf, 0xa7,
+ 0x2b, 0x5b, 0x16, 0x2c, 0xed, 0x1c, 0x7b, 0x6d, 0x9f, 0x6b, 0xc7, 0xbf,
+ 0xf4, 0x00, 0xa1, 0x1b, 0x5d, 0x81, 0xbf, 0x27, 0xdc, 0xa5, 0x23, 0x76,
+ 0xbc, 0xe9, 0xd3, 0xf6, 0x7b, 0x9f, 0xb4, 0x6f, 0x5f, 0x39, 0x08, 0xec,
+ 0xfd, 0xf4, 0x7a, 0x63, 0x66, 0x79, 0x0f, 0x0d, 0xa2, 0x46, 0x7f, 0x94,
+ 0x58, 0x0d, 0xf3, 0x59, 0x95, 0x00, 0x9a, 0x01, 0xf9, 0x3f, 0x44, 0x64,
+ 0x04, 0xf0, 0xb1, 0xfe, 0x6f, 0x00, 0xa7, 0x26, 0x9a, 0x6b, 0x79, 0x01,
+ 0x82, 0x25, 0x1f, 0xc2, 0x21, 0x52, 0x13, 0x0f, 0x07, 0x15, 0xe9, 0x8c,
+ 0x79, 0xeb, 0xb0, 0x44, 0x68, 0x5c, 0x18, 0x3f, 0x47, 0xeb, 0x7e, 0x37,
+ 0xe2, 0x1b, 0x4e, 0xef, 0x75, 0x60, 0xfb, 0xa3, 0x6a, 0x50, 0xa6, 0xcf,
+ 0x2e, 0x44, 0x27, 0x45, 0x8c, 0xfe, 0x3a, 0xc6, 0x9f, 0xc4, 0x57, 0x80,
+ 0x31, 0xbb, 0x64, 0x08, 0x66, 0xc7, 0x35, 0xe5, 0xd2, 0x86, 0xd7, 0x4e,
+ 0xd8, 0x34, 0xd6, 0x2f, 0x1d, 0x1f, 0xe8, 0x1b, 0x79, 0xc0, 0xa0, 0x3c,
+ 0xb6, 0x5f, 0x9f, 0xd1, 0x42, 0xd2, 0x20, 0x36, 0xe1, 0x50, 0x10, 0x18,
+ 0xc1, 0xe9, 0xb0, 0xcc, 0x71, 0x14, 0x8a, 0x8e, 0x40, 0x37, 0xa5, 0x0b,
+ 0x6d, 0x6c, 0xfc, 0x98, 0x3c, 0xf6, 0x8f, 0x0f, 0xe8, 0x18, 0x3f, 0xaa,
+ 0x5b, 0xe6, 0x83, 0x84, 0xe0, 0x68, 0xa7, 0x4b, 0xb1, 0xbc, 0x3e, 0xa5,
+ 0xd5, 0x14, 0x6a, 0xdf, 0x63, 0x78, 0x15, 0x52, 0x5c, 0x1c, 0x74, 0xd4,
+ 0x90, 0xdd, 0x36, 0xe1, 0x4e, 0xa0, 0x42, 0x4b, 0x67, 0x67, 0x5d, 0xf4,
+ 0xb0, 0xb0, 0x4f, 0x9e, 0xf0, 0x6c, 0x68, 0xf8, 0x5a, 0x4a, 0xb1, 0xec,
+ 0x5c, 0x7b, 0x19, 0x8b, 0xbd, 0x39, 0x42, 0x74, 0x4f, 0xf5, 0x17, 0xfd,
+ 0xa4, 0xaa, 0xaf, 0xf8, 0x01, 0xdc, 0x40, 0xd2, 0x0f, 0x83, 0x0d, 0xe6,
+ 0x1e, 0xb8, 0xfd, 0xb7, 0x2c, 0xc9, 0x3a, 0x96, 0xe7, 0xee, 0xc5, 0x8e,
+ 0x8c, 0x89, 0x18, 0xb7, 0x32, 0x2a, 0x96, 0x9b, 0xc4, 0xc4, 0x93, 0xa1,
+ 0xbc, 0x2e, 0x17, 0x3f, 0x7f, 0xa3, 0xaf, 0x0e, 0xb5, 0x7f, 0xbc, 0x32,
+ 0x62, 0x50, 0x86, 0x02, 0x77, 0x25, 0x3f, 0x41, 0x82, 0x02, 0x07, 0xc9,
+ 0x43, 0xb2, 0xf4, 0x32, 0x65, 0xaa, 0x35, 0x36, 0x34, 0x90, 0xda, 0x1e,
+ 0x34, 0x16, 0x4f, 0xd0, 0x3e, 0x96, 0x76, 0x11, 0xd5, 0x64, 0xaf, 0x5a,
+ 0xaf, 0xbc, 0xa4, 0x2a, 0xd4, 0xf3, 0xba, 0x9d, 0x8b, 0x7d, 0x7e, 0xe5,
+ 0x2f, 0x59, 0xc5, 0x5a, 0x9a, 0x70, 0x00, 0xf5, 0xea, 0xec, 0x28, 0x55,
+ 0xd4, 0x4c, 0x25, 0x05, 0xa9, 0x65, 0x0e, 0x7e, 0x12, 0xc4, 0xb7, 0xa9,
+ 0xec, 0xf6, 0x7d, 0x92, 0x4e, 0x43, 0x67, 0x24, 0x8f, 0xe4, 0x1d, 0x5d,
+ 0x12, 0x6b, 0x76, 0xa4, 0x42, 0xba, 0x44, 0xfe, 0xa4, 0x8a, 0xf9, 0xf3,
+ 0x9c, 0xdc, 0x4b, 0x0c, 0x00, 0x41, 0x21, 0x19, 0xde, 0x7c, 0x56, 0xf1,
+ 0x6a, 0xa0, 0xed, 0xfe, 0xfb, 0x8e, 0x3f, 0x06, 0x85, 0x3f, 0x3e, 0x88,
+ 0xf0, 0x67, 0x6b, 0x17, 0xc7, 0xe1, 0x5f, 0x86, 0x03, 0x6e, 0x49, 0x46,
+ 0xbd, 0xec, 0xce, 0x0c, 0x1b, 0x05, 0x5c, 0x03, 0x85, 0xfd, 0xb5, 0xa1,
+ 0x1b, 0xfe, 0x19, 0x7b, 0xc3, 0x1f, 0xf3, 0xf0, 0xaf, 0xf8, 0xa8, 0xb2,
+ 0x15, 0x39, 0xdf, 0x89, 0x4a, 0x79, 0x00, 0x87, 0xa8, 0x6f, 0x2f, 0x05,
+ 0xd7, 0xa6, 0xd4, 0x4a, 0x9a, 0xf8, 0x28, 0x56, 0x36, 0x55, 0xd2, 0x41,
+ 0x54, 0xd9, 0x88, 0xf7, 0xe7, 0xdf, 0x7c, 0x4d, 0x37, 0xdc, 0x28, 0xe2,
+ 0x5a, 0x4a, 0x61, 0x5f, 0x66, 0x14, 0x53, 0x30, 0x5e, 0x90, 0x2d, 0xcc,
+ 0xa0, 0xe8, 0x63, 0xb6, 0x2b, 0x89, 0x97, 0x35, 0x93, 0xc2, 0xcd, 0xd9,
+ 0x0c, 0x7b, 0x95, 0x7f, 0x37, 0x45, 0x4f, 0x3b, 0x60, 0x48, 0x6f, 0x82,
+ 0x2d, 0x77, 0xeb, 0xfe, 0x7a, 0x2d, 0xc4, 0x42, 0x90, 0xc0, 0x62, 0x51,
+ 0x99, 0x5a, 0x7d, 0xff, 0x76, 0xbd, 0x60, 0x92, 0xb7, 0x59, 0xf8, 0xea,
+ 0x0c, 0x76, 0xf6, 0x67, 0x3c, 0x27, 0x39, 0x99, 0x8d, 0xf2, 0x4a, 0x30,
+ 0x39, 0x47, 0x04, 0x8b, 0x17, 0xe0, 0xfa, 0xfb, 0x20, 0xda, 0x9f, 0x36,
+ 0x61, 0x8c, 0xd7, 0x59, 0x08, 0xc4, 0x2f, 0xdb, 0x05, 0x1b, 0x25, 0x0c,
+ 0x6e, 0xaf, 0x78, 0x09, 0x7a, 0x64, 0x04, 0x98, 0x3e, 0x91, 0xa5, 0xcc,
+ 0x20, 0xcf, 0x47, 0xe1, 0x26, 0x8f, 0x9f, 0x10, 0x54, 0xc6, 0x71, 0x59,
+ 0x56, 0x5a, 0x07, 0xe0, 0xb2, 0x0a, 0x5e, 0xa0, 0x88, 0x34, 0x08, 0x31,
+ 0x0d, 0x37, 0x89, 0x70, 0x44, 0x92, 0x9f, 0xf3, 0x06, 0x1a, 0x15, 0x65,
+ 0x1c, 0xe2, 0x2d, 0xd3, 0xd9, 0x90, 0x3a, 0x28, 0x5d, 0x1d, 0x1f, 0x16,
+ 0x79, 0x91, 0xe1, 0xed, 0x97, 0x2b, 0x7c, 0x0d, 0xf8, 0xea, 0x1a, 0xf5,
+ 0x8d, 0xab, 0x4f, 0xdd, 0x09, 0x94, 0xea, 0xea, 0x8c, 0xa5, 0xa2, 0x77,
+ 0xd5, 0x15, 0xa7, 0x00, 0x68, 0xbc, 0x7d, 0x07, 0xdd, 0x17, 0x7e, 0x03,
+ 0xf9, 0x29, 0x5b, 0xaa, 0xa5, 0x5b, 0x91, 0x7f, 0xec, 0x3e, 0x75, 0xe7,
+ 0x4f, 0xd3, 0xdf, 0xa4, 0x69, 0x9f, 0xd0, 0x6d, 0x84, 0xdd, 0x32, 0x70,
+ 0x7c, 0xc4, 0x05, 0xa3, 0x74, 0xcd, 0x27, 0x3e, 0x6d, 0x07, 0x3a, 0x9f,
+ 0x96, 0xb4, 0x8a, 0xf9, 0xf1, 0x37, 0x30, 0x8d, 0x85, 0x0f, 0x35, 0x3b,
+ 0xc3, 0x22, 0xb4, 0xb9, 0x9f, 0x0a, 0x50, 0xa7, 0x2a, 0x27, 0xdc, 0xd8,
+ 0x67, 0xab, 0xa0, 0x16, 0xb2, 0xf2, 0xa6, 0x97, 0x49, 0xd9, 0x02, 0xd9,
+ 0x99, 0xae, 0x4c, 0x2e, 0x19, 0x59, 0xf2, 0x4c, 0x68, 0xc1, 0xf5, 0xc5,
+ 0x07, 0xa2, 0x09, 0xbb, 0xd4, 0x11, 0xa6, 0xbf, 0x90, 0xb0, 0xa0, 0xbd,
+ 0x41, 0x0e, 0x92, 0x97, 0x7b, 0x33, 0xfe, 0x9d, 0x1d, 0x14, 0x9f, 0xbe,
+ 0xfc, 0xb1, 0xf5, 0x3e, 0x95, 0x09, 0xda, 0x62, 0x76, 0x9a, 0x1c, 0x4b,
+ 0x55, 0x6f, 0xc5, 0x31, 0xb5, 0xb4, 0xcc, 0x85, 0x36, 0x62, 0x5f, 0xa7,
+ 0x4e, 0x57, 0x99, 0xd7, 0xb9, 0x2d, 0x3c, 0xc9, 0xbe, 0xd2, 0x6a, 0xd9,
+ 0xdb, 0xeb, 0xb4, 0x3c, 0xc0, 0xea, 0xee, 0x97, 0x43, 0xa4, 0x4e, 0xd8,
+ 0xce, 0xe3, 0xc0, 0xc3, 0xe2, 0x58, 0x1c, 0xc6, 0x7a, 0xde, 0xfc, 0x87,
+ 0xca, 0xc7, 0x13, 0x51, 0xce, 0x6b, 0x15, 0xcf, 0x1c, 0x6d, 0x3e, 0x36,
+ 0xb0, 0x0f, 0x0d, 0xdc, 0x4f, 0x51, 0xc6, 0x65, 0xbb, 0x28, 0x8e, 0xea,
+ 0x62, 0x8c, 0xf9, 0x79, 0x5e, 0x51, 0xd4, 0x6c, 0x77, 0xb2, 0x25, 0x9b,
+ 0xb8, 0xcc, 0xc3, 0x23, 0x04, 0x36, 0x81, 0xfd, 0xef, 0x80, 0xe4, 0x7e,
+ 0x92, 0xe6, 0xee, 0x1b, 0x3c, 0xcd, 0xf3, 0x68, 0xec, 0xca, 0x1e, 0x6d,
+ 0xb3, 0xd1, 0x40, 0x5a, 0xc8, 0xb6, 0x81, 0x60, 0x6a, 0x32, 0xa9, 0x11,
+ 0x93, 0xf7, 0xff, 0xa0, 0xab, 0x72, 0x26, 0x27, 0x11, 0xbf, 0x2c, 0xb4,
+ 0x88, 0x1c, 0x2b, 0x40, 0x4e, 0x9d, 0x88, 0x9b, 0x3b, 0x04, 0xdb, 0x3d,
+ 0x13, 0x9f, 0xbf, 0x10, 0x81, 0xa1, 0xe2, 0x0f, 0x1c, 0x22, 0xce, 0x64,
+ 0x2e, 0x53, 0xe0, 0xc7, 0x76, 0xd5, 0xf5, 0x52, 0x01, 0x78, 0x46, 0x94,
+ 0x50, 0x54, 0xc9, 0x68, 0xe6, 0x78, 0xa1, 0xd8, 0xff, 0xe6, 0xa0, 0x50,
+ 0xb6, 0x26, 0xce, 0x28, 0xb7, 0x02, 0xa5, 0x6d, 0x39, 0xd8, 0xac, 0xb7,
+ 0x1f, 0x55, 0xb6, 0xa4, 0x91, 0xdd, 0x67, 0x1e, 0x31, 0xd7, 0x2d, 0xcc,
+ 0xe3, 0x8f, 0x31, 0x9b, 0x39, 0xdc, 0x8e, 0xf3, 0xc7, 0xd4, 0x58, 0x42,
+ 0x93, 0x92, 0xd8, 0x3a, 0xce, 0x7b, 0xdc, 0x2c, 0xfb, 0x37, 0xf8, 0xff,
+ 0x7f, 0xc0, 0xa8, 0x6a, 0xc2, 0xc5, 0xd3, 0x7a, 0x76, 0x46, 0xd1, 0xa7,
+ 0x82, 0x0a, 0x27, 0xb0, 0xf7, 0xaf, 0xa5, 0x6b, 0xbe, 0xfa, 0x58, 0xd9,
+ 0x16, 0x1c, 0x8b, 0x3e, 0x7b, 0x7e, 0x9b, 0x61, 0x1c, 0x9f, 0x5d, 0xd1,
+ 0xd2, 0x44, 0x5f, 0x7b, 0x35, 0x42, 0x6b, 0xa5, 0x06, 0x26, 0x99, 0x83,
+ 0x58, 0xe6, 0x05, 0xd5, 0x28, 0x07, 0x5b, 0xad, 0xa0, 0xcf, 0xb3, 0xb0,
+ 0xa9, 0x75, 0x40, 0x3e, 0xe1, 0x39, 0xd1, 0x25, 0x21, 0x22, 0x8f, 0x5f,
+ 0x0e, 0x7a, 0xf0, 0xb5, 0x42, 0xeb, 0xf3, 0x4b, 0x3b, 0x89, 0xf8, 0x9e,
+ 0x5f, 0x60, 0x0f, 0xfd, 0xe3, 0x16, 0xc6, 0x45, 0x19, 0x72, 0xa1, 0x06,
+ 0xb0, 0xfc, 0x20, 0xa9, 0x59, 0xfd, 0x7b, 0x95, 0xd8, 0xfb, 0x0c, 0x3d,
+ 0xdc, 0xb7, 0xec, 0xbc, 0xf6, 0x95, 0x5f, 0x52, 0xb0, 0xcb, 0xe5, 0xe2,
+ 0x35, 0xb5, 0x5a, 0x5f, 0x6d, 0x92, 0x20, 0x6a, 0x2a, 0x18, 0x70, 0x9c,
+ 0xf0, 0xdf, 0x64, 0xb4, 0x83, 0x39, 0xe1, 0xcf, 0x22, 0xa2, 0x88, 0x9e,
+ 0x64, 0xc3, 0x6a, 0x32, 0xbc, 0x0b, 0x7b, 0xbc, 0xb8, 0x0b, 0xe0, 0xce,
+ 0x0e, 0x2a, 0xdc, 0xac, 0xff, 0x88, 0x24, 0x1d, 0x5b, 0xff, 0xb6, 0xc0,
+ 0xec, 0x0e, 0x40, 0x38, 0xf7, 0x01, 0x59, 0x66, 0x42, 0xba, 0xb0, 0xec,
+ 0xe2, 0x35, 0x61, 0xf0, 0x1e, 0x11, 0x24, 0x29, 0x59, 0x48, 0x1f, 0x69,
+ 0x0d, 0x50, 0x92, 0x8b, 0x68, 0x21, 0x19, 0x5e, 0xf7, 0xa6, 0x3e, 0x96,
+ 0x71, 0x26, 0xe8, 0x8c, 0x8e, 0x59, 0x48, 0x45, 0x66, 0xea, 0x8e, 0x7a,
+ 0x1b, 0xee, 0x55, 0x5a, 0x03, 0x8f, 0x14, 0x5c, 0x68, 0x15, 0x9b, 0x89,
+ 0xde, 0xb5, 0x38, 0xac, 0xbb, 0x86, 0x48, 0x32, 0x36, 0x20, 0x5a, 0x2b,
+ 0x2d, 0x4d, 0xbf, 0xa6, 0x4e, 0xf7, 0x35, 0x38, 0x21, 0x11, 0x8a, 0x5b,
+ 0x09, 0x14, 0x8e, 0xcc, 0x28, 0x00, 0x24, 0xcd, 0xb3, 0xb7, 0xec, 0x54,
+ 0x61, 0x85, 0x21, 0x94, 0xf7, 0xfd, 0x45, 0xa3, 0xde, 0x4b, 0x99, 0xc0,
+ 0x9a, 0x87, 0x79, 0x04, 0xc8, 0xd4, 0xd2, 0xf5, 0xf5, 0xeb, 0x00, 0x1b,
+ 0xa8, 0x25, 0xaf, 0x06, 0x49, 0xb0, 0x6e, 0xe5, 0xca, 0x51, 0x27, 0x55,
+ 0xd4, 0xa8, 0x74, 0x33, 0xa0, 0x4e, 0x89, 0xa4, 0xfd, 0x54, 0xf8, 0xa9,
+ 0x66, 0x95, 0x55, 0x5d, 0xec, 0x26, 0x17, 0xff, 0x7e, 0x1f, 0xb5, 0x3f,
+ 0xf8, 0xd0, 0x0b, 0x22, 0x05, 0x52, 0x02, 0x27, 0x79, 0x77, 0x75, 0xad,
+ 0xa1, 0xf7, 0x0f, 0x99, 0xcf, 0x70, 0xea, 0xc7, 0xcf, 0x0d, 0xd3, 0xd0,
+ 0x66, 0x01, 0x1c, 0xb7, 0xcc, 0x67, 0x3a, 0x9d, 0xd6, 0x3f, 0xa1, 0xb9,
+ 0xf4, 0xa9, 0x61, 0x1a, 0xe9, 0xeb, 0x42, 0xd2, 0xdb, 0x62, 0xd2, 0x9d,
+ 0xb7, 0xae, 0x06, 0x16, 0x1e, 0xd0, 0xdf, 0xe9, 0x17, 0xeb, 0xa9, 0xb0,
+ 0xdb, 0xe8, 0xf3, 0xd2, 0xa2, 0x64, 0x71, 0xe8, 0x7b, 0x7d, 0xd4, 0x3f,
+ 0x5e, 0x73, 0xb7, 0x6c, 0x86, 0x30, 0xdf, 0x30, 0x5b, 0x0f, 0x70, 0xb0,
+ 0xe0, 0xee, 0x29, 0x83, 0x15, 0x4e, 0x04, 0xac, 0xcb, 0x34, 0xe7, 0x21,
+ 0xeb, 0xcd, 0xa2, 0x08, 0xb6, 0x36, 0x87, 0x92, 0xa9, 0xb5, 0xc0, 0xb9,
+ 0xe6, 0x4a, 0x9f, 0xf0, 0xf2, 0xe1, 0x36, 0xe9, 0x22, 0x27, 0x46, 0xd8,
+ 0xf3, 0x81, 0xf4, 0xee, 0x08, 0x88, 0xc5, 0x5f, 0x17, 0xae, 0x8e, 0xac,
+ 0x12, 0x23, 0xa9, 0x92, 0x9c, 0xc1, 0x7e, 0xfa, 0x9f, 0x73, 0x60, 0x89,
+ 0xc7, 0xe5, 0x4d, 0x4c, 0xd3, 0xa9, 0x44, 0x48, 0xea, 0xcc, 0xc7, 0xaf,
+ 0xe2, 0x6a, 0x94, 0x6a, 0x78, 0x9e, 0x36, 0x49, 0x13, 0x0d, 0xb1, 0x53,
+ 0xee, 0x2a, 0xb5, 0xd3, 0x46, 0x71, 0xd8, 0xee, 0xdf, 0xac, 0x54, 0xe1,
+ 0x50, 0xca, 0xfd, 0xd2, 0xf1, 0x1c, 0xc6, 0x13, 0x96, 0x59, 0xf8, 0x36,
+ 0x05, 0x98, 0x6c, 0xe7, 0x3b, 0x11, 0x1d, 0xb0, 0x2c, 0xa8, 0xce, 0x3f,
+ 0xa5, 0xd4, 0xd6, 0xe5, 0xb3, 0x9a, 0xb7, 0x47, 0xd1, 0xab, 0x01, 0xc6,
+ 0xcd, 0x00, 0x95, 0x3e, 0xd2, 0x5d, 0x61, 0x63, 0xea, 0xe8, 0xec, 0x81,
+ 0x51, 0xc1, 0xe8, 0xc1, 0x0a, 0x13, 0x53, 0x7c, 0x1d, 0x86, 0x38, 0xd9,
+ 0xca, 0xb9, 0x0e, 0xbc, 0xf9, 0xcc, 0xac, 0x00, 0xe6, 0x10, 0x51, 0x54,
+ 0x69, 0x44, 0x80, 0x44, 0xd1, 0x2c, 0x3a, 0x6d, 0xaa, 0xee, 0x86, 0x71,
+ 0x12, 0xa3, 0x5c, 0x3b, 0x19, 0x71, 0x01, 0xdf, 0x62, 0x7d, 0x13, 0xdd,
+ 0x10, 0x72, 0x97, 0x8b, 0xc1, 0x3b, 0x4a, 0x1a, 0x3a, 0xdb, 0x6b, 0x0d,
+ 0x9b, 0x90, 0x70, 0x61, 0xa4, 0x5d, 0xac, 0x96, 0xdc, 0xb8, 0x00, 0x78,
+ 0x0f, 0x10, 0x40, 0xbd, 0xa8, 0xc3, 0x0d, 0x75, 0x5c, 0xcd, 0x42, 0x5f,
+ 0x09, 0xcf, 0x3e, 0x77, 0xcf, 0x72, 0x64, 0x18, 0x5a, 0x77, 0xe4, 0x35,
+ 0x76, 0x30, 0xd4, 0x90, 0xf6, 0x3b, 0xbc, 0x7e, 0x0c, 0x35, 0xfc, 0x82,
+ 0x9e, 0x1f, 0x3d, 0xdf, 0x9c, 0x8a, 0x91, 0xdf, 0xd8, 0xc2, 0x4b, 0xd4,
+ 0x3b, 0x31, 0xe6, 0xac, 0x36, 0x12, 0xa3, 0xa7, 0x13, 0x51, 0x44, 0x96,
+ 0x4f, 0xd3, 0x17, 0x5a, 0x8d, 0xcb, 0x21, 0x0e, 0xab, 0xa7, 0xb8, 0xca,
+ 0x07, 0xe7, 0x05, 0x4a, 0x99, 0x53, 0xcc, 0x46, 0x6c, 0x9f, 0x6a, 0xc1,
+ 0xe4, 0x6e, 0x90, 0x02, 0xbf, 0x83, 0x1c, 0xfd, 0xfc, 0x2d, 0xa4, 0xc1,
+ 0x6b, 0x7d, 0x0b, 0x20, 0xc2, 0xe3, 0x6b, 0xfe, 0x86, 0x60, 0xe7, 0x78,
+ 0x59, 0x60, 0x5e, 0x90, 0x4f, 0x91, 0xa1, 0xf2, 0x90, 0x9b, 0x5a, 0x2b,
+ 0x81, 0x7f, 0x0a, 0x94, 0x3c, 0xaf, 0x0e, 0x23, 0x12, 0x76, 0xcd, 0x62,
+ 0x47, 0xd3, 0xd2, 0x99, 0x52, 0x53, 0xc2, 0xd3, 0x60, 0xc1, 0x71, 0x15,
+ 0xb1, 0x2f, 0xdf, 0x6e, 0x0f, 0x6e, 0x4c, 0x31, 0x29, 0x78, 0x2c, 0xea,
+ 0xfb, 0xa7, 0x5f, 0x31, 0x68, 0xd4, 0x3d, 0x38, 0x87, 0xe0, 0x75, 0xd9,
+ 0xa8, 0x95, 0xde, 0xb2, 0x42, 0xfa, 0x20, 0x17, 0xc9, 0x7e, 0xba, 0x2a,
+ 0xd6, 0x34, 0x83, 0xeb, 0xd5, 0xa7, 0x78, 0xa1, 0x6d, 0xd4, 0x21, 0x29,
+ 0xa3, 0x71, 0xfe, 0xa7, 0x75, 0x48, 0xcd, 0xd3, 0xd5, 0x89, 0xb4, 0xab,
+ 0x23, 0xb2, 0x01, 0x37, 0xf8, 0xe2, 0xe2, 0xc5, 0xae, 0x90, 0x66, 0xa9,
+ 0xc5, 0xad, 0xf5, 0x34, 0xdf, 0x79, 0x05, 0x87, 0x74, 0xb7, 0x1c, 0x77,
+ 0x8d, 0xa7, 0x11, 0x59, 0x71, 0x8b, 0x2a, 0x50, 0xa7, 0xc9, 0xe4, 0xf1,
+ 0x6d, 0x98, 0xe2, 0x4f, 0x45, 0xa8, 0xea, 0xe7, 0x5b, 0x9f, 0xd0, 0xf4,
+ 0x45, 0x4c, 0xf8, 0x8c, 0x1e, 0x58, 0x13, 0x7b, 0xc8, 0xe2, 0xfe, 0xae,
+ 0x13, 0x87, 0x33, 0xff, 0x08, 0xe0, 0xcf, 0xb8, 0x8a, 0x51, 0xff, 0xbb,
+ 0x9e, 0xbb, 0x38, 0x45, 0x09, 0xf1, 0x88, 0x8c, 0x02, 0xd3, 0x17, 0x99,
+ 0x52, 0x37, 0xfe, 0x27, 0x81, 0xc6, 0x47, 0x74, 0x40, 0xf4, 0x87, 0x1f,
+ 0xd5, 0x73, 0x00, 0x91, 0x9e, 0x23, 0xda, 0xec, 0x9a, 0xe1, 0x3d, 0x7b,
+ 0xd6, 0x0b, 0x28, 0xde, 0x8d, 0xe3, 0x2f, 0xb8, 0xfd, 0x9a, 0xba, 0x0e,
+ 0xc7, 0xe9, 0x0d, 0x5d, 0x2b, 0x19, 0x77, 0xe8, 0xd5, 0x07, 0xa0, 0x1a,
+ 0x91, 0xb7, 0xb0, 0x6f, 0xfa, 0x96, 0x3a, 0x3b, 0xab, 0x1f, 0xfc, 0x25,
+ 0xa8, 0x85, 0x41, 0x81, 0x42, 0x5a, 0x8f, 0x5b, 0xe9, 0x69, 0xd5, 0xe9,
+ 0x12, 0xdc, 0xa0, 0xf5, 0xaf, 0xeb, 0x0f, 0x1c, 0x40, 0x9d, 0x16, 0x83,
+ 0xb8, 0xec, 0x2b, 0xcf, 0x5e, 0x8d, 0x06, 0x14, 0xa1, 0xd7, 0xac, 0xfb,
+ 0x35, 0x52, 0xfc, 0xeb, 0xea, 0x99, 0xd5, 0xa7, 0xda, 0x7c, 0x07, 0x32,
+ 0x02, 0xd9, 0x66, 0x07, 0x0d, 0x08, 0x28, 0x23, 0x12, 0x8f, 0xea, 0x1f,
+ 0x49, 0xce, 0x5f, 0x4c, 0x05, 0xfa, 0x89, 0x0b, 0x56, 0x04, 0x26, 0x54,
+ 0x51, 0xdb, 0x77, 0x34, 0x30, 0xd9, 0x82, 0x56, 0xf3, 0xf8, 0x74, 0x5c,
+ 0x7a, 0x95, 0x7f, 0xe5, 0xad, 0xff, 0xc9, 0x1d, 0x8e, 0x44, 0x02, 0x18,
+ 0x96, 0x72, 0x34, 0x81, 0xe4, 0x21, 0x44, 0x8a, 0x3d, 0x4d, 0x88, 0xa2,
+ 0x29, 0x42, 0x85, 0xa5, 0x92, 0x6b, 0x6c, 0x35, 0x15, 0xed, 0x0f, 0xc4,
+ 0x6d, 0x73, 0x5e, 0x03, 0x66, 0xb8, 0x55, 0xff, 0x63, 0x26, 0xa9, 0x1a,
+ 0x55, 0xe4, 0x97, 0x2d, 0xfd, 0xff, 0x2d, 0x72, 0xf2, 0x17, 0xe8, 0xee,
+ 0x24, 0x70, 0x4b, 0x9b, 0xfc, 0xa5, 0x4e, 0x93, 0xf5, 0x55, 0x4a, 0x28,
+ 0x07, 0xda, 0xc8, 0x0f, 0x27, 0x60, 0x76, 0x8f, 0x77, 0x7e, 0xb3, 0x00,
+ 0xb5, 0x58, 0x6c, 0x39, 0xe7, 0xad, 0x0c, 0xa2, 0xc9, 0x1a, 0x7a, 0x9d,
+ 0x48, 0xe3, 0x69, 0x39, 0xc4, 0xb1, 0x5c, 0x94, 0x09, 0x4e, 0xb7, 0x1a,
+ 0x09, 0x3c, 0x92, 0x1e, 0xa1, 0xc2, 0x5a, 0x95, 0xf2, 0x47, 0x6e, 0x70,
+ 0x2f, 0x01, 0x77, 0x73, 0x6b, 0x6b, 0xae, 0xf1, 0x37, 0x7e, 0x66, 0x55,
+ 0x1b, 0x5e, 0x42, 0x0c, 0xcc, 0xc0, 0x65, 0xb6, 0x1c, 0x6a, 0xe9, 0x8e,
+ 0xe0, 0x2f, 0x44, 0x9c, 0x68, 0x32, 0xd3, 0x95, 0x85, 0xc8, 0xf7, 0x3f,
+ 0xca, 0x49, 0x9f, 0x68, 0x05, 0xd9, 0x91, 0x24, 0xc0, 0x48, 0x64, 0x77,
+ 0x8e, 0x69, 0x6a, 0x00, 0x7c, 0xa3, 0xf5, 0x9f, 0xd0, 0xb3, 0xc3, 0x8e,
+ 0xc7, 0x32, 0x1a, 0x65, 0x72, 0x33, 0x68, 0xb2, 0x78, 0x2d, 0x7c, 0xe1,
+ 0x27, 0x57, 0x41, 0x7a, 0x08, 0x33, 0xff, 0xfc, 0xff, 0xff, 0x4f, 0x37,
+ 0x7b, 0x11, 0xaf, 0x91, 0x87, 0xfa, 0x10, 0x45, 0x62, 0xd4, 0xbf, 0xd7,
+ 0x33, 0x5f, 0x79, 0x44, 0xfb, 0xbc, 0xa7, 0xde, 0xee, 0xbb, 0xeb, 0x4f,
+ 0x70, 0x5c, 0x3a, 0x01, 0xe1, 0x0e, 0x2f, 0xad, 0x86, 0x65, 0x2b, 0xcc,
+ 0x6c, 0x5b, 0x31, 0x1b, 0x0b, 0x78, 0x90, 0x4a, 0x20, 0x1c, 0xe1, 0x21,
+ 0x05, 0x98, 0xb4, 0x1e, 0x10, 0x2c, 0x46, 0x2f, 0x1e, 0xed, 0x78, 0x59,
+ 0x9e, 0xec, 0x90, 0x3e, 0xd0, 0xde, 0x05, 0xda, 0x5c, 0x38, 0xb4, 0x1c,
+ 0x72, 0x84, 0x40, 0x3e, 0x90, 0xb6, 0x4d, 0x52, 0x01, 0x47, 0x11, 0xf5,
+ 0x2a, 0xec, 0x95, 0x77, 0xf2, 0x54, 0x1f, 0x95, 0x3a, 0xef, 0x23, 0xa2,
+ 0x0d, 0xd4, 0x35, 0xbe, 0x65, 0xa8, 0xe6, 0xac, 0x94, 0x6f, 0x90, 0xf6,
+ 0xb7, 0xa3, 0x6d, 0x0e, 0x17, 0x92, 0x78, 0x4f, 0x1a, 0x3b, 0x81, 0x59,
+ 0xb5, 0x36, 0xaf, 0xcd, 0xa0, 0xf1, 0x32, 0xc3, 0x13, 0x74, 0x5d, 0xeb,
+ 0xb0, 0xcf, 0x00, 0x2f, 0xa6, 0x84, 0xef, 0x4e, 0x0f, 0xcb, 0x74, 0xda,
+ 0x32, 0x99, 0x32, 0x92, 0x54, 0x48, 0xb3, 0x95, 0x56, 0xc2, 0x76, 0x3f,
+ 0x00, 0x9d, 0x7b, 0xc6, 0x3a, 0xd9, 0xf7, 0x5c, 0xbf, 0x92, 0x14, 0xa4,
+ 0x44, 0x74, 0xe0, 0x49, 0x56, 0x75, 0xc9, 0x2d, 0x4d, 0x63, 0x3c, 0xab,
+ 0x96, 0xa7, 0x61, 0xdc, 0x0f, 0x7b, 0x07, 0x36, 0xe7, 0xfc, 0x0a, 0xfb,
+ 0x61, 0x4a, 0xae, 0x7c, 0x68, 0x3c, 0xa0, 0x86, 0xab, 0xe8, 0x38, 0x8b,
+ 0x6f, 0xfd, 0xf0, 0x1c, 0x7b, 0x17, 0x88, 0x68, 0x64, 0xf7, 0xae, 0x11,
+ 0x12, 0xb0, 0x6e, 0x65, 0x7d, 0x9f, 0x3f, 0xcd, 0x4e, 0x2b, 0x9b, 0x03,
+ 0x7a, 0xf9, 0x53, 0x8f, 0xff, 0x77, 0x37, 0xdb, 0xeb, 0x80, 0x56, 0xbe,
+ 0xce, 0xb3, 0xc4, 0x2f, 0x2e, 0x22, 0x0a, 0x7e, 0xbc, 0xf5, 0xb5, 0xe6,
+ 0x29, 0xe7, 0xec, 0x01, 0x83, 0x76, 0x8a, 0x73, 0x5f, 0xab, 0x90, 0xba,
+ 0x68, 0xc3, 0x83, 0x24, 0x96, 0x3f, 0x30, 0x38, 0x54, 0xae, 0x87, 0xdd,
+ 0xe7, 0xaf, 0xa6, 0xa0, 0x7e, 0xcd, 0x2e, 0x72, 0x05, 0x86, 0x13, 0x9f,
+ 0xf0, 0x87, 0x21, 0x60, 0xd9, 0x56, 0x23, 0x32, 0xa3, 0xce, 0x02, 0xd6,
+ 0xad, 0xc2, 0xbc, 0x0e, 0x28, 0xd5, 0x1e, 0x74, 0xe9, 0xc2, 0xe1, 0x67,
+ 0x3b, 0xf0, 0xf6, 0x99, 0xc5, 0x33, 0x81, 0xca, 0xcb, 0x36, 0xf5, 0x32,
+ 0xa8, 0x7a, 0xe8, 0xc3, 0x4b, 0x41, 0x1b, 0x48, 0x33, 0x1e, 0xd0, 0xc4,
+ 0x41, 0x58, 0x73, 0x43, 0x03, 0xef, 0x2e, 0xeb, 0x58, 0x07, 0x9f, 0xe2,
+ 0x4f, 0x31, 0xcb, 0x02, 0x69, 0x7d, 0x3e, 0xef, 0x80, 0x5c, 0x97, 0x26,
+ 0xf3, 0x93, 0x97, 0x2a, 0x20, 0x81, 0x84, 0x67, 0x92, 0x29, 0x71, 0xff,
+ 0xcb, 0x79, 0x96, 0x34, 0x11, 0xf5, 0xcb, 0x56, 0x0e, 0x13, 0xae, 0x26,
+ 0xc5, 0x40, 0x70, 0xc0, 0xa5, 0x74, 0x8f, 0x5c, 0x16, 0x43, 0xc6, 0x36,
+ 0x81, 0x8d, 0x95, 0xb9, 0xc9, 0x52, 0x2c, 0x19, 0x02, 0xfc, 0x4d, 0x24,
+ 0x05, 0xef, 0xd4, 0xfe, 0x9a, 0xd5, 0xbf, 0x5b, 0x68, 0x6c, 0x0a, 0x52,
+ 0x41, 0xa5, 0xb9, 0x0d, 0x88, 0xd4, 0xa7, 0x84, 0xe9, 0x8b, 0x75, 0xbd,
+ 0x85, 0x36, 0x2b, 0x2b, 0x37, 0xd4, 0x6b, 0x46, 0xaa, 0xd8, 0xcb, 0x8e,
+ 0x30, 0xa6, 0x21, 0x0d, 0x42, 0x7e, 0xb1, 0x7f, 0x33, 0x89, 0x88, 0xcb,
+ 0xe2, 0x89, 0x35, 0xf4, 0x82, 0xc5, 0xe0, 0x58, 0x6d, 0x01, 0x93, 0x82,
+ 0xc6, 0x15, 0xda, 0xa3, 0x5b, 0xae, 0xdd, 0x03, 0xd9, 0xad, 0xb6, 0x81,
+ 0xa2, 0x7a, 0x3b, 0xa2, 0xc5, 0xbb, 0xbf, 0xa6, 0x61, 0x24, 0xc7, 0xf5,
+ 0x8b, 0xdf, 0xcc, 0xa8, 0xfd, 0x35, 0xfa, 0xae, 0x28, 0x99, 0x55, 0x52,
+ 0xf4, 0x40, 0x9b, 0x71, 0xd8, 0xbb, 0xd8, 0xdc, 0x7e, 0xdd, 0xad, 0xee,
+ 0xc9, 0xa1, 0x9b, 0xc9, 0x9a, 0xf0, 0x96, 0x34, 0xc2, 0xde, 0x5d, 0x0d,
+ 0x1d, 0xcd, 0x20, 0x86, 0x99, 0x34, 0x80, 0x90, 0x78, 0x99, 0x4b, 0x60,
+ 0x00, 0x26, 0x4e, 0x2d, 0x43, 0xc1, 0x17, 0x90, 0x8c, 0x72, 0x3f, 0xd0,
+ 0x4a, 0xec, 0x08, 0x1b, 0xe8, 0xa0, 0xa5, 0xb4, 0x54, 0x80, 0x36, 0x97,
+ 0x3c, 0xb0, 0x75, 0x2c, 0x9c, 0x5f, 0xf7, 0xde, 0xdd, 0x62, 0xfa, 0xe7,
+ 0xd3, 0xef, 0x84, 0x0b, 0x56, 0x32, 0xc9, 0xfb, 0x08, 0x8b, 0x71, 0xe2,
+ 0x5a, 0x0b, 0x24, 0xd9, 0x75, 0xcd, 0x1e, 0x0d, 0x9e, 0xf7, 0xf4, 0x5e,
+ 0x28, 0x60, 0xa5, 0xd8, 0x2e, 0x7c, 0x73, 0x90, 0xb5, 0xae, 0xc0, 0xd5,
+ 0xb0, 0x9c, 0xfb, 0xdb, 0x35, 0x2e, 0x36, 0x17, 0xe7, 0x85, 0x2e, 0xe2,
+ 0xab, 0x01, 0x14, 0xf6, 0x69, 0x12, 0x9a, 0x88, 0x6f, 0x19, 0xa9, 0x42,
+ 0x85, 0x5c, 0x5c, 0x85, 0x5e, 0xce, 0xcd, 0x29, 0x02, 0x78, 0x2d, 0x0a,
+ 0x0d, 0x1c, 0xf8, 0x61, 0x38, 0xd7, 0x51, 0x1a, 0x8f, 0xdd, 0xe0, 0xf5,
+ 0x74, 0xf3, 0xd8, 0xb6, 0x60, 0x6b, 0x22, 0x9f, 0xe8, 0xd3, 0x40, 0xc4,
+ 0xc6, 0x14, 0x6d, 0x23, 0xa2, 0x3b, 0xcb, 0x80, 0xa8, 0x5c, 0xf5, 0x3a,
+ 0x0c, 0x15, 0xa0, 0xc8, 0x28, 0xa8, 0xb0, 0x04, 0xef, 0xbf, 0x99, 0x51,
+ 0x7f, 0x02, 0x41, 0x08, 0xe8, 0x3c, 0x44, 0x7c, 0xd1, 0xd5, 0xa2, 0x38,
+ 0x61, 0x0a, 0xc3, 0xfd, 0x7e, 0xe3, 0x0a, 0xef, 0x5e, 0x81, 0x8f, 0xbb,
+ 0xb3, 0xd9, 0x65, 0xda, 0x3b, 0x45, 0x23, 0xdf, 0x5c, 0xdb, 0x05, 0x11,
+ 0x71, 0xfa, 0x4c, 0x4a, 0xf3, 0x56, 0xb6, 0x10, 0xfd, 0x27, 0x14, 0xdc,
+ 0x80, 0xb5, 0xd7, 0x6a, 0xe5, 0x66, 0x4e, 0xf0, 0xaa, 0x60, 0x72, 0x90,
+ 0x4c, 0x0e, 0x35, 0xb8, 0x1b, 0x86, 0xda, 0x3a, 0xe8, 0x00, 0x00, 0x09,
+ 0x05, 0x4f, 0x2f, 0x48, 0x3d, 0x16, 0x30, 0x9d, 0xc7, 0xff, 0x3c, 0xa7,
+ 0xff, 0xed, 0x38, 0x23, 0x17, 0x66, 0xc9, 0xea, 0xa8, 0xe6, 0xbe, 0xcc,
+ 0xf7, 0xfb, 0xdc, 0x91, 0xd1, 0x0e, 0xd3, 0x1b, 0x6b, 0x86, 0xd6, 0x69,
+ 0xa3, 0x23, 0x73, 0x4c, 0xf9, 0x72, 0x5b, 0x6b, 0x1f, 0x59, 0x64, 0x9e,
+ 0xde, 0x7f, 0x62, 0x0a, 0x58, 0x97, 0x73, 0x4b, 0x9c, 0xdf, 0x28, 0x14,
+ 0x61, 0x01, 0xdd, 0x14, 0xc6, 0xad, 0x7f, 0x46, 0x6e, 0x4e, 0xe2, 0xd1,
+ 0x79, 0xe7, 0x75, 0x7d, 0xc8, 0x89, 0x66, 0xd4, 0x37, 0x29, 0x9e, 0x9e,
+ 0x89, 0x19, 0xc9, 0xa1, 0x21, 0x8a, 0xaf, 0x3d, 0x0d, 0x7c, 0xdc, 0xe2,
+ 0x73, 0xcc, 0x8d, 0x01, 0x9b, 0xab, 0xd9, 0x9b, 0xd0, 0xf0, 0x09, 0x22,
+ 0xb9, 0xac, 0x82, 0x6b, 0x80, 0xb7, 0x69, 0xb2, 0xee, 0xa3, 0xe2, 0xc1,
+ 0x8d, 0x35, 0xeb, 0x03, 0x3d, 0x1c, 0xaa, 0xd7, 0x4b, 0xd1, 0x08, 0xfd,
+ 0xab, 0x84, 0x2e, 0xc4, 0xdd, 0xbd, 0xfd, 0x54, 0x04, 0xb5, 0x2d, 0xc8,
+ 0xea, 0xd3, 0xcd, 0x54, 0x04, 0x2c, 0xad, 0x42, 0x7d, 0xa4, 0x17, 0xcd,
+ 0xcf, 0x1f, 0x8e, 0xe0, 0x7b, 0x4e, 0x6e, 0xfa, 0x2d, 0xa8, 0xca, 0x03,
+ 0x01, 0xe2, 0xde, 0x32, 0xcf, 0xd6, 0x24, 0x91, 0x54, 0xc8, 0x4a, 0x6f,
+ 0x5a, 0x7f, 0x51, 0xa9, 0x87, 0x8a, 0x2b, 0x87, 0x87, 0x2b, 0xcf, 0x84,
+ 0x69, 0xa7, 0x1f, 0x91, 0x4b, 0x41, 0x2a, 0xa3, 0x9c, 0xef, 0x55, 0x31,
+ 0x3c, 0xe4, 0x84, 0x74, 0x76, 0x2b, 0x60, 0xb5, 0x2b, 0x68, 0xa5, 0x5a,
+ 0x6b, 0x58, 0xb9, 0x77, 0xf4, 0x0a, 0x8e, 0x25, 0x8b, 0x98, 0x2b, 0xa2,
+ 0x94, 0x6c, 0x8e, 0xa5, 0xee, 0x91, 0x1a, 0x22, 0x11, 0x51, 0xd4, 0xa4,
+ 0xaa, 0x0c, 0x0e, 0x9c, 0x43, 0x13, 0x17, 0xce, 0xb6, 0xca, 0xe4, 0x87,
+ 0xfa, 0x9d, 0x1a, 0xaf, 0xe3, 0xa3, 0x99, 0x7d, 0xa1, 0xb8, 0x00, 0x9d,
+ 0x5d, 0xd6, 0x0b, 0x3d, 0xb3, 0x86, 0x75, 0x1b, 0xd6, 0xb0, 0x2b, 0x21,
+ 0x16, 0x85, 0x34, 0x35, 0xa5, 0x7e, 0xa7, 0x5a, 0x3c, 0xba, 0x82, 0x78,
+ 0xdf, 0x95, 0x76, 0x57, 0x30, 0x3b, 0x2d, 0x17, 0x04, 0x25, 0x90, 0x3c,
+ 0x6e, 0xbc, 0xc1, 0x97, 0x17, 0xb6, 0xf4, 0xc0, 0x9f, 0xc0, 0xa3, 0xd0,
+ 0xea, 0x8a, 0x1c, 0x96, 0x55, 0xe2, 0x68, 0x49, 0xd9, 0x52, 0xa8, 0xa0,
+ 0x45, 0x86, 0x00, 0xc1, 0x0b, 0x82, 0x5a, 0x69, 0xfb, 0x0c, 0xc6, 0x13,
+ 0xad, 0x19, 0x1b, 0xc9, 0xc1, 0x80, 0x51, 0x72, 0x04, 0xa8, 0x7e, 0x0c,
+ 0x66, 0x2b, 0x07, 0xe1, 0xfa, 0x02, 0x95, 0xf9, 0x9e, 0xe9, 0x1a, 0xff,
+ 0x2f, 0xf7, 0x27, 0x01, 0x12, 0x27, 0x37, 0x65, 0x13, 0x99, 0x3a, 0xe9,
+ 0xa5, 0x53, 0xbe, 0xf3, 0x93, 0x02, 0x73, 0x24, 0x19, 0x9c, 0x4a, 0x5a,
+ 0x1d, 0x0d, 0x02, 0xbb, 0x53, 0x4c, 0x0d, 0x24, 0xa5, 0x7c, 0x50, 0x0e,
+ 0x1e, 0x7f, 0x25, 0xf6, 0xca, 0x96, 0x59, 0x2a, 0x24, 0x1a, 0x67, 0x4a,
+ 0x8b, 0x7f, 0xdd, 0x90, 0x36, 0x7f, 0xf0, 0x05, 0xae, 0x4e, 0xc9, 0xa9,
+ 0x6f, 0x1f, 0x8d, 0xfd, 0x1c, 0x70, 0xe1, 0xe5, 0xc7, 0xe7, 0x18, 0x27,
+ 0x58, 0xb6, 0xf7, 0x09, 0x31, 0x67, 0x94, 0x98, 0xc4, 0x89, 0x8d, 0xf2,
+ 0x95, 0xff, 0x55, 0xb6, 0xcc, 0x3c, 0x0e, 0xa7, 0xf9, 0x77, 0x7a, 0xfc,
+ 0x33, 0xfb, 0xa7, 0x07, 0x6f, 0x5d, 0x2e, 0x5f, 0x72, 0xba, 0xc6, 0x2f,
+ 0xd3, 0x34, 0x0e, 0x87, 0x51, 0x8d, 0x5b, 0xd7, 0x61, 0x85, 0x18, 0x2f,
+ 0xff, 0xd3, 0x92, 0xdb, 0x1c, 0xbd, 0x46, 0xaa, 0x9c, 0xae, 0x7b, 0xc0,
+ 0x45, 0xa0, 0x0d, 0xbd, 0x3c, 0xf4, 0x78, 0xd4, 0xd2, 0x47, 0x1f, 0x21,
+ 0x91, 0xcf, 0x5d, 0xf9, 0xca, 0x02, 0xe2, 0x6c, 0xe5, 0xdb, 0x9a, 0x7e,
+ 0x2d, 0x21, 0x17, 0x23, 0x90, 0x71, 0x0b, 0x7f, 0x87, 0xf1, 0x6c, 0xbc,
+ 0x2d, 0x31, 0x18, 0xa0, 0x2a, 0xd6, 0x44, 0xf4, 0xd4, 0xd5, 0x3b, 0x25,
+ 0x85, 0x90, 0xc6, 0x6d, 0xd3, 0x7e, 0xac, 0x6e, 0x0a, 0x49, 0xa5, 0xb2,
+ 0xd3, 0x8a, 0x25, 0x79, 0x08, 0xda, 0x2e, 0x2d, 0x1d, 0xbc, 0x8f, 0xc4,
+ 0x15, 0x1e, 0x15, 0xa4, 0xf6, 0xc5, 0x8b, 0xb9, 0x0e, 0x9b, 0x04, 0x77,
+ 0x3a, 0x20, 0xdc, 0xcc, 0x6b, 0x0d, 0x30, 0xc2, 0x43, 0x87, 0xae, 0xad,
+ 0x36, 0xd6, 0x84, 0x4f, 0xf3, 0x59, 0x60, 0x1e, 0x42, 0x83, 0x9c, 0xaa,
+ 0x00, 0x9b, 0x6d, 0x65, 0xac, 0x39, 0xc2, 0x6b, 0xad, 0x31, 0x4f, 0x3d,
+ 0xa5, 0x79, 0xc8, 0xe5, 0x7a, 0x91, 0xcc, 0xee, 0xef, 0x39, 0x7d, 0x91,
+ 0xd9, 0x10, 0xeb, 0x08, 0xbb, 0x78, 0xc0, 0xe3, 0x6c, 0xe7, 0xed, 0x46,
+ 0xd4, 0xb0, 0x21, 0x68, 0x58, 0x33, 0xa5, 0xe0, 0x73, 0x47, 0xc3, 0xc0,
+ 0x9e, 0x2b, 0xc8, 0x48, 0xca, 0xd3, 0x44, 0xd9, 0xc6, 0xb5, 0x9f, 0x44,
+ 0xe7, 0xf4, 0x80, 0xfb, 0x99, 0xbc, 0x3a, 0xde, 0x9e, 0x78, 0xcc, 0x17,
+ 0xf0, 0xd7, 0x8d, 0x77, 0xe3, 0x03, 0xad, 0xce, 0xda, 0xeb, 0xce, 0x44,
+ 0x5f, 0x30, 0x86, 0xff, 0xd0, 0xbf, 0x61, 0x52, 0xce, 0x49, 0x54, 0xfc,
+ 0xdb, 0x98, 0x9b, 0xcb, 0x58, 0x18, 0xce, 0xec, 0xf1, 0xde, 0xfc, 0x32,
+ 0xf8, 0x06, 0xb2, 0x15, 0xff, 0xd9, 0x4e, 0x81, 0xfb, 0x8c, 0x01, 0x17,
+ 0x79, 0x65, 0x02, 0xfe, 0x9e, 0x42, 0x91, 0x22, 0x86, 0xa9, 0xed, 0x7d,
+ 0xaa, 0xf4, 0x41, 0x79, 0xe7, 0xbe, 0x30, 0x75, 0x74, 0x71, 0x41, 0x2d,
+ 0x54, 0x6c, 0x44, 0x38, 0x50, 0xa9, 0xdf, 0x7f, 0x1f, 0x18, 0x7f, 0xf8,
+ 0x5e, 0xe4, 0x0e, 0x42, 0x0c, 0x84, 0x74, 0x48, 0x1a, 0xb8, 0x74, 0x72,
+ 0x08, 0x98, 0xf9, 0x71, 0xea, 0xea, 0x1f, 0xa8, 0x1c, 0xa3, 0x5e, 0x7e,
+ 0xe1, 0x46, 0x34, 0x2b, 0xac, 0x6e, 0x75, 0xa8, 0x96, 0x8d, 0x22, 0x61,
+ 0x56, 0xc7, 0x5c, 0x11, 0x47, 0xc8, 0xcd, 0x36, 0x01, 0x3a, 0x67, 0x05,
+ 0x5b, 0x61, 0x03, 0x46, 0xfc, 0x97, 0x62, 0xa2, 0x1d, 0x31, 0x5f, 0xe3,
+ 0x62, 0x8f, 0x8b, 0x4f, 0x7e, 0xc2, 0x93, 0x93, 0x52, 0x0e, 0x0c, 0xfc,
+ 0x4c, 0x69, 0x4d, 0xce, 0xfc, 0x54, 0x45, 0x96, 0x95, 0x67, 0xe6, 0x7c,
+ 0xc1, 0x21, 0xc7, 0x2e, 0xb9, 0x42, 0x2f, 0xab, 0x96, 0xeb, 0xa0, 0xd1,
+ 0x4e, 0x91, 0x89, 0xe1, 0xf2, 0x66, 0x27, 0xb2, 0xd1, 0xb8, 0xc9, 0x6a,
+ 0xc4, 0x0f, 0xcf, 0x9c, 0x76, 0x3f, 0xd0, 0x35, 0x12, 0xe1, 0x28, 0x89,
+ 0x3a, 0x08, 0xf5, 0x42, 0x39, 0x54, 0x0c, 0x8d, 0x85, 0x5f, 0x13, 0xb9,
+ 0xfc, 0x4d, 0x6c, 0xd9, 0xe4, 0x8c, 0xb6, 0x4a, 0x3b, 0x32, 0xc9, 0x6e,
+ 0xca, 0x33, 0xc3, 0x79, 0xda, 0xa6, 0x3c, 0xcb, 0x25, 0x32, 0xcb, 0x4c,
+ 0x9d, 0xa8, 0x73, 0x72, 0x88, 0x81, 0xb1, 0x3a, 0xe9, 0x9c, 0x5f, 0xf2,
+ 0x87, 0x43, 0xcd, 0x03, 0x4c, 0xd9, 0x53, 0x6e, 0x0a, 0xe4, 0x6b, 0x45,
+ 0xfe, 0x77, 0xd9, 0xdb, 0x4c, 0xd5, 0xf3, 0xd0, 0x51, 0x1b, 0x7c, 0x82,
+ 0xf9, 0x45, 0x00, 0x71, 0xfe, 0xb9, 0x78, 0x27, 0x8f, 0xa0, 0x70, 0x32,
+ 0xeb, 0xb7, 0x11, 0x9c, 0x33, 0x27, 0xf1, 0x05, 0x51, 0x5e, 0xe0, 0x26,
+ 0x09, 0xe6, 0x26, 0x8a, 0x99, 0xac, 0xed, 0x32, 0x46, 0x2f, 0x1e, 0x48,
+ 0xf2, 0x73, 0xdf, 0xb5, 0x67, 0x8c, 0x72, 0x7c, 0x10, 0xf9, 0xd1, 0xa3,
+ 0xf1, 0x65, 0x89, 0x9d, 0xde, 0x0b, 0x6d, 0xe7, 0x86, 0xbe, 0xbb, 0x67,
+ 0xc0, 0x50, 0x4f, 0x92, 0xde, 0xc2, 0xf4, 0xd1, 0x38, 0x7a, 0xf9, 0x38,
+ 0xef, 0x36, 0x61, 0x3c, 0x3e, 0x02, 0x11, 0xcc, 0xe4, 0x3a, 0xa0, 0xd9,
+ 0xdd, 0x41, 0x56, 0xef, 0xb4, 0xf1, 0x02, 0x47, 0xbb, 0x53, 0x3f, 0x0c,
+ 0x23, 0x6f, 0xf8, 0x42, 0x7c, 0x17, 0x1c, 0x88, 0xfb, 0x33, 0xbb, 0x9a,
+ 0xa1, 0x9d, 0xb1, 0x2c, 0x8a, 0x4f, 0x0a, 0xcc, 0x83, 0x13, 0xd2, 0xa7,
+ 0x00, 0xff, 0x05, 0x2a, 0xa5, 0xcf, 0x20, 0xd4, 0x63, 0x26, 0x54, 0x39,
+ 0x8c, 0xa2, 0xa6, 0x06, 0xa1, 0xc2, 0x4e, 0x85, 0x21, 0x84, 0xf1, 0x4f,
+ 0x6a, 0x5c, 0xce, 0x66, 0x05, 0xbc, 0x17, 0x2c, 0x10, 0x08, 0x0c, 0x90,
+ 0x6e, 0xf0, 0xff, 0xf6, 0xef, 0xf7, 0x79, 0x1d, 0x6b, 0x5f, 0x7c, 0xd2,
+ 0x03, 0x35, 0xc3, 0x2b, 0x06, 0x01, 0x8d, 0xfc, 0xc7, 0x1f, 0x19, 0xc5,
+ 0xda, 0x7e, 0xed, 0x25, 0x36, 0xf1, 0xed, 0xab, 0x4c, 0x03, 0x56, 0x94,
+ 0xb3, 0x08, 0x28, 0x1b, 0xc5, 0xbe, 0xf0, 0xf9, 0x43, 0x95, 0xc6, 0xb9,
+ 0xab, 0x08, 0xab, 0xe0, 0x03, 0x14, 0x3c, 0x65, 0xf8, 0x79, 0xea, 0x3c,
+ 0xcd, 0x1c, 0x73, 0xf1, 0x77, 0x78, 0x64, 0x75, 0x01, 0x2e, 0x94, 0x99,
+ 0xf4, 0x06, 0xf4, 0x70, 0x0c, 0x75, 0x32, 0xc7, 0x1f, 0xc2, 0x53, 0xee,
+ 0x13, 0x17, 0x50, 0xe1, 0x7b, 0xea, 0x47, 0x92, 0xc0, 0xc3, 0x27, 0x1b,
+ 0x18, 0x79, 0x10, 0x34, 0xec, 0x6f, 0xce, 0xb1, 0x50, 0x2f, 0x53, 0xec,
+ 0x48, 0xb7, 0x1e, 0x2b, 0x00, 0x11, 0x5a, 0x93, 0x1f, 0x5f, 0x8b, 0x4b,
+ 0xc8, 0xcf, 0x69, 0xbb, 0xb4, 0x1b, 0x9c, 0x1d, 0xde, 0x8d, 0xa9, 0xe9,
+ 0x47, 0xe8, 0xe9, 0xd9, 0xe7, 0xce, 0x0a, 0xe5, 0x2a, 0x81, 0xc7, 0x5f,
+ 0x2a, 0x62, 0xcb, 0xa2, 0x05, 0x80, 0x00, 0xdc, 0x39, 0xa1, 0x24, 0x18,
+ 0x0e, 0xf6, 0x82, 0x87, 0x93, 0x5c, 0xb3, 0x8f, 0xda, 0xae, 0x8f, 0x3b,
+ 0x0c, 0x1b, 0xe7, 0x43, 0x79, 0xc3, 0xdf, 0xef, 0xdf, 0xff, 0x0f, 0xff,
+ 0x79, 0xc9, 0xad, 0x25, 0xff, 0x7d, 0xec, 0xd2, 0x3a, 0xab, 0x74, 0xf6,
+ 0xe8, 0xb8, 0x28, 0x55, 0x78, 0x00, 0xa8, 0xa9, 0x84, 0x08, 0x67, 0x03,
+ 0x83, 0x79, 0xd1, 0x8f, 0x86, 0xf8, 0xc4, 0xce, 0x8b, 0x2d, 0x08, 0x65,
+ 0xb5, 0x74, 0x33, 0xc8, 0x35, 0x16, 0xf4, 0xc7, 0xef, 0x5c, 0x11, 0x7a,
+ 0xfb, 0x7f, 0x47, 0x9a, 0x7c, 0x92, 0x8c, 0x42, 0xd4, 0x2a, 0x7c, 0xfa,
+ 0xa5, 0x65, 0x06, 0x0e, 0xb9, 0x7c, 0x23, 0x79, 0x19, 0x17, 0x0c, 0x4d,
+ 0xd3, 0x2c, 0x18, 0x6a, 0x25, 0xe0, 0xb0, 0x1b, 0x0b, 0x09, 0x28, 0x88,
+ 0xcc, 0x02, 0x8b, 0x20, 0xa8, 0x41, 0xef, 0xca, 0xd5, 0xc1, 0x9f, 0x87,
+ 0x8b, 0x7a, 0xaa, 0xd8, 0x82, 0x7e, 0x0a, 0x03, 0xbd, 0xe5, 0x83, 0x0b,
+ 0xf7, 0x84, 0xea, 0x2c, 0x15, 0xfb, 0xf5, 0x0a, 0x88, 0xf8, 0x61, 0x8d,
+ 0x65, 0xa7, 0x75, 0xeb, 0x05, 0x6b, 0x44, 0x4e, 0xcf, 0x67, 0xd5, 0xc7,
+ 0xe3, 0x35, 0x80, 0x3a, 0x18, 0x31, 0x1c, 0x23, 0x7a, 0x2e, 0x95, 0x27,
+ 0xea, 0x6f, 0x4c, 0xf3, 0x50, 0x80, 0xe0, 0x75, 0x5c, 0x3b, 0x8b, 0x4c,
+ 0xe2, 0xec, 0xa5, 0x76, 0x71, 0xe8, 0xd5, 0xe7, 0xfc, 0xfa, 0xa2, 0xd4,
+ 0x8c, 0x46, 0x8d, 0xff, 0xe6, 0x39, 0x40, 0x38, 0x81, 0xb5, 0x65, 0xdb,
+ 0x29, 0x7d, 0x4f, 0xef, 0x47, 0xfd, 0xd6, 0x49, 0x06, 0x3b, 0xb6, 0xdc,
+ 0x9c, 0xf1, 0x3b, 0xd9, 0xa7, 0x26, 0xb9, 0x7e, 0xc1, 0x56, 0xe1, 0x87,
+ 0x74, 0x1d, 0xed, 0x6e, 0xe2, 0xdd, 0xee, 0xed, 0x06, 0xff, 0x5b, 0x36,
+ 0x7e, 0xe1, 0x72, 0x28, 0xb7, 0xc8, 0x28, 0x28, 0x4f, 0x00, 0x84, 0xdc,
+ 0xa1, 0x27, 0x57, 0x52, 0x19, 0x45, 0xe0, 0x2b, 0x45, 0x01, 0xb7, 0xa9,
+ 0x6c, 0x33, 0x3c, 0xc0, 0x31, 0xca, 0xd7, 0x43, 0x6c, 0x92, 0xf4, 0xc6,
+ 0x7a, 0x7b, 0x0f, 0x6c, 0x02, 0xfc, 0x0c, 0x54, 0xff, 0x16, 0x98, 0x7b,
+ 0xb5, 0x4f, 0xd5, 0xf6, 0x4f, 0xc2, 0x17, 0x2f, 0x86, 0xc1, 0xe9, 0x48,
+ 0x9e, 0x6d, 0xaa, 0xbe, 0xc8, 0x0a, 0x70, 0x8a, 0x7c, 0xf7, 0x5f, 0xb9,
+ 0x91, 0x2c, 0xc1, 0xc5, 0x57, 0xab, 0x8c, 0xef, 0xf0, 0xb9, 0xe3, 0x3e,
+ 0x2a, 0xfd, 0xaa, 0x3f, 0x75, 0xa4, 0xcc, 0x86, 0x2b, 0x51, 0x2e, 0xf7,
+ 0x57, 0x3e, 0xef, 0x6b, 0x7c, 0x82, 0x90, 0x8e, 0x08, 0x8f, 0x95, 0xd7,
+ 0xa9, 0xfa, 0x19, 0x77, 0xca, 0x3e, 0x80, 0xc3, 0xef, 0x25, 0xc2, 0x2c,
+ 0xa9, 0x04, 0x44, 0x6d, 0xe5, 0xc7, 0xd2, 0xd4, 0x79, 0xae, 0x41, 0x74,
+ 0x94, 0x87, 0x1a, 0x26, 0x87, 0x71, 0x62, 0x26, 0x02, 0x59, 0x7d, 0xd3,
+ 0xef, 0x3c, 0xe3, 0x47, 0x8e, 0x84, 0x4e, 0x24, 0x32, 0x85, 0xa7, 0xc7,
+ 0x06, 0xe0, 0x78, 0xe5, 0x69, 0x19, 0x19, 0xe5, 0xa7, 0x8d, 0xa8, 0xc8,
+ 0xff, 0xa9, 0x39, 0x6b, 0x4c, 0x13, 0x35, 0x8a, 0x8e, 0x86, 0x6e, 0xd7,
+ 0xdd, 0xf5, 0xfd, 0x9e, 0xe2, 0x01, 0x43, 0x5d, 0xb4, 0x5f, 0xba, 0x41,
+ 0xca, 0x3c, 0xec, 0x7f, 0xee, 0xa3, 0x3b, 0xe7, 0xf7, 0x14, 0xb8, 0xee,
+ 0x82, 0x3f, 0xf9, 0x2c, 0xca, 0x14, 0x90, 0x2a, 0xf6, 0x73, 0x38, 0x3b,
+ 0xb6, 0xff, 0xff, 0xa4, 0x55, 0x3f, 0x63, 0x4f, 0x0f, 0x60, 0x19, 0x79,
+ 0xdc, 0xe8, 0x02, 0x5d, 0x24, 0x75, 0x89, 0x55, 0x91, 0xe4, 0xa3, 0x87,
+ 0xa9, 0x05, 0x38, 0x69, 0x0e, 0xde, 0x9c, 0x84, 0x2d, 0xff, 0x5b, 0x18,
+ 0x49, 0xeb, 0x8b, 0xc7, 0x01, 0x08, 0x75, 0x63, 0x66, 0x3a, 0x98, 0x24,
+ 0x38, 0x6f, 0x0a, 0x9d, 0x17, 0x95, 0xec, 0x86, 0x43, 0x1d, 0xae, 0xe4,
+ 0x59, 0xfe, 0xdb, 0xed, 0xa0, 0xec, 0x4a, 0x4d, 0x0d, 0x00, 0x48, 0x56,
+ 0x0b, 0x09, 0xff, 0xbb, 0x85, 0xbf, 0x0d, 0x60, 0xd7, 0x10, 0xe8, 0x07,
+ 0xda, 0x3f, 0xb4, 0xd5, 0xf6, 0x16, 0x66, 0xf6, 0xc8, 0x36, 0x47, 0x1e,
+ 0xbb, 0x75, 0xd8, 0x13, 0xdf, 0xe4, 0x01, 0x11, 0xa8, 0xab, 0x05, 0xba,
+ 0xb9, 0x7e, 0xcd, 0x2b, 0xfe, 0x41, 0x6c, 0x60, 0x34, 0xde, 0x2e, 0x30,
+ 0xc2, 0x8e, 0xdd, 0x91, 0x28, 0x0e, 0x35, 0x41, 0x4c, 0x8b, 0x22, 0xc7,
+ 0xd2, 0xf7, 0xdc, 0x6b, 0x09, 0x31, 0x19, 0x56, 0x58, 0x2c, 0x71, 0xde,
+ 0x62, 0x6b, 0xdc, 0x7e, 0xc1, 0x86, 0x04, 0x28, 0x10, 0xcc, 0xd0, 0x09,
+ 0x97, 0x2e, 0xae, 0x33, 0xa7, 0xe0, 0xfa, 0x7c, 0x2d, 0x0f, 0xa1, 0x07,
+ 0xb3, 0x6d, 0x7c, 0x8c, 0xb3, 0x9a, 0xcb, 0x33, 0x8e, 0xc0, 0x04, 0x95,
+ 0xfd, 0xc4, 0xf9, 0x6a, 0xc3, 0xb8, 0xa7, 0xf5, 0x4f, 0x65, 0x2b, 0x83,
+ 0xd7, 0xae, 0xe6, 0x9a, 0x96, 0x3f, 0xe5, 0x7f, 0xd2, 0x2e, 0xee, 0x6f,
+ 0xdd, 0xb3, 0x55, 0x97, 0xa7, 0x69, 0x02, 0xf4, 0xb3, 0xd6, 0xc6, 0xf7,
+ 0x1a, 0x60, 0x0e, 0x6a, 0xed, 0x1f, 0x5d, 0x98, 0xfd, 0xef, 0x08, 0x1b,
+ 0xac, 0x64, 0x93, 0x1c, 0xe2, 0x3e, 0x08, 0x3b, 0x3c, 0x05, 0xff, 0x22,
+ 0xda, 0xc2, 0x07, 0xdb, 0xf7, 0x50, 0x91, 0xab, 0xb5, 0x41, 0xc2, 0x01,
+ 0xd2, 0x35, 0x15, 0x03, 0xfb, 0x6a, 0x6d, 0x56, 0xbb, 0x07, 0xd2, 0xc2,
+ 0xc9, 0x59, 0x85, 0xf8, 0x5e, 0x7c, 0x3c, 0xca, 0xc7, 0x70, 0x10, 0x16,
+ 0xb6, 0x52, 0xac, 0xab, 0x7b, 0x59, 0x0a, 0xf7, 0x0b, 0xde, 0xdc, 0x01,
+ 0x91, 0x48, 0x89, 0xc7, 0xb5, 0x48, 0x28, 0xf5, 0xa9, 0x99, 0x55, 0xee,
+ 0xc8, 0x39, 0x66, 0x99, 0xf5, 0x67, 0xd8, 0x04, 0x77, 0xdf, 0xf6, 0x83,
+ 0x19, 0x49, 0x8b, 0x11, 0x20, 0x24, 0x15, 0x3d, 0x0d, 0x32, 0xa3, 0x01,
+ 0x1d, 0x05, 0xa6, 0x06, 0xf5, 0x63, 0xa7, 0x3d, 0x9f, 0xf8, 0xe5, 0x53,
+ 0x5c, 0xaf, 0x01, 0xc1, 0xd4, 0x4a, 0xbb, 0x35, 0xbd, 0x05, 0x24, 0x0e,
+ 0x2b, 0x9e, 0x07, 0x86, 0xbc, 0xc2, 0x64, 0xd1, 0xc0, 0x04, 0xb9, 0x7c,
+ 0x38, 0x38, 0xd0, 0xc8, 0xd8, 0x14, 0x73, 0xfb, 0x49, 0x10, 0x90, 0x00,
+ 0x20, 0xf1, 0xc8, 0x46, 0x7e, 0x8e, 0x57, 0xf4, 0x7a, 0x56, 0x92, 0x23,
+ 0x10, 0xe0, 0x9c, 0xcd, 0x8f, 0x92, 0x9b, 0x43, 0x23, 0xe6, 0x6b, 0xdc,
+ 0x1c, 0x2c, 0x5a, 0x10, 0x9b, 0x0c, 0x92, 0x03, 0xbc, 0xcb, 0x27, 0x39,
+ 0x19, 0xed, 0xad, 0x5e, 0x76, 0x5f, 0x6e, 0x9f, 0x90, 0x36, 0xea, 0x95,
+ 0xa6, 0xad, 0x16, 0x28, 0x75, 0x9f, 0x54, 0x63, 0x4f, 0x45, 0xa9, 0xa0,
+ 0x62, 0x6d, 0xe4, 0x36, 0xf2, 0xb5, 0xae, 0x86, 0x45, 0x9c, 0x0f, 0xeb,
+ 0x88, 0x91, 0xbe, 0x9d, 0x6f, 0x3c, 0xb0, 0x16, 0xa2, 0x6a, 0xbe, 0x87,
+ 0x43, 0xa7, 0x79, 0xf8, 0x7f, 0x1f, 0xb6, 0x6c, 0xb4, 0xcf, 0x3f, 0xc4,
+ 0x12, 0x56, 0xde, 0x12, 0x3b, 0x57, 0xcc, 0x73, 0x34, 0xae, 0xcb, 0xc3,
+ 0x5a, 0x5b, 0x84, 0x79, 0x29, 0xab, 0xc0, 0x66, 0xc2, 0xaa, 0xef, 0x09,
+ 0x3c, 0x01, 0x2b, 0x57, 0x8c, 0xda, 0x71, 0x0d, 0x73, 0x9a, 0x57, 0x1d,
+ 0xe0, 0xbd, 0x0e, 0x33, 0xb5, 0xf2, 0x94, 0x05, 0x8f, 0x24, 0x00, 0x4b,
+ 0xbd, 0xbe, 0x29, 0x7d, 0xd3, 0xdb, 0x1e, 0x91, 0x52, 0xd4, 0x98, 0x21,
+ 0x62, 0x82, 0xb2, 0x04, 0x23, 0xd0, 0x0f, 0x5c, 0x53, 0x7e, 0x12, 0x36,
+ 0x13, 0x8a, 0xb6, 0x74, 0x9e, 0xb1, 0xcd, 0xbb, 0x8b, 0x57, 0x36, 0x4f,
+ 0xae, 0x4e, 0xf7, 0xa4, 0x3f, 0xdd, 0x9d, 0x92, 0xcd, 0x08, 0x5a, 0x76,
+ 0x0c, 0x46, 0x2b, 0xca, 0xe9, 0x24, 0xf2, 0xc8, 0x76, 0xb9, 0xab, 0x05,
+ 0x2f, 0x03, 0x9e, 0x78, 0x61, 0xf3, 0xd4, 0xdc, 0xb9, 0x43, 0x68, 0x38,
+ 0xc3, 0xde, 0x7f, 0x28, 0x80, 0x22, 0x55, 0x89, 0xc3, 0xe8, 0x64, 0x50,
+ 0x11, 0x6b, 0x55, 0xe2, 0x30, 0xd8, 0xb2, 0xd6, 0x0e, 0xc0, 0x1f, 0x15,
+ 0x42, 0x2e, 0xfe, 0x05, 0xff, 0x0e, 0xb5, 0xe4, 0x04, 0x64, 0x47, 0x3f,
+ 0x0a, 0x47, 0x9e, 0xe7, 0xcb, 0x79, 0x86, 0x83, 0x4f, 0x66, 0xca, 0xaf,
+ 0xd8, 0x71, 0x85, 0x6e, 0x6c, 0x27, 0xaa, 0x74, 0x79, 0xf3, 0x21, 0x2a,
+ 0xef, 0x27, 0xe0, 0x78, 0x54, 0x53, 0x19, 0xc4, 0xcb, 0x93, 0x4b, 0xcd,
+ 0xc7, 0xa5, 0xea, 0xcb, 0xeb, 0xa7, 0x21, 0x4c, 0x4a, 0x09, 0xf5, 0x68,
+ 0x1f, 0x72, 0xc8, 0x0f, 0xfe, 0xfc, 0xd5, 0xee, 0x98, 0xd4, 0xd4, 0xd5,
+ 0x64, 0x23, 0xfe, 0xcb, 0xb9, 0x88, 0x95, 0x91, 0xa9, 0xce, 0x85, 0x45,
+ 0xe1, 0x2f, 0x92, 0xe1, 0x3b, 0x59, 0xb2, 0x3a, 0x1a, 0x59, 0x88, 0xf2,
+ 0x32, 0xe3, 0x23, 0x73, 0xd7, 0xc3, 0x84, 0x9b, 0x9e, 0xb4, 0xb5, 0x59,
+ 0xe6, 0x53, 0x0c, 0x5f, 0x26, 0x97, 0x29, 0xe4, 0xc9, 0xad, 0x05, 0x50,
+ 0xc0, 0x50, 0x07, 0xcd, 0x2a, 0x11, 0x62, 0xd4, 0xe1, 0xde, 0x20, 0xaa,
+ 0xf5, 0x76, 0x8e, 0x32, 0x53, 0x09, 0x29, 0x5e, 0x0b, 0xf1, 0xf9, 0x31,
+ 0xf0, 0x7b, 0x37, 0x6e, 0x06, 0xb3, 0x25, 0xf3, 0xf8, 0x6d, 0xe0, 0x32,
+ 0x54, 0x74, 0xe9, 0x5a, 0x21, 0xa3, 0xbe, 0x35, 0x15, 0x61, 0xf1, 0x41,
+ 0xc7, 0x4d, 0xc1, 0xe5, 0x1b, 0x0e, 0x36, 0x93, 0x0b, 0x9b, 0x69, 0xf8,
+ 0x1c, 0x0f, 0x4d, 0x09, 0x1d, 0xbc, 0xf6, 0xfe, 0x4a, 0x98, 0x29, 0x3b,
+ 0x00, 0xa5, 0x71, 0xa6, 0xa8, 0xa1, 0x02, 0xf8, 0xbe, 0xb6, 0x41, 0x14,
+ 0xc4, 0x83, 0x94, 0x2e, 0x55, 0x04, 0x04, 0xba, 0xe8, 0x1e, 0xf6, 0x29,
+ 0xd4, 0xa3, 0x3b, 0xa4, 0xaa, 0xd2, 0xea, 0x22, 0xc1, 0xf9, 0x22, 0xfe,
+ 0x14, 0x12, 0x53, 0x22, 0xe7, 0xc6, 0x9c, 0xeb, 0x51, 0xa0, 0x28, 0x0a,
+ 0x24, 0xd7, 0xdc, 0xeb, 0x9b, 0x7f, 0x6e, 0x50, 0x31, 0x23, 0x82, 0xac,
+ 0x92, 0x53, 0xfc, 0xda, 0x16, 0xdf, 0x24, 0x69, 0x89, 0x37, 0x5a, 0xbc,
+ 0xc7, 0xff, 0xda, 0x1b, 0xda, 0xa9, 0xaf, 0x7a, 0x11, 0x9a, 0xb8, 0xf8,
+ 0x32, 0xd6, 0x91, 0xfc, 0xd9, 0x49, 0x25, 0x0b, 0x8a, 0xd7, 0x50, 0xf8,
+ 0xa6, 0xe8, 0x17, 0x1b, 0xa8, 0x7e, 0xf3, 0x9c, 0x82, 0x35, 0x5f, 0xfb,
+ 0x0c, 0x05, 0x8d, 0x50, 0x4b, 0x76, 0xaf, 0xa8, 0x93, 0xda, 0x94, 0x75,
+ 0xbf, 0x4a, 0x33, 0x4e, 0x4d, 0xa7, 0xb4, 0x80, 0xcb, 0xd3, 0xe3, 0xb3,
+ 0x1b, 0xda, 0x95, 0xe1, 0x22, 0x06, 0xe6, 0x97, 0x71, 0x36, 0xe4, 0x79,
+ 0xc9, 0xc0, 0xc9, 0x49, 0xbc, 0x4e, 0x4e, 0xe6, 0x78, 0x07, 0xc4, 0xbc,
+ 0x70, 0xee, 0x8a, 0x7f, 0x3f, 0x65, 0x66, 0x55, 0x6c, 0x83, 0x21, 0x2e,
+ 0xd9, 0x53, 0x50, 0xde, 0x64, 0xef, 0x9a, 0x01, 0x66, 0x3d, 0x1e, 0x92,
+ 0x5b, 0xa2, 0x29, 0xb4, 0x75, 0xd1, 0xc8, 0x0a, 0x63, 0xeb, 0xd6, 0x9e,
+ 0x5b, 0x76, 0x6a, 0x06, 0xf0, 0xfe, 0x56, 0xff, 0x1d, 0xa3, 0xe6, 0x23,
+ 0xed, 0xa1, 0xf9, 0x13, 0xdb, 0x3c, 0x7d, 0x7b, 0x5b, 0x5e, 0x55, 0x24,
+ 0x92, 0x3f, 0x16, 0xd4, 0x84, 0xbf, 0xa0, 0x67, 0x62, 0xfd, 0x69, 0xe8,
+ 0x16, 0x10, 0x43, 0x6d, 0x58, 0xca, 0x78, 0xfd, 0xaa, 0x19, 0x16, 0x00,
+ 0xec, 0x31, 0xd4, 0x56, 0xec, 0x4f, 0x52, 0xaa, 0xf0, 0x6d, 0xae, 0xc2,
+ 0x85, 0xb2, 0x8c, 0xa7, 0x8b, 0xbd, 0xab, 0x65, 0xdd, 0x7a, 0x4c, 0xdf,
+ 0x19, 0x9a, 0x93, 0xf1, 0x19, 0x2c, 0x70, 0xc8, 0xcd, 0x40, 0xe7, 0x13,
+ 0xca, 0x64, 0xc8, 0xed, 0xa4, 0x40, 0x66, 0xed, 0x2e, 0xc7, 0x04, 0x8e,
+ 0xd8, 0xe8, 0xf5, 0x35, 0x72, 0xc1, 0x5f, 0x8c, 0x9d, 0x94, 0xbc, 0xcb,
+ 0x7a, 0xf8, 0x64, 0xf6, 0xc0, 0x8e, 0x7c, 0xa3, 0x2d, 0x93, 0x4d, 0x99,
+ 0x50, 0x71, 0x60, 0xbb, 0x90, 0xba, 0x68, 0x9c, 0x91, 0xff, 0x3d, 0x0a,
+ 0x80, 0x04, 0x74, 0x27, 0x2e, 0x1e, 0x0e, 0x95, 0x01, 0x41, 0xae, 0xa5,
+ 0xb2, 0x5b, 0x95, 0x39, 0x2b, 0x71, 0x97, 0xe1, 0x0e, 0x35, 0x92, 0x5f,
+ 0x25, 0xf3, 0x5a, 0xab, 0xf2, 0x09, 0xd9, 0xcf, 0xe0, 0x04, 0xb8, 0xcf,
+ 0x16, 0x75, 0xb3, 0x27, 0xd7, 0x3a, 0xbc, 0xa4, 0x6c, 0x43, 0xfb, 0x73,
+ 0xa3, 0x90, 0x59, 0xe8, 0x63, 0x81, 0xd3, 0xc0, 0x75, 0xf6, 0x6f, 0x69,
+ 0xb4, 0x8b, 0x36, 0xa6, 0x58, 0xd6, 0x19, 0xde, 0xec, 0x80, 0xd6, 0x39,
+ 0x32, 0x1c, 0x55, 0x2e, 0x31, 0x9c, 0xde, 0x14, 0xd4, 0x73, 0x98, 0xc8,
+ 0xe0, 0x47, 0x36, 0xac, 0x81, 0xac, 0x96, 0x86, 0x4d, 0x04, 0x0b, 0x82,
+ 0xae, 0x64, 0x01, 0x33, 0x4c, 0x6d, 0xba, 0x12, 0xd2, 0xd2, 0x8e, 0x5f,
+ 0xcc, 0x9c, 0xaa, 0xfe, 0xeb, 0x46, 0xae, 0x7f, 0x0d, 0xf4, 0x61, 0x9b,
+ 0xda, 0x2d, 0xb4, 0x68, 0x3e, 0x9d, 0xf2, 0xda, 0x8a, 0xf7, 0x6f, 0x72,
+ 0x64, 0x45, 0x22, 0x13, 0x08, 0x0f, 0x79, 0xa0, 0xe5, 0xa2, 0x4d, 0x0e,
+ 0x3f, 0xf3, 0xc4, 0xe9, 0x0c, 0xd9, 0x48, 0x00, 0x65, 0x2d, 0xd4, 0x50,
+ 0xfd, 0xcd, 0x52, 0xf4, 0xc4, 0x29, 0xe1, 0xa2, 0x6f, 0xf1, 0x7b, 0x4d,
+ 0xf5, 0x30, 0xc9, 0xe7, 0xf4, 0xf3, 0xd2, 0x92, 0xe9, 0xf7, 0x07, 0x6f,
+ 0x8b, 0x59, 0xf5, 0x30, 0xad, 0xcf, 0xd9, 0xfd, 0xb1, 0x1a, 0x17, 0x75,
+ 0x1d, 0x47, 0x21, 0x95, 0x3d, 0x6e, 0xec, 0x80, 0xd0, 0x73, 0x4d, 0xc4,
+ 0x19, 0xb6, 0x72, 0xd3, 0x23, 0xa2, 0x8f, 0x63, 0x07, 0x1f, 0x29, 0xb2,
+ 0x59, 0x1e, 0x2c, 0x50, 0x5d, 0x95, 0x02, 0xf0, 0xb2, 0x56, 0x35, 0xac,
+ 0xda, 0xca, 0xe9, 0xe7, 0xa4, 0xe2, 0x92, 0xab, 0x73, 0x76, 0x85, 0xfc,
+ 0x31, 0x3a, 0xad, 0x14, 0xe8, 0xe2, 0x56, 0x60, 0xc0, 0x7b, 0xb1, 0xb4,
+ 0x6a, 0x13, 0x77, 0xf8, 0x7d, 0x5e, 0xf0, 0x82, 0xf2, 0xab, 0x30, 0xe0,
+ 0x5b, 0x4e, 0xff, 0x55, 0xc5, 0x89, 0x43, 0xbe, 0x8d, 0x96, 0xca, 0x2c,
+ 0xa9, 0x9e, 0x83, 0x1d, 0x61, 0x7a, 0xd3, 0x14, 0xac, 0x28, 0x32, 0xfe,
+ 0x97, 0x7a, 0xd6, 0x08, 0x54, 0xf0, 0xf2, 0x39, 0xb8, 0x0c, 0x95, 0xa6,
+ 0xdd, 0x78, 0xa0, 0x3c, 0xde, 0x30, 0x44, 0xc3, 0xa3, 0x8f, 0xc2, 0xf4,
+ 0xce, 0x3f, 0xda, 0x73, 0xe2, 0x22, 0x4f, 0xd4, 0x46, 0xc1, 0x71, 0x5d,
+ 0xa3, 0x7a, 0x05, 0xb5, 0x35, 0xbb, 0xf2, 0x46, 0x7b, 0xcc, 0xb9, 0x20,
+ 0xc0, 0x35, 0x90, 0x19, 0xba, 0x92, 0x6e, 0xa6, 0x2b, 0x0b, 0x63, 0x1f,
+ 0xed, 0xec, 0x05, 0xed, 0x20, 0xed, 0x07, 0x00, 0xa1, 0x13, 0xb1, 0x36,
+ 0xeb, 0x75, 0xa2, 0x60, 0x01, 0xb0, 0xbc, 0x2c, 0xdc, 0xb4, 0x19, 0x19,
+ 0x39, 0xc1, 0x9e, 0xb5, 0xed, 0xdf, 0x8f, 0xe4, 0x56, 0xec, 0xad, 0xc1,
+ 0xdf, 0x76, 0xce, 0x39, 0x24, 0x01, 0x78, 0x62, 0x07, 0x3a, 0x7c, 0x3b,
+ 0x48, 0x2b, 0xb3, 0xec, 0x34, 0x87, 0xfb, 0x82, 0xae, 0x63, 0x0a, 0x74,
+ 0x26, 0x6a, 0xde, 0xca, 0x06, 0xc1, 0x54, 0x24, 0x29, 0xb5, 0x04, 0x73,
+ 0x1f, 0x03, 0x49, 0x8b, 0xcd, 0x8a, 0xf6, 0xd9, 0xd3, 0x64, 0xd6, 0x20,
+ 0xea, 0x8b, 0x18, 0xf4, 0x9a, 0x6b, 0x05, 0x39, 0x8b, 0x1f, 0xd5, 0x2a,
+ 0xd5, 0x63, 0x63, 0xa7, 0x91, 0xa2, 0x86, 0x80, 0x69, 0xc7, 0xe2, 0x13,
+ 0x29, 0xc0, 0x87, 0x79, 0x3f, 0xa7, 0x94, 0xa6, 0xb8, 0x31, 0x56, 0xd4,
+ 0x45, 0x7e, 0x09, 0xea, 0x48, 0x19, 0xcc, 0xa5, 0xca, 0x25, 0x14, 0x7b,
+ 0x6d, 0xb2, 0x88, 0x19, 0x5a, 0xa0, 0x79, 0x15, 0x1a, 0xb1, 0xa2, 0x24,
+ 0x49, 0xda, 0x70, 0x89, 0x1c, 0x98, 0x64, 0x46, 0xeb, 0x2b, 0x66, 0xcf,
+ 0xca, 0x34, 0x01, 0x8e, 0xb9, 0x7e, 0x60, 0xf2, 0x7a, 0x60, 0x40, 0xdd,
+ 0x38, 0x11, 0xe3, 0xb8, 0x5e, 0x25, 0xa2, 0x69, 0xde, 0x42, 0x9b, 0xd1,
+ 0x98, 0xfb, 0x28, 0x56, 0xa1, 0xc5, 0x23, 0xd1, 0x36, 0x34, 0xa7, 0xde,
+ 0x08, 0xd4, 0x58, 0xe9, 0x30, 0x98, 0x85, 0x87, 0xaf, 0xca, 0x90, 0xc7,
+ 0xfa, 0x01, 0x34, 0xdd, 0x98, 0xc2, 0x85, 0xf4, 0xfd, 0xeb, 0xfa, 0x75,
+ 0x5f, 0xa5, 0xb1, 0x10, 0xae, 0xff, 0xfd, 0xba, 0x76, 0xaf, 0xfe, 0x24,
+ 0xda, 0x11, 0x6a, 0x11, 0x2d, 0x6d, 0x62, 0x63, 0xc5, 0x8d, 0x5f, 0x44,
+ 0xfe, 0x4f, 0xa0, 0xe8, 0x8c, 0x5b, 0x9c, 0x5d, 0xfe, 0xf8, 0xec, 0x2f,
+ 0x5f, 0x9b, 0x7a, 0x3c, 0x0e, 0x05, 0x4d, 0x57, 0xa2, 0x89, 0x73, 0xc5,
+ 0x24, 0x41, 0xf4, 0x3a, 0x22, 0x1b, 0xc7, 0x7f, 0xfb, 0xba, 0xd1, 0x74,
+ 0xfc, 0x4f, 0xaf, 0xdb, 0x2f, 0xc3, 0xf8, 0x4f, 0x00, 0xb3, 0x6a, 0x25,
+ 0xb6, 0xf7, 0x4a, 0xe1, 0x84, 0xfa, 0x29, 0xf8, 0x31, 0x4a, 0x3f, 0x06,
+ 0xfe, 0xc1, 0x82, 0x90, 0xe0, 0x5c, 0x07, 0xfb, 0xbb, 0x87, 0xb9, 0x91,
+ 0xc6, 0x21, 0x10, 0x4b, 0x80, 0x0a, 0xf6, 0xa7, 0xdc, 0x5c, 0x7f, 0xa9,
+ 0x62, 0x33, 0x95, 0x43, 0x2f, 0x17, 0xbd, 0x51, 0x6e, 0x4b, 0x21, 0x0a,
+ 0x02, 0x15, 0xc8, 0xef, 0x2f, 0xa7, 0xb0, 0xf7, 0x1e, 0xae, 0xce, 0xe3,
+ 0xc4, 0xe6, 0x08, 0x9f, 0x6a, 0xd7, 0x5a, 0x70, 0x80, 0xdf, 0x3a, 0xff,
+ 0x6d, 0xa5, 0xaa, 0xde, 0xfc, 0xe9, 0x3e, 0x14, 0xb6, 0x20, 0xd6, 0x95,
+ 0x61, 0xb4, 0xfe, 0xec, 0x39, 0x59, 0xd8, 0x43, 0xa9, 0x27, 0x11, 0xd6,
+ 0xe8, 0x75, 0xff, 0x9a, 0xc2, 0x64, 0xa7, 0x76, 0x2b, 0xf0, 0xb5, 0x0d,
+ 0xa6, 0x7d, 0xdf, 0xe7, 0xb8, 0xdf, 0x9e, 0xb2, 0x38, 0xab, 0x57, 0xcf,
+ 0x41, 0x1a, 0x6f, 0xc2, 0x1c, 0x86, 0xd9, 0x58, 0x19, 0x04, 0xe9, 0xbe,
+ 0x6b, 0xb9, 0x96, 0xb4, 0x8d, 0x50, 0x6b, 0x59, 0x67, 0xc6, 0xa6, 0xf5,
+ 0x6b, 0x2c, 0x89, 0xc1, 0x4e, 0x7c, 0x3d, 0x9e, 0x99, 0xc6, 0xf2, 0x75,
+ 0xd2, 0x7d, 0x4c, 0x0a, 0x9a, 0x26, 0xc1, 0x56, 0x66, 0x25, 0x24, 0xdc,
+ 0x42, 0xd8, 0xc6, 0xb2, 0xd0, 0xe1, 0xcd, 0x07, 0x29, 0xbe, 0x92, 0x26,
+ 0x26, 0x35, 0xed, 0x2f, 0xea, 0xb4, 0x52, 0xfe, 0xcc, 0x1a, 0xdf, 0xd2,
+ 0xe8, 0xde, 0x59, 0x77, 0x55, 0xe5, 0x26, 0xae, 0x38, 0x9e, 0xf2, 0xf3,
+ 0x65, 0x72, 0xfc, 0x25, 0xa8, 0xfb, 0x22, 0xda, 0x9a, 0x24, 0x8f, 0x73,
+ 0xc9, 0xcc, 0x2d, 0x62, 0xc1, 0xa5, 0x97, 0x49, 0x2e, 0x02, 0xb4, 0x05,
+ 0x13, 0x84, 0x02, 0x2a, 0xf4, 0x53, 0x60, 0x87, 0x82, 0x73, 0xfc, 0x6e,
+ 0x91, 0x1a, 0x3c, 0x28, 0xaf, 0x3e, 0xd5, 0xc8, 0xfe, 0x11, 0x39, 0x38,
+ 0x4c, 0x1e, 0xeb, 0x45, 0x3e, 0xca, 0x79, 0x91, 0x47, 0x89, 0x8d, 0xaf,
+ 0xb4, 0xea, 0xfe, 0xd7, 0x4d, 0x4f, 0x44, 0x06, 0x3e, 0x69, 0xa5, 0x1b,
+ 0x9a, 0x58, 0x2f, 0x68, 0xc1, 0x98, 0xd8, 0xe4, 0xfa, 0xac, 0xbd, 0x74,
+ 0x9e, 0x7a, 0x04, 0xa7, 0x17, 0x57, 0xca, 0x3e, 0x37, 0x17, 0x20, 0xd9,
+ 0x6a, 0x8e, 0x48, 0xec, 0x0f, 0x8a, 0x20, 0x9b, 0x1f, 0xa4, 0x6b, 0xbc,
+ 0xf2, 0x51, 0xac, 0xd6, 0x51, 0xb7, 0x36, 0x6b, 0x0d, 0x63, 0x1d, 0x0e,
+ 0x30, 0xd1, 0xd3, 0x87, 0xbb, 0x77, 0xd5, 0xa7, 0xdd, 0x7b, 0xcd, 0xf0,
+ 0xe7, 0x53, 0xe2, 0xc9, 0x7f, 0x9b, 0xaa, 0x7c, 0x50, 0x86, 0x40, 0x8e,
+ 0x4e, 0xeb, 0xf6, 0x4c, 0x86, 0xe7, 0xd9, 0xf9, 0xec, 0x20, 0x8b, 0x14,
+ 0xef, 0xf1, 0xce, 0xc9, 0xa0, 0x45, 0x8a, 0x1d, 0x80, 0x92, 0xed, 0xe8,
+ 0xbc, 0xcf, 0x61, 0xcc, 0x0a, 0x10, 0x95, 0x85, 0x33, 0x79, 0x9e, 0xdb,
+ 0x19, 0x4d, 0xac, 0x58, 0xa9, 0x24, 0x12, 0x41, 0x52, 0x97, 0x55, 0x71,
+ 0x9f, 0x48, 0xa3, 0xec, 0xb4, 0x72, 0xee, 0xa2, 0xe3, 0xd9, 0x32, 0x09,
+ 0x2d, 0x29, 0x88, 0x3f, 0x25, 0x4f, 0xfd, 0xa0, 0x21, 0xf1, 0x07, 0xb0,
+ 0xd4, 0x18, 0x9e, 0x36, 0xd1, 0xbd, 0xb7, 0xd5, 0x1c, 0xb2, 0x6a, 0x4c,
+ 0x5b, 0x1d, 0x33, 0x03, 0x33, 0x4e, 0x8f, 0x18, 0xfd, 0x72, 0x9e, 0x2e,
+ 0x70, 0xec, 0x71, 0x18, 0x61, 0x2a, 0xaa, 0xb8, 0x56, 0x03, 0x04, 0x69,
+ 0x33, 0xa9, 0x57, 0x57, 0xdc, 0x0d, 0xfd, 0x46, 0x0d, 0xba, 0x40, 0x77,
+ 0xf5, 0x80, 0xb7, 0x0f, 0x86, 0xb1, 0x1c, 0x2f, 0x83, 0xb1, 0xcd, 0x32,
+ 0x5f, 0x77, 0x9f, 0x70, 0x16, 0x8d, 0x6a, 0x6d, 0x32, 0xc7, 0xae, 0x01,
+ 0x20, 0x9f, 0x93, 0xbf, 0xb6, 0xd4, 0x2d, 0xdf, 0x4f, 0x72, 0x18, 0x7d,
+ 0x87, 0x3b, 0xde, 0xe1, 0x55, 0xbe, 0xb0, 0xa1, 0x3d, 0xf5, 0x67, 0xcf,
+ 0x26, 0x0b, 0x96, 0x4e, 0x38, 0xca, 0xf3, 0x9c, 0xf9, 0x74, 0x7b, 0x4f,
+ 0x37, 0x39, 0x9f, 0x1d, 0x13, 0xde, 0x5a, 0xe2, 0x70, 0x31, 0x51, 0x96,
+ 0x0f, 0xb8, 0xf3, 0x74, 0x06, 0x99, 0xd1, 0x34, 0x94, 0xee, 0xb6, 0x01,
+ 0x23, 0x07, 0x7c, 0xb4, 0x9a, 0x0c, 0x9e, 0x8c, 0xf0, 0x54, 0xb6, 0x83,
+ 0x00, 0x63, 0xea, 0x30, 0x8c, 0xdc, 0xcc, 0x48, 0xb1, 0xcc, 0xb1, 0xe6,
+ 0x90, 0xf9, 0x40, 0x8e, 0xf8, 0x7b, 0x84, 0x94, 0x6f, 0x1c, 0x8d, 0xbd,
+ 0x73, 0xed, 0x83, 0xcd, 0xfc, 0x30, 0xac, 0x03, 0xd7, 0x94, 0x62, 0x4c,
+ 0xaa, 0x79, 0xfd, 0xcc, 0x64, 0x3c, 0x62, 0x47, 0x51, 0xfb, 0x18, 0x9f,
+ 0x76, 0x14, 0x7e, 0xb4, 0xce, 0x47, 0x23, 0x5f, 0x06, 0x02, 0x89, 0x37,
+ 0x80, 0x88, 0xc6, 0xab, 0x89, 0x5c, 0xc5, 0xf5, 0x4d, 0x2a, 0x13, 0x63,
+ 0xc4, 0xfa, 0x2d, 0xb1, 0x07, 0x2b, 0xe3, 0x19, 0x63, 0x98, 0xbe, 0x5d,
+ 0x08, 0x8b, 0x8e, 0x70, 0xa7, 0x5d, 0x55, 0x7b, 0xa6, 0x87, 0x80, 0xaf,
+ 0x2f, 0x6e, 0x14, 0xb8, 0x8c, 0xff, 0xff, 0x8c, 0x4e, 0x3e, 0xdc, 0x0d,
+ 0x06, 0x65, 0x72, 0x24, 0xc5, 0x28, 0x9f, 0xd4, 0xb1, 0x3d, 0xab, 0xf4,
+ 0x13, 0x90, 0x54, 0xed, 0x89, 0xa2, 0xea, 0x0a, 0x9c, 0x7a, 0x25, 0x93,
+ 0xef, 0x69, 0x9a, 0x3f, 0x38, 0x68, 0xbc, 0xfe, 0x2c, 0x37, 0x85, 0xaa,
+ 0x21, 0xbd, 0x37, 0x09, 0x22, 0x37, 0xf2, 0x36, 0xe3, 0x05, 0x37, 0x1b,
+ 0xf3, 0x9a, 0x5a, 0x5c, 0xf2, 0xfe, 0x44, 0x7b, 0x36, 0xee, 0x3e, 0x2f,
+ 0xbb, 0x60, 0x6e, 0x6d, 0x21, 0x4e, 0x15, 0x92, 0xf6, 0x1f, 0x9b, 0x32,
+ 0x83, 0x58, 0x8c, 0x6e, 0x9c, 0xa2, 0xab, 0x4e, 0x61, 0xae, 0xc2, 0xc6,
+ 0x98, 0x14, 0x1a, 0x96, 0x41, 0xa0, 0x0f, 0x70, 0x4c, 0xfd, 0xb9, 0x1a,
+ 0xb1, 0x53, 0x9c, 0xdf, 0x6a, 0xab, 0xe3, 0xa4, 0x69, 0x23, 0xd7, 0xb1,
+ 0x2d, 0x40, 0xd1, 0x38, 0xb8, 0x6a, 0xbc, 0xe5, 0x33, 0xd3, 0x04, 0x37,
+ 0x6a, 0xac, 0x28, 0xf0, 0x0e, 0xeb, 0x4f, 0xc4, 0x75, 0xef, 0x36, 0xf9,
+ 0xaf, 0xfe, 0x1a, 0xd0, 0xcf, 0x99, 0x54, 0xb7, 0xd5, 0x69, 0xb2, 0xff,
+ 0x83, 0xa0, 0xe8, 0x0b, 0xef, 0x1e, 0x47, 0x8b, 0x72, 0xb2, 0xd3, 0x97,
+ 0x0b, 0xc8, 0x55, 0x5b, 0x7d, 0x66, 0x4d, 0xa4, 0x42, 0x61, 0xf7, 0x79,
+ 0x6c, 0x10, 0x22, 0x81, 0x41, 0x44, 0x91, 0x65, 0x29, 0x26, 0x17, 0x34,
+ 0x29, 0xb2, 0x39, 0xc0, 0xe2, 0x7b, 0xf9, 0xea, 0xc3, 0x15, 0xe7, 0xf4,
+ 0x19, 0x9e, 0x83, 0x84, 0xcb, 0x7d, 0x20, 0xae, 0x3a, 0xdf, 0xd4, 0x5d,
+ 0x30, 0x6a, 0xf3, 0x01, 0xe7, 0xa5, 0xe4, 0x99, 0x3d, 0x7e, 0xef, 0x57,
+ 0x0a, 0x53, 0xe9, 0x01, 0x93, 0x85, 0x24, 0x73, 0x56, 0x46, 0xbb, 0x4c,
+ 0x00, 0x12, 0xd5, 0xb3, 0xe8, 0x19, 0xfe, 0xc9, 0x23, 0xe0, 0xd4, 0xe4,
+ 0x59, 0x77, 0xc8, 0xca, 0x66, 0xde, 0x4e, 0x3e, 0xd4, 0xcb, 0x02, 0x6e,
+ 0x76, 0xe7, 0x51, 0x9e, 0x70, 0x2e, 0x77, 0xd9, 0xa6, 0xcd, 0x99, 0xf6,
+ 0xaf, 0x17, 0x4b, 0xfb, 0x6d, 0x61, 0x5d, 0x78, 0xb6, 0x42, 0x83, 0x6b,
+ 0x21, 0x83, 0x51, 0x91, 0x69, 0x27, 0x8c, 0xc6, 0xd7, 0x4a, 0xe7, 0xf6,
+ 0x00, 0x06, 0x17, 0x5b, 0xa5, 0x99, 0x87, 0x12, 0x38, 0x6e, 0x3a, 0x37,
+ 0xee, 0xad, 0x39, 0x97, 0xa8, 0x54, 0x93, 0x14, 0x13, 0xf4, 0x85, 0x34,
+ 0xfb, 0xea, 0xa6, 0xca, 0xaa, 0x57, 0x76, 0xd7, 0x05, 0x26, 0xc5, 0x32,
+ 0x7c, 0x7f, 0x52, 0xf7, 0x96, 0xec, 0x38, 0xc3, 0x77, 0x0d, 0x07, 0xde,
+ 0xe9, 0x83, 0x1f, 0x2c, 0x8a, 0x82, 0xac, 0x73, 0x10, 0x66, 0x7e, 0x44,
+ 0x16, 0x99, 0x05, 0xf7, 0x68, 0xc2, 0x5b, 0xd5, 0xa8, 0xb8, 0xac, 0xdd,
+ 0x98, 0x16, 0x21, 0xc3, 0xa5, 0xbc, 0xbe, 0xa9, 0xc2, 0xbf, 0xb0, 0x2d,
+ 0xf1, 0x31, 0xcd, 0x1a, 0x0b, 0x31, 0x90, 0xbf, 0x22, 0x94, 0x60, 0x36,
+ 0x86, 0xf6, 0x3d, 0x0e, 0x24, 0xea, 0x1a, 0xdc, 0x8b, 0x09, 0x83, 0xc2,
+ 0xf3, 0x80, 0x9c, 0xc1, 0x39, 0x9a, 0x6f, 0x65, 0xcf, 0x64, 0x9e, 0xa9,
+ 0x73, 0x87, 0x64, 0xce, 0xd4, 0xc2, 0xde, 0x8f, 0x25, 0x8a, 0x83, 0xa1,
+ 0xfa, 0xf5, 0x48, 0xd2, 0xbd, 0xc1, 0x4f, 0x58, 0x9b, 0x89, 0x12, 0x38,
+ 0x90, 0x54, 0xaf, 0xd4, 0x35, 0x6a, 0xa2, 0x42, 0x54, 0xdb, 0xb4, 0xa5,
+ 0xae, 0xd6, 0xd6, 0xbd, 0xe7, 0x4f, 0xda, 0xc8, 0xce, 0xd8, 0x24, 0x62,
+ 0xb6, 0x0a, 0x80, 0x2b, 0x41, 0x30, 0xfe, 0x14, 0x79, 0xa1, 0xaa, 0xb2,
+ 0xfa, 0xa8, 0x74, 0x1e, 0x22, 0x77, 0x1e, 0x05, 0xaf, 0xb0, 0x32, 0xce,
+ 0xba, 0x1b, 0xb5, 0xb5, 0x10, 0xab, 0x59, 0x7a, 0x90, 0x4e, 0xd4, 0x89,
+ 0x62, 0x5f, 0xcc, 0x0e, 0x32, 0x29, 0x17, 0x24, 0x41, 0x94, 0x27, 0x9f,
+ 0xff, 0x8a, 0xe4, 0x01, 0xb7, 0xc5, 0xdb, 0x94, 0xaf, 0x84, 0x3a, 0xcc,
+ 0x13, 0x50, 0x25, 0xb1, 0x2a, 0x87, 0x2b, 0x32, 0x84, 0x56, 0x0c, 0xea,
+ 0xda, 0xf4, 0x6d, 0xb2, 0xf1, 0x5c, 0xed, 0xfc, 0x17, 0x6a, 0x9e, 0x1b,
+ 0x6b, 0xe3, 0x54, 0x3c, 0x05, 0xfa, 0x92, 0x73, 0x6d, 0x9b, 0xfe, 0xf8,
+ 0x7d, 0x7a, 0xfb, 0x95, 0x94, 0xe0, 0x30, 0xad, 0x7c, 0x20, 0xa6, 0xc4,
+ 0x9b, 0x9e, 0xd3, 0xe8, 0x76, 0x64, 0xea, 0x9b, 0x16, 0x08, 0x49, 0xab,
+ 0x26, 0xa3, 0xd4, 0xbe, 0x16, 0x15, 0x28, 0x79, 0x12, 0x2f, 0x2c, 0x64,
+ 0xa3, 0x1e, 0x12, 0xf1, 0xcd, 0xed, 0x88, 0xb1, 0x37, 0xc0, 0x4f, 0x4d,
+ 0x8d, 0xe4, 0xc9, 0x00, 0x00, 0x00, 0x82, 0x74, 0x71, 0xaf, 0xff, 0xff,
+ 0xfd, 0x85, 0x62, 0x25, 0xb6, 0x91, 0x1e, 0x44, 0x9b, 0x4b, 0x3b, 0xd9,
+ 0x55, 0x21, 0xb7, 0x25, 0x79, 0x39, 0x5f, 0xb1, 0x02, 0x3d, 0xe4, 0xb1,
+ 0x75, 0x32, 0x03, 0x0b, 0x95, 0xc9, 0xbd, 0x37, 0xe4, 0x8a, 0x26, 0xd7,
+ 0x3b, 0x1b, 0xed, 0x05, 0xee, 0x89, 0xba, 0xa3, 0xd2, 0x6f, 0xd3, 0x65,
+ 0x92, 0x56, 0x16, 0x29, 0x49, 0xb0, 0x10, 0x82, 0x17, 0xa1, 0x06, 0xc7,
+ 0xfc, 0xbb, 0x75, 0xaa, 0xfc, 0xd7, 0x68, 0x62, 0x85, 0x4e, 0xb7, 0x41,
+ 0x10, 0x45, 0xb1, 0x5f, 0xef, 0x48, 0xf7, 0x1e, 0x6c, 0x2d, 0x26, 0xa5,
+ 0x99, 0xd3, 0x6d, 0xae, 0x52, 0xc4, 0x0a, 0xd1, 0x14, 0x1b, 0x02, 0x7f,
+ 0xd8, 0x16, 0xeb, 0xcd, 0x59, 0x7f, 0xec, 0x5e, 0xc6, 0x94, 0x22, 0xe1,
+ 0xcd, 0x09, 0x86, 0x0a, 0x0d, 0x73, 0xed, 0x09, 0x97, 0x88, 0x3c, 0x0e,
+ 0x54, 0x9b, 0xff, 0x77, 0x41, 0x32, 0x9e, 0x68, 0x3d, 0xcf, 0x8b, 0xda,
+ 0x47, 0x75, 0x8b, 0x03, 0xb9, 0x62, 0x81, 0xa5, 0xce, 0x37, 0xa3, 0x3f,
+ 0x3f, 0x87, 0xfe, 0x1b, 0xdd, 0x2a, 0xdb, 0x1c, 0x98, 0x41, 0x28, 0xd0,
+ 0xac, 0xe1, 0x9f, 0x80, 0xb3, 0x4c, 0x4b, 0xe9, 0x2b, 0xa9, 0xdd, 0x25,
+ 0x25, 0x14, 0x7c, 0xe9, 0x5f, 0x89, 0x91, 0x6b, 0xec, 0x6f, 0xf7, 0x8c,
+ 0xab, 0x8c, 0x8a, 0x28, 0x26, 0x79, 0x36, 0xbf, 0xbb, 0xfe, 0x4d, 0x46,
+ 0x59, 0x75, 0x98, 0xb3, 0x37, 0xe5, 0x1b, 0x3d, 0x4c, 0xdc, 0xf4, 0x9d,
+ 0x7e, 0xdc, 0xdf, 0x73, 0x47, 0x94, 0xcd, 0x32, 0x4a, 0x2a, 0x31, 0x6f,
+ 0x91, 0x92, 0x31, 0xba, 0xbc, 0xc2, 0xfb, 0x54, 0xb9, 0x0e, 0x61, 0x1a,
+ 0x91, 0xa7, 0x83, 0x0f, 0xee, 0xc7, 0x72, 0xd6, 0x69, 0x1b, 0x5a, 0xf7,
+ 0x8e, 0x5e, 0x7f, 0xc2, 0xfe, 0x3a, 0xb3, 0x7d, 0x1d, 0x91, 0x44, 0xa8,
+ 0x29, 0x05, 0x8f, 0x11, 0x23, 0x9b, 0xda, 0xf0, 0xc8, 0x6c, 0xf2, 0x43,
+ 0x81, 0x70, 0xb4, 0x98, 0x84, 0xa7, 0x83, 0x0b, 0x14, 0x22, 0xfb, 0xe9,
+ 0x16, 0x96, 0xbc, 0xe2, 0x47, 0x33, 0xcc, 0x3a, 0x62, 0xf9, 0xc9, 0xdf,
+ 0x89, 0x65, 0xcd, 0x61, 0xfe, 0x7a, 0x56, 0xe5, 0xd1, 0x5a, 0x09, 0x64,
+ 0xbf, 0x7b, 0x29, 0x58, 0x1b, 0x25, 0x71, 0x7e, 0xfb, 0x19, 0x00, 0x4c,
+ 0x85, 0x74, 0xe2, 0xe0, 0x9b, 0x59, 0x1b, 0x84, 0xee, 0xe2, 0x42, 0x75,
+ 0xc3, 0x76, 0x29, 0xf3, 0x79, 0x0f, 0xe0, 0x4a, 0x42, 0xbd, 0x28, 0x70,
+ 0xc9, 0x86, 0x6e, 0x8f, 0xfe, 0x28, 0xa4, 0xe8, 0x97, 0xeb, 0xaa, 0xb2,
+ 0x3e, 0x04, 0x95, 0xd8, 0x7f, 0x4e, 0x49, 0x7a, 0x95, 0x9a, 0xea, 0xd2,
+ 0x92, 0x98, 0xcd, 0xe2, 0x6b, 0x6d, 0xf0, 0xd1, 0xb2, 0xae, 0x52, 0x6e,
+ 0x9b, 0xce, 0x24, 0xa1, 0x66, 0x52, 0x66, 0xd1, 0x6b, 0xbd, 0xb3, 0xc7,
+ 0x47, 0x91, 0x30, 0x18, 0x13, 0xd0, 0xa7, 0xe3, 0x61, 0xd7, 0x0c, 0x51,
+ 0x35, 0x6f, 0xb6, 0xe8, 0xfe, 0xf9, 0x50, 0x24, 0xc0, 0x56, 0x8b, 0x18,
+ 0xe3, 0xae, 0x4e, 0x1b, 0x08, 0x8e, 0x09, 0xa2, 0x1c, 0xeb, 0x97, 0x18,
+ 0xe3, 0x0c, 0x4b, 0x62, 0xf3, 0xea, 0x76, 0xf8, 0x82, 0xb9, 0xc1, 0xc2,
+ 0x5a, 0x92, 0x42, 0x2d, 0x2d, 0x04, 0xef, 0x06, 0x86, 0x8f, 0xb9, 0xda,
+ 0x0a, 0x31, 0x3c, 0x14, 0xf5, 0x01, 0x6e, 0x88, 0xc5, 0x70, 0xeb, 0xaf,
+ 0xad, 0x33, 0x21, 0x18, 0xe8, 0xbd, 0xe9, 0x32, 0x63, 0x0a, 0xe8, 0xf0,
+ 0xfe, 0xdc, 0x14, 0x4d, 0x5a, 0x9d, 0xc6, 0xd4, 0xad, 0x24, 0x1f, 0x3c,
+ 0xe5, 0xdf, 0x14, 0x1d, 0xd4, 0x32, 0xc5, 0x25, 0x78, 0xbf, 0x67, 0x84,
+ 0x37, 0x51, 0x59, 0x41, 0xb9, 0x65, 0x22, 0xf1, 0x44, 0xfe, 0x12, 0x60,
+ 0xab, 0x47, 0x00, 0x79, 0x50, 0x0a, 0x3a, 0x26, 0x2b, 0x92, 0xb7, 0x0b,
+ 0xf0, 0x6a, 0xee, 0xc5, 0x41, 0x18, 0x98, 0x90, 0xe0, 0xd5, 0x1c, 0xef,
+ 0x78, 0xa5, 0x6f, 0xeb, 0x13, 0xb5, 0x7f, 0xa6, 0xeb, 0x06, 0x6c, 0xf1,
+ 0xad, 0x44, 0xaf, 0x8b, 0xc2, 0x74, 0x9b, 0x39, 0x99, 0xda, 0xee, 0xfe,
+ 0x07, 0x54, 0x9f, 0xc3, 0x05, 0x5e, 0x2d, 0x0a, 0x13, 0x1c, 0x50, 0x90,
+ 0x43, 0x44, 0x5a, 0x6c, 0x34, 0xe5, 0xec, 0x71, 0x4e, 0x37, 0x5c, 0xa4,
+ 0xa9, 0x84, 0x48, 0x80, 0x3d, 0xb6, 0x69, 0xa2, 0x84, 0x2d, 0xe7, 0xbd,
+ 0x05, 0x9d, 0xd7, 0x40, 0x05, 0xf6, 0x69, 0x06, 0xf5, 0x16, 0x3f, 0xed,
+ 0x09, 0xa0, 0x3e, 0x5b, 0x5c, 0x61, 0x7c, 0x6b, 0xc6, 0xb8, 0x77, 0x77,
+ 0x24, 0x12, 0x72, 0xfc, 0xf3, 0xf7, 0x31, 0xce, 0x04, 0xe9, 0xd7, 0x5a,
+ 0x84, 0x2e, 0xaf, 0x69, 0x04, 0x58, 0x38, 0x4b, 0xf4, 0x20, 0x3e, 0xf6,
+ 0x25, 0x89, 0x5f, 0xb3, 0x02, 0x72, 0x59, 0x4b, 0x82, 0x9f, 0x98, 0x3a,
+ 0x09, 0x32, 0xb6, 0x94, 0xdc, 0x52, 0xbb, 0xe0, 0x5f, 0xef, 0xd2, 0x7e,
+ 0x5b, 0x13, 0x9d, 0x96, 0x12, 0x10, 0xbf, 0x01, 0xb4, 0x31, 0xcc, 0xfd,
+ 0xee, 0x27, 0x9c, 0xdc, 0x56, 0xae, 0x9a, 0x94, 0xb9, 0xde, 0xcf, 0xf4,
+ 0x39, 0x80, 0x8f, 0xbe, 0x3e, 0xc9, 0x94, 0xe5, 0xaa, 0xb7, 0x77, 0xd0,
+ 0xb7, 0xaf, 0x83, 0xee, 0x3c, 0xa3, 0xe2, 0x22, 0xd8, 0xf1, 0x48, 0xcd,
+ 0xfe, 0x72, 0x2f, 0x7e, 0x36, 0xe9, 0x5e, 0xb8, 0x27, 0x05, 0xe6, 0x18,
+ 0xb9, 0x5d, 0x78, 0x5e, 0x01, 0x59, 0xd1, 0xe8, 0x3f, 0x5a, 0xf9, 0xaf,
+ 0x93, 0x68, 0x9f, 0x8e, 0xdd, 0xc6, 0xcb, 0x7c, 0xbe, 0x1e, 0x51, 0x9e,
+ 0x64, 0x48, 0xd0, 0x13, 0xfb, 0x63, 0xfe, 0xa5, 0x5c, 0xcb, 0x79, 0xb6,
+ 0xc7, 0xac, 0xe6, 0x89, 0xb9, 0x08, 0x8a, 0xb6, 0x3e, 0x63, 0xf2, 0x17,
+ 0x2c, 0x34, 0xa4, 0x30, 0xb7, 0x06, 0x41, 0x7c, 0x04, 0xa8, 0xf9, 0xe5,
+ 0x5b, 0x6f, 0x5a, 0x9d, 0x73, 0x5a, 0x60, 0x7a, 0x60, 0x1f, 0x7c, 0x32,
+ 0x5a, 0xa3, 0xb1, 0xc7, 0xf8, 0x20, 0x91, 0xb1, 0x85, 0xb8, 0xd3, 0x3a,
+ 0xd3, 0x7e, 0x55, 0x60, 0xba, 0x46, 0x43, 0x8c, 0x46, 0xdf, 0xa9, 0x3e,
+ 0xb8, 0x13, 0x52, 0x2c, 0x5c, 0xad, 0x4d, 0x26, 0x54, 0x87, 0x63, 0x3c,
+ 0x9a, 0x12, 0x95, 0x4c, 0x21, 0x28, 0xbc, 0xc5, 0xa1, 0x24, 0xc0, 0x11,
+ 0x9d, 0x28, 0x50, 0xb2, 0x10, 0xd9, 0x7f, 0x69, 0xaf, 0xc0, 0x14, 0xe6,
+ 0xeb, 0x73, 0xfc, 0xb8, 0x90, 0xb5, 0x06, 0x49, 0xc9, 0x73, 0x99, 0xfb,
+ 0x6e, 0x4d, 0x85, 0x0f, 0x1c, 0x76, 0x5b, 0x9e, 0x67, 0x22, 0x3a, 0x9e,
+ 0x85, 0xf6, 0x9f, 0x76, 0xe7, 0x55, 0xeb, 0xe6, 0xba, 0x16, 0xa4, 0x7a,
+ 0x73, 0xa7, 0xe1, 0xd9, 0xf9, 0x3c, 0x2f, 0xc1, 0x48, 0x46, 0xc2, 0x0a,
+ 0x0a, 0xd3, 0xd2, 0x59, 0x7c, 0x6e, 0xaf, 0xf0, 0xa5, 0x28, 0x62, 0xd1,
+ 0xb3, 0xd3, 0xe9, 0xf1, 0xcc, 0x2c, 0x1c, 0x04, 0xe6, 0x92, 0x2b, 0xce,
+ 0x8f, 0xbd, 0xce, 0x1f, 0x55, 0xc3, 0x29, 0xad, 0x24, 0x01, 0xfa, 0x3f,
+ 0xba, 0x88, 0x35, 0x74, 0x53, 0x56, 0xda, 0x50, 0x68, 0x66, 0x66, 0xb9,
+ 0xe2, 0x70, 0x70, 0x68, 0x1b, 0x6b, 0x16, 0xc4, 0xef, 0x8e, 0x58, 0x10,
+ 0x21, 0x37, 0x54, 0x1a, 0x23, 0xa3, 0x1e, 0x39, 0x56, 0xb3, 0x5a, 0x18,
+ 0x38, 0xf1, 0xef, 0xb4, 0x9a, 0xdc, 0x00, 0x0c, 0xcf, 0xcc, 0x0b, 0xc2,
+ 0xf1, 0xea, 0x05, 0xf7, 0xd9, 0xe8, 0xe1, 0xeb, 0x16, 0x28, 0x7f, 0x6d,
+ 0x0d, 0x2c, 0xfb, 0x8e, 0x71, 0x41, 0x99, 0xe8, 0x53, 0x1b, 0x4f, 0xbf,
+ 0xff, 0x1d, 0x4b, 0xfd, 0x83, 0xe1, 0x2a, 0x58, 0x96, 0xe8, 0x31, 0x55,
+ 0xd2, 0xb8, 0x39, 0xd8, 0x18, 0x26, 0x91, 0xfb, 0xac, 0x76, 0xbd, 0x52,
+ 0x65, 0xac, 0x37, 0xef, 0x3c, 0xde, 0x2f, 0xb0, 0x76, 0x55, 0xd4, 0xeb,
+ 0xd1, 0x1b, 0x1a, 0x3f, 0x43, 0x22, 0x35, 0x78, 0x43, 0x45, 0xb5, 0xaf,
+ 0x9b, 0x04, 0x11, 0x24, 0x4f, 0xe8, 0xde, 0xdd, 0x03, 0xd0, 0x5e, 0x50,
+ 0xd6, 0x1f, 0x7c, 0x4a, 0xc4, 0x9d, 0xb0, 0x9c, 0x85, 0x09, 0xb1, 0x89,
+ 0x18, 0x7f, 0x4a, 0x85, 0x9f, 0x7d, 0x2a, 0xfe, 0x7a, 0xb7, 0xfa, 0x66,
+ 0xd3, 0xe4, 0x28, 0x91, 0xea, 0xb7, 0xd9, 0x8a, 0x11, 0x8f, 0x6c, 0x25,
+ 0x65, 0x1a, 0x99, 0xb2, 0x0b, 0x4e, 0x20, 0x97, 0x9b, 0x43, 0x44, 0x6c,
+ 0xc0, 0xe6, 0x81, 0x76, 0x95, 0xd9, 0xd8, 0x01, 0x3c, 0xc9, 0x6f, 0x1f,
+ 0xf3, 0xfa, 0x0f, 0x13, 0x1b, 0x1f, 0xaa, 0x84, 0x5e, 0xbe, 0xec, 0x4e,
+ 0xb3, 0x2e, 0x0b, 0x16, 0x41, 0x2d, 0xfb, 0xb9, 0x9b, 0x59, 0xb7, 0x8d,
+ 0xd9, 0x61, 0xa3, 0x88, 0x7f, 0xc2, 0x4d, 0x87, 0x65, 0xc0, 0xf1, 0x67,
+ 0xf7, 0xc0, 0x0d, 0xc5, 0xcd, 0x56, 0x50, 0xc3, 0x93, 0xd7, 0x34, 0xb1,
+ 0xae, 0xb4, 0x47, 0x7b, 0xf4, 0x3d, 0x55, 0x99, 0xb7, 0x92, 0xc5, 0x34,
+ 0x94, 0x7b, 0x76, 0x1f, 0x26, 0x70, 0x0a, 0xce, 0xa8, 0xd7, 0x9f, 0x1d,
+ 0x63, 0x46, 0xcd, 0x0a, 0x36, 0x0b, 0x8c, 0x98, 0xd9, 0xc4, 0x1b, 0x16,
+ 0x69, 0x60, 0x47, 0xeb, 0x5a, 0xf1, 0xec, 0x33, 0x88, 0xac, 0x5c, 0x45,
+ 0xc0, 0x5a, 0x55, 0x53, 0x59, 0x3d, 0xee, 0xc1, 0xc5, 0xfa, 0x82, 0xe5,
+ 0x44, 0x83, 0xca, 0xa7, 0x0b, 0x1d, 0xc2, 0xaa, 0x6f, 0xe9, 0x73, 0x0c,
+ 0xf1, 0x37, 0x32, 0xab, 0xea, 0xe7, 0x08, 0x90, 0x52, 0x01, 0x6e, 0x80,
+ 0x8f, 0x03, 0x59, 0x3d, 0xc4, 0xcf, 0x2f, 0x1b, 0xce, 0xd8, 0x95, 0x23,
+ 0xfc, 0x9e, 0x8e, 0x0f, 0x6a, 0x20, 0xe8, 0xcf, 0x3f, 0xc6, 0x32, 0x42,
+ 0x74, 0x7f, 0x31, 0xef, 0x2d, 0xba, 0x52, 0x59, 0xfc, 0x9e, 0x0d, 0x86,
+ 0x4d, 0xd5, 0x1b, 0x94, 0x5a, 0x61, 0xc7, 0x9f, 0x06, 0x96, 0x6a, 0xaa,
+ 0x39, 0x2a, 0x94, 0x44, 0xf0, 0x7f, 0x67, 0x5b, 0x50, 0x93, 0xaa, 0xdf,
+ 0xcc, 0x40, 0xb9, 0x8a, 0xb8, 0x27, 0x41, 0x95, 0xcf, 0x91, 0xcf, 0x59,
+ 0xf6, 0x57, 0xc9, 0x55, 0x70, 0xae, 0xd7, 0xb6, 0x26, 0x02, 0xd2, 0x41,
+ 0x53, 0x72, 0xc8, 0xb3, 0xc7, 0x97, 0x39, 0x3d, 0x6a, 0x58, 0x60, 0xc1,
+ 0x14, 0xd2, 0x8b, 0x0a, 0x0a, 0x4a, 0xbe, 0xb2, 0xbe, 0x40, 0xc6, 0x4a,
+ 0x3c, 0xd2, 0xbd, 0xc6, 0x1a, 0x71, 0x2f, 0x8a, 0x76, 0x4d, 0x45, 0xe0,
+ 0xcb, 0x6d, 0xec, 0x74, 0x15, 0x24, 0x93, 0xf4, 0xf8, 0xcc, 0x5f, 0x27,
+ 0x4c, 0xe7, 0x29, 0x38, 0x4d, 0xa0, 0x1f, 0x7e, 0x8e, 0x8f, 0x36, 0x9c,
+ 0x2d, 0x3c, 0xd5, 0x90, 0x05, 0x77, 0x85, 0xd0, 0x58, 0xb9, 0x65, 0xd0,
+ 0x4b, 0x5c, 0x27, 0xe0, 0xc9, 0xff, 0x90, 0xfe, 0x8e, 0xd5, 0xf9, 0xf7,
+ 0x62, 0xf5, 0xa4, 0xbe, 0x58, 0xa2, 0x82, 0x29, 0x8a, 0x71, 0xd1, 0x68,
+ 0xba, 0x7e, 0x72, 0x02, 0x52, 0xeb, 0xf7, 0x01, 0x6b, 0x7b, 0x29, 0x93,
+ 0x4b, 0xd2, 0x84, 0x1f, 0xdf, 0x4b, 0x6a, 0x24, 0xd8, 0xc5, 0xd6, 0x13,
+ 0x94, 0xab, 0x7b, 0xfc, 0x17, 0xa0, 0x21, 0xf1, 0xbe, 0xcd, 0xba, 0x9b,
+ 0xcb, 0xe0, 0x3b, 0xda, 0x45, 0x0a, 0x84, 0x45, 0x99, 0x20, 0x9a, 0x30,
+ 0x23, 0xd8, 0xd8, 0x91, 0xaa, 0xb7, 0x3f, 0x30, 0xc9, 0x6e, 0xcb, 0x4d,
+ 0xdf, 0x8a, 0x9d, 0x75, 0x35, 0x8a, 0xbb, 0x93, 0xe3, 0x0c, 0x97, 0x3e,
+ 0xdf, 0x8f, 0xc6, 0xec, 0x70, 0xc4, 0x39, 0x7f, 0x05, 0xae, 0xa6, 0x0f,
+ 0x23, 0x1f, 0xac, 0x0f, 0xdd, 0xe4, 0x1a, 0xa1, 0x16, 0x03, 0x0b, 0xd5,
+ 0xa5, 0x79, 0x7b, 0xab, 0x3d, 0xe1, 0xc1, 0xaa, 0x5e, 0x3b, 0xf5, 0x83,
+ 0x8e, 0x38, 0x8c, 0x95, 0x8f, 0xfc, 0xf0, 0xb5, 0x70, 0x90, 0xc3, 0xe6,
+ 0x26, 0x86, 0x1c, 0x0c, 0xd8, 0x71, 0x22, 0x63, 0xb7, 0xe1, 0x8a, 0x64,
+ 0xe0, 0x2e, 0x1b, 0x2d, 0x12, 0x30, 0x2a, 0x65, 0x32, 0xdb, 0xf0, 0x2d,
+ 0x3b, 0xbb, 0x35, 0xd7, 0x05, 0x9f, 0x87, 0x1e, 0xad, 0x46, 0xeb, 0x8c,
+ 0x41, 0x10, 0x8d, 0xb2, 0xb3, 0x49, 0x68, 0xb7, 0x40, 0x5a, 0xae, 0x92,
+ 0xa6, 0xba, 0xae, 0x18, 0xab, 0xfb, 0x01, 0xd7, 0xb4, 0xe4, 0xfc, 0xdd,
+ 0x49, 0xd7, 0x16, 0x81, 0xd5, 0xb0, 0x5c, 0xb6, 0x9d, 0xcf, 0x3f, 0x92,
+ 0x67, 0x6e, 0xa9, 0x0d, 0x49, 0x77, 0xd8, 0x27, 0x6d, 0xd4, 0xe3, 0xb0,
+ 0x53, 0xac, 0xbc, 0x0f, 0xeb, 0xe1, 0x4e, 0x5d, 0x99, 0xce, 0x55, 0x41,
+ 0xa7, 0x96, 0x94, 0x0b, 0xa9, 0x00, 0x4b, 0xb6, 0xb4, 0xca, 0x8a, 0xe9,
+ 0x8a, 0xfc, 0xbe, 0x1c, 0xf4, 0x6c, 0x47, 0x99, 0xe2, 0x4a, 0x68, 0xc5,
+ 0x4c, 0x36, 0x40, 0xf7, 0xc4, 0xf2, 0xe0, 0x22, 0xbb, 0x18, 0x40, 0x8e,
+ 0x9f, 0xe9, 0x27, 0xea, 0x40, 0x0f, 0x26, 0xef, 0x7c, 0xad, 0xf3, 0xab,
+ 0x5f, 0x29, 0xdc, 0x6b, 0x9f, 0xab, 0xc5, 0xf5, 0x2a, 0x6c, 0x8c, 0x20,
+ 0xf6, 0xc7, 0xc4, 0x5a, 0x42, 0x7c, 0x99, 0x66, 0x6b, 0x25, 0x59, 0xee,
+ 0xe0, 0xf9, 0x4b, 0x05, 0x53, 0xac, 0x24, 0xf2, 0x0c, 0x87, 0xa5, 0xcb,
+ 0x6c, 0xed, 0x3e, 0x47, 0xec, 0x7b, 0x36, 0x9f, 0xb4, 0xfe, 0x3f, 0xfa,
+ 0x6d, 0xa4, 0x6b, 0x4e, 0x6e, 0x55, 0x32, 0xec, 0xd2, 0x0d, 0xba, 0x34,
+ 0xdc, 0x88, 0x98, 0xed, 0x8b, 0x45, 0xea, 0x6d, 0xd5, 0x01, 0xa7, 0xae,
+ 0xcd, 0x76, 0x44, 0x66, 0x9d, 0x7f, 0x8a, 0xee, 0x27, 0x26, 0x1d, 0xd2,
+ 0xdb, 0x69, 0xcc, 0xa5, 0xbb, 0xe5, 0x26, 0x87, 0xb2, 0xd7, 0x88, 0xb5,
+ 0x47, 0xe9, 0xe9, 0xc8, 0xd5, 0xcf, 0xdd, 0xfa, 0x09, 0xcb, 0xec, 0x26,
+ 0xab, 0xaa, 0x47, 0x12, 0x70, 0x5f, 0xa3, 0x9d, 0x65, 0xbb, 0x49, 0xc1,
+ 0xf6, 0x1e, 0x48, 0xea, 0x20, 0x73, 0x67, 0xc0, 0xa6, 0x4a, 0x95, 0x7f,
+ 0x41, 0x9e, 0x89, 0xb5, 0x02, 0x4f, 0x68, 0x2f, 0x64, 0x24, 0xc4, 0x82,
+ 0x0e, 0xe0, 0xe2, 0x07, 0x35, 0x98, 0x78, 0x8e, 0xc3, 0xba, 0x0b, 0xef,
+ 0x77, 0xe6, 0xbf, 0xe3, 0x23, 0x7b, 0xb4, 0x59, 0x11, 0xa1, 0xb7, 0xfc,
+ 0xce, 0x8a, 0x52, 0xa8, 0x34, 0x3f, 0x70, 0x8a, 0xd4, 0x1d, 0x0f, 0x13,
+ 0xfa, 0xf2, 0x67, 0x36, 0x6a, 0x42, 0x08, 0x6e, 0xc9, 0x5d, 0x98, 0xd6,
+ 0xc4, 0xc9, 0x19, 0xd3, 0x2f, 0xcd, 0xbd, 0x65, 0x72, 0xeb, 0x41, 0x83,
+ 0x10, 0x66, 0x54, 0x16, 0x25, 0xe1, 0xca, 0x17, 0x4e, 0x1c, 0x7b, 0x9f,
+ 0xb0, 0xf9, 0x2b, 0x81, 0xe6, 0xb7, 0x2d, 0x8b, 0x5f, 0xf3, 0xb3, 0xca,
+ 0x97, 0x82, 0x4e, 0x25, 0x77, 0xfe, 0x16, 0x74, 0xd2, 0x35, 0x12, 0x15,
+ 0xec, 0x8c, 0x83, 0x0a, 0xca, 0x21, 0x31, 0x68, 0x8b, 0xd7, 0x58, 0x46,
+ 0x24, 0x31, 0x26, 0xf5, 0x90, 0xdb, 0x7b, 0x3f, 0xe5, 0xbb, 0x1a, 0x28,
+ 0x54, 0x89, 0xfe, 0xa2, 0x60, 0x04, 0xdc, 0x09, 0x11, 0x6c, 0x6c, 0x66,
+ 0x14, 0x69, 0x28, 0x81, 0x9d, 0x52, 0x63, 0xbf, 0x7f, 0xe9, 0xfe, 0x62,
+ 0x48, 0x1c, 0x0a, 0xfe, 0x19, 0x28, 0x09, 0x2a, 0x83, 0xdd, 0xa8, 0x82,
+ 0xeb, 0xe7, 0x6b, 0xe5, 0x23, 0x7c, 0x58, 0x3e, 0xcd, 0x38, 0x32, 0xbe,
+ 0x47, 0xe4, 0x28, 0x96, 0xce, 0x77, 0x70, 0x7e, 0x65, 0xd9, 0xb1, 0x1b,
+ 0xef, 0x7e, 0x51, 0xf6, 0xac, 0xf0, 0x6c, 0xe5, 0xc7, 0x38, 0x03, 0xf7,
+ 0x9d, 0xf1, 0x73, 0x56, 0x02, 0xf1, 0xc2, 0x0f, 0x41, 0x27, 0x9f, 0x67,
+ 0xd3, 0xa6, 0x30, 0x29, 0x81, 0x65, 0x0b, 0x02, 0xf7, 0xac, 0xad, 0x4a,
+ 0xb0, 0x90, 0x63, 0x20, 0x11, 0x74, 0x28, 0xdd, 0xb7, 0x12, 0x76, 0xe9,
+ 0xe9, 0xe3, 0xcf, 0xae, 0xeb, 0x01, 0xbf, 0xf3, 0x82, 0x80, 0xdb, 0x00,
+ 0x08, 0xff, 0xe6, 0xce, 0x09, 0xfb, 0xc9, 0x3d, 0x31, 0x65, 0x39, 0x54,
+ 0x4a, 0x45, 0xc0, 0x1a, 0x1c, 0xb2, 0x90, 0x1e, 0x57, 0x50, 0xef, 0x3c,
+ 0x60, 0x5d, 0x1e, 0x4e, 0xa6, 0x0f, 0xab, 0xbb, 0x41, 0x8d, 0xf7, 0x4c,
+ 0xa6, 0xd8, 0xb2, 0x97, 0xfa, 0xfa, 0x47, 0xcf, 0xa1, 0xe7, 0x57, 0x73,
+ 0x74, 0xa0, 0x01, 0xf6, 0x6e, 0x92, 0x7a, 0xc0, 0x3f, 0x0d, 0x92, 0x49,
+ 0xa8, 0x28, 0xfd, 0xa2, 0x78, 0x9a, 0x3e, 0x15, 0x8a, 0x8a, 0x07, 0x05,
+ 0xc3, 0xd0, 0x24, 0x12, 0x25, 0x9a, 0x5a, 0x42, 0xfa, 0xfe, 0x9e, 0x41,
+ 0x21, 0xb5, 0xa5, 0x29, 0xb3, 0x09, 0xcb, 0x16, 0x70, 0x0f, 0xd0, 0x17,
+ 0x2b, 0x9f, 0xda, 0xf3, 0x29, 0x48, 0xa2, 0x32, 0xd7, 0x40, 0xb0, 0x4c,
+ 0x14, 0x1b, 0x33, 0xcd, 0x5d, 0x4f, 0xbe, 0x3a, 0x4a, 0x34, 0x2b, 0x6b,
+ 0xbe, 0x17, 0xc0, 0x86, 0x07, 0xfd, 0x2e, 0xc4, 0x8e, 0xec, 0xd7, 0x74,
+ 0xd7, 0x3a, 0xf7, 0x86, 0x6b, 0x55, 0x8a, 0xf5, 0x66, 0xd8, 0xb0, 0x41,
+ 0x5e, 0xc9, 0x3c, 0x21, 0x8e, 0x64, 0x55, 0xef, 0x95, 0xa3, 0x18, 0x64,
+ 0x18, 0xda, 0x8c, 0xd1, 0x29, 0xc5, 0xf3, 0xf0, 0xad, 0x38, 0x5f, 0xda,
+ 0x24, 0xf9, 0x66, 0xe0, 0xeb, 0xdc, 0xcb, 0x8b, 0x8e, 0xc9, 0x09, 0x0c,
+ 0x0e, 0xdd, 0xf3, 0x15, 0x86, 0xc3, 0xec, 0x38, 0x05, 0xdf, 0x27, 0x83,
+ 0xd6, 0x04, 0x68, 0xf0, 0x56, 0xa1, 0xd2, 0x02, 0xf0, 0x84, 0x41, 0x0b,
+ 0xdb, 0x66, 0xb3, 0x73, 0x86, 0x66, 0xce, 0x88, 0x4f, 0xb3, 0x39, 0x9c,
+ 0xa8, 0xca, 0xcf, 0x50, 0xd8, 0xa5, 0x6d, 0x56, 0x7b, 0x2c, 0xad, 0xc1,
+ 0x20, 0xc8, 0xec, 0x15, 0x2a, 0x88, 0x11, 0x3b, 0xea, 0xb2, 0xe6, 0x96,
+ 0xf4, 0x32, 0xdf, 0xf9, 0xa9, 0xaa, 0x37, 0x97, 0x68, 0xec, 0x7e, 0x7f,
+ 0x2b, 0x18, 0x2f, 0x0c, 0x75, 0x86, 0x53, 0x9d, 0x76, 0xf5, 0x98, 0x82,
+ 0xbb, 0x50, 0x86, 0xce, 0xdc, 0x0f, 0x43, 0x95, 0x89, 0x0e, 0x7f, 0x91,
+ 0x1d, 0x93, 0x70, 0x35, 0xef, 0x6a, 0x8e, 0x1c, 0x38, 0x58, 0xa5, 0x8f,
+ 0x2d, 0x8f, 0xea, 0x54, 0x70, 0xf1, 0xc8, 0x03, 0xce, 0x50, 0xd2, 0x23,
+ 0xdd, 0xd4, 0xc1, 0xd2, 0x12, 0x74, 0xbe, 0x79, 0x94, 0x2c, 0x45, 0x67,
+ 0x67, 0x6b, 0xa3, 0x4d, 0x22, 0x95, 0x91, 0x30, 0x7c, 0x8a, 0x30, 0x10,
+ 0x0a, 0x42, 0xcd, 0xaa, 0x46, 0x11, 0xf7, 0xa1, 0x87, 0x24, 0xfc, 0x0c,
+ 0xbb, 0x0c, 0xb7, 0x6f, 0x7f, 0x19, 0x09, 0x0a, 0x6b, 0x08, 0xc4, 0x2e,
+ 0x24, 0xec, 0xf8, 0x40, 0x69, 0x00, 0xad, 0x46, 0xda, 0x51, 0x74, 0xf5,
+ 0x8d, 0x76, 0x3e, 0x88, 0xd5, 0x7a, 0x87, 0xc9, 0x11, 0xce, 0x20, 0xc6,
+ 0x5d, 0x86, 0x44, 0x4b, 0xb3, 0x7a, 0x14, 0x83, 0x3a, 0x99, 0x82, 0x54,
+ 0x28, 0x69, 0x7b, 0x08, 0x09, 0x1e, 0x69, 0x86, 0x34, 0x0c, 0xdc, 0x3f,
+ 0x0a, 0x68, 0xd4, 0xc8, 0x09, 0x2b, 0x3e, 0x97, 0x7a, 0xc0, 0x11, 0x58,
+ 0x9b, 0xc8, 0x9a, 0x53, 0x4f, 0xbe, 0x42, 0x9a, 0x2b, 0xca, 0x7a, 0x27,
+ 0xc6, 0x54, 0x5d, 0xaf, 0x88, 0x0e, 0xe3, 0x22, 0x84, 0xad, 0xc2, 0xf5,
+ 0x8c, 0xd0, 0x55, 0xfc, 0x2a, 0x33, 0xe4, 0xd7, 0xbe, 0xc5, 0xcd, 0xe8,
+ 0x51, 0x82, 0xe5, 0xb8, 0xdb, 0x8a, 0x10, 0xd4, 0x3f, 0x93, 0x60, 0xf2,
+ 0xe6, 0x50, 0x93, 0xce, 0x43, 0xb6, 0x80, 0xb2, 0x2f, 0xb7, 0xff, 0x1a,
+ 0xdb, 0xb2, 0x7d, 0x71, 0xa2, 0x30, 0x93, 0x01, 0x4b, 0xaa, 0x03, 0x1a,
+ 0xf2, 0xce, 0x2b, 0x17, 0xcf, 0xcb, 0xa0, 0xe0, 0x5e, 0x03, 0xab, 0x71,
+ 0xc5, 0x04, 0x8f, 0x7c, 0x71, 0xce, 0x36, 0x1c, 0x29, 0xed, 0xfe, 0x11,
+ 0x7a, 0xfe, 0x6c, 0x82, 0xc8, 0x8d, 0xdb, 0x68, 0x25, 0x9d, 0xd2, 0x0f,
+ 0xd0, 0xa0, 0x26, 0xea, 0x8d, 0x03, 0xb4, 0xcb, 0x34, 0x0a, 0xf2, 0x23,
+ 0x4f, 0xe3, 0x8b, 0x62, 0xde, 0x22, 0xf0, 0x0c, 0x84, 0x05, 0xb4, 0xcf,
+ 0xa8, 0xb7, 0x95, 0xd2, 0x0a, 0xe7, 0x11, 0x19, 0x2a, 0xe9, 0x85, 0x5f,
+ 0x1e, 0x83, 0x22, 0x48, 0x4b, 0x2e, 0x9c, 0x0e, 0x1f, 0x44, 0x2c, 0xf2,
+ 0xdc, 0xc6, 0x4d, 0xb1, 0xc4, 0xfd, 0xe9, 0xaa, 0xb4, 0x93, 0x17, 0xf3,
+ 0xa7, 0xdd, 0x03, 0x68, 0xc3, 0x23, 0xd7, 0xd3, 0xcb, 0x53, 0x80, 0x66,
+ 0xb2, 0x96, 0x58, 0xea, 0x2e, 0x26, 0x76, 0xf8, 0xeb, 0x2d, 0x84, 0x1d,
+ 0xb2, 0x41, 0xd4, 0xdc, 0x9b, 0x94, 0x90, 0xb5, 0x6f, 0xde, 0xa3, 0x07,
+ 0x44, 0x91, 0xd5, 0xc5, 0x74, 0xb6, 0xbc, 0xbd, 0xc2, 0x0e, 0x28, 0xd4,
+ 0xd9, 0x1c, 0x7c, 0x5b, 0x47, 0xec, 0x17, 0x8a, 0x28, 0x2c, 0xcf, 0x8f,
+ 0xd5, 0xe4, 0x22, 0x67, 0x73, 0xde, 0xce, 0x35, 0xdb, 0x70, 0x2b, 0x83,
+ 0x65, 0x0b, 0xe0, 0x6a, 0x7a, 0x57, 0xdc, 0x88, 0x15, 0x1a, 0x54, 0xa2,
+ 0xdf, 0xc2, 0x4c, 0xca, 0x7f, 0xe0, 0x6a, 0xe7, 0x2d, 0x2e, 0x6b, 0xb5,
+ 0x92, 0xfd, 0xc8, 0x95, 0xe1, 0x50, 0xa2, 0x0a, 0xdd, 0x7d, 0x6c, 0x2a,
+ 0x0e, 0x00, 0x01, 0xc9, 0x6f, 0x73, 0x68, 0x0a, 0x47, 0x74, 0x61, 0xc4,
+ 0x7c, 0xd1, 0xe1, 0x7d, 0xd3, 0x86, 0xa8, 0xb4, 0xc8, 0x3c, 0x39, 0x87,
+ 0x1c, 0xa8, 0x36, 0x90, 0xd2, 0xcc, 0x8d, 0x56, 0xd0, 0x3a, 0xe7, 0x9a,
+ 0x97, 0x33, 0xd2, 0x88, 0x70, 0x90, 0x8b, 0x01, 0x17, 0x76, 0xb1, 0x49,
+ 0x55, 0x95, 0xed, 0x9c, 0x97, 0x80, 0xb5, 0xb2, 0x4b, 0x78, 0x49, 0x47,
+ 0x16, 0x02, 0xa6, 0x2a, 0xb0, 0x21, 0x84, 0x9f, 0x9d, 0xe1, 0xd4, 0x25,
+ 0x14, 0xaf, 0x30, 0xbe, 0xa0, 0x86, 0x05, 0x0e, 0x12, 0xe8, 0x07, 0x11,
+ 0xd8, 0xf0, 0xa3, 0x70, 0xe1, 0x9e, 0xf4, 0x1a, 0x50, 0xb2, 0x55, 0x7d,
+ 0xf4, 0xa8, 0x24, 0x5e, 0x79, 0x56, 0x1f, 0x93, 0x23, 0x6d, 0x50, 0x67,
+ 0x54, 0x9c, 0x96, 0xca, 0x1a, 0x69, 0xf8, 0xfe, 0xfa, 0xc7, 0x48, 0x6b,
+ 0xf9, 0x98, 0x61, 0x96, 0xa4, 0x94, 0x2d, 0x15, 0x6b, 0x44, 0x57, 0x23,
+ 0x06, 0xa3, 0x16, 0x91, 0x19, 0x7b, 0x02, 0xb4, 0x03, 0x8b, 0x79, 0xc3,
+ 0xb8, 0xc9, 0x25, 0xd5, 0x64, 0xa7, 0x42, 0xc3, 0xd3, 0x72, 0x2e, 0x6f,
+ 0x73, 0x85, 0xc9, 0x18, 0x1c, 0x8a, 0x7b, 0x8f, 0x4d, 0x49, 0xae, 0x00,
+ 0xe1, 0xe6, 0x4c, 0xb3, 0x9f, 0x37, 0xda, 0x2c, 0xd6, 0xd1, 0x33, 0x9b,
+ 0x3e, 0x97, 0x63, 0x44, 0xb1, 0x04, 0xc2, 0x63, 0x4a, 0x7a, 0x65, 0x14,
+ 0x24, 0xe9, 0x09, 0x9d, 0x48, 0x93, 0x72, 0x3c, 0xbd, 0x8b, 0x14, 0x95,
+ 0xb7, 0x84, 0x6d, 0x3f, 0xe7, 0xd6, 0xd1, 0x22, 0x9d, 0xd1, 0xb2, 0xd5,
+ 0x4e, 0xd2, 0xd3, 0x34, 0x0b, 0xea, 0x41, 0x54, 0xbc, 0x97, 0x4d, 0x96,
+ 0x77, 0x4d, 0xdc, 0xaf, 0xae, 0xbd, 0xa8, 0x42, 0x0d, 0x5d, 0x25, 0x4c,
+ 0x18, 0x8f, 0x89, 0xb9, 0x62, 0x3c, 0x57, 0x49, 0xa1, 0x07, 0x1e, 0xcb,
+ 0xb4, 0xf3, 0x43, 0x0b, 0x93, 0xfd, 0x12, 0x46, 0x32, 0xc4, 0x44, 0x5f,
+ 0x4e, 0xcd, 0x8d, 0xba, 0xb2, 0x47, 0x36, 0xde, 0x4c, 0x75, 0x81, 0x96,
+ 0xd8, 0xe4, 0x2f, 0xfe, 0x65, 0xb9, 0x1f, 0xa3, 0x60, 0x7d, 0xd4, 0x7e,
+ 0xa2, 0x9d, 0x2e, 0xd1, 0xba, 0x08, 0x1a, 0x39, 0x24, 0xc4, 0xab, 0xdb,
+ 0x16, 0xaf, 0x1d, 0x42, 0x55, 0x5e, 0x9a, 0xc5, 0xef, 0x2d, 0xb9, 0xab,
+ 0xe9, 0x32, 0xe8, 0x25, 0x69, 0xdb, 0x7b, 0x60, 0xb1, 0xe9, 0x90, 0x32,
+ 0x71, 0xb8, 0x33, 0xc7, 0x57, 0x0c, 0x50, 0x00, 0x97, 0x8c, 0x0f, 0x47,
+ 0x4c, 0x0c, 0x9f, 0x55, 0xac, 0x10, 0xfb, 0xd3, 0xe0, 0x81, 0x43, 0xba,
+ 0x06, 0x04, 0xaa, 0xf1, 0x1a, 0x33, 0x9e, 0x7b, 0xe8, 0xeb, 0xea, 0x05,
+ 0xff, 0x2c, 0x7e, 0xd4, 0x7d, 0xb4, 0xc1, 0xc3, 0xe4, 0xb5, 0x37, 0xc6,
+ 0x3c, 0x34, 0x34, 0x36, 0x76, 0x51, 0x67, 0x1a, 0x68, 0xe3, 0xee, 0x59,
+ 0x09, 0xb7, 0xe1, 0x19, 0x90, 0xb9, 0x4c, 0x89, 0xf4, 0x3b, 0x30, 0xce,
+ 0xe8, 0xa1, 0x90, 0xc6, 0xc3, 0x1f, 0x9e, 0x7e, 0x36, 0x0f, 0xd8, 0xac,
+ 0x84, 0x4e, 0xbd, 0x80, 0x06, 0x38, 0x77, 0x2a, 0x96, 0xa0, 0xef, 0xd9,
+ 0xf9, 0x54, 0xdf, 0x0b, 0x11, 0xdb, 0xf8, 0xb5, 0x21, 0xdd, 0x2d, 0xc6,
+ 0xc1, 0x09, 0x85, 0xec, 0x1b, 0xcb, 0x55, 0x14, 0x39, 0x13, 0xf9, 0xf5,
+ 0xc4, 0x65, 0xeb, 0xeb, 0x2e, 0xe7, 0x01, 0x47, 0x39, 0x9b, 0x57, 0x0f,
+ 0x1d, 0x64, 0x4d, 0xaf, 0x86, 0xaf, 0x7e, 0xc5, 0x88, 0x02, 0x7d, 0x2c,
+ 0x22, 0x70, 0xa8, 0x4d, 0x5c, 0x18, 0x5d, 0x7b, 0x07, 0x1a, 0x6b, 0x30,
+ 0x33, 0xff, 0x0b, 0x2e, 0xf6, 0x12, 0xa1, 0x2d, 0xe4, 0xab, 0x49, 0xa4,
+ 0x8d, 0xde, 0x12, 0xe5, 0x40, 0x79, 0xc3, 0x78, 0x68, 0xc2, 0x6a, 0x5f,
+ 0x7b, 0xe3, 0x00, 0x0d, 0xb8, 0xf1, 0x38, 0x29, 0x7d, 0xc9, 0x06, 0xd9,
+ 0x7e, 0xc8, 0xc3, 0x2c, 0xb1, 0x37, 0xf2, 0x01, 0x46, 0xf0, 0xca, 0xaa,
+ 0xed, 0xa1, 0xe6, 0x57, 0x2b, 0xc9, 0x28, 0x0a, 0x43, 0x77, 0x11, 0x55,
+ 0x78, 0x39, 0xb8, 0x0f, 0x70, 0x56, 0xc6, 0x3a, 0xf0, 0xa6, 0x92, 0xae,
+ 0x2b, 0xbf, 0x47, 0xc7, 0x25, 0xcd, 0xb1, 0x7e, 0x7f, 0x00, 0x0c, 0xab,
+ 0xa5, 0xe0, 0x1e, 0x18, 0x0a, 0xb4, 0xb2, 0x11, 0xd0, 0xab, 0x53, 0x8b,
+ 0x91, 0x63, 0x4a, 0x88, 0x94, 0xf7, 0xd1, 0x25, 0xa3, 0xa2, 0xb2, 0x60,
+ 0x77, 0x78, 0x8a, 0x82, 0xb8, 0x2e, 0xfb, 0x00, 0x37, 0x7e, 0xe9, 0xdd,
+ 0x80, 0x5c, 0x3a, 0x3e, 0x67, 0x32, 0x67, 0x44, 0xbe, 0x68, 0x61, 0xd8,
+ 0xa0, 0xcd, 0xb2, 0xb0, 0x7a, 0x79, 0x45, 0xdc, 0x94, 0x54, 0xd4, 0xa0,
+ 0xa4, 0xbb, 0x5a, 0x18, 0x85, 0x46, 0x8d, 0xe7, 0x26, 0x56, 0x92, 0x02,
+ 0xfd, 0x47, 0x9a, 0x32, 0x68, 0xc9, 0x03, 0x35, 0xd4, 0x70, 0x21, 0x8d,
+ 0x2f, 0x08, 0xab, 0x84, 0x30, 0xab, 0x27, 0x6e, 0x63, 0x50, 0xc6, 0x13,
+ 0x48, 0xc4, 0x60, 0xaf, 0xb1, 0xf7, 0xfd, 0x70, 0x62, 0xc4, 0x15, 0x23,
+ 0x16, 0x70, 0x1e, 0x3a, 0xe3, 0x47, 0x4f, 0xac, 0xdf, 0xd6, 0x5f, 0x96,
+ 0x51, 0x9b, 0x37, 0x40, 0x16, 0xb1, 0xce, 0xda, 0xd5, 0x7e, 0xe5, 0x64,
+ 0x06, 0x63, 0x03, 0x54, 0x34, 0xb3, 0xe9, 0x5c, 0x1e, 0x2e, 0x18, 0xe2,
+ 0x05, 0xf3, 0xe5, 0x35, 0x3b, 0x09, 0x15, 0xef, 0xb8, 0xc9, 0x7a, 0x3f,
+ 0x42, 0x51, 0x02, 0x7b, 0x3e, 0x3a, 0xcc, 0xf8, 0x5b, 0x95, 0x19, 0x99,
+ 0xd9, 0xa2, 0x93, 0x87, 0x44, 0x5c, 0x83, 0x25, 0xb6, 0xdd, 0x92, 0xdb,
+ 0x50, 0x3c, 0xde, 0x81, 0x08, 0x88, 0x86, 0xf3, 0x92, 0xbb, 0xa2, 0x6a,
+ 0x5a, 0x40, 0x69, 0x7f, 0xf4, 0xfc, 0x77, 0x92, 0xb5, 0x7d, 0xdf, 0x57,
+ 0x7c, 0x64, 0x8f, 0xd7, 0x73, 0x0b, 0x5c, 0xd5, 0x34, 0xa7, 0xce, 0xaa,
+ 0xb6, 0x61, 0xd0, 0x2c, 0x1d, 0x6c, 0xfb, 0xfb, 0x51, 0xa7, 0x98, 0x9f,
+ 0x6e, 0x65, 0x97, 0xed, 0x70, 0x6e, 0x7f, 0x99, 0x5d, 0x01, 0xf1, 0x3c,
+ 0xac, 0xbb, 0xc0, 0x27, 0xb8, 0xf9, 0x0c, 0xca, 0x07, 0x88, 0xde, 0x0e,
+ 0xd9, 0x12, 0x18, 0x6b, 0x5c, 0x86, 0x0c, 0x51, 0x19, 0x4e, 0xf8, 0x70,
+ 0x58, 0x7c, 0xc7, 0xd0, 0xf8, 0x87, 0x5b, 0xc1, 0x8a, 0x3b, 0x5e, 0xd7,
+ 0x16, 0x3c, 0xaf, 0xbf, 0x32, 0x65, 0xac, 0x27, 0x18, 0x25, 0xcb, 0x2c,
+ 0xd3, 0x19, 0x82, 0x74, 0xd7, 0x3a, 0xd0, 0xc5, 0x9f, 0x42, 0xa7, 0x92,
+ 0xc7, 0xae, 0x87, 0x7d, 0xab, 0x4f, 0x53, 0xa5, 0x73, 0x9c, 0xcc, 0x77,
+ 0x34, 0x20, 0x67, 0x3b, 0x18, 0xee, 0x5d, 0x8c, 0x0d, 0x44, 0xc8, 0xc3,
+ 0xb4, 0xcd, 0x34, 0x41, 0x3b, 0xad, 0xe5, 0x7c, 0xd0, 0xf4, 0xf2, 0xba,
+ 0xe1, 0xae, 0x36, 0xc7, 0xd2, 0x12, 0x49, 0xfc, 0xd3, 0x62, 0x29, 0xbe,
+ 0x02, 0xf1, 0xa8, 0xb7, 0xec, 0xec, 0xc9, 0x18, 0x91, 0x72, 0x91, 0xa0,
+ 0x63, 0xe7, 0x0a, 0xb8, 0x21, 0x1f, 0x34, 0x30, 0x23, 0xa7, 0xb9, 0x81,
+ 0xe5, 0x80, 0xf6, 0x5d, 0x32, 0xe8, 0x93, 0x4a, 0x8b, 0xa9, 0x85, 0xb0,
+ 0xa4, 0x25, 0xb7, 0xc0, 0x86, 0x3e, 0x93, 0x8e, 0x3b, 0x32, 0x75, 0x5b,
+ 0x4a, 0xf2, 0x10, 0xb7, 0xb6, 0x6b, 0x77, 0x3c, 0x92, 0x01, 0x84, 0xb8,
+ 0xf0, 0x4f, 0x08, 0xa7, 0xd7, 0xb6, 0x4d, 0x34, 0x15, 0xf8, 0x90, 0x9f,
+ 0x07, 0xe2, 0x26, 0x95, 0xad, 0x17, 0x1c, 0xd2, 0xc7, 0x25, 0x24, 0xd6,
+ 0xc5, 0x85, 0x0f, 0x59, 0xef, 0x9c, 0xf4, 0x5b, 0xb6, 0x58, 0x36, 0x9a,
+ 0x07, 0x9e, 0x3e, 0x67, 0x0d, 0x0c, 0x79, 0x20, 0x75, 0x0d, 0xd3, 0x86,
+ 0x3b, 0x92, 0xab, 0xb4, 0xf8, 0xad, 0x04, 0xc9, 0x87, 0x18, 0x45, 0xce,
+ 0x32, 0x29, 0x35, 0xcf, 0x9a, 0x9e, 0x42, 0xf6, 0x0a, 0x3c, 0xcd, 0x4a,
+ 0xb6, 0x28, 0x52, 0x70, 0x34, 0x43, 0x1d, 0x1a, 0x8d, 0x47, 0x19, 0x62,
+ 0x3d, 0x77, 0x6b, 0x2e, 0x8b, 0x33, 0x8f, 0xee, 0x32, 0xe6, 0xb8, 0x44,
+ 0x1a, 0xc4, 0x82, 0xae, 0x6f, 0xc2, 0x37, 0x18, 0x4f, 0xb3, 0xdd, 0xc0,
+ 0x2e, 0xb3, 0x3c, 0x5c, 0x53, 0x35, 0xbf, 0x92, 0x02, 0x22, 0xc9, 0x0c,
+ 0xee, 0x82, 0xcd, 0x3a, 0xb2, 0xd9, 0xf6, 0x29, 0x62, 0xbb, 0xf4, 0x75,
+ 0xd3, 0x70, 0x87, 0xd1, 0x36, 0x47, 0x40, 0xfe, 0x6e, 0x0e, 0x14, 0xf1,
+ 0xd1, 0x71, 0x02, 0x9d, 0x92, 0xec, 0xb5, 0x63, 0xc4, 0x59, 0xd5, 0xc3,
+ 0x27, 0x99, 0x05, 0x4c, 0xb6, 0xfa, 0xde, 0x2e, 0xf5, 0xdd, 0xa5, 0x00,
+ 0xa6, 0x66, 0xc9, 0xa7, 0x48, 0x7b, 0x3c, 0x11, 0xbd, 0x40, 0xf6, 0xf8,
+ 0x7f, 0x25, 0x82, 0x8c, 0x78, 0xbd, 0xf1, 0x32, 0x55, 0x5b, 0x7f, 0xb6,
+ 0xd5, 0xbb, 0x01, 0x55, 0x91, 0xd6, 0x4b, 0xd1, 0x54, 0xf9, 0x0c, 0x8e,
+ 0x21, 0x77, 0xe2, 0x6c, 0x2d, 0x1a, 0x97, 0x44, 0x40, 0x3f, 0x7f, 0xec,
+ 0x1e, 0x52, 0x3b, 0x8d, 0xd3, 0xbf, 0xa7, 0xa2, 0x15, 0xb4, 0x9d, 0xf2,
+ 0x24, 0x14, 0x2b, 0x6c, 0x83, 0x37, 0xae, 0x81, 0xb8, 0x87, 0xfd, 0x3f,
+ 0xf7, 0x4a, 0x51, 0x7d, 0xb6, 0x5b, 0x32, 0x1e, 0x40, 0xbe, 0xa7, 0x62,
+ 0x8d, 0x9c, 0xae, 0xcf, 0xd2, 0xbf, 0x88, 0x01, 0x05, 0x20, 0xe3, 0x70,
+ 0xa3, 0x66, 0x54, 0xbf, 0x65, 0xc8, 0x04, 0xbb, 0xd4, 0x87, 0x9a, 0xea,
+ 0xfc, 0x1c, 0x5e, 0x13, 0x20, 0x2a, 0x73, 0x06, 0x2c, 0x1f, 0x48, 0xa4,
+ 0xf6, 0x4c, 0x7c, 0xfb, 0xb6, 0x72, 0x39, 0x65, 0xc0, 0x49, 0xa3, 0x93,
+ 0x5d, 0x6c, 0x84, 0xfb, 0x23, 0xe6, 0x86, 0x26, 0x02, 0xa8, 0x4c, 0xfd,
+ 0x87, 0x0b, 0x58, 0xba, 0xc0, 0xac, 0x2d, 0x6d, 0xc5, 0x7f, 0xf4, 0xa0,
+ 0xf4, 0xce, 0xaf, 0x3e, 0x3d, 0x3b, 0xcf, 0x16, 0xae, 0x7e, 0x89, 0xb1,
+ 0x1a, 0xa5, 0x9d, 0x96, 0x27, 0xa3, 0x20, 0xcb, 0xf3, 0xe8, 0xe7, 0x32,
+ 0x28, 0x8d, 0xa8, 0xc8, 0x1b, 0x91, 0x7e, 0x0e, 0xea, 0x0a, 0x41, 0xce,
+ 0xae, 0x62, 0x37, 0x37, 0xf2, 0x1e, 0x84, 0xa8, 0x1f, 0x00, 0xe2, 0x6c,
+ 0x79, 0xb3, 0x7f, 0xfb, 0xf9, 0x88, 0x3d, 0x1a, 0x8d, 0xb1, 0x1c, 0xa1,
+ 0x0b, 0x2e, 0x21, 0x01, 0xde, 0x1b, 0x47, 0xb4, 0x8c, 0x47, 0x1d, 0x9c,
+ 0x71, 0x3f, 0x25, 0x75, 0xb8, 0xa8, 0x34, 0xc3, 0x83, 0x11, 0xab, 0x35,
+ 0xc2, 0x99, 0x38, 0xd0, 0x19, 0x90, 0x40, 0xf8, 0x2e, 0x33, 0xe2, 0x4d,
+ 0x1a, 0x15, 0xc4, 0xb5, 0x17, 0x3a, 0x63, 0x6f, 0x77, 0xe0, 0x8f, 0xac,
+ 0xef, 0x51, 0x42, 0xec, 0xed, 0xd2, 0xc3, 0x43, 0x47, 0xe6, 0x3e, 0x5b,
+ 0xe6, 0x97, 0xfe, 0x87, 0x38, 0xc6, 0x78, 0xd1, 0x50, 0xbe, 0xef, 0x08,
+ 0xd7, 0xe7, 0x5f, 0x26, 0xb7, 0xe0, 0x76, 0xff, 0x57, 0x95, 0x8b, 0xef,
+ 0x0f, 0xd9, 0x6d, 0xbb, 0x02, 0x0f, 0xee, 0xc9, 0xdb, 0x72, 0x63, 0xee,
+ 0xd6, 0x75, 0xa5, 0x3a, 0xcb, 0xbc, 0xe9, 0xc7, 0xee, 0xf0, 0x01, 0xae,
+ 0xf1, 0x13, 0x2d, 0xd2, 0xfc, 0xe9, 0x5c, 0xc4, 0xae, 0x59, 0x36, 0x3e,
+ 0xb4, 0xdb, 0x36, 0xc4, 0x77, 0xe1, 0xbf, 0x65, 0x40, 0xa7, 0x30, 0x24,
+ 0x2c, 0x3e, 0xe5, 0xac, 0xe6, 0x9f, 0x29, 0xbb, 0x4d, 0xf0, 0x24, 0x71,
+ 0x59, 0xef, 0xcd, 0xd8, 0x92, 0x5e, 0x85, 0x94, 0xc8, 0x59, 0x91, 0x38,
+ 0x1c, 0x99, 0xd1, 0xe1, 0xde, 0x4f, 0xfc, 0x46, 0x8a, 0x2f, 0x25, 0xe0,
+ 0xca, 0xd1, 0xee, 0x14, 0xd4, 0xaa, 0xd8, 0xf4, 0x34, 0xd7, 0xa3, 0xfb,
+ 0x6e, 0xce, 0xdf, 0x62, 0x08, 0x0c, 0x9c, 0xe8, 0xad, 0xfd, 0x0f, 0x79,
+ 0x85, 0xf6, 0xec, 0x5b, 0xf8, 0x35, 0x64, 0x55, 0xa8, 0x69, 0x54, 0x4f,
+ 0x78, 0x1a, 0xd7, 0x8b, 0x91, 0x28, 0x44, 0x18, 0x8a, 0xe8, 0xb5, 0xcf,
+ 0xb5, 0x14, 0xf3, 0x6e, 0x65, 0x6f, 0x48, 0x57, 0xc0, 0x8d, 0x09, 0xca,
+ 0x52, 0xf8, 0x19, 0xb6, 0xbc, 0x4a, 0x17, 0xd0, 0x8e, 0xd5, 0xd6, 0x5f,
+ 0x88, 0xcf, 0xc3, 0x62, 0x35, 0x63, 0x14, 0x05, 0x9d, 0x2c, 0x6e, 0x33,
+ 0x11, 0x88, 0x4f, 0xd7, 0x1e, 0x71, 0x67, 0xa6, 0xae, 0xfb, 0x66, 0xcb,
+ 0x8e, 0x9d, 0x39, 0x37, 0xbd, 0xa3, 0xf8, 0xba, 0x63, 0xf4, 0x01, 0xcc,
+ 0xd1, 0x98, 0x26, 0x83, 0xeb, 0xec, 0x43, 0x7f, 0x3f, 0xf2, 0xeb, 0x7f,
+ 0x82, 0x16, 0xed, 0xf6, 0x22, 0x36, 0x28, 0xda, 0xa6, 0xda, 0x64, 0x2f,
+ 0x3b, 0x50, 0x9c, 0x6e, 0xeb, 0xb7, 0x72, 0xf8, 0x27, 0x3f, 0xa2, 0x50,
+ 0xc7, 0xc8, 0x24, 0xab, 0xa7, 0x53, 0x14, 0x8c, 0x2a, 0x5d, 0xa9, 0x11,
+ 0xb6, 0xd5, 0x8e, 0xa0, 0x2a, 0x68, 0x0e, 0x4e, 0x48, 0x34, 0x39, 0x8c,
+ 0x0a, 0x29, 0x6d, 0x8d, 0x7b, 0x09, 0x9f, 0xeb, 0xd3, 0xeb, 0x83, 0x75,
+ 0xcc, 0x5a, 0xe4, 0xfe, 0x0c, 0x57, 0x31, 0x52, 0xd5, 0xdb, 0x2c, 0x16,
+ 0xdb, 0x0c, 0x9f, 0x04, 0x04, 0x0a, 0xdd, 0x78, 0x98, 0x31, 0xb0, 0xd4,
+ 0xcb, 0x84, 0x44, 0x08, 0xe6, 0xa3, 0x5b, 0xcd, 0x4a, 0xa8, 0x02, 0xce,
+ 0x4c, 0x05, 0x81, 0x4c, 0xae, 0xa8, 0x99, 0x04, 0xca, 0x91, 0xca, 0xdb,
+ 0x1d, 0xc1, 0xcc, 0xfc, 0xa1, 0x85, 0xb2, 0x2b, 0x88, 0x42, 0x0e, 0xc6,
+ 0x36, 0xfb, 0xe4, 0x23, 0x6e, 0xf9, 0x43, 0xba, 0x10, 0x0c, 0x82, 0xef,
+ 0x90, 0x1b, 0x8e, 0xa0, 0xf4, 0xae, 0xe8, 0x40, 0x72, 0x3a, 0x34, 0x1a,
+ 0xa2, 0xc5, 0x3d, 0x5c, 0xe5, 0xdb, 0xfc, 0x4e, 0xb0, 0xe5, 0x8a, 0xaf,
+ 0x19, 0x0d, 0xfb, 0x68, 0x06, 0xc2, 0x50, 0x0a, 0x5f, 0xf3, 0x6f, 0xdf,
+ 0x33, 0xcb, 0x5e, 0x61, 0x7e, 0xe0, 0x26, 0xd1, 0x1f, 0xb7, 0x26, 0x2f,
+ 0x17, 0x9b, 0x97, 0x42, 0x0f, 0xc2, 0x4a, 0x3d, 0xf1, 0x19, 0x0a, 0xf8,
+ 0xca, 0x95, 0xee, 0x56, 0x50, 0x57, 0xbe, 0x34, 0x4d, 0x55, 0x65, 0x96,
+ 0xe3, 0xfb, 0x05, 0x35, 0xe8, 0x4e, 0x2b, 0x71, 0x26, 0x71, 0xe8, 0xfd,
+ 0x2b, 0xf3, 0xa8, 0x5b, 0xaf, 0x6d, 0x01, 0x7c, 0x84, 0xde, 0xfb, 0x1f,
+ 0x6c, 0x7a, 0x22, 0xc4, 0xae, 0x85, 0x20, 0x40, 0x94, 0x2e, 0x44, 0x1d,
+ 0x0e, 0x66, 0x0e, 0xed, 0xac, 0x2f, 0xa8, 0xa6, 0x09, 0xcf, 0xe0, 0x33,
+ 0x93, 0xfc, 0x2f, 0xc3, 0xc1, 0xd1, 0x69, 0x6c, 0x12, 0xaf, 0xda, 0x31,
+ 0x68, 0xbc, 0xff, 0x8b, 0x90, 0xea, 0x94, 0xe9, 0xc7, 0xb3, 0x63, 0x66,
+ 0x0b, 0x64, 0xa7, 0x96, 0xf3, 0x1b, 0x37, 0xfc, 0x55, 0x37, 0x48, 0x38,
+ 0xc7, 0x0c, 0x74, 0xec, 0x7e, 0xc4, 0xc5, 0xe4, 0x0b, 0x85, 0x75, 0x05,
+ 0x57, 0x07, 0x35, 0xbc, 0x94, 0x0b, 0xf1, 0xb2, 0x6e, 0x70, 0x8b, 0xcf,
+ 0x70, 0x3f, 0xf4, 0xa2, 0xa5, 0x19, 0xde, 0x28, 0x5b, 0xa8, 0xda, 0x27,
+ 0xc8, 0x1b, 0x32, 0xf4, 0x68, 0x41, 0x69, 0x04, 0xd3, 0x9a, 0x26, 0xff,
+ 0xce, 0x1f, 0x77, 0x83, 0x97, 0xa2, 0x1b, 0x15, 0x89, 0xc4, 0xfa, 0xf3,
+ 0x17, 0x41, 0xf4, 0x6a, 0x52, 0xde, 0x37, 0x1c, 0x4c, 0x3f, 0x99, 0x5b,
+ 0x90, 0x8e, 0x51, 0xc6, 0x54, 0xd7, 0x8a, 0xb6, 0xd9, 0x8e, 0x63, 0x1a,
+ 0x2a, 0xeb, 0x9d, 0xb2, 0xeb, 0x18, 0xb1, 0x13, 0xda, 0x7c, 0x23, 0x6d,
+ 0xa7, 0x0f, 0xe2, 0x47, 0x8a, 0x4e, 0x5e, 0xd6, 0xc3, 0x4f, 0x67, 0x67,
+ 0xaa, 0x5f, 0x00, 0xcf, 0xc0, 0x0d, 0xac, 0x24, 0x9a, 0xc5, 0x88, 0xfa,
+ 0x3e, 0xb7, 0x80, 0xd8, 0xaf, 0x34, 0xd4, 0xf4, 0xd9, 0xca, 0x74, 0xe8,
+ 0xcf, 0xa5, 0x5f, 0x88, 0x65, 0x39, 0xc2, 0xff, 0xf2, 0x05, 0x72, 0xe9,
+ 0x71, 0x8e, 0xe0, 0xc4, 0xc6, 0x93, 0xbb, 0x58, 0x7a, 0x26, 0x5a, 0x37,
+ 0x90, 0xeb, 0xd9, 0xab, 0xc0, 0xf3, 0xd3, 0x9f, 0x1e, 0xea, 0xcf, 0x39,
+ 0x67, 0x15, 0x09, 0xe3, 0x94, 0x17, 0xea, 0x88, 0xf3, 0x72, 0x81, 0x42,
+ 0xa0, 0xcc, 0x4d, 0x4e, 0xb6, 0x31, 0x25, 0x2d, 0x4a, 0xe8, 0xa7, 0xbb,
+ 0xef, 0xab, 0x94, 0x14, 0xda, 0xfc, 0xcb, 0x98, 0x28, 0x24, 0x2c, 0x8a,
+ 0x5d, 0x91, 0x18, 0x67, 0xa5, 0x40, 0x0c, 0x1f, 0x29, 0xbc, 0x30, 0x61,
+ 0xfb, 0x0d, 0x54, 0x07, 0x50, 0x2b, 0xcb, 0xca, 0xeb, 0xbb, 0xa6, 0x39,
+ 0x24, 0x80, 0x52, 0x88, 0xf7, 0x5c, 0xa0, 0x39, 0xc1, 0xe8, 0xb7, 0x10,
+ 0xb9, 0xb5, 0x29, 0xdf, 0x56, 0xd6, 0x40, 0xcc, 0xf4, 0x26, 0x5a, 0xfc,
+ 0x50, 0xd3, 0x18, 0x7b, 0x31, 0xe6, 0xce, 0xd8, 0x16, 0xac, 0xe3, 0xe2,
+ 0xa5, 0x0d, 0x20, 0xd0, 0x70, 0x57, 0x02, 0x38, 0x68, 0x9c, 0x32, 0x84,
+ 0xee, 0x9a, 0x1b, 0x8b, 0x33, 0x65, 0x32, 0x8a, 0xb2, 0xe0, 0xb2, 0x0a,
+ 0x1d, 0xa8, 0x28, 0x38, 0x16, 0x39, 0xf3, 0xdc, 0x2b, 0x14, 0x6b, 0x79,
+ 0x14, 0xc1, 0x63, 0x58, 0x54, 0x78, 0xe0, 0xf3, 0x65, 0x16, 0x3d, 0xc5,
+ 0x78, 0x20, 0xb1, 0x93, 0x83, 0xde, 0x57, 0xac, 0x15, 0xce, 0x56, 0x84,
+ 0x35, 0x45, 0x99, 0x89, 0x31, 0x71, 0x01, 0xa9, 0x43, 0x86, 0x5b, 0x71,
+ 0xb7, 0x8c, 0x7f, 0x1b, 0x14, 0x95, 0xe5, 0x74, 0xaa, 0x97, 0x57, 0xd6,
+ 0x41, 0xd9, 0xd4, 0xd9, 0xdb, 0x95, 0xe1, 0xde, 0xe0, 0x1e, 0xa1, 0x88,
+ 0xc2, 0x30, 0x72, 0xa9, 0x54, 0x53, 0x8d, 0x93, 0x75, 0xc3, 0x25, 0x2d,
+ 0x4a, 0x07, 0x19, 0xec, 0x0e, 0x74, 0x2b, 0x1e, 0x03, 0xc3, 0x5e, 0xbd,
+ 0x43, 0x8e, 0xaa, 0x66, 0x98, 0x50, 0xc7, 0xfb, 0xef, 0x2b, 0x37, 0x8e,
+ 0xd8, 0xb5, 0x1e, 0x4f, 0x10, 0x7f, 0xe3, 0x2b, 0xf4, 0x99, 0x3d, 0x04,
+ 0xc3, 0xed, 0x07, 0x23, 0xf2, 0xcd, 0x2a, 0x7d, 0x2d, 0xc3, 0x53, 0x80,
+ 0x30, 0x82, 0x8a, 0x0d, 0x89, 0xb2, 0xc7, 0xce, 0xf3, 0xbb, 0xd8, 0x9a,
+ 0x98, 0xb4, 0x72, 0x45, 0x7d, 0x68, 0xa8, 0xb7, 0x52, 0x15, 0x30, 0x9e,
+ 0xd0, 0x5c, 0x87, 0x47, 0x83, 0x0a, 0xa7, 0x83, 0x75, 0x29, 0x0a, 0x78,
+ 0x8a, 0x86, 0x73, 0x05, 0x87, 0x65, 0x48, 0x45, 0xec, 0x09, 0x5e, 0x9b,
+ 0xc4, 0xe5, 0x12, 0xf7, 0x3b, 0xec, 0xdf, 0x19, 0xb8, 0x81, 0x05, 0xc9,
+ 0x97, 0xc9, 0x1f, 0x1e, 0x13, 0xc4, 0x36, 0xee, 0x79, 0x82, 0x52, 0xfb,
+ 0x6a, 0x38, 0xe5, 0x63, 0x56, 0x43, 0x48, 0xf9, 0xf7, 0x61, 0xe1, 0xf8,
+ 0xc8, 0x12, 0xf9, 0x8e, 0xb4, 0x0a, 0xcd, 0xa4, 0xa8, 0xbf, 0x59, 0x42,
+ 0xca, 0x4b, 0x6b, 0x23, 0x6c, 0x77, 0x07, 0xaf, 0xfc, 0xea, 0x77, 0xd6,
+ 0xf1, 0xa6, 0xc9, 0xf0, 0xd4, 0x23, 0x95, 0xb0, 0xdb, 0xee, 0x60, 0xf0,
+ 0x92, 0x5e, 0xdb, 0xec, 0xac, 0x62, 0xcf, 0xc3, 0x15, 0x7f, 0xff, 0x4a,
+ 0x7c, 0x29, 0x66, 0x91, 0x17, 0x00, 0xee, 0x56, 0x2b, 0xa7, 0x8b, 0x94,
+ 0x3c, 0xd3, 0x38, 0x66, 0x00, 0x80, 0x4f, 0x69, 0x91, 0x0f, 0x25, 0xfe,
+ 0xbd, 0x51, 0xf7, 0xa1, 0xcb, 0x29, 0xee, 0x5f, 0x54, 0xc5, 0xe9, 0x66,
+ 0x5e, 0xa4, 0xbe, 0x98, 0xab, 0xb4, 0x3b, 0x1a, 0x9f, 0x4d, 0xc9, 0x7d,
+ 0xeb, 0x7c, 0xe8, 0xa2, 0x51, 0x82, 0xb0, 0x54, 0xaa, 0x38, 0x47, 0xc2,
+ 0x6e, 0x29, 0x71, 0x08, 0xfb, 0xc1, 0xcb, 0x27, 0xef, 0x81, 0xcb, 0x8e,
+ 0xb4, 0x16, 0x00, 0xa1, 0xb2, 0x6b, 0x47, 0x5b, 0x3b, 0x3b, 0xf2, 0x7c,
+ 0x2d, 0x78, 0xd9, 0x42, 0xca, 0xd3, 0x41, 0x3e, 0x9d, 0xfd, 0x50, 0x32,
+ 0x8f, 0xeb, 0xb1, 0x73, 0x17, 0xe0, 0xda, 0xa4, 0x07, 0x74, 0x50, 0xe1,
+ 0xe0, 0x0d, 0xe7, 0x0a, 0x2c, 0x27, 0x39, 0xad, 0x4c, 0xbf, 0x4a, 0xe7,
+ 0x13, 0xda, 0x76, 0x0b, 0x24, 0x0a, 0x0d, 0x5a, 0x17, 0x26, 0x15, 0x72,
+ 0x03, 0xa1, 0xd4, 0x06, 0x42, 0xc8, 0x27, 0x7a, 0x53, 0x32, 0x76, 0xe3,
+ 0xef, 0xbd, 0x23, 0x3c, 0xa6, 0xbd, 0x80, 0x12, 0x0d, 0x06, 0x50, 0x76,
+ 0x9a, 0xdd, 0x51, 0x4c, 0x87, 0x0c, 0x85, 0x91, 0x6f, 0x48, 0x49, 0x84,
+ 0x20, 0xa2, 0xa3, 0x51, 0x82, 0x9d, 0xb1, 0x77, 0xca, 0x11, 0x73, 0x8d,
+ 0xa1, 0xb6, 0x89, 0x76, 0x01, 0xe5, 0x58, 0xe6, 0xca, 0xee, 0x29, 0x22,
+ 0x8c, 0x68, 0x3b, 0xbc, 0x19, 0x8a, 0x20, 0xd8, 0x8d, 0xd6, 0x95, 0x53,
+ 0x84, 0x65, 0xe7, 0x6b, 0xb2, 0x74, 0x76, 0xc9, 0xe3, 0x9d, 0x04, 0x47,
+ 0x6d, 0x22, 0xc9, 0x17, 0xdf, 0x5d, 0x6f, 0x0a, 0x04, 0xfe, 0xbc, 0x82,
+ 0xca, 0x3c, 0xcb, 0xe1, 0xab, 0xc8, 0xdb, 0x34, 0xd2, 0xde, 0xd9, 0x79,
+ 0x84, 0xb2, 0x7e, 0x4d, 0x62, 0xc1, 0xf2, 0xa3, 0x65, 0xda, 0x0b, 0x0f,
+ 0xa5, 0x24, 0x73, 0xa9, 0x07, 0x4d, 0xfa, 0x1d, 0x6b, 0x46, 0x77, 0x7e,
+ 0xfa, 0xcd, 0x8c, 0x43, 0x26, 0x71, 0x65, 0x5e, 0x12, 0xea, 0xcc, 0x62,
+ 0x66, 0x16, 0xdb, 0x97, 0x47, 0x40, 0xe6, 0x62, 0x5c, 0x30, 0x8e, 0x56,
+ 0x50, 0x75, 0xe0, 0x0b, 0x49, 0xae, 0xa4, 0xbf, 0x44, 0xea, 0xee, 0xe8,
+ 0xeb, 0x05, 0xf6, 0x9e, 0x44, 0xd0, 0x01, 0xc0, 0x4e, 0x92, 0xb9, 0x0e,
+ 0xe2, 0x48, 0xe0, 0xb6, 0xee, 0x4e, 0x7e, 0xec, 0xdf, 0x93, 0xe7, 0x39,
+ 0x3c, 0xc5, 0xf2, 0x21, 0xe9, 0x83, 0x77, 0x2e, 0xd7, 0x08, 0xc2, 0x05,
+ 0x14, 0x81, 0x57, 0x69, 0xf6, 0x5d, 0x31, 0x86, 0x4a, 0x55, 0xcd, 0xeb,
+ 0x96, 0xf1, 0xba, 0x3b, 0x80, 0x3d, 0x06, 0xd7, 0xb8, 0x04, 0xda, 0x66,
+ 0x0e, 0x04, 0x2a, 0x39, 0x8d, 0xfb, 0xe2, 0x1d, 0xe6, 0x94, 0x7b, 0x9c,
+ 0x12, 0xbb, 0xf0, 0x57, 0x13, 0x05, 0x02, 0xd3, 0xe5, 0x8d, 0x2e, 0x3a,
+ 0x19, 0x48, 0x46, 0xbc, 0x32, 0xdb, 0xaf, 0xa8, 0xf0, 0x8f, 0xd5, 0xd0,
+ 0xd4, 0xac, 0xda, 0xcb, 0x01, 0xbc, 0x02, 0xc9, 0xce, 0x33, 0x08, 0x42,
+ 0x45, 0x3c, 0xb3, 0x13, 0x2c, 0x24, 0xaa, 0x92, 0x37, 0x48, 0xbf, 0x40,
+ 0xed, 0xe0, 0xe6, 0xc8, 0x77, 0xb3, 0x53, 0xa5, 0x28, 0xd8, 0x19, 0xfc,
+ 0x33, 0xf5, 0x5d, 0x51, 0xa5, 0x2a, 0xb2, 0xb1, 0x6e, 0x57, 0x9b, 0x48,
+ 0xdc, 0x00, 0x22, 0xa9, 0x0d, 0x23, 0x7a, 0xb4, 0xb4, 0x3d, 0x8a, 0x95,
+ 0xb3, 0x9e, 0x6b, 0x03, 0x87, 0x69, 0xb0, 0x2f, 0xcb, 0x69, 0x7b, 0xb2,
+ 0xc6, 0x8d, 0x51, 0x3b, 0x73, 0x80, 0x9f, 0x04, 0xb1, 0x9f, 0x4d, 0x97,
+ 0x7a, 0xb8, 0x76, 0x01, 0xf9, 0x77, 0xfe, 0x31, 0x7f, 0x6b, 0x6c, 0xd0,
+ 0x45, 0x85, 0x1b, 0xd3, 0xab, 0x7b, 0x11, 0x99, 0xfa, 0x36, 0xad, 0x25,
+ 0xaf, 0x57, 0x87, 0x3c, 0xa9, 0x4c, 0xe0, 0x42, 0xa9, 0x0c, 0x47, 0x7a,
+ 0x8b, 0xb4, 0x54, 0x31, 0xf3, 0x77, 0xee, 0x08, 0xb2, 0x8e, 0x07, 0x9e,
+ 0x2a, 0x41, 0x8f, 0xf7, 0x76, 0x17, 0x83, 0x26, 0xb4, 0x80, 0xad, 0x27,
+ 0x2d, 0x59, 0x3a, 0xca, 0x9d, 0x0b, 0xbf, 0x79, 0x39, 0x8b, 0x20, 0xf0,
+ 0x7d, 0x4c, 0xe9, 0x0e, 0xd9, 0x46, 0x39, 0x60, 0x0e, 0xa4, 0x7f, 0x8d,
+ 0xf5, 0x0c, 0x8c, 0xbb, 0x25, 0x78, 0xe7, 0x43, 0x09, 0x87, 0xad, 0x20,
+ 0xfc, 0x51, 0xa6, 0x71, 0x6a, 0xdf, 0xfd, 0xde, 0x29, 0x6d, 0xbb, 0x19,
+ 0x73, 0x3a, 0x5e, 0xa5, 0x41, 0x2c, 0xef, 0x5b, 0x42, 0x57, 0xc6, 0x6f,
+ 0x8e, 0x63, 0xf1, 0x0d, 0x4b, 0xb7, 0xb2, 0xa4, 0x94, 0x67, 0x35, 0xa4,
+ 0xae, 0x7e, 0x8d, 0x07, 0xe5, 0x3c, 0x87, 0xa4, 0xd9, 0x25, 0x7d, 0xde,
+ 0xb6, 0x9e, 0x85, 0x3b, 0xc7, 0x8c, 0x12, 0x41, 0xa1, 0x85, 0x00, 0xd9,
+ 0xcb, 0xc7, 0x4d, 0x1d, 0xac, 0xc7, 0x0e, 0x46, 0x4e, 0x7d, 0xb1, 0x42,
+ 0x9d, 0x8a, 0x26, 0x8c, 0x3e, 0x21, 0x8e, 0x82, 0xa1, 0x33, 0xa8, 0xe2,
+ 0xf6, 0x84, 0x36, 0xc2, 0x3b, 0x29, 0x14, 0x90, 0xaf, 0x84, 0x9a, 0x78,
+ 0x7e, 0x85, 0x3d, 0x7f, 0x30, 0x10, 0x4c, 0xcd, 0x12, 0x3e, 0x40, 0xe9,
+ 0xd0, 0x0c, 0x84, 0x69, 0xdf, 0xac, 0x2d, 0xc0, 0x7d, 0xdc, 0x3b, 0xef,
+ 0x69, 0x82, 0x02, 0x65, 0x81, 0xdb, 0x57, 0x3b, 0xa8, 0x56, 0x2a, 0xe7,
+ 0x11, 0x0b, 0x34, 0x3e, 0x6f, 0x81, 0x63, 0xe8, 0xd4, 0x3c, 0x18, 0xcf,
+ 0xdc, 0xff, 0x15, 0x3e, 0xe9, 0x98, 0xf8, 0x66, 0x1a, 0x44, 0x50, 0xb2,
+ 0xec, 0xbf, 0xc9, 0xc8, 0x1b, 0xe4, 0x11, 0xe5, 0x00, 0x76, 0x01, 0x6d,
+ 0x91, 0xde, 0xd4, 0xa8, 0x2e, 0x33, 0xa6, 0x69, 0x08, 0x15, 0xf7, 0x42,
+ 0xfa, 0x11, 0x93, 0xb8, 0x4f, 0x27, 0xdd, 0x35, 0xdf, 0x3a, 0x8f, 0x19,
+ 0xd1, 0x9f, 0x72, 0x15, 0x1d, 0xad, 0x32, 0x8e, 0xb8, 0x28, 0xd6, 0xad,
+ 0xe5, 0xad, 0xbe, 0x0f, 0x75, 0x7d, 0x14, 0xbc, 0x46, 0x1e, 0x13, 0x6b,
+ 0x86, 0xdf, 0x2f, 0xd1, 0xfd, 0x51, 0x7e, 0x88, 0x7d, 0xf8, 0xc0, 0x31,
+ 0x67, 0x31, 0x8a, 0x6b, 0x28, 0x35, 0xe2, 0x06, 0xa1, 0xa0, 0x73, 0x5c,
+ 0x40, 0x57, 0xb7, 0x26, 0x0e, 0x84, 0xab, 0x5b, 0x9e, 0x6d, 0x01, 0x34,
+ 0x51, 0x6f, 0x6f, 0xfd, 0x7f, 0x0d, 0x33, 0x50, 0x98, 0x7d, 0xd1, 0xd0,
+ 0x05, 0xaf, 0x27, 0x4c, 0x5a, 0xa9, 0xa8, 0x0a, 0x4e, 0x24, 0x6b, 0x67,
+ 0x23, 0xd3, 0xb9, 0x16, 0x6e, 0x61, 0x25, 0x63, 0xad, 0x19, 0xab, 0xe8,
+ 0x65, 0x8f, 0x0f, 0xb4, 0xa6, 0xd8, 0x4f, 0x78, 0xec, 0xa2, 0x36, 0x5f,
+ 0x3b, 0xe0, 0xcb, 0xd5, 0x85, 0x64, 0xa0, 0x1f, 0x96, 0x4f, 0xef, 0x0f,
+ 0x79, 0x99, 0xe4, 0x67, 0x47, 0x40, 0xe8, 0xc5, 0xdf, 0x05, 0x63, 0xa3,
+ 0x24, 0x04, 0x7e, 0x98, 0x3c, 0x8c, 0xfb, 0xec, 0x96, 0xb4, 0x41, 0x49,
+ 0x93, 0x07, 0xad, 0x84, 0xe2, 0x23, 0x36, 0xcc, 0x57, 0x60, 0xa0, 0xc2,
+ 0xbb, 0xc8, 0x8a, 0xa5, 0x67, 0x61, 0x2a, 0x40, 0xbe, 0x0e, 0xae, 0x62,
+ 0xa5, 0x1e, 0x09, 0xe1, 0x32, 0x43, 0xd1, 0xb1, 0x25, 0xdd, 0xeb, 0x2f,
+ 0x92, 0x55, 0x86, 0x9f, 0x1a, 0xa6, 0xd7, 0x7c, 0x91, 0xb4, 0xab, 0xde,
+ 0x80, 0xd4, 0xc6, 0x42, 0x1f, 0x63, 0x45, 0xca, 0xbf, 0xd2, 0x81, 0xec,
+ 0x5f, 0xf8, 0x0a, 0xd6, 0x81, 0x5e, 0x90, 0x3d, 0x57, 0x33, 0x31, 0xf2,
+ 0x29, 0x2c, 0x66, 0x78, 0xce, 0xbf, 0xb5, 0xca, 0x15, 0xca, 0xad, 0x3f,
+ 0xcc, 0x4c, 0xc9, 0x15, 0xb4, 0xff, 0x60, 0x2f, 0xe6, 0x3c, 0x34, 0x46,
+ 0x4b, 0x13, 0xe8, 0x03, 0xf1, 0xfa, 0xa9, 0x4d, 0x5b, 0x2a, 0x3e, 0xfd,
+ 0x62, 0x7e, 0x7b, 0x84, 0x2e, 0x53, 0xa0, 0x8e, 0x6f, 0xd4, 0xaa, 0xb8,
+ 0x29, 0x71, 0xfc, 0x0f, 0xb1, 0x7f, 0x54, 0x00, 0x16, 0x14, 0xd2, 0xd2,
+ 0x1a, 0xd7, 0xec, 0x56, 0xcf, 0xed, 0xc1, 0x6d, 0xa5, 0x5e, 0xfc, 0xcf,
+ 0x42, 0x43, 0x8a, 0xa6, 0xc0, 0x6c, 0x13, 0xbf, 0xdd, 0xaa, 0x94, 0x08,
+ 0xfb, 0x4e, 0x24, 0xce, 0x4d, 0xb8, 0xc9, 0x16, 0xb7, 0xf1, 0x04, 0x1a,
+ 0x8c, 0x9c, 0xc2, 0x19, 0x91, 0x4f, 0xb3, 0x9f, 0x56, 0xc7, 0x30, 0xc4,
+ 0x40, 0x7c, 0x32, 0xff, 0xd2, 0x2d, 0x3d, 0x35, 0xd1, 0x76, 0x40, 0x4b,
+ 0xb9, 0x11, 0x05, 0x92, 0x85, 0x02, 0x57, 0xc4, 0xea, 0x67, 0xe9, 0xf0,
+ 0xd8, 0x9f, 0x06, 0xa2, 0x79, 0xe0, 0x6f, 0x01, 0x81, 0xff, 0xd8, 0x99,
+ 0xd7, 0x82, 0xbb, 0x5e, 0x1a, 0x7c, 0x44, 0xc4, 0xb0, 0xb8, 0x9a, 0xba,
+ 0x60, 0x22, 0x93, 0xb0, 0x0b, 0x0e, 0x1d, 0xbc, 0x1d, 0x94, 0xad, 0x60,
+ 0xbd, 0xed, 0xb2, 0x91, 0x73, 0xef, 0xdc, 0x6b, 0xfc, 0x04, 0xf1, 0xcb,
+ 0x52, 0xb8, 0xa1, 0xf4, 0x13, 0x7e, 0x12, 0xef, 0xac, 0x42, 0x5a, 0x35,
+ 0x3e, 0x56, 0x20, 0x5d, 0x28, 0x4a, 0x9d, 0xc4, 0xd7, 0x81, 0x91, 0x97,
+ 0x2d, 0x62, 0xc3, 0x03, 0x8c, 0x2e, 0xd3, 0x77, 0x62, 0x23, 0x5a, 0xb8,
+ 0xbb, 0x76, 0x97, 0x2f, 0x12, 0x65, 0x11, 0x96, 0x38, 0x29, 0xf9, 0xee,
+ 0xd3, 0x63, 0xd5, 0xfb, 0x6a, 0xc1, 0x1e, 0x83, 0x87, 0xea, 0x6b, 0xe0,
+ 0xe9, 0xcb, 0xeb, 0x68, 0xca, 0x4c, 0x76, 0x60, 0x55, 0xc1, 0x5b, 0x6a,
+ 0x3a, 0xf2, 0xab, 0xbc, 0xc4, 0x36, 0x10, 0xe2, 0xce, 0xa0, 0x91, 0x99,
+ 0x5d, 0xd9, 0x9e, 0xae, 0x1d, 0xaa, 0xcb, 0xfb, 0x96, 0xe7, 0x6c, 0x56,
+ 0x00, 0xcc, 0xf3, 0x4f, 0xcf, 0x32, 0x82, 0x4d, 0x25, 0x99, 0x38, 0x84,
+ 0xd4, 0x14, 0x28, 0xbd, 0x7a, 0x1e, 0xd0, 0x3d, 0x12, 0x7d, 0x5a, 0x85,
+ 0x62, 0xfb, 0x4c, 0xfe, 0x61, 0xc0, 0xda, 0x5c, 0x29, 0xdc, 0xe0, 0xbf,
+ 0x50, 0x3f, 0x58, 0x4b, 0x10, 0xe3, 0x38, 0xe1, 0xcb, 0xf9, 0xd9, 0xf6,
+ 0xf2, 0x83, 0xc9, 0xe1, 0x3d, 0x11, 0xe7, 0xe9, 0x26, 0x5e, 0xd1, 0xf6,
+ 0x67, 0xe9, 0xbd, 0x11, 0x17, 0xd9, 0xaf, 0x44, 0xc5, 0x75, 0x69, 0x19,
+ 0x98, 0x9c, 0x35, 0x39, 0xda, 0xe6, 0x1c, 0x65, 0x55, 0xac, 0xfc, 0x0a,
+ 0xf0, 0xc1, 0x57, 0x5b, 0xc1, 0x59, 0x86, 0x52, 0x38, 0x0e, 0xa6, 0x3a,
+ 0x43, 0x07, 0x9e, 0x7c, 0x93, 0x65, 0x88, 0x91, 0x8f, 0x48, 0xf7, 0x64,
+ 0x7e, 0xe9, 0x38, 0xaf, 0xd6, 0x93, 0x41, 0xf4, 0x48, 0xd9, 0x28, 0x70,
+ 0xf9, 0x75, 0xf7, 0x8d, 0xd2, 0x5b, 0x65, 0x4e, 0x01, 0x5e, 0x6a, 0x19,
+ 0x53, 0xde, 0x13, 0x89, 0xa4, 0x45, 0x96, 0xe9, 0x37, 0x2d, 0x5f, 0xeb,
+ 0xb7, 0x48, 0xda, 0x24, 0xf9, 0x5f, 0xed, 0xb0, 0xf3, 0x1d, 0x29, 0x19,
+ 0x2a, 0xe7, 0xa8, 0xe0, 0x93, 0xa9, 0x48, 0x1e, 0xc2, 0xe5, 0x0a, 0x7a,
+ 0x75, 0x06, 0xe1, 0x7f, 0xc4, 0xf4, 0x65, 0xff, 0x9a, 0x3b, 0x93, 0x8f,
+ 0x71, 0xee, 0x92, 0xad, 0xb9, 0x20, 0x1c, 0x76, 0x72, 0x45, 0x95, 0xa6,
+ 0xd8, 0xec, 0x97, 0x23, 0x5e, 0x37, 0x3e, 0x28, 0xf2, 0x3f, 0xc7, 0xdf,
+ 0x46, 0x04, 0x32, 0x79, 0xd3, 0x5b, 0xd8, 0xab, 0x35, 0x34, 0x51, 0xe4,
+ 0x3f, 0x69, 0xb4, 0xd9, 0xe3, 0xd4, 0xc6, 0xbb, 0x2f, 0x54, 0x4b, 0xaa,
+ 0x1d, 0x91, 0xd3, 0xd0, 0xc7, 0xe0, 0x78, 0x9c, 0xc9, 0x34, 0xc3, 0xbb,
+ 0x1c, 0x1d, 0x15, 0xa8, 0x8c, 0x4f, 0x2d, 0x93, 0x10, 0x4a, 0x15, 0x0f,
+ 0x0e, 0xa8, 0x7d, 0x66, 0x70, 0x63, 0xa8, 0x2d, 0x0b, 0x6c, 0xce, 0x86,
+ 0xb4, 0xd8, 0xc6, 0x09, 0xdd, 0xcd, 0x98, 0x74, 0x49, 0x03, 0xf6, 0xb3,
+ 0x6b, 0x10, 0xb1, 0x17, 0x04, 0x36, 0x10, 0x1d, 0x1e, 0x75, 0xa3, 0xdb,
+ 0x9b, 0x5a, 0x39, 0x1c, 0xfa, 0x56, 0x5d, 0x35, 0x11, 0x16, 0x5d, 0x3b,
+ 0x0d, 0x39, 0xd9, 0x3a, 0x82, 0x2c, 0x56, 0xd1, 0xb2, 0xa0, 0x56, 0x85,
+ 0xf4, 0xdd, 0x2c, 0xe6, 0x5d, 0xa1, 0x4a, 0x88, 0x06, 0x00, 0x1a, 0x59,
+ 0xbb, 0xd0, 0x68, 0x29, 0x0b, 0xba, 0xa5, 0xc6, 0x89, 0xe6, 0x4b, 0x9c,
+ 0xdc, 0xf1, 0x12, 0x63, 0x64, 0x75, 0xc7, 0xc4, 0xaa, 0xdc, 0x66, 0x3e,
+ 0xaf, 0xa5, 0xdf, 0xef, 0xb0, 0x7f, 0x6d, 0x7b, 0x22, 0x70, 0xdb, 0x33,
+ 0x14, 0x45, 0x74, 0xf6, 0xe1, 0x2c, 0x47, 0x5e, 0x6e, 0xa6, 0x3c, 0x58,
+ 0x8d, 0xcc, 0x03, 0x12, 0x8b, 0xc6, 0xd0, 0xf7, 0x4d, 0xab, 0xcd, 0x90,
+ 0x84, 0x9b, 0xb9, 0x3b, 0xe5, 0xe2, 0x23, 0x00, 0x3d, 0xfd, 0x8f, 0xba,
+ 0x33, 0x71, 0x7b, 0xf9, 0xeb, 0xe8, 0x80, 0xe4, 0x61, 0x5d, 0x4a, 0xd7,
+ 0xe8, 0x0e, 0x12, 0x05, 0xa7, 0xcd, 0x40, 0xeb, 0xe4, 0x59, 0x1c, 0xeb,
+ 0x9a, 0xb1, 0xf4, 0xb7, 0x99, 0x26, 0xd1, 0x30, 0xc5, 0x66, 0xfc, 0x50,
+ 0xd6, 0x7d, 0x63, 0xe9, 0x0e, 0xa5, 0xf3, 0x3d, 0x8e, 0x98, 0xc3, 0xde,
+ 0x4a, 0xc9, 0xd7, 0xaf, 0xae, 0x73, 0x8d, 0xa3, 0x63, 0x78, 0xc8, 0x68,
+ 0xe7, 0x7f, 0xb7, 0x75, 0xe0, 0x2a, 0x52, 0x6b, 0xdb, 0x27, 0xe0, 0x94,
+ 0xab, 0x75, 0xd0, 0x84, 0xec, 0x65, 0xec, 0x2b, 0x53, 0x3a, 0x34, 0xf4,
+ 0x75, 0x46, 0x5e, 0x66, 0x2c, 0xf5, 0x40, 0x29, 0x84, 0xc6, 0x4d, 0xee,
+ 0xb6, 0x7e, 0xdb, 0x4d, 0x61, 0xe4, 0xe0, 0xd6, 0xfb, 0xb9, 0x7d, 0xd5,
+ 0xc5, 0xcb, 0x32, 0x7d, 0x9b, 0x9c, 0x2c, 0x0e, 0xbe, 0xea, 0xab, 0xd0,
+ 0x12, 0x90, 0x21, 0x06, 0x37, 0xaa, 0xeb, 0x8f, 0xdd, 0x08, 0x68, 0xbc,
+ 0xda, 0xd7, 0x1c, 0x11, 0x14, 0x7a, 0x4d, 0xf2, 0xf8, 0xf3, 0x10, 0xd2,
+ 0x5e, 0xca, 0x10, 0x15, 0xfa, 0x77, 0xd8, 0xb9, 0x7f, 0xfe, 0x6b, 0xa8,
+ 0x7a, 0xea, 0x0a, 0xe4, 0xd1, 0x2e, 0x2f, 0xa2, 0x81, 0x6f, 0xf5, 0xca,
+ 0xa6, 0xd2, 0x70, 0x34, 0xff, 0x69, 0x72, 0x6b, 0x20, 0x69, 0x8c, 0x9a,
+ 0x38, 0x5e, 0xfd, 0x95, 0xef, 0xb5, 0x07, 0x94, 0x2a, 0x43, 0x58, 0x45,
+ 0xa0, 0xf6, 0x13, 0xdd, 0x16, 0xdc, 0xa1, 0x65, 0xbc, 0xc8, 0x2e, 0x86,
+ 0xa0, 0x2a, 0xee, 0xb6, 0x22, 0x29, 0xeb, 0xca, 0x54, 0x97, 0x60, 0x33,
+ 0x84, 0xb6, 0xbb, 0xa6, 0x26, 0x4e, 0x55, 0x8c, 0x1f, 0xc3, 0x6e, 0x92,
+ 0xa5, 0xcf, 0x39, 0x67, 0xd5, 0x69, 0x8f, 0x7f, 0xa0, 0x4c, 0x7c, 0x8e,
+ 0x33, 0x3f, 0xcb, 0x14, 0x13, 0x77, 0xae, 0x86, 0xb9, 0x49, 0xe1, 0x53,
+ 0x9c, 0x2c, 0x8a, 0xc6, 0x7b, 0xd9, 0x0a, 0xc5, 0x10, 0x9d, 0xde, 0x8d,
+ 0x9d, 0x74, 0x34, 0xb5, 0xf6, 0xbe, 0x04, 0xa7, 0x56, 0xda, 0xd4, 0x8a,
+ 0x70, 0xdd, 0x50, 0x96, 0xe8, 0x65, 0x3a, 0xa7, 0x43, 0xe8, 0xc4, 0x19,
+ 0xfa, 0xfe, 0xb3, 0xa6, 0x72, 0x76, 0xd6, 0xdf, 0x8a, 0xe6, 0x8e, 0x1a,
+ 0x51, 0xa6, 0x21, 0x8f, 0x80, 0xd3, 0x48, 0xa1, 0xc3, 0xd4, 0xd5, 0xde,
+ 0x66, 0x84, 0x80, 0x68, 0x2b, 0x14, 0xa2, 0x75, 0xe5, 0x99, 0x00, 0x52,
+ 0xa5, 0x0a, 0x11, 0xd7, 0x85, 0x6b, 0xc8, 0xc6, 0x74, 0x16, 0xc8, 0x14,
+ 0x6a, 0xcb, 0x02, 0x91, 0x02, 0x05, 0x82, 0xca, 0x8b, 0x94, 0x2a, 0x3e,
+ 0x51, 0x6b, 0x9d, 0xda, 0x73, 0xf0, 0x77, 0x46, 0x90, 0x34, 0x22, 0xa7,
+ 0xa7, 0xd8, 0x29, 0xbf, 0x13, 0x6d, 0x65, 0x02, 0x0c, 0x01, 0x3b, 0x75,
+ 0xa1, 0x00, 0xb7, 0x09, 0xf8, 0x65, 0x04, 0x08, 0x2f, 0xab, 0xf0, 0x6b,
+ 0x28, 0x53, 0x9b, 0xdc, 0xbd, 0x3f, 0xf0, 0x5a, 0xd3, 0x47, 0x5c, 0x52,
+ 0xb9, 0xa6, 0xba, 0x04, 0x57, 0x39, 0x71, 0x13, 0x4c, 0xa1, 0xaf, 0xc6,
+ 0x46, 0x5e, 0x3e, 0x4c, 0xa1, 0xa2, 0xf0, 0x41, 0x95, 0x42, 0x3f, 0x64,
+ 0xbb, 0x45, 0xbc, 0xc2, 0x00, 0x87, 0x42, 0x0c, 0x8e, 0xdd, 0xcd, 0x37,
+ 0x57, 0xe5, 0x39, 0xd8, 0x81, 0x5d, 0x07, 0x85, 0x34, 0x52, 0x05, 0xb3,
+ 0x0f, 0x0a, 0x46, 0x2d, 0x40, 0xac, 0x3a, 0xd8, 0x4e, 0xf9, 0xc7, 0xbe,
+ 0xfd, 0x5c, 0xdd, 0x83, 0x60, 0x07, 0xda, 0x7d, 0x45, 0x1f, 0x21, 0xf8,
+ 0xa5, 0xe2, 0x2d, 0xe8, 0xe5, 0x2f, 0xc6, 0x19, 0x16, 0x4b, 0x2f, 0x48,
+ 0x72, 0x57, 0x30, 0xcd, 0xa6, 0x27, 0xa6, 0x19, 0x41, 0x64, 0x73, 0x1b,
+ 0x29, 0x3d, 0x4b, 0x10, 0xa5, 0x41, 0x22, 0x6b, 0x9c, 0x82, 0xc4, 0xb9,
+ 0x69, 0xa3, 0x9c, 0x9d, 0x8a, 0xcd, 0x17, 0x2e, 0xa0, 0x12, 0x9e, 0xf2,
+ 0x22, 0x54, 0xe7, 0xaa, 0x50, 0x92, 0xc6, 0xed, 0x77, 0xd4, 0x89, 0xe8,
+ 0xcf, 0x73, 0x6f, 0x66, 0x55, 0x6c, 0xb3, 0x5d, 0x4b, 0x8d, 0x5b, 0x72,
+ 0xb0, 0xa8, 0xfb, 0x18, 0x37, 0xae, 0x85, 0x25, 0x07, 0x8b, 0xfe, 0x67,
+ 0x9b, 0x2a, 0x99, 0x7b, 0x2d, 0x94, 0x6f, 0x6d, 0x9e, 0x83, 0x7d, 0xf0,
+ 0xec, 0x24, 0x3c, 0x2d, 0xf6, 0x4d, 0x0a, 0x08, 0xbb, 0x54, 0xbd, 0x01,
+ 0x7c, 0xd3, 0xf5, 0x4f, 0x7f, 0xac, 0xc6, 0x33, 0x4c, 0x78, 0x7f, 0x7a,
+ 0xd1, 0x0f, 0xad, 0xb9, 0x10, 0x4a, 0xba, 0x26, 0x19, 0xf5, 0xe0, 0x36,
+ 0xbf, 0xf7, 0xff, 0x79, 0x91, 0xba, 0x0e, 0x35, 0x0c, 0x53, 0xb0, 0x0b,
+ 0xd6, 0xd9, 0x84, 0x36, 0xa2, 0x85, 0xa3, 0x88, 0x1c, 0x7f, 0xde, 0xb7,
+ 0x3b, 0xb9, 0x8b, 0x38, 0x85, 0x91, 0xf9, 0x8b, 0x41, 0xc0, 0x92, 0x8b,
+ 0x8d, 0x1d, 0x74, 0x99, 0x6b, 0xa9, 0x1c, 0xd9, 0x2d, 0xff, 0xfb, 0x01,
+ 0xd6, 0xa3, 0xf0, 0x57, 0xbe, 0x67, 0x15, 0xa9, 0xa2, 0x30, 0xc8, 0x5a,
+ 0xb0, 0xf1, 0x31, 0x95, 0xb7, 0x86, 0x37, 0xa7, 0xd4, 0xb7, 0xf4, 0x2c,
+ 0xb9, 0xb4, 0x1c, 0x5e, 0x37, 0xd6, 0x1c, 0x4d, 0x83, 0xe5, 0x77, 0xdd,
+ 0xa7, 0xd9, 0x31, 0xc7, 0xc1, 0x47, 0x5b, 0x81, 0x9f, 0x08, 0xb4, 0x51,
+ 0x1f, 0x5e, 0x27, 0x7c, 0xde, 0xe4, 0x7f, 0xc2, 0x2d, 0x91, 0x38, 0x2d,
+ 0x35, 0x44, 0x0a, 0xbf, 0xf0, 0xd6, 0x29, 0xca, 0xae, 0x2e, 0x67, 0xba,
+ 0xc7, 0xe4, 0xd1, 0xb7, 0x7a, 0x5b, 0xff, 0xcf, 0xd6, 0x0b, 0x1b, 0x02,
+ 0xc0, 0x7b, 0x56, 0x51, 0x61, 0x8d, 0xf6, 0x0f, 0x88, 0x5c, 0xe1, 0xe1,
+ 0x8b, 0x79, 0x65, 0xfb, 0x11, 0xf1, 0x8e, 0x11, 0x78, 0xed, 0x89, 0xc5,
+ 0x13, 0xd1, 0xe0, 0x2e, 0xd5, 0xd4, 0x0c, 0x86, 0xca, 0xd5, 0xa0, 0x12,
+ 0x0e, 0xc1, 0xdf, 0x57, 0xcc, 0xb9, 0x64, 0x0c, 0x37, 0x77, 0xbc, 0x5c,
+ 0x94, 0xe4, 0x60, 0x80, 0xfb, 0x6e, 0x44, 0xa8, 0xa7, 0xa3, 0xd1, 0xec,
+ 0x0d, 0x5e, 0x7b, 0xa0, 0x0e, 0x2c, 0x52, 0x0a, 0x7e, 0xb7, 0x21, 0xe4,
+ 0x87, 0x75, 0xb1, 0x4e, 0x12, 0x45, 0x82, 0xe4, 0xe6, 0x68, 0x1b, 0xc6,
+ 0x90, 0xc0, 0x09, 0xfc, 0xc1, 0x8b, 0x26, 0x60, 0xa5, 0xf6, 0xb9, 0x16,
+ 0x75, 0x83, 0xc7, 0x99, 0xab, 0xc3, 0x90, 0x3e, 0x7b, 0x1f, 0xde, 0xd5,
+ 0x93, 0x08, 0xf8, 0x11, 0x7a, 0xfc, 0x66, 0xc6, 0xb1, 0xff, 0x82, 0x89,
+ 0x63, 0x64, 0xd4, 0xd6, 0x84, 0xb9, 0x49, 0x1e, 0x3a, 0x26, 0xaf, 0x05,
+ 0xfc, 0xdb, 0xc4, 0xf2, 0xa5, 0xd8, 0xec, 0xd1, 0xe4, 0xfb, 0xae, 0x7e,
+ 0x8a, 0x34, 0x33, 0x31, 0x9d, 0x81, 0xd1, 0x6e, 0xf7, 0x95, 0x1d, 0xc5,
+ 0x4c, 0x3a, 0x92, 0x02, 0x05, 0xa8, 0x89, 0x37, 0x89, 0x28, 0x1a, 0xaf,
+ 0xa8, 0x3c, 0xe9, 0x1c, 0x2e, 0x47, 0xb3, 0xc9, 0xb9, 0xbf, 0x59, 0x1a,
+ 0xed, 0xf1, 0xa9, 0xeb, 0xb5, 0x5a, 0x73, 0x22, 0xa2, 0x65, 0x5a, 0xcc,
+ 0xef, 0x69, 0x8e, 0xb0, 0x9c, 0xe9, 0xb8, 0x06, 0x69, 0x44, 0x58, 0xb2,
+ 0x42, 0x45, 0x09, 0x71, 0xa8, 0x17, 0x57, 0xd8, 0xd9, 0x61, 0x63, 0x79,
+ 0x13, 0x2b, 0xee, 0xf8, 0x59, 0x1e, 0x71, 0x19, 0x69, 0x05, 0xea, 0xb0,
+ 0xef, 0xde, 0x3b, 0xef, 0x85, 0x2a, 0x76, 0x57, 0xb5, 0x7c, 0xf0, 0x6b,
+ 0xa9, 0x89, 0x9a, 0x48, 0xc8, 0x74, 0x5a, 0x47, 0xcd, 0xcc, 0x1a, 0x1c,
+ 0x80, 0x6e, 0xd1, 0xd9, 0xab, 0x09, 0x02, 0x66, 0x86, 0x73, 0x85, 0xbe,
+ 0x45, 0x26, 0x9a, 0x74, 0xa9, 0x82, 0xd6, 0xa1, 0xc3, 0xf3, 0x24, 0x7f,
+ 0xe4, 0x5b, 0xd0, 0xf3, 0x29, 0xd9, 0xd1, 0x00, 0x9a, 0xa8, 0x67, 0x1f,
+ 0xa4, 0xcd, 0x17, 0x7c, 0x9d, 0x2d, 0xaa, 0x34, 0x1b, 0xba, 0x39, 0x53,
+ 0x10, 0x8a, 0xea, 0x65, 0xbe, 0xb4, 0x2c, 0x22, 0xe6, 0xea, 0x7e, 0x25,
+ 0x65, 0x41, 0xf9, 0xf1, 0xe0, 0x8d, 0x14, 0xfd, 0x78, 0x9c, 0x7b, 0xb1,
+ 0x67, 0x15, 0xee, 0x67, 0x49, 0x0a, 0x76, 0x1f, 0x21, 0x66, 0xb5, 0xd3,
+ 0x38, 0x43, 0x9e, 0x10, 0xfa, 0xd7, 0x6b, 0xf3, 0x6b, 0x67, 0xa2, 0x76,
+ 0xa4, 0x56, 0x47, 0x13, 0x29, 0xb6, 0x8d, 0xe9, 0xe7, 0xc8, 0x10, 0xe6,
+ 0x4f, 0x2c, 0x68, 0xd3, 0xfb, 0x83, 0xfa, 0xd9, 0xfc, 0x9f, 0xe7, 0x30,
+ 0xbe, 0x0a, 0x0d, 0xf2, 0xca, 0x18, 0xcb, 0x83, 0x86, 0x6e, 0x83, 0x32,
+ 0x0b, 0xd9, 0xe8, 0x06, 0x40, 0x59, 0x7b, 0x12, 0x42, 0x6b, 0xf1, 0xa2,
+ 0xaf, 0xe2, 0x1d, 0x0d, 0x11, 0xd9, 0x59, 0x0e, 0xa3, 0x49, 0xbd, 0xdd,
+ 0x1a, 0x29, 0x09, 0x4e, 0x6e, 0x0b, 0x4f, 0x88, 0x5d, 0x79, 0xda, 0x93,
+ 0x08, 0x65, 0xd9, 0x56, 0x4b, 0xef, 0x8c, 0x44, 0xe8, 0x14, 0x58, 0x6b,
+ 0xfc, 0x9f, 0x8b, 0x57, 0x59, 0xe6, 0xdd, 0xd6, 0xf2, 0x3e, 0x2a, 0x7e,
+ 0x80, 0x7f, 0x52, 0x14, 0x86, 0xe9, 0xe1, 0xc5, 0x5f, 0x47, 0x07, 0x85,
+ 0x99, 0xa7, 0x4e, 0x0c, 0x37, 0x6c, 0xdc, 0x60, 0xe9, 0xe7, 0x5e, 0xe0,
+ 0x35, 0x98, 0xe5, 0x30, 0x3d, 0x12, 0xdb, 0x41, 0x3c, 0xce, 0xee, 0x05,
+ 0xb7, 0xb1, 0x13, 0xcc, 0x69, 0xf5, 0xcc, 0xe7, 0xa8, 0x38, 0x5a, 0xb5,
+ 0xfb, 0x65, 0xac, 0xc5, 0xa1, 0x0a, 0x34, 0x08, 0x93, 0x15, 0x5b, 0xfa,
+ 0x18, 0x20, 0x00, 0x05, 0x01, 0x13, 0xe9, 0x07, 0xb5, 0x9d, 0x98, 0x7f,
+ 0xab, 0x49, 0xb2, 0xb4, 0x51, 0x0f, 0xfa, 0x5c, 0x03, 0x62, 0x46, 0xb3,
+ 0xe8, 0x03, 0xce, 0xfe, 0x84, 0x29, 0xd5, 0x36, 0x4c, 0x42, 0x36, 0x1f,
+ 0x75, 0x1f, 0x98, 0x91, 0xd7, 0x1a, 0x5c, 0x84, 0x60, 0xb5, 0xe3, 0x32,
+ 0xa7, 0xfb, 0xd8, 0x30, 0x10, 0x5f, 0x15, 0x35, 0x58, 0x41, 0xa2, 0xf8,
+ 0xf7, 0xea, 0xbc, 0x21, 0x49, 0x81, 0x46, 0xf2, 0xd5, 0xbb, 0x4c, 0x7f,
+ 0x89, 0x06, 0xe9, 0xd6, 0x19, 0x51, 0x1a, 0x06, 0x93, 0x37, 0x92, 0x11,
+ 0x87, 0x5a, 0x7d, 0x68, 0x75, 0x4c, 0xf7, 0x59, 0xd2, 0x1a, 0x62, 0xae,
+ 0xe7, 0xb0, 0xc0, 0x50, 0x52, 0xfd, 0x66, 0x30, 0x34, 0xb3, 0x42, 0x1a,
+ 0x42, 0xd9, 0xdc, 0x6d, 0xe1, 0x58, 0xbd, 0x30, 0x2e, 0x19, 0xed, 0xcd,
+ 0xc3, 0xe1, 0xd1, 0xd2, 0x16, 0x93, 0x0a, 0xe4, 0x8f, 0xa5, 0xc6, 0xc7,
+ 0xb8, 0x26, 0x89, 0xa1, 0x2f, 0x6d, 0x3d, 0xf3, 0xc9, 0xeb, 0x6b, 0x26,
+ 0xdb, 0xe9, 0x4e, 0x37, 0x76, 0x8f, 0x9c, 0x99, 0x53, 0x16, 0x7b, 0x9b,
+ 0x8d, 0x5c, 0xa6, 0x9e, 0x20, 0x26, 0xca, 0xcb, 0xde, 0x87, 0xe1, 0x30,
+ 0xcf, 0xa1, 0xa1, 0x1f, 0xa0, 0xe0, 0x10, 0x41, 0x91, 0xf5, 0xac, 0x96,
+ 0x88, 0xd3, 0x31, 0x2b, 0x00, 0x41, 0x11, 0xb4, 0x1c, 0x90, 0xb5, 0xdd,
+ 0x05, 0x25, 0x66, 0xc0, 0x43, 0x47, 0x0b, 0xcb, 0xb5, 0x62, 0x47, 0xb7,
+ 0xb2, 0x9b, 0x3b, 0x17, 0x87, 0xa0, 0x09, 0x6a, 0x85, 0x8a, 0xce, 0xcc,
+ 0x41, 0xb2, 0x04, 0x3a, 0x4f, 0xd0, 0x2e, 0x6c, 0xa7, 0xb4, 0x1b, 0x6f,
+ 0x37, 0x0e, 0x1f, 0x87, 0x59, 0x39, 0x96, 0x74, 0x32, 0xfa, 0xc1, 0xc2,
+ 0xb2, 0x2e, 0xba, 0xc1, 0x47, 0xf8, 0x42, 0xbf, 0x6c, 0x89, 0xe3, 0x75,
+ 0xf2, 0x05, 0x0e, 0x96, 0x22, 0x1d, 0x6e, 0x2f, 0x5f, 0xc3, 0x8d, 0x71,
+ 0x0a, 0xcd, 0x4f, 0xe7, 0xe7, 0xd6, 0x82, 0x8c, 0x9c, 0xb0, 0x4f, 0xe5,
+ 0x86, 0x91, 0x65, 0xf7, 0xe4, 0x3e, 0x00, 0xb3, 0x8f, 0x9d, 0xb6, 0xeb,
+ 0xf2, 0xcc, 0xc5, 0x9b, 0x32, 0xb4, 0xd8, 0x17, 0x7e, 0xa9, 0x4f, 0xe0,
+ 0xa1, 0xb3, 0x7e, 0x40, 0x0d, 0x53, 0xa0, 0xbe, 0x81, 0x03, 0x0d, 0x94,
+ 0x4b, 0x71, 0xfb, 0xb2, 0x82, 0x41, 0x3a, 0x6c, 0xf1, 0x9a, 0xe0, 0x49,
+ 0x53, 0x8a, 0xc4, 0x9e, 0x64, 0xef, 0x2a, 0xaf, 0xa3, 0x43, 0xc2, 0x7c,
+ 0x5b, 0x93, 0xac, 0xc6, 0xaa, 0x02, 0x68, 0x05, 0xeb, 0x90, 0x50, 0x65,
+ 0xda, 0x26, 0x8f, 0x61, 0xcf, 0x29, 0x8a, 0xbe, 0x3a, 0x6f, 0x5a, 0xe7,
+ 0x30, 0xb7, 0x4f, 0x75, 0x09, 0xae, 0xce, 0x62, 0xe0, 0x3a, 0x65, 0xb4,
+ 0x9c, 0x12, 0x3b, 0x14, 0x4e, 0x4e, 0x19, 0x57, 0xb2, 0x20, 0xcd, 0x81,
+ 0xd3, 0xf7, 0xe0, 0x11, 0xf5, 0xbe, 0x15, 0x31, 0xf1, 0x24, 0x9d, 0x06,
+ 0x2f, 0x0f, 0x18, 0x36, 0xc7, 0x2f, 0x55, 0x74, 0x5c, 0x08, 0xaa, 0x1a,
+ 0xde, 0x0e, 0x50, 0x5d, 0xd5, 0xc9, 0xf1, 0x89, 0x9f, 0xd0, 0x5d, 0x45,
+ 0xef, 0x0a, 0x01, 0xdd, 0xd2, 0x41, 0xe8, 0xa0, 0x1a, 0xc9, 0xea, 0xc3,
+ 0xe2, 0x34, 0x48, 0xe6, 0x0c, 0x24, 0xdb, 0xa2, 0xef, 0xe2, 0x61, 0x16,
+ 0xcb, 0xde, 0x0e, 0x26, 0x6f, 0x76, 0xb0, 0x28, 0x53, 0xa8, 0x78, 0xb8,
+ 0x5e, 0x39, 0x42, 0x78, 0x5a, 0x28, 0x7e, 0x11, 0x77, 0x20, 0x90, 0xa9,
+ 0xca, 0x8b, 0xd6, 0xaa, 0xc3, 0xb4, 0x0e, 0x70, 0xc5, 0x8a, 0xb5, 0x6e,
+ 0x74, 0x82, 0x21, 0xa6, 0x69, 0xe9, 0xc1, 0x9e, 0xef, 0x3e, 0x93, 0xed,
+ 0xc0, 0x6f, 0x26, 0xdc, 0x46, 0xa6, 0x50, 0xda, 0x86, 0xd2, 0xe8, 0xf1,
+ 0x8c, 0xbc, 0xad, 0x40, 0x58, 0x1e, 0x6b, 0x6f, 0xb1, 0x42, 0x6d, 0x47,
+ 0xf5, 0x24, 0xd6, 0xf7, 0x78, 0x1c, 0x63, 0xb9, 0xd0, 0xe1, 0xd5, 0x82,
+ 0x0f, 0xd3, 0x14, 0x56, 0x6a, 0xd2, 0xa7, 0xb7, 0xc3, 0x48, 0x9f, 0xfe,
+ 0xa2, 0xa8, 0x51, 0x3f, 0x20, 0x42, 0x9c, 0x8c, 0xd2, 0xe8, 0xd7, 0xce,
+ 0xfa, 0xb1, 0xe8, 0x60, 0x3f, 0xcc, 0xf5, 0xd2, 0xf8, 0x38, 0x40, 0xc5,
+ 0xa8, 0xc4, 0x58, 0x63, 0xb6, 0xf4, 0x0d, 0x46, 0x3c, 0x45, 0xb2, 0xea,
+ 0x98, 0x9c, 0x8e, 0x86, 0x2b, 0x89, 0x20, 0xab, 0x45, 0xf7, 0x71, 0x9c,
+ 0x09, 0x97, 0x4b, 0x33, 0x1e, 0x4b, 0x4f, 0x12, 0x5b, 0xe5, 0x60, 0xdb,
+ 0xc2, 0xb9, 0x86, 0x83, 0x38, 0x8a, 0x2f, 0xd6, 0xa4, 0x2f, 0xc6, 0x81,
+ 0x97, 0xc4, 0xfd, 0xa4, 0xbf, 0xe9, 0xc5, 0x05, 0x24, 0x3b, 0xb1, 0xc4,
+ 0x22, 0xde, 0x73, 0x5a, 0x61, 0x78, 0x36, 0xe1, 0xa3, 0xf4, 0x52, 0x1d,
+ 0xaa, 0xfc, 0xbd, 0x32, 0xda, 0x56, 0x69, 0x41, 0x96, 0x3f, 0xdb, 0xa9,
+ 0xad, 0xe5, 0x79, 0xcb, 0x50, 0xb4, 0x31, 0x4c, 0x35, 0xf9, 0xb8, 0xbf,
+ 0xa8, 0x88, 0xd1, 0x05, 0xe6, 0x35, 0xbc, 0xfd, 0x39, 0x1f, 0x92, 0xef,
+ 0xb3, 0xe0, 0x46, 0x66, 0x2b, 0xa4, 0xf2, 0x04, 0xeb, 0xa2, 0x56, 0xdc,
+ 0xc8, 0x42, 0x76, 0x6c, 0x95, 0x8c, 0x8d, 0xc7, 0x4e, 0x6b, 0x54, 0xc7,
+ 0x5f, 0x48, 0x3f, 0xce, 0x20, 0xdd, 0x34, 0xb0, 0x92, 0x7d, 0x60, 0x93,
+ 0x4d, 0x20, 0xf3, 0x7c, 0x31, 0xda, 0x9c, 0x46, 0x0e, 0x21, 0x50, 0xb9,
+ 0xaf, 0x37, 0x93, 0xfb, 0x58, 0xce, 0x54, 0xce, 0x88, 0x65, 0x93, 0x7b,
+ 0x7b, 0xd3, 0x18, 0xe5, 0xc0, 0x51, 0xee, 0x0c, 0x37, 0x73, 0x30, 0x61,
+ 0x57, 0xb0, 0x30, 0xda, 0x54, 0xb0, 0x97, 0x4f, 0x44, 0xee, 0x14, 0x13,
+ 0xbc, 0x3b, 0xe6, 0xb1, 0x4b, 0xc6, 0xae, 0x0e, 0x13, 0x31, 0xc3, 0x65,
+ 0x2e, 0x28, 0xa8, 0x2c, 0xc5, 0xbb, 0x19, 0x5d, 0xf5, 0x9d, 0xfa, 0x7a,
+ 0x9a, 0xf8, 0x36, 0x4a, 0x8b, 0xfe, 0xdb, 0x55, 0xaf, 0x6f, 0x37, 0x27,
+ 0xfc, 0x59, 0x41, 0x02, 0x08, 0xe8, 0xda, 0x90, 0x6c, 0x42, 0x15, 0xe7,
+ 0xce, 0xdc, 0xce, 0x7d, 0x96, 0xc1, 0xc6, 0x61, 0x6a, 0x00, 0xba, 0x69,
+ 0xc7, 0x49, 0xa5, 0x80, 0x01, 0x9f, 0x69, 0x86, 0xa4, 0xb2, 0xd5, 0x0f,
+ 0x7a, 0x43, 0xee, 0x92, 0x1d, 0x73, 0xcf, 0x84, 0xc7, 0x4d, 0x83, 0x25,
+ 0x7b, 0xc7, 0x9d, 0x7e, 0xab, 0x73, 0xb2, 0x42, 0x6c, 0x72, 0x25, 0x4f,
+ 0xc1, 0xda, 0xfe, 0x15, 0x47, 0x1a, 0xd8, 0x39, 0x84, 0xc0, 0x10, 0xf3,
+ 0xf7, 0xa8, 0x8e, 0x76, 0xe1, 0x63, 0xa2, 0xb3, 0x51, 0xf3, 0x79, 0xb6,
+ 0xc0, 0xa4, 0xbf, 0xbe, 0xb7, 0xf1, 0xf0, 0x2a, 0x24, 0xf7, 0x79, 0x51,
+ 0x88, 0x93, 0x01, 0x49, 0xf3, 0xaf, 0x4a, 0x21, 0xc1, 0xbf, 0xbb, 0xf6,
+ 0x6b, 0xe1, 0x70, 0x63, 0x29, 0x40, 0x87, 0x78, 0xb5, 0x07, 0xa4, 0x46,
+ 0x90, 0xc8, 0x45, 0x0b, 0x58, 0x08, 0xbf, 0xdf, 0x9c, 0x9d, 0xd7, 0x01,
+ 0xf8, 0xb5, 0x81, 0x03, 0xa4, 0x73, 0x7f, 0x52, 0xfa, 0x24, 0x08, 0x27,
+ 0x8f, 0x35, 0xd7, 0x76, 0xd5, 0x3f, 0xe0, 0x4c, 0x99, 0xb8, 0x52, 0x55,
+ 0x5c, 0x3f, 0xbd, 0x23, 0x8b, 0x8a, 0xca, 0xec, 0xdd, 0xb9, 0xbb, 0x18,
+ 0x68, 0x39, 0xcd, 0xd2, 0xa3, 0x96, 0x51, 0x61, 0xe2, 0x72, 0x3a, 0x6b,
+ 0xf8, 0x22, 0xe2, 0x9d, 0xe6, 0x86, 0x4a, 0x08, 0xb9, 0x64, 0x0e, 0x28,
+ 0x4b, 0xc9, 0xd4, 0x9e, 0x91, 0x84, 0x6b, 0xf2, 0x00, 0x1e, 0xea, 0x30,
+ 0xa4, 0x4c, 0x3b, 0x67, 0xff, 0xd5, 0x12, 0xb7, 0x1f, 0x62, 0xa5, 0x99,
+ 0x05, 0xbd, 0x69, 0x23, 0xb5, 0x33, 0xb0, 0x63, 0x64, 0x88, 0x9e, 0xa7,
+ 0x8b, 0x69, 0xf4, 0x92, 0x2e, 0xd7, 0x43, 0x3d, 0x19, 0x78, 0xc2, 0x0f,
+ 0x91, 0x40, 0x0b, 0x8f, 0xef, 0xa6, 0x3e, 0x08, 0x3c, 0x54, 0x8d, 0x7c,
+ 0x84, 0x47, 0x73, 0xe3, 0x7e, 0x70, 0x68, 0x11, 0x82, 0xcd, 0xfe, 0x77,
+ 0x4e, 0x91, 0x4c, 0x26, 0x56, 0xa0, 0x6f, 0x81, 0x97, 0x1f, 0xff, 0x05,
+ 0x83, 0xbd, 0x41, 0xf8, 0x9d, 0x98, 0x91, 0xf5, 0xb0, 0xd7, 0x07, 0x1b,
+ 0x13, 0x63, 0x52, 0xd7, 0xe4, 0x1a, 0xe5, 0xe0, 0x12, 0x9a, 0x55, 0xe9,
+ 0x40, 0xf1, 0xb3, 0xcc, 0xbb, 0x82, 0xbb, 0xda, 0xfc, 0xe6, 0x83, 0xb3,
+ 0x3d, 0xe6, 0x77, 0x74, 0x78, 0x7f, 0xe5, 0x14, 0x0c, 0xea, 0x87, 0x61,
+ 0x3d, 0x85, 0xe3, 0x08, 0x7f, 0x72, 0x08, 0x02, 0xc8, 0xc9, 0xef, 0xb4,
+ 0x04, 0x9a, 0x3e, 0xe3, 0xad, 0x52, 0x89, 0xea, 0x27, 0x5f, 0x47, 0xe2,
+ 0xa6, 0xba, 0xc4, 0x63, 0xe3, 0xd0, 0x2b, 0x10, 0x60, 0xe7, 0x59, 0x74,
+ 0xae, 0x7d, 0x6d, 0x55, 0x83, 0x13, 0x75, 0x11, 0x82, 0x22, 0x95, 0x09,
+ 0x8b, 0x8c, 0x6c, 0x10, 0xa4, 0x52, 0x15, 0x41, 0xb5, 0x3e, 0x81, 0x82,
+ 0x44, 0x5d, 0xa7, 0xd9, 0x1b, 0xd2, 0xcb, 0x11, 0xd0, 0x53, 0xf4, 0x74,
+ 0x36, 0xba, 0xf1, 0xe6, 0x4b, 0x23, 0x2b, 0x0b, 0xb5, 0xa3, 0xe7, 0xe3,
+ 0x38, 0xe5, 0x1d, 0x45, 0xf2, 0xf6, 0xe9, 0xfd, 0x9d, 0xb8, 0x69, 0xd5,
+ 0x96, 0xf2, 0xa7, 0x2f, 0x24, 0x6b, 0x55, 0x20, 0xe6, 0xf7, 0xe2, 0xcf,
+ 0x18, 0x91, 0xb2, 0x41, 0x70, 0xda, 0x7a, 0xfc, 0x07, 0x6f, 0x45, 0xd9,
+ 0xdd, 0x53, 0xb9, 0x29, 0x82, 0x9d, 0xdb, 0x9e, 0xea, 0xdd, 0x6b, 0x32,
+ 0x9e, 0xb1, 0x4a, 0x80, 0x5a, 0xf8, 0x6d, 0x00, 0xd8, 0xcd, 0x25, 0x2d,
+ 0x9b, 0x52, 0xdf, 0x91, 0xdf, 0x55, 0x9b, 0xce, 0x8a, 0xc3, 0xe8, 0x9c,
+ 0xf2, 0xd5, 0x66, 0x11, 0x8c, 0x27, 0x1c, 0x29, 0x5f, 0x27, 0xf7, 0x7d,
+ 0x19, 0x2d, 0xbc, 0x00, 0x16, 0x9c, 0x32, 0x5f, 0x7c, 0xfc, 0x15, 0xe4,
+ 0x16, 0xc6, 0x58, 0xeb, 0x9b, 0x40, 0x8f, 0xc7, 0xbb, 0x05, 0xda, 0xa6,
+ 0xf7, 0xbb, 0x74, 0x36, 0xcd, 0xf4, 0xa0, 0xde, 0x71, 0xc3, 0xd5, 0x89,
+ 0xd1, 0x19, 0x99, 0xb6, 0x7f, 0xd5, 0xf4, 0xb8, 0x8f, 0x11, 0xea, 0x5d,
+ 0xc2, 0x40, 0x51, 0xcb, 0x6a, 0x5c, 0xc2, 0x8a, 0x11, 0x22, 0xd9, 0x53,
+ 0xb1, 0x1e, 0x48, 0xcc, 0xb8, 0x6b, 0xeb, 0x9a, 0x69, 0xe1, 0x5a, 0x39,
+ 0x84, 0x4e, 0x8d, 0xcb, 0xef, 0x8a, 0x5b, 0xac, 0x73, 0x72, 0xe0, 0xf4,
+ 0xf8, 0x60, 0x76, 0xcd, 0x28, 0xf1, 0x78, 0x08, 0x29, 0x14, 0x07, 0x14,
+ 0xfe, 0x4c, 0x5b, 0x03, 0x27, 0xcd, 0x98, 0x3c, 0x9c, 0x90, 0x14, 0xc4,
+ 0x67, 0xa6, 0x66, 0xf0, 0xc6, 0x19, 0x91, 0x61, 0xa4, 0x33, 0x9f, 0xef,
+ 0xbe, 0x5f, 0x07, 0xaf, 0x41, 0x02, 0x13, 0x88, 0x2b, 0xf6, 0x77, 0xa1,
+ 0x42, 0x82, 0xef, 0x42, 0x8c, 0x7f, 0x05, 0x3f, 0xb3, 0x8d, 0x14, 0x58,
+ 0x62, 0xe9, 0x5e, 0xb0, 0xab, 0x80, 0xdf, 0xf0, 0x3c, 0x97, 0x81, 0xb2,
+ 0x0f, 0x98, 0xd3, 0xd0, 0x93, 0x33, 0x0f, 0x91, 0x1c, 0xcc, 0xde, 0xf8,
+ 0xf5, 0x77, 0xb6, 0xfa, 0x34, 0x4b, 0x0c, 0x9f, 0x22, 0xeb, 0x74, 0x6c,
+ 0xc6, 0xf5, 0x2e, 0xa4, 0x07, 0x31, 0xa4, 0x23, 0xde, 0xd1, 0x9b, 0x4c,
+ 0x15, 0x9f, 0xe4, 0xd1, 0x8b, 0x3b, 0x2d, 0x01, 0x17, 0x83, 0xea, 0x39,
+ 0x62, 0xd5, 0x0b, 0xae, 0x5a, 0x17, 0x73, 0xcd, 0x34, 0xa0, 0x53, 0x65,
+ 0x3c, 0x44, 0x19, 0x13, 0x53, 0x3f, 0x04, 0xf0, 0x55, 0x48, 0xe8, 0x02,
+ 0xd9, 0xd8, 0xdd, 0x94, 0x68, 0x19, 0x33, 0xeb, 0x93, 0x04, 0x61, 0xe0,
+ 0xea, 0x3b, 0x4c, 0x9a, 0xe6, 0x90, 0x78, 0x60, 0x25, 0xad, 0x98, 0x5a,
+ 0x76, 0xfb, 0x5d, 0xb0, 0x74, 0xc8, 0x91, 0xcc, 0x80, 0xd1, 0xc1, 0x9c,
+ 0xd3, 0x21, 0x88, 0x0b, 0x9a, 0x88, 0x02, 0xd8, 0x6b, 0xb3, 0xad, 0x46,
+ 0x2e, 0xac, 0x45, 0xc1, 0x1b, 0x04, 0x27, 0x1c, 0xb2, 0x53, 0xed, 0x34,
+ 0x59, 0x62, 0xf0, 0xf8, 0x7c, 0xee, 0xce, 0x91, 0x93, 0x68, 0x3f, 0xad,
+ 0xb8, 0xdd, 0xa1, 0x59, 0x8b, 0x21, 0x80, 0x12, 0x63, 0x00, 0xc9, 0xc9,
+ 0xe6, 0x2e, 0x5c, 0x7f, 0xb5, 0x25, 0x6f, 0xfb, 0x71, 0xf8, 0x8a, 0x58,
+ 0x3c, 0x25, 0xfa, 0x14, 0xd1, 0xbf, 0x33, 0x28, 0x64, 0x84, 0x42, 0x73,
+ 0x13, 0x7c, 0xc8, 0x68, 0xbb, 0x64, 0x67, 0x84, 0xbf, 0xce, 0x46, 0xfd,
+ 0xdc, 0x56, 0x8b, 0x6e, 0x11, 0xc5, 0x83, 0x98, 0x6b, 0x67, 0x19, 0xee,
+ 0xfd, 0xb2, 0xf1, 0xe9, 0xac, 0x0f, 0xbf, 0x9b, 0xc0, 0xba, 0xbe, 0x6d,
+ 0xf0, 0x57, 0xa7, 0x2b, 0xeb, 0xca, 0xef, 0x03, 0xe0, 0xb5, 0xe1, 0xfb,
+ 0x57, 0x67, 0xa3, 0xba, 0xb6, 0x84, 0xab, 0xae, 0xce, 0x45, 0xee, 0x0a,
+ 0x43, 0x96, 0xe3, 0x81, 0x09, 0x11, 0x5d, 0x27, 0xcb, 0xca, 0x3c, 0xca,
+ 0xb0, 0x24, 0x78, 0x2f, 0x6a, 0xa8, 0x8d, 0xa5, 0x81, 0xb4, 0x49, 0x80,
+ 0x2b, 0x44, 0xd7, 0x32, 0xbc, 0x1f, 0xef, 0x2f, 0xbb, 0x1c, 0xa4, 0x9f,
+ 0xcc, 0x19, 0x48, 0x9f, 0x3c, 0x48, 0x31, 0xfa, 0x84, 0x81, 0xe7, 0x02,
+ 0x83, 0xbc, 0x55, 0xaa, 0x67, 0x36, 0x65, 0x82, 0xbf, 0x30, 0xa5, 0xe4,
+ 0x4c, 0x12, 0x47, 0xee, 0x67, 0xf1, 0x87, 0x68, 0x91, 0x4a, 0x14, 0xdf,
+ 0x4b, 0x88, 0xca, 0xe2, 0xfa, 0x2f, 0xef, 0xed, 0x68, 0xc4, 0x44, 0xa2,
+ 0x02, 0xba, 0x4d, 0x05, 0x9c, 0x65, 0xc0, 0x88, 0x46, 0x84, 0x8a, 0x35,
+ 0x40, 0x59, 0x8d, 0x96, 0x61, 0x17, 0x3e, 0x4e, 0x81, 0x31, 0xf5, 0x7f,
+ 0xa3, 0x32, 0xc2, 0xea, 0xc7, 0x7b, 0x2d, 0xad, 0x4e, 0x1e, 0x45, 0x29,
+ 0x56, 0xfc, 0xc5, 0x56, 0xd9, 0x99, 0x97, 0x30, 0x4f, 0x51, 0x3d, 0xb8,
+ 0x72, 0x88, 0x82, 0x66, 0xb4, 0x1f, 0xd8, 0xd8, 0x00, 0xfd, 0x53, 0x94,
+ 0x5b, 0x6e, 0xe5, 0x1e, 0xb2, 0xb6, 0xe7, 0x5a, 0x80, 0xa2, 0xa6, 0x0f,
+ 0x0b, 0x53, 0xa9, 0x83, 0x6c, 0x17, 0x14, 0x63, 0xe9, 0x51, 0xc9, 0x47,
+ 0xca, 0x50, 0xdc, 0x26, 0x90, 0xb8, 0x66, 0x0f, 0x2e, 0x4c, 0xb4, 0x98,
+ 0x89, 0x5d, 0x63, 0x8a, 0x06, 0xf6, 0x9d, 0xd3, 0xaa, 0xad, 0x15, 0x68,
+ 0xf2, 0xdf, 0xad, 0x27, 0x43, 0xf5, 0x30, 0x08, 0xd1, 0x2a, 0xf3, 0x18,
+ 0x22, 0xb0, 0x84, 0x39, 0x16, 0x7a, 0x7b, 0x9f, 0x93, 0x92, 0x57, 0x35,
+ 0xcb, 0xdf, 0x78, 0x4a, 0xc4, 0x99, 0xd9, 0xe5, 0x72, 0xac, 0x80, 0xea,
+ 0xab, 0x8d, 0x1f, 0xde, 0x34, 0xdc, 0x1f, 0x5f, 0x3a, 0xfc, 0x07, 0x9b,
+ 0xef, 0x62, 0xeb, 0x15, 0x69, 0x9b, 0x20, 0x53, 0x02, 0x43, 0x4b, 0x65,
+ 0x51, 0x54, 0xb5, 0x2e, 0xad, 0x53, 0x0a, 0x72, 0xa2, 0x19, 0xd4, 0x0c,
+ 0x77, 0x8e, 0xd4, 0x5a, 0x9c, 0x4d, 0x8e, 0x1e, 0x8e, 0x72, 0x2b, 0xdc,
+ 0x0d, 0x36, 0x73, 0xbd, 0xe4, 0x80, 0x3e, 0xae, 0x32, 0x36, 0x7f, 0x29,
+ 0x50, 0xd9, 0x26, 0xd8, 0xb5, 0xea, 0xf4, 0x30, 0x23, 0xdb, 0x4e, 0x5e,
+ 0xfa, 0x01, 0x85, 0xe2, 0xe6, 0x35, 0x2b, 0x01, 0x6b, 0x3e, 0xe4, 0xf3,
+ 0xcc, 0x04, 0x1c, 0x47, 0x38, 0xeb, 0xa5, 0x0e, 0x59, 0x48, 0x2f, 0x67,
+ 0xe6, 0x80, 0x1d, 0x4a, 0x54, 0xab, 0x90, 0x5a, 0x16, 0x7d, 0x35, 0x58,
+ 0xf6, 0x61, 0x6a, 0xaa, 0x7a, 0xd0, 0x0f, 0xb0, 0x85, 0xf0, 0x59, 0x2c,
+ 0x5a, 0x09, 0x97, 0xa6, 0x10, 0xce, 0x65, 0x1d, 0x2e, 0xe9, 0xed, 0x66,
+ 0xc6, 0x0e, 0x1b, 0x94, 0x8e, 0x67, 0x6b, 0xea, 0xb0, 0xc1, 0xd5, 0x5d,
+ 0x0f, 0xdd, 0xe7, 0xc0, 0xdd, 0xc1, 0x4e, 0x3f, 0x80, 0xa6, 0x1f, 0x78,
+ 0x73, 0x75, 0x80, 0xd3, 0x1d, 0xd0, 0x35, 0x1b, 0xb2, 0x14, 0xd6, 0xf8,
+ 0x20, 0xcf, 0x54, 0x61, 0xe7, 0xbd, 0x2b, 0x55, 0x95, 0xbc, 0x02, 0x5b,
+ 0x40, 0xc3, 0x58, 0xe5, 0x96, 0x26, 0x30, 0x9a, 0xa4, 0x9c, 0x82, 0x4b,
+ 0x14, 0xa9, 0x35, 0x4c, 0x61, 0x6e, 0x25, 0xa9, 0xf9, 0x4a, 0xf8, 0x93,
+ 0xd7, 0x77, 0x6c, 0xb5, 0x2c, 0x53, 0xbc, 0x36, 0x7d, 0x57, 0xca, 0x05,
+ 0x1e, 0x6f, 0x70, 0x70, 0x01, 0x5c, 0x6a, 0xc4, 0x27, 0xa0, 0x45, 0xb1,
+ 0xc8, 0x7f, 0x5b, 0x61, 0x5d, 0x4a, 0x67, 0xc6, 0x0a, 0x6b, 0xb8, 0x56,
+ 0x35, 0x8e, 0xa9, 0x88, 0x99, 0x13, 0xa1, 0xe8, 0x79, 0x1b, 0x34, 0x2d,
+ 0x2d, 0xe2, 0xab, 0x68, 0x0b, 0xde, 0xb2, 0x49, 0xa8, 0xa8, 0x3c, 0x1d,
+ 0xd6, 0x6f, 0x28, 0x3d, 0xfc, 0xe2, 0x31, 0x27, 0x8d, 0x3b, 0x74, 0xe0,
+ 0x84, 0xc7, 0x72, 0x03, 0x20, 0x2a, 0x76, 0x3d, 0x9c, 0xdc, 0x69, 0x3d,
+ 0x30, 0x41, 0x07, 0xcb, 0x45, 0x72, 0x18, 0xab, 0xd1, 0xd9, 0x8e, 0x3c,
+ 0xa6, 0xe2, 0x51, 0x40, 0xf3, 0x09, 0xcc, 0xb3, 0x31, 0x91, 0x63, 0xd5,
+ 0x61, 0x4f, 0x16, 0x0e, 0x07, 0x00, 0x3a, 0x21, 0x70, 0x8d, 0x26, 0xef,
+ 0x01, 0x67, 0xb7, 0xbc, 0x8f, 0xaa, 0x1a, 0x77, 0x3d, 0x5b, 0xcc, 0x03,
+ 0xde, 0xcc, 0x1e, 0xa5, 0xe6, 0x01, 0xbb, 0x8f, 0x84, 0x48, 0x0f, 0x1f,
+ 0xb6, 0x2e, 0x46, 0xe8, 0xe6, 0x78, 0x0f, 0xd0, 0x62, 0xae, 0xe5, 0x6e,
+ 0xbc, 0x18, 0xe6, 0xc5, 0x88, 0x81, 0x9d, 0x2d, 0x4e, 0xa7, 0xf9, 0x5b,
+ 0xc1, 0xd0, 0x79, 0x56, 0x45, 0x68, 0x19, 0x81, 0x3d, 0xc6, 0xcc, 0xe2,
+ 0xac, 0x05, 0x44, 0xcf, 0x07, 0x8a, 0xec, 0x2b, 0xaf, 0xac, 0x5b, 0x91,
+ 0xba, 0x3b, 0xa9, 0x5b, 0x40, 0x7f, 0x5f, 0x78, 0xd1, 0x66, 0x0f, 0xc0,
+ 0x0b, 0x26, 0x74, 0x74, 0x54, 0x89, 0xc0, 0xb7, 0x7b, 0x33, 0x9a, 0xf5,
+ 0x4f, 0x57, 0x5d, 0xfb, 0x67, 0x86, 0x89, 0x17, 0xbd, 0x6e, 0x74, 0x40,
+ 0xb6, 0x9a, 0xe1, 0xf4, 0x5b, 0x8f, 0x23, 0x38, 0x85, 0xec, 0xda, 0x68,
+ 0x17, 0x4b, 0xb7, 0xfd, 0x33, 0xba, 0xea, 0xca, 0xdf, 0x16, 0x6b, 0x4c,
+ 0xc9, 0x26, 0xa6, 0xe7, 0x9c, 0xc2, 0x6a, 0x4f, 0xf6, 0x2c, 0xf3, 0x46,
+ 0xde, 0x40, 0xcb, 0xa4, 0xa7, 0x7b, 0x05, 0x01, 0x03, 0xb5, 0x0a, 0x14,
+ 0x25, 0x86, 0x43, 0x66, 0xdf, 0x06, 0xcc, 0xd9, 0xf0, 0xa7, 0xb9, 0x4d,
+ 0x8c, 0xf3, 0x7c, 0xc6, 0xd1, 0xa0, 0x67, 0x2f, 0x7f, 0xd3, 0x41, 0xa0,
+ 0xe0, 0x35, 0x75, 0x16, 0xa2, 0xfe, 0x57, 0xed, 0x33, 0x73, 0xfa, 0x6a,
+ 0x24, 0xc3, 0x72, 0x32, 0xa9, 0x46, 0x42, 0x61, 0x4c, 0xf5, 0xad, 0x71,
+ 0x04, 0xd0, 0x91, 0xe0, 0x93, 0x1c, 0x4e, 0x63, 0xc4, 0x2f, 0x8e, 0x9b,
+ 0xc8, 0xf5, 0x77, 0x5c, 0xdc, 0xd7, 0x75, 0xc4, 0x45, 0x72, 0x38, 0x34,
+ 0xca, 0xc2, 0xac, 0x70, 0xc7, 0x85, 0xb1, 0xbb, 0xf3, 0xe6, 0xea, 0x95,
+ 0x8e, 0x67, 0xc6, 0x6e, 0x86, 0x44, 0x7e, 0x9c, 0x66, 0x15, 0x1b, 0xae,
+ 0x80, 0x99, 0x6b, 0xf8, 0xa4, 0x39, 0x0e, 0x82, 0x15, 0x1e, 0xca, 0x2f,
+ 0xc0, 0xf2, 0x0b, 0x61, 0xf6, 0xef, 0xd8, 0x40, 0xa0, 0x2b, 0x5a, 0x5b,
+ 0xc7, 0x35, 0x6d, 0xc4, 0x1f, 0xba, 0xcd, 0x37, 0x02, 0xa8, 0xe5, 0x7b,
+ 0x43, 0xdb, 0xb7, 0x72, 0xb5, 0xe8, 0x5a, 0x4f, 0x9e, 0x02, 0xda, 0x93,
+ 0x47, 0x04, 0xe0, 0xee, 0xcd, 0x3e, 0xba, 0xca, 0x15, 0xcd, 0xfa, 0x00,
+ 0x6a, 0xa0, 0x22, 0xe1, 0x18, 0xbc, 0xcd, 0x74, 0x7f, 0xc7, 0x69, 0x1e,
+ 0x85, 0x41, 0xb7, 0x59, 0x94, 0xd8, 0xcf, 0xc3, 0x85, 0x20, 0x33, 0x89,
+ 0x8d, 0x01, 0x88, 0xd0, 0xea, 0xad, 0x55, 0x58, 0xe4, 0xc3, 0x2d, 0x7a,
+ 0x0a, 0xd9, 0x97, 0xf0, 0x13, 0x32, 0xfb, 0x37, 0xde, 0x00, 0x93, 0xc1,
+ 0x3b, 0x7c, 0x91, 0xdd, 0xa4, 0x51, 0x9d, 0xd2, 0xc2, 0x50, 0xbf, 0x80,
+ 0x9c, 0x55, 0xc1, 0x56, 0x4c, 0x41, 0x4e, 0x90, 0x5f, 0x44, 0x8f, 0x31,
+ 0x9a, 0x11, 0x51, 0x15, 0x0a, 0xa3, 0xd9, 0x3d, 0x0d, 0xa2, 0x35, 0xd0,
+ 0x70, 0xd2, 0xa5, 0x58, 0xa5, 0x1d, 0x89, 0x9b, 0x53, 0x3e, 0x12, 0x7e,
+ 0x3f, 0x9a, 0xf7, 0x07, 0xf9, 0xe3, 0xf5, 0x15, 0xc8, 0x49, 0x25, 0xe6,
+ 0x91, 0x7f, 0xd4, 0x6a, 0xd8, 0x4c, 0x24, 0xa6, 0xf4, 0x89, 0x9d, 0xa6,
+ 0x8a, 0x75, 0x0b, 0xde, 0xd0, 0xeb, 0xc3, 0x4e, 0x75, 0xb8, 0xfd, 0x36,
+ 0x6b, 0x12, 0xca, 0x97, 0x06, 0xaf, 0xc0, 0xe0, 0x9d, 0x99, 0xba, 0xe2,
+ 0x5c, 0xb6, 0x3c, 0x6f, 0x25, 0x9d, 0x72, 0x43, 0x59, 0xd9, 0xb9, 0xad,
+ 0x17, 0x07, 0x7d, 0xd1, 0xe6, 0xf2, 0x84, 0xba, 0x50, 0xea, 0x92, 0xf2,
+ 0xb2, 0x9c, 0xa0, 0x18, 0x57, 0xb9, 0xe5, 0x0b, 0x95, 0xe2, 0xc4, 0x68,
+ 0x7e, 0xe0, 0x83, 0xc3, 0x3a, 0x46, 0x20, 0x9f, 0x1e, 0xcb, 0x20, 0x33,
+ 0xb6, 0xb7, 0xcf, 0x1a, 0x5c, 0x58, 0xac, 0xeb, 0x29, 0x8b, 0x22, 0xaa,
+ 0x00, 0xa0, 0x33, 0x32, 0xa4, 0xfe, 0x17, 0x98, 0xed, 0xa1, 0xd8, 0x52,
+ 0x5d, 0x16, 0x6f, 0xc0, 0x4a, 0x62, 0x94, 0x70, 0x4b, 0xb6, 0x66, 0x57,
+ 0x8b, 0x58, 0x20, 0xf4, 0x7c, 0xe8, 0x3f, 0xda, 0x79, 0x8a, 0x33, 0x4c,
+ 0xe2, 0xbc, 0xa9, 0x31, 0x1c, 0x3a, 0xf3, 0xbb, 0xe8, 0x32, 0x2a, 0x45,
+ 0x96, 0x10, 0xe1, 0x92, 0x83, 0xa3, 0xbc, 0x0e, 0xfb, 0xa6, 0x2d, 0x9b,
+ 0x2d, 0xa0, 0x98, 0x71, 0xd8, 0x2c, 0x68, 0x23, 0x0d, 0x34, 0x94, 0xdd,
+ 0x4b, 0xd8, 0xd5, 0x04, 0xb3, 0x6d, 0xdb, 0x76, 0x32, 0xa0, 0x21, 0x44,
+ 0x99, 0xb4, 0x98, 0x2c, 0x22, 0xed, 0x7e, 0xce, 0x74, 0x72, 0xee, 0xaf,
+ 0x71, 0x59, 0x39, 0x3b, 0x4c, 0xba, 0x8b, 0x1e, 0x19, 0x27, 0xca, 0x05,
+ 0x5d, 0xf6, 0x35, 0xe3, 0xb4, 0x12, 0xd6, 0x1f, 0xef, 0x6b, 0xfd, 0x0c,
+ 0x4a, 0x78, 0xd7, 0xa3, 0x28, 0x7a, 0xb8, 0x67, 0xf6, 0x9b, 0x75, 0xb5,
+ 0x7d, 0xcd, 0x4d, 0x9a, 0x46, 0xc2, 0x8d, 0x24, 0xdf, 0x70, 0x31, 0x7c,
+ 0xc4, 0x9c, 0x54, 0x56, 0x36, 0xf7, 0xdf, 0x3d, 0xe3, 0xe0, 0xdd, 0xd7,
+ 0xc5, 0x7d, 0x34, 0x77, 0x10, 0x23, 0x3e, 0x44, 0xb1, 0xf8, 0x7c, 0x30,
+ 0x5f, 0x6e, 0x0e, 0xb0, 0xb6, 0x49, 0x10, 0x21, 0x1c, 0x79, 0x22, 0x5c,
+ 0x87, 0x1d, 0x61, 0x43, 0x27, 0x82, 0x58, 0x14, 0x32, 0x30, 0x5c, 0x8b,
+ 0x8b, 0x3c, 0xf1, 0x05, 0xf1, 0xc6, 0xf4, 0x42, 0x5f, 0x5a, 0x57, 0x30,
+ 0x88, 0x0b, 0x40, 0xfa, 0x24, 0x08, 0xca, 0x25, 0x97, 0x5a, 0xf3, 0x11,
+ 0x0c, 0xc2, 0x4f, 0x8e, 0x1d, 0x31, 0x04, 0x6d, 0x06, 0xb0, 0xfc, 0xea,
+ 0x81, 0xa8, 0x47, 0x8b, 0xbf, 0x30, 0x7f, 0xc1, 0x87, 0x12, 0xaa, 0x35,
+ 0xe1, 0xce, 0xc3, 0xa8, 0xfc, 0x12, 0xca, 0x76, 0xbf, 0xe9, 0x22, 0x00,
+ 0x89, 0x5f, 0xa4, 0xd5, 0xce, 0xcb, 0xb9, 0x2c, 0xe4, 0xb5, 0x27, 0x60,
+ 0xe5, 0xd6, 0x7b, 0xcc, 0xde, 0xbc, 0xc3, 0x9e, 0x77, 0x7e, 0x73, 0xb6,
+ 0xb8, 0x78, 0x5d, 0x32, 0x75, 0xe6, 0x0c, 0x0a, 0x66, 0x52, 0x31, 0xfb,
+ 0xb4, 0xcc, 0x46, 0x43, 0xef, 0x00, 0xee, 0xf4, 0x66, 0x7b, 0x35, 0x7a,
+ 0x7c, 0x63, 0xbd, 0x1d, 0xd0, 0xab, 0xe0, 0x01, 0x79, 0x1e, 0xbf, 0xc1,
+ 0x4b, 0x3a, 0xe5, 0xc3, 0xb9, 0x7a, 0x1a, 0x0d, 0xc3, 0x21, 0xb0, 0xa0,
+ 0xf9, 0xbf, 0x06, 0x1b, 0x5d, 0x3d, 0xa8, 0x64, 0x20, 0x5c, 0x65, 0xbf,
+ 0x81, 0x49, 0xb8, 0x8a, 0xa1, 0xc7, 0x60, 0x96, 0x72, 0xb1, 0x34, 0xf8,
+ 0xf4, 0xd4, 0x81, 0xb2, 0x3f, 0xa5, 0xc9, 0x76, 0x90, 0x73, 0x39, 0x42,
+ 0xdb, 0x80, 0xed, 0x8c, 0xf4, 0xea, 0x7c, 0xc5, 0xe7, 0xd0, 0xad, 0x4e,
+ 0x12, 0x60, 0x94, 0x80, 0xaf, 0x9e, 0x5c, 0x86, 0x9c, 0x04, 0x95, 0xbe,
+ 0x7b, 0xee, 0x1c, 0x92, 0x02, 0xc5, 0x79, 0xe3, 0x99, 0x23, 0x93, 0xab,
+ 0xec, 0x1d, 0x70, 0x50, 0xdb, 0x53, 0x41, 0x80, 0x0d, 0x91, 0x98, 0x4e,
+ 0xa2, 0x67, 0xf4, 0x25, 0xdc, 0xdf, 0xc8, 0xab, 0xf1, 0x48, 0xc5, 0x16,
+ 0xb3, 0x21, 0xe3, 0x8e, 0x25, 0x0b, 0x26, 0x81, 0x2e, 0xa8, 0x17, 0x78,
+ 0x38, 0x7f, 0x94, 0xd5, 0x9e, 0x05, 0x43, 0x38, 0xac, 0xd7, 0x62, 0x2e,
+ 0x0c, 0x6e, 0x91, 0x78, 0xb9, 0x20, 0xd0, 0x06, 0x84, 0x67, 0x33, 0xb3,
+ 0x3a, 0x5c, 0xef, 0xe7, 0x08, 0x9d, 0xd4, 0x89, 0x8b, 0xe2, 0x58, 0x53,
+ 0xa7, 0x6a, 0x17, 0x19, 0xbd, 0x75, 0x76, 0x69, 0x79, 0x40, 0x7a, 0x14,
+ 0x0f, 0x0b, 0xc2, 0x62, 0x0a, 0x82, 0xc4, 0x02, 0x01, 0x27, 0x9d, 0xc3,
+ 0x6e, 0x4d, 0x60, 0xd2, 0x5c, 0xd6, 0xee, 0x97, 0x6d, 0x80, 0xbd, 0x27,
+ 0xde, 0x31, 0xd8, 0x31, 0x81, 0x63, 0x87, 0x02, 0x06, 0x7a, 0x1a, 0x8c,
+ 0xd8, 0x6c, 0x91, 0x36, 0x3f, 0xd3, 0xc3, 0x6f, 0x37, 0xc7, 0x7a, 0xf7,
+ 0x46, 0xc6, 0x87, 0x8d, 0x9a, 0x6f, 0xfe, 0x5b, 0xdc, 0x01, 0x3e, 0xe9,
+ 0xd6, 0xd4, 0x75, 0x27, 0xd3, 0x79, 0x5e, 0xea, 0x7c, 0x92, 0xb0, 0xde,
+ 0x99, 0x9c, 0xd4, 0x08, 0x04, 0xcf, 0xe0, 0x94, 0x69, 0xb1, 0x69, 0xe6,
+ 0x3b, 0x22, 0x01, 0x29, 0xf3, 0x5d, 0xd0, 0x6b, 0x61, 0x78, 0x67, 0x93,
+ 0x4c, 0xd1, 0x65, 0xb0, 0x45, 0xd8, 0x88, 0x35, 0x5a, 0x4b, 0x8a, 0xca,
+ 0xf6, 0xb7, 0x2d, 0x4b, 0x8e, 0x7e, 0xba, 0x49, 0x2a, 0x5c, 0xf3, 0xe1,
+ 0x01, 0xa3, 0xd2, 0xd8, 0xa8, 0x69, 0xbd, 0x0e, 0x50, 0x16, 0x18, 0x54,
+ 0xb8, 0xa2, 0xaa, 0x36, 0xd1, 0x8a, 0x16, 0xdc, 0x8b, 0x9a, 0x40, 0x73,
+ 0xaf, 0xc3, 0x92, 0xb6, 0x02, 0xdc, 0x73, 0x29, 0xec, 0xb3, 0x7a, 0x9d,
+ 0x00, 0xdd, 0x59, 0x59, 0x39, 0x80, 0x5d, 0x07, 0xbf, 0x87, 0xab, 0x2e,
+ 0x87, 0x5b, 0x3c, 0xe4, 0x8a, 0x47, 0xef, 0x0a, 0x31, 0x8a, 0x2b, 0x90,
+ 0xff, 0x44, 0xb1, 0xb2, 0x90, 0xe3, 0xc7, 0x08, 0x9a, 0xb9, 0x5a, 0xef,
+ 0xf3, 0xcb, 0x32, 0x20, 0x0f, 0x2e, 0xa1, 0x4a, 0x1d, 0x59, 0x61, 0x24,
+ 0xc2, 0xc7, 0x9b, 0x68, 0x1d, 0x02, 0xb8, 0xe0, 0xbb, 0x8f, 0xbd, 0xf5,
+ 0x19, 0x54, 0xc7, 0x6e, 0x44, 0x16, 0x94, 0x7f, 0x23, 0x33, 0xbb, 0x05,
+ 0x30, 0x8f, 0xd4, 0xe3, 0x28, 0xb3, 0xbb, 0x17, 0x5e, 0x3a, 0xff, 0x6d,
+ 0x75, 0x21, 0x23, 0x60, 0x51, 0xfe, 0xbb, 0x53, 0xff, 0x9f, 0x74, 0xd7,
+ 0x45, 0x29, 0xbd, 0x5c, 0xef, 0x94, 0x36, 0x0e, 0x63, 0x20, 0xd8, 0xaa,
+ 0x1c, 0x3c, 0xf9, 0x5d, 0xb9, 0x4d, 0x2d, 0x7a, 0x0e, 0xe5, 0x6d, 0x9b,
+ 0xd6, 0xd3, 0x21, 0x85, 0x14, 0xc9, 0x80, 0x4f, 0x3d, 0xc5, 0xd2, 0x79,
+ 0xcf, 0xf0, 0x4f, 0xa3, 0x49, 0x58, 0x5d, 0xcc, 0xf6, 0xf9, 0xda, 0x5f,
+ 0x20, 0x32, 0x60, 0x6a, 0x73, 0xb4, 0x45, 0xfe, 0x60, 0x00, 0x07, 0xd3,
+ 0xc0, 0x8a, 0x6a, 0xc3, 0x96, 0xb6, 0x12, 0x6b, 0xbd, 0x3e, 0xd3, 0x67,
+ 0x7b, 0x70, 0xf5, 0xc7, 0xaf, 0xaa, 0x81, 0x6c, 0x67, 0x1e, 0xf4, 0x32,
+ 0xac, 0xd7, 0x9f, 0x93, 0x79, 0x0c, 0xa7, 0x2f, 0x9a, 0x9d, 0x45, 0xb9,
+ 0xba, 0x6e, 0x4c, 0xee, 0xa0, 0x93, 0x41, 0xc4, 0xb7, 0x32, 0x3b, 0x72,
+ 0xe0, 0x9a, 0xe6, 0x61, 0x38, 0x18, 0x4a, 0x26, 0xcd, 0x08, 0xa1, 0xda,
+ 0x55, 0xb5, 0x35, 0x12, 0x86, 0x7a, 0x9d, 0x28, 0xd0, 0x37, 0x5e, 0x0e,
+ 0x96, 0x12, 0xa2, 0x39, 0x78, 0x75, 0x02, 0x08, 0x3d, 0x48, 0x4f, 0x6c,
+ 0x6b, 0x2e, 0x01, 0x50, 0x38, 0x25, 0x57, 0xa6, 0x8a, 0xc6, 0xee, 0x3d,
+ 0xf6, 0xfe, 0x02, 0x0a, 0x2a, 0x19, 0x36, 0x1e, 0xeb, 0xc1, 0xa2, 0xd0,
+ 0x18, 0xc0, 0x60, 0x49, 0xa4, 0x7d, 0xde, 0x0a, 0x52, 0x5b, 0x4c, 0x51,
+ 0x52, 0x55, 0xe9, 0xce, 0xc0, 0x77, 0x4b, 0xd3, 0xda, 0x8a, 0x58, 0x1d,
+ 0xc0, 0x33, 0x5d, 0x1a, 0x86, 0xd1, 0xdf, 0xe7, 0x28, 0x61, 0x5f, 0x53,
+ 0x2d, 0x20, 0xe2, 0xa8, 0x97, 0xda, 0x1c, 0xe3, 0x71, 0x95, 0xff, 0xbc,
+ 0xa9, 0x8d, 0xe6, 0xd2, 0xe5, 0x41, 0xb4, 0x9e, 0x19, 0xba, 0x00, 0x1f,
+ 0x78, 0x52, 0x3b, 0x3a, 0x50, 0x85, 0xbb, 0xa5, 0x2a, 0x3f, 0x46, 0x59,
+ 0x08, 0xae, 0x2f, 0x18, 0x58, 0x02, 0x46, 0x60, 0xaf, 0x5d, 0xe7, 0x6f,
+ 0xaa, 0x11, 0xee, 0x37, 0x22, 0xb6, 0xef, 0x62, 0xba, 0x0a, 0xc2, 0x24,
+ 0xaa, 0xf9, 0x54, 0xc2, 0x4f, 0xb0, 0xb4, 0x32, 0xa7, 0x59, 0x16, 0xa3,
+ 0x2b, 0xfb, 0x69, 0x7d, 0x51, 0xba, 0xd3, 0x00, 0xa5, 0xe6, 0x5c, 0xb6,
+ 0xd4, 0xcc, 0x5a, 0x50, 0xd4, 0x46, 0x77, 0x7d, 0x0b, 0x1a, 0xf1, 0xce,
+ 0x88, 0x51, 0xbd, 0x87, 0xc9, 0xfa, 0x33, 0x99, 0xeb, 0xc0, 0x5d, 0xee,
+ 0x04, 0x37, 0x21, 0x51, 0x15, 0x08, 0xf3, 0x70, 0x34, 0xb2, 0xfe, 0x74,
+ 0x1c, 0x91, 0x38, 0x2e, 0x54, 0x7b, 0xf8, 0xd3, 0xd7, 0x1c, 0xa4, 0x42,
+ 0x62, 0x76, 0x1d, 0x25, 0x10, 0xad, 0xc6, 0x12, 0xef, 0x07, 0xcb, 0x3b,
+ 0x9d, 0x5d, 0x66, 0x27, 0xe2, 0x82, 0x57, 0xb5, 0x36, 0xd7, 0x31, 0xdb,
+ 0x71, 0x2e, 0xb8, 0x55, 0xea, 0xc2, 0x25, 0x54, 0xf8, 0x1f, 0x6b, 0xcf,
+ 0xdd, 0x4a, 0x1f, 0xc6, 0x69, 0x3c, 0xf9, 0xc1, 0x96, 0x0d, 0xaf, 0xda,
+ 0xc2, 0x7e, 0x88, 0x10, 0x8e, 0xb6, 0x89, 0x2f, 0x1f, 0x09, 0xae, 0x7e,
+ 0x2d, 0x65, 0xec, 0xea, 0xa4, 0x6f, 0xd3, 0x54, 0x33, 0xea, 0x1b, 0x63,
+ 0x1c, 0x27, 0x91, 0x00, 0x77, 0x34, 0x5b, 0x73, 0x1b, 0xf1, 0xd6, 0xbd,
+ 0xcc, 0x86, 0xa2, 0x38, 0x8a, 0xd5, 0x55, 0xe2, 0x9f, 0x90, 0xdd, 0xb9,
+ 0xfa, 0xd9, 0xe2, 0x82, 0x24, 0xe9, 0x17, 0xfc, 0x8b, 0xb3, 0x8b, 0xeb,
+ 0x41, 0xed, 0xe5, 0xd9, 0xbd, 0x78, 0x20, 0xde, 0x20, 0xed, 0x98, 0x17,
+ 0x8a, 0x7f, 0x5d, 0x40, 0xbd, 0x0a, 0x03, 0x5d, 0xe2, 0x22, 0x65, 0x94,
+ 0xd3, 0x11, 0xb2, 0xc9, 0x83, 0x9b, 0xc6, 0xb7, 0x3f, 0x27, 0x03, 0x47,
+ 0x16, 0xd2, 0xfd, 0x6c, 0x5a, 0x19, 0x0c, 0x77, 0xb7, 0xd2, 0x41, 0x89,
+ 0xd9, 0xeb, 0x1b, 0x56, 0xfa, 0xb4, 0xbe, 0x0b, 0xd1, 0x43, 0x5d, 0xfb,
+ 0x13, 0x41, 0x54, 0x66, 0xc2, 0x23, 0x96, 0x9a, 0x79, 0x38, 0xaf, 0x6f,
+ 0x46, 0x44, 0x18, 0x5c, 0x11, 0xd6, 0xb1, 0xf7, 0xfa, 0x84, 0x2a, 0x2e,
+ 0x9d, 0x23, 0x4f, 0xfe, 0x72, 0xc8, 0x87, 0x9e, 0x17, 0x0a, 0xe4, 0xf8,
+ 0x38, 0x27, 0x36, 0x16, 0x72, 0xf4, 0xc0, 0xae, 0xd8, 0x4a, 0x83, 0xd8,
+ 0xf3, 0x6e, 0x7f, 0x5a, 0x6a, 0x65, 0xd5, 0x12, 0x91, 0x42, 0x50, 0x28,
+ 0xc2, 0xfe, 0x02, 0xa4, 0x57, 0xf1, 0xb3, 0x0a, 0x86, 0xaa, 0x67, 0xda,
+ 0x0d, 0xe2, 0x54, 0xda, 0x44, 0x38, 0x44, 0xf4, 0x9f, 0xfc, 0x56, 0xe6,
+ 0x05, 0x4a, 0x5f, 0x33, 0xcb, 0x64, 0x17, 0xb2, 0xd1, 0x1d, 0x08, 0x07,
+ 0x51, 0x57, 0xd3, 0x3a, 0xc6, 0x54, 0x52, 0x6c, 0x03, 0xe9, 0x2e, 0x3f,
+ 0x92, 0x05, 0xe4, 0x53, 0x80, 0xeb, 0xbe, 0xdf, 0x29, 0xb4, 0xed, 0xea,
+ 0xb4, 0x5e, 0x97, 0xdf, 0x2a, 0xa2, 0x20, 0x69, 0x27, 0x94, 0xa6, 0x77,
+ 0xb1, 0xae, 0x0a, 0x5a, 0x05, 0x1e, 0x34, 0x49, 0x27, 0x0c, 0x5d, 0xa2,
+ 0x77, 0xd0, 0xeb, 0x4a, 0x96, 0x53, 0xf4, 0xbc, 0x0f, 0x8e, 0x58, 0x3a,
+ 0x8b, 0xbc, 0xf2, 0xe3, 0x09, 0xe8, 0xd8, 0xf2, 0xf1, 0x17, 0xe3, 0x9a,
+ 0x18, 0xa5, 0x6d, 0x3e, 0x52, 0x0b, 0xc0, 0xc5, 0x95, 0xf6, 0xb8, 0x15,
+ 0xfc, 0x8b, 0x67, 0xd9, 0x28, 0x88, 0x27, 0xb5, 0x0a, 0x62, 0xdf, 0x4d,
+ 0x31, 0x62, 0xca, 0x12, 0x9e, 0xa3, 0x09, 0x25, 0x90, 0x94, 0x18, 0x26,
+ 0xa9, 0x41, 0x21, 0x21, 0x37, 0xb6, 0x4d, 0xcd, 0x99, 0x7b, 0x22, 0xe2,
+ 0x3f, 0x6b, 0x71, 0x5b, 0x84, 0xeb, 0x19, 0xca, 0x15, 0xa7, 0x6d, 0x65,
+ 0x57, 0x3e, 0x15, 0xcb, 0x1a, 0xb9, 0x66, 0xf2, 0x18, 0xa1, 0xfc, 0xd7,
+ 0xd8, 0x1a, 0xd0, 0x08, 0x5f, 0xd6, 0xe9, 0x18, 0x0e, 0x0d, 0x08, 0x13,
+ 0x1d, 0xd8, 0xb2, 0x45, 0x74, 0xb8, 0x46, 0xea, 0x2f, 0xe4, 0x24, 0xca,
+ 0x74, 0x21, 0xee, 0xf8, 0x5c, 0x1d, 0xed, 0x8b, 0x23, 0x76, 0x6a, 0x10,
+ 0xd5, 0x3a, 0x19, 0x9b, 0xfe, 0x6b, 0xc0, 0xcf, 0xf1, 0x29, 0x22, 0xec,
+ 0x22, 0x55, 0xc4, 0xc2, 0xae, 0xb2, 0x52, 0x92, 0x8b, 0x18, 0xbc, 0xbc,
+ 0x25, 0xb0, 0x4c, 0xf2, 0x08, 0x05, 0x71, 0xa1, 0xe9, 0xdd, 0x4c, 0xc8,
+ 0x91, 0xe7, 0xe7, 0x61, 0xc0, 0xf6, 0x12, 0x59, 0x4f, 0xb6, 0x98, 0x81,
+ 0xe0, 0x15, 0x29, 0xbe, 0x08, 0x4a, 0x6a, 0x73, 0x53, 0xff, 0x5f, 0x22,
+ 0x3d, 0xa0, 0xfd, 0xb3, 0xf1, 0xa5, 0x30, 0x57, 0xdb, 0x14, 0x12, 0xf3,
+ 0x1a, 0xa1, 0x53, 0x16, 0xe1, 0x90, 0xe7, 0x53, 0x18, 0xc8, 0x53, 0x00,
+ 0x80, 0x3e, 0xc4, 0xc8, 0xa4, 0x70, 0x2f, 0x6f, 0xc6, 0x7a, 0x5b, 0x5a,
+ 0xe7, 0x9c, 0xa7, 0x0e, 0x80, 0x99, 0x71, 0x37, 0x60, 0x23, 0x11, 0x31,
+ 0xc1, 0xbb, 0xb1, 0x92, 0x96, 0x07, 0xf5, 0xad, 0xaa, 0x66, 0xbd, 0x0b,
+ 0x28, 0x85, 0x30, 0x02, 0x85, 0x83, 0x73, 0x68, 0x9e, 0xdf, 0xfc, 0x19,
+ 0xc3, 0xb2, 0x50, 0x17, 0xa9, 0x72, 0xf3, 0x91, 0x77, 0xdd, 0x4f, 0xae,
+ 0xc1, 0xb6, 0xc0, 0x7e, 0x31, 0x87, 0xc0, 0x2f, 0xe6, 0x8f, 0x6a, 0xc8,
+ 0x3d, 0xbb, 0xe9, 0x49, 0x8f, 0xc5, 0x0d, 0x68, 0xc9, 0x5d, 0x90, 0x39,
+ 0x65, 0x5e, 0x07, 0x38, 0xd1, 0x66, 0x6a, 0xc5, 0x8b, 0xb8, 0xd3, 0xb7,
+ 0x24, 0x1a, 0xd2, 0x03, 0x1b, 0xed, 0xfb, 0x41, 0x07, 0x43, 0x74, 0xe3,
+ 0x1a, 0x54, 0x1a, 0xc0, 0xc5, 0x87, 0x70, 0x52, 0x31, 0x22, 0x50, 0x8c,
+ 0x31, 0x4b, 0x5c, 0xd9, 0x4d, 0xba, 0xaf, 0xc6, 0x1f, 0x4f, 0x4d, 0xa1,
+ 0x5a, 0xa2, 0x52, 0x7a, 0xe6, 0xe3, 0x0f, 0xcc, 0xf6, 0x5c, 0xd4, 0xdb,
+ 0x96, 0xf2, 0xc7, 0x2e, 0x69, 0x2d, 0xb5, 0x46, 0x1a, 0x2d, 0xbc, 0xf7,
+ 0xe1, 0x78, 0x86, 0x2c, 0x41, 0x8b, 0x70, 0x17, 0xaa, 0xe4, 0x7c, 0x8b,
+ 0xdb, 0xfa, 0xdc, 0x09, 0x53, 0xaf, 0xa9, 0xa3, 0xb8, 0x66, 0x16, 0x16,
+ 0xb7, 0x8f, 0xf6, 0x1f, 0x0a, 0x36, 0x4b, 0x06, 0x40, 0xef, 0xef, 0x1e,
+ 0x8d, 0x46, 0x7d, 0x35, 0x43, 0xfe, 0xa6, 0x8b, 0xe9, 0x19, 0x31, 0x2c,
+ 0x2c, 0xb8, 0x96, 0x30, 0xa4, 0x8c, 0x36, 0x2a, 0x64, 0x27, 0x72, 0xad,
+ 0x62, 0xfb, 0x70, 0x90, 0x58, 0xe7, 0x51, 0x59, 0x01, 0xc2, 0x98, 0x43,
+ 0xee, 0x7f, 0xc1, 0x36, 0x85, 0xc7, 0x32, 0xd5, 0x25, 0x7d, 0x56, 0xef,
+ 0xa4, 0x1d, 0x29, 0x8c, 0x44, 0xba, 0x7b, 0x82, 0x85, 0xee, 0x4a, 0x2c,
+ 0x45, 0xd0, 0xdf, 0xcc, 0x8c, 0x77, 0x02, 0xf7, 0x41, 0x0c, 0x44, 0x16,
+ 0x19, 0xc6, 0xd4, 0x25, 0xa6, 0xae, 0xfb, 0x07, 0x74, 0x90, 0x40, 0x5a,
+ 0x68, 0x6e, 0x4a, 0xaa, 0xe2, 0xb8, 0xa9, 0x90, 0xb0, 0x20, 0x5f, 0x48,
+ 0xe7, 0xb4, 0xe8, 0xa8, 0xa3, 0x70, 0xb5, 0x5a, 0xb8, 0xc4, 0xf9, 0x8a,
+ 0x61, 0x62, 0x99, 0xc3, 0x95, 0xaa, 0xd7, 0x21, 0x5e, 0x1b, 0x86, 0xe0,
+ 0xba, 0xa9, 0x5b, 0xf8, 0x0b, 0x3b, 0xa1, 0x69, 0x35, 0xe3, 0x6b, 0xe9,
+ 0x7e, 0x14, 0x00, 0x91, 0xe5, 0x67, 0x92, 0xcb, 0xb9, 0xc6, 0xae, 0x3a,
+ 0x6b, 0xaa, 0x4c, 0x80, 0x5e, 0x30, 0xaf, 0x0c, 0xf5, 0x26, 0xfc, 0x5e,
+ 0xbb, 0xbe, 0x34, 0x21, 0x42, 0x44, 0x7f, 0x31, 0x9c, 0x6e, 0x37, 0x0f,
+ 0xa0, 0xaa, 0x69, 0x66, 0xbc, 0x25, 0x48, 0xe5, 0x41, 0x51, 0xf8, 0x04,
+ 0x4f, 0xd3, 0x19, 0x97, 0x75, 0xb4, 0xd5, 0xe9, 0x08, 0x72, 0x56, 0x9c,
+ 0xe0, 0x48, 0x1a, 0x71, 0x2a, 0x3b, 0x38, 0x9d, 0x15, 0xd9, 0x00, 0xc8,
+ 0x43, 0x03, 0x5a, 0x7d, 0x20, 0xde, 0xc3, 0xfd, 0x9b, 0xb9, 0x35, 0xc8,
+ 0x88, 0x34, 0xf3, 0x16, 0xf1, 0xb2, 0xbf, 0xd1, 0xc8, 0x71, 0x46, 0xaf,
+ 0x30, 0x5e, 0x82, 0xb8, 0x37, 0x71, 0x80, 0x06, 0x17, 0x89, 0x06, 0x39,
+ 0xe2, 0x89, 0x40, 0x87, 0x60, 0x74, 0x08, 0x42, 0xd5, 0x47, 0x58, 0x9d,
+ 0xc4, 0x56, 0xa3, 0xa4, 0x21, 0xfa, 0x80, 0x41, 0x3c, 0x0b, 0xb4, 0x6e,
+ 0xc9, 0x49, 0x28, 0x93, 0x21, 0x2a, 0xef, 0xdd, 0xcb, 0x2c, 0x2f, 0xfa,
+ 0x1d, 0x45, 0x2a, 0xb7, 0xef, 0x4c, 0xd2, 0xdc, 0xf0, 0x0e, 0x21, 0xf3,
+ 0x51, 0xed, 0x61, 0x03, 0xc3, 0x0f, 0x4c, 0x75, 0xdd, 0xde, 0x8b, 0x0a,
+ 0x41, 0x2c, 0xd9, 0x01, 0xab, 0x95, 0xd1, 0xba, 0x09, 0x32, 0x3c, 0xb4,
+ 0x0e, 0xf0, 0x44, 0xcd, 0xfe, 0x96, 0xe3, 0xa8, 0x53, 0x51, 0xb8, 0xe8,
+ 0x6c, 0xa8, 0x0d, 0xa4, 0x8e, 0x35, 0x23, 0x02, 0x78, 0xe9, 0x08, 0x35,
+ 0x9f, 0x8e, 0x01, 0x66, 0x64, 0x85, 0xa9, 0x66, 0xa7, 0x48, 0x67, 0x82,
+ 0x69, 0xe5, 0x4a, 0x06, 0x6f, 0x2c, 0x0b, 0xac, 0xc2, 0x49, 0x82, 0x76,
+ 0xcd, 0xae, 0x9d, 0x50, 0x64, 0x7a, 0x0a, 0x2a, 0xc1, 0x13, 0xed, 0xe4,
+ 0x0c, 0xb1, 0xd3, 0x6a, 0x9d, 0xed, 0x87, 0x2f, 0x19, 0x2c, 0x3e, 0x64,
+ 0x59, 0x65, 0xe6, 0x1a, 0x43, 0xed, 0xef, 0x28, 0x92, 0xd7, 0xf6, 0x72,
+ 0xe5, 0xb6, 0x4b, 0xcd, 0x03, 0x90, 0xac, 0xa5, 0xad, 0x4b, 0xae, 0x85,
+ 0x42, 0x82, 0xff, 0x64, 0xfd, 0xb3, 0x42, 0x24, 0x28, 0x9c, 0xec, 0xe9,
+ 0xe5, 0xb6, 0xcc, 0xec, 0xc8, 0x4c, 0xd2, 0x21, 0x82, 0xcd, 0xe1, 0x5c,
+ 0x74, 0x2e, 0xe9, 0x78, 0xf6, 0x3b, 0x97, 0x09, 0x89, 0x6a, 0xd0, 0xb3,
+ 0x62, 0xd6, 0x21, 0x41, 0x76, 0x83, 0xaa, 0x34, 0x19, 0x85, 0x29, 0x5b,
+ 0x1f, 0x6c, 0xf8, 0xc7, 0x8e, 0xe7, 0x78, 0x59, 0x20, 0x83, 0x8a, 0x20,
+ 0xe8, 0x83, 0xdd, 0x1e, 0xd8, 0x11, 0x1f, 0x1c, 0x71, 0x1d, 0x1f, 0x0c,
+ 0xb2, 0x77, 0xc9, 0xf2, 0x3d, 0x7b, 0xcb, 0xad, 0xa7, 0x92, 0x6a, 0x72,
+ 0x07, 0x2c, 0x06, 0x66, 0xea, 0x3b, 0xd6, 0x9f, 0xcd, 0xf0, 0xa1, 0x95,
+ 0xc4, 0xc3, 0x7b, 0xbb, 0xad, 0xe0, 0x11, 0x1b, 0x8f, 0x80, 0xec, 0xf1,
+ 0x86, 0x1d, 0xd2, 0x1c, 0x1c, 0x00, 0xda, 0xba, 0xe5, 0x29, 0x68, 0x9a,
+ 0xff, 0x7f, 0x02, 0x0c, 0x01, 0x4f, 0x22, 0x85, 0x96, 0x85, 0x4c, 0xad,
+ 0x29, 0x92, 0xa6, 0x7d, 0xa5, 0x02, 0x8f, 0x19, 0xd1, 0x01, 0x49, 0x11,
+ 0x72, 0x6a, 0x8d, 0x86, 0x96, 0xd4, 0xce, 0x72, 0x6f, 0x61, 0x34, 0x08,
+ 0xbd, 0x2c, 0x07, 0xdc, 0x10, 0x74, 0x5e, 0xa8, 0xdf, 0xb7, 0x53, 0xfd,
+ 0x6c, 0x2a, 0x99, 0x75, 0x39, 0x7d, 0x7d, 0xda, 0x3b, 0x8f, 0x29, 0x55,
+ 0xae, 0x70, 0x17, 0x1d, 0x9b, 0x2c, 0x50, 0xcc, 0xa8, 0x5f, 0xf1, 0xf6,
+ 0xaa, 0x29, 0x28, 0x46, 0x75, 0xac, 0x4c, 0xbe, 0x4e, 0x36, 0x94, 0x67,
+ 0x91, 0x55, 0x70, 0x3a, 0x8b, 0x33, 0x51, 0x49, 0x29, 0x11, 0xd9, 0xdc,
+ 0x1b, 0xc1, 0x85, 0x48, 0x3e, 0xd3, 0x40, 0x00, 0xe7, 0x63, 0xaa, 0x49,
+ 0xdd, 0x96, 0x9a, 0xad, 0x3f, 0xf6, 0xb3, 0xfe, 0xc5, 0x00, 0x6c, 0x5e,
+ 0x3c, 0xcc, 0x04, 0x23, 0x01, 0xa7, 0xaa, 0xaa, 0x19, 0x14, 0x89, 0x42,
+ 0xf0, 0x05, 0x56, 0x61, 0x99, 0xc7, 0x8f, 0x8d, 0x43, 0xe7, 0xc0, 0xf1,
+ 0x84, 0x9f, 0x57, 0x2c, 0xda, 0x87, 0xa0, 0x3d, 0x2e, 0xfd, 0xd6, 0xf9,
+ 0xc3, 0x23, 0x1d, 0xe2, 0x9b, 0xeb, 0x21, 0xef, 0xf1, 0x27, 0xdc, 0x9e,
+ 0xc0, 0x50, 0xe7, 0xea, 0x6c, 0xee, 0x1a, 0x8d, 0x82, 0x18, 0x01, 0x34,
+ 0x72, 0x53, 0x62, 0x1f, 0x4a, 0xcf, 0x07, 0x0a, 0x04, 0xa5, 0x49, 0x73,
+ 0x37, 0x13, 0xe1, 0xb7, 0x22, 0xac, 0x16, 0xbc, 0x3d, 0xe1, 0x34, 0x75,
+ 0xca, 0x54, 0x1b, 0x1b, 0xb2, 0xe6, 0xe4, 0x41, 0x83, 0xb8, 0xc9, 0xec,
+ 0xd1, 0xaf, 0x9b, 0x25, 0x59, 0x76, 0xc0, 0x7b, 0x10, 0xa8, 0xda, 0xb6,
+ 0x14, 0xdc, 0xc2, 0x33, 0xa7, 0xfb, 0x35, 0x8e, 0x8f, 0x3d, 0x6c, 0x42,
+ 0xbe, 0x4f, 0x40, 0x08, 0xe0, 0x9f, 0xd3, 0xb2, 0xaa, 0xc9, 0x73, 0x2c,
+ 0x66, 0x43, 0x01, 0xa4, 0xb9, 0x26, 0x9a, 0x01, 0x96, 0x5f, 0x12, 0x10,
+ 0x0f, 0xf8, 0x2d, 0x43, 0x07, 0xab, 0x33, 0x73, 0x3a, 0x58, 0xf1, 0x31,
+ 0x98, 0x61, 0x25, 0x28, 0xa4, 0x69, 0x1a, 0x77, 0xce, 0x55, 0x71, 0xe9,
+ 0x3e, 0x3b, 0x38, 0xd3, 0xa8, 0x9c, 0x13, 0xa1, 0xe7, 0xae, 0xa7, 0x96,
+ 0x3a, 0x31, 0x0d, 0x85, 0xb0, 0xa3, 0xb1, 0x1a, 0x8f, 0x19, 0x41, 0xfa,
+ 0xe9, 0x34, 0x42, 0x42, 0x89, 0x00, 0x03, 0x18, 0x60, 0x07, 0x12, 0x34,
+ 0xa3, 0x2c, 0x7e, 0x8e, 0x9f, 0x22, 0x0b, 0x2c, 0xd8, 0x23, 0xb4, 0x8e,
+ 0xff, 0x0b, 0xfc, 0xd8, 0x37, 0x9e, 0x63, 0xc7, 0xe4, 0x9d, 0xf6, 0xbf,
+ 0x65, 0xca, 0x65, 0xb7, 0xbd, 0xfb, 0x55, 0x08, 0x66, 0x34, 0x60, 0xea,
+ 0x3e, 0xcd, 0x9e, 0xa1, 0x7b, 0xd6, 0x26, 0x25, 0x1a, 0xb7, 0x51, 0x8c,
+ 0xda, 0x4b, 0xf4, 0xc4, 0xcc, 0x3f, 0x7c, 0xb5, 0x94, 0x85, 0x36, 0xb3,
+ 0xd7, 0x7c, 0xe4, 0x91, 0x88, 0xce, 0x79, 0x5d, 0xbd, 0x71, 0x48, 0x34,
+ 0xf5, 0x29, 0xba, 0x84, 0x9a, 0x0f, 0xb8, 0x4c, 0xa3, 0x2f, 0x7e, 0x17,
+ 0xc4, 0x37, 0x17, 0x57, 0x33, 0x9e, 0xce, 0xea, 0x7f, 0xd9, 0x32, 0x3c,
+ 0x69, 0xed, 0xb4, 0x50, 0xc9, 0xd4, 0xaf, 0x89, 0x30, 0x6e, 0xe6, 0xec,
+ 0x90, 0x00, 0x4c, 0xa0, 0x00, 0x77, 0xf9, 0x61, 0x45, 0x41, 0x77, 0x55,
+ 0x97, 0xaf, 0xe6, 0x92, 0x25, 0xb5, 0x10, 0x0e, 0x48, 0xf4, 0xc0, 0xc2,
+ 0x52, 0x09, 0xc7, 0x6c, 0x2d, 0x31, 0x6f, 0xd1, 0x64, 0xb5, 0x54, 0x13,
+ 0x1e, 0xc6, 0x93, 0xf8, 0x5c, 0x7f, 0x90, 0xa1, 0x4e, 0x15, 0xeb, 0x93,
+ 0x14, 0x95, 0x75, 0x48, 0xa5, 0x19, 0x70, 0x64, 0xee, 0xff, 0x57, 0xc9,
+ 0x9a, 0xf9, 0x3f, 0x38, 0x5b, 0x34, 0x20, 0x2d, 0x65, 0xb9, 0xc3, 0xac,
+ 0x89, 0x21, 0x4f, 0x12, 0xc6, 0x31, 0xd7, 0x16, 0x35, 0xb3, 0x58, 0x78,
+ 0xd6, 0xe7, 0x15, 0xa2, 0x66, 0xa6, 0xe3, 0x3c, 0x31, 0xc4, 0xf2, 0xcc,
+ 0x89, 0xc6, 0xa7, 0x31, 0xa8, 0x37, 0xb2, 0x4c, 0x19, 0xe1, 0x3e, 0x82,
+ 0x61, 0x72, 0xa1, 0x8c, 0xe1, 0xac, 0xad, 0x84, 0x72, 0x0f, 0xfb, 0x1f,
+ 0xd5, 0x40, 0x47, 0x98, 0x3a, 0x6c, 0xf8, 0xce, 0x54, 0x01, 0x07, 0xcc,
+ 0x19, 0x5a, 0x41, 0x17, 0x65, 0x69, 0x71, 0x96, 0x13, 0x38, 0xa6, 0xd1,
+ 0x97, 0xfd, 0xdc, 0x7b, 0x3c, 0x52, 0xf7, 0x1f, 0x8b, 0x7a, 0x3a, 0x65,
+ 0xe7, 0xbb, 0xe5, 0x8a, 0x7f, 0x4d, 0x5f, 0xa2, 0x74, 0xc7, 0x7b, 0x85,
+ 0xdf, 0x43, 0x9c, 0x2e, 0x63, 0x12, 0x40, 0xc5, 0xad, 0xa9, 0xe8, 0xd4,
+ 0x5b, 0xb0, 0x81, 0x57, 0xdb, 0x24, 0x21, 0x6e, 0x10, 0xaa, 0x05, 0x09,
+ 0x4b, 0x4c, 0x37, 0x0b, 0x35, 0x07, 0xa5, 0xb9, 0x3c, 0xdf, 0xb9, 0x9e,
+ 0x9d, 0x9d, 0x4f, 0x0e, 0x11, 0x86, 0x39, 0xd8, 0x32, 0x28, 0xfe, 0x5b,
+ 0x8d, 0xeb, 0x99, 0x2d, 0x06, 0xe9, 0xf1, 0x73, 0xa6, 0x69, 0xd5, 0xb7,
+ 0xd6, 0x7a, 0xbe, 0xe7, 0x84, 0xc1, 0x37, 0x94, 0xe6, 0xfa, 0x2a, 0x17,
+ 0xb3, 0x40, 0xed, 0x02, 0x9c, 0xc5, 0x50, 0x47, 0x4b, 0xfe, 0x56, 0xdb,
+ 0x74, 0x9d, 0x68, 0x7f, 0x36, 0xd4, 0xeb, 0x2c, 0x81, 0xa2, 0x24, 0xa8,
+ 0x30, 0x0c, 0x71, 0xd3, 0x98, 0xa0, 0xec, 0xb8, 0x7e, 0x95, 0x07, 0x5c,
+ 0xca, 0x90, 0x52, 0xbd, 0xa3, 0xaf, 0x20, 0x4f, 0x67, 0xc9, 0x29, 0x2c,
+ 0x7c, 0xf8, 0x3f, 0x29, 0x96, 0xa5, 0x12, 0x66, 0xb3, 0x57, 0x74, 0x32,
+ 0x9a, 0x17, 0x24, 0x41, 0x29, 0xb2, 0xff, 0x7a, 0xf2, 0xee, 0x63, 0x8c,
+ 0x33, 0xe8, 0x8d, 0x4c, 0x46, 0x51, 0x4c, 0x34, 0x97, 0x50, 0x56, 0x7d,
+ 0x9a, 0x62, 0x01, 0xef, 0x54, 0xd9, 0x70, 0xbb, 0x40, 0xbe, 0x68, 0x12,
+ 0x59, 0x9e, 0xe6, 0x75, 0xae, 0x05, 0xe2, 0x6a, 0x7f, 0xae, 0x91, 0x37,
+ 0x3c, 0xd6, 0x82, 0x31, 0x7c, 0x83, 0x21, 0xff, 0xb0, 0x5d, 0x01, 0xc1,
+ 0xee, 0xc6, 0xff, 0x77, 0x94, 0xe0, 0x0e, 0xe0, 0xe6, 0x64, 0x86, 0xf2,
+ 0xea, 0xb9, 0x7a, 0xa4, 0xc4, 0x42, 0xf4, 0xbd, 0x71, 0x7d, 0xa4, 0xd6,
+ 0x27, 0x34, 0x69, 0x4d, 0xa2, 0x82, 0xa7, 0x70, 0xcf, 0xc4, 0x0f, 0x85,
+ 0xe4, 0x8a, 0x9f, 0x45, 0x20, 0x15, 0x27, 0x14, 0xf8, 0x7f, 0xb1, 0x1b,
+ 0xf6, 0x65, 0xf2, 0xa9, 0xed, 0x01, 0xec, 0x56, 0x3a, 0xae, 0xcb, 0x39,
+ 0x0a, 0xd6, 0x50, 0xe5, 0x53, 0x9a, 0x3e, 0x68, 0xf7, 0x98, 0x44, 0x0d,
+ 0x1a, 0x3a, 0x9b, 0xc5, 0xb5, 0x26, 0x52, 0x04, 0x0b, 0x8f, 0x0b, 0x46,
+ 0x1d, 0x93, 0x2d, 0xd8, 0x2f, 0xfe, 0x9d, 0x73, 0x54, 0x2b, 0xb5, 0x8a,
+ 0xb6, 0xd4, 0x8b, 0x23, 0xfd, 0x05, 0x28, 0xe5, 0xe8, 0xef, 0xf5, 0xa1,
+ 0x91, 0xaf, 0x8a, 0x73, 0x3e, 0xd7, 0x57, 0xd1, 0xcd, 0x3b, 0xc6, 0xee,
+ 0x28, 0xb5, 0x87, 0xd4, 0xa4, 0x92, 0xaa, 0x93, 0xa2, 0x42, 0xbd, 0xb2,
+ 0x1b, 0xcb, 0x24, 0xcb, 0x4c, 0x71, 0x31, 0xce, 0x00, 0xeb, 0x82, 0xf9,
+ 0xdb, 0xe9, 0xab, 0xf2, 0x24, 0x0e, 0x79, 0xbc, 0x72, 0x7c, 0xce, 0x40,
+ 0xcb, 0xc6, 0x3e, 0xb4, 0xb7, 0x16, 0xdf, 0x8b, 0xb2, 0x10, 0x98, 0x9d,
+ 0xf7, 0x8d, 0x0b, 0x4d, 0xb9, 0xa7, 0xd8, 0x3d, 0xc1, 0xa0, 0x6d, 0x4a,
+ 0x8b, 0x54, 0x8a, 0xfe, 0xcd, 0x20, 0x80, 0x11, 0x61, 0x6d, 0x6d, 0x1b,
+ 0xec, 0x36, 0x9b, 0x0b, 0x80, 0xf8, 0x27, 0x76, 0x48, 0x04, 0x6a, 0x29,
+ 0x34, 0x2d, 0x0c, 0x3b, 0xcf, 0xe5, 0x52, 0x8b, 0x5a, 0x00, 0x05, 0xe3,
+ 0x24, 0xe7, 0x93, 0xd9, 0xe2, 0x50, 0x02, 0xa4, 0x45, 0xd2, 0x25, 0x37,
+ 0xbf, 0x8c, 0x77, 0x1e, 0xc9, 0x40, 0x04, 0x3c, 0x16, 0x19, 0x3d, 0x40,
+ 0xce, 0x9d, 0xe3, 0xa6, 0xbe, 0x19, 0x0f, 0x22, 0xe7, 0xdd, 0x9b, 0x25,
+ 0xaa, 0x32, 0x54, 0x1b, 0x84, 0x7b, 0xa4, 0x5f, 0xd3, 0xaf, 0x22, 0x23,
+ 0x3f, 0xec, 0x17, 0xe8, 0x49, 0x2a, 0xb6, 0xd8, 0xd0, 0x0d, 0x9d, 0x72,
+ 0xd5, 0xae, 0x06, 0xcb, 0x25, 0x52, 0xa8, 0xdc, 0x4c, 0x5a, 0xe3, 0x16,
+ 0xba, 0x25, 0x7c, 0xc8, 0x49, 0xf0, 0x51, 0xf6, 0x48, 0x17, 0xe5, 0x63,
+ 0xb3, 0xe5, 0x32, 0x56, 0x2a, 0x87, 0xa9, 0xbf, 0x7b, 0xaa, 0x7f, 0x1e,
+ 0xf1, 0x98, 0x38, 0x2b, 0xa4, 0x5a, 0x3e, 0x0d, 0xb5, 0x02, 0xbf, 0x74,
+ 0x8f, 0x5b, 0xd2, 0x2c, 0x0a, 0x6f, 0x2c, 0x18, 0x40, 0x32, 0x97, 0x2b,
+ 0x82, 0xaa, 0x18, 0xe6, 0x54, 0x49, 0xc0, 0x4f, 0xbd, 0x87, 0xd7, 0xe8,
+ 0x0f, 0x4e, 0xcb, 0x0b, 0xfd, 0x9d, 0xe2, 0xdf, 0xb4, 0xa4, 0xe9, 0x3a,
+ 0x7b, 0x5b, 0x57, 0x20, 0x50, 0x62, 0x7f, 0xca, 0x22, 0x08, 0x03, 0x71,
+ 0x6c, 0x6e, 0x90, 0x50, 0x23, 0x98, 0x35, 0x32, 0x8c, 0x29, 0xac, 0xa5,
+ 0x59, 0x53, 0x5e, 0xd9, 0xee, 0x2e, 0x0c, 0x0e, 0xa8, 0x36, 0xce, 0xf8,
+ 0xe1, 0xd0, 0xf0, 0xbe, 0xde, 0x53, 0x62, 0xac, 0x1f, 0xcb, 0x8b, 0x17,
+ 0x10, 0xd5, 0x34, 0xcb, 0x85, 0x8c, 0xf5, 0x54, 0x51, 0x8c, 0xf3, 0x4d,
+ 0x44, 0x90, 0x2d, 0x0b, 0x2c, 0x14, 0x0d, 0xc5, 0xe8, 0x9b, 0x76, 0xa5,
+ 0x1a, 0x0e, 0x56, 0xf1, 0x51, 0xaf, 0x4f, 0x07, 0xf5, 0x2e, 0xfd, 0xaa,
+ 0x98, 0xe7, 0x76, 0x8e, 0xb0, 0x8d, 0x19, 0x75, 0x8b, 0xbf, 0xa0, 0x54,
+ 0x2a, 0x96, 0x76, 0xfc, 0x56, 0x20, 0xf6, 0x3b, 0x58, 0xbc, 0x16, 0x87,
+ 0xa9, 0x16, 0x7a, 0xda, 0x0a, 0x81, 0x67, 0x51, 0x40, 0x33, 0x37, 0x54,
+ 0x36, 0x7c, 0x41, 0x6d, 0xc7, 0x6f, 0x3c, 0xf0, 0x0b, 0x3c, 0x37, 0xe8,
+ 0x9d, 0xf4, 0x9a, 0x79, 0xf8, 0x8e, 0x23, 0x27, 0x7c, 0x5a, 0x7d, 0xc3,
+ 0xde, 0x1e, 0xa8, 0xd8, 0x7a, 0x0d, 0x4e, 0x2c, 0x2f, 0x18, 0xa6, 0xeb,
+ 0xcd, 0x33, 0xd1, 0xe2, 0x1d, 0x3a, 0x5b, 0xd1, 0x4f, 0x06, 0xc4, 0x80,
+ 0x96, 0x7c, 0xeb, 0x22, 0xe8, 0xb3, 0x80, 0x5f, 0x3a, 0x30, 0x16, 0x13,
+ 0x8b, 0x48, 0xc6, 0x4b, 0x92, 0x24, 0xd3, 0xc6, 0x0e, 0xd3, 0x3f, 0xab,
+ 0xc1, 0x71, 0x0b, 0xfc, 0xe0, 0x45, 0xbe, 0xaa, 0xd3, 0xc9, 0xe1, 0x00,
+ 0x7c, 0x37, 0x48, 0xb1, 0x62, 0xaf, 0x6d, 0x62, 0xf9, 0x31, 0xb9, 0x92,
+ 0xbc, 0x79, 0x7e, 0xe5, 0x61, 0xae, 0xcc, 0x4b, 0x19, 0x31, 0xf4, 0x51,
+ 0xbf, 0x1d, 0x22, 0x25, 0x6e, 0xe3, 0xf5, 0x88, 0x70, 0x01, 0x77, 0xff,
+ 0x03, 0x6f, 0x61, 0x2b, 0x82, 0x1b, 0x3a, 0xc3, 0x6b, 0x1b, 0xe5, 0xef,
+ 0xc5, 0x40, 0x50, 0x49, 0x4b, 0x39, 0x33, 0x7c, 0x22, 0x5b, 0x5b, 0xbc,
+ 0xbe, 0x6e, 0x3d, 0x09, 0xfc, 0xa0, 0x6e, 0xb8, 0x76, 0x78, 0x0f, 0xeb,
+ 0x0c, 0xf4, 0x23, 0x79, 0x98, 0x9b, 0xf3, 0xc4, 0xfd, 0xbe, 0xb2, 0x85,
+ 0x45, 0xdc, 0xa0, 0x9b, 0xed, 0x82, 0xa9, 0x36, 0xcd, 0x94, 0xf6, 0x2b,
+ 0x60, 0x9d, 0xf4, 0x27, 0xe6, 0xbd, 0xf2, 0x6a, 0x91, 0xaf, 0xd8, 0x10,
+ 0xf1, 0xf4, 0xd4, 0xee, 0x7f, 0x33, 0x16, 0x04, 0xaf, 0xae, 0xd9, 0x88,
+ 0x28, 0x4e, 0xa8, 0xde, 0xce, 0x60, 0x02, 0x16, 0x45, 0x3e, 0x39, 0x7f,
+ 0x1c, 0xfc, 0xa3, 0x81, 0x97, 0x17, 0xf8, 0xbd, 0xad, 0xca, 0x32, 0x02,
+ 0x86, 0x46, 0xaa, 0xff, 0x7a, 0x39, 0xf9, 0xf0, 0xee, 0xa8, 0xaf, 0x56,
+ 0x74, 0x11, 0x92, 0xbf, 0xb4, 0x0a, 0x12, 0x80, 0x3d, 0x21, 0xd4, 0xca,
+ 0x28, 0x99, 0xb4, 0xd9, 0x99, 0x72, 0xe3, 0xdf, 0x0e, 0x31, 0xfb, 0x42,
+ 0xd6, 0xc3, 0x10, 0xd2, 0x28, 0x45, 0x67, 0x67, 0xfd, 0x81, 0xc8, 0x80,
+ 0x46, 0xed, 0xb2, 0x53, 0x54, 0x13, 0xbf, 0x21, 0x6e, 0xed, 0xe4, 0x3f,
+ 0x8c, 0xb7, 0x4b, 0x28, 0x5c, 0x2f, 0x4a, 0x78, 0x3c, 0x56, 0x09, 0x8b,
+ 0x36, 0x45, 0x81, 0xcb, 0x17, 0xbc, 0x3f, 0x86, 0x87, 0xdf, 0x64, 0xfa,
+ 0x76, 0x04, 0xd8, 0xb2, 0xa8, 0x3d, 0xf6, 0x69, 0x1c, 0xbc, 0x80, 0x19,
+ 0xea, 0x37, 0x1c, 0xf3, 0x28, 0xa8, 0x80, 0x1c, 0xc5, 0x85, 0x3c, 0xb8,
+ 0xc3, 0x29, 0x61, 0xba, 0x90, 0xc9, 0x6b, 0x25, 0xfb, 0xa6, 0x37, 0x7a,
+ 0x5c, 0xd6, 0x2a, 0xdd, 0xdb, 0x30, 0x40, 0x3b, 0x8e, 0x8d, 0xee, 0xd0,
+ 0xb8, 0x7e, 0xee, 0xe7, 0x2e, 0xd0, 0x34, 0xdf, 0x27, 0x6f, 0x23, 0xf5,
+ 0x5c, 0x2c, 0x13, 0x67, 0x4c, 0xb8, 0xb3, 0x52, 0xbc, 0xb6, 0x98, 0xf8,
+ 0x3f, 0x98, 0x20, 0x0a, 0x76, 0x7a, 0x27, 0x17, 0xa6, 0xab, 0x4b, 0x70,
+ 0x3d, 0x6d, 0xdc, 0x5d, 0x0a, 0xc7, 0x48, 0x82, 0x47, 0x8b, 0x62, 0x54,
+ 0x0b, 0x5a, 0x05, 0x03, 0x4d, 0x6f, 0xa4, 0xcb, 0x2b, 0x7c, 0x07, 0xe6,
+ 0x17, 0x9e, 0x8b, 0xb9, 0x52, 0x9e, 0xbf, 0x0f, 0x71, 0x9a, 0x61, 0x90,
+ 0x45, 0x59, 0x09, 0xa5, 0x90, 0x28, 0xfa, 0x49, 0x2e, 0x06, 0x21, 0x1b,
+ 0x7b, 0x13, 0x43, 0x75, 0x14, 0x95, 0x17, 0x8a, 0x91, 0x74, 0x47, 0x0c,
+ 0x90, 0x4b, 0xd5, 0xa0, 0x91, 0x80, 0x91, 0xa3, 0xa2, 0x39, 0x00, 0xdd,
+ 0x87, 0x4d, 0x2e, 0x90, 0x0a, 0x61, 0xca, 0x0f, 0xb3, 0x8b, 0xae, 0x04,
+ 0xa9, 0x04, 0xa6, 0xd9, 0x6e, 0x04, 0x83, 0xd8, 0xa4, 0xcc, 0x4a, 0x05,
+ 0x80, 0x53, 0x07, 0xa0, 0xe8, 0x68, 0xae, 0x64, 0x44, 0x9e, 0x51, 0xfb,
+ 0x41, 0xae, 0xb9, 0x68, 0x26, 0xfb, 0x8d, 0xae, 0x3f, 0xc7, 0x60, 0xbd,
+ 0x6c, 0x26, 0xe1, 0x63, 0xa1, 0x9a, 0xe8, 0x17, 0x84, 0x52, 0xee, 0xb0,
+ 0xa0, 0x8a, 0x83, 0x09, 0xcc, 0xbd, 0xd8, 0x68, 0x64, 0x4b, 0xcd, 0x72,
+ 0x39, 0xd1, 0xa0, 0xc0, 0x04, 0x46, 0xc6, 0xa8, 0x5e, 0x4d, 0x01, 0x7f,
+ 0x14, 0xdc, 0xd0, 0x42, 0xc3, 0xa0, 0xae, 0x45, 0x80, 0x4e, 0x52, 0x85,
+ 0x12, 0xa8, 0x0c, 0x86, 0x7a, 0xf8, 0x2d, 0x5a, 0x47, 0x5d, 0xa1, 0x99,
+ 0x33, 0x25, 0x3b, 0x4a, 0xc8, 0x3f, 0x57, 0xbf, 0x79, 0x07, 0xe8, 0x73,
+ 0x5b, 0x77, 0x52, 0x3a, 0x87, 0xe2, 0x67, 0x27, 0x4c, 0xda, 0xa6, 0x66,
+ 0x48, 0xe0, 0x99, 0x3d, 0xd0, 0xbe, 0x39, 0x09, 0xf7, 0xfb, 0x79, 0xce,
+ 0xe7, 0xf0, 0x47, 0xd0, 0x80, 0x3a, 0x6e, 0x26, 0x25, 0x27, 0xf7, 0x78,
+ 0xec, 0x53, 0xd4, 0x0b, 0x34, 0x42, 0x4a, 0x5c, 0xf9, 0x1f, 0xab, 0x86,
+ 0xd5, 0x2f, 0x5a, 0x4b, 0xc2, 0x53, 0x10, 0x40, 0xda, 0x75, 0x1b, 0x88,
+ 0x8d, 0x33, 0xdd, 0xe1, 0x29, 0x5f, 0xe5, 0x30, 0x95, 0xfc, 0x8b, 0x13,
+ 0x29, 0x3a, 0x7c, 0x5d, 0xa2, 0x24, 0x88, 0xf9, 0xa5, 0x30, 0xcb, 0xb1,
+ 0xb4, 0xae, 0x0f, 0x60, 0x59, 0x89, 0x6c, 0x98, 0x82, 0xef, 0xb7, 0x28,
+ 0xcf, 0x7b, 0xfe, 0xfa, 0x88, 0x0d, 0x62, 0x70, 0x49, 0x90, 0xd3, 0x31,
+ 0xe0, 0x0e, 0xb2, 0x74, 0x9b, 0x45, 0x4b, 0x43, 0xf4, 0xc4, 0xa9, 0xec,
+ 0x44, 0xd7, 0xb9, 0x21, 0x78, 0xf8, 0x18, 0xfe, 0x3e, 0xf5, 0xb2, 0x76,
+ 0x5b, 0x01, 0x70, 0xe6, 0xb8, 0xef, 0xc3, 0xcf, 0x0c, 0xb3, 0x0e, 0x6b,
+ 0xaa, 0xc9, 0xcf, 0xcf, 0x4a, 0x84, 0xa6, 0x40, 0x7d, 0xe9, 0xb7, 0x8d,
+ 0xcb, 0x7a, 0x7a, 0x29, 0xbd, 0x12, 0xf9, 0x88, 0x3f, 0xdc, 0x69, 0xa8,
+ 0x70, 0xc3, 0xf5, 0x01, 0x6f, 0x84, 0x5f, 0x1a, 0x21, 0xc0, 0x10, 0x58,
+ 0xaf, 0x2d, 0x10, 0x68, 0xbf, 0xc4, 0xdd, 0x50, 0xc9, 0x1d, 0x44, 0xf7,
+ 0x86, 0x86, 0x87, 0x0b, 0xa6, 0x1d, 0xef, 0xf3, 0x72, 0x0d, 0x34, 0xe6,
+ 0x68, 0xe7, 0x09, 0x9c, 0x93, 0x9b, 0x83, 0x2f, 0x81, 0x28, 0xfb, 0x4f,
+ 0x03, 0x83, 0xac, 0x75, 0x00, 0x14, 0x51, 0xb2, 0x21, 0x73, 0x35, 0x37,
+ 0x25, 0xab, 0xc0, 0x1c, 0x4d, 0x21, 0x0a, 0xab, 0x28, 0xa0, 0xe7, 0x5b,
+ 0x47, 0x5a, 0x07, 0x58, 0xa9, 0x85, 0xfc, 0x19, 0x43, 0x32, 0xc5, 0xea,
+ 0x89, 0x84, 0x57, 0xe6, 0x7e, 0xa3, 0x95, 0x81, 0x61, 0x6b, 0xbd, 0xc5,
+ 0x35, 0xf2, 0xc4, 0x24, 0xcd, 0xf0, 0x42, 0xff, 0xe0, 0x5e, 0xc2, 0xed,
+ 0x43, 0xd7, 0xea, 0x72, 0x34, 0x7f, 0x8b, 0x14, 0xc1, 0x85, 0xe5, 0x74,
+ 0x0e, 0x76, 0xa1, 0xe5, 0xd5, 0xa4, 0x5a, 0xe1, 0xc4, 0xe9, 0xf4, 0xa4,
+ 0xd2, 0xa8, 0xd3, 0x11, 0xc9, 0xb8, 0xeb, 0xa4, 0xd7, 0x30, 0xba, 0x49,
+ 0xb5, 0x62, 0xa2, 0xc4, 0x30, 0x26, 0x5b, 0x0d, 0x45, 0xaa, 0xce, 0x7b,
+ 0x6d, 0xb3, 0xe9, 0x90, 0x38, 0xf8, 0xaa, 0xc0, 0xb0, 0x00, 0x9b, 0x45,
+ 0x03, 0x34, 0x2d, 0x23, 0xa2, 0x75, 0xda, 0x59, 0xa0, 0xcb, 0x01, 0xcb,
+ 0xf5, 0x74, 0x60, 0x19, 0x47, 0x9f, 0xc9, 0x2b, 0xfa, 0x0c, 0xef, 0xde,
+ 0x1b, 0x7e, 0x33, 0x53, 0xbd, 0x78, 0x6f, 0x88, 0x34, 0x86, 0x80, 0x9a,
+ 0xd1, 0xbd, 0x6b, 0x89, 0x4f, 0xb0, 0x21, 0x7d, 0xf1, 0xcb, 0xc0, 0x23,
+ 0xc9, 0xf0, 0xbd, 0xa3, 0x6b, 0x79, 0xc3, 0xa5, 0x15, 0x09, 0xbc, 0xe0,
+ 0x25, 0x38, 0xed, 0x61, 0x4d, 0x7e, 0xc7, 0x68, 0xa7, 0x19, 0x2e, 0xb6,
+ 0xc8, 0xa0, 0x36, 0x8c, 0xda, 0xdf, 0xce, 0x3b, 0x8f, 0x95, 0x41, 0x5b,
+ 0x71, 0x95, 0xe3, 0xd3, 0x18, 0x89, 0x2e, 0x2b, 0x71, 0x11, 0x14, 0xf2,
+ 0x33, 0x84, 0x11, 0x69, 0xb0, 0x43, 0x0b, 0xd2, 0x45, 0xbd, 0x68, 0xfe,
+ 0xc4, 0x9e, 0x32, 0xc5, 0x77, 0x13, 0xea, 0xba, 0x8c, 0x5d, 0x20, 0x44,
+ 0x30, 0xe5, 0x25, 0xca, 0x75, 0x13, 0xef, 0xd8, 0x15, 0x6d, 0x8d, 0xe6,
+ 0xef, 0xe1, 0xe7, 0x6c, 0x83, 0xd8, 0x15, 0x6c, 0x6a, 0x66, 0x40, 0x9c,
+ 0x1b, 0xc2, 0x41, 0x41, 0xc0, 0x20, 0x66, 0x39, 0x87, 0x72, 0xc7, 0x59,
+ 0xdb, 0x0f, 0x4e, 0x15, 0x3a, 0x6d, 0x6f, 0x23, 0x55, 0x92, 0x59, 0xdc,
+ 0x46, 0xff, 0xd6, 0x19, 0x94, 0x01, 0xb5, 0x58, 0x16, 0x3c, 0x0b, 0x2e,
+ 0xdc, 0xa6, 0x4d, 0xf0, 0xcc, 0xcb, 0x8c, 0xab, 0xc1, 0xca, 0xa9, 0x36,
+ 0x13, 0x7e, 0xf2, 0x29, 0x13, 0xf0, 0x25, 0x1e, 0xd4, 0x8c, 0x21, 0x7a,
+ 0x93, 0xce, 0xc7, 0xb2, 0xca, 0x79, 0x69, 0x2d, 0xd4, 0x40, 0x0e, 0x59,
+ 0xae, 0x49, 0xaa, 0xea, 0xd3, 0x24, 0x01, 0xc6, 0x02, 0x36, 0x0d, 0x5e,
+ 0xd6, 0x00, 0x03, 0x03, 0x04, 0xe2, 0x9c, 0x01, 0x26, 0xbe, 0xd7, 0x28,
+ 0xde, 0x72, 0x44, 0xf7, 0xca, 0x5e, 0x20, 0xdb, 0x3a, 0x35, 0x63, 0xe9,
+ 0x90, 0x05, 0xfd, 0xb8, 0x3e, 0x98, 0x71, 0x42, 0x9d, 0x55, 0x96, 0xa4,
+ 0xd3, 0x78, 0x3b, 0x2c, 0xa5, 0x4a, 0x29, 0x40, 0xda, 0x42, 0x50, 0x9a,
+ 0x8a, 0x92, 0xec, 0x22, 0x92, 0xfd, 0x9f, 0x04, 0xe3, 0xa1, 0x9d, 0xea,
+ 0x01, 0x35, 0x88, 0xb2, 0xa9, 0x86, 0xcc, 0x2b, 0x48, 0x44, 0x9e, 0x96,
+ 0x1e, 0xf9, 0x85, 0xa2, 0x30, 0xab, 0xb5, 0x5b, 0xf1, 0x53, 0xb6, 0x0e,
+ 0xaa, 0xee, 0x0f, 0xd0, 0x80, 0x66, 0x6f, 0x47, 0xbd, 0x18, 0x3f, 0x04,
+ 0x5c, 0x6c, 0xdb, 0xe6, 0xc1, 0x02, 0x0c, 0xb1, 0x31, 0x93, 0x91, 0x16,
+ 0x7c, 0xfd, 0x70, 0x7f, 0x91, 0x66, 0x00, 0x13, 0x74, 0x6b, 0x65, 0xf9,
+ 0xdf, 0x33, 0xfc, 0xb5, 0xb3, 0x84, 0x59, 0xa4, 0xb0, 0x18, 0x8f, 0x5b,
+ 0x38, 0xc1, 0x2f, 0x83, 0x00, 0x97, 0x33, 0x14, 0x68, 0x0d, 0x64, 0x93,
+ 0xac, 0xbd, 0xfa, 0xdf, 0x6b, 0x97, 0x20, 0x42, 0x7e, 0x7e, 0xde, 0x2d,
+ 0x90, 0x32, 0x7d, 0x0f, 0x8b, 0xde, 0x2e, 0x7f, 0x10, 0x33, 0x59, 0xeb,
+ 0x85, 0xf7, 0x1f, 0x5a, 0x62, 0x1e, 0x3c, 0x14, 0xc2, 0x8a, 0xf7, 0xe2,
+ 0xeb, 0xbf, 0x9d, 0xd2, 0xe9, 0xe5, 0x6d, 0xa1, 0xa0, 0x87, 0x95, 0xa5,
+ 0x64, 0x72, 0xf3, 0xf3, 0x4f, 0x76, 0x7a, 0x2d, 0x53, 0x76, 0x5a, 0xfe,
+ 0xa5, 0xd0, 0xd2, 0xfa, 0xdd, 0x33, 0xb2, 0xaa, 0x67, 0x5f, 0x5c, 0x54,
+ 0xcb, 0xe7, 0xe2, 0x40, 0xcf, 0x69, 0x1d, 0xe8, 0x68, 0xba, 0x9c, 0xa8,
+ 0x96, 0xe7, 0xef, 0xce, 0xae, 0xfe, 0x70, 0x88, 0xcd, 0x1a, 0xec, 0xa3,
+ 0xd7, 0x6b, 0xf3, 0x1b, 0x1d, 0x2f, 0xc6, 0x83, 0x5e, 0x92, 0xb0, 0x11,
+ 0x74, 0x75, 0x1c, 0x6d, 0x62, 0xc8, 0x14, 0x6c, 0x33, 0x1a, 0x92, 0x25,
+ 0x6b, 0x61, 0x04, 0x29, 0xf1, 0x27, 0x36, 0xd3, 0x35, 0x5d, 0xd8, 0x01,
+ 0x6d, 0x31, 0xaa, 0xac, 0x61, 0xe6, 0xeb, 0x96, 0xd2, 0x39, 0x3b, 0xb8,
+ 0xa3, 0x0a, 0xde, 0x40, 0xc4, 0xaa, 0x59, 0x09, 0x0f, 0x82, 0x67, 0xe0,
+ 0xa6, 0xc3, 0x74, 0x2d, 0x37, 0x34, 0x5a, 0x47, 0x9d, 0xf8, 0x8e, 0x72,
+ 0xee, 0x8d, 0x74, 0x5d, 0x68, 0x3a, 0xc0, 0x2a, 0x80, 0x80, 0x16, 0x8b,
+ 0x14, 0x9d, 0xca, 0x04, 0xf4, 0x6c, 0x3b, 0x78, 0x6f, 0x38, 0x09, 0xb9,
+ 0xf6, 0xe0, 0x6b, 0x61, 0x65, 0x10, 0x34, 0x22, 0xe9, 0xac, 0x0f, 0x0b,
+ 0x27, 0x12, 0x0b, 0x77, 0x11, 0x29, 0x53, 0xb9, 0xdd, 0x98, 0x02, 0x1b,
+ 0xa1, 0x63, 0x63, 0x46, 0x1d, 0xb7, 0x73, 0x35, 0x2c, 0xf9, 0xb7, 0x39,
+ 0x50, 0x5a, 0xe9, 0xd5, 0x10, 0x9d, 0xa4, 0xc0, 0xe9, 0xe0, 0xa1, 0x42,
+ 0xe0, 0xd3, 0x39, 0x6d, 0x78, 0xa3, 0x42, 0x49, 0x42, 0xc1, 0x24, 0xe8,
+ 0x06, 0xc1, 0x08, 0xb7, 0xa4, 0xe4, 0xcc, 0xb7, 0x04, 0xf5, 0x65, 0x26,
+ 0x3d, 0xd0, 0xaa, 0x88, 0x7f, 0xec, 0x42, 0x19, 0xe2, 0xc3, 0x78, 0x9b,
+ 0x55, 0x77, 0xf7, 0x57, 0x79, 0x8b, 0xc0, 0x90, 0x47, 0xba, 0x0e, 0x58,
+ 0x23, 0x51, 0x01, 0x03, 0xcf, 0xd9, 0x1f, 0x97, 0xba, 0x05, 0x43, 0x59,
+ 0x9b, 0x3f, 0xad, 0x42, 0x82, 0xd8, 0xdf, 0x23, 0x3b, 0xf9, 0x0c, 0x66,
+ 0xa7, 0xed, 0x26, 0xc1, 0xc7, 0x82, 0x92, 0xf2, 0x7b, 0xe9, 0xbd, 0x7d,
+ 0x97, 0xb2, 0x04, 0xce, 0x6e, 0x60, 0x1c, 0x4e, 0x8e, 0x64, 0xa3, 0xa0,
+ 0x75, 0x49, 0x8a, 0xb3, 0x4a, 0x2d, 0xea, 0xef, 0x47, 0x8f, 0xaf, 0x8a,
+ 0x81, 0x5f, 0xb3, 0xee, 0x31, 0x69, 0x1f, 0x67, 0xe6, 0x73, 0xc5, 0x9a,
+ 0xc3, 0x74, 0xb0, 0x4e, 0x52, 0x0e, 0x2f, 0x73, 0xb9, 0x17, 0x8c, 0x2b,
+ 0x28, 0xce, 0xd5, 0x42, 0xcf, 0x0b, 0xf5, 0x35, 0x81, 0x53, 0x72, 0xea,
+ 0x0c, 0xaa, 0xad, 0xee, 0x42, 0x8c, 0x74, 0x78, 0x6b, 0x03, 0xf2, 0x6e,
+ 0x03, 0x66, 0x5f, 0x56, 0x2b, 0x51, 0x95, 0x22, 0xf5, 0x8d, 0xd6, 0x09,
+ 0x53, 0x60, 0xe7, 0xdf, 0x50, 0x13, 0x70, 0x18, 0x65, 0x29, 0x99, 0x62,
+ 0x56, 0xb4, 0x35, 0xb7, 0x64, 0x93, 0xb7, 0x48, 0x32, 0xbc, 0x15, 0xe8,
+ 0x87, 0xe5, 0xdb, 0xf6, 0xd2, 0x6a, 0x71, 0x02, 0x01, 0x69, 0x87, 0x79,
+ 0xa6, 0x86, 0xfc, 0x1f, 0x4d, 0x0a, 0x88, 0x0d, 0x51, 0x7f, 0xdc, 0x55,
+ 0x06, 0x96, 0xd0, 0x0f, 0x34, 0xa9, 0x5e, 0xda, 0x34, 0x75, 0x0f, 0x0f,
+ 0x9b, 0x9c, 0x49, 0xea, 0xac, 0x1e, 0x48, 0x72, 0xa2, 0x96, 0x2d, 0x7a,
+ 0x74, 0x74, 0x0f, 0x4b, 0x6e, 0x10, 0x6c, 0x00, 0xa7, 0x6a, 0xfd, 0x43,
+ 0xbc, 0x9a, 0x4a, 0x71, 0x68, 0xad, 0xef, 0x90, 0x15, 0x11, 0x5b, 0x4d,
+ 0x0c, 0xd4, 0xe5, 0xf4, 0x81, 0x1f, 0x01, 0x8b, 0x2d, 0x87, 0xb2, 0xeb,
+ 0x6f, 0xaf, 0x3c, 0xe6, 0xef, 0x37, 0x65, 0x6d, 0x6d, 0x3c, 0x49, 0xa1,
+ 0x85, 0x5a, 0x25, 0xf1, 0x9f, 0x04, 0xd9, 0x16, 0x36, 0xfa, 0x18, 0xc7,
+ 0x8e, 0x5a, 0x6c, 0x57, 0xb7, 0x73, 0xf7, 0xf5, 0xbd, 0xb0, 0x5c, 0x00,
+ 0x60, 0xc8, 0xe3, 0xda, 0x05, 0x50, 0x0f, 0xad, 0x18, 0xf3, 0xa9, 0x49,
+ 0xb1, 0xe0, 0x10, 0xe3, 0xd8, 0x06, 0x7f, 0x5f, 0xda, 0x82, 0xf7, 0xd7,
+ 0x67, 0xa1, 0xc6, 0xe2, 0x20, 0x89, 0xca, 0x94, 0xc0, 0xc0, 0xf3, 0x8e,
+ 0xbf, 0x79, 0x24, 0x48, 0x6e, 0x09, 0x68, 0x60, 0x0a, 0xa0, 0x1a, 0x66,
+ 0x54, 0xa0, 0xbc, 0x77, 0xb1, 0xd7, 0xf4, 0x24, 0x7e, 0x47, 0xdc, 0x8d,
+ 0x6e, 0x4b, 0xd4, 0x74, 0x6b, 0xde, 0x8c, 0xb0, 0xd2, 0xed, 0x8e, 0x40,
+ 0xd4, 0x14, 0x51, 0x81, 0xad, 0x82, 0x51, 0x4d, 0xe2, 0xf9, 0x72, 0x66,
+ 0xd9, 0x51, 0x9f, 0x1f, 0x41, 0x70, 0x3a, 0x60, 0xe1, 0x5d, 0x39, 0x35,
+ 0xb3, 0x66, 0xdb, 0xc0, 0xbd, 0x12, 0xd6, 0xf8, 0x5e, 0x3e, 0x2f, 0x9c,
+ 0x96, 0x1a, 0x62, 0xb7, 0xa4, 0x2f, 0x3b, 0xd2, 0x41, 0x36, 0x06, 0x4b,
+ 0xe6, 0x58, 0x53, 0x5e, 0x10, 0xbf, 0xa2, 0x21, 0xf2, 0x1d, 0xd2, 0x93,
+ 0xb6, 0x71, 0x8c, 0xe5, 0x51, 0x5d, 0xad, 0x7b, 0x24, 0x28, 0x9f, 0x70,
+ 0x70, 0xdc, 0x3a, 0x3a, 0xff, 0x6e, 0x91, 0x13, 0xe3, 0xfa, 0x3d, 0xfe,
+ 0x35, 0x38, 0x69, 0x03, 0xfb, 0x69, 0x8d, 0x2b, 0xe3, 0x0f, 0x16, 0x3e,
+ 0x5e, 0x8a, 0x3e, 0x1e, 0x50, 0x98, 0xce, 0x00, 0x8f, 0x0e, 0xbe, 0xbb,
+ 0x22, 0x09, 0xd5, 0x13, 0x9f, 0x12, 0x66, 0x81, 0xfa, 0x34, 0x81, 0x58,
+ 0xa1, 0x04, 0x17, 0x22, 0x1a, 0xe7, 0x51, 0x05, 0x98, 0x8d, 0x9d, 0x83,
+ 0xda, 0x24, 0xb6, 0x2f, 0x82, 0xbc, 0x15, 0xe0, 0x51, 0x7b, 0xb8, 0x70,
+ 0x7d, 0xf2, 0xaa, 0x19, 0x6c, 0x16, 0xdc, 0xf5, 0x27, 0xa7, 0xd6, 0x7e,
+ 0xca, 0x94, 0x0b, 0xe7, 0x37, 0x40, 0xd6, 0x17, 0x93, 0x7a, 0x32, 0xc9,
+ 0x4d, 0x9b, 0xd9, 0xbc, 0x2c, 0xa6, 0x1e, 0xdc, 0xb2, 0x9b, 0x00, 0xba,
+ 0x86, 0xf4, 0xce, 0x87, 0xb4, 0x3f, 0x9a, 0x1a, 0xc7, 0xa7, 0xd0, 0x49,
+ 0x41, 0x39, 0xe9, 0x99, 0x0b, 0x88, 0x54, 0x27, 0xf0, 0x76, 0xec, 0x01,
+ 0x3d, 0x3e, 0x2a, 0x39, 0x12, 0xa1, 0x34, 0xda, 0xab, 0x41, 0x8b, 0x86,
+ 0x80, 0x4c, 0x46, 0x03, 0x15, 0x64, 0x49, 0x29, 0x6b, 0xb9, 0xc4, 0xb0,
+ 0x34, 0xae, 0x6c, 0x44, 0x0d, 0xff, 0xa6, 0x91, 0x94, 0x43, 0xf0, 0xaf,
+ 0x4b, 0x3b, 0xef, 0x0f, 0x70, 0x4f, 0xcd, 0x9d, 0x78, 0xeb, 0xb7, 0x3b,
+ 0x1e, 0x08, 0x86, 0xa4, 0x02, 0x8c, 0xa2, 0xaf, 0x23, 0x43, 0xe6, 0x35,
+ 0x0e, 0xfc, 0x31, 0xdc, 0xeb, 0xf8, 0xa3, 0x81, 0xec, 0xde, 0x68, 0x87,
+ 0x2b, 0x8b, 0x93, 0xf4, 0xf7, 0x4c, 0xd8, 0x6c, 0xcc, 0x40, 0xe9, 0x5f,
+ 0xe0, 0x60, 0xfd, 0x6e, 0xd4, 0x52, 0x8e, 0x65, 0x89, 0xd6, 0xcc, 0x01,
+ 0xb4, 0x0c, 0x03, 0xdd, 0xb1, 0x1f, 0x7d, 0x0e, 0x5e, 0x09, 0x62, 0x5b,
+ 0xe0, 0x3f, 0x32, 0xd8, 0x7a, 0x8b, 0x84, 0xa9, 0x82, 0x90, 0xf9, 0x06,
+ 0xef, 0xea, 0xe2, 0x3b, 0x6e, 0x1e, 0x2c, 0x02, 0x97, 0xad, 0xae, 0x67,
+ 0xb4, 0x7d, 0xc6, 0xd5, 0xf3, 0xd5, 0xad, 0xb4, 0xcc, 0xa4, 0x87, 0x1d,
+ 0x9e, 0x0e, 0xfb, 0xc0, 0x4d, 0xd5, 0xcd, 0xd1, 0xb2, 0xfb, 0xe9, 0x02,
+ 0x7d, 0x40, 0xc2, 0x86, 0xb4, 0x5d, 0xe0, 0x07, 0x9b, 0x36, 0x03, 0x8f,
+ 0x93, 0xa3, 0x68, 0x91, 0x05, 0xea, 0x05, 0x55, 0x25, 0xd1, 0x6e, 0x4e,
+ 0x5b, 0xd6, 0x01, 0x0a, 0x9b, 0x66, 0x47, 0x56, 0xb5, 0x7d, 0xf4, 0x6c,
+ 0xe1, 0x43, 0xa0, 0x94, 0x7e, 0x4c, 0x9a, 0x48, 0xd0, 0x22, 0x54, 0x55,
+ 0x1b, 0x9d, 0x70, 0xe6, 0x34, 0x8c, 0x3e, 0xbb, 0xe5, 0xa6, 0x28, 0x89,
+ 0x66, 0xc8, 0xd4, 0xd5, 0x83, 0x1b, 0xaf, 0x55, 0xc3, 0x58, 0xce, 0x82,
+ 0x1e, 0x66, 0xa4, 0xbe, 0x37, 0x9c, 0x07, 0xe5, 0x1e, 0xac, 0x80, 0x6d,
+ 0x82, 0xe4, 0xf0, 0x8d, 0x81, 0x20, 0xb2, 0x71, 0xee, 0x28, 0x39, 0x74,
+ 0x11, 0xbb, 0xc6, 0xe5, 0x93, 0xde, 0xdf, 0x32, 0x8a, 0x1a, 0x9a, 0x37,
+ 0xf6, 0x0d, 0xf3, 0x07, 0x2f, 0x2f, 0x2c, 0x84, 0x71, 0xc8, 0x32, 0x45,
+ 0xb3, 0x70, 0x99, 0x93, 0x22, 0x64, 0xa3, 0x22, 0x54, 0x08, 0xd6, 0xd0,
+ 0xb6, 0x64, 0xaf, 0xc6, 0x03, 0x0c, 0x2f, 0x48, 0x75, 0x02, 0xda, 0x7c,
+ 0xb3, 0xfa, 0xab, 0x03, 0x09, 0xbf, 0xd0, 0xdd, 0xdb, 0xa5, 0x45, 0xef,
+ 0xec, 0xe9, 0x6e, 0x5b, 0xba, 0xbd, 0xbc, 0xa2, 0x01, 0x2b, 0xc0, 0x5f,
+ 0xa1, 0x7d, 0x9a, 0x05, 0x19, 0xfe, 0x63, 0xaa, 0x49, 0xcc, 0x3c, 0x7f,
+ 0x88, 0x41, 0x60, 0x12, 0x76, 0xa7, 0x50, 0x9f, 0x3e, 0x91, 0x0e, 0x6f,
+ 0x55, 0x94, 0x83, 0xf8, 0x54, 0xde, 0x07, 0x3b, 0x20, 0x9f, 0xc8, 0x50,
+ 0x13, 0xba, 0x48, 0x9a, 0xc4, 0x36, 0x05, 0xc9, 0x05, 0x62, 0x6e, 0x3e,
+ 0x42, 0xfa, 0xc9, 0x5d, 0x6f, 0x63, 0xd9, 0x30, 0x8d, 0x48, 0x93, 0xd4,
+ 0x08, 0x39, 0x81, 0x0f, 0x1f, 0x23, 0x51, 0x94, 0x2b, 0xa0, 0x0a, 0x5f,
+ 0x2c, 0x6d, 0x89, 0x0a, 0x24, 0x47, 0x3a, 0x57, 0x16, 0xaa, 0x4b, 0xab,
+ 0x8c, 0x87, 0x5d, 0x2a, 0xc1, 0x10, 0x26, 0x98, 0x7c, 0x0d, 0x52, 0x83,
+ 0xcc, 0x81, 0x04, 0x1b, 0x54, 0xd4, 0xf2, 0x09, 0xac, 0xe1, 0x70, 0x8e,
+ 0xe4, 0x2f, 0xc2, 0xc0, 0x8e, 0x77, 0x19, 0x8e, 0x34, 0x44, 0xe1, 0x42,
+ 0x3e, 0x60, 0x0e, 0xa0, 0x6e, 0xd9, 0xea, 0x13, 0xa4, 0xe8, 0x33, 0xbd,
+ 0xa5, 0xd4, 0x95, 0x05, 0x0f, 0xf9, 0x8c, 0x8d, 0x1f, 0x90, 0x68, 0x15,
+ 0x2a, 0xe4, 0xb2, 0x12, 0xc3, 0x6d, 0x6b, 0xbf, 0x15, 0xd6, 0x07, 0xd8,
+ 0x3d, 0xbf, 0x34, 0xe6, 0x4c, 0x18, 0x6b, 0x05, 0x78, 0x3b, 0x04, 0x3f,
+ 0x21, 0x5c, 0xc2, 0xba, 0xa0, 0x75, 0xdf, 0xc4, 0x97, 0x98, 0xd7, 0xd0,
+ 0x12, 0x27, 0xd2, 0x44, 0xde, 0x75, 0x4b, 0x9c, 0x0b, 0x39, 0x36, 0x60,
+ 0x69, 0x81, 0xb4, 0xa3, 0x75, 0xe8, 0x67, 0xd2, 0xfa, 0x3a, 0x5d, 0x6f,
+ 0x13, 0x20, 0x55, 0xe5, 0xbb, 0x2f, 0xd3, 0x4e, 0x3f, 0x08, 0x17, 0xc7,
+ 0x77, 0xfa, 0xad, 0x4d, 0xc6, 0xad, 0xe3, 0x10, 0x20, 0xf5, 0xb2, 0x90,
+ 0xbf, 0xf6, 0xef, 0xe9, 0x24, 0x9f, 0xd1, 0xfd, 0xa4, 0xfa, 0xd5, 0x5b,
+ 0xc2, 0xb4, 0x5a, 0xa1, 0xd2, 0xcc, 0x59, 0xd6, 0x43, 0xf7, 0xff, 0x06,
+ 0xf9, 0xeb, 0xef, 0x70, 0x4d, 0x3f, 0x67, 0x15, 0x80, 0x9a, 0x4f, 0xf8,
+ 0x9c, 0xce, 0xff, 0x62, 0x40, 0x82, 0x1c, 0x96, 0xc5, 0xd5, 0xac, 0x75,
+ 0x99, 0x92, 0xc4, 0xae, 0x9a, 0xd6, 0x6b, 0x09, 0x0e, 0x30, 0x9b, 0xa6,
+ 0xa6, 0x64, 0x5d, 0x52, 0x3d, 0x5f, 0x6c, 0x72, 0x0d, 0xdd, 0x11, 0x4d,
+ 0x71, 0xee, 0xb7, 0xe7, 0xf8, 0xe4, 0xd7, 0x78, 0x42, 0xe1, 0x4c, 0x24,
+ 0xf5, 0x60, 0x28, 0x30, 0x86, 0x92, 0x98, 0x6b, 0x4e, 0x8c, 0x1f, 0x79,
+ 0x89, 0x90, 0xb8, 0x49, 0x19, 0xfc, 0x04, 0x5a, 0xe4, 0x35, 0x45, 0x16,
+ 0x73, 0x17, 0x85, 0xa5, 0xe7, 0xb0, 0xba, 0xdb, 0x20, 0x1c, 0x52, 0x46,
+ 0xce, 0xbe, 0x82, 0xeb, 0x66, 0x20, 0x84, 0xaf, 0x5d, 0x35, 0xfa, 0xc8,
+ 0xb6, 0xf3, 0x5e, 0x80, 0x6a, 0xcd, 0xfd, 0xb7, 0x56, 0x87, 0x88, 0xfd,
+ 0x92, 0xaf, 0x74, 0xa6, 0xb7, 0xbc, 0x20, 0x60, 0x85, 0x30, 0x55, 0xf9,
+ 0xa0, 0x51, 0xf5, 0x74, 0x65, 0x7c, 0x64, 0xf8, 0x11, 0x2c, 0x6c, 0x30,
+ 0xf5, 0xb8, 0x6e, 0x33, 0xda, 0x9c, 0x33, 0xe1, 0xce, 0x14, 0x24, 0xfc,
+ 0x2f, 0x65, 0xba, 0x2f, 0x51, 0xe0, 0x3b, 0x2b, 0x67, 0x9b, 0xaf, 0x7c,
+ 0xac, 0x9e, 0x9a, 0xa6, 0x24, 0x87, 0x5a, 0xfd, 0xb4, 0x56, 0x51, 0xa4,
+ 0x62, 0x7f, 0x69, 0xd9, 0x3c, 0xce, 0x45, 0xfa, 0x97, 0xfd, 0xc5, 0xe3,
+ 0x36, 0xa0, 0x3a, 0x10, 0xf5, 0xe5, 0x88, 0x5c, 0x97, 0x34, 0x10, 0xab,
+ 0xbc, 0xe5, 0x45, 0xd3, 0xd6, 0xd6, 0x3f, 0x2e, 0x65, 0x64, 0xed, 0x7b,
+ 0x37, 0xae, 0x4e, 0xcc, 0xb0, 0x4b, 0x8c, 0xe6, 0x3a, 0x78, 0xb2, 0xea,
+ 0xfd, 0x0f, 0xbf, 0x7e, 0x23, 0xf8, 0x10, 0x97, 0xa2, 0xd0, 0xa8, 0x9a,
+ 0x74, 0x3a, 0x51, 0xaa, 0x95, 0x11, 0x04, 0x89, 0x0b, 0xb2, 0x20, 0xcd,
+ 0xec, 0x3d, 0xe5, 0xdf, 0x65, 0xfb, 0xe1, 0x13, 0xcb, 0x23, 0xee, 0x89,
+ 0x58, 0x2c, 0xa5, 0xc6, 0xef, 0xfa, 0x5d, 0x79, 0x5f, 0x73, 0x7d, 0x0a,
+ 0x51, 0xb1, 0x26, 0x9a, 0x7a, 0xb9, 0x16, 0x3e, 0xb3, 0x0f, 0x0b, 0x25,
+ 0x9f, 0xe9, 0xb6, 0xb5, 0xe7, 0x98, 0x52, 0x2e, 0xcc, 0x29, 0xc6, 0x4f,
+ 0xd3, 0xc7, 0x73, 0x40, 0xbb, 0xfb, 0x48, 0x57, 0x3f, 0xea, 0xf8, 0x12,
+ 0x79, 0x0f, 0x15, 0x86, 0x8f, 0x9d, 0xcc, 0xde, 0x73, 0xed, 0xc1, 0x25,
+ 0x72, 0x20, 0xb1, 0x04, 0xef, 0x56, 0x50, 0x16, 0x81, 0x96, 0x7b, 0x5f,
+ 0x50, 0x39, 0x8b, 0x1b, 0x5d, 0xcb, 0xce, 0x73, 0xe3, 0x05, 0x31, 0x63,
+ 0x5b, 0xcd, 0x45, 0x30, 0xa5, 0xee, 0x10, 0x39, 0xd6, 0xc6, 0x6d, 0x3b,
+ 0xda, 0xe7, 0x23, 0x4d, 0x55, 0x44, 0x05, 0x82, 0x9f, 0x63, 0x7a, 0x10,
+ 0x1b, 0x7d, 0x1c, 0xcf, 0x30, 0x93, 0xe1, 0x6c, 0xc5, 0xa1, 0x17, 0x45,
+ 0x8a, 0x4e, 0x76, 0xb0, 0xf8, 0x03, 0x61, 0x72, 0x8a, 0x42, 0x7b, 0x49,
+ 0xca, 0x9a, 0xf4, 0x83, 0x64, 0xce, 0xa5, 0xd0, 0xf1, 0x58, 0xa8, 0xfb,
+ 0x53, 0x90, 0xde, 0x2f, 0x14, 0x40, 0x49, 0x04, 0xd4, 0xb4, 0xd9, 0x5f,
+ 0x8d, 0x1c, 0x3e, 0x67, 0x35, 0x53, 0x03, 0x28, 0x45, 0xb9, 0x98, 0x77,
+ 0x33, 0x37, 0x81, 0x26, 0xae, 0xcb, 0x50, 0x22, 0x02, 0x8c, 0xe6, 0xda,
+ 0xf6, 0x0e, 0x9f, 0xd6, 0x40, 0xd9, 0x2d, 0xa3, 0xa0, 0xca, 0xaa, 0x4d,
+ 0xe0, 0x70, 0x39, 0x94, 0xb2, 0xa1, 0xf3, 0x57, 0xa6, 0x3f, 0x60, 0x56,
+ 0x7f, 0xc2, 0x80, 0x54, 0x1d, 0xff, 0x9b, 0x35, 0x71, 0x4a, 0xd6, 0xcd,
+ 0x54, 0xd0, 0xdb, 0x0b, 0xb2, 0xba, 0xda, 0xe1, 0xe6, 0xd1, 0x23, 0x2b,
+ 0x29, 0xc0, 0xdb, 0xa8, 0x63, 0x59, 0x8e, 0x84, 0xe1, 0x83, 0xa3, 0x73,
+ 0x9e, 0xd3, 0xb3, 0xb5, 0x70, 0x0e, 0xe7, 0xed, 0x44, 0xff, 0x25, 0x26,
+ 0xbf, 0x7c, 0xd5, 0xc7, 0xe2, 0xf7, 0xe0, 0x4c, 0xeb, 0x3f, 0xe1, 0x57,
+ 0xaa, 0xb5, 0x3f, 0x6c, 0xb4, 0x98, 0x42, 0xfd, 0xb5, 0x68, 0x96, 0xb3,
+ 0x46, 0x33, 0x84, 0xaa, 0x63, 0xe0, 0x0b, 0xb3, 0x5c, 0x5f, 0x2f, 0x1f,
+ 0xf1, 0x2f, 0xf9, 0x26, 0x7a, 0x66, 0x67, 0x0f, 0xa8, 0x09, 0x05, 0x28,
+ 0x3a, 0x51, 0x38, 0x0d, 0x92, 0x45, 0xba, 0xb7, 0xe2, 0x16, 0xad, 0xad,
+ 0x17, 0x16, 0x0f, 0x15, 0x8c, 0x12, 0xca, 0x21, 0x5d, 0xa7, 0x64, 0x1b,
+ 0xef, 0xf3, 0x55, 0x09, 0x26, 0xa6, 0x95, 0x8d, 0xa2, 0x69, 0x08, 0x42,
+ 0x0d, 0xa4, 0xc8, 0x74, 0xe9, 0xbc, 0x1f, 0x2f, 0x88, 0x99, 0x52, 0x9d,
+ 0xd5, 0x6d, 0x09, 0xc3, 0xf1, 0x8f, 0x1d, 0x7d, 0xd5, 0x48, 0x1f, 0x60,
+ 0x74, 0x25, 0xdf, 0xe6, 0x29, 0xf5, 0x19, 0x4f, 0xe3, 0x89, 0x03, 0x2c,
+ 0x58, 0xcd, 0x46, 0x63, 0xdd, 0xa8, 0x94, 0x86, 0x91, 0xb7, 0xb0, 0x8e,
+ 0x76, 0xd2, 0x3c, 0x87, 0x3d, 0xf7, 0xf2, 0xda, 0xe6, 0xcd, 0x70, 0xf3,
+ 0x5b, 0x43, 0x83, 0x7b, 0x5e, 0xb7, 0xcd, 0xde, 0xdb, 0x8d, 0x95, 0x91,
+ 0x14, 0x6d, 0xb5, 0xba, 0xb7, 0xae, 0x4a, 0xf1, 0x5b, 0xcd, 0x81, 0xbb,
+ 0xcd, 0x2c, 0x32, 0x4e, 0xdf, 0xac, 0x5a, 0x77, 0x7e, 0x5d, 0x8a, 0x0b,
+ 0x24, 0xe1, 0x32, 0x1e, 0x34, 0x6f, 0x48, 0xaa, 0x83, 0x6c, 0x1e, 0x94,
+ 0x76, 0x7c, 0xf7, 0xa0, 0xf7, 0xc1, 0xfb, 0xac, 0x8a, 0x56, 0xe7, 0xad,
+ 0x4a, 0xf3, 0xe4, 0xc8, 0xca, 0xfd, 0xab, 0xd6, 0xda, 0x4d, 0xb3, 0xe3,
+ 0xc9, 0xd2, 0xf9, 0x26, 0x45, 0x62, 0x84, 0xe9, 0xb6, 0xfe, 0xea, 0xbc,
+ 0xb1, 0xce, 0xf5, 0xb9, 0xe4, 0xe8, 0x9e, 0xc2, 0xed, 0x6d, 0x08, 0xeb,
+ 0x3b, 0x67, 0xcf, 0x3e, 0x69, 0x2a, 0x66, 0xf9, 0x52, 0xd5, 0x52, 0xf1,
+ 0x48, 0x6f, 0xa5, 0x9c, 0xe6, 0xc1, 0xd1, 0xbd, 0xff, 0xfd, 0x55, 0x03,
+ 0x31, 0xaa, 0x57, 0xae, 0x3f, 0x15, 0x29, 0x3d, 0xe2, 0x85, 0x76, 0x4d,
+ 0x57, 0xdc, 0x0b, 0x3c, 0x5b, 0xb5, 0xcb, 0x92, 0xcb, 0x8f, 0x1d, 0xf4,
+ 0xad, 0x2f, 0xc7, 0x54, 0xe2, 0xdc, 0xfa, 0xe1, 0xd7, 0xd1, 0xbf, 0x0d,
+ 0x1f, 0x69, 0x02, 0xdb, 0xb3, 0x74, 0xba, 0x4b, 0x5a, 0x96, 0xf8, 0xa8,
+ 0x2f, 0xfa, 0x5e, 0x18, 0xb7, 0xf5, 0x09, 0xbf, 0x44, 0xb4, 0xcc, 0xf7,
+ 0x57, 0xd9, 0x05, 0x6b, 0x7a, 0x57, 0x55, 0x59, 0xbe, 0x51, 0x96, 0x0c,
+ 0x2a, 0xc7, 0x59, 0xf9, 0x00, 0xad, 0xcd, 0xc6, 0x55, 0xee, 0x0b, 0xe2,
+ 0xfc, 0xf5, 0x8d, 0x3b, 0x9b, 0x8b, 0x19, 0x42, 0x7b, 0x1a, 0xba, 0x6c,
+ 0x99, 0x9a, 0xb4, 0x00, 0xee, 0x12, 0xf9, 0x3b, 0x98, 0x9a, 0x51, 0x9c,
+ 0xdc, 0xa7, 0xc5, 0x89, 0x76, 0xda, 0x16, 0x7c, 0xe7, 0x1e, 0xb3, 0xe5,
+ 0x9e, 0x20, 0x9f, 0xe7, 0x4c, 0xd9, 0x14, 0xef, 0x07, 0xcf, 0xb2, 0xb7,
+ 0x82, 0xec, 0xcf, 0x2e, 0xd6, 0x77, 0x91, 0x42, 0x2d, 0x38, 0xc4, 0xba,
+ 0x6b, 0x75, 0x26, 0x23, 0x85, 0xe6, 0x86, 0x37, 0x22, 0x26, 0xc5, 0x79,
+ 0xe9, 0x87, 0xf1, 0x43, 0xb6, 0xcf, 0xba, 0x79, 0xcd, 0x6e, 0x40, 0x77,
+ 0x2f, 0xec, 0x6b, 0x64, 0xba, 0x28, 0x61, 0x7c, 0xed, 0x60, 0x8c, 0x69,
+ 0x4a, 0xa8, 0xc9, 0x00, 0x59, 0xc5, 0x41, 0x8e, 0x87, 0x01, 0x2a, 0x1f,
+ 0x0c, 0xc4, 0x76, 0x30, 0x27, 0x56, 0xd3, 0x22, 0x86, 0x54, 0x6c, 0xc0,
+ 0xd8, 0x1c, 0x5b, 0x78, 0x34, 0xcc, 0xd2, 0x0f, 0x0f, 0x82, 0x80, 0x67,
+ 0x75, 0x09, 0x49, 0xaa, 0x14, 0xe6, 0x64, 0x89, 0xab, 0xd0, 0xeb, 0x7e,
+ 0x5a, 0x95, 0x80, 0xd7, 0x5a, 0x89, 0x32, 0x4d, 0xbd, 0x12, 0x3f, 0x94,
+ 0xbf, 0x13, 0xe7, 0x49, 0x22, 0x17, 0xc3, 0x02, 0xc3, 0xe1, 0x15, 0x46,
+ 0x68, 0xbc, 0xd6, 0x60, 0x89, 0x89, 0x23, 0x98, 0xd8, 0x03, 0xbf, 0x24,
+ 0x71, 0x0b, 0x00, 0xcb, 0x0e, 0x6c, 0xb2, 0x93, 0xcf, 0xf1, 0x3d, 0x1c,
+ 0xb3, 0xef, 0x86, 0xb5, 0xd7, 0x08, 0x4f, 0x43, 0x29, 0xb6, 0xf2, 0x0e,
+ 0x08, 0x2d, 0xb5, 0x4b, 0x0d, 0x74, 0xe5, 0xb6, 0x0d, 0x6c, 0xee, 0xf8,
+ 0xb4, 0x81, 0x00, 0x39, 0x53, 0x6a, 0x89, 0x75, 0x42, 0x0f, 0x78, 0x7f,
+ 0x2e, 0xcb, 0xe4, 0x63, 0x97, 0x30, 0x8b, 0x32, 0xad, 0xaa, 0xd8, 0xf9,
+ 0xc4, 0xe5, 0x82, 0x9b, 0x2b, 0xde, 0xc1, 0x2d, 0x82, 0x64, 0xed, 0x51,
+ 0x2a, 0x63, 0x66, 0xef, 0x18, 0x48, 0x94, 0x8d, 0xfc, 0x67, 0xc8, 0x1c,
+ 0x57, 0xe7, 0xef, 0x12, 0x2e, 0x88, 0xc4, 0xfe, 0x7e, 0xab, 0xd2, 0x74,
+ 0x5a, 0xa4, 0x73, 0xf4, 0x28, 0x9f, 0xc8, 0x78, 0x29, 0xc8, 0xce, 0xa7,
+ 0x86, 0xea, 0x22, 0xa3, 0xb9, 0x99, 0x19, 0x33, 0xd9, 0x89, 0xd6, 0xc0,
+ 0x20, 0x9f, 0x98, 0x72, 0x8e, 0x9b, 0xfe, 0x5b, 0xff, 0xbb, 0x61, 0x11,
+ 0x11, 0x04, 0x49, 0xe4, 0xb5, 0x3f, 0x5b, 0x53, 0xb7, 0x58, 0x61, 0x97,
+ 0x90, 0xc9, 0x9b, 0x08, 0xdf, 0xd5, 0x81, 0xad, 0x7a, 0x50, 0x57, 0xbf,
+ 0xba, 0x03, 0xfa, 0xef, 0x9e, 0x74, 0x52, 0xe7, 0x5f, 0x16, 0xab, 0xa3,
+ 0x94, 0xeb, 0x8b, 0x0a, 0xe6, 0x85, 0x16, 0xb1, 0x7e, 0x55, 0x10, 0x60,
+ 0x92, 0x6b, 0x00, 0x07, 0x1d, 0xc4, 0x32, 0x3c, 0x29, 0x7a, 0x78, 0xa7,
+ 0x58, 0x23, 0x79, 0xfd, 0x6c, 0x57, 0x9e, 0x63, 0xc6, 0x50, 0xe8, 0x7d,
+ 0x23, 0xe8, 0xc7, 0x8a, 0x41, 0x94, 0x58, 0x0c, 0xf0, 0x33, 0x75, 0x0a,
+ 0x20, 0x45, 0x74, 0xcd, 0x5d, 0x33, 0x9c, 0x4d, 0x97, 0x6c, 0xbe, 0xee,
+ 0xdb, 0xb5, 0x8b, 0x61, 0x69, 0xfd, 0xd4, 0xa1, 0x2a, 0x52, 0x7e, 0x08,
+ 0x36, 0x4b, 0xfa, 0xe9, 0xf4, 0x5b, 0x3d, 0x0c, 0x2f, 0x4e, 0x06, 0xb8,
+ 0xfe, 0x96, 0x6d, 0xf5, 0x9c, 0x4e, 0x45, 0xa6, 0x68, 0x5b, 0x78, 0xf0,
+ 0x1d, 0x2e, 0x83, 0x59, 0x35, 0xc0, 0x99, 0x66, 0x98, 0x85, 0xe4, 0xc3,
+ 0x33, 0xac, 0x61, 0x9d, 0x9f, 0xe1, 0x14, 0x4c, 0x78, 0xf3, 0xff, 0x0a,
+ 0x97, 0xf7, 0x64, 0xb4, 0x0c, 0x93, 0x0c, 0x71, 0xe9, 0x01, 0x90, 0xae,
+ 0x18, 0xe3, 0x4a, 0xc3, 0x04, 0x9e, 0xce, 0x9d, 0x10, 0x32, 0x42, 0x54,
+ 0xdb, 0xbd, 0x40, 0x72, 0x89, 0xc6, 0x32, 0xda, 0x08, 0x82, 0xea, 0x41,
+ 0xe2, 0x4a, 0x40, 0x50, 0x41, 0xaf, 0x90, 0x81, 0x94, 0x9f, 0xac, 0x9a,
+ 0xa0, 0x3b, 0xc2, 0x62, 0x38, 0x40, 0xe0, 0xc6, 0x7a, 0x56, 0x6e, 0x69,
+ 0xb6, 0xe5, 0xdc, 0xd6, 0x24, 0x77, 0x7c, 0xf6, 0x38, 0xfe, 0x3c, 0x98,
+ 0xcc, 0xe1, 0x95, 0x87, 0x68, 0xc3, 0x76, 0x5a, 0x9e, 0x89, 0xb6, 0x9f,
+ 0xf3, 0xd6, 0x3c, 0x1f, 0x88, 0xcc, 0x59, 0x4b, 0x9e, 0x09, 0x31, 0xff,
+ 0xa3, 0x12, 0xa3, 0xa9, 0x22, 0x65, 0xaf, 0xc9, 0xd3, 0x58, 0x79, 0xa3,
+ 0x0c, 0x7e, 0x82, 0xa2, 0x83, 0xd2, 0xa1, 0x34, 0x5b, 0x54, 0x17, 0x36,
+ 0x72, 0x5b, 0x73, 0x64, 0x36, 0x3e, 0x4d, 0xb8, 0xfd, 0x0d, 0x13, 0xa0,
+ 0x8a, 0xd0, 0x36, 0x87, 0x63, 0x82, 0x98, 0x99, 0x3e, 0x49, 0x09, 0xb1,
+ 0x55, 0x0d, 0x26, 0x6a, 0xda, 0x9c, 0x5a, 0x10, 0x0f, 0xdd, 0xa7, 0xdf,
+ 0xaa, 0x0b, 0x1c, 0xa1, 0xee, 0x6e, 0xe1, 0xd3, 0xff, 0x82, 0xeb, 0x26,
+ 0xa3, 0xbc, 0x33, 0x71, 0xb5, 0xb7, 0xfe, 0x30, 0xd6, 0x28, 0x65, 0x01,
+ 0xc2, 0x6a, 0x36, 0xa2, 0x76, 0x69, 0x53, 0x19, 0x43, 0x1f, 0xdf, 0x4d,
+ 0x55, 0x1b, 0xf2, 0xa1, 0xdb, 0x3f, 0xbc, 0x73, 0x05, 0x82, 0xb1, 0xab,
+ 0xca, 0x04, 0x99, 0xb9, 0x93, 0x3f, 0x19, 0x6e, 0x30, 0xef, 0x87, 0xc9,
+ 0xbf, 0x78, 0x38, 0x83, 0xf9, 0x5c, 0x60, 0xc1, 0x84, 0xc7, 0x2b, 0xcb,
+ 0xf4, 0x3d, 0x16, 0xf3, 0x5b, 0xc2, 0xf4, 0x07, 0x2f, 0xf6, 0xe4, 0x6a,
+ 0x3c, 0x4a, 0xc3, 0x23, 0x5f, 0x1d, 0x6d, 0xb1, 0xe9, 0x2b, 0xa7, 0xf7,
+ 0x91, 0x44, 0xca, 0x0a, 0xce, 0xa1, 0x59, 0xf8, 0x7f, 0x08, 0x17, 0x66,
+ 0xbc, 0xa7, 0x1b, 0x4c, 0x41, 0x50, 0xee, 0x4f, 0xbc, 0xed, 0x50, 0xbd,
+ 0xf6, 0xfa, 0xa1, 0xd2, 0x81, 0x23, 0x9e, 0x9e, 0x4a, 0x78, 0xa1, 0xe1,
+ 0xe9, 0xf1, 0xac, 0xe4, 0xb9, 0x1c, 0x71, 0x75, 0x5c, 0xdd, 0x28, 0x22,
+ 0x21, 0x86, 0xaa, 0xcf, 0x2f, 0xed, 0x02, 0x78, 0x53, 0xa2, 0xf4, 0x7c,
+ 0x49, 0x46, 0xa0, 0xe0, 0x04, 0x14, 0x3a, 0xac, 0x48, 0x38, 0xc1, 0x7a,
+ 0x8f, 0x22, 0x13, 0xb5, 0x90, 0x24, 0x51, 0xe6, 0xe5, 0x44, 0x77, 0xe8,
+ 0xbe, 0x6f, 0xb4, 0xe2, 0xa0, 0xa4, 0x38, 0xad, 0x3a, 0xb8, 0x21, 0xbc,
+ 0x0e, 0x69, 0x2f, 0xc7, 0xfe, 0x53, 0x5a, 0x06, 0xb7, 0x88, 0x42, 0xbd,
+ 0xbe, 0x8d, 0x1f, 0x1d, 0xe4, 0xf3, 0xda, 0xa1, 0xcd, 0x03, 0x3e, 0x33,
+ 0x6a, 0x6d, 0x31, 0x35, 0xbf, 0x5f, 0xb3, 0x0d, 0x6f, 0xf8, 0x7c, 0x16,
+ 0xde, 0xa8, 0xcb, 0x69, 0xf3, 0x91, 0x09, 0xb5, 0xe2, 0x9d, 0x7b, 0xdf,
+ 0x23, 0x10, 0xb4, 0xd3, 0x40, 0x1c, 0xdc, 0x33, 0x90, 0x05, 0x5a, 0x9b,
+ 0x83, 0xd8, 0x8e, 0x49, 0xd7, 0x1e, 0x91, 0xf8, 0x1c, 0xbd, 0xcf, 0x01,
+ 0x54, 0xb6, 0x4f, 0x01, 0x86, 0x90, 0xda, 0x68, 0x41, 0x72, 0xea, 0x2e,
+ 0xe3, 0xb1, 0x44, 0x06, 0x19, 0x9d, 0xc2, 0x7c, 0x4d, 0x86, 0xe9, 0x0d,
+ 0x71, 0x5f, 0x63, 0xc2, 0xdd, 0x78, 0x7f, 0x4e, 0x6f, 0x16, 0x11, 0x3d,
+ 0x35, 0xd9, 0xb4, 0xf0, 0xb4, 0xa7, 0x88, 0xc4, 0x7d, 0x80, 0x04, 0x96,
+ 0x1b, 0xae, 0x78, 0x0a, 0x3d, 0x8f, 0xe4, 0x14, 0x31, 0xa0, 0x2f, 0x6c,
+ 0xac, 0xd6, 0x06, 0xf4, 0x03, 0x44, 0xfb, 0xaa, 0x3d, 0xa9, 0x7e, 0xf1,
+ 0xf2, 0x86, 0x3d, 0x58, 0xdc, 0xb4, 0xba, 0x67, 0xf8, 0xc6, 0x71, 0x0d,
+ 0xf0, 0x1a, 0xfe, 0x5d, 0x9b, 0x9d, 0x48, 0x91, 0x27, 0x44, 0xd2, 0x3c,
+ 0x2b, 0x1d, 0x91, 0xb5, 0x46, 0x3e, 0x39, 0x0d, 0xb5, 0xe8, 0x36, 0xeb,
+ 0xe3, 0xf2, 0xb6, 0x8a, 0xe6, 0xb2, 0xbd, 0x6d, 0xa3, 0xe0, 0x3f, 0xf9,
+ 0xf3, 0xef, 0xe6, 0xc8, 0xd1, 0x88, 0x48, 0xda, 0xf5, 0xd4, 0x3d, 0x16,
+ 0xee, 0x95, 0x28, 0xee, 0x79, 0x4d, 0xe1, 0xbb, 0xb0, 0x96, 0x57, 0xea,
+ 0x27, 0xfb, 0x95, 0x31, 0x49, 0x14, 0xba, 0xc3, 0xa5, 0xd2, 0xf9, 0x60,
+ 0xf4, 0xdd, 0x95, 0x78, 0x38, 0xa4, 0x5f, 0x44, 0x49, 0xc6, 0x7d, 0x24,
+ 0x63, 0x30, 0x49, 0x83, 0x07, 0x2d, 0xb6, 0xa8, 0xd7, 0x35, 0x36, 0xa1,
+ 0x10, 0xfd, 0xda, 0xa8, 0x62, 0x99, 0x18, 0xec, 0x73, 0x4a, 0x03, 0x91,
+ 0x82, 0x8d, 0xf8, 0x1c, 0xa4, 0x9d, 0xd5, 0x57, 0x45, 0x79, 0xd1, 0xaa,
+ 0xbf, 0x24, 0xba, 0xeb, 0x8b, 0x18, 0x6f, 0x42, 0x7f, 0x34, 0x14, 0xc9,
+ 0x07, 0x7e, 0xfe, 0x0a, 0xc9, 0x27, 0x28, 0x7e, 0x31, 0x03, 0x9e, 0xb7,
+ 0x13, 0xbb, 0x3f, 0x95, 0xec, 0x6b, 0x81, 0x60, 0xa6, 0x52, 0x3a, 0xf2,
+ 0x13, 0xb4, 0x4d, 0xbb, 0xd2, 0x54, 0x60, 0xfb, 0x74, 0x9f, 0x2d, 0xd6,
+ 0xaa, 0xf3, 0x8b, 0xfa, 0x5f, 0xb2, 0x88, 0x64, 0x77, 0x17, 0x0a, 0xc6,
+ 0xa4, 0x47, 0x2a, 0xe2, 0xf3, 0x50, 0x72, 0xba, 0x21, 0x2d, 0xbf, 0xe2,
+ 0xf7, 0x23, 0xd3, 0x27, 0x2d, 0x5f, 0xf6, 0x00, 0x44, 0xfe, 0xe6, 0xc4,
+ 0xfb, 0x1f, 0xb1, 0x58, 0x7c, 0x2c, 0x80, 0x2e, 0x34, 0x01, 0x6f, 0xce,
+ 0xd4, 0x3b, 0x70, 0x40, 0x5e, 0x77, 0x56, 0x17, 0xac, 0xb8, 0xd7, 0xe3,
+ 0x5d, 0x94, 0x06, 0x0c, 0x85, 0xbd, 0x6e, 0xf1, 0xd7, 0x0d, 0xc7, 0x5d,
+ 0x13, 0x9e, 0xed, 0x57, 0xd3, 0x85, 0xcb, 0x5c, 0x4d, 0x44, 0x1c, 0xe8,
+ 0x05, 0xcc, 0xd0, 0x74, 0xad, 0x83, 0x6e, 0x30, 0x27, 0x71, 0x4b, 0xe6,
+ 0x38, 0x09, 0x8a, 0x6b, 0xb6, 0xd2, 0x1a, 0xb8, 0xb4, 0xc0, 0x54, 0xb3,
+ 0x18, 0x80, 0xf2, 0x77, 0x25, 0xf5, 0x68, 0x09, 0x6e, 0x2e, 0x17, 0x39,
+ 0x9d, 0xda, 0x22, 0x1a, 0xce, 0xae, 0x40, 0xf3, 0x0a, 0x88, 0xbc, 0xf4,
+ 0x8b, 0x7d, 0x4a, 0x9f, 0xec, 0x9a, 0x1a, 0xb8, 0xd7, 0x5e, 0x03, 0x56,
+ 0x4d, 0xf7, 0x03, 0x19, 0x9b, 0x9c, 0xaf, 0x8c, 0x5e, 0xf9, 0x99, 0x21,
+ 0xa2, 0xb9, 0xa7, 0x46, 0x34, 0x0b, 0x16, 0x47, 0x8d, 0xfa, 0xca, 0x91,
+ 0x49, 0x2e, 0x5f, 0x58, 0xf5, 0x81, 0xd4, 0xf9, 0x4b, 0x68, 0x45, 0x1b,
+ 0xdb, 0x67, 0xd5, 0x6a, 0x85, 0xc8, 0xb9, 0x71, 0x53, 0x31, 0x45, 0xd8,
+ 0x5f, 0x70, 0xc1, 0xce, 0x4a, 0xd0, 0x51, 0x43, 0xd3, 0x95, 0xe7, 0x69,
+ 0x6b, 0xcc, 0x19, 0x2c, 0xf7, 0xc2, 0x89, 0x7a, 0x50, 0x41, 0x65, 0x15,
+ 0x09, 0x2a, 0x76, 0x2d, 0x12, 0xb8, 0xd9, 0x7b, 0x33, 0x9f, 0xb1, 0xaf,
+ 0x81, 0xa5, 0xc8, 0x53, 0xc4, 0x8f, 0xe8, 0x89, 0x80, 0xef, 0x93, 0x66,
+ 0xb7, 0x15, 0x74, 0x4a, 0xf9, 0x41, 0x3d, 0x95, 0x75, 0x92, 0xa2, 0x16,
+ 0x76, 0xe0, 0xa3, 0x5d, 0x55, 0x9b, 0x3b, 0x6d, 0x0d, 0x06, 0x62, 0x46,
+ 0x11, 0xd6, 0xe5, 0xeb, 0x6d, 0x93, 0x62, 0x11, 0x5c, 0xcb, 0x22, 0x86,
+ 0x4d, 0x68, 0x0b, 0xa2, 0x63, 0x74, 0x67, 0x79, 0x6d, 0x66, 0x2d, 0x51,
+ 0x8e, 0xd5, 0xaa, 0x45, 0xc2, 0x34, 0x0f, 0xad, 0x4c, 0xef, 0xa5, 0x90,
+ 0x94, 0x19, 0x4d, 0x48, 0xf9, 0xb5, 0x92, 0xa7, 0xdb, 0x37, 0xc8, 0x6f,
+ 0x24, 0x85, 0xf3, 0xc4, 0xd9, 0x26, 0x2c, 0x64, 0x7f, 0xa8, 0x86, 0x47,
+ 0x46, 0x35, 0xc2, 0xdf, 0x54, 0x42, 0x52, 0x1f, 0x43, 0x43, 0xcb, 0x50,
+ 0x1b, 0xbf, 0xf6, 0xd8, 0x11, 0x7d, 0x31, 0x4c, 0xc3, 0x2d, 0x70, 0xb3,
+ 0xa0, 0x6b, 0x6d, 0xcc, 0x88, 0xc3, 0xa7, 0x59, 0xe0, 0x2f, 0xaa, 0xdb,
+ 0x98, 0x9d, 0x9f, 0x7b, 0x28, 0x79, 0x0b, 0xa7, 0x7a, 0x90, 0x9f, 0x11,
+ 0x3a, 0xca, 0x3a, 0xc5, 0xd1, 0x6f, 0x0b, 0x90, 0xe9, 0x7a, 0x9d, 0x72,
+ 0x7a, 0xb5, 0x84, 0xf3, 0x46, 0xf2, 0x61, 0x96, 0xda, 0xf3, 0x3a, 0x72,
+ 0x50, 0x67, 0xc0, 0xe7, 0xe3, 0x4c, 0x82, 0xb6, 0x38, 0x72, 0x02, 0x05,
+ 0xcd, 0x8e, 0xe6, 0xf1, 0x9b, 0x21, 0x00, 0x4c, 0xfe, 0x78, 0xf6, 0x73,
+ 0x49, 0x95, 0x8b, 0x41, 0x5e, 0x30, 0xf8, 0x1e, 0xc2, 0x96, 0x3b, 0x4f,
+ 0xfd, 0x9a, 0x10, 0xc2, 0x1f, 0x54, 0x14, 0x7e, 0x14, 0x4d, 0x69, 0xd9,
+ 0xc8, 0xa3, 0xfa, 0x4a, 0x7d, 0x02, 0x0e, 0x32, 0x88, 0x02, 0x22, 0x31,
+ 0x6d, 0x13, 0xab, 0xd1, 0x1a, 0xcf, 0x80, 0xc7, 0x11, 0xbc, 0xa7, 0xd7,
+ 0x73, 0xc1, 0xc5, 0x15, 0x52, 0x7e, 0x82, 0xe7, 0x4f, 0x84, 0xc8, 0x50,
+ 0xa8, 0xb3, 0xb4, 0x70, 0x5e, 0x95, 0x71, 0x48, 0x31, 0x26, 0xc9, 0x91,
+ 0xb4, 0x53, 0xcc, 0x82, 0xd0, 0xf0, 0xf9, 0x84, 0x1f, 0xff, 0x49, 0x08,
+ 0x40, 0xfe, 0x39, 0x3c, 0x64, 0x72, 0x1c, 0x8b, 0x9f, 0x02, 0xc9, 0x27,
+ 0x23, 0xda, 0x4b, 0x86, 0x00, 0x4d, 0x84, 0x63, 0x23, 0x58, 0x60, 0x80,
+ 0x2a, 0x2a, 0xa8, 0xf0, 0x70, 0x86, 0x2c, 0xf6, 0x4a, 0xcc, 0x4f, 0xa5,
+ 0x9e, 0x98, 0x31, 0x90, 0xa6, 0x70, 0x6a, 0xd1, 0x31, 0xd5, 0x10, 0x06,
+ 0x63, 0xc6, 0x2e, 0x70, 0x22, 0x79, 0x7f, 0x21, 0x71, 0x5a, 0x01, 0xef,
+ 0x3f, 0x15, 0x2f, 0xed, 0x8c, 0x29, 0x01, 0x11, 0x52, 0xa2, 0xc2, 0xa3,
+ 0x6b, 0xdf, 0xcc, 0x6a, 0xae, 0x8b, 0x57, 0x11, 0x22, 0x5c, 0xa8, 0x90,
+ 0x93, 0xa2, 0x68, 0x85, 0x66, 0xcc, 0x9f, 0xa8, 0x13, 0x65, 0x41, 0x6c,
+ 0xee, 0x6f, 0xba, 0xc9, 0x0a, 0xa6, 0x0b, 0x5a, 0x5f, 0x72, 0x49, 0x05,
+ 0x55, 0xcf, 0xb7, 0x81, 0x16, 0xd5, 0xf2, 0xdb, 0x2e, 0x74, 0xd1, 0x40,
+ 0x7f, 0xf8, 0x96, 0xc5, 0xe4, 0x29, 0x4f, 0xc4, 0x0e, 0x1b, 0x9c, 0x86,
+ 0xf9, 0xfd, 0x0b, 0x4e, 0x64, 0x8b, 0xdc, 0x9a, 0xef, 0x59, 0xdb, 0x28,
+ 0x7f, 0x7f, 0xda, 0x16, 0x59, 0x0b, 0x2e, 0x42, 0x65, 0xe4, 0x0f, 0xbb,
+ 0xbe, 0xe6, 0x09, 0x3d, 0xa3, 0x95, 0xc9, 0x38, 0xa3, 0xb6, 0x4e, 0x07,
+ 0x9c, 0x7e, 0xe8, 0xe4, 0x77, 0x04, 0xa9, 0xdd, 0x75, 0x3f, 0x46, 0x90,
+ 0xe0, 0xc8, 0x25, 0x0d, 0x4b, 0x44, 0xc5, 0x7a, 0x03, 0x73, 0xd6, 0x9e,
+ 0xd9, 0x40, 0x10, 0x99, 0x85, 0x2b, 0x09, 0xa5, 0xa1, 0xff, 0xff, 0x96,
+ 0x83, 0xb2, 0x22, 0x18, 0xb5, 0x97, 0x9b, 0xd8, 0x87, 0xe4, 0xdb, 0x63,
+ 0xa2, 0x34, 0x65, 0xaa, 0x8f, 0x19, 0x61, 0x28, 0x0a, 0x1f, 0xda, 0x61,
+ 0xda, 0x6e, 0x93, 0x42, 0xff, 0xdd, 0x72, 0xad, 0x4c, 0x57, 0x0f, 0xc2,
+ 0x50, 0x75, 0xb3, 0x32, 0x7a, 0x86, 0x14, 0x7a, 0xbb, 0xac, 0xdb, 0x01,
+ 0x67, 0x7b, 0x24, 0x97, 0xbd, 0xee, 0xf0, 0x75, 0x2f, 0x5d, 0xff, 0x4f,
+ 0x66, 0x0c, 0x65, 0x36, 0x12, 0x15, 0x77, 0x13, 0xfe, 0x10, 0x35, 0x6d,
+ 0x93, 0x2b, 0x55, 0x94, 0xbe, 0xe3, 0x75, 0xc6, 0xba, 0xe0, 0x57, 0x09,
+ 0xf5, 0x46, 0x4e, 0x75, 0x8d, 0xb5, 0x54, 0x5b, 0x41, 0xa4, 0x19, 0xb0,
+ 0xbb, 0xda, 0x9f, 0xb6, 0xed, 0xc6, 0x10, 0x62, 0xb6, 0xfb, 0x6e, 0xf4,
+ 0xef, 0x5d, 0xec, 0x66, 0xcb, 0xee, 0x15, 0x41, 0x22, 0x2a, 0x8e, 0x3c,
+ 0xca, 0x74, 0xe1, 0x41, 0x64, 0xaf, 0x53, 0x03, 0xbb, 0xa3, 0x80, 0xbe,
+ 0xc6, 0x1b, 0x72, 0x42, 0x32, 0x9a, 0xc6, 0xe6, 0x6f, 0x72, 0x64, 0x4c,
+ 0x11, 0xdb, 0x59, 0xa3, 0x57, 0x49, 0x92, 0x63, 0x9c, 0x1f, 0x16, 0x9e,
+ 0xf7, 0x17, 0x43, 0x66, 0x06, 0x41, 0x8b, 0x0d, 0xbe, 0x29, 0xfe, 0xec,
+ 0x51, 0x80, 0xc6, 0x0c, 0xbc, 0xf8, 0xa7, 0x0d, 0x99, 0xee, 0x05, 0x54,
+ 0x02, 0x9f, 0x89, 0x37, 0xa5, 0x1c, 0x91, 0x34, 0xac, 0xc6, 0x0b, 0x2e,
+ 0x06, 0x0c, 0x96, 0xd8, 0xaa, 0x76, 0x73, 0x8b, 0x62, 0xe7, 0x83, 0x92,
+ 0xbd, 0x65, 0x5a, 0x46, 0x78, 0x01, 0x11, 0xbd, 0x7a, 0x43, 0xd9, 0xce,
+ 0x01, 0x3b, 0x7b, 0x79, 0x3b, 0xb4, 0x42, 0x1f, 0xc1, 0x2d, 0x00, 0x2b,
+ 0xa9, 0x5a, 0xad, 0xf8, 0x74, 0xc8, 0xba, 0xdd, 0x5a, 0x4f, 0x85, 0x8a,
+ 0x34, 0xd4, 0xad, 0x60, 0xd2, 0xfb, 0x3f, 0x03, 0xf3, 0x52, 0x57, 0x08,
+ 0x16, 0x1e, 0x4d, 0x92, 0x38, 0xfb, 0x07, 0x40, 0x68, 0xef, 0x8b, 0xba,
+ 0xd6, 0xbd, 0xc8, 0xbc, 0x1f, 0x2a, 0xab, 0x82, 0x16, 0x01, 0x44, 0x77,
+ 0x76, 0x54, 0xfa, 0x90, 0xd4, 0x11, 0x4c, 0xb1, 0x9c, 0x5f, 0xc6, 0xce,
+ 0xf5, 0x3e, 0x31, 0x7a, 0x23, 0x55, 0x69, 0xc1, 0x13, 0x7e, 0x47, 0x69,
+ 0xa0, 0x68, 0x1f, 0xf6, 0x3b, 0x01, 0xbb, 0x2c, 0xce, 0xb1, 0x8d, 0x6e,
+ 0x06, 0x9d, 0xe4, 0x8c, 0xe2, 0xe3, 0xc9, 0xd4, 0x33, 0x68, 0x68, 0x7c,
+ 0xfd, 0x4d, 0xcd, 0x0f, 0x6d, 0xd5, 0xfe, 0x93, 0x5a, 0x1c, 0x48, 0x6a,
+ 0x84, 0x63, 0x95, 0x3c, 0xaf, 0x89, 0x65, 0xee, 0x74, 0x51, 0xcb, 0xad,
+ 0x0b, 0xc2, 0xb6, 0xb2, 0x22, 0xb3, 0xcb, 0x0f, 0x79, 0xe8, 0xa9, 0x85,
+ 0x46, 0x9f, 0x13, 0x6d, 0x5b, 0xc5, 0x50, 0xe9, 0xed, 0xa3, 0x91, 0xaa,
+ 0x0c, 0x2f, 0xad, 0xac, 0x64, 0x53, 0xb3, 0x8b, 0x0c, 0xf0, 0xa5, 0xf0,
+ 0x7c, 0x1b, 0x8a, 0x3c, 0xce, 0x7a, 0x40, 0x84, 0x1b, 0x32, 0x6f, 0x10,
+ 0xde, 0x3e, 0x15, 0x8b, 0xea, 0xb3, 0x67, 0xee, 0x50, 0xef, 0x17, 0xff,
+ 0x16, 0x71, 0xbc, 0xdc, 0x89, 0xb4, 0x95, 0xb8, 0x4b, 0x87, 0xd4, 0x5f,
+ 0xa8, 0x99, 0xca, 0xed, 0x1f, 0xd9, 0xdb, 0x87, 0x8a, 0x02, 0xa5, 0x45,
+ 0xb6, 0x51, 0xe1, 0xce, 0x8a, 0x1c, 0xc2, 0xb4, 0xd3, 0xe0, 0xdb, 0xb5,
+ 0x37, 0x88, 0xd7, 0x95, 0xd1, 0x5b, 0x59, 0x4d, 0xef, 0x86, 0x39, 0xf8,
+ 0xb1, 0xed, 0x9d, 0xaf, 0xfa, 0x7e, 0x2e, 0x11, 0x11, 0xde, 0x75, 0x03,
+ 0xd5, 0x0e, 0x92, 0xad, 0x75, 0xbb, 0xfa, 0x38, 0xc3, 0xef, 0x74, 0x71,
+ 0xe6, 0x96, 0x33, 0xd7, 0x40, 0xd2, 0x4c, 0x06, 0xee, 0x4a, 0x2b, 0x54,
+ 0x2d, 0xd4, 0x77, 0xe4, 0xfc, 0x27, 0x80, 0x83, 0xbe, 0x33, 0xbb, 0xd9,
+ 0x29, 0x7a, 0x45, 0x43, 0xc2, 0x54, 0x79, 0xcd, 0x33, 0x45, 0xe2, 0x1c,
+ 0xda, 0x91, 0x3e, 0x56, 0xbb, 0x7e, 0x56, 0x3b, 0xa3, 0x2a, 0xc3, 0x5a,
+ 0x1e, 0x5f, 0x03, 0x27, 0xaa, 0x10, 0x22, 0x62, 0x31, 0x1c, 0x31, 0x66,
+ 0x3f, 0x52, 0xc1, 0xa7, 0x1f, 0x0e, 0x4d, 0xe8, 0xc0, 0x9a, 0x2f, 0x2b,
+ 0x55, 0x0b, 0xa6, 0x60, 0x7c, 0xc2, 0x57, 0xa7, 0x3e, 0x94, 0x38, 0x0f,
+ 0x94, 0x52, 0x06, 0x5d, 0xfc, 0x5b, 0xc4, 0x75, 0x22, 0x00, 0x2e, 0x16,
+ 0x8c, 0x75, 0x0e, 0x07, 0x0e, 0x99, 0x2e, 0x2b, 0x73, 0xf2, 0x89, 0x7e,
+ 0xe4, 0xb4, 0x96, 0x5e, 0x84, 0x86, 0x71, 0x32, 0xc5, 0x0a, 0x3a, 0x3f,
+ 0xc1, 0x51, 0xac, 0x3d, 0xb3, 0x93, 0x7a, 0x53, 0x10, 0x74, 0x99, 0x02,
+ 0xa1, 0x86, 0xe0, 0xad, 0xd1, 0xf0, 0x1f, 0xb1, 0x67, 0xa1, 0xac, 0xc1,
+ 0x87, 0x79, 0xb9, 0x4e, 0x2a, 0x74, 0xc7, 0x54, 0xcc, 0x49, 0x28, 0x71,
+ 0x04, 0xad, 0xec, 0xae, 0xf6, 0xb0, 0x4b, 0x37, 0x3f, 0xa3, 0x5e, 0x52,
+ 0x74, 0x00, 0xa2, 0x72, 0x21, 0x45, 0x54, 0xb6, 0xb4, 0x9c, 0x96, 0xbd,
+ 0x31, 0x9e, 0x28, 0xf6, 0x6f, 0xb5, 0x61, 0xa4, 0x00, 0x51, 0x22, 0xbb,
+ 0xe2, 0x3a, 0x4d, 0xab, 0x3c, 0x7e, 0x40, 0x95, 0x81, 0x97, 0x1d, 0xbe,
+ 0x60, 0xb0, 0xe5, 0x7b, 0xf0, 0x63, 0xd3, 0x4e, 0x49, 0x72, 0x07, 0xa9,
+ 0x19, 0xb4, 0x4f, 0x12, 0x44, 0x21, 0xe2, 0x3f, 0x85, 0x27, 0x1b, 0xe2,
+ 0xc6, 0x8c, 0xc7, 0x1a, 0x2e, 0xc1, 0x6e, 0x05, 0x1d, 0xc4, 0x69, 0x76,
+ 0xb3, 0x96, 0xe0, 0xa7, 0x4c, 0x70, 0xeb, 0x2b, 0x47, 0x38, 0x54, 0x40,
+ 0x60, 0x02, 0x55, 0x3a, 0xb6, 0xa1, 0xac, 0x96, 0xae, 0xa8, 0x7f, 0x11,
+ 0x45, 0xa5, 0x43, 0x61, 0x47, 0x86, 0x80, 0x27, 0x5f, 0xf3, 0x53, 0x9b,
+ 0x91, 0x25, 0x02, 0x4d, 0xaf, 0x3b, 0x85, 0x11, 0x55, 0xf6, 0x74, 0xf4,
+ 0xc3, 0xd5, 0xdd, 0xc0, 0x05, 0x14, 0xd8, 0xd1, 0x90, 0xc8, 0x7e, 0x55,
+ 0xf7, 0xb3, 0x53, 0x2d, 0x87, 0x72, 0x66, 0x29, 0x46, 0x22, 0x92, 0xdc,
+ 0xd7, 0x88, 0x47, 0xce, 0x9a, 0xc9, 0x8c, 0x12, 0xc3, 0xd6, 0x71, 0xf9,
+ 0xf9, 0xb7, 0x5d, 0xb9, 0xcd, 0x01, 0x5e, 0x83, 0x6f, 0x67, 0xf4, 0x40,
+ 0xac, 0xd9, 0xfa, 0x35, 0x72, 0x0c, 0x97, 0x83, 0xa3, 0x3d, 0x10, 0xca,
+ 0xbf, 0xd2, 0xa6, 0xc9, 0x17, 0xf5, 0x59, 0xef, 0xb0, 0xb0, 0x1d, 0x8d,
+ 0x9c, 0x00, 0x83, 0x25, 0x43, 0x41, 0x1c, 0x43, 0x1b, 0xa0, 0x30, 0xb6,
+ 0xee, 0x3f, 0xf2, 0x70, 0x04, 0x28, 0xb0, 0x20, 0xab, 0x45, 0x65, 0x9a,
+ 0x49, 0x4f, 0x4d, 0x5c, 0x4f, 0x2a, 0x11, 0x64, 0x2c, 0xa9, 0x39, 0x36,
+ 0xb7, 0xe2, 0x5d, 0x1d, 0xa4, 0x61, 0x88, 0x24, 0xf5, 0x52, 0xde, 0x0f,
+ 0x1b, 0x35, 0x32, 0xb7, 0xde, 0x18, 0x11, 0x52, 0x23, 0x90, 0x3c, 0x49,
+ 0xc2, 0xd1, 0x93, 0x73, 0x98, 0x64, 0x00, 0x6a, 0x37, 0x90, 0xe7, 0xb8,
+ 0x40, 0x7e, 0xc0, 0x2f, 0xbb, 0x55, 0x5e, 0xf1, 0xf6, 0x25, 0x22, 0x5b,
+ 0x93, 0xb8, 0xe2, 0x29, 0x8a, 0x81, 0xa8, 0x6d, 0xcc, 0xcf, 0xe5, 0xc1,
+ 0x7e, 0x1a, 0xc3, 0xe7, 0xd7, 0xb8, 0xd3, 0x88, 0xb1, 0xcb, 0x71, 0x87,
+ 0x5e, 0x28, 0xf5, 0x19, 0x4c, 0xd5, 0xe8, 0xc8, 0xa8, 0x77, 0x57, 0x02,
+ 0x9d, 0x4b, 0x97, 0x52, 0xba, 0x6a, 0x3a, 0xa1, 0x5e, 0x50, 0x89, 0xdc,
+ 0x4f, 0xfd, 0xdf, 0x62, 0xae, 0xd7, 0x88, 0xc6, 0x37, 0xda, 0x61, 0x89,
+ 0x68, 0x19, 0x05, 0xf6, 0xa6, 0xad, 0xfc, 0x1a, 0xcd, 0xbf, 0x0e, 0x49,
+ 0x28, 0xf7, 0xd3, 0x29, 0xc2, 0xd7, 0xfc, 0x81, 0x85, 0xad, 0xa8, 0x40,
+ 0x87, 0xd8, 0xed, 0xd1, 0xce, 0x5f, 0xa4, 0xfd, 0x32, 0xb2, 0x8e, 0xa4,
+ 0xd6, 0xd8, 0x99, 0x4f, 0x78, 0x89, 0xc4, 0x64, 0x92, 0x61, 0x18, 0xd3,
+ 0x77, 0x76, 0xab, 0xce, 0x93, 0x42, 0x86, 0x9b, 0xce, 0x36, 0x1d, 0x13,
+ 0x05, 0x0a, 0x27, 0x86, 0xf1, 0xaf, 0xee, 0xa1, 0x73, 0xcb, 0xae, 0x14,
+ 0x9f, 0x10, 0x1d, 0xc5, 0x4d, 0x37, 0x09, 0xd4, 0x7a, 0x30, 0x34, 0xbe,
+ 0x84, 0x94, 0xf6, 0x7b, 0x16, 0xac, 0xef, 0x25, 0x33, 0x4a, 0x89, 0x86,
+ 0xed, 0x65, 0x27, 0xf0, 0xc0, 0x82, 0x3d, 0x90, 0x33, 0x69, 0x9c, 0x73,
+ 0x7d, 0x69, 0xa0, 0xaf, 0x85, 0xaf, 0xc3, 0x00, 0x19, 0xa0, 0xb0, 0xf1,
+ 0x3b, 0xea, 0x1a, 0x64, 0xbe, 0xf1, 0x30, 0xb2, 0xd7, 0x26, 0xcb, 0x98,
+ 0xaa, 0x04, 0x45, 0x6b, 0x16, 0xb6, 0x69, 0x90, 0x50, 0xd0, 0x05, 0xfa,
+ 0xad, 0xcf, 0x77, 0xf5, 0xb2, 0x96, 0xfa, 0xb8, 0x47, 0x8f, 0x23, 0xc2,
+ 0x2c, 0x8f, 0x94, 0xfb, 0x40, 0xf8, 0x65, 0x68, 0xc7, 0x80, 0x2e, 0x93,
+ 0x46, 0xa7, 0xaa, 0xbb, 0xae, 0x6f, 0x52, 0x03, 0x77, 0x6b, 0xdf, 0x82,
+ 0x6c, 0x9a, 0xb2, 0xf7, 0xd4, 0x90, 0xbf, 0x3c, 0x35, 0x4b, 0xbd, 0xe5,
+ 0xd5, 0x2f, 0x63, 0x82, 0x2b, 0xf0, 0xc5, 0xb8, 0x90, 0xb8, 0x81, 0xa1,
+ 0x71, 0x96, 0x66, 0x29, 0x3d, 0xeb, 0x8c, 0xc8, 0x24, 0x69, 0xc8, 0xb2,
+ 0x91, 0xdf, 0xc4, 0x52, 0xfa, 0x30, 0xcf, 0xa8, 0x39, 0x1d, 0x7b, 0x09,
+ 0xd5, 0xe1, 0xb3, 0x6e, 0x21, 0xa8, 0x08, 0x8e, 0x70, 0xd9, 0xb3, 0xc7,
+ 0x6d, 0x6a, 0x53, 0x9b, 0xf8, 0xbc, 0x9e, 0x8f, 0x6c, 0xa3, 0x84, 0x24,
+ 0xae, 0x70, 0x5b, 0x12, 0xb5, 0x8c, 0xb4, 0xf1, 0x75, 0xae, 0x5a, 0x24,
+ 0xe0, 0x08, 0x5d, 0xe8, 0x7e, 0xa9, 0xf4, 0xd8, 0x25, 0x72, 0xf2, 0xca,
+ 0x6f, 0x87, 0x38, 0xc7, 0x1b, 0xca, 0x3c, 0x82, 0xc7, 0x1b, 0xa1, 0xcb,
+ 0xf1, 0x94, 0xe4, 0x6c, 0xdd, 0x1c, 0x51, 0x6e, 0x4c, 0x06, 0x8e, 0x49,
+ 0x50, 0x29, 0x84, 0x17, 0xe7, 0xdc, 0x69, 0xdf, 0xb6, 0x7c, 0xe4, 0xc0,
+ 0xa2, 0xef, 0x1d, 0x6c, 0x84, 0x4c, 0x06, 0xc4, 0x0e, 0xda, 0xe7, 0x46,
+ 0xd3, 0xd4, 0x94, 0x22, 0x11, 0xc9, 0x07, 0xed, 0xf0, 0xfd, 0x06, 0x7e,
+ 0x3b, 0x52, 0x3a, 0x2b, 0x97, 0x6a, 0xb9, 0x5a, 0xff, 0xb2, 0xa6, 0xe3,
+ 0x8b, 0x48, 0xde, 0x07, 0xe0, 0xa0, 0x8d, 0xd5, 0x27, 0x61, 0x98, 0x32,
+ 0x23, 0x85, 0x30, 0xbc, 0xb6, 0x40, 0x94, 0xe2, 0x7f, 0xd8, 0xbf, 0x43,
+ 0x74, 0xa9, 0x3c, 0x0d, 0xc9, 0x57, 0x4f, 0x9a, 0x44, 0xa2, 0xd6, 0x97,
+ 0x82, 0x09, 0xff, 0x0d, 0x83, 0xae, 0x1c, 0xcc, 0xab, 0x93, 0xe6, 0xcf,
+ 0xcb, 0x77, 0x35, 0xe8, 0xc2, 0x93, 0xdc, 0xa5, 0x29, 0x34, 0x26, 0x54,
+ 0x40, 0x5e, 0x0c, 0x46, 0xfb, 0x16, 0x9b, 0x1e, 0xb8, 0x3e, 0xb7, 0xce,
+ 0xe5, 0x16, 0x2b, 0x58, 0x9c, 0x60, 0x06, 0xfd, 0x0c, 0x40, 0x51, 0x63,
+ 0xf5, 0x19, 0x2f, 0xfa, 0x1e, 0x3a, 0xc6, 0x5b, 0x52, 0x71, 0xe7, 0x52,
+ 0x55, 0x42, 0x64, 0x6b, 0x49, 0x75, 0x2b, 0x63, 0xca, 0x4a, 0xbc, 0x51,
+ 0xd8, 0x59, 0x17, 0x14, 0xed, 0xe1, 0xed, 0x7b, 0xbf, 0x59, 0x89, 0x6a,
+ 0x06, 0x3b, 0x0b, 0xe4, 0xec, 0xcd, 0x4f, 0x48, 0x52, 0x41, 0x6f, 0xf6,
+ 0x75, 0x7e, 0xd9, 0xb2, 0x4e, 0x52, 0xde, 0xa2, 0xb2, 0xb4, 0xdf, 0x1a,
+ 0x70, 0x39, 0x2a, 0x0b, 0xc6, 0x26, 0xa1, 0xc5, 0x62, 0x4b, 0xff, 0x8f,
+ 0x11, 0xf1, 0x67, 0x03, 0x03, 0x51, 0x8c, 0xd9, 0x8a, 0x22, 0x8b, 0x51,
+ 0x52, 0x86, 0x2c, 0x3a, 0xb5, 0x57, 0x25, 0xc0, 0xea, 0x7c, 0x65, 0x7a,
+ 0xc2, 0xea, 0x37, 0x37, 0x38, 0xfe, 0x91, 0x50, 0xda, 0x1e, 0xc9, 0x3b,
+ 0x91, 0x2b, 0x29, 0x79, 0x67, 0x07, 0x5f, 0xf1, 0x74, 0x20, 0x21, 0x63,
+ 0xe0, 0xdf, 0xa7, 0x71, 0xdb, 0xe9, 0x7e, 0x15, 0x65, 0x04, 0xf9, 0xa2,
+ 0x64, 0xcd, 0x79, 0x79, 0x75, 0x33, 0x1a, 0x79, 0x3a, 0xe3, 0xf3, 0xa8,
+ 0x57, 0x65, 0xec, 0xe0, 0xea, 0x08, 0x5a, 0x53, 0x25, 0x7a, 0x93, 0x7b,
+ 0x51, 0x0a, 0x52, 0xac, 0x1b, 0x40, 0x38, 0xfa, 0x77, 0xb6, 0xa2, 0x3f,
+ 0xb1, 0x79, 0x69, 0xd8, 0xd2, 0x9e, 0x3d, 0x21, 0x54, 0x0c, 0xdf, 0x25,
+ 0xb3, 0xa2, 0xc7, 0x05, 0x78, 0x9d, 0x7f, 0xb1, 0x5e, 0xb5, 0x9e, 0x28,
+ 0x38, 0x3b, 0x7f, 0x13, 0xa7, 0xd9, 0x58, 0xf0, 0x1c, 0x04, 0xa4, 0x36,
+ 0x59, 0x78, 0xa9, 0x49, 0x46, 0xbd, 0xb8, 0x5c, 0x69, 0xef, 0x20, 0x54,
+ 0xad, 0x1c, 0xdd, 0x5c, 0x64, 0xf3, 0xbc, 0x07, 0xd1, 0xba, 0x9d, 0x2e,
+ 0xe1, 0xae, 0x31, 0x71, 0xb6, 0xc8, 0x16, 0xbd, 0x80, 0x77, 0x4c, 0x3c,
+ 0xb7, 0xd8, 0x4d, 0x5d, 0x4a, 0x58, 0x62, 0x7a, 0xb1, 0x80, 0x5b, 0x49,
+ 0x82, 0x42, 0x37, 0xe0, 0xc1, 0xbe, 0x9e, 0x71, 0x98, 0x3a, 0x9e, 0x88,
+ 0x61, 0x5b, 0x37, 0xb4, 0x98, 0x4d, 0x71, 0x15, 0x64, 0x36, 0xb3, 0xd4,
+ 0x2d, 0x5b, 0x80, 0x17, 0xae, 0x33, 0xa7, 0x65, 0xe1, 0xc4, 0x82, 0x3e,
+ 0x5e, 0xba, 0x56, 0x8f, 0x06, 0x33, 0x54, 0xee, 0xa3, 0x69, 0xf9, 0x2e,
+ 0xe0, 0xfe, 0x91, 0x7f, 0xfc, 0xfb, 0x57, 0xf7, 0x3a, 0x2c, 0xf4, 0x02,
+ 0x1b, 0xfc, 0x04, 0x50, 0x13, 0x19, 0x1c, 0x3c, 0x30, 0x66, 0xa5, 0x7f,
+ 0xb8, 0x8d, 0x53, 0xb6, 0x20, 0x9e, 0x24, 0x7d, 0xa4, 0x72, 0xa4, 0x17,
+ 0x23, 0x1e, 0xe7, 0x4e, 0x4b, 0xe8, 0x51, 0x82, 0x8d, 0x2a, 0xcd, 0x49,
+ 0x11, 0xe5, 0x64, 0xeb, 0x05, 0xc1, 0x78, 0xe4, 0xac, 0x9a, 0x41, 0x41,
+ 0xcb, 0x63, 0x5e, 0x23, 0xf1, 0xfb, 0x9a, 0xdc, 0x29, 0xcc, 0xae, 0xf5,
+ 0x21, 0x71, 0x77, 0xea, 0xa0, 0x53, 0x4b, 0xd9, 0x2b, 0x51, 0xc0, 0x9d,
+ 0x88, 0xaf, 0xed, 0x0b, 0x81, 0x17, 0x79, 0xbb, 0x4a, 0xb6, 0xa4, 0x97,
+ 0x39, 0x22, 0xcf, 0x72, 0xa6, 0xbd, 0x02, 0x15, 0xe0, 0xae, 0x2f, 0x5c,
+ 0xfc, 0x25, 0x60, 0xc3, 0xcd, 0x05, 0x6d, 0x4d, 0x4e, 0x06, 0x2d, 0xae,
+ 0x94, 0xfc, 0x2b, 0x9a, 0xec, 0x30, 0x4e, 0x56, 0x5c, 0xc6, 0xf7, 0x02,
+ 0x6d, 0x85, 0x26, 0x9a, 0xb6, 0x86, 0xcf, 0x64, 0xf6, 0xec, 0x6c, 0x4f,
+ 0x7d, 0x94, 0x0f, 0xd4, 0x1b, 0x38, 0x77, 0x2f, 0x45, 0x8c, 0x67, 0xce,
+ 0xc6, 0xd3, 0xa0, 0x59, 0x18, 0x4e, 0x27, 0x81, 0x12, 0x73, 0x4c, 0x23,
+ 0x12, 0x84, 0x4e, 0xe6, 0x4a, 0x45, 0x08, 0x4b, 0xba, 0x8b, 0x99, 0x8a,
+ 0xf8, 0xca, 0x0b, 0x9f, 0x78, 0xec, 0x3c, 0x51, 0xd7, 0x27, 0x83, 0x4d,
+ 0x05, 0x7c, 0x99, 0xa3, 0xe6, 0x38, 0x05, 0x20, 0x92, 0x7a, 0x67, 0x31,
+ 0xb5, 0xa7, 0x28, 0x83, 0xb2, 0x24, 0xa3, 0xa8, 0xab, 0xfb, 0xc2, 0xdf,
+ 0x2e, 0x03, 0xee, 0xd0, 0x50, 0x8e, 0x74, 0x4a, 0xcd, 0x0d, 0x29, 0x57,
+ 0x60, 0x8c, 0x07, 0xe1, 0x22, 0x9b, 0x11, 0xa1, 0xed, 0x92, 0x9c, 0x8d,
+ 0x2e, 0x5a, 0xd8, 0x7c, 0xbf, 0xfa, 0xb9, 0x96, 0x06, 0xa7, 0x05, 0xc5,
+ 0x01, 0x74, 0x2d, 0x1a, 0x22, 0x38, 0x11, 0x4f, 0x4d, 0xc2, 0xba, 0xf6,
+ 0x4b, 0x21, 0x2a, 0xca, 0xa4, 0x7f, 0xa9, 0xfe, 0xe3, 0xc8, 0x63, 0x28,
+ 0xa3, 0x6f, 0xc8, 0xb5, 0xeb, 0xe6, 0xcf, 0x8e, 0xd6, 0x21, 0x60, 0x4e,
+ 0x36, 0x64, 0xcc, 0x44, 0x03, 0xc9, 0x77, 0xb5, 0x19, 0x9a, 0x16, 0x98,
+ 0x98, 0x1d, 0x38, 0x8b, 0x54, 0xc7, 0x3e, 0xd5, 0xe7, 0x97, 0x63, 0x6b,
+ 0x29, 0xf3, 0x54, 0xd3, 0xe3, 0xfd, 0x30, 0x4f, 0xb0, 0xeb, 0x00, 0xfa,
+ 0x8e, 0xe3, 0x58, 0xa6, 0x14, 0xa0, 0x1b, 0x0c, 0x36, 0x03, 0x84, 0x6f,
+ 0x68, 0xe5, 0x3a, 0xb3, 0xa9, 0x97, 0xc6, 0xe9, 0xf0, 0x1d, 0x56, 0x20,
+ 0x49, 0x1c, 0x62, 0x01, 0x54, 0x10, 0x63, 0x31, 0xfc, 0x9d, 0xf6, 0xba,
+ 0x27, 0xfe, 0x57, 0xd2, 0x24, 0x21, 0x26, 0xbb, 0x0c, 0x66, 0x60, 0x7c,
+ 0x31, 0x88, 0x79, 0x67, 0xdb, 0x54, 0x31, 0x81, 0xc2, 0x69, 0xd1, 0xec,
+ 0x0c, 0xb9, 0x2e, 0x86, 0x20, 0x19, 0x22, 0x67, 0xfe, 0xf4, 0x83, 0x27,
+ 0x9c, 0x79, 0xa6, 0x0d, 0x58, 0xe5, 0xbe, 0x79, 0xb3, 0x11, 0x93, 0xc6,
+ 0x7e, 0x74, 0xb6, 0xcb, 0x71, 0x02, 0x0c, 0x52, 0x73, 0xff, 0x59, 0xed,
+ 0xc9, 0x84, 0xf4, 0x38, 0x6a, 0xf0, 0x77, 0x52, 0x57, 0x3d, 0x54, 0xb2,
+ 0xf0, 0xd3, 0x75, 0xf4, 0x4e, 0x58, 0x18, 0x15, 0x68, 0xee, 0x34, 0xe7,
+ 0xd9, 0xa5, 0xab, 0xbe, 0x95, 0x36, 0x46, 0xb5, 0x2d, 0xe3, 0x47, 0x99,
+ 0xfb, 0x54, 0x86, 0x5a, 0xca, 0x52, 0x12, 0x52, 0x18, 0x60, 0x08, 0x21,
+ 0x80, 0xde, 0xdd, 0x6b, 0xaa, 0xea, 0x2e, 0x6d, 0x70, 0x80, 0xd5, 0x01,
+ 0x44, 0xc9, 0x49, 0xa1, 0xeb, 0x04, 0x7c, 0xba, 0xa0, 0x4a, 0xfb, 0x86,
+ 0x48, 0x54, 0x88, 0x45, 0x73, 0x60, 0x3a, 0x7c, 0x6c, 0xd7, 0xc3, 0xaf,
+ 0x92, 0x2d, 0x97, 0x41, 0xc7, 0x8d, 0xab, 0x7a, 0xa2, 0xa9, 0x3f, 0xe4,
+ 0xe9, 0x16, 0xb9, 0x23, 0x44, 0x32, 0xe6, 0x4f, 0xd6, 0x39, 0x79, 0x78,
+ 0xeb, 0x26, 0xc6, 0x84, 0x28, 0x3e, 0xcc, 0x81, 0x8a, 0xaa, 0xec, 0xc2,
+ 0x05, 0x5c, 0x9d, 0x22, 0xd9, 0x6f, 0x9a, 0xa2, 0x32, 0xad, 0x20, 0x77,
+ 0x75, 0xf8, 0x8a, 0x85, 0x22, 0x4a, 0xe9, 0xa0, 0x5f, 0x87, 0x81, 0x95,
+ 0x2f, 0x61, 0xff, 0x8a, 0x86, 0x0d, 0x12, 0x89, 0x62, 0xab, 0x36, 0xf9,
+ 0x8c, 0x05, 0x6f, 0x6e, 0x23, 0x2c, 0xb3, 0x7c, 0x7c, 0xe1, 0x30, 0x9d,
+ 0x07, 0x21, 0x41, 0xd6, 0xef, 0x35, 0xc2, 0x08, 0x2e, 0xf7, 0x2f, 0x9b,
+ 0xb0, 0xad, 0x36, 0x2f, 0xd4, 0x8d, 0x65, 0xe1, 0xba, 0x26, 0x28, 0x80,
+ 0xfe, 0x52, 0x27, 0x67, 0x15, 0x43, 0x27, 0x3e, 0x8f, 0x54, 0xf3, 0x79,
+ 0x7b, 0x39, 0xec, 0x34, 0x97, 0x6b, 0xb3, 0xbf, 0xb6, 0x03, 0xba, 0xfa,
+ 0x0a, 0x9d, 0xbe, 0x6d, 0x70, 0x79, 0x33, 0x0e, 0x91, 0x46, 0x3f, 0xa3,
+ 0xa1, 0x24, 0x97, 0x32, 0xbe, 0xf3, 0x4d, 0xba, 0x8e, 0xa4, 0x2d, 0x0b,
+ 0xeb, 0x67, 0xa6, 0x11, 0x5c, 0xfa, 0xfe, 0x0e, 0xaf, 0x17, 0xc2, 0x74,
+ 0x0e, 0x45, 0x39, 0xdb, 0xdb, 0xf7, 0xae, 0xcf, 0xc0, 0x75, 0xe7, 0xcc,
+ 0x12, 0x2e, 0x1a, 0xb2, 0x0d, 0xe5, 0x5d, 0xe8, 0x2d, 0x52, 0x67, 0x44,
+ 0xab, 0x2e, 0x13, 0x6b, 0xb7, 0x0c, 0xa7, 0xbe, 0x65, 0x08, 0xc5, 0x74,
+ 0xba, 0xf1, 0x45, 0xac, 0x2d, 0xe6, 0xc2, 0x33, 0x5e, 0xf0, 0xa0, 0x49,
+ 0xf6, 0xd0, 0xdc, 0x02, 0x0b, 0x0a, 0x73, 0x03, 0x16, 0x30, 0xfc, 0xb4,
+ 0xad, 0xe5, 0xac, 0x9e, 0x18, 0xae, 0x79, 0xee, 0xb4, 0xf1, 0x30, 0xcd,
+ 0x76, 0xba, 0x36, 0x8e, 0xfe, 0xce, 0x0d, 0x83, 0xb7, 0xb7, 0x87, 0x36,
+ 0xb0, 0x17, 0x64, 0xf2, 0xa1, 0x52, 0xdc, 0x13, 0xd0, 0xd6, 0xd9, 0xd2,
+ 0xf8, 0xa1, 0x81, 0xf0, 0x96, 0x8f, 0x69, 0xe5, 0x8e, 0x1f, 0x66, 0xcc,
+ 0xac, 0xe6, 0x65, 0x28, 0x55, 0x32, 0x03, 0xfa, 0xa1, 0x69, 0xeb, 0x6f,
+ 0xbf, 0x2e, 0xff, 0xfe, 0x5b, 0xaf, 0x8e, 0x44, 0x6d, 0x10, 0x17, 0xfd,
+ 0x14, 0x67, 0xd9, 0x5d, 0xe3, 0xc7, 0x84, 0xd6, 0xe3, 0x1e, 0x11, 0x4f,
+ 0xa9, 0x10, 0x48, 0x2c, 0x7b, 0x04, 0x6a, 0x78, 0x36, 0x2d, 0xfb, 0xe0,
+ 0x1f, 0x39, 0x94, 0x67, 0xf2, 0xb5, 0xf0, 0xcf, 0xef, 0x2d, 0x2e, 0xe8,
+ 0x50, 0x1f, 0xab, 0x72, 0xb4, 0xf0, 0x62, 0xd5, 0xca, 0xf4, 0x69, 0xa0,
+ 0x79, 0x62, 0x9f, 0xff, 0x97, 0xa2, 0xc7, 0xd0, 0x6b, 0xd1, 0x78, 0x51,
+ 0x75, 0x95, 0x23, 0x33, 0xf9, 0x41, 0xc4, 0x46, 0xea, 0x91, 0x01, 0x0c,
+ 0x66, 0xa3, 0x78, 0x17, 0x6d, 0xc1, 0x32, 0x38, 0x4e, 0x16, 0x49, 0x5d,
+ 0x34, 0x86, 0x02, 0x85, 0xc7, 0x88, 0xca, 0x26, 0x97, 0xbd, 0xe5, 0xd8,
+ 0xd9, 0xb4, 0xc6, 0xbd, 0xfa, 0x59, 0xaf, 0x09, 0xae, 0x2e, 0x0d, 0x1d,
+ 0xc0, 0x10, 0x5a, 0xe7, 0x6b, 0x87, 0xab, 0xcc, 0x7c, 0xc1, 0x57, 0x83,
+ 0x61, 0xe9, 0x25, 0x07, 0xc9, 0x54, 0x6c, 0x95, 0x76, 0xca, 0x84, 0xbc,
+ 0x21, 0x26, 0x38, 0x1f, 0x14, 0x6c, 0x29, 0xbc, 0xd1, 0xf9, 0xc0, 0x4d,
+ 0xb2, 0xd0, 0x5b, 0x28, 0x75, 0x09, 0x9b, 0x08, 0x1b, 0x73, 0xe3, 0xa7,
+ 0x4d, 0xd8, 0x4d, 0x6b, 0x57, 0xec, 0x0c, 0xd0, 0x9a, 0xd0, 0xd1, 0xca,
+ 0x0c, 0xcf, 0x49, 0xc3, 0x19, 0x49, 0x0a, 0x51, 0x01, 0x98, 0x6e, 0x82,
+ 0x1f, 0x1e, 0x17, 0x06, 0xec, 0x8f, 0xc3, 0xf8, 0xc0, 0x0d, 0xbd, 0x5c,
+ 0xcc, 0x17, 0x74, 0x87, 0x2b, 0x2a, 0x4a, 0xce, 0x62, 0xc4, 0x0c, 0xa2,
+ 0x8e, 0x84, 0x44, 0xdd, 0x84, 0x31, 0x36, 0xfe, 0x1d, 0x31, 0xc8, 0x91,
+ 0xe2, 0x4c, 0x1f, 0xca, 0xa4, 0xca, 0x01, 0x6a, 0x41, 0x14, 0x91, 0x5d,
+ 0xd5, 0x2c, 0xb2, 0x31, 0xd1, 0x3d, 0xff, 0xde, 0xce, 0x96, 0x86, 0x29,
+ 0xad, 0xea, 0x4e, 0x95, 0x4e, 0xed, 0x75, 0x7f, 0xa9, 0xb1, 0xbe, 0xc2,
+ 0x13, 0xb7, 0x31, 0xc9, 0x84, 0x95, 0xb6, 0x1f, 0x92, 0x70, 0x69, 0x43,
+ 0x4a, 0x1a, 0xdd, 0x2e, 0x11, 0x38, 0xa8, 0xb1, 0x09, 0x76, 0x65, 0xe9,
+ 0x0c, 0xe2, 0xe5, 0xb3, 0x43, 0x2d, 0x7c, 0xbe, 0xb9, 0x04, 0xb6, 0x84,
+ 0x5d, 0x6b, 0x77, 0x6f, 0x5a, 0x21, 0x2c, 0x93, 0x2f, 0xb8, 0x35, 0x1a,
+ 0xf6, 0x46, 0xb1, 0x95, 0x73, 0x17, 0x59, 0xc8, 0x02, 0xe3, 0x31, 0xbd,
+ 0x43, 0x35, 0x99, 0x89, 0x20, 0xf2, 0xc4, 0x80, 0x0f, 0x73, 0x23, 0x1b,
+ 0x4f, 0xeb, 0xc4, 0x8a, 0x8b, 0xd3, 0x25, 0x6b, 0x87, 0xcc, 0x43, 0x6b,
+ 0x92, 0x65, 0xfb, 0xb5, 0x1f, 0x5e, 0xf5, 0x7b, 0x54, 0x7d, 0xc5, 0x77,
+ 0xef, 0x7c, 0xb1, 0xfe, 0x50, 0x6e, 0xa4, 0x0d, 0xc6, 0xc7, 0x2c, 0x4d,
+ 0xd2, 0xba, 0x55, 0x60, 0xeb, 0x3f, 0xe3, 0xbf, 0x6e, 0x13, 0xea, 0x84,
+ 0xd5, 0x41, 0x43, 0x29, 0x6c, 0xf2, 0x2c, 0xbb, 0xa4, 0x89, 0x4a, 0xf3,
+ 0xc0, 0x2b, 0x82, 0x50, 0x4b, 0x51, 0x5b, 0x95, 0x77, 0x60, 0xf4, 0xd6,
+ 0x7a, 0x15, 0x37, 0x38, 0xe7, 0x8b, 0x93, 0xce, 0xd0, 0x1b, 0x60, 0x3b,
+ 0x86, 0xfa, 0x91, 0xf2, 0x87, 0xfa, 0x08, 0xb0, 0x38, 0x99, 0x53, 0xf0,
+ 0x76, 0x42, 0x87, 0x9f, 0x66, 0xd6, 0xa4, 0xfd, 0x9e, 0x86, 0xa8, 0xf7,
+ 0xf2, 0xa1, 0xaa, 0xa2, 0x48, 0x5f, 0x79, 0x21, 0x21, 0xb7, 0xa1, 0x00,
+ 0x48, 0x2e, 0x33, 0x7e, 0x82, 0x05, 0x1c, 0x86, 0x22, 0x68, 0xa6, 0x05,
+ 0xeb, 0xba, 0xad, 0xf6, 0x39, 0xbd, 0x52, 0x6e, 0x6c, 0x5f, 0x0e, 0x7f,
+ 0xeb, 0x62, 0xfb, 0x1d, 0x6c, 0xfd, 0xa1, 0xd9, 0xf5, 0x55, 0xfc, 0xe9,
+ 0x06, 0xcd, 0xda, 0x85, 0xc2, 0x5c, 0xb2, 0x4e, 0x3a, 0xf4, 0xdf, 0xc6,
+ 0x7f, 0x54, 0xfb, 0xdd, 0x21, 0x26, 0x44, 0x9f, 0x3b, 0x88, 0x8c, 0xff,
+ 0x25, 0x1c, 0xb3, 0x16, 0x55, 0xcd, 0x51, 0x77, 0x9c, 0x08, 0x76, 0xe3,
+ 0xa9, 0x2f, 0xa0, 0x99, 0xc4, 0x31, 0x53, 0xc4, 0x77, 0xab, 0x7f, 0x79,
+ 0x82, 0xb0, 0x5a, 0x93, 0x88, 0xde, 0xbd, 0x3e, 0x04, 0xa1, 0xe6, 0x60,
+ 0xc5, 0xe7, 0xae, 0x89, 0x42, 0x71, 0x6d, 0x30, 0x91, 0x89, 0xe0, 0x26,
+ 0x57, 0x0a, 0x2d, 0x50, 0x83, 0xe7, 0x9d, 0x77, 0xc5, 0xe3, 0x4c, 0x4b,
+ 0x7a, 0x5b, 0x9e, 0xab, 0x26, 0x12, 0x24, 0xa6, 0xd9, 0x91, 0xee, 0x31,
+ 0x6b, 0x3c, 0x06, 0x28, 0x25, 0x4f, 0xe3, 0x0b, 0xbd, 0xe3, 0x13, 0xcb,
+ 0x97, 0x0c, 0x20, 0xba, 0x8a, 0xbd, 0x02, 0xff, 0xe0, 0xc6, 0x5c, 0xa6,
+ 0x3c, 0x4f, 0xd6, 0xdd, 0x45, 0x82, 0x35, 0x35, 0x46, 0xca, 0xf9, 0x67,
+ 0xb7, 0x47, 0x43, 0x9a, 0x5f, 0x32, 0x3a, 0xa9, 0x83, 0x86, 0xa0, 0xc8,
+ 0x35, 0x06, 0xc2, 0x36, 0x9d, 0xed, 0x39, 0x56, 0xa5, 0x10, 0xcb, 0x39,
+ 0x0d, 0xa7, 0x65, 0x0b, 0x77, 0x34, 0x98, 0xee, 0xe5, 0xee, 0xc1, 0x17,
+ 0x35, 0x46, 0xcd, 0x8a, 0xf6, 0xed, 0x42, 0x4a, 0x9e, 0x8a, 0xfe, 0x41,
+ 0x00, 0x4e, 0x77, 0x8c, 0x53, 0xd6, 0xb1, 0x9b, 0x08, 0xde, 0x57, 0xf5,
+ 0xa0, 0x92, 0x09, 0xba, 0x1b, 0x45, 0xb3, 0x11, 0x4a, 0xc4, 0xc8, 0x68,
+ 0xb3, 0x75, 0x4c, 0xd6, 0x3f, 0x40, 0xbd, 0xc4, 0x05, 0xd5, 0x00, 0xea,
+ 0xb6, 0xc1, 0x38, 0x77, 0x51, 0xea, 0xab, 0x4b, 0xf1, 0x2b, 0x27, 0x68,
+ 0xfb, 0x95, 0xe4, 0xf2, 0xee, 0xe0, 0x07, 0xa2, 0x10, 0xa6, 0xfa, 0xfa,
+ 0xd3, 0xc7, 0xf3, 0x7f, 0x4b, 0xc1, 0xed, 0x51, 0xac, 0x2a, 0xaa, 0x2b,
+ 0x4e, 0x20, 0x76, 0x02, 0x52, 0x1f, 0x70, 0x58, 0x06, 0x97, 0x10, 0x59,
+ 0x96, 0x36, 0x40, 0xa4, 0x1d, 0x90, 0xf6, 0xfa, 0x92, 0x1b, 0xf5, 0x85,
+ 0x18, 0xdf, 0x2b, 0xfe, 0xa6, 0x90, 0xc3, 0x44, 0xce, 0xd9, 0x53, 0xb3,
+ 0x4e, 0x56, 0x92, 0x7d, 0x3d, 0x05, 0x3e, 0x16, 0x42, 0x81, 0x0e, 0x3d,
+ 0xcc, 0xb7, 0x7e, 0x34, 0x4a, 0xe6, 0x6d, 0x79, 0x4d, 0x3d, 0x9b, 0xbd,
+ 0x8b, 0xb3, 0xdc, 0xe7, 0xb5, 0xa7, 0x67, 0xe8, 0xf8, 0x7e, 0x27, 0x27,
+ 0x9c, 0xa3, 0xfe, 0x0d, 0x76, 0x02, 0x54, 0x98, 0x4f, 0x7a, 0x98, 0x96,
+ 0x21, 0xbe, 0xc7, 0x76, 0xce, 0xcf, 0x64, 0x99, 0x0b, 0x44, 0x8b, 0x53,
+ 0x85, 0x59, 0x91, 0x46, 0x80, 0x2a, 0x49, 0x6c, 0xa6, 0x7f, 0x4d, 0x12,
+ 0xdb, 0xe8, 0x19, 0x0e, 0x92, 0xa8, 0xab, 0x60, 0x59, 0x4a, 0x16, 0x38,
+ 0xfb, 0xd7, 0xd5, 0xb6, 0x60, 0xa4, 0xc6, 0x6f, 0x1a, 0xc9, 0x7f, 0x3b,
+ 0x6b, 0x4d, 0xff, 0x7e, 0xb1, 0x6a, 0x6a, 0x52, 0x42, 0xe2, 0x0a, 0x60,
+ 0xe8, 0x9c, 0xf1, 0x96, 0x5c, 0x1b, 0x5f, 0x07, 0xb9, 0x6f, 0x22, 0x4c,
+ 0x68, 0x0e, 0x4f, 0x43, 0xe2, 0xcf, 0x4c, 0x11, 0x88, 0xed, 0x2c, 0x4a,
+ 0xc1, 0x40, 0x17, 0xa4, 0xf2, 0xc1, 0xaa, 0x0e, 0x24, 0x13, 0x3b, 0xbc,
+ 0x3a, 0x6a, 0xe0, 0xe0, 0x5f, 0x03, 0x2e, 0x9c, 0x1e, 0x0d, 0xc0, 0x8c,
+ 0xa7, 0x30, 0x52, 0x95, 0xb2, 0x22, 0xb5, 0x7b, 0xb4, 0x6b, 0x01, 0x9b,
+ 0x3d, 0xbc, 0xd6, 0x79, 0xa1, 0x28, 0x58, 0x76, 0x97, 0xec, 0xae, 0xf6,
+ 0x65, 0x20, 0x01, 0xc0, 0x5e, 0xe2, 0x4d, 0x21, 0xcf, 0x3a, 0x1e, 0x7b,
+ 0x05, 0x04, 0x62, 0xe3, 0x72, 0x6c, 0x54, 0x31, 0x52, 0x41, 0xa1, 0x74,
+ 0x4b, 0x65, 0xd0, 0xef, 0x05, 0xcf, 0x65, 0x1f, 0x6c, 0x98, 0x89, 0xb1,
+ 0xff, 0xad, 0xe7, 0x37, 0x6d, 0xee, 0x10, 0xf3, 0x62, 0x1a, 0xf5, 0x96,
+ 0x1a, 0x41, 0x40, 0x7d, 0x33, 0x50, 0x04, 0xa2, 0x25, 0xb7, 0xf3, 0x16,
+ 0x6f, 0xae, 0x9f, 0xb8, 0x51, 0x4f, 0x38, 0xa3, 0x0c, 0xf0, 0x0a, 0x53,
+ 0x2f, 0x19, 0x88, 0x67, 0xac, 0x31, 0x7f, 0xdd, 0x82, 0x01, 0xd8, 0xca,
+ 0xc1, 0xe2, 0x32, 0xcb, 0x74, 0xed, 0x24, 0x3b, 0x33, 0xa9, 0xa1, 0xe2,
+ 0xfc, 0x1f, 0x8b, 0x6f, 0xaf, 0x4c, 0x67, 0x96, 0xce, 0x0f, 0x03, 0xd9,
+ 0x75, 0xcb, 0x8f, 0xad, 0xe3, 0xa2, 0xa6, 0xd9, 0x74, 0xb6, 0x99, 0xef,
+ 0x24, 0x75, 0x76, 0x34, 0xc7, 0x91, 0xc6, 0xb5, 0x8c, 0xaf, 0xe6, 0x28,
+ 0xfe, 0x06, 0x6e, 0x0b, 0xc9, 0x04, 0x92, 0xaf, 0x71, 0xa2, 0x9c, 0x31,
+ 0xbf, 0x8f, 0x6f, 0xd1, 0xcc, 0x00, 0xdd, 0x97, 0x9f, 0xb8, 0x8d, 0x4b,
+ 0x97, 0x8a, 0xda, 0x66, 0x65, 0x10, 0x20, 0xcb, 0x46, 0xc1, 0x6f, 0xdc,
+ 0x75, 0x69, 0x5d, 0x74, 0x5b, 0x3f, 0x70, 0xf8, 0xa2, 0xff, 0xf7, 0xba,
+ 0x3e, 0xfa, 0x13, 0x7c, 0xf3, 0x27, 0x74, 0x3e, 0x5c, 0x6b, 0x4d, 0x14,
+ 0xd4, 0x61, 0xbe, 0xe1, 0x20, 0xed, 0xfa, 0x43, 0x68, 0xa5, 0xcc, 0xdd,
+ 0x8b, 0x82, 0x21, 0xa0, 0x21, 0x4d, 0x5a, 0x2a, 0x90, 0xae, 0x33, 0x39,
+ 0x3b, 0xac, 0xb6, 0x85, 0xec, 0x28, 0x1c, 0xc8, 0x3a, 0xad, 0x0b, 0x67,
+ 0xa9, 0xd1, 0xc7, 0x29, 0x6c, 0x54, 0x94, 0x03, 0x5e, 0x90, 0x3a, 0x06,
+ 0xdd, 0xbb, 0x4b, 0x4d, 0xdb, 0x06, 0x8b, 0x98, 0x3c, 0xf2, 0x54, 0x25,
+ 0xc6, 0x29, 0x21, 0x0e, 0x87, 0xc3, 0xe8, 0x0c, 0x51, 0x79, 0x48, 0x77,
+ 0x7a, 0x61, 0x16, 0x7d, 0xca, 0x36, 0x24, 0xe8, 0xca, 0xab, 0xc6, 0x67,
+ 0x95, 0xa7, 0x8f, 0xc3, 0x69, 0xc3, 0x51, 0x94, 0x4e, 0x7c, 0x5a, 0x41,
+ 0x61, 0x1d, 0xf2, 0x6a, 0x14, 0x0a, 0xda, 0x90, 0xba, 0x96, 0x67, 0xa2,
+ 0x31, 0x02, 0xc9, 0xfa, 0x43, 0xb5, 0x51, 0xeb, 0x75, 0x42, 0xa4, 0x7e,
+ 0x12, 0x93, 0x85, 0xad, 0x62, 0xb2, 0x95, 0x93, 0xba, 0x8a, 0x8e, 0x90,
+ 0x53, 0x66, 0x0b, 0x4f, 0x2f, 0xa0, 0x3b, 0x5a, 0x81, 0xa9, 0x62, 0x66,
+ 0xf2, 0xe9, 0xcb, 0x61, 0x12, 0x17, 0x2b, 0x08, 0xf3, 0x95, 0x79, 0x98,
+ 0xe5, 0x3a, 0x59, 0xdf, 0x45, 0xe6, 0x6a, 0x08, 0x33, 0xad, 0x67, 0x11,
+ 0xed, 0x0a, 0x51, 0xeb, 0xc0, 0xa6, 0x1c, 0x68, 0xc9, 0x79, 0x98, 0xb7,
+ 0x0a, 0xb4, 0x46, 0x6b, 0x28, 0x8a, 0xe1, 0x98, 0x65, 0x0e, 0x9b, 0xa0,
+ 0xac, 0xb2, 0xe8, 0x4e, 0x75, 0xf0, 0xf4, 0xfe, 0xa5, 0x08, 0x43, 0xd8,
+ 0x1e, 0x20, 0x8e, 0x8d, 0x77, 0xaa, 0xfd, 0x45, 0xfd, 0x8d, 0xbe, 0xef,
+ 0xd0, 0x69, 0x6b, 0x20, 0x88, 0x0a, 0x08, 0x19, 0x8e, 0x8d, 0x6e, 0x29,
+ 0x53, 0xbd, 0xf5, 0xc8, 0x38, 0xca, 0x30, 0xd0, 0x07, 0x62, 0x59, 0x70,
+ 0x2e, 0xa0, 0x0b, 0x00, 0xd5, 0x2b, 0x8d, 0x9e, 0x03, 0x84, 0x49, 0x01,
+ 0xfe, 0x8b, 0x5b, 0x5c, 0x4d, 0x22, 0x80, 0x7a, 0xea, 0x2b, 0xa2, 0xd2,
+ 0x1a, 0xc7, 0x01, 0xc2, 0x46, 0x1e, 0x20, 0x8a, 0xb4, 0x93, 0x4f, 0x56,
+ 0xec, 0xd2, 0xf5, 0x01, 0x6e, 0x38, 0x57, 0x94, 0xe9, 0x9e, 0xb2, 0x61,
+ 0x6f, 0x29, 0x8c, 0x81, 0xbe, 0x92, 0xc5, 0x9e, 0x71, 0xc1, 0xb3, 0x6b,
+ 0x62, 0x03, 0x35, 0xd8, 0x71, 0x02, 0x50, 0x3e, 0x48, 0xad, 0xd2, 0x77,
+ 0x1c, 0x50, 0x88, 0xf5, 0xc9, 0x2b, 0x11, 0xe2, 0x62, 0xb4, 0x6f, 0xf3,
+ 0x7b, 0xeb, 0xba, 0xe7, 0x7f, 0x08, 0xbc, 0x1d, 0xef, 0xd0, 0x54, 0xa9,
+ 0x2c, 0xb1, 0x8b, 0x27, 0xd5, 0x33, 0xfe, 0xf0, 0x32, 0xb9, 0x99, 0x1a,
+ 0xac, 0x8e, 0x6c, 0x6e, 0xe0, 0x88, 0x22, 0xe3, 0xe9, 0x4d, 0xd7, 0x90,
+ 0x91, 0x1f, 0x0d, 0xdf, 0x43, 0xfe, 0x4a, 0x1d, 0x46, 0xc0, 0xea, 0x7c,
+ 0xf8, 0x5a, 0x34, 0x78, 0x60, 0x8d, 0xbd, 0xea, 0xfa, 0xca, 0xdb, 0x07,
+ 0xfd, 0x76, 0x97, 0xbd, 0xfd, 0x5b, 0xa6, 0xc2, 0xf4, 0x5e, 0x77, 0xe9,
+ 0x7a, 0xae, 0x61, 0xc0, 0x9e, 0xf7, 0x07, 0xd1, 0xde, 0x62, 0xe6, 0xe3,
+ 0xa1, 0xfc, 0xcf, 0x8a, 0x61, 0x9e, 0xbb, 0x05, 0x75, 0xcd, 0x2e, 0x8f,
+ 0xa6, 0xa1, 0x2c, 0x91, 0xf8, 0x25, 0xf0, 0xa5, 0x11, 0xd0, 0xa8, 0x6a,
+ 0x74, 0x08, 0x72, 0xfc, 0xa3, 0xe3, 0x44, 0x0c, 0x96, 0x7b, 0x60, 0x71,
+ 0xff, 0x04, 0xa3, 0x57, 0x6f, 0x34, 0xe8, 0xf0, 0xc9, 0x6b, 0x75, 0x76,
+ 0xeb, 0xc2, 0x59, 0xfd, 0x31, 0x38, 0xbf, 0x51, 0x8c, 0x5c, 0xf0, 0xd9,
+ 0x8e, 0x98, 0x01, 0x61, 0x45, 0x54, 0xa9, 0x7a, 0x28, 0x2b, 0x56, 0x18,
+ 0x76, 0x64, 0x68, 0x09, 0x2f, 0x20, 0xd0, 0x87, 0xd6, 0x8f, 0x39, 0x79,
+ 0x11, 0x11, 0x52, 0xf5, 0xa6, 0x93, 0x49, 0xe4, 0xcf, 0xba, 0xff, 0x5e,
+ 0x61, 0x4f, 0x30, 0xee, 0x38, 0xb7, 0xfc, 0xac, 0xeb, 0x3c, 0xa1, 0x2a,
+ 0x35, 0x32, 0xb4, 0x9c, 0x6b, 0x54, 0xfc, 0xd8, 0xe3, 0x1b, 0xf4, 0x53,
+ 0x36, 0xf3, 0x92, 0xca, 0x13, 0x4e, 0xbb, 0xc7, 0x9f, 0x95, 0x27, 0xc5,
+ 0x17, 0x99, 0x13, 0xbb, 0xbe, 0x6b, 0xf3, 0x24, 0x55, 0x4d, 0x5c, 0x7d,
+ 0x30, 0x35, 0x87, 0x5e, 0x56, 0x54, 0xb4, 0xdc, 0x54, 0xf1, 0x1c, 0x4c,
+ 0xca, 0x1e, 0xde, 0xdb, 0xaa, 0x04, 0x54, 0xe8, 0x1e, 0x30, 0x7f, 0xde,
+ 0xba, 0xa9, 0x66, 0xc5, 0x71, 0xda, 0x70, 0x44, 0x41, 0x72, 0x91, 0x4f,
+ 0xcc, 0x77, 0x55, 0x24, 0xff, 0x09, 0xe2, 0xbb, 0x5c, 0xbc, 0x19, 0xe0,
+ 0x15, 0xf2, 0x6a, 0x1d, 0x08, 0xca, 0xd9, 0x95, 0x10, 0x67, 0x51, 0x35,
+ 0xaf, 0xd4, 0xd7, 0x36, 0x40, 0x84, 0xb4, 0x39, 0x43, 0x4b, 0x43, 0x73,
+ 0x3b, 0xc6, 0x92, 0xe5, 0x19, 0x57, 0x56, 0x86, 0xfa, 0x1e, 0x8a, 0xe2,
+ 0x57, 0xb2, 0x0c, 0x5a, 0xde, 0x6c, 0x5d, 0xef, 0xb8, 0x0b, 0x38, 0xc6,
+ 0x91, 0x2e, 0x84, 0xf2, 0x49, 0xe4, 0x3b, 0xda, 0x5d, 0xcc, 0xcb, 0x6f,
+ 0x61, 0x20, 0xd5, 0xb6, 0xf5, 0xab, 0x02, 0x47, 0xbd, 0x53, 0xf2, 0x10,
+ 0x89, 0xfc, 0x3f, 0x69, 0xb3, 0xd2, 0x45, 0x9f, 0xdf, 0x3b, 0x99, 0x89,
+ 0x4a, 0x4f, 0x84, 0x48, 0x16, 0x75, 0x90, 0xa9, 0x74, 0xf6, 0xaf, 0xee,
+ 0x8e, 0x8a, 0x35, 0x90, 0x4b, 0x80, 0x06, 0x48, 0x04, 0x75, 0x72, 0xce,
+ 0xa8, 0xd4, 0xf8, 0xe5, 0x95, 0xb4, 0xab, 0x39, 0x69, 0x0d, 0x1a, 0x9b,
+ 0x82, 0xd7, 0x81, 0x2f, 0x07, 0x21, 0x29, 0x91, 0xda, 0x0c, 0x32, 0x18,
+ 0x64, 0x36, 0xc0, 0x37, 0x69, 0xde, 0x1a, 0xb4, 0xce, 0x50, 0x29, 0x02,
+ 0x0c, 0x91, 0x3c, 0xe5, 0x80, 0x28, 0x79, 0x66, 0x8e, 0xa6, 0x0a, 0x6e,
+ 0x9d, 0xb1, 0xf8, 0xab, 0x19, 0xae, 0x23, 0xea, 0x34, 0x10, 0x12, 0x06,
+ 0xd5, 0xcf, 0xb5, 0xf7, 0x5c, 0x29, 0xb3, 0x0c, 0xf9, 0xc4, 0x29, 0x28,
+ 0x64, 0x03, 0x6f, 0x05, 0x41, 0xc0, 0xf4, 0x59, 0x91, 0xaa, 0x5c, 0x0b,
+ 0x13, 0x62, 0xdd, 0x48, 0x13, 0xf7, 0x8e, 0xbf, 0xec, 0xb2, 0xb0, 0xe5,
+ 0x81, 0xdc, 0xd3, 0x3f, 0x0c, 0xf0, 0x67, 0x5b, 0x8b, 0x04, 0xf5, 0x1e,
+ 0x00, 0xcb, 0xcf, 0x13, 0xe3, 0xa7, 0xc8, 0x19, 0x0b, 0xe7, 0xbe, 0x61,
+ 0xa4, 0xf9, 0x63, 0x11, 0xa9, 0x0d, 0x1f, 0x04, 0xbf, 0x73, 0xc9, 0x5d,
+ 0x93, 0x84, 0x9a, 0x14, 0xa6, 0xa4, 0x28, 0x22, 0x00, 0x69, 0xea, 0x0a,
+ 0x78, 0xc4, 0x29, 0x6b, 0x57, 0x22, 0x36, 0xae, 0x2c, 0x02, 0xc3, 0x08,
+ 0xd0, 0x4c, 0x80, 0x62, 0xe5, 0x4e, 0xda, 0xbc, 0x0f, 0x83, 0x0e, 0xc3,
+ 0x95, 0x1e, 0x5f, 0x27, 0xac, 0xd1, 0x63, 0x91, 0xdf, 0xaf, 0x8e, 0x5b,
+ 0xb1, 0x6b, 0x47, 0x29, 0x07, 0xa7, 0xd0, 0x1f, 0x51, 0xdf, 0x8b, 0x2b,
+ 0xd7, 0xac, 0xd1, 0x78, 0x19, 0x02, 0x6a, 0x64, 0x65, 0x0a, 0x75, 0xb3,
+ 0x3a, 0x44, 0x42, 0x19, 0x4d, 0x2d, 0x3a, 0x1c, 0x24, 0xcb, 0x4d, 0x7e,
+ 0xa2, 0xfa, 0x87, 0x92, 0xfc, 0x08, 0x92, 0x5c, 0x4a, 0xfc, 0x7c, 0xbf,
+ 0x3b, 0xfc, 0x7f, 0xcf, 0x0e, 0x52, 0x0e, 0xad, 0xf6, 0x36, 0x14, 0xc6,
+ 0xa2, 0x46, 0x9c, 0x3a, 0xd9, 0xa2, 0x47, 0x74, 0x58, 0x81, 0x79, 0x21,
+ 0xa0, 0x7d, 0x72, 0xbd, 0x70, 0x1d, 0x11, 0xe7, 0x48, 0x84, 0xde, 0xf1,
+ 0xc5, 0xbf, 0xa5, 0xee, 0xa1, 0xfd, 0xa3, 0x72, 0x77, 0x27, 0x24, 0xc8,
+ 0xdf, 0x64, 0x9b, 0x22, 0x14, 0x56, 0x7d, 0x1a, 0x9d, 0xc0, 0x18, 0x88,
+ 0x30, 0x8b, 0xf6, 0x1f, 0xb3, 0xb6, 0x5d, 0x49, 0x87, 0x0f, 0x78, 0x93,
+ 0xdf, 0x9c, 0x1e, 0x22, 0x27, 0x95, 0x65, 0xc1, 0x3a, 0xd9, 0x42, 0x40,
+ 0xba, 0xac, 0xa2, 0x2c, 0xc4, 0xb2, 0x74, 0xfd, 0x57, 0xb4, 0x88, 0xa2,
+ 0xf7, 0x28, 0xbc, 0x50, 0x79, 0xc8, 0x38, 0x75, 0x06, 0x17, 0x9f, 0xb7,
+ 0xd2, 0xf4, 0x3a, 0xbb, 0x56, 0xfc, 0xde, 0x3c, 0xca, 0x3e, 0xcc, 0x71,
+ 0xae, 0x79, 0x6c, 0x4e, 0x8c, 0x75, 0x11, 0xb5, 0x30, 0xb4, 0x63, 0xde,
+ 0xfe, 0x95, 0x65, 0xb7, 0x35, 0x7c, 0x09, 0x46, 0x78, 0xb7, 0xa3, 0x24,
+ 0x0f, 0x06, 0x22, 0xf6, 0x60, 0x76, 0xd0, 0xee, 0x63, 0x1d, 0xaa, 0x2b,
+ 0x68, 0x2a, 0x89, 0x4d, 0x4b, 0xbc, 0xf4, 0xfe, 0x78, 0x35, 0xa7, 0x2f,
+ 0x48, 0xa6, 0xf1, 0xc9, 0x33, 0xa3, 0x2d, 0x3e, 0x5e, 0x5d, 0xdd, 0xdc,
+ 0x54, 0xae, 0xad, 0x20, 0xd0, 0x5c, 0x40, 0x6b, 0x21, 0x6f, 0x10, 0xd5,
+ 0xbb, 0x5d, 0x90, 0x3f, 0xd3, 0x34, 0x5c, 0x82, 0x53, 0xa0, 0x46, 0x8f,
+ 0xfb, 0x2f, 0xf3, 0xc2, 0x5f, 0x44, 0xf1, 0xa6, 0x83, 0x79, 0x6b, 0xf5,
+ 0x1f, 0x41, 0x45, 0xcb, 0x99, 0x2e, 0x7c, 0xd5, 0x23, 0xff, 0x92, 0x4e,
+ 0x9b, 0xc3, 0xc2, 0x67, 0x68, 0x24, 0xa0, 0x57, 0xa0, 0xb6, 0x29, 0xcb,
+ 0x25, 0xb1, 0x87, 0x56, 0x13, 0x86, 0x70, 0xb9, 0x42, 0x2b, 0x9c, 0xa4,
+ 0x18, 0xa7, 0x0c, 0xd6, 0x8b, 0xb0, 0xfc, 0xe0, 0xe3, 0x7e, 0x95, 0x3d,
+ 0x5e, 0x9b, 0x83, 0x09, 0x88, 0xf8, 0x9e, 0x91, 0x68, 0x70, 0x13, 0xdc,
+ 0x6f, 0x91, 0x47, 0x5a, 0x31, 0x7d, 0xc8, 0x79, 0x04, 0x39, 0x0b, 0xdb,
+ 0x2e, 0xe3, 0xe9, 0xfb, 0xbb, 0x07, 0x0b, 0x21, 0x93, 0xc3, 0xc6, 0xaf,
+ 0x1a, 0xb3, 0x25, 0xd7, 0xd4, 0x1e, 0x3e, 0xf0, 0xaf, 0x3c, 0xaa, 0xea,
+ 0xd3, 0x8a, 0x41, 0x2a, 0x8e, 0x83, 0x6c, 0xe3, 0xad, 0x0c, 0x12, 0xa3,
+ 0xc3, 0x67, 0x40, 0x0f, 0x14, 0x0f, 0xbe, 0xf6, 0x4c, 0x86, 0x08, 0xd1,
+ 0xc8, 0x4f, 0x75, 0xcf, 0x62, 0xde, 0x6a, 0x24, 0x7c, 0x67, 0x7f, 0x2b,
+ 0x2e, 0x97, 0x42, 0xa7, 0x2a, 0x07, 0x96, 0xa8, 0x36, 0xc1, 0xd1, 0xe9,
+ 0x6b, 0xb2, 0xdc, 0x36, 0x9a, 0x6e, 0x19, 0x99, 0xf6, 0x36, 0x28, 0xdb,
+ 0xdc, 0xb0, 0x8d, 0x33, 0x6d, 0x22, 0x8c, 0xe3, 0x78, 0x55, 0x56, 0xed,
+ 0xe7, 0x68, 0xdb, 0xd5, 0x77, 0x5e, 0x25, 0x6c, 0xdf, 0x41, 0x5d, 0x7e,
+ 0xce, 0x33, 0xd6, 0xee, 0x35, 0x04, 0xa7, 0xda, 0x94, 0x27, 0x0e, 0xd8,
+ 0x57, 0x3c, 0x6b, 0x18, 0x04, 0x93, 0x64, 0xb8, 0xf4, 0x5b, 0x3e, 0xe1,
+ 0xfb, 0x0c, 0xb8, 0x72, 0x4e, 0x27, 0xb8, 0xa4, 0x53, 0xc4, 0x27, 0xfc,
+ 0x22, 0x76, 0x78, 0xf2, 0x52, 0x00, 0xe5, 0x3b, 0xff, 0x10, 0xba, 0x75,
+ 0x52, 0xd7, 0xe8, 0xb9, 0x3f, 0xcb, 0x56, 0xc3, 0x2e, 0x3d, 0x5d, 0xcc,
+ 0x67, 0x88, 0x7b, 0x33, 0x6c, 0x9e, 0xa4, 0x0f, 0x80, 0xa3, 0x62, 0x3f,
+ 0x03, 0x89, 0x06, 0x82, 0x93, 0xbb, 0x32, 0xcd, 0xe8, 0x99, 0x5a, 0x3a,
+ 0x4c, 0xf6, 0x42, 0x0d, 0x56, 0x60, 0x3c, 0xdc, 0xcf, 0x6b, 0xc2, 0x5a,
+ 0x1d, 0x35, 0x8d, 0xd1, 0x11, 0x31, 0xe0, 0x30, 0xba, 0xa6, 0x48, 0xfc,
+ 0x94, 0x6a, 0x31, 0x29, 0xc7, 0xa6, 0x47, 0xaf, 0x14, 0x93, 0x33, 0x2b,
+ 0x3e, 0x34, 0x5d, 0x79, 0xb7, 0xca, 0xea, 0xac, 0xa7, 0xf3, 0xfa, 0x65,
+ 0x57, 0x4c, 0xcd, 0x2c, 0x0b, 0xa5, 0xbc, 0x25, 0x8a, 0x2a, 0xa2, 0xfe,
+ 0x40, 0xf7, 0x28, 0x30, 0x4a, 0x1e, 0x4c, 0x29, 0x82, 0x86, 0x04, 0x2a,
+ 0x63, 0xc3, 0xcf, 0xd4, 0x81, 0xda, 0xb8, 0x43, 0x71, 0x57, 0x3b, 0xf6,
+ 0x58, 0xf1, 0xae, 0x84, 0xa8, 0xd0, 0xa0, 0x3b, 0xfe, 0x5a, 0x39, 0x8a,
+ 0xf2, 0x35, 0x00, 0x58, 0xb6, 0x25, 0x46, 0x62, 0x05, 0xab, 0x27, 0x98,
+ 0x28, 0x4e, 0x37, 0xbc, 0xe2, 0xd7, 0x30, 0x2c, 0xae, 0x1c, 0xea, 0x59,
+ 0x96, 0x72, 0xa9, 0x9d, 0xa8, 0x9e, 0xed, 0x24, 0xc0, 0x48, 0x63, 0xad,
+ 0x54, 0x2d, 0x17, 0x0f, 0xa1, 0xff, 0x72, 0xac, 0xbb, 0xa6, 0x62, 0x3a,
+ 0x83, 0x71, 0xd8, 0x02, 0x51, 0x7c, 0xaa, 0x9e, 0x3b, 0x72, 0xdc, 0x10,
+ 0xa7, 0xd5, 0x1b, 0xa1, 0x7a, 0x7a, 0x21, 0x99, 0x5a, 0x6f, 0x83, 0x07,
+ 0x5f, 0x71, 0xa3, 0x2b, 0x6d, 0x81, 0xd3, 0x20, 0x18, 0xc2, 0x08, 0xd5,
+ 0xe8, 0x00, 0xaa, 0xfb, 0x3b, 0x3c, 0xd7, 0xba, 0x4d, 0xa2, 0x67, 0x96,
+ 0x42, 0x89, 0xb7, 0x4b, 0x6c, 0x0f, 0xa0, 0x0d, 0x07, 0x82, 0x33, 0xf7,
+ 0x8d, 0xdc, 0xf2, 0x7d, 0x61, 0x02, 0x1b, 0xd6, 0x78, 0xfe, 0x24, 0x11,
+ 0xbd, 0xb1, 0x0e, 0x82, 0x97, 0x3c, 0x44, 0x82, 0xb0, 0x4d, 0x58, 0xa8,
+ 0x9f, 0x4c, 0x28, 0x52, 0x7c, 0xbe, 0xeb, 0xc1, 0x18, 0x22, 0x55, 0xb0,
+ 0xb3, 0x23, 0x8d, 0x2a, 0xc2, 0xcb, 0x73, 0x58, 0x74, 0x07, 0xe1, 0xa6,
+ 0xb7, 0x71, 0x1d, 0x93, 0xff, 0x43, 0xba, 0xe0, 0xad, 0x93, 0x42, 0x12,
+ 0xa9, 0x54, 0xab, 0xc9, 0x29, 0x7e, 0x5a, 0xf1, 0xf0, 0x99, 0xb9, 0x59,
+ 0x22, 0x05, 0xb4, 0xd4, 0xfb, 0xf6, 0xca, 0xb2, 0x81, 0x12, 0x28, 0xf8,
+ 0x2a, 0x57, 0xc9, 0x14, 0x60, 0x4c, 0xd0, 0xcd, 0x2e, 0x3a, 0xd6, 0x65,
+ 0x63, 0x88, 0xe7, 0xff, 0xdc, 0x21, 0xfd, 0xe4, 0xd9, 0x91, 0xc2, 0xa5,
+ 0xd2, 0x93, 0xa3, 0xd7, 0x81, 0x19, 0x79, 0x5a, 0x02, 0x08, 0xae, 0x45,
+ 0xa2, 0xb2, 0x2a, 0x38, 0x02, 0x8a, 0x67, 0x49, 0x00, 0xb8, 0x74, 0xec,
+ 0xd9, 0x78, 0xd7, 0x97, 0xd2, 0xba, 0xd7, 0x7f, 0xd3, 0x1c, 0x6f, 0x40,
+ 0x2c, 0x4c, 0xb6, 0x95, 0xf1, 0x2b, 0xa1, 0x19, 0x66, 0x76, 0x35, 0xf2,
+ 0x04, 0x1f, 0xcc, 0x0b, 0x0b, 0x89, 0x97, 0x4a, 0xf7, 0x49, 0x66, 0x25,
+ 0x90, 0x22, 0x3f, 0x9a, 0xf2, 0x9c, 0x7a, 0x68, 0xed, 0x73, 0x42, 0x9c,
+ 0x83, 0x27, 0xbf, 0x44, 0x9e, 0x77, 0xcd, 0x21, 0xb6, 0x4b, 0xe5, 0x9f,
+ 0x97, 0x48, 0x78, 0x86, 0x1d, 0x3c, 0x20, 0xc3, 0xb9, 0xbf, 0x42, 0xdd,
+ 0x29, 0x2f, 0xd9, 0x2b, 0xd9, 0x7a, 0xc1, 0x1c, 0xca, 0x2b, 0x9c, 0xcd,
+ 0xd0, 0x76, 0x86, 0x06, 0xcd, 0x23, 0x3e, 0x41, 0x15, 0xd2, 0x57, 0xe9,
+ 0x7b, 0xca, 0x6f, 0x02, 0x26, 0x43, 0xa8, 0xd6, 0x87, 0x83, 0xed, 0xb3,
+ 0xa8, 0x84, 0x54, 0xad, 0x8f, 0x83, 0x7a, 0x34, 0xc3, 0x6a, 0x5d, 0x97,
+ 0x1e, 0xb9, 0x93, 0x58, 0x6b, 0x67, 0x4d, 0x3b, 0x6b, 0x69, 0x86, 0xfb,
+ 0x40, 0xa6, 0x76, 0xee, 0x74, 0xaa, 0x54, 0x25, 0x5d, 0x24, 0x8c, 0x52,
+ 0xc2, 0x31, 0xd0, 0x86, 0xab, 0xd2, 0x59, 0x6f, 0x46, 0x8a, 0x18, 0x53,
+ 0x0a, 0xf3, 0x94, 0xe0, 0x6c, 0x2c, 0x69, 0xf9, 0x47, 0x49, 0x7e, 0xc5,
+ 0xb0, 0x80, 0x72, 0xf7, 0xc1, 0x4e, 0xc2, 0x9f, 0x7e, 0x8b, 0x94, 0x1c,
+ 0xb3, 0x00, 0xa1, 0xdc, 0x13, 0x66, 0x73, 0x7b, 0x91, 0x2a, 0x78, 0xe6,
+ 0x11, 0xd4, 0x7a, 0xa0, 0x98, 0xe6, 0x2e, 0x4b, 0x73, 0x4d, 0x19, 0xe2,
+ 0x8c, 0x7b, 0x13, 0xf6, 0x03, 0x32, 0x78, 0x4c, 0x99, 0xdf, 0xd8, 0x75,
+ 0x2b, 0x2b, 0x01, 0x35, 0x0c, 0xea, 0xb4, 0x80, 0x2c, 0x82, 0x83, 0xe1,
+ 0xd2, 0x81, 0x38, 0xad, 0x34, 0x71, 0xf8, 0x56, 0xfc, 0x05, 0xa0, 0xa5,
+ 0x6d, 0xfa, 0x67, 0x01, 0x78, 0x8d, 0x1c, 0xb4, 0x72, 0x06, 0x7b, 0x8e,
+ 0xf9, 0x11, 0x72, 0xc6, 0xe6, 0x9b, 0x7d, 0x7c, 0x6d, 0x6d, 0x20, 0xd9,
+ 0x60, 0xa1, 0x76, 0x65, 0x0a, 0x13, 0x41, 0x96, 0xea, 0x24, 0x56, 0xbf,
+ 0x45, 0xf0, 0x0e, 0xa1, 0xe1, 0x6c, 0x70, 0x9d, 0x16, 0x76, 0xf5, 0x22,
+ 0x63, 0xd9, 0x1b, 0xf4, 0x83, 0x39, 0xc9, 0xad, 0xde, 0xa4, 0x83, 0x95,
+ 0x1a, 0x0d, 0xba, 0xd6, 0x48, 0xff, 0x7a, 0x41, 0x46, 0x27, 0x02, 0x0f,
+ 0x47, 0xff, 0x6a, 0x1d, 0xe3, 0xba, 0xc3, 0x24, 0xc4, 0x9f, 0x02, 0xc2,
+ 0x65, 0xd6, 0xd1, 0x32, 0x40, 0x91, 0x50, 0x74, 0x75, 0x44, 0x3e, 0xf8,
+ 0x24, 0xc8, 0xdd, 0xdb, 0xaa, 0xd3, 0xff, 0x08, 0x0d, 0x2e, 0x61, 0x41,
+ 0x87, 0xbc, 0x8a, 0x96, 0xd8, 0xd5, 0xe2, 0x25, 0x65, 0xd4, 0xa8, 0xe1,
+ 0x61, 0x3d, 0x11, 0x00, 0xff, 0xcf, 0x25, 0x87, 0xd3, 0xa8, 0x6b, 0x88,
+ 0x73, 0xa2, 0xb8, 0xa4, 0x96, 0xa8, 0xcf, 0x6c, 0xd3, 0xb8, 0xb0, 0x07,
+ 0x7a, 0xdc, 0xf0, 0x52, 0x27, 0xfe, 0xe1, 0x44, 0x93, 0x2c, 0x9f, 0x91,
+ 0x68, 0xbe, 0x37, 0x0f, 0x8c, 0x89, 0x4c, 0x2e, 0x8e, 0x1c, 0xe9, 0xdc,
+ 0xeb, 0x68, 0x91, 0x1b, 0x8d, 0x69, 0x88, 0x7e, 0xea, 0x56, 0xa3, 0x79,
+ 0x72, 0x62, 0xd2, 0x63, 0xfb, 0x32, 0xa3, 0xa0, 0xcc, 0x2c, 0x4e, 0xaa,
+ 0xb4, 0x0a, 0x19, 0xa4, 0x8d, 0x8c, 0x03, 0x31, 0x20, 0x7a, 0x9b, 0x09,
+ 0x63, 0x4b, 0x81, 0x4b, 0x50, 0x33, 0x42, 0xe0, 0x24, 0x82, 0x66, 0xb2,
+ 0x7f, 0x44, 0x19, 0xed, 0x44, 0x17, 0x4b, 0x66, 0x9e, 0x22, 0x28, 0x80,
+ 0x55, 0xdf, 0xb6, 0xad, 0xce, 0x1b, 0xb0, 0xcd, 0x5d, 0x73, 0xe0, 0x2d,
+ 0x3c, 0xf0, 0xc4, 0xd8, 0x4d, 0x05, 0x0c, 0x31, 0x3b, 0xb1, 0xfc, 0xf9,
+ 0x49, 0xcd, 0xc3, 0x04, 0x65, 0xb0, 0x0e, 0xcc, 0xd3, 0xd4, 0xb4, 0xef,
+ 0xe3, 0xd6, 0xb1, 0xd5, 0x3a, 0xa3, 0x96, 0x54, 0x84, 0x1a, 0x4a, 0x2f,
+ 0x05, 0xb3, 0x86, 0x99, 0x65, 0x31, 0xc1, 0xed, 0x46, 0xfb, 0x7e, 0x36,
+ 0xc8, 0x10, 0x5a, 0x8a, 0x4f, 0x64, 0xf7, 0x66, 0xad, 0x79, 0x0e, 0xd4,
+ 0x6e, 0x65, 0x81, 0x2e, 0x4a, 0xcb, 0x32, 0xf2, 0xe2, 0x86, 0x59, 0xb5,
+ 0x65, 0x4f, 0x9e, 0xf1, 0x42, 0x0b, 0xdc, 0x67, 0xb2, 0x78, 0xf7, 0x60,
+ 0x1a, 0xb8, 0xa6, 0x8f, 0x4d, 0x55, 0x2f, 0xeb, 0x45, 0x05, 0x5b, 0xb4,
+ 0xc5, 0x67, 0xbc, 0x2b, 0x2c, 0xbe, 0x75, 0xe1, 0x5c, 0xa3, 0x2a, 0xdf,
+ 0xb9, 0xc9, 0x6e, 0xc7, 0x20, 0xe9, 0x90, 0xdf, 0x24, 0x56, 0x72, 0xf7,
+ 0xa1, 0xc7, 0x3e, 0xbc, 0x69, 0x7d, 0x4c, 0xc7, 0xcc, 0xfc, 0xbe, 0xe4,
+ 0x3e, 0xe0, 0x8a, 0xd1, 0x06, 0xb5, 0x95, 0xcd, 0xc0, 0xb7, 0x77, 0x13,
+ 0x8a, 0xfb, 0x56, 0xb7, 0x94, 0x8a, 0x77, 0x52, 0xcb, 0x00, 0x87, 0x1b,
+ 0xc8, 0xd2, 0xb3, 0xe3, 0x50, 0xf1, 0xb3, 0xdc, 0x3f, 0x62, 0x0c, 0xbf,
+ 0xad, 0xf7, 0x92, 0xf0, 0x57, 0xfb, 0xc2, 0xaa, 0x91, 0x4b, 0xc9, 0x1a,
+ 0x96, 0x19, 0xed, 0x52, 0x52, 0x0c, 0x0a, 0x39, 0x14, 0x4d, 0x77, 0xc8,
+ 0x5a, 0xdb, 0x46, 0xbb, 0xd6, 0x80, 0x6a, 0xd2, 0xe3, 0xaa, 0xd8, 0xda,
+ 0x28, 0xb9, 0xc4, 0x85, 0x7d, 0x50, 0xbc, 0x5d, 0xbe, 0xa7, 0xfa, 0x90,
+ 0xbe, 0x3f, 0x18, 0x46, 0xb5, 0xf9, 0x5d, 0xd4, 0xfa, 0xa3, 0x8b, 0x97,
+ 0x1c, 0x81, 0xb8, 0x06, 0x4a, 0x4a, 0x74, 0xcb, 0xb5, 0x07, 0xcc, 0xea,
+ 0xa2, 0xa3, 0x16, 0x2f, 0x88, 0xff, 0xb6, 0xc3, 0x05, 0x4f, 0x0a, 0x5c,
+ 0x20, 0xea, 0x05, 0x43, 0xca, 0x7c, 0x60, 0x00, 0x43, 0xd3, 0xfa, 0x19,
+ 0x1e, 0x5b, 0x77, 0xbb, 0x5b, 0xff, 0x3b, 0xa4, 0xfb, 0x96, 0xe9, 0x9f,
+ 0x45, 0x6f, 0x87, 0xce, 0x02, 0x26, 0x7b, 0x8a, 0xbe, 0x92, 0xe4, 0xf1,
+ 0x38, 0x51, 0x5a, 0xe0, 0x16, 0x83, 0x8a, 0xc2, 0x84, 0x8a, 0x34, 0x9c,
+ 0x14, 0xa3, 0xa6, 0x9d, 0x9f, 0xac, 0x44, 0x0c, 0x5b, 0x03, 0x9b, 0x8e,
+ 0xa5, 0xd5, 0x4d, 0xae, 0x42, 0xa8, 0x29, 0x75, 0xdd, 0x5e, 0xee, 0x3c,
+ 0xa8, 0x3d, 0xb9, 0xcb, 0x12, 0x33, 0x0c, 0x21, 0x6a, 0xe1, 0x29, 0xf7,
+ 0xde, 0x63, 0x8d, 0x38, 0x78, 0xdf, 0x5a, 0x6e, 0x8f, 0x60, 0x7b, 0xab,
+ 0xdf, 0x29, 0x67, 0xb2, 0x5c, 0x56, 0x50, 0x08, 0x37, 0x13, 0x67, 0xf6,
+ 0xc3, 0xe3, 0xed, 0x29, 0xc7, 0xd5, 0xdb, 0xcc, 0x3a, 0x98, 0x92, 0xba,
+ 0x8f, 0x11, 0x5c, 0x79, 0x21, 0x8a, 0x46, 0x1b, 0x97, 0x49, 0x3d, 0x49,
+ 0xca, 0xf9, 0x4a, 0xf7, 0x49, 0x37, 0x9e, 0x2c, 0x17, 0xae, 0x5d, 0xa3,
+ 0x35, 0x6d, 0xdb, 0xc7, 0xaf, 0xfb, 0x50, 0xfd, 0xf8, 0xd4, 0x4f, 0x2e,
+ 0x4b, 0xc1, 0xa1, 0x9d, 0xdf, 0x32, 0x72, 0x5b, 0x1d, 0x67, 0x7e, 0x91,
+ 0xf3, 0x46, 0xed, 0xdd, 0x1f, 0x69, 0x89, 0x34, 0xb7, 0x0a, 0x16, 0xbf,
+ 0xa1, 0x29, 0xf5, 0x4d, 0xf8, 0x3a, 0xb3, 0xfa, 0xf7, 0x0b, 0xeb, 0x64,
+ 0xad, 0x3c, 0x47, 0x3e, 0x14, 0x37, 0x5b, 0xde, 0xf0, 0xe7, 0x27, 0xce,
+ 0x4b, 0xe5, 0x1f, 0x72, 0xad, 0x7b, 0xca, 0x8c, 0xb8, 0x69, 0xef, 0x39,
+ 0x98, 0x13, 0xcd, 0xaa, 0xe7, 0x58, 0x90, 0x51, 0x8b, 0xfd, 0xbb, 0x51,
+ 0xe4, 0xa8, 0x80, 0xb3, 0x58, 0x59, 0x1f, 0x7b, 0x96, 0xcc, 0x7e, 0xa8,
+ 0xef, 0xc5, 0x98, 0xa0, 0x22, 0x8e, 0xbc, 0x5e, 0x25, 0x01, 0x0f, 0x70,
+ 0x04, 0x93, 0x5f, 0x96, 0xc2, 0x9e, 0x1d, 0xbb, 0xf5, 0x88, 0xd4, 0xf2,
+ 0x7e, 0xfd, 0x4f, 0x3b, 0xf0, 0xbe, 0xe6, 0xad, 0x41, 0x82, 0xf4, 0x0b,
+ 0x2e, 0xa8, 0xbc, 0xfd, 0xb5, 0xc7, 0xd6, 0x72, 0xb5, 0x48, 0x7b, 0x12,
+ 0x18, 0xbf, 0x9e, 0x26, 0x11, 0x49, 0x58, 0xab, 0x53, 0x86, 0xa4, 0xa4,
+ 0x91, 0x35, 0x35, 0x5e, 0x91, 0x33, 0xe0, 0xe6, 0x03, 0xee, 0x2e, 0x8d,
+ 0x22, 0xfb, 0x12, 0x87, 0xdc, 0xd8, 0x17, 0x3a, 0xdb, 0x08, 0xfb, 0x33,
+ 0xf0, 0x6e, 0x94, 0x4b, 0xfc, 0x35, 0x59, 0x9f, 0x79, 0xfd, 0x6f, 0x11,
+ 0x72, 0x33, 0xa0, 0xdb, 0x08, 0xbc, 0x75, 0x7a, 0x44, 0xdb, 0x9b, 0xac,
+ 0x50, 0x2d, 0xff, 0xec, 0x87, 0x52, 0xee, 0xf7, 0xeb, 0x24, 0xd9, 0x7e,
+ 0xab, 0xd4, 0x0d, 0x3d, 0x3a, 0x69, 0x57, 0x82, 0x0f, 0x79, 0xca, 0x5b,
+ 0xea, 0x3d, 0x09, 0x5a, 0xaf, 0xf7, 0x5f, 0xe1, 0x4a, 0x1a, 0x84, 0xdb,
+ 0xe7, 0x7a, 0x74, 0x0d, 0xdf, 0x21, 0x67, 0x7f, 0x99, 0x29, 0x01, 0x31,
+ 0xd1, 0x84, 0x0e, 0xf6, 0xd3, 0x62, 0xfc, 0x80, 0xd4, 0x95, 0xc1, 0x8b,
+ 0x12, 0x90, 0xe6, 0x5c, 0xd2, 0xf5, 0x19, 0x8a, 0xad, 0x09, 0xf8, 0x24,
+ 0x28, 0x05, 0x1f, 0x2a, 0xc6, 0x6d, 0xdf, 0xaf, 0xdf, 0xa5, 0xcb, 0xa4,
+ 0x04, 0x51, 0xd0, 0x1f, 0x68, 0xd3, 0xc0, 0x63, 0xbb, 0xd1, 0xa4, 0x43,
+ 0x7d, 0xc4, 0x6e, 0x7d, 0x25, 0xe6, 0x73, 0xfe, 0x96, 0xbd, 0xb1, 0x27,
+ 0x76, 0x97, 0xf6, 0x64, 0xfc, 0x1c, 0x12, 0x7f, 0xec, 0xea, 0x2e, 0xa5,
+ 0x98, 0xe9, 0xc6, 0x8f, 0xf9, 0x15, 0xb3, 0x0a, 0xc8, 0xf7, 0xca, 0x23,
+ 0x0f, 0x7e, 0x42, 0x79, 0x10, 0x31, 0x96, 0x78, 0x7d, 0x05, 0x2c, 0x82,
+ 0xea, 0xaf, 0x59, 0xc9, 0x77, 0xb4, 0xa7, 0xa4, 0x48, 0x31, 0xa6, 0x29,
+ 0x7b, 0x9d, 0x83, 0x87, 0xfc, 0x11, 0x96, 0x3d, 0x78, 0x02, 0xc6, 0xfe,
+ 0x7d, 0x81, 0x3b, 0xa1, 0xf3, 0xc6, 0xf2, 0xdb, 0x45, 0xfc, 0xe3, 0xfb,
+ 0x04, 0x35, 0xbd, 0xaa, 0xdb, 0x97, 0xaa, 0x6a, 0x4e, 0xac, 0xc3, 0x98,
+ 0xff, 0x70, 0xad, 0xa1, 0xab, 0x15, 0x43, 0xcf, 0x31, 0x0d, 0xd1, 0x5c,
+ 0xe5, 0xf5, 0x4e, 0x0c, 0x45, 0x22, 0xc1, 0x27, 0x4f, 0x82, 0x90, 0x39,
+ 0x97, 0xb9, 0x1d, 0x5e, 0xe3, 0x7d, 0x0e, 0x90, 0x04, 0x3a, 0xc2, 0x71,
+ 0xbb, 0x17, 0x64, 0xe2, 0xd0, 0x97, 0x0d, 0x79, 0x29, 0xbc, 0x67, 0x30,
+ 0xd0, 0xb8, 0x42, 0x3c, 0x64, 0x7f, 0x98, 0x4d, 0x13, 0x95, 0x87, 0xe4,
+ 0x17, 0xd9, 0x00, 0x02, 0x6b, 0x4b, 0x67, 0x7d, 0x6c, 0x24, 0x3f, 0x5b,
+ 0xe2, 0x6f, 0x9d, 0xa4, 0xf7, 0x00, 0x4f, 0x96, 0x1f, 0x23, 0xb3, 0x42,
+ 0x87, 0x8c, 0xa1, 0x48, 0x51, 0x15, 0x16, 0x82, 0xf9, 0xb8, 0x65, 0xf4,
+ 0x4e, 0x87, 0x59, 0xf7, 0x9b, 0x12, 0xf5, 0x3b, 0xc2, 0xff, 0xe6, 0xcc,
+ 0xfc, 0x62, 0x4c, 0xd9, 0x85, 0x2b, 0x45, 0xdc, 0x04, 0x2c, 0x47, 0x69,
+ 0x58, 0xc4, 0x0a, 0x95, 0xd9, 0xfe, 0xf7, 0x09, 0xd9, 0xd3, 0x8e, 0xf1,
+ 0xc6, 0xe7, 0xc3, 0x51, 0xf3, 0x69, 0x18, 0xeb, 0x7b, 0xf2, 0x00, 0x80,
+ 0x23, 0x75, 0x1f, 0x1e, 0x0f, 0xba, 0xab, 0x41, 0xe1, 0xa9, 0x35, 0xd0,
+ 0x5d, 0x5a, 0x49, 0xbf, 0x1f, 0x88, 0x40, 0xa6, 0x77, 0x4a, 0x40, 0xcf,
+ 0x6b, 0xb8, 0xec, 0x9a, 0x45, 0x05, 0xbf, 0xe5, 0x0f, 0xde, 0xd4, 0xaf,
+ 0xeb, 0x73, 0xef, 0x70, 0xa9, 0xe5, 0xfe, 0xba, 0x03, 0xd9, 0xea, 0xfa,
+ 0x8f, 0x0b, 0x37, 0xcd, 0xc1, 0xcd, 0x95, 0xd7, 0x65, 0x12, 0xfc, 0x18,
+ 0x43, 0x2b, 0xbb, 0xcd, 0x65, 0xc9, 0x25, 0x4a, 0x9b, 0x70, 0xcb, 0xe2,
+ 0x95, 0x39, 0x55, 0x90, 0xe9, 0xf5, 0x8c, 0x97, 0x6d, 0xd4, 0xbe, 0x1c,
+ 0xc0, 0x6b, 0x7c, 0x14, 0x59, 0x09, 0xae, 0xbd, 0x09, 0x8a, 0x41, 0xba,
+ 0x79, 0xde, 0x29, 0x91, 0x63, 0x29, 0xda, 0x07, 0xe4, 0xbc, 0xe0, 0x27,
+ 0xa4, 0xdb, 0xe5, 0x85, 0x80, 0xe9, 0xdd, 0x55, 0xbe, 0xc3, 0xb5, 0x4e,
+ 0x33, 0x89, 0xda, 0x2d, 0x42, 0x74, 0xdc, 0x02, 0x06, 0x16, 0x46, 0xeb,
+ 0xce, 0xb0, 0x7b, 0x73, 0x3d, 0x0b, 0x90, 0xcb, 0x16, 0x48, 0x75, 0x7e,
+ 0x4c, 0x1d, 0x13, 0x53, 0x57, 0x57, 0x89, 0x75, 0x43, 0x43, 0x55, 0x26,
+ 0x4d, 0x7a, 0xa5, 0xdf, 0x59, 0xdf, 0x47, 0x44, 0xb1, 0xdc, 0x55, 0x8f,
+ 0xec, 0x3e, 0x5f, 0x3f, 0x8a, 0x2a, 0xd0, 0x9c, 0x1f, 0xe7, 0xef, 0x41,
+ 0x2d, 0x6f, 0x66, 0xb1, 0x08, 0x5a, 0x2b, 0xcb, 0xfc, 0xdb, 0x6c, 0x2e,
+ 0x28, 0x66, 0xa6, 0x8f, 0xcb, 0x9a, 0x88, 0x81, 0x7b, 0x51, 0x0a, 0x6c,
+ 0x65, 0xba, 0x0a, 0xf6, 0xd5, 0x49, 0x1a, 0xf8, 0x35, 0x68, 0x07, 0x1b,
+ 0x0d, 0x12, 0xce, 0x75, 0xb1, 0x48, 0xfc, 0x8f, 0x6f, 0xf2, 0xb4, 0x33,
+ 0xdd, 0xf2, 0xce, 0xa1, 0xa8, 0x75, 0x86, 0x20, 0x0a, 0x25, 0xe1, 0x6f,
+ 0xf4, 0x6a, 0x93, 0x49, 0xe3, 0xd6, 0x9e, 0x4f, 0xbf, 0xc2, 0x48, 0xfe,
+ 0x21, 0x01, 0x13, 0xde, 0x3b, 0x62, 0x4a, 0xff, 0x83, 0xe7, 0x55, 0x2a,
+ 0x81, 0x9f, 0xbd, 0x43, 0xe1, 0x4d, 0x6d, 0x80, 0x5c, 0xd3, 0xa9, 0x51,
+ 0x03, 0xf0, 0x01, 0x17, 0x10, 0x1f, 0xe5, 0x0f, 0xbb, 0x0a, 0x87, 0x67,
+ 0xff, 0x64, 0xcc, 0x19, 0xc8, 0xba, 0xd4, 0xba, 0x2c, 0xd7, 0x71, 0xd0,
+ 0x05, 0xa9, 0xa9, 0x7e, 0x01, 0xac, 0x48, 0xdb, 0xee, 0x24, 0xc2, 0x16,
+ 0xfa, 0x04, 0x8a, 0x83, 0x9a, 0x42, 0x96, 0x22, 0x84, 0xe5, 0x2b, 0xa3,
+ 0x74, 0xb9, 0xc2, 0x98, 0xd0, 0x2a, 0x58, 0xab, 0x03, 0xf1, 0xfc, 0xb2,
+ 0x1a, 0x2d, 0xed, 0x46, 0x33, 0xb6, 0x5b, 0xb6, 0xa4, 0x9e, 0xf4, 0x54,
+ 0xba, 0x93, 0xf4, 0x6a, 0x4a, 0x91, 0xce, 0xa5, 0x17, 0x3b, 0x01, 0xd3,
+ 0x40, 0x17, 0xa0, 0xfa, 0x9f, 0x70, 0xa5, 0x45, 0x2c, 0x6b, 0xb1, 0xb9,
+ 0x5b, 0x6d, 0xdd, 0xc3, 0xcb, 0x68, 0xd0, 0x7e, 0x12, 0x9e, 0xc5, 0xf0,
+ 0x7a, 0x8e, 0xd0, 0xe2, 0xc5, 0xf1, 0x3c, 0xb0, 0x32, 0x73, 0xeb, 0xc9,
+ 0x04, 0xbe, 0xc3, 0x57, 0xcd, 0x84, 0xa2, 0x6f, 0x74, 0xb1, 0x59, 0xa7,
+ 0x8d, 0x6b, 0xad, 0x5b, 0x1b, 0x2d, 0x8b, 0xf3, 0x07, 0x5c, 0xb5, 0x1c,
+ 0x83, 0xee, 0x82, 0x18, 0x7c, 0xf1, 0xc0, 0x8e, 0x64, 0xd6, 0x48, 0xef,
+ 0x28, 0x5e, 0x28, 0x20, 0x3c, 0x91, 0xb1, 0xcd, 0xa3, 0x1e, 0x37, 0x2a,
+ 0x66, 0xba, 0x3d, 0x5a, 0x11, 0xb1, 0x6b, 0x77, 0x29, 0xb3, 0x46, 0x15,
+ 0x8e, 0x59, 0xc2, 0x2e, 0xc4, 0x5a, 0x03, 0xa1, 0x60, 0xb5, 0x03, 0x63,
+ 0x46, 0xbf, 0x94, 0x4f, 0xef, 0xa7, 0x2f, 0x9d, 0x20, 0x40, 0xfa, 0x5f,
+ 0xf2, 0xad, 0x2b, 0x0c, 0x70, 0xed, 0xdf, 0x98, 0x08, 0xf6, 0x94, 0x1c,
+ 0x93, 0x8e, 0x59, 0xf4, 0xe3, 0x87, 0x94, 0xe4, 0xfc, 0xae, 0x3e, 0x08,
+ 0x07, 0xc4, 0x96, 0x90, 0x6a, 0x0e, 0xbb, 0x5b, 0x76, 0x61, 0xf6, 0x76,
+ 0x2b, 0x71, 0x03, 0x41, 0xc2, 0x8f, 0x84, 0x2b, 0xad, 0x39, 0x52, 0x26,
+ 0xa5, 0x64, 0xfe, 0x3c, 0x84, 0x84, 0x25, 0x21, 0xe7, 0x9a, 0x54, 0xc0,
+ 0xf1, 0xfb, 0xdf, 0x74, 0x16, 0xbe, 0x98, 0xd2, 0x5b, 0xfd, 0x3a, 0xd9,
+ 0xe5, 0x38, 0x0a, 0x81, 0x06, 0x62, 0xbd, 0xe4, 0x85, 0x25, 0xbd, 0x35,
+ 0x64, 0x40, 0xc0, 0x33, 0x2d, 0x20, 0x8d, 0xc9, 0xa3, 0xcd, 0xdb, 0x01,
+ 0xec, 0xec, 0xa4, 0x5f, 0xde, 0xb4, 0xe0, 0x6d, 0xd2, 0x13, 0x9d, 0x92,
+ 0xb8, 0x1d, 0xa8, 0xe8, 0xb8, 0xe0, 0x80, 0x19, 0xbe, 0x5a, 0x0e, 0x5a,
+ 0xaf, 0x91, 0xe0, 0xe8, 0x27, 0xe1, 0x81, 0xef, 0xa4, 0x71, 0xff, 0xe8,
+ 0x0a, 0x76, 0x8d, 0x56, 0x8b, 0x39, 0x18, 0x54, 0x81, 0xf2, 0xa8, 0x0b,
+ 0x56, 0x7e, 0x55, 0x46, 0x18, 0x8e, 0xb2, 0x3e, 0xad, 0xf1, 0x02, 0x4e,
+ 0xc5, 0x08, 0xf6, 0xb1, 0xce, 0x85, 0xf4, 0xab, 0x02, 0xfc, 0x45, 0x42,
+ 0x1c, 0xe8, 0xa1, 0xb7, 0x29, 0xd0, 0x76, 0x58, 0xdc, 0xd6, 0x6c, 0xdf,
+ 0x90, 0x59, 0x36, 0xd0, 0x2e, 0xd2, 0x9b, 0x86, 0xa2, 0x2b, 0x62, 0xc8,
+ 0x47, 0x67, 0x89, 0xab, 0xa3, 0xae, 0xf9, 0x92, 0xc3, 0x82, 0x64, 0x2a,
+ 0x63, 0x01, 0x91, 0xd3, 0x08, 0xca, 0xe7, 0x36, 0x17, 0x77, 0xbb, 0xee,
+ 0xd0, 0x43, 0x3f, 0x4e, 0x53, 0xc7, 0x2a, 0x55, 0x0f, 0x03, 0xdd, 0xb2,
+ 0x30, 0xb8, 0x1c, 0x66, 0x95, 0x5c, 0x93, 0x39, 0xba, 0x4d, 0xc6, 0xe2,
+ 0xd2, 0xf0, 0x67, 0x2d, 0x1d, 0xea, 0xe3, 0xe7, 0x21, 0x0c, 0x1d, 0x84,
+ 0xaf, 0xe0, 0xf1, 0x2a, 0x76, 0x1f, 0x9c, 0x93, 0xfc, 0x50, 0x5b, 0xea,
+ 0x73, 0x82, 0xa2, 0x96, 0xa1, 0x6e, 0x8b, 0xd3, 0xbe, 0x95, 0x5d, 0xd7,
+ 0x0a, 0xe4, 0xc4, 0xc6, 0x6b, 0x5f, 0xd3, 0xe9, 0x50, 0x7e, 0xc0, 0x5d,
+ 0x5b, 0xb6, 0x3e, 0x9c, 0xad, 0x74, 0x1b, 0xe1, 0xbf, 0xa3, 0x2c, 0x8e,
+ 0xb0, 0x99, 0x52, 0x5f, 0x51, 0x52, 0x68, 0x91, 0x96, 0x4c, 0xed, 0xd3,
+ 0x37, 0x7c, 0xb1, 0x8a, 0x75, 0xb7, 0xfc, 0xff, 0x1e, 0xfd, 0x60, 0xe0,
+ 0x09, 0x8b, 0xdb, 0x60, 0xa6, 0xde, 0x34, 0x1d, 0x9b, 0x0a, 0xf3, 0x49,
+ 0xd5, 0xf2, 0xbb, 0xa7, 0xee, 0xce, 0x2e, 0x45, 0xca, 0xc0, 0x5e, 0x95,
+ 0xfe, 0xb3, 0xc6, 0xc6, 0xde, 0x22, 0xfd, 0x91, 0x7a, 0x4a, 0x3c, 0x7d,
+ 0xec, 0xb0, 0x21, 0xd4, 0xb7, 0xd2, 0x42, 0x9b, 0x8b, 0xa8, 0x2f, 0x60,
+ 0xa2, 0x78, 0x03, 0x47, 0x8e, 0x01, 0x57, 0xe7, 0x7e, 0xf0, 0x91, 0xb5,
+ 0xfb, 0x86, 0xd4, 0xc1, 0x1c, 0xf6, 0xf9, 0x88, 0x30, 0xbb, 0x04, 0x4a,
+ 0x52, 0xfb, 0xd3, 0x08, 0x0a, 0xa8, 0x97, 0x2c, 0xad, 0xa5, 0xf2, 0xf4,
+ 0x63, 0x65, 0x17, 0x53, 0xa7, 0xeb, 0x6d, 0x93, 0xb8, 0xbc, 0x1d, 0xc9,
+ 0x17, 0x59, 0x7e, 0x3f, 0x7c, 0xf0, 0x27, 0x2e, 0xe8, 0xe4, 0x9e, 0x0c,
+ 0xcd, 0xe4, 0x9a, 0xbc, 0x7e, 0xcf, 0xf5, 0x46, 0xb7, 0x01, 0x3e, 0x73,
+ 0xd2, 0x32, 0x77, 0x1c, 0x97, 0x54, 0xe9, 0xdb, 0xc7, 0x20, 0xc1, 0xb7,
+ 0xc9, 0xd0, 0x55, 0x6f, 0xb9, 0x2e, 0x58, 0x63, 0xd6, 0xc0, 0x0c, 0x86,
+ 0xe8, 0xcf, 0x55, 0x02, 0xde, 0x2e, 0x8a, 0xe0, 0x26, 0x23, 0xd3, 0x43,
+ 0xb8, 0x78, 0x79, 0x16, 0x11, 0x0d, 0xc6, 0x17, 0x8b, 0x89, 0xf6, 0x4a,
+ 0xfd, 0xc1, 0xd7, 0x6b, 0x25, 0xa4, 0xde, 0x3d, 0xd4, 0xea, 0x08, 0x38,
+ 0xbb, 0x51, 0x6f, 0x4f, 0xb7, 0x74, 0xd9, 0x22, 0x63, 0x90, 0x79, 0x6b,
+ 0x7a, 0x4a, 0xde, 0x68, 0xb1, 0x75, 0x1c, 0x7e, 0xb5, 0xd4, 0x3a, 0x43,
+ 0xc2, 0xdb, 0x42, 0x53, 0x4f, 0xe4, 0xad, 0xfc, 0xdf, 0x56, 0xc6, 0xbb,
+ 0x5c, 0x5c, 0x2f, 0xaf, 0xdb, 0xed, 0xb3, 0x08, 0xd3, 0x75, 0x56, 0x9d,
+ 0xb1, 0x6c, 0x45, 0x55, 0x7e, 0x92, 0xd0, 0x17, 0xb8, 0x2a, 0x14, 0xd2,
+ 0x6d, 0x15, 0x91, 0xa0, 0x27, 0x55, 0x10, 0xf6, 0x90, 0xba, 0x26, 0x22,
+ 0x0c, 0x2a, 0xa3, 0xbf, 0xf3, 0xac, 0x6e, 0x0f, 0x3f, 0xfb, 0xa8, 0x3d,
+ 0x06, 0xb9, 0x32, 0x06, 0xb6, 0x9e, 0x84, 0x35, 0x91, 0xff, 0xe5, 0x4e,
+ 0x53, 0x1c, 0xe1, 0x43, 0x90, 0x7a, 0x61, 0x4d, 0x08, 0xf3, 0xae, 0xb5,
+ 0xde, 0x13, 0xb4, 0x6a, 0x11, 0x6d, 0xcc, 0x50, 0x99, 0xf7, 0xf7, 0x33,
+ 0xf9, 0x56, 0x64, 0x27, 0x71, 0x42, 0xc2, 0x21, 0x12, 0x76, 0x2a, 0xb0,
+ 0x12, 0xb1, 0x72, 0x28, 0xdb, 0xc3, 0xdf, 0x0a, 0x42, 0xf6, 0x40, 0xd8,
+ 0xbb, 0x34, 0x8c, 0x0f, 0xad, 0x17, 0xe7, 0xba, 0x22, 0x7f, 0x4e, 0x72,
+ 0xd2, 0x56, 0xbc, 0x93, 0x28, 0x3a, 0x37, 0x33, 0x8f, 0x22, 0xc0, 0x18,
+ 0x1e, 0x8b, 0x3f, 0xf7, 0x43, 0x89, 0x09, 0x1d, 0x6d, 0xba, 0xcb, 0x1d,
+ 0xee, 0x4d, 0x9f, 0x45, 0xbe, 0x24, 0x24, 0x86, 0xd3, 0x4c, 0xb3, 0xef,
+ 0x77, 0x5b, 0xa8, 0x66, 0xcb, 0x7a, 0x2f, 0x88, 0xd0, 0x98, 0xb4, 0x51,
+ 0x7e, 0x26, 0x6b, 0x50, 0x1d, 0xa9, 0xe4, 0xe4, 0x17, 0x90, 0xc2, 0x89,
+ 0x5b, 0xc5, 0xd5, 0x9f, 0x9b, 0x1d, 0x66, 0x0f, 0xec, 0x83, 0x91, 0x6e,
+ 0x6c, 0x20, 0x3f, 0x55, 0xea, 0xd7, 0x4e, 0x76, 0xc9, 0x55, 0xf1, 0xbc,
+ 0xbc, 0x9e, 0x9d, 0x3b, 0x63, 0x7c, 0x71, 0xbd, 0x6f, 0x2f, 0xfe, 0xe0,
+ 0x19, 0x8e, 0xef, 0xca, 0x19, 0x8b, 0xf7, 0x00, 0xa5, 0x08, 0x6a, 0xf1,
+ 0xaa, 0xf3, 0x4e, 0x78, 0x36, 0xbd, 0x8f, 0xdf, 0x36, 0x30, 0xe3, 0xd3,
+ 0xdb, 0x24, 0x21, 0x3c, 0xe3, 0x18, 0xb4, 0x0d, 0xc5, 0x04, 0x88, 0x48,
+ 0x82, 0xe3, 0x06, 0x45, 0xe1, 0x6f, 0xe5, 0x2d, 0x6c, 0x54, 0x65, 0x6c,
+ 0x61, 0xac, 0xf8, 0x96, 0x3c, 0xf9, 0x31, 0xa5, 0x78, 0xb1, 0xd3, 0x96,
+ 0x01, 0xf0, 0x14, 0xc2, 0xbb, 0xdd, 0x3e, 0x4f, 0x85, 0xb2, 0xa3, 0x81,
+ 0xff, 0x14, 0xf0, 0xec, 0x41, 0x4a, 0x5e, 0x63, 0x33, 0xd3, 0x9b, 0xce,
+ 0x9d, 0x26, 0x47, 0x17, 0x2e, 0x33, 0xfb, 0xb0, 0x72, 0x49, 0x42, 0x64,
+ 0xe2, 0xd9, 0x41, 0xc1, 0x3c, 0x4b, 0x7e, 0xe7, 0x39, 0xd6, 0x11, 0xc9,
+ 0x2b, 0x32, 0x6a, 0x39, 0xe8, 0x74, 0xf1, 0x40, 0x2f, 0xdf, 0x77, 0x19,
+ 0x32, 0x75, 0x79, 0xb2, 0x5e, 0x55, 0x40, 0xce, 0x39, 0x16, 0x3b, 0xf8,
+ 0x6d, 0x59, 0x7f, 0x2a, 0xa4, 0x00, 0xd7, 0x28, 0x98, 0x61, 0x77, 0xed,
+ 0xff, 0xd7, 0x24, 0xc7, 0x4a, 0x9c, 0xd2, 0x59, 0xaa, 0x30, 0x17, 0xdf,
+ 0x3c, 0xb9, 0x82, 0x27, 0xaf, 0x2d, 0xa3, 0x15, 0x52, 0xb7, 0x25, 0x83,
+ 0x60, 0xed, 0x37, 0xf1, 0x45, 0x4b, 0x9c, 0xf5, 0x09, 0xa9, 0x1b, 0xbc,
+ 0x13, 0x47, 0xd5, 0x8b, 0x03, 0xd3, 0x0d, 0x51, 0xb2, 0x62, 0x5b, 0x69,
+ 0x66, 0x8a, 0x7b, 0xfb, 0x43, 0x6b, 0x76, 0x88, 0x5b, 0x6e, 0x6e, 0x6b,
+ 0x2f, 0x4c, 0x95, 0x25, 0x2e, 0x6f, 0x55, 0xf8, 0x4c, 0xdb, 0x3f, 0x51,
+ 0x34, 0x78, 0x3c, 0xc7, 0x2a, 0x10, 0xa7, 0x1b, 0x28, 0x80, 0x56, 0xc9,
+ 0x0c, 0x03, 0xdd, 0xe7, 0x67, 0x44, 0xfe, 0x7f, 0xdc, 0xcd, 0x14, 0x15,
+ 0xcb, 0xe0, 0x74, 0x38, 0xc9, 0xc7, 0x1c, 0xe2, 0x9f, 0x81, 0xab, 0xfc,
+ 0x02, 0x4c, 0xb4, 0x6c, 0x86, 0x1d, 0x73, 0x4b, 0x77, 0x4e, 0x35, 0x78,
+ 0xb5, 0x68, 0x60, 0xc2, 0x62, 0xf0, 0xa5, 0xa3, 0x0b, 0xf1, 0xea, 0x33,
+ 0xea, 0x95, 0x24, 0x06, 0x6f, 0xca, 0x19, 0x32, 0xfa, 0xbe, 0xbe, 0x80,
+ 0xda, 0x43, 0xac, 0x51, 0xd1, 0xa8, 0xc1, 0x6e, 0xc6, 0xd8, 0xaf, 0xaa,
+ 0xde, 0x1c, 0xf4, 0xfb, 0x49, 0x2d, 0xea, 0xa6, 0x9d, 0x47, 0x92, 0x0b,
+ 0x6d, 0x9c, 0x75, 0xda, 0xd2, 0x6d, 0x64, 0xbd, 0x08, 0x13, 0x38, 0x3c,
+ 0xfd, 0x57, 0x5b, 0x0b, 0x05, 0xb4, 0x7c, 0x7a, 0x46, 0x5c, 0x6e, 0x24,
+ 0x7a, 0x96, 0xb1, 0x57, 0x4f, 0x62, 0x43, 0x00, 0x13, 0x6b, 0x9d, 0x30,
+ 0xa9, 0x35, 0x5a, 0x4a, 0xd0, 0x2a, 0xd1, 0xcf, 0xdd, 0xc8, 0x14, 0x41,
+ 0x7c, 0x78, 0xad, 0xf7, 0xa4, 0x5d, 0x32, 0x38, 0x6a, 0x41, 0xc9, 0xd2,
+ 0xe3, 0xc0, 0x9d, 0xf5, 0x84, 0x25, 0x2b, 0xfe, 0x09, 0x99, 0xdc, 0x82,
+ 0x55, 0x86, 0x2d, 0x08, 0x6a, 0xee, 0x1f, 0xb6, 0xa0, 0xb3, 0xfd, 0x3f,
+ 0x23, 0x0b, 0xa4, 0x30, 0x96, 0xa0, 0x70, 0xda, 0xd9, 0xda, 0xb6, 0xbf,
+ 0xa6, 0x19, 0x00, 0x06, 0xa4, 0x6c, 0xb5, 0xb0, 0x46, 0x71, 0x0d, 0x98,
+ 0x60, 0x85, 0xbd, 0x45, 0x84, 0x86, 0xc9, 0x18, 0x62, 0xed, 0x4d, 0x8a,
+ 0x23, 0x34, 0xd0, 0x00, 0xdc, 0x43, 0x7c, 0xa6, 0x94, 0x65, 0x58, 0x64,
+ 0x41, 0xa2, 0xd8, 0x40, 0xc0, 0x74, 0x90, 0x9b, 0xda, 0x0c, 0xdc, 0x94,
+ 0x7b, 0x4b, 0xd7, 0x6e, 0x42, 0xbe, 0x99, 0xac, 0x17, 0x58, 0x71, 0x03,
+ 0x88, 0x1f, 0xe3, 0xc9, 0xac, 0x69, 0x10, 0xf4, 0x2d, 0xf7, 0xd3, 0x9b,
+ 0xb5, 0xa4, 0xcd, 0xf7, 0xa2, 0xdc, 0x04, 0x82, 0x32, 0x12, 0x18, 0x83,
+ 0x35, 0xd3, 0x0e, 0xca, 0xa8, 0x6c, 0x5d, 0x5c, 0x78, 0xce, 0x4c, 0xa9,
+ 0x13, 0x8d, 0xce, 0x3f, 0xac, 0xc8, 0x00, 0x18, 0x9f, 0x34, 0xab, 0xc3,
+ 0x8e, 0xe3, 0x4d, 0x58, 0xbc, 0x79, 0xa2, 0x04, 0x97, 0x6c, 0x25, 0x38,
+ 0x9b, 0x45, 0x28, 0x39, 0x92, 0x57, 0x13, 0xac, 0x55, 0xb8, 0x63, 0x09,
+ 0xc1, 0x75, 0x61, 0x3a, 0x13, 0x20, 0xf4, 0x7c, 0x74, 0xa2, 0xcf, 0xc2,
+ 0x5d, 0x18, 0xe2, 0x91, 0x24, 0x4f, 0xa3, 0x6e, 0x0a, 0x70, 0x8d, 0x6f,
+ 0x8f, 0xd3, 0xa6, 0x2e, 0x32, 0x4c, 0x44, 0x40, 0xd0, 0xf1, 0xe2, 0xe9,
+ 0xb5, 0xf9, 0x14, 0xe5, 0x73, 0x4d, 0x80, 0xc4, 0x5c, 0xc0, 0x32, 0xd4,
+ 0xb1, 0x9a, 0xf6, 0x94, 0x3a, 0xc8, 0x04, 0x61, 0xb5, 0xb3, 0x5e, 0xbb,
+ 0x44, 0x8e, 0xdf, 0x4e, 0xb3, 0x2a, 0xad, 0xed, 0xdd, 0x9e, 0x71, 0xf6,
+ 0x48, 0x41, 0xb5, 0xdb, 0xf8, 0x81, 0x28, 0x81, 0x29, 0xf0, 0x8e, 0xb9,
+ 0xb7, 0xb3, 0x08, 0x60, 0x99, 0xb8, 0xef, 0x97, 0x8c, 0x9a, 0x65, 0x7a,
+ 0x3b, 0x13, 0x38, 0xbd, 0x07, 0x80, 0xc2, 0xba, 0x31, 0x4c, 0x56, 0x10,
+ 0x2f, 0x87, 0xbd, 0x9c, 0xb5, 0x13, 0x90, 0x83, 0x34, 0x2e, 0x65, 0xb4,
+ 0x0c, 0x84, 0xd1, 0x93, 0x4e, 0xaa, 0x26, 0x76, 0xf2, 0xc0, 0xdd, 0xe0,
+ 0x90, 0x16, 0x02, 0x1f, 0x3c, 0xbe, 0x51, 0xf6, 0xbf, 0xbc, 0xca, 0x22,
+ 0x46, 0xdb, 0x2b, 0xb9, 0x4f, 0x23, 0xe9, 0xee, 0x7c, 0x8f, 0x33, 0xab,
+ 0x4f, 0x98, 0xa8, 0xe5, 0x0e, 0xb9, 0x5a, 0x38, 0x9a, 0xae, 0xca, 0x9c,
+ 0xbe, 0x76, 0x28, 0xb6, 0xfd, 0x42, 0x14, 0x07, 0xbe, 0x2a, 0xdb, 0xab,
+ 0x0f, 0x03, 0x80, 0xfe, 0x78, 0x30, 0x5d, 0xea, 0x66, 0x19, 0xcf, 0x35,
+ 0x6d, 0x25, 0xe5, 0x55, 0xeb, 0x79, 0xd1, 0x2d, 0x97, 0x81, 0x7c, 0x15,
+ 0xfc, 0xfa, 0x5d, 0xbf, 0x85, 0xe0, 0x06, 0x4e, 0xf5, 0x36, 0xd6, 0x31,
+ 0x62, 0x11, 0x5f, 0xb8, 0x2b, 0x08, 0xdd, 0x50, 0x60, 0x91, 0x45, 0x0d,
+ 0xbd, 0xa7, 0x0a, 0x2a, 0xdf, 0x44, 0xa6, 0x07, 0xf4, 0x53, 0xe1, 0x40,
+ 0xa2, 0xcc, 0xa8, 0xa8, 0x86, 0x76, 0x64, 0xc8, 0xd8, 0x6c, 0x48, 0x0b,
+ 0x38, 0xbf, 0xe2, 0x13, 0xd0, 0xaa, 0xc4, 0xff, 0x83, 0xc0, 0x5e, 0xda,
+ 0x69, 0x38, 0xf3, 0x5b, 0xba, 0x82, 0x48, 0xa8, 0x28, 0x0d, 0xb2, 0xc6,
+ 0x77, 0xdc, 0x37, 0x8e, 0xb4, 0x94, 0x22, 0xa1, 0x42, 0xa9, 0xf3, 0x3e,
+ 0x3c, 0x1e, 0x75, 0x43, 0xdc, 0x0b, 0x9c, 0x77, 0x84, 0x2f, 0xf5, 0x27,
+ 0xd8, 0xcb, 0x0f, 0x5a, 0x87, 0x30, 0x52, 0x71, 0x71, 0xa0, 0x6d, 0xf3,
+ 0xaf, 0x2e, 0x68, 0xed, 0xa2, 0x6e, 0x10, 0x2c, 0xc6, 0xdb, 0xc0, 0x77,
+ 0x52, 0xbc, 0xe2, 0xa3, 0x69, 0x5f, 0x2f, 0x0f, 0x68, 0xd9, 0x2d, 0x29,
+ 0x3e, 0xf0, 0x83, 0x6f, 0x05, 0xca, 0xc9, 0x2a, 0x72, 0x86, 0x95, 0xbc,
+ 0xde, 0xab, 0x44, 0x7f, 0xbc, 0x2a, 0xad, 0xa8, 0x29, 0x03, 0x3e, 0xd4,
+ 0x0e, 0x3d, 0x2f, 0xde, 0x0e, 0xc4, 0xcf, 0x61, 0x47, 0xb7, 0x29, 0x77,
+ 0xd1, 0x58, 0x43, 0xf8, 0xa2, 0xaa, 0x1d, 0xb1, 0x8d, 0x6f, 0x31, 0xb3,
+ 0x5e, 0xab, 0x83, 0x55, 0xa2, 0xe0, 0xab, 0x60, 0x9e, 0x31, 0x0b, 0xf6,
+ 0x9c, 0x9e, 0x13, 0x4f, 0xe7, 0x26, 0xc3, 0x97, 0x89, 0xea, 0x60, 0x1d,
+ 0xeb, 0xd9, 0xfc, 0x11, 0xdf, 0xde, 0xe7, 0xd2, 0x3b, 0x65, 0xd8, 0x19,
+ 0x8e, 0xbd, 0x4d, 0xaf, 0xcc, 0xa5, 0x46, 0x02, 0x0c, 0xd7, 0x9f, 0x2f,
+ 0xf7, 0xf3, 0x38, 0xb5, 0x01, 0x56, 0xc4, 0xef, 0x0a, 0x19, 0xca, 0x28,
+ 0x1a, 0xc0, 0x1a, 0x55, 0x64, 0x81, 0x19, 0xf6, 0x52, 0xeb, 0xef, 0x70,
+ 0xa8, 0xfb, 0xa2, 0xb3, 0xae, 0x57, 0xd1, 0x4c, 0x3c, 0xc1, 0x82, 0x49,
+ 0x39, 0x04, 0x19, 0xdd, 0xab, 0xb6, 0x31, 0x7c, 0xde, 0x9d, 0xee, 0xc9,
+ 0xf6, 0xbd, 0x5c, 0xf4, 0xff, 0xfb, 0x5c, 0xf2, 0x02, 0x08, 0x41, 0x4f,
+ 0xff, 0xa9, 0xb2, 0x95, 0x0b, 0x01, 0xa9, 0x3c, 0xd3, 0x71, 0x48, 0xef,
+ 0xf1, 0x51, 0xea, 0xe8, 0xc1, 0xff, 0x62, 0xb2, 0x95, 0xc7, 0xd6, 0xc1,
+ 0xc7, 0xaa, 0xd6, 0x05, 0xd7, 0x64, 0xe7, 0x6e, 0x18, 0xbb, 0x51, 0xa2,
+ 0x7f, 0x19, 0x0f, 0x60, 0x2a, 0x3c, 0xf9, 0x70, 0xad, 0xb6, 0xe0, 0xa7,
+ 0xb1, 0x21, 0x9c, 0x45, 0xa9, 0x4f, 0x2b, 0xda, 0x53, 0x32, 0x07, 0x46,
+ 0x12, 0xb6, 0xf9, 0x97, 0x8a, 0x39, 0xc7, 0xc7, 0xb8, 0x19, 0xca, 0x18,
+ 0x74, 0xcc, 0x10, 0xef, 0x4d, 0x12, 0x90, 0xfd, 0xbb, 0xd8, 0x1b, 0x3d,
+ 0x31, 0x34, 0x79, 0x1d, 0x0b, 0x8c, 0x5e, 0xfd, 0x99, 0xbf, 0x78, 0x56,
+ 0x7b, 0x25, 0x24, 0x4f, 0x4b, 0xda, 0x8d, 0xbf, 0xd6, 0x5f, 0xb7, 0xfa,
+ 0x63, 0xf1, 0x99, 0xa7, 0x63, 0x39, 0x82, 0x1a, 0x79, 0x5b, 0x0c, 0xc4,
+ 0xcd, 0xfd, 0xfb, 0x66, 0xdc, 0xc7, 0x22, 0xa8, 0x44, 0x19, 0xc5, 0xf3,
+ 0xeb, 0x44, 0xbb, 0x98, 0xc0, 0x47, 0xc3, 0xa3, 0x0c, 0x12, 0x82, 0xd8,
+ 0x75, 0x66, 0x50, 0x1f, 0xae, 0xe0, 0x05, 0x5a, 0x42, 0x78, 0x9a, 0xbf,
+ 0x70, 0xd5, 0x20, 0xae, 0xb1, 0xf5, 0x91, 0xe4, 0x33, 0x46, 0x58, 0x6a,
+ 0xf8, 0x7c, 0xe5, 0x06, 0x98, 0xbc, 0xdb, 0xd6, 0xfd, 0xcb, 0xee, 0x6b,
+ 0x6b, 0x8b, 0xd4, 0x96, 0x25, 0xbd, 0xe8, 0x44, 0xb0, 0x62, 0xcb, 0xaf,
+ 0xad, 0x11, 0x55, 0xe7, 0x69, 0xe2, 0x6a, 0x78, 0x44, 0xe2, 0x93, 0xdc,
+ 0x0f, 0x4e, 0x63, 0x18, 0x69, 0xda, 0xe8, 0xcc, 0xae, 0x35, 0x20, 0x9a,
+ 0xd7, 0x49, 0xe7, 0xf0, 0xf7, 0x94, 0x10, 0xea, 0x0f, 0x2c, 0x4e, 0x1b,
+ 0xfe, 0x0d, 0xc5, 0xc1, 0x0f, 0x88, 0x84, 0x99, 0x34, 0x8d, 0xd8, 0x25,
+ 0xfa, 0x88, 0x84, 0x29, 0x98, 0xd1, 0x6d, 0x51, 0x70, 0xd5, 0x58, 0x7c,
+ 0x65, 0xd8, 0x0c, 0x53, 0xd5, 0x78, 0xb2, 0x7c, 0x0d, 0x60, 0x0a, 0x82,
+ 0x37, 0x20, 0x12, 0x9c, 0x37, 0x20, 0xca, 0xdc, 0xbd, 0xfd, 0xbf, 0xe8,
+ 0xdf, 0xe3, 0x16, 0x43, 0x4d, 0x1d, 0xb5, 0x3e, 0x75, 0x83, 0xfb, 0x43,
+ 0x78, 0x46, 0xe6, 0x9c, 0x00, 0x18, 0xde, 0xa6, 0xdc, 0xfa, 0x57, 0x3c,
+ 0x7b, 0x98, 0x0e, 0xc9, 0x39, 0x1e, 0x01, 0xc1, 0x7d, 0x5f, 0x29, 0x47,
+ 0x40, 0xf4, 0x1c, 0x56, 0x0f, 0x02, 0xc0, 0xa2, 0xde, 0x1a, 0x3c, 0x9f,
+ 0x64, 0x1a, 0x8c, 0x42, 0x16, 0x96, 0xb0, 0x30, 0x25, 0x99, 0xb5, 0xbf,
+ 0xf7, 0x4e, 0xce, 0x02, 0xd0, 0x54, 0x55, 0x29, 0xa3, 0xf6, 0xf0, 0x6d,
+ 0xc8, 0x10, 0x38, 0x96, 0x2d, 0xfa, 0x3e, 0x30, 0xae, 0xa0, 0xea, 0x0f,
+ 0x48, 0x73, 0x93, 0x69, 0x94, 0x4e, 0xe8, 0x44, 0xdc, 0x16, 0xe4, 0xad,
+ 0x9b, 0xa1, 0x38, 0x09, 0x82, 0x9d, 0x4b, 0x7c, 0xf5, 0x32, 0x95, 0x72,
+ 0x27, 0x43, 0x0e, 0x86, 0x00, 0x0e, 0x23, 0x94, 0x5c, 0x85, 0x66, 0xe3,
+ 0x39, 0x79, 0xa3, 0x70, 0xb5, 0x47, 0x94, 0x0e, 0x11, 0xbb, 0x6b, 0x51,
+ 0x4d, 0x91, 0xc6, 0xc8, 0x2d, 0xab, 0x4c, 0xfc, 0x52, 0x42, 0x99, 0x74,
+ 0x13, 0x86, 0xb2, 0x1f, 0x1c, 0x70, 0x71, 0x36, 0xbf, 0xa5, 0x92, 0x38,
+ 0xda, 0x33, 0x8e, 0x97, 0x89, 0xae, 0xf0, 0xd1, 0x06, 0x6f, 0x86, 0xc9,
+ 0x5d, 0x2a, 0xe7, 0x80, 0x04, 0x94, 0xdc, 0xdb, 0xd5, 0x5a, 0x6d, 0x0c,
+ 0xac, 0x84, 0x07, 0xfd, 0xcb, 0xf5, 0xa7, 0x7c, 0x96, 0xc5, 0x4b, 0xd6,
+ 0x00, 0x43, 0x9e, 0x34, 0x27, 0x56, 0x01, 0x18, 0x8e, 0x05, 0xe5, 0x20,
+ 0x38, 0xd0, 0x67, 0x44, 0xe0, 0xc5, 0x9f, 0x10, 0x48, 0x9d, 0x6a, 0xf5,
+ 0x79, 0x84, 0x83, 0x32, 0xc3, 0x61, 0xf5, 0x80, 0xd9, 0xbf, 0xfe, 0x7e,
+ 0xca, 0xcc, 0xad, 0x8d, 0x40, 0x58, 0x62, 0xc6, 0xe5, 0x24, 0x17, 0x75,
+ 0x58, 0x7d, 0x48, 0x85, 0xc9, 0xac, 0xc6, 0x1e, 0x36, 0x83, 0x76, 0xcf,
+ 0xc3, 0x9e, 0xb2, 0xd6, 0xc5, 0x76, 0x4e, 0x67, 0xfd, 0x24, 0xcd, 0xb6,
+ 0x7e, 0xf7, 0x92, 0x2a, 0x30, 0x9d, 0xfd, 0x36, 0x81, 0x9a, 0xe6, 0x25,
+ 0x31, 0x1f, 0xb8, 0x52, 0xb9, 0x7b, 0x56, 0xf5, 0xff, 0x9a, 0xdf, 0x5a,
+ 0x1a, 0xf8, 0xa1, 0x30, 0xc5, 0x8c, 0x0d, 0x3c, 0xc4, 0x5f, 0x33, 0xaf,
+ 0x36, 0xe3, 0x8e, 0x4e, 0x45, 0x22, 0xa4, 0xfa, 0x5f, 0x34, 0x7a, 0x42,
+ 0x50, 0x14, 0x23, 0xa2, 0x2f, 0xb6, 0xea, 0xc6, 0xe3, 0xd7, 0xa6, 0x73,
+ 0x83, 0xb3, 0xdc, 0xd6, 0x1b, 0x65, 0xa8, 0xae, 0xc6, 0xc2, 0xe6, 0x10,
+ 0xaf, 0x46, 0x5c, 0x8e, 0xbe, 0x88, 0x11, 0xae, 0x34, 0xe5, 0x95, 0xf0,
+ 0x49, 0x08, 0x1c, 0xfa, 0x30, 0xb6, 0x07, 0x4b, 0x71, 0x6e, 0x88, 0xaa,
+ 0xf2, 0xfe, 0xc1, 0x01, 0x42, 0xba, 0x38, 0x50, 0xb8, 0xdf, 0x14, 0x11,
+ 0x5f, 0xf8, 0x58, 0x19, 0x9d, 0x18, 0xa2, 0x0d, 0x0a, 0x7d, 0x3e, 0xed,
+ 0x84, 0xfa, 0xb4, 0x17, 0x78, 0x7f, 0xd3, 0x86, 0x0f, 0xd2, 0xa0, 0xbf,
+ 0xc6, 0xdf, 0xaa, 0xdc, 0x6c, 0xd0, 0x04, 0x84, 0x18, 0xe4, 0x0e, 0x2f,
+ 0x27, 0x50, 0x1f, 0xa0, 0xe4, 0x03, 0x65, 0xce, 0xd0, 0xdf, 0x42, 0x2b,
+ 0x69, 0x17, 0x9b, 0xa6, 0x27, 0xb7, 0xbc, 0x08, 0x26, 0xe5, 0x76, 0x82,
+ 0x69, 0x99, 0x7d, 0x7b, 0xc6, 0x4b, 0x61, 0xe5, 0xac, 0x21, 0x4b, 0xe1,
+ 0x7f, 0x05, 0x7a, 0x3f, 0xdb, 0x74, 0x21, 0x58, 0xe2, 0x49, 0x65, 0x6d,
+ 0xc6, 0xc0, 0x94, 0x15, 0x58, 0x90, 0x97, 0xc5, 0xe2, 0xaf, 0xe0, 0x2f,
+ 0x9d, 0xc5, 0xdb, 0xde, 0xc9, 0xbc, 0xc4, 0x11, 0xf3, 0xe4, 0x40, 0x2d,
+ 0xbe, 0x36, 0x59, 0x58, 0x0f, 0x10, 0x05, 0x6e, 0x8b, 0x12, 0x11, 0xfe,
+ 0xdb, 0x96, 0x9a, 0x2b, 0x9c, 0x72, 0xc3, 0xa4, 0xbd, 0xfa, 0x08, 0x83,
+ 0xed, 0x20, 0x2e, 0x0e, 0xd3, 0x80, 0x35, 0xb7, 0xda, 0xdd, 0xaa, 0xff,
+ 0xb1, 0x63, 0x67, 0x53, 0xf8, 0x6e, 0x75, 0x2f, 0x7d, 0x11, 0x1b, 0x4e,
+ 0xb8, 0x5e, 0x4f, 0xb3, 0x30, 0xc7, 0xfe, 0xc1, 0xa3, 0xc8, 0x3f, 0x3b,
+ 0x2d, 0x28, 0x92, 0xd0, 0xa8, 0x65, 0x5f, 0x1e, 0x4e, 0xf7, 0xf3, 0xe6,
+ 0x8f, 0x30, 0x7c, 0x55, 0x8a, 0x47, 0x4a, 0xb3, 0x86, 0xd5, 0x03, 0x5c,
+ 0x9b, 0x96, 0x54, 0xad, 0xee, 0x80, 0x7a, 0x76, 0xa6, 0x98, 0x65, 0x69,
+ 0xdf, 0x4f, 0x85, 0x63, 0xa4, 0xdf, 0xf1, 0xcc, 0x1a, 0x64, 0xa0, 0x8a,
+ 0x81, 0x09, 0x2c, 0xfd, 0x66, 0xc7, 0xb6, 0xd2, 0x41, 0x14, 0xeb, 0xad,
+ 0x25, 0xef, 0x72, 0xb1, 0xba, 0x69, 0xb1, 0x9b, 0x74, 0x8d, 0x00, 0xc6,
+ 0xd3, 0x10, 0x40, 0x8d, 0x7f, 0xf7, 0xdc, 0xf6, 0x29, 0x2d, 0xff, 0xe9,
+ 0xb5, 0x6a, 0x5d, 0x3b, 0x25, 0xb6, 0x24, 0xe1, 0x30, 0x97, 0xfc, 0xa4,
+ 0xf3, 0xf6, 0x2c, 0x7d, 0x47, 0x0a, 0xff, 0x20, 0x45, 0xd2, 0x58, 0x27,
+ 0x67, 0x53, 0x43, 0x2c, 0x58, 0xdd, 0x64, 0x5d, 0xb5, 0xd5, 0x47, 0x5a,
+ 0x9b, 0x6f, 0xb0, 0x9e, 0x33, 0x61, 0xa1, 0x15, 0xc8, 0x5b, 0x79, 0x16,
+ 0x79, 0x2c, 0x1a, 0xfd, 0x59, 0xf2, 0x7b, 0xd7, 0x8c, 0xa5, 0xf4, 0xbc,
+ 0x6b, 0x30, 0x7c, 0xe5, 0x70, 0xd0, 0x18, 0x6e, 0xf0, 0x5c, 0x67, 0x16,
+ 0xe1, 0x7e, 0xff, 0xc8, 0x2f, 0x11, 0x90, 0x7c, 0x1f, 0xf4, 0xd2, 0x21,
+ 0x0a, 0x74, 0x1a, 0x76, 0xb1, 0xad, 0xc3, 0xf8, 0x77, 0xff, 0xbb, 0x3a,
+ 0x89, 0x9a, 0xe6, 0x80, 0xed, 0xf4, 0x79, 0x5f, 0xa7, 0xef, 0xd8, 0x53,
+ 0x6e, 0x67, 0xce, 0xb6, 0xe4, 0x25, 0x2e, 0xf9, 0xaf, 0x60, 0x53, 0x95,
+ 0x31, 0xd8, 0x95, 0x9f, 0x4c, 0x9c, 0x2b, 0x55, 0x8b, 0x78, 0x0d, 0x17,
+ 0x96, 0xb0, 0xdf, 0xa3, 0x04, 0xa4, 0xc1, 0xe1, 0x4a, 0x0d, 0x9e, 0x4d,
+ 0xcb, 0x2d, 0x96, 0x83, 0xa9, 0xea, 0xd4, 0xcc, 0xf1, 0x78, 0x47, 0xed,
+ 0xb8, 0xbd, 0x29, 0x4c, 0x46, 0xc8, 0x68, 0x82, 0x6a, 0x17, 0xfa, 0x87,
+ 0x36, 0xf9, 0xe9, 0xa9, 0x52, 0xc3, 0x47, 0x7d, 0x3d, 0x69, 0xa3, 0x54,
+ 0xd8, 0x39, 0xd9, 0x57, 0x37, 0xa5, 0x6e, 0xbd, 0xb9, 0x08, 0x03, 0x52,
+ 0x84, 0xa5, 0xd4, 0x2d, 0x5c, 0xd1, 0xd2, 0x54, 0xad, 0xb5, 0xfa, 0x5a,
+ 0x6b, 0x69, 0xb6, 0xb0, 0x99, 0xaa, 0x73, 0xab, 0x6e, 0x24, 0xec, 0x63,
+ 0x6c, 0x3e, 0x33, 0x4b, 0x17, 0x1c, 0xd8, 0xac, 0xf9, 0x8d, 0xa7, 0xef,
+ 0xd2, 0x00, 0x8c, 0x84, 0xfa, 0x2d, 0x41, 0xce, 0xaf, 0x2b, 0x43, 0x49,
+ 0xa3, 0xae, 0xab, 0x81, 0xee, 0xc8, 0xf3, 0x02, 0xd7, 0x39, 0xdc, 0x43,
+ 0x68, 0xff, 0xf4, 0xba, 0xf0, 0xdd, 0x67, 0x6e, 0x4f, 0x50, 0x8c, 0xdb,
+ 0xb2, 0xb1, 0x9b, 0x7c, 0x45, 0xd4, 0x28, 0xc4, 0xff, 0x59, 0xe3, 0x6f,
+ 0x77, 0x2b, 0x1a, 0x69, 0xd6, 0x7f, 0xfe, 0x8d, 0x66, 0x3f, 0x83, 0x94,
+ 0xe5, 0xd9, 0x0d, 0x9a, 0xcb, 0x4e, 0x88, 0xb1, 0x22, 0xe0, 0xe3, 0xb2,
+ 0x3b, 0x26, 0x46, 0x1d, 0xb1, 0x19, 0x94, 0xc3, 0xf2, 0xcc, 0xcb, 0xa3,
+ 0xde, 0xb5, 0xee, 0xbf, 0x7a, 0x69, 0x59, 0xb7, 0x07, 0xb7, 0x79, 0x53,
+ 0x17, 0xb1, 0x63, 0xeb, 0xb9, 0x56, 0xe6, 0x08, 0x6c, 0x06, 0xa1, 0xec,
+ 0x9e, 0x48, 0x4e, 0xcb, 0xb2, 0xa2, 0xc7, 0x65, 0x87, 0x4d, 0x9d, 0x2a,
+ 0xfd, 0x27, 0xbf, 0x53, 0xca, 0xf4, 0x7f, 0x5d, 0xdf, 0x32, 0x0c, 0x55,
+ 0xe4, 0xff, 0xa3, 0x5f, 0xb2, 0xa4, 0xc4, 0x86, 0x16, 0x57, 0x79, 0xbb,
+ 0xb3, 0xf4, 0xb5, 0x32, 0x0b, 0x09, 0x02, 0x4b, 0x7c, 0x38, 0xa1, 0xc7,
+ 0x79, 0x19, 0x0b, 0xbb, 0x9b, 0xeb, 0xca, 0x86, 0x1c, 0x84, 0x43, 0x6e,
+ 0x02, 0x0e, 0x86, 0x20, 0xee, 0x47, 0x5d, 0x8c, 0xdd, 0xf9, 0x36, 0xf7,
+ 0x06, 0x97, 0x5c, 0x92, 0xe9, 0xe8, 0xb8, 0x5b, 0xf5, 0x99, 0x8c, 0x0d,
+ 0x54, 0x38, 0xcc, 0x73, 0xba, 0x99, 0xd1, 0x7b, 0x46, 0x4e, 0xfa, 0x10,
+ 0x43, 0x3d, 0x1f, 0xd8, 0xd2, 0x38, 0x87, 0x28, 0x85, 0x61, 0x38, 0x96,
+ 0xef, 0x6a, 0x48, 0xb0, 0x72, 0xb8, 0x6a, 0xcb, 0x91, 0xb6, 0x2d, 0x67,
+ 0x25, 0xbe, 0x64, 0x4d, 0xf4, 0x2e, 0x46, 0x2c, 0x68, 0x2c, 0xef, 0x8c,
+ 0x75, 0x9d, 0x6e, 0xde, 0xcd, 0xaa, 0xe3, 0x88, 0x2d, 0x98, 0x74, 0xae,
+ 0xa5, 0xbc, 0x0a, 0x80, 0x4b, 0x6b, 0x67, 0xfe, 0xc7, 0xcf, 0x8c, 0x4e,
+ 0x53, 0x4e, 0x52, 0xdf, 0x50, 0x79, 0x3a, 0x9e, 0xc6, 0xb6, 0x0c, 0x74,
+ 0xff, 0x18, 0xda, 0xb6, 0xc9, 0x40, 0x70, 0x27, 0xa8, 0x16, 0x70, 0x88,
+ 0x38, 0xb4, 0x0f, 0x6d, 0xf1, 0x5d, 0xe3, 0x1a, 0xeb, 0x27, 0x9a, 0x8b,
+ 0x39, 0xeb, 0xcf, 0xff, 0x7b, 0x66, 0xbe, 0x17, 0x53, 0xcb, 0x2f, 0x17,
+ 0xc3, 0xf3, 0x4b, 0x2b, 0xaf, 0x2c, 0x90, 0x32, 0x8a, 0x31, 0x49, 0x70,
+ 0x0a, 0xed, 0x7f, 0xad, 0xc6, 0xc9, 0xf4, 0x20, 0x4c, 0x08, 0x36, 0x17,
+ 0xbe, 0x97, 0x26, 0xff, 0x0d, 0x26, 0x5b, 0x76, 0x83, 0x79, 0x78, 0xa4,
+ 0x11, 0xf7, 0x90, 0xcf, 0x1c, 0x9d, 0xee, 0xa2, 0x1c, 0xc1, 0xc4, 0x57,
+ 0x9c, 0xb1, 0x1d, 0xc4, 0xfb, 0xf9, 0xa8, 0x87, 0x66, 0xe7, 0xa3, 0xf1,
+ 0xe1, 0x43, 0xb9, 0x16, 0x56, 0x7b, 0x52, 0x1d, 0xa0, 0xf4, 0x7b, 0xa9,
+ 0xee, 0x26, 0x16, 0x5a, 0x2f, 0xdb, 0xf9, 0x17, 0x30, 0x29, 0x47, 0xcc,
+ 0x98, 0x54, 0x8a, 0x74, 0xd8, 0x11, 0xab, 0xbc, 0x6b, 0xb3, 0x02, 0x55,
+ 0x8c, 0xe6, 0xf2, 0xa0, 0x41, 0xa3, 0xfb, 0xe8, 0x0d, 0xa9, 0x81, 0xea,
+ 0x50, 0x05, 0x4f, 0x56, 0x96, 0x18, 0x9a, 0x87, 0x98, 0x4e, 0xf3, 0x7d,
+ 0xbc, 0x37, 0x5d, 0xf6, 0xa1, 0x6c, 0x1b, 0x6c, 0x59, 0x75, 0x87, 0x41,
+ 0xb9, 0xa8, 0xe6, 0xff, 0xb9, 0x0a, 0x48, 0x38, 0x0c, 0x01, 0xe5, 0xf7,
+ 0xc5, 0x0d, 0x43, 0x76, 0xfc, 0xee, 0x89, 0xe5, 0xa1, 0x67, 0x01, 0x64,
+ 0x15, 0x45, 0x0e, 0xf5, 0x9c, 0xe7, 0xc4, 0x76, 0x4b, 0x3d, 0xc9, 0xd7,
+ 0x0e, 0x6c, 0x99, 0xa0, 0xd8, 0x50, 0x44, 0xdc, 0x3a, 0xcf, 0xf8, 0xbf,
+ 0x05, 0x33, 0x1c, 0xd2, 0xbd, 0xe0, 0x5f, 0xf5, 0x64, 0xb0, 0x6b, 0x94,
+ 0xec, 0x73, 0x06, 0xd7, 0x03, 0xef, 0xdf, 0xd0, 0xfe, 0x70, 0xf2, 0x23,
+ 0xb4, 0x47, 0xae, 0x6e, 0xcb, 0x23, 0xc4, 0x12, 0xe1, 0x72, 0xd0, 0xd0,
+ 0xa9, 0x67, 0x80, 0x4d, 0x8d, 0x8f, 0x92, 0x54, 0xce, 0xce, 0x81, 0xae,
+ 0x22, 0xf3, 0xaa, 0xec, 0x5f, 0x5b, 0xda, 0xed, 0xd5, 0xbc, 0x43, 0x1a,
+ 0x3d, 0xd0, 0x5f, 0xa2, 0xb8, 0x25, 0x6d, 0x1c, 0x21, 0x75, 0x32, 0xf7,
+ 0x35, 0x8f, 0xae, 0xc1, 0x1b, 0x6d, 0x50, 0xa0, 0x9b, 0xf9, 0x5e, 0xe6,
+ 0x2a, 0x08, 0xf7, 0x30, 0xac, 0xc4, 0xfb, 0x6b, 0x05, 0x48, 0x47, 0x3a,
+ 0x78, 0x0b, 0x36, 0x82, 0x89, 0xac, 0x91, 0x94, 0xd5, 0x87, 0xd8, 0x69,
+ 0x59, 0x7b, 0x6e, 0xb6, 0x60, 0xce, 0xa0, 0x56, 0x95, 0x74, 0x92, 0xb4,
+ 0x68, 0x4d, 0x04, 0x4b, 0xfe, 0xd7, 0xcf, 0x25, 0x68, 0x73, 0x7d, 0x6e,
+ 0xc7, 0xa1, 0x47, 0x5d, 0x95, 0xa1, 0x05, 0x06, 0x6d, 0xe4, 0x63, 0xdc,
+ 0x30, 0x7a, 0x19, 0xb3, 0xf2, 0x18, 0x32, 0xda, 0x09, 0xfb, 0x37, 0x96,
+ 0x7e, 0x5e, 0x1f, 0xf2, 0x43, 0xe5, 0xc0, 0xe1, 0xc0, 0xa2, 0x54, 0x49,
+ 0x2d, 0x3a, 0x91, 0x3c, 0x72, 0x90, 0xbf, 0x56, 0xc3, 0x00, 0x30, 0xcd,
+ 0xd0, 0x3c, 0x0d, 0xc3, 0xe7, 0x76, 0x79, 0xaa, 0x0a, 0xc4, 0x10, 0x1e,
+ 0x3d, 0x80, 0x6d, 0x8b, 0xfe, 0xc6, 0xed, 0x42, 0x37, 0x1d, 0x87, 0x4b,
+ 0x5d, 0x98, 0xe1, 0x1a, 0x1c, 0x08, 0x61, 0xc1, 0xe2, 0x2f, 0xaf, 0x8b,
+ 0xc2, 0x50, 0x72, 0xad, 0xe8, 0xe5, 0x56, 0x09, 0xb4, 0xce, 0xaf, 0xbc,
+ 0x96, 0x0b, 0xf5, 0x89, 0xdb, 0xc8, 0x71, 0x72, 0x80, 0x09, 0xcb, 0x3d,
+ 0x9b, 0xf5, 0x68, 0xbf, 0xfc, 0x90, 0x95, 0x8b, 0x82, 0x83, 0xce, 0x8a,
+ 0xc3, 0x52, 0x58, 0xfe, 0x45, 0x13, 0x76, 0x1c, 0x4b, 0xfc, 0xf7, 0xf1,
+ 0x1c, 0x51, 0xaa, 0x5c, 0x70, 0x92, 0xf9, 0x69, 0x47, 0x65, 0x72, 0xf1,
+ 0xa9, 0xf9, 0xc0, 0xb8, 0xe0, 0xca, 0xd9, 0x92, 0xc8, 0x92, 0x0e, 0x8c,
+ 0x3b, 0x70, 0x92, 0xa3, 0x71, 0xd3, 0xf5, 0x4c, 0x05, 0x95, 0x56, 0x98,
+ 0xac, 0x86, 0x93, 0x9a, 0xb6, 0xe4, 0x5a, 0x92, 0xa6, 0x70, 0xa4, 0x06,
+ 0x32, 0xc7, 0x67, 0x9d, 0xbd, 0xff, 0xa2, 0x94, 0x8d, 0xf2, 0xd7, 0xe4,
+ 0x00, 0x36, 0xa8, 0x6b, 0x12, 0x47, 0x8c, 0xb3, 0x48, 0x97, 0xbd, 0xe2,
+ 0x1f, 0x2f, 0xa1, 0x00, 0x41, 0xa5, 0x3f, 0xfc, 0xa1, 0xa2, 0xc7, 0x24,
+ 0x4b, 0xaf, 0xc8, 0x90, 0x0d, 0x51, 0x3a, 0x5b, 0x7e, 0xe5, 0xbf, 0xd0,
+ 0xc6, 0xb0, 0x20, 0x61, 0xdd, 0x09, 0x10, 0x48, 0x51, 0x3e, 0xdc, 0xb8,
+ 0x30, 0xba, 0x12, 0x31, 0x50, 0x75, 0x8e, 0xe3, 0x40, 0x5c, 0xa7, 0x14,
+ 0x6e, 0xa2, 0xce, 0x36, 0x83, 0x1d, 0xdf, 0xb8, 0xd4, 0xc7, 0x50, 0xb7,
+ 0xf8, 0x6a, 0x57, 0x37, 0x29, 0x93, 0x57, 0xc7, 0x75, 0x6f, 0x41, 0xf8,
+ 0x5f, 0xf3, 0x7d, 0x9d, 0x3d, 0x8c, 0xb6, 0xb4, 0x81, 0x08, 0xf3, 0x44,
+ 0xdd, 0x3f, 0x63, 0xa9, 0xcd, 0xa6, 0xd7, 0x21, 0xee, 0x2d, 0x41, 0xbd,
+ 0xa0, 0x83, 0x05, 0xbc, 0x1d, 0x92, 0xb4, 0x1d, 0x36, 0xfb, 0xef, 0x6c,
+ 0x9b, 0x68, 0x93, 0x52, 0x59, 0x16, 0x43, 0x14, 0x30, 0xc3, 0x8f, 0x9d,
+ 0x75, 0xd1, 0x3b, 0x19, 0xfd, 0x62, 0x81, 0xf3, 0x29, 0x8e, 0xe1, 0xf8,
+ 0xc1, 0xba, 0x9c, 0x66, 0x95, 0x93, 0x79, 0x34, 0x25, 0x1f, 0x03, 0xf1,
+ 0x06, 0x06, 0xfd, 0x1c, 0x17, 0x00, 0x8e, 0x21, 0x59, 0xae, 0xae, 0x71,
+ 0x80, 0xe9, 0xe5, 0x5d, 0x86, 0x2c, 0xce, 0xaa, 0x45, 0x7a, 0xdf, 0xe5,
+ 0x3f, 0xfb, 0xf0, 0x69, 0x6c, 0xcf, 0x30, 0x97, 0x5f, 0xe1, 0xb4, 0xe8,
+ 0x94, 0xf0, 0x1f, 0x70, 0x6e, 0x0a, 0x4c, 0xeb, 0x08, 0xdc, 0x6f, 0xd6,
+ 0xe7, 0x9c, 0x3c, 0x1b, 0x16, 0xa5, 0x02, 0xff, 0x26, 0x91, 0x99, 0x6d,
+ 0x1a, 0x17, 0x73, 0xfe, 0xa2, 0xdd, 0xf5, 0xf2, 0x42, 0xda, 0xa6, 0xe2,
+ 0x7a, 0x9f, 0xd3, 0x28, 0xc5, 0x0a, 0xa8, 0x1f, 0x45, 0x78, 0xe2, 0x1c,
+ 0x52, 0xdd, 0x9f, 0x72, 0xf5, 0x67, 0x83, 0x56, 0x0a, 0xc2, 0x11, 0x2a,
+ 0x92, 0x35, 0xb5, 0xc7, 0xdd, 0xbc, 0x5f, 0xd8, 0x3b, 0x95, 0xb0, 0x01,
+ 0xcd, 0x58, 0x4b, 0xd4, 0x7d, 0x0e, 0x5e, 0x91, 0xa7, 0x76, 0xdc, 0xc6,
+ 0xb9, 0x54, 0xa6, 0x45, 0xf4, 0xa2, 0xd3, 0x58, 0x35, 0x02, 0x3b, 0xa2,
+ 0x9b, 0x53, 0x5c, 0x31, 0xac, 0x1c, 0x91, 0x33, 0xf3, 0x8b, 0xb5, 0x2a,
+ 0x44, 0x1c, 0xd4, 0x4f, 0x64, 0xad, 0xda, 0x46, 0x9e, 0xba, 0x9a, 0x13,
+ 0xb6, 0xad, 0x81, 0x43, 0xdb, 0x1b, 0x91, 0xf6, 0x84, 0xec, 0xd0, 0x6b,
+ 0xcf, 0x06, 0xff, 0x08, 0xe5, 0x0b, 0x45, 0xa4, 0x5b, 0x44, 0x9b, 0xdc,
+ 0x3f, 0xb5, 0x4a, 0xb0, 0x12, 0x4f, 0xd7, 0xb0, 0xb0, 0x24, 0x07, 0xf9,
+ 0xf9, 0xc9, 0x16, 0x16, 0x58, 0x1b, 0x56, 0x66, 0x42, 0x3c, 0xee, 0x55,
+ 0xe0, 0xd5, 0xc6, 0xdb, 0xa5, 0x3b, 0x96, 0xff, 0x66, 0x19, 0xf8, 0xcd,
+ 0xd0, 0x7a, 0x28, 0xe2, 0x12, 0xb5, 0x6c, 0x6a, 0xdb, 0xc1, 0xd1, 0xf7,
+ 0x2d, 0x46, 0xe6, 0x57, 0x3e, 0x1b, 0x9e, 0xcd, 0x46, 0xe9, 0x59, 0xed,
+ 0x29, 0xc3, 0xfb, 0xf8, 0x9c, 0x97, 0xe2, 0x7e, 0xf3, 0x97, 0xef, 0x2d,
+ 0xc4, 0x65, 0x37, 0x38, 0x31, 0x62, 0xd7, 0x83, 0x20, 0xb3, 0xc8, 0xc1,
+ 0x38, 0xb3, 0x5e, 0x6a, 0x62, 0x90, 0x79, 0x81, 0x66, 0x39, 0x69, 0x23,
+ 0xed, 0x67, 0xdc, 0x28, 0x4d, 0x17, 0x5b, 0x50, 0xeb, 0xf8, 0x55, 0xd0,
+ 0x5d, 0x19, 0x39, 0x58, 0x3d, 0x2d, 0xfb, 0xc2, 0x37, 0x76, 0xe8, 0x94,
+ 0x65, 0xcc, 0x39, 0x5e, 0x50, 0xb2, 0xed, 0xa0, 0x1d, 0xdd, 0x0b, 0x3a,
+ 0x74, 0x24, 0xba, 0x17, 0x27, 0x4c, 0xf9, 0x1d, 0xae, 0xd1, 0x63, 0x37,
+ 0xcd, 0x07, 0x37, 0xcd, 0x89, 0xda, 0xa8, 0x3b, 0x21, 0xd1, 0x50, 0xd3,
+ 0xf6, 0x3b, 0x01, 0x03, 0xd2, 0x9e, 0xb8, 0x76, 0xe0, 0xeb, 0x43, 0xd4,
+ 0x6b, 0x22, 0x02, 0x16, 0x4b, 0x6c, 0x25, 0x65, 0x20, 0x86, 0x09, 0x61,
+ 0x73, 0xa3, 0x59, 0x12, 0x59, 0xf5, 0xe5, 0x4c, 0x14, 0xef, 0x9d, 0x3f,
+ 0xac, 0x5b, 0xad, 0x90, 0x0b, 0x85, 0x81, 0x7c, 0x63, 0x43, 0xc7, 0x28,
+ 0xe3, 0x9c, 0x04, 0xc5, 0x85, 0x45, 0xf0, 0x4a, 0x68, 0x76, 0x1c, 0x53,
+ 0xc4, 0xb4, 0xab, 0xde, 0x80, 0x96, 0x36, 0x6b, 0xd1, 0xfc, 0xa9, 0x38,
+ 0xc9, 0xf2, 0x72, 0x3c, 0x45, 0x37, 0xc7, 0xb2, 0xeb, 0xe2, 0x03, 0x34,
+ 0x76, 0x94, 0x2f, 0x69, 0x71, 0x41, 0xc8, 0x9e, 0x52, 0x7e, 0xf7, 0x5b,
+ 0x49, 0xd6, 0x77, 0x06, 0x7a, 0xd7, 0x04, 0x01, 0x46, 0x2e, 0x65, 0x10,
+ 0xd2, 0xee, 0x26, 0xdb, 0xb9, 0xf0, 0xbe, 0x5b, 0x92, 0x2d, 0xf6, 0x57,
+ 0xb2, 0x33, 0x25, 0x0a, 0xec, 0x45, 0xf6, 0xd4, 0xcb, 0x85, 0xe0, 0xc1,
+ 0x77, 0xe7, 0x7c, 0x02, 0x92, 0x98, 0xb1, 0x02, 0xd8, 0xcb, 0xe0, 0x38,
+ 0x50, 0xfc, 0x11, 0x2e, 0x8b, 0xc3, 0xdf, 0xa2, 0x91, 0x32, 0x07, 0xea,
+ 0x82, 0xc5, 0x4d, 0x67, 0x19, 0xe6, 0x45, 0x3f, 0xd0, 0xdc, 0x50, 0x87,
+ 0x9c, 0xdb, 0x25, 0x75, 0xd1, 0x47, 0x68, 0x60, 0xc0, 0x82, 0xd0, 0x23,
+ 0xbd, 0x48, 0x2e, 0x00, 0xc9, 0x8d, 0xfb, 0x92, 0x08, 0xda, 0xea, 0xfb,
+ 0x60, 0xb4, 0x7c, 0xb4, 0xe7, 0xec, 0x58, 0x76, 0x40, 0xa6, 0x6d, 0xe0,
+ 0x2e, 0x3c, 0xe0, 0xa8, 0x2b, 0x23, 0xbe, 0x67, 0x6b, 0x59, 0x1a, 0xa2,
+ 0x1d, 0x23, 0x05, 0xa2, 0xfa, 0x8c, 0x69, 0xbf, 0x80, 0x21, 0x43, 0x0c,
+ 0x85, 0x8a, 0x8f, 0x30, 0x99, 0x10, 0xf0, 0xc2, 0xf6, 0x8f, 0xfc, 0xaa,
+ 0x2f, 0x11, 0x9f, 0xea, 0xd0, 0xc2, 0xe3, 0xba, 0x05, 0x20, 0x2c, 0x3a,
+ 0x1b, 0xb0, 0xfb, 0x1e, 0x5f, 0x78, 0xac, 0xd5, 0xc4, 0x73, 0x26, 0x7a,
+ 0xa6, 0x6f, 0x56, 0xde, 0x5a, 0x63, 0x7d, 0xa2, 0x8e, 0x30, 0xd9, 0xaf,
+ 0x87, 0x54, 0xe6, 0xbd, 0x6a, 0x8c, 0x51, 0x32, 0x6f, 0x05, 0x83, 0xea,
+ 0x53, 0xc3, 0xba, 0xac, 0x4a, 0x2b, 0x87, 0x2e, 0x1f, 0x21, 0x77, 0x58,
+ 0x8b, 0x80, 0xe3, 0xff, 0x92, 0x41, 0x97, 0x0f, 0x76, 0x30, 0xe2, 0xe0,
+ 0x99, 0x90, 0xb2, 0xb1, 0x00, 0xb1, 0xaa, 0x79, 0x04, 0x4f, 0x34, 0xff,
+ 0x73, 0x35, 0x7f, 0x9d, 0x7d, 0x29, 0x1e, 0x45, 0x43, 0xb3, 0x46, 0xc6,
+ 0xa6, 0x39, 0xe1, 0x52, 0xe6, 0x8b, 0xd4, 0xa5, 0x71, 0x96, 0xd7, 0x0a,
+ 0x02, 0x23, 0xf8, 0xf3, 0x02, 0x03, 0x31, 0xe5, 0xa2, 0x81, 0x9b, 0xb0,
+ 0xb5, 0x12, 0x09, 0x6f, 0x30, 0x58, 0x2a, 0xeb, 0x3c, 0x03, 0x46, 0xe7,
+ 0x30, 0xb6, 0x21, 0x55, 0x30, 0x01, 0x0e, 0x94, 0xb9, 0x5f, 0xa6, 0x72,
+ 0xd1, 0xb3, 0x5e, 0xc4, 0xde, 0xf3, 0xbb, 0x95, 0x91, 0x19, 0x31, 0x82,
+ 0x7a, 0x63, 0xa1, 0xbf, 0xdf, 0x42, 0x2c, 0x90, 0x26, 0xad, 0xc9, 0xc8,
+ 0x78, 0x6d, 0x91, 0xdb, 0x34, 0x6e, 0x0b, 0x6f, 0x35, 0x7d, 0x6a, 0x2d,
+ 0x69, 0x11, 0x2f, 0x46, 0x64, 0x2f, 0xa8, 0xb6, 0xd6, 0xf0, 0x97, 0x8b,
+ 0x5c, 0x7d, 0xbb, 0x90, 0x10, 0xb8, 0x07, 0x26, 0xf9, 0x7e, 0xba, 0xf0,
+ 0xbf, 0x64, 0xf3, 0x6d, 0xa2, 0x14, 0xa3, 0xe6, 0x0e, 0x0b, 0xda, 0x64,
+ 0x26, 0x81, 0x64, 0x3b, 0xff, 0x82, 0x10, 0x0a, 0xc7, 0xac, 0xa7, 0x35,
+ 0x94, 0xd4, 0x31, 0x0b, 0x1d, 0x4e, 0x78, 0xc8, 0xf0, 0xef, 0xa8, 0x77,
+ 0xac, 0xcf, 0x11, 0x89, 0x93, 0x43, 0x4c, 0x18, 0x1f, 0xbc, 0x96, 0xe8,
+ 0x32, 0x19, 0xed, 0x48, 0x97, 0x98, 0x86, 0xb6, 0x6f, 0x33, 0x03, 0xda,
+ 0x53, 0xba, 0x68, 0x8c, 0xba, 0x99, 0xba, 0x78, 0xf3, 0x58, 0x10, 0x1b,
+ 0x4d, 0x93, 0x8a, 0x02, 0xcb, 0x73, 0x33, 0x79, 0x91, 0x2e, 0x83, 0xee,
+ 0xd7, 0x91, 0x83, 0x98, 0xdf, 0x3b, 0x50, 0x49, 0x4e, 0x83, 0x75, 0x34,
+ 0x27, 0x4d, 0x39, 0x65, 0x66, 0x95, 0x04, 0x5e, 0x97, 0x56, 0x9c, 0x5e,
+ 0xfd, 0x2a, 0x4d, 0xf6, 0x2d, 0xe2, 0x9b, 0xd3, 0xbf, 0xfb, 0x2c, 0x08,
+ 0x52, 0x3c, 0x76, 0x07, 0x58, 0x40, 0xb9, 0x51, 0x5c, 0x42, 0x52, 0x0d,
+ 0x62, 0xcc, 0x88, 0xf9, 0xc2, 0x26, 0x22, 0xb4, 0xd0, 0x91, 0x5f, 0x7d,
+ 0xe8, 0x9a, 0x0a, 0x86, 0x8e, 0xe9, 0xf6, 0x1d, 0xcd, 0x66, 0x83, 0xc0,
+ 0x13, 0xf6, 0x4d, 0xe3, 0xd2, 0xce, 0x8f, 0xa1, 0x0a, 0xc2, 0x14, 0x0e,
+ 0xb6, 0xb5, 0xb9, 0x93, 0x11, 0xd5, 0xa7, 0xa0, 0xb0, 0xb9, 0x8a, 0x93,
+ 0x46, 0xdd, 0xa2, 0xb3, 0x0e, 0x0b, 0x63, 0xf5, 0x93, 0x24, 0xc0, 0x76,
+ 0x30, 0x56, 0xe8, 0x4f, 0xb2, 0x0a, 0x67, 0x6e, 0x67, 0x16, 0x6c, 0x6a,
+ 0x52, 0x9a, 0xe2, 0x2d, 0xa9, 0xcf, 0xdf, 0x46, 0xf3, 0x99, 0xfd, 0xfd,
+ 0xdb, 0x2e, 0x87, 0x4d, 0x6e, 0x0f, 0xa6, 0x23, 0x4a, 0x36, 0x56, 0x11,
+ 0xcf, 0x1b, 0x8a, 0x67, 0x32, 0x75, 0x13, 0x85, 0x71, 0xec, 0x91, 0x43,
+ 0x16, 0x9a, 0xb4, 0x64, 0xf9, 0xf3, 0xd3, 0x0e, 0xa4, 0x2c, 0x15, 0x6f,
+ 0x4a, 0x49, 0x12, 0xed, 0x31, 0x2b, 0x47, 0xa2, 0x65, 0xa7, 0x04, 0xea,
+ 0x8d, 0xe3, 0x3f, 0xfd, 0x9a, 0x79, 0x8a, 0x99, 0x9c, 0xcf, 0xaf, 0xdb,
+ 0x5d, 0x62, 0xa8, 0x3e, 0x9d, 0x89, 0x8c, 0x91, 0xd9, 0x8b, 0xbe, 0x76,
+ 0xc8, 0xb9, 0x4f, 0x03, 0xa6, 0xe4, 0xa0, 0xc0, 0x85, 0x2c, 0xb3, 0xa8,
+ 0xdb, 0x67, 0xb4, 0x22, 0xc0, 0xe9, 0x3c, 0x87, 0x6a, 0x96, 0xd7, 0xb3,
+ 0xd8, 0xe9, 0xae, 0x69, 0x44, 0x49, 0xf3, 0x15, 0x21, 0x3a, 0x71, 0xb1,
+ 0xc0, 0xe8, 0x78, 0xb3, 0x76, 0xbf, 0x92, 0x41, 0x44, 0xd1, 0x58, 0x2a,
+ 0x2f, 0xe0, 0xff, 0xa9, 0x0a, 0xaa, 0xa9, 0x69, 0xa1, 0x58, 0xeb, 0x71,
+ 0x58, 0xc0, 0xd4, 0x4a, 0xa2, 0x32, 0x1c, 0x1b, 0x04, 0xea, 0x77, 0xb3,
+ 0xb4, 0x2c, 0xf3, 0x07, 0xb9, 0x1f, 0x8e, 0x4e, 0x1e, 0xe2, 0x26, 0xaf,
+ 0xe5, 0x64, 0xa2, 0x0d, 0xa0, 0x8d, 0xc9, 0xb0, 0xee, 0xf0, 0xbb, 0x4f,
+ 0x66, 0x6d, 0x48, 0xb9, 0x1b, 0xb6, 0x39, 0x95, 0xce, 0x54, 0x30, 0x18,
+ 0xf7, 0xfb, 0x98, 0x67, 0x1a, 0xf6, 0xb4, 0x17, 0x31, 0xd4, 0x88, 0xae,
+ 0x53, 0x49, 0x86, 0x80, 0x2e, 0x9c, 0x7b, 0x30, 0x1f, 0x6a, 0x3d, 0xc2,
+ 0xa4, 0x83, 0x30, 0x4a, 0x23, 0xd4, 0xa9, 0xb1, 0xeb, 0x37, 0xad, 0x09,
+ 0xfd, 0x2a, 0x07, 0xb3, 0xbc, 0x98, 0x39, 0xb4, 0x6c, 0x9d, 0x90, 0x69,
+ 0x8e, 0xd2, 0xd6, 0x5f, 0xbc, 0x0d, 0x29, 0x27, 0x12, 0xa6, 0x82, 0xc4,
+ 0x59, 0xac, 0x71, 0x95, 0xbd, 0xcc, 0x28, 0x82, 0x43, 0xab, 0xb8, 0x44,
+ 0xfe, 0xfe, 0x3b, 0x4c, 0x9a, 0x2e, 0x93, 0x37, 0xaf, 0xb4, 0xeb, 0x9f,
+ 0x58, 0xdd, 0x2b, 0x9f, 0xc4, 0xd4, 0x44, 0xad, 0xaf, 0x39, 0x49, 0xb7,
+ 0x43, 0x66, 0x5e, 0x4c, 0xe5, 0x3a, 0x65, 0xe1, 0xb7, 0xf8, 0xa8, 0xdd,
+ 0xf5, 0x6b, 0x37, 0x13, 0xb2, 0xba, 0x0f, 0x57, 0x66, 0x95, 0xf1, 0x4d,
+ 0x92, 0x52, 0x39, 0xde, 0x85, 0x41, 0xae, 0xc5, 0xe4, 0xa0, 0xa3, 0xab,
+ 0xd8, 0x25, 0xab, 0xe1, 0x2a, 0x91, 0x31, 0xbb, 0x41, 0x0e, 0xab, 0x52,
+ 0x6f, 0xf3, 0x58, 0xa7, 0xfc, 0x9f, 0x36, 0x3f, 0x43, 0x8b, 0xcd, 0x0a,
+ 0xa0, 0xe2, 0xb9, 0xcb, 0x18, 0x47, 0x83, 0x4a, 0x81, 0x6f, 0xe9, 0x56,
+ 0xcf, 0x2b, 0xad, 0x48, 0x76, 0xe4, 0xba, 0x03, 0xb0, 0x2b, 0x2d, 0x7d,
+ 0xd6, 0x3c, 0xec, 0xcb, 0xb7, 0x23, 0x16, 0xbc, 0x2c, 0x2f, 0x90, 0xff,
+ 0xa5, 0x7a, 0xe5, 0x9a, 0xf7, 0xc3, 0x60, 0xd9, 0x91, 0x5a, 0x99, 0x2b,
+ 0x3f, 0xe7, 0xfa, 0x90, 0xd6, 0x3a, 0xf7, 0x1a, 0x73, 0xe5, 0x36, 0x02,
+ 0xec, 0xf4, 0x18, 0x41, 0xde, 0x90, 0x4c, 0xb5, 0xd9, 0xe3, 0x9c, 0x38,
+ 0xb7, 0x73, 0xc7, 0x05, 0x5e, 0xd8, 0xc0, 0x1a, 0xd9, 0x44, 0xa9, 0x9e,
+ 0x20, 0x0d, 0xbd, 0x12, 0xe5, 0xcb, 0x18, 0xcc, 0xaf, 0x05, 0xcc, 0xbb,
+ 0x60, 0x56, 0xbe, 0x38, 0xfa, 0x18, 0x2c, 0xf9, 0x76, 0x81, 0x2f, 0x88,
+ 0xc5, 0xf6, 0xdb, 0x37, 0xd4, 0x55, 0x00, 0x30, 0x9b, 0x84, 0xef, 0xdd,
+ 0xba, 0x75, 0xe2, 0x52, 0xb2, 0x23, 0xa7, 0xc8, 0x0e, 0xcf, 0xe8, 0x7f,
+ 0xbb, 0xda, 0xdb, 0x27, 0xf8, 0xcf, 0x9f, 0xe4, 0x3c, 0xd7, 0xc8, 0x0c,
+ 0x7a, 0x99, 0x08, 0x02, 0xa2, 0xe0, 0xa8, 0x43, 0x70, 0xcd, 0x7f, 0x79,
+ 0x6f, 0x25, 0x2b, 0x47, 0x6c, 0xe3, 0x8f, 0xa1, 0x6a, 0x1c, 0x7c, 0x6a,
+ 0x77, 0x92, 0xd2, 0x01, 0x5c, 0xa9, 0xb3, 0xd2, 0x1f, 0x26, 0xba, 0xb8,
+ 0x94, 0x59, 0xf9, 0x50, 0x0c, 0xc3, 0x72, 0xe8, 0x7a, 0x8b, 0x51, 0x85,
+ 0x4b, 0xf7, 0xa8, 0x3e, 0x9d, 0x52, 0x4f, 0xb1, 0xa8, 0x90, 0x59, 0x2c,
+ 0x2f, 0xac, 0xed, 0xbf, 0x22, 0xf0, 0x38, 0x35, 0x03, 0xb3, 0xbb, 0x0e,
+ 0xc8, 0x61, 0xf2, 0x60, 0x5e, 0xe1, 0xdb, 0xd3, 0x7b, 0x88, 0x5e, 0x54,
+ 0x9d, 0xe8, 0xc3, 0xdc, 0xcd, 0x48, 0x54, 0x3a, 0x96, 0xd3, 0xda, 0x72,
+ 0xe2, 0x96, 0xb6, 0x50, 0xb1, 0xe6, 0x28, 0x5f, 0xf3, 0x1a, 0x6b, 0x7c,
+ 0x29, 0x8e, 0x12, 0x00, 0x10, 0x50, 0xe3, 0x08, 0x3f, 0x3e, 0x19, 0xf5,
+ 0xf7, 0xc5, 0x1a, 0xa4, 0x44, 0x48, 0xfc, 0xa9, 0x39, 0x10, 0xcb, 0x4c,
+ 0xa2, 0xcb, 0x60, 0xf5, 0xf8, 0x4b, 0xfb, 0xe4, 0x5d, 0xf3, 0x87, 0x92,
+ 0x62, 0xee, 0x66, 0x91, 0x0e, 0x3d, 0x5d, 0xe4, 0x2c, 0x58, 0xe5, 0xce,
+ 0x8e, 0x52, 0xe5, 0x6e, 0xad, 0xf5, 0xc8, 0x54, 0x1c, 0x33, 0x38, 0xfa,
+ 0x2b, 0x06, 0x4d, 0xde, 0x3b, 0xbe, 0x16, 0xf6, 0x42, 0x44, 0x57, 0xc6,
+ 0x74, 0xe1, 0xcd, 0xa7, 0x09, 0x30, 0x88, 0x80, 0xd5, 0x60, 0xe9, 0x39,
+ 0x95, 0x29, 0x8f, 0xe5, 0x67, 0x59, 0x23, 0xcc, 0x44, 0xe2, 0x43, 0x77,
+ 0xa2, 0x9f, 0xc0, 0xab, 0xe9, 0x6e, 0xc0, 0x97, 0x6c, 0xd8, 0xa0, 0x6b,
+ 0xc9, 0x3d, 0xb6, 0xf9, 0x4d, 0x34, 0xad, 0xe0, 0x18, 0xe7, 0xe3, 0xc7,
+ 0xa3, 0x57, 0xd0, 0x93, 0x1b, 0xfc, 0x5a, 0x9c, 0xa2, 0x7c, 0xf8, 0x40,
+ 0x53, 0xa4, 0xb2, 0xa4, 0x18, 0x4f, 0x67, 0xf7, 0x2e, 0xe2, 0x43, 0x5b,
+ 0x30, 0xfe, 0x18, 0x52, 0xc5, 0x38, 0xfa, 0xe0, 0x9a, 0x48, 0x66, 0x32,
+ 0x66, 0x4d, 0x09, 0xa5, 0x64, 0x3d, 0xed, 0x4f, 0x50, 0xb9, 0x09, 0xca,
+ 0x46, 0xe8, 0x9e, 0x72, 0x95, 0xe5, 0x6f, 0x26, 0x5c, 0x36, 0x0f, 0xcf,
+ 0x8e, 0x06, 0x6d, 0x9b, 0xc0, 0xf0, 0x56, 0x6f, 0xbd, 0x4b, 0x01, 0xba,
+ 0x33, 0x33, 0x86, 0xb9, 0xa4, 0xd6, 0xcd, 0xe0, 0x19, 0x00, 0x17, 0xf0,
+ 0xdb, 0x6e, 0x94, 0x6d, 0x00, 0x1b, 0x0e, 0x8f, 0x9b, 0x24, 0x00, 0x69,
+ 0x25, 0x6e, 0xc8, 0x3f, 0xd2, 0x0e, 0x4b, 0x66, 0x72, 0x25, 0x75, 0x3a,
+ 0xe0, 0x44, 0x5c, 0xad, 0x7c, 0x00, 0xde, 0xdb, 0x2b, 0xcf, 0xc5, 0x36,
+ 0x98, 0xa3, 0x6f, 0x31, 0xeb, 0x3d, 0x57, 0x72, 0x71, 0xdc, 0xc3, 0x37,
+ 0x49, 0x9b, 0x01, 0x06, 0xb9, 0x89, 0x0f, 0x7f, 0x7b, 0x04, 0x66, 0x26,
+ 0x41, 0xdd, 0xb2, 0x74, 0xca, 0xed, 0xef, 0xc2, 0x35, 0xe8, 0x78, 0x49,
+ 0x00, 0x47, 0x9a, 0x4a, 0x58, 0x1c, 0xcd, 0xd3, 0x0f, 0x99, 0xeb, 0x8d,
+ 0x2e, 0xab, 0x73, 0x47, 0x39, 0xaf, 0x6c, 0x89, 0x28, 0xbc, 0x2e, 0x4c,
+ 0x44, 0x9e, 0x6d, 0x9e, 0x43, 0xe1, 0x43, 0x2f, 0x3c, 0x6d, 0x4b, 0xe5,
+ 0xef, 0xfa, 0x14, 0xb5, 0x55, 0xcf, 0x03, 0x96, 0xaf, 0x2c, 0x2b, 0xf3,
+ 0x43, 0x84, 0xbb, 0x53, 0x44, 0xb7, 0x42, 0x5f, 0x37, 0xf1, 0x60, 0xbb,
+ 0xb4, 0xd6, 0x5e, 0x6e, 0xc9, 0x71, 0x98, 0xa0, 0xe7, 0x5a, 0xe3, 0x3e,
+ 0x5b, 0x55, 0xff, 0x17, 0x2e, 0x21, 0xdd, 0x79, 0x28, 0xfd, 0x8e, 0x34,
+ 0xd8, 0xe9, 0x35, 0x3e, 0x88, 0x60, 0x1a, 0x89, 0x0b, 0x0d, 0xb5, 0xb6,
+ 0xd8, 0x58, 0x17, 0xd2, 0x66, 0xe6, 0xe2, 0x3d, 0x71, 0x67, 0xfd, 0x26,
+ 0x73, 0x71, 0x83, 0xb7, 0x76, 0x4b, 0x1e, 0x9f, 0x9c, 0x88, 0x5a, 0x5e,
+ 0x8c, 0x19, 0x9a, 0x7b, 0xbd, 0x6e, 0x23, 0x3e, 0x62, 0x9c, 0xac, 0x64,
+ 0xd1, 0x0b, 0x74, 0xb2, 0xac, 0xa4, 0xc9, 0x12, 0xdf, 0x2a, 0xd2, 0xa2,
+ 0xef, 0x7c, 0x98, 0x1b, 0x33, 0x13, 0xa4, 0xb2, 0x5b, 0x8b, 0xbb, 0x97,
+ 0x89, 0x73, 0x12, 0x6f, 0xee, 0xd9, 0xd2, 0x19, 0xdc, 0x59, 0x28, 0xe6,
+ 0xe1, 0x45, 0x8c, 0x53, 0x39, 0xec, 0x01, 0xd8, 0x30, 0x69, 0x12, 0x8b,
+ 0x06, 0x10, 0x9c, 0x5c, 0x76, 0x6b, 0x5d, 0x5c, 0xce, 0xa5, 0x3e, 0x19,
+ 0xfb, 0x9e, 0x39, 0x51, 0xfc, 0xab, 0x9a, 0x16, 0xd0, 0xa6, 0xb0, 0x7a,
+ 0xa6, 0x8f, 0x56, 0x70, 0x13, 0x16, 0x33, 0xf5, 0x2f, 0xee, 0xbe, 0x2f,
+ 0x0d, 0xd4, 0x9b, 0xaa, 0xe7, 0x5d, 0x8b, 0x2a, 0xa4, 0x34, 0xa2, 0x14,
+ 0xd6, 0xde, 0x4c, 0x87, 0x30, 0x49, 0x33, 0xb9, 0xf9, 0x6c, 0x64, 0xf7,
+ 0x92, 0x58, 0xe9, 0x40, 0x2f, 0x3d, 0x84, 0x85, 0x1a, 0xb8, 0xad, 0xc6,
+ 0xe8, 0x15, 0x60, 0xd0, 0x91, 0x16, 0x47, 0x1b, 0xc2, 0xef, 0x73, 0xfa,
+ 0xfe, 0x7d, 0x17, 0x69, 0xd0, 0x65, 0x63, 0x3d, 0x92, 0x97, 0x6a, 0xa6,
+ 0x4b, 0xf3, 0xc4, 0x3d, 0x4d, 0x52, 0x2a, 0x03, 0xf5, 0x7e, 0x75, 0x30,
+ 0xb7, 0xf5, 0xf3, 0xac, 0xc7, 0x3a, 0x17, 0x8e, 0x30, 0xfe, 0x12, 0x6d,
+ 0xce, 0xcf, 0x53, 0x2d, 0xb6, 0x22, 0xa7, 0x05, 0xcc, 0x81, 0x29, 0xec,
+ 0xd1, 0xfd, 0x9e, 0xde, 0xfa, 0x2d, 0xc9, 0x15, 0x18, 0x33, 0x46, 0xac,
+ 0xed, 0x01, 0xbd, 0x0a, 0xa0, 0x54, 0x01, 0xa0, 0xa2, 0xe4, 0xc9, 0x13,
+ 0xa6, 0x7e, 0xa3, 0x7d, 0x8f, 0x0a, 0x68, 0xc8, 0x4c, 0xaf, 0x80, 0x5c,
+ 0x11, 0xb1, 0x40, 0x99, 0xaa, 0x29, 0x74, 0x44, 0x1d, 0xff, 0xf7, 0x26,
+ 0xad, 0xbf, 0x8a, 0x8a, 0x24, 0x72, 0x39, 0x59, 0x04, 0x27, 0x19, 0xf1,
+ 0x2d, 0x8a, 0xc8, 0x1f, 0x11, 0x1d, 0x55, 0x63, 0x7c, 0x8f, 0x47, 0x8a,
+ 0x60, 0x4a, 0x3a, 0x1c, 0xb7, 0xf0, 0xb6, 0x56, 0x3f, 0xad, 0xe1, 0x0d,
+ 0xd1, 0xe2, 0x70, 0x67, 0x88, 0xc9, 0x77, 0x25, 0x39, 0x1d, 0x89, 0x34,
+ 0x53, 0xe3, 0x1b, 0x82, 0x9d, 0x72, 0xc6, 0x37, 0xec, 0xb4, 0x50, 0x04,
+ 0x49, 0x13, 0x34, 0x16, 0x0b, 0x8e, 0x9f, 0xc9, 0xf9, 0xec, 0x02, 0xc2,
+ 0x23, 0x49, 0x92, 0x98, 0x43, 0x0d, 0x63, 0x00, 0x00, 0x00, 0x4a, 0xa0,
+ 0x7f, 0xec, 0x15, 0xdb, 0xff, 0xda, 0xbf, 0xb1, 0xa2, 0xe7, 0x17, 0x57,
+ 0xf6, 0xb0, 0x7c, 0xb7, 0x23, 0x2f, 0x0a, 0x01, 0x17, 0xfa, 0xde, 0x3d,
+ 0x82, 0x6e, 0x61, 0x16, 0xaf, 0x7b, 0xd8, 0x48, 0xfb, 0x43, 0x4d, 0xa0,
+ 0xcf, 0xff, 0x7e, 0x7b, 0xca, 0x17, 0x0c, 0xf1, 0x7b, 0xb9, 0xb4, 0xb8,
+ 0x68, 0xf5, 0x86, 0x1d, 0xcc, 0xfd, 0xec, 0xbc, 0x71, 0xd9, 0xf5, 0x77,
+ 0x7d, 0x07, 0xe9, 0x3f, 0xf6, 0x64, 0x84, 0xbd, 0xe1, 0x05, 0x95, 0x37,
+ 0x19, 0x6f, 0xc9, 0xdd, 0x9e, 0x52, 0xbf, 0xe7, 0xb9, 0x6f, 0xd7, 0x0a,
+ 0x3e, 0xc4, 0xba, 0x7a, 0xeb, 0x14, 0x3d, 0xc2, 0x25, 0x62, 0x2f, 0x1f,
+ 0x5c, 0xbd, 0x82, 0x23, 0xa7, 0x37, 0xe0, 0x39, 0xae, 0xae, 0x9d, 0x37,
+ 0x71, 0x3b, 0xa6, 0xb0, 0x37, 0x26, 0x64, 0x40, 0x01, 0x3a, 0x0d, 0x77,
+ 0xe2, 0x84, 0xd4, 0x06, 0xd0, 0x4b, 0x9b, 0x0d, 0xef, 0x71, 0x91, 0xb6,
+ 0x69, 0xab, 0xe1, 0x7e, 0xdc, 0x94, 0x53, 0xef, 0x25, 0xad, 0x9c, 0x3b,
+ 0x95, 0x4c, 0xa7, 0x3c, 0xf2, 0xf3, 0x1a, 0x03, 0xbe, 0x87, 0xf7, 0x4d,
+ 0x68, 0xad, 0x54, 0x3f, 0x7d, 0x23, 0xcd, 0xa1, 0x3a, 0xc0, 0xee, 0x97,
+ 0x05, 0x22, 0xc4, 0xdd, 0x60, 0x17, 0x63, 0x11, 0x3a, 0xdb, 0x73, 0x6f,
+ 0xdd, 0x07, 0xd9, 0x43, 0x3f, 0xea, 0x26, 0x96, 0xfa, 0x12, 0x7f, 0x3d,
+ 0xd6, 0xed, 0xac, 0x20, 0x23, 0x24, 0xcf, 0x8e, 0x88, 0x3d, 0x75, 0xe7,
+ 0x2c, 0xa3, 0xdb, 0x1e, 0x82, 0xd8, 0xf0, 0x3a, 0x20, 0x56, 0xf8, 0x84,
+ 0x89, 0xe7, 0xf9, 0x88, 0x1b, 0x69, 0xd3, 0xf7, 0xa5, 0x79, 0x80, 0xc8,
+ 0x7b, 0xe4, 0x92, 0x30, 0x51, 0x49, 0x1c, 0x3e, 0x64, 0xc2, 0x7b, 0x51,
+ 0x9c, 0x50, 0x63, 0x20, 0x70, 0xec, 0x78, 0xbe, 0xe6, 0x83, 0xab, 0x49,
+ 0xf9, 0xa8, 0x28, 0x01, 0x35, 0x36, 0xf9, 0xee, 0xf4, 0x55, 0x88, 0x65,
+ 0xa8, 0xdb, 0x0c, 0x23, 0xa8, 0xeb, 0x96, 0x25, 0x49, 0x5a, 0x3d, 0xf2,
+ 0x82, 0xea, 0xc6, 0xca, 0xca, 0xc7, 0x2a, 0x5c, 0xc6, 0xfc, 0x5f, 0x5d,
+ 0xab, 0x87, 0xf6, 0xb9, 0xae, 0x41, 0x34, 0x2e, 0x90, 0x26, 0x9f, 0xff,
+ 0x12, 0x47, 0xb8, 0x6a, 0x03, 0x7e, 0x54, 0x65, 0xde, 0xca, 0x5f, 0xf3,
+ 0xcf, 0x7a, 0x86, 0x11, 0x28, 0xac, 0x84, 0x3a, 0xd4, 0x65, 0x97, 0xe2,
+ 0xc5, 0xe4, 0x63, 0x69, 0xa8, 0x04, 0xae, 0x53, 0xc6, 0xba, 0x4e, 0xc7,
+ 0x55, 0x27, 0x3e, 0xa4, 0x02, 0x2a, 0x45, 0x51, 0xc7, 0x1f, 0xc8, 0x0d,
+ 0x15, 0x07, 0x1b, 0x4d, 0x01, 0x91, 0xbe, 0x30, 0xf6, 0xa3, 0x6e, 0x68,
+ 0x95, 0xad, 0x16, 0x58, 0xbb, 0x44, 0x55, 0x3b, 0xc3, 0xb2, 0x87, 0x36,
+ 0x93, 0x1e, 0xa4, 0xa3, 0xef, 0x5d, 0xc2, 0xcc, 0xab, 0x98, 0xfb, 0x71,
+ 0xf3, 0xdf, 0x36, 0x64, 0x4c, 0xc6, 0xa8, 0xb3, 0x09, 0x26, 0x30, 0xce,
+ 0x96, 0x7d, 0x3b, 0xee, 0x4d, 0x04, 0xc6, 0x74, 0x54, 0xc9, 0xa6, 0x2e,
+ 0x38, 0xd7, 0x0d, 0xc8, 0xb8, 0xe7, 0xf6, 0xd1, 0xf6, 0x0b, 0xf4, 0x7f,
+ 0xe2, 0xbf, 0xa4, 0xfa, 0x28, 0xeb, 0xd6, 0x6d, 0x3a, 0xc2, 0x8f, 0xf0,
+ 0x50, 0x80, 0xab, 0xbb, 0x07, 0x29, 0xdd, 0x7b, 0x06, 0xe0, 0x0c, 0x7e,
+ 0x4a, 0x5d, 0x9b, 0xe6, 0x35, 0xb5, 0x6d, 0x27, 0x9a, 0x28, 0x3a, 0xf2,
+ 0xc0, 0x2f, 0x23, 0x8e, 0x48, 0x46, 0x39, 0x7d, 0xc4, 0x03, 0x9c, 0x02,
+ 0xd0, 0x68, 0x04, 0x26, 0x02, 0x14, 0xc7, 0x2e, 0x75, 0xa6, 0xac, 0x1e,
+ 0x72, 0x05, 0x3a, 0xbb, 0x9b, 0x41, 0x7a, 0x00, 0x3a, 0x5e, 0xd8, 0x97,
+ 0xef, 0xea, 0x62, 0x49, 0x4d, 0xc8, 0x71, 0x4a, 0x0c, 0x5d, 0x00, 0x68,
+ 0xa8, 0x72, 0x03, 0xe3, 0xed, 0x74, 0xbc, 0x66, 0xd5, 0x37, 0xab, 0x86,
+ 0x77, 0x49, 0xb1, 0x42, 0x14, 0x3e, 0x2a, 0xc8, 0xbd, 0x71, 0x38, 0xef,
+ 0x7f, 0x64, 0xfa, 0x46, 0xb2, 0xd4, 0xbb, 0x0d, 0x05, 0xd1, 0xb4, 0xa1,
+ 0x29, 0xb7, 0x50, 0x4c, 0x67, 0x3a, 0x39, 0x38, 0x9f, 0xca, 0x5a, 0x82,
+ 0x2f, 0xc8, 0x57, 0xe4, 0x66, 0x82, 0x60, 0x40, 0xb1, 0x3b, 0xca, 0x75,
+ 0xdd, 0x8e, 0x61, 0x05, 0x3f, 0xd5, 0x10, 0xdf, 0x50, 0x10, 0x3e, 0x01,
+ 0x8a, 0x5a, 0xaa, 0x9a, 0xb7, 0x62, 0xaf, 0x41, 0x36, 0xa5, 0x02, 0x2a,
+ 0xb7, 0x3f, 0x11, 0x8b, 0x0a, 0x68, 0x97, 0x22, 0x8b, 0xd2, 0x58, 0x54,
+ 0xba, 0x52, 0xcd, 0x03, 0x3d, 0xb1, 0x9a, 0x45, 0xa5, 0xc4, 0xc3, 0xfe,
+ 0x9b, 0x71, 0x18, 0x70, 0xae, 0x7f, 0xa4, 0x5f, 0xec, 0xf1, 0x65, 0x04,
+ 0xa1, 0xf0, 0xb9, 0x95, 0x5d, 0x4c, 0x6c, 0x2a, 0xe8, 0x4d, 0x28, 0x32,
+ 0xd1, 0x1b, 0x8c, 0x45, 0x9f, 0xf7, 0xe6, 0x98, 0xc4, 0x8b, 0x35, 0x0a,
+ 0xea, 0x70, 0x20, 0xff, 0x8d, 0x76, 0x68, 0x34, 0x10, 0x5b, 0x75, 0x8c,
+ 0x95, 0xb4, 0x2d, 0x33, 0xbe, 0xb5, 0x13, 0x52, 0xe3, 0xa6, 0x53, 0x96,
+ 0x33, 0x4f, 0x0b, 0x00, 0xea, 0x37, 0xf2, 0x7d, 0x57, 0x2b, 0x76, 0xf3,
+ 0xc8, 0x58, 0x8d, 0x9a, 0xa9, 0x35, 0xa8, 0xb7, 0x4b, 0xb5, 0xcf, 0xfb,
+ 0xd4, 0xd6, 0x24, 0x74, 0x31, 0xcc, 0x9c, 0xfe, 0x93, 0x46, 0x16, 0x97,
+ 0x8f, 0xbc, 0x1f, 0x59, 0x79, 0xa2, 0xce, 0x6f, 0x2d, 0xfc, 0x36, 0x27,
+ 0xdd, 0x4a, 0x4a, 0x73, 0xd5, 0xd5, 0xb4, 0x14, 0xa1, 0x1e, 0xc0, 0x0c,
+ 0xc3, 0xf3, 0x90, 0x5d, 0x4b, 0x5c, 0xaa, 0xe3, 0x10, 0xfb, 0x67, 0x76,
+ 0x57, 0x9b, 0xc0, 0x76, 0x18, 0x2f, 0x8d, 0x33, 0x59, 0x1f, 0xfa, 0x75,
+ 0x5f, 0x2d, 0x86, 0xb9, 0x81, 0x21, 0xe9, 0x9a, 0x7f, 0xcc, 0xf9, 0xcb,
+ 0x55, 0x19, 0xe8, 0x70, 0x81, 0xdf, 0x90, 0x13, 0xda, 0xbf, 0x83, 0xf8,
+ 0xe8, 0x44, 0x87, 0x8a, 0x86, 0x38, 0x4f, 0xbc, 0x5c, 0x74, 0x75, 0xe3,
+ 0x65, 0xd4, 0x29, 0xa2, 0x09, 0xc7, 0xcc, 0xa3, 0x07, 0x90, 0xab, 0x17,
+ 0xb7, 0x78, 0x69, 0xbb, 0x17, 0xf8, 0x12, 0x51, 0xc5, 0xa6, 0x92, 0xc8,
+ 0xab, 0x68, 0xb1, 0xcf, 0xcd, 0xf9, 0xbc, 0x37, 0x08, 0x56, 0xfa, 0x1a,
+ 0x05, 0xba, 0x80, 0xb4, 0xa8, 0x64, 0xa5, 0x12, 0xa1, 0xa5, 0xb9, 0x0e,
+ 0x3f, 0x6d, 0x86, 0x29, 0x49, 0x9e, 0x69, 0x73, 0x73, 0xab, 0xda, 0xd0,
+ 0x4c, 0xac, 0x1e, 0xb3, 0xdc, 0xde, 0x08, 0xd1, 0x8b, 0xb1, 0x29, 0xc0,
+ 0x42, 0xa8, 0xc0, 0xcd, 0xd1, 0x52, 0xb9, 0x5d, 0xf2, 0x54, 0xf5, 0x94,
+ 0x5b, 0xab, 0xe1, 0xda, 0x7b, 0xc3, 0x10, 0xb4, 0x9c, 0x2f, 0x54, 0xe4,
+ 0x9c, 0x04, 0xf6, 0xd8, 0x22, 0xde, 0x15, 0xcb, 0x56, 0xee, 0x09, 0x57,
+ 0x27, 0x2e, 0xd0, 0xf5, 0x80, 0x2e, 0x45, 0xb5, 0xb3, 0xac, 0xcd, 0x9f,
+ 0x51, 0x31, 0x22, 0x97, 0x7b, 0x5b, 0x5a, 0xe8, 0x5b, 0x5b, 0x2a, 0x05,
+ 0xab, 0x00, 0xba, 0x22, 0x77, 0xea, 0x4c, 0x84, 0x51, 0xa6, 0x60, 0x18,
+ 0x2b, 0x0c, 0xe7, 0xf9, 0x5e, 0x46, 0x10, 0xbb, 0xc6, 0x76, 0xa3, 0x07,
+ 0xee, 0xd6, 0x8b, 0x45, 0x22, 0xd6, 0x61, 0x3d, 0xac, 0x81, 0x4b, 0x04,
+ 0xc1, 0x8d, 0x62, 0x84, 0x94, 0x6c, 0x5f, 0xe3, 0x73, 0x97, 0xa0, 0xbd,
+ 0x38, 0x0b, 0xd9, 0xa9, 0x72, 0xa3, 0xbc, 0xf2, 0xdd, 0x80, 0x70, 0x0f,
+ 0x08, 0x99, 0xa0, 0x4f, 0xb8, 0x22, 0x24, 0x27, 0x29, 0xf6, 0x95, 0x95,
+ 0x5d, 0xc8, 0xac, 0x13, 0x1b, 0x30, 0x4b, 0x1e, 0x49, 0x35, 0x81, 0x7a,
+ 0x11, 0xc0, 0x94, 0x0c, 0xe0, 0xaf, 0x9b, 0x8f, 0x57, 0xed, 0x38, 0x2c,
+ 0x31, 0x66, 0x94, 0x46, 0x90, 0x40, 0x67, 0x52, 0xa5, 0xb5, 0xcb, 0x69,
+ 0x63, 0x8d, 0xde, 0x3b, 0xe9, 0x94, 0x8a, 0x0c, 0xf3, 0x23, 0x21, 0xb4,
+ 0x5d, 0xd7, 0x57, 0x3d, 0x55, 0x3f, 0x85, 0x1f, 0x2f, 0x22, 0x7f, 0xf2,
+ 0x16, 0xa6, 0x94, 0x53, 0x47, 0x20, 0x46, 0x4c, 0xe4, 0x42, 0xcf, 0x4f,
+ 0x74, 0x83, 0x02, 0xaa, 0xd5, 0x04, 0x43, 0xa1, 0xfa, 0x91, 0xd0, 0x4c,
+ 0x35, 0x1d, 0x4a, 0xa4, 0x36, 0x7f, 0xb5, 0x85, 0x17, 0x69, 0x60, 0xac,
+ 0x72, 0x52, 0xc7, 0x88, 0x70, 0x8b, 0xff, 0x88, 0x1d, 0xde, 0x7d, 0xbb,
+ 0xc1, 0xac, 0xe7, 0x84, 0xff, 0x50, 0xe6, 0x9c, 0x1c, 0xf4, 0xb0, 0xc4,
+ 0x8c, 0x71, 0x5d, 0x4f, 0x26, 0x04, 0x23, 0xf6, 0x57, 0xa0, 0x78, 0x74,
+ 0x3c, 0x5c, 0x9f, 0x40, 0x74, 0xca, 0x90, 0x9a, 0x6a, 0x7b, 0x04, 0x00,
+ 0xe4, 0x44, 0x00, 0xa3, 0x2e, 0x58, 0x1f, 0xb0, 0xf0, 0x5c, 0x91, 0x81,
+ 0xc4, 0x84, 0xf9, 0x9f, 0x45, 0x44, 0x07, 0xfc, 0x28, 0x0d, 0xef, 0x69,
+ 0x3a, 0x84, 0xf1, 0xa5, 0x03, 0x68, 0x82, 0x18, 0x0b, 0xaa, 0x34, 0x8c,
+ 0x7f, 0x38, 0xb6, 0xdf, 0xdb, 0x00, 0x5e, 0x81, 0xe7, 0xa8, 0x1a, 0x70,
+ 0x91, 0xd6, 0x5a, 0xaa, 0xab, 0x00, 0x0b, 0x6f, 0x08, 0xf7, 0xdc, 0x56,
+ 0x4f, 0x05, 0xa9, 0x7e, 0x57, 0x90, 0x60, 0x28, 0xd5, 0xe7, 0x01, 0x1f,
+ 0xeb, 0x9b, 0x85, 0xeb, 0x01, 0x0b, 0xd7, 0xaf, 0xa3, 0x11, 0x4a, 0xf6,
+ 0x2b, 0xd3, 0xac, 0x75, 0xf5, 0xf4, 0xb5, 0x11, 0x5a, 0x64, 0x8f, 0x69,
+ 0x9f, 0x26, 0xf0, 0x83, 0x0f, 0xae, 0x11, 0xae, 0xc0, 0x45, 0x0c, 0x93,
+ 0xf1, 0xa8, 0xe9, 0xbb, 0x1f, 0x29, 0x40, 0xdb, 0x3b, 0x18, 0x30, 0xef,
+ 0xce, 0x8f, 0x80, 0xd8, 0x86, 0xd2, 0x36, 0x84, 0xdc, 0x09, 0x90, 0x65,
+ 0x94, 0x61, 0x9f, 0x53, 0xed, 0xb7, 0xd9, 0x58, 0x79, 0x9d, 0x4c, 0xef,
+ 0xb8, 0xbe, 0x82, 0xe8, 0x52, 0x39, 0xa5, 0x91, 0x59, 0x28, 0x5c, 0x4b,
+ 0x65, 0xa5, 0x41, 0x79, 0x9d, 0xc4, 0x48, 0x47, 0xe6, 0x5c, 0x2d, 0x69,
+ 0x67, 0xe0, 0xd2, 0x9d, 0x6e, 0x35, 0xc5, 0xcc, 0xf1, 0xb5, 0x1c, 0xc9,
+ 0xc0, 0x00, 0x93, 0x23, 0x96, 0x7e, 0xbf, 0x72, 0x30, 0x88, 0xb3, 0x1d,
+ 0x14, 0x12, 0xa5, 0x2a, 0x72, 0xbb, 0x1a, 0x85, 0x05, 0x6b, 0x87, 0xed,
+ 0xce, 0x24, 0x81, 0xcb, 0x4b, 0x45, 0xeb, 0x33, 0xb9, 0xfb, 0x71, 0xd9,
+ 0xcf, 0x33, 0x63, 0x3c, 0x7c, 0x6a, 0xe8, 0xe9, 0x06, 0x0d, 0x1d, 0xec,
+ 0xb6, 0x23, 0x14, 0xbe, 0xc7, 0x51, 0x8b, 0x14, 0x34, 0xfb, 0xaf, 0xa4,
+ 0x77, 0x30, 0xec, 0xe4, 0x2c, 0x30, 0x5b, 0x6b, 0x0a, 0x08, 0xf1, 0xce,
+ 0xa7, 0x27, 0xf7, 0xe0, 0x70, 0x47, 0x02, 0xea, 0xf2, 0x93, 0x80, 0x1e,
+ 0x38, 0xd1, 0xb7, 0xf2, 0xd9, 0x6f, 0x8f, 0x09, 0x20, 0xc9, 0x9f, 0x09,
+ 0xdd, 0x05, 0x60, 0x72, 0xf8, 0x96, 0xb3, 0xa0, 0x25, 0x29, 0xd9, 0xf3,
+ 0x44, 0xf6, 0xdc, 0xb4, 0xbb, 0x10, 0x1f, 0xfd, 0xe9, 0x96, 0xb1, 0x7f,
+ 0x49, 0x45, 0x81, 0x1b, 0x63, 0xd2, 0x29, 0x78, 0x85, 0xb2, 0x44, 0xb5,
+ 0x50, 0x96, 0x62, 0x0c, 0x85, 0xa7, 0x71, 0xad, 0xaf, 0x4f, 0x7d, 0xbe,
+ 0xb1, 0xdc, 0x5b, 0x0d, 0x0f, 0x62, 0x3f, 0x06, 0x05, 0xec, 0x29, 0xf5,
+ 0xc7, 0xba, 0x95, 0x1c, 0xc0, 0x57, 0xff, 0x07, 0x96, 0xc0, 0x65, 0x8f,
+ 0x72, 0xef, 0x8a, 0x94, 0x40, 0xe5, 0xd2, 0x09, 0x76, 0xc1, 0x90, 0x7b,
+ 0x61, 0x26, 0xef, 0x56, 0x87, 0x0f, 0x54, 0x0a, 0xe4, 0x20, 0x8d, 0x46,
+ 0xfc, 0x04, 0x78, 0x47, 0x38, 0x6a, 0xc8, 0xa6, 0x1c, 0xa3, 0x4b, 0x24,
+ 0x01, 0xd7, 0x3e, 0x04, 0x0e, 0x91, 0xfb, 0xce, 0xd6, 0x78, 0xdf, 0x0f,
+ 0x5e, 0xb8, 0xa9, 0xf2, 0xf5, 0x6e, 0xd8, 0x88, 0xc0, 0xb6, 0x9e, 0xea,
+ 0x18, 0x6e, 0x77, 0xed, 0xa0, 0x8e, 0x98, 0x7a, 0x00, 0x59, 0xc5, 0x23,
+ 0xd2, 0x79, 0x47, 0xce, 0xee, 0xfa, 0xc0, 0x05, 0x40, 0x19, 0xd6, 0xb0,
+ 0x72, 0x58, 0xf2, 0xa7, 0x49, 0x14, 0x56, 0xa2, 0x50, 0x8e, 0xf6, 0xf2,
+ 0x33, 0x95, 0x41, 0x4c, 0xea, 0xb7, 0x3e, 0x9b, 0xb3, 0x63, 0x0a, 0xdc,
+ 0x4b, 0xb7, 0xe9, 0x5f, 0x5c, 0x2e, 0x5d, 0xac, 0x07, 0x03, 0xef, 0xda,
+ 0x70, 0x36, 0xb7, 0xf3, 0x69, 0x4f, 0x21, 0xde, 0x0c, 0x7d, 0xe5, 0x71,
+ 0x79, 0xd4, 0xd4, 0xd7, 0xb8, 0xb2, 0xcf, 0xae, 0xd0, 0xd3, 0xae, 0xe2,
+ 0x88, 0xda, 0x7d, 0x24, 0x2b, 0x07, 0x80, 0x01, 0x0b, 0x8b, 0xbc, 0x3d,
+ 0xc9, 0x50, 0x2a, 0x4c, 0xf3, 0x08, 0x5b, 0xc1, 0x51, 0x03, 0x35, 0x29,
+ 0x27, 0x56, 0xd3, 0xb6, 0xdb, 0x5f, 0x54, 0x8b, 0x31, 0xeb, 0x63, 0xe6,
+ 0x3e, 0x8a, 0x24, 0x5d, 0xe3, 0xa3, 0x3a, 0x28, 0x51, 0xd6, 0x07, 0x71,
+ 0x73, 0x54, 0x1b, 0x37, 0xc0, 0xf7, 0xcd, 0x54, 0x74, 0x4e, 0xcf, 0x5a,
+ 0x12, 0x8a, 0x89, 0xf0, 0x07, 0xf9, 0x30, 0xbf, 0x35, 0xd1, 0xd0, 0x84,
+ 0x1c, 0xe2, 0x9e, 0x23, 0x94, 0x00, 0xa9, 0x5c, 0xdf, 0x1e, 0x66, 0x3c,
+ 0xdf, 0x8b, 0x4e, 0xfc, 0x4d, 0x00, 0x97, 0x3d, 0xd1, 0xe8, 0x52, 0x79,
+ 0xda, 0x4e, 0xab, 0x47, 0x24, 0x03, 0xb7, 0xb5, 0xdf, 0x82, 0x6f, 0xfe,
+ 0xb2, 0x3c, 0x43, 0x0f, 0x73, 0x7c, 0x50, 0xdc, 0xb0, 0x80, 0xd8, 0x2d,
+ 0x8e, 0xb6, 0x42, 0x52, 0x4e, 0x44, 0x9c, 0x2b, 0x89, 0x29, 0xe4, 0x01,
+ 0x56, 0x02, 0x7d, 0xba, 0x20, 0x8d, 0x43, 0x16, 0x4a, 0x7a, 0x22, 0x7b,
+ 0xfc, 0xba, 0x7a, 0xdf, 0xbe, 0x96, 0x76, 0x40, 0xc6, 0x43, 0xb3, 0xe3,
+ 0xca, 0x22, 0x1b, 0x5e, 0x9a, 0x2a, 0x75, 0xd5, 0xfc, 0x23, 0xa6, 0xf1,
+ 0xca, 0xad, 0xa8, 0x82, 0xd7, 0xb7, 0x21, 0xd4, 0x20, 0xcd, 0x3c, 0xe9,
+ 0x20, 0x23, 0x1a, 0xae, 0xa2, 0xe6, 0x72, 0x2c, 0xf2, 0x77, 0x3c, 0x0f,
+ 0x70, 0x61, 0xc3, 0x49, 0x0d, 0x1c, 0x54, 0x64, 0xcd, 0x50, 0x2a, 0x89,
+ 0x09, 0x62, 0x2c, 0x13, 0x36, 0x07, 0x54, 0x90, 0x31, 0x7a, 0xcc, 0x90,
+ 0x73, 0x8d, 0xae, 0x4c, 0xab, 0x3e, 0x93, 0xa9, 0x5a, 0x2d, 0x8f, 0xcc,
+ 0x7c, 0x9a, 0x1a, 0x75, 0xb2, 0x18, 0x8b, 0xc7, 0xd5, 0x4f, 0x1e, 0x65,
+ 0xbf, 0x64, 0x7b, 0xac, 0xf6, 0x9f, 0x26, 0x38, 0x91, 0x66, 0xc9, 0xee,
+ 0xea, 0x33, 0x87, 0xb3, 0xe2, 0xcc, 0x4a, 0xab, 0x13, 0xfa, 0x0b, 0xea,
+ 0x4b, 0x99, 0x98, 0xef, 0xa4, 0x98, 0x38, 0x61, 0x57, 0xaf, 0x33, 0x1e,
+ 0xbf, 0x8a, 0xf6, 0xba, 0x47, 0x43, 0x0d, 0xb8, 0x56, 0xd6, 0x3a, 0xef,
+ 0x41, 0x4c, 0xd9, 0xc7, 0x6f, 0x34, 0x59, 0xb4, 0xe0, 0x19, 0xc8, 0x74,
+ 0x11, 0x79, 0xd5, 0xcf, 0x15, 0x40, 0x1a, 0xaa, 0xeb, 0xb8, 0x6c, 0x96,
+ 0x76, 0x6f, 0xb9, 0x01, 0xe0, 0xd0, 0x7e, 0x09, 0x3a, 0x2a, 0x4a, 0xd9,
+ 0xba, 0x90, 0xa9, 0x2a, 0x59, 0xb7, 0xb4, 0x3f, 0x37, 0x49, 0xe8, 0x32,
+ 0x57, 0xfd, 0x51, 0x87, 0xe8, 0x2f, 0x38, 0xcd, 0xce, 0x98, 0xdc, 0x54,
+ 0xa6, 0xc8, 0x89, 0xd8, 0x8b, 0x03, 0x2b, 0x54, 0xad, 0xf3, 0xd9, 0x16,
+ 0xd9, 0x53, 0xec, 0xf8, 0x5c, 0xb7, 0xa5, 0x6b, 0xb2, 0x09, 0xd6, 0x17,
+ 0xcb, 0xbb, 0x45, 0x71, 0xdb, 0x67, 0xd8, 0x49, 0x73, 0x2b, 0x80, 0xfc,
+ 0x01, 0xcd, 0x4e, 0x18, 0xda, 0xb5, 0x9a, 0x9c, 0xcd, 0xac, 0x53, 0x3c,
+ 0x09, 0x85, 0x69, 0x76, 0xa9, 0xdf, 0x9a, 0x8d, 0x00, 0xd2, 0xea, 0xf7,
+ 0xb3, 0xa7, 0x86, 0x51, 0xb6, 0xd8, 0xeb, 0x02, 0xd4, 0x4b, 0xc7, 0xa6,
+ 0x76, 0x57, 0xc0, 0xf8, 0xb5, 0x52, 0xfd, 0x18, 0x3f, 0xf4, 0x24, 0x78,
+ 0xd8, 0xdc, 0xd0, 0x14, 0x00, 0xb9, 0xef, 0x04, 0xe6, 0x62, 0xf4, 0x2f,
+ 0x7c, 0x72, 0x01, 0x93, 0x17, 0xb5, 0xbf, 0x85, 0x2e, 0x2d, 0x79, 0xdc,
+ 0xb9, 0xc8, 0xdd, 0x6a, 0xd2, 0x83, 0xd1, 0x74, 0x01, 0x4d, 0xac, 0xab,
+ 0x6e, 0xbd, 0x21, 0x1d, 0xf7, 0x0b, 0xe2, 0x68, 0xff, 0x46, 0x9d, 0xd8,
+ 0x72, 0x19, 0xeb, 0x33, 0x61, 0x96, 0x93, 0x3b, 0x40, 0xd0, 0x5b, 0x10,
+ 0x03, 0x0d, 0x36, 0xa0, 0xd3, 0x95, 0x51, 0x99, 0x76, 0x11, 0x67, 0xc9,
+ 0x72, 0xba, 0x90, 0x59, 0x45, 0x8a, 0x35, 0x9b, 0x65, 0xbb, 0xc4, 0x6e,
+ 0xa7, 0x11, 0x6b, 0x44, 0xa8, 0xff, 0x2a, 0xc2, 0x47, 0xc8, 0x6e, 0x9b,
+ 0x89, 0x7e, 0xcc, 0x56, 0xe6, 0x3f, 0x04, 0x7d, 0x43, 0x15, 0x42, 0xfa,
+ 0xfb, 0x5c, 0x2e, 0x1d, 0xad, 0xbc, 0xe6, 0x73, 0x34, 0x48, 0xcd, 0xf5,
+ 0x10, 0x5e, 0x4a, 0x69, 0x1b, 0xe7, 0x09, 0xcb, 0xf6, 0x12, 0xff, 0xea,
+ 0xdd, 0x53, 0x57, 0x61, 0x53, 0x15, 0x58, 0x6c, 0xf2, 0x74, 0xdd, 0x05,
+ 0xf6, 0x98, 0x78, 0x34, 0x74, 0x88, 0xee, 0x86, 0xfc, 0x48, 0xc3, 0x37,
+ 0xe8, 0xfb, 0x05, 0xcd, 0xfb, 0x80, 0xd5, 0xee, 0xf6, 0xdc, 0xbc, 0x05,
+ 0xe7, 0x4d, 0x18, 0x7e, 0xe3, 0x08, 0xa6, 0xf3, 0xc0, 0x84, 0x3c, 0xf0,
+ 0xda, 0x27, 0xa9, 0xfb, 0xa6, 0xef, 0x4a, 0xaa, 0x32, 0x8c, 0xc0, 0x46,
+ 0x9f, 0x65, 0xbb, 0x5d, 0x9d, 0xf3, 0x5b, 0x73, 0x9a, 0xe9, 0x73, 0x81,
+ 0x73, 0xd6, 0xcb, 0xb7, 0x52, 0x21, 0x67, 0xce, 0xa4, 0x45, 0x7f, 0xe0,
+ 0x83, 0x1d, 0x9e, 0x60, 0x99, 0xd9, 0x7d, 0x5e, 0x53, 0xaf, 0x6b, 0x9e,
+ 0x5d, 0xfc, 0x11, 0x16, 0xf6, 0xa0, 0xd9, 0xb8, 0x6e, 0xe5, 0x13, 0x3b,
+ 0xbc, 0xd6, 0x43, 0x23, 0xde, 0x43, 0x1f, 0xc4, 0x2f, 0xba, 0x46, 0x03,
+ 0x7d, 0x76, 0x21, 0xc0, 0xf6, 0x87, 0x95, 0xd8, 0x04, 0x9e, 0x4f, 0x9c,
+ 0x0f, 0x11, 0x41, 0x2c, 0x06, 0x02, 0xeb, 0x2c, 0xac, 0x68, 0x49, 0x59,
+ 0x89, 0x71, 0xde, 0x1b, 0x0b, 0x25, 0xc1, 0x83, 0x71, 0x31, 0x66, 0xee,
+ 0xdf, 0xd7, 0x1f, 0xa0, 0xae, 0x13, 0x69, 0x0f, 0x61, 0x60, 0x2b, 0xf5,
+ 0xb5, 0x36, 0xac, 0x52, 0x19, 0xc7, 0x00, 0xe2, 0xd2, 0x0d, 0xff, 0xe5,
+ 0x0f, 0xb7, 0x5a, 0x04, 0x3b, 0xe1, 0x60, 0x6f, 0xf0, 0x45, 0x49, 0x83,
+ 0x97, 0x87, 0x5c, 0xbc, 0x82, 0x15, 0x47, 0x9d, 0x45, 0x01, 0x89, 0x99,
+ 0x10, 0x8a, 0xb7, 0x7a, 0xfc, 0x75, 0x67, 0x82, 0xf0, 0xa9, 0x8b, 0xd9,
+ 0x0c, 0x71, 0xa9, 0x00, 0x39, 0xff, 0x0a, 0xde, 0x5b, 0x78, 0x4b, 0x89,
+ 0x07, 0x23, 0xff, 0x31, 0xb3, 0xaa, 0xa4, 0x2c, 0xb0, 0x2f, 0x69, 0xd0,
+ 0x5a, 0x39, 0x5c, 0x76, 0x51, 0x80, 0x16, 0x2e, 0x43, 0xa9, 0xde, 0xbe,
+ 0x7e, 0xa6, 0xad, 0x04, 0x15, 0x53, 0x85, 0x40, 0x4b, 0xc7, 0x00, 0x46,
+ 0x60, 0x69, 0xca, 0x95, 0xfd, 0x5a, 0x67, 0xc2, 0xd7, 0x11, 0xae, 0x55,
+ 0xca, 0x06, 0xd8, 0x1f, 0x4c, 0x02, 0x75, 0x5e, 0x38, 0x6f, 0x76, 0x9e,
+ 0x07, 0xff, 0x59, 0x0a, 0x62, 0x3d, 0xa1, 0x11, 0xe2, 0xc2, 0xea, 0xa7,
+ 0x90, 0x19, 0x52, 0x2b, 0xf6, 0x5b, 0x35, 0x0c, 0x0a, 0x51, 0x3d, 0x1b,
+ 0x1e, 0x82, 0x13, 0xfa, 0x8a, 0x7b, 0x43, 0x22, 0xdf, 0x99, 0xda, 0x19,
+ 0x2a, 0xdc, 0x33, 0x4c, 0xba, 0x42, 0x1b, 0xc5, 0x45, 0xf0, 0xa6, 0x50,
+ 0x01, 0x05, 0x4e, 0xce, 0x93, 0x04, 0xc2, 0x0a, 0x81, 0xa0, 0x10, 0xfd,
+ 0xf3, 0x46, 0x6f, 0xb0, 0x6d, 0x84, 0x8c, 0x46, 0xcc, 0xe2, 0xa6, 0x1c,
+ 0x27, 0x2c, 0xc3, 0x97, 0xc8, 0x24, 0x6e, 0x2b, 0x8d, 0x9f, 0x7a, 0xfd,
+ 0x5a, 0x55, 0x21, 0x45, 0xa4, 0xbb, 0xc1, 0xa8, 0x2d, 0xd3, 0xc9, 0x7e,
+ 0x13, 0xdb, 0x0f, 0xbf, 0x93, 0xb5, 0x8c, 0x99, 0x11, 0x96, 0x35, 0xc3,
+ 0x8a, 0x1f, 0xde, 0xc3, 0x89, 0x7a, 0x57, 0x1c, 0x08, 0xf0, 0x1f, 0x10,
+ 0xa3, 0x5b, 0xbc, 0x3f, 0xfc, 0x34, 0x0c, 0x28, 0x82, 0xac, 0xeb, 0xbf,
+ 0x31, 0x67, 0x6f, 0x3e, 0xa3, 0xcd, 0xc1, 0x44, 0x57, 0x8a, 0xac, 0x05,
+ 0xb8, 0xf1, 0xdd, 0xab, 0xb8, 0x9f, 0xb6, 0x35, 0x11, 0x38, 0x96, 0xd7,
+ 0x67, 0xe7, 0x7f, 0xe5, 0x0e, 0xd8, 0x87, 0x26, 0x6d, 0x49, 0x9f, 0x68,
+ 0x6a, 0xba, 0xfa, 0x01, 0x06, 0xca, 0xe6, 0xac, 0xac, 0x14, 0x63, 0xd9,
+ 0x95, 0x8a, 0x22, 0x79, 0x1e, 0x03, 0xdf, 0x83, 0x96, 0x71, 0x4d, 0x05,
+ 0x81, 0xb9, 0x95, 0x20, 0x9e, 0x09, 0xc5, 0xdd, 0x5c, 0x1e, 0x9e, 0x24,
+ 0x76, 0xc9, 0xe3, 0x0f, 0x11, 0xb8, 0xf0, 0x04, 0xa4, 0x5d, 0x3d, 0xc4,
+ 0x12, 0xe8, 0xfd, 0x00, 0x0d, 0xb3, 0x97, 0x95, 0xfe, 0xe8, 0x3b, 0xe8,
+ 0xa0, 0x0b, 0xae, 0x56, 0xd4, 0x03, 0xe7, 0xcd, 0xae, 0x53, 0xde, 0xbb,
+ 0x69, 0xde, 0xc3, 0x81, 0x87, 0x45, 0x08, 0x51, 0xa7, 0x02, 0x14, 0x11,
+ 0x23, 0x1d, 0x3e, 0x0d, 0xe6, 0x82, 0x05, 0x18, 0x63, 0x3c, 0x76, 0x5a,
+ 0x27, 0xd2, 0x59, 0xde, 0xe8, 0x94, 0x7a, 0x43, 0x47, 0xa9, 0x2d, 0xc4,
+ 0x6f, 0xf9, 0x77, 0x27, 0x9c, 0xad, 0xe3, 0x4d, 0x8b, 0xc8, 0x1d, 0xfe,
+ 0x2e, 0xfe, 0x4b, 0xa6, 0x39, 0x25, 0x4b, 0x90, 0xb2, 0x54, 0x69, 0xb2,
+ 0x4c, 0xca, 0x17, 0xdd, 0xa8, 0x49, 0xd0, 0xbb, 0x86, 0xca, 0x3b, 0xc4,
+ 0x89, 0x10, 0x78, 0x1f, 0x90, 0x7e, 0xe9, 0xef, 0x89, 0xc3, 0x98, 0x23,
+ 0xcf, 0xbb, 0xce, 0xdc, 0xa7, 0x4a, 0xd2, 0x2b, 0xe7, 0x45, 0xdf, 0xb4,
+ 0x6e, 0xd1, 0xff, 0x2a, 0x7f, 0xa5, 0x89, 0x82, 0x28, 0x2f, 0xa5, 0xbb,
+ 0xee, 0xd1, 0x7f, 0xfc, 0x52, 0xb6, 0x27, 0xa0, 0xa3, 0x42, 0xf5, 0x2d,
+ 0x12, 0x3a, 0xb6, 0xd5, 0x9d, 0x2c, 0x62, 0xb8, 0x5e, 0x88, 0xe5, 0xb0,
+ 0x3c, 0xe8, 0x70, 0xd6, 0x73, 0x35, 0xe0, 0xe7, 0xb8, 0x16, 0x1c, 0xb7,
+ 0x82, 0xcc, 0xa3, 0x8b, 0x6c, 0xc1, 0x9d, 0x45, 0x51, 0x8f, 0xa8, 0xb1,
+ 0xaf, 0x7c, 0x5d, 0xd2, 0x67, 0x9f, 0xf0, 0xa3, 0x4d, 0x1f, 0x10, 0xf1,
+ 0x58, 0x8c, 0x9b, 0x14, 0xd2, 0x2d, 0xa9, 0x42, 0x90, 0xf6, 0xb6, 0x08,
+ 0x9d, 0x5a, 0xe0, 0x1f, 0x7a, 0x0f, 0x56, 0x2c, 0x06, 0x3d, 0xcd, 0x98,
+ 0x73, 0x71, 0x10, 0xc9, 0xf2, 0xa9, 0x18, 0x51, 0x6b, 0xa6, 0x96, 0xde,
+ 0xfb, 0xff, 0x3c, 0x18, 0x82, 0x0d, 0xa8, 0x7e, 0x80, 0xd8, 0x46, 0x3f,
+ 0xac, 0x10, 0xbd, 0xb7, 0xf8, 0x0b, 0x80, 0x2f, 0x7b, 0xda, 0x27, 0xdb,
+ 0xa8, 0x30, 0x1a, 0x78, 0x36, 0xa8, 0x0c, 0xe2, 0x78, 0x4c, 0x99, 0xbd,
+ 0xf9, 0xb0, 0x3d, 0xaa, 0x20, 0x88, 0xd3, 0xfd, 0x75, 0x08, 0x36, 0x3d,
+ 0x53, 0x46, 0xbf, 0xdb, 0xef, 0x94, 0x0f, 0xa2, 0x4c, 0x49, 0x44, 0x85,
+ 0xda, 0x9f, 0x94, 0xb9, 0xed, 0x68, 0x9f, 0x39, 0xa9, 0x47, 0xb8, 0x46,
+ 0x35, 0x45, 0x74, 0x11, 0x78, 0xf5, 0x47, 0xd2, 0x04, 0x19, 0x82, 0xcc,
+ 0x8f, 0x9e, 0xba, 0x50, 0x31, 0xff, 0x44, 0xeb, 0x7c, 0xe5, 0xbb, 0x0a,
+ 0xa4, 0x37, 0x6c, 0xd0, 0x89, 0x84, 0x1d, 0x2c, 0xe5, 0x17, 0x0c, 0x45,
+ 0x0b, 0x03, 0x46, 0xb4, 0xc7, 0xda, 0x4a, 0x5d, 0xb7, 0xb1, 0xb6, 0xc3,
+ 0x54, 0xf2, 0xf2, 0x4c, 0x22, 0x49, 0x95, 0x97, 0x8a, 0xba, 0xcf, 0x20,
+ 0x83, 0x47, 0xf4, 0xf7, 0xb0, 0x60, 0x19, 0x37, 0xc7, 0xb0, 0x56, 0x00,
+ 0x16, 0xe2, 0xca, 0x87, 0x62, 0x9f, 0x73, 0x82, 0x55, 0xb8, 0x89, 0x33,
+ 0xc7, 0xe6, 0xf3, 0xda, 0x3f, 0x4d, 0x85, 0x3b, 0x46, 0x6f, 0xb1, 0xd3,
+ 0x24, 0x7e, 0xb2, 0x92, 0x20, 0xd1, 0x28, 0xb3, 0x4d, 0xe3, 0x4d, 0xac,
+ 0xe6, 0xaa, 0x9f, 0x86, 0x2f, 0x15, 0x07, 0x38, 0x54, 0xa5, 0xce, 0xc2,
+ 0x77, 0x05, 0xd0, 0x63, 0x07, 0x5b, 0xc1, 0x4c, 0x1f, 0xe3, 0x02, 0xf5,
+ 0xf1, 0x58, 0xd4, 0x37, 0xef, 0xf8, 0xcb, 0x20, 0x6c, 0x32, 0x73, 0xb1,
+ 0x0f, 0x4c, 0x67, 0x11, 0x2f, 0x97, 0x66, 0x4d, 0x95, 0xe0, 0x2e, 0x80,
+ 0x99, 0x4d, 0x42, 0x6c, 0xb3, 0x99, 0x35, 0xe0, 0xae, 0x16, 0xc5, 0xe6,
+ 0x7e, 0xd0, 0x77, 0x80, 0x74, 0x34, 0x3c, 0x6e, 0xe8, 0x8a, 0x95, 0x62,
+ 0xbb, 0x47, 0x6e, 0x09, 0xb2, 0x90, 0x96, 0xc3, 0x9c, 0xd5, 0xca, 0x3a,
+ 0xc8, 0xeb, 0x4d, 0x0e, 0xb2, 0x18, 0xf4, 0xd3, 0x54, 0x54, 0x94, 0xe2,
+ 0x80, 0x0c, 0x92, 0x2b, 0xd2, 0xa3, 0xe5, 0x85, 0x75, 0x92, 0xcf, 0x4d,
+ 0xc1, 0x55, 0xe0, 0xad, 0x15, 0x79, 0x00, 0xa5, 0x1a, 0x89, 0x31, 0x40,
+ 0xfa, 0x39, 0xfc, 0x52, 0x35, 0xde, 0x10, 0x9c, 0xad, 0x44, 0xc7, 0x15,
+ 0x55, 0x6d, 0xec, 0x28, 0xf2, 0xfb, 0x51, 0x35, 0x1e, 0xce, 0xa0, 0x62,
+ 0x7b, 0x21, 0x4d, 0x17, 0xc2, 0x44, 0xbe, 0x77, 0x57, 0x5b, 0x17, 0xf0,
+ 0x2b, 0x18, 0xaf, 0x6a, 0x8f, 0x32, 0x31, 0xa4, 0x33, 0xe5, 0x94, 0x85,
+ 0xcf, 0x21, 0x83, 0x87, 0x56, 0xcc, 0x43, 0xd7, 0x69, 0x38, 0x97, 0x88,
+ 0xa5, 0xeb, 0x7f, 0x9a, 0x1b, 0x88, 0x91, 0xcc, 0xf8, 0x50, 0x33, 0xeb,
+ 0x61, 0xa4, 0x06, 0x30, 0x0b, 0x33, 0x30, 0x31, 0x1d, 0xec, 0x99, 0x82,
+ 0xc5, 0x1b, 0xb0, 0x14, 0x9e, 0x66, 0xc5, 0x6b, 0x07, 0x77, 0xf5, 0x39,
+ 0xd8, 0x83, 0x81, 0xa2, 0xf4, 0x2c, 0x40, 0xbc, 0x89, 0x61, 0xa7, 0xc3,
+ 0x04, 0x60, 0x8c, 0xe9, 0x81, 0x84, 0xed, 0x46, 0x4c, 0xc8, 0xb0, 0x11,
+ 0x95, 0x6d, 0x10, 0xb3, 0x81, 0x3b, 0xf4, 0x73, 0xfb, 0xcb, 0x66, 0xeb,
+ 0xfc, 0x54, 0x96, 0x2a, 0xa3, 0xaa, 0xb5, 0x28, 0x0b, 0xe4, 0xee, 0xe6,
+ 0x7e, 0xb3, 0xeb, 0x0f, 0x0f, 0x7d, 0x91, 0x53, 0x2c, 0x68, 0xc4, 0xf0,
+ 0xb2, 0x9e, 0xee, 0x29, 0x4a, 0x8a, 0x39, 0x6e, 0x2f, 0x7b, 0xfc, 0x0d,
+ 0x41, 0x1a, 0xc6, 0xc1, 0x2d, 0x93, 0xaf, 0xa4, 0x11, 0x20, 0xcd, 0xbf,
+ 0x11, 0xe4, 0xe8, 0x5d, 0x8b, 0x0b, 0x6a, 0x9a, 0x6a, 0xac, 0x1f, 0xc9,
+ 0xf9, 0xd9, 0x9d, 0xdb, 0x4b, 0x2f, 0x9e, 0x8e, 0x8d, 0x12, 0xdd, 0xe1,
+ 0xb9, 0xc9, 0x32, 0x76, 0x0b, 0xc6, 0xf7, 0x08, 0x48, 0x6a, 0xb3, 0x70,
+ 0x96, 0x69, 0x9b, 0xfa, 0x31, 0x28, 0xa1, 0xf4, 0x25, 0xe6, 0x30, 0x52,
+ 0x1a, 0x89, 0xe5, 0x4b, 0xbf, 0xc2, 0x25, 0x30, 0x4d, 0xa2, 0xf6, 0x00,
+ 0x99, 0x23, 0x57, 0xfd, 0x79, 0xb6, 0x4c, 0x0d, 0xa8, 0x5a, 0x1e, 0xf7,
+ 0xb2, 0x85, 0x17, 0x74, 0x5e, 0xb9, 0xc2, 0x69, 0x29, 0x67, 0x03, 0xb5,
+ 0x35, 0xc1, 0xd2, 0xc5, 0x70, 0x1a, 0x94, 0x12, 0xe8, 0x2e, 0x59, 0x81,
+ 0xbd, 0xce, 0x72, 0x9a, 0x4d, 0x03, 0x42, 0x51, 0xaa, 0x0c, 0xa5, 0x9c,
+ 0x91, 0xf9, 0xd1, 0x92, 0xc5, 0xbd, 0xef, 0x6c, 0x8d, 0x7d, 0x73, 0x4c,
+ 0x43, 0x41, 0xcf, 0x65, 0xd9, 0xca, 0x38, 0x98, 0x7a, 0x82, 0x56, 0x83,
+ 0x48, 0x08, 0x4d, 0x00, 0x82, 0x25, 0xb9, 0xba, 0xf3, 0x31, 0x96, 0x26,
+ 0xcc, 0x14, 0x56, 0x5a, 0x3c, 0x62, 0x2b, 0xf3, 0x56, 0x33, 0xdf, 0xe2,
+ 0x9f, 0x1e, 0xcf, 0x35, 0x03, 0xad, 0x94, 0x89, 0x60, 0x82, 0x18, 0xce,
+ 0x0d, 0x81, 0xcc, 0x35, 0x96, 0xbd, 0xda, 0x22, 0x58, 0x1c, 0xe9, 0x26,
+ 0x96, 0x4c, 0x0a, 0x98, 0xc5, 0xbb, 0x8f, 0x10, 0xb9, 0x1d, 0x0e, 0x05,
+ 0x59, 0xef, 0x95, 0x22, 0xcd, 0xa9, 0xbb, 0x75, 0x68, 0xac, 0xfc, 0xf5,
+ 0x32, 0x64, 0x38, 0x36, 0xc4, 0x82, 0x02, 0xd5, 0x95, 0x1d, 0xb8, 0xf5,
+ 0x0e, 0x42, 0xa4, 0x69, 0x32, 0x82, 0x3d, 0x68, 0x15, 0xf9, 0x13, 0x66,
+ 0x9e, 0x4f, 0xd0, 0xfa, 0x32, 0x95, 0x42, 0xd1, 0xf7, 0x43, 0xb1, 0x0c,
+ 0x0e, 0x91, 0xfc, 0xa0, 0xcb, 0x0f, 0xda, 0xac, 0x67, 0xf7, 0x64, 0x2e,
+ 0x5f, 0xc7, 0x08, 0xeb, 0xfd, 0x75, 0xf7, 0x74, 0x8e, 0xb9, 0xff, 0x57,
+ 0xe3, 0xe0, 0x73, 0xcd, 0xdf, 0xec, 0x20, 0x27, 0xb8, 0x52, 0x43, 0xce,
+ 0x8c, 0xd0, 0x0f, 0xdd, 0x1a, 0xb2, 0x9c, 0x1e, 0xb9, 0x7b, 0x5b, 0x1c,
+ 0xed, 0xd4, 0x54, 0xc1, 0x80, 0x6c, 0x5f, 0xdb, 0xb1, 0x68, 0x9f, 0xac,
+ 0x8a, 0x82, 0xc2, 0x33, 0x9e, 0xa8, 0x22, 0xe6, 0xf2, 0x13, 0x60, 0xf7,
+ 0x1f, 0x4e, 0x67, 0xbc, 0xa7, 0x5c, 0xf4, 0xa9, 0xa9, 0xf3, 0x8f, 0xb2,
+ 0x36, 0x43, 0xd5, 0x5c, 0x4c, 0x1f, 0x0b, 0x02, 0x55, 0xad, 0xfd, 0x14,
+ 0x54, 0x4f, 0x0e, 0x5d, 0xe4, 0x08, 0xaa, 0xda, 0x65, 0xee, 0x39, 0xf3,
+ 0x79, 0x42, 0x0e, 0xa7, 0xd1, 0x95, 0x5b, 0xcf, 0x06, 0xb4, 0xfa, 0x9d,
+ 0xd1, 0x6b, 0x0d, 0x50, 0xa3, 0x00, 0x57, 0x04, 0x13, 0xfd, 0x3c, 0xe7,
+ 0x0b, 0xf4, 0x62, 0x92, 0xc8, 0x88, 0x44, 0x4c, 0x18, 0x1f, 0xde, 0x59,
+ 0x9f, 0x45, 0xc9, 0x32, 0xdf, 0x6e, 0xa7, 0x00, 0x08, 0x03, 0x18, 0xe0,
+ 0xb7, 0x53, 0x52, 0x81, 0x8a, 0x6f, 0x05, 0x21, 0x3e, 0xc3, 0xa8, 0x8f,
+ 0xc1, 0xc4, 0x50, 0xe2, 0x3b, 0x26, 0x35, 0xbb, 0x69, 0xe1, 0x6b, 0xb7,
+ 0xed, 0xf3, 0x5b, 0x4e, 0xc8, 0x36, 0xf0, 0xe8, 0x0e, 0xa0, 0x2f, 0x43,
+ 0x8b, 0x0d, 0x8b, 0x1f, 0xad, 0xa3, 0x6e, 0x8a, 0x6a, 0x80, 0x33, 0x8b,
+ 0xe0, 0x6f, 0x39, 0xa4, 0x43, 0xbc, 0x9b, 0x41, 0x60, 0x8c, 0xca, 0x5e,
+ 0x25, 0x1f, 0xd4, 0x33, 0xaf, 0xef, 0xe2, 0xed, 0x9c, 0x2d, 0x0c, 0x66,
+ 0x8f, 0x44, 0x47, 0x02, 0x50, 0x38, 0x99, 0x9e, 0x42, 0x9d, 0xd2, 0x3f,
+ 0x94, 0xbc, 0x38, 0xcd, 0x56, 0x68, 0x69, 0x39, 0x64, 0x05, 0x1c, 0x5f,
+ 0x0c, 0xbb, 0xed, 0x6d, 0xb2, 0x31, 0xd7, 0x33, 0x83, 0x51, 0xb1, 0x26,
+ 0x04, 0x67, 0x35, 0x8e, 0x8c, 0x74, 0x90, 0x9b, 0x46, 0xca, 0x0a, 0x7a,
+ 0x6c, 0xae, 0xe3, 0x7c, 0x87, 0x42, 0xd0, 0xbb, 0x82, 0x27, 0x1c, 0x3e,
+ 0xde, 0x81, 0xcb, 0xd3, 0x5f, 0x6c, 0x5a, 0xae, 0xd6, 0xff, 0x51, 0x51,
+ 0xa6, 0x9c, 0xfa, 0x14, 0x4c, 0x88, 0x51, 0x55, 0xab, 0xeb, 0x4b, 0xbc,
+ 0xa7, 0x62, 0x81, 0x4a, 0x26, 0xde, 0x21, 0x79, 0x26, 0x6d, 0xe9, 0xb6,
+ 0x3f, 0x72, 0xb8, 0x51, 0x79, 0x72, 0x3e, 0x83, 0x82, 0xd2, 0x95, 0xbf,
+ 0x70, 0xa5, 0xbc, 0xd7, 0x07, 0x68, 0xaa, 0x00, 0x2c, 0xf8, 0xfe, 0x45,
+ 0xd9, 0xd7, 0x33, 0x61, 0xce, 0xa9, 0xe9, 0xe1, 0x77, 0xa9, 0x8b, 0x7f,
+ 0xda, 0x26, 0x2c, 0x6a, 0xda, 0x31, 0x7a, 0xb9, 0x2f, 0xa2, 0xef, 0x74,
+ 0x93, 0x2c, 0x53, 0x2a, 0x81, 0xd5, 0x61, 0x15, 0x1d, 0xa8, 0x1d, 0x7f,
+ 0xd1, 0x82, 0xee, 0xe1, 0x34, 0x42, 0x5d, 0xea, 0xdd, 0x87, 0x6a, 0x25,
+ 0x95, 0xdf, 0xaf, 0xb8, 0x24, 0x4f, 0x1c, 0xc2, 0x2f, 0xdc, 0xf7, 0x25,
+ 0xe6, 0xea, 0x64, 0x84, 0x37, 0xba, 0x3a, 0x1e, 0xf2, 0x5e, 0x74, 0xaf,
+ 0x32, 0x8e, 0xa7, 0x9d, 0x74, 0x6b, 0x26, 0x09, 0x4c, 0x12, 0x28, 0xf2,
+ 0x1b, 0x69, 0x30, 0xf3, 0x11, 0xf0, 0x5b, 0xbf, 0xad, 0x2a, 0xa4, 0x11,
+ 0x51, 0x6f, 0x11, 0xe4, 0x5b, 0xf5, 0xad, 0x04, 0x4f, 0xe1, 0x73, 0x59,
+ 0xdb, 0x24, 0x2a, 0x78, 0x6c, 0xdf, 0x95, 0xe1, 0x7c, 0x92, 0xf8, 0x69,
+ 0x36, 0xdd, 0x62, 0xf2, 0x6c, 0xbd, 0xe7, 0xfd, 0x49, 0xc5, 0x30, 0xa3,
+ 0xdc, 0x5e, 0xd7, 0x37, 0xd3, 0x3e, 0x7f, 0x3d, 0x5c, 0x4d, 0xee, 0x18,
+ 0xdb, 0xff, 0xb9, 0x7c, 0x33, 0xa5, 0x6d, 0x23, 0x76, 0x3d, 0x81, 0x99,
+ 0x0b, 0x43, 0x7f, 0xbf, 0x35, 0xf9, 0x9f, 0x43, 0xae, 0x73, 0x4d, 0x56,
+ 0x17, 0x36, 0x82, 0x8b, 0x13, 0xaf, 0x03, 0xa4, 0x8e, 0xc4, 0xa0, 0x31,
+ 0x0e, 0xa5, 0x55, 0x43, 0x0d, 0xe3, 0x19, 0xaf, 0xa2, 0x6e, 0x3a, 0x31,
+ 0xa4, 0x07, 0x3f, 0x28, 0x89, 0x8c, 0x47, 0x9f, 0x74, 0xa9, 0x64, 0x9a,
+ 0xa6, 0x0e, 0x55, 0xd2, 0xd5, 0x18, 0x89, 0x88, 0x5b, 0x3f, 0xc5, 0x3a,
+ 0x3e, 0x6a, 0xc8, 0x9d, 0x80, 0x31, 0xb5, 0xc4, 0xc2, 0xe0, 0x5f, 0xb0,
+ 0x6f, 0xba, 0x17, 0x99, 0x21, 0x7d, 0x29, 0x8c, 0xfe, 0x5c, 0x4b, 0x1f,
+ 0x3f, 0x3c, 0xcc, 0x58, 0x06, 0x67, 0x36, 0xd0, 0x78, 0xad, 0x7a, 0x30,
+ 0xb3, 0x66, 0xd1, 0xc5, 0x84, 0x90, 0x93, 0x46, 0xd2, 0x65, 0xfa, 0x30,
+ 0x0e, 0x34, 0x64, 0xd4, 0x01, 0x56, 0x5a, 0x61, 0x92, 0xf7, 0xaa, 0xf2,
+ 0x99, 0x6e, 0xc7, 0x6a, 0xd1, 0x90, 0xeb, 0x59, 0x8c, 0xac, 0x8b, 0xe4,
+ 0x18, 0x23, 0x03, 0x64, 0x01, 0x9d, 0x8d, 0x29, 0xf7, 0x01, 0x38, 0x5f,
+ 0x4d, 0x45, 0x0a, 0x8c, 0x6f, 0x02, 0xc5, 0x10, 0x03, 0x3a, 0xda, 0xbc,
+ 0x0d, 0x52, 0x2c, 0x87, 0xf8, 0x47, 0x32, 0x64, 0x74, 0xb5, 0x84, 0x2f,
+ 0xe5, 0x50, 0x0c, 0xc6, 0xf7, 0x14, 0x25, 0x20, 0x76, 0xd2, 0xcf, 0x14,
+ 0xe3, 0x6e, 0xef, 0x1e, 0x54, 0x83, 0xa6, 0xf4, 0x13, 0x62, 0x22, 0xf0,
+ 0x97, 0x12, 0xf1, 0x2e, 0x49, 0x40, 0x4c, 0x7d, 0xe5, 0x2f, 0xa5, 0x0e,
+ 0x1c, 0x2c, 0xce, 0x5e, 0xfe, 0x55, 0xb2, 0xee, 0x71, 0x7a, 0x89, 0xfd,
+ 0x60, 0xf3, 0xe6, 0x36, 0xc5, 0xd7, 0x5b, 0xd1, 0xb3, 0x72, 0x4d, 0x55,
+ 0x65, 0xf6, 0x47, 0x16, 0x8e, 0x7e, 0xd1, 0xb3, 0x2e, 0xfe, 0xdc, 0xfb,
+ 0x0e, 0x50, 0x3c, 0x99, 0x1c, 0x5c, 0x2e, 0x4a, 0xce, 0x9f, 0x78, 0x49,
+ 0x32, 0x7c, 0x48, 0x99, 0x6a, 0x6c, 0x1d, 0xd0, 0x09, 0x08, 0x26, 0x02,
+ 0x2d, 0x09, 0xf9, 0xff, 0x1a, 0xcd, 0xe8, 0xa2, 0xc8, 0x81, 0xbf, 0xf1,
+ 0x28, 0x6f, 0xf2, 0xf5, 0xd1, 0x4e, 0xa3, 0x0f, 0xbd, 0x15, 0xc2, 0x3e,
+ 0x33, 0x32, 0xdf, 0x04, 0x2d, 0x5f, 0x6b, 0xb7, 0x95, 0xbf, 0x88, 0x2f,
+ 0xa1, 0x2e, 0xf1, 0x51, 0x59, 0xd2, 0x02, 0xf7, 0x22, 0xfb, 0x87, 0x36,
+ 0x39, 0xe7, 0xa2, 0xb9, 0x5e, 0x34, 0x71, 0xda, 0xf7, 0x15, 0x23, 0x2c,
+ 0xed, 0xae, 0xbd, 0x88, 0xaf, 0xf1, 0x8d, 0xa1, 0x25, 0x9a, 0xc2, 0x61,
+ 0xbc, 0xa3, 0xf6, 0x44, 0xd0, 0x9c, 0x57, 0x19, 0x19, 0x8a, 0x30, 0xee,
+ 0x00, 0xab, 0x8f, 0x6d, 0xd2, 0xb4, 0x08, 0x64, 0x08, 0xc9, 0x4e, 0xb9,
+ 0x1e, 0x6f, 0x28, 0xcc, 0xc4, 0x0c, 0x4e, 0x4d, 0xd4, 0x40, 0x0f, 0x04,
+ 0x8e, 0xba, 0x68, 0xb9, 0x71, 0x3b, 0x98, 0x9c, 0x4f, 0xeb, 0x9e, 0x29,
+ 0xff, 0xd3, 0xb6, 0x2d, 0xe5, 0x14, 0xda, 0x25, 0x27, 0x93, 0xb3, 0x6e,
+ 0x63, 0x8f, 0x25, 0x41, 0xb3, 0xc6, 0xa3, 0xe6, 0x76, 0x98, 0x46, 0xd1,
+ 0xbc, 0x54, 0x8d, 0x78, 0xe4, 0xc6, 0x15, 0xf7, 0xc1, 0xe6, 0xeb, 0x61,
+ 0xa9, 0x41, 0x86, 0x65, 0x3e, 0xb9, 0xa2, 0x69, 0x19, 0x42, 0x77, 0xa9,
+ 0x64, 0xa9, 0x55, 0xc6, 0x62, 0x77, 0x41, 0x99, 0x38, 0x7e, 0xfb, 0x46,
+ 0xd2, 0x9d, 0x02, 0xf3, 0x6e, 0x07, 0x45, 0x70, 0x49, 0x98, 0xff, 0xd2,
+ 0x88, 0x57, 0xa3, 0x63, 0x6a, 0x37, 0x84, 0xdc, 0xe7, 0x30, 0x14, 0x02,
+ 0xb2, 0xb1, 0x82, 0x2d, 0x87, 0x0d, 0x5e, 0xf4, 0xc5, 0xe5, 0x84, 0x02,
+ 0x02, 0xdf, 0xf2, 0x4e, 0x69, 0x52, 0x4f, 0x40, 0xfc, 0x01, 0x16, 0x53,
+ 0xb0, 0xca, 0x33, 0x80, 0x98, 0xe3, 0x3a, 0x1b, 0x50, 0x45, 0x82, 0x1f,
+ 0xa9, 0x39, 0x94, 0xe5, 0xf9, 0x27, 0x52, 0x01, 0xe9, 0x53, 0xf2, 0x0a,
+ 0x89, 0xab, 0x8a, 0x58, 0x00, 0xf5, 0xbc, 0x79, 0x35, 0x44, 0x27, 0xef,
+ 0xc2, 0x07, 0xa5, 0x42, 0xa9, 0xd2, 0xc2, 0xb0, 0x78, 0x8b, 0x81, 0xe6,
+ 0x00, 0x07, 0x2d, 0x94, 0xe0, 0x21, 0x56, 0x6e, 0x79, 0x87, 0x20, 0x53,
+ 0x8f, 0xea, 0x48, 0x7f, 0x3e, 0x48, 0xf6, 0x09, 0x82, 0x93, 0x22, 0x35,
+ 0x5c, 0x6d, 0x1b, 0xc1, 0xcd, 0x48, 0xbb, 0x6a, 0xd7, 0xe5, 0x07, 0x31,
+ 0x5c, 0x8a, 0x7a, 0x96, 0x2e, 0x43, 0x6e, 0x38, 0x2d, 0x00, 0x51, 0x9d,
+ 0x12, 0x65, 0x5c, 0x4e, 0xcb, 0xcb, 0x44, 0xe4, 0xa9, 0xe2, 0xf9, 0x35,
+ 0xb6, 0xdd, 0xbf, 0x0f, 0x18, 0xa9, 0x57, 0xe8, 0x3f, 0xce, 0xf8, 0x09,
+ 0x04, 0xcd, 0xff, 0x4f, 0x9b, 0x30, 0x57, 0x48, 0xd5, 0xce, 0x4f, 0x46,
+ 0x1a, 0xae, 0xa6, 0x2c, 0x5f, 0x72, 0x4d, 0xaf, 0x66, 0x41, 0x42, 0x70,
+ 0xe7, 0xac, 0xd4, 0xba, 0x5c, 0x5f, 0x2f, 0xc4, 0xa3, 0xfe, 0x17, 0xf6,
+ 0xeb, 0xdf, 0x38, 0x20, 0x05, 0x16, 0xfb, 0xdf, 0x8d, 0xf8, 0x9f, 0x22,
+ 0x41, 0xab, 0xbc, 0xa8, 0x87, 0xe1, 0x96, 0xe0, 0x42, 0x2a, 0x65, 0x3f,
+ 0x56, 0x96, 0xf0, 0x5a, 0xdd, 0xeb, 0x46, 0x23, 0x29, 0x58, 0xe7, 0xb2,
+ 0x32, 0x37, 0x99, 0x7f, 0x4d, 0x33, 0x52, 0x7e, 0x0d, 0x5f, 0xce, 0x79,
+ 0x3d, 0x48, 0x19, 0x55, 0x8a, 0xb9, 0x1d, 0xfd, 0xe7, 0x36, 0xb8, 0x0f,
+ 0xf1, 0xa8, 0xa2, 0xdb, 0x0e, 0xd9, 0x35, 0x11, 0xa0, 0xd0, 0xa9, 0xf3,
+ 0x16, 0xea, 0x2b, 0x2b, 0x57, 0x5d, 0x40, 0x92, 0x25, 0xff, 0x38, 0x70,
+ 0xeb, 0x3a, 0x01, 0xcf, 0xb6, 0x73, 0x4e, 0x73, 0xa6, 0x2b, 0xdf, 0xae,
+ 0x52, 0xf2, 0xec, 0xde, 0xd7, 0xb7, 0x23, 0x1d, 0xc2, 0x9c, 0x40, 0x5d,
+ 0xd3, 0x83, 0x8a, 0x92, 0x2e, 0xde, 0x5e, 0xb5, 0xcb, 0x31, 0x95, 0xa1,
+ 0x3e, 0x43, 0xc9, 0xfa, 0x0e, 0x64, 0x77, 0xe1, 0x1e, 0x9d, 0xd2, 0x78,
+ 0xc6, 0x5a, 0x30, 0xe5, 0x50, 0xf2, 0xcd, 0x8c, 0x1e, 0x2c, 0x32, 0xf4,
+ 0x03, 0x25, 0xe2, 0x67, 0xd5, 0xe5, 0x2b, 0x31, 0x4f, 0x5a, 0xea, 0xd0,
+ 0xe8, 0x25, 0x6b, 0xf5, 0xbe, 0x5d, 0x72, 0xec, 0xc2, 0xcd, 0x2f, 0x12,
+ 0x95, 0x42, 0xda, 0xc9, 0x70, 0x8d, 0xa8, 0x20, 0x23, 0x22, 0x37, 0x28,
+ 0x28, 0xdb, 0xa6, 0xf9, 0xe3, 0x35, 0xd1, 0x68, 0x53, 0x82, 0x9c, 0x2e,
+ 0x65, 0xa2, 0xbc, 0x62, 0x06, 0xdd, 0x6e, 0x27, 0xa0, 0x14, 0xe3, 0xf6,
+ 0xac, 0x8f, 0x82, 0xdf, 0x2a, 0x1a, 0xbb, 0x8f, 0x2c, 0xd8, 0x8f, 0x8a,
+ 0x06, 0xca, 0xa6, 0xf5, 0x30, 0x41, 0xb8, 0xbf, 0xfe, 0x7d, 0x53, 0x40,
+ 0xc3, 0x3f, 0x3a, 0x1b, 0xa7, 0xc0, 0x64, 0xb1, 0x2a, 0x51, 0xf1, 0x04,
+ 0xbd, 0x19, 0xb9, 0xaf, 0x17, 0xf2, 0x9e, 0x5a, 0x1e, 0x40, 0x78, 0xc3,
+ 0xd3, 0xef, 0x96, 0x78, 0x5d, 0x2f, 0x1a, 0xf0, 0x6f, 0xaf, 0x72, 0x52,
+ 0xae, 0x61, 0x93, 0x4f, 0x36, 0x76, 0x34, 0x8b, 0xf4, 0x0e, 0xd5, 0xd0,
+ 0xee, 0xe7, 0x8f, 0x91, 0x0d, 0x49, 0x7a, 0x45, 0x31, 0xa2, 0x60, 0xd9,
+ 0x8f, 0x51, 0x5c, 0x3f, 0x92, 0xca, 0x4e, 0x0d, 0x92, 0x04, 0x5d, 0x17,
+ 0x56, 0xa6, 0x02, 0x6c, 0x9b, 0x59, 0x32, 0x2c, 0x24, 0x5d, 0x86, 0x85,
+ 0x73, 0x1b, 0x9e, 0x56, 0xcf, 0x96, 0x08, 0x07, 0xc8, 0x5d, 0x78, 0x1d,
+ 0x51, 0x5d, 0x2a, 0x3f, 0x15, 0x40, 0xde, 0x32, 0xe5, 0x79, 0x2e, 0x9f,
+ 0x0b, 0x0b, 0x5a, 0xa4, 0x6b, 0xb3, 0xba, 0x69, 0xee, 0x77, 0x36, 0x08,
+ 0xf4, 0x8d, 0x00, 0x94, 0x86, 0x53, 0x7e, 0xd1, 0xee, 0x64, 0x7c, 0x88,
+ 0x9d, 0xf3, 0x01, 0xfd, 0xfc, 0xf6, 0x9d, 0xb4, 0xe6, 0xac, 0x08, 0x29,
+ 0x2c, 0x86, 0xb3, 0xe1, 0xf7, 0xb0, 0x7b, 0x0d, 0x9d, 0x31, 0xe1, 0x7b,
+ 0x9a, 0x35, 0x0a, 0x6c, 0x97, 0x19, 0xcc, 0x09, 0x07, 0x81, 0x4d, 0xbe,
+ 0xf0, 0xe9, 0x56, 0xb0, 0xd0, 0x4e, 0x4d, 0x18, 0x9b, 0x07, 0xe5, 0xdd,
+ 0x80, 0x04, 0x68, 0x70, 0x83, 0xae, 0xb5, 0x6e, 0x28, 0x80, 0x1a, 0xac,
+ 0xab, 0xe8, 0xcb, 0x56, 0xef, 0xab, 0x34, 0x48, 0x17, 0xe8, 0xca, 0x74,
+ 0x6e, 0xff, 0xbc, 0x9f, 0x0c, 0xcb, 0xcb, 0x7b, 0xe0, 0x16, 0x93, 0x5c,
+ 0xdf, 0x18, 0x51, 0x3b, 0x97, 0x57, 0x6e, 0xef, 0xac, 0x1d, 0xc4, 0x93,
+ 0xc1, 0x5d, 0x29, 0x32, 0x98, 0x18, 0x9e, 0x3a, 0x0d, 0x45, 0x90, 0xeb,
+ 0xf8, 0xb9, 0xbe, 0xd4, 0x35, 0x6f, 0xb5, 0xea, 0xe7, 0xf6, 0xc7, 0x6f,
+ 0x2b, 0xf1, 0xe7, 0x40, 0x98, 0xe7, 0x04, 0x5e, 0x1b, 0x1c, 0x19, 0x5f,
+ 0xf6, 0x6a, 0xce, 0x56, 0x07, 0x18, 0xcf, 0x78, 0xca, 0x25, 0xb7, 0x35,
+ 0xa3, 0xf0, 0xf7, 0x3b, 0xdd, 0x1a, 0x16, 0x5d, 0x97, 0x3e, 0x7d, 0x21,
+ 0xce, 0x62, 0x6a, 0xf1, 0x1a, 0xcc, 0x8b, 0xb5, 0x19, 0xd1, 0x03, 0x94,
+ 0x33, 0x86, 0xe2, 0xc2, 0x43, 0xb5, 0xa8, 0xdc, 0x4c, 0xd5, 0x67, 0x29,
+ 0x2e, 0x4e, 0x81, 0xe6, 0x06, 0x1b, 0x81, 0x39, 0xd1, 0x66, 0xcb, 0x18,
+ 0x7c, 0xd0, 0x04, 0x17, 0xdd, 0x83, 0x02, 0xbf, 0xf5, 0xaa, 0xe9, 0xec,
+ 0xdb, 0x1a, 0xc9, 0xca, 0x9d, 0x53, 0xe7, 0x01, 0xc8, 0xa7, 0xcf, 0x49,
+ 0xc0, 0x8f, 0x53, 0x40, 0x28, 0x51, 0x62, 0x5f, 0xd5, 0xf9, 0x6a, 0x01,
+ 0x7b, 0x7f, 0x09, 0x13, 0x57, 0xf9, 0xc9, 0x56, 0xd6, 0x88, 0xbb, 0x13,
+ 0x7b, 0x53, 0xcc, 0x79, 0x7e, 0x2e, 0x13, 0x42, 0xfc, 0x37, 0x48, 0xf0,
+ 0x04, 0x0a, 0x5b, 0x88, 0x74, 0xd1, 0x85, 0x56, 0x8c, 0x92, 0xf4, 0x5a,
+ 0x99, 0x1f, 0x3a, 0xbe, 0x57, 0xcc, 0xa7, 0x77, 0xb5, 0x27, 0x2c, 0x09,
+ 0x0d, 0x8f, 0x41, 0x67, 0xd5, 0xdc, 0x3a, 0x55, 0x3d, 0xd5, 0x13, 0x96,
+ 0x64, 0xe4, 0xb8, 0xa6, 0x5a, 0xa3, 0x22, 0x84, 0xae, 0x7a, 0x24, 0xee,
+ 0x93, 0x86, 0x7d, 0x52, 0x31, 0xb4, 0xb0, 0x89, 0x04, 0x46, 0x8d, 0x86,
+ 0x63, 0xcb, 0xd4, 0xd2, 0xaf, 0x0a, 0x69, 0xad, 0xa9, 0x96, 0x42, 0xf1,
+ 0x1e, 0xef, 0x07, 0xa9, 0x24, 0xb7, 0xca, 0xfa, 0xda, 0x68, 0x51, 0xb6,
+ 0xde, 0x6f, 0x5d, 0x46, 0x79, 0xc1, 0xa6, 0xbe, 0x43, 0x9a, 0x90, 0x41,
+ 0xd1, 0xc4, 0x99, 0x5b, 0x3d, 0xd4, 0x25, 0xb9, 0xa4, 0xdb, 0xfc, 0x29,
+ 0xfd, 0x27, 0x26, 0xeb, 0x23, 0x6f, 0xa3, 0xdf, 0x70, 0x7b, 0x07, 0xae,
+ 0x96, 0x58, 0x3a, 0xe7, 0x2b, 0x26, 0x31, 0xd8, 0xb9, 0xf5, 0x45, 0x10,
+ 0xc0, 0x2e, 0x33, 0x98, 0xe5, 0x24, 0xcf, 0xc4, 0x54, 0x6e, 0x12, 0x5b,
+ 0x1b, 0xbe, 0xc2, 0x4a, 0x56, 0x03, 0x76, 0x1e, 0x87, 0x5e, 0x2f, 0x9e,
+ 0xe5, 0xd7, 0x90, 0x4e, 0x45, 0x5f, 0x42, 0x56, 0x49, 0x2c, 0xee, 0x52,
+ 0xa0, 0x5a, 0xb4, 0xc5, 0xc2, 0x2d, 0x62, 0xd7, 0xc6, 0xc8, 0x75, 0xee,
+ 0x9e, 0x6a, 0x90, 0x1a, 0x8e, 0x69, 0x3f, 0x93, 0x26, 0x70, 0x8c, 0xaa,
+ 0x55, 0x82, 0x51, 0x80, 0x59, 0x23, 0xc8, 0xec, 0x9e, 0xc3, 0x9b, 0x06,
+ 0xd6, 0x21, 0x19, 0x45, 0xab, 0xa4, 0x33, 0x0a, 0x0e, 0x1d, 0x0e, 0xdc,
+ 0x7c, 0x4d, 0x9c, 0xa4, 0xa7, 0xfa, 0x49, 0x67, 0x9a, 0x5b, 0xc0, 0x7b,
+ 0x59, 0x9b, 0x97, 0xc5, 0xf6, 0x76, 0x03, 0x22, 0x17, 0xa4, 0xf5, 0x35,
+ 0x34, 0x25, 0xcd, 0xba, 0xc3, 0x24, 0xec, 0xe5, 0xe5, 0x0e, 0x86, 0xbd,
+ 0xa9, 0xdf, 0xd0, 0xfd, 0x82, 0xf9, 0x8a, 0x18, 0x29, 0x51, 0x00, 0xb6,
+ 0xff, 0xf5, 0x47, 0x6d, 0xf2, 0x19, 0xf0, 0xed, 0x4e, 0xa0, 0x4d, 0x48,
+ 0x84, 0xa1, 0xb8, 0x94, 0xc2, 0xf1, 0x48, 0x16, 0x87, 0xbd, 0x73, 0x18,
+ 0xa7, 0x79, 0x64, 0x7a, 0x07, 0xc8, 0xc4, 0x52, 0x52, 0xab, 0x57, 0xfd,
+ 0xec, 0x77, 0xf8, 0x1d, 0x54, 0x11, 0xf5, 0x45, 0x4a, 0x37, 0x8e, 0x31,
+ 0xca, 0xb9, 0x2a, 0x8a, 0xe8, 0x2f, 0x0d, 0x19, 0x01, 0x35, 0xcd, 0x77,
+ 0x21, 0xa8, 0xeb, 0x04, 0x87, 0xa1, 0x59, 0x28, 0x79, 0xd7, 0xc7, 0xd0,
+ 0x2a, 0x21, 0xaf, 0xc0, 0x02, 0xac, 0x27, 0xd5, 0xf0, 0x28, 0xd2, 0x09,
+ 0x4c, 0x14, 0x4f, 0xf4, 0xcc, 0xa6, 0x8b, 0x03, 0xe6, 0xcb, 0x90, 0xaf,
+ 0xb8, 0x1c, 0xf5, 0xd7, 0xb6, 0x87, 0xa2, 0xd9, 0x78, 0x96, 0xa6, 0xc0,
+ 0x65, 0x61, 0xcf, 0x46, 0x82, 0x9b, 0x17, 0x39, 0xe1, 0xc0, 0x7f, 0xa7,
+ 0x87, 0x50, 0xd0, 0x05, 0xcd, 0xa4, 0xbb, 0xdf, 0xc9, 0xe0, 0x59, 0x1e,
+ 0x70, 0xa4, 0xa3, 0x9c, 0x26, 0x80, 0xaa, 0x6c, 0x21, 0x4a, 0xa1, 0xb9,
+ 0x35, 0x61, 0xff, 0xb3, 0x5d, 0x3c, 0x1a, 0x19, 0xbc, 0xf7, 0x95, 0xf1,
+ 0x37, 0x55, 0x45, 0xfe, 0x2e, 0x85, 0x96, 0x39, 0xa5, 0xc8, 0x82, 0x6b,
+ 0x73, 0xaf, 0x23, 0xe2, 0x5c, 0xb3, 0x68, 0xc5, 0x78, 0xb3, 0x04, 0x49,
+ 0x99, 0x72, 0x9c, 0x3f, 0x08, 0xf5, 0x02, 0x3b, 0x89, 0x07, 0x3f, 0x38,
+ 0x2c, 0xcd, 0x9a, 0x7a, 0xdb, 0xfd, 0xe8, 0x84, 0xc9, 0x50, 0xd2, 0x67,
+ 0x02, 0x63, 0xbe, 0x57, 0x2d, 0xe4, 0x00, 0x02, 0xa2, 0x10, 0x34, 0x25,
+ 0xcf, 0x41, 0x45, 0x49, 0x01, 0xef, 0xb5, 0xd7, 0x43, 0x99, 0x12, 0x25,
+ 0xe8, 0xb9, 0x0c, 0x5a, 0xde, 0xfd, 0x91, 0xcf, 0xf2, 0x53, 0x9b, 0xf0,
+ 0x13, 0x5e, 0x70, 0x72, 0x39, 0x3e, 0x3c, 0x18, 0xf3, 0x1a, 0x73, 0x42,
+ 0x00, 0xd2, 0x07, 0xff, 0xdf, 0x66, 0xfb, 0x2b, 0x50, 0x62, 0x2c, 0x1c,
+ 0x71, 0x4f, 0x00, 0x44, 0xc5, 0xa3, 0xb3, 0xad, 0x09, 0xd9, 0x26, 0x51,
+ 0xe8, 0xa9, 0x39, 0x1f, 0x0e, 0xa6, 0xb2, 0x03, 0x17, 0x34, 0x07, 0x6a,
+ 0x22, 0xdf, 0xfb, 0xcf, 0x9f, 0xb1, 0xc6, 0x38, 0xa7, 0x92, 0xd6, 0x71,
+ 0x84, 0x76, 0x3d, 0xaa, 0x21, 0x88, 0x0a, 0x37, 0x46, 0x51, 0x41, 0x60,
+ 0xc9, 0x8b, 0x31, 0x8c, 0x94, 0x5b, 0x37, 0xae, 0x71, 0x31, 0xdf, 0xb6,
+ 0x8b, 0xaa, 0xa7, 0x68, 0x86, 0xa3, 0x2f, 0xcf, 0xbb, 0x2e, 0x40, 0xd1,
+ 0x36, 0xb2, 0xa2, 0xdb, 0x2c, 0x1a, 0x78, 0x34, 0xdd, 0x1b, 0xa4, 0x53,
+ 0xe7, 0xcc, 0x42, 0xfe, 0x84, 0xf0, 0x32, 0x0d, 0xfc, 0x0c, 0x4a, 0x9f,
+ 0x1d, 0x3f, 0xcc, 0x19, 0x40, 0x08, 0x36, 0x01, 0x74, 0x1a, 0xbc, 0x16,
+ 0x73, 0x3a, 0xc6, 0xb1, 0x1c, 0x06, 0xaa, 0xe7, 0xe2, 0xca, 0x01, 0x13,
+ 0x79, 0x71, 0xb0, 0x1d, 0x0b, 0xb2, 0x10, 0x10, 0x2e, 0xe1, 0x8b, 0x96,
+ 0x9f, 0xb2, 0xe1, 0xcf, 0x91, 0x62, 0x06, 0x66, 0xf4, 0x07, 0x7f, 0xbc,
+ 0xb5, 0xd4, 0xc5, 0xa8, 0x2e, 0xee, 0xb2, 0x8d, 0x9a, 0xa2, 0x23, 0xaf,
+ 0xd4, 0x86, 0x8e, 0x03, 0xe6, 0x96, 0x8a, 0x29, 0x87, 0x47, 0xa3, 0x63,
+ 0x81, 0x2b, 0x17, 0xd1, 0x24, 0xa7, 0xc1, 0xec, 0x7d, 0x76, 0x6d, 0x62,
+ 0xab, 0x88, 0x16, 0x5e, 0x63, 0x92, 0x74, 0x78, 0xee, 0x49, 0x14, 0x18,
+ 0x69, 0x8b, 0x02, 0x72, 0xc5, 0x21, 0x45, 0x03, 0xe0, 0xa1, 0xde, 0xd1,
+ 0x9d, 0x25, 0xb1, 0x1e, 0x9b, 0x45, 0x6b, 0xd7, 0x80, 0x0d, 0x04, 0x68,
+ 0x15, 0x5f, 0x9b, 0x27, 0xab, 0x78, 0x6b, 0x11, 0xa8, 0x7d, 0x29, 0x39,
+ 0x6f, 0x1d, 0x12, 0xc9, 0x3b, 0xc0, 0x12, 0x96, 0xa0, 0xf2, 0x27, 0xcc,
+ 0x97, 0x22, 0x6c, 0x55, 0xc2, 0xfd, 0xc2, 0xe0, 0xa6, 0x99, 0xaf, 0x44,
+ 0xe5, 0x96, 0xa8, 0x59, 0xe4, 0xb0, 0xfc, 0xa5, 0x68, 0x24, 0x50, 0x60,
+ 0xfa, 0xba, 0x20, 0x8f, 0x75, 0xab, 0xbe, 0xe5, 0x32, 0x02, 0x3b, 0xd4,
+ 0x83, 0x19, 0xeb, 0x0d, 0x19, 0x14, 0x10, 0x7d, 0xb7, 0xc0, 0x58, 0x62,
+ 0xa2, 0xb6, 0x75, 0xbf, 0x93, 0x78, 0xff, 0x40, 0x06, 0x1c, 0x62, 0x4a,
+ 0x18, 0x28, 0xb6, 0xcf, 0xf3, 0x6b, 0xee, 0xf3, 0x3e, 0xd9, 0x59, 0xf7,
+ 0x95, 0xd7, 0x91, 0x2f, 0x5c, 0x4a, 0xd6, 0xe4, 0x44, 0xa4, 0x51, 0x0d,
+ 0xec, 0xa1, 0x26, 0xd2, 0xc6, 0x15, 0xd5, 0x43, 0xff, 0xac, 0x70, 0x98,
+ 0xbf, 0xa2, 0xec, 0x43, 0xf9, 0x14, 0xf2, 0x9d, 0xc3, 0xaa, 0x42, 0x88,
+ 0xd0, 0x96, 0x78, 0xd9, 0x8a, 0x0e, 0x3e, 0x1c, 0xc4, 0xef, 0x49, 0x24,
+ 0x2a, 0xf7, 0x4f, 0xf3, 0xc1, 0xeb, 0x18, 0x17, 0x3c, 0x18, 0x58, 0x86,
+ 0xa3, 0x3a, 0x3f, 0x41, 0x9a, 0x04, 0x4e, 0xed, 0x64, 0xfe, 0x46, 0x36,
+ 0x4f, 0x62, 0xff, 0xae, 0x6c, 0x08, 0xa2, 0x2a, 0x76, 0xe4, 0x52, 0xaa,
+ 0x7c, 0xe8, 0xa6, 0xdd, 0x08, 0x51, 0xe5, 0xf4, 0x7d, 0x84, 0x61, 0x75,
+ 0xa7, 0x90, 0x3c, 0x5e, 0x67, 0x3d, 0xec, 0x11, 0xbd, 0x1b, 0x87, 0x03,
+ 0x48, 0x92, 0x58, 0x90, 0x6b, 0x90, 0x47, 0xfb, 0x30, 0x87, 0x6d, 0x0f,
+ 0x56, 0x78, 0x6a, 0x9b, 0x5d, 0xe9, 0xb5, 0xed, 0xbc, 0xfb, 0xa3, 0x58,
+ 0x04, 0x67, 0x2c, 0x94, 0x47, 0xed, 0xc3, 0x78, 0x77, 0x19, 0x25, 0x65,
+ 0xa1, 0xb9, 0x34, 0x34, 0xfe, 0x84, 0x29, 0x5c, 0x31, 0xc9, 0x57, 0x1e,
+ 0xbd, 0xa1, 0xd7, 0x6d, 0x7e, 0x11, 0x1e, 0xf9, 0x5e, 0xa8, 0x19, 0xda,
+ 0xe3, 0xad, 0xef, 0x4e, 0x0a, 0x69, 0x55, 0x6e, 0xb9, 0xbd, 0x6d, 0x02,
+ 0x14, 0x56, 0xff, 0xfe, 0xc2, 0x84, 0xbc, 0x2b, 0xc4, 0x21, 0x1d, 0xb3,
+ 0x39, 0x40, 0x09, 0x06, 0xb2, 0x0b, 0x64, 0x1b, 0xfc, 0xb1, 0x9e, 0x21,
+ 0x8d, 0x49, 0xd1, 0x67, 0x52, 0x0d, 0xa5, 0xae, 0x69, 0xbc, 0x37, 0xbe,
+ 0xff, 0xd0, 0x95, 0xad, 0x3e, 0x76, 0xbc, 0x82, 0x89, 0x15, 0x0a, 0x4e,
+ 0x4f, 0x7c, 0x5e, 0xa4, 0xda, 0xb4, 0xb1, 0x9d, 0x80, 0x57, 0x7b, 0x1b,
+ 0x56, 0x1b, 0xee, 0x25, 0x53, 0x1c, 0xeb, 0x35, 0x5d, 0x8f, 0x25, 0x4e,
+ 0x62, 0x26, 0xc2, 0x50, 0x2a, 0x9f, 0x27, 0x84, 0x4c, 0x21, 0x31, 0x18,
+ 0x54, 0xf7, 0xe3, 0x00, 0xe1, 0x39, 0xa6, 0xa1, 0x99, 0x93, 0x5f, 0xc1,
+ 0x95, 0x13, 0x6d, 0x32, 0x9e, 0x02, 0x53, 0x67, 0x87, 0x13, 0xdb, 0x41,
+ 0x1c, 0xef, 0x66, 0x6a, 0xc8, 0xf8, 0x6f, 0xeb, 0x51, 0xea, 0xed, 0xe2,
+ 0x5d, 0x5a, 0x26, 0xde, 0xa8, 0x53, 0xb8, 0x8e, 0x3b, 0x5a, 0x93, 0xf9,
+ 0xec, 0x1c, 0xaf, 0x12, 0x9d, 0x69, 0xb8, 0x6c, 0xf9, 0x77, 0xd9, 0xa6,
+ 0x45, 0x71, 0x7b, 0xd0, 0x16, 0x7a, 0xe2, 0x32, 0x40, 0x2d, 0xe3, 0x34,
+ 0x41, 0x9c, 0x4c, 0x2d, 0x19, 0x82, 0x11, 0x90, 0x39, 0xea, 0x9e, 0xd1,
+ 0x91, 0x6f, 0x06, 0x9f, 0x09, 0x7d, 0x26, 0x14, 0x35, 0x0b, 0xc2, 0xc1,
+ 0x24, 0x4b, 0xc7, 0xdc, 0xb8, 0x0a, 0x98, 0x94, 0x8e, 0x7a, 0x04, 0x7b,
+ 0x08, 0x95, 0x0e, 0x6f, 0x6c, 0x42, 0x1a, 0xd4, 0x2b, 0x93, 0x16, 0xe9,
+ 0xee, 0x8f, 0xff, 0x36, 0xde, 0x77, 0x5b, 0xec, 0x60, 0x34, 0x99, 0xbe,
+ 0x0a, 0x29, 0xdc, 0x8c, 0x1e, 0x5a, 0xc4, 0x8b, 0x01, 0x97, 0xd4, 0xd1,
+ 0x37, 0xe7, 0x69, 0x29, 0xe6, 0x8a, 0x69, 0xb9, 0xe0, 0xc2, 0x0d, 0x62,
+ 0x26, 0xd8, 0xa6, 0x8a, 0x09, 0xd6, 0x87, 0xfc, 0xea, 0x18, 0x1b, 0x55,
+ 0x31, 0xae, 0x48, 0xa7, 0x3f, 0x72, 0xc8, 0xf1, 0xe1, 0x2f, 0x32, 0x10,
+ 0x7d, 0xf8, 0xa0, 0xd4, 0x57, 0xd1, 0x7b, 0x80, 0x60, 0x3d, 0x20, 0x1a,
+ 0x53, 0x88, 0x5a, 0xc2, 0xb8, 0x86, 0xc3, 0xe2, 0x3b, 0x01, 0x95, 0xe6,
+ 0xbe, 0x0b, 0x36, 0x60, 0xaa, 0x72, 0xef, 0x18, 0x0f, 0x52, 0x4d, 0x56,
+ 0x78, 0x79, 0x9c, 0xaf, 0xb1, 0x23, 0x61, 0x4c, 0x5b, 0x2d, 0x9c, 0x96,
+ 0x1a, 0x09, 0x17, 0x4d, 0x87, 0x8c, 0xe6, 0xfb, 0xbd, 0x9f, 0x62, 0x13,
+ 0xce, 0x61, 0x4b, 0x4e, 0x28, 0xf6, 0xa3, 0x19, 0xe6, 0x9e, 0xf7, 0x7d,
+ 0x19, 0xde, 0x05, 0x39, 0xf8, 0x0b, 0xbc, 0x47, 0xcb, 0x64, 0xa8, 0xf2,
+ 0x26, 0x97, 0x73, 0xdb, 0x7b, 0x8b, 0x64, 0xc1, 0xec, 0x10, 0xc3, 0xce,
+ 0xf4, 0xfb, 0x74, 0x33, 0xeb, 0x48, 0xd3, 0x86, 0x04, 0x6b, 0x1a, 0xa6,
+ 0xd2, 0x3c, 0x4a, 0xa2, 0x91, 0x35, 0x05, 0x21, 0x12, 0x93, 0xdb, 0x4e,
+ 0xf3, 0x74, 0x63, 0x32, 0xd5, 0x15, 0xb3, 0x1b, 0xa1, 0x0c, 0x72, 0xfc,
+ 0x35, 0x79, 0xf5, 0x39, 0x4a, 0x0b, 0x6b, 0x35, 0xd4, 0xa8, 0x59, 0x8e,
+ 0xe1, 0x41, 0x5f, 0x80, 0xc0, 0x7e, 0x8c, 0xea, 0x61, 0x71, 0x03, 0x6d,
+ 0xd0, 0x6d, 0x03, 0xec, 0x5c, 0x50, 0xec, 0xea, 0x70, 0x75, 0x65, 0x6a,
+ 0xc1, 0x5d, 0xbc, 0x4f, 0xf5, 0xc9, 0xaa, 0x3f, 0x84, 0x90, 0x5d, 0x52,
+ 0x5b, 0xde, 0xc4, 0xa8, 0x80, 0xbb, 0xff, 0x1b, 0x21, 0xd3, 0xc0, 0x32,
+ 0x4c, 0xea, 0x2f, 0xd1, 0xc4, 0x8d, 0x41, 0x5d, 0x89, 0x5c, 0xb9, 0xc3,
+ 0x5c, 0x92, 0x7e, 0xd8, 0x91, 0xed, 0xdc, 0xc3, 0xfe, 0x78, 0xb8, 0xd0,
+ 0x4c, 0x29, 0x9e, 0x1e, 0x52, 0xe8, 0x1a, 0x89, 0xa9, 0xb1, 0xd3, 0x9f,
+ 0x4d, 0x69, 0xf1, 0x02, 0x95, 0xd6, 0x0e, 0xc5, 0xea, 0x88, 0xe6, 0x3b,
+ 0x20, 0x37, 0xd5, 0xfc, 0xec, 0x38, 0xcc, 0xd1, 0x24, 0x29, 0x54, 0x13,
+ 0xd8, 0xb9, 0x55, 0x35, 0x72, 0xd9, 0x21, 0x43, 0xcd, 0xfa, 0x83, 0xa1,
+ 0x82, 0x73, 0x52, 0xf5, 0x05, 0x34, 0xfa, 0xe6, 0xf2, 0xfc, 0xeb, 0xe2,
+ 0xaa, 0xc7, 0x0f, 0x6a, 0xeb, 0xb9, 0x74, 0xf0, 0xde, 0x04, 0xd6, 0xc0,
+ 0xf7, 0x5e, 0xdb, 0x3e, 0xc4, 0xbf, 0x1b, 0xc9, 0x59, 0xf0, 0x55, 0x0a,
+ 0x8e, 0xdb, 0x84, 0xaf, 0x5d, 0xcf, 0x4d, 0xe8, 0xed, 0xc3, 0x01, 0xd1,
+ 0xb4, 0xd6, 0xe0, 0x19, 0xb9, 0x1d, 0x6c, 0x26, 0x9d, 0xdb, 0xac, 0xb3,
+ 0x60, 0x9b, 0xa4, 0xa9, 0xfb, 0xb4, 0xba, 0x4d, 0x0e, 0xb3, 0x38, 0x60,
+ 0xb1, 0x26, 0x76, 0x74, 0xb8, 0x4b, 0x42, 0xa3, 0x93, 0x7a, 0xca, 0xc9,
+ 0x14, 0xb3, 0xce, 0xd8, 0x4b, 0xc9, 0x51, 0x75, 0x4e, 0x87, 0xb0, 0xc7,
+ 0x0c, 0x53, 0xa3, 0xc3, 0x06, 0xd2, 0x93, 0xaa, 0xb5, 0x57, 0xad, 0xbc,
+ 0x02, 0xf8, 0x0d, 0xa8, 0x22, 0x63, 0x2e, 0x61, 0x56, 0xd1, 0xa9, 0x35,
+ 0x69, 0x83, 0x45, 0x82, 0xe1, 0x83, 0x4b, 0xae, 0x45, 0x3d, 0xdc, 0x83,
+ 0x87, 0x58, 0x1f, 0xe2, 0x69, 0xb8, 0x16, 0x55, 0xcb, 0xcc, 0x51, 0xc0,
+ 0xe7, 0x92, 0x9b, 0x62, 0x64, 0x57, 0x59, 0x01, 0x80, 0x10, 0xc0, 0xa9,
+ 0x26, 0x20, 0xa8, 0x10, 0x0b, 0xbe, 0x1f, 0x71, 0x51, 0xe3, 0x71, 0x3d,
+ 0x89, 0x77, 0x49, 0x77, 0xd1, 0x00, 0x19, 0xb0, 0x0e, 0x55, 0x0e, 0x91,
+ 0x0b, 0x79, 0xdb, 0xbe, 0x0f, 0x04, 0xd8, 0x25, 0xfb, 0xc5, 0x11, 0x99,
+ 0x48, 0xa6, 0xe6, 0x8f, 0xf9, 0x31, 0x31, 0x75, 0x6e, 0x13, 0x21, 0x5f,
+ 0x8a, 0xd9, 0xc4, 0xd7, 0xb9, 0x7a, 0x29, 0x4f, 0x58, 0x0d, 0x41, 0x4c,
+ 0x17, 0x4f, 0xb6, 0x35, 0xf3, 0x71, 0x2b, 0x11, 0x47, 0x16, 0x3b, 0xe7,
+ 0x02, 0xa0, 0xb5, 0x9b, 0x24, 0xf1, 0x52, 0xd6, 0x70, 0xf8, 0x20, 0x6a,
+ 0x4d, 0xfc, 0x10, 0x9f, 0x8e, 0x1f, 0xbe, 0x11, 0x1d, 0x50, 0x27, 0xf8,
+ 0x4e, 0x03, 0x4b, 0x22, 0x09, 0xcf, 0xab, 0xc4, 0x69, 0x17, 0x5e, 0x3a,
+ 0x0a, 0x7d, 0xc5, 0x76, 0x4a, 0xf4, 0x74, 0xce, 0x2d, 0x60, 0x0e, 0x20,
+ 0xa6, 0x7e, 0x8c, 0x9e, 0x83, 0x34, 0x45, 0x73, 0xfc, 0xdb, 0xd5, 0xe7,
+ 0x97, 0x58, 0x60, 0x63, 0x98, 0x5f, 0xa3, 0xea, 0x4e, 0x9a, 0x7a, 0x17,
+ 0x38, 0x6b, 0xb1, 0xba, 0x67, 0xb3, 0x62, 0x3a, 0x69, 0x9b, 0x94, 0xa6,
+ 0x6c, 0x0a, 0x32, 0x96, 0x8f, 0x6e, 0xe6, 0x1a, 0x50, 0x2d, 0x36, 0x77,
+ 0xed, 0x7b, 0xaf, 0xa4, 0xf1, 0xd8, 0x3c, 0x69, 0x6b, 0x01, 0xec, 0xa2,
+ 0x72, 0x74, 0x5d, 0xcb, 0x65, 0x42, 0xb8, 0x9a, 0x89, 0x33, 0xa2, 0x0c,
+ 0x07, 0xc8, 0x66, 0xd4, 0x57, 0x3b, 0x0c, 0x04, 0xda, 0x65, 0xc4, 0xfb,
+ 0xb1, 0xcc, 0x43, 0xe2, 0x0c, 0x1f, 0xf4, 0x5a, 0x85, 0xa5, 0x78, 0xfb,
+ 0xc5, 0x49, 0x85, 0xc1, 0x7f, 0x52, 0xec, 0xc6, 0x47, 0x0e, 0xe7, 0x93,
+ 0x2f, 0x51, 0xbf, 0x35, 0x71, 0x21, 0x3c, 0x7b, 0x35, 0x91, 0xe9, 0x12,
+ 0xc8, 0x7b, 0xeb, 0x76, 0x1a, 0x4a, 0xca, 0xc8, 0x63, 0xa5, 0xce, 0x20,
+ 0xf4, 0x1b, 0x26, 0x30, 0x5c, 0x00, 0xe7, 0x83, 0xf6, 0x57, 0x6b, 0x47,
+ 0x02, 0x09, 0x7c, 0x8e, 0x6d, 0x4f, 0x39, 0x9c, 0x73, 0x6c, 0x91, 0xb9,
+ 0xda, 0x93, 0x18, 0xa2, 0x27, 0x2f, 0x4f, 0x59, 0x79, 0x36, 0xab, 0x16,
+ 0x7e, 0x4e, 0x41, 0x55, 0x20, 0x76, 0xd7, 0xf3, 0xa1, 0x89, 0x1b, 0xce,
+ 0xbc, 0x7e, 0xaf, 0x8b, 0x82, 0x24, 0x74, 0x65, 0xe6, 0xa0, 0xf6, 0x33,
+ 0xc0, 0xb3, 0xae, 0x19, 0x96, 0x51, 0xd5, 0xaa, 0xe8, 0xa4, 0x4d, 0xf9,
+ 0x7d, 0x84, 0x21, 0x30, 0xe4, 0xf9, 0x61, 0xfe, 0xd0, 0x2a, 0xf0, 0xb4,
+ 0x93, 0x97, 0xb8, 0x40, 0x6a, 0x41, 0xb0, 0xc0, 0x1c, 0x9a, 0x9a, 0x9d,
+ 0x51, 0x03, 0x30, 0x89, 0xe1, 0xde, 0x8c, 0x23, 0x0f, 0x00, 0x04, 0x01,
+ 0x45, 0x71, 0x33, 0x43, 0xfd, 0x8b, 0x5a, 0xf2, 0x2d, 0x1c, 0xe6, 0x45,
+ 0xc7, 0xac, 0xac, 0x3b, 0x35, 0x7a, 0xb9, 0x85, 0x37, 0xd2, 0x37, 0x8b,
+ 0x52, 0xb6, 0x0b, 0xe6, 0x86, 0xae, 0x15, 0x8e, 0xff, 0x71, 0x8b, 0xcb,
+ 0x42, 0x9d, 0xd6, 0x9d, 0xfb, 0x64, 0x97, 0x59, 0x81, 0xb7, 0x81, 0x95,
+ 0x06, 0xba, 0x82, 0x09, 0xdf, 0x32, 0x17, 0x48, 0xfd, 0x5d, 0x3a, 0x1c,
+ 0xcc, 0x75, 0xcd, 0x69, 0x34, 0xd9, 0x57, 0xb0, 0xc1, 0x04, 0xbb, 0x71,
+ 0x3c, 0x1e, 0xf6, 0xf5, 0xa9, 0x06, 0x72, 0x02, 0x9c, 0x0c, 0x9b, 0x5d,
+ 0x8d, 0x58, 0x67, 0x4f, 0xf8, 0x74, 0xf2, 0x6f, 0x9a, 0x56, 0xba, 0xf1,
+ 0x0d, 0x0b, 0x9a, 0x21, 0xd0, 0x7a, 0x4c, 0x38, 0x82, 0x83, 0x51, 0xe4,
+ 0xad, 0x3f, 0xb1, 0x37, 0x32, 0xca, 0xc0, 0x2a, 0xf5, 0x0d, 0x85, 0x0d,
+ 0xf1, 0x59, 0x79, 0xcd, 0x3b, 0x10, 0x7c, 0xba, 0x07, 0x88, 0x29, 0xe0,
+ 0xef, 0x97, 0x7b, 0xcc, 0xfc, 0x64, 0xd3, 0x39, 0x1d, 0x34, 0x7a, 0x46,
+ 0x9d, 0xb9, 0xe8, 0x99, 0x71, 0x16, 0x95, 0x4a, 0xac, 0xf5, 0xd0, 0xcd,
+ 0x47, 0xd1, 0xbb, 0xdc, 0x0b, 0x4a, 0xcc, 0x4a, 0xb7, 0x67, 0x3f, 0xb6,
+ 0xd9, 0x38, 0x44, 0x29, 0x71, 0xa9, 0x4a, 0x0e, 0x57, 0x3e, 0xed, 0x60,
+ 0xef, 0xe9, 0xd4, 0xc4, 0x4e, 0xeb, 0x82, 0xab, 0xb8, 0x01, 0x51, 0x9d,
+ 0xad, 0xaa, 0xdc, 0xc1, 0xad, 0xf2, 0x00, 0x6e, 0x60, 0xfe, 0x63, 0xe3,
+ 0x49, 0x9a, 0xe0, 0x7f, 0x03, 0xe6, 0xcb, 0x82, 0xab, 0x25, 0xbc, 0x90,
+ 0xea, 0xbe, 0xd9, 0xf0, 0x3b, 0x92, 0x81, 0x64, 0x15, 0xb7, 0xa3, 0x78,
+ 0xe3, 0xd1, 0xb2, 0x46, 0x8e, 0xdc, 0x30, 0xb4, 0x56, 0x18, 0x82, 0x65,
+ 0xf4, 0x26, 0x08, 0x85, 0x66, 0x02, 0xb6, 0x69, 0x88, 0x3d, 0x18, 0x66,
+ 0xf4, 0x73, 0x74, 0x11, 0x8d, 0x16, 0xd8, 0x9a, 0x4f, 0xfe, 0x3d, 0x14,
+ 0x91, 0x76, 0x3a, 0xed, 0x00, 0x1d, 0x1e, 0x2f, 0xdd, 0x9f, 0xc2, 0xa2,
+ 0x54, 0x18, 0x4f, 0x1f, 0xd3, 0x2e, 0x64, 0x74, 0xfd, 0x84, 0xc2, 0x84,
+ 0x88, 0x27, 0xb2, 0xdd, 0x71, 0xd6, 0x38, 0x91, 0xcd, 0xcd, 0x86, 0xe9,
+ 0x89, 0x06, 0x8e, 0x91, 0x1d, 0xac, 0xbb, 0x55, 0xa5, 0x82, 0x07, 0x5a,
+ 0x19, 0x02, 0x7e, 0xc8, 0x44, 0xe2, 0x9a, 0x9b, 0xb5, 0xf1, 0xcb, 0x5a,
+ 0x03, 0x34, 0x8f, 0x28, 0x1d, 0xa8, 0x2d, 0xd7, 0x9b, 0x55, 0xde, 0x42,
+ 0x2e, 0xdd, 0x9a, 0x84, 0x9a, 0x09, 0x57, 0xca, 0xdd, 0x0b, 0x01, 0xc7,
+ 0x8f, 0xc8, 0x08, 0x21, 0x8d, 0xf2, 0x11, 0xb3, 0x94, 0x6e, 0x00, 0xc3,
+ 0xac, 0x19, 0x0f, 0x80, 0x21, 0xc8, 0x8e, 0x5f, 0x83, 0x0d, 0x19, 0x9d,
+ 0x14, 0x4a, 0xa5, 0x6e, 0x24, 0xe9, 0x24, 0x75, 0xbc, 0x10, 0xe9, 0xc2,
+ 0x75, 0xe0, 0x60, 0xa5, 0x60, 0xd3, 0x64, 0x6e, 0x6e, 0xe3, 0x2e, 0x93,
+ 0x81, 0x42, 0x66, 0xa0, 0xa4, 0x96, 0xae, 0x46, 0xc9, 0xc3, 0xe4, 0x74,
+ 0xac, 0xc8, 0x62, 0xc3, 0xd7, 0x4e, 0xa1, 0x38, 0x7f, 0x6f, 0xd0, 0x13,
+ 0x0e, 0x68, 0x2e, 0x85, 0x1d, 0x83, 0xab, 0xc2, 0x7a, 0x2a, 0x1b, 0x1d,
+ 0xaf, 0x09, 0x32, 0x41, 0xd0, 0xfb, 0xd3, 0x57, 0x13, 0x18, 0x5a, 0x87,
+ 0xf5, 0xc5, 0xca, 0xda, 0x3e, 0xaa, 0x2c, 0x9c, 0x3a, 0x8a, 0x07, 0x42,
+ 0x98, 0x7a, 0xfa, 0xc5, 0xc8, 0x1d, 0xec, 0xa4, 0x65, 0xea, 0x01, 0x80,
+ 0x35, 0x9c, 0x93, 0xd0, 0xdd, 0x49, 0x55, 0x58, 0x99, 0xfc, 0x4a, 0x32,
+ 0xe3, 0xdc, 0x73, 0xde, 0x10, 0xcd, 0x77, 0xba, 0x20, 0x3a, 0x60, 0x88,
+ 0x8a, 0x06, 0xc2, 0x64, 0x4e, 0xd8, 0xc5, 0x03, 0x2c, 0x46, 0xf8, 0x48,
+ 0x34, 0x1e, 0xa6, 0x17, 0x24, 0xb2, 0x37, 0xc3, 0x52, 0xfc, 0xf9, 0x42,
+ 0xfa, 0x51, 0x05, 0x03, 0xf3, 0x48, 0x38, 0x14, 0x38, 0xfa, 0x54, 0xb4,
+ 0x87, 0xee, 0x5a, 0xfe, 0xef, 0xd1, 0x7b, 0x8b, 0x37, 0xf5, 0xc5, 0x7f,
+ 0x29, 0xf7, 0x12, 0x00, 0x84, 0xd0, 0x30, 0xc8, 0x03, 0x4b, 0xa5, 0xf9,
+ 0x4a, 0x2c, 0xae, 0x69, 0x1c, 0xea, 0x5f, 0x3b, 0xda, 0xe8, 0x15, 0xe5,
+ 0xb9, 0xf9, 0x51, 0x02, 0x1c, 0x9a, 0xc6, 0x27, 0xb9, 0x5d, 0x7e, 0x96,
+ 0x64, 0xd0, 0x34, 0xc9, 0x18, 0xe4, 0x6b, 0x0b, 0x83, 0xb6, 0x2b, 0x49,
+ 0x13, 0x75, 0x08, 0xbd, 0xf5, 0x5a, 0x53, 0xf6, 0xd4, 0xa6, 0x76, 0x87,
+ 0xc9, 0x18, 0x5c, 0x10, 0x73, 0x3f, 0x47, 0x81, 0x31, 0x72, 0x35, 0xcf,
+ 0x9c, 0x97, 0xd5, 0x55, 0x1a, 0x01, 0x5f, 0x0f, 0x56, 0x53, 0xa1, 0x07,
+ 0x55, 0x4a, 0x57, 0x79, 0xd9, 0xbe, 0x69, 0x91, 0x57, 0x6e, 0x3e, 0x4a,
+ 0xc3, 0xe8, 0xce, 0x3f, 0x1b, 0x5c, 0xc7, 0x82, 0x92, 0x6d, 0xcd, 0xbf,
+ 0x4d, 0x8b, 0xf4, 0x0c, 0x2c, 0x54, 0x65, 0x2d, 0xb3, 0x03, 0x21, 0x54,
+ 0x67, 0xd7, 0x91, 0xf7, 0x4c, 0xf4, 0x17, 0x79, 0x56, 0xe3, 0x15, 0xfc,
+ 0x69, 0xb7, 0xab, 0x84, 0xc5, 0x92, 0x3e, 0x9a, 0x47, 0x54, 0xb4, 0xf1,
+ 0x6e, 0xc3, 0xf2, 0x4a, 0x36, 0xcf, 0x35, 0x7a, 0xb4, 0xc8, 0x64, 0x91,
+ 0xb2, 0x5b, 0x6e, 0x68, 0x3e, 0x95, 0x9e, 0xeb, 0x8d, 0xb0, 0x66, 0x62,
+ 0xf4, 0xc3, 0x6c, 0xf2, 0xb0, 0x9d, 0x32, 0xc5, 0x95, 0x2d, 0xb9, 0x72,
+ 0xd5, 0xc8, 0xe2, 0x58, 0x10, 0xda, 0xb2, 0x1f, 0xc0, 0x54, 0xdb, 0x49,
+ 0x96, 0x9f, 0xb0, 0x21, 0x3b, 0x6a, 0x42, 0x8b, 0xdc, 0xfa, 0x4b, 0x4e,
+ 0x2b, 0xe0, 0xa1, 0x0d, 0x43, 0xbc, 0x5b, 0x9b, 0x05, 0xc9, 0x9f, 0x40,
+ 0x15, 0x1a, 0x65, 0x69, 0x40, 0xab, 0x80, 0x01, 0xe9, 0x5e, 0xac, 0x8b,
+ 0xb4, 0x64, 0x9b, 0xac, 0x11, 0x72, 0x57, 0xa3, 0x24, 0x18, 0x55, 0x2a,
+ 0x6f, 0x32, 0xb7, 0xe1, 0x3f, 0x7c, 0x42, 0x0a, 0x0a, 0xc8, 0xe7, 0x17,
+ 0x7d, 0x1e, 0x67, 0xdc, 0xea, 0x92, 0x84, 0x65, 0xc2, 0xbc, 0x84, 0x2c,
+ 0x54, 0x08, 0xab, 0x25, 0x8e, 0x59, 0x3d, 0x5a, 0x1b, 0xb6, 0xb7, 0x17,
+ 0xd5, 0x0f, 0x98, 0x2f, 0x7c, 0x17, 0x10, 0x3c, 0x04, 0x84, 0x46, 0x34,
+ 0x1e, 0x32, 0x3b, 0x70, 0xda, 0x4f, 0xde, 0x29, 0xe7, 0x87, 0x41, 0xb9,
+ 0x4d, 0x29, 0x3b, 0xd3, 0x14, 0x39, 0x35, 0x7a, 0x84, 0x6e, 0x80, 0xb2,
+ 0xc2, 0xe2, 0x28, 0x91, 0xc0, 0x61, 0xbb, 0x4d, 0x19, 0x8a, 0xac, 0x96,
+ 0x25, 0xba, 0x65, 0x41, 0xc9, 0xd0, 0xbc, 0xd6, 0x2e, 0xf6, 0x88, 0xd1,
+ 0x75, 0xe7, 0x6b, 0x86, 0xa1, 0x87, 0x4e, 0xae, 0xdb, 0x0c, 0x1a, 0x03,
+ 0x87, 0x4b, 0xd1, 0xb2, 0x37, 0x00, 0x22, 0x29, 0x7d, 0xc8, 0x37, 0x4a,
+ 0x29, 0xc6, 0xd3, 0x12, 0xd1, 0x3d, 0xdf, 0x00, 0x19, 0xab, 0x70, 0x65,
+ 0x31, 0x76, 0x68, 0x76, 0x40, 0x0d, 0x7a, 0xb6, 0x5a, 0xe1, 0xbe, 0x95,
+ 0xa7, 0x2a, 0x77, 0xb6, 0xf7, 0x5b, 0x94, 0xc3, 0xba, 0x4e, 0x6b, 0x93,
+ 0x4f, 0x98, 0x6b, 0xda, 0x28, 0xa3, 0x41, 0x28, 0xbc, 0xed, 0xa9, 0x51,
+ 0x26, 0x98, 0xf6, 0xef, 0xa6, 0x55, 0x3e, 0x6d, 0xc1, 0x87, 0x59, 0xb9,
+ 0xb0, 0xdc, 0xea, 0x6f, 0x2c, 0x2a, 0xa3, 0x14, 0x43, 0x6d, 0x72, 0xfc,
+ 0x06, 0x06, 0x9c, 0xd5, 0xfa, 0xe9, 0xfb, 0x9f, 0x57, 0xb1, 0x4f, 0x69,
+ 0x47, 0xee, 0xb2, 0x59, 0x44, 0xe3, 0x88, 0x44, 0x04, 0xa4, 0xb2, 0x7b,
+ 0x3e, 0xed, 0xb3, 0xde, 0x60, 0x71, 0x23, 0xdb, 0x88, 0x6b, 0x54, 0x6d,
+ 0xee, 0xd6, 0xc1, 0x29, 0xdb, 0x19, 0xfa, 0x36, 0x5b, 0xba, 0x1c, 0x93,
+ 0x1c, 0xfd, 0x6c, 0x32, 0x6d, 0xdc, 0x92, 0x85, 0x92, 0xf9, 0x50, 0xad,
+ 0x97, 0x43, 0x32, 0xe2, 0x23, 0x27, 0x84, 0x16, 0x9d, 0x11, 0x15, 0x2b,
+ 0x73, 0x14, 0xd6, 0xc3, 0x98, 0xa7, 0x48, 0xac, 0x6e, 0xca, 0x23, 0x1b,
+ 0x6a, 0xa8, 0x27, 0x4c, 0xfd, 0x69, 0x17, 0x8e, 0x0e, 0x52, 0x5e, 0x36,
+ 0x36, 0x46, 0x18, 0xb5, 0xa1, 0xf7, 0x5f, 0x02, 0xcf, 0x2d, 0xeb, 0x6f,
+ 0xcc, 0x61, 0x12, 0x32, 0x2a, 0x7a, 0xae, 0x2d, 0x7c, 0x30, 0xd4, 0x72,
+ 0xb1, 0xdf, 0x01, 0x89, 0xe0, 0x00, 0x9e, 0x48, 0x48, 0x04, 0x42, 0xb2,
+ 0x15, 0x30, 0xc5, 0xc1, 0x51, 0x49, 0x3c, 0x67, 0x57, 0xd6, 0xd7, 0xf8,
+ 0x30, 0xc6, 0xa7, 0x97, 0x84, 0x7f, 0x03, 0x6c, 0xdf, 0xd2, 0xb4, 0xf8,
+ 0x40, 0xcf, 0x6c, 0x6a, 0x72, 0xa1, 0x85, 0xb4, 0x6d, 0x5a, 0x3f, 0xa1,
+ 0xac, 0xbb, 0xd2, 0xac, 0x97, 0xfb, 0x33, 0x53, 0xfe, 0xf9, 0x4b, 0xae,
+ 0x0e, 0xa1, 0x39, 0xfc, 0x8c, 0xdb, 0xf9, 0x4f, 0xbf, 0xe8, 0x48, 0x91,
+ 0xc1, 0xe3, 0x01, 0xf3, 0x4b, 0xd6, 0x24, 0xcc, 0xd0, 0x11, 0x77, 0x52,
+ 0x5a, 0x09, 0x6f, 0xa9, 0x42, 0x32, 0x0b, 0x7a, 0xf4, 0xa0, 0xf4, 0x04,
+ 0x5a, 0x10, 0x72, 0x84, 0x4c, 0x25, 0x51, 0xbb, 0xb9, 0xd2, 0xf9, 0xbc,
+ 0x2d, 0xc2, 0x32, 0xf7, 0xa3, 0xd1, 0x27, 0x6f, 0x9a, 0xa0, 0xda, 0xc9,
+ 0x46, 0xcf, 0xfb, 0x3f, 0xb5, 0xf8, 0x3d, 0xe8, 0xd2, 0x72, 0x93, 0x66,
+ 0x78, 0x2c, 0x77, 0x2d, 0x10, 0x78, 0x3f, 0xa8, 0x87, 0x90, 0x5d, 0xe4,
+ 0x2c, 0x77, 0xf4, 0x1d, 0x02, 0x23, 0xf5, 0xf2, 0xdc, 0x71, 0x15, 0xa4,
+ 0xc0, 0xda, 0x00, 0x80, 0xcc, 0x62, 0x91, 0x43, 0xc5, 0xe5, 0x80, 0x09,
+ 0x46, 0xaa, 0x1b, 0x7a, 0x10, 0x8f, 0x56, 0x45, 0x72, 0xa9, 0xa6, 0x87,
+ 0x1b, 0x92, 0xb3, 0x95, 0x13, 0x76, 0xf4, 0x88, 0x44, 0xd8, 0x39, 0x6c,
+ 0x82, 0x4d, 0x00, 0x90, 0x79, 0xc6, 0x84, 0x10, 0xb4, 0x2e, 0x54, 0x14,
+ 0x8a, 0x42, 0x3e, 0x4c, 0x35, 0x7e, 0x8b, 0xbb, 0x0c, 0x49, 0x7c, 0xff,
+ 0xa0, 0x58, 0xc2, 0x35, 0x67, 0xf3, 0x49, 0x3e, 0xa2, 0x41, 0x78, 0xb9,
+ 0x47, 0x4d, 0x0d, 0xfd, 0x26, 0xb2, 0xf1, 0xe7, 0x0b, 0x63, 0xb1, 0xa7,
+ 0x31, 0x10, 0x27, 0x1e, 0x53, 0x86, 0x6a, 0xf7, 0xb3, 0x10, 0xd9, 0x81,
+ 0x64, 0x2e, 0xe4, 0xbd, 0xdf, 0x0a, 0x15, 0x54, 0xa3, 0xe9, 0x57, 0xc0,
+ 0xdf, 0xd9, 0xb9, 0xeb, 0x42, 0xe8, 0x76, 0xf0, 0x3c, 0xcb, 0xbf, 0xbf,
+ 0xae, 0x57, 0xd5, 0xe9, 0x5a, 0x78, 0x6e, 0x21, 0xc4, 0xc9, 0x36, 0x22,
+ 0x6a, 0x99, 0x27, 0xfd, 0xf5, 0xd6, 0xc8, 0x42, 0x80, 0xc6, 0x06, 0x30,
+ 0x08, 0xa0, 0x25, 0x3f, 0x0e, 0xf0, 0x8e, 0xe0, 0x0d, 0xb6, 0x0a, 0xe5,
+ 0x08, 0x13, 0x3d, 0xe8, 0x2d, 0x35, 0x82, 0x28, 0x84, 0xb9, 0x2e, 0x8a,
+ 0x78, 0xd0, 0xeb, 0xa7, 0xaf, 0xa5, 0x11, 0x2a, 0x83, 0x72, 0xe8, 0x07,
+ 0x93, 0xaa, 0xff, 0x11, 0xd0, 0x95, 0x33, 0xc4, 0x00, 0x76, 0xc0, 0xab,
+ 0xe6, 0x29, 0xda, 0xb3, 0x67, 0x2f, 0x5d, 0xed, 0xf9, 0x89, 0xfb, 0x62,
+ 0x2d, 0x7a, 0x8f, 0xd8, 0xa7, 0x92, 0xd1, 0xf0, 0x4c, 0x73, 0x34, 0xa1,
+ 0xff, 0x4c, 0xa7, 0x3c, 0x0e, 0x4a, 0x4b, 0x0e, 0xd2, 0x3e, 0xdd, 0x9d,
+ 0xa5, 0x3c, 0xed, 0xc2, 0x50, 0x46, 0x5e, 0xcd, 0x81, 0x81, 0x49, 0xca,
+ 0x9e, 0x50, 0x40, 0xc1, 0xae, 0xd8, 0x30, 0xc9, 0x64, 0x4b, 0x52, 0x84,
+ 0x57, 0x0d, 0xa2, 0x11, 0x40, 0x00, 0x07, 0x10, 0x43, 0x49, 0x18, 0x33,
+ 0x5a, 0x29, 0xfd, 0xb1, 0xe6, 0x37, 0x44, 0x7d, 0xad, 0xe8, 0xda, 0x6d,
+ 0x19, 0x8b, 0x31, 0x8c, 0x01, 0xb0, 0xcb, 0xa4, 0x8d, 0x14, 0x42, 0x66,
+ 0xa5, 0x7b, 0x25, 0x00, 0xe6, 0x92, 0x5b, 0xef, 0x21, 0xe6, 0x16, 0x1b,
+ 0xbf, 0xce, 0x80, 0x80, 0x8c, 0x97, 0xcf, 0x66, 0xd5, 0xa1, 0xe5, 0xbc,
+ 0x82, 0x6b, 0xab, 0x0c, 0x68, 0x74, 0x39, 0x71, 0x9c, 0x98, 0x42, 0xa1,
+ 0xc3, 0xe6, 0x75, 0xb1, 0x5e, 0xea, 0x2c, 0xe1, 0xb1, 0x14, 0x52, 0x65,
+ 0x5e, 0xb1, 0xa3, 0x51, 0x8d, 0x19, 0xb7, 0xee, 0x1f, 0x29, 0x99, 0xf7,
+ 0x20, 0x01, 0x26, 0xa3, 0xd5, 0xf0, 0x80, 0x24, 0xf7, 0x8a, 0xcb, 0x1c,
+ 0x33, 0xc1, 0x9a, 0x0c, 0x0c, 0x27, 0xb9, 0x6c, 0x18, 0x39, 0xb6, 0x87,
+ 0x9d, 0x35, 0x30, 0xff, 0xaa, 0xeb, 0xb3, 0x1f, 0x59, 0xd1, 0xb5, 0x06,
+ 0x6f, 0x62, 0xba, 0x2d, 0x61, 0xd6, 0xf4, 0xf4, 0xb7, 0x86, 0xda, 0x83,
+ 0x2b, 0x94, 0xf6, 0x1d, 0x68, 0x63, 0x27, 0xeb, 0xb0, 0xd0, 0xd2, 0x24,
+ 0xfd, 0xbe, 0xed, 0x91, 0xfc, 0xb1, 0xc9, 0x09, 0xda, 0x54, 0x0a, 0x76,
+ 0x75, 0xea, 0xc4, 0xba, 0xb7, 0x7e, 0x1b, 0xb2, 0x0b, 0x53, 0x53, 0xed,
+ 0x54, 0xd0, 0xa6, 0xc1, 0x85, 0xa1, 0x1a, 0x62, 0x16, 0xde, 0xca, 0xe9,
+ 0xd9, 0x55, 0x31, 0x5c, 0x6c, 0x0b, 0x60, 0x3b, 0x39, 0x29, 0x5c, 0x49,
+ 0x88, 0x20, 0xc5, 0x80, 0x47, 0xfd, 0x94, 0x0f, 0xac, 0x81, 0x9c, 0x98,
+ 0xe9, 0xfd, 0x05, 0x70, 0x85, 0xa3, 0xbf, 0x4b, 0xf9, 0x57, 0xbe, 0xe3,
+ 0x01, 0xde, 0xb3, 0x23, 0xf3, 0x64, 0xd4, 0xe0, 0x89, 0x3c, 0xfb, 0xe3,
+ 0x45, 0x3e, 0xda, 0x03, 0x0b, 0xd8, 0x80, 0xae, 0x07, 0x50, 0x19, 0x59,
+ 0x25, 0x7f, 0x93, 0x07, 0x75, 0xd2, 0x9a, 0x94, 0x94, 0xdf, 0x8f, 0x8c,
+ 0xd0, 0xc7, 0x38, 0x37, 0xf4, 0xbb, 0x27, 0x02, 0xbb, 0x40, 0x44, 0x3b,
+ 0xea, 0xfd, 0xb4, 0xdd, 0x6a, 0x29, 0x7d, 0x3c, 0x83, 0xef, 0xc4, 0x47,
+ 0x88, 0x9d, 0xed, 0xf1, 0x24, 0x62, 0xae, 0xe0, 0x9d, 0xa9, 0xb2, 0x47,
+ 0x4d, 0xfc, 0xb2, 0xc4, 0x2e, 0xd7, 0x6e, 0x0b, 0x65, 0xf8, 0xe9, 0x12,
+ 0xc7, 0xb7, 0x19, 0x33, 0x4b, 0x1f, 0x70, 0xa3, 0x35, 0x51, 0x38, 0xf8,
+ 0x4c, 0x59, 0xf5, 0xe9, 0xf8, 0xff, 0x4b, 0x2d, 0x3d, 0x5a, 0xdc, 0x67,
+ 0x5c, 0xfd, 0x17, 0x82, 0x13, 0xde, 0x2d, 0x99, 0xab, 0x85, 0xe0, 0xfa,
+ 0xe2, 0x3d, 0x1b, 0x95, 0x0f, 0xd2, 0xd4, 0xc8, 0x51, 0x31, 0x03, 0x1b,
+ 0xa6, 0xf3, 0xb0, 0x54, 0x0d, 0x58, 0x84, 0xfd, 0x34, 0x01, 0xeb, 0xd3,
+ 0xc4, 0x12, 0x05, 0x84, 0x06, 0xa3, 0x3a, 0x18, 0xc9, 0x2d, 0xad, 0x73,
+ 0x1e, 0x3b, 0x1b, 0xbb, 0x29, 0xcb, 0x59, 0xd3, 0x02, 0x81, 0xaf, 0x4d,
+ 0x62, 0x39, 0xad, 0x3d, 0xa4, 0x79, 0xd8, 0x0d, 0x9c, 0x44, 0xdc, 0x0c,
+ 0x16, 0xd6, 0xfd, 0xdf, 0x2d, 0xd9, 0x1e, 0x74, 0x14, 0xc4, 0x61, 0x26,
+ 0x13, 0x49, 0xed, 0xfb, 0xcb, 0x71, 0xd6, 0x72, 0x87, 0xe8, 0x5a, 0xcf,
+ 0xa2, 0x59, 0xf5, 0x45, 0x47, 0xae, 0xc9, 0x8d, 0x86, 0x0a, 0x2a, 0x3b,
+ 0xd2, 0x57, 0x50, 0x95, 0x5f, 0x52, 0x43, 0xc7, 0xad, 0xb3, 0x8b, 0x44,
+ 0xee, 0x9a, 0x9f, 0x84, 0x04, 0x36, 0x8c, 0x6f, 0x76, 0xbd, 0xcd, 0xd8,
+ 0xf3, 0x32, 0x32, 0x4f, 0xdd, 0x8d, 0x78, 0x75, 0x81, 0x75, 0x27, 0x18,
+ 0x73, 0xc4, 0xde, 0x0c, 0x3b, 0x58, 0x2e, 0x36, 0x2c, 0xba, 0x0d, 0x06,
+ 0x53, 0x52, 0x6c, 0x0d, 0xf1, 0x4e, 0xf8, 0xfc, 0x59, 0xe1, 0xd5, 0x38,
+ 0x4b, 0x25, 0x48, 0x9e, 0x35, 0xfa, 0x83, 0x32, 0xe4, 0x9c, 0x27, 0x81,
+ 0x7a, 0x02, 0x40, 0x2b, 0xd4, 0x4e, 0xa2, 0x1b, 0xca, 0xa9, 0xa6, 0x8b,
+ 0x71, 0x95, 0x57, 0xfd, 0x5f, 0x36, 0x44, 0xbd, 0x12, 0x99, 0x27, 0x7c,
+ 0x46, 0xee, 0xab, 0x3c, 0xb4, 0xae, 0xc8, 0x58, 0xc2, 0x6d, 0x1d, 0x6c,
+ 0xb9, 0x07, 0xa5, 0xcf, 0xbd, 0x53, 0x70, 0x79, 0x5a, 0xb1, 0x91, 0x5a,
+ 0x31, 0x0a, 0x21, 0x2f, 0x97, 0x88, 0xde, 0x22, 0xf3, 0x49, 0x68, 0xab,
+ 0xeb, 0xb2, 0xfc, 0x13, 0xff, 0x88, 0xa7, 0xcd, 0x0d, 0x52, 0x89, 0xb8,
+ 0x0d, 0xde, 0xe8, 0xfa, 0xbc, 0x38, 0xcf, 0x8f, 0x11, 0x5f, 0xb4, 0xa0,
+ 0xe9, 0x80, 0x5f, 0x0b, 0x1b, 0x04, 0xc9, 0xd6, 0x26, 0x21, 0xdd, 0xc6,
+ 0x13, 0xda, 0x8a, 0x0a, 0xa8, 0x39, 0x1e, 0xcd, 0x70, 0xf2, 0x2a, 0x05,
+ 0x0c, 0xda, 0x34, 0xc3, 0x8a, 0x2e, 0xe1, 0x75, 0x4c, 0x80, 0x29, 0x67,
+ 0x72, 0xe8, 0xdc, 0x4e, 0xe1, 0xed, 0x3c, 0x9f, 0xbf, 0x24, 0x16, 0x11,
+ 0x32, 0xc2, 0x7d, 0x2e, 0x6b, 0xed, 0x04, 0xbe, 0x1f, 0x79, 0x2b, 0xc7,
+ 0xfc, 0xc7, 0x62, 0x21, 0xed, 0x64, 0x84, 0xba, 0xa4, 0xae, 0xa6, 0x61,
+ 0x5e, 0xeb, 0xf8, 0x54, 0xf9, 0x85, 0xe7, 0x2e, 0xbb, 0xd6, 0x70, 0x8d,
+ 0x28, 0x60, 0x27, 0x02, 0x3a, 0xc5, 0xb3, 0xa5, 0xf0, 0x12, 0x32, 0x88,
+ 0x56, 0x8d, 0x5b, 0xa9, 0xc2, 0x0b, 0x51, 0x8b, 0x43, 0x84, 0x93, 0xc9,
+ 0xc8, 0xb9, 0x6c, 0xad, 0x00, 0x31, 0x96, 0x2e, 0x84, 0x65, 0x4c, 0x1e,
+ 0x44, 0x22, 0x62, 0xda, 0xa5, 0x92, 0x23, 0x62, 0x84, 0x99, 0x64, 0xca,
+ 0x16, 0xff, 0xb7, 0x08, 0xe6, 0xcf, 0xde, 0xf2, 0xb9, 0x9b, 0x51, 0x03,
+ 0x15, 0xc0, 0x20, 0x4e, 0x30, 0x2b, 0x09, 0x60, 0x90, 0x91, 0xb8, 0x67,
+ 0xe2, 0x3b, 0x4e, 0x9b, 0x10, 0xe9, 0x1c, 0x32, 0x34, 0x89, 0xd6, 0xb1,
+ 0x3a, 0x3d, 0x89, 0xa6, 0xd0, 0x2a, 0x5e, 0x1d, 0xea, 0x47, 0x75, 0xc7,
+ 0xc9, 0xed, 0x8f, 0x76, 0xa4, 0xe7, 0x5d, 0x96, 0xcd, 0x5e, 0x65, 0x51,
+ 0x4b, 0x47, 0xe1, 0x87, 0xcb, 0x83, 0xb2, 0xfc, 0x52, 0x2b, 0x15, 0x15,
+ 0x81, 0x95, 0x7c, 0x27, 0x33, 0xe3, 0x6f, 0xaa, 0x3c, 0xc9, 0x0d, 0xe3,
+ 0x45, 0xba, 0xfe, 0x1d, 0x9d, 0xd4, 0xf1, 0xb8, 0xd2, 0xba, 0xf8, 0x98,
+ 0xde, 0x26, 0x7e, 0xc5, 0x0b, 0x41, 0x8f, 0x00, 0x9e, 0xc3, 0xd0, 0x91,
+ 0x72, 0x10, 0x38, 0x1b, 0x67, 0x33, 0x7e, 0x7b, 0x71, 0xb0, 0x8b, 0x87,
+ 0x23, 0x60, 0x26, 0x8f, 0x58, 0x9f, 0x57, 0x81, 0xe3, 0x47, 0xe5, 0x66,
+ 0xd5, 0xa2, 0x59, 0x3e, 0x82, 0xac, 0xe1, 0xf6, 0x26, 0x1a, 0xa8, 0x32,
+ 0x46, 0xca, 0xb0, 0x0a, 0x54, 0xc6, 0xdd, 0x4b, 0xe7, 0x6c, 0x5a, 0x41,
+ 0xf1, 0x3d, 0xb0, 0x00, 0x4b, 0x45, 0x29, 0x49, 0x0f, 0x8b, 0x04, 0xcd,
+ 0x18, 0xbc, 0xb1, 0xd8, 0xe2, 0x4b, 0x95, 0x06, 0x2c, 0x13, 0xfc, 0xab,
+ 0xd1, 0x60, 0x71, 0x1a, 0x91, 0x46, 0xdb, 0x8d, 0x8c, 0x71, 0x4b, 0x27,
+ 0xef, 0x51, 0xbc, 0xd7, 0x12, 0x5c, 0xfe, 0xce, 0xde, 0x4f, 0x9d, 0x86,
+ 0x4b, 0xfa, 0x46, 0x2f, 0x71, 0xbe, 0x97, 0x29, 0x6d, 0x64, 0xc0, 0x3f,
+ 0x1b, 0x47, 0x20, 0xb1, 0x19, 0xc2, 0x62, 0x30, 0xa6, 0x98, 0xfd, 0xee,
+ 0xad, 0xe8, 0xb9, 0x75, 0x8b, 0xd0, 0x8e, 0xa8, 0x82, 0x24, 0xe0, 0x0f,
+ 0x27, 0x04, 0x50, 0xc8, 0xe7, 0x3a, 0x0d, 0x57, 0x18, 0x67, 0x81, 0x12,
+ 0xa5, 0xf9, 0xa0, 0x15, 0x4d, 0x0d, 0x30, 0xa7, 0x8d, 0x35, 0x75, 0x95,
+ 0x75, 0x69, 0x90, 0x57, 0xde, 0x42, 0x82, 0x5d, 0x53, 0xf4, 0xc7, 0x6f,
+ 0x9b, 0x5e, 0x4a, 0x5f, 0x6c, 0x76, 0x2a, 0x06, 0xea, 0xeb, 0xa5, 0xa3,
+ 0x1d, 0xea, 0xe8, 0xb3, 0xf6, 0xb6, 0x01, 0x60, 0x62, 0x5c, 0x4e, 0x73,
+ 0x0a, 0xea, 0x25, 0x05, 0x38, 0x91, 0x0d, 0xe8, 0x47, 0x15, 0x3f, 0x54,
+ 0x3a, 0xa4, 0x52, 0x07, 0x5e, 0xb9, 0x1c, 0x55, 0x46, 0x4f, 0x5f, 0xde,
+ 0xe0, 0x0e, 0x62, 0x59, 0x36, 0xb6, 0x3d, 0xb7, 0xa5, 0x03, 0xc7, 0x29,
+ 0x14, 0x60, 0x97, 0x4d, 0x15, 0x9f, 0x5e, 0x32, 0xb1, 0x74, 0x99, 0x5f,
+ 0x5e, 0x14, 0x73, 0x06, 0x6a, 0x64, 0xe2, 0xe9, 0xf7, 0xda, 0x08, 0x88,
+ 0xbd, 0xb8, 0x4e, 0xa0, 0xa2, 0x94, 0x50, 0x41, 0x8f, 0x19, 0x8a, 0x95,
+ 0x86, 0x65, 0xcc, 0x64, 0x51, 0x74, 0x10, 0x1f, 0x1a, 0x81, 0x89, 0x4d,
+ 0x19, 0x2a, 0x84, 0x1b, 0x76, 0xc1, 0x7e, 0x7f, 0xde, 0x3f, 0x1c, 0x88,
+ 0x7e, 0x97, 0xa1, 0x82, 0x93, 0xea, 0x75, 0xa1, 0xd8, 0xf3, 0x73, 0x46,
+ 0x82, 0x3d, 0xe1, 0x60, 0xbd, 0xe7, 0x52, 0x26, 0xf8, 0xf3, 0xfa, 0x10,
+ 0x07, 0x06, 0x48, 0xc2, 0xa3, 0x47, 0x11, 0xc8, 0x92, 0xb9, 0x95, 0x10,
+ 0x01, 0xc1, 0x0b, 0xbd, 0x7b, 0xd1, 0x46, 0xdf, 0x0d, 0xb1, 0x61, 0x53,
+ 0x34, 0xe1, 0x34, 0x4a, 0x8a, 0xdf, 0x70, 0xce, 0x5d, 0xd0, 0x33, 0xb5,
+ 0x1e, 0x5e, 0xe6, 0x42, 0x30, 0x24, 0x32, 0xb7, 0xd6, 0xe4, 0xd4, 0x62,
+ 0x88, 0xd4, 0x7e, 0xb4, 0xd7, 0x87, 0x8c, 0xa5, 0xed, 0x43, 0xf2, 0x76,
+ 0x1c, 0x7b, 0x1d, 0xb4, 0x74, 0x4d, 0xc8, 0xa0, 0xb1, 0xc8, 0x9d, 0x67,
+ 0x59, 0x53, 0x71, 0x82, 0x17, 0x84, 0x6a, 0x7b, 0x10, 0xad, 0xc2, 0xb1,
+ 0x06, 0xff, 0xb1, 0xf0, 0xc0, 0xad, 0xa3, 0x12, 0x77, 0x30, 0xac, 0xe8,
+ 0x13, 0x1a, 0x8b, 0x2b, 0xcd, 0x01, 0xeb, 0x79, 0x0b, 0x30, 0x93, 0x24,
+ 0xe7, 0xf4, 0x44, 0xc4, 0x49, 0xf1, 0xc1, 0x3c, 0x22, 0x4e, 0x26, 0x20,
+ 0x37, 0x73, 0xe5, 0x37, 0x72, 0x33, 0xfb, 0x0d, 0x34, 0x5a, 0x3e, 0x35,
+ 0x58, 0xad, 0xdf, 0x8e, 0xa7, 0xba, 0xcb, 0x87, 0xb6, 0x2a, 0x27, 0xba,
+ 0xb7, 0x79, 0x38, 0xcb, 0x23, 0xa2, 0xb0, 0xbd, 0x26, 0xc4, 0xfe, 0xcd,
+ 0x7d, 0x12, 0x03, 0xfc, 0xd9, 0x9d, 0x52, 0x2f, 0x10, 0xd7, 0x7e, 0xe4,
+ 0x27, 0xa6, 0xa8, 0xba, 0xe3, 0x2d, 0xa7, 0xd6, 0x94, 0x3a, 0x28, 0x84,
+ 0x33, 0x13, 0xd7, 0x93, 0xe6, 0xd4, 0x03, 0x8a, 0x5d, 0xea, 0xd1, 0x32,
+ 0x30, 0x3b, 0x35, 0xe5, 0xaf, 0xcf, 0x18, 0xa0, 0x06, 0x0a, 0xc3, 0x53,
+ 0x40, 0x3b, 0x87, 0x56, 0x41, 0xcb, 0xf2, 0xa0, 0x72, 0x6d, 0xa4, 0xb5,
+ 0xab, 0x1d, 0xfc, 0xc1, 0xf3, 0x33, 0x06, 0x68, 0x63, 0x49, 0x03, 0x5e,
+ 0xb7, 0x93, 0x0c, 0x99, 0x07, 0xd9, 0x3a, 0x35, 0x5a, 0x54, 0x3b, 0x0b,
+ 0xf3, 0xaf, 0x17, 0xe5, 0x34, 0x3d, 0xe5, 0x0e, 0x25, 0xf0, 0x82, 0xf9,
+ 0xc7, 0x4e, 0xaf, 0x56, 0x63, 0x1b, 0xa8, 0x30, 0x55, 0xa4, 0x6b, 0x91,
+ 0x67, 0x49, 0xe2, 0x81, 0x1b, 0x93, 0x11, 0x6a, 0x9b, 0x38, 0xbe, 0x94,
+ 0xa4, 0x1c, 0x7d, 0x2e, 0x4e, 0x14, 0x0f, 0xd2, 0x30, 0xad, 0x39, 0xf1,
+ 0x68, 0x86, 0x55, 0x89, 0x5d, 0xac, 0xde, 0x4e, 0x12, 0xf2, 0xb1, 0x4c,
+ 0xf9, 0x0b, 0x7a, 0x96, 0xe6, 0x6e, 0x2e, 0x82, 0xbb, 0x58, 0xf5, 0xeb,
+ 0x81, 0x72, 0x23, 0x2e, 0x01, 0xef, 0x8d, 0xe2, 0x95, 0xe7, 0x8e, 0xef,
+ 0x82, 0x8a, 0xde, 0x76, 0xea, 0x5b, 0x16, 0xcb, 0xf5, 0x33, 0x70, 0xad,
+ 0xa4, 0x6f, 0x20, 0x92, 0x84, 0x15, 0x0a, 0x20, 0x47, 0x0a, 0x8e, 0x85,
+ 0x4e, 0xf2, 0xe9, 0xea, 0xa2, 0x7c, 0x4b, 0x95, 0xe6, 0x25, 0xef, 0xed,
+ 0x93, 0x7c, 0xca, 0x85, 0xe3, 0x14, 0x22, 0x67, 0x67, 0x62, 0x0f, 0x78,
+ 0x4d, 0x79, 0x05, 0xd0, 0x88, 0xfd, 0x8a, 0xf2, 0x54, 0xbb, 0x40, 0x87,
+ 0xcc, 0x70, 0x46, 0x96, 0x17, 0x04, 0xd5, 0x83, 0x87, 0x4a, 0x5a, 0x0b,
+ 0xc1, 0xd3, 0x34, 0x25, 0x99, 0xcd, 0x8b, 0x6a, 0xde, 0xac, 0x39, 0xe9,
+ 0x08, 0x05, 0xea, 0xb0, 0x21, 0x6b, 0xf5, 0x92, 0x2d, 0xd6, 0xcf, 0x1f,
+ 0x9a, 0x1c, 0x73, 0xb3, 0x04, 0x37, 0xd7, 0x5b, 0x5e, 0xfe, 0xa2, 0x9d,
+ 0x7e, 0x90, 0xf2, 0x19, 0x61, 0x17, 0xef, 0xa1, 0x64, 0x0d, 0x3a, 0x0c,
+ 0xc5, 0x83, 0xf9, 0xca, 0xe2, 0xd7, 0x8e, 0x40, 0xdd, 0x8c, 0xb5, 0xf6,
+ 0x20, 0x00, 0x16, 0x5e, 0x04, 0x10, 0x85, 0x69, 0x79, 0x8f, 0x62, 0x71,
+ 0x32, 0xf2, 0x5e, 0xfd, 0x53, 0x39, 0x1e, 0x5c, 0x60, 0xe7, 0xd9, 0x87,
+ 0xfc, 0x99, 0x86, 0xd1, 0x20, 0x8e, 0x8b, 0x5d, 0xdf, 0xc3, 0xbe, 0x99,
+ 0x25, 0x15, 0xea, 0x6e, 0x12, 0x74, 0x8f, 0x8d, 0x8c, 0x8a, 0xa5, 0xf8,
+ 0x29, 0x00, 0x4b, 0x7e, 0x93, 0x1d, 0x64, 0x91, 0xde, 0xf7, 0x18, 0x49,
+ 0xa5, 0x46, 0x54, 0x27, 0x95, 0x7a, 0xed, 0x24, 0x84, 0x5b, 0x2c, 0x9a,
+ 0x39, 0x40, 0x46, 0x04, 0x56, 0x57, 0x77, 0x1c, 0x61, 0x6d, 0x86, 0xef,
+ 0xda, 0x23, 0x3b, 0xfb, 0x28, 0x5a, 0xdf, 0xae, 0x31, 0x6b, 0x09, 0x3d,
+ 0x43, 0xb2, 0x11, 0x91, 0xc8, 0x5c, 0x9f, 0x97, 0x71, 0xff, 0xf4, 0x40,
+ 0x14, 0x7d, 0xb4, 0x7b, 0x53, 0x50, 0x97, 0xac, 0x15, 0x86, 0x32, 0x42,
+ 0xc5, 0xac, 0xc1, 0x4d, 0x78, 0x4b, 0x9d, 0xe3, 0xb6, 0xd4, 0x22, 0x8f,
+ 0x62, 0x65, 0x11, 0xce, 0xe8, 0x05, 0x3f, 0x53, 0x73, 0x90, 0xa1, 0x26,
+ 0x23, 0xe6, 0x7a, 0xfe, 0x4e, 0x17, 0x05, 0xce, 0x54, 0x2e, 0xbd, 0xe1,
+ 0x75, 0x7d, 0x69, 0xcd, 0xa4, 0xd0, 0xfd, 0x55, 0xf6, 0x43, 0x53, 0xd6,
+ 0xed, 0x40, 0x68, 0xc6, 0x10, 0xa7, 0xef, 0x96, 0x73, 0xca, 0x8a, 0xc7,
+ 0x7b, 0x37, 0x30, 0x3b, 0xad, 0xef, 0xb8, 0x5e, 0x5d, 0x13, 0x78, 0xfc,
+ 0x9c, 0xe5, 0x20, 0xa2, 0x47, 0x13, 0x01, 0xc1, 0xba, 0x3a, 0xab, 0x2d,
+ 0xaa, 0xe1, 0xfc, 0x87, 0xec, 0xf4, 0x87, 0x99, 0xe0, 0x14, 0xb1, 0x3e,
+ 0xf8, 0xb2, 0xe4, 0x90, 0xcf, 0xc0, 0x0c, 0x0d, 0xde, 0x6f, 0xbb, 0x4e,
+ 0x01, 0x66, 0x8e, 0xb3, 0xaa, 0x32, 0xcd, 0xa3, 0x1a, 0x3c, 0x46, 0x65,
+ 0x07, 0x98, 0x32, 0x74, 0x6b, 0x60, 0xd5, 0x6f, 0xbb, 0xe1, 0x55, 0xb2,
+ 0x18, 0xbd, 0xa0, 0x5c, 0xdc, 0x70, 0xa1, 0xf0, 0x03, 0x08, 0x62, 0xed,
+ 0x9c, 0xbd, 0xbf, 0x8b, 0x8c, 0x8e, 0x24, 0x3d, 0xaf, 0x07, 0x7b, 0x42,
+ 0xa1, 0x0b, 0xe2, 0xb5, 0x4d, 0x09, 0xef, 0x4c, 0x0a, 0xfc, 0x35, 0x5d,
+ 0x17, 0xb8, 0x4f, 0x71, 0x11, 0xc7, 0x4b, 0x80, 0x06, 0x29, 0x97, 0x12,
+ 0x6d, 0x1c, 0x8a, 0x53, 0x42, 0x73, 0xe0, 0x00, 0x18, 0x18, 0x1c, 0x5c,
+ 0x32, 0xa9, 0x61, 0x3e, 0x55, 0x4f, 0xee, 0x7c, 0xe3, 0x82, 0x8c, 0xab,
+ 0x04, 0x41, 0x9c, 0xfb, 0x8c, 0x85, 0xad, 0x93, 0x46, 0x94, 0x01, 0x2a,
+ 0x0c, 0x2f, 0xd2, 0x92, 0x3b, 0xcf, 0x99, 0xc9, 0x02, 0x75, 0x8d, 0x5e,
+ 0x2b, 0x7f, 0x4b, 0xa8, 0x80, 0x60, 0x8b, 0xe8, 0x96, 0x4c, 0xd8, 0x80,
+ 0xfc, 0x32, 0xe6, 0x2e, 0x7a, 0x39, 0xe3, 0xfc, 0xd9, 0x53, 0x83, 0x3a,
+ 0x6a, 0x08, 0x49, 0x7b, 0x7b, 0xcb, 0xc9, 0x2d, 0x5c, 0x91, 0x6a, 0x6f,
+ 0x6a, 0x8d, 0x54, 0x22, 0x36, 0xe5, 0x91, 0x48, 0x03, 0x36, 0x5d, 0xa1,
+ 0x03, 0x59, 0xd1, 0xa8, 0xe0, 0x5c, 0xce, 0xaf, 0x58, 0x16, 0x7f, 0xee,
+ 0xc4, 0x43, 0xb3, 0x66, 0xb9, 0x10, 0xb3, 0x49, 0x9b, 0x5a, 0x33, 0x51,
+ 0xa4, 0x20, 0xec, 0xb9, 0x6c, 0x2e, 0x4f, 0xfd, 0x49, 0xae, 0x72, 0xa4,
+ 0x81, 0x6b, 0x86, 0xd0, 0xd1, 0x44, 0x61, 0xab, 0x8d, 0x3e, 0x6b, 0xc5,
+ 0x88, 0x38, 0x48, 0x24, 0xee, 0x46, 0x4a, 0xa1, 0xca, 0x7d, 0x21, 0x80,
+ 0x07, 0x1d, 0xa5, 0xa0, 0x96, 0xc1, 0xb5, 0x20, 0xdb, 0x9f, 0xdc, 0x8e,
+ 0xae, 0x3c, 0x3f, 0xea, 0xd0, 0xb1, 0xd1, 0xc2, 0x74, 0x94, 0x84, 0x37,
+ 0x3d, 0x88, 0x20, 0xc4, 0xc7, 0x8c, 0xdb, 0x61, 0xda, 0x0c, 0x72, 0x06,
+ 0x4d, 0xdb, 0x72, 0x0e, 0x9e, 0xd8, 0xbf, 0x4b, 0x35, 0xd2, 0xf6, 0xb7,
+ 0x32, 0x7e, 0xdb, 0x97, 0x43, 0x3e, 0xf7, 0x48, 0xeb, 0xba, 0x3d, 0x55,
+ 0x3d, 0x7b, 0xa4, 0x3b, 0x7e, 0x24, 0x21, 0xba, 0x12, 0x47, 0xa2, 0x8a,
+ 0x41, 0x63, 0x72, 0x67, 0x97, 0x62, 0xaf, 0x03, 0x6f, 0x3f, 0xb1, 0x72,
+ 0x04, 0x40, 0x06, 0x68, 0x54, 0x92, 0x24, 0x6c, 0x48, 0x0e, 0x2c, 0x78,
+ 0x04, 0x2e, 0x82, 0x51, 0xaf, 0xb3, 0xc2, 0x71, 0xb3, 0x29, 0x21, 0x43,
+ 0x79, 0x0f, 0x1c, 0x08, 0x10, 0x12, 0x2b, 0x62, 0x56, 0xfe, 0x18, 0xb8,
+ 0x36, 0xc7, 0xdd, 0xf1, 0x29, 0xde, 0x05, 0x94, 0xb3, 0x8b, 0x77, 0x98,
+ 0x3d, 0x74, 0x39, 0xc5, 0xa8, 0x1c, 0xea, 0x88, 0x5d, 0x42, 0xe4, 0xa8,
+ 0x8d, 0xf8, 0x32, 0x42, 0x93, 0x8a, 0x4b, 0xa0, 0x9e, 0x27, 0xe1, 0x36,
+ 0x29, 0xec, 0xeb, 0xe0, 0x7b, 0x2a, 0xfc, 0x82, 0x63, 0xe4, 0xba, 0x41,
+ 0x68, 0x34, 0x14, 0x5c, 0xbf, 0x3a, 0x4d, 0xc4, 0xd5, 0xac, 0x45, 0xb7,
+ 0x83, 0x1e, 0x47, 0x27, 0xb8, 0xfe, 0x9e, 0x62, 0x61, 0xf5, 0x52, 0x57,
+ 0x39, 0x4f, 0xfb, 0x96, 0x5c, 0x3e, 0xa1, 0x28, 0xb2, 0x9d, 0x0f, 0x0e,
+ 0x99, 0xac, 0xac, 0xae, 0x57, 0x31, 0x51, 0x25, 0x19, 0x43, 0x50, 0x4f,
+ 0x3c, 0xb2, 0x48, 0xec, 0xff, 0x26, 0xe1, 0xc4, 0xff, 0x59, 0x63, 0x9a,
+ 0xd3, 0x0b, 0xb5, 0x70, 0xf4, 0xaa, 0x4f, 0x85, 0xbb, 0x22, 0x13, 0x57,
+ 0x72, 0xe8, 0x00, 0x61, 0xd7, 0xab, 0x07, 0x81, 0x18, 0xe2, 0x66, 0xb3,
+ 0x5f, 0xe5, 0x89, 0x46, 0x64, 0x47, 0x52, 0xae, 0xc9, 0xd6, 0xd0, 0x15,
+ 0x60, 0xc7, 0x72, 0xb6, 0x87, 0xe9, 0xcd, 0x46, 0x03, 0x60, 0x56, 0xe2,
+ 0x0a, 0x64, 0x1e, 0x2b, 0x50, 0xea, 0xb3, 0x8a, 0x15, 0x98, 0x9b, 0xab,
+ 0x44, 0x99, 0x6b, 0x2a, 0x49, 0xbb, 0xfd, 0x29, 0x94, 0xef, 0xf5, 0x4e,
+ 0x99, 0x9a, 0xf0, 0x7d, 0xcc, 0x1c, 0xe9, 0xbb, 0x33, 0x44, 0x43, 0xd5,
+ 0x63, 0x36, 0x74, 0x9c, 0x2c, 0x1a, 0x52, 0x35, 0x07, 0xd8, 0x7e, 0x73,
+ 0x50, 0x80, 0x9d, 0x28, 0x74, 0x97, 0x10, 0xc6, 0x60, 0x34, 0x9c, 0xe0,
+ 0x82, 0x39, 0xd4, 0x86, 0x24, 0xc4, 0x5f, 0xf6, 0xcb, 0xd5, 0x32, 0x3a,
+ 0x20, 0xac, 0xc1, 0xa6, 0x2a, 0xf5, 0xc9, 0x93, 0x45, 0xc5, 0xc9, 0x09,
+ 0xc0, 0x67, 0x20, 0xa5, 0x8d, 0x50, 0xad, 0xaf, 0xf5, 0xf1, 0x5b, 0xdb,
+ 0x14, 0xee, 0xea, 0x70, 0x0c, 0x86, 0x77, 0xa3, 0x4d, 0x89, 0xd5, 0x1a,
+ 0xd7, 0x22, 0xde, 0xb5, 0x0b, 0x66, 0xc7, 0x96, 0x9c, 0xb1, 0xbb, 0x37,
+ 0x47, 0xda, 0x81, 0x0b, 0x01, 0x7f, 0x10, 0x93, 0xfb, 0xd6, 0x93, 0x69,
+ 0x07, 0xd1, 0x26, 0x81, 0xe2, 0xff, 0x45, 0x35, 0x37, 0x23, 0x7e, 0x4b,
+ 0xc8, 0x0a, 0x02, 0x59, 0x33, 0x2e, 0x51, 0xa5, 0x37, 0x31, 0xb8, 0x71,
+ 0x00, 0xcf, 0x07, 0xe1, 0x6d, 0x36, 0x22, 0x32, 0x6e, 0x48, 0x47, 0xb0,
+ 0x3b, 0x71, 0x03, 0x83, 0xb2, 0x61, 0xc8, 0xac, 0x5f, 0x6e, 0x08, 0x31,
+ 0x10, 0x4e, 0xdf, 0x77, 0xd8, 0x49, 0x68, 0x9b, 0x4d, 0xd4, 0x01, 0xc1,
+ 0xf1, 0x28, 0x43, 0xf5, 0x4f, 0x89, 0xdb, 0x3b, 0xfb, 0x1d, 0xee, 0x86,
+ 0x0b, 0x02, 0x57, 0xd0, 0xf2, 0xf9, 0x8a, 0xd5, 0xf0, 0xf4, 0x9c, 0x25,
+ 0xfd, 0xf1, 0x07, 0x30, 0x16, 0xb6, 0x22, 0xd1, 0x84, 0x5e, 0xda, 0x60,
+ 0xe5, 0xd5, 0x60, 0xe0, 0xad, 0x67, 0xbe, 0xd7, 0x67, 0x6e, 0xe9, 0xd3,
+ 0x0e, 0x01, 0xd8, 0xda, 0x04, 0x00, 0xac, 0xee, 0xc5, 0xe3, 0x33, 0x4b,
+ 0xda, 0x49, 0x1a, 0x41, 0x2d, 0x36, 0xa4, 0x2e, 0x3c, 0x47, 0x1a, 0xe0,
+ 0x18, 0xd9, 0x4f, 0xf3, 0xec, 0xb0, 0x42, 0x5a, 0x81, 0xa2, 0x9e, 0x54,
+ 0x16, 0x8c, 0x93, 0x6c, 0xba, 0x23, 0x83, 0x04, 0xe5, 0xc7, 0xfb, 0x38,
+ 0xd0, 0xfb, 0x64, 0x69, 0xe3, 0x1d, 0x50, 0x09, 0x94, 0xef, 0x6c, 0x97,
+ 0xbd, 0xda, 0xfd, 0xdd, 0xd9, 0x84, 0x38, 0x1f, 0xd8, 0x8d, 0x40, 0xe8,
+ 0xa0, 0x4f, 0x4e, 0x55, 0xf2, 0x7a, 0x49, 0xf9, 0x87, 0xaa, 0x6e, 0xf6,
+ 0xc7, 0xd8, 0x66, 0x3c, 0x3b, 0xfc, 0x68, 0xd9, 0x44, 0x2c, 0x19, 0xc0,
+ 0x2a, 0x88, 0x6a, 0x62, 0x7c, 0x6e, 0xbe, 0x88, 0x18, 0x43, 0x9b, 0xdf,
+ 0xf4, 0x88, 0x37, 0xda, 0xfd, 0x5a, 0x3b, 0x2e, 0xae, 0x2f, 0xf9, 0x48,
+ 0x0a, 0x0b, 0x24, 0x1e, 0x3b, 0x21, 0x9c, 0x1b, 0xd8, 0xc4, 0x85, 0xcd,
+ 0x12, 0xa4, 0x87, 0x4f, 0xed, 0xca, 0xbc, 0x4e, 0x9f, 0xeb, 0xd9, 0x8f,
+ 0xda, 0xfd, 0xf4, 0xc3, 0x6d, 0x13, 0xa6, 0x73, 0x47, 0x50, 0x99, 0x59,
+ 0x81, 0xa8, 0xd6, 0xfb, 0x46, 0xca, 0x1a, 0xde, 0x32, 0x2b, 0xa1, 0x04,
+ 0x29, 0x95, 0x7f, 0xb6, 0x2e, 0x30, 0x2f, 0x8e, 0xc8, 0xe4, 0x44, 0x2f,
+ 0xe9, 0xfd, 0x5c, 0x05, 0x22, 0x8d, 0xfc, 0x57, 0x39, 0xd1, 0x8d, 0x62,
+ 0xc1, 0xee, 0x30, 0x4b, 0xa4, 0xbe, 0x9d, 0x01, 0xd2, 0x87, 0x98, 0xb8,
+ 0x05, 0xe4, 0xf8, 0x1f, 0x61, 0x0d, 0x79, 0x1e, 0x74, 0xb9, 0x24, 0x5c,
+ 0xe0, 0x32, 0xfc, 0x25, 0xf9, 0xef, 0x9f, 0xaf, 0x7e, 0xfd, 0x3a, 0x03,
+ 0x30, 0x09, 0x81, 0x61, 0x44, 0xa3, 0xf4, 0xd6, 0x1a, 0x85, 0x92, 0x6f,
+ 0xf5, 0x9c, 0x76, 0x0d, 0xb3, 0xb3, 0x32, 0x36, 0x2c, 0xeb, 0xa5, 0x08,
+ 0xc0, 0x2b, 0x32, 0xd6, 0x8d, 0x0a, 0x03, 0xbb, 0x06, 0x97, 0x4f, 0xf3,
+ 0xe3, 0x2a, 0x81, 0xd6, 0x3b, 0x09, 0x4a, 0xfc, 0x14, 0x5a, 0xa5, 0x35,
+ 0x08, 0xec, 0xb6, 0x89, 0x25, 0x5a, 0x63, 0x52, 0x00, 0x6b, 0xa0, 0x5c,
+ 0x04, 0x4a, 0x2c, 0xbd, 0x87, 0x28, 0xd8, 0xe4, 0x58, 0xbd, 0xd6, 0x34,
+ 0xe0, 0x55, 0x73, 0x47, 0x41, 0xf1, 0x30, 0xc2, 0xd8, 0x6b, 0x68, 0x2c,
+ 0xb6, 0xf7, 0x4a, 0x6e, 0x81, 0x32, 0xbd, 0x43, 0x74, 0xf4, 0x5a, 0xd0,
+ 0x9e, 0xe1, 0x20, 0xb0, 0x7f, 0x2f, 0xe0, 0x9b, 0xd9, 0xc6, 0x21, 0x72,
+ 0xa4, 0xd6, 0x7e, 0x8d, 0x8e, 0xc2, 0x68, 0xe2, 0x83, 0x7b, 0x86, 0x34,
+ 0x4d, 0xec, 0xe6, 0x01, 0xf3, 0x6b, 0xb3, 0x6b, 0x74, 0x07, 0x4c, 0x52,
+ 0x95, 0x6b, 0x19, 0xe0, 0xbd, 0x2e, 0x53, 0x56, 0x6c, 0x50, 0x07, 0x14,
+ 0xb8, 0xf5, 0xad, 0x98, 0xbb, 0x93, 0x45, 0xd1, 0xc0, 0x7e, 0x09, 0x1b,
+ 0xe1, 0xcb, 0x90, 0xd4, 0xe2, 0x79, 0x19, 0xba, 0xea, 0x7c, 0xda, 0xda,
+ 0x4f, 0x6e, 0x3c, 0xd1, 0xb0, 0x27, 0x2a, 0xb0, 0x5d, 0xed, 0xb0, 0xb0,
+ 0xff, 0x72, 0x65, 0x1f, 0xbc, 0xb6, 0x38, 0x6b, 0xe8, 0xcf, 0x66, 0xd4,
+ 0x35, 0x5e, 0xfd, 0x9c, 0x01, 0x4e, 0xf9, 0x92, 0x1e, 0x09, 0x7b, 0x0f,
+ 0x98, 0x11, 0x27, 0xa2, 0x70, 0xb7, 0xf5, 0x93, 0xdd, 0xbe, 0x69, 0xb8,
+ 0x9d, 0x1d, 0x8b, 0x70, 0x6c, 0x37, 0x4b, 0xfb, 0x78, 0xb2, 0x66, 0x7e,
+ 0xc5, 0xdb, 0xe2, 0xa1, 0xd6, 0x54, 0xc6, 0x3f, 0xbf, 0xf7, 0xd6, 0xae,
+ 0xd0, 0x46, 0xcb, 0x8d, 0x73, 0xbb, 0x01, 0x79, 0xc9, 0x0d, 0xdf, 0x81,
+ 0x63, 0xd4, 0xaf, 0xc4, 0x9e, 0x51, 0xdc, 0x6f, 0x72, 0xde, 0xf8, 0xc7,
+ 0x35, 0xd3, 0xc4, 0xc0, 0xdc, 0x40, 0x80, 0x27, 0x62, 0x36, 0x55, 0xaf,
+ 0xd1, 0x44, 0xc1, 0xe4, 0xb7, 0x3c, 0x00, 0xf2, 0x8d, 0x1b, 0x88, 0xdd,
+ 0x76, 0x9d, 0xc0, 0x3b, 0x29, 0x27, 0xba, 0xd3, 0x7b, 0x9c, 0xb1, 0x92,
+ 0xaa, 0xaa, 0xaf, 0x49, 0x4e, 0x36, 0x14, 0xe3, 0x95, 0x86, 0xee, 0x5a,
+ 0x75, 0xcc, 0x12, 0xde, 0xc8, 0xca, 0x02, 0x04, 0xdc, 0x6b, 0x7d, 0x0d,
+ 0x4b, 0xd2, 0x68, 0x75, 0xeb, 0x32, 0x5f, 0x3c, 0xa3, 0x40, 0x9c, 0x88,
+ 0xda, 0x61, 0xc9, 0xa3, 0xd3, 0xe0, 0xc4, 0x82, 0x13, 0x9c, 0x5c, 0x9f,
+ 0x53, 0xeb, 0xb3, 0x0a, 0x80, 0xb4, 0xe2, 0x6e, 0x3c, 0xbc, 0xac, 0x7e,
+ 0xd7, 0xfe, 0x9a, 0x68, 0xcb, 0x32, 0x05, 0x14, 0x20, 0x73, 0xe7, 0x05,
+ 0xb1, 0x43, 0xd9, 0x71, 0x14, 0xfb, 0x7b, 0x02, 0x01, 0x41, 0xe0, 0xe2,
+ 0xe4, 0x78, 0xba, 0xb6, 0x13, 0xad, 0xdc, 0x0e, 0xd5, 0x38, 0x67, 0xd8,
+ 0x56, 0xc4, 0xbd, 0x47, 0x14, 0x49, 0x24, 0x65, 0x4e, 0x83, 0x87, 0x20,
+ 0x90, 0xe7, 0x3c, 0x39, 0x83, 0x35, 0x88, 0x3f, 0x9c, 0xbc, 0xfa, 0xba,
+ 0x9f, 0xdf, 0xfa, 0x4d, 0xc1, 0x18, 0x64, 0xd7, 0xa0, 0x87, 0xa8, 0xac,
+ 0x58, 0xef, 0x1e, 0xae, 0xb8, 0x58, 0x8b, 0xe3, 0x3c, 0x8e, 0xdf, 0x9a,
+ 0x12, 0x37, 0x8e, 0x62, 0x79, 0xe5, 0xbe, 0xe0, 0x7a, 0x16, 0xa3, 0xdf,
+ 0x99, 0x98, 0x14, 0x56, 0x6e, 0xa8, 0x0a, 0x05, 0x0f, 0xf0, 0x70, 0x9f,
+ 0xcd, 0xfd, 0xb4, 0x23, 0xb3, 0x0b, 0xe1, 0x1d, 0xe6, 0xf8, 0xa7, 0x46,
+ 0x24, 0x7f, 0xef, 0xc4, 0x15, 0xe0, 0x7f, 0xfd, 0xa3, 0x7b, 0x5c, 0x1d,
+ 0x62, 0x0f, 0x5d, 0x91, 0x53, 0x27, 0xb3, 0xde, 0xa0, 0xee, 0xd2, 0xf9,
+ 0x19, 0x16, 0xca, 0x3e, 0x0d, 0x94, 0xa8, 0x78, 0x60, 0xc5, 0xc2, 0x2d,
+ 0xba, 0xbd, 0x33, 0x79, 0x7c, 0x73, 0x08, 0x5a, 0x95, 0x1d, 0xe6, 0x5e,
+ 0x1c, 0x24, 0xaa, 0x34, 0xf0, 0x70, 0xcf, 0xc2, 0xbe, 0x35, 0x43, 0x51,
+ 0x35, 0x05, 0x2f, 0x5e, 0xca, 0x04, 0x1e, 0x08, 0x6e, 0x13, 0xa5, 0x64,
+ 0xbd, 0xe8, 0xb1, 0x63, 0x38, 0xca, 0x4d, 0xc7, 0xf2, 0xde, 0x31, 0xc7,
+ 0xb0, 0x74, 0x0b, 0xef, 0x96, 0x32, 0xb0, 0xfc, 0xf3, 0x7f, 0xe4, 0xd0,
+ 0xf6, 0x78, 0xa0, 0x36, 0x1e, 0x6b, 0x84, 0xa6, 0x30, 0x0d, 0x74, 0xb9,
+ 0x96, 0xec, 0xcc, 0xdd, 0x07, 0x95, 0x8b, 0xa6, 0xf5, 0x00, 0x6c, 0x84,
+ 0x43, 0xfd, 0xe4, 0x3c, 0xbe, 0x34, 0x4d, 0x45, 0xf9, 0x05, 0x95, 0x58,
+ 0x6d, 0x36, 0xa5, 0x14, 0x00, 0x18, 0x6f, 0x31, 0x27, 0x2e, 0x97, 0x16,
+ 0x7b, 0xb8, 0xd8, 0x8d, 0x14, 0x93, 0xd6, 0xa9, 0xb3, 0x8d, 0x9d, 0x35,
+ 0x54, 0xbd, 0xac, 0x82, 0x4b, 0x8f, 0x72, 0x55, 0xbe, 0x9f, 0x14, 0x6c,
+ 0x87, 0x2f, 0x51, 0xe4, 0x3c, 0x02, 0x7e, 0x4f, 0xf6, 0x73, 0x2e, 0x19,
+ 0xb9, 0x62, 0x0a, 0x6c, 0x69, 0x35, 0x6d, 0x4b, 0x06, 0x44, 0x5d, 0x90,
+ 0xd7, 0x12, 0x5c, 0x9b, 0xba, 0x00, 0x53, 0x38, 0x09, 0xd8, 0x52, 0xdb,
+ 0x2d, 0xdd, 0x30, 0xda, 0x09, 0xa9, 0x14, 0xfb, 0x2f, 0xb3, 0x58, 0xdf,
+ 0xcd, 0xe8, 0x9b, 0x69, 0xa4, 0x98, 0x14, 0x3e, 0xc5, 0x34, 0xef, 0xca,
+ 0xee, 0xf9, 0xed, 0x34, 0x60, 0x8d, 0xd4, 0x0a, 0x0e, 0xa7, 0xf8, 0xbb,
+ 0x94, 0x4d, 0x47, 0x92, 0x2e, 0x09, 0xf4, 0xdf, 0x60, 0xf8, 0x35, 0x49,
+ 0xff, 0xe8, 0xfd, 0x1b, 0xf3, 0xcf, 0x2b, 0x11, 0xf6, 0x4e, 0x3a, 0x24,
+ 0x38, 0xe1, 0x82, 0xc7, 0xe5, 0x35, 0xd6, 0xe0, 0x71, 0x70, 0x53, 0x71,
+ 0xc9, 0x0d, 0x1f, 0xfe, 0x04, 0xeb, 0x0c, 0xa4, 0x04, 0xf7, 0x70, 0x1f,
+ 0x49, 0x01, 0x89, 0x55, 0x09, 0x01, 0x54, 0x56, 0xb4, 0xf5, 0xd5, 0x48,
+ 0xff, 0x8d, 0xcf, 0xea, 0xcb, 0x1c, 0xb0, 0x51, 0xf7, 0xb5, 0x96, 0x88,
+ 0x82, 0xb4, 0xd6, 0x2e, 0xba, 0x02, 0x9b, 0xcb, 0x26, 0x84, 0x65, 0x7d,
+ 0x83, 0x6c, 0x8a, 0x73, 0x31, 0x73, 0xd5, 0xdd, 0x15, 0x0a, 0xec, 0x48,
+ 0xf9, 0xe3, 0x2f, 0x7a, 0x59, 0x58, 0xf5, 0xb5, 0x14, 0xf5, 0x74, 0x36,
+ 0xb4, 0x46, 0x62, 0x55, 0xcf, 0x3c, 0x2f, 0x80, 0xa5, 0x20, 0x2a, 0xc7,
+ 0xd5, 0x09, 0xdd, 0xc1, 0xa5, 0xdc, 0xf1, 0x3a, 0xbd, 0xe7, 0xc7, 0x12,
+ 0x25, 0x2b, 0x28, 0xc8, 0x72, 0x2e, 0x98, 0x0b, 0x8f, 0x21, 0x2f, 0x04,
+ 0xe0, 0x7c, 0x51, 0xfd, 0x6a, 0xb0, 0x12, 0xf4, 0x5e, 0x99, 0xb7, 0x02,
+ 0x4a, 0x6e, 0x02, 0xd2, 0xd9, 0x81, 0xef, 0x6a, 0x11, 0xbc, 0x53, 0x82,
+ 0x51, 0xb6, 0xdc, 0x0e, 0x02, 0x60, 0xb3, 0x46, 0x63, 0xe1, 0xf6, 0x03,
+ 0x35, 0xee, 0xb7, 0x3b, 0x28, 0x8b, 0xa2, 0x8e, 0xfc, 0x80, 0x2e, 0x75,
+ 0xdf, 0xdf, 0xeb, 0x15, 0xba, 0x34, 0xda, 0xc8, 0x34, 0xcd, 0x71, 0xf6,
+ 0x47, 0x07, 0x32, 0x53, 0x1e, 0xe8, 0xd6, 0x9b, 0x0a, 0x3d, 0x34, 0xe2,
+ 0xc7, 0x5c, 0x63, 0x60, 0xd4, 0xd9, 0x5d, 0x3d, 0xb1, 0x51, 0x88, 0xc5,
+ 0x06, 0x26, 0xea, 0xb3, 0xc2, 0x8e, 0xdc, 0xb3, 0x41, 0x5c, 0xba, 0xcd,
+ 0x52, 0x55, 0xe6, 0x58, 0xef, 0x97, 0x1e, 0x29, 0xfa, 0x70, 0x64, 0xa6,
+ 0x01, 0x23, 0x41, 0x2e, 0xa8, 0x59, 0x87, 0x70, 0xf4, 0xae, 0xf8, 0xd8,
+ 0x4a, 0x25, 0x27, 0xbc, 0xed, 0xf8, 0x06, 0xfa, 0x5d, 0x28, 0x14, 0x48,
+ 0xb5, 0x63, 0x7a, 0x7e, 0xae, 0x55, 0x22, 0x05, 0xa3, 0xb2, 0xa1, 0xbb,
+ 0x65, 0xa3, 0xa6, 0xeb, 0x12, 0x52, 0x00, 0xd0, 0x99, 0xc3, 0x29, 0x13,
+ 0x9b, 0x11, 0xeb, 0xd5, 0x35, 0xa2, 0x76, 0x82, 0x7c, 0x0f, 0xba, 0xaa,
+ 0x64, 0x05, 0x7a, 0xb6, 0x77, 0xed, 0x76, 0xd6, 0x04, 0x7c, 0x7b, 0x2e,
+ 0x81, 0xef, 0xc4, 0x95, 0xcc, 0x1d, 0x69, 0x63, 0x5f, 0xae, 0x81, 0x70,
+ 0xb2, 0x64, 0x4e, 0x59, 0xb5, 0xd5, 0x4e, 0x50, 0xc3, 0x8d, 0xe3, 0x74,
+ 0x7e, 0xa8, 0x42, 0x68, 0x63, 0xe2, 0x68, 0x91, 0x1c, 0x76, 0x3f, 0x00,
+ 0x1d, 0x1d, 0x52, 0x3f, 0x60, 0x92, 0x84, 0x46, 0x41, 0x00, 0xb9, 0x6d,
+ 0x38, 0x27, 0x8d, 0xca, 0x11, 0x5c, 0xba, 0x17, 0xc7, 0x9c, 0x7f, 0x7b,
+ 0x44, 0xe2, 0x6f, 0x66, 0xcd, 0x36, 0xb0, 0x7a, 0x16, 0x79, 0x1e, 0x65,
+ 0x62, 0x31, 0x52, 0x17, 0xec, 0x70, 0x94, 0x81, 0xaf, 0x48, 0x95, 0xde,
+ 0x3d, 0x3c, 0x62, 0x44, 0x16, 0x71, 0x77, 0x80, 0xa8, 0x7c, 0x1c, 0x6d,
+ 0xb2, 0x13, 0x0a, 0x1c, 0xf6, 0xd2, 0x88, 0x85, 0xf9, 0x19, 0x9b, 0x91,
+ 0x77, 0x08, 0x3f, 0x1c, 0xd6, 0xbc, 0x13, 0x66, 0x1d, 0xea, 0x5c, 0x12,
+ 0x75, 0x17, 0x24, 0xf4, 0xc4, 0xbf, 0xac, 0xa1, 0x4b, 0x60, 0x2c, 0x1b,
+ 0xda, 0x44, 0x07, 0x81, 0x9c, 0x76, 0x63, 0xa2, 0x49, 0x6b, 0xb5, 0x3b,
+ 0x13, 0xf5, 0xa2, 0x10, 0x3b, 0x5e, 0xe6, 0xf8, 0xf9, 0x2b, 0xdb, 0xc4,
+ 0x8c, 0xd0, 0x7b, 0x28, 0xfa, 0x67, 0x7d, 0xc5, 0xb9, 0x25, 0xd3, 0x01,
+ 0x5e, 0x01, 0x10, 0x72, 0xfd, 0xf8, 0x4f, 0xe7, 0x68, 0x38, 0xba, 0x56,
+ 0x88, 0x33, 0xa5, 0x09, 0xb4, 0xd4, 0xcf, 0x31, 0x80, 0x57, 0xa5, 0xfe,
+ 0xce, 0x3e, 0x34, 0x99, 0x61, 0x9d, 0xa3, 0x09, 0x1f, 0x6f, 0x39, 0x53,
+ 0x64, 0xe5, 0xdf, 0xe0, 0x93, 0x6c, 0xbd, 0x68, 0xb5, 0x16, 0xdf, 0x11,
+ 0x60, 0x8d, 0x6a, 0xdc, 0x24, 0xa2, 0xc2, 0xde, 0xc8, 0xf7, 0xac, 0x8d,
+ 0xee, 0x6b, 0x8a, 0xae, 0x29, 0x06, 0xa9, 0xed, 0xaa, 0xa3, 0xab, 0x94,
+ 0x6f, 0xe0, 0x00, 0x19, 0x44, 0xa0, 0x1c, 0x1f, 0x85, 0xca, 0xc9, 0x6e,
+ 0x0b, 0x43, 0x7c, 0xc6, 0xdb, 0x20, 0x32, 0x06, 0x6b, 0x49, 0xd4, 0x54,
+ 0x88, 0x62, 0x93, 0xf0, 0xf1, 0xad, 0x7c, 0xb3, 0xf8, 0x30, 0xc9, 0x00,
+ 0x69, 0x11, 0x37, 0xa5, 0xb2, 0xa8, 0x21, 0x9c, 0x85, 0x74, 0x0b, 0xa1,
+ 0x14, 0x0d, 0x9b, 0x4a, 0x23, 0x22, 0xe0, 0x7a, 0x66, 0x19, 0xe0, 0xaf,
+ 0x47, 0xfb, 0x2a, 0x5d, 0x1e, 0x88, 0xb3, 0xe3, 0x23, 0x70, 0xfe, 0xc7,
+ 0x7b, 0x6b, 0x34, 0x7d, 0xb1, 0x8a, 0xcc, 0x1a, 0x69, 0x0d, 0x5a, 0xe7,
+ 0x34, 0xb9, 0x63, 0xc4, 0xd9, 0xc6, 0x06, 0x80, 0x7f, 0x4f, 0x00, 0x7a,
+ 0x94, 0x55, 0x08, 0x14, 0xee, 0xc5, 0x2a, 0x12, 0x88, 0x28, 0x45, 0xb0,
+ 0x12, 0x66, 0x34, 0xa3, 0xd3, 0x31, 0x3f, 0x05, 0x8e, 0x1f, 0x40, 0x3d,
+ 0x34, 0x16, 0x44, 0xca, 0x87, 0xa0, 0x60, 0xcd, 0xc6, 0x26, 0x37, 0x9f,
+ 0x56, 0x01, 0x8d, 0xc6, 0x90, 0xda, 0x5f, 0x3a, 0xc6, 0xc9, 0xf0, 0x90,
+ 0x1e, 0x88, 0x67, 0x69, 0xa1, 0x5b, 0xec, 0x8b, 0xa5, 0xe7, 0xa2, 0xfd,
+ 0xe3, 0x2c, 0x6f, 0x58, 0x82, 0x78, 0x0f, 0xcd, 0x6b, 0x54, 0x85, 0xa6,
+ 0x00, 0x29, 0x88, 0xdc, 0x67, 0x21, 0xcc, 0x0a, 0x88, 0xbb, 0xb4, 0x12,
+ 0x38, 0xee, 0x06, 0x59, 0x61, 0x08, 0x6a, 0xef, 0x52, 0xd6, 0x23, 0x63,
+ 0xd3, 0x26, 0x3a, 0x1a, 0x73, 0xc0, 0x2c, 0xf1, 0xf4, 0x60, 0xd0, 0x94,
+ 0x04, 0x3a, 0x31, 0x6d, 0xa1, 0x24, 0xab, 0x57, 0x57, 0x92, 0xd2, 0x3f,
+ 0x15, 0xf8, 0xde, 0x83, 0x91, 0x1c, 0xa2, 0x23, 0xd0, 0x82, 0x3e, 0xe3,
+ 0x59, 0x5b, 0x1f, 0xb1, 0x28, 0x1a, 0x54, 0xee, 0x60, 0x3f, 0xf1, 0xb8,
+ 0x9d, 0xee, 0xb1, 0xcb, 0x1d, 0x8b, 0x0f, 0x44, 0x63, 0xb9, 0x37, 0xfc,
+ 0x4e, 0xc6, 0xe9, 0xc9, 0xf4, 0x6d, 0x23, 0xaa, 0x25, 0xe9, 0xcf, 0x46,
+ 0x67, 0xbd, 0x28, 0x98, 0xf8, 0x5e, 0xba, 0xe3, 0x77, 0x00, 0x54, 0x30,
+ 0x52, 0xa8, 0x18, 0xd5, 0x7d, 0xb4, 0x76, 0x8f, 0xb8, 0xb1, 0x5c, 0x16,
+ 0xf8, 0xc0, 0x1d, 0x13, 0x6e, 0xf9, 0x50, 0x89, 0x95, 0xf0, 0x35, 0x77,
+ 0xd7, 0x7f, 0x17, 0x18, 0xe9, 0xf0, 0xa0, 0x6d, 0x10, 0x97, 0x9e, 0x18,
+ 0x10, 0xc4, 0xe3, 0xf4, 0x2b, 0x00, 0xde, 0x2d, 0x24, 0xd4, 0xcf, 0xa2,
+ 0x8a, 0x2b, 0x0a, 0x80, 0x18, 0x15, 0xef, 0x00, 0x9a, 0xf2, 0x7a, 0x01,
+ 0x09, 0x9b, 0xb4, 0xc4, 0xeb, 0x96, 0x70, 0xda, 0xf6, 0xef, 0x43, 0x77,
+ 0x9c, 0xae, 0x0a, 0xca, 0xfd, 0xc5, 0xb0, 0x85, 0xf8, 0xca, 0xa6, 0x49,
+ 0x01, 0xd7, 0x1a, 0x44, 0xfe, 0xe5, 0x1b, 0x91, 0x62, 0x63, 0xc5, 0x70,
+ 0x23, 0x56, 0x45, 0x74, 0xcb, 0x0c, 0x8f, 0xff, 0x95, 0xd1, 0x40, 0x00,
+ 0x0e, 0x86, 0x10, 0x2b, 0xb8, 0x40, 0x92, 0x80, 0x4f, 0xed, 0xb6, 0x5b,
+ 0x94, 0x60, 0x9f, 0x09, 0x51, 0x87, 0xa7, 0x31, 0x51, 0x06, 0xfe, 0x85,
+ 0xb8, 0x05, 0xe7, 0xd1, 0x44, 0x8b, 0x20, 0x13, 0x85, 0xaa, 0x09, 0xc8,
+ 0x8b, 0xa9, 0xec, 0x3f, 0xb7, 0x9d, 0x5b, 0x96, 0x50, 0xf1, 0x16, 0xf4,
+ 0x1d, 0x84, 0x3f, 0x9a, 0x40, 0x14, 0x2c, 0xa3, 0xa3, 0xaf, 0x01, 0xe8,
+ 0x83, 0xd4, 0x08, 0x68, 0x02, 0x58, 0xf4, 0x0c, 0xd5, 0x3a, 0xd2, 0x81,
+ 0x01, 0xeb, 0xf9, 0x91, 0x9c, 0x6e, 0x0f, 0x80, 0xce, 0xf7, 0x0e, 0x93,
+ 0xf7, 0xfe, 0x82, 0xd3, 0x0e, 0x48, 0x8d, 0xc7, 0x26, 0xd0, 0x8e, 0x5a,
+ 0xa3, 0xd7, 0x9a, 0xc3, 0x75, 0xc7, 0x42, 0xdc, 0x31, 0x53, 0x5d, 0xdf,
+ 0xab, 0xfa, 0x2d, 0x0c, 0x24, 0x3f, 0x94, 0x20, 0x84, 0xc2, 0xed, 0x0d,
+ 0x90, 0x8a, 0x98, 0x56, 0x17, 0xd0, 0xa6, 0x6a, 0x6e, 0xf0, 0x0f, 0x8c,
+ 0x5b, 0x49, 0xa6, 0x33, 0xef, 0x69, 0xab, 0x7e, 0x68, 0x52, 0xca, 0x5b,
+ 0xe6, 0x32, 0x2a, 0x2d, 0x57, 0xb5, 0x4e, 0x1a, 0xfa, 0x1b, 0xfa, 0xd2,
+ 0x02, 0x56, 0x79, 0x45, 0xc6, 0x65, 0x80, 0x3a, 0x14, 0xd6, 0xf9, 0x3b,
+ 0x57, 0x8b, 0xe5, 0xf4, 0x09, 0x31, 0x8c, 0xc9, 0x24, 0x8b, 0x4e, 0x40,
+ 0xf7, 0xf6, 0x5b, 0xa8, 0x49, 0x45, 0x9f, 0xca, 0x8d, 0x16, 0x1f, 0x2d,
+ 0x40, 0x8f, 0x82, 0x10, 0x64, 0xbc, 0xe9, 0xaf, 0x9d, 0xae, 0x82, 0x03,
+ 0x71, 0xb3, 0x77, 0xb2, 0x59, 0x4f, 0x58, 0xa6, 0xa5, 0x9c, 0xfc, 0x42,
+ 0x3a, 0x78, 0x60, 0xd6, 0xbd, 0xfd, 0x54, 0xc6, 0x2e, 0x36, 0xd9, 0xd0,
+ 0xe8, 0x12, 0xf4, 0xe3, 0xdd, 0x4c, 0xa5, 0x79, 0x9c, 0xff, 0x81, 0x46,
+ 0x30, 0xe1, 0xb3, 0x5c, 0xd6, 0x2c, 0x8b, 0x44, 0xe5, 0xd2, 0xe8, 0x04,
+ 0x0d, 0x49, 0x4d, 0xc6, 0xb8, 0x58, 0xf5, 0x78, 0x1e, 0xd0, 0x4d, 0x5d,
+ 0x99, 0xcb, 0xbe, 0x7c, 0x8f, 0xf7, 0x1e, 0x59, 0xfb, 0x1a, 0x4e, 0xa4,
+ 0xf1, 0x5d, 0xfc, 0x00, 0x04, 0x7d, 0x4d, 0xc4, 0x07, 0xb3, 0xb2, 0xd4,
+ 0xf3, 0xa3, 0xc0, 0xbc, 0xdd, 0x63, 0xc8, 0x23, 0x03, 0x97, 0x5b, 0xc0,
+ 0xc8, 0x87, 0x72, 0xeb, 0xb5, 0xa3, 0xdb, 0xe2, 0x3e, 0x4a, 0x26, 0x0a,
+ 0xd2, 0x54, 0x45, 0x71, 0x89, 0xaf, 0xa1, 0xd6, 0x21, 0xf9, 0x7f, 0x15,
+ 0xc0, 0xcd, 0x40, 0xa7, 0xb0, 0x1a, 0x7a, 0xfe, 0x34, 0xbf, 0x73, 0xf0,
+ 0x90, 0x43, 0x1f, 0x6f, 0x4e, 0x0a, 0xa6, 0x33, 0xf3, 0x33, 0xc9, 0x9d,
+ 0x85, 0x25, 0xa9, 0x13, 0xdc, 0x2b, 0xd1, 0xe7, 0x01, 0x85, 0x50, 0xb8,
+ 0x4c, 0xa6, 0xb7, 0x6d, 0xf0, 0x96, 0x0f, 0x00, 0xb3, 0x46, 0x2e, 0x94,
+ 0xf0, 0xd3, 0x67, 0xe9, 0xc0, 0x29, 0x4e, 0x75, 0xf7, 0xfa, 0x23, 0x06,
+ 0x5e, 0xee, 0x22, 0xba, 0x94, 0x84, 0xf9, 0x59, 0x26, 0x65, 0x70, 0x2a,
+ 0x81, 0xf6, 0xf9, 0x48, 0x0c, 0x38, 0xf4, 0x0c, 0x6c, 0x59, 0x8b, 0x06,
+ 0xa7, 0x07, 0x82, 0x8c, 0x8e, 0x9b, 0x09, 0xed, 0x98, 0x26, 0x3e, 0xb9,
+ 0x79, 0xcd, 0x4b, 0x03, 0xfa, 0x25, 0x77, 0x47, 0xe9, 0x63, 0xbf, 0xa9,
+ 0x41, 0x29, 0x97, 0x1f, 0xb7, 0x7f, 0x6f, 0xd1, 0xb5, 0xab, 0xae, 0x80,
+ 0xbb, 0xdc, 0x46, 0x29, 0x83, 0x4a, 0x62, 0x48, 0xfd, 0x42, 0x4a, 0xba,
+ 0xee, 0xcb, 0x99, 0xce, 0x6d, 0x5b, 0x7d, 0x0e, 0xf8, 0x35, 0x37, 0xb9,
+ 0xcd, 0xab, 0xf8, 0x49, 0xfa, 0xa9, 0xdb, 0xdb, 0xec, 0x05, 0x97, 0x09,
+ 0x6f, 0x7d, 0x0f, 0x07, 0xad, 0x27, 0x73, 0xf8, 0x51, 0x58, 0x7b, 0x1d,
+ 0x0f, 0x15, 0xb9, 0x24, 0x76, 0x84, 0x06, 0x0f, 0x2d, 0x9f, 0xb6, 0x8a,
+ 0x42, 0x70, 0x97, 0x59, 0x81, 0x7f, 0xa9, 0x5c, 0xbb, 0x89, 0x26, 0xaf,
+ 0xcd, 0x32, 0x32, 0x3a, 0x64, 0x24, 0xc9, 0xd5, 0xec, 0x62, 0xd6, 0x53,
+ 0xda, 0xfe, 0xf5, 0xd1, 0x5a, 0x46, 0x0c, 0x29, 0x94, 0x6c, 0x15, 0x33,
+ 0x63, 0xba, 0x25, 0xbd, 0x44, 0x3e, 0x4b, 0x5e, 0x5b, 0xe0, 0xf6, 0x2d,
+ 0xcf, 0x7a, 0x44, 0x3d, 0xc2, 0xc4, 0xdd, 0x76, 0x3a, 0xd1, 0x72, 0xd9,
+ 0x23, 0xb2, 0xf0, 0xc5, 0x78, 0xe3, 0x0f, 0x79, 0xb2, 0x0c, 0x07, 0x03,
+ 0x1e, 0x3e, 0x5a, 0xc7, 0x5b, 0x88, 0x1b, 0x32, 0x2c, 0x33, 0x04, 0xd8,
+ 0x2b, 0xeb, 0xa6, 0x84, 0xe9, 0xee, 0x1d, 0xed, 0x95, 0x98, 0x25, 0x80,
+ 0x07, 0x10, 0x0b, 0x9b, 0xf8, 0x63, 0xe0, 0xc4, 0xe2, 0x1d, 0x9d, 0x0e,
+ 0xc8, 0xba, 0xbb, 0x24, 0x27, 0x1c, 0xf4, 0x14, 0x9c, 0x56, 0x9e, 0xd3,
+ 0x00, 0x26, 0x61, 0x6a, 0xb8, 0x06, 0xa4, 0x09, 0xf2, 0x0d, 0x1a, 0x68,
+ 0x6c, 0x65, 0xdf, 0xce, 0x9c, 0x66, 0x07, 0x06, 0x4a, 0xba, 0x8b, 0x20,
+ 0x66, 0x4d, 0x4b, 0x88, 0x72, 0xd1, 0xa2, 0xaa, 0xd7, 0xb6, 0x07, 0x79,
+ 0x42, 0x54, 0x87, 0x4c, 0x91, 0xbf, 0xa8, 0x96, 0xf3, 0x76, 0xba, 0xb3,
+ 0xb6, 0x2b, 0x00, 0x04, 0xf1, 0xf0, 0x14, 0xe4, 0x63, 0x66, 0xc5, 0xdc,
+ 0x23, 0xb4, 0x6d, 0x93, 0x0c, 0x10, 0x49, 0x7a, 0x20, 0xb9, 0x07, 0x58,
+ 0xd8, 0xf6, 0x9e, 0x4e, 0x6a, 0x1b, 0x9d, 0x13, 0x24, 0xe5, 0xd9, 0x7b,
+ 0xa5, 0x86, 0xa6, 0x39, 0xef, 0xd3, 0xbd, 0x5b, 0xa9, 0x2b, 0x78, 0x6f,
+ 0xd8, 0x59, 0x4f, 0x20, 0x30, 0x8f, 0x38, 0x7c, 0x70, 0x32, 0xab, 0x11,
+ 0x00, 0xef, 0x47, 0x3e, 0xf5, 0x27, 0xa4, 0x7b, 0xfb, 0xc8, 0x26, 0x87,
+ 0x5a, 0x26, 0x8d, 0xee, 0xa2, 0x01, 0x98, 0x97, 0x2c, 0x57, 0x2f, 0x4e,
+ 0xf6, 0x52, 0x7a, 0x4e, 0x9c, 0xf5, 0x9e, 0x48, 0x58, 0xf4, 0x99, 0x30,
+ 0xe2, 0x36, 0x0e, 0xc5, 0x3c, 0x74, 0x30, 0xb4, 0x2f, 0x2e, 0xd5, 0xb5,
+ 0x4d, 0x39, 0x1f, 0xa0, 0xbd, 0x93, 0x39, 0xc1, 0x46, 0xcc, 0xe1, 0xbc,
+ 0x24, 0x5c, 0x35, 0x57, 0x8e, 0x0a, 0x80, 0xca, 0xd1, 0x89, 0x4d, 0x05,
+ 0xcf, 0xd6, 0x4a, 0xcf, 0xa4, 0xb7, 0xb8, 0xa3, 0x72, 0xb7, 0xa0, 0xda,
+ 0xb3, 0x81, 0xf5, 0x28, 0xae, 0xde, 0x90, 0x6a, 0x61, 0x3d, 0x60, 0xac,
+ 0xf7, 0xe6, 0xb4, 0xdf, 0xba, 0x08, 0x30, 0xd3, 0x1e, 0xc1, 0xe4, 0x39,
+ 0x93, 0xb3, 0xf8, 0x57, 0xa6, 0x9d, 0xb5, 0xbb, 0x7e, 0xe6, 0xe7, 0xb1,
+ 0x30, 0x24, 0x89, 0x2f, 0x4a, 0xec, 0xf8, 0xd9, 0x5a, 0x76, 0x1c, 0x2d,
+ 0xfc, 0xe0, 0xd5, 0x13, 0xfd, 0x48, 0x97, 0x34, 0x59, 0xa9, 0x73, 0x60,
+ 0x0f, 0x89, 0xf0, 0x5b, 0xbe, 0xf1, 0xd7, 0x2c, 0x8e, 0x07, 0x12, 0x23,
+ 0x62, 0x1c, 0xe8, 0xae, 0x1a, 0xef, 0x85, 0xab, 0x94, 0x73, 0xd0, 0xb9,
+ 0x48, 0x56, 0x9f, 0x5e, 0x5b, 0x88, 0x75, 0xf8, 0x36, 0x91, 0x1f, 0xa4,
+ 0xd7, 0xac, 0x94, 0x33, 0xcb, 0x26, 0xed, 0x87, 0x73, 0xc7, 0x7f, 0x2e,
+ 0x34, 0xdc, 0x30, 0x50, 0x2a, 0xb7, 0xac, 0x31, 0x99, 0x80, 0x86, 0xdc,
+ 0xcb, 0x0d, 0xbf, 0xce, 0x11, 0x14, 0x71, 0xf1, 0xa2, 0xa6, 0x83, 0xfc,
+ 0x30, 0x35, 0xf0, 0xb1, 0x36, 0x90, 0x65, 0xbc, 0x04, 0xa7, 0x03, 0x93,
+ 0x8b, 0x79, 0xd6, 0xb0, 0x00, 0xe1, 0x72, 0x1a, 0x13, 0x18, 0x38, 0xc3,
+ 0x5c, 0xfd, 0x9b, 0x07, 0x65, 0x16, 0x55, 0xab, 0x52, 0x16, 0x4c, 0xf2,
+ 0xaf, 0x7f, 0xcd, 0x2e, 0x54, 0x0b, 0x5f, 0xcf, 0xc5, 0x2a, 0xdf, 0xa0,
+ 0xb1, 0x23, 0xbd, 0xf7, 0xeb, 0x5b, 0xc6, 0x2c, 0x12, 0xd2, 0xe4, 0x1f,
+ 0x8b, 0xf8, 0xdb, 0xd6, 0x47, 0x76, 0x41, 0x6b, 0x4a, 0x9a, 0x63, 0x43,
+ 0x6d, 0xfa, 0x33, 0x6c, 0xf6, 0x9f, 0xa2, 0x27, 0x98, 0x06, 0x6d, 0xe0,
+ 0x79, 0x70, 0xf2, 0xd4, 0x83, 0xa4, 0x48, 0x01, 0x7d, 0xd6, 0x00, 0xbd,
+ 0xbf, 0xa3, 0xfc, 0xad, 0x9b, 0x1e, 0x92, 0xdc, 0xa7, 0xf4, 0x01, 0xe9,
+ 0x68, 0xc2, 0xb3, 0x5a, 0xeb, 0xc1, 0xe1, 0x74, 0xa8, 0xc1, 0x44, 0x55,
+ 0xc3, 0x59, 0x41, 0x19, 0xd4, 0x71, 0x56, 0xd6, 0x06, 0x24, 0x36, 0x47,
+ 0xfa, 0x8d, 0xa4, 0xb3, 0xa8, 0x54, 0xed, 0x46, 0x1a, 0xc3, 0xdd, 0x72,
+ 0xab, 0x07, 0x1e, 0xbe, 0x80, 0xfc, 0x25, 0x15, 0xd5, 0x52, 0x1b, 0x61,
+ 0x12, 0x88, 0x12, 0x01, 0x53, 0x57, 0xb7, 0xcc, 0x16, 0xc5, 0xc2, 0x49,
+ 0xe6, 0x28, 0xa3, 0xf6, 0x44, 0xd9, 0xb3, 0xed, 0xf9, 0x7f, 0x20, 0xad,
+ 0x80, 0xe5, 0x9a, 0x4b, 0xa6, 0x8e, 0xda, 0x94, 0x1c, 0x80, 0x60, 0x23,
+ 0xda, 0x75, 0x29, 0x1e, 0x3e, 0xdd, 0xba, 0x06, 0x88, 0x51, 0x25, 0xca,
+ 0x6e, 0x06, 0x39, 0x50, 0xa7, 0xf0, 0x90, 0xbe, 0xc9, 0x45, 0x8e, 0xf8,
+ 0x48, 0xae, 0xa5, 0xa3, 0xc0, 0x18, 0xaa, 0x0f, 0xbe, 0x81, 0x67, 0xe4,
+ 0xb4, 0x03, 0x2b, 0xe4, 0xb9, 0x47, 0x81, 0x8c, 0x6d, 0x48, 0xfd, 0xe7,
+ 0x55, 0xc6, 0xea, 0xb9, 0x65, 0xae, 0xd0, 0x57, 0x92, 0x20, 0x4c, 0xde,
+ 0xeb, 0x7b, 0xa8, 0x75, 0x57, 0xd7, 0x5a, 0xa3, 0xb9, 0x70, 0x9f, 0xb9,
+ 0xfb, 0xe4, 0x9b, 0x14, 0x24, 0x31, 0x31, 0xb6, 0xe9, 0x3f, 0xd5, 0x7e,
+ 0x5e, 0xf2, 0xa1, 0x9d, 0xe0, 0x02, 0x4d, 0x90, 0x3f, 0x48, 0x68, 0x95,
+ 0x37, 0xe6, 0x70, 0x60, 0xa8, 0x59, 0x01, 0x82, 0x4c, 0x66, 0x51, 0xc0,
+ 0xd3, 0x41, 0x00, 0xa7, 0x70, 0x9f, 0x4e, 0xd0, 0x44, 0x8a, 0x7b, 0x46,
+ 0x04, 0x1d, 0x06, 0x4c, 0x38, 0x8d, 0xe5, 0x63, 0x6b, 0x9d, 0x75, 0x9c,
+ 0xa2, 0x69, 0x9a, 0x22, 0x4b, 0x23, 0xe2, 0x8f, 0xe4, 0x15, 0xef, 0x76,
+ 0xf6, 0x36, 0xf8, 0xcd, 0xc1, 0x12, 0x55, 0xb0, 0xa7, 0xe5, 0xb9, 0xfb,
+ 0xd3, 0x92, 0xf9, 0xe1, 0x0c, 0xb1, 0xc7, 0x17, 0x90, 0x1a, 0x78, 0x04,
+ 0xec, 0x88, 0x6b, 0x00, 0xeb, 0xe5, 0x90, 0xec, 0x84, 0x8c, 0xb2, 0x2c,
+ 0x0d, 0xc5, 0x48, 0xc6, 0x34, 0x8b, 0x9a, 0x44, 0x45, 0x5d, 0x27, 0x40,
+ 0x36, 0x82, 0x91, 0x2b, 0xcc, 0x37, 0x5e, 0xc3, 0xd7, 0xef, 0x10, 0x43,
+ 0x7a, 0x07, 0x80, 0x21, 0xd8, 0x50, 0x8d, 0xbd, 0x75, 0xae, 0x89, 0x0f,
+ 0xe2, 0x6f, 0x30, 0x48, 0x00, 0xa1, 0xb8, 0xe8, 0x86, 0x61, 0xdb, 0xa2,
+ 0x26, 0x31, 0x28, 0xfa, 0x64, 0xfb, 0x0f, 0x87, 0x35, 0xef, 0x00, 0x4a,
+ 0x39, 0xb0, 0xf0, 0x26, 0x2a, 0x6e, 0xae, 0x4e, 0xad, 0x71, 0xb2, 0x64,
+ 0x4b, 0xc9, 0xda, 0x49, 0x97, 0x1b, 0x6a, 0x3a, 0xe9, 0x01, 0x43, 0xa0,
+ 0x8f, 0xbc, 0xf5, 0xa0, 0x06, 0xd6, 0x80, 0x70, 0x37, 0x27, 0x85, 0xf8,
+ 0x81, 0x07, 0x69, 0x80, 0x5a, 0xf4, 0x27, 0x4b, 0x41, 0x47, 0xf9, 0xc0,
+ 0x61, 0x8f, 0x79, 0xb2, 0xea, 0xfb, 0x99, 0xf1, 0x23, 0x94, 0xc0, 0xce,
+ 0x69, 0x9a, 0x32, 0x09, 0x02, 0x1a, 0x84, 0x66, 0xd5, 0x50, 0x58, 0xa8,
+ 0x4a, 0xdb, 0xb2, 0x33, 0x7d, 0x7f, 0xb4, 0xa2, 0xe7, 0x8a, 0x5b, 0xc6,
+ 0x9d, 0x5c, 0xbf, 0x75, 0x50, 0xac, 0xea, 0xe5, 0x01, 0x41, 0x12, 0x29,
+ 0x56, 0x15, 0x58, 0x6c, 0xa4, 0x85, 0x71, 0x9a, 0x31, 0x3c, 0xc2, 0x9b,
+ 0xd3, 0x9a, 0xcd, 0xed, 0x9a, 0xb5, 0x48, 0xf5, 0xc6, 0xdc, 0xa8, 0x51,
+ 0x9e, 0x45, 0x8a, 0x50, 0xab, 0x8d, 0x5e, 0x74, 0x00, 0xd7, 0x6f, 0x48,
+ 0x7a, 0xc3, 0xb0, 0x7f, 0x21, 0x3b, 0x7b, 0x9a, 0x40, 0x24, 0xae, 0x42,
+ 0x28, 0x90, 0xd6, 0x38, 0xdc, 0x86, 0x6c, 0xd9, 0x62, 0x9e, 0x2f, 0x48,
+ 0xbc, 0x83, 0x43, 0x51, 0x6c, 0x2b, 0x09, 0x05, 0x78, 0xf0, 0xa1, 0x5c,
+ 0x9c, 0x02, 0x5e, 0xa8, 0x5a, 0xc8, 0x6a, 0x13, 0x01, 0xa7, 0xa3, 0x7d,
+ 0xff, 0x0b, 0xe3, 0x53, 0x0e, 0xae, 0x6c, 0xae, 0xfb, 0x86, 0xaf, 0xf7,
+ 0x00, 0x87, 0x86, 0x76, 0x59, 0x34, 0x88, 0x14, 0x73, 0xef, 0xe3, 0xe1,
+ 0xfd, 0x5a, 0x6c, 0xd2, 0x42, 0x83, 0x55, 0xae, 0x3f, 0x1e, 0x25, 0x09,
+ 0xd6, 0xd7, 0x19, 0x38, 0xb6, 0x47, 0x4b, 0x00, 0x70, 0x22, 0xb5, 0x54,
+ 0x19, 0xd1, 0x5c, 0x46, 0xe9, 0x2a, 0x16, 0x39, 0xf0, 0x37, 0x67, 0xa3,
+ 0x25, 0xe5, 0xbf, 0xe3, 0x12, 0x36, 0xdc, 0x6f, 0xad, 0x51, 0xf0, 0x0f,
+ 0x88, 0x4a, 0xb9, 0x3a, 0x14, 0x94, 0x56, 0xf8, 0x3a, 0xf6, 0x5b, 0x45,
+ 0x80, 0x03, 0x2e, 0xe3, 0xfd, 0x55, 0xee, 0x16, 0x5c, 0x33, 0xe4, 0x73,
+ 0x4e, 0x84, 0x60, 0x28, 0x62, 0x70, 0x7a, 0x1b, 0xca, 0xb6, 0xad, 0x17,
+ 0xa4, 0x3c, 0x08, 0xb6, 0x00, 0x82, 0x38, 0x7f, 0xbe, 0x4d, 0x67, 0x4a,
+ 0x5f, 0x64, 0x3b, 0x37, 0x51, 0x99, 0x9e, 0xd9, 0x98, 0x99, 0xc5, 0x96,
+ 0x10, 0x68, 0x46, 0xc8, 0xed, 0x01, 0x0d, 0x39, 0x04, 0xbc, 0xe0, 0x1a,
+ 0x61, 0xc9, 0x6c, 0x61, 0xd6, 0xf6, 0xee, 0xb3, 0xec, 0xda, 0xf4, 0x73,
+ 0x94, 0x92, 0xc8, 0x01, 0xd9, 0x21, 0xd9, 0xd7, 0x86, 0xf8, 0xd4, 0xfb,
+ 0x85, 0x70, 0x5e, 0x8d, 0x0e, 0xbd, 0x58, 0x79, 0xeb, 0x0b, 0xd7, 0xd4,
+ 0xbf, 0x2d, 0xa7, 0x4e, 0xc0, 0xef, 0x22, 0x04, 0x90, 0x3f, 0x6f, 0xaf,
+ 0xe5, 0xb8, 0x3f, 0xb5, 0x70, 0x26, 0x08, 0xe3, 0xa0, 0x05, 0xae, 0x1d,
+ 0x5a, 0xec, 0xc7, 0xbd, 0x22, 0xc9, 0x13, 0xc9, 0xc1, 0xfc, 0xe0, 0xac,
+ 0x29, 0xb7, 0x5a, 0xab, 0xc1, 0x3e, 0x34, 0xf0, 0xf2, 0x73, 0x19, 0x71,
+ 0xde, 0x3f, 0xa3, 0x28, 0xc1, 0xb4, 0x6d, 0xbe, 0x7b, 0x06, 0xa5, 0x92,
+ 0x15, 0x7a, 0x46, 0x63, 0x8a, 0x4c, 0x31, 0x7d, 0xa0, 0x43, 0x3e, 0xca,
+ 0x05, 0x02, 0x2e, 0xb1, 0x1d, 0x81, 0xd7, 0x3b, 0x16, 0xf8, 0x98, 0x01,
+ 0x6f, 0xff, 0x7a, 0x3a, 0xfd, 0x32, 0x70, 0xb1, 0xf0, 0x20, 0xa7, 0x3e,
+ 0x78, 0x43, 0xde, 0x9c, 0xf9, 0xff, 0x51, 0xd2, 0x1b, 0xe8, 0xa5, 0x51,
+ 0x7b, 0x23, 0x31, 0x2c, 0xa2, 0x59, 0x37, 0x67, 0x6a, 0x79, 0xd2, 0x9f,
+ 0x6c, 0xbd, 0x85, 0xa5, 0xab, 0xfa, 0x4f, 0xea, 0x8a, 0xe8, 0xcd, 0x4d,
+ 0xd8, 0xbc, 0x04, 0x60, 0x5a, 0xff, 0xbf, 0x5b, 0xbb, 0x23, 0xef, 0xe4,
+ 0xb7, 0xb8, 0x2e, 0x14, 0x4f, 0xbc, 0xf5, 0x5b, 0xa2, 0x96, 0x56, 0xba,
+ 0xd7, 0x7f, 0x44, 0x35, 0xed, 0xcc, 0x05, 0xd8, 0xc8, 0xe2, 0x7a, 0x71,
+ 0x39, 0x2f, 0xd9, 0x51, 0xd5, 0x30, 0x10, 0x54, 0xc2, 0xa6, 0x42, 0xd3,
+ 0x8c, 0x64, 0x00, 0x87, 0xc3, 0xd7, 0xec, 0x00, 0xaa, 0xcf, 0xe1, 0xaf,
+ 0x9e, 0x74, 0xf0, 0x0b, 0xf4, 0x0f, 0x97, 0xc9, 0x0d, 0xbe, 0x7e, 0x01,
+ 0xcf, 0xe9, 0x85, 0x08, 0x83, 0x35, 0x08, 0x05, 0xa2, 0x5d, 0x83, 0x7f,
+ 0x6a, 0xe5, 0xad, 0x42, 0xcb, 0xa3, 0x02, 0x1f, 0x39, 0x68, 0x6e, 0x3d,
+ 0xf2, 0x8a, 0x1b, 0x1d, 0x9e, 0x5f, 0x1d, 0x76, 0x6b, 0x97, 0xc2, 0xcc,
+ 0x7e, 0x0c, 0x72, 0x4b, 0xa9, 0x0f, 0x8f, 0x14, 0x31, 0x46, 0x80, 0x0e,
+ 0x25, 0xac, 0xbc, 0xb9, 0x6f, 0x43, 0xe5, 0xa8, 0x53, 0xe8, 0x48, 0xf2,
+ 0x6e, 0xae, 0xfa, 0x3a, 0xa9, 0x08, 0x27, 0xb3, 0x0d, 0xd6, 0xfb, 0x77,
+ 0xfa, 0x28, 0x96, 0x5a, 0x17, 0x31, 0xd7, 0x97, 0x84, 0x2b, 0xae, 0xe5,
+ 0x8a, 0x9e, 0x04, 0xfe, 0x74, 0x93, 0x6b, 0x35, 0xcd, 0x68, 0x14, 0x14,
+ 0x83, 0xbc, 0xe5, 0x59, 0x82, 0x65, 0x67, 0x47, 0xdc, 0x34, 0x4c, 0x7e,
+ 0x8f, 0x65, 0x70, 0xb1, 0xfc, 0xd5, 0x60, 0x2a, 0x95, 0x23, 0x90, 0x5b,
+ 0x07, 0xfc, 0x45, 0x09, 0x52, 0xfa, 0xbb, 0x8f, 0xf4, 0x33, 0xd1, 0x4b,
+ 0x8a, 0x5a, 0xc3, 0xe1, 0x20, 0x9c, 0x40, 0x2d, 0x0b, 0xd2, 0xe6, 0x85,
+ 0xea, 0x93, 0x33, 0x7c, 0xeb, 0x08, 0x42, 0xef, 0x93, 0x0a, 0x1b, 0x9a,
+ 0x4b, 0xbd, 0xef, 0x2d, 0x8a, 0x84, 0x1f, 0x61, 0xe1, 0xed, 0x08, 0x43,
+ 0xc8, 0x64, 0x56, 0x21, 0x8c, 0x1a, 0x56, 0x58, 0x24, 0xa5, 0x78, 0x5d,
+ 0x4f, 0x71, 0xca, 0xa1, 0xae, 0x05, 0x96, 0x4c, 0x98, 0x9c, 0x7d, 0x19,
+ 0x7b, 0x38, 0x59, 0xa9, 0x73, 0xed, 0x37, 0xce, 0x62, 0x45, 0x14, 0xb7,
+ 0x5b, 0x1d, 0x09, 0x1f, 0xc3, 0x6e, 0x57, 0xf8, 0x64, 0xde, 0xe7, 0xb3,
+ 0x25, 0xc0, 0x5c, 0xac, 0xb5, 0xe3, 0x9e, 0xf3, 0x0d, 0xf8, 0xf8, 0x80,
+ 0xf6, 0x8b, 0x25, 0x94, 0xda, 0x14, 0x5f, 0x61, 0x13, 0xf5, 0x2a, 0x6d,
+ 0xe7, 0xf5, 0xaf, 0x5c, 0xf6, 0xdb, 0x11, 0xaf, 0xbf, 0x28, 0xa0, 0xe9,
+ 0x02, 0xa7, 0x4f, 0x26, 0x6f, 0xef, 0xa1, 0x95, 0xa7, 0x3e, 0x5e, 0x86,
+ 0x34, 0x79, 0xb1, 0x24, 0xc5, 0x5e, 0xd8, 0x59, 0x75, 0xf7, 0xf1, 0xd0,
+ 0x50, 0xa7, 0x5c, 0x83, 0x37, 0xbe, 0x03, 0x82, 0xb9, 0xa6, 0xdb, 0x20,
+ 0xe8, 0x8e, 0x06, 0x58, 0xea, 0xea, 0xfd, 0x53, 0x27, 0xeb, 0x62, 0x9f,
+ 0xab, 0x54, 0x17, 0xa0, 0x92, 0x57, 0xc9, 0x3a, 0x1c, 0x7b, 0xad, 0xd1,
+ 0xd7, 0x4e, 0x26, 0x1f, 0xd7, 0x5d, 0x05, 0xc1, 0x4c, 0xee, 0x6b, 0x4f,
+ 0x6b, 0xb4, 0x96, 0x5d, 0x9f, 0x40, 0x37, 0x32, 0x26, 0x65, 0xb8, 0xfb,
+ 0x5f, 0xb3, 0xc4, 0x96, 0x9a, 0xe9, 0x2c, 0x57, 0xc5, 0x0c, 0x82, 0xbc,
+ 0xcf, 0x8a, 0xba, 0xfa, 0x34, 0x9b, 0xa0, 0xd5, 0x7e, 0x0a, 0xab, 0x02,
+ 0xe9, 0x0a, 0xb8, 0x40, 0xf9, 0x15, 0xa7, 0x39, 0xe0, 0xec, 0x2d, 0x32,
+ 0x7f, 0x22, 0x00, 0x8f, 0x44, 0x0a, 0x78, 0x31, 0xa3, 0xd1, 0x40, 0x00,
+ 0x05, 0x42, 0x1c, 0x27, 0x11, 0xf2, 0x13, 0x74, 0xf4, 0x8a, 0xa0, 0x92,
+ 0xe6, 0x11, 0xef, 0x3a, 0x05, 0x0b, 0x08, 0x01, 0x26, 0x1f, 0x1a, 0x54,
+ 0xbd, 0x6d, 0x05, 0x38, 0x83, 0x2d, 0x74, 0x16, 0xc9, 0x6b, 0xa9, 0x54,
+ 0x9f, 0x7e, 0xed, 0xef, 0x5a, 0xd8, 0x83, 0x38, 0x3d, 0x57, 0x34, 0x49,
+ 0x8c, 0xbd, 0x9d, 0x0d, 0x16, 0x46, 0xa2, 0xec, 0xbf, 0x0f, 0x8f, 0x64,
+ 0x53, 0x63, 0x03, 0x99, 0xa6, 0xa8, 0xcc, 0x91, 0xd4, 0x7a, 0xa9, 0x4e,
+ 0xf5, 0xe0, 0xe9, 0xc4, 0x52, 0x36, 0x82, 0x6d, 0xa6, 0x2c, 0x23, 0xa0,
+ 0x1e, 0xca, 0xf3, 0x54, 0xd9, 0x1b, 0x31, 0x79, 0xb1, 0x2c, 0x8f, 0xec,
+ 0x0b, 0x81, 0xa3, 0x0f, 0x21, 0xee, 0x86, 0x30, 0x5a, 0xb1, 0xe2, 0x18,
+ 0x4e, 0xb2, 0x88, 0x08, 0xe0, 0x67, 0x70, 0xdd, 0x56, 0x9b, 0x34, 0xf6,
+ 0xc9, 0x19, 0x46, 0x07, 0x90, 0x1c, 0xf6, 0x31, 0x33, 0x91, 0x65, 0xf2,
+ 0xb2, 0xe4, 0xb1, 0xb5, 0xb9, 0x97, 0x6f, 0x5b, 0x29, 0xde, 0x51, 0x43,
+ 0xa7, 0xac, 0x7e, 0x43, 0xc5, 0x4d, 0x2b, 0xef, 0x2b, 0xd8, 0xcf, 0x88,
+ 0x1d, 0x5b, 0xa3, 0xda, 0x13, 0xf8, 0x55, 0x35, 0xb3, 0xb8, 0x5b, 0xe9,
+ 0x7c, 0x48, 0x70, 0xa7, 0xa8, 0xde, 0xe8, 0x0f, 0x08, 0xc1, 0x66, 0x43,
+ 0xfe, 0xd5, 0x19, 0x6d, 0x05, 0x53, 0x48, 0x2c, 0x6f, 0x21, 0x48, 0x88,
+ 0x9f, 0xaf, 0xa5, 0x1b, 0x3c, 0x3f, 0xcf, 0xcf, 0x3d, 0xd5, 0xcb, 0x5b,
+ 0xf8, 0xb6, 0x3c, 0x1c, 0xe2, 0x7f, 0xd3, 0xae, 0x74, 0x26, 0xd4, 0xc1,
+ 0x27, 0xad, 0xb5, 0xf9, 0x8a, 0x43, 0x92, 0xfe, 0xac, 0x56, 0xc7, 0x86,
+ 0x44, 0xcd, 0x78, 0xb9, 0xd6, 0xf1, 0xec, 0x34, 0x24, 0x4b, 0x03, 0xdc,
+ 0xa6, 0x42, 0x66, 0xd1, 0xf6, 0x5e, 0x98, 0xba, 0x86, 0x9f, 0xdd, 0xf2,
+ 0xb9, 0x0e, 0x91, 0xf6, 0x7c, 0x82, 0xaa, 0x94, 0x0e, 0x34, 0x18, 0x7b,
+ 0x9f, 0x11, 0x73, 0x6a, 0x57, 0x8e, 0x0d, 0x43, 0xb8, 0x0c, 0xe1, 0x0e,
+ 0xc3, 0x1a, 0x6e, 0xbe, 0x4d, 0xb7, 0x22, 0xe1, 0xca, 0x7d, 0x4a, 0x5c,
+ 0xdb, 0x72, 0x2c, 0x29, 0xe7, 0xa8, 0x66, 0x08, 0xfd, 0x84, 0x58, 0x77,
+ 0x02, 0x96, 0xd0, 0xcc, 0xac, 0x07, 0xd9, 0x62, 0xdd, 0xbe, 0xfc, 0xdb,
+ 0x13, 0x65, 0x5a, 0xbc, 0xe1, 0x02, 0xd5, 0x57, 0x2e, 0xda, 0x58, 0x13,
+ 0x11, 0xa9, 0xe5, 0x1f, 0x5d, 0xd2, 0xff, 0xc4, 0x0c, 0x11, 0x2e, 0x4d,
+ 0x39, 0x6e, 0x7b, 0x86, 0x4e, 0xbf, 0xa2, 0x28, 0xdb, 0x7c, 0x5b, 0x72,
+ 0x2d, 0x8a, 0xaf, 0xff, 0x2b, 0x4c, 0x77, 0x4d, 0xe5, 0xcf, 0x3b, 0xd4,
+ 0x86, 0x64, 0x82, 0x9e, 0x6e, 0x4b, 0xdf, 0x4b, 0xa5, 0x80, 0xe4, 0xde,
+ 0x00, 0xcd, 0xb9, 0x8c, 0xfe, 0x54, 0xf9, 0xc1, 0x88, 0x09, 0x7b, 0x05,
+ 0x30, 0xbb, 0xa6, 0x8d, 0x9c, 0x9d, 0xdb, 0x02, 0x7f, 0x83, 0x60, 0xd0,
+ 0x33, 0x15, 0xf8, 0xf0, 0xaa, 0x38, 0x11, 0x2a, 0x75, 0xad, 0x21, 0x5f,
+ 0x1b, 0x1f, 0xdf, 0x89, 0x83, 0xc5, 0x3f, 0xc7, 0x19, 0xad, 0x80, 0x98,
+ 0xb6, 0x6a, 0x29, 0x36, 0x15, 0x87, 0x36, 0x14, 0x66, 0xef, 0x19, 0x10,
+ 0xc2, 0xae, 0x11, 0xe1, 0xd2, 0x05, 0x7d, 0xcb, 0xfe, 0x2b, 0x2a, 0x67,
+ 0xbf, 0xf5, 0xe4, 0xd3, 0xe2, 0x13, 0x40, 0xfc, 0x38, 0x88, 0xe6, 0x26,
+ 0x99, 0x6d, 0xc2, 0x36, 0x2b, 0x56, 0x56, 0x0e, 0xaa, 0xc8, 0x50, 0xe8,
+ 0x9f, 0x6e, 0x03, 0xc9, 0x3d, 0x82, 0x25, 0x86, 0x43, 0xde, 0xdd, 0x25,
+ 0x00, 0x33, 0x63, 0x06, 0xfa, 0xc1, 0xe2, 0xf3, 0x1a, 0x3a, 0x99, 0x95,
+ 0xa1, 0x87, 0x98, 0x5d, 0xa3, 0x69, 0x6b, 0x96, 0xc2, 0x71, 0x02, 0xe8,
+ 0xde, 0x3d, 0x89, 0x93, 0x1b, 0xfa, 0x51, 0x78, 0x8e, 0x42, 0x04, 0xee,
+ 0x22, 0x31, 0x23, 0xd6, 0xfa, 0x6f, 0xcf, 0x1b, 0x07, 0x4e, 0x5a, 0x3f,
+ 0xca, 0xa7, 0x08, 0xda, 0x85, 0xf5, 0xc0, 0x05, 0x4a, 0xb0, 0xe0, 0x20,
+ 0x38, 0x84, 0x26, 0x5b, 0x98, 0xb5, 0xa6, 0x33, 0xe2, 0x28, 0x26, 0x81,
+ 0x58, 0xd9, 0x0c, 0x3a, 0xf3, 0x95, 0x74, 0xa5, 0x0a, 0x32, 0x3f, 0x8c,
+ 0x6c, 0x02, 0x1a, 0xb5, 0x94, 0x40, 0xde, 0xef, 0xc5, 0x87, 0x67, 0x8a,
+ 0xdc, 0x94, 0xb9, 0xd1, 0x39, 0xe5, 0xae, 0x6d, 0x7d, 0x77, 0xe3, 0x93,
+ 0x8d, 0x8e, 0x05, 0xca, 0x81, 0xf2, 0x93, 0x0c, 0xc1, 0x08, 0x50, 0xc6,
+ 0xa7, 0x29, 0xcb, 0xce, 0x3c, 0x3a, 0x02, 0xae, 0xf8, 0x4a, 0x93, 0x18,
+ 0xe5, 0x5d, 0x7f, 0x45, 0x3f, 0x2f, 0x38, 0xdc, 0x97, 0x0f, 0x97, 0x08,
+ 0x7c, 0x94, 0xcd, 0x64, 0xe9, 0x51, 0x40, 0x6c, 0x52, 0x6a, 0x18, 0x33,
+ 0x57, 0x16, 0xb0, 0x6f, 0x14, 0x10, 0x00, 0x1d, 0x8c, 0x85, 0xa6, 0x0a,
+ 0x37, 0xeb, 0xed, 0xb3, 0x0a, 0xe9, 0x70, 0x58, 0xd8, 0x30, 0xd6, 0x80,
+ 0x5b, 0x4b, 0x05, 0x48, 0x01, 0x8d, 0x85, 0xe2, 0xf2, 0xd3, 0x83, 0xe6,
+ 0x08, 0xae, 0x06, 0x1c, 0x07, 0xc5, 0x95, 0xfe, 0xce, 0x07, 0xcb, 0xcc,
+ 0xd1, 0xf2, 0x67, 0x85, 0xa8, 0x73, 0x08, 0xcc, 0x77, 0x90, 0x99, 0x09,
+ 0xdc, 0x54, 0x07, 0x81, 0x14, 0x69, 0x77, 0xfe, 0x4a, 0xb2, 0x70, 0x77,
+ 0x76, 0x16, 0xe6, 0x12, 0x54, 0xa3, 0xc1, 0x51, 0x81, 0x8f, 0xfe, 0x7a,
+ 0xe8, 0x0f, 0x12, 0xc9, 0x2e, 0x7c, 0xc1, 0x70, 0x24, 0x93, 0xd4, 0x06,
+ 0x14, 0x64, 0x4c, 0xa0, 0x62, 0x61, 0xe9, 0x7f, 0xd6, 0x57, 0x28, 0x42,
+ 0xd2, 0x4c, 0x87, 0xb6, 0xb8, 0x74, 0x34, 0x05, 0xb6, 0xad, 0xad, 0x65,
+ 0x75, 0x74, 0x39, 0x0c, 0x2f, 0x8c, 0xae, 0xc7, 0x54, 0xd5, 0xc5, 0x59,
+ 0x56, 0x1a, 0xa5, 0xe0, 0x34, 0xfd, 0x2e, 0xdc, 0x3e, 0x91, 0x9f, 0xa8,
+ 0x79, 0xc3, 0x63, 0x9a, 0x1f, 0x62, 0xd6, 0x0f, 0xe6, 0x22, 0x27, 0x80,
+ 0x43, 0xe5, 0xba, 0x60, 0xaa, 0xcf, 0x75, 0xd7, 0x1f, 0x3e, 0x5b, 0x75,
+ 0x0b, 0x6e, 0x36, 0xf8, 0x98, 0x5d, 0xf6, 0x0e, 0xee, 0x60, 0x52, 0x4b,
+ 0xfd, 0x7c, 0xe1, 0xd0, 0xac, 0x41, 0xad, 0x29, 0x8c, 0x8f, 0x40, 0xe7,
+ 0x6e, 0xd1, 0xfa, 0xea, 0xd7, 0x9a, 0x52, 0xf8, 0xa0, 0x13, 0xe0, 0x8d,
+ 0xb5, 0x17, 0xaf, 0x90, 0x72, 0x07, 0x1c, 0x83, 0x65, 0x76, 0x08, 0x5d,
+ 0xe6, 0x25, 0xc9, 0x26, 0xef, 0x78, 0x17, 0x52, 0xb1, 0x6f, 0x9f, 0x2e,
+ 0x47, 0x04, 0x46, 0x4f, 0x00, 0x96, 0x9b, 0x08, 0x70, 0x72, 0x15, 0xf4,
+ 0x13, 0x86, 0xd5, 0x22, 0x10, 0x4d, 0xf9, 0xe7, 0x36, 0x27, 0x91, 0xf8,
+ 0xdd, 0x36, 0xa9, 0x42, 0x68, 0x5c, 0x8f, 0xcf, 0x24, 0xf7, 0xb6, 0xc4,
+ 0x59, 0x6f, 0x67, 0x41, 0xc1, 0xfb, 0xd7, 0xd5, 0xba, 0x9c, 0xbe, 0xa1,
+ 0xfb, 0x0a, 0x71, 0x45, 0xd0, 0xd4, 0xcb, 0x05, 0x56, 0x00, 0x47, 0x57,
+ 0xb4, 0x65, 0x0a, 0x69, 0x02, 0x1a, 0x5e, 0x36, 0x61, 0xd2, 0x39, 0xff,
+ 0x1e, 0xbd, 0x37, 0x6d, 0xa6, 0x15, 0x33, 0x94, 0x17, 0x06, 0xf3, 0xc6,
+ 0xf4, 0xa3, 0xc2, 0x04, 0x2d, 0x77, 0x44, 0xdd, 0x51, 0xa3, 0xc8, 0x3c,
+ 0xee, 0x8a, 0x97, 0x84, 0x2a, 0xb7, 0x39, 0xb9, 0xdc, 0xd6, 0x0f, 0x26,
+ 0x82, 0x61, 0x77, 0x69, 0x81, 0x9c, 0x7d, 0x13, 0x7a, 0xc4, 0x80, 0x8f,
+ 0x65, 0x5e, 0x41, 0x41, 0x9f, 0xb2, 0x86, 0x0d, 0x20, 0x33, 0x9d, 0x6a,
+ 0x35, 0x5f, 0x3d, 0xe6, 0x93, 0x77, 0x6f, 0xf8, 0xad, 0xd0, 0xd2, 0x97,
+ 0xc5, 0xa9, 0x31, 0xc7, 0x51, 0x26, 0xb0, 0xb5, 0x27, 0x0b, 0x37, 0x03,
+ 0xc8, 0x4d, 0x65, 0xc7, 0x5d, 0x05, 0x44, 0x63, 0xf3, 0x55, 0x97, 0xb9,
+ 0xa8, 0x9d, 0x53, 0xe5, 0x40, 0xd0, 0x4e, 0xaa, 0xb6, 0x65, 0x99, 0x0c,
+ 0x5e, 0x5a, 0x18, 0x09, 0x32, 0x29, 0x9c, 0xc6, 0x90, 0x66, 0x90, 0x74,
+ 0x9f, 0xfb, 0x10, 0xa7, 0xea, 0x35, 0x13, 0x05, 0xe5, 0x38, 0xfd, 0x45,
+ 0x86, 0x26, 0x2d, 0x17, 0x80, 0x8f, 0x44, 0xcd, 0x92, 0xfa, 0x47, 0xc4,
+ 0xef, 0x9e, 0xda, 0x5c, 0x1e, 0x14, 0x85, 0xd8, 0xf7, 0xc2, 0x17, 0xd7,
+ 0x9e, 0xd7, 0xa7, 0x36, 0xec, 0x6a, 0x33, 0x5b, 0xad, 0x7d, 0xcb, 0x72,
+ 0x82, 0xaf, 0x65, 0x8d, 0x13, 0x1d, 0x3d, 0xd5, 0x19, 0xc2, 0xb8, 0x84,
+ 0x64, 0xce, 0xe8, 0x93, 0x86, 0x5f, 0x00, 0x95, 0x26, 0x4e, 0x2e, 0x33,
+ 0x1d, 0x0c, 0x48, 0x3c, 0x5b, 0xbe, 0x74, 0xda, 0x97, 0xde, 0x5d, 0xb9,
+ 0x3e, 0x65, 0x4d, 0x88, 0x3d, 0x05, 0x2d, 0x3a, 0x6b, 0xc7, 0x2f, 0x8c,
+ 0x0d, 0x02, 0x8e, 0x8b, 0x2c, 0x07, 0xa3, 0x24, 0xcb, 0xf6, 0x13, 0x63,
+ 0xd8, 0xee, 0x86, 0xda, 0x2e, 0x53, 0x87, 0xe3, 0x36, 0x81, 0x58, 0x08,
+ 0x6e, 0x6a, 0xfa, 0xd5, 0xc1, 0x8b, 0xb8, 0x10, 0x0a, 0xff, 0x56, 0x2a,
+ 0x4b, 0x0e, 0x0d, 0x63, 0x31, 0xb5, 0x66, 0xe6, 0x7c, 0x53, 0x8e, 0x46,
+ 0x24, 0x28, 0x9c, 0x55, 0x40, 0x5b, 0xe6, 0xc2, 0x14, 0xfa, 0xa9, 0x56,
+ 0xce, 0x88, 0x0f, 0x6d, 0xc5, 0x9c, 0x45, 0x25, 0x5e, 0xf5, 0x82, 0x61,
+ 0x0f, 0x2b, 0x70, 0x9f, 0xbe, 0xd0, 0xdd, 0xd0, 0x9c, 0x0b, 0x8d, 0x97,
+ 0xcd, 0x75, 0xe4, 0x9a, 0x63, 0x28, 0x1c, 0x09, 0xc3, 0x14, 0x77, 0x9e,
+ 0xba, 0x03, 0xcf, 0x89, 0xc6, 0xfe, 0xc3, 0xf1, 0x24, 0xb3, 0xc5, 0xf2,
+ 0xf7, 0xf2, 0xac, 0x1d, 0xc9, 0xb8, 0xb1, 0x9d, 0xfd, 0x2c, 0xd5, 0x27,
+ 0xe3, 0x0e, 0x46, 0x22, 0x6a, 0xa8, 0xc1, 0x2a, 0xa6, 0x6f, 0xcc, 0xed,
+ 0x99, 0xba, 0x5f, 0x5f, 0x72, 0x3d, 0xdf, 0xb9, 0xed, 0x02, 0x5f, 0xaa,
+ 0xe6, 0x2a, 0x25, 0x12, 0xfe, 0x27, 0xaf, 0xed, 0xa2, 0x63, 0xf4, 0x27,
+ 0x5c, 0xb6, 0xea, 0x00, 0x0a, 0x0b, 0x10, 0xdb, 0xf4, 0x9a, 0x3c, 0x90,
+ 0x96, 0xaf, 0x31, 0x7f, 0x94, 0x25, 0xaf, 0x41, 0x8c, 0x7f, 0xf5, 0xe3,
+ 0x4a, 0xb4, 0x96, 0x88, 0x8e, 0x67, 0x52, 0x39, 0x00, 0x0b, 0xe0, 0xad,
+ 0x81, 0x4c, 0xf9, 0xb1, 0x18, 0xc8, 0x07, 0xa3, 0x85, 0x5d, 0x15, 0xc7,
+ 0x3f, 0x69, 0x0c, 0x6b, 0x40, 0x74, 0x74, 0x92, 0x25, 0x4a, 0xc7, 0x66,
+ 0xf1, 0x70, 0x17, 0xc5, 0x0c, 0x7e, 0x63, 0x96, 0xf8, 0xfe, 0xba, 0xa3,
+ 0xd4, 0x39, 0x8d, 0xd8, 0x8b, 0xe8, 0x5e, 0x3b, 0x13, 0x34, 0x4d, 0x5e,
+ 0xca, 0x8e, 0x63, 0xa1, 0x53, 0x61, 0x4c, 0xf0, 0x92, 0x16, 0x05, 0x2f,
+ 0x0a, 0x02, 0x7f, 0x06, 0xfa, 0x19, 0xc8, 0x8b, 0x38, 0x14, 0x86, 0xa5,
+ 0x97, 0x94, 0xd7, 0xce, 0xbe, 0x9e, 0xb4, 0x5c, 0x29, 0xf9, 0x7b, 0x5b,
+ 0x05, 0x1b, 0xc1, 0x97, 0x92, 0xf2, 0x58, 0xd7, 0xbf, 0xb6, 0x3e, 0x29,
+ 0xde, 0x1e, 0x16, 0x46, 0xbe, 0x7d, 0x1c, 0xf8, 0xc4, 0xaa, 0xe8, 0x02,
+ 0x18, 0x1d, 0x79, 0x5d, 0x8b, 0xae, 0x7b, 0x2b, 0xe3, 0x75, 0x8b, 0x5a,
+ 0xb8, 0x2e, 0xb5, 0xb8, 0x84, 0xf5, 0x10, 0x26, 0x6e, 0x3c, 0xbf, 0x3d,
+ 0x4d, 0xb0, 0x2c, 0x49, 0xe2, 0x8d, 0xf6, 0x38, 0x32, 0xf2, 0xfa, 0x3e,
+ 0x8d, 0xa0, 0x48, 0x15, 0x21, 0x2e, 0xaa, 0xe6, 0xf9, 0x73, 0x14, 0x79,
+ 0xaf, 0x8f, 0xaa, 0xcb, 0x6e, 0x0c, 0xeb, 0x21, 0xc3, 0x02, 0x96, 0x07,
+ 0x21, 0x56, 0x6c, 0x41, 0x5f, 0x28, 0x2b, 0x61, 0xaf, 0xd7, 0xdb, 0xdf,
+ 0x01, 0x94, 0x38, 0xfa, 0x49, 0x77, 0x05, 0x99, 0xc5, 0xec, 0x94, 0xfc,
+ 0x89, 0xfc, 0xb0, 0x6d, 0x6a, 0x83, 0xce, 0xe7, 0xd9, 0xb6, 0xfe, 0x4f,
+ 0x65, 0x03, 0x05, 0x7f, 0xdb, 0xd6, 0xd8, 0x11, 0xcd, 0x8b, 0x3d, 0x0c,
+ 0x6f, 0x0f, 0x8c, 0x0b, 0x92, 0x03, 0x7a, 0xf2, 0xb1, 0xb6, 0x66, 0x7a,
+ 0x14, 0x03, 0x4d, 0x49, 0x7a, 0xcb, 0x52, 0x6f, 0xc7, 0x07, 0xee, 0xbb,
+ 0x8a, 0x95, 0x80, 0xe2, 0xa2, 0x35, 0x75, 0x97, 0x66, 0x42, 0x5e, 0xa5,
+ 0xae, 0x37, 0x71, 0x3c, 0x59, 0xf1, 0x5b, 0xe8, 0xa5, 0x7a, 0x89, 0x63,
+ 0xf8, 0x9b, 0x7b, 0x06, 0x6e, 0x3f, 0x46, 0x10, 0xe6, 0xb6, 0xaf, 0x7b,
+ 0x2e, 0x80, 0xd4, 0x8e, 0xbe, 0x44, 0x06, 0x02, 0x01, 0xb4, 0xd8, 0x6b,
+ 0xce, 0x83, 0xa2, 0xec, 0xa3, 0x16, 0xbe, 0x79, 0xd6, 0xdc, 0xd4, 0xc1,
+ 0xd4, 0x6d, 0xc4, 0xdb, 0x3f, 0x81, 0xb2, 0xe3, 0x02, 0xe8, 0xeb, 0x75,
+ 0x6e, 0x36, 0x00, 0xe7, 0xcc, 0x8b, 0x7d, 0x5d, 0xf1, 0xd7, 0xca, 0x2b,
+ 0xe5, 0x08, 0x59, 0xf2, 0x0f, 0x55, 0x98, 0xa3, 0xfd, 0xa1, 0x23, 0xb2,
+ 0x9f, 0x00, 0x52, 0x55, 0x68, 0x98, 0xea, 0xd7, 0xbd, 0x7b, 0x4f, 0xd1,
+ 0x02, 0xa9, 0xe4, 0x3f, 0x56, 0xbf, 0xb1, 0x44, 0x39, 0x4f, 0x27, 0xa9,
+ 0x59, 0x87, 0xc0, 0x26, 0xde, 0x60, 0xfa, 0xea, 0xbf, 0xff, 0xe5, 0x0f,
+ 0x14, 0xbf, 0xab, 0xfd, 0x25, 0xaa, 0x70, 0xd6, 0xf5, 0xac, 0xf1, 0x65,
+ 0xd3, 0x43, 0x4c, 0x98, 0x70, 0x16, 0xf1, 0x3c, 0x92, 0x9d, 0x7c, 0x37,
+ 0x77, 0xfa, 0x61, 0x95, 0x6d, 0x14, 0xb9, 0x24, 0x3f, 0xf8, 0x9e, 0x36,
+ 0x48, 0xdb, 0xab, 0x01, 0xac, 0x19, 0x07, 0x54, 0x80, 0x5c, 0x5f, 0xf4,
+ 0xde, 0xd8, 0x5b, 0x94, 0xd9, 0xe4, 0xc1, 0x50, 0x08, 0x8d, 0xa2, 0x93,
+ 0xd6, 0x8a, 0x7c, 0xad, 0xd8, 0x35, 0xcd, 0xba, 0x2f, 0xf1, 0xbf, 0x0f,
+ 0x42, 0xbe, 0xf4, 0x23, 0x09, 0x43, 0x08, 0xce, 0xea, 0x33, 0xa9, 0x1a,
+ 0x44, 0x77, 0xdf, 0x54, 0x35, 0xfe, 0xd8, 0xa8, 0x9f, 0x57, 0xda, 0xe2,
+ 0xd1, 0x5b, 0x0f, 0x8f, 0x1a, 0x2b, 0xa5, 0x32, 0x79, 0xfb, 0xc6, 0xef,
+ 0x7b, 0x17, 0x7f, 0xea, 0x1d, 0xfe, 0x6c, 0x29, 0xe8, 0x3b, 0xa8, 0x30,
+ 0xe1, 0xc8, 0x69, 0x02, 0x75, 0x3e, 0x7e, 0x1a, 0x81, 0xee, 0xb4, 0x32,
+ 0x14, 0x66, 0x5f, 0xfc, 0x74, 0xeb, 0x3e, 0xf9, 0xc8, 0x91, 0x27, 0x76,
+ 0xeb, 0x53, 0x1e, 0x03, 0xe3, 0x19, 0x3e, 0xb9, 0xca, 0xc2, 0xf8, 0xa8,
+ 0xb7, 0x1a, 0xc7, 0xc6, 0x24, 0xbf, 0xf1, 0x3b, 0x6a, 0xb0, 0x29, 0xc3,
+ 0x09, 0xca, 0x1a, 0x52, 0x1b, 0xa5, 0x1b, 0xfd, 0x24, 0xa6, 0xeb, 0xaf,
+ 0x05, 0x3e, 0xab, 0x66, 0x3a, 0xcd, 0x44, 0x58, 0x53, 0x6c, 0x72, 0xaf,
+ 0x5f, 0x34, 0x0c, 0x9f, 0xdd, 0x20, 0xd8, 0x5b, 0x66, 0x4e, 0x6a, 0x4d,
+ 0xab, 0x43, 0xcd, 0x13, 0x07, 0x73, 0xe8, 0x8d, 0xd1, 0x40, 0xb4, 0x47,
+ 0xee, 0x74, 0x7f, 0x68, 0xf1, 0xb3, 0x09, 0x1e, 0x20, 0x5e, 0x6a, 0xc8,
+ 0xa0, 0xaf, 0xea, 0xfe, 0xc5, 0xe4, 0xed, 0xf9, 0x26, 0xf5, 0xb7, 0x1a,
+ 0x0f, 0xee, 0xe5, 0x7c, 0xcd, 0x12, 0x38, 0x4b, 0x1e, 0xb0, 0x8a, 0xfd,
+ 0xf0, 0x68, 0xbb, 0x90, 0xae, 0xfc, 0x65, 0x9b, 0xcb, 0x7a, 0x51, 0x69,
+ 0x9b, 0xc8, 0xc5, 0x23, 0x9e, 0x96, 0x2f, 0x9e, 0xee, 0xb5, 0xca, 0xa9,
+ 0xaa, 0x73, 0xed, 0xa8, 0xa6, 0x15, 0x5d, 0x17, 0xf2, 0x90, 0xd8, 0x18,
+ 0x99, 0xd4, 0xf0, 0x7f, 0x03, 0x69, 0x68, 0x54, 0xc0, 0x58, 0x88, 0x64,
+ 0xea, 0x95, 0x66, 0x79, 0x85, 0x19, 0x95, 0xd4, 0x52, 0x76, 0x0f, 0x18,
+ 0xfc, 0x4f, 0x2f, 0x12, 0xc9, 0xa5, 0x57, 0x2c, 0x05, 0x2d, 0x40, 0xec,
+ 0xfd, 0xb4, 0xfe, 0x9e, 0x7c, 0xdd, 0x26, 0xa7, 0xc2, 0xfd, 0x94, 0xfb,
+ 0x71, 0xeb, 0xab, 0x10, 0xe6, 0xb0, 0xaa, 0xd4, 0xfd, 0x65, 0x11, 0xbf,
+ 0xb1, 0xa6, 0xb3, 0x7d, 0x98, 0xe4, 0x0e, 0x8f, 0x8d, 0x28, 0x39, 0xee,
+ 0x25, 0x2d, 0x00, 0x11, 0xc7, 0x58, 0x03, 0xd9, 0x87, 0xdf, 0x7b, 0xe0,
+ 0x7f, 0x88, 0x48, 0xa4, 0x82, 0xb4, 0xbd, 0x7a, 0x41, 0xe8, 0x63, 0x79,
+ 0x1a, 0x74, 0x89, 0x19, 0x1f, 0x24, 0x84, 0x46, 0xfe, 0x2f, 0x4e, 0x5b,
+ 0x53, 0x2f, 0xf5, 0x3a, 0x41, 0x70, 0x48, 0xc9, 0x13, 0xe7, 0x02, 0xac,
+ 0xc7, 0x9f, 0x79, 0x1e, 0x6a, 0xb9, 0x97, 0x43, 0x39, 0x39, 0xd3, 0xaf,
+ 0xaa, 0x5a, 0xf1, 0xc5, 0x17, 0x9d, 0x10, 0x2a, 0x68, 0x47, 0x26, 0xae,
+ 0x7c, 0x1f, 0x4d, 0x01, 0x93, 0xfa, 0x02, 0xc7, 0x02, 0xd8, 0x79, 0x0a,
+ 0x26, 0x01, 0xa7, 0x18, 0x09, 0x3b, 0xa4, 0x11, 0xba, 0x42, 0x40, 0xad,
+ 0x5f, 0x9b, 0x15, 0x43, 0xee, 0x58, 0x13, 0x7a, 0x42, 0x6c, 0xc5, 0xd4,
+ 0x37, 0xf7, 0x73, 0x43, 0xb4, 0xac, 0x9a, 0xd9, 0x1e, 0xbc, 0x90, 0xa5,
+ 0xbe, 0x5b, 0x16, 0xa9, 0x55, 0xe5, 0x6b, 0x9a, 0x80, 0xc5, 0x76, 0xc3,
+ 0x37, 0x3d, 0x8f, 0x47, 0x89, 0x70, 0xf2, 0x01, 0x30, 0x64, 0x77, 0x5a,
+ 0xfa, 0xbd, 0x93, 0x2f, 0x77, 0x01, 0x54, 0xb2, 0x84, 0x49, 0xff, 0xd8,
+ 0x53, 0x04, 0x98, 0xdd, 0x8d, 0x6f, 0xb3, 0x27, 0x9e, 0x6f, 0xe8, 0xce,
+ 0x3f, 0xd4, 0x3a, 0xf1, 0x38, 0x99, 0x9e, 0xef, 0x14, 0x54, 0x2d, 0xf8,
+ 0x92, 0x69, 0x2c, 0xeb, 0x3c, 0xaa, 0xb8, 0xf6, 0x18, 0xbb, 0x83, 0xc2,
+ 0xd4, 0xeb, 0x19, 0xca, 0x9e, 0xd7, 0x9e, 0xf5, 0xa2, 0xf3, 0x6c, 0x15,
+ 0x6c, 0xdb, 0x1b, 0x8a, 0xa0, 0xd1, 0x8c, 0x4d, 0x38, 0xae, 0xd0, 0xcb,
+ 0x25, 0xd4, 0x21, 0x95, 0xee, 0xeb, 0x44, 0x41, 0x73, 0x2b, 0xe1, 0x32,
+ 0x8b, 0x33, 0xd3, 0x10, 0xb7, 0x78, 0x33, 0x23, 0xa4, 0x03, 0x6c, 0x28,
+ 0xd6, 0x9d, 0x51, 0x11, 0xa9, 0xc4, 0xe5, 0x86, 0xaa, 0x35, 0x0d, 0x27,
+ 0xc4, 0x32, 0x44, 0xec, 0x8c, 0xb5, 0xa6, 0x25, 0xc5, 0x61, 0x0a, 0xd9,
+ 0x99, 0x8f, 0x6b, 0x7b, 0x17, 0xa3, 0xf7, 0x97, 0x51, 0xdc, 0x0d, 0x67,
+ 0xbd, 0xe9, 0xe7, 0x33, 0xae, 0x34, 0xed, 0x7c, 0x2f, 0xe5, 0xbd, 0x6a,
+ 0x79, 0x91, 0xdd, 0xe9, 0x9e, 0xdb, 0xfd, 0xc3, 0xf9, 0xe4, 0x7c, 0x8a,
+ 0x18, 0x39, 0xa3, 0x07, 0xa6, 0x50, 0x6d, 0xfe, 0xa9, 0xa2, 0xf7, 0x9d,
+ 0xef, 0xab, 0x03, 0x78, 0xab, 0xed, 0xee, 0x3d, 0x6d, 0x03, 0x7c, 0x45,
+ 0x34, 0x4f, 0xb0, 0x76, 0x18, 0xb8, 0x51, 0xe3, 0x13, 0x07, 0x5a, 0x5f,
+ 0xe6, 0xee, 0x4c, 0x0c, 0xc8, 0x77, 0x50, 0x97, 0x21, 0x3f, 0x08, 0x9a,
+ 0xe5, 0x55, 0xea, 0x0e, 0x10, 0xe2, 0xa9, 0x2f, 0x85, 0x7f, 0x71, 0x57,
+ 0x42, 0xda, 0x20, 0xc6, 0xde, 0xc5, 0x1f, 0x7e, 0x73, 0xb8, 0xf4, 0x8a,
+ 0x2a, 0x04, 0x70, 0x79, 0xef, 0xfc, 0xfd, 0xe9, 0x8a, 0x98, 0x24, 0x9f,
+ 0x8f, 0x22, 0xa9, 0xdd, 0x08, 0x45, 0x2e, 0x69, 0x5a, 0x57, 0xd3, 0xc9,
+ 0xd9, 0xce, 0xa2, 0xcc, 0x79, 0xae, 0x39, 0x22, 0xe4, 0x26, 0x2e, 0x81,
+ 0x8a, 0x1a, 0x11, 0x01, 0x87, 0x59, 0x39, 0x23, 0x5a, 0x5f, 0xb1, 0xa0,
+ 0xa5, 0xfc, 0x9a, 0x9c, 0x2e, 0xb0, 0x78, 0x86, 0xdb, 0x02, 0xb0, 0xf8,
+ 0xe7, 0x8c, 0xad, 0x05, 0xe5, 0xa7, 0x6f, 0x16, 0x40, 0xd5, 0x47, 0xe2,
+ 0xd4, 0x5a, 0x56, 0x13, 0x8b, 0xc3, 0xd1, 0xd1, 0xd8, 0x7c, 0x39, 0x64,
+ 0x6f, 0x20, 0x51, 0x3f, 0x13, 0xf3, 0x39, 0x57, 0x34, 0xe6, 0xac, 0x28,
+ 0xe7, 0x8d, 0xd4, 0x10, 0x5c, 0x7e, 0xee, 0x09, 0xa1, 0x65, 0x2d, 0x81,
+ 0xdc, 0xb3, 0x7e, 0xa2, 0xa6, 0x65, 0x3c, 0x19, 0x33, 0x08, 0x19, 0x17,
+ 0xb4, 0x12, 0x54, 0x36, 0xa8, 0x16, 0xe3, 0x2e, 0xd6, 0xcb, 0xa6, 0xf2,
+ 0x0f, 0x51, 0x99, 0x3d, 0x78, 0xce, 0x92, 0xca, 0x96, 0xc5, 0x1f, 0x89,
+ 0x58, 0x01, 0xed, 0x56, 0xa7, 0x33, 0x78, 0x35, 0xf9, 0x6e, 0xad, 0xff,
+ 0xee, 0x91, 0x43, 0x21, 0x0c, 0x65, 0x1f, 0xfb, 0x8d, 0x02, 0x41, 0xda,
+ 0x14, 0xf5, 0x59, 0xf8, 0x4c, 0x60, 0xa9, 0xa5, 0xa9, 0xd4, 0xc2, 0x38,
+ 0x8f, 0xce, 0xe0, 0x06, 0xa4, 0x9a, 0x02, 0xb8, 0x84, 0x3a, 0x03, 0xa1,
+ 0xab, 0xbd, 0x75, 0xb5, 0x23, 0x6f, 0xa5, 0x01, 0xfd, 0xba, 0x89, 0xbe,
+ 0xf2, 0xf7, 0x2a, 0x9e, 0xa8, 0x68, 0x18, 0xd4, 0xa0, 0x4b, 0xbd, 0x23,
+ 0x6c, 0xc2, 0xe5, 0x04, 0x9d, 0x10, 0x69, 0x14, 0xea, 0x83, 0xf1, 0x11,
+ 0x69, 0x08, 0x38, 0xfa, 0x98, 0x86, 0x59, 0x43, 0x37, 0x4c, 0xac, 0xad,
+ 0xf5, 0x22, 0x18, 0x8b, 0x8e, 0x1a, 0xaf, 0x2c, 0x70, 0x72, 0xec, 0x63,
+ 0x35, 0xc9, 0x2e, 0x92, 0xa7, 0x7b, 0x14, 0xb9, 0x03, 0x0c, 0xf4, 0x9d,
+ 0x09, 0xcd, 0x07, 0xf2, 0x94, 0x53, 0x08, 0x0c, 0xd9, 0x74, 0x35, 0xcf,
+ 0xab, 0xd9, 0xe9, 0xa2, 0x99, 0x77, 0x07, 0x3f, 0x3c, 0xf7, 0x48, 0xb5,
+ 0xf3, 0x7a, 0x07, 0xee, 0xfd, 0x51, 0x82, 0xba, 0x47, 0x91, 0x0d, 0x44,
+ 0xb3, 0xeb, 0x33, 0x15, 0x95, 0x62, 0x5e, 0xe1, 0x6e, 0x22, 0xe6, 0xad,
+ 0xa8, 0x43, 0xd7, 0xef, 0xba, 0xb3, 0x16, 0x73, 0xbe, 0x81, 0xfb, 0x25,
+ 0x10, 0x4d, 0x83, 0xfb, 0x13, 0x39, 0x7a, 0x46, 0x7d, 0x60, 0x54, 0x63,
+ 0xa3, 0x1a, 0xa3, 0x46, 0x78, 0xd3, 0xe9, 0xd7, 0x5a, 0x3f, 0xc6, 0xc5,
+ 0x4a, 0xce, 0xa5, 0x62, 0x93, 0x49, 0xde, 0xfc, 0x71, 0xf6, 0xe4, 0x6c,
+ 0x5f, 0x3d, 0x35, 0xea, 0x7d, 0xed, 0x25, 0x4a, 0xf1, 0x47, 0x61, 0xed,
+ 0x75, 0x6a, 0x0c, 0x04, 0x28, 0x2f, 0xca, 0xdf, 0x8b, 0x5a, 0x8c, 0x1b,
+ 0x42, 0xf0, 0xeb, 0x59, 0x18, 0x09, 0x82, 0x51, 0x1e, 0xc1, 0x6d, 0x46,
+ 0xe4, 0xe3, 0x32, 0x1d, 0xe8, 0xc5, 0x8d, 0x31, 0xd2, 0x15, 0x14, 0xdb,
+ 0x38, 0x76, 0xc6, 0xdc, 0xdb, 0xd1, 0x7e, 0x38, 0x41, 0xb6, 0x7a, 0x78,
+ 0x7d, 0xa9, 0xbf, 0xae, 0xe3, 0x99, 0x7c, 0x69, 0x59, 0xd1, 0xc6, 0x2b,
+ 0x3d, 0xf3, 0x9a, 0xc4, 0x35, 0x59, 0x5b, 0x45, 0x87, 0x2a, 0x14, 0xad,
+ 0x49, 0x81, 0x0f, 0x5a, 0x3d, 0x4c, 0xcc, 0x69, 0x7b, 0x95, 0x64, 0x32,
+ 0x24, 0xd0, 0xd4, 0xa0, 0xaa, 0xf2, 0x5a, 0x6b, 0x43, 0xc4, 0xf5, 0x85,
+ 0x64, 0x4e, 0xea, 0x03, 0x2e, 0x9b, 0x25, 0x8e, 0xf5, 0x4e, 0xed, 0xfb,
+ 0xa4, 0x00, 0x7d, 0x7a, 0x1f, 0x26, 0x88, 0x36, 0x22, 0xb2, 0x33, 0x14,
+ 0x47, 0xc0, 0xaa, 0x83, 0x69, 0xdf, 0x49, 0x69, 0x1d, 0x58, 0x5d, 0x1d,
+ 0x80, 0xc9, 0x5c, 0xfe, 0xfe, 0x58, 0x9a, 0x78, 0x26, 0xf6, 0xa0, 0x72,
+ 0xaa, 0x07, 0xd8, 0xbd, 0x80, 0x10, 0xb0, 0x7d, 0xa7, 0x17, 0x04, 0xde,
+ 0x13, 0x21, 0x17, 0x85, 0xca, 0xa3, 0x40, 0x92, 0xe4, 0x93, 0xd8, 0xb4,
+ 0x31, 0xb0, 0x56, 0x04, 0x2c, 0x35, 0xa6, 0xd9, 0xe3, 0x4f, 0x61, 0xdf,
+ 0x96, 0xff, 0xd7, 0x65, 0xf5, 0x92, 0x11, 0xf3, 0x81, 0x3e, 0x03, 0x2b,
+ 0xf1, 0x6a, 0xfc, 0x8b, 0x36, 0x0c, 0x4a, 0x4e, 0xa4, 0xf7, 0x4f, 0x28,
+ 0x34, 0xac, 0x4e, 0x5d, 0xb3, 0x86, 0x1c, 0xe7, 0x96, 0x80, 0xb6, 0x71,
+ 0x40, 0x6d, 0xd9, 0x38, 0xca, 0xb9, 0xd2, 0xbd, 0xe4, 0xc7, 0x71, 0xd3,
+ 0x22, 0x7a, 0x36, 0x8e, 0x75, 0xb5, 0x8c, 0xe6, 0x22, 0xfc, 0x91, 0x91,
+ 0xef, 0x3e, 0x91, 0x2e, 0x6c, 0x94, 0x8e, 0xf2, 0x9b, 0x5a, 0xf2, 0x45,
+ 0x63, 0x61, 0x8b, 0x70, 0x95, 0x7d, 0x5c, 0x2d, 0x50, 0xac, 0x1d, 0xbf,
+ 0x95, 0x26, 0x15, 0x91, 0xd0, 0x88, 0xe9, 0xb5, 0x54, 0xa4, 0x2e, 0x3f,
+ 0x8e, 0x7a, 0x1e, 0xc4, 0x1f, 0x33, 0x6b, 0xfc, 0x1f, 0x35, 0x58, 0xbf,
+ 0x5c, 0xae, 0xe6, 0x0b, 0x59, 0x7d, 0xb5, 0x73, 0x44, 0x29, 0x2d, 0x68,
+ 0x7e, 0xc9, 0x92, 0x1d, 0xa3, 0x66, 0xe6, 0xb3, 0x1e, 0x5e, 0x99, 0x09,
+ 0x21, 0xb8, 0xe9, 0x28, 0x7a, 0xf2, 0x2d, 0xb3, 0xf0, 0xae, 0x3d, 0xb0,
+ 0xe5, 0xb3, 0x85, 0x13, 0x75, 0xbe, 0xb7, 0x9f, 0xfb, 0xe6, 0x38, 0x9f,
+ 0xfa, 0x3f, 0x5c, 0x3e, 0x82, 0xf3, 0xc2, 0x63, 0xcb, 0xff, 0xee, 0x2f,
+ 0x2f, 0xbc, 0xe3, 0xe5, 0x57, 0x7e, 0xba, 0x20, 0xfd, 0x41, 0xbd, 0x3b,
+ 0x63, 0x0d, 0x2f, 0xfa, 0xbd, 0x38, 0xf0, 0x69, 0x69, 0x5d, 0x29, 0xc5,
+ 0xb3, 0xa1, 0xde, 0x52, 0xbe, 0x0c, 0x67, 0xe5, 0x05, 0x5d, 0xf2, 0x06,
+ 0x6a, 0xe0, 0x15, 0x40, 0x0f, 0x97, 0x51, 0xf5, 0x40, 0x49, 0x56, 0x27,
+ 0x8b, 0x81, 0xc6, 0x7b, 0x69, 0xf1, 0xf6, 0x77, 0x43, 0xb2, 0xc2, 0x6a,
+ 0x0c, 0x88, 0x2e, 0xc4, 0xc6, 0xad, 0x81, 0xad, 0x10, 0x64, 0xbc, 0xbc,
+ 0x7c, 0x69, 0xee, 0xa5, 0xb3, 0x2d, 0x63, 0x28, 0x87, 0x63, 0xe7, 0xb0,
+ 0x00, 0x6b, 0x32, 0xcf, 0x6f, 0x8f, 0x5b, 0x12, 0xf5, 0x79, 0x08, 0x36,
+ 0x88, 0x64, 0xad, 0xce, 0x39, 0x77, 0x4c, 0x79, 0x4d, 0x3b, 0xd8, 0xf3,
+ 0x06, 0xf0, 0xac, 0x54, 0x43, 0xe4, 0xd9, 0x30, 0x63, 0x01, 0x1b, 0x14,
+ 0x87, 0x8f, 0x06, 0x00, 0x67, 0xf1, 0x5a, 0x5d, 0xd0, 0xf6, 0x52, 0xfa,
+ 0xe5, 0x39, 0x56, 0xb8, 0xd6, 0x9d, 0xf5, 0x3e, 0x52, 0x88, 0xe2, 0xab,
+ 0xe0, 0x53, 0x0a, 0x4c, 0xd5, 0xcb, 0x1a, 0x63, 0xa2, 0xcc, 0x2b, 0x40,
+ 0x7f, 0xca, 0xc7, 0x6f, 0xff, 0xa8, 0x0e, 0x9f, 0x62, 0xe5, 0x22, 0x9f,
+ 0x1d, 0xa2, 0xa9, 0xeb, 0x3b, 0x5f, 0x98, 0x64, 0x14, 0x0b, 0x27, 0x25,
+ 0x14, 0x89, 0x6d, 0x2b, 0xff, 0xb9, 0xfc, 0xf6, 0x1f, 0xff, 0xd8, 0xe8,
+ 0x65, 0x31, 0xd8, 0x50, 0x5f, 0x47, 0xed, 0xf5, 0x9a, 0xd3, 0x62, 0xf5,
+ 0x8b, 0x92, 0x51, 0x81, 0x4a, 0xde, 0x6b, 0xc1, 0x85, 0xcc, 0x18, 0x42,
+ 0xbc, 0x0d, 0xe5, 0x25, 0x68, 0x41, 0x34, 0x13, 0x49, 0x59, 0x5d, 0x9c,
+ 0x29, 0x2c, 0x0d, 0xe0, 0xfb, 0x8d, 0x72, 0x09, 0xf5, 0x0f, 0x84, 0x1f,
+ 0x78, 0xe2, 0x4f, 0x6f, 0x68, 0x54, 0x43, 0xa3, 0xba, 0x5b, 0x4a, 0x1f,
+ 0x60, 0xda, 0x13, 0x6a, 0xd1, 0x4e, 0x5c, 0xff, 0x53, 0x37, 0xbb, 0x22,
+ 0x44, 0xeb, 0x66, 0x1c, 0xfc, 0x9a, 0xec, 0x6b, 0x8b, 0x52, 0x1a, 0x43,
+ 0x3c, 0x0e, 0x88, 0x09, 0x65, 0x5e, 0xc3, 0x33, 0xeb, 0x47, 0x27, 0x9f,
+ 0xb3, 0xaf, 0xd8, 0x3c, 0x0a, 0xda, 0x7a, 0xda, 0xbf, 0x4c, 0x4c, 0x5e,
+ 0xf8, 0x5d, 0x77, 0xcf, 0xa9, 0x67, 0xf8, 0x93, 0x22, 0x3f, 0xe2, 0xb4,
+ 0xfc, 0xb4, 0x1c, 0x70, 0x45, 0xdc, 0x14, 0xd9, 0x32, 0xe2, 0x81, 0x80,
+ 0xa2, 0x8c, 0xea, 0xb8, 0x85, 0xf2, 0xc0, 0xbc, 0xe5, 0x35, 0xc6, 0x05,
+ 0xcf, 0x2a, 0x3a, 0xcc, 0xfa, 0x9f, 0x17, 0xc2, 0x28, 0x91, 0xe2, 0xab,
+ 0xc6, 0x3c, 0x6d, 0xbc, 0x60, 0xff, 0xaa, 0x52, 0x01, 0x7a, 0x6f, 0x7e,
+ 0xbd, 0x75, 0xdf, 0x7e, 0x0d, 0xd1, 0x99, 0xa2, 0x1e, 0x26, 0x14, 0x61,
+ 0x77, 0x4c, 0x5c, 0x06, 0xd0, 0x8f, 0x43, 0x34, 0x18, 0xfb, 0xb7, 0xf4,
+ 0x46, 0x73, 0xc8, 0x2e, 0x2c, 0xc4, 0x46, 0x28, 0x6d, 0x0d, 0xec, 0x7b,
+ 0x3e, 0x6e, 0xff, 0x16, 0x27, 0xc9, 0x14, 0xa8, 0x7a, 0x1a, 0x1d, 0x94,
+ 0xe4, 0x3b, 0xfc, 0x94, 0x16, 0xe0, 0x0c, 0xef, 0x8e, 0x8a, 0x15, 0x80,
+ 0x19, 0xfb, 0x2a, 0x21, 0xca, 0xbe, 0x0e, 0xb2, 0xd8, 0xbb, 0xfa, 0xaa,
+ 0xd5, 0x5e, 0x39, 0xb3, 0xda, 0xab, 0x9e, 0x24, 0x36, 0x49, 0x8c, 0x4b,
+ 0xf7, 0x25, 0x56, 0x2c, 0x29, 0xab, 0x99, 0x36, 0x90, 0xa5, 0xcf, 0x5e,
+ 0xa4, 0x4b, 0xe5, 0xfb, 0xc8, 0xb7, 0xad, 0x15, 0x49, 0xcb, 0xaa, 0xca,
+ 0x8d, 0x80, 0x21, 0x2e, 0x72, 0x96, 0xc0, 0xb5, 0xde, 0x2b, 0xf5, 0x40,
+ 0x18, 0x46, 0xb0, 0x3b, 0x34, 0x5e, 0x67, 0xeb, 0x6e, 0xe6, 0x0d, 0xfd,
+ 0x9c, 0x34, 0x71, 0xa1, 0xfc, 0x43, 0x5d, 0x07, 0xbc, 0x96, 0xac, 0xb3,
+ 0xfb, 0x79, 0x8f, 0xbb, 0xe2, 0x0e, 0x35, 0x29, 0xd5, 0x0b, 0xf0, 0x25,
+ 0xf0, 0xe9, 0x2b, 0x9e, 0xab, 0xa1, 0x57, 0x09, 0x70, 0x88, 0xb0, 0xf5,
+ 0x10, 0x0c, 0x8f, 0xf1, 0x1f, 0x14, 0xa8, 0x55, 0x5e, 0xdd, 0xc2, 0xa4,
+ 0xba, 0xbc, 0xfa, 0x24, 0x6e, 0x1f, 0xe8, 0x34, 0x9a, 0xa7, 0x4b, 0x2c,
+ 0x04, 0x71, 0xc9, 0x43, 0x64, 0xd1, 0x11, 0xcf, 0xd1, 0x50, 0x0b, 0xf7,
+ 0xc9, 0xa9, 0x85, 0xb1, 0x0b, 0xce, 0x72, 0x72, 0xf2, 0xaa, 0xcd, 0xae,
+ 0xc9, 0x6b, 0x33, 0x5f, 0x4a, 0xa6, 0x07, 0xcd, 0x4b, 0xfb, 0x2f, 0x12,
+ 0x27, 0xf9, 0xee, 0xf3, 0x5e, 0x60, 0x2f, 0x2f, 0x8f, 0x6d, 0x56, 0xcb,
+ 0x12, 0x7f, 0xdf, 0xfd, 0x6c, 0x25, 0xb4, 0x18, 0x82, 0x2e, 0x91, 0x4b,
+ 0xa9, 0xc2, 0x50, 0x71, 0x1e, 0xbf, 0x4a, 0xdc, 0x6f, 0x1f, 0x15, 0x6f,
+ 0x0b, 0xdf, 0x91, 0x5e, 0x7e, 0xf1, 0xf8, 0x1f, 0x5f, 0xd8, 0x43, 0x2b,
+ 0x18, 0xb7, 0xd4, 0x48, 0x5f, 0xb7, 0x8c, 0x7d, 0xa8, 0xd0, 0x21, 0x18,
+ 0xd8, 0x32, 0xd7, 0xb9, 0xf3, 0x56, 0x4b, 0x54, 0xc2, 0x36, 0x8b, 0xe1,
+ 0xa5, 0x23, 0x4e, 0xdc, 0x8d, 0x32, 0x03, 0x31, 0x78, 0x99, 0x64, 0x32,
+ 0x44, 0x1d, 0x8e, 0xa0, 0xce, 0x0b, 0x51, 0x2f, 0xb6, 0x64, 0xe6, 0x04,
+ 0xd8, 0x32, 0xb9, 0x17, 0xff, 0x9d, 0x87, 0x3f, 0x41, 0x32, 0x66, 0x94,
+ 0x91, 0xf2, 0x60, 0x82, 0x19, 0x31, 0x5f, 0x80, 0x8d, 0x54, 0xe0, 0x98,
+ 0x07, 0x03, 0x81, 0x93, 0xe1, 0x67, 0x17, 0x1b, 0x58, 0x62, 0x57, 0x77,
+ 0xa2, 0x4b, 0x2a, 0x7a, 0x77, 0x59, 0x1f, 0x85, 0xdb, 0x3b, 0xcf, 0x2f,
+ 0x7a, 0x7c, 0xce, 0xbf, 0xa6, 0x77, 0x9c, 0x89, 0x83, 0xeb, 0xd9, 0xb9,
+ 0x21, 0xd7, 0x02, 0x6c, 0x78, 0x12, 0x0f, 0x84, 0x29, 0x26, 0xee, 0x5e,
+ 0xb3, 0xd0, 0xa4, 0x48, 0x82, 0x84, 0x4f, 0x1d, 0x77, 0x8d, 0xca, 0x77,
+ 0x7a, 0x10, 0x38, 0x78, 0x0f, 0xde, 0xd9, 0x8d, 0x8b, 0xeb, 0x04, 0xf4,
+ 0x53, 0xff, 0x0b, 0xae, 0x3f, 0x02, 0x16, 0x89, 0x09, 0xa2, 0x20, 0x04,
+ 0x90, 0xf6, 0x72, 0x66, 0xd2, 0x52, 0x50, 0x40, 0x6d, 0x77, 0x7d, 0x7c,
+ 0xf7, 0x38, 0xa6, 0x8b, 0xc4, 0x52, 0xa2, 0xba, 0x33, 0x76, 0x75, 0xc1,
+ 0xf2, 0x26, 0xf1, 0x87, 0x92, 0xa0, 0xc7, 0xa4, 0x80, 0xf8, 0x4e, 0xc7,
+ 0x9d, 0x42, 0xaf, 0x6e, 0xce, 0x38, 0x9d, 0xa5, 0x73, 0xf1, 0x3e, 0x75,
+ 0x26, 0xac, 0x33, 0xee, 0x5a, 0x51, 0xf9, 0x1e, 0x56, 0xcb, 0x6d, 0x1e,
+ 0x3f, 0x1a, 0x94, 0xe9, 0x0d, 0x14, 0x7b, 0xc0, 0x4a, 0xfa, 0xf9, 0x36,
+ 0x98, 0xdb, 0x64, 0xc1, 0x11, 0x33, 0x2d, 0x4f, 0x29, 0xa0, 0xff, 0x56,
+ 0xef, 0x40, 0xba, 0xf0, 0x75, 0xf0, 0xdd, 0x9c, 0x89, 0x02, 0x1b, 0xa5,
+ 0xb0, 0xdf, 0x6f, 0xae, 0x45, 0xca, 0xbf, 0x2e, 0x63, 0xd7, 0x75, 0x11,
+ 0x83, 0x3c, 0x21, 0x5d, 0xc6, 0xc8, 0x88, 0x60, 0xf8, 0x62, 0x11, 0xf5,
+ 0x49, 0xdc, 0x01, 0xa7, 0x04, 0x60, 0xed, 0xac, 0x4b, 0x04, 0x45, 0x80,
+ 0xa8, 0xf3, 0x7a, 0xc1, 0x65, 0x71, 0x76, 0x54, 0x6a, 0x4b, 0x7d, 0xef,
+ 0xb8, 0x05, 0xd6, 0x2a, 0x5d, 0x9e, 0x8a, 0x8c, 0x82, 0xe6, 0x71, 0x89,
+ 0x07, 0x6c, 0xcf, 0xf7, 0xf5, 0xb8, 0xfc, 0xfc, 0x83, 0x11, 0xdd, 0xc3,
+ 0x60, 0x75, 0x8f, 0xb4, 0x2a, 0xdf, 0xcc, 0xe5, 0x3d, 0x89, 0xce, 0x8d,
+ 0x90, 0x6e, 0xed, 0x36, 0x97, 0xf5, 0x44, 0x20, 0xcf, 0xc4, 0xc2, 0x1f,
+ 0xbc, 0xf4, 0x7a, 0x02, 0x6b, 0xa0, 0x9c, 0xad, 0xde, 0xf7, 0x4e, 0xda,
+ 0x6b, 0xd9, 0x5e, 0x74, 0xb2, 0x2c, 0xae, 0x89, 0x74, 0x5d, 0x3e, 0x40,
+ 0x41, 0x8d, 0x4b, 0x38, 0xba, 0x69, 0x1e, 0xe6, 0x42, 0x04, 0x6c, 0xa4,
+ 0x24, 0x46, 0xc8, 0x8e, 0xa3, 0xfb, 0x11, 0x11, 0x55, 0x0b, 0xb6, 0xcf,
+ 0xde, 0xb6, 0x8a, 0x99, 0x92, 0xb0, 0x2f, 0x7d, 0x5d, 0xac, 0x9b, 0xea,
+ 0x41, 0x03, 0x79, 0x5b, 0x95, 0x37, 0xeb, 0x9c, 0xae, 0x8c, 0xe8, 0xee,
+ 0x8b, 0x75, 0xf0, 0xd9, 0x92, 0xfd, 0x94, 0xb9, 0x4a, 0x88, 0x95, 0x9b,
+ 0x41, 0xdf, 0xe5, 0x86, 0x51, 0xcf, 0x7b, 0xa9, 0xe6, 0xad, 0x69, 0xf2,
+ 0x27, 0x96, 0x80, 0xc7, 0xcb, 0x92, 0xb3, 0xb4, 0x9e, 0xca, 0xd6, 0xb7,
+ 0xc8, 0x49, 0x49, 0x89, 0x5c, 0xd3, 0xa5, 0xf1, 0xf7, 0x56, 0x35, 0x59,
+ 0x40, 0x1f, 0xa7, 0x9e, 0x6c, 0x2e, 0x6d, 0xfe, 0x99, 0x7e, 0xc9, 0xb9,
+ 0xe3, 0xf4, 0xb8, 0xa5, 0x20, 0xe2, 0xec, 0x50, 0x67, 0x48, 0x50, 0xfc,
+ 0x7f, 0x7d, 0xbf, 0x26, 0x88, 0x2f, 0x35, 0xd3, 0xea, 0xc1, 0x94, 0x05,
+ 0xd7, 0x7f, 0x8c, 0x01, 0x1d, 0x98, 0x00, 0x39, 0x5f, 0x85, 0x1c, 0x6c,
+ 0x78, 0xf1, 0xe8, 0xa6, 0xca, 0xa2, 0x6d, 0x0e, 0x05, 0x53, 0x9e, 0xf1,
+ 0x1a, 0xee, 0xed, 0x97, 0x1d, 0x3b, 0xc5, 0x26, 0xf7, 0x62, 0x40, 0xa5,
+ 0x5d, 0xba, 0x18, 0x56, 0xcd, 0x8f, 0x7f, 0xd8, 0xa9, 0x4e, 0x9b, 0xbb,
+ 0x97, 0xbe, 0xfe, 0x4a, 0xe7, 0x65, 0x07, 0xa1, 0x49, 0xd6, 0xf7, 0x9c,
+ 0xf8, 0x8e, 0xd7, 0x4f, 0x6d, 0xf7, 0xe2, 0x81, 0x51, 0x37, 0x72, 0x61,
+ 0x2c, 0xae, 0x92, 0x34, 0x56, 0x5a, 0x39, 0xa8, 0xb9, 0x71, 0x83, 0xec,
+ 0x10, 0xfd, 0x66, 0x46, 0xed, 0xbc, 0xcc, 0x3e, 0xac, 0xb2, 0x15, 0xd0,
+ 0xd6, 0x77, 0x17, 0x83, 0x11, 0x50, 0x0b, 0xdc, 0xdf, 0xa6, 0x56, 0x85,
+ 0x11, 0x2e, 0xe6, 0x87, 0x6b, 0x57, 0x77, 0x1a, 0xae, 0x2e, 0x9b, 0x69,
+ 0xc2, 0x16, 0x40, 0x0c, 0x3c, 0xba, 0xd8, 0xe7, 0xe2, 0xdc, 0x7d, 0x05,
+ 0x09, 0x39, 0x97, 0xea, 0xe6, 0xe5, 0x3a, 0xac, 0x26, 0x82, 0x8a, 0x16,
+ 0xba, 0xa3, 0xf4, 0xae, 0xee, 0xa5, 0x0a, 0x39, 0xa3, 0x70, 0xc7, 0xde,
+ 0xf7, 0x61, 0xd2, 0x1c, 0x28, 0xe1, 0x34, 0x5f, 0xf3, 0xf7, 0xce, 0xf9,
+ 0xde, 0xb4, 0xfd, 0xba, 0x71, 0xd9, 0x69, 0xd3, 0x92, 0x35, 0x1c, 0xb9,
+ 0x91, 0x20, 0x34, 0x85, 0x2b, 0x49, 0xc5, 0x80, 0xeb, 0x9f, 0x61, 0x5e,
+ 0xd6, 0x8c, 0xa0, 0x83, 0xe1, 0x61, 0x8e, 0x61, 0xd2, 0x64, 0x41, 0x88,
+ 0xf5, 0xf6, 0x51, 0x22, 0x3b, 0x1d, 0x2c, 0x7d, 0x66, 0x26, 0x4f, 0x5e,
+ 0xe3, 0x78, 0xbc, 0x4b, 0x00, 0x00, 0x96, 0x6e, 0x01, 0xab, 0x0a, 0xf3,
+ 0xdc, 0xc9, 0x97, 0xa0, 0xf6, 0x30, 0x47, 0x56, 0x66, 0x79, 0xa2, 0xb2,
+ 0x1d, 0x98, 0x98, 0x46, 0xbb, 0x78, 0x73, 0x55, 0xe9, 0x6c, 0xbe, 0xcf,
+ 0xf1, 0x08, 0x47, 0x98, 0xa8, 0xdc, 0x9f, 0xbe, 0x9a, 0x43, 0x6b, 0x32,
+ 0x73, 0xa9, 0xc2, 0x4e, 0xa5, 0xfa, 0xe1, 0xe6, 0xd7, 0x4e, 0xf5, 0xff,
+ 0xf3, 0x69, 0x8b, 0x2c, 0x7e, 0x53, 0x19, 0x5b, 0xb0, 0xb5, 0x0e, 0xb2,
+ 0x0e, 0x21, 0x49, 0xac, 0xfc, 0x27, 0x5d, 0x32, 0x73, 0x36, 0x3d, 0x26,
+ 0x50, 0x27, 0x6c, 0xbc, 0x24, 0xb9, 0xf7, 0x50, 0x3a, 0xfb, 0x84, 0x2c,
+ 0xd2, 0x25, 0x61, 0x4b, 0x89, 0xef, 0xd2, 0x17, 0x2f, 0xff, 0x20, 0xba,
+ 0xa4, 0x5d, 0xb5, 0x94, 0xf3, 0xe0, 0x29, 0x64, 0x70, 0xfe, 0x81, 0xa1,
+ 0x72, 0x93, 0x95, 0x6b, 0x55, 0x0f, 0x8b, 0x18, 0xca, 0x85, 0xe6, 0x2c,
+ 0xb8, 0x90, 0x42, 0xbc, 0x5e, 0x5a, 0x9c, 0xd3, 0x30, 0x33, 0x27, 0xa3,
+ 0x9c, 0xb8, 0x69, 0x36, 0xca, 0xaa, 0xbe, 0x24, 0x54, 0x92, 0x5a, 0x5d,
+ 0x41, 0xcc, 0x27, 0xf9, 0x99, 0x4f, 0x10, 0x0b, 0xb7, 0xf3, 0x06, 0xd7,
+ 0x9d, 0x73, 0x22, 0x57, 0xd8, 0x3a, 0x73, 0x22, 0x77, 0x38, 0x00, 0x0e,
+ 0xd4, 0xad, 0x95, 0xac, 0x80, 0xf1, 0xae, 0xf5, 0x4a, 0xff, 0xd7, 0x57,
+ 0x34, 0xd3, 0xd2, 0xb0, 0xee, 0x61, 0xb1, 0x76, 0x17, 0x7e, 0xc1, 0xdd,
+ 0xdc, 0xab, 0x7c, 0x10, 0x95, 0x33, 0xec, 0x73, 0x01, 0x91, 0x51, 0xd9,
+ 0x6d, 0x6e, 0x2b, 0x29, 0x70, 0x41, 0xd8, 0xe7, 0xbe, 0x86, 0x91, 0x13,
+ 0x57, 0x9c, 0x91, 0x88, 0x83, 0x7a, 0xe7, 0xff, 0x1c, 0x8e, 0xbc, 0x04,
+ 0x1a, 0x89, 0xca, 0x29, 0x37, 0x96, 0x4d, 0xb7, 0xf1, 0x5a, 0x2d, 0x38,
+ 0xe2, 0x38, 0x83, 0xff, 0xe9, 0xa1, 0x89, 0xd5, 0xfc, 0xc5, 0xf2, 0xa9,
+ 0x1b, 0x3d, 0x82, 0x39, 0x90, 0xe2, 0x17, 0xfd, 0xf6, 0x4b, 0x22, 0x32,
+ 0x6e, 0xa2, 0x78, 0x46, 0x50, 0xaf, 0xb5, 0x75, 0x8a, 0x6b, 0x28, 0x11,
+ 0xd4, 0x49, 0x6c, 0x3d, 0x56, 0x4e, 0x2d, 0x2b, 0x56, 0x75, 0x41, 0xbf,
+ 0x2d, 0x15, 0x0d, 0xbb, 0x6c, 0x7b, 0x3d, 0xa6, 0xf5, 0xbe, 0x92, 0xeb,
+ 0x32, 0xaa, 0x92, 0x0d, 0x72, 0xd4, 0xa5, 0x8d, 0x83, 0xfb, 0x25, 0xc9,
+ 0xcc, 0x53, 0xb0, 0x78, 0xea, 0x73, 0x04, 0xd1, 0x06, 0x05, 0x16, 0x60,
+ 0xdb, 0xec, 0x49, 0x7a, 0x82, 0x2a, 0xc7, 0x51, 0xa4, 0x17, 0xf8, 0x41,
+ 0x91, 0xb2, 0xed, 0x70, 0xbe, 0x00, 0x73, 0xe4, 0xb0, 0x35, 0x8d, 0xbc,
+ 0xe4, 0x06, 0xa1, 0xcd, 0x29, 0xe8, 0x16, 0xaa, 0x34, 0xcf, 0xaa, 0xe0,
+ 0xc8, 0x75, 0x8b, 0x0d, 0x5e, 0x78, 0x10, 0x03, 0x9f, 0xf8, 0x98, 0xbe,
+ 0x52, 0x88, 0x60, 0x22, 0xf7, 0xc5, 0x11, 0x1e, 0xc4, 0xf1, 0x81, 0x1d,
+ 0x6b, 0x38, 0xb3, 0xf4, 0xad, 0x8f, 0x2a, 0xc9, 0x58, 0x97, 0x4c, 0x7b,
+ 0xea, 0x6f, 0x49, 0x04, 0x1e, 0xcd, 0xc1, 0x81, 0x73, 0x24, 0xe5, 0xf1,
+ 0x78, 0x27, 0x13, 0x01, 0x87, 0xc3, 0x45, 0xa1, 0x6c, 0x80, 0xbd, 0x5b,
+ 0x94, 0x8f, 0x5a, 0x69, 0x01, 0x02, 0xb6, 0xbf, 0x9c, 0x63, 0xa3, 0x3d,
+ 0x57, 0xb0, 0x3c, 0x6c, 0x09, 0x96, 0x73, 0x8f, 0x60, 0xc2, 0x98, 0xbb,
+ 0x34, 0x34, 0x7e, 0x22, 0xd4, 0xf5, 0xc9, 0x55, 0x48, 0x2b, 0x84, 0x4c,
+ 0xb9, 0x1f, 0x9e, 0x82, 0x9b, 0xaf, 0xfc, 0x73, 0xaa, 0x6a, 0x0f, 0x21,
+ 0xa3, 0xfe, 0xa8, 0x37, 0x52, 0xea, 0xea, 0x98, 0x0a, 0x09, 0x33, 0x34,
+ 0x10, 0xad, 0xbf, 0xe6, 0x38, 0xc1, 0xf0, 0xb3, 0xfd, 0xe7, 0xad, 0x8f,
+ 0xd5, 0x46, 0x19, 0xaf, 0xfc, 0xd9, 0x02, 0x73, 0xfe, 0xae, 0x7c, 0x4f,
+ 0xb5, 0xb0, 0x4d, 0x47, 0x83, 0xa0, 0xff, 0xb4, 0x02, 0xd2, 0x3c, 0x5e,
+ 0x64, 0x36, 0x5a, 0x75, 0x39, 0x56, 0xc3, 0x7b, 0x5c, 0x71, 0xed, 0xe4,
+ 0x05, 0x4e, 0xd5, 0x18, 0xe0, 0x03, 0xa2, 0x37, 0xe3, 0xa7, 0x00, 0xef,
+ 0xd0, 0x8a, 0x34, 0x6f, 0x24, 0x56, 0xdf, 0xa5, 0xc5, 0xde, 0xff, 0x90,
+ 0x05, 0x08, 0xfa, 0xd4, 0xf4, 0xb8, 0x06, 0xd0, 0x24, 0x48, 0x65, 0x39,
+ 0xe3, 0x00, 0xb5, 0x44, 0x20, 0xeb, 0xc1, 0xb9, 0x71, 0x05, 0x75, 0xb7,
+ 0xf6, 0x52, 0x87, 0x6c, 0xc0, 0xf1, 0x5f, 0x13, 0x69, 0xca, 0x85, 0xe7,
+ 0x59, 0x0b, 0xac, 0x1e, 0xa0, 0xb1, 0x4a, 0x6b, 0xfb, 0xab, 0xf7, 0x47,
+ 0x8f, 0x9a, 0xfb, 0x77, 0x58, 0x7a, 0x99, 0x0a, 0x17, 0xa3, 0x15, 0xc9,
+ 0x68, 0x19, 0x2b, 0xef, 0xee, 0xeb, 0xda, 0xbf, 0x57, 0xfe, 0x94, 0x88,
+ 0xdc, 0x34, 0x5d, 0xde, 0x96, 0x93, 0x6a, 0x7b, 0x74, 0xbb, 0xff, 0xe8,
+ 0x6f, 0x39, 0x1a, 0x01, 0xd3, 0x1f, 0xb3, 0xe7, 0x28, 0x7d, 0x0a, 0x6f,
+ 0x2a, 0x04, 0xa6, 0xa9, 0x88, 0xb8, 0xa9, 0x0a, 0x12, 0xd0, 0xdd, 0xb3,
+ 0x96, 0x67, 0xfe, 0x3e, 0xef, 0x6c, 0xae, 0xab, 0xc4, 0x30, 0x29, 0x62,
+ 0x0c, 0x17, 0x88, 0x2a, 0x87, 0xd3, 0x51, 0x39, 0xeb, 0x5c, 0x68, 0xaf,
+ 0x03, 0x47, 0xe8, 0xfd, 0xf7, 0x0d, 0x61, 0x54, 0x24, 0x21, 0x64, 0xd7,
+ 0xcc, 0xc1, 0x6b, 0x3f, 0xf2, 0x62, 0x0f, 0x8b, 0xce, 0x12, 0x34, 0x49,
+ 0x0a, 0xf8, 0x1d, 0x6b, 0xe2, 0x25, 0xd0, 0x23, 0x30, 0x63, 0x35, 0xb4,
+ 0xc3, 0xae, 0x7b, 0x2b, 0xb5, 0xe8, 0xe3, 0x85, 0xa9, 0x54, 0x32, 0xdf,
+ 0xce, 0x8b, 0xac, 0x01, 0xaa, 0x8e, 0x82, 0x6e, 0x30, 0xc5, 0x4b, 0x56,
+ 0xed, 0xea, 0x1d, 0xf5, 0xc5, 0x76, 0x56, 0x63, 0x99, 0x37, 0xa9, 0x21,
+ 0x8b, 0xea, 0x18, 0xfd, 0xd1, 0xa7, 0xc7, 0x27, 0x66, 0x6e, 0x7a, 0x86,
+ 0x70, 0xd1, 0xcd, 0x06, 0x80, 0xa6, 0xa0, 0x95, 0xd3, 0x59, 0x21, 0x58,
+ 0xc1, 0x03, 0x3c, 0x33, 0x07, 0xcf, 0xe4, 0x3a, 0x3c, 0x69, 0x20, 0x35,
+ 0x58, 0x56, 0x10, 0x94, 0xff, 0x4f, 0x92, 0xe4, 0xd0, 0xcc, 0x04, 0x72,
+ 0x3c, 0x35, 0x81, 0x5d, 0x31, 0x29, 0x99, 0x4c, 0x52, 0x19, 0x77, 0xa3,
+ 0x38, 0x2f, 0x58, 0x6e, 0x02, 0xc7, 0x2e, 0xd0, 0x7b, 0x14, 0x76, 0x94,
+ 0x8c, 0x1e, 0xeb, 0xee, 0xb2, 0x4c, 0x01, 0x50, 0x59, 0xcd, 0x2c, 0x33,
+ 0x6b, 0x46, 0xf5, 0xa6, 0x2e, 0xa9, 0x68, 0x2b, 0x4e, 0x9d, 0x7d, 0x19,
+ 0xdb, 0xa0, 0x60, 0xf1, 0xd4, 0x10, 0xc8, 0xa7, 0x18, 0x63, 0x67, 0x26,
+ 0xf6, 0x31, 0x18, 0x9b, 0xf3, 0x03, 0xec, 0xab, 0x98, 0x2c, 0xed, 0x87,
+ 0x19, 0x0f, 0x38, 0x85, 0x0e, 0x79, 0xd4, 0xe7, 0xcf, 0x39, 0x32, 0xdf,
+ 0xee, 0xdd, 0xc7, 0xa0, 0x18, 0x78, 0x4a, 0x6e, 0x93, 0x21, 0xc3, 0x26,
+ 0x12, 0x0c, 0x6c, 0xe6, 0x16, 0x46, 0x85, 0x7f, 0x76, 0xca, 0x53, 0x8d,
+ 0xa3, 0xae, 0xa3, 0xd0, 0x31, 0x55, 0x10, 0x32, 0xfe, 0xdf, 0x6d, 0x2b,
+ 0xf6, 0xf7, 0xba, 0x07, 0x5f, 0xca, 0xf8, 0x95, 0x02, 0xd6, 0xad, 0xc1,
+ 0x9e, 0x7c, 0xb5, 0xba, 0x1a, 0xfe, 0x64, 0xd1, 0x6a, 0x35, 0x96, 0x43,
+ 0x61, 0x5d, 0x3d, 0x5a, 0x20, 0x4b, 0x4a, 0x11, 0x39, 0xf5, 0xb3, 0xd1,
+ 0x81, 0x5f, 0xd0, 0x59, 0xef, 0x10, 0x88, 0x2d, 0x24, 0xe1, 0x2c, 0xd5,
+ 0x74, 0x5e, 0xf7, 0x6b, 0xd3, 0x5a, 0x16, 0xb8, 0x23, 0x72, 0x4a, 0x8e,
+ 0x0b, 0xcf, 0x56, 0xc5, 0x41, 0x66, 0x16, 0xb7, 0x90, 0x32, 0xd1, 0xb8,
+ 0x6e, 0x8f, 0xc6, 0x5b, 0xa4, 0x2d, 0x4a, 0x62, 0xac, 0xb4, 0x7d, 0x87,
+ 0x0d, 0xc4, 0x4d, 0x4e, 0x56, 0x9f, 0x78, 0x92, 0x41, 0xf4, 0xd0, 0x9d,
+ 0xf1, 0x5d, 0x73, 0x38, 0xd9, 0xe1, 0xaf, 0xc4, 0xe4, 0x5d, 0x21, 0x6a,
+ 0x02, 0x38, 0x7d, 0x28, 0xd4, 0x9a, 0xea, 0xc2, 0x8c, 0x70, 0x53, 0x37,
+ 0x85, 0x97, 0x6f, 0xaa, 0xc3, 0x28, 0x0d, 0xab, 0x4d, 0x02, 0x4b, 0x9d,
+ 0x1c, 0xca, 0x11, 0x47, 0xf2, 0xf1, 0x97, 0x0f, 0x1a, 0xa9, 0x18, 0xc3,
+ 0x2e, 0x97, 0xd7, 0xff, 0xe4, 0x28, 0x3a, 0x7c, 0x47, 0x5b, 0xde, 0x35,
+ 0x01, 0x5f, 0xbe, 0x42, 0x25, 0xfb, 0x72, 0x89, 0x19, 0xfe, 0x52, 0x27,
+ 0xe9, 0xaf, 0x9e, 0x53, 0x36, 0x7c, 0x0f, 0xe5, 0x66, 0xc9, 0x8a, 0xb8,
+ 0xcb, 0xd3, 0x6f, 0x3d, 0xe7, 0x61, 0xf7, 0x80, 0x93, 0x8f, 0xac, 0x8f,
+ 0x8f, 0xf8, 0xce, 0x80, 0xe7, 0x20, 0x3b, 0xc7, 0x00, 0x9e, 0x20, 0x42,
+ 0x55, 0xd6, 0x0f, 0x87, 0xea, 0xb8, 0xe3, 0xa9, 0x4b, 0xa0, 0xe0, 0xff,
+ 0x64, 0xaa, 0x8d, 0x14, 0xf1, 0xc6, 0x73, 0x31, 0x88, 0x0c, 0x41, 0xb2,
+ 0xa7, 0x51, 0xc3, 0x26, 0x39, 0xc7, 0x87, 0xce, 0xc6, 0x76, 0xc8, 0x10,
+ 0xbd, 0xc2, 0xc1, 0x42, 0xdb, 0x7b, 0x1b, 0x21, 0x03, 0x3b, 0xab, 0x2a,
+ 0xd5, 0xf7, 0x2c, 0x9f, 0x23, 0x00, 0xf4, 0xd2, 0xd2, 0xdc, 0x0b, 0xcf,
+ 0xad, 0xa9, 0xd5, 0x04, 0xc2, 0xbc, 0xef, 0xc3, 0xf1, 0xc1, 0xe1, 0xf9,
+ 0x12, 0xf0, 0x30, 0x3a, 0x37, 0x62, 0x14, 0x70, 0x66, 0x5d, 0x59, 0x98,
+ 0x86, 0x61, 0xb6, 0xee, 0xcf, 0x32, 0xf8, 0x47, 0x27, 0x36, 0xc8, 0xe3,
+ 0x4b, 0xbd, 0x44, 0xdd, 0x6b, 0x9f, 0x96, 0x9e, 0xe5, 0xc7, 0x52, 0x3c,
+ 0x95, 0x20, 0x0c, 0xf0, 0xc6, 0x21, 0x82, 0xa8, 0x16, 0x80, 0x67, 0x31,
+ 0x33, 0xd8, 0x23, 0x7b, 0xe7, 0x87, 0x7b, 0xcf, 0x04, 0x63, 0xc1, 0xd1,
+ 0xfc, 0x5d, 0xf6, 0xb1, 0x32, 0x0c, 0x42, 0xc5, 0xa1, 0x12, 0x93, 0xd1,
+ 0x32, 0x2d, 0xaf, 0x23, 0x9c, 0x68, 0x5c, 0xfe, 0xb0, 0x00, 0x06, 0x21,
+ 0xa0, 0xd7, 0x92, 0x4e, 0xf7, 0x08, 0x27, 0x26, 0x17, 0x0e, 0x05, 0x8b,
+ 0x9c, 0x62, 0x64, 0x19, 0xc3, 0xdc, 0xa9, 0x2f, 0x32, 0xf0, 0x91, 0xd0,
+ 0x86, 0xd6, 0x42, 0xc8, 0x7d, 0x9d, 0x44, 0xc5, 0x1b, 0x93, 0xb3, 0xb6,
+ 0x7d, 0x10, 0x3b, 0xab, 0x6f, 0x02, 0x4c, 0xe5, 0xf5, 0x37, 0xb4, 0x0c,
+ 0x8d, 0xec, 0x72, 0x64, 0x22, 0x5d, 0x77, 0x54, 0x0a, 0x62, 0xb4, 0x8d,
+ 0xae, 0x85, 0xb4, 0x6e, 0x72, 0x0f, 0xdc, 0x2d, 0x6f, 0x5f, 0xa5, 0xf2,
+ 0xeb, 0x95, 0x9a, 0xf2, 0x4c, 0x2a, 0x8b, 0xa9, 0x8e, 0x68, 0x45, 0x2c,
+ 0xab, 0x14, 0x71, 0x69, 0x89, 0x9b, 0x23, 0xdb, 0x3b, 0x30, 0x34, 0x9c,
+ 0xee, 0xef, 0x78, 0x95, 0xf9, 0x1c, 0x2d, 0xce, 0x17, 0xa4, 0x46, 0x9a,
+ 0x63, 0x4a, 0xaa, 0x42, 0x37, 0x8e, 0xac, 0xe4, 0x58, 0x23, 0xe8, 0x4a,
+ 0xc4, 0x91, 0x07, 0xad, 0x3a, 0x4e, 0x6f, 0x6b, 0x53, 0xee, 0x6a, 0xc4,
+ 0x9b, 0xbc, 0x18, 0x86, 0x29, 0x4d, 0x01, 0x3e, 0x7f, 0x2c, 0x79, 0x54,
+ 0xb5, 0xb1, 0x1a, 0x5a, 0xb9, 0x3d, 0x37, 0x1f, 0x8b, 0x89, 0x58, 0xc4,
+ 0x45, 0x6b, 0xc0, 0x58, 0x21, 0xc7, 0xaf, 0xa0, 0x1d, 0x11, 0x3b, 0xd3,
+ 0xf8, 0x1d, 0x31, 0xf2, 0x33, 0x5b, 0x63, 0x54, 0x0c, 0x00, 0xd9, 0x98,
+ 0x4b, 0x42, 0xd9, 0x4e, 0xd8, 0x11, 0x04, 0x49, 0x67, 0x47, 0x27, 0xff,
+ 0x8b, 0xe0, 0x04, 0x96, 0x08, 0xbe, 0x46, 0xf2, 0xff, 0x07, 0xa4, 0xe9,
+ 0x22, 0xbf, 0x4b, 0x44, 0x01, 0x40, 0xfe, 0xf4, 0xe7, 0xe6, 0x8e, 0x95,
+ 0xb3, 0x99, 0x75, 0xd1, 0xe7, 0x59, 0x89, 0xc9, 0x9d, 0xe4, 0x79, 0x80,
+ 0x3c, 0x25, 0xd6, 0xf1, 0xa1, 0xc8, 0xca, 0x27, 0x14, 0x56, 0xc4, 0x6b,
+ 0xc2, 0xda, 0x99, 0x0e, 0x7c, 0xe2, 0xf7, 0x5e, 0x0e, 0x84, 0x37, 0x74,
+ 0xeb, 0x98, 0x81, 0x07, 0x9c, 0xb5, 0x4a, 0x2e, 0x16, 0x3f, 0xab, 0x3e,
+ 0x6d, 0x49, 0x89, 0xce, 0x92, 0x27, 0x97, 0x3a, 0xdd, 0x4f, 0xd5, 0x51,
+ 0xe3, 0x4e, 0x2e, 0x7b, 0x48, 0xaa, 0x17, 0xfb, 0x49, 0xf1, 0x6e, 0xbf,
+ 0xd5, 0x6b, 0xce, 0xc2, 0x7a, 0x74, 0x53, 0x5c, 0x51, 0xb2, 0x4b, 0xa7,
+ 0x7e, 0xf3, 0x44, 0xfb, 0xa6, 0x23, 0xf1, 0xe4, 0x0d, 0xe7, 0x7e, 0x27,
+ 0x58, 0xcc, 0x94, 0x73, 0xbf, 0x93, 0x56, 0xe8, 0x2f, 0x96, 0x97, 0x42,
+ 0x3a, 0x9b, 0x5f, 0xa7, 0x15, 0x7b, 0x06, 0xe4, 0xb6, 0x0e, 0xb3, 0xfb,
+ 0x2f, 0xcc, 0xfb, 0x79, 0xcc, 0x97, 0x7c, 0xf6, 0xe5, 0x25, 0xbb, 0x68,
+ 0x43, 0x87, 0x58, 0xc7, 0xb9, 0x52, 0xd8, 0x39, 0x7d, 0x16, 0x3b, 0x07,
+ 0x32, 0x23, 0x7d, 0x70, 0x3c, 0xcf, 0x96, 0x35, 0xf0, 0xbe, 0xd9, 0x25,
+ 0x84, 0x5b, 0x56, 0x1a, 0xeb, 0xf6, 0x8d, 0x9a, 0x03, 0xab, 0x3e, 0xb5,
+ 0x8a, 0x14, 0xce, 0xd2, 0x0c, 0x78, 0xeb, 0xb9, 0x1e, 0xdb, 0x10, 0x1c,
+ 0x5c, 0xd4, 0xf5, 0x19, 0xbb, 0x09, 0xd2, 0x88, 0x26, 0x5a, 0xba, 0x28,
+ 0x0d, 0x04, 0x87, 0xdb, 0xc0, 0x6d, 0x23, 0x2c, 0xfb, 0x97, 0xf8, 0x8b,
+ 0x3c, 0x3a, 0x29, 0x27, 0xcb, 0x6a, 0x17, 0x68, 0x1c, 0x8f, 0x16, 0x90,
+ 0x6f, 0x2f, 0x71, 0xea, 0x68, 0xe4, 0xc6, 0xc1, 0xee, 0xf3, 0xb5, 0x4a,
+ 0xe0, 0xcc, 0x3f, 0xba, 0x8f, 0x98, 0x45, 0xdf, 0x62, 0x5d, 0x9d, 0x29,
+ 0x65, 0x62, 0x7c, 0x0e, 0x2e, 0x83, 0x37, 0xfb, 0x47, 0x70, 0x06, 0xbc,
+ 0x70, 0x32, 0x99, 0xd1, 0x42, 0xcb, 0x75, 0x7f, 0x41, 0x46, 0x38, 0x82,
+ 0x88, 0x6d, 0x0d, 0x30, 0x9a, 0x99, 0xcc, 0x67, 0xbe, 0x60, 0xa5, 0xa4,
+ 0x6e, 0xc4, 0x0e, 0xb3, 0xfa, 0x0d, 0xa7, 0x9b, 0x22, 0xaa, 0x87, 0xf9,
+ 0x55, 0x3f, 0xe1, 0x18, 0x9a, 0xbf, 0xf0, 0x37, 0xb3, 0xd2, 0x7c, 0x7f,
+ 0x61, 0xde, 0x21, 0xc7, 0x0e, 0x03, 0xe5, 0x10, 0x0b, 0x54, 0x71, 0x70,
+ 0x72, 0x23, 0x77, 0x17, 0xc2, 0xee, 0xc4, 0xff, 0xdf, 0xa2, 0x8c, 0xdd,
+ 0xad, 0x8c, 0x63, 0xe4, 0x8a, 0xd0, 0x02, 0xa1, 0x38, 0xc4, 0x32, 0x97,
+ 0x4b, 0x76, 0x97, 0x97, 0xd1, 0xd6, 0xc5, 0x7c, 0x94, 0x3b, 0x6f, 0x97,
+ 0x3d, 0x0d, 0xb6, 0xd9, 0x78, 0x28, 0xcd, 0x3f, 0x71, 0xca, 0x28, 0x5e,
+ 0xf7, 0xaa, 0x93, 0xfb, 0xb3, 0xb5, 0x5c, 0x05, 0x92, 0xd2, 0xf2, 0x51,
+ 0x9e, 0x6d, 0xbb, 0xec, 0x7c, 0x64, 0xec, 0x50, 0x27, 0x08, 0x17, 0x72,
+ 0xe3, 0xb1, 0x5c, 0xf2, 0x7f, 0x8b, 0x96, 0xea, 0xa1, 0x73, 0x46, 0xa2,
+ 0x6e, 0x47, 0xff, 0x92, 0x5f, 0xba, 0x8c, 0xd9, 0x96, 0xae, 0xf6, 0x99,
+ 0x95, 0x96, 0xf1, 0x21, 0x82, 0xf3, 0x9c, 0x59, 0x92, 0x28, 0x38, 0x38,
+ 0xdf, 0xc1, 0x9a, 0xa3, 0xaf, 0x5f, 0xc5, 0xad, 0xec, 0x66, 0xdf, 0x88,
+ 0x14, 0x5e, 0x11, 0x30, 0x7d, 0x62, 0xbe, 0x46, 0xd0, 0xfa, 0x84, 0x61,
+ 0x8e, 0xd5, 0x9a, 0x87, 0x59, 0x2b, 0x32, 0xc1, 0x0d, 0xfa, 0x4c, 0xa6,
+ 0xd9, 0x5e, 0x09, 0x78, 0x24, 0xf7, 0x72, 0x5a, 0x33, 0x64, 0x07, 0xc0,
+ 0x71, 0x81, 0xdf, 0x96, 0x6d, 0x36, 0x5f, 0x26, 0xee, 0xa2, 0xc1, 0x81,
+ 0x75, 0x67, 0xef, 0x99, 0x1b, 0x5f, 0x04, 0x2e, 0x30, 0x57, 0xe3, 0xfe,
+ 0x9a, 0xf2, 0x3a, 0x94, 0x48, 0xf6, 0x08, 0x62, 0x79, 0x0c, 0x7e, 0xdb,
+ 0x62, 0xff, 0x0f, 0x79, 0x5a, 0xc6, 0xac, 0x16, 0x20, 0x62, 0xdd, 0xb0,
+ 0xee, 0x29, 0x28, 0x1d, 0x79, 0xce, 0xb8, 0x44, 0xaa, 0xb4, 0x9c, 0xb2,
+ 0x54, 0x86, 0x18, 0xda, 0xed, 0x68, 0xff, 0x59, 0xeb, 0x07, 0xca, 0x28,
+ 0xf1, 0x10, 0x9e, 0xdc, 0x14, 0x55, 0x4f, 0x17, 0x62, 0x81, 0xcc, 0xbe,
+ 0xd1, 0xf5, 0x39, 0xe3, 0xc5, 0x75, 0x55, 0x3b, 0x06, 0x6b, 0x98, 0x9f,
+ 0x9d, 0x48, 0x4d, 0xa3, 0x52, 0x96, 0x64, 0x76, 0xa6, 0x16, 0x91, 0x04,
+ 0x02, 0x9b, 0x38, 0x75, 0x41, 0x09, 0xa3, 0xe9, 0xab, 0xf0, 0x9a, 0xb2,
+ 0x14, 0xb7, 0x8d, 0x7d, 0xab, 0x58, 0xbd, 0xaa, 0xe8, 0x69, 0x45, 0x93,
+ 0x06, 0xe9, 0x91, 0xd1, 0x03, 0x60, 0xc5, 0x32, 0x35, 0x95, 0xd8, 0xf8,
+ 0x82, 0x19, 0xef, 0x91, 0x2e, 0xc2, 0xbb, 0x91, 0x79, 0x6f, 0x80, 0x1e,
+ 0x60, 0x4b, 0x08, 0x9f, 0xe8, 0x1e, 0x47, 0x56, 0xc8, 0x40, 0xc3, 0x12,
+ 0x1a, 0x67, 0x17, 0x36, 0x3c, 0xc8, 0x46, 0xd0, 0x2c, 0xe5, 0xe6, 0x43,
+ 0x4f, 0xa7, 0x4a, 0x1d, 0x32, 0xfa, 0x91, 0x01, 0x6d, 0xb9, 0xab, 0x26,
+ 0x22, 0x9e, 0xd9, 0xe8, 0x7e, 0xa2, 0xff, 0xd2, 0xb8, 0xe3, 0x30, 0x40,
+ 0x80, 0xc4, 0x48, 0xf3, 0xcd, 0x45, 0xfe, 0x72, 0x6e, 0x6b, 0x56, 0xfe,
+ 0x88, 0x22, 0x8d, 0xf2, 0x7f, 0xe3, 0x7d, 0xb8, 0x83, 0x53, 0xa7, 0x42,
+ 0x54, 0xfc, 0x1e, 0x1d, 0xb5, 0xba, 0x2e, 0x70, 0xb4, 0x93, 0x41, 0x30,
+ 0x3d, 0x4c, 0x05, 0x36, 0x58, 0x1f, 0x18, 0x14, 0x71, 0xc6, 0xdd, 0x98,
+ 0x90, 0x4d, 0x97, 0xad, 0x02, 0xff, 0x2a, 0x61, 0xae, 0x82, 0xa1, 0xb0,
+ 0xf2, 0xe1, 0x74, 0x15, 0x07, 0x79, 0x5d, 0xb8, 0x14, 0x52, 0xa5, 0x48,
+ 0xed, 0xc0, 0xfc, 0x5b, 0x91, 0x74, 0xb9, 0xff, 0xb6, 0xc9, 0x3f, 0xd9,
+ 0xb2, 0xbf, 0xb1, 0x66, 0x91, 0xc8, 0x0a, 0xaf, 0x48, 0x98, 0x25, 0x8a,
+ 0x07, 0xab, 0x16, 0x97, 0xfc, 0xdf, 0xc8, 0xcc, 0x67, 0xe0, 0x9f, 0x81,
+ 0x18, 0xcb, 0x8a, 0x5e, 0x6c, 0xa2, 0xc8, 0x01, 0xbe, 0x3f, 0xe4, 0x84,
+ 0xb8, 0x70, 0x70, 0xc2, 0xee, 0x43, 0xfc, 0xb7, 0xb7, 0xc9, 0x43, 0x78,
+ 0xf2, 0x87, 0x37, 0xbd, 0xdb, 0xe7, 0x96, 0xda, 0x6d, 0x49, 0xef, 0x01,
+ 0xc0, 0xcd, 0xdb, 0x03, 0x4f, 0x77, 0xf6, 0xdb, 0xc0, 0x8f, 0x0a, 0x1d,
+ 0x3f, 0xd8, 0x60, 0x64, 0x6d, 0x56, 0x52, 0xa2, 0x50, 0x76, 0x35, 0x32,
+ 0x8b, 0x76, 0x77, 0xad, 0x69, 0xa7, 0x79, 0x45, 0x71, 0xb9, 0x9c, 0x73,
+ 0xd5, 0xb6, 0x35, 0x2e, 0x51, 0x6d, 0x47, 0x8a, 0x11, 0xfd, 0xf6, 0x5b,
+ 0x07, 0xd9, 0x7e, 0xf8, 0xb0, 0x2e, 0x5d, 0xac, 0xdf, 0xa5, 0x8a, 0x1e,
+ 0xae, 0x61, 0xcf, 0xf4, 0x1c, 0x73, 0x95, 0x06, 0x69, 0x5e, 0xe7, 0xfd,
+ 0x52, 0x91, 0xec, 0xba, 0x8c, 0x38, 0x91, 0xea, 0x94, 0xf4, 0x0b, 0x7f,
+ 0xd8, 0xce, 0xbf, 0x76, 0xca, 0xf2, 0xc8, 0x02, 0x02, 0xde, 0x99, 0x1a,
+ 0xa4, 0x80, 0x24, 0x3e, 0x60, 0xd1, 0x76, 0xf5, 0xac, 0x0d, 0xc3, 0x7f,
+ 0x2b, 0xce, 0x3e, 0xf2, 0x33, 0x87, 0x77, 0xc2, 0x08, 0xba, 0x55, 0xbb,
+ 0x26, 0x12, 0x20, 0xa1, 0xde, 0xf0, 0x83, 0xe1, 0x48, 0xc5, 0x70, 0x49,
+ 0x29, 0x3b, 0x19, 0xda, 0xd1, 0x46, 0x60, 0x27, 0x2d, 0x15, 0x85, 0xd5,
+ 0x62, 0x94, 0x1d, 0x8f, 0x60, 0x7e, 0x56, 0xb9, 0x43, 0x92, 0xc8, 0x1a,
+ 0x4c, 0xca, 0x13, 0x53, 0x57, 0xb2, 0xb4, 0xb8, 0x4f, 0xa7, 0x5f, 0x4f,
+ 0x41, 0x8e, 0xcf, 0x44, 0x60, 0x05, 0x5f, 0x40, 0x60, 0xee, 0x67, 0x0b,
+ 0x67, 0xa1, 0xff, 0x3f, 0x63, 0x3b, 0x90, 0x56, 0x9d, 0x73, 0x27, 0xd7,
+ 0xd2, 0x64, 0xae, 0x38, 0x33, 0x58, 0xc0, 0xd6, 0x24, 0x7c, 0x99, 0xe9,
+ 0xe9, 0x6f, 0x7e, 0xdb, 0x35, 0x10, 0x94, 0xe9, 0xb7, 0x98, 0x26, 0xec,
+ 0xfc, 0x23, 0xcb, 0xc8, 0x21, 0x68, 0xe9, 0x9b, 0xdb, 0x2d, 0x08, 0x7f,
+ 0x3b, 0x33, 0xa7, 0xc9, 0x98, 0x87, 0x72, 0x3d, 0xfe, 0xfe, 0x67, 0x3f,
+ 0xee, 0x8b, 0xec, 0x65, 0x89, 0x38, 0xab, 0x4d, 0xfb, 0x63, 0xd0, 0x69,
+ 0xa4, 0x1e, 0x68, 0xa2, 0xc3, 0x41, 0xfd, 0x0e, 0x0a, 0xd1, 0x71, 0xd8,
+ 0xd7, 0xc9, 0xa0, 0x16, 0x7b, 0xa0, 0xbb, 0x83, 0x22, 0x8d, 0x70, 0xbb,
+ 0x1c, 0xf5, 0xcf, 0xbe, 0xb0, 0xf9, 0x2b, 0xfd, 0x7d, 0x4c, 0xb9, 0xd8,
+ 0x09, 0x42, 0x46, 0x33, 0x21, 0x96, 0x24, 0x51, 0xf8, 0xdf, 0x85, 0x01,
+ 0xf7, 0x88, 0xaa, 0xb2, 0xa7, 0xc8, 0x1c, 0x2b, 0xa9, 0x76, 0x6b, 0x91,
+ 0x61, 0xfd, 0x5b, 0x87, 0xc0, 0x92, 0xde, 0x3e, 0x35, 0x99, 0x1b, 0x5a,
+ 0xf3, 0x17, 0x7e, 0x1a, 0x6d, 0xd2, 0x78, 0x32, 0x13, 0x96, 0x2d, 0xbe,
+ 0x42, 0x06, 0xee, 0x4b, 0x50, 0x8f, 0xdf, 0x1e, 0xcb, 0x06, 0x35, 0x9f,
+ 0xf9, 0xbc, 0x42, 0xe2, 0x5d, 0xc8, 0xbc, 0xa1, 0x89, 0x75, 0x5d, 0xe1,
+ 0xdc, 0xbe, 0x4c, 0x6f, 0xfa, 0x77, 0xd5, 0x68, 0xf0, 0xde, 0xdd, 0xc7,
+ 0xae, 0x93, 0x16, 0x1f, 0xff, 0x47, 0xec, 0xb5, 0x0d, 0x62, 0xc2, 0xf6,
+ 0xbc, 0xed, 0xd5, 0x82, 0x7d, 0x23, 0x76, 0xf3, 0xa1, 0x63, 0xac, 0x68,
+ 0x36, 0x2d, 0xe6, 0x02, 0x1c, 0x26, 0x96, 0xee, 0x2a, 0x11, 0xe3, 0x24,
+ 0x6a, 0x5c, 0xf9, 0xca, 0x7f, 0x01, 0xcb, 0x37, 0x0b, 0x94, 0x99, 0xfe,
+ 0x04, 0xef, 0x4d, 0x3e, 0x3d, 0x3c, 0x0d, 0x27, 0x0a, 0xbd, 0x47, 0xa5,
+ 0xa0, 0x9b, 0x21, 0xfe, 0x79, 0xa9, 0xd2, 0x50, 0x25, 0x1f, 0x1b, 0xc6,
+ 0x52, 0x14, 0xa9, 0x3a, 0xa1, 0x0c, 0xcd, 0x61, 0xe8, 0x1b, 0xbd, 0x5c,
+ 0x76, 0x08, 0x5f, 0xdf, 0x58, 0xe3, 0x2c, 0x5c, 0x48, 0x22, 0xb5, 0x73,
+ 0x8a, 0xcc, 0x79, 0x52, 0xcd, 0xd2, 0x23, 0x69, 0xce, 0x53, 0xff, 0xef,
+ 0x95, 0xde, 0x6c, 0xe0, 0xe3, 0x14, 0xfa, 0x26, 0xb2, 0x6c, 0x5e, 0xb3,
+ 0x3d, 0xae, 0x4a, 0xff, 0x64, 0xc3, 0xdb, 0x7c, 0xf3, 0xb6, 0x67, 0xff,
+ 0xed, 0x13, 0x93, 0x6e, 0xd7, 0xf7, 0x4e, 0x7b, 0x96, 0x80, 0xa7, 0xcc,
+ 0x14, 0xea, 0xb1, 0xd0, 0xe1, 0x8b, 0x64, 0x5c, 0xed, 0x4a, 0x32, 0x9a,
+ 0x86, 0x79, 0xf3, 0xb2, 0x30, 0x95, 0xe6, 0x9a, 0xc9, 0x8a, 0x4d, 0x69,
+ 0xd4, 0xf3, 0x1a, 0x5a, 0x87, 0xb3, 0x7d, 0xc2, 0xa3, 0x97, 0x2b, 0x26,
+ 0x8b, 0xe6, 0xe1, 0xf7, 0x55, 0xd2, 0x82, 0xa2, 0x4b, 0xd4, 0x7a, 0x4b,
+ 0x98, 0xdd, 0x59, 0x8d, 0x6a, 0x76, 0xe2, 0x33, 0x93, 0x86, 0x6b, 0x4b,
+ 0x61, 0xd4, 0x5e, 0x1b, 0x96, 0xb7, 0xd5, 0xbb, 0x4b, 0x67, 0x20, 0x8f,
+ 0xdb, 0x9f, 0x01, 0xc1, 0x5c, 0x2a, 0x53, 0xf6, 0x67, 0x1f, 0xd9, 0x16,
+ 0x43, 0x33, 0xd2, 0xca, 0xb9, 0x76, 0xe8, 0x88, 0x63, 0xe2, 0xa5, 0x28,
+ 0xc8, 0x0d, 0x06, 0xc1, 0xd1, 0x54, 0x71, 0x22, 0x24, 0x98, 0xb5, 0xab,
+ 0xc6, 0x55, 0xc5, 0x19, 0x9b, 0xa6, 0x9d, 0x31, 0x8c, 0xd6, 0xc3, 0x3e,
+ 0x6e, 0x51, 0x26, 0x3b, 0xde, 0x7f, 0xcc, 0x7c, 0x98, 0x0d, 0xb4, 0xa1,
+ 0xf6, 0x63, 0x37, 0xfa, 0x35, 0xc5, 0x2a, 0xd1, 0x25, 0xe1, 0x85, 0xda,
+ 0xc9, 0xbc, 0xe0, 0x9d, 0x08, 0xf4, 0x70, 0xec, 0xb4, 0x6a, 0x66, 0xa5,
+ 0xd3, 0xf1, 0xba, 0x5a, 0x48, 0x6d, 0xa5, 0xf2, 0x0e, 0x1c, 0x68, 0x12,
+ 0x79, 0xb0, 0xb4, 0x99, 0xda, 0xa5, 0x19, 0xa6, 0xc5, 0x8f, 0x69, 0x60,
+ 0xd2, 0xca, 0xa1, 0x24, 0x7e, 0x03, 0xcd, 0x3d, 0x12, 0x2c, 0xe2, 0x9a,
+ 0x2a, 0xb0, 0x21, 0xda, 0xa2, 0xd7, 0x30, 0x6b, 0x47, 0xf0, 0x90, 0xcf,
+ 0x4b, 0x91, 0x33, 0x26, 0xb3, 0xeb, 0x2b, 0x9b, 0x76, 0xdb, 0xf6, 0x45,
+ 0xcd, 0x43, 0xe3, 0x70, 0x46, 0x89, 0x6a, 0xd4, 0x8a, 0x23, 0x25, 0x92,
+ 0x01, 0x70, 0xea, 0xcf, 0xca, 0xec, 0xe5, 0xb1, 0x6b, 0x55, 0x10, 0xd8,
+ 0xf2, 0x30, 0xbc, 0x32, 0xe4, 0x57, 0xfa, 0xc1, 0x81, 0x5c, 0xea, 0x67,
+ 0x13, 0xaa, 0x76, 0x26, 0x0b, 0xc5, 0xc7, 0x85, 0x9f, 0xb3, 0x40, 0xbc,
+ 0xfe, 0x64, 0xe2, 0xa6, 0xdc, 0x62, 0x11, 0xef, 0xd3, 0x42, 0x33, 0xbd,
+ 0x6e, 0x0d, 0x94, 0xdd, 0x99, 0xfe, 0x1a, 0xe3, 0x18, 0xe0, 0x55, 0xa3,
+ 0x4c, 0xcc, 0xce, 0x8f, 0xee, 0x5f, 0xd8, 0x43, 0xa6, 0xbe, 0x37, 0x4e,
+ 0xc5, 0x31, 0xfc, 0x3f, 0x80, 0x29, 0x33, 0x26, 0x08, 0x93, 0xb4, 0x6b,
+ 0xb0, 0xd3, 0xbb, 0xbc, 0x01, 0x17, 0x6a, 0x59, 0x0c, 0xca, 0x65, 0xaa,
+ 0x07, 0xae, 0xfc, 0x0a, 0xf4, 0x93, 0xcb, 0x4b, 0x7f, 0x29, 0xbc, 0xa8,
+ 0x3c, 0xc8, 0xeb, 0x5c, 0x44, 0xf5, 0xea, 0x06, 0xe2, 0x8e, 0x2b, 0xe2,
+ 0xd9, 0x48, 0x7e, 0x07, 0x89, 0xbc, 0x84, 0x09, 0xd0, 0x68, 0x33, 0x34,
+ 0x18, 0x6c, 0xa5, 0x50, 0x2c, 0xa4, 0xcc, 0xda, 0xda, 0xeb, 0x3f, 0xfa,
+ 0x76, 0x5c, 0x7f, 0x5f, 0xeb, 0x8c, 0x5a, 0xd6, 0x16, 0x0b, 0xf6, 0x85,
+ 0x70, 0x40, 0x47, 0x8f, 0xa3, 0x77, 0xdb, 0xbf, 0xb3, 0xbc, 0xb0, 0xa9,
+ 0xbf, 0xbe, 0x1d, 0x4f, 0x2f, 0x96, 0x9d, 0x87, 0xcc, 0x6d, 0x2e, 0xa4,
+ 0x84, 0xfa, 0x89, 0xb9, 0x05, 0xc5, 0x6e, 0x60, 0x00, 0xaa, 0x82, 0x25,
+ 0x0e, 0xa7, 0x61, 0xa7, 0xc9, 0x55, 0x99, 0xb3, 0x8e, 0x99, 0x41, 0xc8,
+ 0x1a, 0x7e, 0x1d, 0xd6, 0x5f, 0x4c, 0x22, 0xfb, 0x82, 0x2b, 0xaf, 0x7d,
+ 0x3b, 0x85, 0x18, 0xfe, 0x23, 0x3c, 0x0c, 0x8c, 0x95, 0xac, 0xc1, 0x20,
+ 0x2c, 0x37, 0x77, 0x32, 0xf0, 0x22, 0xbd, 0x5f, 0x93, 0xa2, 0xe6, 0x87,
+ 0x4d, 0x40, 0xd7, 0xd1, 0x45, 0x48, 0x77, 0xea, 0xc7, 0x8d, 0xc1, 0x23,
+ 0x40, 0xcd, 0x46, 0x69, 0xf6, 0x08, 0x85, 0x1a, 0x75, 0xa2, 0x7c, 0x73,
+ 0xf7, 0xfa, 0xe3, 0x18, 0x88, 0xb9, 0xde, 0x8d, 0x3c, 0x65, 0xdb, 0x3f,
+ 0xd4, 0x95, 0x91, 0x79, 0x6c, 0x9c, 0xaa, 0xa0, 0x0f, 0xf0, 0x9f, 0x6e,
+ 0xe7, 0x9b, 0xf9, 0x93, 0x83, 0xa1, 0x00, 0x83, 0xa9, 0x1f, 0x52, 0xc0,
+ 0xce, 0x89, 0xab, 0x7a, 0x88, 0x93, 0x72, 0x8c, 0xa7, 0xc4, 0xbb, 0x7d,
+ 0xcb, 0xc0, 0xd7, 0xa4, 0x15, 0x2a, 0x70, 0x16, 0x30, 0x27, 0xac, 0xf8,
+ 0x61, 0xbd, 0x87, 0x00, 0xc9, 0x29, 0x67, 0xb8, 0x27, 0xcb, 0xbc, 0xae,
+ 0x3f, 0xd7, 0xd8, 0xd3, 0x1f, 0x36, 0x07, 0x65, 0xea, 0x69, 0x7d, 0x98,
+ 0xd9, 0x3b, 0xd6, 0xf5, 0xff, 0x63, 0x32, 0xe6, 0xf4, 0x72, 0x1f, 0x18,
+ 0x1f, 0x61, 0xb0, 0x58, 0x54, 0xcc, 0x76, 0xbc, 0xb6, 0xc3, 0xfb, 0x43,
+ 0xd3, 0x79, 0x34, 0x55, 0x7f, 0x44, 0x59, 0x3a, 0x82, 0xdc, 0x7f, 0x09,
+ 0x0d, 0x8c, 0xf9, 0x86, 0x11, 0x64, 0x41, 0x9a, 0xae, 0xd4, 0x16, 0x47,
+ 0xfc, 0xfe, 0x0a, 0x0f, 0x94, 0x78, 0x9f, 0x8e, 0x73, 0xcc, 0xfc, 0x25,
+ 0x30, 0x08, 0x18, 0x3b, 0xcd, 0x2c, 0x9b, 0xe6, 0x27, 0x57, 0x55, 0xe4,
+ 0x37, 0x73, 0x61, 0x2e, 0x8f, 0x07, 0x27, 0xc8, 0x35, 0x72, 0x8d, 0x12,
+ 0x88, 0x52, 0x67, 0xef, 0xc2, 0xe1, 0x8e, 0x18, 0x8b, 0xfd, 0x91, 0x9e,
+ 0xaa, 0x32, 0x71, 0x02, 0x71, 0x5e, 0xbd, 0x71, 0x29, 0xc9, 0xb0, 0xb3,
+ 0x48, 0x1a, 0x7b, 0x41, 0xd9, 0x78, 0x1e, 0x31, 0x97, 0xe9, 0xfb, 0x34,
+ 0xb0, 0xa7, 0x82, 0x28, 0xcd, 0x6f, 0x2c, 0x7b, 0x5a, 0xcb, 0x2d, 0xed,
+ 0xd1, 0x95, 0xbe, 0xe7, 0x1f, 0xb3, 0xc0, 0xaa, 0xce, 0x11, 0xd7, 0x59,
+ 0xe7, 0x8e, 0x52, 0x8d, 0x07, 0x2f, 0xd5, 0xc5, 0x63, 0x65, 0xae, 0x93,
+ 0x06, 0x4d, 0xa8, 0xe3, 0xb4, 0x80, 0xc6, 0xa8, 0x2d, 0x25, 0x90, 0xa3,
+ 0x3a, 0x48, 0x5c, 0xde, 0x43, 0x9e, 0xe2, 0x9f, 0x4d, 0x19, 0x7b, 0xf2,
+ 0xc3, 0xae, 0x84, 0xc3, 0xdd, 0xce, 0x5e, 0x9f, 0x9f, 0x61, 0x99, 0x70,
+ 0x4f, 0xee, 0x08, 0xd5, 0xa9, 0x52, 0x19, 0x71, 0xd1, 0x3d, 0x52, 0xbd,
+ 0x39, 0xbe, 0xbf, 0x29, 0x30, 0x27, 0x4c, 0x60, 0x57, 0xcf, 0x1f, 0x4e,
+ 0xa6, 0xae, 0x21, 0xee, 0x44, 0x4b, 0xf7, 0xa2, 0x8a, 0x66, 0xa0, 0xfc,
+ 0x96, 0xa0, 0x82, 0x9c, 0xa5, 0xab, 0xf6, 0x4c, 0xba, 0x2d, 0x72, 0xd5,
+ 0xad, 0x03, 0x7d, 0x70, 0x89, 0xf1, 0xb3, 0x11, 0xb4, 0xf3, 0xdd, 0x28,
+ 0xf0, 0x23, 0xf0, 0x22, 0x3d, 0x91, 0x3b, 0x34, 0x1a, 0xd5, 0xa0, 0x11,
+ 0x2a, 0xe7, 0xdc, 0x62, 0xc9, 0x39, 0x0a, 0x27, 0x93, 0x4b, 0x81, 0x62,
+ 0x31, 0xe0, 0xb7, 0x5f, 0xa7, 0xed, 0x5a, 0xd3, 0xa5, 0xfb, 0x87, 0x1e,
+ 0x48, 0x3c, 0x5f, 0x69, 0xa6, 0x61, 0x31, 0x77, 0x59, 0x65, 0x9e, 0x3a,
+ 0x39, 0x11, 0x1c, 0x70, 0xc5, 0xbe, 0x8d, 0xd9, 0xf1, 0xdc, 0x31, 0x97,
+ 0xb2, 0x72, 0x6a, 0x78, 0xdd, 0x7d, 0x50, 0x54, 0x10, 0x70, 0x66, 0x6c,
+ 0xc1, 0x41, 0x89, 0x9d, 0x7e, 0xbd, 0x15, 0x52, 0x4a, 0x6e, 0x80, 0x06,
+ 0x13, 0x08, 0x81, 0xf1, 0x7a, 0xaf, 0x40, 0xfb, 0x22, 0xa5, 0x45, 0x73,
+ 0xec, 0xbc, 0x37, 0x8e, 0xb3, 0xfb, 0x1d, 0x7e, 0x5d, 0xa6, 0xe4, 0x9e,
+ 0xa1, 0x43, 0xf9, 0xa3, 0xad, 0x19, 0x86, 0xc3, 0x2a, 0xcd, 0xb8, 0xcd,
+ 0x2d, 0xcc, 0xd7, 0xa1, 0xf3, 0x8c, 0xb2, 0x34, 0x7e, 0x0a, 0xdb, 0x57,
+ 0x0e, 0x33, 0x41, 0x51, 0xcf, 0x75, 0xea, 0x52, 0x74, 0x5c, 0xa7, 0x08,
+ 0xb4, 0xef, 0x4a, 0xb7, 0xb2, 0x3b, 0x81, 0x97, 0xe2, 0x3a, 0x29, 0x3f,
+ 0x71, 0xd1, 0xb0, 0x46, 0x83, 0x3e, 0xcf, 0x91, 0x2b, 0xc0, 0xe4, 0x9e,
+ 0xa8, 0x66, 0x3f, 0xd6, 0x8f, 0x67, 0x26, 0x8e, 0x28, 0xca, 0x79, 0x04,
+ 0x2e, 0xe2, 0x1f, 0xd4, 0x43, 0x37, 0xbb, 0xe1, 0xdb, 0xe5, 0x62, 0xeb,
+ 0x29, 0x02, 0xdf, 0xe2, 0xf7, 0x73, 0x69, 0x74, 0xbc, 0xbc, 0x98, 0x22,
+ 0x28, 0x56, 0x45, 0xc5, 0xe7, 0xf2, 0xec, 0x96, 0x61, 0xa9, 0xf3, 0x4b,
+ 0x43, 0x7b, 0xb2, 0x41, 0x2d, 0xe7, 0xc7, 0x88, 0xf5, 0x17, 0x1a, 0xf4,
+ 0xbe, 0x0f, 0x49, 0xc6, 0x67, 0xd4, 0xd9, 0xe9, 0x55, 0x40, 0x3a, 0x4b,
+ 0xe8, 0xa8, 0x56, 0x53, 0x31, 0xf6, 0xe8, 0xaa, 0xfc, 0xab, 0x39, 0xe0,
+ 0xe6, 0xb1, 0xd7, 0x89, 0xa1, 0x2b, 0xf2, 0xd5, 0x59, 0x3f, 0x67, 0x90,
+ 0x65, 0x2d, 0xa3, 0xb7, 0x39, 0x81, 0xb3, 0x1e, 0xfb, 0x7c, 0x25, 0x76,
+ 0xa9, 0x1e, 0x05, 0x38, 0x0c, 0xd0, 0xcd, 0x20, 0x7f, 0x31, 0x76, 0xb1,
+ 0xfb, 0xba, 0xa1, 0x3b, 0xd7, 0x2c, 0x2a, 0x61, 0x2e, 0x4a, 0xb3, 0x24,
+ 0x70, 0x35, 0xb1, 0x88, 0x87, 0xaf, 0x37, 0xbd, 0x29, 0xf0, 0xcb, 0x22,
+ 0xa6, 0xcf, 0x0b, 0xc8, 0x31, 0x09, 0x51, 0xf8, 0x8f, 0x22, 0x6c, 0xdd,
+ 0x57, 0x51, 0xf2, 0x43, 0xf7, 0xba, 0x9c, 0xcb, 0x25, 0xde, 0x6e, 0x9c,
+ 0xc1, 0xac, 0x4a, 0x01, 0xaf, 0x87, 0x8b, 0x80, 0x65, 0xb4, 0x75, 0x2d,
+ 0xd8, 0x68, 0xd4, 0x03, 0x88, 0x13, 0x37, 0xfe, 0x29, 0xa2, 0x6c, 0x03,
+ 0xe2, 0xe6, 0xe9, 0x19, 0xc4, 0x85, 0xb6, 0x66, 0xf9, 0x7b, 0xe5, 0x63,
+ 0x2e, 0x8c, 0xd3, 0x7c, 0x70, 0x93, 0x15, 0x74, 0x98, 0x50, 0xcb, 0x5f,
+ 0x73, 0xcb, 0x05, 0xa5, 0xa0, 0x56, 0x40, 0x22, 0x57, 0x04, 0xb2, 0xa9,
+ 0xfb, 0xab, 0xfb, 0x40, 0x2a, 0x91, 0x6b, 0x48, 0x84, 0x77, 0x14, 0xe9,
+ 0x15, 0x3a, 0x9f, 0xed, 0xf9, 0x3d, 0xcc, 0x34, 0x8a, 0x56, 0x42, 0xea,
+ 0xa9, 0x4c, 0x9b, 0xa0, 0xb4, 0x75, 0x13, 0x22, 0xca, 0x8c, 0x6e, 0x80,
+ 0xd5, 0xf2, 0xfe, 0x9a, 0xce, 0x3c, 0x49, 0x7b, 0x6e, 0x61, 0xc0, 0x56,
+ 0xe5, 0x5d, 0x4f, 0xb7, 0x5e, 0x7d, 0x56, 0x5d, 0x85, 0xd9, 0xe8, 0x03,
+ 0xdb, 0x30, 0x1f, 0x22, 0x38, 0xdb, 0xff, 0x08, 0xae, 0xf1, 0x9c, 0xf7,
+ 0x47, 0x96, 0xe0, 0x89, 0x83, 0xea, 0x03, 0xc0, 0x1d, 0xb5, 0xbb, 0xbe,
+ 0x79, 0x76, 0x9b, 0x2f, 0xda, 0xe7, 0x65, 0x1f, 0xba, 0x02, 0x14, 0xae,
+ 0x2e, 0xf2, 0x09, 0x6b, 0x40, 0x51, 0x08, 0xc2, 0xb7, 0x5c, 0xa5, 0xf1,
+ 0xf4, 0x55, 0xd8, 0x12, 0xa4, 0xeb, 0x97, 0xc1, 0x33, 0x0d, 0xe4, 0x83,
+ 0x5f, 0xf3, 0xd6, 0xdb, 0xf9, 0xe7, 0x8c, 0x0b, 0x6f, 0xb6, 0x1d, 0x12,
+ 0x10, 0xaf, 0x4d, 0x41, 0x47, 0x50, 0x6d, 0x72, 0xf1, 0x03, 0xe6, 0x8e,
+ 0xe5, 0xbf, 0xb5, 0xe1, 0x34, 0xda, 0x8b, 0x52, 0x17, 0xcb, 0xcf, 0x1c,
+ 0xb6, 0x79, 0x32, 0x15, 0x82, 0x67, 0x47, 0x21, 0xa4, 0xc5, 0xc4, 0xab,
+ 0x74, 0xdb, 0xae, 0x95, 0xbd, 0xbf, 0x59, 0x68, 0x4f, 0x4b, 0x82, 0x51,
+ 0xba, 0xf2, 0x29, 0x78, 0x9f, 0xaa, 0x42, 0x67, 0x90, 0xcb, 0x83, 0x8b,
+ 0xeb, 0x24, 0x76, 0x30, 0x9a, 0xef, 0xe9, 0x95, 0xd2, 0x1c, 0xf6, 0x25,
+ 0xa2, 0x09, 0x8f, 0x2c, 0x04, 0xdc, 0x77, 0xff, 0x42, 0xe1, 0x7a, 0xca,
+ 0x98, 0x7b, 0xc0, 0xa1, 0x63, 0x3e, 0x59, 0x7c, 0xf7, 0xd4, 0xb2, 0x38,
+ 0x0a, 0x2b, 0x8f, 0x79, 0x96, 0x5f, 0xc0, 0x34, 0x36, 0xb9, 0xea, 0xc5,
+ 0xf7, 0x75, 0x1f, 0x5a, 0x72, 0x65, 0x67, 0x63, 0x0a, 0xd9, 0xb5, 0xe8,
+ 0x72, 0xc0, 0xf9, 0x06, 0xed, 0xc5, 0xa1, 0x8f, 0x62, 0x0e, 0x08, 0x62,
+ 0x70, 0x56, 0xf9, 0x70, 0xab, 0x56, 0xe1, 0x86, 0x9f, 0xb0, 0x9d, 0x2a,
+ 0x8a, 0x85, 0xe5, 0xd9, 0xe9, 0xe4, 0x87, 0xa6, 0x7f, 0x6e, 0xd4, 0xda,
+ 0x74, 0xc8, 0x43, 0x5a, 0x37, 0xa0, 0xbc, 0x93, 0x4c, 0x32, 0xc4, 0x81,
+ 0xf5, 0x88, 0x49, 0xf2, 0x13, 0x5f, 0x57, 0x59, 0xc1, 0xc9, 0x95, 0xa1,
+ 0x19, 0x4d, 0x06, 0xb2, 0x4c, 0x3d, 0x54, 0x41, 0x96, 0x59, 0xbf, 0xf3,
+ 0x67, 0xb8, 0xb3, 0x49, 0x79, 0x89, 0x33, 0x21, 0x94, 0x3a, 0xf1, 0x3c,
+ 0x94, 0x1e, 0xa1, 0xf9, 0x3a, 0x07, 0x66, 0xdd, 0xf1, 0x9b, 0xf9, 0x9e,
+ 0x33, 0xce, 0xae, 0xb7, 0x8b, 0x59, 0x1a, 0x71, 0x7f, 0x8f, 0xc7, 0x8c,
+ 0x76, 0x79, 0x70, 0xe9, 0x76, 0xff, 0xa7, 0x42, 0x2d, 0xd9, 0x70, 0xa7,
+ 0x42, 0x3c, 0xe0, 0xcf, 0xe5, 0x4f, 0xd8, 0xdf, 0x0e, 0xeb, 0x7c, 0x65,
+ 0x38, 0x67, 0xdb, 0x3e, 0xdc, 0x00, 0x25, 0xe1, 0xcd, 0x4e, 0x48, 0x6c,
+ 0x19, 0x46, 0xb4, 0x57, 0xf0, 0xad, 0x0b, 0xd2, 0x31, 0x9e, 0x3f, 0xb9,
+ 0x54, 0xdb, 0xdc, 0xd4, 0xae, 0x7f, 0x55, 0xee, 0xb0, 0x9b, 0x42, 0xa8,
+ 0x36, 0xac, 0x90, 0xcb, 0xbb, 0x78, 0x49, 0xb0, 0xdf, 0x8b, 0x5a, 0xf5,
+ 0x59, 0x00, 0x18, 0x57, 0x38, 0x79, 0xaf, 0xb2, 0x78, 0x68, 0x1c, 0x32,
+ 0x44, 0xa0, 0xa1, 0x2a, 0xc9, 0x9e, 0x3c, 0xe2, 0x83, 0x9c, 0xe6, 0x2f,
+ 0x67, 0x32, 0x64, 0xe4, 0x21, 0xed, 0x37, 0xd8, 0x6d, 0x74, 0x1e, 0xfc,
+ 0x33, 0xc9, 0x6e, 0x9e, 0x3d, 0x09, 0xeb, 0x93, 0x68, 0xeb, 0xd4, 0xde,
+ 0x39, 0x8d, 0x7b, 0xd7, 0xf3, 0x37, 0xd8, 0xd9, 0x0f, 0xb5, 0x23, 0x96,
+ 0x5f, 0x3c, 0x2b, 0x14, 0x8d, 0xbb, 0x51, 0x1f, 0x7f, 0xba, 0xb6, 0x13,
+ 0x54, 0x77, 0xb3, 0xcb, 0x18, 0xa0, 0xdc, 0xa4, 0x26, 0x16, 0x84, 0xbe,
+ 0xa8, 0x64, 0x13, 0x4f, 0x2e, 0xfe, 0xd4, 0x12, 0x36, 0x97, 0xf3, 0xb9,
+ 0xe0, 0x50, 0xe1, 0xf2, 0xbd, 0xdb, 0x29, 0xeb, 0x8d, 0x81, 0x4b, 0x40,
+ 0xfb, 0x56, 0x39, 0x65, 0x8a, 0x20, 0x7e, 0x64, 0x47, 0x5d, 0x8d, 0x60,
+ 0xfb, 0xd4, 0x0b, 0xa7, 0x7b, 0xe7, 0x25, 0xb4, 0x0b, 0xef, 0x6d, 0xbc,
+ 0x4b, 0xdb, 0xa2, 0xb4, 0x4e, 0x4f, 0x5d, 0x0f, 0xe4, 0xe0, 0xc1, 0xdb,
+ 0x28, 0x5e, 0x63, 0x75, 0xb0, 0x37, 0x43, 0x96, 0x1d, 0x9f, 0x4e, 0xf0,
+ 0x09, 0xe1, 0x14, 0x9f, 0xab, 0x12, 0xfd, 0x4c, 0x26, 0x23, 0xb3, 0xf3,
+ 0xe9, 0xfd, 0x34, 0x6a, 0x87, 0xed, 0x6d, 0x43, 0x47, 0x31, 0x59, 0x8d,
+ 0xa4, 0xfc, 0x85, 0xe6, 0x25, 0xf1, 0xf1, 0xfd, 0xae, 0xe7, 0xd7, 0x3a,
+ 0x1d, 0x86, 0x1d, 0x45, 0xef, 0x18, 0x82, 0x2d, 0xc7, 0xb1, 0xdf, 0xe1,
+ 0x11, 0xec, 0xe5, 0x55, 0xbe, 0xb0, 0x63, 0xba, 0xde, 0xc2, 0x3b, 0xaa,
+ 0x30, 0xcb, 0x4b, 0xaf, 0x62, 0x0a, 0x22, 0x8d, 0x14, 0x63, 0xdc, 0x86,
+ 0x3b, 0xac, 0x45, 0xca, 0xd4, 0x4e, 0x60, 0x40, 0xcb, 0xd8, 0xfb, 0xda,
+ 0x88, 0xd3, 0x31, 0x37, 0xec, 0x71, 0xbf, 0x44, 0x2d, 0x1e, 0x1d, 0x33,
+ 0x62, 0x0d, 0x3e, 0xf6, 0x34, 0x9f, 0x9d, 0x15, 0xe4, 0x2b, 0x03, 0x0c,
+ 0xd1, 0x52, 0xd4, 0xf9, 0xd5, 0x4b, 0x54, 0xf6, 0x6d, 0x04, 0x94, 0xcf,
+ 0xeb, 0xf3, 0x31, 0xb9, 0xb9, 0x40, 0x3c, 0xd8, 0x23, 0xa0, 0xc3, 0x2f,
+ 0x0d, 0xa3, 0x86, 0x84, 0x88, 0x74, 0xd5, 0x04, 0xc4, 0x19, 0xac, 0x2e,
+ 0xa2, 0x3f, 0xcc, 0xbf, 0xaa, 0xe1, 0x29, 0xb0, 0x1f, 0x74, 0x9d, 0x1d,
+ 0xee, 0xbd, 0x2b, 0x31, 0xc4, 0xdd, 0x71, 0xf1, 0xd7, 0x13, 0x1e, 0xcd,
+ 0xcf, 0x0f, 0x97, 0x5c, 0xbb, 0x61, 0xd5, 0xa3, 0x45, 0x6f, 0x91, 0xcf,
+ 0x73, 0x4a, 0x43, 0x8d, 0xcc, 0xbf, 0xcf, 0x16, 0x15, 0xa2, 0x60, 0xe4,
+ 0xfe, 0x0f, 0x2c, 0xf1, 0x6b, 0xb5, 0x59, 0x02, 0xf4, 0x99, 0x4d, 0x55,
+ 0x1a, 0x33, 0x8e, 0xe5, 0xe0, 0x95, 0x08, 0x41, 0xcf, 0x72, 0x01, 0x57,
+ 0x3e, 0x4a, 0xa0, 0xd9, 0x71, 0xfe, 0xe9, 0x9e, 0x4b, 0x70, 0xfd, 0x7b,
+ 0x67, 0x29, 0x96, 0x9d, 0xed, 0x7f, 0xc8, 0x06, 0x60, 0xa3, 0xe4, 0x34,
+ 0x17, 0xde, 0xe8, 0xa3, 0xf0, 0xfe, 0x0d, 0xa3, 0x69, 0xd3, 0x73, 0x82,
+ 0x49, 0x3a, 0xcb, 0xdb, 0x36, 0x30, 0xed, 0x2a, 0xfd, 0x3f, 0xd7, 0xb7,
+ 0x6e, 0xf8, 0xe2, 0x14, 0xf7, 0xb7, 0xf8, 0x0f, 0x32, 0xdd, 0x8e, 0x8e,
+ 0xe1, 0x5c, 0x5e, 0x6a, 0x06, 0xd7, 0xef, 0x21, 0xf5, 0xc3, 0xf5, 0x79,
+ 0x0b, 0x12, 0xf7, 0x1b, 0x89, 0x7e, 0x6d, 0x57, 0x10, 0x3e, 0x6e, 0x74,
+ 0x88, 0xe6, 0xfd, 0x72, 0x20, 0xc3, 0x83, 0xd1, 0x2e, 0xb7, 0xb8, 0xbb,
+ 0xc8, 0x75, 0x81, 0x14, 0x46, 0x1d, 0xee, 0x36, 0x6c, 0x0b, 0xe1, 0x0d,
+ 0x15, 0xec, 0x0d, 0x5d, 0x30, 0x08, 0xbc, 0x42, 0xde, 0xa8, 0x64, 0x1b,
+ 0x3d, 0xbf, 0xe7, 0x14, 0x2a, 0x83, 0x90, 0x5e, 0x48, 0xfd, 0x96, 0x70,
+ 0x36, 0x2d, 0xa4, 0xeb, 0x28, 0xe7, 0x70, 0xe4, 0xfe, 0xb9, 0xed, 0x40,
+ 0xbc, 0x29, 0x34, 0x79, 0xec, 0x19, 0x79, 0xa1, 0xf3, 0x9d, 0x49, 0x8a,
+ 0xf0, 0x2d, 0x2b, 0x02, 0xd9, 0x50, 0x9e, 0xa7, 0xf4, 0xf8, 0x61, 0xad,
+ 0x79, 0xe0, 0x46, 0xe4, 0xe3, 0x78, 0x3c, 0x57, 0x9b, 0x56, 0xb6, 0x14,
+ 0xa8, 0x2f, 0xf8, 0xca, 0x5f, 0x1e, 0xd8, 0xa6, 0x9c, 0x2c, 0x2d, 0x14,
+ 0xf9, 0xff, 0x98, 0x1c, 0xef, 0x10, 0xe9, 0x75, 0x2e, 0x27, 0xde, 0x78,
+ 0x8a, 0xe0, 0x7b, 0xba, 0xa5, 0x97, 0x77, 0x2a, 0x20, 0xcb, 0xc2, 0x53,
+ 0x2e, 0x8b, 0x34, 0xbd, 0x28, 0xba, 0x57, 0xed, 0x10, 0xc1, 0xfd, 0x87,
+ 0x7c, 0x0e, 0x15, 0x63, 0x84, 0xb6, 0x54, 0xfb, 0x81, 0x05, 0xa5, 0x3c,
+ 0x0f, 0x63, 0x48, 0xbb, 0x54, 0x03, 0x7e, 0x2b, 0x2e, 0x9d, 0xfb, 0xcf,
+ 0x86, 0xd7, 0x04, 0xf7, 0x28, 0x84, 0x96, 0x5a, 0x6f, 0x8e, 0x6f, 0x1d,
+ 0x86, 0x2c, 0xf0, 0xe5, 0x33, 0xfd, 0x5d, 0xb3, 0xc3, 0xbf, 0xf7, 0x81,
+ 0x34, 0x33, 0x5c, 0x72, 0xa7, 0x7f, 0xf0, 0x5d, 0x88, 0x19, 0x2f, 0xbc,
+ 0xc4, 0x1c, 0xc5, 0xd3, 0xc2, 0x4d, 0xf9, 0xa5, 0x68, 0x94, 0x50, 0x97,
+ 0xb5, 0x54, 0x7d, 0xa2, 0xd0, 0x8c, 0xc0, 0xcf, 0x03, 0xd1, 0x93, 0x64,
+ 0xaf, 0xf1, 0x6b, 0xb5, 0xd3, 0x7e, 0x08, 0xb1, 0x44, 0x45, 0x84, 0x74,
+ 0xb8, 0x14, 0x9d, 0xc0, 0x63, 0x1c, 0x7b, 0xbc, 0x78, 0x43, 0x83, 0x1a,
+ 0xb2, 0xdc, 0x61, 0x9a, 0xbe, 0x22, 0xa5, 0x6f, 0x00, 0xb1, 0x99, 0x94,
+ 0x18, 0xf9, 0xbb, 0xd2, 0xb9, 0x6e, 0x1b, 0xb6, 0x2d, 0x36, 0x8d, 0xd3,
+ 0x68, 0xf6, 0x77, 0x32, 0xbd, 0x39, 0x6a, 0xb3, 0x12, 0x1d, 0x58, 0x2c,
+ 0x8a, 0x8e, 0x5b, 0x97, 0x09, 0x85, 0x92, 0x7e, 0xca, 0x42, 0xdc, 0x24,
+ 0x8a, 0x8b, 0x40, 0xbd, 0x66, 0x42, 0x5d, 0xa2, 0x21, 0x44, 0xdd, 0x93,
+ 0x8b, 0x43, 0xd6, 0xa6, 0x4b, 0x38, 0xe3, 0x0d, 0x3a, 0x69, 0x0d, 0xd9,
+ 0xac, 0x3f, 0xde, 0xe7, 0x48, 0x45, 0x29, 0xd6, 0x64, 0x3a, 0x86, 0x55,
+ 0xb4, 0x01, 0x1e, 0xa1, 0x2c, 0xff, 0xdb, 0x8f, 0x40, 0xde, 0xc0, 0xef,
+ 0x18, 0xc4, 0x57, 0x96, 0x35, 0xbe, 0xe2, 0x21, 0x3e, 0xde, 0x88, 0x33,
+ 0xd5, 0x68, 0x24, 0x07, 0xb0, 0x6c, 0xa2, 0xe7, 0xef, 0xff, 0xc6, 0x95,
+ 0x16, 0x85, 0x26, 0xc2, 0xf0, 0xe7, 0x59, 0x94, 0xbc, 0x02, 0x0d, 0x90,
+ 0x56, 0x67, 0x1a, 0x19, 0x9a, 0x1f, 0xf1, 0x4c, 0xdb, 0x96, 0xfd, 0xa3,
+ 0x6f, 0x87, 0x0e, 0xa0, 0xf7, 0x27, 0x95, 0x3a, 0x41, 0x5e, 0x84, 0x18,
+ 0x97, 0x0e, 0x0c, 0xb1, 0x7e, 0x0c, 0x72, 0xbd, 0xe4, 0x16, 0x6b, 0x3d,
+ 0x9f, 0x35, 0x11, 0xa3, 0x23, 0x22, 0x92, 0x93, 0x64, 0x1b, 0x38, 0x5e,
+ 0xb8, 0x20, 0x37, 0xa5, 0x79, 0x93, 0xfc, 0xa9, 0xc2, 0x8d, 0x76, 0xc5,
+ 0xa5, 0x7d, 0x03, 0xf4, 0x65, 0x6e, 0x04, 0xf1, 0x17, 0xc3, 0x24, 0xfa,
+ 0xc1, 0x64, 0xc2, 0xa6, 0xc1, 0x1f, 0x91, 0xc5, 0x16, 0x0c, 0xd4, 0x13,
+ 0x5e, 0xb9, 0x39, 0x3b, 0xe0, 0xf2, 0xc1, 0xe0, 0xa4, 0xa7, 0xcf, 0x33,
+ 0xcd, 0xbc, 0x22, 0xa2, 0x00, 0x35, 0x13, 0x55, 0x37, 0xda, 0x3a, 0x6f,
+ 0x9f, 0x93, 0xb2, 0xa2, 0xa5, 0x33, 0x95, 0x96, 0x1b, 0x6f, 0xfe, 0x0e,
+ 0x57, 0xd6, 0x57, 0xbd, 0x21, 0x82, 0xb0, 0x43, 0x58, 0x80, 0xd1, 0xc6,
+ 0x45, 0x66, 0x7a, 0x67, 0xde, 0xdd, 0x68, 0x79, 0x1a, 0x9c, 0x3d, 0x4e,
+ 0xd3, 0x26, 0x79, 0x0a, 0xcc, 0x36, 0x8d, 0x5a, 0xbf, 0x91, 0x69, 0x21,
+ 0xe1, 0xd6, 0xd6, 0xcf, 0x52, 0x0d, 0x56, 0xc8, 0x35, 0x1f, 0x19, 0x71,
+ 0xde, 0xc8, 0x73, 0x8e, 0x31, 0xf5, 0x1d, 0xf5, 0x63, 0x31, 0x66, 0xcf,
+ 0xb2, 0x60, 0xbf, 0x03, 0x39, 0x40, 0xa4, 0xb7, 0x2a, 0x03, 0x58, 0x78,
+ 0xd8, 0x33, 0x80, 0x6e, 0xbd, 0xf2, 0xf2, 0xaf, 0x88, 0x29, 0xa3, 0x07,
+ 0xc1, 0x20, 0xfe, 0xc8, 0x75, 0x25, 0xe6, 0x52, 0xe9, 0x57, 0x6c, 0x7b,
+ 0x16, 0xcf, 0x2c, 0x61, 0xf9, 0x3a, 0xa8, 0xa2, 0x67, 0x62, 0x9c, 0x7c,
+ 0xd1, 0xf4, 0xab, 0x54, 0x8c, 0xbb, 0x0d, 0x92, 0xde, 0x05, 0x54, 0x1a,
+ 0x5b, 0x84, 0x75, 0x2f, 0x30, 0x2a, 0x30, 0x50, 0xdb, 0x42, 0xb7, 0xfe,
+ 0xff, 0x18, 0x9b, 0xbd, 0x70, 0x2f, 0x47, 0x34, 0x44, 0x34, 0x58, 0x43,
+ 0x0c, 0x29, 0xea, 0xc6, 0xbd, 0x1c, 0xfd, 0xf6, 0x6e, 0x3c, 0xe2, 0x21,
+ 0x55, 0xc3, 0x1f, 0x82, 0x25, 0xd2, 0x5d, 0x4e, 0x81, 0x8f, 0xaa, 0x6d,
+ 0x92, 0x64, 0x90, 0x72, 0xbe, 0x03, 0xda, 0xac, 0xd0, 0x1c, 0x66, 0xc2,
+ 0x50, 0xd3, 0xd8, 0xca, 0xe1, 0xe0, 0xe3, 0xfb, 0xc7, 0xa5, 0x59, 0x71,
+ 0x4f, 0xdc, 0x1b, 0x4f, 0xd0, 0x75, 0x57, 0x0b, 0x1f, 0x99, 0x45, 0x25,
+ 0x20, 0xf1, 0xd9, 0xe4, 0x55, 0x47, 0xd2, 0xc1, 0x13, 0x04, 0x56, 0xc4,
+ 0x37, 0x61, 0x27, 0xaa, 0xb2, 0xfb, 0x52, 0x73, 0xbf, 0xd8, 0x3e, 0x0d,
+ 0xf4, 0xe8, 0x19, 0x21, 0x13, 0x90, 0xfb, 0x93, 0xd4, 0x94, 0x5b, 0x0d,
+ 0x93, 0xc9, 0x00, 0xaa, 0xb9, 0x6f, 0x41, 0x5c, 0xcf, 0x9e, 0x27, 0x2a,
+ 0xf9, 0xb5, 0x09, 0xc9, 0xc3, 0xeb, 0x8b, 0x09, 0xce, 0x27, 0x54, 0x1d,
+ 0x41, 0x0c, 0x13, 0x17, 0xca, 0x2e, 0x90, 0xa4, 0xa1, 0x0d, 0xad, 0xa0,
+ 0x16, 0x7c, 0x57, 0x21, 0x91, 0x27, 0x07, 0xaf, 0x71, 0x96, 0x96, 0xa5,
+ 0x74, 0x51, 0x45, 0xc5, 0x07, 0xd4, 0x7d, 0xae, 0xb8, 0xf6, 0xe0, 0x95,
+ 0x70, 0xe7, 0xe4, 0xbb, 0x91, 0x6b, 0xe0, 0xa6, 0xbe, 0x9c, 0x26, 0x81,
+ 0x16, 0xbf, 0x80, 0xb9, 0x7c, 0x18, 0x34, 0x98, 0xab, 0x60, 0xa1, 0x7f,
+ 0x18, 0xcb, 0x29, 0xc6, 0xe4, 0x50, 0xe7, 0x20, 0x52, 0xc1, 0x43, 0xd0,
+ 0x9f, 0x10, 0x38, 0xf9, 0x3a, 0xf2, 0x56, 0xb1, 0x08, 0x4c, 0x2c, 0xac,
+ 0x07, 0xb3, 0xd6, 0x7e, 0xa8, 0x46, 0x1c, 0x71, 0x1f, 0x92, 0xf5, 0x6e,
+ 0x04, 0x3b, 0x56, 0xf2, 0x92, 0x8c, 0x59, 0xb6, 0x77, 0xb6, 0x9c, 0xdf,
+ 0xda, 0x41, 0xb4, 0x9e, 0x67, 0xb6, 0x51, 0xd8, 0xb6, 0x47, 0xbb, 0xfa,
+ 0x3e, 0xe9, 0xcf, 0x50, 0x63, 0xd3, 0x18, 0xa4, 0x32, 0x39, 0xda, 0xb1,
+ 0xf4, 0xae, 0xcd, 0xbc, 0x83, 0xe6, 0xcb, 0x45, 0xbc, 0xd2, 0xea, 0xcb,
+ 0xba, 0xb1, 0x34, 0x44, 0xea, 0x89, 0xed, 0xe0, 0x56, 0x51, 0xd9, 0x36,
+ 0x32, 0xcf, 0x65, 0xb0, 0x6b, 0xfa, 0x66, 0x45, 0x40, 0xed, 0x2c, 0x58,
+ 0x25, 0xf6, 0xdd, 0x9a, 0xd1, 0x92, 0xf3, 0x0b, 0xad, 0x78, 0x62, 0xbe,
+ 0x6b, 0x7f, 0xc5, 0x4f, 0x3e, 0x98, 0x23, 0x86, 0x77, 0xb3, 0x9f, 0xe3,
+ 0x88, 0xcd, 0xab, 0x9b, 0x2e, 0x3c, 0x55, 0x72, 0x57, 0xe6, 0x03, 0x23,
+ 0x30, 0x34, 0x0c, 0x62, 0xda, 0x30, 0xc6, 0xfb, 0xad, 0xf6, 0x19, 0x7b,
+ 0xde, 0x63, 0xbe, 0x34, 0x1e, 0xd1, 0x27, 0x9b, 0x9f, 0xea, 0x90, 0x53,
+ 0x40, 0x0e, 0x9b, 0xef, 0x5b, 0xa5, 0x69, 0xde, 0xaf, 0x6b, 0x01, 0x8e,
+ 0x2c, 0xf1, 0xab, 0x0e, 0xe7, 0xaf, 0x59, 0xf0, 0xa3, 0x11, 0xb5, 0x00,
+ 0x7c, 0x8f, 0x5e, 0x02, 0x73, 0x1c, 0x88, 0x43, 0x68, 0x8d, 0xa3, 0x3a,
+ 0x36, 0x11, 0xdb, 0x26, 0x92, 0x25, 0x69, 0x7a, 0xa7, 0xed, 0xd0, 0xc3,
+ 0x3b, 0x05, 0xd3, 0x46, 0x22, 0x88, 0x21, 0xf3, 0xa1, 0x87, 0x6d, 0x87,
+ 0xdc, 0xed, 0x4c, 0xad, 0xac, 0xf1, 0xc1, 0x61, 0x70, 0xe8, 0xd2, 0x54,
+ 0x44, 0xb1, 0x49, 0xae, 0x25, 0xc9, 0x91, 0x46, 0xde, 0x89, 0x9a, 0x3a,
+ 0xcb, 0xfe, 0xb8, 0xaf, 0xcb, 0x39, 0x4e, 0x30, 0x87, 0xb7, 0x88, 0xcc,
+ 0xdd, 0x7e, 0x01, 0xa2, 0xad, 0x1b, 0x01, 0xd2, 0x6e, 0x47, 0xf6, 0xa5,
+ 0xfb, 0xf7, 0x73, 0x5f, 0x2c, 0x24, 0xd0, 0x9f, 0xe9, 0x8e, 0x49, 0xbb,
+ 0x85, 0x9d, 0x45, 0x8b, 0x63, 0xbb, 0x7a, 0x79, 0xd0, 0xfa, 0x85, 0xe9,
+ 0x4f, 0xad, 0xa2, 0x19, 0xd4, 0xfc, 0x24, 0x89, 0x44, 0x7d, 0x89, 0x33,
+ 0x34, 0x07, 0x25, 0x22, 0x90, 0x96, 0x45, 0x5e, 0x7e, 0xe3, 0x57, 0x3f,
+ 0x57, 0xd3, 0xc7, 0x60, 0xa1, 0x26, 0x14, 0xad, 0x19, 0x51, 0x39, 0x9d,
+ 0x70, 0x48, 0xf4, 0x8b, 0x06, 0xf1, 0xc7, 0x73, 0xe9, 0xe9, 0xca, 0xb0,
+ 0x7d, 0x7c, 0x8f, 0xf4, 0xda, 0x8e, 0x83, 0x99, 0x9d, 0x64, 0x65, 0x82,
+ 0xb3, 0xf1, 0x85, 0xd5, 0x64, 0xfe, 0xb0, 0x1e, 0x3d, 0x1a, 0x30, 0xfd,
+ 0xe1, 0x6b, 0xe1, 0x65, 0x26, 0x93, 0x53, 0x4d, 0x90, 0x7d, 0xe1, 0xc8,
+ 0xe8, 0x50, 0x24, 0x0f, 0x62, 0x73, 0x73, 0x1f, 0x5f, 0xce, 0x9b, 0x36,
+ 0xe9, 0x16, 0xf6, 0x78, 0x8e, 0x5d, 0x3e, 0x99, 0x79, 0x19, 0x36, 0x2f,
+ 0x96, 0x9a, 0x27, 0x34, 0x95, 0x2d, 0x28, 0x22, 0x0f, 0x30, 0x81, 0x49,
+ 0x49, 0xcd, 0x47, 0x24, 0xa5, 0xde, 0xd9, 0x3a, 0x31, 0x9b, 0xf6, 0x84,
+ 0x48, 0xda, 0x0f, 0xbb, 0x87, 0x59, 0xdc, 0xcc, 0x57, 0xf3, 0x02, 0x53,
+ 0xe6, 0xda, 0xfe, 0x97, 0x1e, 0xc7, 0xf8, 0x34, 0xf6, 0x3f, 0xf7, 0x4f,
+ 0x07, 0x5d, 0xe3, 0x77, 0x80, 0x85, 0xb5, 0xa7, 0xff, 0x60, 0x06, 0xd7,
+ 0xec, 0x67, 0xd3, 0x25, 0x31, 0xd9, 0x9b, 0x41, 0x24, 0x93, 0xdf, 0x68,
+ 0x1d, 0x95, 0x42, 0x39, 0x5d, 0xdc, 0xa8, 0x5c, 0x73, 0xfc, 0x08, 0x97,
+ 0x47, 0xa2, 0x0f, 0xc7, 0x1a, 0xc8, 0xea, 0x7e, 0x61, 0x44, 0x64, 0xd9,
+ 0xef, 0x43, 0x3c, 0x88, 0x83, 0x6b, 0x6f, 0x40, 0x70, 0xb1, 0xad, 0x13,
+ 0x3a, 0x8a, 0x80, 0xae, 0xe6, 0xfa, 0xc3, 0x71, 0x7d, 0xd7, 0x3d, 0xfd,
+ 0xdb, 0x36, 0x04, 0x62, 0x92, 0xc6, 0x70, 0x78, 0x49, 0xc3, 0x42, 0x96,
+ 0x94, 0xed, 0x64, 0xc0, 0xf1, 0x95, 0x05, 0x30, 0x4a, 0x1b, 0x13, 0x46,
+ 0x88, 0xf8, 0x3d, 0x51, 0xdd, 0x6a, 0xb9, 0xfa, 0xd7, 0xe2, 0xae, 0xcd,
+ 0xa6, 0x1f, 0x15, 0xef, 0xc7, 0x04, 0x9e, 0x7a, 0x46, 0x02, 0x3e, 0xc8,
+ 0x76, 0xad, 0x9c, 0xba, 0x04, 0xad, 0x58, 0x0c, 0x31, 0x53, 0x15, 0x79,
+ 0x57, 0x23, 0xc0, 0x14, 0xcd, 0x01, 0x78, 0x3f, 0xe8, 0x80, 0x77, 0x8a,
+ 0x57, 0x33, 0xa5, 0xc5, 0x2e, 0xa4, 0xa6, 0xae, 0xd2, 0x2c, 0x64, 0xbf,
+ 0x55, 0xf8, 0x59, 0xf5, 0x81, 0xa0, 0xf4, 0x66, 0x94, 0x25, 0x0f, 0xfb,
+ 0x6b, 0xb8, 0x52, 0x0d, 0xb6, 0x21, 0x08, 0xba, 0xa9, 0x93, 0xac, 0xd6,
+ 0x3b, 0x71, 0xe6, 0xaa, 0xd5, 0x27, 0x68, 0x82, 0xd1, 0x34, 0xdb, 0x80,
+ 0x69, 0x1c, 0x9b, 0x4b, 0xce, 0x9d, 0xa8, 0x5b, 0xc1, 0x62, 0x05, 0xf7,
+ 0xce, 0x87, 0x66, 0xf4, 0x13, 0x4b, 0xca, 0x75, 0x1e, 0x3f, 0xe6, 0x4b,
+ 0xaf, 0x40, 0xe7, 0xf4, 0x91, 0x64, 0x13, 0x60, 0x52, 0xd3, 0x43, 0x28,
+ 0xab, 0x23, 0xd2, 0x9c, 0x05, 0x34, 0x19, 0xf2, 0x53, 0xb7, 0x77, 0xa7,
+ 0x6f, 0x81, 0x36, 0x94, 0xd7, 0x31, 0x52, 0x89, 0x00, 0x6e, 0x08, 0x30,
+ 0x24, 0x38, 0xb3, 0x95, 0x86, 0xfe, 0x91, 0x1f, 0x47, 0xe4, 0x9e, 0xf6,
+ 0x3c, 0x18, 0xdd, 0x38, 0xde, 0x59, 0x6e, 0x6f, 0xc6, 0x2d, 0x3f, 0x81,
+ 0x49, 0x62, 0xfd, 0x2b, 0xe3, 0xab, 0x70, 0xa3, 0x85, 0xdc, 0x78, 0xad,
+ 0x97, 0xa7, 0x08, 0xd6, 0x91, 0x34, 0xb2, 0xe4, 0x12, 0x1c, 0xe3, 0x1e,
+ 0xba, 0xf8, 0x69, 0xbc, 0x07, 0x2c, 0x3b, 0x76, 0xa5, 0xda, 0xc7, 0x85,
+ 0x2f, 0xb0, 0xb7, 0xc3, 0xd8, 0x1a, 0x69, 0x03, 0x76, 0x14, 0x54, 0x88,
+ 0x94, 0x9e, 0xb5, 0x2e, 0xd7, 0x0c, 0x90, 0x92, 0x05, 0x19, 0x1e, 0x31,
+ 0xe7, 0x92, 0xf8, 0x00, 0x29, 0x59, 0x48, 0x68, 0x5a, 0x9f, 0xcd, 0xa6,
+ 0xde, 0x34, 0x23, 0xdf, 0x82, 0xe3, 0x82, 0x1b, 0x33, 0x65, 0x31, 0xa5,
+ 0x57, 0xa8, 0x15, 0x29, 0xcb, 0x6f, 0xab, 0x0c, 0x6b, 0x56, 0x5e, 0x29,
+ 0xf4, 0x20, 0xa3, 0x4e, 0x81, 0xcf, 0x7b, 0x70, 0x06, 0xba, 0x7c, 0x7c,
+ 0x3e, 0x8e, 0x36, 0x9e, 0x71, 0xaa, 0xa8, 0xc7, 0xfb, 0x39, 0xb0, 0x2e,
+ 0xfd, 0xef, 0x2c, 0x7a, 0x20, 0xea, 0xc8, 0x00, 0x62, 0x76, 0x9f, 0xc8,
+ 0x55, 0x3f, 0x6f, 0xd4, 0x0d, 0x15, 0xf8, 0xd2, 0xda, 0xc3, 0x47, 0x0e,
+ 0x9d, 0xa8, 0x4a, 0xe7, 0xff, 0x6a, 0xe7, 0x90, 0x49, 0xba, 0xc5, 0x12,
+ 0x01, 0x73, 0xaa, 0xbe, 0x90, 0xc2, 0x7c, 0xf5, 0xc6, 0xd5, 0x66, 0x2a,
+ 0x73, 0x7d, 0x05, 0x2a, 0xef, 0x2c, 0x50, 0x00, 0x6a, 0xa6, 0x82, 0x48,
+ 0x4f, 0x03, 0x5b, 0x80, 0x8a, 0x38, 0x5c, 0xeb, 0x81, 0xc3, 0x59, 0xf7,
+ 0x46, 0xd9, 0x87, 0x7b, 0xb7, 0x59, 0xe8, 0x00, 0xde, 0x6d, 0x0d, 0x7c,
+ 0x90, 0x78, 0x23, 0xb8, 0xb1, 0xa2, 0x67, 0x7c, 0x6f, 0xd8, 0xe9, 0x2e,
+ 0x6b, 0x94, 0xd7, 0xac, 0x10, 0x9b, 0xa4, 0x13, 0x27, 0xb8, 0x2b, 0x4e,
+ 0x73, 0x88, 0x38, 0x06, 0xa6, 0xa6, 0x67, 0x68, 0xbc, 0xd0, 0x85, 0xaa,
+ 0x14, 0xf9, 0x33, 0x7e, 0xff, 0x4a, 0x22, 0x67, 0x74, 0xfd, 0x64, 0x0c,
+ 0x83, 0xee, 0x9c, 0xa1, 0x0e, 0xe2, 0x9e, 0x68, 0xa8, 0x1e, 0x59, 0x2d,
+ 0x1e, 0x73, 0x37, 0xab, 0x6b, 0x21, 0x9d, 0x91, 0x9d, 0xce, 0x03, 0x30,
+ 0xbe, 0x0d, 0x32, 0x94, 0xa5, 0xf1, 0x28, 0xe1, 0x2f, 0x8b, 0xda, 0x43,
+ 0xf6, 0x2c, 0xf4, 0x2f, 0xd7, 0x1e, 0x9e, 0xa3, 0x09, 0xdb, 0xb3, 0x76,
+ 0xc0, 0xa1, 0xf1, 0x09, 0xc0, 0xaa, 0x77, 0xc4, 0x35, 0x89, 0xfe, 0xbd,
+ 0x84, 0x6f, 0x4c, 0x66, 0xcf, 0x5f, 0x08, 0x28, 0x16, 0xb1, 0x0d, 0xb7,
+ 0xc2, 0xc3, 0x19, 0xf2, 0x0e, 0x6a, 0xba, 0x42, 0x38, 0xdb, 0x13, 0xc2,
+ 0xf4, 0x73, 0xef, 0x80, 0x50, 0x2c, 0x9d, 0x39, 0xa3, 0x16, 0xd7, 0x5c,
+ 0x58, 0xae, 0x5b, 0xbc, 0x56, 0xf0, 0x79, 0x5c, 0xfe, 0xba, 0x1a, 0xc8,
+ 0x37, 0x07, 0x93, 0x06, 0xfe, 0x7c, 0xa4, 0xc3, 0xa4, 0xe5, 0x75, 0x74,
+ 0xbd, 0xe2, 0x62, 0x10, 0x25, 0xd9, 0xae, 0x71, 0x7c, 0x53, 0x67, 0xac,
+ 0x6a, 0x13, 0xec, 0x22, 0x40, 0x62, 0x40, 0xe7, 0xa4, 0xff, 0x6c, 0xd3,
+ 0x75, 0xee, 0x19, 0x52, 0xe4, 0xac, 0x7f, 0x07, 0x41, 0x58, 0x05, 0xb3,
+ 0xdd, 0xd4, 0xd0, 0xca, 0xd6, 0xe7, 0x3e, 0x57, 0x4f, 0x13, 0x7d, 0x20,
+ 0xbf, 0xcb, 0x39, 0xa0, 0x9e, 0xf8, 0x6d, 0x5b, 0x43, 0x6a, 0x79, 0xb9,
+ 0xdc, 0xd7, 0xb3, 0xf0, 0xd1, 0x17, 0x15, 0x01, 0xb2, 0xd4, 0x60, 0xaa,
+ 0x9c, 0x5f, 0xd2, 0xc2, 0xe7, 0x31, 0x07, 0xc5, 0x31, 0x12, 0xd8, 0x34,
+ 0x53, 0x1f, 0x53, 0x85, 0x4b, 0xd7, 0xbf, 0x87, 0x20, 0x86, 0x74, 0x45,
+ 0x77, 0x97, 0xa5, 0x95, 0xc0, 0xde, 0x7f, 0x09, 0xbd, 0x2d, 0x27, 0x17,
+ 0xb8, 0x37, 0xa7, 0xeb, 0xf0, 0xc4, 0x2f, 0xcb, 0x52, 0x8b, 0x39, 0x3f,
+ 0x56, 0x7a, 0x13, 0x30, 0x89, 0xd1, 0xc2, 0x92, 0x53, 0x82, 0x33, 0xef,
+ 0x64, 0xe2, 0xc9, 0x19, 0xfe, 0xdf, 0x8a, 0xf5, 0x60, 0x58, 0x25, 0x98,
+ 0xad, 0xfb, 0xb6, 0xaf, 0x63, 0xc4, 0xe4, 0xd0, 0xe1, 0xf1, 0x82, 0x81,
+ 0x82, 0x8b, 0x87, 0xce, 0x2c, 0xd1, 0x0a, 0xaa, 0x78, 0x88, 0x21, 0x06,
+ 0x44, 0x78, 0xae, 0x16, 0x90, 0xb6, 0xfb, 0x58, 0x85, 0x3a, 0x7d, 0xb3,
+ 0x6c, 0xe9, 0x1d, 0x5f, 0x96, 0x42, 0xcc, 0xea, 0x58, 0x26, 0x37, 0x67,
+ 0x5d, 0x85, 0x09, 0x66, 0x23, 0x9c, 0x26, 0xeb, 0x25, 0x88, 0xae, 0x79,
+ 0xa5, 0x87, 0x3e, 0xad, 0x9c, 0xda, 0x06, 0x1d, 0xc5, 0xba, 0x24, 0xe1,
+ 0x42, 0x0a, 0xae, 0xc0, 0x41, 0xd5, 0xf2, 0x55, 0x4f, 0xdb, 0x96, 0x37,
+ 0xb7, 0xac, 0x8c, 0xc6, 0x74, 0x3a, 0xc5, 0x09, 0x60, 0x2e, 0xe0, 0xe2,
+ 0x39, 0x4c, 0x9d, 0x81, 0x11, 0x40, 0xf1, 0x8a, 0x23, 0x3d, 0xb0, 0x6c,
+ 0x0c, 0xfe, 0x2e, 0x11, 0x84, 0x14, 0x2e, 0xbb, 0x8d, 0x19, 0x6b, 0x20,
+ 0xbf, 0xb9, 0x6f, 0xcd, 0x01, 0xea, 0xe8, 0x36, 0x7a, 0x3c, 0xfc, 0xb3,
+ 0x2c, 0x7d, 0x07, 0x96, 0x29, 0x14, 0x62, 0x23, 0x08, 0x8f, 0x62, 0x90,
+ 0x0f, 0x60, 0x58, 0xe8, 0x81, 0x3d, 0xe0, 0x74, 0x62, 0x8d, 0x84, 0x31,
+ 0x45, 0xce, 0xc6, 0x79, 0x11, 0x5c, 0xf0, 0x04, 0x95, 0x0b, 0x7d, 0xf4,
+ 0x32, 0xcf, 0x85, 0x83, 0x37, 0x1d, 0x7a, 0x64, 0x9a, 0x7c, 0x7b, 0x79,
+ 0x9d, 0x2d, 0x54, 0xd9, 0x5c, 0x9a, 0xed, 0x1d, 0xa6, 0x4d, 0x35, 0x22,
+ 0x36, 0x84, 0xa4, 0x3a, 0x7e, 0x3e, 0x38, 0xca, 0x8b, 0x5d, 0x1d, 0xab,
+ 0x84, 0x2f, 0x00, 0xa6, 0x4b, 0xce, 0xb3, 0x85, 0x7e, 0x17, 0xdc, 0xa9,
+ 0xcc, 0x42, 0xbf, 0x5a, 0x25, 0x69, 0xf5, 0xed, 0xd3, 0x73, 0xfd, 0xf7,
+ 0x56, 0x24, 0x10, 0xbd, 0xcc, 0xe0, 0xdb, 0xe1, 0x32, 0xac, 0x66, 0xcf,
+ 0x69, 0x2c, 0x02, 0x1b, 0x2d, 0x99, 0x66, 0xf1, 0x57, 0xcb, 0x8d, 0x74,
+ 0xa7, 0xa0, 0x05, 0x8e, 0x57, 0x0d, 0xc7, 0xce, 0xce, 0xcf, 0xcc, 0x20,
+ 0xc4, 0x87, 0x42, 0x91, 0xa7, 0x86, 0xdf, 0xbb, 0xe6, 0x56, 0x7a, 0x59,
+ 0xa6, 0x60, 0xe1, 0xfd, 0x73, 0x68, 0x97, 0xf8, 0x40, 0xc9, 0x16, 0xca,
+ 0xb5, 0xa8, 0xbe, 0x37, 0xb0, 0x84, 0x2e, 0x1a, 0xf3, 0x6f, 0x76, 0x06,
+ 0x6a, 0x44, 0xe4, 0xad, 0x23, 0xb5, 0x9c, 0xc6, 0xf2, 0xff, 0xdd, 0xf4,
+ 0xf8, 0xeb, 0x89, 0x7f, 0xf9, 0xc8, 0x87, 0x99, 0x5f, 0x7c, 0x93, 0x86,
+ 0xb6, 0xe1, 0xaf, 0x7b, 0xa2, 0x1a, 0xde, 0x9d, 0x34, 0xe1, 0x73, 0x24,
+ 0x1b, 0xb0, 0x35, 0xb4, 0x3f, 0xb7, 0x61, 0x0a, 0x20, 0x1e, 0xec, 0x2a,
+ 0x4f, 0x06, 0x6b, 0x53, 0x41, 0xd9, 0x5f, 0x69, 0x46, 0xfa, 0x5f, 0x57,
+ 0xd2, 0xfb, 0x75, 0x32, 0xfb, 0xa0, 0x91, 0x8d, 0x05, 0x8f, 0xfe, 0x86,
+ 0x4d, 0x90, 0xa8, 0x74, 0x5a, 0x71, 0x95, 0xd2, 0x13, 0xeb, 0xcf, 0x05,
+ 0xe6, 0x38, 0x6b, 0x89, 0xb4, 0x08, 0xc0, 0x16, 0x95, 0xe9, 0xe9, 0x2f,
+ 0x36, 0x87, 0x3a, 0xb4, 0x42, 0xc4, 0x68, 0x49, 0x65, 0x3a, 0x94, 0xea,
+ 0xa6, 0x63, 0x08, 0x22, 0xf7, 0xa4, 0x4c, 0x81, 0x0b, 0x04, 0xe5, 0xda,
+ 0x5e, 0x28, 0xd6, 0xa3, 0x08, 0x37, 0x59, 0x03, 0xdd, 0x0d, 0xfa, 0x70,
+ 0xb3, 0x77, 0xc9, 0xc2, 0xc6, 0xf6, 0x8c, 0x2d, 0x26, 0x31, 0xec, 0x2c,
+ 0xf1, 0x2d, 0x05, 0xaa, 0x32, 0x2a, 0xe9, 0xb9, 0xc2, 0xd0, 0x82, 0x61,
+ 0x1e, 0xcf, 0xa4, 0x3c, 0x12, 0x16, 0xf0, 0x4d, 0x66, 0xa0, 0x2b, 0xa1,
+ 0xa6, 0x0c, 0xe9, 0x32, 0xce, 0xae, 0x90, 0x7a, 0xf0, 0x74, 0x99, 0x82,
+ 0xc6, 0x39, 0xdb, 0x9b, 0x75, 0xe2, 0xd3, 0x56, 0x36, 0xc0, 0x1d, 0x19,
+ 0xd6, 0x31, 0x10, 0xca, 0xaa, 0xb4, 0x1e, 0x13, 0x63, 0x69, 0xe8, 0xe4,
+ 0xf8, 0x5d, 0x13, 0x7e, 0xa9, 0x5e, 0x64, 0x79, 0x58, 0xc6, 0xf6, 0xc9,
+ 0x1b, 0x50, 0xc6, 0xd5, 0x96, 0xaa, 0x06, 0xd5, 0x90, 0xbd, 0xa9, 0xf3,
+ 0x8a, 0xed, 0x58, 0x78, 0xba, 0x9f, 0x8b, 0x3d, 0x46, 0x6f, 0x55, 0x1d,
+ 0xf9, 0x14, 0x7d, 0x7b, 0xdd, 0x03, 0x78, 0x9d, 0x06, 0x7d, 0xaf, 0x1b,
+ 0x49, 0xa0, 0x82, 0xb3, 0xeb, 0x2f, 0x81, 0x8a, 0x08, 0x8b, 0xbc, 0xce,
+ 0xf5, 0x26, 0xe8, 0x7e, 0xb9, 0xe9, 0x28, 0x57, 0x76, 0x01, 0x61, 0xca,
+ 0x5f, 0xa9, 0x30, 0x7f, 0xa5, 0x16, 0x7f, 0x24, 0xdf, 0x78, 0xf5, 0xc2,
+ 0xd5, 0x91, 0x39, 0x82, 0x7e, 0x8a, 0x6d, 0x5b, 0x88, 0x97, 0xce, 0x9a,
+ 0xf0, 0x8e, 0x0f, 0xce, 0x12, 0x68, 0x60, 0x2a, 0x9b, 0x39, 0x85, 0xdf,
+ 0x39, 0x19, 0x24, 0xe6, 0x46, 0x59, 0xe3, 0x17, 0xb1, 0x8d, 0xec, 0x97,
+ 0x7e, 0x95, 0x0c, 0xfb, 0x48, 0xe5, 0xab, 0xf4, 0xef, 0xc6, 0x63, 0x4f,
+ 0xa3, 0x5e, 0x92, 0x52, 0x2d, 0x23, 0x3e, 0x7c, 0x5e, 0x57, 0xe1, 0x56,
+ 0x26, 0xce, 0xd6, 0xfe, 0x2a, 0xec, 0xec, 0x47, 0xc7, 0x80, 0x4b, 0x83,
+ 0xee, 0xf6, 0x43, 0x06, 0x9a, 0xf8, 0x6b, 0x82, 0x50, 0x0c, 0x03, 0x5d,
+ 0x4d, 0x30, 0x09, 0x00, 0x24, 0x3d, 0x85, 0x39, 0x5d, 0x05, 0x59, 0x28,
+ 0x72, 0x38, 0xea, 0x56, 0xc9, 0xb8, 0xfc, 0xf8, 0x52, 0xfa, 0x3c, 0x0a,
+ 0x1f, 0x5e, 0xad, 0x91, 0xf2, 0xf4, 0x90, 0xb4, 0xc2, 0xb3, 0x4d, 0xdb,
+ 0xef, 0x64, 0xba, 0x89, 0x72, 0x10, 0x0a, 0xa4, 0x84, 0xff, 0x88, 0x38,
+ 0xab, 0x0f, 0x1b, 0x73, 0xfc, 0x3e, 0x92, 0x48, 0x5a, 0xee, 0x70, 0xcb,
+ 0x80, 0x82, 0x88, 0xb0, 0xfa, 0x85, 0xcd, 0xb7, 0xcc, 0x84, 0x25, 0x3f,
+ 0x99, 0x5f, 0xdb, 0xf4, 0x4d, 0x8f, 0x0e, 0x31, 0x96, 0xf9, 0xab, 0x2b,
+ 0x22, 0xdf, 0xcf, 0x5a, 0x81, 0x50, 0x84, 0x0d, 0xc4, 0xde, 0x90, 0xc4,
+ 0x1a, 0x2f, 0xe0, 0x59, 0x1f, 0x39, 0x1b, 0x56, 0x4f, 0x77, 0x26, 0xf3,
+ 0xcb, 0x96, 0xf7, 0x0b, 0x39, 0x23, 0xe0, 0x2c, 0x1c, 0x0a, 0x03, 0x58,
+ 0xd0, 0xd8, 0x7a, 0xce, 0x24, 0xb5, 0x08, 0x95, 0x43, 0x58, 0xbe, 0x37,
+ 0x83, 0xc2, 0x7a, 0xf3, 0xe2, 0x68, 0xd3, 0x29, 0x61, 0xc9, 0xf6, 0x68,
+ 0xc4, 0x9c, 0x13, 0x04, 0xf0, 0xb5, 0x72, 0x8a, 0xdd, 0x50, 0xdf, 0xdf,
+ 0xe5, 0x09, 0x65, 0x61, 0x14, 0x98, 0x91, 0x1b, 0xc8, 0xcc, 0x7c, 0xa3,
+ 0x5d, 0x05, 0x64, 0x13, 0x46, 0xac, 0xdb, 0x64, 0x01, 0x59, 0x84, 0x2b,
+ 0x77, 0xc7, 0xb1, 0xab, 0x0a, 0x09, 0x8a, 0x4c, 0x7d, 0xee, 0x92, 0x44,
+ 0x36, 0x4d, 0xe5, 0xcb, 0x20, 0x39, 0x3b, 0x23, 0xfd, 0xf3, 0x5c, 0x09,
+ 0x30, 0xff, 0x19, 0xaf, 0xe3, 0xe8, 0x46, 0x46, 0x02, 0x62, 0x81, 0x41,
+ 0x3c, 0xd6, 0x23, 0xa0, 0x66, 0x7b, 0x09, 0xed, 0xe4, 0xbe, 0x11, 0x57,
+ 0x4f, 0xb7, 0xbb, 0x72, 0xc9, 0x31, 0x8c, 0xdd, 0x98, 0x1e, 0x60, 0x87,
+ 0xae, 0x49, 0x61, 0x71, 0x17, 0x79, 0xeb, 0x08, 0x64, 0x06, 0x4f, 0x88,
+ 0x55, 0xa6, 0x65, 0xe5, 0x02, 0x64, 0x4a, 0x26, 0xc2, 0x60, 0x07, 0xfb,
+ 0xba, 0x82, 0xad, 0x6e, 0xc1, 0xb2, 0x87, 0xb2, 0xb0, 0x87, 0x43, 0x3d,
+ 0x94, 0xb1, 0xa1, 0xd0, 0xc7, 0xeb, 0xa0, 0xad, 0x02, 0x07, 0x3f, 0x76,
+ 0x99, 0xbc, 0x7d, 0x8c, 0xec, 0x8a, 0x95, 0x5b, 0x84, 0xce, 0xd9, 0x10,
+ 0x0d, 0xec, 0x2d, 0x2e, 0xc3, 0xf6, 0xb3, 0xd3, 0x27, 0xbf, 0x0e, 0x5d,
+ 0x57, 0xf1, 0x7f, 0xeb, 0x86, 0xd3, 0x96, 0xce, 0xde, 0x48, 0xf8, 0xf7,
+ 0xa5, 0x08, 0x22, 0x5b, 0xf5, 0x9d, 0xd5, 0xb4, 0x18, 0x43, 0x40, 0x12,
+ 0x40, 0x61, 0xa9, 0xcc, 0xe2, 0x6e, 0x22, 0x0f, 0x57, 0x2d, 0x56, 0x8d,
+ 0x46, 0xac, 0x93, 0x12, 0xb9, 0xaa, 0x3e, 0x76, 0x87, 0xcf, 0x14, 0x2d,
+ 0x5a, 0x6a, 0x9a, 0x96, 0x3c, 0x88, 0x34, 0x46, 0x03, 0xa0, 0xe4, 0x1e,
+ 0x36, 0x59, 0x3e, 0x04, 0xa9, 0xff, 0xb9, 0x08, 0x55, 0xa7, 0x9b, 0x76,
+ 0x9c, 0xdc, 0x8b, 0xf0, 0x58, 0x76, 0x88, 0x6c, 0xca, 0xc0, 0x39, 0x8a,
+ 0x17, 0x29, 0xf6, 0xcb, 0xa3, 0xd6, 0x1f, 0xb6, 0x91, 0xee, 0xaa, 0xe6,
+ 0xc3, 0x8a, 0x2b, 0x54, 0xd1, 0x47, 0x67, 0x2f, 0x7b, 0x1d, 0xfd, 0xb3,
+ 0xba, 0xb9, 0x44, 0xc9, 0x41, 0xcd, 0x43, 0x49, 0x3f, 0xb3, 0x6b, 0x17,
+ 0xb6, 0x13, 0xae, 0x31, 0x94, 0x37, 0x08, 0x32, 0xf6, 0x3c, 0xcb, 0x3c,
+ 0xe4, 0xf5, 0xa7, 0x2d, 0x86, 0x32, 0x5e, 0x85, 0x82, 0x85, 0x43, 0x9b,
+ 0x35, 0xe6, 0xd5, 0x0f, 0x15, 0x9f, 0xc3, 0x28, 0xd5, 0x36, 0x41, 0x5b,
+ 0x7e, 0x14, 0x08, 0x96, 0x20, 0x6d, 0x18, 0x92, 0xdf, 0xf4, 0xca, 0x26,
+ 0x33, 0xa9, 0x2f, 0xd2, 0xc9, 0xaf, 0xc5, 0xf1, 0xc1, 0x05, 0x82, 0x79,
+ 0x3a, 0x92, 0xd6, 0x0c, 0xcf, 0xc0, 0xb0, 0xe1, 0xb0, 0x05, 0xf2, 0xe2,
+ 0x2c, 0x27, 0xe7, 0x6b, 0x43, 0x0f, 0x7a, 0x22, 0xc9, 0x62, 0xda, 0x61,
+ 0xce, 0xde, 0x15, 0xb5, 0x7c, 0x2b, 0x08, 0x7e, 0x2e, 0xed, 0xf5, 0xff,
+ 0x9c, 0xa6, 0xaf, 0x56, 0xb9, 0xc2, 0x36, 0xd8, 0x4c, 0x83, 0xcb, 0xeb,
+ 0xe1, 0x7f, 0x3a, 0xd7, 0xf7, 0x37, 0xf2, 0x30, 0xa7, 0x38, 0xd2, 0x07,
+ 0xd4, 0x88, 0xc8, 0xc5, 0x29, 0xb4, 0x93, 0xb3, 0xe6, 0xd0, 0x74, 0x0b,
+ 0x14, 0x77, 0x74, 0x97, 0x21, 0xce, 0xa1, 0x71, 0xa3, 0xb5, 0xa6, 0x5f,
+ 0xdb, 0x11, 0xf4, 0xc4, 0xa3, 0x21, 0xf1, 0x9b, 0xc4, 0xee, 0xa5, 0x84,
+ 0x1e, 0x03, 0xd6, 0x5f, 0xc6, 0xf1, 0xf8, 0x0b, 0xc0, 0x6c, 0x1a, 0x31,
+ 0x68, 0x80, 0x79, 0x3f, 0x65, 0x5a, 0xf8, 0x68, 0xf3, 0x69, 0x2e, 0x48,
+ 0x68, 0x04, 0x38, 0xe1, 0xdd, 0xe7, 0x9f, 0x02, 0x8d, 0xea, 0x87, 0x3e,
+ 0x53, 0x26, 0xbe, 0xcc, 0x6f, 0xfa, 0x47, 0x19, 0x84, 0x82, 0xf3, 0xf4,
+ 0x1d, 0x59, 0xe4, 0xb4, 0xe2, 0x68, 0x40, 0xa8, 0x33, 0x39, 0x34, 0x4b,
+ 0x01, 0x38, 0xb7, 0x8b, 0x1f, 0x0d, 0xdc, 0x63, 0x3a, 0x49, 0x61, 0xfa,
+ 0x45, 0x16, 0x7c, 0x3f, 0x38, 0xb6, 0x0d, 0x1d, 0xd8, 0xa7, 0x13, 0xad,
+ 0x04, 0x32, 0xb8, 0x3a, 0x92, 0x39, 0x40, 0x4c, 0x8c, 0xb7, 0xb3, 0x82,
+ 0xb1, 0xbc, 0x37, 0xe9, 0x70, 0xc1, 0x12, 0x5f, 0x42, 0xb8, 0xda, 0x3c,
+ 0x6d, 0xe6, 0x0f, 0xfc, 0x94, 0x5f, 0x6c, 0x62, 0x3c, 0x95, 0xc2, 0x8b,
+ 0xb8, 0x9f, 0x4b, 0xcb, 0xef, 0x96, 0x27, 0x1c, 0xa0, 0x32, 0x64, 0x6d,
+ 0xc6, 0x6c, 0xb4, 0x29, 0x3f, 0x0f, 0xc9, 0xae, 0xea, 0x8b, 0x2a, 0x9e,
+ 0x50, 0xdd, 0x7c, 0x99, 0xb8, 0xd9, 0x73, 0x28, 0x01, 0x5c, 0x6e, 0x5b,
+ 0xe5, 0x1b, 0x6e, 0xa3, 0x0b, 0xc5, 0x70, 0x08, 0x69, 0x7b, 0xfc, 0x1d,
+ 0x40, 0x5a, 0x40, 0x5f, 0x0c, 0xb7, 0xde, 0xaf, 0x9b, 0xdf, 0xe8, 0x27,
+ 0xe4, 0x46, 0x7b, 0x1b, 0xf3, 0x81, 0x0a, 0x64, 0x27, 0x7a, 0x14, 0xc4,
+ 0xdd, 0x3b, 0x1e, 0x89, 0xa9, 0xff, 0xbb, 0x4d, 0xf3, 0x5a, 0x40, 0xd1,
+ 0x4c, 0x96, 0xaa, 0x35, 0xe2, 0x3c, 0x96, 0xf9, 0x57, 0xb9, 0x59, 0x04,
+ 0x33, 0xab, 0xe5, 0x01, 0x2c, 0x27, 0xef, 0xbf, 0x9e, 0x99, 0x8e, 0x05,
+ 0x54, 0x6a, 0xb4, 0x8c, 0x7c, 0x98, 0x22, 0x08, 0xb8, 0x25, 0xa6, 0x85,
+ 0xe8, 0xc8, 0xd9, 0x91, 0x73, 0x5a, 0x15, 0xb2, 0x4b, 0xf5, 0xca, 0xe1,
+ 0x70, 0xdc, 0x93, 0xde, 0x0e, 0xe0, 0x1a, 0x35, 0xeb, 0x69, 0xee, 0x2b,
+ 0xb6, 0xd3, 0x39, 0x69, 0x80, 0x77, 0xd8, 0x96, 0x60, 0x3a, 0xb9, 0x3b,
+ 0xa7, 0xcc, 0x57, 0xb5, 0xaa, 0x57, 0x96, 0xb7, 0xee, 0x65, 0x88, 0xe4,
+ 0xf6, 0xe8, 0xc5, 0xd9, 0x0f, 0xe5, 0xfb, 0xcb, 0x6b, 0x0d, 0x7f, 0x00,
+ 0x2e, 0xe7, 0x77, 0x43, 0x29, 0xf6, 0xbe, 0xe6, 0xf2, 0xac, 0x73, 0x0a,
+ 0x6b, 0xd5, 0x16, 0x45, 0x4a, 0x00, 0x2c, 0x61, 0x2a, 0x21, 0xec, 0xaa,
+ 0x99, 0x99, 0x6f, 0xe1, 0x25, 0x5c, 0x49, 0x13, 0x6b, 0xd1, 0x10, 0x9a,
+ 0x0c, 0x7d, 0x3e, 0x96, 0xf7, 0x65, 0x4d, 0xb3, 0xaa, 0xe9, 0xf0, 0x80,
+ 0x40, 0x39, 0x08, 0xb4, 0x93, 0xbb, 0xd8, 0xf8, 0x49, 0xdd, 0xdc, 0x25,
+ 0x2b, 0xeb, 0x18, 0x43, 0x5f, 0x76, 0xa5, 0x10, 0x7f, 0x75, 0xd0, 0x2e,
+ 0x69, 0x65, 0xe7, 0xc1, 0xa9, 0x80, 0x24, 0xb0, 0x7f, 0xf9, 0x88, 0xcb,
+ 0xfc, 0x83, 0x9f, 0x3f, 0x0b, 0xa4, 0x8e, 0x12, 0x71, 0xae, 0x98, 0x2e,
+ 0xb6, 0x45, 0xde, 0x2e, 0x95, 0x77, 0x11, 0xbb, 0x89, 0x4c, 0x13, 0x66,
+ 0x78, 0x70, 0x97, 0x0b, 0x19, 0x0e, 0x39, 0x44, 0x93, 0x5a, 0x4a, 0xa2,
+ 0xb5, 0x15, 0x7a, 0x05, 0xd4, 0x2c, 0xed, 0x3f, 0x7c, 0x37, 0xa8, 0x95,
+ 0x21, 0x96, 0x9b, 0x82, 0xb7, 0x10, 0xfe, 0x92, 0x2c, 0x17, 0x7f, 0xfc,
+ 0x03, 0x54, 0x81, 0x75, 0x66, 0x01, 0xfd, 0xcc, 0xf7, 0xef, 0x45, 0x86,
+ 0x7c, 0x05, 0xbd, 0x05, 0x13, 0x67, 0x5e, 0xa1, 0x74, 0x5e, 0x3c, 0xd3,
+ 0xdf, 0xdb, 0x03, 0xa9, 0x6e, 0xd3, 0xcb, 0xcb, 0x8f, 0x40, 0xd5, 0x33,
+ 0x93, 0xf1, 0x7c, 0x6a, 0x53, 0x5a, 0x15, 0x60, 0x6c, 0xc7, 0x1d, 0xaf,
+ 0xf8, 0xa1, 0xf7, 0x7d, 0xd3, 0x33, 0x73, 0xe5, 0x83, 0xf3, 0x63, 0x1b,
+ 0xaf, 0xd9, 0xb6, 0xaa, 0xba, 0xe2, 0x2a, 0xdf, 0xe1, 0xee, 0xc5, 0x7b,
+ 0xd4, 0x76, 0x22, 0xbb, 0xa9, 0x83, 0x52, 0x5d, 0x08, 0x4b, 0x38, 0x6a,
+ 0xb8, 0x9f, 0xfa, 0xc7, 0x60, 0x5f, 0x31, 0x86, 0x70, 0x94, 0x8d, 0x6b,
+ 0xdd, 0x21, 0x39, 0xd7, 0xb5, 0x4a, 0x3c, 0x31, 0xb7, 0xa4, 0x44, 0x22,
+ 0x9a, 0xa3, 0x2c, 0x6a, 0xd2, 0xcf, 0xaf, 0xc9, 0x95, 0x25, 0x74, 0xcb,
+ 0x2b, 0x2f, 0xcc, 0x6f, 0xc5, 0xcf, 0xc7, 0xee, 0x7a, 0x07, 0xf7, 0x20,
+ 0xce, 0x42, 0x98, 0x6a, 0x6d, 0xff, 0xeb, 0x81, 0xff, 0xe2, 0x1a, 0xfd,
+ 0xeb, 0x12, 0x57, 0xfa, 0xd4, 0x2e, 0x50, 0xf8, 0x25, 0x49, 0xb2, 0xff,
+ 0x13, 0x20, 0x4b, 0xca, 0xcc, 0x2b, 0xbc, 0xe7, 0xc6, 0x23, 0x60, 0x60,
+ 0xb7, 0xef, 0x5e, 0x94, 0xb9, 0xa2, 0x6b, 0x9c, 0xfd, 0x8c, 0x8e, 0xbf,
+ 0x4c, 0xe5, 0x69, 0xe0, 0xea, 0xab, 0xe6, 0x31, 0x30, 0xf1, 0x75, 0xa4,
+ 0xe9, 0x12, 0x60, 0x7e, 0xf0, 0xf6, 0xd8, 0x28, 0x86, 0xda, 0x51, 0x85,
+ 0xce, 0x55, 0x74, 0x26, 0x9d, 0x25, 0x19, 0x92, 0x1e, 0x37, 0xd7, 0xc3,
+ 0xae, 0x00, 0x1b, 0x7b, 0x4d, 0x88, 0x33, 0x1a, 0xbe, 0x41, 0x44, 0xa6,
+ 0x12, 0x2b, 0x0f, 0xae, 0xab, 0x47, 0xce, 0x6a, 0xf3, 0x53, 0x76, 0x1c,
+ 0xa4, 0xc9, 0x04, 0x83, 0xbd, 0x13, 0x7e, 0xec, 0x19, 0xea, 0xbb, 0x20,
+ 0x79, 0x0b, 0x12, 0x1e, 0x85, 0xcc, 0x7d, 0xa1, 0x1b, 0x06, 0xc2, 0x7f,
+ 0x71, 0x6b, 0x62, 0xcc, 0xe7, 0x39, 0x80, 0xff, 0xc3, 0x50, 0x1e, 0xea,
+ 0x37, 0x35, 0xbd, 0x38, 0x4c, 0x24, 0x8c, 0x62, 0x5b, 0x72, 0xab, 0x1b,
+ 0x24, 0x3a, 0xc9, 0xc0, 0x7f, 0xd3, 0xdd, 0xb9, 0x59, 0x61, 0xd2, 0xad,
+ 0x6b, 0x89, 0x18, 0x5b, 0xa2, 0x15, 0x98, 0xb5, 0xf7, 0x07, 0xe7, 0x10,
+ 0x92, 0x6a, 0x25, 0xb5, 0x9a, 0x48, 0xe9, 0xad, 0xc8, 0x87, 0x95, 0x8d,
+ 0x28, 0x2a, 0x5c, 0xca, 0xc0, 0x42, 0xa1, 0x10, 0x07, 0x1d, 0xe2, 0x8b,
+ 0xde, 0x92, 0xe2, 0xce, 0x23, 0xe7, 0x7b, 0x37, 0x00, 0x87, 0x41, 0x78,
+ 0x30, 0x8b, 0x79, 0x55, 0xa6, 0x72, 0xee, 0x80, 0xd3, 0x55, 0xd1, 0x78,
+ 0x57, 0x76, 0x6e, 0xe5, 0xe9, 0x7e, 0x9e, 0x00, 0xc3, 0xeb, 0x48, 0x4f,
+ 0x61, 0x20, 0xfc, 0x15, 0xa3, 0x27, 0xe7, 0x36, 0x3d, 0xf7, 0xcf, 0xca,
+ 0xa3, 0x89, 0xe6, 0xfa, 0x9f, 0xee, 0x48, 0xbf, 0x26, 0x56, 0xa9, 0x88,
+ 0x3f, 0x9e, 0xa0, 0x65, 0x93, 0xe9, 0x0c, 0x3d, 0x97, 0x4a, 0xf4, 0x8d,
+ 0x47, 0x8e, 0x59, 0x8d, 0x38, 0xa8, 0x32, 0xe1, 0xe0, 0x04, 0x2f, 0xf1,
+ 0xf3, 0xe5, 0xc8, 0xc5, 0x22, 0xb0, 0x7e, 0x29, 0x1e, 0xe1, 0x53, 0x3a,
+ 0x12, 0x02, 0xaa, 0xa2, 0xd9, 0x4e, 0x93, 0xf5, 0x55, 0x6c, 0xa7, 0x1d,
+ 0x6c, 0xe6, 0x6e, 0x37, 0x98, 0xb7, 0x9f, 0x30, 0xa8, 0x38, 0x02, 0xaf,
+ 0x52, 0xde, 0xe7, 0x65, 0xd9, 0xf2, 0xd6, 0xef, 0x27, 0xc1, 0x78, 0xc4,
+ 0xcb, 0x1c, 0x43, 0x18, 0x72, 0x00, 0x0c, 0xf8, 0x11, 0x89, 0x43, 0xe5,
+ 0xf3, 0x5a, 0xa3, 0xc0, 0x85, 0xa0, 0x68, 0x8d, 0xa7, 0xcb, 0x00, 0x2d,
+ 0x5d, 0x2e, 0x6e, 0xad, 0xf3, 0xaf, 0x7c, 0x6c, 0x47, 0x42, 0x48, 0xf9,
+ 0x36, 0xaa, 0x27, 0x9d, 0x7a, 0x03, 0x7b, 0x09, 0x1e, 0x31, 0x57, 0x39,
+ 0x10, 0x27, 0xb8, 0xdd, 0xc9, 0xc8, 0x84, 0x5d, 0x20, 0xc5, 0x10, 0xf0,
+ 0xee, 0xec, 0x29, 0x2f, 0xa3, 0xd6, 0x2a, 0x51, 0x88, 0x21, 0xb3, 0xc7,
+ 0x18, 0x73, 0x1c, 0xb0, 0xee, 0x51, 0x9e, 0xf9, 0x78, 0x51, 0x84, 0x00,
+ 0x37, 0x2d, 0xde, 0x9d, 0xf3, 0xc1, 0x66, 0xf8, 0xf9, 0xac, 0x5c, 0xc4,
+ 0x46, 0xb8, 0x27, 0xb0, 0xab, 0x25, 0x55, 0x04, 0xf0, 0xb0, 0xe9, 0xcd,
+ 0x4f, 0xdf, 0xc6, 0x7a, 0x9c, 0x26, 0x85, 0xec, 0xa4, 0x54, 0x9a, 0xb1,
+ 0x5e, 0xa5, 0xb0, 0xba, 0x72, 0x1f, 0x6d, 0xec, 0xdc, 0xe1, 0x61, 0xe3,
+ 0xb6, 0x66, 0xb7, 0xd8, 0x6b, 0xff, 0x68, 0x9f, 0xc8, 0xfd, 0xd0, 0xfa,
+ 0x3e, 0x0c, 0xae, 0x25, 0x29, 0x95, 0x56, 0x67, 0x44, 0xb7, 0xf0, 0xb4,
+ 0xe8, 0x44, 0x36, 0x2c, 0xe0, 0x27, 0xa9, 0x21, 0xdb, 0x0f, 0xdd, 0x8c,
+ 0xac, 0x29, 0x4a, 0xb2, 0x55, 0xa7, 0x03, 0xe4, 0xfd, 0x78, 0x16, 0xe6,
+ 0xac, 0x21, 0xa8, 0x3a, 0x58, 0xbe, 0x4c, 0xdc, 0x57, 0xa8, 0xdf, 0x2d,
+ 0x6b, 0x4a, 0xf2, 0xe8, 0x84, 0x32, 0x68, 0xf8, 0x4b, 0x4b, 0x4d, 0x11,
+ 0x76, 0x6f, 0xfe, 0xbb, 0x5c, 0x0a, 0x52, 0x82, 0x8d, 0x28, 0xf4, 0x9c,
+ 0xa6, 0x68, 0x95, 0x37, 0x44, 0x04, 0x64, 0xe4, 0xae, 0x0d, 0x17, 0xf4,
+ 0x0c, 0x58, 0x7f, 0xf3, 0x8a, 0xf4, 0xea, 0x04, 0x3c, 0xc7, 0xa0, 0x49,
+ 0x70, 0x8a, 0xb2, 0x1f, 0xfb, 0x73, 0x8e, 0xe9, 0xc2, 0x2d, 0x74, 0xf5,
+ 0x62, 0x3d, 0x0e, 0x7d, 0xfe, 0xa6, 0x67, 0xd5, 0x34, 0x10, 0xa1, 0x7e,
+ 0xf3, 0x81, 0x28, 0x5c, 0xcc, 0x41, 0x98, 0x32, 0x33, 0x06, 0x9b, 0x02,
+ 0x72, 0x72, 0x94, 0xd3, 0x3b, 0x27, 0xcf, 0x7a, 0xb7, 0x26, 0xe5, 0xc4,
+ 0x30, 0x96, 0xae, 0x02, 0x4d, 0xa6, 0x8c, 0xcf, 0x25, 0x1a, 0x0b, 0x5d,
+ 0x17, 0x89, 0x91, 0x55, 0x04, 0x68, 0xd2, 0xb0, 0xb4, 0x1a, 0x61, 0xe1,
+ 0xb7, 0x0d, 0x53, 0x4f, 0x6e, 0xa2, 0xd0, 0x34, 0x5d, 0x25, 0x9d, 0x81,
+ 0xdc, 0x3a, 0xa8, 0xbe, 0x4f, 0xd6, 0x69, 0x91, 0x49, 0xdd, 0x36, 0xc5,
+ 0xbc, 0x19, 0x93, 0xaf, 0xcf, 0xf0, 0xe1, 0x77, 0x1b, 0xaf, 0xe3, 0x0d,
+ 0x1b, 0x71, 0xad, 0xdb, 0x5e, 0x0f, 0xda, 0xa5, 0x53, 0x58, 0xc7, 0xc5,
+ 0x0d, 0xa5, 0xe5, 0x15, 0xea, 0x38, 0x3d, 0xe9, 0xc8, 0x93, 0x69, 0xdd,
+ 0x7f, 0x56, 0xa8, 0xc2, 0x38, 0xda, 0x0d, 0xe7, 0xe4, 0x19, 0x2b, 0xaa,
+ 0x22, 0xb7, 0x8e, 0x83, 0x43, 0xaa, 0x52, 0x64, 0xb0, 0x78, 0x16, 0xdb,
+ 0x15, 0x77, 0xc8, 0x6f, 0x79, 0xab, 0x8c, 0x0f, 0x51, 0x49, 0xab, 0x04,
+ 0xb8, 0xae, 0xec, 0x15, 0xa0, 0x95, 0x35, 0x28, 0xf1, 0x18, 0xc0, 0x7a,
+ 0x53, 0x1f, 0x9f, 0xd3, 0x20, 0x59, 0xa3, 0x33, 0xab, 0x1e, 0x20, 0x21,
+ 0xa1, 0x66, 0x99, 0x12, 0x6d, 0xdc, 0x8b, 0x73, 0xb3, 0x48, 0x83, 0x6e,
+ 0xa8, 0xf9, 0xd5, 0x98, 0x67, 0x6d, 0x77, 0x67, 0xcf, 0x58, 0xc3, 0xf6,
+ 0xd8, 0x03, 0xd8, 0xa7, 0xa6, 0x6b, 0x12, 0xa6, 0x88, 0xb3, 0x8a, 0x9c,
+ 0x72, 0x0a, 0x55, 0x65, 0xa2, 0x81, 0x3b, 0x2f, 0x85, 0x5a, 0xe8, 0xa4,
+ 0x96, 0xb1, 0xd4, 0x19, 0x68, 0x41, 0x1f, 0x0e, 0x1a, 0xd7, 0x3f, 0x78,
+ 0x3a, 0xce, 0x2b, 0x5e, 0x38, 0xe4, 0x24, 0xf5, 0x4c, 0x63, 0x75, 0x62,
+ 0xdc, 0x0e, 0xb4, 0xc7, 0xe0, 0x7f, 0x71, 0x86, 0x64, 0xa1, 0x45, 0x1f,
+ 0x72, 0xc1, 0x45, 0x8a, 0x60, 0x33, 0x21, 0x01, 0xeb, 0xa1, 0x5e, 0x51,
+ 0x4e, 0x85, 0xd8, 0x3e, 0x4b, 0xd2, 0x33, 0x89, 0x09, 0x47, 0xbf, 0xbd,
+ 0xb1, 0xe1, 0x22, 0xd2, 0x0f, 0xec, 0xa5, 0x51, 0x1e, 0xab, 0x96, 0x82,
+ 0xe3, 0x48, 0x45, 0x47, 0x6a, 0xee, 0xca, 0x07, 0x5e, 0xb3, 0xc9, 0x7a,
+ 0xe1, 0xf2, 0x42, 0xba, 0x3b, 0x2e, 0x5a, 0x86, 0x1f, 0x11, 0x4a, 0x1a,
+ 0xe6, 0xb7, 0xc8, 0xd8, 0xc4, 0x85, 0xed, 0x61, 0xf3, 0xf9, 0xd2, 0xdb,
+ 0x33, 0x5c, 0x3e, 0x94, 0xe1, 0xb7, 0x71, 0x69, 0xff, 0x79, 0x25, 0x0f,
+ 0x40, 0xea, 0xc8, 0x79, 0xb6, 0xdb, 0x09, 0xf8, 0xd9, 0x8c, 0xfe, 0x04,
+ 0x3b, 0x9a, 0x2f, 0xf1, 0x18, 0xad, 0xdd, 0xcc, 0xf6, 0x37, 0x3c, 0x41,
+ 0x87, 0x9a, 0xbe, 0x9f, 0x61, 0x92, 0x95, 0x5a, 0x44, 0xeb, 0xb4, 0x27,
+ 0xed, 0xbf, 0x38, 0xef, 0xd4, 0xc1, 0x5f, 0x0d, 0x2c, 0x41, 0x84, 0x92,
+ 0x3e, 0x3e, 0xb9, 0x83, 0x7a, 0xaa, 0x28, 0x25, 0xb0, 0x80, 0x33, 0x17,
+ 0xb7, 0x79, 0x6a, 0xcb, 0x19, 0x9b, 0x78, 0xd6, 0x8e, 0x51, 0x3b, 0x86,
+ 0x2f, 0x9c, 0x28, 0xb4, 0x87, 0x6d, 0x92, 0x2a, 0x48, 0xed, 0xd6, 0xde,
+ 0x94, 0x52, 0xe0, 0xf3, 0x63, 0x36, 0x1d, 0x14, 0x9c, 0xfc, 0x38, 0xf1,
+ 0x9a, 0x4e, 0xde, 0xc9, 0x8f, 0xd4, 0x1a, 0x15, 0xc7, 0x8e, 0x14, 0xb1,
+ 0xbc, 0x4e, 0x6e, 0xad, 0x88, 0xc0, 0x02, 0xd1, 0x02, 0x66, 0xb3, 0x4c,
+ 0x52, 0x8e, 0x3e, 0x48, 0x02, 0x1f, 0xcb, 0x60, 0x37, 0x78, 0x5c, 0x4d,
+ 0x01, 0x50, 0xe8, 0xb9, 0xf2, 0x1b, 0x3b, 0xed, 0x90, 0x50, 0x1e, 0x36,
+ 0x08, 0x1f, 0x5d, 0x78, 0x6c, 0x36, 0xdc, 0xbf, 0x03, 0x95, 0xf3, 0x43,
+ 0x07, 0x01, 0x99, 0xd6, 0x31, 0x4a, 0x1e, 0x9d, 0x39, 0x69, 0x75, 0xaf,
+ 0x8c, 0x7a, 0xb9, 0x90, 0xa0, 0xbd, 0x83, 0xbb, 0x71, 0x21, 0x58, 0xa3,
+ 0x61, 0xa1, 0xb6, 0x33, 0xd2, 0xb1, 0x82, 0x24, 0x59, 0xad, 0x0f, 0xc1,
+ 0x98, 0x06, 0x7c, 0x9d, 0x28, 0x30, 0xd5, 0x64, 0x32, 0x53, 0xa9, 0x38,
+ 0x04, 0x1c, 0xc3, 0x00, 0xc3, 0xa4, 0xfc, 0x58, 0xbb, 0xa2, 0x07, 0x2b,
+ 0xd4, 0x2b, 0xb9, 0xcd, 0x9f, 0x7c, 0x72, 0xb1, 0x2a, 0x76, 0x8a, 0xca,
+ 0x71, 0x73, 0x00, 0xa0, 0x90, 0xd8, 0xaf, 0x79, 0x03, 0x29, 0x26, 0x8f,
+ 0x23, 0xc9, 0x38, 0x41, 0xd4, 0x6a, 0x03, 0x64, 0x91, 0x53, 0x9b, 0x95,
+ 0xbc, 0x88, 0x6d, 0xf9, 0xca, 0xf3, 0x04, 0x69, 0x8a, 0x07, 0x01, 0x4a,
+ 0x3c, 0x1a, 0x6a, 0x82, 0x99, 0x85, 0x7b, 0x69, 0x6b, 0x51, 0x88, 0x00,
+ 0x0b, 0xc9, 0xca, 0x8e, 0x9f, 0x64, 0x89, 0x8d, 0x19, 0x82, 0xa1, 0xf2,
+ 0x3e, 0xf9, 0xba, 0x70, 0x7e, 0xbc, 0x36, 0x45, 0xef, 0x8f, 0x70, 0xe2,
+ 0x94, 0x37, 0xb5, 0xa7, 0x1f, 0xcb, 0x2d, 0x99, 0x7d, 0x30, 0x7f, 0xcf,
+ 0xe2, 0x91, 0xff, 0x96, 0xcb, 0x8e, 0x3a, 0x19, 0xc8, 0xc3, 0xd7, 0xa4,
+ 0x6a, 0x43, 0xf0, 0x8d, 0x0e, 0x77, 0x37, 0x6d, 0x94, 0xbe, 0x0b, 0x68,
+ 0x96, 0x9e, 0xaa, 0x0c, 0xf7, 0x23, 0xae, 0xcb, 0x77, 0x2a, 0x78, 0xaa,
+ 0xf8, 0x03, 0x15, 0x90, 0xc0, 0x6f, 0x24, 0x64, 0x98, 0xf3, 0xb8, 0x8b,
+ 0xaa, 0xf5, 0x16, 0x79, 0x89, 0xb7, 0x29, 0xeb, 0xfe, 0xd9, 0xbe, 0x54,
+ 0xa0, 0x4b, 0xb5, 0x3c, 0xb2, 0xc8, 0x39, 0x44, 0xe0, 0x4a, 0xd4, 0xa6,
+ 0xf2, 0x8a, 0xc5, 0x21, 0xf5, 0xc2, 0x86, 0x5a, 0xcc, 0x16, 0x49, 0x25,
+ 0x38, 0x39, 0x59, 0xea, 0x48, 0x83, 0xa3, 0xb3, 0x47, 0x6a, 0x9f, 0xc7,
+ 0x2b, 0x91, 0x8a, 0x50, 0x78, 0x47, 0xc5, 0x9d, 0x15, 0x2f, 0xda, 0x23,
+ 0xf5, 0xf2, 0xd6, 0x19, 0x17, 0x26, 0x90, 0xf0, 0xb4, 0x7c, 0xee, 0x3b,
+ 0x75, 0xb6, 0xd7, 0xa4, 0x43, 0x1f, 0x9b, 0x89, 0x9f, 0x14, 0x66, 0x92,
+ 0x2f, 0xd2, 0x0e, 0xab, 0x3b, 0x88, 0x01, 0x57, 0x9c, 0xb1, 0x0e, 0xae,
+ 0x89, 0x89, 0x9d, 0x07, 0x55, 0x5f, 0xb6, 0x9b, 0x8c, 0x61, 0x30, 0xfc,
+ 0x85, 0xc9, 0x80, 0x2f, 0x52, 0x1c, 0x05, 0x59, 0x3c, 0xe8, 0x76, 0x34,
+ 0x26, 0xc6, 0x63, 0x6b, 0xa1, 0x82, 0xe8, 0x9a, 0x60, 0x3b, 0x46, 0x55,
+ 0xc3, 0xfc, 0x25, 0x9a, 0x7e, 0x84, 0x55, 0x1f, 0x86, 0x54, 0x7c, 0xc0,
+ 0xa5, 0xa9, 0x54, 0x9a, 0x15, 0x6e, 0xcf, 0xb9, 0x54, 0x5c, 0x60, 0xd5,
+ 0x15, 0x95, 0xb6, 0x5f, 0x5a, 0x76, 0xd0, 0xc0, 0xed, 0xb8, 0xde, 0xe4,
+ 0x70, 0x56, 0xe9, 0xb4, 0x0a, 0xaa, 0x90, 0xfc, 0x69, 0xf1, 0x25, 0xc4,
+ 0xe3, 0x1f, 0x7b, 0x9e, 0x01, 0x6d, 0xad, 0xcd, 0xcd, 0xfc, 0xba, 0x88,
+ 0xdb, 0x91, 0x4b, 0xa8, 0xb1, 0x96, 0xc6, 0xbd, 0xf2, 0x14, 0x61, 0x40,
+ 0xe1, 0x55, 0x33, 0xfc, 0x98, 0x29, 0x90, 0x9b, 0x3d, 0x5e, 0x9c, 0x28,
+ 0xb0, 0x59, 0x6b, 0x2b, 0x4e, 0xca, 0x6b, 0x00, 0x0b, 0xb2, 0xa2, 0xf8,
+ 0x3f, 0x80, 0xda, 0x23, 0x44, 0x1c, 0x4c, 0x7e, 0xba, 0x6e, 0xfd, 0x55,
+ 0x3c, 0x7a, 0x21, 0xb2, 0x5b, 0x3f, 0xe4, 0xa2, 0xbf, 0xf4, 0x17, 0x5f,
+ 0x88, 0x10, 0x03, 0xfc, 0x81, 0xb0, 0x23, 0x98, 0xe7, 0x2a, 0x0e, 0xe0,
+ 0x0b, 0xb9, 0xf5, 0xc4, 0x44, 0xd2, 0xa9, 0x24, 0x1a, 0xab, 0x2a, 0x46,
+ 0x65, 0x1f, 0x7b, 0xf5, 0xfb, 0xfc, 0xf3, 0x8f, 0xf0, 0x27, 0xeb, 0x82,
+ 0x2d, 0x39, 0xff, 0xdf, 0x08, 0xf6, 0xc2, 0x48, 0x49, 0x31, 0xb9, 0x85,
+ 0x11, 0xec, 0x86, 0x80, 0xe3, 0x68, 0x01, 0x82, 0x5f, 0xc1, 0x55, 0xfc,
+ 0x05, 0xc9, 0xb6, 0x5b, 0xac, 0x04, 0xf1, 0x11, 0x48, 0x0e, 0xd0, 0xc3,
+ 0x9f, 0xb2, 0x2c, 0xad, 0x48, 0xe4, 0x19, 0x06, 0x10, 0x5d, 0x7d, 0xa1,
+ 0x65, 0xbf, 0xbf, 0xf7, 0x95, 0x32, 0x4e, 0x33, 0x95, 0x86, 0x9e, 0x1c,
+ 0x1c, 0x17, 0x09, 0x45, 0x21, 0x8e, 0x48, 0xa5, 0x9f, 0x6f, 0xe8, 0x52,
+ 0x0e, 0xc0, 0x37, 0x22, 0x8a, 0x75, 0x9f, 0xff, 0x8b, 0xd3, 0xc0, 0x2b,
+ 0xb8, 0xe8, 0xb9, 0xbd, 0x22, 0x40, 0x92, 0x2e, 0x3e, 0xbf, 0x4c, 0x21,
+ 0x23, 0x24, 0xc1, 0xcc, 0x0d, 0xfd, 0x43, 0xf6, 0xdd, 0xb8, 0x38, 0x70,
+ 0x1c, 0x68, 0x70, 0xb6, 0xc2, 0x92, 0x2e, 0x83, 0xcc, 0xe8, 0xb2, 0xc2,
+ 0x10, 0x02, 0x5e, 0x67, 0x2f, 0xd4, 0xb7, 0x42, 0xbf, 0x70, 0x52, 0xed,
+ 0xaf, 0x3d, 0x3f, 0xe5, 0x6d, 0xbf, 0x72, 0xec, 0x44, 0xf3, 0x69, 0x7c,
+ 0x33, 0x20, 0xf5, 0x20, 0xe8, 0x12, 0x07, 0x00, 0x86, 0xc5, 0xec, 0xee,
+ 0x7b, 0x93, 0xae, 0x2b, 0xb7, 0x08, 0x93, 0x60, 0x81, 0x51, 0xc1, 0xce,
+ 0xec, 0x7f, 0x23, 0x5c, 0x5c, 0xde, 0xcc, 0x4b, 0x3d, 0x8b, 0x63, 0x47,
+ 0xc0, 0x0c, 0x3c, 0x02, 0x98, 0xc9, 0x15, 0xa6, 0xc0, 0x2d, 0xdb, 0xb1,
+ 0xfc, 0x6f, 0x04, 0x2a, 0x41, 0x2a, 0x05, 0xba, 0x17, 0x59, 0xe5, 0x73,
+ 0xdf, 0x3b, 0xb7, 0x18, 0x6d, 0xd2, 0xe2, 0x2a, 0x15, 0x94, 0x5f, 0xd0,
+ 0xfe, 0x04, 0x45, 0x4c, 0xc2, 0xa9, 0x57, 0x6b, 0x06, 0x7f, 0x93, 0x2a,
+ 0xd7, 0x03, 0x12, 0x31, 0x15, 0xc1, 0x78, 0x08, 0xfb, 0x70, 0x35, 0x4b,
+ 0x8a, 0x9f, 0xc7, 0x03, 0xb5, 0x8b, 0xce, 0x2b, 0x4d, 0x71, 0xd0, 0x6d,
+ 0x79, 0x81, 0xf7, 0xec, 0xad, 0xe9, 0x8b, 0xa9, 0x33, 0xbe, 0x69, 0xb9,
+ 0x66, 0x34, 0xc7, 0x2b, 0x67, 0xec, 0x1f, 0x44, 0x4e, 0x75, 0xcc, 0xe3,
+ 0x77, 0xa9, 0x8c, 0x5b, 0x40, 0x58, 0x18, 0x80, 0xfc, 0xe3, 0xdb, 0x2d,
+ 0xfe, 0xfb, 0x40, 0x58, 0x94, 0x17, 0x59, 0xe7, 0x6e, 0x11, 0xaf, 0x76,
+ 0x04, 0xd0, 0x66, 0x9f, 0x36, 0x49, 0xb7, 0x34, 0xac, 0xf4, 0xae, 0x3f,
+ 0x65, 0xe6, 0xea, 0x63, 0xfe, 0x15, 0x4a, 0x52, 0x82, 0x28, 0x88, 0x0d,
+ 0x18, 0xd9, 0xeb, 0x05, 0xa3, 0x14, 0x45, 0x06, 0xba, 0x35, 0x17, 0xba,
+ 0xb9, 0xfc, 0x27, 0xd8, 0x07, 0x18, 0xe5, 0xb6, 0x3b, 0xd3, 0xaa, 0x63,
+ 0x60, 0x8e, 0x2a, 0x47, 0x32, 0x36, 0xef, 0xf6, 0xa3, 0x13, 0x3b, 0x02,
+ 0xba, 0xf4, 0x24, 0x77, 0x34, 0xe9, 0x7a, 0x55, 0x87, 0xf8, 0x57, 0x00,
+ 0x4d, 0xad, 0xd5, 0x1f, 0xdc, 0xca, 0x58, 0x30, 0xdb, 0xe3, 0xb5, 0x9a,
+ 0x7d, 0x42, 0xda, 0x60, 0xcc, 0x1c, 0xd5, 0x7d, 0xa5, 0xcc, 0x38, 0x9b,
+ 0xba, 0xa1, 0x1f, 0x28, 0x67, 0x3d, 0x9e, 0xd3, 0x18, 0x4f, 0xdf, 0xc7,
+ 0x9f, 0x4a, 0x41, 0x4b, 0x3e, 0x16, 0xcb, 0xa6, 0xdb, 0x5c, 0xdf, 0xce,
+ 0x48, 0xac, 0xc6, 0xf5, 0xc8, 0x6a, 0x68, 0xdc, 0xf4, 0xb4, 0xfa, 0xcb,
+ 0xf4, 0x90, 0x33, 0x5c, 0x43, 0xea, 0xe7, 0xbe, 0x30, 0xaf, 0xf3, 0xaa,
+ 0xed, 0x95, 0x88, 0x47, 0x83, 0x56, 0x65, 0x5e, 0x6d, 0x3e, 0xe7, 0x00,
+ 0x24, 0x2d, 0xd5, 0xe7, 0xeb, 0x22, 0x5b, 0x86, 0x42, 0xc6, 0x41, 0x55,
+ 0x7a, 0xea, 0xd1, 0x94, 0x04, 0xb5, 0xea, 0x7f, 0x4c, 0x49, 0xcf, 0xb2,
+ 0x25, 0x4f, 0x2d, 0x9f, 0xf5, 0xde, 0xf3, 0xe4, 0x5f, 0x3d, 0xbb, 0xd4,
+ 0x67, 0x74, 0xed, 0x05, 0xf2, 0xc5, 0x50, 0x45, 0xef, 0x65, 0x57, 0xf8,
+ 0x63, 0x1a, 0x6a, 0x64, 0x3f, 0xc1, 0xb9, 0xb4, 0xda, 0x3c, 0x93, 0x62,
+ 0xf1, 0xe0, 0xc6, 0x6c, 0x30, 0x1e, 0x4a, 0x26, 0x65, 0x6f, 0xae, 0x6c,
+ 0xb2, 0xc3, 0xd1, 0x03, 0xa4, 0x4a, 0x6a, 0xcf, 0x31, 0x40, 0xa3, 0x40,
+ 0xa5, 0x39, 0x90, 0x9b, 0x33, 0x09, 0xa0, 0x62, 0x94, 0x9a, 0x00, 0xec,
+ 0xc5, 0x58, 0x46, 0x93, 0x67, 0x78, 0xa4, 0xf4, 0x6b, 0x25, 0x0d, 0x10,
+ 0x4b, 0x5c, 0xd1, 0xc5, 0xf2, 0x2a, 0x0b, 0xc1, 0x46, 0xa2, 0xd0, 0x37,
+ 0x9b, 0x12, 0xae, 0x5c, 0x5f, 0x7f, 0xdb, 0x96, 0x15, 0xf0, 0xc1, 0x4a,
+ 0xdb, 0x05, 0x85, 0xc1, 0x08, 0xc9, 0x40, 0x8c, 0x85, 0x7b, 0x6f, 0x2a,
+ 0x6b, 0x9d, 0xa9, 0x9d, 0xff, 0x54, 0xa5, 0xdb, 0xef, 0x45, 0x4f, 0xc2,
+ 0x88, 0xb4, 0xd3, 0x86, 0x33, 0x8e, 0xe9, 0x0f, 0xb3, 0x6d, 0xe8, 0x6f,
+ 0x88, 0xa3, 0xaf, 0xb0, 0x89, 0x3e, 0x1e, 0x1d, 0x8d, 0x10, 0x86, 0x4e,
+ 0xf1, 0x59, 0x85, 0xe2, 0x66, 0xcc, 0x5c, 0x6f, 0x6a, 0xce, 0xce, 0x60,
+ 0xcf, 0x15, 0xeb, 0x1d, 0x8d, 0xaa, 0x67, 0xf6, 0xf8, 0xb0, 0xd9, 0x0c,
+ 0x01, 0xa2, 0x37, 0x6c, 0x8f, 0xb8, 0x79, 0x4f, 0xc3, 0xeb, 0x46, 0x54,
+ 0xc2, 0x85, 0x54, 0xe4, 0x2b, 0x52, 0x3f, 0x10, 0x8e, 0xba, 0x47, 0xfc,
+ 0x5b, 0x0b, 0x41, 0x17, 0xad, 0x5c, 0x07, 0xf5, 0x0c, 0xc3, 0x03, 0xcc,
+ 0x35, 0x9d, 0x64, 0x2e, 0x2d, 0xbb, 0xdc, 0x96, 0x02, 0xb4, 0x52, 0xb8,
+ 0xe9, 0x77, 0x08, 0x19, 0x87, 0x86, 0xe2, 0xdc, 0x82, 0x7f, 0x7d, 0xfe,
+ 0xac, 0x06, 0x98, 0xd8, 0x3b, 0x7f, 0xbf, 0xdb, 0x35, 0x70, 0x48, 0xeb,
+ 0xd1, 0xfb, 0xc4, 0xa4, 0x42, 0x9d, 0x92, 0xbe, 0xa8, 0x4a, 0x25, 0xa7,
+ 0x78, 0xd7, 0xa1, 0x83, 0x03, 0xeb, 0xc7, 0xb2, 0xc3, 0x46, 0x7f, 0xc4,
+ 0x73, 0xbf, 0xf5, 0xe3, 0x1e, 0x91, 0x3d, 0x01, 0xcd, 0x32, 0xc9, 0x25,
+ 0xd9, 0x8f, 0x7e, 0x4a, 0x4f, 0xb7, 0x95, 0xc0, 0x73, 0xd0, 0x2f, 0xd6,
+ 0x0d, 0x48, 0xcc, 0xc7, 0xd6, 0x05, 0x0a, 0xe3, 0xca, 0x8c, 0x0b, 0x6b,
+ 0xb4, 0x1a, 0x45, 0xdc, 0x8a, 0xae, 0xea, 0xae, 0xc7, 0xd3, 0x1e, 0x3b,
+ 0xc3, 0xff, 0x6d, 0x20, 0x07, 0x82, 0x80, 0x6a, 0x10, 0x36, 0x30, 0x71,
+ 0xb0, 0x68, 0x56, 0x81, 0x1c, 0x6b, 0x98, 0x7a, 0xac, 0x1d, 0xd6, 0x2b,
+ 0xb8, 0x73, 0x21, 0x1b, 0x70, 0xd3, 0x5a, 0xe1, 0x0e, 0x14, 0x90, 0x8d,
+ 0x5c, 0xa6, 0xd4, 0x2a, 0xf8, 0xd3, 0x6d, 0x9f, 0x4f, 0x33, 0x92, 0x7d,
+ 0xee, 0x9d, 0x2f, 0x72, 0xf5, 0x93, 0xfc, 0x31, 0x25, 0xaa, 0xd4, 0xef,
+ 0x1a, 0xd9, 0xd6, 0x1f, 0xa1, 0x39, 0x70, 0xdb, 0xf8, 0xd6, 0x02, 0xad,
+ 0x9e, 0xe0, 0xd3, 0x40, 0x68, 0x30, 0x11, 0xe8, 0x93, 0x37, 0x50, 0x72,
+ 0x19, 0x3a, 0xab, 0x65, 0x9c, 0x28, 0xc9, 0x67, 0x0a, 0x6b, 0xf0, 0x80,
+ 0x58, 0x2c, 0xc4, 0x67, 0x06, 0x88, 0x4f, 0x6d, 0xa9, 0xfc, 0x9e, 0xe8,
+ 0xfc, 0xde, 0xd8, 0xb1, 0xe0, 0xcb, 0x32, 0x9c, 0x79, 0x89, 0x8e, 0x2d,
+ 0x0b, 0xe1, 0xae, 0x09, 0x77, 0x97, 0xe2, 0xf2, 0x36, 0x47, 0x0e, 0x87,
+ 0xe8, 0x67, 0x7e, 0xe7, 0x33, 0x8d, 0xc1, 0x61, 0x41, 0xa0, 0xc8, 0x05,
+ 0x8d, 0x80, 0x26, 0xe6, 0x29, 0x3c, 0x00, 0x50, 0x79, 0x25, 0xd1, 0xb3,
+ 0xa4, 0x0a, 0x75, 0x83, 0x4b, 0xf0, 0x6b, 0xbc, 0xf1, 0x24, 0xe0, 0xa8,
+ 0x5b, 0x48, 0xf0, 0x49, 0x88, 0x66, 0x7a, 0xeb, 0x0e, 0x95, 0x88, 0x64,
+ 0xe4, 0x46, 0x13, 0xfe, 0xe3, 0xbe, 0x74, 0xf3, 0xb6, 0x7b, 0x45, 0x2d,
+ 0xc1, 0x37, 0x93, 0x52, 0xb9, 0xf0, 0x1f, 0xc6, 0xe2, 0x48, 0x1a, 0xfd,
+ 0xf9, 0x69, 0x71, 0x12, 0xf1, 0x98, 0x9b, 0x03, 0xce, 0x02, 0x58, 0xf9,
+ 0xd4, 0xb3, 0x20, 0x63, 0xd5, 0x5e, 0x1f, 0x30, 0x65, 0x73, 0x08, 0x81,
+ 0x41, 0x89, 0x5b, 0x7e, 0x1a, 0x2f, 0xc3, 0x9f, 0x5d, 0x6a, 0x61, 0xdb,
+ 0x83, 0x15, 0xda, 0x95, 0xb7, 0x4c, 0x96, 0x63, 0xd3, 0x1b, 0x9d, 0xfe,
+ 0x0b, 0x11, 0xf0, 0x60, 0x01, 0x7b, 0xf5, 0x01, 0x26, 0xbd, 0xad, 0x61,
+ 0xbb, 0xb8, 0x1c, 0x8f, 0x03, 0xe6, 0x8c, 0x39, 0xa6, 0x4f, 0x59, 0xbb,
+ 0x56, 0xa2, 0x5f, 0xe4, 0x19, 0x23, 0x23, 0xf0, 0x9c, 0x52, 0xd4, 0x41,
+ 0x5b, 0xd3, 0xc1, 0xd9, 0x26, 0x96, 0xe5, 0xf4, 0xdf, 0x64, 0x7e, 0xa3,
+ 0xea, 0x67, 0xa3, 0x39, 0x0d, 0x64, 0x77, 0xc7, 0xe5, 0x67, 0x87, 0xd5,
+ 0xd3, 0x71, 0x98, 0xb9, 0x25, 0xa1, 0x0b, 0x64, 0xa1, 0x3b, 0x69, 0x34,
+ 0xea, 0x73, 0xca, 0xeb, 0x2d, 0x9c, 0x29, 0x3d, 0xdd, 0x6d, 0x48, 0x28,
+ 0xd8, 0x96, 0x25, 0x7e, 0x0b, 0x86, 0xda, 0x1f, 0x3b, 0xbf, 0x05, 0x60,
+ 0xa8, 0xbe, 0x46, 0xb2, 0x4d, 0x1d, 0x1f, 0x4e, 0xba, 0xf9, 0x13, 0x8f,
+ 0x7b, 0x51, 0x48, 0xb7, 0xd7, 0x2f, 0x70, 0x35, 0xaa, 0x9a, 0xa0, 0xd7,
+ 0xc3, 0x8a, 0x46, 0x66, 0xd9, 0xd0, 0x6f, 0x63, 0x65, 0x12, 0x04, 0xd8,
+ 0xa5, 0x6b, 0x03, 0x3f, 0x90, 0x5c, 0x1d, 0xad, 0xf0, 0xdb, 0x2b, 0x01,
+ 0x16, 0x39, 0xf5, 0x07, 0xa0, 0x3a, 0x38, 0x20, 0x69, 0x1c, 0x91, 0x6b,
+ 0xec, 0x52, 0x54, 0x1f, 0x7e, 0xd9, 0x30, 0x00, 0x39, 0x9e, 0x07, 0x31,
+ 0x84, 0x2f, 0xe7, 0x81, 0x77, 0xe6, 0x5e, 0xcc, 0x59, 0x8e, 0x7c, 0x98,
+ 0x5a, 0xea, 0x63, 0x89, 0x95, 0x59, 0xdd, 0xcf, 0xac, 0x0a, 0x5c, 0x78,
+ 0x11, 0x2f, 0xa1, 0xf5, 0xc8, 0xf0, 0x2b, 0xb6, 0x01, 0xa5, 0x8a, 0x83,
+ 0xac, 0x78, 0x3a, 0x74, 0xcf, 0xb6, 0x99, 0x3c, 0x87, 0xc4, 0x8e, 0x4c,
+ 0x49, 0xbd, 0xff, 0x63, 0x5b, 0x1d, 0xb9, 0xe9, 0xf5, 0x24, 0x69, 0x28,
+ 0xbb, 0x1d, 0x9e, 0x71, 0x9d, 0xaa, 0xcf, 0x8f, 0x6e, 0xd6, 0x82, 0xce,
+ 0x5b, 0x35, 0x7d, 0xc3, 0x1b, 0x5d, 0x8c, 0x75, 0x77, 0x46, 0xa5, 0x3b,
+ 0x73, 0x3e, 0x1a, 0x2e, 0xcc, 0x23, 0x2f, 0xf6, 0x57, 0x21, 0xfe, 0x8f,
+ 0xcb, 0x04, 0x4d, 0x7d, 0xa3, 0x51, 0x57, 0x4b, 0xee, 0xdf, 0xfb, 0xbe,
+ 0x5b, 0x90, 0xc6, 0x36, 0x66, 0x99, 0x92, 0xd2, 0xea, 0xbf, 0xea, 0xd0,
+ 0x11, 0xea, 0x89, 0x8b, 0x1b, 0x82, 0x7d, 0x8d, 0x4a, 0x51, 0xbb, 0x49,
+ 0x6a, 0xaf, 0x74, 0x43, 0x14, 0xea, 0x02, 0xdc, 0xc1, 0x9e, 0xe1, 0x30,
+ 0x9b, 0xdb, 0x65, 0x6a, 0x93, 0x6d, 0x8b, 0x90, 0x9f, 0x4e, 0xec, 0xd3,
+ 0x7d, 0x26, 0xbb, 0xfd, 0x93, 0xb6, 0x64, 0x8d, 0xb8, 0x2c, 0x90, 0x10,
+ 0xfe, 0xdd, 0x1c, 0x3f, 0xe2, 0xa0, 0xad, 0x9f, 0xa8, 0xa0, 0x37, 0x56,
+ 0x5a, 0x36, 0xfa, 0x89, 0x47, 0xbb, 0x05, 0x5e, 0x23, 0x2c, 0x92, 0xdb,
+ 0x81, 0x3d, 0x13, 0x7a, 0x5f, 0xd3, 0x27, 0x2f, 0xf7, 0xcb, 0x73, 0xe2,
+ 0x4a, 0x84, 0x53, 0x6c, 0x8d, 0x0c, 0x5e, 0x54, 0x4d, 0xfb, 0x00, 0x8b,
+ 0x55, 0xf1, 0x36, 0xcf, 0x0f, 0x80, 0x2c, 0xc7, 0x48, 0x79, 0xe5, 0x4c,
+ 0x54, 0x6a, 0x8a, 0xf1, 0x75, 0x53, 0x88, 0xbb, 0xd3, 0xc6, 0x83, 0xf8,
+ 0x26, 0xc2, 0xd3, 0x76, 0x7d, 0x79, 0xd8, 0x65, 0x90, 0x3e, 0xaa, 0x2c,
+ 0xc6, 0x81, 0x81, 0x15, 0x73, 0xbf, 0xf3, 0x2e, 0xf9, 0xdf, 0x11, 0x5b,
+ 0x3d, 0x98, 0xea, 0x82, 0x39, 0x8c, 0xb1, 0xbd, 0xef, 0x2b, 0x91, 0xfb,
+ 0x16, 0x59, 0x41, 0xf8, 0x3d, 0x4b, 0xac, 0xb3, 0xf2, 0x02, 0x4e, 0xd4,
+ 0x31, 0x57, 0xde, 0xf6, 0xe6, 0x1f, 0x50, 0x26, 0x68, 0xda, 0xbc, 0x03,
+ 0xee, 0xe1, 0x36, 0x8f, 0x2b, 0x10, 0xb3, 0xd1, 0xb6, 0x14, 0x00, 0xfd,
+ 0xea, 0x8a, 0xd7, 0xc3, 0xcf, 0x6f, 0x3c, 0x73, 0xf9, 0x7e, 0xb7, 0xd4,
+ 0x96, 0xa5, 0x4d, 0x06, 0x4b, 0xd3, 0x2b, 0xec, 0xf8, 0x4f, 0x79, 0x57,
+ 0x36, 0x70, 0x78, 0x9d, 0x95, 0x26, 0x56, 0xf6, 0xc7, 0x11, 0x78, 0xef,
+ 0x93, 0xa3, 0x6a, 0xc5, 0xad, 0x33, 0x67, 0x82, 0x6b, 0x8b, 0x3c, 0xce,
+ 0xcd, 0x31, 0x3b, 0xa7, 0x07, 0x88, 0xb4, 0x1d, 0x19, 0x16, 0x7a, 0x78,
+ 0x4d, 0xec, 0x00, 0x61, 0x22, 0x73, 0x63, 0x37, 0xfa, 0x1d, 0x5f, 0xc1,
+ 0xce, 0xf4, 0xa9, 0xa9, 0xfc, 0x86, 0x85, 0x46, 0xd8, 0x37, 0xbe, 0xc1,
+ 0x23, 0x25, 0xf8, 0x9f, 0xeb, 0xaa, 0x17, 0x1b, 0xce, 0x03, 0x42, 0x66,
+ 0x7a, 0xb5, 0x36, 0x58, 0x4f, 0x97, 0xc3, 0x54, 0x43, 0x54, 0xcf, 0x6c,
+ 0xd3, 0xc2, 0xed, 0x0d, 0x9f, 0xc2, 0xb0, 0xd1, 0x7f, 0x87, 0xd1, 0x30,
+ 0xcf, 0x4e, 0xf1, 0x4b, 0x45, 0xe5, 0xff, 0x76, 0x55, 0x3c, 0x9e, 0x39,
+ 0x8b, 0x23, 0xfa, 0xef, 0xbc, 0x79, 0xc5, 0xab, 0xea, 0xdf, 0x2f, 0xf4,
+ 0x80, 0xcc, 0x7f, 0x85, 0x17, 0x05, 0x56, 0x8a, 0x38, 0x25, 0xab, 0x40,
+ 0x6d, 0x29, 0xcd, 0xf3, 0x82, 0xcd, 0x20, 0xfb, 0xfb, 0xda, 0x60, 0x6f,
+ 0xd4, 0x32, 0x8a, 0x12, 0x45, 0xbb, 0xea, 0x29, 0x65, 0x81, 0xc6, 0x90,
+ 0x0d, 0x31, 0x29, 0xc3, 0xd5, 0xeb, 0x43, 0x8f, 0x0e, 0xe5, 0x7a, 0xd6,
+ 0x64, 0x1c, 0x7c, 0xe2, 0x38, 0xe4, 0x9c, 0x5b, 0xe1, 0x78, 0xca, 0x88,
+ 0x32, 0x0a, 0xc3, 0x5f, 0xb6, 0x7a, 0x94, 0x15, 0xdf, 0x69, 0x10, 0x16,
+ 0xca, 0xb9, 0xd3, 0x96, 0x94, 0xd2, 0x19, 0x68, 0x5c, 0x6b, 0xce, 0x78,
+ 0xa3, 0x79, 0x6c, 0x05, 0xbe, 0x67, 0xb9, 0xc2, 0x27, 0x85, 0x1c, 0x57,
+ 0x2e, 0x9c, 0xbe, 0x4e, 0x6e, 0xb1, 0xad, 0x56, 0x8e, 0xa7, 0xcc, 0x69,
+ 0x7b, 0x5b, 0x6b, 0x12, 0x80, 0x80, 0x67, 0x6c, 0x39, 0x89, 0x07, 0xee,
+ 0x8f, 0x2a, 0x1c, 0xc1, 0x35, 0x50, 0xa9, 0xb9, 0x64, 0x59, 0x0b, 0x80,
+ 0xe9, 0xbc, 0xd6, 0x36, 0x13, 0xfa, 0xf0, 0xe6, 0x73, 0x45, 0x8c, 0x7b,
+ 0x02, 0x3f, 0x9c, 0x36, 0xa5, 0x6f, 0xd4, 0x8e, 0x88, 0x56, 0xef, 0x6a,
+ 0x10, 0x70, 0x6d, 0x40, 0x26, 0xab, 0x81, 0xfd, 0x4d, 0xcd, 0x4a, 0x4a,
+ 0x0a, 0xb8, 0x86, 0x0a, 0x20, 0x32, 0x23, 0x0e, 0x1e, 0x40, 0x78, 0x50,
+ 0x01, 0x7e, 0x64, 0xf1, 0x9c, 0x4d, 0x98, 0x83, 0x62, 0x92, 0xdb, 0xe4,
+ 0x57, 0x5a, 0xa2, 0xbf, 0xa5, 0x85, 0xd1, 0x46, 0x12, 0xb7, 0x29, 0x09,
+ 0x51, 0x15, 0xe2, 0x3c, 0x04, 0x4c, 0xdc, 0x10, 0xd5, 0x19, 0x03, 0x6b,
+ 0xee, 0xfd, 0x3c, 0x8e, 0xd4, 0xb1, 0x32, 0x0b, 0xd2, 0x71, 0x95, 0xef,
+ 0x99, 0x50, 0xbb, 0xb6, 0x2d, 0x51, 0x9a, 0x5f, 0x0e, 0x80, 0x4f, 0x79,
+ 0x15, 0xe9, 0x4d, 0xdb, 0xcc, 0x5d, 0x3c, 0x4b, 0xd6, 0x7a, 0xdb, 0x1b,
+ 0x2e, 0x2a, 0x2e, 0xd6, 0x8f, 0xaf, 0x43, 0xa1, 0xaf, 0xbe, 0x52, 0xd5,
+ 0xaf, 0xe2, 0xd4, 0x68, 0x59, 0xec, 0x81, 0x88, 0xa5, 0x61, 0xb5, 0x41,
+ 0x29, 0x0a, 0xeb, 0x61, 0x93, 0x0a, 0xaf, 0xae, 0x08, 0x6f, 0x0a, 0xb9,
+ 0x99, 0x6e, 0x6e, 0x94, 0x80, 0xfd, 0x14, 0x46, 0xc7, 0xd3, 0x9f, 0xe4,
+ 0xee, 0x15, 0x24, 0xf2, 0x36, 0x53, 0xf6, 0x63, 0xba, 0x51, 0x34, 0x71,
+ 0x2b, 0xca, 0x5d, 0xe6, 0x6c, 0x5b, 0x5c, 0x6c, 0xbd, 0x2c, 0x96, 0x39,
+ 0xe2, 0x0a, 0x99, 0x00, 0x88, 0xaf, 0x03, 0x48, 0x27, 0xb8, 0x1b, 0x5b,
+ 0x95, 0x7f, 0x83, 0x7b, 0xf6, 0x76, 0x8d, 0x60, 0x02, 0x05, 0x99, 0x0a,
+ 0xaf, 0xda, 0xfb, 0xe1, 0x45, 0x55, 0x46, 0x04, 0x24, 0xd3, 0x7c, 0x9f,
+ 0x8b, 0x40, 0x3d, 0x26, 0x6e, 0xc9, 0xfe, 0x7d, 0xa3, 0xc9, 0x8c, 0x5a,
+ 0x5b, 0xc6, 0xb4, 0x5b, 0xe6, 0xe4, 0xfb, 0x1c, 0x90, 0x7a, 0x17, 0x9f,
+ 0x4c, 0xaa, 0xff, 0x45, 0x67, 0x3a, 0x1a, 0xeb, 0xa1, 0xa0, 0x2a, 0x27,
+ 0xa6, 0xfa, 0x01, 0x41, 0x9a, 0x7a, 0xb3, 0x8b, 0x41, 0x2b, 0xe2, 0x9d,
+ 0x24, 0xa1, 0x9e, 0xd2, 0xfd, 0x81, 0x9b, 0x2f, 0x10, 0x28, 0x94, 0xda,
+ 0x7b, 0x20, 0x59, 0x01, 0xfd, 0x2d, 0x2d, 0xbc, 0x34, 0xb4, 0xcd, 0xd6,
+ 0x93, 0xae, 0xdb, 0x01, 0x86, 0x29, 0xa8, 0x2a, 0xf9, 0x9d, 0x72, 0x06,
+ 0xdb, 0x59, 0x72, 0x06, 0xbd, 0x62, 0x0f, 0x59, 0x0d, 0xd0, 0x36, 0x75,
+ 0xed, 0xde, 0xf9, 0x22, 0x92, 0x99, 0x34, 0x34, 0x01, 0xb4, 0x76, 0xb5,
+ 0x74, 0x08, 0xfe, 0x0a, 0x19, 0x1d, 0xda, 0xca, 0x7d, 0x06, 0x8c, 0x60,
+ 0x77, 0x85, 0x75, 0x0f, 0x75, 0xd1, 0x97, 0x28, 0xca, 0xaf, 0xba, 0xc8,
+ 0x35, 0xcc, 0xa9, 0x2d, 0xb1, 0xbc, 0x1d, 0x0e, 0xbe, 0x4b, 0x68, 0xa8,
+ 0x4b, 0x00, 0xc3, 0xbe, 0x0e, 0x78, 0xdc, 0x12, 0x80, 0xc6, 0x75, 0x20,
+ 0xf9, 0xb0, 0x05, 0x24, 0xde, 0x3d, 0x25, 0x34, 0x44, 0x9c, 0xb9, 0x66,
+ 0x70, 0x4a, 0xba, 0xa9, 0x3b, 0xe3, 0x80, 0xe2, 0xad, 0x35, 0x4d, 0x0d,
+ 0xe4, 0xbb, 0x4f, 0x7b, 0x97, 0x32, 0x4b, 0x8d, 0x83, 0x13, 0xb8, 0x46,
+ 0x59, 0xbb, 0x12, 0xe3, 0xce, 0x44, 0xb4, 0xbe, 0x8a, 0x79, 0xd0, 0x3c,
+ 0xfe, 0x77, 0x9e, 0xc7, 0xa7, 0x69, 0xc8, 0x39, 0x60, 0xfd, 0x77, 0x05,
+ 0x5a, 0x93, 0xa2, 0x11, 0xec, 0x9e, 0xe5, 0x6d, 0x90, 0xb7, 0x7e, 0x36,
+ 0x8e, 0x13, 0x07, 0xb8, 0x33, 0x70, 0x97, 0xab, 0x4b, 0x25, 0xb3, 0xa3,
+ 0xfa, 0x17, 0x71, 0x22, 0xa7, 0xda, 0x29, 0x66, 0xe2, 0x19, 0x19, 0x3d,
+ 0x08, 0x87, 0xa1, 0xb5, 0x93, 0x30, 0x7a, 0x85, 0x23, 0x64, 0xf4, 0x4c,
+ 0x06, 0x42, 0x2d, 0x44, 0x0b, 0xb1, 0xa2, 0x6d, 0xff, 0x1b, 0xa1, 0x19,
+ 0xd7, 0xe5, 0x88, 0x2e, 0x97, 0x60, 0xbf, 0xa9, 0x79, 0x25, 0x8d, 0xec,
+ 0x05, 0x34, 0xfc, 0x7e, 0x5f, 0xb9, 0xf7, 0x6d, 0x64, 0x55, 0x81, 0x5f,
+ 0x45, 0x98, 0xec, 0x1d, 0x22, 0x8c, 0xb9, 0x72, 0x3c, 0x4f, 0x85, 0x75,
+ 0x01, 0x19, 0xb7, 0x19, 0xa9, 0xda, 0x49, 0x01, 0x58, 0x25, 0xb6, 0x1b,
+ 0x6e, 0x6b, 0xaa, 0x5d, 0x87, 0xff, 0xd1, 0xf3, 0x97, 0x05, 0x3d, 0x11,
+ 0xc5, 0x87, 0x68, 0x12, 0x4d, 0x0c, 0x4a, 0x7d, 0x68, 0x23, 0x12, 0xe9,
+ 0xa3, 0xe6, 0xbb, 0xaf, 0x43, 0xc2, 0x4b, 0xb4, 0x75, 0xfb, 0xe9, 0xbd,
+ 0x02, 0xf3, 0x6f, 0x6f, 0x8b, 0x60, 0x2e, 0x0a, 0xc9, 0x4c, 0xfc, 0xdd,
+ 0xe2, 0x64, 0x86, 0x50, 0x48, 0xfc, 0xdf, 0x46, 0x0a, 0x7a, 0x39, 0xd8,
+ 0x7b, 0x79, 0x4d, 0xbf, 0x31, 0x36, 0xd9, 0x0f, 0x56, 0x85, 0x8d, 0xfb,
+ 0xb7, 0x52, 0xa6, 0xf1, 0x4f, 0xf7, 0x78, 0xc1, 0xb9, 0x33, 0x4e, 0xeb,
+ 0x3c, 0x17, 0xb4, 0xd4, 0x51, 0x1d, 0xe5, 0xc5, 0xc7, 0xac, 0x48, 0x8c,
+ 0x31, 0xcf, 0x50, 0x7c, 0xb3, 0x03, 0x53, 0xcb, 0x59, 0xbe, 0x4d, 0xa5,
+ 0xd3, 0x5f, 0xd3, 0x0d, 0x76, 0x08, 0xa4, 0xcc, 0x96, 0xcd, 0xc6, 0x79,
+ 0x1d, 0xe1, 0xd8, 0x79, 0xd9, 0x75, 0x3f, 0x97, 0x6b, 0x0a, 0xab, 0x83,
+ 0xe5, 0xbf, 0x3e, 0xa8, 0x78, 0xb8, 0x65, 0x00, 0x1f, 0x9b, 0x96, 0xde,
+ 0xb3, 0x22, 0x50, 0x5c, 0x1d, 0x9e, 0xab, 0x7f, 0x75, 0x90, 0x5c, 0xed,
+ 0xd1, 0xcd, 0x3d, 0xa9, 0x09, 0xd4, 0x89, 0x1b, 0x6a, 0x94, 0xce, 0x53,
+ 0xb0, 0x08, 0x3a, 0xf9, 0x55, 0xdf, 0xf6, 0x5d, 0x76, 0xba, 0x9f, 0x87,
+ 0x32, 0xa0, 0x05, 0x0f, 0xea, 0x49, 0x97, 0xaf, 0xc5, 0x9e, 0x0e, 0x0a,
+ 0xeb, 0x9c, 0xf3, 0x21, 0xe8, 0x74, 0xce, 0x91, 0x6f, 0x45, 0x67, 0xe0,
+ 0x04, 0xfe, 0xfb, 0x6c, 0x94, 0x65, 0xe3, 0x60, 0x83, 0x47, 0xe9, 0x56,
+ 0x4a, 0x0c, 0xa2, 0x1a, 0xb6, 0x71, 0xba, 0xe7, 0x41, 0xe7, 0x65, 0x3f,
+ 0x61, 0xee, 0x9b, 0xfa, 0x62, 0xa9, 0x47, 0x15, 0x00, 0xc3, 0x39, 0x0c,
+ 0x97, 0x71, 0x17, 0xa6, 0xfe, 0x8b, 0x24, 0x1d, 0x1b, 0x6d, 0x4e, 0x4b,
+ 0x5d, 0xbe, 0x3c, 0xc8, 0xa3, 0xf4, 0xfc, 0xd4, 0x74, 0x2d, 0xb1, 0xff,
+ 0x39, 0x62, 0x43, 0x20, 0x83, 0x66, 0x36, 0x4d, 0x31, 0x49, 0xbd, 0xbe,
+ 0xb5, 0x55, 0x89, 0x02, 0x80, 0x13, 0x69, 0x3c, 0xc9, 0x5a, 0x75, 0x3c,
+ 0x88, 0x12, 0xd5, 0xed, 0xdc, 0x3f, 0x5e, 0xc0, 0x12, 0x58, 0x17, 0x2d,
+ 0xca, 0xc1, 0xd1, 0x52, 0xd7, 0x56, 0x4d, 0xbb, 0xcb, 0xeb, 0x7c, 0xd9,
+ 0xe7, 0x6a, 0xac, 0x76, 0xe6, 0x50, 0xa4, 0x19, 0x65, 0xae, 0x1c, 0x32,
+ 0x3f, 0xee, 0xd4, 0x30, 0x83, 0x1f, 0x56, 0xf8, 0x53, 0x71, 0xc1, 0xe2,
+ 0x14, 0x94, 0x10, 0x29, 0xe9, 0x28, 0xb6, 0xbc, 0x26, 0xbb, 0xd0, 0x4e,
+ 0x00, 0xed, 0x43, 0xec, 0x7e, 0xae, 0x70, 0xc5, 0x93, 0x69, 0x41, 0x65,
+ 0x5a, 0x58, 0x0c, 0x6b, 0x4c, 0x13, 0x30, 0x92, 0x8f, 0x18, 0x3e, 0xf6,
+ 0x26, 0x9d, 0x62, 0x23, 0x55, 0xae, 0x6e, 0x8f, 0x90, 0x32, 0xdd, 0x9b,
+ 0xd7, 0x14, 0x8a, 0xf3, 0xe8, 0x89, 0xc4, 0xca, 0xd4, 0xc2, 0x80, 0xae,
+ 0x56, 0x00, 0x1b, 0x95, 0x5c, 0xc4, 0x38, 0xf8, 0x28, 0x2e, 0xac, 0x5b,
+ 0x7d, 0x78, 0xd6, 0xdb, 0x9a, 0xa9, 0x3f, 0x27, 0xb9, 0x71, 0xe3, 0xcf,
+ 0x8c, 0xfa, 0x6d, 0xf8, 0x64, 0x1c, 0xa3, 0x66, 0xe1, 0x28, 0x3d, 0x3d,
+ 0xbc, 0x71, 0xe2, 0x46, 0x2e, 0x7c, 0x57, 0x83, 0x73, 0x93, 0xea, 0xcf,
+ 0x53, 0xc1, 0xca, 0x14, 0x55, 0xfd, 0x56, 0x58, 0xc0, 0x56, 0xfa, 0x3c,
+ 0xcb, 0x8d, 0xb7, 0x4f, 0x31, 0x14, 0x75, 0x92, 0x71, 0x33, 0x95, 0x54,
+ 0x23, 0x9e, 0x83, 0x32, 0xa3, 0x0e, 0x29, 0x60, 0x4c, 0xdb, 0xe3, 0x39,
+ 0xa5, 0x45, 0x20, 0x5b, 0x82, 0x24, 0x3a, 0xef, 0xfc, 0xd2, 0xf3, 0xe9,
+ 0x64, 0x2c, 0xed, 0x79, 0xfd, 0x2f, 0x64, 0x2f, 0xab, 0x27, 0x30, 0x51,
+ 0xc3, 0x63, 0x09, 0xb7, 0xb0, 0x99, 0xe6, 0xb7, 0x34, 0x2c, 0xa2, 0x4e,
+ 0x62, 0x7b, 0x6c, 0x41, 0x90, 0xd6, 0x16, 0x9f, 0x04, 0xf6, 0x6d, 0x63,
+ 0xee, 0xe4, 0x36, 0x68, 0x0d, 0x34, 0x71, 0x42, 0xac, 0xdd, 0xbe, 0x7f,
+ 0xd1, 0x96, 0x72, 0xca, 0x5f, 0x12, 0xa2, 0x5d, 0x01, 0xc4, 0x48, 0x92,
+ 0x2b, 0xba, 0x23, 0x70, 0xab, 0x13, 0x91, 0x27, 0x5e, 0xfd, 0x27, 0x1f,
+ 0x2c, 0x72, 0x8e, 0x4d, 0x36, 0x2e, 0x58, 0x2f, 0x6b, 0x01, 0xab, 0x5a,
+ 0x2c, 0xea, 0x74, 0x55, 0xb5, 0x5a, 0x26, 0xc7, 0x2d, 0x96, 0x9b, 0x4d,
+ 0xd2, 0x05, 0x79, 0xee, 0xe8, 0xd5, 0x2a, 0xf4, 0x74, 0x61, 0x18, 0x25,
+ 0x6e, 0xa0, 0x34, 0xae, 0x9d, 0xdc, 0x0a, 0xcd, 0x53, 0xff, 0xc3, 0x52,
+ 0x88, 0x0f, 0x1d, 0xff, 0xd6, 0xff, 0xb6, 0x6c, 0x1e, 0x73, 0x53, 0x91,
+ 0xc4, 0x86, 0x1b, 0x8f, 0x9e, 0xee, 0xee, 0xa4, 0x95, 0x3e, 0xff, 0x76,
+ 0x81, 0x16, 0x90, 0x3f, 0x4d, 0xb6, 0x39, 0x4f, 0x86, 0xf8, 0x35, 0xeb,
+ 0x19, 0x80, 0xe7, 0xfd, 0x65, 0x20, 0x7a, 0x03, 0x7b, 0x69, 0xec, 0xda,
+ 0xf2, 0xb8, 0x0c, 0x06, 0x65, 0xa6, 0x4b, 0xe4, 0x38, 0xc9, 0x6d, 0xd3,
+ 0x39, 0x40, 0x4e, 0x83, 0xfb, 0x98, 0x8b, 0xe9, 0x13, 0xb4, 0xd6, 0x30,
+ 0x95, 0x4b, 0xf7, 0x91, 0x1f, 0xa9, 0xc3, 0xa3, 0xd7, 0x26, 0x7f, 0x4f,
+ 0x30, 0x51, 0xfb, 0x20, 0x62, 0x82, 0x1c, 0xef, 0xd0, 0x54, 0xd3, 0x00,
+ 0x55, 0x3c, 0xa1, 0xdc, 0x4d, 0x08, 0x4f, 0xef, 0x61, 0x4e, 0xd4, 0x75,
+ 0x60, 0xff, 0x8a, 0xb7, 0x13, 0xfd, 0x1f, 0xb8, 0xfb, 0xf2, 0x51, 0x7a,
+ 0x68, 0xf1, 0xb5, 0xc5, 0xbb, 0x41, 0xa8, 0x2f, 0x0b, 0x8d, 0x2b, 0xc3,
+ 0x88, 0x48, 0xd0, 0xe5, 0xb3, 0xd7, 0xee, 0x75, 0x81, 0x73, 0x99, 0x28,
+ 0x0e, 0xb5, 0x86, 0x01, 0x4f, 0x5d, 0x38, 0xf0, 0x47, 0x08, 0x27, 0xd1,
+ 0xaf, 0xe6, 0x9b, 0xf9, 0x4f, 0xd6, 0xe4, 0x01, 0x10, 0x7a, 0x61, 0x2f,
+ 0xb0, 0xed, 0x18, 0xdb, 0xab, 0x9e, 0xde, 0x8c, 0xfc, 0x36, 0x88, 0x91,
+ 0xf4, 0x85, 0x76, 0x6e, 0xa9, 0x4b, 0x28, 0xda, 0xc9, 0x64, 0xee, 0xff,
+ 0xe5, 0xfb, 0x53, 0xa6, 0x23, 0x8d, 0xe8, 0xdc, 0xdb, 0x52, 0x38, 0x11,
+ 0x0b, 0xf2, 0xe0, 0x37, 0x31, 0xad, 0x88, 0xba, 0x4e, 0x25, 0x8c, 0x0a,
+ 0xc6, 0xc1, 0x98, 0x74, 0x03, 0x33, 0x3d, 0xea, 0x3c, 0xaa, 0x2a, 0x0a,
+ 0x30, 0xf4, 0x3a, 0xef, 0xa8, 0x99, 0xe7, 0xba, 0x9f, 0x90, 0x8e, 0x4a,
+ 0xb3, 0x7c, 0xa7, 0x1f, 0xd4, 0x51, 0x1d, 0x09, 0x60, 0x23, 0x1f, 0x57,
+ 0xcb, 0xb9, 0x11, 0x51, 0x64, 0xc0, 0x15, 0x5a, 0x2e, 0x77, 0x12, 0x58,
+ 0xae, 0x45, 0xc7, 0x80, 0x56, 0xde, 0x0a, 0xf2, 0xd2, 0x44, 0x6f, 0x84,
+ 0x16, 0x79, 0x38, 0x65, 0xa7, 0x1b, 0xd1, 0x49, 0x60, 0xdb, 0x05, 0x91,
+ 0x8f, 0xd9, 0x20, 0x8c, 0x88, 0x1f, 0xe8, 0x04, 0xf3, 0x7e, 0xde, 0x22,
+ 0x04, 0xf3, 0x01, 0xcc, 0x83, 0x8a, 0x14, 0x47, 0xd4, 0xba, 0x1b, 0x89,
+ 0x28, 0x5f, 0x69, 0x60, 0x69, 0xbb, 0xb4, 0x00, 0x96, 0xee, 0xb2, 0xfd,
+ 0xc7, 0xab, 0xd6, 0x23, 0x5f, 0x7f, 0x5e, 0xec, 0xf7, 0x1d, 0x36, 0xb4,
+ 0x60, 0xbb, 0x50, 0x4c, 0xe9, 0x9a, 0xc1, 0x9a, 0x8d, 0x8d, 0x57, 0x8e,
+ 0x4a, 0xf0, 0x06, 0xc6, 0x67, 0x1f, 0x55, 0x3a, 0x2d, 0xce, 0x77, 0x5e,
+ 0x2a, 0xc2, 0x76, 0xe1, 0xbc, 0x23, 0x31, 0xd2, 0x7f, 0x21, 0x89, 0xde,
+ 0xe1, 0xcc, 0x47, 0x35, 0xdf, 0x09, 0x85, 0x95, 0x02, 0x2e, 0x30, 0xb4,
+ 0xcc, 0xad, 0x6a, 0x48, 0x7c, 0xa7, 0xa6, 0x9c, 0xf9, 0xce, 0xba, 0x00,
+ 0x91, 0xcf, 0x0d, 0xc0, 0x74, 0x4f, 0x3c, 0x31, 0x0d, 0x3b, 0x5e, 0xcd,
+ 0x46, 0xab, 0xf9, 0x9f, 0x3b, 0xb3, 0x61, 0xbe, 0x15, 0xe7, 0xc7, 0xb1,
+ 0xe2, 0xda, 0xae, 0x95, 0xb9, 0x63, 0x93, 0xcf, 0x62, 0x72, 0xb0, 0x91,
+ 0x3e, 0xfa, 0xff, 0x1e, 0x0a, 0x58, 0x23, 0x21, 0xef, 0x34, 0xca, 0x7b,
+ 0x69, 0x40, 0x12, 0x25, 0xd8, 0xaf, 0x74, 0xd5, 0xb7, 0xfb, 0x52, 0x63,
+ 0xb4, 0x4f, 0x2e, 0x59, 0xcd, 0xdb, 0x86, 0x98, 0x54, 0xde, 0x37, 0xcd,
+ 0x40, 0x5b, 0x9f, 0x0a, 0xab, 0x8d, 0x26, 0x91, 0x94, 0x1f, 0x73, 0x48,
+ 0x27, 0x5e, 0xc1, 0x6a, 0x45, 0x42, 0x24, 0xbf, 0x00, 0x41, 0xfc, 0xe9,
+ 0x65, 0x26, 0x5a, 0xba, 0x38, 0x3d, 0xf2, 0xa2, 0x35, 0x37, 0x14, 0x8d,
+ 0xc5, 0x98, 0xec, 0xa3, 0x5e, 0x73, 0xad, 0x3e, 0x18, 0x28, 0xdc, 0x32,
+ 0x6a, 0xad, 0x5b, 0x69, 0x84, 0x3c, 0x79, 0x83, 0x5d, 0x7c, 0x00, 0x93,
+ 0xcf, 0xd7, 0xff, 0x94, 0xaa, 0x36, 0xa3, 0xbb, 0xa3, 0xdc, 0xb2, 0x8a,
+ 0x65, 0x02, 0x16, 0xd0, 0x2a, 0xd5, 0xbc, 0xb9, 0xf1, 0xb2, 0x5c, 0x20,
+ 0x7a, 0xcc, 0xcb, 0x84, 0xa5, 0x48, 0xc3, 0x93, 0xf4, 0xd9, 0x91, 0xd0,
+ 0xf7, 0x9f, 0x99, 0xb8, 0xa3, 0xb9, 0x1f, 0x3d, 0x83, 0x80, 0x1e, 0x1c,
+ 0x5b, 0x9a, 0x87, 0x71, 0xbb, 0x2a, 0x86, 0x52, 0x88, 0xd3, 0xb4, 0xf8,
+ 0x39, 0xf2, 0xb5, 0x68, 0x49, 0xc8, 0xb9, 0x87, 0x22, 0x82, 0xa6, 0x4a,
+ 0xcf, 0x86, 0x7b, 0x27, 0x0c, 0x18, 0x01, 0x2a, 0x49, 0xdf, 0xc3, 0xf8,
+ 0x92, 0x90, 0x26, 0xa8, 0x30, 0x7f, 0x7b, 0x48, 0xd2, 0xcf, 0x17, 0xac,
+ 0x96, 0x69, 0x5e, 0x48, 0xd0, 0x42, 0x02, 0x56, 0xa5, 0x87, 0xdd, 0x98,
+ 0x5b, 0x3e, 0x9f, 0x04, 0x08, 0xc7, 0xe6, 0xfa, 0x91, 0xf8, 0xc8, 0x8c,
+ 0xa7, 0xaa, 0x4c, 0xfd, 0x05, 0x57, 0xa8, 0xab, 0xae, 0xce, 0xdc, 0x74,
+ 0xe1, 0x9d, 0xa1, 0xfd, 0x3b, 0x43, 0x29, 0x82, 0xab, 0xf7, 0x7e, 0x22,
+ 0xa8, 0x9a, 0x2f, 0x8b, 0xa0, 0x8e, 0x41, 0x05, 0xe3, 0xd8, 0x8a, 0x83,
+ 0x02, 0x48, 0x74, 0x7c, 0x51, 0x09, 0xac, 0x94, 0x44, 0x8f, 0x4a, 0x97,
+ 0x0f, 0xb1, 0xfd, 0xf5, 0xbe, 0x84, 0x4e, 0x68, 0x48, 0x2b, 0x05, 0xfb,
+ 0x03, 0x04, 0x23, 0xfc, 0xf5, 0x60, 0x0e, 0x3f, 0xea, 0xb9, 0xdf, 0xe3,
+ 0x91, 0x14, 0x3e, 0x2d, 0x3a, 0x7e, 0x90, 0xf6, 0x52, 0x3c, 0x5f, 0x9d,
+ 0x56, 0x38, 0xa9, 0x10, 0xa1, 0xec, 0x65, 0xc6, 0x67, 0xb8, 0x14, 0x39,
+ 0xaf, 0x2d, 0xba, 0x46, 0x7b, 0x7f, 0x2e, 0xdd, 0x72, 0xd1, 0x5a, 0x60,
+ 0xf4, 0xbf, 0x57, 0x2c, 0x27, 0xb4, 0x8f, 0x22, 0xde, 0x3c, 0x3a, 0xfc,
+ 0xbe, 0xf9, 0xc7, 0x3c, 0x6c, 0x36, 0xf0, 0x5c, 0xfb, 0xcd, 0x40, 0xc6,
+ 0x87, 0xa3, 0xe7, 0x68, 0x9f, 0x86, 0x4b, 0x1b, 0xb1, 0x90, 0x49, 0xa1,
+ 0x6d, 0xd2, 0x34, 0xc2, 0xa6, 0x55, 0xf3, 0x52, 0x43, 0xf3, 0x41, 0x97,
+ 0x7e, 0x66, 0x46, 0x2b, 0xac, 0xb4, 0x20, 0xd9, 0xd6, 0x2d, 0x05, 0x00,
+ 0x3b, 0xce, 0xb2, 0xfa, 0x17, 0xa0, 0x11, 0xbb, 0xa8, 0x6b, 0xba, 0x25,
+ 0xaa, 0x15, 0x3f, 0x67, 0x4e, 0x18, 0x04, 0xb5, 0x75, 0x0e, 0x29, 0xc0,
+ 0x3e, 0x44, 0x65, 0xfc, 0x19, 0x4e, 0xe0, 0x06, 0x16, 0x37, 0x1f, 0xd7,
+ 0x0d, 0xc5, 0x26, 0x96, 0x1e, 0x3b, 0xdc, 0x52, 0xd6, 0x45, 0xaa, 0x9f,
+ 0x11, 0xe8, 0x44, 0x2d, 0xb8, 0xd4, 0x4e, 0xc5, 0x96, 0x06, 0x1e, 0x9e,
+ 0x04, 0x2e, 0x88, 0xf3, 0x32, 0xde, 0x8f, 0x2e, 0x97, 0x39, 0xe7, 0xef,
+ 0x1c, 0xba, 0xca, 0xfd, 0x33, 0x0c, 0xc4, 0x04, 0xfd, 0xe6, 0xdc, 0x98,
+ 0x3d, 0x20, 0xc8, 0x8d, 0x83, 0x82, 0x19, 0xfd, 0xbb, 0xb6, 0xcb, 0xf6,
+ 0x63, 0x1d, 0x23, 0xd9, 0xe8, 0xf4, 0x60, 0x29, 0x63, 0x61, 0xc6, 0x2b,
+ 0x3f, 0x37, 0xdd, 0x94, 0x50, 0x38, 0x81, 0xef, 0xb0, 0xf4, 0x0f, 0xba,
+ 0xa8, 0x7a, 0x47, 0xe5, 0x19, 0xf7, 0x4d, 0x52, 0x8f, 0xfe, 0x33, 0xde,
+ 0x2e, 0xc1, 0xb8, 0x9d, 0xb0, 0x84, 0xfa, 0x7f, 0x7f, 0x0f, 0x32, 0x23,
+ 0x86, 0x89, 0xce, 0xfe, 0xe7, 0x73, 0xb8, 0x69, 0xa8, 0x91, 0xa6, 0x24,
+ 0x97, 0x84, 0x51, 0x3e, 0x7f, 0x4d, 0xed, 0x98, 0x50, 0xa7, 0xc6, 0xb4,
+ 0x56, 0x68, 0xe2, 0x81, 0x12, 0x78, 0x01, 0x13, 0x82, 0x00, 0x50, 0x7e,
+ 0x69, 0xa5, 0xf9, 0xbb, 0xaa, 0x7a, 0xd8, 0x0d, 0xe9, 0x07, 0x49, 0x89,
+ 0xc5, 0xa1, 0xe9, 0x0c, 0xeb, 0x3c, 0xd3, 0xd0, 0x3a, 0x69, 0xce, 0x9e,
+ 0x67, 0xc1, 0x08, 0x75, 0x7d, 0x71, 0xed, 0x15, 0x1b, 0x3a, 0x57, 0xaf,
+ 0x2b, 0x9b, 0x8c, 0x37, 0x8a, 0x45, 0x86, 0x3a, 0xb0, 0xcd, 0xde, 0x5c,
+ 0x5f, 0x17, 0x41, 0x95, 0x73, 0x7e, 0x16, 0x9d, 0x6b, 0x30, 0xe3, 0xff,
+ 0xe6, 0xa8, 0x91, 0x94, 0xb5, 0x7c, 0xcb, 0xd3, 0x85, 0x1f, 0x5c, 0x15,
+ 0xdc, 0xd9, 0x5f, 0x2b, 0x52, 0x62, 0xac, 0x54, 0x05, 0xd6, 0x00, 0x3c,
+ 0xd7, 0x88, 0xe8, 0x6e, 0xca, 0x58, 0x72, 0xe8, 0xa2, 0x70, 0x96, 0xf0,
+ 0x2d, 0x2e, 0x03, 0x32, 0x1b, 0x37, 0x49, 0x8e, 0x89, 0x93, 0xe4, 0x12,
+ 0xe7, 0x59, 0xbf, 0x8e, 0xd0, 0x8b, 0x58, 0x2f, 0x2b, 0x65, 0xf7, 0x3d,
+ 0x4f, 0xe2, 0xff, 0xad, 0x7d, 0xf3, 0xe7, 0x0a, 0x59, 0x82, 0xf2, 0x10,
+ 0xaa, 0x31, 0xa7, 0x3c, 0x75, 0xfc, 0x98, 0x26, 0x75, 0x0c, 0x28, 0xa8,
+ 0x49, 0x99, 0x5f, 0xeb, 0x6f, 0xa7, 0x39, 0x2a, 0x8e, 0xef, 0xeb, 0x66,
+ 0x57, 0x90, 0x03, 0x2b, 0xb1, 0x1d, 0xd4, 0x7a, 0xdc, 0x01, 0x77, 0xa3,
+ 0x0a, 0xfc, 0xc2, 0xe7, 0x21, 0xf7, 0xe4, 0x2f, 0x8a, 0x22, 0x7b, 0x79,
+ 0x46, 0xf1, 0x72, 0x07, 0xb2, 0x5f, 0x84, 0xed, 0xca, 0x37, 0xc8, 0xef,
+ 0xc1, 0x82, 0xc0, 0x01, 0xf1, 0xd7, 0xc4, 0x2f, 0x1b, 0xbb, 0x8a, 0xb9,
+ 0xdb, 0x68, 0x40, 0xb6, 0x7d, 0x14, 0x7e, 0x27, 0xa4, 0x62, 0xc4, 0x85,
+ 0xc3, 0xb7, 0x58, 0xa0, 0x37, 0xfa, 0x6d, 0x2c, 0x92, 0xca, 0x80, 0x8c,
+ 0x6a, 0x68, 0xef, 0x76, 0x61, 0xb1, 0x13, 0xd9, 0x01, 0xcd, 0xa9, 0xb7,
+ 0x5f, 0x0b, 0x8f, 0x18, 0xa1, 0x66, 0xea, 0x6b, 0xeb, 0xd2, 0xb5, 0x4c,
+ 0xee, 0x52, 0x22, 0x59, 0xc8, 0x26, 0x9f, 0x01, 0x92, 0x48, 0x41, 0x37,
+ 0xec, 0xc6, 0x27, 0xbe, 0x56, 0x42, 0x04, 0x6d, 0x83, 0xa1, 0x96, 0x31,
+ 0x03, 0xff, 0x39, 0xf3, 0x2b, 0xf1, 0x6d, 0x7f, 0x2f, 0xd6, 0xec, 0xf0,
+ 0x18, 0xf2, 0xc8, 0xbc, 0xe3, 0x80, 0x47, 0x08, 0x2d, 0x54, 0xb4, 0xe3,
+ 0x0a, 0x9b, 0x2a, 0xec, 0x5b, 0xdb, 0xb3, 0x31, 0x3b, 0xc5, 0xd9, 0xf1,
+ 0x5f, 0x5a, 0xb9, 0x9b, 0x46, 0x8a, 0x79, 0xd9, 0x59, 0x56, 0x43, 0x4d,
+ 0x60, 0xa6, 0x10, 0xae, 0xca, 0x77, 0xef, 0xa1, 0x5f, 0x20, 0xb5, 0xa9,
+ 0x22, 0xcb, 0xeb, 0xe8, 0x48, 0xe8, 0xc1, 0x29, 0xf8, 0xce, 0x38, 0xd1,
+ 0x31, 0x2f, 0x1a, 0xea, 0x36, 0x25, 0xd1, 0x5e, 0x6a, 0x60, 0x94, 0x9c,
+ 0xf8, 0x1d, 0x3c, 0xf1, 0x75, 0x08, 0x06, 0x6f, 0xaa, 0x5e, 0x78, 0x41,
+ 0x11, 0x94, 0xe4, 0xa3, 0x79, 0x67, 0x78, 0x46, 0xc9, 0x1f, 0xa2, 0xbc,
+ 0x75, 0xb2, 0xc5, 0x15, 0xee, 0x0b, 0x88, 0xf9, 0xff, 0xdf, 0x41, 0xa1,
+ 0xb8, 0x3a, 0x5f, 0xbb, 0x20, 0x1f, 0x1f, 0x81, 0xba, 0xc5, 0xbd, 0x7f,
+ 0x29, 0xdd, 0xcb, 0x05, 0x0e, 0x74, 0xe4, 0x48, 0x0d, 0x02, 0xaa, 0xae,
+ 0xb6, 0x93, 0x5b, 0xa0, 0x3f, 0x65, 0xe6, 0x18, 0xfd, 0xac, 0xac, 0x85,
+ 0x93, 0x54, 0xf6, 0x26, 0xff, 0xa6, 0xe8, 0x9f, 0x86, 0x87, 0xe2, 0xd4,
+ 0xe6, 0x92, 0x59, 0xda, 0x16, 0xed, 0xcf, 0x93, 0x2b, 0x17, 0x7d, 0x35,
+ 0x0b, 0x19, 0x9a, 0x8c, 0x08, 0x27, 0xf3, 0xeb, 0x69, 0x50, 0xe1, 0x33,
+ 0xf6, 0xa1, 0xfe, 0x75, 0x6d, 0x0e, 0xe5, 0x0e, 0x22, 0xe7, 0xf9, 0x21,
+ 0x44, 0x4f, 0x84, 0x5c, 0xfb, 0x32, 0x3e, 0xe1, 0xf8, 0xd4, 0x02, 0x53,
+ 0x9b, 0x8c, 0xb9, 0x07, 0xaa, 0x15, 0x9a, 0x76, 0x58, 0x49, 0x08, 0x33,
+ 0x4d, 0x5c, 0x4f, 0xf4, 0x28, 0x4b, 0x33, 0xb4, 0x51, 0x9b, 0xe9, 0x13,
+ 0x4a, 0x52, 0x5c, 0x83, 0xbf, 0xf7, 0xd3, 0x48, 0xcb, 0x98, 0x0c, 0xb4,
+ 0x5d, 0xcd, 0x97, 0x4a, 0x1f, 0xc2, 0x19, 0x95, 0x03, 0x8a, 0x56, 0x35,
+ 0xef, 0x6a, 0x84, 0xa4, 0x62, 0xe8, 0x9a, 0x0a, 0x52, 0xfb, 0xe8, 0x61,
+ 0xd5, 0xbc, 0xee, 0x27, 0xad, 0xfd, 0x5b, 0xb0, 0x03, 0xb9, 0xde, 0xd3,
+ 0xf6, 0x74, 0x86, 0xc2, 0x0d, 0xaa, 0x4d, 0x89, 0x2e, 0x88, 0x44, 0x1a,
+ 0x18, 0xc8, 0xf0, 0x21, 0x3e, 0x95, 0x6e, 0x58, 0x06, 0xdd, 0x1c, 0x27,
+ 0xbb, 0x5f, 0x52, 0x10, 0xa5, 0x68, 0xb2, 0x95, 0x85, 0xea, 0x11, 0x16,
+ 0x67, 0x6d, 0x2d, 0x35, 0x47, 0x13, 0x6d, 0x8e, 0x4b, 0x17, 0x1c, 0x7f,
+ 0x72, 0xc8, 0xd8, 0x2f, 0x0b, 0x16, 0xf4, 0x04, 0x7a, 0x5e, 0xfa, 0x24,
+ 0x2c, 0xae, 0xde, 0x8d, 0x9b, 0x48, 0xe1, 0x65, 0x83, 0x0c, 0xff, 0xcf,
+ 0x3e, 0x14, 0xf9, 0x1b, 0x8d, 0xf7, 0xbe, 0x38, 0xbf, 0x2d, 0x5a, 0x5a,
+ 0xed, 0xb6, 0x70, 0xc6, 0xab, 0x7b, 0xa3, 0xe9, 0xc8, 0x31, 0x4f, 0x9c,
+ 0x9a, 0x98, 0x88, 0xc4, 0x72, 0x22, 0xe2, 0x85, 0x12, 0xf4, 0xea, 0x52,
+ 0xb6, 0x48, 0x2f, 0x53, 0x12, 0xc6, 0xf9, 0xe5, 0x3c, 0xdc, 0x74, 0x17,
+ 0xcc, 0xe3, 0xe5, 0x04, 0x5d, 0xc1, 0x47, 0xa7, 0xc0, 0xb9, 0xc7, 0xb9,
+ 0x39, 0x98, 0xaf, 0xcf, 0x60, 0xb5, 0x7f, 0x82, 0xa7, 0xb9, 0x0a, 0x25,
+ 0xfb, 0x70, 0xa2, 0x43, 0xf2, 0xc1, 0x8a, 0x99, 0x36, 0xbc, 0x03, 0x73,
+ 0x9a, 0xf1, 0x97, 0x65, 0xff, 0xe6, 0xe2, 0x37, 0xba, 0xac, 0x0d, 0xc1,
+ 0x93, 0x05, 0x4b, 0xf5, 0x69, 0x45, 0x9b, 0x83, 0xc2, 0x44, 0xea, 0x23,
+ 0x65, 0x5c, 0x22, 0xb2, 0x97, 0x2f, 0x8d, 0xab, 0x51, 0x61, 0xa1, 0xa6,
+ 0x8a, 0x6a, 0x1c, 0xf6, 0xff, 0xc2, 0x83, 0xfb, 0x56, 0xef, 0xf0, 0xbd,
+ 0x67, 0x6e, 0xe6, 0x50, 0x2f, 0xa0, 0xe6, 0x26, 0x2d, 0x68, 0x7e, 0x97,
+ 0x09, 0x4c, 0x0a, 0x17, 0x26, 0x98, 0x69, 0x3a, 0x3e, 0x06, 0x15, 0x20,
+ 0x99, 0x38, 0x59, 0x7d, 0x54, 0x5c, 0x34, 0xd7, 0x9a, 0x51, 0xcc, 0x36,
+ 0x28, 0xc1, 0x0b, 0x9c, 0x6c, 0xd2, 0x03, 0xc6, 0x8f, 0x1e, 0x40, 0xa2,
+ 0xd6, 0xa3, 0x34, 0xf2, 0xb1, 0x21, 0xef, 0x93, 0xd5, 0x51, 0xfb, 0x77,
+ 0x76, 0x92, 0x05, 0x8f, 0xad, 0x36, 0x12, 0x33, 0xce, 0x3e, 0x8f, 0xa9,
+ 0x2c, 0x67, 0xe7, 0x4f, 0x02, 0x02, 0xbd, 0x83, 0xcc, 0x16, 0xea, 0xf7,
+ 0x64, 0x7f, 0xde, 0xa0, 0x37, 0xf5, 0x89, 0xa2, 0x3e, 0xa2, 0xca, 0x8f,
+ 0xe1, 0x8a, 0x65, 0x7f, 0x37, 0x12, 0xd9, 0x1e, 0x6f, 0x7f, 0xce, 0x73,
+ 0x87, 0xdc, 0xd3, 0xa6, 0xec, 0xe0, 0x59, 0x62, 0xaf, 0x6d, 0xaa, 0x89,
+ 0xe8, 0x51, 0xfa, 0xa8, 0x9a, 0x75, 0xf9, 0x64, 0xf2, 0x36, 0xd3, 0xbd,
+ 0x89, 0x9d, 0x14, 0x4e, 0xf5, 0x36, 0x81, 0x29, 0x4c, 0x37, 0x84, 0xd9,
+ 0x9b, 0x6f, 0xf1, 0x60, 0x27, 0xef, 0x03, 0x8a, 0xd6, 0xbc, 0x73, 0xe3,
+ 0x91, 0xd0, 0x75, 0x58, 0xb7, 0x0e, 0x49, 0x16, 0x0e, 0x44, 0xf9, 0x13,
+ 0x3f, 0xe7, 0x80, 0xfb, 0x41, 0xf6, 0xdf, 0x14, 0x35, 0x39, 0x30, 0xb9,
+ 0x31, 0x14, 0x4b, 0xba, 0x35, 0x5a, 0x14, 0x1d, 0xdf, 0x56, 0x88, 0x98,
+ 0xa8, 0xae, 0x8b, 0x73, 0xb9, 0xb9, 0xcd, 0xa5, 0x63, 0x21, 0x2b, 0xb2,
+ 0xb9, 0x76, 0xe8, 0x52, 0x33, 0x20, 0xff, 0x90, 0x66, 0x5f, 0xbc, 0x38,
+ 0xeb, 0xf9, 0xd0, 0x57, 0x9d, 0xb6, 0xfa, 0xf6, 0x83, 0xf8, 0xbe, 0x69,
+ 0x97, 0xb4, 0x2e, 0x60, 0xa1, 0x4f, 0x00, 0x9b, 0x7a, 0xae, 0xa2, 0x7f,
+ 0x5e, 0x36, 0x6d, 0x1e, 0x6d, 0x4f, 0xc4, 0xbc, 0xfb, 0xb3, 0x3a, 0x01,
+ 0x65, 0x45, 0xb0, 0x46, 0x24, 0xe0, 0xf8, 0xc8, 0x39, 0xa3, 0x28, 0x99,
+ 0xb9, 0xc1, 0xdf, 0xa1, 0xbc, 0x7e, 0x8c, 0x2f, 0x05, 0xde, 0x81, 0xc2,
+ 0xe8, 0x1a, 0xf1, 0x25, 0xca, 0xd0, 0xc0, 0xd6, 0xdc, 0xc3, 0x7e, 0x4e,
+ 0x48, 0xdb, 0xf6, 0x5a, 0x76, 0x6f, 0x67, 0x37, 0x1c, 0xc9, 0xcf, 0x11,
+ 0xcc, 0x79, 0x24, 0xea, 0xbb, 0xab, 0x47, 0xb1, 0x80, 0x54, 0xe3, 0x99,
+ 0xa6, 0x96, 0x68, 0x10, 0xb9, 0xae, 0x54, 0xbe, 0xfc, 0x76, 0x2c, 0x47,
+ 0x3d, 0xff, 0x2b, 0xf5, 0xec, 0x88, 0xb8, 0x07, 0xb6, 0x6e, 0xbf, 0xbe,
+ 0x46, 0xdb, 0x11, 0x2e, 0x7c, 0x7f, 0xae, 0xa3, 0x17, 0x48, 0x4c, 0x05,
+ 0xb6, 0xd7, 0x29, 0x42, 0xb7, 0xcd, 0x1a, 0xb2, 0xbb, 0x21, 0x74, 0x8b,
+ 0x60, 0xcd, 0x07, 0x75, 0x44, 0x0c, 0x05, 0x85, 0x9a, 0xd4, 0x1f, 0xd2,
+ 0x74, 0xa3, 0x8e, 0x5a, 0x20, 0x3e, 0x60, 0xb8, 0x80, 0x35, 0xac, 0xca,
+ 0x5b, 0x79, 0x5b, 0x9d, 0xcc, 0xaa, 0xbe, 0x44, 0x86, 0x11, 0x06, 0x97,
+ 0x2c, 0xd7, 0xd0, 0xb6, 0xb4, 0xca, 0x4b, 0x6a, 0xd6, 0x01, 0xfe, 0x81,
+ 0x06, 0xdd, 0x34, 0x12, 0xd3, 0xa6, 0xfb, 0xc7, 0x33, 0x41, 0xd1, 0x55,
+ 0x54, 0x54, 0xb5, 0xf7, 0x6e, 0x65, 0x69, 0xff, 0xe6, 0xf9, 0x96, 0x6d,
+ 0x9c, 0xc9, 0x37, 0x16, 0xd3, 0x0e, 0x92, 0xbc, 0xd1, 0x15, 0x6a, 0x5b,
+ 0x69, 0xd8, 0xa5, 0xb6, 0xdc, 0xb6, 0xd8, 0x17, 0xc2, 0x5a, 0x3f, 0xd2,
+ 0xeb, 0xd9, 0x04, 0xc9, 0x1f, 0xee, 0xdc, 0x8b, 0x27, 0x48, 0xad, 0xc3,
+ 0x12, 0x8d, 0x0f, 0xf6, 0x4b, 0x46, 0xd6, 0x86, 0x8c, 0x1c, 0xae, 0x81,
+ 0x6e, 0xd6, 0xb5, 0xae, 0x50, 0xaf, 0x52, 0x95, 0x61, 0xcc, 0x1d, 0x86,
+ 0x11, 0x12, 0x3f, 0x56, 0x4c, 0x98, 0xa8, 0x91, 0x9c, 0x26, 0x98, 0xe7,
+ 0x56, 0x0d, 0xc9, 0x20, 0x4e, 0xe0, 0x67, 0x5e, 0xe9, 0xe0, 0x99, 0xed,
+ 0x93, 0x29, 0x9a, 0xfb, 0x71, 0x24, 0x02, 0x5a, 0x6d, 0xe5, 0xec, 0x4b,
+ 0xc5, 0x7d, 0x35, 0x3d, 0xd7, 0x55, 0xc3, 0x7f, 0x69, 0x5d, 0x5b, 0x87,
+ 0xba, 0x9a, 0x78, 0x39, 0xa8, 0x31, 0x63, 0x3b, 0xdb, 0xc2, 0xed, 0xc2,
+ 0xf8, 0xc8, 0xee, 0xa5, 0x46, 0xf3, 0xb0, 0x0e, 0x76, 0x0a, 0x6e, 0x83,
+ 0xc4, 0x2f, 0xf4, 0xa4, 0x8c, 0x73, 0xab, 0xf8, 0xcf, 0xe2, 0xf8, 0x4d,
+ 0xa5, 0x74, 0x58, 0xae, 0x13, 0xe2, 0xc7, 0x9a, 0x15, 0xf9, 0x18, 0x30,
+ 0x1a, 0xa3, 0x31, 0x65, 0xec, 0x9f, 0xf7, 0xa5, 0x7f, 0x3a, 0x1b, 0xf1,
+ 0x3e, 0x96, 0xb6, 0x23, 0xac, 0xd8, 0xa9, 0x9d, 0x5c, 0x77, 0x63, 0xb4,
+ 0xa8, 0xc8, 0x07, 0x1b, 0x30, 0xe0, 0xea, 0xce, 0xc9, 0x8e, 0x7b, 0x25,
+ 0x95, 0x09, 0x8e, 0xff, 0xf1, 0xad, 0x12, 0x10, 0x32, 0xbb, 0x6a, 0xfa,
+ 0x52, 0x58, 0xa3, 0x4c, 0x5c, 0x14, 0x61, 0x33, 0x46, 0x76, 0x7d, 0xf1,
+ 0x28, 0xb8, 0xdb, 0xe8, 0xf6, 0x3e, 0xdd, 0x17, 0xfb, 0x5a, 0x33, 0x93,
+ 0x67, 0x1e, 0x9b, 0x31, 0xdd, 0x24, 0x76, 0xab, 0x88, 0x50, 0x71, 0x52,
+ 0xcc, 0x30, 0x55, 0x04, 0xf2, 0x62, 0x5f, 0x3d, 0x2e, 0xc0, 0x6d, 0x81,
+ 0x9c, 0xcf, 0xbe, 0x0f, 0x72, 0x04, 0xc1, 0xcb, 0x7a, 0x85, 0x7c, 0x88,
+ 0x1a, 0xe4, 0xbf, 0x5a, 0x25, 0x14, 0x7b, 0x8b, 0x61, 0x4a, 0x78, 0xbf,
+ 0x75, 0xe6, 0x6f, 0x8f, 0xf5, 0xce, 0x2e, 0xbf, 0xe0, 0xa2, 0x17, 0xc1,
+ 0xc3, 0x17, 0x62, 0x36, 0x6f, 0xd3, 0x0e, 0x39, 0xba, 0x76, 0x6c, 0x66,
+ 0x8d, 0xcd, 0x7b, 0xb4, 0xe3, 0x11, 0x8b, 0xeb, 0x7d, 0x83, 0x39, 0xbc,
+ 0xca, 0x55, 0x70, 0xe1, 0x6f, 0x27, 0x46, 0x0e, 0x78, 0x78, 0x77, 0x16,
+ 0xab, 0x03, 0xcc, 0x3e, 0xd2, 0xf3, 0xb6, 0xfd, 0x60, 0xe8, 0x35, 0x1f,
+ 0xf3, 0x5f, 0x2b, 0x9b, 0x5b, 0x57, 0xe6, 0xb4, 0xe3, 0x3e, 0x28, 0x8c,
+ 0xd0, 0xdc, 0xe2, 0xc7, 0xb2, 0x48, 0x6b, 0x76, 0x6e, 0x4d, 0xe4, 0x88,
+ 0xf8, 0xdb, 0x66, 0x58, 0xda, 0x66, 0x1c, 0x03, 0x60, 0xc6, 0x8e, 0xf6,
+ 0x96, 0x46, 0x45, 0x0b, 0xc1, 0x93, 0xa1, 0x3b, 0xbe, 0x73, 0xe6, 0x59,
+ 0x6d, 0x8e, 0xd6, 0x04, 0xe6, 0x7c, 0x1b, 0xd0, 0x38, 0xb2, 0x50, 0x62,
+ 0x2d, 0x03, 0x2a, 0xc5, 0x3a, 0x7f, 0xf3, 0x2e, 0x23, 0x4c, 0x64, 0x00,
+ 0x77, 0xba, 0xa7, 0xaf, 0x81, 0x84, 0x37, 0x6b, 0xf8, 0xe2, 0x93, 0xbe,
+ 0x63, 0x6f, 0x7f, 0xe5, 0x39, 0xfb, 0xad, 0xca, 0xe4, 0x32, 0x70, 0x51,
+ 0x58, 0xb3, 0xac, 0xa2, 0xb6, 0xd2, 0x2f, 0x3c, 0xf5, 0x91, 0xf8, 0xd9,
+ 0x3e, 0xaa, 0x6a, 0xe2, 0x15, 0x49, 0x85, 0x6e, 0xde, 0xae, 0x51, 0xe9,
+ 0x5b, 0x56, 0x2a, 0x7f, 0x87, 0x02, 0x71, 0x89, 0x42, 0x86, 0x09, 0x1f,
+ 0xe2, 0x2a, 0xb7, 0x8e, 0x78, 0xc0, 0xd0, 0xc9, 0xca, 0x94, 0xeb, 0x5f,
+ 0xd5, 0x04, 0x8d, 0x5a, 0x67, 0x47, 0x10, 0x4a, 0xa7, 0x70, 0x62, 0x4f,
+ 0x15, 0x18, 0xd3, 0x19, 0x45, 0x0b, 0x14, 0xef, 0x54, 0xf4, 0x50, 0x64,
+ 0x1a, 0xc8, 0x65, 0xd1, 0x2f, 0x6c, 0xb5, 0x95, 0xb9, 0x42, 0x45, 0x36,
+ 0x8d, 0x2b, 0x69, 0x23, 0xcc, 0xfb, 0x47, 0x5d, 0x81, 0x37, 0x78, 0xc1,
+ 0x41, 0xcb, 0x21, 0xd5, 0xed, 0x9d, 0x01, 0x01, 0x60, 0x82, 0x5c, 0x55,
+ 0x55, 0xa4, 0xe3, 0xfb, 0xb5, 0x1e, 0x8f, 0x8d, 0x70, 0xd3, 0xaf, 0xa6,
+ 0x2c, 0x4e, 0x66, 0x23, 0xd4, 0xa4, 0x18, 0xd6, 0x71, 0x0a, 0x14, 0xa7,
+ 0xfb, 0x05, 0x15, 0xea, 0xaa, 0xaa, 0x5e, 0xa3, 0x07, 0x86, 0xdd, 0xce,
+ 0x60, 0x27, 0x29, 0x5a, 0x49, 0x75, 0xb7, 0xe0, 0x2c, 0xba, 0xff, 0x64,
+ 0x45, 0x0f, 0xe5, 0x40, 0xbd, 0x94, 0x0b, 0x3d, 0xd5, 0x6b, 0x0b, 0x5f,
+ 0x52, 0x70, 0xf7, 0x7c, 0x04, 0x82, 0x6b, 0x07, 0xa1, 0xec, 0xa4, 0xae,
+ 0x79, 0x3b, 0x0f, 0x95, 0x92, 0x1b, 0x04, 0xae, 0xbd, 0xba, 0xdb, 0xf5,
+ 0xd2, 0x14, 0x6b, 0xf5, 0x6e, 0x7f, 0x6a, 0x98, 0xa1, 0xea, 0x33, 0x08,
+ 0x38, 0x3d, 0xac, 0x83, 0xd2, 0xf2, 0x07, 0xdb, 0x00, 0x94, 0x84, 0xb6,
+ 0x19, 0xf9, 0x5a, 0x96, 0xf8, 0x4c, 0xaa, 0x4e, 0xba, 0x8c, 0x11, 0x2e,
+ 0x08, 0xbb, 0x15, 0xed, 0xb5, 0x19, 0x3a, 0x2a, 0xa8, 0xde, 0xf4, 0xb2,
+ 0xbf, 0xa0, 0xae, 0x87, 0x54, 0xae, 0x6d, 0x2b, 0xae, 0xfd, 0x06, 0x61,
+ 0x35, 0x9a, 0xa4, 0x2d, 0x6d, 0x80, 0xb0, 0x1b, 0x47, 0x5b, 0xd5, 0x64,
+ 0xe5, 0xdc, 0x4e, 0xa1, 0x15, 0xe9, 0x71, 0xd2, 0xcc, 0x29, 0x53, 0x36,
+ 0x87, 0x94, 0x1a, 0x0f, 0xcb, 0x3c, 0x1e, 0xdf, 0x20, 0xc4, 0xe5, 0x3f,
+ 0x29, 0x78, 0xf8, 0xc1, 0xf5, 0xab, 0x59, 0x89, 0xd6, 0x95, 0xf9, 0x7b,
+ 0x4f, 0x8d, 0x12, 0x9b, 0xa3, 0x24, 0x37, 0xc3, 0xf4, 0xb0, 0x7c, 0xca,
+ 0x40, 0xe7, 0x0b, 0x13, 0x11, 0x63, 0xf1, 0x71, 0x39, 0x11, 0x04, 0xb0,
+ 0xfb, 0x85, 0x07, 0xb7, 0xc0, 0xdd, 0x75, 0xca, 0xb9, 0x56, 0x15, 0x66,
+ 0xc4, 0x91, 0x3e, 0xff, 0xb3, 0x6e, 0x0d, 0x35, 0x47, 0x6f, 0xde, 0xf2,
+ 0xd4, 0x06, 0xfb, 0x73, 0x84, 0xc6, 0x33, 0xd4, 0x06, 0x74, 0x9d, 0x37,
+ 0x29, 0x9c, 0xe4, 0x17, 0x22, 0xac, 0xb3, 0xe9, 0x26, 0xe4, 0xf9, 0x37,
+ 0x56, 0xcd, 0xd0, 0x4d, 0x90, 0xe0, 0x75, 0x90, 0x64, 0xe2, 0xf3, 0x67,
+ 0x07, 0xdb, 0xde, 0xbe, 0xdd, 0x50, 0x24, 0x86, 0x28, 0xf9, 0x3b, 0xb5,
+ 0x2d, 0x92, 0xaf, 0x78, 0x7a, 0x2d, 0xf7, 0xe7, 0xc9, 0x34, 0xc7, 0xd1,
+ 0x40, 0x25, 0x0a, 0x92, 0x74, 0x7c, 0x8f, 0xb7, 0xfd, 0x9a, 0x13, 0x79,
+ 0xd9, 0x8e, 0x70, 0xa5, 0x9a, 0xcc, 0xfe, 0x9b, 0x64, 0x04, 0xe0, 0xff,
+ 0x66, 0x8d, 0x84, 0xc7, 0xae, 0x3c, 0xfb, 0x66, 0x21, 0x61, 0x09, 0x04,
+ 0x45, 0x11, 0x29, 0x46, 0x8a, 0x49, 0x23, 0x8d, 0xbb, 0xcc, 0xbe, 0x11,
+ 0x02, 0x90, 0x58, 0xcb, 0xe8, 0xd9, 0xd4, 0xad, 0x19, 0x10, 0xd1, 0x9e,
+ 0x70, 0x02, 0x40, 0x02, 0x2f, 0xbe, 0xef, 0xe9, 0x9a, 0xda, 0x80, 0xd3,
+ 0xf3, 0x0f, 0x47, 0xe2, 0x6c, 0xfc, 0x58, 0xac, 0x59, 0x54, 0x2c, 0x51,
+ 0x4c, 0xa5, 0x70, 0x90, 0xa9, 0x83, 0x5b, 0x5e, 0xbc, 0xd3, 0x1e, 0x40,
+ 0x3c, 0x9d, 0x65, 0xa5, 0xc0, 0x0f, 0x66, 0x20, 0x7f, 0x3c, 0xaf, 0x9f,
+ 0xc0, 0x23, 0x6f, 0x52, 0x6f, 0x1d, 0x7a, 0xfd, 0xf1, 0x70, 0xda, 0x07,
+ 0x0a, 0xd8, 0x90, 0xc9, 0x68, 0x58, 0x36, 0xf0, 0xc3, 0x50, 0xf0, 0xe7,
+ 0x68, 0xe2, 0xb5, 0x43, 0x9a, 0x5e, 0x6a, 0x03, 0x81, 0xe6, 0x65, 0x9b,
+ 0xb5, 0x8c, 0xa6, 0xb5, 0xfe, 0x7f, 0x3d, 0xde, 0xc7, 0x75, 0x5e, 0x1b,
+ 0xf4, 0x7f, 0xbe, 0x65, 0x44, 0x8b, 0xd1, 0x47, 0xb5, 0xf5, 0x27, 0xcc,
+ 0xf7, 0xc2, 0x76, 0xc1, 0x5f, 0x26, 0x08, 0xb1, 0x5d, 0x0a, 0x2b, 0xd4,
+ 0xcf, 0xf0, 0xc7, 0x32, 0xf6, 0xad, 0xac, 0x2b, 0x70, 0x72, 0xd5, 0x3c,
+ 0x23, 0xc8, 0xb1, 0x3a, 0xd9, 0x22, 0xb9, 0x93, 0x62, 0x87, 0x1d, 0xab,
+ 0x48, 0xbe, 0xa8, 0x03, 0x68, 0xa4, 0xf1, 0x58, 0x75, 0x17, 0x35, 0x7b,
+ 0x1f, 0x1e, 0x4b, 0x55, 0x12, 0x8e, 0x6a, 0x08, 0xd1, 0x9c, 0x21, 0x44,
+ 0xc6, 0x3b, 0xfb, 0x55, 0x3f, 0x9d, 0x9e, 0xda, 0x3b, 0xf8, 0xb7, 0x79,
+ 0x50, 0xe3, 0x6e, 0x4d, 0xd6, 0x9e, 0x44, 0x9f, 0xb7, 0x39, 0x9a, 0x70,
+ 0x51, 0x2b, 0x7a, 0xe5, 0x60, 0xc8, 0xd9, 0x2a, 0xe0, 0xa5, 0xcf, 0xeb,
+ 0x46, 0xdb, 0xb7, 0xbc, 0x4a, 0x2b, 0x36, 0xb0, 0xc2, 0x24, 0xfb, 0x7f,
+ 0xb9, 0x53, 0x7c, 0x63, 0xbb, 0x88, 0x09, 0xba, 0xe9, 0x0e, 0x46, 0x57,
+ 0x28, 0xd7, 0x7e, 0xff, 0xde, 0x33, 0x23, 0xa7, 0xee, 0x00, 0xe2, 0x28,
+ 0x52, 0x88, 0xc4, 0xb6, 0xa9, 0xe4, 0x64, 0xe3, 0xea, 0x57, 0xab, 0x1b,
+ 0x2b, 0xec, 0x36, 0xe9, 0x07, 0xbd, 0x47, 0x94, 0xad, 0xfa, 0x1a, 0x9e,
+ 0x38, 0x44, 0x79, 0x2c, 0x76, 0x45, 0x3a, 0x59, 0xdb, 0xb5, 0xf1, 0x4b,
+ 0x96, 0x73, 0xcc, 0xc9, 0x84, 0x05, 0x4b, 0xdb, 0x81, 0x98, 0x58, 0xf8,
+ 0x1a, 0xc8, 0x89, 0xa7, 0x26, 0xa4, 0xf9, 0xd4, 0x65, 0x77, 0xc6, 0xd6,
+ 0x18, 0xfc, 0x99, 0x48, 0x72, 0xaa, 0xf0, 0x8a, 0xca, 0x3f, 0xbc, 0x45,
+ 0xf6, 0xc5, 0x08, 0xd7, 0x9f, 0x7a, 0xa5, 0x81, 0xe9, 0xfb, 0x50, 0xd0,
+ 0x42, 0xbe, 0x48, 0x25, 0x37, 0x2a, 0x86, 0xe9, 0x39, 0xdc, 0x6a, 0x9f,
+ 0x08, 0x1e, 0x06, 0xa4, 0x35, 0x1d, 0x99, 0xeb, 0xb8, 0x76, 0xeb, 0x80,
+ 0x9f, 0x1e, 0x40, 0xae, 0xef, 0xd5, 0xf8, 0x74, 0x8a, 0x85, 0x57, 0x11,
+ 0x21, 0x1d, 0xbd, 0xe8, 0xc3, 0x65, 0x6f, 0x53, 0xe9, 0xda, 0xbf, 0xb8,
+ 0x4e, 0xcc, 0x2b, 0x91, 0xb0, 0xa3, 0xc3, 0xa3, 0xe0, 0xa1, 0x9d, 0x05,
+ 0x9e, 0xff, 0xc6, 0x64, 0xb3, 0x8d, 0xc1, 0xaf, 0xde, 0x4d, 0x8f, 0x26,
+ 0x78, 0x04, 0xc9, 0xfb, 0xe6, 0x06, 0x17, 0x86, 0x42, 0xc2, 0xa5, 0xd3,
+ 0xf4, 0xe0, 0x81, 0xbd, 0x16, 0xd3, 0xc6, 0x1a, 0xd7, 0xd9, 0xed, 0x24,
+ 0x22, 0x7a, 0xeb, 0x95, 0xa3, 0x1a, 0x96, 0x81, 0x13, 0x48, 0x15, 0x70,
+ 0x18, 0xf8, 0x2f, 0x98, 0xf3, 0x61, 0x49, 0xaa, 0x69, 0x92, 0x63, 0x6e,
+ 0xd2, 0xe3, 0xa6, 0x45, 0x75, 0x71, 0xc8, 0x29, 0xc0, 0x21, 0xfa, 0x6a,
+ 0xe6, 0xdf, 0x40, 0xbb, 0x94, 0x75, 0xba, 0xf2, 0x6c, 0x44, 0xb6, 0xd6,
+ 0x28, 0xf5, 0x7f, 0x99, 0xb8, 0x56, 0x08, 0xba, 0x30, 0x45, 0x31, 0x87,
+ 0x46, 0xdc, 0xf5, 0xcf, 0xbe, 0x65, 0xbd, 0x63, 0xf0, 0xcb, 0x7b, 0x58,
+ 0x83, 0x79, 0xbe, 0xd8, 0xda, 0x9e, 0xce, 0x54, 0x40, 0x19, 0xf0, 0xe5,
+ 0x3d, 0x99, 0x98, 0x7b, 0xce, 0xbc, 0x8f, 0x0f, 0x72, 0x4d, 0xdc, 0xf1,
+ 0xae, 0x44, 0x13, 0x7c, 0xa7, 0x6c, 0x17, 0x8a, 0xe8, 0xeb, 0x6b, 0xad,
+ 0xc8, 0x3a, 0xce, 0x4a, 0x60, 0xb7, 0x93, 0x5e, 0x32, 0x94, 0xa6, 0x64,
+ 0x15, 0x09, 0xe7, 0x69, 0x07, 0x42, 0xf1, 0x4b, 0x59, 0x3d, 0xf4, 0xb2,
+ 0xf9, 0x10, 0xca, 0xef, 0x64, 0x86, 0x3d, 0x58, 0xac, 0x90, 0x45, 0x4b,
+ 0xe8, 0x27, 0xff, 0x2b, 0x16, 0xb2, 0x33, 0x43, 0x7c, 0xa7, 0xef, 0xc6,
+ 0xbb, 0x7a, 0xfa, 0xce, 0xda, 0x03, 0x95, 0x68, 0x3c, 0xff, 0x1b, 0xcd,
+ 0x84, 0x94, 0x41, 0xac, 0x05, 0x88, 0x19, 0x51, 0xf1, 0x9c, 0x5e, 0x55,
+ 0x2f, 0x69, 0x4a, 0xe7, 0x91, 0x36, 0x1f, 0x20, 0x51, 0xa4, 0xc6, 0x9d,
+ 0xc5, 0x33, 0x80, 0xe0, 0x9d, 0x09, 0x42, 0x63, 0x20, 0x6a, 0x8f, 0x4f,
+ 0xa9, 0x2d, 0xef, 0x33, 0x63, 0x57, 0xed, 0x39, 0xea, 0x34, 0x5b, 0x51,
+ 0x8b, 0x59, 0xcd, 0x80, 0x9c, 0xa3, 0x66, 0x26, 0x51, 0x1c, 0x78, 0x1c,
+ 0xbf, 0x72, 0x11, 0xfe, 0xb3, 0xba, 0x21, 0xe0, 0x8c, 0xea, 0xea, 0xb2,
+ 0x49, 0x02, 0xd3, 0x10, 0x64, 0x74, 0xbf, 0xf0, 0x1e, 0x39, 0xd0, 0x9d,
+ 0x9b, 0x19, 0x69, 0xe0, 0xa4, 0x33, 0xdc, 0xcc, 0x3d, 0x95, 0x9d, 0xd4,
+ 0x10, 0xcd, 0xa5, 0x8e, 0xd4, 0x4a, 0xec, 0x04, 0xf5, 0x9d, 0x47, 0x28,
+ 0x2a, 0x1c, 0xa0, 0x1a, 0x3c, 0x74, 0xd2, 0x86, 0xda, 0x60, 0x0e, 0x68,
+ 0x33, 0x1f, 0x21, 0xdf, 0x32, 0x61, 0xf2, 0x3e, 0x7c, 0xf7, 0x2a, 0xd8,
+ 0xa2, 0xa7, 0xcb, 0xa3, 0x4d, 0xe6, 0x3c, 0x48, 0xb5, 0x5c, 0x44, 0xd4,
+ 0x5a, 0x91, 0x94, 0xda, 0xdf, 0x9b, 0x47, 0xa5, 0x0a, 0x1f, 0x74, 0x66,
+ 0x95, 0x95, 0x9c, 0x04, 0x4c, 0xaa, 0x20, 0x26, 0xe7, 0x28, 0xc4, 0x7a,
+ 0x38, 0xf9, 0x53, 0x33, 0x95, 0x96, 0x4e, 0xaf, 0x27, 0x11, 0x0a, 0x4d,
+ 0x23, 0xc5, 0x77, 0xe0, 0x60, 0xcb, 0x95, 0xc6, 0x98, 0x44, 0xcb, 0xec,
+ 0x2d, 0xa0, 0x37, 0x38, 0x79, 0x72, 0x39, 0x76, 0xee, 0xd2, 0x4a, 0xf9,
+ 0x50, 0x2f, 0xe2, 0x84, 0x17, 0x97, 0x97, 0x4c, 0xab, 0xc5, 0xb8, 0xae,
+ 0xb5, 0x99, 0x4d, 0xd6, 0xe9, 0x02, 0x6f, 0x2d, 0x3a, 0x5c, 0x12, 0x5f,
+ 0x1d, 0x5a, 0x74, 0x34, 0xd1, 0x94, 0x36, 0x0d, 0xc5, 0x63, 0x84, 0x12,
+ 0x9a, 0xa3, 0xa1, 0x73, 0x48, 0x2c, 0x9f, 0x60, 0x93, 0x4f, 0x84, 0xd9,
+ 0x9a, 0xd6, 0x3f, 0x70, 0x79, 0x4d, 0x96, 0x55, 0x6e, 0x5b, 0x52, 0x1c,
+ 0x02, 0xea, 0xae, 0xfb, 0x4f, 0xc1, 0xab, 0x43, 0x87, 0x67, 0x61, 0xd5,
+ 0x83, 0x9d, 0x35, 0xf5, 0x76, 0x0f, 0xc6, 0x9f, 0xf3, 0xdc, 0x29, 0x48,
+ 0x41, 0xa8, 0x23, 0x66, 0x43, 0x63, 0x5c, 0x28, 0x20, 0xf6, 0x33, 0xe6,
+ 0x00, 0x38, 0xaa, 0x7f, 0x05, 0xf5, 0xd7, 0x89, 0xc6, 0x6a, 0xaa, 0x24,
+ 0xcd, 0xde, 0x6c, 0xf9, 0xbc, 0xa8, 0xc3, 0xbd, 0x88, 0xeb, 0x1b, 0x73,
+ 0x1d, 0xb4, 0x50, 0x40, 0x75, 0x29, 0x75, 0x64, 0x83, 0xc4, 0x1c, 0x13,
+ 0x6a, 0xa3, 0x6e, 0xec, 0x0b, 0x5e, 0xce, 0xb4, 0x32, 0x72, 0x4e, 0xb8,
+ 0x54, 0x03, 0x57, 0x7e, 0x04, 0x1a, 0x8f, 0xea, 0x38, 0xac, 0x87, 0x3d,
+ 0x6a, 0x1a, 0x44, 0xfe, 0x34, 0x54, 0x60, 0xab, 0xfe, 0xbf, 0x2e, 0x95,
+ 0xdb, 0xf6, 0x49, 0x66, 0x58, 0x4d, 0xff, 0x93, 0xbf, 0x8f, 0x8a, 0xf9,
+ 0x16, 0xd0, 0xa2, 0x9e, 0x7d, 0xbe, 0x4f, 0xd0, 0xa3, 0xc7, 0xe1, 0x52,
+ 0x24, 0x4d, 0x15, 0x01, 0xfb, 0xca, 0x49, 0xec, 0x30, 0x85, 0xc9, 0x99,
+ 0x78, 0x0a, 0xcb, 0x79, 0x24, 0x56, 0x08, 0x4e, 0x27, 0x93, 0x63, 0xcd,
+ 0x0e, 0xed, 0x39, 0x4a, 0x3a, 0x79, 0x03, 0x19, 0xa7, 0x53, 0x03, 0xaa,
+ 0xf4, 0x71, 0xce, 0x03, 0x64, 0xaf, 0x55, 0x29, 0x53, 0x38, 0x23, 0x23,
+ 0xbc, 0x27, 0xbf, 0x42, 0x31, 0xd5, 0xab, 0xe9, 0xb1, 0xad, 0xda, 0x3f,
+ 0xbc, 0xff, 0x9e, 0x58, 0x91, 0x4f, 0x5e, 0xb5, 0x0d, 0x3b, 0xc0, 0x9d,
+ 0x76, 0x6c, 0xf0, 0x49, 0xb8, 0xf4, 0xc7, 0x81, 0x37, 0x2e, 0x98, 0x13,
+ 0xa5, 0x7e, 0x9b, 0x84, 0x73, 0x38, 0x8d, 0x66, 0x95, 0xbc, 0x17, 0xf0,
+ 0x01, 0x96, 0x3b, 0xa4, 0xb8, 0x54, 0xfc, 0x21, 0x9e, 0x7a, 0x91, 0xc0,
+ 0x19, 0x72, 0xd8, 0xd2, 0xe7, 0x3a, 0x63, 0xf0, 0xf2, 0xca, 0xf1, 0x86,
+ 0x0a, 0xdc, 0x1a, 0x87, 0xdc, 0xdb, 0x5f, 0x07, 0xad, 0xc1, 0xd4, 0xeb,
+ 0x3d, 0x15, 0x78, 0x3e, 0x9f, 0x03, 0xfb, 0x07, 0xff, 0xbe, 0x0d, 0xd2,
+ 0xea, 0xf0, 0x7d, 0xc1, 0x18, 0x19, 0x06, 0x35, 0xcc, 0xea, 0x47, 0x08,
+ 0x17, 0xe5, 0x7f, 0x1b, 0x3c, 0xcd, 0x6b, 0xf5, 0xd9, 0x48, 0xb9, 0x1e,
+ 0x44, 0x31, 0x41, 0x36, 0xcc, 0x11, 0x21, 0xcd, 0xa5, 0xe4, 0x77, 0x3e,
+ 0xca, 0xae, 0x06, 0x8a, 0x57, 0x36, 0xf1, 0xd4, 0x3f, 0x20, 0x1c, 0x3d,
+ 0x47, 0x9d, 0x2e, 0x13, 0x53, 0xf9, 0xb0, 0xf9, 0x33, 0x6e, 0x46, 0xcd,
+ 0xf2, 0x89, 0x77, 0x3b, 0xad, 0xe7, 0x3d, 0x1c, 0x95, 0x29, 0x48, 0x16,
+ 0x23, 0xa2, 0x22, 0x87, 0xe6, 0x31, 0x68, 0xd1, 0xd1, 0xa1, 0x6f, 0xe9,
+ 0x5a, 0xc3, 0x24, 0xc5, 0x40, 0x9a, 0x74, 0xc1, 0x80, 0x0b, 0xc5, 0xf6,
+ 0x32, 0x1d, 0x5c, 0xf4, 0x28, 0x3f, 0x57, 0x3e, 0x87, 0x93, 0xd5, 0x82,
+ 0x23, 0x77, 0xa9, 0x95, 0x3a, 0x01, 0x55, 0x87, 0x72, 0xf5, 0xe7, 0x10,
+ 0x6f, 0x10, 0xf8, 0x18, 0x69, 0x95, 0x08, 0x3c, 0xa8, 0xe1, 0x65, 0x88,
+ 0x4d, 0xbd, 0x2a, 0x9c, 0x61, 0xb9, 0x26, 0x1e, 0x98, 0x98, 0x88, 0x8c,
+ 0x62, 0x47, 0x37, 0x0b, 0x4d, 0xd0, 0xe6, 0x1b, 0x9d, 0x13, 0xf3, 0xcf,
+ 0xe1, 0xdb, 0x3b, 0xe6, 0xcc, 0x92, 0x85, 0xc3, 0xba, 0x68, 0xfd, 0xc8,
+ 0x90, 0xa8, 0xd5, 0x23, 0xd2, 0xe5, 0x90, 0x8b, 0x54, 0xc7, 0x8f, 0x45,
+ 0x17, 0xb7, 0x33, 0x30, 0xca, 0x67, 0x3a, 0x61, 0x9f, 0x70, 0xf4, 0xc5,
+ 0xa1, 0xae, 0x62, 0x3a, 0x1b, 0xb9, 0x5c, 0x0d, 0x40, 0x18, 0xab, 0x6b,
+ 0x37, 0x43, 0xf9, 0xec, 0xe4, 0x4f, 0xb7, 0x3f, 0x99, 0xfe, 0xaa, 0x5d,
+ 0x23, 0x27, 0x88, 0x44, 0xef, 0xd3, 0xcb, 0xfb, 0x3b, 0xc4, 0xcb, 0x78,
+ 0x72, 0x7c, 0x70, 0x03, 0x1b, 0xa8, 0xf7, 0xe9, 0xda, 0xc4, 0xee, 0x9a,
+ 0x79, 0xbb, 0x8a, 0xc6, 0x59, 0xd6, 0x51, 0x67, 0xd7, 0xf6, 0xcb, 0x61,
+ 0x8d, 0xe0, 0xf2, 0xaf, 0xa7, 0x8c, 0xa9, 0x4c, 0x9c, 0x6f, 0xec, 0x2c,
+ 0x33, 0x44, 0x85, 0x1f, 0x36, 0xdc, 0xc7, 0x47, 0xdd, 0xe0, 0x9c, 0xec,
+ 0xf0, 0xaf, 0xf2, 0x2f, 0x88, 0x45, 0x89, 0xf9, 0xd2, 0xc7, 0xba, 0xce,
+ 0x57, 0x24, 0x3d, 0x74, 0xa0, 0x8d, 0xe7, 0x4d, 0x7b, 0xa3, 0xda, 0x67,
+ 0x72, 0x29, 0x32, 0x0d, 0x3b, 0x6b, 0x74, 0xf7, 0xaa, 0xa3, 0x81, 0x8d,
+ 0x0b, 0x25, 0xfa, 0xd2, 0xbc, 0xb7, 0xb1, 0xa6, 0x33, 0xc5, 0xf8, 0x05,
+ 0x26, 0xb6, 0xdf, 0x96, 0x18, 0xd5, 0xf3, 0xab, 0xac, 0x20, 0x54, 0x95,
+ 0x0a, 0xb9, 0x29, 0x34, 0x5f, 0xd4, 0xc4, 0xdd, 0x5b, 0xb4, 0x60, 0xaf,
+ 0xe2, 0x87, 0xf9, 0x1b, 0x96, 0xd0, 0x75, 0x4a, 0x68, 0xcc, 0x4f, 0xef,
+ 0x48, 0x3a, 0x97, 0x4c, 0xa6, 0x57, 0x89, 0x9d, 0xfc, 0x36, 0x8b, 0x3a,
+ 0xb8, 0xa5, 0x20, 0x19, 0xf8, 0x98, 0x45, 0xb4, 0x48, 0xfc, 0xc8, 0xca,
+ 0xf7, 0x26, 0x1d, 0x57, 0xd2, 0xe7, 0x41, 0x86, 0x8e, 0x38, 0x9b, 0xb2,
+ 0x25, 0xf3, 0xfd, 0x26, 0x4d, 0xf6, 0x42, 0x69, 0x7d, 0x49, 0x8b, 0xbf,
+ 0x4d, 0x9f, 0xf7, 0x21, 0xcf, 0x2b, 0xbe, 0x63, 0x7e, 0xdc, 0x3e, 0x15,
+ 0xc9, 0x6f, 0x54, 0xf6, 0x21, 0x57, 0xcc, 0xb5, 0x4a, 0x0c, 0x6d, 0x67,
+ 0xcd, 0xff, 0xb2, 0xe1, 0x40, 0x57, 0x03, 0x8a, 0x57, 0xec, 0x97, 0x08,
+ 0x3b, 0xfe, 0xa9, 0xeb, 0x35, 0xaf, 0x11, 0x58, 0xcd, 0x42, 0x4e, 0xa0,
+ 0x6b, 0xf3, 0xb6, 0x30, 0x6a, 0xf0, 0xff, 0x0c, 0xe9, 0x27, 0x8c, 0x83,
+ 0x0f, 0x90, 0xe7, 0xc5, 0x40, 0xdc, 0xa0, 0xa5, 0x4f, 0xd7, 0x93, 0x45,
+ 0x2b, 0x10, 0x08, 0xa3, 0x89, 0x8a, 0xff, 0x7d, 0x1d, 0x55, 0x84, 0x05,
+ 0x89, 0x44, 0x33, 0x0e, 0x5d, 0xd2, 0x33, 0x71, 0xf7, 0xbe, 0x3c, 0xa1,
+ 0xfc, 0xa0, 0x7e, 0xbf, 0x86, 0x02, 0xb1, 0xb9, 0xf9, 0xec, 0xd7, 0x59,
+ 0x7f, 0xb9, 0x4e, 0x6c, 0xd7, 0x4f, 0x9f, 0xb0, 0x3d, 0x5d, 0x58, 0x59,
+ 0x00, 0x82, 0xa7, 0xf5, 0xb9, 0x33, 0x17, 0x43, 0x17, 0xef, 0xcb, 0x99,
+ 0x1a, 0x08, 0x15, 0x00, 0xd3, 0x53, 0xb9, 0x95, 0xe3, 0x11, 0x15, 0x49,
+ 0x07, 0x98, 0x6f, 0x2f, 0x97, 0x81, 0xf0, 0x61, 0xa7, 0xc0, 0xbe, 0xeb,
+ 0x3a, 0x30, 0xb1, 0x40, 0x3a, 0x0b, 0x1b, 0x9d, 0x8d, 0x4e, 0x94, 0x94,
+ 0x66, 0xe0, 0x57, 0x56, 0xb0, 0xf3, 0xf2, 0xcc, 0x0e, 0x88, 0xb8, 0xc4,
+ 0xfc, 0xfb, 0x93, 0xe6, 0x08, 0xa2, 0x25, 0xbf, 0xb1, 0x57, 0x8e, 0xb4,
+ 0x4d, 0x9e, 0x97, 0x01, 0xd3, 0x15, 0xa2, 0xea, 0x3f, 0x4b, 0x97, 0x91,
+ 0xf7, 0x4e, 0xb5, 0x03, 0xd2, 0x60, 0xd7, 0xca, 0x9c, 0x67, 0x68, 0x55,
+ 0x03, 0xd1, 0xbe, 0x3d, 0xb5, 0x71, 0x9a, 0x5e, 0xea, 0x68, 0x32, 0x7c,
+ 0x3d, 0xc4, 0x73, 0xf2, 0x5c, 0x2a, 0xb2, 0x17, 0xa0, 0x37, 0x3a, 0x8b,
+ 0x8f, 0x77, 0xa7, 0xe2, 0x5f, 0x6b, 0x06, 0xa4, 0x74, 0x97, 0xaa, 0xbe,
+ 0x3b, 0x03, 0xf6, 0x0c, 0xd6, 0x3a, 0xb0, 0x5a, 0x8c, 0x60, 0xdc, 0x1a,
+ 0x5a, 0xdc, 0xfc, 0xa6, 0x8d, 0x2b, 0x99, 0x88, 0xfc, 0xb8, 0x3e, 0xf8,
+ 0x66, 0x6c, 0xa7, 0x00, 0xff, 0xcb, 0x08, 0x99, 0x9c, 0x55, 0xe9, 0xdb,
+ 0x6b, 0x48, 0x62, 0xe5, 0x75, 0x73, 0x20, 0x79, 0x06, 0xea, 0xb4, 0x34,
+ 0xac, 0x92, 0xb4, 0x05, 0xff, 0x4e, 0x08, 0xa6, 0xc4, 0x19, 0x53, 0xe6,
+ 0xcf, 0xcc, 0xc8, 0xbb, 0xa4, 0x73, 0xe9, 0xec, 0xa9, 0x8a, 0x4e, 0xef,
+ 0xc6, 0xdf, 0xd3, 0xe9, 0xe0, 0x29, 0x0d, 0xe5, 0xb1, 0xf8, 0xaf, 0x27,
+ 0xbc, 0xd1, 0xe2, 0x03, 0x73, 0x4d, 0x19, 0x11, 0x4e, 0x3d, 0x33, 0xc7,
+ 0xab, 0x87, 0xa8, 0x31, 0x73, 0xa0, 0x47, 0xd3, 0x17, 0xfa, 0x11, 0xcc,
+ 0x07, 0x87, 0x7c, 0x78, 0xae, 0xac, 0x7a, 0x77, 0x6a, 0x26, 0x99, 0x95,
+ 0xb7, 0xd3, 0x4b, 0xf1, 0xb0, 0x46, 0x53, 0xff, 0x82, 0xde, 0xb6, 0x39,
+ 0xf5, 0xde, 0x8f, 0x12, 0x91, 0x1c, 0x27, 0xd5, 0x9d, 0xf2, 0x38, 0xa7,
+ 0x80, 0x2b, 0xf2, 0xa2, 0xbf, 0x41, 0x8b, 0x89, 0xf5, 0x27, 0x8d, 0xdf,
+ 0xc0, 0x79, 0xe4, 0x66, 0x67, 0x81, 0x6c, 0xcb, 0xa6, 0xf7, 0x92, 0x2a,
+ 0xf1, 0xf5, 0x22, 0x77, 0xd7, 0xe0, 0xf8, 0xa9, 0x9f, 0x2c, 0xd4, 0x11,
+ 0x6d, 0x10, 0xd1, 0xcf, 0x93, 0x35, 0x87, 0x61, 0xad, 0x5d, 0x0b, 0x52,
+ 0x71, 0x3d, 0x4c, 0x57, 0x69, 0xfa, 0xc1, 0x9a, 0xe4, 0xba, 0xeb, 0x9f,
+ 0xd3, 0xd4, 0x07, 0x87, 0x5a, 0x43, 0x29, 0x9d, 0x73, 0xfc, 0x4b, 0x94,
+ 0x42, 0x59, 0x78, 0x16, 0x91, 0x19, 0xdd, 0xf1, 0x38, 0x04, 0xa5, 0x5b,
+ 0x7e, 0x02, 0xe1, 0x6c, 0xd9, 0xd1, 0x17, 0x85, 0xe8, 0xcb, 0x9c, 0x38,
+ 0x3c, 0xa9, 0xa6, 0x13, 0x7d, 0x0a, 0xda, 0x53, 0x1c, 0x31, 0x25, 0xd7,
+ 0x21, 0x91, 0x74, 0xf8, 0x4c, 0xcf, 0xb2, 0xc6, 0x1c, 0xc7, 0xf6, 0x90,
+ 0x96, 0x40, 0x44, 0x24, 0xfe, 0x47, 0xe0, 0xe5, 0xe6, 0x40, 0x6c, 0x47,
+ 0x21, 0xaa, 0x48, 0xcb, 0x17, 0x4b, 0x1f, 0x49, 0x31, 0x89, 0xaa, 0xc6,
+ 0x84, 0x2b, 0x42, 0x28, 0xf9, 0x45, 0x3c, 0xa8, 0xa7, 0xfa, 0x68, 0x08,
+ 0x84, 0x28, 0x9b, 0x08, 0x2d, 0xe0, 0x9e, 0x6c, 0x1c, 0xe4, 0x51, 0xc0,
+ 0x6d, 0x76, 0x35, 0x0b, 0xd2, 0x4c, 0xe6, 0x6f, 0xcf, 0xfd, 0xde, 0x51,
+ 0xbd, 0xc3, 0x60, 0xf0, 0xeb, 0x53, 0xba, 0xfd, 0x3c, 0xa7, 0xfd, 0x64,
+ 0x9a, 0xfc, 0xb4, 0xb2, 0x5b, 0x63, 0xa9, 0xd5, 0x34, 0x98, 0x23, 0x6d,
+ 0x7b, 0x8a, 0xa8, 0x9a, 0x03, 0x76, 0x9b, 0xdf, 0x9c, 0x20, 0x89, 0x78,
+ 0xf1, 0x46, 0x0f, 0x99, 0x51, 0xf0, 0x9b, 0x19, 0x12, 0xc0, 0x96, 0x64,
+ 0xf8, 0x90, 0xa0, 0x4a, 0xa9, 0xec, 0xdf, 0x8e, 0xa1, 0x2c, 0x25, 0x01,
+ 0xcd, 0x38, 0x15, 0x8b, 0x0b, 0x33, 0xec, 0xbb, 0xa2, 0x64, 0x4f, 0xd0,
+ 0x35, 0x47, 0x2f, 0x20, 0x5d, 0x3c, 0x0f, 0x20, 0xca, 0x3b, 0x6a, 0xb2,
+ 0x76, 0x7a, 0xaa, 0x9a, 0xd1, 0x83, 0xd1, 0xab, 0x07, 0xc0, 0x19, 0x97,
+ 0x83, 0xf1, 0xe5, 0xe1, 0x81, 0x70, 0x84, 0x39, 0x4c, 0x17, 0x36, 0x04,
+ 0xc0, 0x99, 0x48, 0x51, 0x2a, 0x8e, 0x80, 0x94, 0x67, 0xcd, 0x1e, 0x03,
+ 0x99, 0x9e, 0x23, 0x26, 0xbd, 0x0e, 0x4b, 0x46, 0xfc, 0x8e, 0xc4, 0x8e,
+ 0x8a, 0xc1, 0xfd, 0x7f, 0xa1, 0x11, 0x87, 0x5f, 0x03, 0x5e, 0x9f, 0x25,
+ 0xb2, 0x3f, 0xf0, 0xcd, 0xdb, 0x30, 0x20, 0x8c, 0xb6, 0x4a, 0x08, 0xf1,
+ 0x7b, 0x21, 0xd6, 0x68, 0xbe, 0x96, 0xcf, 0x17, 0x3f, 0x15, 0x10, 0x07,
+ 0xb5, 0xd2, 0x34, 0xee, 0x73, 0xcd, 0x69, 0xdb, 0x4c, 0x19, 0xc2, 0xca,
+ 0xa2, 0x96, 0x92, 0xab, 0x8f, 0x20, 0xf2, 0xf2, 0x5d, 0xe1, 0xb4, 0xfa,
+ 0x82, 0xa9, 0xd7, 0xc9, 0xb2, 0x64, 0x25, 0xbb, 0x09, 0xa6, 0x4d, 0xf8,
+ 0x12, 0x8d, 0x7c, 0x65, 0x81, 0xba, 0x6c, 0x5f, 0x5b, 0x72, 0x04, 0x67,
+ 0x51, 0x66, 0xbb, 0xb6, 0x5c, 0x83, 0x6d, 0xc1, 0x18, 0xde, 0x84, 0x4a,
+ 0x9d, 0xb6, 0x46, 0x9f, 0xa5, 0x6f, 0x0a, 0x97, 0x5c, 0x16, 0xe7, 0x24,
+ 0x54, 0x3a, 0x12, 0xbd, 0x76, 0xec, 0xd7, 0x14, 0x8a, 0x0e, 0xed, 0xb8,
+ 0xc9, 0xf6, 0x2c, 0x58, 0x49, 0x1a, 0x4a, 0x5a, 0xaf, 0xb8, 0xfb, 0x0a,
+ 0xe5, 0x0f, 0xfd, 0x63, 0xbb, 0x21, 0x81, 0xdf, 0x9d, 0x14, 0x55, 0x46,
+ 0x30, 0x28, 0x47, 0x11, 0x1d, 0x2b, 0xc5, 0x81, 0xa0, 0x41, 0xdb, 0x3b,
+ 0xfe, 0xb6, 0xe7, 0xe6, 0x9d, 0xa4, 0xe0, 0xda, 0x45, 0x04, 0xe4, 0x2d,
+ 0x88, 0x60, 0x14, 0xc1, 0xe3, 0xb4, 0xc0, 0x16, 0xa6, 0x22, 0x4f, 0x55,
+ 0x26, 0x6e, 0x67, 0x73, 0xf1, 0x42, 0x9a, 0x2f, 0xbe, 0x46, 0xd5, 0x35,
+ 0xe6, 0x28, 0x98, 0xa4, 0x20, 0x85, 0x54, 0x4f, 0x17, 0x09, 0xa1, 0x75,
+ 0x73, 0xd5, 0x6b, 0xc8, 0x79, 0xc2, 0x63, 0xd3, 0xe0, 0x03, 0x5c, 0x6b,
+ 0x97, 0x9d, 0x41, 0x48, 0xff, 0x7d, 0x50, 0xba, 0x88, 0x62, 0xc9, 0x6c,
+ 0x8c, 0x39, 0x29, 0x91, 0x3f, 0xd0, 0xa7, 0x9c, 0x06, 0x17, 0x13, 0x41,
+ 0x43, 0x37, 0x12, 0xf2, 0x4b, 0xf4, 0x3d, 0xca, 0x63, 0xf1, 0x60, 0xf2,
+ 0xb0, 0xe0, 0x17, 0xc6, 0x13, 0x00, 0x35, 0x2a, 0x2e, 0xc7, 0x50, 0x19,
+ 0x80, 0x41, 0x26, 0x8b, 0xf9, 0x77, 0xab, 0x44, 0xd0, 0x53, 0x5e, 0xb1,
+ 0x16, 0xd2, 0x61, 0xdc, 0xad, 0xe9, 0xd1, 0x18, 0x54, 0x52, 0xa4, 0xe2,
+ 0x70, 0xee, 0xb0, 0x3e, 0x25, 0x95, 0x61, 0xe3, 0x4f, 0xfa, 0x1a, 0xd1,
+ 0xb0, 0xef, 0x93, 0xba, 0xff, 0xc7, 0x0c, 0x53, 0xfa, 0x8e, 0x51, 0x87,
+ 0x18, 0xf4, 0x9b, 0x59, 0x5f, 0xec, 0x16, 0x0a, 0xb8, 0x45, 0xf4, 0x12,
+ 0x18, 0xdb, 0x75, 0xd3, 0x61, 0x7d, 0xc2, 0x44, 0xf9, 0xdd, 0x21, 0x50,
+ 0xea, 0xde, 0x04, 0x5d, 0x59, 0x82, 0xbf, 0x1a, 0xbb, 0x07, 0xf6, 0x32,
+ 0x66, 0x00, 0x46, 0x1e, 0x1e, 0x62, 0x90, 0xb0, 0x63, 0x45, 0xe7, 0x40,
+ 0x83, 0xb3, 0x9e, 0x81, 0xa8, 0x0e, 0x6f, 0x3f, 0xbf, 0x50, 0x36, 0xcb,
+ 0x07, 0x2c, 0x0a, 0xb2, 0xe5, 0x15, 0x27, 0xb2, 0x02, 0x96, 0x30, 0x36,
+ 0x89, 0x87, 0xd6, 0x82, 0x13, 0xb9, 0xcb, 0x77, 0x78, 0xc2, 0xf2, 0xa6,
+ 0xdd, 0xa2, 0xa3, 0x9a, 0xc8, 0xdc, 0xb5, 0x99, 0x3f, 0x3d, 0x68, 0x99,
+ 0xd9, 0x31, 0xa6, 0xf6, 0x9c, 0xd2, 0x67, 0x19, 0xd8, 0x2c, 0x01, 0xb7,
+ 0xc6, 0xa8, 0xa1, 0xc5, 0x5a, 0x41, 0x08, 0xee, 0xe8, 0x2f, 0x9b, 0x9e,
+ 0xfc, 0x91, 0xd4, 0x4b, 0x04, 0x03, 0x3e, 0xbf, 0x9b, 0x90, 0x5c, 0x73,
+ 0xc4, 0x55, 0x43, 0x7e, 0x08, 0x8e, 0x63, 0x4c, 0xd0, 0xd0, 0xc8, 0x74,
+ 0x40, 0x9d, 0x4a, 0x41, 0x67, 0x4b, 0xc9, 0x27, 0xdb, 0xef, 0x44, 0x79,
+ 0xeb, 0x05, 0x86, 0x53, 0xcd, 0x95, 0x5b, 0x43, 0xab, 0x31, 0xf4, 0xd3,
+ 0x50, 0xda, 0x40, 0xfe, 0x6b, 0x9d, 0x85, 0x7f, 0xf8, 0x00, 0x28, 0xce,
+ 0x66, 0x72, 0xbe, 0x3e, 0xc1, 0xc4, 0xfc, 0x85, 0x6a, 0x61, 0x1e, 0xab,
+ 0x7e, 0xb6, 0xc8, 0xc4, 0xa4, 0x6f, 0x16, 0xd4, 0x56, 0xf4, 0xaa, 0x34,
+ 0x12, 0xdf, 0x94, 0x31, 0xce, 0xaf, 0x43, 0x63, 0x8f, 0x1d, 0xe4, 0x4b,
+ 0xc4, 0xba, 0x5e, 0x7f, 0x6d, 0x75, 0x73, 0x24, 0x09, 0x45, 0xa7, 0x5f,
+ 0x59, 0x40, 0x1a, 0x0e, 0x63, 0xe0, 0x86, 0xb3, 0xe7, 0x88, 0xec, 0x97,
+ 0xb7, 0x36, 0x7f, 0xd5, 0x91, 0x78, 0x23, 0x3e, 0x60, 0xdb, 0xae, 0x7f,
+ 0xc6, 0x8c, 0x43, 0xfb, 0x99, 0xe6, 0xc8, 0xd3, 0xc3, 0xd6, 0xb4, 0x99,
+ 0xb7, 0xaa, 0xcc, 0xd0, 0xb2, 0x48, 0x97, 0xc7, 0x2e, 0x77, 0x21, 0x93,
+ 0x32, 0x9a, 0x4a, 0x62, 0x2a, 0xa3, 0x59, 0x5a, 0x39, 0x6a, 0x2f, 0xaf,
+ 0xec, 0x42, 0xbd, 0xdd, 0x70, 0x0f, 0xeb, 0x27, 0x0a, 0xc2, 0x92, 0x15,
+ 0xe5, 0x7e, 0xf5, 0x8f, 0x85, 0x30, 0xd9, 0x7d, 0x3f, 0xa5, 0x9e, 0x2a,
+ 0x3f, 0xd3, 0x82, 0x23, 0x2c, 0x9a, 0x6a, 0xf0, 0x4b, 0xf9, 0x87, 0xde,
+ 0x5e, 0xe7, 0x8f, 0x8d, 0x8f, 0x6b, 0x77, 0xc3, 0x62, 0x81, 0x2d, 0x68,
+ 0x8a, 0x90, 0x80, 0x56, 0x4e, 0x30, 0xbc, 0xef, 0x22, 0xbd, 0x24, 0x72,
+ 0xb4, 0xf3, 0x65, 0x01, 0x01, 0xf6, 0xb0, 0xbe, 0x7a, 0x76, 0x87, 0xb9,
+ 0xc3, 0xbe, 0x3c, 0x99, 0x76, 0x93, 0xe2, 0x5b, 0x58, 0x88, 0xae, 0x59,
+ 0xfb, 0x13, 0x7f, 0x5b, 0xf0, 0x79, 0xd6, 0xd1, 0x27, 0x21, 0x14, 0x5e,
+ 0x18, 0x21, 0x02, 0xb9, 0xe5, 0x51, 0x7c, 0x87, 0xb0, 0x82, 0xa7, 0x8d,
+ 0x06, 0x6f, 0x94, 0x01, 0x64, 0x72, 0x35, 0xc3, 0x37, 0x5a, 0xae, 0x51,
+ 0x87, 0x54, 0xcd, 0xd3, 0x8f, 0x62, 0x8c, 0xc9, 0xf7, 0x17, 0xe6, 0x40,
+ 0x39, 0x0f, 0x9b, 0x9f, 0x2a, 0x5c, 0xe2, 0x29, 0x84, 0x54, 0xcb, 0xd5,
+ 0x72, 0xac, 0x37, 0x0e, 0xbf, 0x56, 0x77, 0x6d, 0xdb, 0x96, 0x63, 0xa0,
+ 0xad, 0xfe, 0xc8, 0x6a, 0x57, 0xd1, 0x21, 0x63, 0x42, 0x7a, 0xe1, 0x40,
+ 0x5a, 0x47, 0x75, 0xb7, 0xd1, 0x7a, 0x5b, 0xde, 0x69, 0x57, 0x90, 0xff,
+ 0xcb, 0x0b, 0x30, 0xf5, 0xda, 0xab, 0xa6, 0x07, 0xf9, 0xd7, 0x80, 0x1d,
+ 0xe4, 0xe1, 0xf1, 0x79, 0x28, 0xe3, 0xfe, 0x7a, 0x3a, 0xd3, 0xb3, 0x44,
+ 0x7e, 0x4d, 0xaf, 0x0a, 0x22, 0x89, 0x54, 0x0d, 0x12, 0x44, 0xdc, 0xb0,
+ 0x07, 0xa0, 0xd4, 0x24, 0x7f, 0xc5, 0xad, 0x6c, 0x53, 0x5c, 0x2a, 0x50,
+ 0x8d, 0x6a, 0x74, 0x30, 0x09, 0x14, 0x2c, 0x09, 0x9d, 0x30, 0xb5, 0xc8,
+ 0x09, 0x3b, 0x22, 0xb8, 0xe9, 0x41, 0xe6, 0x3d, 0x73, 0x15, 0xb7, 0xa3,
+ 0x6d, 0xb7, 0xad, 0x3d, 0xeb, 0xad, 0x97, 0x15, 0xb9, 0xc2, 0x19, 0x0f,
+ 0xb2, 0x50, 0xde, 0xab, 0x4d, 0x05, 0xb4, 0x6a, 0x4a, 0x01, 0x65, 0xb0,
+ 0x6e, 0xa5, 0x7c, 0xab, 0x15, 0xc0, 0xb1, 0x7d, 0xdf, 0x38, 0x08, 0xf9,
+ 0x89, 0x71, 0x42, 0x11, 0x0c, 0xcf, 0x79, 0x73, 0x46, 0x33, 0x0d, 0x14,
+ 0x4e, 0xf4, 0x55, 0x47, 0x02, 0x7f, 0x91, 0xeb, 0xa5, 0x23, 0xad, 0xf9,
+ 0xe6, 0xfc, 0x22, 0xc3, 0x05, 0x38, 0xdf, 0x72, 0xf5, 0x16, 0x6e, 0x45,
+ 0xc1, 0xac, 0x60, 0x50, 0x8d, 0xaa, 0x29, 0x0c, 0x7e, 0x39, 0x21, 0xeb,
+ 0x8b, 0x45, 0x76, 0x60, 0xb7, 0x98, 0x9a, 0x2e, 0x7c, 0xa0, 0x71, 0xb4,
+ 0x38, 0x3b, 0x41, 0xea, 0xdf, 0x1a, 0x17, 0x5e, 0x13, 0xbe, 0x11, 0x72,
+ 0x66, 0x1f, 0xad, 0x54, 0xd9, 0xee, 0x4d, 0xef, 0x6a, 0x32, 0x5f, 0x14,
+ 0x3f, 0x3b, 0xf4, 0x23, 0x67, 0xa6, 0x12, 0x72, 0x69, 0xd5, 0x62, 0x9e,
+ 0xd5, 0xe5, 0xec, 0x4b, 0x7a, 0x4b, 0x96, 0xde, 0xcc, 0x90, 0x9e, 0xbd,
+ 0x40, 0xbb, 0x8b, 0xa6, 0x38, 0xd4, 0x37, 0x22, 0x7e, 0x0f, 0x86, 0x43,
+ 0x94, 0x3b, 0x72, 0xb7, 0x00, 0x0f, 0x5a, 0x62, 0x6d, 0x87, 0xd1, 0xb1,
+ 0xdf, 0x2a, 0x3e, 0xda, 0x1c, 0x86, 0x2c, 0x08, 0xf1, 0x81, 0x18, 0xf9,
+ 0x20, 0x51, 0x3b, 0x68, 0x1e, 0x7d, 0x04, 0xc0, 0x04, 0x29, 0xad, 0x63,
+ 0xe9, 0x03, 0xcf, 0x73, 0x5d, 0x27, 0x2e, 0xa5, 0x49, 0xe9, 0x57, 0xe1,
+ 0x9a, 0x89, 0x2f, 0x52, 0xa8, 0x9b, 0x56, 0x66, 0xa0, 0x54, 0xd3, 0x1b,
+ 0x06, 0x70, 0x5f, 0x07, 0xc9, 0x5b, 0x56, 0xf0, 0xa9, 0xd6, 0x5f, 0x9b,
+ 0xd0, 0x7b, 0x59, 0x98, 0xa1, 0x3f, 0xf3, 0x32, 0x9d, 0xda, 0x64, 0xa0,
+ 0xbb, 0x33, 0x82, 0x69, 0x61, 0x1e, 0xc3, 0xb7, 0xab, 0x88, 0xe7, 0x40,
+ 0xb6, 0x07, 0x41, 0x6c, 0x96, 0x4d, 0xda, 0x95, 0x05, 0x30, 0x15, 0x69,
+ 0xc1, 0xdb, 0x8e, 0xda, 0x04, 0xff, 0x26, 0x19, 0xbe, 0x7a, 0x53, 0xcf,
+ 0xe1, 0xab, 0x95, 0x24, 0xb6, 0xcb, 0xd0, 0xd8, 0xa8, 0x8e, 0x56, 0x9c,
+ 0x38, 0x69, 0xb9, 0x80, 0x73, 0xbc, 0xde, 0xbd, 0xf6, 0xde, 0xd2, 0xd1,
+ 0x95, 0x01, 0x55, 0x51, 0x56, 0x13, 0x25, 0xf2, 0x43, 0x6b, 0x88, 0xc0,
+ 0xd2, 0x32, 0x01, 0xb6, 0x05, 0x10, 0x57, 0x44, 0x89, 0x06, 0x9e, 0x19,
+ 0xf9, 0x04, 0x8a, 0xff, 0xb9, 0x6c, 0xb3, 0x10, 0x13, 0x4e, 0x49, 0xae,
+ 0xfe, 0x8b, 0xb0, 0xe4, 0xc3, 0xdb, 0xf9, 0x00, 0x84, 0xc1, 0x3f, 0x48,
+ 0x18, 0xd9, 0x3f, 0xb8, 0x53, 0x31, 0xa9, 0x62, 0xdd, 0x61, 0x54, 0xd0,
+ 0xd5, 0x32, 0x3e, 0xad, 0x02, 0x47, 0xc5, 0x28, 0x51, 0x50, 0xa6, 0x0e,
+ 0xb8, 0x94, 0xee, 0x03, 0x47, 0xae, 0x4c, 0x3b, 0x43, 0x5b, 0xd6, 0x4c,
+ 0x49, 0x3e, 0x65, 0xbc, 0xfc, 0x7f, 0x35, 0xfc, 0x4a, 0xdd, 0xda, 0xb3,
+ 0x33, 0x76, 0xf2, 0x08, 0xb7, 0x54, 0xc7, 0x28, 0x1a, 0xc1, 0xa8, 0xe8,
+ 0xf4, 0x35, 0xd3, 0xd4, 0x29, 0x1d, 0xe5, 0x68, 0x42, 0x1a, 0x22, 0x69,
+ 0x63, 0xe9, 0x72, 0x54, 0xcd, 0x93, 0xde, 0x93, 0xba, 0x4f, 0xa6, 0x2d,
+ 0x05, 0xaa, 0x53, 0x99, 0xbc, 0x29, 0x15, 0x3b, 0xbe, 0xba, 0x37, 0x09,
+ 0x12, 0xe7, 0xf3, 0x2a, 0x8f, 0x51, 0x11, 0x1a, 0x71, 0x3c, 0xb9, 0x46,
+ 0x05, 0x22, 0xd6, 0x3a, 0xe3, 0x2e, 0xc5, 0x04, 0x81, 0x4e, 0x0e, 0x6e,
+ 0xad, 0x83, 0xa7, 0x20, 0x6b, 0xcc, 0x7a, 0x92, 0x8b, 0x3e, 0xf8, 0x67,
+ 0x69, 0x59, 0x16, 0x1e, 0xe1, 0x24, 0xbe, 0xbc, 0x4a, 0x0d, 0x80, 0x06,
+ 0xcd, 0x77, 0x89, 0x14, 0x64, 0xbf, 0x5b, 0x2a, 0x35, 0x09, 0xe3, 0x86,
+ 0x73, 0x90, 0xc3, 0x2d, 0xff, 0x1a, 0xc9, 0x83, 0x6e, 0x5d, 0xd9, 0xea,
+ 0xa2, 0xf1, 0xc0, 0xe6, 0x26, 0xa4, 0xc7, 0x9c, 0xd9, 0x61, 0xa8, 0xa8,
+ 0xa9, 0xfe, 0xfd, 0xf3, 0xdd, 0x86, 0x18, 0x57, 0xe9, 0x4d, 0xec, 0x0e,
+ 0x26, 0x49, 0xe5, 0xaa, 0x9e, 0x14, 0xc5, 0xfe, 0xbb, 0xdc, 0x3c, 0x8d,
+ 0x3b, 0x47, 0xb3, 0xd9, 0xe1, 0x0d, 0x8d, 0x2a, 0xb6, 0xff, 0xaa, 0xcc,
+ 0x5c, 0xd2, 0x21, 0xea, 0xa7, 0x1d, 0x97, 0xd7, 0xd1, 0xa9, 0xa7, 0x27,
+ 0x98, 0x4f, 0x89, 0x11, 0xb6, 0xc9, 0x51, 0x8e, 0x49, 0xef, 0xb3, 0x0c,
+ 0x7e, 0x59, 0xb1, 0x8e, 0x0e, 0xb1, 0x1b, 0x67, 0x5c, 0x29, 0xf9, 0x92,
+ 0x77, 0x11, 0x8f, 0x34, 0x8e, 0xb2, 0x1c, 0x02, 0xab, 0x55, 0x8c, 0x71,
+ 0xcb, 0xeb, 0x63, 0xfe, 0x3b, 0x96, 0x5c, 0x5f, 0x0b, 0xa7, 0xc8, 0xfc,
+ 0x02, 0xdf, 0x77, 0x29, 0x81, 0x93, 0x78, 0x40, 0xf9, 0xb9, 0x3a, 0x53,
+ 0xd5, 0x5c, 0x75, 0x39, 0xa1, 0x0e, 0x24, 0x42, 0x15, 0x95, 0xfc, 0x10,
+ 0xbc, 0x2f, 0xbf, 0xb5, 0xb5, 0x02, 0xaf, 0xdf, 0x0b, 0x8d, 0xdf, 0x06,
+ 0x5e, 0x89, 0xab, 0x10, 0xe8, 0x2d, 0x48, 0x6a, 0x3f, 0x20, 0x9f, 0x52,
+ 0x1d, 0x82, 0xab, 0x31, 0xfb, 0xeb, 0x86, 0xe1, 0xb2, 0x8b, 0x16, 0x9c,
+ 0xdd, 0xa9, 0x19, 0xb7, 0xbc, 0xe4, 0x1f, 0x7a, 0x95, 0x87, 0x22, 0xac,
+ 0x85, 0x66, 0x18, 0x5a, 0x56, 0xc5, 0x9f, 0xb3, 0xf2, 0x7b, 0x1e, 0x3e,
+ 0x61, 0xb5, 0x19, 0x04, 0x11, 0xe6, 0xe7, 0x43, 0x78, 0x2a, 0x3b, 0x3b,
+ 0x34, 0x71, 0x6a, 0x74, 0x2b, 0xb7, 0x03, 0x50, 0xe3, 0x76, 0x1c, 0x4a,
+ 0xf2, 0xda, 0xd6, 0x79, 0x69, 0x78, 0x20, 0xff, 0x44, 0xd3, 0x7f, 0x04,
+ 0x82, 0x70, 0x5e, 0x80, 0xf2, 0x5a, 0x22, 0xc0, 0xf4, 0x61, 0x65, 0x92,
+ 0x7f, 0xc7, 0x86, 0xa9, 0xe5, 0xf6, 0x67, 0x30, 0xfc, 0x59, 0x60, 0xb2,
+ 0xff, 0x17, 0xc0, 0xef, 0xd6, 0xec, 0xdb, 0xfe, 0xbf, 0x9e, 0xa6, 0x86,
+ 0x83, 0x6e, 0xa9, 0xdc, 0xd2, 0xf5, 0x61, 0x06, 0xd5, 0xe2, 0x74, 0x83,
+ 0x9d, 0x36, 0x80, 0xe7, 0xb0, 0xa9, 0x5f, 0xdc, 0x2e, 0x33, 0x99, 0x0e,
+ 0xa4, 0xd7, 0xae, 0xaf, 0x4e, 0x67, 0xa1, 0x1e, 0x3b, 0x1c, 0x9e, 0x57,
+ 0x12, 0xd0, 0xe5, 0x87, 0x6a, 0xa3, 0xea, 0xf1, 0x40, 0x64, 0x9b, 0x7a,
+ 0x20, 0x6b, 0x9d, 0xb0, 0x75, 0xf2, 0x16, 0x5f, 0x57, 0x0f, 0xa7, 0xfe,
+ 0x5c, 0x16, 0x0a, 0xd4, 0x7d, 0x19, 0x8f, 0x69, 0xb2, 0x42, 0x70, 0x90,
+ 0xca, 0x56, 0x04, 0xb7, 0x60, 0x8f, 0xa8, 0x32, 0x1a, 0xf5, 0x30, 0x87,
+ 0x7d, 0x2f, 0x0e, 0xd9, 0xec, 0x24, 0x7c, 0xae, 0xdf, 0x16, 0x88, 0xf8,
+ 0x89, 0x1a, 0x47, 0xb6, 0xd9, 0x3b, 0x6f, 0xda, 0xdc, 0x56, 0x42, 0x5a,
+ 0xe6, 0xbe, 0xfe, 0x5d, 0x91, 0x70, 0x2e, 0xe7, 0x5c, 0x78, 0x57, 0x16,
+ 0xb8, 0xfe, 0x97, 0x30, 0x0d, 0x6f, 0xe8, 0x62, 0xa0, 0xa3, 0x25, 0xa6,
+ 0x96, 0x46, 0x87, 0xaa, 0xa7, 0x8f, 0xdf, 0x84, 0x6d, 0x28, 0x43, 0x6a,
+ 0xea, 0x7a, 0xe4, 0x10, 0xb1, 0x03, 0xcb, 0xbf, 0xb8, 0x4a, 0x89, 0x53,
+ 0xbe, 0xb7, 0xac, 0xbd, 0x98, 0x71, 0xad, 0x72, 0xb4, 0xd1, 0x74, 0xb2,
+ 0xeb, 0x83, 0x65, 0x32, 0x33, 0x8b, 0x1a, 0xfe, 0xb7, 0x95, 0x1e, 0x38,
+ 0xa7, 0xf9, 0xa5, 0x21, 0x52, 0x3f, 0x8c, 0xeb, 0x98, 0x5d, 0xce, 0x5d,
+ 0x1e, 0x45, 0xdd, 0xd5, 0xae, 0x62, 0x43, 0xab, 0x45, 0x2d, 0xaa, 0xfb,
+ 0x6d, 0x52, 0xdc, 0x43, 0xc2, 0x60, 0x11, 0xdd, 0x3f, 0x1b, 0x00, 0xec,
+ 0x88, 0x50, 0x9f, 0x39, 0xf1, 0xa8, 0x0a, 0xa4, 0x4c, 0xea, 0xd2, 0xe5,
+ 0x18, 0x0b, 0x19, 0x9e, 0xc2, 0x08, 0x0f, 0x5b, 0x2e, 0x44, 0x4c, 0xa7,
+ 0xe1, 0xd5, 0x72, 0x56, 0xba, 0xad, 0x7a, 0x06, 0xb6, 0x06, 0x1b, 0xff,
+ 0xe4, 0x85, 0x3d, 0xf9, 0x7e, 0x08, 0x8b, 0x71, 0x7d, 0x4b, 0x85, 0x8f,
+ 0x6a, 0x12, 0x20, 0x2e, 0x5c, 0x1c, 0x7e, 0xc8, 0xc6, 0xee, 0x5c, 0x8f,
+ 0x01, 0x6b, 0x68, 0x63, 0xc0, 0xfb, 0x87, 0x5e, 0x02, 0x29, 0x3f, 0x72,
+ 0x20, 0x3f, 0x4e, 0x32, 0x2c, 0x41, 0xdf, 0x59, 0x8c, 0xec, 0x8e, 0x9a,
+ 0x96, 0x46, 0xb6, 0x41, 0xe2, 0x25, 0xbc, 0x85, 0xef, 0xd4, 0xed, 0x60,
+ 0xf7, 0x52, 0xc2, 0xa3, 0x81, 0xe7, 0x2d, 0xbb, 0x25, 0xaa, 0x14, 0x2d,
+ 0x6c, 0xe2, 0x9d, 0xad, 0x63, 0xc6, 0x03, 0x3c, 0x94, 0x75, 0x8e, 0x92,
+ 0x39, 0x55, 0x89, 0xb0, 0xb3, 0x5f, 0x00, 0x85, 0xb5, 0xc9, 0x6b, 0x8f,
+ 0xc6, 0x05, 0xd4, 0xc5, 0x01, 0xc9, 0x82, 0xd2, 0x25, 0x86, 0x4e, 0x7a,
+ 0x11, 0xdc, 0xaf, 0x99, 0xcc, 0xb4, 0x02, 0x55, 0x45, 0xe5, 0xe5, 0x21,
+ 0x5c, 0xff, 0xf0, 0xaa, 0x98, 0xc1, 0xf2, 0xce, 0xeb, 0x98, 0xdd, 0x9c,
+ 0xf2, 0xd6, 0xc0, 0xa2, 0xae, 0x0e, 0xf4, 0x26, 0x3f, 0xfc, 0x5a, 0xb0,
+ 0xa1, 0xb3, 0x66, 0xa7, 0x9e, 0x3d, 0xca, 0x0e, 0xc6, 0x4e, 0x63, 0x81,
+ 0xca, 0x82, 0xa9, 0x59, 0xaf, 0x26, 0x98, 0x75, 0x81, 0x77, 0x50, 0x0a,
+ 0x82, 0x77, 0x68, 0x29, 0x40, 0x35, 0x30, 0x6c, 0xe7, 0xb6, 0x40, 0x72,
+ 0xeb, 0x40, 0x27, 0xb0, 0x14, 0x2e, 0x37, 0x4b, 0x21, 0x17, 0x91, 0xd8,
+ 0x88, 0x32, 0x8b, 0x3a, 0xd3, 0x17, 0xea, 0x51, 0x88, 0xbc, 0x7a, 0xeb,
+ 0xa2, 0x5a, 0x72, 0x7d, 0xff, 0x6e, 0x4f, 0x07, 0xc6, 0xc0, 0x0c, 0x20,
+ 0x69, 0x2e, 0xdd, 0xa0, 0x1f, 0xec, 0xc7, 0x77, 0x31, 0xcb, 0x76, 0x3c,
+ 0x1f, 0xff, 0x08, 0xcb, 0x35, 0x20, 0xe7, 0xed, 0xb9, 0xfb, 0x3e, 0xb0,
+ 0x33, 0xbb, 0x7a, 0x90, 0x8a, 0x90, 0x08, 0x7a, 0xd3, 0x63, 0xa3, 0x59,
+ 0xe3, 0x28, 0xcc, 0xa0, 0x2a, 0xd3, 0x39, 0xc9, 0x11, 0x54, 0xac, 0xaa,
+ 0x54, 0xa7, 0x77, 0x1a, 0xf9, 0x2e, 0x28, 0x6b, 0x18, 0x3c, 0x45, 0x31,
+ 0x90, 0xfc, 0x1e, 0xdd, 0xf2, 0xff, 0x55, 0xec, 0xb3, 0x6d, 0x50, 0xec,
+ 0x5e, 0xad, 0x2b, 0x2e, 0x66, 0x2e, 0x47, 0x0f, 0x8c, 0x6d, 0xe5, 0xb0,
+ 0x6c, 0x06, 0xea, 0x34, 0x5d, 0x8f, 0xda, 0x89, 0x3f, 0xb7, 0xbc, 0x11,
+ 0x18, 0xee, 0x1b, 0x31, 0x6f, 0x89, 0x41, 0x22, 0xa9, 0x12, 0xed, 0xea,
+ 0xf5, 0x71, 0x76, 0xf0, 0xda, 0x07, 0xff, 0xd9, 0x0c, 0xed, 0x64, 0x48,
+ 0x80, 0xee, 0x7b, 0xf4, 0x71, 0x65, 0x37, 0x5e, 0x1a, 0x72, 0xbf, 0x2e,
+ 0xa5, 0xb6, 0xdb, 0x74, 0xdf, 0xf2, 0x65, 0xba, 0xa0, 0x03, 0x5e, 0x41,
+ 0x48, 0xeb, 0xfa, 0xac, 0x70, 0xd6, 0x54, 0xeb, 0xe7, 0x0b, 0x4e, 0x76,
+ 0xbb, 0xc2, 0x12, 0x1f, 0x43, 0x33, 0x36, 0x1d, 0xef, 0xd7, 0xab, 0x52,
+ 0xf6, 0x51, 0xe5, 0xe8, 0x19, 0x9d, 0x9f, 0xaa, 0xe7, 0x1e, 0x0c, 0x4e,
+ 0x95, 0x52, 0x99, 0x8c, 0xf3, 0x8d, 0x03, 0xf2, 0xa4, 0xf6, 0x1c, 0x7a,
+ 0x4b, 0xfe, 0x0c, 0x4c, 0x75, 0x55, 0x52, 0x3b, 0x17, 0x45, 0xe0, 0x81,
+ 0xf2, 0xb8, 0x3e, 0x44, 0x0e, 0x2c, 0xd2, 0xf4, 0x23, 0x44, 0x88, 0x7b,
+ 0xc4, 0xf6, 0x4f, 0x72, 0x11, 0x69, 0x6f, 0x59, 0x46, 0x99, 0x7d, 0x45,
+ 0x58, 0xeb, 0x52, 0x84, 0x3b, 0x22, 0x1c, 0x7f, 0xef, 0xaa, 0xc2, 0xb7,
+ 0x69, 0xcd, 0x1e, 0x7d, 0xf1, 0xcd, 0x85, 0x63, 0xb0, 0x54, 0x44, 0x1b,
+ 0x5d, 0x98, 0xb9, 0x61, 0xa1, 0xa3, 0x12, 0x20, 0xab, 0xdd, 0xaa, 0xa3,
+ 0xd3, 0x31, 0x11, 0x82, 0x8a, 0x02, 0xe5, 0x12, 0x7a, 0x1e, 0xc4, 0x3e,
+ 0x72, 0x5a, 0x98, 0x7f, 0xe8, 0x51, 0xd9, 0x3d, 0xe4, 0xbe, 0x8d, 0x86,
+ 0x35, 0x4b, 0x32, 0x34, 0x0d, 0xf0, 0x55, 0x7d, 0x2b, 0x94, 0xee, 0xcc,
+ 0xaa, 0x60, 0x32, 0x23, 0xf9, 0x61, 0xaf, 0xae, 0xf3, 0x30, 0xda, 0x13,
+ 0xf0, 0xb0, 0x4e, 0xaf, 0x6f, 0xbe, 0x95, 0x52, 0xf9, 0x2e, 0x3a, 0x52,
+ 0xa2, 0x8d, 0xc5, 0xc1, 0xdd, 0xcf, 0xf0, 0xce, 0x7a, 0xb0, 0x64, 0xe6,
+ 0xd0, 0x8e, 0xfe, 0x17, 0x65, 0x9e, 0x03, 0xb3, 0x79, 0xff, 0x77, 0x3b,
+ 0x4e, 0x27, 0xd2, 0x5c, 0x87, 0x6c, 0xda, 0x1b, 0xbf, 0xf8, 0x10, 0x15,
+ 0x21, 0x2b, 0x3f, 0x35, 0xff, 0xa2, 0xfa, 0x77, 0x1d, 0xa9, 0x48, 0x47,
+ 0x68, 0x0b, 0xaf, 0x25, 0xdc, 0xaa, 0x4e, 0x02, 0x21, 0xb3, 0x35, 0x4f,
+ 0xcc, 0xe5, 0xf8, 0x70, 0xff, 0x6c, 0x02, 0x4d, 0x94, 0xf3, 0x40, 0x70,
+ 0xac, 0x8e, 0x47, 0xd3, 0x02, 0x07, 0x29, 0x18, 0xb1, 0xe8, 0xfc, 0xc8,
+ 0xdc, 0x86, 0x29, 0x50, 0x21, 0xc1, 0x9b, 0x1b, 0x53, 0x5e, 0xff, 0x0b,
+ 0xb7, 0xb4, 0xb5, 0xf6, 0xed, 0x51, 0xec, 0x14, 0x83, 0x4a, 0x97, 0x09,
+ 0xdb, 0xf8, 0x06, 0x09, 0xfb, 0x51, 0xfc, 0xb8, 0x70, 0x76, 0x95, 0x3e,
+ 0x42, 0x07, 0xd7, 0x93, 0x53, 0xaf, 0xa4, 0x59, 0x10, 0x93, 0x89, 0x0a,
+ 0xf6, 0x3c, 0x70, 0x52, 0xb5, 0x24, 0x90, 0x2e, 0x7d, 0x82, 0x91, 0x01,
+ 0xa6, 0x6c, 0x31, 0x74, 0x64, 0x54, 0x63, 0x31, 0x81, 0xc7, 0x7d, 0xb1,
+ 0xfa, 0xc4, 0x2d, 0x96, 0xfa, 0x6a, 0x03, 0x4d, 0x39, 0x34, 0x1d, 0x33,
+ 0x9a, 0x28, 0x40, 0xf4, 0xb3, 0xc8, 0xa3, 0xfa, 0x86, 0xd7, 0x05, 0x6b,
+ 0x78, 0xfe, 0x4c, 0xab, 0x89, 0xeb, 0x21, 0x07, 0xfe, 0x02, 0x63, 0x12,
+ 0x5c, 0xed, 0x3d, 0x43, 0xb9, 0x6e, 0x22, 0x15, 0x80, 0x39, 0x39, 0x24,
+ 0x0a, 0x61, 0x67, 0xa0, 0x07, 0x99, 0xaa, 0x92, 0x3e, 0xeb, 0x96, 0x1c,
+ 0x4e, 0x36, 0x65, 0xe4, 0xaf, 0x5f, 0x5e, 0x5e, 0xb2, 0xba, 0xe8, 0xef,
+ 0x33, 0xb9, 0x91, 0x11, 0x94, 0x49, 0xaa, 0x79, 0x2c, 0xa4, 0xf3, 0x14,
+ 0x7b, 0x65, 0x76, 0xb3, 0xe9, 0x68, 0xdf, 0x62, 0x30, 0xa0, 0x5d, 0xc3,
+ 0xeb, 0x80, 0x75, 0x3c, 0x11, 0xc5, 0x7e, 0x73, 0x06, 0x3e, 0x7e, 0x80,
+ 0x0c, 0x99, 0x2c, 0xc3, 0xcd, 0x59, 0x03, 0xd9, 0x0f, 0x2c, 0x9f, 0x2f,
+ 0x84, 0xe8, 0xfc, 0xa7, 0x48, 0x52, 0xa1, 0x71, 0xec, 0xb2, 0x11, 0xec,
+ 0x66, 0x7f, 0xc6, 0xcf, 0xb2, 0x20, 0xfd, 0x33, 0x4a, 0x2b, 0xee, 0xa7,
+ 0x92, 0x60, 0x3d, 0x73, 0xb1, 0xbd, 0xf4, 0x37, 0x53, 0x8f, 0xe6, 0x5b,
+ 0xbb, 0x50, 0x1a, 0x2c, 0xfa, 0xd4, 0x4c, 0xfd, 0x53, 0x7e, 0x66, 0x78,
+ 0x73, 0xaa, 0x6a, 0x7b, 0x68, 0x9c, 0xb4, 0xb0, 0x93, 0xf0, 0x94, 0xbd,
+ 0x59, 0xc8, 0x32, 0x03, 0x92, 0x9b, 0xf8, 0x39, 0x71, 0x48, 0xcc, 0x16,
+ 0x5c, 0x34, 0x10, 0x40, 0x13, 0x18, 0xbe, 0x6c, 0x7e, 0xc0, 0xfd, 0xbf,
+ 0x48, 0x7b, 0xdd, 0x52, 0x5f, 0xe4, 0x67, 0x3f, 0x98, 0x01, 0x54, 0xa2,
+ 0xbc, 0xbb, 0x55, 0x1e, 0x04, 0x11, 0x5e, 0xa9, 0xc2, 0x03, 0xac, 0x82,
+ 0x11, 0xc9, 0xe0, 0x4f, 0x3f, 0x78, 0x31, 0x8b, 0x5a, 0xa4, 0x5a, 0x58,
+ 0xe6, 0xfe, 0xf5, 0x6a, 0xd9, 0x9c, 0x59, 0xc2, 0xe8, 0x09, 0xf2, 0xdb,
+ 0x50, 0x9c, 0xc6, 0x97, 0xb5, 0x09, 0x2b, 0xad, 0xbd, 0xa0, 0x99, 0x1a,
+ 0x3a, 0x69, 0x34, 0xad, 0x58, 0x07, 0x01, 0x1c, 0x57, 0x13, 0x04, 0xaf,
+ 0x5e, 0x2e, 0xd7, 0x42, 0xd9, 0x3e, 0xf4, 0xdc, 0x09, 0x6c, 0x57, 0xb3,
+ 0x22, 0x37, 0x33, 0x0b, 0xf0, 0x97, 0x88, 0x0e, 0x55, 0x41, 0xbe, 0x14,
+ 0xeb, 0x9a, 0xcb, 0xc7, 0xc5, 0x34, 0xe8, 0x99, 0xfa, 0x33, 0x16, 0xb3,
+ 0x50, 0x22, 0x83, 0x7b, 0x1d, 0x7c, 0x0b, 0xc9, 0x05, 0x9f, 0x82, 0x21,
+ 0x02, 0xb6, 0x56, 0xe6, 0xdb, 0x70, 0xab, 0xcf, 0xc3, 0x46, 0x05, 0x18,
+ 0xc6, 0x94, 0xc9, 0xef, 0x26, 0xf8, 0x07, 0x6e, 0x54, 0xdd, 0x27, 0x88,
+ 0x35, 0x2b, 0x9e, 0x83, 0x54, 0x08, 0x08, 0x68, 0x44, 0x65, 0x8f, 0x50,
+ 0x5b, 0x58, 0xd9, 0xf0, 0x3e, 0x90, 0x3d, 0x04, 0x07, 0xa3, 0xa7, 0x7f,
+ 0x1a, 0x7f, 0xe6, 0x1c, 0xe1, 0x08, 0x2d, 0x19, 0x53, 0xe8, 0xd7, 0x24,
+ 0x85, 0xe6, 0x5c, 0xe7, 0xcc, 0x83, 0x97, 0xfc, 0x5f, 0xe7, 0xee, 0x6e,
+ 0xe9, 0xaa, 0x72, 0xc4, 0xff, 0x5e, 0x3c, 0x9b, 0xad, 0x28, 0xe9, 0x5a,
+ 0x23, 0xf8, 0xfd, 0xca, 0x4c, 0x75, 0x77, 0xe6, 0x64, 0x08, 0xa5, 0x7d,
+ 0xf6, 0x37, 0xa6, 0x70, 0xf2, 0x81, 0x21, 0x97, 0xfa, 0x91, 0x18, 0xd2,
+ 0x30, 0xdd, 0x7d, 0xa1, 0x3c, 0x09, 0xe7, 0x8f, 0x13, 0x5f, 0x7c, 0x8e,
+ 0x55, 0xcd, 0xcf, 0x8b, 0xbf, 0xd5, 0xe8, 0xf3, 0xa2, 0xa5, 0x62, 0xe5,
+ 0x0c, 0x5a, 0x00, 0x0e, 0xe9, 0x36, 0xe1, 0xda, 0x7f, 0xc6, 0xe1, 0xc5,
+ 0x54, 0x4a, 0x87, 0x87, 0xc9, 0xf0, 0xfa, 0xd0, 0x14, 0x54, 0x62, 0xad,
+ 0xb6, 0xc9, 0x56, 0x2b, 0xc5, 0x21, 0xa8, 0x0e, 0xc1, 0xcd, 0x9f, 0x41,
+ 0xda, 0x52, 0x35, 0xb6, 0x83, 0x6b, 0x24, 0x37, 0xf5, 0xf8, 0x15, 0xbe,
+ 0xdc, 0x9e, 0x03, 0x66, 0xcc, 0x23, 0xb4, 0xd6, 0xcd, 0x08, 0xfc, 0xd5,
+ 0xb2, 0xa7, 0xa2, 0x35, 0xc3, 0x3a, 0x86, 0xfd, 0x69, 0xe5, 0xeb, 0x7b,
+ 0x5d, 0xd1, 0xad, 0x19, 0x3a, 0xf8, 0xae, 0xcc, 0xfc, 0x50, 0xf1, 0x1e,
+ 0xcc, 0x97, 0x87, 0x1c, 0xe1, 0x51, 0x84, 0xb0, 0x75, 0x6c, 0x4f, 0x17,
+ 0x69, 0xf7, 0xf6, 0x3b, 0xc7, 0x7f, 0x78, 0x41, 0xf8, 0xb5, 0x01, 0x36,
+ 0x69, 0x66, 0x52, 0x05, 0x5e, 0x17, 0x54, 0xfe, 0x33, 0x0a, 0x8d, 0xf2,
+ 0x69, 0xad, 0x71, 0xa8, 0x4f, 0x4e, 0xb2, 0xb1, 0x55, 0x31, 0x0c, 0xe3,
+ 0x73, 0x6c, 0xdd, 0xdf, 0xc2, 0x4c, 0x3e, 0x5a, 0x6b, 0xac, 0x57, 0xbd,
+ 0x95, 0x56, 0x6d, 0x71, 0x96, 0xf5, 0xa1, 0x1d, 0x6c, 0xa6, 0x2c, 0x9e,
+ 0x2d, 0xd1, 0xed, 0xb2, 0xaa, 0xcd, 0x10, 0xcd, 0x5a, 0x7d, 0x35, 0xb8,
+ 0xb9, 0xfc, 0x44, 0x6c, 0xf4, 0x28, 0xc5, 0xcd, 0xa7, 0x6e, 0xdc, 0x03,
+ 0x66, 0xb0, 0xc5, 0x4e, 0xca, 0x97, 0x42, 0x77, 0x0c, 0xec, 0x0b, 0xdb,
+ 0xfe, 0xee, 0xa1, 0x2a, 0x73, 0x32, 0xff, 0x07, 0x50, 0x29, 0x91, 0x1b,
+ 0x41, 0xb0, 0xb7, 0xae, 0xe4, 0xbb, 0xd2, 0x95, 0xf3, 0xbb, 0x85, 0x63,
+ 0x0f, 0xec, 0x44, 0x29, 0xd2, 0x90, 0x31, 0xbc, 0x20, 0x4e, 0x84, 0x75,
+ 0x92, 0x50, 0xe8, 0xa5, 0xcf, 0xd8, 0x15, 0xb3, 0xb2, 0x2d, 0x6d, 0x33,
+ 0xe6, 0xec, 0x05, 0x65, 0xc8, 0x51, 0x1e, 0x31, 0xf4, 0xfa, 0xd6, 0x7c,
+ 0xe2, 0x55, 0x71, 0x33, 0x5f, 0x32, 0xce, 0x81, 0xaa, 0x7c, 0x64, 0x81,
+ 0xc5, 0xc4, 0x2c, 0x95, 0xc1, 0xb3, 0xe6, 0x98, 0x39, 0x4f, 0x6e, 0x14,
+ 0x49, 0x5f, 0xc5, 0xe4, 0x88, 0xed, 0x00, 0x43, 0x87, 0xa5, 0xe6, 0xfd,
+ 0xf0, 0x86, 0xb6, 0x24, 0x1c, 0xb5, 0xf7, 0x7c, 0x6f, 0x51, 0xf3, 0x8a,
+ 0xbc, 0xf9, 0xb5, 0xcc, 0x10, 0x8c, 0x3d, 0x53, 0x39, 0xd7, 0x26, 0x9c,
+ 0x4c, 0x9a, 0x1c, 0xe0, 0xa5, 0x62, 0xb4, 0xb9, 0x13, 0xfa, 0x18, 0xd1,
+ 0xbe, 0x3e, 0xb3, 0x6f, 0x2b, 0xce, 0xc3, 0x6d, 0xa5, 0xc2, 0x02, 0x0c,
+ 0x2c, 0xe1, 0xa9, 0xa7, 0x3e, 0x0c, 0x5b, 0x1d, 0x7c, 0xbd, 0xcf, 0x76,
+ 0xde, 0x2f, 0xea, 0x3f, 0xa3, 0x2b, 0xaf, 0x06, 0xaa, 0xa2, 0x59, 0xf3,
+ 0xe8, 0x09, 0xf8, 0xb9, 0x3c, 0x13, 0x45, 0x12, 0xdd, 0x68, 0xb0, 0xb2,
+ 0x21, 0xcf, 0xd3, 0xb7, 0x59, 0xdd, 0x09, 0xc9, 0xac, 0x57, 0x8b, 0x10,
+ 0xfb, 0x83, 0xee, 0xa4, 0xf2, 0xc9, 0x9d, 0x04, 0x82, 0xf6, 0xa1, 0xcf,
+ 0x9e, 0x91, 0xfa, 0xa8, 0x2f, 0x87, 0x60, 0x70, 0xd8, 0xed, 0x6d, 0x10,
+ 0xe3, 0x86, 0xc1, 0x44, 0x6f, 0x71, 0x9b, 0xee, 0xb4, 0xfd, 0x9d, 0x6f,
+ 0x14, 0xca, 0x36, 0xaf, 0x8a, 0xbb, 0x63, 0xca, 0x8e, 0xff, 0xd8, 0xa7,
+ 0xda, 0x48, 0x32, 0x78, 0x20, 0xd1, 0x2b, 0x5c, 0x63, 0xe4, 0x7a, 0xea,
+ 0x45, 0x2d, 0x53, 0xca, 0x37, 0x82, 0xa2, 0xda, 0x32, 0x0e, 0x92, 0x83,
+ 0xec, 0xd2, 0xa1, 0xeb, 0x5f, 0xfe, 0x85, 0xd5, 0x5c, 0x8c, 0xab, 0xda,
+ 0x60, 0xc5, 0x9c, 0xd5, 0x75, 0xcd, 0xd1, 0xff, 0x5a, 0xc4, 0x10, 0x7f,
+ 0x3d, 0x47, 0xb9, 0x05, 0x9f, 0xaf, 0xca, 0x15, 0xd7, 0xf8, 0x98, 0x53,
+ 0x1a, 0xe0, 0x35, 0x04, 0x19, 0x45, 0xd5, 0x86, 0xc1, 0x6c, 0xb1, 0x90,
+ 0x7a, 0x4d, 0x45, 0x7a, 0xc1, 0x7c, 0x17, 0xcd, 0x9f, 0x59, 0xc5, 0x7c,
+ 0x4e, 0x20, 0x6d, 0xe9, 0x7b, 0x90, 0x37, 0x61, 0xfb, 0xe6, 0x8c, 0x82,
+ 0xfe, 0x22, 0x42, 0xe4, 0x32, 0xc4, 0x97, 0x1f, 0x9b, 0xe3, 0x7e, 0x5d,
+ 0x01, 0x0c, 0xa4, 0xc2, 0x6f, 0xed, 0xb0, 0xf0, 0x8d, 0x63, 0x3a, 0xf6,
+ 0xa1, 0x0d, 0xd3, 0xca, 0x50, 0xf8, 0x01, 0x1a, 0xf7, 0xf8, 0x49, 0x39,
+ 0x6e, 0x04, 0x25, 0xfe, 0x42, 0xdd, 0xa9, 0xa2, 0xe7, 0x87, 0x86, 0x58,
+ 0x72, 0x01, 0x86, 0x29, 0x98, 0xa2, 0x89, 0xa0, 0x0e, 0xe4, 0xcf, 0xd7,
+ 0x00, 0x36, 0xce, 0x33, 0xa6, 0xfa, 0x0c, 0x41, 0x54, 0x96, 0x23, 0x96,
+ 0x0c, 0xdd, 0x1d, 0x82, 0x03, 0x76, 0x1e, 0x06, 0xba, 0x26, 0x01, 0xe2,
+ 0x02, 0xdb, 0xef, 0x29, 0x5a, 0xb2, 0xbe, 0x2a, 0xed, 0x90, 0x8f, 0x8a,
+ 0x50, 0x97, 0xbb, 0x00, 0xbf, 0x9d, 0x4e, 0xeb, 0x18, 0x41, 0x94, 0xb3,
+ 0xef, 0x63, 0x26, 0xb3, 0x21, 0x32, 0xe0, 0x9b, 0x26, 0x70, 0xbe, 0xd9,
+ 0xe2, 0x3c, 0x04, 0x23, 0xb0, 0x80, 0xab, 0x75, 0xce, 0xfc, 0x1a, 0xf2,
+ 0x80, 0x54, 0x02, 0xad, 0xc7, 0xf3, 0xa0, 0x30, 0x72, 0x56, 0x8f, 0x26,
+ 0x39, 0x8f, 0xa8, 0xb9, 0xf3, 0x9b, 0xfa, 0x2b, 0xf9, 0x6e, 0xae, 0x3f,
+ 0xc2, 0xd5, 0x87, 0xc2, 0x33, 0x74, 0x2b, 0xb6, 0xa1, 0x45, 0x8c, 0x29,
+ 0xb2, 0xcb, 0x1c, 0x06, 0x45, 0x83, 0x88, 0xd5, 0xac, 0x8f, 0x2d, 0x52,
+ 0x21, 0xb0, 0x90, 0x86, 0xe0, 0x73, 0xdb, 0x75, 0x0b, 0x86, 0x2d, 0xc7,
+ 0x1a, 0x77, 0x51, 0x34, 0xaa, 0xc7, 0xc0, 0x73, 0x6a, 0x2f, 0xca, 0x36,
+ 0x86, 0xe4, 0xc9, 0x60, 0x34, 0x27, 0x67, 0xef, 0xd5, 0x89, 0x1f, 0x63,
+ 0xd8, 0xc4, 0x91, 0x9e, 0x33, 0xc1, 0xeb, 0xfb, 0x40, 0xd5, 0xe2, 0x3d,
+ 0xe6, 0x0d, 0x82, 0x9e, 0xd4, 0x23, 0x87, 0x4e, 0xbf, 0x52, 0x24, 0xd1,
+ 0x0c, 0x29, 0x5e, 0xf5, 0x20, 0x1c, 0x36, 0x8d, 0xa2, 0x9a, 0x77, 0x90,
+ 0xa1, 0xc6, 0x7a, 0x65, 0x14, 0x8f, 0xf5, 0xb2, 0xf8, 0x5d, 0x70, 0xce,
+ 0xd3, 0xde, 0x89, 0x15, 0x9d, 0x34, 0x9f, 0xec, 0x8a, 0xd8, 0xc4, 0x19,
+ 0x15, 0xcd, 0x6b, 0xd4, 0x07, 0x2a, 0x42, 0x69, 0xfc, 0x1b, 0x7c, 0x9f,
+ 0xde, 0x81, 0x92, 0x28, 0x93, 0x53, 0xaf, 0x6c, 0xdf, 0xb1, 0xff, 0xab,
+ 0x07, 0x07, 0x8f, 0x3f, 0xe0, 0xdc, 0x16, 0x8c, 0xd1, 0xaa, 0x11, 0x09,
+ 0xa1, 0x2b, 0xfc, 0x54, 0x1a, 0x8f, 0x37, 0x19, 0x42, 0xab, 0x1b, 0xac,
+ 0x5a, 0x06, 0x9c, 0x61, 0xb2, 0x02, 0xdf, 0x35, 0x87, 0xdf, 0x1a, 0x63,
+ 0xcf, 0x4c, 0x3e, 0x76, 0x2e, 0x7a, 0xce, 0x6d, 0x7f, 0xda, 0xea, 0xe9,
+ 0x63, 0x4b, 0x2e, 0x8a, 0xf7, 0x11, 0x0f, 0x4f, 0x4f, 0x6e, 0x80, 0x12,
+ 0x76, 0x8e, 0xdd, 0xc0, 0x41, 0x24, 0xc6, 0x22, 0x13, 0xd2, 0xd3, 0x8a,
+ 0x28, 0x8c, 0x21, 0x6e, 0xde, 0xc0, 0x11, 0xa8, 0xaf, 0x4b, 0x07, 0x12,
+ 0x95, 0xc3, 0x44, 0xa4, 0x8d, 0x88, 0x07, 0x9c, 0x51, 0x75, 0xb0, 0x9f,
+ 0xd9, 0xc2, 0x48, 0x63, 0xd4, 0xef, 0xbb, 0x11, 0x39, 0x70, 0x56, 0x53,
+ 0xc5, 0x9b, 0xd9, 0xd3, 0x88, 0x52, 0xd4, 0x47, 0x18, 0x9c, 0xfc, 0xd1,
+ 0xca, 0x89, 0x24, 0x45, 0x9d, 0x43, 0x96, 0xa2, 0x66, 0x70, 0x1f, 0x16,
+ 0x6c, 0x41, 0xb2, 0x45, 0x6d, 0x32, 0x10, 0x46, 0xa9, 0xe5, 0xb9, 0xb1,
+ 0x20, 0x1c, 0x59, 0xd9, 0xdc, 0xca, 0x67, 0x4e, 0x92, 0xc2, 0x20, 0xcf,
+ 0x8a, 0x29, 0x7a, 0xac, 0x48, 0xc4, 0xd6, 0x29, 0xb9, 0x6e, 0x57, 0x15,
+ 0xb0, 0xd5, 0xd8, 0x23, 0xe4, 0xd0, 0xb1, 0xbf, 0x45, 0x29, 0x9e, 0x5b,
+ 0x1f, 0xd1, 0xdf, 0x80, 0x1a, 0x87, 0xe0, 0x30, 0x7d, 0x53, 0xb6, 0x7c,
+ 0x70, 0x8c, 0x35, 0x6f, 0xec, 0x8e, 0xc6, 0x03, 0x73, 0x19, 0x86, 0x57,
+ 0x63, 0x6f, 0x74, 0x98, 0x36, 0xe2, 0x6e, 0x77, 0x62, 0x83, 0xc4, 0xf0,
+ 0xc2, 0xdc, 0xf8, 0x00, 0x06, 0xab, 0x02, 0x05, 0x79, 0x35, 0x9e, 0xbe,
+ 0x56, 0x86, 0xad, 0xd5, 0x07, 0xa4, 0x29, 0x31, 0x71, 0x9a, 0x5f, 0x02,
+ 0xfc, 0x23, 0xd3, 0x1d, 0x0f, 0xc1, 0xc0, 0x0a, 0x79, 0x8a, 0xaa, 0x4d,
+ 0x4c, 0x32, 0x26, 0x3c, 0x46, 0xa2, 0x04, 0xae, 0x94, 0x60, 0xb0, 0x1c,
+ 0x3d, 0xc7, 0x5e, 0xde, 0xe8, 0xa3, 0x20, 0xd9, 0x09, 0xbb, 0x05, 0xf8,
+ 0x92, 0x56, 0xd3, 0x27, 0x5f, 0xe7, 0x42, 0x18, 0xb3, 0x76, 0x0c, 0xa2,
+ 0xd5, 0x91, 0xec, 0x68, 0x05, 0xa0, 0x9e, 0x76, 0x0c, 0x03, 0xdd, 0x56,
+ 0x2c, 0xd6, 0x87, 0xde, 0xf2, 0x3f, 0xcc, 0xb1, 0xd3, 0xd9, 0x45, 0x97,
+ 0xac, 0x77, 0xbc, 0xbc, 0x29, 0x5a, 0xbe, 0x3d, 0x60, 0x11, 0x42, 0x8e,
+ 0xe9, 0x23, 0x5e, 0xed, 0xea, 0x78, 0xc0, 0x44, 0xa8, 0xe7, 0xf6, 0xb3,
+ 0xa5, 0xcc, 0x0e, 0xfa, 0xdd, 0x1b, 0xf7, 0x42, 0x7e, 0x6c, 0xd2, 0xd1,
+ 0x6c, 0x4d, 0xca, 0x49, 0x78, 0xd1, 0x8c, 0x2f, 0xde, 0xa2, 0xad, 0xc9,
+ 0x1f, 0xa2, 0x83, 0x40, 0x33, 0xec, 0x2e, 0x1c, 0x4f, 0x86, 0xf4, 0xcf,
+ 0x3e, 0x3b, 0xdb, 0xf8, 0xaa, 0x50, 0x1a, 0x5d, 0x5a, 0x48, 0xdd, 0x42,
+ 0xd2, 0xce, 0x27, 0x34, 0x21, 0x8a, 0x08, 0xed, 0x66, 0x84, 0x86, 0xe8,
+ 0xc4, 0x9c, 0x2b, 0x14, 0x34, 0xdb, 0x9a, 0xf0, 0xa3, 0xe9, 0x14, 0x70,
+ 0x6e, 0x95, 0x10, 0x4e, 0x9e, 0x7f, 0x45, 0x36, 0x5f, 0x2f, 0xd9, 0x6a,
+ 0x83, 0x3c, 0x81, 0x55, 0x2d, 0xbc, 0xb9, 0xcc, 0x4f, 0xf4, 0xcf, 0x81,
+ 0xc5, 0x96, 0xb2, 0x13, 0x52, 0x3f, 0xef, 0x03, 0x67, 0x20, 0xe6, 0x0b,
+ 0xed, 0x7d, 0x3b, 0x89, 0x42, 0x07, 0xa3, 0xf1, 0xc4, 0x3a, 0xe1, 0xed,
+ 0x0c, 0x49, 0x41, 0x63, 0xa7, 0xf1, 0xe3, 0xcf, 0xc9, 0x95, 0xcf, 0x5d,
+ 0xba, 0x3e, 0xc4, 0xf1, 0x5b, 0x42, 0xb1, 0x07, 0x1f, 0x34, 0x45, 0x4f,
+ 0x32, 0xdc, 0x32, 0x5d, 0x6b, 0xed, 0x9b, 0x0a, 0x87, 0x52, 0x04, 0x69,
+ 0x84, 0xee, 0x60, 0x17, 0x68, 0xb9, 0xcb, 0x5f, 0x10, 0x99, 0x79, 0x22,
+ 0xa0, 0xab, 0x67, 0x71, 0x22, 0xcc, 0x6f, 0xc4, 0x8a, 0x5a, 0xc0, 0xd9,
+ 0x47, 0xee, 0x68, 0x48, 0x46, 0xae, 0x1f, 0x19, 0x10, 0xbe, 0x6b, 0xdb,
+ 0xc5, 0xd1, 0xda, 0x48, 0x56, 0x8b, 0xfb, 0x3f, 0x53, 0x88, 0x5f, 0x58,
+ 0xbb, 0x50, 0xaa, 0x04, 0xe3, 0xcf, 0x9b, 0x89, 0x21, 0x93, 0x62, 0x63,
+ 0xf8, 0x53, 0x9a, 0xd8, 0xba, 0x61, 0x9e, 0x67, 0x09, 0x03, 0x5f, 0x92,
+ 0xa6, 0x68, 0x26, 0xc6, 0x24, 0x04, 0x91, 0x0a, 0x32, 0x16, 0x73, 0xa2,
+ 0x81, 0x1b, 0xa2, 0xa4, 0xb2, 0x17, 0x5b, 0x51, 0x05, 0xcb, 0xb5, 0x4f,
+ 0xe7, 0x40, 0xc2, 0xf0, 0xee, 0xf8, 0x4f, 0x70, 0xc2, 0xb4, 0xa2, 0x06,
+ 0x83, 0xe1, 0x13, 0x88, 0xfd, 0x13, 0xc8, 0x8a, 0x0f, 0xb2, 0x50, 0xbe,
+ 0xc3, 0x66, 0xbe, 0x04, 0xde, 0x47, 0x54, 0xc7, 0x1b, 0x01, 0xf7, 0xdb,
+ 0x56, 0xe7, 0xb4, 0x9f, 0xf5, 0x20, 0x5b, 0x8c, 0xcb, 0x9f, 0x5f, 0xd4,
+ 0xb2, 0x47, 0x72, 0x6b, 0x5b, 0x45, 0x27, 0x9a, 0xbb, 0x4c, 0xcd, 0x37,
+ 0xd4, 0x06, 0x68, 0x2d, 0x50, 0x40, 0x19, 0xcc, 0x76, 0x57, 0x98, 0xe2,
+ 0x07, 0xb6, 0xf4, 0x98, 0x1d, 0x95, 0x0f, 0x96, 0x9c, 0x2a, 0x1b, 0xb8,
+ 0x69, 0xb0, 0xaf, 0x1b, 0xbc, 0x6a, 0xc4, 0xb8, 0x9c, 0x2e, 0x2d, 0x2f,
+ 0x9c, 0xa7, 0xaf, 0x5c, 0x6c, 0x32, 0x8b, 0x31, 0x45, 0x6e, 0x85, 0x89,
+ 0x39, 0xa9, 0x8d, 0x15, 0x66, 0xd2, 0xd6, 0x48, 0x65, 0xda, 0x44, 0x6d,
+ 0x63, 0xdd, 0x30, 0xe9, 0x02, 0xc8, 0x64, 0x5c, 0x79, 0x42, 0xe7, 0x3e,
+ 0x7e, 0x74, 0x86, 0xfa, 0x78, 0x3e, 0x73, 0x7d, 0x79, 0xa8, 0xb7, 0xd1,
+ 0xbf, 0x01, 0x4f, 0x4b, 0x7b, 0xc5, 0x50, 0xae, 0x89, 0x69, 0x10, 0x73,
+ 0x17, 0x14, 0x98, 0x9f, 0x8c, 0xe4, 0x83, 0x49, 0xfc, 0x79, 0xc2, 0x53,
+ 0xfc, 0x1d, 0x16, 0xe9, 0x8b, 0x33, 0xce, 0xcc, 0x81, 0x57, 0xed, 0xd0,
+ 0x9d, 0xc6, 0x27, 0x1d, 0x77, 0x72, 0x65, 0x02, 0xc0, 0x0e, 0x6c, 0x88,
+ 0x9e, 0xb7, 0x25, 0x19, 0x94, 0x7f, 0xdf, 0x75, 0x95, 0xf9, 0x2c, 0x00,
+ 0xf2, 0x69, 0x2a, 0x63, 0x4d, 0x06, 0xef, 0xa9, 0x91, 0x63, 0x41, 0xe9,
+ 0x04, 0x3d, 0x55, 0x0c, 0x78, 0x64, 0xf9, 0x85, 0x7b, 0x03, 0x82, 0xad,
+ 0x3d, 0x09, 0x49, 0xb1, 0xaf, 0x2c, 0xe1, 0x93, 0xbd, 0xd6, 0x4f, 0x66,
+ 0xa8, 0xc8, 0x9e, 0xa5, 0x36, 0x61, 0x67, 0xb0, 0xb7, 0x8e, 0x38, 0x57,
+ 0x1f, 0x5a, 0x66, 0x8d, 0xde, 0x6f, 0x08, 0xb2, 0x2a, 0x72, 0x85, 0xef,
+ 0x13, 0x11, 0x72, 0x3b, 0x9b, 0xa6, 0xce, 0xc3, 0x67, 0x24, 0x20, 0xeb,
+ 0xc5, 0x5a, 0x5d, 0x77, 0x37, 0xc9, 0x47, 0x0c, 0x4f, 0x10, 0x0d, 0x79,
+ 0x23, 0xfd, 0xff, 0x50, 0xe1, 0xe0, 0xb9, 0x08, 0x0e, 0x54, 0x10, 0x47,
+ 0x48, 0x98, 0x60, 0xda, 0x8c, 0x6f, 0x30, 0xb3, 0x93, 0xcd, 0x6e, 0xcf,
+ 0x10, 0x65, 0xd5, 0xa2, 0xcc, 0x06, 0x84, 0x6a, 0x1e, 0x3e, 0x88, 0x7c,
+ 0x86, 0x11, 0x01, 0x5b, 0x27, 0xff, 0x61, 0x4f, 0x1b, 0xce, 0xe6, 0x17,
+ 0x17, 0xfe, 0xcb, 0xe2, 0xb3, 0xfa, 0x39, 0x8d, 0xf8, 0xf1, 0x4d, 0xe9,
+ 0xe7, 0x6d, 0x2e, 0xeb, 0x85, 0x30, 0x81, 0x24, 0xbf, 0xb3, 0x67, 0x9b,
+ 0x49, 0xd8, 0x46, 0x0b, 0xc3, 0x57, 0xee, 0x70, 0x8b, 0xab, 0x36, 0x23,
+ 0x95, 0x4c, 0x24, 0xec, 0x6f, 0x35, 0xd1, 0xd3, 0xb2, 0x69, 0x86, 0x14,
+ 0xa7, 0x4a, 0x38, 0xb3, 0x26, 0x28, 0x35, 0xf4, 0x34, 0x6f, 0x50, 0xf9,
+ 0xec, 0xa8, 0x47, 0x96, 0xb8, 0x69, 0x7a, 0x4a, 0x85, 0xf1, 0x64, 0x90,
+ 0xc1, 0x30, 0x25, 0xba, 0x7a, 0x36, 0x59, 0x6a, 0x5b, 0x7e, 0xf3, 0x48,
+ 0x0d, 0xba, 0x04, 0xd8, 0x8e, 0xc2, 0x1c, 0x63, 0x9f, 0x53, 0x36, 0x22,
+ 0x4c, 0xeb, 0x27, 0xda, 0x61, 0xfc, 0x32, 0xb1, 0xfc, 0x51, 0xfe, 0x95,
+ 0x5d, 0x64, 0xe3, 0x79, 0x34, 0x15, 0x03, 0x19, 0x7a, 0x3c, 0xc8, 0x5f,
+ 0x36, 0xb2, 0x24, 0xb1, 0x52, 0xc2, 0x69, 0xc9, 0xcd, 0xf4, 0x27, 0x6f,
+ 0x02, 0xf5, 0xcf, 0x2f, 0x24, 0xe0, 0xf2, 0x3c, 0x8a, 0xa1, 0x2e, 0x58,
+ 0x3d, 0x7c, 0x9f, 0x90, 0x35, 0x6c, 0x9d, 0xb1, 0xa8, 0xdf, 0xae, 0xde,
+ 0xe9, 0xb4, 0xa1, 0xe8, 0xe1, 0xe5, 0x5c, 0x51, 0x1a, 0xce, 0x7a, 0xc7,
+ 0x46, 0xb1, 0x72, 0x10, 0xc0, 0xaa, 0xd9, 0x62, 0x3e, 0x72, 0x1a, 0x67,
+ 0x4e, 0x73, 0x72, 0x96, 0x29, 0x8f, 0x69, 0x52, 0xb6, 0xc6, 0x3c, 0x5e,
+ 0xbd, 0xfa, 0x41, 0x89, 0x21, 0x29, 0x06, 0xbb, 0xca, 0xc0, 0xd1, 0x30,
+ 0xb7, 0xdf, 0x12, 0x2b, 0xcd, 0x53, 0x5e, 0x99, 0x67, 0x07, 0x03, 0xac,
+ 0x97, 0xa2, 0x3e, 0x87, 0x39, 0x68, 0x8f, 0xac, 0xfe, 0xcb, 0x0c, 0x18,
+ 0xe3, 0x0b, 0xa1, 0xdf, 0x99, 0x1f, 0x7f, 0x32, 0xbc, 0xb1, 0x52, 0x30,
+ 0xbb, 0x12, 0x3c, 0x5a, 0x0d, 0xdf, 0x21, 0xe2, 0xa1, 0xb8, 0x7e, 0xbc,
+ 0x99, 0x78, 0xca, 0x54, 0x38, 0x98, 0x93, 0xdc, 0x25, 0xf8, 0x17, 0x85,
+ 0x57, 0x7f, 0xb8, 0x3e, 0xb3, 0x8d, 0x65, 0x11, 0x24, 0x89, 0x2d, 0xff,
+ 0x96, 0x26, 0x65, 0x20, 0xb5, 0xd6, 0xf6, 0x99, 0xe0, 0xfd, 0xf3, 0x90,
+ 0x28, 0x29, 0x2a, 0x76, 0x72, 0x18, 0x4a, 0xd9, 0x1c, 0xad, 0x89, 0xa1,
+ 0xa9, 0x53, 0x0a, 0x73, 0x14, 0x06, 0xea, 0xa2, 0xd7, 0xcb, 0x8f, 0xc0,
+ 0xf8, 0xce, 0x95, 0x42, 0xb7, 0x3c, 0x90, 0x34, 0xa1, 0x45, 0xc1, 0x1d,
+ 0xd2, 0x94, 0x3d, 0x17, 0xfd, 0x1a, 0xfd, 0x5c, 0x43, 0x89, 0x12, 0x09,
+ 0xec, 0x1f, 0xf0, 0x46, 0x16, 0x11, 0x6b, 0xcd, 0xd5, 0x61, 0x3b, 0xa2,
+ 0x36, 0x97, 0x78, 0x40, 0x2e, 0xdc, 0x01, 0x08, 0x13, 0x65, 0x15, 0xfd,
+ 0xef, 0xd6, 0xa1, 0x9e, 0xf2, 0x7b, 0x80, 0x88, 0x64, 0xdc, 0x7c, 0x91,
+ 0xbb, 0x95, 0x5c, 0x6f, 0xee, 0x23, 0xa5, 0xec, 0x60, 0x68, 0x21, 0xba,
+ 0x98, 0x6b, 0x6f, 0xe0, 0xb6, 0x99, 0x13, 0x28, 0xd8, 0x8d, 0x10, 0x05,
+ 0xda, 0xe6, 0x20, 0xcb, 0xb2, 0xba, 0xb7, 0x9a, 0x73, 0x40, 0xa3, 0x14,
+ 0x8e, 0x74, 0xe4, 0xa4, 0x85, 0x15, 0xdf, 0x17, 0x1e, 0x64, 0xce, 0x53,
+ 0x45, 0x8f, 0x4d, 0x9c, 0x5f, 0x7a, 0x4f, 0xc7, 0xd4, 0xec, 0xeb, 0x96,
+ 0x22, 0x68, 0x1f, 0x1a, 0xa0, 0xac, 0x84, 0x9f, 0xe4, 0x2f, 0xb8, 0x78,
+ 0x13, 0x84, 0xea, 0x0b, 0x9c, 0xd6, 0x1e, 0x74, 0x9a, 0x7a, 0x5d, 0x94,
+ 0x1d, 0x47, 0xa1, 0x48, 0xe2, 0x6c, 0x8f, 0x14, 0xb0, 0xea, 0x94, 0x66,
+ 0xbd, 0xd0, 0x50, 0x0f, 0xfb, 0xd1, 0x9a, 0x6e, 0xb4, 0xcf, 0xb8, 0xfc,
+ 0xe5, 0x63, 0xe8, 0x6d, 0x9f, 0xeb, 0xe9, 0xa2, 0x73, 0x2e, 0xa4, 0xc3,
+ 0x4c, 0x17, 0xb0, 0xf5, 0xaf, 0x8e, 0xe3, 0xe8, 0x84, 0x64, 0x2e, 0x0a,
+ 0x14, 0xa3, 0x37, 0x31, 0x8a, 0xd3, 0x55, 0x80, 0x62, 0x21, 0x29, 0x6b,
+ 0x39, 0x47, 0x79, 0x31, 0x7f, 0x0f, 0xd3, 0xea, 0xb3, 0xdc, 0x81, 0x1c,
+ 0x9f, 0xf0, 0x9c, 0x25, 0xc6, 0xdf, 0xdc, 0x1c, 0x79, 0xad, 0xa8, 0xb7,
+ 0xf5, 0x6c, 0xed, 0xcc, 0x89, 0xc0, 0x26, 0x08, 0x8f, 0x4e, 0xb4, 0xa0,
+ 0xcb, 0x11, 0xb1, 0xe7, 0x6c, 0x33, 0x72, 0x81, 0x39, 0xe8, 0x26, 0x07,
+ 0xd5, 0x9b, 0xcf, 0x9b, 0x82, 0xf9, 0x25, 0x25, 0x8e, 0x00, 0x7b, 0xf0,
+ 0x6a, 0xcb, 0xf0, 0xc6, 0xb3, 0xb3, 0xd4, 0x6b, 0x43, 0x7d, 0xf8, 0x4b,
+ 0x77, 0xfd, 0x66, 0xd4, 0x15, 0x56, 0x0c, 0x58, 0x06, 0x69, 0x53, 0xce,
+ 0xe3, 0xfa, 0x0b, 0x99, 0x7c, 0x41, 0xf1, 0x08, 0xb3, 0x44, 0x65, 0x27,
+ 0xdd, 0x2b, 0xcc, 0xf1, 0xe6, 0xb9, 0xc4, 0x8c, 0x20, 0xa1, 0x79, 0x79,
+ 0x6a, 0x91, 0xc1, 0x9c, 0x88, 0x50, 0xcf, 0x5b, 0x03, 0x13, 0x4e, 0x3d,
+ 0x62, 0xd4, 0x0b, 0x3e, 0x5b, 0xaa, 0x83, 0xfe, 0x6f, 0x7d, 0xc3, 0x49,
+ 0x00, 0x09, 0x9a, 0x87, 0xed, 0x7a, 0xc4, 0xd8, 0xe7, 0xd0, 0xf7, 0xd2,
+ 0x50, 0xdd, 0x3d, 0xf5, 0x59, 0x14, 0x52, 0x7a, 0xd6, 0x6f, 0x65, 0x7b,
+ 0x98, 0xf8, 0x8c, 0x89, 0x14, 0xfb, 0x1e, 0x61, 0xe7, 0x09, 0xa6, 0xe2,
+ 0xf8, 0x1f, 0x9c, 0x7e, 0x80, 0x4c, 0x9e, 0x28, 0xec, 0x00, 0x80, 0xf2,
+ 0x9f, 0xe5, 0x4c, 0x8c, 0xb5, 0x30, 0x70, 0x59, 0x06, 0x4a, 0xca, 0x0e,
+ 0x06, 0x0e, 0xa6, 0xfe, 0x06, 0x5e, 0x9a, 0xe4, 0x58, 0x99, 0x51, 0xdf,
+ 0xe6, 0x62, 0xc8, 0xb5, 0x88, 0xca, 0x4f, 0x7b, 0x39, 0x2f, 0xed, 0x9f,
+ 0x58, 0x3c, 0x54, 0x1f, 0xfc, 0x90, 0x55, 0x08, 0x9d, 0x5d, 0xd9, 0x5f,
+ 0x42, 0x5a, 0x91, 0xd1, 0xcb, 0x3c, 0x2e, 0xa9, 0xc2, 0x85, 0xe3, 0xa6,
+ 0xc8, 0x7a, 0x99, 0xc5, 0x08, 0x24, 0xf3, 0x22, 0x98, 0x8b, 0xb9, 0x33,
+ 0x7d, 0xc1, 0xe3, 0xba, 0xd0, 0x88, 0xcb, 0x7d, 0x5a, 0x63, 0xb7, 0xad,
+ 0x88, 0x10, 0x86, 0x4c, 0x77, 0x9b, 0xac, 0x61, 0xc1, 0xcf, 0xd3, 0x7c,
+ 0x4c, 0x19, 0xe5, 0x6a, 0xe5, 0x6e, 0x74, 0x1f, 0x40, 0xac, 0x9a, 0xbc,
+ 0x64, 0x8c, 0x61, 0x12, 0x30, 0x74, 0xde, 0x31, 0xbc, 0x40, 0x63, 0x02,
+ 0x98, 0x42, 0x17, 0xce, 0xb9, 0x93, 0x05, 0x41, 0x2b, 0x5d, 0x56, 0x7a,
+ 0x15, 0xd4, 0xbf, 0xc5, 0xb5, 0x72, 0x62, 0x07, 0xed, 0x92, 0x6a, 0xa0,
+ 0x6b, 0x48, 0x41, 0x47, 0x78, 0x3f, 0xf2, 0x67, 0x98, 0x65, 0x9b, 0xfc,
+ 0xf4, 0x00, 0x76, 0x64, 0xbe, 0x8a, 0xd1, 0x88, 0x98, 0xe0, 0x0c, 0xc5,
+ 0xc1, 0x28, 0x25, 0x24, 0x21, 0x0f, 0x47, 0x1b, 0x7f, 0x81, 0xca, 0x74,
+ 0x0f, 0x88, 0xe6, 0x83, 0xd3, 0x42, 0x22, 0xba, 0x8b, 0xd4, 0x22, 0x0c,
+ 0xa6, 0x7a, 0xdb, 0x36, 0x86, 0x4b, 0xe3, 0xa2, 0xde, 0x18, 0x8c, 0x4d,
+ 0x3c, 0xd7, 0x30, 0x55, 0x85, 0xb4, 0xde, 0x69, 0xa1, 0x65, 0x88, 0x72,
+ 0xe9, 0xfb, 0xd1, 0x12, 0x58, 0x9c, 0x02, 0x10, 0x26, 0xe7, 0x4b, 0x9e,
+ 0x31, 0x78, 0xbc, 0x15, 0xe7, 0xdc, 0xd8, 0x9e, 0xa2, 0xb5, 0x7f, 0x70,
+ 0x0a, 0x98, 0x88, 0x4c, 0xb7, 0x48, 0x76, 0x6e, 0x66, 0xe8, 0x09, 0xeb,
+ 0xda, 0x43, 0xda, 0x2d, 0x76, 0x9d, 0xdc, 0x61, 0x23, 0xcf, 0xd7, 0x00,
+ 0x8e, 0x2f, 0xc9, 0x98, 0x9a, 0x51, 0x51, 0x98, 0x1b, 0xa9, 0x45, 0x0b,
+ 0x9b, 0x08, 0xf3, 0xfb, 0x65, 0xaf, 0xa8, 0x86, 0x7a, 0xc9, 0x6c, 0x81,
+ 0xa2, 0x5f, 0x8d, 0x7a, 0xb9, 0xfe, 0x62, 0x01, 0xe8, 0x1a, 0x4c, 0x94,
+ 0xe8, 0xa1, 0x4a, 0x52, 0xfa, 0x1e, 0xff, 0xd7, 0xe5, 0xff, 0x8a, 0x84,
+ 0x24, 0x0d, 0x54, 0x7f, 0x9d, 0x5c, 0xdf, 0xf6, 0xa2, 0xc4, 0x64, 0xf7,
+ 0x75, 0xfa, 0x5a, 0xb3, 0xf4, 0xbb, 0xea, 0x0d, 0x3c, 0x32, 0xbe, 0x5c,
+ 0xc9, 0x9b, 0xb0, 0x29, 0x64, 0x7b, 0x11, 0xdc, 0xfc, 0x72, 0xfa, 0x95,
+ 0x67, 0xa5, 0xb9, 0x2f, 0xbc, 0xff, 0x73, 0xe6, 0x1e, 0xa6, 0xa6, 0x08,
+ 0x51, 0xb1, 0x1a, 0xb7, 0xda, 0x8f, 0xbe, 0x00, 0xd1, 0x69, 0x38, 0x20,
+ 0x52, 0x12, 0x3f, 0xdd, 0xde, 0x78, 0xb9, 0xb0, 0x90, 0xe7, 0x1d, 0x48,
+ 0x4d, 0xb9, 0x9c, 0x31, 0x82, 0x5b, 0x81, 0xd5, 0xe8, 0xef, 0xe9, 0xdd,
+ 0x1b, 0x83, 0x44, 0x1d, 0x59, 0x51, 0x5f, 0xeb, 0x70, 0xb1, 0x86, 0xe0,
+ 0x27, 0x08, 0xc6, 0xe2, 0x37, 0xd0, 0xea, 0x28, 0xd0, 0x28, 0xd8, 0x57,
+ 0x2e, 0x38, 0x2e, 0xa5, 0x3d, 0x73, 0x08, 0xac, 0x2d, 0xbb, 0x92, 0xf6,
+ 0x3c, 0xa7, 0x42, 0x8f, 0x2f, 0xbc, 0x3a, 0xd9, 0xc4, 0xc0, 0xf8, 0xb2,
+ 0xce, 0x93, 0x1a, 0xa4, 0x04, 0x6a, 0x91, 0xe4, 0xbf, 0x51, 0x16, 0xd2,
+ 0x39, 0xf3, 0x9d, 0x83, 0x17, 0xe6, 0x71, 0xd6, 0x52, 0x0a, 0x1e, 0x65,
+ 0xc7, 0x0b, 0x5a, 0xd3, 0x94, 0x91, 0xb8, 0x0a, 0xaf, 0x74, 0x37, 0xeb,
+ 0x44, 0xa4, 0x5a, 0x85, 0xae, 0x2f, 0x4a, 0x50, 0x27, 0x5f, 0x0e, 0x52,
+ 0x59, 0xac, 0xe8, 0xdd, 0x4b, 0x7a, 0xb9, 0x72, 0xcf, 0x41, 0x68, 0xda,
+ 0xf2, 0xe2, 0xc6, 0x3f, 0xfc, 0xa7, 0x45, 0xd4, 0x42, 0xe5, 0xe1, 0x38,
+ 0xbc, 0xe0, 0x7c, 0x1d, 0xad, 0x3e, 0xbf, 0xfc, 0x87, 0x21, 0x2f, 0x80,
+ 0xbb, 0x84, 0x2c, 0xb8, 0x75, 0x43, 0x4f, 0x29, 0x13, 0x1e, 0x2a, 0x45,
+ 0x42, 0x62, 0x35, 0x67, 0x47, 0x36, 0xf1, 0xe4, 0x32, 0xc6, 0x2b, 0x59,
+ 0x72, 0x52, 0x7f, 0xb4, 0x35, 0x71, 0x8f, 0xed, 0x1b, 0x80, 0xe9, 0x35,
+ 0xc1, 0xa5, 0xb1, 0xd6, 0xfe, 0x17, 0x7a, 0xcc, 0x91, 0x38, 0x09, 0xac,
+ 0xe3, 0xe7, 0x28, 0xa0, 0xef, 0x8a, 0xf2, 0x03, 0x0d, 0x76, 0x41, 0x24,
+ 0x40, 0x8e, 0x1d, 0x66, 0x5b, 0xea, 0x06, 0xd9, 0xef, 0xfa, 0xa6, 0x28,
+ 0x0a, 0xd5, 0x9c, 0xe1, 0xfb, 0xb0, 0x95, 0x48, 0x82, 0x62, 0xac, 0x11,
+ 0x1b, 0x56, 0x8d, 0xb6, 0xff, 0x13, 0xcc, 0xe7, 0x0d, 0x0c, 0x1f, 0xdb,
+ 0x1a, 0x0b, 0xf8, 0x88, 0xc0, 0x27, 0x23, 0xec, 0x05, 0x11, 0xbe, 0xae,
+ 0xfb, 0xed, 0x4f, 0xef, 0x21, 0xc6, 0xa1, 0x23, 0x6d, 0x2c, 0x81, 0xc7,
+ 0xe4, 0x64, 0x5e, 0x94, 0x9f, 0xc4, 0x76, 0xb8, 0x3a, 0x23, 0xda, 0xc5,
+ 0xa3, 0x20, 0x54, 0xa7, 0x80, 0xde, 0x06, 0xc5, 0x2b, 0x91, 0x2c, 0xa8,
+ 0x10, 0xb7, 0x84, 0x09, 0x32, 0xb3, 0x5d, 0x04, 0x0e, 0xe0, 0xcf, 0x13,
+ 0xb7, 0x3a, 0xbf, 0x8e, 0xdf, 0x5b, 0xd5, 0xbc, 0x3d, 0x60, 0x43, 0x24,
+ 0xca, 0x77, 0xbf, 0x38, 0x9f, 0x2a, 0x97, 0x60, 0xef, 0x81, 0x9a, 0xd0,
+ 0x5a, 0x11, 0x58, 0xce, 0xb3, 0xb9, 0x1c, 0xa8, 0xce, 0x4b, 0x7c, 0xa8,
+ 0xbf, 0x2a, 0x95, 0xca, 0x16, 0xc4, 0xc6, 0x7f, 0x73, 0x2b, 0x15, 0x4d,
+ 0x70, 0x7d, 0x9f, 0x82, 0x4f, 0x65, 0x2f, 0xf8, 0xe2, 0x22, 0x41, 0x17,
+ 0x23, 0xa3, 0x31, 0x6c, 0x59, 0x12, 0xf4, 0xf0, 0xf6, 0x3a, 0x25, 0xe9,
+ 0x74, 0x2e, 0xc8, 0x0b, 0x38, 0xdc, 0x4d, 0x0a, 0x30, 0x12, 0x52, 0x69,
+ 0x78, 0xc5, 0x0f, 0x91, 0x3e, 0x8c, 0xea, 0x37, 0x8d, 0x87, 0x15, 0x2c,
+ 0xa2, 0xe7, 0x5c, 0x2d, 0x6b, 0x12, 0x5e, 0xcb, 0x68, 0x47, 0x32, 0x89,
+ 0x00, 0xc0, 0xbd, 0xa8, 0x9c, 0x26, 0x96, 0xb7, 0x04, 0xae, 0xa0, 0x13,
+ 0xad, 0x2f, 0xd4, 0x02, 0x04, 0x58, 0xaf, 0xec, 0xa7, 0x3c, 0xda, 0x3e,
+ 0x42, 0x26, 0xe5, 0x13, 0x00, 0xa8, 0x96, 0xc5, 0xc4, 0x6f, 0xa9, 0x84,
+ 0x3c, 0xf3, 0x21, 0x6a, 0xd5, 0x08, 0xa4, 0x74, 0x42, 0x1b, 0xa3, 0x36,
+ 0x05, 0x1e, 0xfb, 0x72, 0x49, 0x94, 0x63, 0xce, 0xea, 0xdd, 0xb1, 0x26,
+ 0x0b, 0x77, 0x11, 0x54, 0x3a, 0x6a, 0x09, 0xe2, 0x95, 0x81, 0xbb, 0xa6,
+ 0x29, 0xb0, 0xe4, 0x9d, 0x54, 0xc0, 0x45, 0xf0, 0x07, 0x73, 0x50, 0xea,
+ 0x92, 0x75, 0x1c, 0x41, 0x3b, 0x1c, 0xe5, 0x67, 0x3d, 0x10, 0x65, 0xa7,
+ 0x1c, 0xfc, 0x3e, 0xa7, 0x97, 0x17, 0xb2, 0x5d, 0x48, 0x8e, 0xa1, 0xa8,
+ 0xb7, 0xcd, 0x1b, 0xd1, 0x39, 0x3a, 0x74, 0xa1, 0xdf, 0xc4, 0x62, 0x84,
+ 0xd9, 0x45, 0x20, 0x3a, 0x5f, 0x94, 0x7b, 0xbb, 0xba, 0x10, 0xc2, 0x6a,
+ 0xa7, 0x0f, 0x8e, 0x8f, 0xaf, 0x92, 0x7f, 0x94, 0x04, 0xc5, 0x2a, 0x8c,
+ 0x00, 0x15, 0x7c, 0x00, 0x76, 0x80, 0xf6, 0xd8, 0x13, 0x75, 0x36, 0x24,
+ 0xe7, 0x82, 0x55, 0xdf, 0xe6, 0xdd, 0xc7, 0xcf, 0xde, 0x13, 0x55, 0xb9,
+ 0x38, 0x28, 0x2a, 0x6d, 0xa1, 0x87, 0x03, 0xc2, 0xf0, 0x06, 0x4d, 0xb8,
+ 0x8e, 0x39, 0x8f, 0xb3, 0xa6, 0x6c, 0xe1, 0xe8, 0xde, 0xb6, 0x09, 0xfc,
+ 0xbf, 0x63, 0x05, 0x82, 0xc4, 0x40, 0x5c, 0xd8, 0x35, 0xf9, 0x8e, 0x82,
+ 0x3c, 0xcd, 0x89, 0x3f, 0x86, 0x1e, 0xcd, 0xe6, 0xe1, 0xa3, 0x29, 0xc0,
+ 0x0d, 0x41, 0x03, 0x83, 0x83, 0xdc, 0x10, 0xa2, 0x99, 0x9c, 0xeb, 0x71,
+ 0x2b, 0x53, 0x2d, 0x1f, 0xdb, 0x34, 0xaa, 0x1a, 0x52, 0xc7, 0x29, 0xdf,
+ 0x48, 0x39, 0x6c, 0x45, 0xb2, 0x8b, 0x0f, 0xf1, 0x2e, 0x28, 0x8e, 0x7d,
+ 0xfa, 0x2d, 0x62, 0xa1, 0x29, 0x46, 0xb1, 0xfa, 0x2e, 0x31, 0x9e, 0x7e,
+ 0xc5, 0x31, 0x69, 0xf2, 0x03, 0x4e, 0xef, 0x52, 0x08, 0x68, 0xfc, 0x16,
+ 0x8c, 0xa0, 0x5c, 0x60, 0x6b, 0x48, 0xc3, 0xf7, 0xee, 0xcf, 0x5a, 0x6a,
+ 0xef, 0xb4, 0xe4, 0xe1, 0xbe, 0x53, 0xe7, 0x9f, 0x06, 0xe3, 0x79, 0x82,
+ 0x5f, 0xe9, 0x5e, 0xc7, 0x0a, 0x20, 0xb8, 0xd2, 0x0f, 0x8d, 0x8d, 0x72,
+ 0xd0, 0x42, 0x4b, 0x3d, 0x06, 0xf7, 0x5e, 0xdb, 0x1c, 0x0b, 0x31, 0xa7,
+ 0xf2, 0xc9, 0x53, 0xf5, 0x05, 0x90, 0xb3, 0x7b, 0x38, 0xf4, 0xcb, 0x2e,
+ 0x32, 0x6b, 0xa8, 0x0d, 0x62, 0xc4, 0xbc, 0x88, 0x95, 0x3d, 0x75, 0xc6,
+ 0x6f, 0x0e, 0x00, 0x0c, 0xaf, 0xf0, 0xfe, 0x33, 0x98, 0x49, 0x18, 0x72,
+ 0x40, 0x26, 0x30, 0xb3, 0x3b, 0x79, 0x14, 0x1c, 0xdf, 0x21, 0x45, 0x8c,
+ 0x7f, 0x19, 0xe9, 0xa8, 0x1c, 0xd3, 0x3a, 0x8b, 0x6e, 0x43, 0xc0, 0x0a,
+ 0x32, 0x3e, 0xe2, 0xbd, 0x4c, 0x75, 0xe6, 0xd9, 0x7a, 0x18, 0x39, 0x15,
+ 0x02, 0xdd, 0xfe, 0xae, 0x77, 0x38, 0x46, 0xc0, 0x94, 0x48, 0x91, 0xb5,
+ 0x15, 0xff, 0x52, 0x28, 0x78, 0xf1, 0x5b, 0x19, 0x67, 0x61, 0x6c, 0xcb,
+ 0xed, 0x8d, 0xba, 0x36, 0xed, 0x85, 0x12, 0xb3, 0x22, 0x31, 0x7a, 0x62,
+ 0x73, 0xe1, 0xf1, 0xb6, 0xab, 0x86, 0x16, 0x12, 0x75, 0xee, 0x10, 0xe4,
+ 0x60, 0xcf, 0x33, 0xe6, 0xad, 0x57, 0x68, 0x85, 0x07, 0xfa, 0x65, 0xf2,
+ 0xa2, 0xc6, 0x9a, 0xcf, 0x33, 0x6e, 0x7d, 0x39, 0xd3, 0xc3, 0x65, 0x25,
+ 0x20, 0xa7, 0xe4, 0x21, 0x8d, 0x8d, 0xb9, 0x7c, 0x03, 0xab, 0xc5, 0x2d,
+ 0xd7, 0x1f, 0x4a, 0xa3, 0xbc, 0x92, 0x74, 0xc6, 0x23, 0xe7, 0x7a, 0xd9,
+ 0xe4, 0xfc, 0x4d, 0xfb, 0x3b, 0x92, 0x09, 0x69, 0x95, 0xa7, 0x8f, 0x0d,
+ 0x53, 0xd9, 0x33, 0xb5, 0x09, 0x2b, 0x1f, 0x9c, 0x1a, 0x9b, 0xd4, 0x73,
+ 0x5a, 0xe6, 0xfe, 0xd9, 0x7a, 0x9d, 0xe1, 0xa5, 0x87, 0x14, 0x35, 0xcb,
+ 0xd7, 0xea, 0x0d, 0x14, 0x95, 0xd3, 0xc2, 0x06, 0x2b, 0xa6, 0xf9, 0x57,
+ 0x6c, 0x1d, 0x55, 0x1d, 0x7e, 0x7c, 0xc2, 0xf9, 0xd8, 0x70, 0x52, 0x33,
+ 0x23, 0xf1, 0x6a, 0xf1, 0x3e, 0x70, 0x25, 0x50, 0xcc, 0xc8, 0xd7, 0x25,
+ 0x54, 0x2f, 0x5b, 0x40, 0xe5, 0x58, 0xe5, 0x12, 0x45, 0x58, 0x53, 0xef,
+ 0x67, 0x01, 0xfb, 0x10, 0x9f, 0x76, 0x4c, 0xac, 0x8c, 0x52, 0xda, 0xfc,
+ 0xa2, 0x2f, 0x29, 0xc4, 0x28, 0xaf, 0x69, 0x5d, 0xe5, 0xdb, 0x41, 0x90,
+ 0xb6, 0x36, 0xb9, 0x5e, 0x25, 0x24, 0xcd, 0x76, 0x7b, 0x62, 0xa7, 0xfa,
+ 0x9e, 0xef, 0xf9, 0x01, 0xbe, 0x5d, 0x13, 0xb5, 0x19, 0x93, 0x40, 0x5f,
+ 0xa8, 0x77, 0xa3, 0x40, 0x77, 0x44, 0xdc, 0x97, 0x70, 0x35, 0x3d, 0x28,
+ 0x13, 0xf4, 0xda, 0xc9, 0xa9, 0x18, 0x3c, 0xa1, 0x57, 0x4f, 0x3c, 0x85,
+ 0x7e, 0x17, 0x9d, 0xf2, 0xf2, 0xaf, 0x21, 0xd5, 0xb6, 0x26, 0x4e, 0xc9,
+ 0x6b, 0xcf, 0xa3, 0xe8, 0x3a, 0x92, 0x61, 0xdb, 0x3b, 0x7f, 0x97, 0xd6,
+ 0x9b, 0xc2, 0x8f, 0x8a, 0x70, 0xfa, 0xaf, 0x04, 0x72, 0xa6, 0x02, 0x35,
+ 0x91, 0xb0, 0x81, 0x66, 0xeb, 0xd8, 0x62, 0x84, 0x07, 0x7b, 0x1f, 0x79,
+ 0x7c, 0x4e, 0x27, 0x7b, 0x81, 0x34, 0x99, 0xab, 0xe8, 0xc8, 0xd3, 0x57,
+ 0x0c, 0x8f, 0x43, 0x0d, 0xc2, 0x7f, 0xf2, 0xe6, 0x0d, 0x9c, 0x2f, 0xae,
+ 0xeb, 0xc0, 0x4d, 0x03, 0x6e, 0x0b, 0x52, 0x94, 0xa0, 0x52, 0xc6, 0x49,
+ 0xb1, 0x9b, 0xce, 0x5b, 0x6a, 0x8c, 0xcd, 0x37, 0x93, 0x49, 0xce, 0x82,
+ 0x6d, 0x0c, 0x5a, 0xbd, 0x1e, 0x92, 0xd5, 0xd8, 0x65, 0xfd, 0x50, 0x70,
+ 0x6f, 0x4d, 0x37, 0xac, 0xbc, 0xae, 0x82, 0xe2, 0x4a, 0x5f, 0x25, 0x88,
+ 0x9f, 0x78, 0xf1, 0x09, 0x03, 0x9f, 0x17, 0xca, 0x2a, 0xb4, 0xa6, 0xbb,
+ 0x19, 0xec, 0xcc, 0xbb, 0xe5, 0xbc, 0xab, 0xc6, 0x44, 0xed, 0x05, 0x58,
+ 0xd6, 0x11, 0xb9, 0x15, 0xad, 0xf7, 0x09, 0x49, 0x77, 0xb6, 0x89, 0x6f,
+ 0x12, 0x94, 0xc8, 0x90, 0xdc, 0x92, 0x04, 0x65, 0x44, 0x52, 0x9e, 0x99,
+ 0xc7, 0xf4, 0x5e, 0x16, 0x47, 0x41, 0xa8, 0xd3, 0x8b, 0x81, 0x0d, 0xde,
+ 0x90, 0x69, 0xeb, 0xa2, 0xc7, 0x12, 0x42, 0x17, 0x1b, 0x1e, 0x2b, 0x47,
+ 0x42, 0xaf, 0x96, 0x1f, 0x99, 0x90, 0x5c, 0x19, 0xe9, 0x91, 0x20, 0x62,
+ 0x5a, 0x42, 0x3c, 0x3a, 0x4e, 0xe5, 0x21, 0x8a, 0xe1, 0x7f, 0x4d, 0x35,
+ 0x5b, 0x3f, 0xb9, 0x7b, 0x78, 0x83, 0x0a, 0x9d, 0x78, 0x93, 0x0d, 0x08,
+ 0xcc, 0x5b, 0x5d, 0x02, 0x12, 0x44, 0x10, 0x1a, 0x67, 0x1b, 0xa3, 0xec,
+ 0x74, 0xae, 0xfc, 0x3e, 0x88, 0xac, 0x93, 0x43, 0x8b, 0x54, 0xea, 0x93,
+ 0x4c, 0x85, 0x12, 0xaa, 0x76, 0xe0, 0x48, 0x7c, 0x5d, 0x00, 0x20, 0x4e,
+ 0x8f, 0xa0, 0x07, 0x36, 0x1d, 0x64, 0x51, 0x14, 0xed, 0x07, 0x7b, 0x42,
+ 0x26, 0xc2, 0xfa, 0x5f, 0xab, 0xc7, 0xaf, 0xec, 0x73, 0x1a, 0x96, 0xb5,
+ 0x4c, 0x2a, 0xa4, 0x8d, 0xe0, 0x3f, 0x04, 0x51, 0xdb, 0x65, 0xbf, 0x93,
+ 0x23, 0x8f, 0x6b, 0x69, 0x5c, 0xd3, 0xe3, 0xbc, 0x57, 0xc6, 0x1c, 0x6c,
+ 0xc2, 0x53, 0x11, 0x19, 0x08, 0x03, 0x58, 0xe1, 0x44, 0xb5, 0x8d, 0x11,
+ 0x49, 0xa3, 0x34, 0xac, 0x2a, 0x72, 0x62, 0xda, 0xab, 0x20, 0x35, 0x9d,
+ 0x3f, 0x5f, 0xc8, 0xd6, 0x79, 0x4f, 0xac, 0x1b, 0xcc, 0x80, 0xb6, 0x50,
+ 0x48, 0x84, 0x6a, 0xaa, 0xd3, 0x81, 0x04, 0x28, 0xe2, 0x5c, 0x91, 0x25,
+ 0x97, 0xb7, 0x4e, 0x2c, 0x4b, 0xa0, 0xac, 0x4e, 0x0c, 0x2a, 0x07, 0x9e,
+ 0x9a, 0x06, 0xdd, 0xc2, 0xfd, 0x0b, 0x85, 0x0a, 0x64, 0x33, 0x34, 0x6b,
+ 0x9c, 0x52, 0x99, 0x2d, 0xf2, 0xd2, 0x4d, 0xbe, 0x8c, 0xff, 0x40, 0xc2,
+ 0xbd, 0x16, 0x89, 0x7d, 0x30, 0xe1, 0x52, 0x6d, 0x02, 0x67, 0x1b, 0x4c,
+ 0xd2, 0xaa, 0xf7, 0xb9, 0x93, 0xcf, 0xf4, 0xb9, 0x65, 0x2e, 0x10, 0x4f,
+ 0x02, 0xbd, 0xbd, 0x17, 0xda, 0xb5, 0x4b, 0xf7, 0x54, 0x1f, 0x29, 0x1a,
+ 0xfd, 0xfa, 0xfd, 0xc5, 0x70, 0x34, 0x6b, 0xce, 0x1b, 0x0c, 0x19, 0x2b,
+ 0x20, 0x28, 0xe2, 0x9b, 0xc0, 0xd6, 0x36, 0x22, 0xd3, 0x9f, 0x8e, 0xc1,
+ 0x46, 0xcd, 0x68, 0x00, 0xd9, 0x6a, 0x4c, 0xc5, 0x1a, 0x7d, 0xb5, 0x2a,
+ 0xb7, 0xde, 0x19, 0x58, 0x04, 0x55, 0x43, 0x53, 0x91, 0xed, 0x83, 0xec,
+ 0x98, 0xf0, 0x89, 0x4b, 0xa1, 0xfc, 0xc1, 0xea, 0xda, 0xc4, 0xeb, 0x57,
+ 0xae, 0x2a, 0xcb, 0x8c, 0x75, 0x58, 0xbc, 0x5a, 0x81, 0x95, 0x10, 0x74,
+ 0xfb, 0xd5, 0x35, 0xb5, 0x3e, 0xe1, 0x74, 0x4c, 0x6b, 0x10, 0x26, 0x02,
+ 0xea, 0xa3, 0xfe, 0xc7, 0xd1, 0x75, 0xa4, 0xf5, 0xce, 0x02, 0x5e, 0x2f,
+ 0x8d, 0xf9, 0xc0, 0xc8, 0xd3, 0xa9, 0xa9, 0xc3, 0xd9, 0x32, 0x08, 0x95,
+ 0x24, 0xe9, 0x9f, 0x69, 0x71, 0xa5, 0xbf, 0x97, 0x47, 0x1c, 0x4b, 0x97,
+ 0x29, 0xbe, 0x30, 0x31, 0xe2, 0xe8, 0x78, 0x55, 0x73, 0xdc, 0x0b, 0x15,
+ 0x86, 0x47, 0xc9, 0x31, 0xce, 0xf9, 0x31, 0xa8, 0xe6, 0x32, 0x53, 0x2e,
+ 0x93, 0xc7, 0x6c, 0xfc, 0xa7, 0x49, 0x81, 0x09, 0x6b, 0x91, 0x61, 0x24,
+ 0x7d, 0x88, 0x84, 0x44, 0x56, 0x43, 0x6c, 0xf4, 0x51, 0x01, 0x8e, 0x28,
+ 0x5d, 0xd4, 0xc5, 0x37, 0x5f, 0xf6, 0x8a, 0x68, 0xb8, 0x58, 0xad, 0x39,
+ 0x36, 0x20, 0x6d, 0x74, 0x8a, 0xbe, 0xcc, 0x66, 0x02, 0xaa, 0x72, 0x55,
+ 0xea, 0xa6, 0xd5, 0x5b, 0x65, 0x89, 0x03, 0x7a, 0x34, 0x71, 0x39, 0x7c,
+ 0x37, 0x0f, 0x3f, 0x5a, 0x49, 0xf2, 0x6d, 0xf8, 0x6e, 0x9e, 0xbe, 0x60,
+ 0x7d, 0x80, 0xa5, 0xb4, 0xeb, 0x8c, 0x40, 0x9e, 0x06, 0xc2, 0x0d, 0x56,
+ 0x01, 0xcc, 0x0e, 0xc2, 0x66, 0xe9, 0xf8, 0x43, 0xdd, 0xbe, 0x94, 0x37,
+ 0x63, 0x28, 0x3f, 0x9d, 0xd1, 0x06, 0x7f, 0x1d, 0xdc, 0x6f, 0x5b, 0xd8,
+ 0xb1, 0x72, 0x30, 0x4a, 0xa4, 0x11, 0x1f, 0xc4, 0x5a, 0x47, 0xe7, 0xcd,
+ 0x8b, 0xc2, 0x8e, 0x01, 0x65, 0x0d, 0xe7, 0x01, 0x21, 0x2a, 0x74, 0x62,
+ 0xe5, 0xce, 0x64, 0x3a, 0x23, 0x96, 0x4b, 0x75, 0x8a, 0xbd, 0xdb, 0xa2,
+ 0x56, 0xc6, 0x08, 0x52, 0xad, 0x25, 0x3d, 0x3e, 0x32, 0x7b, 0x47, 0x05,
+ 0x11, 0x38, 0xf7, 0x05, 0xaa, 0x48, 0xd7, 0x96, 0xdf, 0x3e, 0x0e, 0x57,
+ 0xbd, 0x9b, 0x75, 0x74, 0x18, 0x4e, 0xaf, 0xfc, 0x74, 0xf0, 0xef, 0xd8,
+ 0x63, 0xb7, 0xa3, 0x19, 0x1c, 0xbd, 0xbb, 0xe0, 0xc5, 0xe0, 0xb0, 0x69,
+ 0x6f, 0xc8, 0x05, 0x20, 0x4e, 0x5d, 0xef, 0x9b, 0x51, 0x64, 0x43, 0xf9,
+ 0x28, 0x8c, 0xa1, 0x7e, 0x0e, 0x4b, 0x0f, 0xed, 0x63, 0x62, 0xbc, 0x78,
+ 0x35, 0x4d, 0xd0, 0x21, 0x39, 0x33, 0x93, 0xa7, 0xb9, 0x04, 0xb5, 0xed,
+ 0xde, 0x2a, 0x0b, 0xc8, 0xa5, 0x9f, 0x82, 0x5d, 0x37, 0x68, 0x25, 0x51,
+ 0x4a, 0x54, 0x99, 0x54, 0x50, 0xcd, 0xa3, 0x6c, 0xd1, 0xe9, 0x83, 0x26,
+ 0x67, 0x0b, 0xb0, 0x78, 0x34, 0x62, 0x87, 0x3a, 0x4c, 0xce, 0x71, 0xe6,
+ 0x20, 0xd2, 0x8b, 0x0e, 0xa8, 0x42, 0xe5, 0x98, 0x9d, 0xd5, 0x0e, 0x62,
+ 0xe8, 0x40, 0x66, 0x35, 0xf2, 0x6d, 0x80, 0xc6, 0x56, 0xa4, 0x78, 0x20,
+ 0xf5, 0x12, 0x9f, 0x3e, 0xa6, 0x17, 0x4a, 0x05, 0xbd, 0xa6, 0x4b, 0xa4,
+ 0x89, 0x89, 0xcf, 0x3b, 0xdb, 0xc8, 0x42, 0xfc, 0xbb, 0x6d, 0x34, 0x89,
+ 0x78, 0x96, 0x54, 0x48, 0xd0, 0x8b, 0x04, 0x86, 0xfa, 0x32, 0x34, 0xc6,
+ 0x9e, 0xcb, 0x5a, 0x9a, 0xc6, 0xc4, 0xb0, 0xfa, 0xf7, 0x30, 0xec, 0xdf,
+ 0xb3, 0x42, 0x0c, 0x0b, 0x41, 0x09, 0x0d, 0xe9, 0x09, 0xda, 0x2a, 0xf3,
+ 0x8a, 0x29, 0xab, 0xcd, 0x10, 0x38, 0x39, 0x79, 0xd4, 0x20, 0xc4, 0xa7,
+ 0xbb, 0xb6, 0x73, 0x84, 0x2e, 0x7a, 0x0b, 0x3d, 0xb8, 0xa8, 0xde, 0xcb,
+ 0x7c, 0x92, 0x68, 0xbe, 0x81, 0x16, 0xdc, 0x0e, 0x55, 0xbb, 0x64, 0x19,
+ 0xe2, 0x7c, 0xde, 0x01, 0x15, 0x23, 0x2d, 0x97, 0x5f, 0x98, 0x20, 0x18,
+ 0x9d, 0x61, 0x6e, 0x3f, 0x03, 0x01, 0x14, 0x2a, 0x96, 0xd8, 0x2a, 0x13,
+ 0xfa, 0xad, 0x23, 0x60, 0xe8, 0x67, 0xfd, 0x48, 0x68, 0xe7, 0x0b, 0x60,
+ 0x77, 0x60, 0x7d, 0x35, 0x76, 0xb9, 0xd0, 0xd9, 0xcd, 0xaa, 0x72, 0x2e,
+ 0x6c, 0x9c, 0xcd, 0xd6, 0xba, 0x04, 0x1c, 0x05, 0x80, 0x80, 0x39, 0x6a,
+ 0xa3, 0xd0, 0x17, 0x3d, 0x52, 0x57, 0x2a, 0x5d, 0x6f, 0xae, 0x90, 0x8d,
+ 0xea, 0xaf, 0xc2, 0x60, 0x0f, 0xb7, 0x68, 0x5f, 0x26, 0x59, 0x3a, 0xc4,
+ 0xdd, 0x20, 0x05, 0xe4, 0xab, 0x43, 0xb3, 0x26, 0x9d, 0x59, 0x12, 0x6b,
+ 0x7e, 0x87, 0x69, 0x6d, 0xa5, 0x96, 0xf8, 0xd8, 0xc7, 0x00, 0xdd, 0x77,
+ 0xf6, 0x14, 0x33, 0x6c, 0xdf, 0xc4, 0x08, 0xb5, 0xb9, 0x87, 0xb6, 0xa0,
+ 0x6c, 0xe9, 0xe8, 0x28, 0x8b, 0x3b, 0x5a, 0xaf, 0x8e, 0xc5, 0xe0, 0x3a,
+ 0x3b, 0xe2, 0x90, 0x0e, 0xb2, 0x15, 0x2a, 0x56, 0xa3, 0x35, 0xa0, 0xf4,
+ 0x77, 0x61, 0xfc, 0x2d, 0xe8, 0xae, 0x62, 0x14, 0xa4, 0xfa, 0x2e, 0x79,
+ 0x38, 0xd1, 0x70, 0x9d, 0x4a, 0x0d, 0xed, 0x37, 0xd5, 0x60, 0x6c, 0x17,
+ 0xa3, 0xc7, 0x4c, 0x23, 0xfd, 0xa2, 0xd1, 0x52, 0x83, 0xee, 0xde, 0x81,
+ 0x4b, 0x11, 0xdb, 0x56, 0x73, 0x47, 0x1d, 0xa2, 0xa5, 0x39, 0xcb, 0x57,
+ 0x80, 0x37, 0xe0, 0x74, 0xae, 0x49, 0xa0, 0xdd, 0x43, 0x8b, 0xf2, 0x11,
+ 0x89, 0xaa, 0x48, 0x43, 0x79, 0x07, 0x4c, 0x6a, 0x22, 0x6e, 0xe1, 0xd4,
+ 0x97, 0x5c, 0x0e, 0xa2, 0xd3, 0x86, 0x04, 0x0f, 0x17, 0xbc, 0x56, 0xa7,
+ 0x0b, 0x16, 0xe8, 0x9d, 0x92, 0xc9, 0x51, 0xfd, 0x26, 0x04, 0x15, 0xb5,
+ 0x39, 0x06, 0x00, 0xa4, 0x0e, 0x4d, 0x1d, 0xdc, 0x84, 0x38, 0x41, 0x91,
+ 0x46, 0x64, 0xee, 0xeb, 0x00, 0xfb, 0x88, 0xe0, 0x64, 0x41, 0x97, 0x78,
+ 0x5e, 0xa2, 0x15, 0xa8, 0x51, 0x42, 0xbd, 0x95, 0xbf, 0x3c, 0x1b, 0x82,
+ 0x3d, 0xad, 0xc5, 0x9e, 0x5c, 0x64, 0xa7, 0x1f, 0xd0, 0x7b, 0x4d, 0xc7,
+ 0x36, 0x15, 0x4c, 0xa9, 0x04, 0x29, 0x2b, 0x7e, 0x51, 0x2a, 0x20, 0xbe,
+ 0x41, 0x94, 0x0f, 0x58, 0x94, 0xf1, 0x6b, 0x2e, 0x63, 0x2b, 0xc8, 0x26,
+ 0x4c, 0x1e, 0xf3, 0x33, 0x13, 0x14, 0x58, 0x7d, 0x1a, 0x49, 0x56, 0xbf,
+ 0x8e, 0x69, 0xda, 0x9b, 0x6e, 0x94, 0xb9, 0x2a, 0x91, 0x8e, 0x23, 0xaf,
+ 0x58, 0x5a, 0x28, 0x9d, 0x94, 0xfc, 0x39, 0xc7, 0x9d, 0xbe, 0x55, 0x28,
+ 0x6e, 0x05, 0xb9, 0xf4, 0xeb, 0x75, 0xe1, 0xd0, 0xdb, 0xe7, 0xd9, 0x02,
+ 0xcb, 0x84, 0xc7, 0xa9, 0xa1, 0xd4, 0x20, 0x2d, 0x01, 0x27, 0x38, 0xd5,
+ 0x21, 0x0a, 0x88, 0xd8, 0x3b, 0x4b, 0xd4, 0xa3, 0xf8, 0x42, 0x20, 0x59,
+ 0xa1, 0x13, 0x3f, 0x12, 0x1c, 0x05, 0xf2, 0xa2, 0xda, 0x25, 0x75, 0x6d,
+ 0x2c, 0x50, 0x80, 0x64, 0xff, 0x65, 0xdb, 0x40, 0x8c, 0xc6, 0x68, 0xd7,
+ 0x38, 0x15, 0x6f, 0x40, 0xa3, 0x11, 0xe0, 0x02, 0x51, 0x3e, 0x26, 0x12,
+ 0xdd, 0xcb, 0xe4, 0x28, 0xd6, 0xbf, 0xcf, 0xb4, 0xe4, 0x90, 0xe1, 0xf6,
+ 0xc7, 0xf0, 0xf2, 0x7a, 0x0e, 0x2f, 0xa4, 0xd2, 0x1b, 0xca, 0x4e, 0x06,
+ 0xcb, 0x16, 0xda, 0x48, 0xfd, 0x81, 0x78, 0x81, 0x7e, 0xc5, 0x96, 0x85,
+ 0x43, 0x04, 0x23, 0xca, 0x16, 0x76, 0x74, 0xe1, 0x87, 0x13, 0x8b, 0xa7,
+ 0x0b, 0x48, 0x9e, 0x70, 0xf8, 0xf7, 0xda, 0x96, 0x70, 0xdc, 0xaa, 0xfd,
+ 0x1a, 0xe2, 0xa4, 0xcd, 0x8f, 0xfd, 0x30, 0xdf, 0x6e, 0xc3, 0x9d, 0x5a,
+ 0x3f, 0x7f, 0xc8, 0x96, 0x6f, 0x30, 0x19, 0xcb, 0xc1, 0x43, 0xdb, 0x44,
+ 0xa0, 0xab, 0x9b, 0xd9, 0xd1, 0x1b, 0x5f, 0xf6, 0x38, 0x6b, 0xf5, 0x3b,
+ 0xf0, 0xe8, 0x96, 0x58, 0x00, 0xa6, 0x74, 0xfb, 0x6c, 0xee, 0xb1, 0x76,
+ 0xbf, 0x3b, 0xdf, 0xaa, 0xc0, 0x96, 0xea, 0x2e, 0x73, 0xb6, 0x61, 0x7a,
+ 0xb2, 0x9b, 0xd7, 0x49, 0xee, 0x4c, 0x2d, 0x46, 0x7a, 0xba, 0xdf, 0x7a,
+ 0x28, 0xd7, 0xaf, 0xaf, 0x49, 0xc1, 0x80, 0x76, 0x35, 0xff, 0xf6, 0x16,
+ 0xed, 0x6c, 0x4e, 0xea, 0xad, 0x87, 0x08, 0xa7, 0x5d, 0x52, 0xb4, 0xf2,
+ 0x82, 0x7b, 0x52, 0xae, 0x53, 0x88, 0xee, 0x05, 0x34, 0xb7, 0x54, 0xa5,
+ 0x6f, 0xc1, 0xa4, 0xa3, 0x0c, 0x2c, 0x79, 0x01, 0x8e, 0x94, 0x5b, 0x8a,
+ 0xe4, 0x2e, 0xd0, 0x72, 0x1f, 0x48, 0x3e, 0xcd, 0x5a, 0xba, 0x30, 0x5e,
+ 0x0d, 0xb3, 0xba, 0x91, 0x6a, 0xb8, 0x4d, 0x8e, 0x2f, 0x7c, 0x8c, 0xa7,
+ 0x17, 0xe1, 0x9d, 0x8d, 0x0a, 0x22, 0x24, 0x8d, 0x90, 0xfd, 0xc8, 0xa3,
+ 0x7b, 0x53, 0x9c, 0x32, 0x6f, 0x9f, 0x2b, 0xe5, 0x6a, 0x0a, 0x90, 0xc8,
+ 0x37, 0x6a, 0xf3, 0x6a, 0x60, 0xd9, 0xc2, 0xc4, 0x96, 0x9d, 0x53, 0xa6,
+ 0x95, 0xf4, 0xa7, 0xab, 0x30, 0x39, 0x0a, 0xf7, 0x0b, 0x36, 0xe2, 0x55,
+ 0x2f, 0x8f, 0xc7, 0xd1, 0xc8, 0x47, 0x44, 0x98, 0x20, 0xf6, 0x16, 0x50,
+ 0x66, 0x83, 0x91, 0x9c, 0x1a, 0x65, 0x7e, 0x5b, 0x81, 0xde, 0xca, 0x13,
+ 0xfa, 0x42, 0x47, 0x57, 0xeb, 0x9a, 0x84, 0xd7, 0x50, 0xaf, 0xa0, 0xae,
+ 0x0f, 0x5d, 0xe7, 0xcc, 0x65, 0xa0, 0x93, 0x45, 0x1d, 0x2e, 0x72, 0x3b,
+ 0x41, 0xf6, 0xd0, 0x3d, 0xf4, 0xaa, 0x82, 0x37, 0x81, 0xd1, 0xf7, 0x59,
+ 0x0f, 0x9d, 0x6d, 0xb4, 0x3d, 0xd4, 0x53, 0x60, 0xbf, 0x11, 0x12, 0x33,
+ 0xaf, 0xd5, 0x40, 0x09, 0xe3, 0x41, 0xac, 0x28, 0x31, 0x63, 0xf5, 0x93,
+ 0x43, 0xc7, 0x8d, 0x28, 0xba, 0x1a, 0x6a, 0x35, 0x46, 0x1b, 0x99, 0xd7,
+ 0x2b, 0x74, 0x0a, 0xf4, 0x89, 0x33, 0x96, 0xdc, 0x60, 0x6b, 0xd5, 0xa7,
+ 0x68, 0x5f, 0xbf, 0x1c, 0xbf, 0x5a, 0xe2, 0x96, 0x07, 0xbf, 0x3c, 0x0a,
+ 0xc2, 0x3f, 0x27, 0x89, 0x5e, 0x9a, 0x52, 0x27, 0x4b, 0xe6, 0xca, 0xcf,
+ 0x4d, 0x15, 0x53, 0x64, 0xc6, 0x7f, 0x89, 0xd1, 0x3e, 0xc0, 0x6b, 0x9e,
+ 0xd2, 0x30, 0xdc, 0xdf, 0x6c, 0xf5, 0x03, 0xac, 0xcb, 0x02, 0x4c, 0x79,
+ 0xf1, 0xd4, 0x9b, 0xd6, 0xe1, 0x86, 0x56, 0xc8, 0x36, 0xaf, 0x02, 0xd4,
+ 0xd4, 0x8c, 0xb9, 0xd9, 0xea, 0x50, 0x75, 0x97, 0xd7, 0x01, 0x3f, 0x7d,
+ 0x1a, 0x6b, 0x6a, 0x4b, 0x13, 0xfc, 0xce, 0x22, 0xd0, 0x1f, 0x0b, 0x32,
+ 0xbe, 0x7f, 0x01, 0x01, 0xfb, 0x7e, 0xcd, 0xb1, 0x26, 0x2a, 0x47, 0x74,
+ 0xaf, 0xa8, 0x1d, 0xe2, 0xf3, 0xcf, 0xb2, 0x23, 0x69, 0xeb, 0x9e, 0xfd,
+ 0xcf, 0xdf, 0x5d, 0x7d, 0xd2, 0x65, 0x2c, 0xf4, 0x38, 0xb4, 0x07, 0x38,
+ 0x67, 0x0c, 0x6b, 0x04, 0x77, 0xa4, 0x77, 0x8d, 0x94, 0x4c, 0xf2, 0x55,
+ 0x77, 0x95, 0x4e, 0xc2, 0x4e, 0xb7, 0x7a, 0xf9, 0xc1, 0x8b, 0x8b, 0x2a,
+ 0xc9, 0x71, 0xd9, 0x69, 0xf4, 0x35, 0x67, 0x86, 0xb4, 0xbc, 0x2d, 0x91,
+ 0x3e, 0x3c, 0xba, 0x73, 0x8a, 0xba, 0x37, 0xc2, 0x39, 0xaa, 0x4d, 0x5c,
+ 0x8b, 0x3c, 0x8d, 0xd1, 0x64, 0x70, 0x6c, 0xf1, 0x9c, 0x25, 0x26, 0xf4,
+ 0xd1, 0xdb, 0x33, 0x9c, 0x1e, 0xc5, 0xb0, 0x21, 0xf9, 0xaf, 0x77, 0x5a,
+ 0xdc, 0x7b, 0xec, 0xcd, 0xc4, 0x78, 0x12, 0xa0, 0x2a, 0xab, 0x2a, 0xaf,
+ 0xbe, 0xd7, 0x00, 0xcf, 0x22, 0x2c, 0xd5, 0x0b, 0xce, 0xc6, 0x6e, 0xc3,
+ 0x82, 0xbf, 0x1d, 0xf0, 0x6e, 0x61, 0x96, 0xbb, 0x0a, 0xdc, 0x1e, 0x86,
+ 0x8c, 0x37, 0xb8, 0xa5, 0x10, 0x75, 0xfe, 0xb4, 0x7e, 0x01, 0x4d, 0x20,
+ 0x4d, 0xa4, 0x64, 0x1d, 0x4b, 0x56, 0x89, 0x7c, 0x27, 0xb0, 0x41, 0x54,
+ 0xab, 0x5e, 0xcb, 0x36, 0x56, 0xac, 0xbb, 0xfb, 0x53, 0x14, 0xad, 0x5f,
+ 0xeb, 0xaf, 0x19, 0xe7, 0x72, 0xfe, 0x90, 0xb3, 0x29, 0x66, 0x7e, 0x90,
+ 0xab, 0x06, 0x3a, 0x73, 0x37, 0x6a, 0xd1, 0x7e, 0xc9, 0x33, 0xbd, 0x25,
+ 0x81, 0xf6, 0x30, 0x70, 0x66, 0x5f, 0x08, 0xa0, 0x11, 0xd5, 0x0a, 0xa1,
+ 0xc4, 0xa1, 0xef, 0x94, 0xaa, 0x45, 0xf3, 0x12, 0xa6, 0x1b, 0x66, 0xca,
+ 0x34, 0x3e, 0xc8, 0x23, 0xbd, 0x7d, 0x70, 0x96, 0x81, 0xaa, 0x05, 0x22,
+ 0xaa, 0x2e, 0xe4, 0x1c, 0xd4, 0x75, 0xdd, 0xcc, 0x57, 0x81, 0xac, 0x68,
+ 0xb0, 0x68, 0xbe, 0x06, 0x72, 0x4a, 0x11, 0x0b, 0xf4, 0xa7, 0x18, 0x7d,
+ 0x9a, 0xf7, 0xe1, 0x4b, 0x1c, 0xc0, 0x77, 0x3d, 0x25, 0x54, 0xe6, 0x40,
+ 0xd6, 0x09, 0x12, 0x77, 0x57, 0xc6, 0xa7, 0x89, 0x79, 0x4c, 0xf6, 0x7e,
+ 0xe3, 0xe6, 0x86, 0xf9, 0x1d, 0xf6, 0x0d, 0xa4, 0xb8, 0x03, 0x37, 0x47,
+ 0x84, 0xfa, 0x9f, 0xd6, 0x62, 0x19, 0xc3, 0x27, 0xa3, 0x52, 0xfd, 0x06,
+ 0x39, 0x89, 0x81, 0x78, 0x94, 0x1d, 0x9e, 0x01, 0xd0, 0xed, 0xea, 0x22,
+ 0x11, 0x94, 0x71, 0x97, 0x81, 0x16, 0x16, 0xaf, 0xe5, 0x29, 0x9a, 0xb6,
+ 0xaa, 0x06, 0xf6, 0xff, 0x9c, 0x7c, 0x6d, 0xa7, 0xf0, 0xfc, 0x28, 0xac,
+ 0xef, 0x60, 0x36, 0x6c, 0x50, 0x8c, 0x78, 0x85, 0xb9, 0xb0, 0x76, 0x00,
+ 0xdd, 0xbd, 0xbd, 0xab, 0x02, 0xf2, 0x7a, 0x19, 0x54, 0x19, 0xde, 0xad,
+ 0x93, 0x69, 0x4b, 0x4b, 0x53, 0xbe, 0x27, 0x60, 0xc2, 0x28, 0xf4, 0x5b,
+ 0xbf, 0xb5, 0xad, 0x61, 0xc9, 0xf6, 0x71, 0x4b, 0x09, 0x00, 0xfc, 0xd6,
+ 0xff, 0xe2, 0xea, 0xea, 0x91, 0x35, 0xa8, 0x85, 0xa1, 0x03, 0xcb, 0x4e,
+ 0x94, 0x8b, 0x6a, 0x1b, 0x15, 0xbe, 0x5d, 0x24, 0xc3, 0x10, 0x8b, 0x0c,
+ 0x03, 0x69, 0xe0, 0xdd, 0x33, 0x08, 0xb9, 0x72, 0xa8, 0x05, 0xd3, 0xa1,
+ 0x48, 0x48, 0xe4, 0x5a, 0x74, 0xce, 0x22, 0x49, 0xef, 0x2b, 0xe4, 0x14,
+ 0xba, 0x22, 0x94, 0xcd, 0x42, 0xe2, 0x64, 0xe8, 0xce, 0xf8, 0x81, 0x4b,
+ 0xd4, 0xe8, 0x7a, 0xec, 0x63, 0xd3, 0x12, 0xa8, 0x11, 0x9f, 0xd4, 0x30,
+ 0x65, 0x80, 0x1d, 0x00, 0x26, 0x2b, 0xa2, 0x97, 0x90, 0x65, 0xfa, 0xe9,
+ 0x81, 0xa1, 0xdc, 0xc7, 0xbc, 0xdc, 0x7c, 0xee, 0x52, 0xae, 0x2a, 0x69,
+ 0xaf, 0x81, 0xf9, 0xed, 0x04, 0x37, 0xbc, 0x41, 0xda, 0x3a, 0x3d, 0xdb,
+ 0x21, 0x22, 0xe7, 0x8e, 0xec, 0xe3, 0xf3, 0xb1, 0xed, 0xdb, 0xaf, 0x6f,
+ 0xc3, 0x96, 0xa2, 0xcf, 0x6a, 0xdd, 0xb5, 0x35, 0xf9, 0x77, 0xdc, 0xb0,
+ 0x30, 0x26, 0xd9, 0x0b, 0x5c, 0xac, 0x38, 0xb2, 0xe2, 0x5d, 0x8c, 0x0f,
+ 0xcc, 0x57, 0x81, 0x99, 0x3b, 0xab, 0xc6, 0xa7, 0x61, 0xdd, 0xc8, 0x7a,
+ 0xdc, 0x96, 0x99, 0x8f, 0xed, 0x6e, 0x16, 0x82, 0x47, 0xcf, 0x08, 0x6f,
+ 0x61, 0x07, 0xa3, 0x7f, 0x7b, 0x7e, 0x32, 0x60, 0x56, 0x2b, 0x42, 0x60,
+ 0xb7, 0xa6, 0xbc, 0xe8, 0x8a, 0x79, 0x2c, 0x56, 0x58, 0x57, 0xe6, 0xbb,
+ 0xe2, 0x4d, 0x65, 0x50, 0x49, 0x33, 0x46, 0xb0, 0xfb, 0x89, 0x8a, 0x9c,
+ 0xc4, 0x2e, 0x8c, 0x34, 0xa2, 0xee, 0x6d, 0xb7, 0x19, 0x4c, 0x3f, 0x30,
+ 0xd5, 0xf9, 0x80, 0x11, 0xe9, 0x66, 0x9a, 0x71, 0xdf, 0x1b, 0xbc, 0x1d,
+ 0x2f, 0x7b, 0x6e, 0x84, 0x74, 0xb6, 0xca, 0x3f, 0xaa, 0x41, 0xae, 0x1a,
+ 0x3d, 0x34, 0xca, 0xb5, 0xf8, 0x13, 0xbb, 0x96, 0x9d, 0x3f, 0x3c, 0x15,
+ 0xf0, 0xc8, 0x90, 0xbc, 0x5b, 0x4a, 0xa0, 0x71, 0x63, 0x74, 0x1b, 0x49,
+ 0x61, 0xfe, 0x37, 0xac, 0x82, 0xbb, 0x68, 0x3a, 0x06, 0xf7, 0xda, 0x84,
+ 0x59, 0x91, 0x0e, 0xcd, 0xb9, 0x53, 0x2d, 0xbb, 0xe9, 0x58, 0xd1, 0x23,
+ 0x42, 0x64, 0x49, 0x64, 0x15, 0x02, 0xe8, 0x06, 0x17, 0xaa, 0xef, 0x13,
+ 0xb3, 0xc0, 0x47, 0xf2, 0xfa, 0xd5, 0x97, 0xd1, 0x2f, 0xc8, 0x69, 0xa3,
+ 0x21, 0xa7, 0xbe, 0x7f, 0x59, 0x7e, 0x68, 0x82, 0x2f, 0x04, 0x13, 0x38,
+ 0x5a, 0x73, 0x88, 0x70, 0xde, 0x4b, 0xd6, 0xc5, 0x9a, 0x67, 0x4b, 0x04,
+ 0xf6, 0xf4, 0xf9, 0xd2, 0xd2, 0xf9, 0x02, 0x03, 0x04, 0x69, 0xc5, 0x1c,
+ 0x2e, 0x63, 0xb2, 0x25, 0x3b, 0x07, 0x79, 0x40, 0x65, 0x5a, 0xa8, 0xc1,
+ 0x48, 0x9f, 0xef, 0x48, 0x50, 0x75, 0x35, 0xc3, 0x4e, 0xfd, 0x16, 0x05,
+ 0x93, 0x81, 0x2e, 0x31, 0xdf, 0xce, 0x7a, 0xde, 0x9c, 0xc4, 0x5a, 0xa1,
+ 0x9e, 0x9d, 0x5b, 0x13, 0xbc, 0x5e, 0x97, 0x43, 0xcc, 0xa2, 0x47, 0xf1,
+ 0x19, 0x5e, 0x84, 0xaa, 0x5a, 0x7a, 0x26, 0x8a, 0x01, 0x10, 0xd1, 0xe3,
+ 0xc7, 0xcd, 0x9b, 0x57, 0x3e, 0xf5, 0x1f, 0x62, 0xd3, 0x39, 0xd5, 0x56,
+ 0xce, 0x38, 0x0a, 0x11, 0x0e, 0x00, 0xbe, 0xf0, 0x8b, 0xc7, 0xb4, 0x68,
+ 0x30, 0x2b, 0x08, 0x93, 0xf8, 0xb3, 0x12, 0x67, 0x5a, 0x07, 0x34, 0xcf,
+ 0x80, 0x54, 0x2e, 0xa9, 0x3f, 0x00, 0x15, 0x97, 0x34, 0x1f, 0x8e, 0xed,
+ 0xcf, 0xf0, 0x70, 0xae, 0x80, 0xe8, 0x4d, 0xeb, 0x20, 0x53, 0x4b, 0x19,
+ 0x88, 0xfb, 0x29, 0x3d, 0x15, 0xfc, 0x60, 0x3d, 0x1e, 0xb2, 0x99, 0xe6,
+ 0x01, 0xb5, 0x5b, 0x86, 0x8c, 0x5a, 0x91, 0x1d, 0x32, 0xdc, 0x0c, 0xf2,
+ 0xb0, 0x4f, 0xda, 0xdb, 0xc6, 0x7a, 0x8a, 0x36, 0x20, 0x24, 0xfc, 0xcb,
+ 0xd1, 0xc1, 0xbf, 0x18, 0xd0, 0x02, 0xb7, 0xa5, 0xfc, 0xeb, 0x50, 0x1a,
+ 0x27, 0xb0, 0xd5, 0x67, 0x4a, 0xaa, 0x3b, 0x9b, 0x5c, 0x87, 0x3c, 0xd1,
+ 0x4a, 0x4c, 0x5c, 0xd3, 0x85, 0x54, 0x43, 0xe0, 0xdc, 0xe2, 0x15, 0x64,
+ 0x0f, 0x45, 0x6c, 0x21, 0xc0, 0xa9, 0x22, 0x88, 0x22, 0xa9, 0x9a, 0x32,
+ 0x98, 0x8b, 0x0a, 0xd9, 0x35, 0x6a, 0x3e, 0x3b, 0xfc, 0x22, 0x8f, 0xf7,
+ 0xd3, 0x67, 0x2b, 0x57, 0x8b, 0x6d, 0x84, 0x68, 0xf7, 0xa1, 0xe0, 0xd5,
+ 0xaf, 0xb5, 0x05, 0x0c, 0xbf, 0xe4, 0x2e, 0x6c, 0x29, 0x6d, 0xd2, 0xb0,
+ 0x57, 0x26, 0x80, 0xe4, 0xb1, 0xa0, 0x4d, 0x85, 0x7c, 0xe5, 0x64, 0x25,
+ 0x0a, 0x15, 0x1b, 0xf7, 0x52, 0xd8, 0x04, 0x6d, 0xbb, 0xc2, 0xdf, 0xfe,
+ 0xde, 0xa5, 0xd2, 0xd5, 0x75, 0x92, 0x2f, 0x9a, 0xdd, 0x5d, 0x0f, 0xf3,
+ 0xe0, 0xfa, 0x7a, 0x54, 0xbb, 0x40, 0x76, 0xac, 0x35, 0x74, 0x02, 0xc6,
+ 0x59, 0xbf, 0x7f, 0x80, 0x74, 0xac, 0x3b, 0x1d, 0x3a, 0xa2, 0xb1, 0x9b,
+ 0xd6, 0xf7, 0x46, 0x1b, 0xe6, 0x31, 0xc5, 0x06, 0xc6, 0xe2, 0xac, 0x08,
+ 0x80, 0x1e, 0xdc, 0xc4, 0xf6, 0x15, 0x95, 0x54, 0xb9, 0x62, 0xe5, 0x04,
+ 0x5e, 0x0a, 0xa8, 0xcb, 0x84, 0x37, 0xb3, 0xfa, 0xdc, 0xd0, 0x20, 0xcc,
+ 0x75, 0x98, 0xb5, 0x6c, 0x3c, 0x76, 0x6f, 0x34, 0x0e, 0x83, 0x43, 0xae,
+ 0x8d, 0xe1, 0x20, 0x38, 0x6b, 0x5f, 0xee, 0xf2, 0x4c, 0x34, 0xc2, 0xd7,
+ 0x51, 0xc4, 0x4b, 0x4c, 0x48, 0x49, 0xeb, 0xc7, 0x35, 0x30, 0xd9, 0x7a,
+ 0x7f, 0xcf, 0x70, 0xe2, 0xc0, 0xf5, 0x36, 0xbc, 0x5c, 0x2b, 0x5e, 0xae,
+ 0x09, 0xe1, 0x38, 0x1f, 0x7b, 0x9e, 0xca, 0xdb, 0x86, 0xec, 0x77, 0xe6,
+ 0x38, 0x17, 0xce, 0x7b, 0x2e, 0xcf, 0x6a, 0x54, 0xda, 0xb6, 0x0e, 0x51,
+ 0xda, 0x50, 0xd9, 0x0c, 0xa6, 0xe4, 0xa0, 0xeb, 0xf0, 0xb6, 0xb9, 0xd4,
+ 0x91, 0x12, 0x8a, 0x51, 0xf0, 0xef, 0xda, 0xa8, 0x69, 0xec, 0x1f, 0x7f,
+ 0x08, 0xcd, 0x8c, 0xdd, 0x12, 0x96, 0xa2, 0xef, 0xac, 0x52, 0xc9, 0x4b,
+ 0x94, 0xea, 0x69, 0x50, 0x55, 0x3c, 0xc4, 0x4f, 0x7e, 0x52, 0x94, 0xb4,
+ 0xb4, 0x1c, 0xa1, 0x7f, 0x29, 0xf2, 0x32, 0xdd, 0x94, 0xdf, 0xea, 0x47,
+ 0x6c, 0x67, 0xcf, 0xe0, 0x12, 0x7a, 0x29, 0xbf, 0xd9, 0x2c, 0xcb, 0x4f,
+ 0x01, 0xb0, 0x62, 0x9f, 0xf0, 0xf2, 0x58, 0xa5, 0xff, 0x13, 0x3e, 0x7f,
+ 0x95, 0xfc, 0x9b, 0xf2, 0xc3, 0x8c, 0x69, 0x6b, 0xa7, 0xec, 0xf7, 0x33,
+ 0x70, 0x00, 0x08, 0x57, 0x2d, 0x0b, 0xe9, 0xd3, 0xfd, 0x43, 0x98, 0xea,
+ 0x90, 0x3c, 0x0e, 0x41, 0x00, 0xb9, 0x12, 0xcb, 0x22, 0x0f, 0x98, 0x42,
+ 0xab, 0x07, 0xad, 0x06, 0x65, 0xb3, 0xd7, 0xc9, 0x54, 0x16, 0x33, 0x97,
+ 0xca, 0x2d, 0x1d, 0xd9, 0x67, 0xe0, 0x7c, 0x50, 0x58, 0x05, 0x3f, 0x63,
+ 0xc2, 0x5d, 0x57, 0x0d, 0x47, 0xfb, 0x97, 0x8c, 0x6a, 0xff, 0x70, 0x25,
+ 0xe5, 0x7f, 0x68, 0x4c, 0x81, 0x84, 0x08, 0x89, 0xf8, 0x8a, 0x04, 0xa6,
+ 0x5b, 0xa3, 0x32, 0x20, 0x85, 0x1a, 0xf7, 0x70, 0x3f, 0xf2, 0xbc, 0x29,
+ 0x5d, 0x78, 0xa6, 0x0d, 0x82, 0x1d, 0x7d, 0x5c, 0x3b, 0xa3, 0x18, 0xab,
+ 0x5a, 0xfc, 0xd3, 0xda, 0xc1, 0x20, 0x9f, 0xbb, 0x3c, 0x7e, 0x70, 0xca,
+ 0x49, 0x65, 0x6b, 0xcd, 0x6e, 0x34, 0xe5, 0x39, 0x5d, 0x58, 0x44, 0x34,
+ 0x50, 0xc8, 0x73, 0x56, 0x39, 0x8e, 0xf7, 0xf6, 0xec, 0x42, 0x9a, 0x82,
+ 0x87, 0x89, 0x7a, 0x48, 0x84, 0xd8, 0x2c, 0xce, 0xc7, 0xbd, 0x9e, 0xfe,
+ 0xa5, 0x1b, 0xe6, 0xa4, 0xb3, 0x4c, 0xa2, 0xde, 0xae, 0x12, 0x29, 0x8e,
+ 0x3c, 0x42, 0x18, 0xf5, 0xd8, 0x1e, 0xbb, 0x06, 0xa4, 0xc5, 0xff, 0xd0,
+ 0x71, 0xce, 0x1b, 0xec, 0x77, 0x1a, 0x04, 0xd5, 0x5d, 0xd1, 0x46, 0xff,
+ 0x5b, 0x79, 0x4f, 0x9b, 0x1e, 0x14, 0x7a, 0xd7, 0x28, 0x98, 0x23, 0x12,
+ 0x77, 0x52, 0xa6, 0x7d, 0x81, 0x44, 0xc5, 0x40, 0x73, 0x46, 0x28, 0x67,
+ 0x08, 0xc1, 0xf3, 0xde, 0x01, 0xc1, 0x9d, 0x29, 0xcf, 0x7e, 0x62, 0x1c,
+ 0xad, 0xb9, 0xfb, 0xf8, 0x97, 0x5e, 0x73, 0xe0, 0x95, 0x6f, 0x0a, 0x00,
+ 0xce, 0xd2, 0x9d, 0x42, 0xf8, 0x74, 0x4d, 0x5d, 0x0a, 0x7c, 0x9c, 0xda,
+ 0x31, 0x8e, 0x80, 0x36, 0x1c, 0x3f, 0x05, 0x04, 0x5e, 0x37, 0x8a, 0xcf,
+ 0xde, 0x66, 0x2b, 0x5f, 0xda, 0x65, 0x54, 0x42, 0x6c, 0x16, 0x4f, 0x3e,
+ 0xd3, 0xd1, 0x1b, 0xcd, 0xe0, 0xc8, 0x8d, 0x40, 0x64, 0xad, 0x8e, 0x4d,
+ 0xc2, 0xa7, 0x46, 0x40, 0x1b, 0x15, 0x13, 0x2d, 0x82, 0x50, 0x46, 0xe0,
+ 0x1e, 0x53, 0x10, 0x74, 0x7c, 0xf9, 0x64, 0xef, 0xb5, 0xf8, 0xeb, 0xe9,
+ 0x07, 0x15, 0x85, 0x37, 0xc7, 0xb4, 0xdf, 0xa3, 0x51, 0x55, 0xd6, 0xd8,
+ 0x7e, 0xac, 0x18, 0x38, 0x61, 0xd0, 0x8e, 0x17, 0x32, 0x0d, 0xd2, 0x73,
+ 0x16, 0x72, 0x43, 0xae, 0xf5, 0x87, 0x92, 0x72, 0x47, 0xb1, 0xe7, 0xb0,
+ 0xff, 0x35, 0x62, 0x69, 0xaa, 0x32, 0xe8, 0x0d, 0xa1, 0x73, 0xa9, 0x5c,
+ 0xc2, 0x60, 0x51, 0x00, 0x4a, 0xab, 0xf8, 0x1b, 0x16, 0xf9, 0xe0, 0x74,
+ 0x8b, 0xe6, 0xe4, 0x6f, 0x7e, 0x79, 0x9e, 0x81, 0xc7, 0x6d, 0xf7, 0xaa,
+ 0x22, 0xae, 0xc7, 0x7b, 0x7c, 0xdf, 0x04, 0x57, 0x47, 0x5d, 0xfd, 0x05,
+ 0x89, 0x4c, 0xac, 0x31, 0xf0, 0x25, 0x4d, 0x12, 0x03, 0x80, 0x69, 0xae,
+ 0x31, 0xfd, 0x4f, 0x63, 0xf1, 0xc1, 0xd9, 0x32, 0xa9, 0x1c, 0x26, 0x43,
+ 0x1e, 0x71, 0x07, 0xe6, 0xae, 0x88, 0x7b, 0xb4, 0x5d, 0xea, 0x47, 0x04,
+ 0xe4, 0x86, 0x7e, 0x03, 0x31, 0x88, 0x7c, 0x79, 0x5e, 0x0f, 0xb8, 0xd7,
+ 0x92, 0x63, 0x6d, 0x77, 0xab, 0x47, 0x53, 0x5e, 0x17, 0xe6, 0xef, 0x66,
+ 0x12, 0x25, 0x4c, 0x27, 0x16, 0x04, 0x51, 0xd0, 0x74, 0x41, 0xb0, 0x5e,
+ 0x41, 0x66, 0x58, 0x23, 0x3f, 0xad, 0xd1, 0x47, 0xba, 0x9d, 0xd3, 0xa5,
+ 0x2d, 0x20, 0xb8, 0xa6, 0x4e, 0x7e, 0x4a, 0x00, 0xf6, 0x0f, 0x9b, 0x1a,
+ 0x62, 0x30, 0x3d, 0x8e, 0xae, 0xd2, 0x89, 0xe1, 0x98, 0x4b, 0x3e, 0xf0,
+ 0xe8, 0x84, 0x5b, 0xf9, 0x54, 0xd9, 0xae, 0xfd, 0xed, 0x58, 0x80, 0xd3,
+ 0x66, 0xe6, 0x0f, 0x69, 0x91, 0xdf, 0xca, 0xb8, 0x97, 0xb5, 0x6b, 0xdc,
+ 0x0d, 0xcf, 0x2e, 0xd8, 0xb8, 0xe6, 0xd2, 0xbf, 0xf9, 0x08, 0x32, 0xf1,
+ 0xc0, 0x2e, 0x7a, 0xbd, 0x69, 0xa7, 0xd0, 0x5e, 0x52, 0xd3, 0x63, 0xf0,
+ 0x0e, 0xb9, 0x42, 0xa0, 0xcb, 0x30, 0x9b, 0xdd, 0x60, 0x95, 0xe6, 0xf4,
+ 0xeb, 0x48, 0xdc, 0x81, 0xdd, 0x81, 0xfa, 0xa6, 0x1e, 0x31, 0x1b, 0x85,
+ 0xd6, 0x83, 0x16, 0x55, 0xe9, 0x05, 0x0a, 0xf2, 0x24, 0x9e, 0x8c, 0xd5,
+ 0x43, 0xe8, 0x96, 0xce, 0x10, 0x60, 0x23, 0xfe, 0x55, 0x88, 0x21, 0x10,
+ 0x12, 0x5f, 0x96, 0xfa, 0xbf, 0xf2, 0xf9, 0xda, 0x0b, 0x1d, 0x40, 0x94,
+ 0x19, 0xda, 0x0f, 0x4b, 0xe2, 0xc4, 0xe6, 0x8a, 0x81, 0x07, 0x27, 0x3f,
+ 0x1c, 0xef, 0x37, 0xbb, 0xaa, 0xb1, 0x14, 0x23, 0x59, 0x0d, 0x3e, 0x9f,
+ 0x1a, 0x7e, 0x34, 0xaa, 0x9b, 0xd9, 0x25, 0x94, 0x2a, 0xa8, 0x0f, 0xa2,
+ 0xe6, 0xec, 0x48, 0x36, 0xf5, 0x03, 0xf0, 0x36, 0xb9, 0xf6, 0xd9, 0x4d,
+ 0xc2, 0xf7, 0x4e, 0x87, 0x20, 0xf0, 0x33, 0x26, 0x2b, 0x8b, 0xf2, 0xd7,
+ 0x14, 0x58, 0xde, 0x9d, 0x9e, 0x7d, 0xf0, 0x6a, 0x74, 0x9b, 0xd7, 0x50,
+ 0xcf, 0xbc, 0x8d, 0xd0, 0xe7, 0x88, 0xce, 0x4d, 0x18, 0x47, 0x0a, 0x0d,
+ 0x21, 0x7d, 0x96, 0x11, 0xa9, 0xf0, 0x06, 0x38, 0x47, 0xf1, 0x4c, 0xf4,
+ 0xb9, 0x1a, 0x54, 0xd5, 0x02, 0xe0, 0xdb, 0x00, 0x0a, 0x45, 0x42, 0x76,
+ 0x9f, 0xbf, 0x4b, 0xa7, 0xad, 0xf0, 0x42, 0x57, 0xd5, 0xb8, 0x1e, 0xb5,
+ 0x54, 0x2c, 0x4d, 0xff, 0x3f, 0x0c, 0xf7, 0xde, 0xdb, 0x3d, 0x80, 0xc1,
+ 0xdb, 0x32, 0x2c, 0x43, 0x25, 0x18, 0x90, 0x59, 0xac, 0xc7, 0xb9, 0x35,
+ 0xb8, 0x88, 0xfe, 0x76, 0x71, 0x80, 0xdc, 0xe8, 0xdd, 0x5a, 0xfa, 0x44,
+ 0x38, 0x0a, 0xec, 0x07, 0x2c, 0xe8, 0xd2, 0x6a, 0xd1, 0x2a, 0xc8, 0x18,
+ 0x4e, 0x79, 0xf7, 0x4f, 0x60, 0xf6, 0xdf, 0x04, 0xc8, 0x16, 0x69, 0x96,
+ 0x66, 0x06, 0xaf, 0xac, 0x68, 0x73, 0xe7, 0x4b, 0x71, 0x86, 0xae, 0x87,
+ 0xaf, 0xe8, 0x03, 0x7a, 0xb9, 0xb6, 0x0b, 0xbd, 0x1e, 0x43, 0xe2, 0xa7,
+ 0x88, 0x59, 0x80, 0xee, 0x97, 0xe7, 0x68, 0x9f, 0xef, 0x89, 0x44, 0xbf,
+ 0x25, 0xf4, 0xde, 0x42, 0x65, 0x35, 0x91, 0x42, 0xbd, 0x33, 0x60, 0x24,
+ 0xbb, 0xc5, 0xc3, 0x67, 0xe1, 0x5d, 0xd3, 0x62, 0xc8, 0x47, 0xee, 0x2b,
+ 0x47, 0xac, 0xb9, 0x2d, 0xf4, 0x8f, 0x62, 0x16, 0x32, 0xd6, 0xce, 0x06,
+ 0x3e, 0x32, 0x30, 0x93, 0x72, 0x64, 0x41, 0xa5, 0x2d, 0x03, 0x05, 0x6d,
+ 0x0b, 0x2d, 0x7e, 0xb4, 0xfb, 0x86, 0x82, 0x10, 0xe9, 0x3a, 0x16, 0x29,
+ 0x50, 0x86, 0xdb, 0x8c, 0x93, 0x0e, 0x03, 0x66, 0x20, 0xc4, 0x3b, 0x8c,
+ 0x4a, 0x32, 0xd6, 0x3c, 0x4d, 0xb8, 0x1c, 0x57, 0xf3, 0x8e, 0x06, 0xa4,
+ 0x54, 0xd1, 0xd0, 0xab, 0x62, 0x16, 0xb7, 0x5b, 0x4e, 0x8e, 0xfa, 0x8d,
+ 0x78, 0xb0, 0x97, 0x67, 0x26, 0x26, 0xec, 0x9a, 0xfb, 0xc3, 0xee, 0x4b,
+ 0xc5, 0xeb, 0x6c, 0x5d, 0x2e, 0xb4, 0x2f, 0x74, 0x03, 0xf7, 0x20, 0x99,
+ 0x0c, 0x39, 0x23, 0xe1, 0x4e, 0xfe, 0x35, 0x41, 0xe8, 0x59, 0xaa, 0xdd,
+ 0xf8, 0xfe, 0xa5, 0x2d, 0x2c, 0x95, 0x00, 0x1a, 0xc9, 0x9c, 0x3c, 0xb2,
+ 0xd3, 0xa6, 0xfd, 0x6b, 0xe3, 0x59, 0xa9, 0x1d, 0x07, 0x18, 0xca, 0xde,
+ 0x3c, 0x0e, 0x5f, 0x6c, 0x7a, 0xd1, 0x70, 0xe9, 0x2d, 0xb5, 0x61, 0x1b,
+ 0x81, 0x6b, 0x0e, 0x5a, 0x9b, 0x3e, 0x21, 0xf8, 0xab, 0xba, 0xd9, 0xb1,
+ 0x2a, 0x8a, 0x81, 0x9f, 0x5e, 0x2c, 0x64, 0xaa, 0xdd, 0x59, 0xd0, 0xa6,
+ 0x9a, 0xb1, 0x83, 0x5c, 0x18, 0xfa, 0xe0, 0x8f, 0xf8, 0xa3, 0x13, 0x02,
+ 0x37, 0x43, 0xb0, 0x7b, 0x3e, 0x83, 0x6e, 0xdf, 0x87, 0xbc, 0x34, 0xf3,
+ 0x54, 0xa1, 0x9e, 0x43, 0x1a, 0xa1, 0x66, 0x36, 0xee, 0x58, 0x34, 0x34,
+ 0x3a, 0xa2, 0x19, 0xa8, 0xfd, 0x51, 0xcf, 0xae, 0x0a, 0x5a, 0x77, 0x0f,
+ 0xcf, 0x54, 0x38, 0x77, 0x2c, 0xcf, 0x14, 0xdf, 0x4a, 0x2d, 0x6b, 0x9b,
+ 0x57, 0x08, 0x5a, 0x5d, 0xfb, 0x57, 0x2c, 0x4b, 0x75, 0x70, 0x74, 0x4d,
+ 0x31, 0xf2, 0x67, 0xbf, 0xc9, 0x16, 0x35, 0x62, 0xa7, 0x1a, 0xfd, 0xc1,
+ 0xa9, 0x33, 0x86, 0x96, 0x9d, 0xdf, 0x02, 0xcf, 0xf8, 0x0b, 0x6e, 0xc1,
+ 0x3f, 0xf6, 0x05, 0x00, 0xae, 0xb1, 0x7f, 0x39, 0xff, 0x8d, 0x10, 0x0e,
+ 0xf0, 0xfd, 0x50, 0x55, 0x86, 0x71, 0xbc, 0xdc, 0xd7, 0x33, 0x41, 0xae,
+ 0x4d, 0x37, 0x5d, 0xd0, 0x67, 0x3f, 0xda, 0x62, 0x0b, 0x01, 0xe2, 0x87,
+ 0x6c, 0xcd, 0x32, 0x4b, 0x07, 0x54, 0xe9, 0x6f, 0x13, 0x82, 0x12, 0x2e,
+ 0x3b, 0x3e, 0x9b, 0x36, 0x13, 0x10, 0x2c, 0xd0, 0xff, 0xd8, 0x80, 0x17,
+ 0x79, 0xd3, 0x12, 0x8a, 0x4a, 0x13, 0xf8, 0xf9, 0xd1, 0x93, 0x41, 0xca,
+ 0x4c, 0xad, 0xa5, 0x96, 0x12, 0xc6, 0x78, 0xfb, 0x94, 0xa8, 0x89, 0x53,
+ 0x0c, 0xc2, 0x0c, 0xf8, 0x7d, 0x55, 0x5a, 0xff, 0xea, 0xd2, 0x93, 0x95,
+ 0xa0, 0x18, 0x92, 0xb1, 0xf9, 0xfb, 0x30, 0x4f, 0xd5, 0xce, 0xe5, 0xb1,
+ 0x19, 0x69, 0x1b, 0x23, 0xd3, 0x3b, 0x31, 0xe0, 0x7b, 0x65, 0xc7, 0x38,
+ 0x78, 0x74, 0x00, 0x0f, 0x42, 0x19, 0x4a, 0x5c, 0xb0, 0x90, 0xdd, 0xb6,
+ 0x45, 0x8d, 0x6b, 0x5f, 0xe7, 0xe3, 0x0b, 0x37, 0xea, 0xde, 0xa6, 0xc5,
+ 0x73, 0xa2, 0x96, 0xb6, 0x33, 0x3e, 0x6b, 0xb0, 0x4f, 0xa5, 0xe3, 0xfc,
+ 0x15, 0x6d, 0xfc, 0x7d, 0x39, 0x8e, 0x0b, 0xf3, 0x0e, 0x2a, 0x1e, 0x21,
+ 0xce, 0x89, 0x13, 0x80, 0xe2, 0x67, 0x0b, 0x4a, 0x92, 0xa6, 0x2d, 0x7c,
+ 0x4b, 0xc7, 0x93, 0x20, 0x0e, 0x77, 0xde, 0xcf, 0xb1, 0x38, 0x9c, 0x49,
+ 0x61, 0x99, 0xd3, 0x45, 0xc7, 0xc1, 0x39, 0x4b, 0x28, 0x99, 0xb1, 0xab,
+ 0x6e, 0x00, 0xbb, 0x6f, 0x5a, 0xdd, 0x7c, 0x06, 0x43, 0x3c, 0x49, 0xcf,
+ 0x39, 0xe8, 0x6e, 0x44, 0xc9, 0xc7, 0x25, 0xdb, 0x7b, 0x40, 0xee, 0xf5,
+ 0xb4, 0xc1, 0x3b, 0x1c, 0x4e, 0xd0, 0x05, 0x1b, 0x0a, 0x65, 0x64, 0x5e,
+ 0x81, 0xd9, 0x7c, 0xf5, 0x61, 0x7a, 0x2f, 0x2f, 0x8d, 0x2d, 0xea, 0x15,
+ 0x71, 0x23, 0x11, 0x44, 0x31, 0xf9, 0x86, 0xb3, 0x62, 0x76, 0x05, 0x67,
+ 0xcf, 0xb6, 0x72, 0x8f, 0x9a, 0xe4, 0x69, 0xd0, 0x61, 0x0f, 0x98, 0xd7,
+ 0xc1, 0x00, 0x31, 0xd6, 0x9c, 0xf1, 0xb8, 0x7a, 0x07, 0x23, 0xc4, 0x9a,
+ 0xf9, 0xc3, 0x07, 0x1a, 0x9b, 0x8e, 0xba, 0xa7, 0x6f, 0x12, 0xcd, 0x98,
+ 0x2f, 0xbe, 0x3d, 0x8e, 0xc3, 0x04, 0xc1, 0xf4, 0xba, 0x29, 0x29, 0x0c,
+ 0x54, 0x98, 0x70, 0xdc, 0xbe, 0x57, 0xa9, 0x54, 0xdd, 0x67, 0x1e, 0x98,
+ 0xff, 0x04, 0x59, 0x07, 0x5f, 0xa1, 0x1b, 0xe4, 0xe2, 0x1d, 0x0f, 0x05,
+ 0xfd, 0x55, 0x4d, 0x6f, 0xec, 0xe6, 0x1d, 0x82, 0xf3, 0x2e, 0x37, 0x35,
+ 0xc6, 0xff, 0x79, 0x38, 0x9f, 0x7b, 0x47, 0x0b, 0x68, 0x26, 0xc2, 0x46,
+ 0x11, 0x88, 0xae, 0x4a, 0xe0, 0xfe, 0x53, 0x7f, 0x67, 0x1b, 0xb4, 0xd5,
+ 0x52, 0x42, 0xee, 0x5b, 0xa9, 0x07, 0x54, 0x30, 0x4d, 0xb7, 0x15, 0xf1,
+ 0x2c, 0x6c, 0x35, 0x80, 0xb5, 0x90, 0x58, 0xc3, 0xae, 0xcc, 0x7e, 0xc9,
+ 0x9c, 0x40, 0x03, 0xdc, 0xf2, 0x62, 0x76, 0x84, 0x9e, 0x99, 0x37, 0xc3,
+ 0x6d, 0x99, 0x99, 0x23, 0x14, 0xb3, 0xa3, 0xd3, 0x27, 0x3c, 0x6b, 0xb6,
+ 0x2e, 0xb5, 0xc0, 0xb8, 0x4d, 0xa4, 0xf5, 0x85, 0x37, 0x33, 0xf7, 0x4f,
+ 0x10, 0x65, 0x59, 0x16, 0xad, 0x5f, 0xaf, 0xc8, 0x35, 0x92, 0xd7, 0xed,
+ 0x8a, 0xaa, 0x83, 0xb8, 0xbd, 0x06, 0x29, 0x38, 0x9f, 0xec, 0xf1, 0x6f,
+ 0xdf, 0xe4, 0x51, 0xad, 0x58, 0x98, 0x6c, 0x90, 0xb6, 0x8c, 0x12, 0x9a,
+ 0x51, 0xa7, 0xbd, 0x93, 0x72, 0x51, 0x27, 0x82, 0x0e, 0xee, 0x0b, 0xde,
+ 0x2b, 0xf1, 0x80, 0x04, 0x95, 0xf6, 0xed, 0xbc, 0x33, 0xe6, 0xae, 0xf2,
+ 0x56, 0x88, 0xe6, 0x08, 0x31, 0x2c, 0xbc, 0xfe, 0xdb, 0x00, 0x21, 0x54,
+ 0x3e, 0x41, 0x7c, 0x0b, 0xa4, 0x97, 0x68, 0x7f, 0xf0, 0xf3, 0x42, 0x8c,
+ 0x9e, 0xb9, 0x3b, 0x2a, 0xc6, 0xbc, 0xd0, 0x69, 0x64, 0x9f, 0x31, 0xbb,
+ 0x45, 0x3e, 0x7d, 0x47, 0x32, 0x85, 0x8b, 0x23, 0xff, 0x41, 0xff, 0xe6,
+ 0x4d, 0xb3, 0x98, 0xaf, 0xa6, 0x6f, 0x1a, 0x84, 0x55, 0xa4, 0x45, 0xa3,
+ 0xff, 0x4f, 0x2c, 0x18, 0x38, 0x03, 0x25, 0x19, 0x93, 0xe8, 0x0f, 0x78,
+ 0xf4, 0x50, 0x06, 0xfe, 0xc7, 0xcf, 0x10, 0x03, 0x46, 0x99, 0xca, 0x71,
+ 0xd8, 0x1e, 0xac, 0x62, 0xba, 0x55, 0x88, 0x93, 0x19, 0x43, 0x18, 0x82,
+ 0xeb, 0x9a, 0x6c, 0xc2, 0xf4, 0xa3, 0xbf, 0x69, 0x76, 0x20, 0x9b, 0xb7,
+ 0xbb, 0x1c, 0x06, 0x3f, 0xcb, 0xb8, 0xc7, 0xbd, 0xad, 0x08, 0x31, 0xf2,
+ 0xdc, 0x49, 0xc3, 0x0d, 0xb7, 0x48, 0x07, 0x25, 0x15, 0x46, 0x49, 0x09,
+ 0x01, 0xcf, 0xa9, 0x65, 0xc5, 0x21, 0x6b, 0xe6, 0x72, 0x18, 0x46, 0x03,
+ 0xe4, 0xa2, 0xcc, 0x53, 0x55, 0xdc, 0x94, 0xf2, 0x72, 0xae, 0x48, 0x98,
+ 0x0d, 0x63, 0xf3, 0x7a, 0x6d, 0x3c, 0x6a, 0x5c, 0x51, 0x69, 0xa7, 0x53,
+ 0x26, 0x39, 0x14, 0xce, 0x22, 0xbc, 0x7b, 0xe0, 0x99, 0xed, 0x6f, 0x94,
+ 0x4e, 0x02, 0x33, 0x5a, 0xcc, 0x6f, 0xf8, 0xaf, 0x31, 0xbc, 0xae, 0x9e,
+ 0x10, 0xc3, 0xaf, 0x2d, 0x50, 0x31, 0x34, 0x66, 0xe0, 0xc1, 0x55, 0xd1,
+ 0xee, 0x16, 0x21, 0xac, 0xdc, 0x80, 0x64, 0xd5, 0xf9, 0x49, 0xa2, 0xf8,
+ 0xe6, 0xad, 0x46, 0x7e, 0x40, 0x7f, 0xec, 0x2f, 0x29, 0xea, 0xec, 0x16,
+ 0xaf, 0xca, 0x71, 0xff, 0x80, 0x2e, 0x9f, 0x65, 0xbd, 0x69, 0x74, 0x4f,
+ 0x4f, 0x93, 0xf4, 0x36, 0x2e, 0xd4, 0xc5, 0x14, 0x5e, 0x67, 0x99, 0x03,
+ 0x75, 0xa4, 0x4c, 0x1e, 0x00, 0x66, 0xf6, 0xf1, 0x1a, 0xb4, 0xcb, 0x49,
+ 0x6e, 0x78, 0xf9, 0x44, 0x05, 0x4f, 0x3a, 0x97, 0x60, 0xe4, 0x59, 0xe9,
+ 0x78, 0x60, 0x9d, 0x47, 0xf1, 0xc1, 0xcc, 0x1a, 0x42, 0x57, 0xa0, 0x0c,
+ 0x69, 0xa3, 0x9a, 0x78, 0xb5, 0xf2, 0xe6, 0xa7, 0xf2, 0x06, 0xc4, 0xa8,
+ 0xdb, 0xc2, 0xc9, 0xe5, 0x78, 0xa9, 0x81, 0xd0, 0x6c, 0x18, 0xe8, 0x09,
+ 0xa2, 0x13, 0xbe, 0x07, 0xc6, 0x11, 0xec, 0xc5, 0xc2, 0xa2, 0xe9, 0xed,
+ 0x97, 0x03, 0x0a, 0x6d, 0xdd, 0x40, 0x2d, 0x20, 0x58, 0x7b, 0x63, 0x55,
+ 0xd5, 0x9c, 0x67, 0x6b, 0xf8, 0xc8, 0x2b, 0x58, 0x0c, 0xb9, 0x5c, 0xd2,
+ 0x2a, 0xb6, 0xc9, 0x82, 0x96, 0xbf, 0x81, 0x55, 0x71, 0x1a, 0x59, 0x9f,
+ 0x26, 0xce, 0x06, 0x1e, 0x02, 0xca, 0x4c, 0x34, 0xa9, 0x15, 0xda, 0xfb,
+ 0x5b, 0xa9, 0xd2, 0x34, 0xe3, 0xf6, 0xa1, 0xe5, 0xc1, 0xe2, 0x9a, 0x75,
+ 0x5d, 0x30, 0xf8, 0x88, 0xc7, 0xd2, 0x1c, 0x6b, 0x60, 0x82, 0x43, 0xe7,
+ 0xeb, 0x4d, 0x42, 0x87, 0x27, 0xdd, 0x7e, 0x0e, 0x68, 0x21, 0xda, 0x3a,
+ 0x54, 0x88, 0x0b, 0x6d, 0x14, 0x88, 0xd4, 0x26, 0x7e, 0x97, 0x26, 0xd0,
+ 0x1f, 0x00, 0x81, 0xe1, 0x6d, 0x56, 0xf6, 0xf1, 0x4d, 0x79, 0x28, 0x7b,
+ 0xc3, 0x5e, 0x5a, 0x59, 0x07, 0x48, 0x06, 0x14, 0x4f, 0x70, 0x2c, 0xc3,
+ 0x40, 0x62, 0xc7, 0x38, 0x22, 0xb9, 0x7e, 0xcf, 0x25, 0x34, 0xcc, 0x6b,
+ 0x50, 0x74, 0xbd, 0xfd, 0x8f, 0xd8, 0xf1, 0x27, 0x54, 0xf1, 0x53, 0x93,
+ 0xb3, 0x94, 0x12, 0x0b, 0x5b, 0x3e, 0x7e, 0xb2, 0xda, 0xe7, 0x19, 0x32,
+ 0xc4, 0x81, 0xc0, 0x21, 0x17, 0x9b, 0xf3, 0x63, 0x36, 0xdc, 0x57, 0x56,
+ 0x0d, 0x6e, 0x35, 0xe5, 0xdc, 0xe4, 0x4a, 0xe9, 0x4f, 0xd9, 0xc6, 0x6a,
+ 0xff, 0x80, 0xdb, 0xdd, 0x42, 0xe1, 0x99, 0x93, 0x14, 0x1e, 0x60, 0xeb,
+ 0xbf, 0x13, 0x32, 0x4b, 0xf8, 0xa5, 0xc7, 0xee, 0xf4, 0x72, 0x23, 0xa6,
+ 0x63, 0x08, 0x07, 0x69, 0x4d, 0x71, 0x2e, 0x74, 0xc2, 0xd8, 0x52, 0x76,
+ 0x75, 0x5f, 0x66, 0xd7, 0xba, 0x27, 0x74, 0x5d, 0x5e, 0x34, 0x22, 0x97,
+ 0x53, 0x09, 0xdd, 0x35, 0xac, 0x57, 0xd6, 0xdd, 0xbf, 0xe1, 0x61, 0x6e,
+ 0x04, 0xfa, 0x8e, 0x09, 0x03, 0xe8, 0x68, 0xf0, 0x85, 0x1d, 0x49, 0x6a,
+ 0x31, 0xbf, 0xd8, 0x18, 0x88, 0x7b, 0x06, 0xa6, 0xbb, 0x53, 0xd2, 0xab,
+ 0x4b, 0x20, 0x00, 0xe7, 0x6b, 0xc9, 0xd1, 0x2d, 0xbf, 0x21, 0x4d, 0x7a,
+ 0x79, 0xa4, 0x00, 0x41, 0x35, 0x91, 0x76, 0xbb, 0xb3, 0xc6, 0xda, 0xd7,
+ 0x54, 0x53, 0xa5, 0xb0, 0x03, 0x61, 0x33, 0x67, 0xa7, 0x29, 0x76, 0xa5,
+ 0x66, 0x2c, 0xf3, 0x5f, 0xdd, 0x9a, 0xab, 0xb1, 0x9d, 0xaa, 0x37, 0x6b,
+ 0x01, 0x43, 0x40, 0x95, 0x3f, 0xe1, 0xdb, 0xdf, 0x9b, 0x70, 0xfc, 0xd6,
+ 0x4a, 0x45, 0xec, 0x90, 0xa5, 0x77, 0x87, 0xe9, 0xcf, 0xdc, 0x11, 0x39,
+ 0x78, 0x02, 0x42, 0xfd, 0x72, 0xf8, 0x55, 0xf2, 0x13, 0xf7, 0x56, 0x7b,
+ 0x01, 0x70, 0x9a, 0xeb, 0x8b, 0x83, 0x6a, 0x30, 0x11, 0xd2, 0x87, 0x3f,
+ 0x6b, 0x00, 0xf4, 0x73, 0x9b, 0x44, 0xc3, 0xa7, 0xd8, 0xeb, 0xa6, 0xb6,
+ 0x7b, 0x98, 0x1a, 0x11, 0x17, 0x80, 0xb4, 0xb6, 0x85, 0x88, 0x16, 0x7f,
+ 0xb9, 0xb6, 0xae, 0x0a, 0x89, 0x30, 0x38, 0x09, 0x78, 0x39, 0x44, 0x04,
+ 0x85, 0x88, 0x64, 0x42, 0xf1, 0x7c, 0x42, 0x3f, 0xdb, 0x8a, 0x9e, 0x11,
+ 0x56, 0x04, 0x3d, 0x4f, 0x96, 0xb2, 0xe0, 0xa6, 0x03, 0x40, 0x2f, 0x22,
+ 0x10, 0xd5, 0xd6, 0x99, 0xff, 0x12, 0xd6, 0x58, 0x0a, 0xd9, 0x03, 0x6c,
+ 0xce, 0x50, 0xf1, 0x13, 0xdb, 0xb9, 0xd5, 0x9b, 0xae, 0xeb, 0xc2, 0x97,
+ 0x3d, 0xd1, 0x50, 0xa4, 0xd4, 0xa3, 0xf2, 0x9b, 0x47, 0xf2, 0xf0, 0x87,
+ 0xb3, 0x36, 0x10, 0xc1, 0x7f, 0x02, 0x19, 0x4f, 0xc4, 0xb6, 0x2a, 0x8b,
+ 0x9b, 0x2c, 0xe2, 0x66, 0x58, 0xc3, 0x1f, 0x16, 0xaf, 0x78, 0x33, 0xf9,
+ 0x9a, 0xe4, 0x15, 0x05, 0x12, 0x86, 0x9a, 0xce, 0xb6, 0x47, 0x7b, 0x0e,
+ 0x52, 0xde, 0xd3, 0xad, 0x47, 0xc9, 0x1d, 0xd3, 0x76, 0xde, 0x32, 0x38,
+ 0xca, 0x08, 0x08, 0xc7, 0xc1, 0xf0, 0x98, 0x86, 0x8c, 0xfb, 0xf4, 0xf2,
+ 0x6b, 0xfa, 0x60, 0x6b, 0xd0, 0xaf, 0xe0, 0x32, 0x1f, 0x7f, 0x0e, 0x60,
+ 0x08, 0x34, 0x05, 0xf8, 0x4f, 0xab, 0x9c, 0xb9, 0xa3, 0xb0, 0x6a, 0xda,
+ 0xb6, 0x0b, 0x15, 0x35, 0x21, 0x31, 0xbf, 0x8d, 0x79, 0x55, 0x29, 0xd7,
+ 0x25, 0x1b, 0xe8, 0x62, 0x32, 0xd5, 0x35, 0x7a, 0x7a, 0x25, 0x2e, 0x4b,
+ 0xb5, 0xa1, 0xe7, 0x7e, 0xbe, 0xa3, 0xcb, 0x68, 0x1f, 0x44, 0xc6, 0x42,
+ 0xf2, 0x04, 0xa7, 0xc2, 0x55, 0xef, 0x80, 0xe5, 0x8e, 0x28, 0x21, 0xd1,
+ 0x01, 0xe3, 0x8c, 0x43, 0xef, 0xec, 0xbe, 0x60, 0x45, 0xcf, 0x98, 0x23,
+ 0x99, 0x7e, 0x7b, 0x68, 0x5d, 0xc1, 0x1b, 0x6a, 0x12, 0xcc, 0xd6, 0xbe,
+ 0x70, 0x68, 0x5c, 0x86, 0xcc, 0xb9, 0x35, 0x37, 0xdd, 0x75, 0xff, 0xfb,
+ 0x72, 0xd0, 0x6a, 0xd2, 0x81, 0x96, 0x74, 0x2e, 0x66, 0x75, 0x64, 0x6f,
+ 0xf3, 0x8c, 0x7b, 0xad, 0xbc, 0x1c, 0xba, 0x60, 0x68, 0x3c, 0x1c, 0x69,
+ 0xe2, 0x73, 0x2a, 0xcb, 0xe0, 0x44, 0xfc, 0xb0, 0x8d, 0xab, 0x3a, 0x06,
+ 0x49, 0x22, 0x3c, 0x86, 0xad, 0x0f, 0x17, 0x08, 0x44, 0x94, 0x36, 0x50,
+ 0x5b, 0x4a, 0xe5, 0xe2, 0x36, 0xf5, 0x23, 0xb2, 0x1a, 0x71, 0x54, 0x08,
+ 0x70, 0x0a, 0x60, 0x2c, 0xec, 0xb0, 0xaf, 0x95, 0xbf, 0xd0, 0x6a, 0x19,
+ 0x91, 0x53, 0x13, 0xad, 0x3a, 0x14, 0xe2, 0x22, 0x71, 0xd7, 0x86, 0x10,
+ 0x97, 0x88, 0x29, 0xce, 0x18, 0x9f, 0x20, 0xf8, 0x2e, 0x80, 0x19, 0xac,
+ 0xdd, 0x87, 0x6d, 0xb9, 0x6a, 0x63, 0x9e, 0x30, 0xd5, 0x44, 0xc0, 0xd7,
+ 0xf8, 0x0e, 0x52, 0xab, 0x7a, 0x71, 0xff, 0x63, 0x23, 0x27, 0x43, 0x97,
+ 0x8d, 0x96, 0xda, 0xaa, 0x0c, 0x14, 0x5a, 0xb9, 0xb4, 0x0f, 0xb8, 0x56,
+ 0xb1, 0x49, 0x90, 0xaa, 0x3a, 0x63, 0xa8, 0xe2, 0x17, 0x96, 0x10, 0x58,
+ 0x72, 0xa7, 0xd7, 0x9f, 0x0f, 0xff, 0x99, 0x3d, 0x60, 0x8f, 0xd1, 0xa3,
+ 0x94, 0x37, 0xb1, 0x67, 0xf2, 0x6f, 0x97, 0x95, 0x8c, 0x88, 0x40, 0x57,
+ 0x8c, 0x55, 0x99, 0xe2, 0xcf, 0x15, 0x40, 0x24, 0x1e, 0xe4, 0x9d, 0xff,
+ 0x6f, 0xc9, 0xd8, 0xb1, 0x48, 0xb1, 0x1f, 0xc8, 0xaa, 0xa2, 0x14, 0xfb,
+ 0x7d, 0x13, 0xd8, 0xc7, 0xe4, 0x97, 0xe5, 0xd3, 0xc3, 0x8b, 0xae, 0x1c,
+ 0x8d, 0x21, 0x61, 0x25, 0x78, 0x3f, 0x8d, 0x37, 0x81, 0x73, 0x6f, 0x6e,
+ 0xa0, 0xbf, 0xbc, 0xda, 0xc1, 0x21, 0x7b, 0x69, 0x7e, 0x06, 0xc6, 0x44,
+ 0x1b, 0x0b, 0x52, 0xc8, 0xe8, 0xfb, 0x35, 0x32, 0xa6, 0x98, 0x81, 0xe6,
+ 0x65, 0xba, 0x8c, 0x98, 0x79, 0x68, 0xac, 0xe6, 0x75, 0xb4, 0x58, 0x9b,
+ 0xec, 0xa5, 0x1c, 0x0f, 0x35, 0xc5, 0x1e, 0x65, 0xb4, 0xe9, 0x14, 0x22,
+ 0xaf, 0x91, 0x15, 0xeb, 0x8d, 0x57, 0x54, 0xb3, 0x87, 0x61, 0xe9, 0xf7,
+ 0x65, 0x23, 0x95, 0x68, 0x83, 0xf2, 0x6f, 0xb3, 0x65, 0x1d, 0xc2, 0x50,
+ 0xde, 0xd1, 0x65, 0xcb, 0x5b, 0xbc, 0xfe, 0x44, 0xc1, 0x08, 0xc5, 0x75,
+ 0x0c, 0xdc, 0x59, 0xe2, 0xfe, 0x46, 0x25, 0xfc, 0x77, 0xaf, 0xc3, 0xee,
+ 0x1a, 0xc8, 0x41, 0xb5, 0x30, 0xc5, 0xf9, 0xd1, 0x06, 0x0f, 0x92, 0x89,
+ 0x93, 0x0a, 0x84, 0xa3, 0x60, 0xf5, 0x09, 0xb0, 0x42, 0x1d, 0x62, 0x33,
+ 0xaf, 0xb4, 0xbd, 0xe9, 0x99, 0x16, 0x72, 0x33, 0x18, 0x13, 0x98, 0xd3,
+ 0x0e, 0x24, 0xb3, 0x10, 0xe9, 0x43, 0xb9, 0x89, 0xac, 0x2f, 0x6f, 0x42,
+ 0xc3, 0xc5, 0x06, 0xa1, 0xbc, 0xc5, 0x0d, 0x7a, 0xdb, 0xcf, 0x15, 0x89,
+ 0xc6, 0x48, 0x95, 0x79, 0x42, 0xe2, 0xa8, 0xca, 0x5b, 0xb9, 0x51, 0x79,
+ 0x67, 0xdf, 0x5d, 0x90, 0xc0, 0x95, 0x32, 0x4f, 0xc1, 0x50, 0x79, 0x7c,
+ 0xae, 0x4f, 0x59, 0x6a, 0x1a, 0xf9, 0x3b, 0x0a, 0x87, 0xb8, 0xab, 0x1f,
+ 0x92, 0xe4, 0x0e, 0x72, 0x38, 0x6a, 0x10, 0x5a, 0x24, 0x2b, 0x30, 0x75,
+ 0xcd, 0x29, 0xf8, 0x12, 0x95, 0xdf, 0x11, 0x6c, 0x33, 0x9a, 0x02, 0x5e,
+ 0xf9, 0x6c, 0x4d, 0x52, 0xb3, 0x77, 0xe1, 0x9c, 0xac, 0xb8, 0x38, 0x00,
+ 0x2b, 0x16, 0xb1, 0xeb, 0x1d, 0xe9, 0x57, 0xcb, 0x6e, 0xdf, 0x57, 0xdf,
+ 0xd8, 0x6c, 0xa9, 0x56, 0xe3, 0x8d, 0xff, 0xa1, 0xb8, 0x6a, 0x3d, 0xad,
+ 0xb5, 0x6c, 0x42, 0xb6, 0x1f, 0x18, 0x22, 0xd3, 0xcb, 0xf7, 0x32, 0x8d,
+ 0x94, 0x95, 0x15, 0x82, 0x94, 0x9b, 0xee, 0xcd, 0x35, 0xf0, 0x00, 0x21,
+ 0x95, 0x13, 0xf6, 0x17, 0x23, 0x12, 0xf7, 0xd2, 0xbc, 0x71, 0xbe, 0x9d,
+ 0x24, 0xef, 0x54, 0x17, 0xfc, 0xcd, 0x02, 0x13, 0x0d, 0xcf, 0x74, 0x64,
+ 0xa4, 0xc4, 0x8d, 0xdb, 0x1c, 0xd2, 0x56, 0x86, 0x64, 0xf3, 0x26, 0xc2,
+ 0x57, 0x94, 0x50, 0x3c, 0x3d, 0x78, 0x36, 0x2d, 0x2e, 0xb4, 0x69, 0xc9,
+ 0xb7, 0xf8, 0xfc, 0x4e, 0xf3, 0xfd, 0xde, 0x23, 0x3a, 0xe3, 0x0c, 0x3c,
+ 0xf1, 0xe3, 0xeb, 0xb8, 0xaa, 0x0c, 0x32, 0xed, 0x9a, 0x88, 0xa9, 0x11,
+ 0xd5, 0x06, 0xc4, 0x9c, 0x0f, 0x56, 0x2e, 0xa6, 0xe7, 0x42, 0xf0, 0x5d,
+ 0xa0, 0xc4, 0xeb, 0xd7, 0x70, 0x43, 0x39, 0x8b, 0x18, 0x7a, 0xab, 0x83,
+ 0xff, 0x00, 0xc1, 0x7b, 0xc3, 0xe9, 0xff, 0x07, 0x6e, 0xf9, 0x21, 0x0d,
+ 0x9c, 0xad, 0xb6, 0x22, 0xd8, 0x88, 0x91, 0x49, 0x92, 0x6c, 0xa2, 0x7f,
+ 0x61, 0x37, 0x9f, 0xc7, 0xae, 0x6e, 0x15, 0xd7, 0x8f, 0x0d, 0xd3, 0x81,
+ 0x2a, 0xd2, 0xba, 0xe2, 0xdf, 0x85, 0x30, 0x54, 0x09, 0x8c, 0xe8, 0x7e,
+ 0x94, 0xbb, 0x78, 0x06, 0x33, 0x17, 0x2c, 0x67, 0x33, 0x10, 0xa8, 0x8a,
+ 0x3f, 0x67, 0x30, 0x62, 0xc8, 0xf5, 0xa3, 0x39, 0xbb, 0xd4, 0x9d, 0x6f,
+ 0x07, 0x1d, 0xae, 0x59, 0x2b, 0xb7, 0x94, 0x87, 0x04, 0x7f, 0x57, 0x25,
+ 0x17, 0x42, 0x17, 0xd8, 0x9d, 0x5f, 0x5d, 0xee, 0x41, 0x08, 0x31, 0xf1,
+ 0x42, 0x5c, 0xc7, 0x32, 0xd8, 0xc1, 0xc4, 0x3a, 0xff, 0x04, 0xa9, 0x15,
+ 0xbf, 0x8f, 0x17, 0xe1, 0x20, 0xad, 0x8f, 0xb9, 0xd6, 0x3a, 0x18, 0x86,
+ 0xc1, 0x20, 0x29, 0xca, 0xb4, 0xec, 0x55, 0xc5, 0xbd, 0x23, 0xd5, 0x84,
+ 0x52, 0xc6, 0xe8, 0xbe, 0x80, 0x8b, 0x07, 0x0d, 0xb4, 0xc0, 0xfd, 0xb4,
+ 0xbd, 0xb2, 0x76, 0x6e, 0x4a, 0xd9, 0xe6, 0x2e, 0xe2, 0x52, 0xfe, 0xca,
+ 0x02, 0x01, 0x18, 0x41, 0x81, 0xbf, 0xfc, 0x47, 0x8b, 0xf4, 0x1f, 0x54,
+ 0x65, 0xe8, 0xec, 0x8b, 0x8d, 0x6b, 0x73, 0x2b, 0xcf, 0xd5, 0x92, 0xd6,
+ 0xfa, 0x89, 0x2d, 0x67, 0x97, 0x7a, 0xe4, 0x0c, 0x02, 0x08, 0x32, 0x07,
+ 0x3f, 0x6f, 0x48, 0x03, 0xfb, 0x1d, 0xde, 0xd3, 0xe1, 0x42, 0x50, 0xf9,
+ 0xbf, 0xc1, 0x8b, 0x1a, 0x1d, 0x30, 0x6a, 0xd1, 0xd2, 0xb2, 0xf8, 0xd6,
+ 0xfb, 0x27, 0x24, 0x63, 0xfb, 0xf0, 0x74, 0x5a, 0x40, 0xe7, 0x5e, 0xac,
+ 0xc6, 0x5f, 0x44, 0x18, 0x3b, 0x73, 0x92, 0x0f, 0x8d, 0x17, 0x3e, 0xa4,
+ 0x58, 0x19, 0xae, 0xf5, 0xd1, 0x1e, 0x2d, 0xab, 0x0a, 0x77, 0x06, 0xe8,
+ 0x07, 0x35, 0xd7, 0x91, 0xe5, 0x36, 0xa7, 0x90, 0x23, 0x54, 0x44, 0x95,
+ 0xa7, 0xdf, 0x79, 0x05, 0xfb, 0xad, 0xc7, 0xcc, 0x87, 0x4f, 0x7e, 0x50,
+ 0x09, 0xe4, 0x50, 0x12, 0xad, 0x26, 0xd9, 0x44, 0xff, 0xc2, 0xd0, 0x23,
+ 0xe2, 0x08, 0x90, 0x7a, 0xca, 0x30, 0x98, 0x5b, 0xe0, 0x31, 0x06, 0x18,
+ 0x24, 0x82, 0xb5, 0x69, 0xfc, 0xce, 0xa2, 0xbd, 0x3d, 0xe3, 0x44, 0xa9,
+ 0x6e, 0xce, 0x8b, 0x32, 0x97, 0xf8, 0x6f, 0x2d, 0x5d, 0xca, 0x6d, 0x2b,
+ 0xdc, 0x0d, 0xd7, 0xe3, 0x7c, 0x9b, 0xd1, 0xe7, 0xd6, 0x04, 0xa5, 0x6b,
+ 0xf9, 0xa4, 0x33, 0xc9, 0xfe, 0x13, 0x56, 0xaa, 0xd2, 0x6e, 0xfc, 0xee,
+ 0xb7, 0xcd, 0xc1, 0xcc, 0xe0, 0x57, 0xf3, 0x08, 0x7d, 0x99, 0x05, 0xf0,
+ 0xf1, 0x91, 0xb2, 0x2b, 0x91, 0x80, 0x99, 0x63, 0x97, 0x1a, 0x5e, 0x93,
+ 0x89, 0x16, 0xcd, 0x5e, 0xf9, 0xe7, 0x3c, 0x53, 0xbe, 0x94, 0x38, 0x5a,
+ 0x3b, 0xd5, 0x16, 0xe0, 0x74, 0x96, 0x4f, 0x2f, 0x6d, 0xee, 0x9e, 0xd3,
+ 0xbb, 0x58, 0xff, 0x7e, 0xb6, 0x0b, 0x18, 0xe1, 0xf7, 0x04, 0x67, 0x7b,
+ 0x0d, 0x55, 0x57, 0x82, 0xe4, 0x1e, 0x8c, 0x31, 0xf0, 0xe9, 0x15, 0x55,
+ 0xda, 0x43, 0xc4, 0xba, 0x7a, 0x26, 0xf2, 0xe0, 0x05, 0xb1, 0x8e, 0x5f,
+ 0x50, 0x7c, 0x76, 0x00, 0x84, 0xf0, 0x3d, 0x58, 0x3d, 0xe3, 0x58, 0xca,
+ 0x9e, 0xb0, 0x95, 0xa7, 0x92, 0x47, 0xc7, 0xe0, 0x70, 0x3a, 0xd3, 0x27,
+ 0x06, 0xb6, 0x0d, 0x26, 0xc1, 0x87, 0xdf, 0xb3, 0x01, 0xd2, 0xea, 0x77,
+ 0x71, 0x30, 0xdb, 0x9e, 0x3d, 0x0b, 0xed, 0x2d, 0xb5, 0x39, 0x4f, 0x4b,
+ 0x0d, 0x26, 0xeb, 0xff, 0x73, 0x0c, 0xde, 0x01, 0xca, 0xb8, 0x8e, 0x6e,
+ 0xff, 0x0a, 0xda, 0xea, 0x4c, 0x02, 0x3e, 0x5b, 0x0b, 0x79, 0xeb, 0xb2,
+ 0x26, 0xb7, 0x01, 0xfe, 0xcc, 0x2f, 0x44, 0xa1, 0x14, 0x14, 0x57, 0x97,
+ 0xab, 0xc0, 0xac, 0x0b, 0x43, 0x73, 0x67, 0xff, 0xbf, 0x51, 0xae, 0xaa,
+ 0xa0, 0x28, 0xbe, 0xd9, 0xc7, 0x8b, 0xf4, 0x3f, 0xdb, 0x61, 0x01, 0x1a,
+ 0x65, 0x90, 0x05, 0xcb, 0x0c, 0x43, 0xd0, 0x2e, 0xf4, 0xce, 0xcc, 0xe9,
+ 0x88, 0xe8, 0x8b, 0x8a, 0x1c, 0x87, 0x29, 0xc4, 0xe1, 0xcb, 0xaa, 0xfd,
+ 0x70, 0xe5, 0xd4, 0x3f, 0x2a, 0xbf, 0x2f, 0x3a, 0x63, 0x29, 0x0c, 0xcf,
+ 0xe7, 0x61, 0xb3, 0xfc, 0xf4, 0x52, 0x35, 0x2f, 0x09, 0x57, 0x57, 0x78,
+ 0xd0, 0xb6, 0x81, 0xa1, 0x58, 0xfe, 0x65, 0xf9, 0x1d, 0x15, 0x57, 0xbe,
+ 0xbc, 0x95, 0x46, 0xfc, 0x5e, 0x1c, 0x9d, 0x84, 0x0e, 0x4d, 0x11, 0x14,
+ 0x9e, 0x65, 0xd6, 0xc9, 0xb9, 0xe5, 0x87, 0x33, 0x68, 0xa8, 0x5f, 0x82,
+ 0x90, 0xaf, 0x90, 0xe6, 0xbd, 0x62, 0x31, 0xf0, 0xfa, 0x5b, 0xcf, 0x2c,
+ 0x67, 0xe7, 0xf4, 0x05, 0xe3, 0xb9, 0x45, 0x92, 0x16, 0xbf, 0x0f, 0x5a,
+ 0x74, 0xd1, 0xdb, 0x24, 0x16, 0x9e, 0xb9, 0xd8, 0x2c, 0xc2, 0x46, 0x24,
+ 0xbc, 0xaa, 0x1b, 0x9f, 0xd0, 0xad, 0x18, 0x11, 0xb9, 0x9e, 0x8e, 0x46,
+ 0x75, 0xf3, 0x81, 0x37, 0x46, 0x99, 0x43, 0xad, 0x69, 0xf8, 0x75, 0x79,
+ 0xae, 0x2c, 0xeb, 0xb1, 0x77, 0x29, 0x47, 0x15, 0x95, 0x21, 0xa0, 0xf6,
+ 0xec, 0xb9, 0xcc, 0xa0, 0x6a, 0xfd, 0x47, 0x0f, 0x98, 0x5f, 0x12, 0xaf,
+ 0xed, 0x8b, 0x11, 0xe4, 0xcf, 0x93, 0x31, 0x79, 0x50, 0x08, 0xb0, 0x28,
+ 0x91, 0x56, 0xd9, 0x05, 0x16, 0x43, 0xc5, 0x84, 0x50, 0x43, 0x0f, 0x47,
+ 0xc9, 0x3a, 0xb4, 0xef, 0x39, 0xf0, 0xb1, 0xb5, 0x2c, 0xab, 0x3b, 0xbb,
+ 0xed, 0xaf, 0x64, 0x59, 0x5e, 0x6b, 0xa0, 0x0c, 0x6c, 0xf6, 0x4f, 0x69,
+ 0x4f, 0xf1, 0xaa, 0x02, 0x1d, 0x25, 0xa0, 0x39, 0xc8, 0xc7, 0xb4, 0x99,
+ 0xb8, 0x77, 0xf3, 0x10, 0x1a, 0x52, 0x50, 0x3a, 0xcc, 0x14, 0x90, 0x85,
+ 0x75, 0x63, 0xf8, 0x31, 0x14, 0x56, 0x0c, 0xa0, 0xc2, 0x83, 0x1f, 0xa7,
+ 0xcc, 0xc3, 0x69, 0xd8, 0xef, 0x2e, 0x30, 0x13, 0xbb, 0x15, 0xfc, 0x61,
+ 0xbc, 0x01, 0x89, 0x41, 0xbe, 0x04, 0x2f, 0x64, 0x3e, 0xa4, 0x0b, 0xf1,
+ 0x5e, 0x93, 0x7a, 0x85, 0xb0, 0x23, 0xbd, 0xbc, 0x89, 0xc6, 0xb4, 0x27,
+ 0xdd, 0x41, 0x51, 0xde, 0xe2, 0x05, 0x43, 0xf6, 0x91, 0x80, 0xfa, 0xb8,
+ 0xab, 0xc5, 0xf5, 0xc8, 0xca, 0x24, 0xa0, 0xc5, 0x50, 0x86, 0x29, 0x82,
+ 0x84, 0xfd, 0x34, 0x6e, 0xe2, 0x38, 0xd0, 0x86, 0xbe, 0x04, 0xee, 0x50,
+ 0x56, 0x79, 0x9d, 0x5b, 0xcf, 0xc4, 0x77, 0x8c, 0x60, 0xd9, 0x0b, 0xe6,
+ 0xc5, 0x7c, 0xef, 0x7f, 0x5e, 0x55, 0x6d, 0x27, 0xc0, 0x6a, 0xdc, 0xe1,
+ 0x1d, 0x99, 0x87, 0x11, 0xf1, 0x5d, 0xf5, 0xc8, 0x57, 0xd4, 0x30, 0x61,
+ 0xa1, 0x26, 0xd5, 0x59, 0x67, 0x60, 0xbb, 0x22, 0x96, 0xac, 0x97, 0x11,
+ 0xda, 0x21, 0xcf, 0xcf, 0x6c, 0x47, 0xbc, 0x17, 0xd6, 0xda, 0x03, 0x2e,
+ 0x95, 0x2d, 0x88, 0x54, 0xd9, 0xcd, 0x38, 0xb2, 0x08, 0xd7, 0x70, 0x52,
+ 0x0c, 0xed, 0x83, 0x23, 0x0f, 0xfc, 0x9a, 0x61, 0xca, 0x33, 0x05, 0x62,
+ 0x41, 0xde, 0x5c, 0xd2, 0xf8, 0x0a, 0xf3, 0xef, 0x2d, 0x2d, 0xc1, 0xb5,
+ 0x90, 0x01, 0x68, 0x02, 0x69, 0xdb, 0xe8, 0x24, 0xe4, 0x08, 0xa9, 0x17,
+ 0x04, 0x13, 0xab, 0xdc, 0xa7, 0x9e, 0x6c, 0x74, 0x76, 0x18, 0xe2, 0x55,
+ 0x05, 0xb6, 0xef, 0x3d, 0x19, 0xc7, 0x64, 0x63, 0xdf, 0x81, 0xc0, 0x97,
+ 0x4d, 0x78, 0x70, 0xc5, 0xcd, 0x8a, 0xa8, 0x88, 0xf4, 0xd2, 0xb0, 0x4e,
+ 0xfd, 0xe8, 0x79, 0x75, 0x70, 0x1f, 0x1c, 0xf9, 0x6e, 0xb4, 0xbe, 0xae,
+ 0x91, 0x65, 0xb4, 0x68, 0xcb, 0xa0, 0x7a, 0x3e, 0x5e, 0x5f, 0x02, 0x8f,
+ 0x52, 0x00, 0x4b, 0x49, 0x5d, 0x57, 0x9a, 0x37, 0x8d, 0x0a, 0x35, 0xbd,
+ 0x62, 0x4b, 0x26, 0x13, 0xf8, 0xe5, 0xec, 0xcd, 0x80, 0x47, 0x31, 0x1a,
+ 0x94, 0x5f, 0x79, 0x11, 0x81, 0x83, 0x41, 0xcf, 0x48, 0x5b, 0x52, 0xb2,
+ 0x84, 0xfd, 0x9c, 0x40, 0x92, 0xee, 0xb4, 0xad, 0x20, 0x3b, 0xfb, 0x61,
+ 0x0c, 0xf6, 0x39, 0x73, 0xfc, 0xf9, 0x97, 0x05, 0x41, 0x48, 0x97, 0xea,
+ 0x3d, 0x49, 0x15, 0x14, 0xad, 0xf4, 0x09, 0xc7, 0x1d, 0x1e, 0xe8, 0x39,
+ 0x8c, 0xc2, 0x0a, 0x50, 0x94, 0xd0, 0x9c, 0x40, 0x4d, 0x9b, 0xe7, 0x70,
+ 0x33, 0xbc, 0xd9, 0xf1, 0x37, 0xcb, 0xe1, 0xaf, 0x71, 0x9b, 0xa0, 0xa0,
+ 0xc7, 0x0d, 0x0b, 0xeb, 0x62, 0x92, 0x35, 0xb8, 0xaf, 0xa9, 0x36, 0x38,
+ 0x4a, 0x96, 0x01, 0xb0, 0x23, 0x8c, 0x0f, 0x4f, 0x61, 0xdf, 0xb1, 0x7d,
+ 0xaa, 0xc4, 0xe8, 0x6e, 0xde, 0x5b, 0x76, 0x0e, 0x36, 0xf0, 0x95, 0x1b,
+ 0x66, 0xcd, 0xcc, 0xe1, 0xd4, 0xe5, 0x59, 0x2b, 0xe6, 0x42, 0x20, 0x75,
+ 0xc0, 0x7b, 0xdd, 0x26, 0xb8, 0x73, 0xbb, 0x13, 0x55, 0x01, 0xd5, 0xd3,
+ 0xf2, 0x3d, 0x7c, 0x17, 0x43, 0x8f, 0x11, 0x8f, 0xdf, 0x60, 0x3a, 0xd2,
+ 0x96, 0x05, 0xf6, 0x55, 0x95, 0x5b, 0x15, 0x8f, 0xbe, 0x32, 0x83, 0xb3,
+ 0xd6, 0xbf, 0xc9, 0x04, 0x1d, 0x8f, 0x81, 0xab, 0x53, 0x81, 0x28, 0x7d,
+ 0x05, 0x33, 0xb5, 0xd7, 0xf2, 0x4e, 0x74, 0x0e, 0xfc, 0x81, 0xdf, 0x30,
+ 0x0e, 0x34, 0x04, 0x7c, 0x79, 0xd5, 0x4a, 0x5c, 0x52, 0xe8, 0xe0, 0x1a,
+ 0x6b, 0x17, 0x8d, 0x3a, 0xc4, 0x5f, 0xb5, 0x8f, 0x33, 0x65, 0xf7, 0xed,
+ 0xd2, 0x31, 0xab, 0x15, 0xa1, 0x2f, 0xe4, 0x9a, 0xb4, 0xd4, 0x92, 0x58,
+ 0x54, 0xc2, 0x97, 0x20, 0x22, 0x1c, 0x70, 0x29, 0x2c, 0xfe, 0xb0, 0x08,
+ 0x57, 0x98, 0x5c, 0x4d, 0x54, 0x9c, 0x32, 0x49, 0x31, 0x2b, 0xc5, 0x17,
+ 0x17, 0x1c, 0x76, 0x73, 0x5b, 0x26, 0xe3, 0xef, 0xa9, 0x28, 0x20, 0x90,
+ 0x33, 0x49, 0x3c, 0xc0, 0x03, 0x7a, 0xf5, 0x3e, 0xd2, 0x7d, 0xbd, 0x35,
+ 0x06, 0x78, 0x89, 0xb4, 0x6c, 0x6c, 0x3f, 0xeb, 0xac, 0x34, 0xaf, 0x9e,
+ 0xfc, 0x19, 0xbe, 0x37, 0x9c, 0x54, 0x6d, 0x81, 0xdb, 0xa3, 0x4c, 0x22,
+ 0x46, 0x78, 0x2d, 0x62, 0x61, 0xc5, 0xed, 0xe9, 0xe8, 0xe4, 0x71, 0xe8,
+ 0xf8, 0x23, 0x15, 0x4a, 0x4f, 0x0e, 0xaa, 0x56, 0xad, 0x44, 0xde, 0x43,
+ 0x19, 0xcb, 0xaf, 0x89, 0x52, 0x82, 0x58, 0xc4, 0xac, 0xa0, 0x8e, 0x9d,
+ 0x20, 0x99, 0xf2, 0xd2, 0xc3, 0x2e, 0xb7, 0x04, 0x94, 0x6f, 0xc1, 0x1e,
+ 0x4e, 0x09, 0x90, 0x36, 0xb2, 0x37, 0x6d, 0x58, 0x09, 0x72, 0xcf, 0xac,
+ 0xcb, 0xb8, 0xdd, 0x07, 0x07, 0x8b, 0x99, 0x90, 0x1a, 0xb9, 0xf4, 0x5b,
+ 0xd6, 0xe4, 0x51, 0x0a, 0x49, 0x5d, 0x34, 0x3d, 0xe4, 0xe3, 0x4a, 0xb7,
+ 0xc7, 0xe1, 0x1c, 0x47, 0xc6, 0x83, 0x0a, 0x6f, 0xb4, 0x1e, 0x62, 0xb4,
+ 0x3e, 0x73, 0xa7, 0xc4, 0xc6, 0x36, 0x86, 0x95, 0xf3, 0xe8, 0x59, 0xab,
+ 0xe3, 0x2a, 0x1e, 0xb5, 0xce, 0x85, 0xe5, 0x26, 0x54, 0x52, 0x32, 0x28,
+ 0xe7, 0x00, 0x4d, 0xdd, 0x1c, 0x66, 0x0f, 0x0d, 0xcf, 0x9e, 0xdb, 0xfa,
+ 0xb7, 0x62, 0xa4, 0x81, 0x24, 0x58, 0xe5, 0xd8, 0xa4, 0xdb, 0x29, 0x61,
+ 0xbe, 0x15, 0x93, 0x83, 0x56, 0x11, 0x56, 0xfa, 0x6a, 0xb4, 0xb4, 0x58,
+ 0x65, 0x9f, 0x52, 0x1e, 0xdb, 0x74, 0x14, 0x0d, 0x72, 0x4e, 0xc7, 0x85,
+ 0x97, 0x51, 0x36, 0xd8, 0x37, 0x09, 0xb8, 0xd2, 0x4f, 0xb3, 0x77, 0x56,
+ 0x13, 0xe3, 0x76, 0xcd, 0x24, 0x45, 0x8d, 0xc8, 0x20, 0xa4, 0x1e, 0x93,
+ 0xd7, 0xa0, 0x8a, 0x21, 0x5a, 0xa1, 0x6b, 0x01, 0xf1, 0x94, 0xb4, 0xe0,
+ 0x33, 0xc5, 0xbe, 0x18, 0xd4, 0xdc, 0xea, 0xd1, 0xbc, 0xe9, 0x58, 0x73,
+ 0x21, 0x58, 0x28, 0x4e, 0xb6, 0x02, 0x6a, 0xca, 0x70, 0x11, 0x78, 0x35,
+ 0xc8, 0x78, 0xf5, 0x4d, 0xb8, 0xda, 0x51, 0x3e, 0x6c, 0xd5, 0x46, 0x31,
+ 0x5d, 0x69, 0xfb, 0xc2, 0x6f, 0x2f, 0xae, 0xa9, 0x79, 0x6c, 0x0b, 0xfd,
+ 0x6b, 0xed, 0xe0, 0x35, 0xac, 0xc9, 0x11, 0x5f, 0x06, 0x90, 0x80, 0x59,
+ 0x47, 0xcb, 0xd8, 0x21, 0xed, 0x08, 0x46, 0x87, 0xd4, 0x4b, 0xf1, 0x15,
+ 0x74, 0x32, 0x72, 0x66, 0xde, 0x6a, 0xb9, 0xf1, 0x8d, 0x27, 0xdc, 0x59,
+ 0x44, 0xa3, 0x11, 0x32, 0xd8, 0x57, 0xb6, 0x9e, 0xe7, 0x09, 0xaa, 0x58,
+ 0xa6, 0x87, 0x1e, 0xb9, 0xc8, 0xcd, 0xde, 0xa9, 0xbe, 0x53, 0x6d, 0x42,
+ 0xaa, 0x87, 0x74, 0x96, 0x50, 0xaf, 0xdd, 0xf1, 0xe2, 0xcf, 0xa3, 0x99,
+ 0xbd, 0x6f, 0xb8, 0x48, 0xcc, 0x80, 0x73, 0xa8, 0xbb, 0x69, 0x72, 0xa6,
+ 0x41, 0x16, 0xad, 0xc3, 0xa6, 0x8c, 0x23, 0x2a, 0xf0, 0x02, 0xfc, 0x56,
+ 0x1a, 0x41, 0xd3, 0xe9, 0xcf, 0x08, 0x05, 0xb2, 0xd8, 0x56, 0xb7, 0x7a,
+ 0x19, 0xa1, 0x89, 0x1e, 0x31, 0x03, 0x50, 0x4d, 0x19, 0x4e, 0x64, 0x3e,
+ 0xd5, 0xfb, 0x16, 0xfd, 0x91, 0x33, 0x83, 0x67, 0x04, 0xc9, 0xea, 0x98,
+ 0x17, 0xe2, 0x9d, 0x2a, 0x82, 0x7b, 0x2c, 0xb8, 0xbf, 0x35, 0x05, 0xa8,
+ 0x1b, 0x66, 0xed, 0x7c, 0x12, 0x42, 0x1e, 0xee, 0x1b, 0xa9, 0xa4, 0xab,
+ 0x25, 0x8c, 0x01, 0x8e, 0x5e, 0x7b, 0x77, 0x40, 0x7e, 0x49, 0xbf, 0x2a,
+ 0x19, 0x1e, 0x6b, 0xcc, 0x34, 0x45, 0x4f, 0xa8, 0x22, 0xf5, 0x14, 0x68,
+ 0x87, 0x53, 0x7e, 0x5f, 0xa1, 0x46, 0x9d, 0xfe, 0x9d, 0x44, 0xaa, 0x52,
+ 0xc3, 0xab, 0xf2, 0xc0, 0x5d, 0xfe, 0x88, 0xe2, 0x99, 0x1f, 0xf2, 0xd9,
+ 0xa5, 0x75, 0x40, 0xfa, 0x99, 0xbe, 0x9d, 0xc2, 0xfd, 0x0a, 0xd4, 0x7b,
+ 0xdd, 0x17, 0xed, 0x2a, 0x75, 0xc7, 0x4c, 0xd4, 0x3a, 0x76, 0xb3, 0x72,
+ 0xa5, 0x48, 0x8f, 0x10, 0xf3, 0x2b, 0x1f, 0x2f, 0xa5, 0x3c, 0xe8, 0x8b,
+ 0x39, 0x5d, 0x30, 0x7d, 0x49, 0xfb, 0x4b, 0x20, 0x63, 0x98, 0xb8, 0x59,
+ 0x53, 0xc8, 0x3c, 0x59, 0xc6, 0x63, 0xb6, 0xe4, 0x70, 0x6b, 0xbc, 0x1b,
+ 0x0e, 0x0b, 0x29, 0x69, 0xc7, 0xc4, 0xbf, 0xfa, 0x58, 0x29, 0xa5, 0x19,
+ 0x3c, 0x55, 0x78, 0xfc, 0xcb, 0xf1, 0x8a, 0xab, 0x7c, 0xa0, 0x25, 0x48,
+ 0x5d, 0x4b, 0x5b, 0x49, 0xca, 0xa7, 0xf8, 0x23, 0x68, 0x84, 0x1f, 0xd2,
+ 0x6b, 0xdf, 0x5c, 0xcc, 0x8d, 0x4d, 0x97, 0xa5, 0xc7, 0x7e, 0xa4, 0xda,
+ 0x26, 0xbe, 0xfb, 0x9c, 0x8d, 0xaa, 0x4d, 0xb0, 0x38, 0x05, 0x08, 0x68,
+ 0xf4, 0x43, 0x28, 0x44, 0xf8, 0x65, 0x54, 0xc7, 0x50, 0xe7, 0x9d, 0x58,
+ 0x15, 0x01, 0x38, 0x55, 0xfb, 0xc5, 0x4a, 0x42, 0xa8, 0x24, 0xac, 0x84,
+ 0x9b, 0xb3, 0x52, 0x74, 0xf0, 0xdb, 0xdc, 0x37, 0x32, 0x79, 0x54, 0xf2,
+ 0x68, 0x1b, 0x0c, 0x3a, 0x96, 0x20, 0xcc, 0x8d, 0x05, 0xc3, 0xe5, 0x2b,
+ 0x2f, 0xe6, 0xb8, 0xfc, 0x39, 0xa4, 0xc0, 0xd6, 0x91, 0xc0, 0x88, 0xf6,
+ 0xcb, 0x11, 0x67, 0x11, 0xcf, 0x0f, 0x1d, 0xd9, 0xea, 0x84, 0xa4, 0xe3,
+ 0xa6, 0x34, 0x41, 0x7a, 0x9d, 0x37, 0x38, 0x13, 0xe6, 0xbd, 0xba, 0xa7,
+ 0xfd, 0x13, 0x43, 0xb4, 0x00, 0xab, 0xe2, 0x14, 0xd4, 0x62, 0x48, 0x3d,
+ 0x73, 0x0a, 0x26, 0xcb, 0xf5, 0x75, 0xd0, 0x3c, 0x00, 0xb1, 0x5a, 0xc9,
+ 0xa3, 0xe1, 0xd6, 0x2e, 0x06, 0xc3, 0xda, 0x4c, 0x72, 0xd7, 0x6f, 0x98,
+ 0x89, 0x7d, 0x6f, 0xb8, 0xe7, 0x52, 0x3c, 0xc1, 0x64, 0x87, 0x47, 0x18,
+ 0x8c, 0xe7, 0x8e, 0x5e, 0x01, 0xf5, 0x9b, 0x35, 0x82, 0x33, 0xb7, 0xcb,
+ 0x9c, 0x99, 0x6a, 0x38, 0x9d, 0x2d, 0xbb, 0x5e, 0x7e, 0x0d, 0xdc, 0x3a,
+ 0xda, 0xc0, 0xba, 0x87, 0x07, 0xda, 0x94, 0x75, 0xa4, 0xf9, 0xed, 0xb9,
+ 0xe5, 0xc7, 0xdb, 0xe8, 0x2c, 0xac, 0x4c, 0x65, 0xc4, 0x9f, 0xd9, 0x7b,
+ 0xb9, 0x8f, 0x3a, 0xd5, 0x0c, 0x14, 0x40, 0x19, 0x54, 0x21, 0xad, 0xc0,
+ 0x55, 0x68, 0x99, 0xbe, 0xf4, 0xab, 0x89, 0xdc, 0x11, 0x06, 0xb9, 0x59,
+ 0xfc, 0x4a, 0xda, 0x72, 0xe4, 0x27, 0xf5, 0x26, 0xb9, 0xb6, 0xfe, 0x87,
+ 0x6e, 0x3f, 0xaf, 0x63, 0xcc, 0xd7, 0x1e, 0x95, 0x8e, 0x2b, 0xb5, 0xba,
+ 0x0a, 0x54, 0x37, 0x91, 0xf9, 0x7b, 0xc4, 0x4e, 0xe2, 0xe6, 0x11, 0x1e,
+ 0xb4, 0x9f, 0xcd, 0x45, 0xf3, 0x53, 0x71, 0x3b, 0xbc, 0xfa, 0x45, 0x2c,
+ 0x0b, 0x9d, 0x62, 0x7d, 0x73, 0x5a, 0xb0, 0x26, 0x86, 0x30, 0x54, 0x33,
+ 0x72, 0xc9, 0xae, 0x1b, 0xfe, 0xbe, 0x36, 0x24, 0xd0, 0xcc, 0xcd, 0xd8,
+ 0xd4, 0x66, 0x49, 0x8f, 0xdd, 0x4e, 0xb2, 0xa9, 0x66, 0x6e, 0x78, 0xcc,
+ 0xb5, 0x3e, 0xf3, 0xba, 0xe9, 0x8c, 0x38, 0x3e, 0xc1, 0xdd, 0xef, 0x5c,
+ 0x89, 0xfb, 0x5e, 0xca, 0x12, 0xfd, 0x19, 0x80, 0x73, 0x56, 0x39, 0x23,
+ 0x19, 0x2a, 0x3e, 0xae, 0xb5, 0xc0, 0xf0, 0x81, 0x88, 0x0f, 0x60, 0xae,
+ 0x16, 0xa6, 0x3e, 0x5c, 0xbb, 0xda, 0x7d, 0x9c, 0x38, 0xdd, 0x97, 0xd7,
+ 0x31, 0x5f, 0x0b, 0x50, 0x45, 0x6f, 0xc5, 0x96, 0x49, 0x8e, 0x5f, 0x47,
+ 0xe4, 0xaa, 0xeb, 0x7d, 0x58, 0x7a, 0x51, 0xfc, 0xc2, 0x81, 0x5b, 0xc1,
+ 0x8d, 0xc2, 0xf1, 0xd5, 0xbb, 0x8a, 0x1e, 0xe9, 0xd8, 0x9c, 0x45, 0x51,
+ 0x0a, 0x8c, 0x42, 0xff, 0xcd, 0xce, 0x09, 0xf7, 0x89, 0x45, 0xee, 0x3b,
+ 0x3b, 0x51, 0x4f, 0xdc, 0xf1, 0x16, 0x4b, 0x8f, 0xa1, 0x78, 0xcb, 0x9a,
+ 0x44, 0xbe, 0xde, 0x09, 0x77, 0x2d, 0xdb, 0x68, 0x04, 0x9d, 0x9e, 0x41,
+ 0x7a, 0x1e, 0x8d, 0xf0, 0x09, 0x46, 0xb7, 0xd2, 0x6a, 0xc8, 0x8f, 0x8b,
+ 0x2d, 0xb0, 0x81, 0x96, 0xda, 0xbe, 0x70, 0xf1, 0x19, 0xfc, 0x53, 0x45,
+ 0xdb, 0x25, 0x87, 0x66, 0x5c, 0x1f, 0x49, 0xec, 0x0f, 0x5a, 0xb3, 0xab,
+ 0xb0, 0x0c, 0x43, 0x98, 0xcc, 0x38, 0xac, 0x53, 0xbf, 0x2e, 0x2f, 0x1f,
+ 0x66, 0x63, 0x8b, 0xfe, 0xc5, 0x82, 0x65, 0xd5, 0xcb, 0xdf, 0xc1, 0xa2,
+ 0x23, 0x2f, 0xd3, 0x37, 0x05, 0x7f, 0x16, 0xdb, 0xfc, 0x1f, 0x1d, 0xea,
+ 0xf9, 0x5f, 0x06, 0x86, 0x35, 0xff, 0x0f, 0xf6, 0x68, 0x60, 0xd9, 0x29,
+ 0xc9, 0x72, 0xc0, 0xe9, 0x87, 0x24, 0xe2, 0x97, 0x5a, 0x80, 0x0a, 0x09,
+ 0x51, 0xe0, 0x65, 0x9a, 0x25, 0x6a, 0xd4, 0x79, 0x9c, 0xb4, 0xe4, 0xa6,
+ 0xef, 0x5c, 0x33, 0xdf, 0xc1, 0xd9, 0xb1, 0x6e, 0x6d, 0x34, 0x1d, 0x16,
+ 0x3b, 0xc2, 0x4e, 0x5c, 0x3a, 0xbe, 0x1b, 0x05, 0x7e, 0x22, 0x05, 0x0e,
+ 0x8b, 0x76, 0x39, 0xc5, 0xa7, 0x0f, 0xe2, 0x7d, 0x11, 0x81, 0x7e, 0x32,
+ 0x1c, 0xcb, 0xf3, 0xe3, 0xbc, 0x6e, 0x48, 0x5c, 0xa5, 0xbf, 0x95, 0x18,
+ 0xb5, 0x38, 0x63, 0xdd, 0xca, 0xa1, 0xaf, 0x7c, 0x4c, 0x97, 0xff, 0x71,
+ 0xe6, 0x06, 0x21, 0xec, 0x11, 0x88, 0xf5, 0x88, 0x47, 0xcf, 0x94, 0x7f,
+ 0x45, 0xd1, 0x08, 0x7b, 0xfd, 0xf5, 0xef, 0xf0, 0x32, 0x15, 0x91, 0xa2,
+ 0x3e, 0xbc, 0xca, 0x41, 0x39, 0xe7, 0xed, 0x91, 0x67, 0x49, 0x97, 0x38,
+ 0x4e, 0x63, 0xe9, 0xa0, 0x27, 0x2f, 0xe0, 0x39, 0xbb, 0x8d, 0x2c, 0xb1,
+ 0x18, 0x20, 0x63, 0x7e, 0x8a, 0x34, 0x5a, 0x40, 0x58, 0x80, 0xab, 0x01,
+ 0x56, 0xab, 0x01, 0x52, 0xf8, 0xbf, 0x21, 0x88, 0xe6, 0xec, 0x7f, 0xef,
+ 0x2d, 0xda, 0x7f, 0x5e, 0x0f, 0xa6, 0x43, 0xc6, 0x47, 0xf6, 0xcb, 0x7a,
+ 0xcc, 0xb6, 0xbb, 0x8a, 0x0b, 0x1c, 0x33, 0x85, 0xe6, 0xa3, 0xad, 0x35,
+ 0x7a, 0xb7, 0x74, 0x75, 0xfc, 0xe6, 0xa0, 0x81, 0x18, 0x22, 0xbf, 0x25,
+ 0xe8, 0xc7, 0xd4, 0xc1, 0x57, 0x30, 0x32, 0x51, 0x82, 0x37, 0x34, 0x95,
+ 0xdf, 0x3a, 0x96, 0xc1, 0xa6, 0xdc, 0x63, 0x18, 0x84, 0xd6, 0xd0, 0xb9,
+ 0xa0, 0x52, 0x6b, 0x7b, 0x39, 0x8b, 0x1e, 0x4d, 0xdc, 0xa1, 0xaf, 0x4e,
+ 0x42, 0xba, 0xa5, 0x3b, 0x41, 0x02, 0xd1, 0x4d, 0x03, 0x81, 0xe9, 0x14,
+ 0xf0, 0x97, 0x3f, 0x6a, 0x61, 0xe8, 0x8e, 0xe5, 0xd6, 0xc3, 0x2d, 0x8b,
+ 0xa6, 0x50, 0x0b, 0xb9, 0x8d, 0x09, 0x77, 0xd2, 0xc6, 0x7a, 0x86, 0x36,
+ 0x8a, 0x9e, 0x8a, 0x46, 0x96, 0x26, 0x7d, 0xe7, 0x97, 0xf6, 0x62, 0xa4,
+ 0x42, 0x98, 0x07, 0x2d, 0x6f, 0x78, 0x8c, 0xcf, 0x8a, 0x4e, 0xf9, 0x8d,
+ 0xef, 0x28, 0x80, 0x18, 0x62, 0x92, 0x37, 0xa2, 0x26, 0x69, 0x9c, 0x54,
+ 0x1f, 0x12, 0x86, 0xd5, 0x4b, 0xc8, 0xc1, 0xf2, 0x61, 0xb2, 0x8b, 0xee,
+ 0x4f, 0x47, 0xcc, 0xcf, 0x7b, 0xae, 0x23, 0xd0, 0x33, 0x5e, 0x08, 0x74,
+ 0xb2, 0x34, 0x94, 0xca, 0xf0, 0xfb, 0x5a, 0x70, 0xad, 0x82, 0x00, 0x93,
+ 0x4a, 0xbf, 0x19, 0xe5, 0x66, 0xf8, 0x41, 0xfc, 0xc2, 0x85, 0x12, 0x5e,
+ 0x82, 0x46, 0xcd, 0xb3, 0x2d, 0xac, 0x47, 0x58, 0x43, 0x37, 0x90, 0x42,
+ 0x8e, 0x26, 0x0b, 0xd2, 0x05, 0x8e, 0x9f, 0x56, 0x74, 0xcd, 0xf9, 0x44,
+ 0xe0, 0xfd, 0x6e, 0x4f, 0x74, 0xbc, 0xd5, 0x39, 0x93, 0xbf, 0xba, 0x47,
+ 0xd8, 0x8d, 0x00, 0xb5, 0x1c, 0xf4, 0x50, 0xa8, 0xcf, 0xf1, 0x25, 0xaf,
+ 0x27, 0xdc, 0x5c, 0x43, 0xce, 0x73, 0xf6, 0xe8, 0xd8, 0x95, 0x98, 0xef,
+ 0x4b, 0xf3, 0xc4, 0x5a, 0x91, 0xef, 0xa8, 0x40, 0x4f, 0xe7, 0xc1, 0x94,
+ 0x9a, 0xdd, 0xf7, 0x38, 0x1e, 0x81, 0x60, 0xb8, 0xb7, 0x46, 0x2d, 0xa9,
+ 0x23, 0x5a, 0x62, 0x90, 0x97, 0xc6, 0xdf, 0x6f, 0xf8, 0xe3, 0xc5, 0xe0,
+ 0x1b, 0x82, 0xba, 0xb1, 0x18, 0x3d, 0x48, 0xee, 0x01, 0x32, 0x48, 0x10,
+ 0x78, 0x36, 0xa6, 0x55, 0xfa, 0xff, 0xa5, 0xc8, 0x15, 0x59, 0x2a, 0x88,
+ 0xc3, 0xea, 0xd0, 0x2e, 0x15, 0xfb, 0x3f, 0x08, 0xf2, 0xaf, 0x29, 0x10,
+ 0xa6, 0xa4, 0x28, 0xb0, 0xc1, 0xe8, 0x3d, 0x15, 0xfb, 0xc7, 0xc4, 0x83,
+ 0x37, 0x48, 0x97, 0x39, 0xb3, 0xfe, 0xe5, 0x70, 0xfb, 0x43, 0xb2, 0x9e,
+ 0x82, 0x20, 0x94, 0xbe, 0x59, 0xe4, 0xa7, 0x02, 0x63, 0x10, 0xa2, 0xae,
+ 0xc4, 0x61, 0x57, 0x1d, 0x20, 0x1b, 0x37, 0xf9, 0x73, 0x6f, 0x8c, 0xc8,
+ 0xa3, 0x40, 0x57, 0x82, 0x82, 0xb2, 0x0c, 0x75, 0xb4, 0xfe, 0xd8, 0x2c,
+ 0xd2, 0xa0, 0x18, 0x5d, 0x61, 0x10, 0x40, 0xc2, 0x99, 0xd6, 0x6b, 0xd3,
+ 0x54, 0x26, 0x54, 0x07, 0xa1, 0x28, 0x5d, 0xff, 0x37, 0x17, 0xff, 0xa2,
+ 0x33, 0x87, 0xee, 0x51, 0x6a, 0x27, 0x96, 0x35, 0xe3, 0x87, 0x8a, 0x1e,
+ 0x26, 0x56, 0xc8, 0x71, 0x34, 0x53, 0x25, 0x7a, 0x10, 0x5e, 0x8c, 0x37,
+ 0xe8, 0x03, 0x25, 0xf9, 0x70, 0x0a, 0xb5, 0x41, 0xce, 0xc8, 0x7f, 0x36,
+ 0x1d, 0x19, 0x52, 0xf3, 0x54, 0x2c, 0x27, 0xea, 0xdb, 0x7a, 0x44, 0xea,
+ 0x19, 0x8c, 0x11, 0x0e, 0x41, 0x2f, 0xd1, 0xa4, 0xa6, 0x1a, 0x2e, 0x89,
+ 0xc3, 0x78, 0x19, 0x95, 0xce, 0xbc, 0x00, 0x06, 0x90, 0xb4, 0x1f, 0x9b,
+ 0x14, 0xbc, 0x13, 0x9b, 0xfc, 0x68, 0xfa, 0x9c, 0x39, 0x6f, 0xc2, 0x64,
+ 0xcc, 0xdc, 0xf8, 0x7e, 0xae, 0x8e, 0x05, 0xcf, 0xe1, 0xab, 0x38, 0x46,
+ 0xbd, 0x58, 0x8d, 0x49, 0xc5, 0xa0, 0x75, 0x44, 0xbd, 0xca, 0x79, 0xf8,
+ 0xbe, 0xb7, 0xe4, 0x8a, 0x93, 0x29, 0xb8, 0x5d, 0xe7, 0x3c, 0x4f, 0xb8,
+ 0x33, 0x62, 0x4a, 0xbb, 0xaf, 0xf3, 0xb5, 0xfc, 0xf5, 0x30, 0x8e, 0x11,
+ 0x8a, 0x01, 0x20, 0x3e, 0xde, 0xaf, 0x95, 0xb5, 0xd9, 0x39, 0x5f, 0xa4,
+ 0x1c, 0x8f, 0x2f, 0x30, 0xa6, 0x22, 0x3b, 0x86, 0xc6, 0x2a, 0xff, 0x6d,
+ 0x8e, 0x83, 0x9c, 0xc8, 0x42, 0x74, 0x3d, 0x76, 0x55, 0xab, 0xe5, 0x17,
+ 0xda, 0x64, 0xff, 0x96, 0xcd, 0x30, 0xe7, 0xe7, 0x6a, 0x1b, 0x46, 0xa0,
+ 0x24, 0x99, 0x18, 0x21, 0x39, 0xeb, 0x6a, 0xb5, 0xd1, 0x11, 0xdd, 0xf2,
+ 0xb4, 0x6a, 0xac, 0x55, 0x3e, 0xf9, 0xbe, 0x20, 0x0c, 0x51, 0x24, 0xe9,
+ 0xae, 0xdf, 0x59, 0xa8, 0xef, 0xcd, 0xfc, 0xea, 0xb6, 0xe0, 0x61, 0x7d,
+ 0xd3, 0x2a, 0x8f, 0x21, 0xde, 0x20, 0xa8, 0x2e, 0xbe, 0x28, 0x62, 0xaa,
+ 0x95, 0xae, 0x25, 0x14, 0xd0, 0x55, 0x0f, 0x90, 0xb4, 0x03, 0xad, 0xf9,
+ 0xb4, 0x93, 0x0d, 0x7a, 0xff, 0xa9, 0x5b, 0x61, 0x3a, 0x36, 0xe0, 0x15,
+ 0xc9, 0x82, 0xe8, 0x1d, 0x22, 0xd8, 0x57, 0xa4, 0x75, 0x06, 0xe8, 0x86,
+ 0x06, 0x2c, 0x0a, 0xd3, 0x13, 0x62, 0x90, 0xba, 0xf2, 0xde, 0x77, 0xe3,
+ 0x80, 0x51, 0x99, 0xb3, 0xcc, 0x8d, 0x43, 0xdb, 0xc0, 0xcc, 0x45, 0x87,
+ 0xb8, 0x2a, 0xa8, 0x81, 0x20, 0x3b, 0xa1, 0xa0, 0x0c, 0xc6, 0x91, 0x53,
+ 0x4b, 0x21, 0xd5, 0x5f, 0x39, 0x12, 0x0b, 0x27, 0x9f, 0x15, 0x4a, 0xea,
+ 0xec, 0x49, 0x71, 0x4c, 0x2d, 0x2d, 0xa0, 0x89, 0x8f, 0x0c, 0x98, 0x20,
+ 0x5a, 0x45, 0xf2, 0xdf, 0x2e, 0x58, 0x48, 0xcc, 0x2c, 0xba, 0x93, 0x0f,
+ 0x96, 0x6f, 0x0d, 0x30, 0xcd, 0x82, 0xc4, 0xd1, 0xc0, 0x6a, 0xeb, 0x5e,
+ 0x5d, 0x02, 0xaa, 0x46, 0xab, 0x7f, 0x5f, 0x80, 0x72, 0x25, 0xad, 0xc4,
+ 0x04, 0x5e, 0x10, 0xee, 0xa4, 0xf6, 0x83, 0x50, 0x0e, 0x24, 0x2b, 0xdc,
+ 0xaf, 0x8c, 0x0f, 0x2e, 0x88, 0x88, 0xe5, 0xb6, 0xe0, 0x18, 0xfd, 0xdb,
+ 0xbd, 0x80, 0xe2, 0x53, 0x0e, 0xdb, 0x6d, 0x2b, 0x1c, 0x28, 0xd2, 0x33,
+ 0xd6, 0x43, 0x32, 0xc5, 0x3a, 0x1a, 0x7e, 0xee, 0x4b, 0x01, 0x28, 0xb3,
+ 0x8b, 0x43, 0xf8, 0xea, 0x4f, 0xe4, 0x19, 0x07, 0x95, 0x76, 0x1f, 0xec,
+ 0x1d, 0xe7, 0x6d, 0xac, 0xe5, 0x64, 0x9e, 0x3d, 0x16, 0x83, 0x56, 0x35,
+ 0xf3, 0xaa, 0xd0, 0xcd, 0x3b, 0x64, 0x81, 0x90, 0x0d, 0xdf, 0x3c, 0x86,
+ 0x17, 0xff, 0xf6, 0x9d, 0x64, 0x42, 0xcc, 0xc5, 0x41, 0x9b, 0x4d, 0x26,
+ 0x2b, 0x47, 0xa8, 0x53, 0xf9, 0x58, 0xc3, 0x51, 0x00, 0x31, 0x19, 0xa3,
+ 0xef, 0xeb, 0x21, 0x6d, 0x78, 0x7f, 0x45, 0xe6, 0x24, 0xcb, 0x90, 0x09,
+ 0xe8, 0x95, 0x0a, 0xde, 0x34, 0xfd, 0x21, 0xaf, 0x61, 0x34, 0x5d, 0xd3,
+ 0x61, 0x49, 0x4d, 0x26, 0x00, 0xbc, 0x13, 0xc4, 0xbb, 0x96, 0xfa, 0x3e,
+ 0x3d, 0x8c, 0xe7, 0x4f, 0xfa, 0x27, 0xb5, 0x46, 0x71, 0xf3, 0x01, 0x68,
+ 0xe9, 0x09, 0x28, 0xe7, 0x0f, 0x97, 0x0e, 0x69, 0xe3, 0x30, 0x86, 0xef,
+ 0x0d, 0x25, 0x33, 0x43, 0x4c, 0xc8, 0x8a, 0xbb, 0x13, 0x27, 0x08, 0x46,
+ 0x54, 0xa6, 0x28, 0x57, 0xf5, 0xcc, 0x61, 0x55, 0x67, 0xae, 0x43, 0x88,
+ 0xa9, 0x27, 0xe1, 0x3c, 0x61, 0xda, 0x9b, 0x18, 0xc8, 0x60, 0x06, 0x27,
+ 0xf7, 0x67, 0x45, 0xbd, 0xb9, 0x0c, 0x96, 0x73, 0x30, 0x1a, 0x9a, 0x5f,
+ 0xfa, 0x16, 0xa5, 0xbe, 0xc7, 0xed, 0x4a, 0x98, 0xf8, 0x13, 0xaa, 0x89,
+ 0x92, 0xa2, 0x77, 0xb6, 0x66, 0x9d, 0x20, 0x83, 0xd2, 0x25, 0xbb, 0x9e,
+ 0x70, 0xb8, 0x27, 0x63, 0x93, 0x29, 0x55, 0x61, 0x7c, 0xa3, 0x34, 0xeb,
+ 0xa5, 0x3d, 0xa6, 0x71, 0x75, 0x77, 0x55, 0xc9, 0xf7, 0x44, 0xad, 0xa0,
+ 0xac, 0xb5, 0x46, 0x41, 0x23, 0x57, 0x4d, 0x3b, 0x1e, 0xea, 0x38, 0x3b,
+ 0xab, 0xe5, 0x2b, 0xcc, 0x2b, 0x5a, 0xae, 0x84, 0xf1, 0x22, 0xc5, 0x2e,
+ 0xec, 0xe8, 0x0c, 0xbe, 0xb9, 0xe1, 0x20, 0xaa, 0x59, 0xc0, 0xd9, 0x40,
+ 0xe0, 0x5d, 0xdc, 0x73, 0x01, 0x0b, 0x1a, 0x08, 0x82, 0x98, 0x8e, 0x92,
+ 0x3d, 0xf2, 0x74, 0xa2, 0xfe, 0x47, 0x17, 0x46, 0x3e, 0xc6, 0xaa, 0xa3,
+ 0xc0, 0xcd, 0xd0, 0x2e, 0xbe, 0x5b, 0xd8, 0x6c, 0x10, 0xc3, 0x1b, 0x2d,
+ 0xd2, 0x83, 0x70, 0xa0, 0xf4, 0x66, 0xad, 0x9f, 0x89, 0x5c, 0x8b, 0xde,
+ 0xb0, 0x7c, 0x46, 0x9d, 0x2a, 0xd4, 0x9a, 0x75, 0x29, 0x0b, 0x62, 0x0a,
+ 0xb4, 0x1f, 0xeb, 0x81, 0x76, 0x70, 0x71, 0x37, 0xc6, 0xa0, 0xce, 0xab,
+ 0x75, 0xeb, 0x73, 0xac, 0x1a, 0x1d, 0x8b, 0x39, 0x13, 0xca, 0xfa, 0x17,
+ 0x62, 0x75, 0xf2, 0x2e, 0xe3, 0x62, 0xb2, 0xad, 0x96, 0x89, 0x0f, 0xd3,
+ 0x0a, 0xfb, 0x90, 0x83, 0x36, 0x43, 0x5f, 0x43, 0x52, 0x9f, 0x5a, 0xab,
+ 0xd5, 0xb6, 0xdf, 0x69, 0x69, 0x2c, 0xca, 0x5a, 0x20, 0xc7, 0x2a, 0x1f,
+ 0x90, 0x41, 0x24, 0x71, 0xf3, 0x54, 0x0c, 0x7c, 0x06, 0xfc, 0x5d, 0x33,
+ 0xc5, 0x56, 0x2c, 0x4a, 0x90, 0x4e, 0x36, 0x46, 0xa1, 0xef, 0x0d, 0x57,
+ 0xd0, 0x1c, 0x2b, 0xdd, 0x26, 0x94, 0xb4, 0xea, 0xbc, 0xd0, 0xb2, 0x89,
+ 0xb5, 0x53, 0x28, 0xf6, 0x80, 0xf6, 0xb7, 0xb4, 0x73, 0x8b, 0xad, 0x60,
+ 0x3c, 0x4c, 0x44, 0xc0, 0x1b, 0xe0, 0xb4, 0xf2, 0x97, 0xa0, 0xc2, 0x94,
+ 0x33, 0x4f, 0x96, 0x48, 0xe8, 0x35, 0x23, 0x9f, 0xaa, 0x64, 0x25, 0x11,
+ 0x50, 0x60, 0x81, 0x35, 0x62, 0xa0, 0x1e, 0x6b, 0xaf, 0x41, 0xda, 0xdb,
+ 0x40, 0x51, 0xd4, 0xa0, 0x02, 0x2f, 0xff, 0x6e, 0xf0, 0x11, 0x1c, 0xb6,
+ 0x7c, 0x1d, 0x79, 0xba, 0x05, 0x8c, 0x49, 0x96, 0xf5, 0x2d, 0xf4, 0x46,
+ 0x83, 0x80, 0xf1, 0x9f, 0xc2, 0xd9, 0x64, 0x88, 0x0f, 0x07, 0x5b, 0xd0,
+ 0x3a, 0x32, 0x93, 0xed, 0xf3, 0x41, 0xc7, 0x0e, 0x1b, 0xbd, 0x1b, 0x75,
+ 0x49, 0x68, 0xb1, 0xeb, 0x5f, 0xa4, 0x3e, 0xda, 0xec, 0x8a, 0xa3, 0x85,
+ 0x08, 0x8a, 0x90, 0x10, 0x1a, 0x59, 0x6a, 0x46, 0xfe, 0xe2, 0x38, 0x7c,
+ 0x17, 0x93, 0x13, 0x88, 0xee, 0xfc, 0x55, 0xc7, 0x9b, 0xcf, 0x31, 0x8c,
+ 0x8d, 0xd8, 0x32, 0xa6, 0x38, 0xae, 0x61, 0x94, 0xdb, 0x41, 0x32, 0x81,
+ 0x64, 0xe6, 0x44, 0x55, 0x04, 0x08, 0xc0, 0xbb, 0xa6, 0x9b, 0x9f, 0x06,
+ 0xa1, 0x1b, 0xbd, 0x1e, 0xe9, 0x7d, 0xd6, 0x7d, 0x6f, 0x30, 0x69, 0xb8,
+ 0xad, 0x77, 0x0a, 0xfb, 0x35, 0x03, 0x29, 0x16, 0xa7, 0xa6, 0x7d, 0x17,
+ 0xe2, 0xdb, 0x6e, 0xf0, 0xa4, 0x0e, 0x14, 0x5a, 0xb7, 0x18, 0xb7, 0x9f,
+ 0x1b, 0xbf, 0xa9, 0xae, 0x7e, 0xbf, 0x47, 0x79, 0xbe, 0xae, 0x53, 0x11,
+ 0x8f, 0x21, 0x92, 0x1f, 0x4a, 0x4a, 0x17, 0xbd, 0x38, 0x03, 0x2e, 0x28,
+ 0x76, 0xff, 0x3f, 0x0b, 0x43, 0x28, 0xf8, 0xa2, 0xa2, 0x58, 0xee, 0xec,
+ 0x4e, 0x01, 0x77, 0x81, 0x0f, 0x27, 0xdf, 0xc5, 0x6f, 0x43, 0xba, 0xad,
+ 0x28, 0x08, 0xa8, 0xdc, 0xde, 0xdf, 0x3e, 0x9f, 0xc5, 0x15, 0x39, 0xf5,
+ 0xea, 0x11, 0xf4, 0xe1, 0xe8, 0xfa, 0x24, 0x02, 0xa2, 0xbc, 0xd0, 0xca,
+ 0xc0, 0xc7, 0x22, 0x5a, 0x3b, 0x77, 0x18, 0x0b, 0x32, 0xb8, 0x52, 0xba,
+ 0xa5, 0x8d, 0x2b, 0x80, 0xda, 0x29, 0x1a, 0xaf, 0x99, 0x48, 0xf6, 0x81,
+ 0xd7, 0x5c, 0x11, 0x75, 0xe3, 0x10, 0xca, 0x14, 0xa5, 0x3e, 0x1a, 0x6a,
+ 0xf9, 0x30, 0x87, 0xcd, 0x6c, 0xd3, 0x55, 0xc2, 0x15, 0x7e, 0x8c, 0x4e,
+ 0x19, 0x17, 0x3c, 0xcf, 0xa9, 0xdd, 0xda, 0x9d, 0xfa, 0xfb, 0x18, 0x17,
+ 0x11, 0x76, 0x30, 0x2b, 0x9d, 0x60, 0x9d, 0xb5, 0x9a, 0x0f, 0xf4, 0x61,
+ 0xca, 0xd6, 0x29, 0x76, 0xd4, 0x85, 0x7e, 0xba, 0x7d, 0xff, 0x31, 0x17,
+ 0x12, 0xdd, 0xe6, 0xf2, 0x98, 0xf0, 0x5e, 0x5c, 0x1c, 0x1f, 0xc5, 0x00,
+ 0x9d, 0xee, 0xc5, 0xc0, 0xd9, 0xa5, 0xbd, 0xd0, 0x30, 0x8e, 0x24, 0x30,
+ 0x87, 0xfb, 0x9f, 0xcd, 0x51, 0xef, 0x20, 0xf0, 0x67, 0x41, 0x1c, 0x73,
+ 0xc5, 0x8f, 0x4d, 0xcb, 0x30, 0x3f, 0xa3, 0xd8, 0xb1, 0xc7, 0x9c, 0xb7,
+ 0xff, 0x4b, 0xd5, 0xae, 0xad, 0x89, 0x9b, 0xd0, 0x99, 0xd7, 0x52, 0xf7,
+ 0x7a, 0xa7, 0x5d, 0x1d, 0x5f, 0x15, 0xe7, 0x09, 0x84, 0xb4, 0x61, 0x78,
+ 0x47, 0x8f, 0xb1, 0x9f, 0x54, 0xaf, 0xc3, 0x5d, 0xf1, 0xad, 0xc7, 0xdf,
+ 0x0e, 0xd8, 0x99, 0x78, 0xf4, 0x86, 0x9e, 0x77, 0x0f, 0xc6, 0x93, 0xdd,
+ 0xc8, 0xa6, 0x71, 0x6d, 0x19, 0xee, 0x64, 0x6c, 0x6e, 0x53, 0xc0, 0xd3,
+ 0xa3, 0xf5, 0x2d, 0xe7, 0xec, 0x73, 0x34, 0x09, 0x1d, 0x34, 0x0d, 0x8c,
+ 0xae, 0xf9, 0x62, 0x6a, 0xf5, 0x67, 0x20, 0x46, 0x2b, 0xfc, 0x2f, 0x77,
+ 0xee, 0xa3, 0x68, 0xbf, 0x0e, 0x17, 0x5c, 0x8e, 0x44, 0x97, 0x95, 0x68,
+ 0x65, 0xee, 0xf8, 0xb1, 0x13, 0x29, 0xef, 0x10, 0xc7, 0x7b, 0xa6, 0x85,
+ 0xf3, 0xb7, 0xcb, 0x85, 0xa6, 0x38, 0xc4, 0xb1, 0xa1, 0xb8, 0x20, 0x6f,
+ 0x22, 0xd7, 0xd9, 0x54, 0x85, 0x6c, 0x6f, 0xb1, 0x1c, 0xa1, 0xb0, 0x13,
+ 0xab, 0x4e, 0xa6, 0x62, 0x59, 0x69, 0x6f, 0xf7, 0xfd, 0xfd, 0x8c, 0x48,
+ 0xe4, 0x2f, 0x64, 0x30, 0x75, 0x45, 0xe3, 0xa5, 0x02, 0xf3, 0x6b, 0x11,
+ 0x3f, 0xe2, 0xc2, 0x7b, 0x9d, 0x8a, 0x48, 0xfd, 0xa1, 0xa4, 0xe3, 0xf7,
+ 0xcc, 0xd5, 0xca, 0x3f, 0x64, 0x2e, 0x55, 0xcb, 0x67, 0x97, 0xca, 0xe7,
+ 0x45, 0x17, 0xdc, 0x68, 0x01, 0x84, 0xe5, 0xf4, 0x9a, 0x76, 0xfe, 0xb4,
+ 0x1b, 0xdc, 0x83, 0xee, 0x33, 0xa3, 0x74, 0x42, 0x22, 0x6c, 0xbe, 0x72,
+ 0x62, 0xa2, 0x39, 0x33, 0x3a, 0xea, 0x14, 0x4f, 0xf5, 0xf1, 0x9a, 0xbe,
+ 0xa9, 0x4f, 0xb1, 0xfc, 0xee, 0x1c, 0xbe, 0xba, 0x69, 0x03, 0x66, 0x3e,
+ 0x4f, 0x23, 0xe8, 0x95, 0x3f, 0xdb, 0xd0, 0x0a, 0xef, 0x19, 0xff, 0x4d,
+ 0xe8, 0x1e, 0x67, 0x70, 0x36, 0xca, 0x58, 0xf4, 0x4c, 0x97, 0xa5, 0x34,
+ 0xf7, 0xf2, 0xd4, 0xd3, 0x1e, 0x85, 0x18, 0x0e, 0x15, 0xf8, 0x9e, 0xe8,
+ 0x06, 0xbb, 0x7d, 0x77, 0x47, 0x7a, 0xe2, 0xc3, 0x10, 0x13, 0x19, 0xc3,
+ 0xfb, 0xa7, 0xf8, 0x97, 0xf5, 0x73, 0xe2, 0xf5, 0x25, 0x63, 0x71, 0x6b,
+ 0x52, 0xba, 0x93, 0x23, 0x67, 0x5a, 0x11, 0x3a, 0x8c, 0x8c, 0x6e, 0xd4,
+ 0x57, 0x94, 0x75, 0x74, 0x89, 0xdb, 0xb7, 0x11, 0xb0, 0x21, 0x1c, 0xee,
+ 0x83, 0x41, 0x34, 0x7c, 0x99, 0xa7, 0xbd, 0xfb, 0xfc, 0x08, 0xf6, 0xf3,
+ 0x0f, 0xc9, 0x5b, 0x3f, 0xbd, 0xb1, 0xda, 0xda, 0xf9, 0x0d, 0xe8, 0x06,
+ 0xe0, 0xf0, 0x45, 0x44, 0x74, 0x66, 0xc8, 0xb5, 0xa1, 0xf1, 0xa3, 0x3a,
+ 0x3e, 0x75, 0x18, 0xb2, 0x91, 0x0e, 0xd0, 0x68, 0x61, 0xd4, 0xc9, 0xba,
+ 0xb3, 0xad, 0x83, 0x13, 0xe2, 0x41, 0xc7, 0xeb, 0xa1, 0x03, 0x4d, 0xfc,
+ 0xf3, 0x67, 0x11, 0x58, 0xc0, 0x55, 0x08, 0xa3, 0x6f, 0x37, 0x12, 0x15,
+ 0xc7, 0x59, 0x1c, 0x60, 0xa6, 0x4e, 0xcb, 0xe1, 0xb1, 0x5b, 0xf7, 0x20,
+ 0xed, 0x73, 0x23, 0x16, 0x43, 0x4b, 0x66, 0xe7, 0xf5, 0xe0, 0x90, 0x4a,
+ 0x5f, 0xd5, 0x2d, 0x9c, 0xfc, 0x9c, 0x45, 0xf4, 0x48, 0xe9, 0x91, 0xb6,
+ 0xf7, 0x44, 0xb9, 0x6a, 0xb1, 0x48, 0x6c, 0x6f, 0x67, 0xcd, 0x37, 0xe7,
+ 0x96, 0xcd, 0x99, 0x7f, 0xf0, 0xfe, 0x0e, 0x52, 0x7d, 0xdd, 0x0b, 0xae,
+ 0x85, 0x8b, 0x1f, 0x99, 0xb1, 0xfa, 0x05, 0xff, 0x4d, 0xa7, 0x7c, 0xeb,
+ 0x17, 0x35, 0x44, 0xfa, 0x31, 0x6b, 0x57, 0x18, 0x23, 0x8f, 0xee, 0xd7,
+ 0x80, 0xfb, 0xd2, 0xc2, 0xc5, 0xce, 0x98, 0xd7, 0x73, 0x9b, 0x44, 0x27,
+ 0x96, 0x96, 0x82, 0x98, 0x0d, 0x47, 0x7f, 0xf0, 0x44, 0x9a, 0x12, 0x21,
+ 0x22, 0xa8, 0x57, 0x34, 0xf9, 0x68, 0x4c, 0x98, 0xc6, 0xed, 0xa1, 0xb6,
+ 0x68, 0xd2, 0xf2, 0x56, 0x10, 0xfa, 0x78, 0x78, 0x07, 0xe9, 0xf0, 0x08,
+ 0x81, 0x7c, 0x1d, 0x65, 0x18, 0x8f, 0x3d, 0x6b, 0xf7, 0x09, 0xe9, 0x11,
+ 0x58, 0x62, 0x05, 0xbe, 0xf9, 0x8f, 0xfb, 0xb7, 0x20, 0xee, 0x53, 0x46,
+ 0x62, 0xb8, 0x9c, 0x12, 0x2d, 0x4b, 0x67, 0x7b, 0xe1, 0xec, 0xb3, 0x98,
+ 0x6c, 0xe1, 0x89, 0x82, 0x0a, 0xe1, 0x9c, 0xfa, 0x25, 0x79, 0xa0, 0x74,
+ 0x4f, 0xe4, 0x0e, 0x6f, 0xc2, 0xe4, 0x6e, 0x8e, 0xb3, 0xbf, 0x24, 0x06,
+ 0x5c, 0x47, 0xf3, 0xfe, 0x4a, 0xc4, 0x6a, 0x77, 0x33, 0x87, 0x82, 0x92,
+ 0x09, 0xd6, 0xf4, 0x11, 0xe3, 0x43, 0x85, 0x75, 0x99, 0x70, 0x14, 0x16,
+ 0x5d, 0xbd, 0xd9, 0x20, 0x04, 0x51, 0x89, 0x10, 0x30, 0xea, 0x88, 0x84,
+ 0x5a, 0x16, 0x2f, 0x34, 0x85, 0x3b, 0x1f, 0x1d, 0x28, 0x8b, 0x0c, 0xe3,
+ 0xcd, 0xb8, 0x82, 0xa7, 0xd7, 0xae, 0x16, 0x29, 0x29, 0x80, 0x7f, 0x15,
+ 0xb2, 0x69, 0xf4, 0xff, 0x72, 0x85, 0xda, 0x1c, 0xb7, 0xc8, 0x21, 0x6b,
+ 0x35, 0xc8, 0x49, 0x90, 0x07, 0xf3, 0x91, 0x6a, 0x51, 0x1d, 0x78, 0x35,
+ 0x4a, 0xc6, 0xa7, 0xe1, 0x7a, 0x1c, 0x08, 0x48, 0x2e, 0xdc, 0xef, 0xa9,
+ 0xf5, 0xab, 0x69, 0x1a, 0x59, 0xf9, 0xd6, 0x35, 0xa2, 0x42, 0x59, 0xc2,
+ 0xd0, 0x30, 0x31, 0xb4, 0x5b, 0x2c, 0xaf, 0x8d, 0x3e, 0x08, 0x5e, 0xe2,
+ 0xb8, 0x45, 0xa9, 0x82, 0x5e, 0x47, 0x94, 0xb2, 0xc5, 0xd1, 0xdd, 0x35,
+ 0xf5, 0x84, 0x8c, 0xc4, 0x92, 0xa7, 0xf9, 0x57, 0x0b, 0x7e, 0x9f, 0xab,
+ 0x9c, 0xd3, 0x9a, 0x68, 0xd6, 0xa1, 0x91, 0x93, 0x84, 0xbe, 0x0c, 0xfc,
+ 0x75, 0x01, 0x58, 0xde, 0xf4, 0xeb, 0x47, 0x6e, 0x61, 0x1d, 0x6f, 0x6d,
+ 0x94, 0xb6, 0x3a, 0xe1, 0x44, 0x0d, 0xf5, 0x31, 0xc3, 0xc9, 0x3b, 0xd3,
+ 0xa3, 0x5c, 0x5f, 0x96, 0xa1, 0xf0, 0x52, 0x22, 0x87, 0xa9, 0x38, 0x69,
+ 0x4a, 0xef, 0x4d, 0xb0, 0xc6, 0x78, 0xd1, 0xd6, 0x8c, 0xf8, 0x8f, 0x8f,
+ 0x04, 0xb4, 0x54, 0xae, 0x55, 0x6c, 0x5c, 0x6e, 0x98, 0x7d, 0x92, 0x47,
+ 0x14, 0x42, 0x1e, 0xb3, 0x9d, 0xbc, 0xa4, 0x17, 0xd0, 0xbe, 0xf0, 0x53,
+ 0x50, 0x9c, 0xc0, 0xc6, 0x76, 0xfe, 0xf6, 0x95, 0x67, 0xf8, 0x69, 0x5c,
+ 0x2f, 0x98, 0xee, 0xbf, 0xdc, 0x62, 0x16, 0x39, 0x45, 0x01, 0xe1, 0x9d,
+ 0x8a, 0x33, 0x23, 0x49, 0xd3, 0x80, 0x4f, 0x72, 0x3a, 0xfe, 0x81, 0xe7,
+ 0x65, 0x53, 0xda, 0xc1, 0x92, 0x3e, 0x89, 0x8f, 0x65, 0xc6, 0x7d, 0x90,
+ 0x7f, 0x6e, 0x58, 0xc4, 0xba, 0xad, 0xdf, 0x6c, 0xce, 0x0c, 0x66, 0xec,
+ 0xc0, 0xfc, 0xf2, 0xac, 0x5f, 0xb4, 0xb4, 0xab, 0x8b, 0x0d, 0xb6, 0xac,
+ 0x29, 0x73, 0xbf, 0x5f, 0x13, 0x54, 0x89, 0xaf, 0xb8, 0x56, 0x00, 0x41,
+ 0x48, 0x4d, 0x4b, 0x13, 0xb3, 0xb8, 0x92, 0xc4, 0xff, 0x61, 0x26, 0x5e,
+ 0xfb, 0x76, 0xae, 0x05, 0xbb, 0x03, 0xaa, 0x46, 0x45, 0x5b, 0xcd, 0xd4,
+ 0x7e, 0x5f, 0x81, 0x50, 0x8c, 0x66, 0x57, 0x49, 0x8f, 0x3a, 0x5e, 0xf3,
+ 0x43, 0xac, 0x95, 0xb6, 0xdb, 0x18, 0x82, 0xbe, 0x29, 0xea, 0x2b, 0xd9,
+ 0xc0, 0xeb, 0xfe, 0xd5, 0xd3, 0x37, 0xf9, 0xa3, 0xcc, 0xb1, 0xd2, 0xe4,
+ 0x7b, 0x03, 0x0a, 0x84, 0x6a, 0xbe, 0x8c, 0x79, 0x98, 0x14, 0x41, 0x61,
+ 0x4d, 0x48, 0xa9, 0x33, 0x0e, 0x41, 0xf7, 0x03, 0xf8, 0x0f, 0xcc, 0x2a,
+ 0xc0, 0x37, 0x6e, 0x57, 0x24, 0x42, 0xd7, 0xed, 0xf7, 0x11, 0xd2, 0x84,
+ 0x91, 0x1f, 0x97, 0x69, 0x85, 0xc3, 0xec, 0xe0, 0xa2, 0x9b, 0xcb, 0x5b,
+ 0x24, 0x88, 0x29, 0x04, 0x05, 0x56, 0x5a, 0x30, 0x43, 0xc5, 0xba, 0x2a,
+ 0xc1, 0x8e, 0x19, 0x2b, 0x31, 0x6b, 0x5c, 0xbf, 0x12, 0x75, 0xb8, 0x5d,
+ 0x23, 0xce, 0x81, 0x45, 0xec, 0x93, 0x29, 0xcb, 0x5c, 0x18, 0xc6, 0x90,
+ 0x50, 0xd6, 0x1a, 0x48, 0x1d, 0x7d, 0xb7, 0x5c, 0x78, 0xcc, 0x0e, 0xc5,
+ 0xf5, 0x85, 0xc7, 0x28, 0x60, 0x5d, 0x4b, 0x4d, 0xb0, 0x39, 0x64, 0x47,
+ 0x55, 0xb9, 0xe7, 0x3a, 0x57, 0xbc, 0xc3, 0xa7, 0xe2, 0x8a, 0xf9, 0x85,
+ 0x0c, 0xff, 0x3b, 0x3c, 0x74, 0x6c, 0x96, 0xa4, 0xd5, 0x26, 0xe0, 0xb4,
+ 0xa6, 0x4c, 0xb0, 0x83, 0x1f, 0x54, 0x7b, 0x67, 0x7a, 0x2b, 0x9d, 0x79,
+ 0x08, 0x30, 0xd3, 0xe8, 0x62, 0x5e, 0x65, 0x2a, 0x5e, 0x7a, 0xf4, 0xbe,
+ 0x53, 0xac, 0x5d, 0xf4, 0x2f, 0x75, 0x9a, 0xb6, 0xb3, 0x3c, 0xe9, 0x58,
+ 0xe9, 0x12, 0x7f, 0x7e, 0xb8, 0xd6, 0x53, 0x5e, 0x4a, 0x3f, 0x70, 0x8b,
+ 0x67, 0x32, 0x50, 0x5c, 0xd3, 0x10, 0xd9, 0x8d, 0x47, 0x73, 0xc5, 0x21,
+ 0x44, 0x47, 0xe9, 0x4b, 0x89, 0xea, 0x86, 0x21, 0x4e, 0x2c, 0xea, 0x5e,
+ 0x70, 0x65, 0x28, 0xa0, 0x1d, 0x3e, 0x15, 0x9c, 0xa3, 0xd7, 0x62, 0xa5,
+ 0x13, 0x0d, 0x6f, 0x2f, 0x1f, 0xdd, 0x97, 0x28, 0xa5, 0x36, 0x75, 0xc8,
+ 0xdd, 0xfa, 0x02, 0x43, 0xf8, 0x6c, 0x13, 0xef, 0x93, 0x17, 0xe9, 0x3a,
+ 0xa9, 0xa7, 0x5c, 0x9c, 0x74, 0xc8, 0x19, 0x33, 0x6e, 0x0b, 0xf5, 0xca,
+ 0x5c, 0xcc, 0x90, 0x73, 0x88, 0x50, 0x79, 0x36, 0xd9, 0x1b, 0x60, 0xd1,
+ 0xe4, 0xcf, 0x3a, 0x26, 0xa9, 0xdd, 0xa3, 0x27, 0x33, 0x12, 0x95, 0xe3,
+ 0x7e, 0xc4, 0x8d, 0x0a, 0x44, 0x5e, 0x57, 0x4d, 0xd4, 0x3d, 0x9a, 0x6a,
+ 0x1d, 0x55, 0x81, 0x1f, 0x8b, 0xff, 0x84, 0x8d, 0xee, 0xe4, 0x9a, 0xf0,
+ 0x24, 0x89, 0xcc, 0x1a, 0x9d, 0x49, 0xd4, 0x9c, 0xea, 0x73, 0x52, 0x19,
+ 0x15, 0xe9, 0x50, 0x65, 0x18, 0xed, 0x1c, 0x3c, 0x50, 0xd0, 0xef, 0x8e,
+ 0x63, 0xe9, 0x08, 0x5b, 0x4d, 0x60, 0x0c, 0x26, 0x50, 0xb4, 0xe1, 0x49,
+ 0x58, 0x6b, 0x18, 0x54, 0x47, 0x60, 0x1e, 0xff, 0x4a, 0x00, 0x8a, 0x52,
+ 0x07, 0xef, 0x51, 0xcb, 0xc1, 0x41, 0xfe, 0x40, 0xa4, 0x3d, 0x6d, 0x76,
+ 0x5c, 0x43, 0x2c, 0xdc, 0x9c, 0xe8, 0x09, 0x7d, 0x8c, 0xdf, 0x84, 0xef,
+ 0xa9, 0xa8, 0xe8, 0xd3, 0xa3, 0x10, 0x00, 0x2d, 0xb7, 0x30, 0x29, 0xc4,
+ 0x6f, 0x10, 0xe7, 0x2a, 0xfa, 0xe3, 0x26, 0x49, 0xcf, 0x5c, 0xf1, 0xcc,
+ 0xc4, 0x27, 0xc9, 0xc2, 0x35, 0xd8, 0xba, 0xa7, 0x97, 0x61, 0x87, 0x95,
+ 0x13, 0xee, 0x1a, 0xec, 0xe3, 0x2d, 0xae, 0xca, 0x93, 0x26, 0x4e, 0xbe,
+ 0x02, 0x31, 0xb5, 0xec, 0xe9, 0x4e, 0xfc, 0x82, 0x94, 0x84, 0xb9, 0x92,
+ 0x96, 0xef, 0xeb, 0x2b, 0x41, 0xf3, 0xd4, 0x27, 0xb3, 0xef, 0x90, 0x53,
+ 0xd6, 0x83, 0xe8, 0x4f, 0x54, 0xbc, 0x76, 0xd3, 0xa3, 0x6f, 0xf0, 0xc7,
+ 0x2c, 0xc8, 0xa0, 0x64, 0x4b, 0xa3, 0x7f, 0x86, 0x52, 0xc3, 0xc0, 0xdb,
+ 0x63, 0xc8, 0x44, 0xfb, 0x6d, 0x5d, 0xa6, 0x4e, 0xc2, 0xbc, 0x07, 0x51,
+ 0x70, 0x47, 0xf3, 0xd9, 0xe9, 0x45, 0x0b, 0x46, 0xdd, 0x54, 0x49, 0xd8,
+ 0x1f, 0x49, 0x75, 0xc2, 0x86, 0xff, 0x9a, 0x66, 0x53, 0x86, 0xd9, 0xbf,
+ 0xa2, 0x30, 0xed, 0xd0, 0x40, 0x64, 0xcf, 0xb9, 0xb3, 0xbd, 0x6b, 0xb5,
+ 0x5c, 0x00, 0x8a, 0x50, 0xc0, 0xae, 0xf5, 0x56, 0xc9, 0x57, 0x0f, 0xb2,
+ 0x61, 0x65, 0xbe, 0xa6, 0x26, 0x00, 0x1c, 0xc5, 0xdc, 0x30, 0x46, 0xa5,
+ 0x88, 0x72, 0xf8, 0x07, 0x07, 0x84, 0xb5, 0xaf, 0x1b, 0xef, 0x12, 0x8a,
+ 0x61, 0xc2, 0xd9, 0xc3, 0x99, 0x2b, 0x16, 0x0a, 0x02, 0xf5, 0xd8, 0x46,
+ 0xbb, 0x16, 0xc5, 0xb6, 0x90, 0x37, 0x3b, 0x72, 0x9e, 0x3a, 0xb9, 0xbb,
+ 0x86, 0x6e, 0x08, 0x48, 0xd6, 0xad, 0x48, 0x8b, 0x53, 0x4f, 0xbc, 0x27,
+ 0x47, 0x84, 0x1c, 0x17, 0xba, 0xae, 0xf3, 0x00, 0xe1, 0x1a, 0xfd, 0x73,
+ 0xa5, 0xf7, 0x04, 0x44, 0xc1, 0xac, 0xbb, 0x2c, 0xc3, 0x69, 0xe2, 0x45,
+ 0x46, 0x33, 0xdd, 0x2c, 0xea, 0x82, 0xef, 0x7a, 0x45, 0x58, 0x07, 0x7a,
+ 0xaa, 0xdd, 0x36, 0x26, 0x5a, 0xed, 0x41, 0xc5, 0x61, 0x9c, 0xd6, 0x3c,
+ 0x57, 0x45, 0xcf, 0xb6, 0xc1, 0x1f, 0x14, 0x65, 0x68, 0x4e, 0x8b, 0x16,
+ 0xf2, 0x07, 0x76, 0x10, 0x4c, 0x00, 0xc6, 0xc6, 0x14, 0xcf, 0xdc, 0x98,
+ 0x17, 0x50, 0xde, 0x27, 0x86, 0xf2, 0x5f, 0xab, 0x94, 0x73, 0x75, 0xb5,
+ 0x9e, 0x5d, 0x03, 0x5e, 0xed, 0xa5, 0xa9, 0xdb, 0x47, 0x48, 0xf1, 0xb9,
+ 0xb7, 0x72, 0x02, 0xfb, 0x24, 0x86, 0x20, 0x13, 0xa8, 0xf0, 0x44, 0xc4,
+ 0x8a, 0xec, 0x3d, 0x33, 0x40, 0xaa, 0xe7, 0xb0, 0xa4, 0x0e, 0x34, 0x6e,
+ 0x5b, 0x07, 0x5d, 0x08, 0x05, 0x9d, 0x2d, 0x5c, 0x54, 0xd6, 0x1c, 0x35,
+ 0x4c, 0x7f, 0x0b, 0x39, 0x1f, 0xbd, 0xd7, 0xb9, 0x6b, 0xda, 0x86, 0xcd,
+ 0x09, 0x43, 0xb5, 0x9a, 0xf8, 0xed, 0x42, 0x72, 0x49, 0x9f, 0xdd, 0x09,
+ 0x21, 0x61, 0xca, 0x6b, 0x8e, 0x2c, 0x17, 0x5c, 0x7a, 0x08, 0x80, 0x02,
+ 0x9a, 0x3b, 0x42, 0xf6, 0x44, 0x50, 0xfc, 0x7c, 0x6f, 0x24, 0x75, 0xca,
+ 0x4b, 0x14, 0xb2, 0x3c, 0x4a, 0x59, 0x37, 0x7b, 0x4e, 0x0e, 0xa4, 0xa8,
+ 0x21, 0x0b, 0x72, 0x3a, 0x53, 0xc8, 0x63, 0xdf, 0x67, 0xb6, 0x32, 0x79,
+ 0x07, 0x13, 0x61, 0x54, 0x28, 0x17, 0x7d, 0x05, 0xa1, 0xde, 0xdd, 0x7c,
+ 0xbc, 0x15, 0x72, 0x4a, 0xa9, 0xee, 0x6e, 0x32, 0xa0, 0x72, 0x22, 0x63,
+ 0xe5, 0xf8, 0x59, 0x19, 0x31, 0xd7, 0xc0, 0xc7, 0xd0, 0x37, 0xda, 0x3b,
+ 0x15, 0x4e, 0xa1, 0xf7, 0x93, 0x85, 0x63, 0x4c, 0x6b, 0x0e, 0x00, 0xec,
+ 0x47, 0x5f, 0x1f, 0xb1, 0x13, 0xbe, 0xad, 0x13, 0x0d, 0x47, 0xf8, 0x94,
+ 0xce, 0x54, 0xce, 0x00, 0x2e, 0xfd, 0xa4, 0x6f, 0x72, 0xf3, 0x02, 0x9c,
+ 0xd4, 0x4c, 0xce, 0x3b, 0x26, 0x09, 0xbb, 0x54, 0xa5, 0xef, 0x2a, 0x8a,
+ 0xe3, 0x45, 0x38, 0xff, 0xc8, 0x58, 0x12, 0x76, 0x78, 0x2f, 0x15, 0x62,
+ 0xb6, 0x6b, 0x54, 0x3c, 0x10, 0xd3, 0x01, 0x6e, 0x14, 0x7f, 0xa6, 0xe6,
+ 0x31, 0xda, 0x17, 0xee, 0x43, 0x91, 0xcc, 0x84, 0x1f, 0x53, 0xb7, 0xb0,
+ 0xf7, 0x15, 0x5c, 0x24, 0xe1, 0x82, 0x25, 0x62, 0x35, 0x31, 0xf5, 0xa1,
+ 0xee, 0x46, 0x93, 0x8c, 0x55, 0x7d, 0x40, 0x43, 0xc8, 0xec, 0xed, 0x99,
+ 0x94, 0xe4, 0x74, 0x88, 0x9a, 0x81, 0x4a, 0x05, 0xae, 0x5c, 0x88, 0x08,
+ 0xe7, 0x69, 0x2a, 0xe1, 0x04, 0xe4, 0xd0, 0x7a, 0xeb, 0x7a, 0xa6, 0xe5,
+ 0x07, 0x73, 0x1e, 0x70, 0xb5, 0x47, 0xae, 0xee, 0xa9, 0xcf, 0x19, 0xb1,
+ 0x74, 0x43, 0x86, 0x1a, 0x21, 0x08, 0x42, 0x0d, 0xca, 0xd7, 0x1a, 0xa4,
+ 0xe8, 0xae, 0xf0, 0xe1, 0x90, 0x03, 0x46, 0xa3, 0xb6, 0x72, 0xfa, 0x84,
+ 0xf4, 0x98, 0xdd, 0x0e, 0x24, 0x5b, 0x4a, 0x3f, 0x4a, 0x8a, 0x73, 0x78,
+ 0xa6, 0x89, 0x47, 0x36, 0x99, 0x7c, 0x2b, 0xfe, 0x34, 0x95, 0xe6, 0x9e,
+ 0xac, 0x72, 0x92, 0x4c, 0x3a, 0xf2, 0xf9, 0xe3, 0xf6, 0x9d, 0x15, 0xf9,
+ 0xbe, 0x03, 0x59, 0x07, 0x04, 0xe5, 0x27, 0xb7, 0x43, 0x19, 0x6c, 0xba,
+ 0x2a, 0x5b, 0xf9, 0x8c, 0xd8, 0xd2, 0x4f, 0x90, 0x58, 0xd1, 0xaa, 0x97,
+ 0x50, 0x8b, 0xaa, 0x56, 0xb6, 0xf0, 0x0a, 0xd2, 0x4e, 0x55, 0x56, 0x91,
+ 0xeb, 0x4d, 0x9c, 0x66, 0xd1, 0xec, 0x39, 0x92, 0x40, 0xd7, 0xb8, 0x50,
+ 0x0b, 0x18, 0x3c, 0xa1, 0x96, 0xd0, 0xc4, 0x88, 0xc0, 0x04, 0x3e, 0xe2,
+ 0x16, 0xb2, 0x1c, 0x6a, 0x95, 0xb2, 0xb9, 0x82, 0xdb, 0xda, 0x1e, 0x95,
+ 0x91, 0x1f, 0x9d, 0xfc, 0x95, 0x34, 0x77, 0x6d, 0x43, 0x59, 0x90, 0xb6,
+ 0x14, 0x58, 0x93, 0xce, 0xca, 0xb1, 0xdf, 0x09, 0xcf, 0x69, 0x6b, 0x15,
+ 0x60, 0x47, 0xa9, 0x7c, 0x03, 0xf4, 0xf8, 0xec, 0x87, 0x16, 0x49, 0xc0,
+ 0x97, 0x7e, 0x58, 0xd4, 0x5b, 0x39, 0x90, 0x19, 0xc8, 0x92, 0x4f, 0x2b,
+ 0x1e, 0x4c, 0xfc, 0x26, 0xa2, 0x01, 0x98, 0xeb, 0xb7, 0x0f, 0x52, 0x84,
+ 0xa2, 0x7a, 0xfe, 0x6b, 0x97, 0xa7, 0xc6, 0x0b, 0xb8, 0x02, 0xe4, 0x45,
+ 0x19, 0x54, 0x2d, 0x4c, 0x57, 0x50, 0xa1, 0xeb, 0x5f, 0xc6, 0xb0, 0x0c,
+ 0xbf, 0xf0, 0x00, 0x5e, 0xf9, 0xae, 0x0b, 0x86, 0x85, 0xb3, 0xb1, 0x6f,
+ 0x5d, 0xf8, 0x58, 0x87, 0x62, 0x43, 0x82, 0x80, 0x9b, 0xcc, 0x10, 0x78,
+ 0x6d, 0x24, 0xd7, 0xa9, 0x1b, 0xc5, 0x77, 0x5a, 0xff, 0x3d, 0x9d, 0x0c,
+ 0x45, 0xb8, 0x6b, 0xc7, 0x95, 0xf1, 0xb9, 0x7b, 0xe7, 0x87, 0xf3, 0x10,
+ 0x55, 0x1b, 0x25, 0x41, 0x5c, 0xb5, 0x13, 0xe4, 0xf2, 0xba, 0xab, 0x37,
+ 0x6a, 0x1d, 0xca, 0x01, 0xe4, 0x4f, 0x7d, 0xcf, 0xad, 0xbc, 0xb5, 0xfc,
+ 0x98, 0x12, 0xac, 0x9c, 0x4a, 0xdd, 0x51, 0x88, 0x96, 0x0f, 0xf2, 0xed,
+ 0x58, 0xb3, 0x96, 0x48, 0xbe, 0xff, 0x42, 0x7a, 0x9e, 0x16, 0xc0, 0x1c,
+ 0xa9, 0xc7, 0x97, 0x97, 0x6d, 0xfa, 0xcf, 0x9c, 0xac, 0x5d, 0x4e, 0xb7,
+ 0x4c, 0xf1, 0x16, 0x9e, 0x7c, 0x2a, 0x06, 0x2e, 0x9b, 0x8b, 0x56, 0xa3,
+ 0x22, 0x35, 0x86, 0xec, 0x22, 0x33, 0xd1, 0xe0, 0xc2, 0x42, 0x53, 0x95,
+ 0x84, 0xfa, 0xf0, 0x2c, 0xa1, 0x2b, 0x6b, 0xc2, 0x35, 0x0e, 0x9a, 0x21,
+ 0x65, 0xb8, 0xd4, 0xc3, 0xb6, 0x7d, 0x2b, 0x46, 0xa7, 0x62, 0x6d, 0x70,
+ 0x95, 0xc9, 0xd1, 0xfe, 0x88, 0xd2, 0x3a, 0x35, 0x65, 0xa1, 0x8d, 0x16,
+ 0xad, 0x65, 0xde, 0x23, 0x4b, 0x65, 0xf3, 0xe8, 0x2d, 0x42, 0x79, 0xd3,
+ 0x71, 0x8f, 0x54, 0xf4, 0xd7, 0x80, 0x6a, 0x4d, 0x67, 0x26, 0x0d, 0x9b,
+ 0x9c, 0x39, 0x9c, 0x05, 0x3f, 0xe6, 0x23, 0xfd, 0xc0, 0x33, 0x85, 0x7c,
+ 0x42, 0x6f, 0x8b, 0x25, 0x6f, 0x0f, 0x26, 0xf6, 0x52, 0x91, 0xa3, 0x6b,
+ 0xbb, 0xe7, 0x4a, 0x26, 0x17, 0xd8, 0xbd, 0xcd, 0xd1, 0x44, 0xc2, 0xf4,
+ 0x33, 0xfd, 0xbd, 0x3f, 0x66, 0xe8, 0xfd, 0x04, 0xde, 0x37, 0x42, 0xb7,
+ 0xfd, 0xbc, 0x21, 0x17, 0x03, 0x0f, 0x49, 0x54, 0x1d, 0x1d, 0x02, 0xcc,
+ 0x93, 0xa3, 0x16, 0xf1, 0x9b, 0xac, 0xd1, 0x31, 0xdd, 0xe2, 0x34, 0xf1,
+ 0x62, 0xfc, 0xdf, 0xe2, 0xb3, 0x5b, 0x47, 0x78, 0x0b, 0x05, 0xd6, 0xe9,
+ 0xe9, 0xb4, 0x13, 0x46, 0x10, 0x38, 0xd1, 0xfb, 0x90, 0xe8, 0xa6, 0x82,
+ 0x40, 0x8a, 0x8d, 0x7f, 0xc7, 0xe5, 0x35, 0x96, 0xa0, 0xf5, 0x4d, 0x2b,
+ 0xe9, 0xbd, 0x0b, 0x08, 0x39, 0xf3, 0x66, 0xb1, 0xbc, 0x37, 0x0b, 0xe2,
+ 0x56, 0x40, 0x6e, 0x5b, 0x45, 0x73, 0x2a, 0xb8, 0xe0, 0x99, 0x5f, 0x9f,
+ 0x81, 0xad, 0xa5, 0x3f, 0xf3, 0x35, 0x5c, 0xc7, 0xb8, 0xa1, 0xd9, 0x7f,
+ 0x84, 0x84, 0x93, 0xbb, 0xbc, 0x57, 0xed, 0x98, 0xa4, 0xec, 0xb8, 0x1c,
+ 0xfa, 0xd9, 0xae, 0xbf, 0xe0, 0xc9, 0x4e, 0x9e, 0x3c, 0x9a, 0xc1, 0xb3,
+ 0x45, 0x05, 0x24, 0xab, 0x07, 0x20, 0x87, 0xc6, 0xfd, 0x84, 0xf9, 0x6c,
+ 0xfa, 0x5c, 0x9c, 0x09, 0x65, 0xa0, 0x8d, 0x3f, 0x07, 0x80, 0x46, 0x89,
+ 0x32, 0x07, 0xfe, 0xbc, 0xbd, 0xf4, 0xaf, 0x92, 0xe3, 0xee, 0x4a, 0xa6,
+ 0x00, 0xf8, 0x08, 0xec, 0x96, 0xae, 0x3c, 0x49, 0xa5, 0xf4, 0xd3, 0x5e,
+ 0x8e, 0x25, 0x80, 0x3a, 0xfe, 0xa9, 0x4b, 0x78, 0x40, 0x06, 0xbc, 0xf2,
+ 0x4a, 0x44, 0xe6, 0xa3, 0xfe, 0x94, 0x30, 0x7e, 0x72, 0x27, 0xb3, 0xed,
+ 0x9d, 0xca, 0xda, 0xd8, 0xd6, 0xca, 0x73, 0x3d, 0xbe, 0x97, 0xa2, 0xb1,
+ 0xe6, 0x8c, 0xd0, 0xe3, 0x68, 0x57, 0xa3, 0x89, 0x2b, 0xfa, 0x7f, 0x0a,
+ 0x8a, 0x93, 0x09, 0x99, 0x90, 0xf5, 0x6f, 0xbf, 0x79, 0x44, 0x18, 0xe9,
+ 0xb8, 0x40, 0x12, 0x7e, 0xf3, 0x61, 0xd0, 0x8b, 0x0c, 0xcb, 0x1f, 0x1a,
+ 0x5e, 0xb7, 0xf3, 0x81, 0x2e, 0x5e, 0x9e, 0x87, 0xc9, 0x74, 0x57, 0x42,
+ 0x63, 0xe1, 0xba, 0xd5, 0xc9, 0x29, 0x4d, 0xc0, 0x32, 0xbc, 0xeb, 0xba,
+ 0x81, 0x84, 0x21, 0x1e, 0xff, 0x37, 0xe8, 0x45, 0xa2, 0xa0, 0xdb, 0xb8,
+ 0x10, 0xba, 0xf2, 0xe6, 0x7a, 0xbf, 0x4a, 0x9f, 0xc9, 0xd4, 0x7e, 0x30,
+ 0xa2, 0x97, 0xe6, 0xf7, 0x1a, 0x74, 0xd9, 0xa5, 0xe3, 0xa2, 0x50, 0xe3,
+ 0x04, 0xae, 0x4e, 0x49, 0x12, 0x65, 0xc5, 0xf3, 0x58, 0xc7, 0xbc, 0x6a,
+ 0x14, 0x7b, 0x9e, 0x3f, 0x81, 0xbb, 0x21, 0xb2, 0x0e, 0x26, 0x17, 0x32,
+ 0xc7, 0x4e, 0xd8, 0x1e, 0x96, 0x83, 0x54, 0x4f, 0x35, 0xee, 0x21, 0x6c,
+ 0x5a, 0xdc, 0x92, 0xe3, 0x96, 0x53, 0x71, 0x89, 0x44, 0xd1, 0x8f, 0x4f,
+ 0xc8, 0xb4, 0x9b, 0x31, 0x5c, 0xff, 0xf0, 0x0f, 0xf3, 0xea, 0x4e, 0xb8,
+ 0x67, 0x0f, 0x1e, 0xa6, 0x55, 0xcb, 0xa5, 0xfd, 0x3a, 0x84, 0x80, 0xd3,
+ 0x1c, 0x2e, 0x15, 0xd5, 0x17, 0x4a, 0xb2, 0x1a, 0xb9, 0x34, 0x77, 0x79,
+ 0xac, 0x7b, 0x15, 0x43, 0x6d, 0x1a, 0x71, 0x08, 0xe5, 0x8e, 0x78, 0xb0,
+ 0x4e, 0x1f, 0xd5, 0x6c, 0xae, 0x9a, 0x2b, 0xdf, 0x2f, 0xd6, 0x5b, 0x5b,
+ 0x28, 0xac, 0xed, 0xb3, 0x9f, 0xc1, 0x38, 0xd7, 0xfc, 0xd8, 0xd0, 0x23,
+ 0x51, 0xa9, 0x3d, 0x7d, 0xd4, 0x8a, 0x85, 0x8c, 0x24, 0x65, 0x82, 0x91,
+ 0xa0, 0x52, 0x4b, 0xa5, 0x09, 0xe7, 0x0b, 0x88, 0xf6, 0xc0, 0x1f, 0xca,
+ 0x42, 0x37, 0xfc, 0xa6, 0xe0, 0x92, 0x41, 0x93, 0x06, 0x55, 0x65, 0x5e,
+ 0x47, 0x14, 0x72, 0xf1, 0x8f, 0x90, 0x02, 0x58, 0xc0, 0xca, 0xe0, 0xed,
+ 0x1e, 0x56, 0x4f, 0xf0, 0x48, 0xaf, 0x3e, 0x2e, 0xc1, 0x05, 0x6e, 0x12,
+ 0x01, 0x7e, 0xe0, 0xd5, 0x20, 0xb6, 0x5f, 0x35, 0x87, 0x78, 0xc5, 0x15,
+ 0x7e, 0x88, 0x5d, 0x0a, 0xcd, 0xc2, 0x94, 0x94, 0x60, 0xf8, 0x72, 0x40,
+ 0x3a, 0x63, 0xd4, 0x78, 0x5b, 0x05, 0x63, 0x9a, 0x1c, 0x35, 0x58, 0x88,
+ 0x4f, 0x09, 0x39, 0x35, 0xda, 0x06, 0xb0, 0xbf, 0x7c, 0x84, 0xc2, 0xaf,
+ 0xab, 0x12, 0x6d, 0xd7, 0x99, 0xef, 0xe1, 0xfe, 0x9a, 0x01, 0x9a, 0x1f,
+ 0x67, 0xd9, 0x2d, 0x61, 0xa7, 0xb0, 0x25, 0xd6, 0x20, 0x06, 0xbc, 0xb4,
+ 0xa3, 0xce, 0x8f, 0xd8, 0x90, 0xbe, 0x66, 0x48, 0x9f, 0xdc, 0x96, 0xcb,
+ 0x78, 0x04, 0x54, 0x2f, 0x2c, 0x2f, 0x28, 0x4d, 0x63, 0x85, 0x9c, 0x49,
+ 0xfe, 0xd7, 0x77, 0x7e, 0x34, 0x70, 0x6f, 0xd9, 0xdc, 0x18, 0xa1, 0x66,
+ 0xc4, 0x16, 0x33, 0xf7, 0x07, 0xd6, 0xb4, 0xcc, 0xe4, 0x63, 0x67, 0x82,
+ 0xd5, 0xf7, 0xfe, 0x35, 0xe4, 0x0f, 0xff, 0x1a, 0x00, 0x64, 0x4e, 0x8c,
+ 0xf0, 0xdf, 0xc0, 0xf3, 0xe5, 0x3d, 0xad, 0x0d, 0x8e, 0x98, 0x5f, 0x4e,
+ 0x10, 0x6f, 0xbe, 0x51, 0xd2, 0xb4, 0xf5, 0xa0, 0xff, 0xa5, 0x73, 0x7d,
+ 0x99, 0xc6, 0x99, 0xe5, 0xce, 0x40, 0x3b, 0xb1, 0xf5, 0x86, 0x3a, 0xaf,
+ 0x3f, 0x5f, 0x12, 0xa7, 0xff, 0xa3, 0x85, 0xf4, 0xeb, 0xa7, 0xfb, 0xb0,
+ 0x66, 0x02, 0x5f, 0x64, 0x70, 0xba, 0x5a, 0xa2, 0x92, 0x41, 0x9b, 0x49,
+ 0x64, 0x3a, 0x26, 0xd9, 0x55, 0xc3, 0x30, 0xf0, 0x13, 0x54, 0xde, 0xc1,
+ 0xaf, 0xfd, 0x48, 0xe8, 0x60, 0xd5, 0x8c, 0x24, 0x3a, 0xd9, 0xfe, 0xed,
+ 0x4e, 0xda, 0xda, 0xcf, 0x4e, 0xdf, 0x05, 0x46, 0xaa, 0x88, 0x43, 0xa5,
+ 0x8d, 0xd0, 0x6f, 0xc1, 0xfe, 0x50, 0xa5, 0x45, 0xfd, 0xc5, 0x56, 0x67,
+ 0x1c, 0x45, 0x69, 0x05, 0x2a, 0x7b, 0xb0, 0xa8, 0x7c, 0x80, 0x6d, 0x2f,
+ 0xcd, 0x2b, 0xf7, 0x3b, 0xca, 0xee, 0x75, 0x4b, 0xb1, 0xf1, 0x69, 0xe9,
+ 0x5d, 0x59, 0xd0, 0x4d, 0xb7, 0x2e, 0xd4, 0xc7, 0x30, 0x1c, 0x81, 0x5b,
+ 0x57, 0x20, 0x2e, 0xf9, 0x9f, 0x21, 0x10, 0x58, 0x9c, 0x80, 0x9e, 0x40,
+ 0xa5, 0x5a, 0xab, 0x21, 0x5f, 0x2c, 0xce, 0xfb, 0xb3, 0xf3, 0x07, 0x69,
+ 0xd0, 0x92, 0xfb, 0xa1, 0x06, 0x27, 0xd3, 0x29, 0x1e, 0xb3, 0x61, 0xbc,
+ 0xca, 0x9f, 0x85, 0x80, 0x79, 0xf6, 0xcc, 0x6b, 0x62, 0x42, 0x3b, 0x74,
+ 0xa4, 0x0a, 0x53, 0x59, 0x01, 0x54, 0x03, 0x17, 0x19, 0xb6, 0xa8, 0x06,
+ 0xd1, 0xc9, 0xe2, 0x9f, 0x9c, 0x77, 0x62, 0xa7, 0x12, 0x08, 0x01, 0xe8,
+ 0x2c, 0xd7, 0xaf, 0xb0, 0x5b, 0x4f, 0x29, 0x0a, 0xa0, 0x30, 0x84, 0x0a,
+ 0xe0, 0xf3, 0x85, 0xe8, 0x8e, 0xa4, 0x7e, 0xa9, 0x0b, 0x72, 0xac, 0x63,
+ 0xde, 0x19, 0xfd, 0x4d, 0x95, 0xb2, 0x3b, 0xa8, 0x45, 0x8d, 0xe2, 0x83,
+ 0xf6, 0xcd, 0x16, 0xb3, 0x46, 0x1e, 0x56, 0x96, 0x42, 0x5b, 0xf2, 0x2a,
+ 0xda, 0xed, 0xf9, 0x92, 0x21, 0xb3, 0xf0, 0x2f, 0xc2, 0x97, 0x18, 0xbf,
+ 0xa9, 0x26, 0xb6, 0x39, 0x59, 0xf1, 0x46, 0x49, 0x64, 0xf4, 0xb7, 0x7e,
+ 0x87, 0x09, 0x9a, 0x73, 0x5e, 0x10, 0x47, 0x19, 0xf4, 0x58, 0xc2, 0xff,
+ 0x81, 0x61, 0xe1, 0xff, 0x6b, 0xe3, 0xb9, 0xb4, 0x9a, 0x19, 0x71, 0xa7,
+ 0x87, 0x3d, 0x76, 0x6a, 0xbf, 0x4e, 0x89, 0x72, 0x45, 0xdc, 0x6a, 0x44,
+ 0x79, 0xf8, 0x76, 0x10, 0x4d, 0x53, 0xde, 0x56, 0x42, 0x9a, 0x47, 0xd9,
+ 0x13, 0x72, 0xf7, 0xaa, 0xe9, 0x84, 0x4f, 0x2d, 0x4d, 0xb7, 0x5f, 0xda,
+ 0x3f, 0x8f, 0x99, 0xbc, 0x77, 0x71, 0xc9, 0xbf, 0xfa, 0xb4, 0x65, 0x51,
+ 0x16, 0x9c, 0x3f, 0x04, 0x62, 0x6f, 0x45, 0x46, 0x7b, 0xcc, 0xf6, 0xbc,
+ 0xc2, 0x0d, 0xd6, 0x92, 0xe3, 0x29, 0xd4, 0xa5, 0x1c, 0x06, 0xc2, 0xf2,
+ 0xed, 0x4f, 0xf0, 0x5e, 0x55, 0xe2, 0xae, 0x9a, 0x68, 0x67, 0x7a, 0x79,
+ 0x66, 0xd4, 0x9d, 0xb0, 0x35, 0x8e, 0x79, 0x95, 0xf3, 0xca, 0xaf, 0x47,
+ 0xad, 0x84, 0x14, 0x62, 0xcf, 0xe0, 0xe4, 0x09, 0xe1, 0xed, 0xfc, 0xbf,
+ 0x6a, 0x06, 0x72, 0xc7, 0x0a, 0x53, 0x41, 0x93, 0xaf, 0x27, 0x33, 0xfb,
+ 0xd3, 0x68, 0xed, 0x5c, 0x13, 0x23, 0xce, 0xda, 0x62, 0xf3, 0xfe, 0xf2,
+ 0x91, 0x60, 0x0e, 0xb0, 0x6f, 0x43, 0x34, 0x2e, 0x56, 0xb8, 0xbb, 0x3f,
+ 0x52, 0xf3, 0x4d, 0x3f, 0x97, 0xfb, 0xab, 0x88, 0xc1, 0x08, 0xb3, 0xc7,
+ 0x72, 0x6b, 0xc1, 0x49, 0x63, 0xc2, 0xb7, 0x05, 0x00, 0x47, 0x53, 0xf0,
+ 0x22, 0xca, 0x47, 0xa4, 0x7c, 0x89, 0xff, 0x4d, 0x84, 0xe5, 0x90, 0xf1,
+ 0xd3, 0x61, 0x71, 0x35, 0xdf, 0xe9, 0x43, 0x8e, 0x20, 0x8d, 0xd4, 0xe6,
+ 0x74, 0xf7, 0xe8, 0x98, 0x17, 0xcd, 0xfd, 0xd7, 0x32, 0xa1, 0x5a, 0xd1,
+ 0x27, 0x30, 0xfe, 0x22, 0x05, 0x1e, 0xd2, 0xff, 0x1b, 0x23, 0x42, 0x33,
+ 0xf5, 0xa9, 0xbc, 0x43, 0xe9, 0xa5, 0x55, 0xf1, 0x51, 0x5e, 0xc6, 0xd6,
+ 0xac, 0x38, 0x06, 0x84, 0xe9, 0x75, 0x5f, 0xd7, 0xa2, 0xf9, 0xc2, 0xf3,
+ 0x6b, 0x0e, 0x5b, 0x01, 0x56, 0x60, 0x6e, 0xbe, 0x60, 0xaf, 0x96, 0x77,
+ 0xc8, 0x1f, 0x88, 0x9e, 0x19, 0x53, 0x48, 0x6d, 0x37, 0xd6, 0xc2, 0x8d,
+ 0x88, 0x1f, 0x39, 0x61, 0xcd, 0xea, 0x0e, 0x6a, 0x39, 0x62, 0xcd, 0xac,
+ 0x8d, 0xfd, 0x33, 0x1e, 0x1e, 0x6c, 0xfd, 0x1f, 0xd1, 0x8e, 0x14, 0x11,
+ 0x80, 0x9d, 0x29, 0x77, 0xc6, 0x6b, 0x12, 0xb6, 0xa9, 0x2b, 0xf0, 0xe7,
+ 0xa5, 0x4e, 0x47, 0x07, 0xc3, 0x34, 0xb1, 0xa1, 0xa0, 0x6a, 0x4a, 0x55,
+ 0xac, 0x01, 0x2d, 0x7d, 0x38, 0xd0, 0x41, 0xbe, 0x2a, 0xe9, 0xaa, 0xdd,
+ 0xc1, 0x39, 0xb7, 0xfb, 0xb4, 0x76, 0x99, 0xc1, 0xcc, 0x29, 0x06, 0x15,
+ 0x71, 0x7a, 0xb2, 0xdd, 0x82, 0x25, 0x13, 0xfa, 0xa7, 0x16, 0xeb, 0x22,
+ 0x3b, 0xa8, 0x4c, 0x7f, 0x62, 0xd8, 0x23, 0x0e, 0xca, 0x3b, 0x1b, 0x6d,
+ 0x21, 0x99, 0x2c, 0xd0, 0xa2, 0x54, 0x31, 0x58, 0x52, 0xc1, 0xff, 0xac,
+ 0x6c, 0xf6, 0xa2, 0x8f, 0x07, 0x5e, 0x17, 0x17, 0x7e, 0x52, 0x02, 0x4d,
+ 0xfc, 0x2f, 0xa0, 0x56, 0x48, 0xea, 0xb3, 0x84, 0x60, 0x9f, 0xd5, 0x59,
+ 0x98, 0x79, 0x8a, 0xa6, 0xe6, 0x4e, 0xd5, 0x2c, 0x3e, 0x53, 0x30, 0x1a,
+ 0x16, 0x5a, 0xfe, 0x15, 0xe3, 0x6d, 0xc5, 0xc5, 0x39, 0x3e, 0x28, 0xae,
+ 0xf5, 0x59, 0xfd, 0x5b, 0xb3, 0x34, 0x05, 0x92, 0x2a, 0xd5, 0xdd, 0xa9,
+ 0x58, 0xd7, 0x1a, 0xa5, 0xd9, 0xd3, 0xaf, 0xf2, 0xf0, 0xff, 0x91, 0xda,
+ 0x2d, 0x9c, 0xcc, 0xdb, 0xc8, 0xfd, 0xd7, 0x10, 0x31, 0x2b, 0xa9, 0xfd,
+ 0xb7, 0xc6, 0x5d, 0x83, 0x1f, 0xcd, 0xcc, 0x1c, 0xcb, 0x6a, 0xb5, 0xba,
+ 0x4d, 0xdf, 0x4c, 0xe0, 0x4e, 0x53, 0x5c, 0x34, 0x06, 0x35, 0xd3, 0x1f,
+ 0x43, 0xf5, 0xe3, 0x66, 0xb4, 0x7c, 0xb6, 0x06, 0x06, 0xe5, 0xab, 0x3b,
+ 0x12, 0x9e, 0x47, 0x7c, 0xe9, 0x91, 0xf2, 0x34, 0x47, 0x7c, 0xdb, 0xd4,
+ 0xf2, 0xec, 0xf1, 0x70, 0xff, 0x71, 0xf5, 0xf4, 0x24, 0x16, 0x61, 0xf7,
+ 0xa9, 0xb1, 0x12, 0x4d, 0xa6, 0x81, 0xf6, 0x4d, 0x6c, 0xd9, 0x98, 0xbb,
+ 0x66, 0x8f, 0x77, 0x8a, 0x02, 0xde, 0xf4, 0xf6, 0x10, 0xb4, 0xcc, 0xfb,
+ 0x0f, 0x69, 0x94, 0xfb, 0x37, 0x2a, 0x22, 0x26, 0x64, 0xfd, 0xc3, 0x85,
+ 0x30, 0xfb, 0x8f, 0x48, 0x8d, 0xc6, 0x5b, 0xc4, 0xee, 0xc5, 0x9a, 0x7d,
+ 0x57, 0x2d, 0x26, 0xd8, 0x7f, 0x37, 0xb7, 0xab, 0xe6, 0x55, 0x9a, 0x8f,
+ 0x30, 0xd0, 0x85, 0x6d, 0x44, 0x23, 0x8b, 0x9c, 0xc6, 0xdd, 0xd7, 0xa6,
+ 0xc5, 0xca, 0x99, 0x63, 0x8b, 0xd8, 0x0e, 0x50, 0x79, 0xce, 0xaa, 0xb8,
+ 0x62, 0x8c, 0x3d, 0x8b, 0x0b, 0xf1, 0xdc, 0x38, 0x88, 0x76, 0x82, 0xb7,
+ 0x47, 0x69, 0x72, 0xba, 0x9a, 0x4b, 0x20, 0x59, 0x88, 0x65, 0x02, 0x0c,
+ 0x9d, 0x6b, 0x40, 0x98, 0x8d, 0xd7, 0x83, 0xd5, 0xf0, 0xc3, 0x87, 0xca,
+ 0xdb, 0x7e, 0x96, 0x88, 0x64, 0x49, 0x16, 0x64, 0x08, 0xaa, 0x4b, 0xe0,
+ 0x30, 0x4d, 0xe9, 0xe8, 0x3e, 0x10, 0xcb, 0x18, 0x26, 0x13, 0x05, 0x1d,
+ 0x8e, 0xb7, 0x5f, 0xd9, 0x08, 0xca, 0xf8, 0x1e, 0x02, 0xf4, 0x35, 0x69,
+ 0xf2, 0xf1, 0x65, 0x50, 0xde, 0x9c, 0x93, 0x51, 0xb6, 0xa2, 0xc4, 0xa0,
+ 0x57, 0x58, 0x11, 0x9f, 0xce, 0xa0, 0x33, 0xb1, 0x3a, 0x02, 0x1a, 0x7f,
+ 0x15, 0x9d, 0x4a, 0x30, 0x66, 0x3c, 0xd2, 0xd4, 0xef, 0x94, 0xf4, 0x1b,
+ 0x0c, 0x33, 0x1e, 0x3e, 0x1c, 0x99, 0x30, 0xaf, 0x1e, 0x3d, 0x55, 0xc8,
+ 0x28, 0x87, 0xbd, 0x3a, 0x7a, 0xb5, 0xb7, 0x1c, 0x5c, 0xdc, 0xf1, 0xaf,
+ 0x5c, 0x16, 0x35, 0x81, 0x72, 0x8a, 0x95, 0x61, 0x13, 0x19, 0x77, 0x9f,
+ 0x45, 0xf2, 0x74, 0x66, 0x11, 0x8f, 0x91, 0x34, 0x32, 0x5e, 0xd9, 0x8a,
+ 0x0b, 0xad, 0x55, 0x66, 0x54, 0x88, 0xb0, 0x0a, 0xd2, 0xb0, 0x6e, 0x29,
+ 0xd2, 0x1d, 0xb6, 0x7a, 0x94, 0xd0, 0xbd, 0xf7, 0x37, 0x31, 0x04, 0xd3,
+ 0x79, 0x5c, 0xd7, 0x05, 0x5f, 0x29, 0x9d, 0xc1, 0x39, 0x17, 0xf6, 0xaf,
+ 0xfe, 0x2a, 0x26, 0xbe, 0x10, 0x4e, 0x57, 0x2e, 0xcb, 0x7b, 0x2b, 0x0f,
+ 0xd0, 0x27, 0x87, 0xab, 0xbc, 0x73, 0x0e, 0x3e, 0x21, 0x73, 0x81, 0xf1,
+ 0xf8, 0xd4, 0xcb, 0x72, 0xce, 0x81, 0xcc, 0x5d, 0x80, 0x32, 0x9f, 0xe0,
+ 0x25, 0x50, 0x1a, 0x3f, 0xe7, 0xa1, 0xb1, 0x00, 0xd7, 0x07, 0x02, 0x11,
+ 0x4a, 0x56, 0xe0, 0x95, 0x67, 0xb7, 0x7a, 0x7e, 0xcb, 0x2f, 0x76, 0xf0,
+ 0x7f, 0xff, 0xf3, 0x22, 0xf3, 0xd8, 0xac, 0x12, 0xc1, 0xea, 0x79, 0x55,
+ 0xe2, 0x28, 0x2f, 0xf9, 0x44, 0xdf, 0x31, 0xb8, 0x8e, 0xa9, 0x5f, 0x15,
+ 0x2b, 0x4a, 0x00, 0x82, 0x7e, 0xf3, 0xf7, 0xda, 0x27, 0x0e, 0x8d, 0x4f,
+ 0x3f, 0x06, 0x03, 0x33, 0xd2, 0xab, 0xd6, 0xb5, 0x79, 0xbe, 0x5e, 0x0d,
+ 0x10, 0xb1, 0x62, 0xb7, 0xe6, 0x24, 0xb2, 0x9a, 0xa6, 0xe1, 0x97, 0x5f,
+ 0xad, 0x5f, 0x2b, 0xdf, 0x10, 0xf9, 0x12, 0xe6, 0x6a, 0xc1, 0x7e, 0xac,
+ 0x1a, 0x13, 0x3b, 0xe0, 0xe8, 0x23, 0x39, 0x86, 0x9c, 0x5d, 0x27, 0xb8,
+ 0x3e, 0x25, 0xbe, 0x44, 0xcf, 0x2f, 0xcf, 0x7c, 0x41, 0xb5, 0x33, 0xbd,
+ 0xf0, 0x61, 0x6a, 0x93, 0x3f, 0x13, 0x82, 0xa5, 0xde, 0xdc, 0x8d, 0xd4,
+ 0x3e, 0xcd, 0xb8, 0xdb, 0x2b, 0xa8, 0xc9, 0xcd, 0x93, 0xe4, 0xe0, 0x43,
+ 0x50, 0x13, 0x78, 0x67, 0x7f, 0xe5, 0xe0, 0xf9, 0xb6, 0x8a, 0xc1, 0xb2,
+ 0xb2, 0xa6, 0xda, 0x4f, 0xe0, 0x1a, 0x9a, 0x2a, 0x1b, 0x77, 0xd6, 0x5a,
+ 0x21, 0x9e, 0x37, 0xd4, 0xc1, 0x97, 0x73, 0x3c, 0xd0, 0xe4, 0x97, 0x38,
+ 0x5f, 0x7b, 0x67, 0x80, 0x8f, 0x2d, 0x13, 0xd3, 0xac, 0xe6, 0x3c, 0x4d,
+ 0x4b, 0x85, 0x47, 0x30, 0x9d, 0x7c, 0xa5, 0xde, 0xc8, 0x06, 0x27, 0xbd,
+ 0x80, 0x2c, 0xa6, 0x2c, 0x3d, 0x27, 0xe2, 0x01, 0xb5, 0x5a, 0x58, 0x8b,
+ 0x08, 0x77, 0xcb, 0xb4, 0xfe, 0xe8, 0x61, 0x63, 0xe4, 0x54, 0xbb, 0xb7,
+ 0xb1, 0xdb, 0xcf, 0xa2, 0xf3, 0x14, 0xa2, 0x45, 0xc7, 0x69, 0xc3, 0x59,
+ 0xb2, 0x87, 0xab, 0x1d, 0x9d, 0x1a, 0xaf, 0x49, 0x5c, 0x7c, 0x21, 0x1f,
+ 0xaf, 0x0f, 0x8e, 0x00, 0x67, 0xab, 0xb3, 0x33, 0xfc, 0x97, 0xc2, 0x2e,
+ 0xa8, 0x52, 0xae, 0x8a, 0x16, 0x33, 0x39, 0xc4, 0x73, 0xdf, 0xc7, 0xf3,
+ 0x63, 0x0b, 0x31, 0xf8, 0xb8, 0x57, 0x91, 0xcb, 0x0f, 0x0b, 0x74, 0x3a,
+ 0xb4, 0x3e, 0x4d, 0x6d, 0x25, 0x1b, 0xe8, 0x30, 0xac, 0xbe, 0x29, 0x03,
+ 0x12, 0x43, 0xca, 0x12, 0x56, 0x25, 0x4a, 0xad, 0x87, 0x93, 0x43, 0xb1,
+ 0x41, 0xac, 0xa7, 0x06, 0xf2, 0x15, 0xf5, 0x00, 0x84, 0xc5, 0xbe, 0x22,
+ 0xf8, 0x60, 0xa1, 0x38, 0x42, 0x8c, 0x25, 0xda, 0xd2, 0x5d, 0xbe, 0xa1,
+ 0xc4, 0xc0, 0xf8, 0x33, 0x4e, 0x59, 0xf8, 0xd7, 0x3c, 0xfd, 0x65, 0xa0,
+ 0xa1, 0xf8, 0xbb, 0xc6, 0xa3, 0x20, 0xf5, 0x71, 0x04, 0xb6, 0x79, 0x19,
+ 0x85, 0x77, 0xc2, 0x6e, 0x4c, 0x17, 0x21, 0x59, 0xa7, 0xe5, 0x3d, 0x26,
+ 0x42, 0xeb, 0xf0, 0x29, 0xba, 0xef, 0x97, 0x05, 0x63, 0xe9, 0x7f, 0x88,
+ 0x4f, 0x8d, 0x7d, 0xdf, 0x01, 0x6d, 0x9d, 0x1e, 0xf6, 0xe1, 0x11, 0xf8,
+ 0xf1, 0xfa, 0x15, 0x31, 0xfd, 0x50, 0xac, 0x29, 0xbf, 0x4e, 0x3f, 0xbb,
+ 0xa1, 0x20, 0x2e, 0x4f, 0x30, 0xbd, 0xb9, 0xed, 0x5b, 0xbf, 0x23, 0x59,
+ 0x5f, 0x2a, 0x3e, 0xd2, 0x05, 0x00, 0xf9, 0x4b, 0xc6, 0x48, 0xd8, 0xb0,
+ 0x91, 0xa2, 0x9f, 0x5b, 0xbd, 0xd7, 0xa6, 0x7f, 0x41, 0x49, 0x78, 0x94,
+ 0x15, 0x90, 0xe7, 0x58, 0xd1, 0x8a, 0xfa, 0x27, 0xfa, 0x1b, 0xe9, 0x52,
+ 0xf2, 0xa8, 0xc1, 0xc1, 0x48, 0x61, 0x2a, 0x2f, 0xdd, 0x9e, 0x52, 0x9a,
+ 0xa7, 0x9d, 0xe8, 0x23, 0xd9, 0x73, 0x8b, 0x09, 0x8c, 0x79, 0xb3, 0x48,
+ 0x32, 0xe0, 0x4e, 0xce, 0x37, 0x48, 0x61, 0x26, 0x79, 0x8e, 0x02, 0xeb,
+ 0xa0, 0xf8, 0xff, 0xda, 0xaa, 0x36, 0x8b, 0x4d, 0x29, 0x8b, 0x8d, 0x41,
+ 0xea, 0xbc, 0xdc, 0xde, 0x3a, 0x9b, 0x9e, 0xdf, 0xda, 0xcc, 0xc1, 0x42,
+ 0x16, 0x59, 0x5c, 0x2a, 0x83, 0x35, 0xa3, 0x1e, 0x86, 0x6c, 0x53, 0x43,
+ 0xe9, 0xa8, 0x31, 0xd1, 0xfa, 0x24, 0x57, 0x74, 0xad, 0x07, 0xc6, 0x63,
+ 0x07, 0x77, 0x71, 0x9c, 0xe3, 0x58, 0x1a, 0xc2, 0xb3, 0xe6, 0x88, 0x5c,
+ 0x45, 0x0f, 0xe2, 0x4c, 0xd2, 0x20, 0x29, 0x3b, 0xf5, 0xf4, 0x4e, 0x85,
+ 0x82, 0xaa, 0xfd, 0xf1, 0x44, 0xf3, 0x27, 0x89, 0x9e, 0x12, 0x87, 0x14,
+ 0xf6, 0x56, 0x48, 0x03, 0x81, 0x5e, 0x96, 0xff, 0xb2, 0x8e, 0x48, 0x7d,
+ 0xe7, 0x0f, 0xfd, 0x24, 0x9d, 0x37, 0xbc, 0xac, 0xd7, 0x08, 0xbd, 0xe3,
+ 0xe7, 0x7d, 0x80, 0x15, 0x35, 0xdc, 0x88, 0xbe, 0xfa, 0x40, 0xa6, 0x17,
+ 0x85, 0xfc, 0x04, 0xda, 0xfa, 0x7b, 0x6c, 0xe6, 0xb4, 0x8d, 0x1b, 0x7e,
+ 0x27, 0xbc, 0xa7, 0x02, 0xeb, 0x8d, 0xc5, 0xaf, 0xc8, 0xa7, 0x98, 0x2b,
+ 0x4f, 0xad, 0x22, 0xc2, 0xf1, 0x33, 0x2f, 0x2d, 0x3e, 0x8b, 0x8a, 0x1b,
+ 0xea, 0x55, 0xd2, 0xe4, 0x15, 0xb9, 0x96, 0x1e, 0x21, 0xb6, 0x30, 0xb3,
+ 0x72, 0x50, 0xbc, 0x24, 0x29, 0x4c, 0x37, 0x86, 0xbb, 0xe0, 0x0a, 0x92,
+ 0xea, 0x51, 0x2e, 0xa5, 0xf1, 0x7c, 0x94, 0xef, 0x1b, 0x55, 0x8e, 0xe5,
+ 0x40, 0x3b, 0xa2, 0xb3, 0x7c, 0x81, 0x0d, 0xb5, 0x20, 0x76, 0xc5, 0x99,
+ 0x39, 0xfb, 0x61, 0x0b, 0x4b, 0xf4, 0xac, 0xd2, 0x65, 0xb7, 0x20, 0xc0,
+ 0xab, 0x3f, 0xe6, 0x1c, 0x46, 0xcf, 0xdf, 0x84, 0xc2, 0x66, 0xd2, 0x9b,
+ 0x53, 0x32, 0x45, 0xcc, 0x89, 0xbb, 0x89, 0xe7, 0x3d, 0xa3, 0x9d, 0x75,
+ 0xdc, 0xba, 0x67, 0xd4, 0x7a, 0xe0, 0x00, 0x03, 0xfb, 0xd1, 0x6d, 0x10,
+ 0xc0, 0x62, 0x4a, 0xfd, 0xb7, 0x31, 0x0c, 0x01, 0x1c, 0xfc, 0x7f, 0x5f,
+ 0xcb, 0xfe, 0x78, 0x7f, 0xe5, 0x8a, 0x09, 0x5a, 0xc6, 0xd8, 0x97, 0xe7,
+ 0x62, 0xcd, 0xc7, 0x0c, 0xf0, 0x5e, 0x16, 0xfe, 0x18, 0x50, 0xed, 0x73,
+ 0xcc, 0xf7, 0x81, 0x32, 0x0f, 0xad, 0x06, 0xb9, 0x82, 0xa0, 0xe9, 0x49,
+ 0x10, 0x98, 0x3b, 0x5d, 0xf1, 0xaa, 0xbe, 0xb0, 0xb1, 0x11, 0x1b, 0xd0,
+ 0xaf, 0xd1, 0xe5, 0xc8, 0x35, 0x2e, 0x04, 0x88, 0x26, 0x63, 0x25, 0x38,
+ 0x77, 0x1e, 0x66, 0x98, 0x7f, 0x7c, 0x75, 0xe2, 0x13, 0xe6, 0x6b, 0x63,
+ 0x59, 0xd1, 0xd1, 0x65, 0x4a, 0x3a, 0x5a, 0xfe, 0xf8, 0x23, 0xab, 0xed,
+ 0x2d, 0x68, 0x78, 0x55, 0x8f, 0x69, 0x1e, 0x2a, 0x8f, 0xb2, 0xc2, 0xa4,
+ 0xe6, 0x8f, 0xc4, 0xd9, 0x4e, 0xf6, 0xe9, 0x86, 0x0f, 0x3b, 0x98, 0x27,
+ 0x3a, 0x32, 0x17, 0x76, 0xee, 0x3a, 0x48, 0xcb, 0x2b, 0x31, 0x16, 0xe0,
+ 0x46, 0x56, 0xe3, 0x95, 0x11, 0x59, 0xb7, 0xfa, 0x83, 0x2d, 0x40, 0xff,
+ 0x4f, 0x13, 0x07, 0x13, 0xac, 0x47, 0x20, 0xcb, 0xb4, 0x84, 0x63, 0x72,
+ 0x11, 0x5e, 0xee, 0xbe, 0x08, 0xc2, 0xe6, 0x48, 0xdd, 0x28, 0x7f, 0x9f,
+ 0x99, 0x57, 0x66, 0x38, 0x42, 0xd2, 0x95, 0x0c, 0x2a, 0x4e, 0xf2, 0x0e,
+ 0x58, 0xe5, 0x82, 0xb5, 0x34, 0xa5, 0xaf, 0x3b, 0x5a, 0x8e, 0x83, 0xc5,
+ 0x6d, 0xfd, 0x43, 0x3f, 0xa8, 0xbd, 0x65, 0xcd, 0x2b, 0xd2, 0xf1, 0xae,
+ 0x66, 0x13, 0x34, 0x27, 0x39, 0x89, 0x4b, 0x7a, 0xb3, 0x28, 0xbf, 0xfc,
+ 0x66, 0x63, 0x2b, 0x59, 0xaa, 0x81, 0xe1, 0xbe, 0xc6, 0x39, 0xb0, 0x5c,
+ 0xf6, 0x15, 0x60, 0x99, 0xfc, 0x45, 0x0a, 0xc4, 0x31, 0x70, 0xd9, 0xb1,
+ 0x13, 0x97, 0xff, 0x80, 0xd0, 0x3c, 0x3e, 0x22, 0x79, 0x75, 0xc4, 0x66,
+ 0xf5, 0xf3, 0xb3, 0xe6, 0x77, 0x8b, 0xef, 0x56, 0x78, 0x2c, 0x33, 0xa5,
+ 0x01, 0xe3, 0xcf, 0xa0, 0x30, 0x64, 0x32, 0x1d, 0x05, 0x9a, 0x21, 0x41,
+ 0x18, 0x22, 0x0f, 0x7b, 0x60, 0x00, 0xbb, 0xc5, 0x5a, 0xd8, 0x85, 0xc1,
+ 0xd7, 0x11, 0x87, 0x53, 0xbd, 0xa3, 0xae, 0xd5, 0x3a, 0xcf, 0x00, 0x40,
+ 0x64, 0x1f, 0xa0, 0x44, 0xbb, 0x60, 0x49, 0x77, 0x01, 0xba, 0x3a, 0xa2,
+ 0xad, 0xb1, 0x0d, 0x00, 0xc8, 0x76, 0xcc, 0xd2, 0xc0, 0x69, 0x2d, 0x95,
+ 0x05, 0xf7, 0xf6, 0xd1, 0x8c, 0x2b, 0xee, 0x89, 0xab, 0x7c, 0x31, 0x82,
+ 0xfb, 0x9b, 0x09, 0x83, 0x39, 0x18, 0x19, 0x05, 0x73, 0x36, 0xac, 0xaf,
+ 0x0a, 0x66, 0xb7, 0xe3, 0x0d, 0x98, 0x96, 0x2d, 0xc7, 0x93, 0xb3, 0x2f,
+ 0x89, 0xe2, 0xf7, 0xe5, 0x4a, 0x6a, 0x0f, 0x9f, 0x0e, 0xe9, 0x10, 0x77,
+ 0x8f, 0xc4, 0x40, 0xa6, 0x11, 0x7f, 0xb3, 0xb4, 0xc2, 0x3c, 0xd6, 0x4e,
+ 0xe9, 0xa2, 0xdc, 0x2a, 0x37, 0xe2, 0x2f, 0x7b, 0x41, 0x17, 0xe5, 0x7a,
+ 0x80, 0xb4, 0x4a, 0x32, 0x2d, 0x37, 0x24, 0xa3, 0x42, 0xb4, 0xba, 0x58,
+ 0x59, 0x2a, 0x5e, 0x36, 0x09, 0xa5, 0xed, 0x32, 0xcb, 0x57, 0x83, 0xe9,
+ 0xbb, 0xe5, 0x7d, 0x7d, 0x10, 0x6f, 0x82, 0xce, 0x3b, 0x01, 0x49, 0xe4,
+ 0x60, 0xfe, 0xfc, 0xd5, 0xdd, 0xa9, 0xb0, 0xad, 0x4a, 0x12, 0x97, 0x77,
+ 0x0e, 0xa5, 0x10, 0x39, 0xf8, 0xfb, 0x5c, 0xd7, 0xdc, 0x66, 0x39, 0x07,
+ 0xe5, 0xce, 0x4c, 0xa3, 0xcf, 0x59, 0x97, 0xbd, 0x00, 0xa4, 0x90, 0x8b,
+ 0x31, 0x2f, 0xca, 0x91, 0x74, 0xe1, 0x4c, 0xdd, 0x0e, 0x93, 0x85, 0x9a,
+ 0x91, 0x5a, 0xfb, 0x9e, 0x1c, 0x01, 0x0f, 0xcb, 0xce, 0xb2, 0xcb, 0xa3,
+ 0xc2, 0xc4, 0x11, 0x03, 0x0a, 0x1d, 0x88, 0x03, 0x1e, 0x3c, 0xdd, 0x85,
+ 0x55, 0x2e, 0xee, 0x3b, 0xc7, 0x80, 0xf6, 0x63, 0xe9, 0xfc, 0xad, 0x71,
+ 0x76, 0xf5, 0x94, 0x94, 0x53, 0xdf, 0x33, 0x82, 0x2a, 0x68, 0xf6, 0x92,
+ 0x27, 0x67, 0xb2, 0x4c, 0x48, 0x8c, 0x58, 0x1d, 0xde, 0x20, 0x71, 0x57,
+ 0x7b, 0xe4, 0x67, 0xd1, 0x00, 0x9b, 0x40, 0x5b, 0x40, 0xe1, 0x6e, 0x95,
+ 0x4f, 0x6c, 0xf5, 0x4d, 0xd3, 0xf9, 0xca, 0x1d, 0xfa, 0x86, 0x7d, 0x2b,
+ 0x53, 0x1a, 0x21, 0xa2, 0xef, 0x70, 0x69, 0x67, 0x16, 0xbc, 0x2a, 0x56,
+ 0x0f, 0x58, 0xab, 0x44, 0x02, 0xe3, 0x2e, 0x3a, 0x8b, 0x47, 0x3c, 0x52,
+ 0x68, 0x80, 0x8c, 0xef, 0x90, 0x81, 0x6a, 0x60, 0x6a, 0xb5, 0x07, 0xba,
+ 0x1a, 0x82, 0xad, 0x58, 0x59, 0xbb, 0x7b, 0xf3, 0x5b, 0x29, 0xac, 0x2a,
+ 0x31, 0xc2, 0x0f, 0x37, 0x87, 0xb6, 0xd7, 0xe9, 0xaf, 0x65, 0x8f, 0x46,
+ 0x22, 0xe4, 0x6b, 0xcc, 0x4e, 0xd0, 0xdb, 0x56, 0x84, 0x0b, 0x1c, 0xcf,
+ 0xaa, 0xe3, 0x6a, 0x07, 0xbb, 0x0e, 0x4e, 0xb0, 0x76, 0x07, 0x14, 0xc8,
+ 0x77, 0x3d, 0x77, 0x8a, 0xca, 0x2e, 0x85, 0x1b, 0xfe, 0x15, 0x79, 0xe0,
+ 0x57, 0xb2, 0xc6, 0x96, 0x8d, 0xd1, 0x59, 0x5a, 0x9f, 0x47, 0x56, 0xdf,
+ 0xb5, 0x0f, 0x25, 0x8c, 0x2b, 0xd1, 0xbd, 0xa2, 0x4a, 0x8b, 0xdd, 0xc5,
+ 0x7d, 0x52, 0xcb, 0x62, 0x93, 0x79, 0x15, 0xd5, 0x14, 0x03, 0xab, 0x1b,
+ 0x28, 0xc3, 0x9c, 0xd1, 0x95, 0x52, 0x36, 0x7f, 0x35, 0x91, 0xe8, 0xe8,
+ 0xb3, 0xc9, 0xd2, 0xe7, 0x87, 0x5f, 0xc4, 0x8c, 0xc1, 0x83, 0xab, 0x6f,
+ 0xcc, 0xfc, 0x5f, 0x4d, 0xf4, 0xfa, 0x82, 0x53, 0xb9, 0xa8, 0x5f, 0x45,
+ 0x57, 0x50, 0xd9, 0x26, 0x34, 0xd2, 0x4a, 0x8f, 0x98, 0x84, 0xc7, 0xc5,
+ 0x67, 0x63, 0xf8, 0x03, 0x93, 0x1f, 0x47, 0x6f, 0xb1, 0x45, 0xa5, 0xd4,
+ 0xff, 0xa3, 0xcc, 0xca, 0x14, 0x82, 0x94, 0xa1, 0x4f, 0xa3, 0x3d, 0xc5,
+ 0x71, 0xaa, 0x3b, 0xec, 0x1a, 0x7f, 0x4b, 0xc0, 0xfd, 0x00, 0xfd, 0x4b,
+ 0xe1, 0xbf, 0x32, 0xb1, 0x07, 0x8e, 0x84, 0x79, 0x78, 0x88, 0xd7, 0xac,
+ 0xe4, 0x11, 0x48, 0xdf, 0xb0, 0x63, 0x9d, 0xc1, 0x61, 0xe6, 0xdc, 0x78,
+ 0x53, 0x27, 0xa4, 0xb6, 0xb5, 0x61, 0xfd, 0x32, 0x85, 0x0c, 0xfe, 0xc7,
+ 0xfc, 0x99, 0x00, 0x91, 0xc3, 0xb7, 0x28, 0x2f, 0x56, 0xb4, 0x7b, 0xe1,
+ 0x33, 0xe8, 0x11, 0x6c, 0x2f, 0xba, 0x9c, 0xdb, 0x7a, 0x82, 0xbd, 0xab,
+ 0x28, 0xd2, 0x0e, 0xc9, 0x68, 0xfc, 0x66, 0x9d, 0x97, 0x85, 0x00, 0x7f,
+ 0xbc, 0x51, 0xd0, 0xcd, 0x97, 0x74, 0x66, 0x32, 0xb6, 0x34, 0xa2, 0x6c,
+ 0x8f, 0xb8, 0x51, 0xa5, 0xb6, 0xda, 0x57, 0x0d, 0x0b, 0xa7, 0xf5, 0x95,
+ 0x64, 0xd4, 0x5d, 0x9f, 0xe8, 0xfd, 0xd7, 0xd2, 0xf2, 0x54, 0x5f, 0x94,
+ 0x95, 0x92, 0xbd, 0xb4, 0x01, 0x61, 0x00, 0x71, 0x41, 0x72, 0x5f, 0x57,
+ 0xe0, 0xbe, 0x97, 0x4d, 0xdd, 0x22, 0xf4, 0xfa, 0x82, 0x77, 0xea, 0xf1,
+ 0x30, 0x10, 0x4c, 0x94, 0xc4, 0xf2, 0x35, 0x27, 0x75, 0x8d, 0x66, 0x82,
+ 0x7f, 0x79, 0x33, 0xca, 0x4b, 0x6d, 0x8d, 0x08, 0x32, 0x22, 0xc7, 0x59,
+ 0x66, 0x61, 0xb2, 0x48, 0xe9, 0x17, 0x82, 0xa2, 0x4a, 0x72, 0x3b, 0xff,
+ 0xcb, 0x90, 0xea, 0x7d, 0xb5, 0x2a, 0x08, 0x76, 0xcf, 0xc3, 0xf7, 0x0d,
+ 0x69, 0xaa, 0x05, 0x7d, 0x11, 0x64, 0xc5, 0x21, 0xe8, 0x28, 0x7d, 0x91,
+ 0x35, 0x1b, 0xb7, 0x09, 0xa9, 0x7e, 0x7e, 0x35, 0x77, 0x7c, 0xe1, 0xf5,
+ 0x31, 0xea, 0x20, 0xf9, 0x7d, 0xc9, 0xe1, 0x38, 0x99, 0xc6, 0x80, 0x96,
+ 0x1c, 0x5c, 0x10, 0xa5, 0x30, 0x3a, 0x41, 0x48, 0x40, 0x02, 0xba, 0x6a,
+ 0x0b, 0xec, 0x5c, 0xf3, 0xad, 0xd9, 0x86, 0x8b, 0xe3, 0xbd, 0xff, 0x7a,
+ 0x6c, 0x8e, 0x23, 0xdb, 0x71, 0x23, 0xec, 0x01, 0x00, 0xab, 0xce, 0x5b,
+ 0x98, 0x05, 0x5e, 0x32, 0x7c, 0x1d, 0x2b, 0x3e, 0xe8, 0x73, 0xeb, 0x92,
+ 0xbe, 0x48, 0xb3, 0x08, 0x66, 0x03, 0x07, 0xc0, 0x3f, 0xad, 0x25, 0xfc,
+ 0xbe, 0xdd, 0x6c, 0x4d, 0xdf, 0x13, 0x4b, 0xaa, 0xf8, 0xc0, 0xe4, 0x59,
+ 0xfb, 0x55, 0xb5, 0x5d, 0x28, 0x2d, 0x1d, 0x1a, 0x65, 0x2a, 0x23, 0x2e,
+ 0x44, 0xe5, 0x2c, 0xfd, 0x94, 0xe9, 0xd3, 0xa5, 0x94, 0x65, 0xcc, 0xcd,
+ 0x5b, 0x26, 0x1b, 0x04, 0x09, 0xa2, 0xf0, 0x4e, 0xfc, 0x87, 0x51, 0xcc,
+ 0x5a, 0x4e, 0x50, 0x41, 0x00, 0xa8, 0x46, 0x61, 0x71, 0x39, 0xdf, 0xc6,
+ 0x1b, 0xed, 0x8a, 0xe9, 0xf0, 0xcf, 0xea, 0xb7, 0x4f, 0x77, 0x8a, 0x2c,
+ 0x4e, 0xec, 0xcc, 0x2c, 0x3f, 0xdb, 0x91, 0x44, 0x3b, 0x03, 0xf2, 0x1c,
+ 0x2e, 0x0a, 0xfb, 0x58, 0x51, 0xef, 0x0b, 0x34, 0x76, 0x9b, 0x0f, 0x91,
+ 0xfe, 0xa1, 0x59, 0x7c, 0xaa, 0x71, 0xd7, 0x8a, 0xf2, 0x95, 0xd0, 0xfc,
+ 0x54, 0xaa, 0x1a, 0x65, 0x67, 0xb2, 0x29, 0x79, 0x33, 0x5d, 0x7f, 0xd5,
+ 0x46, 0xbb, 0x5f, 0x8d, 0x68, 0x77, 0x54, 0x4c, 0x7c, 0x5f, 0xb4, 0xa3,
+ 0x1b, 0x7c, 0xe3, 0x39, 0x20, 0x2f, 0xf4, 0x43, 0x9b, 0x76, 0xc2, 0x59,
+ 0xf3, 0x52, 0x68, 0xed, 0x15, 0x3a, 0x0b, 0x27, 0xf5, 0x86, 0x7a, 0x09,
+ 0xe8, 0xc7, 0x18, 0xc5, 0x65, 0x9a, 0xc5, 0xb4, 0x88, 0x8c, 0x83, 0x83,
+ 0xfc, 0x89, 0xd5, 0xf8, 0xd5, 0xb4, 0x31, 0x13, 0x8c, 0x26, 0x8a, 0x95,
+ 0x39, 0x4c, 0xab, 0xc5, 0x97, 0x2c, 0xee, 0x9b, 0xb1, 0x36, 0x42, 0x23,
+ 0x1b, 0x67, 0xd1, 0xde, 0xe5, 0xac, 0xe8, 0xbd, 0xea, 0x41, 0x0c, 0x31,
+ 0x88, 0x0e, 0x2a, 0xb6, 0x49, 0xf3, 0x81, 0xb3, 0xe2, 0x5f, 0xc7, 0x5f,
+ 0xbb, 0x8b, 0x71, 0xe4, 0x04, 0x6a, 0x30, 0x4c, 0xa3, 0x01, 0x54, 0xa1,
+ 0x56, 0xaa, 0x0b, 0xf1, 0xec, 0xd0, 0x36, 0x73, 0xaa, 0x0f, 0x7f, 0x5f,
+ 0xeb, 0x97, 0xff, 0x14, 0x1f, 0x21, 0x3f, 0x2c, 0xd5, 0xf1, 0xcd, 0x39,
+ 0x31, 0x0e, 0x91, 0xe4, 0x0e, 0xf5, 0x12, 0x40, 0xe5, 0x62, 0xa2, 0x59,
+ 0xb2, 0x66, 0xee, 0xaa, 0x00, 0xd9, 0x9a, 0x4f, 0xd8, 0x27, 0xc7, 0x5b,
+ 0x0c, 0x5a, 0x2d, 0x37, 0x3e, 0xfa, 0x64, 0x5f, 0x7b, 0xc8, 0xf3, 0xe1,
+ 0x84, 0x47, 0x19, 0xb1, 0x1c, 0x0e, 0xe7, 0x1c, 0x50, 0x40, 0x8f, 0x67,
+ 0xe8, 0x57, 0xb6, 0x47, 0xfc, 0x85, 0xcf, 0xa0, 0x3e, 0xc0, 0x14, 0x86,
+ 0x54, 0xaf, 0xdc, 0x3a, 0x40, 0x0c, 0x67, 0x40, 0xab, 0x69, 0x19, 0xcf,
+ 0xef, 0x3d, 0x8a, 0x13, 0x98, 0xd7, 0x91, 0xc5, 0x45, 0xee, 0x8a, 0xec,
+ 0x44, 0x69, 0x29, 0xb1, 0x99, 0x6f, 0xd7, 0x6a, 0x2e, 0x57, 0x8d, 0x48,
+ 0x3e, 0x86, 0x77, 0x7c, 0xde, 0x6d, 0xf7, 0x1f, 0xdc, 0x41, 0xcc, 0xf4,
+ 0xd9, 0x85, 0x50, 0x55, 0x0f, 0x82, 0x09, 0x38, 0x74, 0x17, 0xa8, 0xe5,
+ 0xe0, 0xd0, 0xea, 0x6c, 0x1a, 0x07, 0x83, 0x3f, 0xa9, 0x93, 0xea, 0xc7,
+ 0xc5, 0xa6, 0x3d, 0xb4, 0x6e, 0x3c, 0x09, 0x62, 0x73, 0xf8, 0x73, 0x9f,
+ 0x94, 0x46, 0x68, 0xf0, 0x52, 0x4a, 0xf7, 0x07, 0xa4, 0x62, 0x5f, 0xaa,
+ 0x90, 0x46, 0x06, 0x86, 0xdf, 0x31, 0x7e, 0x4f, 0x8e, 0x89, 0xdc, 0xfb,
+ 0xbc, 0x48, 0x23, 0x3c, 0x14, 0x30, 0xfb, 0xfc, 0x40, 0xa1, 0x73, 0xdf,
+ 0x87, 0x72, 0xe4, 0x2c, 0x29, 0xf8, 0xd5, 0xa5, 0x9c, 0x00, 0xcf, 0x09,
+ 0x2d, 0xc0, 0xec, 0x7d, 0x55, 0x4c, 0xa9, 0x14, 0x79, 0x09, 0x48, 0xd2,
+ 0x00, 0xb9, 0x07, 0x35, 0xf4, 0xa6, 0x07, 0x4e, 0x00, 0x0f, 0x37, 0x74,
+ 0xf3, 0x10, 0x98, 0x47, 0x9a, 0x38, 0xf5, 0xf7, 0xe2, 0xa3, 0xda, 0xef,
+ 0xba, 0xf6, 0x0d, 0x9e, 0x84, 0xdf, 0x44, 0xe8, 0xf4, 0x66, 0x58, 0x5f,
+ 0xfa, 0x77, 0x4e, 0xd2, 0x7c, 0x6b, 0xcd, 0xed, 0xdd, 0x64, 0xee, 0xc2,
+ 0x53, 0xe2, 0x47, 0x59, 0xd0, 0xe4, 0xa4, 0x9f, 0x62, 0x97, 0xa4, 0xe0,
+ 0x99, 0xcf, 0x9c, 0x8e, 0x2a, 0x78, 0x27, 0x55, 0xa9, 0x12, 0x40, 0x7b,
+ 0x12, 0xfa, 0x98, 0xd0, 0x1f, 0x6e, 0x98, 0x15, 0x15, 0x15, 0xf6, 0x09,
+ 0x9c, 0xe1, 0x07, 0x1e, 0x2d, 0xc7, 0xd8, 0x5b, 0x52, 0x57, 0xa0, 0xd1,
+ 0x7f, 0x07, 0xfd, 0x00, 0x5a, 0x93, 0x09, 0xcb, 0x3a, 0xc7, 0xe0, 0x54,
+ 0x32, 0xe7, 0xaf, 0xdc, 0x42, 0x44, 0x44, 0x1c, 0x10, 0xd9, 0x8b, 0x38,
+ 0x5c, 0x3c, 0x6c, 0x60, 0xd4, 0x87, 0x7b, 0x11, 0x37, 0x67, 0x5f, 0x85,
+ 0x8b, 0xba, 0x9d, 0x99, 0x73, 0xf1, 0xf6, 0x2a, 0x8b, 0xbe, 0x4b, 0xba,
+ 0x83, 0x54, 0xaf, 0xfd, 0xa9, 0xbd, 0x84, 0x06, 0x54, 0xeb, 0x27, 0xd0,
+ 0xec, 0xac, 0x1b, 0x7e, 0x16, 0xcd, 0xb7, 0x1e, 0xee, 0xe3, 0x99, 0x21,
+ 0xc4, 0xf2, 0xc2, 0x5f, 0xe7, 0x9e, 0x7d, 0xa5, 0x8e, 0x6d, 0xb7, 0xcc,
+ 0xcf, 0xc9, 0x4c, 0xdf, 0x67, 0x16, 0x03, 0xd4, 0x54, 0x7c, 0xdf, 0x2b,
+ 0xb4, 0xab, 0xf8, 0x2c, 0x4b, 0xed, 0x20, 0xec, 0x1d, 0x08, 0xff, 0xa9,
+ 0xb1, 0x9f, 0xc5, 0xa9, 0x7f, 0xaa, 0x4a, 0xb5, 0xd3, 0xea, 0x83, 0x51,
+ 0xb9, 0x11, 0xe7, 0xf9, 0x43, 0xec, 0x2f, 0x2c, 0x51, 0x31, 0xa3, 0x77,
+ 0x28, 0x06, 0x2d, 0x2b, 0x96, 0x2a, 0xcb, 0x6d, 0xec, 0x65, 0x69, 0x23,
+ 0xe9, 0x01, 0xce, 0x5a, 0xde, 0xfa, 0xa1, 0xa6, 0x88, 0x4d, 0x0b, 0x1c,
+ 0xfc, 0x29, 0x00, 0xb3, 0xfb, 0xb6, 0x4c, 0xef, 0x16, 0xda, 0xa6, 0x94,
+ 0xa6, 0x90, 0x9d, 0x91, 0xdf, 0xef, 0x85, 0x7f, 0xf5, 0xcf, 0xb3, 0x84,
+ 0xda, 0xf9, 0x17, 0x73, 0x90, 0x73, 0x43, 0x60, 0x22, 0x82, 0x11, 0xee,
+ 0xc5, 0x07, 0x3e, 0x2a, 0x2f, 0xb7, 0x26, 0x85, 0x78, 0xb7, 0xc1, 0xfd,
+ 0xef, 0xb2, 0x9f, 0xd3, 0x93, 0xed, 0x07, 0xde, 0xb3, 0xf1, 0x8c, 0x42,
+ 0x12, 0xfd, 0x58, 0xde, 0x74, 0x44, 0xd2, 0x97, 0xe6, 0xc8, 0x33, 0x50,
+ 0xb9, 0xee, 0xdb, 0x5b, 0x1e, 0x9e, 0x5d, 0xcd, 0x54, 0x9e, 0x72, 0xc1,
+ 0x29, 0xaa, 0xa1, 0xf5, 0x59, 0x92, 0x68, 0xea, 0x76, 0x00, 0x9d, 0x9b,
+ 0x1f, 0x1b, 0x2f, 0x10, 0xa1, 0x19, 0x39, 0x56, 0xf7, 0xa3, 0x7c, 0xd4,
+ 0x99, 0xa7, 0x9f, 0x4f, 0x19, 0x14, 0xec, 0xb2, 0xb9, 0x37, 0x0d, 0x98,
+ 0xda, 0x47, 0x3a, 0x40, 0x17, 0x81, 0x7a, 0x6a, 0x31, 0x2a, 0x42, 0x22,
+ 0xe3, 0xbe, 0xad, 0x4c, 0x00, 0x1f, 0x3f, 0x93, 0x1b, 0xd9, 0x67, 0xd6,
+ 0x8f, 0x0e, 0x87, 0x6b, 0x06, 0xb7, 0x85, 0xe0, 0x7f, 0x9e, 0xa9, 0x31,
+ 0xc6, 0xc5, 0xf7, 0x70, 0x82, 0x47, 0x4a, 0xf2, 0x6a, 0x37, 0x81, 0x9d,
+ 0x17, 0xe3, 0x26, 0xd6, 0x8a, 0xf5, 0xfe, 0x45, 0x8b, 0xe2, 0xc2, 0x7e,
+ 0xa0, 0x6a, 0x4a, 0x5a, 0xe7, 0x7e, 0x90, 0x76, 0xad, 0x60, 0x2a, 0x3c,
+ 0x8d, 0xd2, 0xbb, 0xe1, 0x13, 0xd4, 0x36, 0x54, 0x62, 0xcf, 0x7a, 0xbe,
+ 0x62, 0x81, 0x7d, 0xd2, 0x7a, 0x22, 0x79, 0xc4, 0x94, 0x14, 0x52, 0xf6,
+ 0x09, 0xa7, 0xfd, 0xd7, 0xca, 0xc8, 0x71, 0x92, 0x2a, 0xb5, 0x03, 0x24,
+ 0xbd, 0x22, 0xa9, 0x6e, 0x7b, 0xf8, 0xe4, 0x40, 0x0f, 0x6a, 0xa3, 0x7d,
+ 0xc5, 0x67, 0x4c, 0xdd, 0xb6, 0x1d, 0xd4, 0xf4, 0x61, 0xb3, 0x9a, 0xa5,
+ 0xbc, 0x37, 0xa7, 0x7a, 0x54, 0xa2, 0xef, 0xe0, 0xcf, 0xe7, 0xf7, 0xe8,
+ 0x34, 0x67, 0x49, 0x71, 0x4f, 0x0c, 0x00, 0x29, 0x87, 0x90, 0xce, 0x7d,
+ 0x53, 0xf8, 0x2a, 0x21, 0x9e, 0xdf, 0xb4, 0xd3, 0xd0, 0x39, 0x7c, 0x42,
+ 0xd7, 0xc2, 0x3b, 0x6f, 0x04, 0x6d, 0xe0, 0xe0, 0xde, 0x9b, 0xcf, 0x69,
+ 0x6a, 0x25, 0xfd, 0x7c, 0xcf, 0xb9, 0x61, 0x71, 0xff, 0x2e, 0x6f, 0xa3,
+ 0x0e, 0x57, 0x05, 0x0c, 0x49, 0x50, 0xdf, 0xa5, 0xe9, 0xed, 0x1a, 0x66,
+ 0xe3, 0xab, 0x6d, 0x61, 0x55, 0x65, 0x8a, 0x21, 0x39, 0x96, 0x3b, 0x18,
+ 0x33, 0xe6, 0x71, 0x1a, 0x58, 0x56, 0x60, 0xd4, 0xab, 0x00, 0xec, 0x36,
+ 0x98, 0x02, 0x3f, 0x46, 0x4f, 0x5f, 0x75, 0x0f, 0x8a, 0xb2, 0x4d, 0x85,
+ 0x1e, 0x50, 0xee, 0x18, 0x31, 0xb0, 0xc3, 0x39, 0x04, 0x40, 0x66, 0xb2,
+ 0x7c, 0x46, 0x10, 0x27, 0xba, 0x86, 0x8a, 0x31, 0xef, 0xd1, 0x86, 0x89,
+ 0xaf, 0x84, 0x73, 0xb9, 0xc8, 0xcc, 0xa4, 0xa3, 0x35, 0x49, 0xb4, 0x52,
+ 0x76, 0x01, 0x69, 0x16, 0x8c, 0x23, 0x6c, 0x39, 0xb5, 0x02, 0x3f, 0x56,
+ 0x35, 0x0d, 0x11, 0x79, 0xc3, 0x07, 0xf8, 0xe0, 0x4e, 0x25, 0xd4, 0x39,
+ 0x5b, 0xa3, 0x9a, 0x7f, 0xb5, 0xa5, 0xed, 0x19, 0x1a, 0xf7, 0xb8, 0x1f,
+ 0x23, 0xf8, 0x63, 0x98, 0x2a, 0x90, 0x0e, 0xc6, 0x0e, 0xad, 0xbd, 0x55,
+ 0xda, 0x25, 0xeb, 0xac, 0xf1, 0xde, 0xbe, 0x64, 0x02, 0xae, 0x2b, 0x58,
+ 0x4c, 0x86, 0x7a, 0x0b, 0xb4, 0x3c, 0x3c, 0x68, 0xdd, 0xdd, 0x2d, 0xd2,
+ 0x44, 0x15, 0xce, 0x37, 0xd8, 0x88, 0x99, 0xa0, 0x2b, 0xe7, 0xe7, 0x76,
+ 0x3f, 0x58, 0xfc, 0xc4, 0x91, 0x71, 0xec, 0xae, 0x74, 0x39, 0x63, 0x91,
+ 0x3b, 0x79, 0xd2, 0xa5, 0x67, 0x8e, 0xaf, 0x76, 0x99, 0x9f, 0xbe, 0xfd,
+ 0xde, 0x8d, 0x23, 0xe1, 0xd1, 0xb1, 0x8a, 0x70, 0x4a, 0x0c, 0xbc, 0xf7,
+ 0x24, 0x22, 0xb6, 0xdf, 0x58, 0x4f, 0x4c, 0xde, 0x62, 0x9c, 0x66, 0xff,
+ 0x96, 0x38, 0x6b, 0xef, 0x45, 0x98, 0x9d, 0x31, 0x3a, 0xdc, 0x1f, 0x2c,
+ 0x01, 0xf7, 0x9a, 0x9e, 0x36, 0x98, 0xe9, 0x10, 0x0d, 0xea, 0x60, 0xb1,
+ 0x66, 0xb0, 0x09, 0xf7, 0x1c, 0x10, 0x84, 0xe4, 0xc2, 0xae, 0x03, 0xce,
+ 0xe9, 0x8b, 0xd5, 0xa5, 0xc8, 0xe7, 0xde, 0x58, 0xc2, 0xe5, 0xd8, 0xef,
+ 0xa1, 0x97, 0x77, 0x98, 0xfc, 0x36, 0x66, 0x52, 0xc0, 0x17, 0xde, 0x29,
+ 0x09, 0x98, 0xa5, 0x7b, 0xbf, 0x19, 0xb6, 0xea, 0x02, 0x0b, 0x68, 0xed,
+ 0x43, 0x7c, 0x43, 0x88, 0x02, 0x36, 0x80, 0x2e, 0x76, 0xbb, 0x4e, 0x71,
+ 0xb5, 0x24, 0xcc, 0xbf, 0x9d, 0xb6, 0x78, 0x8a, 0xf5, 0xe6, 0xcd, 0x7a,
+ 0xb4, 0x9b, 0xc9, 0x6f, 0xaa, 0xdc, 0xdb, 0x10, 0xdc, 0x86, 0x90, 0xbe,
+ 0x94, 0x54, 0xbd, 0xb5, 0x79, 0x97, 0x3f, 0x52, 0xbc, 0x07, 0x6d, 0x7f,
+ 0xe8, 0xbf, 0xf2, 0xb1, 0xc3, 0x04, 0x84, 0x75, 0xc3, 0x43, 0x0c, 0xbf,
+ 0x52, 0xbb, 0x5a, 0x35, 0x29, 0x83, 0xb2, 0x7f, 0x9b, 0x73, 0x6e, 0x75,
+ 0x41, 0x20, 0x3b, 0x1f, 0x5c, 0x7b, 0x57, 0x1f, 0x8b, 0xf4, 0x26, 0x24,
+ 0xde, 0x16, 0x4a, 0x1f, 0xdd, 0x7c, 0x7e, 0x3e, 0x2a, 0x47, 0x0e, 0xa2,
+ 0xe9, 0x13, 0xe7, 0x47, 0xdf, 0x2c, 0xab, 0xd6, 0x09, 0xba, 0x0b, 0xae,
+ 0x97, 0xc3, 0xac, 0x86, 0x97, 0xe5, 0x38, 0x49, 0x14, 0xc6, 0x95, 0xc4,
+ 0x0d, 0xd9, 0x37, 0x19, 0x9b, 0x3c, 0x15, 0x7e, 0xe7, 0x79, 0xcf, 0xcb,
+ 0xc6, 0x09, 0xe3, 0x74, 0x14, 0xf9, 0xd2, 0x23, 0x22, 0x56, 0x42, 0x79,
+ 0x5f, 0x88, 0xaa, 0xba, 0xa1, 0xff, 0x6b, 0xc1, 0x59, 0x67, 0xc2, 0x84,
+ 0xa3, 0x0e, 0xdc, 0x9f, 0x4e, 0x9e, 0x85, 0xaf, 0x83, 0xed, 0xd3, 0x7b,
+ 0x91, 0xaf, 0x7c, 0xc2, 0x84, 0x3f, 0xd8, 0xc7, 0x7d, 0x8a, 0x49, 0x90,
+ 0xf3, 0x92, 0xb0, 0x74, 0xb3, 0x2f, 0x96, 0x77, 0x12, 0xed, 0x0a, 0x8e,
+ 0x27, 0x97, 0xcf, 0x3c, 0xd9, 0xb2, 0xcb, 0x4a, 0x37, 0x8b, 0x68, 0xa1,
+ 0x34, 0xdb, 0x84, 0xbd, 0xe7, 0x97, 0x71, 0x1b, 0x99, 0x76, 0xb8, 0x2f,
+ 0xe1, 0x8d, 0x56, 0x7a, 0x45, 0x3d, 0xce, 0xac, 0x25, 0x5f, 0xc7, 0x55,
+ 0xe0, 0xa7, 0x05, 0x82, 0x4a, 0x89, 0x9b, 0x90, 0x5a, 0xca, 0x35, 0xa1,
+ 0x5c, 0x73, 0xb2, 0x80, 0xa4, 0x06, 0x83, 0xf3, 0x9b, 0xf7, 0x4e, 0x87,
+ 0x1e, 0x4d, 0xc0, 0x5c, 0x48, 0xa7, 0xf2, 0xb9, 0x90, 0x9c, 0x4a, 0x06,
+ 0x3b, 0x9a, 0x9f, 0x8c, 0x95, 0xe7, 0x61, 0xe4, 0x37, 0x4a, 0xf1, 0xd5,
+ 0xfa, 0xb5, 0x11, 0x61, 0x29, 0xa7, 0xde, 0x00, 0x8e, 0xc4, 0x35, 0x03,
+ 0x84, 0x21, 0x74, 0x74, 0xa1, 0x98, 0x42, 0xc2, 0x98, 0x00, 0x3a, 0xf7,
+ 0xcf, 0x3c, 0xc0, 0x5e, 0xd8, 0x18, 0x78, 0xcd, 0xc3, 0x18, 0xff, 0x26,
+ 0x65, 0xea, 0x1b, 0xcd, 0xe1, 0x21, 0xe7, 0xd9, 0x87, 0x7f, 0xa9, 0x9c,
+ 0xb4, 0x7a, 0x97, 0x6c, 0x01, 0x55, 0x5b, 0x2c, 0x76, 0xaa, 0x9a, 0xea,
+ 0x3d, 0x32, 0xe1, 0xb0, 0xee, 0x23, 0x46, 0x42, 0xe2, 0x28, 0x87, 0x8d,
+ 0x24, 0xa0, 0x7b, 0x58, 0xd6, 0x30, 0x46, 0x47, 0x68, 0x13, 0xfd, 0x5b,
+ 0x79, 0x86, 0xf7, 0x96, 0x11, 0x3a, 0x4f, 0x7b, 0x69, 0xe6, 0xeb, 0xf2,
+ 0x19, 0x62, 0x45, 0x05, 0xbd, 0xf6, 0xde, 0x67, 0x08, 0xf7, 0x27, 0x72,
+ 0xc8, 0xa7, 0x9e, 0xf2, 0xbb, 0x4c, 0x27, 0xe2, 0x92, 0x51, 0x41, 0xca,
+ 0x6d, 0x96, 0xbf, 0x97, 0xf7, 0x49, 0xe6, 0x26, 0xb3, 0x35, 0x21, 0x08,
+ 0xff, 0x4c, 0x0f, 0x87, 0x8b, 0xa6, 0xbb, 0x67, 0xcd, 0x31, 0x35, 0x3a,
+ 0x94, 0xcb, 0x51, 0xb3, 0x65, 0x2d, 0x85, 0x46, 0xa2, 0x21, 0x07, 0x73,
+ 0x21, 0x12, 0x2f, 0x6e, 0xf2, 0x81, 0x9b, 0xeb, 0x71, 0x9e, 0xb9, 0xa3,
+ 0x20, 0x46, 0xff, 0xf7, 0x2d, 0xad, 0xd9, 0x7e, 0xdc, 0x61, 0x60, 0xa1,
+ 0x64, 0x3a, 0xd8, 0xb4, 0x7b, 0x6c, 0xd8, 0x31, 0x9c, 0x3a, 0x91, 0xf6,
+ 0xb4, 0xe8, 0xeb, 0xbf, 0xbe, 0xe1, 0x32, 0x5d, 0x90, 0x08, 0x2f, 0xa3,
+ 0xde, 0xb6, 0xf9, 0x01, 0x46, 0x34, 0x08, 0xd4, 0xdb, 0x7d, 0x07, 0xcc,
+ 0x46, 0x2b, 0x9a, 0xe5, 0x9a, 0x15, 0x9b, 0xfe, 0x5c, 0x83, 0x68, 0x6a,
+ 0x22, 0x7f, 0x22, 0x92, 0xf3, 0x74, 0x06, 0xae, 0x15, 0x6f, 0x60, 0x64,
+ 0x42, 0x3c, 0xef, 0x82, 0xd8, 0x3f, 0x4d, 0xc0, 0x52, 0xb5, 0xea, 0xf7,
+ 0xc7, 0x10, 0x3d, 0xe2, 0x92, 0x3a, 0x17, 0xa5, 0x41, 0x80, 0x7a, 0x98,
+ 0x91, 0x5d, 0x81, 0x4d, 0x21, 0xc9, 0x4b, 0x9f, 0xf0, 0x51, 0x41, 0xe6,
+ 0x09, 0x36, 0xc3, 0x67, 0x61, 0x86, 0xef, 0x96, 0x74, 0x07, 0xdb, 0x0a,
+ 0x34, 0xe2, 0x08, 0x4f, 0x63, 0x37, 0x42, 0xe5, 0x0e, 0xb2, 0x99, 0x5e,
+ 0x06, 0x5a, 0xd2, 0x19, 0xb7, 0x84, 0x99, 0xc5, 0x7e, 0x9c, 0xca, 0x6b,
+ 0x18, 0x19, 0x54, 0xff, 0x75, 0x5f, 0x1d, 0x6c, 0xbe, 0x5a, 0x49, 0xec,
+ 0x56, 0x89, 0x73, 0xed, 0x92, 0xaf, 0x77, 0xff, 0x42, 0x49, 0xff, 0xd3,
+ 0xb0, 0x42, 0x79, 0xab, 0x6e, 0x74, 0xa9, 0x7a, 0x92, 0x38, 0x97, 0xf6,
+ 0xdd, 0x9c, 0x8b, 0x12, 0xf6, 0x1d, 0x0c, 0xa4, 0x5f, 0x69, 0xfa, 0x67,
+ 0x3c, 0xd4, 0x07, 0x53, 0x1c, 0xbc, 0x73, 0xd7, 0xa0, 0xa5, 0x28, 0x1f,
+ 0x25, 0x59, 0x9d, 0x4f, 0xe8, 0x9b, 0xc3, 0xba, 0x40, 0xe1, 0x16, 0x41,
+ 0x7b, 0x68, 0xa0, 0xe1, 0x73, 0x64, 0xbe, 0xf0, 0x99, 0x23, 0xac, 0x69,
+ 0xa0, 0x7c, 0xfd, 0xec, 0x5e, 0xcb, 0xfe, 0x57, 0x14, 0xa4, 0x1d, 0x80,
+ 0xfe, 0xda, 0x7e, 0x45, 0x56, 0x9d, 0x24, 0x86, 0xad, 0x10, 0x0b, 0xc2,
+ 0x32, 0x81, 0xbd, 0x62, 0x27, 0x9e, 0x7e, 0x45, 0x96, 0xec, 0xb4, 0x13,
+ 0xee, 0x96, 0x34, 0xec, 0x01, 0x3f, 0x48, 0x9d, 0x40, 0xe8, 0xb8, 0xdf,
+ 0xc6, 0xd3, 0xb7, 0x50, 0x05, 0x8f, 0xab, 0x5c, 0x44, 0x66, 0xca, 0x76,
+ 0x9d, 0xa2, 0xfa, 0x8b, 0x51, 0xa0, 0x55, 0x19, 0xfd, 0x66, 0x4f, 0xbf,
+ 0xc6, 0xcb, 0x22, 0xc4, 0xb1, 0xc2, 0x48, 0x16, 0x6d, 0xb8, 0x95, 0x1b,
+ 0x94, 0xe9, 0x6b, 0xe3, 0xec, 0xde, 0x29, 0xb5, 0xda, 0x90, 0xc8, 0x86,
+ 0xdf, 0xd5, 0xf3, 0x00, 0xe2, 0xcd, 0x10, 0x86, 0x9a, 0x8f, 0xaf, 0x3f,
+ 0xe2, 0x56, 0x51, 0x78, 0xca, 0x77, 0x51, 0x5f, 0xd2, 0xe8, 0xf7, 0x86,
+ 0x44, 0x81, 0x7d, 0x33, 0xc0, 0xea, 0xb7, 0x92, 0xe0, 0xbb, 0x1f, 0x5f,
+ 0x33, 0x35, 0xad, 0x19, 0x8f, 0x69, 0xf8, 0x7b, 0x02, 0xd1, 0xbf, 0x8a,
+ 0x02, 0xbe, 0xe1, 0x98, 0x1e, 0xa2, 0x14, 0x9c, 0x69, 0x54, 0xc3, 0xb3,
+ 0x94, 0x04, 0x81, 0xff, 0xa0, 0x48, 0x76, 0x23, 0xf3, 0x89, 0x19, 0x60,
+ 0x31, 0xd3, 0xc5, 0xb5, 0x53, 0xc1, 0x88, 0xdf, 0xbd, 0x7a, 0xd5, 0x71,
+ 0x3b, 0xeb, 0xac, 0x5b, 0xd0, 0x8c, 0x6d, 0x01, 0xca, 0x22, 0x95, 0x28,
+ 0xee, 0xd8, 0x89, 0xd9, 0xa6, 0xc5, 0x05, 0x82, 0xeb, 0x00, 0xf7, 0xf9,
+ 0xec, 0xa6, 0x87, 0xbf, 0x93, 0xb5, 0xfd, 0x3c, 0xc6, 0xa8, 0x70, 0x8d,
+ 0x5e, 0x46, 0xee, 0x60, 0x04, 0xd7, 0x61, 0xec, 0xb5, 0x9f, 0x2e, 0x99,
+ 0x6d, 0xaf, 0x88, 0xc3, 0x8b, 0xa9, 0x54, 0x65, 0x0b, 0x1c, 0x80, 0x87,
+ 0xb0, 0x82, 0x79, 0x7d, 0x3f, 0x51, 0x7f, 0xd1, 0x66, 0x02, 0x7e, 0x73,
+ 0xac, 0xc5, 0x35, 0xd3, 0xa7, 0xf4, 0x6a, 0x0c, 0xbc, 0x16, 0xf9, 0x05,
+ 0x1a, 0x76, 0x78, 0xcc, 0x7b, 0xbd, 0x26, 0x05, 0xa1, 0x57, 0xb3, 0x87,
+ 0x91, 0x79, 0x8b, 0x08, 0x71, 0x57, 0x03, 0xbd, 0xc8, 0xdf, 0x5c, 0x36,
+ 0x57, 0x17, 0x27, 0x59, 0xc5, 0x19, 0x87, 0x04, 0x56, 0x5c, 0xe9, 0x07,
+ 0x60, 0x7d, 0x82, 0x22, 0xda, 0xee, 0x83, 0x3e, 0xed, 0x01, 0xa7, 0x95,
+ 0xe4, 0xbd, 0x7a, 0xd0, 0x29, 0x07, 0x46, 0x76, 0x60, 0x3b, 0x43, 0xdd,
+ 0x06, 0x1e, 0xb3, 0xe1, 0x44, 0xde, 0x02, 0xe4, 0x85, 0x2c, 0x61, 0xe7,
+ 0x06, 0x7a, 0xcc, 0xc9, 0xc3, 0xfc, 0x7a, 0xb3, 0x4c, 0x58, 0x1c, 0xbd,
+ 0x2e, 0xdf, 0x6b, 0xf9, 0x74, 0x83, 0x97, 0x5d, 0x76, 0xf8, 0x63, 0xc4,
+ 0xe8, 0x07, 0x24, 0xd7, 0x8a, 0xf0, 0x71, 0xeb, 0x56, 0xa9, 0xa0, 0xa5,
+ 0xd1, 0x1e, 0x3d, 0x48, 0x60, 0xe7, 0xb3, 0x27, 0x0a, 0xc6, 0x7a, 0xa6,
+ 0x53, 0x8d, 0xe6, 0x5b, 0x33, 0xea, 0xce, 0x39, 0xba, 0x9f, 0xb7, 0x64,
+ 0x59, 0xe2, 0xd8, 0x32, 0x63, 0x6e, 0x65, 0xdf, 0x90, 0xc8, 0xdf, 0x19,
+ 0x26, 0x00, 0xc6, 0x3a, 0xa3, 0xe8, 0x00, 0x4b, 0x1c, 0x58, 0xfa, 0xfc,
+ 0x52, 0x27, 0x31, 0x3d, 0x35, 0x9b, 0xf6, 0xe2, 0xd4, 0x98, 0x01, 0x6e,
+ 0xe4, 0x22, 0x4c, 0x69, 0x44, 0x60, 0x11, 0xbe, 0x3f, 0x37, 0x77, 0xf1,
+ 0xe4, 0x54, 0x24, 0x85, 0x10, 0x66, 0xa8, 0xa9, 0xe9, 0xdf, 0xab, 0xcc,
+ 0x08, 0x93, 0x62, 0x35, 0x6d, 0xb0, 0x95, 0x47, 0xed, 0x92, 0xbe, 0x98,
+ 0x48, 0x74, 0x56, 0x80, 0x74, 0xe3, 0x84, 0x7b, 0x90, 0x39, 0x2e, 0xe8,
+ 0x45, 0xa2, 0xcd, 0xca, 0x11, 0x62, 0xa2, 0xc8, 0xf5, 0x6e, 0x6c, 0xb1,
+ 0x14, 0x83, 0x70, 0x8f, 0xfb, 0x14, 0x9a, 0xa1, 0x87, 0x14, 0x87, 0x42,
+ 0xd0, 0x17, 0x52, 0xd3, 0x59, 0x56, 0xff, 0xad, 0x58, 0x9a, 0x73, 0x07,
+ 0x38, 0xf7, 0x0c, 0xc8, 0xb1, 0x58, 0xd2, 0x18, 0xb3, 0x3f, 0xbc, 0x2d,
+ 0x1b, 0xa3, 0x2b, 0x6d, 0x9a, 0x26, 0xaa, 0x6c, 0x8b, 0xb7, 0x7b, 0x3e,
+ 0xf6, 0x18, 0x70, 0x52, 0x45, 0x96, 0xb0, 0xdf, 0x60, 0xe7, 0xbc, 0x75,
+ 0x4c, 0x34, 0xde, 0xaa, 0x9c, 0x10, 0x16, 0xe0, 0x1f, 0x82, 0x2d, 0xde,
+ 0x4f, 0xd3, 0x5b, 0xe5, 0xdf, 0xc6, 0x7a, 0x8f, 0x22, 0x7a, 0x88, 0xbd,
+ 0x59, 0xa9, 0xb5, 0xc5, 0x3e, 0x0a, 0xbb, 0x49, 0xc3, 0x0f, 0xe2, 0x6d,
+ 0x45, 0xf9, 0xa0, 0x51, 0x50, 0xdd, 0x4b, 0xae, 0xe8, 0x8f, 0xac, 0x24,
+ 0x3a, 0xa2, 0xd9, 0xfa, 0xca, 0x82, 0x8d, 0x13, 0xc8, 0x14, 0x6d, 0x65,
+ 0x02, 0x31, 0x4a, 0x15, 0xeb, 0x4b, 0xdc, 0x88, 0x30, 0xe9, 0x9d, 0xa1,
+ 0x42, 0x05, 0xce, 0x50, 0xa9, 0x6e, 0xb7, 0x30, 0x17, 0xad, 0xd8, 0xb6,
+ 0xaf, 0xf1, 0x17, 0x27, 0xc7, 0xca, 0x1d, 0xdc, 0x3f, 0x16, 0x7f, 0xfd,
+ 0xba, 0xdc, 0xce, 0xcd, 0xef, 0xb3, 0x5a, 0x73, 0xea, 0xf4, 0xb7, 0x85,
+ 0x3c, 0xb3, 0xdf, 0x91, 0xe9, 0x27, 0x6a, 0x38, 0x1b, 0xee, 0x34, 0x96,
+ 0xb3, 0xe3, 0xa1, 0xb8, 0x3e, 0x9f, 0xbd, 0x8e, 0x09, 0x16, 0xf4, 0xc5,
+ 0xf7, 0x6c, 0xbb, 0x61, 0x4d, 0xae, 0x06, 0xf7, 0x22, 0xce, 0xd2, 0x02,
+ 0xe6, 0x1f, 0x75, 0x0c, 0x6e, 0xca, 0x9f, 0x89, 0x27, 0xde, 0x49, 0x79,
+ 0x1a, 0x56, 0x20, 0xcd, 0xa7, 0xdd, 0x5a, 0xf1, 0xfb, 0xaf, 0xd1, 0x5d,
+ 0xf1, 0x2a, 0x29, 0xbe, 0xae, 0xbe, 0x6c, 0x1e, 0x06, 0x74, 0xfa, 0x34,
+ 0xb0, 0x1a, 0xd2, 0x97, 0xae, 0x2b, 0xd1, 0x67, 0xa8, 0x54, 0xbb, 0x6a,
+ 0x4e, 0xd9, 0x18, 0x05, 0xbd, 0xe1, 0xb2, 0x67, 0x46, 0x66, 0xc2, 0x94,
+ 0xe8, 0x2c, 0x1f, 0x20, 0x85, 0xb3, 0xe4, 0x6f, 0x23, 0xb5, 0x56, 0x9f,
+ 0x26, 0x95, 0x61, 0x65, 0xe4, 0x6d, 0xd9, 0xfd, 0x37, 0x44, 0x30, 0x9c,
+ 0xbf, 0x78, 0x6c, 0x03, 0x69, 0x51, 0xad, 0xcc, 0x48, 0xf6, 0x93, 0xa6,
+ 0xb2, 0x62, 0x99, 0x1e, 0x56, 0xc8, 0x54, 0x3f, 0x33, 0x0c, 0x2d, 0xe5,
+ 0xa7, 0x65, 0x7b, 0xe1, 0x6c, 0x42, 0x45, 0x72, 0xcb, 0xa0, 0x28, 0x33,
+ 0xe3, 0x35, 0x35, 0x05, 0xfd, 0x0c, 0x82, 0x29, 0x01, 0x8c, 0x73, 0x30,
+ 0xa9, 0x94, 0x56, 0x00, 0xcc, 0xa4, 0xf6, 0x4c, 0x13, 0x99, 0xfa, 0x38,
+ 0x99, 0xa7, 0x23, 0x10, 0xba, 0x59, 0xf8, 0xa8, 0x63, 0xc1, 0x72, 0x0b,
+ 0x7c, 0x9a, 0x7f, 0x68, 0x94, 0x88, 0xd9, 0x85, 0xb1, 0x46, 0x37, 0x44,
+ 0xf5, 0xfa, 0xdf, 0x37, 0x26, 0xa7, 0xa9, 0x07, 0x7b, 0x1f, 0x17, 0x9a,
+ 0xbf, 0x69, 0xf2, 0x9f, 0x79, 0x44, 0xd0, 0x99, 0x58, 0xbd, 0x21, 0x79,
+ 0x69, 0x7b, 0xec, 0x10, 0x9e, 0xf4, 0x74, 0x42, 0x2d, 0xf8, 0xb5, 0x4b,
+ 0xef, 0xa4, 0xbe, 0xaf, 0x7b, 0x21, 0x7f, 0x2d, 0xea, 0x43, 0x2e, 0xfb,
+ 0x45, 0x02, 0xa6, 0xed, 0x36, 0x99, 0xee, 0xe6, 0x2a, 0x8d, 0x5f, 0x69,
+ 0x2b, 0xa6, 0xe3, 0xb9, 0xea, 0x55, 0x26, 0x6f, 0x6e, 0xa8, 0xfc, 0xb8,
+ 0xb8, 0x52, 0xed, 0x58, 0xca, 0xc1, 0x40, 0x53, 0x33, 0x26, 0xa1, 0xca,
+ 0xcc, 0xde, 0x6b, 0x8e, 0xad, 0x5c, 0xff, 0x44, 0x6e, 0x0f, 0xc3, 0xa5,
+ 0x7f, 0x39, 0xd7, 0xef, 0x0c, 0x45, 0x9b, 0x1f, 0x92, 0x1a, 0x5c, 0x26,
+ 0x94, 0x93, 0x38, 0xdc, 0x5a, 0x05, 0x04, 0xd2, 0x05, 0xf4, 0xdd, 0xf5,
+ 0x4a, 0x77, 0xdf, 0xb6, 0x47, 0x10, 0x5e, 0x3d, 0x4e, 0x73, 0x83, 0x3f,
+ 0x02, 0x69, 0x7a, 0x9c, 0x48, 0x63, 0x06, 0x7b, 0xa5, 0x6b, 0x3c, 0xbc,
+ 0x63, 0xf1, 0x4e, 0x26, 0x87, 0xf8, 0xbc, 0xbd, 0x71, 0x76, 0x4a, 0xd8,
+ 0x60, 0xfe, 0xb2, 0x92, 0x78, 0x20, 0x22, 0xf4, 0xc9, 0xba, 0xed, 0xe6,
+ 0x6e, 0x1b, 0xd9, 0x3e, 0x8f, 0x8a, 0xce, 0x13, 0xfc, 0xb8, 0xed, 0x33,
+ 0x64, 0x09, 0x11, 0x56, 0xa8, 0x40, 0x9f, 0x49, 0x90, 0xc6, 0x16, 0xbb,
+ 0xd9, 0x49, 0xab, 0xea, 0x7e, 0x1d, 0xfd, 0x83, 0x79, 0x79, 0x14, 0xb5,
+ 0xe7, 0x2c, 0x24, 0x75, 0x05, 0xe3, 0x77, 0xc3, 0x4d, 0x61, 0xb0, 0xff,
+ 0x13, 0x12, 0x78, 0x96, 0x7e, 0xc1, 0x91, 0x45, 0x0c, 0x4f, 0x36, 0x01,
+ 0x6b, 0x0e, 0x3d, 0x52, 0xa0, 0xf2, 0xb4, 0x26, 0x81, 0x33, 0x39, 0x10,
+ 0xa3, 0x87, 0x7f, 0x7d, 0x41, 0xbd, 0x03, 0xff, 0x0f, 0x3c, 0xb9, 0x6d,
+ 0xef, 0x1a, 0xe8, 0x1c, 0x8a, 0x99, 0x02, 0x70, 0x23, 0x28, 0x1c, 0x91,
+ 0x60, 0xa9, 0x2d, 0xf8, 0x8d, 0x8a, 0xd1, 0x32, 0x4f, 0xd3, 0xe0, 0x56,
+ 0x9a, 0x04, 0x26, 0x7a, 0xf6, 0x71, 0xe1, 0x58, 0xc0, 0x68, 0x2c, 0x38,
+ 0x0e, 0xa3, 0xb7, 0x4b, 0x29, 0xc8, 0x23, 0x1e, 0xd7, 0x93, 0xb4, 0x31,
+ 0x54, 0x1c, 0x5c, 0x35, 0x56, 0x01, 0x26, 0xd3, 0x27, 0xf7, 0x6b, 0xda,
+ 0x6b, 0x76, 0x16, 0x99, 0xe1, 0x12, 0x9e, 0x2e, 0xdf, 0xd6, 0x2e, 0xdf,
+ 0x5a, 0xfa, 0xeb, 0xed, 0x22, 0x3b, 0xdc, 0x4b, 0x42, 0x4c, 0x46, 0xb8,
+ 0x10, 0xe9, 0x7d, 0x27, 0xb7, 0xf1, 0x1a, 0x03, 0x0e, 0x90, 0x4e, 0xa3,
+ 0xff, 0x40, 0x02, 0x99, 0xfb, 0xfc, 0x91, 0x65, 0xab, 0x90, 0x83, 0xc9,
+ 0xd0, 0xf5, 0xae, 0xcc, 0x77, 0xcc, 0x46, 0x4d, 0x4a, 0x5d, 0xfe, 0x9f,
+ 0x32, 0xff, 0x24, 0xf4, 0x00, 0x74, 0x77, 0x34, 0x5f, 0xb4, 0xcb, 0x90,
+ 0x0e, 0xe3, 0x0f, 0xd4, 0x0e, 0x94, 0x49, 0x5d, 0x8a, 0x1f, 0x2f, 0x23,
+ 0x5b, 0x54, 0xa5, 0x56, 0x10, 0xd1, 0x14, 0x3d, 0x75, 0x86, 0x77, 0x86,
+ 0xa5, 0x69, 0x4c, 0x5d, 0x9c, 0x32, 0xa7, 0x88, 0xa0, 0x1c, 0xe6, 0x0a,
+ 0x7c, 0x66, 0xd2, 0x62, 0xb0, 0x81, 0x5b, 0x13, 0x42, 0x4d, 0x6d, 0x2e,
+ 0xf0, 0xa8, 0x64, 0xf6, 0x95, 0x4e, 0xf3, 0x2a, 0x4f, 0x77, 0x2c, 0xb0,
+ 0x7b, 0xc8, 0xf2, 0xfb, 0xbd, 0xa3, 0xc9, 0x2e, 0x8e, 0x66, 0x53, 0xb7,
+ 0x20, 0x61, 0xad, 0x82, 0x31, 0xb4, 0x18, 0xcc, 0x0d, 0x48, 0xcc, 0x20,
+ 0xd7, 0x90, 0x5b, 0x9f, 0x4d, 0x53, 0x10, 0xb4, 0x63, 0xfb, 0x5d, 0x60,
+ 0x66, 0x1d, 0xa0, 0x46, 0x62, 0x8d, 0x5a, 0xa2, 0xea, 0x35, 0x58, 0x0a,
+ 0xa0, 0x09, 0xff, 0xcb, 0xcd, 0xe4, 0x9a, 0xc0, 0xff, 0x8b, 0x71, 0x30,
+ 0xc4, 0xeb, 0x7a, 0x7d, 0xe1, 0xa6, 0xb8, 0x57, 0x57, 0xb1, 0xc0, 0x44,
+ 0x27, 0x16, 0xc4, 0x3c, 0xad, 0x4a, 0x73, 0xe1, 0x66, 0x0d, 0xf6, 0xea,
+ 0x4c, 0xd4, 0x24, 0x35, 0x34, 0x79, 0x9e, 0xa9, 0x88, 0xe3, 0x81, 0x1e,
+ 0x5e, 0xf3, 0xb0, 0x87, 0x4a, 0x9e, 0xe8, 0x90, 0x02, 0x36, 0x21, 0x58,
+ 0x9c, 0x0f, 0xa4, 0xd8, 0xee, 0xc2, 0xfd, 0x3e, 0x26, 0x2d, 0x37, 0xf4,
+ 0x8d, 0x29, 0xca, 0x97, 0xcf, 0x83, 0x34, 0xcb, 0xd3, 0x15, 0x34, 0x88,
+ 0x45, 0x5b, 0x78, 0xeb, 0xcf, 0xc1, 0x9e, 0xf7, 0xc2, 0x62, 0xf8, 0x72,
+ 0x8d, 0x60, 0x38, 0xaf, 0x08, 0xd7, 0x64, 0xbe, 0xf6, 0xd3, 0x02, 0x3e,
+ 0x15, 0xb1, 0xc4, 0x1e, 0x29, 0x1e, 0x74, 0x76, 0xf6, 0x91, 0x01, 0x86,
+ 0x94, 0xd5, 0xa3, 0x64, 0x40, 0x2b, 0xd0, 0x95, 0x29, 0xc3, 0xf5, 0xde,
+ 0x13, 0x44, 0xe4, 0xdc, 0xae, 0x1f, 0x27, 0x9c, 0x9f, 0x85, 0xb4, 0x5d,
+ 0x53, 0xd4, 0xbf, 0xd7, 0xdb, 0xf9, 0x2a, 0xed, 0xaa, 0xcf, 0x53, 0x2b,
+ 0x87, 0x3b, 0x06, 0x50, 0xc6, 0x8e, 0xa2, 0xaa, 0x0b, 0x07, 0xfe, 0x7f,
+ 0xeb, 0xd6, 0x71, 0xa0, 0x96, 0xb4, 0x69, 0xe4, 0xc2, 0xab, 0x75, 0x6e,
+ 0x07, 0xe8, 0xe0, 0x64, 0x5f, 0xa3, 0x64, 0x22, 0x8d, 0x9e, 0x85, 0x4c,
+ 0x91, 0x2a, 0x5c, 0x8b, 0xd3, 0x1b, 0x63, 0x27, 0xd2, 0x99, 0x08, 0x23,
+ 0x0c, 0x2e, 0xcc, 0x5c, 0x74, 0xd7, 0x44, 0x66, 0x11, 0xa5, 0x9a, 0x40,
+ 0x31, 0x42, 0x23, 0x5d, 0x96, 0xda, 0x90, 0xae, 0x0d, 0x9c, 0x73, 0x3c,
+ 0xf0, 0xdf, 0xf2, 0xcd, 0x08, 0x4c, 0x47, 0xb2, 0xc3, 0x86, 0xfc, 0xf2,
+ 0xf1, 0x05, 0xa5, 0x4f, 0xe2, 0x48, 0x86, 0xba, 0xe5, 0xf6, 0x13, 0xa8,
+ 0xbc, 0x7b, 0x59, 0xc2, 0x4a, 0x20, 0xfc, 0x5c, 0x38, 0x7a, 0xc9, 0xdc,
+ 0x2a, 0xf2, 0xd1, 0x6a, 0x26, 0xd2, 0x97, 0x53, 0x3c, 0x61, 0x4e, 0x0f,
+ 0x95, 0xee, 0xad, 0xd4, 0xad, 0xa2, 0x09, 0xba, 0xa6, 0xd0, 0xc3, 0x6a,
+ 0xf0, 0x68, 0x59, 0x18, 0x3c, 0x1f, 0x91, 0x38, 0xaf, 0xd5, 0x28, 0xfa,
+ 0xdc, 0xda, 0xb7, 0x09, 0xec, 0x0f, 0x8e, 0xa9, 0x7c, 0xb7, 0xc7, 0x1b,
+ 0xb9, 0x00, 0x60, 0x47, 0x5b, 0xbc, 0x02, 0x9b, 0xdf, 0xa3, 0x45, 0xfc,
+ 0x24, 0x51, 0xbc, 0x2a, 0x4e, 0x86, 0x66, 0xf3, 0xed, 0xfe, 0xf9, 0x99,
+ 0x88, 0xb8, 0xd7, 0x87, 0xf3, 0x80, 0x86, 0x0d, 0x47, 0xd3, 0x83, 0x58,
+ 0x94, 0xc1, 0x6e, 0x08, 0x52, 0x8d, 0x79, 0x2b, 0x93, 0x8d, 0x56, 0x15,
+ 0xf5, 0xa3, 0xd5, 0x04, 0xbe, 0xdd, 0xd9, 0x49, 0x52, 0x1b, 0x08, 0xc1,
+ 0x09, 0xe0, 0xd1, 0xb4, 0x2f, 0xc2, 0x60, 0x66, 0x70, 0x2d, 0xf6, 0x9d,
+ 0xda, 0x8a, 0xb6, 0xee, 0x37, 0x5e, 0xf6, 0xa1, 0x67, 0xe4, 0x11, 0xe0,
+ 0x58, 0x8d, 0xbb, 0xbd, 0x9e, 0xba, 0x72, 0x3d, 0xfa, 0x76, 0x7a, 0x16,
+ 0xfd, 0xa8, 0xb1, 0x80, 0xe6, 0x3b, 0xf7, 0xee, 0x07, 0xbf, 0x4b, 0x13,
+ 0x4b, 0x49, 0xd9, 0x9a, 0x89, 0x47, 0xbc, 0x65, 0xa2, 0xd8, 0xbf, 0x84,
+ 0x5e, 0x1a, 0x3c, 0x63, 0x81, 0x57, 0xc2, 0x96, 0x47, 0x29, 0x31, 0xe4,
+ 0xdd, 0xae, 0x50, 0x27, 0x53, 0x49, 0xe1, 0xb1, 0x54, 0x9e, 0xcc, 0x4c,
+ 0x54, 0xd3, 0x4f, 0x37, 0x77, 0x0b, 0x20, 0xb7, 0xac, 0x47, 0x90, 0x33,
+ 0xa0, 0x12, 0xc0, 0x93, 0x08, 0x77, 0x7d, 0x19, 0x6a, 0xaa, 0x29, 0xce,
+ 0xdb, 0x06, 0x37, 0x2e, 0xab, 0xca, 0xf4, 0x36, 0xaa, 0x91, 0xd7, 0x4d,
+ 0x5a, 0xf3, 0xac, 0x04, 0xa9, 0xaa, 0x39, 0xce, 0x63, 0x70, 0x59, 0x97,
+ 0x4f, 0x63, 0xd4, 0x9b, 0x2f, 0x13, 0xef, 0xe1, 0xee, 0x5b, 0x18, 0xa9,
+ 0x38, 0x22, 0x64, 0x60, 0x04, 0x10, 0x2a, 0x7d, 0x75, 0xa4, 0x26, 0xc5,
+ 0x13, 0x4f, 0xcb, 0x17, 0x6d, 0xd8, 0xf8, 0xcd, 0xf4, 0x99, 0x32, 0x8a,
+ 0xdb, 0x31, 0xa5, 0x39, 0x52, 0x7e, 0x76, 0x7e, 0x2e, 0xd5, 0xde, 0x75,
+ 0x3d, 0x16, 0xf8, 0x79, 0xe7, 0x8c, 0x6d, 0x1b, 0xe3, 0xd2, 0x6d, 0x06,
+ 0x5a, 0x65, 0xfc, 0x37, 0x43, 0xac, 0xfc, 0xb2, 0x89, 0x5f, 0x8f, 0x02,
+ 0x58, 0x1b, 0x03, 0xa6, 0x9c, 0x50, 0x6d, 0x1c, 0xa3, 0xd7, 0x19, 0x35,
+ 0x23, 0xaa, 0x13, 0xef, 0xe4, 0xa9, 0x3b, 0xb0, 0x23, 0x11, 0x5f, 0x81,
+ 0xab, 0xa0, 0xd6, 0x5b, 0x53, 0x1c, 0x66, 0x7f, 0xa6, 0x0d, 0xaf, 0x43,
+ 0x8d, 0x80, 0xd9, 0x2c, 0x5f, 0xda, 0xb1, 0x0d, 0xc9, 0x38, 0x91, 0x08,
+ 0x64, 0xac, 0xc0, 0x1f, 0x20, 0xba, 0xdf, 0xaf, 0xaa, 0x8e, 0x11, 0x25,
+ 0xd2, 0xa2, 0x93, 0xdb, 0x3f, 0x67, 0xf0, 0x1c, 0xbc, 0xf3, 0x22, 0xe9,
+ 0x17, 0x47, 0x47, 0xdc, 0x80, 0xf4, 0x17, 0xc6, 0x20, 0xd3, 0x97, 0x49,
+ 0xc9, 0xa6, 0x4c, 0xbc, 0x9d, 0x3f, 0xea, 0xee, 0xe5, 0x2b, 0xba, 0xe5,
+ 0x9a, 0x5a, 0x39, 0x1a, 0x4e, 0x15, 0x10, 0x9b, 0xef, 0xcc, 0x6b, 0x25,
+ 0xfb, 0xef, 0xa2, 0x8b, 0xb4, 0x9b, 0x28, 0x8f, 0xa1, 0x01, 0x51, 0x32,
+ 0x33, 0x7a, 0xeb, 0x34, 0x2c, 0xcb, 0x51, 0x7a, 0x77, 0x02, 0x7b, 0xde,
+ 0x99, 0xc5, 0x9f, 0xe1, 0xc8, 0x2c, 0xb3, 0x2e, 0x45, 0x4d, 0xdf, 0xa6,
+ 0xf0, 0x1c, 0x6a, 0x93, 0x3d, 0xc2, 0xf8, 0xe0, 0xfe, 0x4f, 0x5e, 0xc2,
+ 0x56, 0x57, 0xb6, 0x7e, 0x4e, 0x4d, 0xc1, 0x3f, 0x62, 0xe8, 0xb7, 0xdb,
+ 0x14, 0x0c, 0xae, 0xa9, 0xb3, 0x99, 0xbf, 0x48, 0xd5, 0x62, 0x25, 0x2d,
+ 0x47, 0x03, 0x1c, 0x55, 0x36, 0xda, 0x74, 0x55, 0x9b, 0x70, 0x00, 0x72,
+ 0x86, 0x34, 0x45, 0x8a, 0x77, 0x1a, 0x21, 0x8d, 0x2a, 0x5c, 0x53, 0xad,
+ 0x63, 0x7b, 0xe0, 0x76, 0x82, 0xfc, 0x73, 0x33, 0xac, 0xe5, 0x33, 0xa4,
+ 0x76, 0xd9, 0xb6, 0x2b, 0xb8, 0x0f, 0xcb, 0x78, 0xf7, 0xa7, 0xfd, 0x8c,
+ 0xad, 0x4f, 0x91, 0x60, 0xa5, 0x11, 0x6e, 0x5a, 0x50, 0x8c, 0x02, 0x3b,
+ 0x29, 0x3f, 0x8f, 0x75, 0x76, 0x21, 0x27, 0x8d, 0x37, 0x96, 0x74, 0x02,
+ 0xb3, 0x41, 0x53, 0xe5, 0x2a, 0xb5, 0xdf, 0xcc, 0xa9, 0x05, 0x78, 0xcd,
+ 0xce, 0x78, 0x66, 0xaa, 0x43, 0x81, 0x4a, 0xb1, 0xe1, 0x3b, 0x2c, 0xff,
+ 0x49, 0x57, 0x6c, 0x80, 0x19, 0xf1, 0xeb, 0x69, 0x02, 0x91, 0xf0, 0x7a,
+ 0x2e, 0x7d, 0x45, 0x33, 0x29, 0xc9, 0x26, 0x55, 0x28, 0x16, 0xba, 0xfa,
+ 0x18, 0x11, 0xc6, 0xc2, 0x28, 0x2a, 0x50, 0x8e, 0xf7, 0xf1, 0x26, 0xbe,
+ 0x6c, 0xe6, 0x9f, 0xea, 0x08, 0x92, 0xc1, 0xa9, 0x7c, 0x69, 0xbb, 0x3c,
+ 0x93, 0x5e, 0xdc, 0x0c, 0xaf, 0x0e, 0x4c, 0xa8, 0x1d, 0xca, 0x45, 0x70,
+ 0xac, 0x2b, 0xb9, 0x01, 0xca, 0xaf, 0x82, 0x8b, 0xfc, 0xef, 0x1b, 0x79,
+ 0x7b, 0xfb, 0x9d, 0x3c, 0x7c, 0x1f, 0x69, 0x29, 0xf1, 0x23, 0x91, 0xad,
+ 0xd0, 0xcc, 0x3f, 0x21, 0xfc, 0xee, 0xa1, 0x38, 0xd0, 0xa7, 0x1e, 0x6b,
+ 0x74, 0x72, 0x6a, 0x93, 0x8a, 0x8e, 0xc2, 0x13, 0xf2, 0xc9, 0x79, 0x03,
+ 0xeb, 0xed, 0xf7, 0x6b, 0x4d, 0xba, 0x92, 0xab, 0x34, 0xde, 0x61, 0xd3,
+ 0x66, 0xe6, 0x25, 0x71, 0xd4, 0xa6, 0xe0, 0x78, 0x99, 0xaf, 0xca, 0x60,
+ 0xeb, 0x79, 0x15, 0x87, 0x0f, 0x6a, 0x68, 0x8e, 0x5f, 0xa7, 0x9c, 0xba,
+ 0x8c, 0xcc, 0x9d, 0xe0, 0x3f, 0x93, 0x81, 0x49, 0xbb, 0xb4, 0x8f, 0xed,
+ 0x7a, 0x22, 0x5c, 0x45, 0x52, 0xde, 0x68, 0xac, 0xbe, 0x5b, 0x7b, 0x36,
+ 0xad, 0x1e, 0xc3, 0xa5, 0xe5, 0xfd, 0x42, 0xb7, 0xa6, 0x5d, 0x7b, 0xf8,
+ 0x87, 0xbf, 0xeb, 0xd5, 0xb6, 0xb5, 0x03, 0x4d, 0xa6, 0xc2, 0x50, 0x68,
+ 0x0c, 0x03, 0x2a, 0xc2, 0xf5, 0xf5, 0x33, 0xf8, 0x38, 0xb9, 0x7a, 0x37,
+ 0xcc, 0xd5, 0xfe, 0xc4, 0x2b, 0x09, 0x89, 0x82, 0x0c, 0x83, 0xa1, 0x5d,
+ 0x3b, 0x29, 0x47, 0x4d, 0x9b, 0x1f, 0x13, 0xa9, 0xad, 0xa2, 0x31, 0x5c,
+ 0x36, 0x0a, 0xd5, 0x0b, 0x4c, 0xd8, 0xfb, 0xc3, 0x00, 0x2c, 0x1a, 0xf1,
+ 0x54, 0x63, 0xcc, 0x31, 0xb9, 0x24, 0x4b, 0x16, 0x26, 0x0e, 0x63, 0xbe,
+ 0x1b, 0x49, 0x6f, 0x23, 0x55, 0xe2, 0xe4, 0x20, 0xff, 0xf1, 0x2c, 0xf5,
+ 0x22, 0x88, 0xf9, 0x60, 0xed, 0x59, 0x17, 0x99, 0x58, 0x1d, 0xb7, 0xd8,
+ 0x24, 0x3a, 0x24, 0xe3, 0xd7, 0xbf, 0xd0, 0x4c, 0xac, 0x2b, 0x7b, 0x2c,
+ 0xab, 0xff, 0xc1, 0xa7, 0x13, 0x67, 0xf9, 0x9d, 0x9c, 0x3c, 0x15, 0x25,
+ 0x18, 0xde, 0xb9, 0xe1, 0x3b, 0x5e, 0xe3, 0xca, 0xf4, 0xc2, 0x9e, 0x6b,
+ 0xae, 0xc6, 0xfe, 0xe2, 0xd9, 0xf6, 0xbb, 0xf5, 0x6b, 0xc5, 0x54, 0x13,
+ 0x7e, 0x94, 0x93, 0xe6, 0x17, 0xee, 0xb0, 0x70, 0x22, 0xff, 0xab, 0xe1,
+ 0x93, 0x9f, 0xd5, 0x72, 0xa9, 0x29, 0x98, 0x8b, 0x39, 0x5e, 0x48, 0xd6,
+ 0xb1, 0xbe, 0xf5, 0xcb, 0x0f, 0x06, 0xfd, 0x3c, 0xfd, 0x8a, 0xfc, 0x97,
+ 0x58, 0x1a, 0x0a, 0x6d, 0xf0, 0xbf, 0x17, 0x0e, 0xbc, 0x00, 0x36, 0xbf,
+ 0xa6, 0x57, 0xeb, 0x83, 0x8f, 0x5b, 0x71, 0x5f, 0x1a, 0xa1, 0x37, 0x4d,
+ 0x0c, 0xe3, 0x0a, 0x72, 0x54, 0x1d, 0x46, 0xa0, 0x54, 0x90, 0xe3, 0x04,
+ 0x68, 0xd9, 0x56, 0x6c, 0x66, 0x21, 0xfd, 0xef, 0xa1, 0xd7, 0xb4, 0x40,
+ 0x9b, 0xda, 0x3e, 0x1a, 0x4d, 0xc8, 0x32, 0x1c, 0x49, 0x90, 0xa7, 0x39,
+ 0x97, 0x22, 0xe0, 0x61, 0x18, 0x46, 0x38, 0x02, 0xdd, 0x76, 0x60, 0x59,
+ 0x65, 0xed, 0x24, 0x73, 0x09, 0x25, 0x92, 0x66, 0x7d, 0x70, 0x1b, 0x5e,
+ 0x26, 0xab, 0xa5, 0xa2, 0xe2, 0x6c, 0xf6, 0x8d, 0xd3, 0xa1, 0xef, 0x15,
+ 0x08, 0x4f, 0x7b, 0x6e, 0xd3, 0x7a, 0xc5, 0xa4, 0xc9, 0x00, 0x3a, 0xb7,
+ 0xf1, 0xa9, 0x6f, 0x8f, 0x82, 0xd2, 0x19, 0xba, 0x68, 0xb3, 0xe8, 0x91,
+ 0x45, 0x94, 0x3c, 0x98, 0xd0, 0xe8, 0x12, 0xa7, 0x89, 0xf7, 0xcf, 0xcb,
+ 0xa6, 0x77, 0x8b, 0xc3, 0x25, 0xec, 0x7c, 0x02, 0x64, 0xd0, 0x23, 0x0b,
+ 0x30, 0x64, 0xb8, 0xa4, 0x4f, 0x75, 0x3f, 0x0f, 0x1a, 0xc9, 0x19, 0x20,
+ 0xd8, 0x93, 0xeb, 0xf7, 0x5e, 0xc9, 0xa6, 0x2a, 0x2c, 0x7b, 0x77, 0x0f,
+ 0xb3, 0x21, 0x4d, 0x60, 0x9b, 0xc6, 0x77, 0xb9, 0x41, 0xf3, 0xd3, 0x89,
+ 0xf9, 0xe6, 0x94, 0x85, 0x69, 0x9f, 0xc7, 0x79, 0xa7, 0xe1, 0x13, 0x75,
+ 0xc2, 0xd8, 0x0a, 0x2a, 0xc6, 0xc0, 0x6a, 0xe2, 0xfd, 0x53, 0x7a, 0x86,
+ 0x8f, 0x6a, 0xbd, 0xdd, 0x5d, 0x72, 0x5a, 0xa4, 0x31, 0x2c, 0xed, 0xf9,
+ 0xe1, 0x9b, 0x00, 0x0f, 0x6d, 0x7e, 0x00, 0x6d, 0x3f, 0x77, 0xd5, 0xb0,
+ 0x45, 0xfd, 0x95, 0xff, 0xb0, 0xee, 0x6a, 0xdb, 0x52, 0x7c, 0x11, 0xa8,
+ 0xaa, 0x10, 0x6a, 0x3a, 0xea, 0xe4, 0xd0, 0x46, 0x1e, 0xda, 0x6f, 0xde,
+ 0xb8, 0x14, 0x57, 0x87, 0x01, 0xbc, 0xd1, 0x77, 0xa4, 0x7f, 0x74, 0x29,
+ 0xdf, 0xeb, 0x08, 0xdb, 0xb2, 0xaf, 0xfe, 0xd1, 0xab, 0x91, 0xa6, 0x51,
+ 0xeb, 0x96, 0x6b, 0xef, 0x5f, 0x9e, 0x5d, 0x88, 0x82, 0xa8, 0x7d, 0xb1,
+ 0xb6, 0xab, 0x22, 0x41, 0x8a, 0xaa, 0xb7, 0xa3, 0x72, 0xfd, 0x71, 0x35,
+ 0x86, 0x41, 0xdf, 0xe4, 0x0b, 0xef, 0x34, 0x12, 0x3b, 0x43, 0x6a, 0x14,
+ 0xa1, 0x70, 0x47, 0x44, 0x2f, 0x5c, 0x67, 0xd6, 0x31, 0x98, 0x51, 0x3f,
+ 0x58, 0x1e, 0xd6, 0x3d, 0x27, 0xe5, 0x93, 0xf7, 0xa5, 0x76, 0xa7, 0x1e,
+ 0x5e, 0x4f, 0xc1, 0x5d, 0x1d, 0x8d, 0xce, 0xdd, 0x76, 0xe2, 0xaa, 0x7e,
+ 0x3a, 0x06, 0x0a, 0x3c, 0xc1, 0xad, 0x80, 0x99, 0x67, 0xd9, 0xc6, 0xac,
+ 0x99, 0x73, 0x4d, 0xd1, 0xb4, 0x6d, 0xee, 0x38, 0xe2, 0x77, 0xb9, 0x46,
+ 0xb9, 0x80, 0xbf, 0x24, 0x25, 0x8c, 0x14, 0x7f, 0xd2, 0x9c, 0x4a, 0xef,
+ 0x9f, 0xb6, 0x28, 0x98, 0xa9, 0xfa, 0x29, 0x4c, 0x74, 0xc2, 0x82, 0x52,
+ 0xb1, 0xa9, 0x0e, 0x8e, 0xf2, 0xc1, 0xfc, 0x7f, 0xdf, 0x2e, 0x41, 0x63,
+ 0x9a, 0x17, 0x61, 0xcc, 0xa0, 0x78, 0xca, 0xeb, 0x5c, 0xd9, 0xe6, 0x44,
+ 0x0e, 0xb5, 0x92, 0x30, 0xfe, 0x53, 0xeb, 0x9b, 0x4d, 0x52, 0x8d, 0x74,
+ 0x1e, 0x79, 0x32, 0xb2, 0xbf, 0x0d, 0xe3, 0x77, 0x28, 0x7c, 0xd6, 0x37,
+ 0x62, 0xe2, 0x80, 0xed, 0x98, 0x0a, 0x23, 0x6a, 0x45, 0x5b, 0x81, 0x65,
+ 0x21, 0xd0, 0x4f, 0xf4, 0x64, 0x4c, 0x84, 0x75, 0x6d, 0x52, 0xf8, 0x5e,
+ 0xaa, 0x95, 0xfb, 0x07, 0xe4, 0x1b, 0xd9, 0xce, 0xff, 0x5d, 0x57, 0x35,
+ 0xed, 0x83, 0xc3, 0x27, 0xa2, 0x4f, 0x7a, 0x95, 0xf2, 0x56, 0x69, 0x0b,
+ 0x2c, 0xd6, 0x04, 0xc0, 0x87, 0x4c, 0xf1, 0x36, 0x30, 0x14, 0x37, 0xb7,
+ 0x19, 0xe5, 0xc4, 0x0f, 0x72, 0xac, 0x65, 0x75, 0x5c, 0x83, 0xed, 0xb1,
+ 0x73, 0x45, 0xc5, 0xb1, 0x84, 0xa7, 0x54, 0x45, 0x9f, 0x0e, 0x63, 0x54,
+ 0x53, 0x56, 0x1e, 0xf7, 0xd5, 0x07, 0xcb, 0x58, 0x65, 0xba, 0x5e, 0x9e,
+ 0xad, 0xfd, 0x25, 0xbc, 0x94, 0xb5, 0x18, 0x3a, 0x08, 0xe1, 0xc8, 0xef,
+ 0x5f, 0xd7, 0x4d, 0xa2, 0x73, 0x45, 0xe9, 0xf3, 0x3e, 0x50, 0xc5, 0xf9,
+ 0x27, 0xf2, 0xb1, 0x59, 0x56, 0x8e, 0xf0, 0x6a, 0x14, 0xb5, 0xb1, 0xa1,
+ 0xa8, 0xbf, 0x79, 0x59, 0xa9, 0x5e, 0x5f, 0xf2, 0x4a, 0x12, 0xaa, 0x8a,
+ 0x3d, 0x92, 0x08, 0x20, 0x40, 0xe7, 0x3e, 0x71, 0xde, 0x6e, 0xb3, 0x9c,
+ 0x51, 0xc7, 0x0b, 0xb4, 0xf4, 0x86, 0xb3, 0x7a, 0x44, 0xa3, 0x39, 0xcb,
+ 0x30, 0xfd, 0xae, 0x31, 0xf1, 0xfb, 0x76, 0x01, 0x87, 0x13, 0x03, 0xc2,
+ 0x5a, 0x28, 0xfa, 0x16, 0x05, 0x0b, 0xc6, 0x41, 0xa1, 0x43, 0x3c, 0x73,
+ 0xcf, 0x56, 0xf7, 0x64, 0x88, 0x2c, 0xc5, 0x1f, 0xc4, 0x02, 0x80, 0x63,
+ 0x97, 0xe7, 0x29, 0x92, 0x0a, 0x62, 0x05, 0xd8, 0x1c, 0xfa, 0x1a, 0x2a,
+ 0x36, 0x93, 0xdc, 0x1e, 0x4e, 0x4e, 0x6a, 0x89, 0xfb, 0x1e, 0xc2, 0xf2,
+ 0x8a, 0x2f, 0x48, 0x6c, 0x92, 0x5d, 0xb7, 0xe2, 0x9c, 0x4f, 0x55, 0x36,
+ 0xe4, 0x0d, 0x2c, 0x2f, 0x76, 0xbc, 0x81, 0xdc, 0xec, 0x42, 0xe2, 0x70,
+ 0xf8, 0x83, 0x05, 0x66, 0xf1, 0x14, 0xb1, 0xfe, 0x07, 0x0d, 0x25, 0x98,
+ 0x4f, 0xd3, 0x2f, 0x03, 0xc3, 0xb4, 0x57, 0x29, 0xca, 0x2c, 0xdd, 0x8e,
+ 0x25, 0x0d, 0xea, 0x7b, 0xe3, 0x0c, 0x59, 0x5f, 0x6f, 0x86, 0xa2, 0xbe,
+ 0x97, 0x19, 0xe4, 0x73, 0x0a, 0xc3, 0xc7, 0x58, 0xab, 0x2b, 0xc3, 0x54,
+ 0x33, 0xb5, 0x9a, 0x40, 0x41, 0x6a, 0x37, 0x18, 0x27, 0xa9, 0x44, 0x27,
+ 0x44, 0x4e, 0xdc, 0x01, 0xcc, 0x80, 0x69, 0x33, 0xa0, 0xcb, 0xf9, 0xe3,
+ 0x9c, 0xf6, 0xab, 0xa7, 0xfb, 0x7c, 0xab, 0x97, 0xaf, 0x5f, 0xc9, 0xe8,
+ 0xc0, 0x79, 0x95, 0xea, 0xf9, 0x90, 0xbe, 0xa2, 0xa0, 0x89, 0x3a, 0x85,
+ 0x29, 0x81, 0xc9, 0x88, 0x46, 0xec, 0xee, 0x42, 0xef, 0x78, 0x69, 0x92,
+ 0x6a, 0xf3, 0x87, 0x20, 0x47, 0x20, 0xc1, 0x9e, 0xef, 0x45, 0xbb, 0x59,
+ 0x26, 0xd2, 0x22, 0xaf, 0x8b, 0x61, 0xb6, 0xca, 0x91, 0xf2, 0xa0, 0x0d,
+ 0xff, 0x9c, 0x7a, 0x5c, 0x5c, 0x8c, 0xce, 0x28, 0xb2, 0x73, 0xab, 0x67,
+ 0x47, 0x27, 0x74, 0x5b, 0xdc, 0x01, 0x1c, 0x2e, 0x97, 0x66, 0x7b, 0xf6,
+ 0x7b, 0xa0, 0x23, 0x56, 0xca, 0x5f, 0x7d, 0x42, 0x02, 0xf2, 0xc7, 0x22,
+ 0x2e, 0x1d, 0x7c, 0xbc, 0x65, 0x70, 0x9e, 0x25, 0x9c, 0xd4, 0xa1, 0x63,
+ 0xbc, 0x3c, 0xc7, 0x69, 0xca, 0xd7, 0x26, 0xb5, 0xf6, 0x15, 0xc3, 0x96,
+ 0x3f, 0x72, 0xc0, 0x74, 0xd9, 0x6a, 0x96, 0xbf, 0xe3, 0xe9, 0x16, 0xf4,
+ 0x5d, 0xee, 0x27, 0xe4, 0xb7, 0x26, 0x1b, 0xd5, 0x43, 0x05, 0x2f, 0x49,
+ 0xa4, 0x11, 0xc0, 0x77, 0xcf, 0xe7, 0x41, 0xd3, 0xd6, 0x4d, 0x2a, 0x09,
+ 0x7e, 0xb2, 0x8f, 0x17, 0x19, 0x41, 0x2f, 0xea, 0x17, 0x83, 0xc0, 0x1f,
+ 0xc1, 0xa5, 0x26, 0x4b, 0xca, 0xbc, 0xfe, 0x4a, 0x47, 0x0b, 0x0f, 0x3f,
+ 0x10, 0x0c, 0x97, 0x93, 0x16, 0x03, 0x9c, 0x2f, 0x72, 0x31, 0xd2, 0xed,
+ 0x07, 0x54, 0xb4, 0x60, 0xd9, 0x6c, 0x80, 0xbf, 0xfa, 0x25, 0x38, 0xc8,
+ 0xea, 0xad, 0x71, 0x6e, 0x79, 0x12, 0xe2, 0xbe, 0x04, 0x1a, 0xba, 0x0a,
+ 0xa4, 0xec, 0x6d, 0xce, 0xb4, 0x12, 0x5e, 0xfe, 0xaa, 0xce, 0x58, 0x02,
+ 0x3a, 0x07, 0xfa, 0x5a, 0x6d, 0x71, 0xcb, 0xa0, 0x4a, 0x6e, 0xe9, 0x8f,
+ 0x56, 0xa8, 0x10, 0x29, 0x0e, 0x17, 0xd9, 0x5f, 0x11, 0x2d, 0xa9, 0xb5,
+ 0x45, 0x98, 0x73, 0x45, 0x8c, 0x73, 0x4b, 0xef, 0xce, 0xde, 0xb7, 0xfb,
+ 0x07, 0xdb, 0xa8, 0xeb, 0xd3, 0x69, 0x4b, 0x3b, 0xc4, 0x5f, 0x3c, 0xa6,
+ 0x09, 0x94, 0xdb, 0x64, 0xe9, 0xc8, 0x38, 0xb3, 0x2b, 0xbd, 0x6e, 0x41,
+ 0x99, 0x16, 0xae, 0x82, 0x23, 0x19, 0x77, 0xc5, 0x5b, 0x46, 0xa4, 0xb5,
+ 0xd3, 0x31, 0xac, 0x81, 0xdb, 0x1a, 0xa2, 0x39, 0x28, 0x60, 0x95, 0x75,
+ 0x6f, 0xc3, 0xb1, 0xd1, 0x61, 0x94, 0xb9, 0x8b, 0x8e, 0xf8, 0x22, 0xeb,
+ 0xe5, 0xa1, 0x0a, 0x09, 0x85, 0xf6, 0x6e, 0x19, 0xc4, 0x02, 0xe1, 0xb9,
+ 0xde, 0xd3, 0x44, 0x98, 0x61, 0xe0, 0x82, 0x96, 0xf8, 0x0d, 0x72, 0xac,
+ 0x86, 0x20, 0xbf, 0xb9, 0x38, 0x9f, 0x71, 0x29, 0x86, 0x57, 0xfb, 0xf0,
+ 0x86, 0xb8, 0x62, 0xa0, 0xb0, 0x8b, 0x28, 0xaf, 0xe7, 0x09, 0x5d, 0x07,
+ 0xae, 0xdb, 0xb8, 0xbf, 0x96, 0x45, 0x2b, 0xc6, 0x6d, 0xa6, 0x75, 0x70,
+ 0x54, 0x4c, 0xbc, 0xb7, 0xff, 0x5a, 0xf9, 0xd3, 0xca, 0xb1, 0xa2, 0x8e,
+ 0x11, 0x34, 0x3d, 0x2a, 0x12, 0x37, 0x6a, 0xf5, 0x1e, 0x01, 0x03, 0x6c,
+ 0xce, 0xfe, 0x66, 0xdd, 0x3c, 0xca, 0x45, 0xdc, 0x98, 0x73, 0x9a, 0x95,
+ 0x6b, 0xcc, 0x9e, 0xa8, 0x36, 0xed, 0x3e, 0x46, 0x24, 0xf6, 0xc2, 0xc8,
+ 0x69, 0x38, 0xd7, 0x6f, 0xe2, 0xa3, 0x4b, 0xa8, 0xaf, 0x41, 0xad, 0x1f,
+ 0xdd, 0x6d, 0x86, 0x13, 0x41, 0x23, 0xd6, 0x4c, 0x4e, 0x8b, 0xef, 0x07,
+ 0x60, 0x75, 0x07, 0xca, 0x92, 0x4c, 0x05, 0x5b, 0x7f, 0x47, 0x34, 0x39,
+ 0x8e, 0x1b, 0x02, 0xff, 0xb5, 0x77, 0xef, 0x0f, 0xe9, 0xe6, 0xca, 0x84,
+ 0x2d, 0xbf, 0x71, 0x19, 0x7e, 0x9e, 0xd4, 0x36, 0x9b, 0xf8, 0x39, 0x2a,
+ 0x7b, 0xf9, 0x58, 0x7c, 0xbe, 0x62, 0x82, 0x56, 0xad, 0x6c, 0xbe, 0x7a,
+ 0x05, 0xc1, 0x72, 0xe8, 0x71, 0x73, 0x3a, 0xdb, 0xe4, 0xba, 0x42, 0x3c,
+ 0x04, 0x36, 0xe7, 0x24, 0x97, 0x63, 0x0d, 0x45, 0x7b, 0xa9, 0x18, 0x76,
+ 0x1c, 0x45, 0xba, 0x03, 0x40, 0xc1, 0xf1, 0x3d, 0xac, 0xbd, 0xf1, 0x80,
+ 0xd5, 0x7a, 0x5d, 0xa5, 0xe5, 0x32, 0x67, 0x1a, 0x62, 0xc6, 0x96, 0x6c,
+ 0x3f, 0xcc, 0xa2, 0x94, 0xb8, 0xe8, 0x8e, 0x18, 0x81, 0xe1, 0x8b, 0x18,
+ 0xc5, 0xcb, 0x27, 0xb7, 0x8a, 0xed, 0xdb, 0x1d, 0x2b, 0xdc, 0xf7, 0x41,
+ 0x98, 0xa5, 0x96, 0x31, 0x15, 0x0d, 0x49, 0x5b, 0x7a, 0x70, 0x39, 0x1e,
+ 0xb5, 0xf9, 0xd8, 0x57, 0x3e, 0xa5, 0x46, 0x53, 0xa7, 0x05, 0xbd, 0x16,
+ 0x3c, 0x89, 0xd7, 0x7b, 0xf2, 0x5e, 0x2f, 0xc2, 0x43, 0x37, 0x02, 0x76,
+ 0x56, 0x06, 0x6d, 0x7b, 0xbc, 0x37, 0xef, 0xda, 0x89, 0xcd, 0xf9, 0x03,
+ 0x36, 0x4f, 0x3f, 0x40, 0xbf, 0xab, 0xce, 0x88, 0x2e, 0xd0, 0xff, 0x8e,
+ 0x1e, 0xb2, 0x9c, 0x1a, 0x13, 0xa8, 0xb8, 0xb3, 0x7f, 0x8e, 0x31, 0xe9,
+ 0xf1, 0xdc, 0xe9, 0x30, 0x38, 0x5e, 0xa5, 0x7a, 0xa0, 0x6e, 0x07, 0xf2,
+ 0xe5, 0x9f, 0xb2, 0xfb, 0x81, 0x48, 0x2d, 0x14, 0xef, 0x5e, 0x7b, 0x2c,
+ 0xe9, 0xbd, 0xf7, 0x86, 0xef, 0x20, 0xc5, 0x32, 0xd1, 0x48, 0xab, 0xec,
+ 0xa8, 0xbe, 0x22, 0x88, 0x00, 0x2e, 0x47, 0xe4, 0x43, 0x76, 0x53, 0x89,
+ 0x67, 0xb4, 0x1e, 0x95, 0xb6, 0x97, 0xc0, 0x08, 0xe0, 0xb9, 0x25, 0x13,
+ 0x55, 0xf1, 0xeb, 0x1d, 0x81, 0x50, 0x6d, 0xab, 0x90, 0x32, 0xf0, 0x84,
+ 0xb1, 0x52, 0xfe, 0x5b, 0xaa, 0x96, 0x52, 0x6f, 0x7b, 0xac, 0xa0, 0x54,
+ 0x7b, 0xb5, 0x76, 0x3d, 0x51, 0x09, 0x24, 0xdc, 0x4d, 0x99, 0x3a, 0x2f,
+ 0x69, 0xb1, 0xf1, 0xf3, 0xeb, 0xff, 0x47, 0xfe, 0xdb, 0x24, 0xf6, 0x1a,
+ 0xcc, 0x0d, 0x79, 0xbb, 0xd2, 0x99, 0x8f, 0x8e, 0x5d, 0xf1, 0x9c, 0x21,
+ 0x99, 0x70, 0xa7, 0x7b, 0xb8, 0x32, 0xcf, 0x24, 0xf6, 0xf9, 0x2c, 0x14,
+ 0xb3, 0xc6, 0x00, 0x2d, 0xff, 0xeb, 0xd5, 0x3a, 0xe2, 0x41, 0x00, 0x47,
+ 0x56, 0xf3, 0x1f, 0x53, 0x51, 0x76, 0xff, 0xfe, 0x54, 0x8e, 0xbf, 0xc1,
+ 0x7e, 0x22, 0x7d, 0x25, 0x79, 0x92, 0x33, 0xce, 0x99, 0x07, 0x6a, 0x96,
+ 0xac, 0xa2, 0x26, 0x2f, 0xb8, 0xdc, 0x14, 0x1f, 0xc6, 0x44, 0xec, 0x6f,
+ 0xb8, 0xd0, 0xd4, 0x5d, 0xa1, 0x56, 0x19, 0x31, 0x9b, 0xb9, 0x9a, 0x9b,
+ 0x2b, 0xa1, 0x5f, 0x2f, 0x9c, 0xbf, 0x14, 0x59, 0xca, 0x74, 0x84, 0x9e,
+ 0x96, 0x60, 0xe8, 0x16, 0xa3, 0xcf, 0x6a, 0x79, 0x70, 0x36, 0xc8, 0xe8,
+ 0x6b, 0xa1, 0xde, 0x26, 0x60, 0xed, 0x03, 0x5d, 0xe2, 0x4c, 0x81, 0x0d,
+ 0x27, 0xd3, 0xd7, 0xd1, 0xa6, 0x34, 0x75, 0xde, 0xde, 0xba, 0x44, 0x28,
+ 0x42, 0xa9, 0xd0, 0x72, 0x26, 0x61, 0xdb, 0x37, 0xa9, 0x27, 0x60, 0xbd,
+ 0x1f, 0xd5, 0x3c, 0x74, 0xa1, 0x1f, 0x67, 0xeb, 0x54, 0xca, 0x44, 0x35,
+ 0xf2, 0xe2, 0x08, 0x98, 0x01, 0x7f, 0x11, 0x3c, 0x28, 0x4f, 0x33, 0x37,
+ 0x80, 0x41, 0x7d, 0x11, 0xfb, 0xb6, 0xce, 0x88, 0xc0, 0x9a, 0x6b, 0x64,
+ 0x97, 0xff, 0x9d, 0x6d, 0x39, 0x19, 0x60, 0x46, 0xdb, 0x0d, 0x99, 0xff,
+ 0x53, 0xad, 0x9c, 0xde, 0x77, 0x3b, 0xd7, 0xc0, 0x92, 0x9f, 0x35, 0x9c,
+ 0xb3, 0x0f, 0x7b, 0xd7, 0x66, 0xea, 0x1f, 0x6b, 0x8d, 0x45, 0xd8, 0xc6,
+ 0xa0, 0x97, 0x73, 0xbe, 0x7b, 0x11, 0xf7, 0xd1, 0xdc, 0x53, 0x40, 0x37,
+ 0xf3, 0x5b, 0x1d, 0x3d, 0x39, 0x3c, 0x39, 0x71, 0x1d, 0xad, 0x40, 0xb5,
+ 0x2a, 0x55, 0x0c, 0x2f, 0x6d, 0x92, 0x53, 0x51, 0xa7, 0x6d, 0x9e, 0x4b,
+ 0x32, 0x01, 0x8c, 0x62, 0x36, 0x74, 0x4c, 0x3b, 0xe3, 0xb8, 0x1f, 0xc3,
+ 0xc8, 0x70, 0xf0, 0x7a, 0x7b, 0x02, 0xf9, 0xd9, 0x31, 0x20, 0x36, 0x03,
+ 0x18, 0x82, 0x85, 0x1e, 0x82, 0x0d, 0x26, 0x4b, 0xf0, 0xbc, 0x8c, 0x2a,
+ 0xb8, 0xbc, 0xf4, 0x83, 0x2b, 0xe1, 0x51, 0x8b, 0xef, 0x82, 0x58, 0x86,
+ 0x26, 0x4e, 0x26, 0x48, 0x0a, 0x16, 0x49, 0x9a, 0x1d, 0x2a, 0xd7, 0x6c,
+ 0x53, 0x64, 0x4c, 0x3b, 0xdf, 0xb2, 0x9d, 0x57, 0xb3, 0x69, 0x3d, 0xfb,
+ 0xb3, 0x79, 0xfd, 0x75, 0xe2, 0x0b, 0x42, 0xb1, 0x29, 0x1b, 0xfd, 0xef,
+ 0x03, 0x73, 0x1a, 0x43, 0x1c, 0xfd, 0x8d, 0x02, 0x50, 0xc7, 0x6d, 0x0c,
+ 0x68, 0xa0, 0xd5, 0x3c, 0x75, 0x5e, 0x92, 0x68, 0x64, 0x83, 0xe1, 0x35,
+ 0x15, 0xda, 0x6c, 0xfa, 0x20, 0x35, 0x64, 0x82, 0xdd, 0x25, 0x05, 0xf9,
+ 0xdf, 0xc8, 0x8d, 0x52, 0xfa, 0xcb, 0x84, 0x37, 0x9c, 0x04, 0x4a, 0x17,
+ 0x9c, 0x93, 0x50, 0xbd, 0x26, 0xff, 0xe2, 0x57, 0xb8, 0x1d, 0x11, 0xfa,
+ 0x54, 0x35, 0xef, 0xe4, 0xca, 0x27, 0x79, 0x6a, 0xc7, 0x53, 0x21, 0xb2,
+ 0x9b, 0x3d, 0x9e, 0x2d, 0xe8, 0xdf, 0xaa, 0x49, 0x92, 0x85, 0xdb, 0xd4,
+ 0x9b, 0x8c, 0x2f, 0x18, 0x64, 0xd9, 0x92, 0xbe, 0x03, 0x63, 0x24, 0x2b,
+ 0x35, 0x0f, 0x25, 0x9e, 0xb7, 0x24, 0x95, 0x1f, 0x6c, 0xc1, 0xdd, 0x86,
+ 0x8e, 0xff, 0x6e, 0x6d, 0xa6, 0xe9, 0x1b, 0x80, 0xac, 0x97, 0xb0, 0xe1,
+ 0x4c, 0x93, 0xf5, 0x68, 0x42, 0xff, 0xf8, 0x3d, 0x65, 0x7e, 0xec, 0xfb,
+ 0x90, 0xbe, 0x72, 0x91, 0x6e, 0x80, 0x3d, 0x0e, 0x86, 0xb5, 0xc6, 0x75,
+ 0xc3, 0x9e, 0x9b, 0xc0, 0x30, 0x4a, 0x65, 0x46, 0x11, 0xcd, 0x27, 0xa5,
+ 0xb1, 0xd0, 0x7d, 0x09, 0xcf, 0xe6, 0x45, 0x2c, 0xee, 0x48, 0xe1, 0x22,
+ 0x34, 0x4f, 0xe2, 0x55, 0x2b, 0xb9, 0x9c, 0x7b, 0xed, 0x8a, 0x5c, 0xbc,
+ 0xf9, 0x47, 0x0b, 0x52, 0x73, 0xe2, 0xb3, 0x45, 0x06, 0xf7, 0x1b, 0xae,
+ 0x95, 0x1c, 0x64, 0x7b, 0xe2, 0x3e, 0xaa, 0x0d, 0xf3, 0xf3, 0x28, 0x9c,
+ 0xe3, 0x88, 0x97, 0x0d, 0xda, 0x56, 0x4f, 0x25, 0x90, 0xa5, 0x7d, 0x2a,
+ 0x2b, 0x7d, 0x72, 0x54, 0xb1, 0xb5, 0x75, 0x01, 0x33, 0x44, 0xa1, 0x85,
+ 0x62, 0x3b, 0x5e, 0xfc, 0xf7, 0xcf, 0xa4, 0x36, 0x7a, 0x03, 0x35, 0xd7,
+ 0x03, 0x72, 0x37, 0x28, 0x0f, 0x7e, 0x85, 0x22, 0xb3, 0x26, 0xf4, 0x1b,
+ 0x07, 0x1e, 0x49, 0x6b, 0x6c, 0xc8, 0x8e, 0xa1, 0x17, 0x5b, 0x02, 0xe9,
+ 0xb2, 0x1b, 0xd8, 0xbf, 0x0a, 0xcc, 0xcb, 0xc7, 0x5d, 0x87, 0x5b, 0xcc,
+ 0xc8, 0x12, 0x52, 0x09, 0xae, 0x22, 0x90, 0xa4, 0x83, 0xe7, 0x02, 0xdf,
+ 0x9d, 0xc9, 0x85, 0x3d, 0x50, 0xaa, 0x22, 0x2a, 0x79, 0xae, 0xfb, 0x72,
+ 0x14, 0x93, 0x46, 0xff, 0x4d, 0x96, 0x44, 0x61, 0xca, 0xdf, 0x6f, 0x4e,
+ 0xa4, 0x4c, 0x33, 0xfa, 0x95, 0xf0, 0xc0, 0x16, 0x1d, 0xbe, 0x4c, 0x82,
+ 0x85, 0xc0, 0xe6, 0xf3, 0x58, 0x86, 0x21, 0x5b, 0x50, 0xd6, 0x31, 0xd4,
+ 0xb4, 0x70, 0x38, 0x7b, 0x61, 0x4b, 0x07, 0xab, 0xca, 0xa1, 0xb5, 0xb7,
+ 0xc0, 0x4a, 0x6f, 0x26, 0x38, 0x2b, 0xb1, 0xa8, 0x4a, 0xae, 0x98, 0x03,
+ 0xfc, 0x01, 0xe5, 0x37, 0xb0, 0x0c, 0x51, 0xe1, 0xc0, 0xca, 0xa9, 0xf8,
+ 0x64, 0xda, 0xe8, 0xa9, 0xb2, 0x09, 0x36, 0xde, 0xb2, 0x74, 0x3d, 0x50,
+ 0x6c, 0xf7, 0x31, 0x64, 0x0b, 0x7c, 0x71, 0x57, 0xda, 0x00, 0x47, 0xa1,
+ 0x56, 0x04, 0xbc, 0x08, 0xe7, 0x66, 0x41, 0x5d, 0xab, 0xeb, 0x0a, 0x8b,
+ 0xc4, 0x13, 0x3a, 0x0e, 0xca, 0xd7, 0xe1, 0x43, 0x32, 0x4f, 0xc8, 0xd7,
+ 0x2a, 0xe1, 0x25, 0x89, 0xbe, 0x73, 0x17, 0x39, 0xe3, 0x49, 0x4a, 0x3b,
+ 0x72, 0xdd, 0x4a, 0xa4, 0xe3, 0xf2, 0xe9, 0x06, 0x5b, 0x9f, 0x49, 0x42,
+ 0xb9, 0x48, 0xb7, 0x9e, 0xc2, 0x73, 0x28, 0x18, 0x69, 0x15, 0xf7, 0x6b,
+ 0x0f, 0x20, 0xd5, 0x30, 0xad, 0xab, 0xd1, 0xe2, 0x35, 0xb4, 0xd8, 0xde,
+ 0x9b, 0xd2, 0x5e, 0x72, 0xed, 0xc2, 0x1c, 0x6e, 0xb2, 0xa6, 0xa4, 0x42,
+ 0x57, 0x7d, 0xb1, 0x75, 0xe8, 0x2b, 0x97, 0x0d, 0x8b, 0x12, 0x8f, 0x2e,
+ 0xe5, 0x30, 0x3d, 0x43, 0xe5, 0xdb, 0x0f, 0x9e, 0x57, 0x63, 0x31, 0xf4,
+ 0x69, 0xda, 0x6d, 0xc2, 0x29, 0x6f, 0xda, 0x1c, 0xc9, 0x95, 0x3c, 0x07,
+ 0x60, 0x36, 0xb3, 0x0c, 0x14, 0x2b, 0x47, 0xe4, 0x42, 0xed, 0x0b, 0x8d,
+ 0x72, 0xbd, 0xb9, 0x04, 0xee, 0xf4, 0x64, 0x91, 0xcc, 0xa5, 0x3b, 0x59,
+ 0x4d, 0xef, 0x52, 0x38, 0x68, 0xfd, 0x1e, 0xae, 0x38, 0xd0, 0x36, 0x18,
+ 0x69, 0x7a, 0xf0, 0xfd, 0x03, 0xec, 0x8d, 0x1a, 0x36, 0x21, 0xc4, 0xef,
+ 0x16, 0x8f, 0xef, 0xeb, 0x52, 0x6b, 0xb5, 0x22, 0x36, 0x7a, 0x1f, 0xdf,
+ 0xf0, 0xa0, 0xac, 0x74, 0x90, 0xe8, 0x39, 0x8e, 0x32, 0x94, 0x29, 0x9a,
+ 0xa3, 0x1b, 0x1b, 0x5c, 0xef, 0x7b, 0xeb, 0x25, 0xbb, 0xc1, 0x89, 0x38,
+ 0x90, 0x84, 0xba, 0x14, 0x53, 0xef, 0x61, 0x85, 0xaf, 0x27, 0xef, 0x4c,
+ 0xb4, 0x2b, 0xac, 0xb2, 0xf6, 0x21, 0x3c, 0xbf, 0x2a, 0x24, 0xac, 0xef,
+ 0x8a, 0xeb, 0xd1, 0x82, 0xb8, 0x2e, 0x29, 0x3a, 0xcf, 0xc3, 0xcd, 0xc3,
+ 0x1e, 0xb8, 0xff, 0xac, 0x35, 0xbe, 0x63, 0x3f, 0xbd, 0xa5, 0x97, 0xb2,
+ 0xdb, 0xa6, 0x73, 0x4e, 0xb3, 0x62, 0x70, 0x02, 0x63, 0x4f, 0xdf, 0x9b,
+ 0x81, 0xe0, 0xa8, 0x75, 0x2c, 0xd6, 0x4b, 0x22, 0x4f, 0x83, 0x74, 0xc2,
+ 0xe7, 0x5f, 0x64, 0x73, 0xcf, 0x7d, 0xa8, 0x9a, 0x2d, 0x87, 0x99, 0x91,
+ 0x35, 0x08, 0xe6, 0x14, 0x8d, 0xef, 0x51, 0xd7, 0xa9, 0x97, 0xeb, 0xbd,
+ 0x90, 0xa4, 0xc7, 0xc8, 0x35, 0x32, 0xc4, 0xe0, 0x7a, 0x12, 0x33, 0x12,
+ 0x20, 0x8e, 0x05, 0xde, 0xe9, 0x0f, 0xf9, 0x1b, 0xc8, 0xfa, 0xb2, 0x74,
+ 0x81, 0x36, 0xff, 0x3f, 0x2a, 0x55, 0x09, 0xe0, 0xd6, 0xc4, 0xeb, 0x8f,
+ 0xfe, 0x88, 0xe5, 0xcf, 0x50, 0x2b, 0xd0, 0x53, 0x80, 0xb8, 0x75, 0x5e,
+ 0xbb, 0x08, 0xd9, 0x3a, 0x8a, 0x4b, 0xdc, 0x16, 0xb0, 0x17, 0x43, 0x34,
+ 0x57, 0xb7, 0x42, 0xc1, 0x57, 0xd7, 0x9d, 0x79, 0x13, 0xcb, 0xac, 0x73,
+ 0x16, 0x44, 0xbb, 0xff, 0xa1, 0x34, 0xc5, 0x17, 0xce, 0x13, 0x75, 0xeb,
+ 0x27, 0x84, 0x01, 0xda, 0x44, 0x6d, 0x8c, 0x22, 0x40, 0x7b, 0x02, 0xc7,
+ 0x87, 0x7c, 0x3f, 0xa2, 0x6c, 0xd5, 0xc4, 0xda, 0xbe, 0x32, 0x2c, 0x39,
+ 0x1e, 0x30, 0xb4, 0x48, 0x4e, 0x7c, 0x94, 0x3a, 0x94, 0xbf, 0xb8, 0x98,
+ 0x8e, 0xf1, 0xcc, 0xd8, 0x58, 0x55, 0x43, 0xeb, 0xc8, 0x9c, 0xc8, 0x93,
+ 0x82, 0xaf, 0x0d, 0x59, 0x4d, 0xee, 0xb5, 0x16, 0x60, 0xb7, 0x9e, 0x2c,
+ 0x7c, 0x86, 0x5c, 0xf4, 0x71, 0x56, 0x7b, 0xff, 0xaa, 0x82, 0xf7, 0x55,
+ 0x68, 0x51, 0x63, 0x23, 0x26, 0xdf, 0x43, 0x32, 0x45, 0xbf, 0xa2, 0xeb,
+ 0xc3, 0x7e, 0x3d, 0x38, 0x62, 0x4c, 0xa3, 0x78, 0xf4, 0x45, 0x41, 0xb5,
+ 0x9b, 0xee, 0x33, 0x94, 0x74, 0xe8, 0x43, 0x44, 0x03, 0xb5, 0x30, 0x32,
+ 0x9d, 0x05, 0x5e, 0x83, 0xd7, 0x6c, 0x3d, 0xb3, 0xa7, 0x39, 0x91, 0x15,
+ 0x3f, 0x7d, 0xfe, 0x07, 0x17, 0x86, 0xee, 0x7f, 0x3f, 0xcb, 0xe4, 0xa6,
+ 0x3e, 0xd1, 0x47, 0xd3, 0x50, 0x8e, 0x26, 0x90, 0xef, 0x3b, 0x6c, 0xd7,
+ 0xe5, 0x4c, 0x41, 0xb5, 0xb4, 0x7f, 0x37, 0xe4, 0xec, 0xa9, 0x71, 0x0b,
+ 0xea, 0x83, 0x2f, 0x14, 0xe4, 0xc8, 0x3f, 0x69, 0xef, 0x6f, 0x82, 0xf1,
+ 0xe5, 0x28, 0x54, 0xcf, 0xb1, 0xed, 0x7a, 0xb8, 0x11, 0xac, 0x6c, 0x7b,
+ 0x55, 0x25, 0x49, 0x21, 0xed, 0x0a, 0x29, 0x29, 0x01, 0x41, 0x13, 0xd4,
+ 0xec, 0xf1, 0xaf, 0x7e, 0x21, 0xf6, 0xf9, 0x51, 0xb3, 0x5e, 0x72, 0x83,
+ 0xfb, 0x7c, 0x72, 0x6b, 0xe0, 0xce, 0xdf, 0xff, 0x49, 0x15, 0x79, 0x22,
+ 0x43, 0x63, 0xb9, 0x1c, 0xf9, 0x58, 0x86, 0xa4, 0x20, 0x95, 0x84, 0x16,
+ 0x2d, 0xd4, 0x03, 0x97, 0x7a, 0xca, 0xbb, 0xc6, 0x55, 0x90, 0x39, 0xf4,
+ 0x45, 0x0d, 0xd5, 0x75, 0x0a, 0x38, 0x18, 0x61, 0x1a, 0xcf, 0xe6, 0xc0,
+ 0xe9, 0x0b, 0xba, 0xe4, 0x42, 0x24, 0xa1, 0xba, 0x3d, 0x8d, 0x57, 0xf3,
+ 0x85, 0x0a, 0xba, 0x94, 0x52, 0x86, 0xb7, 0x72, 0x25, 0xa6, 0xe6, 0xaa,
+ 0xbd, 0x33, 0xef, 0xd9, 0x63, 0x1c, 0x8b, 0xb8, 0xd4, 0x1c, 0x5f, 0x26,
+ 0x20, 0x8e, 0x11, 0xb0, 0x04, 0xc6, 0xdc, 0x5b, 0x7e, 0xc9, 0x18, 0x8b,
+ 0xd7, 0xa4, 0x71, 0xed, 0x9b, 0x69, 0xdd, 0xe4, 0xaf, 0xc4, 0x21, 0x0e,
+ 0x31, 0xac, 0x5f, 0x1d, 0x17, 0xf4, 0xf3, 0x71, 0xd0, 0x8a, 0xbd, 0x0f,
+ 0x37, 0x7e, 0x03, 0x22, 0x8c, 0xf5, 0xc5, 0x92, 0x37, 0xd6, 0xca, 0x37,
+ 0x2b, 0xd1, 0x2b, 0x1c, 0x9b, 0x71, 0x5e, 0x0b, 0xe3, 0x91, 0x2e, 0xe1,
+ 0x0d, 0xc0, 0xfa, 0x9f, 0xb0, 0x95, 0xf0, 0x92, 0x9e, 0xdc, 0x98, 0xa9,
+ 0xb8, 0xd4, 0xa5, 0x8e, 0x01, 0xd6, 0x5c, 0x9e, 0x4d, 0xe5, 0x5b, 0x75,
+ 0xfe, 0x2a, 0x43, 0xc6, 0x21, 0xe3, 0x34, 0x8b, 0x36, 0x63, 0xf7, 0xf4,
+ 0xa6, 0xcf, 0x98, 0x24, 0xa2, 0x18, 0x12, 0x1b, 0xb6, 0xef, 0xe5, 0x7f,
+ 0x79, 0xd6, 0xb2, 0x33, 0xd9, 0x51, 0xb1, 0x2e, 0x4c, 0x0c, 0xae, 0x29,
+ 0x23, 0x9d, 0x31, 0x9d, 0x92, 0xdc, 0x78, 0x84, 0x24, 0xfd, 0x0d, 0xd0,
+ 0x5d, 0x59, 0xcd, 0x1a, 0x8b, 0x68, 0x01, 0xb1, 0x4e, 0x53, 0xd9, 0xc7,
+ 0x0f, 0x05, 0x49, 0xaf, 0x87, 0x9c, 0x48, 0xa9, 0xd2, 0xfd, 0x4c, 0xb8,
+ 0x2c, 0x18, 0xa6, 0x64, 0x7f, 0x75, 0x29, 0x7d, 0xb7, 0x4e, 0x3b, 0x6b,
+ 0x57, 0x7e, 0x8d, 0x83, 0x8a, 0x54, 0xe7, 0x08, 0xe9, 0x3b, 0x9a, 0xd3,
+ 0x93, 0xbb, 0x19, 0xa4, 0x3c, 0x0c, 0x6e, 0x9c, 0x41, 0x7b, 0xfb, 0xef,
+ 0x7a, 0x2a, 0x47, 0xbe, 0x0d, 0xe4, 0x4d, 0x95, 0x12, 0x06, 0x5b, 0x1a,
+ 0x99, 0x6f, 0xb8, 0xcc, 0x06, 0x04, 0x8f, 0xeb, 0xff, 0xf4, 0x9f, 0xd8,
+ 0x2e, 0xd0, 0x80, 0x22, 0xfb, 0x6c, 0x5a, 0x21, 0x33, 0x98, 0x7d, 0xaf,
+ 0x36, 0x97, 0x76, 0x02, 0xdb, 0x0d, 0x76, 0xe4, 0x12, 0x77, 0x33, 0x4e,
+ 0x52, 0x1c, 0xd8, 0xe2, 0x33, 0xa2, 0xf9, 0x1c, 0x6f, 0x8d, 0xc3, 0x12,
+ 0xef, 0xa5, 0x35, 0x4f, 0x4b, 0x73, 0x87, 0x57, 0x60, 0xb5, 0x39, 0x22,
+ 0xb4, 0x80, 0xa5, 0x48, 0x7e, 0x2e, 0x78, 0xc3, 0xc2, 0xc5, 0x87, 0x80,
+ 0xff, 0x7f, 0x09, 0x99, 0xd4, 0x96, 0x07, 0x4d, 0xc0, 0x7f, 0xb7, 0x57,
+ 0x83, 0x58, 0xf0, 0xb4, 0xde, 0xc6, 0xd1, 0x1c, 0x82, 0x7f, 0x46, 0x98,
+ 0xf4, 0x81, 0x9f, 0xa3, 0x13, 0xf6, 0xf2, 0x2c, 0x91, 0x3c, 0x5a, 0x6f,
+ 0xac, 0xc0, 0x05, 0x7f, 0x19, 0x31, 0x95, 0x70, 0x8b, 0x63, 0x71, 0x98,
+ 0x3a, 0xf2, 0x72, 0x36, 0xfd, 0x88, 0x88, 0x12, 0x86, 0x28, 0x1a, 0x33,
+ 0xaf, 0x41, 0xab, 0x56, 0xa1, 0xf7, 0x84, 0x16, 0x8d, 0x4f, 0xde, 0xd2,
+ 0x82, 0xb4, 0x72, 0x5a, 0x5b, 0xd2, 0x6f, 0xed, 0x53, 0x64, 0x7e, 0x74,
+ 0x64, 0x22, 0xc2, 0x6a, 0xd9, 0xa6, 0x1a, 0x60, 0x97, 0x75, 0x6c, 0x92,
+ 0x54, 0x77, 0x9c, 0xf5, 0x03, 0xd8, 0x93, 0x2d, 0x95, 0xc5, 0x55, 0x57,
+ 0x73, 0x34, 0xb9, 0x08, 0x2d, 0xb5, 0x87, 0xda, 0xf2, 0x26, 0xca, 0xf0,
+ 0x55, 0x36, 0x74, 0x76, 0x64, 0x77, 0xf7, 0x85, 0x4f, 0xe1, 0xe3, 0xc8,
+ 0x3f, 0x78, 0x3f, 0x00, 0xa0, 0xbe, 0x19, 0x53, 0xbd, 0xec, 0xe1, 0xc3,
+ 0xbe, 0xc1, 0x15, 0xea, 0x09, 0xd1, 0xc2, 0x4a, 0xa4, 0x52, 0x50, 0xa5,
+ 0x1b, 0xd0, 0xb3, 0x8b, 0x91, 0x4f, 0x93, 0xa1, 0xcc, 0x60, 0xd7, 0xd9,
+ 0x8c, 0xc6, 0xbb, 0x76, 0xc5, 0xbf, 0x52, 0x32, 0x66, 0x95, 0xb4, 0x73,
+ 0x48, 0x26, 0xfb, 0x55, 0xd7, 0xa8, 0x04, 0x1e, 0x8f, 0xb9, 0x3d, 0x5d,
+ 0xe7, 0xe2, 0x16, 0x27, 0x04, 0x48, 0x24, 0x4c, 0x0e, 0x45, 0x89, 0x45,
+ 0x5b, 0xab, 0x7a, 0xb7, 0x0d, 0x75, 0x85, 0x7c, 0xa3, 0x73, 0xa2, 0x95,
+ 0x2f, 0xd6, 0x00, 0x80, 0x75, 0x8a, 0x9f, 0x35, 0x39, 0x25, 0x7b, 0x08,
+ 0x2c, 0xef, 0xea, 0x2b, 0x8c, 0xfd, 0xec, 0x59, 0xff, 0x76, 0xc3, 0xa2,
+ 0xa3, 0x34, 0xd8, 0x7a, 0xb0, 0x5e, 0xbe, 0xc5, 0x70, 0x02, 0x74, 0x2d,
+ 0x94, 0xba, 0x62, 0x57, 0x39, 0x36, 0xa2, 0xcf, 0x7e, 0x70, 0x66, 0x3c,
+ 0xe3, 0x20, 0x53, 0x49, 0x40, 0xc2, 0xb0, 0x4b, 0xa5, 0x1f, 0x4f, 0x2b,
+ 0x4c, 0xc5, 0x9c, 0xc1, 0x6a, 0x87, 0xef, 0x22, 0xb2, 0xf9, 0x25, 0x6a,
+ 0xcb, 0x6d, 0xa8, 0xdc, 0xc2, 0x62, 0xf6, 0xb2, 0xb4, 0x25, 0x05, 0x57,
+ 0x79, 0xfb, 0xee, 0xb7, 0x45, 0x25, 0x67, 0x72, 0x27, 0x74, 0x8d, 0xa4,
+ 0xe1, 0xfd, 0xae, 0x83, 0xad, 0xe8, 0xde, 0x59, 0x93, 0x1a, 0xec, 0x06,
+ 0xfd, 0x86, 0x8f, 0x5b, 0x69, 0x73, 0x9c, 0x28, 0x3a, 0xb4, 0xde, 0x63,
+ 0x23, 0x20, 0x0f, 0x6e, 0x2f, 0x2e, 0xe2, 0x28, 0xa9, 0x23, 0xf2, 0xc7,
+ 0xbc, 0xc7, 0x46, 0x83, 0x01, 0xb7, 0xdd, 0xcc, 0x97, 0x13, 0x26, 0xba,
+ 0x76, 0xb8, 0x24, 0x4c, 0x8c, 0x81, 0x39, 0xd5, 0x93, 0xe0, 0x4c, 0x19,
+ 0xd0, 0x52, 0xdd, 0x98, 0x51, 0x67, 0x28, 0x0b, 0xb8, 0x09, 0x58, 0x01,
+ 0xaf, 0x2d, 0x4b, 0x15, 0x38, 0x95, 0x10, 0x20, 0xff, 0x6a, 0xe6, 0x58,
+ 0xd6, 0x7d, 0x7e, 0x2f, 0x7d, 0x03, 0xae, 0xd2, 0x93, 0x1d, 0x40, 0x61,
+ 0xd7, 0xdf, 0x98, 0xd1, 0x0a, 0xa6, 0xd3, 0x03, 0x65, 0xa2, 0xa6, 0xcd,
+ 0xab, 0x42, 0xc0, 0xfc, 0xab, 0x77, 0xf1, 0xf7, 0x61, 0xcb, 0x58, 0x6f,
+ 0x2a, 0x36, 0xd1, 0xa7, 0x6f, 0x60, 0xfd, 0x16, 0x8c, 0x4b, 0x2d, 0xa5,
+ 0x9a, 0x5c, 0x1f, 0xb9, 0x2a, 0xd1, 0xfe, 0x7a, 0x39, 0xa0, 0x97, 0xcd,
+ 0xf6, 0xb4, 0x81, 0x0a, 0xf7, 0x8d, 0x1f, 0x8e, 0x15, 0x29, 0x26, 0xda,
+ 0x7e, 0x76, 0x93, 0x06, 0x0c, 0xe3, 0x20, 0x27, 0x83, 0xe1, 0xcb, 0x43,
+ 0x0e, 0xb4, 0xfe, 0x0f, 0x54, 0xb5, 0x23, 0x05, 0x50, 0x1d, 0x92, 0x3b,
+ 0x3e, 0x58, 0xf1, 0xe7, 0x97, 0x02, 0x7c, 0x59, 0x4d, 0xe7, 0x63, 0xaa,
+ 0xb3, 0x8b, 0x43, 0xdb, 0x61, 0x30, 0xbd, 0x66, 0x4e, 0x5d, 0xa9, 0xde,
+ 0xda, 0x13, 0x5f, 0xf4, 0xeb, 0x72, 0x9f, 0x2b, 0x40, 0xc5, 0x34, 0x00,
+ 0xf3, 0x4b, 0xf8, 0x9a, 0x6e, 0x42, 0x27, 0xd8, 0x18, 0xf6, 0xfe, 0xfc,
+ 0x6a, 0x74, 0x38, 0xe9, 0x9d, 0x97, 0xf0, 0x36, 0x13, 0x65, 0x84, 0x1b,
+ 0x97, 0xe4, 0x9c, 0xfe, 0x53, 0xf0, 0x27, 0xc0, 0xc7, 0xf9, 0x62, 0xcb,
+ 0xb6, 0x0b, 0xba, 0x89, 0x85, 0xb3, 0x45, 0x67, 0x16, 0xa7, 0xd8, 0x6c,
+ 0xf3, 0x44, 0x10, 0x2b, 0x0d, 0xcf, 0x43, 0x25, 0xd2, 0x74, 0x73, 0xff,
+ 0x2e, 0x91, 0x86, 0xb5, 0x7a, 0xda, 0xc1, 0x67, 0x1b, 0x96, 0x00, 0x28,
+ 0xca, 0xb4, 0x74, 0xf2, 0xf4, 0xd2, 0x10, 0x15, 0x58, 0x2d, 0x19, 0x97,
+ 0xd1, 0xc6, 0xd2, 0xac, 0x7a, 0xd6, 0x9e, 0x00, 0x6c, 0x85, 0xfb, 0xad,
+ 0x3a, 0x6a, 0x8c, 0x8a, 0x3a, 0xa3, 0x39, 0x4e, 0x75, 0xf4, 0xb5, 0x8a,
+ 0x16, 0x37, 0x61, 0x60, 0x36, 0x7b, 0xf2, 0xd8, 0x4b, 0x56, 0xfe, 0x4d,
+ 0x47, 0xe4, 0x17, 0x79, 0x08, 0xa9, 0x85, 0x42, 0x13, 0xbe, 0x5c, 0x02,
+ 0x8b, 0x46, 0x1d, 0x36, 0x78, 0x97, 0x09, 0x13, 0xbd, 0x0d, 0xc9, 0xab,
+ 0x3a, 0x64, 0x2c, 0xc2, 0x3c, 0x38, 0x6f, 0xc6, 0xdc, 0x73, 0x0d, 0xb8,
+ 0xb8, 0x9c, 0x12, 0x85, 0x07, 0x39, 0xaa, 0xbb, 0xa0, 0x69, 0x85, 0x6f,
+ 0x7d, 0x8d, 0xcb, 0x9c, 0x22, 0x34, 0xea, 0xe5, 0x53, 0x38, 0x41, 0x15,
+ 0x4e, 0xe8, 0x77, 0x30, 0xa0, 0x21, 0xfa, 0xd1, 0x89, 0xcf, 0x18, 0xdf,
+ 0xa8, 0x11, 0xbf, 0xfb, 0x5b, 0xb3, 0x90, 0xb2, 0xf7, 0x28, 0x9c, 0x69,
+ 0x86, 0x4f, 0x9e, 0xd2, 0xd2, 0x81, 0x1c, 0xaa, 0xe7, 0xc9, 0xe2, 0x1b,
+ 0xf0, 0xde, 0x04, 0x6e, 0xca, 0xda, 0x4a, 0x80, 0xb1, 0x3d, 0x14, 0x93,
+ 0x3a, 0x6f, 0x52, 0xea, 0x55, 0x1f, 0x98, 0x17, 0xfe, 0x56, 0xdf, 0x7e,
+ 0x33, 0x03, 0x96, 0x26, 0xaa, 0xf3, 0xd2, 0x70, 0xfe, 0x5f, 0xb3, 0xac,
+ 0x97, 0x9a, 0x7e, 0xef, 0xeb, 0x45, 0x23, 0x06, 0x94, 0x51, 0x33, 0x63,
+ 0x57, 0xce, 0xad, 0x81, 0xc7, 0xbc, 0xa5, 0x74, 0xfd, 0x67, 0xda, 0xe9,
+ 0x67, 0x59, 0x49, 0xd0, 0x35, 0xb1, 0x7f, 0x23, 0x27, 0x7e, 0xe4, 0xd1,
+ 0x73, 0x25, 0xac, 0x2d, 0xd4, 0x30, 0x4c, 0x0a, 0x1f, 0x13, 0x54, 0x5c,
+ 0x80, 0x7d, 0x80, 0x85, 0xd2, 0x73, 0x91, 0x08, 0x66, 0x22, 0x5a, 0x0c,
+ 0xe1, 0xbb, 0x63, 0xab, 0x6f, 0xb9, 0x49, 0xe3, 0x21, 0xaa, 0x63, 0x91,
+ 0xf5, 0x44, 0x61, 0xd1, 0x5b, 0x5f, 0xb0, 0x47, 0x6a, 0xa2, 0xff, 0xbd,
+ 0x9e, 0xe7, 0x2d, 0x09, 0x27, 0x8d, 0xc4, 0x72, 0x55, 0xa5, 0x7e, 0xea,
+ 0xc9, 0xd6, 0x0b, 0xe1, 0xc3, 0xd6, 0x90, 0x9c, 0x0a, 0x18, 0xa9, 0xfc,
+ 0x35, 0xe5, 0x49, 0xd3, 0x80, 0x16, 0x05, 0x84, 0xdf, 0x05, 0x32, 0x3b,
+ 0xd8, 0x8c, 0x65, 0xfb, 0xc7, 0xaa, 0x73, 0xeb, 0x06, 0x5b, 0x3d, 0xff,
+ 0x8e, 0xf2, 0x20, 0xe5, 0x57, 0x83, 0x7a, 0x25, 0x53, 0x08, 0xc1, 0x1f,
+ 0x4b, 0xb7, 0xa5, 0xa4, 0x20, 0x4f, 0x61, 0xf4, 0x40, 0x15, 0x53, 0xc1,
+ 0x94, 0x11, 0xe5, 0x28, 0xab, 0x52, 0x22, 0xaf, 0x3b, 0x80, 0x31, 0xe2,
+ 0x6f, 0xaa, 0xde, 0xc5, 0x86, 0xb3, 0x6e, 0xe3, 0x3b, 0x27, 0x7c, 0xb9,
+ 0x24, 0xa5, 0x71, 0x57, 0x8a, 0x56, 0x17, 0x7f, 0xd5, 0x1b, 0x7e, 0x5a,
+ 0x8d, 0x8e, 0xcd, 0xfb, 0xc2, 0x4f, 0x3f, 0x8a, 0x05, 0x1a, 0x9e, 0x84,
+ 0x31, 0x26, 0x0e, 0xe4, 0x5e, 0x3f, 0x28, 0xe8, 0x33, 0x55, 0x34, 0x58,
+ 0x7b, 0xb7, 0x21, 0x0b, 0x67, 0x90, 0xd7, 0xcd, 0xdf, 0x2b, 0x2e, 0xd5,
+ 0x50, 0x1a, 0x1b, 0x21, 0x57, 0x1e, 0x9d, 0x17, 0x48, 0x3a, 0x00, 0xf8,
+ 0xc9, 0xca, 0xf6, 0x81, 0x5d, 0x74, 0x6b, 0x8c, 0x91, 0x16, 0x87, 0xf6,
+ 0xd9, 0xca, 0x6c, 0xce, 0xb5, 0xb5, 0x49, 0x6e, 0x5d, 0xda, 0x7b, 0x53,
+ 0xff, 0x5b, 0x4b, 0x1a, 0x59, 0x61, 0x1d, 0x99, 0x2f, 0x50, 0xe1, 0x32,
+ 0x63, 0x4e, 0xd1, 0x46, 0x22, 0x41, 0x88, 0x32, 0x43, 0x97, 0xfc, 0xa2,
+ 0xcc, 0x42, 0x53, 0xbb, 0x14, 0x34, 0x42, 0x6c, 0x2d, 0x1a, 0xc0, 0xf1,
+ 0x0e, 0xe4, 0x43, 0x43, 0xe3, 0xf6, 0xf0, 0xaa, 0x0e, 0x73, 0x68, 0xe5,
+ 0x8c, 0xcd, 0xe5, 0x37, 0x39, 0x8e, 0x95, 0xab, 0x95, 0xdb, 0x4f, 0x88,
+ 0x9f, 0x4b, 0xa9, 0xae, 0x50, 0x81, 0xd6, 0x77, 0x6a, 0xb2, 0xd6, 0x24,
+ 0xd5, 0x8f, 0xc2, 0xd8, 0x65, 0x97, 0x78, 0x84, 0x30, 0x47, 0x6a, 0x53,
+ 0x1e, 0xa7, 0x27, 0x77, 0xc2, 0x94, 0x0a, 0xb3, 0x8c, 0x97, 0x16, 0x70,
+ 0xca, 0x16, 0x10, 0xf4, 0xad, 0xf7, 0x8b, 0x4f, 0x94, 0x7f, 0xb4, 0x0e,
+ 0x0a, 0x12, 0xe4, 0x08, 0x71, 0xe7, 0x3e, 0xbe, 0x10, 0xd9, 0xd1, 0xed,
+ 0x20, 0x9b, 0xee, 0xcd, 0x8d, 0xbf, 0xcb, 0x42, 0x7e, 0xc7, 0x77, 0xad,
+ 0x69, 0xa0, 0x0c, 0xfb, 0x88, 0xf4, 0x8c, 0x82, 0x50, 0xc2, 0xaf, 0xb1,
+ 0x8d, 0x5e, 0xb3, 0x3d, 0x41, 0xee, 0x4d, 0x9c, 0x36, 0x33, 0xa4, 0x4c,
+ 0x8e, 0x72, 0x85, 0xbf, 0x0d, 0x82, 0x53, 0xf3, 0x28, 0x7f, 0xd6, 0xec,
+ 0x99, 0xa8, 0xf4, 0xfe, 0xde, 0xf4, 0xde, 0xd7, 0xd6, 0xd7, 0x1c, 0x29,
+ 0x93, 0x3a, 0x6b, 0x3f, 0x70, 0x46, 0xd0, 0x20, 0x8f, 0xe4, 0xb4, 0x0f,
+ 0x57, 0xb4, 0x48, 0x97, 0xb1, 0x83, 0x2a, 0x3b, 0x61, 0x3f, 0xec, 0xdc,
+ 0x2b, 0x2d, 0xb6, 0x54, 0x6b, 0x3c, 0x86, 0xf0, 0x8d, 0xd7, 0xad, 0x44,
+ 0xc6, 0x49, 0x6f, 0xa9, 0xc4, 0x13, 0x57, 0x94, 0x86, 0xc9, 0xe2, 0x76,
+ 0xbe, 0xe6, 0xb7, 0xad, 0x24, 0xbe, 0x3d, 0xae, 0x94, 0x3e, 0x32, 0x1e,
+ 0x71, 0x72, 0xb7, 0x01, 0xcd, 0x2c, 0x9d, 0x11, 0x24, 0xb9, 0xa1, 0x9b,
+ 0x35, 0x1c, 0x0e, 0xb6, 0x8c, 0x48, 0x1f, 0x5f, 0xbe, 0xc3, 0x65, 0x88,
+ 0xa3, 0x66, 0x21, 0x1f, 0x33, 0xda, 0xf5, 0x4e, 0x30, 0x50, 0x76, 0xb6,
+ 0x77, 0x5f, 0xfb, 0xa6, 0xc4, 0xfc, 0x18, 0x92, 0xe0, 0x07, 0x98, 0x88,
+ 0xcb, 0x50, 0x19, 0x21, 0xc8, 0x3a, 0x80, 0xe5, 0xe9, 0xe9, 0x42, 0xe3,
+ 0xfc, 0x80, 0x3e, 0x61, 0xc5, 0xee, 0x5c, 0xb7, 0xd7, 0x16, 0x42, 0x4a,
+ 0xdc, 0x82, 0x91, 0x98, 0xeb, 0x06, 0xbe, 0x2c, 0xcf, 0xe6, 0x5c, 0x80,
+ 0x99, 0x24, 0x0c, 0xe2, 0xa5, 0x83, 0x6d, 0xb1, 0xc0, 0x5b, 0x2f, 0x93,
+ 0xd4, 0x91, 0x1d, 0x70, 0x15, 0x7e, 0x25, 0xdc, 0x23, 0x89, 0xfb, 0xc4,
+ 0x86, 0xcd, 0x8b, 0x46, 0x0c, 0xd9, 0x64, 0x32, 0x0a, 0x28, 0xc9, 0xeb,
+ 0xcd, 0xc4, 0xaa, 0x81, 0x1c, 0x5d, 0xb4, 0x1f, 0x07, 0xea, 0x7e, 0x0b,
+ 0xdb, 0xf6, 0xb6, 0x41, 0x68, 0x15, 0x0b, 0x3a, 0xdf, 0xbd, 0x47, 0x9f,
+ 0xee, 0x9f, 0xe9, 0x4f, 0x61, 0xa4, 0x91, 0xfd, 0x18, 0xda, 0x07, 0xec,
+ 0xd7, 0xc5, 0x36, 0x1c, 0x8a, 0xe2, 0xe5, 0x96, 0xc6, 0xe3, 0x86, 0xd2,
+ 0x9e, 0xcd, 0x16, 0x6b, 0xe4, 0x33, 0xb7, 0xff, 0xa5, 0x31, 0xf8, 0xdf,
+ 0xd0, 0xb1, 0xbe, 0xc7, 0x29, 0x09, 0xfb, 0x19, 0x86, 0xaf, 0xf4, 0xe3,
+ 0x53, 0x98, 0x33, 0x64, 0x56, 0xa2, 0x57, 0x2d, 0x06, 0x0e, 0xc4, 0xe7,
+ 0x06, 0xdd, 0x56, 0xc7, 0x2a, 0xb3, 0x09, 0xb4, 0xf0, 0x80, 0xaa, 0x3d,
+ 0xf5, 0x97, 0xc6, 0x41, 0x0d, 0x89, 0x69, 0xf8, 0x7e, 0xc4, 0xee, 0x39,
+ 0xc3, 0x9f, 0x74, 0xe4, 0x40, 0x80, 0x3c, 0xf5, 0x4d, 0x91, 0xaf, 0xc0,
+ 0xf1, 0x57, 0x2d, 0xf8, 0xa2, 0x1c, 0x21, 0xe5, 0x50, 0xaf, 0x0c, 0xa1,
+ 0xfb, 0x79, 0xa4, 0x7c, 0x46, 0x22, 0x3f, 0x72, 0xdf, 0x8f, 0x93, 0xe8,
+ 0x62, 0x1f, 0x9e, 0x11, 0x76, 0x3b, 0xeb, 0x49, 0x77, 0x7c, 0x03, 0x66,
+ 0xcf, 0x91, 0x4f, 0x02, 0x0b, 0xdf, 0x15, 0xc1, 0x1f, 0x23, 0x2e, 0x0d,
+ 0x2b, 0x73, 0xca, 0x82, 0x35, 0xdb, 0xb5, 0xe2, 0x8c, 0x1f, 0x00, 0xbc,
+ 0xec, 0xb1, 0xe4, 0x50, 0xca, 0x49, 0x81, 0x69, 0x73, 0xc1, 0x14, 0x06,
+ 0x5c, 0x65, 0xff, 0xc9, 0x22, 0x1c, 0x32, 0x12, 0xca, 0x98, 0x49, 0x0b,
+ 0xdb, 0x8f, 0xd3, 0xac, 0x43, 0x6f, 0x42, 0xdc, 0xc7, 0xa0, 0x6f, 0x53,
+ 0xd6, 0x0f, 0x9b, 0xa4, 0xf8, 0x93, 0x65, 0x2e, 0xe1, 0xf6, 0x49, 0xc5,
+ 0x47, 0x88, 0xca, 0xfd, 0xc2, 0xc7, 0x97, 0xe9, 0xef, 0x9a, 0xd3, 0xa1,
+ 0x2a, 0x9c, 0x8e, 0xbf, 0xd7, 0x17, 0x09, 0xba, 0xa7, 0x3e, 0x67, 0xa8,
+ 0x69, 0xbe, 0xc5, 0xb7, 0xc6, 0x7c, 0xe5, 0xea, 0xc2, 0x42, 0xdc, 0xcd,
+ 0x70, 0xc7, 0xab, 0xef, 0x42, 0x52, 0xe4, 0x8d, 0xd9, 0x31, 0xd4, 0x43,
+ 0xf0, 0xe5, 0x53, 0x15, 0x14, 0x2b, 0x21, 0xd0, 0x9d, 0x2a, 0xdc, 0x7a,
+ 0x81, 0x4b, 0x0b, 0x37, 0x54, 0x0f, 0x53, 0xac, 0x20, 0xb0, 0xfb, 0xaa,
+ 0x41, 0x99, 0x62, 0xae, 0xed, 0xbe, 0xeb, 0x2f, 0x70, 0x1b, 0x36, 0x75,
+ 0xbb, 0xa9, 0x18, 0x1c, 0x59, 0xa1, 0x33, 0x30, 0x7f, 0x3f, 0x56, 0x1d,
+ 0x3f, 0x88, 0x7c, 0x3f, 0xd9, 0x46, 0xa3, 0x92, 0x5c, 0x09, 0x37, 0x31,
+ 0x2e, 0xa5, 0x47, 0x65, 0xad, 0x94, 0x22, 0xcf, 0x3e, 0x8c, 0x29, 0x91,
+ 0x6f, 0xeb, 0x9e, 0xac, 0xe4, 0x93, 0x69, 0x85, 0xae, 0x6f, 0x91, 0xa9,
+ 0xa9, 0x52, 0xfd, 0xc7, 0x7e, 0x25, 0x50, 0x32, 0x98, 0x96, 0x7c, 0xb3,
+ 0x88, 0x23, 0x04, 0xbd, 0x7c, 0x34, 0x26, 0x71, 0xc0, 0xee, 0xb0, 0x5a,
+ 0xc8, 0x16, 0xc6, 0x6f, 0x32, 0x9a, 0x37, 0x8a, 0x5f, 0xf7, 0x2d, 0xed,
+ 0x73, 0xfe, 0x30, 0x84, 0x7d, 0x33, 0xc7, 0x36, 0x37, 0xec, 0xec, 0xda,
+ 0x87, 0xb8, 0x55, 0xc0, 0xd6, 0x9d, 0x95, 0x0d, 0xa6, 0x93, 0x01, 0x19,
+ 0x6f, 0x10, 0x42, 0x89, 0x36, 0x39, 0x44, 0x39, 0x27, 0x75, 0xbb, 0x89,
+ 0x31, 0x26, 0x03, 0x27, 0x2a, 0x34, 0xf5, 0x1e, 0x27, 0xaa, 0x3d, 0xfe,
+ 0x2c, 0x2b, 0x07, 0x9a, 0x7e, 0xf0, 0x4a, 0xc2, 0x46, 0xf7, 0x45, 0x53,
+ 0xb6, 0x94, 0x7d, 0x75, 0x01, 0x6f, 0xc9, 0x02, 0x3e, 0x85, 0x1f, 0x37,
+ 0x0e, 0x8e, 0xfc, 0x72, 0x2e, 0xe2, 0x69, 0x6b, 0xeb, 0x77, 0xd3, 0xf4,
+ 0xae, 0x7f, 0x77, 0xda, 0x18, 0x4f, 0x5a, 0x5d, 0xca, 0xe7, 0xd4, 0xd8,
+ 0x8e, 0x15, 0x82, 0x41, 0x3f, 0xf6, 0xa4, 0x6b, 0x8a, 0x36, 0xc4, 0xa7,
+ 0x10, 0xbd, 0xdc, 0xf9, 0xe0, 0xd9, 0x03, 0xf9, 0x41, 0x39, 0xe5, 0xad,
+ 0xb3, 0xc7, 0xa7, 0x6a, 0x37, 0x1b, 0x23, 0x54, 0xc8, 0x8d, 0x51, 0x41,
+ 0x45, 0x24, 0xe9, 0xf1, 0x94, 0xc6, 0x3e, 0xb3, 0x17, 0x6b, 0xb3, 0x9b,
+ 0xbf, 0xd3, 0x62, 0xed, 0x36, 0x4e, 0x00, 0x88, 0x6c, 0x7b, 0xf0, 0xce,
+ 0x90, 0x4f, 0x0b, 0xa6, 0xb3, 0xa5, 0x66, 0x2d, 0x90, 0x61, 0x21, 0x86,
+ 0x02, 0xf4, 0x0c, 0x80, 0xb4, 0xbc, 0x2b, 0xb9, 0xd1, 0x20, 0x53, 0xd3,
+ 0x24, 0x1f, 0x0f, 0xac, 0x75, 0x92, 0x1b, 0x2b, 0x72, 0x95, 0x57, 0xbb,
+ 0xa6, 0xaa, 0xf5, 0x49, 0xbc, 0xdd, 0x87, 0x36, 0x36, 0xa3, 0xec, 0x0e,
+ 0x23, 0x94, 0x96, 0xcb, 0x10, 0x00, 0xc1, 0x6b, 0x6b, 0xd2, 0x8a, 0x07,
+ 0x66, 0x81, 0xfc, 0x5d, 0xe7, 0x40, 0xfc, 0x54, 0x96, 0x4d, 0x4c, 0x5a,
+ 0xcb, 0x68, 0x91, 0xbf, 0xe9, 0xb1, 0x18, 0x05, 0xb9, 0xeb, 0x1e, 0x9d,
+ 0xc6, 0xcb, 0xe1, 0xcf, 0x82, 0x84, 0xae, 0x39, 0x45, 0x2f, 0x99, 0x7d,
+ 0xcb, 0x78, 0xa0, 0xff, 0x35, 0x6d, 0xee, 0x4a, 0xc5, 0x63, 0x18, 0xad,
+ 0x62, 0xd3, 0xa6, 0xc6, 0x8b, 0x48, 0x65, 0x2d, 0x06, 0x18, 0xe1, 0x3e,
+ 0xe8, 0x5c, 0x18, 0x7f, 0x20, 0x1e, 0x0a, 0xdb, 0xce, 0x56, 0x78, 0x7b,
+ 0x6d, 0x23, 0xc8, 0xae, 0x66, 0x10, 0xb2, 0x8f, 0xd3, 0x33, 0x5d, 0x2c,
+ 0x29, 0x65, 0xa9, 0xac, 0xa9, 0x05, 0xbd, 0x3c, 0xa9, 0xd7, 0xea, 0xe9,
+ 0x81, 0x6d, 0x5c, 0x7e, 0x9d, 0x70, 0x85, 0x1a, 0xb2, 0x13, 0x5f, 0x34,
+ 0x74, 0xb2, 0x05, 0x78, 0x51, 0xd0, 0x72, 0x51, 0x20, 0x8b, 0x90, 0xec,
+ 0xaa, 0xfa, 0x2e, 0xf1, 0x6f, 0xf9, 0x25, 0x8e, 0xe4, 0x5c, 0x5a, 0x0c,
+ 0x0d, 0xba, 0x26, 0x69, 0x56, 0xf5, 0x94, 0x4a, 0x22, 0x54, 0x77, 0xd4,
+ 0xbe, 0x8e, 0xb7, 0xda, 0xee, 0x1d, 0x97, 0x06, 0xb0, 0xe4, 0xed, 0x17,
+ 0x34, 0x93, 0x2d, 0x4d, 0xbe, 0x4d, 0xf2, 0x98, 0x3f, 0x67, 0x61, 0xe1,
+ 0x6e, 0x70, 0xb6, 0x28, 0x7e, 0x84, 0x38, 0x0c, 0xc6, 0x28, 0x45, 0x8f,
+ 0x22, 0xe9, 0x4d, 0xb5, 0xf7, 0x61, 0xfd, 0x97, 0xf4, 0xcc, 0xa9, 0x38,
+ 0x85, 0x06, 0xb7, 0x7e, 0x13, 0xbb, 0x0d, 0x16, 0x4c, 0x33, 0x96, 0x79,
+ 0x89, 0x5b, 0x5f, 0x44, 0xea, 0xfe, 0xc6, 0xce, 0xb8, 0xd7, 0xe7, 0x75,
+ 0xc7, 0xcd, 0x84, 0x59, 0x8b, 0xf9, 0x1f, 0xf0, 0x65, 0x3a, 0x74, 0x26,
+ 0x5e, 0xc1, 0xde, 0xa5, 0x1c, 0x1c, 0x4f, 0xa3, 0x24, 0x3d, 0x09, 0xb7,
+ 0x43, 0xc0, 0x31, 0x0d, 0x89, 0x62, 0x63, 0xf8, 0x14, 0x9f, 0x28, 0x24,
+ 0xf0, 0x2b, 0xae, 0x26, 0x45, 0xc1, 0xd3, 0x95, 0x48, 0xc8, 0x0e, 0x3d,
+ 0xac, 0xf0, 0x24, 0x8c, 0x48, 0x7c, 0xe8, 0x1a, 0x54, 0x3c, 0xbf, 0xf7,
+ 0xb0, 0xd7, 0x4a, 0x9a, 0x95, 0xe0, 0x28, 0xa0, 0x46, 0x07, 0x3d, 0x90,
+ 0xcd, 0x08, 0x80, 0x16, 0x3c, 0x4d, 0x6d, 0x17, 0x56, 0xe8, 0xb4, 0x6c,
+ 0x14, 0x7c, 0x73, 0x56, 0xaf, 0xef, 0xef, 0xae, 0xe4, 0x88, 0x55, 0xe3,
+ 0x53, 0x64, 0x25, 0x2c, 0x03, 0x28, 0x4e, 0x7c, 0xb5, 0x4d, 0x32, 0xb1,
+ 0xd8, 0x1f, 0x2a, 0x3b, 0xe4, 0x35, 0x82, 0x68, 0x06, 0x99, 0xf9, 0x07,
+ 0x56, 0x0d, 0xdb, 0x0d, 0x98, 0x13, 0x89, 0xd3, 0xee, 0xc2, 0x94, 0x4a,
+ 0xfb, 0x58, 0xf5, 0x12, 0x28, 0xe7, 0x2b, 0x67, 0x3b, 0xe0, 0x41, 0xe8,
+ 0x7d, 0xe7, 0xba, 0x54, 0xa2, 0x0a, 0xa8, 0x6c, 0x07, 0x8e, 0x66, 0x6c,
+ 0x64, 0xfd, 0x96, 0x15, 0x0f, 0xd7, 0x58, 0x38, 0xc4, 0xcb, 0x0c, 0xec,
+ 0x98, 0xec, 0x85, 0x26, 0x33, 0x66, 0xe6, 0xd6, 0xd7, 0xd1, 0xc4, 0xcf,
+ 0xe5, 0xcd, 0x20, 0xf2, 0x1b, 0x11, 0x90, 0x22, 0x46, 0x67, 0x47, 0x68,
+ 0xc4, 0xda, 0x47, 0xe0, 0x27, 0x09, 0x9e, 0x2d, 0x06, 0xa3, 0x98, 0xf9,
+ 0xd4, 0xde, 0x3e, 0xaf, 0xee, 0x1b, 0x9b, 0x3d, 0x05, 0xb0, 0x13, 0xa5,
+ 0xe9, 0xf7, 0xcf, 0x0f, 0xc2, 0x28, 0xa1, 0xac, 0xa1, 0xfc, 0x7b, 0xa3,
+ 0x6b, 0xee, 0x3b, 0xfc, 0xf0, 0xd1, 0xb3, 0x05, 0x59, 0xc0, 0x21, 0xc1,
+ 0x84, 0x24, 0x71, 0x55, 0x7e, 0x78, 0x0c, 0x12, 0x0f, 0x8a, 0xcb, 0x86,
+ 0xa6, 0x81, 0xba, 0xd0, 0xc7, 0x34, 0xb2, 0x69, 0x60, 0xb0, 0x50, 0x42,
+ 0xea, 0x46, 0xb7, 0x51, 0x5d, 0x69, 0x53, 0x65, 0xe5, 0xd9, 0x1c, 0x6d,
+ 0x61, 0xb4, 0x8e, 0x27, 0x5b, 0x46, 0xb1, 0x4c, 0x9d, 0xf8, 0x41, 0x7d,
+ 0x0e, 0xfa, 0x56, 0x5d, 0xfb, 0xfa, 0x9a, 0x1d, 0xd3, 0x2a, 0x6e, 0x13,
+ 0xa0, 0x4a, 0x61, 0x70, 0x94, 0xc2, 0x69, 0x91, 0xfd, 0x2b, 0x67, 0x55,
+ 0x7a, 0x5a, 0x10, 0xff, 0x06, 0xbb, 0xcf, 0x71, 0x75, 0x61, 0x38, 0xfa,
+ 0x57, 0xdb, 0x2a, 0xc0, 0xd3, 0x53, 0x2b, 0x46, 0x14, 0x60, 0x7e, 0xa4,
+ 0x19, 0x5f, 0xd8, 0xe5, 0x36, 0x27, 0xa8, 0x80, 0xbb, 0x86, 0x18, 0x4c,
+ 0xbe, 0x54, 0xfe, 0xb0, 0x10, 0x79, 0xd3, 0x09, 0x1a, 0xe3, 0xdf, 0xee,
+ 0x4d, 0x96, 0xab, 0x0f, 0x8a, 0xf9, 0x1a, 0x5b, 0x06, 0x8e, 0x52, 0xca,
+ 0x90, 0xa1, 0x8b, 0x62, 0xc0, 0x2d, 0xe5, 0x2c, 0x6d, 0x69, 0x13, 0xbb,
+ 0x86, 0x2e, 0xb3, 0xb2, 0x06, 0x14, 0x52, 0x04, 0x72, 0x67, 0x55, 0x1c,
+ 0xf1, 0x6a, 0xdf, 0x3d, 0xe2, 0xfb, 0xf1, 0x43, 0xef, 0x22, 0xc9, 0x5e,
+ 0xd3, 0x14, 0x09, 0x5a, 0x2e, 0x0c, 0xb6, 0x89, 0x05, 0xda, 0xf0, 0x3a,
+ 0x0f, 0xed, 0x51, 0x65, 0x86, 0x12, 0x68, 0xd3, 0xa3, 0xfb, 0x14, 0x6d,
+ 0x59, 0x07, 0xeb, 0xa2, 0xd8, 0x86, 0xef, 0x04, 0xe1, 0xbe, 0xeb, 0xff,
+ 0xb9, 0x76, 0xaf, 0x90, 0x6a, 0x9e, 0xbb, 0x5d, 0xef, 0x57, 0x1b, 0x7e,
+ 0xc0, 0xe8, 0x13, 0x30, 0xc9, 0xad, 0x83, 0x16, 0x2c, 0x2d, 0x98, 0x6d,
+ 0x4d, 0x8e, 0x92, 0x88, 0x42, 0x56, 0xce, 0x16, 0xfb, 0x06, 0x00, 0x53,
+ 0xb5, 0x03, 0xbe, 0x0f, 0xf4, 0xfd, 0x11, 0xf2, 0x73, 0xb7, 0xae, 0xea,
+ 0xfc, 0x1f, 0xea, 0xbf, 0x19, 0xee, 0xa0, 0xc7, 0x0d, 0x4e, 0x1e, 0xc1,
+ 0xa4, 0x83, 0xef, 0xdd, 0xb8, 0x1d, 0x23, 0x18, 0x99, 0xb2, 0x13, 0xa9,
+ 0xe6, 0x31, 0x4c, 0xc1, 0x28, 0x4a, 0xb8, 0xc9, 0x30, 0xab, 0xe9, 0xd1,
+ 0x49, 0x58, 0x3b, 0x4f, 0x7f, 0x9c, 0x58, 0x3a, 0x34, 0xc3, 0x12, 0x17,
+ 0x99, 0x4f, 0x81, 0x97, 0x74, 0xaf, 0x71, 0x3c, 0x1a, 0xe3, 0x26, 0x48,
+ 0xb5, 0x05, 0x5e, 0x8e, 0xa8, 0xe9, 0x81, 0xb7, 0x96, 0xec, 0x16, 0xa5,
+ 0x37, 0x07, 0xe1, 0xa7, 0x8e, 0xb7, 0x61, 0xe0, 0xb9, 0xf4, 0xf3, 0x55,
+ 0x4d, 0xc2, 0xc8, 0xb5, 0xe7, 0x8c, 0x85, 0x7a, 0x92, 0x6f, 0x39, 0x1c,
+ 0x17, 0x8b, 0x1f, 0xcc, 0x05, 0x9b, 0x6c, 0x04, 0xda, 0x54, 0x61, 0xb9,
+ 0x41, 0x87, 0x27, 0x38, 0x50, 0x35, 0xa6, 0x8e, 0x3b, 0x66, 0x5c, 0xd8,
+ 0x4a, 0xcd, 0x05, 0x28, 0xb3, 0x29, 0xa5, 0x84, 0xad, 0x8b, 0xea, 0x21,
+ 0x94, 0x04, 0xe9, 0x97, 0x2d, 0x98, 0x44, 0x4b, 0xa9, 0x53, 0x4f, 0xb9,
+ 0x0e, 0x99, 0x50, 0xb4, 0x35, 0xc8, 0x10, 0x6b, 0x4f, 0xcd, 0xa7, 0x4a,
+ 0x69, 0xf2, 0xd0, 0x09, 0x36, 0x20, 0x57, 0xe5, 0xe7, 0x4a, 0x39, 0x6e,
+ 0x73, 0xf1, 0x9b, 0xc7, 0x59, 0x6b, 0x47, 0x73, 0xb9, 0xcc, 0xa2, 0x81,
+ 0x5e, 0x08, 0xd6, 0x18, 0xfe, 0x5c, 0x16, 0x4f, 0x08, 0xac, 0xd3, 0x5b,
+ 0xe0, 0x41, 0x64, 0x8a, 0x06, 0xd4, 0x84, 0xe8, 0x72, 0xe7, 0x07, 0xde,
+ 0xed, 0x69, 0x88, 0xb3, 0xba, 0x04, 0x54, 0x5a, 0xec, 0x1d, 0x04, 0x97,
+ 0xca, 0x27, 0x7e, 0x2c, 0x5a, 0x25, 0xa3, 0xec, 0x01, 0x18, 0xfb, 0x60,
+ 0x34, 0xbf, 0xdd, 0xfd, 0xa6, 0xfa, 0xe6, 0x47, 0xfe, 0x07, 0x2c, 0x20,
+ 0x0e, 0x1c, 0x1c, 0x68, 0xe4, 0x03, 0x08, 0x37, 0xd6, 0xd4, 0x37, 0x12,
+ 0x02, 0xf8, 0x66, 0xff, 0x61, 0x59, 0x07, 0x7b, 0xac, 0x92, 0x01, 0x28,
+ 0x3c, 0xa1, 0x66, 0x8e, 0x05, 0xbb, 0x79, 0x5a, 0xfb, 0x89, 0xff, 0x13,
+ 0x24, 0x1c, 0xf9, 0x32, 0x36, 0x18, 0x50, 0xa2, 0xa6, 0x37, 0x90, 0x19,
+ 0x02, 0x8b, 0xa1, 0x1a, 0x34, 0xd3, 0x10, 0xef, 0xf2, 0xa2, 0xe3, 0x96,
+ 0x19, 0xbe, 0x8c, 0xb0, 0xba, 0xe8, 0xd5, 0xf1, 0x2e, 0xf0, 0x52, 0x83,
+ 0xc5, 0xe8, 0x56, 0xca, 0xae, 0x41, 0x0f, 0x0a, 0x58, 0x3e, 0xa1, 0x58,
+ 0x39, 0xb2, 0x05, 0x23, 0x46, 0x2e, 0xb2, 0xb2, 0x7c, 0x43, 0xda, 0x28,
+ 0x49, 0x7b, 0x96, 0xe9, 0xdd, 0x26, 0x0e, 0x6e, 0x8c, 0xd6, 0x3c, 0xbc,
+ 0xd8, 0xdd, 0x63, 0xe6, 0x2e, 0x84, 0x8f, 0x65, 0xa3, 0x13, 0x60, 0xf7,
+ 0x9c, 0xbc, 0x81, 0xfa, 0xa4, 0x74, 0x49, 0x5f, 0xb4, 0x27, 0xf2, 0x45,
+ 0x20, 0xe4, 0x60, 0xc3, 0xa0, 0xa2, 0x75, 0xf4, 0xfd, 0x53, 0x45, 0xdb,
+ 0x0f, 0x4b, 0xb4, 0x87, 0x7e, 0xea, 0xea, 0x32, 0x9e, 0x10, 0x1d, 0xfe,
+ 0x23, 0x99, 0xee, 0xd8, 0x96, 0x6b, 0x79, 0x49, 0x4d, 0xfa, 0x31, 0x63,
+ 0xcb, 0xe1, 0x80, 0xe1, 0xab, 0x20, 0x68, 0x1b, 0x00, 0x41, 0x96, 0x2f,
+ 0xce, 0x82, 0xf1, 0x30, 0x6e, 0xfe, 0x5b, 0xea, 0x83, 0x1c, 0x63, 0x3a,
+ 0xcd, 0x87, 0xca, 0xb3, 0xf3, 0xc8, 0x0b, 0x8f, 0x1c, 0x1b, 0x3d, 0xfa,
+ 0xfd, 0x1f, 0xf2, 0x01, 0x4a, 0x48, 0x74, 0x5a, 0xba, 0x86, 0x75, 0xfa,
+ 0xff, 0x14, 0xe1, 0xf7, 0xad, 0x8e, 0x4f, 0xe7, 0x27, 0x62, 0x1b, 0x05,
+ 0x40, 0x7f, 0x41, 0x37, 0xcd, 0x9e, 0x18, 0x3b, 0x0e, 0xf6, 0x56, 0xcf,
+ 0xf2, 0x5f, 0x72, 0x5f, 0xfb, 0x8a, 0x1a, 0x96, 0x2c, 0x7f, 0x23, 0xe4,
+ 0xe7, 0xef, 0x33, 0x66, 0xeb, 0xf6, 0x72, 0x65, 0x81, 0x84, 0x90, 0x48,
+ 0xe3, 0xab, 0x09, 0x2a, 0xe1, 0xec, 0xfd, 0x9b, 0x72, 0x95, 0x09, 0xa4,
+ 0x8a, 0x36, 0x1f, 0x18, 0x9c, 0x02, 0x06, 0x98, 0x57, 0xe8, 0xe2, 0xe6,
+ 0xf9, 0x9b, 0xe2, 0x4a, 0x0a, 0x1f, 0x2c, 0xb2, 0x08, 0x6a, 0xda, 0x71,
+ 0x46, 0xdc, 0xcf, 0x65, 0x59, 0xeb, 0x03, 0x14, 0x1c, 0x43, 0x94, 0xa1,
+ 0x3c, 0xe6, 0xee, 0x80, 0x00, 0x9d, 0x7e, 0x9b, 0x6e, 0xac, 0x2f, 0x75,
+ 0x27, 0x75, 0x8f, 0xc7, 0xc4, 0x98, 0xa4, 0xf0, 0x84, 0x7b, 0xc1, 0x3f,
+ 0xf8, 0x89, 0x18, 0xb5, 0x60, 0x2f, 0x16, 0x68, 0x43, 0x87, 0x9d, 0xd7,
+ 0xd2, 0xb0, 0xb2, 0x6d, 0xac, 0xef, 0xdc, 0xcd, 0x0d, 0xa9, 0x73, 0x24,
+ 0x69, 0xcd, 0xbc, 0x77, 0x97, 0x83, 0xa6, 0x52, 0x83, 0x2a, 0x5f, 0xd7,
+ 0xc2, 0x4f, 0x9a, 0xbb, 0x7a, 0x09, 0xa4, 0xd2, 0x64, 0xc2, 0xc6, 0x18,
+ 0xda, 0xd8, 0x20, 0x72, 0xe5, 0x60, 0x92, 0xa5, 0x97, 0x22, 0x63, 0xf5,
+ 0xa8, 0xf1, 0x9f, 0xb2, 0x52, 0x4f, 0x2c, 0x63, 0xee, 0x9b, 0xa3, 0x5f,
+ 0xf9, 0x3f, 0x83, 0xb0, 0x71, 0x0c, 0xb2, 0x07, 0x7f, 0xb6, 0xf8, 0x8f,
+ 0x35, 0xf1, 0xaa, 0x90, 0x4e, 0xcc, 0x52, 0x0b, 0xc7, 0xbe, 0x29, 0xa1,
+ 0xa7, 0xd9, 0xce, 0x50, 0x80, 0x5a, 0x2c, 0xf9, 0x41, 0x93, 0x04, 0x56,
+ 0xad, 0xd0, 0x50, 0xe1, 0x23, 0x84, 0x3d, 0xc8, 0xba, 0xbd, 0xb3, 0x3b,
+ 0x2f, 0xe1, 0x1c, 0xf3, 0xe5, 0x73, 0x3c, 0x51, 0x0b, 0x61, 0xa6, 0x48,
+ 0x1f, 0xcc, 0xd1, 0x96, 0xb6, 0xce, 0x35, 0x22, 0x67, 0x58, 0xf5, 0x58,
+ 0x41, 0x75, 0xf4, 0xf0, 0xe8, 0x35, 0x77, 0xb9, 0x84, 0x82, 0xc6, 0xe1,
+ 0x8a, 0x83, 0xe6, 0x26, 0xe6, 0x30, 0xed, 0x27, 0xf6, 0xaa, 0x91, 0xb6,
+ 0x71, 0x36, 0x54, 0x47, 0xf4, 0x9b, 0xe1, 0xcb, 0x8e, 0xb1, 0xc7, 0x07,
+ 0x92, 0x33, 0x04, 0x87, 0x7e, 0xf2, 0x10, 0xfb, 0x56, 0x8b, 0x22, 0xce,
+ 0xd0, 0x78, 0x85, 0x7b, 0x41, 0xb5, 0x9e, 0x56, 0x26, 0x3e, 0x11, 0x20,
+ 0x38, 0xec, 0xe7, 0x48, 0x37, 0x04, 0xb5, 0x2b, 0x28, 0x1b, 0x26, 0x6f,
+ 0xc6, 0xb5, 0x65, 0x8b, 0x0a, 0x5d, 0x64, 0x44, 0x1c, 0xb9, 0x07, 0x8a,
+ 0x38, 0xa9, 0x20, 0x86, 0x06, 0xdf, 0x1b, 0xee, 0x50, 0xf5, 0x27, 0x85,
+ 0x5d, 0xb8, 0x19, 0x6f, 0xf7, 0xbc, 0x31, 0x27, 0xa6, 0xe9, 0x05, 0xbc,
+ 0x98, 0x35, 0x0b, 0xc5, 0x95, 0x8f, 0x81, 0x21, 0x51, 0xfa, 0xcc, 0xc7,
+ 0xf8, 0xaa, 0x0a, 0x95, 0xae, 0x77, 0x71, 0x0e, 0xa1, 0x70, 0x0e, 0x40,
+ 0x65, 0x0f, 0x90, 0x26, 0x3a, 0xb2, 0x3d, 0x20, 0xeb, 0x0a, 0x8b, 0x0d,
+ 0xc5, 0x1e, 0xfa, 0x78, 0x6e, 0xfe, 0x20, 0x7d, 0xd9, 0x46, 0xc1, 0xee,
+ 0x44, 0x9d, 0x11, 0xd2, 0xd7, 0xa9, 0xc3, 0xa8, 0x9f, 0x59, 0x08, 0x84,
+ 0x9e, 0x77, 0xc4, 0x7d, 0x11, 0x89, 0x4b, 0x75, 0xc0, 0xf7, 0x79, 0xe0,
+ 0xbd, 0x7e, 0xb3, 0xcf, 0x31, 0x2d, 0x50, 0xd3, 0xaf, 0xc1, 0xcc, 0x5b,
+ 0xf2, 0x5a, 0x31, 0x63, 0xf4, 0x5f, 0x01, 0x49, 0x5b, 0xa1, 0x7e, 0x3c,
+ 0x53, 0xc3, 0xf3, 0x42, 0x2d, 0x1e, 0xe0, 0x16, 0x52, 0xdc, 0xff, 0x6b,
+ 0xe0, 0x3d, 0xa5, 0x82, 0x44, 0x0f, 0x76, 0x5b, 0xb6, 0xe5, 0xda, 0x75,
+ 0x1f, 0xf8, 0xbe, 0xd6, 0xb2, 0xe9, 0xb7, 0xd2, 0x99, 0x35, 0xf2, 0x2d,
+ 0xc7, 0xcb, 0xf9, 0x3a, 0x41, 0xcb, 0x11, 0x8e, 0x23, 0x7e, 0xe5, 0xbc,
+ 0xff, 0x4a, 0xa7, 0xc5, 0xbc, 0x2d, 0x22, 0x8b, 0xd2, 0xb4, 0x2f, 0xdf,
+ 0x6b, 0x52, 0x42, 0x3e, 0xbf, 0x6e, 0x39, 0x9c, 0xc2, 0xa9, 0x7b, 0x8c,
+ 0xba, 0xd2, 0x73, 0x0d, 0x4b, 0x5e, 0x16, 0x41, 0xd6, 0x21, 0x81, 0x49,
+ 0x72, 0x78, 0x52, 0x61, 0xad, 0x96, 0x28, 0x23, 0x9c, 0xce, 0xdc, 0x6c,
+ 0x4d, 0xa3, 0xed, 0x2c, 0x40, 0x3c, 0xf1, 0xe2, 0x77, 0x21, 0x69, 0x35,
+ 0xab, 0xe8, 0x2b, 0x48, 0x0d, 0xd3, 0x83, 0x14, 0xbe, 0x3b, 0x0c, 0x36,
+ 0xc8, 0x77, 0x56, 0x35, 0xfd, 0x95, 0x40, 0xe9, 0xfb, 0xfa, 0x6c, 0xd9,
+ 0x2d, 0xf4, 0x78, 0x09, 0x18, 0x24, 0x0d, 0xe7, 0xee, 0xff, 0x5d, 0x68,
+ 0x12, 0x72, 0x79, 0xa8, 0x2f, 0xee, 0x9e, 0x68, 0x9c, 0xf7, 0x6d, 0x07,
+ 0xbf, 0xfb, 0xe2, 0xe0, 0x09, 0x49, 0xac, 0x41, 0x1f, 0x6b, 0xfd, 0x3c,
+ 0xef, 0x83, 0x9d, 0x6d, 0x8a, 0x97, 0xef, 0x3b, 0xc1, 0xb2, 0xfa, 0x3f,
+ 0x17, 0x14, 0x3d, 0xad, 0x33, 0x3e, 0x83, 0xda, 0xd2, 0x14, 0x8a, 0xd7,
+ 0x56, 0x40, 0xaf, 0x71, 0x07, 0xc7, 0xf4, 0x8b, 0xcb, 0x05, 0x11, 0x03,
+ 0x9d, 0x59, 0x83, 0x04, 0xac, 0x80, 0x71, 0xa2, 0x75, 0x0b, 0xee, 0x7f,
+ 0xd2, 0x6f, 0xb7, 0x3b, 0xa5, 0xbb, 0xad, 0x46, 0x43, 0x8f, 0x6d, 0x23,
+ 0x97, 0x39, 0xd6, 0x32, 0x65, 0xe7, 0x6e, 0x1c, 0x68, 0x77, 0xc4, 0x62,
+ 0x25, 0xfb, 0xed, 0xcd, 0x3e, 0x9d, 0xd7, 0xa5, 0x72, 0xfb, 0xad, 0x29,
+ 0x30, 0xd7, 0x13, 0xda, 0x75, 0x57, 0x00, 0x58, 0x4b, 0xab, 0x8c, 0xec,
+ 0xf1, 0x17, 0x49, 0x4e, 0x5b, 0x10, 0x61, 0x5e, 0x5f, 0x17, 0x6a, 0x86,
+ 0xa8, 0xae, 0x52, 0xa8, 0x1b, 0x9d, 0x15, 0x9f, 0xe3, 0x16, 0xa3, 0x38,
+ 0xec, 0x8a, 0x6b, 0xfd, 0x22, 0x9d, 0x9e, 0xf5, 0xf4, 0x6e, 0xca, 0xd8,
+ 0xc6, 0xd9, 0xe2, 0x2f, 0xbb, 0x69, 0x6f, 0x04, 0x86, 0xe7, 0xeb, 0xae,
+ 0xf6, 0xa4, 0x57, 0xd7, 0x24, 0xc4, 0xbb, 0xdb, 0x81, 0x91, 0xa2, 0x7c,
+ 0x80, 0x6d, 0xb9, 0x8b, 0x13, 0xaa, 0xb6, 0x10, 0xd7, 0x33, 0x8d, 0xb7,
+ 0x3b, 0xd1, 0x80, 0xa6, 0x2a, 0x18, 0x52, 0xd8, 0x75, 0x84, 0x18, 0x66,
+ 0x1c, 0x7a, 0xc4, 0x80, 0x23, 0x0d, 0x04, 0x33, 0x77, 0xb3, 0x99, 0x12,
+ 0x33, 0x26, 0x9e, 0xeb, 0xcf, 0xb8, 0xb1, 0x5f, 0xbb, 0x43, 0xc2, 0x2f,
+ 0xdf, 0xf8, 0x7b, 0x8f, 0x6d, 0x23, 0xc1, 0x58, 0x86, 0xde, 0x30, 0xba,
+ 0x32, 0x36, 0x39, 0xe5, 0xc3, 0x48, 0xd9, 0xa7, 0x56, 0x4c, 0x49, 0xb1,
+ 0x96, 0xdf, 0xe0, 0x6b, 0xf0, 0xa8, 0x9e, 0x02, 0xc4, 0x9a, 0x9b, 0x5e,
+ 0xcd, 0x10, 0xc1, 0x68, 0x8b, 0xac, 0x65, 0xf6, 0x7e, 0xc8, 0xf3, 0x23,
+ 0x27, 0x82, 0xbd, 0xd8, 0xfa, 0x16, 0x4d, 0xd9, 0xf7, 0xc1, 0x01, 0x8a,
+ 0x24, 0xce, 0x56, 0xb5, 0x4e, 0xf2, 0xb4, 0xe1, 0x5e, 0x25, 0x18, 0x3b,
+ 0x22, 0x6a, 0xba, 0x63, 0xa9, 0x19, 0xae, 0xee, 0x6d, 0x33, 0xba, 0x78,
+ 0x3e, 0xbe, 0x4b, 0x24, 0x0e, 0x7e, 0x98, 0x8f, 0xe3, 0xd1, 0x6a, 0xe4,
+ 0x4c, 0x87, 0xb0, 0xdc, 0x71, 0x06, 0x5d, 0xd2, 0x37, 0x99, 0xa1, 0x63,
+ 0xc3, 0x4f, 0x96, 0x0a, 0xba, 0x3e, 0xdf, 0x6f, 0xf8, 0x9c, 0x3c, 0xb4,
+ 0x02, 0xeb, 0x68, 0xb3, 0xf1, 0x01, 0x0e, 0xbe, 0x92, 0x3e, 0xb9, 0x6d,
+ 0x68, 0x3d, 0x6b, 0x68, 0x57, 0x43, 0x7c, 0x52, 0xc6, 0xed, 0x4a, 0xc2,
+ 0xb4, 0x5f, 0x54, 0x5a, 0x2a, 0xc7, 0xc6, 0x96, 0x4e, 0x54, 0xed, 0x04,
+ 0xe6, 0xc1, 0x55, 0xac, 0xcd, 0x4b, 0xeb, 0x20, 0x24, 0xc0, 0xf7, 0x99,
+ 0x49, 0xd3, 0xa6, 0xc6, 0x32, 0xc6, 0x60, 0x7c, 0xd3, 0x36, 0x2b, 0xfa,
+ 0x62, 0xac, 0xd4, 0xa2, 0xc2, 0x29, 0xef, 0x26, 0x59, 0x6c, 0x05, 0x92,
+ 0x6e, 0xc6, 0x0b, 0xe3, 0x12, 0x0f, 0x5c, 0xef, 0x19, 0x32, 0xde, 0xd1,
+ 0x5e, 0x0b, 0x90, 0xa4, 0x88, 0xc9, 0xa5, 0x94, 0x58, 0xd2, 0xe2, 0xf1,
+ 0xe5, 0x4c, 0xfe, 0x8c, 0x01, 0x7b, 0x5d, 0xcb, 0x2e, 0xc3, 0x26, 0xe5,
+ 0xf3, 0x38, 0xf0, 0xfa, 0x2c, 0x1b, 0xd9, 0x2c, 0x41, 0x2f, 0x8c, 0x5f,
+ 0xc1, 0x42, 0x4c, 0x8a, 0x1e, 0x33, 0xda, 0x02, 0xb9, 0xc7, 0xb7, 0x82,
+ 0xcc, 0x3d, 0x68, 0x63, 0xde, 0xf5, 0x64, 0x05, 0xe2, 0x2a, 0xc3, 0x92,
+ 0xc9, 0x7b, 0x09, 0x56, 0x20, 0x21, 0x88, 0xb6, 0xa1, 0x25, 0x6e, 0x9a,
+ 0x89, 0x06, 0x88, 0xe7, 0x2d, 0x64, 0xb6, 0x31, 0x8a, 0x3c, 0xa0, 0x47,
+ 0xeb, 0x48, 0x30, 0xba, 0xe5, 0xd7, 0xb9, 0xce, 0x43, 0xe0, 0x6c, 0xbe,
+ 0x77, 0x4e, 0x35, 0xb6, 0x01, 0xaf, 0xf4, 0x42, 0xb7, 0xa8, 0x85, 0x7d,
+ 0xb2, 0xa4, 0x05, 0x6a, 0x30, 0x3f, 0x30, 0x75, 0xa9, 0x4e, 0xab, 0x9d,
+ 0x15, 0x98, 0x98, 0xf8, 0x53, 0x81, 0xbd, 0x0b, 0x5d, 0x75, 0x79, 0x56,
+ 0xd5, 0xd4, 0xab, 0x72, 0x1d, 0x33, 0x3c, 0x7b, 0x9b, 0x2c, 0x47, 0xeb,
+ 0xe6, 0x3f, 0xdd, 0x84, 0x5a, 0x67, 0x6d, 0x35, 0x91, 0x33, 0xb3, 0x48,
+ 0x9f, 0x92, 0x9b, 0x28, 0xdb, 0x36, 0xd6, 0x68, 0x47, 0x8c, 0xce, 0xe7,
+ 0x2e, 0x1c, 0x45, 0x36, 0xca, 0xf0, 0x91, 0xce, 0x3b, 0x48, 0x14, 0xc9,
+ 0xd7, 0x89, 0x74, 0x3b, 0xdb, 0xca, 0x57, 0x2d, 0x8a, 0xc0, 0xe8, 0xc7,
+ 0xb4, 0xad, 0xce, 0xa2, 0x5d, 0xa7, 0xe7, 0x20, 0x67, 0x62, 0x7a, 0xfd,
+ 0x5e, 0xc4, 0x2e, 0xd1, 0xab, 0x8d, 0xb5, 0xa1, 0x22, 0x83, 0x1f, 0x20,
+ 0xed, 0xb0, 0xe9, 0xaa, 0x3d, 0x57, 0x0c, 0xe0, 0xfa, 0x8a, 0x2e, 0x25,
+ 0xe9, 0xab, 0x0f, 0xe6, 0x7d, 0xe5, 0x3a, 0x69, 0x34, 0xab, 0xd6, 0x84,
+ 0x57, 0xac, 0x9c, 0x24, 0xc3, 0x77, 0x18, 0x50, 0xd1, 0x02, 0x99, 0x43,
+ 0x8d, 0x0c, 0x01, 0xcd, 0x24, 0x57, 0x62, 0xa1, 0x73, 0x27, 0x3a, 0x08,
+ 0xbd, 0x61, 0x51, 0x03, 0xaf, 0xb4, 0xc3, 0x6e, 0x0a, 0x59, 0x2f, 0x98,
+ 0x64, 0x05, 0x68, 0x5a, 0x9f, 0xb4, 0xa5, 0x02, 0xaf, 0x8f, 0xa3, 0xa7,
+ 0xbb, 0x27, 0x6c, 0x04, 0xa5, 0x05, 0xcd, 0x1f, 0x15, 0x43, 0xec, 0x08,
+ 0x57, 0x96, 0x4b, 0xfa, 0x3a, 0x3c, 0x80, 0xce, 0xab, 0xf5, 0x4e, 0x6c,
+ 0xa8, 0x19, 0x8a, 0xef, 0x50, 0x2a, 0xdf, 0x54, 0x8f, 0xc8, 0x95, 0x4b,
+ 0xa1, 0x8d, 0x97, 0x97, 0xdd, 0x89, 0x31, 0x29, 0xed, 0x5f, 0xa0, 0xb7,
+ 0xfe, 0xae, 0xb7, 0xbb, 0x4e, 0xee, 0xee, 0xd3, 0xe9, 0xcb, 0x4f, 0x4c,
+ 0x63, 0x47, 0x19, 0xff, 0xdf, 0xc2, 0x38, 0x61, 0x80, 0xa5, 0x68, 0x8f,
+ 0xdd, 0xa3, 0x2f, 0x6d, 0x56, 0xc9, 0x30, 0x7a, 0x98, 0xc0, 0xc2, 0xe8,
+ 0xb1, 0xb2, 0x13, 0x89, 0x86, 0x56, 0x65, 0xe7, 0xb5, 0xdd, 0x15, 0x8a,
+ 0xa5, 0x8c, 0x96, 0x0e, 0x68, 0x65, 0x1d, 0xd6, 0xdf, 0x10, 0x44, 0x7f,
+ 0x2f, 0xf8, 0xa3, 0x24, 0x32, 0xdf, 0x46, 0xe2, 0xc8, 0xbf, 0x8d, 0xef,
+ 0x24, 0x55, 0x06, 0xbd, 0x3b, 0x92, 0x36, 0x3c, 0x8a, 0xad, 0x8f, 0xd0,
+ 0x39, 0x96, 0xac, 0xfd, 0x7c, 0xfa, 0xd8, 0xe4, 0x1a, 0xc9, 0xdd, 0x0a,
+ 0x9a, 0xfa, 0x63, 0x18, 0xa6, 0x03, 0xdc, 0xbf, 0xc4, 0xc1, 0x70, 0xd8,
+ 0x42, 0xc5, 0x11, 0xf1, 0x27, 0xbb, 0x47, 0x5a, 0x6a, 0xac, 0xf9, 0xf2,
+ 0x38, 0x34, 0x77, 0xec, 0x47, 0x76, 0x5c, 0x92, 0x15, 0x28, 0x92, 0xbf,
+ 0x6c, 0x15, 0x44, 0x01, 0x4e, 0x1f, 0x26, 0xf8, 0xc3, 0x3a, 0x73, 0x3d,
+ 0x8e, 0x66, 0xb7, 0xb8, 0xc3, 0xdb, 0x66, 0x00, 0x1a, 0xe9, 0x2c, 0xae,
+ 0xdd, 0x92, 0xc0, 0x94, 0x59, 0x3b, 0x9a, 0x01, 0x8d, 0x14, 0x48, 0x30,
+ 0x2f, 0x86, 0x03, 0xb2, 0xdf, 0x2e, 0xe1, 0x14, 0xed, 0xe0, 0x8d, 0x9f,
+ 0x03, 0x54, 0x4a, 0xf0, 0x5c, 0x44, 0xc4, 0x60, 0x75, 0x0b, 0x27, 0x1f,
+ 0xd1, 0xea, 0xd2, 0xae, 0x9c, 0x7d, 0xd1, 0x33, 0x81, 0x39, 0x2d, 0x47,
+ 0xc3, 0x20, 0x10, 0x13, 0xcf, 0x06, 0x4d, 0x3e, 0x79, 0x37, 0x2b, 0x28,
+ 0x93, 0x66, 0xdd, 0x73, 0xe6, 0x4a, 0x4c, 0xa4, 0x7f, 0xba, 0xa5, 0x76,
+ 0xf5, 0x07, 0x89, 0x79, 0xb9, 0x46, 0x5f, 0x1f, 0xbf, 0x47, 0x77, 0x09,
+ 0x78, 0xa3, 0xdf, 0xf9, 0xf9, 0x42, 0x6c, 0x9b, 0x3b, 0xa1, 0x59, 0x0e,
+ 0x22, 0x82, 0x87, 0xe4, 0x32, 0xd9, 0xf4, 0x02, 0xf1, 0xca, 0x5d, 0x09,
+ 0x9f, 0xe3, 0x94, 0xa8, 0x8a, 0xe8, 0x3f, 0xa0, 0x0b, 0x5a, 0x29, 0xa4,
+ 0xec, 0xd3, 0x06, 0x6a, 0x6c, 0x3f, 0x6b, 0x74, 0xdc, 0x3d, 0xbf, 0xa3,
+ 0x91, 0x00, 0x76, 0xaf, 0xfd, 0x28, 0xb6, 0x43, 0xc7, 0xc3, 0x58, 0xdf,
+ 0xaa, 0x31, 0x09, 0x7b, 0xb3, 0x88, 0x22, 0x40, 0x58, 0x44, 0xda, 0x18,
+ 0x77, 0x61, 0xcd, 0xe8, 0x84, 0x2b, 0xca, 0x18, 0xd8, 0x8a, 0xff, 0x65,
+ 0xbf, 0xdb, 0x60, 0xd9, 0x0b, 0xf0, 0xc7, 0xc2, 0x1d, 0x26, 0x6a, 0xa1,
+ 0xcc, 0x30, 0x0d, 0x89, 0x4f, 0x55, 0x1d, 0x48, 0x6f, 0xca, 0xc8, 0x4f,
+ 0xec, 0x7b, 0x74, 0x52, 0x6f, 0x14, 0xc5, 0xfb, 0xc9, 0x09, 0xec, 0xc8,
+ 0x54, 0x65, 0xf6, 0x77, 0x7c, 0x62, 0x86, 0xa4, 0xf0, 0x7c, 0x25, 0x77,
+ 0xdc, 0x51, 0xde, 0xf7, 0x67, 0xd4, 0x74, 0xf2, 0xf9, 0x01, 0x2e, 0x61,
+ 0xb2, 0x9a, 0x13, 0xe6, 0x8a, 0x93, 0x20, 0x39, 0x02, 0x14, 0x3e, 0xc6,
+ 0x9d, 0x27, 0x44, 0x1a, 0x7b, 0x60, 0x18, 0xd1, 0xb4, 0xa7, 0xc9, 0xb7,
+ 0xe4, 0x42, 0x73, 0x26, 0x1f, 0xb4, 0xb2, 0x7e, 0x18, 0x5e, 0x9a, 0x02,
+ 0x7f, 0x64, 0x95, 0xee, 0x92, 0x1b, 0x3f, 0xfd, 0xda, 0x21, 0x91, 0x7d,
+ 0x5d, 0x47, 0xe8, 0x2a, 0x52, 0x90, 0xf3, 0xd1, 0xb8, 0x4d, 0x56, 0x84,
+ 0x25, 0xd5, 0x6c, 0xca, 0x5a, 0xd1, 0xb5, 0x60, 0x9e, 0x91, 0xbf, 0x12,
+ 0x82, 0x65, 0xda, 0xef, 0x2e, 0x83, 0xaa, 0x20, 0x72, 0x08, 0x13, 0x3a,
+ 0x9f, 0x1f, 0x3e, 0xb2, 0x52, 0x48, 0x8b, 0xaf, 0xa3, 0x4a, 0xbc, 0x4b,
+ 0x5a, 0x9a, 0x1b, 0xf0, 0x13, 0xab, 0x9f, 0x03, 0x3d, 0xe5, 0x2b, 0x6f,
+ 0xc9, 0x82, 0x18, 0x69, 0x20, 0x24, 0xa2, 0x66, 0x10, 0x3e, 0xb8, 0x65,
+ 0x34, 0x2c, 0x27, 0xcf, 0xdc, 0x35, 0xd0, 0x6b, 0x65, 0xf4, 0x61, 0x5b,
+ 0x86, 0x6f, 0x1b, 0xef, 0x20, 0x1a, 0x80, 0xb7, 0x94, 0x36, 0xeb, 0x89,
+ 0x01, 0x92, 0xd8, 0xf8, 0x4e, 0xb0, 0xa5, 0xa8, 0x4c, 0x38, 0x20, 0xc7,
+ 0xed, 0x79, 0x5a, 0xe5, 0x6a, 0x31, 0xc8, 0x6d, 0xb6, 0x17, 0xeb, 0xe3,
+ 0x62, 0x07, 0xc0, 0x75, 0x58, 0xc2, 0xa0, 0xbb, 0x6b, 0x7f, 0xec, 0x9c,
+ 0x26, 0x53, 0xd0, 0x79, 0xc8, 0x5c, 0x03, 0xea, 0xd6, 0x09, 0x46, 0x33,
+ 0xbb, 0x03, 0x7a, 0xb6, 0xc2, 0xe8, 0xa4, 0xab, 0xf4, 0xed, 0x7d, 0xae,
+ 0xfa, 0xcc, 0x96, 0x5a, 0x2d, 0x07, 0x38, 0x61, 0x1c, 0x82, 0xe9, 0xbb,
+ 0x14, 0x1c, 0x52, 0x35, 0xb5, 0x62, 0x2c, 0xfc, 0x54, 0x5e, 0xe1, 0xd3,
+ 0x37, 0xec, 0xbe, 0xdc, 0x4a, 0xe2, 0xd2, 0xb9, 0x45, 0x8e, 0x75, 0xdd,
+ 0xbd, 0xff, 0x7e, 0x2c, 0x88, 0x1c, 0x48, 0x82, 0x35, 0x27, 0xce, 0xa5,
+ 0x18, 0x5a, 0xa9, 0xbd, 0x9c, 0x11, 0x13, 0xcf, 0x88, 0x38, 0x0e, 0x6b,
+ 0x40, 0x0f, 0x72, 0x16, 0x2b, 0x69, 0xfd, 0x27, 0x73, 0xbf, 0x14, 0x5b,
+ 0x20, 0xfd, 0x0b, 0x9c, 0x30, 0x48, 0x87, 0xeb, 0x56, 0xbd, 0x8f, 0x22,
+ 0x47, 0x0c, 0xca, 0xe8, 0xf4, 0x31, 0x12, 0x5b, 0x56, 0xc6, 0x0b, 0x59,
+ 0x1f, 0x88, 0x0c, 0xa0, 0xc6, 0x39, 0xd2, 0xae, 0xad, 0x3c, 0x20, 0x71,
+ 0xd4, 0xc0, 0xdd, 0x8b, 0x4f, 0x7e, 0xde, 0x0d, 0xf4, 0x79, 0x55, 0xbb,
+ 0x17, 0xe6, 0xfb, 0xb9, 0xc6, 0x5f, 0x68, 0x72, 0xd5, 0x1a, 0xfc, 0x0b,
+ 0xbc, 0x07, 0x27, 0x5b, 0x2c, 0x22, 0x50, 0x39, 0x22, 0xc1, 0x6f, 0xe4,
+ 0xfb, 0xec, 0x77, 0xc3, 0x3c, 0xd1, 0x61, 0x58, 0xf3, 0x02, 0xd2, 0x18,
+ 0xd4, 0xd1, 0xad, 0x0e, 0x5f, 0x85, 0x43, 0xa9, 0x1c, 0xee, 0xc6, 0x59,
+ 0xe8, 0x83, 0x8b, 0x56, 0xc7, 0x1f, 0xcf, 0xd1, 0x19, 0x03, 0x57, 0xc7,
+ 0x04, 0x22, 0x52, 0x41, 0xb2, 0x40, 0xc9, 0x62, 0xfe, 0xa8, 0x17, 0xf2,
+ 0x08, 0x10, 0xfb, 0x28, 0xb6, 0xf5, 0x35, 0x30, 0xd3, 0x2c, 0xe8, 0x8b,
+ 0xb6, 0xea, 0x5a, 0xf1, 0x70, 0x9b, 0x8e, 0x68, 0x8b, 0xec, 0xff, 0x29,
+ 0xf2, 0xfe, 0x20, 0xce, 0xf1, 0x46, 0xce, 0x1c, 0x68, 0xab, 0xc3, 0xaf,
+ 0x83, 0xb2, 0x19, 0xb5, 0x53, 0xb9, 0x90, 0xf2, 0xac, 0x95, 0xb6, 0x53,
+ 0x18, 0x40, 0x2f, 0x4f, 0xa6, 0xe3, 0xed, 0x8e, 0xac, 0x29, 0xa8, 0xbd,
+ 0x40, 0x57, 0xa7, 0x7e, 0x02, 0xcd, 0x76, 0x8c, 0xe0, 0x28, 0xd6, 0x49,
+ 0x76, 0x60, 0xf4, 0xe1, 0xaa, 0x95, 0x7f, 0x29, 0xae, 0x80, 0x97, 0xed,
+ 0xf6, 0xaf, 0x41, 0xc3, 0x77, 0x7b, 0xfa, 0xa8, 0x26, 0x63, 0xd6, 0x00,
+ 0x36, 0xec, 0x45, 0x2a, 0x6f, 0xc7, 0x46, 0x41, 0x06, 0x5c, 0xfe, 0xc6,
+ 0x8b, 0x7e, 0xe7, 0x1d, 0x4b, 0x8e, 0xde, 0x8c, 0x66, 0x41, 0xc7, 0x58,
+ 0xd5, 0xff, 0x32, 0xdf, 0x06, 0x45, 0xfc, 0x16, 0x9e, 0x08, 0xf6, 0x36,
+ 0x40, 0x53, 0x04, 0xe5, 0x36, 0x32, 0xdc, 0xf2, 0xbe, 0x7d, 0xc2, 0x58,
+ 0xab, 0x08, 0x5c, 0xf6, 0x5f, 0x97, 0x4c, 0x77, 0x76, 0x8c, 0x1f, 0xd2,
+ 0xe8, 0xcf, 0xec, 0xea, 0x38, 0x0f, 0xbd, 0x29, 0xd2, 0x86, 0xcc, 0x29,
+ 0xe1, 0x18, 0x02, 0xa8, 0xf6, 0x1f, 0x34, 0x60, 0x8a, 0xe5, 0xa6, 0x22,
+ 0xa8, 0x39, 0x86, 0xe6, 0xb7, 0x63, 0xc9, 0x05, 0xff, 0x3e, 0xad, 0xd1,
+ 0x5d, 0xc2, 0x3b, 0xce, 0x51, 0xc5, 0x55, 0x19, 0xdf, 0x33, 0x13, 0x53,
+ 0xa1, 0x80, 0x0d, 0xb0, 0xc5, 0xd3, 0x0f, 0x6b, 0x56, 0x1b, 0x84, 0x73,
+ 0x49, 0x29, 0x82, 0x4e, 0x99, 0x32, 0xbc, 0x28, 0x42, 0xf3, 0xae, 0x74,
+ 0x15, 0x11, 0x3b, 0x24, 0x6e, 0x4f, 0x43, 0xe3, 0x75, 0x22, 0x45, 0xb3,
+ 0xed, 0x02, 0xb1, 0x05, 0x0a, 0x3d, 0xe4, 0xc3, 0xeb, 0x1d, 0x62, 0x25,
+ 0x5b, 0x3b, 0xa7, 0x64, 0x2e, 0xc5, 0x75, 0x57, 0x46, 0x8f, 0x90, 0xb4,
+ 0xd0, 0xe7, 0xc3, 0xe1, 0x8a, 0xd2, 0xa2, 0xc8, 0xc1, 0x3e, 0x08, 0x7a,
+ 0x55, 0xaf, 0xb0, 0x03, 0xae, 0xb2, 0x12, 0x11, 0xf8, 0x06, 0xe9, 0xf6,
+ 0x98, 0x7e, 0x7c, 0xde, 0x01, 0x67, 0x81, 0x85, 0x8e, 0xd8, 0xb8, 0x8a,
+ 0xf2, 0x07, 0xb8, 0xc6, 0xca, 0x65, 0xd4, 0xae, 0x58, 0x62, 0x97, 0x28,
+ 0x6e, 0x94, 0xd0, 0x11, 0xd1, 0xc2, 0x6f, 0xd2, 0x74, 0xa7, 0x90, 0x8d,
+ 0x25, 0x7e, 0x5a, 0xe6, 0x1a, 0xe7, 0xb2, 0x16, 0x20, 0x0c, 0x6b, 0x74,
+ 0x48, 0x08, 0x76, 0x14, 0x18, 0x01, 0xee, 0xff, 0x54, 0x9c, 0x7a, 0x25,
+ 0x08, 0xf1, 0x54, 0x57, 0x00, 0xd7, 0xe7, 0x14, 0x1d, 0xae, 0xec, 0x20,
+ 0x5d, 0x99, 0xf3, 0xc1, 0x2c, 0x57, 0xe8, 0xbd, 0x9a, 0x7a, 0x33, 0x0d,
+ 0xe8, 0x0b, 0xd0, 0x0e, 0x69, 0xa9, 0x20, 0xe8, 0xcc, 0xd9, 0xdf, 0x8b,
+ 0x32, 0x14, 0x69, 0x9d, 0x98, 0xf2, 0xf8, 0x63, 0xfe, 0x68, 0x5f, 0x94,
+ 0x02, 0xd9, 0x23, 0xd6, 0xa4, 0x45, 0x3f, 0xcc, 0x1f, 0x09, 0xc9, 0x07,
+ 0xd9, 0x11, 0xd4, 0x70, 0x75, 0x4d, 0xfb, 0xca, 0x1d, 0xd2, 0xfc, 0x1c,
+ 0x0e, 0x91, 0x20, 0x38, 0xef, 0xa9, 0x30, 0x2e, 0xe4, 0x89, 0x53, 0x9c,
+ 0x22, 0xf9, 0x4e, 0xe6, 0x9b, 0x69, 0x27, 0x53, 0x1e, 0x8c, 0x68, 0xa7,
+ 0x3b, 0x75, 0x37, 0x0f, 0x5f, 0xd4, 0x6b, 0x36, 0x1c, 0xdd, 0xfb, 0xb5,
+ 0xac, 0x16, 0x5c, 0xa4, 0xf4, 0x2a, 0x43, 0x44, 0xaa, 0x28, 0xa2, 0xc7,
+ 0x2f, 0xdd, 0x45, 0x90, 0xe8, 0xb8, 0xd8, 0x10, 0x69, 0xe0, 0xd0, 0x4c,
+ 0x7c, 0x4f, 0xd7, 0xc8, 0x35, 0xfa, 0xf8, 0x71, 0x4a, 0xb7, 0x36, 0x6e,
+ 0xfd, 0x44, 0x9d, 0x40, 0x1f, 0xb9, 0xb1, 0x64, 0x0a, 0x82, 0x50, 0x38,
+ 0xf3, 0x78, 0x61, 0xd3, 0xdb, 0x18, 0xa8, 0xa8, 0x66, 0x42, 0x81, 0xf4,
+ 0x13, 0x68, 0xec, 0xff, 0x4a, 0x58, 0xb1, 0x44, 0x06, 0x9f, 0xca, 0x65,
+ 0x66, 0x87, 0xc3, 0xbf, 0x7e, 0xa6, 0xea, 0x6f, 0x93, 0xd2, 0x0e, 0x9a,
+ 0x26, 0x5c, 0xa1, 0x7c, 0x7c, 0x00, 0x34, 0xe8, 0xb2, 0x45, 0x1f, 0xff,
+ 0x8c, 0xb4, 0xaa, 0x9e, 0x21, 0x76, 0x06, 0x7e, 0xa6, 0x2f, 0x85, 0xd8,
+ 0x94, 0xaa, 0xd5, 0x2f, 0xc2, 0xe4, 0x22, 0x61, 0x8a, 0xaf, 0xa4, 0x7a,
+ 0x8e, 0x5f, 0x50, 0x28, 0xb4, 0xc5, 0x55, 0x57, 0xb4, 0x2a, 0xb6, 0x5f,
+ 0x4c, 0xce, 0x1b, 0xa2, 0x02, 0x78, 0x5e, 0x19, 0xb6, 0x65, 0x50, 0x5c,
+ 0x48, 0x5e, 0x6f, 0xe2, 0x07, 0x61, 0x75, 0x26, 0xf4, 0xa2, 0x94, 0xbd,
+ 0x5c, 0xc4, 0x91, 0x3d, 0x6a, 0x16, 0x61, 0x44, 0xd8, 0xb1, 0xaf, 0xc6,
+ 0xcc, 0x4b, 0x4d, 0x8a, 0x0b, 0xec, 0x2d, 0xec, 0x7c, 0x0b, 0x45, 0xc4,
+ 0xdb, 0x29, 0xc3, 0x03, 0x5a, 0xad, 0x53, 0xe3, 0x8b, 0xf4, 0x86, 0x12,
+ 0xd0, 0xc3, 0x34, 0x0d, 0xac, 0x03, 0xdf, 0x9b, 0x0d, 0x56, 0x70, 0xd0,
+ 0x4b, 0x79, 0x2f, 0x25, 0x12, 0xbc, 0x70, 0x13, 0xfa, 0xcb, 0xed, 0x4e,
+ 0x4b, 0xcf, 0x2b, 0x46, 0xe4, 0xfe, 0xf2, 0x6c, 0x47, 0x79, 0x66, 0x73,
+ 0xe1, 0x35, 0x10, 0xc8, 0x9c, 0x8f, 0xf9, 0xcb, 0x0d, 0x80, 0xf6, 0x8d,
+ 0xd8, 0xd7, 0xa9, 0xb0, 0x54, 0x24, 0xc3, 0x9d, 0x91, 0xac, 0x16, 0xd2,
+ 0xff, 0x98, 0x7e, 0x86, 0xd4, 0x8b, 0x68, 0xc0, 0xc5, 0xa2, 0x3a, 0x8f,
+ 0x48, 0x8b, 0xc8, 0x62, 0xae, 0x71, 0x58, 0x6e, 0xba, 0x23, 0x80, 0xec,
+ 0xf6, 0x0d, 0x47, 0x79, 0x32, 0x5a, 0x95, 0x33, 0x3d, 0xfc, 0x37, 0x56,
+ 0xe9, 0x2f, 0x5c, 0xbd, 0xc3, 0x3c, 0x9c, 0x63, 0xe2, 0x94, 0x10, 0xf8,
+ 0x50, 0x52, 0x22, 0xbe, 0xad, 0x0a, 0x16, 0xc8, 0x91, 0x6a, 0x08, 0x45,
+ 0x67, 0x7c, 0xe8, 0x67, 0x30, 0x9f, 0x5c, 0xe0, 0x9a, 0x1d, 0x79, 0x09,
+ 0x11, 0x80, 0xf0, 0x04, 0xf6, 0xf7, 0x27, 0xed, 0x6a, 0x20, 0x25, 0xe7,
+ 0x8b, 0xb8, 0x17, 0x3e, 0x70, 0xf0, 0xd9, 0x56, 0x0e, 0x23, 0xd2, 0xa2,
+ 0x95, 0x2f, 0x40, 0xcb, 0x36, 0x1c, 0x57, 0xba, 0x79, 0x77, 0xe0, 0xc1,
+ 0x4b, 0x31, 0xe7, 0x5d, 0x2a, 0xa6, 0x23, 0x2f, 0x83, 0x0e, 0xca, 0x5f,
+ 0x45, 0x7e, 0x2f, 0xbb, 0xb1, 0xa3, 0x9a, 0xb4, 0x9e, 0xa9, 0x4c, 0xf8,
+ 0x8f, 0xaf, 0x86, 0x76, 0x20, 0x79, 0xa5, 0x3e, 0x85, 0xe9, 0xd3, 0x2a,
+ 0xab, 0x89, 0xfd, 0x64, 0x36, 0x21, 0xd0, 0xf4, 0xc4, 0x91, 0xea, 0x3c,
+ 0xe1, 0x20, 0x1b, 0x82, 0xde, 0x4d, 0x92, 0xaf, 0x1a, 0xfe, 0xb5, 0x2b,
+ 0x09, 0x51, 0xd7, 0x57, 0x9e, 0xa6, 0xe8, 0xbf, 0xee, 0xef, 0xb7, 0xc2,
+ 0x15, 0xe7, 0x6a, 0x52, 0x22, 0x32, 0xce, 0x77, 0xb3, 0x4d, 0xc2, 0xe4,
+ 0x90, 0xaf, 0x68, 0x14, 0x41, 0xcd, 0x37, 0x3d, 0xd7, 0x4a, 0x6d, 0x3e,
+ 0x31, 0xa4, 0x28, 0x7b, 0x59, 0xd2, 0xa7, 0x56, 0x7d, 0x51, 0x74, 0x2f,
+ 0x48, 0x77, 0x71, 0xe8, 0xa1, 0xaf, 0x6a, 0xa0, 0x4f, 0xf3, 0xb6, 0xbf,
+ 0xb6, 0x10, 0x14, 0x94, 0xd4, 0x61, 0x4a, 0xfc, 0xff, 0xd7, 0x6d, 0xd4,
+ 0x01, 0x6a, 0x41, 0xf6, 0xd6, 0x0b, 0xe9, 0x5b, 0xb7, 0x0a, 0x0b, 0xa6,
+ 0x6d, 0x51, 0x64, 0x01, 0x91, 0xe6, 0xf1, 0x69, 0x7b, 0x7d, 0xee, 0xb0,
+ 0xb9, 0x6f, 0x92, 0x41, 0xb4, 0xfa, 0x7c, 0xf1, 0xcf, 0xb6, 0xc1, 0x3e,
+ 0xcb, 0xe2, 0x3a, 0xc1, 0x74, 0x61, 0xc2, 0xf1, 0xbd, 0xd2, 0xff, 0x2d,
+ 0x4c, 0x56, 0xee, 0x26, 0xe3, 0xf3, 0xbd, 0xb9, 0xc7, 0xf8, 0x42, 0xab,
+ 0xcd, 0x37, 0x97, 0xd2, 0x87, 0x81, 0xbb, 0xd5, 0xd0, 0xc6, 0xe7, 0x85,
+ 0xfb, 0x1a, 0xd7, 0xac, 0x04, 0xcf, 0xa8, 0xb9, 0xc5, 0x66, 0xa3, 0x55,
+ 0x05, 0x9c, 0xfb, 0x6c, 0x02, 0x67, 0x21, 0x1f, 0x1e, 0xd7, 0x33, 0x3c,
+ 0x17, 0xba, 0x97, 0x09, 0x0c, 0xb3, 0xb9, 0x7c, 0x24, 0x8c, 0xdf, 0x99,
+ 0x47, 0xdc, 0xcc, 0x31, 0xdf, 0xf6, 0x5a, 0x31, 0xa7, 0x94, 0x8f, 0xf2,
+ 0xfb, 0x1d, 0x5b, 0x05, 0x61, 0x8f, 0xf9, 0x2d, 0xb2, 0xc5, 0xc7, 0x2d,
+ 0x09, 0x3d, 0x8a, 0x07, 0x5e, 0xfa, 0x9f, 0xd2, 0xfe, 0x81, 0x7b, 0x35,
+ 0x42, 0xa7, 0x7b, 0x33, 0x7c, 0xcc, 0x7b, 0x63, 0x7c, 0x83, 0x05, 0x1c,
+ 0xbe, 0x3a, 0x54, 0xde, 0x35, 0xf0, 0xd0, 0xdf, 0x89, 0x30, 0x05, 0x1d,
+ 0x84, 0x3b, 0xbd, 0x0d, 0x4e, 0xc7, 0x2c, 0x6f, 0x4b, 0xb4, 0x88, 0xc7,
+ 0x93, 0xab, 0xad, 0x64, 0xcb, 0x7c, 0x7b, 0xf4, 0x8d, 0x4b, 0xfc, 0x85,
+ 0x25, 0xff, 0x84, 0x7b, 0xf1, 0xb5, 0x56, 0xa8, 0x7d, 0x16, 0xf8, 0x8d,
+ 0x47, 0x40, 0x61, 0x1c, 0x1b, 0xde, 0xdb, 0xbf, 0xc7, 0xfd, 0x3d, 0x3b,
+ 0x8c, 0xf8, 0x73, 0x30, 0x6c, 0x0c, 0x05, 0x26, 0x67, 0xa1, 0x83, 0xaa,
+ 0x04, 0x7a, 0x36, 0xc0, 0x0b, 0x8d, 0x8c, 0x50, 0x94, 0xb0, 0x85, 0x89,
+ 0xae, 0xf0, 0x94, 0x0d, 0xd1, 0x51, 0xd8, 0x33, 0xc6, 0x91, 0x8d, 0xc3,
+ 0x11, 0xd8, 0xb7, 0xcb, 0xb3, 0x61, 0x33, 0x6b, 0x64, 0xf8, 0x74, 0x01,
+ 0xfa, 0x92, 0x89, 0xa0, 0xbd, 0xb5, 0x2b, 0x8c, 0x42, 0x3a, 0x8f, 0xae,
+ 0xb3, 0xd4, 0xb5, 0xcc, 0x4e, 0x77, 0x3e, 0xc5, 0x31, 0x2a, 0xd7, 0xb3,
+ 0x9e, 0x66, 0xec, 0x09, 0x8e, 0x75, 0x14, 0xbf, 0x0d, 0x5b, 0x5b, 0xb6,
+ 0x47, 0x18, 0xed, 0xf3, 0x72, 0x61, 0xf5, 0xac, 0x54, 0xf2, 0x35, 0xa8,
+ 0x39, 0xf7, 0x86, 0x1b, 0x7c, 0xf9, 0x7a, 0x75, 0xab, 0xa9, 0x01, 0xec,
+ 0x88, 0x6b, 0x15, 0x2e, 0x6f, 0xea, 0x8e, 0xfd, 0x9e, 0x4e, 0x18, 0xc4,
+ 0xb0, 0xcf, 0x8f, 0x3d, 0x31, 0x08, 0x64, 0x9a, 0xba, 0xd3, 0x2d, 0xf1,
+ 0x60, 0x6b, 0x61, 0x65, 0x0a, 0x8a, 0x25, 0xd9, 0x53, 0xd5, 0xc3, 0x15,
+ 0xeb, 0xcc, 0x69, 0x23, 0x26, 0x12, 0x92, 0x58, 0x0c, 0x7d, 0x19, 0x3e,
+ 0xf5, 0xc9, 0x68, 0x7d, 0x44, 0x8a, 0xf3, 0x1e, 0x14, 0x89, 0x2d, 0x18,
+ 0x82, 0x1a, 0x54, 0x55, 0xf5, 0x8a, 0x64, 0xbf, 0x63, 0x46, 0x0c, 0xdf,
+ 0xca, 0x05, 0x92, 0xbc, 0x94, 0x4b, 0xd1, 0x68, 0x85, 0x1f, 0xab, 0x07,
+ 0xaa, 0xde, 0x2d, 0x5c, 0x80, 0x71, 0x73, 0x41, 0x78, 0x0c, 0xb7, 0xc9,
+ 0x7a, 0xda, 0x4c, 0x5c, 0xc1, 0x2c, 0x6a, 0x7e, 0xad, 0x5f, 0x02, 0xeb,
+ 0xc8, 0x68, 0x00, 0x4c, 0x6b, 0xa4, 0x65, 0x6c, 0xd4, 0xed, 0x85, 0x48,
+ 0x82, 0x29, 0xe3, 0xa4, 0x77, 0xf3, 0x32, 0xf0, 0x43, 0x5f, 0x12, 0x6a,
+ 0xd2, 0x0d, 0x40, 0x8a, 0x76, 0x5e, 0x7a, 0x91, 0x2a, 0x2a, 0xfc, 0x18,
+ 0x15, 0x4d, 0x84, 0x3f, 0xa4, 0x72, 0xbb, 0x27, 0xd4, 0xea, 0x49, 0xb0,
+ 0x47, 0xc8, 0xcf, 0x08, 0xca, 0xe0, 0xdd, 0x39, 0xc6, 0x87, 0x26, 0xde,
+ 0xae, 0xa4, 0xab, 0xb6, 0x29, 0x40, 0xfa, 0x00, 0xfa, 0xa9, 0xd5, 0xad,
+ 0x05, 0x8c, 0x32, 0xd4, 0xdb, 0xd6, 0xd6, 0x5d, 0x9b, 0xfb, 0xd0, 0xa1,
+ 0x76, 0x0b, 0xde, 0x79, 0x59, 0x8a, 0x69, 0xdf, 0xcc, 0x77, 0xb3, 0xaa,
+ 0x4a, 0xf5, 0xc0, 0x80, 0x9d, 0x61, 0x9d, 0xc4, 0xcb, 0x82, 0x3a, 0xc5,
+ 0x2d, 0x25, 0x40, 0xc3, 0x35, 0xd0, 0x7a, 0xe1, 0x83, 0x5b, 0x01, 0x61,
+ 0x9b, 0xa1, 0x56, 0x1a, 0x07, 0x8b, 0xd1, 0x9d, 0x38, 0xfa, 0x4e, 0xa9,
+ 0xa3, 0x84, 0x78, 0xa2, 0x36, 0x09, 0x9d, 0xea, 0xb3, 0xbe, 0x70, 0x31,
+ 0xfc, 0xcb, 0x28, 0x12, 0x34, 0x64, 0xca, 0x95, 0x78, 0xcc, 0x71, 0xbc,
+ 0x10, 0xd0, 0x69, 0x3e, 0xee, 0x66, 0xff, 0x27, 0x49, 0x00, 0xb1, 0xd6,
+ 0x1e, 0x48, 0x33, 0x87, 0xab, 0x3a, 0xbe, 0xf3, 0x55, 0xf7, 0xec, 0xee,
+ 0xd7, 0x3f, 0x5b, 0x6a, 0xaa, 0xbe, 0xc9, 0xae, 0x9c, 0xee, 0xba, 0x1b,
+ 0x3b, 0xca, 0x0b, 0xb0, 0x95, 0x3e, 0x0a, 0x73, 0xbb, 0x1c, 0x5a, 0xb3,
+ 0x5f, 0x88, 0xcf, 0x6f, 0x97, 0xe4, 0x9a, 0x71, 0x45, 0xb0, 0x5e, 0xca,
+ 0x67, 0x21, 0x06, 0xfb, 0xae, 0xfe, 0xff, 0x61, 0x9c, 0xdf, 0xd4, 0x0b,
+ 0x5c, 0x5d, 0x00, 0x4f, 0xdf, 0xea, 0xe6, 0x23, 0xdf, 0xfc, 0x9c, 0xc6,
+ 0x45, 0xcf, 0xfd, 0x6c, 0xf7, 0x40, 0x96, 0xc7, 0xca, 0x91, 0xa5, 0xb1,
+ 0x1f, 0x16, 0x85, 0x42, 0x61, 0x14, 0xa3, 0x41, 0x9d, 0x6c, 0x6e, 0x13,
+ 0xb3, 0xf5, 0x73, 0x8d, 0x5b, 0xd6, 0x54, 0xc2, 0xa9, 0xab, 0x57, 0x80,
+ 0x33, 0x23, 0x86, 0x8e, 0xb3, 0x9e, 0xc8, 0xc3, 0xc9, 0x94, 0x36, 0xf5,
+ 0x40, 0x9d, 0x7d, 0x14, 0xb9, 0x7e, 0x77, 0x0a, 0x85, 0x3b, 0x5d, 0x12,
+ 0x67, 0xa8, 0x25, 0x51, 0x5f, 0xc8, 0x5c, 0x54, 0xff, 0x73, 0xc5, 0xaf,
+ 0x3b, 0xfd, 0xc8, 0x39, 0x4a, 0x9c, 0xa7, 0x5d, 0x8f, 0x8f, 0x32, 0x1a,
+ 0x4d, 0xce, 0x92, 0xab, 0xec, 0x36, 0x38, 0xe5, 0x73, 0xb2, 0x48, 0x7e,
+ 0x2b, 0x6c, 0x0c, 0x02, 0x37, 0x8c, 0xbe, 0x7d, 0xe0, 0x40, 0xfa, 0xfb,
+ 0xb9, 0x0f, 0xab, 0xa1, 0x05, 0x74, 0x76, 0x48, 0x6d, 0x57, 0x3d, 0xd0,
+ 0xed, 0x38, 0x64, 0x59, 0x0f, 0xcc, 0x52, 0x45, 0xea, 0x11, 0x80, 0x12,
+ 0x04, 0xac, 0x43, 0x31, 0xb3, 0x2d, 0xd4, 0xa4, 0xb4, 0x5c, 0x88, 0xcf,
+ 0xa5, 0xaa, 0xf8, 0x31, 0x46, 0x61, 0x9d, 0xb3, 0x3a, 0x78, 0xd7, 0xb4,
+ 0x9a, 0x38, 0x46, 0x08, 0xa3, 0xc6, 0x68, 0xe7, 0x78, 0xee, 0x1c, 0x93,
+ 0xdf, 0x94, 0x90, 0xed, 0xd3, 0x24, 0x30, 0x0e, 0x2a, 0x23, 0xd3, 0x76,
+ 0x27, 0x3a, 0x33, 0x12, 0x45, 0x05, 0x86, 0xa6, 0x06, 0x45, 0x3f, 0x9b,
+ 0x6d, 0x2f, 0x8f, 0x51, 0xeb, 0x2c, 0x96, 0xf3, 0x7d, 0x29, 0x55, 0xd6,
+ 0x9d, 0x4b, 0x89, 0x10, 0xa2, 0x76, 0xe1, 0x52, 0xa5, 0x6c, 0x88, 0xf2,
+ 0x0c, 0x06, 0xa0, 0xd0, 0x9e, 0x2e, 0x6e, 0x59, 0xc3, 0x99, 0x36, 0x1e,
+ 0xc6, 0xef, 0xe6, 0x11, 0x66, 0x59, 0xf3, 0xe5, 0x0d, 0x03, 0x48, 0x51,
+ 0x0a, 0xe6, 0x8a, 0xf6, 0xeb, 0x69, 0x7c, 0x47, 0x4c, 0xa1, 0x56, 0x83,
+ 0xcc, 0x51, 0x47, 0x86, 0x74, 0x87, 0x7a, 0xc9, 0x87, 0xf3, 0x0a, 0x34,
+ 0x6c, 0xf6, 0x96, 0x69, 0xd7, 0x61, 0x85, 0x2d, 0xa6, 0x97, 0x31, 0x4c,
+ 0xd4, 0x72, 0xb5, 0x67, 0x2e, 0xff, 0x02, 0x33, 0x59, 0x7b, 0x0c, 0xa5,
+ 0x07, 0xf2, 0x01, 0xf7, 0x23, 0x53, 0x24, 0x20, 0x17, 0xad, 0x7b, 0x0e,
+ 0xb9, 0x4e, 0xb3, 0xd9, 0x01, 0x8a, 0x73, 0xc6, 0x3a, 0x06, 0xb6, 0x5c,
+ 0xe3, 0x57, 0x21, 0x10, 0x8e, 0x38, 0x5f, 0xa8, 0x89, 0xc1, 0xfe, 0xf5,
+ 0xf3, 0xe2, 0x75, 0x61, 0x4c, 0x1a, 0x98, 0x12, 0xc5, 0x39, 0x57, 0x78,
+ 0x90, 0x5f, 0x9f, 0x55, 0xc4, 0xcc, 0x9b, 0x7f, 0x6a, 0x86, 0xc4, 0x2b,
+ 0xf5, 0x16, 0xd2, 0xc7, 0x31, 0xa1, 0x7f, 0x63, 0x1c, 0xbb, 0xb8, 0x0d,
+ 0x50, 0xb1, 0x5a, 0xd5, 0xe1, 0xc9, 0xdb, 0x8e, 0xff, 0x31, 0x53, 0xc3,
+ 0xbd, 0xea, 0x5b, 0xed, 0x82, 0x4c, 0xbb, 0x7b, 0x5d, 0x1a, 0x72, 0xf0,
+ 0x2b, 0x3e, 0xf9, 0xce, 0x88, 0x32, 0xb5, 0xb0, 0x30, 0x37, 0x26, 0xd5,
+ 0xca, 0x9a, 0xde, 0x5b, 0xc7, 0x04, 0x4b, 0x6a, 0x68, 0xdc, 0x09, 0x6c,
+ 0x3b, 0x7f, 0x36, 0x6b, 0x53, 0x27, 0xd2, 0x7a, 0x03, 0x0a, 0x26, 0x06,
+ 0x5a, 0x27, 0xc4, 0x5c, 0xf8, 0xbb, 0xa2, 0x0f, 0xbf, 0xd9, 0xa7, 0x50,
+ 0xf0, 0x1b, 0xb2, 0x90, 0xb8, 0x3b, 0x0e, 0x64, 0x73, 0x98, 0x77, 0xd2,
+ 0x32, 0x76, 0x21, 0x97, 0x7b, 0x4c, 0x3d, 0x3c, 0xad, 0xbe, 0x94, 0xde,
+ 0x23, 0x44, 0xcd, 0x03, 0x83, 0x36, 0x00, 0xd1, 0x97, 0x8f, 0x37, 0x6b,
+ 0x95, 0xb8, 0x92, 0x57, 0xb4, 0xfb, 0xeb, 0x67, 0x40, 0x33, 0x64, 0xba,
+ 0x7d, 0x48, 0xfe, 0x9f, 0xe7, 0xab, 0x2e, 0x18, 0x91, 0x5b, 0x38, 0x18,
+ 0xa3, 0x77, 0x57, 0x0b, 0x46, 0xf0, 0x00, 0xde, 0x7e, 0x89, 0xac, 0x53,
+ 0x10, 0xa6, 0x3a, 0xdd, 0x73, 0x42, 0x97, 0x32, 0x9d, 0x7d, 0x9c, 0x19,
+ 0x80, 0xd7, 0x72, 0x01, 0x46, 0x11, 0xc8, 0x60, 0xcb, 0xaf, 0xd1, 0x88,
+ 0xfc, 0x19, 0x3f, 0x53, 0xb7, 0xab, 0x61, 0x55, 0x01, 0x1c, 0x5f, 0x49,
+ 0xa5, 0x8c, 0x59, 0x7a, 0x5c, 0x7a, 0x2f, 0x65, 0xc9, 0x0a, 0xab, 0xae,
+ 0x2e, 0x4a, 0xdd, 0x44, 0x5f, 0x48, 0x9c, 0xb5, 0x17, 0x42, 0xd7, 0x8b,
+ 0xfc, 0xdf, 0x39, 0xf1, 0xa0, 0x6b, 0x1b, 0x92, 0x11, 0xa0, 0x77, 0xa8,
+ 0x95, 0xab, 0xf3, 0xe3, 0x9e, 0xf6, 0x12, 0x93, 0x29, 0xa2, 0x6c, 0xba,
+ 0xe2, 0x60, 0xd5, 0xcb, 0x01, 0x46, 0x55, 0x0c, 0x3f, 0x08, 0xb0, 0x74,
+ 0x84, 0xbb, 0xc2, 0x18, 0x8a, 0x1c, 0x6d, 0x36, 0x6e, 0xea, 0xa8, 0x6a,
+ 0x78, 0xe8, 0x58, 0x84, 0xd8, 0x33, 0xac, 0xe1, 0x74, 0xf6, 0xb2, 0x80,
+ 0x68, 0x62, 0x74, 0x27, 0xf5, 0x2e, 0x00, 0xdc, 0x3c, 0xee, 0x75, 0x72,
+ 0x27, 0xa4, 0x85, 0xe4, 0xba, 0x97, 0xc0, 0x57, 0xaf, 0xfe, 0xdb, 0xc4,
+ 0x2c, 0xaf, 0xb1, 0x22, 0xee, 0x82, 0x21, 0x93, 0xdd, 0x87, 0xe4, 0xdc,
+ 0xfd, 0x0c, 0x6a, 0x6c, 0xb4, 0x3a, 0x6e, 0xf9, 0x54, 0x00, 0x4f, 0x98,
+ 0x2a, 0xde, 0xea, 0x5c, 0xa4, 0xde, 0x6e, 0x6a, 0x5c, 0x49, 0xfd, 0x9f,
+ 0xcf, 0xee, 0xe0, 0x06, 0xfd, 0xfc, 0xee, 0xb3, 0xf6, 0x02, 0xd8, 0xb1,
+ 0x7f, 0x5b, 0x6c, 0x2e, 0xc2, 0xd1, 0x5c, 0xe1, 0xec, 0x8a, 0x86, 0xe0,
+ 0x12, 0x63, 0x87, 0x7a, 0x48, 0x5f, 0x43, 0xb7, 0x8f, 0xf7, 0x8a, 0xcd,
+ 0x1b, 0x08, 0xc8, 0x40, 0xb4, 0x10, 0xfb, 0xc6, 0xa7, 0xc6, 0xf6, 0xee,
+ 0x41, 0x81, 0x07, 0x19, 0x49, 0x10, 0x26, 0xc1, 0xb2, 0x74, 0xba, 0x26,
+ 0xeb, 0xed, 0x08, 0x3b, 0xb9, 0x1d, 0xfc, 0x37, 0xc0, 0x78, 0xcd, 0xe9,
+ 0x25, 0xbe, 0xc8, 0xcf, 0x31, 0x12, 0xd1, 0xec, 0xf1, 0x0a, 0x41, 0x28,
+ 0x34, 0x2d, 0xbe, 0x9f, 0xfe, 0x68, 0x48, 0x8c, 0x89, 0x25, 0x65, 0x0b,
+ 0x3e, 0xc1, 0x1c, 0x9d, 0xee, 0xac, 0x65, 0x4b, 0xc7, 0x32, 0x2f, 0xa3,
+ 0x78, 0xd6, 0x1f, 0x62, 0xe7, 0xae, 0x95, 0x64, 0x6b, 0xc9, 0xef, 0xbd,
+ 0xd3, 0xc6, 0x19, 0xdf, 0x5d, 0x7f, 0x06, 0x0e, 0xe7, 0x0f, 0xaf, 0x8c,
+ 0x36, 0x6f, 0x84, 0xe5, 0xe1, 0x66, 0x3b, 0x96, 0xc4, 0xdd, 0xbc, 0x01,
+ 0xeb, 0x6d, 0xce, 0xf9, 0x1e, 0x8c, 0x71, 0xb8, 0xa0, 0x94, 0x70, 0xb4,
+ 0x67, 0x50, 0xd0, 0x9c, 0xd4, 0xa8, 0xa6, 0x35, 0xe7, 0x9f, 0x00, 0x8e,
+ 0x33, 0x51, 0x4e, 0x66, 0x69, 0xb5, 0x89, 0x63, 0x0e, 0x63, 0x72, 0xd7,
+ 0x18, 0xe1, 0x1d, 0x59, 0xde, 0x14, 0x27, 0xbc, 0x44, 0x4e, 0x42, 0x6c,
+ 0xa1, 0x18, 0xb8, 0x0d, 0x6d, 0x34, 0x82, 0x88, 0xbf, 0x9a, 0x7d, 0xe1,
+ 0x8b, 0xf3, 0xd9, 0x02, 0xae, 0xbd, 0x6f, 0x2e, 0x4f, 0x76, 0xc7, 0x7f,
+ 0x7f, 0x42, 0x1f, 0x77, 0x69, 0x80, 0x01, 0x24, 0xf6, 0xae, 0x04, 0xc5,
+ 0x07, 0xe0, 0xeb, 0xed, 0x46, 0x5e, 0x37, 0xf4, 0xde, 0x17, 0x03, 0xe2,
+ 0xdc, 0xe8, 0x49, 0x38, 0x74, 0xb1, 0xc7, 0xd8, 0x98, 0x0c, 0x2e, 0xad,
+ 0xf8, 0x9b, 0xc1, 0x8c, 0x91, 0x51, 0xdc, 0xb7, 0xbe, 0xfc, 0x86, 0xbf,
+ 0x8c, 0x60, 0x4e, 0xb3, 0xdb, 0xb8, 0x3f, 0xea, 0xf0, 0x81, 0x22, 0x1e,
+ 0x29, 0x21, 0x6f, 0x8b, 0x1e, 0x1c, 0x00, 0xc4, 0x2a, 0x48, 0x88, 0x0c,
+ 0x07, 0xd8, 0xb6, 0x5d, 0xa6, 0x66, 0xac, 0xaa, 0x76, 0x64, 0xe4, 0x4f,
+ 0x62, 0xfa, 0xba, 0x11, 0xfe, 0x34, 0x1b, 0x68, 0x35, 0xb3, 0x04, 0x4f,
+ 0xfd, 0xb7, 0x81, 0x4b, 0x47, 0x23, 0xdb, 0x63, 0x1b, 0x1d, 0x5b, 0x89,
+ 0x5e, 0x01, 0x1a, 0x6b, 0x22, 0x02, 0x40, 0x2c, 0x7f, 0x0f, 0x50, 0x6d,
+ 0xf0, 0x54, 0x29, 0xf0, 0xfc, 0x84, 0x88, 0xcc, 0xeb, 0x3c, 0x21, 0x19,
+ 0x6d, 0x1f, 0x10, 0xa2, 0xec, 0x19, 0x03, 0xf1, 0xb1, 0xcd, 0xc2, 0x1c,
+ 0x8d, 0x94, 0xe0, 0xa4, 0x16, 0xb5, 0x34, 0xbb, 0xee, 0x25, 0x08, 0x27,
+ 0xee, 0x38, 0x2e, 0x24, 0xae, 0x18, 0x52, 0xf1, 0x58, 0x29, 0xbc, 0xc5,
+ 0x65, 0x4f, 0xa6, 0x0e, 0x4b, 0x59, 0xb8, 0x00, 0x07, 0x80, 0x67, 0x87,
+ 0x61, 0x42, 0xa3, 0x11, 0x4a, 0xd5, 0xba, 0x8c, 0x7e, 0x46, 0x8c, 0x6f,
+ 0xef, 0xc9, 0x48, 0x69, 0x16, 0x70, 0xf3, 0x1a, 0xb3, 0xe7, 0x1e, 0xd9,
+ 0x4a, 0x35, 0x02, 0xcb, 0xf8, 0x75, 0x30, 0x77, 0x87, 0xeb, 0x84, 0xfc,
+ 0x41, 0x2b, 0xe7, 0xaa, 0xf6, 0x37, 0x36, 0xd7, 0xac, 0x53, 0xc1, 0xd5,
+ 0x6b, 0xe8, 0xd1, 0x57, 0xf5, 0xc3, 0x42, 0x78, 0x98, 0x09, 0xec, 0xd1,
+ 0x5e, 0x84, 0x26, 0x2b, 0x38, 0x34, 0x1e, 0xc1, 0x20, 0xab, 0xc5, 0x00,
+ 0xe7, 0xf1, 0x43, 0x55, 0xf6, 0x60, 0x8b, 0xee, 0xd4, 0xed, 0x5e, 0x03,
+ 0x2b, 0x1b, 0xbf, 0x4b, 0x77, 0xbe, 0xb3, 0xbd, 0xc8, 0xfa, 0x53, 0x4a,
+ 0xb5, 0xdc, 0xa3, 0x4c, 0x6f, 0xf1, 0x77, 0x54, 0xce, 0x95, 0xca, 0x78,
+ 0xf0, 0xcc, 0x3a, 0xbb, 0xce, 0x9d, 0x20, 0xe6, 0xe4, 0xc6, 0xcd, 0x71,
+ 0x25, 0xbe, 0xd4, 0xac, 0x05, 0xdc, 0x21, 0x66, 0x41, 0xe0, 0x5d, 0x3c,
+ 0x79, 0x14, 0x67, 0xcf, 0x7e, 0x77, 0xf0, 0x8c, 0x33, 0xe7, 0x85, 0xdc,
+ 0xfc, 0x03, 0xe8, 0xf5, 0x2d, 0x34, 0x1e, 0xa6, 0x81, 0x65, 0x7f, 0xe9,
+ 0xe4, 0xa4, 0xb9, 0x1e, 0x9e, 0x5c, 0x66, 0x71, 0xc1, 0xbe, 0xaa, 0x0a,
+ 0x4b, 0x7e, 0x05, 0x66, 0x75, 0x6e, 0x89, 0x44, 0xed, 0x7b, 0x06, 0x60,
+ 0x66, 0x09, 0xb1, 0x1d, 0xf8, 0x21, 0xd0, 0xeb, 0x63, 0x11, 0x06, 0x8a,
+ 0xbc, 0x24, 0xcc, 0xd2, 0x99, 0xc6, 0xc8, 0xbf, 0x5e, 0x94, 0x17, 0xb5,
+ 0x27, 0x74, 0x5b, 0x3c, 0xaf, 0xe3, 0xcb, 0xd6, 0xb5, 0x31, 0xe1, 0xf3,
+ 0x8d, 0x4e, 0x4a, 0xcb, 0xa3, 0x1d, 0x53, 0x43, 0xa0, 0x4f, 0x99, 0xf1,
+ 0x5d, 0x82, 0x22, 0xbd, 0xbd, 0xc4, 0xb8, 0x01, 0xe8, 0xe3, 0xca, 0xd0,
+ 0x68, 0xe6, 0xa3, 0x15, 0xf2, 0x91, 0xd2, 0xf4, 0x1f, 0x65, 0x1b, 0xde,
+ 0x82, 0x3d, 0xdc, 0x01, 0x48, 0x27, 0x64, 0xc1, 0x63, 0x9e, 0xfb, 0x44,
+ 0x51, 0x3a, 0x4b, 0xf9, 0x1a, 0x1e, 0x68, 0xd5, 0x21, 0xee, 0xfe, 0xac,
+ 0x1f, 0x26, 0x9c, 0x5e, 0x53, 0x2f, 0x10, 0x04, 0xda, 0xc3, 0xe2, 0xf7,
+ 0x8e, 0x9e, 0x24, 0xd1, 0xd3, 0xbd, 0x3c, 0x5c, 0xc0, 0x57, 0xf8, 0x57,
+ 0x1f, 0xfa, 0x9f, 0x98, 0xeb, 0x94, 0x80, 0x91, 0x2c, 0x3f, 0x78, 0x61,
+ 0xdc, 0x96, 0x0c, 0x87, 0x45, 0xe5, 0x10, 0x27, 0xc1, 0x7b, 0x69, 0x89,
+ 0x45, 0x6b, 0x34, 0x3a, 0xf6, 0x65, 0x02, 0x81, 0xd8, 0x3c, 0xdb, 0xee,
+ 0x32, 0x06, 0xb2, 0x49, 0x45, 0x9d, 0x61, 0xbe, 0xc1, 0x0b, 0xe3, 0xa5,
+ 0xaa, 0x99, 0xb9, 0x96, 0x6c, 0xcc, 0x73, 0xb9, 0x97, 0x2f, 0xfb, 0xf3,
+ 0x80, 0xf3, 0x51, 0x28, 0x2d, 0x03, 0xb5, 0xc5, 0x56, 0xa6, 0x52, 0xb4,
+ 0x76, 0xd8, 0x6b, 0x6e, 0x32, 0x6b, 0x6b, 0xca, 0x92, 0x14, 0x67, 0xf3,
+ 0xa7, 0xb9, 0x7c, 0xab, 0xa6, 0xf4, 0x27, 0x94, 0x21, 0x5f, 0xa0, 0xb6,
+ 0x36, 0x54, 0x30, 0x60, 0xaa, 0x21, 0x45, 0xac, 0xe5, 0xe6, 0xa1, 0x7c,
+ 0x99, 0x12, 0x7e, 0xdb, 0xab, 0xa2, 0xbd, 0x1c, 0xa3, 0x89, 0xc0, 0x70,
+ 0x45, 0xdf, 0x97, 0xc3, 0xac, 0xe1, 0x5f, 0xe9, 0xee, 0x50, 0x66, 0x25,
+ 0x05, 0xcc, 0x97, 0xad, 0xd4, 0x36, 0x84, 0x97, 0xcb, 0x09, 0xeb, 0x32,
+ 0xf5, 0xf2, 0xfa, 0x3e, 0x2e, 0x3d, 0x68, 0xf7, 0x41, 0xa3, 0x54, 0x27,
+ 0xc5, 0xf2, 0x4d, 0x6f, 0xf9, 0x6d, 0x92, 0x5c, 0x7e, 0xd9, 0x6e, 0x77,
+ 0xd4, 0x0a, 0x56, 0x15, 0x64, 0x06, 0xdd, 0xd0, 0xa7, 0xd1, 0x53, 0xf7,
+ 0x3c, 0x3b, 0x7e, 0x13, 0xc8, 0x96, 0x9e, 0x3a, 0x9a, 0xcf, 0x48, 0x10,
+ 0xa9, 0xca, 0x20, 0xcc, 0x36, 0xed, 0x6c, 0x46, 0x08, 0xc1, 0xf0, 0x7e,
+ 0x74, 0xab, 0xe2, 0xa6, 0x2d, 0xbe, 0x7d, 0x95, 0xd2, 0x27, 0x43, 0x0b,
+ 0x7b, 0x46, 0x48, 0x8f, 0x6c, 0x3b, 0x14, 0x2b, 0x26, 0xb8, 0x41, 0xef,
+ 0xdf, 0x8b, 0x7e, 0x74, 0x62, 0x94, 0x2b, 0x4a, 0x6a, 0x74, 0xc5, 0xc5,
+ 0x8f, 0xa3, 0x77, 0x74, 0xc9, 0x51, 0xa6, 0x4f, 0xc3, 0x07, 0x94, 0x44,
+ 0xb7, 0xac, 0x80, 0x15, 0x4c, 0xf7, 0x47, 0xd2, 0x0d, 0x3d, 0xb6, 0x77,
+ 0x2a, 0x2f, 0x22, 0x5e, 0x0c, 0xf3, 0x73, 0xa6, 0x84, 0x66, 0xb5, 0x34,
+ 0xf5, 0x6b, 0xda, 0x05, 0x89, 0xff, 0x0d, 0x3c, 0x8d, 0x90, 0x8d, 0xb7,
+ 0x73, 0xc9, 0xc2, 0x2e, 0x49, 0x08, 0x01, 0xbe, 0xdd, 0x28, 0xec, 0xcd,
+ 0x02, 0xb6, 0x09, 0xd9, 0xef, 0xcc, 0xe3, 0x2c, 0x9e, 0xc2, 0x91, 0xe4,
+ 0x88, 0x85, 0x86, 0x30, 0x00, 0xab, 0x06, 0x28, 0xd8, 0x80, 0xaa, 0xe5,
+ 0x93, 0x14, 0xd3, 0xbd, 0x88, 0xf6, 0x69, 0xd8, 0x84, 0x57, 0xda, 0x84,
+ 0xa1, 0x90, 0x32, 0xdb, 0x2e, 0x6e, 0xe9, 0xb1, 0x91, 0x66, 0x87, 0x68,
+ 0x4a, 0xc0, 0x51, 0xe9, 0x8b, 0x0f, 0xe8, 0x4c, 0x7d, 0xc6, 0xe7, 0xc5,
+ 0xf7, 0x79, 0x47, 0xa2, 0x67, 0x7b, 0x40, 0x91, 0xdc, 0xdb, 0x2a, 0xd3,
+ 0x40, 0xf0, 0xc7, 0x2d, 0x29, 0x11, 0x38, 0x7f, 0x3a, 0x2e, 0x1a, 0x93,
+ 0xc8, 0xd7, 0x8e, 0x99, 0x6c, 0x8e, 0x6e, 0xe1, 0x05, 0xbb, 0xee, 0x96,
+ 0xdb, 0x0b, 0x8f, 0x9d, 0x74, 0x78, 0xcf, 0x68, 0x9e, 0x10, 0x81, 0x95,
+ 0xf4, 0x75, 0x9b, 0x2f, 0xf1, 0x7d, 0xe6, 0xa4, 0xc8, 0xcb, 0x0d, 0xaa,
+ 0xc2, 0xf0, 0xf0, 0xd2, 0xbb, 0x98, 0x07, 0x68, 0xbd, 0x25, 0x00, 0x14,
+ 0xbc, 0x12, 0x6b, 0xb8, 0xa3, 0xa0, 0xc5, 0xf9, 0x42, 0xae, 0x10, 0xf0,
+ 0x4e, 0xb7, 0xd0, 0xa0, 0x5f, 0x13, 0x97, 0x66, 0x8c, 0x04, 0x1e, 0xc4,
+ 0x25, 0x8b, 0x49, 0x2c, 0x5d, 0x83, 0xef, 0x4e, 0xd0, 0x2b, 0x17, 0x6c,
+ 0x2d, 0x05, 0x48, 0xc0, 0x9b, 0x8f, 0xda, 0xb0, 0x99, 0xf6, 0xe2, 0xb8,
+ 0x01, 0x97, 0x06, 0xf1, 0xcb, 0x0f, 0xdb, 0x11, 0xc0, 0xb1, 0x19, 0x93,
+ 0x79, 0x6e, 0x90, 0x3f, 0xca, 0xa2, 0x9a, 0xc1, 0x81, 0x4f, 0x1e, 0x64,
+ 0xeb, 0xf5, 0x5d, 0x9c, 0xd7, 0x38, 0xf8, 0x7b, 0x1a, 0x9b, 0x7b, 0xd7,
+ 0x58, 0x72, 0x7b, 0xca, 0xd3, 0xe3, 0xbe, 0xbe, 0xf7, 0xa7, 0xa1, 0x74,
+ 0x4e, 0x63, 0x11, 0xb9, 0xec, 0x03, 0xf5, 0x0e, 0x79, 0x8b, 0xf0, 0x77,
+ 0xf8, 0x3d, 0xbb, 0xfe, 0x47, 0x6a, 0xeb, 0x11, 0xcc, 0xa3, 0xd1, 0x6d,
+ 0x92, 0x13, 0xdc, 0x2b, 0x97, 0x9c, 0xb9, 0x00, 0xd6, 0xee, 0xe6, 0xfb,
+ 0xa2, 0x5e, 0x43, 0x12, 0xdc, 0x65, 0x7e, 0x06, 0xc9, 0x40, 0x3c, 0x4b,
+ 0x90, 0x46, 0xc4, 0x22, 0xdf, 0x4b, 0x48, 0xa7, 0xf0, 0xad, 0xac, 0x72,
+ 0x87, 0x06, 0x62, 0x10, 0xea, 0xce, 0x01, 0x27, 0xcf, 0x42, 0x9a, 0x6e,
+ 0x8b, 0x64, 0xbc, 0xa3, 0xcc, 0x4b, 0xa9, 0x2a, 0xd9, 0x41, 0x63, 0xd5,
+ 0xa7, 0x08, 0x7f, 0x9c, 0xe7, 0x54, 0xf2, 0x91, 0x33, 0x88, 0xab, 0xe4,
+ 0x97, 0xce, 0x37, 0xc6, 0x1e, 0x9e, 0x14, 0x4a, 0x0c, 0xef, 0xa4, 0xd1,
+ 0xfc, 0xa5, 0x0d, 0x8b, 0x7a, 0x61, 0x2b, 0xea, 0x05, 0x79, 0xba, 0x33,
+ 0xcd, 0x38, 0xa7, 0x78, 0x07, 0xd2, 0xb8, 0xfb, 0x22, 0x7f, 0x96, 0xb4,
+ 0x6e, 0x0b, 0xdf, 0x35, 0xfd, 0x57, 0x5a, 0xb4, 0x38, 0xc7, 0xfb, 0x90,
+ 0x1b, 0x44, 0x1d, 0x79, 0x12, 0x98, 0x11, 0x95, 0x7b, 0x38, 0xc1, 0x28,
+ 0x75, 0xd9, 0x90, 0xbd, 0xc3, 0x5b, 0x7f, 0x7f, 0x22, 0x1e, 0xb8, 0x87,
+ 0x03, 0xad, 0x03, 0xe3, 0xab, 0x68, 0x25, 0x37, 0x69, 0x06, 0x58, 0xb9,
+ 0x51, 0x8f, 0xb3, 0x1b, 0x73, 0xe1, 0x5c, 0xc5, 0x68, 0xd9, 0x40, 0x22,
+ 0x1b, 0x25, 0xec, 0xc9, 0x4c, 0xe7, 0x96, 0xfb, 0x30, 0xb8, 0x0c, 0x67,
+ 0xbe, 0xcf, 0x2f, 0x6f, 0x5c, 0x42, 0xce, 0x38, 0x23, 0xe1, 0x82, 0xbb,
+ 0xd5, 0x6e, 0x0c, 0x57, 0xe9, 0xc9, 0x29, 0xf7, 0x30, 0xb9, 0xf2, 0xca,
+ 0x63, 0xf4, 0x90, 0x67, 0xdd, 0x51, 0x24, 0xac, 0x4f, 0x05, 0x44, 0xec,
+ 0xb3, 0x77, 0xcd, 0xb5, 0x9e, 0x98, 0xf3, 0xa9, 0xc9, 0x7a, 0xb0, 0x5a,
+ 0xac, 0x8b, 0x22, 0x52, 0x07, 0x27, 0xe3, 0x1e, 0x2d, 0xdd, 0x8e, 0xca,
+ 0xee, 0x1b, 0x61, 0x7f, 0xb0, 0xd3, 0x77, 0xe8, 0x29, 0xc8, 0xc1, 0x8f,
+ 0x60, 0x79, 0x0d, 0xdf, 0xac, 0x83, 0x1a, 0x76, 0x84, 0x23, 0x8a, 0x0e,
+ 0x17, 0xfd, 0x93, 0xaf, 0x77, 0x5c, 0x69, 0xa8, 0x44, 0x53, 0x83, 0xd2,
+ 0xcf, 0x46, 0x91, 0xf1, 0xca, 0x48, 0x8f, 0xcc, 0xc9, 0xd3, 0x7b, 0x73,
+ 0x86, 0xa3, 0xc7, 0x5c, 0x02, 0x8f, 0xe2, 0xac, 0x90, 0xa9, 0x38, 0x1f,
+ 0x57, 0xee, 0xc1, 0xde, 0x91, 0xc5, 0xa6, 0x0c, 0x70, 0xd4, 0x63, 0x08,
+ 0x04, 0x16, 0x8f, 0x53, 0x27, 0xba, 0x06, 0x75, 0x8e, 0xe7, 0x54, 0x6f,
+ 0xd9, 0x7d, 0xef, 0x6d, 0x42, 0xfb, 0x67, 0x99, 0xfb, 0x97, 0x8b, 0xe3,
+ 0x28, 0x88, 0xf2, 0x92, 0xcb, 0x78, 0x6e, 0x86, 0x85, 0x01, 0x59, 0x19,
+ 0x4b, 0x1c, 0x3f, 0x25, 0x6f, 0xd0, 0x6c, 0x58, 0x2f, 0xdb, 0x05, 0xeb,
+ 0xc3, 0x14, 0x06, 0x5c, 0xa0, 0x6e, 0x89, 0x4d, 0x81, 0x20, 0x55, 0xf3,
+ 0xba, 0xf0, 0x36, 0x4f, 0x66, 0x83, 0x3c, 0x8f, 0xe1, 0x34, 0xcc, 0x23,
+ 0xe2, 0x8b, 0x91, 0x03, 0x80, 0xa9, 0x88, 0x69, 0xa7, 0x97, 0xb6, 0x43,
+ 0x47, 0x17, 0xa2, 0x0f, 0x58, 0x41, 0x86, 0x60, 0xfc, 0x8f, 0x4f, 0xca,
+ 0x44, 0x0a, 0x79, 0x6b, 0x75, 0xfa, 0x65, 0x73, 0x9f, 0x17, 0xe1, 0x3d,
+ 0x53, 0xc8, 0x9b, 0x8c, 0x15, 0xf0, 0x91, 0x35, 0xe4, 0x04, 0x32, 0x8c,
+ 0x61, 0x46, 0xcd, 0x48, 0x6d, 0x34, 0xaf, 0xfb, 0x49, 0x92, 0xd0, 0xcb,
+ 0xb9, 0x9d, 0xcb, 0x53, 0x51, 0x67, 0xd2, 0x7e, 0x0a, 0xa3, 0x33, 0x02,
+ 0x99, 0x66, 0xf2, 0xdd, 0x63, 0x54, 0xe5, 0x5e, 0xa5, 0x2f, 0xd3, 0xf8,
+ 0x2c, 0xa6, 0x10, 0xe6, 0xba, 0x4a, 0xb4, 0x04, 0xc1, 0x9e, 0xe1, 0xe5,
+ 0x2e, 0xf6, 0xc0, 0x70, 0x10, 0x7a, 0x73, 0xff, 0xd8, 0x4a, 0x10, 0xc9,
+ 0x13, 0x29, 0xa8, 0x94, 0x41, 0xfa, 0xd1, 0xf8, 0xb7, 0xad, 0xad, 0xcf,
+ 0xd8, 0xaf, 0xf2, 0x4a, 0x72, 0x7b, 0xd3, 0xa5, 0x7d, 0xe8, 0x63, 0x17,
+ 0x76, 0x58, 0xf7, 0xdb, 0xc4, 0xc9, 0xa8, 0x96, 0x63, 0x48, 0xa7, 0xf4,
+ 0x46, 0x58, 0x8e, 0x64, 0x21, 0xc1, 0xcb, 0x66, 0x1d, 0xe4, 0x03, 0x3b,
+ 0xa9, 0x77, 0x5f, 0x15, 0x05, 0xac, 0x70, 0x80, 0x2b, 0xc8, 0x90, 0x9e,
+ 0xd9, 0xfc, 0xc3, 0xbb, 0xd4, 0xb6, 0x37, 0x24, 0x78, 0x45, 0x51, 0xff,
+ 0x2b, 0xd3, 0xe6, 0x13, 0x76, 0x73, 0x15, 0xf7, 0x39, 0xeb, 0x1f, 0xc8,
+ 0xd9, 0x4e, 0xe3, 0x6c, 0x9b, 0x6c, 0xe7, 0x3b, 0xe2, 0x2e, 0xec, 0x6a,
+ 0xc0, 0x50, 0xcb, 0x3e, 0xe8, 0xfb, 0x58, 0xb4, 0x72, 0xf4, 0x54, 0x38,
+ 0xc3, 0xf7, 0x43, 0x2d, 0xec, 0xd0, 0x85, 0x31, 0x27, 0xa2, 0x77, 0x0b,
+ 0x16, 0x26, 0xe6, 0x79, 0x4c, 0xc0, 0xd4, 0x49, 0x0f, 0x78, 0x8a, 0x63,
+ 0xca, 0x75, 0xeb, 0x15, 0x36, 0xce, 0xd3, 0x4f, 0x93, 0x95, 0xda, 0xa0,
+ 0x73, 0x25, 0x3a, 0xbd, 0xa4, 0x24, 0x5d, 0xb7, 0x55, 0x71, 0x5d, 0x09,
+ 0x4b, 0x1c, 0xf2, 0xd6, 0xf8, 0x36, 0x20, 0x82, 0x70, 0x33, 0xa1, 0x1f,
+ 0xb4, 0x17, 0xcc, 0xfd, 0xb8, 0xaf, 0xe7, 0x4a, 0x30, 0x35, 0xe1, 0x8c,
+ 0x38, 0x4c, 0x3c, 0x1c, 0xb0, 0xc0, 0x31, 0x98, 0x47, 0x84, 0x59, 0x67,
+ 0xbf, 0x6c, 0x57, 0x55, 0xe6, 0x0d, 0x82, 0x6b, 0xe2, 0xd0, 0xd2, 0x71,
+ 0x51, 0x4c, 0x83, 0x5d, 0xea, 0x17, 0xbf, 0xb9, 0xbb, 0x37, 0xdf, 0x30,
+ 0x3f, 0xe9, 0x85, 0x32, 0x06, 0xd5, 0xbb, 0x4a, 0xb7, 0xca, 0xab, 0xca,
+ 0x15, 0x65, 0x11, 0x35, 0x1b, 0x4b, 0xdb, 0x72, 0xd1, 0xbc, 0xc4, 0x55,
+ 0xd4, 0x8f, 0xa0, 0xb6, 0x70, 0x46, 0x9a, 0x07, 0x61, 0xee, 0xc7, 0xbb,
+ 0x8a, 0x93, 0xe7, 0x1f, 0xfc, 0x42, 0x77, 0x27, 0xcc, 0xff, 0x78, 0xa7,
+ 0x26, 0x08, 0x97, 0x93, 0x7a, 0xda, 0x32, 0xc8, 0x3d, 0xbb, 0x0d, 0x2c,
+ 0x77, 0x22, 0x1e, 0xd3, 0x37, 0x74, 0xe7, 0xb8, 0x00, 0x7c, 0x88, 0x08,
+ 0x9c, 0x93, 0xa6, 0x83, 0xea, 0x5c, 0xec, 0xdc, 0x3f, 0x6a, 0x87, 0x69,
+ 0x65, 0x9a, 0x03, 0xb1, 0xe4, 0x6d, 0xdc, 0x3a, 0x27, 0xad, 0x72, 0xcb,
+ 0x15, 0x7a, 0x10, 0x30, 0x4a, 0xe8, 0xfe, 0x0c, 0xfc, 0x54, 0x12, 0xfa,
+ 0xca, 0x24, 0x83, 0x28, 0xde, 0x87, 0xf2, 0x89, 0x78, 0x99, 0xa2, 0x28,
+ 0xce, 0xce, 0x30, 0x8e, 0x90, 0x6f, 0x6a, 0x75, 0x78, 0x84, 0xa5, 0x01,
+ 0x5c, 0x2c, 0x99, 0xc2, 0x93, 0x16, 0xa9, 0x0b, 0xc7, 0x4e, 0xa3, 0xb4,
+ 0xb3, 0x54, 0x5f, 0x06, 0x41, 0xe1, 0xd7, 0x43, 0xed, 0x52, 0xde, 0x5f,
+ 0x66, 0xf4, 0xb9, 0x67, 0xad, 0x07, 0xe8, 0x18, 0xaa, 0x6a, 0x7a, 0x0c,
+ 0xe6, 0x9b, 0xba, 0x76, 0xb4, 0x23, 0x06, 0x18, 0x27, 0x8f, 0x11, 0xba,
+ 0x4e, 0x30, 0xdd, 0x46, 0x00, 0xa9, 0xc1, 0x3b, 0x05, 0xe7, 0xdd, 0x5f,
+ 0xfb, 0x53, 0xe0, 0x62, 0x56, 0x12, 0x03, 0x61, 0x5e, 0xd9, 0x5d, 0x68,
+ 0x0d, 0x29, 0x89, 0x87, 0xb5, 0x37, 0x13, 0x6f, 0xa7, 0x8b, 0x68, 0x2f,
+ 0xcf, 0x0c, 0x30, 0xed, 0x0c, 0xda, 0x10, 0x9e, 0xf7, 0x5b, 0xa1, 0x99,
+ 0x30, 0xcb, 0xfa, 0xec, 0x53, 0x18, 0xe4, 0x40, 0x97, 0xbc, 0x13, 0x0b,
+ 0xa0, 0x07, 0x57, 0xf1, 0x5d, 0x57, 0xe5, 0x86, 0x67, 0x11, 0x17, 0xab,
+ 0x89, 0x93, 0xdc, 0x06, 0x92, 0x36, 0xd9, 0x9c, 0x78, 0xbd, 0xd6, 0xe1,
+ 0x4f, 0xb7, 0x7f, 0xd5, 0x89, 0x18, 0x4f, 0xc8, 0x7a, 0x2d, 0x3d, 0x01,
+ 0x26, 0x6b, 0xa0, 0xd3, 0xfa, 0xa7, 0x42, 0xf7, 0xf4, 0x9f, 0xcb, 0x77,
+ 0xc3, 0x40, 0x44, 0x55, 0x85, 0x36, 0xd5, 0x7d, 0xaf, 0x58, 0xac, 0x44,
+ 0x4e, 0x84, 0xfb, 0x86, 0x40, 0x8b, 0xea, 0x17, 0x73, 0x63, 0x65, 0xd2,
+ 0x27, 0xc0, 0x7b, 0x7f, 0x56, 0xd3, 0x30, 0xc1, 0xe0, 0x09, 0x49, 0x11,
+ 0xd7, 0x2b, 0x2c, 0x15, 0x3c, 0x8d, 0x57, 0xff, 0xb5, 0x22, 0x62, 0x31,
+ 0x66, 0x6f, 0x01, 0x93, 0xc7, 0x45, 0xd6, 0xdb, 0xa6, 0xba, 0x18, 0x0d,
+ 0x91, 0x1f, 0x33, 0x23, 0xb5, 0x77, 0x58, 0x56, 0xe7, 0xdd, 0x27, 0xa1,
+ 0xf8, 0x8d, 0x9f, 0x3f, 0xe5, 0x0b, 0x2f, 0xa4, 0x29, 0x4e, 0x3a, 0x47,
+ 0x98, 0xdd, 0x23, 0x02, 0x53, 0x6c, 0x27, 0xa3, 0x3e, 0xbe, 0x76, 0xac,
+ 0x8b, 0xe4, 0xbd, 0x7a, 0x7d, 0xe0, 0x35, 0xe6, 0x1b, 0xc9, 0x1c, 0xb1,
+ 0xf5, 0xfd, 0xa4, 0xee, 0xdd, 0x6d, 0x14, 0x37, 0x8f, 0x1a, 0x5d, 0xbe,
+ 0xa1, 0x6e, 0xed, 0xa4, 0x78, 0xc5, 0xaa, 0x1d, 0xf4, 0xc3, 0x5d, 0xd8,
+ 0x21, 0x40, 0xc4, 0xd7, 0x74, 0x15, 0x2c, 0x77, 0x0e, 0xb8, 0x79, 0x2d,
+ 0x3c, 0xb2, 0x42, 0xc8, 0x7d, 0x66, 0x3c, 0xe8, 0xab, 0xdd, 0xa5, 0x4a,
+ 0x09, 0x47, 0x15, 0x54, 0x92, 0x97, 0x4d, 0xf0, 0x85, 0x91, 0x08, 0x83,
+ 0x26, 0x44, 0xdd, 0xd3, 0x51, 0x28, 0xb4, 0x9b, 0xd5, 0x01, 0xac, 0x04,
+ 0x1a, 0x27, 0x16, 0x0c, 0xc7, 0x75, 0xc2, 0x10, 0x42, 0x66, 0x22, 0x53,
+ 0x6b, 0xa8, 0x3d, 0x72, 0xf9, 0xc5, 0x6a, 0x6b, 0xf7, 0x93, 0xb8, 0x1f,
+ 0x6e, 0xb3, 0x8b, 0x41, 0x55, 0xb2, 0x30, 0x5c, 0xc5, 0x00, 0xca, 0x7d,
+ 0x63, 0xeb, 0xbc, 0x30, 0xcd, 0x99, 0xbb, 0x80, 0xb8, 0x66, 0x90, 0x38,
+ 0x90, 0xb6, 0x4f, 0x4e, 0xab, 0x93, 0x5d, 0x53, 0xec, 0x57, 0x5c, 0xd3,
+ 0x53, 0x68, 0x8c, 0x99, 0xfa, 0x47, 0x23, 0xe1, 0x8e, 0x0f, 0x37, 0xff,
+ 0xdf, 0x96, 0xc2, 0x73, 0x91, 0x79, 0x06, 0x04, 0x94, 0xe9, 0xb3, 0xe7,
+ 0x7d, 0x30, 0xf4, 0x8d, 0x43, 0xdb, 0xc8, 0x3e, 0x25, 0x95, 0x8c, 0x8b,
+ 0x31, 0x6e, 0x5b, 0x6a, 0x32, 0x29, 0x93, 0xbd, 0x45, 0xea, 0x90, 0x39,
+ 0x7a, 0x82, 0xe5, 0x92, 0xbf, 0x61, 0x23, 0x3b, 0x5f, 0xcc, 0x81, 0x76,
+ 0x26, 0x20, 0xce, 0x28, 0x78, 0xe1, 0xe6, 0xb0, 0xc6, 0xe3, 0xa3, 0xb0,
+ 0x90, 0x62, 0x91, 0xa1, 0x8e, 0x47, 0x49, 0x23, 0xb9, 0xbb, 0x90, 0xc8,
+ 0xdc, 0x99, 0x63, 0xe8, 0x22, 0x10, 0xca, 0x46, 0xb3, 0xd0, 0x13, 0xd2,
+ 0x90, 0xac, 0x02, 0xb5, 0xba, 0x86, 0x3d, 0x46, 0x7f, 0xbe, 0xbc, 0x98,
+ 0x40, 0x67, 0xaa, 0x58, 0xa5, 0x73, 0xdf, 0xb3, 0x1f, 0x90, 0x9a, 0x09,
+ 0xa8, 0x57, 0x32, 0x13, 0x62, 0x95, 0xf1, 0x2b, 0xcd, 0x28, 0xf3, 0x61,
+ 0xf6, 0x4b, 0xad, 0x0b, 0xec, 0x10, 0xb8, 0x4c, 0x1f, 0x2f, 0x84, 0x02,
+ 0xea, 0x6c, 0x1b, 0xb3, 0x71, 0x93, 0x55, 0x95, 0xbd, 0xd0, 0xfa, 0x18,
+ 0x96, 0xd4, 0x78, 0x62, 0x89, 0x5f, 0xcb, 0x21, 0xad, 0x9c, 0xcf, 0xbc,
+ 0x80, 0xbb, 0x13, 0x0c, 0x1f, 0xad, 0xa9, 0x9d, 0x12, 0x57, 0x29, 0xc1,
+ 0xe1, 0xc4, 0x97, 0xd6, 0x65, 0xe9, 0xdc, 0xc3, 0x1b, 0xc3, 0x0d, 0x5e,
+ 0xe5, 0x5f, 0x12, 0xe8, 0xb2, 0x87, 0xd2, 0xc7, 0x57, 0x9b, 0x8a, 0xa8,
+ 0x63, 0xfb, 0x61, 0x42, 0x9f, 0x5b, 0xf6, 0xbb, 0xfe, 0x50, 0x39, 0x36,
+ 0x4d, 0x27, 0x70, 0x96, 0xe0, 0xc8, 0x6b, 0xa1, 0xe6, 0xd1, 0xe4, 0x35,
+ 0xad, 0x91, 0xb5, 0x20, 0x62, 0x3b, 0x97, 0x92, 0x94, 0xd1, 0xac, 0xa4,
+ 0xac, 0xf8, 0x54, 0x16, 0xf3, 0xf8, 0xa4, 0x15, 0x3d, 0x0e, 0x0d, 0x22,
+ 0x93, 0x11, 0xa4, 0x42, 0xa4, 0x55, 0xc4, 0x13, 0x9b, 0x95, 0x75, 0xf0,
+ 0xcb, 0x06, 0x12, 0x3f, 0x55, 0xbf, 0xe8, 0x04, 0x8e, 0x35, 0xa9, 0x10,
+ 0xa1, 0x09, 0x67, 0xe7, 0xd2, 0x65, 0xb9, 0x77, 0x3f, 0x2a, 0x43, 0xf6,
+ 0xae, 0xe1, 0xdb, 0x92, 0xbf, 0x25, 0x9f, 0x24, 0xb2, 0x42, 0x44, 0x65,
+ 0x35, 0xb4, 0x02, 0x74, 0x2d, 0x93, 0xe5, 0x33, 0xf2, 0x75, 0xae, 0xba,
+ 0x6e, 0xb9, 0x55, 0xeb, 0xb6, 0x60, 0x22, 0x24, 0xfd, 0xf3, 0x98, 0x32,
+ 0x25, 0x5e, 0xb9, 0x60, 0x82, 0xe8, 0x46, 0x0a, 0x68, 0x47, 0x5c, 0x65,
+ 0xd5, 0xa2, 0xe7, 0x29, 0x43, 0x80, 0x9f, 0x1d, 0x2d, 0xb2, 0x27, 0x67,
+ 0x39, 0x47, 0x19, 0xfe, 0xfa, 0x1b, 0x4a, 0x09, 0xee, 0xa1, 0xe7, 0x87,
+ 0xdf, 0xd7, 0xc4, 0xb8, 0x20, 0xff, 0x8b, 0xda, 0x42, 0x92, 0x41, 0x20,
+ 0xd5, 0xd1, 0x8b, 0xfb, 0x2a, 0xfd, 0x37, 0xa4, 0xc6, 0xaf, 0x68, 0xd8,
+ 0xae, 0xd5, 0xbc, 0xf9, 0x83, 0x88, 0xb4, 0xc7, 0x3d, 0x02, 0x99, 0x6c,
+ 0x5c, 0xc3, 0x56, 0xde, 0x7a, 0xb2, 0x2f, 0x59, 0xdb, 0x10, 0x1e, 0xf7,
+ 0x9f, 0xfb, 0x45, 0x81, 0x4b, 0x6e, 0xd4, 0x6e, 0x27, 0x1b, 0x4f, 0x06,
+ 0x9b, 0x35, 0x02, 0xe7, 0xbe, 0xca, 0x65, 0xd8, 0xa4, 0xcc, 0xe8, 0x99,
+ 0x36, 0xaf, 0x96, 0x98, 0xe4, 0x73, 0x75, 0x07, 0x5f, 0x09, 0xf5, 0x8a,
+ 0x2a, 0x39, 0x19, 0x9e, 0x24, 0xfa, 0x21, 0x78, 0x41, 0xb1, 0xab, 0x6e,
+ 0x8b, 0xeb, 0xa3, 0xd9, 0x0e, 0x9f, 0xf4, 0x43, 0x08, 0x36, 0xe3, 0xa2,
+ 0x46, 0x7a, 0x93, 0x0c, 0x3f, 0x56, 0x3d, 0x55, 0xbe, 0xb0, 0x04, 0x96,
+ 0x3f, 0x22, 0xcf, 0x89, 0x02, 0xac, 0xf3, 0x0f, 0x17, 0xbe, 0xbc, 0x6f,
+ 0x61, 0x2b, 0xfb, 0x5e, 0xf2, 0x21, 0x31, 0xdc, 0xdc, 0x51, 0xbf, 0x59,
+ 0x83, 0xf8, 0xb1, 0x72, 0x23, 0x07, 0xea, 0xea, 0xc9, 0x7a, 0xe7, 0x2e,
+ 0x9a, 0x45, 0x90, 0xa5, 0xb9, 0x1d, 0x09, 0x79, 0xaa, 0x66, 0xe6, 0xf8,
+ 0x59, 0x40, 0x79, 0x37, 0xc9, 0x24, 0xa2, 0x91, 0xa0, 0x1f, 0xa0, 0x68,
+ 0x7b, 0x91, 0x3c, 0x85, 0xa8, 0x23, 0xbc, 0xd8, 0x6c, 0x86, 0x5b, 0x76,
+ 0xa2, 0xdd, 0x28, 0x04, 0x46, 0x23, 0x63, 0xb6, 0xfc, 0x26, 0x51, 0xde,
+ 0x87, 0xd5, 0x98, 0xf3, 0x9f, 0xde, 0xd2, 0xee, 0xde, 0x6d, 0x17, 0x70,
+ 0x0e, 0xb4, 0x62, 0xb3, 0xfa, 0x96, 0x3a, 0xff, 0x6b, 0x16, 0x30, 0x0f,
+ 0x1f, 0xd7, 0x61, 0xb3, 0xf2, 0x62, 0x76, 0x4a, 0x36, 0x62, 0x63, 0x9f,
+ 0x79, 0x8c, 0xb8, 0x9d, 0x91, 0x81, 0x23, 0x37, 0x05, 0xfc, 0xd7, 0x69,
+ 0xac, 0x2b, 0xaf, 0x98, 0x47, 0xad, 0x81, 0x05, 0xc3, 0xec, 0xeb, 0xd1,
+ 0x9d, 0xbf, 0xe6, 0x82, 0xf1, 0x0b, 0x9a, 0x8a, 0x1c, 0xf5, 0xd4, 0xcc,
+ 0xf5, 0x6e, 0x67, 0xa4, 0xe1, 0xdb, 0x2f, 0x18, 0x7a, 0xf3, 0x8b, 0x24,
+ 0xac, 0xca, 0x71, 0xef, 0x42, 0x44, 0xb6, 0xd7, 0x17, 0xa4, 0x68, 0x22,
+ 0x67, 0x8e, 0xad, 0xe3, 0x92, 0x63, 0x40, 0xe0, 0xea, 0x91, 0x8f, 0x4c,
+ 0xe8, 0xc6, 0x2b, 0x4c, 0xca, 0x4b, 0x29, 0x12, 0xf4, 0xb6, 0x2b, 0xdf,
+ 0xd5, 0x50, 0x62, 0x64, 0x43, 0x87, 0x54, 0x87, 0x4e, 0x18, 0x0c, 0x51,
+ 0x07, 0x8f, 0xe6, 0x3f, 0x3f, 0xc2, 0xb9, 0x96, 0xee, 0xd0, 0x74, 0x2b,
+ 0x19, 0xab, 0x88, 0x7b, 0xb5, 0x78, 0x93, 0xc7, 0x04, 0x2e, 0x27, 0x64,
+ 0x78, 0x29, 0x7f, 0x0a, 0x37, 0x79, 0xbc, 0x6b, 0x1c, 0x7f, 0x97, 0x01,
+ 0xe8, 0x97, 0x35, 0xf3, 0xb6, 0x1c, 0xd2, 0xa1, 0x7f, 0x83, 0xb5, 0x44,
+ 0x62, 0xc0, 0x90, 0xc7, 0xd5, 0xa5, 0xd0, 0x80, 0x58, 0x5e, 0xaa, 0x80,
+ 0xb2, 0x62, 0x0e, 0xe3, 0xc1, 0xb5, 0x3f, 0x5c, 0x0b, 0x4f, 0xea, 0x4c,
+ 0x6c, 0x08, 0xcd, 0xe8, 0xd8, 0xb0, 0x50, 0x8a, 0xc7, 0x8d, 0x07, 0x74,
+ 0x9c, 0x41, 0x52, 0x42, 0x15, 0x15, 0x32, 0x3b, 0xa6, 0x04, 0x29, 0x30,
+ 0x3f, 0xf7, 0x7b, 0x1a, 0x13, 0x6e, 0x85, 0x90, 0xde, 0x95, 0x2f, 0x48,
+ 0x03, 0x5f, 0x2c, 0x3a, 0x0e, 0x8d, 0x77, 0x36, 0x25, 0xc5, 0x16, 0x6a,
+ 0xfb, 0x5c, 0x88, 0x86, 0xdf, 0x49, 0x81, 0xc5, 0x61, 0xef, 0xee, 0x27,
+ 0x64, 0xa5, 0x68, 0x56, 0xc4, 0x78, 0xe3, 0x41, 0x0e, 0x02, 0xd8, 0x58,
+ 0xe7, 0xbd, 0x79, 0xa8, 0x56, 0xdf, 0x18, 0x15, 0xb0, 0x64, 0x11, 0x5b,
+ 0x83, 0xdd, 0x6d, 0x7f, 0x0b, 0x96, 0xe0, 0x0a, 0x48, 0xc8, 0x9d, 0xf5,
+ 0x5c, 0xf0, 0x7d, 0xcd, 0x14, 0x83, 0xc0, 0x9a, 0x78, 0x34, 0xcc, 0xd7,
+ 0xa7, 0x11, 0x65, 0x85, 0xb8, 0x44, 0x3a, 0xa5, 0x89, 0x94, 0xd3, 0x44,
+ 0x60, 0xe9, 0xa3, 0xbf, 0xe2, 0xbf, 0xe4, 0xee, 0xc8, 0xcd, 0x2d, 0x7b,
+ 0xa9, 0xab, 0x8d, 0x90, 0x02, 0x34, 0x6f, 0xe5, 0x4c, 0xe7, 0xfc, 0xdc,
+ 0x1e, 0x95, 0xee, 0xc7, 0xa8, 0x06, 0xaf, 0x21, 0xda, 0x6e, 0x45, 0x12,
+ 0x0f, 0x06, 0x4e, 0x86, 0x0c, 0x97, 0x71, 0x8c, 0x40, 0x47, 0xd9, 0x2d,
+ 0x1e, 0x76, 0xed, 0xb7, 0xf2, 0x27, 0xe0, 0x5e, 0x7a, 0xa8, 0xb1, 0x88,
+ 0x40, 0xd8, 0xc2, 0x57, 0x51, 0x30, 0xc8, 0x02, 0x6b, 0xcd, 0x6b, 0xb7,
+ 0x35, 0x60, 0xd7, 0xd8, 0x70, 0x34, 0x79, 0x6f, 0x01, 0xaf, 0x26, 0xf0,
+ 0x13, 0x18, 0x1f, 0x39, 0x69, 0xfc, 0x75, 0xb7, 0x16, 0x8e, 0x7a, 0x40,
+ 0x64, 0xf3, 0x7f, 0xa9, 0x94, 0xa1, 0x50, 0xe4, 0x7c, 0xcf, 0x54, 0x08,
+ 0x04, 0x45, 0x8a, 0xfe, 0x9c, 0x6a, 0x03, 0x29, 0xea, 0x5d, 0x25, 0xcd,
+ 0xad, 0xfb, 0xd7, 0x14, 0xb8, 0xef, 0x1c, 0x50, 0xc6, 0xd6, 0xab, 0x5a,
+ 0x89, 0x5e, 0x57, 0x39, 0x01, 0xf0, 0xf2, 0x76, 0x18, 0x5f, 0x37, 0x1e,
+ 0xc7, 0x38, 0x63, 0x17, 0x1c, 0x88, 0x3f, 0xa9, 0x72, 0xb3, 0xd8, 0xfd,
+ 0xed, 0xe3, 0xee, 0x3c, 0xa7, 0xf1, 0xb6, 0x48, 0x3a, 0xf8, 0xb4, 0xe2,
+ 0x49, 0x7c, 0x78, 0x83, 0xe8, 0x98, 0xfd, 0xbb, 0x54, 0x2d, 0xa5, 0x9b,
+ 0xab, 0x8f, 0xd2, 0x21, 0xaa, 0x7d, 0x20, 0x7a, 0xb5, 0x97, 0x16, 0x33,
+ 0xba, 0xf7, 0x94, 0x8f, 0xd6, 0x30, 0x77, 0xda, 0x83, 0x67, 0x5a, 0xc7,
+ 0x79, 0x7b, 0x60, 0x8e, 0x5e, 0x50, 0x1a, 0x36, 0xd0, 0x6d, 0x03, 0x1c,
+ 0x32, 0xf9, 0x6c, 0xa7, 0xaf, 0x82, 0xcc, 0x4c, 0x27, 0x8c, 0xca, 0x23,
+ 0x67, 0x32, 0x39, 0xc0, 0x5c, 0x23, 0x71, 0xa4, 0x37, 0x38, 0xf0, 0xf8,
+ 0xa2, 0x16, 0x0e, 0x31, 0x42, 0xaf, 0x5d, 0x09, 0xff, 0x07, 0x71, 0x47,
+ 0x76, 0x6a, 0xb0, 0xd8, 0x27, 0x9e, 0x95, 0x49, 0x73, 0xf2, 0xea, 0x81,
+ 0xa2, 0xc4, 0xfb, 0x48, 0xa3, 0xf7, 0x6d, 0x5b, 0xd7, 0x68, 0x95, 0xfc,
+ 0x9d, 0x81, 0x98, 0x25, 0x56, 0xc8, 0x82, 0x40, 0x0a, 0xe1, 0x6a, 0xc0,
+ 0x80, 0xe8, 0x68, 0x4f, 0x18, 0x2f, 0x4a, 0xa3, 0xb4, 0x87, 0xa8, 0xc5,
+ 0xb3, 0xcf, 0xb3, 0xae, 0x40, 0x65, 0x55, 0x5c, 0x46, 0xd0, 0x20, 0xb3,
+ 0xae, 0x51, 0x99, 0xe2, 0xfd, 0x3a, 0x23, 0x67, 0xdd, 0x31, 0x75, 0xa1,
+ 0x38, 0x1a, 0x70, 0x9d, 0x1c, 0x50, 0xa6, 0xe6, 0xf6, 0x5a, 0x64, 0x6a,
+ 0x7f, 0xbd, 0x34, 0xa8, 0xba, 0xf6, 0x5e, 0x51, 0x3c, 0x16, 0x45, 0xfa,
+ 0x89, 0x4c, 0x8a, 0x4f, 0x6e, 0x55, 0xbb, 0xcc, 0xe7, 0x5f, 0x9f, 0xd7,
+ 0x65, 0x79, 0xa4, 0xe4, 0x9c, 0x1a, 0xf6, 0xba, 0x63, 0x98, 0x4e, 0xd2,
+ 0xa5, 0x50, 0x80, 0x8f, 0xed, 0xf5, 0xfc, 0x7e, 0x57, 0xc5, 0xe8, 0xdf,
+ 0x0e, 0xc6, 0xdc, 0x13, 0xbf, 0xab, 0xb6, 0x23, 0xc9, 0xb7, 0x6d, 0x8c,
+ 0xac, 0x45, 0x2f, 0x28, 0xa8, 0x01, 0xb2, 0x09, 0xf5, 0x8d, 0xcd, 0x34,
+ 0x72, 0x14, 0x12, 0x24, 0x2c, 0x47, 0x92, 0xa5, 0x1c, 0xb6, 0x42, 0x4b,
+ 0x0b, 0x1a, 0x1f, 0xb8, 0x2c, 0xcb, 0x0a, 0x7c, 0x04, 0x90, 0x9c, 0x21,
+ 0x39, 0xe1, 0x85, 0x0c, 0x83, 0xf8, 0xf0, 0xf6, 0xe5, 0xc5, 0x85, 0x62,
+ 0xaf, 0xb0, 0x92, 0xc1, 0x45, 0x81, 0xcc, 0xa8, 0x95, 0xdf, 0x97, 0xa2,
+ 0x65, 0x45, 0x38, 0x47, 0x42, 0xf6, 0x17, 0xf8, 0x2d, 0x3b, 0x82, 0x0e,
+ 0x6c, 0x15, 0xb0, 0xe3, 0xc6, 0x7f, 0x65, 0x40, 0x91, 0xbb, 0x6d, 0xac,
+ 0xdf, 0x57, 0x21, 0x91, 0x9b, 0x27, 0xd9, 0x33, 0x4e, 0x8c, 0x5f, 0x9a,
+ 0x56, 0xb7, 0x42, 0x84, 0x94, 0xdb, 0xe4, 0x8a, 0x45, 0xde, 0x0c, 0x90,
+ 0x15, 0x00, 0x3c, 0xd6, 0xd4, 0xcf, 0xd0, 0xec, 0xaa, 0xf1, 0x61, 0xe2,
+ 0x27, 0xe8, 0x34, 0xe0, 0x6f, 0x52, 0x97, 0xee, 0x0e, 0xd1, 0xbd, 0x15,
+ 0xe1, 0x53, 0x1d, 0xac, 0x42, 0xc7, 0x73, 0x3d, 0xd8, 0x72, 0x22, 0xbf,
+ 0x56, 0x28, 0xb6, 0x40, 0x37, 0x91, 0xe2, 0xbe, 0xeb, 0x56, 0xf1, 0x0b,
+ 0xcf, 0x1e, 0xd3, 0x5c, 0x40, 0x0d, 0x17, 0x07, 0x76, 0x6f, 0x03, 0x2f,
+ 0x81, 0x62, 0xee, 0xf3, 0xc0, 0xe3, 0xfc, 0x6c, 0x73, 0x73, 0x37, 0x5e,
+ 0x7d, 0xad, 0x07, 0x92, 0x1a, 0x23, 0x38, 0xb6, 0x07, 0x97, 0x35, 0x8e,
+ 0x9b, 0x95, 0x42, 0x8a, 0xf2, 0xbf, 0x5a, 0x46, 0xf8, 0x3b, 0x55, 0xfc,
+ 0xde, 0x79, 0xa6, 0xa4, 0x1b, 0xa8, 0xe9, 0xba, 0x7a, 0x97, 0x26, 0x18,
+ 0x8c, 0xac, 0x34, 0xa3, 0xd3, 0x2f, 0x55, 0x8a, 0xd9, 0xa6, 0x56, 0x86,
+ 0xd0, 0x76, 0x30, 0x9f, 0x30, 0x64, 0x57, 0xbd, 0x78, 0xf7, 0xaf, 0x25,
+ 0xef, 0x01, 0xdd, 0x99, 0xfe, 0x5c, 0x52, 0x40, 0x1d, 0xdb, 0x84, 0xb4,
+ 0x55, 0x8a, 0x29, 0x64, 0x50, 0x22, 0x66, 0xc4, 0xa2, 0xc5, 0x49, 0x74,
+ 0x37, 0x08, 0x36, 0xec, 0x6a, 0x5c, 0x7a, 0x95, 0x75, 0xca, 0xec, 0xc1,
+ 0x05, 0x13, 0x27, 0x4d, 0xd2, 0x03, 0xee, 0x7b, 0xe0, 0x0b, 0xbc, 0x9b,
+ 0x7d, 0x4b, 0x64, 0x67, 0x1f, 0x2c, 0x1c, 0x65, 0x6e, 0x1c, 0xea, 0x3a,
+ 0x45, 0x41, 0x70, 0xdb, 0xad, 0x08, 0x2c, 0x06, 0x70, 0x43, 0xa8, 0x6c,
+ 0xa5, 0x3e, 0xd1, 0x9d, 0x6b, 0x3f, 0x6f, 0x75, 0x0c, 0xeb, 0xdc, 0xc0,
+ 0x49, 0x33, 0x4b, 0x07, 0xc2, 0x00, 0x79, 0x88, 0x17, 0x79, 0x0e, 0x62,
+ 0x84, 0x12, 0x51, 0xc5, 0x3b, 0xed, 0xe7, 0x8f, 0x6c, 0xbe, 0x90, 0x9c,
+ 0xe5, 0xc1, 0x1d, 0x0b, 0x46, 0xd1, 0xdb, 0xa8, 0x57, 0xd7, 0xa0, 0x18,
+ 0xd9, 0xb5, 0x7a, 0x09, 0xb9, 0xd7, 0xae, 0xca, 0xa1, 0xfb, 0xcd, 0x7a,
+ 0x16, 0xe5, 0x4a, 0x1e, 0x88, 0x79, 0x7e, 0xfd, 0x6e, 0xaf, 0x56, 0xe9,
+ 0x80, 0x61, 0xe8, 0xaa, 0x14, 0xde, 0x43, 0x35, 0xb7, 0x58, 0xf1, 0xe2,
+ 0x03, 0x02, 0xcb, 0x9b, 0x14, 0x99, 0xcc, 0xba, 0x93, 0x27, 0x96, 0xc3,
+ 0x41, 0xcd, 0x06, 0xed, 0x81, 0x95, 0x64, 0x37, 0xb3, 0xfe, 0xd5, 0x4c,
+ 0xc5, 0x6a, 0x06, 0x61, 0xcb, 0xfa, 0x83, 0x40, 0xb7, 0x0e, 0x16, 0xa7,
+ 0xd4, 0x7e, 0x5f, 0xba, 0x60, 0xef, 0xd4, 0x7f, 0xaa, 0xaa, 0x6b, 0x33,
+ 0xfa, 0x3b, 0x78, 0x8d, 0xc8, 0x28, 0xf4, 0x07, 0xf6, 0xa7, 0x76, 0xf7,
+ 0x75, 0x8f, 0x78, 0x13, 0xb0, 0x7e, 0xb5, 0xb7, 0x87, 0x8a, 0x01, 0x6a,
+ 0x62, 0x1e, 0x78, 0xd4, 0xf2, 0x00, 0x49, 0x8e, 0x5f, 0x81, 0x19, 0x1a,
+ 0x09, 0x00, 0x06, 0x4c, 0x79, 0xa6, 0xfb, 0x43, 0x01, 0x93, 0x6f, 0xfa,
+ 0x1e, 0x3f, 0x51, 0xe4, 0x74, 0x4e, 0xcd, 0x04, 0x35, 0x23, 0xad, 0xa0,
+ 0x77, 0x77, 0x0e, 0x4c, 0xef, 0xf3, 0xa9, 0x41, 0x63, 0x9b, 0x26, 0xcf,
+ 0xa1, 0x6b, 0x8e, 0x30, 0x49, 0x2d, 0xb4, 0x47, 0xfe, 0x64, 0x4b, 0xc5,
+ 0xd7, 0xa4, 0x47, 0x8a, 0x97, 0x36, 0x57, 0xaa, 0xc4, 0x76, 0x98, 0x7a,
+ 0x49, 0x65, 0x7c, 0x3f, 0x44, 0xb8, 0xf8, 0x9a, 0x3b, 0x44, 0xf8, 0x24,
+ 0x36, 0x9e, 0x34, 0x66, 0xbd, 0x68, 0xb4, 0x8f, 0x90, 0xf0, 0x6c, 0x8f,
+ 0x77, 0x1a, 0x44, 0x15, 0x0e, 0xf1, 0x27, 0x82, 0x8e, 0xc4, 0x41, 0xaf,
+ 0x6e, 0x63, 0x37, 0xce, 0x0e, 0x18, 0x90, 0xa2, 0x9f, 0xb3, 0x1c, 0x19,
+ 0xbf, 0x20, 0xf6, 0xd6, 0x9d, 0xa7, 0x84, 0xe5, 0x76, 0xd8, 0xda, 0xd7,
+ 0x91, 0xa2, 0xe2, 0xca, 0xae, 0xa3, 0xcd, 0xe8, 0x2e, 0xc8, 0x9f, 0x90,
+ 0x0f, 0xfb, 0x55, 0xc9, 0x01, 0xa8, 0x86, 0x27, 0x65, 0x22, 0x9b, 0x0a,
+ 0x17, 0x73, 0xd0, 0xd6, 0x62, 0xe6, 0x2c, 0xf5, 0x6c, 0xab, 0x81, 0xfe,
+ 0x5b, 0xa6, 0xaa, 0x28, 0x86, 0x9d, 0x05, 0xbd, 0x26, 0x4e, 0x8c, 0xfe,
+ 0x72, 0x98, 0x57, 0x49, 0x2c, 0x45, 0x14, 0xbb, 0x87, 0x92, 0x4b, 0xd9,
+ 0x62, 0x5a, 0xf5, 0x7f, 0x5f, 0x4f, 0xff, 0xf4, 0x58, 0x91, 0xfb, 0xe5,
+ 0xf6, 0x82, 0xab, 0xad, 0x2c, 0x16, 0x1f, 0x0e, 0x66, 0x1e, 0x97, 0x97,
+ 0x3c, 0x18, 0x42, 0x33, 0x11, 0xa9, 0x22, 0x99, 0x08, 0x01, 0x07, 0x55,
+ 0xf2, 0x38, 0xf2, 0xff, 0x1e, 0x6f, 0xcb, 0x69, 0xf6, 0xc8, 0x1c, 0xae,
+ 0xac, 0xf7, 0x09, 0x17, 0x7c, 0x5b, 0x4c, 0x17, 0xa1, 0x94, 0x44, 0xa9,
+ 0x99, 0xc8, 0x94, 0x63, 0x46, 0x8b, 0x4f, 0x7e, 0x28, 0xf5, 0x23, 0x3e,
+ 0x44, 0x7f, 0xa8, 0x05, 0x38, 0x5a, 0x25, 0xc2, 0x10, 0xbd, 0x7a, 0x92,
+ 0xf9, 0xbb, 0x31, 0xb6, 0xd1, 0x39, 0x52, 0xdd, 0x63, 0x65, 0xe1, 0x68,
+ 0x6b, 0x4f, 0x9f, 0x3f, 0xfb, 0xa3, 0xcb, 0x10, 0xc6, 0xc4, 0x76, 0x6a,
+ 0x93, 0x65, 0x10, 0x24, 0x8f, 0xb2, 0x64, 0x13, 0x3e, 0xe5, 0x66, 0x87,
+ 0xfa, 0x96, 0x1b, 0x6d, 0x19, 0xfb, 0x2d, 0x37, 0x4e, 0x5a, 0xc5, 0xfe,
+ 0xcf, 0xea, 0x04, 0xf8, 0xf4, 0xd4, 0x15, 0x33, 0xf1, 0x26, 0xb1, 0x51,
+ 0x62, 0x9b, 0xfa, 0xca, 0xfb, 0xed, 0x40, 0x57, 0xd9, 0x9a, 0x93, 0x6c,
+ 0xa7, 0xe5, 0x56, 0xea, 0x16, 0x5d, 0x7c, 0xb1, 0xdb, 0xb4, 0x2e, 0x8d,
+ 0xf2, 0xad, 0x55, 0x77, 0x0e, 0xf6, 0x66, 0xb8, 0x2e, 0x4a, 0x09, 0x03,
+ 0x76, 0x80, 0x71, 0xe0, 0x93, 0x05, 0x19, 0xa7, 0x68, 0xf2, 0xd3, 0xb1,
+ 0xd0, 0x18, 0x35, 0x8b, 0x6e, 0xec, 0x49, 0x77, 0xc2, 0x67, 0x53, 0xf2,
+ 0x1b, 0xf7, 0x63, 0xca, 0x7d, 0xc7, 0xce, 0xa7, 0x70, 0xae, 0x11, 0x44,
+ 0xec, 0x71, 0x7e, 0x16, 0x0c, 0x4e, 0x9c, 0x23, 0xec, 0xad, 0xe7, 0x3d,
+ 0xe4, 0x14, 0xb2, 0xec, 0x1f, 0x85, 0xcf, 0x48, 0xde, 0x63, 0x71, 0xf3,
+ 0xbc, 0x68, 0xe4, 0x47, 0xaf, 0x10, 0x3e, 0x8d, 0x68, 0x36, 0x62, 0x10,
+ 0xc1, 0x23, 0x3d, 0x1d, 0x82, 0x1e, 0x2d, 0x57, 0xb6, 0x5e, 0xae, 0x96,
+ 0x37, 0xc5, 0x46, 0x0e, 0x78, 0x43, 0x4f, 0xbf, 0xd0, 0x6b, 0x2e, 0x85,
+ 0xc3, 0xf3, 0x06, 0x29, 0xc3, 0xc3, 0x45, 0x88, 0x0b, 0xeb, 0x4b, 0xb6,
+ 0xaf, 0x49, 0x6a, 0x57, 0xa5, 0xa4, 0xf1, 0x94, 0xf6, 0xfa, 0x18, 0x07,
+ 0xf9, 0xb3, 0x8c, 0x4a, 0x5c, 0x4f, 0xd5, 0x35, 0xba, 0x39, 0xb9, 0x91,
+ 0x81, 0x85, 0x48, 0x6f, 0xaa, 0x0f, 0x5d, 0x66, 0xf4, 0x21, 0x01, 0xcc,
+ 0x2a, 0x55, 0x33, 0xa4, 0xe5, 0x96, 0xd0, 0x07, 0x61, 0xc9, 0x68, 0xc2,
+ 0xb4, 0x59, 0x7a, 0x1f, 0xe1, 0x52, 0x8c, 0xe4, 0x1c, 0x2e, 0x22, 0xc7,
+ 0x22, 0xc1, 0x1d, 0x41, 0x96, 0xb3, 0xbe, 0x4b, 0xcd, 0x56, 0xb6, 0x7b,
+ 0x4c, 0x52, 0xdc, 0xc5, 0x0e, 0xe0, 0x1e, 0x1c, 0x78, 0x04, 0xd5, 0x1f,
+ 0x8e, 0xdf, 0xa6, 0x28, 0xa5, 0xee, 0x16, 0xcb, 0x60, 0x96, 0x4a, 0xf1,
+ 0x73, 0x20, 0xb6, 0x2d, 0x4a, 0x37, 0x3e, 0x4b, 0x7a, 0x3e, 0x20, 0x8c,
+ 0x21, 0xd9, 0x46, 0x1e, 0xd0, 0xd2, 0x2c, 0xba, 0x65, 0x88, 0x80, 0xda,
+ 0x72, 0x6b, 0x91, 0x40, 0xfb, 0x89, 0x3f, 0xa4, 0xc8, 0xec, 0xdc, 0x9d,
+ 0x54, 0x0c, 0x33, 0x57, 0x84, 0xc4, 0x33, 0xbe, 0x2a, 0x56, 0x11, 0x57,
+ 0x39, 0xd3, 0xef, 0x39, 0x44, 0x0b, 0xd9, 0x81, 0xe4, 0xd2, 0xca, 0xf2,
+ 0x9b, 0x5d, 0x7d, 0x33, 0x04, 0x0b, 0xb6, 0xf0, 0xc6, 0x73, 0x6a, 0xa1,
+ 0xc1, 0x2f, 0xc3, 0x94, 0x37, 0x87, 0xa8, 0x88, 0x45, 0x63, 0x13, 0x3c,
+ 0x1b, 0xdf, 0xc0, 0xd3, 0x21, 0x09, 0x13, 0xd3, 0xe7, 0x8a, 0xef, 0xaa,
+ 0x64, 0xc8, 0xcd, 0xf9, 0x94, 0x6d, 0x23, 0x98, 0x1a, 0x51, 0x6a, 0x62,
+ 0x79, 0xce, 0x1e, 0xdc, 0xec, 0x00, 0xa0, 0x66, 0x9c, 0x6b, 0x83, 0x19,
+ 0x40, 0xf5, 0xba, 0x5e, 0xb4, 0x5b, 0x3e, 0xaa, 0x35, 0x9d, 0xb6, 0xa4,
+ 0xe5, 0x49, 0x07, 0x69, 0xf7, 0x0f, 0xaa, 0x4c, 0xd2, 0x09, 0x4e, 0xeb,
+ 0x87, 0x33, 0xae, 0x07, 0x0b, 0xd4, 0xe0, 0xee, 0xdb, 0x72, 0x71, 0x07,
+ 0x6b, 0x16, 0x7d, 0x47, 0x1e, 0x92, 0x0e, 0xc9, 0xf8, 0xd0, 0xbb, 0x31,
+ 0x58, 0x26, 0x9b, 0x1c, 0xe5, 0x73, 0x95, 0xa7, 0xc2, 0xb4, 0x3b, 0x73,
+ 0x7a, 0x77, 0xb2, 0xda, 0x55, 0x96, 0x4d, 0xf4, 0xb9, 0xf2, 0x86, 0xd0,
+ 0x84, 0xab, 0x3c, 0x4f, 0x00, 0xd7, 0x9c, 0x61, 0x93, 0xc7, 0xa6, 0x09,
+ 0x3b, 0xfe, 0x9b, 0xaf, 0xb7, 0xd1, 0x63, 0xca, 0x15, 0x83, 0xde, 0xc4,
+ 0xc6, 0x97, 0x09, 0xa0, 0x52, 0x1b, 0xfa, 0x94, 0x7e, 0xed, 0xfc, 0x27,
+ 0xd1, 0x51, 0xf7, 0x38, 0x16, 0x7a, 0x78, 0xa0, 0x67, 0xba, 0x6c, 0xda,
+ 0x7b, 0x0f, 0x75, 0xfb, 0xe5, 0x13, 0xf8, 0xfb, 0x9a, 0x0f, 0x14, 0x80,
+ 0xb5, 0x06, 0x6d, 0x57, 0x37, 0xff, 0x8d, 0x4a, 0xca, 0xd2, 0x9f, 0x5c,
+ 0x61, 0x6a, 0xbe, 0x89, 0x15, 0xf7, 0x07, 0x95, 0x49, 0x71, 0xb9, 0x33,
+ 0x8a, 0xa2, 0xc3, 0xd2, 0x34, 0xa5, 0x67, 0x74, 0xc8, 0x8e, 0x19, 0xee,
+ 0xa2, 0x03, 0x0d, 0x4b, 0xbf, 0x88, 0xc4, 0x9c, 0xae, 0x70, 0x1b, 0xc9,
+ 0x84, 0x0a, 0x1d, 0x31, 0x68, 0x99, 0x98, 0x53, 0xe4, 0x78, 0x95, 0x8c,
+ 0xf7, 0xa8, 0x88, 0xf6, 0x75, 0xcf, 0xbf, 0x7c, 0x09, 0x57, 0x8a, 0x9b,
+ 0x64, 0x68, 0xb0, 0xeb, 0x95, 0x9b, 0xf3, 0x34, 0xae, 0x6a, 0x56, 0xe1,
+ 0x8a, 0x8a, 0x1b, 0x97, 0x4d, 0xe4, 0x38, 0x88, 0xe4, 0x36, 0x28, 0x59,
+ 0x1d, 0x72, 0xb3, 0xbf, 0xb2, 0x8b, 0x23, 0xe9, 0x4d, 0x26, 0xc9, 0xdf,
+ 0x4a, 0x44, 0xc9, 0xfe, 0xd6, 0xe5, 0xba, 0xda, 0x7f, 0xf9, 0x96, 0xfb,
+ 0xf0, 0xcf, 0xae, 0xee, 0x6e, 0x60, 0x0f, 0xec, 0xf4, 0xd2, 0x49, 0x49,
+ 0x65, 0x9f, 0xb9, 0x3d, 0x52, 0xee, 0x23, 0xb2, 0x88, 0x7c, 0xd0, 0x7f,
+ 0xb8, 0xee, 0x81, 0xe2, 0x24, 0x75, 0x8e, 0xcc, 0x39, 0x3c, 0xc7, 0x87,
+ 0x97, 0xa6, 0xc7, 0xb8, 0x6f, 0xff, 0x60, 0x73, 0x62, 0x27, 0x84, 0xf1,
+ 0x8d, 0xf6, 0xb2, 0xaf, 0x3b, 0x24, 0xce, 0xa7, 0x3a, 0x8f, 0xfb, 0x27,
+ 0x29, 0x64, 0x31, 0x81, 0x6e, 0x8b, 0x47, 0x08, 0x46, 0x6f, 0x96, 0x81,
+ 0x27, 0xe4, 0xf1, 0xef, 0x0a, 0x07, 0xab, 0xb9, 0xda, 0xd6, 0x26, 0xdd,
+ 0x73, 0x9c, 0xe4, 0x27, 0xcf, 0xd8, 0xfb, 0xb1, 0x13, 0x59, 0x28, 0x9f,
+ 0xbb, 0x44, 0x04, 0x97, 0x7a, 0x13, 0x02, 0xde, 0x18, 0x7d, 0x19, 0x38,
+ 0x15, 0x3a, 0xd8, 0x46, 0x9b, 0x57, 0x8f, 0x85, 0x2f, 0x69, 0xea, 0xd4,
+ 0x22, 0xf5, 0x3d, 0x57, 0xcb, 0x4f, 0xa9, 0x33, 0xa5, 0xae, 0x95, 0xda,
+ 0x2a, 0xa1, 0x55, 0xbc, 0x53, 0xcc, 0xc1, 0xce, 0x86, 0x12, 0x1a, 0x96,
+ 0x7b, 0x73, 0xfa, 0x97, 0xcf, 0xc5, 0x1a, 0x22, 0x50, 0xf1, 0x89, 0x42,
+ 0x23, 0x1b, 0xeb, 0x76, 0xc1, 0xf2, 0x4c, 0x45, 0xd8, 0xba, 0x6c, 0x8a,
+ 0x76, 0xc9, 0xe6, 0xae, 0xb6, 0xaa, 0x71, 0x79, 0x5c, 0x95, 0x6a, 0x8f,
+ 0x20, 0xfa, 0x96, 0xde, 0xf2, 0x1e, 0xc0, 0xef, 0x8d, 0xdb, 0xb0, 0xef,
+ 0xd0, 0xb2, 0x40, 0x71, 0x10, 0x67, 0x7a, 0xbd, 0xc5, 0x98, 0x56, 0x02,
+ 0x75, 0xf4, 0x03, 0x2e, 0xd2, 0x39, 0x26, 0x38, 0x85, 0x1e, 0x85, 0x10,
+ 0x46, 0xf4, 0x82, 0x1d, 0x0e, 0x74, 0xe3, 0x2a, 0x2f, 0x24, 0x36, 0x0a,
+ 0x29, 0xfb, 0x7a, 0x19, 0xfa, 0x2d, 0x93, 0xdc, 0xe3, 0x1f, 0x2a, 0x0c,
+ 0x2e, 0x2c, 0xbf, 0xb5, 0x6d, 0xc9, 0x94, 0x58, 0x5c, 0x02, 0xbc, 0x83,
+ 0xc4, 0xc5, 0xfe, 0x70, 0x6f, 0xa6, 0x0c, 0x47, 0x81, 0xfc, 0x5a, 0xbf,
+ 0x6c, 0x58, 0xca, 0xb4, 0xf6, 0x2e, 0x95, 0x63, 0x90, 0xf2, 0x15, 0x40,
+ 0x2e, 0x74, 0x3e, 0x4a, 0xd2, 0xd0, 0x3f, 0x8e, 0x87, 0x83, 0xac, 0x1f,
+ 0x22, 0xa0, 0x53, 0x66, 0x74, 0x4f, 0x57, 0x51, 0x3c, 0xba, 0x4a, 0x01,
+ 0xdf, 0xd9, 0xcc, 0x87, 0x70, 0xc2, 0xa3, 0x49, 0x2b, 0x9c, 0x9c, 0xa0,
+ 0xbf, 0x96, 0xb0, 0xc5, 0x52, 0xce, 0x58, 0x11, 0xa0, 0x6b, 0xf5, 0x3e,
+ 0x9a, 0x56, 0x6f, 0x48, 0x48, 0xf8, 0x28, 0x9e, 0x83, 0x92, 0xd9, 0xad,
+ 0x52, 0xbb, 0x20, 0x37, 0x12, 0x03, 0xf2, 0xac, 0xf9, 0xbf, 0x3a, 0x10,
+ 0xea, 0x42, 0xe0, 0x4b, 0x87, 0xb7, 0x41, 0xd5, 0xcb, 0x35, 0xf5, 0x06,
+ 0x49, 0xfa, 0x33, 0xf6, 0xae, 0x75, 0x04, 0x12, 0xde, 0xc0, 0x39, 0x69,
+ 0x6d, 0x11, 0xcb, 0x2c, 0x72, 0xd4, 0x5c, 0xde, 0x53, 0x44, 0xba, 0x57,
+ 0x7b, 0x84, 0x9c, 0xad, 0x77, 0x53, 0x35, 0x8f, 0x2c, 0x40, 0x34, 0x7d,
+ 0xfd, 0x8f, 0x84, 0x8e, 0xcf, 0x64, 0xd4, 0x15, 0x2c, 0x67, 0xb6, 0x8f,
+ 0xdf, 0x5e, 0xdc, 0x73, 0x7d, 0xef, 0x3f, 0xb3, 0x04, 0xa5, 0x37, 0x8c,
+ 0x71, 0x59, 0x39, 0xaf, 0x55, 0x10, 0x37, 0xcb, 0xae, 0x60, 0x3d, 0xb5,
+ 0x41, 0x45, 0x31, 0xb0, 0xcc, 0x56, 0x30, 0x68, 0x59, 0x68, 0xf1, 0x9f,
+ 0x04, 0xe4, 0xbc, 0xfe, 0x51, 0xfd, 0x49, 0x7c, 0x6a, 0xb1, 0xca, 0xb2,
+ 0x10, 0x72, 0xda, 0xdf, 0x8c, 0xb4, 0x44, 0x3d, 0x27, 0x66, 0x47, 0x31,
+ 0x26, 0x6d, 0x3e, 0x29, 0x39, 0x4e, 0x74, 0x9e, 0x4a, 0x7e, 0x5a, 0xfe,
+ 0x9c, 0xa9, 0x4a, 0x33, 0x89, 0x12, 0x24, 0x40, 0xec, 0xec, 0x20, 0x37,
+ 0xb8, 0xc7, 0xc2, 0xb4, 0xef, 0xfd, 0xf0, 0xbb, 0x13, 0x74, 0xad, 0x85,
+ 0xa9, 0xb9, 0x39, 0xe9, 0x05, 0x73, 0x98, 0xc3, 0xee, 0x18, 0x87, 0xaa,
+ 0x12, 0xf7, 0x65, 0x2f, 0xee, 0xaa, 0x3f, 0x38, 0xdb, 0xa6, 0x2e, 0xf6,
+ 0xbc, 0x33, 0x30, 0xfd, 0x6b, 0x02, 0xb2, 0xab, 0x6b, 0x27, 0x8c, 0x45,
+ 0x6c, 0x01, 0xf1, 0x9d, 0x03, 0xf4, 0x6d, 0x44, 0x75, 0x59, 0xf7, 0xda,
+ 0x59, 0x50, 0x69, 0xef, 0xe7, 0x90, 0xfe, 0xb1, 0x37, 0x6e, 0x0d, 0x73,
+ 0x3e, 0xb1, 0x93, 0x8e, 0x40, 0x45, 0x61, 0xe3, 0x73, 0xc7, 0xdd, 0x51,
+ 0x69, 0x31, 0xb2, 0x07, 0xdd, 0xa5, 0x17, 0x85, 0x85, 0xae, 0x33, 0xa9,
+ 0x54, 0xb3, 0xb8, 0xb6, 0xc3, 0xcd, 0x50, 0x4a, 0x35, 0x7c, 0xfb, 0x66,
+ 0xf7, 0x1a, 0x52, 0x1a, 0xdd, 0xd6, 0x92, 0x6d, 0xb0, 0xb4, 0x68, 0xd0,
+ 0x3c, 0xe9, 0x63, 0xd4, 0x1c, 0xfd, 0xfb, 0xa1, 0xc0, 0xb6, 0x31, 0x61,
+ 0xd4, 0xc1, 0x95, 0xd1, 0x0a, 0xef, 0x21, 0x84, 0x41, 0xce, 0xe3, 0xc7,
+ 0xce, 0xe8, 0x6e, 0x4e, 0x55, 0xb8, 0x96, 0x9a, 0x84, 0x6b, 0x22, 0x00,
+ 0xfc, 0xb6, 0xf8, 0x9e, 0x22, 0xeb, 0xae, 0xbc, 0x14, 0x5d, 0x18, 0x71,
+ 0x8f, 0x44, 0x03, 0x87, 0xe1, 0xf5, 0xd5, 0x84, 0x14, 0xc1, 0x73, 0xc0,
+ 0xa1, 0x22, 0x08, 0x4c, 0x49, 0xfe, 0x3f, 0xda, 0x23, 0xa0, 0x47, 0x2a,
+ 0x2c, 0xeb, 0xda, 0xe8, 0x18, 0xc0, 0x6e, 0x9e, 0xc2, 0x56, 0xcc, 0xbd,
+ 0x6a, 0x4b, 0x5a, 0xd2, 0xf2, 0xcb, 0x0c, 0xf8, 0xa0, 0xec, 0x82, 0xdf,
+ 0x41, 0x5c, 0x71, 0xc3, 0x6a, 0xfc, 0xc4, 0x3a, 0x7b, 0x97, 0x1e, 0x07,
+ 0x99, 0xe9, 0xa6, 0xeb, 0x67, 0x10, 0xcb, 0xd1, 0xed, 0xaa, 0x38, 0x77,
+ 0x8e, 0xd8, 0xfe, 0xd3, 0xb6, 0x31, 0x0c, 0xc3, 0xcf, 0x6f, 0xd5, 0xb5,
+ 0xd0, 0x5a, 0xab, 0xd9, 0x5a, 0x38, 0x28, 0x64, 0xba, 0x63, 0x34, 0xeb,
+ 0x19, 0x51, 0x93, 0x91, 0x21, 0x4f, 0x98, 0x7d, 0x3a, 0xd7, 0x1b, 0x2a,
+ 0x16, 0x15, 0xd9, 0xd4, 0x63, 0xa6, 0x71, 0x43, 0x74, 0x84, 0xd7, 0xbc,
+ 0x77, 0x39, 0x7b, 0xbb, 0x22, 0x4e, 0xc6, 0xe6, 0x4b, 0x51, 0x8f, 0x3d,
+ 0x15, 0x28, 0x6d, 0x18, 0x57, 0x26, 0x01, 0x84, 0x1a, 0xb5, 0xbc, 0xdc,
+ 0x7a, 0x22, 0xb2, 0xb7, 0xf7, 0xd9, 0xbf, 0x9f, 0x86, 0x2f, 0x44, 0x4c,
+ 0xb1, 0xd1, 0x87, 0x61, 0x3a, 0xcd, 0xd7, 0x08, 0x12, 0xed, 0x45, 0x76,
+ 0xd3, 0x64, 0x73, 0xf0, 0x87, 0xcc, 0xff, 0x10, 0x8e, 0x2b, 0x57, 0x45,
+ 0x9f, 0xcd, 0x69, 0x86, 0x2a, 0xe6, 0x92, 0xfe, 0x37, 0x86, 0x92, 0x32,
+ 0xc3, 0xde, 0xca, 0xa6, 0x9d, 0xf1, 0x3f, 0xd2, 0x44, 0xc5, 0xc2, 0x60,
+ 0xa8, 0x9a, 0x74, 0xb7, 0xb9, 0x20, 0x6b, 0x0c, 0xe3, 0xde, 0x40, 0xb8,
+ 0xda, 0xc9, 0x6b, 0x1f, 0x5b, 0x2e, 0xb6, 0x5d, 0xfc, 0x03, 0x36, 0x0b,
+ 0x9d, 0xcb, 0xdf, 0x11, 0x8f, 0x62, 0x18, 0xf6, 0x12, 0xb4, 0x7c, 0x7c,
+ 0xf2, 0x2f, 0x06, 0xe6, 0x47, 0x32, 0xf0, 0xce, 0xcd, 0xa7, 0x19, 0xfb,
+ 0x24, 0x32, 0xf4, 0xf0, 0xbf, 0xd5, 0xae, 0x1a, 0x0a, 0x55, 0x30, 0x2d,
+ 0x8c, 0xce, 0xdc, 0xa4, 0xf4, 0x9d, 0xce, 0x33, 0x24, 0x77, 0x82, 0x25,
+ 0x2f, 0xaf, 0x01, 0x32, 0x67, 0x20, 0x3c, 0x4e, 0xc9, 0x6d, 0xeb, 0x94,
+ 0x57, 0xbe, 0x59, 0x29, 0x10, 0x8a, 0x3f, 0x72, 0x2a, 0x8a, 0xa4, 0x5d,
+ 0x56, 0x3e, 0x9c, 0xca, 0x4e, 0xc9, 0xc7, 0xf6, 0xb4, 0x6b, 0xa1, 0xcd,
+ 0x72, 0xa1, 0x91, 0xa9, 0x2f, 0x32, 0xfb, 0xd2, 0x81, 0x2f, 0x2b, 0x27,
+ 0xb8, 0x54, 0x1d, 0x02, 0xef, 0xdb, 0x1e, 0xf1, 0x6a, 0xc3, 0x8c, 0x54,
+ 0x8b, 0x31, 0xec, 0x3c, 0x65, 0x34, 0xa4, 0x72, 0x15, 0x32, 0x35, 0xef,
+ 0x69, 0x4f, 0x87, 0xed, 0x78, 0x38, 0xc9, 0x53, 0x9b, 0x27, 0x42, 0x4e,
+ 0xcb, 0xe4, 0x12, 0x1b, 0x65, 0xe3, 0x7a, 0xcd, 0x3d, 0xba, 0xd2, 0xb2,
+ 0x48, 0xc0, 0x13, 0xdf, 0xa0, 0x02, 0x2e, 0xf2, 0x1a, 0x88, 0x69, 0xef,
+ 0x42, 0xcd, 0x84, 0x02, 0x50, 0x11, 0x3a, 0xef, 0x0f, 0x88, 0x91, 0x71,
+ 0x33, 0xcb, 0xa8, 0xaa, 0x71, 0x72, 0x40, 0xb8, 0xa5, 0x1e, 0xc9, 0xb4,
+ 0x1c, 0x58, 0xef, 0xbe, 0xe4, 0xda, 0x5e, 0xc2, 0xff, 0x5f, 0x36, 0xf9,
+ 0x7a, 0x68, 0x3d, 0xd3, 0xa8, 0x8f, 0x4e, 0x02, 0x2b, 0xf9, 0x8b, 0xf8,
+ 0x60, 0x34, 0x1c, 0x50, 0xfc, 0x6a, 0x41, 0x87, 0xe3, 0xe4, 0xb0, 0x50,
+ 0x19, 0x2e, 0x1c, 0x95, 0xb0, 0xa9, 0x11, 0x95, 0x69, 0x51, 0xe9, 0x6b,
+ 0x29, 0xd3, 0xd3, 0xca, 0x72, 0x33, 0x86, 0x95, 0xe1, 0x4f, 0x0d, 0xb6,
+ 0x9f, 0xa9, 0x4b, 0xaf, 0x1b, 0xa2, 0x15, 0x6f, 0xc0, 0x19, 0xbb, 0xeb,
+ 0x06, 0x25, 0x44, 0x1c, 0x0a, 0x0a, 0x1f, 0x4a, 0x5a, 0xa2, 0xbd, 0xec,
+ 0x46, 0x0c, 0x39, 0xfd, 0xb9, 0x8d, 0x6c, 0xcc, 0x1c, 0x54, 0x88, 0xf7,
+ 0xb4, 0x56, 0x90, 0xff, 0x32, 0xcd, 0x64, 0xee, 0xd2, 0x30, 0x5c, 0xab,
+ 0x0b, 0xa2, 0x3e, 0x0e, 0xd7, 0x09, 0x2e, 0x33, 0x5e, 0x2c, 0x89, 0xae,
+ 0x28, 0x24, 0x96, 0xd3, 0x8c, 0x82, 0x93, 0xfb, 0x19, 0x1d, 0xcf, 0x5b,
+ 0x33, 0xe5, 0x1f, 0x82, 0xe6, 0x26, 0x6d, 0xa8, 0xa1, 0xdf, 0x8d, 0x94,
+ 0x53, 0x01, 0x33, 0x63, 0x74, 0x28, 0xe7, 0xfb, 0xf7, 0x43, 0xf1, 0xee,
+ 0xf0, 0xbd, 0x08, 0x58, 0x9c, 0xdd, 0x5e, 0xaa, 0x38, 0x6c, 0x40, 0x41,
+ 0x32, 0xa7, 0x6b, 0xe1, 0x25, 0x8f, 0x01, 0x6e, 0xb0, 0xdb, 0x0e, 0x51,
+ 0x7f, 0x6d, 0x22, 0x79, 0x74, 0xc1, 0x34, 0x0b, 0x98, 0x06, 0xe7, 0xcf,
+ 0xff, 0x87, 0x27, 0x9a, 0x46, 0x48, 0xa4, 0xae, 0xd3, 0xc9, 0xe1, 0x5a,
+ 0xbd, 0xe8, 0xe9, 0xa2, 0x31, 0xe3, 0xb2, 0x58, 0x67, 0xf0, 0xc0, 0xbb,
+ 0xd8, 0xa7, 0x34, 0xa1, 0x71, 0x39, 0x24, 0x05, 0x54, 0xec, 0xc6, 0x59,
+ 0x5d, 0xf2, 0x8d, 0xd7, 0x74, 0x7c, 0x31, 0x23, 0x60, 0x75, 0xb7, 0xdb,
+ 0xf0, 0x0b, 0x03, 0x01, 0xe8, 0xf2, 0x97, 0xbf, 0x6e, 0xff, 0xa5, 0x9b,
+ 0x4a, 0xe3, 0x84, 0x49, 0x7f, 0x08, 0x95, 0xc2, 0x1f, 0x8c, 0xd6, 0x81,
+ 0xda, 0x69, 0x71, 0x48, 0x80, 0x87, 0x25, 0xdd, 0x89, 0x5e, 0x1c, 0x29,
+ 0xfe, 0x9a, 0xfe, 0x34, 0x32, 0x9c, 0x74, 0x27, 0x57, 0x53, 0xc0, 0x5c,
+ 0x12, 0xe7, 0x4d, 0xc7, 0xea, 0xb4, 0x6b, 0x48, 0xf7, 0xf2, 0x3b, 0x59,
+ 0xaa, 0x1f, 0x9b, 0x5f, 0xf9, 0xf7, 0xb8, 0x2b, 0x7f, 0x4f, 0x71, 0xba,
+ 0x18, 0xc1, 0x73, 0x37, 0xe4, 0xa8, 0x14, 0xe8, 0x69, 0xc6, 0x07, 0x80,
+ 0x93, 0x68, 0xec, 0x22, 0x69, 0xd8, 0xd3, 0x17, 0xd0, 0x66, 0x15, 0x80,
+ 0x81, 0xeb, 0x30, 0xde, 0xc6, 0xb4, 0x6b, 0xdd, 0x8c, 0xce, 0xbe, 0xcb,
+ 0x1a, 0x75, 0x83, 0x31, 0x20, 0xcf, 0x8d, 0x9f, 0x15, 0x25, 0xc8, 0x20,
+ 0x7b, 0x40, 0x3f, 0x12, 0x82, 0x75, 0xff, 0x76, 0x1a, 0x36, 0xfe, 0x66,
+ 0x7b, 0xae, 0x6f, 0xb0, 0x5c, 0x69, 0x2e, 0x1a, 0x55, 0xdc, 0x2a, 0x17,
+ 0x7c, 0xf6, 0x27, 0xf4, 0x76, 0xd6, 0xe5, 0x2f, 0x0d, 0x19, 0x23, 0x18,
+ 0x57, 0xc3, 0x96, 0x8f, 0x7c, 0xcf, 0xf6, 0x89, 0xdc, 0xb7, 0xa7, 0xa3,
+ 0x66, 0x2a, 0xc0, 0xf6, 0x07, 0x00, 0xcb, 0x13, 0x71, 0xd0, 0xbc, 0xd0,
+ 0x84, 0x94, 0x24, 0x51, 0xf5, 0x68, 0xeb, 0x8f, 0xeb, 0xc4, 0xb8, 0xa1,
+ 0xca, 0x4e, 0x3a, 0x85, 0xf2, 0xd9, 0x32, 0x22, 0xc5, 0xc4, 0x62, 0x1a,
+ 0xe0, 0xb6, 0x58, 0x7a, 0xd2, 0xb1, 0xd7, 0x44, 0x60, 0x8f, 0xc8, 0x9b,
+ 0xde, 0xfd, 0xa2, 0x29, 0x18, 0xab, 0x58, 0xf5, 0xf1, 0x35, 0xca, 0xe5,
+ 0x28, 0x46, 0xbc, 0x0d, 0x97, 0xf5, 0x53, 0xb4, 0x4d, 0xd1, 0x5d, 0xaf,
+ 0xf7, 0xa7, 0xaa, 0xd9, 0x26, 0xa8, 0x7a, 0x7f, 0x73, 0x8d, 0xf2, 0x6a,
+ 0x8a, 0x51, 0x8a, 0x31, 0x04, 0x6f, 0xd8, 0x79, 0x32, 0x5d, 0x52, 0xa8,
+ 0x9c, 0x6f, 0xdd, 0x70, 0x99, 0x0a, 0x8e, 0x76, 0xc7, 0x1f, 0x76, 0xeb,
+ 0x67, 0x65, 0xf7, 0x79, 0xfc, 0x58, 0xf4, 0xcf, 0x12, 0xd7, 0x19, 0x4d,
+ 0xb4, 0x23, 0xa9, 0x1f, 0xf1, 0x60, 0x70, 0xd0, 0x9a, 0x2d, 0xa5, 0xe4,
+ 0xde, 0x70, 0x0a, 0x72, 0xf7, 0x66, 0x31, 0x62, 0x2e, 0x85, 0x72, 0x6c,
+ 0x83, 0xce, 0x71, 0xb8, 0x68, 0xfd, 0x40, 0x76, 0xf9, 0x1d, 0xc9, 0x12,
+ 0x93, 0x51, 0xa4, 0xe1, 0xbc, 0x9c, 0xb5, 0x4d, 0x5e, 0x0e, 0x6b, 0xd5,
+ 0x33, 0x97, 0xb7, 0xa2, 0xe4, 0xd7, 0x92, 0x3c, 0x43, 0xe5, 0x0e, 0x7f,
+ 0x86, 0x75, 0x82, 0x29, 0x58, 0x4b, 0x28, 0xf2, 0x59, 0x56, 0x5e, 0x68,
+ 0x80, 0x2e, 0x7b, 0xb0, 0xd3, 0x66, 0x08, 0x3e, 0x79, 0x67, 0xf6, 0x84,
+ 0x6f, 0x9d, 0x97, 0x69, 0x14, 0x32, 0x3c, 0x15, 0x6e, 0xd5, 0x09, 0x2c,
+ 0x36, 0x46, 0xa1, 0x56, 0x65, 0x62, 0x25, 0xe4, 0x2c, 0xd7, 0x1d, 0x14,
+ 0xe3, 0x02, 0x17, 0xad, 0xcd, 0x5b, 0x84, 0xf7, 0xc6, 0xf9, 0xd3, 0xe6,
+ 0x8e, 0xbe, 0x05, 0xc5, 0x3e, 0x8b, 0xed, 0x5f, 0x05, 0x2a, 0x92, 0x71,
+ 0xe6, 0x0e, 0x1e, 0x92, 0x9a, 0xc5, 0x1c, 0x49, 0x15, 0x7f, 0x6f, 0xf4,
+ 0x40, 0xc9, 0x53, 0x58, 0xe8, 0x11, 0xd5, 0x8c, 0x14, 0xd8, 0xf0, 0x29,
+ 0x04, 0x0f, 0xbc, 0xfe, 0x41, 0x98, 0xc7, 0xf2, 0xb1, 0xf0, 0x2f, 0x9a,
+ 0x86, 0x82, 0xe9, 0x1b, 0x33, 0xbb, 0x91, 0x38, 0x5e, 0xf0, 0xfc, 0xba,
+ 0xca, 0x73, 0x9f, 0xcd, 0xfa, 0xf1, 0xfa, 0xe2, 0x5b, 0x8e, 0xe3, 0xea,
+ 0x53, 0x61, 0x3e, 0x06, 0x33, 0x0b, 0x85, 0x5e, 0x7e, 0xe1, 0xb2, 0x8a,
+ 0x3c, 0x1c, 0x5f, 0x00, 0x6c, 0xe9, 0x15, 0x82, 0xce, 0x27, 0x4d, 0xdf,
+ 0x3a, 0x09, 0xd3, 0x0a, 0xb2, 0x7d, 0xab, 0xf0, 0x1e, 0x99, 0xdf, 0xf6,
+ 0x82, 0x96, 0x03, 0x65, 0x38, 0x2e, 0x7b, 0xe2, 0x32, 0x92, 0x32, 0x0f,
+ 0xa7, 0x05, 0xef, 0xcc, 0x8d, 0x01, 0xcf, 0x1e, 0x1f, 0x1d, 0xa8, 0x72,
+ 0x25, 0x2c, 0x8b, 0x92, 0xaf, 0x56, 0xd0, 0xa8, 0x2f, 0x52, 0x47, 0xe0,
+ 0xf0, 0xc8, 0x41, 0xcb, 0x3b, 0xed, 0xc3, 0xb1, 0x7d, 0xc5, 0x0d, 0xe6,
+ 0x49, 0x59, 0xce, 0x01, 0xab, 0x69, 0xff, 0xcd, 0x4c, 0x7f, 0x6c, 0xf4,
+ 0xc6, 0x0d, 0x57, 0xfe, 0x28, 0xcc, 0x73, 0x10, 0x8f, 0x2d, 0xed, 0x3b,
+ 0x33, 0x5f, 0xee, 0xe0, 0x31, 0x7c, 0xd7, 0x80, 0xc1, 0x10, 0x23, 0xba,
+ 0xe2, 0xec, 0x50, 0x1f, 0xf1, 0x49, 0x40, 0xfe, 0x06, 0xd1, 0x84, 0x30,
+ 0xf5, 0xd4, 0xdc, 0x87, 0xf1, 0xee, 0xf7, 0xfd, 0xd3, 0xfe, 0x06, 0x6f,
+ 0xf5, 0xce, 0x96, 0xaf, 0xb1, 0x6a, 0xbc, 0xd2, 0xc1, 0xcd, 0x60, 0x19,
+ 0x15, 0x3d, 0x81, 0xba, 0x06, 0x84, 0x48, 0x21, 0x4f, 0xf6, 0xdd, 0x35,
+ 0xcd, 0xf7, 0x2e, 0x3d, 0x04, 0x7f, 0xd3, 0x84, 0xc9, 0x61, 0x21, 0x2f,
+ 0xe9, 0xd7, 0x6e, 0xae, 0x43, 0x35, 0x38, 0x46, 0x14, 0xcd, 0xe7, 0x86,
+ 0x8a, 0x0f, 0xc9, 0x9f, 0xdc, 0x6a, 0x18, 0x66, 0x37, 0x2d, 0x54, 0x3f,
+ 0xbf, 0x1f, 0x24, 0xd7, 0xd9, 0x0a, 0x5c, 0x8f, 0x05, 0x3e, 0xec, 0x81,
+ 0x07, 0xf1, 0xc3, 0xdb, 0xff, 0x7f, 0x52, 0xec, 0x62, 0xaa, 0xad, 0x4c,
+ 0x8e, 0x65, 0xc6, 0xc4, 0x0e, 0xdd, 0x9e, 0x56, 0x1a, 0xa6, 0x66, 0xad,
+ 0x7a, 0x43, 0xe4, 0xfb, 0x39, 0x09, 0x7b, 0x7f, 0x32, 0x6a, 0xbb, 0xdd,
+ 0x6c, 0x43, 0x48, 0x7e, 0x95, 0x51, 0x82, 0xdc, 0x84, 0x62, 0x50, 0x0b,
+ 0x7c, 0x3d, 0xfa, 0x3c, 0x35, 0x97, 0x01, 0xd7, 0x9b, 0xc4, 0xe2, 0x20,
+ 0x40, 0x2f, 0xb7, 0x03, 0x08, 0xda, 0x88, 0x29, 0x10, 0xc9, 0xb7, 0x64,
+ 0xba, 0x5d, 0xd6, 0x9e, 0xa5, 0x18, 0xd6, 0x8e, 0xf6, 0x97, 0x70, 0x1d,
+ 0xdb, 0x32, 0x29, 0x36, 0x79, 0xc2, 0x37, 0xc2, 0xdc, 0x1c, 0x4a, 0x04,
+ 0xba, 0x9f, 0x8d, 0x74, 0xfd, 0x34, 0xd0, 0xd5, 0xf4, 0xee, 0xc7, 0xb2,
+ 0xda, 0x82, 0x7a, 0x6e, 0x4e, 0x36, 0xb1, 0xc4, 0x87, 0xab, 0x59, 0x3d,
+ 0x97, 0x49, 0x3b, 0xa6, 0x98, 0x3b, 0xbd, 0x12, 0xf2, 0x40, 0x44, 0xaa,
+ 0xf1, 0x07, 0x4d, 0xa1, 0xff, 0x98, 0xca, 0xb0, 0x88, 0xae, 0x32, 0xcb,
+ 0xf3, 0x3d, 0x7e, 0x12, 0x2c, 0x2f, 0x8c, 0x75, 0x83, 0x6b, 0x4d, 0x66,
+ 0x3f, 0x93, 0x91, 0xd4, 0x73, 0x76, 0x0c, 0x2a, 0xed, 0x6a, 0xb0, 0x86,
+ 0x44, 0xfa, 0xe7, 0x1c, 0x89, 0x5b, 0x70, 0x33, 0x9c, 0x2f, 0x2b, 0xa9,
+ 0x3c, 0x9b, 0x58, 0xef, 0x60, 0xa0, 0x8b, 0xbb, 0x58, 0xda, 0x04, 0x38,
+ 0x10, 0x3a, 0x6c, 0x88, 0x64, 0x24, 0x97, 0x60, 0x04, 0x90, 0x9e, 0x72,
+ 0x97, 0x0d, 0x03, 0xf0, 0xf7, 0xfa, 0x26, 0xbf, 0xd7, 0xd1, 0x33, 0xa4,
+ 0x6f, 0x2b, 0xb9, 0xd0, 0xf7, 0x0b, 0xaf, 0xd7, 0x2c, 0x23, 0xb9, 0xfe,
+ 0x66, 0x46, 0x18, 0x6e, 0xb7, 0x0f, 0xdb, 0x93, 0x1b, 0x67, 0x86, 0xa6,
+ 0xb2, 0x57, 0x9b, 0x8f, 0xe0, 0xf2, 0x37, 0x4e, 0x09, 0xb6, 0x0b, 0x11,
+ 0xc7, 0x0c, 0x7d, 0x18, 0x57, 0x8b, 0xc4, 0x1f, 0xaa, 0x1b, 0x75, 0x9a,
+ 0xb7, 0x27, 0x8d, 0x59, 0x3b, 0xbb, 0x99, 0x30, 0xbb, 0x85, 0xde, 0x8f,
+ 0x22, 0x82, 0xa6, 0x50, 0xae, 0x48, 0xcb, 0x1e, 0x32, 0x3a, 0x22, 0x1f,
+ 0xed, 0x2a, 0x24, 0x47, 0x64, 0xd9, 0x66, 0x7a, 0x9a, 0xa5, 0x59, 0xfa,
+ 0x9f, 0x64, 0xd2, 0xc2, 0x65, 0x07, 0x0b, 0x82, 0xe7, 0xa7, 0x1e, 0x5f,
+ 0xed, 0x91, 0x9d, 0xc3, 0x79, 0x7f, 0x1a, 0xc9, 0x58, 0x6a, 0xfc, 0xe2,
+ 0x41, 0xf4, 0x3b, 0xdf, 0x3d, 0x0a, 0xb2, 0x2d, 0x46, 0x27, 0x57, 0x0f,
+ 0xfb, 0xc4, 0x36, 0x90, 0x66, 0xbf, 0x10, 0x14, 0x33, 0xed, 0x1d, 0xba,
+ 0xe7, 0xe5, 0xcb, 0x4c, 0xf5, 0xc6, 0x55, 0x18, 0x13, 0xba, 0xc5, 0xa1,
+ 0xac, 0xf6, 0xe2, 0x79, 0xd6, 0xb7, 0xb3, 0x8d, 0x93, 0x0d, 0xa0, 0x78,
+ 0x9e, 0xc0, 0x33, 0x08, 0x74, 0x7b, 0x57, 0xef, 0x29, 0xa0, 0x54, 0xdf,
+ 0xf6, 0x60, 0x63, 0xde, 0x5a, 0x2e, 0x9b, 0xf9, 0xf9, 0x24, 0x65, 0x78,
+ 0x86, 0xb7, 0xc1, 0xa7, 0xd8, 0x36, 0xe2, 0x9d, 0x42, 0xee, 0x76, 0xcd,
+ 0x00, 0x77, 0x90, 0xec, 0x5f, 0xc2, 0x1e, 0xd8, 0x2f, 0x4a, 0x31, 0x05,
+ 0x62, 0xd4, 0x2a, 0x8e, 0x33, 0x63, 0xce, 0xce, 0x16, 0x88, 0x50, 0x3f,
+ 0x26, 0x7d, 0x44, 0xad, 0xd9, 0xc5, 0xf4, 0xd3, 0xe7, 0x69, 0xfd, 0xb3,
+ 0x66, 0x76, 0x5f, 0xd5, 0x5d, 0xf7, 0x54, 0x77, 0xf9, 0x3c, 0x8c, 0xc0,
+ 0x61, 0x30, 0x54, 0x9a, 0x8b, 0x48, 0x92, 0x18, 0xdb, 0x96, 0x2b, 0xc7,
+ 0x84, 0x75, 0x10, 0x69, 0x61, 0x52, 0x6d, 0x1f, 0x1a, 0x2f, 0x09, 0x4b,
+ 0x07, 0x10, 0xec, 0x46, 0x1f, 0x97, 0x8f, 0xe7, 0x94, 0x82, 0x6b, 0x8f,
+ 0x72, 0x7d, 0x12, 0xca, 0x5a, 0x85, 0xec, 0x62, 0x62, 0x84, 0xba, 0x38,
+ 0xf5, 0xf4, 0x94, 0x3b, 0x63, 0xd9, 0xeb, 0x36, 0xb3, 0x9e, 0xbe, 0x76,
+ 0xb6, 0xa0, 0x0b, 0x76, 0x40, 0x4a, 0x84, 0xc0, 0x70, 0x3e, 0x0a, 0x1c,
+ 0x6f, 0x02, 0x55, 0x7d, 0x94, 0x0e, 0x1a, 0x3e, 0x8a, 0x5e, 0xf2, 0xc9,
+ 0xcb, 0xfd, 0x6b, 0xf1, 0xe4, 0xcf, 0xca, 0xa6, 0x26, 0x69, 0xc5, 0xba,
+ 0xdb, 0xa6, 0x54, 0x25, 0x44, 0xe0, 0x46, 0xaf, 0x06, 0xff, 0x05, 0x38,
+ 0x23, 0xb9, 0xfa, 0xf5, 0x63, 0xf8, 0x1c, 0x2d, 0xcd, 0x47, 0x03, 0xd9,
+ 0x38, 0x61, 0xbb, 0xcb, 0x84, 0x75, 0x2e, 0xf8, 0x77, 0x5d, 0x5a, 0x19,
+ 0x73, 0xbe, 0xb6, 0x06, 0x5a, 0x28, 0xc2, 0xc6, 0x49, 0xfd, 0xf0, 0xad,
+ 0x54, 0xc9, 0xbd, 0x1b, 0xd3, 0x24, 0x01, 0x42, 0x63, 0x75, 0x5b, 0xe4,
+ 0xce, 0x94, 0x61, 0x15, 0x05, 0xd5, 0x8e, 0x7a, 0xa3, 0xc7, 0xd7, 0x6f,
+ 0xbc, 0x61, 0xf6, 0x96, 0x59, 0xc8, 0x7b, 0x7f, 0x2c, 0x63, 0x8e, 0x17,
+ 0xbc, 0x34, 0x2f, 0xfd, 0xf8, 0x2a, 0x51, 0x8a, 0x89, 0x2e, 0xa1, 0xdc,
+ 0x7e, 0xa9, 0xb5, 0xc7, 0xc6, 0x1f, 0xb4, 0x0d, 0x31, 0x85, 0x2d, 0x05,
+ 0x71, 0x1e, 0xeb, 0xdf, 0xda, 0xed, 0x55, 0x84, 0xd3, 0xf9, 0x3e, 0x1f,
+ 0xb7, 0x71, 0x58, 0x93, 0x68, 0x3b, 0xd8, 0x76, 0xc3, 0x0e, 0xee, 0xb6,
+ 0x35, 0x87, 0x10, 0x2e, 0x56, 0x70, 0x1f, 0x68, 0x1a, 0x2b, 0x38, 0x00,
+ 0x97, 0x5f, 0xdf, 0x8c, 0x43, 0x5e, 0xc8, 0x6b, 0xbe, 0xfd, 0x5c, 0x6d,
+ 0xa5, 0x38, 0x8a, 0x2f, 0x63, 0x77, 0xb6, 0x42, 0xbb, 0x7f, 0x65, 0xd6,
+ 0xeb, 0xe3, 0x96, 0x40, 0xd3, 0xee, 0x09, 0xd7, 0xa9, 0xad, 0x17, 0xb3,
+ 0xa9, 0x1e, 0xff, 0x5e, 0xcb, 0x5f, 0xfe, 0xcb, 0x03, 0xf8, 0x4e, 0x6f,
+ 0xd7, 0xbd, 0x89, 0x4c, 0x05, 0xa5, 0x6c, 0x77, 0xb3, 0x71, 0x37, 0x0b,
+ 0x79, 0x8b, 0xf0, 0x59, 0xfd, 0xff, 0x1f, 0xb8, 0x69, 0x6d, 0xdb, 0x09,
+ 0x13, 0x75, 0xc2, 0x31, 0x9a, 0xd7, 0x9f, 0x69, 0x2b, 0xff, 0x42, 0x16,
+ 0x51, 0xcd, 0x25, 0x83, 0x7d, 0xdf, 0x6e, 0x27, 0x14, 0x28, 0xae, 0xb5,
+ 0x85, 0x01, 0x9d, 0x84, 0x54, 0x99, 0x46, 0xa6, 0xcb, 0x36, 0xfa, 0x36,
+ 0xe7, 0xf0, 0x03, 0xc0, 0x30, 0x5e, 0xc6, 0xbe, 0x29, 0x1a, 0x5a, 0xeb,
+ 0x43, 0x47, 0x31, 0xdd, 0x43, 0x7b, 0x84, 0xbb, 0x28, 0xe3, 0xe2, 0x5c,
+ 0x2d, 0xdc, 0x15, 0x88, 0x58, 0x0a, 0xbf, 0xe0, 0xf0, 0x16, 0xf6, 0x79,
+ 0x72, 0x5e, 0x23, 0x8c, 0xb0, 0xb9, 0x0e, 0xb6, 0xbe, 0x35, 0xe4, 0xe1,
+ 0x60, 0x5e, 0xe2, 0xd6, 0xb4, 0x31, 0xc6, 0x49, 0xfe, 0xd4, 0x72, 0x47,
+ 0xd8, 0xff, 0xf5, 0x6e, 0x19, 0xe6, 0x2c, 0x41, 0x19, 0xd2, 0xd3, 0x17,
+ 0xb6, 0xd8, 0x90, 0xad, 0xee, 0x1f, 0x14, 0x0c, 0xee, 0xa1, 0x1d, 0x6b,
+ 0x2d, 0xe9, 0x77, 0x7f, 0xd9, 0xc6, 0xf5, 0xf1, 0x30, 0x08, 0x82, 0xf4,
+ 0xb2, 0x56, 0x65, 0xc4, 0xf3, 0xbf, 0x8b, 0x7e, 0xe6, 0xbd, 0x4f, 0x55,
+ 0xd7, 0xfe, 0xe9, 0x04, 0x73, 0x4b, 0x66, 0xa5, 0x87, 0xe9, 0x7a, 0x36,
+ 0x30, 0x30, 0xe9, 0x36, 0x83, 0xe3, 0x14, 0x57, 0xe3, 0xa0, 0xc3, 0x7a,
+ 0xc3, 0x4f, 0x38, 0x0d, 0x04, 0x56, 0x9e, 0x4b, 0x30, 0xbd, 0xba, 0x7f,
+ 0xd9, 0x2b, 0xdc, 0x45, 0xcb, 0xb5, 0xaf, 0x01, 0xc2, 0x29, 0x09, 0x3e,
+ 0x40, 0xbf, 0xcd, 0xaa, 0x7e, 0xe0, 0xbe, 0x9c, 0x8d, 0xde, 0x09, 0x35,
+ 0xb5, 0xd6, 0x40, 0x1b, 0xcc, 0xc8, 0xaf, 0x75, 0x33, 0xd9, 0x10, 0xe2,
+ 0x55, 0x42, 0x09, 0xca, 0x74, 0x35, 0xb6, 0xd0, 0x19, 0xba, 0xda, 0x47,
+ 0xbd, 0x42, 0x19, 0x92, 0x7d, 0x8f, 0x6e, 0xc4, 0x91, 0x76, 0x13, 0xf0,
+ 0x2c, 0xd6, 0xa0, 0x5c, 0x9e, 0xff, 0x28, 0x97, 0xd3, 0x57, 0x63, 0xe5,
+ 0x20, 0x9c, 0x1e, 0x31, 0x45, 0x3f, 0xec, 0x1a, 0xd8, 0xff, 0xd0, 0xc2,
+ 0xef, 0x22, 0xed, 0xc7, 0xee, 0x8c, 0xc5, 0xa2, 0xd6, 0xad, 0x38, 0x0f,
+ 0xc2, 0xe3, 0xdf, 0x72, 0x35, 0xe9, 0xe1, 0xeb, 0x2f, 0xb8, 0xf5, 0x18,
+ 0xe1, 0x6d, 0xe2, 0x22, 0x24, 0x42, 0xf1, 0x18, 0x2e, 0xf5, 0x7b, 0xff,
+ 0xf2, 0x17, 0xd1, 0x8a, 0x51, 0x25, 0x55, 0x82, 0xbd, 0xb5, 0xa2, 0x78,
+ 0x76, 0x32, 0xe0, 0xa5, 0x47, 0x2d, 0x5a, 0x88, 0xaa, 0x47, 0xde, 0x06,
+ 0xb8, 0xec, 0x76, 0xbc, 0x88, 0x1b, 0xc8, 0x7d, 0x9c, 0xe7, 0x9d, 0x6d,
+ 0xe3, 0x09, 0xd3, 0xc8, 0xeb, 0x78, 0x1c, 0xc3, 0x00, 0xe5, 0x17, 0x49,
+ 0xfd, 0xf3, 0x6d, 0x84, 0x21, 0xcc, 0x6f, 0xbb, 0x8b, 0xdb, 0xc0, 0x56,
+ 0x90, 0x0e, 0x32, 0xbd, 0xa5, 0xed, 0xd5, 0xb5, 0xf3, 0xd1, 0x37, 0x2d,
+ 0x6f, 0x47, 0xd7, 0xba, 0xb7, 0xbf, 0x58, 0x09, 0xbe, 0x07, 0x46, 0x96,
+ 0xd4, 0xcc, 0xcb, 0xfd, 0x6a, 0xa0, 0x56, 0x39, 0xea, 0xe4, 0x67, 0xc1,
+ 0xc6, 0x0a, 0xb3, 0x55, 0xb8, 0xb3, 0xc1, 0x99, 0xbf, 0x52, 0x23, 0x84,
+ 0x63, 0x31, 0xeb, 0xfc, 0x53, 0x5d, 0x58, 0x2a, 0x00, 0x00, 0xaa, 0x5a,
+ 0x11, 0xc2, 0x30, 0x54, 0xb6, 0x2f, 0xba, 0x10, 0xe5, 0x3e, 0xf3, 0xb7,
+ 0x46, 0xbd, 0xbe, 0x8e, 0x08, 0xf6, 0x89, 0xf1, 0x18, 0x05, 0x95, 0xba,
+ 0x2d, 0xad, 0xaf, 0x64, 0x58, 0x98, 0x86, 0x6d, 0xa7, 0x48, 0xbb, 0xf1,
+ 0xb4, 0xab, 0x14, 0xa1, 0x09, 0x98, 0xcc, 0x9f, 0xcb, 0x7f, 0xba, 0x39,
+ 0xb5, 0x8d, 0x93, 0xd7, 0x03, 0x48, 0x86, 0x29, 0x7c, 0x69, 0x17, 0xc5,
+ 0xa6, 0xa0, 0xe5, 0xce, 0x90, 0x8a, 0x7d, 0xd7, 0xb8, 0xf7, 0x55, 0x63,
+ 0x06, 0x88, 0xe8, 0x04, 0x51, 0x88, 0xf8, 0x8b, 0xb5, 0x99, 0x2b, 0x7d,
+ 0x52, 0x5d, 0x23, 0x2d, 0xa2, 0x19, 0xcd, 0x31, 0x55, 0x30, 0x3e, 0xd4,
+ 0x8a, 0xfb, 0x6a, 0x5b, 0x99, 0xb7, 0xfb, 0xc1, 0x03, 0x98, 0xf0, 0x53,
+ 0x5c, 0xb1, 0x5e, 0x4b, 0x47, 0x8a, 0x9c, 0x21, 0xeb, 0x77, 0x44, 0x96,
+ 0xc3, 0x4f, 0x56, 0x01, 0x65, 0x7e, 0x93, 0x37, 0xaa, 0x0e, 0x97, 0xce,
+ 0x3d, 0xf2, 0xb1, 0x69, 0x93, 0x5e, 0x95, 0x6f, 0xe9, 0x27, 0xbc, 0x9c,
+ 0x45, 0xa5, 0x16, 0xec, 0xb4, 0xc8, 0xfa, 0x41, 0x0e, 0x6e, 0x79, 0x0a,
+ 0x0f, 0x6d, 0x9d, 0x6e, 0x4c, 0x07, 0x6b, 0x35, 0x6f, 0x0c, 0x4c, 0x65,
+ 0x2f, 0x48, 0x87, 0x3e, 0x47, 0x41, 0x9d, 0x5e, 0xe4, 0x7a, 0xd1, 0x54,
+ 0xd2, 0xd6, 0x45, 0x04, 0x4d, 0xde, 0xb2, 0x1f, 0x3f, 0x91, 0xac, 0x15,
+ 0x3a, 0x38, 0xf8, 0x6e, 0xe5, 0x29, 0xc6, 0x96, 0xa4, 0x61, 0xae, 0xf6,
+ 0x52, 0x89, 0x63, 0x96, 0x7d, 0xd5, 0x5d, 0x07, 0x15, 0x18, 0x11, 0xba,
+ 0x07, 0x59, 0xb4, 0xe4, 0x10, 0x5b, 0xfe, 0x08, 0x3e, 0xab, 0xad, 0xfc,
+ 0x32, 0x70, 0x84, 0xa4, 0xdc, 0x69, 0x69, 0xc9, 0x10, 0xb8, 0x1a, 0x7b,
+ 0x55, 0xd9, 0x44, 0xe5, 0x53, 0xf5, 0x2c, 0x46, 0x82, 0x1d, 0xac, 0xfd,
+ 0x6f, 0xc4, 0x65, 0x12, 0x69, 0xc7, 0x2a, 0x85, 0x95, 0x77, 0x54, 0x23,
+ 0x7b, 0xdd, 0x7c, 0x55, 0x76, 0x9e, 0x17, 0x66, 0x3b, 0xda, 0xf7, 0xd1,
+ 0xf0, 0x7a, 0xd4, 0x28, 0x07, 0x10, 0x8d, 0x5b, 0xab, 0x63, 0x91, 0x3b,
+ 0x6e, 0xa6, 0xa2, 0xae, 0x10, 0x87, 0x4b, 0xe2, 0x82, 0x1b, 0xfa, 0x12,
+ 0xb8, 0xff, 0xaf, 0x0f, 0x57, 0xa9, 0x01, 0x98, 0xc0, 0x2f, 0x6b, 0x71,
+ 0x1e, 0xc3, 0x63, 0x40, 0x4f, 0x3a, 0xf8, 0xd1, 0x68, 0x50, 0x58, 0x64,
+ 0x5f, 0x97, 0xe8, 0x3d, 0x7c, 0xcf, 0x71, 0x55, 0xcf, 0x7b, 0xea, 0xf5,
+ 0x97, 0x2a, 0x45, 0x6d, 0xcb, 0x7f, 0xc6, 0x74, 0x27, 0x9f, 0xc4, 0xea,
+ 0x30, 0x3d, 0x1a, 0x05, 0x88, 0x90, 0xcf, 0x0e, 0x7f, 0xdf, 0xcd, 0xe5,
+ 0x09, 0x8b, 0x4f, 0x0f, 0x83, 0xe9, 0x74, 0x47, 0x76, 0x5f, 0x9b, 0xd4,
+ 0x01, 0x14, 0x4c, 0x96, 0xcb, 0x9c, 0xde, 0x02, 0xc9, 0x95, 0x4e, 0x7b,
+ 0x97, 0x8a, 0x90, 0x62, 0x68, 0x52, 0x05, 0x61, 0xc8, 0x25, 0x89, 0x95,
+ 0x90, 0xd8, 0x08, 0xe8, 0xa6, 0xf9, 0x66, 0x94, 0x7c, 0xad, 0xe9, 0x3e,
+ 0x32, 0x0f, 0xe3, 0xac, 0x2d, 0xd0, 0x3b, 0xc7, 0x56, 0x01, 0x52, 0x3c,
+ 0x61, 0x51, 0x0b, 0xb3, 0x66, 0x75, 0x80, 0xe1, 0x6b, 0xd8, 0x95, 0xea,
+ 0x88, 0x90, 0x69, 0x43, 0x69, 0xd6, 0xc5, 0x04, 0x39, 0x6c, 0xe4, 0x0a,
+ 0x7c, 0x54, 0xcb, 0xcb, 0x35, 0x7c, 0xbe, 0x8c, 0x8f, 0x3c, 0xb0, 0x16,
+ 0x6f, 0xa6, 0x60, 0x52, 0xca, 0x0f, 0x1d, 0x23, 0x87, 0x38, 0x44, 0xbd,
+ 0x4e, 0xd7, 0xb8, 0x07, 0x2d, 0xc9, 0xdd, 0x45, 0xea, 0xca, 0x0c, 0x0e,
+ 0x2f, 0x6a, 0x28, 0xad, 0x94, 0xde, 0xc3, 0x8a, 0xce, 0x97, 0x9b, 0xb0,
+ 0x03, 0xe0, 0x3d, 0xb9, 0xc9, 0xe9, 0x1a, 0x69, 0xa6, 0xf7, 0xcb, 0x29,
+ 0x6a, 0xe7, 0xb4, 0xfa, 0xc5, 0xad, 0x45, 0xe2, 0x32, 0x73, 0xb2, 0x2e,
+ 0x0b, 0xc1, 0xf3, 0xbe, 0x54, 0xb6, 0xf5, 0xc4, 0xda, 0x15, 0x8a, 0x1f,
+ 0x90, 0x8b, 0x7a, 0x62, 0x77, 0x70, 0x01, 0xc6, 0x60, 0xbc, 0x90, 0x87,
+ 0x64, 0x3f, 0xea, 0x56, 0x6d, 0xe2, 0x53, 0xa0, 0x3c, 0xee, 0x41, 0x0c,
+ 0x37, 0xeb, 0x7e, 0x2e, 0x0a, 0xec, 0x73, 0x02, 0x09, 0xce, 0xbd, 0x04,
+ 0x0e, 0xfc, 0x05, 0x9f, 0x0e, 0xdf, 0x80, 0x17, 0x50, 0xb9, 0xe3, 0xb9,
+ 0x4f, 0xff, 0x19, 0xc5, 0x6b, 0x74, 0x43, 0xdc, 0x2b, 0x00, 0xdc, 0xca,
+ 0x26, 0x97, 0xae, 0x9f, 0x39, 0xb3, 0x4f, 0x56, 0x60, 0x5f, 0x8b, 0x24,
+ 0x16, 0x69, 0xff, 0x4c, 0xe9, 0x2e, 0x91, 0x48, 0xa6, 0x09, 0xda, 0xcf,
+ 0x72, 0x56, 0x45, 0xb7, 0x29, 0x87, 0x78, 0xbb, 0x29, 0xe0, 0x78, 0x04,
+ 0xfd, 0xad, 0x91, 0x77, 0xe5, 0xe9, 0xd9, 0x42, 0x16, 0xbf, 0xfb, 0x14,
+ 0xb7, 0x54, 0xd1, 0x56, 0x4d, 0x4f, 0x0c, 0x42, 0x02, 0x9a, 0xdc, 0xea,
+ 0x21, 0x7a, 0x24, 0x7c, 0x1b, 0xbf, 0x74, 0x3c, 0x39, 0x1f, 0x96, 0x34,
+ 0x6e, 0xaa, 0x7b, 0xcb, 0x5f, 0x11, 0x72, 0x76, 0x96, 0x62, 0x17, 0xd8,
+ 0x28, 0x4d, 0x32, 0x19, 0x74, 0x0f, 0xb0, 0x59, 0xe8, 0x4d, 0x93, 0x68,
+ 0x3e, 0x38, 0x71, 0x84, 0x6e, 0x80, 0xfc, 0x96, 0x0a, 0x56, 0xfa, 0x3b,
+ 0x93, 0x2d, 0x84, 0x57, 0xb6, 0xac, 0x18, 0xe9, 0x07, 0x07, 0xa8, 0x45,
+ 0xed, 0x54, 0xd3, 0xdf, 0x53, 0x3a, 0x48, 0xcf, 0xbe, 0x49, 0x8f, 0xf8,
+ 0xbb, 0xe4, 0xf8, 0xe1, 0x7e, 0x83, 0xcc, 0xda, 0x26, 0xea, 0xba, 0x99,
+ 0xe4, 0x23, 0x0f, 0xd0, 0xd0, 0x61, 0xd9, 0x6b, 0xa0, 0x9b, 0xa1, 0x2e,
+ 0xcf, 0x0c, 0x7b, 0x8f, 0x8d, 0xd1, 0x29, 0x35, 0x9e, 0x48, 0x9a, 0xdb,
+ 0x64, 0x2f, 0x42, 0x8c, 0x76, 0x24, 0xc8, 0x2c, 0x48, 0xdf, 0x57, 0xdd,
+ 0x3d, 0xb7, 0xd6, 0x4e, 0x0a, 0x04, 0xfe, 0x79, 0x92, 0xb2, 0x48, 0xef,
+ 0x0f, 0xf4, 0x91, 0xbd, 0xa2, 0x5b, 0x40, 0x9a, 0x8d, 0x48, 0x71, 0xc3,
+ 0x83, 0x85, 0x63, 0x22, 0x17, 0x0a, 0x7a, 0xd8, 0xa0, 0x40, 0x57, 0xf9,
+ 0x93, 0x66, 0xf4, 0xef, 0xd5, 0xc9, 0xcc, 0x90, 0x5b, 0x45, 0x6e, 0x85,
+ 0xe8, 0xa3, 0x06, 0xb0, 0xfe, 0x73, 0x2c, 0x5b, 0x93, 0x9a, 0x57, 0x73,
+ 0x65, 0x95, 0x7d, 0xef, 0x18, 0x9d, 0xed, 0x6d, 0x12, 0x34, 0xec, 0xfa,
+ 0xdd, 0xc5, 0x42, 0x2d, 0x24, 0xf6, 0x78, 0x64, 0x58, 0x88, 0xf5, 0x7d,
+ 0xc6, 0xf0, 0x13, 0xe1, 0xa8, 0x37, 0x81, 0x03, 0x54, 0x9c, 0x40, 0xfd,
+ 0x0c, 0x8d, 0x6c, 0x4f, 0xf9, 0x2a, 0x4e, 0x8f, 0x43, 0xe3, 0x24, 0x60,
+ 0x01, 0x9d, 0x43, 0x39, 0x4f, 0xbf, 0xc7, 0x91, 0xcf, 0x3e, 0xe9, 0xa9,
+ 0xb4, 0xbb, 0x9d, 0xe7, 0xc0, 0x2d, 0xc1, 0x51, 0x94, 0xec, 0x91, 0x9e,
+ 0x14, 0x3f, 0xae, 0xd2, 0x27, 0xa8, 0x8c, 0x83, 0x5b, 0x6d, 0xba, 0x11,
+ 0xc4, 0x4b, 0xc9, 0x47, 0xad, 0x94, 0x48, 0x0e, 0x29, 0xfa, 0x42, 0x45,
+ 0x1a, 0x71, 0xba, 0x65, 0xf8, 0x89, 0x69, 0xcc, 0x64, 0xd0, 0x50, 0xfc,
+ 0x9c, 0xc5, 0x33, 0x1b, 0x65, 0x88, 0x30, 0xf7, 0xca, 0x64, 0x05, 0x5a,
+ 0xc9, 0x30, 0x79, 0x99, 0x3b, 0x79, 0xca, 0x37, 0x84, 0x02, 0x60, 0x3b,
+ 0x69, 0x43, 0x55, 0x95, 0x37, 0x10, 0x25, 0xa4, 0xdd, 0xfe, 0xdf, 0x70,
+ 0xbf, 0xa8, 0xaa, 0x7c, 0x0a, 0xf9, 0x77, 0x64, 0x00, 0x48, 0xdf, 0x0e,
+ 0xf3, 0x99, 0xf6, 0x75, 0x76, 0xac, 0xe6, 0xa9, 0x5b, 0x7d, 0x64, 0x90,
+ 0xe3, 0xb6, 0x2d, 0x88, 0x03, 0x33, 0x95, 0x2a, 0x49, 0xda, 0xd5, 0xa7,
+ 0x99, 0xaf, 0xee, 0x81, 0xba, 0x9d, 0x82, 0xf6, 0x93, 0x24, 0xf1, 0xdb,
+ 0x26, 0x41, 0x1a, 0x1c, 0xfc, 0x38, 0x49, 0xb3, 0xa1, 0x97, 0xb0, 0x1d,
+ 0x5a, 0x7f, 0x2a, 0xb0, 0x3e, 0xfa, 0x3b, 0x02, 0x34, 0xc8, 0x39, 0xc4,
+ 0x02, 0x66, 0x91, 0xd9, 0xc2, 0x4e, 0x02, 0x27, 0x52, 0x90, 0x13, 0xaa,
+ 0x41, 0xfa, 0xae, 0xd5, 0xdc, 0xab, 0xc9, 0xdd, 0x9a, 0xa7, 0x17, 0xdd,
+ 0x41, 0x3e, 0xca, 0x0f, 0x19, 0x07, 0x33, 0x94, 0xd7, 0xbf, 0x15, 0x47,
+ 0x39, 0x20, 0x1e, 0x7d, 0x9c, 0x88, 0x64, 0xd4, 0x0c, 0x95, 0xb6, 0x02,
+ 0x9f, 0x87, 0x95, 0xba, 0x0e, 0x00, 0x3d, 0x94, 0xb3, 0x28, 0x16, 0xb7,
+ 0xeb, 0x3e, 0x86, 0x59, 0xae, 0x1e, 0xb9, 0xbc, 0x95, 0xbf, 0x50, 0xd5,
+ 0x39, 0x9c, 0xe6, 0x74, 0xcd, 0xba, 0x1b, 0x03, 0xa6, 0xf6, 0xc3, 0xfa,
+ 0x58, 0x36, 0x01, 0x81, 0xfa, 0xfa, 0xb4, 0xdf, 0x49, 0xb5, 0x77, 0x34,
+ 0xcb, 0x56, 0xc7, 0x14, 0x98, 0xb0, 0x8d, 0xb9, 0x52, 0x6d, 0x94, 0x5e,
+ 0x5f, 0x0d, 0x31, 0xe1, 0x4c, 0x22, 0x75, 0x4a, 0x95, 0x78, 0x1a, 0x70,
+ 0xbe, 0x5e, 0x22, 0xe6, 0x27, 0x7d, 0x65, 0x8e, 0xff, 0xb5, 0x0a, 0x1f,
+ 0x24, 0x30, 0x1d, 0x23, 0x3a, 0x5e, 0x3d, 0xd8, 0xfe, 0x00, 0x83, 0x75,
+ 0xa2, 0x8c, 0x6f, 0x09, 0x59, 0x4f, 0x68, 0x45, 0x69, 0xde, 0xe0, 0xbf,
+ 0x95, 0x9a, 0x12, 0x8e, 0x22, 0x72, 0x66, 0xf7, 0x0e, 0xf1, 0x8c, 0x20,
+ 0x34, 0x9a, 0xdb, 0x9f, 0xfa, 0xf7, 0xac, 0x65, 0x91, 0xb0, 0x73, 0x1f,
+ 0x88, 0x73, 0xef, 0x2a, 0xe4, 0x81, 0x61, 0x5b, 0x20, 0xef, 0x43, 0xba,
+ 0xfb, 0x56, 0xa7, 0x9d, 0x12, 0xb1, 0xdd, 0xb1, 0x9e, 0x75, 0xa4, 0x71,
+ 0xe0, 0x5f, 0x74, 0x56, 0x61, 0x6a, 0x15, 0x5f, 0x03, 0x36, 0xf9, 0x88,
+ 0x3e, 0xbb, 0xc1, 0x67, 0xfd, 0x4f, 0xc2, 0x2e, 0x9c, 0xcf, 0x5c, 0x7b,
+ 0xd5, 0x3a, 0x32, 0xa6, 0xef, 0xad, 0x7e, 0x9f, 0x84, 0xe7, 0xb7, 0x36,
+ 0x21, 0x68, 0x79, 0x1e, 0xbc, 0x24, 0x8c, 0xf9, 0x92, 0xb4, 0x90, 0x18,
+ 0xe3, 0x7a, 0x6e, 0xe4, 0xa7, 0xf2, 0x16, 0xc0, 0x62, 0x92, 0x2b, 0x19,
+ 0xd7, 0x65, 0x26, 0xde, 0x0b, 0x51, 0x8a, 0xbf, 0x64, 0xe0, 0xf0, 0x57,
+ 0xe8, 0x4f, 0x1f, 0x1a, 0x24, 0x4c, 0x73, 0x9c, 0xac, 0xf0, 0x8c, 0x40,
+ 0x8a, 0x23, 0xf8, 0x5b, 0x46, 0x70, 0x63, 0x78, 0x6c, 0x13, 0x38, 0x75,
+ 0x0e, 0xb9, 0x34, 0xb6, 0x72, 0x1c, 0xff, 0x8c, 0x6a, 0xcf, 0x8a, 0x55,
+ 0x29, 0x5e, 0xc6, 0x3a, 0x85, 0x01, 0x40, 0x1d, 0x13, 0x70, 0xd4, 0xa6,
+ 0x69, 0x69, 0xcf, 0x39, 0xa2, 0x7a, 0x06, 0xe0, 0x5b, 0xc5, 0xe4, 0x85,
+ 0xb8, 0x6a, 0xc7, 0xa0, 0x0b, 0x9d, 0x0e, 0xf5, 0xa2, 0xc4, 0xd4, 0x1c,
+ 0xc7, 0x8f, 0x53, 0x06, 0xf5, 0x2a, 0xb0, 0x92, 0x57, 0x55, 0xd6, 0xf0,
+ 0xc3, 0x05, 0xb9, 0x73, 0x77, 0xb1, 0xdd, 0x1f, 0xb1, 0xeb, 0xb5, 0xa3,
+ 0x35, 0xdc, 0x44, 0x1b, 0x74, 0x85, 0x62, 0x46, 0xce, 0x5e, 0xde, 0x91,
+ 0x1b, 0x21, 0x6d, 0xa6, 0xc1, 0x34, 0x7d, 0xb8, 0x06, 0x86, 0xfc, 0x72,
+ 0x31, 0xcf, 0xd0, 0x83, 0xd8, 0xaa, 0xdd, 0xf4, 0x38, 0x6f, 0x32, 0x16,
+ 0x5f, 0x86, 0x8c, 0xf0, 0xd4, 0x76, 0xdc, 0xd1, 0x37, 0xdb, 0xc5, 0x75,
+ 0xda, 0xd4, 0xf9, 0xd7, 0x3f, 0xa1, 0x98, 0x9f, 0xd6, 0xb8, 0x2d, 0xaa,
+ 0xb3, 0x24, 0xc0, 0xb3, 0x87, 0xa2, 0x8b, 0x34, 0x8f, 0x1b, 0x64, 0x0d,
+ 0x1a, 0xf3, 0x60, 0x85, 0x34, 0x97, 0xac, 0xe8, 0xf3, 0xb4, 0x0b, 0x25,
+ 0x6e, 0x58, 0xea, 0x12, 0x22, 0x0b, 0x03, 0x7b, 0xbe, 0xc9, 0xb0, 0x49,
+ 0x92, 0xc6, 0x03, 0x25, 0xd1, 0x99, 0xdd, 0x44, 0x8c, 0xe3, 0xec, 0x7c,
+ 0x46, 0x4a, 0x0a, 0x2d, 0xed, 0x65, 0x5f, 0x55, 0x51, 0xd6, 0x60, 0xcd,
+ 0x38, 0xb9, 0xe8, 0xaa, 0x49, 0x02, 0x22, 0x18, 0x91, 0x5d, 0x9a, 0x32,
+ 0x37, 0x6c, 0xa7, 0x82, 0x3e, 0x69, 0xbc, 0x7e, 0xc6, 0x72, 0xa6, 0x97,
+ 0xa2, 0x1c, 0x2d, 0xdb, 0x03, 0xe0, 0x6d, 0xeb, 0x64, 0xd0, 0xf0, 0x84,
+ 0x7c, 0x9f, 0xed, 0xd4, 0x18, 0xfe, 0x53, 0xf8, 0xab, 0x31, 0xf5, 0x4e,
+ 0xbe, 0xa1, 0x97, 0x8e, 0x76, 0xb5, 0x45, 0x46, 0x08, 0xfa, 0x44, 0x5c,
+ 0x93, 0x4f, 0x7e, 0x84, 0xcd, 0x33, 0xda, 0x9a, 0xca, 0x87, 0x96, 0xd0,
+ 0x01, 0x9e, 0x5f, 0xa1, 0x3e, 0x3a, 0x4d, 0xe1, 0x71, 0xfb, 0x5a, 0x6c,
+ 0xc7, 0xfe, 0x06, 0x91, 0x84, 0x94, 0x00, 0x06, 0xe6, 0xda, 0xa8, 0xd9,
+ 0x08, 0x99, 0x0f, 0xa5, 0x5d, 0x69, 0x2b, 0x2d, 0x47, 0xbd, 0xc6, 0x64,
+ 0xf2, 0xa1, 0xb0, 0x20, 0x4c, 0x54, 0x85, 0xe7, 0x88, 0x5d, 0xd0, 0x2b,
+ 0xea, 0x3e, 0x16, 0xc7, 0x17, 0x1c, 0xb4, 0x6d, 0x49, 0xee, 0x3c, 0x63,
+ 0xe8, 0xe3, 0x67, 0x53, 0x1e, 0xff, 0xca, 0xd2, 0xed, 0x47, 0x13, 0x26,
+ 0x6f, 0x1e, 0x36, 0xc0, 0xb4, 0x9c, 0x6a, 0x9d, 0xf3, 0x58, 0x79, 0x09,
+ 0x60, 0x41, 0x48, 0xaa, 0xac, 0x47, 0x37, 0x2b, 0xec, 0x76, 0x8c, 0x90,
+ 0xc6, 0xd4, 0x5d, 0x0d, 0x7c, 0x8c, 0xc1, 0x26, 0x83, 0x61, 0xc2, 0x0f,
+ 0x33, 0xc2, 0x12, 0x5d, 0xef, 0x69, 0x61, 0x6a, 0x4e, 0x59, 0x0a, 0x5b,
+ 0xac, 0x85, 0xc7, 0x38, 0xf4, 0x3d, 0x7c, 0x35, 0xeb, 0xec, 0xa5, 0xc2,
+ 0x94, 0x83, 0x4a, 0x30, 0xff, 0xcb, 0xe0, 0xb5, 0xbc, 0xf8, 0x80, 0x0d,
+ 0x3e, 0x92, 0x39, 0x44, 0x18, 0x8d, 0x5b, 0xce, 0xf1, 0x1b, 0x22, 0x0c,
+ 0x05, 0xd3, 0xc2, 0x7f, 0xca, 0x32, 0xde, 0x4a, 0x57, 0xc0, 0x31, 0xb6,
+ 0x86, 0xe2, 0x94, 0x19, 0xdc, 0x6c, 0x6f, 0xa0, 0xe2, 0x5d, 0x63, 0xc3,
+ 0xef, 0x4f, 0xbc, 0x03, 0x0c, 0x1c, 0x95, 0x3f, 0xe1, 0x73, 0x4b, 0xea,
+ 0xa2, 0xce, 0x14, 0xb9, 0x99, 0x17, 0x60, 0x58, 0xe9, 0x37, 0x33, 0xc4,
+ 0x3f, 0xf5, 0xa7, 0x98, 0xc8, 0x2e, 0xa6, 0xe5, 0xdb, 0x67, 0x16, 0xff,
+ 0x35, 0x07, 0x7c, 0x50, 0xd7, 0x3e, 0xb4, 0x74, 0xe6, 0x7e, 0xe5, 0xc8,
+ 0xbf, 0x5f, 0x0f, 0xc8, 0x35, 0x8b, 0x52, 0x3e, 0x68, 0xd8, 0xb0, 0x71,
+ 0x31, 0x1f, 0xab, 0x05, 0xde, 0xad, 0x0d, 0x99, 0x9e, 0xf6, 0xff, 0x2b,
+ 0x31, 0x0d, 0xc6, 0x77, 0xdd, 0x98, 0x79, 0x18, 0x9d, 0xdd, 0x4c, 0xef,
+ 0x5f, 0xda, 0xc0, 0xcc, 0x36, 0x1b, 0x7a, 0xe6, 0xb1, 0xb8, 0x50, 0x2c,
+ 0x93, 0x0b, 0x15, 0xe0, 0xe4, 0x85, 0xe7, 0xd2, 0x24, 0xab, 0xda, 0x32,
+ 0x65, 0x55, 0x09, 0x66, 0x21, 0x0c, 0x76, 0x2c, 0xb0, 0xe1, 0xa3, 0x1a,
+ 0x91, 0x8c, 0xd3, 0xf1, 0x27, 0xa7, 0x8a, 0x1e, 0xd9, 0xa2, 0x74, 0x41,
+ 0xeb, 0xb0, 0xcc, 0x85, 0x1d, 0xdc, 0xf1, 0x62, 0x50, 0xc4, 0x00, 0x88,
+ 0x8c, 0xe2, 0xb1, 0x90, 0xbe, 0x68, 0xbd, 0x68, 0xcc, 0xfb, 0x8e, 0xde,
+ 0xc9, 0x0a, 0x51, 0x43, 0x9a, 0x13, 0x28, 0xe0, 0x5b, 0x68, 0x21, 0x4d,
+ 0xba, 0x05, 0xa3, 0xdc, 0x77, 0x4f, 0xd9, 0x92, 0xe8, 0xb6, 0x39, 0xcf,
+ 0x14, 0x76, 0x34, 0x49, 0x72, 0x86, 0xd2, 0x0c, 0xa4, 0x5a, 0x2a, 0x0e,
+ 0xc5, 0x52, 0x4b, 0x20, 0x6f, 0x33, 0xe3, 0xd7, 0xe1, 0x55, 0x4e, 0x8e,
+ 0x4b, 0xc0, 0xef, 0x7b, 0x14, 0x64, 0x91, 0xf2, 0xe2, 0xd3, 0xc9, 0x70,
+ 0xcf, 0x10, 0x2c, 0xf1, 0x04, 0x2f, 0xf5, 0xe3, 0x54, 0x69, 0x08, 0xc1,
+ 0xd3, 0x7f, 0x68, 0x67, 0x05, 0x86, 0xc7, 0x44, 0x66, 0xff, 0x6f, 0xda,
+ 0x3d, 0x36, 0x79, 0xf1, 0xad, 0x46, 0x7f, 0xc9, 0x95, 0x66, 0x12, 0x8c,
+ 0x1b, 0x53, 0xab, 0x91, 0x76, 0x97, 0xbb, 0xfd, 0x46, 0x14, 0x5a, 0xe9,
+ 0xab, 0x60, 0x8b, 0xa2, 0xd2, 0x60, 0x98, 0x21, 0x0c, 0x9a, 0xd1, 0xa1,
+ 0x35, 0x24, 0x4d, 0x01, 0x76, 0xa4, 0x95, 0x3c, 0x1f, 0x6c, 0x96, 0x44,
+ 0xd4, 0xc0, 0xd1, 0xdb, 0x92, 0x4d, 0xfe, 0x4d, 0xf1, 0x9e, 0x5a, 0x8c,
+ 0x9d, 0x92, 0x5c, 0xff, 0x50, 0x21, 0xcf, 0x59, 0x8a, 0xe4, 0x20, 0x19,
+ 0x56, 0x28, 0x20, 0x8a, 0x77, 0x7e, 0x58, 0xd1, 0xe1, 0x5e, 0x43, 0x70,
+ 0x72, 0x65, 0xfd, 0x9f, 0x07, 0x99, 0x1f, 0x88, 0x72, 0x8d, 0xf3, 0x12,
+ 0xb1, 0x54, 0x2a, 0x5b, 0xe8, 0xd7, 0x1e, 0x54, 0x36, 0xe2, 0x3f, 0x21,
+ 0xde, 0x87, 0x37, 0xa7, 0x9d, 0x68, 0xc9, 0x82, 0x57, 0xe4, 0x43, 0xe8,
+ 0xc6, 0x34, 0x19, 0x48, 0x5b, 0xfb, 0x1c, 0x3c, 0x5e, 0x8b, 0xdd, 0xb4,
+ 0x00, 0x77, 0xdf, 0x41, 0x92, 0x1d, 0x4b, 0x17, 0xb8, 0xdb, 0x13, 0x35,
+ 0x02, 0xb7, 0xa0, 0xed, 0x52, 0x05, 0xb3, 0x78, 0x0a, 0x85, 0x53, 0x0c,
+ 0xed, 0x77, 0xa1, 0x25, 0xb4, 0x95, 0xec, 0xa1, 0x86, 0xd5, 0x64, 0x44,
+ 0xc8, 0x87, 0x6d, 0x06, 0xfb, 0xdc, 0x33, 0x41, 0x06, 0xe6, 0x79, 0x2d,
+ 0xc4, 0xf8, 0xee, 0xa8, 0x38, 0x99, 0xb9, 0x4d, 0x5e, 0x29, 0x60, 0xb2,
+ 0x0b, 0xd6, 0xa3, 0xf7, 0x6f, 0x34, 0x65, 0xc3, 0xf4, 0xcc, 0x85, 0x44,
+ 0x60, 0xe5, 0xb9, 0x53, 0x3c, 0xf3, 0x99, 0x81, 0x6e, 0xa9, 0x52, 0x3a,
+ 0x0f, 0x6f, 0x36, 0xac, 0xd4, 0x6b, 0xbe, 0x00, 0xae, 0x40, 0xd4, 0xda,
+ 0x89, 0x46, 0x7f, 0x53, 0x12, 0x98, 0xbd, 0xca, 0x5a, 0x66, 0x81, 0x3f,
+ 0x55, 0x36, 0x31, 0xf0, 0x15, 0x10, 0x01, 0xac, 0x77, 0x11, 0xa9, 0x97,
+ 0x1c, 0xa0, 0xaf, 0xd1, 0x9f, 0x71, 0x76, 0x86, 0x55, 0x5a, 0x26, 0x0e,
+ 0x01, 0x22, 0x26, 0x9a, 0xf0, 0xb5, 0x51, 0xa4, 0x83, 0x46, 0xa0, 0x84,
+ 0x08, 0xd4, 0x85, 0x38, 0x64, 0x7e, 0x3b, 0xa3, 0x2f, 0x13, 0xb0, 0x5b,
+ 0x3f, 0x7c, 0x59, 0x0b, 0x26, 0x3c, 0x70, 0xe0, 0x15, 0xee, 0x17, 0x04,
+ 0x0c, 0x13, 0x19, 0xe4, 0xdf, 0xc7, 0xad, 0x5b, 0xd6, 0x53, 0x0c, 0xe8,
+ 0x3e, 0x17, 0x94, 0x7e, 0x9c, 0xa8, 0x52, 0x85, 0x79, 0x0d, 0x93, 0xe2,
+ 0xef, 0xc8, 0x2f, 0x80, 0x84, 0xf4, 0xa0, 0xba, 0x51, 0xfc, 0xf6, 0xf8,
+ 0xe4, 0xed, 0x4b, 0xbb, 0x1b, 0xcc, 0x16, 0xe7, 0x6b, 0x03, 0xb8, 0xc0,
+ 0x5c, 0xbb, 0x18, 0xe9, 0x5e, 0x37, 0x29, 0x4d, 0x45, 0x97, 0xc8, 0xf7,
+ 0xee, 0x5d, 0x41, 0x32, 0xd8, 0xd4, 0x37, 0x56, 0x5d, 0xd2, 0x6d, 0x4f,
+ 0xed, 0xfb, 0xc7, 0x1c, 0xf2, 0xac, 0x82, 0x84, 0xee, 0x3f, 0x90, 0x91,
+ 0x1a, 0x55, 0xb0, 0x54, 0xdc, 0x97, 0x25, 0x7f, 0xf7, 0x2d, 0x93, 0x0e,
+ 0x0f, 0x59, 0xa8, 0xf5, 0x18, 0x62, 0x14, 0xb2, 0x10, 0xd1, 0xf7, 0x6f,
+ 0x22, 0x65, 0x40, 0x40, 0x63, 0x4f, 0xf1, 0x93, 0x80, 0x4f, 0x8a, 0xf5,
+ 0xa6, 0x00, 0x2f, 0x00, 0x3a, 0xb6, 0x23, 0x94, 0x35, 0x74, 0x91, 0x02,
+ 0x21, 0x25, 0x46, 0x71, 0xc4, 0x7a, 0x90, 0x3a, 0xdd, 0x60, 0x67, 0x5a,
+ 0x54, 0x22, 0x67, 0xc7, 0xb8, 0xb1, 0xfa, 0xb7, 0x73, 0xc4, 0x75, 0x9b,
+ 0xc0, 0x51, 0x6f, 0x5e, 0xc1, 0x5f, 0xd6, 0x39, 0x52, 0x10, 0x5c, 0x61,
+ 0x22, 0xe4, 0xa4, 0xce, 0x59, 0xca, 0x6b, 0x54, 0x07, 0xd7, 0x79, 0x6f,
+ 0x15, 0x2e, 0x28, 0xe4, 0x4a, 0x04, 0x0f, 0x0c, 0x85, 0xb4, 0x7a, 0xfa,
+ 0x8c, 0xca, 0x7d, 0x12, 0x2d, 0xe7, 0xfc, 0xa2, 0xae, 0x11, 0xba, 0x31,
+ 0x32, 0xe8, 0xdd, 0x6b, 0x49, 0xb9, 0x82, 0xf7, 0x60, 0x3d, 0xdd, 0xee,
+ 0xd9, 0xc2, 0xb6, 0x2f, 0xbd, 0x3c, 0xd3, 0x60, 0x94, 0xed, 0x32, 0x4c,
+ 0x12, 0xf0, 0xd8, 0xf9, 0x01, 0xc8, 0x22, 0xa0, 0x60, 0xc9, 0xe4, 0xd6,
+ 0xaa, 0x71, 0x9f, 0xd5, 0x8d, 0x59, 0x0c, 0xd5, 0x54, 0x81, 0x17, 0xdd,
+ 0xe6, 0x8d, 0x85, 0xc8, 0xfa, 0x3d, 0x50, 0x09, 0x9d, 0xf5, 0xb6, 0x95,
+ 0x2d, 0x30, 0xfe, 0xeb, 0x67, 0x6e, 0x0e, 0x6e, 0xe4, 0x77, 0x9e, 0x9d,
+ 0x2e, 0x2f, 0x15, 0xbe, 0x81, 0x3c, 0x65, 0xb7, 0x2b, 0xf9, 0x9f, 0x3c,
+ 0xf0, 0xdc, 0x79, 0x14, 0xca, 0xd2, 0x55, 0x80, 0xe8, 0x9e, 0xd0, 0x10,
+ 0x9b, 0x91, 0x56, 0xf9, 0x02, 0x34, 0xb7, 0xd9, 0xa8, 0x73, 0x0d, 0x0b,
+ 0x9c, 0x71, 0xc9, 0x78, 0xf2, 0x9d, 0xc2, 0xf7, 0x71, 0xef, 0x16, 0x91,
+ 0xf8, 0x30, 0x16, 0x29, 0x61, 0x7f, 0xb3, 0xca, 0x82, 0x7c, 0xe3, 0x2f,
+ 0xdb, 0x09, 0x3d, 0xa7, 0xee, 0x7a, 0xb4, 0x4e, 0x20, 0xce, 0xf6, 0xd7,
+ 0x17, 0x13, 0xc1, 0x25, 0x09, 0x35, 0x24, 0x6d, 0xec, 0x92, 0x69, 0x3d,
+ 0xbf, 0x6f, 0xac, 0xaa, 0x2b, 0x43, 0x43, 0xfb, 0x12, 0x59, 0x18, 0xc4,
+ 0x0e, 0x1e, 0x43, 0x12, 0x5e, 0x22, 0x88, 0x3f, 0xf7, 0x2a, 0x9b, 0x1c,
+ 0x62, 0xbf, 0x2e, 0x03, 0x90, 0x5f, 0x68, 0x57, 0x4b, 0x61, 0x29, 0x57,
+ 0xcb, 0x80, 0x6a, 0x51, 0x01, 0x37, 0x96, 0xd7, 0x19, 0x8e, 0xc1, 0x3f,
+ 0x40, 0xe4, 0x6a, 0x29, 0x17, 0xe2, 0xdf, 0xea, 0xe7, 0xd8, 0xf6, 0x0d,
+ 0xa0, 0x6d, 0xb3, 0x0f, 0x12, 0x67, 0xec, 0x55, 0x91, 0xc0, 0x73, 0x03,
+ 0x90, 0xb9, 0x5f, 0xac, 0x73, 0xec, 0x33, 0x86, 0xd3, 0x25, 0x4d, 0xbc,
+ 0x93, 0x44, 0x21, 0x56, 0x4b, 0x99, 0xfa, 0xc0, 0x94, 0xc4, 0x4f, 0x87,
+ 0xcf, 0xb0, 0x3a, 0xd7, 0xd9, 0xf2, 0x38, 0x31, 0x99, 0x4d, 0x7f, 0xc3,
+ 0x24, 0x22, 0x9f, 0xe4, 0xf9, 0xbf, 0xcd, 0x31, 0x75, 0x93, 0x24, 0x19,
+ 0xb2, 0xd6, 0xee, 0xfa, 0xf9, 0x31, 0x58, 0x0c, 0x34, 0xc1, 0xf5, 0x3e,
+ 0x1f, 0x84, 0x92, 0x27, 0xb9, 0xfa, 0xda, 0xde, 0xce, 0x38, 0x17, 0x31,
+ 0x7d, 0xdc, 0x69, 0xdb, 0x3d, 0x6e, 0xe8, 0xb2, 0xe6, 0x77, 0x5a, 0xcf,
+ 0xac, 0xbf, 0x58, 0xb3, 0x59, 0x09, 0xa3, 0xf9, 0x12, 0x25, 0x50, 0x89,
+ 0xee, 0x4e, 0x74, 0x61, 0xca, 0x79, 0x25, 0xbb, 0x20, 0x6f, 0xf1, 0x2c,
+ 0x8e, 0xe6, 0xae, 0xdc, 0x76, 0x7f, 0x6c, 0xf0, 0x53, 0x8a, 0x3b, 0xe5,
+ 0x4c, 0xb3, 0xd3, 0x75, 0x8e, 0x9c, 0xbf, 0x18, 0xfc, 0xc3, 0x62, 0x16,
+ 0x2a, 0xc2, 0x1c, 0xc5, 0x94, 0xd0, 0xbc, 0x0f, 0x59, 0x41, 0xcf, 0xd1,
+ 0x07, 0xbd, 0x09, 0x00, 0xbd, 0x28, 0x3c, 0x87, 0xff, 0x3e, 0x6d, 0x42,
+ 0x2d, 0xb2, 0xbd, 0xba, 0xda, 0x71, 0x47, 0x00, 0xfd, 0xf9, 0x3e, 0x8e,
+ 0x37, 0x2d, 0x65, 0x78, 0x3a, 0xf3, 0x72, 0xda, 0x5e, 0xf1, 0xe7, 0x22,
+ 0x63, 0xb9, 0xb1, 0x1a, 0x2f, 0x79, 0xa6, 0x76, 0x23, 0x76, 0xd2, 0xf0,
+ 0xe0, 0x1f, 0xcf, 0x71, 0x27, 0x42, 0x4d, 0x30, 0xbc, 0xdf, 0x12, 0xb1,
+ 0x02, 0xf6, 0x01, 0x16, 0x93, 0x26, 0x1b, 0x85, 0x08, 0x73, 0x69, 0xeb,
+ 0x06, 0x3a, 0xf4, 0xe7, 0x25, 0xe9, 0x19, 0x3e, 0x40, 0xf7, 0x81, 0x02,
+ 0x21, 0xc8, 0xbe, 0x1f, 0xfa, 0x7d, 0x20, 0x5a, 0x02, 0x2f, 0x3b, 0x1c,
+ 0x92, 0x19, 0x39, 0xbb, 0xdf, 0xbf, 0x22, 0xac, 0x5c, 0x2a, 0x73, 0xbf,
+ 0x7b, 0x7d, 0x34, 0xc6, 0xb4, 0xc5, 0x94, 0x11, 0x9d, 0x81, 0x3d, 0xea,
+ 0xbe, 0x63, 0x60, 0xe0, 0x9b, 0x58, 0xc3, 0x4e, 0x9f, 0xb8, 0xe7, 0x55,
+ 0x5a, 0x72, 0x69, 0x91, 0x84, 0x64, 0x63, 0x94, 0x6d, 0x4c, 0x88, 0xde,
+ 0xfc, 0xcf, 0x6f, 0xa9, 0x07, 0x0a, 0x12, 0x96, 0xb6, 0xc4, 0x4c, 0x42,
+ 0x49, 0xb3, 0x75, 0x10, 0xba, 0xa1, 0xb1, 0x29, 0xe2, 0x84, 0xd4, 0x0e,
+ 0xa8, 0x77, 0x7c, 0x3e, 0x3b, 0xf4, 0xbd, 0xa1, 0x73, 0x89, 0xd5, 0x39,
+ 0x14, 0xeb, 0xdb, 0xda, 0x46, 0x70, 0x95, 0x2c, 0x7d, 0xa5, 0x54, 0x4d,
+ 0x56, 0xcc, 0x20, 0xcb, 0x59, 0x12, 0x33, 0xc1, 0xe5, 0xb8, 0x0a, 0xc5,
+ 0x26, 0xc6, 0x58, 0xdb, 0x8d, 0x98, 0x25, 0xbf, 0xf9, 0x5f, 0x93, 0x4d,
+ 0x4e, 0xb9, 0xc1, 0x2c, 0xae, 0x51, 0x2e, 0x38, 0xa2, 0x11, 0x5a, 0x87,
+ 0x0a, 0xbe, 0x68, 0xae, 0x17, 0xcc, 0xf0, 0x43, 0xd4, 0x7a, 0xc1, 0xc4,
+ 0xd4, 0x3b, 0x4c, 0x48, 0x9c, 0xb6, 0x79, 0xbf, 0xfc, 0x1b, 0xfa, 0x34,
+ 0xea, 0x60, 0x3b, 0xca, 0xda, 0x41, 0x88, 0x78, 0x00, 0xea, 0xa2, 0x9e,
+ 0x65, 0xb9, 0xaf, 0x29, 0x35, 0xcc, 0x8f, 0x23, 0x13, 0x90, 0x82, 0x5b,
+ 0x40, 0x0b, 0xa1, 0xea, 0xfd, 0xd4, 0xea, 0x84, 0xa5, 0x31, 0xd2, 0xb9,
+ 0xdf, 0x93, 0xa6, 0x05, 0xf9, 0xb1, 0xf2, 0x31, 0x3a, 0x6e, 0xbb, 0xf4,
+ 0xcc, 0x44, 0xa5, 0x83, 0x8f, 0xeb, 0x37, 0x7e, 0xbb, 0x65, 0x64, 0x13,
+ 0xa0, 0xfe, 0xb3, 0xb9, 0x19, 0x40, 0xbf, 0x6e, 0xe6, 0x4a, 0xff, 0x50,
+ 0x3d, 0x49, 0xaa, 0x72, 0xda, 0x25, 0xb0, 0x7d, 0x18, 0x58, 0xab, 0x55,
+ 0xb2, 0xf1, 0x07, 0xe5, 0xf1, 0x7e, 0x92, 0x66, 0x3d, 0x8b, 0xd7, 0x59,
+ 0xd4, 0x73, 0x87, 0xe9, 0x79, 0xd4, 0xbe, 0xda, 0x6a, 0xb1, 0x2c, 0x94,
+ 0x91, 0xfa, 0x53, 0x47, 0xb8, 0xf5, 0xe5, 0xb2, 0x71, 0xfd, 0x54, 0x6c,
+ 0x0c, 0x2c, 0xd3, 0x57, 0x92, 0x3a, 0xd0, 0xc5, 0xce, 0x91, 0xbe, 0x21,
+ 0x93, 0xf0, 0x56, 0x53, 0x84, 0x37, 0x85, 0x77, 0xab, 0xc4, 0x8a, 0x1b,
+ 0xb6, 0x85, 0xfe, 0x9c, 0x84, 0x46, 0x4a, 0x5e, 0xf9, 0x49, 0x18, 0x78,
+ 0xa6, 0x33, 0xf3, 0xd2, 0xca, 0xcf, 0xeb, 0x17, 0xcb, 0xaf, 0x11, 0x1c,
+ 0x48, 0x02, 0x71, 0x6b, 0x0b, 0x05, 0x17, 0x29, 0xc7, 0xda, 0x22, 0x83,
+ 0x40, 0xe2, 0xa9, 0xdf, 0x5d, 0x89, 0xa3, 0x03, 0x25, 0x0b, 0x10, 0x17,
+ 0x90, 0x1b, 0x55, 0xea, 0x98, 0x95, 0x98, 0xca, 0xd5, 0x74, 0x20, 0xb9,
+ 0x21, 0x4d, 0x5f, 0xbf, 0x99, 0x08, 0x45, 0x3e, 0x52, 0xb0, 0x25, 0x8e,
+ 0x5e, 0x18, 0xe2, 0xf6, 0x09, 0x16, 0x71, 0x03, 0x2b, 0xc9, 0x5f, 0x98,
+ 0x93, 0xe8, 0xd8, 0x1d, 0xff, 0x69, 0x95, 0x45, 0xfd, 0x73, 0xf5, 0x00,
+ 0x6b, 0xbf, 0x44, 0x27, 0x8a, 0x8b, 0xd8, 0xba, 0xc4, 0x07, 0x03, 0x77,
+ 0x54, 0x73, 0x79, 0x0e, 0x29, 0x04, 0x98, 0x6c, 0x0f, 0x5b, 0x82, 0x03,
+ 0x5f, 0xca, 0x80, 0x29, 0x85, 0x1b, 0xf6, 0xb3, 0x71, 0x07, 0xae, 0xca,
+ 0x2d, 0xb1, 0xdd, 0xb5, 0x9b, 0xfb, 0x36, 0x36, 0x8a, 0x79, 0x42, 0xc3,
+ 0x43, 0xa5, 0xe4, 0xda, 0x12, 0xb9, 0xf5, 0x58, 0x7b, 0xe6, 0x26, 0x02,
+ 0x13, 0xc1, 0x23, 0x0a, 0xe6, 0x4d, 0xa7, 0xac, 0x99, 0x1f, 0x06, 0xc7,
+ 0xe1, 0x54, 0x3e, 0x90, 0xc2, 0x1e, 0xdb, 0x52, 0x4a, 0x17, 0xfc, 0x1e,
+ 0xa6, 0xfc, 0x58, 0x8b, 0xc9, 0x13, 0xb2, 0xca, 0x30, 0xaa, 0xab, 0x4d,
+ 0x65, 0xf3, 0x6a, 0x93, 0x5c, 0x55, 0x4f, 0xff, 0x6c, 0xcc, 0x61, 0xc4,
+ 0x5c, 0xe8, 0x36, 0x97, 0xb3, 0xc6, 0x15, 0xe0, 0x5a, 0x8b, 0x8c, 0x33,
+ 0x0f, 0x7c, 0x32, 0xad, 0x9c, 0x98, 0x6f, 0x43, 0x52, 0x5b, 0xa8, 0x89,
+ 0x9f, 0xfb, 0x8e, 0x3b, 0x1e, 0x92, 0xaa, 0x70, 0xc2, 0x19, 0xa1, 0x58,
+ 0xd1, 0x57, 0x30, 0x21, 0xd4, 0x8e, 0xf3, 0x6a, 0x9e, 0xa3, 0x9f, 0x1f,
+ 0xa5, 0xf0, 0xf6, 0x83, 0x1e, 0x0d, 0x30, 0xf8, 0xa9, 0x49, 0x6a, 0xb4,
+ 0x7f, 0xb8, 0x23, 0x7e, 0xe8, 0x2e, 0xb2, 0xdd, 0xc0, 0x17, 0x37, 0xe5,
+ 0x3a, 0xc1, 0x3e, 0x8e, 0x21, 0x58, 0x01, 0x49, 0xf9, 0x8f, 0xfe, 0xb7,
+ 0xe8, 0x4d, 0xdd, 0x8b, 0xaa, 0x83, 0x4c, 0x5e, 0xeb, 0x59, 0x25, 0x1e,
+ 0x07, 0x58, 0x20, 0x17, 0xce, 0x96, 0xc3, 0x39, 0xd3, 0x17, 0x8e, 0x4c,
+ 0xf9, 0x46, 0xe2, 0x4d, 0x9a, 0x30, 0xf9, 0xd5, 0xbf, 0x40, 0xb0, 0x2c,
+ 0xa9, 0x0e, 0x41, 0x32, 0xe1, 0xd4, 0x03, 0x63, 0xe7, 0x32, 0x18, 0x29,
+ 0xb6, 0x21, 0x47, 0xdc, 0xda, 0x8a, 0xb5, 0x3c, 0xb3, 0x60, 0xba, 0x4c,
+ 0x84, 0x6e, 0x0c, 0xf4, 0xd3, 0xbc, 0x75, 0x53, 0x8e, 0x3a, 0x2b, 0xfd,
+ 0x9a, 0x08, 0x77, 0x1e, 0xc7, 0x63, 0x90, 0xb7, 0x94, 0x1e, 0xac, 0xb7,
+ 0x96, 0xf1, 0x38, 0x17, 0x0c, 0xa8, 0x29, 0x77, 0x4e, 0xa9, 0x73, 0x0a,
+ 0x78, 0x75, 0xca, 0xeb, 0x23, 0xf3, 0xc8, 0x47, 0xad, 0x87, 0x5b, 0x82,
+ 0x09, 0x78, 0x9a, 0x67, 0x4d, 0x00, 0xa7, 0xa2, 0x13, 0x07, 0x9e, 0xcc,
+ 0x12, 0xd8, 0x9e, 0x6a, 0x69, 0xeb, 0xe8, 0x74, 0xd5, 0x2e, 0x82, 0x75,
+ 0x26, 0xbd, 0xec, 0x47, 0xbb, 0x15, 0x2d, 0x17, 0xe0, 0x8d, 0x37, 0x53,
+ 0x57, 0x0d, 0x17, 0x9f, 0xd3, 0x8f, 0x75, 0xb8, 0x3f, 0x6c, 0xff, 0xec,
+ 0x76, 0x95, 0xaa, 0x4e, 0xaa, 0xa5, 0xee, 0xb7, 0x94, 0x6b, 0xbd, 0x8e,
+ 0x51, 0x6f, 0xb6, 0x21, 0xeb, 0x78, 0x5b, 0x75, 0x57, 0x6b, 0x4f, 0xf3,
+ 0xfd, 0xe2, 0x29, 0xef, 0x6d, 0xc4, 0xd5, 0xd7, 0x60, 0x18, 0x47, 0xee,
+ 0xa0, 0xfd, 0xb0, 0xd1, 0xc0, 0xf8, 0x4f, 0x77, 0x5f, 0xfe, 0x26, 0x15,
+ 0x58, 0x59, 0xee, 0x27, 0x3e, 0x93, 0x5e, 0xbe, 0x7a, 0x19, 0xeb, 0x46,
+ 0xdb, 0x39, 0xc3, 0x8a, 0x18, 0x52, 0xee, 0xed, 0x6c, 0xff, 0x66, 0x87,
+ 0xea, 0x6d, 0x0e, 0xfc, 0x35, 0xe1, 0xe6, 0x1c, 0x21, 0x64, 0xeb, 0x87,
+ 0x7c, 0x63, 0x4c, 0x28, 0x65, 0xbf, 0xcc, 0x81, 0xc3, 0x38, 0x87, 0x18,
+ 0x3b, 0x62, 0x8c, 0xce, 0x3d, 0x42, 0x99, 0x04, 0x6b, 0x54, 0xea, 0xd8,
+ 0xa2, 0x45, 0x84, 0xb7, 0x68, 0xc2, 0x00, 0xcc, 0x83, 0x73, 0xf1, 0xe3,
+ 0xfe, 0x32, 0x96, 0xcd, 0xef, 0xc9, 0x56, 0xe8, 0xe8, 0xe4, 0x93, 0xc1,
+ 0xae, 0x21, 0x9a, 0xe3, 0x66, 0xc7, 0xed, 0x31, 0x86, 0xb2, 0x83, 0xa7,
+ 0x38, 0x9e, 0x7d, 0xe6, 0x26, 0xf1, 0x84, 0x58, 0x41, 0x4a, 0x8a, 0xf6,
+ 0x1e, 0x90, 0x23, 0xf3, 0x8d, 0x54, 0x26, 0x8c, 0x49, 0x38, 0x81, 0x57,
+ 0xc3, 0xd5, 0x91, 0x6b, 0x92, 0x2e, 0x10, 0x2d, 0x56, 0x4c, 0x21, 0xe7,
+ 0x52, 0xc8, 0x8a, 0x15, 0x6b, 0xb2, 0xe8, 0x5f, 0xfd, 0x92, 0x59, 0xd0,
+ 0xf4, 0x28, 0xc8, 0x7d, 0xce, 0xfe, 0xb2, 0x3f, 0x1e, 0xd8, 0xfa, 0x0c,
+ 0xd1, 0x8b, 0xff, 0xff, 0x08, 0x67, 0xd7, 0x92, 0x2c, 0x0f, 0x90, 0x65,
+ 0xf2, 0xb9, 0xaf, 0xdd, 0x04, 0x0e, 0xf4, 0x01, 0xd0, 0x0b, 0xf9, 0x0d,
+ 0x0a, 0x36, 0xf5, 0x4b, 0x7c, 0x91, 0x44, 0x74, 0x1b, 0xef, 0x91, 0x68,
+ 0x86, 0x65, 0x12, 0x54, 0xb3, 0x4b, 0x55, 0xab, 0xc1, 0x74, 0x8a, 0x0b,
+ 0x5c, 0xd3, 0xc3, 0x4e, 0xcd, 0x97, 0x65, 0xcd, 0x25, 0xb2, 0x30, 0x72,
+ 0x84, 0x53, 0xa6, 0xde, 0x43, 0x8e, 0x8b, 0x59, 0x3c, 0x4a, 0x21, 0x22,
+ 0x7f, 0x64, 0xb8, 0xb6, 0x83, 0x14, 0x62, 0xbe, 0x15, 0xe8, 0xa4, 0x1b,
+ 0xc1, 0xe2, 0xf9, 0xe4, 0x0c, 0xc4, 0x9f, 0x21, 0x9e, 0x6d, 0x55, 0x46,
+ 0x12, 0x89, 0xf9, 0x6f, 0x6f, 0x93, 0x65, 0x04, 0x40, 0xf3, 0x28, 0x68,
+ 0x53, 0xc2, 0x99, 0xa9, 0x03, 0x75, 0xfb, 0xd6, 0xbe, 0x09, 0xcf, 0x3f,
+ 0x2f, 0x76, 0xd0, 0x52, 0x45, 0x2e, 0x63, 0xdf, 0x85, 0x2d, 0xb5, 0xfa,
+ 0xc5, 0x78, 0xdb, 0xda, 0x53, 0x67, 0x1d, 0x18, 0x1f, 0x89, 0xb5, 0x5a,
+ 0x41, 0x97, 0xd2, 0xa7, 0x09, 0xcd, 0x16, 0x73, 0xf0, 0x9a, 0x89, 0x01,
+ 0x31, 0x30, 0x9f, 0xb5, 0x43, 0x59, 0x9f, 0xf6, 0xb2, 0xc2, 0x59, 0x57,
+ 0x57, 0x85, 0xa1, 0xc1, 0xba, 0xe7, 0x18, 0xd5, 0x4f, 0xce, 0xc9, 0xe9,
+ 0x00, 0xbc, 0xf8, 0x52, 0xbe, 0x40, 0x3c, 0x3b, 0x38, 0x1b, 0xc9, 0x29,
+ 0x67, 0x06, 0x48, 0xd7, 0xf6, 0xb0, 0xd0, 0x21, 0x90, 0x30, 0xf7, 0x67,
+ 0xd8, 0xe5, 0x18, 0x33, 0x1f, 0x39, 0x4d, 0xaf, 0x97, 0x66, 0x91, 0x7d,
+ 0xbb, 0xfe, 0x56, 0xb4, 0x77, 0x0b, 0xa0, 0xd5, 0xe5, 0x1c, 0x70, 0x05,
+ 0x5c, 0x82, 0xd7, 0x6d, 0x8b, 0x0a, 0xfc, 0x03, 0x91, 0x4d, 0xd0, 0x12,
+ 0xa8, 0x0e, 0xf5, 0x32, 0xae, 0xf7, 0x14, 0x64, 0x42, 0xf6, 0xfb, 0x2a,
+ 0x7f, 0x26, 0x7b, 0xdc, 0xc7, 0xb0, 0xae, 0xa9, 0xe7, 0xe6, 0x28, 0x58,
+ 0xdc, 0xd0, 0x0b, 0xf2, 0xe3, 0xcc, 0x6f, 0x28, 0x31, 0x95, 0x1b, 0xb1,
+ 0x0b, 0xb5, 0xaa, 0xf9, 0xf1, 0x8b, 0x8d, 0x07, 0xe1, 0x9b, 0xb4, 0xc3,
+ 0x6c, 0xd2, 0xec, 0xc7, 0x45, 0x6f, 0xec, 0xc3, 0x67, 0xc7, 0x09, 0x16,
+ 0x10, 0xd3, 0x79, 0x29, 0xbd, 0x26, 0xbb, 0xa8, 0x85, 0xae, 0xb2, 0x76,
+ 0xe6, 0x0c, 0x35, 0xac, 0xa8, 0xe7, 0xab, 0xe1, 0xde, 0x11, 0x99, 0x56,
+ 0xcd, 0x80, 0x1d, 0x1b, 0x16, 0x6f, 0xe2, 0xef, 0x02, 0x21, 0x2b, 0x67,
+ 0xdd, 0xcf, 0x9c, 0x88, 0x25, 0xfe, 0x15, 0xa2, 0xd8, 0x3e, 0x8d, 0x37,
+ 0xc8, 0xdd, 0xbb, 0xf2, 0xfb, 0x14, 0x95, 0xae, 0x93, 0x30, 0x3b, 0x86,
+ 0x2a, 0x16, 0x92, 0x6d, 0xa0, 0xd8, 0xc4, 0xa5, 0x3e, 0xa7, 0xa8, 0x93,
+ 0xe3, 0x56, 0xce, 0x18, 0x5b, 0x3e, 0x5b, 0xc4, 0x30, 0xc7, 0x85, 0x1c,
+ 0xe2, 0x9d, 0x1f, 0x49, 0x33, 0xee, 0x6b, 0xb1, 0xfd, 0x77, 0x75, 0x2f,
+ 0x5b, 0x33, 0xa3, 0x15, 0xec, 0xd9, 0xf1, 0x3d, 0x06, 0x05, 0xf7, 0x4b,
+ 0x48, 0x51, 0x3c, 0x3e, 0x3f, 0x97, 0x4d, 0xc0, 0xb8, 0xd9, 0x9b, 0xff,
+ 0x3e, 0xc5, 0xd7, 0x94, 0x89, 0x38, 0x5a, 0x69, 0xf5, 0x0c, 0x02, 0x98,
+ 0x6e, 0x28, 0xc2, 0xa7, 0x9b, 0x9a, 0x42, 0x93, 0x02, 0x89, 0x4a, 0x67,
+ 0x43, 0xfc, 0x78, 0xb7, 0x37, 0x24, 0x0a, 0x62, 0x7e, 0xdf, 0x63, 0x2b,
+ 0xef, 0x79, 0x69, 0xcc, 0xd9, 0xe1, 0xab, 0x3f, 0x8e, 0x4d, 0xa5, 0xd0,
+ 0xca, 0x24, 0x05, 0xce, 0x35, 0x4f, 0x34, 0xbe, 0xff, 0x12, 0x52, 0x44,
+ 0x43, 0x35, 0x63, 0x64, 0x4d, 0x22, 0x6a, 0x40, 0x89, 0x92, 0xa7, 0x04,
+ 0x30, 0x29, 0x8a, 0x70, 0x34, 0xda, 0x2d, 0xf5, 0x78, 0xf5, 0x0c, 0x1c,
+ 0x0e, 0xd8, 0x06, 0x49, 0x5c, 0xaf, 0x68, 0xd0, 0x18, 0x0a, 0xd9, 0xe1,
+ 0x17, 0x1a, 0x84, 0x03, 0x71, 0x4a, 0x43, 0xb5, 0x34, 0xdd, 0xd7, 0xd7,
+ 0x84, 0xc6, 0xa6, 0x8d, 0x06, 0xd5, 0x15, 0xfa, 0x19, 0x8c, 0xe7, 0xac,
+ 0x1c, 0x4a, 0x95, 0x48, 0xf2, 0xb3, 0xcd, 0xf5, 0xd3, 0xfe, 0x3e, 0x28,
+ 0x07, 0x90, 0x57, 0x95, 0x7d, 0x5c, 0xda, 0x29, 0x5b, 0x42, 0x70, 0x58,
+ 0xdb, 0x6c, 0x69, 0xba, 0x88, 0xa6, 0x4f, 0xc2, 0xb4, 0xf7, 0x33, 0x55,
+ 0xd9, 0x62, 0x6f, 0x63, 0x65, 0x6a, 0x1f, 0x77, 0xc7, 0xec, 0xd9, 0x99,
+ 0x6b, 0xed, 0xd8, 0x9b, 0x09, 0x9a, 0xca, 0x07, 0x73, 0xab, 0xe5, 0x61,
+ 0x5b, 0x49, 0x4a, 0xa4, 0xff, 0xff, 0x18, 0x43, 0x73, 0xde, 0xdc, 0x65,
+ 0x47, 0xcb, 0xc2, 0x37, 0xcc, 0x10, 0xe7, 0xc4, 0xa1, 0x3d, 0xb5, 0x11,
+ 0xce, 0x1f, 0xd7, 0xb3, 0x4c, 0x46, 0x4a, 0xa1, 0x6d, 0x2f, 0xd6, 0x7e,
+ 0x30, 0xa7, 0x0e, 0x31, 0x71, 0xbc, 0xfb, 0xf3, 0xf3, 0x97, 0x48, 0xf9,
+ 0x38, 0xf4, 0x76, 0x22, 0x78, 0x4e, 0x90, 0x9f, 0x54, 0x2d, 0xdc, 0x23,
+ 0x05, 0xc2, 0x3e, 0xf1, 0x83, 0x4c, 0x3b, 0x42, 0x00, 0xdc, 0x5b, 0x1e,
+ 0x2f, 0x18, 0x6b, 0x45, 0xe3, 0x08, 0xd0, 0xa7, 0xad, 0x7e, 0xad, 0x85,
+ 0xde, 0x3f, 0x34, 0x5b, 0xb4, 0x64, 0xbc, 0x50, 0x4f, 0x26, 0xb1, 0x21,
+ 0x98, 0xb0, 0xc1, 0x9a, 0x7d, 0x47, 0xa9, 0xa3, 0x49, 0x8d, 0xbb, 0xad,
+ 0xfc, 0x65, 0xc7, 0xeb, 0x12, 0xdd, 0xdb, 0x38, 0x2d, 0xb3, 0xa1, 0x42,
+ 0x55, 0xa3, 0x7e, 0x1a, 0x78, 0x98, 0xc7, 0x0f, 0x63, 0x39, 0xc7, 0x72,
+ 0x04, 0x61, 0x3a, 0xf0, 0x77, 0x20, 0xb7, 0xf6, 0x59, 0x3c, 0x55, 0x2c,
+ 0x8f, 0x72, 0x54, 0x66, 0x49, 0x41, 0xb6, 0xd9, 0x99, 0x53, 0x06, 0x19,
+ 0x95, 0xfc, 0x28, 0x7c, 0x50, 0x8b, 0xcd, 0x9c, 0xc3, 0x92, 0xdf, 0x4d,
+ 0xa3, 0x68, 0x9a, 0x5a, 0xaa, 0x06, 0x32, 0x9f, 0xff, 0xdf, 0xcc, 0x11,
+ 0x6e, 0x9e, 0x33, 0xb1, 0x5d, 0x57, 0x3f, 0xea, 0xf6, 0xfb, 0x01, 0xa1,
+ 0xf5, 0xa1, 0x77, 0x0d, 0x98, 0x34, 0x5c, 0xd0, 0xa1, 0xd6, 0x94, 0x6d,
+ 0x8d, 0xb1, 0xc9, 0xbe, 0x4f, 0x43, 0x48, 0xca, 0xe6, 0x73, 0xad, 0xc6,
+ 0xee, 0x7a, 0x36, 0x6c, 0xae, 0xbb, 0x96, 0x96, 0x61, 0xfa, 0xcb, 0x07,
+ 0xd6, 0x09, 0xba, 0xff, 0x11, 0xe9, 0x08, 0x82, 0x9a, 0xb8, 0x80, 0x2e,
+ 0x22, 0x26, 0x43, 0xdb, 0xd4, 0xef, 0xd6, 0xe1, 0xcc, 0xbc, 0x88, 0x53,
+ 0x1d, 0xa3, 0x57, 0x7a, 0x99, 0xab, 0xe6, 0x9e, 0x0b, 0x43, 0x03, 0x11,
+ 0x68, 0x4c, 0xa5, 0xde, 0x07, 0xc1, 0x6c, 0xb1, 0x85, 0xae, 0x81, 0xb8,
+ 0x3b, 0x34, 0x5e, 0xa8, 0x62, 0x75, 0x5e, 0x9c, 0x28, 0x02, 0xf7, 0x76,
+ 0xda, 0x1a, 0x82, 0xe2, 0xba, 0xb5, 0x4a, 0xd8, 0xaa, 0x90, 0xf9, 0x51,
+ 0x7e, 0xa3, 0x49, 0xb3, 0x86, 0xc1, 0x5d, 0xe0, 0x52, 0x01, 0xd1, 0xfd,
+ 0x71, 0x30, 0xe4, 0x19, 0x25, 0xd3, 0x28, 0x15, 0x15, 0xaa, 0x23, 0x6e,
+ 0x97, 0xfa, 0x77, 0xb6, 0xd2, 0x6e, 0x7e, 0x93, 0x63, 0x3f, 0x75, 0x24,
+ 0x57, 0xf3, 0x9f, 0x18, 0x46, 0xd5, 0x09, 0x89, 0x55, 0xa3, 0xfd, 0x6e,
+ 0xfd, 0x40, 0xb4, 0xe7, 0x5d, 0x21, 0x44, 0x6e, 0x52, 0x62, 0x66, 0xae,
+ 0xe2, 0xeb, 0x93, 0x4c, 0x17, 0x12, 0x48, 0x88, 0xf6, 0x59, 0x81, 0xfb,
+ 0x88, 0x5c, 0x21, 0x14, 0x96, 0xeb, 0x70, 0xcf, 0xdc, 0xed, 0xc0, 0x56,
+ 0xaf, 0x83, 0x57, 0x8b, 0x6f, 0x65, 0x65, 0x65, 0x9e, 0x8d, 0x90, 0x92,
+ 0x91, 0x08, 0x08, 0x9e, 0x71, 0x3b, 0x14, 0x97, 0xd6, 0xc8, 0xdb, 0x17,
+ 0x1e, 0xef, 0x5c, 0x95, 0x87, 0xd0, 0x6f, 0x84, 0x66, 0xa0, 0x01, 0x97,
+ 0xd9, 0xfb, 0x24, 0x65, 0x41, 0xfe, 0x1c, 0x95, 0x59, 0xb5, 0xe6, 0x52,
+ 0x05, 0x7a, 0xac, 0xeb, 0xd6, 0xff, 0xcd, 0xb6, 0xde, 0x7f, 0xd4, 0xd1,
+ 0x1c, 0x04, 0x8c, 0x01, 0x8d, 0x54, 0xac, 0xcd, 0x00, 0xf6, 0x40, 0xfc,
+ 0x6e, 0xbe, 0x9a, 0x87, 0x51, 0xf6, 0x77, 0x45, 0x5a, 0xfc, 0x4c, 0x5b,
+ 0x0d, 0x4b, 0xa1, 0xb7, 0x98, 0xde, 0x78, 0xd0, 0x61, 0x92, 0x97, 0xfe,
+ 0x9a, 0x08, 0xf4, 0x07, 0xd6, 0xee, 0x4c, 0x2b, 0x43, 0x22, 0xde, 0x43,
+ 0xab, 0x59, 0x35, 0x1e, 0x0e, 0xb4, 0x95, 0x05, 0xf7, 0x84, 0x6a, 0xa8,
+ 0x2b, 0x93, 0x6e, 0xb5, 0xb0, 0xf6, 0xd8, 0x12, 0xab, 0x6b, 0x7c, 0x65,
+ 0x5b, 0x74, 0x4e, 0xce, 0xf9, 0x45, 0x57, 0xa1, 0x6a, 0xaa, 0x1a, 0xcd,
+ 0x08, 0x67, 0x57, 0x9c, 0x4f, 0x1f, 0x5d, 0x95, 0xde, 0x4c, 0x64, 0xda,
+ 0x96, 0x69, 0x8c, 0x13, 0x7c, 0x54, 0x2a, 0x15, 0x4e, 0x12, 0xf1, 0x86,
+ 0xa2, 0x8c, 0x9f, 0xcf, 0x97, 0x15, 0x3a, 0xd3, 0xaa, 0x4b, 0x1c, 0x5f,
+ 0xfc, 0xdd, 0x9a, 0xb4, 0xc4, 0xaf, 0x56, 0xc8, 0x70, 0xbe, 0x05, 0x0b,
+ 0x11, 0xfa, 0xff, 0x35, 0x50, 0xd5, 0x03, 0xfb, 0x33, 0xa5, 0x31, 0xf3,
+ 0x04, 0x57, 0xed, 0x42, 0x22, 0xde, 0xe3, 0x80, 0xd8, 0xea, 0xc9, 0xb3,
+ 0xa4, 0xed, 0x73, 0x64, 0xc9, 0xa8, 0x62, 0xaf, 0x30, 0xb1, 0xe4, 0x43,
+ 0x9b, 0xc5, 0x3f, 0x96, 0xa5, 0x67, 0x6d, 0xb5, 0x1e, 0xf0, 0xb0, 0x47,
+ 0x45, 0x8c, 0x6d, 0xd1, 0xf5, 0x96, 0x12, 0xd9, 0x83, 0x00, 0xc8, 0xb0,
+ 0x38, 0xa6, 0xb4, 0x4d, 0x60, 0xd2, 0x9f, 0x3f, 0x31, 0xa0, 0x5a, 0x93,
+ 0x9d, 0xc4, 0x9d, 0xf9, 0xbc, 0xfd, 0x9f, 0xa5, 0xdd, 0x20, 0x96, 0x37,
+ 0x99, 0xd9, 0x80, 0xf3, 0xa3, 0x5a, 0xa6, 0x8f, 0x24, 0x0e, 0x9a, 0xd7,
+ 0x81, 0x00, 0x22, 0xbc, 0x9a, 0xcd, 0x3b, 0x1e, 0x67, 0x64, 0xbd, 0x1a,
+ 0xda, 0x26, 0xb4, 0x47, 0xb0, 0x18, 0x99, 0xad, 0xb4, 0x72, 0xe0, 0x63,
+ 0x35, 0x2b, 0xe3, 0x19, 0xe6, 0x42, 0xeb, 0x42, 0x97, 0x0f, 0x8c, 0x0a,
+ 0xae, 0x36, 0xff, 0x34, 0xfd, 0x1b, 0x14, 0x06, 0xbe, 0xa6, 0xe0, 0xf3,
+ 0x5f, 0x02, 0x0a, 0x8b, 0xed, 0x4d, 0xa9, 0xa6, 0xb4, 0xc9, 0x66, 0x86,
+ 0x90, 0xa1, 0xa5, 0x02, 0x2d, 0xce, 0x97, 0xcf, 0xc6, 0x5c, 0xea, 0x56,
+ 0x63, 0x13, 0x5c, 0xba, 0x9b, 0xb1, 0x9b, 0xfe, 0xd0, 0xef, 0xb7, 0x43,
+ 0x0a, 0x60, 0xbd, 0x78, 0xfe, 0xd2, 0x28, 0xca, 0xf5, 0xe0, 0x85, 0x0d,
+ 0x02, 0x5d, 0x2e, 0x6a, 0xcb, 0x83, 0xae, 0x75, 0xe4, 0xba, 0xa4, 0x8b,
+ 0xa2, 0x6b, 0xad, 0x2f, 0x47, 0xb4, 0xee, 0x6b, 0xcc, 0x5a, 0x21, 0xa1,
+ 0xac, 0xad, 0x9f, 0x12, 0x6e, 0x95, 0x75, 0xb2, 0x03, 0x54, 0x7c, 0xa0,
+ 0xd7, 0x80, 0xeb, 0x91, 0x04, 0xd4, 0xc0, 0x4a, 0x51, 0x75, 0x3c, 0x25,
+ 0xf1, 0x89, 0x32, 0x63, 0x47, 0xaf, 0x63, 0x68, 0xff, 0x05, 0x31, 0x1f,
+ 0x77, 0xce, 0xd7, 0x9b, 0xf7, 0x38, 0xd4, 0x30, 0xf8, 0x83, 0xf0, 0x68,
+ 0xff, 0x8f, 0xf5, 0x25, 0x78, 0xa3, 0x99, 0xcd, 0xa2, 0x11, 0x0e, 0x26,
+ 0xa0, 0x7b, 0x22, 0xf5, 0xdc, 0xdd, 0x17, 0x94, 0x00, 0x5c, 0xde, 0xb2,
+ 0xe2, 0x37, 0x00, 0x94, 0x57, 0x6f, 0xc5, 0x65, 0xa1, 0xfe, 0xdd, 0xa1,
+ 0x51, 0x6d, 0x38, 0xb2, 0xe4, 0xd4, 0x62, 0xfc, 0xf1, 0xc8, 0xf2, 0x60,
+ 0x08, 0x2e, 0x58, 0xb0, 0x43, 0xa5, 0xc6, 0xe0, 0x9f, 0xbb, 0xff, 0xb5,
+ 0xc0, 0x36, 0xca, 0xf9, 0x48, 0x7a, 0x87, 0x15, 0xf6, 0xad, 0x09, 0x3f,
+ 0x20, 0xe8, 0x11, 0x89, 0xcd, 0xe5, 0x8a, 0x09, 0x35, 0x49, 0xee, 0x31,
+ 0xa4, 0x7a, 0xe6, 0x37, 0xe1, 0x6f, 0x26, 0x61, 0xd6, 0xd1, 0xa4, 0x8f,
+ 0xa2, 0x7b, 0x43, 0x83, 0x6d, 0xcc, 0x50, 0x4a, 0x88, 0x5e, 0x14, 0x8b,
+ 0xc2, 0xb4, 0x40, 0x2f, 0xe0, 0xdf, 0xcb, 0xb2, 0xf5, 0x44, 0x12, 0x91,
+ 0x31, 0x04, 0x84, 0xce, 0xda, 0x58, 0xc8, 0x74, 0x7b, 0x2e, 0xaf, 0xf7,
+ 0x8b, 0xcc, 0x53, 0x9d, 0xd0, 0x2a, 0x6c, 0x70, 0xf7, 0xe4, 0x80, 0xd1,
+ 0x94, 0xc0, 0x51, 0x27, 0xe3, 0x9a, 0xeb, 0x26, 0x7d, 0xc7, 0xc5, 0x96,
+ 0x63, 0x1e, 0x2d, 0x22, 0xd9, 0x53, 0x79, 0x0b, 0xf5, 0x1d, 0xbf, 0x94,
+ 0xd8, 0xe1, 0xea, 0xe6, 0x84, 0x46, 0x5c, 0xf3, 0x55, 0x59, 0xa4, 0x06,
+ 0x96, 0xc3, 0x6f, 0x53, 0xac, 0x76, 0xff, 0x8a, 0xa5, 0xc4, 0x6a, 0xa6,
+ 0xdd, 0xba, 0x81, 0x52, 0xe1, 0x25, 0xbb, 0xad, 0x29, 0x84, 0xa6, 0x6e,
+ 0x3d, 0x16, 0xdc, 0xcc, 0xd7, 0x2a, 0x9b, 0x3e, 0x20, 0x42, 0x62, 0x47,
+ 0xcc, 0xed, 0x5f, 0xea, 0xd7, 0x71, 0x4c, 0x39, 0xf5, 0x5e, 0x99, 0x3c,
+ 0xaa, 0x2d, 0xbe, 0x0f, 0xb2, 0xbd, 0x60, 0x55, 0xd8, 0x3f, 0xbf, 0xc0,
+ 0x91, 0xf8, 0xe9, 0xd1, 0x64, 0x24, 0xed, 0x62, 0xb1, 0xd7, 0xb6, 0x8f,
+ 0xba, 0x3f, 0x4e, 0x8d, 0xe5, 0xea, 0x84, 0xbe, 0x2e, 0x52, 0xb9, 0x59,
+ 0xf7, 0xc6, 0x36, 0xa3, 0xb6, 0xce, 0xbe, 0x4e, 0xbc, 0x19, 0xc7, 0x6e,
+ 0xd9, 0x47, 0x6e, 0x21, 0x91, 0xf3, 0x0e, 0xaa, 0x6f, 0xe0, 0x95, 0x58,
+ 0xb1, 0x4f, 0x41, 0xcb, 0x37, 0x3d, 0x67, 0x6a, 0x48, 0x9c, 0xda, 0xfe,
+ 0x88, 0x05, 0xc4, 0x67, 0x3d, 0x75, 0x5d, 0x2b, 0xa2, 0x2c, 0x93, 0x17,
+ 0x86, 0x3f, 0xe0, 0x33, 0xc5, 0x84, 0x24, 0x53, 0x45, 0x5e, 0xff, 0x9b,
+ 0xef, 0x77, 0x20, 0xe2, 0x30, 0xd4, 0xa9, 0xbd, 0xce, 0xa3, 0x43, 0x4e,
+ 0xe1, 0xe6, 0xf9, 0x40, 0x7b, 0x37, 0x72, 0xcf, 0x0e, 0x86, 0x9d, 0xb1,
+ 0x16, 0xf6, 0x2b, 0x57, 0x85, 0x00, 0xfe, 0x0c, 0x21, 0x46, 0x15, 0x75,
+ 0x39, 0xaa, 0xcf, 0xa6, 0x65, 0x79, 0x73, 0x0a, 0x7b, 0x4c, 0x66, 0x28,
+ 0xfd, 0x8f, 0xdd, 0xd0, 0x5c, 0xe3, 0x47, 0xca, 0x2f, 0x67, 0x57, 0xef,
+ 0x5b, 0x67, 0xeb, 0x52, 0x40, 0x31, 0xa1, 0xf7, 0xf1, 0x1f, 0x1f, 0xcf,
+ 0x6c, 0x63, 0x18, 0x64, 0x3e, 0xbb, 0xdd, 0xbc, 0xe9, 0x55, 0xcf, 0x22,
+ 0x95, 0xd8, 0xc3, 0xec, 0x64, 0x74, 0x14, 0x4a, 0x1f, 0x08, 0x60, 0xa9,
+ 0xdf, 0xb0, 0x7b, 0xe1, 0x7f, 0x22, 0x64, 0xe5, 0xed, 0xab, 0x06, 0xfa,
+ 0x3b, 0x9c, 0xf3, 0xfa, 0xc2, 0x77, 0xd7, 0xcb, 0xcc, 0xba, 0xdd, 0x22,
+ 0x61, 0x46, 0x0d, 0xba, 0x4a, 0x1b, 0x43, 0xee, 0x14, 0x89, 0x29, 0xe1,
+ 0xa5, 0x5f, 0x5f, 0x59, 0x08, 0x8f, 0xdd, 0xee, 0x6b, 0x24, 0x33, 0x76,
+ 0x82, 0xcb, 0x58, 0xab, 0xea, 0xb8, 0xc3, 0x2a, 0x97, 0x2d, 0x85, 0x71,
+ 0x26, 0x8b, 0x19, 0x12, 0x1a, 0xd0, 0x70, 0xb5, 0xd4, 0xf5, 0x9b, 0xb9,
+ 0xf2, 0xca, 0x72, 0xc3, 0x12, 0x94, 0x96, 0x91, 0x00, 0xbd, 0xce, 0x26,
+ 0xeb, 0x9a, 0xce, 0x33, 0x77, 0xfc, 0x45, 0x6b, 0x14, 0x61, 0x48, 0xf4,
+ 0xab, 0xe5, 0xae, 0x8a, 0x2a, 0x4d, 0x6c, 0x5b, 0xce, 0xfc, 0xa8, 0x16,
+ 0xe5, 0xef, 0xa5, 0x3d, 0x48, 0xef, 0x4c, 0x92, 0xe7, 0x26, 0x53, 0x16,
+ 0xc6, 0x5a, 0xb0, 0x57, 0x91, 0x38, 0xa8, 0x67, 0x52, 0x7f, 0x87, 0x57,
+ 0xf9, 0x74, 0xf4, 0xe2, 0xef, 0xbf, 0xd4, 0x3d, 0xb4, 0xe9, 0x4f, 0x25,
+ 0xd3, 0x59, 0xce, 0xc7, 0x14, 0xfd, 0x23, 0x4b, 0x22, 0x63, 0xea, 0x0d,
+ 0xe7, 0xdf, 0x1c, 0xbc, 0xaa, 0x61, 0xa3, 0xd1, 0x14, 0xf8, 0x96, 0x43,
+ 0xf5, 0xe6, 0xaf, 0x57, 0x46, 0xa1, 0xc3, 0xe5, 0x7f, 0x53, 0xed, 0x71,
+ 0x62, 0x59, 0x65, 0xfa, 0x99, 0x9f, 0x9f, 0x63, 0xf1, 0xfa, 0x64, 0x89,
+ 0xe1, 0xcf, 0xeb, 0x0c, 0xdb, 0x88, 0x7a, 0xaa, 0x4c, 0x69, 0x4c, 0x50,
+ 0x73, 0x97, 0x9a, 0xee, 0xd2, 0x39, 0x1f, 0xbe, 0x22, 0x33, 0x51, 0x8a,
+ 0xb2, 0xf0, 0xe6, 0x0f, 0x85, 0xe3, 0x4f, 0x98, 0x22, 0x94, 0x2f, 0x15,
+ 0x67, 0x92, 0xd2, 0x44, 0x35, 0x87, 0xa1, 0xdb, 0x9a, 0x13, 0x60, 0x71,
+ 0x50, 0x9c, 0x49, 0xf4, 0xbd, 0x8c, 0xfd, 0x17, 0x4e, 0x43, 0x4e, 0xb5,
+ 0x48, 0x40, 0x70, 0x02, 0xd7, 0x51, 0x24, 0x01, 0x3f, 0xdf, 0xe5, 0x38,
+ 0x3a, 0x2f, 0xb1, 0x64, 0x1a, 0x21, 0x38, 0xa8, 0x7d, 0x95, 0x97, 0x9d,
+ 0x44, 0x74, 0x4b, 0xb2, 0x23, 0x41, 0x09, 0x95, 0x58, 0xc5, 0x14, 0x8c,
+ 0x89, 0x03, 0xb3, 0x83, 0x3d, 0x82, 0xe4, 0x3a, 0xa4, 0x95, 0x2d, 0x6e,
+ 0x6f, 0x7a, 0x49, 0x8b, 0x49, 0x4e, 0xb9, 0x5b, 0x5a, 0x8f, 0x9d, 0x6e,
+ 0xf5, 0x1d, 0x11, 0x93, 0x47, 0xfd, 0xa5, 0xe4, 0xaa, 0x91, 0xb1, 0x0c,
+ 0x8e, 0xab, 0xf6, 0xff, 0x14, 0x14, 0xa4, 0xfa, 0x4d, 0x95, 0x1a, 0x44,
+ 0x99, 0xe7, 0xf4, 0xbd, 0xdc, 0x4c, 0x8e, 0x1e, 0xc6, 0x0c, 0xc0, 0x93,
+ 0x7d, 0xf2, 0x8c, 0x50, 0xba, 0x8a, 0xcf, 0x2d, 0xd4, 0xc9, 0xd8, 0x05,
+ 0xe8, 0x9b, 0xc3, 0x3c, 0x44, 0xb4, 0xad, 0x28, 0x3e, 0x76, 0x11, 0xf1,
+ 0x3e, 0x41, 0x98, 0x51, 0xa1, 0x18, 0x99, 0x37, 0xbe, 0xd8, 0xd5, 0x7b,
+ 0x97, 0x39, 0x7a, 0xe3, 0xfe, 0x6f, 0xc5, 0x5e, 0xa8, 0x56, 0x1d, 0x38,
+ 0xac, 0x7c, 0xc6, 0xc7, 0x10, 0x40, 0xff, 0xb4, 0x16, 0xf6, 0x7a, 0x9c,
+ 0x57, 0x06, 0x41, 0x8d, 0xb1, 0x0e, 0xb9, 0x0c, 0xeb, 0x23, 0xe7, 0x2a,
+ 0x1e, 0x98, 0xfe, 0x22, 0x07, 0xb6, 0xa1, 0xb5, 0x22, 0xe3, 0x2e, 0x48,
+ 0x8c, 0x44, 0xd3, 0x60, 0x39, 0xac, 0x9f, 0xaa, 0x60, 0xe1, 0x03, 0x61,
+ 0xbe, 0x32, 0x9a, 0x1f, 0xee, 0x20, 0xe3, 0x16, 0x61, 0x74, 0x19, 0xc1,
+ 0xf1, 0x15, 0x39, 0x5a, 0x8f, 0xf0, 0xea, 0xda, 0xdc, 0xa7, 0x3f, 0x3e,
+ 0xb7, 0x70, 0x41, 0x3e, 0xa4, 0xbb, 0x97, 0xee, 0x2c, 0x2f, 0xa9, 0x17,
+ 0x54, 0x43, 0x4a, 0x5b, 0x32, 0x5e, 0xe1, 0x5e, 0x0d, 0x8b, 0xbc, 0x2c,
+ 0x97, 0x0d, 0x07, 0x36, 0x61, 0x31, 0x87, 0x2c, 0x63, 0xf2, 0xfc, 0x4e,
+ 0x4e, 0x47, 0x87, 0xfb, 0xe0, 0xfc, 0xa9, 0x25, 0xcd, 0xbc, 0x17, 0xb0,
+ 0x88, 0x91, 0x15, 0xaa, 0xcf, 0x26, 0xa3, 0x24, 0x8d, 0xc9, 0xa0, 0xdb,
+ 0x09, 0xeb, 0x82, 0x15, 0x68, 0x0b, 0xb5, 0x20, 0xe7, 0xad, 0xc6, 0x67,
+ 0x67, 0xa7, 0xb0, 0xce, 0x06, 0xcd, 0x24, 0x7c, 0x8d, 0x3a, 0x05, 0xef,
+ 0x0f, 0xb3, 0x4a, 0x26, 0x72, 0x0d, 0x00, 0x19, 0x65, 0xbf, 0x5f, 0x9e,
+ 0xb7, 0xda, 0x40, 0xbb, 0x8d, 0xe5, 0x4d, 0x71, 0x0e, 0xc6, 0x89, 0xa5,
+ 0x8d, 0x4f, 0x55, 0x09, 0xe4, 0x8a, 0xce, 0x3a, 0x0c, 0x97, 0x7e, 0x6b,
+ 0x73, 0x30, 0x17, 0x56, 0x45, 0x5f, 0x0d, 0xa9, 0x5c, 0xe3, 0x93, 0x79,
+ 0x0f, 0x1b, 0x5e, 0xf4, 0x26, 0xae, 0xc1, 0x30, 0x04, 0x6c, 0x66, 0xb6,
+ 0xed, 0x04, 0x74, 0x11, 0x5e, 0x6b, 0xb6, 0x99, 0x1d, 0x29, 0x64, 0x85,
+ 0xe4, 0xb5, 0x3e, 0x3f, 0x7b, 0xe3, 0xcf, 0x5c, 0x29, 0xda, 0xca, 0xa3,
+ 0x70, 0x9f, 0x21, 0xc5, 0x9f, 0xd2, 0xa0, 0xa1, 0x9a, 0x4d, 0x38, 0xe5,
+ 0x87, 0x60, 0x25, 0x6c, 0xd2, 0x22, 0xd3, 0xc6, 0x05, 0x24, 0xd4, 0xbc,
+ 0x4b, 0x4d, 0xbc, 0xa8, 0xe0, 0xc4, 0x5d, 0x17, 0x7c, 0x43, 0x9d, 0xc8,
+ 0xdb, 0x58, 0x1e, 0x86, 0xaa, 0xcb, 0x48, 0xd5, 0x70, 0x4f, 0x35, 0x8a,
+ 0x96, 0x77, 0x90, 0x3a, 0x62, 0xb1, 0x93, 0x16, 0x36, 0xf3, 0xd7, 0x29,
+ 0x25, 0xb0, 0xea, 0xc9, 0xec, 0x7f, 0xb5, 0xa5, 0x1f, 0x23, 0x30, 0xff,
+ 0x7e, 0x13, 0xc0, 0x6e, 0xa7, 0x2f, 0x5a, 0x35, 0xd1, 0x9e, 0x57, 0xed,
+ 0xf5, 0xd3, 0x42, 0xcf, 0x02, 0x67, 0x0e, 0x21, 0x2d, 0x6b, 0x90, 0xe7,
+ 0xc2, 0x07, 0x2d, 0x43, 0xb5, 0xc5, 0x09, 0xb5, 0x0f, 0x93, 0xbc, 0xb6,
+ 0x72, 0x36, 0x82, 0x12, 0x8e, 0xfd, 0x52, 0x1c, 0x2f, 0x5a, 0x2b, 0x23,
+ 0xc0, 0xcb, 0x91, 0x18, 0xe1, 0xe2, 0xa0, 0x12, 0x89, 0x61, 0x7d, 0xa8,
+ 0xc2, 0x09, 0xfe, 0x5e, 0xdf, 0x1c, 0xd8, 0xc1, 0x6d, 0x01, 0xe1, 0x9b,
+ 0x5d, 0xc8, 0x3f, 0xa8, 0xd5, 0x71, 0xd9, 0xea, 0x89, 0xb5, 0x9b, 0xd3,
+ 0xb0, 0xa7, 0x51, 0xa6, 0x22, 0xf6, 0xf6, 0xeb, 0x48, 0x9b, 0xd6, 0x45,
+ 0xf6, 0x01, 0x67, 0x91, 0x97, 0x3a, 0x0b, 0xb1, 0xfa, 0x9c, 0xd1, 0x42,
+ 0xb1, 0x9e, 0x39, 0xaa, 0x02, 0xb7, 0xb9, 0xbf, 0x08, 0xf5, 0xaf, 0xe8,
+ 0xb8, 0x7e, 0xf3, 0x02, 0x53, 0xff, 0x1f, 0xae, 0x8b, 0xee, 0xaf, 0x44,
+ 0x11, 0x6c, 0x16, 0xfb, 0x79, 0x8b, 0xb8, 0x44, 0xb3, 0x13, 0x77, 0xdd,
+ 0xde, 0xa9, 0x9b, 0xd5, 0x66, 0x7c, 0x8f, 0xde, 0x59, 0x15, 0x3d, 0xc3,
+ 0x3b, 0x18, 0xff, 0x91, 0x3f, 0xc6, 0x32, 0xd2, 0xe1, 0x2d, 0xe4, 0x9b,
+ 0xa8, 0x9f, 0x97, 0xf1, 0x7e, 0x23, 0x6e, 0x31, 0x79, 0xb9, 0x7e, 0x6e,
+ 0xcb, 0xbc, 0x47, 0x62, 0x24, 0x82, 0x94, 0xcb, 0xc0, 0xe9, 0x19, 0x25,
+ 0xaf, 0xf5, 0xa0, 0x3a, 0xf2, 0xcb, 0x45, 0xf3, 0xa2, 0x3b, 0xdb, 0xe7,
+ 0x51, 0x63, 0xa4, 0x6f, 0xb6, 0x0f, 0xd0, 0x52, 0xef, 0xce, 0x35, 0x33,
+ 0x0e, 0xcf, 0xf8, 0x7a, 0x1a, 0x53, 0xec, 0xfb, 0xfa, 0xdb, 0xca, 0xd1,
+ 0xa6, 0xab, 0x77, 0x43, 0xb0, 0x8a, 0x63, 0x92, 0xa0, 0x2b, 0x23, 0xe9,
+ 0xf4, 0x01, 0x63, 0xd7, 0xe6, 0xbf, 0x18, 0x57, 0x2a, 0xf4, 0xc5, 0xd6,
+ 0x5a, 0xe1, 0x7c, 0x79, 0xff, 0xf9, 0xc5, 0x68, 0xb1, 0xdb, 0xab, 0xaf,
+ 0x26, 0x78, 0x05, 0x81, 0x13, 0x27, 0x03, 0xbe, 0x2d, 0xdc, 0x83, 0x60,
+ 0x1d, 0x41, 0x58, 0xf3, 0xa0, 0x08, 0x8c, 0x3c, 0x99, 0xa9, 0xae, 0x8e,
+ 0xcd, 0x5c, 0xcc, 0x8f, 0x03, 0x6f, 0x88, 0x7b, 0x18, 0x7d, 0x37, 0x80,
+ 0x30, 0x1d, 0xc3, 0x3d, 0x7b, 0xa1, 0x21, 0x3b, 0xae, 0x45, 0xb3, 0xf2,
+ 0x76, 0x54, 0x5d, 0xa8, 0x1e, 0x2b, 0x80, 0x64, 0xc2, 0xce, 0x5d, 0x44,
+ 0x5c, 0x35, 0x7e, 0x1f, 0xa8, 0x2a, 0xcd, 0x52, 0x43, 0x1e, 0xd6, 0x67,
+ 0x56, 0x56, 0x8b, 0x4d, 0xca, 0x11, 0x38, 0x5a, 0x00, 0xce, 0xbb, 0xc0,
+ 0xb3, 0x23, 0xf3, 0xe2, 0xab, 0xbc, 0xd2, 0xa4, 0xeb, 0x5e, 0x69, 0x27,
+ 0xc6, 0x31, 0x14, 0x3e, 0x63, 0x41, 0xce, 0x09, 0x32, 0xab, 0xc5, 0xc2,
+ 0x31, 0x47, 0x3a, 0x99, 0x03, 0x05, 0x77, 0x23, 0xc3, 0xa3, 0x77, 0x13,
+ 0x60, 0x79, 0x4a, 0x77, 0xd1, 0x92, 0xb8, 0x7d, 0xe6, 0x8e, 0x3b, 0x8c,
+ 0x69, 0x39, 0xcc, 0x00, 0x06, 0xe2, 0x62, 0x84, 0x80, 0x73, 0x12, 0xae,
+ 0x4d, 0x61, 0x60, 0xad, 0x6d, 0x1d, 0x43, 0x44, 0x65, 0x84, 0xdd, 0x46,
+ 0x4b, 0x11, 0xea, 0xdc, 0x47, 0x69, 0xc9, 0xbe, 0x72, 0x25, 0x6c, 0x69,
+ 0x57, 0x23, 0xae, 0x8d, 0x91, 0x63, 0x3e, 0x9a, 0xde, 0x96, 0xb8, 0x13,
+ 0x60, 0xd6, 0x4b, 0x8b, 0x05, 0x06, 0x7d, 0x78, 0xcf, 0x51, 0x0b, 0x5a,
+ 0x3d, 0x88, 0x33, 0x6d, 0x1c, 0x0b, 0x91, 0x3f, 0x17, 0xa9, 0x4e, 0x5a,
+ 0xb9, 0x89, 0x1a, 0xcd, 0x5a, 0x86, 0x6e, 0x9f, 0x64, 0x8c, 0xf0, 0xab,
+ 0x98, 0x41, 0xd2, 0xf6, 0x3e, 0x1b, 0x79, 0x29, 0xb7, 0x66, 0xa1, 0x2a,
+ 0xc2, 0x71, 0xfc, 0xda, 0xb4, 0xdf, 0xbd, 0x2d, 0x7c, 0x03, 0xba, 0xdd,
+ 0x64, 0x6b, 0xa7, 0x17, 0x55, 0xf1, 0xfe, 0xc5, 0xc6, 0xdf, 0x96, 0x65,
+ 0x8a, 0xc5, 0x3b, 0xb5, 0x37, 0x39, 0xa7, 0x3c, 0x83, 0x0a, 0xbc, 0x5e,
+ 0xf0, 0x3c, 0x8b, 0x30, 0xb0, 0x7b, 0x90, 0xba, 0x1b, 0x85, 0x2d, 0xc3,
+ 0xd6, 0x60, 0x2e, 0x4b, 0xa4, 0x2d, 0x62, 0xc8, 0xb3, 0x01, 0x8a, 0x48,
+ 0xd3, 0xe2, 0xd6, 0xf0, 0x56, 0x7d, 0xb7, 0x36, 0x64, 0xa9, 0x1f, 0x42,
+ 0x88, 0xa4, 0x34, 0xee, 0x7a, 0x28, 0x32, 0xd1, 0x2f, 0x2f, 0x26, 0x11,
+ 0x63, 0xab, 0x6c, 0xf2, 0x94, 0x54, 0x08, 0x66, 0xaf, 0x55, 0x0e, 0xd8,
+ 0x45, 0xe0, 0x8c, 0x8c, 0x17, 0x7b, 0xb5, 0xa3, 0x15, 0xfa, 0xb0, 0xcd,
+ 0x04, 0x9e, 0xbf, 0xc5, 0x09, 0x4e, 0x76, 0xb3, 0x74, 0xe0, 0x50, 0xad,
+ 0xaf, 0xf1, 0xff, 0x18, 0xe1, 0x7b, 0x94, 0x44, 0x2f, 0x6e, 0xb3, 0x4b,
+ 0x3f, 0x75, 0x8a, 0xa8, 0x05, 0xd5, 0x55, 0xab, 0xbb, 0xc0, 0x32, 0x3a,
+ 0xca, 0xe2, 0x78, 0x74, 0x0d, 0x40, 0x19, 0xdb, 0x99, 0x9e, 0xad, 0xbf,
+ 0xc1, 0x04, 0xa3, 0x10, 0x49, 0xfa, 0x7b, 0xf7, 0x2f, 0x1c, 0xf6, 0xa8,
+ 0xba, 0x37, 0xc8, 0x77, 0x36, 0x39, 0x27, 0x54, 0xc9, 0x76, 0x9b, 0xa5,
+ 0x43, 0x51, 0x12, 0x59, 0x76, 0x09, 0x3e, 0x53, 0x68, 0xd8, 0x4c, 0x50,
+ 0xc1, 0xf5, 0x08, 0x5f, 0x88, 0x22, 0xae, 0xd5, 0x84, 0x65, 0x59, 0x9f,
+ 0x57, 0xea, 0x9d, 0x6f, 0x14, 0x19, 0xf6, 0xd4, 0x82, 0xac, 0x34, 0xfa,
+ 0x1d, 0xdd, 0x66, 0x14, 0x36, 0x70, 0xf3, 0x40, 0x0d, 0x25, 0xfa, 0xa8,
+ 0x45, 0x64, 0xea, 0x9d, 0xe7, 0xf4, 0x80, 0x50, 0x91, 0x9e, 0x21, 0x54,
+ 0xc9, 0x54, 0x29, 0x9d, 0xf6, 0xde, 0xe2, 0x91, 0x76, 0x98, 0x3e, 0xe9,
+ 0x9b, 0x0b, 0x90, 0xe3, 0x5d, 0xdb, 0xcd, 0x06, 0x1e, 0xf4, 0xdb, 0xd1,
+ 0xcf, 0x88, 0x72, 0x07, 0x65, 0xa8, 0xe2, 0x0e, 0xa6, 0xe1, 0x93, 0x83,
+ 0xfd, 0x40, 0x98, 0x82, 0x03, 0x82, 0x7f, 0xd3, 0x7a, 0xc1, 0xef, 0x25,
+ 0xc2, 0x51, 0xfd, 0x02, 0x5d, 0xe2, 0x2f, 0x9b, 0xd1, 0xf7, 0xc9, 0x03,
+ 0xcd, 0x47, 0xf1, 0xee, 0x32, 0xd9, 0x75, 0xb2, 0xad, 0x27, 0x91, 0x9f,
+ 0x65, 0x0d, 0x5a, 0x73, 0x39, 0xc9, 0x93, 0x7d, 0x47, 0xc1, 0xf7, 0xd4,
+ 0x8f, 0xfe, 0x9b, 0x4b, 0xf2, 0xb8, 0x5e, 0x93, 0x4b, 0xc0, 0x46, 0xe1,
+ 0xc8, 0x3e, 0xd4, 0x8d, 0xb3, 0xf2, 0x99, 0xb9, 0x6f, 0xe8, 0x69, 0xec,
+ 0xa9, 0x7b, 0x01, 0x90, 0x7a, 0xf8, 0x2e, 0x17, 0xb7, 0x57, 0x11, 0x5a,
+ 0x46, 0xdc, 0x4b, 0x53, 0x16, 0x5a, 0xd9, 0xef, 0xb9, 0x44, 0x09, 0x21,
+ 0x55, 0xf1, 0xc7, 0x99, 0x70, 0x4f, 0xc3, 0x4c, 0x9f, 0x17, 0x61, 0xb7,
+ 0x56, 0xc0, 0xe7, 0x61, 0xda, 0xe3, 0x8e, 0xb9, 0x9e, 0x88, 0x5d, 0x73,
+ 0x44, 0x47, 0x6a, 0x0a, 0x98, 0x9a, 0xbc, 0xf9, 0x10, 0x03, 0x08, 0x90,
+ 0x5b, 0x95, 0xce, 0x06, 0x10, 0xcf, 0xc0, 0xbe, 0x97, 0x3b, 0xb7, 0xb2,
+ 0x0f, 0x0a, 0x53, 0x55, 0x73, 0x5d, 0x03, 0x5a, 0x7f, 0x5f, 0x53, 0xe1,
+ 0x37, 0x98, 0x6f, 0xf7, 0x79, 0x8e, 0x22, 0xfd, 0x49, 0x7c, 0x3f, 0x05,
+ 0xdd, 0xf6, 0xb9, 0x63, 0x7e, 0xc6, 0x6e, 0xe6, 0x94, 0x7e, 0x4f, 0x5d,
+ 0x2c, 0xeb, 0xb7, 0x45, 0x79, 0xcf, 0xf6, 0x5f, 0x4d, 0x3b, 0x45, 0x68,
+ 0xf6, 0x54, 0xf8, 0x89, 0x90, 0x30, 0xde, 0xdc, 0x5f, 0x52, 0x04, 0x7a,
+ 0x85, 0x02, 0x18, 0x16, 0x23, 0x2f, 0xbd, 0xe9, 0x72, 0x04, 0xef, 0x0e,
+ 0x75, 0x1e, 0x33, 0x5b, 0x65, 0x91, 0x2a, 0x25, 0xc9, 0xf9, 0xb2, 0x13,
+ 0x54, 0xde, 0xcc, 0xa0, 0x75, 0xc6, 0xed, 0xf4, 0x22, 0x8f, 0xde, 0xf1,
+ 0x4e, 0x25, 0x9b, 0xe9, 0x13, 0x0a, 0xf4, 0xba, 0x4f, 0xe4, 0x23, 0x0f,
+ 0x5c, 0x38, 0x63, 0xe7, 0xff, 0x6a, 0xaa, 0x85, 0xda, 0x68, 0xd8, 0xad,
+ 0x45, 0x80, 0x5c, 0x86, 0x8c, 0x9f, 0x9f, 0xf3, 0xb0, 0xee, 0x3f, 0xf0,
+ 0x54, 0x06, 0x32, 0xfd, 0x57, 0xc2, 0x71, 0x16, 0xed, 0xd4, 0xe3, 0x22,
+ 0xa8, 0xd3, 0x9a, 0x00, 0x7b, 0x83, 0xf4, 0xc5, 0x86, 0x3b, 0xfa, 0x30,
+ 0xba, 0x85, 0x68, 0x06, 0x53, 0x8a, 0x7f, 0x77, 0xc2, 0x66, 0xfe, 0x8e,
+ 0x4b, 0x7f, 0xf7, 0xb1, 0x76, 0x42, 0xf1, 0x69, 0x7b, 0xb1, 0x25, 0xb6,
+ 0xb3, 0xfb, 0x9f, 0xb3, 0xcf, 0x1f, 0xf1, 0xaf, 0x55, 0x22, 0x44, 0x51,
+ 0x2e, 0x82, 0x89, 0x84, 0xf2, 0xc3, 0x09, 0xfd, 0x22, 0xf4, 0x75, 0x88,
+ 0x78, 0xd2, 0x3e, 0x82, 0x14, 0x46, 0x7e, 0xd9, 0xe5, 0xee, 0x50, 0xb5,
+ 0x1d, 0x41, 0x89, 0x95, 0xe2, 0x60, 0x56, 0xd4, 0x39, 0x48, 0x65, 0x06,
+ 0x11, 0x64, 0xa0, 0x76, 0x27, 0x1b, 0xf6, 0x3a, 0x90, 0x9e, 0x36, 0xf7,
+ 0x33, 0x81, 0x3b, 0x58, 0x75, 0xce, 0x87, 0x03, 0x36, 0x23, 0xe9, 0x38,
+ 0x81, 0x4f, 0xf4, 0x72, 0x6e, 0xbf, 0xa6, 0x3a, 0x23, 0x15, 0x70, 0x11,
+ 0x95, 0xcc, 0xfa, 0x60, 0x3b, 0x8f, 0x14, 0xe6, 0xad, 0x2a, 0xd0, 0x91,
+ 0xe8, 0x7e, 0x11, 0xbb, 0x2e, 0xbb, 0xfe, 0x76, 0xe1, 0x9c, 0xa4, 0x12,
+ 0xd3, 0xb1, 0xa0, 0x05, 0xd2, 0xf3, 0x28, 0x4d, 0xb6, 0x13, 0xbe, 0x19,
+ 0x1d, 0x8e, 0x0b, 0x95, 0x4b, 0x0a, 0x57, 0xa3, 0xa3, 0x84, 0xf4, 0x8e,
+ 0x0a, 0x98, 0x0c, 0xb6, 0x96, 0x3e, 0xb2, 0x39, 0xd0, 0x12, 0x51, 0xee,
+ 0x6c, 0x15, 0xc4, 0xfd, 0x67, 0x33, 0x2f, 0xe1, 0xc8, 0x8d, 0x9f, 0xce,
+ 0xec, 0xd0, 0xd1, 0xa8, 0xda, 0x8a, 0x24, 0xa4, 0x4a, 0x1b, 0x8e, 0x4a,
+ 0xe3, 0x93, 0xef, 0x4e, 0xdf, 0x17, 0x66, 0x84, 0x31, 0x78, 0x3a, 0x18,
+ 0xe1, 0xfb, 0xae, 0x97, 0xa7, 0x30, 0x94, 0x75, 0x5c, 0xfc, 0xd6, 0xf7,
+ 0xdc, 0x50, 0x44, 0xfe, 0x62, 0xb1, 0x57, 0xfc, 0x11, 0x18, 0xa7, 0x7d,
+ 0xc9, 0x6b, 0x37, 0x04, 0x00, 0xff, 0x7a, 0x25, 0x78, 0x5c, 0xf1, 0x80,
+ 0xac, 0x03, 0x60, 0xd4, 0xda, 0x78, 0x46, 0x52, 0xb1, 0xf3, 0xa4, 0x3d,
+ 0x1d, 0x56, 0xe3, 0x49, 0x09, 0xcf, 0xa8, 0x75, 0x72, 0x68, 0x99, 0x20,
+ 0x22, 0x01, 0xf5, 0x3c, 0x91, 0xc9, 0xbe, 0x2d, 0x17, 0xa8, 0x16, 0xe3,
+ 0x8d, 0x16, 0x90, 0x3f, 0xb4, 0xf4, 0xfd, 0x64, 0xaa, 0x68, 0x08, 0x61,
+ 0xba, 0x65, 0xb9, 0x3d, 0x25, 0x4b, 0x25, 0x2e, 0xe2, 0x5d, 0xd5, 0xa4,
+ 0xfa, 0x6b, 0x2f, 0x01, 0x8f, 0x07, 0x55, 0xb7, 0x72, 0x11, 0xe0, 0x48,
+ 0xdb, 0x1f, 0xdf, 0x67, 0x97, 0x40, 0x32, 0xab, 0x0d, 0x9e, 0xfe, 0xf8,
+ 0xca, 0xb7, 0x6b, 0x03, 0x5a, 0x06, 0x9f, 0x2c, 0xed, 0x23, 0x5e, 0x4a,
+ 0xb1, 0x41, 0x7c, 0xa1, 0x92, 0x24, 0x6c, 0x98, 0x8c, 0x78, 0x59, 0x94,
+ 0x2e, 0x64, 0x37, 0x13, 0x5c, 0xab, 0xc7, 0x10, 0x5a, 0xf1, 0xe1, 0xd6,
+ 0xa2, 0x78, 0x87, 0x6c, 0x26, 0x9b, 0x96, 0x32, 0x6f, 0x82, 0x4c, 0x29,
+ 0x41, 0x54, 0xfa, 0x9c, 0xce, 0x4b, 0x43, 0x3a, 0xac, 0x98, 0x89, 0x64,
+ 0x95, 0x3c, 0x80, 0xaa, 0xb4, 0xee, 0x1f, 0x7c, 0x9b, 0xc8, 0xed, 0xf4,
+ 0xb1, 0x85, 0x9f, 0x06, 0x62, 0x76, 0xe3, 0x59, 0x26, 0x18, 0x91, 0x5f,
+ 0xc1, 0xe5, 0x98, 0xe2, 0x9f, 0x13, 0x53, 0x2e, 0xa1, 0x84, 0xee, 0xf5,
+ 0x5b, 0x97, 0xba, 0x8b, 0xaa, 0x90, 0x4f, 0x53, 0x8e, 0x78, 0x61, 0x07,
+ 0x94, 0x8c, 0x3f, 0x8c, 0x34, 0x75, 0x93, 0x4b, 0x7e, 0x25, 0xd9, 0x6e,
+ 0x9c, 0xaa, 0x14, 0xd6, 0x85, 0x15, 0xd5, 0x6a, 0xfe, 0x4a, 0x1a, 0x0e,
+ 0x3f, 0xcf, 0x23, 0xb8, 0xf8, 0x77, 0xba, 0x5a, 0x4d, 0xb6, 0x0c, 0x2d,
+ 0xe5, 0xe7, 0x7c, 0x0b, 0xb9, 0x4b, 0x6a, 0xaf, 0xda, 0xec, 0x05, 0x5e,
+ 0x6a, 0xaa, 0xbe, 0x91, 0x58, 0x72, 0x1a, 0x52, 0x84, 0xdb, 0x3c, 0x72,
+ 0x60, 0x12, 0x5a, 0x3a, 0x05, 0xdf, 0x11, 0x94, 0xbf, 0xce, 0x4a, 0x14,
+ 0xf3, 0xc1, 0x70, 0x52, 0xd3, 0xee, 0x32, 0xfc, 0x3a, 0xce, 0x58, 0x84,
+ 0x9c, 0x55, 0x1f, 0x4f, 0xe9, 0xb4, 0x7a, 0x8b, 0xfe, 0x1c, 0xb5, 0xee,
+ 0x86, 0xdb, 0x94, 0x9d, 0x2c, 0x9b, 0x1e, 0x7b, 0xef, 0xb5, 0xec, 0x2d,
+ 0x61, 0x6e, 0x4d, 0xa1, 0xb3, 0x08, 0x26, 0x59, 0x80, 0xd5, 0x66, 0x03,
+ 0xb2, 0x83, 0xfb, 0xff, 0xb0, 0x3f, 0xa7, 0x01, 0x1b, 0xf8, 0x14, 0x89,
+ 0x5a, 0x79, 0x8c, 0x75, 0x8e, 0x60, 0x57, 0x8f, 0xcd, 0x16, 0xeb, 0xfe,
+ 0xde, 0xf6, 0xeb, 0x84, 0xe9, 0xf7, 0xad, 0xb8, 0x06, 0xc0, 0x07, 0x80,
+ 0x19, 0xed, 0x7d, 0x88, 0xd1, 0xb5, 0xef, 0x7e, 0x88, 0xfe, 0x96, 0xf1,
+ 0x2a, 0x71, 0x9d, 0x54, 0xae, 0xfe, 0x27, 0x6c, 0xe5, 0xa3, 0x4a, 0xca,
+ 0x00, 0xac, 0x67, 0x14, 0x99, 0x0f, 0x03, 0xb6, 0xb5, 0xd0, 0xa9, 0x0c,
+ 0x04, 0x7b, 0x70, 0x4d, 0xe0, 0x83, 0xb6, 0x56, 0x8a, 0xc2, 0x04, 0xbb,
+ 0x76, 0xbc, 0xdb, 0x24, 0x23, 0x07, 0xa6, 0xf2, 0x70, 0x46, 0xab, 0x2f,
+ 0xc9, 0x80, 0xbe, 0x36, 0x3b, 0xb0, 0xe0, 0xc9, 0x49, 0xf2, 0x83, 0x15,
+ 0x0a, 0xd3, 0xab, 0x64, 0x8d, 0xd5, 0x6b, 0x13, 0xaf, 0x61, 0x94, 0x27,
+ 0x5f, 0x56, 0xe0, 0x69, 0x3e, 0x0d, 0x4c, 0xf9, 0xb4, 0xba, 0xab, 0x2d,
+ 0xf5, 0x36, 0x69, 0xfe, 0xda, 0x20, 0xb9, 0xd3, 0xf3, 0x8a, 0xf7, 0x56,
+ 0xca, 0x5d, 0x74, 0x6d, 0x35, 0x61, 0x89, 0x1c, 0x51, 0x32, 0xf1, 0x63,
+ 0xfc, 0xaa, 0x72, 0x6e, 0x95, 0xde, 0xbd, 0x76, 0xd2, 0x7f, 0x0b, 0x8d,
+ 0x11, 0xf8, 0xdd, 0x73, 0x92, 0x85, 0x29, 0x3a, 0xdf, 0x50, 0x87, 0x81,
+ 0xaa, 0x0f, 0xfe, 0x9b, 0x0d, 0x52, 0x66, 0x73, 0xd0, 0xff, 0xff, 0xf6,
+ 0xd4, 0xb8, 0xe7, 0xcd, 0x07, 0x49, 0x4e, 0xeb, 0x1f, 0x81, 0x50, 0xb3,
+ 0x51, 0x63, 0x78, 0x46, 0x1b, 0x20, 0x84, 0x8e, 0x71, 0xa7, 0x0a, 0xca,
+ 0x80, 0x39, 0xdc, 0x6b, 0xd8, 0xc1, 0xbe, 0xd1, 0x36, 0xda, 0x4c, 0x7c,
+ 0x94, 0xd2, 0x8e, 0x0f, 0xfb, 0x0a, 0x95, 0x6d, 0x5e, 0x59, 0x50, 0x15,
+ 0x9a, 0xda, 0x39, 0xc7, 0x2b, 0xda, 0x91, 0x82, 0x75, 0x3f, 0xee, 0xae,
+ 0xbb, 0xa7, 0x1c, 0x3c, 0x52, 0xfa, 0x19, 0x05, 0x30, 0x25, 0x37, 0xa0,
+ 0x59, 0x4a, 0x98, 0xe1, 0x28, 0x40, 0x53, 0x72, 0x6f, 0xa9, 0x5a, 0x26,
+ 0x49, 0x14, 0xd0, 0x8d, 0xb5, 0x98, 0x6f, 0x68, 0x1c, 0x7f, 0x67, 0xfd,
+ 0x4d, 0x46, 0xfe, 0x0a, 0x7b, 0xc0, 0x6d, 0xbd, 0xa1, 0x30, 0xcf, 0x99,
+ 0x3f, 0x01, 0x55, 0xdd, 0x07, 0x5e, 0x22, 0xf2, 0x4d, 0xf9, 0xba, 0xc0,
+ 0x82, 0x6f, 0x39, 0x78, 0x08, 0x6f, 0xb9, 0x40, 0x82, 0x9f, 0xf2, 0xd1,
+ 0x7a, 0x12, 0xcd, 0xa2, 0x8c, 0xa0, 0x80, 0xa7, 0xf9, 0x53, 0xec, 0x64,
+ 0x35, 0xa6, 0xec, 0x0b, 0x34, 0x83, 0x7a, 0x89, 0x2c, 0x28, 0x8c, 0x25,
+ 0x79, 0xd6, 0x64, 0x83, 0x79, 0x42, 0xea, 0x99, 0x66, 0xfd, 0x7e, 0x35,
+ 0x59, 0xb7, 0x4b, 0x24, 0x3c, 0x5c, 0x9c, 0xb0, 0x2a, 0xbc, 0x39, 0x83,
+ 0xed, 0x49, 0x3c, 0xc6, 0x79, 0x6b, 0x20, 0x27, 0xd3, 0xfa, 0x26, 0xf2,
+ 0x0e, 0x20, 0xb9, 0xe8, 0x5a, 0x24, 0xf1, 0xea, 0x17, 0xc1, 0xeb, 0xe7,
+ 0xd7, 0xee, 0x61, 0x6b, 0xf5, 0x09, 0xf1, 0xb1, 0x70, 0x9b, 0x4d, 0xfc,
+ 0xcf, 0xfc, 0x67, 0x82, 0x9b, 0x02, 0x11, 0x62, 0x66, 0x8e, 0x07, 0xad,
+ 0x3a, 0x9b, 0xc4, 0x5f, 0x1c, 0xf6, 0xc6, 0xc5, 0xd8, 0x34, 0x61, 0x80,
+ 0xfa, 0x27, 0x3d, 0x35, 0x27, 0x77, 0x74, 0x21, 0xee, 0x7e, 0x15, 0x29,
+ 0xe3, 0xd5, 0xbe, 0x02, 0xee, 0x5c, 0x0e, 0x7a, 0x04, 0xe6, 0xd6, 0xe6,
+ 0xa9, 0x1c, 0x13, 0x37, 0x7c, 0xc9, 0x3c, 0x2e, 0xf3, 0xf5, 0x7d, 0xbc,
+ 0x50, 0x2c, 0x17, 0x6a, 0x51, 0xac, 0x22, 0xa8, 0x34, 0x0c, 0xc4, 0x95,
+ 0xd3, 0xbc, 0xe5, 0x6d, 0x0d, 0x89, 0x0a, 0x39, 0xd7, 0x24, 0x8b, 0xc2,
+ 0xbd, 0x31, 0xf5, 0xa7, 0xc4, 0x23, 0x57, 0xcc, 0x3b, 0xdf, 0x67, 0x64,
+ 0xcd, 0x7a, 0x8e, 0xb8, 0x5a, 0x62, 0xe6, 0x29, 0xa3, 0x8b, 0xba, 0x24,
+ 0x5f, 0xdc, 0x2e, 0xb5, 0x30, 0x9b, 0x7b, 0x21, 0xfa, 0x4e, 0x3b, 0xa0,
+ 0x03, 0x07, 0x2d, 0x7b, 0x51, 0x9f, 0xd3, 0xba, 0x91, 0x73, 0x25, 0xce,
+ 0xdb, 0x8e, 0xcd, 0x2f, 0xdf, 0x9c, 0x3f, 0xa3, 0x39, 0xa7, 0x8e, 0x50,
+ 0x06, 0x21, 0x42, 0xb5, 0x52, 0xfe, 0x4c, 0x5d, 0xdd, 0xf1, 0x75, 0x9b,
+ 0x9d, 0xb8, 0xce, 0x2a, 0xfc, 0x28, 0x34, 0xa1, 0x5b, 0xa3, 0x95, 0xf7,
+ 0x7f, 0x53, 0xd2, 0x4f, 0x25, 0xc1, 0x8a, 0x21, 0x93, 0xba, 0xdc, 0xb5,
+ 0x61, 0xcc, 0x35, 0xcc, 0x66, 0x8e, 0xb7, 0x43, 0x1a, 0x4c, 0xb1, 0xbb,
+ 0x19, 0x80, 0xe0, 0xd5, 0x76, 0x44, 0x26, 0x16, 0x54, 0x17, 0x6a, 0x13,
+ 0x19, 0xe8, 0x67, 0x1a, 0x7b, 0x64, 0xb0, 0x49, 0x70, 0x0e, 0xdb, 0xc1,
+ 0xd7, 0xa9, 0x53, 0x63, 0x8b, 0x54, 0x23, 0xe1, 0x46, 0x61, 0xd5, 0xda,
+ 0x7f, 0x3c, 0xf1, 0x52, 0x62, 0xe7, 0x27, 0x8c, 0x68, 0xcc, 0x84, 0x05,
+ 0x05, 0xe0, 0xa9, 0xb1, 0x2f, 0x2f, 0x23, 0x38, 0x78, 0x66, 0xac, 0x07,
+ 0xec, 0xaf, 0x3a, 0x99, 0xf6, 0x46, 0x2e, 0xb4, 0x43, 0xa2, 0xc3, 0x98,
+ 0x9b, 0x10, 0x39, 0x63, 0x5a, 0x0b, 0x1e, 0x44, 0x30, 0xf4, 0xfb, 0x7a,
+ 0x2f, 0x2f, 0xeb, 0xad, 0x4e, 0xd0, 0x12, 0xeb, 0x57, 0x0e, 0x78, 0xb1,
+ 0x9c, 0x01, 0xe2, 0xfd, 0xf3, 0xcb, 0x86, 0x70, 0x5c, 0x65, 0xcc, 0xc0,
+ 0x91, 0xe5, 0x15, 0xbd, 0x3a, 0x1b, 0x2e, 0xb1, 0xc1, 0xfb, 0x97, 0x2d,
+ 0x68, 0x7a, 0xb6, 0x46, 0xd4, 0x5e, 0x90, 0x17, 0x91, 0x94, 0xcd, 0x78,
+ 0x16, 0xba, 0xa5, 0x0b, 0xe5, 0xfc, 0xf6, 0xcf, 0xb3, 0xd3, 0xcb, 0x28,
+ 0xa5, 0xf7, 0xde, 0xb2, 0x0f, 0xb6, 0x65, 0xa5, 0xd3, 0xe9, 0xeb, 0xe0,
+ 0x72, 0x1b, 0xf3, 0x5c, 0xde, 0xfb, 0x36, 0xf3, 0x94, 0x1e, 0xe1, 0x7a,
+ 0x85, 0x43, 0x4e, 0x8f, 0x66, 0xf0, 0x50, 0x88, 0xfa, 0x11, 0xf6, 0x44,
+ 0x96, 0x66, 0xc3, 0xd5, 0x0d, 0xb0, 0x56, 0x9e, 0x93, 0xbe, 0x22, 0xc0,
+ 0x54, 0xa6, 0x68, 0x2c, 0x31, 0xea, 0x87, 0x15, 0x53, 0xd2, 0xae, 0xb2,
+ 0x5a, 0x20, 0x93, 0x68, 0xa6, 0x9f, 0xe5, 0x8e, 0xe9, 0xe8, 0xa9, 0xc7,
+ 0x49, 0xe9, 0x63, 0xbe, 0x2f, 0xd4, 0xf3, 0x7d, 0xa5, 0xb1, 0xb7, 0x78,
+ 0x85, 0x0c, 0x5c, 0x11, 0x77, 0xea, 0xef, 0x16, 0x2f, 0xbe, 0xdd, 0x40,
+ 0x9d, 0xdd, 0x96, 0xbf, 0x9f, 0x7e, 0xa0, 0xd4, 0x72, 0x7c, 0x42, 0xc0,
+ 0x2e, 0x81, 0x88, 0xf7, 0x74, 0xbd, 0x93, 0x7c, 0x21, 0x53, 0x29, 0xb1,
+ 0x61, 0x6e, 0x74, 0xe4, 0xb4, 0xd7, 0xd3, 0x6b, 0x9f, 0x72, 0xa3, 0x9e,
+ 0x10, 0xad, 0xd8, 0xf0, 0xe4, 0x54, 0xb5, 0x79, 0xeb, 0x8e, 0x68, 0xc3,
+ 0xeb, 0xa0, 0x5d, 0x05, 0xee, 0xd3, 0xb4, 0x60, 0x4d, 0xd8, 0x6a, 0x37,
+ 0x18, 0x87, 0x81, 0x98, 0xfc, 0x8e, 0x07, 0x2d, 0xf9, 0x24, 0xc7, 0x0b,
+ 0x20, 0x8e, 0xf4, 0x7e, 0x89, 0xd1, 0x66, 0xf9, 0x92, 0x0f, 0xf8, 0x7f,
+ 0x49, 0x40, 0x8f, 0x99, 0x19, 0xd5, 0xc2, 0x93, 0x9d, 0x06, 0x7c, 0xe2,
+ 0x0b, 0xa8, 0xb0, 0xaa, 0x6a, 0x5b, 0x81, 0x1e, 0xa8, 0x6a, 0x03, 0xe0,
+ 0xa2, 0xdc, 0x7b, 0x5b, 0x5d, 0xab, 0xf2, 0x94, 0xbc, 0x44, 0x05, 0xa0,
+ 0x2f, 0x82, 0x66, 0xd3, 0x6c, 0xd7, 0xce, 0x30, 0x64, 0x71, 0xb8, 0x27,
+ 0x96, 0xae, 0xdc, 0xcc, 0x3d, 0xdf, 0x2d, 0x3c, 0x3e, 0x5a, 0x36, 0xb8,
+ 0x90, 0x75, 0x2a, 0xb2, 0x29, 0xfd, 0xd4, 0x63, 0xa2, 0xe7, 0x82, 0xcc,
+ 0x2e, 0x39, 0x8b, 0x30, 0x4b, 0xc9, 0x60, 0xaa, 0x27, 0xc9, 0x51, 0x6a,
+ 0x69, 0x22, 0x3e, 0xde, 0xd6, 0xba, 0xb2, 0x39, 0x4e, 0x43, 0xd5, 0x41,
+ 0xc5, 0xca, 0x22, 0x68, 0x88, 0xa4, 0x1a, 0xad, 0x5a, 0x05, 0x7e, 0xe7,
+ 0xdb, 0xb2, 0x2c, 0xe7, 0xf0, 0xec, 0xa8, 0x00, 0x9a, 0x5a, 0xe6, 0x75,
+ 0x94, 0x62, 0x7e, 0xc3, 0xcd, 0x59, 0x63, 0x02, 0xd1, 0xa1, 0xd2, 0xf9,
+ 0xeb, 0x6d, 0x79, 0x69, 0xa7, 0xd2, 0xeb, 0x0b, 0x7c, 0xeb, 0x51, 0x10,
+ 0xcf, 0xcd, 0x1e, 0x16, 0x0b, 0x5b, 0x5e, 0xe4, 0xe8, 0x6f, 0x30, 0xcc,
+ 0xeb, 0xc0, 0xf6, 0xb6, 0xd0, 0xd0, 0x1a, 0xee, 0x02, 0x3e, 0x1f, 0x73,
+ 0x31, 0x8e, 0xd7, 0x9f, 0x6d, 0xfb, 0x76, 0x17, 0x6e, 0x99, 0x7f, 0xec,
+ 0xe4, 0x56, 0xa0, 0xe6, 0x4d, 0x9f, 0x6e, 0x72, 0x18, 0x4d, 0x6b, 0x7a,
+ 0x20, 0x53, 0xa9, 0x7b, 0x4d, 0x3f, 0xf8, 0xc5, 0x7e, 0x0d, 0x5d, 0x27,
+ 0x9b, 0x7c, 0xb8, 0x69, 0x7b, 0xc4, 0xf6, 0xc8, 0x17, 0xa1, 0xa8, 0x11,
+ 0xa4, 0xa4, 0x73, 0xb6, 0x4e, 0xc7, 0x94, 0x18, 0x4d, 0x8c, 0xe1, 0x90,
+ 0x36, 0x04, 0xbc, 0xf1, 0xd5, 0x55, 0xce, 0x57, 0x92, 0x23, 0x69, 0xce,
+ 0x3c, 0xb0, 0x63, 0x50, 0x05, 0x50, 0x8b, 0xa4, 0x50, 0xf7, 0x24, 0xcd,
+ 0x89, 0xed, 0xc0, 0x64, 0xe5, 0x93, 0x79, 0x3e, 0x7c, 0x15, 0x89, 0x01,
+ 0x2e, 0x9c, 0xf0, 0x30, 0xcb, 0xf3, 0x03, 0x87, 0x70, 0xaa, 0x07, 0x4c,
+ 0xf3, 0x4c, 0xe4, 0xbb, 0x6d, 0x3f, 0xde, 0x52, 0xde, 0x0a, 0xfe, 0xbe,
+ 0x6a, 0xc1, 0x9c, 0xd1, 0x0d, 0x02, 0x9e, 0xb4, 0x68, 0x1d, 0x23, 0x01,
+ 0x5d, 0x6a, 0xa7, 0x27, 0x9d, 0xb9, 0xad, 0xaa, 0x8c, 0xf1, 0x76, 0xbf,
+ 0xda, 0x37, 0x79, 0x8a, 0x84, 0xd8, 0x0c, 0x5d, 0x89, 0x5a, 0x70, 0xe6,
+ 0x7f, 0xb5, 0x3c, 0xd8, 0xad, 0x29, 0x59, 0xd5, 0x75, 0x66, 0x84, 0x87,
+ 0x82, 0xd8, 0xc0, 0x66, 0xf2, 0x2a, 0x53, 0xe9, 0xbd, 0xe2, 0xb0, 0x10,
+ 0x5d, 0x2e, 0x73, 0xad, 0x9c, 0xff, 0xd2, 0xcc, 0x6a, 0x76, 0x95, 0xcb,
+ 0x59, 0xf1, 0x41, 0xf3, 0x08, 0x3e, 0x96, 0x13, 0x54, 0xb4, 0x5a, 0x82,
+ 0x8b, 0x93, 0x00, 0x87, 0x54, 0x2e, 0x0a, 0xb5, 0x65, 0x5c, 0xdd, 0x86,
+ 0x9c, 0x35, 0x31, 0x7d, 0x82, 0x86, 0x83, 0x9f, 0x44, 0x80, 0x16, 0xc9,
+ 0x70, 0xd1, 0xff, 0x33, 0x21, 0xb0, 0xf2, 0x10, 0xd2, 0x47, 0x00, 0xff,
+ 0x56, 0x04, 0x25, 0xe7, 0xe4, 0xca, 0x5a, 0x27, 0xc0, 0x9a, 0x4e, 0xc2,
+ 0xa1, 0x37, 0xf1, 0x3f, 0x02, 0xec, 0xd1, 0xfd, 0x3d, 0xa2, 0xef, 0x42,
+ 0xf4, 0xca, 0x83, 0xca, 0x50, 0x9f, 0x08, 0x1e, 0x63, 0x4f, 0x82, 0x8a,
+ 0x8a, 0xfa, 0xaa, 0x89, 0xb0, 0x4c, 0xc9, 0x59, 0xbc, 0x9a, 0xf7, 0xbd,
+ 0xfd, 0xc0, 0xf8, 0xbd, 0xe6, 0xb6, 0x6e, 0x9b, 0x58, 0x8a, 0x55, 0x98,
+ 0x37, 0xa4, 0xf1, 0x0b, 0x6a, 0x5a, 0x5d, 0x41, 0xbc, 0x19, 0xa1, 0x78,
+ 0x5a, 0xcf, 0xee, 0x8f, 0x9c, 0x5a, 0x97, 0x28, 0xa8, 0x11, 0x15, 0x40,
+ 0xe4, 0x21, 0x9d, 0xdd, 0xd8, 0xcc, 0x60, 0xf6, 0x78, 0x14, 0xc8, 0x14,
+ 0xff, 0x47, 0x76, 0x26, 0x3e, 0x96, 0x01, 0x72, 0x80, 0x07, 0x36, 0xa9,
+ 0xc9, 0xf8, 0x9e, 0xec, 0x69, 0x2a, 0x1c, 0xd3, 0x09, 0x5c, 0xd7, 0x3a,
+ 0x6f, 0xdf, 0xa8, 0x92, 0x66, 0xe4, 0x7f, 0x8c, 0x85, 0x95, 0x12, 0xf5,
+ 0x42, 0x87, 0xa4, 0x30, 0x55, 0xcf, 0x56, 0x9a, 0x46, 0xfb, 0xaf, 0x29,
+ 0x20, 0x7b, 0xca, 0x4e, 0xc1, 0xf9, 0x6b, 0x6a, 0x22, 0x8a, 0x2c, 0x3a,
+ 0xda, 0x22, 0x50, 0x83, 0x3b, 0x08, 0xf9, 0xcb, 0xdb, 0xce, 0xf7, 0xb7,
+ 0x82, 0x14, 0x41, 0xec, 0xcf, 0xbd, 0xe6, 0x44, 0x38, 0x14, 0xad, 0x03,
+ 0xaa, 0x05, 0xe2, 0x4e, 0xda, 0x30, 0xee, 0xb9, 0xb2, 0x8b, 0xa6, 0x37,
+ 0xfd, 0x6e, 0xa4, 0xa1, 0x02, 0xde, 0x3e, 0x36, 0xfd, 0x1c, 0xdc, 0x2c,
+ 0xd1, 0x6b, 0xc3, 0xd5, 0xcd, 0x16, 0x28, 0xd4, 0x66, 0x77, 0xa9, 0x1a,
+ 0x34, 0x3e, 0x8b, 0x54, 0x81, 0x41, 0xfd, 0xff, 0x3c, 0xad, 0x05, 0xa9,
+ 0x14, 0x6d, 0xc6, 0xa3, 0x95, 0xb0, 0x02, 0x6b, 0x9d, 0x24, 0x5a, 0xe2,
+ 0x9d, 0x86, 0xd6, 0x62, 0x0c, 0x38, 0x44, 0x1a, 0x65, 0x65, 0x4b, 0x84,
+ 0xe7, 0xd9, 0x20, 0x83, 0xa2, 0x7a, 0x4e, 0xac, 0xe9, 0x1b, 0x06, 0x54,
+ 0x63, 0x86, 0x65, 0xc4, 0x28, 0xf9, 0x66, 0x55, 0x8b, 0x12, 0x68, 0x5a,
+ 0x87, 0x78, 0x63, 0xcd, 0xff, 0x4c, 0x00, 0x7a, 0x04, 0x75, 0x6b, 0x05,
+ 0xd0, 0xd8, 0xed, 0x33, 0x84, 0xae, 0x00, 0x5a, 0x35, 0xea, 0xf3, 0xa8,
+ 0x86, 0x11, 0xd0, 0xe6, 0x5c, 0xb0, 0x06, 0xc5, 0x13, 0xb3, 0x44, 0xe5,
+ 0x2f, 0x21, 0x13, 0x06, 0xe5, 0x1f, 0x06, 0x90, 0xa6, 0xb0, 0xff, 0x0d,
+ 0x29, 0x06, 0xb1, 0x5f, 0x1a, 0x41, 0x16, 0x60, 0x14, 0xbc, 0xaa, 0xbf,
+ 0xe1, 0xd3, 0x9b, 0x0d, 0xc8, 0x8b, 0x84, 0x24, 0x73, 0x43, 0x76, 0xe3,
+ 0x9b, 0x99, 0x73, 0x32, 0xcd, 0x22, 0x41, 0xe6, 0x15, 0x70, 0x5f, 0xa9,
+ 0xd5, 0x3d, 0x58, 0xb5, 0x09, 0xa2, 0x97, 0x96, 0x81, 0x1d, 0xcf, 0xa2,
+ 0x69, 0x42, 0xb8, 0xc0, 0x82, 0x04, 0x29, 0x03, 0x91, 0x26, 0x10, 0x39,
+ 0x95, 0x1e, 0xe2, 0xb0, 0xcb, 0x8e, 0x47, 0x0a, 0xdc, 0x1d, 0x7e, 0xd4,
+ 0x29, 0x90, 0xb1, 0x7e, 0xdf, 0x94, 0x8c, 0xcf, 0x17, 0x3f, 0xcd, 0x14,
+ 0x7b, 0xb5, 0xe8, 0xbb, 0x41, 0x73, 0xb1, 0x03, 0xd2, 0x18, 0x52, 0x7b,
+ 0xa5, 0x9b, 0x59, 0x9c, 0x1b, 0xf1, 0xff, 0xde, 0x73, 0x70, 0xb3, 0xbb,
+ 0xee, 0x8b, 0xdb, 0xc4, 0xa6, 0x44, 0x51, 0xef, 0x3d, 0xb0, 0x0e, 0x9e,
+ 0x60, 0xdd, 0x12, 0xbd, 0x42, 0xe4, 0xef, 0x24, 0x99, 0x33, 0x42, 0x01,
+ 0x71, 0x93, 0x4a, 0xc3, 0xf4, 0xa5, 0x47, 0x14, 0xc0, 0x21, 0x62, 0xb0,
+ 0xf3, 0x53, 0xe0, 0xf1, 0xba, 0x9e, 0x81, 0xbd, 0xe8, 0x0a, 0xa2, 0x16,
+ 0xad, 0x5c, 0x89, 0xe7, 0xa5, 0x1f, 0xb3, 0x53, 0x5b, 0x8a, 0x75, 0x43,
+ 0x68, 0xbc, 0x83, 0xe6, 0x0e, 0x74, 0x80, 0xfa, 0xe5, 0x99, 0x7e, 0xd0,
+ 0x5a, 0x0f, 0x0b, 0x7d, 0xc7, 0x9d, 0x53, 0x20, 0xf7, 0x2b, 0x34, 0xb6,
+ 0xfd, 0x60, 0x75, 0x45, 0x88, 0x2b, 0x10, 0x2c, 0x82, 0xc5, 0xea, 0xb8,
+ 0xa2, 0x5e, 0x0e, 0xaf, 0x0c, 0xf3, 0x7c, 0x1d, 0x91, 0x4a, 0x24, 0xff,
+ 0x9d, 0xa4, 0xae, 0xb4, 0xc7, 0x2c, 0xaa, 0xc9, 0x42, 0x4d, 0x3c, 0x33,
+ 0x21, 0xc8, 0xb1, 0x1e, 0x66, 0xc8, 0x5b, 0xba, 0x10, 0xa9, 0x17, 0x13,
+ 0x67, 0x3c, 0x52, 0x2d, 0x0b, 0x11, 0xb4, 0x0d, 0xf4, 0x47, 0x90, 0x4d,
+ 0x61, 0xab, 0x86, 0x60, 0xf3, 0x53, 0x39, 0x92, 0xa4, 0xd6, 0xeb, 0x9e,
+ 0x69, 0xe7, 0xa8, 0x6e, 0x24, 0x78, 0xfe, 0x52, 0x27, 0xa2, 0xdc, 0x9f,
+ 0xed, 0xc6, 0x99, 0xe1, 0x94, 0x41, 0xcd, 0xfe, 0x0b, 0x95, 0xb4, 0x85,
+ 0x20, 0x16, 0x71, 0x07, 0x5d, 0xab, 0xc0, 0xc0, 0x87, 0x3d, 0xd2, 0xe2,
+ 0x58, 0x4f, 0xf9, 0x1f, 0x63, 0x68, 0xbe, 0x50, 0x5e, 0xe0, 0x51, 0xab,
+ 0x47, 0x65, 0xaf, 0x46, 0xe2, 0x64, 0x56, 0x0b, 0x02, 0x51, 0x9e, 0xd4,
+ 0x84, 0xc8, 0xa5, 0x8d, 0xf3, 0x17, 0xa7, 0xe7, 0x06, 0xe9, 0x77, 0x74,
+ 0xf1, 0xf1, 0x2f, 0xa1, 0x18, 0x4b, 0xf7, 0x75, 0x4b, 0xa6, 0xf0, 0x53,
+ 0x5b, 0xd0, 0x11, 0xd5, 0xad, 0xb2, 0x33, 0xc0, 0xca, 0x73, 0x0a, 0xc6,
+ 0x46, 0xa0, 0x68, 0xda, 0x36, 0x27, 0xa5, 0x95, 0x49, 0x8d, 0x9d, 0x08,
+ 0xfe, 0x27, 0xce, 0x2f, 0x10, 0x27, 0xd6, 0x46, 0x85, 0x6c, 0x71, 0xce,
+ 0xa0, 0x5a, 0x0e, 0x3d, 0x4f, 0x4d, 0x9d, 0x29, 0xf5, 0xbc, 0x67, 0x58,
+ 0xb5, 0x29, 0x87, 0x85, 0xdb, 0xcd, 0x78, 0x5b, 0xab, 0xa9, 0x1e, 0x73,
+ 0x45, 0x97, 0x38, 0x20, 0x76, 0xd9, 0x16, 0xb1, 0x0c, 0xfe, 0x1e, 0x25,
+ 0x2b, 0x5c, 0xd8, 0x5b, 0xf0, 0x32, 0xf5, 0xa3, 0x1d, 0xb9, 0xef, 0x4c,
+ 0x34, 0x80, 0x73, 0x42, 0xec, 0xb9, 0x49, 0x61, 0xdd, 0xad, 0x38, 0xa8,
+ 0x9a, 0x2e, 0xf5, 0x75, 0x56, 0x5a, 0xc2, 0xf4, 0x20, 0xc0, 0x13, 0x03,
+ 0xbb, 0xfa, 0x72, 0x7d, 0xbe, 0xdc, 0xc8, 0xca, 0x5c, 0x7d, 0x62, 0x5e,
+ 0xb0, 0x75, 0xe9, 0xd7, 0x7f, 0xfe, 0xf2, 0xb8, 0x34, 0xc9, 0xdb, 0x1c,
+ 0x69, 0x7b, 0x59, 0x3f, 0xed, 0xc9, 0xa1, 0x6f, 0x82, 0xdd, 0x61, 0x11,
+ 0x0b, 0x44, 0x0b, 0xbc, 0x8f, 0xba, 0xe1, 0xbe, 0xea, 0x34, 0x66, 0x9a,
+ 0xfe, 0x1d, 0x9a, 0x31, 0x9f, 0x61, 0x15, 0x5c, 0xee, 0x98, 0x63, 0x94,
+ 0x4b, 0x01, 0x7d, 0x62, 0xcd, 0xa7, 0x47, 0x3b, 0x65, 0xd6, 0x4d, 0xb0,
+ 0x4b, 0x73, 0xcd, 0x36, 0xb7, 0x25, 0x50, 0x4f, 0xae, 0x1f, 0x73, 0xa7,
+ 0xbd, 0x84, 0x4c, 0x74, 0x1c, 0x45, 0x87, 0x63, 0x17, 0xba, 0x84, 0xf6,
+ 0xbb, 0x32, 0x1e, 0x90, 0xb1, 0x67, 0x72, 0x62, 0x8a, 0x1f, 0xdb, 0x32,
+ 0x4c, 0x93, 0xfc, 0x31, 0x00, 0x30, 0x69, 0xbd, 0xf2, 0x55, 0xe1, 0x17,
+ 0x63, 0x7f, 0x8d, 0xf8, 0x7f, 0x2f, 0x1b, 0xa0, 0x29, 0x51, 0x14, 0x9e,
+ 0x15, 0x00, 0x59, 0xcb, 0x0e, 0xf1, 0x78, 0x6b, 0xeb, 0x0d, 0xc2, 0xba,
+ 0xb0, 0x9f, 0xb6, 0x93, 0x2b, 0x03, 0xd6, 0x44, 0x6f, 0xfd, 0xe8, 0xb4,
+ 0x20, 0x5b, 0xec, 0xaa, 0xd4, 0x7f, 0x45, 0x65, 0x22, 0x0a, 0x3f, 0xd1,
+ 0x24, 0x15, 0x28, 0x28, 0x95, 0xe5, 0xef, 0x7c, 0xfe, 0xe8, 0x52, 0xc3,
+ 0x6d, 0x8e, 0xca, 0x33, 0x89, 0xa8, 0xb6, 0x5c, 0xef, 0xee, 0xdf, 0x54,
+ 0x51, 0x4f, 0xfd, 0x81, 0x9a, 0x6a, 0x7d, 0xcd, 0x90, 0x6c, 0xb7, 0xaf,
+ 0x47, 0x12, 0x9e, 0x03, 0x0f, 0xd9, 0xb3, 0x14, 0x8f, 0x72, 0x03, 0x18,
+ 0x30, 0xb3, 0xb7, 0xb7, 0x6c, 0xc6, 0x53, 0x09, 0x77, 0x4d, 0x8d, 0x11,
+ 0xc1, 0x2c, 0xe1, 0xf6, 0x6c, 0xdf, 0x51, 0xc2, 0x56, 0x1f, 0x75, 0xc4,
+ 0x92, 0xe9, 0x33, 0x68, 0x14, 0x2b, 0xf5, 0xe6, 0x67, 0x15, 0x57, 0x6c,
+ 0x2c, 0xe2, 0x30, 0x50, 0x40, 0x26, 0xec, 0xf2, 0x4f, 0x56, 0x37, 0xaa,
+ 0xb7, 0x7a, 0x4e, 0xb3, 0x57, 0xee, 0x2a, 0xa0, 0xa1, 0x1c, 0xcf, 0x7f,
+ 0xc7, 0x59, 0x72, 0x08, 0x07, 0xc5, 0xce, 0xfd, 0x8f, 0x72, 0x89, 0x5b,
+ 0xbc, 0xdf, 0xec, 0x60, 0xf9, 0xff, 0xeb, 0x40, 0xe5, 0xf8, 0x6a, 0x97,
+ 0x79, 0x30, 0x87, 0xff, 0x5b, 0x0e, 0x9b, 0x37, 0x15, 0xca, 0x45, 0x5a,
+ 0x2f, 0x8b, 0xf4, 0xd1, 0xde, 0x00, 0xc3, 0x9f, 0x6d, 0x7b, 0x5c, 0xa8,
+ 0x84, 0x7d, 0x56, 0x92, 0x15, 0xdb, 0x6e, 0xe4, 0x9a, 0xde, 0x4d, 0xe1,
+ 0xa4, 0xe9, 0xfd, 0x8c, 0x15, 0xec, 0x18, 0xb6, 0x5a, 0xe5, 0xf2, 0x4c,
+ 0x54, 0xce, 0x77, 0xed, 0xb7, 0xba, 0x14, 0xad, 0x19, 0xce, 0x45, 0x5e,
+ 0x4f, 0x53, 0x41, 0x01, 0xbd, 0x65, 0x06, 0xcd, 0x07, 0x8a, 0x5b, 0xcc,
+ 0x60, 0xfa, 0x48, 0xba, 0x1c, 0xde, 0x26, 0xd9, 0x83, 0x82, 0xd2, 0x12,
+ 0x1f, 0x49, 0x75, 0xcf, 0xb4, 0xde, 0x80, 0xa0, 0xe0, 0x78, 0x9b, 0x49,
+ 0xce, 0xcf, 0xb1, 0x22, 0x32, 0x37, 0x56, 0x94, 0x9a, 0x2f, 0xa9, 0xef,
+ 0xbb, 0x4e, 0xc1, 0x07, 0x92, 0x0d, 0xbc, 0xe9, 0x6b, 0x0e, 0x5a, 0xd5,
+ 0x5b, 0xe1, 0x43, 0x4d, 0x12, 0x1a, 0x72, 0x2e, 0x0b, 0x5a, 0x4a, 0x4d,
+ 0xf6, 0x56, 0xe5, 0x1c, 0x71, 0x49, 0xfe, 0x0c, 0x3e, 0x30, 0x8f, 0x15,
+ 0xd0, 0x7e, 0xb2, 0x0d, 0x5d, 0x48, 0xda, 0x23, 0x10, 0x22, 0xa4, 0xf5,
+ 0xf4, 0x3a, 0x3a, 0xe1, 0xd4, 0xfc, 0x63, 0x6d, 0x0c, 0xcd, 0x7b, 0x60,
+ 0xa8, 0x89, 0xe3, 0x97, 0x7c, 0x7f, 0xe9, 0x54, 0x43, 0xe4, 0xac, 0xd1,
+ 0x6c, 0x31, 0xf8, 0x61, 0x96, 0x14, 0xb5, 0x4c, 0xa8, 0x42, 0xb8, 0x67,
+ 0x79, 0xfe, 0x46, 0xc3, 0x06, 0x6b, 0x29, 0x1d, 0x4f, 0x84, 0x6f, 0x8e,
+ 0x1d, 0xbf, 0xb1, 0x81, 0x51, 0xe7, 0x21, 0x71, 0x78, 0x2a, 0x8a, 0xb8,
+ 0x53, 0x6a, 0xcc, 0x06, 0x79, 0x9d, 0xa3, 0xd7, 0xe0, 0x69, 0xc4, 0xc4,
+ 0x56, 0x75, 0xb2, 0x82, 0x6e, 0x6d, 0xff, 0x62, 0x4b, 0x21, 0x58, 0x8a,
+ 0x62, 0x09, 0x0d, 0xdc, 0xd3, 0x31, 0xf2, 0x55, 0x75, 0xd6, 0xcf, 0x28,
+ 0xf7, 0xed, 0xe1, 0x41, 0xd6, 0x3f, 0x6c, 0x88, 0x18, 0xe6, 0x1a, 0x82,
+ 0xfb, 0x07, 0xd4, 0x14, 0x33, 0x6a, 0xd9, 0xa5, 0x66, 0x09, 0x2a, 0xce,
+ 0x95, 0x4d, 0x7d, 0xfa, 0xfc, 0x58, 0x9b, 0xd5, 0xfd, 0x1a, 0x5b, 0x61,
+ 0x38, 0xc5, 0x6a, 0x93, 0xcc, 0x62, 0xe0, 0x91, 0x27, 0x47, 0x2e, 0xaa,
+ 0x4e, 0x5a, 0x2c, 0x5e, 0x54, 0x98, 0xff, 0x98, 0xa6, 0x88, 0xb7, 0x76,
+ 0x73, 0x05, 0xf1, 0x8c, 0x18, 0x34, 0x88, 0xf5, 0xcd, 0xad, 0x4f, 0xdb,
+ 0x85, 0x42, 0x67, 0xe7, 0x88, 0xaf, 0xbe, 0x4e, 0x76, 0xc1, 0x22, 0x21,
+ 0x46, 0x3c, 0x64, 0xd5, 0x6e, 0x6a, 0x46, 0x1c, 0x86, 0x9b, 0x51, 0x87,
+ 0x14, 0x39, 0x2e, 0x83, 0x08, 0x47, 0x05, 0x49, 0xed, 0x09, 0xc2, 0xbd,
+ 0x2f, 0x77, 0x3a, 0x90, 0xd0, 0x7c, 0xe9, 0xcb, 0xcd, 0xb0, 0x4c, 0x10,
+ 0x39, 0xfb, 0xde, 0x73, 0xbb, 0x70, 0x27, 0xcc, 0xf0, 0x1f, 0xfb, 0x50,
+ 0x83, 0x62, 0xf4, 0x76, 0xb9, 0x47, 0x12, 0x11, 0xa4, 0x3e, 0xbe, 0x8d,
+ 0xe2, 0xa4, 0x19, 0xfe, 0x21, 0xcc, 0xca, 0x6d, 0x93, 0x15, 0xdd, 0xd2,
+ 0x8c, 0x56, 0x55, 0xc7, 0xf4, 0x6c, 0x62, 0xcf, 0xa9, 0x81, 0x01, 0xce,
+ 0x1b, 0x0f, 0xc6, 0x3b, 0xf0, 0xf3, 0xa8, 0xa0, 0x96, 0x9d, 0x9b, 0xb7,
+ 0x14, 0x85, 0x65, 0x0f, 0x57, 0x6f, 0xc2, 0x14, 0xfc, 0xfe, 0xb4, 0x98,
+ 0x9b, 0xc6, 0xa4, 0x34, 0xf9, 0x04, 0x6d, 0x7f, 0x90, 0xaf, 0xfa, 0x6f,
+ 0xe1, 0x37, 0xfb, 0x26, 0xca, 0xd0, 0xd6, 0x1d, 0x65, 0x36, 0x07, 0x4d,
+ 0x09, 0x8e, 0xae, 0x1f, 0x3a, 0x56, 0x14, 0x8b, 0x8f, 0xec, 0x23, 0x03,
+ 0x78, 0xe6, 0x7c, 0xef, 0xbd, 0xf9, 0xd6, 0xb8, 0xd9, 0x67, 0x81, 0xe3,
+ 0x24, 0xb2, 0xef, 0xf8, 0x48, 0x08, 0x7f, 0x46, 0xac, 0x02, 0x58, 0x83,
+ 0xb8, 0x32, 0xa0, 0x90, 0xa3, 0x1f, 0x9b, 0xfb, 0xe2, 0x02, 0x4c, 0x90,
+ 0x0f, 0x80, 0xf9, 0xd6, 0x6f, 0xa4, 0xb2, 0xd4, 0xa0, 0x24, 0x18, 0xef,
+ 0xdf, 0xf3, 0x87, 0x0c, 0x8d, 0x19, 0xe5, 0x61, 0x6e, 0x75, 0x81, 0x3b,
+ 0x56, 0x26, 0xbe, 0x0b, 0xed, 0x52, 0x8d, 0x0b, 0xa5, 0x0e, 0xa0, 0x3e,
+ 0x9f, 0xa1, 0x89, 0xe1, 0xff, 0x2b, 0xcd, 0xa4, 0xf3, 0x8c, 0x9d, 0xf5,
+ 0xe9, 0xb5, 0xad, 0xb5, 0x19, 0xf7, 0x9f, 0x74, 0x1b, 0xf2, 0xb4, 0x54,
+ 0x64, 0x26, 0xba, 0x58, 0x26, 0x4a, 0x16, 0x42, 0xbf, 0xa6, 0xc7, 0xb5,
+ 0xbd, 0x39, 0x7d, 0x12, 0xae, 0x95, 0xcd, 0x28, 0x10, 0x63, 0x71, 0x49,
+ 0x76, 0xe0, 0x0b, 0x1e, 0x65, 0x63, 0x87, 0xc4, 0x09, 0xb7, 0x58, 0xb3,
+ 0x9b, 0x76, 0x37, 0x1a, 0xa4, 0xa7, 0xf2, 0xd7, 0x89, 0xbf, 0x29, 0xd2,
+ 0x8a, 0xf6, 0xf4, 0xcc, 0xa9, 0x9b, 0x82, 0x60, 0x32, 0x41, 0x34, 0xdc,
+ 0x68, 0x2e, 0x74, 0x32, 0x3a, 0xf6, 0xd6, 0x1e, 0x31, 0xf0, 0x99, 0xf4,
+ 0xe8, 0x78, 0x20, 0xc4, 0x35, 0xd0, 0x67, 0x7b, 0xd1, 0xc9, 0xc4, 0xf0,
+ 0x0a, 0xf9, 0xcb, 0x98, 0xd7, 0xea, 0x35, 0x65, 0x7d, 0xab, 0xca, 0x65,
+ 0x78, 0xb7, 0xd6, 0xf7, 0x67, 0xa4, 0x41, 0x72, 0x6e, 0xa0, 0x88, 0x31,
+ 0x91, 0xf7, 0x43, 0xe2, 0xa3, 0x6e, 0x78, 0x7e, 0x70, 0xeb, 0x28, 0xd9,
+ 0x1c, 0x5b, 0x47, 0xc3, 0x73, 0x6e, 0x86, 0x57, 0x66, 0x1b, 0x83, 0xae,
+ 0x3a, 0xa9, 0x01, 0x26, 0x0c, 0xa1, 0x61, 0xec, 0xa7, 0x50, 0xe0, 0xdd,
+ 0xb3, 0xbd, 0x2c, 0x4f, 0xf1, 0x71, 0xe0, 0xa6, 0xbb, 0xc1, 0x00, 0x9e,
+ 0x80, 0x42, 0x07, 0x31, 0x6e, 0xdc, 0x48, 0x9c, 0x1b, 0xd7, 0xb8, 0x53,
+ 0xf9, 0x73, 0xba, 0x81, 0x23, 0x12, 0x8f, 0x90, 0x5f, 0x38, 0xda, 0xfd,
+ 0x07, 0x97, 0x63, 0x1e, 0xa6, 0x23, 0xf4, 0xb2, 0xf8, 0xcd, 0x75, 0x7e,
+ 0x70, 0x9d, 0x60, 0x50, 0x57, 0x4c, 0xf0, 0x01, 0xdf, 0x0a, 0xd8, 0x51,
+ 0x58, 0x5e, 0x30, 0x7a, 0xaa, 0x0f, 0x98, 0x18, 0xaf, 0x4b, 0xe7, 0x59,
+ 0xc6, 0x70, 0x03, 0x36, 0xae, 0x82, 0x69, 0x33, 0x53, 0xf1, 0x90, 0xf0,
+ 0xb4, 0x99, 0xf6, 0x85, 0x1c, 0x60, 0x87, 0xb8, 0x67, 0xea, 0x5a, 0xd0,
+ 0xd6, 0xcb, 0xeb, 0x45, 0x3e, 0x06, 0x61, 0xec, 0xcf, 0x5e, 0x86, 0xe3,
+ 0x9d, 0x36, 0xd5, 0x3c, 0x89, 0x6d, 0x42, 0x94, 0x5b, 0x31, 0xa7, 0x6b,
+ 0x94, 0xd7, 0x1a, 0x36, 0xaa, 0xad, 0x82, 0x1d, 0x61, 0x16, 0x9a, 0xcf,
+ 0x8c, 0x4a, 0xdc, 0xd8, 0x1e, 0xea, 0x32, 0x79, 0x3b, 0x94, 0xa9, 0x65,
+ 0x2a, 0x27, 0x97, 0xa7, 0xc2, 0x89, 0xaa, 0x5d, 0x29, 0xbb, 0xef, 0x3a,
+ 0xc0, 0xc8, 0x18, 0x81, 0x92, 0x0e, 0x87, 0x75, 0x14, 0x91, 0x77, 0x09,
+ 0xc1, 0xff, 0xc8, 0x99, 0x19, 0xdb, 0x3c, 0x72, 0x0d, 0xdf, 0xfd, 0xd3,
+ 0xaf, 0x6d, 0x68, 0xb0, 0x0f, 0xc9, 0x7e, 0x2b, 0x24, 0xa4, 0x34, 0x2f,
+ 0x53, 0xa4, 0x6f, 0xb1, 0xa0, 0xef, 0x30, 0x7a, 0x9a, 0x21, 0x11, 0x24,
+ 0x12, 0xc7, 0x74, 0x8d, 0x12, 0x75, 0xba, 0x6c, 0x31, 0xc2, 0xf0, 0xa1,
+ 0x6a, 0x23, 0xba, 0x65, 0x96, 0x13, 0xc3, 0xd3, 0x6c, 0xbb, 0xbf, 0xfb,
+ 0xfb, 0x5f, 0x54, 0xf2, 0xfd, 0xbe, 0xf0, 0xfc, 0x58, 0x4a, 0x52, 0x15,
+ 0x57, 0xce, 0x3e, 0xee, 0x01, 0xb0, 0xcc, 0x90, 0xf8, 0xed, 0x27, 0xbe,
+ 0x99, 0x04, 0x75, 0x37, 0xe8, 0x14, 0x60, 0x12, 0x3c, 0x8e, 0xbb, 0x32,
+ 0x7b, 0x0b, 0x42, 0xa5, 0x1e, 0xa2, 0xba, 0xff, 0x44, 0xa7, 0x0b, 0x7d,
+ 0x2f, 0xb2, 0xac, 0xc6, 0xaf, 0xfd, 0xa5, 0x81, 0xfe, 0xa7, 0x8c, 0x3b,
+ 0xfd, 0xb3, 0x37, 0x9e, 0x1b, 0x32, 0xa8, 0xe8, 0x28, 0x05, 0x94, 0x25,
+ 0xda, 0xe9, 0x6c, 0x48, 0x21, 0xc6, 0x8a, 0xc8, 0x76, 0x6a, 0xc3, 0x4b,
+ 0x34, 0x68, 0xd9, 0xcf, 0x05, 0xf9, 0xb7, 0x99, 0x31, 0xc8, 0x25, 0x2e,
+ 0xfb, 0xc1, 0xcd, 0x4c, 0x82, 0xae, 0x0d, 0x4d, 0x10, 0x18, 0x15, 0x5a,
+ 0x93, 0x42, 0xeb, 0xf9, 0x3f, 0xe7, 0x76, 0x6e, 0xfe, 0xbc, 0x15, 0x27,
+ 0x36, 0xb4, 0x5f, 0x14, 0x16, 0xb6, 0x8a, 0x22, 0x19, 0xbb, 0xe6, 0x9f,
+ 0x5a, 0xa7, 0xec, 0xff, 0x00, 0x04, 0x3a, 0x74, 0x95, 0x6a, 0xb3, 0xb1,
+ 0xc5, 0xa2, 0x69, 0x97, 0x28, 0xc3, 0xb7, 0x37, 0xb9, 0x48, 0xd8, 0x69,
+ 0xc9, 0xac, 0xba, 0x0b, 0x57, 0xc1, 0x6c, 0x0f, 0xc6, 0x6b, 0x84, 0xb4,
+ 0x28, 0x40, 0x69, 0x86, 0x78, 0xea, 0xba, 0xb4, 0xac, 0x0a, 0x58, 0x86,
+ 0x46, 0x73, 0x26, 0x0c, 0x6b, 0x38, 0x7c, 0xfb, 0xb1, 0xc8, 0x02, 0x50,
+ 0x06, 0x1d, 0xc7, 0x82, 0xb1, 0x73, 0x71, 0x99, 0x42, 0xa5, 0x94, 0x34,
+ 0x83, 0x01, 0x43, 0xc3, 0x04, 0x18, 0x5f, 0x86, 0xbc, 0x9f, 0x45, 0x58,
+ 0x93, 0x20, 0xfe, 0xe8, 0xe5, 0x0d, 0x03, 0x2b, 0x6b, 0x1c, 0xf6, 0x5b,
+ 0x5b, 0x0a, 0xdd, 0x57, 0xb4, 0x12, 0x3e, 0xf3, 0x81, 0xf8, 0x94, 0x0e,
+ 0x84, 0xe1, 0xf1, 0x2f, 0x97, 0x06, 0x9e, 0x96, 0x35, 0x3d, 0x93, 0x6e,
+ 0xd1, 0x15, 0xd7, 0xe5, 0x13, 0x18, 0x8e, 0x91, 0xaf, 0x77, 0x26, 0x8d,
+ 0xe1, 0x20, 0x23, 0x73, 0x7e, 0x3a, 0x95, 0xb8, 0x2e, 0x2d, 0x90, 0xf0,
+ 0x09, 0x89, 0x07, 0x55, 0xef, 0x90, 0xdd, 0x74, 0x3f, 0x32, 0xfb, 0x3d,
+ 0x45, 0xa9, 0xdd, 0x59, 0xc5, 0x99, 0xc1, 0x28, 0x6d, 0xfd, 0x61, 0x71,
+ 0x77, 0x21, 0xb8, 0x83, 0x17, 0xab, 0xab, 0x6c, 0x4e, 0x30, 0xbb, 0xd4,
+ 0x3d, 0x4c, 0xe6, 0x4c, 0xf2, 0x2c, 0xbc, 0x99, 0xf6, 0x9a, 0xa9, 0x76,
+ 0x34, 0x03, 0x58, 0x1a, 0x09, 0x65, 0x67, 0x5d, 0x8c, 0x96, 0x1c, 0x6a,
+ 0x4e, 0x5a, 0x54, 0x64, 0xd7, 0x77, 0x1d, 0xa3, 0x1a, 0xa6, 0xda, 0xc6,
+ 0x27, 0x6e, 0x9c, 0x50, 0x49, 0x6f, 0xb5, 0x2b, 0x50, 0x27, 0xdc, 0x02,
+ 0x4d, 0xf3, 0x36, 0xa5, 0x7b, 0x90, 0x70, 0x30, 0xae, 0xa5, 0x38, 0x92,
+ 0xfc, 0xce, 0x19, 0x0e, 0x8b, 0x44, 0x9f, 0x8f, 0x78, 0xb7, 0x6e, 0x05,
+ 0xe8, 0xbe, 0xdf, 0x0f, 0x29, 0xbd, 0xd8, 0x6a, 0x18, 0xd1, 0x54, 0xad,
+ 0x1a, 0x5d, 0x48, 0xa1, 0x2f, 0x66, 0xeb, 0x18, 0x90, 0x4f, 0x84, 0x7d,
+ 0x6f, 0xec, 0xd3, 0x0e, 0xcf, 0x9e, 0xfe, 0xf8, 0x3e, 0x57, 0xef, 0x43,
+ 0xd6, 0xfb, 0x44, 0x33, 0x6c, 0x18, 0x4a, 0xd4, 0x94, 0xbd, 0x55, 0x2a,
+ 0xdc, 0x85, 0x11, 0x8f, 0x41, 0x79, 0xbc, 0xd2, 0x97, 0x33, 0x8a, 0xa0,
+ 0xc7, 0x8d, 0xc4, 0x38, 0x53, 0xfb, 0x07, 0xcc, 0x67, 0x3b, 0xe1, 0x8b,
+ 0xc7, 0x17, 0xef, 0xa4, 0xb1, 0xc3, 0xea, 0x3d, 0xbe, 0xb9, 0x4d, 0xe7,
+ 0x9e, 0xab, 0x26, 0xc7, 0x68, 0xd9, 0x6b, 0xe7, 0x41, 0xd6, 0xd7, 0xab,
+ 0xc7, 0xea, 0xe0, 0x47, 0xc7, 0x10, 0xd2, 0x9d, 0xb9, 0xa9, 0x49, 0xec,
+ 0x10, 0x82, 0x97, 0x73, 0xd5, 0x34, 0xd1, 0x0d, 0xaf, 0x4a, 0x57, 0x97,
+ 0x1b, 0xf4, 0x33, 0xdf, 0xd5, 0x9d, 0x01, 0xa9, 0xcb, 0xe5, 0x8a, 0xf5,
+ 0x94, 0x57, 0x2a, 0xa6, 0xc8, 0xe7, 0xa2, 0xd5, 0xc0, 0xf0, 0x3c, 0x91,
+ 0x59, 0x0d, 0x45, 0xd3, 0x1d, 0x17, 0x9c, 0xee, 0x70, 0x7c, 0x2c, 0xbc,
+ 0x47, 0x99, 0xb6, 0x5e, 0x28, 0x69, 0xd1, 0xf7, 0xbb, 0xb3, 0x6f, 0xff,
+ 0xdf, 0x97, 0xe1, 0x1e, 0xa0, 0x9b, 0x00, 0xef, 0xaa, 0x30, 0x49, 0x2e,
+ 0x90, 0xed, 0x93, 0x21, 0x31, 0xd0, 0x49, 0x72, 0x84, 0x16, 0xb3, 0x87,
+ 0x99, 0x2e, 0xfa, 0xe6, 0x1f, 0xe2, 0x2a, 0xcf, 0x46, 0x21, 0xa8, 0x59,
+ 0x45, 0xf9, 0xcf, 0x5c, 0x60, 0xad, 0x93, 0xbc, 0xe1, 0x6d, 0xac, 0xd5,
+ 0x2a, 0x18, 0x34, 0x08, 0x2a, 0x94, 0xcb, 0x2e, 0xa8, 0xa7, 0xe1, 0x35,
+ 0x81, 0x56, 0x16, 0x7f, 0xd8, 0x3a, 0xf2, 0x08, 0xbf, 0xdf, 0x8d, 0x5a,
+ 0x8d, 0x9e, 0xfc, 0x8a, 0x82, 0xf4, 0xde, 0x3a, 0x7a, 0x14, 0xa3, 0x87,
+ 0xb9, 0x80, 0xac, 0x0b, 0x16, 0x76, 0x43, 0x95, 0x32, 0xd0, 0xd1, 0x2d,
+ 0xf4, 0x9d, 0x9a, 0x0d, 0x1c, 0x8e, 0x48, 0x9b, 0xe8, 0xcc, 0x89, 0xe0,
+ 0x66, 0xde, 0x17, 0x82, 0x5f, 0xdc, 0x93, 0x47, 0x23, 0x27, 0x6d, 0xbf,
+ 0x8c, 0xd0, 0x9a, 0xf9, 0x17, 0xc3, 0xe7, 0xd6, 0x9d, 0x93, 0xe2, 0x2a,
+ 0x83, 0x1d, 0x19, 0x36, 0xe8, 0xbd, 0x55, 0x53, 0xda, 0x7b, 0x11, 0x66,
+ 0x59, 0xe5, 0xef, 0xed, 0x95, 0xb8, 0x55, 0x16, 0x49, 0xa0, 0x83, 0x97,
+ 0x21, 0x9a, 0x4b, 0x01, 0x53, 0xb7, 0x6f, 0x4e, 0x40, 0x04, 0x6b, 0xbe,
+ 0x01, 0xeb, 0xac, 0x1c, 0x29, 0xe0, 0xb9, 0x53, 0xb2, 0x97, 0xf9, 0x3e,
+ 0x4b, 0x11, 0x38, 0x09, 0x02, 0xfd, 0x9d, 0x0b, 0xc7, 0xaa, 0x38, 0x36,
+ 0x29, 0x32, 0x76, 0xe7, 0x12, 0x7f, 0x85, 0x92, 0xab, 0xbe, 0x96, 0x52,
+ 0x4e, 0x1b, 0x6c, 0x6f, 0x19, 0xd2, 0x43, 0xf8, 0xb9, 0x3f, 0xa3, 0x88,
+ 0x0d, 0xf0, 0x2a, 0xf3, 0x08, 0x80, 0xab, 0x32, 0x22, 0xe0, 0x6d, 0x1a,
+ 0x5d, 0x12, 0x1c, 0x25, 0xf5, 0x1e, 0x19, 0xfe, 0x93, 0xd3, 0x49, 0x10,
+ 0x51, 0x7e, 0xe5, 0x54, 0x87, 0x93, 0xf5, 0xa9, 0xc1, 0x1c, 0xa1, 0x02,
+ 0x70, 0x43, 0xae, 0xbb, 0x12, 0x61, 0x24, 0x63, 0x95, 0x22, 0xc1, 0x2c,
+ 0xb5, 0x08, 0x18, 0x11, 0x9a, 0x35, 0x00, 0x7e, 0x7a, 0x51, 0x0d, 0x1f,
+ 0x24, 0xfe, 0xb9, 0xd2, 0x9f, 0x44, 0x52, 0x0b, 0x97, 0x4b, 0xec, 0x8d,
+ 0x79, 0x84, 0x14, 0xcd, 0xa5, 0xe8, 0x2c, 0xf2, 0x39, 0x9f, 0x3e, 0xbb,
+ 0x04, 0xf0, 0xb3, 0x10, 0xa3, 0xac, 0x79, 0x0f, 0x80, 0xf2, 0x8c, 0xb9,
+ 0xbf, 0x80, 0x47, 0x78, 0xfb, 0xb5, 0x8e, 0x1d, 0x54, 0x4c, 0x4e, 0x46,
+ 0x7f, 0x9f, 0xd6, 0x65, 0x3b, 0xc5, 0xb9, 0x11, 0x1b, 0x97, 0xdf, 0xc3,
+ 0xbe, 0xa0, 0x9a, 0xab, 0x12, 0x72, 0x2c, 0x99, 0xd3, 0x07, 0xff, 0x4e,
+ 0x68, 0x47, 0x13, 0xa8, 0xea, 0x7c, 0x9f, 0x74, 0xbb, 0xed, 0xdb, 0x78,
+ 0x4f, 0x5c, 0xf8, 0xe6, 0xca, 0xfd, 0xeb, 0x79, 0x2b, 0x3e, 0xf9, 0x5c,
+ 0x05, 0x77, 0xd3, 0x72, 0x01, 0xd3, 0x3f, 0x5a, 0x16, 0x16, 0xb7, 0xdb,
+ 0xe7, 0x7f, 0xf5, 0x05, 0x54, 0x9b, 0xa3, 0xa8, 0xa1, 0xda, 0x55, 0xa8,
+ 0xb3, 0x02, 0x25, 0xb6, 0x65, 0xe1, 0xff, 0x5e, 0x0a, 0x8f, 0x5b, 0xb7,
+ 0xcb, 0xbb, 0x8b, 0xaf, 0x18, 0x3a, 0x9c, 0x39, 0x2c, 0x4f, 0x2e, 0x70,
+ 0x0f, 0xde, 0x09, 0x57, 0x40, 0x77, 0x59, 0x7e, 0x72, 0xc0, 0x7d, 0xb2,
+ 0x26, 0x77, 0xa9, 0x1f, 0x3a, 0x6b, 0x96, 0x2e, 0x9c, 0x96, 0x9d, 0x4e,
+ 0x5a, 0xe3, 0xc9, 0xa8, 0x6b, 0xee, 0xe0, 0xac, 0x8d, 0x0e, 0x22, 0x61,
+ 0x5d, 0xe0, 0x1d, 0x41, 0xce, 0x6c, 0x96, 0x39, 0x4a, 0xb8, 0xf0, 0x09,
+ 0x35, 0x94, 0x63, 0x5f, 0xe2, 0xd8, 0x3c, 0x9f, 0xdb, 0x0a, 0x94, 0xdd,
+ 0x7c, 0x2a, 0x0a, 0x9e, 0x8b, 0xe9, 0xd6, 0xf2, 0x91, 0xa9, 0x30, 0x3f,
+ 0xa4, 0xf5, 0x42, 0x8f, 0x67, 0x4f, 0xfb, 0x91, 0xb7, 0x6d, 0x72, 0xd2,
+ 0x23, 0x15, 0x77, 0x0d, 0x53, 0xf6, 0x38, 0x3b, 0x36, 0xcd, 0x81, 0xc6,
+ 0xa6, 0x51, 0x5c, 0xa0, 0x1a, 0xd1, 0x07, 0x10, 0x97, 0x2b, 0xe9, 0x70,
+ 0xe9, 0x40, 0x98, 0xe9, 0xa5, 0xed, 0x98, 0xab, 0xd8, 0x6c, 0xf8, 0xbf,
+ 0xca, 0xaa, 0xe0, 0x28, 0xde, 0x59, 0xe8, 0xa4, 0x1f, 0xf2, 0x62, 0x52,
+ 0x10, 0x07, 0x97, 0x03, 0xad, 0xe3, 0xff, 0x7a, 0xed, 0x5e, 0x07, 0xcb,
+ 0xdf, 0x66, 0x89, 0xf8, 0xab, 0x83, 0xd3, 0x8f, 0xdf, 0x39, 0x97, 0x6a,
+ 0x61, 0x9c, 0xae, 0x4b, 0xc2, 0x84, 0x5b, 0xa9, 0xdb, 0xe0, 0xb3, 0xcb,
+ 0xb0, 0xbd, 0xbc, 0x8e, 0xa4, 0x3d, 0x8e, 0xa5, 0xda, 0xe5, 0xf9, 0x0d,
+ 0x29, 0x6c, 0x72, 0x6a, 0x73, 0x7c, 0xa7, 0x7a, 0x02, 0x5f, 0xd0, 0xd9,
+ 0xf6, 0x10, 0xcd, 0xea, 0x95, 0x12, 0x2d, 0xf4, 0x2b, 0x92, 0x3d, 0x36,
+ 0xef, 0x40, 0x43, 0x46, 0x02, 0x51, 0xe9, 0x72, 0x03, 0x43, 0xce, 0xe7,
+ 0x5b, 0x7b, 0xfa, 0x37, 0xe4, 0xef, 0x55, 0x49, 0x9b, 0xd6, 0x3e, 0xa2,
+ 0x93, 0xc4, 0xe6, 0xa6, 0x32, 0x40, 0x99, 0x5f, 0xee, 0xb2, 0xd9, 0x92,
+ 0x0f, 0xad, 0x23, 0x68, 0xe9, 0xf6, 0xbf, 0xf2, 0x58, 0x07, 0x34, 0x63,
+ 0x07, 0x9e, 0x1d, 0xe0, 0xfa, 0x2d, 0x5e, 0x76, 0xa4, 0x92, 0x2d, 0xff,
+ 0xf4, 0x3a, 0x3c, 0xbd, 0x70, 0xb2, 0xde, 0x99, 0xbd, 0xa5, 0x3b, 0xae,
+ 0xa0, 0x1a, 0x10, 0xa8, 0x2b, 0xd7, 0xc2, 0x52, 0x9e, 0xc9, 0x36, 0xaa,
+ 0x0c, 0x82, 0x6f, 0x4f, 0x1f, 0x46, 0x16, 0x15, 0x35, 0x07, 0x63, 0x25,
+ 0x68, 0xf1, 0x9f, 0x60, 0x2b, 0x9e, 0xbe, 0xec, 0x08, 0x84, 0x60, 0x20,
+ 0x1b, 0x5c, 0xbc, 0x6c, 0x0d, 0x36, 0x42, 0x87, 0x0f, 0xb4, 0xd8, 0xb0,
+ 0xf1, 0x1e, 0xfe, 0xfc, 0x56, 0xe5, 0xd8, 0xd2, 0x03, 0x5b, 0x4e, 0x6b,
+ 0xc6, 0x31, 0x8a, 0xcc, 0x29, 0xa4, 0x54, 0x72, 0xff, 0x2b, 0x5f, 0xe4,
+ 0x4f, 0xfc, 0x5c, 0x66, 0x7c, 0x52, 0x3f, 0xd6, 0xc4, 0x85, 0x86, 0x2b,
+ 0x23, 0x20, 0xb6, 0x83, 0x00, 0x6f, 0xa4, 0x23, 0x68, 0x84, 0x4c, 0xb9,
+ 0xbc, 0x3b, 0xac, 0xc5, 0xed, 0x03, 0xad, 0xa5, 0xc8, 0x73, 0x98, 0x9b,
+ 0x00, 0xe6, 0xdf, 0x60, 0xc6, 0x6c, 0xe3, 0x80, 0xe2, 0x72, 0xae, 0x2b,
+ 0xeb, 0x5f, 0x92, 0x62, 0x34, 0x70, 0x0d, 0x9b, 0x51, 0xc3, 0x96, 0x3d,
+ 0x27, 0x27, 0x28, 0xb1, 0x44, 0x9d, 0x4b, 0x9a, 0x59, 0x43, 0x4d, 0x52,
+ 0xbe, 0x2f, 0xd1, 0xdb, 0x50, 0x88, 0x48, 0xf4, 0x02, 0x4c, 0x95, 0x94,
+ 0x43, 0x17, 0x08, 0x22, 0x15, 0x8d, 0x37, 0x03, 0x73, 0x9a, 0x10, 0x0b,
+ 0x62, 0xa8, 0x21, 0x5b, 0x04, 0x9e, 0x40, 0x63, 0xf1, 0xee, 0x34, 0xaa,
+ 0xa6, 0xec, 0xeb, 0xb5, 0xc8, 0xf9, 0x67, 0x9b, 0x1e, 0x4f, 0x1e, 0xbc,
+ 0xa2, 0x5e, 0xcb, 0x57, 0xf0, 0x9a, 0xa0, 0xe3, 0xcc, 0x18, 0x96, 0xa3,
+ 0xcc, 0x69, 0xe4, 0x9b, 0x16, 0xe4, 0xb3, 0x8e, 0xb6, 0xef, 0xae, 0x34,
+ 0x75, 0x43, 0x2a, 0x37, 0xe4, 0x49, 0x5c, 0xc2, 0x1d, 0x33, 0xda, 0x06,
+ 0x33, 0x1c, 0x52, 0xe5, 0x64, 0xb0, 0x00, 0x81, 0xd5, 0xb8, 0x80, 0x18,
+ 0xd6, 0xed, 0xb7, 0x9e, 0x6a, 0x6e, 0xe4, 0xd8, 0x9b, 0xcf, 0x7e, 0x4c,
+ 0xee, 0x53, 0x19, 0x48, 0x0f, 0x24, 0x3a, 0x77, 0x48, 0xbe, 0x09, 0xb8,
+ 0x87, 0x39, 0x0f, 0xc4, 0xb5, 0x90, 0x5e, 0x5c, 0x48, 0x1d, 0xe6, 0xf4,
+ 0x42, 0x1e, 0xd8, 0xa4, 0x3d, 0x0f, 0x61, 0x5d, 0xd7, 0x0b, 0x7a, 0x70,
+ 0x86, 0x9d, 0x12, 0x83, 0x13, 0xdf, 0x92, 0x64, 0xca, 0x39, 0x1d, 0x58,
+ 0x80, 0xc8, 0xdf, 0xed, 0xbb, 0x5a, 0xf7, 0xa8, 0xfa, 0xee, 0x90, 0x68,
+ 0x18, 0xda, 0x4e, 0xfd, 0xae, 0xec, 0xf2, 0xf6, 0xd5, 0xe1, 0xf9, 0xd7,
+ 0x89, 0xd6, 0xec, 0x7e, 0xd3, 0x81, 0xa1, 0xa4, 0x6c, 0xd5, 0x1c, 0xcb,
+ 0xc8, 0x1a, 0x89, 0xbc, 0x2e, 0xc6, 0x12, 0x7e, 0x6d, 0xa0, 0xf5, 0xf8,
+ 0x72, 0x01, 0x24, 0x5f, 0xf1, 0xbe, 0x67, 0x51, 0xe1, 0x83, 0x96, 0xca,
+ 0x98, 0x9d, 0xb1, 0x27, 0x31, 0x08, 0xf2, 0xa8, 0xf1, 0x5a, 0xd2, 0xc4,
+ 0x7c, 0x3f, 0xf1, 0xc1, 0xe0, 0xcd, 0x8f, 0x71, 0xfb, 0x16, 0x5f, 0x18,
+ 0x6b, 0xa3, 0xae, 0xa0, 0x15, 0x4b, 0x01, 0xd7, 0xb5, 0xfe, 0xeb, 0xae,
+ 0xf1, 0xe3, 0x28, 0x39, 0x38, 0x9b, 0xae, 0x93, 0x69, 0xb8, 0x8c, 0x20,
+ 0xef, 0x88, 0xcd, 0x1d, 0x5b, 0xc5, 0x09, 0x6d, 0xac, 0x1e, 0xa9, 0x0d,
+ 0xc1, 0x86, 0xc6, 0xbb, 0xb5, 0xb7, 0xf7, 0x1a, 0x06, 0xf8, 0x53, 0x2c,
+ 0x51, 0x2f, 0x1b, 0x88, 0x9f, 0xac, 0x44, 0x6b, 0x8a, 0x1a, 0xc3, 0xe7,
+ 0x48, 0x64, 0x8f, 0xf5, 0x67, 0x1f, 0x93, 0x83, 0x63, 0x77, 0x27, 0xcc,
+ 0x59, 0x74, 0xb3, 0x30, 0xce, 0x11, 0x9b, 0xff, 0x6b, 0x67, 0x13, 0x47,
+ 0xae, 0xb9, 0xaa, 0xcd, 0x22, 0x4a, 0x52, 0x71, 0x12, 0x1a, 0x63, 0x6a,
+ 0x8d, 0x94, 0xd3, 0x5d, 0xd4, 0x87, 0x64, 0x7e, 0x33, 0xd7, 0xcc, 0xd7,
+ 0x5f, 0x4c, 0xb0, 0x31, 0xba, 0xb0, 0xe0, 0x3b, 0x76, 0x31, 0x2f, 0x34,
+ 0x6f, 0x58, 0x94, 0x33, 0x9d, 0xa4, 0x6b, 0x1b, 0x0d, 0x6e, 0x8e, 0x70,
+ 0xcb, 0x27, 0xce, 0x6c, 0x9c, 0x8a, 0xe1, 0xac, 0x4a, 0x45, 0xb8, 0xe5,
+ 0xd2, 0xf9, 0xf2, 0x61, 0x6d, 0xb6, 0x08, 0xaf, 0x67, 0xc6, 0xdd, 0x31,
+ 0x7d, 0x6c, 0xe7, 0x0a, 0x05, 0x40, 0xcd, 0x01, 0xfa, 0xbe, 0x57, 0x65,
+ 0x21, 0x42, 0xb9, 0x26, 0x74, 0x4d, 0x3e, 0xef, 0x8a, 0x92, 0x90, 0x8b,
+ 0x96, 0xbe, 0x6d, 0x16, 0x5a, 0x58, 0x75, 0x8e, 0xec, 0x5d, 0xfa, 0xe7,
+ 0x03, 0x3e, 0x15, 0x25, 0x09, 0xcc, 0x71, 0x81, 0xac, 0xfb, 0xf6, 0xe1,
+ 0xce, 0xa0, 0xff, 0xee, 0xf9, 0x1a, 0xd4, 0x29, 0x26, 0x82, 0x4d, 0x50,
+ 0xc6, 0xe6, 0xf8, 0x78, 0x26, 0x27, 0x6d, 0x00, 0x7d, 0xb9, 0x2b, 0x39,
+ 0xdf, 0x7c, 0x37, 0xe5, 0xd1, 0x63, 0xbb, 0x44, 0x4b, 0x46, 0xe7, 0x4c,
+ 0x08, 0x75, 0xe1, 0x4c, 0x5b, 0xa8, 0xd8, 0x8d, 0x4e, 0x80, 0xee, 0x7c,
+ 0xcd, 0xf4, 0x9d, 0xb4, 0xc7, 0x59, 0x37, 0x81, 0x66, 0x12, 0xbc, 0xe4,
+ 0xbd, 0xda, 0x0d, 0x22, 0x12, 0xb2, 0x0d, 0xee, 0xd2, 0x2d, 0xbf, 0x73,
+ 0x5c, 0x58, 0x43, 0x3f, 0xb3, 0xd1, 0x9a, 0x65, 0xb7, 0xbe, 0xc0, 0x6b,
+ 0xb7, 0xd2, 0x47, 0x18, 0xac, 0x63, 0xb9, 0x01, 0x9f, 0xbc, 0x6c, 0x17,
+ 0x4f, 0x1a, 0xae, 0x01, 0x7e, 0xd1, 0x99, 0x39, 0x5b, 0x2d, 0xf2, 0xec,
+ 0xae, 0x03, 0x48, 0x48, 0x37, 0x5d, 0xbd, 0xd2, 0x75, 0xd9, 0x64, 0x46,
+ 0x14, 0xf2, 0xc6, 0xe7, 0x27, 0xac, 0xf7, 0x52, 0xbc, 0x07, 0xb4, 0x0a,
+ 0x7d, 0xf8, 0xf6, 0x1e, 0xf2, 0xb4, 0xcd, 0xfc, 0x9c, 0x90, 0xe0, 0xd5,
+ 0x3d, 0x77, 0xc4, 0x95, 0x30, 0x27, 0x93, 0xe6, 0xc0, 0x24, 0xb4, 0x6a,
+ 0x4b, 0x79, 0x98, 0x64, 0x80, 0x6b, 0x17, 0x34, 0xb6, 0xcb, 0x3f, 0xbd,
+ 0xe6, 0x0d, 0x9f, 0xe6, 0xfd, 0x8d, 0x7c, 0x36, 0x00, 0x87, 0x0b, 0x57,
+ 0xdc, 0x66, 0x46, 0x7f, 0xce, 0xed, 0x75, 0x37, 0xb4, 0xb1, 0xfc, 0x8d,
+ 0x22, 0x3c, 0x40, 0x48, 0x87, 0x51, 0xb6, 0x53, 0xdb, 0x15, 0x4d, 0x84,
+ 0x2d, 0xcb, 0xee, 0x66, 0xdc, 0x87, 0x6d, 0x22, 0x3b, 0x4e, 0xf1, 0x96,
+ 0x2e, 0x20, 0x75, 0x88, 0xa2, 0x52, 0xed, 0x77, 0xef, 0xe3, 0x9a, 0x2c,
+ 0x50, 0x67, 0xf5, 0x37, 0x9c, 0xab, 0xc4, 0x93, 0xbe, 0xd2, 0xfa, 0x0e,
+ 0xb5, 0x8d, 0x23, 0x3d, 0xd3, 0xb2, 0x50, 0x58, 0x40, 0xfc, 0x58, 0xf7,
+ 0x89, 0x91, 0xeb, 0x25, 0x5b, 0x8a, 0xe5, 0x51, 0xe3, 0xbd, 0x06, 0x05,
+ 0x45, 0x49, 0x6a, 0xcd, 0x1f, 0xc5, 0x9b, 0xa7, 0x91, 0x78, 0x45, 0x6a,
+ 0xd3, 0x65, 0x8d, 0x8c, 0xdb, 0x58, 0xbb, 0x40, 0x40, 0x8c, 0x50, 0x8f,
+ 0xd2, 0x31, 0xad, 0x19, 0x81, 0x89, 0xcb, 0xfb, 0x8c, 0xa3, 0xf4, 0x5b,
+ 0xc3, 0xe9, 0x7f, 0xff, 0x25, 0x79, 0xc9, 0xf6, 0x79, 0x2a, 0x5f, 0x31,
+ 0xf8, 0xb0, 0xd3, 0xc2, 0x91, 0x98, 0xe9, 0xe2, 0x8d, 0x91, 0xc3, 0xb1,
+ 0xc0, 0x93, 0x51, 0x97, 0xc1, 0x26, 0x79, 0xac, 0x9e, 0x73, 0xb3, 0xec,
+ 0xac, 0x4b, 0x78, 0x69, 0x94, 0x27, 0xf9, 0xef, 0x3f, 0x9d, 0xb8, 0x34,
+ 0x03, 0xa6, 0xb3, 0x14, 0x2d, 0x7e, 0xb1, 0x5f, 0xc3, 0x05, 0x8b, 0xd8,
+ 0x17, 0xe5, 0x11, 0xbb, 0x49, 0x13, 0x78, 0x6a, 0x60, 0x6f, 0xcc, 0x93,
+ 0x16, 0xc1, 0x5c, 0xfd, 0x43, 0xad, 0x03, 0xfe, 0x43, 0x51, 0x05, 0xa1,
+ 0xb1, 0xde, 0xaa, 0x7d, 0x4e, 0x66, 0xd6, 0x96, 0xc3, 0x0c, 0xc2, 0x68,
+ 0x9d, 0x17, 0xd3, 0xfd, 0x69, 0xd3, 0x25, 0xf8, 0x9a, 0x73, 0xaa, 0x5a,
+ 0xd8, 0xae, 0xcc, 0x2c, 0x81, 0xe7, 0x1a, 0x62, 0x77, 0x0b, 0x0e, 0x19,
+ 0x52, 0x6c, 0x56, 0x32, 0xdb, 0x44, 0x52, 0x4d, 0xa4, 0x88, 0x07, 0x94,
+ 0x13, 0x38, 0x74, 0xd6, 0xab, 0x95, 0x87, 0x95, 0xfe, 0xe8, 0x65, 0x48,
+ 0x23, 0x4a, 0x69, 0x4d, 0x12, 0xc6, 0xa2, 0x1b, 0x3a, 0x65, 0xf5, 0x84,
+ 0x0a, 0xd1, 0xa4, 0xe5, 0x64, 0xcb, 0x3a, 0x40, 0x6f, 0x0f, 0xa5, 0x87,
+ 0x57, 0x8a, 0xec, 0x69, 0xbc, 0xdc, 0xc3, 0xae, 0x6b, 0xbe, 0x50, 0x59,
+ 0x0e, 0xa8, 0x66, 0xfe, 0x28, 0xc3, 0x0b, 0xb2, 0x33, 0xf8, 0xa7, 0x46,
+ 0x29, 0x69, 0x81, 0x5b, 0xa5, 0x6f, 0xfe, 0xe3, 0xc2, 0xee, 0xbc, 0xdc,
+ 0xc6, 0xab, 0x72, 0x46, 0xa3, 0xc3, 0x0b, 0x54, 0xf5, 0xa4, 0xb2, 0x63,
+ 0xba, 0xf7, 0x96, 0x85, 0xc4, 0xd5, 0x51, 0x96, 0xb6, 0x42, 0x92, 0xbc,
+ 0x7b, 0x0b, 0xbf, 0x13, 0x9b, 0x0a, 0x1a, 0xfa, 0x3f, 0x82, 0x1b, 0x54,
+ 0xc2, 0x39, 0x08, 0xc9, 0xc5, 0xa1, 0x95, 0xc5, 0xf9, 0x20, 0xeb, 0xb5,
+ 0x7f, 0x6d, 0x5a, 0x8a, 0x36, 0x1e, 0x7a, 0x50, 0xe1, 0xce, 0xd5, 0x23,
+ 0xf5, 0x7e, 0x47, 0x94, 0x81, 0xfe, 0xca, 0x43, 0xae, 0x27, 0x3e, 0xf3,
+ 0x69, 0x87, 0xbd, 0x7e, 0x86, 0x4e, 0xc5, 0xdf, 0xdd, 0x83, 0x4c, 0x28,
+ 0x68, 0x38, 0x95, 0x81, 0x98, 0xba, 0xb4, 0xd5, 0xf1, 0xcf, 0xb7, 0x57,
+ 0x7b, 0x40, 0xea, 0x23, 0x69, 0xf2, 0x5e, 0xa3, 0x7b, 0xab, 0x4e, 0x89,
+ 0xe2, 0x3f, 0x6a, 0xa0, 0x9c, 0xaa, 0xf8, 0x78, 0xa9, 0x40, 0x28, 0xdd,
+ 0x40, 0x11, 0x23, 0xe1, 0xc2, 0x98, 0x92, 0xbe, 0x00, 0xc5, 0x2e, 0x48,
+ 0x35, 0x0a, 0xf7, 0x93, 0xe2, 0xc0, 0x18, 0xd9, 0x34, 0x99, 0x9f, 0x28,
+ 0xb0, 0x18, 0xa7, 0xe5, 0x20, 0x3f, 0xd0, 0x5d, 0xc4, 0xca, 0xac, 0x25,
+ 0x0b, 0x50, 0x91, 0x69, 0xea, 0xe9, 0x9d, 0x2b, 0x5a, 0x25, 0x78, 0x26,
+ 0x3d, 0xdc, 0x67, 0x0c, 0xf3, 0x30, 0x9e, 0xe7, 0xfa, 0x54, 0x88, 0x3e,
+ 0x96, 0x2d, 0x67, 0x79, 0x20, 0x2c, 0xfc, 0x70, 0xb1, 0x3f, 0xf6, 0x57,
+ 0x8e, 0x1a, 0x64, 0x08, 0x03, 0x34, 0x55, 0x6a, 0xb3, 0x11, 0xf1, 0xa9,
+ 0xcc, 0x87, 0x60, 0x94, 0xe9, 0x58, 0x37, 0x56, 0xb0, 0x3d, 0x8d, 0x26,
+ 0xa1, 0x55, 0xfc, 0x8e, 0xd3, 0xa6, 0xfd, 0x74, 0x42, 0x82, 0x8d, 0x45,
+ 0xf9, 0x0d, 0x46, 0x23, 0x7d, 0xd3, 0xfe, 0x91, 0x0d, 0x2c, 0x4a, 0xda,
+ 0x3b, 0x52, 0x89, 0x3b, 0xf6, 0x7b, 0xb1, 0x3f, 0x52, 0xea, 0x08, 0xf8,
+ 0xc2, 0xe8, 0xb1, 0x39, 0x8c, 0xee, 0x62, 0x22, 0x99, 0x5b, 0xf2, 0xe7,
+ 0x05, 0xb8, 0x6e, 0x97, 0x5f, 0x0c, 0x3e, 0x8c, 0xdc, 0x7f, 0xbf, 0xfe,
+ 0x4d, 0x79, 0xc3, 0xc0, 0x42, 0xab, 0xcb, 0xbd, 0x95, 0x8f, 0x01, 0xf3,
+ 0xdc, 0x12, 0x5a, 0x96, 0x70, 0x31, 0x85, 0xf4, 0x5d, 0x8f, 0xea, 0x6f,
+ 0xab, 0xf9, 0x5b, 0xb1, 0x34, 0x03, 0x80, 0xea, 0x80, 0xa0, 0x7b, 0xdf,
+ 0xa4, 0x3b, 0x5b, 0x68, 0xb4, 0x92, 0xa4, 0x64, 0x5f, 0x15, 0x88, 0x4d,
+ 0xec, 0xfc, 0x41, 0xa2, 0x36, 0xd9, 0xc3, 0x6a, 0x48, 0x9d, 0x3d, 0x1f,
+ 0x58, 0x47, 0x16, 0xe5, 0xd4, 0xd0, 0x33, 0xb0, 0x9b, 0xef, 0xe4, 0x83,
+ 0x85, 0x86, 0xc9, 0x4c, 0xb6, 0x83, 0x99, 0xa8, 0x2a, 0x25, 0x0d, 0x12,
+ 0x38, 0x48, 0xd8, 0xff, 0xdf, 0xc2, 0xda, 0xf3, 0x7c, 0xae, 0xbc, 0x05,
+ 0x1a, 0xfc, 0x90, 0x81, 0x9e, 0x08, 0x27, 0x45, 0x36, 0x56, 0x5f, 0xe6,
+ 0x9e, 0xf9, 0x52, 0xd5, 0x48, 0x88, 0x6d, 0x63, 0x90, 0xbb, 0xcc, 0x3d,
+ 0xef, 0x43, 0x88, 0x3c, 0x3d, 0xd0, 0x77, 0x4d, 0x13, 0x85, 0xad, 0xd4,
+ 0x1c, 0x18, 0xb9, 0x89, 0xbc, 0xb8, 0x11, 0xd0, 0x0e, 0x98, 0x27, 0x15,
+ 0x08, 0x7d, 0xf1, 0x76, 0xe6, 0x53, 0x3e, 0x0d, 0x98, 0x08, 0xdc, 0x79,
+ 0x44, 0x4f, 0x5d, 0x6a, 0x9f, 0xff, 0x9f, 0x69, 0xb4, 0x07, 0x8a, 0x27,
+ 0x78, 0xb1, 0x9b, 0x1e, 0x21, 0xc8, 0xa3, 0xc7, 0xeb, 0x09, 0x83, 0xb2,
+ 0x1b, 0x25, 0x0c, 0x43, 0xc1, 0xed, 0xc0, 0xd5, 0x01, 0xc3, 0x2e, 0x9d,
+ 0x47, 0x14, 0x1a, 0x30, 0xe2, 0xc6, 0x54, 0x3d, 0xbc, 0xc3, 0xc1, 0x5f,
+ 0xb1, 0x62, 0x18, 0xee, 0x00, 0xc5, 0x43, 0x29, 0xe8, 0x6b, 0x3c, 0xcb,
+ 0x5f, 0x79, 0xdc, 0x28, 0x32, 0xb5, 0x63, 0x86, 0xad, 0xc5, 0x06, 0xf9,
+ 0x26, 0x2e, 0x7c, 0x2c, 0x1f, 0x76, 0x3e, 0x63, 0x2a, 0xdf, 0x5b, 0x40,
+ 0x56, 0x46, 0x1b, 0xd7, 0x42, 0xa6, 0x7e, 0x10, 0x02, 0x96, 0x2b, 0x9a,
+ 0x1e, 0xaa, 0x5c, 0x5f, 0x46, 0xd6, 0xc7, 0x32, 0x62, 0x39, 0xf0, 0x9b,
+ 0x72, 0xd5, 0xba, 0x87, 0x8b, 0x06, 0x61, 0xec, 0x57, 0x5f, 0xcc, 0x81,
+ 0x14, 0xc4, 0xc9, 0x73, 0xe8, 0xd1, 0x47, 0x6f, 0x7b, 0xbd, 0x1b, 0x63,
+ 0x99, 0x1a, 0x2d, 0xc2, 0xaf, 0xe9, 0x28, 0x27, 0xa2, 0x2b, 0xc8, 0x88,
+ 0x45, 0x47, 0xbc, 0x52, 0x29, 0x9f, 0x7e, 0x63, 0x2c, 0xb0, 0xcb, 0xa0,
+ 0xcb, 0x83, 0x42, 0xe5, 0xd7, 0xf6, 0xa4, 0xd3, 0x27, 0xbf, 0x89, 0x49,
+ 0x83, 0xdc, 0x1f, 0x2b, 0xbd, 0x32, 0x58, 0x5b, 0x9d, 0xd6, 0x74, 0x7c,
+ 0x8d, 0x8b, 0x38, 0xca, 0x5e, 0xce, 0xd6, 0xf6, 0x95, 0xcd, 0x5b, 0xb7,
+ 0x85, 0x3a, 0x64, 0xed, 0x3a, 0x2e, 0xcc, 0xeb, 0xad, 0x0a, 0x87, 0xc7,
+ 0x93, 0xf8, 0x55, 0x2c, 0x5b, 0xac, 0x63, 0xfc, 0x76, 0xa8, 0xd7, 0xa3,
+ 0xcf, 0x6e, 0x46, 0x8b, 0x18, 0x51, 0x5b, 0xdd, 0xdc, 0x00, 0x5f, 0xbe,
+ 0x34, 0xc2, 0x92, 0xb2, 0xcf, 0x11, 0x83, 0x85, 0x84, 0x23, 0x1b, 0xa9,
+ 0x54, 0xd9, 0xbf, 0x94, 0xa8, 0x1d, 0x90, 0x82, 0xf0, 0xbb, 0x86, 0x39,
+ 0xb8, 0x2b, 0x95, 0x78, 0xc2, 0xd9, 0xa4, 0x72, 0x0f, 0x11, 0xf0, 0x84,
+ 0xc3, 0x49, 0x8f, 0xdb, 0xa4, 0xf3, 0x85, 0x1b, 0xd6, 0xc5, 0xcb, 0x47,
+ 0xea, 0xa6, 0x69, 0xa5, 0x48, 0x26, 0xab, 0xd9, 0x95, 0xfb, 0xaa, 0x6a,
+ 0x55, 0xcc, 0x25, 0x43, 0xfe, 0x2b, 0x70, 0x66, 0xb9, 0x44, 0xef, 0x49,
+ 0x34, 0xcf, 0x66, 0xd0, 0x0f, 0x0e, 0x46, 0x2b, 0x6c, 0x94, 0x9e, 0x3f,
+ 0x9c, 0xa0, 0x6d, 0xd0, 0x15, 0x00, 0x42, 0x8f, 0x5b, 0x53, 0x11, 0x40,
+ 0x26, 0x06, 0x02, 0xbc, 0x21, 0xd5, 0x32, 0x0a, 0xe2, 0xb7, 0xb5, 0x49,
+ 0xd2, 0x19, 0x79, 0xce, 0x06, 0x40, 0x4f, 0x48, 0xca, 0x4f, 0x10, 0x15,
+ 0x91, 0xf9, 0x41, 0xd9, 0xcd, 0xb4, 0x64, 0x1b, 0xed, 0xa4, 0xca, 0x4e,
+ 0x7c, 0xcc, 0xa8, 0xa7, 0xa4, 0x87, 0xb5, 0x68, 0xd7, 0xba, 0x70, 0x69,
+ 0xd6, 0x4e, 0x05, 0x2a, 0xb6, 0x9c, 0x4f, 0x9b, 0xc4, 0x01, 0x8d, 0xc5,
+ 0x1a, 0x2b, 0x53, 0x97, 0x2a, 0x9e, 0x66, 0x87, 0xc5, 0xf8, 0xd6, 0x6b,
+ 0x28, 0xcf, 0x4d, 0x8b, 0x14, 0xa9, 0x4b, 0xdd, 0xe7, 0xd9, 0x22, 0xc7,
+ 0xa1, 0xeb, 0xe6, 0x4c, 0x1f, 0x32, 0xe4, 0xb0, 0x8d, 0x1a, 0x8c, 0x06,
+ 0xfe, 0xc4, 0x4f, 0x40, 0x4d, 0x30, 0x48, 0x00, 0x00, 0xf4, 0xd4, 0x5f,
+ 0x89, 0xb7, 0x81, 0xde, 0x32, 0x73, 0x42, 0x14, 0xf0, 0x56, 0x42, 0x0a,
+ 0xac, 0x9a, 0x1e, 0x22, 0x2f, 0xf0, 0x32, 0x59, 0x75, 0x80, 0x30, 0x80,
+ 0x4a, 0x17, 0xe5, 0x0d, 0x5a, 0xcb, 0x54, 0x4b, 0xbf, 0x45, 0x7b, 0x1c,
+ 0x8a, 0xe7, 0x32, 0xec, 0x5c, 0xd5, 0xd6, 0x5a, 0xc5, 0x2e, 0xb7, 0x7a,
+ 0x45, 0xc5, 0xe3, 0x50, 0x17, 0x54, 0x8d, 0x05, 0xdb, 0x35, 0xa2, 0x2d,
+ 0xce, 0x77, 0x08, 0x04, 0x1b, 0x0e, 0x2d, 0x40, 0x01, 0xca, 0x4a, 0x29,
+ 0x9f, 0x0e, 0x54, 0x36, 0x69, 0x94, 0xec, 0x0e, 0x34, 0x5f, 0xc1, 0xba,
+ 0x8b, 0x35, 0x4e, 0xde, 0x1a, 0x75, 0x88, 0xac, 0xc9, 0xde, 0x13, 0x2f,
+ 0x31, 0xfd, 0x8f, 0x5d, 0x93, 0x59, 0xf2, 0xf2, 0x22, 0x88, 0x70, 0x18,
+ 0x49, 0x46, 0x3f, 0x35, 0x8a, 0x36, 0xf4, 0x34, 0xd1, 0x1a, 0x84, 0x72,
+ 0xa6, 0x02, 0x75, 0xaa, 0x7c, 0x6d, 0x16, 0x89, 0x21, 0xfe, 0x2b, 0x42,
+ 0x36, 0xf4, 0x8e, 0xf2, 0xb4, 0xa8, 0xd2, 0xc3, 0xaa, 0xd2, 0x3e, 0xb0,
+ 0x22, 0xd9, 0xbd, 0x5d, 0x6c, 0x7f, 0xa9, 0x88, 0xb8, 0x63, 0x8d, 0x50,
+ 0x4e, 0x7d, 0x4b, 0x85, 0xd7, 0x3a, 0x5b, 0xd4, 0x4d, 0xfb, 0xd8, 0xae,
+ 0xd8, 0x66, 0xb9, 0x85, 0x51, 0xb2, 0x4c, 0x4f, 0xe6, 0xe6, 0x46, 0x66,
+ 0x30, 0xd3, 0xc2, 0x0c, 0x87, 0x6e, 0x65, 0x5d, 0x04, 0x54, 0xdb, 0x3d,
+ 0x60, 0xae, 0x1f, 0xc3, 0x07, 0x48, 0xd0, 0x83, 0x34, 0x27, 0xab, 0xa7,
+ 0x7f, 0xe6, 0x91, 0x21, 0xf8, 0x9c, 0x45, 0x57, 0x2a, 0x76, 0x64, 0xa6,
+ 0xc6, 0xf4, 0xf3, 0x33, 0x2a, 0xfe, 0x76, 0x70, 0xff, 0xba, 0x25, 0xbe,
+ 0x0e, 0xb8, 0x42, 0x8f, 0x22, 0x43, 0xfe, 0x16, 0x0f, 0x2a, 0xa9, 0x3e,
+ 0x0e, 0xe5, 0x5d, 0x8a, 0xe2, 0x79, 0x05, 0xb1, 0x19, 0xb5, 0x33, 0x2d,
+ 0xb7, 0xf2, 0x82, 0xad, 0x17, 0x24, 0x06, 0x53, 0xb9, 0xd4, 0x3c, 0xc5,
+ 0x74, 0xc1, 0x2e, 0xaa, 0x5c, 0x88, 0x85, 0x0f, 0xa7, 0x1c, 0x67, 0x97,
+ 0x6b, 0x03, 0x85, 0x76, 0xf9, 0xce, 0x16, 0xdd, 0x1a, 0x11, 0x8e, 0xec,
+ 0xfa, 0x0e, 0xaf, 0x57, 0x72, 0x02, 0x0b, 0xbb, 0x63, 0x48, 0xf4, 0x27,
+ 0x9f, 0xe6, 0x32, 0x63, 0xb1, 0xac, 0x0d, 0x0e, 0x5b, 0x22, 0x43, 0xe9,
+ 0x06, 0xa8, 0x50, 0x0f, 0x15, 0xb6, 0x37, 0x31, 0x2b, 0x0b, 0x6a, 0xc1,
+ 0x33, 0x2f, 0x91, 0xe2, 0x7c, 0x94, 0x71, 0xaf, 0xbe, 0xd5, 0x91, 0x39,
+ 0x30, 0x72, 0xba, 0x26, 0xfa, 0x48, 0x05, 0x6a, 0x1f, 0x4b, 0xa0, 0x7d,
+ 0x9d, 0x24, 0x73, 0x89, 0xfa, 0xe2, 0xab, 0xbd, 0x46, 0x25, 0x65, 0x9e,
+ 0xf1, 0xea, 0xb9, 0x25, 0x18, 0x87, 0x04, 0xdd, 0xdb, 0x1e, 0xd9, 0xde,
+ 0x32, 0xfd, 0x3e, 0xa3, 0x1f, 0x26, 0xf0, 0x90, 0xfb, 0x2c, 0xaf, 0xba,
+ 0x6a, 0x62, 0xf5, 0x66, 0xb2, 0x97, 0x26, 0x74, 0x70, 0xf8, 0x9a, 0x2c,
+ 0x5c, 0xbf, 0x89, 0x0a, 0x2b, 0x2b, 0xb1, 0x91, 0x1d, 0xaa, 0x39, 0x20,
+ 0x2b, 0x67, 0x8f, 0x38, 0xf4, 0x91, 0x4b, 0xad, 0x6b, 0xcb, 0x02, 0xfb,
+ 0x52, 0xf9, 0x38, 0x5a, 0xd3, 0x48, 0xc8, 0x4d, 0xc6, 0x9e, 0x7d, 0x1b,
+ 0x60, 0x6d, 0xa3, 0x3a, 0x07, 0x74, 0xde, 0xb8, 0x64, 0xd9, 0xed, 0x7e,
+ 0x77, 0x38, 0x7e, 0x0c, 0x93, 0x40, 0x89, 0xc8, 0x7e, 0x41, 0xb4, 0x7b,
+ 0xde, 0xb3, 0xa5, 0xbc, 0x6a, 0xf4, 0x74, 0xd6, 0x82, 0x47, 0x45, 0x99,
+ 0x5f, 0x7f, 0x3f, 0x77, 0x81, 0x82, 0xd2, 0xbb, 0xb0, 0x62, 0x3a, 0x9a,
+ 0x7c, 0x21, 0x10, 0xf6, 0x75, 0x92, 0xb8, 0xdb, 0x26, 0xb7, 0x53, 0xde,
+ 0xe4, 0x81, 0xc2, 0x49, 0xc1, 0x91, 0xfe, 0x64, 0x83, 0x27, 0xc3, 0xe5,
+ 0x4f, 0x33, 0x95, 0x65, 0x7c, 0x0f, 0xcf, 0xbc, 0x22, 0x82, 0x70, 0x89,
+ 0xe5, 0xba, 0x85, 0x05, 0x3b, 0xc9, 0x7b, 0xc1, 0x53, 0x36, 0x7f, 0x66,
+ 0x8f, 0x52, 0xdb, 0xd4, 0x8e, 0x2b, 0x3d, 0x24, 0x13, 0x6b, 0x4c, 0xa8,
+ 0x9f, 0xfa, 0x3c, 0x59, 0xb9, 0x2b, 0x71, 0xbb, 0x4a, 0x46, 0x9d, 0x57,
+ 0xe1, 0x05, 0x28, 0xd4, 0x22, 0x93, 0x2f, 0x12, 0x57, 0xf5, 0xe2, 0xd3,
+ 0x95, 0xd0, 0xb9, 0xe4, 0x23, 0x2d, 0x75, 0xd6, 0x2c, 0x88, 0x68, 0x4a,
+ 0x5d, 0x94, 0xfb, 0x60, 0xd6, 0x3c, 0xae, 0xde, 0xb4, 0x0a, 0x49, 0x1a,
+ 0x74, 0x61, 0x9a, 0xd8, 0xe4, 0xd4, 0x3e, 0x85, 0xc5, 0xda, 0x0d, 0x25,
+ 0x6d, 0xc8, 0x9f, 0x51, 0xa9, 0x9b, 0xa0, 0xe5, 0x7b, 0x5e, 0xe5, 0x28,
+ 0xa9, 0x8e, 0xd3, 0x06, 0x85, 0x09, 0x07, 0x74, 0xc0, 0xd0, 0x09, 0xa9,
+ 0xf1, 0xea, 0xca, 0x82, 0x8c, 0x4a, 0xf6, 0x02, 0x8d, 0xf1, 0xd9, 0x86,
+ 0xff, 0x1f, 0x11, 0x0c, 0xd9, 0x00, 0x4f, 0x5b, 0x22, 0x98, 0x4e, 0x50,
+ 0x7d, 0xc6, 0x34, 0xbb, 0x23, 0xdb, 0x75, 0xc1, 0x5b, 0x17, 0x44, 0x3a,
+ 0x3f, 0x91, 0xb7, 0x0a, 0x17, 0xaf, 0x3e, 0x4f, 0xb6, 0xbd, 0x3b, 0x69,
+ 0xb9, 0x92, 0xbf, 0xe3, 0x47, 0x89, 0x41, 0xe2, 0x55, 0xbd, 0xb5, 0x4c,
+ 0xbe, 0xb7, 0x57, 0xd6, 0x41, 0x12, 0xf4, 0x20, 0x1b, 0x10, 0x22, 0x3f,
+ 0x8b, 0x62, 0x0f, 0x27, 0x76, 0xdf, 0xa7, 0xef, 0x73, 0xd1, 0x3b, 0x61,
+ 0x18, 0xd7, 0x90, 0xd8, 0x1b, 0xdb, 0xd1, 0xa9, 0x2c, 0x4e, 0x88, 0xa8,
+ 0x0c, 0xc9, 0xe4, 0xed, 0x73, 0x76, 0x8a, 0x4c, 0xda, 0xcd, 0xc5, 0x8b,
+ 0xce, 0x70, 0xcb, 0xc2, 0x57, 0x72, 0xb8, 0xe5, 0xfc, 0x80, 0x02, 0xb4,
+ 0x2d, 0xa6, 0x72, 0x88, 0xde, 0x96, 0x99, 0xd1, 0x8f, 0x08, 0xec, 0xd4,
+ 0xda, 0x8f, 0x84, 0x44, 0x0f, 0x0b, 0xba, 0xc5, 0x0c, 0xac, 0xbd, 0x03,
+ 0x7e, 0x95, 0x33, 0x65, 0x5b, 0x86, 0xfb, 0x25, 0xf3, 0xe4, 0x2e, 0x7e,
+ 0x8b, 0x70, 0x19, 0x4e, 0x35, 0xe5, 0x1d, 0x22, 0xa8, 0xad, 0xc9, 0xbf,
+ 0x80, 0xd0, 0x2b, 0xc4, 0x2d, 0x5a, 0xfe, 0x67, 0x96, 0x88, 0x4f, 0xda,
+ 0x1b, 0x48, 0xa4, 0x25, 0x51, 0x8c, 0x83, 0x3f, 0x22, 0xf6, 0x5d, 0xc1,
+ 0x8f, 0x48, 0x56, 0xd3, 0xed, 0xa4, 0x12, 0x29, 0x18, 0x32, 0xa9, 0x9c,
+ 0xcc, 0x42, 0xec, 0x0c, 0xf7, 0x26, 0x2b, 0xb6, 0xd0, 0x49, 0x6d, 0xa0,
+ 0xf4, 0x59, 0x58, 0x5c, 0xab, 0x9f, 0x2f, 0xfe, 0x7b, 0x2c, 0x8b, 0xf3,
+ 0x61, 0xb1, 0xd9, 0x84, 0xd4, 0x84, 0xed, 0x0a, 0x9a, 0xc6, 0x0b, 0x0f,
+ 0x67, 0x8a, 0xdd, 0x26, 0xdb, 0xd2, 0x54, 0xea, 0x22, 0x56, 0xff, 0x8b,
+ 0x2b, 0xf8, 0xd3, 0x18, 0x10, 0x18, 0x2c, 0xeb, 0xb9, 0x4f, 0xa9, 0x3d,
+ 0xa0, 0xea, 0x86, 0xb9, 0x27, 0xb1, 0xac, 0xa8, 0xfd, 0xc1, 0x72, 0x9a,
+ 0xaa, 0xcf, 0x51, 0x27, 0x2e, 0x39, 0xb7, 0x7e, 0x29, 0x2c, 0xad, 0xe7,
+ 0x42, 0x8b, 0xd1, 0x8a, 0x91, 0x5f, 0x17, 0xd9, 0xcb, 0xf4, 0x5c, 0x9f,
+ 0xeb, 0x67, 0xf4, 0x99, 0xe8, 0x09, 0x0a, 0x83, 0xc7, 0xe0, 0x43, 0x58,
+ 0xd9, 0x1c, 0xc3, 0xb8, 0x0c, 0x13, 0xb0, 0x4e, 0x05, 0xfd, 0x9b, 0x48,
+ 0xd1, 0x33, 0x87, 0xec, 0xf1, 0x99, 0xd4, 0xd6, 0xfe, 0x74, 0x89, 0x2e,
+ 0x46, 0xe7, 0xdb, 0x5c, 0x14, 0x8b, 0x87, 0x06, 0x1e, 0x97, 0x65, 0x31,
+ 0x20, 0x3e, 0x68, 0x9f, 0x78, 0x0d, 0x6b, 0x22, 0xaf, 0x05, 0xfb, 0xba,
+ 0x8e, 0x12, 0xd3, 0x86, 0x03, 0x71, 0x45, 0x87, 0x8a, 0x2e, 0x69, 0xef,
+ 0x52, 0xe7, 0xce, 0xac, 0xe4, 0xe2, 0x9d, 0x5b, 0xc4, 0x84, 0x27, 0x38,
+ 0x2b, 0xbd, 0x3b, 0x7a, 0xed, 0xe4, 0x11, 0x7e, 0xbf, 0x30, 0xa9, 0x8a,
+ 0x86, 0x4a, 0x3c, 0xde, 0x16, 0xa9, 0x41, 0x4e, 0xb9, 0xf6, 0x31, 0xb5,
+ 0x04, 0x09, 0xaa, 0xf2, 0x66, 0x90, 0xce, 0x97, 0x21, 0x3f, 0x34, 0x8a,
+ 0x66, 0x10, 0xa2, 0xd0, 0xa9, 0x28, 0xae, 0xa5, 0xef, 0xf7, 0x1a, 0x59,
+ 0x41, 0x7d, 0xe9, 0x87, 0x17, 0x7d, 0x31, 0x28, 0xdd, 0x62, 0xb3, 0x9d,
+ 0x63, 0x16, 0xa9, 0x05, 0x53, 0xf0, 0xfa, 0x6f, 0xc9, 0x49, 0xe3, 0x02,
+ 0x37, 0xa6, 0xcb, 0xb2, 0xb2, 0x55, 0x5f, 0x89, 0xbf, 0xb7, 0x71, 0x5a,
+ 0xa0, 0x6b, 0x0a, 0x15, 0xb9, 0x8a, 0xc2, 0xec, 0xa8, 0x25, 0x75, 0x78,
+ 0x27, 0x07, 0x0a, 0x0d, 0x47, 0xc5, 0x6c, 0x59, 0x47, 0x7b, 0x31, 0x24,
+ 0xce, 0x64, 0x29, 0x66, 0xba, 0xe0, 0x4e, 0xf5, 0x3d, 0x33, 0x1a, 0xfc,
+ 0x8f, 0xac, 0xcd, 0x1c, 0x02, 0xe5, 0x60, 0xe9, 0x45, 0xad, 0x80, 0x2f,
+ 0x25, 0xb7, 0x0b, 0xb6, 0x5a, 0x5b, 0x23, 0x69, 0xf9, 0x1f, 0xe6, 0x68,
+ 0xc3, 0xc7, 0x28, 0x6d, 0x2d, 0xa6, 0x68, 0x62, 0x4e, 0x8f, 0x11, 0x86,
+ 0xee, 0x72, 0x86, 0xaa, 0x47, 0x80, 0xe4, 0x0c, 0x61, 0x51, 0x67, 0xca,
+ 0x95, 0x86, 0x89, 0xa4, 0x46, 0x8a, 0xa5, 0xa5, 0x08, 0xc8, 0xb9, 0x14,
+ 0x0f, 0x90, 0xf8, 0x76, 0xab, 0x53, 0x57, 0x18, 0xd0, 0x51, 0x70, 0xd9,
+ 0x7f, 0xd7, 0xdd, 0xe3, 0xc9, 0x17, 0x5a, 0xfe, 0x42, 0x29, 0x0f, 0xf8,
+ 0x1e, 0x48, 0x4d, 0xd1, 0x5d, 0x3e, 0x0f, 0x8f, 0x10, 0x85, 0x07, 0x0f,
+ 0x6b, 0xeb, 0x58, 0x40, 0x87, 0xd5, 0x46, 0xb5, 0x16, 0x81, 0xdc, 0x40,
+ 0x46, 0xdf, 0xde, 0xcc, 0xeb, 0xc2, 0x07, 0x71, 0x7b, 0xa2, 0x28, 0xd5,
+ 0xd0, 0xf2, 0x0c, 0x05, 0xee, 0x43, 0x3f, 0xfe, 0x09, 0xfe, 0x3b, 0x51,
+ 0xf6, 0x4a, 0xf7, 0xcb, 0xbf, 0x36, 0x99, 0xe5, 0xda, 0xf3, 0x0c, 0xaf,
+ 0xaf, 0xd1, 0x5a, 0xa0, 0x58, 0xa6, 0x19, 0x48, 0x04, 0xdd, 0x2f, 0x41,
+ 0x57, 0xf4, 0x93, 0xca, 0x5d, 0xe7, 0x61, 0x66, 0xcc, 0x12, 0x8d, 0x00,
+ 0xdb, 0x55, 0xa8, 0x69, 0x96, 0xdc, 0x5a, 0xc4, 0xcf, 0x32, 0x74, 0x30,
+ 0xcf, 0xca, 0x2e, 0xa9, 0xda, 0x3d, 0x21, 0x47, 0x15, 0xe8, 0xf1, 0xb6,
+ 0xa2, 0xdf, 0xf7, 0x9c, 0xc2, 0x43, 0xd6, 0x46, 0x64, 0x5e, 0xd4, 0x15,
+ 0xa3, 0x22, 0x90, 0x68, 0x5a, 0xfa, 0xb4, 0xdf, 0x1f, 0x54, 0xfa, 0xe5,
+ 0xb0, 0xa0, 0xad, 0x4a, 0xdf, 0xb0, 0x14, 0x44, 0x05, 0x17, 0xb5, 0xe1,
+ 0x94, 0xb2, 0x3a, 0x51, 0x6c, 0xcd, 0xfa, 0xee, 0x4f, 0x29, 0x17, 0x71,
+ 0x99, 0x2c, 0xfb, 0x1a, 0x3c, 0xa0, 0x62, 0x59, 0xc2, 0xee, 0xbd, 0x9f,
+ 0x7f, 0xf7, 0xa5, 0x7a, 0x25, 0x74, 0xbf, 0xb0, 0x59, 0xff, 0x97, 0xe4,
+ 0xff, 0xed, 0x29, 0x3d, 0x86, 0xf3, 0xf6, 0xdb, 0x6e, 0x1a, 0x11, 0x15,
+ 0x29, 0x09, 0xb6, 0x98, 0x10, 0xe3, 0x94, 0x15, 0xe2, 0x2a, 0x5c, 0xc5,
+ 0x27, 0xf4, 0x58, 0xea, 0x3d, 0x1f, 0x0b, 0xca, 0xfe, 0x5a, 0x5f, 0xb5,
+ 0x42, 0x7d, 0x72, 0x00, 0x9e, 0x45, 0xab, 0x0d, 0xd7, 0xa2, 0x75, 0x88,
+ 0x56, 0x20, 0x99, 0xc1, 0x28, 0x79, 0x9f, 0x16, 0x2b, 0x08, 0xc1, 0xe0,
+ 0x25, 0x6a, 0x01, 0xee, 0xdc, 0x03, 0xc7, 0x1c, 0xcf, 0xe6, 0x17, 0x77,
+ 0xa8, 0xb6, 0xae, 0x5a, 0xd4, 0x75, 0xd8, 0x84, 0x70, 0xf4, 0xdf, 0x0c,
+ 0xe4, 0xce, 0xaa, 0x70, 0x34, 0x61, 0xbd, 0x8a, 0x79, 0x57, 0x11, 0x63,
+ 0xd3, 0x78, 0x66, 0xb7, 0xc6, 0xbf, 0xda, 0x63, 0xc0, 0x23, 0xef, 0x9e,
+ 0xea, 0x35, 0x6d, 0x50, 0xa7, 0x04, 0x6a, 0xda, 0x30, 0x56, 0x1b, 0x6d,
+ 0xdf, 0x1c, 0xae, 0xcb, 0xa5, 0xb6, 0xd2, 0xd8, 0x5b, 0x48, 0x74, 0x01,
+ 0xc4, 0x64, 0xcc, 0x09, 0xfb, 0xc7, 0x6b, 0xe5, 0xf8, 0xd6, 0x0b, 0x0b,
+ 0x71, 0x39, 0xfa, 0xee, 0x58, 0x0e, 0xf3, 0xf9, 0x0e, 0x5b, 0x38, 0xeb,
+ 0x16, 0xc5, 0xb6, 0x5a, 0x26, 0xeb, 0xf8, 0x23, 0xe2, 0x4a, 0x10, 0x59,
+ 0xc3, 0xb2, 0x38, 0x2a, 0x0f, 0x64, 0x3b, 0xa1, 0x65, 0x8b, 0x30, 0x7c,
+ 0x54, 0x8e, 0xe3, 0x54, 0x75, 0x9c, 0xd6, 0xb2, 0x0f, 0x37, 0x5b, 0xe0,
+ 0xf7, 0x25, 0x3c, 0x73, 0xbd, 0x8f, 0xba, 0xb4, 0xa0, 0xd2, 0xcf, 0x90,
+ 0x57, 0x19, 0x65, 0x64, 0x9f, 0x4d, 0x34, 0x9a, 0x40, 0x6e, 0xbf, 0xeb,
+ 0xbe, 0x6f, 0xfc, 0x76, 0x6b, 0x0b, 0x14, 0xda, 0xa1, 0x09, 0xff, 0x11,
+ 0x2c, 0xa8, 0x8f, 0x70, 0xc9, 0xe1, 0x96, 0x2b, 0xf8, 0x2c, 0x21, 0x87,
+ 0x21, 0xfd, 0x5d, 0xd0, 0x21, 0x2e, 0x75, 0xfb, 0x46, 0xd9, 0x1d, 0xa2,
+ 0x72, 0x9d, 0x44, 0xf5, 0x29, 0xfa, 0x01, 0x2c, 0xc7, 0x4f, 0x42, 0xa4,
+ 0x55, 0xb1, 0x90, 0x3f, 0xec, 0x8c, 0x56, 0xba, 0x69, 0x57, 0xd0, 0xf1,
+ 0x9d, 0x9f, 0xd3, 0x83, 0x21, 0xb8, 0xda, 0xc1, 0x90, 0x03, 0xd5, 0xfb,
+ 0x7e, 0xc8, 0xbd, 0x96, 0xe8, 0xce, 0x48, 0x31, 0x9f, 0xc6, 0x37, 0x8a,
+ 0xc5, 0x42, 0x54, 0x46, 0x38, 0xf2, 0x42, 0x6c, 0xfa, 0xfa, 0x5f, 0xe6,
+ 0x32, 0x4b, 0xda, 0xab, 0xb9, 0xd6, 0x96, 0x4a, 0x2c, 0xbb, 0x7d, 0x7b,
+ 0x7b, 0x4a, 0xca, 0xaf, 0x9c, 0xad, 0x4d, 0xfe, 0xde, 0x5c, 0x5e, 0x92,
+ 0x73, 0x73, 0xed, 0x8d, 0x57, 0x49, 0x52, 0x17, 0xeb, 0x96, 0x2e, 0x27,
+ 0x15, 0x54, 0x4f, 0xf8, 0x7a, 0xc2, 0x37, 0xf3, 0xc9, 0x70, 0xd2, 0x88,
+ 0x10, 0x1b, 0xc5, 0x6d, 0x52, 0x9a, 0xc5, 0x8b, 0x56, 0x7e, 0xc1, 0x5a,
+ 0xd8, 0xea, 0xc9, 0x69, 0x0e, 0x70, 0xda, 0xed, 0x0e, 0xd4, 0x5c, 0xe1,
+ 0xcd, 0x68, 0xbe, 0x26, 0xb9, 0xb3, 0xfc, 0x00, 0x4d, 0xb2, 0xbf, 0x1a,
+ 0x9e, 0x03, 0x82, 0xac, 0x79, 0x3b, 0x51, 0xc6, 0xf7, 0xce, 0xdc, 0xf2,
+ 0x1e, 0x62, 0xb1, 0x8b, 0x1f, 0xb8, 0x45, 0xdf, 0xe4, 0xb1, 0xc6, 0x95,
+ 0x07, 0x0e, 0x27, 0xe1, 0xd1, 0x30, 0x60, 0xec, 0x90, 0xd2, 0x0f, 0xac,
+ 0xd1, 0xd4, 0x57, 0xc4, 0x1b, 0xe6, 0xdf, 0xa3, 0x2a, 0x55, 0x20, 0xc3,
+ 0x35, 0x1a, 0x23, 0x27, 0x90, 0x77, 0xc9, 0xb1, 0x75, 0xfa, 0xf1, 0x03,
+ 0x1a, 0x5a, 0xf8, 0x6b, 0x6b, 0xc0, 0x10, 0x69, 0x8d, 0x5e, 0x9e, 0x40,
+ 0x96, 0xf8, 0x46, 0xc2, 0x38, 0x07, 0xcf, 0x00, 0x26, 0x7e, 0x7f, 0x4e,
+ 0x72, 0x66, 0xca, 0x39, 0xf9, 0x73, 0x07, 0x46, 0xd9, 0xd5, 0x87, 0x23,
+ 0x6d, 0xa2, 0xd8, 0xe4, 0x05, 0x5c, 0x80, 0x7a, 0xe6, 0x5a, 0x0a, 0x4c,
+ 0x58, 0x45, 0xf5, 0x4d, 0x4c, 0xcf, 0x0b, 0x59, 0xa2, 0x07, 0x63, 0xb9,
+ 0x99, 0xa4, 0xec, 0x67, 0xb1, 0x96, 0x7f, 0x6c, 0xd7, 0xe0, 0x2b, 0x30,
+ 0x1e, 0x57, 0x75, 0x92, 0x1f, 0x47, 0x41, 0x5c, 0xdd, 0xf2, 0xe2, 0xa2,
+ 0xc5, 0x43, 0x2e, 0xd5, 0x2c, 0x9a, 0x15, 0x7a, 0x98, 0xa6, 0x7e, 0x9b,
+ 0x5f, 0x9f, 0xe7, 0xc7, 0xf5, 0x91, 0xc3, 0x39, 0xd0, 0xa6, 0xd6, 0x58,
+ 0x1a, 0x08, 0xa3, 0x2f, 0xce, 0x7e, 0x7c, 0xe5, 0x34, 0x9b, 0x8f, 0xd6,
+ 0x0d, 0x0e, 0xbc, 0x3c, 0x99, 0xad, 0x77, 0x87, 0x60, 0x23, 0x99, 0xcf,
+ 0xd5, 0x2c, 0x82, 0x80, 0xf2, 0x4e, 0x8f, 0x1a, 0x2e, 0x5e, 0x10, 0x9f,
+ 0xcb, 0x7b, 0x21, 0xfe, 0x17, 0x03, 0x99, 0x20, 0x6e, 0x2a, 0x3d, 0x97,
+ 0x96, 0x4c, 0x52, 0x58, 0x07, 0xa1, 0xa1, 0xdd, 0xfc, 0xa1, 0x52, 0x94,
+ 0x3a, 0xa5, 0xbd, 0xbb, 0xb5, 0x08, 0x13, 0xff, 0xcb, 0xfc, 0x79, 0xa8,
+ 0xdb, 0xde, 0xb5, 0x11, 0x4d, 0x5f, 0xb0, 0x2e, 0x2e, 0x0c, 0xa4, 0x95,
+ 0x40, 0x44, 0xe4, 0x8a, 0xe8, 0x94, 0xc2, 0x32, 0x84, 0x19, 0xfb, 0x46,
+ 0xfe, 0xd3, 0xdc, 0xfb, 0xb4, 0x7e, 0x27, 0xec, 0x17, 0x6c, 0xa5, 0xa6,
+ 0xb6, 0x6e, 0x89, 0x15, 0x1b, 0xa4, 0xda, 0xa3, 0xac, 0x25, 0x7e, 0x9e,
+ 0xf8, 0x97, 0x07, 0x41, 0x21, 0x61, 0x55, 0x40, 0xe0, 0xd9, 0x20, 0x1f,
+ 0x26, 0xad, 0xe9, 0x7f, 0x0f, 0xc7, 0x89, 0x0c, 0x6e, 0x13, 0xca, 0xef,
+ 0x5e, 0x7d, 0x71, 0x12, 0x73, 0xe5, 0xf3, 0xdf, 0xd8, 0xcf, 0xac, 0xda,
+ 0xbf, 0x27, 0xa3, 0x53, 0xf8, 0x8a, 0x5f, 0xbb, 0x44, 0x04, 0x34, 0x7d,
+ 0x42, 0xe2, 0x64, 0x88, 0x4a, 0xf4, 0x67, 0x24, 0x7e, 0xc9, 0x06, 0x63,
+ 0x54, 0xc6, 0x0d, 0x46, 0x86, 0xf2, 0x41, 0x50, 0xee, 0xf9, 0xd2, 0x2c,
+ 0x87, 0xd1, 0x4e, 0x5f, 0x93, 0x69, 0x47, 0x9e, 0x02, 0x98, 0xcc, 0x34,
+ 0xc9, 0x46, 0x84, 0x02, 0x31, 0x12, 0x6a, 0x47, 0x3d, 0x9a, 0x70, 0x87,
+ 0xbb, 0xc4, 0xc0, 0x48, 0x90, 0x88, 0x0a, 0x61, 0x3c, 0x6a, 0x68, 0x63,
+ 0x0c, 0x5c, 0x2e, 0x80, 0x19, 0x54, 0x0a, 0xc5, 0x2b, 0x61, 0x7d, 0x55,
+ 0x92, 0x2d, 0x83, 0x33, 0x1c, 0x7a, 0x0c, 0xfd, 0x82, 0x40, 0x4c, 0x53,
+ 0xb0, 0x64, 0xd2, 0x02, 0x96, 0x35, 0xd8, 0x3c, 0x08, 0x6d, 0x4b, 0x3d,
+ 0x15, 0x55, 0xf8, 0x2a, 0xa7, 0xca, 0x89, 0x68, 0xb0, 0xc0, 0xf6, 0x84,
+ 0xc4, 0xf9, 0x86, 0xb4, 0x9e, 0x61, 0x23, 0xee, 0x87, 0xce, 0x15, 0x3e,
+ 0xa1, 0xf6, 0x86, 0xeb, 0x0c, 0x90, 0xca, 0x7a, 0x4e, 0xc5, 0xba, 0x8c,
+ 0x0d, 0x05, 0x41, 0x1a, 0xf7, 0x84, 0x3c, 0x64, 0x1e, 0x1f, 0x55, 0x83,
+ 0x74, 0x95, 0x34, 0x64, 0xb2, 0x03, 0xd6, 0xcf, 0x0f, 0xe6, 0xd2, 0xe5,
+ 0x56, 0x8f, 0x8a, 0x87, 0xe7, 0x24, 0x79, 0xb2, 0xbb, 0x91, 0x24, 0xf6,
+ 0x3f, 0x6a, 0x2e, 0x38, 0x1b, 0xd9, 0xa8, 0x06, 0x4e, 0x1f, 0x85, 0x76,
+ 0x7d, 0x0e, 0x90, 0xa2, 0x50, 0x4d, 0x01, 0xdc, 0x04, 0xae, 0x7e, 0x4d,
+ 0xfe, 0xe9, 0x43, 0x79, 0x84, 0x8b, 0xf1, 0xd6, 0x7a, 0x97, 0xa2, 0xc6,
+ 0x08, 0xb3, 0xbe, 0xeb, 0xd5, 0xe6, 0x31, 0x1b, 0x51, 0xdb, 0x6e, 0xf9,
+ 0xd9, 0xe0, 0x99, 0xcb, 0x8a, 0x35, 0xd3, 0x53, 0x48, 0xbb, 0x80, 0xe3,
+ 0xde, 0xdd, 0xa4, 0xad, 0x5e, 0x97, 0x27, 0xb7, 0xd6, 0xff, 0x8d, 0xd8,
+ 0x50, 0x62, 0xb6, 0xe3, 0xd3, 0xc3, 0xf6, 0x24, 0xff, 0x8a, 0x2a, 0x3a,
+ 0x2d, 0xd4, 0x8e, 0x22, 0x27, 0xae, 0x00, 0x42, 0xce, 0x28, 0x2e, 0x07,
+ 0x47, 0x29, 0x14, 0x46, 0x38, 0xd2, 0x73, 0x0d, 0xad, 0xb9, 0xca, 0x1d,
+ 0x68, 0x13, 0x45, 0xd3, 0xc1, 0xcf, 0xe7, 0x2b, 0x77, 0x6b, 0x01, 0xeb,
+ 0xe6, 0x83, 0x28, 0x31, 0x47, 0x33, 0x74, 0xb0, 0xc1, 0x5d, 0x3c, 0xe4,
+ 0xb4, 0x05, 0xe8, 0xb0, 0x5b, 0xaa, 0xeb, 0x74, 0xc5, 0x98, 0xd8, 0xf3,
+ 0x1c, 0x83, 0x9b, 0x4b, 0xa1, 0x14, 0xfe, 0x66, 0x1e, 0x22, 0x7d, 0x82,
+ 0x43, 0x6c, 0xa8, 0x2b, 0x99, 0x97, 0xdd, 0xbc, 0x67, 0x2d, 0x26, 0x2b,
+ 0x98, 0xe5, 0xf1, 0x1f, 0x5a, 0x15, 0xd5, 0x1e, 0x13, 0x21, 0x4e, 0x2c,
+ 0x75, 0x78, 0x6d, 0x23, 0x3b, 0x66, 0xff, 0x95, 0x6b, 0xdf, 0x93, 0x36,
+ 0x9e, 0xab, 0x59, 0xc2, 0x18, 0x5e, 0x0e, 0x42, 0xa4, 0x13, 0x3c, 0x4f,
+ 0x91, 0x3f, 0x36, 0xd4, 0xa4, 0xa8, 0x40, 0x4b, 0xf9, 0xc9, 0xdb, 0xce,
+ 0x55, 0x17, 0xc5, 0x72, 0x49, 0xd5, 0x29, 0xb0, 0x7b, 0x15, 0x45, 0x9c,
+ 0x57, 0xf3, 0x38, 0x22, 0x0e, 0x92, 0x10, 0x69, 0xaf, 0xb5, 0x72, 0x22,
+ 0x05, 0x7d, 0xb6, 0xad, 0x6e, 0x85, 0x83, 0xca, 0x03, 0x18, 0x08, 0xab,
+ 0x13, 0xf3, 0xa1, 0x33, 0x6e, 0x7f, 0xae, 0xa8, 0x7f, 0x7e, 0x0e, 0x60,
+ 0xb6, 0x77, 0xea, 0xe1, 0x36, 0xe5, 0x47, 0xe7, 0x26, 0x19, 0xa1, 0x22,
+ 0x43, 0x23, 0x75, 0xf5, 0x97, 0xd1, 0x90, 0xe3, 0x25, 0xd5, 0x79, 0x65,
+ 0xb4, 0xf7, 0xa1, 0xd7, 0x90, 0xd0, 0x37, 0x02, 0xbc, 0xb9, 0xc4, 0x56,
+ 0x14, 0x2a, 0xf7, 0x10, 0xb8, 0x60, 0x8a, 0x1b, 0x03, 0xdc, 0x62, 0x57,
+ 0xb8, 0xa5, 0x3a, 0x4f, 0xfc, 0x7d, 0xbe, 0x48, 0x97, 0xe1, 0x6f, 0xb0,
+ 0xe6, 0x79, 0x7a, 0x6c, 0x08, 0x3a, 0x2a, 0xc6, 0x3b, 0x04, 0xbc, 0x3b,
+ 0x3b, 0xf6, 0x96, 0x42, 0xf8, 0x8f, 0xc5, 0x8c, 0xc2, 0x1f, 0x0a, 0x23,
+ 0xd0, 0x06, 0x48, 0x34, 0x81, 0x4b, 0x46, 0x16, 0x2c, 0x95, 0xb0, 0x18,
+ 0x19, 0x01, 0x54, 0x38, 0xea, 0x47, 0x1e, 0xbd, 0x9e, 0xf7, 0x5a, 0x9c,
+ 0x4b, 0xc0, 0x3f, 0x05, 0xa2, 0x34, 0xff, 0xf7, 0x40, 0xc3, 0xe9, 0xb8,
+ 0x2b, 0x99, 0xc1, 0xdf, 0x51, 0x2d, 0x5c, 0xe9, 0x78, 0x81, 0x51, 0x2f,
+ 0x4d, 0xfb, 0x69, 0x5f, 0x9c, 0x2a, 0x57, 0xe3, 0x7d, 0xb6, 0xa5, 0xeb,
+ 0x97, 0x0e, 0x23, 0x98, 0x9d, 0x78, 0xdb, 0x7b, 0x48, 0x60, 0xd7, 0xf3,
+ 0xa0, 0x67, 0x88, 0xaf, 0xa7, 0xf1, 0xf8, 0x57, 0xc8, 0x2a, 0x50, 0xc4,
+ 0x77, 0x80, 0xba, 0x37, 0xf6, 0x00, 0x16, 0xea, 0xbb, 0x4d, 0xef, 0x94,
+ 0xa2, 0x4c, 0x5c, 0x5b, 0x58, 0xc3, 0x74, 0x54, 0xff, 0x96, 0x1a, 0x2f,
+ 0xb4, 0x07, 0x3f, 0x1e, 0x27, 0x02, 0x3f, 0x3e, 0xf5, 0x0d, 0xe5, 0x39,
+ 0x87, 0xe8, 0x73, 0x61, 0xe7, 0x49, 0x98, 0x45, 0xad, 0xc6, 0x23, 0xa8,
+ 0xfb, 0x69, 0x0b, 0x81, 0x3d, 0x2f, 0x7d, 0x2d, 0xc4, 0xca, 0x36, 0x69,
+ 0xe4, 0x7a, 0x76, 0x23, 0x60, 0xd0, 0x9d, 0x3e, 0x49, 0x4e, 0x1e, 0x33,
+ 0x84, 0x08, 0xa0, 0xd3, 0x7a, 0x8b, 0x33, 0x82, 0xb3, 0x4a, 0x9b, 0x93,
+ 0xe9, 0x02, 0xa8, 0xb6, 0x25, 0x6a, 0x77, 0x1a, 0x53, 0x4d, 0x21, 0xe0,
+ 0x3a, 0xad, 0xf5, 0x7f, 0x2c, 0x7a, 0xcf, 0xef, 0x84, 0x7c, 0xd0, 0xef,
+ 0x5d, 0xb6, 0x3a, 0xea, 0x44, 0xe4, 0xe1, 0x19, 0x5a, 0xfe, 0xe8, 0xae,
+ 0xfb, 0x16, 0x0c, 0x4b, 0x8d, 0xb0, 0x57, 0xa7, 0xf1, 0x3c, 0x19, 0x67,
+ 0x37, 0x4a, 0x5b, 0x26, 0x8d, 0x68, 0x61, 0xe2, 0x95, 0x1b, 0xc7, 0x80,
+ 0xfb, 0x2e, 0x19, 0x44, 0xfb, 0x1f, 0x18, 0xa1, 0x6a, 0xc6, 0xc8, 0xc0,
+ 0xec, 0xa5, 0x02, 0x09, 0x8b, 0x3f, 0x9f, 0x9c, 0xe9, 0xfe, 0x23, 0x43,
+ 0x1f, 0x4c, 0xea, 0x0c, 0x21, 0x53, 0x02, 0x85, 0x08, 0x3f, 0x53, 0x82,
+ 0x94, 0xe1, 0xa5, 0x41, 0x25, 0x44, 0x6c, 0x5a, 0xe0, 0x8b, 0xe0, 0x83,
+ 0xff, 0xfa, 0x85, 0x4f, 0xac, 0xfc, 0xd8, 0x25, 0x2c, 0xc8, 0x2e, 0x88,
+ 0xc4, 0x58, 0xf3, 0x6d, 0xb3, 0xef, 0xe8, 0x53, 0x0e, 0xf8, 0x86, 0xd4,
+ 0xa4, 0xf5, 0x9c, 0x56, 0xbc, 0xb1, 0x69, 0x82, 0x50, 0x9f, 0x38, 0x2c,
+ 0x68, 0xd4, 0x94, 0xc4, 0x8c, 0x19, 0x41, 0xd2, 0x10, 0x2f, 0xd5, 0x63,
+ 0x66, 0x80, 0x5e, 0x7d, 0x14, 0xb5, 0x4a, 0xbc, 0x32, 0x61, 0x21, 0x83,
+ 0x82, 0x2c, 0x0f, 0xe2, 0x48, 0x10, 0xb8, 0x23, 0xd4, 0xf6, 0xbc, 0xa9,
+ 0xff, 0x7d, 0xaa, 0xcd, 0xc1, 0x66, 0x74, 0xa4, 0x44, 0x2b, 0xcd, 0x2d,
+ 0x12, 0x4c, 0x1e, 0x61, 0xf3, 0x07, 0x83, 0xeb, 0xe2, 0x50, 0x94, 0xf0,
+ 0xa8, 0x6e, 0xff, 0xe9, 0xf6, 0x0c, 0xfd, 0x54, 0x25, 0x14, 0xaa, 0xaf,
+ 0x1f, 0x3b, 0x29, 0x9d, 0xb5, 0xb3, 0x66, 0x92, 0xee, 0x99, 0xda, 0xd6,
+ 0xdf, 0xc7, 0x1a, 0x88, 0xfd, 0xa0, 0xe4, 0x86, 0x92, 0x10, 0xe9, 0x83,
+ 0xaf, 0xd4, 0xeb, 0x6a, 0xf6, 0x4d, 0x39, 0x47, 0x46, 0x90, 0xa6, 0xa1,
+ 0x92, 0x0b, 0x0b, 0x3b, 0xe6, 0xaa, 0x85, 0xdd, 0x86, 0x33, 0xb2, 0x98,
+ 0xf3, 0x67, 0x48, 0x34, 0xaa, 0x0b, 0x81, 0xd4, 0xc8, 0x31, 0x15, 0x8f,
+ 0x0e, 0x5b, 0xc0, 0x9d, 0xdf, 0xc7, 0x9e, 0x11, 0x54, 0x47, 0xd2, 0x2a,
+ 0x13, 0x21, 0x55, 0x88, 0x61, 0xed, 0xd5, 0x4f, 0x9a, 0x66, 0xb2, 0x86,
+ 0xa1, 0xe4, 0x96, 0x60, 0xb2, 0x65, 0xe1, 0x9e, 0x53, 0x96, 0x2f, 0xd0,
+ 0x91, 0x42, 0xb1, 0x2d, 0xcb, 0xf0, 0x55, 0xb5, 0xb5, 0xb0, 0x83, 0x30,
+ 0x8a, 0x22, 0xb4, 0x65, 0x54, 0xae, 0xe1, 0xde, 0x91, 0xdc, 0x48, 0x60,
+ 0x81, 0xe0, 0x24, 0x46, 0xdf, 0x19, 0xaa, 0x18, 0xde, 0xc9, 0x22, 0x31,
+ 0x50, 0x2e, 0xd1, 0x8e, 0x4e, 0x03, 0xc2, 0x7f, 0x19, 0x9f, 0x21, 0x52,
+ 0xba, 0xdd, 0x2a, 0xac, 0xbd, 0x92, 0x59, 0x45, 0x31, 0xde, 0x53, 0x1a,
+ 0x40, 0x79, 0x76, 0x83, 0xfd, 0x5d, 0x8f, 0x4c, 0xd6, 0xfc, 0x92, 0x6a,
+ 0x28, 0xb9, 0x20, 0x32, 0xae, 0xa6, 0xc6, 0x0e, 0x14, 0xd8, 0xea, 0x01,
+ 0x1c, 0xf4, 0x17, 0x23, 0xc4, 0x37, 0x23, 0x33, 0x6c, 0xc1, 0x7e, 0x80,
+ 0xe3, 0x20, 0xde, 0x1b, 0x88, 0xb1, 0x14, 0xa9, 0xe3, 0x7c, 0xae, 0xb1,
+ 0xf6, 0xc4, 0xeb, 0xa2, 0xf1, 0x59, 0xcb, 0xb0, 0xf0, 0x26, 0x68, 0xd5,
+ 0x6d, 0x9b, 0x98, 0xa4, 0x8f, 0xb0, 0xc4, 0x2e, 0xf7, 0xca, 0x60, 0x25,
+ 0xcc, 0xd5, 0x9c, 0xfa, 0x98, 0xca, 0x6d, 0x13, 0x21, 0x3b, 0x0e, 0x36,
+ 0xd7, 0x68, 0xb4, 0xd3, 0x05, 0x3f, 0xcd, 0x78, 0x63, 0x41, 0x22, 0x9e,
+ 0xfd, 0x09, 0xfc, 0xcc, 0xce, 0x6e, 0xef, 0x9e, 0x1f, 0xb9, 0x6a, 0x31,
+ 0xf8, 0xe0, 0xdc, 0xf5, 0x51, 0xae, 0x9f, 0x81, 0xfa, 0xe4, 0x7a, 0x51,
+ 0x26, 0xce, 0x87, 0xa3, 0x96, 0xca, 0x10, 0x43, 0xfa, 0x4a, 0xbf, 0x95,
+ 0xdd, 0x15, 0xb2, 0x9c, 0x9a, 0xd4, 0x59, 0xe1, 0xd8, 0x69, 0xb5, 0xc1,
+ 0x8c, 0x42, 0x46, 0x06, 0xc2, 0x3e, 0x31, 0xf3, 0x29, 0xe0, 0x5d, 0x04,
+ 0x23, 0x25, 0x42, 0x89, 0xda, 0xd1, 0x49, 0x8a, 0xed, 0x63, 0xa1, 0x0c,
+ 0x51, 0xb3, 0xe8, 0x9b, 0x55, 0x3e, 0xc3, 0x40, 0x20, 0x96, 0x2d, 0xbf,
+ 0x1e, 0x5c, 0x56, 0x37, 0xda, 0x15, 0x8a, 0x6e, 0x20, 0xac, 0x10, 0x1f,
+ 0x8c, 0x87, 0xfa, 0x34, 0xc3, 0x57, 0xfb, 0xec, 0xfc, 0x6b, 0x62, 0xf9,
+ 0x90, 0x58, 0x39, 0xe6, 0x8d, 0xd4, 0xd0, 0x72, 0xdd, 0x5a, 0x00, 0xed,
+ 0xf1, 0x9f, 0x61, 0x24, 0xb4, 0xd2, 0x60, 0x41, 0xfc, 0x0f, 0xff, 0xf9,
+ 0xfe, 0xc6, 0x29, 0xbe, 0x05, 0x0a, 0xcb, 0x4f, 0xea, 0x0a, 0x2c, 0x42,
+ 0x0c, 0x42, 0xc4, 0x64, 0x49, 0xba, 0xd5, 0x50, 0x11, 0x3d, 0xc4, 0x6e,
+ 0x02, 0xfe, 0xc5, 0x0e, 0x47, 0x1e, 0x7f, 0x74, 0xef, 0x76, 0x20, 0x8d,
+ 0xd6, 0xe5, 0x71, 0xe4, 0xd7, 0x60, 0x89, 0xc2, 0xf6, 0xa6, 0xa0, 0xfc,
+ 0x0a, 0x29, 0x95, 0xf8, 0x66, 0x31, 0x7d, 0x0e, 0xde, 0x29, 0x74, 0x29,
+ 0xe3, 0x9c, 0x0a, 0xb3, 0x18, 0x80, 0x8b, 0x64, 0xc4, 0x1f, 0x47, 0x50,
+ 0x88, 0x26, 0x13, 0xaa, 0x2d, 0x59, 0x61, 0x17, 0x0e, 0x6f, 0xd9, 0x6b,
+ 0x69, 0xb9, 0x60, 0x50, 0xc4, 0x74, 0xa6, 0x17, 0x9b, 0x3d, 0x19, 0x0d,
+ 0xe9, 0x24, 0x20, 0xe9, 0xb5, 0x03, 0x13, 0xb0, 0x41, 0xbb, 0x97, 0xcc,
+ 0xe3, 0x29, 0xc1, 0xaf, 0x40, 0xa9, 0xed, 0x2a, 0x4c, 0x09, 0x40, 0xa3,
+ 0x20, 0xe7, 0x59, 0x81, 0x00, 0x21, 0x00, 0x84, 0x00, 0x80, 0x49, 0x7a,
+ 0x00, 0x85, 0x43, 0x00, 0xa4, 0x80, 0x7f, 0x05, 0xf2, 0xe1, 0x8f, 0x79,
+ 0xb0, 0x7f, 0x7b, 0xf2, 0x43, 0xcb, 0x2b, 0x99, 0x9f, 0x1a, 0x7d, 0x77,
+ 0x95, 0xf7, 0xae, 0xde, 0x35, 0x2d, 0x7f, 0x41, 0x7e, 0x67, 0xf2, 0x1f,
+ 0xd5, 0x3b, 0xbb, 0x7e, 0xd4, 0xf7, 0xea, 0x3d, 0x2f, 0xf2, 0x1f, 0xce,
+ 0x6e, 0xe8, 0x61, 0xff, 0x51, 0xe8, 0xbf, 0xc6, 0xb1, 0x21, 0xd2, 0xfc,
+ 0x5e, 0x5f, 0x4f, 0xc7, 0xf0, 0xff, 0xe2, 0x7f, 0x17, 0xfb, 0x9f, 0xf5,
+ 0x5f, 0xa3, 0xf1, 0xff, 0xc2, 0x7e, 0x2f, 0xf6, 0xbf, 0xe6, 0xbf, 0x26,
+ 0x67, 0xfc, 0x5f, 0x8b, 0xf3, 0xfe, 0x4f, 0xc9, 0x99, 0xff, 0xe0, 0xbf,
+ 0xca, 0xfd, 0x1f, 0xf2, 0xbf, 0xf0, 0xd9, 0x9f, 0xec, 0x8f, 0xd9, 0xf0,
+ 0xb3, 0x47, 0xaa, 0xb1, 0x44, 0xd3, 0x72, 0xf7, 0xaa, 0xf6, 0xbf, 0xb6,
+ 0x7f, 0xd7, 0xff, 0x47, 0xfc, 0x3f, 0xfa, 0x0f, 0xb8, 0x4f, 0x87, 0xf8,
+ 0xbd, 0x27, 0xa7, 0xf3, 0x5f, 0xfa, 0xbf, 0x17, 0xda, 0x1f, 0xf0, 0x9f,
+ 0x0f, 0xf3, 0x6d, 0x83, 0xf6, 0xaf, 0xdb, 0x7f, 0xf0, 0x9e, 0x93, 0x6a,
+ 0xff, 0xc8, 0xff, 0x5f, 0xe2, 0x7f, 0xc9, 0xfc, 0x3f, 0xcd, 0xae, 0x47,
+ 0xe2, 0xfe, 0xcf, 0xf7, 0xbf, 0xde, 0xf4, 0x44, 0x45, 0x3d, 0xef, 0x63,
+ 0xf6, 0xfd, 0x23, 0xdb, 0xfa, 0xee, 0x9d, 0xfb, 0x97, 0xb4, 0xf6, 0x8b,
+ 0x23, 0xf8, 0xdf, 0xdc, 0xba, 0xd7, 0xae, 0x62, 0x73, 0xf5, 0xae, 0xa9,
+ 0xeb, 0x98, 0xa2, 0x3d, 0x47, 0x98, 0x67, 0xae, 0x66, 0xfe, 0xa3, 0xf6,
+ 0xcf, 0x63, 0xfd, 0x89, 0xfc, 0xe3, 0x03, 0xb7, 0xaf, 0x6c, 0x8a, 0x59,
+ 0x8d, 0xfb, 0xa3, 0xf9, 0xc6, 0x18, 0x3f, 0x9c, 0xf2, 0xbb, 0xf5, 0xdb,
+ 0x98, 0x53, 0x7c, 0xf8, 0xc6, 0x1c, 0x2c, 0xef, 0x6f, 0xe5, 0xfc, 0xbf,
+ 0xdb, 0xfe, 0x4f, 0xfd, 0x0f, 0xf7, 0xfe, 0x5f, 0xcf, 0xf2, 0x7b, 0xcb,
+ 0xf8, 0x3f, 0x17, 0xd2, 0x7f, 0x7f, 0xe6, 0xd4, 0x5a, 0xc1, 0xbb, 0x2e,
+ 0xb4, 0xde, 0xca, 0xff, 0x8c, 0x79, 0x6b, 0x7c, 0x6f, 0x63, 0xff, 0x5a,
+ 0xff, 0x09, 0xf5, 0x7f, 0xd9, 0x5f, 0xd9, 0x5a, 0x53, 0xf9, 0xcf, 0x75,
+ 0xd8, 0x39, 0x54, 0xfd, 0x53, 0xea, 0xf7, 0xd7, 0x54, 0xac, 0x17, 0xb3,
+ 0x7a, 0xa1, 0xff, 0xf9, 0x3c, 0xc0, 0x63, 0xff, 0x92, 0x79, 0x84, 0x9f,
+ 0xe4, 0x3e, 0x5b, 0x6a, 0xef, 0xc9, 0xfc, 0xf2, 0x71, 0x60, 0x00, 0x00,
+ 0x00, 0x42, 0x6e, 0x7a, 0xe7, 0x9c, 0xa9, 0x13, 0xb3, 0xbc, 0xf2, 0x5b,
+ 0xb9, 0xfb, 0x8a, 0x48, 0x83, 0x58, 0xe7, 0x1f, 0xe0, 0xb0, 0x46, 0xe8,
+ 0x6c, 0xd0, 0x67, 0x48, 0xa0, 0xb7, 0x87, 0x8e, 0xa2, 0xae, 0x44, 0x15,
+ 0x93, 0xab, 0x6b, 0x61, 0x52, 0xe0, 0x7f, 0x98, 0xb7, 0x29, 0xa5, 0x17,
+ 0x77, 0x48, 0x57, 0xf1, 0x2c, 0x73, 0x6f, 0xd3, 0x9e, 0x0e, 0xaa, 0x50,
+ 0x47, 0x2c, 0x13, 0x1a, 0xa4, 0x59, 0x2d, 0xa9, 0xe3, 0xff, 0xf6, 0xbf,
+ 0x79, 0x09, 0xa1, 0xa8, 0xdd, 0x57, 0x55, 0x26, 0x4c, 0x1d, 0xa7, 0x9c,
+ 0xd6, 0xfb, 0xef, 0xc9, 0x5b, 0x80, 0xb6, 0xe4, 0x49, 0xb1, 0xe6, 0x71,
+ 0x5e, 0xe0, 0x12, 0x65, 0x7c, 0x11, 0x2d, 0xf8, 0xb2, 0xa8, 0x03, 0xd3,
+ 0x66, 0xa4, 0x94, 0x63, 0x9d, 0x12, 0x75, 0x7e, 0xd3, 0xef, 0x2a, 0xb1,
+ 0x4f, 0xc7, 0x56, 0x35, 0x9e, 0xae, 0x91, 0x5d, 0xa7, 0x99, 0x66, 0x21,
+ 0x1d, 0xdb, 0xf9, 0x9e, 0xf3, 0xfd, 0x70, 0x3b, 0x2f, 0xe5, 0xa8, 0xf1,
+ 0xc3, 0x0c, 0x44, 0xbc, 0x44, 0xef, 0x2e, 0x4d, 0x9b, 0x0e, 0xaf, 0xb2,
+ 0x7b, 0xea, 0xb2, 0x28, 0xc2, 0x92, 0xdd, 0xce, 0x7b, 0x2f, 0x90, 0xdb,
+ 0xf5, 0xa5, 0xdd, 0x00, 0x7e, 0x53, 0x94, 0x2b, 0x7d, 0xb7, 0x61, 0x71,
+ 0x17, 0x54, 0x05, 0x75, 0x6d, 0x99, 0x47, 0x43, 0xa3, 0x63, 0x25, 0xe4,
+ 0x35, 0x27, 0x1d, 0xfc, 0xb3, 0x46, 0xd9, 0xf4, 0x72, 0x18, 0x83, 0x24,
+ 0x61, 0xc1, 0x7f, 0xcf, 0x28, 0x2a, 0x13, 0x75, 0x73, 0x54, 0xb2, 0xfb,
+ 0xe5, 0xa2, 0xb0, 0xad, 0xe2, 0x83, 0x09, 0xc5, 0xc4, 0x98, 0xd6, 0xe6,
+ 0xb4, 0xc6, 0xc4, 0xe5, 0xdf, 0x57, 0x71, 0x41, 0xb7, 0xaa, 0xfc, 0xd2,
+ 0x73, 0xe1, 0xb8, 0xf6, 0x12, 0x81, 0x81, 0x69, 0x14, 0x55, 0x2a, 0xf5,
+ 0xb6, 0xc4, 0x56, 0x10, 0x9b, 0x35, 0x10, 0xf7, 0x8f, 0xfa, 0xd5, 0xd3,
+ 0x51, 0x5e, 0x10, 0x32, 0xee, 0x32, 0x92, 0x40, 0xc7, 0x5d, 0x2b, 0xc9,
+ 0xea, 0x14, 0x30, 0xec, 0xae, 0x1c, 0x0a, 0x79, 0xfb, 0x1b, 0xca, 0x91,
+ 0xab, 0x58, 0x83, 0x54, 0x7f, 0x64, 0x24, 0x6d, 0xae, 0x63, 0x88, 0x9a,
+ 0x6d, 0x09, 0x63, 0xb6, 0x90, 0xb4, 0x99, 0xa0, 0xa4, 0x72, 0x0b, 0xd1,
+ 0x81, 0xa3, 0x07, 0x3e, 0x33, 0xde, 0xf6, 0x27, 0x36, 0xee, 0x40, 0x91,
+ 0x82, 0x87, 0x5c, 0x35, 0x1d, 0x3e, 0xb9, 0xbc, 0xcd, 0x03, 0x2d, 0x32,
+ 0x10, 0xba, 0xbf, 0x10, 0x6d, 0x42, 0x68, 0x69, 0x3a, 0x6a, 0x54, 0xc7,
+ 0xcb, 0xdc, 0x5d, 0xe3, 0x29, 0xce, 0x1d, 0x30, 0xf2, 0x87, 0x54, 0xe5,
+ 0xc7, 0x60, 0x13, 0x76, 0x89, 0xb5, 0xc1, 0x00, 0x1c, 0x59, 0x93, 0x5e,
+ 0x3b, 0x9d, 0xe2, 0x9e, 0xda, 0x39, 0xf1, 0x20, 0x19, 0x9d, 0xd6, 0x1a,
+ 0xeb, 0xf6, 0x1d, 0xf9, 0x9c, 0xf7, 0xe9, 0x02, 0xa0, 0x6a, 0xf2, 0x79,
+ 0xee, 0xe1, 0xc9, 0xdc, 0x1a, 0x7a, 0x9b, 0xbc, 0xec, 0xf1, 0x2e, 0xb4,
+ 0x67, 0xe9, 0xa3, 0xf3, 0x61, 0x51, 0xce, 0xd4, 0xf9, 0xaf, 0x7d, 0x33,
+ 0xb6, 0xd7, 0xc1, 0x8f, 0xbc, 0xba, 0x91, 0xfa, 0xa6, 0xa1, 0x31, 0x1c,
+ 0x63, 0x71, 0x69, 0x81, 0x16, 0xcb, 0x8a, 0x3e, 0x18, 0x04, 0xf9, 0x7d,
+ 0x13, 0xcf, 0x84, 0x84, 0xfe, 0x8f, 0xe4, 0x8e, 0x60, 0xad, 0x6e, 0xfe,
+ 0xf7, 0xc5, 0x60, 0xdb, 0x08, 0xc2, 0xc4, 0xd5, 0xfe, 0x7c, 0x40, 0xed,
+ 0x41, 0xc8, 0x61, 0x30, 0x53, 0xa6, 0xb9, 0x26, 0x40, 0x67, 0x58, 0x64,
+ 0xbe, 0x3c, 0xd4, 0x45, 0x72, 0x71, 0x2b, 0x20, 0x03, 0x3e, 0xb6, 0x48,
+ 0x26, 0x23, 0x4d, 0xb7, 0xab, 0xed, 0x79, 0xc8, 0xbb, 0x01, 0x83, 0x34,
+ 0xc5, 0x3e, 0x89, 0x8f, 0x7a, 0x49, 0x64, 0x4e, 0xf2, 0xed, 0x1f, 0x55,
+ 0xb8, 0x45, 0x50, 0xe9, 0x07, 0x72, 0xbd, 0x9b, 0xab, 0x54, 0x86, 0xfd,
+ 0xf4, 0x9e, 0x5a, 0xfb, 0x29, 0x09, 0x3d, 0x95, 0x91, 0x9c, 0x1d, 0x3e,
+ 0x9a, 0xfa, 0xad, 0x27, 0x97, 0x53, 0x78, 0x61, 0x88, 0xaf, 0x45, 0x88,
+ 0x1a, 0x59, 0x36, 0x59, 0x92, 0x17, 0x0e, 0x11, 0x7c, 0x23, 0x2e, 0xb0,
+ 0xa8, 0xd5, 0x9b, 0xb0, 0xfc, 0x2c, 0xc3, 0xf8, 0x8a, 0x15, 0x90, 0x7a,
+ 0xbd, 0xb4, 0xc4, 0x22, 0x70, 0x04, 0x01, 0xd5, 0xc7, 0x42, 0x34, 0xd0,
+ 0x2a, 0x55, 0x65, 0xcf, 0x7a, 0x8f, 0x33, 0xb9, 0x40, 0xe0, 0xb9, 0x46,
+ 0x47, 0x4e, 0x24, 0x16, 0x32, 0x54, 0x5a, 0x47, 0xa5, 0xeb, 0xbc, 0x42,
+ 0xee, 0x47, 0xd1, 0x2e, 0x0b, 0xd3, 0x5b, 0x29, 0x05, 0x7a, 0x0a, 0xeb,
+ 0x91, 0x69, 0x34, 0xec, 0xee, 0x66, 0x52, 0x37, 0xc6, 0xaf, 0x61, 0xd0,
+ 0x7b, 0xa7, 0x55, 0x62, 0x51, 0x57, 0x76, 0xd1, 0x7c, 0x4a, 0x83, 0x85,
+ 0x1d, 0xab, 0x3a, 0x8b, 0x2b, 0x33, 0x2c, 0x4b, 0x8f, 0x59, 0x20, 0xe4,
+ 0x58, 0x57, 0xdb, 0x85, 0x96, 0x51, 0x25, 0x4b, 0xb3, 0xa3, 0xcf, 0x6f,
+ 0xbc, 0x59, 0x1c, 0x99, 0xe5, 0x2f, 0xb9, 0x13, 0x20, 0x77, 0x20, 0xab,
+ 0x3e, 0x4a, 0xd8, 0x13, 0x47, 0x5f, 0xb2, 0x64, 0x17, 0xb7, 0xbd, 0x2e,
+ 0x04, 0x30, 0x01, 0x25, 0x32, 0xda, 0x94, 0x38, 0xf4, 0xfd, 0x64, 0xb7,
+ 0x57, 0x9a, 0xf8, 0xa4, 0xfd, 0x36, 0x5f, 0x5b, 0x8f, 0x32, 0x24, 0xeb,
+ 0x81, 0x12, 0xb4, 0xb3, 0xc5, 0x14, 0x7c, 0xce, 0x3a, 0x6d, 0xb2, 0x81,
+ 0x23, 0x3a, 0x78, 0x78, 0x17, 0xa1, 0x2c, 0xc3, 0xb6, 0xd2, 0x68, 0x55,
+ 0x1f, 0x48, 0x12, 0x94, 0xb5, 0x84, 0xee, 0x9e, 0x5b, 0x8c, 0xb0, 0x66,
+ 0xdf, 0x88, 0xf9, 0x8f, 0x75, 0xac, 0x93, 0xd4, 0x26, 0xa5, 0x61, 0x81,
+ 0xa3, 0x1a, 0x81, 0x9b, 0xac, 0x9d, 0x14, 0x1c, 0xa5, 0xbc, 0x13, 0xe2,
+ 0xd4, 0x7c, 0xc9, 0x5a, 0x00, 0x81, 0x66, 0xe0, 0xfc, 0x1c, 0xc7, 0x52,
+ 0x53, 0x32, 0xe9, 0x52, 0x80, 0x8b, 0x13, 0x91, 0x4e, 0x69, 0x6c, 0xda,
+ 0x66, 0xe3, 0x18, 0xe9, 0x84, 0x4e, 0x1e, 0x07, 0x02, 0x18, 0x53, 0x11,
+ 0x5d, 0x8e, 0x9a, 0xf2, 0x12, 0x35, 0xa6, 0x93, 0x94, 0xce, 0x5f, 0x8e,
+ 0x99, 0x33, 0x12, 0x26, 0xc1, 0x8a, 0x40, 0xac, 0xc5, 0x7e, 0x12, 0xde,
+ 0x6a, 0xa2, 0x01, 0x6e, 0xa0, 0x75, 0xde, 0x75, 0x14, 0x5f, 0xfa, 0xe4,
+ 0xef, 0x23, 0xbc, 0xc3, 0x63, 0xc8, 0x12, 0x07, 0x6a, 0xc2, 0x5d, 0x76,
+ 0xad, 0x99, 0x0e, 0xe1, 0xf4, 0xfc, 0xa0, 0x3f, 0x72, 0xff, 0xf1, 0x48,
+ 0x58, 0x0a, 0x7d, 0x2a, 0x8c, 0x3d, 0xab, 0x45, 0xc2, 0x81, 0xf3, 0x67,
+ 0x4d, 0x8f, 0x29, 0x6d, 0x17, 0x88, 0x1f, 0x06, 0x87, 0xba, 0x59, 0x45,
+ 0xd4, 0x6b, 0x0f, 0x62, 0x8c, 0xe4, 0xf0, 0x82, 0x61, 0xe4, 0xf9, 0xcb,
+ 0xc5, 0x89, 0x75, 0x5e, 0x71, 0x14, 0x0b, 0x93, 0xdc, 0x50, 0xdc, 0x08,
+ 0xb9, 0xdc, 0x7b, 0x32, 0xc0, 0x67, 0x00, 0x6e, 0xb9, 0xc4, 0x6e, 0x34,
+ 0xa2, 0xc2, 0x9f, 0xa1, 0x4f, 0x88, 0x77, 0x00, 0x40, 0x14, 0x9c, 0xe0,
+ 0xe6, 0x9f, 0x4b, 0x62, 0x6d, 0x8c, 0x94, 0xcb, 0x29, 0x7a, 0xe2, 0x24,
+ 0x4c, 0x0d, 0x31, 0xeb, 0x70, 0x97, 0xcf, 0x06, 0xd8, 0x4f, 0xd2, 0xed,
+ 0x42, 0xc5, 0x6c, 0x51, 0x01, 0x6d, 0x27, 0x9d, 0xd2, 0x71, 0x86, 0xcf,
+ 0x80, 0xf7, 0xba, 0xb7, 0x43, 0xb5, 0xc5, 0xdf, 0x39, 0x92, 0x7b, 0xcc,
+ 0xa0, 0x2d, 0xc8, 0x57, 0x2c, 0x3b, 0xb1, 0xb2, 0x0d, 0x6e, 0xef, 0xfd,
+ 0x77, 0x8a, 0x4b, 0xb7, 0x77, 0x96, 0x22, 0x90, 0xa9, 0x71, 0x3c, 0x2b,
+ 0x2b, 0x85, 0xb3, 0x89, 0x1a, 0xb6, 0xd7, 0x43, 0x04, 0xfa, 0xda, 0xbe,
+ 0x3f, 0x47, 0x07, 0x9b, 0xd3, 0xaf, 0xa4, 0xee, 0xd5, 0xd3, 0xc4, 0xe0,
+ 0x4a, 0x24, 0xbc, 0x78, 0xbd, 0x88, 0x55, 0xfe, 0x45, 0xba, 0x47, 0xeb,
+ 0xcc, 0x61, 0x71, 0x21, 0xd5, 0x9b, 0x4a, 0xc0, 0x1a, 0x7c, 0xf9, 0xd5,
+ 0x37, 0xf8, 0x9f, 0x79, 0x40, 0xa0, 0x10, 0xc8, 0x3e, 0x75, 0xfe, 0x99,
+ 0xef, 0xec, 0xeb, 0x53, 0x87, 0x8a, 0xf4, 0x95, 0x53, 0xb8, 0x4b, 0x01,
+ 0xa0, 0x66, 0x46, 0x9c, 0x4a, 0x95, 0x95, 0xa9, 0x44, 0x45, 0xec, 0x02,
+ 0xd2, 0xd8, 0x6b, 0x71, 0x8a, 0xb2, 0x21, 0x70, 0xe2, 0xa1, 0x25, 0xd6,
+ 0x11, 0x3a, 0x29, 0x01, 0x5b, 0x58, 0x0d, 0x96, 0x6e, 0xe8, 0xa8, 0x82,
+ 0xb5, 0x95, 0xa0, 0xab, 0xc1, 0xea, 0x7d, 0x39, 0x02, 0x32, 0x8c, 0x68,
+ 0x54, 0x10, 0x3f, 0x2d, 0x01, 0x2f, 0x93, 0x26, 0xce, 0x35, 0x0d, 0xfb,
+ 0x9a, 0x6e, 0xd4, 0xf6, 0xb8, 0x01, 0x8a, 0x79, 0x21, 0xfe, 0x0e, 0x3f,
+ 0x03, 0x66, 0x18, 0xd1, 0x16, 0x9e, 0xcc, 0xbd, 0xde, 0xcf, 0x8b, 0x7c,
+ 0x36, 0xca, 0x4d, 0x7a, 0x84, 0x4d, 0x7f, 0xdf, 0xb1, 0x8f, 0xaf, 0x7d,
+ 0x95, 0x60, 0x7b, 0xa5, 0xce, 0x18, 0x18, 0x9a, 0x47, 0xa1, 0x6e, 0x22,
+ 0x39, 0xb9, 0x51, 0x9b, 0xab, 0x92, 0xfd, 0x2c, 0x28, 0x21, 0x96, 0x56,
+ 0x06, 0x57, 0xf2, 0x7b, 0x71, 0xb8, 0x0b, 0x16, 0x23, 0xa4, 0x1b, 0x22,
+ 0x47, 0xff, 0x47, 0x03, 0x61, 0x1c, 0xbf, 0x2a, 0xa3, 0x86, 0x0a, 0x2b,
+ 0x6f, 0x60, 0x71, 0x7d, 0xb1, 0xe1, 0x76, 0x5f, 0xe0, 0x51, 0x28, 0xeb,
+ 0x08, 0x1d, 0xea, 0x6d, 0x57, 0xb8, 0x35, 0xe4, 0x29, 0x39, 0xd4, 0xb5,
+ 0xde, 0x63, 0xde, 0x45, 0xe0, 0xd1, 0x72, 0xe4, 0x69, 0x78, 0xb0, 0xa5,
+ 0x83, 0x61, 0x54, 0x23, 0xb7, 0xf8, 0x26, 0xf7, 0xc8, 0x4a, 0xfd, 0xe1,
+ 0xb6, 0x75, 0x63, 0x2e, 0x80, 0xe7, 0x9a, 0x71, 0xb8, 0x92, 0x79, 0x1d,
+ 0xac, 0xfb, 0x15, 0xfe, 0x67, 0x0a, 0xed, 0xde, 0x24, 0x79, 0x16, 0xd3,
+ 0x5c, 0x00, 0xa1, 0x7c, 0x85, 0xad, 0x0d, 0x10, 0x5d, 0xe4, 0xbf, 0x6e,
+ 0x76, 0x6e, 0x19, 0xd2, 0x46, 0xb9, 0x73, 0xab, 0x35, 0x14, 0xf6, 0xc7,
+ 0x86, 0x78, 0x0e, 0x88, 0x47, 0x6e, 0x4e, 0x75, 0xf4, 0x0f, 0x1b, 0x5b,
+ 0x6b, 0xce, 0x44, 0xd0, 0x87, 0xd0, 0x8e, 0xd5, 0x6e, 0x37, 0x08, 0xc9,
+ 0xb1, 0x37, 0xff, 0xb0, 0xa9, 0xaa, 0xf1, 0x9d, 0x51, 0xb9, 0x6d, 0x15,
+ 0x31, 0x34, 0x7a, 0x8a, 0x6d, 0xf2, 0xfa, 0x43, 0xb9, 0xed, 0xfe, 0x00,
+ 0x63, 0xb5, 0x01, 0x7f, 0xf1, 0x0b, 0x54, 0x99, 0xd7, 0xd0, 0x34, 0xbb,
+ 0x07, 0x17, 0xe1, 0xca, 0x8b, 0xda, 0x26, 0x4d, 0xd6, 0xe4, 0x98, 0xfc,
+ 0x22, 0x94, 0xf1, 0xa1, 0x1c, 0x11, 0xf4, 0x30, 0xe0, 0x0c, 0xf6, 0x7a,
+ 0x2e, 0x7f, 0x1c, 0x6c, 0xe0, 0x07, 0xb8, 0x90, 0x2d, 0x7e, 0xea, 0x35,
+ 0xd8, 0xa9, 0xc6, 0x89, 0xa0, 0x39, 0xd0, 0x66, 0x41, 0xcf, 0x6f, 0x3f,
+ 0x77, 0x88, 0x8a, 0xb5, 0x71, 0xb6, 0x68, 0x51, 0xdf, 0xa9, 0x5b, 0x96,
+ 0x28, 0x14, 0x81, 0x4e, 0x36, 0x6b, 0x16, 0xa3, 0xb7, 0x0d, 0x20, 0xad,
+ 0x22, 0x92, 0xaf, 0x4a, 0x57, 0x0f, 0xcb, 0x27, 0xc4, 0x8f, 0xb7, 0x84,
+ 0x00, 0x2b, 0xb9, 0x07, 0xee, 0x88, 0xf7, 0xa0, 0x40, 0x52, 0xfd, 0xcb,
+ 0x0c, 0x85, 0xec, 0x52, 0x38, 0x67, 0xf4, 0x63, 0x52, 0x2a, 0xe7, 0x6f,
+ 0x19, 0x65, 0xdd, 0xa4, 0x45, 0x37, 0xbf, 0xe1, 0xa4, 0x7c, 0xed, 0xfa,
+ 0x20, 0x2c, 0xa2, 0x9f, 0x48, 0xfc, 0xc2, 0xae, 0x24, 0x5d, 0xad, 0x93,
+ 0xbe, 0x3e, 0xdf, 0x25, 0xd0, 0x11, 0xa3, 0xf6, 0xb1, 0x2e, 0x26, 0x66,
+ 0xd8, 0x88, 0x63, 0x9e, 0x84, 0x40, 0xfc, 0x5b, 0x47, 0x53, 0xcd, 0xc2,
+ 0xff, 0xa4, 0xfd, 0x5d, 0x8c, 0xdb, 0xad, 0xe2, 0x32, 0x79, 0x65, 0xe1,
+ 0xeb, 0x5a, 0x74, 0xc5, 0xc1, 0x5e, 0x50, 0xb1, 0x4f, 0xab, 0xec, 0x0a,
+ 0xd2, 0x2e, 0x62, 0x58, 0x5b, 0xfd, 0x6f, 0xed, 0x4e, 0x10, 0x95, 0xb3,
+ 0x97, 0x00, 0x79, 0xd9, 0xe5, 0x56, 0x11, 0xea, 0x85, 0x8f, 0xc0, 0x26,
+ 0x2c, 0x8d, 0x0c, 0x71, 0x65, 0x02, 0x55, 0xe9, 0xa9, 0x81, 0x24, 0x08,
+ 0x92, 0xfc, 0x48, 0x5b, 0x52, 0xaf, 0xf3, 0xa2, 0xc6, 0x46, 0xe1, 0x82,
+ 0xb6, 0xc1, 0x13, 0xa5, 0x0a, 0xe7, 0xdb, 0xa8, 0x5e, 0xf0, 0x2d, 0x3d,
+ 0x1d, 0x65, 0xd9, 0x67, 0xc0, 0x3a, 0xba, 0xf2, 0x40, 0x4f, 0xe6, 0x46,
+ 0x05, 0x5d, 0x1a, 0x32, 0x5d, 0xb1, 0x75, 0xdb, 0x05, 0x89, 0x5e, 0xad,
+ 0xe7, 0x38, 0x9a, 0xc3, 0x6a, 0x40, 0x08, 0xb4, 0x76, 0x82, 0xe7, 0xdb,
+ 0x68, 0x0f, 0xb4, 0x8a, 0x25, 0x99, 0x40, 0x8e, 0x39, 0x73, 0x40, 0x88,
+ 0x28, 0x06, 0x74, 0x72, 0xcd, 0x24, 0x5e, 0x05, 0x88, 0xda, 0xdd, 0xb6,
+ 0x80, 0x0d, 0xb0, 0x92, 0x52, 0x8b, 0x36, 0xec, 0x6a, 0x54, 0x32, 0xd9,
+ 0xad, 0x96, 0xd5, 0x19, 0x45, 0x05, 0x50, 0x8d, 0x64, 0x51, 0x79, 0xa5,
+ 0x1f, 0x0f, 0x2e, 0xc1, 0x1b, 0xca, 0x0a, 0x13, 0x43, 0x3c, 0x4b, 0xd2,
+ 0x52, 0xa2, 0x99, 0x53, 0xb4, 0xdd, 0x96, 0xb9, 0xf0, 0x8c, 0x25, 0x63,
+ 0x6e, 0x4f, 0x4e, 0x48, 0xa6, 0x51, 0x8e, 0xc9, 0x8e, 0x23, 0x98, 0x3f,
+ 0xb2, 0x3f, 0x1b, 0x39, 0x05, 0xc2, 0x3a, 0x0e, 0x83, 0xb5, 0x0f, 0x7e,
+ 0xc2, 0xcd, 0x18, 0x50, 0x1b, 0x29, 0x64, 0xd2, 0x4d, 0x91, 0x44, 0x02,
+ 0xb3, 0xb3, 0x9a, 0x95, 0xc7, 0xeb, 0x68, 0xd6, 0xc8, 0x99, 0xe5, 0xf6,
+ 0xb2, 0xba, 0xfd, 0x09, 0x20, 0xfe, 0x8d, 0xa2, 0x43, 0xa2, 0x3b, 0x85,
+ 0x01, 0x0c, 0x3b, 0x51, 0xa2, 0x6d, 0x06, 0xf6, 0xd2, 0x0d, 0x55, 0x48,
+ 0x9d, 0x38, 0x0e, 0xcc, 0x0d, 0x4b, 0xd4, 0x3a, 0x1f, 0x49, 0x9a, 0xa9,
+ 0xa3, 0x02, 0x7e, 0xaa, 0x9f, 0x80, 0x95, 0xd6, 0x55, 0x21, 0x64, 0x95,
+ 0x57, 0x95, 0x99, 0xf9, 0xa6, 0x65, 0x6e, 0x6e, 0x0a, 0x7a, 0xd1, 0xcb,
+ 0x3e, 0xdf, 0xd8, 0x48, 0xcc, 0x31, 0x2a, 0x05, 0xf0, 0xaf, 0xa5, 0x42,
+ 0xc6, 0x93, 0x45, 0xe5, 0x1e, 0x50, 0xab, 0xb8, 0xf5, 0x8a, 0x5d, 0x53,
+ 0x49, 0xde, 0xcd, 0xa4, 0x42, 0x7a, 0x96, 0xae, 0xfe, 0xbf, 0xf2, 0xc7,
+ 0x9f, 0x56, 0xda, 0x68, 0xb9, 0x65, 0x5c, 0xfc, 0xcc, 0x23, 0x72, 0x3c,
+ 0xfc, 0x51, 0xb9, 0xc7, 0xe7, 0x0e, 0x48, 0xd2, 0x63, 0x44, 0xe6, 0xba,
+ 0xb2, 0x83, 0xc0, 0xe3, 0xa6, 0xa9, 0x6d, 0x6e, 0x26, 0xf8, 0x70, 0xd9,
+ 0xde, 0x9f, 0xa8, 0x50, 0x5a, 0x99, 0xff, 0x69, 0xf4, 0xd1, 0xdb, 0x1a,
+ 0x98, 0xfd, 0x34, 0x9c, 0x8a, 0xbe, 0x7b, 0xc9, 0xb1, 0xc0, 0x6c, 0xbc,
+ 0x9d, 0x5f, 0xf1, 0x2c, 0x8e, 0x4b, 0x36, 0x11, 0x97, 0x63, 0x2a, 0xec,
+ 0x56, 0xf5, 0x88, 0x1f, 0x25, 0xbd, 0xf6, 0x2a, 0x48, 0x16, 0xb3, 0xdd,
+ 0x42, 0xf9, 0x0c, 0x03, 0xe5, 0x36, 0x2e, 0xf0, 0x5a, 0x7b, 0x8b, 0x40,
+ 0xab, 0xb0, 0xe5, 0x4e, 0xc4, 0xed, 0xc0, 0xc9, 0x80, 0x95, 0x34, 0x3d,
+ 0x36, 0x63, 0xb5, 0xcb, 0x4e, 0x74, 0x5d, 0x23, 0x22, 0x97, 0x5c, 0x62,
+ 0x8e, 0xa3, 0x83, 0xf2, 0x09, 0xdb, 0x36, 0xc1, 0xd6, 0x92, 0xd3, 0xf6,
+ 0xf5, 0xe5, 0x58, 0x3f, 0x98, 0x9e, 0x8a, 0x14, 0x05, 0xd0, 0xfc, 0x22,
+ 0x1f, 0x27, 0xcb, 0xec, 0xa0, 0x0b, 0x97, 0xb6, 0x27, 0x28, 0xe1, 0xe3,
+ 0x18, 0x86, 0x45, 0xf8, 0x6a, 0x1d, 0xc6, 0x2b, 0x61, 0x27, 0x0e, 0xf4,
+ 0x5d, 0x23, 0xe6, 0x11, 0xe1, 0xc8, 0x02, 0x1e, 0x6d, 0x79, 0x01, 0xce,
+ 0xa2, 0xbb, 0x02, 0xbf, 0x8e, 0xd5, 0xb0, 0xb9, 0x2c, 0x15, 0xa6, 0x94,
+ 0x9c, 0x8d, 0x92, 0x99, 0x5e, 0x20, 0x19, 0xec, 0x1f, 0xec, 0x63, 0x79,
+ 0xd8, 0xb3, 0x64, 0x94, 0x89, 0x28, 0xa3, 0xac, 0x32, 0xd7, 0xe7, 0xe7,
+ 0x25, 0xf6, 0x42, 0x58, 0xff, 0x21, 0x1c, 0x6c, 0xa3, 0x8a, 0xeb, 0xd3,
+ 0x3d, 0xbe, 0xe4, 0x89, 0xaa, 0x39, 0x43, 0x5a, 0x66, 0x3e, 0x8b, 0xc5,
+ 0x6b, 0x42, 0x0b, 0x90, 0xf4, 0xd1, 0x69, 0x84, 0x68, 0x71, 0x60, 0x04,
+ 0x9c, 0xf6, 0xa9, 0xb7, 0x89, 0x5c, 0x96, 0x7e, 0x4d, 0x78, 0xb4, 0xa0,
+ 0x1a, 0x3e, 0xd4, 0xa1, 0x81, 0x29, 0x3d, 0x86, 0xd0, 0x76, 0x9e, 0xd1,
+ 0xac, 0xaa, 0x09, 0x2c, 0x18, 0x4f, 0x15, 0x26, 0x30, 0x9d, 0x3e, 0xe4,
+ 0x51, 0x2e, 0x35, 0xa8, 0x43, 0x01, 0x72, 0xce, 0x2a, 0xbf, 0x9b, 0x75,
+ 0x1b, 0xd8, 0x68, 0xe2, 0x21, 0x91, 0x28, 0xee, 0x32, 0x8b, 0x4d, 0x67,
+ 0x68, 0xb2, 0x5f, 0x19, 0x5e, 0x47, 0xd4, 0x78, 0x70, 0x4b, 0x05, 0xdb,
+ 0x8b, 0x2e, 0x80, 0x4a, 0xd0, 0xda, 0x00, 0xac, 0xa5, 0x69, 0xd9, 0xe7,
+ 0xe8, 0x1f, 0x4d, 0xc6, 0x32, 0x5f, 0xd4, 0x04, 0xa2, 0x3a, 0xa8, 0x43,
+ 0x8b, 0xdf, 0x30, 0xe7, 0x40, 0x57, 0x51, 0x68, 0x73, 0xa3, 0xf1, 0xa9,
+ 0xe0, 0xfa, 0x18, 0x19, 0x5f, 0x3e, 0xf6, 0x86, 0xe3, 0x6e, 0x3e, 0xf9,
+ 0xf0, 0xdb, 0xab, 0x6a, 0x88, 0x8a, 0x1b, 0x59, 0xcc, 0x5e, 0xfe, 0x96,
+ 0x32, 0x79, 0x6b, 0xdf, 0x79, 0x9d, 0xae, 0x20, 0xd0, 0xc2, 0x0f, 0xf3,
+ 0x64, 0xb0, 0x36, 0xeb, 0x65, 0x9a, 0xae, 0xa2, 0x4c, 0xc8, 0x45, 0x74,
+ 0xd4, 0x9b, 0x00, 0x6d, 0xb2, 0xe3, 0x3a, 0x18, 0xb0, 0x1b, 0x16, 0x87,
+ 0xd8, 0xef, 0x1c, 0xed, 0xec, 0x36, 0xe3, 0x6f, 0xe8, 0x5a, 0xf2, 0x06,
+ 0xd9, 0x80, 0xf8, 0xb3, 0x31, 0x12, 0x7b, 0x9f, 0x9e, 0xe8, 0x0b, 0x24,
+ 0x37, 0x97, 0xa9, 0xf9, 0xcb, 0x77, 0xa4, 0xcf, 0x4a, 0xd6, 0x64, 0xf5,
+ 0xd2, 0x7d, 0x99, 0x4f, 0x9d, 0xf3, 0xa8, 0x9c, 0xbf, 0x61, 0x0c, 0xae,
+ 0x7b, 0xf6, 0xc5, 0x0a, 0x03, 0x98, 0x40, 0x59, 0xd5, 0xfd, 0x98, 0x11,
+ 0x4f, 0x9c, 0x95, 0x01, 0x63, 0x6b, 0xbf, 0x2a, 0xe7, 0x90, 0xd2, 0x49,
+ 0xc5, 0x02, 0x3c, 0x04, 0xa0, 0x52, 0x91, 0x26, 0x45, 0x9d, 0xa9, 0xd9,
+ 0x41, 0x9d, 0x3b, 0x05, 0x93, 0x70, 0x50, 0xb2, 0x09, 0x56, 0x55, 0xbf,
+ 0x56, 0xde, 0xb4, 0x63, 0x42, 0x03, 0x84, 0xf9, 0x74, 0x85, 0xc1, 0x1b,
+ 0x5e, 0xf6, 0x5e, 0xdf, 0x01, 0x86, 0xa6, 0x99, 0x3b, 0x7d, 0x73, 0x0c,
+ 0xc9, 0x6e, 0xad, 0x12, 0x9c, 0x4d, 0x31, 0x4e, 0xd4, 0x12, 0x0a, 0x4e,
+ 0x2b, 0x17, 0xaa, 0xb8, 0x4f, 0x5c, 0x20, 0xe3, 0xe6, 0x3a, 0x0a, 0xcc,
+ 0xbd, 0x5d, 0x26, 0x98, 0x96, 0x9d, 0x2c, 0xf2, 0xcc, 0x1e, 0x0f, 0x03,
+ 0x67, 0x67, 0x8e, 0x34, 0x9d, 0x54, 0x9c, 0xc8, 0x58, 0x99, 0xe3, 0x45,
+ 0xc3, 0xb5, 0xba, 0xc7, 0x45, 0x97, 0xb0, 0x56, 0x60, 0xf6, 0x80, 0xee,
+ 0x2b, 0x06, 0x90, 0x99, 0x3e, 0xa8, 0xb7, 0xdc, 0xb0, 0xb0, 0x6d, 0xb9,
+ 0x68, 0x2b, 0x31, 0x79, 0xf4, 0x1d, 0x05, 0xe1, 0x6e, 0x67, 0x04, 0xb1,
+ 0x9b, 0xa7, 0x6a, 0x69, 0x0c, 0x4d, 0x76, 0x17, 0xf4, 0x9b, 0x2a, 0xc1,
+ 0x90, 0x6c, 0xf5, 0x67, 0x98, 0x5e, 0xb4, 0x8d, 0xad, 0x12, 0x6c, 0xaf,
+ 0x89, 0x09, 0x82, 0xb2, 0x64, 0x58, 0x44, 0xd6, 0x01, 0xd6, 0x0d, 0x20,
+ 0xf7, 0x04, 0xad, 0x4e, 0x1d, 0x9b, 0x4f, 0xad, 0xf9, 0x45, 0x34, 0xbf,
+ 0xeb, 0x38, 0x9e, 0x55, 0x87, 0xdd, 0xcc, 0x61, 0x29, 0x5f, 0xe1, 0xf6,
+ 0x35, 0x87, 0xc6, 0x5b, 0xc2, 0x90, 0xb9, 0x60, 0xfc, 0xdb, 0x74, 0x7d,
+ 0xce, 0x0e, 0x93, 0x58, 0x28, 0xfa, 0xf8, 0xee, 0x8e, 0x75, 0xe6, 0xcd,
+ 0x8d, 0x9c, 0xc5, 0x15, 0x05, 0x4c, 0x39, 0x34, 0x73, 0x12, 0x56, 0x74,
+ 0x80, 0x09, 0x0a, 0x9f, 0x25, 0x53, 0x80, 0x76, 0x20, 0x04, 0xf4, 0x99,
+ 0x6e, 0xbb, 0x29, 0x68, 0x54, 0xe0, 0xe8, 0x1e, 0x40, 0x1d, 0x82, 0x25,
+ 0xdc, 0xfe, 0x44, 0x37, 0x79, 0xe7, 0x97, 0x0a, 0x02, 0x8c, 0x56, 0xc1,
+ 0x16, 0xe4, 0xce, 0xf7, 0xfb, 0x43, 0x9f, 0xd2, 0xe5, 0xa0, 0x18, 0xab,
+ 0xdb, 0x7f, 0xb7, 0x08, 0x43, 0xe6, 0xed, 0xae, 0xbf, 0x00, 0x8f, 0xa5,
+ 0xfa, 0xdd, 0x95, 0x69, 0x3d, 0xfe, 0xcf, 0x0f, 0x8a, 0x5c, 0x8e, 0x21,
+ 0x1b, 0xf4, 0xae, 0x79, 0x54, 0x2a, 0xbd, 0x68, 0x9c, 0xde, 0xd2, 0x7c,
+ 0x51, 0xd6, 0x12, 0x72, 0xed, 0xaa, 0xa3, 0x34, 0x6f, 0x1e, 0x00, 0x44,
+ 0xc5, 0xec, 0x02, 0x17, 0x60, 0xf2, 0xd4, 0xe9, 0x82, 0x2f, 0xfe, 0x53,
+ 0x1c, 0xaf, 0xd1, 0xac, 0xdb, 0x8c, 0xf8, 0xce, 0x89, 0x27, 0xa1, 0x53,
+ 0xcf, 0x6a, 0x3f, 0xd3, 0xb7, 0x53, 0xce, 0x98, 0x5c, 0x57, 0x54, 0x6d,
+ 0x9b, 0xf4, 0x6e, 0x67, 0xad, 0x69, 0xeb, 0xa6, 0x86, 0xba, 0x9a, 0xd4,
+ 0x99, 0xb0, 0xde, 0x53, 0x28, 0x90, 0x42, 0x43, 0xf0, 0x57, 0x1a, 0x01,
+ 0x05, 0x08, 0x41, 0xe2, 0x9b, 0x5d, 0x95, 0xe4, 0x5c, 0x4d, 0x67, 0xd5,
+ 0xd2, 0xe8, 0xb6, 0xcb, 0xa8, 0x5e, 0x66, 0x8e, 0x27, 0xa6, 0x01, 0x40,
+ 0x22, 0x40, 0x29, 0xff, 0x74, 0x30, 0xf9, 0x3d, 0xf7, 0x93, 0x40, 0xb6,
+ 0x46, 0xb2, 0xe1, 0x26, 0x57, 0x51, 0xd3, 0x24, 0x5d, 0x2b, 0xc7, 0x4c,
+ 0x6e, 0x53, 0xca, 0x6d, 0xc5, 0xee, 0x05, 0x51, 0x2b, 0xe1, 0x6a, 0xa1,
+ 0x78, 0x0a, 0x97, 0x63, 0xdb, 0x09, 0x6e, 0x99, 0x02, 0x4f, 0x68, 0x2d,
+ 0x5a, 0xd2, 0xe9, 0x60, 0x27, 0x9a, 0x9f, 0xb3, 0xcf, 0xb2, 0xfe, 0xe2,
+ 0x8e, 0x3a, 0xdf, 0x6a, 0xaf, 0x4f, 0x6e, 0x79, 0x91, 0xfb, 0x78, 0x54,
+ 0x54, 0x1d, 0xaf, 0x59, 0x25, 0x84, 0xdf, 0x57, 0x58, 0x1c, 0x4f, 0xf2,
+ 0xf8, 0x2d, 0x4a, 0xd2, 0x97, 0xbe, 0xdc, 0xcd, 0xf1, 0x87, 0xe7, 0x42,
+ 0x30, 0xa7, 0x57, 0x6f, 0x8f, 0x98, 0x5e, 0x2c, 0x34, 0x2b, 0x76, 0x07,
+ 0xe8, 0x58, 0xca, 0x89, 0x01, 0x92, 0x8f, 0x71, 0x07, 0x35, 0x12, 0x74,
+ 0x78, 0x50, 0x8d, 0x9f, 0x47, 0xed, 0xa9, 0x02, 0xa3, 0xbf, 0xb0, 0xc6,
+ 0x98, 0x8d, 0x8b, 0x32, 0x2b, 0xca, 0x72, 0xdc, 0xbe, 0xa5, 0x12, 0x09,
+ 0xd1, 0xc7, 0x4b, 0xc7, 0xef, 0x70, 0x92, 0x37, 0xb7, 0x42, 0x16, 0x31,
+ 0x28, 0x3c, 0xc4, 0x31, 0x18, 0x8d, 0x9f, 0x1e, 0x8c, 0x6b, 0xef, 0x26,
+ 0xd8, 0xfd, 0xe0, 0x5a, 0x11, 0x60, 0x02, 0xf0, 0x3f, 0x01, 0x4c, 0x15,
+ 0x4d, 0x35, 0x90, 0x64, 0xc4, 0xc7, 0xc7, 0x59, 0x4e, 0x2b, 0x2d, 0x84,
+ 0x46, 0xf7, 0xe4, 0x68, 0xad, 0x93, 0xa7, 0xb9, 0x51, 0xdf, 0x7c, 0x62,
+ 0x73, 0x4e, 0xa2, 0x1a, 0x24, 0xb7, 0xed, 0x51, 0x64, 0x48, 0xdd, 0xe7,
+ 0x76, 0x21, 0x62, 0xa2, 0x3f, 0xda, 0xa3, 0xd9, 0x66, 0x66, 0xbb, 0x04,
+ 0x45, 0x27, 0x10, 0xbf, 0x9a, 0x1d, 0xbf, 0xf9, 0x58, 0x6e, 0xc5, 0xeb,
+ 0xd9, 0xd7, 0x0d, 0xe0, 0x60, 0x2f, 0x16, 0x47, 0xce, 0xc5, 0x7b, 0x26,
+ 0xfc, 0xbc, 0x54, 0xd2, 0xbf, 0x40, 0xc0, 0x31, 0xdb, 0x6c, 0x22, 0x81,
+ 0x69, 0x07, 0x1c, 0x9e, 0x33, 0xf7, 0x4e, 0xf3, 0x0c, 0x38, 0x45, 0x1e,
+ 0x8d, 0x51, 0x49, 0x7a, 0x26, 0x43, 0xfe, 0x32, 0x0e, 0x98, 0xef, 0x90,
+ 0x55, 0x50, 0xbb, 0xbf, 0xca, 0x15, 0x91, 0x38, 0x5e, 0x1b, 0xcb, 0xb0,
+ 0xc3, 0xd5, 0x89, 0x80, 0x0f, 0x91, 0x4e, 0xbb, 0x31, 0xd9, 0x49, 0xa0,
+ 0x4f, 0x28, 0xf6, 0x26, 0x8e, 0x88, 0x9e, 0x9e, 0x4a, 0x47, 0x0d, 0xac,
+ 0xb6, 0x8a, 0xdf, 0xc4, 0x4c, 0xc9, 0x3f, 0x16, 0xf3, 0x24, 0x0f, 0x52,
+ 0x60, 0x12, 0x2b, 0x2f, 0x08, 0x35, 0xd1, 0x08, 0xff, 0x13, 0xef, 0x27,
+ 0xd8, 0x97, 0xc4, 0x9e, 0x16, 0x4d, 0xbd, 0xdb, 0x25, 0xca, 0xff, 0x50,
+ 0x65, 0x6f, 0xc3, 0x6f, 0x6c, 0x62, 0x86, 0x4f, 0x10, 0xb5, 0x3c, 0x64,
+ 0xe0, 0xd5, 0xb8, 0xaf, 0xff, 0xea, 0xc7, 0xef, 0xed, 0x64, 0x7d, 0xcd,
+ 0xc4, 0x2f, 0x81, 0xf9, 0x7b, 0x9c, 0xaa, 0xb2, 0x62, 0x75, 0xfe, 0xb8,
+ 0x43, 0x79, 0x10, 0xe8, 0xa1, 0xa8, 0x3a, 0x6f, 0x43, 0x89, 0x64, 0xc0,
+ 0x7a, 0x5f, 0x4a, 0xe7, 0xd0, 0x3b, 0xab, 0x2e, 0x32, 0x9d, 0x3b, 0x35,
+ 0x23, 0xec, 0x47, 0x85, 0xf4, 0xff, 0xcc, 0xfd, 0x14, 0x72, 0x6d, 0x1c,
+ 0x33, 0xfe, 0x38, 0xb7, 0xf4, 0x84, 0x08, 0x2a, 0x17, 0x67, 0xd3, 0x48,
+ 0x9c, 0x43, 0x93, 0x5d, 0xa6, 0x8c, 0xbb, 0xb5, 0x7a, 0x40, 0xab, 0xd6,
+ 0x15, 0xc5, 0x36, 0x2d, 0x67, 0x42, 0xf7, 0xe7, 0xdb, 0xbd, 0x2a, 0xd9,
+ 0x29, 0xe6, 0x3b, 0x82, 0xff, 0x10, 0x02, 0xe8, 0x3d, 0xc4, 0x70, 0x4f,
+ 0xe3, 0xe5, 0x98, 0x76, 0xf3, 0x37, 0x30, 0x53, 0xd9, 0x49, 0x9d, 0x45,
+ 0xed, 0x2d, 0xad, 0x0b, 0xfa, 0xd0, 0xe9, 0xd9, 0x29, 0x12, 0x30, 0xc1,
+ 0xf7, 0x38, 0x7b, 0x2e, 0xad, 0xb1, 0xb5, 0x9c, 0xe6, 0xc8, 0xf0, 0x3d,
+ 0xf6, 0x45, 0x9c, 0x07, 0x5e, 0xfc, 0x34, 0x66, 0x51, 0x79, 0xc8, 0x5a,
+ 0x82, 0x51, 0x48, 0x58, 0x70, 0x42, 0x14, 0x29, 0x19, 0x53, 0x90, 0xeb,
+ 0xe3, 0x0f, 0xea, 0xbf, 0x58, 0x06, 0xb7, 0x9a, 0x5e, 0x17, 0x95, 0x3b,
+ 0x70, 0x1d, 0xad, 0x3d, 0x52, 0xc7, 0xcd, 0xcd, 0x77, 0x75, 0x01, 0xb0,
+ 0xbe, 0xa8, 0xe7, 0x4a, 0x2e, 0x5b, 0xa7, 0xe2, 0xc7, 0xfa, 0x6f, 0x2c,
+ 0xd0, 0xad, 0x1f, 0xa5, 0x76, 0xff, 0xd2, 0x18, 0xe3, 0x81, 0x71, 0xbc,
+ 0xb6, 0xfe, 0x88, 0xa4, 0xea, 0xdb, 0xc7, 0xa5, 0xf0, 0x4e, 0x64, 0xc0,
+ 0xf3, 0xea, 0xe3, 0x7a, 0x5c, 0x53, 0xad, 0xc4, 0xa9, 0x14, 0x45, 0x48,
+ 0xfd, 0x2f, 0x51, 0xf1, 0x46, 0xe3, 0x5d, 0x87, 0x29, 0xd8, 0x48, 0x4e,
+ 0x4b, 0xef, 0x2b, 0x8d, 0x04, 0x21, 0x6f, 0x9c, 0x4e, 0x8b, 0x88, 0x8c,
+ 0xd4, 0xf4, 0x97, 0x0e, 0x13, 0xcd, 0x79, 0x3f, 0x0d, 0xd8, 0x8a, 0xf0,
+ 0xf1, 0xf8, 0x85, 0xf3, 0x9c, 0x6e, 0x3d, 0x29, 0x65, 0xb2, 0x2d, 0x16,
+ 0x73, 0x27, 0x7d, 0x59, 0xf6, 0xbd, 0x4c, 0x2c, 0x8f, 0xaa, 0x0e, 0x34,
+ 0xd0, 0x4c, 0xa2, 0x0b, 0xcb, 0x69, 0x32, 0x80, 0x07, 0x58, 0xc5, 0x50,
+ 0x61, 0x05, 0x00, 0x4b, 0xf1, 0x55, 0xce, 0xbb, 0x52, 0x35, 0xe9, 0xe6,
+ 0x85, 0x48, 0x1f, 0x85, 0x4e, 0x01, 0xdf, 0xeb, 0x6f, 0x6f, 0x0a, 0xe0,
+ 0xad, 0x61, 0x94, 0xc6, 0x2e, 0x99, 0xc0, 0xcd, 0x59, 0xf9, 0xc4, 0x8d,
+ 0x15, 0x17, 0x43, 0x22, 0xcf, 0x63, 0xf8, 0x01, 0x3a, 0xbc, 0x00, 0x06,
+ 0x07, 0xb5, 0xf6, 0x00, 0x97, 0xd5, 0xff, 0x7e, 0x89, 0x26, 0x70, 0x14,
+ 0xdc, 0x12, 0xdd, 0x48, 0x47, 0x35, 0xf8, 0x09, 0xb9, 0x65, 0x95, 0x2a,
+ 0xa5, 0x40, 0x0a, 0x07, 0xc4, 0x4f, 0x18, 0x4a, 0x6a, 0x3f, 0x90, 0x66,
+ 0xeb, 0x71, 0xae, 0xb8, 0x0f, 0x74, 0xb0, 0x3c, 0x42, 0x63, 0x6c, 0x77,
+ 0x5d, 0xd5, 0x97, 0xb7, 0x5a, 0x04, 0xe9, 0x47, 0xb3, 0x61, 0xbe, 0x05,
+ 0x5b, 0x96, 0x93, 0x72, 0x30, 0x86, 0x67, 0x47, 0x83, 0x58, 0xd0, 0x00,
+ 0x7e, 0x47, 0x2e, 0x40, 0xf1, 0xb2, 0x45, 0xc8, 0x71, 0x9a, 0x45, 0x25,
+ 0xdc, 0xdf, 0x34, 0x16, 0x4e, 0x52, 0x74, 0x64, 0x7b, 0xcd, 0x5b, 0x67,
+ 0x64, 0xf6, 0x59, 0x17, 0x5a, 0x91, 0x2d, 0xc0, 0x65, 0x97, 0x9c, 0x60,
+ 0x1f, 0xca, 0xd2, 0x0d, 0xea, 0x53, 0xf9, 0xa5, 0xe4, 0x9f, 0x9d, 0x5f,
+ 0x32, 0xd8, 0x4d, 0x8a, 0xc8, 0x0a, 0x63, 0xa2, 0x90, 0x5d, 0x24, 0x2b,
+ 0x35, 0xc3, 0x44, 0x20, 0xe1, 0xac, 0x89, 0xa4, 0xa1, 0x93, 0x39, 0xe9,
+ 0x3e, 0xcf, 0x9f, 0x71, 0xda, 0x14, 0x53, 0x61, 0x8a, 0x3e, 0xec, 0xce,
+ 0xc3, 0x84, 0xc1, 0x0b, 0xfa, 0x4b, 0x8c, 0x21, 0x95, 0x99, 0x86, 0x36,
+ 0x5c, 0x21, 0xde, 0x39, 0xee, 0xe7, 0x46, 0x33, 0x1c, 0x64, 0x2e, 0x6d,
+ 0x32, 0x2c, 0xd3, 0xcf, 0x06, 0xa8, 0x91, 0x3d, 0xd2, 0x92, 0x97, 0x7b,
+ 0x8c, 0x23, 0x56, 0x05, 0x9b, 0x4b, 0xc9, 0x6f, 0x43, 0xa8, 0x9d, 0xba,
+ 0xbb, 0x90, 0x73, 0x4b, 0x8e, 0x1e, 0xa3, 0x0f, 0x9c, 0x8c, 0x81, 0xf8,
+ 0xb2, 0xda, 0x41, 0x47, 0x7b, 0x9e, 0x0f, 0x8a, 0xea, 0x48, 0x00, 0xa2,
+ 0x4a, 0x93, 0xf9, 0x3d, 0x04, 0xcd, 0xa9, 0xf8, 0x95, 0xb9, 0x45, 0x64,
+ 0x28, 0xb3, 0x27, 0x30, 0xfc, 0x59, 0x4e, 0xbc, 0x19, 0xe9, 0x68, 0x41,
+ 0x35, 0x65, 0xc9, 0xaf, 0x07, 0x57, 0xf9, 0x26, 0x88, 0x96, 0x4f, 0xa1,
+ 0xb3, 0xff, 0x5c, 0x0e, 0xb5, 0xc9, 0x1a, 0x66, 0x07, 0x19, 0x4c, 0x59,
+ 0x11, 0xf7, 0xa5, 0xcb, 0x76, 0x7d, 0x75, 0x20, 0x8d, 0xa2, 0x31, 0x56,
+ 0x8e, 0xae, 0x75, 0xba, 0x3f, 0x57, 0x19, 0x33, 0xa6, 0xbc, 0x15, 0x19,
+ 0x7f, 0x2e, 0x8c, 0xf1, 0x1d, 0xee, 0x0b, 0x66, 0xa9, 0x1a, 0xe3, 0x65,
+ 0xa9, 0xc5, 0xda, 0xbb, 0x8a, 0xe5, 0x8a, 0xe6, 0xae, 0x51, 0x73, 0xed,
+ 0x48, 0xaf, 0x22, 0x73, 0xd9, 0xd8, 0x9b, 0x74, 0x8b, 0xfe, 0xdb, 0xb2,
+ 0x06, 0xf6, 0x45, 0x80, 0xcc, 0x59, 0xce, 0x0c, 0x40, 0xb6, 0xfd, 0x38,
+ 0x65, 0x79, 0x4b, 0x5e, 0xb9, 0x99, 0x78, 0x37, 0x49, 0xb9, 0x4f, 0x82,
+ 0xef, 0x6d, 0xdf, 0xb7, 0xe1, 0xd6, 0xca, 0xe8, 0x77, 0x96, 0xb1, 0x83,
+ 0x4c, 0x3d, 0xc6, 0x5a, 0xb9, 0xa3, 0xb5, 0xf1, 0x3e, 0x18, 0xc5, 0xbc,
+ 0x8f, 0xac, 0xbf, 0xb5, 0x01, 0xbf, 0x00, 0x11, 0x45, 0x26, 0xf6, 0x39,
+ 0x07, 0x2e, 0xee, 0x1d, 0x5f, 0xc9, 0x8d, 0xc5, 0xc6, 0xeb, 0x84, 0x9b,
+ 0x0a, 0xd1, 0x0f, 0xb7, 0xbe, 0x00, 0x52, 0xd1, 0xc8, 0xdb, 0xc7, 0x5c,
+ 0xee, 0xb3, 0xca, 0x39, 0x69, 0xe4, 0x69, 0x29, 0xe0, 0x8d, 0x47, 0x71,
+ 0x9d, 0xd5, 0x98, 0x5f, 0x1b, 0xa2, 0xa7, 0xce, 0xf0, 0xfd, 0x44, 0x52,
+ 0x68, 0xee, 0x83, 0x5b, 0xfa, 0xab, 0xa9, 0x94, 0x33, 0x82, 0x39, 0x12,
+ 0xe5, 0xa4, 0x1b, 0x02, 0x1e, 0xb7, 0x07, 0x04, 0x6b, 0xa0, 0x3d, 0xb6,
+ 0x55, 0x19, 0xe8, 0x4d, 0x4a, 0x70, 0x6b, 0x7b, 0x4d, 0x22, 0x05, 0xfa,
+ 0xff, 0x88, 0x4f, 0xac, 0x41, 0xa7, 0xf2, 0x9d, 0x32, 0x93, 0x90, 0x6c,
+ 0x8f, 0x95, 0xc1, 0x19, 0xfa, 0xdb, 0x5d, 0xde, 0x07, 0x5d, 0x54, 0x37,
+ 0x7d, 0x8d, 0xa2, 0x80, 0xc2, 0x01, 0xf0, 0xf5, 0x65, 0x2a, 0xf8, 0x6c,
+ 0x25, 0x5d, 0xae, 0x9f, 0xc7, 0x99, 0x63, 0x55, 0x85, 0x46, 0x12, 0x61,
+ 0x85, 0xcb, 0x92, 0x6b, 0xba, 0xb0, 0xfe, 0x54, 0xc2, 0xae, 0x6c, 0xfa,
+ 0x4d, 0x10, 0x0b, 0xdd, 0xff, 0x31, 0x30, 0x26, 0x79, 0x94, 0xa6, 0xd2,
+ 0xb8, 0xa2, 0x78, 0xed, 0x8f, 0x77, 0xbe, 0x9c, 0xcf, 0xbf, 0x5a, 0xe0,
+ 0xf5, 0xda, 0x3b, 0x0b, 0x3b, 0xd1, 0x6e, 0x05, 0x21, 0xad, 0x23, 0xe7,
+ 0x62, 0x51, 0xdd, 0x17, 0xda, 0x5c, 0x35, 0x22, 0x5d, 0xfc, 0x2c, 0x40,
+ 0x48, 0x14, 0xa2, 0x2f, 0x47, 0x76, 0xbc, 0x5b, 0x6e, 0x39, 0xd7, 0x2d,
+ 0xcc, 0x50, 0xba, 0x51, 0x85, 0x48, 0x7b, 0x5f, 0x5d, 0xb4, 0xa3, 0x78,
+ 0xb4, 0xe8, 0x3c, 0x66, 0xa6, 0x94, 0x3b, 0x76, 0x9d, 0x3f, 0x7a, 0x8e,
+ 0x4b, 0xb3, 0x43, 0x93, 0x94, 0x6f, 0xb8, 0x51, 0x49, 0x9e, 0x77, 0xf3,
+ 0xc4, 0xc9, 0x16, 0x6a, 0x4b, 0xd4, 0xa2, 0xb2, 0x8e, 0x60, 0xfe, 0xaf,
+ 0x23, 0x7a, 0xdf, 0x26, 0x6f, 0x87, 0xc8, 0x8a, 0x81, 0x11, 0xe7, 0xc2,
+ 0x24, 0x24, 0x47, 0x85, 0x5b, 0xc1, 0x93, 0x58, 0x8d, 0xf9, 0x60, 0x78,
+ 0x8a, 0xc8, 0x94, 0x78, 0xbc, 0xe1, 0x8c, 0xf7, 0xbc, 0xe0, 0xa4, 0x41,
+ 0x5e, 0x33, 0x34, 0x1b, 0x3b, 0xf1, 0x1f, 0x98, 0x24, 0xce, 0xe5, 0xe0,
+ 0x4f, 0x22, 0x2a, 0x9b, 0x1d, 0x05, 0xc6, 0xbf, 0x3f, 0x7e, 0x3f, 0x15,
+ 0x15, 0xce, 0x1b, 0x85, 0x8b, 0x73, 0x2c, 0x85, 0x20, 0xd3, 0x2e, 0x22,
+ 0x62, 0x4b, 0x8c, 0x51, 0x8b, 0xd9, 0x44, 0x1a, 0x01, 0x02, 0xf0, 0xa6,
+ 0x56, 0x87, 0x21, 0xfc, 0x0a, 0xa8, 0xa6, 0x8f, 0x4e, 0xf2, 0xd3, 0xd5,
+ 0x8e, 0x94, 0xdf, 0x3a, 0xe6, 0x54, 0x30, 0x90, 0xe3, 0xcd, 0x26, 0xde,
+ 0x3d, 0x2e, 0x6c, 0x30, 0xa3, 0x54, 0x5d, 0x02, 0x5a, 0x51, 0xff, 0xa8,
+ 0x93, 0x1e, 0x03, 0x15, 0x85, 0x5e, 0xda, 0x52, 0x39, 0x91, 0x61, 0x21,
+ 0xdd, 0x2d, 0xb5, 0x63, 0x80, 0xdd, 0x13, 0x5e, 0x15, 0x44, 0x69, 0xae,
+ 0x61, 0xa1, 0xb8, 0x7d, 0xb5, 0xa9, 0x22, 0xe6, 0xf7, 0xce, 0xa3, 0x0c,
+ 0xff, 0xcb, 0xef, 0x05, 0xff, 0xbd, 0xf5, 0xe6, 0xc5, 0x56, 0xa4, 0x8e,
+ 0x2c, 0x5f, 0xab, 0x46, 0x6e, 0xb5, 0xe5, 0x5c, 0x86, 0xa3, 0x70, 0x69,
+ 0x01, 0xb5, 0x6d, 0x44, 0x19, 0x44, 0x98, 0xa8, 0x8d, 0x1c, 0xb9, 0x8c,
+ 0xd3, 0x1b, 0x0b, 0xe6, 0x28, 0x36, 0xfd, 0x4e, 0x14, 0x67, 0x3c, 0xb8,
+ 0x0a, 0x17, 0xee, 0xb2, 0x6c, 0x01, 0x5d, 0x58, 0xf0, 0x6a, 0x8f, 0x19,
+ 0x90, 0x0b, 0xdf, 0xd5, 0xa9, 0xef, 0xb7, 0x69, 0xd2, 0x37, 0x1e, 0xdd,
+ 0x6b, 0xa6, 0xc0, 0x16, 0xa9, 0x4d, 0xb6, 0xf0, 0x5f, 0xf2, 0x39, 0x76,
+ 0x20, 0x99, 0x5d, 0x9e, 0x43, 0x84, 0xc4, 0x8e, 0x3b, 0xb9, 0xc4, 0xf8,
+ 0x62, 0x88, 0x3c, 0xd3, 0x60, 0x66, 0xd8, 0xbd, 0x1f, 0xc5, 0x01, 0x90,
+ 0xa0, 0x8e, 0x3a, 0xbd, 0x05, 0x03, 0x63, 0x45, 0x5d, 0xc8, 0x2e, 0xb2,
+ 0xa5, 0x56, 0xb4, 0xe1, 0x84, 0x0c, 0xcc, 0xff, 0x8e, 0xbb, 0xb7, 0x99,
+ 0x6e, 0x15, 0x93, 0x33, 0xca, 0xe8, 0x1e, 0x66, 0x96, 0x04, 0x98, 0x33,
+ 0x20, 0x47, 0x3b, 0xe6, 0x35, 0xdb, 0x69, 0xb7, 0x2a, 0x54, 0x03, 0xe4,
+ 0x66, 0xde, 0x97, 0x4e, 0x95, 0xbe, 0xbe, 0x1d, 0xd3, 0xe3, 0x7f, 0x75,
+ 0x76, 0xdd, 0xae, 0xfd, 0xbc, 0x7b, 0xa8, 0xcf, 0xf9, 0xb8, 0x04, 0x0c,
+ 0xdb, 0x81, 0xaa, 0x7a, 0xa3, 0xa0, 0xff, 0xda, 0xeb, 0x8d, 0x88, 0x29,
+ 0x9d, 0x77, 0xe0, 0x61, 0x8a, 0x6e, 0x84, 0xca, 0x76, 0xd4, 0x0a, 0x7a,
+ 0x80, 0xdf, 0xaa, 0x56, 0x96, 0x38, 0x26, 0x4f, 0x23, 0x6f, 0xd7, 0x02,
+ 0xd8, 0xa9, 0xd6, 0xad, 0x37, 0x4c, 0x43, 0x8e, 0xad, 0x72, 0x89, 0xfe,
+ 0x44, 0x5a, 0xb7, 0x5d, 0xf7, 0xdf, 0x8f, 0xfd, 0x79, 0x2c, 0xa9, 0xb6,
+ 0xa4, 0xba, 0xda, 0x8a, 0xa1, 0xa5, 0xb9, 0xe2, 0x0f, 0x28, 0xf2, 0xa9,
+ 0xef, 0x85, 0xbd, 0xce, 0xd8, 0x66, 0x6a, 0xb8, 0x6c, 0xe6, 0x38, 0x5b,
+ 0xbd, 0x15, 0xf2, 0x6b, 0x2f, 0xcb, 0xbf, 0xee, 0xe5, 0xc7, 0xf0, 0xf4,
+ 0x8b, 0xca, 0xbc, 0xdd, 0x58, 0xd2, 0x06, 0xaa, 0x0c, 0x9c, 0x42, 0x9b,
+ 0xf9, 0x05, 0x0e, 0xf0, 0xad, 0x28, 0xe8, 0x53, 0x27, 0x0e, 0xb5, 0x00,
+ 0x06, 0xc3, 0x50, 0x78, 0x55, 0x93, 0x91, 0x70, 0xb9, 0x20, 0xea, 0x1f,
+ 0xbb, 0xdc, 0xa4, 0x9b, 0xd0, 0x7a, 0xd6, 0xcd, 0xd7, 0x45, 0x83, 0x41,
+ 0xa1, 0x78, 0xef, 0xa0, 0x2c, 0xc2, 0x2d, 0x18, 0x6f, 0x72, 0x5e, 0x56,
+ 0x36, 0xd5, 0x94, 0x41, 0x6e, 0x2e, 0xe4, 0x96, 0xe1, 0xe4, 0xb9, 0xf9,
+ 0xf5, 0x52, 0x1b, 0xd9, 0xf3, 0x82, 0x00, 0x38, 0x91, 0xf8, 0x37, 0xe4,
+ 0x32, 0xb8, 0x02, 0xc3, 0x30, 0x78, 0xf6, 0x24, 0x35, 0xef, 0x31, 0x98,
+ 0x8b, 0x58, 0x8d, 0xf0, 0x42, 0xab, 0x47, 0xc2, 0xe1, 0x20, 0x1b, 0x14,
+ 0x19, 0x51, 0x7d, 0xb7, 0xb9, 0x97, 0x31, 0x00, 0xb8, 0x84, 0x59, 0xdd,
+ 0xb2, 0x97, 0xfe, 0x2e, 0x84, 0x30, 0x6d, 0xe9, 0x0d, 0x43, 0xca, 0x04,
+ 0x40, 0x6b, 0x08, 0x57, 0xa2, 0xdb, 0xc6, 0xc6, 0x00, 0xa0, 0xa9, 0x86,
+ 0x5b, 0x2d, 0xad, 0x89, 0xbf, 0xdc, 0x9d, 0xec, 0x1f, 0x13, 0xea, 0x78,
+ 0xde, 0xb5, 0x20, 0xe4, 0x4d, 0x74, 0xde, 0xa7, 0xaa, 0xa9, 0xc6, 0x78,
+ 0x43, 0xcb, 0xe0, 0x44, 0x4c, 0x12, 0x3c, 0x7d, 0x2b, 0xd7, 0x94, 0xc0,
+ 0x05, 0xa8, 0x97, 0xbc, 0xda, 0x83, 0x85, 0xc5, 0x1a, 0xe6, 0xd2, 0x68,
+ 0xdf, 0xfa, 0xb0, 0x5d, 0x63, 0x3b, 0x73, 0xcb, 0xfc, 0x75, 0xb3, 0xbd,
+ 0xe8, 0x60, 0xb8, 0xc4, 0x99, 0xd6, 0xea, 0xf6, 0x21, 0x6e, 0x14, 0xd3,
+ 0x4a, 0x6b, 0x78, 0xb2, 0xd3, 0x1e, 0xa8, 0x5f, 0x88, 0x7d, 0xfd, 0x70,
+ 0x3a, 0x57, 0x17, 0x23, 0xf1, 0x3f, 0x5f, 0xa9, 0xee, 0x1f, 0x52, 0xdb,
+ 0x92, 0x3f, 0xea, 0xf0, 0x97, 0x96, 0x56, 0x77, 0x33, 0xd0, 0x57, 0xe5,
+ 0xa7, 0xb3, 0xf8, 0xa2, 0x59, 0xe9, 0x08, 0xb9, 0x56, 0xdc, 0x07, 0xdc,
+ 0x9f, 0xff, 0x8e, 0x97, 0x57, 0xe9, 0x92, 0xec, 0xff, 0xdd, 0x72, 0xd6,
+ 0x55, 0x94, 0xa7, 0xf8, 0xb8, 0x1d, 0x55, 0xf2, 0xf9, 0xda, 0xa3, 0xd4,
+ 0x27, 0x99, 0xb4, 0x65, 0x5d, 0xe7, 0xa7, 0x86, 0x46, 0xe0, 0x39, 0x20,
+ 0xe4, 0x5d, 0xeb, 0x4d, 0x5f, 0xbe, 0x9f, 0x66, 0x6f, 0xdd, 0xab, 0xbd,
+ 0x16, 0xd2, 0x3d, 0x9e, 0xc2, 0x57, 0x68, 0x6b, 0x0a, 0x09, 0xc5, 0xac,
+ 0x5f, 0x3e, 0x59, 0x6e, 0x68, 0xf6, 0x10, 0x22, 0x9c, 0x02, 0x18, 0x0a,
+ 0x6c, 0xb6, 0x77, 0xa6, 0x77, 0x41, 0x9c, 0x4c, 0x60, 0xde, 0xec, 0xdd,
+ 0x15, 0xc1, 0x21, 0x02, 0xde, 0x8c, 0x3a, 0x49, 0x5b, 0xf0, 0xc3, 0xc8,
+ 0x1e, 0x01, 0x27, 0xf5, 0xc0, 0x22, 0x35, 0x3e, 0x86, 0x95, 0x33, 0xbb,
+ 0xb8, 0x5d, 0x46, 0x96, 0x93, 0xcb, 0x8c, 0x3c, 0xf4, 0xfc, 0x02, 0x7f,
+ 0x59, 0x79, 0x81, 0xf6, 0x3e, 0x55, 0x44, 0x16, 0xb7, 0xb1, 0x7a, 0xa8,
+ 0x08, 0x2b, 0x97, 0xfc, 0xef, 0x26, 0x09, 0x51, 0xf0, 0x77, 0x0d, 0x93,
+ 0xea, 0x19, 0xff, 0x55, 0x6d, 0x7a, 0x25, 0x35, 0xfd, 0x3c, 0x3b, 0xc4,
+ 0xac, 0x55, 0xf3, 0xbd, 0x99, 0xb0, 0xbf, 0x01, 0xac, 0x34, 0xb3, 0xad,
+ 0x9d, 0xcf, 0xa5, 0x15, 0x3d, 0x14, 0xf5, 0x31, 0xbb, 0x8c, 0xa3, 0xa4,
+ 0x86, 0xe1, 0xbb, 0x14, 0x2a, 0xda, 0x4b, 0x57, 0x45, 0x74, 0xbf, 0x51,
+ 0xfe, 0xe3, 0xa4, 0xff, 0xb8, 0xab, 0xcb, 0xbc, 0x56, 0x61, 0x3c, 0x11,
+ 0xf5, 0x15, 0xb1, 0xa0, 0xb0, 0x40, 0xf1, 0x88, 0xbc, 0x53, 0xe2, 0x6a,
+ 0x4d, 0x67, 0xbb, 0x6d, 0x22, 0xc5, 0x32, 0xcd, 0xd2, 0xad, 0xba, 0x88,
+ 0x3c, 0x7a, 0x22, 0xcb, 0x0b, 0xbf, 0xfd, 0xe7, 0xb4, 0xe9, 0xd8, 0x95,
+ 0xbb, 0xfc, 0xef, 0x23, 0xa3, 0x57, 0xa4, 0x5c, 0x78, 0xc6, 0xd2, 0x17,
+ 0x39, 0x46, 0x20, 0x0a, 0xe0, 0xa3, 0xcc, 0x0f, 0x57, 0x4d, 0xa1, 0xa9,
+ 0xd6, 0x4c, 0x92, 0x84, 0xb8, 0x7f, 0xf5, 0x74, 0xb5, 0xe1, 0x56, 0xb2,
+ 0xe4, 0x7a, 0xc0, 0xe1, 0x8f, 0xf2, 0x60, 0x95, 0x4e, 0x88, 0xb3, 0x93,
+ 0x35, 0xd0, 0x24, 0x21, 0x01, 0xcd, 0x11, 0xfa, 0xee, 0x24, 0x2b, 0x1d,
+ 0xe4, 0x63, 0x6f, 0x26, 0x2a, 0x87, 0xa0, 0x72, 0x86, 0x43, 0x9f, 0x52,
+ 0x32, 0xee, 0x9d, 0xf6, 0x37, 0x0b, 0xec, 0x80, 0xb9, 0xc4, 0xc4, 0x02,
+ 0x98, 0x0c, 0xb2, 0x79, 0x66, 0xc1, 0xf6, 0x70, 0x0e, 0xf4, 0x41, 0x9b,
+ 0x3d, 0xe9, 0x94, 0xb9, 0xda, 0x75, 0x01, 0x78, 0x31, 0x77, 0xfc, 0xf9,
+ 0x7a, 0x18, 0xba, 0xc8, 0xac, 0x0b, 0xb0, 0xee, 0xf8, 0x77, 0xa5, 0x33,
+ 0xc2, 0xaf, 0xa0, 0x39, 0xa8, 0x96, 0xc4, 0x1f, 0x40, 0x31, 0x04, 0x5b,
+ 0x0f, 0x9b, 0x2b, 0x96, 0xf2, 0x82, 0x0d, 0xf9, 0x4f, 0x8e, 0xcc, 0xeb,
+ 0x27, 0xac, 0x68, 0x84, 0xba, 0x34, 0x0b, 0xd8, 0x9a, 0xe7, 0x35, 0x6c,
+ 0x19, 0xb5, 0x49, 0xd0, 0x2a, 0xf8, 0x44, 0xa6, 0xdd, 0x80, 0x75, 0xc9,
+ 0x56, 0xaf, 0xd2, 0x28, 0x41, 0x58, 0x0e, 0x76, 0xdc, 0x42, 0xd2, 0x77,
+ 0x0c, 0x15, 0x00, 0x04, 0x24, 0x7f, 0x7a, 0x13, 0x53, 0x42, 0xaa, 0x93,
+ 0x32, 0x9a, 0x7c, 0xc9, 0x44, 0x7b, 0x85, 0xa9, 0xff, 0x2a, 0xe8, 0x86,
+ 0x51, 0xc0, 0x06, 0xa4, 0xc9, 0xff, 0x1e, 0x05, 0xc0, 0x13, 0x8b, 0xf8,
+ 0x9a, 0xfc, 0xea, 0xba, 0xdf, 0x82, 0x56, 0x41, 0x96, 0x6f, 0x67, 0x7e,
+ 0x7c, 0xe1, 0x4e, 0xae, 0xa6, 0x83, 0x52, 0x36, 0x7e, 0x9e, 0xf4, 0xfd,
+ 0xe0, 0xcf, 0x0c, 0xbb, 0x52, 0x55, 0xc2, 0xc8, 0xd7, 0x39, 0x71, 0xff,
+ 0x17, 0x45, 0x77, 0xaa, 0xd2, 0x1a, 0xfe, 0x66, 0x88, 0xc0, 0xf1, 0xcd,
+ 0x2d, 0xe5, 0xe2, 0x19, 0x20, 0x20, 0x47, 0x4b, 0x87, 0x8f, 0x68, 0xce,
+ 0x31, 0x74, 0x63, 0x5e, 0xce, 0x08, 0x09, 0x28, 0x35, 0xc4, 0x1e, 0x87,
+ 0xc2, 0xc5, 0x6c, 0xaf, 0x69, 0x9e, 0xb3, 0x6f, 0xf0, 0x98, 0x2b, 0x34,
+ 0xf2, 0xe2, 0xbc, 0x0e, 0xc4, 0xa2, 0xdd, 0x28, 0xb7, 0x9d, 0xf1, 0xa9,
+ 0xc7, 0x64, 0x84, 0x58, 0x0e, 0xa5, 0x32, 0x1f, 0xab, 0x44, 0xfd, 0xf3,
+ 0xe8, 0x2e, 0xf5, 0x35, 0x96, 0xdf, 0x5d, 0xc8, 0x46, 0xa2, 0xdd, 0x20,
+ 0xa6, 0x8b, 0x24, 0x0e, 0xa7, 0x74, 0x49, 0x4c, 0xb6, 0x36, 0x4b, 0xf1,
+ 0xea, 0x7c, 0xf3, 0x10, 0x2a, 0xde, 0xe3, 0x9b, 0x27, 0xf1, 0x6c, 0x83,
+ 0x74, 0xc2, 0xb4, 0xb1, 0xec, 0xfa, 0x4c, 0x75, 0x41, 0x5b, 0x64, 0x3d,
+ 0x1e, 0x1e, 0xbc, 0xc5, 0x25, 0x0f, 0x72, 0x9c, 0x11, 0x13, 0xe7, 0xa2,
+ 0x44, 0x23, 0x86, 0xc8, 0x32, 0xe0, 0x6b, 0x53, 0xdd, 0xcd, 0x30, 0xb7,
+ 0xc1, 0xd1, 0x10, 0xe8, 0xe0, 0x60, 0xcb, 0x1e, 0x24, 0xab, 0xeb, 0x97,
+ 0x4b, 0x51, 0xa5, 0x3a, 0x8b, 0x76, 0x2c, 0x8a, 0xec, 0x35, 0xb4, 0xf8,
+ 0x1d, 0x9c, 0x51, 0x40, 0xc7, 0x0e, 0x74, 0x21, 0x73, 0xd8, 0x6f, 0xc5,
+ 0x89, 0x15, 0xd0, 0x32, 0x9b, 0xfb, 0x5b, 0xe8, 0xc7, 0xc9, 0x6e, 0x61,
+ 0x1c, 0x6e, 0x3e, 0xb6, 0xc3, 0xcd, 0x02, 0xda, 0x5c, 0x36, 0xe2, 0x0e,
+ 0xa0, 0xd3, 0xd6, 0x1c, 0x95, 0x1f, 0x73, 0xec, 0xdb, 0x07, 0x11, 0x97,
+ 0x73, 0x1e, 0xf9, 0xa9, 0x17, 0x75, 0x19, 0x43, 0x43, 0x4b, 0x7e, 0x5d,
+ 0xf7, 0x22, 0xa8, 0xf8, 0xb4, 0x62, 0xe8, 0x9f, 0x69, 0x61, 0x50, 0xf6,
+ 0x40, 0x22, 0x95, 0x4b, 0x11, 0x57, 0x25, 0x7b, 0xd7, 0x53, 0x10, 0x92,
+ 0xc2, 0x37, 0x9f, 0xd6, 0x4c, 0x03, 0x65, 0x0a, 0x42, 0x6b, 0x4d, 0x05,
+ 0x92, 0xa9, 0xba, 0x3e, 0x3b, 0xac, 0xd0, 0x67, 0x71, 0xe6, 0xf1, 0xb0,
+ 0x35, 0x6b, 0x30, 0x7f, 0xe3, 0x90, 0xe8, 0x1f, 0x3d, 0x17, 0x1d, 0xef,
+ 0xc7, 0xac, 0x2b, 0x3c, 0x73, 0x36, 0x74, 0x69, 0x7b, 0x11, 0x4a, 0xcd,
+ 0xdb, 0x12, 0x63, 0x3e, 0xe6, 0x3d, 0xa5, 0x13, 0x05, 0x93, 0xf8, 0x91,
+ 0x15, 0x6c, 0xbb, 0xd2, 0x61, 0x30, 0x0d, 0x0d, 0x79, 0x0a, 0x1a, 0x71,
+ 0xae, 0xf2, 0x4a, 0x83, 0xb2, 0x4e, 0xb5, 0x83, 0xab, 0xb7, 0x3d, 0xce,
+ 0xd0, 0xa7, 0x43, 0x1c, 0x67, 0x5c, 0x14, 0xd1, 0x31, 0x7f, 0xb2, 0x31,
+ 0x8f, 0xb3, 0xe5, 0x2c, 0x1b, 0x52, 0xf6, 0xac, 0xc8, 0xb2, 0x84, 0x4b,
+ 0xe2, 0x6f, 0x01, 0xb7, 0x96, 0x5f, 0xd9, 0xa3, 0xf4, 0x11, 0xdf, 0xb6,
+ 0x7e, 0xd9, 0xb7, 0x10, 0x9a, 0xf6, 0x04, 0x0d, 0x7f, 0x18, 0x82, 0x56,
+ 0xa6, 0x24, 0xa4, 0xfb, 0x26, 0xff, 0xa3, 0x39, 0xd1, 0xbd, 0xc9, 0x0a,
+ 0x6d, 0x1d, 0x9e, 0x1d, 0x2c, 0x75, 0xa2, 0x37, 0x92, 0x5e, 0x68, 0xb9,
+ 0x80, 0x4d, 0x9f, 0xc0, 0x3c, 0x86, 0xc9, 0xed, 0x46, 0xaf, 0xd4, 0x41,
+ 0xe2, 0xb4, 0xdf, 0x50, 0x94, 0x4f, 0x7a, 0xf2, 0xf9, 0x66, 0xf1, 0x76,
+ 0xca, 0xc9, 0x28, 0xf1, 0xb0, 0x8d, 0x8b, 0x90, 0xce, 0x8b, 0x91, 0xda,
+ 0xe2, 0x88, 0x53, 0xd6, 0x5a, 0x44, 0x5b, 0x49, 0x67, 0x21, 0x9a, 0x86,
+ 0xf3, 0x8f, 0xdf, 0xe2, 0xdf, 0x79, 0x88, 0xde, 0xc7, 0x53, 0x5a, 0x31,
+ 0x53, 0x7b, 0x0d, 0x54, 0x9b, 0x21, 0xc1, 0xc0, 0x49, 0xd0, 0x4b, 0xc8,
+ 0x14, 0x05, 0x95, 0xb5, 0x1e, 0xd7, 0xe4, 0xf7, 0xaf, 0x1c, 0x16, 0x08,
+ 0x08, 0xab, 0xce, 0x84, 0xb1, 0x9f, 0xcc, 0x6f, 0xef, 0x5f, 0x30, 0x70,
+ 0x84, 0x6c, 0xfd, 0xe1, 0xa7, 0x1c, 0xae, 0x99, 0x00, 0x15, 0xe7, 0x0f,
+ 0x5f, 0x9a, 0x08, 0x51, 0x5e, 0x02, 0xc6, 0xde, 0xd4, 0x4b, 0xe0, 0xf4,
+ 0xf8, 0x3c, 0x89, 0x71, 0x7a, 0x9f, 0x23, 0x16, 0x86, 0xd5, 0xe7, 0x5c,
+ 0x9d, 0xde, 0x98, 0xdd, 0x5c, 0x54, 0x94, 0x96, 0x18, 0x05, 0x96, 0xa0,
+ 0x1d, 0x34, 0x13, 0xef, 0xc6, 0x92, 0x48, 0x7a, 0x79, 0x85, 0xe1, 0x32,
+ 0xf8, 0x84, 0xc1, 0xca, 0x3e, 0xad, 0x6b, 0xf6, 0x45, 0x89, 0x3f, 0x4d,
+ 0x25, 0xf9, 0x24, 0x03, 0xad, 0x85, 0xac, 0x75, 0x47, 0x44, 0xff, 0x33,
+ 0x24, 0xcd, 0x01, 0xcc, 0xe4, 0xd3, 0x08, 0xde, 0x68, 0x56, 0xa2, 0x0e,
+ 0x22, 0xab, 0x83, 0xe8, 0x03, 0x0e, 0x5e, 0xee, 0xb9, 0x2e, 0x24, 0x2f,
+ 0x55, 0xa1, 0x1f, 0x83, 0xb0, 0x4a, 0x4c, 0xaf, 0x4b, 0x2c, 0x71, 0x0c,
+ 0x2c, 0x84, 0xac, 0xa2, 0xa0, 0xfc, 0x83, 0x6f, 0x24, 0xa8, 0x04, 0x72,
+ 0xf0, 0x6b, 0xb8, 0x84, 0xfa, 0x82, 0x2c, 0x29, 0x9b, 0xd6, 0xbb, 0x47,
+ 0x2c, 0x19, 0x74, 0x82, 0x01, 0x4b, 0x3e, 0x7b, 0x32, 0x7a, 0xb6, 0x6f,
+ 0x2d, 0x58, 0x16, 0xd0, 0x45, 0x3e, 0x98, 0xce, 0x86, 0x9d, 0x25, 0xab,
+ 0x42, 0x94, 0x43, 0x91, 0x73, 0xef, 0x4d, 0x75, 0xf3, 0x36, 0x8a, 0x8f,
+ 0xcc, 0xa7, 0x19, 0xe0, 0x5c, 0x25, 0xda, 0x91, 0x1c, 0x59, 0x84, 0x65,
+ 0x28, 0xc2, 0x01, 0xd1, 0xaa, 0x79, 0x1f, 0x00, 0x6e, 0x21, 0x11, 0x73,
+ 0x95, 0xbe, 0xcd, 0x72, 0x38, 0xf0, 0xc2, 0xf9, 0xbf, 0x0f, 0xbb, 0x1e,
+ 0x05, 0x9e, 0x00, 0x9c, 0x4d, 0x4b, 0xcf, 0x36, 0xc2, 0x9b, 0xcd, 0xa0,
+ 0xb7, 0x4b, 0x88, 0x15, 0x76, 0x18, 0xcc, 0x76, 0xe7, 0x11, 0x27, 0x04,
+ 0xea, 0x43, 0x92, 0xd6, 0xe6, 0x7d, 0x11, 0xb3, 0x21, 0x3d, 0xff, 0xea,
+ 0x7d, 0x22, 0x22, 0x6c, 0x18, 0x96, 0x54, 0x35, 0x80, 0xd1, 0x0c, 0x29,
+ 0x72, 0x96, 0x8e, 0x26, 0xe0, 0x12, 0x5f, 0xc1, 0x06, 0x1e, 0xaf, 0xf1,
+ 0x42, 0x1f, 0x09, 0xad, 0x3d, 0x09, 0x06, 0x1b, 0x28, 0x3e, 0xaa, 0x27,
+ 0x21, 0xf6, 0x20, 0x78, 0x9c, 0x69, 0x92, 0x1f, 0x78, 0x75, 0x1d, 0x66,
+ 0x89, 0x25, 0x22, 0x35, 0x5f, 0x05, 0xf6, 0xb2, 0x98, 0xa0, 0xd1, 0xd8,
+ 0x4b, 0xa8, 0x98, 0x29, 0x90, 0x97, 0x1f, 0xfc, 0xc3, 0xd6, 0xb9, 0x5d,
+ 0x8f, 0xa2, 0x87, 0xcc, 0xfe, 0x80, 0xcf, 0x67, 0xe7, 0x12, 0xae, 0xd6,
+ 0x57, 0x0d, 0x59, 0xfe, 0xaf, 0x8d, 0x40, 0x04, 0x1a, 0xcb, 0x1d, 0x01,
+ 0x48, 0x83, 0xfa, 0x0d, 0xbe, 0xc5, 0xc4, 0xcf, 0xb3, 0xf2, 0x7e, 0x7b,
+ 0x18, 0x0d, 0x7c, 0x8c, 0x2e, 0xbe, 0x54, 0xe4, 0x02, 0x32, 0x0d, 0x0f,
+ 0x9c, 0x34, 0xb0, 0xff, 0x5d, 0x6b, 0x75, 0x52, 0xa7, 0x58, 0xcc, 0x96,
+ 0x29, 0x11, 0x13, 0x2e, 0x97, 0x74, 0x4e, 0x7e, 0x52, 0xe7, 0x5d, 0x8c,
+ 0xf9, 0x6a, 0x52, 0xef, 0xbc, 0x7d, 0x41, 0x28, 0xef, 0x2c, 0x09, 0xf3,
+ 0xb6, 0x51, 0xcd, 0xcc, 0x6b, 0x41, 0x1e, 0x0b, 0x91, 0xda, 0xbd, 0x08,
+ 0x7f, 0x82, 0x59, 0x01, 0xfc, 0x1d, 0xe9, 0xb1, 0x6d, 0xbb, 0x5d, 0x90,
+ 0xa1, 0x60, 0xde, 0xa7, 0xd5, 0x9d, 0x9e, 0x8f, 0x24, 0xd7, 0x07, 0x0d,
+ 0xbc, 0xb4, 0x09, 0xfd, 0x40, 0xdd, 0x9f, 0x5f, 0xd3, 0xb8, 0x64, 0xb1,
+ 0x9a, 0x36, 0xe5, 0xc5, 0x2d, 0x8f, 0x96, 0xf3, 0x1e, 0xdc, 0x14, 0x17,
+ 0xa4, 0x79, 0x23, 0xe5, 0x38, 0xae, 0x1f, 0x4f, 0xa5, 0x77, 0xa9, 0x2b,
+ 0x85, 0x8d, 0x64, 0x1b, 0x65, 0x9b, 0x47, 0x3b, 0xb4, 0x65, 0xf8, 0x18,
+ 0xac, 0xab, 0xd3, 0x7c, 0x02, 0x8b, 0x77, 0x4d, 0x1e, 0x19, 0xcb, 0x28,
+ 0xb3, 0x2d, 0xd6, 0x9f, 0x5c, 0x54, 0xda, 0xb0, 0x69, 0xec, 0x90, 0x55,
+ 0x65, 0x93, 0x91, 0x4f, 0x00, 0xf2, 0x98, 0x66, 0xa6, 0x38, 0x8f, 0x9f,
+ 0xb7, 0x6a, 0xaf, 0x99, 0x88, 0x90, 0x14, 0xee, 0xef, 0x1f, 0x20, 0x45,
+ 0x68, 0xf3, 0x46, 0x00, 0x32, 0x8e, 0x0f, 0xc4, 0x7f, 0xee, 0xd8, 0x3d,
+ 0x09, 0xd5, 0x3e, 0xbd, 0xc4, 0xa0, 0x5b, 0x63, 0x39, 0x56, 0x8f, 0x6b,
+ 0x69, 0xe8, 0xbf, 0x6b, 0x59, 0xc3, 0x4f, 0xda, 0x52, 0x63, 0x86, 0xf2,
+ 0x5b, 0x4f, 0xea, 0x03, 0xdf, 0xd6, 0x53, 0x3f, 0x08, 0x4f, 0xe4, 0xd6,
+ 0xcc, 0x24, 0x26, 0x7e, 0x8e, 0x81, 0x88, 0xdd, 0xfc, 0xbb, 0xb9, 0xef,
+ 0x21, 0xa4, 0xf1, 0xef, 0xa8, 0x93, 0xd3, 0x09, 0x9e, 0x71, 0x32, 0xdc,
+ 0x2b, 0x6a, 0x11, 0xac, 0x01, 0xb7, 0xf3, 0x18, 0x25, 0xe3, 0x44, 0x3e,
+ 0xf8, 0xa5, 0xfd, 0x65, 0xaa, 0xb1, 0xe1, 0x31, 0xaa, 0x06, 0x59, 0x69,
+ 0xaf, 0x3a, 0x9b, 0x81, 0x1b, 0xdc, 0xdf, 0x1f, 0x71, 0x9a, 0x62, 0x0e,
+ 0x01, 0xb8, 0xcc, 0xa3, 0x2a, 0x20, 0x3e, 0x56, 0xc8, 0xd0, 0x57, 0x87,
+ 0xc6, 0x1b, 0x4a, 0xa9, 0x6e, 0x24, 0x74, 0x5d, 0xbd, 0x71, 0xa0, 0x05,
+ 0xd4, 0x3f, 0xba, 0x7b, 0xc9, 0x3d, 0xa4, 0x78, 0xdd, 0xbc, 0xdf, 0xee,
+ 0x1c, 0xad, 0x80, 0x40, 0x0e, 0xcb, 0x2d, 0xf7, 0x47, 0xd4, 0x83, 0x29,
+ 0x78, 0x1e, 0x0a, 0x2a, 0xb0, 0xf6, 0xe8, 0x85, 0x61, 0x72, 0x52, 0xe3,
+ 0x87, 0x15, 0xcf, 0xa5, 0xaf, 0x26, 0x97, 0x1a, 0x8b, 0x13, 0xbb, 0x7a,
+ 0x4e, 0xde, 0xc7, 0x26, 0x8c, 0xe0, 0x1b, 0x34, 0xad, 0x86, 0xc5, 0x97,
+ 0xef, 0xbd, 0xd3, 0x0b, 0x5a, 0xa0, 0xd9, 0x0b, 0xce, 0xd7, 0x8c, 0x65,
+ 0xb1, 0xf3, 0x4f, 0xc4, 0x63, 0x15, 0x47, 0x38, 0x6e, 0x9c, 0x8a, 0x84,
+ 0x30, 0x98, 0x93, 0xe3, 0xb1, 0xb0, 0x46, 0x9b, 0xa5, 0xc7, 0xa8, 0x72,
+ 0x83, 0xe8, 0xcd, 0x25, 0x1f, 0x41, 0xcc, 0xaa, 0x75, 0x0e, 0x3d, 0xc4,
+ 0x43, 0xbf, 0xb4, 0x32, 0x18, 0x90, 0x48, 0x38, 0x94, 0x7d, 0xae, 0xdb,
+ 0x5a, 0xb9, 0x39, 0x75, 0x41, 0x62, 0x73, 0xc4, 0x44, 0x26, 0xe6, 0x93,
+ 0x9c, 0xb4, 0x69, 0xda, 0x0e, 0x81, 0x7a, 0x3a, 0x28, 0x94, 0xea, 0x46,
+ 0x48, 0x8f, 0x83, 0x27, 0x75, 0x90, 0x19, 0x02, 0x32, 0x93, 0xe1, 0xc0,
+ 0x18, 0x80, 0x84, 0xa3, 0xd0, 0xf3, 0x8b, 0x4f, 0x27, 0x1e, 0x29, 0x17,
+ 0xd4, 0xc8, 0x7e, 0x6f, 0xb7, 0xc6, 0x4f, 0xb3, 0x91, 0xd3, 0x44, 0xb5,
+ 0xa6, 0xa6, 0x52, 0x39, 0x0a, 0x3a, 0xe5, 0xdf, 0xff, 0x4f, 0x15, 0x79,
+ 0x97, 0x03, 0xe5, 0xfd, 0xd6, 0x80, 0x6d, 0xfc, 0x66, 0x0b, 0x53, 0xde,
+ 0xdb, 0x4e, 0x85, 0x3b, 0x0e, 0xf8, 0xcb, 0x17, 0xf0, 0x91, 0xc6, 0xb5,
+ 0xfe, 0x09, 0xd0, 0xb7, 0x37, 0xbd, 0xdf, 0x53, 0x66, 0x90, 0x74, 0xe9,
+ 0x37, 0x96, 0xde, 0x98, 0x94, 0xf5, 0xc8, 0x94, 0xb5, 0xba, 0x70, 0xfe,
+ 0x89, 0xa2, 0x1e, 0x49, 0xb3, 0x8c, 0xd8, 0x9f, 0x0b, 0x8e, 0x6e, 0x5d,
+ 0x22, 0x81, 0x64, 0xe0, 0x33, 0xb8, 0xb0, 0x9b, 0x85, 0x78, 0x6a, 0xbc,
+ 0x99, 0xb4, 0xdc, 0x19, 0x00, 0x3b, 0x5d, 0x41, 0x54, 0x86, 0x98, 0x44,
+ 0xd7, 0x3a, 0x8f, 0xf9, 0x16, 0x6e, 0x70, 0x50, 0xc0, 0x01, 0xfe, 0xe1,
+ 0x08, 0x01, 0x04, 0xbe, 0x55, 0xab, 0xcf, 0x29, 0xa5, 0xb5, 0xfa, 0x90,
+ 0x2c, 0x98, 0xf0, 0xd7, 0xd5, 0xaa, 0x07, 0xd4, 0x19, 0xf4, 0xd2, 0x7c,
+ 0xbb, 0xc3, 0x8a, 0x6b, 0x2c, 0x89, 0xf0, 0x10, 0xa6, 0xb6, 0x51, 0xc9,
+ 0x92, 0x23, 0xa1, 0x22, 0x93, 0x97, 0xce, 0xd2, 0xd5, 0xb6, 0x3e, 0x1b,
+ 0x4a, 0x80, 0x6f, 0xac, 0x11, 0xca, 0x14, 0x73, 0xb5, 0xcb, 0xd9, 0x50,
+ 0xe3, 0x42, 0x6e, 0x8e, 0xe3, 0x3a, 0x7c, 0x30, 0x3e, 0x83, 0xec, 0x68,
+ 0x2c, 0x22, 0x16, 0x07, 0xaa, 0xc1, 0x72, 0xe1, 0xbd, 0x07, 0x58, 0xfe,
+ 0xc3, 0xbc, 0x9e, 0xf0, 0x74, 0xc6, 0x89, 0x85, 0x9e, 0x85, 0xc4, 0xec,
+ 0x99, 0xf7, 0x3c, 0xc9, 0x94, 0xdf, 0xb5, 0xc2, 0xf2, 0xb5, 0x6b, 0xa9,
+ 0x33, 0x0b, 0x01, 0x92, 0xfd, 0x20, 0x35, 0xa0, 0x37, 0x01, 0x85, 0xae,
+ 0x8d, 0xa1, 0x3e, 0x13, 0x12, 0x65, 0xa8, 0x13, 0xe6, 0x08, 0xfa, 0xbf,
+ 0x33, 0xf5, 0xc1, 0x6d, 0xf8, 0x33, 0x22, 0x79, 0xcb, 0x40, 0xb4, 0xe5,
+ 0xfe, 0xfc, 0xbe, 0x39, 0x25, 0xf7, 0x40, 0x96, 0xa7, 0xfd, 0x05, 0x97,
+ 0x93, 0x82, 0xc1, 0xa4, 0xa8, 0x74, 0xdd, 0xbb, 0xb4, 0x6d, 0x8a, 0x21,
+ 0xc9, 0xe8, 0x7e, 0x4b, 0x2c, 0x46, 0x06, 0x49, 0xa3, 0x1b, 0xbb, 0x85,
+ 0x66, 0x35, 0x61, 0x50, 0x2e, 0x45, 0x89, 0x0d, 0x32, 0xeb, 0xf3, 0xc1,
+ 0xec, 0x20, 0xc7, 0xf5, 0xce, 0x56, 0x35, 0x3d, 0x00, 0x48, 0xff, 0xa6,
+ 0x93, 0xc1, 0x37, 0xdf, 0x8a, 0xb0, 0x14, 0x39, 0x52, 0xfc, 0x62, 0x0e,
+ 0xc4, 0xf9, 0xc6, 0xe3, 0xb6, 0xa3, 0x7b, 0xb0, 0xf6, 0x71, 0x5c, 0x99,
+ 0x82, 0xd8, 0x51, 0x3c, 0x6d, 0x4e, 0x1f, 0x21, 0xd5, 0xd4, 0x39, 0x42,
+ 0x5a, 0xea, 0xcb, 0x13, 0x86, 0xfc, 0x78, 0xca, 0x3d, 0xaa, 0x42, 0x3c,
+ 0xdb, 0x41, 0x72, 0x07, 0x36, 0x45, 0xe3, 0xb1, 0xc0, 0x79, 0xda, 0x72,
+ 0x4f, 0xff, 0x6b, 0x72, 0x59, 0xee, 0xe0, 0x3b, 0x98, 0xf1, 0x0f, 0x9a,
+ 0x3d, 0x6f, 0x7b, 0x30, 0x36, 0x99, 0x11, 0xf3, 0x03, 0xa8, 0xcd, 0xaf,
+ 0x9c, 0xe4, 0x8b, 0x40, 0xc8, 0x3c, 0xc0, 0xbf, 0x17, 0x80, 0xbf, 0xc5,
+ 0x4b, 0x1f, 0xf9, 0x46, 0x78, 0xd0, 0xe6, 0x72, 0x73, 0xd6, 0xee, 0x27,
+ 0x47, 0x60, 0xe5, 0xe9, 0x09, 0x14, 0xfd, 0x56, 0xad, 0xf8, 0x7e, 0x34,
+ 0x98, 0xd1, 0x1e, 0xce, 0xd5, 0x16, 0x80, 0x3f, 0x47, 0xc4, 0x6e, 0xe3,
+ 0x7c, 0x63, 0x55, 0x20, 0xcf, 0x66, 0x42, 0x3d, 0xbd, 0x88, 0x95, 0x0a,
+ 0x2b, 0xd9, 0x63, 0xd2, 0x7b, 0xdd, 0x39, 0xb2, 0x79, 0xfd, 0xe8, 0xa8,
+ 0x94, 0x27, 0x3e, 0x7a, 0x50, 0x4b, 0x6f, 0x37, 0x6f, 0x0b, 0x11, 0x5c,
+ 0xb5, 0xfa, 0x23, 0xf2, 0xda, 0xce, 0x62, 0x4e, 0x1e, 0xb5, 0xdb, 0x16,
+ 0x37, 0x1e, 0x85, 0xde, 0x19, 0x05, 0x61, 0xcd, 0x8b, 0x77, 0xc1, 0x44,
+ 0x57, 0x34, 0x4e, 0x61, 0x79, 0x2b, 0xd7, 0xe7, 0x63, 0x57, 0x1d, 0x46,
+ 0x6c, 0x82, 0x9e, 0xc8, 0xb4, 0x84, 0xa7, 0x9f, 0x1c, 0x30, 0x7e, 0x0b,
+ 0x96, 0xd3, 0x1e, 0x50, 0x55, 0xea, 0x95, 0x02, 0xe6, 0xc1, 0x44, 0xb9,
+ 0x2b, 0x9d, 0x47, 0x77, 0x0a, 0xf2, 0x42, 0x0d, 0x33, 0x07, 0x12, 0x96,
+ 0x2d, 0x6f, 0x60, 0xa4, 0xcc, 0x23, 0x73, 0x53, 0x5f, 0xc8, 0x42, 0xba,
+ 0xa8, 0x73, 0x68, 0x2e, 0xef, 0x17, 0xd9, 0x99, 0x11, 0x8f, 0x0b, 0x48,
+ 0xe0, 0x6d, 0x4c, 0xea, 0xc4, 0x68, 0x57, 0x23, 0x2b, 0x47, 0x2d, 0x1a,
+ 0xe8, 0x00, 0x0f, 0x66, 0xf4, 0xa9, 0x1a, 0x65, 0xfa, 0x58, 0xc2, 0xb0,
+ 0x84, 0x2d, 0x47, 0xaa, 0x66, 0xa5, 0x2b, 0x98, 0xe8, 0x0c, 0x12, 0xc8,
+ 0xa4, 0x7f, 0x02, 0xca, 0x30, 0x2c, 0x7f, 0x4e, 0xf2, 0xd5, 0x1a, 0x1c,
+ 0xe4, 0xa6, 0x96, 0xff, 0x0d, 0x18, 0x12, 0x51, 0x92, 0x87, 0xb1, 0x0b,
+ 0x15, 0x04, 0x25, 0xd4, 0xf5, 0x47, 0xfd, 0xb9, 0x3c, 0x97, 0xb5, 0xd1,
+ 0xa4, 0x04, 0x51, 0xca, 0x6a, 0x9c, 0xf9, 0x07, 0x42, 0xf3, 0xa7, 0x48,
+ 0xf6, 0x62, 0x12, 0xe3, 0x21, 0x61, 0x78, 0x89, 0xd5, 0xbf, 0x00, 0xea,
+ 0xe3, 0x04, 0xb6, 0xd4, 0xbf, 0xe5, 0x92, 0xed, 0xa7, 0x3a, 0x47, 0xc2,
+ 0x77, 0x2d, 0x30, 0x60, 0x50, 0x69, 0xb3, 0x0f, 0x45, 0x32, 0x28, 0x25,
+ 0x59, 0x62, 0xf5, 0x36, 0x8c, 0x0b, 0xc1, 0xfb, 0xe6, 0x4b, 0x65, 0x90,
+ 0x3c, 0x25, 0x8d, 0xb8, 0x51, 0xd3, 0x94, 0xbe, 0xf8, 0x3f, 0xa5, 0xce,
+ 0xb7, 0x2f, 0x4e, 0xbe, 0x95, 0xf0, 0xb7, 0x79, 0xa8, 0xcb, 0xb3, 0x3c,
+ 0x31, 0xf9, 0xe7, 0x8d, 0x00, 0xd8, 0xc7, 0xf6, 0xca, 0x1d, 0xe1, 0x0b,
+ 0xa5, 0x92, 0x38, 0x80, 0xd2, 0x9f, 0x89, 0x6c, 0x66, 0x68, 0x3a, 0x03,
+ 0x11, 0x44, 0x41, 0x16, 0xc9, 0xa5, 0x13, 0x87, 0xcb, 0x13, 0x43, 0x4b,
+ 0x0c, 0x85, 0x0e, 0x7a, 0x01, 0x2a, 0xab, 0xc1, 0xf7, 0x6e, 0xe6, 0x61,
+ 0x84, 0xdb, 0xb4, 0x27, 0x6f, 0xc2, 0x90, 0x98, 0x3a, 0x12, 0xc8, 0x80,
+ 0x80, 0x64, 0x63, 0x1a, 0x14, 0x95, 0x8a, 0x68, 0x9d, 0xdb, 0x80, 0x8f,
+ 0xd6, 0x96, 0x51, 0xb4, 0x4d, 0x93, 0x10, 0xe2, 0xda, 0xec, 0x4a, 0xd9,
+ 0xb4, 0x8d, 0x80, 0x6f, 0xf1, 0x6c, 0x90, 0x46, 0x0c, 0x6b, 0xc2, 0x7a,
+ 0x5c, 0xd7, 0xa9, 0x06, 0x39, 0x40, 0xff, 0xfa, 0x23, 0xe5, 0x97, 0x38,
+ 0x48, 0xba, 0x82, 0x6d, 0xc7, 0x7d, 0xa4, 0x17, 0xe7, 0x61, 0x18, 0xf1,
+ 0x99, 0xa7, 0xc1, 0x2f, 0x4f, 0x5a, 0x88, 0xfd, 0x74, 0xe2, 0xaa, 0xa2,
+ 0xa3, 0x62, 0xf0, 0x62, 0x36, 0x2a, 0x9a, 0x76, 0x92, 0xed, 0x86, 0xc0,
+ 0x10, 0xc4, 0xe0, 0xa7, 0xa4, 0x2f, 0x5f, 0x8a, 0xbd, 0xfa, 0x10, 0x90,
+ 0x1b, 0xd4, 0xb1, 0xb6, 0xe7, 0x6a, 0xc2, 0xa6, 0xa3, 0xda, 0x66, 0xc9,
+ 0x5b, 0xbd, 0x83, 0x6c, 0xe8, 0xfe, 0x70, 0x8f, 0x55, 0x48, 0x8a, 0x81,
+ 0x6d, 0x2a, 0x7a, 0xfa, 0xcd, 0xd0, 0x08, 0x0c, 0xa7, 0x96, 0x33, 0xd3,
+ 0x46, 0x01, 0xe4, 0x3a, 0x05, 0x4f, 0xf3, 0x76, 0x1f, 0xcd, 0x66, 0xb7,
+ 0x2d, 0xda, 0x26, 0x84, 0x8c, 0x71, 0x36, 0x05, 0x5a, 0x21, 0xc8, 0xb0,
+ 0xc0, 0x34, 0x2e, 0xcd, 0x1d, 0xae, 0x47, 0x36, 0xfd, 0xbd, 0x9d, 0x51,
+ 0x5d, 0xb0, 0xfb, 0x0c, 0xff, 0x9c, 0xe1, 0xbc, 0xa1, 0x95, 0xa5, 0x5e,
+ 0xb0, 0x34, 0x1a, 0xdb, 0xce, 0x3c, 0x66, 0x86, 0x65, 0xf3, 0x9f, 0x86,
+ 0xb6, 0x54, 0x88, 0x4a, 0x07, 0x15, 0x51, 0x7c, 0x74, 0xea, 0x3b, 0x69,
+ 0xb5, 0x46, 0x7e, 0xa5, 0xed, 0x02, 0x81, 0x7c, 0x44, 0x3b, 0xab, 0xea,
+ 0x31, 0xbc, 0x3e, 0xe3, 0xf3, 0xef, 0xb1, 0xbe, 0xa6, 0x53, 0xec, 0xd5,
+ 0xc8, 0x95, 0x2a, 0x23, 0xb7, 0x3e, 0xb0, 0xa2, 0xa5, 0x77, 0x8c, 0x58,
+ 0xa8, 0x65, 0x4f, 0x83, 0x14, 0x79, 0xd6, 0x1c, 0xdd, 0xf5, 0x57, 0x45,
+ 0x79, 0xf5, 0x81, 0x4a, 0xe7, 0xec, 0x96, 0x10, 0x8c, 0x85, 0x2c, 0x51,
+ 0x09, 0x4d, 0x94, 0x7d, 0xd3, 0x14, 0x35, 0xc5, 0x62, 0x66, 0xb1, 0xf3,
+ 0x0e, 0x0f, 0x76, 0x80, 0x06, 0x58, 0x26, 0x67, 0xcb, 0x97, 0xfa, 0x97,
+ 0x3a, 0xf0, 0x8e, 0x02, 0x13, 0x07, 0x1f, 0xcc, 0x48, 0x85, 0x1b, 0xef,
+ 0x7c, 0xe2, 0x7d, 0xd7, 0xbd, 0xaf, 0xd2, 0x08, 0x42, 0xf5, 0x17, 0xcd,
+ 0xa9, 0xa9, 0xe4, 0x86, 0x50, 0x94, 0x23, 0xb4, 0xa0, 0x03, 0xd5, 0xb5,
+ 0x0f, 0x30, 0x78, 0x2d, 0x0e, 0xe4, 0xf9, 0xfb, 0xbb, 0x3d, 0x02, 0x1e,
+ 0x91, 0xdb, 0x1e, 0x19, 0xbf, 0x14, 0x95, 0x92, 0x45, 0x65, 0xd0, 0xf7,
+ 0x5e, 0x54, 0x85, 0x76, 0x0b, 0x3a, 0x18, 0x6e, 0x57, 0xb5, 0xe9, 0x87,
+ 0xef, 0x10, 0x9f, 0x08, 0x13, 0xde, 0x6a, 0xeb, 0x57, 0x76, 0x52, 0xd9,
+ 0x69, 0x5b, 0x30, 0x8a, 0xb3, 0xf0, 0x7f, 0x22, 0x53, 0xc4, 0xb9, 0x56,
+ 0xb5, 0x05, 0x41, 0xbf, 0x43, 0xcf, 0xbc, 0x39, 0xbc, 0x04, 0x13, 0x16,
+ 0xd6, 0xad, 0x5b, 0x24, 0x28, 0xc2, 0x51, 0xf0, 0x0b, 0x2c, 0x31, 0xfd,
+ 0x72, 0x99, 0xce, 0x3a, 0xd9, 0xa6, 0x96, 0x64, 0x32, 0xe2, 0x70, 0xcb,
+ 0xbc, 0xea, 0x07, 0x22, 0xd5, 0xd7, 0x14, 0x1a, 0x6f, 0xf6, 0x54, 0x4d,
+ 0x60, 0x45, 0x6f, 0x71, 0x8c, 0x73, 0x30, 0x7f, 0x75, 0x08, 0x41, 0x61,
+ 0x1f, 0x22, 0xaf, 0x3d, 0x3c, 0x9d, 0x62, 0x69, 0x1e, 0x58, 0x92, 0x39,
+ 0x22, 0x29, 0x56, 0xbc, 0x02, 0xc0, 0xeb, 0xba, 0xab, 0x7f, 0xed, 0x5a,
+ 0xfa, 0xcb, 0x6e, 0xea, 0x26, 0x97, 0x86, 0x08, 0x7c, 0x9b, 0xf5, 0xab,
+ 0xf1, 0x23, 0x58, 0xcf, 0xe0, 0xeb, 0x01, 0x6d, 0x8f, 0xf5, 0x5e, 0x8f,
+ 0x12, 0xa8, 0xbd, 0xe4, 0xbb, 0x0f, 0xa0, 0xf9, 0xbe, 0xbd, 0xd9, 0x24,
+ 0x88, 0x13, 0xa6, 0x5b, 0x5f, 0x2d, 0x1e, 0x13, 0xd5, 0x3a, 0xcc, 0x69,
+ 0x4a, 0x96, 0x5b, 0xf3, 0xb4, 0xde, 0xd4, 0x41, 0x62, 0x2e, 0x65, 0xdf,
+ 0x0d, 0xee, 0x06, 0xf3, 0x17, 0x07, 0x8d, 0x45, 0xfe, 0x57, 0xc8, 0x6c,
+ 0x8e, 0xfa, 0x9e, 0x24, 0x0d, 0x24, 0xfc, 0xff, 0x15, 0xba, 0x89, 0x9c,
+ 0x9c, 0x64, 0x06, 0x1f, 0xea, 0x7a, 0xf5, 0x9d, 0x41, 0x75, 0x15, 0x1f,
+ 0x58, 0x15, 0x7c, 0xbf, 0xa5, 0x2f, 0x88, 0xe4, 0x6c, 0x34, 0x08, 0xae,
+ 0x61, 0x05, 0x31, 0x2e, 0x36, 0xb6, 0x9c, 0xbb, 0x73, 0x61, 0x46, 0x84,
+ 0xee, 0x4c, 0x2b, 0xbd, 0x51, 0x61, 0x8d, 0x89, 0x0b, 0x7f, 0x2e, 0x42,
+ 0x9d, 0x2f, 0xf1, 0xd2, 0xae, 0x09, 0xd6, 0xf2, 0x59, 0x27, 0xe6, 0x64,
+ 0xfa, 0x54, 0xad, 0x83, 0x0f, 0xe0, 0x32, 0xc9, 0x13, 0xae, 0x75, 0x73,
+ 0xdf, 0x7a, 0x42, 0x47, 0xd8, 0xd2, 0x14, 0x56, 0x6c, 0x59, 0xe6, 0xd8,
+ 0x5b, 0x86, 0x82, 0x8e, 0xef, 0x5e, 0xaa, 0xa7, 0x99, 0x4e, 0x4d, 0x02,
+ 0x18, 0x13, 0x1e, 0xfa, 0x42, 0x11, 0x70, 0x55, 0x13, 0x97, 0x91, 0x20,
+ 0xf5, 0x98, 0xd3, 0xdf, 0xc2, 0x7b, 0x75, 0x74, 0x56, 0x77, 0x7c, 0x6b,
+ 0x9e, 0x8f, 0x55, 0x52, 0xad, 0xe8, 0x10, 0x02, 0x6a, 0xf7, 0xf5, 0xf9,
+ 0xc6, 0x3e, 0xd8, 0x28, 0x03, 0x33, 0xfd, 0xef, 0x3f, 0xb6, 0x40, 0x66,
+ 0x2d, 0xa6, 0x16, 0xbf, 0xee, 0x82, 0x13, 0x96, 0x0d, 0x46, 0xe8, 0x9a,
+ 0x9c, 0xdf, 0x98, 0x02, 0x58, 0xbb, 0x92, 0x55, 0x96, 0xb6, 0xde, 0x6e,
+ 0xaa, 0x49, 0xaf, 0x70, 0x84, 0x4d, 0xdc, 0x86, 0xe0, 0xd4, 0x60, 0x73,
+ 0xba, 0xcd, 0x84, 0x23, 0x43, 0xa2, 0x90, 0x68, 0xd7, 0x10, 0xc6, 0x88,
+ 0x5c, 0xea, 0xb3, 0x3b, 0x65, 0x45, 0x88, 0x05, 0x4a, 0x5f, 0x85, 0xbf,
+ 0xc1, 0xe9, 0xd1, 0x9f, 0xe8, 0x99, 0x47, 0xd3, 0x1c, 0x0d, 0x79, 0x49,
+ 0xe1, 0xb5, 0xe9, 0xcb, 0x24, 0x7b, 0xb4, 0x35, 0xd8, 0x35, 0x5c, 0xbf,
+ 0x58, 0xf1, 0xb9, 0x72, 0x55, 0xcb, 0x27, 0xc5, 0x43, 0xc9, 0x78, 0xce,
+ 0x49, 0x00, 0x2b, 0x9d, 0x55, 0x9d, 0xcf, 0xdd, 0x9c, 0xe7, 0xb8, 0xd8,
+ 0x91, 0x41, 0x74, 0xe8, 0x22, 0x65, 0xf1, 0x96, 0x41, 0x81, 0x6e, 0xad,
+ 0x66, 0x23, 0x71, 0x89, 0x65, 0x45, 0x9a, 0x1f, 0x1b, 0x43, 0x0f, 0xcd,
+ 0x4a, 0xd8, 0x6a, 0x2f, 0x48, 0x69, 0x50, 0x15, 0x6e, 0x67, 0x6b, 0x0a,
+ 0x0d, 0x29, 0x93, 0xe0, 0x1a, 0x68, 0x5f, 0x4c, 0xe2, 0x72, 0x59, 0x49,
+ 0x43, 0x49, 0x10, 0x71, 0x9c, 0xe0, 0x8d, 0x14, 0x8f, 0xac, 0xce, 0x1d,
+ 0xaf, 0xc7, 0x9c, 0x41, 0x9e, 0xb5, 0xe2, 0x61, 0x6b, 0x50, 0xbc, 0x83,
+ 0xb2, 0x4c, 0x05, 0x6e, 0x71, 0x6d, 0x63, 0xa5, 0x65, 0x87, 0xd5, 0x74,
+ 0x39, 0x16, 0xe9, 0xee, 0xc7, 0xc3, 0xec, 0x8c, 0x8c, 0x70, 0xb2, 0xa1,
+ 0x55, 0xee, 0xb3, 0xa3, 0x9f, 0x12, 0x02, 0x87, 0x30, 0xfc, 0x7e, 0x52,
+ 0xf8, 0x97, 0x82, 0x0a, 0x0f, 0x20, 0xd5, 0x8e, 0xfa, 0xe3, 0xa9, 0x17,
+ 0xa7, 0x9a, 0x65, 0x64, 0xbf, 0x66, 0x69, 0x2e, 0xe6, 0x7b, 0xd6, 0x9a,
+ 0x4b, 0x02, 0x10, 0x15, 0xb2, 0x60, 0x3a, 0x11, 0x9c, 0x4d, 0x87, 0xc6,
+ 0x70, 0x2d, 0x1b, 0xe8, 0xa9, 0xfc, 0xac, 0x91, 0x61, 0x94, 0x2f, 0x4a,
+ 0x55, 0x33, 0xfc, 0xd6, 0x73, 0x52, 0xee, 0x0f, 0xbc, 0x3b, 0xc0, 0x87,
+ 0x5a, 0x0b, 0x01, 0x97, 0x02, 0x13, 0x58, 0x77, 0x08, 0xf6, 0xe3, 0x3b,
+ 0xca, 0x22, 0x04, 0x82, 0x3f, 0x58, 0x7f, 0x98, 0xf2, 0x9b, 0xea, 0x0f,
+ 0x32, 0xf6, 0xa5, 0xf5, 0x21, 0x4e, 0x86, 0xfb, 0x76, 0x20, 0xb9, 0xea,
+ 0xee, 0xa2, 0x0a, 0x9f, 0x7b, 0x01, 0xe2, 0x17, 0xe1, 0x7b, 0x8f, 0x8a,
+ 0x92, 0xef, 0x06, 0x8d, 0x3c, 0x83, 0x71, 0x3f, 0x75, 0x10, 0xe7, 0x52,
+ 0xb7, 0x5b, 0xfb, 0x58, 0xc0, 0x8c, 0xe0, 0x33, 0x68, 0x1c, 0x07, 0xf2,
+ 0x55, 0x93, 0x2c, 0xb0, 0xb8, 0xb4, 0xa3, 0xef, 0x07, 0x11, 0xc0, 0x63,
+ 0x5a, 0x27, 0x87, 0x32, 0x2e, 0x88, 0x6f, 0xd6, 0x94, 0x94, 0xb4, 0x81,
+ 0x02, 0x16, 0x42, 0xe4, 0x0a, 0xd9, 0x71, 0x5f, 0x8a, 0xf4, 0xca, 0x9e,
+ 0x51, 0xd0, 0x23, 0x8a, 0x42, 0xd7, 0x39, 0xe7, 0x71, 0x3f, 0x41, 0x3d,
+ 0x7a, 0xd8, 0xd8, 0x23, 0xdb, 0xb3, 0x84, 0x55, 0x85, 0x05, 0x2e, 0xd1,
+ 0xcd, 0x4c, 0x5c, 0xff, 0xcb, 0xfb, 0xfc, 0x30, 0x59, 0x46, 0x0d, 0x16,
+ 0x49, 0x34, 0x09, 0x08, 0x27, 0x89, 0x5f, 0x77, 0x32, 0xce, 0x31, 0xf3,
+ 0xd0, 0x4c, 0x61, 0xe7, 0xc4, 0x56, 0x36, 0x46, 0x6b, 0xb5, 0xbb, 0x41,
+ 0x18, 0x71, 0xc9, 0x0d, 0x29, 0x56, 0x52, 0xf1, 0x37, 0x42, 0x8d, 0xf4,
+ 0xde, 0x1d, 0x42, 0xea, 0xde, 0x23, 0x88, 0xc1, 0x60, 0x0e, 0xaf, 0x79,
+ 0x42, 0x28, 0x1f, 0x5f, 0x53, 0x8a, 0x6f, 0x85, 0x99, 0x56, 0x41, 0x79,
+ 0xbe, 0x75, 0x3c, 0x70, 0xa9, 0x52, 0xf8, 0x88, 0x62, 0xf5, 0x06, 0x1b,
+ 0x19, 0x77, 0x24, 0x79, 0x83, 0x80, 0x6e, 0x61, 0x06, 0xbd, 0xc7, 0x36,
+ 0xf4, 0x4e, 0xc4, 0xb2, 0x4f, 0x6b, 0x68, 0x1d, 0xa6, 0xb1, 0xb1, 0xe1,
+ 0x37, 0x42, 0x53, 0x4c, 0xf4, 0x9c, 0x86, 0xb1, 0x06, 0x82, 0x90, 0x5d,
+ 0x84, 0xb3, 0xce, 0xd1, 0x1b, 0xf8, 0x60, 0xba, 0x8a, 0xdd, 0x7e, 0xf6,
+ 0x7f, 0xd1, 0xa4, 0xdb, 0x44, 0x0a, 0xef, 0xce, 0xd6, 0x6e, 0xc0, 0x12,
+ 0xf3, 0xee, 0xd3, 0x2e, 0x80, 0x53, 0xac, 0x0e, 0x22, 0xe0, 0x79, 0x9f,
+ 0x4d, 0x4f, 0xf7, 0xa8, 0x61, 0x55, 0xa0, 0x23, 0x36, 0xb6, 0x3b, 0xb4,
+ 0x11, 0x92, 0xdf, 0x1f, 0xe3, 0xb3, 0x80, 0xd0, 0x37, 0xe0, 0x16, 0x07,
+ 0x2c, 0xd7, 0xbe, 0x3b, 0x44, 0x40, 0xc2, 0xad, 0x59, 0xe3, 0x7e, 0x21,
+ 0xe6, 0x23, 0xf0, 0x7b, 0xa9, 0xba, 0x7e, 0x84, 0x7c, 0x02, 0x25, 0x5f,
+ 0x4a, 0xc5, 0x88, 0x3d, 0xab, 0xf0, 0xd0, 0xcf, 0xe7, 0xe9, 0x87, 0xd3,
+ 0x67, 0x04, 0x2e, 0x5d, 0x7a, 0x83, 0xc5, 0x54, 0x84, 0x1d, 0x0f, 0x96,
+ 0x1e, 0x99, 0x30, 0xb6, 0x89, 0xda, 0xba, 0x16, 0xa7, 0x42, 0xd4, 0xe7,
+ 0x04, 0xbe, 0x3a, 0x41, 0xc0, 0x8a, 0x98, 0xf5, 0x81, 0x21, 0xd3, 0xde,
+ 0xc7, 0x3b, 0xb7, 0x1f, 0x30, 0xe4, 0x34, 0x81, 0xfc, 0x23, 0x3b, 0xd6,
+ 0xe7, 0xe8, 0x17, 0x64, 0x5f, 0xae, 0xef, 0x02, 0x90, 0x56, 0x36, 0x1d,
+ 0x18, 0x6d, 0x45, 0x23, 0x15, 0x7f, 0x8f, 0x21, 0x0c, 0xa3, 0xaf, 0xf6,
+ 0xa5, 0xf3, 0x3b, 0xa1, 0x29, 0xf9, 0x19, 0x47, 0x2e, 0x1b, 0xbc, 0x92,
+ 0x4e, 0x5a, 0x37, 0xd5, 0xca, 0xe2, 0x6d, 0xe8, 0x9b, 0x56, 0x3e, 0x83,
+ 0xbb, 0xf7, 0x09, 0x37, 0x31, 0x5b, 0x60, 0xc4, 0xc5, 0x49, 0x0d, 0x2e,
+ 0xbe, 0x90, 0xa4, 0x12, 0x28, 0x13, 0x23, 0x0b, 0xa3, 0x08, 0x2d, 0x86,
+ 0x10, 0xb9, 0xef, 0x45, 0x1f, 0x4c, 0x84, 0xa5, 0xb5, 0xc5, 0x42, 0x7e,
+ 0x62, 0x8e, 0xd5, 0x96, 0x93, 0xed, 0xcf, 0xb0, 0x55, 0x1c, 0x84, 0xbc,
+ 0x5e, 0x41, 0x38, 0x3f, 0xe9, 0x1c, 0x2c, 0x1b, 0x4f, 0x7a, 0x52, 0x37,
+ 0xd0, 0xd2, 0x04, 0xeb, 0x1e, 0x06, 0xbb, 0xd0, 0x99, 0xda, 0x08, 0x25,
+ 0xf9, 0xc2, 0x4e, 0x53, 0xcf, 0x76, 0x6d, 0xf5, 0x57, 0x97, 0x6b, 0x46,
+ 0x3c, 0xbd, 0xe2, 0xec, 0xb4, 0x9a, 0xd1, 0x03, 0xe5, 0x44, 0x38, 0x4d,
+ 0x7c, 0x67, 0x30, 0x46, 0x8f, 0xc5, 0x3f, 0x4f, 0x46, 0x94, 0xd9, 0xc9,
+ 0x45, 0xac, 0x07, 0x36, 0xc4, 0x2a, 0x58, 0x70, 0x4a, 0x5c, 0xb9, 0xa0,
+ 0x0a, 0x61, 0xe5, 0xf2, 0xfb, 0xe2, 0x47, 0x45, 0xb5, 0x3a, 0xde, 0x96,
+ 0x9f, 0x21, 0xdf, 0xe7, 0x39, 0xed, 0xa6, 0x04, 0x0b, 0x80, 0x8a, 0x32,
+ 0x65, 0x63, 0xd5, 0x4f, 0x9c, 0x5d, 0x3c, 0x18, 0xbb, 0x20, 0x65, 0x29,
+ 0x24, 0xa8, 0xad, 0xca, 0x4e, 0x0f, 0xac, 0x75, 0xad, 0x3e, 0xc6, 0x26,
+ 0x3c, 0xc9, 0xd8, 0x91, 0x97, 0x16, 0x69, 0xd8, 0x77, 0x01, 0x16, 0x22,
+ 0x7f, 0xd6, 0x82, 0x47, 0x7f, 0xfd, 0x1c, 0x31, 0x20, 0x9f, 0xcd, 0x4e,
+ 0x90, 0xcf, 0x57, 0x78, 0xf7, 0x1d, 0x17, 0xe4, 0xa5, 0xb5, 0xe4, 0x33,
+ 0x7d, 0x06, 0x29, 0x58, 0x46, 0xf4, 0xcb, 0xf1, 0x7a, 0x74, 0x8f, 0xda,
+ 0x22, 0x83, 0x6f, 0x5b, 0x59, 0x09, 0x12, 0x7d, 0xa2, 0x64, 0x36, 0x01,
+ 0xee, 0x4e, 0xe1, 0x94, 0x1f, 0x7a, 0x3c, 0x05, 0x4c, 0x2a, 0x47, 0x4d,
+ 0x32, 0x7e, 0x5b, 0xcc, 0x86, 0x1d, 0xb7, 0x1d, 0xca, 0x48, 0xa0, 0x55,
+ 0x2f, 0x0e, 0x2d, 0x67, 0x54, 0xd6, 0x63, 0x7a, 0xb1, 0x7a, 0xae, 0x5c,
+ 0xfa, 0x08, 0x75, 0x8f, 0x45, 0x0d, 0x62, 0x99, 0xc0, 0xcb, 0x63, 0xe3,
+ 0x57, 0x22, 0xa9, 0xb2, 0xb2, 0x74, 0x75, 0xd3, 0xe6, 0x99, 0x18, 0x8e,
+ 0xfa, 0x26, 0x62, 0xb5, 0x01, 0xb0, 0xe9, 0x16, 0xb7, 0xe3, 0x17, 0x74,
+ 0x99, 0x00, 0x1a, 0xb6, 0x9f, 0xa6, 0x71, 0x6a, 0x56, 0x05, 0xc6, 0x1b,
+ 0xb1, 0x2a, 0xbd, 0x85, 0xe6, 0xdf, 0x1c, 0xa5, 0x26, 0x4a, 0xc1, 0xcd,
+ 0x0d, 0xbb, 0xa3, 0x60, 0xc3, 0x5f, 0x91, 0x5d, 0x90, 0x88, 0x1e, 0xe8,
+ 0x25, 0x4a, 0xb8, 0xbf, 0x88, 0xd8, 0xe6, 0xc6, 0x21, 0x87, 0x04, 0x89,
+ 0xdf, 0x95, 0x8c, 0x83, 0x70, 0x5b, 0x14, 0xd3, 0x98, 0x02, 0xaf, 0x2c,
+ 0xf0, 0x11, 0x62, 0x7b, 0x0d, 0xd2, 0xcd, 0x34, 0x8a, 0x57, 0x1d, 0x1e,
+ 0x2c, 0xe0, 0x6f, 0xd2, 0xd6, 0x50, 0x40, 0x65, 0x9e, 0x67, 0xd7, 0x74,
+ 0x58, 0xbc, 0x4d, 0x16, 0x3b, 0x54, 0xa1, 0x00, 0x3b, 0xb8, 0x1c, 0xce,
+ 0x2d, 0x01, 0x1b, 0x97, 0xe1, 0x67, 0x1a, 0xd4, 0xf7, 0xab, 0x6f, 0x42,
+ 0x2e, 0xa6, 0x72, 0x1e, 0x8a, 0x8c, 0xb3, 0xa5, 0x33, 0x80, 0xe6, 0xc0,
+ 0x1d, 0x43, 0x95, 0x84, 0x84, 0x6c, 0x72, 0x77, 0x5c, 0x70, 0xcc, 0x18,
+ 0xbd, 0x2f, 0xc0, 0x21, 0x23, 0xb4, 0x13, 0x93, 0x63, 0xde, 0x34, 0x0d,
+ 0xa0, 0x74, 0x26, 0xd4, 0x55, 0xfb, 0xdc, 0xf0, 0x59, 0xa8, 0xba, 0x07,
+ 0xc9, 0x21, 0xa8, 0x95, 0x52, 0xd0, 0xa0, 0x32, 0x23, 0x45, 0x6f, 0x00,
+ 0xf2, 0x80, 0xdd, 0x3a, 0xb2, 0xb9, 0x92, 0x7b, 0x6c, 0x5f, 0xa3, 0x34,
+ 0x60, 0x96, 0x2a, 0xff, 0x7e, 0x61, 0x73, 0x11, 0x1e, 0x29, 0xc5, 0xb5,
+ 0x45, 0xf3, 0xc6, 0x99, 0x6e, 0xfc, 0x79, 0x18, 0xe2, 0xb2, 0xbd, 0x75,
+ 0xd7, 0xeb, 0xdd, 0x57, 0x7c, 0x1b, 0xf0, 0x65, 0xd5, 0xd3, 0x9d, 0x9f,
+ 0xc9, 0x2a, 0x2f, 0xd8, 0xfb, 0xe2, 0x38, 0x2f, 0x20, 0x5e, 0xd2, 0x71,
+ 0x03, 0x8d, 0xbc, 0xff, 0x60, 0x2d, 0xa0, 0x1d, 0x18, 0x8a, 0x99, 0x4f,
+ 0xf1, 0x62, 0x94, 0xdb, 0xbc, 0xb1, 0xd8, 0xbf, 0xe0, 0x82, 0x33, 0x2c,
+ 0x51, 0x36, 0x38, 0xfd, 0x88, 0x46, 0x85, 0x1f, 0x5f, 0xb2, 0x5f, 0x20,
+ 0xd3, 0x85, 0xa8, 0xdf, 0x73, 0x1b, 0xe9, 0xb1, 0x3d, 0x3e, 0x15, 0x81,
+ 0xfc, 0xae, 0xdc, 0x1c, 0x44, 0x9e, 0xe6, 0x03, 0x47, 0x93, 0x22, 0x14,
+ 0x5b, 0xe7, 0x1e, 0xec, 0xee, 0xdc, 0x49, 0xb9, 0x11, 0x8b, 0x91, 0x02,
+ 0x85, 0x1f, 0xd0, 0xb6, 0x26, 0xb3, 0x16, 0x12, 0x85, 0x3d, 0xeb, 0xde,
+ 0x8f, 0xab, 0x74, 0xb7, 0x4a, 0xe8, 0xf2, 0x14, 0x26, 0x27, 0xfb, 0x4b,
+ 0x9a, 0xea, 0x97, 0xdc, 0x4a, 0x4a, 0x9b, 0xcf, 0x30, 0x5b, 0x00, 0x6a,
+ 0x2b, 0x7c, 0x55, 0xea, 0xb8, 0xfa, 0xa7, 0x77, 0xfa, 0x2e, 0xea, 0x91,
+ 0x92, 0x48, 0x16, 0x5e, 0xfa, 0x86, 0x6b, 0x76, 0xc2, 0xfc, 0x02, 0xf4,
+ 0x39, 0xeb, 0x9e, 0xdb, 0xbc, 0x53, 0x66, 0x27, 0x79, 0xa7, 0x1d, 0x40,
+ 0xc8, 0x19, 0xec, 0xfd, 0xf4, 0x71, 0x67, 0x27, 0x98, 0xf8, 0x63, 0xd1,
+ 0x90, 0xa5, 0x02, 0xfe, 0x60, 0x8c, 0x3e, 0xab, 0x13, 0x81, 0x76, 0x45,
+ 0x18, 0xa8, 0x81, 0x84, 0x25, 0x91, 0x16, 0x71, 0x5e, 0x0e, 0xd1, 0x2c,
+ 0xf2, 0x85, 0x06, 0xa8, 0x51, 0x92, 0xa8, 0x1a, 0x8e, 0xa3, 0x4b, 0xc6,
+ 0x3a, 0xea, 0x31, 0x9a, 0x0a, 0xc3, 0x7e, 0x3f, 0xf8, 0xd6, 0x00, 0x18,
+ 0xd1, 0xaa, 0x91, 0x0b, 0xc8, 0x5f, 0xde, 0x26, 0xba, 0x95, 0x02, 0x6e,
+ 0x92, 0xc8, 0x64, 0x23, 0xd5, 0x03, 0x20, 0x77, 0x6c, 0x78, 0xf3, 0xeb,
+ 0xe8, 0x31, 0xfc, 0x11, 0x61, 0x8a, 0x23, 0x4e, 0x18, 0x11, 0xe6, 0xe1,
+ 0x79, 0x6c, 0x65, 0xc5, 0x4b, 0x1f, 0xfb, 0x4f, 0x7b, 0xf9, 0xaf, 0xb2,
+ 0x7c, 0xbb, 0x9f, 0x1f, 0x5d, 0xec, 0x10, 0x94, 0x62, 0xe8, 0x50, 0x42,
+ 0xdd, 0x2a, 0x58, 0x10, 0x49, 0x7e, 0x85, 0x81, 0x9e, 0xc1, 0xa4, 0x64,
+ 0x6d, 0x9a, 0x6d, 0x32, 0xf7, 0x4d, 0x70, 0xd9, 0x0e, 0x0e, 0x3f, 0x76,
+ 0x2a, 0x7a, 0x8f, 0xea, 0xd9, 0xf7, 0xd8, 0x06, 0xd9, 0x32, 0x8f, 0x37,
+ 0x8f, 0xda, 0x5c, 0x02, 0x43, 0xad, 0x79, 0xda, 0xf8, 0xeb, 0x0c, 0x62,
+ 0x4f, 0x4c, 0x89, 0xef, 0xf0, 0x6c, 0xdc, 0x71, 0xbb, 0xea, 0xaa, 0x6c,
+ 0x34, 0x00, 0xa9, 0x38, 0xbd, 0xca, 0xda, 0xc5, 0x41, 0x41, 0xa5, 0x0b,
+ 0xd2, 0xa4, 0x4f, 0x2b, 0x4f, 0x84, 0xb9, 0x6d, 0xa3, 0xbd, 0xb9, 0xb5,
+ 0x6b, 0xcf, 0xdb, 0xa8, 0x15, 0xd7, 0xa7, 0xc3, 0x74, 0x47, 0x57, 0xa1,
+ 0xa7, 0x1e, 0x4e, 0xcd, 0xa0, 0x56, 0xd7, 0xfd, 0xc3, 0x12, 0xe9, 0xaf,
+ 0x2f, 0x4c, 0xa5, 0x76, 0xaa, 0xd3, 0x74, 0x19, 0xac, 0x71, 0xf2, 0xf7,
+ 0xf6, 0x26, 0x3d, 0x3f, 0x83, 0xb9, 0xf7, 0xec, 0x21, 0x6c, 0x8e, 0xe2,
+ 0x4d, 0x99, 0xd7, 0xf6, 0xca, 0xcd, 0x8d, 0xd6, 0x8f, 0x43, 0x78, 0xb2,
+ 0x2e, 0xdc, 0x7d, 0xf6, 0x39, 0x9d, 0x1b, 0xab, 0x13, 0x27, 0x51, 0x6a,
+ 0x31, 0xae, 0xf9, 0xec, 0x59, 0xbd, 0x85, 0x73, 0x53, 0xa6, 0x8e, 0x91,
+ 0x9a, 0xf1, 0x14, 0x84, 0x3b, 0xfa, 0x88, 0x5b, 0x32, 0x35, 0xe2, 0x66,
+ 0x55, 0x13, 0x5f, 0xef, 0x5c, 0x28, 0xfc, 0x59, 0x7c, 0x0b, 0x64, 0x58,
+ 0x6e, 0x4b, 0x4e, 0x10, 0x46, 0x28, 0x1f, 0x09, 0xaa, 0x17, 0x49, 0x84,
+ 0x25, 0x3b, 0x63, 0x2a, 0x24, 0xce, 0x4f, 0xea, 0xf9, 0xdc, 0xc9, 0x4b,
+ 0x15, 0xad, 0xe5, 0x2d, 0x1f, 0xb8, 0xc1, 0xae, 0x14, 0x39, 0x3c, 0x3a,
+ 0x63, 0x24, 0xcf, 0x62, 0xdb, 0xf8, 0x1f, 0x74, 0xa9, 0xff, 0x02, 0x76,
+ 0x87, 0x1f, 0x62, 0xa6, 0x8d, 0x43, 0x34, 0xe7, 0x7e, 0x80, 0xa1, 0x2f,
+ 0xf1, 0x82, 0x77, 0x49, 0x86, 0x73, 0x73, 0x77, 0x3a, 0xa3, 0xd7, 0xf1,
+ 0x01, 0x7b, 0xa4, 0xf0, 0xf8, 0x37, 0x64, 0x67, 0x6e, 0x98, 0x72, 0x96,
+ 0xc4, 0x33, 0xb7, 0xcd, 0x90, 0x19, 0x26, 0xf1, 0x96, 0xcf, 0x27, 0xae,
+ 0xef, 0x3d, 0x5f, 0xbb, 0x61, 0x56, 0x8f, 0x38, 0x7e, 0x57, 0x2b, 0xea,
+ 0x93, 0x88, 0x71, 0x1b, 0x7d, 0xd9, 0x8d, 0x0d, 0x96, 0x1b, 0x0c, 0x3b,
+ 0x6d, 0x66, 0x93, 0x75, 0x42, 0x96, 0xe5, 0xa5, 0xb9, 0xa2, 0xcc, 0xba,
+ 0x64, 0xec, 0x7c, 0x27, 0x59, 0xa8, 0x4d, 0xd0, 0x76, 0x56, 0x7b, 0x7c,
+ 0xc2, 0xa6, 0x19, 0x77, 0x54, 0x51, 0x83, 0xc8, 0xef, 0x2d, 0xaf, 0xeb,
+ 0x8d, 0xcb, 0xf9, 0xcb, 0x3e, 0xa8, 0xaa, 0x70, 0x3a, 0xe3, 0x26, 0x04,
+ 0xfe, 0xb1, 0x59, 0x85, 0x03, 0x24, 0x11, 0x33, 0x53, 0x5a, 0x0f, 0x5a,
+ 0x1a, 0x0f, 0xf6, 0x54, 0xc4, 0xdb, 0x43, 0x73, 0xec, 0x32, 0x22, 0x37,
+ 0xa5, 0xe7, 0x08, 0xeb, 0x69, 0x94, 0xd0, 0xed, 0xdc, 0x05, 0x07, 0x00,
+ 0x42, 0xdf, 0x01, 0x2b, 0x39, 0xd8, 0x3b, 0xbb, 0x57, 0x6a, 0xab, 0xb6,
+ 0xb4, 0x12, 0xc3, 0x32, 0x11, 0xf3, 0xdf, 0x81, 0x4e, 0x4e, 0x1f, 0x11,
+ 0x55, 0x26, 0x3a, 0x1d, 0x71, 0xd6, 0xbc, 0xbf, 0x33, 0x01, 0xc6, 0xa4,
+ 0x16, 0xce, 0x22, 0x13, 0xdb, 0xdb, 0x89, 0x32, 0x6d, 0x14, 0xed, 0xcb,
+ 0x1b, 0xf2, 0x85, 0x34, 0x2b, 0x60, 0x21, 0x6f, 0xb4, 0x80, 0x9b, 0x8f,
+ 0x84, 0x42, 0xf8, 0xd4, 0xc7, 0x28, 0x6a, 0x1f, 0x15, 0xee, 0x12, 0x04,
+ 0xd2, 0x7a, 0xb2, 0xdb, 0xa1, 0x85, 0x89, 0xbb, 0x73, 0x38, 0x5b, 0x95,
+ 0xe6, 0xb2, 0xda, 0xd8, 0x67, 0xe4, 0xed, 0x27, 0x6b, 0x74, 0x33, 0x10,
+ 0xff, 0xb0, 0x5a, 0x47, 0xf2, 0x90, 0x0b, 0xf1, 0xc3, 0xbf, 0x07, 0x23,
+ 0x07, 0xef, 0x30, 0x87, 0xe7, 0xe4, 0xee, 0x2e, 0x48, 0xbd, 0x25, 0x5f,
+ 0x9e, 0x6b, 0xff, 0xf0, 0xe1, 0x1d, 0xeb, 0x52, 0x85, 0xe9, 0x99, 0x31,
+ 0x5e, 0x3f, 0xbf, 0x0a, 0x16, 0x5e, 0x6f, 0x5b, 0x98, 0xe3, 0x87, 0x20,
+ 0x7b, 0xb6, 0x75, 0x10, 0xb3, 0xa1, 0xe7, 0xcc, 0xa0, 0x85, 0x7e, 0xd5,
+ 0x21, 0xe8, 0xcc, 0x2c, 0x63, 0x9a, 0x9b, 0xac, 0x6e, 0xa3, 0xad, 0x53,
+ 0x2a, 0xb4, 0xf7, 0x79, 0x0c, 0x7c, 0xb9, 0x64, 0xb6, 0x5c, 0x6c, 0x33,
+ 0xd0, 0x8f, 0x9a, 0x07, 0x49, 0x4c, 0x6a, 0x99, 0xf6, 0x89, 0x91, 0x5e,
+ 0xfd, 0xbc, 0xb7, 0x43, 0x99, 0x9c, 0xf4, 0x08, 0x02, 0x18, 0x23, 0xab,
+ 0x1d, 0x53, 0x64, 0x04, 0x5c, 0xb8, 0x21, 0xee, 0x59, 0x13, 0x5d, 0x7d,
+ 0xa0, 0xd1, 0x37, 0xb8, 0x35, 0x38, 0xb6, 0x90, 0x8b, 0x7c, 0x0a, 0x42,
+ 0x4c, 0x71, 0x21, 0x43, 0x07, 0x66, 0x36, 0x8f, 0xc6, 0x1a, 0xaa, 0xb6,
+ 0xa8, 0xda, 0x20, 0x6d, 0xcb, 0x61, 0xdc, 0xdd, 0xe5, 0xa7, 0x11, 0xed,
+ 0x98, 0x13, 0xf9, 0x87, 0xae, 0x17, 0x7a, 0x32, 0x78, 0xe8, 0x31, 0x99,
+ 0x46, 0x2c, 0x3a, 0xf5, 0x95, 0x5c, 0x90, 0xc9, 0xd0, 0xd9, 0xb5, 0xbd,
+ 0xf8, 0x74, 0x12, 0x5b, 0xfc, 0x5a, 0x97, 0xd6, 0x71, 0xd4, 0xee, 0x25,
+ 0x1d, 0x0d, 0x3d, 0xf8, 0x75, 0xfa, 0xda, 0x9d, 0x1f, 0x37, 0x80, 0x72,
+ 0x00, 0x37, 0xf8, 0x2c, 0x88, 0x01, 0x21, 0x72, 0xfd, 0x84, 0xf4, 0xda,
+ 0x69, 0xe4, 0xde, 0x52, 0x3c, 0xbe, 0x5f, 0x9d, 0xca, 0x9a, 0x61, 0x87,
+ 0xf9, 0xb5, 0x57, 0x81, 0x00, 0x2b, 0x7e, 0xb8, 0x8b, 0xa9, 0x76, 0xca,
+ 0xa4, 0xe4, 0x84, 0x20, 0x38, 0x03, 0x49, 0x85, 0x06, 0x9c, 0x91, 0x7a,
+ 0xcc, 0x21, 0x64, 0x15, 0xbe, 0x91, 0xdd, 0x00, 0x2e, 0x18, 0x9f, 0x42,
+ 0x9b, 0x7f, 0xa9, 0x64, 0x43, 0x98, 0xbc, 0xe1, 0x5d, 0x59, 0x3e, 0x87,
+ 0xe3, 0x7b, 0xd9, 0x34, 0x29, 0x0e, 0x61, 0x88, 0xb3, 0xea, 0xa4, 0x51,
+ 0xfb, 0x7e, 0x25, 0x11, 0xe3, 0x14, 0x5f, 0x16, 0x30, 0x0f, 0xcf, 0x53,
+ 0xab, 0x0a, 0x2b, 0x2d, 0x3f, 0x97, 0x41, 0xc0, 0x07, 0x61, 0x57, 0xb1,
+ 0xa0, 0x21, 0xa7, 0x1a, 0x2d, 0x6a, 0xc3, 0x9f, 0xf6, 0xda, 0x09, 0x5d,
+ 0xe3, 0x80, 0x4b, 0x59, 0xd9, 0xa8, 0x8f, 0xa1, 0x98, 0x60, 0x4c, 0xe1,
+ 0x3e, 0xac, 0x97, 0xa8, 0x13, 0x85, 0x41, 0xdf, 0x0c, 0x6a, 0xfc, 0xec,
+ 0x1b, 0xb6, 0x98, 0x89, 0x46, 0xeb, 0xba, 0x1b, 0xec, 0xd1, 0x1c, 0x2e,
+ 0x5c, 0xb9, 0x76, 0x44, 0x41, 0x3c, 0x4c, 0xa0, 0xdc, 0xd1, 0x80, 0xa5,
+ 0xfd, 0x1b, 0x4c, 0xf6, 0x20, 0xdb, 0xb4, 0xbb, 0x0e, 0xc1, 0x14, 0x85,
+ 0xad, 0x1b, 0x7f, 0x3e, 0xa3, 0x0b, 0xac, 0x5d, 0x69, 0x62, 0x83, 0x67,
+ 0xac, 0x9c, 0xf2, 0x46, 0xb2, 0x99, 0x6e, 0xe2, 0x40, 0xb8, 0xab, 0xe5,
+ 0x1f, 0xeb, 0xe0, 0xd6, 0x73, 0x6a, 0x53, 0x69, 0x42, 0xc8, 0x2d, 0x11,
+ 0x3a, 0x2f, 0xdf, 0x1d, 0x02, 0x92, 0x62, 0xe0, 0x78, 0xb1, 0x38, 0x27,
+ 0x94, 0xd8, 0x30, 0xeb, 0x50, 0xdd, 0xa7, 0xa2, 0xc6, 0x57, 0x10, 0x4b,
+ 0xcf, 0x15, 0xca, 0x7b, 0x80, 0xfd, 0x5c, 0x41, 0x15, 0x8d, 0x6d, 0x68,
+ 0x7e, 0xc0, 0x03, 0x8a, 0x70, 0xfa, 0x6d, 0xbb, 0x32, 0x04, 0xa4, 0x95,
+ 0x4d, 0x19, 0xce, 0x13, 0xb7, 0x29, 0x7b, 0x6d, 0x1f, 0xbc, 0xc4, 0x36,
+ 0x78, 0x55, 0x0b, 0x88, 0x75, 0xc4, 0x16, 0xd3, 0xa0, 0xf9, 0xfd, 0xf2,
+ 0xee, 0x23, 0xe6, 0x18, 0x13, 0x5c, 0xf2, 0x6d, 0x64, 0x62, 0x19, 0xef,
+ 0xdf, 0x80, 0xae, 0x4f, 0x07, 0x9a, 0x21, 0x58, 0xe1, 0xce, 0x28, 0xa8,
+ 0xfe, 0x8e, 0x6f, 0x39, 0x94, 0xaa, 0x9a, 0x06, 0xb9, 0x0c, 0xbb, 0x12,
+ 0x00, 0xb7, 0xab, 0x65, 0xf8, 0x42, 0x92, 0x99, 0x01, 0x48, 0x83, 0x70,
+ 0xc2, 0x89, 0xf2, 0xd1, 0xe4, 0x44, 0xb6, 0xf9, 0x14, 0x53, 0x9c, 0x6c,
+ 0x0a, 0x52, 0xe2, 0xce, 0x28, 0xda, 0x43, 0xbc, 0x21, 0x1e, 0xab, 0x39,
+ 0x1d, 0xfc, 0x0a, 0xb6, 0x89, 0xc9, 0x58, 0x26, 0x74, 0x96, 0x68, 0xde,
+ 0xe2, 0x1b, 0x1c, 0x15, 0x20, 0x7b, 0x72, 0xf5, 0x4f, 0x9b, 0x67, 0x7d,
+ 0x90, 0xb2, 0x79, 0xbf, 0xec, 0x78, 0x63, 0xcc, 0x9b, 0x88, 0xf6, 0x73,
+ 0x55, 0xe4, 0xde, 0x64, 0x2a, 0xab, 0xdc, 0xd0, 0xd0, 0xd9, 0xc5, 0xc2,
+ 0xd3, 0xae, 0x82, 0xb4, 0x5b, 0xd8, 0xc3, 0x39, 0xea, 0x34, 0xaa, 0x82,
+ 0xcb, 0xe3, 0xb8, 0xd4, 0x35, 0x8d, 0xf3, 0x08, 0x12, 0xbb, 0x51, 0x16,
+ 0x60, 0x24, 0xf9, 0x5c, 0xbf, 0x91, 0x06, 0x9a, 0x7c, 0x64, 0x27, 0xaf,
+ 0x53, 0x76, 0xf0, 0xd5, 0x4b, 0x2b, 0x0b, 0xe9, 0x29, 0xa0, 0x1f, 0x4d,
+ 0xb7, 0x07, 0x90, 0xf0, 0x81, 0x4b, 0x88, 0x63, 0x93, 0x81, 0x82, 0x1b,
+ 0xbe, 0x57, 0xa1, 0x49, 0xf8, 0x1f, 0x2e, 0x96, 0xe3, 0xd7, 0xb7, 0x81,
+ 0x44, 0x4b, 0x41, 0x7f, 0xc9, 0x1c, 0x38, 0x77, 0xc7, 0x38, 0x05, 0xcb,
+ 0x0c, 0x6a, 0x84, 0x74, 0x33, 0x30, 0xdb, 0xa4, 0xef, 0x4a, 0x63, 0xa2,
+ 0x58, 0x39, 0xe8, 0xaf, 0xb8, 0xf2, 0xdb, 0x12, 0xdb, 0x42, 0x56, 0xab,
+ 0xb7, 0x6d, 0xc3, 0xfd, 0xdc, 0x08, 0x80, 0xfd, 0xc5, 0xc5, 0x59, 0xab,
+ 0x72, 0xb9, 0x9e, 0x03, 0xc1, 0xf6, 0x3b, 0x92, 0x00, 0xa4, 0xba, 0x3d,
+ 0xb5, 0x9f, 0xed, 0xbe, 0xd7, 0xc5, 0x6a, 0x23, 0x1f, 0x62, 0x6f, 0x45,
+ 0x49, 0x43, 0xee, 0x49, 0x6d, 0x2c, 0xd5, 0x4c, 0x0f, 0x54, 0xb9, 0x57,
+ 0x39, 0x74, 0xad, 0x54, 0x31, 0x47, 0xed, 0x7f, 0x25, 0x07, 0x8a, 0x47,
+ 0xaf, 0x85, 0x5b, 0x54, 0xa9, 0xd1, 0x3d, 0x5d, 0x1f, 0x0b, 0x8d, 0xee,
+ 0x17, 0x50, 0xae, 0xb0, 0xfc, 0x5c, 0x75, 0x95, 0xad, 0xb8, 0xe5, 0xf8,
+ 0x50, 0xc5, 0xd3, 0x7a, 0xa3, 0xc1, 0x59, 0x17, 0xea, 0xa4, 0xd0, 0x87,
+ 0xcd, 0x0d, 0xc2, 0xb8, 0xbc, 0x8f, 0x8c, 0x5a, 0xb7, 0x33, 0x76, 0xf5,
+ 0x6b, 0xc3, 0xfc, 0x99, 0x65, 0x78, 0x91, 0x7b, 0x33, 0x73, 0x7c, 0x41,
+ 0x9c, 0xdf, 0xad, 0x41, 0x75, 0x76, 0xe9, 0xa9, 0xc4, 0x8d, 0x56, 0xb9,
+ 0x2a, 0x9e, 0xa3, 0x22, 0x2a, 0xed, 0x37, 0xad, 0xfa, 0x4f, 0x26, 0xb8,
+ 0x5a, 0x35, 0x57, 0x83, 0xc4, 0xcd, 0x5f, 0xbe, 0x75, 0xed, 0x2f, 0x62,
+ 0x9d, 0x58, 0x5f, 0x48, 0x4d, 0x48, 0x1f, 0x9e, 0x73, 0x6a, 0x5a, 0x85,
+ 0x12, 0xee, 0x47, 0xac, 0x84, 0x4f, 0x15, 0xb0, 0x5e, 0xe7, 0xbf, 0x35,
+ 0xde, 0xb2, 0xf7, 0x19, 0x45, 0x8f, 0x0a, 0xd7, 0x37, 0x7e, 0x38, 0xcc,
+ 0x75, 0x65, 0x70, 0xfb, 0xef, 0x1b, 0x24, 0x0e, 0x4a, 0xc6, 0x80, 0x5d,
+ 0x4c, 0x3a, 0xf6, 0x71, 0x61, 0x6a, 0x56, 0xe9, 0x0b, 0x6f, 0x5e, 0xb6,
+ 0x7f, 0x66, 0xd9, 0x53, 0x61, 0xfc, 0x43, 0x94, 0x1f, 0xeb, 0x74, 0x81,
+ 0x39, 0x03, 0xbe, 0xe5, 0xcc, 0x6d, 0xc2, 0xc2, 0x31, 0xd2, 0x15, 0x44,
+ 0xcb, 0x35, 0x90, 0x8b, 0xa7, 0x81, 0xc2, 0xb9, 0x4c, 0x6f, 0x40, 0x26,
+ 0x4b, 0xf8, 0x80, 0x05, 0x10, 0x9c, 0x90, 0x85, 0xb3, 0x17, 0xa1, 0x89,
+ 0xa6, 0x44, 0x36, 0xc5, 0xdf, 0x78, 0xb2, 0x82, 0xdb, 0x92, 0x7f, 0x6d,
+ 0x58, 0x1f, 0xa1, 0xf5, 0x52, 0xdc, 0xf2, 0x4f, 0x70, 0xfa, 0xad, 0x42,
+ 0xf5, 0x81, 0x38, 0x44, 0xfb, 0x29, 0x71, 0x97, 0xe6, 0xbb, 0xb2, 0x02,
+ 0x91, 0xe6, 0xc8, 0xd3, 0xfe, 0x95, 0x5e, 0x87, 0xb8, 0xbb, 0x8b, 0xdc,
+ 0xb5, 0x1a, 0x0d, 0x0b, 0xaa, 0x21, 0xac, 0xa2, 0x86, 0xa4, 0xc0, 0xe9,
+ 0x4d, 0xd8, 0xb9, 0x28, 0xa8, 0x82, 0xa3, 0xbb, 0xed, 0xbe, 0xed, 0xbc,
+ 0x6c, 0x54, 0x3b, 0x80, 0xf2, 0xe4, 0xe6, 0x0d, 0x9c, 0xf3, 0x8c, 0xe2,
+ 0x7d, 0xb1, 0xa6, 0xb0, 0x07, 0xae, 0x13, 0xca, 0x11, 0x83, 0xc8, 0x6b,
+ 0x59, 0x01, 0xb2, 0x44, 0xda, 0x80, 0xf7, 0xe5, 0x6a, 0xbd, 0xcc, 0x33,
+ 0x20, 0xe3, 0xac, 0x8b, 0x07, 0xd5, 0x3b, 0xc6, 0x5c, 0x18, 0xba, 0xac,
+ 0xe7, 0x90, 0x81, 0xd8, 0xcd, 0x4d, 0xc6, 0xfa, 0xb3, 0x4e, 0x05, 0xb1,
+ 0xcb, 0xc8, 0xb7, 0x90, 0x94, 0x17, 0x1f, 0xfc, 0x8b, 0xad, 0x8b, 0x35,
+ 0x1c, 0x33, 0x3f, 0x5a, 0x6d, 0xee, 0x5b, 0x48, 0x75, 0x8e, 0xbf, 0xc6,
+ 0xc2, 0xd9, 0xbc, 0x0c, 0xd8, 0x20, 0xd0, 0xe9, 0x68, 0x1a, 0x7d, 0xa9,
+ 0xcc, 0x03, 0x63, 0x2b, 0x67, 0x70, 0xd6, 0x41, 0x3b, 0xd7, 0xe2, 0x92,
+ 0x4d, 0x7b, 0xca, 0x18, 0x2b, 0x2e, 0xf7, 0x5e, 0x34, 0xb6, 0xe6, 0x5a,
+ 0xb3, 0xae, 0xc8, 0x3d, 0xbd, 0xf3, 0xfb, 0x02, 0x34, 0xc4, 0xd1, 0xb2,
+ 0xee, 0xe6, 0xcc, 0xfd, 0xbc, 0xe8, 0xaf, 0x80, 0x70, 0x34, 0x70, 0x45,
+ 0x8b, 0x33, 0x64, 0xd1, 0x60, 0xd3, 0x27, 0x4f, 0x2c, 0xf8, 0xb3, 0xa9,
+ 0xf6, 0x96, 0x75, 0x5f, 0x49, 0x4f, 0x64, 0xa5, 0x25, 0x9d, 0xa7, 0xc7,
+ 0x53, 0xc4, 0x54, 0xdb, 0x86, 0xaa, 0x9f, 0xc9, 0xf4, 0x60, 0xf9, 0xd7,
+ 0xf5, 0x45, 0xc2, 0x2d, 0xcf, 0x54, 0xe2, 0x1d, 0x1a, 0x52, 0xae, 0x3c,
+ 0xd0, 0x9e, 0x9a, 0xd5, 0x00, 0x63, 0xd5, 0xbd, 0x69, 0x79, 0xac, 0xe5,
+ 0xb3, 0xab, 0xb7, 0x98, 0xa9, 0x72, 0xb4, 0x03, 0x46, 0xc9, 0x16, 0x70,
+ 0x2b, 0x22, 0xc8, 0xbf, 0xd5, 0x7a, 0xe3, 0x74, 0xe5, 0x2e, 0xa4, 0x14,
+ 0x57, 0x62, 0x9e, 0x11, 0xf9, 0x58, 0xc4, 0x10, 0xcb, 0x6d, 0x90, 0x1a,
+ 0xe0, 0xe1, 0x36, 0x9d, 0x2c, 0xfb, 0x79, 0xad, 0xd0, 0x96, 0xa3, 0xe9,
+ 0x2a, 0x97, 0xd0, 0x54, 0xa3, 0xce, 0xf4, 0x8a, 0x90, 0xcf, 0x2e, 0xa9,
+ 0x73, 0x6a, 0x43, 0x87, 0xce, 0xf2, 0x9d, 0x61, 0xb3, 0x48, 0x2e, 0xa1,
+ 0x61, 0xe5, 0x66, 0xa6, 0x8f, 0xd1, 0x1d, 0x4c, 0xc5, 0xae, 0x61, 0xbc,
+ 0xf8, 0x12, 0x15, 0xc4, 0xe6, 0x22, 0x0b, 0x39, 0x9c, 0x1f, 0x03, 0x3f,
+ 0x54, 0xe1, 0x60, 0x1c, 0x94, 0x81, 0xae, 0x40, 0xdc, 0x42, 0xbe, 0x36,
+ 0x5a, 0x20, 0x1c, 0x57, 0x99, 0xbb, 0x28, 0xcd, 0x7c, 0x95, 0x9d, 0x14,
+ 0x01, 0x8f, 0x0a, 0x68, 0x32, 0x99, 0x67, 0xef, 0xd8, 0x76, 0xca, 0x99,
+ 0xd9, 0x41, 0x3a, 0x1f, 0x85, 0xb3, 0x3c, 0x2a, 0x53, 0xbf, 0x2f, 0x10,
+ 0xc9, 0xeb, 0x67, 0x2b, 0x08, 0x3f, 0x0b, 0xf9, 0x27, 0x72, 0xec, 0xc0,
+ 0x9d, 0x4a, 0x88, 0xdd, 0xe7, 0x8f, 0xd2, 0x5f, 0xb0, 0xf2, 0xd8, 0xa1,
+ 0x31, 0x2a, 0xa1, 0x5a, 0x8b, 0x06, 0xee, 0x32, 0x6f, 0xbb, 0x61, 0xf5,
+ 0xca, 0xe3, 0xbc, 0x50, 0xfa, 0x69, 0xee, 0xc1, 0x5b, 0x5b, 0x86, 0xa0,
+ 0xac, 0x59, 0x1c, 0xff, 0x28, 0x93, 0x00, 0x8a, 0x74, 0x98, 0x2e, 0xec,
+ 0x9d, 0x23, 0x13, 0xcf, 0xae, 0x98, 0x5f, 0x5e, 0xed, 0x6f, 0x19, 0xa6,
+ 0xce, 0xeb, 0x64, 0x89, 0x46, 0x55, 0x74, 0x3c, 0xb1, 0xfd, 0x0c, 0x80,
+ 0x43, 0xa2, 0x92, 0x5f, 0xb4, 0x7d, 0x35, 0x95, 0xdb, 0x69, 0x7a, 0xc0,
+ 0x5c, 0x51, 0x77, 0x0b, 0x9a, 0x68, 0x4f, 0xce, 0x82, 0xf4, 0x5f, 0x15,
+ 0xf2, 0x38, 0x45, 0x93, 0x0a, 0xf0, 0xab, 0xc0, 0x15, 0x9f, 0xa5, 0xcf,
+ 0xac, 0x97, 0x4d, 0x6e, 0xa7, 0xb6, 0x60, 0x75, 0x50, 0x96, 0x16, 0xf9,
+ 0x11, 0xa3, 0x0a, 0x7a, 0x60, 0x18, 0x37, 0x00, 0x4d, 0x70, 0x63, 0xf0,
+ 0x21, 0x26, 0x21, 0x01, 0xcf, 0xa2, 0x7e, 0x41, 0x38, 0x60, 0xdb, 0x7a,
+ 0x96, 0x5d, 0x90, 0xe8, 0x5d, 0xd7, 0xe1, 0x0f, 0x47, 0x1d, 0x60, 0x18,
+ 0x8d, 0x81, 0xb7, 0x24, 0x39, 0x3c, 0x5b, 0x54, 0xdd, 0x4e, 0xed, 0xb3,
+ 0xa6, 0xb0, 0xb0, 0x8d, 0x9d, 0x11, 0xcb, 0x81, 0x5d, 0xcd, 0x97, 0xe0,
+ 0xf2, 0x86, 0x4f, 0xa0, 0x8f, 0xc4, 0x18, 0xc5, 0x96, 0xfe, 0x12, 0x3a,
+ 0xac, 0x36, 0xe5, 0xee, 0xa4, 0x83, 0xb7, 0x5c, 0x7c, 0xc6, 0x3d, 0xa2,
+ 0x93, 0x72, 0xeb, 0xd0, 0x2b, 0x70, 0xa8, 0x7a, 0x44, 0xdc, 0x25, 0x8a,
+ 0xba, 0xcc, 0x32, 0xad, 0x98, 0xef, 0xab, 0x86, 0xcd, 0xdb, 0x23, 0xf0,
+ 0xf2, 0xf7, 0x78, 0x8a, 0x25, 0x38, 0x97, 0x5d, 0x8f, 0x88, 0xb7, 0x0b,
+ 0xaf, 0xc5, 0xe6, 0xf6, 0xa7, 0x18, 0xbf, 0x7d, 0x8c, 0x7f, 0x51, 0xb4,
+ 0x14, 0xe1, 0x03, 0x03, 0x02, 0xa6, 0x7a, 0xd6, 0xe1, 0xcd, 0xca, 0xc0,
+ 0x32, 0xa7, 0xc7, 0x2b, 0x3f, 0x71, 0x4a, 0x5e, 0x8a, 0x2c, 0x14, 0x3a,
+ 0x76, 0xe5, 0x50, 0x86, 0x62, 0xae, 0x17, 0x31, 0x96, 0x5e, 0x21, 0x90,
+ 0x00, 0xc8, 0x84, 0x41, 0x4a, 0x04, 0x06, 0x8b, 0x3d, 0xf5, 0xb2, 0x0c,
+ 0x47, 0xaf, 0x47, 0x55, 0x95, 0xa1, 0x40, 0x5a, 0xfe, 0x0e, 0x7f, 0x79,
+ 0x14, 0x7c, 0x67, 0xa2, 0x4c, 0xb8, 0xee, 0x36, 0x77, 0x52, 0x48, 0x2f,
+ 0x7d, 0x29, 0xb7, 0x1b, 0x65, 0x7e, 0xee, 0x20, 0xd6, 0x20, 0x9e, 0x77,
+ 0xeb, 0x9f, 0x58, 0xd9, 0xa5, 0x10, 0xe3, 0xf6, 0xd5, 0x3e, 0x33, 0xec,
+ 0xfd, 0x0a, 0xbb, 0x94, 0xb1, 0x14, 0x58, 0x1f, 0xae, 0x71, 0x4c, 0x4e,
+ 0x76, 0xbd, 0x1d, 0x05, 0x38, 0x8d, 0xf3, 0xa0, 0x43, 0xb1, 0x18, 0xd0,
+ 0xb8, 0xcf, 0xbf, 0x90, 0x2d, 0xf7, 0x91, 0xfd, 0xb6, 0x51, 0x3e, 0xd8,
+ 0x23, 0x40, 0x4d, 0x03, 0x64, 0x9c, 0x84, 0xa1, 0x80, 0x50, 0xb8, 0xcc,
+ 0x47, 0x82, 0x5d, 0xae, 0x4e, 0x39, 0x22, 0x64, 0xc3, 0x87, 0x5a, 0x55,
+ 0x46, 0x46, 0x8a, 0xbd, 0x63, 0xac, 0x96, 0x82, 0xc4, 0x2e, 0xbb, 0xf8,
+ 0x32, 0x74, 0x45, 0x56, 0xdb, 0x79, 0x78, 0xe9, 0xd1, 0xd6, 0x9d, 0x5f,
+ 0x57, 0x97, 0xa0, 0xee, 0x32, 0x09, 0xe8, 0x27, 0x02, 0xd5, 0xfe, 0xb1,
+ 0x88, 0xe6, 0x0a, 0xdb, 0x13, 0x16, 0x66, 0x0e, 0xe9, 0xa8, 0x39, 0x9c,
+ 0xcd, 0xe0, 0x55, 0xa3, 0x85, 0x11, 0x62, 0x12, 0xa0, 0xb2, 0xcc, 0xdb,
+ 0x83, 0x3c, 0xb9, 0x4f, 0x02, 0x7d, 0x03, 0x25, 0x09, 0x5f, 0x05, 0x64,
+ 0xb9, 0x1a, 0x54, 0xc9, 0x48, 0xc8, 0xde, 0x2e, 0xae, 0x13, 0x8e, 0x86,
+ 0x5c, 0xbb, 0x77, 0x95, 0xc4, 0x4d, 0xb7, 0x37, 0xbc, 0x1f, 0x08, 0x08,
+ 0xbb, 0xc7, 0x9d, 0x1c, 0x4c, 0x81, 0x72, 0xa8, 0x9e, 0x80, 0xba, 0xb0,
+ 0xe8, 0x37, 0x93, 0x85, 0xb5, 0x7d, 0x81, 0x04, 0xc0, 0xd8, 0x17, 0x2b,
+ 0x86, 0xbe, 0xeb, 0x91, 0x46, 0x01, 0x70, 0xc9, 0xa2, 0x35, 0xa9, 0x58,
+ 0x92, 0xe8, 0x3a, 0xc6, 0x5c, 0xf7, 0xc8, 0x0a, 0x85, 0x04, 0x49, 0x23,
+ 0x6c, 0xbf, 0xb4, 0x30, 0x2c, 0xf2, 0xe5, 0xc8, 0xbe, 0xc4, 0x97, 0xdd,
+ 0xeb, 0x18, 0xc5, 0xc7, 0xe7, 0x66, 0x1e, 0x75, 0xa1, 0x12, 0xea, 0xd0,
+ 0x57, 0xbc, 0x53, 0x01, 0x85, 0x85, 0x80, 0xac, 0x19, 0x5d, 0x1a, 0x00,
+ 0x42, 0x54, 0x23, 0xe1, 0x1d, 0x48, 0x5a, 0xcf, 0x00, 0xeb, 0x53, 0xc6,
+ 0x91, 0x66, 0xff, 0x5f, 0x26, 0x8e, 0xc3, 0x21, 0x29, 0xc4, 0xa6, 0xfa,
+ 0xc3, 0xa6, 0xf0, 0x45, 0x3a, 0x1e, 0x4e, 0x59, 0x7e, 0x4c, 0x1e, 0xec,
+ 0xc5, 0x49, 0x1f, 0x03, 0x3c, 0x1b, 0x77, 0xeb, 0x43, 0x9a, 0xc2, 0x09,
+ 0x59, 0xf8, 0x66, 0x57, 0x8a, 0x01, 0x40, 0x56, 0xfa, 0xcf, 0xb8, 0x24,
+ 0xc1, 0x7c, 0x41, 0xd0, 0xc2, 0x57, 0x61, 0xb6, 0xd9, 0x69, 0x21, 0x7e,
+ 0x2a, 0x04, 0xd8, 0xfc, 0x50, 0x52, 0x68, 0x53, 0x94, 0xe1, 0xde, 0x31,
+ 0xd5, 0xd6, 0xe7, 0x7c, 0xd7, 0x4d, 0x39, 0xab, 0x37, 0x8f, 0xa1, 0x2c,
+ 0x09, 0x39, 0xf2, 0x2f, 0x0f, 0xdd, 0x5b, 0xa2, 0x82, 0x5e, 0xd2, 0x7c,
+ 0x8e, 0x89, 0x8a, 0xfe, 0x9e, 0x6e, 0x77, 0xda, 0x9d, 0xe7, 0xc5, 0xd0,
+ 0x56, 0x17, 0xc2, 0xac, 0x9f, 0x92, 0x5d, 0xf4, 0x31, 0xea, 0x94, 0x4c,
+ 0xb7, 0x8c, 0xd5, 0x9d, 0xc4, 0xcb, 0xf2, 0x0f, 0x5a, 0xd0, 0x0b, 0x70,
+ 0x8c, 0x55, 0x2d, 0x8e, 0x84, 0x1f, 0x52, 0xd8, 0xdc, 0xd0, 0xa0, 0xe1,
+ 0x69, 0x4a, 0xcb, 0x29, 0x58, 0xd4, 0xd3, 0xcb, 0xa5, 0x4a, 0x97, 0x78,
+ 0xff, 0xf5, 0x83, 0x0b, 0x35, 0x50, 0x18, 0xcf, 0xa2, 0x70, 0x9a, 0xf8,
+ 0x7f, 0x73, 0x30, 0xd6, 0xba, 0xb8, 0x48, 0x14, 0x5d, 0x12, 0x11, 0x8f,
+ 0xdb, 0x23, 0x72, 0x0b, 0xfd, 0x73, 0x7a, 0xdb, 0xa2, 0xd4, 0x49, 0xb4,
+ 0x42, 0xa9, 0xb5, 0xd9, 0xcc, 0x06, 0x9c, 0x29, 0x7a, 0xdc, 0x46, 0xf9,
+ 0x03, 0x90, 0x90, 0x21, 0xb8, 0x9a, 0xb5, 0xf7, 0xb2, 0xfc, 0xe5, 0xcf,
+ 0xfd, 0x6d, 0x43, 0xd8, 0x53, 0x6f, 0x0e, 0xe1, 0x3e, 0xe0, 0x3c, 0x65,
+ 0xdd, 0xb0, 0xba, 0x65, 0x6e, 0xc2, 0x96, 0x01, 0x7b, 0x5a, 0xf1, 0x5e,
+ 0x0b, 0x44, 0x4a, 0x94, 0xfc, 0x93, 0x77, 0x64, 0x57, 0xef, 0xb3, 0xd2,
+ 0xc2, 0xc8, 0xb5, 0xa8, 0x27, 0xc6, 0xa5, 0x37, 0x88, 0x24, 0xf7, 0xd9,
+ 0xf9, 0x03, 0xfd, 0x3d, 0x8e, 0x6b, 0x6d, 0xd8, 0x31, 0x11, 0xd8, 0xb9,
+ 0x38, 0xc6, 0x0a, 0x6a, 0xa2, 0x4e, 0x84, 0x87, 0x07, 0xeb, 0x18, 0x8e,
+ 0xc7, 0x48, 0x0a, 0xd0, 0xe7, 0x86, 0xab, 0x29, 0xd5, 0xca, 0x7f, 0xe5,
+ 0xcd, 0x5f, 0xb1, 0xc6, 0x83, 0x6a, 0x32, 0xa7, 0xbb, 0xbd, 0xb8, 0x28,
+ 0x94, 0x08, 0xd7, 0x26, 0x3b, 0x3a, 0xdb, 0xd5, 0xfe, 0x7d, 0x1d, 0x05,
+ 0x14, 0x58, 0x43, 0xac, 0x07, 0x17, 0x7b, 0x2c, 0x9d, 0x35, 0xc8, 0x71,
+ 0xb6, 0x69, 0x2c, 0x15, 0xce, 0xd1, 0x6f, 0x67, 0x4d, 0x01, 0x49, 0xee,
+ 0x15, 0x0d, 0xce, 0xfb, 0x8d, 0x0f, 0xcd, 0xc9, 0xd3, 0x54, 0x41, 0x04,
+ 0xb4, 0xc9, 0xe5, 0x38, 0xf5, 0x1b, 0xed, 0x52, 0x55, 0xea, 0x69, 0x68,
+ 0x64, 0x96, 0x14, 0x2f, 0xa1, 0x0d, 0xd9, 0xe5, 0x4b, 0x60, 0x42, 0x8a,
+ 0xe7, 0xfb, 0x4f, 0xd7, 0x45, 0xa8, 0x11, 0x0c, 0xd0, 0xb8, 0x54, 0x98,
+ 0x87, 0x1c, 0x0a, 0xcb, 0x3d, 0x29, 0x4d, 0x3f, 0x9e, 0xd5, 0x1e, 0x07,
+ 0x68, 0xc2, 0x8e, 0xc4, 0x38, 0x68, 0x83, 0x07, 0x35, 0x45, 0x2c, 0x29,
+ 0xce, 0x4a, 0xf6, 0xc2, 0xe6, 0xb4, 0x14, 0xd1, 0x42, 0x66, 0x38, 0x48,
+ 0x02, 0x17, 0x11, 0x29, 0xa1, 0x7f, 0x4d, 0x4b, 0x03, 0x1f, 0x27, 0xb0,
+ 0x80, 0x99, 0xba, 0x94, 0x3e, 0xa1, 0x08, 0xd0, 0x78, 0x52, 0x64, 0x35,
+ 0x92, 0x77, 0xaa, 0xc3, 0x86, 0x7d, 0x9d, 0xcc, 0xd9, 0xb2, 0xf1, 0xa0,
+ 0x5f, 0x6e, 0x15, 0x9f, 0x9d, 0x74, 0x29, 0x8c, 0xce, 0x4c, 0x26, 0xac,
+ 0x65, 0xd4, 0x9f, 0x5b, 0x23, 0x3d, 0x4e, 0x0b, 0x44, 0xe1, 0x6e, 0xb4,
+ 0x8c, 0xc2, 0xa1, 0x95, 0x22, 0x67, 0x5e, 0x21, 0x02, 0x89, 0x48, 0x90,
+ 0x9e, 0x29, 0x23, 0x4f, 0xbb, 0xbb, 0x42, 0x9e, 0x9f, 0xd4, 0xb4, 0x18,
+ 0x45, 0xf8, 0xae, 0x8d, 0x13, 0x83, 0x1b, 0x93, 0x15, 0x89, 0xf2, 0x50,
+ 0x5b, 0x7d, 0x3e, 0x70, 0x32, 0xac, 0x26, 0x87, 0xa8, 0x77, 0x7a, 0x9e,
+ 0xe3, 0xcf, 0xce, 0xc7, 0x93, 0xcc, 0x4e, 0x14, 0x1c, 0xa5, 0xe0, 0x4a,
+ 0xaa, 0x52, 0x5f, 0xbb, 0x1f, 0xec, 0xca, 0x6e, 0x8c, 0x97, 0xd4, 0xb7,
+ 0x10, 0xf1, 0xee, 0x22, 0x84, 0x8a, 0x37, 0xe3, 0xec, 0xb4, 0x4d, 0xa3,
+ 0xbf, 0x6e, 0xd3, 0x1e, 0x3d, 0x92, 0x0a, 0x59, 0x0e, 0x66, 0x38, 0xf3,
+ 0xd9, 0x49, 0xbd, 0x33, 0x7b, 0x87, 0x86, 0x95, 0x10, 0xe7, 0xf6, 0x2d,
+ 0xd5, 0x3f, 0x09, 0x56, 0xf1, 0xeb, 0xed, 0x34, 0x97, 0xa2, 0x49, 0xd2,
+ 0xa0, 0x05, 0xa3, 0x20, 0xc6, 0x79, 0xec, 0x69, 0x2c, 0x16, 0x0f, 0x5b,
+ 0x6d, 0x6c, 0x82, 0x51, 0x51, 0x07, 0xdf, 0xfc, 0x65, 0xc0, 0x15, 0x29,
+ 0x72, 0xc8, 0x40, 0xe4, 0x3e, 0x31, 0xba, 0x6b, 0x6d, 0x7e, 0xf7, 0x4b,
+ 0xaa, 0x8b, 0x87, 0xe1, 0x30, 0xe6, 0x93, 0xb4, 0x0f, 0x1a, 0xcd, 0x36,
+ 0x94, 0xa2, 0xf1, 0x5e, 0x7f, 0x7f, 0x42, 0x75, 0x61, 0xa0, 0xb6, 0xda,
+ 0xa8, 0xa1, 0x8f, 0x40, 0x5e, 0xfe, 0x2c, 0x1b, 0x21, 0xb7, 0x92, 0xc6,
+ 0x9b, 0x9c, 0xe1, 0x6b, 0x63, 0x0e, 0x49, 0x09, 0x69, 0x57, 0x07, 0x11,
+ 0x6d, 0x5e, 0x07, 0x9a, 0x6c, 0xfa, 0x39, 0xc1, 0x04, 0x29, 0x86, 0x92,
+ 0x51, 0x1d, 0x59, 0xa8, 0x00, 0xc3, 0x85, 0x7d, 0xce, 0x06, 0x2b, 0x8a,
+ 0x3c, 0x02, 0x58, 0x32, 0x28, 0x2b, 0x2f, 0x9b, 0x12, 0x08, 0x15, 0x47,
+ 0x32, 0x73, 0xf4, 0x95, 0x27, 0x34, 0xe4, 0xd6, 0x70, 0xb4, 0x69, 0x1f,
+ 0x30, 0xb9, 0x71, 0x37, 0xe4, 0x48, 0x27, 0xd2, 0x39, 0xb4, 0x8d, 0x17,
+ 0x4d, 0xd4, 0x31, 0xa5, 0x95, 0x8c, 0x77, 0xaa, 0x5c, 0x7d, 0x9d, 0x10,
+ 0x37, 0xb8, 0xc7, 0x04, 0x27, 0xad, 0x0f, 0x11, 0x9e, 0x7a, 0x07, 0xad,
+ 0x8d, 0x54, 0x95, 0x4a, 0x8e, 0xf4, 0x8d, 0x92, 0x8f, 0x2c, 0x9d, 0x93,
+ 0x7a, 0xdd, 0x50, 0x38, 0x3f, 0xe2, 0xa3, 0x37, 0xd5, 0xb8, 0xa5, 0x90,
+ 0x14, 0x2b, 0x2f, 0x16, 0x2d, 0x0f, 0xbc, 0x56, 0xd8, 0x3c, 0xf5, 0x7f,
+ 0xf5, 0x77, 0xf1, 0x34, 0x5a, 0x74, 0x71, 0xad, 0x67, 0xae, 0xa3, 0x6b,
+ 0x39, 0x5f, 0xdb, 0xc0, 0xcb, 0x3e, 0xd8, 0x83, 0x21, 0x8b, 0x4b, 0x1c,
+ 0x5a, 0xf2, 0xb5, 0xc8, 0x7a, 0x58, 0x67, 0xf1, 0x52, 0x3a, 0x5b, 0x49,
+ 0xc3, 0x57, 0xa6, 0x58, 0xc6, 0x92, 0xa2, 0xac, 0xe0, 0xde, 0x6a, 0x28,
+ 0xc8, 0xb9, 0x07, 0x30, 0x58, 0x82, 0x64, 0x3a, 0x52, 0x5a, 0x08, 0xfe,
+ 0x43, 0xe3, 0xda, 0x1a, 0xa8, 0xce, 0x4d, 0x4b, 0xcc, 0x63, 0x60, 0x97,
+ 0x6b, 0xb2, 0xbd, 0x26, 0xa9, 0x08, 0x5e, 0xec, 0x4e, 0xaa, 0xd9, 0x54,
+ 0x76, 0x3c, 0x57, 0x2a, 0x15, 0x92, 0x87, 0xc9, 0xe3, 0xfa, 0x14, 0xa3,
+ 0x9b, 0xb7, 0x3b, 0xd1, 0x1d, 0x42, 0x5a, 0x98, 0x98, 0x67, 0x41, 0x17,
+ 0x8b, 0x96, 0x64, 0x07, 0xb8, 0x34, 0x32, 0x25, 0x20, 0x2a, 0x6d, 0x4b,
+ 0x34, 0x53, 0x4b, 0x28, 0x7b, 0x84, 0xbf, 0xc7, 0xcf, 0xee, 0x7f, 0x41,
+ 0x3c, 0xef, 0xca, 0x55, 0xfc, 0x82, 0x65, 0xe2, 0x28, 0x31, 0xcb, 0x24,
+ 0x8c, 0x0f, 0x1d, 0x40, 0x5d, 0xef, 0x79, 0xb3, 0x03, 0xdc, 0x8f, 0x1f,
+ 0x8e, 0xcb, 0xd7, 0x61, 0xfc, 0xf3, 0x41, 0x01, 0x6b, 0x20, 0xf5, 0x7f,
+ 0xc4, 0x85, 0x6c, 0x06, 0xf7, 0xbd, 0x50, 0xb1, 0xe0, 0x91, 0xee, 0x98,
+ 0xf4, 0x30, 0x7c, 0x3c, 0x2c, 0x5e, 0xb2, 0x66, 0xb9, 0xc6, 0x13, 0x24,
+ 0x36, 0x55, 0xcf, 0xa7, 0xb0, 0xfa, 0x16, 0x2c, 0x3c, 0x18, 0x21, 0xfe,
+ 0xe0, 0x28, 0xbe, 0x67, 0x05, 0x45, 0x7f, 0xcf, 0x64, 0x55, 0xdf, 0x59,
+ 0x0b, 0x68, 0x58, 0x41, 0x9e, 0xf0, 0xf4, 0x74, 0x52, 0x32, 0x51, 0x5a,
+ 0xa2, 0xa9, 0xc6, 0xdb, 0x9e, 0x52, 0x29, 0x67, 0xec, 0xe7, 0xeb, 0x12,
+ 0x06, 0x0d, 0x22, 0xc7, 0xfa, 0xda, 0x5e, 0x5f, 0xf4, 0x16, 0x11, 0xf0,
+ 0x61, 0x4d, 0x84, 0xe5, 0x77, 0x3e, 0x17, 0x0f, 0xcc, 0xc6, 0xcd, 0x13,
+ 0x69, 0x36, 0x99, 0x4f, 0x79, 0x1c, 0x67, 0x91, 0xa8, 0xd9, 0xf1, 0x17,
+ 0x13, 0xaa, 0x23, 0x89, 0x8e, 0x85, 0xb5, 0x01, 0x55, 0x35, 0x41, 0xf5,
+ 0xa2, 0x39, 0x8c, 0xa1, 0xac, 0x79, 0xcd, 0xf6, 0x27, 0xe5, 0x68, 0xa9,
+ 0x35, 0x77, 0xfc, 0x5a, 0x0c, 0x7d, 0x19, 0x85, 0x90, 0xee, 0x52, 0xaf,
+ 0x96, 0x3b, 0x15, 0xfb, 0xa7, 0xa1, 0x3f, 0xcc, 0xaa, 0x8e, 0x8d, 0x7e,
+ 0x30, 0x93, 0xe8, 0x5d, 0xa1, 0x7c, 0x11, 0x57, 0xb9, 0xf8, 0x26, 0x20,
+ 0xe6, 0xb0, 0xcf, 0xff, 0xe7, 0x8e, 0x95, 0xcb, 0x5d, 0x0d, 0x1b, 0xf4,
+ 0xa9, 0x09, 0xb0, 0x07, 0x1d, 0x77, 0x46, 0x5d, 0xcd, 0xb5, 0x28, 0x1a,
+ 0xd7, 0xe4, 0x7e, 0x02, 0xee, 0xcd, 0x66, 0x1f, 0x6f, 0x90, 0xdd, 0x4a,
+ 0xe6, 0x66, 0xaf, 0xa9, 0x25, 0xbe, 0xaf, 0xce, 0xea, 0xfa, 0x84, 0x26,
+ 0x54, 0x76, 0x8c, 0x8a, 0x25, 0xee, 0x8d, 0xf7, 0x25, 0xb6, 0x2f, 0x4e,
+ 0xdb, 0x46, 0x8d, 0xf2, 0xf9, 0xfe, 0xad, 0xae, 0x4a, 0x29, 0x85, 0x38,
+ 0x7a, 0xea, 0xf9, 0x8d, 0x15, 0x70, 0x10, 0xf5, 0x12, 0xbc, 0x9a, 0xb1,
+ 0x27, 0x9a, 0xb0, 0xe7, 0x2f, 0xe4, 0x3e, 0x91, 0x52, 0x3b, 0xed, 0x44,
+ 0xcc, 0xfe, 0xca, 0xc7, 0xa7, 0xf6, 0xac, 0xce, 0xba, 0x8c, 0x4f, 0xad,
+ 0x6d, 0xca, 0x20, 0x33, 0xeb, 0xfc, 0xd6, 0xe5, 0x5c, 0xb8, 0x33, 0x71,
+ 0x2b, 0x5d, 0x3f, 0x66, 0xf5, 0x5a, 0x0d, 0xa6, 0x71, 0x2c, 0x55, 0xf8,
+ 0xb5, 0x77, 0x79, 0xeb, 0xfd, 0x31, 0xea, 0xcb, 0xae, 0xfc, 0xb2, 0x09,
+ 0x6f, 0x8e, 0x3d, 0xa0, 0x7e, 0x9d, 0x75, 0x1f, 0x70, 0x93, 0x38, 0xb4,
+ 0x28, 0xa2, 0xce, 0x7b, 0x6a, 0x92, 0x74, 0xf5, 0xcf, 0xd4, 0x80, 0x9a,
+ 0x46, 0x07, 0x49, 0x1c, 0x46, 0x42, 0xbb, 0x17, 0x38, 0x51, 0xc0, 0x86,
+ 0xa4, 0x44, 0xc4, 0x6d, 0xf5, 0x6c, 0x03, 0x6a, 0xc1, 0x52, 0x34, 0xa6,
+ 0x87, 0x1a, 0x66, 0xb1, 0x90, 0xcc, 0x7f, 0x5f, 0x14, 0xc4, 0xab, 0xcc,
+ 0xa7, 0xdf, 0x81, 0x3f, 0x98, 0x94, 0x64, 0xfb, 0xc1, 0xb3, 0x04, 0x2c,
+ 0xb2, 0xf1, 0x00, 0xaf, 0xfc, 0x76, 0x7f, 0x95, 0x2a, 0x14, 0x4b, 0x52,
+ 0x47, 0x0f, 0xeb, 0xee, 0xda, 0x75, 0x0a, 0x31, 0x87, 0x1f, 0x3b, 0xa7,
+ 0x9f, 0x14, 0xcf, 0x2a, 0x43, 0xd2, 0x63, 0x8c, 0xff, 0x42, 0xea, 0x03,
+ 0x21, 0x2c, 0xfc, 0x96, 0x49, 0x98, 0x84, 0x9f, 0x91, 0x9a, 0xd7, 0x3f,
+ 0x95, 0x27, 0xf9, 0x3c, 0x82, 0xc9, 0x48, 0x66, 0xba, 0xb0, 0x90, 0xc2,
+ 0x31, 0xde, 0xa3, 0x14, 0x18, 0x4b, 0xf6, 0x6d, 0x62, 0x95, 0x16, 0xe7,
+ 0x84, 0x63, 0xa4, 0xec, 0xd7, 0x74, 0x65, 0xf7, 0x3a, 0x48, 0x1f, 0xf3,
+ 0x92, 0xf3, 0x3f, 0x5a, 0xea, 0x8f, 0x82, 0xb9, 0x39, 0x44, 0xdb, 0x71,
+ 0x73, 0x21, 0x9b, 0x53, 0xb2, 0x14, 0x07, 0x15, 0x04, 0x9a, 0x55, 0x17,
+ 0xc3, 0xdf, 0x7a, 0x2d, 0xc4, 0x7e, 0xa6, 0x0f, 0xb7, 0xf1, 0x47, 0xb9,
+ 0x62, 0x0d, 0xb1, 0x92, 0xf0, 0xde, 0x1d, 0xfe, 0xed, 0x90, 0xb3, 0x00,
+ 0x02, 0x1b, 0x0f, 0x22, 0x0a, 0x3e, 0x9f, 0xbb, 0x94, 0x55, 0xdf, 0x84,
+ 0x97, 0xe4, 0x03, 0x17, 0xc0, 0x5d, 0x5b, 0x89, 0x7d, 0x46, 0xf9, 0x15,
+ 0xdc, 0x76, 0x69, 0xfc, 0x98, 0xc9, 0x78, 0xbd, 0x9d, 0x85, 0xb4, 0xec,
+ 0x55, 0xbe, 0x5d, 0x3a, 0xc9, 0xc5, 0xf3, 0x0b, 0xb0, 0x3b, 0xf0, 0x38,
+ 0x94, 0x4c, 0x89, 0x4c, 0x9a, 0xc6, 0xd1, 0xe9, 0xe2, 0x8c, 0xde, 0xcf,
+ 0x46, 0x43, 0x25, 0x51, 0x9e, 0xf2, 0xa8, 0x72, 0xad, 0x11, 0xfe, 0xb4,
+ 0x44, 0x05, 0x3e, 0x06, 0x53, 0xb6, 0xb0, 0xe6, 0x3b, 0x71, 0x4f, 0x7c,
+ 0x08, 0x4d, 0xbe, 0xab, 0x42, 0xd6, 0xec, 0x7c, 0x6f, 0xae, 0x44, 0x63,
+ 0x94, 0xc9, 0x82, 0x09, 0x60, 0x54, 0xc6, 0x4a, 0x35, 0x27, 0x72, 0x1c,
+ 0x74, 0xb9, 0xc7, 0x4a, 0xc8, 0xe9, 0x47, 0x73, 0x65, 0xbf, 0x66, 0x60,
+ 0xc1, 0x1d, 0xc7, 0xbd, 0xe2, 0xdc, 0x44, 0xf1, 0xf9, 0xd3, 0x8c, 0x48,
+ 0x69, 0xc0, 0xf9, 0x02, 0xe5, 0xff, 0xbc, 0x5e, 0x1d, 0x66, 0xb7, 0x5b,
+ 0xbc, 0x95, 0x4c, 0xaa, 0x29, 0x1c, 0x53, 0x84, 0x6d, 0xbe, 0x0f, 0xda,
+ 0x40, 0x72, 0x5f, 0x0d, 0xe7, 0xae, 0xe3, 0x05, 0xa2, 0x16, 0x4f, 0x3d,
+ 0x4b, 0xbc, 0x84, 0x4c, 0xcf, 0xa5, 0x10, 0x72, 0xf1, 0x7e, 0x2c, 0xfa,
+ 0x48, 0x45, 0xb6, 0xa7, 0x57, 0xa7, 0xeb, 0x69, 0x54, 0xc0, 0x96, 0x9b,
+ 0xa5, 0xf7, 0x06, 0x3f, 0xdf, 0xe9, 0x78, 0x09, 0x24, 0x6e, 0x41, 0x49,
+ 0x81, 0x22, 0x11, 0x4e, 0xcd, 0xda, 0xfb, 0x92, 0x99, 0xe4, 0xd8, 0x86,
+ 0x8e, 0xe4, 0x0b, 0xdc, 0xa2, 0x7e, 0x8d, 0x99, 0x45, 0xed, 0x24, 0x40,
+ 0x2b, 0x34, 0x72, 0x4c, 0x27, 0x8e, 0xd0, 0x3d, 0x1a, 0x7c, 0x6a, 0x85,
+ 0xa7, 0x1d, 0x71, 0x05, 0xe7, 0xda, 0xf4, 0x48, 0xd6, 0x87, 0x0a, 0xb8,
+ 0xc0, 0x55, 0xe3, 0xed, 0xbb, 0xfd, 0x82, 0x42, 0xfa, 0x68, 0x55, 0x73,
+ 0x66, 0x93, 0x81, 0x6b, 0x33, 0xfe, 0xa3, 0x15, 0xf3, 0x92, 0x16, 0xcd,
+ 0x4a, 0xf3, 0x67, 0x26, 0x96, 0x1b, 0x15, 0x68, 0x20, 0x31, 0xfe, 0xc7,
+ 0x93, 0x67, 0x23, 0xeb, 0xa9, 0x8e, 0x79, 0xe2, 0xa0, 0x9a, 0xf1, 0x69,
+ 0xd1, 0x31, 0x61, 0x6d, 0x0b, 0x5e, 0xd7, 0x59, 0xac, 0xa3, 0x4a, 0x9e,
+ 0xff, 0x69, 0x24, 0xc1, 0x03, 0x76, 0x73, 0x27, 0x33, 0xce, 0xbd, 0x06,
+ 0xca, 0xbc, 0xf4, 0x4d, 0x6d, 0x6e, 0x48, 0x0a, 0x5b, 0x4f, 0xcd, 0xa0,
+ 0x06, 0xb2, 0xf6, 0x88, 0x06, 0x5e, 0xf5, 0x4d, 0xb5, 0x27, 0x6d, 0xd3,
+ 0x87, 0xa7, 0xc6, 0x49, 0xef, 0x52, 0x69, 0x98, 0x18, 0xf9, 0x07, 0x9b,
+ 0x72, 0x11, 0x87, 0x8d, 0x43, 0xb9, 0x2b, 0x07, 0xb1, 0x51, 0x4b, 0x91,
+ 0x52, 0x39, 0x96, 0x3f, 0x63, 0x83, 0x82, 0xa1, 0xbc, 0x27, 0x99, 0x77,
+ 0xff, 0xc8, 0xa1, 0x07, 0xd0, 0x87, 0x2a, 0x50, 0x56, 0x65, 0x64, 0x41,
+ 0x3f, 0x18, 0x95, 0xb1, 0xf2, 0xf1, 0xa8, 0x51, 0x72, 0x46, 0xd6, 0x49,
+ 0x96, 0xee, 0xbf, 0x11, 0xcf, 0xe6, 0x81, 0x8d, 0x3c, 0xc5, 0xf6, 0x1c,
+ 0xac, 0xf2, 0x37, 0x75, 0x2e, 0x53, 0xe2, 0xee, 0x54, 0x3b, 0x46, 0x7f,
+ 0x8e, 0x15, 0x46, 0xc0, 0x12, 0xf2, 0xf5, 0xe0, 0xa6, 0x48, 0x23, 0x95,
+ 0x64, 0x43, 0xe4, 0x76, 0x7d, 0x2b, 0x2b, 0xe7, 0x3d, 0x0a, 0x84, 0x7d,
+ 0x18, 0xf7, 0x70, 0x0d, 0x3d, 0x2b, 0xf7, 0xa7, 0x08, 0x5e, 0x28, 0x59,
+ 0xaa, 0x62, 0x6f, 0x56, 0x1c, 0x74, 0x8f, 0x57, 0x33, 0xb6, 0xf2, 0x2f,
+ 0x49, 0x18, 0x67, 0xb3, 0x93, 0x5f, 0x45, 0xcc, 0xb7, 0xa9, 0x2b, 0x16,
+ 0x5d, 0x4b, 0x6b, 0x98, 0xf2, 0x07, 0xe2, 0x60, 0xe9, 0x30, 0x8e, 0x29,
+ 0x7c, 0xb6, 0xca, 0x31, 0xfd, 0x3a, 0xff, 0x06, 0x06, 0x10, 0x0c, 0x4a,
+ 0x7c, 0x3c, 0x97, 0x10, 0x4f, 0x4d, 0x86, 0x2d, 0x8b, 0x66, 0x49, 0xd9,
+ 0x73, 0x49, 0x93, 0x7f, 0x0d, 0x95, 0x83, 0x7f, 0x0b, 0x85, 0x53, 0x85,
+ 0xf9, 0x3f, 0x29, 0x7f, 0x53, 0xd5, 0x6f, 0xe7, 0xf6, 0xf2, 0xec, 0x5b,
+ 0xb5, 0x86, 0x4f, 0xb5, 0xd5, 0x11, 0x18, 0xde, 0x35, 0xe8, 0x3d, 0x79,
+ 0x81, 0x8f, 0xa2, 0x05, 0x58, 0x40, 0x0d, 0x9b, 0x77, 0x22, 0x38, 0x06,
+ 0x5b, 0xac, 0xf7, 0xd0, 0x0e, 0x06, 0x06, 0x23, 0xd2, 0xff, 0xa0, 0xcb,
+ 0xff, 0x84, 0xb5, 0xf3, 0x7a, 0xe1, 0x8a, 0x76, 0xcd, 0x3b, 0x0b, 0xbc,
+ 0x23, 0x46, 0xb0, 0xc7, 0xfa, 0xa8, 0xd6, 0xf6, 0x56, 0x57, 0x72, 0xd2,
+ 0x49, 0x95, 0x6e, 0x32, 0x2e, 0x00, 0x3d, 0x0b, 0xab, 0x36, 0x05, 0x6e,
+ 0xc5, 0x11, 0x4a, 0xb5, 0x1c, 0x89, 0xb2, 0x81, 0xc8, 0x0b, 0x9f, 0x6a,
+ 0x9f, 0x0f, 0x75, 0x22, 0x79, 0x3c, 0xb7, 0xa6, 0xa1, 0x24, 0x02, 0xcc,
+ 0x98, 0x89, 0x27, 0x8d, 0xdc, 0xc4, 0xa9, 0x4a, 0xb5, 0x87, 0xb4, 0x5c,
+ 0x8c, 0x16, 0x2f, 0x63, 0x1a, 0x27, 0x68, 0xf4, 0x25, 0x9e, 0xa1, 0x6c,
+ 0x9e, 0x36, 0xc6, 0xc6, 0xa5, 0xab, 0x0e, 0x26, 0x22, 0x51, 0xc3, 0x8d,
+ 0xc8, 0xa2, 0x58, 0x27, 0x03, 0xe5, 0xe4, 0x72, 0xc2, 0x79, 0x4d, 0xa0,
+ 0xcb, 0x19, 0x37, 0x99, 0xca, 0x21, 0x67, 0x8c, 0xdf, 0x7c, 0xce, 0x8f,
+ 0xd6, 0xba, 0x78, 0x54, 0xda, 0xb4, 0x90, 0x70, 0xe1, 0x78, 0x81, 0x03,
+ 0x64, 0x28, 0xcc, 0x43, 0x31, 0xd7, 0xa6, 0xf6, 0xea, 0x34, 0x2c, 0x3e,
+ 0x24, 0xbc, 0x7d, 0x5c, 0x3a, 0x5b, 0x3e, 0x61, 0xdc, 0x32, 0x23, 0x30,
+ 0x64, 0x2f, 0x51, 0x15, 0x43, 0x04, 0xdd, 0xa2, 0xa2, 0x2c, 0x66, 0x94,
+ 0x9a, 0xb4, 0x6c, 0x7b, 0x99, 0x2e, 0xc3, 0x57, 0x6a, 0xaf, 0xe6, 0x61,
+ 0xdc, 0x66, 0x76, 0x8c, 0x36, 0xe2, 0x41, 0xe9, 0xea, 0xf3, 0x97, 0x68,
+ 0x8d, 0x22, 0x94, 0x93, 0x97, 0xb3, 0xc4, 0xd3, 0xfd, 0x25, 0x47, 0xb5,
+ 0x7c, 0x53, 0xb6, 0x23, 0xdd, 0xd0, 0x2f, 0x50, 0xf2, 0x20, 0xc7, 0xd1,
+ 0xf8, 0xca, 0xe8, 0xbf, 0xb8, 0x17, 0xff, 0xa9, 0xef, 0x07, 0x73, 0xcf,
+ 0xd0, 0x86, 0xb5, 0xae, 0xc9, 0x05, 0x86, 0x69, 0xd5, 0xf8, 0x8f, 0x3c,
+ 0xfe, 0xa6, 0xb7, 0x82, 0x6c, 0x63, 0x91, 0x18, 0xc6, 0x08, 0x19, 0xf5,
+ 0xa1, 0xe4, 0x12, 0xd7, 0x74, 0x94, 0x49, 0x8e, 0xa1, 0xcf, 0xd0, 0x2e,
+ 0x86, 0x98, 0xc3, 0x33, 0x16, 0x1b, 0x20, 0x13, 0x78, 0x58, 0x7e, 0x17,
+ 0x73, 0x91, 0x1c, 0x9a, 0xdf, 0x81, 0xd3, 0x2c, 0x46, 0xe8, 0x50, 0xaf,
+ 0x7f, 0x86, 0x9a, 0xaa, 0x1a, 0x09, 0xe6, 0x92, 0xf0, 0x1c, 0xdd, 0x78,
+ 0xc4, 0x29, 0xdf, 0x6d, 0xcc, 0x4c, 0x5a, 0x2f, 0xc1, 0xa5, 0x76, 0xcc,
+ 0x05, 0xed, 0xf3, 0xd6, 0xf9, 0x23, 0x45, 0x58, 0xa3, 0x5f, 0x4a, 0x25,
+ 0xcd, 0x4a, 0x84, 0xfc, 0x1a, 0xfb, 0xd6, 0xe2, 0xe3, 0x70, 0xf1, 0x31,
+ 0xea, 0x20, 0x56, 0x24, 0x99, 0xd3, 0xc9, 0x89, 0xcc, 0x2e, 0xd5, 0x22,
+ 0x57, 0xec, 0x58, 0xbb, 0x83, 0xae, 0x19, 0x15, 0xf5, 0x8c, 0xa0, 0xd7,
+ 0xaa, 0x1c, 0x79, 0x83, 0xdc, 0x29, 0xae, 0xd1, 0xfd, 0x60, 0x21, 0xb3,
+ 0xbf, 0x1b, 0x6f, 0x05, 0xb6, 0x9c, 0xf2, 0xa2, 0x98, 0xe2, 0xc5, 0x87,
+ 0xe8, 0xa8, 0xe4, 0x2c, 0x1d, 0x4f, 0x91, 0xde, 0xdf, 0x2c, 0x5b, 0x11,
+ 0x2d, 0x91, 0x3a, 0xa8, 0xc0, 0xf7, 0xc2, 0xd2, 0x77, 0x05, 0xe1, 0xfa,
+ 0x85, 0x30, 0x9d, 0xda, 0xf0, 0x6c, 0x10, 0xa2, 0xf5, 0x47, 0xcc, 0x29,
+ 0xae, 0xdf, 0xc1, 0x20, 0xfa, 0x1f, 0xf3, 0xae, 0xe4, 0x00, 0x38, 0xc8,
+ 0x0c, 0x18, 0x6f, 0x08, 0xcf, 0xc3, 0xe0, 0x3e, 0x01, 0x9d, 0x79, 0xa7,
+ 0xa6, 0xe2, 0x0a, 0x82, 0x58, 0xa4, 0x11, 0x48, 0xb5, 0x9b, 0xe2, 0x4b,
+ 0x42, 0x09, 0x17, 0x20, 0x09, 0xc3, 0x0e, 0xe7, 0x0f, 0xe4, 0xcd, 0x62,
+ 0x26, 0x7c, 0x12, 0xe7, 0x5b, 0x1e, 0xc4, 0xaa, 0x5d, 0x92, 0x0f, 0xbb,
+ 0x8c, 0x39, 0xfc, 0x57, 0x28, 0x75, 0x84, 0xf2, 0xa1, 0xb9, 0xa1, 0x38,
+ 0xde, 0xfc, 0xbc, 0x84, 0x96, 0x86, 0x6a, 0xdb, 0x0e, 0x87, 0x89, 0x6f,
+ 0x98, 0xb5, 0x83, 0x8d, 0x01, 0xaf, 0xf1, 0xd9, 0xd9, 0x0e, 0x04, 0xcd,
+ 0x5e, 0xda, 0x0a, 0xd6, 0x48, 0x7a, 0xc5, 0xaa, 0x7c, 0xcc, 0x4c, 0x20,
+ 0x48, 0xe4, 0xb8, 0x44, 0xca, 0x50, 0xa5, 0x9f, 0xc7, 0x27, 0xed, 0x1b,
+ 0x40, 0x2b, 0xdd, 0x1a, 0xae, 0xe1, 0x8d, 0xe0, 0x26, 0x31, 0xec, 0x2b,
+ 0xb6, 0xc6, 0xda, 0x43, 0x52, 0xc4, 0xfa, 0xc2, 0x35, 0x39, 0x2c, 0x63,
+ 0xef, 0x2b, 0x42, 0xc4, 0xfa, 0x53, 0x8c, 0x7d, 0xcd, 0x85, 0x84, 0x44,
+ 0x98, 0x17, 0x08, 0x63, 0x1f, 0xb9, 0xc8, 0x1e, 0x33, 0x1f, 0xf1, 0xab,
+ 0x83, 0x97, 0x22, 0x62, 0x22, 0x57, 0x7d, 0x8d, 0xa6, 0x41, 0x77, 0x38,
+ 0xdf, 0x5f, 0x45, 0xa1, 0xbb, 0x49, 0x82, 0x6e, 0x0a, 0xf9, 0x94, 0x00,
+ 0xea, 0x6f, 0x4a, 0xe8, 0x16, 0x4b, 0x78, 0x33, 0x26, 0x18, 0x3b, 0x3b,
+ 0x5f, 0x28, 0x58, 0x3f, 0xa7, 0x4e, 0xdc, 0x21, 0x31, 0xd4, 0x3b, 0xec,
+ 0x71, 0xf1, 0xd5, 0xb5, 0xa1, 0x0f, 0x91, 0xc9, 0xc4, 0xd3, 0xca, 0xbd,
+ 0x39, 0xd2, 0xef, 0x21, 0xdf, 0xba, 0xd9, 0xce, 0x65, 0xdd, 0x1a, 0xb5,
+ 0x66, 0xba, 0x7a, 0x80, 0x37, 0x68, 0x32, 0xc1, 0xb4, 0x2b, 0x4b, 0x44,
+ 0xc2, 0x35, 0x0e, 0x7e, 0xd9, 0x71, 0x93, 0xb9, 0xac, 0x6b, 0x7e, 0x63,
+ 0x9c, 0xa0, 0xab, 0x10, 0x7c, 0x62, 0xe8, 0xfc, 0x1c, 0x62, 0x79, 0x09,
+ 0x70, 0xa8, 0xe5, 0x67, 0x1f, 0x5b, 0xc0, 0x5c, 0x87, 0x1a, 0x90, 0x24,
+ 0x0c, 0xb4, 0x3c, 0x8d, 0xe4, 0xf6, 0x44, 0x62, 0xd0, 0x55, 0x9e, 0x05,
+ 0x75, 0xa3, 0xe2, 0xac, 0x10, 0x11, 0x7f, 0x5b, 0x96, 0x65, 0x38, 0xa7,
+ 0x2a, 0x0f, 0xbe, 0x5e, 0x01, 0x14, 0xb4, 0x8a, 0xa1, 0xa8, 0xe8, 0x5a,
+ 0xbc, 0x4b, 0x22, 0x67, 0xd6, 0x14, 0xc3, 0x60, 0xf2, 0x12, 0xd9, 0x6f,
+ 0x1a, 0x6d, 0xba, 0x17, 0xc7, 0x12, 0x93, 0x2e, 0xa7, 0x8d, 0x19, 0x86,
+ 0x65, 0x56, 0x2b, 0xf5, 0x10, 0x45, 0xda, 0xef, 0xc0, 0xdc, 0x92, 0x18,
+ 0xc4, 0xdd, 0x6c, 0xfc, 0xcb, 0x41, 0xfb, 0x04, 0x88, 0xfc, 0x1f, 0x7d,
+ 0xdf, 0xe6, 0x34, 0xab, 0x85, 0x56, 0x81, 0x43, 0x2e, 0xea, 0x45, 0x14,
+ 0xd8, 0x5e, 0x3e, 0xd4, 0xd0, 0x41, 0x47, 0x7c, 0x16, 0xf2, 0xa1, 0x15,
+ 0xdc, 0x09, 0x97, 0xd3, 0x6b, 0x4c, 0x26, 0x58, 0xa4, 0xb9, 0x5e, 0x7c,
+ 0x80, 0x55, 0x13, 0x50, 0xde, 0x11, 0x36, 0x60, 0xd4, 0x47, 0x15, 0xea,
+ 0x80, 0x6d, 0x55, 0x91, 0x02, 0x15, 0x6f, 0xe5, 0x4a, 0x11, 0xa6, 0x6e,
+ 0x30, 0xe0, 0x14, 0xd9, 0x9a, 0x9e, 0x8b, 0x85, 0x81, 0x7f, 0xd0, 0x17,
+ 0xb3, 0xdf, 0x8b, 0x3a, 0xcc, 0xaa, 0x65, 0x73, 0xcc, 0x15, 0xe1, 0x09,
+ 0x1a, 0x99, 0xa9, 0x03, 0x1e, 0x00, 0x5c, 0xc8, 0x67, 0xf4, 0x73, 0x03,
+ 0x0f, 0x78, 0x29, 0x37, 0x7d, 0x68, 0x1b, 0x86, 0x06, 0xf9, 0x27, 0x04,
+ 0xa6, 0xef, 0xf2, 0xe8, 0xbc, 0xa7, 0xc1, 0xf9, 0x08, 0x6a, 0x45, 0x89,
+ 0xea, 0x81, 0x86, 0x77, 0xa4, 0xea, 0x5b, 0x8c, 0xd6, 0x60, 0x23, 0x19,
+ 0x42, 0xc6, 0x42, 0x01, 0x7d, 0x33, 0x83, 0xd7, 0x07, 0x7e, 0xd9, 0x91,
+ 0x45, 0xf9, 0x28, 0x59, 0x37, 0x1a, 0x47, 0x98, 0x53, 0xcd, 0x3f, 0x3d,
+ 0x3d, 0xaa, 0xbc, 0x2b, 0x1c, 0x0e, 0x0a, 0x4f, 0x47, 0x35, 0x01, 0xbe,
+ 0x2c, 0xda, 0xd8, 0xa4, 0xa2, 0x50, 0x66, 0x99, 0x8c, 0x44, 0x59, 0x60,
+ 0xe8, 0xff, 0x9b, 0x65, 0x4c, 0x26, 0xcf, 0x43, 0x95, 0x27, 0x5d, 0xf4,
+ 0x73, 0x9f, 0x76, 0x04, 0x9e, 0xaf, 0xbb, 0x41, 0x55, 0xe6, 0xa6, 0x9a,
+ 0xa5, 0xd1, 0x0c, 0x55, 0x79, 0xf1, 0xba, 0x48, 0x37, 0x2a, 0x33, 0x82,
+ 0x77, 0x87, 0x05, 0xeb, 0x86, 0x77, 0x3a, 0x98, 0x30, 0xe9, 0xe0, 0x0f,
+ 0x28, 0x46, 0x3c, 0x94, 0xfa, 0x3c, 0x60, 0xbf, 0x82, 0xb9, 0xff, 0xf7,
+ 0x46, 0xa6, 0x2a, 0x4f, 0x4a, 0xe0, 0x95, 0x38, 0x4a, 0x97, 0x33, 0x15,
+ 0x51, 0xcd, 0xe7, 0xf9, 0x56, 0x4f, 0x0d, 0x59, 0x9f, 0x61, 0xdd, 0x36,
+ 0x6f, 0xf7, 0x78, 0x86, 0x16, 0xd0, 0x3b, 0x82, 0xc0, 0x59, 0xc1, 0xa8,
+ 0x8a, 0xcb, 0x6f, 0xc2, 0xce, 0x24, 0x1c, 0x0c, 0x50, 0x13, 0x1c, 0xe4,
+ 0xa7, 0xaf, 0x0c, 0xf7, 0x6e, 0xb9, 0x92, 0x44, 0xbb, 0xeb, 0xd9, 0x3b,
+ 0x3d, 0xff, 0xd2, 0xea, 0x9b, 0x52, 0x7b, 0x3f, 0x4f, 0x43, 0x0c, 0xa4,
+ 0x28, 0xbc, 0x46, 0x2e, 0xb0, 0xef, 0xbd, 0xeb, 0xa8, 0x7a, 0x2c, 0xf3,
+ 0xfb, 0x91, 0x9e, 0x46, 0xbc, 0x76, 0x53, 0x36, 0xe1, 0x6d, 0x76, 0xc6,
+ 0x2b, 0x58, 0x99, 0x28, 0xa6, 0xa3, 0x4e, 0xd7, 0xce, 0x18, 0xbc, 0x29,
+ 0x32, 0x9c, 0x06, 0xf7, 0x00, 0xca, 0xcb, 0x66, 0x38, 0xa6, 0x2e, 0xe9,
+ 0xa8, 0x18, 0x1a, 0x8c, 0x6e, 0x80, 0xe8, 0xc8, 0x89, 0x3d, 0xb9, 0xb3,
+ 0x41, 0x98, 0x62, 0x23, 0x73, 0x02, 0xb1, 0x05, 0x2e, 0xb1, 0xec, 0x1c,
+ 0x20, 0x82, 0xcd, 0x7b, 0xf3, 0xb1, 0x84, 0xaf, 0x8c, 0xf2, 0x2c, 0x0b,
+ 0x61, 0x6b, 0x76, 0xf7, 0xd4, 0xbf, 0x0d, 0x54, 0x99, 0x3a, 0xca, 0xf9,
+ 0x8d, 0x1e, 0x3a, 0xad, 0x02, 0xbc, 0x54, 0x75, 0x0c, 0x32, 0xe1, 0xa8,
+ 0xa3, 0xb2, 0xa9, 0xf6, 0x49, 0x70, 0x51, 0xcb, 0x9d, 0x91, 0xdd, 0x23,
+ 0x9e, 0xb0, 0x30, 0xb4, 0xf5, 0x3c, 0x93, 0xaa, 0x34, 0x7d, 0x51, 0x4f,
+ 0xb7, 0xb2, 0x47, 0xbc, 0x08, 0x83, 0x2f, 0x74, 0xc4, 0xcc, 0xfc, 0xda,
+ 0x06, 0x4a, 0xc3, 0x74, 0x38, 0x85, 0x55, 0x48, 0x7a, 0x1e, 0x61, 0x64,
+ 0x8f, 0x69, 0x51, 0x5b, 0xf9, 0x92, 0xd0, 0x64, 0x96, 0x14, 0x13, 0x61,
+ 0x9e, 0xfe, 0x00, 0x7b, 0xdc, 0x31, 0x12, 0x0f, 0x3e, 0x48, 0x3f, 0x16,
+ 0xb7, 0xc5, 0xa0, 0x19, 0x07, 0x66, 0xc6, 0xb5, 0xf0, 0xa1, 0x80, 0xf8,
+ 0xe0, 0x38, 0x74, 0x3b, 0x49, 0x0f, 0xbf, 0x61, 0x5c, 0x4a, 0x1f, 0x33,
+ 0xa2, 0x85, 0x17, 0xf9, 0xeb, 0x6b, 0x1e, 0x91, 0x98, 0x45, 0x96, 0x7a,
+ 0x55, 0xa5, 0x2d, 0x68, 0xcb, 0x72, 0x09, 0x22, 0x0f, 0xe3, 0x5c, 0x4f,
+ 0x43, 0x3f, 0xcb, 0xb4, 0xce, 0x37, 0xfa, 0x27, 0xe0, 0x5f, 0xc5, 0x44,
+ 0xd9, 0x9d, 0x8c, 0x0e, 0x97, 0x95, 0xb6, 0x81, 0xdf, 0xf2, 0x8e, 0xa5,
+ 0x0d, 0x8a, 0x94, 0x43, 0x8f, 0x3c, 0xb8, 0xfd, 0xdc, 0x7a, 0x4a, 0xe7,
+ 0xb5, 0xc0, 0x5a, 0xc0, 0xa4, 0x75, 0x48, 0x4c, 0xd5, 0xf4, 0x3b, 0x71,
+ 0x8c, 0x36, 0x6e, 0x28, 0xb0, 0xb2, 0x51, 0x3e, 0x2b, 0xe9, 0x79, 0x62,
+ 0xb1, 0x7b, 0x13, 0x4b, 0x9c, 0x19, 0x15, 0x40, 0xf6, 0x72, 0x2d, 0xdf,
+ 0x97, 0x21, 0x18, 0x11, 0xd6, 0xcc, 0xc0, 0xa2, 0xab, 0x58, 0xf1, 0x71,
+ 0x44, 0xca, 0xca, 0x1f, 0x36, 0x85, 0xba, 0x62, 0x93, 0x77, 0x36, 0xc4,
+ 0xed, 0x3d, 0x5d, 0x84, 0x64, 0x2c, 0x72, 0xc6, 0xa7, 0x1a, 0xdb, 0x8f,
+ 0xdb, 0xf9, 0xf5, 0xf2, 0x91, 0x79, 0x5f, 0xd5, 0xfc, 0x15, 0x6c, 0xc4,
+ 0x8d, 0x7e, 0x2b, 0x19, 0x0e, 0xb3, 0x2e, 0xa7, 0x84, 0x6a, 0xf2, 0xba,
+ 0x44, 0xa5, 0x51, 0x96, 0x31, 0xd4, 0x7e, 0x2e, 0x18, 0xc1, 0x20, 0xe1,
+ 0x69, 0xe6, 0x8b, 0xab, 0x00, 0x9d, 0x73, 0x5e, 0xa4, 0xe1, 0xf4, 0x27,
+ 0x7b, 0x25, 0x3f, 0x90, 0x13, 0x5d, 0xf5, 0xcb, 0x13, 0xbd, 0xcb, 0x06,
+ 0xa3, 0x04, 0x97, 0x0b, 0x0b, 0x38, 0x84, 0xe2, 0xc2, 0x7b, 0xe0, 0x59,
+ 0x35, 0x18, 0x7f, 0x89, 0x6d, 0x78, 0x0f, 0x63, 0xb4, 0x88, 0xd5, 0x93,
+ 0x80, 0xec, 0x88, 0x8c, 0xe0, 0xfa, 0x28, 0x11, 0x34, 0x7d, 0x71, 0xb8,
+ 0x07, 0xc8, 0xdb, 0x8f, 0x08, 0xda, 0xf3, 0x76, 0xa8, 0x75, 0x1b, 0x96,
+ 0x11, 0x48, 0xb4, 0x11, 0x70, 0xa2, 0x30, 0xb7, 0xdf, 0x98, 0xce, 0x31,
+ 0xa2, 0xeb, 0x6a, 0x27, 0xee, 0x0d, 0xea, 0xbc, 0x2d, 0x52, 0x81, 0x9b,
+ 0xe2, 0xca, 0x71, 0x59, 0x31, 0x34, 0x0d, 0xd8, 0x4b, 0x7e, 0x83, 0xb2,
+ 0xeb, 0x95, 0xa4, 0x19, 0xd5, 0xa4, 0xe7, 0xb1, 0xe4, 0x79, 0xcf, 0xb3,
+ 0x48, 0xcc, 0x24, 0xeb, 0xfe, 0x00, 0x9d, 0x92, 0x0c, 0x5b, 0xd7, 0x42,
+ 0x25, 0x2c, 0x67, 0xea, 0x20, 0x60, 0xee, 0x0b, 0x46, 0x74, 0xfe, 0xa0,
+ 0x6c, 0x07, 0x0e, 0x98, 0xb2, 0xf5, 0xa0, 0x27, 0x47, 0x35, 0xc8, 0xc4,
+ 0x08, 0x9a, 0xfc, 0x9f, 0x32, 0x21, 0x4f, 0x22, 0x04, 0xbf, 0x4c, 0x06,
+ 0x68, 0x3e, 0xc6, 0x93, 0xd1, 0x7e, 0x10, 0x83, 0x40, 0xfe, 0x1a, 0x69,
+ 0x12, 0x77, 0xb5, 0x00, 0x1e, 0x38, 0x90, 0xf7, 0xd8, 0xd7, 0x6f, 0x86,
+ 0xf5, 0xb4, 0x58, 0xa7, 0x66, 0x48, 0x26, 0xfd, 0x14, 0xfd, 0x8e, 0xe5,
+ 0xca, 0x94, 0x1c, 0x95, 0x10, 0x9c, 0x30, 0xa7, 0xd1, 0xd3, 0xe3, 0x1e,
+ 0x08, 0xf4, 0x80, 0xf0, 0x73, 0x09, 0x58, 0xe0, 0x5a, 0xf2, 0x23, 0x30,
+ 0xcf, 0x3c, 0x61, 0xbb, 0x08, 0x07, 0x89, 0x37, 0xad, 0x9d, 0x46, 0xca,
+ 0xbd, 0x03, 0x0c, 0xde, 0xe1, 0x94, 0xeb, 0xdb, 0x7d, 0x8c, 0x0e, 0x3b,
+ 0xe3, 0x24, 0x99, 0x23, 0x7c, 0x64, 0x79, 0x9f, 0x66, 0x5c, 0x83, 0xe7,
+ 0x8e, 0xb7, 0xf5, 0x8a, 0x98, 0xdc, 0x60, 0xa7, 0x67, 0x2d, 0x4e, 0xf5,
+ 0x0a, 0x45, 0x6e, 0x60, 0x9f, 0xfd, 0x28, 0x4d, 0xb4, 0x66, 0xed, 0xcd,
+ 0xb8, 0x50, 0x77, 0x7a, 0x89, 0xad, 0xc7, 0x42, 0xee, 0xe8, 0x3d, 0x73,
+ 0x68, 0xee, 0xd1, 0x80, 0x1b, 0x13, 0x10, 0x2f, 0xe5, 0x33, 0xd9, 0x12,
+ 0x1f, 0x08, 0x56, 0x10, 0x82, 0x32, 0x0e, 0x51, 0x18, 0x06, 0x91, 0x2c,
+ 0xce, 0x1b, 0x0a, 0x1d, 0x92, 0x89, 0xa9, 0xd3, 0x3c, 0x35, 0x1f, 0xe4,
+ 0x0a, 0x40, 0xea, 0x80, 0x8a, 0xdc, 0x2d, 0x86, 0x58, 0x6f, 0xd9, 0x1e,
+ 0x88, 0xc0, 0xad, 0x26, 0x95, 0xce, 0x84, 0xf8, 0xfc, 0x6a, 0x4a, 0x33,
+ 0x23, 0x11, 0xed, 0xea, 0x1a, 0xe8, 0x59, 0x1e, 0xa0, 0xb4, 0x71, 0x92,
+ 0x49, 0x43, 0x0b, 0x8d, 0xb4, 0x31, 0x33, 0x79, 0xfb, 0x49, 0x3b, 0xab,
+ 0x66, 0x58, 0x77, 0x4b, 0xdf, 0x6c, 0x11, 0xcb, 0x60, 0x09, 0x94, 0xd6,
+ 0xa5, 0xc9, 0xad, 0xe3, 0x73, 0xc0, 0x26, 0x44, 0xd9, 0xe8, 0x8e, 0x46,
+ 0x82, 0x3a, 0x58, 0x1d, 0x58, 0xb8, 0x40, 0x76, 0x2d, 0x0d, 0xc3, 0x26,
+ 0x70, 0x40, 0x6d, 0xd9, 0xe0, 0xc8, 0xca, 0xbe, 0x4c, 0x13, 0x40, 0x03,
+ 0x43, 0xc8, 0xa5, 0x8f, 0x85, 0xd5, 0xcb, 0x36, 0x81, 0x3d, 0xb2, 0xd1,
+ 0xfa, 0xce, 0xf9, 0x74, 0x07, 0xdb, 0xcf, 0x61, 0x28, 0x49, 0x44, 0xcb,
+ 0x72, 0x28, 0x15, 0xe9, 0x56, 0xd3, 0x0f, 0xd8, 0xc4, 0x07, 0xac, 0x63,
+ 0xff, 0xc6, 0x7f, 0xe1, 0x78, 0x94, 0xf9, 0xce, 0xaf, 0x94, 0x5b, 0x09,
+ 0x17, 0xa5, 0x45, 0x70, 0xc7, 0x61, 0xfb, 0x91, 0xbb, 0x0b, 0x5c, 0xe6,
+ 0xb8, 0x62, 0xb6, 0x1c, 0x95, 0xf9, 0x48, 0xa5, 0xe9, 0x25, 0x70, 0x35,
+ 0x65, 0x07, 0x06, 0x20, 0xca, 0x12, 0x54, 0x36, 0x9b, 0xda, 0x04, 0x8d,
+ 0x07, 0x6d, 0xb0, 0x57, 0x5b, 0x16, 0xc4, 0xd4, 0x33, 0x6a, 0xa6, 0x3b,
+ 0x57, 0x10, 0x63, 0x00, 0x50, 0x9e, 0x91, 0x1c, 0x28, 0xd7, 0x1a, 0x59,
+ 0x20, 0xa3, 0x87, 0x14, 0x3f, 0x26, 0x29, 0xa6, 0xf0, 0x73, 0x20, 0x54,
+ 0xfb, 0xa0, 0xc5, 0xf1, 0xd4, 0x8b, 0x01, 0x5e, 0xd4, 0x53, 0x78, 0x49,
+ 0x6f, 0x32, 0xfb, 0x42, 0xec, 0x69, 0x77, 0x28, 0xc1, 0x6f, 0xa9, 0xc4,
+ 0x5c, 0x94, 0x6b, 0x59, 0x59, 0xfd, 0xff, 0x8b, 0xf4, 0xd5, 0x42, 0x12,
+ 0xd1, 0xb1, 0x0d, 0x71, 0xbd, 0x54, 0x0d, 0xa6, 0xb6, 0x14, 0x03, 0x8b,
+ 0xe3, 0xf8, 0x89, 0x35, 0xd7, 0xdc, 0x62, 0x5b, 0x86, 0x0f, 0xc1, 0x52,
+ 0x06, 0x73, 0x68, 0xa3, 0xe3, 0x58, 0x2e, 0x85, 0x2e, 0xe7, 0xf7, 0xad,
+ 0xc5, 0x61, 0x39, 0xb6, 0xff, 0x00, 0xf3, 0xc6, 0x4e, 0x02, 0xc8, 0x2a,
+ 0x8b, 0x4f, 0x2e, 0x14, 0x61, 0xbf, 0xb9, 0x7b, 0xba, 0xf3, 0xac, 0x99,
+ 0xac, 0xb0, 0x08, 0xcc, 0x8b, 0x9a, 0xf1, 0xc9, 0x20, 0x89, 0x00, 0xd7,
+ 0xbb, 0x5c, 0xe4, 0x4b, 0x44, 0x4d, 0xff, 0x21, 0x4b, 0xb8, 0x8b, 0x05,
+ 0x91, 0x8c, 0x09, 0x2b, 0x3b, 0x1f, 0xbb, 0xeb, 0x4d, 0xd2, 0xe0, 0x92,
+ 0x00, 0x05, 0x91, 0x30, 0x01, 0x87, 0xa2, 0x92, 0x67, 0x21, 0xe0, 0x5c,
+ 0x2f, 0x5a, 0x56, 0x55, 0x11, 0x6d, 0x7c, 0x33, 0x26, 0xff, 0x93, 0xa0,
+ 0x40, 0xdd, 0xdd, 0x59, 0x05, 0x53, 0x6d, 0x95, 0x20, 0x27, 0xdb, 0xec,
+ 0x38, 0xb1, 0x4b, 0x76, 0x68, 0xd9, 0x7d, 0x49, 0x89, 0x60, 0x40, 0x97,
+ 0x79, 0xa9, 0xcd, 0x9a, 0x61, 0x2d, 0xac, 0x14, 0x9c, 0x20, 0x53, 0xea,
+ 0xa8, 0x17, 0xf8, 0xb2, 0x3a, 0x0e, 0x6c, 0x73, 0xe6, 0x93, 0x07, 0xd1,
+ 0x43, 0xa6, 0xc8, 0x42, 0x76, 0xe6, 0xf3, 0x46, 0x68, 0x56, 0xe0, 0xef,
+ 0x22, 0x07, 0xe6, 0x6d, 0x37, 0x2e, 0x6b, 0xba, 0xa9, 0x73, 0x1d, 0x43,
+ 0x5b, 0x7e, 0x46, 0x53, 0x6b, 0x28, 0x60, 0x45, 0x97, 0x2f, 0xc2, 0xea,
+ 0xbf, 0x1d, 0x23, 0x52, 0x57, 0x3d, 0x26, 0xf4, 0x16, 0x06, 0x72, 0x05,
+ 0xc4, 0xcd, 0xcb, 0xcd, 0x8c, 0xb7, 0x14, 0xb1, 0xbb, 0xb0, 0xcf, 0xb9,
+ 0xaa, 0xd0, 0x8b, 0x89, 0x91, 0x37, 0xdb, 0xeb, 0x0f, 0xbe, 0xcd, 0xc5,
+ 0xec, 0x31, 0x61, 0xca, 0xa4, 0x66, 0xa4, 0x7b, 0xe8, 0x8d, 0x96, 0xbd,
+ 0x4a, 0x11, 0xf6, 0xcf, 0xe9, 0x6c, 0xd5, 0xea, 0xc5, 0xfd, 0xe3, 0x04,
+ 0x27, 0x32, 0xc8, 0x21, 0xab, 0x16, 0xc1, 0x2b, 0xf1, 0x99, 0x4b, 0x8d,
+ 0x38, 0x9b, 0xd4, 0xb8, 0xa6, 0xef, 0x3e, 0x12, 0xc4, 0xf9, 0x7e, 0x4a,
+ 0x7c, 0x26, 0xcf, 0xc2, 0xac, 0x9c, 0xd5, 0xe7, 0x78, 0xba, 0x54, 0x00,
+ 0x7c, 0xa3, 0x51, 0x92, 0x4e, 0x62, 0x1b, 0xa4, 0xa2, 0xbd, 0x3b, 0xf7,
+ 0xb5, 0xfb, 0x0f, 0xef, 0x89, 0xef, 0x8b, 0xa0, 0x6c, 0x08, 0x44, 0xe0,
+ 0x4d, 0x00, 0x3c, 0x28, 0xda, 0x64, 0xde, 0xef, 0xa8, 0x22, 0x08, 0x72,
+ 0x5a, 0x67, 0x44, 0xb0, 0x9a, 0x18, 0xfb, 0x62, 0x54, 0xa7, 0xf4, 0x7f,
+ 0x9f, 0xc7, 0x1e, 0xb3, 0x8f, 0x28, 0xe1, 0x0f, 0x21, 0x16, 0x38, 0xb0,
+ 0xe4, 0xfc, 0xdd, 0x8a, 0x94, 0x09, 0xc0, 0x93, 0x28, 0x2f, 0x81, 0x8c,
+ 0x90, 0xd6, 0x26, 0x74, 0x14, 0xaa, 0x76, 0x49, 0x6a, 0xd4, 0x18, 0xb7,
+ 0x4d, 0x82, 0xfd, 0x4a, 0x5d, 0xb0, 0xd1, 0x05, 0xfd, 0xf8, 0x2f, 0xb2,
+ 0xf9, 0x69, 0x80, 0x44, 0xd2, 0x1b, 0x0a, 0x67, 0xe1, 0xd3, 0xf6, 0xe0,
+ 0x67, 0x66, 0x59, 0xf0, 0xcb, 0x3b, 0x91, 0x6c, 0x04, 0x05, 0xdd, 0x94,
+ 0xbb, 0x9d, 0x08, 0x27, 0x0a, 0x1b, 0xc9, 0x6d, 0x87, 0x3c, 0x6c, 0x36,
+ 0xed, 0x1b, 0x03, 0x18, 0x68, 0x81, 0xaa, 0x6d, 0x8c, 0xf1, 0xea, 0x7b,
+ 0x96, 0x47, 0x81, 0xb3, 0x24, 0x0f, 0x62, 0x5a, 0x59, 0x7e, 0x1f, 0x55,
+ 0x7c, 0x25, 0x23, 0x07, 0x3a, 0x42, 0xec, 0x9c, 0x73, 0x12, 0xc5, 0xa2,
+ 0x8f, 0x0d, 0x35, 0xe3, 0x01, 0xa3, 0x6f, 0x90, 0x04, 0x90, 0xee, 0xbc,
+ 0x8f, 0xc0, 0x46, 0x93, 0x21, 0x9e, 0xed, 0xf2, 0x9c, 0x2f, 0x85, 0x6f,
+ 0xee, 0x60, 0x01, 0x68, 0x89, 0x16, 0x33, 0x57, 0x73, 0x9e, 0xc9, 0x78,
+ 0xf1, 0xf5, 0xab, 0x2a, 0x62, 0x00, 0x07, 0xed, 0x6e, 0x30, 0xf1, 0xae,
+ 0xa5, 0x66, 0xfe, 0x54, 0xf3, 0x56, 0x77, 0x5d, 0x80, 0x79, 0x60, 0x11,
+ 0x69, 0xc4, 0x46, 0x5d, 0xfa, 0x96, 0x27, 0x8b, 0x2b, 0x19, 0x3b, 0x51,
+ 0x4a, 0x15, 0x12, 0xec, 0xc0, 0xf2, 0x05, 0xb1, 0x60, 0x54, 0x52, 0xe3,
+ 0x96, 0xbf, 0xc7, 0x45, 0xe9, 0x8e, 0x68, 0x57, 0x78, 0x91, 0x0e, 0x14,
+ 0x9e, 0xae, 0xfa, 0x7d, 0x2e, 0x06, 0x2e, 0x0b, 0xd4, 0x04, 0xfa, 0x07,
+ 0x9d, 0x62, 0x91, 0x0a, 0xc6, 0xdd, 0x68, 0x8f, 0x8c, 0xe7, 0x10, 0xf0,
+ 0xe9, 0xd1, 0x8a, 0x02, 0xf1, 0xde, 0x8e, 0xf7, 0xdf, 0x5f, 0x3e, 0xf9,
+ 0x59, 0x96, 0xb9, 0xa0, 0x3b, 0x6e, 0xc8, 0xad, 0x1e, 0x04, 0xa8, 0x8a,
+ 0xa9, 0x84, 0x0e, 0xc8, 0x2b, 0x2f, 0xf0, 0x1c, 0xf0, 0x0a, 0xb3, 0xaf,
+ 0xaa, 0x31, 0x5f, 0x98, 0xf8, 0xbc, 0xa2, 0x94, 0x5c, 0x4d, 0x72, 0xf6,
+ 0xfd, 0x30, 0x16, 0xd0, 0x00, 0x00, 0x00, 0x24, 0x83, 0x7d, 0x18, 0xb2,
+ 0x1e, 0xea, 0xe4, 0x5c, 0xe0, 0x5b, 0x31, 0x68, 0x13, 0x9d, 0x45, 0x46,
+ 0x45, 0x1a, 0x5e, 0xf5, 0x77, 0x1e, 0xa8, 0x21, 0xcb, 0x32, 0x50, 0xe1,
+ 0x60, 0xaa, 0xfc, 0xba, 0x26, 0x67, 0xe1, 0x9f, 0xd0, 0xaa, 0x14, 0xca,
+ 0x09, 0xe7, 0xf0, 0xf6, 0x83, 0x2a, 0xba, 0xa7, 0xc5, 0x4f, 0x71, 0x3c,
+ 0x96, 0x3c, 0x46, 0xb1, 0xf8, 0x3f, 0x2c, 0xa6, 0x6c, 0xb7, 0x2d, 0x7b,
+ 0xd0, 0x2f, 0x65, 0x88, 0x33, 0x20, 0xf3, 0x3c, 0x64, 0x85, 0xda, 0x4a,
+ 0x64, 0xa1, 0xcf, 0x0a, 0x41, 0x86, 0x05, 0xe1, 0x4d, 0x4d, 0x79, 0xc6,
+ 0x20, 0x47, 0x54, 0x65, 0xcd, 0x56, 0x10, 0x4f, 0xe1, 0x20, 0x5f, 0x5f,
+ 0x83, 0xf3, 0x8d, 0xcd, 0x93, 0x0e, 0xce, 0xfa, 0x2e, 0xf3, 0x06, 0xb3,
+ 0x95, 0x3a, 0xc4, 0xa4, 0xcd, 0x63, 0xf2, 0xe1, 0x64, 0x84, 0x1f, 0x38,
+ 0xce, 0x90, 0x0f, 0xd4, 0x75, 0xb9, 0xb6, 0x7f, 0xf1, 0x28, 0xfc, 0xbe,
+ 0xe2, 0xdd, 0xa7, 0x9a, 0xa1, 0xdf, 0x0a, 0x7c, 0xc3, 0xff, 0x7b, 0xc1,
+ 0xbf, 0x58, 0x8c, 0xfa, 0x7f, 0x7f, 0xa9, 0xe4, 0x10, 0x8b, 0x00, 0xc1,
+ 0x8b, 0xbd, 0xbd, 0xe6, 0xe2, 0x99, 0x57, 0x28, 0x40, 0x0f, 0x3b, 0x88,
+ 0xb8, 0xa9, 0x68, 0xfc, 0x2d, 0x2e, 0xf2, 0xb4, 0xea, 0x35, 0x57, 0x06,
+ 0x0c, 0x87, 0x05, 0xe9, 0xeb, 0x88, 0x78, 0xe6, 0x3e, 0x78, 0x61, 0xf7,
+ 0x38, 0xd3, 0x5d, 0x22, 0x3b, 0x60, 0x6d, 0x93, 0xdc, 0xe6, 0xc3, 0x2d,
+ 0x16, 0x3b, 0x8d, 0x9c, 0x8c, 0xd2, 0xd7, 0x38, 0x29, 0x8d, 0x5e, 0x83,
+ 0x4f, 0x4a, 0xe3, 0x5e, 0xd4, 0xba, 0xdb, 0x75, 0x85, 0x3c, 0x5b, 0x13,
+ 0xb7, 0x6f, 0xda, 0x59, 0x65, 0x62, 0x58, 0xe3, 0x09, 0x35, 0xac, 0x7e,
+ 0x50, 0xb4, 0x7b, 0x75, 0x54, 0x86, 0x62, 0x79, 0x28, 0xcc, 0xec, 0x8c,
+ 0x5c, 0x36, 0x1d, 0xe7, 0xdd, 0x4a, 0xd4, 0xb5, 0xa4, 0x8b, 0xf1, 0x67,
+ 0x50, 0xfd, 0x62, 0xcd, 0x61, 0xbc, 0xd5, 0xbe, 0xee, 0x16, 0xbf, 0x04,
+ 0xcc, 0x04, 0x01, 0x1e, 0x4c, 0x17, 0x41, 0xde, 0x99, 0x86, 0x80, 0x73,
+ 0xc5, 0x65, 0xcc, 0x4d, 0x09, 0x37, 0x23, 0x80, 0xb3, 0x61, 0xa7, 0xb3,
+ 0xeb, 0x62, 0x67, 0xc2, 0x19, 0x60, 0x68, 0x14, 0xea, 0x77, 0x10, 0x17,
+ 0x5e, 0xf7, 0x45, 0x3c, 0x1c, 0x26, 0xa7, 0x4a, 0x81, 0x5b, 0x78, 0x3b,
+ 0x8a, 0x19, 0x62, 0x90, 0x22, 0xe4, 0x5f, 0x6d, 0xdf, 0x4e, 0xde, 0x46,
+ 0x88, 0x03, 0x6a, 0xf1, 0x0a, 0x4c, 0x3d, 0x46, 0xa9, 0x99, 0xc0, 0x4c,
+ 0x1e, 0xa3, 0x70, 0x77, 0xf6, 0xc8, 0x03, 0xa3, 0x58, 0xd8, 0x8b, 0xf8,
+ 0x81, 0xec, 0x92, 0x0e, 0x1a, 0x24, 0x51, 0x13, 0x59, 0x04, 0x42, 0x3d,
+ 0x21, 0x73, 0x96, 0x56, 0xfe, 0x35, 0x73, 0xe7, 0x13, 0x31, 0xac, 0xbb,
+ 0xd3, 0xdd, 0xb9, 0xf7, 0xa7, 0xf5, 0x85, 0xe2, 0x8c, 0x49, 0x84, 0x7d,
+ 0x4b, 0xf4, 0x01, 0xc4, 0x59, 0x09, 0xd0, 0x6e, 0x66, 0xbe, 0x26, 0xc9,
+ 0xee, 0x13, 0x0b, 0x9a, 0x68, 0x0d, 0x1f, 0xc7, 0x6e, 0xfe, 0xc4, 0x8a,
+ 0xed, 0x6e, 0xbc, 0x2e, 0x47, 0x77, 0x14, 0xd5, 0x62, 0x69, 0x32, 0x5a,
+ 0x9c, 0x5e, 0x59, 0xc1, 0x11, 0xf2, 0x3b, 0x7b, 0x3c, 0xf5, 0x6f, 0x0a,
+ 0x25, 0x98, 0x19, 0x65, 0xf9, 0x86, 0xa1, 0x04, 0x74, 0x65, 0x98, 0x75,
+ 0x67, 0x13, 0x30, 0xc6, 0xef, 0xfc, 0xe7, 0xd0, 0xff, 0x05, 0x5c, 0xe6,
+ 0x94, 0xd0, 0x6e, 0x8a, 0x15, 0x1b, 0x68, 0x34, 0x6c, 0xf2, 0xfb, 0xe4,
+ 0xc3, 0xfe, 0xe1, 0x6c, 0xe9, 0x8f, 0x68, 0xcd, 0x1d, 0x5b, 0x62, 0xf8,
+ 0xe4, 0x0b, 0xb8, 0x2c, 0x36, 0xed, 0xb6, 0x89, 0x3c, 0xd4, 0x60, 0xa5,
+ 0xa1, 0x93, 0x39, 0x58, 0xe0, 0x62, 0xea, 0xde, 0x54, 0xdc, 0xb1, 0xf2,
+ 0x2e, 0x58, 0x9b, 0x00, 0xad, 0xec, 0x05, 0x5b, 0x71, 0x47, 0x20, 0x07,
+ 0xee, 0x5f, 0xc7, 0x51, 0x2c, 0xa8, 0x37, 0xd8, 0xc8, 0xb0, 0xf9, 0xd0,
+ 0x17, 0x22, 0xce, 0xce, 0x66, 0x8a, 0xbe, 0xd2, 0x2d, 0x72, 0xf1, 0x56,
+ 0x3c, 0x46, 0x4d, 0xa5, 0xa4, 0x60, 0x85, 0xde, 0x15, 0x35, 0x9c, 0x31,
+ 0xc6, 0x90, 0x73, 0xeb, 0xb2, 0xda, 0xf1, 0xb2, 0x35, 0x11, 0x20, 0xa6,
+ 0x7f, 0x3f, 0x11, 0x8a, 0xb5, 0x1c, 0x1a, 0x25, 0x24, 0xac, 0x7d, 0x3f,
+ 0x1b, 0xb3, 0x1c, 0xbb, 0x04, 0xa0, 0x5d, 0xc2, 0x19, 0x23, 0x49, 0x32,
+ 0x21, 0x51, 0x4a, 0xcd, 0x36, 0x2b, 0xac, 0x78, 0xd5, 0x9d, 0x0b, 0xa0,
+ 0x72, 0x3c, 0x39, 0xb3, 0xa3, 0xb9, 0xd6, 0xe3, 0x5c, 0x95, 0x52, 0xd1,
+ 0x74, 0x4d, 0xf0, 0x9c, 0xb3, 0x95, 0x06, 0x7f, 0x49, 0x2b, 0x26, 0xee,
+ 0xfc, 0xcd, 0x0b, 0x12, 0x0e, 0xec, 0x80, 0x16, 0x15, 0x5f, 0x2c, 0x6e,
+ 0x37, 0xc7, 0x88, 0x6d, 0x2d, 0x4f, 0xf3, 0x14, 0xf6, 0xdf, 0xcd, 0x24,
+ 0xc7, 0x8d, 0x81, 0x87, 0x30, 0xa2, 0xab, 0x23, 0xa5, 0x6c, 0x4e, 0x3c,
+ 0xf0, 0x75, 0xa0, 0x86, 0xae, 0x02, 0x04, 0x7f, 0x67, 0x89, 0x39, 0x92,
+ 0xc3, 0xfb, 0x19, 0xc3, 0x76, 0xb6, 0x39, 0x4f, 0xce, 0x35, 0x91, 0x07,
+ 0x6c, 0x98, 0xbd, 0x95, 0xc6, 0x5a, 0x91, 0xa1, 0x16, 0x3b, 0x8c, 0xb5,
+ 0x58, 0x0e, 0xdb, 0x02, 0x07, 0xd6, 0xa9, 0xf2, 0x2c, 0x77, 0x58, 0x87,
+ 0xbc, 0x47, 0x30, 0xd3, 0x51, 0x64, 0x28, 0xea, 0x7b, 0x8e, 0x01, 0x0b,
+ 0xed, 0x9c, 0xdc, 0xf2, 0x41, 0xb5, 0x5c, 0x5b, 0x8e, 0x29, 0x86, 0xaa,
+ 0x48, 0xd7, 0x21, 0x75, 0x34, 0x5c, 0xc1, 0xd0, 0x11, 0x91, 0x13, 0xee,
+ 0x4f, 0x72, 0xa2, 0x11, 0x9e, 0x56, 0xc2, 0xbe, 0x61, 0x91, 0x64, 0x6a,
+ 0x8b, 0x09, 0x6b, 0x37, 0x75, 0x97, 0x23, 0xf0, 0x45, 0x61, 0x94, 0xcc,
+ 0x28, 0x40, 0xa9, 0xb8, 0xfe, 0xa5, 0xce, 0x32, 0xe9, 0x02, 0x19, 0x7d,
+ 0xe9, 0x6f, 0x58, 0x93, 0x33, 0xb7, 0x2c, 0x3b, 0x28, 0x1d, 0xc8, 0x5c,
+ 0xb6, 0xaf, 0xe7, 0xcc, 0x05, 0xac, 0xcc, 0x59, 0xac, 0x1f, 0xd4, 0xca,
+ 0x42, 0x6b, 0xa6, 0xf6, 0xa3, 0x25, 0x11, 0x13, 0x86, 0x1e, 0xaf, 0xa6,
+ 0xb4, 0xcd, 0x0c, 0x5d, 0xf7, 0x50, 0xa0, 0x20, 0xad, 0xa7, 0xe2, 0x1b,
+ 0xe7, 0xb2, 0xca, 0xb6, 0x81, 0xd3, 0xb0, 0x13, 0xbf, 0xf4, 0xbc, 0x38,
+ 0x01, 0x65, 0x19, 0x83, 0x14, 0xbd, 0xf3, 0x74, 0x71, 0x81, 0x80, 0xa2,
+ 0x9a, 0x9f, 0xaa, 0x93, 0xec, 0xc2, 0xe2, 0xdb, 0xab, 0xb7, 0x95, 0x7d,
+ 0x95, 0xc0, 0x99, 0xcc, 0xa7, 0xd2, 0x01, 0x74, 0x5a, 0xee, 0x09, 0x16,
+ 0x07, 0x07, 0xca, 0xb4, 0x98, 0x4f, 0xa1, 0x19, 0x9d, 0x55, 0x7e, 0x7e,
+ 0xa0, 0xd6, 0x0a, 0x85, 0x73, 0x4e, 0xe1, 0xf2, 0x59, 0x0c, 0xd9, 0xf2,
+ 0xf1, 0xfc, 0xc1, 0x67, 0x18, 0x41, 0x9b, 0x86, 0x0c, 0xa1, 0xf1, 0x38,
+ 0x6c, 0x01, 0xaa, 0x38, 0x69, 0xa0, 0x9c, 0x37, 0x17, 0x59, 0xb4, 0xf1,
+ 0x48, 0xf5, 0x0c, 0xf9, 0x56, 0xe3, 0x94, 0xb8, 0xf7, 0x8b, 0x6a, 0x71,
+ 0x24, 0x88, 0x75, 0x94, 0x37, 0x9a, 0xeb, 0x79, 0x93, 0xff, 0xfb, 0xf5,
+ 0xc3, 0x90, 0x49, 0xc4, 0x02, 0x09, 0xb6, 0xfa, 0xbc, 0x68, 0xd6, 0xa9,
+ 0x7a, 0x59, 0xee, 0xde, 0xd6, 0x89, 0xaa, 0x71, 0xa9, 0xc0, 0xa0, 0xac,
+ 0x92, 0xa8, 0xc0, 0xfa, 0x24, 0x96, 0x55, 0x9d, 0x99, 0x66, 0x43, 0xc5,
+ 0xd0, 0xd1, 0x6c, 0x2a, 0xe4, 0x79, 0xb1, 0x63, 0x9a, 0xe9, 0x2b, 0x7b,
+ 0x00, 0x58, 0xf7, 0x33, 0x13, 0xd6, 0x25, 0xab, 0x36, 0xd0, 0x20, 0xb8,
+ 0x26, 0xd0, 0xfe, 0x76, 0x19, 0x9a, 0xdc, 0x6d, 0xdb, 0x85, 0x28, 0xe8,
+ 0x6a, 0x28, 0x2a, 0x3d, 0x86, 0xa2, 0xa6, 0xf1, 0x80, 0x85, 0xbb, 0xde,
+ 0xcb, 0x49, 0xb0, 0x63, 0x60, 0x15, 0x86, 0x08, 0x83, 0x27, 0x2d, 0xd8,
+ 0x31, 0x99, 0xb9, 0x87, 0x9a, 0x63, 0x0b, 0x34, 0x46, 0x6f, 0x11, 0x2b,
+ 0xdc, 0x8b, 0x08, 0x05, 0xdc, 0x2d, 0x95, 0x19, 0x75, 0xb4, 0xbd, 0x7d,
+ 0x1c, 0xb7, 0xf8, 0x39, 0x1c, 0xe0, 0x26, 0xd6, 0x86, 0x25, 0x71, 0x6a,
+ 0x7b, 0x35, 0x4e, 0x16, 0x43, 0x87, 0x7b, 0xd8, 0x27, 0x90, 0x3f, 0xeb,
+ 0x44, 0x82, 0x1d, 0x4f, 0x56, 0xae, 0x74, 0xc7, 0x5e, 0x7a, 0x4f, 0xed,
+ 0x98, 0x72, 0x74, 0x5d, 0xfd, 0xe0, 0x5d, 0x07, 0x5e, 0x36, 0xad, 0x46,
+ 0x87, 0x7f, 0xda, 0xd6, 0x1c, 0xaf, 0xe5, 0x5f, 0x38, 0x58, 0x78, 0x4b,
+ 0x03, 0x40, 0x44, 0x39, 0x67, 0x0f, 0x5c, 0xbe, 0x4f, 0x47, 0x8e, 0xb4,
+ 0xc1, 0x3e, 0x09, 0x79, 0x6b, 0x2f, 0xc8, 0xfe, 0x87, 0x30, 0x43, 0x99,
+ 0x11, 0xd5, 0x2e, 0x92, 0xe2, 0xa2, 0x79, 0x73, 0xff, 0xba, 0xed, 0xf6,
+ 0x77, 0x14, 0xc6, 0x4c, 0x6c, 0x90, 0x8a, 0x0c, 0x44, 0x74, 0x4b, 0x6c,
+ 0xd1, 0x82, 0x95, 0x78, 0xcd, 0x25, 0xbe, 0x12, 0x87, 0x0f, 0xb7, 0x27,
+ 0x28, 0x26, 0x83, 0xea, 0xa2, 0xcf, 0x9b, 0xcb, 0x53, 0x19, 0x7b, 0xa9,
+ 0xe2, 0xe2, 0x0d, 0x85, 0x31, 0xb5, 0xc7, 0x95, 0xcb, 0x0d, 0x47, 0x02,
+ 0x31, 0x6b, 0xd4, 0xa0, 0xdf, 0x22, 0xd2, 0x25, 0xc5, 0x79, 0x1f, 0x1e,
+ 0x26, 0x69, 0x01, 0x26, 0xe1, 0x1b, 0xca, 0xee, 0x66, 0x19, 0xc9, 0xf6,
+ 0xa6, 0x53, 0x8d, 0x9e, 0x03, 0x7e, 0x54, 0xa3, 0xa9, 0xb0, 0x79, 0x75,
+ 0xe1, 0x07, 0x23, 0x95, 0x21, 0x48, 0xb1, 0xa0, 0xc0, 0xb0, 0x20, 0xc2,
+ 0x56, 0xbd, 0x70, 0x4f, 0x09, 0x82, 0xcb, 0x4f, 0xb0, 0x97, 0xc6, 0xb5,
+ 0xe7, 0xff, 0xad, 0xd0, 0x5f, 0x69, 0x7e, 0x6f, 0xb4, 0x24, 0x91, 0x86,
+ 0x22, 0x30, 0xa7, 0x01, 0xdc, 0xda, 0x82, 0x92, 0x82, 0xf4, 0x88, 0x70,
+ 0x69, 0x21, 0x21, 0x07, 0xde, 0xe4, 0xec, 0x11, 0xff, 0x8c, 0xeb, 0xb7,
+ 0xf1, 0x45, 0x9d, 0xbc, 0x85, 0x9c, 0xe9, 0x61, 0xe6, 0xc9, 0xd0, 0xb4,
+ 0x78, 0xad, 0xad, 0xdd, 0x2e, 0x4e, 0x02, 0x35, 0xf7, 0xe8, 0xae, 0x8c,
+ 0x15, 0x71, 0x02, 0x21, 0xe4, 0x81, 0x72, 0x2c, 0x7c, 0x42, 0xb1, 0x0a,
+ 0x33, 0xae, 0x61, 0xc7, 0x62, 0x9e, 0x22, 0x96, 0xbf, 0xc3, 0xcf, 0xb5,
+ 0xd1, 0xd5, 0x92, 0xb2, 0xe7, 0xec, 0xa2, 0xc0, 0xa9, 0x79, 0x57, 0xa0,
+ 0x13, 0xd8, 0xcb, 0x16, 0x99, 0xe5, 0xac, 0x6e, 0xb1, 0xde, 0x54, 0xd0,
+ 0xc5, 0x6e, 0x9d, 0x6d, 0xe6, 0x87, 0xb1, 0xf6, 0x7a, 0xf4, 0x28, 0xaf,
+ 0xf3, 0x6b, 0x3d, 0x86, 0x44, 0xac, 0x09, 0xa9, 0x77, 0x97, 0x6c, 0xaf,
+ 0xef, 0x2b, 0xc6, 0x1e, 0x0c, 0x37, 0x5d, 0xba, 0xaa, 0xa6, 0x1f, 0x52,
+ 0x52, 0xe8, 0x78, 0x5e, 0x0d, 0x4d, 0x9a, 0xfe, 0xbe, 0x5d, 0x9c, 0x25,
+ 0x0a, 0x4c, 0x52, 0x2c, 0xbf, 0xcd, 0xd5, 0xf4, 0xf2, 0x17, 0xba, 0x07,
+ 0x95, 0xbe, 0x2c, 0x8b, 0x09, 0xe9, 0x78, 0x4c, 0x2c, 0x18, 0x2b, 0x6b,
+ 0xa2, 0x44, 0x41, 0x87, 0x71, 0xab, 0x65, 0x6e, 0xb3, 0x20, 0x7e, 0x9d,
+ 0x79, 0xa0, 0x61, 0x99, 0xdf, 0xe9, 0xa3, 0x35, 0x0c, 0x90, 0x2b, 0x65,
+ 0xca, 0xb6, 0xfb, 0x9e, 0xe9, 0x56, 0x3e, 0x73, 0x03, 0x08, 0x3a, 0xc6,
+ 0xc2, 0xd0, 0x14, 0xc7, 0x57, 0x35, 0x1f, 0xb9, 0xce, 0x26, 0x0b, 0x8d,
+ 0xb8, 0x48, 0xab, 0x85, 0x41, 0x15, 0x33, 0x0f, 0x50, 0x51, 0xba, 0x01,
+ 0xf6, 0xf1, 0x04, 0xd4, 0x30, 0xea, 0x88, 0xad, 0x9c, 0x96, 0x96, 0xa5,
+ 0x60, 0x2b, 0x7e, 0xdd, 0xc3, 0x44, 0x86, 0xa2, 0x6e, 0x66, 0x53, 0x3d,
+ 0x27, 0x6e, 0xf1, 0x6a, 0x82, 0xf1, 0x53, 0x39, 0x8a, 0x2b, 0x06, 0xdd,
+ 0x8a, 0x73, 0x06, 0x51, 0xe4, 0x37, 0x57, 0x2a, 0x19, 0x73, 0x7b, 0xa8,
+ 0xc0, 0x55, 0x4e, 0xd9, 0xa2, 0xa0, 0x5a, 0x79, 0x7f, 0xd3, 0xee, 0xab,
+ 0x01, 0xdd, 0x72, 0x39, 0xcc, 0x3d, 0xe4, 0xf7, 0x7c, 0x7d, 0x38, 0x97,
+ 0xaf, 0x07, 0x82, 0x9b, 0x36, 0x9d, 0x5e, 0x60, 0xf3, 0xe2, 0x3c, 0xcd,
+ 0x53, 0x00, 0x38, 0x82, 0x07, 0x01, 0x4b, 0x7c, 0x7e, 0x13, 0x2d, 0x66,
+ 0xaf, 0xea, 0xd6, 0x78, 0xf5, 0xce, 0xff, 0x95, 0x87, 0xe3, 0x9a, 0xd5,
+ 0x20, 0x90, 0x0d, 0x62, 0x94, 0x57, 0xaf, 0xf2, 0xc1, 0x4d, 0x06, 0xa4,
+ 0x77, 0x4a, 0xac, 0x55, 0x8d, 0xf1, 0x82, 0xc8, 0xce, 0xe1, 0x3a, 0xa9,
+ 0x0c, 0xea, 0xba, 0x12, 0xad, 0xcf, 0x0d, 0xe1, 0x1b, 0x53, 0x69, 0x3d,
+ 0xc3, 0xcd, 0xa6, 0xb9, 0xe7, 0x10, 0x4a, 0xdb, 0x54, 0x1f, 0x11, 0x0e,
+ 0xbf, 0xe0, 0x6b, 0x52, 0x3a, 0xd7, 0xf8, 0xe5, 0x8d, 0xba, 0x9c, 0x15,
+ 0x1d, 0x82, 0x8a, 0x56, 0x3b, 0x18, 0x61, 0x64, 0x9b, 0x21, 0x8c, 0x67,
+ 0xc7, 0xaa, 0xef, 0x85, 0x69, 0xb8, 0x34, 0x07, 0x37, 0x98, 0x90, 0x7d,
+ 0x57, 0xa2, 0x84, 0x6f, 0x72, 0x6c, 0x8a, 0xf0, 0x28, 0xaf, 0x1c, 0xbc,
+ 0x2b, 0x72, 0xb9, 0x43, 0xbe, 0x67, 0x56, 0x92, 0x37, 0x5f, 0x2c, 0x7c,
+ 0xff, 0x8f, 0x19, 0xce, 0x9b, 0x78, 0xda, 0x99, 0x92, 0xd5, 0xce, 0x3c,
+ 0xae, 0x70, 0x80, 0x71, 0x73, 0x59, 0x20, 0xbf, 0x7a, 0x0e, 0xf9, 0x2e,
+ 0x71, 0xa1, 0x74, 0x91, 0x3d, 0x21, 0x75, 0xa2, 0x68, 0xb5, 0xd8, 0x8f,
+ 0x6d, 0xe0, 0xdc, 0xf7, 0x18, 0xde, 0x02, 0xe0, 0x19, 0x42, 0x4b, 0x85,
+ 0x53, 0xc2, 0x75, 0x56, 0xca, 0x64, 0xf7, 0xe0, 0x71, 0x4c, 0x7f, 0xba,
+ 0x0a, 0x3d, 0xe3, 0x49, 0xed, 0xae, 0x9f, 0x5c, 0x7b, 0xfd, 0xf3, 0x6d,
+ 0xa9, 0x34, 0xc4, 0x5c, 0xbf, 0x28, 0x93, 0xe2, 0xbd, 0x6e, 0xb4, 0xe5,
+ 0x96, 0x11, 0xeb, 0x2d, 0xff, 0x95, 0x25, 0x25, 0xb6, 0xd7, 0x3b, 0xb5,
+ 0x51, 0xad, 0xa9, 0x60, 0xd1, 0xbc, 0xc5, 0x1d, 0xd5, 0x12, 0xaf, 0x55,
+ 0x3b, 0xa5, 0x93, 0x8a, 0x15, 0x3a, 0x36, 0x2f, 0xd7, 0x2d, 0xaf, 0xa9,
+ 0xf3, 0x2c, 0xff, 0x95, 0x2c, 0x49, 0xa4, 0x36, 0x06, 0x9d, 0x45, 0x6a,
+ 0x7f, 0xb1, 0x0d, 0x21, 0x7b, 0xa3, 0xb6, 0x4a, 0x5a, 0xae, 0xea, 0x41,
+ 0x08, 0x65, 0x88, 0xc2, 0x3c, 0x8f, 0xcd, 0xd7, 0xd4, 0x7c, 0x72, 0xd4,
+ 0xf2, 0x1d, 0x2a, 0xff, 0x88, 0xc3, 0x81, 0xed, 0x05, 0x9b, 0x4b, 0x17,
+ 0x3b, 0xc2, 0x48, 0x23, 0xb9, 0x17, 0x70, 0x57, 0xda, 0x10, 0xb9, 0xa0,
+ 0xb8, 0xa2, 0x61, 0x06, 0x6f, 0x48, 0x8b, 0xc5, 0x52, 0x09, 0x39, 0xb6,
+ 0x62, 0x78, 0x44, 0xc1, 0x34, 0x8a, 0x11, 0x70, 0x09, 0x7f, 0x1a, 0x77,
+ 0xe7, 0x1d, 0x1b, 0x86, 0x71, 0x9e, 0x7d, 0xd4, 0xdf, 0xea, 0x55, 0x87,
+ 0x27, 0xaf, 0x3c, 0x74, 0x26, 0xdb, 0x10, 0x4b, 0xf2, 0x49, 0xac, 0x9b,
+ 0x9d, 0x82, 0x4f, 0x37, 0x49, 0xff, 0x24, 0xb0, 0x87, 0xec, 0x12, 0x09,
+ 0x95, 0xb1, 0x95, 0x9c, 0xae, 0xd5, 0x6c, 0x80, 0xd4, 0x65, 0x7c, 0x1b,
+ 0xab, 0xd7, 0x99, 0x1f, 0xca, 0x91, 0xdc, 0x7d, 0x02, 0xdb, 0xd6, 0x02,
+ 0xcb, 0x11, 0x6c, 0x02, 0x8a, 0x2f, 0xec, 0xb0, 0x3a, 0xeb, 0x9b, 0x80,
+ 0x60, 0x62, 0x28, 0x36, 0x84, 0x7e, 0x4d, 0xa5, 0xa0, 0xc6, 0xb6, 0x28,
+ 0xc6, 0xa3, 0xda, 0xeb, 0x6d, 0x74, 0xb7, 0x99, 0x9f, 0xbb, 0x00, 0x33,
+ 0xea, 0x67, 0x9c, 0xd9, 0x8c, 0x6c, 0xc3, 0x3b, 0x0c, 0xc6, 0x26, 0xcc,
+ 0x47, 0x5c, 0x7a, 0xae, 0x64, 0x9e, 0x30, 0x31, 0xda, 0xd2, 0xe5, 0x9d,
+ 0xe9, 0xc4, 0xff, 0xd0, 0x2e, 0x1d, 0x89, 0xa5, 0xb8, 0x78, 0xab, 0xb0,
+ 0xc9, 0x8d, 0xe1, 0xef, 0x5d, 0x22, 0x5d, 0xc5, 0x15, 0x82, 0x0b, 0x35,
+ 0x06, 0xc0, 0xef, 0x81, 0x1a, 0x02, 0x17, 0x86, 0x41, 0xb6, 0xfd, 0x1d,
+ 0x3f, 0x58, 0x3f, 0xf8, 0x52, 0xf2, 0x4b, 0x96, 0x75, 0xb2, 0x01, 0x76,
+ 0x82, 0x1c, 0x6e, 0x2d, 0x0e, 0xf2, 0x32, 0xd0, 0x80, 0x05, 0xeb, 0xa3,
+ 0x50, 0x3e, 0xa0, 0x48, 0xd3, 0xd7, 0xed, 0xa3, 0x3a, 0xb6, 0x8d, 0x03,
+ 0xc5, 0x4c, 0xd6, 0xae, 0x7b, 0xf0, 0xb9, 0x51, 0x9c, 0xd7, 0xcd, 0x25,
+ 0x9d, 0xf7, 0x0f, 0x99, 0x6f, 0x8b, 0x4c, 0xcb, 0x31, 0x07, 0xb2, 0xbd,
+ 0x1e, 0x49, 0x6b, 0xd5, 0x26, 0x0c, 0xda, 0x01, 0x73, 0x78, 0x0e, 0xfd,
+ 0x57, 0xca, 0x3e, 0xc8, 0x20, 0x35, 0x05, 0xb7, 0x68, 0x5d, 0x22, 0xe9,
+ 0xc3, 0x7b, 0x13, 0xac, 0x90, 0xe1, 0x93, 0x2f, 0xcd, 0xad, 0x5f, 0x9a,
+ 0x1f, 0x25, 0xb3, 0xed, 0xb5, 0xe7, 0xa6, 0xb6, 0x76, 0x4f, 0xdc, 0xcb,
+ 0xff, 0x71, 0x59, 0x8e, 0x3d, 0xbb, 0x0f, 0xe7, 0x87, 0x88, 0x49, 0xf1,
+ 0x1e, 0x00, 0x3d, 0x0a, 0x80, 0x7c, 0x8e, 0x33, 0x39, 0x6a, 0xcb, 0x10,
+ 0x54, 0xd5, 0xcc, 0xa6, 0xb7, 0x67, 0x0a, 0x19, 0x0c, 0x04, 0x24, 0xf9,
+ 0x4f, 0xaa, 0x08, 0xf8, 0x81, 0x88, 0xe8, 0x82, 0xb0, 0xac, 0x1c, 0x19,
+ 0x23, 0x4f, 0xad, 0xa5, 0xb2, 0x2f, 0xa7, 0x23, 0xda, 0x73, 0xdf, 0xdf,
+ 0xad, 0x39, 0x3b, 0x1e, 0xb7, 0x96, 0xb3, 0x38, 0x0e, 0x70, 0x2f, 0xf6,
+ 0x85, 0xae, 0x0a, 0xa5, 0xe3, 0x41, 0x10, 0x7b, 0x00, 0x1c, 0x6f, 0xba,
+ 0x85, 0x3b, 0xf4, 0x29, 0xb3, 0x74, 0xb0, 0x08, 0x2c, 0x8d, 0xda, 0xf6,
+ 0x39, 0x7b, 0x08, 0xf2, 0x91, 0x3c, 0xb0, 0x74, 0x89, 0x3a, 0xea, 0xab,
+ 0x8f, 0x4f, 0x67, 0xb1, 0x49, 0x95, 0x08, 0x7d, 0x0b, 0xfb, 0x93, 0x3a,
+ 0x93, 0x1a, 0x80, 0x1c, 0xe0, 0x5a, 0x92, 0xc1, 0xe8, 0xfb, 0xbc, 0x73,
+ 0xc9, 0x53, 0xe3, 0xae, 0x92, 0x6d, 0x7c, 0x00, 0x0a, 0xc4, 0xb0, 0xc7,
+ 0x46, 0x78, 0xfc, 0xc5, 0x00, 0xf6, 0xbf, 0xa2, 0xee, 0xf9, 0x40, 0xdf,
+ 0xc3, 0xdd, 0x4d, 0xc0, 0xf6, 0x04, 0xcb, 0xb2, 0x69, 0x41, 0x08, 0xd8,
+ 0xe9, 0xd2, 0x43, 0xa5, 0xca, 0x04, 0x7d, 0x4c, 0xf4, 0x43, 0xcb, 0x2a,
+ 0x27, 0xb3, 0xf5, 0x0e, 0x3c, 0xaf, 0x69, 0x8e, 0x8e, 0x08, 0x66, 0xa3,
+ 0xf1, 0x80, 0x31, 0xf3, 0x71, 0x30, 0x59, 0xc1, 0x27, 0x10, 0xb7, 0x15,
+ 0x4e, 0x97, 0xe9, 0x46, 0x65, 0x89, 0xc2, 0xd9, 0x83, 0xea, 0xa2, 0x27,
+ 0x1e, 0x97, 0xa6, 0x58, 0xf6, 0xc9, 0x33, 0x75, 0x98, 0x1e, 0x03, 0xff,
+ 0x5e, 0x46, 0x01, 0x24, 0xdb, 0x7f, 0xed, 0xa9, 0x6a, 0xb5, 0xc4, 0x5f,
+ 0x48, 0xb7, 0x32, 0xc6, 0xb1, 0x7e, 0x65, 0x15, 0x01, 0x3a, 0x91, 0xa9,
+ 0xa8, 0xa1, 0x1f, 0x5a, 0x56, 0xe2, 0x70, 0xcb, 0xc3, 0xa1, 0x96, 0xc5,
+ 0x34, 0x6b, 0x31, 0x11, 0xe6, 0x8b, 0x42, 0xac, 0xea, 0x4c, 0xf0, 0x60,
+ 0x89, 0x54, 0x5b, 0x91, 0x02, 0x3d, 0xf5, 0x39, 0xfc, 0x6a, 0x0c, 0x8d,
+ 0xb7, 0x37, 0x02, 0x4c, 0xb6, 0x07, 0x50, 0x7d, 0xb8, 0xd6, 0xff, 0xe9,
+ 0x0e, 0x18, 0xd7, 0xff, 0x06, 0x73, 0x22, 0x95, 0x9c, 0x8f, 0x68, 0x2a,
+ 0x71, 0x73, 0xa4, 0x1f, 0xc6, 0x7a, 0x89, 0xaa, 0x9f, 0x44, 0xaf, 0x06,
+ 0xdc, 0x3b, 0x82, 0x3d, 0xcc, 0x60, 0xd9, 0x2d, 0xf9, 0xb7, 0xc1, 0x83,
+ 0x35, 0x9a, 0x00, 0x7c, 0xf7, 0x54, 0x71, 0x16, 0x3a, 0xf6, 0x61, 0x7d,
+ 0x80, 0xf5, 0x2b, 0xcf, 0x91, 0xf4, 0x4c, 0x63, 0xcd, 0xa2, 0xcb, 0x60,
+ 0x58, 0x67, 0xe8, 0x7f, 0xc3, 0x34, 0x61, 0x01, 0x46, 0x36, 0xcf, 0xda,
+ 0x11, 0x56, 0x06, 0x64, 0x2a, 0x59, 0xfe, 0xb3, 0x9a, 0xdd, 0x9d, 0xef,
+ 0x30, 0xbe, 0xc4, 0x92, 0x00, 0xd1, 0x4e, 0x34, 0x45, 0x3c, 0x33, 0x8f,
+ 0xa4, 0x0b, 0x7e, 0x83, 0xc4, 0xde, 0x44, 0x31, 0x1f, 0x69, 0xf1, 0x6a,
+ 0x94, 0x1f, 0xf4, 0x50, 0x7c, 0xe9, 0xdb, 0x68, 0xdb, 0x4d, 0x4e, 0xde,
+ 0x1d, 0xec, 0x50, 0x67, 0x96, 0x61, 0x93, 0x2c, 0x30, 0x56, 0x83, 0x9d,
+ 0x94, 0x08, 0xb1, 0x58, 0x4c, 0x23, 0x85, 0xaa, 0x5b, 0x14, 0x17, 0x5b,
+ 0x12, 0x38, 0xa6, 0x2f, 0x57, 0x0e, 0x44, 0xaf, 0xbc, 0xa5, 0xe7, 0x88,
+ 0x66, 0xbb, 0x9f, 0x50, 0xd6, 0xb8, 0x7b, 0xfc, 0x4b, 0x28, 0xbd, 0xd3,
+ 0x08, 0xf5, 0x28, 0xc6, 0xf5, 0xf3, 0x1e, 0xdb, 0xcf, 0x90, 0x6b, 0x03,
+ 0x75, 0x23, 0xbc, 0xe2, 0x56, 0x35, 0x21, 0x65, 0x7c, 0x77, 0x77, 0xe3,
+ 0x01, 0xf7, 0xac, 0xdd, 0x15, 0x1e, 0x13, 0xc8, 0x7c, 0x0a, 0xb9, 0xc1,
+ 0x64, 0xcf, 0xfd, 0xfd, 0x71, 0xce, 0x8a, 0x47, 0x53, 0xe3, 0x4b, 0x6c,
+ 0xa1, 0xcd, 0x22, 0x24, 0xeb, 0xda, 0xed, 0xfd, 0xc2, 0xe5, 0xde, 0x96,
+ 0x01, 0x1a, 0xb6, 0x39, 0x37, 0xd1, 0xe0, 0xce, 0xec, 0xb5, 0xee, 0xc5,
+ 0x89, 0xcb, 0x10, 0xcb, 0x70, 0x88, 0xfa, 0x17, 0x88, 0xf2, 0xad, 0xf8,
+ 0x39, 0xb4, 0x7e, 0x9f, 0xd4, 0xc0, 0x49, 0xd1, 0x5a, 0xc5, 0xbd, 0x8a,
+ 0xb2, 0x5d, 0xf4, 0x87, 0x35, 0xee, 0x74, 0xfe, 0x14, 0x79, 0xb0, 0xb9,
+ 0x16, 0x90, 0xbf, 0x1c, 0x48, 0x95, 0x18, 0x98, 0x8a, 0x41, 0xa4, 0x99,
+ 0x3d, 0x0f, 0xbe, 0x42, 0xa2, 0x78, 0xc4, 0xeb, 0xfd, 0xbd, 0x02, 0xd3,
+ 0x82, 0x4b, 0xaf, 0x3c, 0x5b, 0x0b, 0x23, 0x5f, 0xf3, 0x33, 0x1a, 0x08,
+ 0x91, 0xf9, 0xdb, 0x91, 0xdf, 0x41, 0x61, 0x66, 0x80, 0x57, 0xe7, 0x13,
+ 0x97, 0x2e, 0xdf, 0x74, 0xe7, 0xc1, 0xbf, 0xe3, 0xf7, 0xb0, 0xf8, 0xcd,
+ 0x1e, 0x67, 0xae, 0x53, 0x42, 0x0b, 0x51, 0x30, 0x4c, 0x66, 0xc2, 0x9e,
+ 0x83, 0x0d, 0x17, 0x16, 0x19, 0xcf, 0x68, 0x6e, 0x97, 0xf0, 0xa6, 0x69,
+ 0x24, 0xe1, 0x5a, 0xbd, 0x6e, 0x9c, 0x57, 0x51, 0x97, 0xa7, 0x3b, 0x28,
+ 0xfc, 0x41, 0xf5, 0xc3, 0x19, 0xb4, 0x90, 0x0d, 0x1f, 0x0d, 0xa2, 0x04,
+ 0x21, 0x39, 0x62, 0x6c, 0x01, 0x0d, 0xd8, 0xd7, 0x0b, 0x13, 0xd6, 0x29,
+ 0xf6, 0x89, 0x3f, 0x19, 0xc9, 0x00, 0xfb, 0xaf, 0x92, 0xa3, 0x9f, 0x22,
+ 0xcc, 0x5c, 0x45, 0xea, 0xd7, 0xca, 0x38, 0x0e, 0xc8, 0xd2, 0x32, 0x9c,
+ 0x2b, 0x10, 0xd0, 0xcd, 0xea, 0xc4, 0xc1, 0xe6, 0x6c, 0x72, 0xab, 0xd0,
+ 0x17, 0xc3, 0xdd, 0x4d, 0x4c, 0x10, 0xaf, 0xae, 0x42, 0x20, 0x2e, 0x17,
+ 0x9e, 0x67, 0xab, 0x34, 0xa5, 0x2a, 0xf8, 0x01, 0xec, 0xf6, 0x31, 0x22,
+ 0x3d, 0x42, 0x25, 0x63, 0x81, 0x14, 0x4f, 0xe9, 0xe3, 0x80, 0x72, 0x16,
+ 0x80, 0x75, 0x77, 0xf3, 0x06, 0x76, 0xa5, 0x34, 0xef, 0xc0, 0x30, 0x40,
+ 0xe0, 0x68, 0x70, 0x30, 0x9d, 0x65, 0x9d, 0xb3, 0x74, 0x79, 0x1b, 0xe3,
+ 0xc4, 0x79, 0xcd, 0xdc, 0xe3, 0x63, 0xa0, 0x45, 0x1a, 0x78, 0xd2, 0xf5,
+ 0xe8, 0xb6, 0x5a, 0x49, 0x92, 0x69, 0x58, 0x5f, 0x1a, 0xb9, 0x1e, 0x30,
+ 0xbe, 0xaf, 0xbb, 0x5b, 0xf5, 0xd2, 0x6a, 0x3b, 0x58, 0xb8, 0x15, 0xd8,
+ 0x93, 0x86, 0xe0, 0x24, 0xe1, 0x66, 0x91, 0x87, 0x0b, 0x07, 0x03, 0x95,
+ 0x58, 0xc8, 0xc0, 0x59, 0x16, 0xa9, 0x1d, 0x4b, 0xd8, 0x5e, 0xf2, 0xb7,
+ 0x8f, 0xb1, 0xb1, 0x2f, 0x72, 0xd7, 0xcb, 0x77, 0x26, 0xf6, 0x06, 0x93,
+ 0xa4, 0x1f, 0x40, 0x48, 0xad, 0xf9, 0x62, 0xf7, 0xf0, 0xab, 0x41, 0x02,
+ 0xc2, 0xcc, 0xc1, 0x92, 0x27, 0x65, 0x7e, 0xad, 0x59, 0x81, 0x16, 0xb9,
+ 0x85, 0x9b, 0xf6, 0x90, 0x27, 0xf0, 0xd6, 0x84, 0x83, 0x30, 0xda, 0x07,
+ 0x1f, 0x92, 0x49, 0xfc, 0xa3, 0x1b, 0xb4, 0x03, 0x18, 0xda, 0xde, 0x45,
+ 0x99, 0x7b, 0x67, 0x9c, 0x54, 0xcc, 0x5f, 0xb1, 0xe9, 0x32, 0x01, 0x0b,
+ 0xe0, 0x54, 0xd5, 0xcb, 0xfd, 0x3b, 0x49, 0x9d, 0x51, 0xee, 0xf1, 0xe1,
+ 0x62, 0x3e, 0x4e, 0x07, 0xf9, 0x2b, 0x70, 0x62, 0x76, 0x8b, 0x9a, 0x9a,
+ 0x47, 0xde, 0x10, 0x13, 0x74, 0x15, 0xc6, 0x0a, 0x69, 0xc3, 0xf1, 0x1e,
+ 0x9c, 0x7f, 0xba, 0xab, 0x1f, 0xb3, 0xc4, 0x1c, 0x26, 0x0c, 0x38, 0xdf,
+ 0x90, 0xb8, 0x15, 0x37, 0x67, 0x91, 0x7e, 0x11, 0x7a, 0x0f, 0xdc, 0xee,
+ 0x69, 0x8a, 0x73, 0xa1, 0x26, 0xc0, 0x0f, 0xcb, 0x41, 0xf7, 0x0b, 0xdc,
+ 0x95, 0x4a, 0x0e, 0x3e, 0x23, 0xdf, 0x7d, 0x4b, 0x82, 0xa4, 0x50, 0xad,
+ 0x9e, 0x49, 0x6b, 0xdd, 0x71, 0x18, 0x9a, 0x9f, 0x75, 0x40, 0x48, 0x50,
+ 0xbb, 0xfd, 0x84, 0x16, 0xc7, 0x20, 0x8f, 0x76, 0x6d, 0x5f, 0x4f, 0x74,
+ 0x45, 0xed, 0xf8, 0x58, 0x7d, 0xad, 0x80, 0x7c, 0xc2, 0x31, 0xe2, 0x10,
+ 0xef, 0xc8, 0xee, 0xbb, 0x27, 0xe3, 0xc9, 0x1e, 0x83, 0x2d, 0xc3, 0x1e,
+ 0x70, 0x1c, 0x73, 0x11, 0x56, 0x17, 0xb4, 0xbd, 0x73, 0xdd, 0x4c, 0x61,
+ 0x3e, 0xb8, 0x60, 0x83, 0xf6, 0x0c, 0xc0, 0xfe, 0x87, 0xc1, 0x34, 0xa0,
+ 0x9d, 0x34, 0x69, 0xa6, 0x81, 0x76, 0x7b, 0x5b, 0x47, 0xb0, 0xe9, 0xf2,
+ 0x52, 0x38, 0x04, 0x3a, 0x41, 0x98, 0x1a, 0x3d, 0xf9, 0x3a, 0x28, 0x0a,
+ 0x80, 0x9c, 0x73, 0x08, 0xfd, 0xcd, 0x95, 0xa8, 0xe2, 0x3b, 0xfa, 0x96,
+ 0xb5, 0xae, 0xfd, 0x82, 0xea, 0xcb, 0xeb, 0x8f, 0xeb, 0xe4, 0x69, 0x46,
+ 0x07, 0x33, 0xcf, 0x46, 0xd6, 0x27, 0x62, 0x9e, 0xad, 0x68, 0x89, 0xf9,
+ 0xa1, 0x9b, 0x42, 0x25, 0xfb, 0xe8, 0x67, 0xde, 0xf0, 0xc5, 0x16, 0x12,
+ 0x55, 0xc7, 0x9b, 0x45, 0x5b, 0xd0, 0xfc, 0xb7, 0xb9, 0xea, 0x3e, 0xb5,
+ 0x52, 0xf5, 0x2c, 0x68, 0x18, 0xae, 0xe9, 0x60, 0x40, 0x9e, 0xea, 0xdb,
+ 0xab, 0x8b, 0x93, 0x37, 0x07, 0x23, 0x1a, 0x42, 0x25, 0x68, 0x3a, 0xae,
+ 0x11, 0xb1, 0x41, 0x9f, 0xd5, 0xa5, 0x08, 0x58, 0xba, 0x7a, 0x8e, 0x82,
+ 0xce, 0x95, 0xbb, 0x27, 0x83, 0xab, 0xda, 0x8c, 0x5d, 0xf9, 0x85, 0xee,
+ 0x19, 0x6b, 0xbe, 0x71, 0x80, 0xd5, 0x79, 0x60, 0x2e, 0xa8, 0xd1, 0x96,
+ 0x92, 0xab, 0x40, 0x58, 0x9c, 0x4a, 0x15, 0x44, 0x72, 0xa3, 0x9e, 0x9e,
+ 0xa5, 0xe9, 0x1f, 0xc2, 0x33, 0x29, 0x73, 0x23, 0x3b, 0xed, 0x5d, 0xdc,
+ 0xce, 0x69, 0x43, 0xca, 0x96, 0x26, 0xcd, 0x58, 0xda, 0x78, 0x05, 0xe9,
+ 0xfa, 0x13, 0x60, 0x57, 0x96, 0x58, 0x1c, 0xea, 0x63, 0xb0, 0xb5, 0xcb,
+ 0x96, 0xa8, 0x2f, 0x28, 0x41, 0x5f, 0x7e, 0xcb, 0x76, 0x99, 0x31, 0xcc,
+ 0xc3, 0xec, 0xd4, 0x34, 0xf9, 0x18, 0x2f, 0x7c, 0xa0, 0x5a, 0xf2, 0xe0,
+ 0xdb, 0xea, 0xbb, 0x12, 0x9c, 0x55, 0xb9, 0x68, 0x9d, 0xd7, 0xe5, 0x93,
+ 0x88, 0x52, 0xef, 0xd5, 0x7f, 0x0a, 0x80, 0xf9, 0x68, 0xe3, 0xfb, 0x5e,
+ 0xb0, 0x3b, 0xb3, 0xdc, 0xe4, 0xc6, 0xe1, 0x23, 0xbe, 0x8b, 0xf0, 0xc3,
+ 0xa6, 0x1d, 0x56, 0x14, 0x2d, 0xa4, 0xa7, 0xe8, 0x08, 0x30, 0x29, 0xb8,
+ 0x1e, 0x0d, 0x77, 0xc6, 0x7c, 0xa3, 0x7c, 0x74, 0x34, 0xf3, 0xf8, 0x4e,
+ 0xac, 0x22, 0x35, 0xb0, 0x02, 0x2a, 0x46, 0xc0, 0x32, 0x83, 0x13, 0x80,
+ 0x5a, 0xc8, 0x56, 0xbd, 0x79, 0x79, 0x9d, 0xe1, 0xd6, 0x6e, 0xd7, 0x2a,
+ 0xc0, 0xf5, 0x17, 0xa8, 0x15, 0xe1, 0xf8, 0xd1, 0x50, 0x0b, 0xd6, 0x3f,
+ 0x6a, 0xc8, 0x39, 0xb1, 0x95, 0xab, 0x8c, 0x06, 0xe3, 0xa2, 0x4e, 0xd1,
+ 0x81, 0x06, 0xb5, 0x1b, 0x34, 0x06, 0xb4, 0x49, 0x7b, 0xe7, 0x07, 0x38,
+ 0xca, 0x05, 0xd9, 0x4c, 0x1a, 0x7c, 0x3a, 0xdc, 0xc5, 0xec, 0xb8, 0x29,
+ 0x19, 0xc9, 0x0d, 0x84, 0x22, 0x62, 0x2b, 0xcb, 0xd3, 0x30, 0xb5, 0xe1,
+ 0xeb, 0xdf, 0x59, 0x75, 0x61, 0xa7, 0x58, 0x83, 0xd9, 0x3a, 0x11, 0x14,
+ 0x5a, 0x26, 0x1b, 0x98, 0x27, 0xf9, 0x11, 0x64, 0xcd, 0xf1, 0xe9, 0xf2,
+ 0xf3, 0x2c, 0x71, 0x2d, 0x7b, 0xda, 0xde, 0xc1, 0x18, 0xe2, 0xb4, 0x7c,
+ 0xcf, 0x0c, 0xd6, 0x1a, 0x81, 0x89, 0xab, 0x24, 0x0c, 0xa0, 0x6f, 0xfd,
+ 0x9c, 0x70, 0x70, 0xcd, 0xad, 0x77, 0x30, 0xf0, 0x40, 0x65, 0xdc, 0x25,
+ 0xc2, 0xc2, 0x19, 0xa8, 0x2f, 0x4e, 0x2a, 0xa8, 0xc3, 0xee, 0xd2, 0x6e,
+ 0x4f, 0xc1, 0x8c, 0x06, 0x47, 0x60, 0x36, 0x12, 0x02, 0xbc, 0x96, 0x44,
+ 0x50, 0xc7, 0x1b, 0xcb, 0x52, 0x09, 0x4f, 0x44, 0xd2, 0xe3, 0x59, 0x77,
+ 0x82, 0x3c, 0x6e, 0x0f, 0x29, 0xa1, 0x91, 0xeb, 0xc0, 0xec, 0x04, 0x13,
+ 0x4f, 0x7f, 0xd9, 0xeb, 0x9d, 0xd1, 0x2a, 0xea, 0xf4, 0xee, 0xee, 0x93,
+ 0x85, 0x73, 0x93, 0x63, 0x99, 0x74, 0x6a, 0xbe, 0x8d, 0x4a, 0x64, 0xe0,
+ 0x1c, 0x89, 0x43, 0xfd, 0x6c, 0x02, 0xd5, 0xa9, 0xec, 0x62, 0xfc, 0xd8,
+ 0x61, 0x24, 0xe8, 0x58, 0x3d, 0x6e, 0x14, 0x63, 0x68, 0xa6, 0xea, 0x49,
+ 0xaa, 0xdd, 0x84, 0xcb, 0xe9, 0xc3, 0x8f, 0x83, 0x1e, 0x3b, 0x7b, 0x3a,
+ 0xd1, 0x71, 0x5e, 0x72, 0x5d, 0x60, 0x2c, 0xd8, 0xb0, 0x44, 0x19, 0x97,
+ 0x1b, 0x9f, 0x57, 0x28, 0xb8, 0xe4, 0x8e, 0xe2, 0x02, 0x2f, 0x38, 0x7f,
+ 0x86, 0xc6, 0xc3, 0x78, 0x1f, 0x87, 0x2f, 0xda, 0x13, 0x13, 0x49, 0x11,
+ 0x49, 0x36, 0x1c, 0xe2, 0x37, 0x5d, 0x6f, 0x4d, 0x86, 0xb4, 0x45, 0x6b,
+ 0xc1, 0x54, 0x26, 0x82, 0x72, 0x6d, 0x63, 0x4a, 0x3e, 0xbb, 0x96, 0x90,
+ 0x47, 0xb7, 0x0c, 0xc7, 0xd1, 0xfc, 0xbd, 0x0e, 0x15, 0x59, 0x80, 0xa7,
+ 0x47, 0xfe, 0x63, 0xd0, 0x3f, 0x9a, 0x24, 0x2a, 0x3c, 0xb9, 0xa8, 0x3f,
+ 0xa7, 0x67, 0x2c, 0xd7, 0xfb, 0x9f, 0x75, 0x56, 0x1b, 0xde, 0x73, 0x02,
+ 0x78, 0xbc, 0x4a, 0x2f, 0x39, 0x81, 0xf1, 0xee, 0x1d, 0xed, 0x36, 0xb7,
+ 0xb6, 0x91, 0x6b, 0xce, 0xff, 0x10, 0xe9, 0xf7, 0x3f, 0x69, 0x6b, 0x4d,
+ 0x3c, 0x74, 0xa1, 0x9f, 0x51, 0xcd, 0x12, 0xb0, 0x59, 0x10, 0x4e, 0xfe,
+ 0x83, 0x1f, 0x4e, 0x92, 0x2c, 0xfe, 0xaf, 0xcb, 0x9e, 0x17, 0x2f, 0xd9,
+ 0x05, 0x88, 0xf1, 0x25, 0x36, 0xcc, 0xeb, 0x6f, 0xbc, 0x47, 0xeb, 0x97,
+ 0x43, 0x8b, 0xd5, 0x2c, 0x50, 0x19, 0xdd, 0x33, 0x98, 0xd5, 0x86, 0xb6,
+ 0x3b, 0x1a, 0x5c, 0xb6, 0xdb, 0xe1, 0xc1, 0x6d, 0x84, 0x39, 0x23, 0x52,
+ 0x81, 0x21, 0x9c, 0xd5, 0x56, 0x71, 0xcb, 0xe1, 0xdc, 0x05, 0x81, 0xc1,
+ 0xa8, 0x92, 0x5c, 0x2d, 0xdf, 0xea, 0x30, 0x27, 0x82, 0x56, 0x26, 0x86,
+ 0xf5, 0x19, 0x7e, 0xed, 0xec, 0xda, 0xed, 0x80, 0x19, 0xdd, 0xe8, 0x31,
+ 0xbd, 0x18, 0x73, 0xea, 0x61, 0xd5, 0x48, 0x2e, 0x78, 0x21, 0x92, 0x88,
+ 0x0e, 0x9e, 0x82, 0xc0, 0xe0, 0x99, 0xf4, 0x6f, 0xfd, 0x6e, 0x88, 0x7c,
+ 0xbe, 0xeb, 0x2d, 0x89, 0x37, 0xa7, 0x03, 0xd0, 0x75, 0x5e, 0x69, 0x0d,
+ 0x63, 0x34, 0x51, 0x62, 0x51, 0x91, 0x88, 0x87, 0xb9, 0x7d, 0x3f, 0x90,
+ 0xc6, 0xb6, 0xd4, 0x8e, 0x8d, 0xe6, 0x55, 0xa8, 0x98, 0x8c, 0xa4, 0xd9,
+ 0xbb, 0x51, 0x4b, 0x85, 0x8c, 0x53, 0xef, 0xc7, 0x8d, 0xd9, 0x64, 0x92,
+ 0xde, 0x38, 0xb4, 0x70, 0x35, 0x04, 0xf2, 0x13, 0xb9, 0x7e, 0x61, 0x6e,
+ 0x91, 0x9c, 0x17, 0x2d, 0x05, 0x06, 0x9f, 0xe9, 0xaa, 0x86, 0x35, 0xcd,
+ 0x25, 0x9c, 0x59, 0x3e, 0x3c, 0x01, 0x67, 0x30, 0xbb, 0x32, 0x32, 0x27,
+ 0x0d, 0x01, 0x56, 0x1c, 0xd6, 0x1f, 0xd5, 0xa1, 0xc6, 0xc1, 0x73, 0xf1,
+ 0x7e, 0xa3, 0x66, 0xf0, 0xcc, 0x2b, 0x99, 0xe2, 0xf8, 0x96, 0x19, 0x25,
+ 0x5f, 0xa2, 0x50, 0x0f, 0xe2, 0xe1, 0x0f, 0xa7, 0x18, 0x28, 0x31, 0x1c,
+ 0x6e, 0xd4, 0xca, 0x7d, 0x77, 0x28, 0x63, 0x95, 0xff, 0x4a, 0xc0, 0x45,
+ 0x36, 0xf2, 0x0c, 0x19, 0x16, 0x8d, 0xfc, 0x93, 0x3a, 0xe0, 0x67, 0x08,
+ 0x05, 0x0f, 0xe2, 0xbd, 0xe9, 0xc1, 0x0b, 0xcc, 0x9c, 0xed, 0x5e, 0xbc,
+ 0x8c, 0x48, 0xaf, 0x74, 0x05, 0x96, 0xd7, 0x58, 0x66, 0x5e, 0x1f, 0x0e,
+ 0x95, 0x1f, 0xd3, 0xef, 0xa7, 0x45, 0xaf, 0x6d, 0xd0, 0x3b, 0xe3, 0x19,
+ 0x96, 0x45, 0x12, 0x32, 0xfd, 0x11, 0x6b, 0xd0, 0xe7, 0x44, 0x18, 0x2d,
+ 0x71, 0x71, 0x36, 0x99, 0x9f, 0x11, 0x67, 0x11, 0x0b, 0x4e, 0x79, 0xac,
+ 0x49, 0x79, 0x91, 0x35, 0x87, 0xdd, 0xc1, 0xa2, 0xb2, 0x76, 0xae, 0x90,
+ 0x76, 0x51, 0x60, 0x51, 0x8b, 0x5c, 0xff, 0x3b, 0xc9, 0x39, 0x55, 0x79,
+ 0xd3, 0xa9, 0x27, 0xe0, 0xc6, 0xb2, 0x17, 0x6a, 0x9a, 0x35, 0xdb, 0xb6,
+ 0x0e, 0x2a, 0xa0, 0x17, 0x53, 0xa3, 0x83, 0x3e, 0x3b, 0x15, 0xd0, 0x61,
+ 0xf6, 0x50, 0xc1, 0xa8, 0xf6, 0x86, 0x86, 0x22, 0x6e, 0x9b, 0x3e, 0xe1,
+ 0xdb, 0x04, 0x04, 0xa4, 0x6a, 0x96, 0x32, 0x02, 0xf1, 0xa5, 0xd3, 0xc0,
+ 0x39, 0xb7, 0xb6, 0x35, 0xaa, 0x5e, 0x0a, 0x75, 0x50, 0x9d, 0x20, 0x82,
+ 0x73, 0x3e, 0x6d, 0xde, 0x5b, 0x28, 0x11, 0xf5, 0x63, 0xab, 0xe5, 0x1a,
+ 0x55, 0x02, 0xd2, 0x64, 0xcb, 0x2c, 0x6b, 0x2e, 0x8c, 0x17, 0xdb, 0x8c,
+ 0x7c, 0xea, 0xa5, 0x43, 0xe2, 0xb4, 0xa8, 0x4f, 0xa9, 0x52, 0x9e, 0x45,
+ 0x59, 0xc3, 0x1b, 0x7d, 0xf9, 0x4a, 0xeb, 0x86, 0x0b, 0x56, 0xc5, 0x08,
+ 0xa8, 0xb6, 0xec, 0x2c, 0xc8, 0x22, 0x69, 0xe3, 0x0d, 0x49, 0xa3, 0x42,
+ 0x32, 0x55, 0xce, 0x33, 0x04, 0xeb, 0xb9, 0x85, 0x5f, 0x12, 0x1c, 0x97,
+ 0xc2, 0x2c, 0x9a, 0xac, 0x2e, 0x92, 0xe8, 0x67, 0xc1, 0x54, 0xd9, 0x24,
+ 0x1d, 0xc8, 0xe8, 0x34, 0x30, 0x65, 0xee, 0xb8, 0x03, 0xfb, 0xcd, 0xcf,
+ 0xfa, 0xca, 0x9d, 0xf8, 0x2d, 0x63, 0x69, 0x6f, 0x44, 0x9d, 0x8b, 0xb1,
+ 0xc6, 0x4e, 0x9a, 0xd5, 0xe0, 0xfc, 0xae, 0x28, 0xd8, 0xe2, 0xb6, 0x77,
+ 0x73, 0xd4, 0x17, 0x34, 0xb5, 0x30, 0x6e, 0x5c, 0x69, 0xc5, 0xcf, 0xa9,
+ 0x8d, 0x4f, 0x60, 0x52, 0x0e, 0xc2, 0x07, 0x6c, 0x3c, 0x6c, 0x6f, 0xc3,
+ 0xc8, 0xa1, 0xea, 0xc2, 0xbf, 0x36, 0xe3, 0xf1, 0x80, 0x0b, 0x30, 0x57,
+ 0x05, 0xa6, 0xe0, 0xcd, 0x0a, 0x68, 0x56, 0xcb, 0xfc, 0x18, 0xbf, 0xef,
+ 0xf9, 0x96, 0x10, 0x07, 0xd2, 0x77, 0xd4, 0x53, 0xcc, 0x50, 0x84, 0x39,
+ 0x1f, 0x04, 0x72, 0xb3, 0x02, 0x67, 0xb7, 0x8f, 0x32, 0xba, 0xd3, 0x4f,
+ 0x20, 0x2f, 0xbb, 0xbe, 0x38, 0xf7, 0x68, 0x5a, 0x75, 0x5a, 0x16, 0x30,
+ 0x35, 0x3e, 0x29, 0x5d, 0xc2, 0x94, 0x12, 0xb0, 0xeb, 0x6e, 0xa8, 0xd5,
+ 0x6f, 0xc8, 0xcb, 0xc6, 0x90, 0x85, 0x49, 0x6b, 0xd3, 0xa0, 0xa8, 0x6c,
+ 0x3c, 0x49, 0x89, 0x94, 0x66, 0xa7, 0xca, 0x83, 0xa5, 0xf3, 0xe3, 0x4a,
+ 0x46, 0x4f, 0x55, 0xc2, 0x35, 0x71, 0x76, 0xb5, 0xe2, 0xc0, 0x19, 0x9b,
+ 0x83, 0xad, 0xdf, 0x11, 0x7e, 0xd3, 0x01, 0xbf, 0x2e, 0xd5, 0x6d, 0xd2,
+ 0x66, 0x8b, 0xac, 0xb6, 0x02, 0xb8, 0xd8, 0xe0, 0xc0, 0xf4, 0x85, 0xd4,
+ 0x5f, 0xf4, 0xd9, 0x83, 0xb7, 0x83, 0x5b, 0xa9, 0x70, 0xe7, 0x32, 0xdc,
+ 0x22, 0x23, 0xa0, 0x12, 0x5a, 0xbe, 0x18, 0x1e, 0x85, 0x35, 0xd3, 0xa5,
+ 0x6d, 0x2b, 0x8c, 0x25, 0x75, 0xfc, 0x86, 0xb2, 0x92, 0xac, 0xd6, 0x76,
+ 0xaf, 0x5b, 0x3b, 0xd0, 0x9a, 0xae, 0xb7, 0xde, 0xa1, 0xac, 0x87, 0xa2,
+ 0x28, 0x07, 0x90, 0x76, 0x30, 0x3a, 0x81, 0x76, 0x30, 0xa7, 0x8b, 0x6b,
+ 0x48, 0x7b, 0xae, 0xd5, 0xdf, 0xf5, 0xb3, 0x3f, 0x2b, 0x02, 0x38, 0x07,
+ 0x5e, 0x7e, 0xab, 0x2d, 0xea, 0xb4, 0x66, 0x71, 0xad, 0x1e, 0xe6, 0x0f,
+ 0xaa, 0xe1, 0x14, 0x98, 0x2f, 0xef, 0x71, 0xa7, 0x2a, 0xa7, 0x7a, 0x1e,
+ 0x84, 0x06, 0xc9, 0xfc, 0x43, 0x33, 0xc0, 0x33, 0x30, 0xff, 0xcc, 0x21,
+ 0xc1, 0x82, 0x8b, 0x84, 0x9c, 0x55, 0x95, 0x29, 0x3a, 0xd4, 0x2a, 0x44,
+ 0x03, 0xe4, 0xbf, 0x89, 0x04, 0x6e, 0x8f, 0x8e, 0xf9, 0xf0, 0x11, 0x03,
+ 0x7f, 0x4b, 0xa8, 0x92, 0x96, 0x20, 0x24, 0x1d, 0xfd, 0xb9, 0x5d, 0xb7,
+ 0xff, 0x1b, 0x69, 0x52, 0x5f, 0x51, 0x9f, 0x8f, 0x71, 0x3c, 0x40, 0x20,
+ 0x49, 0xbf, 0x89, 0xfb, 0x07, 0x76, 0xdc, 0xa8, 0x81, 0xf8, 0xb7, 0xe5,
+ 0x1b, 0x98, 0x39, 0xe2, 0xc7, 0xe0, 0x68, 0xf2, 0x98, 0x4a, 0xa3, 0x96,
+ 0x7b, 0xae, 0x4d, 0xef, 0x3a, 0xe2, 0x0a, 0x83, 0x42, 0x60, 0x58, 0x66,
+ 0x8f, 0xe0, 0x05, 0x97, 0x0b, 0x77, 0x42, 0x91, 0x4b, 0x35, 0xfe, 0xac,
+ 0xa9, 0xec, 0x9f, 0x4c, 0xb5, 0x9d, 0xc1, 0xfc, 0xd0, 0x1c, 0xd0, 0x8e,
+ 0x1e, 0x51, 0xa3, 0x92, 0x90, 0xb5, 0xcc, 0x56, 0xbb, 0x4d, 0xe3, 0x8c,
+ 0x8c, 0xa5, 0x6a, 0x4e, 0x45, 0x94, 0xdf, 0xfa, 0xa9, 0x21, 0x4a, 0x2c,
+ 0xf9, 0xc1, 0xfd, 0xec, 0xf7, 0xe4, 0x40, 0xe4, 0xc0, 0xb4, 0x7f, 0xaa,
+ 0x26, 0x94, 0x27, 0x79, 0x69, 0x60, 0x16, 0xe3, 0x0b, 0xea, 0xf4, 0x48,
+ 0x1f, 0xbe, 0xab, 0x3c, 0xb3, 0xae, 0x35, 0x23, 0xf2, 0x7e, 0x74, 0xa8,
+ 0xdb, 0x80, 0x56, 0xcc, 0x31, 0x96, 0xa3, 0x86, 0x7e, 0x52, 0x56, 0x67,
+ 0x24, 0x93, 0x8b, 0xd0, 0xbc, 0xca, 0x9f, 0xf9, 0xf4, 0x36, 0x9c, 0x85,
+ 0xed, 0x26, 0x94, 0x88, 0xd7, 0x12, 0x5f, 0x6f, 0x78, 0x65, 0x12, 0xd4,
+ 0x8d, 0xf4, 0x4b, 0x63, 0xa0, 0x29, 0x6a, 0xc5, 0x2b, 0xbb, 0x68, 0xf9,
+ 0xf3, 0xb1, 0xdd, 0x96, 0x02, 0x48, 0x8d, 0x05, 0xbd, 0xc1, 0x5a, 0x9f,
+ 0x67, 0x82, 0x59, 0xe1, 0x43, 0xf8, 0xbd, 0x38, 0x35, 0x6c, 0xcc, 0xdf,
+ 0xa2, 0x50, 0x4e, 0x93, 0xb0, 0xab, 0x60, 0xd8, 0x4f, 0x25, 0xa3, 0xe7,
+ 0x16, 0xf4, 0xf2, 0xc8, 0x29, 0xd1, 0x37, 0x3a, 0xbc, 0xf6, 0xcc, 0x54,
+ 0xe6, 0xa3, 0xa7, 0xa6, 0x7d, 0xc7, 0x57, 0x1d, 0x7b, 0xe9, 0x09, 0x5c,
+ 0x2e, 0xa9, 0x8a, 0x56, 0x5f, 0x1a, 0xe4, 0xb1, 0x4a, 0xff, 0xd1, 0x1f,
+ 0x84, 0x71, 0x60, 0xcf, 0x82, 0x3c, 0x8f, 0x51, 0x04, 0xe5, 0xe3, 0xd2,
+ 0x9b, 0xaf, 0x6f, 0x3c, 0xb6, 0x15, 0xc0, 0x2d, 0xb0, 0x9b, 0xa5, 0x3f,
+ 0x53, 0xd2, 0xc9, 0x92, 0xa0, 0x59, 0x9a, 0xb2, 0x56, 0x57, 0x0b, 0xca,
+ 0x5f, 0x9a, 0x95, 0xe6, 0xa1, 0xad, 0xa2, 0x94, 0x27, 0x12, 0x09, 0x00,
+ 0x1b, 0x79, 0xb1, 0x19, 0x2d, 0x66, 0xd0, 0xed, 0x6e, 0xd1, 0x71, 0x14,
+ 0xfc, 0x78, 0x9f, 0x5b, 0x4a, 0xc1, 0x41, 0x89, 0xd1, 0x92, 0x87, 0xdb,
+ 0x83, 0x94, 0xc1, 0x5b, 0x56, 0x3d, 0xca, 0x16, 0x75, 0x78, 0x58, 0x03,
+ 0x40, 0x47, 0xf1, 0xb0, 0xec, 0x4d, 0x61, 0xe7, 0xa2, 0x2c, 0x42, 0x43,
+ 0xf0, 0xe4, 0xe5, 0xb2, 0x81, 0xf6, 0x34, 0xd1, 0x17, 0xf3, 0xff, 0x0f,
+ 0xad, 0x20, 0xb9, 0xd7, 0x45, 0x6b, 0xf1, 0x62, 0x99, 0x49, 0xdf, 0xa7,
+ 0xd4, 0xea, 0xc3, 0xd4, 0x41, 0x04, 0xc6, 0x6c, 0x0b, 0xf3, 0x55, 0x90,
+ 0x4f, 0x33, 0x49, 0xe0, 0xa1, 0x72, 0xd2, 0xd8, 0xb9, 0x4c, 0x53, 0x64,
+ 0xdf, 0x46, 0xff, 0x39, 0x09, 0x68, 0x10, 0x66, 0x28, 0x05, 0xa6, 0xc2,
+ 0x35, 0xb4, 0x44, 0x73, 0xac, 0x88, 0xf6, 0x45, 0x94, 0x34, 0xb9, 0x50,
+ 0x94, 0xc3, 0xc1, 0xac, 0xde, 0x15, 0x95, 0x54, 0xad, 0x51, 0xc6, 0xa0,
+ 0x40, 0xbc, 0xe1, 0xa9, 0x4e, 0x8c, 0x24, 0x74, 0x46, 0x03, 0xbb, 0xd8,
+ 0x69, 0xd1, 0x2d, 0x1a, 0x2a, 0xc4, 0xb4, 0xc8, 0xa4, 0x25, 0x00, 0x57,
+ 0xb4, 0x4a, 0x19, 0xbd, 0xf5, 0x2f, 0x63, 0x6a, 0x9b, 0xa9, 0x0a, 0x0a,
+ 0x05, 0x15, 0xff, 0x01, 0x0a, 0x93, 0x84, 0x1b, 0x6c, 0x21, 0x6b, 0x34,
+ 0x8f, 0xcc, 0x36, 0xe7, 0xab, 0x99, 0x7b, 0xbc, 0x25, 0xb5, 0x83, 0x66,
+ 0x8d, 0xec, 0x5f, 0x7d, 0xd5, 0x4b, 0xf1, 0xcc, 0x18, 0x39, 0xd0, 0xd6,
+ 0xb1, 0xfb, 0x22, 0x41, 0xb0, 0x91, 0x66, 0x14, 0x40, 0xae, 0xa3, 0x49,
+ 0x39, 0xe2, 0x62, 0xf3, 0x3f, 0x18, 0xe2, 0xdf, 0x9a, 0x01, 0x4a, 0xfc,
+ 0xd1, 0x93, 0x68, 0x5f, 0xfc, 0x3a, 0x0a, 0xbf, 0x37, 0x4e, 0x8d, 0x30,
+ 0x2c, 0x87, 0x30, 0xc2, 0xd1, 0xa9, 0xac, 0xb9, 0x51, 0xdd, 0xcf, 0x40,
+ 0x8f, 0xde, 0x25, 0xe8, 0xf1, 0xa4, 0xd0, 0x99, 0xad, 0x45, 0x20, 0xdf,
+ 0x32, 0xb5, 0x1c, 0xfa, 0xe6, 0x38, 0xd0, 0x2e, 0x46, 0x23, 0x52, 0xe1,
+ 0x4f, 0x00, 0x65, 0xa9, 0x9d, 0xe0, 0xc6, 0x4c, 0x3c, 0x00, 0x57, 0x60,
+ 0xe8, 0x8f, 0xf0, 0x0b, 0x70, 0x88, 0x1c, 0xa2, 0x49, 0x46, 0xb5, 0x4d,
+ 0x00, 0xbc, 0x2c, 0x5d, 0xb9, 0x35, 0xa0, 0x0a, 0xbb, 0xf2, 0xf7, 0x0b,
+ 0x1f, 0xa5, 0x5a, 0x5f, 0x08, 0x9c, 0xa9, 0x1a, 0xdb, 0x86, 0x42, 0x0b,
+ 0xb1, 0x83, 0x12, 0x98, 0x15, 0x1f, 0x77, 0x80, 0x2b, 0x2c, 0xe4, 0x87,
+ 0x2a, 0x05, 0x35, 0x22, 0xef, 0xd5, 0xc4, 0xc7, 0x2a, 0xa6, 0xe2, 0x0c,
+ 0x13, 0x69, 0x18, 0x7c, 0x0c, 0x0b, 0xba, 0x7a, 0x6f, 0xd3, 0xb1, 0x67,
+ 0x4d, 0x60, 0x94, 0x78, 0xfc, 0x26, 0x3d, 0xec, 0xb4, 0x7b, 0xd1, 0xea,
+ 0x50, 0x39, 0xb4, 0xdd, 0xff, 0xe6, 0x0f, 0xde, 0x24, 0x60, 0x47, 0xff,
+ 0xf1, 0x96, 0xb8, 0x68, 0x18, 0x42, 0xce, 0x98, 0x6e, 0x76, 0x94, 0x09,
+ 0x5f, 0x98, 0xc6, 0x08, 0xaf, 0x01, 0x19, 0x21, 0x0f, 0xbb, 0x63, 0xb7,
+ 0x94, 0xe9, 0x95, 0xb0, 0xa6, 0x25, 0x8d, 0x04, 0x04, 0x35, 0xfc, 0x81,
+ 0xbe, 0xc7, 0x3a, 0x6a, 0xb5, 0xb2, 0x7a, 0xe5, 0x10, 0xcd, 0x7f, 0xe3,
+ 0x40, 0x97, 0xa0, 0x41, 0x99, 0xd7, 0x6d, 0xb1, 0x1f, 0x41, 0x8e, 0x6c,
+ 0xa6, 0x69, 0xca, 0x39, 0x43, 0x94, 0x9f, 0xa1, 0x12, 0x4e, 0xd5, 0xa7,
+ 0x21, 0x2c, 0x96, 0x75, 0x20, 0x84, 0x24, 0x4a, 0xff, 0x82, 0x29, 0x7d,
+ 0x78, 0xd6, 0xeb, 0x76, 0x92, 0x1e, 0xcb, 0x10, 0xca, 0xde, 0x6c, 0x2a,
+ 0xed, 0x66, 0xff, 0x24, 0xd2, 0x42, 0x6a, 0xf5, 0x0b, 0x72, 0xa0, 0xb1,
+ 0x16, 0x76, 0x00, 0x45, 0x18, 0xa5, 0xf9, 0xce, 0x20, 0xf8, 0x1f, 0x25,
+ 0x7a, 0x00, 0x2e, 0x28, 0xab, 0xcd, 0x0a, 0xef, 0x71, 0x4f, 0xe9, 0x88,
+ 0xc5, 0x29, 0x13, 0x09, 0x66, 0x98, 0x3f, 0x71, 0x07, 0xd1, 0xff, 0xb8,
+ 0x01, 0xc8, 0x14, 0xe8, 0xef, 0x73, 0x9b, 0xbe, 0xe9, 0x8f, 0x17, 0x7d,
+ 0x0c, 0x48, 0x34, 0x96, 0xe1, 0xab, 0x9e, 0x3c, 0xf0, 0x27, 0x9b, 0xe4,
+ 0x26, 0xab, 0xfc, 0x28, 0xf9, 0x44, 0xe4, 0x1e, 0xce, 0xba, 0xd2, 0x7d,
+ 0x9d, 0x25, 0xb8, 0x2f, 0x81, 0x74, 0xea, 0x27, 0x90, 0xc4, 0x15, 0x20,
+ 0xe3, 0x30, 0x05, 0xac, 0xd9, 0x35, 0xc6, 0x3c, 0x06, 0xf2, 0x71, 0xfd,
+ 0xaa, 0x54, 0xab, 0x4e, 0x07, 0xb8, 0xd8, 0x82, 0xc7, 0x83, 0x43, 0x39,
+ 0xa9, 0xa3, 0x9c, 0xc0, 0xaf, 0x49, 0xa8, 0x5c, 0x74, 0x5f, 0x59, 0x12,
+ 0x1c, 0xa9, 0xbf, 0xeb, 0x81, 0x34, 0xce, 0x3c, 0x2e, 0x4d, 0x0c, 0xe7,
+ 0x0c, 0x86, 0x47, 0xc2, 0x4c, 0xe0, 0x38, 0x7d, 0xf6, 0x03, 0x0b, 0x58,
+ 0x30, 0x78, 0x2b, 0xb5, 0xa3, 0xcc, 0xf5, 0xd7, 0x1f, 0x36, 0x59, 0x4c,
+ 0x83, 0xff, 0xfd, 0x9f, 0x8c, 0xaf, 0x55, 0xcf, 0x01, 0x4c, 0x4d, 0x6f,
+ 0xd9, 0x18, 0x3d, 0xb3, 0xfd, 0x82, 0x00, 0x34, 0x34, 0xc9, 0x40, 0xe4,
+ 0xc5, 0x93, 0x56, 0x8d, 0x2c, 0xa5, 0x3d, 0xc3, 0xbd, 0xb6, 0x15, 0x82,
+ 0x4a, 0x59, 0xda, 0x40, 0x54, 0x5d, 0x8a, 0xe9, 0x73, 0x63, 0x28, 0xc1,
+ 0x33, 0x65, 0x43, 0x10, 0x52, 0xb2, 0x95, 0xbf, 0x17, 0x85, 0x01, 0xf3,
+ 0xab, 0x87, 0x67, 0xb1, 0xea, 0x9a, 0x08, 0x4d, 0xab, 0x41, 0xd9, 0xb7,
+ 0x7c, 0x08, 0x2d, 0x69, 0x70, 0x84, 0x42, 0x96, 0xd6, 0x9e, 0x0e, 0x75,
+ 0xbb, 0x28, 0xa1, 0x83, 0x25, 0x4c, 0x2e, 0x80, 0x33, 0x08, 0x39, 0xdb,
+ 0x27, 0x38, 0x96, 0x16, 0x51, 0x00, 0x32, 0xb4, 0x9b, 0x6c, 0x3e, 0x09,
+ 0x82, 0x31, 0xed, 0xfb, 0x28, 0xa4, 0x8b, 0x98, 0x78, 0x9d, 0xcd, 0xd9,
+ 0xa9, 0x12, 0x28, 0x1e, 0x55, 0x5b, 0x76, 0xd5, 0x45, 0x86, 0x78, 0x47,
+ 0x65, 0xc1, 0x01, 0x45, 0x05, 0x09, 0xcb, 0x15, 0xcd, 0x36, 0xbd, 0x01,
+ 0xcf, 0xe5, 0x92, 0x37, 0xca, 0x2d, 0x92, 0x56, 0x12, 0x54, 0x9c, 0xe2,
+ 0x4d, 0x55, 0xb3, 0xfe, 0xf7, 0x99, 0x78, 0x5c, 0xe8, 0xff, 0xcf, 0x62,
+ 0x94, 0x6b, 0x22, 0xf1, 0x13, 0x83, 0x78, 0x07, 0x73, 0x59, 0x26, 0x28,
+ 0x3b, 0xd2, 0x89, 0x89, 0x07, 0xe7, 0xef, 0xc1, 0x60, 0xf0, 0x35, 0xa0,
+ 0xd7, 0x51, 0xef, 0x81, 0xfd, 0x98, 0xbf, 0xe4, 0x07, 0x14, 0x25, 0x72,
+ 0x2d, 0xa9, 0x41, 0x97, 0x36, 0xa9, 0xbe, 0x02, 0x60, 0x9e, 0xe5, 0x8e,
+ 0x18, 0x6f, 0x6e, 0x04, 0xef, 0xb5, 0x4f, 0x96, 0xbf, 0xa6, 0x1a, 0xd4,
+ 0x6f, 0x7e, 0xd9, 0xd2, 0x15, 0x8b, 0x67, 0x57, 0x03, 0x89, 0x5a, 0x3e,
+ 0x8a, 0xf5, 0xfe, 0xd5, 0xb8, 0x9c, 0x89, 0x76, 0x5d, 0xca, 0x40, 0x0a,
+ 0xf4, 0x0b, 0xdc, 0xe3, 0x2e, 0xa4, 0xbc, 0xd7, 0x40, 0x89, 0x10, 0x9e,
+ 0xeb, 0x3e, 0x6e, 0x9e, 0x1b, 0xbb, 0xaf, 0x87, 0xba, 0x4a, 0x4f, 0x6b,
+ 0x4d, 0xa5, 0xd7, 0xba, 0x31, 0xbc, 0x73, 0xe4, 0x7a, 0x00, 0xd6, 0x2e,
+ 0x0d, 0xc1, 0x8f, 0x66, 0xc3, 0xbb, 0x0b, 0x6f, 0x5f, 0x8e, 0x23, 0xe5,
+ 0xa9, 0xf5, 0x22, 0x19, 0x68, 0xeb, 0x9b, 0xe5, 0x86, 0xf1, 0x79, 0x02,
+ 0x92, 0xc2, 0x43, 0x94, 0x9e, 0xc8, 0x65, 0x07, 0x0c, 0x13, 0x5a, 0x4e,
+ 0x08, 0x05, 0x97, 0x0c, 0x2c, 0x2f, 0x50, 0x07, 0x0d, 0x51, 0x93, 0x5b,
+ 0xf9, 0xf0, 0x48, 0xff, 0x0a, 0xf3, 0x66, 0x5c, 0xe6, 0x2c, 0xa1, 0xf9,
+ 0xab, 0x19, 0xa9, 0xf7, 0x58, 0xa0, 0xc5, 0xc2, 0xbf, 0x3a, 0x2e, 0xb1,
+ 0x3e, 0xbf, 0x72, 0xb0, 0x49, 0x81, 0xd6, 0x8f, 0xaf, 0xda, 0x32, 0xec,
+ 0x1e, 0x98, 0x4b, 0x94, 0xe7, 0x18, 0x0e, 0x63, 0x9a, 0x81, 0xc6, 0x2d,
+ 0xb4, 0xb8, 0x32, 0xf4, 0x6d, 0xdb, 0xca, 0x1c, 0x42, 0x9f, 0x1f, 0x7e,
+ 0x17, 0x2c, 0xf0, 0x90, 0xb3, 0xf5, 0x7c, 0xb3, 0x7a, 0x86, 0xd8, 0x9b,
+ 0xdc, 0xe8, 0x0d, 0xb7, 0xea, 0x0f, 0xd4, 0xeb, 0xf2, 0x93, 0x21, 0xbd,
+ 0xb6, 0x89, 0x95, 0xf3, 0x0f, 0xd3, 0xf4, 0x95, 0x24, 0xde, 0xb3, 0x2d,
+ 0x21, 0x36, 0xff, 0xee, 0xcd, 0x42, 0xcc, 0x09, 0x36, 0x56, 0x1b, 0xfb,
+ 0xa9, 0x9b, 0x55, 0x90, 0x6e, 0xd7, 0x2a, 0xab, 0x2e, 0x57, 0xb5, 0x4c,
+ 0xc2, 0x1e, 0x6e, 0x8f, 0x7e, 0x4d, 0x16, 0x07, 0xe9, 0x31, 0xb0, 0x18,
+ 0x76, 0x32, 0x3d, 0x0d, 0xe9, 0xde, 0x63, 0xde, 0x9f, 0xdf, 0x0e, 0x5b,
+ 0x6d, 0x33, 0x7f, 0xd7, 0x53, 0x14, 0x58, 0x12, 0x46, 0x51, 0x07, 0x2e,
+ 0x5a, 0xec, 0x5d, 0x1f, 0x36, 0x28, 0x73, 0x54, 0x3c, 0x8c, 0xdf, 0x0e,
+ 0xe5, 0x18, 0x6d, 0x7b, 0x2b, 0x0a, 0x94, 0x7d, 0xa3, 0x86, 0xc2, 0x66,
+ 0xf5, 0x86, 0x90, 0x21, 0x43, 0x51, 0x86, 0x96, 0x49, 0x91, 0xff, 0x26,
+ 0x16, 0x92, 0x82, 0x73, 0x80, 0x82, 0x93, 0xfd, 0x0f, 0x24, 0xcd, 0x1e,
+ 0x69, 0x75, 0x9f, 0x78, 0x89, 0x68, 0xf3, 0x2b, 0x8b, 0xaa, 0x2b, 0x4a,
+ 0x05, 0x06, 0x01, 0xb9, 0x41, 0x53, 0x49, 0x80, 0x09, 0x35, 0x55, 0x4d,
+ 0xea, 0x0a, 0xd6, 0x54, 0x3d, 0xf1, 0x53, 0xc4, 0xe9, 0xf4, 0x54, 0xbf,
+ 0xd3, 0xe4, 0x59, 0x61, 0xcb, 0x11, 0x86, 0x10, 0x63, 0x5c, 0xa5, 0xad,
+ 0x21, 0xcd, 0x4e, 0x64, 0xb1, 0x4f, 0x98, 0x7a, 0xb6, 0x75, 0x10, 0xb2,
+ 0x1d, 0x0f, 0x44, 0xef, 0x04, 0x88, 0xee, 0x80, 0x0f, 0xd1, 0x43, 0x9e,
+ 0xc0, 0xf9, 0xdc, 0xfa, 0xfd, 0x32, 0xfb, 0xb1, 0xfd, 0xa3, 0x02, 0xea,
+ 0x19, 0xd4, 0xde, 0xbd, 0x7d, 0x91, 0xf4, 0x11, 0x8a, 0xcf, 0x43, 0x5b,
+ 0x27, 0x96, 0x40, 0x0e, 0xa7, 0x98, 0x8b, 0x4e, 0x55, 0x9c, 0x03, 0xf5,
+ 0xc7, 0xb1, 0x2e, 0x5a, 0xca, 0xed, 0x47, 0x2a, 0x12, 0xbe, 0x90, 0x8f,
+ 0x52, 0x51, 0x16, 0x12, 0x7e, 0xa8, 0x59, 0x89, 0x5b, 0x30, 0x63, 0x89,
+ 0x28, 0x5b, 0x1d, 0x68, 0x83, 0xb3, 0x56, 0x98, 0x7e, 0xff, 0xfd, 0x87,
+ 0xd8, 0xc6, 0xd7, 0x16, 0x80, 0x67, 0xc9, 0x18, 0xbb, 0x67, 0x06, 0x25,
+ 0xa6, 0xb6, 0xc2, 0x01, 0x34, 0x7a, 0x95, 0xbf, 0x4d, 0x21, 0x08, 0xe1,
+ 0x3a, 0xab, 0xe4, 0x12, 0x98, 0x86, 0x1f, 0xd8, 0x95, 0x6e, 0xb9, 0xa8,
+ 0xcc, 0xcd, 0xa2, 0x36, 0x82, 0xe3, 0xf5, 0x39, 0x82, 0xec, 0xc5, 0x3b,
+ 0xb0, 0xb9, 0x26, 0xd2, 0x1f, 0x97, 0xcd, 0x21, 0xff, 0x57, 0xe1, 0x6f,
+ 0x67, 0x30, 0x50, 0x46, 0x4c, 0xdf, 0xcf, 0xbe, 0xea, 0xf7, 0x42, 0xe9,
+ 0x5a, 0xa1, 0x20, 0x75, 0xae, 0xa7, 0xc1, 0x45, 0xef, 0x4c, 0xab, 0xff,
+ 0x74, 0x3c, 0x11, 0xdd, 0x39, 0x68, 0x7e, 0x02, 0x53, 0xfb, 0x9c, 0xf7,
+ 0x7e, 0xd9, 0x80, 0x73, 0xae, 0xa5, 0x7a, 0x8f, 0x7c, 0xc9, 0xdc, 0xe1,
+ 0x76, 0xd1, 0xca, 0xc5, 0x73, 0x9c, 0xf0, 0x96, 0x31, 0x60, 0xca, 0x1b,
+ 0x4b, 0x7b, 0xbd, 0x41, 0xc2, 0xf4, 0x07, 0x28, 0xe2, 0x6c, 0xf6, 0xcb,
+ 0xf2, 0x2f, 0x03, 0x79, 0x61, 0xea, 0x39, 0x9a, 0x80, 0x88, 0x56, 0x26,
+ 0x9d, 0xab, 0x2b, 0x1d, 0x14, 0xcb, 0x9d, 0x54, 0xda, 0x6a, 0x60, 0x92,
+ 0x12, 0x82, 0x84, 0xd0, 0xd9, 0xd7, 0xa3, 0x78, 0x34, 0x7b, 0x12, 0x65,
+ 0x86, 0x9d, 0x8e, 0x4c, 0xde, 0x3c, 0xd9, 0x89, 0xcb, 0xe4, 0xb8, 0xd2,
+ 0xb7, 0x4a, 0x1e, 0x72, 0xfd, 0x03, 0x0d, 0x0e, 0xe4, 0xa0, 0x52, 0xd3,
+ 0x77, 0xea, 0x38, 0xb7, 0x30, 0x38, 0x2b, 0xe5, 0x92, 0xdd, 0x6f, 0xd0,
+ 0x8a, 0xe2, 0xc9, 0x2d, 0x5f, 0x77, 0x75, 0x3e, 0x8c, 0x9d, 0xcb, 0x3d,
+ 0xd3, 0xc2, 0xc6, 0x34, 0xa6, 0x5c, 0xc4, 0xb2, 0x38, 0x10, 0x9c, 0xff,
+ 0xf1, 0x8f, 0x05, 0x39, 0x23, 0xc4, 0x65, 0x12, 0xf2, 0x08, 0xfa, 0xaf,
+ 0xc5, 0xfa, 0x08, 0x91, 0xf0, 0xef, 0x92, 0xf3, 0xd7, 0x09, 0x04, 0xf2,
+ 0x6f, 0x54, 0x02, 0x0c, 0xe1, 0x0c, 0xad, 0x6c, 0xdf, 0xbc, 0x68, 0xd3,
+ 0xd4, 0xf3, 0xef, 0x71, 0xcf, 0x0e, 0x09, 0x74, 0x92, 0xa8, 0x3b, 0x39,
+ 0xb9, 0x8e, 0x05, 0x29, 0x87, 0xa3, 0x8f, 0x50, 0x28, 0xdc, 0x6e, 0x7f,
+ 0xdd, 0x76, 0xa0, 0x77, 0x3d, 0x82, 0x80, 0x5d, 0xa3, 0x5b, 0x6d, 0x0f,
+ 0x24, 0x6f, 0xb1, 0x1a, 0xea, 0xf5, 0x1c, 0x44, 0xb2, 0x47, 0xce, 0x97,
+ 0xae, 0xe4, 0x74, 0xfc, 0x4a, 0x46, 0x22, 0x9a, 0xb1, 0x27, 0xcf, 0xf8,
+ 0xc2, 0x87, 0xd3, 0x85, 0x77, 0xb2, 0xb3, 0x94, 0x27, 0x93, 0x22, 0x40,
+ 0x86, 0xe3, 0x90, 0x02, 0x4d, 0xb2, 0x76, 0x9e, 0x56, 0x53, 0x06, 0xa5,
+ 0xbf, 0x92, 0x6e, 0x57, 0xfb, 0x79, 0xed, 0x2b, 0xb2, 0x04, 0x0b, 0x89,
+ 0xd8, 0x11, 0x31, 0x1c, 0x2a, 0xe7, 0xdf, 0xa3, 0x2a, 0xb2, 0xda, 0xa1,
+ 0xd7, 0xc5, 0xe4, 0xb4, 0x50, 0x86, 0x3d, 0x49, 0x71, 0xc2, 0xae, 0xba,
+ 0x46, 0xd3, 0x17, 0x35, 0xe4, 0x5b, 0xc5, 0xb0, 0x23, 0x46, 0x24, 0x3a,
+ 0xb9, 0xc4, 0x62, 0x71, 0xbb, 0xce, 0x9c, 0xd4, 0xf3, 0x88, 0x01, 0x05,
+ 0x56, 0x60, 0x88, 0x01, 0x85, 0x15, 0xaa, 0xf3, 0x99, 0xa6, 0x1d, 0x97,
+ 0x48, 0xb2, 0x12, 0xfc, 0x6e, 0xd3, 0x63, 0x2b, 0x55, 0xea, 0x68, 0x56,
+ 0xa1, 0xbb, 0x94, 0x47, 0xef, 0x10, 0x02, 0xdc, 0xf1, 0x60, 0x92, 0x53,
+ 0xb4, 0x5c, 0x4d, 0xe3, 0x31, 0xa0, 0x4d, 0xa3, 0x34, 0xca, 0xe7, 0x66,
+ 0x87, 0xb3, 0x8a, 0x3e, 0xdb, 0xc7, 0xe7, 0x6b, 0x36, 0x8c, 0xd5, 0xa2,
+ 0x1e, 0x4c, 0xc5, 0x5d, 0x03, 0xb8, 0xcb, 0x66, 0x63, 0x19, 0xc0, 0x59,
+ 0x1b, 0x44, 0x94, 0x2a, 0x7e, 0xc0, 0x37, 0x44, 0x81, 0x41, 0x53, 0x66,
+ 0xc2, 0x80, 0x4a, 0x5b, 0xcd, 0xaf, 0xe8, 0xee, 0x58, 0xa5, 0x94, 0x33,
+ 0xda, 0xe8, 0x34, 0x0f, 0xee, 0x1a, 0xc3, 0xe5, 0x5e, 0xba, 0x98, 0x52,
+ 0xa1, 0x05, 0xa3, 0x4f, 0x30, 0x8d, 0xe3, 0x98, 0xaf, 0x22, 0xd3, 0x16,
+ 0x0a, 0x08, 0x49, 0x2c, 0x6a, 0xe4, 0x8f, 0x7f, 0x09, 0x67, 0x82, 0x08,
+ 0xfc, 0x3b, 0x5b, 0x92, 0x28, 0x8a, 0x09, 0xc7, 0x32, 0xa0, 0x94, 0x1b,
+ 0x51, 0xc5, 0x49, 0x43, 0xeb, 0xa8, 0xed, 0xed, 0xab, 0xae, 0xc5, 0xf7,
+ 0x77, 0x36, 0x48, 0x91, 0xaf, 0xd3, 0xfd, 0x16, 0xc8, 0x5d, 0xb5, 0x24,
+ 0xe9, 0xaf, 0x7e, 0xcf, 0xa1, 0x8e, 0x22, 0x01, 0xc5, 0xe1, 0x3a, 0xbe,
+ 0x91, 0x7f, 0xf6, 0x51, 0x88, 0x99, 0x7d, 0xf5, 0xfd, 0x0a, 0x1f, 0x05,
+ 0xa9, 0xa5, 0x5a, 0xd0, 0x33, 0x78, 0x12, 0x69, 0x0c, 0x07, 0xd3, 0x8c,
+ 0xb9, 0x09, 0xdb, 0x6f, 0xec, 0x8c, 0x38, 0x9f, 0xfb, 0x34, 0xd3, 0x5c,
+ 0x91, 0x4d, 0x06, 0x36, 0x21, 0x5e, 0x74, 0x5e, 0x2f, 0x03, 0xbf, 0x3a,
+ 0x1e, 0x90, 0x6b, 0x93, 0xac, 0x79, 0xe2, 0xad, 0x7d, 0xfc, 0xa5, 0x27,
+ 0x2d, 0x4d, 0xc1, 0xf1, 0x8d, 0x81, 0xfc, 0xec, 0x1a, 0xd8, 0x19, 0x3c,
+ 0x69, 0x58, 0xf9, 0x6c, 0x3b, 0x0a, 0x4b, 0xf8, 0x9c, 0xec, 0xec, 0xd4,
+ 0x35, 0x6c, 0x1a, 0x5b, 0x7e, 0x5a, 0x0d, 0x58, 0x61, 0x80, 0xd9, 0xbf,
+ 0x68, 0x40, 0x0b, 0x9b, 0xeb, 0x1b, 0x50, 0xe5, 0xf7, 0x63, 0x11, 0xb1,
+ 0x74, 0x12, 0x49, 0x9a, 0x47, 0xdb, 0x4d, 0x6c, 0xbf, 0xf4, 0x75, 0x82,
+ 0xc2, 0x68, 0x28, 0x08, 0x56, 0xe9, 0x28, 0x7f, 0x37, 0x90, 0x02, 0x81,
+ 0x11, 0xec, 0x98, 0x8c, 0x30, 0xc8, 0xe9, 0x11, 0x9a, 0xc2, 0x78, 0xfb,
+ 0x4f, 0x21, 0xe4, 0xeb, 0x67, 0x07, 0xae, 0xc3, 0x28, 0x89, 0x1c, 0x35,
+ 0xd7, 0xa8, 0xed, 0x3f, 0xb8, 0x2f, 0x94, 0xe7, 0x07, 0xa8, 0x26, 0x7f,
+ 0x47, 0x05, 0x26, 0xc7, 0xae, 0x3e, 0xb4, 0x4b, 0x1f, 0x58, 0xe0, 0xc2,
+ 0xd7, 0xc6, 0x70, 0x97, 0xde, 0xa2, 0xcd, 0xe1, 0xfc, 0x0c, 0x41, 0xb5,
+ 0xec, 0xb6, 0x96, 0x16, 0x31, 0x5b, 0x44, 0x9a, 0x96, 0x79, 0x63, 0xc0,
+ 0x6c, 0x07, 0x56, 0xef, 0x9a, 0x98, 0x0b, 0x84, 0xd5, 0x4d, 0x5f, 0x80,
+ 0xae, 0x97, 0x4d, 0x71, 0xb0, 0xb3, 0x2e, 0xdd, 0x30, 0xaa, 0xb6, 0x08,
+ 0xb5, 0x0c, 0x4e, 0xba, 0xd8, 0x6d, 0x6c, 0x7b, 0x09, 0xd0, 0x36, 0x3a,
+ 0x9e, 0xb5, 0x0d, 0x6c, 0xad, 0xc1, 0xc8, 0xe3, 0xd9, 0x83, 0x4d, 0xd9,
+ 0xde, 0x43, 0x7d, 0x10, 0x94, 0x58, 0x72, 0x23, 0xb1, 0xcc, 0xc3, 0x4d,
+ 0xf7, 0x2d, 0x29, 0xfb, 0x37, 0x7c, 0x79, 0x3e, 0xb8, 0xd3, 0x95, 0x70,
+ 0x04, 0xc6, 0x96, 0xcc, 0xf0, 0x38, 0xca, 0x15, 0x34, 0xf0, 0x05, 0x0f,
+ 0x96, 0x1c, 0x06, 0x4e, 0xab, 0x15, 0xb6, 0x8e, 0x63, 0xca, 0x46, 0xce,
+ 0x18, 0x76, 0x8f, 0xd0, 0x38, 0xfc, 0x65, 0x8b, 0x02, 0x83, 0xc8, 0xd2,
+ 0xa3, 0x27, 0x4a, 0xf6, 0xc5, 0xb3, 0x14, 0xb6, 0xe4, 0x43, 0xb9, 0x9f,
+ 0xc8, 0x6a, 0x29, 0x9b, 0xde, 0x9a, 0x3e, 0x94, 0xcb, 0x73, 0x71, 0xe3,
+ 0x21, 0xcd, 0x23, 0x55, 0x2a, 0xd2, 0x9d, 0x57, 0xdc, 0x83, 0x6e, 0xca,
+ 0x5a, 0x39, 0x92, 0x3d, 0x29, 0xd1, 0xf2, 0x69, 0x8c, 0x8d, 0xd4, 0x24,
+ 0x89, 0xe7, 0x4f, 0xba, 0xb9, 0x50, 0x0a, 0x76, 0x6a, 0xa3, 0x31, 0x9f,
+ 0xc8, 0x44, 0xf7, 0xf2, 0xdb, 0x8e, 0x22, 0xdf, 0x49, 0x16, 0x26, 0x99,
+ 0x52, 0xec, 0xca, 0x60, 0x02, 0x91, 0x59, 0x9d, 0x8d, 0x86, 0xfd, 0x7a,
+ 0xf9, 0x76, 0x0c, 0xeb, 0x12, 0x57, 0xf1, 0x03, 0xdd, 0x2c, 0x16, 0x1d,
+ 0xf7, 0xe8, 0x0b, 0x1a, 0x37, 0x37, 0xd1, 0x98, 0xdd, 0xe6, 0x1e, 0xfb,
+ 0xcc, 0xee, 0xdf, 0xd0, 0x9e, 0x0e, 0xf6, 0x99, 0x92, 0x31, 0x06, 0xd6,
+ 0x38, 0x68, 0x66, 0xc3, 0x9d, 0xc7, 0x06, 0x15, 0x68, 0x5e, 0xec, 0x82,
+ 0x5e, 0x4e, 0xa0, 0xb2, 0x59, 0xea, 0x93, 0xb5, 0x4e, 0x4e, 0x18, 0x13,
+ 0xc1, 0x0d, 0x7f, 0x4c, 0xaf, 0x1d, 0x31, 0x35, 0xac, 0xba, 0x99, 0x82,
+ 0x82, 0x1c, 0x1c, 0xf5, 0x21, 0xca, 0xde, 0x82, 0x00, 0x52, 0xde, 0xb4,
+ 0x59, 0xc2, 0xf0, 0xdb, 0x13, 0x37, 0xc5, 0x39, 0x95, 0x95, 0x86, 0x02,
+ 0x33, 0x00, 0x26, 0x40, 0xb0, 0x32, 0x0c, 0xd5, 0xd4, 0x9b, 0xc9, 0xe4,
+ 0xe4, 0x9b, 0x96, 0x94, 0x25, 0x9f, 0x54, 0xab, 0x2d, 0x0f, 0x19, 0x59,
+ 0x49, 0x53, 0x96, 0x3e, 0x20, 0xc5, 0x78, 0xde, 0x7e, 0x15, 0x8c, 0x53,
+ 0xa1, 0xf5, 0xd6, 0xe8, 0xa0, 0xaf, 0x11, 0x9d, 0xa3, 0x67, 0x5e, 0xcb,
+ 0x42, 0x39, 0x17, 0xf2, 0x8a, 0xfe, 0x91, 0x28, 0x03, 0x59, 0x96, 0x05,
+ 0xc6, 0x37, 0x19, 0x8d, 0x7b, 0xad, 0x16, 0x84, 0x39, 0xc8, 0xb5, 0xea,
+ 0x01, 0x5a, 0xd7, 0xce, 0x4c, 0x51, 0x8c, 0x7c, 0xb9, 0xc9, 0x1a, 0xcd,
+ 0x41, 0xe1, 0x13, 0x6c, 0x35, 0xc6, 0xb5, 0x3f, 0x3a, 0x4b, 0x60, 0x8b,
+ 0x06, 0x91, 0xca, 0x1c, 0x7c, 0xf4, 0x9e, 0x51, 0x04, 0x59, 0x42, 0x55,
+ 0x07, 0x45, 0x56, 0xca, 0x2f, 0xde, 0x39, 0x50, 0x17, 0x60, 0x38, 0x04,
+ 0xde, 0xb1, 0x96, 0xba, 0x2c, 0x1b, 0x7e, 0xd1, 0xbc, 0x58, 0x9d, 0x31,
+ 0xe0, 0x2d, 0xaf, 0xce, 0xbe, 0xcc, 0xd6, 0x3f, 0xe9, 0x11, 0xca, 0x7f,
+ 0x59, 0x9a, 0x05, 0xda, 0x46, 0xaf, 0xb0, 0xd3, 0x98, 0x69, 0xfe, 0x6e,
+ 0xa5, 0x10, 0xa7, 0x0d, 0x64, 0x34, 0xe2, 0xae, 0xeb, 0xef, 0xef, 0x72,
+ 0x04, 0xc3, 0x68, 0xa6, 0xe5, 0xde, 0xb3, 0x05, 0x52, 0x94, 0x5f, 0x26,
+ 0x5a, 0x5a, 0x5a, 0xbb, 0x1f, 0x2a, 0x97, 0x92, 0xc2, 0xbc, 0x20, 0x6d,
+ 0x3a, 0xae, 0xaf, 0x85, 0xd6, 0xec, 0x29, 0x5e, 0xfd, 0xe6, 0xd2, 0xec,
+ 0xb7, 0x92, 0xfe, 0x53, 0x3a, 0x11, 0x2f, 0x15, 0xf8, 0x90, 0x06, 0x9b,
+ 0xd1, 0xca, 0x86, 0xf3, 0x5b, 0x4e, 0x92, 0xa2, 0x7d, 0x71, 0x72, 0x7c,
+ 0xcc, 0x5e, 0xa2, 0x48, 0x3e, 0xab, 0xe1, 0x67, 0xa4, 0x3d, 0x0e, 0x15,
+ 0x67, 0x86, 0x07, 0xc6, 0xfa, 0x1d, 0x02, 0x24, 0x5e, 0x1f, 0x2e, 0x48,
+ 0x51, 0x9c, 0x20, 0x80, 0x3c, 0x9e, 0x41, 0xf7, 0xa3, 0x90, 0x5e, 0xd4,
+ 0xba, 0xd1, 0xd1, 0x33, 0x03, 0x6c, 0xc8, 0x7c, 0xca, 0x64, 0xe2, 0xe2,
+ 0x2f, 0x62, 0xec, 0xa3, 0x2e, 0x7d, 0xe5, 0x58, 0x4b, 0xb6, 0xb0, 0xa1,
+ 0xea, 0xc9, 0x9d, 0x9e, 0x2e, 0xbc, 0x87, 0xd1, 0x6f, 0x5d, 0xb0, 0x6f,
+ 0xf6, 0x27, 0x9b, 0x99, 0x55, 0xf8, 0x99, 0x3e, 0x49, 0x23, 0x06, 0xc8,
+ 0xbb, 0x28, 0xbf, 0x7b, 0xd1, 0x37, 0xe1, 0x2f, 0x40, 0x09, 0xe7, 0xc4,
+ 0x5b, 0x1c, 0x6e, 0x41, 0x8a, 0xe9, 0x67, 0x3e, 0x25, 0xfb, 0xf0, 0x1e,
+ 0xc2, 0x07, 0x69, 0x4b, 0x80, 0x8d, 0xc1, 0x4e, 0xb0, 0x2d, 0xc0, 0x3c,
+ 0x74, 0xa5, 0x6d, 0x9b, 0x41, 0xc6, 0x85, 0x19, 0x95, 0x91, 0xa3, 0xf3,
+ 0xe6, 0x2f, 0x7d, 0xce, 0x3e, 0x49, 0x91, 0xd6, 0x4d, 0x62, 0x31, 0xee,
+ 0x8d, 0x4e, 0x34, 0x25, 0x76, 0x64, 0x29, 0xbf, 0x1a, 0xa6, 0xb5, 0x4b,
+ 0x81, 0xb1, 0xea, 0x3a, 0xb0, 0x57, 0x43, 0xd5, 0xbb, 0x63, 0x84, 0xf9,
+ 0x0a, 0x97, 0xda, 0x69, 0xbb, 0x95, 0x23, 0x8d, 0x26, 0x4f, 0x48, 0xf4,
+ 0x62, 0x36, 0xab, 0x32, 0x3d, 0x92, 0x6f, 0xc6, 0xe0, 0xa7, 0x60, 0x17,
+ 0x79, 0xd2, 0xcd, 0x3b, 0xb9, 0xce, 0x43, 0x13, 0xf1, 0xd8, 0x07, 0x3a,
+ 0xa2, 0x91, 0x40, 0xe2, 0x9c, 0xeb, 0x91, 0x1a, 0xf0, 0x04, 0xae, 0xad,
+ 0x9c, 0x34, 0x49, 0xfa, 0x9d, 0x5f, 0xc9, 0xc3, 0x64, 0xc0, 0xd0, 0x6b,
+ 0x80, 0xa7, 0x37, 0x39, 0x80, 0xe9, 0x90, 0xa3, 0x19, 0x12, 0xa0, 0x12,
+ 0x61, 0x15, 0x2a, 0x94, 0xd6, 0x44, 0x2c, 0x97, 0x82, 0x7d, 0x55, 0x23,
+ 0x3a, 0x77, 0xb5, 0x37, 0xe4, 0xc9, 0x10, 0x7d, 0xda, 0xe8, 0x85, 0x04,
+ 0xe2, 0x7a, 0x1e, 0xfa, 0x9d, 0x18, 0x82, 0xb5, 0xa7, 0x4e, 0x66, 0xeb,
+ 0x27, 0xc5, 0xd6, 0xfe, 0xa6, 0xd8, 0xa4, 0x3e, 0x53, 0x29, 0x1d, 0xe5,
+ 0x67, 0x41, 0x76, 0x0a, 0x1a, 0x9d, 0x44, 0xac, 0x35, 0x25, 0xfe, 0xeb,
+ 0xb3, 0x42, 0xc0, 0x72, 0x01, 0x2e, 0xce, 0x03, 0xf5, 0x2c, 0x9d, 0x7c,
+ 0x26, 0x3e, 0x32, 0xa2, 0x1f, 0xad, 0xb3, 0xbe, 0x67, 0x92, 0xba, 0xf2,
+ 0xef, 0x9f, 0x3b, 0x29, 0x68, 0x93, 0x3b, 0x24, 0x77, 0x4c, 0x71, 0xe0,
+ 0xc0, 0x3d, 0x81, 0xf6, 0x9c, 0x3f, 0x41, 0x7f, 0x60, 0x89, 0x3d, 0x85,
+ 0x14, 0x32, 0x8c, 0x86, 0x9f, 0x67, 0x19, 0x00, 0x38, 0x85, 0x07, 0xf7,
+ 0x6f, 0x37, 0x1e, 0xf1, 0xa4, 0xb2, 0xe7, 0x54, 0x34, 0xf9, 0x33, 0xa0,
+ 0x18, 0xe1, 0x89, 0xe8, 0x67, 0x04, 0x02, 0x45, 0xf6, 0x1e, 0x82, 0xd3,
+ 0x81, 0xeb, 0xb7, 0xb4, 0xb9, 0x25, 0xd0, 0x09, 0x86, 0xf2, 0xf2, 0x06,
+ 0x66, 0x37, 0x78, 0x15, 0x28, 0x66, 0x52, 0xae, 0xa8, 0xaa, 0x52, 0xb3,
+ 0x12, 0xb7, 0xc2, 0x4e, 0x8b, 0x79, 0xe4, 0x36, 0xb4, 0x8b, 0x78, 0x48,
+ 0xa0, 0xc0, 0x0b, 0xba, 0xc0, 0x20, 0xc7, 0x37, 0x4a, 0x85, 0xd3, 0x41,
+ 0x25, 0x26, 0xb8, 0x0d, 0x05, 0xac, 0xa7, 0x4e, 0x67, 0xda, 0x3b, 0xac,
+ 0x04, 0x9e, 0x95, 0xf6, 0xba, 0xa4, 0xf4, 0x1a, 0x6e, 0x6f, 0x92, 0xfb,
+ 0x11, 0x98, 0xb0, 0x07, 0x6b, 0x1f, 0x2e, 0x59, 0x37, 0x6d, 0x27, 0xa8,
+ 0xde, 0x3e, 0x6a, 0xf1, 0xe0, 0x6a, 0x9a, 0x98, 0x98, 0x3b, 0xed, 0x08,
+ 0x95, 0x28, 0xbc, 0xd2, 0xf0, 0x59, 0x1b, 0xbf, 0xd6, 0x52, 0x8f, 0xdb,
+ 0xa5, 0x26, 0xf3, 0x06, 0xc9, 0xdc, 0xbc, 0x3c, 0x42, 0x70, 0x3d, 0xb8,
+ 0x1a, 0x64, 0xff, 0xff, 0x4b, 0xd0, 0x98, 0x7f, 0x5d, 0xbf, 0x2d, 0x0c,
+ 0x43, 0x38, 0xa7, 0xf6, 0x56, 0x10, 0x0f, 0xd4, 0x05, 0xba, 0xde, 0x83,
+ 0x92, 0xbc, 0x49, 0xc1, 0x6b, 0xad, 0xda, 0xaf, 0xd6, 0x53, 0x46, 0x61,
+ 0xec, 0x94, 0xfd, 0x46, 0xe5, 0x13, 0x6c, 0xd4, 0x91, 0xe0, 0x6b, 0x08,
+ 0xc6, 0x04, 0x8e, 0xdc, 0x0a, 0x93, 0x2c, 0xe2, 0xb6, 0xec, 0x7d, 0x8a,
+ 0x65, 0x9e, 0xfc, 0x0e, 0xe0, 0xae, 0x9f, 0xa3, 0x3d, 0x37, 0x30, 0x56,
+ 0xd1, 0x59, 0xed, 0x5b, 0x8a, 0x1f, 0xc6, 0x9c, 0x5f, 0xf4, 0xcb, 0x91,
+ 0x8a, 0x20, 0x92, 0x0c, 0x1c, 0x0f, 0x63, 0x0c, 0xa5, 0x9e, 0x23, 0xca,
+ 0xdb, 0xab, 0xd0, 0x1f, 0xd7, 0x69, 0x40, 0xb2, 0xfe, 0x30, 0x75, 0x89,
+ 0xa0, 0x7c, 0x21, 0xcb, 0x28, 0x06, 0x39, 0x33, 0xd3, 0xb0, 0xd7, 0x91,
+ 0x41, 0x36, 0x16, 0xa1, 0x46, 0xcc, 0x70, 0x42, 0x06, 0x4d, 0x8c, 0xb8,
+ 0x8c, 0xfc, 0x74, 0x10, 0xd6, 0x04, 0x3e, 0xa9, 0xfd, 0x3c, 0x61, 0x25,
+ 0xdd, 0xe9, 0xe6, 0x26, 0xe9, 0x33, 0x86, 0x33, 0x65, 0x05, 0x13, 0xc7,
+ 0x23, 0x50, 0xbe, 0x0c, 0x9f, 0xea, 0x7f, 0x9e, 0x3a, 0x10, 0x0b, 0x58,
+ 0x26, 0x1a, 0xdc, 0x8d, 0xed, 0x74, 0x0d, 0xc9, 0xe8, 0xb0, 0x4c, 0x70,
+ 0x59, 0x15, 0x44, 0x50, 0x6a, 0xef, 0x9c, 0xe6, 0x26, 0x6c, 0x0f, 0x5c,
+ 0xad, 0x20, 0x19, 0x99, 0x7c, 0x74, 0xc7, 0xc7, 0x99, 0x23, 0x48, 0xf2,
+ 0x2c, 0xb8, 0x4a, 0xf0, 0x4e, 0x92, 0xdd, 0xc9, 0x46, 0xb6, 0xb2, 0xa2,
+ 0x1e, 0x25, 0x35, 0x89, 0x2d, 0x42, 0x35, 0x1d, 0xc8, 0x8b, 0x10, 0x88,
+ 0x55, 0xee, 0x89, 0x83, 0x70, 0x62, 0x75, 0x7b, 0xd2, 0x28, 0x5b, 0xb9,
+ 0x25, 0xe0, 0xb6, 0x5a, 0xc7, 0x79, 0x4d, 0xed, 0x1d, 0x20, 0xc4, 0x72,
+ 0xe5, 0x75, 0xf8, 0x6f, 0xe8, 0x8d, 0xa6, 0x9e, 0xa2, 0x55, 0xfb, 0xd2,
+ 0xf3, 0x74, 0x70, 0x2c, 0x94, 0x89, 0x91, 0xb2, 0x78, 0xc1, 0x20, 0x17,
+ 0x73, 0xe5, 0xca, 0xe5, 0xf1, 0x7a, 0x11, 0x94, 0x0f, 0xce, 0xb9, 0x3a,
+ 0x8b, 0xf4, 0xe7, 0xd1, 0x62, 0x05, 0xb0, 0x13, 0xdb, 0xef, 0x66, 0x5f,
+ 0x47, 0xc7, 0x26, 0x3a, 0x96, 0xf4, 0x93, 0xf4, 0x14, 0xb9, 0x04, 0x70,
+ 0x2a, 0xbe, 0xb2, 0xf8, 0xfc, 0x62, 0x13, 0x08, 0x00, 0x2e, 0xf4, 0x12,
+ 0xdf, 0x2a, 0xd8, 0xc9, 0x08, 0x7b, 0x05, 0xc8, 0x3c, 0xee, 0x17, 0x76,
+ 0xcb, 0xb8, 0xb6, 0x84, 0x78, 0x27, 0x8a, 0xbc, 0x82, 0x0c, 0x89, 0x1c,
+ 0x89, 0xaa, 0xba, 0x90, 0x8c, 0x58, 0xe9, 0xa8, 0xcd, 0x72, 0x6e, 0xbb,
+ 0x45, 0xe8, 0x5a, 0x74, 0xcd, 0x15, 0x5b, 0x9b, 0xac, 0x4b, 0x69, 0xb8,
+ 0x60, 0x95, 0xf1, 0x49, 0xf7, 0x4e, 0x62, 0x91, 0x28, 0x6c, 0x30, 0x99,
+ 0x4d, 0x8c, 0x99, 0x48, 0xd8, 0x63, 0xe4, 0x56, 0x47, 0xae, 0x3d, 0xa3,
+ 0x1f, 0xb1, 0xb4, 0xca, 0x35, 0x1d, 0x7a, 0x9f, 0x4a, 0xde, 0xe8, 0x39,
+ 0xdd, 0x98, 0x18, 0xd1, 0x54, 0xa8, 0xab, 0xcb, 0xa5, 0x55, 0xfa, 0x35,
+ 0x8a, 0x8b, 0x87, 0xf3, 0xed, 0xbc, 0xfa, 0x9f, 0x82, 0xe5, 0x34, 0x04,
+ 0x1a, 0x20, 0x4a, 0xb7, 0x2e, 0xb0, 0x70, 0xdd, 0xd1, 0x5f, 0xc8, 0x82,
+ 0xa3, 0xdc, 0xf0, 0x55, 0x7c, 0xac, 0xa4, 0x99, 0xbb, 0xbd, 0x5b, 0x7d,
+ 0x55, 0x5b, 0xe8, 0xaf, 0x7a, 0x45, 0x1b, 0x41, 0xc9, 0x53, 0xdd, 0x38,
+ 0x46, 0xb1, 0x3b, 0xf5, 0xf9, 0x5c, 0xd6, 0x44, 0xbd, 0x57, 0x46, 0x42,
+ 0x5a, 0x39, 0xac, 0xe1, 0xa6, 0x18, 0x67, 0xf3, 0x49, 0xd9, 0x0d, 0x03,
+ 0xdb, 0x47, 0xda, 0x1d, 0xfe, 0x73, 0x95, 0x60, 0xb0, 0xe9, 0x87, 0x06,
+ 0x11, 0x28, 0x15, 0x35, 0x0a, 0xb0, 0xee, 0x43, 0x48, 0x62, 0x47, 0xb7,
+ 0x28, 0xb5, 0x7d, 0x0e, 0xd8, 0x7c, 0xd6, 0x4e, 0x7b, 0x9d, 0x4e, 0x1f,
+ 0x1b, 0x1b, 0xc8, 0x7c, 0x82, 0x3d, 0x6a, 0x56, 0x19, 0x49, 0x46, 0xdd,
+ 0x17, 0x3f, 0x5e, 0x94, 0x42, 0x40, 0x9d, 0x4e, 0x07, 0x84, 0xcf, 0x32,
+ 0x70, 0xf5, 0xfb, 0xef, 0xed, 0xcf, 0x06, 0xc9, 0x72, 0x08, 0xc7, 0x58,
+ 0xb3, 0x85, 0xfc, 0x0f, 0xeb, 0x1e, 0x47, 0x36, 0xbf, 0xbe, 0x10, 0x22,
+ 0x88, 0xa6, 0x52, 0x3f, 0x19, 0x32, 0x91, 0x56, 0x9f, 0x64, 0x46, 0x16,
+ 0x4c, 0x2d, 0x59, 0xe0, 0x7c, 0x01, 0xa4, 0x6b, 0x5b, 0x7c, 0xcb, 0x21,
+ 0xef, 0xbc, 0xe2, 0xdc, 0x03, 0x67, 0x83, 0x8a, 0xa6, 0x5c, 0x4c, 0xb7,
+ 0x66, 0x99, 0x4f, 0xde, 0xbf, 0x0d, 0x30, 0x93, 0x3c, 0x09, 0x62, 0x19,
+ 0xf7, 0xc2, 0xf2, 0x34, 0xa6, 0x61, 0xd2, 0x2b, 0x71, 0x02, 0x41, 0x7c,
+ 0xe4, 0xe7, 0xdb, 0x34, 0xc9, 0x43, 0xf6, 0x96, 0x97, 0xc2, 0x30, 0x1d,
+ 0x4a, 0x51, 0x06, 0xe4, 0xc8, 0xa8, 0x2e, 0x8d, 0x26, 0x9e, 0xdf, 0x88,
+ 0x16, 0x9a, 0x9d, 0x50, 0xb3, 0xc4, 0x81, 0x78, 0xcd, 0x90, 0x2f, 0x47,
+ 0x2f, 0x0f, 0x4b, 0x0c, 0x20, 0x9a, 0x6d, 0x53, 0x45, 0x37, 0xaf, 0x6a,
+ 0x1d, 0x00, 0x86, 0x7d, 0x0f, 0xa0, 0xe5, 0xc7, 0x69, 0xe3, 0x46, 0xaf,
+ 0x39, 0x13, 0x74, 0x66, 0x29, 0x65, 0x97, 0xbb, 0x9d, 0x1f, 0xd5, 0x18,
+ 0xfc, 0x40, 0xe5, 0x45, 0x49, 0xe8, 0xe6, 0x15, 0x88, 0x66, 0x6c, 0x4e,
+ 0xda, 0xda, 0x63, 0x38, 0x4d, 0xc1, 0x7d, 0x7b, 0x59, 0xf9, 0xc7, 0xca,
+ 0x12, 0x64, 0x8e, 0x7d, 0x04, 0x56, 0x5c, 0x2a, 0x08, 0x47, 0xd1, 0x98,
+ 0x10, 0x55, 0x43, 0xf5, 0x4c, 0xd3, 0xad, 0x74, 0xa0, 0x58, 0x9e, 0xa2,
+ 0x68, 0x4e, 0xa8, 0x8b, 0x39, 0xd1, 0xa1, 0x97, 0x32, 0x88, 0x41, 0x09,
+ 0x35, 0xd8, 0x32, 0xfe, 0xb5, 0xea, 0x8f, 0xb7, 0x8e, 0xec, 0xb1, 0x7c,
+ 0xc0, 0x69, 0x9c, 0x86, 0x57, 0xb3, 0xb7, 0x99, 0xd6, 0xb2, 0xc1, 0x60,
+ 0x47, 0x2a, 0xf3, 0x4e, 0x3b, 0x16, 0x13, 0x6b, 0xca, 0x6c, 0x31, 0x75,
+ 0xd7, 0xaf, 0xa4, 0xc0, 0xb5, 0x45, 0x72, 0xff, 0x18, 0xa3, 0x8c, 0x32,
+ 0x82, 0xd8, 0x48, 0x5a, 0x5b, 0x1f, 0xdf, 0x1f, 0x02, 0xd3, 0x75, 0xeb,
+ 0x40, 0x37, 0x71, 0x86, 0x9a, 0xd2, 0x13, 0x45, 0x60, 0xea, 0x06, 0x04,
+ 0x3c, 0x6e, 0x87, 0x40, 0x5c, 0xcd, 0x78, 0xf3, 0x39, 0x22, 0xe5, 0x46,
+ 0xc0, 0x43, 0xb6, 0x04, 0xb1, 0xd2, 0xc0, 0x5d, 0x0b, 0x46, 0x19, 0x61,
+ 0xf1, 0x38, 0x24, 0xa4, 0xa7, 0xa6, 0x28, 0xf0, 0xfa, 0x87, 0x54, 0xee,
+ 0xbe, 0xa7, 0xa4, 0x33, 0xcc, 0x05, 0x9e, 0x8b, 0x7b, 0xcd, 0xa3, 0xf5,
+ 0xe2, 0xe1, 0x2c, 0x0b, 0x2a, 0x3c, 0xa6, 0x69, 0x9a, 0x5b, 0xfd, 0x7b,
+ 0x72, 0x54, 0x8f, 0x65, 0xa2, 0x7c, 0x7e, 0xc7, 0x14, 0x43, 0x4c, 0xfe,
+ 0xed, 0x3b, 0xb1, 0x4a, 0x7b, 0x58, 0xe4, 0xae, 0x8b, 0x9b, 0x76, 0x3a,
+ 0x8a, 0xfc, 0x4b, 0x08, 0xd5, 0x5a, 0x09, 0xcd, 0xfe, 0xf0, 0x72, 0xd8,
+ 0xbd, 0x4d, 0x37, 0xb7, 0x10, 0x12, 0x92, 0xa2, 0x98, 0x84, 0x97, 0x5a,
+ 0x02, 0x95, 0xbd, 0x7b, 0x65, 0x80, 0x84, 0xf5, 0x26, 0x2d, 0x3f, 0xa6,
+ 0x28, 0xb3, 0x74, 0xcf, 0x55, 0x4b, 0xf0, 0x89, 0xec, 0x8b, 0x0a, 0xc8,
+ 0x28, 0x51, 0x53, 0x26, 0xb9, 0xf7, 0xac, 0x3e, 0xc3, 0x47, 0x51, 0x73,
+ 0x1f, 0x02, 0x68, 0x71, 0x5d, 0x52, 0x2e, 0xc2, 0x0a, 0xfd, 0xe5, 0xe4,
+ 0xaa, 0x8c, 0x68, 0xb6, 0x22, 0xdc, 0xb9, 0x05, 0x27, 0xd5, 0xd2, 0x8f,
+ 0x49, 0xb2, 0xcd, 0xac, 0x99, 0x72, 0xa8, 0x28, 0xf0, 0x9b, 0xc4, 0x9d,
+ 0xc7, 0x1b, 0xbb, 0x5a, 0x64, 0x49, 0xbd, 0x20, 0x7b, 0xab, 0x6d, 0xe5,
+ 0xa4, 0x4d, 0xbc, 0x2f, 0x62, 0xba, 0xf3, 0xce, 0x08, 0xff, 0x6e, 0x30,
+ 0x92, 0xd5, 0x2e, 0x85, 0x43, 0x7a, 0x36, 0xb7, 0x21, 0xf5, 0x55, 0x5f,
+ 0xce, 0x5c, 0x2d, 0x8d, 0x9a, 0x1a, 0x3e, 0xf5, 0xaf, 0x95, 0x46, 0xe5,
+ 0x7c, 0xc9, 0x83, 0x2b, 0xf9, 0x04, 0x3c, 0x32, 0xcf, 0x72, 0xa8, 0x57,
+ 0x08, 0x48, 0xf6, 0xda, 0x23, 0x65, 0xcb, 0xe7, 0xf4, 0xa7, 0x25, 0x68,
+ 0xf5, 0x66, 0x60, 0x5f, 0x32, 0x09, 0xbd, 0x75, 0x05, 0xa3, 0x63, 0x79,
+ 0x8f, 0x0e, 0x55, 0x9f, 0x48, 0xd8, 0xbc, 0xe7, 0x6c, 0x47, 0xe2, 0x48,
+ 0x6f, 0xde, 0x00, 0xd5, 0xe1, 0x41, 0x28, 0x0b, 0xc7, 0xbf, 0x54, 0x7e,
+ 0x61, 0x65, 0xaf, 0x31, 0x12, 0xc3, 0x44, 0x00, 0x00, 0x00, 0x11, 0x3a,
+ 0x72, 0x71, 0x9c, 0xc0, 0xcf, 0xf1, 0x49, 0x53, 0xba, 0xbe, 0xe4, 0x01,
+ 0xbf, 0x08, 0xc4, 0x2e, 0x5b, 0xf8, 0xa8, 0xd6, 0xc2, 0x70, 0x62, 0x50,
+ 0xa7, 0xf1, 0xa3, 0xb6, 0x68, 0x95, 0x1e, 0x0a, 0x3c, 0xee, 0x47, 0x75,
+ 0xb0, 0x75, 0xd3, 0xaf, 0x4e, 0x40, 0xe0, 0x8b, 0x8b, 0xc9, 0xfc, 0x13,
+ 0xe3, 0xce, 0xe3, 0xbc, 0x27, 0xb7, 0x6c, 0x24, 0x97, 0x71, 0xc2, 0x80,
+ 0x7d, 0x87, 0x45, 0x5c, 0x85, 0x58, 0x16, 0xfd, 0x4f, 0x98, 0x14, 0x7f,
+ 0x57, 0x87, 0xf3, 0x72, 0xba, 0x22, 0x16, 0x13, 0xbb, 0x17, 0xa3, 0x16,
+ 0x5d, 0xeb, 0xc6, 0xdb, 0x2b, 0x39, 0x49, 0xbc, 0x19, 0x96, 0x6f, 0x86,
+ 0x4b, 0x5a, 0xc9, 0x76, 0xbf, 0xe6, 0xda, 0x55, 0x7d, 0xc2, 0x2a, 0x0b,
+ 0xa9, 0x4a, 0x77, 0x80, 0x72, 0x76, 0x51, 0xe7, 0x2b, 0x3d, 0x62, 0xab,
+ 0xd9, 0xe6, 0x4d, 0x3d, 0x1f, 0x6b, 0x9d, 0x36, 0xff, 0x24, 0xab, 0x5a,
+ 0x40, 0x01, 0x4f, 0x95, 0x40, 0x89, 0x8d, 0x8f, 0xad, 0xcf, 0x29, 0x73,
+ 0x08, 0x97, 0x5d, 0xff, 0x7d, 0x9c, 0x5c, 0xc6, 0x5b, 0x40, 0xc4, 0x20,
+ 0x70, 0x6f, 0x5c, 0x19, 0xe7, 0x64, 0x13, 0x40, 0xc9, 0xa9, 0x93, 0xd2,
+ 0x5a, 0xbe, 0x36, 0x0e, 0x0e, 0x96, 0x8c, 0x23, 0xa6, 0x1a, 0xb1, 0x0f,
+ 0x74, 0x78, 0xb4, 0x23, 0x6b, 0x27, 0x40, 0xfa, 0xff, 0x2e, 0x79, 0xaa,
+ 0x46, 0x9a, 0x4a, 0x76, 0x10, 0xfd, 0xe0, 0xc8, 0xbc, 0x13, 0x7e, 0xff,
+ 0x5a, 0x6d, 0x86, 0x3c, 0x7f, 0x51, 0xa2, 0x6a, 0x19, 0xa5, 0xda, 0x45,
+ 0xce, 0x1d, 0xd3, 0xbf, 0x8e, 0xaa, 0x74, 0x3b, 0x3d, 0x35, 0x88, 0xf7,
+ 0x3f, 0x45, 0x40, 0x17, 0xa4, 0x04, 0x28, 0xd2, 0x63, 0x2e, 0xdd, 0x93,
+ 0x4c, 0x4e, 0xac, 0x90, 0x35, 0x03, 0x98, 0x8f, 0x13, 0x87, 0xac, 0xc0,
+ 0x30, 0x18, 0x16, 0x09, 0x90, 0x01, 0x2b, 0x2e, 0x8c, 0x82, 0x03, 0xf6,
+ 0x54, 0xda, 0x95, 0x72, 0x46, 0xf6, 0xb3, 0x3f, 0x40, 0x0a, 0xc5, 0x91,
+ 0x92, 0x65, 0x3b, 0xc8, 0xa7, 0x52, 0xa0, 0x41, 0x5e, 0x17, 0xd3, 0x7b,
+ 0x8a, 0xf2, 0x0d, 0xb7, 0xac, 0x1e, 0x49, 0xfd, 0xd0, 0x62, 0x79, 0x6c,
+ 0xb5, 0x8d, 0xce, 0xd0, 0x52, 0x9b, 0x2b, 0x44, 0x14, 0x9e, 0x98, 0x12,
+ 0x34, 0xfa, 0xca, 0x8a, 0x7c, 0x83, 0x49, 0x0a, 0x0e, 0xd5, 0xcc, 0x0d,
+ 0xba, 0xa0, 0x28, 0x00, 0xb5, 0x76, 0xb3, 0x5c, 0x81, 0x42, 0xe7, 0xd8,
+ 0x29, 0xea, 0xca, 0x9f, 0x4a, 0x21, 0xc1, 0x54, 0x03, 0x30, 0xb0, 0x4b,
+ 0x7e, 0xc5, 0x6a, 0xa9, 0xa7, 0xdc, 0xa4, 0xa8, 0x7d, 0x43, 0x52, 0xf5,
+ 0xe1, 0x19, 0x1d, 0xa4, 0xee, 0xf9, 0x5f, 0x86, 0xab, 0x25, 0x7c, 0x62,
+ 0x65, 0xb5, 0x20, 0x6e, 0x68, 0x41, 0x67, 0xde, 0x50, 0x02, 0x11, 0x4d,
+ 0x79, 0x32, 0x8f, 0x2a, 0x9f, 0x2e, 0x90, 0x71, 0x84, 0xd0, 0x71, 0xbb,
+ 0xa6, 0xc0, 0xf0, 0x99, 0xf5, 0x8a, 0x02, 0x3d, 0xb9, 0x42, 0x8b, 0x1a,
+ 0x06, 0xb9, 0x78, 0x87, 0xf7, 0x78, 0xbe, 0xdf, 0x28, 0x7b, 0xd5, 0xc4,
+ 0x12, 0x26, 0xb2, 0x15, 0xbe, 0xac, 0x28, 0x58, 0x8c, 0xe9, 0x88, 0xab,
+ 0x0e, 0x8d, 0x54, 0x96, 0x8b, 0x87, 0xa8, 0xfd, 0x84, 0x03, 0xf9, 0xd4,
+ 0xea, 0xb8, 0x89, 0xe3, 0xaa, 0xa3, 0x27, 0x18, 0x3f, 0x9c, 0x28, 0x9e,
+ 0xa3, 0x96, 0x7c, 0xb0, 0x40, 0x34, 0xc5, 0x33, 0x05, 0x2d, 0xb7, 0xae,
+ 0x8f, 0xfe, 0xcd, 0x2c, 0x71, 0x20, 0xa7, 0x81, 0xfe, 0xdb, 0xd3, 0xd8,
+ 0x4f, 0xf0, 0xa3, 0x02, 0x72, 0x18, 0x69, 0x2d, 0x03, 0x51, 0x41, 0xc6,
+ 0xe7, 0x8c, 0x8d, 0x55, 0xee, 0x6e, 0xc5, 0x62, 0xa5, 0x87, 0xcd, 0xb7,
+ 0xc4, 0x4b, 0xc4, 0xbc, 0x19, 0x07, 0xad, 0xb8, 0x29, 0xec, 0x4f, 0xba,
+ 0x09, 0x23, 0x13, 0x0b, 0xc7, 0xd2, 0x94, 0x9e, 0xa3, 0x32, 0xf0, 0x49,
+ 0xfc, 0x93, 0x50, 0xf3, 0xd5, 0x8f, 0x0e, 0x21, 0x9b, 0x48, 0x80, 0x4d,
+ 0x39, 0xd9, 0xc5, 0x92, 0xa8, 0xc6, 0xc2, 0xc6, 0x4e, 0x5d, 0xc6, 0xe7,
+ 0xa3, 0x43, 0x00, 0xf2, 0x58, 0xa6, 0xb5, 0x60, 0x01, 0xe6, 0x99, 0x38,
+ 0xa5, 0xb7, 0xd6, 0xa7, 0x84, 0xb3, 0x1d, 0xc3, 0x8f, 0xba, 0xf9, 0x45,
+ 0xf1, 0x87, 0xf9, 0xc4, 0xd8, 0x40, 0x58, 0x42, 0xe2, 0xce, 0x58, 0xc5,
+ 0x4b, 0xcf, 0xc0, 0x0d, 0x48, 0x12, 0xb9, 0x56, 0xb4, 0x54, 0x08, 0x74,
+ 0xe4, 0x80, 0x0b, 0xd9, 0xc0, 0xe1, 0x2f, 0xbf, 0x09, 0x1a, 0x80, 0xc9,
+ 0xbd, 0xc9, 0x37, 0x2c, 0x8e, 0x3e, 0xc4, 0x1c, 0x76, 0x59, 0x99, 0x8a,
+ 0x30, 0x84, 0x37, 0x24, 0xde, 0x14, 0x0d, 0x35, 0x9d, 0x5b, 0x76, 0x56,
+ 0x14, 0x05, 0xba, 0x53, 0x77, 0xd5, 0x69, 0x39, 0x3e, 0x4e, 0x71, 0x83,
+ 0xb2, 0x27, 0xdb, 0x7b, 0x7b, 0xc6, 0xaa, 0x1e, 0xa7, 0xf4, 0xaa, 0x4a,
+ 0x24, 0x46, 0x82, 0xf3, 0x5f, 0x2a, 0xf8, 0xde, 0x32, 0xb0, 0x0c, 0x66,
+ 0x7b, 0xa7, 0xdf, 0x84, 0x6d, 0xff, 0x0c, 0x4f, 0x2a, 0x8a, 0x65, 0xe8,
+ 0x83, 0xae, 0x6a, 0x82, 0xdb, 0x22, 0x03, 0xde, 0xdc, 0x3f, 0x46, 0xd3,
+ 0xee, 0x8d, 0xe9, 0x0f, 0x1f, 0xe3, 0xd1, 0x97, 0xa2, 0x75, 0x95, 0x14,
+ 0x6f, 0x0d, 0x60, 0x9c, 0x10, 0x3f, 0x71, 0x8c, 0xed, 0x13, 0x38, 0x89,
+ 0xa6, 0xfc, 0x0b, 0x9f, 0x51, 0xcd, 0x94, 0xa5, 0xe8, 0xa9, 0x09, 0x3a,
+ 0x2b, 0x25, 0x80, 0x06, 0xe8, 0xaf, 0x91, 0x06, 0xc7, 0xfb, 0x23, 0x49,
+ 0x04, 0x7d, 0xb5, 0x0e, 0xdf, 0x93, 0xe1, 0x4c, 0x37, 0x81, 0x88, 0x0d,
+ 0x5f, 0x57, 0x8d, 0xc7, 0x76, 0x5e, 0x65, 0x5d, 0xe6, 0xa4, 0x89, 0x97,
+ 0x74, 0x8d, 0x5a, 0x22, 0x35, 0x85, 0xc9, 0x50, 0x67, 0xf5, 0x58, 0xd7,
+ 0x19, 0x66, 0x00, 0x2f, 0x84, 0x8e, 0xb2, 0x50, 0xee, 0x61, 0x8a, 0x77,
+ 0x1a, 0x65, 0x86, 0x4e, 0x68, 0xa4, 0x95, 0xa4, 0xb7, 0xc1, 0x1c, 0xc9,
+ 0x1a, 0x60, 0xf5, 0x90, 0x40, 0x20, 0x03, 0xe4, 0x06, 0x50, 0x0b, 0x98,
+ 0x6f, 0x5f, 0x97, 0xe4, 0xc3, 0x52, 0x71, 0xfc, 0x3e, 0xf4, 0x57, 0x6a,
+ 0xe4, 0x5b, 0xda, 0x24, 0xa0, 0x1a, 0x9c, 0xb5, 0x9d, 0x85, 0x9b, 0xf9,
+ 0x4f, 0x31, 0xd9, 0x16, 0x05, 0x89, 0xd2, 0x7a, 0xe5, 0xb2, 0xbe, 0x56,
+ 0xe8, 0x72, 0xb1, 0x6c, 0xe0, 0x1c, 0x5d, 0xcf, 0xc4, 0x61, 0xf7, 0xf7,
+ 0x9e, 0x80, 0xd5, 0x71, 0xe9, 0xb5, 0x19, 0x43, 0x53, 0xb9, 0x65, 0x91,
+ 0x4c, 0x7f, 0x26, 0x30, 0x96, 0x33, 0x74, 0xdf, 0xca, 0xdf, 0x24, 0x1c,
+ 0xc0, 0x63, 0xda, 0x37, 0x49, 0x68, 0x71, 0x68, 0x52, 0x66, 0x80, 0x5d,
+ 0x24, 0x35, 0x24, 0x8e, 0x21, 0x43, 0xa4, 0x99, 0x49, 0xf1, 0xec, 0xf2,
+ 0x18, 0xac, 0x39, 0x22, 0x3f, 0xb1, 0x07, 0x46, 0xbe, 0x71, 0x51, 0x2a,
+ 0x24, 0x8e, 0x0a, 0xa0, 0x72, 0xb0, 0x6a, 0xd0, 0x60, 0x1f, 0xb2, 0x70,
+ 0xa1, 0xdb, 0x3b, 0xf9, 0x22, 0x8a, 0xbc, 0xec, 0x6c, 0xb2, 0x67, 0x9f,
+ 0x44, 0xbc, 0x53, 0x2d, 0x84, 0x49, 0x52, 0x80, 0x20, 0x3c, 0xa0, 0x97,
+ 0x64, 0x50, 0x61, 0x57, 0x23, 0x5b, 0xab, 0xbc, 0xc7, 0x69, 0x27, 0xe9,
+ 0x89, 0x55, 0x81, 0x93, 0xd0, 0xda, 0x3d, 0x23, 0xfa, 0xa5, 0x68, 0xdf,
+ 0x8e, 0xf2, 0x50, 0xd6, 0x34, 0x8f, 0x47, 0xd9, 0x52, 0xed, 0xda, 0xb3,
+ 0x70, 0xea, 0xd0, 0xe4, 0xb6, 0x83, 0x8a, 0x36, 0xc2, 0xf8, 0x33, 0x2f,
+ 0x18, 0xb5, 0x03, 0x9e, 0xfc, 0x5b, 0xb5, 0x23, 0xcb, 0xe3, 0xf0, 0x4f,
+ 0xab, 0x15, 0x37, 0x13, 0xd4, 0x99, 0xcf, 0x34, 0xb4, 0xac, 0xc2, 0xb1,
+ 0x74, 0xde, 0xf3, 0xd9, 0xd2, 0x21, 0x27, 0x92, 0x22, 0x3a, 0xa9, 0x1a,
+ 0x12, 0x0e, 0x33, 0x68, 0x6b, 0x40, 0x04, 0x86, 0x58, 0x6d, 0x2d, 0xb6,
+ 0x73, 0xc1, 0x67, 0xe2, 0x8f, 0x90, 0x3f, 0x03, 0x71, 0xaa, 0xb8, 0x5a,
+ 0x31, 0x32, 0x96, 0x7e, 0xb2, 0x52, 0xea, 0x94, 0xcc, 0xfd, 0x0f, 0xe9,
+ 0xbf, 0x8d, 0xaf, 0x09, 0x94, 0xe3, 0x03, 0xaf, 0xf7, 0x96, 0xb3, 0xda,
+ 0x08, 0x20, 0x06, 0x74, 0x28, 0x48, 0x60, 0x76, 0x19, 0x87, 0x1c, 0xb7,
+ 0xd2, 0x16, 0xd4, 0x12, 0xcc, 0x93, 0x45, 0x03, 0x74, 0x1a, 0x53, 0xf9,
+ 0xa8, 0xf3, 0x73, 0x3e, 0xc2, 0x51, 0x3e, 0x15, 0x39, 0x1e, 0x6e, 0xf5,
+ 0x15, 0x47, 0x59, 0x9e, 0x84, 0xcf, 0xc1, 0xda, 0xb3, 0xfd, 0xec, 0x81,
+ 0x5a, 0x4e, 0xa6, 0x82, 0x85, 0x19, 0x87, 0xdd, 0x18, 0xb9, 0xf2, 0x88,
+ 0x7d, 0x59, 0x85, 0x23, 0xac, 0x7d, 0x4a, 0x67, 0x1b, 0x1e, 0x74, 0xb0,
+ 0x3d, 0x31, 0xc0, 0x64, 0x7f, 0x2f, 0xfe, 0xca, 0xa9, 0x87, 0xc9, 0x1b,
+ 0x25, 0xe7, 0xcf, 0xa6, 0xcb, 0xba, 0x55, 0x44, 0x86, 0xd0, 0x97, 0x63,
+ 0x3c, 0x4c, 0xdf, 0x6d, 0x20, 0x6f, 0x3e, 0x6c, 0x95, 0xc9, 0x08, 0xe6,
+ 0x2d, 0xe2, 0x40, 0x62, 0xd8, 0xe4, 0xcc, 0xb8, 0xa9, 0xf5, 0x09, 0xc2,
+ 0x4b, 0x8a, 0xc7, 0x82, 0xbc, 0x3b, 0x40, 0x8d, 0x80, 0x23, 0xfe, 0x37,
+ 0x54, 0x6d, 0xc6, 0x5c, 0x30, 0xb5, 0x4a, 0x0e, 0xe0, 0xf6, 0x6e, 0x53,
+ 0x4d, 0x0f, 0x33, 0x87, 0x44, 0x97, 0xf7, 0xbf, 0x56, 0x08, 0xbe, 0xdb,
+ 0x39, 0xd4, 0xf5, 0x50, 0x63, 0x42, 0xfd, 0xd0, 0xd3, 0xe7, 0xa4, 0xdd,
+ 0x30, 0xc8, 0x34, 0x21, 0x3a, 0xc3, 0xc6, 0x4b, 0xab, 0x89, 0x78, 0x79,
+ 0x4b, 0xb1, 0xc2, 0xf2, 0x33, 0x08, 0xd9, 0xf0, 0x25, 0x15, 0x10, 0xd4,
+ 0xc6, 0x69, 0xb2, 0x55, 0x5f, 0x7a, 0x7c, 0x36, 0xd3, 0x95, 0x80, 0x03,
+ 0x88, 0xa6, 0xb3, 0xf4, 0x57, 0x91, 0x07, 0x3f, 0x61, 0x02, 0x38, 0xe9,
+ 0x0c, 0x71, 0xc2, 0x84, 0x66, 0xd9, 0x49, 0xf3, 0x70, 0xd1, 0xaf, 0x6a,
+ 0xdb, 0x73, 0xa4, 0x2b, 0xb0, 0xb9, 0x77, 0xe0, 0xe8, 0x9b, 0xb6, 0xe0,
+ 0xb4, 0x38, 0x2d, 0x0a, 0x26, 0xad, 0xb6, 0x0b, 0x3e, 0x9c, 0x24, 0x57,
+ 0x60, 0x00, 0x63, 0x98, 0xcc, 0xe8, 0x61, 0x11, 0xcc, 0x0c, 0xfb, 0xd1,
+ 0x19, 0xb3, 0x39, 0x63, 0x7a, 0xfe, 0x93, 0x5c, 0x6b, 0x57, 0x1c, 0xc9,
+ 0xe2, 0xa7, 0xf0, 0x3e, 0xa7, 0x99, 0xb4, 0xa3, 0x32, 0xa7, 0x7f, 0x9d,
+ 0x75, 0x03, 0x03, 0x01, 0xd6, 0x7e, 0x09, 0x32, 0xdf, 0x74, 0xf1, 0xaa,
+ 0x53, 0xbc, 0xeb, 0xb1, 0xb9, 0xeb, 0xa7, 0xe0, 0x47, 0x15, 0xe9, 0x64,
+ 0xe1, 0x63, 0x3e, 0xc7, 0x0e, 0x12, 0x16, 0xa0, 0x2a, 0xa3, 0xd6, 0x35,
+ 0xad, 0x01, 0x6a, 0xdd, 0x74, 0xa4, 0xbc, 0xe6, 0xfd, 0x00, 0xa8, 0x6b,
+ 0xd9, 0x13, 0x7f, 0x8b, 0xbb, 0x7b, 0xb6, 0x1e, 0x20, 0xf0, 0x8c, 0x5f,
+ 0x8b, 0x5e, 0x41, 0xf9, 0x11, 0xb5, 0x4c, 0xae, 0xda, 0xa5, 0x1e, 0x12,
+ 0xe5, 0x96, 0xd6, 0x9a, 0xc6, 0xed, 0xaa, 0x12, 0x12, 0x4c, 0x84, 0x88,
+ 0x24, 0xcb, 0x49, 0xa5, 0x0d, 0x31, 0x64, 0xf0, 0xf6, 0x99, 0xea, 0xb9,
+ 0xbe, 0xcf, 0xdc, 0x88, 0x76, 0xc4, 0xce, 0x2b, 0x92, 0x42, 0xdd, 0xee,
+ 0x91, 0xf4, 0x4d, 0xe4, 0x15, 0xb5, 0x79, 0x60, 0xe7, 0x53, 0xe8, 0x2b,
+ 0x7f, 0xd9, 0xbb, 0xbb, 0xca, 0x06, 0xf5, 0x3c, 0x63, 0xf7, 0x83, 0xe3,
+ 0x03, 0xba, 0x55, 0xdf, 0x3b, 0x11, 0x3e, 0x53, 0x18, 0xc7, 0xc9, 0x34,
+ 0x12, 0x10, 0xbc, 0x9f, 0x78, 0x59, 0x27, 0x6b, 0x05, 0xec, 0x8d, 0xb3,
+ 0x70, 0x16, 0x64, 0x92, 0x90, 0x98, 0x8f, 0x0b, 0x2c, 0x2b, 0x2d, 0x23,
+ 0x4f, 0xd6, 0x21, 0x3f, 0xc5, 0xa0, 0x71, 0xb0, 0x8f, 0x0e, 0x39, 0x3a,
+ 0x0c, 0xc3, 0x13, 0x03, 0x61, 0xab, 0xb3, 0x97, 0x20, 0x60, 0x8a, 0x9f,
+ 0x54, 0xd1, 0x3b, 0x44, 0x7d, 0xf9, 0xa4, 0x88, 0x41, 0xe4, 0x03, 0x64,
+ 0xb2, 0xf1, 0x82, 0x17, 0xde, 0x5f, 0x34, 0xee, 0x20, 0x42, 0xf5, 0x2d,
+ 0x01, 0x02, 0x28, 0x41, 0xbe, 0x5b, 0x12, 0x1f, 0xff, 0x0b, 0xa4, 0x8c,
+ 0x5f, 0x8a, 0x40, 0x8d, 0x46, 0x3e, 0xf5, 0x83, 0x13, 0x19, 0xfb, 0x7b,
+ 0x20, 0xbb, 0x2b, 0x1b, 0x3e, 0x31, 0x9e, 0x33, 0x6d, 0x3c, 0xd6, 0x6a,
+ 0x08, 0x99, 0x9a, 0x01, 0xad, 0x5c, 0x41, 0x3d, 0xb9, 0xf1, 0x15, 0xd1,
+ 0x03, 0xdb, 0x10, 0x91, 0xda, 0x7f, 0x5e, 0x78, 0x93, 0x94, 0x3f, 0xac,
+ 0xc0, 0xc3, 0x8b, 0x87, 0xe3, 0xed, 0x90, 0xbe, 0xc1, 0xc6, 0x09, 0x49,
+ 0xb3, 0x6a, 0x52, 0xeb, 0xcf, 0x85, 0xa4, 0x8d, 0x41, 0x42, 0x76, 0x17,
+ 0x3d, 0x2c, 0x42, 0x3b, 0xd7, 0xd7, 0x3e, 0x7c, 0xe0, 0xd6, 0x12, 0x1f,
+ 0xbb, 0x87, 0xe7, 0xc5, 0x50, 0x75, 0xa8, 0xe2, 0xf3, 0x15, 0x41, 0x6f,
+ 0xc0, 0x47, 0xf7, 0xf5, 0x9f, 0xd0, 0x8b, 0x23, 0x0a, 0x9c, 0xbc, 0x1a,
+ 0x33, 0x20, 0x30, 0x8a, 0xd1, 0x07, 0x6c, 0x84, 0x19, 0xb2, 0xdb, 0x0e,
+ 0xde, 0x9f, 0x39, 0x18, 0x8a, 0x75, 0x0f, 0x3e, 0x24, 0xcb, 0x50, 0x6f,
+ 0x90, 0x8a, 0x64, 0x0e, 0x23, 0x6e, 0x41, 0x56, 0xf3, 0x0f, 0xa2, 0x4d,
+ 0x29, 0x31, 0x3e, 0xcc, 0x5f, 0xc4, 0xa8, 0x52, 0x7a, 0x54, 0x43, 0x25,
+ 0x0d, 0x05, 0x6f, 0xea, 0x45, 0x3b, 0x09, 0x25, 0xa1, 0x24, 0xcd, 0x29,
+ 0x54, 0x86, 0xd4, 0xfe, 0x94, 0xbe, 0xe6, 0x73, 0x6e, 0xc7, 0x13, 0x0d,
+ 0x43, 0x2f, 0x86, 0xf1, 0x4a, 0x13, 0x2b, 0x86, 0x51, 0x8b, 0xaf, 0x81,
+ 0x6d, 0x95, 0xb4, 0x1f, 0x0d, 0x30, 0x44, 0x66, 0x96, 0x7e, 0xd2, 0xa5,
+ 0x74, 0x38, 0x03, 0x56, 0x1f, 0x99, 0x2a, 0xfe, 0x99, 0xcb, 0xa8, 0x35,
+ 0xf3, 0x5f, 0x2c, 0x3a, 0x06, 0xb3, 0x5c, 0x83, 0x91, 0x0a, 0x6a, 0x05,
+ 0x50, 0x72, 0x83, 0xc7, 0x9f, 0xbb, 0xcf, 0x50, 0x3d, 0xc2, 0x2a, 0x34,
+ 0x42, 0xc7, 0xb9, 0x2d, 0xba, 0xf5, 0xa1, 0xe4, 0x39, 0x63, 0xe4, 0x35,
+ 0x9b, 0xaa, 0xc6, 0x7b, 0x8b, 0x14, 0x25, 0x95, 0xba, 0x76, 0xa4, 0x89,
+ 0x2e, 0xbb, 0x3c, 0xf1, 0x30, 0xe7, 0xe6, 0xe2, 0x62, 0x59, 0xc5, 0x1e,
+ 0x86, 0xf1, 0xe1, 0x77, 0xfb, 0x04, 0x82, 0x2f, 0xa7, 0xca, 0x99, 0x7c,
+ 0xbc, 0x5b, 0x2f, 0xa8, 0x51, 0xc1, 0x10, 0x07, 0x70, 0x42, 0x20, 0x3a,
+ 0xec, 0xae, 0x92, 0x9d, 0x76, 0x8e, 0xc0, 0xa6, 0xb8, 0x3d, 0xb7, 0x0b,
+ 0x93, 0x2d, 0x35, 0x09, 0x66, 0x47, 0xc6, 0x3e, 0x53, 0x31, 0x34, 0x4f,
+ 0xa3, 0x7b, 0x23, 0x1f, 0xc8, 0x0a, 0x1d, 0xa8, 0x8e, 0xb4, 0xc4, 0xb7,
+ 0x03, 0x59, 0xbf, 0x0d, 0x03, 0xd3, 0x33, 0xd1, 0x29, 0x46, 0xaf, 0xb5,
+ 0xa5, 0x48, 0x9a, 0xdb, 0xc5, 0x5a, 0x6c, 0x86, 0xca, 0x7d, 0xa6, 0x16,
+ 0x21, 0x1e, 0x26, 0xe2, 0x98, 0x6a, 0xdb, 0x0f, 0xf8, 0xa9, 0x10, 0x7e,
+ 0xcf, 0x35, 0x5a, 0x36, 0xd9, 0x0b, 0x24, 0xef, 0x4f, 0x34, 0x7e, 0x0f,
+ 0x5a, 0x18, 0xb0, 0xb7, 0x21, 0xfd, 0xeb, 0xa5, 0x18, 0x02, 0x86, 0xfc,
+ 0x3d, 0x80, 0x19, 0x4b, 0x2c, 0xf7, 0xed, 0x75, 0xd0, 0x8d, 0xd8, 0x21,
+ 0xd3, 0x5e, 0x59, 0x5e, 0x30, 0x61, 0xee, 0xad, 0xf8, 0xd8, 0xb2, 0x3a,
+ 0x46, 0x7a, 0x59, 0x3a, 0x24, 0x3c, 0xda, 0xd8, 0xac, 0x8a, 0xb4, 0x9c,
+ 0xd7, 0xe0, 0x24, 0x64, 0x0d, 0x79, 0xe5, 0xc1, 0x69, 0xcf, 0x89, 0x24,
+ 0xad, 0x0b, 0xcf, 0xed, 0xc3, 0xe2, 0x83, 0xe5, 0x16, 0x5d, 0x8f, 0x46,
+ 0xca, 0x8f, 0x46, 0x4d, 0x92, 0x55, 0xea, 0x65, 0x01, 0xa2, 0x53, 0xbd,
+ 0xc6, 0x7c, 0x79, 0x1d, 0x06, 0x6c, 0x19, 0x2d, 0x35, 0x6d, 0xad, 0x1b,
+ 0x87, 0xf9, 0xdc, 0xa3, 0x69, 0x39, 0xe9, 0x1d, 0x9f, 0xdd, 0xe3, 0x90,
+ 0x83, 0x45, 0xfa, 0xa6, 0xf3, 0x37, 0xb7, 0x87, 0x18, 0x5b, 0x7c, 0xdc,
+ 0xe2, 0x55, 0x4d, 0x4d, 0x3f, 0x13, 0xa9, 0x9f, 0xbe, 0x9f, 0xb2, 0x30,
+ 0x8a, 0x1f, 0xa9, 0xa0, 0xba, 0x9b, 0x57, 0x8f, 0x9b, 0xa4, 0x84, 0x07,
+ 0xd7, 0x0c, 0x09, 0x1f, 0x2d, 0x21, 0xd0, 0xab, 0x46, 0xa7, 0xd8, 0x27,
+ 0x61, 0x90, 0x6b, 0x60, 0x9d, 0x3f, 0x89, 0xdf, 0x15, 0x64, 0x0d, 0x2c,
+ 0xca, 0x99, 0x7e, 0xbd, 0xed, 0x0f, 0xf6, 0xd7, 0x4e, 0x5b, 0x4c, 0x7d,
+ 0x1f, 0xfb, 0xb1, 0x50, 0x36, 0x6d, 0x73, 0x15, 0x63, 0xca, 0xa3, 0xf0,
+ 0xc4, 0x73, 0x16, 0x5e, 0x62, 0xa4, 0xae, 0xcf, 0x0b, 0x87, 0x3e, 0x9a,
+ 0xb2, 0x72, 0xb3, 0x60, 0x98, 0x39, 0xc0, 0x0b, 0x33, 0x48, 0x5d, 0x88,
+ 0xeb, 0x55, 0x44, 0xad, 0xa2, 0x47, 0xe5, 0x29, 0x45, 0x38, 0xed, 0xe7,
+ 0x82, 0x4c, 0xa1, 0xb1, 0x61, 0x41, 0x57, 0xd3, 0xce, 0x65, 0x15, 0x2f,
+ 0x5c, 0x7b, 0xc3, 0x80, 0xe2, 0xcc, 0x7c, 0xaf, 0xe5, 0xb9, 0xdc, 0x09,
+ 0x70, 0x09, 0x26, 0xd6, 0x70, 0x4d, 0xab, 0x7a, 0xce, 0x39, 0x32, 0x3e,
+ 0xd4, 0x34, 0x0c, 0x64, 0x78, 0xe9, 0x8e, 0xb7, 0x56, 0x0d, 0xea, 0xda,
+ 0x01, 0x70, 0xe9, 0x8e, 0xc1, 0xbc, 0x71, 0xd8, 0xcd, 0x44, 0x6c, 0x95,
+ 0x07, 0x26, 0xf5, 0x2f, 0x07, 0x12, 0x9c, 0xf3, 0x8c, 0x54, 0x97, 0x38,
+ 0x65, 0x9a, 0xc7, 0x54, 0xfc, 0x5b, 0x29, 0x14, 0x54, 0xa1, 0xe2, 0xe2,
+ 0x98, 0x3b, 0xbd, 0x96, 0x2d, 0x1f, 0xa6, 0xa2, 0x5b, 0xf9, 0xdd, 0x71,
+ 0xea, 0x0d, 0xc2, 0x4d, 0x44, 0xe7, 0xfc, 0x8e, 0xde, 0xe2, 0x73, 0xad,
+ 0x72, 0x99, 0x41, 0x19, 0x18, 0xc5, 0xc4, 0x12, 0x41, 0xb6, 0x55, 0x53,
+ 0x7c, 0x8c, 0x96, 0xf9, 0xd0, 0xad, 0xf0, 0xe2, 0x0e, 0xfe, 0x55, 0x3e,
+ 0x07, 0xb9, 0x82, 0x6b, 0xdb, 0xd9, 0x3e, 0x1f, 0x04, 0xe8, 0xeb, 0xeb,
+ 0x58, 0x27, 0x0c, 0x55, 0x61, 0x04, 0xf0, 0x89, 0x9d, 0x68, 0x54, 0xbb,
+ 0xbe, 0x40, 0x6d, 0x2b, 0x15, 0xaa, 0xe6, 0x68, 0x56, 0xfd, 0xee, 0x49,
+ 0x25, 0xe4, 0xd7, 0x36, 0x2c, 0x61, 0x91, 0xd0, 0x55, 0xaa, 0xcb, 0xb2,
+ 0x8c, 0xbd, 0x3a, 0x7b, 0x33, 0x85, 0x7a, 0xd6, 0x5f, 0xfb, 0x50, 0xd4,
+ 0xcf, 0xbe, 0x09, 0xe5, 0xc2, 0x82, 0x56, 0xba, 0x92, 0x9f, 0x4c, 0xea,
+ 0x94, 0xee, 0x97, 0x8b, 0x4a, 0xc0, 0x8f, 0xe2, 0x23, 0x24, 0xde, 0x7e,
+ 0x9c, 0xde, 0x5d, 0xb1, 0xf0, 0x38, 0x68, 0xab, 0xc9, 0x69, 0x54, 0xb5,
+ 0xfd, 0xa0, 0x87, 0x06, 0x68, 0x25, 0xbe, 0x07, 0x65, 0xca, 0xa2, 0x73,
+ 0x84, 0x3b, 0x93, 0xce, 0x13, 0x23, 0x76, 0x44, 0x9a, 0x28, 0x5a, 0x2d,
+ 0x32, 0xc9, 0xef, 0xc2, 0xd6, 0x68, 0x6b, 0xbb, 0xac, 0x50, 0x76, 0x0e,
+ 0xdd, 0x0e, 0x78, 0x1f, 0x74, 0x54, 0x65, 0xe7, 0x32, 0x18, 0x90, 0x8a,
+ 0xd8, 0x3c, 0xd5, 0x90, 0x00, 0x78, 0xf6, 0xda, 0x1a, 0x3e, 0x63, 0xf0,
+ 0x0e, 0x54, 0x1b, 0x70, 0xac, 0xf5, 0x79, 0x9a, 0x40, 0x12, 0x14, 0x2c,
+ 0x0f, 0xa5, 0xef, 0x91, 0x77, 0x4e, 0xef, 0x5e, 0x61, 0xbc, 0x71, 0x73,
+ 0xe7, 0x8f, 0x22, 0x13, 0x46, 0x8e, 0x86, 0xed, 0xdf, 0xfc, 0x39, 0xb2,
+ 0x86, 0x67, 0x90, 0x51, 0x7e, 0x69, 0x90, 0x9e, 0xa1, 0x54, 0x73, 0xd1,
+ 0x66, 0x89, 0xca, 0xf5, 0x12, 0xd0, 0xf6, 0x5c, 0x08, 0x62, 0x76, 0x0d,
+ 0x83, 0xc3, 0xb9, 0x2b, 0x0c, 0x14, 0x8b, 0x02, 0xa8, 0x11, 0x3a, 0x58,
+ 0xe5, 0x8a, 0xe4, 0x42, 0xe0, 0x14, 0xc4, 0x66, 0xf2, 0x8f, 0xce, 0x03,
+ 0x9f, 0x9d, 0xc7, 0x26, 0x01, 0x3d, 0x09, 0x83, 0x56, 0xd6, 0x36, 0x20,
+ 0xf5, 0xe2, 0xa3, 0xf4, 0x0a, 0xc0, 0xec, 0x44, 0x7c, 0x42, 0xf0, 0x00,
+ 0x79, 0x38, 0x8f, 0x20, 0xd0, 0xaa, 0xaf, 0x37, 0xd2, 0xb3, 0x63, 0xd4,
+ 0xf3, 0x04, 0xc7, 0x28, 0x37, 0x9e, 0x5a, 0x9b, 0x1d, 0xd4, 0xb6, 0x59,
+ 0x4b, 0x9d, 0x03, 0x60, 0x5c, 0x77, 0x8a, 0xc9, 0xff, 0xb2, 0x82, 0x19,
+ 0x4f, 0x87, 0xc5, 0x64, 0x0b, 0xc2, 0x1e, 0x4a, 0x0c, 0x01, 0x8a, 0xe6,
+ 0xc8, 0x9a, 0x8e, 0xbd, 0xd7, 0x14, 0x19, 0x54, 0x11, 0x2c, 0x23, 0x15,
+ 0x19, 0x01, 0xe7, 0xe9, 0xf1, 0x18, 0x3b, 0x4e, 0xa5, 0x0b, 0xec, 0x6f,
+ 0xa0, 0xda, 0xc0, 0x54, 0x6f, 0xf9, 0x35, 0x6a, 0xf4, 0x9e, 0x98, 0xb4,
+ 0x58, 0x18, 0x24, 0x93, 0x68, 0xa8, 0xa9, 0xd1, 0x18, 0x0f, 0x8c, 0xc4,
+ 0xea, 0xed, 0x93, 0x18, 0xd6, 0xeb, 0xc8, 0xc5, 0xb5, 0x15, 0x5f, 0xf3,
+ 0x16, 0xa3, 0xb9, 0xc6, 0x30, 0x43, 0xee, 0x57, 0xfb, 0x2d, 0x80, 0x60,
+ 0xac, 0x57, 0x9f, 0xde, 0x03, 0xf4, 0x4c, 0xf0, 0x0b, 0x12, 0x1e, 0x91,
+ 0xa3, 0x14, 0xa1, 0xaf, 0x4c, 0x87, 0x24, 0x93, 0x4d, 0x83, 0x5a, 0x2c,
+ 0x00, 0xd8, 0x5d, 0x7c, 0xbe, 0x06, 0x43, 0x2c, 0xd7, 0xc4, 0xc8, 0x1a,
+ 0x80, 0x71, 0xb6, 0x52, 0x90, 0xed, 0x10, 0x19, 0xbf, 0xab, 0x93, 0x58,
+ 0x02, 0x7e, 0xcd, 0x66, 0x4b, 0x15, 0x17, 0xa7, 0xb4, 0x75, 0x85, 0x7e,
+ 0x7d, 0x64, 0x2b, 0x38, 0xd6, 0x7e, 0x2f, 0xea, 0x26, 0xb3, 0x09, 0x0a,
+ 0x19, 0xaf, 0x0c, 0x96, 0x7d, 0x5f, 0x9c, 0xec, 0x30, 0xd0, 0xaf, 0x94,
+ 0xb0, 0x38, 0x2f, 0xd9, 0x3c, 0xcc, 0x7d, 0x53, 0xf1, 0x1d, 0x71, 0x2e,
+ 0x41, 0x2b, 0x3b, 0x03, 0x34, 0xa2, 0x4d, 0x6c, 0x54, 0xc7, 0x58, 0x4e,
+ 0x00, 0x5d, 0x71, 0x1d, 0x0e, 0x39, 0xdb, 0xf9, 0x5f, 0xc6, 0xb6, 0x70,
+ 0xa1, 0x41, 0xab, 0xd1, 0x00, 0x45, 0xad, 0x51, 0x3b, 0x48, 0xbd, 0x39,
+ 0x50, 0xf2, 0x2d, 0x6d, 0x30, 0x11, 0x33, 0xb3, 0xb6, 0x31, 0x15, 0xce,
+ 0x3a, 0x3d, 0x32, 0xda, 0x11, 0x57, 0xf6, 0x68, 0x5f, 0xeb, 0xde, 0xf3,
+ 0xb3, 0x51, 0x6d, 0xb2, 0x76, 0xda, 0xc3, 0x04, 0x24, 0x9b, 0xd7, 0xd9,
+ 0x09, 0x99, 0x31, 0xbd, 0x16, 0x50, 0xf5, 0xea, 0x38, 0x4c, 0xdc, 0x4c,
+ 0x12, 0xea, 0xe5, 0x63, 0x78, 0xe2, 0x7f, 0x4f, 0x03, 0x20, 0x42, 0xfc,
+ 0xe7, 0x4b, 0x1e, 0x64, 0x1a, 0xed, 0x72, 0x94, 0xa6, 0xae, 0xab, 0x99,
+ 0xf4, 0x25, 0x87, 0xe2, 0x82, 0xe1, 0xe0, 0x16, 0xdc, 0x67, 0x4e, 0xb7,
+ 0x06, 0xe9, 0x33, 0x12, 0x90, 0x10, 0x0d, 0x92, 0xe9, 0x66, 0x34, 0xb3,
+ 0xbb, 0x50, 0x22, 0x39, 0x9a, 0x9c, 0x5c, 0xa2, 0x13, 0xb5, 0x89, 0xb7,
+ 0x8f, 0xc1, 0xc1, 0x1a, 0xc9, 0x12, 0x63, 0x2a, 0x86, 0xa6, 0x37, 0xf5,
+ 0x37, 0x23, 0x91, 0xee, 0x97, 0xb0, 0x43, 0xcb, 0x8c, 0xad, 0x73, 0x81,
+ 0xad, 0xa2, 0x40, 0x56, 0xfb, 0x1e, 0x1a, 0xf9, 0xd7, 0x5f, 0xf4, 0xcf,
+ 0x30, 0x8e, 0x6c, 0xba, 0x99, 0x00, 0x7b, 0x8c, 0x75, 0xb7, 0xa2, 0x94,
+ 0xbf, 0x7a, 0x31, 0x9a, 0xec, 0xe9, 0x8f, 0xf0, 0xde, 0xf0, 0x5a, 0x47,
+ 0xc4, 0x58, 0x16, 0xdb, 0x9a, 0x9d, 0x0d, 0x9d, 0x7b, 0x11, 0x51, 0x97,
+ 0xd7, 0xe5, 0xff, 0x32, 0x69, 0xb7, 0x11, 0xab, 0xaa, 0x00, 0xa8, 0x44,
+ 0xe4, 0xd5, 0xa6, 0x85, 0xb8, 0x83, 0xa1, 0x86, 0x3f, 0x09, 0x7e, 0xf8,
+ 0x7f, 0x2e, 0x4c, 0xa6, 0x60, 0x10, 0xe4, 0x50, 0xa4, 0xca, 0xfc, 0xab,
+ 0xcc, 0x1a, 0x3c, 0xec, 0x16, 0xe9, 0x72, 0x7a, 0xee, 0xbb, 0x3b, 0xc7,
+ 0x1a, 0xce, 0xe4, 0x45, 0x6a, 0x60, 0x9d, 0xe5, 0x30, 0x04, 0x18, 0x10,
+ 0x82, 0x2c, 0x57, 0xad, 0x44, 0x3e, 0x5e, 0x59, 0x44, 0x66, 0xdb, 0x24,
+ 0x18, 0xa3, 0x9f, 0xa0, 0x46, 0xdf, 0x5b, 0xe8, 0xc8, 0x18, 0x4e, 0xc1,
+ 0x71, 0x0b, 0x62, 0xcf, 0x9b, 0xd4, 0xc0, 0x64, 0x12, 0xa7, 0x60, 0xe1,
+ 0x31, 0x4b, 0x6c, 0xea, 0x78, 0x2b, 0x36, 0x18, 0xff, 0xe3, 0x57, 0x3b,
+ 0xff, 0x2e, 0x9d, 0xb1, 0xcb, 0x9e, 0x5a, 0x75, 0x51, 0x11, 0xc9, 0xa1,
+ 0xb2, 0x7a, 0x04, 0x1c, 0x5b, 0xf9, 0x4e, 0xf6, 0x17, 0x34, 0x66, 0x69,
+ 0x24, 0xbe, 0x5f, 0xf2, 0x7e, 0x13, 0xff, 0x01, 0x47, 0xe8, 0xd4, 0x96,
+ 0x40, 0x32, 0x79, 0x6b, 0xf2, 0xaf, 0x00, 0xcd, 0xd5, 0xff, 0xc8, 0x05,
+ 0x12, 0xa6, 0x1d, 0x18, 0xb1, 0x83, 0x70, 0x01, 0xc9, 0x26, 0x72, 0xe8,
+ 0x23, 0x9d, 0x4d, 0x85, 0xb8, 0xa0, 0x3e, 0x01, 0x30, 0x3c, 0xf4, 0xf8,
+ 0xc1, 0xc2, 0xda, 0xe9, 0x70, 0xf5, 0xed, 0xc5, 0x7a, 0xf5, 0x86, 0x46,
+ 0x06, 0x05, 0x07, 0x71, 0x17, 0x6c, 0x53, 0xe8, 0x77, 0x8c, 0xbf, 0x3b,
+ 0x77, 0x62, 0x6b, 0x99, 0x00, 0x63, 0xee, 0x76, 0x98, 0x97, 0x86, 0x58,
+ 0x9e, 0xd7, 0x7b, 0xb5, 0x32, 0x63, 0xaf, 0xd4, 0x55, 0x2f, 0xa2, 0xb8,
+ 0x86, 0xb4, 0x13, 0xef, 0x9c, 0x1b, 0xc2, 0x46, 0x4e, 0x9e, 0x1e, 0xcd,
+ 0x0d, 0xab, 0x92, 0x50, 0xd2, 0x04, 0x7b, 0x2a, 0xc3, 0x50, 0x36, 0x47,
+ 0x9c, 0xab, 0x32, 0xc4, 0x55, 0x57, 0xbe, 0x21, 0xba, 0x47, 0x62, 0xf5,
+ 0x03, 0x63, 0xa6, 0x55, 0x84, 0x63, 0x75, 0x54, 0xe1, 0x93, 0xd5, 0x9b,
+ 0x92, 0x35, 0x3c, 0x71, 0xe0, 0xae, 0x97, 0x65, 0x30, 0x0a, 0x5e, 0x2e,
+ 0x1b, 0x95, 0xb7, 0xf2, 0xe9, 0x0f, 0x80, 0xe6, 0x6c, 0x4b, 0x36, 0xe1,
+ 0x83, 0x21, 0xfc, 0x78, 0x83, 0xe0, 0x93, 0xff, 0x03, 0x27, 0x14, 0x38,
+ 0x42, 0x98, 0x28, 0x51, 0x4b, 0x69, 0x0d, 0xc3, 0xe1, 0x92, 0x4b, 0xf2,
+ 0x5b, 0xc0, 0x8e, 0xa9, 0xb9, 0x89, 0x93, 0xa3, 0x3b, 0x49, 0x96, 0x3c,
+ 0x34, 0x00, 0x05, 0x3d, 0x37, 0x2a, 0x8c, 0x47, 0x13, 0x54, 0x8e, 0xd7,
+ 0xa6, 0xda, 0xcc, 0x89, 0x57, 0xe7, 0x40, 0x73, 0xc6, 0xe4, 0xd2, 0xe2,
+ 0x91, 0x2c, 0xe3, 0xfc, 0x03, 0x4e, 0xf0, 0xb2, 0x84, 0x7c, 0xb9, 0x61,
+ 0x9b, 0x63, 0x8b, 0x5c, 0xfe, 0x40, 0xf9, 0xe6, 0x32, 0x4e, 0x54, 0xfb,
+ 0x8f, 0x11, 0x45, 0x83, 0x23, 0xf6, 0x84, 0xd9, 0x4f, 0x34, 0xff, 0x8c,
+ 0xb5, 0x40, 0x67, 0xa8, 0x09, 0x16, 0x0b, 0xea, 0x43, 0xbe, 0xc2, 0xbb,
+ 0x43, 0xb1, 0xdc, 0xe7, 0x5b, 0xf7, 0xdb, 0x32, 0xdd, 0x85, 0xb4, 0x74,
+ 0xd5, 0x35, 0x41, 0x86, 0x0d, 0x26, 0x1f, 0x81, 0x1b, 0x13, 0xb0, 0xea,
+ 0x76, 0x06, 0x54, 0x76, 0xee, 0x6e, 0x07, 0x69, 0xbd, 0x5e, 0x13, 0xd5,
+ 0x54, 0x0b, 0xc5, 0xfd, 0xc6, 0x50, 0x36, 0x63, 0x3f, 0x47, 0x25, 0x09,
+ 0x96, 0x38, 0x34, 0x85, 0x21, 0x1e, 0xd8, 0x4d, 0xf2, 0x90, 0xe1, 0x3c,
+ 0xca, 0x3c, 0x96, 0x1b, 0x6d, 0x37, 0x1e, 0x71, 0xaf, 0x72, 0xa3, 0xd5,
+ 0xf6, 0x7b, 0x56, 0x97, 0x56, 0x79, 0xda, 0xf9, 0x98, 0x4d, 0x92, 0x02,
+ 0x15, 0x54, 0xbc, 0x06, 0xf9, 0x32, 0x64, 0x1c, 0x49, 0x44, 0x45, 0xe7,
+ 0x40, 0x13, 0xd2, 0xe1, 0x99, 0xcc, 0x58, 0x0b, 0xda, 0x21, 0x2e, 0x99,
+ 0x26, 0xc6, 0x5d, 0x40, 0xf3, 0xaf, 0x1a, 0x5a, 0x70, 0x7d, 0x79, 0x7d,
+ 0x47, 0x1f, 0x74, 0xc9, 0x1a, 0xe8, 0x18, 0x01, 0x46, 0x7a, 0x25, 0xdf,
+ 0xd1, 0xf6, 0xf5, 0x1b, 0x62, 0x8c, 0xf9, 0x85, 0xc1, 0xc9, 0x73, 0x94,
+ 0xfd, 0x52, 0xd3, 0x2a, 0xb0, 0x0f, 0xf7, 0x32, 0xc6, 0xc1, 0xc6, 0xce,
+ 0xea, 0xdd, 0x4d, 0x53, 0x86, 0x8c, 0x84, 0x8e, 0xd8, 0x03, 0xe7, 0x7f,
+ 0xfe, 0x62, 0x96, 0x20, 0xd7, 0xb2, 0xd3, 0x71, 0xc2, 0xac, 0xd6, 0x96,
+ 0x3b, 0x52, 0x34, 0x8d, 0x5f, 0xf4, 0xc9, 0xa4, 0x68, 0xdd, 0xd9, 0x54,
+ 0x06, 0x18, 0xd2, 0x98, 0x49, 0xc5, 0x5a, 0xd7, 0x6b, 0xfc, 0x43, 0x7a,
+ 0x44, 0x01, 0x45, 0x11, 0x4d, 0x44, 0x1a, 0x42, 0xcd, 0xf1, 0x8d, 0xae,
+ 0xaf, 0x91, 0x0a, 0xb3, 0xae, 0x07, 0xb6, 0xef, 0x74, 0x44, 0x9a, 0xbc,
+ 0x7b, 0x57, 0x4c, 0x70, 0x69, 0x65, 0xac, 0x88, 0x9d, 0x8c, 0x05, 0x22,
+ 0xe3, 0xdd, 0x5c, 0x46, 0xd3, 0x86, 0xd7, 0x42, 0x85, 0x9c, 0x76, 0x8e,
+ 0x96, 0x06, 0x18, 0x19, 0xbc, 0xb3, 0x2b, 0x7d, 0x92, 0x52, 0x6f, 0x07,
+ 0xbe, 0x7a, 0x49, 0xe3, 0x3a, 0xf9, 0x4f, 0x02, 0x9c, 0xd1, 0x54, 0x75,
+ 0xf0, 0xca, 0x03, 0x89, 0xc9, 0x3d, 0xba, 0x83, 0xc2, 0xf4, 0x77, 0xad,
+ 0x02, 0x1a, 0x7e, 0xcc, 0xa7, 0xba, 0x39, 0xbe, 0x32, 0x36, 0x75, 0x99,
+ 0x43, 0xa9, 0xf6, 0x63, 0xdd, 0xe9, 0xc3, 0x58, 0x10, 0x1e, 0xec, 0xd6,
+ 0x1d, 0x2d, 0x34, 0x71, 0x12, 0xec, 0x4c, 0xf2, 0xe8, 0x97, 0x72, 0x3f,
+ 0xc9, 0xa9, 0xb4, 0x3b, 0x57, 0x3a, 0xa4, 0xb3, 0xc0, 0x47, 0xc0, 0x7e,
+ 0x7a, 0xce, 0x19, 0x4b, 0x03, 0x22, 0xed, 0xc8, 0x7f, 0xd4, 0xc7, 0xe1,
+ 0x2a, 0x26, 0x15, 0xf8, 0x76, 0x69, 0xbe, 0xd6, 0x18, 0x28, 0x10, 0x68,
+ 0x51, 0xfb, 0x5d, 0x1b, 0x03, 0x63, 0xb8, 0x33, 0xb1, 0x90, 0x1f, 0x69,
+ 0x94, 0x51, 0xfb, 0x20, 0x5a, 0xa4, 0x3c, 0xee, 0x22, 0x2f, 0x83, 0x4f,
+ 0x5d, 0x26, 0x5f, 0x5e, 0xc7, 0x15, 0x3b, 0x90, 0x62, 0x0d, 0x48, 0x47,
+ 0x31, 0x18, 0x9c, 0xd3, 0xea, 0x46, 0x22, 0xbb, 0x4e, 0x44, 0xb1, 0x89,
+ 0xf0, 0x53, 0xdf, 0x63, 0x3d, 0x33, 0x2c, 0xca, 0xee, 0xfa, 0x55, 0x7c,
+ 0x3b, 0x9f, 0xaa, 0xdd, 0x68, 0xe5, 0x25, 0x46, 0x9d, 0xde, 0x49, 0x44,
+ 0x78, 0x4e, 0x0e, 0x9e, 0xba, 0x3b, 0xc4, 0x3d, 0xfb, 0x9c, 0x51, 0x05,
+ 0x06, 0x59, 0x20, 0xca, 0xda, 0x70, 0xb3, 0xa5, 0x91, 0xf5, 0x6d, 0xf8,
+ 0x42, 0x93, 0x9e, 0x39, 0x9a, 0xd1, 0x87, 0x49, 0xc8, 0x70, 0xf1, 0xc9,
+ 0x75, 0x2c, 0xf8, 0xdf, 0x15, 0x5d, 0x65, 0x92, 0x97, 0x2f, 0x77, 0x52,
+ 0x20, 0x9b, 0xec, 0xdb, 0xfb, 0x7b, 0xfc, 0x0e, 0x4c, 0x3d, 0xeb, 0x97,
+ 0xa3, 0x8e, 0x15, 0x78, 0xeb, 0x28, 0xde, 0xf0, 0xfd, 0x85, 0x09, 0x1b,
+ 0x60, 0x12, 0x98, 0xb0, 0x54, 0xc0, 0xdb, 0xcb, 0x84, 0x9b, 0xbe, 0x8f,
+ 0x61, 0x6e, 0xf1, 0x2c, 0xf5, 0xfa, 0x5e, 0x06, 0x42, 0xf7, 0xf3, 0xf4,
+ 0x9d, 0x05, 0xc2, 0x3f, 0xf7, 0x77, 0x26, 0xae, 0x49, 0xd4, 0x9e, 0x84,
+ 0x87, 0x01, 0xf4, 0xa0, 0x5c, 0x79, 0x44, 0x3d, 0xa9, 0xe4, 0xdf, 0x80,
+ 0xff, 0x8e, 0x6e, 0x0b, 0xba, 0xff, 0xc7, 0x8d, 0xee, 0x1a, 0x72, 0x87,
+ 0x07, 0xd2, 0xc4, 0x6b, 0xf9, 0x03, 0x0f, 0x04, 0x22, 0xe4, 0xeb, 0x8e,
+ 0xea, 0xfd, 0xbb, 0x91, 0x43, 0xa1, 0xeb, 0xda, 0x50, 0x3b, 0xbc, 0xda,
+ 0xf5, 0x25, 0xbb, 0x49, 0xda, 0xaa, 0x1c, 0x4b, 0x0b, 0xb7, 0x1c, 0xa3,
+ 0x4e, 0x5e, 0x72, 0x8e, 0xf1, 0x67, 0x5b, 0x4e, 0x34, 0x7f, 0x03, 0x1f,
+ 0x33, 0x59, 0x43, 0x45, 0x31, 0x50, 0x01, 0x46, 0xfd, 0x35, 0x04, 0x76,
+ 0x1e, 0x94, 0xfe, 0xf5, 0x27, 0xc7, 0x7d, 0xba, 0x54, 0x32, 0x27, 0x32,
+ 0xf1, 0x6f, 0xde, 0x53, 0xfd, 0x6a, 0x41, 0x6b, 0xa6, 0x25, 0xe8, 0xf8,
+ 0x9e, 0xba, 0xca, 0xd8, 0x68, 0xad, 0xde, 0xaf, 0xc1, 0xae, 0x3f, 0xa7,
+ 0xfa, 0x7f, 0xf2, 0xee, 0x5f, 0x4d, 0x69, 0x04, 0x1e, 0x15, 0xa7, 0xd6,
+ 0x5c, 0xcb, 0xf9, 0x34, 0xcf, 0x62, 0x39, 0xf0, 0xd3, 0x25, 0x83, 0x8d,
+ 0xad, 0x44, 0xaa, 0xd1, 0x6a, 0xd8, 0xbe, 0x81, 0xdc, 0x96, 0xa0, 0x39,
+ 0xf5, 0xe1, 0x50, 0x75, 0xef, 0x0c, 0x25, 0xc6, 0x24, 0x72, 0x1b, 0x25,
+ 0x4b, 0xb7, 0xc7, 0xab, 0xf4, 0xaf, 0x9c, 0xca, 0x2f, 0x3b, 0xbb, 0x9a,
+ 0xb9, 0xb1, 0xaf, 0x0b, 0xf6, 0xfa, 0xd5, 0x6e, 0x66, 0xf2, 0x92, 0xc4,
+ 0x38, 0xfa, 0x4a, 0x9a, 0x2d, 0xab, 0xf6, 0x14, 0x55, 0x3c, 0x7f, 0x7e,
+ 0xd7, 0xcb, 0x3b, 0xa5, 0xbe, 0x54, 0x1a, 0xa7, 0xac, 0xc2, 0x66, 0xa6,
+ 0xa5, 0xa9, 0x65, 0xdb, 0x98, 0x2a, 0x57, 0x66, 0x84, 0xae, 0x6f, 0x59,
+ 0xa8, 0xd4, 0x44, 0x0d, 0xcd, 0xbd, 0x2d, 0xb0, 0x60, 0x4b, 0x89, 0x1d,
+ 0xbe, 0x79, 0x15, 0x9f, 0xad, 0xde, 0xf1, 0x27, 0xe3, 0x42, 0x06, 0x12,
+ 0x37, 0x08, 0xd6, 0xca, 0xc6, 0xf3, 0x4f, 0xbc, 0xd4, 0x93, 0x51, 0xa8,
+ 0x97, 0x27, 0xc1, 0x12, 0x25, 0x7a, 0xb7, 0xb1, 0xb2, 0x03, 0x67, 0x21,
+ 0x3d, 0xf5, 0xd4, 0x37, 0x2e, 0xb5, 0x53, 0xc8, 0x6c, 0x5a, 0x0b, 0x2f,
+ 0xcc, 0xb7, 0xa1, 0x63, 0x17, 0x8c, 0x41, 0xb0, 0x8f, 0xd1, 0x39, 0x5e,
+ 0xba, 0x1d, 0xc5, 0x1b, 0xa0, 0x0e, 0xd0, 0xe6, 0x5a, 0xc2, 0xb0, 0x66,
+ 0x50, 0xfd, 0x2e, 0x99, 0x89, 0xcf, 0x57, 0x54, 0x81, 0xb4, 0x50, 0x44,
+ 0x46, 0x57, 0x87, 0x00, 0x82, 0xcc, 0x8a, 0xd5, 0xf8, 0x23, 0x07, 0xae,
+ 0x63, 0x6b, 0xec, 0xe2, 0x77, 0x9e, 0x79, 0xa5, 0xc5, 0x53, 0x33, 0xa1,
+ 0x29, 0x50, 0x57, 0x3c, 0xed, 0x95, 0x6a, 0xb3, 0x5f, 0xf5, 0x0a, 0xc5,
+ 0x80, 0x5d, 0x9f, 0x3a, 0xfe, 0x26, 0x79, 0xe7, 0xc1, 0x46, 0x6b, 0x38,
+ 0x58, 0xd1, 0x02, 0x7d, 0x5c, 0x60, 0xfb, 0xb8, 0xb7, 0x01, 0xc4, 0x58,
+ 0x3b, 0xd8, 0x33, 0xe5, 0xcf, 0x2c, 0xf2, 0x5b, 0xa1, 0x70, 0xc7, 0xde,
+ 0xac, 0x97, 0x5a, 0x6d, 0xf1, 0xb8, 0x0a, 0x1c, 0x81, 0xfe, 0x7e, 0x7f,
+ 0x24, 0x78, 0x49, 0xb9, 0xe4, 0x00, 0x7a, 0xe7, 0x9c, 0xfa, 0x88, 0x4e,
+ 0x73, 0x0b, 0x38, 0x0b, 0x0e, 0x87, 0x40, 0xe1, 0x86, 0x86, 0x44, 0x70,
+ 0x02, 0x48, 0xc3, 0x10, 0xc0, 0xe2, 0xad, 0xd8, 0x60, 0x64, 0xc6, 0x56,
+ 0xf2, 0x23, 0xb2, 0x99, 0xe3, 0xb5, 0xef, 0x24, 0x84, 0x20, 0x39, 0x2b,
+ 0xcc, 0x12, 0x1a, 0x7b, 0x30, 0xd2, 0xed, 0x2c, 0x23, 0xb2, 0x84, 0x5a,
+ 0xff, 0x4a, 0x6f, 0x9f, 0xfa, 0xcd, 0x34, 0xcc, 0x09, 0x1b, 0xdc, 0x2e,
+ 0x9a, 0x13, 0x7d, 0x4d, 0x87, 0x09, 0x37, 0x72, 0x82, 0x21, 0x09, 0xcf,
+ 0xce, 0xa8, 0x3b, 0x3b, 0x62, 0x67, 0xc5, 0x24, 0x2a, 0x6a, 0x70, 0xad,
+ 0x99, 0x56, 0xaa, 0x8e, 0xde, 0x9b, 0x56, 0xd1, 0x5c, 0xab, 0x4b, 0x13,
+ 0x9c, 0x6b, 0xea, 0xb3, 0xf0, 0x39, 0xd9, 0x4f, 0xc0, 0x45, 0xf2, 0xe0,
+ 0x52, 0x07, 0x11, 0x14, 0xb0, 0x0a, 0x81, 0x29, 0x16, 0x8a, 0x74, 0x79,
+ 0x1c, 0x3d, 0xfd, 0xfe, 0x42, 0x81, 0x98, 0x48, 0x91, 0x89, 0xb0, 0xc5,
+ 0x83, 0x6b, 0xe9, 0xa0, 0x09, 0x9e, 0x01, 0x27, 0xdf, 0x2f, 0x82, 0x28,
+ 0x40, 0xef, 0xcb, 0x49, 0xac, 0xa3, 0x87, 0x59, 0x3a, 0x34, 0x2c, 0x48,
+ 0x0f, 0x28, 0x9d, 0x44, 0x89, 0xde, 0xbb, 0x25, 0x8c, 0x72, 0x88, 0x5b,
+ 0x03, 0x2c, 0x39, 0x3f, 0xa3, 0xf7, 0xb3, 0x4d, 0xd7, 0x25, 0x69, 0xb0,
+ 0x99, 0x0c, 0x02, 0x09, 0xd4, 0x1d, 0xc1, 0xb4, 0x3a, 0x6d, 0x18, 0x69,
+ 0x18, 0xe7, 0x11, 0xc4, 0xc6, 0x53, 0xb3, 0x0d, 0xd2, 0xb9, 0x1f, 0x20,
+ 0x8c, 0x3f, 0xd5, 0xab, 0xfe, 0x14, 0x65, 0xc6, 0x79, 0x15, 0xc6, 0x38,
+ 0xec, 0xca, 0x2a, 0xb3, 0x47, 0x0a, 0xf7, 0x64, 0x6a, 0xd0, 0xe1, 0x27,
+ 0xcd, 0x36, 0x2e, 0xab, 0x96, 0xed, 0xf4, 0x99, 0x4b, 0x16, 0x93, 0x5e,
+ 0x31, 0x9b, 0x48, 0xf0, 0x70, 0xa6, 0x80, 0xff, 0xa0, 0xab, 0x8f, 0xe7,
+ 0xcb, 0x52, 0x04, 0xcd, 0xf5, 0x81, 0xa8, 0x97, 0xb0, 0xfa, 0xb4, 0x20,
+ 0x9e, 0x5f, 0xdd, 0x95, 0x2b, 0x6a, 0x35, 0x45, 0x3c, 0xfa, 0xa0, 0xd6,
+ 0x56, 0xc5, 0xf6, 0x65, 0xaf, 0x66, 0xb6, 0x56, 0xbc, 0x3b, 0x5d, 0x6b,
+ 0x5a, 0x5f, 0x19, 0x41, 0xf4, 0x6f, 0x9f, 0xbe, 0x75, 0x42, 0xa2, 0x2c,
+ 0x7a, 0x06, 0x4c, 0xdb, 0x43, 0x2d, 0x8a, 0x07, 0xc2, 0x7c, 0x6e, 0x6f,
+ 0x74, 0x2e, 0x83, 0xaa, 0xe4, 0x94, 0xfe, 0xba, 0x25, 0x2b, 0xc7, 0xbb,
+ 0xb3, 0xad, 0x88, 0x65, 0xaf, 0x1c, 0xb3, 0xc6, 0x12, 0x1a, 0x3c, 0x69,
+ 0xa6, 0x37, 0xfd, 0xf2, 0x5f, 0x77, 0x2b, 0xbb, 0xe6, 0x28, 0x66, 0xd0,
+ 0xcf, 0x2d, 0x9a, 0x36, 0x59, 0xa2, 0xb0, 0x99, 0x82, 0xf4, 0x09, 0x62,
+ 0x29, 0x95, 0x1a, 0xa7, 0x03, 0xa6, 0x6f, 0x7b, 0xfa, 0x7f, 0x23, 0xb7,
+ 0xaa, 0x78, 0xb8, 0x84, 0xe0, 0x6f, 0xff, 0xd1, 0x38, 0x63, 0x29, 0xd5,
+ 0x0f, 0x56, 0x1c, 0x49, 0xef, 0x56, 0xe3, 0x03, 0x7a, 0x90, 0x2b, 0x11,
+ 0xbe, 0xb9, 0x57, 0x16, 0xa2, 0xab, 0xc2, 0xb7, 0x89, 0x28, 0xa3, 0xd1,
+ 0x06, 0x54, 0xde, 0xac, 0xc1, 0xe5, 0x7d, 0x07, 0xad, 0xc3, 0x11, 0x61,
+ 0xd9, 0x45, 0xe8, 0x55, 0xb4, 0x0a, 0xf5, 0x1a, 0x77, 0xbd, 0x4a, 0x4c,
+ 0x28, 0xd0, 0x10, 0x42, 0xb8, 0x33, 0x0f, 0x5c, 0x87, 0xa5, 0xc5, 0x7d,
+ 0x86, 0xcf, 0x54, 0x45, 0x89, 0xf9, 0x4e, 0xb6, 0xcf, 0xe3, 0x57, 0xbb,
+ 0x47, 0x93, 0xd3, 0x3d, 0x2f, 0x29, 0x27, 0x99, 0x47, 0x22, 0x90, 0x5b,
+ 0xa6, 0xae, 0x50, 0x84, 0xac, 0x60, 0x5c, 0x59, 0x27, 0x3d, 0xe8, 0xfa,
+ 0x3c, 0x0c, 0x37, 0xef, 0xa3, 0x7f, 0x06, 0xd4, 0x68, 0x28, 0x81, 0xa2,
+ 0x5c, 0x35, 0x85, 0x0c, 0xb2, 0x89, 0x3d, 0xfd, 0x0e, 0x07, 0x29, 0x3d,
+ 0x2d, 0xa5, 0x20, 0x3e, 0xa0, 0x79, 0x1a, 0xb3, 0x56, 0x84, 0xab, 0x43,
+ 0x24, 0x9c, 0x4e, 0x11, 0xac, 0xe5, 0xec, 0xd4, 0xf3, 0x47, 0x96, 0x96,
+ 0x8d, 0x8e, 0x41, 0x62, 0x38, 0xf1, 0xaf, 0x8a, 0x1f, 0xb6, 0xec, 0x1c,
+ 0x56, 0xc4, 0x87, 0xc7, 0x1f, 0x7d, 0x1c, 0xd8, 0xaf, 0x96, 0xe2, 0xf9,
+ 0xb6, 0x2d, 0xb2, 0x7c, 0xe8, 0xeb, 0xf7, 0x13, 0x64, 0x7b, 0x22, 0x49,
+ 0xfe, 0x81, 0xeb, 0x67, 0x11, 0x2d, 0x73, 0x5d, 0xaa, 0x80, 0x82, 0x07,
+ 0x0a, 0x0f, 0x3d, 0x5a, 0x80, 0xdb, 0x59, 0xad, 0x56, 0x1f, 0x6a, 0xa6,
+ 0xad, 0xb9, 0x63, 0x87, 0xc6, 0x3d, 0xc6, 0xc1, 0x28, 0x21, 0xf7, 0xc2,
+ 0x78, 0xc3, 0xe2, 0x7c, 0x47, 0xba, 0x20, 0xab, 0x05, 0xbf, 0xf8, 0xa1,
+ 0xc3, 0x51, 0x30, 0xa3, 0x05, 0x4c, 0xd9, 0x43, 0x7a, 0xf9, 0xd0, 0x9f,
+ 0x14, 0x95, 0x12, 0xf6, 0xce, 0xfe, 0x92, 0x83, 0x09, 0x86, 0xb4, 0x67,
+ 0xd2, 0xe9, 0x02, 0x34, 0x5b, 0xf7, 0x79, 0x46, 0x7a, 0xab, 0x02, 0xcf,
+ 0x40, 0x0f, 0x7e, 0x7e, 0x46, 0x6f, 0x94, 0xce, 0x79, 0x70, 0x03, 0xcb,
+ 0x27, 0x85, 0x5a, 0x18, 0x41, 0xfc, 0x50, 0xf9, 0x85, 0x4c, 0xdb, 0xe6,
+ 0xc0, 0xb2, 0xd4, 0xa7, 0x01, 0xb0, 0xb5, 0x22, 0x93, 0x2b, 0x44, 0xc8,
+ 0xc9, 0x41, 0x9b, 0x2f, 0xd0, 0x3c, 0x67, 0x00, 0xf6, 0xcd, 0x5f, 0x04,
+ 0x3b, 0xef, 0x04, 0xb8, 0x97, 0xed, 0x40, 0x7f, 0x7c, 0x6f, 0x80, 0xc2,
+ 0xe5, 0x47, 0xb3, 0x84, 0xf2, 0x98, 0x1e, 0xb4, 0x19, 0xd8, 0xab, 0xaf,
+ 0x40, 0xf7, 0xfc, 0xcb, 0x7c, 0xd0, 0xf5, 0x2d, 0x7f, 0x48, 0x7f, 0xd0,
+ 0x01, 0x08, 0x47, 0xaa, 0xbe, 0xa0, 0xce, 0x26, 0x13, 0xf4, 0x9e, 0xce,
+ 0x82, 0x81, 0xec, 0xe8, 0x27, 0x23, 0x44, 0x51, 0x02, 0x93, 0xfe, 0x9c,
+ 0xff, 0x7c, 0x66, 0x2e, 0x48, 0xfc, 0x0e, 0x5a, 0xce, 0x3d, 0xe6, 0xe4,
+ 0x46, 0x72, 0x51, 0xba, 0x7d, 0xb9, 0xbd, 0x47, 0xbd, 0x09, 0xab, 0x6b,
+ 0xb6, 0x8f, 0xa0, 0x19, 0xa2, 0x03, 0xd7, 0xe6, 0x20, 0x1a, 0xb1, 0xd1,
+ 0x15, 0x79, 0x16, 0xac, 0x6d, 0xa5, 0x32, 0xa9, 0xe0, 0x01, 0xa7, 0x71,
+ 0x77, 0x65, 0x48, 0x90, 0xe0, 0x9f, 0xf1, 0xc6, 0x7f, 0xc7, 0x2c, 0x2a,
+ 0x50, 0xa2, 0xf8, 0x9d, 0x02, 0xc6, 0x11, 0xdc, 0x14, 0x07, 0xd8, 0x51,
+ 0xc9, 0x8f, 0xd9, 0x2f, 0x04, 0x8a, 0x3a, 0x13, 0x00, 0x31, 0xb6, 0x22,
+ 0xc9, 0xd9, 0x76, 0x27, 0x76, 0x8f, 0x09, 0x96, 0xf8, 0xf9, 0x0c, 0x64,
+ 0x02, 0xd8, 0x71, 0xf8, 0x56, 0xd2, 0xaf, 0x4b, 0x9e, 0x8b, 0xbe, 0x8b,
+ 0xa7, 0x4b, 0x33, 0xfa, 0xeb, 0x5a, 0x16, 0xe3, 0xe8, 0xb7, 0x4a, 0xb4,
+ 0x8a, 0xfb, 0x94, 0xcb, 0xeb, 0x45, 0x8e, 0x4e, 0xa8, 0x0d, 0x73, 0x48,
+ 0xca, 0x22, 0x6f, 0xc0, 0xab, 0xba, 0xaf, 0x6b, 0x30, 0x56, 0x0b, 0xa8,
+ 0x5c, 0x63, 0xd4, 0xdb, 0xe9, 0xbf, 0x3f, 0x43, 0x42, 0x67, 0x61, 0xef,
+ 0x65, 0x8d, 0x27, 0x2b, 0x5b, 0xd4, 0xa5, 0xf7, 0x6d, 0x6b, 0x90, 0xc0,
+ 0x5c, 0x57, 0xd8, 0x63, 0x74, 0xd0, 0xce, 0x26, 0x51, 0x77, 0x8d, 0xbc,
+ 0xc2, 0xe8, 0x76, 0xae, 0x0d, 0x90, 0xc1, 0x71, 0x78, 0x6c, 0xac, 0x71,
+ 0xa0, 0x80, 0x1f, 0xe4, 0xf6, 0x34, 0x8c, 0x24, 0x7c, 0x7f, 0x27, 0x98,
+ 0x52, 0xe5, 0x27, 0x01, 0x32, 0x9b, 0x7a, 0x53, 0x74, 0x68, 0x3c, 0x96,
+ 0x79, 0x8e, 0x5e, 0x6a, 0x76, 0x90, 0xf3, 0x05, 0xd2, 0xa4, 0xb1, 0x01,
+ 0xcb, 0x40, 0xaf, 0xc2, 0x76, 0xfa, 0xf1, 0x6b, 0xbd, 0x7d, 0xfe, 0x98,
+ 0xad, 0xf1, 0x80, 0x18, 0x79, 0x4d, 0x24, 0x3b, 0x71, 0xa5, 0xff, 0x4b,
+ 0x03, 0x75, 0xad, 0x73, 0x46, 0x29, 0xea, 0xec, 0x7b, 0xa7, 0xc8, 0x04,
+ 0xcf, 0x43, 0x4e, 0x17, 0x05, 0x82, 0xee, 0x89, 0xe1, 0xfd, 0x8c, 0x4a,
+ 0xe2, 0xc0, 0x14, 0xcc, 0x58, 0x7b, 0x9c, 0x0e, 0x59, 0x53, 0x0e, 0x52,
+ 0x60, 0xd3, 0x67, 0xb5, 0x64, 0x3c, 0x52, 0xae, 0x0c, 0x60, 0xbf, 0x05,
+ 0xc9, 0xfc, 0xe1, 0x68, 0x0d, 0x57, 0x25, 0xc7, 0x5b, 0x27, 0x2c, 0x3a,
+ 0xff, 0x6e, 0xf3, 0xea, 0xab, 0xb9, 0xbb, 0x1f, 0x6a, 0x7e, 0xd5, 0x4d,
+ 0x70, 0x24, 0x4d, 0x89, 0x8a, 0x6e, 0xb5, 0x37, 0x6b, 0xd2, 0x91, 0x3c,
+ 0x51, 0x62, 0x09, 0xfd, 0x95, 0xf4, 0x3c, 0xf5, 0xaa, 0xb0, 0xed, 0x78,
+ 0x09, 0x35, 0x76, 0x9f, 0x69, 0x0c, 0x47, 0xd1, 0x36, 0x97, 0x44, 0x79,
+ 0x47, 0x2c, 0x62, 0x57, 0x2f, 0xae, 0x2c, 0xfc, 0x4c, 0x26, 0xf9, 0x4a,
+ 0x0c, 0xc0, 0x9b, 0x64, 0xaa, 0x42, 0x7c, 0x62, 0xf0, 0xe7, 0xda, 0x92,
+ 0xad, 0x0f, 0x1f, 0x98, 0xc8, 0x8b, 0x55, 0x97, 0x66, 0xce, 0x10, 0xee,
+ 0x17, 0xd1, 0x43, 0x05, 0x59, 0xbe, 0x47, 0x49, 0xad, 0x7c, 0x25, 0xa3,
+ 0x5b, 0xa8, 0x9e, 0x6e, 0x82, 0xc6, 0xff, 0x54, 0xd4, 0xff, 0x61, 0xb3,
+ 0xa1, 0x13, 0x28, 0x29, 0x2b, 0x70, 0x10, 0x59, 0x28, 0x5e, 0x64, 0x53,
+ 0x58, 0x38, 0x15, 0xd8, 0x46, 0xe8, 0xad, 0x43, 0x37, 0xd7, 0x4a, 0x88,
+ 0xa1, 0x4a, 0x12, 0x8e, 0x77, 0x93, 0x34, 0xb9, 0x25, 0x73, 0x3d, 0x59,
+ 0x13, 0xe7, 0xcd, 0x8a, 0xb4, 0x36, 0xd8, 0xaf, 0x64, 0x34, 0x15, 0x3c,
+ 0x5e, 0xab, 0x62, 0x90, 0x40, 0x41, 0x6b, 0xdf, 0x2b, 0x7d, 0x2c, 0x30,
+ 0xd9, 0xe2, 0xf8, 0xb7, 0x87, 0x07, 0xc8, 0x35, 0xd9, 0x81, 0x00, 0x89,
+ 0x9e, 0xbb, 0xb3, 0x94, 0x83, 0xb0, 0xea, 0xfb, 0xfb, 0x2c, 0x51, 0x4a,
+ 0x10, 0xa5, 0xe2, 0xb5, 0x0e, 0x42, 0xcd, 0x08, 0xc4, 0x42, 0xac, 0x70,
+ 0x44, 0xa3, 0xa0, 0xdf, 0x42, 0x02, 0x5e, 0x61, 0xd7, 0x86, 0xe7, 0x10,
+ 0xca, 0xdf, 0x6d, 0x41, 0x8c, 0x7f, 0xa2, 0x62, 0x76, 0x97, 0xc0, 0x17,
+ 0x2c, 0x0f, 0xf1, 0x32, 0x6b, 0x56, 0xcb, 0x55, 0x5c, 0xcb, 0x9e, 0xd4,
+ 0xbc, 0x85, 0xf2, 0x3a, 0x64, 0x79, 0xee, 0x87, 0xfd, 0xb0, 0x53, 0x4a,
+ 0x69, 0xb0, 0x91, 0xa9, 0xed, 0x21, 0x41, 0x2b, 0xb0, 0x27, 0x1b, 0xa1,
+ 0xdf, 0x5e, 0x13, 0x8d, 0xe7, 0x25, 0xd2, 0xc8, 0x3f, 0xb1, 0x84, 0xae,
+ 0xb0, 0xa6, 0xe4, 0xe8, 0xa0, 0x7a, 0x4b, 0x89, 0xfe, 0x3d, 0x2b, 0x3a,
+ 0x9d, 0xd1, 0x98, 0xa2, 0xce, 0xeb, 0x31, 0x62, 0xa8, 0x3c, 0xe1, 0xcd,
+ 0xc0, 0xfa, 0xf1, 0xfb, 0x5d, 0xd3, 0x75, 0xbe, 0x0f, 0xb6, 0x60, 0x38,
+ 0xec, 0x84, 0x9a, 0x9c, 0xe4, 0xdd, 0xf8, 0xac, 0x05, 0x17, 0xe6, 0x4b,
+ 0xa4, 0x73, 0x37, 0x46, 0x7c, 0x2f, 0xc1, 0xff, 0xa6, 0x0a, 0xa4, 0xce,
+ 0xb3, 0x5e, 0x72, 0x80, 0xc1, 0xeb, 0xfe, 0x4f, 0x72, 0x00, 0x9e, 0xea,
+ 0x04, 0xba, 0x57, 0x27, 0xa7, 0xb2, 0x49, 0x47, 0x7f, 0xd5, 0x59, 0xf8,
+ 0x25, 0x43, 0x7c, 0xcb, 0xf7, 0xda, 0x56, 0xa7, 0x1b, 0x50, 0xbb, 0xd3,
+ 0xec, 0x54, 0xec, 0xce, 0x5c, 0xdd, 0x94, 0x96, 0x64, 0x92, 0x2f, 0x8d,
+ 0xaf, 0xb2, 0x85, 0x42, 0x50, 0x1e, 0xf3, 0x0b, 0x34, 0xa4, 0xbb, 0x4f,
+ 0xac, 0xb1, 0x5b, 0x87, 0xfe, 0x9d, 0x9a, 0xc9, 0x20, 0x23, 0xc9, 0x3e,
+ 0x4a, 0x12, 0xba, 0xd2, 0x4b, 0x6e, 0x81, 0xdb, 0xf8, 0xaf, 0xf6, 0xbd,
+ 0x15, 0x1d, 0xc5, 0xea, 0xa0, 0x55, 0xc1, 0x36, 0x8f, 0xd8, 0x94, 0xf6,
+ 0x62, 0x99, 0xeb, 0x72, 0xf7, 0xa4, 0xf8, 0x90, 0x31, 0x6d, 0xdb, 0x15,
+ 0xc3, 0xc0, 0x0a, 0x07, 0x40, 0xab, 0xeb, 0xb1, 0xd4, 0x10, 0x7c, 0x8d,
+ 0x46, 0x1c, 0x8f, 0xd2, 0xfb, 0xfb, 0x1e, 0xeb, 0x78, 0xcd, 0x19, 0xae,
+ 0xda, 0x9c, 0x09, 0xe0, 0x5d, 0xf4, 0xc0, 0xf4, 0x4f, 0xb4, 0xb0, 0x10,
+ 0xea, 0x57, 0x7d, 0xa3, 0x15, 0x57, 0x01, 0x13, 0x96, 0x99, 0xa6, 0x2b,
+ 0x44, 0x5e, 0x2c, 0x55, 0xfe, 0x28, 0xe9, 0x8a, 0x50, 0x85, 0x1f, 0x49,
+ 0x0c, 0xbe, 0x84, 0x25, 0x6d, 0x0b, 0xbf, 0x68, 0xb7, 0x57, 0x4d, 0x9c,
+ 0xf1, 0x47, 0xce, 0xec, 0x58, 0x91, 0xd5, 0x78, 0x9a, 0x13, 0xa5, 0x2d,
+ 0xa6, 0xbd, 0x5f, 0x7c, 0x28, 0x41, 0x27, 0x94, 0x91, 0x1a, 0x95, 0x26,
+ 0x7c, 0xb7, 0x67, 0x45, 0x6b, 0x53, 0x8c, 0x15, 0x7e, 0x53, 0x65, 0x7d,
+ 0xa5, 0x15, 0x35, 0x2b, 0x9b, 0x5c, 0x5f, 0x78, 0x2a, 0xca, 0x59, 0x73,
+ 0xb9, 0x4f, 0x85, 0xa8, 0x3a, 0xe9, 0x71, 0x8b, 0xd2, 0x55, 0x29, 0x35,
+ 0x8f, 0xdd, 0x46, 0xa8, 0x4e, 0x7f, 0xac, 0x67, 0x62, 0xec, 0x80, 0x48,
+ 0x83, 0x20, 0xae, 0x24, 0x20, 0xac, 0x11, 0xec, 0x84, 0x86, 0x08, 0xdc,
+ 0xba, 0xbb, 0xf9, 0xfe, 0xd0, 0x28, 0x28, 0x82, 0x1a, 0x4a, 0x7e, 0x2c,
+ 0x00, 0x3c, 0xf7, 0xfc, 0x40, 0x29, 0xdd, 0x4d, 0xa5, 0x76, 0x70, 0x1e,
+ 0x8e, 0x69, 0xe7, 0x81, 0x6a, 0xa0, 0xdf, 0xa4, 0x2b, 0x0f, 0x20, 0x06,
+ 0x52, 0xb7, 0x56, 0xcc, 0x2c, 0xe2, 0x34, 0x84, 0x50, 0x78, 0xfb, 0x83,
+ 0xb4, 0xb6, 0x50, 0x2e, 0x25, 0x52, 0xaa, 0xab, 0xf3, 0x4f, 0x44, 0x5b,
+ 0x06, 0xa1, 0x35, 0x9e, 0x1c, 0xf0, 0x85, 0x51, 0x88, 0xb0, 0x89, 0x04,
+ 0xdc, 0xac, 0x5f, 0x00, 0x3e, 0x2e, 0xf9, 0x8a, 0x0e, 0xc7, 0x5a, 0x02,
+ 0x95, 0xcd, 0x48, 0x28, 0x54, 0x99, 0xd3, 0x95, 0x2e, 0xe3, 0x70, 0x27,
+ 0x57, 0xad, 0x2c, 0x43, 0xf1, 0xc3, 0xe0, 0x04, 0xac, 0x81, 0x7d, 0x57,
+ 0xf7, 0x2c, 0xaf, 0x14, 0xc5, 0xf8, 0xba, 0x7e, 0x3a, 0x47, 0x33, 0x4d,
+ 0x0f, 0x91, 0x5c, 0x64, 0xd4, 0x7c, 0x81, 0x12, 0x5a, 0x05, 0x67, 0xe3,
+ 0x7b, 0x91, 0x46, 0xe4, 0x6a, 0xac, 0x73, 0xbe, 0x98, 0x08, 0x13, 0x71,
+ 0xf3, 0xaa, 0x0c, 0xcd, 0x1a, 0x26, 0xfd, 0x87, 0x0b, 0x37, 0x93, 0xc8,
+ 0x77, 0xae, 0x8a, 0x58, 0x69, 0xa6, 0x2e, 0xc6, 0xfd, 0x50, 0xd6, 0xcb,
+ 0x54, 0x74, 0xda, 0xfa, 0xa1, 0x92, 0xd0, 0x1c, 0x9f, 0x29, 0x46, 0x45,
+ 0x92, 0xdc, 0xd8, 0x1d, 0xad, 0x2d, 0xa0, 0x3c, 0xce, 0xd3, 0x2f, 0xe0,
+ 0xd5, 0x99, 0xc8, 0xe9, 0xcf, 0x00, 0x46, 0x7c, 0x80, 0x1d, 0x4f, 0x36,
+ 0xb1, 0x62, 0x7c, 0x9b, 0xc0, 0x15, 0xcb, 0x62, 0x17, 0x87, 0xf9, 0xcb,
+ 0x19, 0x6f, 0xfd, 0xb4, 0xb8, 0xb2, 0x40, 0x23, 0xdf, 0x68, 0x39, 0x07,
+ 0xc8, 0x11, 0xee, 0xcb, 0x29, 0xff, 0x22, 0x34, 0x23, 0x8b, 0x9e, 0x5b,
+ 0xc6, 0x28, 0x78, 0xc1, 0x2c, 0x42, 0xdf, 0x8a, 0x7f, 0x57, 0x14, 0x4b,
+ 0x88, 0x47, 0xe1, 0xfe, 0x94, 0x83, 0x18, 0x34, 0x2e, 0xc6, 0x6f, 0x91,
+ 0x32, 0xdf, 0x6d, 0x49, 0xe1, 0x32, 0x2d, 0x82, 0x08, 0xea, 0x42, 0x39,
+ 0x08, 0xe4, 0xef, 0x49, 0x6f, 0xe4, 0xb1, 0x3d, 0x76, 0x3f, 0x02, 0x57,
+ 0x24, 0x50, 0xb0, 0x1d, 0xcf, 0x4f, 0x2d, 0x19, 0x34, 0xb0, 0x15, 0x2e,
+ 0x01, 0xde, 0x58, 0x58, 0xe0, 0x44, 0x4d, 0x2c, 0x35, 0xbc, 0xcc, 0x7f,
+ 0xd5, 0x24, 0x04, 0x60, 0xa1, 0x94, 0x13, 0x87, 0xad, 0xd7, 0xbc, 0xd7,
+ 0x3c, 0xc3, 0x53, 0x68, 0x14, 0xae, 0xce, 0xa4, 0x60, 0x52, 0x3e, 0x1f,
+ 0xe4, 0xda, 0x02, 0x3b, 0x5b, 0x39, 0x95, 0x36, 0xfb, 0x8b, 0x6d, 0xbd,
+ 0xe2, 0x1b, 0xff, 0xce, 0x87, 0xb8, 0x44, 0xaa, 0xe0, 0x23, 0x0f, 0x40,
+ 0x6b, 0x4c, 0x58, 0xbf, 0xa0, 0x87, 0xbc, 0x19, 0x03, 0x2e, 0x58, 0x6d,
+ 0x1f, 0x05, 0xe8, 0x65, 0xb8, 0x88, 0xc9, 0x8b, 0x88, 0xae, 0xd3, 0x4b,
+ 0xc3, 0x23, 0x0c, 0x7f, 0x05, 0x4f, 0x93, 0x17, 0x84, 0x7c, 0x03, 0xcc,
+ 0xed, 0xb7, 0x60, 0xf6, 0xd4, 0xd6, 0x8b, 0x91, 0x7e, 0xb8, 0x14, 0x2c,
+ 0xb9, 0x8c, 0xf6, 0x0b, 0xcd, 0xbb, 0x02, 0xde, 0xc3, 0x8d, 0x28, 0xff,
+ 0xb6, 0x53, 0x56, 0x03, 0xce, 0xb0, 0xe1, 0xa5, 0x92, 0xf2, 0x9c, 0x2d,
+ 0x3f, 0x6a, 0x26, 0x72, 0xe2, 0xf7, 0x55, 0x16, 0x3e, 0x1b, 0x42, 0x44,
+ 0xad, 0x17, 0x09, 0xa7, 0x29, 0x25, 0x1f, 0x04, 0x80, 0xba, 0x3a, 0x29,
+ 0xc3, 0xf7, 0xe8, 0x7c, 0xf7, 0xf4, 0x32, 0x5b, 0x3a, 0xa2, 0xbd, 0xfd,
+ 0x85, 0xe6, 0xf3, 0x0d, 0x40, 0xed, 0xeb, 0x99, 0x73, 0x31, 0x82, 0x27,
+ 0x96, 0xea, 0x8f, 0x73, 0xa7, 0x77, 0x1f, 0x46, 0xe9, 0xe2, 0xff, 0x50,
+ 0xe0, 0xca, 0xb2, 0xa7, 0x63, 0xeb, 0xc7, 0x6c, 0x8f, 0x81, 0x05, 0x91,
+ 0xbe, 0x0a, 0x0c, 0x22, 0x34, 0xf6, 0xd5, 0x9d, 0x53, 0xe1, 0x8e, 0x22,
+ 0x04, 0xa7, 0x12, 0x9e, 0xc1, 0xef, 0xb6, 0x17, 0xb1, 0x18, 0xae, 0x0f,
+ 0x87, 0xe3, 0x96, 0x76, 0x72, 0xc6, 0xef, 0x0b, 0xbc, 0xce, 0xaf, 0x58,
+ 0x99, 0x9b, 0x72, 0xa2, 0xc1, 0x24, 0x91, 0x5c, 0xc7, 0xb7, 0x4a, 0x0f,
+ 0x39, 0x05, 0x5a, 0xf4, 0x66, 0x8c, 0x95, 0xd1, 0x5b, 0xff, 0x74, 0x16,
+ 0x9f, 0xa0, 0xa3, 0x2c, 0xfb, 0x85, 0x80, 0xe5, 0xf0, 0x33, 0x17, 0x38,
+ 0x01, 0x8b, 0x5f, 0x8b, 0xcd, 0x58, 0x22, 0x3e, 0xcf, 0x09, 0x1e, 0x76,
+ 0x3a, 0x0c, 0x7a, 0xc4, 0xf2, 0xff, 0x7c, 0x14, 0xca, 0x36, 0x6c, 0x26,
+ 0x39, 0xde, 0x23, 0x0d, 0xd3, 0xfa, 0x5c, 0x1a, 0xff, 0xe6, 0x98, 0xc5,
+ 0x86, 0x4c, 0xda, 0xb1, 0x6c, 0x27, 0x87, 0x54, 0x3b, 0xb7, 0x74, 0xb3,
+ 0x71, 0x3d, 0x17, 0x56, 0x11, 0xe7, 0xf4, 0x92, 0x6e, 0x2d, 0xe1, 0x05,
+ 0x53, 0xd9, 0x5f, 0x41, 0x36, 0xed, 0x0c, 0x10, 0xa4, 0xfa, 0xd8, 0x7e,
+ 0xb0, 0xce, 0x35, 0xd1, 0xf5, 0x14, 0x7e, 0x50, 0xed, 0x79, 0x44, 0x7e,
+ 0x1d, 0xde, 0xd3, 0xd8, 0xdf, 0xdf, 0xcf, 0x24, 0xf9, 0xee, 0xae, 0xfc,
+ 0x80, 0xa0, 0x59, 0x87, 0x67, 0x63, 0x69, 0x79, 0x4a, 0x70, 0x92, 0x3c,
+ 0x2d, 0xba, 0x38, 0xac, 0x5e, 0xff, 0xe9, 0x1a, 0x35, 0x92, 0x5d, 0x62,
+ 0x3d, 0xd9, 0xdc, 0x36, 0xaf, 0x85, 0x39, 0x35, 0xe4, 0x89, 0xa2, 0x78,
+ 0xfe, 0xb7, 0xf7, 0x7c, 0xa8, 0x95, 0xcf, 0xfe, 0xfe, 0x5d, 0xd0, 0x31,
+ 0x1d, 0x9d, 0xe0, 0xa3, 0x35, 0x63, 0xd5, 0x4a, 0xd5, 0xba, 0xa2, 0x99,
+ 0xba, 0x2a, 0xf1, 0xd6, 0x34, 0x46, 0x60, 0x43, 0x3a, 0xab, 0xbf, 0x41,
+ 0xe5, 0x6b, 0xbb, 0xb8, 0x0f, 0x04, 0xfc, 0x34, 0x55, 0x8d, 0x57, 0x93,
+ 0x0e, 0x3e, 0x27, 0xfb, 0x3d, 0x9f, 0x65, 0x97, 0xd8, 0x70, 0x93, 0xc2,
+ 0xa2, 0x15, 0x6c, 0x17, 0x66, 0xf7, 0xf6, 0x56, 0x4f, 0x21, 0x14, 0x5f,
+ 0x2f, 0x8d, 0x5b, 0xb2, 0xe5, 0x4f, 0xac, 0xc8, 0x02, 0x26, 0x20, 0xc5,
+ 0xa7, 0x52, 0xfb, 0x74, 0xc0, 0xcc, 0xcc, 0x3f, 0x36, 0x59, 0x87, 0x79,
+ 0x91, 0x05, 0x45, 0x59, 0x31, 0xff, 0xa8, 0x94, 0x7a, 0x19, 0xa2, 0xa5,
+ 0xfc, 0x5c, 0x4e, 0xbf, 0xfa, 0x35, 0x5b, 0x16, 0x4e, 0xfc, 0x3d, 0xcd,
+ 0x5d, 0x83, 0x31, 0x67, 0x58, 0x8b, 0xb7, 0x51, 0xfc, 0xb7, 0x07, 0x00,
+ 0xe8, 0xf9, 0xde, 0x60, 0x19, 0xab, 0xa3, 0x57, 0x3a, 0xaf, 0x62, 0x21,
+ 0x02, 0x58, 0xfd, 0xa0, 0x4a, 0x67, 0x71, 0xe2, 0x9f, 0x73, 0xe8, 0x8d,
+ 0x5a, 0x85, 0x2b, 0xdf, 0x38, 0x06, 0x9c, 0xb7, 0x86, 0x22, 0xec, 0xc2,
+ 0xd0, 0xc3, 0x92, 0xe3, 0xbe, 0x20, 0x69, 0xc0, 0xd8, 0x80, 0xfb, 0xfa,
+ 0xda, 0xb3, 0x4e, 0x4d, 0xda, 0xda, 0x7b, 0x0a, 0x5e, 0xa1, 0xac, 0xf6,
+ 0xb6, 0x94, 0xd6, 0x9b, 0x70, 0xdc, 0x91, 0x83, 0x77, 0xa9, 0x3f, 0xbf,
+ 0x27, 0x6b, 0xdd, 0x31, 0xd1, 0xad, 0x5b, 0xdb, 0x59, 0x06, 0xce, 0xb9,
+ 0x3e, 0x24, 0x3a, 0xd2, 0x6e, 0x1f, 0x3a, 0x31, 0xa6, 0x36, 0x59, 0x8e,
+ 0x4b, 0x3a, 0x2e, 0x3d, 0x6f, 0x28, 0xa8, 0xe6, 0xcd, 0x84, 0x74, 0x63,
+ 0xd1, 0xdd, 0x81, 0x7d, 0xbb, 0xad, 0x30, 0x01, 0x2b, 0x1b, 0xcf, 0x15,
+ 0x39, 0x28, 0xcb, 0x75, 0x55, 0xe7, 0x41, 0x08, 0xfa, 0x7e, 0x64, 0x6b,
+ 0xf7, 0x66, 0x51, 0xb4, 0xe2, 0x93, 0x8a, 0x10, 0xa5, 0x13, 0x0a, 0x3b,
+ 0x9e, 0xdd, 0xb1, 0x79, 0xf3, 0x43, 0x8d, 0xc8, 0x9b, 0x4e, 0xc5, 0x51,
+ 0x5c, 0x8d, 0xa7, 0x91, 0xdf, 0x39, 0x5a, 0x7b, 0x72, 0x53, 0xf9, 0x6c,
+ 0xa6, 0x89, 0x41, 0x81, 0xb5, 0xcf, 0xfb, 0x71, 0x08, 0x64, 0xc8, 0x77,
+ 0x3e, 0x5a, 0x84, 0x18, 0xb6, 0x5f, 0x42, 0xce, 0xb4, 0x46, 0xc9, 0x61,
+ 0x92, 0x13, 0x4a, 0xc9, 0xb0, 0xa6, 0x10, 0xaf, 0x4f, 0xdc, 0x00, 0x21,
+ 0xfa, 0x74, 0x9a, 0xdf, 0x21, 0x93, 0xe0, 0x5d, 0x1a, 0x3b, 0xa3, 0x43,
+ 0x2a, 0x67, 0x97, 0xfc, 0x16, 0xde, 0x4c, 0x69, 0xa3, 0xa5, 0x28, 0x74,
+ 0xd1, 0xcf, 0xa5, 0x1c, 0xff, 0x8e, 0xea, 0xfa, 0x16, 0x4a, 0xa0, 0xe5,
+ 0xb3, 0x42, 0x53, 0x78, 0xf4, 0xc0, 0xaa, 0x12, 0x52, 0xcb, 0x1e, 0x65,
+ 0x2d, 0x12, 0xa8, 0x92, 0x7e, 0xd4, 0x9f, 0x70, 0xb9, 0x28, 0x27, 0x69,
+ 0x12, 0xc2, 0x6b, 0xb6, 0x0e, 0xb4, 0x08, 0x82, 0x4e, 0xae, 0xf3, 0xba,
+ 0x3b, 0xea, 0xdb, 0xff, 0x90, 0xda, 0x58, 0xde, 0xac, 0xdd, 0xe6, 0xc5,
+ 0x9f, 0x1c, 0xdf, 0x4e, 0x8e, 0xa2, 0xf9, 0xef, 0x00, 0x5f, 0x63, 0x58,
+ 0x74, 0x8c, 0x58, 0x63, 0xe6, 0x74, 0xd7, 0x72, 0x59, 0x75, 0xcf, 0x21,
+ 0x3b, 0xbb, 0x5d, 0x4a, 0x19, 0x89, 0xb9, 0xd9, 0x16, 0x18, 0x20, 0x3d,
+ 0x1e, 0x68, 0xc5, 0xeb, 0x04, 0x7d, 0x3c, 0x3c, 0xa8, 0xc9, 0x35, 0x3d,
+ 0xda, 0x25, 0x0c, 0x73, 0xb9, 0x48, 0xf5, 0x43, 0x40, 0xa8, 0xff, 0x9a,
+ 0x1e, 0x88, 0xf1, 0x9d, 0x3f, 0xbf, 0x1a, 0xb2, 0xde, 0xec, 0x62, 0x75,
+ 0x36, 0xcf, 0xc6, 0x6e, 0x61, 0x1b, 0x39, 0x8b, 0xec, 0x22, 0x12, 0xec,
+ 0xf2, 0x05, 0x39, 0x45, 0x69, 0xf1, 0x65, 0x05, 0x35, 0x4e, 0xe4, 0x3b,
+ 0x9d, 0x6d, 0xb2, 0xf9, 0x9a, 0x6d, 0x5d, 0x06, 0x57, 0xc4, 0x6e, 0x84,
+ 0x55, 0xab, 0xf4, 0x83, 0xb4, 0x5b, 0xa1, 0x9f, 0x32, 0x4c, 0xc8, 0x84,
+ 0x07, 0xa6, 0x31, 0xa2, 0x26, 0xf3, 0x75, 0x49, 0x22, 0x77, 0xb9, 0xdc,
+ 0xb8, 0x6a, 0x79, 0x7e, 0x08, 0x9a, 0x45, 0xfd, 0x03, 0x40, 0x14, 0x4b,
+ 0x98, 0xff, 0x02, 0x0e, 0xf3, 0x78, 0x51, 0x94, 0x1e, 0x05, 0x45, 0xdb,
+ 0x91, 0x1e, 0xf5, 0x66, 0xc3, 0x70, 0xbd, 0x44, 0x55, 0xce, 0xd9, 0xec,
+ 0x98, 0x15, 0x45, 0x89, 0x12, 0x2e, 0x2c, 0x39, 0x8d, 0x94, 0x17, 0xf4,
+ 0x54, 0xaa, 0x91, 0xee, 0xef, 0x50, 0xb2, 0xde, 0xa0, 0xe6, 0xa8, 0xa0,
+ 0xf2, 0x4c, 0x46, 0x45, 0x48, 0x1d, 0xa9, 0x8c, 0xfc, 0x48, 0xd4, 0xc9,
+ 0x45, 0x65, 0x4d, 0x74, 0x57, 0x61, 0x3f, 0x22, 0xaf, 0x32, 0x60, 0x5b,
+ 0xd7, 0xb0, 0x0b, 0xbe, 0x26, 0xcc, 0x51, 0x52, 0x2c, 0x41, 0x2e, 0xc8,
+ 0xd0, 0xf1, 0x56, 0xf5, 0xf5, 0x7a, 0xa4, 0x4e, 0xb7, 0x69, 0xc2, 0x1b,
+ 0x71, 0x91, 0xe4, 0x52, 0xb5, 0xd2, 0x4c, 0xca, 0x29, 0xb9, 0x27, 0x17,
+ 0x97, 0x77, 0x72, 0x3a, 0x14, 0x30, 0x16, 0x88, 0x80, 0x86, 0x5a, 0xaa,
+ 0xc8, 0x99, 0x28, 0x55, 0x17, 0x63, 0xdf, 0x98, 0xfa, 0xe5, 0xc6, 0xa3,
+ 0x97, 0x40, 0x7a, 0x3c, 0x39, 0x13, 0x00, 0x27, 0xd6, 0x1d, 0x3a, 0x6b,
+ 0x47, 0xa8, 0x3e, 0xde, 0x25, 0x77, 0x1a, 0x3a, 0xd1, 0x66, 0x7f, 0x18,
+ 0x63, 0x0b, 0x96, 0xab, 0x33, 0x79, 0xc7, 0x61, 0xac, 0xea, 0x1f, 0x3c,
+ 0x95, 0xfc, 0x8d, 0xc7, 0x54, 0x2e, 0x9e, 0x77, 0xa3, 0x74, 0xbb, 0x84,
+ 0x40, 0xc9, 0x80, 0x4d, 0x8b, 0xe1, 0xa7, 0x75, 0x53, 0x7c, 0x88, 0x48,
+ 0x35, 0x4e, 0x64, 0xfc, 0xe2, 0x50, 0x0b, 0x7c, 0xc5, 0x03, 0xef, 0xf5,
+ 0xd9, 0x0a, 0x63, 0xc2, 0xc4, 0xed, 0xf1, 0x29, 0x3f, 0x87, 0x53, 0x11,
+ 0x81, 0x80, 0x2c, 0x3c, 0xcc, 0x69, 0x55, 0x11, 0x93, 0x5f, 0xe1, 0xe9,
+ 0xd9, 0xc4, 0x05, 0x95, 0x6a, 0x6f, 0xdd, 0xb0, 0x6a, 0xfc, 0x90, 0x3a,
+ 0xf7, 0x0a, 0x05, 0x64, 0xd3, 0x7d, 0xe4, 0xf3, 0x53, 0x3b, 0x72, 0x21,
+ 0xfe, 0x14, 0xd8, 0xff, 0x97, 0x93, 0xd4, 0x2b, 0x91, 0x2a, 0x30, 0x72,
+ 0x9f, 0x2b, 0xab, 0x5b, 0xe7, 0x81, 0xce, 0xce, 0x39, 0xc8, 0x5c, 0xd3,
+ 0xa7, 0xde, 0x05, 0xa5, 0xc7, 0xba, 0xe8, 0xa5, 0xca, 0xe6, 0xb4, 0xce,
+ 0x82, 0x2e, 0xd7, 0x83, 0xc2, 0x37, 0x01, 0x2d, 0x3b, 0x27, 0x64, 0x12,
+ 0x87, 0xc9, 0xf5, 0x4f, 0xa3, 0xbc, 0x08, 0x74, 0x9c, 0x1d, 0x9e, 0xb3,
+ 0xef, 0x12, 0xdc, 0x84, 0xe0, 0x4e, 0x62, 0xd2, 0xb4, 0x8e, 0xb1, 0x37,
+ 0x3c, 0x65, 0xc7, 0x7b, 0xc4, 0x98, 0x58, 0xff, 0xe4, 0xeb, 0x2f, 0x43,
+ 0x5f, 0x94, 0xff, 0x6e, 0x69, 0x0a, 0x17, 0x24, 0x82, 0x42, 0xae, 0xb2,
+ 0x80, 0xca, 0x86, 0xa9, 0xe2, 0x9d, 0x23, 0x3e, 0x07, 0x1a, 0xc6, 0x6b,
+ 0xe0, 0x96, 0xb8, 0xcd, 0x11, 0x63, 0x67, 0x12, 0xc9, 0xdf, 0x3e, 0xeb,
+ 0x2f, 0x45, 0xea, 0x04, 0x0d, 0xfd, 0x94, 0xbc, 0x15, 0xcb, 0xfc, 0x55,
+ 0xfc, 0x7d, 0x1a, 0x6d, 0xbc, 0x93, 0x41, 0xb6, 0x89, 0x4e, 0xa6, 0xfc,
+ 0x53, 0xd0, 0x72, 0xfa, 0xa0, 0xa2, 0xe7, 0x9f, 0xbe, 0xbe, 0xce, 0x56,
+ 0x24, 0x76, 0xd0, 0xfb, 0x8e, 0x21, 0xfe, 0x23, 0xb2, 0xbb, 0x79, 0xa8,
+ 0x83, 0x3b, 0x5c, 0xb2, 0x41, 0x85, 0x68, 0xb8, 0xdf, 0x7d, 0x17, 0xf8,
+ 0xb0, 0x64, 0xce, 0xbd, 0x75, 0xbd, 0x6e, 0xe1, 0xdf, 0xe6, 0x67, 0xcc,
+ 0xc1, 0x2b, 0x8c, 0x01, 0xdf, 0xc4, 0xd3, 0x53, 0xc2, 0x6c, 0x57, 0x16,
+ 0xc3, 0x52, 0x56, 0x18, 0xcc, 0x97, 0xfc, 0x1b, 0x2f, 0x86, 0x56, 0x0d,
+ 0x12, 0x86, 0x8d, 0x4f, 0x95, 0x88, 0x40, 0xaa, 0xa6, 0x51, 0xfa, 0x97,
+ 0x22, 0xb9, 0x6b, 0x13, 0x32, 0xfc, 0x6a, 0x20, 0x88, 0xbc, 0x28, 0xa5,
+ 0x34, 0x6e, 0x5c, 0x5a, 0x1e, 0x38, 0xbf, 0x19, 0x47, 0xfb, 0x44, 0x5e,
+ 0xba, 0x3f, 0x16, 0x06, 0x2a, 0x58, 0x30, 0xe7, 0x93, 0x49, 0xc9, 0x09,
+ 0xa0, 0x34, 0xb3, 0x1e, 0x9d, 0x15, 0x5e, 0xdb, 0x76, 0xfb, 0xa8, 0x2b,
+ 0x9a, 0x09, 0xd4, 0x89, 0x2e, 0x4b, 0x5d, 0x01, 0xa4, 0x45, 0xd4, 0xcc,
+ 0xd1, 0xb1, 0xb4, 0xa7, 0xb3, 0x54, 0xe9, 0x8a, 0xc2, 0x28, 0x8c, 0xcb,
+ 0x95, 0xa0, 0x23, 0x11, 0x02, 0x94, 0x7d, 0xfc, 0x3e, 0xa3, 0x23, 0x9c,
+ 0xde, 0xf1, 0x92, 0x8b, 0x3a, 0x1e, 0x8f, 0x0a, 0xbd, 0x93, 0x1a, 0x08,
+ 0x03, 0xad, 0x2d, 0x6f, 0x83, 0x5b, 0x32, 0x28, 0xff, 0x51, 0x58, 0x4f,
+ 0x1b, 0x64, 0xaa, 0x4e, 0x3c, 0x3d, 0x44, 0x04, 0x3a, 0x01, 0x3b, 0xfb,
+ 0x07, 0xa5, 0xc9, 0x1b, 0x07, 0x0f, 0xd3, 0x9f, 0x5e, 0xfd, 0x3d, 0x71,
+ 0x94, 0x54, 0x19, 0x85, 0x87, 0x88, 0x15, 0x08, 0x50, 0xdc, 0x26, 0xbd,
+ 0x22, 0x36, 0x14, 0x9f, 0xc1, 0x6d, 0x12, 0xf3, 0x2b, 0x18, 0x7b, 0xc9,
+ 0xf1, 0xe2, 0x25, 0xe6, 0xa7, 0x44, 0xa9, 0x9c, 0x37, 0x15, 0x11, 0xa2,
+ 0xbb, 0xa3, 0xc3, 0xb4, 0x7b, 0x09, 0x0a, 0x31, 0xed, 0x37, 0x98, 0xd3,
+ 0x7e, 0x7a, 0xcb, 0x0a, 0xc8, 0xed, 0x13, 0x05, 0x1e, 0x14, 0x9a, 0x3b,
+ 0xc4, 0xd2, 0x9d, 0x35, 0x60, 0xb7, 0x6f, 0x0b, 0x3d, 0x1e, 0xef, 0x34,
+ 0xcd, 0x40, 0x24, 0x7c, 0xf4, 0x2f, 0x99, 0xd2, 0x4f, 0x9f, 0xf6, 0x11,
+ 0xb5, 0x9e, 0xb8, 0x64, 0x55, 0xbf, 0xc1, 0x78, 0x3e, 0xe9, 0xbe, 0x15,
+ 0xf5, 0x1a, 0x86, 0xa0, 0x8e, 0x16, 0xda, 0x5f, 0x94, 0x9e, 0x2a, 0xdf,
+ 0x50, 0x97, 0x42, 0x30, 0x8c, 0xe3, 0x01, 0xe0, 0x95, 0x51, 0xa8, 0x54,
+ 0xd5, 0xee, 0xf0, 0x1a, 0xa4, 0x0d, 0x63, 0x8d, 0xdf, 0x32, 0xa2, 0x96,
+ 0x82, 0x94, 0xbf, 0xe7, 0xab, 0xb0, 0x0e, 0xec, 0x51, 0xa8, 0x9b, 0x53,
+ 0x25, 0x8e, 0x15, 0x78, 0xb2, 0xd2, 0xf7, 0x9c, 0x86, 0xbb, 0x9a, 0x9e,
+ 0xa6, 0xc8, 0x4b, 0x02, 0x47, 0x00, 0xfd, 0x1a, 0x0b, 0x40, 0x87, 0x84,
+ 0x9b, 0xf6, 0xaf, 0x22, 0x08, 0x3d, 0x89, 0x4b, 0xe8, 0x5f, 0x66, 0x06,
+ 0x62, 0xe3, 0x29, 0xbd, 0xb5, 0x39, 0xa7, 0x17, 0x7f, 0xb5, 0x8b, 0xe5,
+ 0x57, 0x6e, 0xbb, 0xa9, 0x3c, 0x41, 0x9b, 0x64, 0xf1, 0x44, 0x32, 0xb2,
+ 0x14, 0xd7, 0x4d, 0x9b, 0x03, 0x5c, 0x39, 0x3a, 0x72, 0x8f, 0x04, 0xd2,
+ 0x02, 0xcd, 0x9f, 0xd9, 0xf8, 0x67, 0xd2, 0x20, 0xc1, 0xe2, 0x15, 0x33,
+ 0x75, 0xe7, 0xd3, 0xa1, 0x65, 0xe7, 0xea, 0xf8, 0x28, 0xb8, 0xc5, 0x56,
+ 0xca, 0x82, 0xd8, 0x11, 0x1d, 0x3c, 0x98, 0x60, 0xd8, 0xe7, 0xe2, 0x7d,
+ 0x3d, 0xd4, 0x5e, 0x38, 0x6f, 0x6e, 0x83, 0x40, 0x6a, 0x2a, 0x72, 0x52,
+ 0xee, 0x05, 0x23, 0xee, 0x13, 0x6d, 0xfb, 0x81, 0x17, 0xda, 0xf3, 0x0c,
+ 0xab, 0xb4, 0x41, 0xca, 0x57, 0xe9, 0x3b, 0x26, 0x0e, 0x1d, 0x88, 0xe7,
+ 0xff, 0xc6, 0xee, 0xd0, 0x32, 0x78, 0x11, 0xb3, 0x93, 0xfc, 0xf6, 0x80,
+ 0x8c, 0xcf, 0xf6, 0x22, 0x1b, 0x9d, 0x7d, 0x45, 0x62, 0x16, 0xf7, 0x4b,
+ 0x20, 0xd1, 0x68, 0xcd, 0xe5, 0xe0, 0xb2, 0x15, 0x11, 0xa3, 0xac, 0x0b,
+ 0x06, 0x7b, 0x2a, 0x0a, 0xa8, 0x42, 0xdd, 0x93, 0xa3, 0xf9, 0xc5, 0xa8,
+ 0xb4, 0x2e, 0x4a, 0x2d, 0xa8, 0x2b, 0xbc, 0x44, 0x44, 0xe6, 0xae, 0xe4,
+ 0xb4, 0xb2, 0x85, 0x76, 0x89, 0xf1, 0xff, 0x55, 0x5b, 0xe9, 0x36, 0xdc,
+ 0xb6, 0xa6, 0xc6, 0x0c, 0xa0, 0x27, 0x7e, 0x58, 0x74, 0xb6, 0xcd, 0x45,
+ 0xd6, 0x88, 0x2a, 0x4c, 0xd5, 0x10, 0x99, 0xeb, 0x0b, 0x8e, 0x8a, 0x4a,
+ 0xd7, 0x93, 0x6b, 0xc3, 0xde, 0x07, 0xfc, 0x8d, 0xa8, 0xcf, 0x09, 0x51,
+ 0x8d, 0xf2, 0xba, 0x26, 0x51, 0xd0, 0xba, 0xdb, 0xbb, 0x74, 0xb4, 0x9f,
+ 0x09, 0xfb, 0x85, 0x53, 0xb5, 0x72, 0xfb, 0xa7, 0x44, 0xe3, 0x95, 0x83,
+ 0x65, 0x25, 0x53, 0x87, 0x5e, 0xf1, 0xcb, 0xf6, 0xd1, 0x03, 0x71, 0xed,
+ 0x97, 0x20, 0x32, 0x77, 0x06, 0x86, 0x1b, 0x78, 0x17, 0xa0, 0x2c, 0x93,
+ 0x3c, 0xb3, 0xc5, 0xc5, 0x9b, 0x9e, 0xb8, 0x18, 0x82, 0x62, 0x35, 0x8f,
+ 0x9d, 0x13, 0x58, 0x21, 0xd5, 0xf8, 0x51, 0x43, 0x12, 0xa8, 0x26, 0xf5,
+ 0x52, 0x3c, 0x2f, 0x26, 0x5a, 0x7e, 0x21, 0x11, 0x2d, 0x29, 0x5e, 0xe8,
+ 0x67, 0x41, 0x41, 0xc9, 0x0a, 0xda, 0xe1, 0xab, 0x11, 0xcd, 0xd4, 0x8e,
+ 0x8c, 0x47, 0x49, 0x1a, 0x4b, 0x2c, 0xc7, 0x1f, 0xab, 0x2d, 0x44, 0xb0,
+ 0xa4, 0x3a, 0xd7, 0xe8, 0x0b, 0x35, 0xfc, 0x8f, 0xe0, 0x44, 0x36, 0xc8,
+ 0xe2, 0x83, 0x81, 0xbf, 0xa5, 0x38, 0x91, 0x00, 0x05, 0x85, 0xab, 0x66,
+ 0xa2, 0x92, 0xa5, 0x6d, 0xda, 0xa6, 0x11, 0xa3, 0x24, 0x8a, 0xd8, 0x4d,
+ 0x0b, 0x0a, 0x8f, 0x5b, 0x17, 0xbe, 0xe5, 0x13, 0xe7, 0xd6, 0x9b, 0xc2,
+ 0x2c, 0x32, 0xba, 0x50, 0x6a, 0xf3, 0x63, 0x7f, 0x54, 0x64, 0x27, 0x3f,
+ 0xfb, 0x6f, 0x67, 0x0a, 0x23, 0xd6, 0x66, 0xbf, 0xf3, 0x9c, 0x58, 0x25,
+ 0x33, 0xe2, 0x1c, 0x46, 0x7e, 0x0b, 0x10, 0x5d, 0xd5, 0x5e, 0x37, 0x70,
+ 0x96, 0x11, 0x3b, 0x44, 0x9e, 0x68, 0x13, 0x53, 0x21, 0x2a, 0x60, 0x63,
+ 0x17, 0xe5, 0xc5, 0x8c, 0x3e, 0xda, 0x81, 0x26, 0xb0, 0x34, 0xff, 0x93,
+ 0xaf, 0xfc, 0x5c, 0x93, 0xbd, 0x9f, 0x82, 0xf0, 0x66, 0x59, 0xb2, 0x28,
+ 0xaa, 0xd3, 0xc5, 0xfc, 0x12, 0x5f, 0x13, 0x89, 0x8d, 0x49, 0x07, 0x2d,
+ 0x7b, 0x4b, 0xc9, 0x69, 0xf5, 0xaa, 0xbf, 0x94, 0xb5, 0x1f, 0xba, 0x36,
+ 0xa4, 0x49, 0x3e, 0x50, 0xed, 0x92, 0x57, 0x66, 0x32, 0xeb, 0x06, 0xb0,
+ 0xb3, 0x5f, 0xa6, 0xcd, 0x75, 0x92, 0xa0, 0x40, 0x13, 0xe2, 0x49, 0x54,
+ 0x8c, 0x92, 0x78, 0x1f, 0x2d, 0x1b, 0x9d, 0x67, 0xec, 0x87, 0x7e, 0x94,
+ 0xb0, 0x7f, 0x17, 0xfc, 0xc2, 0xec, 0x1d, 0xd3, 0x1a, 0xd7, 0x95, 0xc1,
+ 0x97, 0x5f, 0xc2, 0xd3, 0xf5, 0x13, 0xa8, 0x0b, 0xfe, 0xa6, 0x0a, 0xf5,
+ 0xe3, 0xe0, 0xd3, 0x1d, 0x3c, 0x73, 0x07, 0xbc, 0xb8, 0xef, 0x91, 0x6f,
+ 0x78, 0x79, 0xb0, 0x29, 0xcf, 0xc1, 0xc7, 0x84, 0x49, 0x26, 0x2f, 0x12,
+ 0xb5, 0x44, 0x65, 0xb9, 0xec, 0x6a, 0x65, 0x1d, 0x00, 0x45, 0xb4, 0x96,
+ 0x6b, 0xe4, 0x88, 0x81, 0x5a, 0x4d, 0x02, 0x5b, 0x3d, 0xfb, 0x55, 0x38,
+ 0xb5, 0x77, 0x62, 0x6b, 0xbf, 0x43, 0x4f, 0x73, 0x64, 0xce, 0x72, 0x4c,
+ 0xbf, 0x19, 0xab, 0x9a, 0xf0, 0x91, 0x93, 0xc1, 0x81, 0xf5, 0xd2, 0xd6,
+ 0x4f, 0xf5, 0x11, 0xbb, 0x4e, 0xc0, 0x42, 0x06, 0x75, 0xd1, 0x0a, 0x9b,
+ 0xe6, 0xef, 0x26, 0xbc, 0xc4, 0x7f, 0x9c, 0x53, 0xe6, 0x6c, 0x44, 0xa3,
+ 0xe9, 0xce, 0x79, 0x7f, 0xca, 0xb0, 0xef, 0xea, 0xd7, 0xd2, 0x2e, 0xea,
+ 0xa2, 0x46, 0x2a, 0xf6, 0xe1, 0xda, 0x29, 0x5b, 0xdc, 0x6a, 0x1b, 0xc1,
+ 0xc3, 0x25, 0xcc, 0x70, 0x64, 0x1c, 0x60, 0x61, 0xe2, 0x27, 0x9c, 0xaa,
+ 0xe4, 0x53, 0x1f, 0xfb, 0x57, 0xc7, 0xa2, 0xf7, 0x22, 0xd9, 0x07, 0x18,
+ 0xe0, 0x3d, 0x0c, 0xf6, 0xf1, 0xed, 0x72, 0xe6, 0x28, 0xf0, 0x1d, 0x96,
+ 0xfb, 0xcb, 0xe3, 0x69, 0x6e, 0x18, 0x4a, 0x86, 0x3f, 0x5b, 0xa4, 0x09,
+ 0x78, 0x50, 0x43, 0x8c, 0xba, 0xe7, 0x07, 0xb0, 0x0d, 0x32, 0xbe, 0xb9,
+ 0x2d, 0xc6, 0x48, 0x9b, 0x86, 0x29, 0xd3, 0xa5, 0x90, 0xee, 0xb3, 0x34,
+ 0x54, 0x1c, 0x5b, 0x88, 0x78, 0xe9, 0x9f, 0x25, 0x84, 0xd2, 0x4f, 0xc2,
+ 0x36, 0xdf, 0xdb, 0xf5, 0x09, 0xd3, 0x7c, 0x77, 0x33, 0x8a, 0x18, 0xf3,
+ 0x2a, 0x1c, 0xa6, 0xc6, 0x7e, 0x12, 0x3a, 0xf3, 0xb5, 0xe5, 0xba, 0x95,
+ 0x89, 0x54, 0xe0, 0x29, 0x90, 0xca, 0xdc, 0x6a, 0xcc, 0x1e, 0x7b, 0x44,
+ 0x16, 0xcb, 0x21, 0x64, 0xb0, 0xbf, 0x57, 0xbc, 0xd9, 0x5c, 0x17, 0xa8,
+ 0xbd, 0x41, 0xfa, 0xf1, 0x0d, 0x09, 0x11, 0x7d, 0x1b, 0x35, 0x6f, 0x71,
+ 0x2c, 0x40, 0xa5, 0xa6, 0x46, 0x4a, 0xe9, 0xa8, 0xbc, 0x12, 0xd9, 0xad,
+ 0x79, 0x8c, 0x92, 0xa2, 0x85, 0xee, 0x7e, 0xf9, 0xfc, 0xc3, 0xaa, 0xfa,
+ 0x7c, 0xec, 0x95, 0xc0, 0x8a, 0x92, 0xc4, 0x60, 0x97, 0x03, 0xf2, 0xe6,
+ 0xc4, 0x80, 0xf3, 0x5f, 0x4b, 0xfb, 0x52, 0xab, 0xc5, 0x2b, 0xfb, 0xdd,
+ 0x1a, 0x1e, 0x9d, 0x0a, 0x43, 0x50, 0x26, 0x5b, 0x06, 0xf7, 0xe8, 0x16,
+ 0x3d, 0xbe, 0x87, 0x2e, 0x14, 0xfe, 0xb6, 0x99, 0x29, 0x25, 0x3d, 0x8c,
+ 0x71, 0x20, 0x3e, 0xf8, 0x2d, 0xd0, 0xda, 0x6d, 0xc1, 0x2d, 0xe5, 0xfa,
+ 0xed, 0x84, 0x6b, 0x90, 0x0a, 0x8e, 0xd6, 0x88, 0x76, 0xb6, 0xbc, 0x18,
+ 0xe9, 0x9f, 0xef, 0xad, 0x15, 0xa3, 0x30, 0x0c, 0xe6, 0x29, 0x32, 0xba,
+ 0x39, 0x32, 0xbd, 0xfb, 0x38, 0xe5, 0xda, 0x60, 0x0e, 0x38, 0x90, 0x94,
+ 0x7d, 0x3c, 0x67, 0x5a, 0x7b, 0x28, 0x08, 0xec, 0x2b, 0x86, 0xe7, 0x68,
+ 0x1e, 0xa1, 0xe3, 0x8f, 0x36, 0xb9, 0x9e, 0xc7, 0x3d, 0xb7, 0x29, 0x39,
+ 0x45, 0xe7, 0xf8, 0xa0, 0x7b, 0x4e, 0x65, 0x47, 0x1e, 0xf7, 0xce, 0x2e,
+ 0x00, 0x14, 0xec, 0x8b, 0x32, 0xcb, 0x20, 0x0c, 0xc2, 0x97, 0x10, 0x3d,
+ 0x02, 0x3e, 0x86, 0x9b, 0x51, 0x69, 0xd9, 0xdc, 0xc7, 0x93, 0x74, 0x7d,
+ 0xaf, 0x61, 0x83, 0x8a, 0x78, 0xdc, 0x46, 0x2a, 0xdb, 0x07, 0x6c, 0x17,
+ 0xaa, 0x6c, 0x05, 0x44, 0xbe, 0xd9, 0xaa, 0x0c, 0x61, 0x6b, 0x3c, 0x12,
+ 0xf8, 0xb2, 0xbc, 0x67, 0x51, 0xb7, 0xc0, 0x0a, 0x49, 0xf1, 0x0a, 0x42,
+ 0x7b, 0xbd, 0xeb, 0x86, 0xc1, 0x56, 0x99, 0x40, 0x84, 0x6d, 0x8d, 0xdd,
+ 0x24, 0xc5, 0xee, 0x8d, 0xc8, 0xc9, 0xee, 0xe9, 0xba, 0x88, 0x3d, 0xcb,
+ 0xc0, 0x32, 0x6b, 0x1a, 0xe0, 0x13, 0x25, 0x47, 0x67, 0x24, 0xd1, 0xb4,
+ 0xde, 0xaf, 0xd4, 0x71, 0x10, 0xe8, 0x66, 0x61, 0x6a, 0x22, 0x36, 0xfe,
+ 0x44, 0xbe, 0x16, 0xa0, 0xab, 0xe4, 0x43, 0x6e, 0x25, 0xf6, 0xc7, 0x9a,
+ 0xbe, 0x14, 0x3e, 0x64, 0x61, 0x7f, 0x03, 0x57, 0x58, 0xa4, 0xcd, 0xf0,
+ 0x3a, 0x6c, 0x87, 0x75, 0xc3, 0x94, 0x5d, 0x58, 0x17, 0x9d, 0xc4, 0xba,
+ 0xc2, 0x31, 0xdf, 0x84, 0x8b, 0xf6, 0x59, 0xde, 0xa8, 0x7b, 0x41, 0x74,
+ 0x68, 0xb6, 0xac, 0xff, 0x83, 0xe3, 0x56, 0x46, 0xa9, 0xbe, 0xce, 0xf1,
+ 0x49, 0x86, 0x20, 0x6d, 0x64, 0x6a, 0x07, 0xb3, 0x9a, 0xf3, 0xbb, 0xf0,
+ 0xc6, 0x31, 0x94, 0x58, 0xe5, 0x69, 0x52, 0x6c, 0xcc, 0x35, 0x7c, 0xbf,
+ 0x13, 0xd9, 0x43, 0xa6, 0xe7, 0x0a, 0x0e, 0x51, 0xba, 0xff, 0x1c, 0x63,
+ 0xb8, 0x69, 0xb7, 0x77, 0xb5, 0x6b, 0x20, 0x7b, 0xf0, 0x00, 0xf9, 0xb9,
+ 0x07, 0x34, 0xc9, 0x9d, 0x22, 0xb6, 0xd6, 0xa6, 0x52, 0x0b, 0xed, 0xea,
+ 0x50, 0x6e, 0x2e, 0x63, 0x68, 0x67, 0x4a, 0x5b, 0xff, 0xe2, 0xd2, 0xb8,
+ 0xb1, 0x2d, 0xc8, 0xcf, 0x09, 0x5e, 0x9c, 0xfd, 0xff, 0x0d, 0xe4, 0xab,
+ 0xa5, 0xbd, 0x70, 0xad, 0xb0, 0x46, 0xbe, 0xee, 0x77, 0x57, 0x13, 0x55,
+ 0x30, 0x3d, 0x76, 0x17, 0x3d, 0x8c, 0x8d, 0x3f, 0x1a, 0x1e, 0x32, 0xcd,
+ 0xc4, 0xa0, 0x5c, 0xc0, 0xe6, 0x19, 0x37, 0x6d, 0x82, 0xce, 0x05, 0xe4,
+ 0x9c, 0x74, 0x60, 0x07, 0x0f, 0x77, 0x45, 0xd9, 0x92, 0x33, 0xe8, 0x87,
+ 0xcf, 0x83, 0x2a, 0x9c, 0xc1, 0xf0, 0xc6, 0x5c, 0x8f, 0xb3, 0x51, 0x4e,
+ 0x11, 0x9e, 0xb6, 0x8a, 0xaf, 0xf9, 0x11, 0x74, 0xab, 0x8f, 0xc4, 0x39,
+ 0x62, 0x82, 0x06, 0x3c, 0x1f, 0x9a, 0xde, 0xbd, 0x33, 0xc4, 0x3b, 0x7a,
+ 0x14, 0xcb, 0x8f, 0xeb, 0x5b, 0xc6, 0x9b, 0xa4, 0x4d, 0x20, 0xef, 0x41,
+ 0xf3, 0xe5, 0x35, 0x84, 0x55, 0xaa, 0x49, 0xc1, 0x70, 0x32, 0x85, 0x94,
+ 0x4c, 0x42, 0x93, 0xfd, 0xad, 0x70, 0xeb, 0xc0, 0xed, 0x95, 0xbe, 0x2a,
+ 0xa5, 0x0b, 0x2b, 0x46, 0xe9, 0x58, 0x90, 0x5c, 0x06, 0x84, 0xc1, 0xb5,
+ 0x40, 0x42, 0x0e, 0xfa, 0xac, 0xc7, 0xcd, 0x97, 0xc8, 0x86, 0x99, 0x9a,
+ 0x83, 0x29, 0x05, 0x49, 0x28, 0x55, 0x57, 0xa0, 0x8b, 0x4b, 0xe9, 0x27,
+ 0xf4, 0x67, 0xec, 0xb0, 0xe5, 0x71, 0xca, 0x84, 0x54, 0xca, 0xba, 0x94,
+ 0x04, 0xa8, 0x65, 0xef, 0xee, 0xb1, 0x2b, 0xda, 0xb1, 0x5c, 0x30, 0x08,
+ 0x70, 0xeb, 0xc3, 0x66, 0x72, 0xb9, 0xd0, 0x75, 0xd1, 0x89, 0x92, 0x3a,
+ 0x90, 0x16, 0xc5, 0xf4, 0xac, 0xab, 0xac, 0xd3, 0x63, 0x52, 0xea, 0x27,
+ 0xa9, 0xe7, 0x0a, 0x93, 0x27, 0x55, 0xf8, 0xc8, 0x8a, 0x99, 0x7f, 0x49,
+ 0xd4, 0x48, 0x1e, 0x54, 0xf7, 0xbe, 0x07, 0x1c, 0xcc, 0x1b, 0x06, 0xcb,
+ 0xee, 0xc8, 0x47, 0xfe, 0x0c, 0xe0, 0x90, 0xa9, 0xa1, 0x31, 0x4a, 0xe0,
+ 0x2d, 0x63, 0x55, 0xd1, 0x94, 0xca, 0x83, 0xda, 0xa7, 0x9e, 0xd0, 0xc6,
+ 0x24, 0x61, 0xd7, 0xe8, 0x1f, 0x41, 0xc4, 0xb8, 0xe2, 0x4f, 0x82, 0x77,
+ 0xfc, 0x5f, 0xd7, 0xc2, 0x35, 0x0c, 0x6a, 0x35, 0x8d, 0x4f, 0x65, 0x3c,
+ 0x89, 0x23, 0xb8, 0xf3, 0x69, 0x7b, 0x0b, 0x7b, 0x0e, 0x77, 0xc7, 0x4e,
+ 0x45, 0x26, 0x06, 0x5f, 0x44, 0x0c, 0x45, 0xc0, 0xbb, 0x5c, 0xa9, 0x7f,
+ 0xc6, 0xa7, 0x04, 0x6e, 0x1b, 0x80, 0x62, 0x9a, 0xf2, 0xa1, 0xb8, 0xe3,
+ 0x3b, 0x65, 0xe8, 0x7a, 0xe9, 0xbb, 0xdc, 0xa3, 0x3a, 0x4f, 0x47, 0xa8,
+ 0x8f, 0x07, 0x2a, 0xc2, 0xbe, 0xa5, 0x81, 0xbd, 0x61, 0x9c, 0xe2, 0x6c,
+ 0xf0, 0x99, 0x2b, 0x4f, 0x5e, 0xad, 0xb5, 0x48, 0x09, 0x58, 0x9b, 0x24,
+ 0xe8, 0x69, 0xac, 0xa9, 0xe3, 0x51, 0xfc, 0xa5, 0xa4, 0x50, 0x08, 0xa7,
+ 0x99, 0xb8, 0xce, 0x6c, 0x68, 0x73, 0xda, 0x18, 0x98, 0x25, 0xbc, 0x72,
+ 0x9f, 0x61, 0x6a, 0xf7, 0xa8, 0x3c, 0xdc, 0x84, 0x93, 0x88, 0x0f, 0xe0,
+ 0xfc, 0xde, 0xcc, 0xae, 0xf4, 0x98, 0x08, 0xb5, 0x8a, 0x95, 0x4b, 0x67,
+ 0x81, 0x2c, 0x6a, 0xf8, 0x7c, 0xd3, 0x59, 0x7b, 0x09, 0xbc, 0xd4, 0xe4,
+ 0x0e, 0xb6, 0x76, 0x6c, 0x0d, 0xa0, 0xb6, 0x27, 0x98, 0x75, 0x81, 0x2e,
+ 0x18, 0xc4, 0x54, 0xcc, 0xf8, 0xe3, 0x2c, 0x2b, 0xfa, 0xc1, 0xbe, 0x49,
+ 0x5d, 0xf7, 0x45, 0xdf, 0x06, 0xc2, 0xb1, 0xc5, 0x10, 0x3a, 0x1c, 0x12,
+ 0x37, 0x14, 0xf2, 0x6c, 0xd3, 0xa7, 0x4f, 0xfd, 0x05, 0x0f, 0xda, 0x4e,
+ 0xa4, 0xef, 0x56, 0x20, 0x0f, 0x62, 0x6f, 0xcf, 0xb5, 0xe6, 0xe7, 0x75,
+ 0x77, 0x9f, 0x5a, 0xa0, 0x7e, 0x8b, 0x1d, 0x15, 0xbe, 0x12, 0xf2, 0xcf,
+ 0xe9, 0x09, 0x8b, 0xcc, 0x2b, 0xfb, 0xfd, 0xb4, 0xef, 0x38, 0xde, 0xe0,
+ 0xe6, 0x93, 0xcb, 0x17, 0x78, 0x9e, 0x53, 0xfa, 0x76, 0x22, 0x75, 0x60,
+ 0x66, 0xd8, 0x23, 0x18, 0x95, 0xd4, 0x3f, 0x6d, 0xb7, 0x6b, 0x6b, 0x39,
+ 0xde, 0x54, 0xbb, 0x14, 0xe8, 0x73, 0x3f, 0x22, 0x6e, 0xca, 0x1d, 0x40,
+ 0x62, 0x70, 0x91, 0xc9, 0x45, 0xea, 0xe9, 0x3e, 0xfb, 0xd0, 0xa5, 0xb5,
+ 0x07, 0xd0, 0x45, 0x93, 0x6c, 0x55, 0x55, 0xdc, 0x2f, 0xbd, 0x27, 0x9a,
+ 0x13, 0xc5, 0xa5, 0x08, 0x2c, 0x26, 0x58, 0x73, 0x64, 0xf0, 0xf5, 0x27,
+ 0x2a, 0x72, 0xec, 0x07, 0xdc, 0xf3, 0xd1, 0x85, 0x67, 0xfb, 0xf7, 0x0e,
+ 0x19, 0xf5, 0xf0, 0x7d, 0x63, 0xac, 0x51, 0xbe, 0x87, 0x22, 0x00, 0xba,
+ 0x35, 0x56, 0x72, 0xc3, 0x4a, 0xa1, 0x0c, 0x03, 0xe5, 0x8d, 0xcc, 0x21,
+ 0x69, 0x8d, 0xe0, 0x54, 0x8f, 0x97, 0x4c, 0x07, 0x99, 0xef, 0x46, 0xf0,
+ 0x94, 0xea, 0x59, 0x54, 0x01, 0xa9, 0xcd, 0xbf, 0x48, 0x63, 0x99, 0x7d,
+ 0x53, 0x08, 0x03, 0xfa, 0x63, 0xb6, 0x2f, 0xcb, 0x41, 0xff, 0x83, 0x66,
+ 0xea, 0x45, 0x8b, 0x20, 0x0d, 0xb3, 0xa6, 0x29, 0x67, 0x56, 0x94, 0x07,
+ 0x58, 0x05, 0x72, 0x62, 0x79, 0xfa, 0xc6, 0x47, 0x96, 0xee, 0x65, 0x0b,
+ 0xbd, 0xcc, 0x54, 0x70, 0xaf, 0x44, 0x05, 0xfa, 0x1a, 0x09, 0xdd, 0xf4,
+ 0xf8, 0x9b, 0xad, 0xb2, 0x68, 0xe4, 0xf6, 0x87, 0x92, 0x5d, 0xcb, 0xc7,
+ 0xbb, 0x73, 0x99, 0xe8, 0x82, 0x4c, 0x91, 0x26, 0x66, 0xed, 0x10, 0x79,
+ 0xe0, 0x63, 0x8b, 0x45, 0xd9, 0x10, 0x68, 0x9e, 0xe6, 0x68, 0xee, 0x11,
+ 0x40, 0xdb, 0x02, 0x26, 0xa0, 0x07, 0xca, 0x7f, 0x07, 0x94, 0xca, 0x97,
+ 0x7e, 0x03, 0xf1, 0x13, 0x81, 0x3a, 0xe3, 0x94, 0xc8, 0x4c, 0x1f, 0xe7,
+ 0xda, 0x2f, 0xef, 0x7e, 0x18, 0x97, 0xbf, 0x51, 0x07, 0x16, 0x18, 0x08,
+ 0x15, 0x6b, 0x8a, 0xdc, 0xab, 0xb9, 0xb7, 0xce, 0x5c, 0x2c, 0x78, 0x4b,
+ 0x58, 0xc3, 0xe0, 0xde, 0x4d, 0x9d, 0x3e, 0x3d, 0xb9, 0xe5, 0x81, 0xb0,
+ 0x0c, 0xe2, 0x1a, 0xfe, 0xbe, 0x13, 0x0b, 0x45, 0x7e, 0x0f, 0xb1, 0x31,
+ 0xaa, 0x8d, 0x72, 0xbc, 0x99, 0xcb, 0x87, 0xe9, 0x20, 0x84, 0x83, 0xd0,
+ 0xb0, 0x2c, 0x39, 0x3e, 0xa9, 0x5e, 0x6c, 0x38, 0x3b, 0x52, 0x01, 0xde,
+ 0x26, 0x9d, 0x15, 0xec, 0x5e, 0x6b, 0x0b, 0x43, 0x8e, 0x1b, 0xdb, 0x36,
+ 0xfa, 0x02, 0x80, 0x68, 0x7e, 0x3e, 0xb5, 0x75, 0x4b, 0xae, 0xf0, 0x10,
+ 0xfa, 0x9b, 0x75, 0xa9, 0xa3, 0x82, 0xf1, 0x26, 0x3c, 0x8c, 0x35, 0x41,
+ 0x8b, 0x9c, 0x95, 0x66, 0x22, 0x8b, 0xa5, 0xaf, 0xe6, 0x91, 0xcb, 0x69,
+ 0x5f, 0x27, 0xe1, 0x3e, 0x80, 0x35, 0xac, 0xc7, 0xb2, 0xed, 0x13, 0xfc,
+ 0x20, 0xa8, 0x12, 0x6c, 0x0c, 0x50, 0xd3, 0xdb, 0xf7, 0xfe, 0x29, 0x64,
+ 0xb6, 0x76, 0xda, 0xc9, 0x63, 0x80, 0x37, 0x99, 0x50, 0x26, 0x2b, 0xe4,
+ 0x01, 0x55, 0xc6, 0x1c, 0xfa, 0x02, 0x94, 0xc2, 0x2d, 0x7d, 0xd6, 0x44,
+ 0x0c, 0x70, 0x4d, 0x8d, 0x6a, 0x14, 0x48, 0x9e, 0x52, 0x05, 0x1e, 0xf4,
+ 0x90, 0x68, 0xb3, 0xef, 0x7d, 0x3a, 0x66, 0x65, 0x46, 0x53, 0x91, 0x4a,
+ 0x31, 0x21, 0xa0, 0xed, 0xba, 0xf2, 0xff, 0x6e, 0x0f, 0x50, 0x86, 0x93,
+ 0x1c, 0x92, 0xa3, 0x45, 0x61, 0xe2, 0x17, 0xaa, 0xa6, 0x64, 0x4e, 0x4f,
+ 0xc3, 0x29, 0x9f, 0x15, 0x63, 0x75, 0x23, 0x01, 0xee, 0x50, 0x12, 0x89,
+ 0x08, 0x3f, 0x9a, 0x6f, 0x95, 0x37, 0x5d, 0x06, 0x23, 0x53, 0x6e, 0x64,
+ 0xa9, 0x29, 0xbc, 0x1a, 0x9b, 0xee, 0x17, 0x72, 0x54, 0x87, 0x4f, 0xc4,
+ 0x39, 0xb3, 0xa4, 0x0d, 0x52, 0x7a, 0x9b, 0xa2, 0x3a, 0xfc, 0x5a, 0xa1,
+ 0xd4, 0xda, 0x30, 0x05, 0xf4, 0x2c, 0x40, 0xb9, 0x61, 0x37, 0x83, 0xd8,
+ 0x0b, 0x71, 0xd4, 0x80, 0x9b, 0x5a, 0x07, 0x83, 0x80, 0xe3, 0xda, 0x61,
+ 0x82, 0x3d, 0x99, 0x35, 0x5c, 0x0f, 0x5d, 0x01, 0x16, 0x75, 0x79, 0x57,
+ 0xd6, 0x75, 0x86, 0x85, 0xfc, 0xb2, 0x7a, 0xfd, 0x82, 0x7e, 0x8e, 0x45,
+ 0x80, 0x01, 0xe2, 0x50, 0x14, 0xec, 0x5e, 0x74, 0x10, 0x35, 0x2f, 0xa9,
+ 0x47, 0x78, 0x1e, 0xc0, 0xe0, 0x2d, 0x00, 0xef, 0x27, 0xf1, 0x81, 0x62,
+ 0x88, 0x2f, 0x2e, 0x39, 0x14, 0x23, 0xb7, 0x76, 0x6e, 0x5a, 0xd2, 0xd4,
+ 0x71, 0x1a, 0x37, 0x8e, 0x96, 0x5f, 0x8f, 0x39, 0xef, 0x61, 0x77, 0xfb,
+ 0x73, 0x10, 0xc8, 0xef, 0xff, 0x65, 0x2e, 0xaa, 0xc6, 0xb0, 0x82, 0xe1,
+ 0xea, 0x2d, 0x6e, 0x18, 0x3f, 0x45, 0x40, 0x52, 0x5b, 0xcf, 0x3a, 0xc4,
+ 0x83, 0x69, 0x16, 0x86, 0x23, 0xb8, 0xd6, 0xc7, 0x83, 0x9a, 0x05, 0x57,
+ 0x9f, 0x21, 0x8e, 0x11, 0x39, 0x55, 0x8a, 0xa7, 0x5b, 0xc6, 0xdd, 0x79,
+ 0x1d, 0x2f, 0x93, 0x82, 0x5c, 0x6a, 0xc1, 0x65, 0x2d, 0x14, 0x14, 0xd4,
+ 0x02, 0xc6, 0x45, 0xba, 0x25, 0xee, 0x2b, 0x23, 0x55, 0xf5, 0x78, 0x1a,
+ 0x04, 0x1c, 0xe9, 0x06, 0x72, 0x75, 0xda, 0x03, 0xfd, 0x5d, 0xf0, 0x75,
+ 0xfe, 0xbd, 0xb2, 0x23, 0x1f, 0x8f, 0x7d, 0xeb, 0xf3, 0x14, 0x2a, 0xbb,
+ 0xd1, 0x4f, 0x96, 0xbe, 0x9e, 0xf5, 0xbe, 0x83, 0x1e, 0x94, 0x3b, 0x73,
+ 0xdf, 0x39, 0x0a, 0x6e, 0x1a, 0x36, 0xcc, 0xe3, 0xc0, 0x30, 0xa1, 0x34,
+ 0x02, 0xc9, 0xd6, 0x88, 0x81, 0x50, 0xea, 0x77, 0xff, 0xe3, 0xa2, 0x13,
+ 0xdc, 0x8a, 0xa4, 0xca, 0xdd, 0xa2, 0xde, 0x83, 0x38, 0xca, 0x85, 0xe1,
+ 0x1a, 0x6b, 0x82, 0x91, 0xa4, 0x84, 0x79, 0x27, 0x98, 0xae, 0x44, 0x63,
+ 0x49, 0x9b, 0xd8, 0x45, 0x77, 0x3b, 0xa5, 0x21, 0xe0, 0xb4, 0xae, 0x93,
+ 0x06, 0x38, 0xd6, 0x13, 0x6e, 0xa0, 0xb3, 0x67, 0xfd, 0xeb, 0x5a, 0xe0,
+ 0x6a, 0x9e, 0xa1, 0x39, 0x5e, 0x16, 0x7f, 0x7e, 0xee, 0x97, 0x7e, 0x67,
+ 0x2d, 0x6d, 0x7e, 0xa2, 0x6a, 0x39, 0xd6, 0xd3, 0xba, 0x2c, 0xa8, 0x50,
+ 0x03, 0x1b, 0xf8, 0x25, 0x4c, 0x03, 0x0e, 0xe6, 0x8d, 0x6e, 0xb4, 0xdb,
+ 0x0e, 0xcc, 0xb3, 0x31, 0xe7, 0x55, 0x91, 0xee, 0x5e, 0xc0, 0x52, 0x79,
+ 0x12, 0x06, 0xb3, 0x67, 0x51, 0xb3, 0xbe, 0x0a, 0xfa, 0x58, 0x28, 0x46,
+ 0x96, 0xac, 0x06, 0x17, 0xd0, 0xf1, 0xa0, 0x8c, 0x52, 0x5a, 0xa3, 0x8c,
+ 0x6f, 0xbd, 0x79, 0x91, 0xf8, 0x43, 0xdd, 0x46, 0x1e, 0xe2, 0x51, 0xb5,
+ 0x10, 0xba, 0x3f, 0x70, 0xf5, 0x78, 0x7f, 0x45, 0x1d, 0xea, 0xc9, 0x71,
+ 0xe1, 0xe3, 0xd1, 0x6b, 0xb7, 0xb6, 0xc7, 0x8a, 0x42, 0xbd, 0xe1, 0xa0,
+ 0x3e, 0x31, 0x1b, 0x05, 0xb9, 0xe8, 0xb9, 0xfa, 0xc7, 0xb3, 0xd8, 0x2e,
+ 0x7a, 0x9d, 0x61, 0x73, 0xcf, 0x43, 0x3b, 0x32, 0xaa, 0x6d, 0xda, 0x6f,
+ 0x2f, 0xce, 0x40, 0x9b, 0x60, 0xdc, 0xac, 0x6a, 0x37, 0xd6, 0x20, 0xee,
+ 0x80, 0xee, 0xdf, 0xc2, 0xd8, 0x4d, 0x25, 0x8e, 0x5b, 0x54, 0xee, 0x9b,
+ 0x99, 0x77, 0xd3, 0x70, 0xba, 0x6d, 0xde, 0x08, 0x0a, 0x04, 0xde, 0x1d,
+ 0xfb, 0x86, 0x63, 0x08, 0x70, 0x93, 0x47, 0x09, 0x06, 0x49, 0xce, 0x79,
+ 0x73, 0x9f, 0xd8, 0xd2, 0x4f, 0xca, 0xc3, 0x25, 0xb0, 0xb0, 0x94, 0x36,
+ 0xb5, 0x00, 0x3d, 0xf8, 0xab, 0x2a, 0x2e, 0xe4, 0x24, 0x73, 0x1f, 0xfc,
+ 0xae, 0xa2, 0x0b, 0x47, 0x17, 0xb3, 0x51, 0xe6, 0x4d, 0xdd, 0xb5, 0x3e,
+ 0x65, 0x92, 0x3f, 0xd4, 0xb3, 0x50, 0x56, 0xf8, 0x73, 0x00, 0x92, 0x93,
+ 0x3b, 0x6d, 0x6a, 0x97, 0xe5, 0x70, 0x64, 0xf0, 0x52, 0x12, 0x5f, 0x9a,
+ 0xfe, 0xba, 0x82, 0x99, 0x9d, 0x3f, 0xca, 0xca, 0x6e, 0x41, 0xcf, 0xfa,
+ 0x64, 0xa8, 0x4f, 0xe2, 0x68, 0xd0, 0x3b, 0x09, 0xd9, 0x5c, 0x01, 0x0c,
+ 0xd1, 0x6a, 0xa5, 0xd6, 0x7a, 0xcc, 0xa6, 0x60, 0x28, 0xb3, 0xbf, 0xfa,
+ 0xd1, 0x6e, 0x3d, 0x8b, 0xf7, 0x0a, 0x6b, 0xe7, 0x50, 0x31, 0x3c, 0x1e,
+ 0x16, 0x04, 0x29, 0x01, 0x51, 0x81, 0x9d, 0x67, 0x9d, 0x14, 0x69, 0xfb,
+ 0xd2, 0x52, 0x95, 0x98, 0x12, 0x6b, 0xc5, 0x8e, 0x42, 0xfa, 0x32, 0xff,
+ 0xc3, 0xa6, 0x30, 0x72, 0x95, 0x88, 0xa8, 0x2d, 0x7b, 0xdb, 0x36, 0x7b,
+ 0x1a, 0x8b, 0x2d, 0x09, 0x12, 0xb2, 0xe7, 0x49, 0x5c, 0x27, 0x60, 0x01,
+ 0xd6, 0xe0, 0x86, 0x89, 0xa6, 0x30, 0x89, 0x16, 0x6d, 0xc0, 0x8a, 0x59,
+ 0xb0, 0xa9, 0x41, 0xf7, 0x50, 0x6a, 0x6b, 0x6c, 0x1c, 0x92, 0xfd, 0x6e,
+ 0xae, 0x94, 0x4e, 0xee, 0x17, 0x77, 0xd7, 0xd4, 0x49, 0x02, 0x21, 0xeb,
+ 0x5b, 0x4c, 0xe2, 0xf0, 0x43, 0x4d, 0x7c, 0x56, 0x22, 0xb4, 0x0a, 0x26,
+ 0xd1, 0x03, 0xb6, 0x46, 0x55, 0x98, 0x4b, 0x6c, 0x71, 0xa8, 0x56, 0xea,
+ 0x7a, 0x0d, 0x48, 0x70, 0x06, 0x5b, 0xd4, 0x69, 0x91, 0xbb, 0x37, 0x93,
+ 0xea, 0x46, 0xfd, 0xd0, 0xab, 0xbf, 0xdd, 0xda, 0xc8, 0xb2, 0xba, 0x4e,
+ 0x7e, 0xc5, 0x76, 0x23, 0xf1, 0x72, 0xa1, 0x73, 0x59, 0xc2, 0x56, 0x9a,
+ 0x77, 0x20, 0x2d, 0xb7, 0x20, 0x9f, 0x23, 0x7f, 0x2b, 0x7c, 0x14, 0x67,
+ 0x95, 0xd0, 0x1b, 0x40, 0x40, 0xf9, 0xf2, 0x8f, 0x03, 0x5d, 0xcf, 0xbd,
+ 0x69, 0x02, 0x52, 0x3b, 0x3b, 0x09, 0x12, 0x56, 0x7e, 0xb9, 0x4e, 0x21,
+ 0x3f, 0x25, 0x13, 0x17, 0x91, 0x78, 0xda, 0xa2, 0x03, 0x70, 0xa2, 0xae,
+ 0x4f, 0x93, 0xbd, 0x9c, 0x59, 0xb4, 0x18, 0x87, 0xe1, 0xe8, 0x3b, 0xeb,
+ 0x39, 0x7f, 0x31, 0x20, 0x8a, 0x74, 0x19, 0x9b, 0x6c, 0x3e, 0x66, 0xde,
+ 0xe4, 0x10, 0x45, 0x8f, 0x2a, 0xf5, 0x18, 0x5e, 0x37, 0xe5, 0x44, 0xc9,
+ 0x0d, 0xa6, 0x11, 0x87, 0x06, 0x71, 0xbc, 0x4b, 0x58, 0x5b, 0x12, 0x0b,
+ 0x2b, 0x10, 0x2b, 0xe4, 0x4d, 0x58, 0xd1, 0x5f, 0x54, 0x7c, 0x3e, 0x69,
+ 0xad, 0xac, 0x01, 0xed, 0xf0, 0x42, 0xa5, 0x89, 0x1c, 0x72, 0x51, 0xf3,
+ 0x95, 0x8d, 0xf2, 0xe0, 0xa1, 0x95, 0x5a, 0x77, 0x51, 0x38, 0x26, 0xc9,
+ 0x2c, 0xea, 0x51, 0x06, 0xae, 0xf1, 0x40, 0xcf, 0x71, 0x53, 0xd6, 0x02,
+ 0x2d, 0xdf, 0xbb, 0x4f, 0xb3, 0xcb, 0x14, 0xb2, 0x9f, 0xb8, 0x5a, 0x39,
+ 0x96, 0xa2, 0x3c, 0xf7, 0xfb, 0x4c, 0x64, 0xca, 0x64, 0x7c, 0x26, 0x0c,
+ 0x6b, 0x27, 0xe4, 0xe7, 0xed, 0x33, 0x0d, 0xd2, 0x86, 0x81, 0x67, 0x12,
+ 0xd5, 0x13, 0x09, 0x6c, 0x2c, 0x97, 0x06, 0x7a, 0x07, 0xb3, 0x1f, 0x99,
+ 0xca, 0xc4, 0x97, 0x45, 0x14, 0xe9, 0x34, 0x9e, 0xac, 0x5a, 0xf7, 0x85,
+ 0x8f, 0x67, 0x28, 0x3e, 0x2a, 0xc4, 0x6c, 0x34, 0x25, 0x76, 0x70, 0xb7,
+ 0x41, 0x68, 0x24, 0xbb, 0xf7, 0x4b, 0x87, 0xf2, 0x71, 0xf2, 0xf0, 0x0c,
+ 0xc6, 0x8a, 0x63, 0x7e, 0x5a, 0x22, 0x73, 0xdc, 0xd4, 0xd6, 0xa8, 0x31,
+ 0xa5, 0x6f, 0x62, 0xc4, 0xc5, 0x6f, 0x5c, 0xee, 0x51, 0xe1, 0x1c, 0x91,
+ 0x27, 0x00, 0x6b, 0x72, 0x7d, 0x74, 0xa8, 0xb4, 0x5b, 0xf2, 0x90, 0xa6,
+ 0x42, 0x47, 0xcc, 0x41, 0x13, 0x6e, 0x10, 0xba, 0x44, 0x32, 0x33, 0x18,
+ 0x6f, 0xd6, 0x9a, 0xe9, 0x17, 0xf0, 0x35, 0xa2, 0x2f, 0xa0, 0xb2, 0x1b,
+ 0xed, 0x12, 0x67, 0x1a, 0x78, 0xb1, 0xc6, 0x63, 0x2b, 0x99, 0xa3, 0x99,
+ 0xb5, 0x75, 0x50, 0xdc, 0xf4, 0xd6, 0x19, 0xb2, 0xd7, 0x5f, 0xba, 0xfc,
+ 0xd3, 0xb1, 0x1b, 0x9d, 0x3c, 0xe1, 0x38, 0x51, 0xd5, 0x65, 0x85, 0xd4,
+ 0xc9, 0x56, 0x4a, 0xc0, 0x9e, 0x07, 0x2a, 0xb9, 0x64, 0x76, 0x4a, 0x0b,
+ 0x24, 0x42, 0xd2, 0x40, 0xca, 0x5c, 0x10, 0xdb, 0x55, 0xd8, 0xfe, 0x8f,
+ 0x2a, 0x1f, 0x22, 0x42, 0xe0, 0xe0, 0x52, 0xd2, 0xd8, 0xdf, 0xd6, 0x0e,
+ 0xb4, 0x9f, 0xa5, 0x5f, 0x2a, 0x5f, 0x88, 0x97, 0x5f, 0x88, 0x70, 0x4d,
+ 0xcc, 0x3e, 0x81, 0x80, 0x41, 0x88, 0x70, 0x19, 0xb9, 0xfa, 0xa1, 0x43,
+ 0xd0, 0x88, 0xed, 0x68, 0xc5, 0x65, 0x63, 0xb4, 0x54, 0x0d, 0x69, 0x03,
+ 0xb0, 0x6e, 0xc9, 0xc1, 0xdc, 0x28, 0xdd, 0x4a, 0x7d, 0xb0, 0x43, 0xf6,
+ 0x53, 0x68, 0xb4, 0xf8, 0x15, 0x43, 0x54, 0xa8, 0xc0, 0x5f, 0x63, 0xde,
+ 0xf0, 0x94, 0xc0, 0x17, 0xed, 0x7c, 0x3e, 0x16, 0xca, 0xc5, 0xa8, 0x0f,
+ 0x24, 0xf7, 0xa7, 0xb1, 0x4e, 0x31, 0x8c, 0x17, 0x39, 0x6d, 0xad, 0x69,
+ 0xb0, 0x4f, 0x6c, 0xf5, 0xc4, 0xcb, 0x59, 0x05, 0x30, 0x67, 0xa5, 0x71,
+ 0xa9, 0xf3, 0x23, 0xa4, 0x23, 0x2d, 0xea, 0x34, 0x4f, 0x4b, 0x21, 0xda,
+ 0xee, 0x33, 0x63, 0x19, 0x8c, 0x33, 0xcc, 0xa8, 0x59, 0xba, 0x5b, 0xd3,
+ 0x48, 0x6c, 0x24, 0x61, 0xdc, 0x0f, 0x87, 0xd7, 0x92, 0x8a, 0x59, 0xc0,
+ 0x25, 0xdc, 0xcb, 0x4d, 0x08, 0xa0, 0xb9, 0x8e, 0xdd, 0xd5, 0xca, 0x32,
+ 0x95, 0xeb, 0x22, 0xa9, 0x6d, 0x34, 0x7d, 0x98, 0x3d, 0xea, 0x38, 0x6a,
+ 0x4a, 0xc7, 0xbc, 0x0a, 0xe5, 0xa0, 0x97, 0x93, 0xe6, 0x61, 0xa9, 0x5e,
+ 0x43, 0xcb, 0x2b, 0xb4, 0x0a, 0xf7, 0xc5, 0x5d, 0x9e, 0xaf, 0xdf, 0x52,
+ 0x99, 0x65, 0x5c, 0x31, 0xec, 0xb6, 0x88, 0xec, 0xa7, 0x96, 0xcf, 0x64,
+ 0x17, 0x68, 0x9d, 0x1b, 0x6d, 0x9e, 0x4a, 0xdf, 0xae, 0x79, 0x0e, 0x39,
+ 0x72, 0x89, 0xb2, 0x99, 0x53, 0x9e, 0x12, 0x16, 0xc4, 0xfc, 0x71, 0xc3,
+ 0x96, 0x4d, 0x0e, 0x02, 0x1d, 0xf7, 0xf9, 0x20, 0xfe, 0x2d, 0xdb, 0xdf,
+ 0x10, 0x0b, 0x5a, 0x9b, 0xda, 0x46, 0xc8, 0xe9, 0xa5, 0x34, 0xad, 0x38,
+ 0x4b, 0xe9, 0x08, 0x11, 0xfd, 0xc7, 0xb9, 0x52, 0xbd, 0x31, 0x2f, 0x9b,
+ 0xdf, 0xf5, 0x0b, 0x79, 0xe1, 0x2c, 0x7a, 0xe8, 0xcd, 0x35, 0xa5, 0xf9,
+ 0x3b, 0x76, 0x86, 0xcf, 0x46, 0xf7, 0x2a, 0x6b, 0xec, 0x24, 0xed, 0x82,
+ 0x29, 0xee, 0xd3, 0x0f, 0xd0, 0x1b, 0xa3, 0xfd, 0x55, 0xa3, 0x57, 0x49,
+ 0xe0, 0x32, 0x32, 0xbe, 0x44, 0x05, 0x0b, 0x99, 0x04, 0x5c, 0xd5, 0xde,
+ 0x75, 0x6d, 0xcd, 0xf4, 0xc0, 0x28, 0x96, 0xa1, 0xd9, 0x0c, 0xb3, 0xc9,
+ 0x16, 0x3c, 0x44, 0x91, 0xd2, 0xab, 0xf2, 0x3d, 0x52, 0x0a, 0xaa, 0xf3,
+ 0x72, 0xb8, 0x81, 0x8c, 0x1f, 0xdc, 0x29, 0x86, 0x7e, 0x48, 0x6d, 0x39,
+ 0xa9, 0x92, 0xd9, 0x51, 0x44, 0x0e, 0x96, 0x46, 0xce, 0xf9, 0x6a, 0xfa,
+ 0xd1, 0xb5, 0xc8, 0xa3, 0xac, 0x97, 0xdf, 0x94, 0xa0, 0x10, 0xfc, 0x65,
+ 0xa6, 0xcf, 0x54, 0x27, 0x16, 0xca, 0xa8, 0xad, 0xee, 0x68, 0xf6, 0x13,
+ 0x12, 0x2b, 0x46, 0x78, 0xe4, 0x7a, 0x27, 0xa4, 0x38, 0x77, 0xdf, 0x35,
+ 0x08, 0x98, 0x11, 0x78, 0x67, 0xde, 0xd0, 0xb2, 0x1f, 0x67, 0xe3, 0x09,
+ 0xf6, 0xc9, 0xf4, 0xc8, 0x40, 0xb8, 0x69, 0x3b, 0xdb, 0x5c, 0xad, 0x87,
+ 0xcc, 0x46, 0xb2, 0xc4, 0x3e, 0x91, 0x57, 0xec, 0xe0, 0x23, 0x9a, 0x99,
+ 0x83, 0x73, 0x01, 0x40, 0x6e, 0x52, 0x73, 0xfa, 0x57, 0x62, 0xa8, 0x2f,
+ 0xab, 0x8a, 0x0f, 0xf5, 0x6c, 0xde, 0x79, 0x19, 0xae, 0x03, 0x46, 0xc2,
+ 0xc5, 0x3b, 0x8c, 0xa9, 0x79, 0x3d, 0xe4, 0xb2, 0x04, 0x3a, 0x96, 0xd9,
+ 0x0c, 0x9c, 0x78, 0xa6, 0x3e, 0x43, 0x05, 0x3e, 0x2d, 0x02, 0xa9, 0xec,
+ 0xc4, 0xa7, 0x4d, 0x6e, 0x56, 0x6b, 0x55, 0x14, 0x4d, 0x1b, 0x87, 0x7e,
+ 0xf1, 0x92, 0x6e, 0xe2, 0x3e, 0x41, 0x06, 0xc7, 0x8a, 0xe2, 0x9e, 0x7b,
+ 0xc6, 0x2f, 0x88, 0xc3, 0xa1, 0x84, 0xe1, 0x0a, 0xff, 0x94, 0xa2, 0xcd,
+ 0xd1, 0x93, 0xaf, 0x5c, 0x39, 0x81, 0x90, 0x6f, 0x43, 0x34, 0x26, 0xeb,
+ 0x72, 0x0f, 0x99, 0x07, 0xe2, 0xf8, 0xaa, 0xb9, 0x57, 0x36, 0xbc, 0xe9,
+ 0x7d, 0xa9, 0x9e, 0x19, 0xf5, 0x46, 0x3c, 0x18, 0x58, 0xdf, 0x23, 0xa2,
+ 0x87, 0x89, 0x5e, 0xd6, 0x91, 0x5d, 0xeb, 0x1f, 0x95, 0x6d, 0x3f, 0x8a,
+ 0xde, 0x5b, 0x5a, 0x9f, 0xd3, 0xf8, 0xab, 0xf8, 0x87, 0x77, 0xd4, 0xf1,
+ 0x44, 0xa5, 0x59, 0x93, 0x45, 0xdc, 0x67, 0xeb, 0x20, 0x94, 0x6b, 0x4c,
+ 0x44, 0xdf, 0x02, 0xe9, 0x4f, 0x72, 0x83, 0x5f, 0x90, 0x06, 0x6c, 0x41,
+ 0x56, 0x78, 0xb2, 0x51, 0x33, 0xc6, 0x70, 0xa9, 0x7a, 0x64, 0xe0, 0xb1,
+ 0xd6, 0x24, 0xdc, 0x75, 0xc6, 0xea, 0x5d, 0xcf, 0x2c, 0x2e, 0xc8, 0x50,
+ 0x43, 0x9c, 0x3c, 0x6c, 0xc8, 0x6c, 0x5b, 0xff, 0xdf, 0x1a, 0x47, 0x82,
+ 0x6e, 0xde, 0x23, 0x16, 0xca, 0x57, 0xa9, 0xfb, 0xe5, 0xb1, 0xf9, 0xff,
+ 0x53, 0xde, 0xa7, 0x15, 0xf6, 0x13, 0x18, 0xf9, 0x1f, 0x33, 0xd5, 0xad,
+ 0xf8, 0x0a, 0x0a, 0x0c, 0x2d, 0xc0, 0xd3, 0xbc, 0xc0, 0xa5, 0xe3, 0x95,
+ 0xb5, 0xf9, 0x76, 0xda, 0xb2, 0x12, 0xfa, 0x83, 0x65, 0xb6, 0x0a, 0x14,
+ 0x65, 0x8e, 0x75, 0x4b, 0x88, 0x71, 0x97, 0xb3, 0xcc, 0x2c, 0x91, 0x0d,
+ 0xbe, 0x98, 0x92, 0xca, 0x73, 0xe8, 0x0f, 0xe0, 0x58, 0x76, 0x03, 0x24,
+ 0xa0, 0x17, 0xc6, 0x35, 0x2f, 0x75, 0x7f, 0x88, 0x12, 0xa6, 0x36, 0x59,
+ 0x05, 0x72, 0x1f, 0xe1, 0x9c, 0x34, 0x70, 0xf0, 0x16, 0xe8, 0x6e, 0xe3,
+ 0x2d, 0xc9, 0x50, 0x49, 0x50, 0xb0, 0x51, 0xd9, 0x62, 0xf1, 0x0f, 0x2a,
+ 0x77, 0x69, 0x8d, 0x53, 0x39, 0x6b, 0xa4, 0xfd, 0xe1, 0xd0, 0x76, 0x67,
+ 0x62, 0x74, 0x0e, 0xc7, 0x3d, 0xf3, 0x3c, 0xee, 0xbb, 0xd9, 0x56, 0x42,
+ 0x90, 0xf5, 0xd1, 0x6f, 0xf7, 0x13, 0xd6, 0xdc, 0x5e, 0xd6, 0x7b, 0x9c,
+ 0x4a, 0x63, 0x78, 0x56, 0xd2, 0x03, 0xa2, 0x33, 0xb1, 0x5e, 0x54, 0xba,
+ 0xb3, 0x5c, 0xd4, 0xd2, 0x30, 0xb4, 0x59, 0x80, 0xe7, 0xeb, 0xd9, 0x4b,
+ 0xa8, 0x1d, 0x35, 0xd2, 0x33, 0x67, 0x6a, 0xc8, 0x3e, 0x71, 0x4c, 0xc2,
+ 0x8b, 0xe2, 0x56, 0x1c, 0x0a, 0xec, 0xb8, 0xb8, 0xe4, 0xef, 0x22, 0xcd,
+ 0xb9, 0xf7, 0x5b, 0xba, 0x3b, 0xf1, 0xb9, 0xba, 0xdb, 0xe7, 0xc7, 0xae,
+ 0x0f, 0x9f, 0xa5, 0x79, 0x4c, 0x84, 0x25, 0x2c, 0x50, 0x65, 0x64, 0x01,
+ 0x7c, 0xe4, 0xc3, 0xd5, 0x81, 0xe6, 0x06, 0x6f, 0x1e, 0x79, 0x3b, 0x2a,
+ 0x9f, 0x8e, 0x09, 0xb8, 0xab, 0x3b, 0xa7, 0xce, 0x17, 0x70, 0x64, 0x29,
+ 0xa3, 0x72, 0x0f, 0x61, 0x75, 0x77, 0xd1, 0xd7, 0x0d, 0x62, 0xc9, 0xad,
+ 0xe1, 0x19, 0xd9, 0x49, 0x0a, 0x49, 0x88, 0x34, 0xa2, 0xa6, 0x1a, 0x3d,
+ 0x1d, 0xd2, 0x95, 0x63, 0x88, 0x84, 0xaf, 0x92, 0x41, 0x50, 0x63, 0x6b,
+ 0x15, 0x13, 0xda, 0x65, 0xd6, 0x47, 0x3d, 0xf0, 0x7c, 0x7e, 0xa1, 0xe4,
+ 0x10, 0x63, 0xa3, 0xa3, 0xf5, 0x9d, 0x4f, 0xac, 0xb9, 0x48, 0x92, 0x37,
+ 0xcc, 0x6f, 0xc3, 0xfe, 0x55, 0xf1, 0xd5, 0x94, 0x4d, 0xf6, 0x5b, 0x49,
+ 0x5d, 0xbb, 0xc2, 0x2e, 0xd4, 0x2f, 0x2b, 0xe1, 0x08, 0xa1, 0x4c, 0x48,
+ 0xce, 0xf8, 0x3b, 0x56, 0x2d, 0xe6, 0xa3, 0xa0, 0xd1, 0xf8, 0xfc, 0x16,
+ 0x30, 0xd1, 0xaa, 0xbd, 0x7f, 0x95, 0xe4, 0xe5, 0x74, 0xe6, 0x34, 0x74,
+ 0xe1, 0x6d, 0xc9, 0xb4, 0xc3, 0x8f, 0xae, 0x04, 0xd9, 0x11, 0xd1, 0x26,
+ 0xdd, 0x46, 0x89, 0x1f, 0xf0, 0x78, 0xa6, 0x1d, 0xa0, 0xa1, 0x38, 0x23,
+ 0x33, 0xb1, 0x39, 0xde, 0x7b, 0x54, 0x51, 0xa4, 0xf4, 0x94, 0xe8, 0x47,
+ 0xac, 0xa9, 0xbe, 0xf7, 0x34, 0xad, 0x21, 0x32, 0xe6, 0x1a, 0x81, 0xf4,
+ 0x0d, 0x27, 0x7e, 0x64, 0x14, 0x66, 0xcb, 0x56, 0xff, 0x00, 0xcc, 0xf4,
+ 0x73, 0x54, 0xba, 0xe1, 0x27, 0x42, 0xbb, 0x8b, 0xce, 0x38, 0x2d, 0xc1,
+ 0x76, 0x05, 0xe6, 0x99, 0x03, 0x8d, 0x23, 0xd7, 0x9b, 0xa0, 0x7d, 0x13,
+ 0xdb, 0xba, 0xc7, 0x81, 0x81, 0x3c, 0x37, 0xba, 0x16, 0x23, 0x7e, 0x32,
+ 0x4e, 0xac, 0xaa, 0xf5, 0x67, 0xed, 0x43, 0xc3, 0x42, 0x80, 0xae, 0x54,
+ 0x1f, 0xc2, 0xa2, 0xfa, 0x48, 0xce, 0x6b, 0xbd, 0xce, 0x15, 0x6a, 0xc0,
+ 0xb3, 0x7c, 0x30, 0xa7, 0xdd, 0x37, 0x2b, 0xb9, 0xb5, 0xc0, 0x2b, 0xcb,
+ 0xf6, 0x97, 0x0d, 0x28, 0xbc, 0x66, 0x37, 0xe8, 0x01, 0xb6, 0xf8, 0x07,
+ 0x0c, 0xa9, 0x2e, 0xd5, 0x2d, 0x6e, 0xdd, 0x00, 0xad, 0x0a, 0x6b, 0x6c,
+ 0x63, 0xc8, 0xbc, 0x8e, 0xf0, 0x37, 0x1a, 0xb9, 0xb0, 0x00, 0x27, 0xce,
+ 0x49, 0x50, 0xaa, 0x20, 0xf9, 0xbd, 0xae, 0x10, 0x37, 0x69, 0x4d, 0x11,
+ 0xe8, 0x18, 0x7a, 0xfc, 0xcd, 0x91, 0x00, 0x0a, 0x4b, 0xe8, 0x71, 0x4e,
+ 0xe3, 0x27, 0x3e, 0x84, 0x8f, 0xcf, 0xd8, 0xa9, 0x6c, 0xd6, 0x78, 0xc0,
+ 0x06, 0x83, 0x60, 0x8a, 0x45, 0x78, 0x60, 0xf2, 0x6c, 0x61, 0x6f, 0xf1,
+ 0x2f, 0x3f, 0x7b, 0xa5, 0xd8, 0x5e, 0x7b, 0x13, 0xba, 0xfd, 0x1d, 0x3e,
+ 0xb3, 0x21, 0x26, 0xe4, 0x8f, 0x78, 0xd7, 0xa4, 0x80, 0x0b, 0x48, 0x96,
+ 0xbe, 0x02, 0x45, 0x14, 0xa9, 0xe7, 0x6b, 0xcd, 0xa0, 0xa1, 0x3c, 0x44,
+ 0x66, 0x7b, 0xba, 0xf5, 0xf2, 0x2a, 0xad, 0x71, 0x1a, 0x15, 0x6f, 0xda,
+ 0x86, 0x91, 0x4e, 0x80, 0x8f, 0xdf, 0x26, 0x6d, 0x56, 0x75, 0x0d, 0x48,
+ 0xd9, 0xea, 0xd5, 0xf8, 0xab, 0x03, 0x62, 0xa3, 0x78, 0x20, 0xe7, 0x16,
+ 0x3e, 0x32, 0x72, 0x1f, 0xfd, 0xbe, 0xda, 0xd6, 0x83, 0xcb, 0x1a, 0x3d,
+ 0x01, 0x41, 0x9b, 0x4e, 0x85, 0xbf, 0x35, 0xa1, 0xd5, 0xac, 0x08, 0xf7,
+ 0x9c, 0xfb, 0x1d, 0x0a, 0x98, 0x6f, 0x6e, 0x9a, 0x90, 0xae, 0x09, 0x8e,
+ 0x35, 0xda, 0x22, 0xc2, 0xc2, 0x71, 0xf3, 0x8e, 0x49, 0x4b, 0xd4, 0x9b,
+ 0x21, 0x76, 0x54, 0xf8, 0xca, 0x99, 0x3a, 0x7d, 0xc3, 0xe9, 0xef, 0xf7,
+ 0x69, 0x57, 0x4f, 0xfb, 0x61, 0x4a, 0x30, 0xd2, 0xdc, 0xd5, 0x97, 0x16,
+ 0xfe, 0x32, 0x4e, 0xa7, 0x57, 0x04, 0xda, 0x2d, 0x61, 0x90, 0xf9, 0x48,
+ 0xfe, 0x7a, 0x3d, 0x7a, 0xcb, 0x5e, 0x3b, 0xa8, 0x80, 0xce, 0x58, 0xb3,
+ 0x03, 0xbf, 0xe2, 0x1e, 0x39, 0xf6, 0x8f, 0x6d, 0x7e, 0xbd, 0x15, 0x7f,
+ 0xc2, 0xb1, 0x33, 0x9d, 0xd0, 0x9d, 0x30, 0xa7, 0x09, 0xef, 0xd6, 0x4d,
+ 0x84, 0xac, 0x55, 0x37, 0x07, 0x7b, 0x4f, 0xbb, 0x65, 0xed, 0x36, 0xf6,
+ 0x8f, 0x32, 0xf7, 0x89, 0xde, 0x7a, 0x7f, 0x2e, 0xfc, 0x59, 0xab, 0xaf,
+ 0xed, 0x09, 0x64, 0x38, 0x96, 0x66, 0xd6, 0xf9, 0x42, 0x90, 0x47, 0xbc,
+ 0x14, 0x04, 0xff, 0xdb, 0x0b, 0x27, 0xb2, 0x0b, 0xe7, 0x78, 0xba, 0x87,
+ 0x0c, 0x39, 0x13, 0xba, 0x02, 0x77, 0xee, 0x8f, 0xb9, 0xc9, 0x10, 0x35,
+ 0x1c, 0xf0, 0xcb, 0x8f, 0x77, 0xce, 0xd8, 0x8e, 0x5c, 0x39, 0x6c, 0x09,
+ 0x82, 0x0b, 0x98, 0x53, 0x8d, 0x88, 0x91, 0xb9, 0x36, 0x4d, 0x26, 0x8c,
+ 0xc2, 0xc9, 0xc9, 0x5c, 0x03, 0xae, 0x56, 0xf0, 0x63, 0x77, 0x38, 0x7c,
+ 0xf4, 0xae, 0xda, 0x54, 0x18, 0x15, 0xa3, 0x0a, 0x59, 0xf9, 0xa0, 0xdd,
+ 0x81, 0xfc, 0x10, 0x29, 0xf9, 0x92, 0x82, 0x5d, 0xff, 0x1f, 0x26, 0x79,
+ 0x84, 0x57, 0x7f, 0x27, 0xaa, 0x37, 0xf9, 0x94, 0x9b, 0xd1, 0x61, 0x70,
+ 0xf2, 0x03, 0xa8, 0x40, 0x4d, 0x44, 0x0b, 0xe5, 0x7f, 0x56, 0xdb, 0x4b,
+ 0xb2, 0xb9, 0xaf, 0x1c, 0x92, 0x01, 0x6e, 0x02, 0xdb, 0xa7, 0x44, 0xa7,
+ 0x4b, 0xb0, 0x28, 0x63, 0xec, 0x5d, 0x33, 0xe5, 0x46, 0x0b, 0x31, 0xad,
+ 0xd9, 0x31, 0x4a, 0x3a, 0xe8, 0x64, 0xed, 0xea, 0x76, 0x65, 0xa7, 0xf3,
+ 0x8d, 0x2c, 0x77, 0xbf, 0xcc, 0x0d, 0xd8, 0xa0, 0x87, 0x22, 0x1e, 0x60,
+ 0x30, 0x1e, 0xf4, 0x94, 0x3c, 0xdd, 0x31, 0x77, 0x61, 0x03, 0x09, 0xa2,
+ 0xac, 0x2d, 0x7c, 0x2b, 0x7b, 0xe3, 0x26, 0x65, 0x5d, 0xf3, 0xc8, 0x43,
+ 0x7f, 0x92, 0xea, 0x98, 0x74, 0xe9, 0x81, 0xc4, 0x89, 0xfa, 0xae, 0x2e,
+ 0xaf, 0x52, 0xa7, 0xd9, 0xb3, 0x88, 0x54, 0x8c, 0x57, 0x1c, 0xec, 0xf0,
+ 0x35, 0x70, 0x44, 0x48, 0x15, 0x13, 0xc8, 0x6e, 0xe2, 0x88, 0xac, 0xfc,
+ 0x2f, 0xff, 0xfe, 0xda, 0xd2, 0x38, 0xc4, 0x2a, 0xd4, 0xe6, 0x9e, 0x2b,
+ 0x26, 0x53, 0x8b, 0xa9, 0xc8, 0x98, 0x7f, 0xb8, 0x14, 0x4f, 0x5d, 0xef,
+ 0xd0, 0xd1, 0x20, 0xf9, 0x49, 0x78, 0xe5, 0x9d, 0xf0, 0x23, 0xa4, 0xc5,
+ 0x1b, 0x05, 0x93, 0x36, 0x66, 0x05, 0x34, 0xb3, 0xc5, 0xe9, 0xe3, 0x8f,
+ 0xad, 0xb0, 0x71, 0x3c, 0x7a, 0xbd, 0x94, 0x84, 0x87, 0xea, 0xb6, 0xb9,
+ 0xf1, 0x69, 0xf7, 0x3c, 0x70, 0x3d, 0x85, 0x35, 0x8d, 0x0b, 0xc6, 0x08,
+ 0x3f, 0x45, 0x97, 0xc5, 0xf8, 0x1b, 0x7a, 0x0c, 0x7e, 0x54, 0xaf, 0xfb,
+ 0x1d, 0xdc, 0x7a, 0x2f, 0x70, 0xc5, 0xa2, 0x6e, 0x33, 0x36, 0x0a, 0xd8,
+ 0xaa, 0x57, 0xa1, 0x51, 0xce, 0xa3, 0x42, 0x65, 0x93, 0x58, 0xa5, 0x81,
+ 0x06, 0x71, 0x4e, 0xf7, 0x88, 0x4e, 0xfa, 0xba, 0xc0, 0xa5, 0x57, 0x78,
+ 0x8c, 0xd8, 0x4c, 0x06, 0xf7, 0x20, 0xf1, 0x38, 0xfb, 0x22, 0xc0, 0x2d,
+ 0x1d, 0xa9, 0xf5, 0x75, 0xf1, 0x5f, 0x5b, 0x29, 0xe5, 0xd1, 0x41, 0xd8,
+ 0x89, 0x07, 0x3d, 0x05, 0xb8, 0xd0, 0xfb, 0xfc, 0x89, 0x99, 0x29, 0x17,
+ 0x80, 0xcc, 0xd3, 0xc3, 0x9e, 0x77, 0x12, 0xf1, 0x54, 0x6c, 0x41, 0xfe,
+ 0x1e, 0x8b, 0xe9, 0x14, 0x99, 0x22, 0x27, 0xdf, 0x04, 0x07, 0x9a, 0x7e,
+ 0xe4, 0xaa, 0xed, 0x48, 0xdd, 0x6d, 0xc3, 0xe8, 0x97, 0xb0, 0xa1, 0x33,
+ 0xb1, 0x0f, 0xde, 0xd9, 0x3f, 0xf1, 0x97, 0x9e, 0xc4, 0xd9, 0x13, 0xd0,
+ 0x09, 0xb7, 0xf1, 0x7f, 0xd5, 0xf3, 0xe6, 0x18, 0x14, 0x9e, 0xcb, 0x50,
+ 0x1e, 0x5d, 0x78, 0x24, 0x37, 0xb0, 0x67, 0x6f, 0x92, 0x18, 0xb1, 0x45,
+ 0xc3, 0x3c, 0x7c, 0xef, 0xe6, 0x43, 0x37, 0xd3, 0x0a, 0x92, 0xcb, 0x07,
+ 0x19, 0x11, 0xba, 0xa3, 0xe7, 0x26, 0xa6, 0xa7, 0x10, 0x0c, 0xb3, 0x16,
+ 0xce, 0x44, 0x81, 0x52, 0x3c, 0x13, 0xc6, 0xd7, 0xf6, 0x81, 0x5e, 0xa1,
+ 0x30, 0x95, 0xac, 0xce, 0x3b, 0x08, 0xbf, 0xff, 0x10, 0xc9, 0x08, 0xe6,
+ 0x95, 0x46, 0x28, 0x9b, 0x68, 0xa4, 0xc0, 0x01, 0xcb, 0x6d, 0x48, 0x53,
+ 0x7f, 0x31, 0xde, 0x25, 0xa0, 0x60, 0xd1, 0x22, 0x9a, 0x71, 0xf7, 0xf4,
+ 0x15, 0x37, 0x6a, 0x8e, 0x05, 0x26, 0x4b, 0x12, 0xe5, 0x67, 0x42, 0x7c,
+ 0x11, 0xc4, 0xd9, 0x79, 0xcc, 0x7c, 0x37, 0x7c, 0x82, 0x81, 0x3d, 0x54,
+ 0xc1, 0xff, 0xdd, 0x75, 0x9b, 0x10, 0x6a, 0x93, 0x46, 0xfa, 0x6b, 0x44,
+ 0xa8, 0x4b, 0x17, 0x93, 0x38, 0x95, 0xd6, 0xc7, 0x1b, 0x50, 0x64, 0x81,
+ 0x7e, 0x5e, 0x51, 0x93, 0xbe, 0x8a, 0x11, 0x46, 0x20, 0xfa, 0xc8, 0x32,
+ 0xd0, 0x78, 0x23, 0xa9, 0xb0, 0xba, 0xc4, 0xcd, 0x5c, 0xf3, 0xd8, 0xff,
+ 0xc0, 0x52, 0xe6, 0x3b, 0x99, 0xd1, 0xd4, 0x5d, 0xd1, 0x26, 0xfc, 0x8a,
+ 0x6a, 0x50, 0x50, 0x31, 0x8a, 0xd5, 0xbe, 0x8f, 0xd7, 0xc3, 0x66, 0xcb,
+ 0x6f, 0x8a, 0x92, 0x6d, 0x9e, 0x93, 0x92, 0xb4, 0xbf, 0x96, 0xf9, 0x08,
+ 0x7f, 0xe0, 0xa8, 0x0a, 0xc8, 0x4e, 0xb5, 0x70, 0x34, 0x08, 0xaa, 0xf4,
+ 0x1d, 0x3b, 0xef, 0x27, 0xab, 0x5a, 0xe7, 0xa7, 0x92, 0x65, 0x29, 0xaa,
+ 0x8d, 0x41, 0x42, 0x75, 0x35, 0x57, 0xcb, 0x0a, 0x39, 0x86, 0x04, 0x90,
+ 0xf3, 0xa2, 0x34, 0x1a, 0xfa, 0xd5, 0x48, 0x12, 0x46, 0x47, 0xf9, 0xd7,
+ 0xda, 0xda, 0x5e, 0xe8, 0xef, 0x0e, 0x1f, 0x1e, 0xc3, 0xe9, 0xd1, 0x9d,
+ 0xf8, 0x61, 0x99, 0xdf, 0x08, 0x70, 0x88, 0x0c, 0xf3, 0x9f, 0x74, 0x65,
+ 0x68, 0xd5, 0xf5, 0xe7, 0x4f, 0xc8, 0xc7, 0x79, 0x69, 0x75, 0x6b, 0xd9,
+ 0x13, 0xb4, 0x31, 0x88, 0x32, 0xd7, 0xba, 0xab, 0xe8, 0x4e, 0x77, 0x9e,
+ 0xb2, 0x9e, 0x68, 0xc5, 0xac, 0x7e, 0xca, 0xd7, 0x05, 0xd1, 0x9b, 0x3d,
+ 0x63, 0x6f, 0x55, 0xcc, 0xe6, 0x39, 0x09, 0x13, 0xe7, 0xfd, 0x99, 0x34,
+ 0x1f, 0x8e, 0xc4, 0x7f, 0x95, 0x5d, 0x79, 0x22, 0x3a, 0xc5, 0x23, 0x1c,
+ 0x39, 0xbe, 0x76, 0x7b, 0x37, 0x41, 0x0e, 0xcc, 0xd2, 0xc7, 0xc1, 0x9c,
+ 0x28, 0x9a, 0x29, 0xf9, 0x8c, 0x5a, 0x98, 0x4a, 0x13, 0xa5, 0x6c, 0xc6,
+ 0x77, 0x38, 0xb8, 0x31, 0x6c, 0x8f, 0xdc, 0x94, 0x4b, 0x1c, 0x1c, 0x7d,
+ 0x53, 0xad, 0xd4, 0x57, 0x4d, 0x6e, 0x90, 0x30, 0x83, 0x21, 0xf4, 0x4e,
+ 0x65, 0x77, 0x60, 0x8b, 0xf1, 0x2c, 0x66, 0x01, 0xcf, 0x58, 0x9f, 0x43,
+ 0x71, 0x76, 0x25, 0xe5, 0x9e, 0x09, 0x94, 0x5f, 0xca, 0x19, 0xe3, 0xe3,
+ 0x89, 0x44, 0xd6, 0x45, 0xe1, 0x92, 0x3b, 0x8d, 0xae, 0x38, 0xfe, 0x65,
+ 0x65, 0xf8, 0x48, 0xcc, 0x5d, 0x7d, 0xed, 0xd6, 0xe1, 0x47, 0x4b, 0x3a,
+ 0xec, 0xe3, 0x2d, 0xe9, 0x10, 0x8f, 0xa6, 0x78, 0xf9, 0x1c, 0xfe, 0x89,
+ 0x0d, 0x1c, 0x19, 0x45, 0x52, 0xb6, 0x02, 0x32, 0x92, 0x25, 0xc8, 0x88,
+ 0x8a, 0x9c, 0xc0, 0xcf, 0x9b, 0x27, 0x1c, 0x43, 0xb9, 0x04, 0xbc, 0x02,
+ 0xac, 0x34, 0xef, 0x6d, 0x7a, 0x5f, 0x58, 0x74, 0x1b, 0xa9, 0x98, 0x3e,
+ 0x92, 0x0f, 0x26, 0x11, 0x75, 0x9f, 0x45, 0xa9, 0xe1, 0x5c, 0x37, 0xbe,
+ 0xfd, 0xd7, 0x28, 0xcd, 0x72, 0xdc, 0xc6, 0x9b, 0x2c, 0xaa, 0x23, 0xb6,
+ 0x8e, 0x18, 0x0d, 0x8b, 0xe7, 0x1a, 0xe4, 0xba, 0x72, 0x0c, 0x75, 0xa8,
+ 0x04, 0xed, 0xbe, 0xce, 0xd4, 0xbd, 0x2e, 0xd5, 0x74, 0xd5, 0x46, 0xa1,
+ 0x27, 0xdf, 0x49, 0x7d, 0x4f, 0xf2, 0x93, 0x08, 0xbb, 0x18, 0x6f, 0xc8,
+ 0xde, 0x99, 0x50, 0x5a, 0x10, 0x6f, 0xaf, 0xbb, 0xb6, 0xc3, 0x10, 0x41,
+ 0xc1, 0xe4, 0xe4, 0x3c, 0x83, 0xdb, 0xee, 0x0d, 0xc0, 0x50, 0xe4, 0x93,
+ 0x1d, 0xd7, 0x9d, 0x7c, 0x6b, 0xc0, 0x62, 0xf1, 0x34, 0x39, 0xaa, 0xef,
+ 0xd9, 0x3c, 0x21, 0x2e, 0x78, 0xd3, 0x27, 0x3f, 0x74, 0x3d, 0x97, 0xa0,
+ 0x76, 0x0a, 0xdc, 0x91, 0xb1, 0x25, 0x71, 0x70, 0x83, 0xef, 0xbb, 0x7d,
+ 0x4d, 0xcb, 0xea, 0x63, 0xd9, 0x45, 0x9c, 0x4e, 0xca, 0x74, 0x9f, 0x68,
+ 0xa1, 0x3d, 0x51, 0x28, 0x94, 0x8f, 0x2a, 0xcc, 0x8c, 0x16, 0x22, 0x1e,
+ 0x8e, 0x0a, 0xcc, 0xe8, 0x82, 0x8e, 0x1d, 0xdd, 0xfe, 0x6f, 0x53, 0x5e,
+ 0xae, 0x05, 0xf4, 0x06, 0xb3, 0xe9, 0x2d, 0xa9, 0x35, 0xab, 0x97, 0xe0,
+ 0xaf, 0x85, 0xee, 0x08, 0xdc, 0x9a, 0x95, 0xce, 0x4d, 0x66, 0x1e, 0x47,
+ 0xdf, 0x7c, 0x5f, 0x99, 0x61, 0xad, 0x01, 0x0f, 0xfa, 0xf1, 0x92, 0x32,
+ 0x12, 0x7d, 0xa8, 0xf5, 0xb1, 0x8a, 0xfb, 0x0b, 0x32, 0xa9, 0xe5, 0x84,
+ 0x52, 0x50, 0xcd, 0x79, 0x01, 0x56, 0xf2, 0x2e, 0xba, 0xda, 0x8b, 0x70,
+ 0xd6, 0x05, 0x72, 0xee, 0xef, 0x2c, 0xbd, 0x26, 0x84, 0x34, 0x8a, 0x9d,
+ 0xcc, 0x02, 0x74, 0x18, 0x20, 0x27, 0xff, 0x0b, 0x2d, 0x38, 0x36, 0x0a,
+ 0x2f, 0xfd, 0xc6, 0xd8, 0x30, 0x4f, 0x51, 0x74, 0x78, 0xbf, 0x52, 0xff,
+ 0xee, 0x9f, 0xe7, 0xeb, 0xfc, 0xf9, 0xef, 0x69, 0x3c, 0xc5, 0xa6, 0xcc,
+ 0x14, 0x9a, 0x1d, 0x83, 0x94, 0xf6, 0x58, 0xd6, 0x0e, 0xe8, 0x51, 0xf7,
+ 0xce, 0x17, 0xcf, 0x23, 0x66, 0x9d, 0x1c, 0x17, 0x63, 0x2e, 0xcf, 0x3b,
+ 0x6c, 0xcb, 0xb9, 0x33, 0x03, 0x11, 0xc7, 0x3f, 0x4b, 0x8d, 0xee, 0xf8,
+ 0xe3, 0xf9, 0x49, 0x81, 0x5c, 0xb0, 0xb4, 0x3f, 0xd8, 0x3c, 0xe6, 0x6f,
+ 0x9d, 0xad, 0x2a, 0xc0, 0x24, 0xb1, 0xcf, 0x63, 0xdd, 0xc6, 0x32, 0x5f,
+ 0x14, 0x92, 0x34, 0x94, 0xd1, 0x2a, 0xb2, 0xfa, 0x96, 0x80, 0xe8, 0xf8,
+ 0xdc, 0x4b, 0x5b, 0x40, 0x05, 0x90, 0x21, 0x33, 0xd3, 0x3c, 0x34, 0x71,
+ 0x52, 0xfc, 0x07, 0x7b, 0x86, 0x5d, 0xbb, 0xca, 0x6d, 0xde, 0xad, 0x57,
+ 0x79, 0xaa, 0x21, 0x8e, 0x1e, 0x32, 0xc5, 0xce, 0xb8, 0x5f, 0xa8, 0xac,
+ 0x83, 0x62, 0x97, 0xde, 0xfc, 0x98, 0x67, 0x58, 0xe2, 0xd3, 0x41, 0xc5,
+ 0x5e, 0x7a, 0x22, 0x74, 0x12, 0x76, 0xca, 0xb4, 0x32, 0xb4, 0xb5, 0xe7,
+ 0x57, 0x36, 0x73, 0x82, 0xd1, 0x5c, 0x8f, 0xc9, 0x9b, 0x97, 0x05, 0x2b,
+ 0x79, 0x79, 0xed, 0xeb, 0x05, 0x3d, 0x07, 0xb7, 0xa8, 0x87, 0xab, 0x0e,
+ 0x63, 0x0f, 0x04, 0xf9, 0xa8, 0x38, 0xae, 0x9d, 0xcd, 0xd6, 0x47, 0xa3,
+ 0xab, 0xf9, 0x7d, 0x18, 0x3d, 0xa3, 0x06, 0x4f, 0x64, 0x9a, 0x17, 0x46,
+ 0x54, 0x24, 0xb3, 0xca, 0xa8, 0xb4, 0x3d, 0x4f, 0x67, 0x1f, 0x4a, 0xe5,
+ 0x4a, 0x5b, 0xf5, 0x1e, 0xd1, 0x3e, 0x3b, 0x0c, 0x5f, 0xc6, 0x88, 0x6d,
+ 0xd8, 0xc4, 0xb8, 0x34, 0x6b, 0xe9, 0xd0, 0xa4, 0x6d, 0xd2, 0x4d, 0x47,
+ 0x22, 0x15, 0xfe, 0x80, 0x53, 0xf4, 0xc4, 0x32, 0x87, 0xe4, 0x2b, 0x8c,
+ 0x7f, 0x5f, 0x00, 0x28, 0xa7, 0x03, 0x90, 0xb0, 0x9d, 0x18, 0x72, 0x05,
+ 0xc7, 0xcc, 0xf7, 0xb8, 0xd3, 0xfb, 0x01, 0xc8, 0xb8, 0x77, 0x9f, 0xae,
+ 0x4a, 0x36, 0xcd, 0x0e, 0x96, 0x79, 0x70, 0x71, 0x05, 0x4e, 0x00, 0x93,
+ 0x9d, 0x83, 0x8c, 0xe5, 0x0a, 0x9d, 0xb8, 0xcf, 0xad, 0xfd, 0xda, 0x69,
+ 0x0f, 0xf1, 0xb7, 0x3d, 0xa3, 0xfe, 0xcb, 0x71, 0xbf, 0x85, 0x66, 0x51,
+ 0x80, 0x37, 0xe7, 0x08, 0xf8, 0x91, 0xda, 0xfa, 0x93, 0x3f, 0xa1, 0xcc,
+ 0x2c, 0x91, 0x58, 0x36, 0x72, 0x6e, 0xdf, 0x7f, 0x1c, 0x8b, 0x08, 0x1b,
+ 0xeb, 0x13, 0x06, 0xcd, 0x36, 0x30, 0xc8, 0x96, 0x27, 0xc4, 0xb1, 0x33,
+ 0xce, 0x8b, 0xc6, 0x51, 0x1a, 0x55, 0x64, 0xfa, 0xd5, 0xd9, 0x7c, 0xc5,
+ 0xb5, 0x8b, 0x46, 0xbf, 0x8e, 0xed, 0x95, 0x2f, 0xb6, 0x81, 0xf1, 0x55,
+ 0x62, 0x52, 0xee, 0x97, 0x9b, 0xec, 0x2d, 0xf8, 0x7a, 0x9f, 0xc5, 0x7a,
+ 0xee, 0xc3, 0x10, 0xda, 0xf1, 0x67, 0x96, 0x78, 0xee, 0x37, 0x4a, 0x18,
+ 0xd8, 0xa9, 0xbd, 0x4a, 0x58, 0x75, 0x2a, 0xdd, 0xb6, 0xf2, 0x7f, 0xaf,
+ 0xff, 0xd6, 0x6d, 0x19, 0xe0, 0x7d, 0xb0, 0x2c, 0x9c, 0x69, 0x24, 0xb9,
+ 0x9b, 0x17, 0x53, 0xe5, 0x73, 0x99, 0xaf, 0xb7, 0xa2, 0x4d, 0x4f, 0xc0,
+ 0xcf, 0x7c, 0xee, 0x6e, 0x2e, 0xbe, 0xbb, 0xbc, 0x87, 0x04, 0x6a, 0xb8,
+ 0xa6, 0xba, 0xa1, 0x85, 0x87, 0x2e, 0xbe, 0x34, 0xd8, 0xa8, 0x28, 0x36,
+ 0x4b, 0x42, 0xbb, 0x56, 0x49, 0xda, 0x18, 0xf7, 0xd1, 0xb2, 0x3f, 0x1f,
+ 0x03, 0x66, 0x16, 0x5c, 0xd7, 0x65, 0x09, 0x91, 0x5f, 0xd2, 0x02, 0xa8,
+ 0x90, 0x14, 0x2c, 0x85, 0x51, 0x3b, 0x81, 0x46, 0x6f, 0x27, 0xd1, 0xc8,
+ 0x84, 0xc5, 0x4e, 0x0b, 0xc9, 0xd1, 0x2d, 0x02, 0xcd, 0x26, 0x26, 0x6f,
+ 0xa1, 0xfd, 0x13, 0xb4, 0x03, 0x1f, 0xa9, 0x53, 0x54, 0x29, 0xcd, 0x99,
+ 0x09, 0x63, 0x1a, 0x8a, 0x9d, 0x21, 0x82, 0x3a, 0x27, 0xe4, 0x8f, 0xfd,
+ 0xfd, 0x11, 0x14, 0xf0, 0x7d, 0xe7, 0x42, 0xee, 0x5b, 0x4a, 0x88, 0x8c,
+ 0x0a, 0x2b, 0xca, 0xa1, 0xea, 0x83, 0x69, 0xa9, 0xb8, 0xcc, 0x99, 0x1b,
+ 0x1b, 0x96, 0x86, 0x23, 0x8d, 0x92, 0xd3, 0xef, 0x55, 0x71, 0x1c, 0x9e,
+ 0x90, 0x4e, 0x34, 0x8b, 0x5c, 0xc7, 0x5a, 0x6d, 0x7e, 0xf8, 0xc9, 0x96,
+ 0x12, 0x5a, 0x41, 0x21, 0xf7, 0xcb, 0x0d, 0x9c, 0x3f, 0xf5, 0xff, 0xee,
+ 0x0c, 0xee, 0x66, 0x8a, 0x57, 0x8e, 0x3c, 0xc8, 0xf3, 0xdc, 0x06, 0x64,
+ 0x2b, 0x2f, 0xd2, 0x4c, 0x4b, 0xdb, 0x57, 0xd7, 0x3d, 0xde, 0xe2, 0xa3,
+ 0x53, 0xb0, 0x59, 0x4a, 0x54, 0x57, 0x22, 0x58, 0x28, 0x93, 0xf2, 0x4f,
+ 0x5d, 0xc9, 0x39, 0x88, 0xf5, 0xfe, 0x2e, 0x63, 0x0c, 0x13, 0xd4, 0x74,
+ 0xe7, 0x99, 0xa5, 0x3b, 0x19, 0x3b, 0x47, 0x4c, 0x0d, 0x53, 0xf9, 0x78,
+ 0x02, 0xf1, 0xce, 0xc0, 0x2e, 0x98, 0x44, 0xbd, 0x9b, 0xf0, 0x9e, 0x54,
+ 0x94, 0x6e, 0x85, 0x7a, 0xa4, 0x40, 0x4c, 0xe6, 0x71, 0x90, 0x28, 0x92,
+ 0x4e, 0x22, 0x9d, 0x5c, 0xec, 0x71, 0x69, 0xc7, 0xe3, 0x2f, 0x7c, 0x85,
+ 0x64, 0x69, 0x4b, 0xa8, 0x35, 0x9b, 0x00, 0x87, 0xeb, 0xa0, 0xfe, 0xaf,
+ 0xf8, 0x02, 0x61, 0x29, 0x01, 0xd8, 0x68, 0xce, 0x27, 0x83, 0x54, 0x47,
+ 0x53, 0xa9, 0xc3, 0x5e, 0x22, 0x36, 0xec, 0xbd, 0x60, 0xe1, 0xe3, 0xf1,
+ 0x35, 0x73, 0x90, 0x66, 0x2d, 0xc2, 0x86, 0xf0, 0x43, 0xc6, 0x7d, 0x18,
+ 0x9d, 0xab, 0xe6, 0xed, 0xdc, 0xcf, 0x48, 0x40, 0x44, 0xba, 0x80, 0x78,
+ 0x96, 0xd7, 0xe1, 0x10, 0xe1, 0x41, 0xa9, 0x21, 0x23, 0x50, 0xfc, 0x81,
+ 0x14, 0xe1, 0xb9, 0x49, 0xf8, 0xbc, 0x13, 0x4f, 0x3b, 0x39, 0x44, 0x22,
+ 0xf8, 0x0c, 0x36, 0x33, 0x35, 0xe0, 0x84, 0x75, 0x2e, 0x3e, 0x4e, 0x6b,
+ 0xd7, 0x3c, 0xc3, 0x67, 0x51, 0x7d, 0x3a, 0xb3, 0x06, 0x32, 0x60, 0x4b,
+ 0x61, 0x00, 0xaf, 0x15, 0xdd, 0xc2, 0xea, 0x69, 0x6f, 0xa9, 0x79, 0x4e,
+ 0x45, 0x3b, 0x52, 0xae, 0x5d, 0xfa, 0x7b, 0x38, 0xce, 0xed, 0x56, 0x14,
+ 0xc2, 0x58, 0xdb, 0xcf, 0x15, 0x04, 0xe4, 0xb3, 0x57, 0x1f, 0x83, 0x00,
+ 0xcc, 0x07, 0x4d, 0x3c, 0x55, 0xf5, 0x24, 0xe4, 0x97, 0x32, 0xd9, 0x54,
+ 0x0b, 0x58, 0xb2, 0x3e, 0xe1, 0x24, 0x8a, 0x32, 0xc2, 0x9f, 0x77, 0x1c,
+ 0x57, 0x5a, 0x19, 0x4d, 0x2d, 0x71, 0x72, 0xd9, 0xd7, 0x28, 0xde, 0xa4,
+ 0xf8, 0xa5, 0x07, 0x4a, 0x6a, 0xcc, 0x6a, 0x51, 0x7f, 0xca, 0x1c, 0x24,
+ 0xa4, 0x31, 0xc1, 0x81, 0x2a, 0x14, 0xa4, 0x47, 0x3a, 0x86, 0x80, 0x43,
+ 0xf1, 0x46, 0x32, 0xc0, 0x86, 0x01, 0xfa, 0x97, 0x08, 0x76, 0xd9, 0x63,
+ 0x64, 0x9f, 0x75, 0xc1, 0x1c, 0xe6, 0x6d, 0xec, 0x28, 0x48, 0xd0, 0xa4,
+ 0x11, 0x03, 0xc2, 0x36, 0x62, 0xe8, 0xaf, 0xcb, 0x7d, 0x64, 0xf7, 0xe9,
+ 0xbd, 0x03, 0x1b, 0x62, 0xe4, 0x5d, 0x2d, 0x79, 0x7d, 0xd6, 0xa6, 0xee,
+ 0xca, 0x9e, 0x99, 0xec, 0x43, 0xfd, 0xac, 0xa1, 0x7a, 0xb4, 0xbe, 0xbb,
+ 0x73, 0x13, 0x13, 0xaf, 0xbc, 0x86, 0xf8, 0x53, 0x3e, 0xbd, 0xb0, 0x2c,
+ 0x05, 0x14, 0xc6, 0xb7, 0xbb, 0xc7, 0x4b, 0x36, 0x24, 0xde, 0x6c, 0x38,
+ 0xbf, 0xd5, 0x7a, 0xbe, 0x0b, 0x7b, 0xd6, 0x2b, 0xea, 0x5a, 0x14, 0x09,
+ 0x0c, 0x2b, 0xa5, 0xda, 0xae, 0xe3, 0x42, 0xde, 0x5e, 0x49, 0x73, 0x3d,
+ 0x3f, 0x99, 0xa1, 0x33, 0x76, 0x25, 0x7a, 0x2c, 0x3f, 0xdf, 0x5a, 0x95,
+ 0x78, 0x79, 0x32, 0x55, 0xfe, 0xc4, 0xec, 0x2a, 0x61, 0x3c, 0xc8, 0x86,
+ 0xba, 0xc0, 0x63, 0x90, 0xfe, 0x6f, 0x5b, 0xc9, 0x7f, 0x22, 0xe7, 0xbf,
+ 0x8a, 0x8d, 0xab, 0x42, 0x1f, 0x54, 0x1b, 0x5d, 0x84, 0xc9, 0x58, 0xe2,
+ 0x7c, 0x24, 0x54, 0x09, 0x44, 0x3e, 0x7f, 0x64, 0xa7, 0x5c, 0x5f, 0xf3,
+ 0x31, 0x68, 0x0d, 0x17, 0xb5, 0xaf, 0x5b, 0xee, 0xd8, 0x4c, 0x65, 0xe5,
+ 0x46, 0xfc, 0x4f, 0xd2, 0x8b, 0xe9, 0x0c, 0x8f, 0xbd, 0x48, 0xe7, 0x89,
+ 0x07, 0x9b, 0x18, 0x03, 0x2b, 0x74, 0x3d, 0xbb, 0xea, 0x2c, 0x53, 0x49,
+ 0xad, 0x29, 0xc5, 0x84, 0x8e, 0x79, 0x2e, 0xd2, 0x42, 0xc9, 0xbc, 0x86,
+ 0x53, 0x42, 0x7c, 0xee, 0x7b, 0xc0, 0xb2, 0x6d, 0x24, 0xd9, 0x25, 0x20,
+ 0xb0, 0x7d, 0xf1, 0x0f, 0xe3, 0xe7, 0xad, 0xb4, 0x6e, 0x09, 0xea, 0x6c,
+ 0x5e, 0x93, 0x68, 0xb3, 0x3c, 0x27, 0xaf, 0xe9, 0x7e, 0xac, 0x9c, 0x02,
+ 0x1c, 0x36, 0xae, 0x4d, 0xbf, 0x7a, 0x5a, 0x1d, 0x28, 0x17, 0xb7, 0x6b,
+ 0x8c, 0x15, 0xed, 0xca, 0xb7, 0x59, 0xa8, 0x2b, 0x77, 0x61, 0x94, 0x58,
+ 0x98, 0xdf, 0x1f, 0xa9, 0x66, 0x06, 0x48, 0xce, 0xd5, 0x27, 0xd0, 0x16,
+ 0x57, 0xb0, 0xe4, 0x4d, 0xdf, 0xd8, 0x91, 0x42, 0xe8, 0x3b, 0x33, 0xb0,
+ 0xdb, 0xdf, 0xc8, 0xd5, 0x0f, 0xeb, 0xea, 0xc1, 0xfe, 0xa3, 0xc7, 0x07,
+ 0xb2, 0x23, 0x76, 0x5b, 0x83, 0xf5, 0x39, 0x88, 0xf1, 0x45, 0x24, 0xdf,
+ 0x90, 0xd8, 0x08, 0xb5, 0x7f, 0x17, 0x4d, 0xc2, 0x1b, 0xe8, 0xaa, 0xb5,
+ 0x9d, 0xfd, 0xf6, 0xbe, 0x50, 0x86, 0x1c, 0x3c, 0xba, 0x68, 0x98, 0xd6,
+ 0x95, 0x5c, 0x49, 0xb8, 0x79, 0x9b, 0x0d, 0xe0, 0xa7, 0x5a, 0xe3, 0x0d,
+ 0x28, 0x93, 0x16, 0xa6, 0x85, 0x23, 0x27, 0x4e, 0x10, 0x68, 0x18, 0xad,
+ 0x95, 0xb3, 0x57, 0xd0, 0xbd, 0xbe, 0x25, 0xd8, 0x2a, 0x15, 0x3b, 0x56,
+ 0x48, 0x16, 0x1b, 0x7d, 0x50, 0x09, 0xd4, 0xcd, 0x8d, 0xb8, 0x7a, 0x85,
+ 0x13, 0xbe, 0x36, 0x49, 0xa5, 0x25, 0x10, 0xdf, 0xc3, 0x08, 0xbc, 0x64,
+ 0x2c, 0x97, 0x38, 0x8d, 0x69, 0x8c, 0xb9, 0x5c, 0xe9, 0x07, 0x88, 0x5f,
+ 0xdd, 0xf5, 0x48, 0x74, 0x49, 0xd8, 0x88, 0xac, 0x7f, 0xdb, 0x0d, 0x22,
+ 0xac, 0x55, 0x40, 0x18, 0x7c, 0x04, 0x08, 0x7d, 0x05, 0x81, 0x71, 0x0f,
+ 0x65, 0x8f, 0x3b, 0x5f, 0x34, 0x8c, 0xbd, 0xbe, 0x7a, 0xf1, 0xf8, 0x48,
+ 0x67, 0xb1, 0x74, 0x8b, 0x37, 0xf4, 0xfd, 0x1a, 0x9f, 0x9b, 0x4a, 0x57,
+ 0xbd, 0x81, 0xf4, 0xf8, 0x56, 0x84, 0x92, 0x24, 0x96, 0x5c, 0x9a, 0x06,
+ 0x71, 0x4e, 0x2d, 0x61, 0xa2, 0x52, 0x6c, 0x96, 0x07, 0x19, 0x05, 0x02,
+ 0x58, 0xe6, 0x24, 0x42, 0x21, 0x5f, 0x0f, 0x0a, 0x92, 0x89, 0x6f, 0xf8,
+ 0x49, 0xa3, 0xd1, 0xf9, 0xd2, 0x22, 0xe4, 0x07, 0x76, 0xff, 0xcb, 0xd6,
+ 0xa3, 0xdd, 0x58, 0x50, 0x59, 0x18, 0x67, 0x8f, 0x68, 0xdd, 0xd4, 0x68,
+ 0x0e, 0x2a, 0x58, 0xb9, 0x2d, 0xb1, 0xa7, 0xf6, 0x4d, 0x41, 0x70, 0x39,
+ 0x23, 0xe5, 0x99, 0x4c, 0x05, 0x2f, 0x04, 0xb5, 0x6f, 0xfc, 0xc1, 0xf1,
+ 0xac, 0x2b, 0xf4, 0x02, 0x22, 0xce, 0x46, 0xa1, 0x38, 0xef, 0x50, 0xd1,
+ 0x6b, 0x1f, 0x34, 0xb5, 0x0c, 0x6a, 0xb3, 0x30, 0xe6, 0xa3, 0x91, 0x82,
+ 0x9a, 0x2d, 0x3c, 0x8f, 0xf6, 0x76, 0x97, 0xe9, 0xb7, 0x92, 0xb7, 0xb1,
+ 0x1e, 0x19, 0x97, 0xad, 0x1c, 0x6d, 0xd4, 0xe6, 0x2c, 0x5d, 0xe8, 0x33,
+ 0xf2, 0xc0, 0x17, 0xa2, 0x20, 0xdd, 0x0f, 0x5c, 0x32, 0xe2, 0x4c, 0x63,
+ 0xaa, 0x2e, 0x36, 0xf6, 0xf0, 0xde, 0x40, 0x41, 0x6d, 0xa8, 0xcb, 0x13,
+ 0xd4, 0xd9, 0xc4, 0xfb, 0xb3, 0xb3, 0xd6, 0xf2, 0xed, 0x24, 0x00, 0x1a,
+ 0xf8, 0xfa, 0xc9, 0x70, 0x60, 0x40, 0x72, 0x59, 0xea, 0x3f, 0x17, 0x04,
+ 0x46, 0xec, 0x5f, 0x9b, 0xab, 0x1f, 0x99, 0x9f, 0x46, 0x61, 0x79, 0x4c,
+ 0x6f, 0x94, 0xb4, 0xae, 0x8f, 0x81, 0xc6, 0xf1, 0x7a, 0x79, 0x4e, 0x8f,
+ 0xa5, 0x03, 0x92, 0xf7, 0xbb, 0xed, 0x91, 0x40, 0xff, 0x5e, 0xa2, 0xd4,
+ 0x32, 0x76, 0x2e, 0x25, 0xc6, 0xce, 0xc8, 0x64, 0x9f, 0xe1, 0x26, 0x81,
+ 0xe6, 0xd6, 0xbd, 0x80, 0xda, 0xc9, 0xc2, 0x1c, 0x9a, 0x5d, 0x5e, 0xce,
+ 0x51, 0x14, 0x79, 0x40, 0x04, 0x48, 0x57, 0x48, 0x13, 0xff, 0xdf, 0x81,
+ 0x10, 0xf5, 0x46, 0x0c, 0x23, 0xc6, 0xfa, 0x7c, 0xc3, 0x59, 0x34, 0x26,
+ 0x47, 0x16, 0x8a, 0x1b, 0x89, 0xbe, 0x34, 0x08, 0xe9, 0x45, 0x65, 0xff,
+ 0x4d, 0xc2, 0xf1, 0xfa, 0x4a, 0xf4, 0x72, 0x37, 0x9c, 0x9e, 0x57, 0x0b,
+ 0x0d, 0x57, 0x56, 0x84, 0xe9, 0xcb, 0xe4, 0x5c, 0xa1, 0x50, 0x77, 0x28,
+ 0x36, 0x82, 0xcf, 0x20, 0xee, 0x92, 0x0b, 0xe3, 0xba, 0x1e, 0x2a, 0x8b,
+ 0xa8, 0x72, 0x20, 0xc8, 0x80, 0xcb, 0x0d, 0x45, 0x34, 0x52, 0x10, 0xc3,
+ 0xe8, 0x33, 0x0b, 0xa5, 0xa3, 0x33, 0x90, 0xb2, 0xe0, 0xf5, 0x88, 0xfe,
+ 0xf7, 0xf3, 0xca, 0x3d, 0x32, 0x08, 0xd6, 0xa8, 0x41, 0x74, 0x8d, 0xcd,
+ 0x3b, 0x9b, 0xae, 0x97, 0x49, 0xe4, 0x60, 0x54, 0xd2, 0x96, 0x2c, 0xba,
+ 0xb6, 0x43, 0x8f, 0xb6, 0x40, 0x07, 0xf7, 0x5b, 0x64, 0xb1, 0x1a, 0x5b,
+ 0xdd, 0x96, 0xbf, 0x2d, 0x68, 0xe8, 0x3c, 0xe3, 0x93, 0xe6, 0x7e, 0x39,
+ 0xf6, 0xb2, 0x2e, 0x2c, 0xb8, 0xf1, 0x27, 0x3b, 0x91, 0xae, 0x01, 0x5f,
+ 0x4a, 0x4b, 0xd8, 0xf4, 0xee, 0xa9, 0x63, 0xf4, 0xb2, 0xd3, 0xef, 0x3a,
+ 0xcd, 0x40, 0x0e, 0x74, 0x11, 0xb0, 0xc5, 0x39, 0x46, 0xa7, 0x76, 0x38,
+ 0x98, 0xb7, 0xd5, 0x12, 0x93, 0x27, 0xd9, 0x0d, 0xae, 0xbd, 0x72, 0xde,
+ 0x28, 0xe7, 0x85, 0xd2, 0x17, 0x50, 0xc8, 0x84, 0x27, 0xe7, 0x3b, 0x6b,
+ 0x2b, 0x91, 0xcc, 0x36, 0x58, 0x73, 0xba, 0xe9, 0xe2, 0x16, 0x9d, 0xd0,
+ 0x43, 0x23, 0xf8, 0xf7, 0x3b, 0x4e, 0x63, 0x27, 0x9b, 0x85, 0xc3, 0x3d,
+ 0x4b, 0x98, 0xa9, 0x98, 0xb7, 0x4b, 0x62, 0x7c, 0xd8, 0x38, 0x89, 0xf3,
+ 0x78, 0x11, 0xed, 0x59, 0x99, 0xa9, 0x3a, 0x18, 0x59, 0xd4, 0xfc, 0x20,
+ 0xdd, 0x97, 0xe4, 0x91, 0x1b, 0xd6, 0x68, 0x87, 0xc4, 0x1d, 0xfb, 0x88,
+ 0xc0, 0x4f, 0x62, 0x3f, 0x99, 0x6d, 0xaf, 0xee, 0x9c, 0xd9, 0xb1, 0x31,
+ 0x08, 0x05, 0x45, 0x27, 0xee, 0xc0, 0x15, 0xb5, 0x53, 0xd6, 0xee, 0xa4,
+ 0x6f, 0x26, 0x82, 0x91, 0xba, 0xec, 0xff, 0xa2, 0x44, 0x3a, 0x8e, 0x73,
+ 0xb0, 0x92, 0xc9, 0xf2, 0xed, 0xd9, 0xb5, 0xfd, 0x5e, 0x0a, 0x50, 0x74,
+ 0xca, 0x63, 0x61, 0x89, 0x58, 0xb1, 0xfd, 0xa3, 0xfe, 0xe8, 0x5b, 0x37,
+ 0x8e, 0x92, 0x38, 0xde, 0xf2, 0xaa, 0x28, 0x25, 0x29, 0x4e, 0xe5, 0x32,
+ 0x43, 0xd8, 0x60, 0x25, 0xd6, 0x17, 0x3f, 0xbb, 0xa1, 0x20, 0x3e, 0x05,
+ 0x6b, 0xbe, 0x1c, 0x6a, 0x67, 0x6e, 0xa8, 0xf5, 0xd9, 0xe0, 0x98, 0x6e,
+ 0x5b, 0x9d, 0x53, 0x1e, 0x17, 0x64, 0x08, 0x38, 0x9a, 0xc4, 0xc5, 0x6b,
+ 0xeb, 0x8c, 0x02, 0x46, 0xc4, 0x35, 0xb2, 0xd5, 0x9d, 0x5d, 0x3a, 0x6a,
+ 0x69, 0x98, 0x1a, 0x7b, 0x36, 0x7b, 0x74, 0xa0, 0xb0, 0x4e, 0x40, 0x1a,
+ 0xd7, 0x56, 0xb5, 0x97, 0x96, 0xd7, 0xbc, 0x9b, 0x65, 0x83, 0xa6, 0xa3,
+ 0xb3, 0x11, 0x57, 0x9c, 0x4b, 0xd1, 0xd9, 0x5e, 0xe0, 0x5d, 0x35, 0xcf,
+ 0xdd, 0x9d, 0x73, 0xbb, 0x63, 0x93, 0x2e, 0x9e, 0xc2, 0x4b, 0xbb, 0xd9,
+ 0x88, 0xbe, 0xa0, 0xc4, 0xaf, 0xc5, 0xe2, 0x9a, 0xe1, 0xa8, 0x34, 0xd4,
+ 0xa9, 0xe7, 0xae, 0x38, 0x2e, 0x76, 0x6e, 0xea, 0xd2, 0xfa, 0x16, 0x74,
+ 0xc0, 0x6b, 0xec, 0xaf, 0x0f, 0x9f, 0x62, 0xd3, 0x37, 0xec, 0x8c, 0xf3,
+ 0xdd, 0x0c, 0x12, 0x9f, 0xf6, 0x8a, 0x96, 0x20, 0x25, 0x9f, 0x21, 0x72,
+ 0x6c, 0xcb, 0x84, 0x78, 0x3d, 0xe9, 0x65, 0x58, 0xb6, 0xf6, 0xc4, 0x5f,
+ 0x35, 0xe8, 0x1d, 0xe3, 0x0e, 0x77, 0x31, 0x13, 0xf3, 0xac, 0xb3, 0x0c,
+ 0x64, 0xcc, 0xa8, 0x35, 0x6a, 0xf4, 0xa0, 0x62, 0xda, 0x15, 0x5b, 0x14,
+ 0xfb, 0xd9, 0xbe, 0x1b, 0x5a, 0xc8, 0xd1, 0x58, 0x5a, 0xb5, 0xca, 0x9c,
+ 0x47, 0xaf, 0xf2, 0xdb, 0xc6, 0x29, 0x9d, 0x0d, 0x62, 0xcb, 0xb3, 0xd8,
+ 0xbb, 0x21, 0x7a, 0x70, 0xea, 0x0b, 0x7d, 0xb9, 0xa8, 0x6a, 0xf1, 0x19,
+ 0x88, 0x3f, 0x55, 0x18, 0xad, 0x4d, 0xc0, 0x01, 0x16, 0xed, 0xc4, 0x0c,
+ 0x84, 0xf9, 0xba, 0x5c, 0x60, 0x68, 0x63, 0x81, 0xee, 0x9b, 0xd3, 0xa9,
+ 0x22, 0x4d, 0x34, 0x35, 0x6d, 0x9d, 0x9f, 0x00, 0xbc, 0x21, 0x32, 0xda,
+ 0x58, 0x1a, 0x64, 0xad, 0x09, 0x2e, 0x0d, 0xed, 0xff, 0x79, 0x71, 0x73,
+ 0x7d, 0x3b, 0x69, 0x2e, 0x7f, 0x51, 0xc4, 0xdf, 0x60, 0xef, 0xeb, 0x23,
+ 0xd0, 0xd0, 0xcc, 0xc5, 0x7e, 0x02, 0x2b, 0x9b, 0xa8, 0x8b, 0x52, 0x6e,
+ 0x13, 0xfb, 0x49, 0x04, 0xdd, 0x39, 0x12, 0x60, 0x45, 0x5e, 0xe3, 0xe6,
+ 0x50, 0x97, 0x9c, 0x10, 0xf5, 0x55, 0x09, 0x16, 0xe8, 0x2a, 0xd2, 0x4f,
+ 0xe4, 0x88, 0x75, 0x5a, 0xb4, 0x25, 0x27, 0xe0, 0x6f, 0xc5, 0x14, 0xde,
+ 0xb6, 0x37, 0x97, 0xed, 0x46, 0x4f, 0x93, 0x9f, 0xd0, 0x64, 0x0b, 0xa3,
+ 0x78, 0xc8, 0x71, 0x93, 0xdf, 0x42, 0xa0, 0xe9, 0xc2, 0xdb, 0x8c, 0xdc,
+ 0x6f, 0x0d, 0x43, 0xd2, 0xd7, 0x95, 0x34, 0x8f, 0x39, 0xc0, 0x43, 0xed,
+ 0x97, 0x5d, 0x49, 0xfd, 0xac, 0x32, 0x55, 0x7a, 0x30, 0x15, 0xf3, 0x96,
+ 0xdc, 0xd9, 0x9d, 0x0b, 0x28, 0xa9, 0xd6, 0xa4, 0x38, 0x0c, 0x38, 0xb9,
+ 0x95, 0x24, 0x15, 0x4e, 0x44, 0xd8, 0x3e, 0x0a, 0xe5, 0x0c, 0x68, 0xba,
+ 0x18, 0x7d, 0x30, 0x2d, 0x11, 0x2f, 0xed, 0x45, 0x3c, 0x8c, 0xaa, 0xee,
+ 0xbc, 0xcc, 0x35, 0x59, 0xd9, 0xfc, 0x54, 0xb3, 0x4b, 0x63, 0x78, 0x14,
+ 0x5b, 0x8e, 0xc7, 0x5c, 0x72, 0xc0, 0x02, 0xc9, 0x52, 0x84, 0xf2, 0x13,
+ 0x87, 0x78, 0xf0, 0x50, 0xe8, 0xfe, 0x53, 0x46, 0xb6, 0x3f, 0xaf, 0xe2,
+ 0xf8, 0x0c, 0x94, 0x63, 0xd1, 0x06, 0xcf, 0x4c, 0xb8, 0xd1, 0xe8, 0x81,
+ 0x2a, 0x9d, 0xa3, 0x05, 0xdb, 0xda, 0x2c, 0x38, 0xe8, 0x02, 0x99, 0x33,
+ 0x5e, 0xc9, 0x63, 0x73, 0x79, 0x4d, 0xf5, 0xc1, 0xc3, 0xeb, 0x1a, 0x94,
+ 0x1c, 0x36, 0xfa, 0x1a, 0x8b, 0xee, 0x18, 0x2e, 0x36, 0xc3, 0x82, 0xdc,
+ 0x76, 0x29, 0x7f, 0xfc, 0x8a, 0xbc, 0x68, 0x63, 0xf4, 0x1b, 0x44, 0x0f,
+ 0x9b, 0x11, 0x5f, 0x8b, 0x30, 0x26, 0xac, 0x48, 0xef, 0x35, 0x77, 0xfa,
+ 0x97, 0xde, 0x22, 0xda, 0x4a, 0xfe, 0x06, 0xad, 0x14, 0xd7, 0x42, 0x92,
+ 0x08, 0xbd, 0x48, 0x85, 0x50, 0x8e, 0x92, 0xd5, 0x35, 0x32, 0xce, 0x00,
+ 0xb8, 0x5c, 0x8b, 0xac, 0x21, 0xf8, 0xf3, 0x5c, 0x02, 0x7e, 0x22, 0xf5,
+ 0x07, 0xbf, 0x31, 0x7e, 0x6c, 0x6a, 0x22, 0xbb, 0x99, 0x4b, 0x6b, 0xd4,
+ 0x8a, 0xc2, 0xfe, 0x2a, 0xf7, 0xc3, 0xce, 0xd7, 0x44, 0x96, 0x15, 0x51,
+ 0x1a, 0xfd, 0x06, 0xcb, 0x9f, 0xa3, 0xe4, 0x02, 0x2b, 0xae, 0x70, 0xff,
+ 0x20, 0x8b, 0xf2, 0xea, 0x5c, 0x09, 0xe8, 0x5f, 0x6b, 0xa9, 0x61, 0xbe,
+ 0x38, 0xd3, 0xb9, 0x79, 0x2c, 0x90, 0xfe, 0x84, 0x1c, 0x7e, 0x64, 0x66,
+ 0xc0, 0xf1, 0x4e, 0x44, 0xdd, 0xb5, 0x8c, 0x01, 0x19, 0x1d, 0x2a, 0xa7,
+ 0x42, 0x93, 0x44, 0x35, 0x6a, 0x15, 0x97, 0xa3, 0x8b, 0xc3, 0x10, 0xd5,
+ 0x2e, 0xa9, 0x62, 0x74, 0x65, 0xfd, 0xca, 0xc9, 0x84, 0x06, 0xfc, 0xbf,
+ 0xcf, 0x9c, 0xe4, 0x3e, 0xe9, 0x0f, 0xd5, 0xdf, 0xd6, 0x89, 0xb3, 0x68,
+ 0x28, 0x42, 0xe2, 0xd5, 0xa5, 0x92, 0x3f, 0xba, 0xf6, 0x42, 0x86, 0xd7,
+ 0xa8, 0xf9, 0x19, 0x25, 0x5f, 0x21, 0x95, 0xad, 0xc5, 0xe0, 0x4d, 0xde,
+ 0x9a, 0xea, 0xef, 0x77, 0x10, 0xbc, 0x38, 0xb4, 0x03, 0x33, 0x99, 0x5f,
+ 0x8c, 0x56, 0x4e, 0xba, 0x77, 0x8f, 0x8f, 0x84, 0x29, 0x6e, 0x98, 0x2a,
+ 0x2d, 0xd5, 0x4f, 0x8e, 0xae, 0xea, 0x19, 0xdf, 0xce, 0xe4, 0x88, 0x1a,
+ 0x07, 0x9f, 0x1e, 0xe6, 0xa9, 0x54, 0x64, 0xcc, 0x59, 0x5d, 0x47, 0xea,
+ 0x29, 0xcd, 0x7f, 0x6b, 0xf6, 0x0f, 0xf7, 0xce, 0xcc, 0x01, 0xf3, 0xf3,
+ 0x50, 0x3e, 0xd8, 0x35, 0x46, 0x3d, 0xbf, 0x49, 0x07, 0xb4, 0x5b, 0x03,
+ 0xe3, 0x69, 0xea, 0x1a, 0xdf, 0xb6, 0xbb, 0xdf, 0x24, 0xb2, 0xdb, 0x98,
+ 0x9b, 0xba, 0xf4, 0x85, 0xe4, 0xf1, 0x01, 0x7b, 0x8a, 0x83, 0x07, 0x51,
+ 0x68, 0x70, 0x59, 0xe8, 0xec, 0xb5, 0x53, 0x6c, 0xb0, 0x0d, 0xdb, 0x12,
+ 0x58, 0x99, 0x19, 0x75, 0x8f, 0x53, 0x74, 0x25, 0x63, 0xbe, 0x0b, 0xa4,
+ 0x97, 0xcb, 0x13, 0x4e, 0xdf, 0xd9, 0x39, 0xda, 0x52, 0xd6, 0x9c, 0x9d,
+ 0xe1, 0x31, 0x68, 0xcd, 0x28, 0xa6, 0x7a, 0x80, 0xd7, 0x50, 0xab, 0x98,
+ 0xa0, 0x9b, 0x38, 0x0d, 0x3c, 0x34, 0x27, 0xd3, 0x49, 0x65, 0x07, 0x64,
+ 0xec, 0x53, 0x18, 0xa3, 0xba, 0xf0, 0x97, 0x7c, 0x14, 0x18, 0x1a, 0x0b,
+ 0xae, 0xa6, 0x2c, 0xf6, 0x73, 0xef, 0xab, 0xed, 0x48, 0xfc, 0xd6, 0x3b,
+ 0xe6, 0xfd, 0x2d, 0xdc, 0xaa, 0xaf, 0x2f, 0xfa, 0xd9, 0x87, 0x1a, 0xec,
+ 0x08, 0xe6, 0x56, 0x01, 0x46, 0x32, 0xd7, 0x23, 0x16, 0x6d, 0xd1, 0x79,
+ 0x00, 0x55, 0xa5, 0xed, 0x2a, 0x24, 0x69, 0xf7, 0x9d, 0x59, 0xa2, 0x3b,
+ 0x86, 0xbc, 0x3f, 0x83, 0xcc, 0x92, 0x79, 0xcf, 0x3e, 0x3d, 0x0e, 0x2f,
+ 0x67, 0x9b, 0x09, 0x09, 0xdf, 0xa0, 0x78, 0x09, 0x16, 0x37, 0x6a, 0xc7,
+ 0xcf, 0x27, 0x8c, 0x23, 0x85, 0x68, 0xa4, 0xa9, 0xb0, 0x2f, 0x98, 0x22,
+ 0x53, 0xd4, 0x75, 0xc5, 0xd8, 0xfe, 0x39, 0xaa, 0x69, 0x0d, 0xea, 0x3a,
+ 0xdb, 0x0b, 0xaf, 0xc3, 0x4a, 0x7c, 0x88, 0x3f, 0xb4, 0x41, 0x80, 0x5e,
+ 0x8b, 0x71, 0x79, 0xd6, 0x77, 0xb1, 0xc5, 0xf9, 0x8a, 0x99, 0x67, 0xe8,
+ 0x2b, 0x85, 0x06, 0x93, 0x82, 0xa2, 0x08, 0xf7, 0x1f, 0x5b, 0xbd, 0xdb,
+ 0x53, 0xdc, 0x25, 0x65, 0x84, 0xf0, 0xf7, 0x8b, 0x87, 0xbc, 0xf0, 0x85,
+ 0xfc, 0x9d, 0xcd, 0x5d, 0x67, 0x0d, 0x9d, 0xef, 0x0c, 0x2b, 0x62, 0xc6,
+ 0xb8, 0x2b, 0x98, 0xfd, 0x81, 0x84, 0x19, 0xa9, 0x92, 0xa4, 0x2e, 0xac,
+ 0x9e, 0x35, 0x0e, 0x07, 0xd3, 0xd3, 0x75, 0x6a, 0x5d, 0xc3, 0x8c, 0xc1,
+ 0x87, 0x88, 0xab, 0x53, 0x9c, 0x2e, 0x22, 0x94, 0xfe, 0x18, 0x65, 0xc2,
+ 0x5c, 0x9d, 0x02, 0x3b, 0xc1, 0x7f, 0xfd, 0xa7, 0xb1, 0x4d, 0xae, 0xc9,
+ 0x75, 0x0c, 0x2a, 0x86, 0x6a, 0xd4, 0x3a, 0xb3, 0x49, 0x02, 0x6e, 0x43,
+ 0xe4, 0x61, 0xf7, 0x18, 0xcb, 0xad, 0x32, 0x78, 0xaf, 0xfe, 0x38, 0x8b,
+ 0xb3, 0xec, 0xef, 0x86, 0x68, 0xb5, 0xe3, 0xef, 0x53, 0x2e, 0x53, 0x87,
+ 0x7c, 0xe8, 0x55, 0x93, 0x8a, 0xb1, 0x2e, 0x68, 0x76, 0x24, 0xcc, 0xfa,
+ 0x2b, 0xdd, 0xd8, 0xb1, 0x77, 0x10, 0x19, 0x70, 0x47, 0xd8, 0xf7, 0xc6,
+ 0x52, 0xa0, 0x68, 0xf9, 0x78, 0x37, 0xf0, 0x48, 0x6d, 0xcd, 0xf5, 0x6e,
+ 0x4e, 0x51, 0xbf, 0xec, 0x68, 0x22, 0x5e, 0x84, 0x2b, 0x25, 0xfb, 0x05,
+ 0x04, 0x84, 0x41, 0xfd, 0x5c, 0x0b, 0x23, 0x2d, 0xd2, 0x0c, 0xa0, 0x08,
+ 0x5f, 0x9d, 0xfb, 0xf8, 0x43, 0x6f, 0xa4, 0x9e, 0x64, 0x2f, 0x4c, 0xeb,
+ 0xb5, 0xe5, 0x6e, 0x8a, 0x58, 0x35, 0x17, 0x27, 0x8f, 0x0e, 0xf8, 0x74,
+ 0x96, 0x7a, 0x6c, 0x0c, 0xd4, 0x27, 0x3b, 0xa9, 0xa3, 0x61, 0x51, 0xe6,
+ 0xfc, 0x5f, 0x7d, 0x4f, 0x31, 0xaf, 0x8f, 0xea, 0xef, 0xe0, 0x49, 0x50,
+ 0x31, 0x41, 0xbc, 0x25, 0x0e, 0x67, 0x43, 0x0f, 0x0f, 0x0e, 0x37, 0x72,
+ 0xf6, 0x98, 0x2d, 0x12, 0x89, 0x2a, 0x01, 0x27, 0xf1, 0x89, 0x63, 0x04,
+ 0x3a, 0x62, 0x1f, 0xa1, 0x2c, 0x81, 0xf7, 0x08, 0x1d, 0xa2, 0x33, 0xc8,
+ 0xd9, 0x26, 0xc9, 0x29, 0x14, 0x56, 0x4c, 0x34, 0xe9, 0x5b, 0x3c, 0xb9,
+ 0x2d, 0x0f, 0x16, 0x37, 0x8e, 0x31, 0x06, 0xda, 0x02, 0x35, 0x1c, 0xd6,
+ 0xc6, 0xe9, 0xda, 0x0c, 0x03, 0xed, 0xb7, 0xfa, 0xa3, 0x5a, 0x9e, 0xdd,
+ 0x5b, 0xe2, 0x10, 0x84, 0x19, 0xc8, 0x57, 0x44, 0x9f, 0x5d, 0xa6, 0x26,
+ 0x29, 0x65, 0xe0, 0xdd, 0x18, 0xc3, 0xd2, 0x79, 0x72, 0xf5, 0xeb, 0xea,
+ 0x91, 0xbb, 0x70, 0x6e, 0x68, 0x88, 0x65, 0xab, 0xfe, 0xba, 0x3f, 0xa0,
+ 0x45, 0xd9, 0xe4, 0x1a, 0xab, 0xd2, 0x7a, 0x1a, 0xe4, 0x80, 0xb5, 0x8b,
+ 0x2d, 0x26, 0x4a, 0xa7, 0xa5, 0x8b, 0x7e, 0x8e, 0x2d, 0x21, 0x67, 0xd4,
+ 0xdf, 0xd4, 0xf6, 0x8a, 0xea, 0xdb, 0xc0, 0xb3, 0x66, 0x7a, 0xe9, 0x00,
+ 0xa6, 0xe7, 0xce, 0x7d, 0xe1, 0xe2, 0x26, 0xe6, 0x85, 0xe5, 0x11, 0x7e,
+ 0x89, 0x6e, 0xe4, 0xda, 0xfb, 0x21, 0xf3, 0x3c, 0x25, 0x76, 0x22, 0x4f,
+ 0x37, 0xfd, 0xc2, 0x59, 0x1f, 0xc4, 0x8e, 0x45, 0x54, 0x23, 0x97, 0xa8,
+ 0x61, 0xcb, 0x2e, 0xce, 0x56, 0xf1, 0xc2, 0xcd, 0x65, 0x67, 0x44, 0x7b,
+ 0x43, 0x1e, 0xab, 0xc8, 0xdb, 0xed, 0xfc, 0xb4, 0x47, 0x8e, 0x8e, 0xd0,
+ 0x3c, 0xad, 0xd2, 0xdd, 0xf7, 0xb0, 0xa7, 0x47, 0x68, 0x61, 0x34, 0x33,
+ 0x99, 0xc7, 0x18, 0x0c, 0xdc, 0xda, 0xd3, 0xc1, 0xaf, 0x45, 0xc7, 0x6e,
+ 0xbe, 0x0a, 0x69, 0xf9, 0xb6, 0x21, 0x7c, 0xa1, 0xfa, 0x12, 0xd8, 0x39,
+ 0x24, 0xf2, 0x8f, 0x1c, 0x6d, 0x3b, 0x99, 0x1b, 0x73, 0xc8, 0x01, 0x05,
+ 0x17, 0xd0, 0x88, 0x5b, 0xa8, 0x55, 0xfd, 0x5f, 0xe8, 0x24, 0xf5, 0xcd,
+ 0x9c, 0xb4, 0x13, 0x5b, 0x16, 0x74, 0x91, 0xb4, 0x61, 0x80, 0x90, 0x65,
+ 0x33, 0x2f, 0x69, 0xc2, 0xa0, 0x64, 0xcc, 0x53, 0x54, 0x57, 0x13, 0xb1,
+ 0x23, 0xec, 0x78, 0x86, 0xb3, 0x0a, 0x75, 0xd1, 0xdd, 0x30, 0xb7, 0xab,
+ 0xf6, 0x24, 0x49, 0x68, 0x34, 0xe3, 0x89, 0xac, 0xa1, 0x95, 0xb3, 0x75,
+ 0xed, 0x21, 0xc2, 0xc0, 0x2d, 0x6c, 0xf5, 0x9e, 0xc7, 0x17, 0xba, 0xe3,
+ 0x4e, 0xf9, 0x78, 0xf2, 0x5c, 0x98, 0x12, 0xe5, 0x3d, 0x45, 0xc4, 0xc5,
+ 0x42, 0x90, 0x61, 0x59, 0x0c, 0x47, 0xa7, 0xcc, 0xd7, 0xcb, 0xa9, 0x3f,
+ 0xd9, 0xdb, 0xc7, 0x78, 0x8b, 0x0e, 0x5b, 0xab, 0x4c, 0x25, 0x2a, 0x41,
+ 0xae, 0x52, 0x3b, 0x15, 0xe1, 0x0a, 0x25, 0x07, 0x4a, 0x7f, 0xe4, 0x10,
+ 0x5a, 0x9d, 0x0d, 0x2c, 0x15, 0xcd, 0x6b, 0x18, 0xeb, 0xb5, 0x9d, 0x08,
+ 0x2c, 0x81, 0xcf, 0xc0, 0x7c, 0x58, 0xbe, 0x3b, 0x92, 0xc2, 0x85, 0xe2,
+ 0xce, 0xc7, 0x8b, 0x0a, 0x13, 0x8b, 0x82, 0x69, 0xac, 0x8b, 0xf2, 0x67,
+ 0xb5, 0x92, 0x63, 0x1e, 0x68, 0x8d, 0x4c, 0x5f, 0xad, 0x7f, 0xd3, 0x27,
+ 0x44, 0x73, 0x78, 0x0d, 0xdb, 0x4d, 0xd5, 0x62, 0x1a, 0x74, 0x68, 0x6a,
+ 0x6e, 0xb9, 0x6f, 0x8e, 0x09, 0x33, 0x8f, 0xac, 0xf4, 0x80, 0x3d, 0xba,
+ 0x41, 0x7e, 0xb7, 0xdc, 0xe6, 0x3d, 0x25, 0xb0, 0xfc, 0xee, 0x29, 0x03,
+ 0x08, 0x83, 0x22, 0xcf, 0x04, 0x16, 0x1a, 0x55, 0x09, 0x50, 0xc2, 0xfb,
+ 0xe5, 0xe9, 0xd1, 0xd6, 0x86, 0xbf, 0x3a, 0x25, 0x03, 0xd3, 0x4b, 0xaa,
+ 0x0a, 0xde, 0x6e, 0x2e, 0x9d, 0xdb, 0x70, 0x6a, 0xe4, 0x0b, 0xf1, 0x91,
+ 0x48, 0xf7, 0x37, 0xa0, 0xa1, 0xd4, 0x94, 0xad, 0xf7, 0x30, 0x30, 0x71,
+ 0x10, 0x49, 0x2f, 0xed, 0x59, 0xd4, 0x37, 0x5a, 0xbf, 0xce, 0x6d, 0x0e,
+ 0xf0, 0x86, 0x23, 0x17, 0x4e, 0xc3, 0x81, 0xbc, 0x31, 0xfb, 0xe3, 0xe2,
+ 0x29, 0xd6, 0xbc, 0xef, 0x0a, 0xa4, 0x90, 0x4b, 0x90, 0x26, 0x12, 0xd2,
+ 0x7f, 0x27, 0x62, 0x55, 0x57, 0x72, 0xe4, 0xb8, 0x63, 0x94, 0x7f, 0x79,
+ 0xbc, 0xc6, 0x1e, 0x65, 0x28, 0x4c, 0xd4, 0xaa, 0x2e, 0x2c, 0x35, 0x63,
+ 0x02, 0xaa, 0x6e, 0x82, 0x57, 0x53, 0x3b, 0x32, 0xf0, 0x2c, 0x93, 0xe8,
+ 0x68, 0x7a, 0x47, 0x12, 0x69, 0xd8, 0x5e, 0xff, 0x75, 0xe2, 0x64, 0xd1,
+ 0xbb, 0x54, 0x92, 0x96, 0xb3, 0x86, 0xc6, 0x26, 0xd4, 0x92, 0x02, 0xa6,
+ 0xec, 0xcd, 0x64, 0x7f, 0x15, 0xee, 0xcc, 0x1d, 0x9a, 0xfa, 0x95, 0xf0,
+ 0x82, 0x11, 0xb8, 0x04, 0xfb, 0x14, 0xd4, 0xdd, 0x37, 0x86, 0x80, 0x30,
+ 0x2b, 0xcb, 0x4b, 0x05, 0x2b, 0x97, 0x86, 0x35, 0xe5, 0x15, 0xb4, 0xa9,
+ 0xf9, 0x0d, 0xbe, 0x1f, 0x55, 0xfa, 0x9a, 0x93, 0xe1, 0x9a, 0x9a, 0x15,
+ 0x5f, 0x51, 0x31, 0x85, 0xd6, 0xfe, 0xb7, 0xad, 0x3f, 0xcf, 0x74, 0x17,
+ 0x9c, 0xc4, 0x62, 0xc7, 0x02, 0x95, 0x68, 0xed, 0xb0, 0x89, 0x4b, 0x0e,
+ 0x7a, 0x2d, 0x93, 0x19, 0x6b, 0x90, 0x57, 0x92, 0x01, 0x2f, 0x56, 0x98,
+ 0x58, 0x38, 0xb7, 0xb1, 0xaf, 0x45, 0x81, 0x03, 0xe4, 0xd9, 0x09, 0x60,
+ 0x1f, 0x77, 0xb3, 0xe3, 0x88, 0xd4, 0xaa, 0x13, 0x16, 0xad, 0x91, 0x01,
+ 0xc0, 0x3c, 0xe3, 0xb5, 0xc9, 0x4c, 0x7a, 0xb0, 0x81, 0x36, 0xaf, 0x4e,
+ 0x21, 0x12, 0xf4, 0xf1, 0xd6, 0xe8, 0x60, 0x90, 0xb6, 0x3b, 0x45, 0xe6,
+ 0xeb, 0x22, 0xc5, 0x7f, 0xb1, 0xae, 0xce, 0xc0, 0x6c, 0x04, 0xd6, 0xcc,
+ 0xfd, 0xa2, 0x1b, 0x7d, 0x8c, 0x61, 0x5b, 0x6b, 0x91, 0x92, 0x22, 0x71,
+ 0xa0, 0xea, 0x01, 0xc3, 0xcf, 0x46, 0x8a, 0x8f, 0xdd, 0x28, 0xd4, 0x9f,
+ 0x0f, 0xfb, 0xfc, 0x25, 0xf2, 0xfd, 0x04, 0x84, 0x1e, 0x42, 0x02, 0x26,
+ 0x03, 0x83, 0x5c, 0x20, 0xee, 0xfa, 0x20, 0xe4, 0x18, 0xe0, 0xb3, 0xc9,
+ 0xe0, 0x3c, 0x6f, 0x4d, 0x66, 0xd3, 0x2a, 0x81, 0x35, 0xbf, 0xd4, 0x9c,
+ 0xe8, 0x11, 0xec, 0xe4, 0x8b, 0x33, 0x9a, 0xb0, 0xf4, 0x6b, 0x66, 0x62,
+ 0xa1, 0xa8, 0x07, 0xc9, 0x52, 0x64, 0xc4, 0x4e, 0xa6, 0x36, 0xdf, 0x21,
+ 0x4f, 0xab, 0xbc, 0xd2, 0x83, 0x7d, 0x13, 0x88, 0x4a, 0xff, 0x87, 0xab,
+ 0x25, 0xab, 0xbf, 0x64, 0xb5, 0x90, 0x9b, 0x4a, 0x7a, 0xf1, 0x6c, 0xfa,
+ 0x1b, 0x57, 0x8d, 0x55, 0xc7, 0xe6, 0x0e, 0x63, 0x49, 0x94, 0x60, 0x75,
+ 0x1c, 0x82, 0xde, 0xb0, 0xe1, 0x6c, 0x52, 0xaa, 0xd2, 0x2f, 0x1c, 0x03,
+ 0xe2, 0x29, 0x8c, 0xc2, 0x55, 0xfb, 0x09, 0x59, 0xd0, 0x85, 0xe2, 0x38,
+ 0x01, 0xac, 0x50, 0xe0, 0x7f, 0x2d, 0xe8, 0x2d, 0xf1, 0x6d, 0x24, 0xf2,
+ 0xda, 0x15, 0x83, 0x03, 0xa4, 0x2d, 0xc4, 0x5f, 0xb9, 0xd0, 0x90, 0x56,
+ 0x3c, 0xad, 0x40, 0x30, 0x0d, 0x9e, 0xb6, 0xa1, 0x1a, 0x0b, 0x18, 0xad,
+ 0x51, 0x7c, 0x5f, 0xff, 0x01, 0x95, 0x03, 0xe7, 0x7e, 0xa1, 0x4a, 0xc8,
+ 0x66, 0x22, 0x8b, 0x0d, 0x65, 0x0b, 0xac, 0x3d, 0x48, 0x91, 0x82, 0x72,
+ 0x74, 0x76, 0x47, 0xec, 0xed, 0xb9, 0xd4, 0x26, 0xc1, 0x6c, 0xea, 0xf0,
+ 0x30, 0x34, 0x8f, 0x8d, 0x0c, 0x43, 0x0f, 0xd1, 0x6b, 0x9e, 0x6f, 0xeb,
+ 0xa5, 0x58, 0xf0, 0xde, 0x11, 0xa6, 0x6a, 0xde, 0xff, 0xdd, 0xc7, 0x24,
+ 0x5b, 0x75, 0xf1, 0x51, 0xe5, 0x3e, 0x30, 0x9f, 0x09, 0xc4, 0xdf, 0x97,
+ 0xed, 0xac, 0xa0, 0x1d, 0x94, 0x97, 0xca, 0x17, 0x80, 0xb2, 0x3c, 0x6f,
+ 0x67, 0xc8, 0x2a, 0x57, 0xbd, 0xff, 0xf7, 0x77, 0x76, 0x8e, 0xdc, 0x78,
+ 0xe7, 0xc1, 0xd7, 0xb9, 0x56, 0xd5, 0xf0, 0xf2, 0xd9, 0x95, 0x85, 0xdf,
+ 0xa5, 0x51, 0xb4, 0x58, 0x2b, 0x9b, 0x72, 0xac, 0x5b, 0x7e, 0x8b, 0xda,
+ 0x78, 0x55, 0x05, 0xe1, 0xb9, 0x0b, 0x97, 0x32, 0x95, 0x3a, 0xd8, 0xce,
+ 0x6b, 0xaf, 0x9a, 0xf6, 0x1b, 0x2a, 0xb5, 0xe5, 0x0b, 0xfc, 0x7b, 0xb9,
+ 0xb0, 0x97, 0xe6, 0x39, 0xac, 0xcb, 0x9a, 0xff, 0xc6, 0x32, 0x20, 0xa9,
+ 0xe5, 0x42, 0x8a, 0x3b, 0x21, 0xdd, 0x96, 0x5e, 0x78, 0xb0, 0x9c, 0x68,
+ 0xf6, 0x03, 0xfe, 0x5d, 0xac, 0x0b, 0x75, 0x68, 0x44, 0x22, 0x24, 0xf9,
+ 0x1d, 0x94, 0xef, 0xfd, 0xdd, 0x18, 0x03, 0x37, 0xff, 0x16, 0xc5, 0xd1,
+ 0x5d, 0x20, 0xb9, 0x87, 0xf7, 0x64, 0xcf, 0xe0, 0x8d, 0x25, 0xdb, 0x10,
+ 0xa1, 0x9a, 0x7e, 0xa7, 0xd5, 0x0f, 0x7d, 0x95, 0x3c, 0xdc, 0xa5, 0x72,
+ 0x57, 0xb6, 0x25, 0x7b, 0xd0, 0xe7, 0x07, 0x74, 0x38, 0xc8, 0x7a, 0x29,
+ 0xc2, 0xca, 0x21, 0x55, 0xd7, 0xd9, 0xeb, 0x27, 0x56, 0xae, 0xdd, 0xc5,
+ 0x68, 0x9e, 0xd3, 0x06, 0xe7, 0xe9, 0x5c, 0x9e, 0x0c, 0x32, 0x62, 0xb2,
+ 0x98, 0x97, 0xd2, 0x87, 0x3e, 0xcc, 0x60, 0x6f, 0x71, 0x67, 0xf5, 0xcf,
+ 0x7e, 0x08, 0x6c, 0xd4, 0x9e, 0xab, 0x4c, 0xa2, 0xfa, 0x7d, 0x4e, 0x57,
+ 0x00, 0x66, 0x93, 0x2a, 0xb6, 0x3c, 0x83, 0x1f, 0x32, 0xc0, 0x42, 0xdd,
+ 0x6e, 0xa9, 0xca, 0x5d, 0xb6, 0xc1, 0xee, 0xb2, 0x45, 0x51, 0xf4, 0xb3,
+ 0x8b, 0xb4, 0x7b, 0x89, 0x4d, 0x15, 0x11, 0x16, 0xe2, 0xf1, 0xc5, 0xb6,
+ 0x64, 0x00, 0x53, 0x90, 0x88, 0x80, 0xec, 0x25, 0x50, 0xef, 0xf1, 0x8a,
+ 0x27, 0x72, 0xad, 0xd6, 0x70, 0xfc, 0x36, 0x0d, 0x59, 0xb9, 0x25, 0x34,
+ 0xd3, 0x66, 0xa0, 0x7a, 0x49, 0x2b, 0xfe, 0x5a, 0xbf, 0x7f, 0x7e, 0x26,
+ 0x03, 0x51, 0xa2, 0x36, 0xc5, 0x06, 0xed, 0xc7, 0xea, 0x5c, 0xea, 0xa4,
+ 0x26, 0x47, 0xb7, 0xc8, 0x22, 0x62, 0x16, 0x5a, 0x56, 0xf5, 0xf6, 0x67,
+ 0xbd, 0x8a, 0xdd, 0x81, 0x6f, 0x90, 0xa9, 0xaa, 0x5a, 0x97, 0xc9, 0x03,
+ 0x36, 0x5c, 0x43, 0x2d, 0xbd, 0x90, 0x4a, 0x27, 0x62, 0x20, 0x1f, 0x9c,
+ 0x93, 0x86, 0xae, 0xd3, 0x5f, 0xe3, 0xd4, 0x95, 0x5b, 0x23, 0x50, 0x6d,
+ 0x01, 0xcd, 0x4b, 0x6a, 0x63, 0xf5, 0x32, 0x9c, 0xab, 0xfa, 0x42, 0xe5,
+ 0x42, 0x1a, 0x5d, 0xc6, 0x1c, 0x65, 0x71, 0xe1, 0x61, 0xcd, 0x0c, 0xc5,
+ 0x95, 0xe2, 0xb1, 0xf9, 0xeb, 0xa3, 0xfb, 0xe6, 0x2e, 0xdd, 0x0f, 0xd6,
+ 0xf1, 0x1b, 0xb2, 0x6e, 0x9d, 0xbb, 0xc9, 0x7c, 0x9a, 0x02, 0xce, 0xb1,
+ 0x78, 0x86, 0x66, 0x6e, 0x44, 0x11, 0xe9, 0x78, 0x4b, 0x43, 0x33, 0x3d,
+ 0xec, 0x5c, 0xd0, 0x4a, 0x47, 0x5e, 0xf3, 0x96, 0x9e, 0xef, 0x06, 0x00,
+ 0xa1, 0xe5, 0x38, 0xec, 0x89, 0xcf, 0xe6, 0xde, 0x5f, 0x0f, 0xb4, 0x7c,
+ 0xdd, 0xaf, 0xe8, 0xdc, 0x38, 0x36, 0xb6, 0xb4, 0x1d, 0xb3, 0xad, 0x86,
+ 0x52, 0x6d, 0xa0, 0xe6, 0xb5, 0x8c, 0x11, 0x4d, 0x64, 0xf6, 0x41, 0x8c,
+ 0xcd, 0xbc, 0x4b, 0xad, 0x53, 0x6e, 0xbc, 0xb1, 0xc7, 0xd5, 0xa6, 0xf9,
+ 0xdc, 0x2c, 0xc8, 0x18, 0xdd, 0x2f, 0x8d, 0xef, 0x9a, 0xa8, 0xa6, 0x3e,
+ 0x25, 0x65, 0xfd, 0xfb, 0xa4, 0xd4, 0x4c, 0x45, 0x54, 0xe9, 0x1b, 0x11,
+ 0x06, 0xc9, 0x67, 0x51, 0xc8, 0x1e, 0xc6, 0xcf, 0x92, 0xc1, 0xce, 0xcf,
+ 0x52, 0x9c, 0x29, 0x4e, 0x24, 0xfb, 0x30, 0xce, 0x9e, 0xda, 0xa9, 0xc8,
+ 0x69, 0x69, 0x16, 0xb2, 0x4e, 0x98, 0x4e, 0xf7, 0x9a, 0x1a, 0xf7, 0x75,
+ 0xcc, 0xe2, 0x57, 0x11, 0xcc, 0x5b, 0xf1, 0xfd, 0xd3, 0x57, 0xa9, 0xef,
+ 0x29, 0x20, 0x66, 0x41, 0x1b, 0x7e, 0x35, 0xc2, 0x0e, 0x69, 0xa2, 0x16,
+ 0xa1, 0x37, 0x2d, 0x5b, 0x86, 0x6b, 0x12, 0x0c, 0x9c, 0x8d, 0xb9, 0x15,
+ 0xe0, 0xb7, 0xd5, 0xfb, 0x40, 0x6c, 0xc5, 0x0a, 0x77, 0xe7, 0xfc, 0x89,
+ 0xe0, 0x41, 0xf7, 0x6b, 0xda, 0x93, 0x5d, 0xef, 0xc3, 0x35, 0x1e, 0xe3,
+ 0x4b, 0x40, 0x89, 0x96, 0x3a, 0x9e, 0x05, 0x8b, 0xc1, 0x6d, 0xe3, 0xd6,
+ 0xde, 0x61, 0x3c, 0xdd, 0x40, 0xac, 0xda, 0x3b, 0x5c, 0x43, 0x1d, 0xef,
+ 0x22, 0x6c, 0xee, 0x99, 0xd4, 0x3a, 0xc4, 0x56, 0xd1, 0x25, 0xba, 0x5d,
+ 0x88, 0xeb, 0xf4, 0xd3, 0x50, 0xef, 0xed, 0x1c, 0x17, 0x0c, 0x2c, 0x94,
+ 0xdf, 0xcb, 0x92, 0xcb, 0xc0, 0x9d, 0xbe, 0xaf, 0x5c, 0x83, 0xf6, 0x70,
+ 0x96, 0x7a, 0xac, 0x0e, 0x44, 0xd1, 0x6d, 0x9a, 0xd2, 0x24, 0x61, 0x12,
+ 0xda, 0x91, 0xbc, 0xce, 0x95, 0x6a, 0x4d, 0x7f, 0x5d, 0xf5, 0x2b, 0x10,
+ 0xa4, 0x25, 0x05, 0xf3, 0xcd, 0xf1, 0x1d, 0x14, 0x91, 0x73, 0xb9, 0xf3,
+ 0xf6, 0x50, 0xeb, 0x73, 0xf5, 0xb5, 0xff, 0xcc, 0xbd, 0xcb, 0x7d, 0x09,
+ 0xfb, 0x17, 0x06, 0x87, 0x6b, 0x84, 0x42, 0x91, 0x50, 0xf7, 0x4c, 0x07,
+ 0x5d, 0x44, 0x2f, 0x04, 0x68, 0x84, 0x71, 0x23, 0x8b, 0x11, 0x1b, 0x22,
+ 0xd1, 0xb8, 0xba, 0xab, 0x11, 0xd8, 0x08, 0xa0, 0x90, 0xda, 0xea, 0xb9,
+ 0xf3, 0x5b, 0x82, 0x5e, 0x26, 0x25, 0x53, 0xae, 0x89, 0x1b, 0x15, 0xf4,
+ 0x07, 0xc1, 0xc2, 0x94, 0x4c, 0x25, 0x95, 0x89, 0xd2, 0x09, 0x99, 0x10,
+ 0x8b, 0xda, 0x5f, 0xa8, 0xfd, 0x23, 0x4f, 0xad, 0xa3, 0xfa, 0x13, 0x41,
+ 0xf3, 0xbf, 0x2c, 0xbd, 0x29, 0xfc, 0x2c, 0xf6, 0x3c, 0x07, 0x2a, 0x12,
+ 0xcc, 0xf3, 0x36, 0xd4, 0xf5, 0x02, 0x2c, 0x63, 0x17, 0xd0, 0xf4, 0x16,
+ 0x0a, 0xca, 0x17, 0x40, 0x1c, 0xd4, 0x94, 0xdc, 0x9f, 0x27, 0x93, 0x28,
+ 0x39, 0x8e, 0x8f, 0x5e, 0x57, 0x0b, 0x9b, 0x60, 0x73, 0xa1, 0x2b, 0xd9,
+ 0xc3, 0x4e, 0x5e, 0x1b, 0x1c, 0xaa, 0x5a, 0x0c, 0x80, 0xec, 0x00, 0x15,
+ 0x0d, 0x36, 0xb7, 0xbc, 0x8b, 0xae, 0x1f, 0x24, 0x9b, 0xd8, 0xa8, 0x3b,
+ 0x1d, 0x51, 0xb4, 0x3b, 0x63, 0x33, 0xf1, 0x99, 0x17, 0xbf, 0xaf, 0x9e,
+ 0xd1, 0x25, 0xb2, 0x43, 0x18, 0x07, 0xc2, 0x27, 0xad, 0x5d, 0x77, 0x7a,
+ 0x87, 0x55, 0x83, 0xfc, 0xdd, 0x05, 0x70, 0x95, 0xab, 0xfe, 0x2a, 0x32,
+ 0x82, 0x9a, 0xb0, 0xd5, 0xd4, 0x17, 0x84, 0x97, 0x61, 0xb7, 0x86, 0x08,
+ 0x2c, 0x2c, 0xb5, 0x1a, 0x99, 0x7d, 0x68, 0xfc, 0xc6, 0x82, 0xea, 0x7c,
+ 0x43, 0x0f, 0xbe, 0x8d, 0x9c, 0xbf, 0x2a, 0x9f, 0x84, 0x88, 0x30, 0xc6,
+ 0x69, 0xf2, 0xca, 0x4c, 0x83, 0xef, 0x42, 0x43, 0x05, 0x2f, 0xee, 0xf9,
+ 0xe0, 0x5b, 0xef, 0xd8, 0x5c, 0x1f, 0x04, 0x2b, 0xf5, 0x2f, 0x9f, 0xf5,
+ 0x5c, 0x52, 0x53, 0x1e, 0x1e, 0xaa, 0xe2, 0xab, 0xdf, 0x19, 0x33, 0xbb,
+ 0x5b, 0x07, 0xa5, 0x79, 0x0d, 0xd4, 0xeb, 0x1b, 0xc6, 0x64, 0x24, 0x5d,
+ 0x9e, 0x2a, 0x22, 0x67, 0x08, 0xb6, 0x6f, 0x20, 0xb3, 0xf7, 0x34, 0xc9,
+ 0xac, 0xd3, 0x5f, 0xa6, 0xc2, 0x97, 0xcf, 0xe4, 0x74, 0x47, 0x60, 0x7d,
+ 0x5f, 0xde, 0x64, 0x52, 0x4f, 0xb6, 0x79, 0x2e, 0xad, 0x4d, 0xaa, 0x81,
+ 0x02, 0x7b, 0xce, 0x30, 0xdf, 0xb2, 0xd0, 0xa7, 0xbe, 0x60, 0x39, 0x41,
+ 0x6e, 0x63, 0x50, 0xc7, 0x41, 0x45, 0xcc, 0x62, 0x55, 0x98, 0x7c, 0x43,
+ 0x48, 0x3b, 0xe1, 0xfd, 0xb7, 0xd7, 0x4d, 0x9f, 0xf6, 0x4d, 0x53, 0xdb,
+ 0xcb, 0x48, 0x36, 0x1a, 0x1e, 0xb9, 0x16, 0x34, 0xa2, 0x4f, 0x44, 0x01,
+ 0xfb, 0x97, 0xe9, 0x35, 0x10, 0xf8, 0x9c, 0x74, 0x56, 0x8a, 0x94, 0x3c,
+ 0x31, 0x0e, 0x96, 0xa4, 0x36, 0x54, 0x6d, 0x0f, 0x4a, 0x57, 0x7b, 0xb2,
+ 0xf2, 0xe0, 0x12, 0x87, 0x69, 0xec, 0x0b, 0xa0, 0x0e, 0xdc, 0x7c, 0x60,
+ 0xcb, 0x88, 0x20, 0x5f, 0x57, 0x6f, 0xa3, 0xc8, 0x1a, 0x1c, 0x95, 0x8f,
+ 0x8d, 0xed, 0xd6, 0x90, 0x6a, 0x50, 0x6b, 0x78, 0x8b, 0x8b, 0x98, 0x01,
+ 0x21, 0xf7, 0xf5, 0x67, 0xf5, 0xa7, 0xb8, 0xd7, 0xbd, 0xf4, 0x0c, 0x77,
+ 0x12, 0x63, 0xe7, 0x4e, 0xde, 0x3d, 0x1f, 0x61, 0x2d, 0xb1, 0x3c, 0xc2,
+ 0x7e, 0x4e, 0x78, 0x8c, 0x30, 0x4a, 0x8b, 0xcf, 0xfb, 0x38, 0xcd, 0xc2,
+ 0x87, 0xd1, 0x27, 0xa0, 0x6d, 0xde, 0x71, 0x41, 0xc8, 0x0e, 0x44, 0xc5,
+ 0x60, 0xc9, 0x6d, 0xca, 0xee, 0xf9, 0xe5, 0x9b, 0xbc, 0xf7, 0x8f, 0x03,
+ 0x8d, 0x25, 0x19, 0xe9, 0xe6, 0xdf, 0xf4, 0x7e, 0xc1, 0x84, 0x9c, 0x2d,
+ 0x80, 0xfd, 0x55, 0xb0, 0x42, 0xd6, 0xd4, 0x09, 0xb6, 0x37, 0x7b, 0xee,
+ 0x2f, 0xa9, 0x81, 0xba, 0x6b, 0xd5, 0xc6, 0xd8, 0x38, 0xf5, 0x49, 0xed,
+ 0xa7, 0x18, 0x79, 0x7f, 0x8e, 0x36, 0x97, 0x8f, 0x3f, 0x83, 0x19, 0x49,
+ 0xea, 0x8d, 0x90, 0xb6, 0x59, 0x5c, 0xe4, 0x03, 0xe0, 0x2e, 0xbb, 0x3c,
+ 0xf4, 0x5a, 0xf0, 0x01, 0x16, 0xd1, 0x6b, 0x35, 0x40, 0x5b, 0x9d, 0x50,
+ 0xf2, 0xaf, 0xdc, 0xf1, 0x39, 0x23, 0xbd, 0xa4, 0xe4, 0x85, 0x19, 0x8f,
+ 0xc6, 0xdc, 0xa1, 0xf9, 0x03, 0x89, 0xea, 0x3a, 0x58, 0x35, 0x5e, 0xb5,
+ 0xa2, 0x7d, 0x42, 0x72, 0xcf, 0xa0, 0x4e, 0xdd, 0xdc, 0x0d, 0x34, 0x8c,
+ 0x0c, 0x5c, 0xc0, 0x2b, 0x53, 0x3e, 0x58, 0x91, 0xc0, 0xc0, 0x6d, 0xef,
+ 0x26, 0x57, 0xd9, 0x1e, 0x5f, 0xd7, 0xec, 0xc8, 0x6e, 0x4c, 0xec, 0xca,
+ 0xda, 0x9d, 0x17, 0x85, 0xd2, 0x74, 0xa3, 0xc0, 0x14, 0xf4, 0x16, 0xb7,
+ 0x53, 0x03, 0xc4, 0x0e, 0x0b, 0x5a, 0x6f, 0xad, 0x8f, 0xde, 0x6b, 0xc1,
+ 0x4b, 0xff, 0x9d, 0xbc, 0x9a, 0x24, 0xd8, 0xe1, 0x30, 0x91, 0x43, 0xdf,
+ 0xd1, 0x0a, 0xe8, 0x25, 0x6b, 0xc2, 0x8f, 0xc1, 0xc6, 0xcb, 0x43, 0xe4,
+ 0xef, 0x1b, 0xc3, 0xba, 0xf5, 0x09, 0xba, 0xbc, 0x06, 0x90, 0xa4, 0x0e,
+ 0x00, 0x7d, 0x54, 0x18, 0xe4, 0xfe, 0x3c, 0xef, 0x48, 0x25, 0x69, 0x3c,
+ 0x7c, 0x31, 0x6e, 0xde, 0x8f, 0xac, 0x47, 0x23, 0xc0, 0x93, 0xab, 0xa0,
+ 0xef, 0xfd, 0x9a, 0xfc, 0x54, 0xd9, 0x3d, 0x0c, 0xd5, 0x43, 0x1a, 0xa9,
+ 0x7a, 0xc2, 0x45, 0x5a, 0xff, 0xca, 0x27, 0x5a, 0x76, 0x3e, 0xb7, 0x48,
+ 0x30, 0x84, 0x9e, 0xc6, 0x09, 0x7c, 0xc1, 0x27, 0xd0, 0x46, 0xc5, 0xac,
+ 0x10, 0x50, 0x03, 0x94, 0xe2, 0xf1, 0x39, 0xb9, 0xf6, 0x44, 0x78, 0x8e,
+ 0xee, 0xaa, 0x20, 0xf0, 0xc0, 0x55, 0xc8, 0xe9, 0x39, 0xa2, 0x16, 0xb8,
+ 0x0b, 0xa8, 0xf1, 0x4d, 0x66, 0xda, 0x1d, 0xfd, 0xee, 0x96, 0x0a, 0x17,
+ 0x92, 0xa7, 0x3a, 0xf2, 0xa2, 0xac, 0xb8, 0x02, 0x40, 0x5d, 0x51, 0x19,
+ 0xd6, 0xad, 0x70, 0x56, 0x9d, 0x68, 0xb5, 0xa0, 0xc2, 0xb3, 0xfa, 0xfb,
+ 0x54, 0xc9, 0xc3, 0xad, 0xf6, 0xb8, 0x69, 0xd1, 0x42, 0x1d, 0x5c, 0x06,
+ 0x63, 0x41, 0x70, 0x5d, 0xd9, 0xaa, 0x31, 0x32, 0xa0, 0x9f, 0x55, 0xaf,
+ 0x0f, 0xfa, 0x0c, 0x01, 0x0f, 0xdd, 0xf4, 0x23, 0x3f, 0xa0, 0xd1, 0x0d,
+ 0x6a, 0xa2, 0xe2, 0xee, 0x9d, 0xfa, 0x5b, 0x74, 0xaa, 0xfe, 0x7c, 0x23,
+ 0x8c, 0x9c, 0xae, 0x89, 0x26, 0xfc, 0x13, 0xca, 0xc6, 0x09, 0xac, 0xc4,
+ 0x2a, 0x5b, 0x5d, 0xdc, 0x52, 0x10, 0xf0, 0x27, 0x4a, 0x98, 0xaa, 0x7f,
+ 0xa1, 0x3b, 0xbc, 0xd2, 0xaf, 0x8d, 0x5e, 0x3d, 0xbe, 0x51, 0xdf, 0x4e,
+ 0xf3, 0xf5, 0xc3, 0xe9, 0x31, 0x72, 0x20, 0x6b, 0xb0, 0x62, 0xe6, 0x80,
+ 0x70, 0x6a, 0x40, 0xa0, 0xe9, 0x79, 0x0d, 0x91, 0x2f, 0x1a, 0x8d, 0x81,
+ 0xb5, 0xe0, 0x2a, 0xc4, 0xec, 0x27, 0x26, 0x76, 0x49, 0x79, 0x5f, 0x69,
+ 0x50, 0x13, 0x1c, 0x31, 0xb3, 0x55, 0x59, 0xf2, 0x0f, 0x68, 0x4a, 0x3b,
+ 0x25, 0x44, 0x45, 0x3e, 0x56, 0x96, 0xb8, 0x21, 0xd5, 0x6c, 0x50, 0xe1,
+ 0x56, 0x47, 0x80, 0x22, 0xd6, 0xe5, 0x8c, 0xf5, 0x78, 0xdb, 0xd4, 0x0b,
+ 0xd3, 0xa9, 0x4d, 0x7b, 0xf4, 0x61, 0x49, 0x3e, 0xe3, 0xf2, 0xd7, 0x16,
+ 0x87, 0xd4, 0x56, 0xda, 0x06, 0x8c, 0xc4, 0x1c, 0xb7, 0xf4, 0x52, 0x4b,
+ 0x84, 0x26, 0xce, 0x78, 0x17, 0xa0, 0x25, 0xaa, 0x6a, 0x11, 0x68, 0x31,
+ 0xa5, 0xc8, 0x5b, 0xb8, 0x06, 0x39, 0xea, 0x0b, 0xf9, 0x79, 0x4d, 0xe0,
+ 0x75, 0x78, 0xb8, 0x43, 0x6d, 0xdd, 0x62, 0x74, 0xdd, 0x0a, 0x83, 0xc5,
+ 0x4c, 0x2e, 0xe4, 0x81, 0x58, 0x6c, 0xab, 0xfd, 0xb1, 0x46, 0x30, 0x74,
+ 0xcc, 0x6e, 0xa7, 0x52, 0xa5, 0x93, 0xe9, 0xf8, 0x9b, 0xb3, 0xc3, 0x28,
+ 0x4d, 0x81, 0xa9, 0x55, 0x74, 0x0e, 0xa2, 0x33, 0xa1, 0xd3, 0xe9, 0xa8,
+ 0x6a, 0xf5, 0x54, 0xaa, 0xc9, 0x2d, 0x31, 0xc1, 0x51, 0x25, 0x86, 0xaa,
+ 0x64, 0x7d, 0x81, 0x91, 0xd0, 0x7c, 0x6d, 0x42, 0xe7, 0x1c, 0x0f, 0xc7,
+ 0x13, 0xa0, 0x1d, 0xb0, 0x1c, 0xa5, 0xeb, 0xbc, 0x44, 0xbe, 0x54, 0x29,
+ 0x0a, 0x8b, 0x26, 0xee, 0xae, 0x88, 0xd0, 0x4b, 0x50, 0xd3, 0x43, 0x7b,
+ 0x3a, 0x4e, 0xd6, 0xe4, 0x6b, 0x41, 0x68, 0xba, 0xe7, 0x3b, 0xae, 0xb0,
+ 0x35, 0x23, 0x92, 0x52, 0xfe, 0x60, 0x31, 0xa2, 0xaf, 0xd2, 0x51, 0xca,
+ 0xb1, 0xa5, 0x1a, 0x04, 0xf3, 0x7e, 0x24, 0x78, 0x89, 0xda, 0xec, 0x2d,
+ 0xca, 0x88, 0x12, 0x5c, 0xf8, 0xab, 0xec, 0xbe, 0x50, 0xb9, 0x9e, 0x28,
+ 0x74, 0x10, 0x8b, 0x0c, 0x06, 0xad, 0xcc, 0x37, 0x57, 0x44, 0x57, 0x38,
+ 0x6b, 0x43, 0x65, 0xc1, 0xe8, 0x4e, 0x7c, 0x22, 0xa8, 0xe3, 0x2b, 0x45,
+ 0x48, 0xd3, 0xd5, 0xdd, 0x2b, 0x79, 0xbf, 0xc9, 0xe7, 0xd0, 0xe5, 0x61,
+ 0xb9, 0x31, 0xd9, 0xe5, 0xb8, 0xe1, 0x37, 0xaf, 0xf8, 0x8b, 0x60, 0xf6,
+ 0xc7, 0xcf, 0xff, 0x4e, 0xaa, 0xd5, 0xf0, 0x3c, 0x33, 0x01, 0x37, 0x54,
+ 0x99, 0x2e, 0xef, 0xbc, 0x05, 0x4b, 0xd1, 0x10, 0xb6, 0xbb, 0xc7, 0x3c,
+ 0x37, 0x6c, 0xb1, 0x95, 0x13, 0xb9, 0x04, 0x2c, 0x2f, 0xec, 0x3e, 0x9f,
+ 0xb1, 0x9b, 0x72, 0x11, 0xab, 0x40, 0xec, 0xee, 0x91, 0x8d, 0x56, 0x66,
+ 0x78, 0x7b, 0x40, 0xa7, 0xb6, 0x5d, 0x77, 0x28, 0xa7, 0x4f, 0x7d, 0x7f,
+ 0x83, 0x29, 0xa9, 0x0e, 0xe4, 0xcf, 0x18, 0x28, 0x95, 0x60, 0x0c, 0x89,
+ 0x3a, 0xb3, 0xb9, 0x52, 0x28, 0x16, 0x76, 0x26, 0x16, 0x40, 0xca, 0xc5,
+ 0x4c, 0x6f, 0xc6, 0x58, 0x61, 0xd0, 0x92, 0x3a, 0x91, 0x18, 0x94, 0x6e,
+ 0xb8, 0x12, 0xc2, 0xfd, 0x7c, 0xaa, 0x29, 0x67, 0x33, 0xb9, 0x12, 0x59,
+ 0x64, 0xe7, 0x0d, 0xd3, 0x05, 0x08, 0x4e, 0xd6, 0xd4, 0xaa, 0xaa, 0xc6,
+ 0x07, 0x6c, 0xa6, 0x57, 0x9d, 0x48, 0x95, 0x8a, 0x33, 0xe6, 0x22, 0x05,
+ 0xa2, 0x18, 0xcc, 0x52, 0x65, 0x2e, 0xc8, 0xfe, 0x28, 0x8e, 0x23, 0x1f,
+ 0x02, 0x4e, 0x5a, 0x07, 0xc0, 0xaf, 0x6c, 0x9b, 0x11, 0x97, 0xc1, 0xdd,
+ 0x4b, 0x6e, 0xc4, 0x90, 0x43, 0x0f, 0xcf, 0x90, 0x1d, 0x9d, 0xf6, 0x9e,
+ 0xf1, 0x77, 0xed, 0x4e, 0x69, 0x28, 0xac, 0x75, 0x7f, 0x7e, 0x9d, 0x76,
+ 0xd9, 0xb2, 0x5a, 0x69, 0x85, 0x21, 0xff, 0x14, 0xd2, 0x79, 0xc1, 0xa7,
+ 0x1f, 0x4a, 0xac, 0x0a, 0xf9, 0x31, 0x38, 0x63, 0x09, 0xfa, 0x4e, 0x84,
+ 0x99, 0xfc, 0x84, 0x40, 0xcc, 0x8f, 0x25, 0xbb, 0xbb, 0x77, 0x58, 0xb4,
+ 0x08, 0x66, 0x59, 0x5d, 0x31, 0xdf, 0xbe, 0x6a, 0xd7, 0xd1, 0xdf, 0x18,
+ 0x34, 0xfc, 0xab, 0x9c, 0xea, 0x5a, 0xd6, 0xb2, 0x31, 0x75, 0x43, 0xd2,
+ 0x80, 0x51, 0xa6, 0x84, 0xba, 0x37, 0xfd, 0x42, 0x00, 0x58, 0x85, 0xa9,
+ 0xa9, 0x57, 0xf2, 0x79, 0xce, 0x9d, 0x00, 0xe6, 0x55, 0xf4, 0x17, 0x14,
+ 0x0a, 0xed, 0x97, 0xdc, 0xb7, 0x4d, 0x7b, 0xed, 0xe4, 0x57, 0x13, 0xcd,
+ 0x92, 0xb3, 0x06, 0xd9, 0x8d, 0xff, 0x8a, 0x0b, 0x36, 0x83, 0x0b, 0x1b,
+ 0x61, 0x24, 0xb4, 0x74, 0x65, 0x93, 0xc8, 0x2a, 0x19, 0xa9, 0x25, 0x39,
+ 0x4b, 0x01, 0xfb, 0xf6, 0x16, 0xf3, 0xf9, 0x17, 0xf5, 0x9b, 0x53, 0xdf,
+ 0x21, 0xea, 0xd4, 0x18, 0x6a, 0xc3, 0x1e, 0x00, 0xfc, 0x1b, 0xd1, 0xd6,
+ 0x75, 0x6f, 0x09, 0x01, 0x53, 0x81, 0x78, 0x74, 0x5e, 0xd4, 0x6d, 0xc7,
+ 0xf2, 0xdf, 0x6d, 0xa2, 0xda, 0x4c, 0x8c, 0xbb, 0xac, 0x50, 0xc4, 0x9a,
+ 0x3a, 0x97, 0x83, 0xd7, 0xfe, 0xe6, 0x38, 0x06, 0x9b, 0xb4, 0x41, 0x00,
+ 0x1a, 0x3b, 0x22, 0x8f, 0x81, 0xcf, 0x65, 0x74, 0xd9, 0x17, 0x3d, 0xd0,
+ 0xe2, 0xe9, 0xbd, 0xf5, 0x3a, 0x19, 0xa1, 0xb6, 0x60, 0xcf, 0xdf, 0x90,
+ 0x06, 0xb5, 0x83, 0x44, 0xd2, 0x7b, 0x6f, 0xb2, 0xb3, 0x1d, 0x8f, 0xdc,
+ 0x8c, 0x26, 0x41, 0x3a, 0x24, 0x06, 0xd8, 0x25, 0x58, 0xa3, 0x16, 0x16,
+ 0xd0, 0x89, 0xa1, 0x33, 0x77, 0x19, 0x1e, 0xa8, 0xce, 0xd1, 0x94, 0xba,
+ 0xb8, 0x21, 0x8b, 0x42, 0x55, 0x26, 0x60, 0x2d, 0x7f, 0xc1, 0x69, 0xdd,
+ 0x2a, 0x24, 0x41, 0x31, 0x40, 0x88, 0x11, 0x24, 0xe0, 0xe7, 0x91, 0x69,
+ 0x12, 0xc2, 0xaa, 0xa6, 0x6e, 0xd6, 0xd2, 0x76, 0x52, 0x92, 0x22, 0x34,
+ 0x73, 0xe8, 0xae, 0xfa, 0x14, 0x0f, 0xb9, 0xb0, 0x40, 0x50, 0x28, 0xfa,
+ 0xb2, 0x6c, 0xa7, 0x2c, 0x2e, 0x79, 0x5d, 0xf8, 0x43, 0x39, 0x16, 0x01,
+ 0xf5, 0x9d, 0x07, 0x42, 0x90, 0xae, 0xac, 0x9d, 0xec, 0x8a, 0xc7, 0x59,
+ 0x56, 0x42, 0x9a, 0x1f, 0xb7, 0xd6, 0x71, 0x26, 0x4f, 0xf6, 0x44, 0xcd,
+ 0x16, 0xad, 0x2e, 0xa2, 0x9a, 0xee, 0xff, 0xf3, 0xaf, 0xdd, 0xdf, 0xf7,
+ 0x29, 0x1a, 0x35, 0x09, 0x35, 0xd0, 0x05, 0xd5, 0x5d, 0x56, 0xcc, 0x14,
+ 0x89, 0x56, 0x3b, 0x96, 0x1f, 0xfe, 0xcc, 0xec, 0x5e, 0x74, 0x61, 0x2e,
+ 0x8f, 0x52, 0xbf, 0x4b, 0x4a, 0xe4, 0x9c, 0x15, 0x3f, 0xcd, 0x47, 0xd6,
+ 0xda, 0x3e, 0x7c, 0x54, 0xad, 0x50, 0xe1, 0x67, 0x7a, 0x32, 0xbe, 0xc9,
+ 0xe0, 0x4f, 0xfe, 0x92, 0x07, 0xa2, 0xf9, 0xd5, 0xe2, 0xdf, 0x27, 0x99,
+ 0x33, 0xc9, 0xed, 0x8f, 0x63, 0x97, 0x8a, 0xdd, 0x44, 0x40, 0x50, 0x21,
+ 0xeb, 0xcf, 0x7c, 0x69, 0xd6, 0xe1, 0xdd, 0x74, 0xcd, 0x56, 0x9b, 0x92,
+ 0xd7, 0xac, 0xdd, 0x77, 0x4c, 0x09, 0x29, 0x19, 0xe3, 0x11, 0xa7, 0xf2,
+ 0x67, 0x46, 0x96, 0x5d, 0xae, 0xa7, 0xe0, 0x57, 0x36, 0x67, 0xd0, 0x96,
+ 0x22, 0x01, 0x05, 0xf7, 0xa2, 0x4c, 0x5e, 0x42, 0x0b, 0x47, 0xd1, 0xf1,
+ 0x69, 0xed, 0x12, 0x18, 0x62, 0x76, 0xde, 0x34, 0x45, 0x49, 0xd9, 0xb8,
+ 0xe4, 0x63, 0xbe, 0x8b, 0x19, 0x22, 0x56, 0xd5, 0xaf, 0xdf, 0x4f, 0x84,
+ 0x33, 0xf3, 0x29, 0xeb, 0xa3, 0x97, 0xd6, 0x74, 0xea, 0x97, 0x35, 0x89,
+ 0xb2, 0x95, 0xf3, 0x32, 0x60, 0xc2, 0x44, 0xdb, 0x3d, 0xba, 0xfa, 0xd3,
+ 0x28, 0x4b, 0x23, 0x7c, 0x41, 0xd3, 0x4f, 0xa9, 0xf4, 0x24, 0x06, 0x76,
+ 0xe9, 0x01, 0x0b, 0x91, 0xb7, 0xd8, 0x4c, 0x34, 0x49, 0x1b, 0xb5, 0xbf,
+ 0x4a, 0xea, 0x74, 0x0b, 0xeb, 0xda, 0x8c, 0x12, 0xc4, 0x96, 0xed, 0x95,
+ 0x29, 0x22, 0x77, 0x1d, 0x65, 0xea, 0x0d, 0xf0, 0x36, 0x47, 0xc8, 0xfc,
+ 0x9c, 0xc5, 0x92, 0x34, 0xba, 0xdb, 0x77, 0xcb, 0x29, 0xfb, 0x96, 0x70,
+ 0xd1, 0x1f, 0x8d, 0xf1, 0x2e, 0xa4, 0xf4, 0x9a, 0xe5, 0x34, 0x49, 0xc2,
+ 0xe0, 0xb1, 0xd3, 0xad, 0xef, 0x9d, 0x50, 0xd9, 0xeb, 0xd6, 0x4e, 0x91,
+ 0x62, 0x0e, 0x89, 0x41, 0x63, 0x85, 0x48, 0x9d, 0xc5, 0xb1, 0xf1, 0xe0,
+ 0x22, 0x87, 0x1a, 0xb8, 0xb8, 0x72, 0xc5, 0xea, 0xf4, 0x74, 0xe1, 0xd8,
+ 0xcc, 0x5b, 0x59, 0x3f, 0x65, 0x37, 0xaa, 0xe9, 0x40, 0x4a, 0x27, 0xae,
+ 0x51, 0xa0, 0x75, 0xd4, 0x13, 0x9b, 0x8c, 0x44, 0x5b, 0x06, 0xe5, 0x15,
+ 0xb5, 0x8f, 0x8a, 0xa4, 0xfa, 0x9e, 0x8a, 0xc2, 0x54, 0xa6, 0x75, 0xf1,
+ 0x3c, 0xf7, 0x62, 0x03, 0x4f, 0x9a, 0xd3, 0xf6, 0x93, 0x7b, 0x61, 0xdf,
+ 0x2f, 0xce, 0xf2, 0xa5, 0xdf, 0xf8, 0xba, 0x9d, 0xed, 0x73, 0x2a, 0xbb,
+ 0xef, 0xbc, 0xac, 0xa3, 0xc6, 0x5a, 0x4d, 0xee, 0x21, 0x8a, 0x5f, 0x9a,
+ 0xd0, 0xec, 0x43, 0x00, 0xa4, 0xf5, 0x03, 0x3a, 0x4f, 0xbc, 0xd2, 0x42,
+ 0xe7, 0x6a, 0x1a, 0x4c, 0x45, 0xb8, 0x3b, 0x31, 0xf1, 0x5a, 0x20, 0x84,
+ 0x3e, 0x9f, 0x4c, 0x14, 0x69, 0xef, 0xf2, 0x22, 0x2a, 0x42, 0x74, 0x14,
+ 0x98, 0x97, 0xb7, 0x21, 0xf6, 0x90, 0xf9, 0x87, 0x8b, 0x79, 0xc7, 0x93,
+ 0x52, 0x81, 0x8d, 0x30, 0x9a, 0x35, 0x3d, 0xc7, 0x87, 0x33, 0x0d, 0xdc,
+ 0xcf, 0x67, 0x11, 0xba, 0x9e, 0x6d, 0x81, 0x67, 0x76, 0x4b, 0x35, 0x75,
+ 0x07, 0x46, 0xc2, 0x6f, 0x45, 0xa6, 0xda, 0xb7, 0x04, 0xa0, 0x3b, 0xe1,
+ 0x60, 0x7c, 0x41, 0xb2, 0xc9, 0x15, 0x65, 0xab, 0xc0, 0xff, 0xf4, 0x31,
+ 0x5b, 0xcd, 0xd4, 0x5c, 0x9b, 0x11, 0xe4, 0x0c, 0x8b, 0x16, 0xc2, 0xbe,
+ 0x0a, 0x75, 0x11, 0x8a, 0x27, 0xaf, 0xe7, 0x8d, 0xf5, 0x3c, 0xe8, 0x33,
+ 0xb0, 0x82, 0x1b, 0x95, 0x25, 0x46, 0xbd, 0xc5, 0x41, 0x33, 0xca, 0x09,
+ 0x5e, 0xdb, 0x82, 0xd3, 0x71, 0x51, 0x4a, 0x15, 0xc1, 0xfd, 0x50, 0x06,
+ 0x31, 0x6b, 0x42, 0x8d, 0x07, 0x65, 0xcf, 0xfc, 0x9f, 0xd7, 0x7b, 0xcb,
+ 0x94, 0x3c, 0xf4, 0xe8, 0x3c, 0x5e, 0x44, 0x7f, 0xc2, 0xb8, 0x58, 0xa2,
+ 0xf7, 0xff, 0xcd, 0x9f, 0x00, 0x74, 0x4e, 0x82, 0xe2, 0xa2, 0x63, 0x53,
+ 0x12, 0x2b, 0x06, 0x0a, 0x57, 0x4b, 0xbf, 0x67, 0xa2, 0x5e, 0x11, 0x36,
+ 0x4a, 0x3a, 0x02, 0x95, 0x16, 0x38, 0xe6, 0x2f, 0x7b, 0x25, 0xe2, 0x9a,
+ 0xc2, 0xa9, 0x32, 0xf2, 0xeb, 0x8b, 0x48, 0xb1, 0xa6, 0xe7, 0xd9, 0xf0,
+ 0x5b, 0xf6, 0x90, 0x59, 0x49, 0xcc, 0xb5, 0x14, 0x53, 0x08, 0x60, 0xc0,
+ 0xd2, 0x89, 0x58, 0xf3, 0xd8, 0x24, 0x27, 0x11, 0xba, 0xda, 0x80, 0x8a,
+ 0xdd, 0x4c, 0x5c, 0x8d, 0xb5, 0x4b, 0xf4, 0x9d, 0xb7, 0xae, 0x2b, 0xc2,
+ 0xe2, 0xce, 0xdc, 0xa7, 0xaf, 0xfc, 0xac, 0x7b, 0x1d, 0xe7, 0x12, 0x2f,
+ 0x8f, 0x42, 0x69, 0x68, 0x55, 0xd9, 0x65, 0x1b, 0x0d, 0xa9, 0x73, 0xc1,
+ 0x79, 0x21, 0x7f, 0xff, 0xdc, 0x88, 0x7b, 0x10, 0x3b, 0x7e, 0x40, 0xab,
+ 0x58, 0x1b, 0x13, 0x8a, 0x2f, 0xfb, 0xe5, 0x27, 0x31, 0xad, 0xc8, 0x66,
+ 0x9c, 0xfd, 0x2c, 0x99, 0x83, 0xc4, 0xfd, 0xe0, 0x87, 0xcf, 0xa1, 0x87,
+ 0xb9, 0xf0, 0x23, 0x17, 0x54, 0xfa, 0x91, 0xaa, 0x8c, 0x54, 0x34, 0xba,
+ 0x1c, 0xae, 0xcb, 0x8e, 0xc0, 0x26, 0xa3, 0x10, 0xe6, 0x26, 0xef, 0xfb,
+ 0x4a, 0xec, 0x71, 0x03, 0x21, 0xd3, 0xa8, 0x78, 0x4b, 0xcc, 0xf2, 0xc4,
+ 0x7b, 0xd2, 0x89, 0x71, 0xe6, 0x4f, 0x09, 0x95, 0x92, 0x77, 0xcc, 0x35,
+ 0xc9, 0x14, 0x5d, 0x28, 0xfb, 0x93, 0xa7, 0xeb, 0x08, 0x5c, 0xd4, 0x01,
+ 0x2b, 0xa2, 0x9d, 0x1a, 0xf7, 0x8e, 0x19, 0x63, 0x95, 0x4f, 0x3d, 0xcd,
+ 0x28, 0xcb, 0x37, 0x1d, 0xd0, 0x99, 0x66, 0x29, 0x02, 0xbc, 0x03, 0x3f,
+ 0xf4, 0xdf, 0xe4, 0x99, 0x73, 0xb2, 0xea, 0x9d, 0xdb, 0xc6, 0x93, 0x3d,
+ 0xf0, 0x45, 0xba, 0x55, 0xdf, 0x3c, 0x44, 0x0a, 0xfc, 0xe9, 0x68, 0x9d,
+ 0x40, 0x71, 0x68, 0x95, 0xb2, 0x38, 0x87, 0x09, 0x3f, 0x2e, 0x58, 0x21,
+ 0x50, 0xca, 0x0f, 0x6a, 0x6e, 0x01, 0xe0, 0xcf, 0xd6, 0xb5, 0xc4, 0x18,
+ 0x43, 0xb3, 0x98, 0x50, 0xf9, 0x3f, 0x93, 0xaa, 0x1d, 0x0f, 0xbe, 0x2b,
+ 0x62, 0x1f, 0x41, 0x12, 0xe0, 0xbb, 0x2a, 0x8d, 0x51, 0x10, 0x50, 0x34,
+ 0x1a, 0xbc, 0x6d, 0x79, 0x04, 0x86, 0x2a, 0x5c, 0x35, 0xd5, 0x0d, 0x10,
+ 0x37, 0x77, 0xbc, 0xaf, 0x90, 0x1f, 0x8e, 0x6e, 0xf4, 0xa3, 0xb5, 0xfd,
+ 0xe8, 0x55, 0x10, 0xdc, 0x7e, 0x69, 0xce, 0x89, 0xbf, 0x29, 0x02, 0xc0,
+ 0x75, 0xd1, 0x97, 0x64, 0x2d, 0xbb, 0x7e, 0xa3, 0x27, 0xa9, 0xa1, 0xf1,
+ 0xcb, 0x8d, 0xb5, 0x0e, 0xe2, 0xa3, 0x30, 0x00, 0xed, 0x68, 0xac, 0xc6,
+ 0x73, 0x64, 0x76, 0x43, 0xc4, 0x09, 0xd5, 0xea, 0xef, 0x91, 0x3b, 0xd4,
+ 0x78, 0x7c, 0x0c, 0xcf, 0x9a, 0xce, 0x3f, 0x98, 0x7d, 0xdb, 0x3d, 0x9b,
+ 0xc5, 0x08, 0x5b, 0x58, 0x6b, 0x6e, 0x21, 0xdc, 0xd6, 0x78, 0x07, 0x1e,
+ 0x8a, 0x10, 0xb2, 0x08, 0x97, 0x0e, 0xf3, 0xc2, 0x2d, 0xdc, 0x76, 0x9f,
+ 0xa8, 0xd3, 0x15, 0x35, 0x35, 0xcd, 0xe5, 0x71, 0xf1, 0x0e, 0xd7, 0x30,
+ 0xca, 0x0e, 0x9a, 0xe7, 0xaa, 0x45, 0xb0, 0x29, 0x8e, 0x87, 0x8e, 0xca,
+ 0xd0, 0x3c, 0xd9, 0xd7, 0x00, 0x75, 0x34, 0xa4, 0xd1, 0xa8, 0x94, 0xb0,
+ 0xf6, 0xe8, 0xa5, 0x43, 0x66, 0xbb, 0x20, 0x98, 0xe6, 0x5c, 0xc6, 0x6a,
+ 0x81, 0x6c, 0xa3, 0xc5, 0xab, 0x38, 0xfc, 0x1a, 0x3b, 0x6c, 0x4a, 0xba,
+ 0x13, 0x38, 0xa7, 0x5a, 0x68, 0xaa, 0x38, 0x1c, 0x68, 0xeb, 0x3c, 0x79,
+ 0x6d, 0x79, 0x09, 0x02, 0xbe, 0x96, 0xd5, 0x9c, 0xf7, 0xc9, 0x42, 0xba,
+ 0x6b, 0x3f, 0xe2, 0x1f, 0x6d, 0x06, 0x61, 0xb8, 0x16, 0x25, 0x1d, 0x93,
+ 0xbd, 0x44, 0x55, 0xeb, 0x67, 0x83, 0xcc, 0x17, 0x71, 0xd6, 0x5d, 0xce,
+ 0x3d, 0x85, 0xc0, 0x0d, 0xba, 0xa3, 0x7d, 0x36, 0x43, 0x5f, 0xf9, 0x8c,
+ 0x9d, 0x30, 0x0e, 0x67, 0x3e, 0x94, 0x36, 0x13, 0x89, 0x75, 0xa0, 0xd7,
+ 0xd2, 0xdd, 0x29, 0x0f, 0xdf, 0x66, 0x2a, 0x2a, 0x06, 0x9c, 0x08, 0xe6,
+ 0xe1, 0x6f, 0xef, 0xea, 0x7a, 0xc7, 0xbd, 0xc7, 0x18, 0x6d, 0xf6, 0xec,
+ 0x1c, 0x40, 0x3d, 0x36, 0xa1, 0x9c, 0x2a, 0xe9, 0x08, 0x9d, 0x7d, 0x03,
+ 0x80, 0x94, 0x2a, 0xf7, 0x04, 0xcd, 0xc6, 0x46, 0x85, 0xdd, 0x5b, 0xc4,
+ 0x3b, 0x4b, 0x43, 0x80, 0x82, 0x05, 0x8d, 0x89, 0xb8, 0x78, 0x8d, 0xda,
+ 0x2c, 0x67, 0x65, 0x12, 0xbb, 0xcc, 0x40, 0x4c, 0x7e, 0xcb, 0x1d, 0x7f,
+ 0x49, 0x1b, 0x80, 0x8f, 0x63, 0x2c, 0x5d, 0x5e, 0x72, 0xe1, 0x44, 0x2c,
+ 0xe8, 0xd1, 0xa1, 0x9d, 0x3f, 0xe0, 0x7d, 0xc2, 0x48, 0x98, 0x6f, 0x94,
+ 0x0d, 0xb6, 0xb1, 0xad, 0xfa, 0xb7, 0x4f, 0x94, 0xef, 0xac, 0x4f, 0xc0,
+ 0xbf, 0xa0, 0x9e, 0x9e, 0x4c, 0xe5, 0x0c, 0x2e, 0x65, 0x60, 0x24, 0xef,
+ 0xa2, 0xec, 0x54, 0xfc, 0x78, 0x77, 0x3c, 0x46, 0x6e, 0x8d, 0xc1, 0xbd,
+ 0x20, 0x21, 0xf5, 0x5d, 0x8b, 0x66, 0x5d, 0xd7, 0x2e, 0x51, 0x52, 0xf5,
+ 0x01, 0x83, 0xa3, 0x10, 0xe3, 0x12, 0x88, 0x8a, 0x4f, 0xd1, 0xbc, 0x55,
+ 0xd0, 0x2f, 0x3c, 0xb6, 0xb7, 0x87, 0x07, 0xb3, 0x7f, 0xf6, 0x76, 0x90,
+ 0x18, 0xa1, 0xef, 0xee, 0xa6, 0x88, 0xbb, 0xb7, 0x24, 0x50, 0x78, 0x20,
+ 0xe5, 0x2e, 0xf7, 0xb5, 0x27, 0xbc, 0x4b, 0xa2, 0x5a, 0x8e, 0x2f, 0x87,
+ 0x7d, 0x57, 0xec, 0x74, 0x38, 0x2f, 0xc9, 0x48, 0x56, 0x28, 0xe3, 0xd3,
+ 0x3d, 0x4b, 0xb2, 0x9d, 0x92, 0x9b, 0xef, 0x57, 0x9b, 0x72, 0x96, 0x3e,
+ 0x9c, 0xd2, 0xf9, 0xf5, 0x10, 0x10, 0xac, 0x3e, 0xd7, 0x5d, 0x3b, 0xbc,
+ 0xf4, 0x55, 0x4b, 0x2b, 0x57, 0xf7, 0xe2, 0x06, 0xdf, 0x3c, 0x04, 0x2a,
+ 0x25, 0xda, 0xf3, 0x97, 0x6c, 0x3e, 0x1c, 0x0a, 0xf7, 0x81, 0xe8, 0xef,
+ 0x2b, 0xda, 0xa8, 0xcc, 0x0e, 0x3c, 0x26, 0xae, 0x18, 0x90, 0x9e, 0x47,
+ 0x93, 0xef, 0x7e, 0xfc, 0x1a, 0x68, 0x49, 0x75, 0x79, 0x59, 0xd6, 0xdd,
+ 0x1c, 0xbe, 0x20, 0x40, 0x0f, 0xca, 0xe3, 0xfa, 0x94, 0x84, 0x20, 0xcf,
+ 0x49, 0x02, 0x3d, 0xcc, 0x91, 0xae, 0xe0, 0x68, 0xfd, 0x33, 0x1d, 0xf7,
+ 0x96, 0x01, 0x6f, 0x7c, 0x46, 0x3c, 0x13, 0x50, 0x9b, 0xd9, 0xc9, 0xb5,
+ 0xb6, 0x3c, 0xce, 0x12, 0x73, 0x78, 0x55, 0x5f, 0x5f, 0x77, 0x40, 0xee,
+ 0xd9, 0x93, 0x99, 0x39, 0x9c, 0x11, 0x91, 0xbd, 0x3e, 0x64, 0xac, 0x4a,
+ 0xcd, 0xe9, 0xbe, 0x43, 0x8e, 0xf3, 0xe6, 0x50, 0x8c, 0x79, 0x8b, 0xf5,
+ 0xea, 0xd1, 0xf4, 0x34, 0xb9, 0x6f, 0x44, 0xb4, 0x21, 0xef, 0xd3, 0x05,
+ 0x92, 0xdd, 0x4c, 0x45, 0x2d, 0x6a, 0x3b, 0x45, 0x39, 0xce, 0xaf, 0xe4,
+ 0x4f, 0xd8, 0xe4, 0x08, 0xfa, 0xec, 0x8b, 0x24, 0xd7, 0x3f, 0x19, 0x12,
+ 0x85, 0x0d, 0x2b, 0x12, 0x33, 0x5c, 0x2d, 0xa1, 0xb1, 0xc0, 0x4c, 0x5c,
+ 0xb4, 0x7a, 0x3f, 0xf6, 0x68, 0xda, 0x2b, 0xaf, 0xe3, 0x79, 0xa1, 0x08,
+ 0xdd, 0xff, 0x3a, 0x31, 0x94, 0x77, 0x6b, 0x10, 0x23, 0x09, 0xe1, 0x43,
+ 0x1b, 0xbb, 0x86, 0xa3, 0x81, 0x41, 0x4a, 0xce, 0x90, 0x0f, 0x46, 0x6b,
+ 0x27, 0x61, 0xe5, 0x25, 0x67, 0x0c, 0xb8, 0xe9, 0x4b, 0x03, 0x40, 0x8c,
+ 0x7f, 0x5a, 0x01, 0xcd, 0x98, 0xb3, 0xd0, 0x21, 0x7b, 0x86, 0x4f, 0xc2,
+ 0x1f, 0x5e, 0x9b, 0x6a, 0x2d, 0x7f, 0x92, 0x23, 0xff, 0x0d, 0x6a, 0xa6,
+ 0x11, 0x86, 0xab, 0x6d, 0xb6, 0x7a, 0xe1, 0xd3, 0xbf, 0xce, 0x8b, 0x09,
+ 0x13, 0xb7, 0x06, 0x4d, 0xd4, 0xdd, 0x4f, 0xfc, 0x09, 0x20, 0x1a, 0xee,
+ 0x5d, 0xb8, 0x43, 0x80, 0x29, 0x5c, 0xe0, 0x69, 0xa2, 0x45, 0x4b, 0x02,
+ 0xe1, 0x84, 0x0b, 0x7e, 0xaf, 0x35, 0xe2, 0xd9, 0x79, 0xb2, 0x6c, 0x7b,
+ 0x5d, 0x38, 0x8a, 0x23, 0x62, 0x87, 0x27, 0x46, 0x3d, 0xf9, 0xe8, 0x22,
+ 0xf6, 0x8c, 0x8d, 0x8d, 0x75, 0xf3, 0xa6, 0x65, 0x75, 0xdd, 0x79, 0xc8,
+ 0xc2, 0x01, 0xf7, 0x1f, 0x54, 0x39, 0x9c, 0xe0, 0xbe, 0x99, 0x4e, 0xbc,
+ 0xcf, 0x12, 0x2f, 0x81, 0xdb, 0xc0, 0xef, 0x72, 0x07, 0x16, 0x92, 0x18,
+ 0xcf, 0x3d, 0xb9, 0x7f, 0x4e, 0x49, 0xa5, 0x22, 0x28, 0x2d, 0x4c, 0x0e,
+ 0x55, 0x3d, 0x4d, 0xa8, 0xb7, 0xf4, 0xf8, 0xc5, 0xfa, 0x24, 0xc7, 0xbf,
+ 0x72, 0x74, 0x61, 0x47, 0x72, 0xa0, 0xe3, 0x5e, 0x9d, 0x9f, 0xf6, 0x14,
+ 0x75, 0x14, 0x3b, 0x32, 0xd4, 0xad, 0x71, 0x25, 0x03, 0x28, 0x98, 0x61,
+ 0x9d, 0x9c, 0xf6, 0xf7, 0xc4, 0x38, 0xa3, 0xaa, 0xa0, 0x2a, 0x19, 0x34,
+ 0x23, 0x73, 0xd2, 0xfe, 0x0f, 0x42, 0x48, 0xec, 0xe2, 0xcb, 0xcb, 0xed,
+ 0xd7, 0x74, 0x72, 0xde, 0xb4, 0xeb, 0x68, 0x02, 0xa5, 0x7c, 0x10, 0x3e,
+ 0x58, 0x51, 0xa2, 0x8e, 0x79, 0x2f, 0x29, 0x45, 0x05, 0xa9, 0x99, 0xc0,
+ 0xa1, 0xe6, 0xd6, 0xae, 0xe6, 0xbd, 0x57, 0x7b, 0xa9, 0x25, 0x07, 0x8e,
+ 0x74, 0x58, 0x0c, 0x00, 0x8f, 0x8e, 0x58, 0x06, 0xb7, 0x8c, 0x7e, 0xfa,
+ 0x36, 0x0e, 0xc2, 0x7f, 0x37, 0x59, 0x45, 0x9e, 0x85, 0x21, 0x18, 0x48,
+ 0xe7, 0x79, 0x68, 0x42, 0x0c, 0x5e, 0x59, 0xe2, 0x6b, 0xe5, 0x10, 0xff,
+ 0x75, 0x16, 0x4e, 0x81, 0xc6, 0x29, 0x89, 0x60, 0xee, 0xb0, 0x56, 0x86,
+ 0x04, 0x0f, 0xfe, 0x93, 0x28, 0x92, 0x70, 0x52, 0x38, 0x5c, 0xcd, 0x9c,
+ 0xde, 0x13, 0x4e, 0x6a, 0x5e, 0x05, 0x5b, 0xeb, 0xeb, 0x0f, 0x22, 0x34,
+ 0x56, 0xac, 0x84, 0x64, 0xd9, 0x9c, 0x14, 0xbe, 0xe9, 0x6e, 0x2f, 0x5d,
+ 0xf5, 0xb7, 0x91, 0x95, 0x1a, 0x97, 0x62, 0xd6, 0xbd, 0x2a, 0xfe, 0x11,
+ 0x91, 0x69, 0x38, 0x08, 0x9c, 0x11, 0xe5, 0xe5, 0x46, 0xcf, 0x94, 0x0c,
+ 0x0d, 0xdb, 0x49, 0x67, 0x5a, 0x21, 0xd0, 0x19, 0xff, 0xd9, 0x4d, 0xfc,
+ 0x28, 0xa7, 0x2a, 0xac, 0x1f, 0x66, 0x11, 0xf7, 0x6e, 0x4d, 0x41, 0x6a,
+ 0x7f, 0x77, 0x08, 0xba, 0x5a, 0xfe, 0xe0, 0xa7, 0xed, 0xe4, 0xd0, 0x50,
+ 0xe6, 0x21, 0x5a, 0xa3, 0xcd, 0xb2, 0xa8, 0xff, 0x12, 0xf9, 0x1d, 0xad,
+ 0x7a, 0xd9, 0xa8, 0x22, 0xb6, 0xd2, 0x76, 0x75, 0xa5, 0x6c, 0xca, 0xf3,
+ 0x59, 0xe4, 0x25, 0x58, 0x9e, 0x1d, 0x05, 0xbf, 0x53, 0x45, 0x35, 0xd1,
+ 0x34, 0xaf, 0xe6, 0xac, 0x43, 0x53, 0xd4, 0xe4, 0x61, 0x2d, 0xf1, 0xf4,
+ 0xc7, 0x9c, 0xb5, 0xca, 0x1a, 0xc1, 0x32, 0x41, 0xe6, 0x43, 0x4b, 0xc9,
+ 0x02, 0xa5, 0x14, 0xf4, 0x72, 0x8c, 0xd4, 0x97, 0x2e, 0xbc, 0xee, 0xa8,
+ 0xd2, 0x53, 0xaf, 0xe1, 0x80, 0x58, 0xbd, 0xa9, 0x25, 0xa0, 0x0c, 0x4e,
+ 0x4c, 0x4d, 0x67, 0x27, 0x3b, 0x6b, 0xd0, 0xf6, 0x2c, 0x0f, 0x6e, 0xc8,
+ 0xbc, 0x0f, 0x12, 0xea, 0xce, 0x34, 0xe1, 0x1c, 0xe7, 0x36, 0x7a, 0x9b,
+ 0xe8, 0xa8, 0xb8, 0x23, 0xa8, 0x4b, 0x77, 0x5f, 0x77, 0x99, 0xde, 0x36,
+ 0x2b, 0x6a, 0x1d, 0x6a, 0xb5, 0x5d, 0x48, 0x40, 0x0a, 0x4d, 0x20, 0x89,
+ 0x1e, 0xf4, 0x14, 0x8e, 0x27, 0x9f, 0x74, 0x07, 0x10, 0x69, 0x59, 0xed,
+ 0xcb, 0x47, 0x64, 0x9a, 0x37, 0x26, 0x37, 0x03, 0x17, 0xaf, 0xc8, 0x6a,
+ 0x93, 0x4f, 0xce, 0xdf, 0x2c, 0x53, 0x6a, 0x36, 0x76, 0xe7, 0xc3, 0xbf,
+ 0x09, 0x38, 0x92, 0x3a, 0x53, 0xe8, 0x07, 0xe2, 0xf2, 0xe1, 0xc2, 0x9f,
+ 0xd8, 0x7e, 0x4c, 0x27, 0xf0, 0x44, 0xa4, 0x91, 0x3a, 0xc7, 0x62, 0xf6,
+ 0x86, 0x31, 0x3a, 0xaa, 0x38, 0x30, 0x41, 0xa7, 0x6b, 0xb2, 0x63, 0x83,
+ 0x99, 0xf1, 0x7e, 0x64, 0x47, 0xc1, 0x95, 0x53, 0x26, 0x15, 0x37, 0x06,
+ 0xa1, 0x30, 0x50, 0xb9, 0x24, 0x63, 0xf9, 0xd7, 0xca, 0x96, 0xeb, 0xf6,
+ 0xf3, 0x6a, 0xdb, 0x5e, 0x9b, 0x57, 0x1c, 0x47, 0x94, 0xfe, 0x92, 0xd3,
+ 0x4e, 0x35, 0x25, 0xb8, 0xa3, 0xb2, 0xc8, 0xa5, 0xda, 0xb8, 0x22, 0xc0,
+ 0xa5, 0x68, 0x5c, 0x16, 0x0b, 0xd2, 0x33, 0x1b, 0x89, 0x35, 0x1f, 0xf8,
+ 0x47, 0x86, 0x6b, 0x0d, 0xc4, 0x7c, 0x1a, 0x50, 0x48, 0x84, 0xc3, 0x8c,
+ 0x59, 0x88, 0x08, 0x26, 0x4e, 0x85, 0xd2, 0x8c, 0x37, 0x65, 0x86, 0x7d,
+ 0x47, 0x0b, 0xc3, 0x9f, 0xd2, 0x3e, 0x06, 0x1d, 0x05, 0x06, 0xf2, 0xa4,
+ 0xc3, 0x3b, 0xae, 0xd3, 0x02, 0xae, 0x44, 0xbb, 0xd0, 0xc7, 0x6e, 0xd2,
+ 0x82, 0x14, 0x4c, 0xfd, 0xf6, 0x69, 0x8a, 0x0c, 0x2e, 0x04, 0xc0, 0x28,
+ 0xd5, 0x20, 0x7d, 0xb0, 0x7d, 0x7c, 0xad, 0xb6, 0x6d, 0x36, 0x1a, 0x72,
+ 0xe8, 0x4f, 0x9f, 0x7b, 0x69, 0x8c, 0xaf, 0x45, 0x23, 0x5e, 0x37, 0xc5,
+ 0x97, 0x2c, 0xbd, 0x24, 0x54, 0x35, 0x6d, 0xec, 0x1a, 0xcc, 0x26, 0xdf,
+ 0xb3, 0x8a, 0xac, 0x71, 0xd0, 0x8c, 0x8c, 0xeb, 0xa7, 0x9b, 0x5d, 0x89,
+ 0xfd, 0x94, 0x67, 0x3a, 0x0a, 0x6e, 0xef, 0x95, 0x1c, 0xaa, 0xfc, 0x82,
+ 0xb7, 0xdb, 0xaa, 0x2c, 0xd5, 0x14, 0x58, 0xdd, 0xd7, 0x11, 0xa1, 0xed,
+ 0x75, 0x1f, 0x77, 0xf9, 0xb9, 0xf5, 0xbf, 0x24, 0x8b, 0xcc, 0x54, 0x89,
+ 0x1d, 0x04, 0x0d, 0x6a, 0x3d, 0xf8, 0xc1, 0xd4, 0xc1, 0xae, 0x1c, 0xcc,
+ 0xb1, 0xde, 0xfe, 0xf9, 0x45, 0xab, 0xd8, 0x37, 0xb1, 0x56, 0x66, 0x15,
+ 0xf4, 0x9d, 0x59, 0x16, 0xc8, 0x67, 0x8a, 0x0d, 0x25, 0x69, 0x3f, 0x8a,
+ 0x56, 0x16, 0x7d, 0xca, 0x69, 0x98, 0xac, 0x0e, 0x8c, 0x0e, 0xf6, 0x3a,
+ 0xb5, 0x5f, 0xd2, 0x3c, 0xe6, 0xa4, 0x3b, 0x67, 0x4a, 0xa5, 0x2d, 0xff,
+ 0xfd, 0x04, 0x04, 0xb3, 0xf1, 0x75, 0xbb, 0x8c, 0x9c, 0xf3, 0x39, 0x7d,
+ 0xfa, 0x9e, 0x9c, 0x55, 0x63, 0x76, 0xb2, 0x45, 0xf4, 0x18, 0x7b, 0xbf,
+ 0x31, 0xbc, 0xfa, 0x5d, 0xed, 0x9f, 0x97, 0x8a, 0x4c, 0xf1, 0x5b, 0xd9,
+ 0x44, 0xad, 0x7f, 0x14, 0xc6, 0x5c, 0xbb, 0x75, 0x0c, 0xe3, 0x5b, 0xd3,
+ 0xba, 0x57, 0x79, 0x5a, 0x99, 0x79, 0xad, 0xb4, 0x9e, 0x9c, 0x18, 0x7c,
+ 0x2f, 0x2b, 0x07, 0x8d, 0xa0, 0x9d, 0x98, 0xfc, 0xe3, 0xe5, 0x54, 0x51,
+ 0x17, 0xb8, 0xde, 0x5b, 0xd0, 0xf1, 0x43, 0x39, 0xae, 0x6d, 0xaf, 0xb5,
+ 0x77, 0xd8, 0x35, 0xa5, 0xbe, 0x57, 0x6d, 0xe2, 0xc8, 0x82, 0x59, 0x63,
+ 0xc8, 0x23, 0xd5, 0xc1, 0x86, 0x05, 0xd5, 0xdf, 0x2f, 0x70, 0x16, 0x2e,
+ 0x47, 0x42, 0x30, 0xab, 0x18, 0x4d, 0xc6, 0x9a, 0x54, 0x18, 0x9f, 0x86,
+ 0x4a, 0x02, 0x36, 0x47, 0x05, 0xd6, 0xce, 0x6a, 0x94, 0x88, 0xd5, 0xe8,
+ 0xc7, 0x48, 0xa1, 0x2a, 0x91, 0xc7, 0xf9, 0x97, 0x23, 0x06, 0xc6, 0xcb,
+ 0xad, 0xf2, 0x4e, 0x29, 0xb9, 0x84, 0x8c, 0x07, 0x4f, 0x46, 0x21, 0x0a,
+ 0xbe, 0x80, 0xc4, 0x24, 0xe8, 0x3e, 0x78, 0xbe, 0xd1, 0xd2, 0xa4, 0x32,
+ 0x1d, 0x59, 0xbc, 0x9e, 0x75, 0xbd, 0x84, 0x45, 0x34, 0xde, 0x6e, 0x66,
+ 0x69, 0x68, 0x83, 0xc1, 0x68, 0x8c, 0x59, 0xa3, 0xb6, 0xab, 0xe0, 0xb5,
+ 0x81, 0x22, 0x0e, 0x56, 0x7d, 0x55, 0x1e, 0xd0, 0xa4, 0x50, 0x7b, 0xe1,
+ 0x9a, 0xc3, 0xb1, 0x86, 0x0a, 0x8b, 0xcb, 0x9b, 0x85, 0x57, 0xc6, 0x81,
+ 0xfe, 0x32, 0xd1, 0x61, 0x47, 0xbc, 0xdf, 0x70, 0x94, 0xf5, 0x9c, 0xc4,
+ 0xb9, 0xc5, 0xe0, 0xab, 0x33, 0x30, 0xbb, 0xd1, 0x8f, 0xa3, 0xb4, 0x48,
+ 0x4e, 0x35, 0x60, 0x2c, 0x0a, 0x97, 0x9f, 0x0e, 0xbe, 0x15, 0xcf, 0x1a,
+ 0x95, 0x92, 0xfa, 0x16, 0x74, 0xce, 0x41, 0x0c, 0x8f, 0xcc, 0x6e, 0x4e,
+ 0x32, 0xb8, 0x40, 0x96, 0x4f, 0x5d, 0x4c, 0x6e, 0xb1, 0xd8, 0x28, 0x6f,
+ 0x69, 0x43, 0x08, 0x9d, 0xb3, 0xf5, 0x97, 0x7f, 0x13, 0xf1, 0xbf, 0x13,
+ 0x25, 0x03, 0xc0, 0x5a, 0xa0, 0x98, 0x1e, 0x75, 0xa8, 0xfd, 0x50, 0x1e,
+ 0xbc, 0x8a, 0x23, 0xa7, 0x41, 0x18, 0x10, 0x7c, 0x42, 0xb9, 0x5f, 0xa4,
+ 0x2b, 0xf4, 0x09, 0x05, 0xad, 0x10, 0xc0, 0x29, 0xa8, 0xeb, 0xce, 0x41,
+ 0xff, 0x4c, 0x45, 0x5f, 0x47, 0x7e, 0x7b, 0x4f, 0xeb, 0xf6, 0x5e, 0xbe,
+ 0xe6, 0xa5, 0x56, 0x72, 0x7c, 0x7b, 0x58, 0x43, 0x05, 0x20, 0x11, 0x0a,
+ 0x02, 0x0e, 0x86, 0xb1, 0x9a, 0x51, 0xb9, 0xa3, 0x2f, 0x14, 0x69, 0x5e,
+ 0x15, 0xe5, 0x30, 0x26, 0x56, 0xff, 0x3f, 0xeb, 0xae, 0x53, 0x2e, 0xc6,
+ 0x5c, 0x94, 0x1a, 0x78, 0xd4, 0xa1, 0x36, 0x27, 0xe6, 0x72, 0x1c, 0x11,
+ 0x70, 0xdb, 0xb6, 0x5f, 0x47, 0x13, 0xfe, 0xcf, 0xc4, 0x50, 0x92, 0x3d,
+ 0xda, 0x8f, 0x96, 0xe7, 0x00, 0xf7, 0x73, 0xc7, 0x52, 0xad, 0x42, 0x34,
+ 0x03, 0x5f, 0x5d, 0x2c, 0x4e, 0x37, 0xcc, 0xff, 0xad, 0x6f, 0x72, 0xb8,
+ 0x75, 0x23, 0x95, 0x2c, 0xb9, 0x89, 0x8e, 0xe7, 0xff, 0x7a, 0x99, 0x76,
+ 0xa6, 0x88, 0xa1, 0x39, 0xdc, 0x67, 0x5d, 0x90, 0xc8, 0x63, 0x3c, 0xa2,
+ 0x80, 0x31, 0xc1, 0x9b, 0x08, 0xc6, 0x48, 0x5e, 0x22, 0x4b, 0xb1, 0x54,
+ 0xeb, 0x59, 0x66, 0xc8, 0xe0, 0x95, 0x38, 0x53, 0x1d, 0x7b, 0x46, 0xd1,
+ 0xc9, 0x6e, 0xf9, 0x07, 0x62, 0xcb, 0xcf, 0xff, 0x4f, 0x29, 0xfb, 0xab,
+ 0x3c, 0x8f, 0x0c, 0x04, 0xaf, 0x9e, 0x25, 0x38, 0x50, 0x44, 0xfa, 0xdf,
+ 0xbd, 0x34, 0x1a, 0xf2, 0x21, 0x33, 0x77, 0xae, 0xf1, 0xe4, 0x64, 0x15,
+ 0x82, 0x1f, 0x78, 0x01, 0x5a, 0x6d, 0x80, 0xe5, 0xa3, 0x53, 0x54, 0xa6,
+ 0xfe, 0xa7, 0x82, 0x7c, 0x39, 0xb7, 0xc9, 0xcb, 0x22, 0x2c, 0x32, 0x9c,
+ 0x17, 0xf0, 0x9f, 0x5c, 0x9b, 0xde, 0x46, 0x82, 0x74, 0x88, 0x47, 0xb8,
+ 0xe4, 0x57, 0x88, 0x4d, 0xb5, 0x4c, 0x1c, 0x8f, 0x38, 0x9c, 0x9e, 0x42,
+ 0xb2, 0x10, 0x3c, 0x50, 0xcd, 0x6a, 0xc8, 0x80, 0x17, 0x24, 0x9a, 0xb9,
+ 0x8c, 0x4b, 0x16, 0x99, 0xa1, 0xf7, 0x96, 0xd1, 0x5c, 0xbc, 0x8e, 0x23,
+ 0xcb, 0x64, 0x7d, 0xd4, 0x1e, 0x3f, 0x97, 0xc9, 0x77, 0x0e, 0x95, 0x80,
+ 0x54, 0x53, 0x65, 0x91, 0x3e, 0x9c, 0xee, 0x3b, 0xd6, 0x91, 0x65, 0x46,
+ 0xee, 0x47, 0x07, 0x40, 0x0f, 0x4b, 0xa4, 0x21, 0xc2, 0x3b, 0x4a, 0x75,
+ 0xae, 0x3b, 0xef, 0x6a, 0x03, 0xa3, 0xcf, 0xb5, 0x29, 0x8d, 0xcc, 0x59,
+ 0xfc, 0xf6, 0xdf, 0x56, 0xe5, 0x4a, 0x93, 0x59, 0x06, 0x8e, 0x98, 0x62,
+ 0x15, 0xbd, 0xd2, 0x40, 0xac, 0xa0, 0xca, 0x21, 0x6d, 0xda, 0x4c, 0x2f,
+ 0x7e, 0x9b, 0xc7, 0xde, 0x5c, 0xf4, 0x13, 0x79, 0xfa, 0xea, 0x6c, 0x90,
+ 0x08, 0xed, 0x32, 0xa0, 0xb2, 0x5d, 0xed, 0x11, 0x1c, 0xeb, 0xe2, 0x32,
+ 0x48, 0x7f, 0xcb, 0x50, 0x6a, 0x64, 0xeb, 0xad, 0x86, 0x49, 0x71, 0x97,
+ 0xce, 0x07, 0xe5, 0x15, 0x86, 0x70, 0x04, 0xf5, 0xd7, 0xa7, 0x6f, 0x2b,
+ 0xb7, 0x4a, 0xff, 0x85, 0xc6, 0x90, 0xe2, 0x4d, 0x62, 0x9b, 0x0a, 0x77,
+ 0xae, 0xae, 0x86, 0x6a, 0xb6, 0x3a, 0x6d, 0x41, 0xd1, 0xfa, 0x6d, 0x5a,
+ 0x2d, 0x98, 0xd5, 0xab, 0x68, 0x12, 0x7e, 0x91, 0xfe, 0x8f, 0xca, 0xb5,
+ 0x1d, 0x2b, 0xbb, 0x99, 0x12, 0xbe, 0x55, 0x09, 0xf8, 0xfa, 0xa9, 0x6b,
+ 0x08, 0x46, 0x1d, 0x73, 0xfa, 0xcf, 0xd3, 0x07, 0x1e, 0x65, 0xa7, 0x9f,
+ 0xa7, 0xb4, 0xba, 0x6f, 0x4d, 0xbe, 0xf2, 0xf8, 0xe2, 0x70, 0xc4, 0x0e,
+ 0x09, 0xb5, 0xf6, 0xb9, 0xb0, 0xe2, 0xa6, 0x6f, 0x3d, 0x4e, 0x56, 0xf2,
+ 0xe2, 0x4d, 0xfa, 0x75, 0x16, 0x40, 0x00, 0xf2, 0x40, 0xb8, 0x70, 0xe3,
+ 0xd4, 0xb3, 0x43, 0x3b, 0x0e, 0x33, 0x03, 0xe3, 0xf8, 0x11, 0x15, 0x87,
+ 0xdb, 0x68, 0x14, 0x64, 0xd0, 0x3e, 0xac, 0x7a, 0x1c, 0xdf, 0x40, 0x92,
+ 0x80, 0x80, 0xb4, 0xcb, 0xf1, 0x75, 0x04, 0x29, 0x32, 0x59, 0xdd, 0x4d,
+ 0x7e, 0x7e, 0x96, 0x66, 0x02, 0x33, 0x1d, 0x0f, 0x4f, 0x8e, 0xa8, 0x64,
+ 0xaf, 0xc8, 0x1f, 0x8c, 0xd6, 0x35, 0x36, 0x62, 0x29, 0x5a, 0xd4, 0xad,
+ 0xd7, 0x9e, 0x49, 0x23, 0x01, 0xb4, 0xb1, 0xc8, 0x07, 0x46, 0x3c, 0x33,
+ 0x8d, 0x0f, 0xf3, 0x9e, 0x6f, 0x11, 0x37, 0x1f, 0x78, 0x57, 0xbe, 0xbc,
+ 0x8a, 0x9b, 0xe3, 0x33, 0x8f, 0xde, 0x1b, 0x8d, 0x2f, 0x9e, 0x5d, 0xec,
+ 0x6a, 0xd7, 0x6b, 0xad, 0xb1, 0x3a, 0xd0, 0x10, 0xb3, 0xe4, 0x4e, 0xc8,
+ 0xcf, 0x15, 0x49, 0xcb, 0x79, 0x13, 0x0c, 0xc7, 0x57, 0x51, 0x62, 0xdd,
+ 0xec, 0xff, 0x18, 0x6b, 0x72, 0x6d, 0x24, 0x90, 0xb8, 0x73, 0x38, 0x05,
+ 0x88, 0xba, 0x69, 0x47, 0x79, 0xc5, 0x53, 0xbf, 0xba, 0x96, 0x69, 0x81,
+ 0x32, 0xa1, 0xbf, 0x4f, 0x87, 0xdc, 0xd9, 0xa2, 0x46, 0x3f, 0xf1, 0xbe,
+ 0xa0, 0x67, 0x80, 0xc2, 0x82, 0xd4, 0x8a, 0x10, 0x10, 0xa5, 0x23, 0xe2,
+ 0xb8, 0xd1, 0x37, 0xe5, 0xd2, 0x6a, 0x6f, 0xf2, 0xc0, 0xd9, 0x6a, 0xdc,
+ 0x04, 0xda, 0xd2, 0xe6, 0x12, 0x6a, 0x81, 0xfb, 0xff, 0x90, 0x31, 0x30,
+ 0xfb, 0xf9, 0xb4, 0xed, 0x6a, 0xa4, 0x2b, 0x90, 0x6d, 0xcc, 0x9c, 0xba,
+ 0x2b, 0xd0, 0x63, 0x9f, 0x5c, 0xd3, 0xe7, 0x37, 0xb5, 0xdd, 0x6b, 0x0a,
+ 0xe8, 0xbd, 0x9e, 0xd2, 0x12, 0x61, 0x19, 0x6f, 0x2e, 0x31, 0x56, 0x63,
+ 0x5e, 0x47, 0xa4, 0x72, 0xa9, 0x9e, 0x4a, 0x6b, 0x37, 0x15, 0x1d, 0xfb,
+ 0xa9, 0x16, 0x49, 0xfc, 0x00, 0x33, 0x94, 0x75, 0xa6, 0x46, 0x58, 0x82,
+ 0x06, 0xc9, 0xba, 0xb4, 0xed, 0x7f, 0x2d, 0x8c, 0x0b, 0x70, 0xca, 0x77,
+ 0x66, 0x22, 0xf8, 0x75, 0x68, 0xb2, 0x17, 0x4a, 0x00, 0x2a, 0x3e, 0x29,
+ 0x04, 0x37, 0xdf, 0x9e, 0x8b, 0x09, 0x42, 0xdd, 0x84, 0xad, 0x85, 0x58,
+ 0xf9, 0xe7, 0x48, 0x3b, 0xcb, 0x1c, 0xa2, 0x5b, 0x1a, 0xda, 0x17, 0x2e,
+ 0x5c, 0xa0, 0x60, 0xb8, 0x62, 0x94, 0x31, 0x10, 0x99, 0xa1, 0x2d, 0xec,
+ 0x67, 0x41, 0xad, 0x5e, 0x87, 0x28, 0x86, 0x37, 0x91, 0xf1, 0x46, 0x74,
+ 0x9b, 0xa5, 0x57, 0xf7, 0x45, 0xd7, 0xe0, 0xcd, 0x73, 0xd3, 0x9e, 0x74,
+ 0x12, 0x35, 0x4c, 0xc5, 0x20, 0x28, 0x9c, 0x19, 0xe3, 0x36, 0x65, 0x65,
+ 0x28, 0xf9, 0x22, 0xc6, 0x6b, 0x7d, 0x33, 0x57, 0xd3, 0xda, 0x0b, 0xbb,
+ 0xbe, 0xba, 0xc1, 0x8a, 0x24, 0x8d, 0xf3, 0x02, 0xe7, 0x3f, 0x27, 0xec,
+ 0xb2, 0x2f, 0xf8, 0x67, 0x5a, 0x78, 0x4b, 0xb5, 0x20, 0xab, 0x1c, 0x01,
+ 0x83, 0x14, 0x06, 0xd4, 0x64, 0xc2, 0x45, 0x6c, 0x54, 0x1e, 0xa1, 0x0f,
+ 0x96, 0x5a, 0x83, 0x38, 0xe7, 0x45, 0x3d, 0xd7, 0x12, 0x8b, 0xd2, 0xf2,
+ 0x8a, 0xe2, 0x43, 0x61, 0x26, 0x64, 0xae, 0x61, 0x0b, 0x77, 0xdd, 0x5a,
+ 0x0d, 0x8a, 0x48, 0x86, 0x54, 0x21, 0xe9, 0x54, 0x74, 0x4e, 0x1f, 0x93,
+ 0xf2, 0x6b, 0x71, 0xe3, 0xe5, 0xaf, 0x56, 0x78, 0x99, 0xdd, 0xe5, 0xd2,
+ 0x8b, 0x03, 0x27, 0xfa, 0x80, 0xbe, 0x11, 0x9a, 0x9b, 0xe9, 0xaa, 0x1a,
+ 0x1e, 0xc3, 0xf9, 0x47, 0xa3, 0xbc, 0x19, 0x6d, 0x4c, 0x55, 0xac, 0x88,
+ 0x80, 0xaf, 0x07, 0x01, 0x22, 0xb2, 0xfd, 0xb6, 0xb1, 0x93, 0x18, 0xb6,
+ 0x16, 0xa1, 0x8b, 0x66, 0x92, 0x00, 0x43, 0xdf, 0x0b, 0x97, 0x80, 0x55,
+ 0x01, 0xb6, 0x58, 0xb8, 0x97, 0xcf, 0xc2, 0xe9, 0x8b, 0x2d, 0x94, 0x73,
+ 0xa6, 0xae, 0x52, 0x1b, 0x65, 0x13, 0x29, 0x0b, 0xad, 0x68, 0x5a, 0x60,
+ 0xa4, 0xed, 0x51, 0x61, 0x1f, 0xeb, 0x26, 0xfe, 0x6c, 0xf9, 0xe2, 0xbb,
+ 0x48, 0x47, 0xa5, 0xf3, 0x96, 0xdd, 0x77, 0x17, 0xed, 0x98, 0x5c, 0xc6,
+ 0x2c, 0xf3, 0x4e, 0x90, 0xb7, 0x8f, 0xf0, 0x84, 0x59, 0x00, 0x46, 0x7e,
+ 0x0b, 0xc4, 0x15, 0xa1, 0xe7, 0x1b, 0xfd, 0xd5, 0x8a, 0x45, 0x27, 0xf4,
+ 0xa7, 0x9a, 0x22, 0x92, 0x16, 0x0a, 0xb3, 0x65, 0xcf, 0xb3, 0x14, 0xe8,
+ 0x92, 0x48, 0x54, 0x5c, 0xc7, 0xa4, 0xd2, 0xfc, 0xf4, 0x8c, 0x61, 0x6f,
+ 0x3d, 0x64, 0x34, 0x8b, 0x13, 0x9b, 0x09, 0x3d, 0xef, 0xe0, 0x08, 0xfd,
+ 0x50, 0xbd, 0xe8, 0x4f, 0xdb, 0x1e, 0x6e, 0xfa, 0x80, 0x94, 0x5c, 0x69,
+ 0xb3, 0x7d, 0x24, 0xb2, 0xd9, 0x6a, 0x0a, 0x34, 0x97, 0xdb, 0x36, 0xf9,
+ 0xc8, 0x46, 0x61, 0x12, 0x0e, 0x43, 0xc1, 0x19, 0xf8, 0xc4, 0xa9, 0x54,
+ 0x10, 0x02, 0x0d, 0xc8, 0x1a, 0x42, 0xd8, 0xcc, 0x95, 0x3d, 0x87, 0x93,
+ 0x3c, 0x32, 0x72, 0xd3, 0x67, 0x70, 0x17, 0x59, 0x9f, 0x5b, 0xea, 0x11,
+ 0xa4, 0x3d, 0x64, 0x5c, 0xea, 0xfc, 0x6f, 0x4e, 0x97, 0x2e, 0x57, 0x43,
+ 0x26, 0x7e, 0x98, 0x21, 0x49, 0x4c, 0xfc, 0x63, 0x69, 0xff, 0xe0, 0xa8,
+ 0x2f, 0x87, 0x01, 0x03, 0x69, 0xca, 0x99, 0xa6, 0x3b, 0x8d, 0x86, 0xb4,
+ 0xdf, 0xb7, 0x10, 0xde, 0x9f, 0x53, 0x6e, 0x49, 0x06, 0x7f, 0x96, 0xd5,
+ 0x2c, 0x1c, 0xb7, 0xf3, 0x16, 0xf5, 0xd4, 0x63, 0x18, 0x61, 0x10, 0x6e,
+ 0x67, 0xb3, 0x81, 0x26, 0xf1, 0x64, 0x5a, 0xd2, 0xab, 0x57, 0x63, 0xe7,
+ 0xa6, 0x11, 0x09, 0xce, 0x19, 0xd0, 0x69, 0x73, 0xfd, 0xaa, 0x5a, 0xc8,
+ 0xa0, 0xc0, 0x55, 0xca, 0x08, 0x72, 0x7d, 0xaa, 0x06, 0x27, 0x22, 0x65,
+ 0x1c, 0xe0, 0x26, 0x86, 0xf2, 0x25, 0x9f, 0xdc, 0x28, 0x0a, 0xad, 0x30,
+ 0x0d, 0x71, 0x97, 0xfc, 0xef, 0x24, 0x6b, 0x80, 0x1f, 0x45, 0x84, 0x67,
+ 0xed, 0x80, 0x08, 0x5d, 0xc7, 0x16, 0xa7, 0x28, 0x5b, 0x96, 0xb7, 0xc4,
+ 0xd7, 0xa7, 0x05, 0xa6, 0x9b, 0xbf, 0xa7, 0x80, 0xf7, 0xae, 0x6c, 0xb9,
+ 0x2c, 0x24, 0x64, 0x3b, 0x59, 0xf1, 0xf9, 0x32, 0x97, 0x12, 0x18, 0x6b,
+ 0x95, 0x4c, 0x43, 0x3a, 0x57, 0xe0, 0x8c, 0xdb, 0x8e, 0x06, 0x87, 0xfc,
+ 0x20, 0x91, 0x05, 0xea, 0x71, 0x59, 0xe8, 0x95, 0xab, 0x6d, 0xd7, 0xe1,
+ 0xfe, 0x57, 0x99, 0xdd, 0xe8, 0x9e, 0x4d, 0x6c, 0x8a, 0xe3, 0x32, 0x00,
+ 0xb6, 0x6f, 0x6d, 0x2a, 0x56, 0xb6, 0x5d, 0x71, 0x97, 0xed, 0xbf, 0xcf,
+ 0xcc, 0x0f, 0xc5, 0xdc, 0x64, 0x94, 0x34, 0xe5, 0x08, 0xa6, 0x71, 0x65,
+ 0x08, 0x10, 0x9c, 0x2c, 0x52, 0xe5, 0x8d, 0x67, 0x5c, 0xe5, 0x44, 0xd8,
+ 0xb9, 0x18, 0xdd, 0x89, 0x36, 0x68, 0x74, 0x8c, 0x35, 0x5b, 0xa8, 0xe9,
+ 0xbe, 0x97, 0xc5, 0x8f, 0xfc, 0x40, 0x31, 0xd4, 0x62, 0xed, 0x81, 0xd8,
+ 0xf1, 0x7e, 0x12, 0x01, 0x66, 0x92, 0xb2, 0x25, 0x39, 0xcd, 0xde, 0xb6,
+ 0x9f, 0xd9, 0xf9, 0x8c, 0x2f, 0xe6, 0xbc, 0x57, 0x9d, 0x69, 0x2a, 0x98,
+ 0xdb, 0xfc, 0xf7, 0xfd, 0x89, 0xcd, 0xc4, 0xb6, 0x5c, 0x60, 0xc9, 0xdd,
+ 0x77, 0xf7, 0xda, 0x4c, 0x3a, 0x11, 0xd8, 0xf5, 0x8e, 0x7e, 0xfc, 0xff,
+ 0x3f, 0x8d, 0x00, 0x71, 0xf1, 0x6a, 0x5e, 0xcc, 0x94, 0x4f, 0x53, 0xf1,
+ 0x60, 0x19, 0x10, 0x56, 0x4e, 0xe9, 0x56, 0xff, 0x08, 0xa5, 0xda, 0xf8,
+ 0x0e, 0x76, 0x91, 0x19, 0x8b, 0x7a, 0x79, 0xde, 0x77, 0xb6, 0xda, 0xef,
+ 0x1d, 0x4c, 0x22, 0x44, 0x11, 0xfa, 0x5a, 0x98, 0x9e, 0xf1, 0x28, 0xd7,
+ 0xfa, 0xad, 0xb2, 0x17, 0x53, 0x79, 0x1a, 0x29, 0xaa, 0x0d, 0x64, 0xf8,
+ 0xf5, 0xf6, 0xd4, 0x2e, 0x90, 0x26, 0xde, 0xce, 0x98, 0xa2, 0x5a, 0x46,
+ 0xe5, 0x78, 0xf7, 0x17, 0x8b, 0xb6, 0xc8, 0xda, 0xa4, 0xcd, 0x99, 0xf1,
+ 0x3f, 0x4b, 0x85, 0x34, 0xce, 0x3b, 0xe4, 0x60, 0xa5, 0x08, 0xc5, 0x4a,
+ 0xfd, 0xf5, 0x4b, 0x29, 0xd4, 0xbb, 0x6e, 0x98, 0x5e, 0x0d, 0xf1, 0x32,
+ 0x76, 0x70, 0xac, 0x5e, 0x9f, 0xcb, 0x8d, 0x4c, 0xa9, 0xfd, 0x72, 0xc5,
+ 0x39, 0x4a, 0x23, 0x3c, 0xb3, 0xd9, 0x35, 0xee, 0xf3, 0x51, 0xbf, 0x7e,
+ 0x28, 0x82, 0x9d, 0xda, 0xfb, 0x0a, 0x35, 0x4f, 0x72, 0xd6, 0x10, 0xa1,
+ 0x1c, 0x84, 0x30, 0x6e, 0x40, 0x4f, 0x82, 0x54, 0xec, 0x00, 0x58, 0x24,
+ 0xea, 0xa3, 0xdb, 0x14, 0x9e, 0x29, 0xd2, 0x12, 0xc0, 0xd2, 0x23, 0xce,
+ 0x8b, 0xa1, 0x9d, 0x4b, 0xe4, 0x14, 0x92, 0xce, 0xe0, 0xaa, 0xc2, 0xeb,
+ 0xac, 0x26, 0x0c, 0x66, 0x49, 0x40, 0xd6, 0xba, 0x1d, 0x06, 0xc2, 0x43,
+ 0x51, 0x74, 0x7b, 0xcd, 0x10, 0x56, 0x35, 0xd9, 0x11, 0x9b, 0xed, 0x2a,
+ 0xa2, 0x3b, 0x94, 0x02, 0xd3, 0x1f, 0x60, 0xc4, 0x23, 0x63, 0x50, 0xe2,
+ 0x73, 0xbd, 0x2e, 0x0d, 0x1b, 0xee, 0xa2, 0x89, 0xfd, 0x84, 0x6b, 0x4c,
+ 0x00, 0xdd, 0x41, 0x2a, 0x07, 0x14, 0x99, 0x41, 0xb5, 0x72, 0x5e, 0xf7,
+ 0xe7, 0xb5, 0xfc, 0xe1, 0x4f, 0xc4, 0xa1, 0x57, 0x73, 0x10, 0xdd, 0x86,
+ 0xee, 0x21, 0x16, 0xb2, 0xe2, 0x74, 0x01, 0x04, 0x85, 0xc4, 0x8d, 0x9f,
+ 0xe9, 0x96, 0xb5, 0x0f, 0x29, 0xe0, 0x40, 0x9b, 0xc4, 0x69, 0xac, 0xe5,
+ 0xa0, 0xfd, 0x58, 0x81, 0xdf, 0x9f, 0x51, 0x5c, 0x98, 0x20, 0x4c, 0x86,
+ 0xc0, 0x8d, 0x6e, 0xfa, 0x96, 0x8b, 0xe7, 0xc1, 0xe1, 0x3b, 0x46, 0x5c,
+ 0xd2, 0x6a, 0x26, 0xf5, 0x88, 0xc9, 0xf0, 0x43, 0xa1, 0x04, 0x1d, 0xb9,
+ 0x7c, 0xbd, 0x4c, 0xfa, 0x7d, 0x88, 0x7e, 0xf4, 0x2f, 0xf5, 0x28, 0x25,
+ 0xcd, 0xfc, 0x6b, 0xb4, 0xba, 0xef, 0x52, 0xba, 0x6f, 0x7d, 0xb2, 0xb4,
+ 0x24, 0x26, 0xd5, 0x4d, 0x80, 0xa4, 0x98, 0x8c, 0x9b, 0x84, 0x46, 0xde,
+ 0xb6, 0x4d, 0x09, 0x38, 0x7f, 0xd1, 0xeb, 0x97, 0x97, 0xe8, 0x98, 0x1c,
+ 0xf8, 0x7b, 0x24, 0xb5, 0xd4, 0x95, 0xd4, 0x3e, 0x1b, 0x1e, 0xd0, 0x1f,
+ 0x1f, 0x90, 0x01, 0x45, 0xd9, 0x92, 0xb1, 0xc8, 0xed, 0x4e, 0x1e, 0xf9,
+ 0x0a, 0x67, 0xd1, 0xf6, 0x8e, 0xb9, 0x6c, 0x4c, 0x5a, 0x22, 0x09, 0x1f,
+ 0xfa, 0x4c, 0xc8, 0x5d, 0xdb, 0xca, 0xeb, 0x1a, 0x76, 0x79, 0xb5, 0x4a,
+ 0xa2, 0xcc, 0xf6, 0xe9, 0xff, 0x7d, 0x86, 0x2d, 0xee, 0x94, 0x83, 0x50,
+ 0xc3, 0x29, 0x38, 0x05, 0x56, 0x42, 0xd7, 0x19, 0x38, 0x6f, 0xef, 0xe9,
+ 0x0a, 0xeb, 0xf0, 0x83, 0xcb, 0x1f, 0x3b, 0x1a, 0x6c, 0xf1, 0x11, 0x50,
+ 0xef, 0x38, 0xdf, 0x5b, 0xd4, 0x5b, 0x2a, 0x18, 0x8c, 0x35, 0x57, 0xdb,
+ 0x57, 0xda, 0x3f, 0x76, 0xa8, 0x57, 0xb3, 0xee, 0x04, 0x39, 0x1c, 0x21,
+ 0x98, 0xc6, 0x41, 0x6d, 0xd7, 0xe0, 0xb0, 0x61, 0xa3, 0x9d, 0xb7, 0xb8,
+ 0x78, 0xf7, 0x39, 0x66, 0xba, 0x44, 0x4c, 0x69, 0xac, 0xa2, 0xdf, 0xca,
+ 0xd7, 0x8f, 0xee, 0x85, 0xa3, 0xb6, 0x42, 0xfa, 0x16, 0x08, 0x56, 0x6d,
+ 0x00, 0x41, 0xd0, 0x91, 0xcd, 0x72, 0x46, 0xad, 0x1d, 0xf4, 0xaa, 0xd0,
+ 0x1d, 0xb2, 0xcf, 0xbb, 0x4d, 0x5d, 0xf6, 0xda, 0xf6, 0x6d, 0xf4, 0x6d,
+ 0x91, 0xad, 0xc8, 0xe6, 0xbd, 0x89, 0x5d, 0x57, 0xc6, 0xbe, 0xad, 0x07,
+ 0x73, 0xda, 0xf1, 0xf7, 0x36, 0x53, 0xda, 0x84, 0x89, 0xc1, 0x44, 0xeb,
+ 0x88, 0x58, 0xc2, 0xcf, 0x9b, 0x90, 0x05, 0x9a, 0xba, 0xb2, 0x23, 0x95,
+ 0xf0, 0x61, 0xca, 0xcf, 0xa7, 0x4c, 0xdf, 0xc7, 0x17, 0x9f, 0x33, 0x51,
+ 0x0c, 0x4f, 0x8d, 0xab, 0xc6, 0x8f, 0x0a, 0xed, 0xf3, 0xd3, 0x85, 0x05,
+ 0xd7, 0x09, 0xec, 0x23, 0x56, 0x61, 0xd5, 0x65, 0x86, 0xa6, 0x7e, 0xdd,
+ 0x31, 0x63, 0x6e, 0x40, 0xf1, 0x59, 0xa4, 0x09, 0x8f, 0x47, 0x61, 0x4b,
+ 0xb0, 0x40, 0x29, 0x11, 0xed, 0xfa, 0xca, 0x67, 0x0b, 0xaf, 0x5b, 0xbb,
+ 0xd6, 0xda, 0x4f, 0xb5, 0x37, 0xc8, 0xe4, 0x5d, 0x1f, 0x46, 0xb8, 0x76,
+ 0xfb, 0xff, 0x5f, 0xa3, 0xab, 0x77, 0x63, 0x64, 0x54, 0xab, 0x48, 0x4f,
+ 0x70, 0x0d, 0x4b, 0x33, 0xcd, 0xf3, 0x98, 0xdf, 0xee, 0x15, 0xb1, 0x0e,
+ 0x3e, 0x44, 0x73, 0x5d, 0x5a, 0xdf, 0xfe, 0x94, 0x97, 0x13, 0xa8, 0x2c,
+ 0x32, 0x61, 0xcf, 0xba, 0x84, 0x8b, 0x2f, 0xd1, 0x30, 0x2b, 0xea, 0x13,
+ 0x0e, 0xed, 0x87, 0xbc, 0x7f, 0xd9, 0x73, 0x54, 0xbb, 0xfd, 0x9e, 0xd7,
+ 0x5a, 0x36, 0xfc, 0xfd, 0xbe, 0x36, 0x86, 0xc6, 0xc9, 0xf2, 0x18, 0x46,
+ 0x05, 0x80, 0x13, 0xbb, 0xe7, 0xe2, 0x7d, 0x33, 0x3a, 0xc6, 0x6e, 0x84,
+ 0x57, 0xef, 0x6f, 0x4a, 0xb9, 0x8e, 0x5e, 0x1e, 0xbf, 0xed, 0xd9, 0x6d,
+ 0xe7, 0xb9, 0x6f, 0x05, 0xe5, 0x5c, 0x5d, 0x4c, 0xd5, 0x7f, 0x24, 0xb8,
+ 0xe6, 0xa8, 0xee, 0xbe, 0x4f, 0x5a, 0xf1, 0xd5, 0xdc, 0xf1, 0x8f, 0xa8,
+ 0xe0, 0xbd, 0xa3, 0x0c, 0x63, 0xb5, 0x45, 0x0f, 0xbd, 0xd4, 0x1a, 0xcd,
+ 0xfe, 0x83, 0x78, 0x06, 0x35, 0xb6, 0x14, 0xaa, 0x54, 0xec, 0x73, 0x5a,
+ 0x7a, 0x1d, 0x5e, 0xec, 0xea, 0xe7, 0xc5, 0xe5, 0x16, 0x52, 0x45, 0x12,
+ 0xd3, 0x3d, 0xf6, 0x20, 0x43, 0x9e, 0xf9, 0x13, 0xb6, 0xcb, 0x95, 0x80,
+ 0xfc, 0xbc, 0xb1, 0x6a, 0x8d, 0x57, 0xcb, 0x45, 0x2c, 0x72, 0x6f, 0xb6,
+ 0x19, 0xbe, 0x0f, 0x01, 0xce, 0x06, 0xe7, 0xb9, 0x37, 0x78, 0xfa, 0x30,
+ 0x7e, 0x52, 0x14, 0x52, 0xcd, 0x2e, 0x0f, 0xc2, 0x8a, 0x77, 0xd9, 0xf2,
+ 0xbf, 0x05, 0x73, 0x67, 0x53, 0xc7, 0xfb, 0x79, 0xe1, 0x88, 0x10, 0xcf,
+ 0xab, 0x26, 0xf5, 0xe0, 0x76, 0xd4, 0x5f, 0xa3, 0x82, 0xc5, 0xe1, 0x77,
+ 0x50, 0xe5, 0x5c, 0xde, 0xe6, 0xd2, 0xf7, 0x7b, 0xf6, 0x41, 0xa2, 0xb9,
+ 0xe6, 0x4d, 0x89, 0x00, 0x30, 0xb4, 0x82, 0x35, 0xec, 0x85, 0x7c, 0x63,
+ 0x0f, 0x3d, 0xb9, 0x3d, 0xb1, 0x2d, 0x72, 0xaa, 0xc9, 0xa7, 0xdf, 0x48,
+ 0x1c, 0x0e, 0x0e, 0x16, 0xd4, 0x4e, 0x9a, 0xf5, 0x64, 0x25, 0xe3, 0x14,
+ 0xbe, 0xd3, 0xc4, 0xac, 0xd3, 0xfe, 0xf3, 0xcf, 0x07, 0x69, 0x83, 0xae,
+ 0xda, 0x7b, 0xc9, 0x0f, 0x14, 0xe7, 0xbb, 0xab, 0x9a, 0x08, 0x44, 0xd5,
+ 0xd7, 0x01, 0x18, 0x79, 0x85, 0xbc, 0x56, 0x0c, 0x8f, 0x87, 0x27, 0x52,
+ 0x96, 0x4b, 0x6f, 0x0e, 0x02, 0xc0, 0x81, 0xae, 0x65, 0x87, 0x63, 0xd2,
+ 0xac, 0x46, 0x51, 0xdb, 0x44, 0xba, 0xbf, 0x96, 0x43, 0x52, 0xf1, 0x64,
+ 0xc6, 0x73, 0x53, 0xf5, 0xa2, 0xe2, 0xe8, 0x28, 0x57, 0x4b, 0xe3, 0x25,
+ 0xeb, 0x7c, 0x9e, 0x58, 0x38, 0xe5, 0x1d, 0x10, 0xf1, 0x60, 0xdc, 0x0d,
+ 0xa7, 0x4c, 0xd6, 0x12, 0x3e, 0x18, 0x5c, 0x5d, 0x38, 0x38, 0x06, 0xf7,
+ 0x66, 0x13, 0xf8, 0x97, 0x90, 0x8c, 0x50, 0x37, 0xa5, 0xd3, 0x50, 0xfa,
+ 0xd7, 0x7e, 0xf9, 0x56, 0x20, 0xa5, 0x3e, 0x6b, 0x07, 0xc8, 0xfc, 0xc5,
+ 0x34, 0x93, 0x76, 0x6e, 0x32, 0x9d, 0xfa, 0x3d, 0x3f, 0x63, 0xc9, 0xf8,
+ 0x22, 0xf2, 0xf2, 0xbb, 0xfa, 0x21, 0x8f, 0xf2, 0x47, 0xd7, 0x59, 0x04,
+ 0xc8, 0x91, 0x61, 0xaa, 0x9f, 0x7f, 0x9f, 0x53, 0xce, 0xa7, 0x84, 0xf6,
+ 0x38, 0x79, 0x9e, 0xa2, 0x8b, 0x8d, 0x0f, 0x52, 0xab, 0xae, 0x5e, 0xb2,
+ 0x10, 0xd2, 0xaf, 0x30, 0x76, 0xb8, 0x04, 0xf0, 0xcd, 0x1b, 0x44, 0xef,
+ 0xd3, 0x62, 0xa1, 0x13, 0xbd, 0x26, 0x0d, 0x8e, 0x5f, 0x35, 0xd0, 0x38,
+ 0x37, 0xf0, 0x03, 0x26, 0x6f, 0x50, 0x20, 0x10, 0x28, 0xbe, 0xae, 0x3e,
+ 0x63, 0x8b, 0x38, 0xc0, 0x11, 0x2c, 0x4e, 0x32, 0x3d, 0xbe, 0x25, 0xaf,
+ 0x01, 0xc3, 0x66, 0x42, 0x6c, 0xbd, 0x00, 0x90, 0xcb, 0x50, 0x7b, 0x75,
+ 0xc8, 0x85, 0x99, 0x5c, 0xae, 0x94, 0x36, 0xbc, 0x98, 0x9f, 0xdb, 0x09,
+ 0x61, 0xd2, 0x30, 0x5f, 0x23, 0xf6, 0x5b, 0x41, 0x96, 0xb7, 0xcb, 0x7c,
+ 0x20, 0x26, 0x09, 0x0c, 0x0e, 0x57, 0x8b, 0x33, 0x66, 0x73, 0x2e, 0x58,
+ 0xd4, 0x7c, 0x6f, 0xe7, 0x94, 0x8b, 0x1c, 0xe9, 0x7b, 0xcb, 0x28, 0x73,
+ 0xce, 0xfd, 0xfb, 0x58, 0x8e, 0xbf, 0x51, 0x7f, 0x61, 0x4b, 0x4e, 0xaa,
+ 0x82, 0x3a, 0xcb, 0xe2, 0xd4, 0x2e, 0x30, 0x09, 0x6f, 0x25, 0xa5, 0x3a,
+ 0xe7, 0xfb, 0xac, 0x13, 0x1e, 0xb3, 0x16, 0xc8, 0x33, 0xfe, 0x6d, 0x65,
+ 0xa8, 0xb6, 0xf2, 0xf0, 0x00, 0x9f, 0xed, 0x36, 0xa7, 0xf6, 0xa9, 0xf7,
+ 0x0c, 0x42, 0x61, 0xe3, 0x49, 0x68, 0x5c, 0xdc, 0xc8, 0x51, 0x3c, 0x85,
+ 0xcf, 0xda, 0xe0, 0xac, 0x29, 0xa3, 0x65, 0xe7, 0x38, 0x3c, 0xba, 0x0c,
+ 0x7d, 0x12, 0xf9, 0x0a, 0x51, 0x96, 0x51, 0x01, 0x0f, 0xae, 0xf3, 0xbd,
+ 0x76, 0xd3, 0x06, 0x7a, 0x5d, 0x0e, 0x41, 0x53, 0x21, 0x3c, 0xc3, 0x4a,
+ 0x69, 0xdd, 0x5d, 0x39, 0x60, 0x80, 0x49, 0x76, 0xbe, 0x34, 0xaf, 0x28,
+ 0x37, 0x01, 0x17, 0xbb, 0x88, 0x21, 0xb5, 0x40, 0xfa, 0x0a, 0x2a, 0x9c,
+ 0xa5, 0xfe, 0xf6, 0x2a, 0x23, 0x49, 0xc5, 0xd7, 0x17, 0xa6, 0x98, 0x55,
+ 0xa7, 0x53, 0xe3, 0xe5, 0x48, 0xdb, 0xbc, 0x72, 0x0d, 0x69, 0xf0, 0xc1,
+ 0x40, 0xda, 0x1f, 0x37, 0x1c, 0x24, 0xb8, 0x0e, 0xd1, 0x47, 0xf7, 0x36,
+ 0x19, 0xc4, 0xa5, 0xdd, 0x46, 0x3d, 0x6c, 0x92, 0x76, 0x67, 0x1f, 0x3c,
+ 0x77, 0xaf, 0x14, 0x24, 0xf2, 0x09, 0x7a, 0x12, 0xaa, 0x3f, 0xf5, 0xe5,
+ 0x7d, 0x08, 0x3f, 0x80, 0xc5, 0xe6, 0x32, 0xc2, 0x46, 0x07, 0xa7, 0xc7,
+ 0xe1, 0xef, 0xca, 0x8d, 0x07, 0xed, 0x79, 0xa1, 0x42, 0x5a, 0xd3, 0x4a,
+ 0x90, 0xf6, 0x2d, 0x5d, 0x53, 0xa1, 0x7d, 0x30, 0xc0, 0xc7, 0x9f, 0x40,
+ 0xdc, 0x22, 0x5c, 0x8e, 0x18, 0x72, 0x85, 0x31, 0x3d, 0x86, 0x2c, 0x37,
+ 0xea, 0xd5, 0xd6, 0x1e, 0xa0, 0x1c, 0xaf, 0x18, 0x26, 0x60, 0xaf, 0x44,
+ 0xd3, 0x06, 0xef, 0x10, 0x32, 0xf2, 0x57, 0x1e, 0x97, 0xe5, 0x45, 0xbb,
+ 0x84, 0x14, 0x67, 0x0f, 0x29, 0x9d, 0x3a, 0x78, 0xa4, 0x95, 0x05, 0x8d,
+ 0xbf, 0x3d, 0xd0, 0x5d, 0x08, 0x98, 0xc8, 0xf4, 0xad, 0xa8, 0x65, 0xd1,
+ 0xf4, 0x5c, 0xa6, 0x5a, 0xc0, 0xd0, 0xfd, 0xe2, 0xed, 0x47, 0xe9, 0x98,
+ 0x43, 0xef, 0x97, 0x35, 0xbe, 0xff, 0xcb, 0x79, 0x50, 0x7d, 0x30, 0xcb,
+ 0xd8, 0x9f, 0x54, 0xf8, 0xc4, 0x1b, 0x27, 0x40, 0xa8, 0x33, 0x0f, 0xce,
+ 0x69, 0xf2, 0xcb, 0x82, 0x0b, 0x1f, 0x80, 0x9c, 0x69, 0xc4, 0x7f, 0xde,
+ 0x75, 0x88, 0x62, 0x3a, 0xfa, 0x69, 0x10, 0x5b, 0x26, 0x47, 0xd5, 0xcb,
+ 0xb5, 0xfc, 0x45, 0x21, 0x42, 0xe5, 0xf5, 0xa5, 0xf0, 0x26, 0x93, 0x76,
+ 0x6f, 0x2a, 0x3e, 0x4e, 0x9d, 0x3f, 0xeb, 0x85, 0xbf, 0x1c, 0x7f, 0x84,
+ 0x9f, 0xba, 0xc1, 0x18, 0x15, 0x96, 0x2e, 0x00, 0x61, 0x9b, 0x57, 0x8b,
+ 0x6d, 0x6d, 0xfd, 0x1d, 0x24, 0x7b, 0x72, 0x43, 0x6b, 0x38, 0xcd, 0x66,
+ 0x79, 0x02, 0x79, 0x76, 0x07, 0x52, 0x1f, 0x6b, 0x9d, 0xb0, 0x9b, 0x59,
+ 0x8a, 0x0e, 0x89, 0x86, 0xf3, 0x48, 0x08, 0xbf, 0xbf, 0x2b, 0x50, 0x87,
+ 0x1c, 0x22, 0x4e, 0x56, 0xbe, 0x85, 0x10, 0x57, 0x3a, 0x93, 0xbc, 0xf3,
+ 0xce, 0x2b, 0x3c, 0xa1, 0xdb, 0x44, 0x55, 0x2e, 0x6d, 0x63, 0x9f, 0x28,
+ 0x43, 0xe3, 0xbd, 0x0b, 0x99, 0x0c, 0xf8, 0x5d, 0x2e, 0x75, 0xeb, 0x86,
+ 0xb0, 0x5d, 0x0f, 0x48, 0xbb, 0x7f, 0x2c, 0xbd, 0xe0, 0x4b, 0x2f, 0xd9,
+ 0xb7, 0x36, 0x1a, 0x4c, 0xfe, 0x61, 0x6e, 0xe6, 0x38, 0x28, 0x27, 0xf6,
+ 0x74, 0xd9, 0xd7, 0xbf, 0xf3, 0x74, 0x20, 0x87, 0xb1, 0xa6, 0x21, 0xf2,
+ 0xa9, 0x57, 0xb5, 0xc2, 0x2e, 0x35, 0x53, 0x4b, 0x55, 0xbe, 0xc5, 0x4c,
+ 0x15, 0x84, 0x1f, 0xda, 0xf0, 0xa8, 0x4d, 0x79, 0xc7, 0x19, 0xb6, 0xac,
+ 0x98, 0x02, 0xc3, 0x3d, 0x95, 0x9a, 0x4c, 0x17, 0x7a, 0x8a, 0x4b, 0x84,
+ 0xb1, 0x87, 0x20, 0x01, 0x59, 0x79, 0x8e, 0x95, 0xe1, 0xf0, 0x02, 0x18,
+ 0x45, 0xcf, 0x0d, 0x26, 0xb0, 0x7d, 0xbd, 0x3d, 0x60, 0x34, 0xba, 0xdc,
+ 0x27, 0xb8, 0x9a, 0x0c, 0x22, 0x05, 0x9b, 0xe8, 0x60, 0xce, 0x50, 0x58,
+ 0xe1, 0xbd, 0xd0, 0x5e, 0x8e, 0xf7, 0x8c, 0x3d, 0x94, 0x72, 0x8c, 0x48,
+ 0x5b, 0x36, 0x87, 0x8f, 0x1a, 0xd4, 0x50, 0xc1, 0x3d, 0xa8, 0xf7, 0xc1,
+ 0xd9, 0x0c, 0x37, 0x22, 0xb5, 0xca, 0xf7, 0x52, 0x7c, 0xbe, 0x6d, 0x2f,
+ 0x0d, 0x4a, 0xa2, 0x1a, 0x6c, 0x47, 0xa5, 0x2b, 0x40, 0xf2, 0xc1, 0x99,
+ 0xfd, 0x75, 0xe1, 0xb6, 0xc2, 0x20, 0xa3, 0x0e, 0x65, 0x87, 0x1a, 0xfd,
+ 0x4d, 0x96, 0xea, 0x71, 0xf5, 0x84, 0x7a, 0xd2, 0x7c, 0x52, 0xb0, 0xef,
+ 0xba, 0x4c, 0x74, 0x99, 0xe2, 0x67, 0x2d, 0x76, 0x3f, 0xcc, 0x04, 0x2e,
+ 0x2f, 0x0b, 0x82, 0xf0, 0xa0, 0xd2, 0x76, 0x07, 0xe5, 0x5c, 0x43, 0xde,
+ 0x85, 0x92, 0xa4, 0x12, 0x03, 0x2d, 0xbf, 0xb1, 0x9e, 0x26, 0x43, 0xfa,
+ 0xf5, 0xe2, 0x6d, 0x7f, 0x5d, 0x8a, 0xf8, 0x58, 0xf9, 0x28, 0x60, 0xfa,
+ 0x0f, 0x79, 0x83, 0x2e, 0x81, 0x4a, 0xa7, 0xa7, 0x19, 0x54, 0x0b, 0x3c,
+ 0x83, 0x89, 0xf5, 0xfc, 0xc2, 0xa2, 0x8f, 0x87, 0x66, 0x70, 0x85, 0xbe,
+ 0xe3, 0xb8, 0x9f, 0xfe, 0xa5, 0xf9, 0x6e, 0x71, 0xa1, 0xb4, 0x52, 0x39,
+ 0x84, 0x83, 0x60, 0x22, 0x50, 0x77, 0x73, 0x31, 0x1c, 0xfb, 0x9f, 0x9d,
+ 0xb7, 0xab, 0x2a, 0x50, 0x19, 0x61, 0x31, 0xe1, 0xb5, 0xf5, 0x8e, 0x57,
+ 0xd3, 0xde, 0x99, 0x35, 0x09, 0x9b, 0xab, 0x68, 0x68, 0x12, 0x21, 0xbd,
+ 0x47, 0xc5, 0x02, 0xc4, 0xee, 0xf1, 0xa4, 0x53, 0x08, 0xd5, 0x85, 0xab,
+ 0x92, 0xbc, 0xbf, 0x4b, 0x7b, 0xba, 0x9d, 0x68, 0x2e, 0x29, 0x0e, 0x63,
+ 0xfa, 0x9c, 0xd5, 0xed, 0x17, 0xef, 0x7f, 0x38, 0x1e, 0x28, 0x78, 0x92,
+ 0x72, 0x21, 0x98, 0x9c, 0x6e, 0x57, 0xe5, 0x06, 0x0a, 0x53, 0x42, 0x92,
+ 0xb0, 0x25, 0x90, 0xcc, 0xbe, 0x47, 0xfd, 0x12, 0xd7, 0xa7, 0x3c, 0x39,
+ 0xe5, 0xdb, 0x4e, 0x54, 0xfa, 0x7b, 0xc4, 0x82, 0xd1, 0x21, 0xc8, 0x6d,
+ 0x72, 0x8f, 0x8f, 0xcd, 0x1a, 0xf4, 0xbb, 0xfb, 0x17, 0xbb, 0x05, 0xa6,
+ 0x3f, 0xc1, 0x2e, 0x08, 0x55, 0xc8, 0x5f, 0xfb, 0xd3, 0xbf, 0x2c, 0xdf,
+ 0xe0, 0x00, 0xdd, 0xca, 0x4f, 0x35, 0x40, 0xd1, 0xcc, 0x22, 0x55, 0x79,
+ 0x70, 0x69, 0x06, 0x75, 0x78, 0x3b, 0x1c, 0x35, 0x01, 0x96, 0xcd, 0xa0,
+ 0xd1, 0x1e, 0xca, 0x07, 0xe6, 0x2a, 0x1c, 0xa6, 0xc6, 0x55, 0x61, 0x30,
+ 0x06, 0xf8, 0x6a, 0xa3, 0x5f, 0xeb, 0x2d, 0x2a, 0xe2, 0x4f, 0xff, 0x8c,
+ 0x6c, 0x84, 0xd1, 0xad, 0x6a, 0x65, 0xf9, 0x3e, 0x1b, 0xa3, 0x0e, 0x3f,
+ 0x00, 0xd0, 0x82, 0xed, 0x39, 0x5c, 0x35, 0x41, 0x38, 0xc9, 0x05, 0xf9,
+ 0x21, 0x66, 0x29, 0x08, 0x06, 0x58, 0x60, 0x81, 0x49, 0xa1, 0xa2, 0xfb,
+ 0x78, 0x36, 0x0f, 0xb8, 0xc2, 0xf3, 0x3a, 0x25, 0xb5, 0x3b, 0xb3, 0x84,
+ 0xce, 0x9f, 0x95, 0x77, 0x03, 0x31, 0xd7, 0xfb, 0x06, 0xfb, 0x2d, 0xad,
+ 0xd4, 0x29, 0xa7, 0x7f, 0x12, 0xeb, 0x4e, 0x6a, 0x09, 0x71, 0x87, 0xf0,
+ 0x8a, 0x89, 0xb9, 0xc5, 0xae, 0xad, 0x54, 0xcc, 0x52, 0xe2, 0x45, 0x0f,
+ 0x32, 0xa9, 0x61, 0xf4, 0xd9, 0xac, 0x7a, 0x65, 0x81, 0xca, 0x8e, 0x07,
+ 0x3c, 0x7e, 0x17, 0x55, 0x55, 0x87, 0x11, 0xf2, 0x23, 0xc3, 0xbb, 0xe7,
+ 0x27, 0x51, 0xec, 0x1d, 0xc1, 0xfa, 0x8f, 0x05, 0x46, 0xe0, 0x05, 0x7b,
+ 0x81, 0x48, 0x43, 0x8a, 0x9f, 0x7e, 0xaa, 0x51, 0xfb, 0x39, 0x24, 0x64,
+ 0x04, 0x95, 0x12, 0xc0, 0xed, 0x7f, 0xc2, 0x9c, 0x90, 0x72, 0xaa, 0xa7,
+ 0xe0, 0x5e, 0x1e, 0xad, 0x7e, 0x01, 0x0b, 0x3a, 0x96, 0xb6, 0x5b, 0x2d,
+ 0x5e, 0xc8, 0xef, 0xe3, 0x9f, 0x08, 0xee, 0x81, 0x33, 0xc7, 0xe1, 0xa7,
+ 0x87, 0x14, 0x04, 0xe2, 0x3e, 0xd2, 0x95, 0x93, 0x37, 0x5a, 0xe0, 0x3f,
+ 0x13, 0x7c, 0x8e, 0x2c, 0xb7, 0x82, 0x59, 0x2a, 0x86, 0xd4, 0xf8, 0xd6,
+ 0x12, 0xbc, 0xa7, 0x61, 0x34, 0x4a, 0xf6, 0x50, 0xc6, 0xbe, 0x50, 0x3a,
+ 0x22, 0x4e, 0x53, 0x95, 0x18, 0xfc, 0x29, 0xab, 0x53, 0x22, 0xbe, 0x16,
+ 0x9a, 0xdd, 0xb5, 0xcd, 0x7a, 0xc1, 0xa9, 0x70, 0x5a, 0xa8, 0x1d, 0xf3,
+ 0x37, 0x54, 0x10, 0xd3, 0x6e, 0xa4, 0xe4, 0x69, 0x38, 0xaa, 0x3b, 0xde,
+ 0x62, 0x9e, 0xc8, 0x0a, 0x9a, 0x3b, 0x41, 0xc0, 0xfc, 0x2c, 0x1d, 0xd5,
+ 0x4d, 0xc2, 0x7f, 0x87, 0x9b, 0x4d, 0xa4, 0xf7, 0x06, 0x1d, 0xbd, 0xaf,
+ 0xa9, 0x77, 0x24, 0x79, 0x21, 0xb0, 0x1f, 0x3d, 0xce, 0x3e, 0x98, 0x32,
+ 0xbe, 0xae, 0xf4, 0xb8, 0x81, 0xbd, 0xbb, 0x54, 0x3c, 0x76, 0x93, 0x0f,
+ 0x7c, 0x8e, 0x9a, 0x07, 0x26, 0x6e, 0x96, 0xb7, 0xb8, 0xe8, 0xd8, 0x39,
+ 0x6d, 0xf0, 0x08, 0x42, 0x42, 0x29, 0x31, 0x4d, 0x0a, 0x5f, 0xdc, 0xf2,
+ 0x75, 0xb8, 0xbd, 0x13, 0xcd, 0xd1, 0xd6, 0xec, 0x83, 0xd9, 0x9b, 0x83,
+ 0xaf, 0x27, 0x91, 0xc5, 0x4d, 0x7e, 0x68, 0xd2, 0x22, 0x29, 0x08, 0x72,
+ 0xe6, 0xa0, 0xe7, 0x40, 0xac, 0x04, 0x8f, 0x19, 0x72, 0x24, 0xb8, 0xd4,
+ 0x90, 0x90, 0x99, 0x04, 0x96, 0x99, 0xd0, 0x87, 0xce, 0x98, 0x7d, 0x1c,
+ 0x40, 0x2f, 0x7f, 0xf1, 0x16, 0x5e, 0x57, 0xb4, 0xb6, 0x11, 0xf3, 0x6d,
+ 0xf6, 0x5e, 0x6f, 0x1d, 0xfb, 0x5d, 0xcf, 0x4f, 0x23, 0x6e, 0xd2, 0x91,
+ 0xae, 0x3d, 0x58, 0x0d, 0xed, 0xd7, 0x80, 0x4b, 0xea, 0xdd, 0x3e, 0x95,
+ 0xa5, 0x52, 0xd2, 0xd9, 0xc8, 0x39, 0xdb, 0x85, 0x98, 0xde, 0x01, 0x33,
+ 0x7b, 0xb7, 0xb3, 0x7d, 0x29, 0xe9, 0x47, 0xbe, 0x4d, 0x2d, 0xf8, 0x4c,
+ 0xae, 0x07, 0xf1, 0xc7, 0x75, 0x27, 0x76, 0xed, 0x3a, 0x40, 0xa6, 0xeb,
+ 0x2d, 0x37, 0xc4, 0xf7, 0xd1, 0xab, 0x0e, 0x50, 0x1b, 0xba, 0x88, 0x48,
+ 0x1c, 0x4f, 0xb6, 0xfe, 0x59, 0xe7, 0x5c, 0x44, 0xf1, 0xaf, 0x92, 0x3e,
+ 0x81, 0xa5, 0x9a, 0xbd, 0x3d, 0xdb, 0x57, 0xe2, 0xbf, 0x0c, 0x17, 0x61,
+ 0x2b, 0x58, 0xec, 0xda, 0xf1, 0xb9, 0xb5, 0x7f, 0x0f, 0x08, 0x25, 0xb2,
+ 0x60, 0xdb, 0x79, 0xd3, 0xee, 0x61, 0xd3, 0x9d, 0x1c, 0xd8, 0xb6, 0x78,
+ 0x2f, 0x08, 0x53, 0x91, 0xa2, 0xf1, 0x80, 0xd4, 0x62, 0xda, 0x60, 0xec,
+ 0x9a, 0xc2, 0xf0, 0xb4, 0x3a, 0xb5, 0x6e, 0x37, 0x1e, 0xae, 0x38, 0xae,
+ 0x0c, 0x1e, 0x08, 0x1b, 0xc3, 0x3b, 0x11, 0xd2, 0xce, 0x1d, 0x08, 0x11,
+ 0x48, 0x1c, 0x9c, 0x88, 0x19, 0x12, 0x0f, 0xf6, 0xb9, 0x7e, 0x81, 0x69,
+ 0x1c, 0x95, 0x76, 0xc7, 0x50, 0x30, 0xbd, 0x43, 0x13, 0xcd, 0x32, 0x7c,
+ 0xa9, 0xe9, 0xa3, 0xf9, 0x7b, 0x0a, 0x00, 0x39, 0x45, 0xbf, 0xfe, 0xb3,
+ 0x46, 0x17, 0x5b, 0x7a, 0x21, 0x1b, 0x40, 0x08, 0x06, 0xf0, 0x31, 0x91,
+ 0x87, 0xe4, 0x5c, 0x49, 0xd0, 0xbe, 0x65, 0x5e, 0x9d, 0xbf, 0xee, 0x92,
+ 0x11, 0x47, 0xd1, 0x21, 0x3d, 0x51, 0xe2, 0xe2, 0x29, 0x5e, 0x0e, 0x19,
+ 0xef, 0xad, 0x80, 0x73, 0x10, 0x5b, 0xa2, 0x21, 0xb9, 0x5a, 0xea, 0x99,
+ 0x81, 0x4e, 0x89, 0x31, 0x7b, 0xa8, 0x68, 0xa0, 0xa3, 0xd9, 0xba, 0xc6,
+ 0xf6, 0xf3, 0xcc, 0x2b, 0xab, 0x3f, 0x8e, 0x52, 0x03, 0x95, 0x9d, 0x4c,
+ 0x75, 0x36, 0xfe, 0x93, 0x18, 0xd1, 0x26, 0x12, 0x30, 0xc1, 0x80, 0x37,
+ 0x6f, 0x4a, 0xea, 0x96, 0xa7, 0x2e, 0xce, 0xbd, 0x4e, 0x2c, 0x92, 0x19,
+ 0x0d, 0x10, 0x06, 0xa0, 0xc9, 0x80, 0xcf, 0x50, 0xa0, 0xab, 0x5a, 0x23,
+ 0x42, 0xac, 0xc1, 0xb2, 0x49, 0x3f, 0x45, 0x46, 0x47, 0x01, 0x24, 0xf9,
+ 0xa8, 0xb0, 0xc7, 0x72, 0x4a, 0x7c, 0xfe, 0xd6, 0xd7, 0x42, 0xbd, 0x0f,
+ 0xcb, 0x8b, 0xd0, 0x3e, 0x1e, 0x2c, 0x72, 0x2a, 0x04, 0x26, 0xca, 0x69,
+ 0x13, 0x84, 0xaf, 0xd7, 0xbd, 0x24, 0xa8, 0xd9, 0x15, 0x16, 0x31, 0xd3,
+ 0x84, 0xd4, 0xc1, 0x55, 0xf2, 0xd0, 0x7e, 0x50, 0xaf, 0x26, 0x10, 0x47,
+ 0xb2, 0xf6, 0x94, 0xa4, 0x04, 0x27, 0x6e, 0xdf, 0xd0, 0x54, 0x9c, 0x31,
+ 0x88, 0xaa, 0xff, 0x52, 0x8a, 0x51, 0x99, 0x51, 0x5f, 0xe7, 0xa7, 0xf2,
+ 0xdc, 0x47, 0x68, 0x28, 0x7f, 0xb1, 0x95, 0xf1, 0xbb, 0x11, 0x50, 0x3a,
+ 0x74, 0x8a, 0xa6, 0x9a, 0xb8, 0x27, 0xc0, 0x78, 0x53, 0x3e, 0xbc, 0x83,
+ 0x6c, 0xf3, 0xb3, 0xab, 0xa3, 0x84, 0x44, 0xdb, 0x02, 0xb9, 0xe2, 0xb1,
+ 0xaa, 0x17, 0x3d, 0x0e, 0xfa, 0x72, 0x2f, 0x0b, 0x4f, 0x0f, 0x92, 0x13,
+ 0x4d, 0x6e, 0xad, 0x75, 0x21, 0xf6, 0xb7, 0x55, 0xac, 0x16, 0x3e, 0x4d,
+ 0xa8, 0x21, 0x1e, 0x96, 0xcf, 0xb7, 0x67, 0xcd, 0xa6, 0x89, 0xa4, 0x73,
+ 0xa8, 0xf8, 0xe4, 0x9d, 0xd3, 0xa4, 0xae, 0x2e, 0x6a, 0x34, 0x56, 0xdd,
+ 0xd2, 0x58, 0x52, 0x81, 0x71, 0x4d, 0x4a, 0xec, 0x3e, 0x2d, 0x70, 0xc8,
+ 0xd4, 0xd0, 0xdb, 0x7e, 0x58, 0xcb, 0xce, 0x9d, 0x2d, 0xa5, 0xeb, 0xba,
+ 0x46, 0x58, 0x7e, 0x02, 0x42, 0x8f, 0x56, 0xe9, 0xa2, 0x7e, 0x5c, 0x79,
+ 0xd1, 0xd2, 0x2e, 0x1e, 0x29, 0x60, 0xc8, 0x80, 0xcf, 0x0d, 0x8d, 0x8f,
+ 0xd0, 0xf6, 0x0b, 0x2a, 0xd0, 0x9d, 0x1a, 0xbe, 0xac, 0x55, 0x09, 0x77,
+ 0x92, 0x6a, 0x60, 0xa0, 0xb7, 0xef, 0x3a, 0xbd, 0x6c, 0x5f, 0x51, 0x82,
+ 0x06, 0x7c, 0x7a, 0x1f, 0x7b, 0xdf, 0x6d, 0xc3, 0xdf, 0x87, 0x2f, 0x32,
+ 0x9c, 0x9b, 0xaf, 0x46, 0x96, 0x9e, 0x3a, 0xda, 0x3b, 0x1e, 0x25, 0x8c,
+ 0x4f, 0x1d, 0x9f, 0x1f, 0x07, 0x5b, 0x93, 0x25, 0xa2, 0x50, 0x3f, 0xab,
+ 0xcf, 0x07, 0xd8, 0xc9, 0x3e, 0xc8, 0x48, 0x75, 0xb7, 0xa1, 0x03, 0x7a,
+ 0x7c, 0xfa, 0xf1, 0xde, 0x7e, 0xf3, 0x3c, 0xbb, 0x91, 0xb7, 0x9e, 0x51,
+ 0x4f, 0x17, 0x66, 0xb5, 0xf5, 0x3b, 0xe5, 0x88, 0x6d, 0x72, 0xb2, 0x0a,
+ 0xab, 0x38, 0xef, 0x21, 0x61, 0x26, 0x80, 0x8a, 0x4f, 0x1c, 0xc5, 0x70,
+ 0x17, 0xc0, 0x1d, 0x28, 0x88, 0x6b, 0x28, 0x54, 0x84, 0x49, 0x3e, 0x47,
+ 0xe8, 0xf4, 0x88, 0x24, 0xb5, 0x54, 0xfc, 0xe0, 0x88, 0x58, 0x20, 0xc9,
+ 0x9e, 0x57, 0x42, 0x95, 0x5b, 0x38, 0x8a, 0x0a, 0x77, 0xa5, 0x16, 0x1d,
+ 0x3a, 0x3f, 0xab, 0xbc, 0xf7, 0x66, 0x2a, 0xfa, 0x67, 0xd6, 0x2f, 0xae,
+ 0x84, 0x15, 0x8c, 0x75, 0x4f, 0xd5, 0x0c, 0x2b, 0x97, 0x30, 0xe5, 0xa7,
+ 0x66, 0xcf, 0xb2, 0x7d, 0xe9, 0xd8, 0x52, 0x64, 0x62, 0xbc, 0xde, 0x82,
+ 0x38, 0xbb, 0x9b, 0x41, 0x43, 0xa0, 0xd3, 0xad, 0x4b, 0xb8, 0x97, 0xd7,
+ 0x1b, 0x28, 0x27, 0xf4, 0x17, 0x4a, 0x55, 0xab, 0xb5, 0xe7, 0x4e, 0x8b,
+ 0xbc, 0x07, 0x25, 0x1d, 0xb1, 0xb2, 0xab, 0xa1, 0x50, 0xb4, 0x88, 0xd7,
+ 0x78, 0x41, 0xd7, 0x99, 0x9d, 0x81, 0xf6, 0x68, 0xc7, 0xd6, 0x9d, 0x8f,
+ 0xd7, 0x94, 0xac, 0x6a, 0x16, 0xe2, 0xba, 0xf9, 0xcd, 0x2b, 0x4a, 0xfd,
+ 0x6d, 0x7c, 0xcb, 0x2d, 0x67, 0xcb, 0xe0, 0xfe, 0xb6, 0x29, 0x84, 0xd2,
+ 0x2e, 0x17, 0x6a, 0xed, 0x25, 0xaa, 0xaf, 0xfd, 0xf2, 0x58, 0x3b, 0xe0,
+ 0x01, 0x77, 0x1d, 0x94, 0x8c, 0xf6, 0x2a, 0x69, 0x4e, 0xff, 0x9c, 0x2c,
+ 0x74, 0x2f, 0xed, 0x57, 0x5e, 0xfc, 0x47, 0xad, 0x64, 0x1f, 0x84, 0x96,
+ 0xf1, 0x7a, 0xdd, 0x4e, 0x1d, 0x02, 0x4f, 0x12, 0xf0, 0x12, 0x6a, 0x10,
+ 0x0a, 0x3b, 0x5b, 0xfb, 0x50, 0x6b, 0xa1, 0xfd, 0xe8, 0x08, 0xb9, 0x01,
+ 0x42, 0x79, 0x99, 0xc8, 0x8a, 0x3b, 0xb1, 0xd5, 0xc1, 0xd1, 0x03, 0x1e,
+ 0x9b, 0xce, 0x30, 0xd3, 0xb0, 0x1f, 0x3e, 0xec, 0x44, 0x72, 0x0b, 0xa8,
+ 0xb6, 0xd1, 0xce, 0x6f, 0x19, 0x38, 0xe7, 0xb4, 0x57, 0xec, 0xd0, 0x3c,
+ 0x4c, 0xaf, 0xf3, 0xb3, 0x4a, 0xd8, 0x13, 0x4d, 0x42, 0x5b, 0x64, 0xb6,
+ 0xd4, 0x00, 0x9c, 0x2b, 0x13, 0x63, 0x37, 0x1a, 0x81, 0xb6, 0x1f, 0x59,
+ 0x81, 0xe5, 0xf7, 0xa6, 0xa5, 0x00, 0xc7, 0xb0, 0xa9, 0xca, 0xbc, 0x6d,
+ 0xa5, 0xa1, 0x43, 0x60, 0x18, 0xde, 0x01, 0xf1, 0x7d, 0x32, 0xdd, 0xf4,
+ 0xac, 0xb0, 0xaf, 0xa2, 0x13, 0x9d, 0x08, 0xd1, 0x7c, 0x30, 0x7a, 0x3c,
+ 0x95, 0x8c, 0x64, 0x93, 0xec, 0x90, 0xb9, 0x22, 0xad, 0x36, 0xbd, 0x2c,
+ 0x3d, 0x0a, 0x1c, 0x07, 0x1a, 0x05, 0x61, 0x34, 0xea, 0x05, 0xf5, 0x8f,
+ 0xb1, 0x7d, 0x59, 0x21, 0xcf, 0x57, 0x8b, 0x68, 0xc4, 0x9c, 0x2f, 0x37,
+ 0xf5, 0x76, 0x62, 0xf6, 0xd1, 0x4e, 0xfd, 0xf1, 0xa0, 0x15, 0x15, 0x7c,
+ 0x4d, 0xb9, 0x8f, 0x23, 0x20, 0x6c, 0xc0, 0x6f, 0x2c, 0x71, 0x8e, 0x12,
+ 0x55, 0x06, 0xe3, 0x1a, 0xac, 0x43, 0x7f, 0x92, 0x0d, 0xbb, 0xac, 0x76,
+ 0xb6, 0x4b, 0x75, 0x47, 0x8c, 0x2b, 0x0e, 0x43, 0x73, 0x4f, 0x2d, 0x3b,
+ 0xfa, 0x5d, 0x60, 0x97, 0x48, 0x6d, 0xd4, 0x8b, 0xce, 0x2a, 0x4a, 0xf5,
+ 0x8a, 0x58, 0xfb, 0xb4, 0x8c, 0xae, 0xbc, 0xba, 0xa5, 0x12, 0x74, 0xc0,
+ 0x1d, 0x49, 0xed, 0x3a, 0x6e, 0x24, 0x0f, 0xdb, 0x94, 0x84, 0x3e, 0x73,
+ 0x96, 0xd3, 0x50, 0x3a, 0x21, 0x55, 0x49, 0x27, 0x32, 0x2b, 0x43, 0x22,
+ 0x0a, 0x32, 0x85, 0xbc, 0x7d, 0x15, 0x18, 0x95, 0xf6, 0xa2, 0x78, 0x9a,
+ 0x1f, 0xdf, 0x81, 0x03, 0xb6, 0x40, 0xb7, 0x18, 0xb0, 0x18, 0x2a, 0x59,
+ 0xdf, 0x8f, 0x15, 0x04, 0x65, 0xf7, 0x61, 0x86, 0x6e, 0x36, 0x5d, 0x02,
+ 0xf4, 0xe2, 0x41, 0x1a, 0x0d, 0xcc, 0xaf, 0x54, 0x76, 0x35, 0xb5, 0xb0,
+ 0xb3, 0xc2, 0x88, 0x97, 0x66, 0x17, 0xc3, 0xb2, 0xbf, 0x20, 0xb3, 0x54,
+ 0x27, 0x7b, 0xc3, 0x96, 0x5b, 0xd2, 0x06, 0x55, 0xbb, 0xbd, 0x5b, 0x74,
+ 0x5f, 0x88, 0x7f, 0xb6, 0xa8, 0x4d, 0x1b, 0x8e, 0xd1, 0x02, 0x92, 0x53,
+ 0xbb, 0x32, 0xab, 0xf0, 0xde, 0xc3, 0x21, 0xa4, 0x03, 0x7a, 0xc9, 0x20,
+ 0xbd, 0x84, 0xdd, 0xff, 0x1b, 0xc6, 0xfe, 0x1c, 0xbf, 0x96, 0x09, 0xa2,
+ 0x75, 0x43, 0x6c, 0x8f, 0xf8, 0x60, 0x14, 0x48, 0x2f, 0x98, 0x15, 0x1e,
+ 0x47, 0x11, 0x8c, 0x71, 0x57, 0xfe, 0x8e, 0x72, 0xe8, 0x37, 0x92, 0x6f,
+ 0xfd, 0x4d, 0x17, 0x7e, 0x13, 0xfe, 0xad, 0x9b, 0xac, 0xd0, 0xc0, 0xe2,
+ 0x30, 0xb9, 0xd7, 0xbb, 0x8a, 0xea, 0x3d, 0xee, 0x0e, 0xdc, 0xad, 0xc4,
+ 0xe1, 0xd7, 0x70, 0x67, 0x86, 0x44, 0x49, 0x4a, 0x91, 0x5d, 0xd3, 0x85,
+ 0x02, 0xdf, 0xbd, 0x4e, 0xd5, 0x00, 0x90, 0x67, 0x03, 0x8b, 0x52, 0xcd,
+ 0x06, 0xb1, 0x4e, 0xbe, 0xa9, 0xb0, 0x42, 0x55, 0xa3, 0xcb, 0xf3, 0xeb,
+ 0x8d, 0x4d, 0xe1, 0x68, 0x09, 0x5f, 0xd1, 0xd2, 0x99, 0xa7, 0xe9, 0xcc,
+ 0x22, 0xb7, 0x89, 0xef, 0xd9, 0x47, 0x2b, 0x79, 0x50, 0xbd, 0x95, 0xf5,
+ 0x37, 0x07, 0xce, 0x73, 0x81, 0x27, 0x42, 0x4d, 0xc4, 0x68, 0x33, 0xf3,
+ 0xd3, 0x99, 0xdd, 0x88, 0xff, 0xab, 0xee, 0x6f, 0x5f, 0x51, 0xe5, 0xf8,
+ 0xf6, 0xbc, 0x6a, 0xa6, 0x02, 0xff, 0x8b, 0xe3, 0xd2, 0xa0, 0xc6, 0x44,
+ 0xbc, 0x48, 0x0c, 0x6c, 0xc4, 0x0f, 0x43, 0xe0, 0xfc, 0x7f, 0xda, 0x9a,
+ 0x37, 0x59, 0x4d, 0x20, 0xf7, 0x96, 0x9c, 0x67, 0x8b, 0x94, 0x7a, 0xf2,
+ 0xd5, 0x77, 0x3e, 0xad, 0x49, 0x57, 0x58, 0x02, 0x66, 0x93, 0x51, 0x03,
+ 0x48, 0xf0, 0xb3, 0x69, 0xa5, 0x7d, 0x50, 0x1e, 0x46, 0x6e, 0x6f, 0xc3,
+ 0x36, 0xc0, 0xc6, 0x52, 0xc2, 0x47, 0xce, 0x85, 0xe9, 0xca, 0x68, 0x29,
+ 0xaf, 0xff, 0xa8, 0x24, 0x69, 0x98, 0x6a, 0xdb, 0x70, 0xc8, 0x1d, 0x35,
+ 0x62, 0xcf, 0x21, 0xfa, 0x72, 0xbf, 0x54, 0x05, 0x15, 0x48, 0x54, 0xee,
+ 0xe6, 0x78, 0xc1, 0x53, 0x13, 0x1b, 0xee, 0xd0, 0xd5, 0x44, 0xd1, 0xd1,
+ 0x5c, 0xe1, 0x41, 0xce, 0x3e, 0x1b, 0xd0, 0xe5, 0xc0, 0x3c, 0xd7, 0xa7,
+ 0x49, 0xdf, 0xb4, 0x1a, 0x77, 0xae, 0xa3, 0x18, 0xa8, 0x26, 0xc2, 0x11,
+ 0x11, 0x2c, 0x0e, 0x69, 0xa5, 0x07, 0x17, 0x62, 0x67, 0xb7, 0xbb, 0x58,
+ 0x8e, 0x40, 0x43, 0xab, 0x31, 0x36, 0x76, 0x17, 0x3c, 0x75, 0x1c, 0x8c,
+ 0x8c, 0x7b, 0x2d, 0x5c, 0xd1, 0x82, 0x0f, 0x37, 0x10, 0x4d, 0xa1, 0xe0,
+ 0x12, 0x5d, 0x9d, 0xb6, 0xb5, 0xa4, 0x15, 0xee, 0x18, 0xff, 0x85, 0x65,
+ 0x1a, 0xf0, 0xa3, 0x1d, 0xc2, 0xdf, 0x1a, 0x1f, 0x1f, 0xfc, 0xc5, 0x32,
+ 0x4f, 0xf2, 0xb0, 0xaf, 0x80, 0x28, 0xff, 0x28, 0xd2, 0x59, 0xee, 0x33,
+ 0x6e, 0x3d, 0x3c, 0x7c, 0x78, 0xe5, 0x8e, 0xf3, 0x0b, 0xcf, 0xec, 0x07,
+ 0xd0, 0xbb, 0xcd, 0xce, 0xa8, 0xea, 0xab, 0x6a, 0xd2, 0xea, 0x05, 0xda,
+ 0x08, 0xac, 0x96, 0xd7, 0xf9, 0xe3, 0xc7, 0x06, 0x09, 0xee, 0xc1, 0x65,
+ 0xd3, 0x3d, 0x97, 0x1d, 0x37, 0x24, 0x7b, 0x4e, 0xa4, 0x5b, 0x80, 0x49,
+ 0x5a, 0x31, 0x09, 0xe5, 0x60, 0xab, 0x8b, 0x5f, 0x76, 0x6b, 0x27, 0x4c,
+ 0x59, 0x03, 0x37, 0x1a, 0x7a, 0xb6, 0x35, 0x8d, 0xad, 0xf4, 0xcf, 0x70,
+ 0xc9, 0x6d, 0x87, 0x9e, 0xde, 0xe0, 0xba, 0x59, 0x65, 0xc1, 0x96, 0x7e,
+ 0x5d, 0x81, 0x04, 0x76, 0xa3, 0x49, 0x09, 0x0e, 0xdc, 0x8f, 0xfe, 0xe2,
+ 0x37, 0xbf, 0xd0, 0x7c, 0xa8, 0x31, 0x25, 0x3c, 0x79, 0xfb, 0x5d, 0x6a,
+ 0x80, 0x88, 0x92, 0xcb, 0xa2, 0xff, 0x26, 0x91, 0x6d, 0x90, 0xb4, 0x54,
+ 0xbf, 0xfb, 0x42, 0x0a, 0x20, 0xed, 0x75, 0xd8, 0x49, 0xed, 0xf1, 0xc1,
+ 0xe2, 0xc7, 0x5f, 0x9a, 0xf0, 0xe5, 0x5a, 0xbd, 0xde, 0x13, 0xd2, 0xaf,
+ 0x2a, 0x9b, 0x6b, 0x08, 0xda, 0x6a, 0xe4, 0x32, 0x50, 0xad, 0xbc, 0x88,
+ 0xdb, 0x2c, 0xb1, 0x3f, 0x1f, 0x23, 0x85, 0x41, 0xd0, 0xed, 0x19, 0x79,
+ 0x44, 0x79, 0x9d, 0xfb, 0xde, 0x58, 0x06, 0xff, 0x14, 0xc1, 0xbd, 0xe3,
+ 0x8d, 0x9b, 0x84, 0x26, 0xaa, 0x6f, 0xbe, 0x96, 0xb9, 0x1a, 0x52, 0xc9,
+ 0x4e, 0xfe, 0x64, 0xdf, 0xc2, 0x04, 0xc9, 0x6f, 0x25, 0x64, 0x66, 0xaf,
+ 0x49, 0x13, 0x68, 0x02, 0x8d, 0x62, 0x74, 0x17, 0x54, 0x15, 0x68, 0x55,
+ 0x15, 0x6f, 0x84, 0xe5, 0x79, 0x97, 0x0c, 0x1b, 0x54, 0x9a, 0x1e, 0xea,
+ 0x83, 0x57, 0xa9, 0x1f, 0xe7, 0xbe, 0x66, 0xe1, 0xd8, 0x2e, 0x69, 0x01,
+ 0xce, 0xbe, 0xdd, 0x63, 0x39, 0x0c, 0x9d, 0x3f, 0xa8, 0xa0, 0xd5, 0xb3,
+ 0x0f, 0xc6, 0x31, 0x62, 0xa9, 0xf3, 0x3e, 0x75, 0x90, 0x8f, 0x50, 0x73,
+ 0x68, 0x1c, 0xcc, 0x5f, 0xfb, 0x1f, 0x88, 0x69, 0x0e, 0x2d, 0xeb, 0x93,
+ 0x5e, 0xf6, 0xbb, 0x15, 0x03, 0xa7, 0xbf, 0x1e, 0x87, 0x08, 0x0b, 0x8b,
+ 0x26, 0x1a, 0xc7, 0xc3, 0xd3, 0x31, 0x49, 0x23, 0x46, 0xd8, 0x5d, 0x7b,
+ 0xb6, 0x56, 0x82, 0x4f, 0x26, 0x9a, 0x0b, 0xfb, 0x30, 0x44, 0x2c, 0xf2,
+ 0x93, 0xad, 0x74, 0xfc, 0x43, 0x03, 0xd0, 0x6d, 0xba, 0x28, 0x7a, 0x60,
+ 0x69, 0x9e, 0x95, 0xe5, 0x03, 0xbd, 0x37, 0x6a, 0x3e, 0xfd, 0x68, 0x2e,
+ 0xb4, 0x1e, 0xb6, 0x3c, 0x49, 0xae, 0x66, 0x7f, 0x0a, 0x4f, 0x34, 0xf2,
+ 0x6b, 0xe7, 0x00, 0xa3, 0xfd, 0x94, 0xd5, 0x18, 0xba, 0xa0, 0xeb, 0x81,
+ 0x47, 0x1b, 0x9f, 0x44, 0xaa, 0x82, 0x55, 0x79, 0xd6, 0x74, 0x7c, 0xc5,
+ 0xc4, 0x7a, 0x19, 0x90, 0x86, 0xf3, 0xab, 0x22, 0x02, 0xd6, 0x1f, 0x38,
+ 0x68, 0xfc, 0xaa, 0x42, 0x39, 0x9c, 0x72, 0x98, 0x0b, 0xef, 0x57, 0x12,
+ 0x17, 0xb2, 0x93, 0x29, 0xb9, 0x00, 0xb2, 0x5b, 0xd2, 0x12, 0x4f, 0xf6,
+ 0x0d, 0x2f, 0x44, 0x89, 0x00, 0x19, 0x9c, 0xa6, 0xea, 0xfe, 0xa0, 0xcb,
+ 0x57, 0x83, 0xf1, 0xba, 0x44, 0x7b, 0x68, 0x57, 0x5e, 0x71, 0x7c, 0x36,
+ 0x9b, 0x4c, 0x63, 0x12, 0x5f, 0xff, 0x6a, 0x39, 0xfe, 0x6e, 0x22, 0x4c,
+ 0xf4, 0x53, 0x50, 0x69, 0xe1, 0xc2, 0x91, 0xe3, 0xa3, 0x4b, 0x88, 0x00,
+ 0xbf, 0xc5, 0x3c, 0xe1, 0x22, 0x80, 0x0e, 0x53, 0x2b, 0x8e, 0xfc, 0xb1,
+ 0x42, 0x85, 0x21, 0xb5, 0xa3, 0xd8, 0xf8, 0x23, 0x70, 0xe4, 0x46, 0xe7,
+ 0xf2, 0x4f, 0xb0, 0x50, 0x35, 0xee, 0x30, 0x33, 0x0b, 0x05, 0x30, 0xac,
+ 0xe1, 0xf6, 0x07, 0x00, 0x51, 0xf3, 0x10, 0x24, 0x98, 0xa8, 0x5b, 0x66,
+ 0x61, 0xff, 0x4b, 0xa6, 0xee, 0xfe, 0x8c, 0x42, 0xae, 0xc1, 0xab, 0x9c,
+ 0x4e, 0x94, 0xe9, 0xc9, 0xc0, 0x18, 0xcb, 0x34, 0x27, 0xc0, 0x29, 0xb1,
+ 0x4b, 0x5f, 0x9a, 0x26, 0xc2, 0x01, 0xc1, 0x95, 0xea, 0x9a, 0xd7, 0x61,
+ 0xa6, 0xfc, 0x0f, 0x94, 0x8c, 0xe8, 0xb5, 0xdd, 0xe2, 0xb6, 0x2b, 0x98,
+ 0xb7, 0xf9, 0xd9, 0x7e, 0xd1, 0xe9, 0x67, 0xbb, 0xda, 0xe8, 0x9d, 0x7c,
+ 0xc9, 0x7f, 0xfe, 0xd0, 0x2a, 0x53, 0xc2, 0x1c, 0xbe, 0xfc, 0x14, 0x97,
+ 0x3f, 0x04, 0xd9, 0x59, 0x39, 0xeb, 0xb7, 0x8a, 0xed, 0x69, 0x8c, 0x87,
+ 0x30, 0x1b, 0xf9, 0xcb, 0x1f, 0xb4, 0xef, 0xac, 0x66, 0x4b, 0x9f, 0xa8,
+ 0x03, 0xdc, 0x01, 0x2f, 0xb2, 0xfa, 0x68, 0xfd, 0x2c, 0x3c, 0xa0, 0xba,
+ 0x63, 0xcb, 0x52, 0x44, 0xbf, 0x88, 0xab, 0xdd, 0xb4, 0x14, 0xa2, 0x25,
+ 0xe3, 0x09, 0xa8, 0xa4, 0xe8, 0x6a, 0x61, 0x0e, 0xcc, 0x05, 0x90, 0x3a,
+ 0xa5, 0x0a, 0x7a, 0x58, 0x3f, 0x45, 0x73, 0x84, 0x98, 0x72, 0xc6, 0xd3,
+ 0xdb, 0x57, 0xab, 0x65, 0x44, 0xcd, 0x0d, 0x0d, 0x45, 0xfd, 0xe8, 0x2e,
+ 0xa8, 0xa6, 0x52, 0x0c, 0x61, 0x73, 0xb2, 0x18, 0x65, 0x61, 0xa8, 0x68,
+ 0xc8, 0x78, 0xf3, 0xf4, 0x76, 0x5b, 0x48, 0x11, 0xaa, 0xa9, 0xa0, 0xc1,
+ 0xa9, 0x4a, 0xf5, 0x1f, 0xef, 0xcb, 0x64, 0x9e, 0x2e, 0x8e, 0xbd, 0x28,
+ 0x8c, 0x8e, 0x54, 0x63, 0x85, 0xda, 0xe7, 0x73, 0xae, 0xb5, 0x2b, 0x71,
+ 0x6f, 0x2b, 0x8d, 0x44, 0xb4, 0x16, 0x2d, 0xb0, 0xe7, 0x05, 0x30, 0x9c,
+ 0x00, 0x85, 0xb2, 0x1d, 0x2e, 0x04, 0xf6, 0xcf, 0x99, 0xd6, 0x0f, 0x70,
+ 0x08, 0xb6, 0x71, 0xba, 0x65, 0x59, 0xb0, 0x3e, 0xe8, 0x69, 0x27, 0xf3,
+ 0x13, 0x59, 0xf4, 0xef, 0xbd, 0xdf, 0x6a, 0xd9, 0x3e, 0xe8, 0x1f, 0x11,
+ 0xd2, 0x53, 0x75, 0x56, 0x52, 0x4b, 0x6c, 0x98, 0xef, 0x00, 0xdf, 0x21,
+ 0xeb, 0x3f, 0x54, 0x4b, 0x6a, 0xa8, 0x98, 0x5d, 0x26, 0x06, 0x0d, 0x2e,
+ 0xd0, 0x6e, 0xf6, 0x00, 0x22, 0xfc, 0xc2, 0x8d, 0x5c, 0x4b, 0xf6, 0x83,
+ 0x55, 0x02, 0x90, 0x04, 0xef, 0x37, 0x04, 0xc6, 0xb4, 0xd3, 0x6c, 0x48,
+ 0x0d, 0xeb, 0x39, 0x13, 0xf0, 0xda, 0x28, 0xe8, 0xf0, 0x96, 0x73, 0x0d,
+ 0x9b, 0xb9, 0x22, 0x55, 0x96, 0x9d, 0x38, 0x1b, 0xac, 0x18, 0x13, 0x04,
+ 0xf1, 0xe4, 0x53, 0x19, 0x76, 0x33, 0x40, 0x6d, 0xe4, 0xb2, 0x5e, 0x73,
+ 0x84, 0x83, 0xa1, 0xf0, 0xb3, 0x75, 0xeb, 0x70, 0x53, 0x19, 0xd1, 0x5c,
+ 0x5b, 0x57, 0x57, 0xfb, 0xce, 0x5d, 0x9b, 0xc8, 0xd9, 0xc6, 0x55, 0xca,
+ 0x20, 0x84, 0x87, 0xd7, 0x84, 0xa4, 0xb0, 0x03, 0x9d, 0x61, 0xcc, 0xc2,
+ 0x84, 0xe1, 0x91, 0x90, 0x44, 0x72, 0xbf, 0xb8, 0xed, 0xcb, 0x29, 0xd3,
+ 0xbe, 0x0d, 0x37, 0x2f, 0xf6, 0xe1, 0x5a, 0xd5, 0xaa, 0xf4, 0xc2, 0xc6,
+ 0x71, 0x7c, 0x7e, 0xcd, 0x4a, 0x64, 0x8c, 0xda, 0x12, 0x71, 0xa3, 0x59,
+ 0x04, 0x7d, 0xe2, 0xe3, 0x68, 0x42, 0x10, 0x1b, 0x3a, 0x5f, 0x92, 0x1f,
+ 0x06, 0x20, 0x9b, 0xb7, 0x73, 0x8b, 0x42, 0xbc, 0x76, 0xba, 0x24, 0x88,
+ 0xe8, 0xad, 0xfe, 0x3b, 0xef, 0x7d, 0x2c, 0x49, 0x8d, 0xa0, 0xc6, 0x2a,
+ 0x01, 0x41, 0xa6, 0x1c, 0x32, 0xae, 0xf7, 0xf5, 0x1f, 0xe1, 0x58, 0xcd,
+ 0xb4, 0x17, 0x63, 0x0b, 0x8a, 0x4b, 0x1e, 0x62, 0xae, 0x32, 0x33, 0xf8,
+ 0x1f, 0x14, 0x2b, 0x85, 0x53, 0xab, 0x09, 0x0b, 0x16, 0x81, 0x23, 0x5e,
+ 0x8b, 0x4c, 0x96, 0x0d, 0x7d, 0xa2, 0xd1, 0x22, 0x9d, 0x3e, 0x95, 0x5c,
+ 0xe4, 0x8b, 0xa4, 0x16, 0x6d, 0x2d, 0xec, 0xf1, 0x16, 0x4e, 0x7e, 0x36,
+ 0x18, 0x22, 0x8a, 0xfe, 0x9c, 0x3c, 0x6e, 0xf3, 0xe0, 0x3e, 0x96, 0xa5,
+ 0xb5, 0xc1, 0x2f, 0xe5, 0xeb, 0xf3, 0x44, 0xcb, 0xb1, 0x5e, 0xd6, 0xbe,
+ 0x79, 0x86, 0x44, 0x9a, 0x84, 0x02, 0x78, 0x1e, 0x58, 0xe9, 0x80, 0x6a,
+ 0xd6, 0x4f, 0xaa, 0x51, 0xec, 0xb4, 0xb6, 0xd0, 0x4f, 0xb2, 0x8c, 0x6e,
+ 0x2c, 0xa9, 0x16, 0xc0, 0x1b, 0x69, 0x6c, 0x28, 0x97, 0xcc, 0xb8, 0x3f,
+ 0x2f, 0x72, 0xdd, 0x1b, 0x29, 0x06, 0xff, 0xca, 0x2c, 0xe7, 0xd7, 0x20,
+ 0x61, 0x87, 0x39, 0x71, 0x8e, 0xe1, 0x6e, 0x2c, 0xbc, 0x3a, 0x3c, 0x43,
+ 0x6f, 0x6d, 0x18, 0x53, 0x4d, 0x41, 0xb3, 0xe8, 0x0c, 0xb3, 0x83, 0xcb,
+ 0xb4, 0x9a, 0xa8, 0x27, 0x1f, 0x0c, 0x84, 0xdb, 0x22, 0x61, 0x3f, 0xc3,
+ 0x2e, 0x42, 0x78, 0xd6, 0x5e, 0x3e, 0xd8, 0x5a, 0x67, 0x88, 0x5e, 0x16,
+ 0x23, 0xb4, 0x3a, 0x8d, 0x6b, 0x00, 0x2e, 0xf1, 0x01, 0x26, 0x56, 0x77,
+ 0x4f, 0xf5, 0xed, 0xbd, 0x2d, 0x05, 0x14, 0xd1, 0xa8, 0x11, 0x2b, 0x5b,
+ 0x64, 0x5e, 0xe7, 0xa3, 0x78, 0x15, 0x73, 0xce, 0x61, 0xaa, 0x03, 0x4f,
+ 0xa7, 0x70, 0xd6, 0x2a, 0x20, 0xa4, 0xb3, 0xe4, 0xb7, 0x09, 0xd0, 0xf1,
+ 0xfc, 0x55, 0x6a, 0x18, 0xcd, 0x1d, 0x7b, 0xda, 0x27, 0xcd, 0x5b, 0x68,
+ 0x77, 0x76, 0x75, 0x1b, 0x6e, 0xed, 0x80, 0x51, 0x62, 0x6e, 0x28, 0xcb,
+ 0x25, 0xaf, 0xdf, 0x0a, 0xf8, 0xfd, 0xf5, 0x2a, 0xde, 0x61, 0x54, 0x7b,
+ 0x11, 0x2b, 0x69, 0x6c, 0x95, 0x0c, 0x83, 0x84, 0x62, 0x3d, 0xb8, 0x8e,
+ 0x76, 0x58, 0xfa, 0x2e, 0x4e, 0xee, 0x1b, 0x01, 0xec, 0x82, 0xd9, 0xcc,
+ 0xa4, 0xa2, 0x36, 0x24, 0xfd, 0x71, 0xf0, 0x18, 0x27, 0x07, 0x2d, 0xd3,
+ 0xb6, 0xff, 0x46, 0x53, 0x76, 0xa8, 0xda, 0xb0, 0x8a, 0xae, 0x23, 0xc8,
+ 0xf6, 0xb2, 0x81, 0x1f, 0xb7, 0x33, 0x24, 0xe1, 0xe1, 0x4a, 0x6e, 0xd0,
+ 0x9f, 0x30, 0xe1, 0xc9, 0xff, 0x19, 0xf3, 0x47, 0x1c, 0x10, 0x86, 0xc0,
+ 0xaa, 0xab, 0x5c, 0x58, 0xa7, 0xc6, 0x37, 0x35, 0x01, 0xff, 0xd3, 0xe8,
+ 0xf9, 0xf4, 0x8e, 0x75, 0x88, 0x89, 0xf9, 0x89, 0x29, 0xee, 0x9f, 0xf0,
+ 0xe6, 0x93, 0x96, 0x39, 0xe8, 0x97, 0x89, 0xff, 0x89, 0x3a, 0x34, 0x00,
+ 0x6f, 0x66, 0x6f, 0x7c, 0xdc, 0xc1, 0xf6, 0x00, 0xa0, 0x86, 0x5d, 0xc5,
+ 0xdf, 0x0a, 0xc5, 0xac, 0xa9, 0xf1, 0x39, 0x1a, 0xe0, 0x73, 0x8b, 0x73,
+ 0x1e, 0x48, 0xe1, 0x9a, 0x11, 0xa4, 0x3f, 0xa4, 0xd2, 0x13, 0x21, 0xa1,
+ 0x1f, 0x64, 0x7e, 0x26, 0x16, 0x6e, 0x2b, 0x8e, 0x85, 0x52, 0x02, 0xe5,
+ 0xdc, 0x89, 0xa8, 0xcf, 0x1d, 0x1f, 0xe9, 0xd8, 0x51, 0x9f, 0x65, 0x98,
+ 0x2a, 0x39, 0x23, 0x69, 0x00, 0xb1, 0xf4, 0x8e, 0x00, 0xcb, 0xc5, 0x24,
+ 0x90, 0x74, 0x0e, 0x3b, 0xae, 0xd9, 0x1a, 0xa9, 0xb3, 0x3e, 0xc4, 0xeb,
+ 0xb3, 0xe6, 0x50, 0xf9, 0xa8, 0x33, 0xbd, 0x49, 0x83, 0xd2, 0x14, 0xee,
+ 0xfe, 0xca, 0x66, 0xfb, 0x1a, 0xf7, 0x37, 0xe4, 0x2b, 0xd6, 0x75, 0xbd,
+ 0xe2, 0x54, 0x6e, 0x14, 0xd6, 0xb6, 0x9b, 0x16, 0xd2, 0x2c, 0x5a, 0xa4,
+ 0x3e, 0xb0, 0x89, 0x57, 0xd7, 0x92, 0x96, 0xbb, 0xf5, 0xeb, 0xef, 0x69,
+ 0xbe, 0x10, 0xee, 0xb8, 0x49, 0x8c, 0xa8, 0x46, 0x5f, 0x55, 0xdc, 0xbe,
+ 0xfb, 0xbc, 0x32, 0xcd, 0x05, 0x37, 0x8a, 0x6b, 0xfe, 0xd1, 0xa4, 0x19,
+ 0xd5, 0x62, 0xe2, 0x70, 0x3d, 0x25, 0x6d, 0x52, 0xa1, 0xfc, 0xed, 0x6e,
+ 0x7c, 0x86, 0x75, 0x24, 0x5f, 0x5b, 0x4d, 0x9a, 0x8c, 0xba, 0x2e, 0x11,
+ 0xd5, 0x08, 0x48, 0x06, 0xa6, 0x8b, 0x16, 0x95, 0x4b, 0x13, 0xda, 0xeb,
+ 0xae, 0xb6, 0x12, 0x9f, 0x5e, 0xa4, 0x15, 0x75, 0x7f, 0xdc, 0xc7, 0x73,
+ 0x99, 0xe4, 0x20, 0xc7, 0x07, 0x06, 0xd7, 0x40, 0x27, 0xf0, 0x50, 0xc8,
+ 0x58, 0x05, 0x85, 0x62, 0x38, 0x4a, 0xaf, 0xcb, 0x69, 0x88, 0x95, 0x1b,
+ 0xa0, 0x1d, 0x57, 0x1c, 0xd6, 0x27, 0x35, 0x20, 0x9b, 0x1e, 0xb2, 0xdb,
+ 0xbc, 0xcf, 0x3d, 0xc2, 0x73, 0xea, 0x65, 0xb1, 0xe3, 0xcc, 0xb7, 0x17,
+ 0xdc, 0x7a, 0xad, 0xe7, 0xcd, 0x2e, 0xa7, 0x63, 0x44, 0xd1, 0x37, 0x07,
+ 0xbd, 0xa4, 0x76, 0xcb, 0xa8, 0x8f, 0x0f, 0x40, 0x3e, 0x3c, 0x69, 0x68,
+ 0x89, 0xba, 0xe5, 0x68, 0xf8, 0x61, 0xa5, 0xa8, 0xdc, 0xae, 0x63, 0x8a,
+ 0xef, 0xc6, 0x07, 0xcc, 0x3b, 0xd6, 0x53, 0x37, 0xee, 0xf2, 0x16, 0xf0,
+ 0x7a, 0xdf, 0xde, 0xb2, 0x42, 0xd2, 0xca, 0xcb, 0x95, 0xd3, 0x62, 0xbf,
+ 0x71, 0xa2, 0xd9, 0xd5, 0x7d, 0x3f, 0x8e, 0xde, 0x6c, 0xf2, 0xb7, 0xa4,
+ 0x77, 0xf5, 0xa8, 0xeb, 0x54, 0x88, 0x6e, 0xc0, 0xdc, 0x2e, 0x34, 0x00,
+ 0x53, 0xc4, 0xa4, 0x1e, 0x58, 0xc4, 0x9c, 0xfa, 0x69, 0xce, 0x3f, 0x12,
+ 0x38, 0x18, 0x47, 0x50, 0x50, 0x51, 0xd4, 0xbf, 0x25, 0xb8, 0xd3, 0x99,
+ 0xca, 0xb8, 0x47, 0x5d, 0x04, 0xb7, 0x5d, 0x9b, 0x7c, 0xad, 0x77, 0x0b,
+ 0xbb, 0xf6, 0xa1, 0x32, 0x61, 0xa7, 0x94, 0x3e, 0xeb, 0x97, 0xf3, 0x85,
+ 0x25, 0x07, 0x06, 0x69, 0x8b, 0x04, 0x56, 0xa9, 0xe1, 0x38, 0xbe, 0x95,
+ 0x92, 0x69, 0xa6, 0x40, 0xbf, 0xe7, 0xa8, 0xf0, 0x8f, 0xbd, 0x5a, 0xe0,
+ 0x7c, 0x33, 0x80, 0x1d, 0x37, 0x47, 0xfe, 0x7b, 0xe6, 0x63, 0x02, 0x1d,
+ 0x0e, 0x8e, 0x21, 0x76, 0x8a, 0xfe, 0xfc, 0x04, 0x54, 0x44, 0xc0, 0xda,
+ 0x40, 0xce, 0x27, 0x69, 0xa5, 0x37, 0x3c, 0xbc, 0x3a, 0x90, 0x88, 0x8d,
+ 0xd0, 0xc3, 0x5f, 0x88, 0x0c, 0x7f, 0x5a, 0xa4, 0x16, 0x1b, 0x1d, 0xa5,
+ 0x4e, 0x2d, 0x7b, 0x31, 0x8b, 0xef, 0x96, 0xf5, 0xa3, 0x84, 0x85, 0xb4,
+ 0x76, 0x1e, 0x9d, 0xa3, 0x1c, 0x40, 0xff, 0xd3, 0xdd, 0x99, 0x69, 0x58,
+ 0xfb, 0x0f, 0x98, 0x2d, 0xb4, 0xfb, 0xcc, 0xd3, 0x07, 0x85, 0xd2, 0xb7,
+ 0x24, 0xf2, 0xa1, 0x9c, 0xfd, 0x60, 0x7a, 0xa0, 0x40, 0xb2, 0x9d, 0x82,
+ 0x30, 0x68, 0x42, 0x66, 0x47, 0xba, 0x3c, 0xef, 0x80, 0x9b, 0x2d, 0xd2,
+ 0xb1, 0x1d, 0x93, 0xcb, 0x43, 0x4e, 0x64, 0x91, 0x64, 0x65, 0xad, 0x61,
+ 0x24, 0xce, 0xd6, 0xfc, 0xaf, 0xef, 0x02, 0x60, 0x7e, 0x4f, 0x57, 0x74,
+ 0x5c, 0x43, 0x42, 0x86, 0xd5, 0xac, 0xc7, 0xec, 0xb4, 0xb2, 0x2c, 0x73,
+ 0xd3, 0x74, 0x55, 0xa2, 0x80, 0xbb, 0xea, 0xaf, 0x51, 0xa9, 0xa8, 0xdb,
+ 0x48, 0x8b, 0xfe, 0xf6, 0x11, 0x83, 0x4d, 0x0c, 0xcd, 0x7b, 0x26, 0x71,
+ 0xe8, 0xf2, 0xf7, 0x87, 0x28, 0x98, 0x3c, 0xe6, 0xb5, 0xea, 0xea, 0x53,
+ 0x8c, 0x87, 0xdd, 0xf8, 0x96, 0x02, 0x94, 0xf9, 0x94, 0x19, 0xd3, 0x83,
+ 0x72, 0x71, 0x13, 0x86, 0x69, 0xb0, 0x7c, 0x5b, 0x36, 0x45, 0x2e, 0x6e,
+ 0x7d, 0x88, 0x80, 0xfb, 0x6c, 0x9b, 0xc6, 0x05, 0xe1, 0xa8, 0x74, 0x39,
+ 0x89, 0x51, 0xfc, 0x2e, 0x43, 0x10, 0xec, 0xc3, 0xdc, 0xfd, 0xc9, 0xa1,
+ 0x25, 0x36, 0xff, 0x7b, 0x7d, 0x47, 0x67, 0x98, 0xc6, 0x4f, 0xb4, 0x61,
+ 0x9d, 0x8d, 0x0b, 0x28, 0x64, 0x84, 0x69, 0x1f, 0xd6, 0x46, 0xc2, 0x3e,
+ 0x3a, 0xe2, 0x64, 0xa1, 0x3c, 0x60, 0xce, 0x73, 0xcf, 0xcd, 0xd0, 0x01,
+ 0x99, 0x1f, 0x17, 0xad, 0xfb, 0xcc, 0x38, 0x3f, 0xeb, 0x20, 0x84, 0xf7,
+ 0xd0, 0x72, 0x73, 0xa0, 0x57, 0xce, 0x38, 0x24, 0xa1, 0xc5, 0x59, 0x4f,
+ 0x49, 0x3a, 0x7f, 0xd9, 0x61, 0x0c, 0x85, 0xce, 0xa6, 0x5c, 0xb2, 0x65,
+ 0xbc, 0xd1, 0x79, 0x17, 0xc5, 0x5c, 0x3f, 0x79, 0xbe, 0xec, 0xe4, 0x22,
+ 0x81, 0xb8, 0x03, 0x24, 0x03, 0x9c, 0xb3, 0xf7, 0xeb, 0xaa, 0xf8, 0x1a,
+ 0x40, 0x0e, 0xcd, 0xb7, 0x17, 0xad, 0xc4, 0x9d, 0x15, 0x54, 0x5e, 0x15,
+ 0xf8, 0x58, 0x32, 0x8c, 0x52, 0x8b, 0x77, 0x84, 0x6b, 0xab, 0x88, 0xdf,
+ 0x82, 0x27, 0x7e, 0xfb, 0xea, 0xa5, 0x83, 0xec, 0x29, 0x4c, 0x7c, 0x2f,
+ 0x75, 0xf6, 0xad, 0xce, 0x21, 0x10, 0x0a, 0x64, 0x64, 0xf9, 0xab, 0x46,
+ 0x91, 0xfb, 0xf4, 0xb7, 0x9c, 0x3d, 0x72, 0x03, 0x79, 0x52, 0x71, 0x54,
+ 0x1d, 0xfe, 0x51, 0x57, 0xc5, 0xba, 0xf2, 0x2c, 0x0e, 0x41, 0x3a, 0x57,
+ 0xe6, 0xe9, 0x5b, 0x64, 0xac, 0xff, 0xc1, 0xc4, 0x2f, 0x9e, 0xe6, 0x29,
+ 0x95, 0x8e, 0xa0, 0xa0, 0x17, 0x8a, 0xf5, 0x32, 0x83, 0x5b, 0x57, 0x92,
+ 0x1f, 0x4c, 0xa8, 0x0e, 0xfb, 0x89, 0x05, 0xb6, 0xc6, 0xa5, 0x45, 0x52,
+ 0x61, 0xf2, 0x0f, 0x85, 0x27, 0xbf, 0x9d, 0xb8, 0x1f, 0x16, 0x05, 0x58,
+ 0x5f, 0x2b, 0x85, 0x0a, 0xd3, 0xbe, 0xa0, 0xbe, 0x68, 0xb5, 0xac, 0x87,
+ 0xd0, 0x34, 0x92, 0x9f, 0x18, 0x92, 0x11, 0x5f, 0xcb, 0x72, 0xa4, 0xd6,
+ 0xa1, 0x59, 0x06, 0xfe, 0x62, 0x13, 0x50, 0x56, 0x38, 0xba, 0x97, 0xbc,
+ 0x9d, 0xfb, 0xa6, 0xb0, 0x35, 0x3d, 0x22, 0x47, 0x52, 0x70, 0xea, 0xcb,
+ 0xa4, 0x75, 0x8e, 0xcd, 0xcb, 0x2e, 0x82, 0x1b, 0xfc, 0xf1, 0x31, 0xbd,
+ 0xbe, 0xba, 0x96, 0x17, 0x06, 0x2b, 0x09, 0x52, 0x8e, 0x30, 0x83, 0x3a,
+ 0x19, 0x8c, 0x2e, 0xec, 0x7a, 0x50, 0x53, 0x60, 0x13, 0xf8, 0x4a, 0x0a,
+ 0xe6, 0x23, 0xc7, 0xb2, 0x95, 0x24, 0x40, 0x9b, 0x4f, 0xfb, 0x68, 0xb2,
+ 0xc7, 0xce, 0xa1, 0x54, 0x14, 0x3c, 0x39, 0xc0, 0x7a, 0x65, 0x7c, 0x97,
+ 0x00, 0x27, 0xb8, 0xbc, 0x01, 0x8f, 0x88, 0x0e, 0x12, 0xfd, 0x33, 0xdd,
+ 0xaf, 0x30, 0x60, 0x45, 0x21, 0x5d, 0x9e, 0x50, 0xce, 0x69, 0x35, 0x8f,
+ 0x6b, 0xd3, 0xb4, 0xc4, 0x19, 0x03, 0x58, 0xd5, 0xad, 0x8c, 0xfe, 0xd0,
+ 0xac, 0x15, 0xaa, 0xd7, 0x90, 0x92, 0xcc, 0xb5, 0xb5, 0x79, 0xdc, 0x95,
+ 0x90, 0x51, 0x04, 0xb3, 0x62, 0xe3, 0xaf, 0xdd, 0xb6, 0xeb, 0xc3, 0x79,
+ 0x44, 0x9e, 0x21, 0xd5, 0x1d, 0xec, 0x93, 0x47, 0x7b, 0xa9, 0x30, 0x9b,
+ 0x80, 0xa0, 0x09, 0x6b, 0x95, 0x8b, 0x30, 0x46, 0x6a, 0xdf, 0xe5, 0x5c,
+ 0x54, 0x62, 0x21, 0x49, 0x8c, 0xbf, 0xc7, 0x57, 0x2e, 0xd8, 0x39, 0xe6,
+ 0x60, 0xac, 0x54, 0xe8, 0x97, 0x96, 0xba, 0x21, 0xba, 0x37, 0x47, 0x61,
+ 0x34, 0x73, 0xdf, 0xda, 0x27, 0x24, 0xa3, 0x25, 0x5f, 0xbb, 0xd1, 0xca,
+ 0xec, 0x17, 0xda, 0x76, 0xb4, 0x0b, 0x0d, 0x51, 0x8a, 0xd4, 0xdd, 0x94,
+ 0x30, 0xb4, 0x71, 0x28, 0xa5, 0x0a, 0x89, 0x80, 0xc8, 0xbe, 0x30, 0x79,
+ 0x32, 0xed, 0x6e, 0x1c, 0xd6, 0x16, 0xc2, 0x38, 0x4f, 0xd6, 0xf5, 0x30,
+ 0x92, 0x19, 0x35, 0xcd, 0x23, 0x41, 0xde, 0x59, 0x4f, 0xbf, 0x9a, 0x86,
+ 0xad, 0x86, 0x97, 0xa1, 0x22, 0x9e, 0x0e, 0xe2, 0xf0, 0xcc, 0x1e, 0x72,
+ 0xe6, 0x77, 0x8f, 0x21, 0x82, 0xa1, 0xf9, 0x7b, 0x4b, 0x89, 0x22, 0xc0,
+ 0x50, 0xbe, 0x75, 0x5f, 0x79, 0xca, 0x2d, 0x32, 0xf1, 0x13, 0x43, 0xec,
+ 0x5f, 0x9a, 0x8a, 0xe0, 0xda, 0xab, 0xb6, 0xdb, 0xfd, 0xc5, 0x39, 0x15,
+ 0x4c, 0x79, 0x10, 0x6c, 0x8b, 0x68, 0xb4, 0x37, 0x3b, 0x3e, 0x1c, 0x80,
+ 0xaf, 0x72, 0xb3, 0x43, 0x7b, 0x95, 0x7b, 0x5d, 0xa8, 0xde, 0x78, 0x99,
+ 0xba, 0x9e, 0xe7, 0xbd, 0x94, 0x71, 0xfb, 0x9c, 0xa6, 0x50, 0x76, 0xdb,
+ 0xbb, 0xa1, 0x07, 0x2f, 0x76, 0x91, 0x26, 0x08, 0xe3, 0x4c, 0xc8, 0xec,
+ 0xac, 0x8e, 0xed, 0x63, 0x7c, 0x8c, 0x1c, 0xf8, 0xb4, 0xd5, 0xb4, 0xda,
+ 0x0d, 0x9f, 0xcf, 0x8a, 0xc1, 0x68, 0x98, 0xd5, 0x5f, 0x73, 0x34, 0xc2,
+ 0xe0, 0xaf, 0x27, 0x78, 0xa9, 0xa6, 0x2f, 0x9a, 0x0b, 0x6a, 0xdc, 0xd3,
+ 0xb7, 0x66, 0x5b, 0xae, 0x1a, 0x23, 0x7e, 0xfb, 0xce, 0x4b, 0x6e, 0x01,
+ 0x97, 0x84, 0x09, 0x21, 0x89, 0x77, 0x21, 0xa2, 0xc4, 0xe5, 0xed, 0xb7,
+ 0xc7, 0x23, 0x9f, 0x34, 0x2b, 0xf9, 0x94, 0x83, 0xe3, 0xff, 0xf1, 0x2f,
+ 0x49, 0x8f, 0x34, 0xb3, 0x73, 0xa3, 0x83, 0x87, 0xfc, 0xaf, 0x54, 0x9d,
+ 0xb1, 0xbc, 0x51, 0x2a, 0x0b, 0x12, 0x49, 0xc0, 0xbd, 0xfc, 0x09, 0x20,
+ 0xd6, 0x52, 0x26, 0xc1, 0xb5, 0xbf, 0xed, 0xdb, 0xf1, 0x75, 0xde, 0x62,
+ 0xc2, 0x4c, 0xc7, 0xa2, 0x5a, 0x10, 0xf9, 0xa8, 0xba, 0x1c, 0x99, 0xe6,
+ 0x7c, 0x40, 0x70, 0x81, 0x09, 0xfd, 0x45, 0x8b, 0xb5, 0x05, 0x55, 0x2f,
+ 0x11, 0x1c, 0x41, 0xbd, 0x79, 0x19, 0x54, 0xdb, 0xd1, 0x79, 0xe0, 0x76,
+ 0xde, 0x6c, 0x56, 0x38, 0x0e, 0x5e, 0x87, 0x69, 0xb0, 0x88, 0x1a, 0xcf,
+ 0x3e, 0xf2, 0x02, 0x63, 0x7b, 0xc9, 0x61, 0x0e, 0xe9, 0xac, 0xf0, 0x0e,
+ 0xaf, 0x81, 0x99, 0xe6, 0x90, 0x6e, 0x5e, 0xc1, 0x5e, 0xbb, 0xff, 0xee,
+ 0xf2, 0xfb, 0xc4, 0x66, 0x03, 0xe4, 0xd4, 0xbd, 0xcc, 0x79, 0xd9, 0x8b,
+ 0xee, 0xe2, 0x2e, 0xac, 0x68, 0x29, 0x55, 0x9a, 0x9e, 0x7f, 0x0c, 0x51,
+ 0x19, 0x9e, 0x89, 0xf8, 0x72, 0xe8, 0x6e, 0xfe, 0x74, 0x17, 0x2c, 0x95,
+ 0xe3, 0x1e, 0xcf, 0x3e, 0xeb, 0x9e, 0xff, 0x68, 0xbd, 0xe6, 0xd0, 0x59,
+ 0x66, 0x1c, 0xc3, 0xed, 0x04, 0x0d, 0x35, 0xc1, 0x9e, 0x86, 0xbe, 0x26,
+ 0xc5, 0x59, 0x77, 0x6d, 0x63, 0xc4, 0x3d, 0xc4, 0x63, 0x6e, 0x24, 0xa6,
+ 0x0d, 0xf9, 0x73, 0xd6, 0x40, 0xaf, 0xa3, 0x13, 0xe6, 0x4e, 0x52, 0x02,
+ 0x27, 0x00, 0xf1, 0x9e, 0x4a, 0x32, 0x9b, 0x0e, 0xc7, 0x8d, 0x12, 0x2e,
+ 0x95, 0x2e, 0xff, 0xa6, 0xd4, 0xb0, 0xcc, 0x16, 0xe9, 0xd6, 0xf6, 0x99,
+ 0x13, 0xbf, 0x07, 0x94, 0x5e, 0x8f, 0xa5, 0xc5, 0x34, 0xca, 0x4a, 0x65,
+ 0xb9, 0x4a, 0x5d, 0x0e, 0xd6, 0xf7, 0x08, 0x8f, 0xdb, 0xcb, 0xdd, 0xcf,
+ 0x84, 0xff, 0x17, 0x90, 0xc8, 0xc4, 0x41, 0x06, 0x95, 0x5c, 0x43, 0x80,
+ 0xb6, 0x34, 0xfc, 0xfe, 0x37, 0xb8, 0xba, 0xd9, 0x0e, 0x5b, 0x6c, 0x35,
+ 0xcb, 0xe2, 0xd8, 0x5a, 0x8e, 0xe4, 0x76, 0x98, 0xc2, 0x6d, 0xb1, 0x28,
+ 0xc2, 0xa3, 0x4d, 0xed, 0xed, 0x77, 0x74, 0xe6, 0x15, 0x69, 0x46, 0xe9,
+ 0x87, 0xa7, 0x76, 0x54, 0x16, 0x5f, 0x3e, 0x5b, 0x2b, 0x64, 0xb7, 0xf7,
+ 0x0b, 0xe8, 0x69, 0x3b, 0x05, 0x91, 0x71, 0xea, 0x9b, 0x36, 0xa9, 0x46,
+ 0x19, 0x15, 0xdb, 0x4e, 0x16, 0x04, 0xf0, 0x27, 0x20, 0xf9, 0xca, 0x01,
+ 0x97, 0x6b, 0xe6, 0xe2, 0xb1, 0x74, 0xc6, 0x85, 0x3e, 0x76, 0x33, 0x49,
+ 0x59, 0xbe, 0x42, 0x40, 0x50, 0x38, 0x44, 0x75, 0x9c, 0xad, 0x80, 0xbf,
+ 0x55, 0x0f, 0x1b, 0xac, 0xd6, 0x3d, 0x55, 0x5e, 0xf6, 0x4c, 0x1d, 0x1d,
+ 0xf0, 0xde, 0xa2, 0xb7, 0x4d, 0x28, 0xc7, 0x4a, 0x12, 0xc3, 0x56, 0xb7,
+ 0x15, 0xcf, 0xbf, 0x63, 0x4c, 0x27, 0x9a, 0x6f, 0xdd, 0x20, 0x02, 0x4a,
+ 0x44, 0xbe, 0x90, 0x66, 0xfb, 0x4c, 0x3e, 0x2d, 0x13, 0x2b, 0x5c, 0x11,
+ 0x9e, 0x36, 0x2f, 0x5f, 0xcc, 0xd6, 0xe8, 0x19, 0xbc, 0x36, 0x73, 0x48,
+ 0x98, 0xb4, 0xe2, 0xf0, 0x9b, 0xfd, 0xdd, 0xe6, 0x16, 0x39, 0x9d, 0x95,
+ 0x93, 0x72, 0x7c, 0x37, 0x49, 0xc4, 0x7e, 0xea, 0x50, 0x3d, 0xc6, 0x50,
+ 0xcd, 0x54, 0x32, 0xa7, 0x77, 0xc7, 0x9a, 0x46, 0x9b, 0xb9, 0xf9, 0x75,
+ 0xa7, 0x7c, 0x8f, 0x20, 0xfa, 0xf9, 0x6a, 0xbe, 0x38, 0x55, 0x3f, 0xd7,
+ 0x8e, 0xa8, 0x3c, 0x08, 0x01, 0x7e, 0xa3, 0x4b, 0x0a, 0x93, 0xb1, 0x3d,
+ 0x87, 0xa7, 0xdb, 0x13, 0x9c, 0x7f, 0x93, 0xcd, 0x75, 0x12, 0x31, 0x2c,
+ 0x8e, 0xf1, 0xa8, 0x50, 0xc0, 0xda, 0x14, 0x01, 0x5e, 0x7d, 0x27, 0x3c,
+ 0xa5, 0x9d, 0x24, 0x6a, 0x6e, 0x35, 0x55, 0x38, 0x94, 0x0d, 0xcc, 0x44,
+ 0xf0, 0x4b, 0xb2, 0xe0, 0xab, 0x3b, 0x50, 0xf2, 0x98, 0xb1, 0x4c, 0x43,
+ 0xae, 0x1c, 0x12, 0xe8, 0x6b, 0xb3, 0x4d, 0x5a, 0x50, 0xc1, 0x61, 0x93,
+ 0x19, 0x7c, 0x32, 0xf1, 0x05, 0xf4, 0xe3, 0x02, 0xab, 0x41, 0x48, 0xb0,
+ 0xe8, 0xc4, 0x87, 0xee, 0xd7, 0x5f, 0xe1, 0xca, 0x4d, 0xe2, 0xa7, 0x4c,
+ 0xe3, 0xc7, 0xa5, 0xf9, 0x2c, 0x1b, 0xcb, 0xf2, 0xf1, 0xc8, 0x29, 0xa4,
+ 0xd3, 0xf5, 0x81, 0x3e, 0x2b, 0xe5, 0x9f, 0x95, 0x61, 0x97, 0xfa, 0xd6,
+ 0xe1, 0xe0, 0xd9, 0x9b, 0x00, 0x87, 0x0a, 0x65, 0x35, 0x7f, 0x33, 0x2f,
+ 0x78, 0x20, 0x2d, 0x92, 0x55, 0x34, 0xd5, 0x61, 0x9b, 0x2d, 0x6f, 0x8e,
+ 0xa6, 0xd7, 0x8f, 0x72, 0x64, 0x1f, 0xdf, 0xbe, 0xea, 0x1f, 0x64, 0x7c,
+ 0x4b, 0x06, 0x6e, 0xb3, 0xad, 0xad, 0xbd, 0xed, 0xb4, 0x97, 0x5c, 0xc8,
+ 0xce, 0xaa, 0x36, 0xd7, 0x51, 0xfa, 0x63, 0x99, 0x5e, 0x01, 0x5a, 0x3e,
+ 0x60, 0xaa, 0x74, 0x7b, 0x68, 0xf8, 0x80, 0xdf, 0xfb, 0x71, 0xde, 0xc4,
+ 0x49, 0x73, 0xdf, 0xee, 0xaa, 0xb9, 0x30, 0xa3, 0x8b, 0x46, 0x62, 0x4c,
+ 0xc4, 0x43, 0x04, 0xf9, 0xdf, 0x9d, 0x31, 0xbb, 0xa8, 0xc8, 0x3b, 0xb6,
+ 0xe9, 0x0c, 0x7d, 0x47, 0xa0, 0x41, 0x15, 0x9e, 0x06, 0x45, 0x14, 0xdc,
+ 0x66, 0xfe, 0x4a, 0x07, 0x46, 0xdd, 0x62, 0xd4, 0xfa, 0xa2, 0x46, 0x26,
+ 0x64, 0x5d, 0x1c, 0x9a, 0x39, 0xe6, 0x54, 0xa2, 0xc4, 0x7a, 0xdf, 0x5a,
+ 0x02, 0x74, 0x92, 0xe0, 0x7a, 0x61, 0xc6, 0x58, 0x47, 0xee, 0xa9, 0x25,
+ 0x8b, 0xe5, 0xb5, 0xc1, 0x54, 0x74, 0xd0, 0x5b, 0xfd, 0xac, 0x75, 0x00,
+ 0x3f, 0x46, 0x43, 0x97, 0x55, 0xcd, 0x1e, 0xe2, 0x27, 0x6c, 0xfe, 0x8c,
+ 0xd2, 0xd1, 0x0b, 0xc8, 0x11, 0x4c, 0x25, 0xfd, 0x2e, 0xa6, 0x2e, 0x7f,
+ 0x21, 0xa5, 0xd1, 0x42, 0x74, 0x58, 0x63, 0x71, 0xa6, 0x6f, 0xb4, 0x53,
+ 0x97, 0x70, 0x22, 0xf7, 0x2c, 0x76, 0x0e, 0x94, 0x08, 0x1e, 0xca, 0xdb,
+ 0x48, 0xb4, 0xe6, 0x44, 0x16, 0xf6, 0x04, 0xe0, 0xa5, 0xfb, 0x1a, 0x96,
+ 0xfd, 0xf5, 0xb7, 0x86, 0xd7, 0x87, 0x2d, 0xb1, 0x39, 0xb1, 0x1c, 0x52,
+ 0xf2, 0x91, 0x6b, 0x16, 0xdc, 0xf7, 0xf7, 0x1b, 0xac, 0xa5, 0x80, 0x76,
+ 0xa7, 0x96, 0x75, 0xa7, 0x95, 0xbf, 0xed, 0xa7, 0x20, 0x69, 0xd0, 0xc5,
+ 0xcc, 0x2d, 0xee, 0x31, 0xa5, 0x80, 0x31, 0x6d, 0x04, 0x77, 0x1e, 0xb0,
+ 0xdd, 0x30, 0x09, 0xc9, 0x8b, 0x59, 0x3a, 0x5a, 0x5f, 0x3f, 0xb0, 0x58,
+ 0xdd, 0x64, 0xa1, 0x8b, 0x80, 0x60, 0x8d, 0x28, 0x17, 0xec, 0x0a, 0x9b,
+ 0xad, 0x8f, 0x5f, 0xfc, 0x33, 0x3c, 0x2b, 0x57, 0xed, 0xcf, 0x5a, 0x6f,
+ 0xfe, 0x62, 0x45, 0x5c, 0x80, 0x57, 0x71, 0x58, 0x77, 0x6b, 0x29, 0xa8,
+ 0xd4, 0xd1, 0xdc, 0x15, 0x4a, 0x0e, 0xf6, 0x1b, 0x17, 0xb6, 0xa8, 0x58,
+ 0x89, 0x28, 0x03, 0xac, 0xe4, 0xba, 0x82, 0xb4, 0x37, 0x5e, 0x4f, 0x4f,
+ 0x45, 0xd6, 0x19, 0x2d, 0xe3, 0xc3, 0xde, 0xa0, 0x38, 0xe7, 0x72, 0xb6,
+ 0xb9, 0x36, 0x73, 0x6c, 0x65, 0x9a, 0xb3, 0xa8, 0x0e, 0x1c, 0xf0, 0xf1,
+ 0x96, 0x1a, 0x5b, 0xed, 0x82, 0xad, 0x4f, 0x2c, 0x3e, 0x5d, 0xaf, 0xab,
+ 0x31, 0x5b, 0x15, 0x96, 0x08, 0xb1, 0x64, 0xdb, 0x8e, 0x8f, 0xb7, 0x42,
+ 0x40, 0x93, 0x38, 0x99, 0x78, 0xcd, 0x23, 0xf1, 0x5b, 0xff, 0xad, 0x69,
+ 0x60, 0xce, 0x70, 0xdf, 0x9c, 0x95, 0x53, 0x82, 0x03, 0xf5, 0xdd, 0xd9,
+ 0x22, 0xbe, 0xb8, 0x0b, 0x3f, 0x55, 0x4d, 0x7a, 0x15, 0x88, 0xd4, 0xb1,
+ 0xbf, 0xc7, 0x78, 0x37, 0x62, 0xd0, 0x6d, 0xf4, 0x29, 0xbc, 0xb7, 0x7c,
+ 0xc5, 0x1c, 0x9e, 0xf8, 0xd2, 0x63, 0x47, 0x4e, 0x18, 0x6e, 0x05, 0x51,
+ 0xcc, 0x3f, 0xc8, 0x87, 0xb6, 0x04, 0xa5, 0x84, 0xff, 0xdc, 0xd4, 0x9d,
+ 0x4e, 0x94, 0xf8, 0x9c, 0x97, 0x76, 0xcc, 0xe4, 0xe5, 0xbe, 0x2c, 0xc4,
+ 0x23, 0x3e, 0xce, 0xa3, 0x97, 0x4a, 0x3f, 0xe2, 0x04, 0x3a, 0x44, 0xf9,
+ 0x7c, 0x07, 0x9d, 0x19, 0x4b, 0xad, 0x11, 0xfb, 0x66, 0x6b, 0x1a, 0x93,
+ 0x81, 0xca, 0x8c, 0x36, 0x08, 0x25, 0xb4, 0xb4, 0xe4, 0x03, 0x39, 0x31,
+ 0x74, 0x44, 0x90, 0x6e, 0x1b, 0xc0, 0xed, 0x7f, 0x83, 0xa3, 0x72, 0x48,
+ 0xb8, 0xae, 0x56, 0xfe, 0x19, 0xbc, 0xb0, 0xdb, 0x19, 0x82, 0x95, 0x17,
+ 0x0c, 0x97, 0x50, 0x71, 0x7c, 0xdb, 0x3d, 0xd5, 0x21, 0x57, 0xaa, 0x69,
+ 0x22, 0x75, 0xcc, 0x0c, 0x13, 0x46, 0x0f, 0x2c, 0x96, 0x76, 0xc0, 0x1e,
+ 0xae, 0x03, 0xc4, 0xf8, 0x42, 0xfd, 0x8a, 0xb1, 0xc0, 0x59, 0x6b, 0x91,
+ 0x54, 0xfb, 0xc2, 0x80, 0x0f, 0xaa, 0xcb, 0xc1, 0xea, 0xdb, 0x83, 0x57,
+ 0xe9, 0x51, 0x37, 0xe7, 0x35, 0xdc, 0x17, 0x06, 0x63, 0xdc, 0x6f, 0xf1,
+ 0xc6, 0x3d, 0x9b, 0x7d, 0x40, 0xbe, 0x45, 0x50, 0x19, 0x39, 0x9f, 0xa5,
+ 0x47, 0x3a, 0xfa, 0xde, 0x99, 0xa4, 0xd4, 0x0a, 0x3d, 0x8f, 0x54, 0xa7,
+ 0xe6, 0x59, 0x37, 0x08, 0x2c, 0xda, 0x44, 0x97, 0x35, 0x4e, 0x54, 0x84,
+ 0xff, 0x14, 0xeb, 0x1a, 0x07, 0x45, 0x70, 0x0a, 0x06, 0xc9, 0x24, 0xed,
+ 0x6f, 0x2e, 0x07, 0x57, 0x5b, 0xe8, 0xeb, 0x0f, 0xa9, 0xba, 0x3e, 0x6f,
+ 0x37, 0xef, 0xa5, 0xc4, 0x35, 0x43, 0xb3, 0x19, 0x27, 0xe5, 0x20, 0x78,
+ 0xdc, 0xb6, 0x58, 0x36, 0x17, 0xd8, 0x54, 0x1f, 0xdd, 0x10, 0x61, 0x8c,
+ 0x3c, 0xc9, 0x5d, 0x9c, 0xcc, 0xa4, 0xb8, 0x47, 0x42, 0x71, 0xc7, 0x9d,
+ 0xb9, 0x04, 0x42, 0xb4, 0xa8, 0x68, 0xe0, 0x77, 0xe7, 0x94, 0x4f, 0xab,
+ 0xf3, 0x83, 0x51, 0x0b, 0x61, 0x22, 0x6b, 0x90, 0xc9, 0x0d, 0x0c, 0xd6,
+ 0x4b, 0x84, 0xb4, 0x28, 0x7d, 0x15, 0xc2, 0x60, 0xe2, 0x93, 0xcf, 0x79,
+ 0x26, 0xdd, 0x76, 0x5b, 0x76, 0x32, 0x7d, 0x24, 0x84, 0x54, 0x45, 0x8d,
+ 0xfb, 0x77, 0xc4, 0x29, 0x71, 0x06, 0xa4, 0x0d, 0xce, 0xc5, 0x87, 0xa4,
+ 0x3a, 0x38, 0xfa, 0x4c, 0x9a, 0x09, 0x98, 0xb8, 0x8f, 0x94, 0xa4, 0xe9,
+ 0xc4, 0xb8, 0x26, 0xfe, 0xd9, 0x51, 0x57, 0x09, 0x20, 0x2d, 0x9a, 0xfc,
+ 0xdd, 0x93, 0x1d, 0x4b, 0x07, 0x02, 0x9f, 0x18, 0x9f, 0x43, 0x57, 0x6a,
+ 0x49, 0xe5, 0x9e, 0x56, 0x0c, 0x63, 0x4c, 0x29, 0xd8, 0x0d, 0x2b, 0xf3,
+ 0x80, 0xaa, 0xe0, 0xf2, 0x0b, 0x1b, 0x2e, 0x34, 0xf6, 0xc4, 0x61, 0x7c,
+ 0xf4, 0x5e, 0xf8, 0xea, 0x55, 0xf4, 0xfc, 0x00, 0xa3, 0x45, 0x7c, 0x81,
+ 0x00, 0x21, 0x00, 0x86, 0x00, 0x40, 0x96, 0xf0, 0x01, 0x1b, 0x06, 0x00,
+ 0x49, 0x73, 0xc0, 0x76, 0x6d, 0x74, 0xe2, 0xb4, 0x55, 0x3a, 0xf2, 0xc2,
+ 0x04, 0x9c, 0x97, 0x59, 0xb4, 0xb0, 0xbf, 0x43, 0xbf, 0x4f, 0x77, 0xbb,
+ 0x57, 0xbf, 0xf3, 0xd8, 0xc2, 0x25, 0xaf, 0xf1, 0xf6, 0x9e, 0xb9, 0xf5,
+ 0xc3, 0x09, 0xf3, 0xef, 0x7f, 0xe8, 0xb9, 0xb1, 0xc8, 0x1b, 0x33, 0x61,
+ 0x4c, 0xc3, 0x1a, 0x1b, 0xf5, 0x7e, 0xcf, 0xdb, 0xeb, 0xf8, 0x47, 0x27,
+ 0xd0, 0x9c, 0xfe, 0xdb, 0xcd, 0x87, 0x38, 0x5d, 0x4f, 0x9e, 0x75, 0x7e,
+ 0x2c, 0x00, 0x00, 0x00, 0x01, 0xa0, 0x7f, 0xef, 0x1a, 0x6d, 0xe9, 0x60,
+ 0xf0, 0x29, 0x98, 0x68, 0x36, 0xa6, 0x3b, 0x2c, 0x65, 0xfc, 0xea, 0x42,
+ 0x51, 0x61, 0x1c, 0xed, 0xb7, 0xb9, 0x39, 0x43, 0x70, 0xa1, 0x03, 0x6e,
+ 0x82, 0x6b, 0x80, 0x55, 0x68, 0x5a, 0x3c, 0x49, 0x21, 0xb7, 0x05, 0x0c,
+ 0xe3, 0x74, 0xcd, 0xfa, 0x47, 0xbc, 0x74, 0x51, 0xc3, 0x68, 0x3b, 0xc8,
+ 0xd9, 0x0a, 0x68, 0x37, 0xc6, 0xe2, 0x56, 0xb5, 0x7e, 0x76, 0x49, 0x1e,
+ 0x37, 0x06, 0x58, 0x5a, 0xf4, 0xed, 0x00, 0xbd, 0x38, 0x5e, 0x3b, 0x6f,
+ 0x3c, 0x14, 0x29, 0xa0, 0x42, 0xcd, 0x3f, 0x9f, 0x56, 0x7e, 0x3b, 0xbb,
+ 0xef, 0xcc, 0x16, 0xd3, 0xab, 0xde, 0x0d, 0xd3, 0x61, 0x14, 0xaa, 0x7f,
+ 0x7c, 0x17, 0x64, 0x20, 0x68, 0xfe, 0xc8, 0xbb, 0x8b, 0x02, 0x3b, 0x0f,
+ 0x6d, 0xad, 0x15, 0x3d, 0x72, 0xe3, 0x2b, 0xa6, 0xe7, 0x53, 0x7c, 0x0f,
+ 0x92, 0x34, 0xb2, 0x47, 0xbd, 0x7b, 0xae, 0x9d, 0x38, 0xcc, 0x2e, 0xc9,
+ 0xdb, 0xe5, 0xce, 0xcb, 0x0b, 0x28, 0x0b, 0x76, 0xb9, 0x4e, 0xd9, 0xff,
+ 0x32, 0x9d, 0xf4, 0xe9, 0xc4, 0x01, 0x72, 0x49, 0x6e, 0xfb, 0x55, 0x14,
+ 0x30, 0xba, 0x41, 0xb3, 0xd9, 0x70, 0x4c, 0xe7, 0x4f, 0xf2, 0xa5, 0x10,
+ 0x96, 0x80, 0x20, 0xce, 0x01, 0xbc, 0x0a, 0x97, 0xbe, 0xb8, 0xc6, 0xfb,
+ 0xe8, 0x3e, 0xe1, 0xbd, 0x1a, 0xa2, 0xde, 0x94, 0x35, 0xa4, 0x3b, 0x1d,
+ 0x22, 0x12, 0xd3, 0xaf, 0xeb, 0x93, 0xfa, 0x3f, 0x8a, 0x47, 0x32, 0x91,
+ 0x87, 0x14, 0xde, 0xc6, 0xaa, 0x42, 0x3e, 0x68, 0xe8, 0x57, 0x4a, 0x36,
+ 0xc0, 0xda, 0xeb, 0x76, 0x71, 0x86, 0xaa, 0xfa, 0x5b, 0xf5, 0x85, 0xd7,
+ 0x0b, 0xd5, 0xee, 0xc0, 0x07, 0xf5, 0xbe, 0x1a, 0xc7, 0x4a, 0x87, 0x2b,
+ 0x27, 0x27, 0x9a, 0x42, 0x72, 0x16, 0xc3, 0xe0, 0x07, 0x97, 0x91, 0x06,
+ 0xcf, 0xcc, 0x02, 0xcc, 0x70, 0x6a, 0xbc, 0xb7, 0x8a, 0x4b, 0x20, 0x0c,
+ 0x1b, 0xb3, 0x98, 0xd1, 0xe6, 0x3b, 0x09, 0xcb, 0xa6, 0xb3, 0x40, 0x0b,
+ 0x77, 0xea, 0xb0, 0xcf, 0x54, 0x85, 0xc0, 0x4f, 0xaf, 0x54, 0x76, 0xca,
+ 0x22, 0x6b, 0xeb, 0x94, 0x0d, 0x22, 0x5b, 0x11, 0x61, 0x06, 0x3e, 0xed,
+ 0xcf, 0x6c, 0xc4, 0x0c, 0xf0, 0xda, 0xd6, 0xd0, 0x69, 0xde, 0xc7, 0x1a,
+ 0xbb, 0x76, 0x42, 0x1d, 0x3d, 0x7e, 0x39, 0xc0, 0xe3, 0x42, 0xc2, 0x43,
+ 0x68, 0x31, 0x04, 0xa0, 0x34, 0xec, 0xdd, 0xb4, 0x6b, 0x5e, 0x8d, 0xc2,
+ 0x7f, 0x21, 0x45, 0x4e, 0x14, 0xc2, 0x81, 0xc1, 0x4e, 0x83, 0xa7, 0x79,
+ 0xd7, 0x68, 0x3c, 0xca, 0x57, 0x6e, 0x9a, 0xcd, 0xfe, 0x0e, 0xd2, 0x6f,
+ 0x8a, 0xba, 0x4c, 0x8c, 0xf4, 0x41, 0xaa, 0x71, 0x96, 0x71, 0xca, 0x5f,
+ 0xea, 0xff, 0x69, 0x90, 0xb3, 0x5c, 0xd8, 0xa1, 0x18, 0x05, 0xdb, 0x33,
+ 0x17, 0x9b, 0x89, 0xb1, 0x6f, 0xce, 0x4b, 0x26, 0x6d, 0x19, 0x6f, 0x6a,
+ 0xf7, 0xa0, 0xbe, 0xb4, 0xc0, 0xa3, 0x0b, 0x96, 0x8b, 0x8f, 0x12, 0x97,
+ 0xfc, 0x40, 0x00, 0x00, 0x00, 0xd6, 0x5e, 0x4d, 0x99, 0x80, 0x3a, 0x02,
+ 0x0b, 0x9e, 0x5e, 0xf5, 0xe8, 0x5e, 0x83, 0x37, 0x34, 0x70, 0xc3, 0xc9,
+ 0x26, 0x09, 0x6f, 0x35, 0xd0, 0xcc, 0xc4, 0x2f, 0xce, 0x18, 0xae, 0x63,
+ 0xa9, 0xf1, 0xda, 0xb2, 0xa4, 0xa2, 0xd4, 0xd2, 0x23, 0xe1, 0xbb, 0xe7,
+ 0x21, 0xdb, 0xc3, 0xb8, 0xf4, 0x34, 0x0d, 0x04, 0xe5, 0x7b, 0xbb, 0x62,
+ 0xfe, 0x5e, 0x33, 0xca, 0xf2, 0x24, 0x73, 0xc3, 0x1e, 0x52, 0x42, 0xb1,
+ 0x87, 0xa5, 0x04, 0x67, 0x88, 0x14, 0x96, 0xe3, 0x8e, 0xb1, 0x10, 0x7a,
+ 0xf5, 0x28, 0xc2, 0x44, 0xf8, 0x22, 0x6c, 0x73, 0x09, 0x3a, 0xe2, 0x83,
+ 0xed, 0xd2, 0x4e, 0x0e, 0x34, 0x80, 0x38, 0xe4, 0x0b, 0x6c, 0xcc, 0x73,
+ 0x70, 0x58, 0x4c, 0xd4, 0x9c, 0x75, 0xfa, 0xd9, 0x45, 0x03, 0x71, 0xb2,
+ 0xe9, 0x70, 0xd6, 0xac, 0x67, 0xce, 0x01, 0x85, 0xf6, 0x09, 0xfd, 0x29,
+ 0x9d, 0xb2, 0xb9, 0xdd, 0xfd, 0xac, 0x10, 0x96, 0x6a, 0x73, 0xe2, 0x2a,
+ 0x5a, 0x76, 0x6b, 0xcf, 0x5d, 0x6c, 0x15, 0xb7, 0x86, 0x4e, 0x6a, 0xa9,
+ 0x36, 0x1b, 0xf2, 0x01, 0x26, 0x51, 0x9c, 0x4c, 0x0a, 0x85, 0x8f, 0x96,
+ 0xfc, 0x59, 0x0a, 0xec, 0x88, 0x1e, 0xcc, 0x57, 0x60, 0x0b, 0xfd, 0x15,
+ 0xab, 0x89, 0x50, 0x1f, 0x74, 0x37, 0xb1, 0x07, 0x7e, 0xd4, 0xf6, 0x57,
+ 0x04, 0x5f, 0xea, 0xc0, 0x9e, 0x63, 0x9d, 0xe4, 0xea, 0x49, 0xc9, 0xfa,
+ 0xca, 0xd3, 0x7c, 0xb9, 0x8e, 0xfa, 0x96, 0xbc, 0x10, 0x0b, 0x03, 0x84,
+ 0x38, 0x22, 0x0b, 0x80, 0x00, 0x00, 0x00, 0x63, 0x60, 0xd8, 0x8b, 0x8d,
+ 0x27, 0xa1, 0xbd, 0x83, 0xcf, 0x24, 0xff, 0x3b, 0xef, 0x20, 0x60, 0x48,
+ 0xf4, 0x50, 0x91, 0xe8, 0x1f, 0xde, 0x67, 0x18, 0xaa, 0x75, 0xa0, 0xdc,
+ 0x22, 0x15, 0x13, 0x07, 0xca, 0x9b, 0xc4, 0xc5, 0x7e, 0x95, 0xf0, 0xf4,
+ 0x6a, 0x03, 0x06, 0x08, 0x39, 0x23, 0xa8, 0xee, 0xca, 0x1f, 0x93, 0x3b,
+ 0xde, 0x1e, 0xb2, 0xc2, 0x8d, 0xa1, 0xd7, 0x09, 0x29, 0x15, 0x7b, 0x72,
+ 0x8d, 0x83, 0x9d, 0xd9, 0xd7, 0xee, 0x18, 0x57, 0x86, 0x13, 0x42, 0xa9,
+ 0x9a, 0xb6, 0x89, 0x57, 0x97, 0x3a, 0xba, 0x85, 0xb2, 0xa6, 0x65, 0xb9,
+ 0x9c, 0x39, 0x9b, 0x37, 0xd6, 0x46, 0xb9, 0x98, 0x59, 0xc6, 0x00, 0x7f,
+ 0x8f, 0x2f, 0xff, 0xc0, 0xa4, 0xb8, 0xbf, 0x11, 0xc3, 0xa6, 0x48, 0xab,
+ 0x86, 0x74, 0x98, 0x9f, 0xf5, 0x0b, 0x46, 0x73, 0xf1, 0x49, 0x69, 0xbb,
+ 0x25, 0x4f, 0xe4, 0xd3, 0xae, 0x3c, 0x10, 0xaa, 0xf1, 0x33, 0x27, 0x6d,
+ 0x41, 0x74, 0xaa, 0x67, 0xbb, 0x19, 0x78, 0xf6, 0x8c, 0xa6, 0xe1, 0x1a,
+ 0xf5, 0xb3, 0x3c, 0x69, 0x27, 0x25, 0x04, 0x35, 0xe0, 0x41, 0xff, 0xb2,
+ 0xdd, 0xb4, 0xf5, 0xcb, 0x47, 0xfd, 0x01, 0xea, 0x95, 0x40, 0x64, 0xaf,
+ 0x41, 0x52, 0xda, 0x34, 0xee, 0xc9, 0x2f, 0x14, 0x3f, 0x9c, 0xca, 0x9b,
+ 0xa1, 0x50, 0x7a, 0xac, 0x4c, 0xd7, 0x93, 0xcf, 0xf2, 0x42, 0xb2, 0x73,
+ 0xca, 0xe3, 0xf3, 0xd7, 0x17, 0xb5, 0xbb, 0x68, 0xb2, 0xc1, 0xe6, 0x51,
+ 0x06, 0xdc, 0xbf, 0x33, 0xa7, 0x47, 0x7c, 0x3c, 0x38, 0x61, 0x06, 0x2f,
+ 0x65, 0xb5, 0x18, 0xbb, 0xbb, 0x48, 0x86, 0x12, 0x99, 0x7b, 0x09, 0x68,
+ 0x55, 0xf4, 0x23, 0xbb, 0xba, 0x34, 0xa5, 0x81, 0x24, 0x8b, 0x21, 0x2c,
+ 0x2c, 0xb3, 0x13, 0xbc, 0xfb, 0x1e, 0x99, 0x10, 0xd3, 0xad, 0x0f, 0x84,
+ 0xa5, 0x45, 0xda, 0x91, 0x74, 0xbb, 0x5d, 0xcc, 0x6c, 0xa3, 0x60, 0xec,
+ 0x26, 0xcf, 0x86, 0xa1, 0x96, 0xf1, 0xe1, 0x06, 0x7d, 0x15, 0x92, 0x5d,
+ 0x70, 0xed, 0x7b, 0xb6, 0x0b, 0x62, 0xa0, 0xca, 0x98, 0x95, 0xa8, 0xff,
+ 0x14, 0xab, 0x1c, 0x35, 0x65, 0xed, 0x3c, 0x11, 0x9f, 0xb6, 0x1e, 0x41,
+ 0x55, 0xe2, 0x93, 0x1f, 0x97, 0x93, 0x75, 0x91, 0x93, 0x20, 0x19, 0x76,
+ 0xef, 0x52, 0x92, 0xa7, 0x39, 0xd9, 0x74, 0xb7, 0xef, 0x3a, 0x1d, 0xef,
+ 0x77, 0x37, 0x9a, 0x7f, 0x8d, 0xdb, 0xb3, 0xb9, 0x67, 0x89, 0xfc, 0x7d,
+ 0x1b, 0x9b, 0xa9, 0x0e, 0x45, 0x58, 0x35, 0x48, 0x79, 0x45, 0x6d, 0x3e,
+ 0x5a, 0x13, 0x80, 0xf6, 0x72, 0x3b, 0xf4, 0xbb, 0x0a, 0xaa, 0x62, 0xdd,
+ 0x55, 0x4e, 0x68, 0x80, 0xee, 0xb2, 0x8f, 0x8d, 0x2c, 0xb3, 0x9d, 0x5e,
+ 0x17, 0x80, 0x3b, 0x27, 0x2b, 0x33, 0x97, 0x96, 0x4f, 0xb1, 0x7b, 0xc9,
+ 0x63, 0xce, 0x65, 0xf4, 0x66, 0x0e, 0x51, 0x67, 0xaf, 0xc5, 0x3f, 0x17,
+ 0xdb, 0xe2, 0x44, 0x81, 0x2d, 0x41, 0x43, 0xd0, 0x75, 0xd7, 0x37, 0xef,
+ 0x65, 0x49, 0x80, 0x8b, 0x70, 0xff, 0xa5, 0x40, 0x3b, 0x72, 0x09, 0xa3,
+ 0x10, 0xc7, 0x1e, 0x08, 0xd0, 0x50, 0x5c, 0x33, 0xee, 0x08, 0x2c, 0x0c,
+ 0x18, 0x71, 0x37, 0xdf, 0xa3, 0x19, 0x3e, 0xc5, 0x3f, 0x47, 0xe0, 0x64,
+ 0xdf, 0x15, 0x0e, 0x37, 0xa8, 0xbd, 0xf2, 0x6f, 0xec, 0x56, 0xa5, 0x41,
+ 0x41, 0xff, 0xf8, 0xea, 0xba, 0x52, 0xf3, 0xc3, 0x05, 0x90, 0x06, 0x07,
+ 0x01, 0x58, 0xef, 0x58, 0xb1, 0xac, 0xc3, 0x33, 0x87, 0xa4, 0x5b, 0xfd,
+ 0xe3, 0xf3, 0x71, 0x5f, 0x3b, 0xcc, 0xb7, 0x92, 0xa8, 0x60, 0x30, 0x02,
+ 0xa0, 0x5b, 0xc4, 0x79, 0xfc, 0x2f, 0xb6, 0x83, 0x4a, 0xae, 0x54, 0xfe,
+ 0x7c, 0x57, 0x42, 0xd9, 0x37, 0xf4, 0xca, 0x65, 0x80, 0xd1, 0x3c, 0x01,
+ 0x37, 0xd3, 0x33, 0xd8, 0x03, 0x21, 0x83, 0x42, 0x63, 0x85, 0x52, 0x91,
+ 0xb2, 0x02, 0x58, 0x22, 0x10, 0x07, 0xa0, 0xda, 0x40, 0x68, 0xf4, 0x0a,
+ 0x94, 0x4c, 0x57, 0x55, 0x9c, 0x2b, 0xc6, 0xef, 0xf7, 0xe9, 0xa1, 0x70,
+ 0xba, 0x5a, 0x2a, 0xaa, 0xe5, 0xc6, 0x52, 0xd3, 0x32, 0xa9, 0xad, 0x48,
+ 0xf5, 0xe3, 0x96, 0x09, 0x12, 0xbd, 0xc0, 0xc9, 0x63, 0xa5, 0x3b, 0x98,
+ 0x07, 0x6f, 0x1e, 0x5d, 0x58, 0x61, 0x5a, 0xbc, 0x54, 0x9e, 0x1a, 0xf0,
+ 0xe0, 0xe7, 0x33, 0xc9, 0x0a, 0x13, 0x48, 0x7d, 0x1e, 0x9b, 0xe8, 0x4d,
+ 0x99, 0x70, 0xaf, 0x59, 0x1b, 0x7e, 0x20, 0x72, 0x53, 0x68, 0x07, 0xac,
+ 0x1e, 0x10, 0x52, 0x71, 0xd6, 0x16, 0x10, 0x6e, 0x4f, 0x73, 0xea, 0x42,
+ 0x66, 0xa8, 0x8b, 0xcf, 0x9c, 0x5d, 0xc7, 0x7b, 0x9f, 0xe2, 0x61, 0x3a,
+ 0x04, 0xdc, 0x57, 0xe7, 0x1a, 0x4a, 0xee, 0x26, 0x4b, 0x83, 0xbd, 0x63,
+ 0xe8, 0xc1, 0xa1, 0xcc, 0x28, 0x68, 0x8e, 0x64, 0xae, 0xaf, 0x5a, 0x6c,
+ 0x61, 0x55, 0xde, 0x4b, 0x33, 0xf8, 0x4a, 0xb1, 0x91, 0x5f, 0x00, 0xa3,
+ 0x49, 0xcf, 0x81, 0x00, 0x43, 0x00, 0x86, 0x00, 0x40, 0x96, 0xf0, 0x01,
+ 0x1b, 0x06, 0x00, 0x39, 0x76, 0x86, 0x6b, 0xa3, 0x0d, 0xbd, 0xd1, 0xea,
+ 0xb1, 0x15, 0xd3, 0x8c, 0x45, 0xdf, 0x4b, 0x25, 0x63, 0x2e, 0x8c, 0x78,
+ 0xff, 0xa3, 0x0d, 0x06, 0xf5, 0xdc, 0x1f, 0x8b, 0xf1, 0xfc, 0x3f, 0x4f,
+ 0x69, 0xaf, 0x4e, 0xef, 0x7f, 0x99, 0xd5, 0x3d, 0x2d, 0xc7, 0x1e, 0x6e,
+ 0xd9, 0xd8, 0xf4, 0xd7, 0x60, 0xeb, 0x2a, 0xf7, 0xf1, 0xaf, 0x7c, 0x00,
+ 0x00, 0x00, 0x00, 0x03, 0xa5, 0x73, 0x48, 0x16, 0xe6, 0xb4, 0xd1, 0xf1,
+ 0x65, 0x57, 0xc6, 0x3f, 0xef, 0x7e, 0x82, 0x70, 0x20, 0x11, 0x60, 0xcf,
+ 0xa3, 0x5b, 0x19, 0x4a, 0x9c, 0x7a, 0xd7, 0xe4, 0x5f, 0xf3, 0x00, 0xaf,
+ 0x44, 0xf0, 0xf4, 0xb7, 0xfb, 0x24, 0x4d, 0x0e, 0x48, 0xbf, 0x3d, 0xb8,
+ 0xa9, 0x8f, 0xe6, 0x22, 0xa4, 0xba, 0xad, 0xdb, 0xde, 0x1b, 0x2f, 0x3e,
+ 0x83, 0x10, 0xea, 0x7c, 0x38, 0xbb, 0xa8, 0x81, 0xbf, 0x07, 0xe3, 0x35,
+ 0x80, 0x04, 0xa4, 0xee, 0xa5, 0x58, 0x04, 0x1f, 0x33, 0x4c, 0x40, 0x36,
+ 0xd3, 0x2b, 0x77, 0x2e, 0xc0, 0x04, 0x52, 0xcf, 0x85, 0x27, 0xfc, 0x38,
+ 0xda, 0x58, 0x2d, 0x85, 0x70, 0xe6, 0xb7, 0x33, 0xb6, 0x29, 0xd8, 0xbc,
+ 0x50, 0xce, 0x3b, 0xe3, 0xa4, 0x04, 0x94, 0xca, 0x9f, 0xce, 0x40, 0x34,
+ 0x00, 0x8e, 0x86, 0x94, 0xf4, 0xea, 0x01, 0xf6, 0xdb, 0x72, 0x17, 0x08,
+ 0x5f, 0x02, 0x3e, 0x88, 0xd3, 0x89, 0xe4, 0x7d, 0xfa, 0x9a, 0x6f, 0x68,
+ 0xb0, 0x9d, 0xce, 0x55, 0xab, 0xe1, 0x66, 0x1e, 0x6f, 0x02, 0xc4, 0xb6,
+ 0x7c, 0x5b, 0x81, 0xf5, 0x62, 0xb8, 0x62, 0xe5, 0x03, 0xff, 0xbf, 0x1f,
+ 0xa4, 0xd6, 0x0d, 0x5e, 0xed, 0xc4, 0xe9, 0xe8, 0x14, 0x4a, 0x34, 0x7f,
+ 0xbf, 0x33, 0x71, 0x9e, 0x05, 0x9a, 0xb9, 0x0a, 0x9e, 0x63, 0xa4, 0xb7,
+ 0xae, 0xde, 0x01, 0xc0, 0x54, 0xfa, 0x21, 0x1f, 0xde, 0x09, 0x96, 0x05,
+ 0xc1, 0xc3, 0xaa, 0x73, 0xb2, 0x21, 0xf1, 0xc1, 0x36, 0x21, 0x8e, 0x41,
+ 0xb7, 0xc7, 0xe9, 0x40, 0x78, 0xde, 0xcf, 0xa6, 0x01, 0x1b, 0xed, 0xbf,
+ 0x47, 0xc9, 0xef, 0x61, 0x6c, 0x12, 0x1d, 0xb5, 0x03, 0x94, 0x5e, 0x6f,
+ 0xcc, 0x27, 0x73, 0xc0, 0x0b, 0xe5, 0x40, 0xf8, 0x06, 0xf1, 0x21, 0xba,
+ 0x31, 0x9c, 0xab, 0xd8, 0x43, 0x60, 0x27, 0x34, 0x57, 0xc4, 0x55, 0x7c,
+ 0xaf, 0x04, 0x18, 0xf4, 0xb5, 0xf7, 0x93, 0x43, 0xc7, 0x0a, 0x17, 0x8c,
+ 0x95, 0xe2, 0xec, 0x74, 0xc4, 0x93, 0x39, 0x64, 0xe9, 0x21, 0xfa, 0xcf,
+ 0x82, 0xc9, 0xcf, 0xe4, 0x2c, 0x56, 0x76, 0xe5, 0x02, 0x87, 0xf5, 0x79,
+ 0x95, 0xb6, 0x4d, 0xbb, 0xa9, 0xd4, 0x66, 0x94, 0x36, 0x05, 0x55, 0x59,
+ 0xb5, 0x0d, 0xf5, 0x20, 0xa5, 0xfc, 0x68, 0x5c, 0xb7, 0x3d, 0xf1, 0xf1,
+ 0x0f, 0x7f, 0x7f, 0x8d, 0x8b, 0xec, 0x34, 0xfa, 0xd8, 0x7e, 0xb9, 0x9a,
+ 0x91, 0xed, 0xe7, 0x9d, 0x6c, 0x62, 0xd3, 0xfd, 0x38, 0x6c, 0x14, 0x58,
+ 0xf4, 0x03, 0x01, 0xd5, 0x0a, 0x02, 0x19, 0x9d, 0x85, 0x69, 0x40, 0xa0,
+ 0x33, 0x24, 0x94, 0xde, 0x96, 0xc9, 0xfa, 0x0c, 0xc8, 0x28, 0xd7, 0x85,
+ 0xfa, 0x64, 0x97, 0x12, 0x2b, 0x95, 0x6a, 0x2f, 0x63, 0xc3, 0x50, 0x40,
+ 0x2f, 0x43, 0x61, 0x26, 0x82, 0x21, 0x5e, 0x1b, 0x53, 0x55, 0x9a, 0x15,
+ 0x8d, 0xad, 0x66, 0x7b, 0x99, 0x9b, 0x8a, 0x86, 0x75, 0xc6, 0xa8, 0xad,
+ 0x6c, 0x13, 0xfc, 0x8c, 0x86, 0xf3, 0x7a, 0x09, 0x93, 0xc4, 0x4a, 0x57,
+ 0x9f, 0x92, 0x0a, 0x1b, 0x0d, 0xd2, 0xde, 0x5d, 0xa1, 0x14, 0xc8, 0x01,
+ 0x86, 0x4f, 0x3e, 0xa3, 0xcf, 0x8b, 0xaf, 0x04, 0xee, 0x71, 0xa5, 0x0e,
+ 0xc5, 0x3d, 0xc7, 0xfd, 0xca, 0x5a, 0x27, 0xd1, 0x3a, 0x3e, 0xb5, 0x13,
+ 0x1c, 0xf5, 0x20, 0x91, 0xbe, 0x5f, 0x62, 0x04, 0xb3, 0x14, 0xb4, 0x03,
+ 0x82, 0x41, 0xab, 0x20, 0x11, 0xfc, 0xc9, 0xd1, 0x08, 0x33, 0xe1, 0x83,
+ 0xe8, 0xf7, 0x31, 0x7f, 0x89, 0x61, 0x13, 0xae, 0xce, 0x0e, 0x3e, 0xec,
+ 0x97, 0xe4, 0x76, 0xc3, 0x37, 0x8d, 0xac, 0xca, 0xfe, 0xd5, 0x27, 0xa0,
+ 0xbf, 0x9a, 0x31, 0x20, 0x09, 0x6f, 0xcf, 0x81, 0x69, 0xf9, 0x85, 0xb0,
+ 0x88, 0x80, 0x45, 0xed, 0x65, 0x1d, 0xc7, 0xc8, 0xd9, 0x87, 0x3a, 0x3b,
+ 0x58, 0x79, 0xbc, 0x8d, 0x23, 0xbe, 0xdc, 0x4c, 0x31, 0xed, 0x20, 0x8d,
+ 0xf7, 0x64, 0x7e, 0x8a, 0xbd, 0x0e, 0xdd, 0x19, 0x11, 0x64, 0xa9, 0x91,
+ 0x60, 0x92, 0xda, 0xb5, 0xfc, 0x3f, 0x32, 0x32, 0x23, 0xa6, 0x93, 0xd8,
+ 0x34, 0xd7, 0x56, 0xd6, 0xea, 0x1a, 0x00, 0x0b, 0xf3, 0x52, 0xbf, 0x99,
+ 0x0a, 0x88, 0x60, 0x9d, 0x3d, 0x42, 0x9d, 0xd5, 0xde, 0x5b, 0x17, 0x24,
+ 0x3b, 0xff, 0x26, 0xc2, 0x85, 0x4f, 0x90, 0x33, 0x4a, 0x80, 0xee, 0x2d,
+ 0xff, 0xf0, 0xb6, 0x7c, 0x24, 0x48, 0x74, 0x6d, 0x48, 0x6a, 0x27, 0x46,
+ 0x87, 0x9d, 0x56, 0x69, 0xa1, 0xf4, 0x7e, 0x77, 0xb0, 0x96, 0x5f, 0x52,
+ 0x95, 0x1f, 0xe8, 0x71, 0x53, 0x65, 0xe8, 0x2d, 0x45, 0x7d, 0x52, 0x95,
+ 0xa8, 0x4b, 0xc8, 0x94, 0x6c, 0x76, 0x7d, 0x72, 0xba, 0x7f, 0xc3, 0x16,
+ 0xe6, 0x68, 0xc3, 0xe9, 0xea, 0xac, 0xf6, 0x17, 0xbc, 0xd9, 0x75, 0x9c,
+ 0x39, 0x4c, 0x5f, 0x91, 0x86, 0xe5, 0x60, 0x93, 0x9f, 0xbd, 0xad, 0x97,
+ 0x64, 0x60, 0x16, 0x61, 0x03, 0xab, 0xf9, 0x93, 0x7b, 0xf0, 0x24, 0xbd,
+ 0x52, 0x8f, 0xab, 0xd9, 0x7d, 0x12, 0x11, 0x1b, 0xb1, 0x13, 0x51, 0x18,
+ 0x1a, 0x92, 0xff, 0x48, 0xa0, 0xaa, 0x46, 0xbf, 0xc2, 0xef, 0xf1, 0x8a,
+ 0xab, 0x61, 0x9d, 0xe1, 0xaf, 0x05, 0x68, 0xee, 0xec, 0xab, 0x5d, 0x7f,
+ 0x19, 0x50, 0x73, 0xeb, 0x54, 0x8a, 0x68, 0xf9, 0xe3, 0x50, 0xa9, 0x26,
+ 0xc0, 0xe3, 0x11, 0xe8, 0x2a, 0x09, 0xca, 0x5f, 0x07, 0xda, 0xcb, 0xca,
+ 0x3c, 0x56, 0xb0, 0xb2, 0xb3, 0x76, 0xef, 0x5a, 0x2d, 0xdd, 0xaf, 0x1d,
+ 0x1a, 0x9f, 0xd1, 0xdc, 0x80, 0x77, 0x15, 0x51, 0xa5, 0x07, 0x85, 0x75,
+ 0x08, 0xe8, 0xac, 0x79, 0xc0, 0xaa, 0x2d, 0x1c, 0x5b, 0x0f, 0x92, 0x27,
+ 0xf1, 0x1f, 0xed, 0xcb, 0xe9, 0xf2, 0xfa, 0x1c, 0xa4, 0xb2, 0x5d, 0xc4,
+ 0x4f, 0x74, 0xe8, 0x6b, 0x19, 0xcf, 0xed, 0x4b, 0xfc, 0xf4, 0x5f, 0xb4,
+ 0x82, 0x68, 0x9b, 0xbf, 0xa4, 0x3e, 0x26, 0x25, 0x51, 0x0d, 0x0d, 0xbc,
+ 0xfd, 0x05, 0x79, 0xe6, 0x40, 0x40, 0x17, 0x2a, 0x86, 0x85, 0x98, 0x27,
+ 0x8d, 0xae, 0xbb, 0xf1, 0x0c, 0x87, 0x9d, 0x33, 0x40, 0xfb, 0xff, 0x83,
+ 0x7a, 0x75, 0x9f, 0x2d, 0x36, 0x6d, 0x23, 0x53, 0xbc, 0x6c, 0xbb, 0x89,
+ 0x3a, 0x37, 0x26, 0x3d, 0xf5, 0xa7, 0xaf, 0xcb, 0x19, 0x8f, 0x16, 0x98,
+ 0xde, 0x1e, 0x78, 0x6d, 0x88, 0x82, 0x01, 0x2c, 0x71, 0xba, 0x50, 0xad,
+ 0x44, 0x1e, 0x35, 0x43, 0x8c, 0x97, 0x95, 0xd6, 0x0d, 0x6b, 0x09, 0xb0,
+ 0xa2, 0xdd, 0xae, 0x16, 0xb0, 0x5c, 0xe7, 0xba, 0x78, 0xe7, 0xf9, 0x2c,
+ 0x6e, 0x07, 0x4a, 0xbf, 0xbc, 0xe8, 0xcf, 0x5d, 0xc1, 0x20, 0x92, 0xd7,
+ 0x5c, 0x0d, 0x30, 0x04, 0x40, 0x4e, 0xd0, 0x41, 0x39, 0x4b, 0x60, 0x65,
+ 0x67, 0x71, 0x4e, 0x04, 0x94, 0x18, 0x99, 0xf3, 0x06, 0x72, 0xb6, 0xb8,
+ 0x6f, 0xa0, 0x00, 0x00, 0x01, 0x05, 0x7f, 0xc9, 0x47, 0x3d, 0x5b, 0xf8,
+ 0x17, 0x4c, 0x12, 0x41, 0x5e, 0x5d, 0x1a, 0xec, 0x4c, 0x8d, 0xbd, 0x17,
+ 0x62, 0xd2, 0xc0, 0xdf, 0xf0, 0x9c, 0xfe, 0x15, 0x66, 0xdc, 0xee, 0x5f,
+ 0x85, 0x1e, 0xf2, 0x4f, 0x19, 0xc3, 0x4e, 0xcf, 0x7d, 0xed, 0x3d, 0xe1,
+ 0xe4, 0x24, 0x55, 0xc3, 0xd3, 0xa7, 0x4e, 0xc8, 0x60, 0x91, 0x51, 0x10,
+ 0x1e, 0x20, 0x8d, 0x16, 0xc0, 0x4f, 0x0b, 0x7d, 0x99, 0xeb, 0x79, 0x20,
+ 0x9d, 0xf8, 0x54, 0xd9, 0x81, 0xbe, 0x6e, 0x78, 0xfc, 0xa9, 0x62, 0xcc,
+ 0x92, 0xc2, 0x69, 0x11, 0x3f, 0x7a, 0xfd, 0x68, 0x01, 0x71, 0x59, 0xa9,
+ 0xf3, 0x19, 0x81, 0x7d, 0x57, 0x4b, 0x9a, 0x2b, 0x52, 0x5e, 0x72, 0x04,
+ 0x46, 0x50, 0x42, 0x4f, 0x0e, 0x9f, 0xfc, 0x45, 0x77, 0x30, 0xa3, 0x6a,
+ 0x0b, 0x0b, 0xc3, 0xec, 0x81, 0x65, 0xa2, 0xfe, 0x24, 0x2f, 0x6a, 0x6f,
+ 0xc6, 0xbb, 0x28, 0x4b, 0x01, 0x16, 0x71, 0x9a, 0x1c, 0x45, 0x4b, 0x50,
+ 0x97, 0x45, 0x37, 0x76, 0x50, 0x7d, 0x6b, 0x82, 0x70, 0xbe, 0x77, 0x7a,
+ 0x71, 0x10, 0x6a, 0xfc, 0x4c, 0xf4, 0x7f, 0xfe, 0x97, 0x8b, 0x8c, 0x7e,
+ 0x6e, 0x58, 0x07, 0x25, 0xe1, 0xae, 0x9e, 0x8b, 0x18, 0x91, 0x16, 0x27,
+ 0x0c, 0x90, 0x55, 0xeb, 0x12, 0x5b, 0x01, 0x27, 0x07, 0xdb, 0xf0, 0xf4,
+ 0x3b, 0x8b, 0x9b, 0xce, 0x96, 0x2d, 0x08, 0xd6, 0x69, 0xcc, 0x1d, 0xaa,
+ 0x88, 0xc7, 0x07, 0x56, 0x6f, 0x6f, 0xd5, 0xa9, 0xa0, 0x82, 0x51, 0x53,
+ 0x28, 0x10, 0xb5, 0x61, 0x85, 0xd2, 0x4c, 0x99, 0x9f, 0xe5, 0xfa, 0xe8,
+ 0x1e, 0x2a, 0x06, 0xa9, 0x88, 0xfe, 0x5d, 0xe0, 0x0f, 0xce, 0xe1, 0xd5,
+ 0x37, 0x16, 0x30, 0xd8, 0x3b, 0x86, 0x25, 0x0a, 0x3c, 0xc0, 0x94, 0x83,
+ 0xab, 0xf9, 0x06, 0x5c, 0x29, 0xcc, 0x59, 0x33, 0x05, 0x8e, 0x59, 0x53,
+ 0x53, 0x8d, 0x26, 0x00, 0x00, 0x00, 0x84, 0x7f, 0x79, 0xae, 0x34, 0xd1,
+ 0x76, 0x9f, 0xa0, 0x36, 0x80, 0xf7, 0xb2, 0x30, 0x51, 0x02, 0xf2, 0x0f,
+ 0xc6, 0x05, 0xad, 0x85, 0x6e, 0x4a, 0x43, 0x53, 0xcf, 0xa8, 0x89, 0xa2,
+ 0x90, 0xd4, 0x64, 0x11, 0xfa, 0xe6, 0x0b, 0x08, 0x3a, 0x6c, 0x9c, 0xfc,
+ 0xf7, 0xe8, 0xc2, 0x53, 0xa1, 0x5b, 0x21, 0x51, 0x22, 0x3e, 0xd7, 0x7b,
+ 0xcf, 0x9f, 0x23, 0x2f, 0x59, 0xa6, 0xdb, 0x99, 0x8a, 0x9b, 0x66, 0xb7,
+ 0x1e, 0x86, 0xaf, 0x91, 0xd8, 0x06, 0x1c, 0xd8, 0xe8, 0x20, 0x91, 0x35,
+ 0xcd, 0x07, 0xf5, 0x94, 0x53, 0x20, 0xfe, 0x3c, 0xfd, 0x52, 0x30, 0x4f,
+ 0x77, 0xf6, 0x14, 0x1d, 0x7b, 0x58, 0xee, 0x7a, 0x45, 0xfa, 0xb9, 0xf8,
+ 0xab, 0xbf, 0x81, 0x5e, 0xaa, 0xeb, 0x01, 0x78, 0x59, 0xf6, 0xd3, 0x57,
+ 0x19, 0x8e, 0x3c, 0x68, 0x03, 0xf8, 0x86, 0xd9, 0x8b, 0x0d, 0xbd, 0x59,
+ 0xa5, 0x52, 0x62, 0xc2, 0x86, 0xc8, 0x00, 0x7e, 0x96, 0xfb, 0x17, 0x7e,
+ 0xaa, 0x21, 0xc0, 0x42, 0x7d, 0x2d, 0xb3, 0xd1, 0xc4, 0x10, 0xa2, 0x01,
+ 0x8b, 0xd5, 0xa2, 0x26, 0x38, 0x4d, 0xcc, 0xb6, 0x5c, 0xc4, 0x05, 0xbb,
+ 0xab, 0xf9, 0x36, 0x56, 0x79, 0xad, 0x0f, 0xa1, 0xcd, 0x95, 0xfe, 0x79,
+ 0xf4, 0x35, 0x27, 0x66, 0x06, 0x7b, 0x6b, 0xa1, 0xae, 0x75, 0x8e, 0xf4,
+ 0xd7, 0x38, 0x68, 0x20, 0x68, 0x20, 0x7e, 0xcf, 0x8a, 0x96, 0x34, 0x32,
+ 0xac, 0x3d, 0xc7, 0xd0, 0xdc, 0xa1, 0xa7, 0x12, 0x88, 0x93, 0xbc, 0xae,
+ 0xa1, 0x2e, 0xcf, 0x0b, 0xf6, 0x4f, 0x7f, 0x93, 0x2a, 0x62, 0x7c, 0x55,
+ 0xd0, 0xb2, 0x80, 0x98, 0xb0, 0x96, 0xf7, 0x09, 0x15, 0x5d, 0x8b, 0x4c,
+ 0x74, 0x6e, 0xd6, 0x9a, 0x9d, 0x24, 0x32, 0x17, 0x46, 0xc8, 0x9f, 0xaa,
+ 0x85, 0x57, 0x0e, 0xb7, 0xee, 0x54, 0x46, 0x6b, 0x8e, 0xf5, 0xf5, 0xe5,
+ 0xae, 0xe6, 0xad, 0x3b, 0x7c, 0xbb, 0x42, 0x50, 0x73, 0x8a, 0x85, 0xc2,
+ 0xdb, 0xc4, 0x94, 0x34, 0x86, 0x80, 0xc3, 0x72, 0x58, 0x56, 0xb3, 0xe8,
+ 0x11, 0xc5, 0xbb, 0x24, 0x5b, 0xd9, 0x71, 0x0e, 0xe1, 0x4d, 0xf2, 0xbf,
+ 0x31, 0xe1, 0x03, 0x20, 0x64, 0xe7, 0x9f, 0x16, 0xda, 0xd3, 0x42, 0xfb,
+ 0x60, 0x92, 0xfe, 0xe2, 0x71, 0xdc, 0x17, 0xa2, 0x96, 0x2e, 0xeb, 0x2e,
+ 0x73, 0xfe, 0x33, 0x30, 0xc4, 0xcc, 0x0f, 0x6d, 0x70, 0xab, 0x8a, 0x08,
+ 0x82, 0x33, 0x9f, 0x54, 0x6f, 0x4a, 0xa4, 0x7c, 0x8b, 0xfd, 0xd7, 0x05,
+ 0xd5, 0x10, 0x44, 0x58, 0xe7, 0xb6, 0xb6, 0xdb, 0x6f, 0xe8, 0x07, 0x6d,
+ 0xfe, 0x0b, 0xd2, 0xd9, 0x43, 0x6c, 0x2a, 0x87, 0xc8, 0xaf, 0x57, 0x3b,
+ 0xde, 0x36, 0xa2, 0xdb, 0xc0, 0x1b, 0x72, 0xe8, 0x56, 0x69, 0x51, 0xbb,
+ 0x46, 0xc6, 0x05, 0x3e, 0xa2, 0xb3, 0xef, 0xbe, 0xa3, 0x0c, 0x09, 0x46,
+ 0x16, 0xbb, 0xb5, 0x51, 0x47, 0x31, 0x47, 0x79, 0xa6, 0x52, 0xc9, 0x4d,
+ 0x07, 0x5a, 0x57, 0x32, 0xac, 0xc8, 0x2f, 0xaf, 0x5f, 0xf4, 0x67, 0x40,
+ 0x20, 0xb6, 0x92, 0xe1, 0x42, 0x5a, 0x19, 0x8a, 0x14, 0x39, 0x7a, 0x11,
+ 0xbb, 0x19, 0xd3, 0xd6, 0x37, 0xec, 0x6a, 0xae, 0x87, 0x5a, 0xd1, 0x46,
+ 0x86, 0x66, 0x12, 0x70, 0x70, 0x0a, 0x69, 0x33, 0xeb, 0x41, 0xcf, 0x4c,
+ 0x7c, 0x8c, 0xa1, 0x04, 0xfd, 0x46, 0x18, 0x18, 0x89, 0xb2, 0x97, 0x17,
+ 0x22, 0x34, 0x4d, 0xba, 0x82, 0xd3, 0xab, 0x06, 0x67, 0x83, 0x78, 0xbf,
+ 0x52, 0x33, 0xea, 0x41, 0xb7, 0x78, 0x6a, 0x55, 0x06, 0x85, 0xc5, 0x71,
+ 0x68, 0x8b, 0x7f, 0xfe, 0x9d, 0x8e, 0x96, 0x56, 0x93, 0x2f, 0xe9, 0x07,
+ 0xfe, 0x99, 0xa6, 0xec, 0x6e, 0xdd, 0xbf, 0x0a, 0x1d, 0x4d, 0x91, 0xdb,
+ 0xf8, 0xbe, 0x89, 0xce, 0x31, 0x28, 0xc2, 0xca, 0xae, 0xa1, 0xac, 0x74,
+ 0x18, 0xae, 0xe4, 0x22, 0x38, 0x5c, 0x23, 0x9f, 0xa9, 0x4e, 0xd9, 0x8e,
+ 0xff, 0x58, 0xa0, 0xb0, 0xf0, 0xb7, 0x7f, 0x2e, 0x8f, 0x5c, 0xb3, 0xb0,
+ 0xfe, 0x7e, 0x64, 0xb4, 0x73, 0x50, 0xb3, 0x6f, 0x8d, 0x69, 0xe6, 0xb9,
+ 0xe3, 0x7f, 0xe8, 0xea, 0x1c, 0x0f, 0xc2, 0xb3, 0x94, 0x2d, 0x7c, 0x7b,
+ 0x5a, 0x72, 0x37, 0x0e, 0x63, 0x17, 0xa5, 0x73, 0x16, 0x42, 0xc3, 0x87,
+ 0x23, 0x9a, 0x4e, 0x4e, 0x33, 0x9c, 0x46, 0x43, 0x54, 0x73, 0x41, 0x44,
+ 0x7c, 0x9e, 0x1a, 0x6b, 0xdb, 0x36, 0x74, 0x05, 0x51, 0x08, 0x9b, 0x88,
+ 0x71, 0x52, 0x20, 0x00, 0xac, 0xd3, 0x54, 0x94, 0xf8, 0x7a, 0x25, 0xe1,
+ 0x88, 0x77, 0x11, 0x75, 0x63, 0xc3, 0x2c, 0xdf, 0xa3, 0x2f, 0xd3, 0x0d,
+ 0x0a, 0xb1, 0x4b, 0x11, 0xc4, 0x5e, 0xb9, 0xbe, 0xfa, 0xa4, 0x28, 0xfd,
+ 0x93, 0x9e, 0x21, 0x0a, 0x3e, 0xf2, 0xad, 0x49, 0xc7, 0xd5, 0x14, 0x35,
+ 0x92, 0x58, 0x3e, 0x02, 0x38, 0xeb, 0x4d, 0x12, 0x9d, 0xe9, 0x7b, 0xf8,
+ 0x69, 0x5c, 0xc3, 0x77, 0x71, 0xf6, 0x5a, 0x50, 0xa7, 0x78, 0xbd, 0xe1,
+ 0x5b, 0xa9, 0x6d, 0x8f, 0xb2, 0xe2, 0xdd, 0x0b, 0x10, 0xa8, 0x72, 0xcf,
+ 0x5d, 0xe5, 0xf9, 0xfe, 0xf1, 0x95, 0x00, 0x97, 0xb5, 0xf5, 0x2a, 0x0c,
+ 0x41, 0x49, 0xf2, 0xb9, 0xba, 0x3b, 0xa2, 0x0d, 0xaa, 0x59, 0xd6, 0xf2,
+ 0x23, 0xfd, 0xe6, 0xac, 0x33, 0xaa, 0x7e, 0x11, 0x6e, 0xd8, 0x93, 0x5b,
+ 0x99, 0xd1, 0x73, 0x13, 0xe0, 0x4a, 0x53, 0xdf, 0x9c, 0x15, 0x80, 0xda,
+ 0x4e, 0xaf, 0x00, 0xc6, 0xc2, 0x1a, 0xac, 0xc1, 0x7e, 0xc8, 0xaf, 0x92,
+ 0x36, 0xb3, 0xd2, 0x61, 0xb0, 0xcd, 0x55, 0xf6, 0x7d, 0x28, 0xf2, 0x01,
+ 0xc1, 0x5b, 0x5b, 0xf1, 0x4c, 0xb8, 0xcf, 0x72, 0xf6, 0xae, 0x4b, 0x66,
+ 0x74, 0xc3, 0xaf, 0x77, 0x3c, 0xd8, 0x0f, 0xe4, 0x38, 0xcc, 0xa5, 0x06,
+ 0xfc, 0x5e, 0x15, 0xf8, 0x74, 0xb1, 0xd1, 0x37, 0x6f, 0xfb, 0x97, 0x2e,
+ 0x18, 0x64, 0x20, 0x17, 0x8d, 0x5e, 0xf7, 0xf6, 0x83, 0xfd, 0x5b, 0x80,
+ 0x82, 0x7a, 0xc9, 0x12, 0x37, 0x51, 0x19, 0x8d, 0x0f, 0x09, 0x12, 0x8e,
+ 0x76, 0x95, 0x54, 0xd8, 0x92, 0x21, 0xc5, 0xcc, 0x53, 0x46, 0xae, 0xd7,
+ 0x7e, 0xe4, 0xb7, 0x9b, 0x70, 0xa8, 0xf9, 0x4a, 0xa8, 0x07, 0x5c, 0x79,
+ 0x7b, 0xdf, 0x82, 0x6c, 0x14, 0xc3, 0x4a, 0x9b, 0x93, 0xed, 0x12, 0xbf,
+ 0x36, 0x86, 0x3a, 0x9f, 0xe5, 0xe3, 0x6d, 0xc7, 0xb0, 0x1c, 0x1c, 0x2a,
+ 0x0f, 0xd1, 0x34, 0xf4, 0xca, 0x6b, 0xf0, 0x92, 0xb8, 0xf9, 0x9a, 0x07,
+ 0x1c, 0x4b, 0xc8, 0x21, 0xed, 0x27, 0x96, 0xf7, 0x00, 0x2c, 0xb5, 0xa0,
+ 0x05, 0xf5, 0xf8, 0xab, 0xb1, 0x47, 0x6b, 0x9a, 0xf5, 0xed, 0xdb, 0x8e,
+ 0xba, 0x9b, 0xb7, 0xf6, 0xe2, 0x99, 0x8f, 0xc0, 0x29, 0x63, 0x31, 0x19,
+ 0xcb, 0x16, 0x2d, 0x7f, 0x12, 0x6f, 0x90, 0x24, 0x31, 0xbc, 0x89, 0xa4,
+ 0x39, 0xb1, 0x07, 0xab, 0x0a, 0x7f, 0x92, 0xaa, 0xc0, 0x00, 0xa0, 0xb3,
+ 0x73, 0x28, 0x3e, 0x14, 0xdc, 0xc5, 0x8a, 0xbe, 0x00, 0xc1, 0xc1, 0xc6,
+ 0x2b, 0x6f, 0x69, 0x36, 0x86, 0x05, 0xc2, 0x4b, 0xf7, 0xbe, 0x9c, 0x67,
+ 0x35, 0x06, 0x71, 0x2f, 0xcb, 0xc1, 0x67, 0xac, 0x64, 0xee, 0x57, 0xa6,
+ 0x6d, 0x68, 0xb2, 0xa4, 0xfb, 0xf1, 0x4d, 0x21, 0xcf, 0x74, 0xe9, 0x9d,
+ 0x25, 0x99, 0x57, 0x70, 0xc5, 0x41, 0xbf, 0x4c, 0xc3, 0xbc, 0x12, 0x51,
+ 0x7c, 0x91, 0x61, 0xa5, 0x46, 0x67, 0xc9, 0x24, 0xd1, 0x89, 0x04, 0x02,
+ 0xeb, 0xca, 0x12, 0xd4, 0xb7, 0xa8, 0x3e, 0xa4, 0xac, 0x1a, 0x6f, 0x07,
+ 0x91, 0x38, 0x05, 0x69, 0xba, 0x44, 0x97, 0x90, 0xb8, 0xfb, 0x4f, 0xc8,
+ 0x25, 0x2b, 0x00, 0x61, 0x05, 0xc7, 0x23, 0xb6, 0x96, 0x66, 0xb9, 0x06,
+ 0x22, 0x89, 0x52, 0xe5, 0x22, 0x4d, 0xa0, 0xc4, 0x98, 0x4d, 0x9c, 0xbd,
+ 0x29, 0xcc, 0xbf, 0x4b, 0xf5, 0x8c, 0x56, 0xfc, 0xae, 0x86, 0x7c, 0x48,
+ 0xb9, 0x72, 0x14, 0xca, 0x55, 0xac, 0x50, 0x5b, 0x62, 0x79, 0x15, 0xe9,
+ 0x6d, 0x52, 0xad, 0xca, 0x17, 0x18, 0xf0, 0x3e, 0xa8, 0xd9, 0x5d, 0x0b,
+ 0xbb, 0xa7, 0x71, 0xc9, 0x33, 0xf8, 0xa8, 0x36, 0x65, 0xc3, 0x6a, 0xad,
+ 0xb1, 0xea, 0x02, 0x45, 0x95, 0xcd, 0x7a, 0xe7, 0xef, 0xad, 0xfd, 0xc0,
+ 0x8c, 0xc1, 0x68, 0x83, 0x5e, 0x85, 0x20, 0x74, 0xec, 0x0f, 0xd2, 0x3f,
+ 0x1d, 0x2b, 0xac, 0x80, 0xe7, 0x8a, 0x3d, 0x6e, 0xab, 0xd4, 0xbe, 0xe6,
+ 0x07, 0xcc, 0x93, 0x5c, 0x36, 0xf6, 0x50, 0xf7, 0xff, 0x3b, 0x5d, 0x29,
+ 0xe3, 0x6e, 0xa0, 0x05, 0xda, 0x45, 0xa5, 0xba, 0x8a, 0x7e, 0xc3, 0xc0,
+ 0x11, 0xe4, 0xfa, 0xf2, 0xd8, 0x84, 0x5e, 0x7f, 0xb4, 0x13, 0x1e, 0xcb,
+ 0x54, 0x2e, 0x4d, 0x40, 0x63, 0x4d, 0xe7, 0x68, 0x12, 0xc0, 0x75, 0xb0,
+ 0x10, 0x04, 0xd9, 0x06, 0x41, 0x22, 0x2f, 0x35, 0x08, 0x12, 0xb4, 0x58,
+ 0x4b, 0x91, 0x0f, 0x8c, 0xa8, 0x6c, 0xa4, 0x7e, 0xea, 0x85, 0x8c, 0xac,
+ 0x9b, 0xcf, 0x93, 0xcf, 0x48, 0xf7, 0xfe, 0x62, 0x01, 0x84, 0x34, 0x18,
+ 0x25, 0xf1, 0xbe, 0x0c, 0x80, 0xa3, 0x4f, 0x71, 0x81, 0x00, 0x64, 0x00,
+ 0x86, 0x00, 0x40, 0x96, 0xf0, 0x01, 0x17, 0x86, 0x00, 0x37, 0x77, 0x6f,
+ 0x3d, 0x1a, 0xea, 0x85, 0x30, 0x98, 0x31, 0x5e, 0x78, 0xf0, 0xb6, 0xae,
+ 0xcb, 0x2c, 0x84, 0xbe, 0x35, 0xc9, 0x41, 0xcd, 0x98, 0xd1, 0x18, 0x0f,
+ 0x0d, 0xd0, 0x7f, 0x93, 0xd1, 0xfb, 0xbb, 0xde, 0x1f, 0x05, 0xd1, 0x3d,
+ 0x1f, 0xc4, 0xfd, 0x5f, 0x2f, 0xf2, 0xad, 0x68, 0xfc, 0xba, 0x3f, 0xfd,
+ 0x8e, 0x75, 0x6f, 0x64, 0x00, 0x00, 0x00, 0x05, 0x7a, 0x7f, 0x70, 0xd9,
+ 0xcd, 0x8a, 0x9c, 0x65, 0xac, 0xbc, 0x05, 0xc7, 0xac, 0x85, 0x8d, 0x79,
+ 0x14, 0xe6, 0x67, 0x0c, 0xb0, 0xf0, 0xd9, 0xc6, 0xeb, 0xa5, 0x01, 0x43,
+ 0x7f, 0x0d, 0xee, 0xae, 0xd8, 0xfc, 0xad, 0x34, 0x08, 0xb2, 0x0e, 0x95,
+ 0x6f, 0x1b, 0x91, 0x24, 0x11, 0xff, 0x37, 0x10, 0xe1, 0xaf, 0x64, 0xd5,
+ 0x10, 0xa8, 0x7a, 0xcb, 0x24, 0xae, 0x2d, 0x26, 0x98, 0x4a, 0x4e, 0x92,
+ 0x54, 0x85, 0x59, 0xf2, 0xa3, 0x2d, 0x64, 0xc3, 0x39, 0xde, 0x0e, 0x75,
+ 0xb5, 0x2b, 0xf5, 0x83, 0x49, 0x73, 0x95, 0xff, 0x05, 0xf6, 0x87, 0xa3,
+ 0x5f, 0xd7, 0x23, 0x55, 0xf8, 0x97, 0xef, 0x68, 0x33, 0xf2, 0xee, 0x03,
+ 0x70, 0xb3, 0x88, 0xef, 0x94, 0x01, 0x27, 0x06, 0xfc, 0xbf, 0xaf, 0x5d,
+ 0x44, 0xe2, 0x35, 0xe5, 0x24, 0x45, 0xc1, 0x49, 0x71, 0xf0, 0xbc, 0x47,
+ 0x81, 0xcd, 0x3f, 0x67, 0x67, 0x38, 0x3b, 0xf3, 0x3b, 0xbe, 0xcb, 0x2f,
+ 0x50, 0xc8, 0xc3, 0x32, 0x75, 0x2a, 0x4f, 0x90, 0x9f, 0xf5, 0x2b, 0x38,
+ 0xab, 0xca, 0x70, 0xf2, 0x5d, 0x93, 0xab, 0xea, 0xe5, 0xa3, 0x55, 0x86,
+ 0x2b, 0x69, 0xf5, 0x24, 0xd2, 0xb2, 0x48, 0x03, 0xbb, 0x02, 0xb1, 0xad,
+ 0xcb, 0xa1, 0xe5, 0xac, 0x80, 0x05, 0x88, 0xb9, 0xd1, 0xaa, 0x4f, 0x97,
+ 0xa7, 0x0e, 0x43, 0x6b, 0xf4, 0x92, 0x5a, 0xcb, 0xf9, 0xf9, 0xe6, 0x59,
+ 0xf2, 0x7c, 0x9a, 0xd0, 0x8e, 0x79, 0x79, 0x60, 0xdd, 0xf3, 0x0a, 0xf3,
+ 0x1e, 0x77, 0x9b, 0xcf, 0x1d, 0xca, 0x0c, 0x4a, 0x73, 0xf3, 0x97, 0xb7,
+ 0x35, 0xef, 0x99, 0x89, 0x72, 0x9a, 0x18, 0x28, 0x47, 0x84, 0xf6, 0x89,
+ 0x73, 0x85, 0x78, 0x80, 0x1f, 0x91, 0xfb, 0x9b, 0x61, 0x68, 0xec, 0x17,
+ 0x54, 0x99, 0xb6, 0xeb, 0xfc, 0xb6, 0xde, 0x3f, 0x6f, 0x2d, 0x4a, 0x63,
+ 0x74, 0xbe, 0x45, 0xe2, 0x54, 0xbe, 0xeb, 0x6a, 0x62, 0x4a, 0x6e, 0x83,
+ 0x09, 0x30, 0x74, 0x1a, 0x60, 0x8a, 0x4b, 0xfa, 0xf5, 0x1b, 0x67, 0x38,
+ 0xf1, 0x66, 0x75, 0x9f, 0x61, 0xde, 0xe1, 0x50, 0x98, 0x62, 0x0a, 0xc1,
+ 0x69, 0x6b, 0x54, 0x38, 0x62, 0x96, 0x48, 0x67, 0xd5, 0x64, 0x28, 0x2d,
+ 0x42, 0x17, 0xe7, 0xc4, 0x36, 0x21, 0xce, 0x67, 0x90, 0x5d, 0x9e, 0x67,
+ 0x2d, 0x4c, 0x8f, 0x9d, 0xf9, 0x29, 0x52, 0x1f, 0xa3, 0x20, 0xde, 0x52,
+ 0x4d, 0xc2, 0x03, 0x83, 0x20, 0xfc, 0x59, 0x48, 0x9b, 0x15, 0xfd, 0x12,
+ 0x4c, 0xb3, 0x2f, 0x47, 0x0f, 0x2e, 0xc8, 0xda, 0xc4, 0x9a, 0xba, 0x1f,
+ 0x0c, 0xcc, 0x6f, 0x0d, 0x44, 0x21, 0x8f, 0x32, 0x23, 0x80, 0x35, 0xbb,
+ 0x4d, 0x8b, 0x3f, 0xab, 0x75, 0xa7, 0x55, 0x74, 0x05, 0x54, 0xdb, 0xc8,
+ 0x91, 0x06, 0xf5, 0xdc, 0xbd, 0x0d, 0xcc, 0x89, 0x06, 0xbc, 0xae, 0x40,
+ 0xbf, 0x6d, 0x45, 0xb2, 0x36, 0x6b, 0x88, 0xb7, 0xa4, 0xd9, 0xcd, 0x7f,
+ 0xfb, 0xd5, 0xbc, 0x4d, 0x67, 0xdc, 0xe4, 0x12, 0x46, 0x54, 0x7c, 0x53,
+ 0xd2, 0x40, 0xb2, 0x35, 0xe5, 0x2b, 0x35, 0xcf, 0xc1, 0xf9, 0xe0, 0x06,
+ 0x9f, 0x55, 0xd1, 0x2a, 0x5c, 0x01, 0x6e, 0xab, 0xfb, 0xeb, 0xbb, 0x9c,
+ 0xb0, 0x40, 0x1e, 0x24, 0x85, 0x5d, 0x95, 0xb3, 0xf5, 0x0c, 0xf2, 0xd6,
+ 0x07, 0x23, 0xb4, 0x5b, 0xe3, 0xc2, 0x93, 0x9c, 0xa2, 0xf9, 0x6d, 0xe2,
+ 0x0a, 0x1d, 0xeb, 0xdc, 0xb7, 0xbc, 0x04, 0xf5, 0xa9, 0xa9, 0xea, 0xf3,
+ 0x82, 0x53, 0x92, 0x79, 0x23, 0xcc, 0xc6, 0xab, 0x3e, 0x56, 0xfc, 0xad,
+ 0xf7, 0x4f, 0x10, 0xef, 0x62, 0xf2, 0x87, 0x38, 0x9a, 0x8d, 0x54, 0xb2,
+ 0x62, 0x55, 0xb2, 0x05, 0x0a, 0x0e, 0xc0, 0x15, 0x71, 0xda, 0x8b, 0x27,
+ 0x09, 0x0f, 0x2b, 0x5f, 0x3f, 0xdc, 0x08, 0x72, 0x94, 0x79, 0x98, 0x5e,
+ 0x5e, 0xb0, 0xef, 0xe7, 0x7b, 0x25, 0x1a, 0xc2, 0x1d, 0x1a, 0x54, 0x85,
+ 0x60, 0x05, 0xf5, 0x59, 0x01, 0xfd, 0x38, 0x10, 0x2b, 0x4d, 0x84, 0x8d,
+ 0x30, 0x4e, 0xa0, 0x7b, 0xd3, 0x99, 0xc8, 0xe4, 0x15, 0x2b, 0x7b, 0xfa,
+ 0x98, 0xb4, 0xd3, 0x2d, 0x96, 0x99, 0xc0, 0xb6, 0x44, 0x8f, 0xdd, 0x9a,
+ 0x69, 0x0f, 0x69, 0x6d, 0xe5, 0x76, 0x1d, 0x76, 0xe0, 0x4f, 0x5d, 0xf5,
+ 0x8a, 0xf1, 0x00, 0x30, 0xe3, 0x6a, 0xe3, 0x03, 0x3e, 0xe2, 0x8d, 0x3c,
+ 0x4e, 0x71, 0x33, 0x31, 0xf8, 0xc1, 0xa1, 0x17, 0xd5, 0xc9, 0x88, 0xfb,
+ 0xf8, 0x83, 0xe2, 0x8a, 0xbd, 0xba, 0x87, 0xe1, 0x5f, 0x70, 0x3e, 0xe2,
+ 0x4e, 0xd6, 0x1c, 0xc2, 0x3e, 0x4e, 0xe1, 0xf5, 0x42, 0x8a, 0x3d, 0xa5,
+ 0x9c, 0xca, 0xbf, 0x2c, 0xcc, 0xf3, 0x4b, 0x2f, 0x15, 0x8f, 0xa7, 0x2b,
+ 0x35, 0xe7, 0x84, 0x31, 0x46, 0x51, 0xcf, 0xc8, 0x5c, 0x5c, 0xf3, 0xc6,
+ 0x19, 0xf1, 0xd7, 0x42, 0x61, 0x42, 0xb3, 0x7d, 0x90, 0x0b, 0x1d, 0xfd,
+ 0x52, 0xa3, 0x81, 0xa3, 0xf2, 0xe9, 0x36, 0x20, 0xdd, 0xe0, 0x91, 0xcd,
+ 0x8d, 0x38, 0x41, 0xc7, 0xe5, 0xfb, 0xad, 0xe4, 0xf0, 0x64, 0x19, 0xca,
+ 0x05, 0x71, 0xd8, 0xfc, 0x0e, 0xd1, 0xfa, 0x13, 0x58, 0x48, 0x2f, 0x14,
+ 0x31, 0xa5, 0xf0, 0xbc, 0x07, 0xe6, 0xb4, 0xc4, 0x85, 0x11, 0x5e, 0xb8,
+ 0xc4, 0x68, 0xfa, 0x29, 0x91, 0x63, 0x77, 0x83, 0x4f, 0xe8, 0x61, 0x4c,
+ 0xcb, 0x49, 0xda, 0xf9, 0x7f, 0xfd, 0x2f, 0x11, 0xb0, 0xc0, 0xfd, 0x74,
+ 0x5b, 0xa1, 0x07, 0x97, 0xd2, 0xa3, 0x99, 0xc2, 0x86, 0xee, 0xc9, 0x39,
+ 0x6a, 0xaa, 0x38, 0x3b, 0xd6, 0xa1, 0xba, 0xe8, 0x40, 0x96, 0x85, 0x8c,
+ 0x26, 0xf6, 0x8d, 0xbb, 0x91, 0xde, 0x9a, 0x08, 0xf9, 0x77, 0xba, 0x7a,
+ 0x55, 0xc8, 0xcf, 0xc6, 0x43, 0xf0, 0x00, 0x96, 0xd8, 0xb5, 0x1f, 0x8e,
+ 0xc2, 0x52, 0xd1, 0x2e, 0xe4, 0x0e, 0x4b, 0x07, 0xfb, 0xfc, 0x3e, 0xa1,
+ 0x6e, 0xa0, 0xa4, 0xc4, 0x03, 0xb6, 0x1b, 0x04, 0x72, 0x10, 0x48, 0x02,
+ 0xad, 0x5e, 0xa3, 0xfc, 0x0e, 0xb7, 0x44, 0x49, 0x3e, 0x2f, 0xac, 0xfe,
+ 0x9c, 0xcc, 0x84, 0x16, 0xd1, 0xf0, 0x5a, 0xa4, 0x9a, 0x4f, 0xf4, 0x0c,
+ 0xbf, 0xae, 0x7a, 0x3a, 0x5b, 0x39, 0x05, 0x2b, 0x00, 0x55, 0xfe, 0x8c,
+ 0x1a, 0x43, 0x78, 0x41, 0x42, 0x65, 0x80, 0x50, 0x76, 0xe5, 0xb7, 0x26,
+ 0x7b, 0x7e, 0xad, 0xe7, 0xd2, 0x37, 0xa0, 0xd2, 0xc7, 0x3f, 0xc4, 0x1d,
+ 0x4d, 0xdb, 0x7e, 0xeb, 0xff, 0xea, 0xeb, 0x41, 0xdb, 0x55, 0x11, 0xf7,
+ 0x6b, 0x38, 0xd0, 0xd7, 0xac, 0xee, 0x82, 0x00, 0x99, 0x69, 0x2a, 0xd3,
+ 0x74, 0x3d, 0x2f, 0x56, 0xe0, 0x32, 0xf0, 0xd7, 0xdc, 0x72, 0x2e, 0xd8,
+ 0x45, 0xf2, 0xd1, 0x9e, 0xab, 0xfb, 0xff, 0x0b, 0x16, 0x4c, 0x94, 0xc5,
+ 0x43, 0xd8, 0x44, 0xe7, 0x3c, 0x4a, 0x18, 0xd4, 0xf8, 0x2b, 0xa0, 0xe8,
+ 0x0e, 0x08, 0x1b, 0x58, 0x75, 0x71, 0xbb, 0xed, 0x1e, 0x91, 0xfa, 0x83,
+ 0x0f, 0xdf, 0x5e, 0xb2, 0x55, 0x16, 0x21, 0x67, 0xe1, 0xe1, 0xa8, 0xa8,
+ 0x93, 0x62, 0x4d, 0x82, 0xca, 0x7d, 0x04, 0x09, 0xe4, 0x7b, 0x78, 0x11,
+ 0xa9, 0x30, 0xcd, 0xfb, 0x62, 0xee, 0xbe, 0x47, 0x05, 0x13, 0x30, 0xe8,
+ 0xa1, 0x47, 0x11, 0xd7, 0x75, 0x8b, 0x1b, 0x15, 0xcc, 0xd5, 0x5a, 0xa5,
+ 0xb1, 0x05, 0x12, 0xc4, 0x20, 0xfe, 0x46, 0x17, 0x6a, 0x10, 0x8c, 0xd9,
+ 0xb4, 0x05, 0x6e, 0x1b, 0x16, 0xf5, 0xdd, 0x0e, 0x43, 0xd6, 0x54, 0x36,
+ 0x2b, 0x5d, 0x9c, 0x72, 0xca, 0xb7, 0xdb, 0xaa, 0xca, 0x18, 0x86, 0x57,
+ 0x11, 0x1a, 0x7d, 0xbb, 0x5d, 0xdf, 0x0b, 0xcf, 0x3d, 0x0e, 0x02, 0xd6,
+ 0x8c, 0x9f, 0xe6, 0x5c, 0xe3, 0xd3, 0x01, 0x12, 0x5c, 0x40, 0xe5, 0xa8,
+ 0x24, 0x40, 0xd7, 0xea, 0x33, 0x22, 0x23, 0xf1, 0xdc, 0x2e, 0x55, 0x6a,
+ 0x05, 0xbd, 0xc5, 0x90, 0xd0, 0xd9, 0x74, 0xff, 0x98, 0xa4, 0x59, 0x38,
+ 0x2b, 0x9a, 0xe4, 0x68, 0xf2, 0x7f, 0xc0, 0x53, 0x92, 0x0e, 0x49, 0x51,
+ 0x9f, 0x8b, 0x68, 0xe9, 0x42, 0x0f, 0x08, 0xb3, 0xb4, 0x8d, 0x3a, 0x7f,
+ 0x48, 0xaf, 0x6f, 0x31, 0x9c, 0x03, 0x3b, 0xe9, 0xab, 0x84, 0xf0, 0x5a,
+ 0x2a, 0x6d, 0xa7, 0x38, 0x91, 0x0e, 0x90, 0x35, 0x49, 0xc2, 0xd5, 0xb9,
+ 0xd2, 0x54, 0x14, 0x1b, 0x11, 0xe7, 0xe9, 0x4a, 0xc1, 0x6c, 0x4b, 0x6d,
+ 0x60, 0xe3, 0xa4, 0x0a, 0x1e, 0x3e, 0x41, 0x96, 0xae, 0x35, 0xef, 0xe1,
+ 0xca, 0x88, 0x4a, 0x07, 0xc2, 0xcb, 0xee, 0x2a, 0x6f, 0x08, 0x95, 0x3d,
+ 0x64, 0x76, 0xad, 0x31, 0xe1, 0xfb, 0x82, 0x2f, 0xbc, 0x59, 0x1c, 0x1f,
+ 0xcd, 0xf0, 0xa0, 0x96, 0x61, 0xfb, 0xde, 0xa2, 0x99, 0xe1, 0x90, 0xcc,
+ 0x43, 0x25, 0x4c, 0x8d, 0x9d, 0x5b, 0xe7, 0x34, 0xdb, 0xe9, 0x6a, 0x08,
+ 0x6f, 0x40, 0x78, 0x24, 0x25, 0x90, 0xd8, 0x7e, 0x76, 0x39, 0x9d, 0x00,
+ 0x86, 0x7c, 0xf4, 0xa2, 0xfa, 0x3b, 0x2d, 0x0e, 0xee, 0x25, 0x2b, 0x12,
+ 0x36, 0x10, 0x67, 0x84, 0xe7, 0xa9, 0xc4, 0xb0, 0xfb, 0x9e, 0x6c, 0xa4,
+ 0x53, 0x77, 0x68, 0xfc, 0x3b, 0xd5, 0x77, 0x65, 0x3c, 0xb8, 0x56, 0x9a,
+ 0x2c, 0x42, 0x64, 0x98, 0x53, 0x8f, 0x1d, 0x8c, 0xf7, 0x4c, 0xc5, 0xbe,
+ 0xad, 0xe9, 0x40, 0x0d, 0x4a, 0xd4, 0xad, 0x5e, 0xd9, 0x5e, 0x71, 0x9c,
+ 0xfb, 0x38, 0x73, 0x93, 0xb4, 0xa6, 0x1c, 0xa5, 0x48, 0x49, 0xb3, 0xa2,
+ 0x33, 0xf7, 0x54, 0x15, 0xfa, 0xc5, 0x2f, 0xda, 0x20, 0xaa, 0x95, 0x67,
+ 0xee, 0x47, 0x96, 0x6f, 0xce, 0x04, 0x9b, 0x75, 0xff, 0xfc, 0x3e, 0x96,
+ 0xc8, 0x56, 0x6d, 0x90, 0x04, 0xc0, 0x0e, 0x9b, 0x9b, 0x66, 0xff, 0x30,
+ 0xbc, 0x6f, 0xd1, 0x80, 0x34, 0xe7, 0x22, 0xfc, 0x2b, 0x9a, 0x36, 0x4b,
+ 0xcf, 0xf0, 0x3c, 0xa3, 0xd5, 0x7a, 0x3b, 0x91, 0xef, 0xcd, 0xd7, 0x25,
+ 0xd9, 0xa4, 0x43, 0xbc, 0x09, 0xb9, 0xf5, 0xc2, 0x35, 0xbd, 0x6c, 0x2d,
+ 0x04, 0x7a, 0xa0, 0xf7, 0xd3, 0xf4, 0x90, 0x2d, 0x53, 0xb4, 0x9e, 0x56,
+ 0xc6, 0x5e, 0x52, 0x36, 0x22, 0x7c, 0xac, 0xa3, 0xb7, 0xef, 0x59, 0xde,
+ 0x12, 0x83, 0x72, 0xba, 0x3d, 0x47, 0xd7, 0x9a, 0xd3, 0x03, 0x43, 0x01,
+ 0xed, 0x79, 0x60, 0x96, 0x4e, 0x34, 0xd9, 0x98, 0xdb, 0x06, 0xb9, 0x6f,
+ 0xc6, 0xca, 0x63, 0x5b, 0x0f, 0xc0, 0x00, 0x00, 0x00, 0x02, 0x1e, 0x7f,
+ 0xa2, 0x79, 0x45, 0x83, 0x11, 0xcb, 0xe3, 0x7a, 0x18, 0x46, 0x8d, 0xc0,
+ 0xce, 0xc5, 0x1c, 0x78, 0x39, 0xdb, 0x78, 0xff, 0x7b, 0xbf, 0xac, 0x52,
+ 0xc8, 0x56, 0xe6, 0x20, 0x76, 0xb2, 0xd6, 0x3f, 0x46, 0x0a, 0x67, 0x1d,
+ 0xc5, 0xb3, 0xd0, 0xd3, 0x16, 0xee, 0x18, 0x02, 0xdb, 0xb4, 0x11, 0xa7,
+ 0x2d, 0x12, 0x59, 0xab, 0xa8, 0xab, 0x3c, 0xb0, 0xe6, 0x2d, 0xed, 0x1b,
+ 0xe0, 0x84, 0xee, 0xd8, 0xdf, 0xf8, 0x5a, 0x0b, 0x58, 0x29, 0x1e, 0x24,
+ 0x0e, 0x0f, 0x1f, 0x71, 0x4b, 0x72, 0x3d, 0x99, 0x3f, 0x14, 0x92, 0xf2,
+ 0xb2, 0x7b, 0xc6, 0xa6, 0x69, 0x5c, 0xb6, 0x02, 0x1f, 0x19, 0x66, 0x91,
+ 0x6b, 0x7f, 0x56, 0x65, 0x67, 0x70, 0x43, 0xd4, 0x75, 0x8d, 0x77, 0x49,
+ 0xef, 0x24, 0xee, 0xb8, 0xd7, 0x28, 0x5c, 0x31, 0xdd, 0x73, 0x0d, 0x92,
+ 0x76, 0xe8, 0x12, 0x55, 0xfa, 0x5f, 0xc0, 0xa2, 0x3f, 0x95, 0xa2, 0x08,
+ 0xf8, 0x1b, 0xc0, 0x11, 0x31, 0xf1, 0x3e, 0x64, 0x7d, 0x40, 0x7c, 0xcb,
+ 0x59, 0xb6, 0x98, 0x73, 0x42, 0xdb, 0x08, 0xf9, 0xfe, 0xce, 0x6b, 0xa9,
+ 0x31, 0xec, 0xed, 0x8d, 0xc1, 0xd5, 0xb3, 0x9e, 0xcd, 0xff, 0xa7, 0x09,
+ 0xd8, 0x6f, 0x2a, 0x51, 0x6c, 0xa1, 0x1e, 0x04, 0x4c, 0x8a, 0x90, 0x11,
+ 0x29, 0xd0, 0xa1, 0xb4, 0xdb, 0x1d, 0xf3, 0xea, 0xe9, 0x86, 0xd7, 0xa5,
+ 0x65, 0xb8, 0x34, 0x8a, 0x1a, 0xb7, 0x37, 0xe2, 0xa2, 0x35, 0x50, 0x97,
+ 0x61, 0xc7, 0x2c, 0xb9, 0x62, 0x6f, 0x40, 0x3f, 0x53, 0xe2, 0x30, 0x9e,
+ 0x99, 0xc7, 0xd5, 0xa1, 0x79, 0xbf, 0xc3, 0x1c, 0x37, 0x64, 0x73, 0x19,
+ 0xc7, 0x2f, 0xab, 0x07, 0x9f, 0x4d, 0x90, 0xf8, 0x17, 0x38, 0x89, 0xa4,
+ 0x22, 0x60, 0x05, 0x25, 0xe1, 0x57, 0x95, 0xa9, 0x86, 0x7d, 0xa0, 0x7e,
+ 0x7d, 0x7f, 0x49, 0xe4, 0xd0, 0x25, 0x04, 0x2b, 0x7a, 0x60, 0xff, 0x42,
+ 0xbe, 0x10, 0x88, 0x78, 0x45, 0xd8, 0xb4, 0xf9, 0x82, 0x2f, 0x7d, 0xa7,
+ 0x30, 0xf0, 0xf9, 0xbc, 0x5d, 0x86, 0x57, 0x04, 0x55, 0x36, 0xbf, 0x94,
+ 0x07, 0xa1, 0x4f, 0x0a, 0xed, 0xc9, 0x6b, 0x32, 0xc1, 0x5c, 0x6b, 0x53,
+ 0x7c, 0x8c, 0x5c, 0x33, 0xc4, 0x9b, 0x58, 0x15, 0x1b, 0x85, 0x4d, 0x96,
+ 0x73, 0xc4, 0xb2, 0xec, 0x34, 0xea, 0x1b, 0x61, 0x3f, 0x78, 0xe3, 0xc1,
+ 0x99, 0x64, 0xf1, 0x06, 0x33, 0x5d, 0x58, 0xe8, 0xde, 0x74, 0xba, 0xc5,
+ 0x70, 0x07, 0xe0, 0xb0, 0xc5, 0x40, 0xc2, 0xe6, 0xcf, 0x5c, 0xcc, 0x06,
+ 0xbe, 0x3c, 0xe1, 0x6c, 0x5b, 0xf3, 0xc5, 0x5f, 0x77, 0x25, 0x70, 0x71,
+ 0x25, 0x22, 0xb4, 0x94, 0xf8, 0xc1, 0x23, 0x9d, 0xc7, 0x70, 0xd4, 0xbd,
+ 0x19, 0x54, 0xf9, 0x22, 0xd0, 0xee, 0xed, 0x1c, 0xec, 0x47, 0x25, 0xfe,
+ 0x02, 0x00, 0x46, 0x5e, 0x9f, 0xaf, 0x33, 0x9d, 0x94, 0x5c, 0xbb, 0x3e,
+ 0x56, 0xf8, 0xca, 0x73, 0xcb, 0x25, 0x98, 0xfe, 0xfe, 0x6c, 0x63, 0xf7,
+ 0x7f, 0x2e, 0xc2, 0x3c, 0x87, 0x13, 0x97, 0xdb, 0xd2, 0x50, 0xef, 0xfd,
+ 0xf6, 0x92, 0xde, 0x38, 0x9a, 0x06, 0x5c, 0xfe, 0xb3, 0x83, 0xf8, 0xbe,
+ 0x92, 0x77, 0x58, 0xef, 0xf7, 0x31, 0x25, 0xfa, 0x61, 0x24, 0x9b, 0x2c,
+ 0xaf, 0xae, 0x4e, 0xe9, 0xe9, 0xb5, 0x13, 0x11, 0x83, 0xfa, 0xe7, 0xa3,
+ 0x12, 0xa8, 0x9b, 0xe2, 0xe9, 0xfb, 0x18, 0xd8, 0x75, 0x94, 0x3a, 0xe9,
+ 0x3a, 0xb4, 0x73, 0x94, 0xf2, 0xda, 0xcb, 0xe0, 0x22, 0xf2, 0x7f, 0xe6,
+ 0x48, 0x60, 0x2a, 0xf9, 0xf1, 0xe0, 0xa0, 0x31, 0x89, 0xc6, 0x9e, 0x7f,
+ 0x38, 0xd6, 0xb5, 0x3e, 0x67, 0x47, 0xb2, 0xc1, 0xa2, 0xcf, 0xf4, 0x5d,
+ 0x33, 0x2b, 0x3b, 0x87, 0xd0, 0x3c, 0x32, 0xc6, 0xa5, 0xae, 0xda, 0xab,
+ 0x4e, 0x0c, 0x04, 0xf9, 0x33, 0x04, 0x9e, 0xfd, 0xb6, 0x95, 0x54, 0x02,
+ 0x3d, 0x77, 0x68, 0x9e, 0x02, 0x23, 0x7f, 0x62, 0x65, 0x07, 0xf6, 0xc7,
+ 0x80, 0x00, 0x00, 0x01, 0x4e, 0x7f, 0xa4, 0x01, 0xde, 0x65, 0xb9, 0x4c,
+ 0xc8, 0x2b, 0xa4, 0xd3, 0x7c, 0xf2, 0x87, 0x6d, 0x11, 0x12, 0x1c, 0x3f,
+ 0x22, 0xca, 0x14, 0xca, 0x91, 0x51, 0x22, 0x02, 0x77, 0xd7, 0x75, 0xdf,
+ 0x8a, 0x8d, 0x57, 0x5d, 0xd1, 0xd1, 0x87, 0xd3, 0x60, 0x3e, 0x51, 0xa2,
+ 0xe9, 0x2b, 0x1e, 0x9a, 0xbd, 0xa9, 0x9d, 0xf7, 0x74, 0x61, 0xf8, 0x6a,
+ 0xf1, 0xd0, 0x6f, 0xfa, 0x3d, 0xd0, 0x80, 0xa2, 0x00, 0x75, 0xa5, 0x41,
+ 0x14, 0xf2, 0x84, 0x34, 0x96, 0xe2, 0x1f, 0xea, 0x60, 0xf6, 0x1b, 0x30,
+ 0x7e, 0x8b, 0xee, 0xd1, 0xc5, 0x98, 0xb6, 0x8e, 0xaa, 0xd5, 0xb3, 0x45,
+ 0x27, 0x51, 0x1d, 0x23, 0x44, 0xc3, 0x0b, 0x86, 0x97, 0xf4, 0xa8, 0x9f,
+ 0xf2, 0x74, 0xaf, 0xb0, 0x58, 0x6b, 0x39, 0x98, 0xe5, 0xe1, 0xf5, 0x61,
+ 0xb4, 0x56, 0xc0, 0x26, 0xdc, 0xe2, 0x09, 0xdc, 0x16, 0xd9, 0xec, 0x81,
+ 0x3e, 0x8d, 0x3c, 0x79, 0x65, 0x9d, 0x56, 0x18, 0x6a, 0x30, 0x53, 0x60,
+ 0x0d, 0x3c, 0x06, 0x2d, 0xb6, 0x9a, 0x18, 0x0e, 0x81, 0xde, 0x2e, 0x02,
+ 0xba, 0x06, 0x5e, 0xe6, 0x9b, 0x42, 0xa8, 0x6c, 0x48, 0xd3, 0xcd, 0x95,
+ 0xb0, 0x9a, 0xc9, 0xa2, 0x6a, 0xbe, 0x45, 0xf9, 0x39, 0xb6, 0x5e, 0x64,
+ 0x7c, 0x40, 0x7d, 0x36, 0xa2, 0x8d, 0x16, 0xe2, 0x2d, 0x79, 0xda, 0x83,
+ 0x5a, 0xfa, 0x53, 0x00, 0x14, 0x36, 0xc3, 0xd8, 0x0b, 0x4f, 0xfa, 0x69,
+ 0x41, 0x5f, 0x7c, 0xf6, 0xd1, 0x10, 0x1b, 0xbd, 0x2e, 0x17, 0xad, 0x51,
+ 0x87, 0xaf, 0xe1, 0x98, 0xd6, 0x70, 0x76, 0x43, 0x34, 0x68, 0xae, 0xf9,
+ 0xe4, 0x12, 0x48, 0xf1, 0x0f, 0xe5, 0x50, 0x47, 0x81, 0x9d, 0xa6, 0xab,
+ 0x4a, 0xa3, 0x29, 0xb3, 0x55, 0x81, 0xf3, 0x34, 0xdf, 0x79, 0x56, 0x71,
+ 0x13, 0xd0, 0x18, 0xaf, 0xde, 0x5d, 0x32, 0xdd, 0x14, 0xc2, 0x5a, 0x4f,
+ 0xcf, 0x81, 0x59, 0x31, 0x17, 0xbf, 0xae, 0x77, 0x5e, 0x62, 0x57, 0x03,
+ 0xb2, 0x8c, 0xfd, 0xdf, 0x61, 0xe5, 0x5d, 0xc6, 0x95, 0xaa, 0x55, 0xbb,
+ 0x0d, 0x93, 0x7e, 0xb9, 0x55, 0xf1, 0x01, 0xeb, 0xa9, 0xb6, 0xdb, 0x78,
+ 0xfe, 0xbf, 0x61, 0xf6, 0xbd, 0x86, 0x52, 0x91, 0xa5, 0x51, 0xf4, 0xbd,
+ 0x96, 0x63, 0x9c, 0x08, 0x57, 0x0f, 0xcd, 0x13, 0x69, 0xa5, 0xa3, 0xd1,
+ 0xee, 0x2d, 0xa9, 0xe5, 0xc0, 0xd1, 0x3b, 0x5d, 0x26, 0x16, 0x75, 0x10,
+ 0x70, 0x08, 0x00, 0x7f, 0xd8, 0x34, 0x3a, 0x8c, 0xb2, 0x4a, 0x41, 0xaf,
+ 0x0f, 0x8c, 0x36, 0xa5, 0x47, 0x46, 0x68, 0x6b, 0x7b, 0x11, 0x71, 0xb1,
+ 0x9c, 0xd7, 0xa0, 0x83, 0x48, 0x02, 0x5a, 0xcc, 0x32, 0x6e, 0x38, 0x5a,
+ 0x2c, 0x26, 0xfb, 0xe7, 0x3d, 0xb5, 0x64, 0x81, 0xfa, 0x91, 0x1e, 0xfd,
+ 0x77, 0x7e, 0xe1, 0x9e, 0xab, 0x55, 0x44, 0x76, 0x22, 0x95, 0xff, 0xb3,
+ 0xa5, 0x67, 0xbd, 0xf5, 0x4d, 0x84, 0x34, 0xc2, 0x7b, 0xe6, 0x6f, 0xbf,
+ 0x55, 0x52, 0x27, 0x31, 0x8e, 0xa3, 0xf7, 0xad, 0xde, 0xb7, 0x7b, 0x77,
+ 0x06, 0xb2, 0xc8, 0xec, 0xf4, 0xd6, 0xdb, 0xad, 0xdf, 0x03, 0x9f, 0x09,
+ 0xc8, 0x4a, 0xaf, 0x1d, 0x8e, 0x01, 0x04, 0x8d, 0x68, 0xf2, 0xa8, 0xb5,
+ 0xb4, 0x33, 0xa1, 0xb0, 0x6a, 0x5a, 0x12, 0x7e, 0xfe, 0xe2, 0x7a, 0xd6,
+ 0x1c, 0x30, 0x81, 0xb7, 0x84, 0x8a, 0x5c, 0xbd, 0x63, 0x31, 0x7b, 0x50,
+ 0x8c, 0xf1, 0x45, 0x7f, 0x6e, 0xb7, 0x7f, 0x26, 0x0e, 0xdb, 0xa7, 0x42,
+ 0x7b, 0x0d, 0x06, 0x9f, 0x81, 0x79, 0x39, 0x38, 0x80, 0xd2, 0xe8, 0xe6,
+ 0xef, 0xdf, 0x39, 0x1b, 0x0d, 0x5d, 0x26, 0xd9, 0x71, 0x14, 0xb0, 0x9f,
+ 0xdc, 0x17, 0xa2, 0xa0, 0x8a, 0x70, 0x67, 0x28, 0x0e, 0x65, 0x6f, 0x0a,
+ 0x04, 0x4d, 0x69, 0xbb, 0x45, 0xf7, 0xcc, 0x7a, 0xc7, 0xb3, 0x38, 0x8c,
+ 0x0b, 0xab, 0x46, 0xc7, 0x4e, 0x62, 0x06, 0x65, 0x85, 0x06, 0xe8, 0x87,
+ 0x28, 0xe1, 0x99, 0x3d, 0x44, 0xc8, 0xcd, 0x05, 0xef, 0xc9, 0xa9, 0x9f,
+ 0x6d, 0xc9, 0x17, 0x2f, 0x9a, 0xa8, 0xb1, 0x78, 0x41, 0xf1, 0x09, 0x7a,
+ 0xe3, 0xd9, 0xc5, 0x3e, 0xf8, 0x18, 0xb3, 0xfb, 0x70, 0xef, 0x31, 0x01,
+ 0x0d, 0xc7, 0xd1, 0x28, 0x7d, 0xd2, 0x74, 0xe8, 0x50, 0x0c, 0x21, 0x3c,
+ 0x77, 0x3c, 0xa8, 0xec, 0x7f, 0x6d, 0x95, 0xd5, 0x18, 0x26, 0xfc, 0xe1,
+ 0x67, 0xa6, 0xb3, 0xe6, 0x27, 0x4b, 0x6a, 0x68, 0x46, 0x19, 0x4b, 0xa6,
+ 0xa4, 0x69, 0xa9, 0x51, 0x40, 0x44, 0x37, 0xc6, 0xd5, 0x47, 0x0e, 0xdc,
+ 0xb3, 0x27, 0x36, 0x75, 0x28, 0x66, 0x70, 0xc6, 0x41, 0x69, 0xd4, 0xa8,
+ 0xea, 0x97, 0x1b, 0x1c, 0xb2, 0xf3, 0x57, 0x78, 0x62, 0xfc, 0xc6, 0xad,
+ 0x02, 0xbe, 0x1a, 0xa0, 0x6c, 0x29, 0xd1, 0x40, 0x83, 0xa1, 0x39, 0x5b,
+ 0x21, 0x6d, 0xb9, 0x14, 0xd2, 0x53, 0xb6, 0x56, 0x41, 0x7d, 0x41, 0x21,
+ 0x38, 0x10, 0x4d, 0x28, 0x5f, 0xb6, 0xa0, 0xb4, 0x76, 0xcd, 0xdb, 0x6f,
+ 0xac, 0xfd, 0x7a, 0x29, 0x97, 0xe5, 0x14, 0x52, 0xc4, 0xc0, 0x97, 0x8d,
+ 0x81, 0xf3, 0xa6, 0x0e, 0x86, 0x83, 0xab, 0x04, 0x90, 0xc5, 0xfc, 0xd9,
+ 0xae, 0x00, 0xb7, 0xa3, 0x61, 0xd0, 0x29, 0x57, 0x56, 0xbf, 0xa7, 0xf9,
+ 0x8d, 0x7e, 0xc4, 0xb0, 0x64, 0xf4, 0x01, 0xf4, 0x6c, 0x27, 0xdb, 0x27,
+ 0x08, 0x52, 0xed, 0x44, 0xc7, 0x54, 0xc9, 0xaa, 0xbc, 0x16, 0x8f, 0xee,
+ 0x89, 0x17, 0xee, 0x70, 0x75, 0xaa, 0xa9, 0xb5, 0x62, 0x7f, 0x50, 0x35,
+ 0xc4, 0xae, 0xa3, 0x37, 0x6e, 0x48, 0x98, 0xa9, 0x89, 0x58, 0x4a, 0xcf,
+ 0x39, 0xad, 0x44, 0xbd, 0x71, 0x23, 0xe9, 0x0d, 0xc7, 0xbc, 0x55, 0xca,
+ 0x92, 0xd6, 0xaf, 0x4a, 0x47, 0x54, 0xd2, 0x76, 0xe2, 0x36, 0x6b, 0xb7,
+ 0x89, 0x2d, 0x5b, 0x55, 0x5c, 0xd5, 0xdf, 0xf0, 0xcd, 0xdf, 0x2f, 0xf6,
+ 0xab, 0xfa, 0x3f, 0x52, 0xc3, 0xa2, 0x91, 0x6b, 0x6d, 0xed, 0x30, 0x64,
+ 0xe7, 0x2e, 0x28, 0x8e, 0xff, 0xd0, 0x83, 0x88, 0x33, 0x4f, 0x51, 0x1a,
+ 0x9f, 0x1f, 0xa5, 0xd7, 0x05, 0xca, 0x43, 0x14, 0xde, 0xf7, 0x0c, 0x7d,
+ 0x50, 0x13, 0xd5, 0x42, 0x37, 0x1f, 0x92, 0xa5, 0x60, 0xb2, 0x5f, 0x06,
+ 0xe4, 0x55, 0xdb, 0x54, 0x63, 0xe2, 0xb1, 0x34, 0x89, 0xe5, 0xda, 0xae,
+ 0x95, 0x08, 0x87, 0xf1, 0xe0, 0x14, 0x7c, 0x28, 0x19, 0x8a, 0xce, 0xd2,
+ 0x18, 0xef, 0x3d, 0x95, 0x45, 0x17, 0xd1, 0xb3, 0x5c, 0x05, 0x74, 0x55,
+ 0x0d, 0x2a, 0xaa, 0x9c, 0x75, 0xab, 0xc8, 0xb8, 0x46, 0x7c, 0x2f, 0xec,
+ 0x36, 0x2d, 0xb5, 0xd1, 0xa2, 0x5e, 0xe7, 0xc9, 0xe5, 0xfe, 0x05, 0x00,
+ 0xba, 0x7f, 0xbc, 0x3a, 0x2b, 0x46, 0xb0, 0xa1, 0x15, 0x39, 0x45, 0xab,
+ 0x63, 0x42, 0x63, 0xd7, 0x5e, 0x11, 0xf2, 0xb0, 0xe6, 0x04, 0x7f, 0x52,
+ 0xa2, 0x43, 0xad, 0x5c, 0x3a, 0x60, 0xbf, 0x15, 0xe5, 0xd1, 0xb9, 0x97,
+ 0xf6, 0xf7, 0xf9, 0xd1, 0xf5, 0x43, 0xa4, 0x7d, 0x85, 0x2e, 0xad, 0x4e,
+ 0x16, 0x5f, 0xb4, 0x63, 0x44, 0x03, 0xe3, 0xcc, 0x8a, 0x4a, 0x39, 0x0e,
+ 0x61, 0x72, 0xef, 0xd2, 0x87, 0x7a, 0x2a, 0x59, 0x6a, 0xc1, 0x31, 0x96,
+ 0x1d, 0x70, 0xbb, 0x85, 0xe3, 0xb7, 0xb7, 0x61, 0x17, 0xc5, 0x43, 0x9b,
+ 0x7c, 0x79, 0x8f, 0x20, 0x12, 0x20, 0x28, 0x39, 0x61, 0x87, 0xfe, 0x71,
+ 0x08, 0xb1, 0x9a, 0x22, 0xdc, 0xa2, 0x90, 0x26, 0x80, 0x31, 0x36, 0xda,
+ 0xf3, 0x6a, 0xba, 0xac, 0x6a, 0x25, 0x8e, 0x77, 0xa2, 0x7a, 0x5b, 0xdf,
+ 0xf3, 0x11, 0x33, 0x8d, 0xe2, 0x00, 0xcc, 0xeb, 0x7f, 0x48, 0x94, 0x59,
+ 0xa7, 0xce, 0x97, 0x01, 0xb0, 0x53, 0x62, 0x8a, 0x82, 0x61, 0xd8, 0x62,
+ 0xfd, 0x80, 0x96, 0x0f, 0x34, 0x48, 0xe6, 0x04, 0xa3, 0x88, 0x36, 0x76,
+ 0xee, 0xe5, 0x9e, 0xec, 0xb4, 0x33, 0x96, 0xcc, 0xa3, 0x33, 0x15, 0x00,
+ 0x63, 0x67, 0x9e, 0x3c, 0x15, 0xe4, 0x5c, 0x58, 0xcd, 0xc6, 0x69, 0x30,
+ 0x85, 0x86, 0x32, 0x68, 0x09, 0x29, 0x6d, 0xec, 0xdd, 0x59, 0x46, 0xe0,
+ 0x6b, 0x86, 0x6a, 0x56, 0x28, 0x99, 0xab, 0xfc, 0xb9, 0x8f, 0x21, 0xc8,
+ 0x8f, 0xf2, 0xec, 0x33, 0x1b, 0xce, 0x4a, 0x33, 0xd5, 0x12, 0x5e, 0x9f,
+ 0x01, 0xde, 0x26, 0x49, 0x4e, 0xb1, 0xe3, 0x54, 0xdb, 0x13, 0x6f, 0x1b,
+ 0x23, 0x22, 0x4c, 0xd0, 0xd8, 0xe8, 0x13, 0xfb, 0x7e, 0xae, 0x49, 0x77,
+ 0xb4, 0x0d, 0x42, 0x59, 0xfa, 0x29, 0x7e, 0x13, 0xdb, 0x5e, 0xac, 0x11,
+ 0xa5, 0xfd, 0x91, 0x06, 0x0b, 0xbf, 0xe8, 0x13, 0x07, 0x55, 0xa4, 0x7c,
+ 0x48, 0xc4, 0x71, 0xe7, 0x13, 0x64, 0x93, 0xba, 0x39, 0xb4, 0xca, 0x97,
+ 0x10, 0x76, 0x99, 0x6b, 0x78, 0x8b, 0x23, 0x49, 0x03, 0xda, 0x12, 0xd5,
+ 0x9c, 0xcb, 0x19, 0x86, 0x1a, 0xb2, 0x3b, 0x80, 0x47, 0xa7, 0x50, 0x19,
+ 0x73, 0xfe, 0x06, 0x0c, 0xec, 0x99, 0xc3, 0x9a, 0x0b, 0x61, 0x9c, 0x16,
+ 0x8e, 0x83, 0xca, 0xa3, 0xc9, 0x7a, 0xd3, 0xad, 0x2a, 0xac, 0xb6, 0x5a,
+ 0x34, 0xf5, 0x9b, 0xb2, 0xfe, 0x3b, 0x2d, 0x7d, 0x6f, 0xd4, 0xa4, 0xd8,
+ 0xf2, 0xc0, 0xea, 0xef, 0x70, 0x96, 0x68, 0xfa, 0x6f, 0x77, 0xdc, 0xc5,
+ 0xfc, 0xb5, 0x07, 0x21, 0x18, 0xf5, 0xeb, 0x03, 0xb3, 0xa5, 0x00, 0xa9,
+ 0xdf, 0x58, 0xa2, 0xb0, 0xad, 0x0c, 0xc2, 0x5a, 0x0b, 0x87, 0x14, 0x6c,
+ 0x2e, 0xb5, 0x2b, 0xc6, 0xcc, 0xc8, 0x55, 0xbd, 0x63, 0xaa, 0xae, 0x15,
+ 0x2d, 0x31, 0x85, 0x89, 0xee, 0x1a, 0x82, 0xe0, 0x6e, 0x27, 0xa3, 0x46,
+ 0x50, 0x93, 0x1a, 0x90, 0xf6, 0x72, 0xb8, 0x34, 0x97, 0xfb, 0x31, 0x1f,
+ 0x47, 0x62, 0xcd, 0x2a, 0x51, 0x81, 0xc8, 0x77, 0x1c, 0xf5, 0xac, 0xe9,
+ 0x6a, 0x82, 0x00, 0x5a, 0x3d, 0xa0, 0xe1, 0xa6, 0x72, 0x1b, 0x36, 0x33,
+ 0x11, 0xd1, 0x72, 0xe4, 0x6d, 0x89, 0x8d, 0xc3, 0x46, 0x88, 0xbd, 0xf7,
+ 0x78, 0x32, 0x07, 0xca, 0xb4, 0x2b, 0x4c, 0x27, 0xf7, 0x2d, 0x64, 0x2d,
+ 0xbf, 0xc7, 0x8a, 0x3f, 0xed, 0x3a, 0x15, 0x0e, 0x61, 0x08, 0x4e, 0xca,
+ 0x76, 0x17, 0xaa, 0x4e, 0x45, 0x08, 0xce, 0xcc, 0x78, 0xe3, 0x5e, 0xce,
+ 0x90, 0x4d, 0xc5, 0x1f, 0xe4, 0x16, 0x67, 0xed, 0x2c, 0xf3, 0xf1, 0x5e,
+ 0xce, 0xeb, 0x29, 0x92, 0xc2, 0x1c, 0xe3, 0x7c, 0x4e, 0x7d, 0xe9, 0x6f,
+ 0xad, 0xfa, 0x30, 0x35, 0x86, 0x11, 0xab, 0x0b, 0xa8, 0xad, 0xa3, 0x88,
+ 0x47, 0xf5, 0x36, 0x5b, 0xbb, 0xa7, 0x5f, 0x88, 0xf6, 0xd9, 0x6b, 0x74,
+ 0xd6, 0xe9, 0x6c, 0x07, 0x64, 0x6f, 0xd5, 0x2f, 0xa3, 0xe4, 0xbc, 0xf6,
+ 0xc9, 0xe5, 0xde, 0x2b, 0x50, 0xac, 0xf7, 0x0c, 0x02, 0x5b, 0x3a, 0x0c,
+ 0x52, 0xc2, 0x76, 0x4b, 0xe1, 0xb1, 0x63, 0xcb, 0x44, 0xc9, 0x1a, 0xb6,
+ 0xbb, 0x91, 0xe2, 0x22, 0x75, 0x49, 0x6d, 0x84, 0x98, 0x70, 0xab, 0xb8,
+ 0x0a, 0x5b, 0x79, 0x64, 0xe8, 0xde, 0x52, 0xa0, 0x04, 0x1b, 0x78, 0x22,
+ 0xc0, 0xe9, 0xd0, 0x9d, 0xf1, 0x24, 0x79, 0x44, 0xe2, 0x11, 0xda, 0x21,
+ 0xcc, 0xc6, 0xf9, 0x13, 0x88, 0x19, 0xa3, 0x4b, 0xd2, 0x6b, 0x3d, 0x2a,
+ 0xfd, 0x01, 0xad, 0xa4, 0x80, 0x93, 0x5c, 0xea, 0xb6, 0x99, 0x27, 0x45,
+ 0x3d, 0x2b, 0xa7, 0x17, 0xa5, 0xb5, 0xbd, 0x27, 0xfb, 0x4e, 0xe3, 0x16,
+ 0x20, 0x7c, 0x81, 0xb3, 0x38, 0x8e, 0x0d, 0x80, 0x65, 0x7f, 0x26, 0x07,
+ 0x22, 0xe3, 0x80, 0x0c, 0x4a, 0x25, 0x23, 0xae, 0x65, 0x1a, 0xc9, 0x45,
+ 0xfc, 0xc7, 0x80, 0xf9, 0x55, 0x0f, 0xcd, 0xa3, 0xbe, 0xcb, 0x3c, 0x8a,
+ 0xe7, 0x92, 0xe6, 0xd8, 0x7a, 0x51, 0x3f, 0x99, 0x84, 0xfc, 0xa1, 0x32,
+ 0x33, 0x2b, 0x3d, 0x8c, 0x82, 0x7a, 0x5c, 0xe0, 0x2b, 0x91, 0xd8, 0xad,
+ 0x48, 0x39, 0xb6, 0xc9, 0x8f, 0xb2, 0x17, 0x08, 0xba, 0xb9, 0x6e, 0x81,
+ 0x67, 0x8a, 0x96, 0x51, 0x0c, 0xa7, 0xd9, 0xa9, 0x4a, 0x39, 0xe5, 0xc7,
+ 0x3b, 0x2c, 0xc4, 0xdf, 0x89, 0xa5, 0x39, 0x99, 0xa7, 0x40, 0x42, 0xde,
+ 0x0b, 0x38, 0xec, 0xb6, 0xce, 0x90, 0xf7, 0xeb, 0xe6, 0x27, 0x3f, 0xbe,
+ 0xfb, 0x6d, 0x8a, 0x05, 0x58, 0x1d, 0xbe, 0xbf, 0x9d, 0x26, 0x10, 0xe3,
+ 0x31, 0x7f, 0x02, 0x78, 0x51, 0x74, 0xdf, 0x12, 0x1f, 0xde, 0xc6, 0x92,
+ 0x53, 0x1e, 0x47, 0x4f, 0xa7, 0x04, 0x01, 0x1c, 0xea, 0x0b, 0x71, 0x4d,
+ 0xe0, 0x6d, 0xde, 0x89, 0x2e, 0x0d, 0x3b, 0x0d, 0xcc, 0xd6, 0xa3, 0x09,
+ 0xce, 0x0b, 0x71, 0x33, 0xf3, 0x85, 0x6d, 0x3a, 0xe7, 0x50, 0x40, 0x21,
+ 0x5d, 0x0b, 0x58, 0xfe, 0x60, 0xc8, 0xa8, 0xdf, 0x8e, 0xa3, 0x7e, 0xd8,
+ 0x07, 0x58, 0x0f, 0x95, 0xae, 0xdb, 0x1e, 0x13, 0x5d, 0x04, 0xdb, 0x16,
+ 0x13, 0x12, 0x41, 0x44, 0x16, 0x93, 0x8c, 0xff, 0xc6, 0xf3, 0x53, 0x3a,
+ 0x83, 0x00, 0x30, 0x21, 0x4b, 0xef, 0x08, 0x29, 0x98, 0xf0, 0x1f, 0x6d,
+ 0x66, 0xa7, 0x58, 0xe0, 0x4c, 0xd8, 0xae, 0x9d, 0xdb, 0x7a, 0x9b, 0xa6,
+ 0xb6, 0xa4, 0x8a, 0x92, 0x82, 0xa2, 0x03, 0x8d, 0xd9, 0x38, 0xd1, 0x84,
+ 0xe4, 0x38, 0x44, 0xb3, 0x2c, 0xdb, 0x66, 0xba, 0x47, 0x35, 0x4e, 0xf6,
+ 0xd9, 0x8a, 0x30, 0xe3, 0x48, 0x94, 0x59, 0xce, 0xb7, 0x22, 0xd7, 0x44,
+ 0xb1, 0x7e, 0x71, 0xf7, 0x1f, 0xdc, 0xd7, 0x6f, 0x85, 0x74, 0x08, 0x17,
+ 0x7a, 0xdb, 0xe1, 0x6f, 0x71, 0x6d, 0xb0, 0x33, 0x5c, 0xbb, 0x15, 0xaa,
+ 0xc5, 0xbc, 0x9f, 0xc9, 0x14, 0x2d, 0xb9, 0xe2, 0x22, 0x4b, 0x52, 0xa8,
+ 0xbc, 0x07, 0xd6, 0xfb, 0x5e, 0x35, 0x2c, 0xfb, 0xfc, 0x17, 0x87, 0x52,
+ 0x15, 0xa8, 0x3b, 0x25, 0x3c, 0x37, 0x0e, 0xa8, 0xf6, 0xed, 0x06, 0x8d,
+ 0x6c, 0x87, 0x94, 0xa3, 0xd8, 0x04, 0x55, 0x3c, 0xd6, 0xc9, 0xe5, 0x11,
+ 0xbb, 0xf9, 0xd1, 0x21, 0x67, 0x06, 0x74, 0xf2, 0x0e, 0x6c, 0x15, 0xca,
+ 0x7a, 0x27, 0xf4, 0x0c, 0xc5, 0x70, 0xb6, 0xbd, 0x3d, 0x34, 0x61, 0x8f,
+ 0x40, 0x7c, 0x86, 0x1f, 0x8a, 0x33, 0xe6, 0x81, 0x02, 0xd7, 0xf4, 0xfd,
+ 0xbd, 0x03, 0xb6, 0x8e, 0x7b, 0xe7, 0x0a, 0xe8, 0xb5, 0xba, 0x6a, 0xb9,
+ 0x40, 0xa3, 0x50, 0x98, 0x81, 0x00, 0x85, 0x00, 0x86, 0x00, 0x40, 0x96,
+ 0xf0, 0x01, 0x17, 0x86, 0x00, 0x48, 0x7f, 0x1a, 0xfe, 0xdf, 0x93, 0xdd,
+ 0x5a, 0x96, 0x15, 0x51, 0xf8, 0x02, 0x7e, 0x53, 0xe3, 0x75, 0x4f, 0x5d,
+ 0xd3, 0xbb, 0x36, 0xa0, 0xdf, 0x62, 0xf8, 0xee, 0xfc, 0x15, 0x75, 0x99,
+ 0x33, 0x4d, 0xd5, 0xbd, 0x37, 0xa6, 0xe9, 0x9f, 0x2b, 0x34, 0x5d, 0x4e,
+ 0x52, 0xf0, 0x83, 0xea, 0xf8, 0x61, 0xa1, 0xd7, 0xa9, 0xce, 0xf5, 0x11,
+ 0x3b, 0xf9, 0x66, 0x17, 0xb1, 0x9c, 0x59, 0xb7, 0x1d, 0xdc, 0x99, 0xce,
+ 0x3f, 0x94, 0xcd, 0xfa, 0x60, 0x00, 0x00, 0x00, 0x05, 0x30, 0x7f, 0x70,
+ 0xb9, 0x14, 0x96, 0x64, 0x7d, 0xfa, 0xfe, 0x53, 0x74, 0x3e, 0xcb, 0x80,
+ 0x5d, 0x04, 0xe0, 0x7a, 0x8a, 0xbe, 0x8c, 0xdf, 0x4d, 0xde, 0xfd, 0x1a,
+ 0xe1, 0xda, 0x61, 0x04, 0x5b, 0x50, 0xda, 0x5b, 0x7e, 0xc2, 0x5a, 0xb5,
+ 0xb7, 0x0d, 0x9a, 0xf8, 0xae, 0x47, 0x8e, 0x24, 0x6b, 0x46, 0x5a, 0xae,
+ 0x9e, 0xd2, 0x7d, 0xbf, 0x08, 0x0c, 0x31, 0x19, 0xf3, 0x60, 0x07, 0x8e,
+ 0xd9, 0x46, 0x88, 0x68, 0xe6, 0x66, 0x8e, 0xe6, 0x86, 0xd7, 0xaf, 0xf7,
+ 0x3e, 0x60, 0x3a, 0x9b, 0xe5, 0x1e, 0xa0, 0x00, 0xd6, 0xf8, 0x0f, 0xc6,
+ 0xb8, 0x50, 0x84, 0x76, 0x4e, 0x4e, 0x97, 0xf3, 0x28, 0x78, 0xd0, 0xbb,
+ 0x04, 0x6f, 0x79, 0xdf, 0x81, 0xbb, 0x0f, 0xe5, 0xad, 0xd7, 0xd7, 0xa1,
+ 0x78, 0x20, 0x6b, 0xe7, 0xff, 0xd7, 0xfa, 0x6d, 0xa0, 0xe8, 0x4b, 0xde,
+ 0x39, 0x60, 0x51, 0x1a, 0x03, 0xf4, 0xd2, 0xdd, 0xbb, 0xb7, 0xa1, 0x63,
+ 0x70, 0x1e, 0x10, 0x80, 0x40, 0xf2, 0x5f, 0x4c, 0x1c, 0xf3, 0xd6, 0xa5,
+ 0xd5, 0x5e, 0xba, 0xec, 0x08, 0xf8, 0x5b, 0x3f, 0x2f, 0xbd, 0x8a, 0xed,
+ 0x7b, 0x4b, 0xc8, 0xe3, 0x55, 0x71, 0x64, 0x68, 0x45, 0x2b, 0x13, 0x87,
+ 0x1f, 0xfa, 0xa8, 0x86, 0x92, 0xda, 0xa8, 0xf0, 0x02, 0x2b, 0x6b, 0xfb,
+ 0xb2, 0xb9, 0x3d, 0x3b, 0xbb, 0xb7, 0xed, 0x87, 0x3d, 0xc0, 0x81, 0xd6,
+ 0x30, 0x8a, 0xd8, 0x1f, 0xb9, 0x3e, 0xfa, 0xc4, 0x2f, 0xd6, 0xef, 0x50,
+ 0xc7, 0x40, 0xa4, 0x30, 0x41, 0xe1, 0xa5, 0xd6, 0x26, 0x5f, 0xac, 0xdb,
+ 0x44, 0x69, 0xdc, 0x66, 0x55, 0x5b, 0x59, 0xe3, 0xff, 0xc1, 0xd2, 0xc3,
+ 0xbb, 0x78, 0x04, 0x55, 0xd9, 0x5d, 0x64, 0x42, 0x87, 0x86, 0xcd, 0xc5,
+ 0xcf, 0xc9, 0xbd, 0x24, 0x77, 0x16, 0xbb, 0x1e, 0x8e, 0x92, 0x6e, 0x9e,
+ 0x72, 0xb6, 0xe3, 0xd0, 0x41, 0xac, 0x91, 0x49, 0xd2, 0x20, 0x38, 0x3a,
+ 0xcc, 0xcf, 0xf6, 0x81, 0x85, 0xee, 0x68, 0x6e, 0x90, 0xac, 0x5a, 0x9e,
+ 0x50, 0x41, 0x9d, 0xf8, 0xe8, 0xc4, 0x31, 0x05, 0x2a, 0x1c, 0x2e, 0x66,
+ 0x90, 0xd5, 0xcc, 0x76, 0xb3, 0x3d, 0x20, 0xd4, 0xd1, 0x84, 0x5c, 0x76,
+ 0xaa, 0x54, 0x5c, 0x89, 0xb7, 0xe8, 0x6c, 0x4b, 0x35, 0x24, 0x2d, 0xd9,
+ 0xe3, 0xdc, 0x18, 0x9b, 0xdf, 0x12, 0xdf, 0xe5, 0x3f, 0xe9, 0x07, 0x66,
+ 0x29, 0x7f, 0x09, 0x0f, 0x66, 0xc9, 0x4b, 0x25, 0xce, 0x51, 0xb4, 0x9e,
+ 0xea, 0x32, 0xef, 0xf1, 0x13, 0x60, 0xdd, 0xe8, 0xe7, 0x93, 0xf0, 0x56,
+ 0x46, 0x0b, 0x95, 0x47, 0xfe, 0x17, 0xd5, 0xc7, 0x7c, 0x7a, 0xae, 0xac,
+ 0x94, 0x2a, 0x63, 0x9f, 0x5f, 0x39, 0x3e, 0xae, 0xd7, 0xa1, 0x62, 0x5b,
+ 0xc8, 0xb1, 0x00, 0xd3, 0x22, 0x76, 0x9d, 0x37, 0x0c, 0x3d, 0x33, 0xa1,
+ 0xcc, 0xf0, 0x11, 0x1f, 0x1f, 0x7e, 0x74, 0x05, 0x9d, 0x45, 0xcc, 0xc7,
+ 0x30, 0xc7, 0x1a, 0xa7, 0x72, 0x18, 0x07, 0x22, 0x72, 0xd4, 0xbe, 0xaa,
+ 0xe0, 0xf1, 0x23, 0x2f, 0x15, 0x5e, 0xbd, 0x94, 0xcb, 0x98, 0x9d, 0xa0,
+ 0x81, 0xe1, 0x4a, 0x53, 0x91, 0x8f, 0x50, 0x58, 0x34, 0x2a, 0xce, 0x26,
+ 0xac, 0x4d, 0x1a, 0xbc, 0xca, 0x03, 0xd0, 0x4c, 0x42, 0x12, 0x9e, 0x20,
+ 0x61, 0xcf, 0xcf, 0x46, 0x31, 0xcb, 0x92, 0x25, 0xde, 0x5a, 0xcd, 0x64,
+ 0x52, 0xb5, 0xfc, 0xeb, 0x4e, 0x6f, 0xf6, 0xae, 0x72, 0x8b, 0x00, 0xac,
+ 0x7d, 0x2e, 0x5e, 0xfc, 0xd0, 0x91, 0xfa, 0x41, 0xf4, 0xbb, 0xbc, 0xd7,
+ 0xe9, 0x76, 0x57, 0xf3, 0xed, 0x16, 0x6f, 0xb1, 0xc9, 0xeb, 0x96, 0x09,
+ 0x02, 0x27, 0x7a, 0xcd, 0xd3, 0x6b, 0x82, 0x98, 0xd4, 0xb2, 0xca, 0x2b,
+ 0x58, 0x29, 0x0b, 0x11, 0xeb, 0xcb, 0xa7, 0xca, 0x29, 0x50, 0x43, 0xe4,
+ 0x2f, 0x67, 0xb3, 0x7c, 0xb7, 0xbc, 0xf8, 0xbb, 0xc2, 0x72, 0xac, 0x65,
+ 0xf8, 0x7c, 0x26, 0xf1, 0x33, 0x92, 0xd6, 0x2d, 0x26, 0xba, 0x99, 0x38,
+ 0xa2, 0x6d, 0xc8, 0xba, 0x13, 0x5f, 0x30, 0x6b, 0xe6, 0x90, 0x80, 0x86,
+ 0x90, 0xb1, 0x8a, 0x49, 0xdc, 0x97, 0x42, 0x3a, 0xaf, 0x17, 0x97, 0x3b,
+ 0xf9, 0xcb, 0x3a, 0x32, 0x61, 0x32, 0x24, 0x47, 0x94, 0xce, 0xf0, 0xfb,
+ 0x22, 0x87, 0x54, 0x4f, 0x6b, 0xf1, 0x8b, 0xaa, 0x97, 0xce, 0xa2, 0xc8,
+ 0xd7, 0x3f, 0x0d, 0x11, 0x32, 0xd1, 0x3f, 0xc5, 0x62, 0x69, 0x31, 0x1a,
+ 0xfd, 0x2e, 0x4b, 0xc3, 0xdf, 0xb6, 0xf5, 0x33, 0x93, 0x2c, 0x55, 0x10,
+ 0x83, 0xaf, 0xf2, 0xd4, 0xe2, 0x5c, 0x55, 0xc9, 0xf5, 0x5f, 0x71, 0xdc,
+ 0x10, 0xef, 0xf3, 0x3d, 0xfd, 0xa6, 0xd1, 0x61, 0x1f, 0x15, 0x5a, 0x89,
+ 0xd1, 0xb0, 0x6d, 0xc5, 0x7c, 0x1c, 0x94, 0x2a, 0xd5, 0xa8, 0xcd, 0x4e,
+ 0x0a, 0x74, 0x06, 0x8e, 0x41, 0x6e, 0x5e, 0x66, 0x6a, 0x1e, 0x30, 0xa6,
+ 0xba, 0x8c, 0x76, 0xbe, 0x06, 0x68, 0x9b, 0xcb, 0xa8, 0x42, 0x0f, 0x0d,
+ 0x73, 0x70, 0xb4, 0x67, 0x3d, 0xed, 0x61, 0x19, 0xbf, 0x95, 0xa3, 0x8f,
+ 0x7b, 0x9a, 0xa8, 0x47, 0xd5, 0x99, 0x8f, 0xc7, 0x2c, 0xcd, 0xf4, 0xd4,
+ 0x6b, 0xb8, 0xe7, 0xb2, 0xd5, 0xb6, 0xa8, 0x73, 0x60, 0x25, 0x4f, 0xab,
+ 0x41, 0x07, 0x7b, 0x45, 0x2f, 0x62, 0x0a, 0xb3, 0x78, 0xa4, 0x75, 0x53,
+ 0x4a, 0xe1, 0x1b, 0xee, 0x01, 0xd7, 0x95, 0x65, 0xfa, 0x12, 0x98, 0x1b,
+ 0xdb, 0x9d, 0xae, 0x1e, 0xd2, 0x86, 0xa1, 0x36, 0xc3, 0x73, 0x93, 0x79,
+ 0x40, 0x50, 0x4a, 0xd3, 0x49, 0x9f, 0x96, 0x0a, 0x92, 0xfe, 0xff, 0xd2,
+ 0x07, 0x96, 0xc0, 0x2d, 0xab, 0xde, 0xad, 0xaf, 0xc4, 0x96, 0xb7, 0x55,
+ 0x31, 0x7c, 0x61, 0x70, 0x6b, 0x32, 0xcd, 0x65, 0x72, 0x9c, 0xa8, 0x71,
+ 0x07, 0x36, 0xc2, 0x34, 0x19, 0xbe, 0x1a, 0x7d, 0x49, 0x89, 0x07, 0xb0,
+ 0xdf, 0xf2, 0x0c, 0xab, 0x64, 0x5a, 0x99, 0xf4, 0x26, 0xe9, 0x48, 0x95,
+ 0x7a, 0x6c, 0xda, 0xf7, 0x2b, 0x6d, 0x88, 0x07, 0x32, 0xc8, 0x02, 0x62,
+ 0x2c, 0xe4, 0xfe, 0xef, 0x4a, 0x4f, 0x98, 0xf4, 0xbb, 0x49, 0xd8, 0x2d,
+ 0x23, 0x77, 0x10, 0x03, 0x20, 0x13, 0x7a, 0x05, 0xc7, 0x99, 0x54, 0xec,
+ 0x3e, 0x48, 0x4f, 0x5a, 0xe3, 0xda, 0x66, 0xe4, 0xea, 0x4d, 0xbd, 0x57,
+ 0xb7, 0x9d, 0xce, 0x12, 0x5b, 0x3c, 0xa2, 0xb8, 0x24, 0x17, 0x4f, 0x7f,
+ 0x95, 0xbd, 0xfb, 0xaa, 0x59, 0xce, 0x7c, 0xfc, 0xb8, 0x30, 0xe2, 0x7f,
+ 0xbe, 0x3c, 0x04, 0xfe, 0x1c, 0x62, 0xa9, 0x48, 0x54, 0x90, 0xf2, 0x24,
+ 0x82, 0xdd, 0xd9, 0xc9, 0x69, 0x74, 0x17, 0xc9, 0x2e, 0xf5, 0x58, 0xdc,
+ 0xb5, 0xa4, 0x43, 0x74, 0x9c, 0x11, 0x29, 0x80, 0x23, 0x60, 0x19, 0x03,
+ 0xfe, 0xcb, 0x33, 0x98, 0x53, 0xb9, 0x10, 0xba, 0xc8, 0x7f, 0xa2, 0x95,
+ 0x57, 0x6b, 0x9b, 0x37, 0x5f, 0x52, 0x5e, 0x09, 0x65, 0x6d, 0x09, 0x74,
+ 0xdc, 0x6a, 0x03, 0xb3, 0x64, 0x01, 0x35, 0x19, 0xee, 0xb1, 0xe4, 0x82,
+ 0xba, 0xf0, 0x14, 0x05, 0x44, 0xfa, 0x81, 0x07, 0xd4, 0xd5, 0xc1, 0x4f,
+ 0xe8, 0xd0, 0xd1, 0xcb, 0xd9, 0xa2, 0x31, 0xa3, 0x53, 0x5d, 0x48, 0x24,
+ 0xbd, 0xd8, 0x38, 0xaa, 0x72, 0x92, 0x28, 0xdb, 0x8b, 0x65, 0x51, 0x05,
+ 0xf3, 0x50, 0xad, 0xa3, 0xa1, 0x57, 0xd3, 0x36, 0x4b, 0xbd, 0x50, 0xf0,
+ 0xe2, 0xf6, 0x11, 0xce, 0xc7, 0x97, 0x74, 0x5e, 0x2a, 0xf2, 0xde, 0x83,
+ 0xdf, 0xf2, 0xe8, 0x10, 0xe2, 0x02, 0x68, 0x4c, 0xfe, 0x55, 0x61, 0xf4,
+ 0x91, 0x4c, 0x56, 0x0a, 0x43, 0xbc, 0x49, 0xea, 0xf6, 0x9a, 0x63, 0x35,
+ 0xa1, 0x99, 0xa0, 0xfd, 0x2a, 0x19, 0xfc, 0x47, 0x82, 0x52, 0x03, 0x18,
+ 0xe5, 0xa7, 0xa6, 0xd1, 0x53, 0x7f, 0xc6, 0x7d, 0x04, 0x15, 0x81, 0xd4,
+ 0x67, 0x55, 0x07, 0x3b, 0x81, 0x5e, 0x85, 0x99, 0xf0, 0xbd, 0x29, 0x6e,
+ 0x38, 0x97, 0x34, 0x30, 0x30, 0xc3, 0x75, 0x55, 0x3d, 0xf2, 0x6a, 0xdb,
+ 0x25, 0x3e, 0x33, 0x82, 0xd0, 0x91, 0x19, 0x8c, 0x01, 0xeb, 0x56, 0x11,
+ 0x46, 0x28, 0x9a, 0x8c, 0xf8, 0x43, 0x1a, 0x60, 0x6c, 0x18, 0x55, 0x05,
+ 0x6c, 0x44, 0x20, 0xe0, 0x4b, 0x45, 0xf1, 0x18, 0x2c, 0x03, 0xf9, 0xce,
+ 0x77, 0x86, 0x91, 0x0e, 0xd3, 0xd9, 0x1a, 0x67, 0x60, 0x19, 0x52, 0xbc,
+ 0xa1, 0x0f, 0xbd, 0x2e, 0x0a, 0xba, 0xf7, 0x1d, 0x51, 0x4a, 0x16, 0xc6,
+ 0xf7, 0xc1, 0x40, 0x64, 0x50, 0x9b, 0xf2, 0x5a, 0x66, 0x4a, 0x6b, 0x04,
+ 0x56, 0x2f, 0x26, 0x7b, 0x11, 0x3f, 0xb9, 0xf0, 0xe8, 0xc2, 0xa5, 0x61,
+ 0x1c, 0xff, 0xff, 0x5b, 0xc5, 0x53, 0x32, 0x6c, 0xeb, 0xab, 0x2b, 0x4d,
+ 0x0d, 0xe3, 0x73, 0x87, 0xdc, 0x48, 0x54, 0xcf, 0xc3, 0x21, 0x98, 0x29,
+ 0xbb, 0x19, 0x29, 0x81, 0xd8, 0x71, 0xbe, 0xad, 0xa8, 0xab, 0xbd, 0x1e,
+ 0xc6, 0x82, 0x6f, 0x68, 0x37, 0x42, 0x5b, 0xa3, 0x1e, 0x87, 0xe2, 0x6d,
+ 0x51, 0x75, 0x4b, 0x6f, 0x92, 0x94, 0x31, 0xe6, 0xbe, 0xf9, 0xf7, 0x68,
+ 0x30, 0x68, 0x7d, 0x07, 0xe9, 0x56, 0x8b, 0xe8, 0x1c, 0x57, 0xaf, 0xe0,
+ 0xb4, 0x7f, 0x61, 0xea, 0x86, 0xa3, 0x4c, 0xdf, 0xe4, 0xc2, 0x71, 0xc2,
+ 0xd8, 0x19, 0x4d, 0x31, 0x2e, 0xd6, 0x43, 0x15, 0x49, 0xd9, 0x2e, 0x60,
+ 0x01, 0x64, 0x51, 0x12, 0x21, 0x11, 0x93, 0x13, 0x57, 0x18, 0xd1, 0x8b,
+ 0xa4, 0x1c, 0xe8, 0x3a, 0xdd, 0x63, 0x52, 0x41, 0x34, 0x35, 0x75, 0x20,
+ 0xc7, 0x03, 0x7d, 0x44, 0x8a, 0xd7, 0x51, 0x2a, 0x64, 0xb7, 0x29, 0x31,
+ 0xab, 0xab, 0x0c, 0x9c, 0x0d, 0x40, 0xd5, 0x57, 0x96, 0xee, 0x21, 0x96,
+ 0xa6, 0x23, 0x19, 0xd5, 0x62, 0x49, 0x47, 0xfa, 0x67, 0x5a, 0xaa, 0x07,
+ 0xcf, 0xce, 0x3f, 0x4b, 0x14, 0x00, 0x00, 0x00, 0x02, 0x04, 0x7f, 0x6e,
+ 0xb3, 0xbd, 0xea, 0x91, 0xf0, 0x1b, 0xe1, 0x32, 0x4f, 0xa1, 0xe2, 0xe9,
+ 0xbe, 0x34, 0x23, 0xf2, 0xb7, 0x11, 0x1b, 0x03, 0x5c, 0x46, 0x5c, 0xbd,
+ 0xc1, 0x41, 0x3f, 0x93, 0x49, 0xe4, 0xf8, 0xd0, 0xd0, 0x4a, 0x3a, 0x5c,
+ 0xf3, 0x20, 0x16, 0x81, 0xad, 0xa2, 0x55, 0xc3, 0xd1, 0x50, 0x4f, 0x2c,
+ 0x81, 0xe7, 0x35, 0xde, 0xa6, 0x9f, 0xa1, 0x49, 0xf4, 0x90, 0x9b, 0x19,
+ 0x6a, 0xb9, 0x2c, 0x95, 0x61, 0x21, 0xdc, 0x3b, 0x37, 0x1d, 0x3f, 0xab,
+ 0x40, 0x64, 0x6a, 0x8d, 0x76, 0x34, 0x17, 0xa0, 0x72, 0x11, 0x3d, 0x32,
+ 0xbf, 0xa6, 0xf9, 0xf6, 0xb3, 0x9c, 0xda, 0xd5, 0x32, 0x89, 0xf5, 0xbf,
+ 0xb8, 0x65, 0xb4, 0x4c, 0x87, 0xfd, 0x14, 0x47, 0x82, 0x84, 0x96, 0xde,
+ 0x9e, 0xd1, 0x3e, 0x2d, 0xfb, 0x3e, 0x13, 0xcd, 0xee, 0xba, 0x51, 0x6e,
+ 0x0d, 0x68, 0x85, 0xdc, 0xe3, 0x9e, 0xfd, 0xd7, 0x7d, 0xb0, 0x94, 0xc1,
+ 0x70, 0x7c, 0x6d, 0x85, 0x3d, 0x02, 0xc7, 0x84, 0xd3, 0xae, 0x86, 0x34,
+ 0x38, 0xfe, 0x80, 0x1e, 0xad, 0xc6, 0xba, 0xf1, 0x5f, 0xe2, 0x3b, 0x86,
+ 0x48, 0x49, 0x0e, 0x30, 0x9b, 0x83, 0xff, 0xe5, 0x89, 0xf3, 0x5b, 0xb7,
+ 0x7e, 0x12, 0x3b, 0x25, 0x61, 0x85, 0xbd, 0xaf, 0xb8, 0xaf, 0x91, 0xd1,
+ 0x99, 0xa4, 0xcb, 0x6c, 0x92, 0x4a, 0xf4, 0x94, 0xbf, 0x91, 0xa6, 0xc1,
+ 0x63, 0x40, 0xa7, 0x09, 0x7d, 0x47, 0xd4, 0x36, 0x71, 0x27, 0x8c, 0x2d,
+ 0x78, 0xf7, 0x41, 0x5c, 0x8f, 0xbc, 0x31, 0xf6, 0x9e, 0xd6, 0xdd, 0x50,
+ 0x81, 0x3a, 0x02, 0xf7, 0x8d, 0x9a, 0xfd, 0xd5, 0x64, 0xac, 0x42, 0xa7,
+ 0x87, 0xbd, 0xf5, 0xf0, 0x27, 0x0f, 0xf0, 0x0e, 0x22, 0x8f, 0x9c, 0x30,
+ 0xdc, 0xdc, 0x06, 0x35, 0xa1, 0xcd, 0x10, 0xc5, 0x84, 0xb5, 0x3a, 0x3c,
+ 0x7c, 0xa3, 0xa7, 0xc3, 0x5e, 0xaf, 0x69, 0xf2, 0xa4, 0xab, 0x8a, 0xf7,
+ 0xf4, 0x69, 0xfe, 0x41, 0x90, 0x5f, 0xa8, 0xc9, 0x49, 0xc1, 0x6e, 0x5f,
+ 0x10, 0x3a, 0x80, 0x7d, 0x76, 0x04, 0x08, 0x2f, 0x9f, 0x53, 0xf6, 0x51,
+ 0xd6, 0x41, 0x83, 0xc9, 0x34, 0x11, 0x47, 0x53, 0xc7, 0xae, 0xd7, 0xc4,
+ 0xb6, 0x3e, 0x16, 0x16, 0x0f, 0x60, 0x7f, 0xf6, 0x2e, 0xaf, 0x4b, 0xd0,
+ 0xdc, 0xab, 0xfe, 0x70, 0xba, 0xb9, 0xc4, 0x81, 0xd0, 0xbc, 0x88, 0xfa,
+ 0x71, 0xeb, 0x1b, 0xfe, 0x9a, 0x70, 0x9d, 0xfb, 0xbd, 0x73, 0x79, 0x44,
+ 0xcf, 0x49, 0xed, 0x20, 0x47, 0x48, 0x61, 0x82, 0x0c, 0xf6, 0x0a, 0x12,
+ 0x16, 0xcb, 0x08, 0xcf, 0xc8, 0x26, 0x2c, 0x1a, 0x1e, 0x6d, 0xa2, 0x86,
+ 0x6a, 0x0e, 0x63, 0x98, 0xa8, 0xcf, 0xb0, 0x5b, 0x07, 0x1d, 0x78, 0x57,
+ 0xc6, 0x6a, 0x73, 0x03, 0xb5, 0xa4, 0x47, 0x6c, 0xe5, 0x3d, 0x6e, 0x82,
+ 0xdd, 0x41, 0xca, 0x4e, 0xe1, 0x28, 0x9b, 0x2b, 0x5f, 0xdf, 0xa2, 0xba,
+ 0x5c, 0xaf, 0xae, 0x0b, 0xe0, 0xbb, 0x13, 0x7e, 0x0e, 0x49, 0x85, 0x10,
+ 0x06, 0xdd, 0xe6, 0x8d, 0x1a, 0x82, 0x5f, 0xd6, 0xf8, 0x8f, 0x13, 0xa6,
+ 0x1e, 0x26, 0xc7, 0x78, 0x18, 0x3f, 0xe4, 0xe2, 0xae, 0x26, 0x6d, 0xf9,
+ 0xae, 0xcd, 0xc4, 0xe1, 0x9f, 0x1a, 0x80, 0x3a, 0x92, 0x5b, 0x37, 0x0a,
+ 0xae, 0xc5, 0xee, 0x43, 0xd8, 0x1e, 0x2d, 0xbc, 0x2a, 0xb2, 0x34, 0xfe,
+ 0xd4, 0x41, 0xb1, 0xe7, 0xc5, 0x0f, 0xb3, 0xbf, 0x3a, 0xc2, 0x2d, 0xe5,
+ 0x4e, 0xd3, 0x1c, 0x6c, 0x18, 0xc0, 0x66, 0x52, 0x47, 0x2d, 0x2f, 0xf3,
+ 0x6d, 0x99, 0xbe, 0xc9, 0x67, 0xfe, 0xe2, 0x32, 0xd9, 0xff, 0x91, 0xa7,
+ 0x44, 0x3c, 0xe2, 0xe4, 0xde, 0x06, 0xce, 0x1f, 0x11, 0x7b, 0xaf, 0x18,
+ 0x6f, 0xdc, 0xc4, 0xeb, 0x93, 0x01, 0x6f, 0xb7, 0x04, 0x00, 0x00, 0x00,
+ 0x01, 0x1e, 0x7f, 0x70, 0xdb, 0x9b, 0x10, 0x21, 0x28, 0x18, 0x37, 0xd8,
+ 0x0a, 0xdb, 0x5a, 0x3e, 0xcd, 0xd8, 0x44, 0xa7, 0xd5, 0x17, 0x2a, 0x9a,
+ 0x10, 0x1b, 0xd7, 0xa8, 0x87, 0x6a, 0x02, 0xe3, 0x5d, 0x00, 0xf7, 0x88,
+ 0x66, 0xc8, 0xb6, 0x8c, 0xd5, 0x1f, 0xb2, 0x6b, 0x0a, 0x6f, 0x86, 0xc8,
+ 0x9e, 0x0a, 0x2a, 0x6b, 0x89, 0xaf, 0x45, 0x6b, 0x0b, 0x5d, 0xa1, 0xc8,
+ 0xc2, 0x92, 0xd4, 0xc1, 0x4f, 0x2e, 0xf1, 0x40, 0x30, 0x0b, 0x20, 0xf2,
+ 0x26, 0x69, 0x65, 0x09, 0xe0, 0x93, 0xe8, 0x8c, 0xe0, 0x74, 0x0f, 0x1a,
+ 0x1c, 0x0d, 0xcd, 0xaa, 0x2d, 0xec, 0x81, 0xe5, 0x1d, 0x2b, 0x95, 0xae,
+ 0x92, 0x7a, 0x6b, 0x12, 0x10, 0xfa, 0xe2, 0x4b, 0xfa, 0x52, 0x31, 0x1e,
+ 0xb0, 0xff, 0x61, 0x44, 0xd9, 0x1f, 0xad, 0x26, 0x90, 0x98, 0x08, 0x24,
+ 0x01, 0xfb, 0x2e, 0xd5, 0xf7, 0xe9, 0x6d, 0x43, 0xa8, 0x08, 0x9c, 0x0b,
+ 0xb2, 0x45, 0xac, 0x10, 0xc5, 0xf0, 0xc5, 0x20, 0x15, 0xdf, 0x86, 0x63,
+ 0x34, 0xfe, 0xbb, 0x66, 0x5c, 0x02, 0xf2, 0x81, 0x8e, 0x75, 0x2d, 0x78,
+ 0x97, 0xfc, 0x7e, 0x39, 0x66, 0x66, 0xd8, 0x2c, 0xcd, 0x58, 0x87, 0x8f,
+ 0x1f, 0x20, 0x47, 0x2b, 0x35, 0x51, 0x66, 0x74, 0x39, 0xca, 0x1e, 0xac,
+ 0xf5, 0xc4, 0x06, 0x1b, 0xb3, 0x6e, 0x76, 0x33, 0x03, 0x07, 0x5b, 0x4d,
+ 0x82, 0x87, 0xd6, 0x09, 0x7b, 0x5a, 0x60, 0xb3, 0x31, 0x3a, 0xbe, 0xb1,
+ 0x6b, 0xf2, 0x3f, 0x7e, 0xa3, 0xec, 0x06, 0x12, 0xb6, 0x13, 0x91, 0x9f,
+ 0x3b, 0xd7, 0xe8, 0x2d, 0xa5, 0xf3, 0x23, 0xcb, 0x26, 0xe2, 0x8d, 0xca,
+ 0x19, 0x68, 0x33, 0x77, 0xbe, 0x62, 0x2e, 0x23, 0x15, 0x3b, 0x9c, 0x5f,
+ 0xe2, 0x6d, 0x9d, 0xb1, 0x1d, 0x9b, 0x8c, 0xf1, 0xd0, 0x76, 0xcf, 0x77,
+ 0xeb, 0x7f, 0x5d, 0xae, 0xb8, 0xca, 0x58, 0xdc, 0x52, 0xdd, 0xfd, 0x7b,
+ 0xbe, 0x04, 0x92, 0x81, 0x51, 0x1b, 0x53, 0x9c, 0x46, 0x9b, 0x89, 0x0c,
+ 0xbc, 0x2d, 0x85, 0x41, 0x30, 0xb0, 0x6e, 0x5c, 0xa1, 0x6f, 0xdf, 0x00,
+ 0x7f, 0x6e, 0x24, 0x86, 0xc7, 0x8c, 0x85, 0xaa, 0xf1, 0x46, 0xbe, 0x07,
+ 0xde, 0x2d, 0x72, 0x14, 0xfd, 0xf9, 0x68, 0x60, 0x7b, 0xb4, 0x54, 0xe2,
+ 0x09, 0x07, 0x8a, 0x65, 0x5a, 0xf0, 0x83, 0xe1, 0x8c, 0x92, 0xc7, 0x20,
+ 0x6e, 0xdb, 0xb5, 0x11, 0xac, 0x21, 0x4c, 0xe7, 0xb6, 0xd3, 0x6e, 0xb9,
+ 0x7e, 0x85, 0x55, 0xd2, 0x79, 0xf5, 0x44, 0x67, 0x1b, 0x50, 0x87, 0xa7,
+ 0xac, 0x0a, 0x6c, 0x1d, 0x21, 0x03, 0x1a, 0xf0, 0x2a, 0x54, 0x0a, 0xc2,
+ 0x4e, 0x33, 0xca, 0x32, 0xf9, 0xca, 0x47, 0xc9, 0x90, 0x79, 0xc8, 0xbd,
+ 0xf1, 0x78, 0x24, 0xfa, 0xb8, 0x3c, 0xe8, 0xce, 0x6c, 0x00, 0xc9, 0x6d,
+ 0x72, 0xfa, 0x3d, 0x10, 0x7e, 0x63, 0x1b, 0x94, 0x54, 0xf1, 0x53, 0x9a,
+ 0xdc, 0x09, 0x89, 0xc7, 0x70, 0x5c, 0xf2, 0xb0, 0x1b, 0x1b, 0xb8, 0xfe,
+ 0xb9, 0x41, 0x10, 0x55, 0x8b, 0xdf, 0x97, 0x31, 0x11, 0xc7, 0x89, 0xd0,
+ 0x21, 0x40, 0x7b, 0x95, 0x84, 0x2e, 0x7a, 0x33, 0x8c, 0xb8, 0x85, 0xd1,
+ 0xff, 0xf3, 0x70, 0x44, 0x42, 0x46, 0x90, 0xba, 0xe9, 0xc8, 0xb6, 0x78,
+ 0xed, 0x9f, 0xfe, 0xab, 0x34, 0x8a, 0x19, 0x3a, 0xd5, 0x90, 0xf1, 0x5c,
+ 0x31, 0x36, 0x9d, 0x60, 0x97, 0xd5, 0x90, 0x72, 0x0c, 0xef, 0xe2, 0xc0,
+ 0xb4, 0xcf, 0x59, 0x76, 0xcf, 0xbf, 0x3b, 0xf0, 0x72, 0x5c, 0xee, 0x5b,
+ 0x5b, 0x9f, 0xeb, 0x8f, 0x09, 0xec, 0xe6, 0x82, 0x85, 0x82, 0xe2, 0x88,
+ 0xd6, 0xbd, 0xe5, 0x8e, 0x7f, 0x62, 0x8d, 0x8f, 0x5c, 0x15, 0x2b, 0x2d,
+ 0xf2, 0x96, 0xf1, 0x9b, 0x50, 0x61, 0xac, 0x22, 0x1b, 0xf7, 0xbb, 0xf2,
+ 0xda, 0xd2, 0x1f, 0xbb, 0x9a, 0x6a, 0x8c, 0xaa, 0x8e, 0xba, 0xdb, 0x3f,
+ 0xe2, 0xdc, 0x52, 0xee, 0xea, 0x79, 0x22, 0x2b, 0x9c, 0x04, 0x84, 0x05,
+ 0xc8, 0x99, 0x9e, 0x0e, 0x39, 0xcc, 0xb9, 0x3d, 0x15, 0x39, 0x43, 0x26,
+ 0xb5, 0xf7, 0x68, 0x26, 0x61, 0x04, 0xeb, 0x6d, 0x9a, 0x89, 0xcc, 0x2a,
+ 0xff, 0x40, 0x7c, 0xd7, 0xac, 0x78, 0xa5, 0xb5, 0x02, 0xa1, 0x83, 0x7b,
+ 0x6a, 0x2b, 0x1e, 0xdd, 0x32, 0x50, 0x98, 0x27, 0x4e, 0x9c, 0xc0, 0x36,
+ 0xc2, 0x7f, 0x31, 0x9d, 0x74, 0x1c, 0xea, 0xff, 0xea, 0x6d, 0xda, 0xe8,
+ 0x99, 0x7a, 0x7f, 0xe5, 0xb0, 0xe3, 0xd6, 0xcd, 0x68, 0x3b, 0x3b, 0xf5,
+ 0xeb, 0x80, 0x25, 0x23, 0x8d, 0x85, 0xd4, 0x7a, 0xaa, 0x2e, 0xdf, 0xab,
+ 0xb0, 0x6b, 0x2c, 0x35, 0x18, 0x76, 0xe7, 0x18, 0x63, 0xa6, 0x64, 0x9e,
+ 0x13, 0x7d, 0xe0, 0xc0, 0x80, 0xcf, 0x6f, 0x35, 0xa6, 0xf8, 0x3c, 0xef,
+ 0x65, 0xf4, 0xbf, 0xe4, 0x00, 0x00, 0x7b, 0x4a, 0xe9, 0xa6, 0x54, 0x71,
+ 0x9f, 0xfa, 0x88, 0xdf, 0xc1, 0x28, 0x7f, 0x39, 0x74, 0x9d, 0xdd, 0x4c,
+ 0x2d, 0xf2, 0x3c, 0x72, 0x8c, 0x4f, 0x2d, 0xe2, 0x86, 0xa0, 0xee, 0x3d,
+ 0xed, 0xac, 0x41, 0xbe, 0xa2, 0xda, 0xde, 0xdb, 0x2a, 0xa1, 0x53, 0xb2,
+ 0x04, 0xd8, 0xdd, 0xa7, 0xeb, 0x96, 0x2d, 0xd5, 0xc9, 0xd9, 0x1f, 0x46,
+ 0x15, 0x30, 0x53, 0xed, 0x49, 0x2d, 0xd1, 0xc1, 0x67, 0x24, 0x41, 0x01,
+ 0xd1, 0x4b, 0xb4, 0x6d, 0xb1, 0x61, 0x9b, 0xf9, 0x50, 0xd1, 0xcb, 0xde,
+ 0x2d, 0x8c, 0x99, 0xa8, 0x77, 0xc1, 0xee, 0xc3, 0x73, 0xc0, 0x37, 0x5f,
+ 0x80, 0x2c, 0x62, 0xd2, 0xf0, 0x2c, 0x09, 0xfd, 0x1b, 0xc6, 0x46, 0xc1,
+ 0xf7, 0xb0, 0x03, 0x70, 0x9f, 0x86, 0x2c, 0xe6, 0x12, 0x9a, 0xec, 0x4a,
+ 0x11, 0x98, 0x5d, 0x21, 0x1e, 0xc8, 0x07, 0x08, 0x98, 0x81, 0x91, 0x0f,
+ 0x60, 0xe6, 0x52, 0x44, 0x62, 0x25, 0xfb, 0x89, 0x3e, 0xb4, 0xff, 0x6e,
+ 0x45, 0xbb, 0x00, 0x5d, 0xbb, 0x07, 0x58, 0xdc, 0x76, 0x88, 0x34, 0xb6,
+ 0x23, 0x28, 0xe1, 0x0f, 0x50, 0xb1, 0xee, 0xef, 0x87, 0xae, 0xd6, 0x08,
+ 0x62, 0x47, 0xfc, 0x7a, 0x05, 0xfd, 0xe3, 0x3d, 0x3f, 0xd3, 0x91, 0xd6,
+ 0xfb, 0x21, 0xd9, 0xad, 0xad, 0x34, 0x01, 0x41, 0xda, 0xab, 0xd8, 0xbf,
+ 0x81, 0x08, 0x8a, 0x14, 0x83, 0x3b, 0xc1, 0xb8, 0xc5, 0xed, 0xa4, 0xab,
+ 0x72, 0x12, 0xc4, 0xd3, 0x1e, 0xc6, 0xdf, 0x07, 0xf8, 0xd7, 0x9f, 0x38,
+ 0x4f, 0xca, 0x2f, 0xc3, 0x8d, 0x28, 0x80, 0x52, 0xac, 0x4d, 0x59, 0x35,
+ 0x7c, 0x63, 0x1a, 0x49, 0xc9, 0x29, 0xcc, 0xed, 0xab, 0xfc, 0x4d, 0xae,
+ 0xc4, 0x4f, 0x8c, 0x91, 0x8b, 0x64, 0x1a, 0xdb, 0xdd, 0xcc, 0x05, 0xfb,
+ 0x8e, 0x15, 0x7e, 0x56, 0x0c, 0x6f, 0x17, 0x6e, 0xc7, 0xbe, 0x45, 0x8d,
+ 0xa0, 0xb5, 0xe1, 0x55, 0xe4, 0x4d, 0xd6, 0xb5, 0xb0, 0x6d, 0xd9, 0xb0,
+ 0x9c, 0xe4, 0xbe, 0x47, 0xa9, 0xfd, 0x08, 0x6c, 0x86, 0x50, 0x5e, 0x92,
+ 0xdb, 0xcd, 0xc3, 0xd1, 0xeb, 0x22, 0xf9, 0x7e, 0x86, 0x83, 0x05, 0xb0,
+ 0x2f, 0xd2, 0xe5, 0x9a, 0xbd, 0xf7, 0x3d, 0x67, 0x6a, 0x74, 0x3d, 0xd8,
+ 0xb8, 0xd2, 0x27, 0xe8, 0x72, 0xc9, 0xf0, 0xbf, 0xbd, 0xc8, 0x66, 0x7d,
+ 0x37, 0x40, 0x5c, 0x44, 0xf1, 0xf1, 0x17, 0xfe, 0x38, 0x4b, 0x55, 0x76,
+ 0xff, 0xea, 0xf9, 0xc4, 0x9b, 0x12, 0xca, 0x31, 0x90, 0x4c, 0x36, 0xc8,
+ 0x80, 0xf1, 0x3f, 0x94, 0x84, 0xca, 0x1a, 0xf3, 0xc6, 0xba, 0x76, 0x08,
+ 0x89, 0x38, 0x26, 0x92, 0x46, 0x00, 0xd2, 0x32, 0x91, 0x3e, 0x8d, 0x3e,
+ 0xa7, 0x39, 0xba, 0x6d, 0x8c, 0x20, 0xfe, 0xcc, 0x40, 0xee, 0xa7, 0xd8,
+ 0x5d, 0x93, 0xc9, 0x17, 0x13, 0x43, 0x5c, 0x87, 0xf5, 0x92, 0x2b, 0x8a,
+ 0x82, 0xd3, 0xbb, 0x80, 0x76, 0xd3, 0x13, 0x83, 0x65, 0x4e, 0x7d, 0x0b,
+ 0x37, 0xbb, 0xb2, 0x73, 0x8d, 0x4c, 0x54, 0x89, 0xd7, 0x5e, 0x6c, 0xc5,
+ 0x95, 0xa9, 0x15, 0x01, 0x85, 0x41, 0x69, 0x77, 0x0a, 0x52, 0x8a, 0x23,
+ 0xae, 0xf8, 0xd7, 0x26, 0x17, 0x02, 0xd8, 0xc0, 0x9c, 0x6b, 0x7a, 0x6f,
+ 0x90, 0x6d, 0x52, 0xb6, 0x80, 0xfc, 0x80, 0x01, 0x72, 0x5c, 0x4b, 0xab,
+ 0x70, 0x48, 0x73, 0xb8, 0x54, 0x2f, 0x9b, 0xbb, 0x97, 0xc7, 0xd8, 0x55,
+ 0xa9, 0x5a, 0x7d, 0x17, 0xd8, 0xf5, 0xb0, 0x2d, 0x82, 0x25, 0x7c, 0xad,
+ 0xfd, 0x59, 0x31, 0x60, 0xb5, 0xd1, 0xee, 0x9f, 0x2a, 0xe8, 0xd6, 0xc7,
+ 0x84, 0x3d, 0xd4, 0x42, 0x36, 0x99, 0x1e, 0xcf, 0xf1, 0x41, 0xa5, 0x6a,
+ 0x8e, 0x3b, 0x32, 0xe3, 0xcd, 0xf7, 0x27, 0x79, 0x82, 0x2e, 0x0f, 0xb2,
+ 0xf9, 0x8f, 0xc9, 0xa0, 0x47, 0xb2, 0x4d, 0x02, 0xa5, 0x8c, 0x33, 0x38,
+ 0xd2, 0x29, 0xbc, 0x69, 0xb5, 0xb7, 0x4e, 0xed, 0x60, 0x1a, 0x13, 0x7c,
+ 0xa1, 0x31, 0x66, 0x08, 0x37, 0xf8, 0x72, 0x01, 0xd1, 0xc9, 0xdc, 0xac,
+ 0x1e, 0xea, 0x03, 0x4a, 0xf8, 0x74, 0x92, 0xee, 0xa0, 0x69, 0x26, 0xaf,
+ 0x93, 0xf1, 0x05, 0xef, 0x38, 0x2f, 0xa4, 0xa6, 0x4c, 0x5d, 0xce, 0xfb,
+ 0x9f, 0x74, 0xd7, 0x9c, 0xcc, 0x81, 0x96, 0x6a, 0x53, 0x2c, 0x34, 0x66,
+ 0x9a, 0x44, 0xde, 0x7d, 0x35, 0x14, 0x9e, 0xe7, 0x80, 0x91, 0xf2, 0x9a,
+ 0x08, 0x7e, 0x28, 0xd1, 0x9c, 0x28, 0xd2, 0x9b, 0x63, 0x54, 0xce, 0xe7,
+ 0xd9, 0xb2, 0xf1, 0xc7, 0x49, 0x50, 0xcf, 0x59, 0xa6, 0xb6, 0x73, 0xdd,
+ 0x9b, 0xb5, 0x93, 0xa3, 0x27, 0x6f, 0x20, 0x95, 0x2f, 0x2e, 0xff, 0x29,
+ 0xef, 0x0b, 0x68, 0xce, 0x18, 0x5f, 0x11, 0x02, 0xb5, 0x2f, 0x22, 0x8b,
+ 0x25, 0x17, 0x29, 0xc3, 0x62, 0x68, 0x15, 0xb2, 0xec, 0x95, 0xf1, 0xd3,
+ 0x5a, 0xc8, 0x0c, 0x31, 0x9c, 0x1d, 0x8a, 0x44, 0xe5, 0xb5, 0x63, 0x8e,
+ 0xd7, 0x85, 0x93, 0x32, 0x0c, 0x53, 0xbc, 0x00, 0x80, 0xf9, 0x73, 0x41,
+ 0x2b, 0x44, 0xee, 0xbe, 0xbc, 0x66, 0x88, 0xcb, 0xf7, 0x40, 0xb6, 0x39,
+ 0xd3, 0xbe, 0xe0, 0x2a, 0x9b, 0x08, 0xdc, 0xa2, 0x91, 0x7c, 0xed, 0x50,
+ 0xdb, 0x14, 0x42, 0xbb, 0xc8, 0xce, 0x69, 0xe3, 0x99, 0x4e, 0x3e, 0x19,
+ 0x77, 0x98, 0xac, 0xce, 0x81, 0xac, 0xad, 0x59, 0x37, 0x10, 0x79, 0x7e,
+ 0x5e, 0x8a, 0x26, 0x71, 0x9c, 0xe6, 0xf1, 0xca, 0x7c, 0xf9, 0xb7, 0x5c,
+ 0xb7, 0x4b, 0x4e, 0xcd, 0x73, 0x96, 0x10, 0xb2, 0x7e, 0x66, 0xe2, 0x6a,
+ 0x65, 0x8e, 0x42, 0x23, 0xea, 0xba, 0x16, 0x7e, 0x1e, 0x41, 0x42, 0xbe,
+ 0x6e, 0xab, 0xb7, 0x90, 0x90, 0x8d, 0xee, 0x5e, 0xfa, 0xee, 0x3b, 0x6d,
+ 0x50, 0xde, 0x62, 0x5f, 0x22, 0x66, 0x34, 0x65, 0x24, 0x32, 0xe1, 0x9f,
+ 0x2e, 0x9d, 0x42, 0x04, 0xe0, 0x23, 0x12, 0x35, 0x30, 0xcf, 0x40, 0x9a,
+ 0x99, 0xda, 0xda, 0xbc, 0xe5, 0xfc, 0xb4, 0x81, 0x05, 0xe8, 0x70, 0xda,
+ 0x8b, 0x83, 0xae, 0xf7, 0xc4, 0xaf, 0x1a, 0x41, 0x7d, 0xf5, 0x49, 0xc3,
+ 0x9c, 0x28, 0x0f, 0x37, 0xd8, 0xee, 0x49, 0x86, 0x97, 0x24, 0xc1, 0xd7,
+ 0x8c, 0x7b, 0xcf, 0x95, 0xea, 0xcd, 0xe2, 0x26, 0xca, 0x89, 0xa2, 0xfa,
+ 0xa5, 0xc5, 0x83, 0x16, 0xf4, 0x67, 0xea, 0x64, 0x82, 0x2f, 0x6e, 0xab,
+ 0x18, 0x77, 0xdd, 0xbb, 0x93, 0x8f, 0xa8, 0x0f, 0x93, 0xed, 0x13, 0x48,
+ 0x37, 0xc7, 0x1e, 0x2c, 0x59, 0x8b, 0xf8, 0x53, 0x17, 0x0c, 0xbf, 0xda,
+ 0x02, 0x33, 0x1a, 0x30, 0x3d, 0x22, 0x99, 0x19, 0x51, 0x91, 0xf3, 0x68,
+ 0x24, 0x38, 0x7e, 0x68, 0x3e, 0x32, 0xa3, 0x28, 0x97, 0xdf, 0x07, 0x67,
+ 0x34, 0xa1, 0x5d, 0x29, 0xfb, 0x41, 0x6c, 0x5d, 0xc0, 0x67, 0xc4, 0xfa,
+ 0xb3, 0x94, 0x88, 0x7a, 0xa5, 0xec, 0x6a, 0x69, 0x36, 0x52, 0x19, 0xa8,
+ 0xb7, 0x89, 0x01, 0xce, 0x9b, 0x1c, 0xcf, 0x5e, 0x9e, 0x52, 0x33, 0x75,
+ 0x47, 0x8f, 0xc6, 0xbb, 0xa2, 0x08, 0x35, 0xd3, 0x92, 0x8b, 0xf9, 0x3b,
+ 0xd5, 0xbd, 0xa5, 0x2e, 0x11, 0x9b, 0x7c, 0x65, 0xcd, 0xfe, 0xc7, 0x2f,
+ 0xd2, 0xd4, 0x00, 0x9c, 0x63, 0x11, 0xc8, 0x7d, 0x7a, 0xa2, 0x5a, 0x82,
+ 0x2b, 0x01, 0xf1, 0xa9, 0xb5, 0x1c, 0xad, 0x50, 0x8b, 0x6b, 0x16, 0x42,
+ 0x7b, 0xd8, 0x99, 0x6c, 0xf3, 0x0f, 0xe9, 0xfb, 0xdf, 0xf5, 0xb2, 0x16,
+ 0x7b, 0x2a, 0xba, 0x2c, 0x49, 0x81, 0x1e, 0x2e, 0x04, 0x01, 0xc2, 0x07,
+ 0xd3, 0x54, 0x84, 0x81, 0xdc, 0x6f, 0xfb, 0x28, 0x07, 0x4f, 0x56, 0x1e,
+ 0x31, 0x68, 0x33, 0x9a, 0xed, 0xbf, 0x69, 0x31, 0x74, 0x28, 0x46, 0xf8,
+ 0x96, 0xef, 0xe5, 0x52, 0x1e, 0xc5, 0x21, 0x86, 0x63, 0x42, 0xff, 0xae,
+ 0x36, 0x79, 0xf4, 0x9c, 0x2d, 0x60, 0xc5, 0xe5, 0x9d, 0x9d, 0x32, 0x1c,
+ 0x82, 0x6b, 0xdc, 0x35, 0x39, 0x94, 0x31, 0xb4, 0xd0, 0x45, 0x16, 0xfe,
+ 0xe3, 0x3e, 0x77, 0x28, 0x3c, 0x3d, 0xc1, 0x41, 0x21, 0x4c, 0x42, 0xb3,
+ 0xa3, 0x3c, 0xf7, 0x33, 0x94, 0x7d, 0xf3, 0xe6, 0x87, 0x05, 0x98, 0xed,
+ 0xac, 0x6c, 0xb3, 0xb0, 0xb0, 0x73, 0x3d, 0x12, 0xda, 0x84, 0x7d, 0x9b,
+ 0x8f, 0x83, 0x40, 0xf7, 0x39, 0x6c, 0x38, 0x6c, 0x67, 0x4e, 0x8f, 0xbd,
+ 0xe5, 0xe3, 0x57, 0x6b, 0xa6, 0x5f, 0xcb, 0xe1, 0xa8, 0x15, 0x3e, 0xdc,
+ 0x67, 0xc5, 0x97, 0xc7, 0xe5, 0xe1, 0xf0, 0x80, 0xa0, 0x59, 0xae, 0x0e,
+ 0x9d, 0xd4, 0x70, 0x5c, 0x9c, 0x81, 0x5a, 0x98, 0x06, 0xf5, 0x72, 0x04,
+ 0x5c, 0xfc, 0x48, 0x1f, 0xf4, 0x73, 0x88, 0xae, 0x3e, 0xd6, 0x49, 0x47,
+ 0x00, 0x57, 0xbe, 0x1c, 0xcf, 0x72, 0x3d, 0xc5, 0xc2, 0xea, 0x60, 0x37,
+ 0xa2, 0xf6, 0x33, 0xdb, 0x27, 0xc3, 0x5a, 0xa7, 0x5a, 0xb5, 0x54, 0x44,
+ 0xd3, 0x25, 0xde, 0x21, 0x0d, 0x96, 0xda, 0x16, 0x9d, 0xde, 0x8e, 0xc1,
+ 0xf7, 0x1c, 0x6c, 0xc5, 0x4e, 0x6e, 0xae, 0xad, 0x4a, 0x52, 0x94, 0x67,
+ 0xf9, 0x85, 0xaf, 0x92, 0xf9, 0x30, 0x20, 0xaa, 0xfe, 0x83, 0x26, 0x1d,
+ 0xba, 0x3e, 0x45, 0x01, 0xcf, 0x99, 0xdd, 0xdd, 0x4d, 0x76, 0xd5, 0x63,
+ 0x88, 0x18, 0x79, 0xf3, 0xa4, 0x0b, 0xbf, 0xf6, 0x1c, 0x6e, 0x8f, 0x7e,
+ 0x20, 0x95, 0xc4, 0x9e, 0xf9, 0x94, 0x0d, 0x00, 0xc6, 0xb7, 0xb2, 0x27,
+ 0xa7, 0x90, 0x45, 0x50, 0x82, 0xac, 0xfa, 0xb9, 0x04, 0x66, 0xfc, 0xb8,
+ 0xaf, 0x8c, 0x80, 0x5b, 0xd5, 0xd7, 0xad, 0x15, 0x50, 0xa8, 0xa5, 0x7c,
+ 0x0a, 0x3d, 0x6b, 0x8a, 0xc6, 0x7f, 0xc2, 0xb5, 0x74, 0x2b, 0x00, 0xa8,
+ 0x38, 0x5d, 0x78, 0x11, 0x85, 0xc7, 0x71, 0x83, 0xd0, 0xc7, 0x0a, 0x56,
+ 0x95, 0x47, 0x05, 0x76, 0x81, 0xeb, 0xcd, 0x8e, 0xab, 0xd4, 0xcc, 0xc8,
+ 0xef, 0xaf, 0xe9, 0x89, 0xb8, 0x81, 0xc5, 0xf0, 0x8a, 0xf1, 0x0a, 0x3f,
+ 0x85, 0x2e, 0x9d, 0x43, 0x15, 0x21, 0x5c, 0x1c, 0xcf, 0x6b, 0x97, 0x92,
+ 0x4d, 0xa5, 0xfd, 0x3f, 0xcf, 0x26, 0x48, 0x81, 0x99, 0xb8, 0x15, 0x9f,
+ 0x4f, 0x08, 0x02, 0xa8, 0xf8, 0x3a, 0x48, 0x56, 0x20, 0x7b, 0x3a, 0xde,
+ 0xfc, 0x40, 0x22, 0x9e, 0x65, 0xc1, 0xcc, 0x1f, 0x22, 0xc2, 0x13, 0x84,
+ 0xb3, 0x8e, 0x37, 0x41, 0x5d, 0x96, 0xbc, 0x53, 0x3e, 0x8a, 0x75, 0xc6,
+ 0x2d, 0xb7, 0x48, 0x43, 0xe9, 0xb4, 0xb9, 0x02, 0x1f, 0x8e, 0x4e, 0xbb,
+ 0xd0, 0x3c, 0x06, 0x6d, 0xdb, 0xe1, 0x9b, 0x89, 0xe2, 0x76, 0xfd, 0x27,
+ 0x09, 0x12, 0x73, 0x50, 0x08, 0xe8, 0xc9, 0xbd, 0x56, 0x80, 0x8a, 0xad,
+ 0xf4, 0x72, 0xac, 0xa0, 0xfa, 0x74, 0x6c, 0x30, 0x31, 0x02, 0xca, 0x59,
+ 0xb2, 0x35, 0xdc, 0x4e, 0xd1, 0xc8, 0x29, 0xf0, 0x0f, 0xd5, 0xc3, 0xdf,
+ 0x60, 0xc9, 0x5a, 0x97, 0x30, 0xff, 0x94, 0xc5, 0x4d, 0x94, 0x44, 0x28,
+ 0x9d, 0x45, 0x94, 0x02, 0x5e, 0xfa, 0x3d, 0x37, 0x03, 0x0d, 0xc9, 0x96,
+ 0x37, 0xa4, 0x1b, 0x1f, 0x10, 0xa5, 0xce, 0x76, 0xf1, 0xc0, 0x08, 0x33,
+ 0x85, 0xbd, 0xe9, 0xf2, 0x65, 0x15, 0x58, 0xf8, 0x10, 0x18, 0xe6, 0xe3,
+ 0x2d, 0x07, 0x7e, 0x5d, 0x37, 0x1a, 0x2b, 0x38, 0xd4, 0x8d, 0x07, 0xc3,
+ 0x7a, 0x47, 0xd7, 0x96, 0x8e, 0x0e, 0x5e, 0xb3, 0x13, 0x8c, 0xc8, 0x2d,
+ 0x0b, 0xc9, 0x9e, 0x92, 0xfe, 0x81, 0x24, 0x94, 0xe1, 0x6f, 0x86, 0xb9,
+ 0xdb, 0x09, 0xaa, 0xf1, 0x3a, 0x75, 0x49, 0x00, 0xa3, 0xf8, 0x95, 0xdb,
+ 0x45, 0xad, 0xe1, 0x15, 0x2a, 0x61, 0x9d, 0x83, 0x25, 0xcd, 0xb9, 0x78,
+ 0x97, 0x00, 0x3f, 0xa1, 0x26, 0x83, 0x61, 0xaf, 0x29, 0xbf, 0x92, 0xde,
+ 0xbe, 0xf9, 0xbd, 0x91, 0x01, 0xf3, 0x97, 0xd7, 0x7f, 0xb5, 0xab, 0x51,
+ 0x18, 0x2b, 0x44, 0xb9, 0xf1, 0x73, 0x69, 0x3a, 0x89, 0xef, 0x4a, 0x90,
+ 0x3d, 0x28, 0x55, 0xfe, 0xc4, 0x3f, 0xbf, 0xa9, 0x6b, 0x25, 0x5a, 0xbc,
+ 0x9a, 0x56, 0x94, 0xb1, 0x81, 0x47, 0x0a, 0xb0, 0x89, 0x4d, 0x93, 0x89,
+ 0x83, 0xe5, 0x55, 0x39, 0xec, 0x4d, 0x6e, 0xd2, 0xb1, 0xce, 0xbe, 0x79,
+ 0xab, 0x49, 0xe7, 0xd5, 0x36, 0x43, 0x74, 0xb8, 0x68, 0x46, 0x3b, 0x09,
+ 0x83, 0xd1, 0x00, 0x00, 0xa3, 0x4e, 0xf9, 0x81, 0x00, 0xa7, 0x00, 0x86,
+ 0x00, 0x40, 0x96, 0xf0, 0x01, 0x17, 0x86, 0x00, 0x06, 0x70, 0x67, 0xbe,
+ 0x60, 0xf8, 0x00, 0x00, 0x00, 0x05, 0x05, 0x7c, 0x17, 0x3f, 0x4b, 0x06,
+ 0x6b, 0x82, 0xef, 0x6a, 0xef, 0x50, 0x9b, 0x12, 0x16, 0x71, 0x2d, 0x4f,
+ 0x23, 0xf4, 0xe9, 0x34, 0x4f, 0x5a, 0x6b, 0x04, 0xf5, 0x73, 0x96, 0x6c,
+ 0xc6, 0x70, 0x9c, 0xa9, 0x3c, 0xff, 0xf5, 0xe5, 0x36, 0x17, 0xd8, 0x4f,
+ 0x56, 0xd5, 0x99, 0xff, 0x62, 0x92, 0xef, 0x1d, 0x98, 0xab, 0x94, 0x50,
+ 0xfe, 0xdf, 0x70, 0xb7, 0x15, 0x40, 0x1f, 0x0a, 0x6b, 0x5e, 0xc4, 0xd8,
+ 0xba, 0x84, 0xb1, 0xe6, 0x44, 0x1e, 0xd5, 0x26, 0xc6, 0xde, 0xc5, 0xd4,
+ 0x68, 0xac, 0xb0, 0x37, 0xdb, 0x05, 0x02, 0x98, 0x08, 0xc3, 0xd2, 0xdc,
+ 0xc5, 0xe2, 0x5d, 0x9e, 0x13, 0xae, 0x56, 0x3d, 0xae, 0x21, 0xf8, 0x23,
+ 0xb6, 0x84, 0x5a, 0x0c, 0xf0, 0x79, 0x13, 0x9e, 0x24, 0x0e, 0x3d, 0xb6,
+ 0x2f, 0x4c, 0xc4, 0x30, 0x41, 0xa5, 0x56, 0x58, 0x47, 0x49, 0xe8, 0x42,
+ 0x74, 0xa5, 0xfb, 0x97, 0xfe, 0xfc, 0x2e, 0x8b, 0x07, 0x98, 0x36, 0x49,
+ 0x5d, 0x11, 0x88, 0xba, 0x23, 0xb4, 0x46, 0x8c, 0x42, 0xaf, 0xfb, 0xfb,
+ 0x8b, 0xcd, 0xd9, 0x90, 0xaa, 0xa0, 0x81, 0x37, 0x60, 0x02, 0xea, 0x5b,
+ 0xf2, 0xcb, 0xcb, 0x57, 0xaa, 0x07, 0x95, 0x06, 0x5e, 0xf6, 0xad, 0x34,
+ 0x53, 0xca, 0x3c, 0x73, 0xa9, 0x95, 0xa8, 0x4e, 0x27, 0xac, 0x26, 0x3a,
+ 0xed, 0xe5, 0x5e, 0xac, 0xdf, 0x91, 0xfc, 0x47, 0x1f, 0x33, 0xfd, 0xa9,
+ 0x3b, 0xe7, 0x21, 0x65, 0xb5, 0x90, 0xc7, 0xf0, 0xf6, 0x49, 0x8b, 0x0f,
+ 0xae, 0x4c, 0xf9, 0x5b, 0x68, 0x79, 0xd4, 0xb4, 0xff, 0x5d, 0x0e, 0xc1,
+ 0x71, 0x59, 0x6b, 0x9f, 0x27, 0x2e, 0x97, 0xf2, 0x08, 0xda, 0xe7, 0xe0,
+ 0xa3, 0x6a, 0x3f, 0xd6, 0x83, 0x5b, 0xc4, 0x88, 0x30, 0x5b, 0x17, 0xe7,
+ 0xbc, 0x78, 0x66, 0x57, 0x14, 0xcb, 0x8b, 0x73, 0x36, 0x73, 0xfe, 0xd4,
+ 0x13, 0x40, 0xcc, 0x18, 0xe9, 0xcb, 0x3b, 0x82, 0x0f, 0x50, 0xaa, 0x2f,
+ 0x45, 0x10, 0x15, 0x29, 0xb5, 0x52, 0xa7, 0xd5, 0x33, 0xcf, 0x72, 0xb2,
+ 0xbc, 0xf9, 0x15, 0xbb, 0xf1, 0xf2, 0x54, 0x3b, 0x5c, 0xc9, 0xd2, 0x94,
+ 0x4c, 0x7f, 0x2a, 0xd1, 0xd8, 0x70, 0xb6, 0x6b, 0x39, 0x5c, 0x91, 0x46,
+ 0x28, 0x17, 0xb1, 0x2b, 0x8e, 0xdc, 0xbb, 0xce, 0xc3, 0x8d, 0x72, 0x5a,
+ 0xb0, 0xb1, 0x6a, 0xa7, 0x84, 0x77, 0xed, 0xdd, 0x7e, 0x49, 0xce, 0x63,
+ 0x5d, 0xb5, 0x7c, 0xe9, 0x59, 0xbb, 0xfc, 0xe8, 0x9e, 0x68, 0x6b, 0x1b,
+ 0x8d, 0xaa, 0x33, 0xa1, 0xc0, 0x60, 0x03, 0xe4, 0xac, 0xf8, 0xa9, 0xe0,
+ 0x43, 0xc5, 0x66, 0x49, 0xe4, 0x83, 0x63, 0xc3, 0xe0, 0x78, 0xe1, 0xe6,
+ 0x1e, 0x47, 0xbd, 0xb0, 0xeb, 0x32, 0xa9, 0x7f, 0x8d, 0xdb, 0xe3, 0xce,
+ 0xab, 0x79, 0x71, 0x8e, 0x3b, 0x84, 0xc4, 0xcd, 0x33, 0x83, 0xa5, 0x20,
+ 0x0b, 0xeb, 0xcd, 0xcb, 0xb6, 0xf2, 0x50, 0x11, 0x5e, 0x68, 0x46, 0xa2,
+ 0x9a, 0x55, 0xc3, 0x0a, 0x7f, 0xe9, 0x49, 0x70, 0x77, 0xab, 0x81, 0x5b,
+ 0xe4, 0x9e, 0x95, 0x9a, 0xa8, 0xb3, 0xa4, 0x84, 0xcc, 0x02, 0x47, 0xfb,
+ 0x1b, 0x04, 0x5f, 0x73, 0x25, 0xea, 0xed, 0x54, 0xf5, 0x1b, 0x44, 0x54,
+ 0x4d, 0x68, 0x01, 0x4d, 0x7a, 0xc3, 0x13, 0x31, 0x85, 0xf5, 0x10, 0xca,
+ 0x57, 0xab, 0x36, 0x98, 0x53, 0xc2, 0x14, 0x45, 0x73, 0x4f, 0xb7, 0xac,
+ 0xd1, 0x6f, 0x3c, 0x1e, 0xa4, 0x92, 0x11, 0x2e, 0xe9, 0x0c, 0xa1, 0x25,
+ 0xf0, 0xab, 0x70, 0x00, 0xe6, 0x67, 0xf5, 0xbd, 0x52, 0x3d, 0x63, 0xa7,
+ 0xef, 0x36, 0x98, 0x9b, 0x04, 0xfc, 0x72, 0x73, 0x49, 0x2c, 0x38, 0x19,
+ 0xb9, 0xac, 0x71, 0x1d, 0xd9, 0x68, 0xd6, 0x0c, 0x68, 0xc3, 0xf3, 0xfa,
+ 0xad, 0xc4, 0xcd, 0x94, 0x30, 0x2f, 0x31, 0xfe, 0x7b, 0xfd, 0x7d, 0x34,
+ 0x79, 0xa9, 0x5f, 0x60, 0x28, 0xf4, 0xa3, 0xfc, 0xdc, 0x60, 0x1a, 0x82,
+ 0xbe, 0x6d, 0x26, 0xb9, 0x4b, 0x29, 0xec, 0x8c, 0x83, 0x5c, 0x06, 0x8a,
+ 0x5d, 0xd4, 0x34, 0xf0, 0x4e, 0x53, 0xe9, 0xfe, 0xdc, 0x93, 0x2a, 0xff,
+ 0x05, 0x4b, 0x06, 0x94, 0x59, 0xad, 0x5d, 0xdf, 0x87, 0x3e, 0x7d, 0xf2,
+ 0xb8, 0x96, 0x05, 0x3c, 0x07, 0xf9, 0xfa, 0xb5, 0x1f, 0x9c, 0x64, 0xbb,
+ 0x61, 0x8a, 0xea, 0x22, 0x3f, 0xbb, 0x1c, 0x6d, 0x1e, 0x00, 0xf5, 0x21,
+ 0x5a, 0x28, 0x1c, 0x06, 0xca, 0x18, 0x79, 0x46, 0x1c, 0x40, 0x7f, 0x08,
+ 0x50, 0x42, 0x08, 0x93, 0xa4, 0x75, 0x9e, 0x94, 0x44, 0x2b, 0x4b, 0x88,
+ 0x22, 0xc2, 0xd8, 0x58, 0xc3, 0x1b, 0x62, 0xf8, 0xc1, 0x22, 0x1e, 0xb0,
+ 0x7f, 0x3a, 0x2d, 0xfa, 0x74, 0x2c, 0xc7, 0x3f, 0x1c, 0x53, 0x89, 0x28,
+ 0xd2, 0x3c, 0xa6, 0x0a, 0x65, 0x7b, 0x3a, 0x01, 0x5c, 0xfc, 0x2f, 0x51,
+ 0xaf, 0x67, 0x61, 0x7a, 0xe0, 0x05, 0xfe, 0xfc, 0x7f, 0x32, 0x43, 0xf2,
+ 0xc6, 0x60, 0xb2, 0xcc, 0xbc, 0xc0, 0xd8, 0xdc, 0xc5, 0xb8, 0xc2, 0x91,
+ 0x9e, 0x10, 0x26, 0x76, 0x8c, 0xbc, 0xa9, 0xd9, 0x3a, 0x7a, 0xf0, 0xe6,
+ 0xd1, 0x3d, 0x0e, 0x31, 0x3d, 0x57, 0xe2, 0x18, 0x02, 0x24, 0xe6, 0xd2,
+ 0x27, 0x94, 0x37, 0x56, 0x81, 0xf3, 0x13, 0x39, 0xc6, 0x51, 0x02, 0xc5,
+ 0x8b, 0xdd, 0xae, 0xcb, 0x8b, 0x08, 0x6a, 0x71, 0xb1, 0x25, 0x1d, 0xdb,
+ 0x6c, 0x9b, 0x95, 0x4a, 0x8e, 0x9f, 0x6c, 0x0a, 0xe4, 0x9a, 0xb3, 0x3d,
+ 0x6f, 0xf4, 0x51, 0x88, 0xc1, 0x6a, 0x0a, 0xdb, 0xd1, 0xae, 0xd6, 0x68,
+ 0xe2, 0xf3, 0x17, 0x5c, 0xfa, 0xe8, 0xa3, 0x8a, 0x5a, 0xde, 0xb1, 0x28,
+ 0x27, 0xc6, 0xec, 0xa6, 0xa4, 0xc1, 0x85, 0x42, 0xa7, 0x1d, 0x61, 0xca,
+ 0x31, 0x3c, 0x62, 0x63, 0xdc, 0x96, 0x1b, 0x9f, 0x82, 0xe7, 0x41, 0x63,
+ 0x11, 0x1a, 0x56, 0x1c, 0x8d, 0x65, 0xd7, 0xe6, 0x50, 0xb9, 0x74, 0x12,
+ 0xd6, 0xa9, 0xca, 0x75, 0x36, 0x98, 0x17, 0x43, 0x1d, 0x0f, 0x64, 0x7f,
+ 0x9e, 0x98, 0xc8, 0x21, 0xd7, 0xe4, 0xec, 0xc7, 0xec, 0x88, 0x77, 0x67,
+ 0xfa, 0x0f, 0xc5, 0x27, 0x09, 0xed, 0x73, 0x64, 0x2c, 0x15, 0x4b, 0xa4,
+ 0x96, 0x7e, 0x2a, 0x33, 0xad, 0x5d, 0xe5, 0xf1, 0xf9, 0x96, 0xbf, 0xc2,
+ 0x63, 0x3b, 0x7c, 0xb9, 0x0a, 0xc7, 0xf4, 0x48, 0x6f, 0x42, 0xc8, 0xd1,
+ 0xf1, 0xe6, 0x54, 0xc0, 0xe2, 0x0b, 0xd2, 0x54, 0x88, 0x8e, 0xba, 0x0d,
+ 0x26, 0xfe, 0xb1, 0x5f, 0xc1, 0xa9, 0x33, 0x00, 0xef, 0x1b, 0xe3, 0x5f,
+ 0x38, 0x3b, 0xae, 0x17, 0x22, 0xe7, 0x79, 0xce, 0x7c, 0x8b, 0x36, 0x72,
+ 0x03, 0x95, 0x96, 0x1b, 0x8b, 0x6b, 0x21, 0xbb, 0x5e, 0x59, 0x5b, 0x05,
+ 0xb1, 0x00, 0xa9, 0x65, 0x30, 0xaf, 0x22, 0x93, 0x4e, 0x7d, 0x76, 0x96,
+ 0xb9, 0xdf, 0x86, 0x91, 0x4d, 0x8d, 0x8a, 0xb6, 0x9f, 0x8b, 0x35, 0xdd,
+ 0xab, 0xf5, 0x91, 0xf3, 0xdc, 0x1a, 0x9b, 0x6d, 0xfb, 0x9f, 0x4f, 0xdf,
+ 0x45, 0xbc, 0xfe, 0x0b, 0x2a, 0x20, 0xb9, 0x22, 0xbc, 0x99, 0x00, 0x05,
+ 0xc3, 0xd4, 0x40, 0x62, 0xc8, 0x4f, 0xb0, 0x0e, 0x69, 0xb7, 0x52, 0xcf,
+ 0x5a, 0x14, 0xa0, 0x81, 0xe0, 0x96, 0xd5, 0x98, 0x57, 0x00, 0x13, 0x80,
+ 0x98, 0x70, 0xa4, 0x1a, 0x0b, 0x46, 0x33, 0x02, 0xfa, 0x06, 0x20, 0x98,
+ 0x85, 0x3c, 0xda, 0x05, 0x2c, 0xae, 0xa5, 0xc0, 0xa6, 0x5e, 0x87, 0xe9,
+ 0xb1, 0xeb, 0xff, 0x74, 0xe2, 0xfc, 0xc5, 0xcc, 0x67, 0x49, 0x91, 0xb5,
+ 0x7e, 0xdd, 0x1f, 0xfb, 0x3b, 0x78, 0xa1, 0x40, 0x6c, 0x43, 0xe7, 0x97,
+ 0x00, 0x25, 0xb7, 0x92, 0x34, 0x4f, 0x49, 0x8a, 0xf8, 0x9f, 0x3e, 0x4d,
+ 0x8f, 0xa3, 0x7d, 0xe4, 0x66, 0xfd, 0x30, 0x4d, 0xe7, 0xc0, 0xf5, 0xbc,
+ 0xf3, 0x1f, 0x3e, 0x0b, 0x8f, 0x9b, 0x91, 0xc2, 0x4c, 0x8c, 0x8e, 0x27,
+ 0x11, 0x53, 0xcd, 0x0d, 0xdb, 0x48, 0xf3, 0x47, 0xdc, 0x23, 0xfc, 0xfc,
+ 0xdf, 0x2d, 0x75, 0xf4, 0xa4, 0x00, 0x11, 0x22, 0x83, 0x22, 0x64, 0x90,
+ 0xb5, 0xa5, 0x69, 0xb4, 0x26, 0x8b, 0xea, 0x4d, 0x5c, 0xb9, 0x5b, 0xb3,
+ 0xb0, 0x68, 0x6b, 0x6e, 0x28, 0x3c, 0xb8, 0x4f, 0xa0, 0x67, 0xfd, 0x10,
+ 0xc2, 0xe4, 0xe9, 0x3b, 0x06, 0x48, 0x01, 0x70, 0x67, 0xde, 0x7b, 0x7d,
+ 0x0e, 0xdf, 0x50, 0x2a, 0x1f, 0x6e, 0x0a, 0xe2, 0x89, 0x68, 0xf1, 0x45,
+ 0x90, 0xa3, 0xd1, 0x27, 0xe6, 0x96, 0xa5, 0x92, 0xa2, 0xf2, 0x87, 0x06,
+ 0x80, 0x85, 0xd5, 0x76, 0x46, 0x1b, 0x2f, 0x08, 0x75, 0x4e, 0x6d, 0x43,
+ 0x5c, 0x78, 0x17, 0x8e, 0x9d, 0x8c, 0xc3, 0xe8, 0x10, 0xcd, 0x2a, 0x88,
+ 0xfe, 0x6b, 0xbc, 0x61, 0x23, 0x35, 0x68, 0x82, 0x6c, 0x45, 0x7a, 0x38,
+ 0x51, 0xf8, 0x3e, 0xff, 0xd4, 0xc2, 0x80, 0x48, 0x2c, 0x6f, 0x03, 0xe9,
+ 0x85, 0x21, 0x04, 0x9b, 0x07, 0x61, 0x6c, 0xa4, 0x6f, 0xa2, 0xdd, 0x0f,
+ 0x4a, 0x61, 0x78, 0xcd, 0x70, 0xac, 0x47, 0xaf, 0x1b, 0xc0, 0xd9, 0x72,
+ 0x2a, 0xd6, 0xe4, 0xd6, 0x84, 0xec, 0xed, 0xc9, 0xf6, 0xba, 0x70, 0xaa,
+ 0xf8, 0xa1, 0xb9, 0x8b, 0x13, 0xcc, 0x50, 0x5f, 0x34, 0x57, 0xc2, 0xc0,
+ 0x0a, 0xd8, 0x39, 0xe4, 0xe0, 0xa8, 0xe8, 0x07, 0x92, 0x09, 0x48, 0xed,
+ 0xeb, 0x50, 0x48, 0x02, 0xe5, 0xde, 0x03, 0x39, 0x5f, 0x43, 0xff, 0xc5,
+ 0x22, 0x1c, 0xb9, 0x7a, 0xd9, 0x97, 0x27, 0x5a, 0x18, 0xe9, 0x02, 0xb3,
+ 0xd9, 0x6e, 0x52, 0x8e, 0xa1, 0x0f, 0xb4, 0x00, 0x00, 0x00, 0x01, 0x9f,
+ 0x7f, 0xa4, 0x48, 0xd9, 0xf2, 0x9b, 0x10, 0x81, 0xcf, 0x2e, 0x40, 0x8e,
+ 0x6d, 0x93, 0xaf, 0x84, 0xb5, 0x23, 0xef, 0x2a, 0x4c, 0x94, 0x69, 0x4c,
+ 0x10, 0x91, 0x6b, 0xb5, 0x0a, 0x6b, 0xe3, 0xe9, 0xcd, 0x5d, 0xad, 0x88,
+ 0x19, 0x4e, 0x0a, 0x52, 0xab, 0x62, 0x70, 0x28, 0x68, 0xa2, 0x28, 0xe1,
+ 0x62, 0xc3, 0x1c, 0x44, 0x26, 0x6a, 0x6c, 0xba, 0x0c, 0x95, 0xea, 0x99,
+ 0xc8, 0x94, 0x1f, 0xb2, 0x8b, 0xf2, 0xe7, 0x53, 0xca, 0xbc, 0x47, 0x56,
+ 0xe7, 0x88, 0xfb, 0x8b, 0xc7, 0xf1, 0xbd, 0x0a, 0xee, 0xc2, 0xc7, 0xf2,
+ 0x22, 0x6f, 0x44, 0x7c, 0x25, 0x6a, 0xda, 0xe0, 0x32, 0xc7, 0x2b, 0x99,
+ 0x57, 0xe5, 0xb2, 0xa8, 0x3d, 0x89, 0x32, 0xc0, 0xab, 0x99, 0xd5, 0xc1,
+ 0xff, 0xfd, 0x70, 0x57, 0x61, 0x68, 0x20, 0xb6, 0x07, 0xe3, 0x9a, 0x86,
+ 0xd5, 0x80, 0xb8, 0x6a, 0xc3, 0xf0, 0xf3, 0x90, 0x05, 0x46, 0x29, 0x5e,
+ 0x13, 0xae, 0x0c, 0x99, 0xfe, 0x51, 0x10, 0xb6, 0xf6, 0x03, 0x7d, 0xa7,
+ 0x5c, 0xf9, 0xf8, 0x0a, 0x37, 0x12, 0xbc, 0xf2, 0x5f, 0x3d, 0x66, 0xea,
+ 0x10, 0xfb, 0xd6, 0x5f, 0x43, 0x29, 0x91, 0xa8, 0xa9, 0x4c, 0x5e, 0xa3,
+ 0x1d, 0x2c, 0x19, 0xb0, 0xab, 0x4b, 0x17, 0xf9, 0x0a, 0x5a, 0x6a, 0x46,
+ 0x36, 0x6f, 0xfc, 0x06, 0x7d, 0xb3, 0x88, 0x4c, 0xc8, 0x14, 0xad, 0xc2,
+ 0xea, 0x32, 0x35, 0x13, 0xa7, 0x6a, 0xef, 0x09, 0x4e, 0x05, 0xf4, 0x7d,
+ 0xda, 0x32, 0x7c, 0x82, 0x4a, 0x9c, 0x02, 0x65, 0xd6, 0xf3, 0x3b, 0x7e,
+ 0xe4, 0x4a, 0xe9, 0x18, 0x9a, 0xde, 0x7e, 0x8a, 0x2c, 0x00, 0xf5, 0x58,
+ 0x4f, 0x61, 0xb0, 0x76, 0x98, 0x00, 0x5d, 0x2e, 0x91, 0x16, 0xf4, 0x2e,
+ 0x60, 0x83, 0x32, 0xd6, 0x28, 0x7b, 0xf8, 0x12, 0x72, 0x8c, 0xdd, 0xe4,
+ 0x56, 0x71, 0xb5, 0xdb, 0x5c, 0x88, 0x92, 0xe0, 0xbf, 0x5d, 0xd4, 0x9f,
+ 0xc5, 0x7e, 0x17, 0x92, 0x00, 0x38, 0xa6, 0xe0, 0x0d, 0xb1, 0x9f, 0xa9,
+ 0xbb, 0xad, 0x01, 0x25, 0xb6, 0x8d, 0x40, 0x87, 0x2f, 0x42, 0x24, 0xa2,
+ 0x42, 0xd2, 0x5f, 0xe0, 0xff, 0x49, 0x32, 0x74, 0x38, 0xea, 0x5c, 0x68,
+ 0x76, 0x45, 0xd9, 0x94, 0x93, 0xd4, 0x67, 0xd0, 0x21, 0x09, 0xe0, 0xb2,
+ 0x19, 0x1c, 0xa3, 0x04, 0x63, 0xec, 0x0d, 0x36, 0x60, 0xf3, 0x41, 0xce,
+ 0xc1, 0xc1, 0x19, 0x14, 0xd3, 0x8d, 0xc9, 0x00, 0xeb, 0xa6, 0xcf, 0x36,
+ 0xe2, 0xad, 0xa5, 0x73, 0x0b, 0x2a, 0x79, 0x14, 0x8c, 0x7d, 0xd8, 0x05,
+ 0x29, 0xe0, 0xd8, 0x2a, 0xbb, 0x29, 0xe3, 0x12, 0xbe, 0x1d, 0xbd, 0x6c,
+ 0xd4, 0x2a, 0x76, 0xc3, 0xb1, 0xfb, 0x5e, 0x10, 0x12, 0xf5, 0xe1, 0xb1,
+ 0xd5, 0x9b, 0x39, 0x35, 0xc8, 0x12, 0x0b, 0x92, 0xe0, 0x8e, 0x8c, 0x60,
+ 0x5d, 0x6c, 0x3f, 0x2c, 0xd5, 0x01, 0xb9, 0x83, 0x2f, 0x7f, 0xd0, 0xb1,
+ 0x67, 0xe6, 0xfc, 0x2c, 0xf9, 0x77, 0x32, 0x33, 0x2d, 0x08, 0xff, 0x4f,
+ 0xce, 0x61, 0x68, 0x2c, 0x02, 0x5e, 0x80, 0x00, 0x00, 0x00, 0xe9, 0x7f,
+ 0xe6, 0x9f, 0xae, 0x81, 0xa6, 0xd1, 0x24, 0xbd, 0x43, 0xb9, 0x42, 0x20,
+ 0x5b, 0xcc, 0xb7, 0x49, 0x69, 0x76, 0x42, 0x50, 0xb6, 0x8a, 0x7f, 0x31,
+ 0xc8, 0x15, 0xc3, 0x56, 0x73, 0xf6, 0x74, 0x1f, 0x51, 0xba, 0x34, 0x36,
+ 0xf1, 0xe7, 0xfa, 0xdf, 0x86, 0x59, 0x85, 0x09, 0x72, 0x58, 0x33, 0xf4,
+ 0x59, 0x3d, 0x4b, 0x2f, 0x76, 0x94, 0x20, 0x8a, 0x92, 0xb8, 0xf7, 0x3a,
+ 0x70, 0x1b, 0xb7, 0xbb, 0xfb, 0x12, 0xdb, 0x6b, 0x98, 0x5d, 0x76, 0xe8,
+ 0xf4, 0x1b, 0xff, 0x61, 0x10, 0xb1, 0x12, 0x17, 0x59, 0xbe, 0xe7, 0x7a,
+ 0xb6, 0x6b, 0x91, 0x61, 0x7c, 0x56, 0x93, 0xb6, 0xdf, 0xa2, 0x1b, 0x92,
+ 0xb7, 0x85, 0xac, 0x63, 0x76, 0x38, 0x13, 0x89, 0x88, 0xf6, 0x6d, 0xc0,
+ 0x9e, 0xbf, 0x09, 0x3e, 0x37, 0x17, 0xaa, 0x2c, 0x52, 0xcb, 0xc1, 0x73,
+ 0xca, 0xa7, 0xf8, 0xc5, 0x44, 0x0e, 0x82, 0x0f, 0xa0, 0xcc, 0xcc, 0x3f,
+ 0xb0, 0xae, 0x21, 0xc6, 0xc2, 0xdd, 0x20, 0x8f, 0x06, 0x8e, 0xee, 0x04,
+ 0xea, 0x55, 0x1e, 0xab, 0xa3, 0xf9, 0x38, 0xc7, 0x5a, 0x74, 0x35, 0xc0,
+ 0x9b, 0xc5, 0x85, 0x24, 0x81, 0x5a, 0x8b, 0x22, 0x7f, 0xc7, 0xd7, 0x3b,
+ 0x96, 0x3c, 0xf2, 0x89, 0x39, 0x84, 0x92, 0x87, 0xe6, 0x10, 0x78, 0x57,
+ 0xb3, 0x5c, 0xe9, 0x46, 0xa4, 0xb0, 0xb4, 0x83, 0x64, 0x87, 0x9b, 0xd2,
+ 0x67, 0x66, 0x87, 0x86, 0xe6, 0x0d, 0x14, 0xa8, 0x18, 0x15, 0x42, 0xad,
+ 0x6c, 0xf3, 0x98, 0x4c, 0x19, 0x11, 0xbc, 0x50, 0x8d, 0x96, 0x48, 0xee,
+ 0xe1, 0x16, 0x7d, 0xf0, 0x79, 0xc8, 0xa0, 0x79, 0x2e, 0x01, 0x02, 0x4e,
+ 0x7f, 0x56, 0xc2, 0x00, 0x7f, 0x6b, 0x82, 0x89, 0xf7, 0x05, 0x28, 0x2f,
+ 0x91, 0xb1, 0x70, 0x25, 0xf5, 0xaa, 0x06, 0xd5, 0xd5, 0x97, 0xa9, 0x76,
+ 0x0b, 0x35, 0x61, 0x95, 0xed, 0x08, 0xc7, 0x43, 0x69, 0xda, 0x89, 0x6e,
+ 0x83, 0x16, 0xdc, 0x64, 0xcf, 0x93, 0x6d, 0x08, 0x11, 0xd1, 0x3e, 0xed,
+ 0xdd, 0xb7, 0x8e, 0xd0, 0xfd, 0xed, 0x8e, 0x3d, 0x2c, 0x3c, 0x7a, 0x0c,
+ 0x0d, 0x1e, 0x87, 0xe0, 0x7e, 0x33, 0xcf, 0xbb, 0x5d, 0x8b, 0x79, 0x29,
+ 0xad, 0xab, 0x7e, 0x86, 0xaa, 0x04, 0x0e, 0xa0, 0x68, 0x80, 0x9c, 0xfe,
+ 0x46, 0xd4, 0xe7, 0x26, 0xe3, 0x52, 0xf6, 0x3e, 0xc6, 0xe7, 0x86, 0x0d,
+ 0x14, 0x56, 0xf7, 0xbd, 0xfe, 0x04, 0x2c, 0x66, 0xc5, 0x7d, 0xd8, 0xad,
+ 0xec, 0xbc, 0xf7, 0x0e, 0x2e, 0x6f, 0x06, 0x20, 0x2f, 0xc4, 0x22, 0xfa,
+ 0x89, 0x3c, 0x7a, 0xa3, 0x31, 0xa1, 0xa8, 0x8b, 0xcc, 0x53, 0xd4, 0x5c,
+ 0x1e, 0x9e, 0xbe, 0x91, 0x7b, 0x26, 0x42, 0x65, 0xfa, 0xbb, 0x71, 0xb8,
+ 0x9e, 0xc8, 0xb6, 0x5f, 0xb2, 0x43, 0xef, 0x2b, 0xa6, 0x36, 0xf3, 0x76,
+ 0x94, 0x6c, 0x4d, 0x03, 0x32, 0x1d, 0x3d, 0x8c, 0x88, 0x8b, 0x59, 0x75,
+ 0xb3, 0xca, 0x07, 0xeb, 0x46, 0x3f, 0x34, 0x03, 0xe3, 0xcb, 0xdf, 0xb2,
+ 0x3c, 0x2c, 0x42, 0x52, 0xd7, 0x8b, 0xa5, 0x32, 0x7a, 0x34, 0x88, 0x30,
+ 0xa8, 0x99, 0x40, 0x94, 0xcd, 0x98, 0x24, 0xf3, 0xe6, 0xaf, 0x27, 0x7c,
+ 0xe6, 0x45, 0x26, 0x4b, 0xc1, 0x3d, 0xda, 0x47, 0xad, 0xc0, 0x92, 0xf1,
+ 0xcf, 0x13, 0x37, 0x9d, 0xc0, 0xa6, 0xea, 0x7d, 0xc4, 0x8b, 0x6e, 0xbd,
+ 0x10, 0xa8, 0xcb, 0x2c, 0x8d, 0xac, 0xb7, 0xbf, 0xc7, 0x27, 0xb6, 0x67,
+ 0x9d, 0x01, 0x3d, 0xb4, 0xef, 0x6d, 0x10, 0xb1, 0xf8, 0x86, 0xba, 0x5c,
+ 0xae, 0x06, 0x35, 0x4b, 0x99, 0xe0, 0x4d, 0xf7, 0x07, 0x63, 0x07, 0x8d,
+ 0xf3, 0xa0, 0xb8, 0x06, 0x6b, 0xe4, 0xb0, 0xfe, 0x79, 0xa4, 0x2d, 0x28,
+ 0x6e, 0xd3, 0x9e, 0x7e, 0x92, 0xd7, 0x69, 0x0a, 0x6e, 0xb5, 0x12, 0xb7,
+ 0x79, 0x13, 0xd8, 0xf1, 0x82, 0x11, 0xf6, 0xe0, 0xb8, 0x05, 0x13, 0xf8,
+ 0x8b, 0xcf, 0xb3, 0xab, 0x17, 0xfe, 0xf4, 0x22, 0x1e, 0xbd, 0x88, 0x4e,
+ 0x95, 0x7d, 0x05, 0xf4, 0x2a, 0xba, 0xc9, 0x22, 0xaa, 0xb8, 0x96, 0x19,
+ 0x4b, 0xed, 0x2b, 0xfc, 0x21, 0x3f, 0xf8, 0x9c, 0xaf, 0xb4, 0x31, 0x3e,
+ 0xa0, 0x45, 0x3c, 0xce, 0xde, 0x94, 0x56, 0xc7, 0xd8, 0x24, 0xfc, 0xb7,
+ 0x22, 0x74, 0x62, 0x20, 0x3a, 0x47, 0xae, 0xdc, 0x05, 0x64, 0x27, 0x4b,
+ 0xab, 0xb5, 0xf1, 0x18, 0x43, 0xa4, 0x6c, 0x00, 0xdc, 0x61, 0xb6, 0xeb,
+ 0x14, 0x5e, 0xaf, 0xe8, 0xd2, 0x43, 0x67, 0x88, 0xc9, 0xb7, 0x1e, 0xd8,
+ 0x3b, 0x8a, 0xe1, 0x4c, 0xa5, 0x03, 0x6c, 0x8c, 0x38, 0xa8, 0x40, 0xe0,
+ 0xe9, 0xf8, 0x42, 0x58, 0x21, 0x7c, 0x85, 0x5d, 0x82, 0x9a, 0x9f, 0x11,
+ 0xf4, 0x72, 0x05, 0xe7, 0x6e, 0x04, 0x25, 0x18, 0x84, 0xf3, 0x3a, 0x1b,
+ 0xa0, 0x18, 0xe4, 0x5a, 0x58, 0xb7, 0xd2, 0x9b, 0xd3, 0xb9, 0x0e, 0xb8,
+ 0x93, 0x86, 0x91, 0x29, 0xa4, 0x1b, 0x60, 0xa7, 0x2d, 0x0d, 0x1d, 0xea,
+ 0x08, 0x90, 0x8a, 0x98, 0x02, 0xf8, 0x1b, 0x06, 0xb0, 0xef, 0x88, 0xbb,
+ 0xc4, 0xef, 0xfb, 0xbc, 0xf4, 0x74, 0x41, 0x28, 0xd0, 0x64, 0xdb, 0x47,
+ 0x2e, 0xa5, 0xfe, 0x01, 0x15, 0x4c, 0xb0, 0x0d, 0x80, 0x98, 0x1b, 0x17,
+ 0x8e, 0x50, 0xaf, 0x12, 0x5b, 0xd9, 0xf2, 0x41, 0xb4, 0x84, 0x33, 0x74,
+ 0xe9, 0x86, 0xf9, 0xb7, 0xae, 0x69, 0x7e, 0x52, 0x91, 0x27, 0xd3, 0xa5,
+ 0x85, 0x3c, 0xec, 0x2d, 0x39, 0x8f, 0xd8, 0xbb, 0x31, 0x42, 0xa1, 0xb6,
+ 0x27, 0x28, 0x4d, 0x72, 0x74, 0xbe, 0xca, 0xf5, 0xd8, 0xce, 0x42, 0x72,
+ 0x26, 0xcb, 0x68, 0x78, 0x1f, 0x35, 0xcc, 0x2d, 0xac, 0xb4, 0xbb, 0x7a,
+ 0xb6, 0x1f, 0x51, 0x09, 0x0e, 0xe4, 0xce, 0x92, 0x70, 0x36, 0x54, 0xa7,
+ 0x7f, 0x24, 0x60, 0xe8, 0x44, 0xcc, 0x19, 0x4e, 0x2d, 0xd7, 0x8d, 0x84,
+ 0x73, 0xd6, 0xbd, 0x0e, 0x3d, 0xea, 0x45, 0xbc, 0x70, 0xcb, 0x30, 0x28,
+ 0x83, 0x5d, 0x01, 0x9a, 0xe1, 0xb0, 0x21, 0x66, 0x88, 0xe7, 0xad, 0x46,
+ 0xbd, 0x1c, 0x50, 0xb2, 0x39, 0x08, 0x7c, 0x1d, 0x8d, 0xca, 0xcd, 0x9d,
+ 0xf9, 0x63, 0xc2, 0x0b, 0xdf, 0x01, 0xb0, 0x58, 0x49, 0xb9, 0x24, 0xe3,
+ 0xe9, 0x3d, 0xa3, 0xfc, 0x8a, 0x90, 0x9c, 0xbd, 0xe2, 0x0c, 0x74, 0xc3,
+ 0x12, 0x52, 0x6a, 0xa4, 0x67, 0xb2, 0x9b, 0xda, 0x81, 0xa1, 0x25, 0x53,
+ 0x0d, 0x87, 0xec, 0x1b, 0x44, 0x80, 0xe5, 0x94, 0x2c, 0xb9, 0xc4, 0xfc,
+ 0xf3, 0xa5, 0xdc, 0xb3, 0x88, 0x0f, 0x27, 0xc1, 0x8d, 0xe3, 0xfb, 0x4e,
+ 0x76, 0x57, 0xfd, 0x28, 0x02, 0x2b, 0x9a, 0x0e, 0x39, 0x78, 0x34, 0x34,
+ 0x10, 0xe8, 0x35, 0x07, 0x28, 0x69, 0xdc, 0x25, 0xa7, 0x3a, 0xa2, 0xa5,
+ 0x69, 0xe6, 0xaf, 0xad, 0x69, 0xbf, 0x07, 0x48, 0x48, 0xc9, 0x2b, 0xf0,
+ 0xcc, 0x7d, 0x60, 0x86, 0xa7, 0x56, 0xe2, 0x18, 0x23, 0x6e, 0x8f, 0xa4,
+ 0x4a, 0xf4, 0x8b, 0x1c, 0x16, 0xf8, 0x30, 0x60, 0xd5, 0xc4, 0x93, 0x66,
+ 0x31, 0x11, 0x2d, 0xa6, 0x8d, 0x2f, 0x34, 0x38, 0x66, 0xc2, 0x5e, 0xca,
+ 0xbd, 0x79, 0xeb, 0x42, 0xbb, 0x20, 0x88, 0x89, 0xcb, 0xd5, 0xbe, 0x22,
+ 0xa9, 0x31, 0x0e, 0x21, 0x9d, 0xd6, 0x27, 0x3b, 0xab, 0xa1, 0xf7, 0xd7,
+ 0xf3, 0x6a, 0x74, 0xe5, 0xab, 0xad, 0xd3, 0xc9, 0xc5, 0x85, 0x5a, 0x1d,
+ 0xd1, 0xc3, 0xb5, 0x85, 0xc2, 0xde, 0x51, 0x3b, 0x0b, 0x4a, 0x6e, 0x2b,
+ 0x35, 0x5a, 0xe5, 0xa6, 0x3a, 0xce, 0xe8, 0x77, 0x08, 0x91, 0x26, 0xe1,
+ 0x68, 0xa8, 0xf4, 0x01, 0x49, 0x6a, 0xca, 0x05, 0x19, 0x18, 0x2a, 0x55,
+ 0x7e, 0x56, 0x03, 0xd6, 0x6b, 0x07, 0xa4, 0xa2, 0x96, 0x4f, 0x7f, 0x73,
+ 0x89, 0x32, 0x09, 0xba, 0x76, 0xfc, 0x40, 0x7b, 0x77, 0x28, 0x8f, 0x81,
+ 0x84, 0x1d, 0x5a, 0xb3, 0x8a, 0xc3, 0xb2, 0x30, 0x69, 0xcf, 0xab, 0x1c,
+ 0x04, 0xcb, 0x79, 0x10, 0x3f, 0x6b, 0x42, 0xc0, 0xcb, 0x36, 0x96, 0xe6,
+ 0xde, 0xc2, 0x59, 0xca, 0x52, 0xdc, 0x41, 0x3d, 0xaf, 0xcd, 0xea, 0x83,
+ 0xa3, 0x9f, 0x25, 0xab, 0x19, 0x26, 0xef, 0x69, 0xb9, 0xd9, 0x1a, 0xfa,
+ 0x28, 0x61, 0x27, 0x07, 0x81, 0xf9, 0xc1, 0xc4, 0xf6, 0x34, 0x98, 0x72,
+ 0x11, 0x11, 0xf6, 0x95, 0x0c, 0x82, 0x02, 0xaa, 0xbb, 0xc2, 0xec, 0xcb,
+ 0x49, 0x8f, 0xa0, 0x22, 0x00, 0xf0, 0x96, 0xb5, 0xa3, 0x7b, 0xd1, 0x6a,
+ 0x91, 0x88, 0x88, 0x7a, 0x41, 0xdc, 0xb4, 0x1c, 0x37, 0xe6, 0x67, 0x78,
+ 0x8d, 0x30, 0xd1, 0xe2, 0x97, 0x5a, 0xab, 0xdc, 0xad, 0xdf, 0x84, 0x72,
+ 0x78, 0xfa, 0xed, 0x40, 0x5d, 0x16, 0x97, 0x6a, 0x17, 0xf2, 0x03, 0x6c,
+ 0x5b, 0xdc, 0x0b, 0x21, 0x35, 0x0d, 0x17, 0x2a, 0x72, 0x3d, 0x1b, 0xd5,
+ 0x9a, 0x5d, 0x97, 0x93, 0xe2, 0x84, 0x35, 0x91, 0xc3, 0xde, 0xa3, 0xb8,
+ 0x1f, 0x51, 0xf7, 0x15, 0xc9, 0x32, 0xe6, 0x47, 0x20, 0x15, 0x07, 0x2d,
+ 0x15, 0xb3, 0xce, 0xea, 0x19, 0xfa, 0xcc, 0x4e, 0xfa, 0x34, 0x0c, 0xcb,
+ 0x79, 0xb1, 0xbd, 0xf6, 0xc6, 0xac, 0x7b, 0x46, 0xc5, 0xfb, 0xb6, 0x5d,
+ 0x67, 0x99, 0x83, 0xa1, 0xba, 0x32, 0xa9, 0x58, 0x99, 0xb8, 0xb7, 0xe0,
+ 0x5e, 0x2a, 0xf9, 0x77, 0xc0, 0x44, 0x81, 0x5b, 0x97, 0x16, 0x49, 0x15,
+ 0x18, 0x20, 0x92, 0x5c, 0x4e, 0xeb, 0xf0, 0xc9, 0x39, 0x0a, 0x9a, 0x59,
+ 0xd3, 0x49, 0x25, 0x2a, 0xf1, 0xe9, 0x2f, 0x4a, 0x65, 0x40, 0x77, 0x1b,
+ 0x67, 0xb0, 0xff, 0x4e, 0xb9, 0x1e, 0x86, 0xac, 0x55, 0xc0, 0x1e, 0xeb,
+ 0xaf, 0x56, 0x4d, 0x8a, 0xc0, 0xe1, 0xb4, 0x77, 0x13, 0xc4, 0x98, 0x0e,
+ 0x3c, 0xb9, 0x8c, 0x2d, 0xe0, 0x3d, 0x23, 0xc7, 0x3b, 0x30, 0x7b, 0xc7,
+ 0xc2, 0x65, 0xd5, 0x8d, 0x0a, 0x28, 0xc1, 0x5f, 0xf4, 0x28, 0x3a, 0x70,
+ 0x57, 0xfd, 0xbb, 0x7c, 0x7b, 0x67, 0x25, 0x71, 0xa6, 0x36, 0x93, 0xff,
+ 0xef, 0xcd, 0x51, 0x1a, 0xfc, 0xc9, 0x87, 0x29, 0x11, 0xce, 0x75, 0x54,
+ 0x28, 0x50, 0xe2, 0x31, 0x9a, 0x87, 0x9a, 0xf0, 0x29, 0x6a, 0x3a, 0x19,
+ 0x89, 0xe1, 0x27, 0xd1, 0x70, 0x72, 0x1d, 0x14, 0x71, 0x6d, 0x5b, 0xad,
+ 0x58, 0xc3, 0xb1, 0xfe, 0xbd, 0x72, 0x11, 0x48, 0x0b, 0x06, 0xe8, 0x74,
+ 0x82, 0x1f, 0xd9, 0x96, 0xa6, 0x2c, 0x7d, 0x9f, 0x21, 0x3d, 0xcd, 0x57,
+ 0x5c, 0x5a, 0x78, 0xfd, 0x49, 0xd9, 0xf1, 0xdb, 0x31, 0xb8, 0xd8, 0xaa,
+ 0x73, 0x51, 0xba, 0x51, 0xcf, 0x22, 0x66, 0x78, 0x71, 0xb8, 0x11, 0xd2,
+ 0x7b, 0xd7, 0x84, 0x7e, 0x35, 0xe9, 0x2a, 0x40, 0x74, 0xb8, 0x3f, 0xda,
+ 0xe3, 0xf2, 0x5c, 0x00, 0x29, 0xb9, 0x60, 0xb8, 0x15, 0x50, 0xab, 0x08,
+ 0xa5, 0x76, 0x20, 0xb3, 0x6d, 0x93, 0x2e, 0x58, 0x9f, 0xc8, 0x51, 0xf4,
+ 0xba, 0x96, 0xc0, 0x3c, 0x39, 0x4d, 0xff, 0x46, 0x86, 0xc0, 0xdb, 0xd3,
+ 0x21, 0xa2, 0x1c, 0x7b, 0xce, 0x2e, 0x4a, 0x8c, 0x3b, 0x59, 0x81, 0xfb,
+ 0xa5, 0xcc, 0xd8, 0xe9, 0x07, 0x6b, 0xc1, 0xcc, 0x23, 0xbb, 0xfc, 0x8e,
+ 0x5c, 0xb8, 0x44, 0x68, 0x36, 0x34, 0x9c, 0x44, 0xd2, 0x9e, 0x0f, 0x53,
+ 0xc9, 0xfe, 0x41, 0xba, 0x55, 0x49, 0xad, 0x8e, 0x5b, 0x82, 0x9d, 0xcd,
+ 0xc4, 0xbc, 0x16, 0xa2, 0x0d, 0xfc, 0xaf, 0x6f, 0xe5, 0xb3, 0x63, 0x7c,
+ 0x47, 0x79, 0x07, 0x59, 0x8b, 0xfe, 0xf3, 0x00, 0x07, 0xf0, 0x50, 0x57,
+ 0xa6, 0xd7, 0xb8, 0xdd, 0x46, 0xb6, 0xa3, 0x80, 0x3b, 0x36, 0xbd, 0xaa,
+ 0xb3, 0x5b, 0xa0, 0xee, 0x02, 0x19, 0xf4, 0xd7, 0x7e, 0xaf, 0xbb, 0xa1,
+ 0xc4, 0xb3, 0x0f, 0xab, 0x57, 0x89, 0x66, 0x82, 0x4f, 0xdc, 0x15, 0xa0,
+ 0xf2, 0xe2, 0x6a, 0x49, 0x41, 0x86, 0xe4, 0x50, 0x06, 0xda, 0x49, 0xa2,
+ 0x06, 0xca, 0xf1, 0x2d, 0xbb, 0x8b, 0x83, 0xb7, 0x0e, 0xdd, 0x44, 0xd4,
+ 0xe7, 0xf2, 0xe6, 0x4e, 0xac, 0x8e, 0x61, 0xa3, 0x8a, 0x01, 0xba, 0x7b,
+ 0x88, 0xf4, 0xd7, 0x0f, 0xeb, 0xf6, 0x6c, 0x7f, 0x49, 0x4e, 0xd4, 0x9d,
+ 0xe7, 0x9e, 0xaf, 0xbb, 0x0a, 0x3b, 0x69, 0x7d, 0xbb, 0x2b, 0xbb, 0xf4,
+ 0x04, 0x4a, 0xe3, 0x57, 0xdc, 0x70, 0x7e, 0xec, 0x7a, 0xf0, 0xc5, 0x0c,
+ 0xb7, 0x1d, 0x1b, 0xa2, 0xae, 0xd5, 0xcd, 0xaf, 0xb1, 0xdc, 0x3b, 0x03,
+ 0x28, 0xec, 0xc2, 0x70, 0xf2, 0x7a, 0x4e, 0x47, 0x3b, 0x45, 0xb9, 0x8b,
+ 0x15, 0xed, 0xd9, 0x19, 0xbd, 0x6e, 0x56, 0x75, 0xf0, 0xe4, 0x49, 0x3b,
+ 0xc4, 0xb2, 0xfa, 0x5a, 0x44, 0x4c, 0x1a, 0xbb, 0xce, 0x64, 0xed, 0x7a,
+ 0x18, 0xc8, 0x02, 0x62, 0x43, 0x75, 0xfc, 0x20, 0x46, 0xf5, 0xf2, 0x5b,
+ 0xc5, 0xc8, 0xe7, 0xce, 0xe4, 0xf4, 0x9d, 0x28, 0xb9, 0x76, 0x46, 0xff,
+ 0xb3, 0x18, 0x8f, 0x72, 0x7b, 0x4a, 0xfa, 0x01, 0xdc, 0x38, 0x97, 0x10,
+ 0x46, 0x79, 0x15, 0x69, 0x4f, 0xb7, 0xed, 0xa6, 0xb5, 0x48, 0xa2, 0xf1,
+ 0xe3, 0x39, 0xec, 0x98, 0x07, 0x27, 0x3d, 0xcf, 0x52, 0x50, 0x77, 0x35,
+ 0x9c, 0xd2, 0x98, 0x82, 0x63, 0xeb, 0xed, 0x0e, 0x80, 0xe7, 0x02, 0x4b,
+ 0xfa, 0xb5, 0x67, 0x99, 0x32, 0x76, 0xd8, 0xae, 0x80, 0x6f, 0x44, 0xf7,
+ 0x26, 0x24, 0x1a, 0x07, 0xe2, 0x7f, 0x3d, 0xa7, 0x64, 0xa6, 0x62, 0x6b,
+ 0x8f, 0x85, 0xa9, 0x44, 0xf1, 0x1d, 0x48, 0x28, 0xdf, 0x74, 0xf0, 0x2e,
+ 0x92, 0x5e, 0x45, 0x32, 0x34, 0x43, 0x10, 0x9d, 0x4c, 0xcb, 0x58, 0x58,
+ 0xcf, 0xd6, 0xbb, 0x3a, 0xe7, 0xc9, 0x26, 0x9f, 0x82, 0x25, 0xe1, 0x56,
+ 0x83, 0x44, 0x73, 0x72, 0x34, 0x52, 0x70, 0x00, 0x00, 0xcc, 0xa6, 0xe0,
+ 0x39, 0xff, 0x70, 0x71, 0x9f, 0x2a, 0x41, 0x36, 0xca, 0x3c, 0xb7, 0x7c,
+ 0xb7, 0x68, 0x8c, 0x11, 0xb0, 0xf0, 0x0c, 0x63, 0x4e, 0x13, 0x60, 0xc7,
+ 0xca, 0x0a, 0xa0, 0xec, 0x69, 0x11, 0x7c, 0x81, 0xc1, 0x55, 0xba, 0x17,
+ 0x45, 0xeb, 0xc5, 0x39, 0x79, 0x39, 0xa6, 0x55, 0xc7, 0xa5, 0xb8, 0x01,
+ 0x9e, 0x92, 0x79, 0xeb, 0xd5, 0xe2, 0x94, 0x7d, 0x1c, 0x84, 0xd8, 0xd0,
+ 0x7f, 0xe7, 0x04, 0xed, 0xac, 0xca, 0x99, 0x1f, 0xa4, 0xe7, 0x29, 0x0d,
+ 0x66, 0x22, 0xb0, 0xb2, 0x9d, 0xb2, 0x4c, 0xe7, 0x85, 0xce, 0xc0, 0x9d,
+ 0xbb, 0x78, 0x4c, 0x37, 0x2c, 0x94, 0x7e, 0xc8, 0x05, 0x98, 0x22, 0x42,
+ 0xf3, 0x1b, 0x45, 0x69, 0x47, 0xc0, 0xff, 0xfe, 0xc7, 0x48, 0x9e, 0xc7,
+ 0xa4, 0xb8, 0x9f, 0xd1, 0x29, 0x76, 0x0e, 0x04, 0x79, 0xd5, 0x23, 0x55,
+ 0xad, 0x8a, 0x25, 0xfc, 0xad, 0x60, 0x98, 0x42, 0x0c, 0xa6, 0xe4, 0x64,
+ 0x30, 0xc4, 0xea, 0xbc, 0x5f, 0x6a, 0x07, 0x26, 0x7c, 0x8b, 0xdc, 0x36,
+ 0x1f, 0xf6, 0x4a, 0x36, 0x77, 0x42, 0x3c, 0xcd, 0x36, 0xd2, 0x4e, 0x8a,
+ 0x8e, 0x05, 0xb4, 0xa0, 0xab, 0x1e, 0x41, 0xb4, 0x82, 0xe4, 0xe9, 0x5e,
+ 0x6e, 0x27, 0xf0, 0xfe, 0x9f, 0x79, 0x41, 0x2a, 0xdf, 0x1d, 0xbe, 0x62,
+ 0xca, 0x1b, 0x3a, 0x5b, 0xcb, 0x86, 0x2b, 0xfd, 0xe6, 0x83, 0x94, 0xf5,
+ 0x65, 0x23, 0xff, 0x4d, 0xb6, 0xe2, 0x4a, 0x56, 0x89, 0x7e, 0x7d, 0xc3,
+ 0x2c, 0xd4, 0xa3, 0x69, 0x9a, 0x2f, 0x72, 0x25, 0x6e, 0x58, 0x00, 0xaa,
+ 0x2c, 0x66, 0x88, 0x86, 0xd2, 0xcc, 0x04, 0x19, 0xcb, 0x1e, 0xf0, 0x9b,
+ 0x25, 0x8a, 0x1c, 0xb3, 0x5f, 0x48, 0x51, 0x74, 0x84, 0xca, 0xae, 0x3d,
+ 0x99, 0x1f, 0xa3, 0x2d, 0xbf, 0x97, 0x56, 0x54, 0x61, 0xb3, 0x4e, 0x00,
+ 0xa3, 0x4c, 0xbd, 0x81, 0x00, 0xc8, 0x00, 0x86, 0x00, 0x40, 0x96, 0xf0,
+ 0x01, 0x17, 0x86, 0x00, 0x0c, 0x7f, 0x88, 0x15, 0x07, 0xf2, 0x2e, 0x0e,
+ 0x27, 0xcb, 0x0c, 0x2c, 0x00, 0x00, 0x00, 0x04, 0x0c, 0x7e, 0xdd, 0x5f,
+ 0x17, 0xc9, 0x99, 0x69, 0x17, 0x29, 0xd6, 0xfd, 0xe0, 0x80, 0xc1, 0xa3,
+ 0xa0, 0x87, 0x0b, 0x49, 0xaa, 0xba, 0x18, 0xdf, 0xad, 0xb0, 0x55, 0xc1,
+ 0x56, 0x6f, 0xba, 0x53, 0x1d, 0x28, 0xec, 0x91, 0x2d, 0x57, 0x8c, 0x2c,
+ 0x58, 0x49, 0x16, 0x3c, 0xb6, 0x7b, 0x90, 0x8a, 0xee, 0xb0, 0xf1, 0x45,
+ 0x36, 0x10, 0x86, 0xea, 0xe2, 0x1f, 0x90, 0x99, 0x5a, 0x60, 0x6a, 0xbb,
+ 0x08, 0xc6, 0x90, 0xa9, 0x7c, 0x60, 0x06, 0x8c, 0x1e, 0xf3, 0x46, 0x34,
+ 0x0e, 0x2b, 0x75, 0xd5, 0xd8, 0x3e, 0x60, 0x0b, 0xa7, 0x12, 0x59, 0x93,
+ 0xa8, 0xef, 0xa0, 0x94, 0xc7, 0x89, 0x58, 0x6b, 0x99, 0x2f, 0x0c, 0xb1,
+ 0xef, 0x02, 0x89, 0x17, 0x49, 0xb9, 0x32, 0x8f, 0x8b, 0x8e, 0xfd, 0x06,
+ 0x00, 0x23, 0x81, 0xdc, 0x73, 0x2e, 0xce, 0x22, 0x57, 0xec, 0x20, 0xd2,
+ 0x64, 0x9f, 0x6f, 0x43, 0xad, 0xe0, 0x0c, 0x82, 0xe2, 0xa1, 0xc3, 0x2e,
+ 0x2b, 0x9e, 0xba, 0x93, 0xb6, 0x57, 0x75, 0xfc, 0x62, 0x0e, 0x63, 0x94,
+ 0x30, 0xf7, 0x0e, 0x3d, 0x6c, 0x6c, 0xdb, 0x28, 0xc6, 0xe3, 0x11, 0x52,
+ 0x2a, 0xf1, 0x81, 0x8f, 0xa6, 0xbd, 0xb9, 0xa8, 0x85, 0xda, 0x7f, 0x6c,
+ 0xe6, 0x02, 0x8d, 0x63, 0x41, 0x41, 0x0e, 0xb2, 0x49, 0x18, 0x1d, 0x3e,
+ 0x2a, 0xcf, 0xed, 0x36, 0x1c, 0x19, 0xb0, 0x13, 0x11, 0xcf, 0xf3, 0xdc,
+ 0x4d, 0x1b, 0xce, 0xf3, 0xac, 0xd8, 0xdb, 0x39, 0x5d, 0xb8, 0xd2, 0x3d,
+ 0xb4, 0xaf, 0x7f, 0xca, 0x44, 0xe8, 0xf4, 0x53, 0x73, 0xeb, 0xe2, 0x25,
+ 0x7c, 0x09, 0x6a, 0xa8, 0x42, 0xe9, 0xaf, 0x87, 0x7c, 0xfa, 0x2b, 0x70,
+ 0x2f, 0x60, 0xb7, 0x5b, 0x77, 0xa9, 0x37, 0x14, 0xae, 0xae, 0x6b, 0x33,
+ 0xc3, 0x0e, 0x23, 0x2c, 0xb7, 0xd5, 0x73, 0xa8, 0x90, 0xd8, 0xba, 0x39,
+ 0xdb, 0x56, 0x9d, 0x4a, 0x14, 0x0b, 0x5c, 0x74, 0xab, 0x72, 0x0e, 0x2a,
+ 0xb8, 0x0d, 0xc8, 0xb6, 0x53, 0x87, 0xd8, 0xb3, 0xd9, 0xf5, 0x41, 0xf9,
+ 0xfa, 0xf1, 0x6f, 0x2a, 0x28, 0x28, 0xde, 0xe4, 0x9d, 0xd6, 0xf7, 0x00,
+ 0xc1, 0xd4, 0x22, 0x03, 0x9d, 0x5e, 0x37, 0x93, 0x4a, 0x8a, 0x1a, 0xf1,
+ 0xb8, 0x8e, 0x86, 0xf3, 0x82, 0x4f, 0xd5, 0x9e, 0xb0, 0x2c, 0x4b, 0xba,
+ 0x96, 0x4b, 0x6a, 0xca, 0x0f, 0xd0, 0xf1, 0xfd, 0xd0, 0x1e, 0xe7, 0xa9,
+ 0xfd, 0x39, 0x1d, 0x15, 0xf5, 0x2b, 0x67, 0xef, 0x65, 0x8f, 0xe4, 0x15,
+ 0x91, 0x0e, 0xc1, 0x6c, 0xc2, 0xfa, 0xad, 0xe1, 0xfd, 0x4b, 0x0d, 0x95,
+ 0x39, 0x1d, 0xbf, 0xe7, 0x02, 0x7c, 0x32, 0x6d, 0x3c, 0x9b, 0x49, 0x05,
+ 0x7c, 0x89, 0xa6, 0x5a, 0xeb, 0x7e, 0x0e, 0xf8, 0x66, 0xd8, 0xdf, 0xb5,
+ 0x17, 0x25, 0x72, 0x72, 0xa9, 0x66, 0x15, 0x05, 0x17, 0xfd, 0x51, 0x1b,
+ 0x9c, 0xae, 0x02, 0x39, 0xf2, 0xec, 0x53, 0x45, 0x00, 0x1f, 0x89, 0x93,
+ 0x22, 0xf7, 0xed, 0xe4, 0xd5, 0x33, 0xb4, 0xf7, 0x0c, 0xf8, 0xc7, 0xa1,
+ 0x85, 0x3d, 0x03, 0xd1, 0x44, 0xf3, 0x36, 0xfc, 0x1d, 0x0c, 0xca, 0x47,
+ 0xa7, 0xe2, 0x5b, 0x1d, 0x1f, 0x1b, 0x29, 0x04, 0xe8, 0x11, 0xa2, 0x43,
+ 0xa9, 0x32, 0x76, 0xc3, 0x6a, 0x38, 0xe2, 0xac, 0xe9, 0xc4, 0xdb, 0x13,
+ 0x37, 0x5c, 0x2d, 0xd3, 0x02, 0x6a, 0x4b, 0x5f, 0xd7, 0x78, 0x74, 0x02,
+ 0xfb, 0x1a, 0x60, 0x56, 0x2b, 0xab, 0x7a, 0xa8, 0x89, 0xcc, 0x78, 0xb0,
+ 0x9a, 0xe4, 0x0a, 0x5b, 0x08, 0xca, 0x1d, 0x55, 0x2f, 0xbf, 0x46, 0xee,
+ 0xaa, 0x38, 0xcc, 0x85, 0x4e, 0x9c, 0x7e, 0x25, 0x22, 0x51, 0x84, 0x3a,
+ 0xda, 0x25, 0xfa, 0x6e, 0x32, 0x4a, 0xa3, 0xd4, 0x45, 0x92, 0xba, 0x46,
+ 0x87, 0x48, 0x48, 0x3b, 0xa8, 0xd8, 0x2c, 0xd0, 0x98, 0x2f, 0xe5, 0xe3,
+ 0xee, 0xd8, 0xf5, 0xba, 0x55, 0xf4, 0xe4, 0x4d, 0x74, 0xf5, 0x7e, 0xa0,
+ 0x8e, 0x31, 0x20, 0x30, 0xe0, 0x33, 0x10, 0xac, 0x22, 0x9f, 0xcb, 0xf9,
+ 0x7d, 0xef, 0x1c, 0x0a, 0x59, 0x79, 0x35, 0xa3, 0x50, 0x36, 0x42, 0x71,
+ 0x1a, 0x2b, 0x06, 0x2c, 0x90, 0xac, 0x16, 0x77, 0x66, 0x42, 0x84, 0xd3,
+ 0x68, 0xbc, 0xe7, 0x0b, 0x60, 0x33, 0x71, 0x09, 0xa2, 0xa8, 0xa8, 0x1a,
+ 0x5c, 0x15, 0xbe, 0xc7, 0xb9, 0x19, 0xc7, 0xcd, 0x00, 0xa1, 0x84, 0x57,
+ 0xe5, 0x59, 0xa1, 0x7e, 0xc2, 0xe6, 0x45, 0xa1, 0xec, 0x19, 0xf4, 0xd1,
+ 0x5d, 0x35, 0xc0, 0x0a, 0x0e, 0xd9, 0xce, 0x96, 0x74, 0x6a, 0x94, 0x5b,
+ 0x21, 0x02, 0xce, 0x86, 0xd3, 0x7e, 0x26, 0x84, 0x66, 0xe0, 0x40, 0xc9,
+ 0xa1, 0xe1, 0xfb, 0x4c, 0xab, 0x42, 0x7d, 0x52, 0x0e, 0x62, 0x6a, 0xaf,
+ 0xe8, 0xe0, 0x40, 0x00, 0xb0, 0x0c, 0xc5, 0xc9, 0x3d, 0xbb, 0x32, 0xe9,
+ 0x80, 0x28, 0xf2, 0xb5, 0x31, 0xf6, 0x36, 0x3d, 0xb7, 0xc4, 0x27, 0x6a,
+ 0x34, 0x3b, 0x3c, 0x4b, 0xc5, 0x25, 0x4f, 0x62, 0xdc, 0x2c, 0xfb, 0x22,
+ 0x0a, 0x54, 0x81, 0x16, 0x1f, 0xa2, 0x03, 0x61, 0xaf, 0x38, 0x04, 0x81,
+ 0x2f, 0x68, 0x52, 0xae, 0xb4, 0x85, 0x7e, 0x3c, 0xd7, 0xd2, 0xca, 0x73,
+ 0xc2, 0x48, 0x7a, 0xad, 0x05, 0x09, 0xfb, 0x7d, 0xd6, 0xed, 0xc8, 0x04,
+ 0xaf, 0x91, 0x7b, 0x5e, 0xa0, 0xbe, 0x19, 0x84, 0x98, 0xca, 0x00, 0x6f,
+ 0x14, 0x9c, 0xc6, 0x02, 0xbf, 0x40, 0xd1, 0x7e, 0x14, 0x31, 0xc7, 0xa3,
+ 0xf7, 0xbb, 0x8f, 0xd6, 0x6a, 0x38, 0xaf, 0x5b, 0x1f, 0x90, 0xa8, 0xff,
+ 0xc8, 0xe3, 0x5c, 0x95, 0x32, 0x88, 0x66, 0xbb, 0x36, 0x91, 0x04, 0xbc,
+ 0xfe, 0x15, 0x13, 0xce, 0x31, 0x17, 0x2a, 0x8b, 0xb4, 0x15, 0x1e, 0xc7,
+ 0xf0, 0xdf, 0x45, 0x3e, 0x22, 0x0e, 0x1d, 0x9e, 0xf2, 0x57, 0x38, 0x07,
+ 0x4f, 0xf6, 0x64, 0xb1, 0x31, 0xf5, 0xcc, 0x7f, 0xdb, 0x38, 0x80, 0x69,
+ 0x0c, 0xbc, 0x69, 0xbd, 0x6b, 0x40, 0x85, 0x21, 0x58, 0xb7, 0x89, 0x79,
+ 0xd3, 0x5d, 0xef, 0x34, 0xf6, 0xee, 0xee, 0x5f, 0x71, 0x0c, 0x65, 0x19,
+ 0x97, 0xcd, 0x41, 0x46, 0x93, 0x71, 0x32, 0x61, 0x84, 0xff, 0xd0, 0x84,
+ 0x3f, 0xa5, 0x97, 0xa5, 0x00, 0xb5, 0x4b, 0x88, 0x98, 0xaf, 0xfb, 0xe1,
+ 0x82, 0x1d, 0x9f, 0x6b, 0x8c, 0xbf, 0xeb, 0xee, 0xe8, 0x07, 0xaf, 0x92,
+ 0x6d, 0x3e, 0x6a, 0x12, 0xb2, 0x64, 0x6a, 0x6c, 0x07, 0xaa, 0x6b, 0x07,
+ 0x09, 0x7d, 0x30, 0x67, 0x17, 0xd5, 0x9c, 0x50, 0xd4, 0x51, 0x11, 0x52,
+ 0xeb, 0x2b, 0x44, 0x11, 0x80, 0x64, 0x1a, 0x24, 0xf5, 0xdd, 0xc5, 0x78,
+ 0xd0, 0x9c, 0x75, 0x32, 0x74, 0x41, 0xca, 0x89, 0x08, 0x87, 0xf8, 0xb5,
+ 0x09, 0x44, 0xd9, 0x18, 0x09, 0x27, 0x77, 0x7a, 0xb7, 0x83, 0x1b, 0x48,
+ 0xe6, 0x2c, 0x8d, 0xbe, 0x44, 0x21, 0xcf, 0xa5, 0x20, 0x01, 0x07, 0x9d,
+ 0xa4, 0x2b, 0x74, 0x32, 0x30, 0xc9, 0x46, 0x92, 0xde, 0x32, 0x3e, 0x49,
+ 0x7a, 0xfa, 0x30, 0xf4, 0x51, 0x64, 0x4a, 0xb6, 0x84, 0x75, 0x15, 0x9a,
+ 0x1a, 0xb8, 0x13, 0xb6, 0xe6, 0x00, 0x12, 0xcc, 0x16, 0x72, 0x5b, 0x73,
+ 0x70, 0xb8, 0x53, 0x5f, 0x77, 0x31, 0xad, 0xeb, 0x81, 0x4f, 0x12, 0xaa,
+ 0x4b, 0xeb, 0xd5, 0x73, 0x32, 0xa4, 0x9a, 0x56, 0xf7, 0xed, 0x5d, 0xcb,
+ 0xd7, 0xf6, 0xa3, 0x99, 0x51, 0xb8, 0xb6, 0x9d, 0x7c, 0x5d, 0x75, 0x06,
+ 0x94, 0x8d, 0x59, 0x47, 0x32, 0xd2, 0x3d, 0x11, 0x74, 0x0a, 0x84, 0x4a,
+ 0x8a, 0x72, 0xf8, 0x46, 0xfd, 0xe1, 0x00, 0x85, 0x90, 0x0d, 0xf0, 0xcb,
+ 0x9f, 0xb1, 0x7a, 0x6c, 0x01, 0x63, 0xc0, 0x34, 0xc5, 0x2e, 0xd5, 0x38,
+ 0x00, 0x00, 0x00, 0x01, 0x7a, 0x6e, 0xa3, 0xaa, 0xc7, 0xf5, 0xb1, 0x7f,
+ 0x7e, 0xec, 0x6e, 0x9f, 0x02, 0x11, 0x40, 0x10, 0xb5, 0xd0, 0x38, 0xc6,
+ 0xab, 0x56, 0x70, 0x30, 0x93, 0xf1, 0xde, 0x78, 0x21, 0x83, 0x8f, 0x78,
+ 0x4b, 0x78, 0xce, 0x82, 0xb6, 0x9b, 0x29, 0xe5, 0x8f, 0x49, 0x16, 0x0d,
+ 0x81, 0x37, 0x76, 0xbd, 0xf6, 0x9b, 0x07, 0xb9, 0x45, 0x74, 0xbd, 0x75,
+ 0x71, 0x9a, 0xe8, 0xde, 0x12, 0xd7, 0x9a, 0x44, 0xca, 0xcd, 0xa1, 0x8f,
+ 0xf0, 0xf5, 0x67, 0x8d, 0xbc, 0x62, 0x28, 0x59, 0x14, 0xd1, 0xdb, 0x95,
+ 0x18, 0xad, 0x27, 0xa5, 0x70, 0x13, 0x8d, 0x1a, 0x32, 0x56, 0xab, 0xcc,
+ 0xd3, 0x16, 0x4c, 0x68, 0x56, 0xbb, 0xe1, 0x6c, 0x33, 0x72, 0xc4, 0xc8,
+ 0x15, 0x9e, 0x8b, 0xe2, 0x0a, 0xd7, 0xac, 0xc5, 0x16, 0xe6, 0x68, 0xdb,
+ 0xeb, 0x6c, 0x38, 0x16, 0xf3, 0xea, 0x4e, 0x68, 0xe7, 0xe3, 0x1f, 0x89,
+ 0xf8, 0x4d, 0xa2, 0x5f, 0x33, 0x4c, 0x87, 0x3c, 0x74, 0x2d, 0x60, 0xd9,
+ 0x88, 0xe8, 0x75, 0x25, 0x11, 0xfa, 0x0f, 0x8f, 0x4f, 0xc3, 0x0f, 0x48,
+ 0x35, 0x46, 0xed, 0x47, 0x3c, 0x85, 0x8f, 0x93, 0x71, 0xa7, 0xa4, 0x04,
+ 0x06, 0xc6, 0x68, 0x41, 0x77, 0xd7, 0x6e, 0x36, 0x93, 0xd2, 0x57, 0x8e,
+ 0xd2, 0x8e, 0x6c, 0x4a, 0x15, 0x33, 0xee, 0x8c, 0xdf, 0xd7, 0xb8, 0x04,
+ 0x57, 0xb0, 0x34, 0xd9, 0x35, 0x59, 0x91, 0x46, 0x86, 0x9f, 0xf4, 0x8e,
+ 0x82, 0x88, 0x94, 0x7e, 0xc8, 0x6b, 0x25, 0x78, 0xdf, 0x2a, 0x80, 0x24,
+ 0x5e, 0x0b, 0x1e, 0x84, 0x53, 0xca, 0xd5, 0x43, 0x5f, 0xfc, 0xb7, 0xbd,
+ 0x81, 0xc9, 0xbf, 0x8d, 0xf9, 0xb5, 0xfe, 0xcd, 0x3b, 0x3e, 0x8e, 0xcb,
+ 0x34, 0xcc, 0xd8, 0xc4, 0xe5, 0xaf, 0x68, 0x18, 0x53, 0xac, 0x48, 0xbf,
+ 0x04, 0xb3, 0x38, 0x64, 0xf0, 0x9a, 0x3d, 0x7c, 0x8f, 0xc0, 0xb4, 0x7b,
+ 0x75, 0x48, 0x49, 0x72, 0x04, 0x2e, 0x28, 0x14, 0x90, 0xbc, 0x12, 0xc6,
+ 0xee, 0x2a, 0xa1, 0x90, 0xaf, 0x7f, 0xb7, 0xa9, 0x46, 0x2d, 0x40, 0xfd,
+ 0x4b, 0xf6, 0x5c, 0xf8, 0x6d, 0xd9, 0xd0, 0x8a, 0x58, 0xf1, 0xae, 0x9b,
+ 0xbc, 0xfd, 0xec, 0x17, 0xa0, 0x74, 0x94, 0x28, 0x6a, 0xf4, 0xf0, 0x47,
+ 0xe4, 0xf2, 0xe7, 0x2b, 0xfc, 0xcc, 0x83, 0x9c, 0xe7, 0xcf, 0xcb, 0x47,
+ 0xa2, 0x96, 0xca, 0x4f, 0xff, 0x79, 0xf7, 0x96, 0xd1, 0xfa, 0x10, 0x3d,
+ 0x93, 0x03, 0xe2, 0xbb, 0x03, 0x62, 0xdc, 0xf1, 0x53, 0xe0, 0x69, 0xe5,
+ 0xac, 0x45, 0xbb, 0x86, 0x7a, 0x0f, 0xb6, 0x79, 0xc9, 0xcf, 0x32, 0x04,
+ 0x5d, 0xce, 0x2b, 0xde, 0x58, 0x78, 0x3e, 0x24, 0x73, 0x07, 0xeb, 0xab,
+ 0x80, 0xed, 0x06, 0xe1, 0x2f, 0x72, 0x2b, 0xf0, 0x8d, 0xff, 0x00, 0x00,
+ 0x00, 0x00, 0xed, 0x7f, 0x76, 0xcd, 0x2f, 0x8f, 0xec, 0x04, 0x16, 0x1e,
+ 0x91, 0x73, 0xc7, 0x81, 0x80, 0x77, 0xf7, 0x4d, 0x29, 0x6b, 0x2f, 0x8c,
+ 0x8e, 0xb8, 0x97, 0xe9, 0x9e, 0xe5, 0xcb, 0x08, 0xe4, 0x67, 0x42, 0x31,
+ 0x8b, 0xd9, 0x23, 0x51, 0xa6, 0xc7, 0xfa, 0x2c, 0x30, 0x76, 0xfb, 0x66,
+ 0xfe, 0x39, 0x2f, 0xde, 0x53, 0x3f, 0x31, 0xf3, 0x81, 0x4f, 0x29, 0xb8,
+ 0x62, 0xa3, 0xc6, 0xc9, 0x19, 0x43, 0x03, 0x34, 0x5c, 0x35, 0x95, 0xb2,
+ 0x5a, 0x06, 0x8d, 0xe1, 0x05, 0x17, 0xb9, 0xfd, 0x78, 0xe6, 0xde, 0x8b,
+ 0x6c, 0x06, 0x4b, 0x61, 0xb8, 0x2f, 0x65, 0x3c, 0x37, 0x8d, 0x7d, 0x7a,
+ 0x67, 0x8e, 0xf1, 0x50, 0x70, 0x6a, 0xff, 0x7f, 0x6e, 0x35, 0x32, 0x2f,
+ 0x45, 0x8d, 0xb8, 0xdd, 0xca, 0x58, 0x68, 0x45, 0xbf, 0x5b, 0xf8, 0xb4,
+ 0xbc, 0x3c, 0x52, 0xfb, 0xa3, 0x78, 0x45, 0xea, 0x60, 0xe2, 0x37, 0xcd,
+ 0xb5, 0xc3, 0xca, 0x95, 0xe1, 0x72, 0x3f, 0x0f, 0xd5, 0x12, 0x7d, 0x2f,
+ 0x54, 0xd8, 0xea, 0x6c, 0x38, 0x36, 0x88, 0x46, 0xf8, 0x89, 0x61, 0xa7,
+ 0x6c, 0x10, 0xa8, 0xec, 0xa2, 0x6b, 0x4f, 0x36, 0x07, 0x71, 0xbf, 0x97,
+ 0x02, 0x65, 0xa0, 0x61, 0x18, 0x4a, 0x98, 0x40, 0xcb, 0xf1, 0xb3, 0x93,
+ 0x43, 0xc7, 0xb2, 0x09, 0x86, 0xe5, 0x23, 0x36, 0x37, 0x88, 0x04, 0xa5,
+ 0x3a, 0xef, 0xde, 0x03, 0x83, 0xba, 0x70, 0x1a, 0x9d, 0x1a, 0x8d, 0xac,
+ 0x3c, 0xa5, 0x22, 0x43, 0x89, 0x4f, 0x03, 0x9b, 0x6c, 0x7d, 0xe0, 0x45,
+ 0xe4, 0x84, 0x64, 0x1b, 0x4a, 0xc6, 0xee, 0x85, 0x69, 0xb7, 0xfb, 0x96,
+ 0xde, 0xd0, 0x29, 0xc0, 0xaf, 0xe4, 0x02, 0x7d, 0x57, 0x37, 0xa9, 0xb0,
+ 0x7f, 0xd7, 0x3c, 0xc3, 0x7d, 0x35, 0xa9, 0x1c, 0x4e, 0x9a, 0xdc, 0xae,
+ 0x33, 0xc5, 0xfe, 0x79, 0xb4, 0x9c, 0x57, 0x68, 0xe5, 0x2d, 0xe4, 0x13,
+ 0x7d, 0x5f, 0x66, 0xfa, 0xc8, 0xd2, 0x4b, 0x52, 0x99, 0x9d, 0x1c, 0x5c,
+ 0xee, 0x41, 0xa3, 0x59, 0x1c, 0xe6, 0xbf, 0x80, 0x00, 0x00, 0x0c, 0xa1,
+ 0x1a, 0x9a, 0x69, 0x82, 0x2d, 0x3c, 0x15, 0x90, 0x25, 0x28, 0x1f, 0xf3,
+ 0x3b, 0x35, 0xdf, 0x20, 0x8b, 0x36, 0x80, 0xa4, 0xe7, 0x7b, 0x42, 0x92,
+ 0xe6, 0xa2, 0x73, 0xbf, 0xa4, 0xfd, 0x58, 0xe6, 0xe9, 0xac, 0xd6, 0xd1,
+ 0xfe, 0x7d, 0xba, 0x8e, 0xaf, 0x31, 0x70, 0x2b, 0x96, 0x85, 0x98, 0x9c,
+ 0xd5, 0x29, 0xda, 0xb7, 0x43, 0x90, 0xbc, 0x04, 0x34, 0xe2, 0xa4, 0x6c,
+ 0x0a, 0xbf, 0x73, 0x86, 0xf1, 0x95, 0xfa, 0x8d, 0x19, 0x8c, 0xb9, 0xfa,
+ 0x8a, 0x36, 0x60, 0xaf, 0x7c, 0xd8, 0x7b, 0x9d, 0x3d, 0xbb, 0xed, 0xd7,
+ 0xef, 0xb8, 0x89, 0x84, 0xea, 0xf7, 0xf2, 0xef, 0x2f, 0x6c, 0xdc, 0xbc,
+ 0xb1, 0x9d, 0x56, 0x34, 0x9c, 0xe3, 0xfb, 0x08, 0xf9, 0xa8, 0xe6, 0xb6,
+ 0x94, 0x99, 0x41, 0xf3, 0x61, 0xc4, 0xe1, 0x44, 0xee, 0x9b, 0x29, 0xf1,
+ 0x94, 0x7e, 0xf9, 0x27, 0xc6, 0x7b, 0x00, 0x46, 0x6b, 0xd4, 0xd8, 0x5e,
+ 0x2b, 0xfa, 0x3a, 0xd4, 0x06, 0x1e, 0x83, 0x71, 0x04, 0xcf, 0x8f, 0x29,
+ 0x47, 0x92, 0x82, 0xd9, 0x0e, 0x2d, 0x33, 0x7f, 0x80, 0x1b, 0x20, 0x7b,
+ 0xde, 0x68, 0xac, 0xd9, 0x3c, 0xcd, 0x4c, 0x1f, 0xbe, 0xb4, 0x5d, 0x27,
+ 0x90, 0xbd, 0xdf, 0x95, 0x10, 0x44, 0x65, 0x58, 0x93, 0xc0, 0x1f, 0xe8,
+ 0x48, 0x14, 0x99, 0xd8, 0x65, 0x43, 0x51, 0xfa, 0xcb, 0x01, 0x2e, 0x43,
+ 0xe3, 0x79, 0x4b, 0x1b, 0x78, 0xac, 0x8e, 0x29, 0x16, 0x05, 0x05, 0x3a,
+ 0xaf, 0xfd, 0xca, 0x2c, 0x50, 0x89, 0x29, 0x38, 0x21, 0x1c, 0x6a, 0xc3,
+ 0xb2, 0x65, 0xfc, 0xe6, 0xd2, 0xfb, 0xdc, 0xfd, 0xc7, 0x62, 0x12, 0x97,
+ 0xd5, 0xdd, 0x72, 0xe1, 0xf7, 0x8d, 0x1b, 0x49, 0x3e, 0x49, 0x91, 0x66,
+ 0xeb, 0x8c, 0x94, 0x5e, 0x13, 0x44, 0xac, 0x44, 0x18, 0xc0, 0x2f, 0xe6,
+ 0xd7, 0xf9, 0x78, 0xbf, 0xb8, 0x65, 0xb6, 0x71, 0x44, 0xf6, 0xdc, 0x9d,
+ 0xd0, 0x12, 0xd1, 0x44, 0x1b, 0x4c, 0xb4, 0xb3, 0x83, 0xce, 0xf5, 0xee,
+ 0x9a, 0xf8, 0x8a, 0xeb, 0xaa, 0xc5, 0xc4, 0xe0, 0x9a, 0xec, 0x82, 0x3c,
+ 0x2b, 0x35, 0xe4, 0xe2, 0x20, 0x37, 0xcb, 0xc6, 0x4e, 0xb0, 0xbd, 0xd3,
+ 0x77, 0xdb, 0x0a, 0xf7, 0xd2, 0xc6, 0x22, 0xb2, 0x2d, 0x6b, 0x17, 0xd5,
+ 0xb1, 0xbf, 0x73, 0xc9, 0xe0, 0xc8, 0x9f, 0x41, 0x37, 0xd4, 0x26, 0xdb,
+ 0x33, 0xc0, 0x80, 0x41, 0xaa, 0x1e, 0x04, 0xed, 0x47, 0x68, 0x30, 0x51,
+ 0xf4, 0x90, 0x31, 0x6d, 0x86, 0x66, 0x8c, 0xb2, 0x96, 0xbf, 0x1c, 0x32,
+ 0xd9, 0x6e, 0x2e, 0x02, 0x61, 0x61, 0x27, 0x4a, 0x54, 0x8d, 0x49, 0x18,
+ 0x56, 0xd1, 0x5a, 0xc1, 0x56, 0x96, 0x9c, 0x5e, 0x9c, 0x09, 0xa3, 0x02,
+ 0x74, 0x83, 0x6a, 0x5e, 0xd3, 0xe5, 0x47, 0xd7, 0x5b, 0xff, 0x6d, 0x38,
+ 0x10, 0xd4, 0x6b, 0x0f, 0x18, 0xae, 0xfa, 0x8f, 0x68, 0xf2, 0x6a, 0x3c,
+ 0x70, 0x00, 0x18, 0x81, 0xb9, 0x5a, 0x94, 0x46, 0x99, 0x3e, 0xc2, 0x4d,
+ 0xae, 0x83, 0x0c, 0x9f, 0x3f, 0x3d, 0xe5, 0xac, 0x65, 0x28, 0xdc, 0xb9,
+ 0x61, 0x24, 0x11, 0xd5, 0x52, 0xc8, 0x29, 0xc2, 0xf3, 0xf5, 0x6f, 0x8a,
+ 0x6c, 0xa6, 0x38, 0xa9, 0xb0, 0x87, 0x79, 0x19, 0xc2, 0x8f, 0x77, 0x9a,
+ 0x73, 0x00, 0xba, 0x82, 0x4d, 0x18, 0x86, 0xb7, 0xf8, 0xc0, 0x56, 0xbe,
+ 0xdd, 0xf7, 0xdf, 0xc7, 0xec, 0x9f, 0xfa, 0x9a, 0x56, 0xd5, 0xbc, 0x35,
+ 0x6b, 0x73, 0xbf, 0xfb, 0x0a, 0xa9, 0xaa, 0x1b, 0x92, 0xcc, 0xfa, 0x42,
+ 0x40, 0xbb, 0x0c, 0x5d, 0xd2, 0x86, 0xbe, 0x0f, 0x1b, 0x1a, 0x77, 0x92,
+ 0x65, 0x95, 0x80, 0xa7, 0x4a, 0x13, 0x56, 0xed, 0x00, 0x78, 0xb5, 0x68,
+ 0x22, 0xac, 0x1c, 0x40, 0x5c, 0xac, 0xe0, 0xc8, 0xe8, 0xe6, 0xaa, 0x9d,
+ 0x44, 0xb0, 0x29, 0xc4, 0xbe, 0x1d, 0xd0, 0xee, 0x95, 0x2c, 0x6b, 0x29,
+ 0x64, 0xfe, 0x44, 0xb1, 0xa7, 0xde, 0x33, 0x0f, 0xb5, 0x74, 0x26, 0x6f,
+ 0x49, 0x2b, 0x48, 0x13, 0xec, 0xe6, 0xa5, 0xce, 0xd7, 0x87, 0x7a, 0x67,
+ 0x0c, 0x29, 0x50, 0x7b, 0x7d, 0x26, 0xc9, 0xf8, 0x3f, 0xf3, 0xe0, 0x48,
+ 0x8c, 0xa1, 0xec, 0x75, 0xd7, 0xe0, 0x61, 0xea, 0x0d, 0x85, 0x79, 0x7f,
+ 0x30, 0x85, 0xe4, 0x18, 0xa0, 0x46, 0x99, 0x6f, 0xce, 0xd7, 0x53, 0x1a,
+ 0xc5, 0x7d, 0x82, 0x3e, 0x3f, 0xe9, 0x99, 0x42, 0x51, 0x99, 0x5d, 0xf9,
+ 0x8f, 0xc5, 0x68, 0x1a, 0x94, 0x82, 0x21, 0xab, 0x46, 0x9e, 0x35, 0xe6,
+ 0xdf, 0xd3, 0x69, 0x98, 0x5d, 0xb1, 0xfb, 0x10, 0x2d, 0x6a, 0x48, 0x87,
+ 0xe0, 0x20, 0xe5, 0xae, 0xec, 0xda, 0x3d, 0x8e, 0xdb, 0x1b, 0x2a, 0x8b,
+ 0x5d, 0x61, 0xa4, 0x95, 0x25, 0x5d, 0x54, 0xf3, 0xfc, 0x14, 0x66, 0x3b,
+ 0x00, 0xdc, 0x59, 0x55, 0x67, 0x40, 0x13, 0xbe, 0x57, 0x67, 0x5f, 0xb4,
+ 0xdf, 0x90, 0x23, 0x08, 0x3f, 0x4a, 0x98, 0x3c, 0xb9, 0x4b, 0x4d, 0xc2,
+ 0x12, 0x68, 0x29, 0xd7, 0x0c, 0x5b, 0x43, 0x0f, 0x5a, 0x62, 0x6d, 0xe0,
+ 0x35, 0x9b, 0xb1, 0x63, 0x1a, 0x50, 0x3f, 0xd9, 0x19, 0xd8, 0x7c, 0x64,
+ 0xa6, 0xf1, 0x6a, 0x65, 0xbc, 0xe1, 0x66, 0x1f, 0x41, 0xb7, 0x6a, 0x27,
+ 0x11, 0xa0, 0x5c, 0x12, 0x4f, 0x97, 0xd5, 0xac, 0xb6, 0x1d, 0xcc, 0x9c,
+ 0xe2, 0xff, 0x13, 0xee, 0x61, 0x99, 0xcf, 0x48, 0x54, 0x24, 0x1f, 0x32,
+ 0x69, 0x98, 0x59, 0xfd, 0xe5, 0x56, 0xb6, 0x89, 0x9f, 0xf3, 0x12, 0x5a,
+ 0x92, 0x83, 0x92, 0x57, 0x55, 0x1f, 0xa5, 0xf5, 0x07, 0x6a, 0x00, 0xad,
+ 0xac, 0x80, 0x5d, 0x76, 0x42, 0xd4, 0x44, 0xcf, 0x2b, 0x48, 0x67, 0xdd,
+ 0x47, 0xd1, 0xbc, 0x7d, 0xd1, 0x41, 0x0a, 0xc9, 0x71, 0x4d, 0x83, 0xa1,
+ 0x4b, 0xf3, 0x70, 0x23, 0xe6, 0xef, 0x83, 0x33, 0x8c, 0x0c, 0x47, 0xcb,
+ 0x18, 0x6c, 0xef, 0xf1, 0x48, 0xf1, 0xbd, 0x2d, 0x22, 0xac, 0x5f, 0xfa,
+ 0x0b, 0x60, 0x44, 0xa4, 0x50, 0x60, 0x72, 0x2b, 0x4d, 0x13, 0xc6, 0xb7,
+ 0xb8, 0x55, 0xd5, 0xc1, 0xe1, 0x46, 0x34, 0xc3, 0x25, 0x1e, 0x71, 0xf2,
+ 0xf8, 0x61, 0xa4, 0x59, 0x09, 0xba, 0x4f, 0x6f, 0xd3, 0xac, 0xae, 0x65,
+ 0x38, 0xa8, 0x99, 0xfb, 0xfe, 0x7e, 0xf0, 0x7c, 0xdd, 0xca, 0xae, 0x70,
+ 0x17, 0xc4, 0x55, 0xd9, 0x8e, 0x24, 0xb5, 0x08, 0x0f, 0x85, 0xba, 0xff,
+ 0x8c, 0x5a, 0x41, 0x96, 0x6c, 0xdb, 0xfd, 0x24, 0x01, 0x17, 0xfb, 0x04,
+ 0x2c, 0xb6, 0x58, 0x0f, 0x48, 0x6b, 0xa8, 0x9b, 0xb3, 0x61, 0x39, 0x6e,
+ 0x19, 0x54, 0x5b, 0xe2, 0x83, 0x44, 0x39, 0x45, 0xfc, 0xf8, 0xca, 0xd4,
+ 0x24, 0xa3, 0x5c, 0x6d, 0x96, 0x35, 0x9b, 0x19, 0x85, 0xa8, 0xc9, 0xc4,
+ 0x1b, 0xf2, 0x0d, 0x59, 0xb9, 0x19, 0xb4, 0xdf, 0xd4, 0xcd, 0x55, 0xbb,
+ 0x8a, 0xce, 0x1a, 0xfe, 0xeb, 0x0e, 0x2b, 0x35, 0x74, 0x74, 0xeb, 0xd7,
+ 0xb5, 0xef, 0x43, 0x79, 0xa8, 0x9f, 0x3c, 0x7d, 0xde, 0x23, 0x3c, 0x72,
+ 0xab, 0xed, 0x8d, 0xb9, 0xa5, 0x19, 0x9d, 0x5f, 0xef, 0xf8, 0x2d, 0xc0,
+ 0xfe, 0xab, 0xc7, 0x01, 0x31, 0x20, 0x17, 0x22, 0xa5, 0x56, 0x10, 0x38,
+ 0x15, 0x69, 0x25, 0xd3, 0xf8, 0x3e, 0x14, 0x9c, 0xa0, 0x51, 0x9d, 0x1d,
+ 0xd3, 0xb5, 0x87, 0x94, 0x47, 0x09, 0xd7, 0x5f, 0x78, 0x9b, 0xd0, 0x4b,
+ 0xe9, 0x52, 0x5e, 0x9b, 0x51, 0x22, 0x48, 0xe4, 0x00, 0x96, 0xc4, 0x85,
+ 0x11, 0xb3, 0x19, 0xd4, 0x9f, 0x88, 0x0a, 0x6d, 0x14, 0xec, 0x5a, 0xd5,
+ 0xac, 0x4d, 0xf7, 0x1e, 0x16, 0xe1, 0x9b, 0x80, 0x82, 0x44, 0x76, 0x7c,
+ 0x21, 0xac, 0xe4, 0x39, 0xd3, 0xa3, 0x08, 0x8c, 0x10, 0xaa, 0xed, 0x15,
+ 0xb4, 0x27, 0xa6, 0xd0, 0xfe, 0x00, 0x8c, 0x73, 0x08, 0x5a, 0x12, 0x63,
+ 0x9c, 0xf5, 0x1b, 0x53, 0xf9, 0xbd, 0x4a, 0x00, 0x87, 0x2d, 0xe5, 0xf5,
+ 0xa3, 0xea, 0x84, 0xc1, 0x6f, 0xee, 0xc8, 0x71, 0x25, 0x64, 0xf7, 0xdc,
+ 0x77, 0x29, 0xf5, 0x8a, 0xfc, 0x93, 0x03, 0x0a, 0x9b, 0xaf, 0x44, 0xf7,
+ 0x7b, 0x56, 0xa6, 0x75, 0xd3, 0x3b, 0x35, 0xb2, 0x48, 0x35, 0xdd, 0x45,
+ 0x18, 0xf6, 0x85, 0x3d, 0x8f, 0x0d, 0x3c, 0x20, 0x02, 0x2d, 0x66, 0x63,
+ 0xb2, 0x31, 0x5d, 0xb0, 0x2e, 0x99, 0xaf, 0x2c, 0xd9, 0xa5, 0xe4, 0xae,
+ 0xc0, 0x28, 0x3e, 0x75, 0x52, 0xca, 0x4f, 0x3f, 0x66, 0x30, 0x08, 0xf9,
+ 0x97, 0xdd, 0x28, 0x07, 0x8f, 0x3c, 0x6d, 0x2e, 0xc1, 0xb0, 0x6c, 0x98,
+ 0x2e, 0x16, 0x36, 0x13, 0x4f, 0x33, 0x69, 0x74, 0x40, 0x54, 0x58, 0x90,
+ 0x9a, 0xbd, 0x84, 0xc1, 0xc5, 0x89, 0xbf, 0x8e, 0x9c, 0x96, 0x84, 0xd8,
+ 0x18, 0xdf, 0x41, 0xac, 0xdb, 0x0a, 0xc3, 0x39, 0x47, 0xf1, 0xe5, 0x1b,
+ 0xe9, 0xe4, 0xa6, 0x42, 0x4d, 0x14, 0x8e, 0x24, 0x0d, 0x14, 0x1e, 0xad,
+ 0x2c, 0x6f, 0xce, 0x1a, 0xd6, 0x23, 0xad, 0x18, 0x38, 0xbe, 0x63, 0xe1,
+ 0x11, 0xd8, 0x54, 0x2a, 0xca, 0x00, 0xf9, 0x0d, 0xd2, 0xb3, 0xca, 0x97,
+ 0x17, 0xd6, 0x44, 0xf4, 0x97, 0xcf, 0x4a, 0xa1, 0xd6, 0x3a, 0x09, 0xf2,
+ 0xc4, 0x98, 0xf6, 0x36, 0x1c, 0xa9, 0x68, 0x83, 0x06, 0x9d, 0xdb, 0x17,
+ 0x19, 0xd2, 0xa1, 0x5e, 0xbe, 0x11, 0x7b, 0x3a, 0x16, 0x39, 0x55, 0xdd,
+ 0xdc, 0x13, 0x30, 0xc2, 0xad, 0x5b, 0xb1, 0xb4, 0x17, 0x2a, 0x3b, 0xf2,
+ 0x9d, 0x96, 0x56, 0xcd, 0x20, 0xd4, 0xd9, 0xf4, 0xa8, 0x71, 0x14, 0xc9,
+ 0x4f, 0x8f, 0xc0, 0x0e, 0x43, 0xd6, 0x8c, 0xc3, 0x38, 0x58, 0xe8, 0x4c,
+ 0xa8, 0x68, 0x6c, 0x81, 0x9f, 0xbb, 0x00, 0xb6, 0x6e, 0x73, 0x6e, 0x65,
+ 0xfe, 0x73, 0x66, 0x6c, 0xae, 0xf3, 0x0e, 0x26, 0xd4, 0xf0, 0x2a, 0x1b,
+ 0xd0, 0xd4, 0x80, 0xf2, 0x2b, 0x8d, 0x36, 0xbb, 0x38, 0xc4, 0x00, 0x23,
+ 0xa0, 0x01, 0xd6, 0x1b, 0x17, 0x2a, 0x7d, 0x72, 0xa5, 0x1e, 0x89, 0x2a,
+ 0x14, 0x7b, 0xd1, 0x17, 0x51, 0x6f, 0x03, 0x8e, 0xce, 0x06, 0x6b, 0xe7,
+ 0x94, 0xf3, 0x15, 0xe5, 0x07, 0x89, 0xb8, 0x89, 0x04, 0x4d, 0x72, 0x30,
+ 0x78, 0xfc, 0xc0, 0x51, 0xac, 0x02, 0x68, 0x3a, 0x91, 0xde, 0xb7, 0x32,
+ 0xeb, 0x4f, 0x74, 0x09, 0x10, 0x9d, 0x74, 0xa2, 0x4f, 0x17, 0x95, 0x2a,
+ 0x75, 0x79, 0xad, 0x84, 0x3b, 0x98, 0xaa, 0xb8, 0xb1, 0x09, 0x25, 0x1d,
+ 0x6f, 0xd8, 0x35, 0xbe, 0xf0, 0x07, 0x03, 0xb8, 0xb1, 0xf8, 0x93, 0x72,
+ 0x33, 0xb8, 0xd2, 0xad, 0xcd, 0xd0, 0x6e, 0x99, 0x13, 0x00, 0x3b, 0x49,
+ 0x33, 0x7f, 0xec, 0x13, 0x71, 0xf6, 0x96, 0x45, 0xdf, 0xa3, 0x8b, 0x87,
+ 0x61, 0x47, 0x62, 0x6c, 0x91, 0x19, 0xe6, 0xbb, 0x8b, 0xc5, 0x46, 0x45,
+ 0xff, 0x57, 0xcf, 0xb6, 0x09, 0xa2, 0x6a, 0x80, 0x27, 0x8c, 0x1a, 0xea,
+ 0xa7, 0xca, 0x16, 0x0c, 0x66, 0x97, 0x00, 0x47, 0x2d, 0x34, 0x42, 0xad,
+ 0xfa, 0x41, 0x69, 0x85, 0x4c, 0x91, 0x23, 0x67, 0xd0, 0x6b, 0x4e, 0x27,
+ 0x5d, 0x75, 0xc5, 0x40, 0x39, 0xe2, 0xdf, 0x93, 0xe2, 0x23, 0x3a, 0x57,
+ 0xab, 0x36, 0xb7, 0x05, 0x31, 0x1b, 0x9c, 0xfd, 0x4e, 0x90, 0x26, 0xce,
+ 0xb4, 0x52, 0xff, 0x61, 0xc6, 0x76, 0x87, 0x58, 0x57, 0xf9, 0x39, 0x00,
+ 0x1c, 0x53, 0xbb, 0x6b, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x11,
+ 0xbb, 0x8f, 0xb3, 0x81, 0x0e, 0xb7, 0x8a, 0xf7, 0x81, 0x01, 0xf1, 0x82,
+ 0x01, 0xa7, 0xf0, 0x81, 0x03};
diff --git a/dom/media/XiphExtradata.cpp b/dom/media/XiphExtradata.cpp
new file mode 100644
index 0000000000..ff8ea1901d
--- /dev/null
+++ b/dom/media/XiphExtradata.cpp
@@ -0,0 +1,77 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "XiphExtradata.h"
+
+namespace mozilla {
+
+bool XiphHeadersToExtradata(MediaByteBuffer* aCodecSpecificConfig,
+ const nsTArray<const unsigned char*>& aHeaders,
+ const nsTArray<size_t>& aHeaderLens) {
+ size_t nheaders = aHeaders.Length();
+ if (!nheaders || nheaders > 255) return false;
+ aCodecSpecificConfig->AppendElement(nheaders - 1);
+ for (size_t i = 0; i < nheaders - 1; i++) {
+ size_t headerLen;
+ for (headerLen = aHeaderLens[i]; headerLen >= 255; headerLen -= 255) {
+ aCodecSpecificConfig->AppendElement(255);
+ }
+ aCodecSpecificConfig->AppendElement(headerLen);
+ }
+ for (size_t i = 0; i < nheaders; i++) {
+ aCodecSpecificConfig->AppendElements(aHeaders[i], aHeaderLens[i]);
+ }
+ return true;
+}
+
+bool XiphExtradataToHeaders(nsTArray<unsigned char*>& aHeaders,
+ nsTArray<size_t>& aHeaderLens, unsigned char* aData,
+ size_t aAvailable) {
+ size_t total = 0;
+ if (aAvailable < 1) {
+ return false;
+ }
+ aAvailable--;
+ int nHeaders = *aData++ + 1;
+ for (int i = 0; i < nHeaders - 1; i++) {
+ size_t headerLen = 0;
+ for (;;) {
+ // After this test, we know that (aAvailable - total > headerLen) and
+ // (headerLen >= 0) so (aAvailable - total > 0). The loop decrements
+ // aAvailable by 1 and total remains fixed, so we know that in the next
+ // iteration (aAvailable - total >= 0). Thus (aAvailable - total) can
+ // never underflow.
+ if (aAvailable - total <= headerLen) {
+ return false;
+ }
+ // Since we know (aAvailable > total + headerLen), this can't overflow
+ // unless total is near 0 and both aAvailable and headerLen are within
+ // 255 bytes of the maximum representable size. However, that is
+ // impossible, since we would have had to have gone through this loop
+ // more than 255 times to make headerLen that large, and thus decremented
+ // aAvailable more than 255 times.
+ headerLen += *aData;
+ aAvailable--;
+ if (*aData++ != 255) break;
+ }
+ // And this check ensures updating total won't cause (aAvailable - total)
+ // to underflow.
+ if (aAvailable - total < headerLen) {
+ return false;
+ }
+ aHeaderLens.AppendElement(headerLen);
+ // Since we know aAvailable >= total + headerLen, this can't overflow.
+ total += headerLen;
+ }
+ aHeaderLens.AppendElement(aAvailable - total);
+ for (int i = 0; i < nHeaders; i++) {
+ aHeaders.AppendElement(aData);
+ aData += aHeaderLens[i];
+ }
+ return true;
+}
+
+} // namespace mozilla
diff --git a/dom/media/XiphExtradata.h b/dom/media/XiphExtradata.h
new file mode 100644
index 0000000000..d3caea6244
--- /dev/null
+++ b/dom/media/XiphExtradata.h
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#if !defined(XiphExtradata_h)
+# define XiphExtradata_h
+
+# include "MediaData.h"
+
+namespace mozilla {
+
+/* This converts a list of headers to the canonical form of extradata for Xiph
+ codecs in non-Ogg containers. We use it to pass those headers from demuxer
+ to decoder even when demuxing from an Ogg cotainer. */
+bool XiphHeadersToExtradata(MediaByteBuffer* aCodecSpecificConfig,
+ const nsTArray<const unsigned char*>& aHeaders,
+ const nsTArray<size_t>& aHeaderLens);
+
+/* This converts a set of extradata back into a list of headers. */
+bool XiphExtradataToHeaders(nsTArray<unsigned char*>& aHeaders,
+ nsTArray<size_t>& aHeaderLens, unsigned char* aData,
+ size_t aAvailable);
+
+} // namespace mozilla
+
+#endif // XiphExtradata_h
diff --git a/dom/media/autoplay/AutoplayPolicy.cpp b/dom/media/autoplay/AutoplayPolicy.cpp
new file mode 100644
index 0000000000..8da0e60ead
--- /dev/null
+++ b/dom/media/autoplay/AutoplayPolicy.cpp
@@ -0,0 +1,497 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AutoplayPolicy.h"
+
+#include "mozilla/dom/AudioContext.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/FeaturePolicyUtils.h"
+#include "mozilla/dom/HTMLMediaElement.h"
+#include "mozilla/dom/HTMLMediaElementBinding.h"
+#include "mozilla/dom/NavigatorBinding.h"
+#include "mozilla/dom/UserActivation.h"
+#include "mozilla/dom/WindowContext.h"
+#include "mozilla/Logging.h"
+#include "mozilla/MediaManager.h"
+#include "mozilla/Components.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "nsContentUtils.h"
+#include "nsGlobalWindowInner.h"
+#include "nsIAutoplay.h"
+#include "nsIDocShell.h"
+#include "nsIDocShellTreeItem.h"
+#include "nsIPermissionManager.h"
+#include "nsIPrincipal.h"
+#include "nsPIDOMWindow.h"
+
+mozilla::LazyLogModule gAutoplayPermissionLog("Autoplay");
+
+#define AUTOPLAY_LOG(msg, ...) \
+ MOZ_LOG(gAutoplayPermissionLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
+
+using namespace mozilla::dom;
+
+namespace mozilla::media {
+
+static const uint32_t sPOLICY_STICKY_ACTIVATION = 0;
+// static const uint32_t sPOLICY_TRANSIENT_ACTIVATION = 1;
+static const uint32_t sPOLICY_USER_INPUT_DEPTH = 2;
+
+static bool IsActivelyCapturingOrHasAPermission(nsPIDOMWindowInner* aWindow) {
+ // Pages which have been granted permission to capture WebRTC camera or
+ // microphone or screen are assumed to be trusted, and are allowed to
+ // autoplay.
+ if (MediaManager::GetIfExists()) {
+ return MediaManager::GetIfExists()->IsActivelyCapturingOrHasAPermission(
+ aWindow->WindowID());
+ }
+
+ auto principal = nsGlobalWindowInner::Cast(aWindow)->GetPrincipal();
+ return (nsContentUtils::IsExactSitePermAllow(principal, "camera"_ns) ||
+ nsContentUtils::IsExactSitePermAllow(principal, "microphone"_ns) ||
+ nsContentUtils::IsExactSitePermAllow(principal, "screen"_ns));
+}
+
+static uint32_t SiteAutoplayPerm(nsPIDOMWindowInner* aWindow) {
+ if (!aWindow || !aWindow->GetBrowsingContext()) {
+ return nsIPermissionManager::UNKNOWN_ACTION;
+ }
+
+ WindowContext* topContext =
+ aWindow->GetBrowsingContext()->GetTopWindowContext();
+ if (!topContext) {
+ return nsIPermissionManager::UNKNOWN_ACTION;
+ }
+ return topContext->GetAutoplayPermission();
+}
+
+static bool IsWindowAllowedToPlayByUserGesture(nsPIDOMWindowInner* aWindow) {
+ if (!aWindow) {
+ return false;
+ }
+
+ WindowContext* topContext =
+ aWindow->GetBrowsingContext()->GetTopWindowContext();
+ if (topContext && topContext->HasBeenUserGestureActivated()) {
+ AUTOPLAY_LOG(
+ "Allow autoplay as top-level context has been activated by user "
+ "gesture.");
+ return true;
+ }
+ return false;
+}
+
+static bool IsWindowAllowedToPlayByTraits(nsPIDOMWindowInner* aWindow) {
+ if (!aWindow) {
+ return false;
+ }
+
+ if (IsActivelyCapturingOrHasAPermission(aWindow)) {
+ AUTOPLAY_LOG(
+ "Allow autoplay as document has camera or microphone or screen"
+ " permission.");
+ return true;
+ }
+
+ Document* currentDoc = aWindow->GetExtantDoc();
+ if (!currentDoc) {
+ return false;
+ }
+
+ bool isTopLevelContent = !aWindow->GetBrowsingContext()->GetParent();
+ if (currentDoc->MediaDocumentKind() == Document::MediaDocumentKind::Video &&
+ isTopLevelContent) {
+ AUTOPLAY_LOG("Allow top-level video document to autoplay.");
+ return true;
+ }
+
+ if (StaticPrefs::media_autoplay_allow_extension_background_pages() &&
+ currentDoc->IsExtensionPage()) {
+ AUTOPLAY_LOG("Allow autoplay as in extension document.");
+ return true;
+ }
+
+ return false;
+}
+
+static bool IsWindowAllowedToPlayOverall(nsPIDOMWindowInner* aWindow) {
+ return IsWindowAllowedToPlayByUserGesture(aWindow) ||
+ IsWindowAllowedToPlayByTraits(aWindow);
+}
+
+static uint32_t DefaultAutoplayBehaviour() {
+ int32_t prefValue = StaticPrefs::media_autoplay_default();
+ if (prefValue == nsIAutoplay::ALLOWED) {
+ return nsIAutoplay::ALLOWED;
+ }
+ if (prefValue == nsIAutoplay::BLOCKED_ALL) {
+ return nsIAutoplay::BLOCKED_ALL;
+ }
+ return nsIAutoplay::BLOCKED;
+}
+
+static bool IsMediaElementInaudible(const HTMLMediaElement& aElement) {
+ if (aElement.Volume() == 0.0 || aElement.Muted()) {
+ AUTOPLAY_LOG("Media %p is muted.", &aElement);
+ return true;
+ }
+
+ if (!aElement.HasAudio() &&
+ aElement.ReadyState() >= HTMLMediaElement_Binding::HAVE_METADATA) {
+ AUTOPLAY_LOG("Media %p has no audio track", &aElement);
+ return true;
+ }
+
+ return false;
+}
+
+static bool IsAudioContextAllowedToPlay(const AudioContext& aContext) {
+ // Offline context won't directly output sound to audio devices.
+ return aContext.IsOffline() ||
+ IsWindowAllowedToPlayOverall(aContext.GetParentObject());
+}
+
+static bool IsEnableBlockingWebAudioByUserGesturePolicy() {
+ return StaticPrefs::media_autoplay_block_webaudio() &&
+ StaticPrefs::media_autoplay_blocking_policy() ==
+ sPOLICY_STICKY_ACTIVATION;
+}
+
+static bool IsAllowedToPlayByBlockingModel(const HTMLMediaElement& aElement) {
+ const uint32_t policy = StaticPrefs::media_autoplay_blocking_policy();
+ if (policy == sPOLICY_STICKY_ACTIVATION) {
+ const bool isAllowed =
+ IsWindowAllowedToPlayOverall(aElement.OwnerDoc()->GetInnerWindow());
+ AUTOPLAY_LOG("Use 'sticky-activation', isAllowed=%d", isAllowed);
+ return isAllowed;
+ }
+ // If element is blessed, it would always be allowed to play().
+ const bool isElementBlessed = aElement.IsBlessed();
+ if (policy == sPOLICY_USER_INPUT_DEPTH) {
+ const bool isUserInput = UserActivation::IsHandlingUserInput();
+ AUTOPLAY_LOG("Use 'User-Input-Depth', isBlessed=%d, isUserInput=%d",
+ isElementBlessed, isUserInput);
+ return isElementBlessed || isUserInput;
+ }
+ const bool hasTransientActivation =
+ aElement.OwnerDoc()->HasValidTransientUserGestureActivation();
+ AUTOPLAY_LOG(
+ "Use 'transient-activation', isBlessed=%d, "
+ "hasValidTransientActivation=%d",
+ isElementBlessed, hasTransientActivation);
+ return isElementBlessed || hasTransientActivation;
+}
+
+// On GeckoView, we don't store any site's permission in permission manager, we
+// would check the GV request status to know if the site can be allowed to play.
+// But on other platforms, we would store the site's permission in permission
+// manager.
+#if defined(MOZ_WIDGET_ANDROID)
+using RType = GVAutoplayRequestType;
+
+static bool IsGVAutoplayRequestAllowed(nsPIDOMWindowInner* aWindow,
+ RType aType) {
+ if (!aWindow) {
+ return false;
+ }
+
+ RefPtr<BrowsingContext> context = aWindow->GetBrowsingContext()->Top();
+ GVAutoplayRequestStatus status =
+ aType == RType::eAUDIBLE ? context->GetGVAudibleAutoplayRequestStatus()
+ : context->GetGVInaudibleAutoplayRequestStatus();
+ return status == GVAutoplayRequestStatus::eALLOWED;
+}
+
+static bool IsGVAutoplayRequestAllowed(const HTMLMediaElement& aElement,
+ RType aType) {
+ // On GV, blocking model is the first thing we would check inside Gecko, and
+ // if the media is not allowed by that, then we would check the response from
+ // the embedding app to decide the final result.
+ if (IsAllowedToPlayByBlockingModel(aElement)) {
+ return true;
+ }
+
+ RefPtr<nsPIDOMWindowInner> window = aElement.OwnerDoc()->GetInnerWindow();
+ if (!window) {
+ return false;
+ }
+ return IsGVAutoplayRequestAllowed(window, aType);
+}
+#endif
+
+static bool IsAllowedToPlayInternal(const HTMLMediaElement& aElement) {
+#if defined(MOZ_WIDGET_ANDROID)
+ if (StaticPrefs::media_geckoview_autoplay_request()) {
+ return IsGVAutoplayRequestAllowed(
+ aElement, IsMediaElementInaudible(aElement) ? RType::eINAUDIBLE
+ : RType::eAUDIBLE);
+ }
+#endif
+ bool isInaudible = IsMediaElementInaudible(aElement);
+ bool isUsingAutoplayModel = IsAllowedToPlayByBlockingModel(aElement);
+
+ uint32_t defaultBehaviour = DefaultAutoplayBehaviour();
+ uint32_t sitePermission =
+ SiteAutoplayPerm(aElement.OwnerDoc()->GetInnerWindow());
+
+ AUTOPLAY_LOG(
+ "IsAllowedToPlayInternal, isInaudible=%d,"
+ "isUsingAutoplayModel=%d, sitePermission=%d, defaultBehaviour=%d",
+ isInaudible, isUsingAutoplayModel, sitePermission, defaultBehaviour);
+
+ // For site permissions we store permissionManager values except
+ // for BLOCKED_ALL, for the default pref values we store
+ // nsIAutoplay values.
+ if (sitePermission == nsIPermissionManager::ALLOW_ACTION) {
+ return true;
+ }
+
+ if (sitePermission == nsIPermissionManager::DENY_ACTION) {
+ return isInaudible || isUsingAutoplayModel;
+ }
+
+ if (sitePermission == nsIAutoplay::BLOCKED_ALL) {
+ return isUsingAutoplayModel;
+ }
+
+ if (defaultBehaviour == nsIAutoplay::ALLOWED) {
+ return true;
+ }
+
+ if (defaultBehaviour == nsIAutoplay::BLOCKED) {
+ return isInaudible || isUsingAutoplayModel;
+ }
+
+ MOZ_ASSERT(defaultBehaviour == nsIAutoplay::BLOCKED_ALL);
+ return isUsingAutoplayModel;
+}
+
+/* static */
+bool AutoplayPolicy::IsAllowedToPlay(const HTMLMediaElement& aElement) {
+ const bool result = IsAllowedToPlayInternal(aElement);
+ AUTOPLAY_LOG("IsAllowedToPlay, mediaElement=%p, isAllowToPlay=%s", &aElement,
+ result ? "allowed" : "blocked");
+ return result;
+}
+
+/* static */
+bool AutoplayPolicy::IsAllowedToPlay(const AudioContext& aContext) {
+ /**
+ * The autoplay checking has 5 different phases,
+ * 1. check whether audio context itself meets the autoplay condition
+ * 2. check if we enable blocking web audio or not
+ * (only support blocking when using user-gesture-activation model)
+ * 3. check whether the site is in the autoplay whitelist
+ * 4. check global autoplay setting and check wether the site is in the
+ * autoplay blacklist.
+ * 5. check whether media is allowed under current blocking model
+ * (only support user-gesture-activation model)
+ */
+ if (aContext.IsOffline()) {
+ return true;
+ }
+
+ if (!IsEnableBlockingWebAudioByUserGesturePolicy()) {
+ return true;
+ }
+
+ nsPIDOMWindowInner* window = aContext.GetParentObject();
+ uint32_t sitePermission = SiteAutoplayPerm(window);
+
+ if (sitePermission == nsIPermissionManager::ALLOW_ACTION) {
+ AUTOPLAY_LOG(
+ "Allow autoplay as document has permanent autoplay permission.");
+ return true;
+ }
+
+ if (DefaultAutoplayBehaviour() == nsIAutoplay::ALLOWED &&
+ sitePermission != nsIPermissionManager::DENY_ACTION &&
+ sitePermission != nsIAutoplay::BLOCKED_ALL) {
+ AUTOPLAY_LOG(
+ "Allow autoplay as global autoplay setting is allowing autoplay by "
+ "default.");
+ return true;
+ }
+
+ return IsWindowAllowedToPlayOverall(window);
+}
+
+enum class DocumentAutoplayPolicy : uint8_t {
+ Allowed,
+ Allowed_muted,
+ Disallowed
+};
+
+/* static */
+DocumentAutoplayPolicy IsDocAllowedToPlay(const Document& aDocument) {
+ RefPtr<nsPIDOMWindowInner> window = aDocument.GetInnerWindow();
+
+#if defined(MOZ_WIDGET_ANDROID)
+ if (StaticPrefs::media_geckoview_autoplay_request()) {
+ const bool isWindowAllowedToPlay = IsWindowAllowedToPlayOverall(window);
+ if (IsGVAutoplayRequestAllowed(window, RType::eAUDIBLE)) {
+ return DocumentAutoplayPolicy::Allowed;
+ }
+
+ if (IsGVAutoplayRequestAllowed(window, RType::eINAUDIBLE)) {
+ return isWindowAllowedToPlay ? DocumentAutoplayPolicy::Allowed
+ : DocumentAutoplayPolicy::Allowed_muted;
+ }
+
+ return isWindowAllowedToPlay ? DocumentAutoplayPolicy::Allowed
+ : DocumentAutoplayPolicy::Disallowed;
+ }
+#endif
+ const uint32_t sitePermission = SiteAutoplayPerm(window);
+ const uint32_t globalPermission = DefaultAutoplayBehaviour();
+ const uint32_t policy = StaticPrefs::media_autoplay_blocking_policy();
+ const bool isWindowAllowedToPlayByGesture =
+ policy != sPOLICY_USER_INPUT_DEPTH &&
+ IsWindowAllowedToPlayByUserGesture(window);
+ const bool isWindowAllowedToPlayByTraits =
+ IsWindowAllowedToPlayByTraits(window);
+
+ AUTOPLAY_LOG(
+ "IsDocAllowedToPlay(), policy=%d, sitePermission=%d, "
+ "globalPermission=%d, isWindowAllowedToPlayByGesture=%d, "
+ "isWindowAllowedToPlayByTraits=%d",
+ policy, sitePermission, globalPermission, isWindowAllowedToPlayByGesture,
+ isWindowAllowedToPlayByTraits);
+
+ if ((globalPermission == nsIAutoplay::ALLOWED &&
+ (sitePermission != nsIPermissionManager::DENY_ACTION &&
+ sitePermission != nsIAutoplay::BLOCKED_ALL)) ||
+ sitePermission == nsIPermissionManager::ALLOW_ACTION ||
+ isWindowAllowedToPlayByGesture || isWindowAllowedToPlayByTraits) {
+ return DocumentAutoplayPolicy::Allowed;
+ }
+
+ if ((globalPermission == nsIAutoplay::BLOCKED &&
+ sitePermission != nsIAutoplay::BLOCKED_ALL) ||
+ sitePermission == nsIPermissionManager::DENY_ACTION) {
+ return DocumentAutoplayPolicy::Allowed_muted;
+ }
+
+ return DocumentAutoplayPolicy::Disallowed;
+}
+
+/* static */
+uint32_t AutoplayPolicy::GetSiteAutoplayPermission(nsIPrincipal* aPrincipal) {
+ if (!aPrincipal) {
+ return nsIPermissionManager::DENY_ACTION;
+ }
+
+ nsCOMPtr<nsIPermissionManager> permMgr =
+ components::PermissionManager::Service();
+ if (!permMgr) {
+ return nsIPermissionManager::DENY_ACTION;
+ }
+
+ uint32_t perm = nsIPermissionManager::DENY_ACTION;
+ permMgr->TestExactPermissionFromPrincipal(aPrincipal, "autoplay-media"_ns,
+ &perm);
+ return perm;
+}
+
+/* static */
+bool AutoplayPolicyTelemetryUtils::WouldBeAllowedToPlayIfAutoplayDisabled(
+ const AudioContext& aContext) {
+ return IsAudioContextAllowedToPlay(aContext);
+}
+
+/* static */
+dom::AutoplayPolicy AutoplayPolicy::GetAutoplayPolicy(
+ const dom::HTMLMediaElement& aElement) {
+ // Note, the site permission can contain following values :
+ // - UNKNOWN_ACTION : no permission set for this site
+ // - ALLOW_ACTION : allowed to autoplay
+ // - DENY_ACTION : allowed inaudible autoplay, disallowed inaudible autoplay
+ // - nsIAutoplay::BLOCKED_ALL : autoplay disallowed
+ // and the global permissions would be nsIAutoplay::{BLOCKED, ALLOWED,
+ // BLOCKED_ALL}
+ const uint32_t sitePermission =
+ SiteAutoplayPerm(aElement.OwnerDoc()->GetInnerWindow());
+ const uint32_t globalPermission = DefaultAutoplayBehaviour();
+ const bool isAllowedToPlayByBlockingModel =
+ IsAllowedToPlayByBlockingModel(aElement);
+
+ AUTOPLAY_LOG(
+ "IsAllowedToPlay(element), sitePermission=%d, globalPermission=%d, "
+ "isAllowedToPlayByBlockingModel=%d",
+ sitePermission, globalPermission, isAllowedToPlayByBlockingModel);
+
+#if defined(MOZ_WIDGET_ANDROID)
+ if (StaticPrefs::media_geckoview_autoplay_request()) {
+ if (IsGVAutoplayRequestAllowed(aElement, RType::eAUDIBLE)) {
+ return dom::AutoplayPolicy::Allowed;
+ } else if (IsGVAutoplayRequestAllowed(aElement, RType::eINAUDIBLE)) {
+ return isAllowedToPlayByBlockingModel
+ ? dom::AutoplayPolicy::Allowed
+ : dom::AutoplayPolicy::Allowed_muted;
+ } else {
+ return isAllowedToPlayByBlockingModel ? dom::AutoplayPolicy::Allowed
+ : dom::AutoplayPolicy::Disallowed;
+ }
+ }
+#endif
+
+ // These are situations when an element is allowed to autoplay
+ // 1. The site permission is explicitly allowed
+ // 2. The global permission is allowed, and the site isn't explicitly
+ // disallowed
+ // 3. The blocking model is explicitly allowed this element
+ if (sitePermission == nsIPermissionManager::ALLOW_ACTION ||
+ (globalPermission == nsIAutoplay::ALLOWED &&
+ (sitePermission != nsIPermissionManager::DENY_ACTION &&
+ sitePermission != nsIAutoplay::BLOCKED_ALL)) ||
+ isAllowedToPlayByBlockingModel) {
+ return dom::AutoplayPolicy::Allowed;
+ }
+
+ // These are situations when a element is allowed to autoplay only when it's
+ // inaudible.
+ // 1. The site permission is block-audible-autoplay
+ // 2. The global permission is block-audible-autoplay, and the site permission
+ // isn't block-all-autoplay
+ if (sitePermission == nsIPermissionManager::DENY_ACTION ||
+ (globalPermission == nsIAutoplay::BLOCKED &&
+ sitePermission != nsIAutoplay::BLOCKED_ALL)) {
+ return dom::AutoplayPolicy::Allowed_muted;
+ }
+
+ return dom::AutoplayPolicy::Disallowed;
+}
+
+/* static */
+dom::AutoplayPolicy AutoplayPolicy::GetAutoplayPolicy(
+ const dom::AudioContext& aContext) {
+ if (AutoplayPolicy::IsAllowedToPlay(aContext)) {
+ return dom::AutoplayPolicy::Allowed;
+ }
+ return dom::AutoplayPolicy::Disallowed;
+}
+
+/* static */
+dom::AutoplayPolicy AutoplayPolicy::GetAutoplayPolicy(
+ const dom::AutoplayPolicyMediaType& aType, const dom::Document& aDoc) {
+ DocumentAutoplayPolicy policy = IsDocAllowedToPlay(aDoc);
+ // https://w3c.github.io/autoplay/#query-by-a-media-type
+ if (aType == dom::AutoplayPolicyMediaType::Audiocontext) {
+ return policy == DocumentAutoplayPolicy::Allowed
+ ? dom::AutoplayPolicy::Allowed
+ : dom::AutoplayPolicy::Disallowed;
+ }
+ MOZ_ASSERT(aType == dom::AutoplayPolicyMediaType::Mediaelement);
+ if (policy == DocumentAutoplayPolicy::Allowed) {
+ return dom::AutoplayPolicy::Allowed;
+ }
+ if (policy == DocumentAutoplayPolicy::Allowed_muted) {
+ return dom::AutoplayPolicy::Allowed_muted;
+ }
+ return dom::AutoplayPolicy::Disallowed;
+}
+
+} // namespace mozilla::media
diff --git a/dom/media/autoplay/AutoplayPolicy.h b/dom/media/autoplay/AutoplayPolicy.h
new file mode 100644
index 0000000000..c7b009f2d8
--- /dev/null
+++ b/dom/media/autoplay/AutoplayPolicy.h
@@ -0,0 +1,80 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(AutoplayPolicy_h_)
+# define AutoplayPolicy_h_
+
+# include "mozilla/NotNull.h"
+
+class nsIPrincipal;
+
+namespace mozilla::dom {
+
+class HTMLMediaElement;
+class AudioContext;
+class Document;
+enum class AutoplayPolicy : uint8_t;
+enum class AutoplayPolicyMediaType : uint8_t;
+
+} // namespace mozilla::dom
+
+namespace mozilla::media {
+/**
+ * AutoplayPolicy is used to manage autoplay logic for all kinds of media,
+ * including MediaElement, Web Audio and Web Speech.
+ *
+ * Autoplay could be disable by setting the pref "media.autoplay.default"
+ * to anything but nsIAutoplay::Allowed. Once user disables autoplay, media
+ * could only be played if one of following conditions is true.
+ * 1) Owner document is activated by user gestures
+ * We restrict user gestures to "mouse click", "keyboard press" and "touch".
+ * 2) Muted media content or video without audio content.
+ * 3) Document's origin has the "autoplay-media" permission.
+ */
+class AutoplayPolicy {
+ public:
+ // Returns whether a given media element is allowed to play.
+ static bool IsAllowedToPlay(const dom::HTMLMediaElement& aElement);
+
+ // Returns whether a given AudioContext is allowed to play.
+ static bool IsAllowedToPlay(const dom::AudioContext& aContext);
+
+ // Return the value of the autoplay permission for given principal. The return
+ // value can be 0=unknown, 1=allow, 2=block audio, 5=block audio and video.
+ static uint32_t GetSiteAutoplayPermission(nsIPrincipal* aPrincipal);
+
+ // Following methods are used for the internal implementation for the Autoplay
+ // Policy Detection API, the public JS interfaces are in exposed on Navigator.
+ // https://w3c.github.io/autoplay/#autoplay-detection-methods
+ static dom::AutoplayPolicy GetAutoplayPolicy(
+ const dom::HTMLMediaElement& aElement);
+
+ static dom::AutoplayPolicy GetAutoplayPolicy(
+ const dom::AudioContext& aContext);
+
+ static dom::AutoplayPolicy GetAutoplayPolicy(
+ const dom::AutoplayPolicyMediaType& aType, const dom::Document& aDoc);
+};
+
+/**
+ * This class contains helper funtions which could be used in AutoplayPolicy
+ * for determing Telemetry use-only result. They shouldn't represent the final
+ * result of blocking autoplay.
+ */
+class AutoplayPolicyTelemetryUtils {
+ public:
+ // Returns true if a given AudioContext would be allowed to play
+ // if block autoplay was enabled. If this returns false, it means we would
+ // either block or ask for permission.
+ // Note: this is for telemetry purposes, and doesn't check the prefs
+ // which enable/disable block autoplay. Do not use for blocking logic!
+ static bool WouldBeAllowedToPlayIfAutoplayDisabled(
+ const dom::AudioContext& aContext);
+};
+
+} // namespace mozilla::media
+
+#endif
diff --git a/dom/media/autoplay/GVAutoplayPermissionRequest.cpp b/dom/media/autoplay/GVAutoplayPermissionRequest.cpp
new file mode 100644
index 0000000000..600c7d291e
--- /dev/null
+++ b/dom/media/autoplay/GVAutoplayPermissionRequest.cpp
@@ -0,0 +1,236 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GVAutoplayPermissionRequest.h"
+
+#include "mozilla/dom/HTMLMediaElement.h"
+#include "mozilla/Logging.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "nsGlobalWindowInner.h"
+
+mozilla::LazyLogModule gGVAutoplayRequestLog("GVAutoplay");
+
+namespace mozilla::dom {
+
+using RType = GVAutoplayRequestType;
+using RStatus = GVAutoplayRequestStatus;
+
+const char* ToGVRequestTypeStr(RType aType) {
+ switch (aType) {
+ case RType::eINAUDIBLE:
+ return "inaudible";
+ case RType::eAUDIBLE:
+ return "audible";
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid request type.");
+ return "invalid";
+ }
+}
+
+const char* ToGVRequestStatusStr(RStatus aStatus) {
+ switch (aStatus) {
+ case RStatus::eUNKNOWN:
+ return "Unknown";
+ case RStatus::eALLOWED:
+ return "Allowed";
+ case RStatus::eDENIED:
+ return "Denied";
+ case RStatus::ePENDING:
+ return "Pending";
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid status.");
+ return "Invalid";
+ }
+}
+
+// avoid redefined macro in unified build
+#undef REQUEST_LOG
+#define REQUEST_LOG(msg, ...) \
+ if (MOZ_LOG_TEST(gGVAutoplayRequestLog, mozilla::LogLevel::Debug)) { \
+ MOZ_LOG(gGVAutoplayRequestLog, LogLevel::Debug, \
+ ("Request=%p, Type=%s, " msg, this, \
+ ToGVRequestTypeStr(this->mType), ##__VA_ARGS__)); \
+ }
+
+#undef LOG
+#define LOG(msg, ...) \
+ MOZ_LOG(gGVAutoplayRequestLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
+
+static RStatus GetRequestStatus(BrowsingContext* aContext, RType aType) {
+ MOZ_ASSERT(aContext);
+ AssertIsOnMainThread();
+ return aType == RType::eAUDIBLE
+ ? aContext->GetGVAudibleAutoplayRequestStatus()
+ : aContext->GetGVInaudibleAutoplayRequestStatus();
+}
+
+// This is copied from the value of `media.geckoview.autoplay.request.testing`.
+enum class TestRequest : uint32_t {
+ ePromptAsNormal = 0,
+ eAllowAll = 1,
+ eDenyAll = 2,
+ eAllowAudible = 3,
+ eDenyAudible = 4,
+ eAllowInAudible = 5,
+ eDenyInAudible = 6,
+ eLeaveAllPending = 7,
+};
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(GVAutoplayPermissionRequest,
+ ContentPermissionRequestBase)
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(GVAutoplayPermissionRequest,
+ ContentPermissionRequestBase)
+
+/* static */
+void GVAutoplayPermissionRequest::CreateRequest(nsGlobalWindowInner* aWindow,
+ BrowsingContext* aContext,
+ GVAutoplayRequestType aType) {
+ RefPtr<GVAutoplayPermissionRequest> request =
+ new GVAutoplayPermissionRequest(aWindow, aContext, aType);
+ request->SetRequestStatus(RStatus::ePENDING);
+ const TestRequest testingPref = static_cast<TestRequest>(
+ StaticPrefs::media_geckoview_autoplay_request_testing());
+ if (testingPref != TestRequest::ePromptAsNormal) {
+ LOG("Create testing request, tesing value=%u",
+ static_cast<uint32_t>(testingPref));
+ if (testingPref == TestRequest::eAllowAll ||
+ (testingPref == TestRequest::eAllowAudible &&
+ aType == RType::eAUDIBLE) ||
+ (testingPref == TestRequest::eAllowInAudible &&
+ aType == RType::eINAUDIBLE)) {
+ request->Allow(JS::UndefinedHandleValue);
+ } else if (testingPref == TestRequest::eDenyAll ||
+ (testingPref == TestRequest::eDenyAudible &&
+ aType == RType::eAUDIBLE) ||
+ (testingPref == TestRequest::eDenyInAudible &&
+ aType == RType::eINAUDIBLE)) {
+ request->Cancel();
+ }
+ } else {
+ LOG("Dispatch async request");
+ request->RequestDelayedTask(
+ aWindow->EventTargetFor(TaskCategory::Other),
+ GVAutoplayPermissionRequest::DelayedTaskType::Request);
+ }
+}
+
+GVAutoplayPermissionRequest::GVAutoplayPermissionRequest(
+ nsGlobalWindowInner* aWindow, BrowsingContext* aContext, RType aType)
+ : ContentPermissionRequestBase(aWindow->GetPrincipal(), aWindow,
+ ""_ns, // No testing pref used in this class
+ aType == RType::eAUDIBLE
+ ? "autoplay-media-audible"_ns
+ : "autoplay-media-inaudible"_ns),
+ mType(aType),
+ mContext(aContext) {
+ MOZ_ASSERT(mContext);
+ REQUEST_LOG("Request created");
+}
+
+GVAutoplayPermissionRequest::~GVAutoplayPermissionRequest() {
+ REQUEST_LOG("Request destroyed");
+ // If user doesn't response to the request before it gets destroyed (ex.
+ // request dismissed, tab closed, naviagation to a new page), then we should
+ // treat it as a denial.
+ if (mContext) {
+ Cancel();
+ }
+}
+
+void GVAutoplayPermissionRequest::SetRequestStatus(RStatus aStatus) {
+ REQUEST_LOG("SetRequestStatus, new status=%s", ToGVRequestStatusStr(aStatus));
+ MOZ_ASSERT(mContext);
+ AssertIsOnMainThread();
+ if (mType == RType::eAUDIBLE) {
+ // Return value of setting synced field should be checked. See bug 1656492.
+ Unused << mContext->SetGVAudibleAutoplayRequestStatus(aStatus);
+ } else {
+ // Return value of setting synced field should be checked. See bug 1656492.
+ Unused << mContext->SetGVInaudibleAutoplayRequestStatus(aStatus);
+ }
+}
+
+NS_IMETHODIMP
+GVAutoplayPermissionRequest::Cancel() {
+ MOZ_ASSERT(mContext, "Do not call 'Cancel()' twice!");
+ // As the process of replying of the request is an async task, the status
+ // might have be reset at the time we get the result from parent process.
+ // Ex. if the page got closed or naviagated immediately after user replied to
+ // the request. Therefore, the status should be either `pending` or `unknown`.
+ const RStatus status = GetRequestStatus(mContext, mType);
+ REQUEST_LOG("Cancel, current status=%s", ToGVRequestStatusStr(status));
+ MOZ_ASSERT(status == RStatus::ePENDING || status == RStatus::eUNKNOWN);
+ if ((status == RStatus::ePENDING) && !mContext->IsDiscarded()) {
+ SetRequestStatus(RStatus::eDENIED);
+ }
+ mContext = nullptr;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GVAutoplayPermissionRequest::Allow(JS::Handle<JS::Value> aChoices) {
+ MOZ_ASSERT(mContext, "Do not call 'Allow()' twice!");
+ // As the process of replying of the request is an async task, the status
+ // might have be reset at the time we get the result from parent process.
+ // Ex. if the page got closed or naviagated immediately after user replied to
+ // the request. Therefore, the status should be either `pending` or `unknown`.
+ const RStatus status = GetRequestStatus(mContext, mType);
+ REQUEST_LOG("Allow, current status=%s", ToGVRequestStatusStr(status));
+ MOZ_ASSERT(status == RStatus::ePENDING || status == RStatus::eUNKNOWN);
+ if (status == RStatus::ePENDING) {
+ SetRequestStatus(RStatus::eALLOWED);
+ }
+ mContext = nullptr;
+ return NS_OK;
+}
+
+/* static */
+void GVAutoplayPermissionRequestor::AskForPermissionIfNeeded(
+ nsPIDOMWindowInner* aWindow) {
+ LOG("Requestor, AskForPermissionIfNeeded");
+ if (!aWindow) {
+ return;
+ }
+
+ // The request is used for content permission, so it's no need to create a
+ // content request in parent process if we're in e10s.
+ if (XRE_IsE10sParentProcess()) {
+ return;
+ }
+
+ if (!StaticPrefs::media_geckoview_autoplay_request()) {
+ return;
+ }
+
+ LOG("Requestor, check status to decide if we need to create the new request");
+ // The request status is stored in top-level browsing context only.
+ RefPtr<BrowsingContext> context = aWindow->GetBrowsingContext()->Top();
+ if (!HasEverAskForRequest(context, RType::eAUDIBLE)) {
+ CreateAsyncRequest(aWindow, context, RType::eAUDIBLE);
+ }
+ if (!HasEverAskForRequest(context, RType::eINAUDIBLE)) {
+ CreateAsyncRequest(aWindow, context, RType::eINAUDIBLE);
+ }
+}
+
+/* static */
+bool GVAutoplayPermissionRequestor::HasEverAskForRequest(
+ BrowsingContext* aContext, RType aType) {
+ return GetRequestStatus(aContext, aType) != RStatus::eUNKNOWN;
+}
+
+/* static */
+void GVAutoplayPermissionRequestor::CreateAsyncRequest(
+ nsPIDOMWindowInner* aWindow, BrowsingContext* aContext,
+ GVAutoplayRequestType aType) {
+ nsGlobalWindowInner* innerWindow = nsGlobalWindowInner::Cast(aWindow);
+ if (!innerWindow || !innerWindow->GetPrincipal()) {
+ return;
+ }
+
+ GVAutoplayPermissionRequest::CreateRequest(innerWindow, aContext, aType);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/autoplay/GVAutoplayPermissionRequest.h b/dom/media/autoplay/GVAutoplayPermissionRequest.h
new file mode 100644
index 0000000000..f432e365c0
--- /dev/null
+++ b/dom/media/autoplay/GVAutoplayPermissionRequest.h
@@ -0,0 +1,86 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_GVAUTOPLAYPERMISSIONREQUEST_H_
+#define DOM_MEDIA_GVAUTOPLAYPERMISSIONREQUEST_H_
+
+#include "GVAutoplayRequestUtils.h"
+#include "nsContentPermissionHelper.h"
+
+class nsGlobalWindowInner;
+
+namespace mozilla::dom {
+
+/**
+ * This class is used to provide an ability for GeckoView (GV) to allow its
+ * embedder (application) to decide whether the autoplay media should be allowed
+ * or denied on the page. We have two types of request, one for audible media,
+ * another one for inaudible media. Each page would at most have one request per
+ * type at a time, and the result of request would be effective on that page
+ * until the page gets reloaded or closed.
+ */
+class GVAutoplayPermissionRequest : public ContentPermissionRequestBase {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(GVAutoplayPermissionRequest,
+ ContentPermissionRequestBase)
+
+ // nsIContentPermissionRequest methods
+ NS_IMETHOD Cancel(void) override;
+ NS_IMETHOD Allow(JS::Handle<JS::Value> choices) override;
+
+ private:
+ // Only allow to create this request from the requestor.
+ friend class GVAutoplayPermissionRequestor;
+ static void CreateRequest(nsGlobalWindowInner* aWindow,
+ BrowsingContext* aContext,
+ GVAutoplayRequestType aType);
+
+ GVAutoplayPermissionRequest(nsGlobalWindowInner* aWindow,
+ BrowsingContext* aContext,
+ GVAutoplayRequestType aType);
+ ~GVAutoplayPermissionRequest();
+
+ void SetRequestStatus(GVAutoplayRequestStatus aStatus);
+
+ GVAutoplayRequestType mType;
+ RefPtr<BrowsingContext> mContext;
+};
+
+/**
+ * This class provides a method to request autoplay permission for a page, which
+ * would be used to be a factor to determine if media is allowed to autoplay or
+ * not on GeckoView.
+ *
+ * A page could only have at most one audible request and one inaudible request,
+ * and once a page has been closed or reloaded, those requests would be dropped.
+ * In order to achieve that all media existing in the same page can share the
+ * result of those requests, the request status would only be stored in the
+ * top-level browsing context, which allows them to be synchronized among
+ * different processes when Fission is enabled.
+ *
+ * The current way we choose is to request for a permission when creating media
+ * element, in order to get the response from the embedding app before media
+ * starts playing if the app can response the request quickly enough. However,
+ * the request might be pending if the app doesn't response to it, we might
+ * never get the response. As that is just one factor of determining the
+ * autoplay result, even if we don't get the response for the request, we still
+ * have a chance to play media. Check AutoplayPolicy to see more details about
+ * how we decide the final autoplay decision.
+ */
+class GVAutoplayPermissionRequestor final {
+ public:
+ static void AskForPermissionIfNeeded(nsPIDOMWindowInner* aWindow);
+
+ private:
+ static bool HasEverAskForRequest(BrowsingContext* aContext,
+ GVAutoplayRequestType aType);
+ static void CreateAsyncRequest(nsPIDOMWindowInner* aWindow,
+ BrowsingContext* aContext,
+ GVAutoplayRequestType aType);
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/autoplay/GVAutoplayRequestStatusIPC.h b/dom/media/autoplay/GVAutoplayRequestStatusIPC.h
new file mode 100644
index 0000000000..39d1c22700
--- /dev/null
+++ b/dom/media/autoplay/GVAutoplayRequestStatusIPC.h
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_GVAUTOPLAYREQUESTSTATUSIPC_H_
+#define DOM_MEDIA_GVAUTOPLAYREQUESTSTATUSIPC_H_
+
+#include "ipc/EnumSerializer.h"
+
+#include "GVAutoplayRequestUtils.h"
+
+namespace IPC {
+
+template <>
+struct ParamTraits<mozilla::dom::GVAutoplayRequestStatus>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::dom::GVAutoplayRequestStatus,
+ mozilla::dom::GVAutoplayRequestStatus::eUNKNOWN,
+ mozilla::dom::GVAutoplayRequestStatus::ePENDING> {};
+
+} // namespace IPC
+
+#endif // DOM_MEDIA_GVAUTOPLAYREQUESTSTATUSIPC_H_
diff --git a/dom/media/autoplay/GVAutoplayRequestUtils.h b/dom/media/autoplay/GVAutoplayRequestUtils.h
new file mode 100644
index 0000000000..8122afa07e
--- /dev/null
+++ b/dom/media/autoplay/GVAutoplayRequestUtils.h
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_GVAUTOPLAYREQUESTUTILS_H_
+#define DOM_MEDIA_GVAUTOPLAYREQUESTUTILS_H_
+
+#include <cstdint>
+
+namespace mozilla {
+namespace dom {
+
+enum class GVAutoplayRequestType : bool { eINAUDIBLE = false, eAUDIBLE = true };
+
+enum class GVAutoplayRequestStatus : uint32_t {
+ eUNKNOWN,
+ eALLOWED,
+ eDENIED,
+ ePENDING,
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/autoplay/moz.build b/dom/media/autoplay/moz.build
new file mode 100644
index 0000000000..338f0798f8
--- /dev/null
+++ b/dom/media/autoplay/moz.build
@@ -0,0 +1,32 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("test/**"):
+ BUG_COMPONENT = ("Core", "Audio/Video: Playback")
+
+EXPORTS += [
+ "AutoplayPolicy.h",
+ "GVAutoplayPermissionRequest.h",
+ "GVAutoplayRequestStatusIPC.h",
+ "GVAutoplayRequestUtils.h",
+]
+
+UNIFIED_SOURCES += [
+ "AutoplayPolicy.cpp",
+ "GVAutoplayPermissionRequest.cpp",
+]
+
+XPIDL_MODULE = "autoplay"
+XPIDL_SOURCES += [
+ "nsIAutoplay.idl",
+]
+
+MOCHITEST_MANIFESTS += ["test/mochitest/mochitest.ini"]
+
+BROWSER_CHROME_MANIFESTS += ["test/browser/browser.ini"]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/autoplay/nsIAutoplay.idl b/dom/media/autoplay/nsIAutoplay.idl
new file mode 100644
index 0000000000..f18c72ada9
--- /dev/null
+++ b/dom/media/autoplay/nsIAutoplay.idl
@@ -0,0 +1,17 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(048a24f6-c4d6-47bc-bea2-f6038d1db80a)]
+interface nsIAutoplay : nsISupports
+{
+ /*
+ * Possible values for the "media.autoplay.default" preference.
+ */
+ const uint32_t ALLOWED = 0;
+ const uint32_t BLOCKED = 1;
+ const uint32_t BLOCKED_ALL = 5;
+};
diff --git a/dom/media/autoplay/test/browser/audio.ogg b/dom/media/autoplay/test/browser/audio.ogg
new file mode 100644
index 0000000000..7f1833508a
--- /dev/null
+++ b/dom/media/autoplay/test/browser/audio.ogg
Binary files differ
diff --git a/dom/media/autoplay/test/browser/browser.ini b/dom/media/autoplay/test/browser/browser.ini
new file mode 100644
index 0000000000..8bdf83859e
--- /dev/null
+++ b/dom/media/autoplay/test/browser/browser.ini
@@ -0,0 +1,27 @@
+[DEFAULT]
+subsuite = media-bc
+skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1536573
+tags = autoplay
+support-files =
+ ../../../test/gizmo.mp4
+ audio.ogg
+ file_empty.html
+ file_mediaplayback_frame.html
+ file_nonAutoplayAudio.html
+ file_video.html
+ head.js
+
+[browser_autoplay_policy_detection_global_sticky.js]
+[browser_autoplay_policy_detection_global_and_site_sticky.js]
+[browser_autoplay_policy_detection_click_to_play.js]
+[browser_autoplay_policy_play_twice.js]
+[browser_autoplay_policy_user_gestures.js]
+https_first_disabled = true
+[browser_autoplay_policy_request_permission.js]
+https_first_disabled = true
+[browser_autoplay_policy_touchScroll.js]
+https_first_disabled = true
+[browser_autoplay_policy_web_audio.js]
+[browser_autoplay_policy_web_audio_with_gum.js]
+[browser_autoplay_policy_webRTC_permission.js]
+[browser_autoplay_videoDocument.js]
diff --git a/dom/media/autoplay/test/browser/browser_autoplay_policy_detection_click_to_play.js b/dom/media/autoplay/test/browser/browser_autoplay_policy_detection_click_to_play.js
new file mode 100644
index 0000000000..576f01b1cf
--- /dev/null
+++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_detection_click_to_play.js
@@ -0,0 +1,120 @@
+/**
+ * This test will check the Autoplay Policy Detection API for click-to-play
+ * blocking policy (media.autoplay.blocking_policy=2) and the blocked value set
+ * to BLOCKED (block audible) and BLOCKED_ALL (block audible & inaudible).
+ *
+ * We will create two video elements in the test page, and then click one of
+ * them. After doing that, only the element has been clicked can be allowed to
+ * autoplay, other elements should remain blocked depend on the default blocking
+ * value.
+ */
+"use strict";
+
+// TODO : remove this when it's enabled by default in bug 1812189.
+add_setup(async function setSharedPrefs() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.media.autoplay-policy-detection.enabled", true]],
+ });
+});
+
+async function testAutoplayPolicy(defaultPolicy) {
+ await setupTestPref(defaultPolicy);
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "about:blank"
+ );
+ await createVideoElements(tab);
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [defaultPolicy],
+ defaultPolicy => {
+ is(
+ content.navigator.getAutoplayPolicy("mediaelement"),
+ defaultPolicy,
+ "Check autoplay policy by media element type is correct"
+ );
+ let videos = content.document.getElementsByTagName("video");
+ for (let video of videos) {
+ is(
+ content.navigator.getAutoplayPolicy(video),
+ defaultPolicy,
+ "Check autoplay policy by element is correct"
+ );
+ }
+ }
+ );
+
+ info("click on one video to make it play");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#will-be-clicked",
+ { button: 0 },
+ tab.linkedBrowser
+ );
+
+ info("only the element has been clicked can be allowed to autoplay");
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [defaultPolicy],
+ defaultPolicy => {
+ is(
+ content.navigator.getAutoplayPolicy("mediaelement"),
+ defaultPolicy,
+ "Check autoplay policy by media element type is correct"
+ );
+ let videos = content.document.getElementsByTagName("video");
+ for (let video of videos) {
+ is(
+ content.navigator.getAutoplayPolicy(video),
+ video.id === "will-be-clicked" ? "allowed" : defaultPolicy,
+ "Check autoplay policy by element is correct"
+ );
+ }
+ }
+ );
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function testAutoplayPolicyDetectionForClickToPlay() {
+ await testAutoplayPolicy("allowed-muted");
+ await testAutoplayPolicy("disallowed");
+});
+
+// Following are helper functions
+async function setupTestPref(defaultPolicy) {
+ function policyToBlockedValue(defaultPolicy) {
+ // Value for media.autoplay.default
+ if (defaultPolicy === "allowed") {
+ return 0 /* Allowed */;
+ } else if (defaultPolicy === "allowed-muted") {
+ return 1 /* Blocked */;
+ }
+ return 5 /* Blocked All */;
+ }
+ const defaultBlocked = policyToBlockedValue(defaultPolicy);
+ info(`Set 'media.autoplay.default' to ${defaultBlocked}`);
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.autoplay.default", defaultBlocked],
+ ["media.autoplay.blocking_policy", 2 /* click-to-play */],
+ ],
+ });
+}
+
+function createVideoElements(tab) {
+ info("create two video elements in the page");
+ let url = GetTestWebBasedURL("gizmo.mp4");
+ return SpecialPowers.spawn(tab.linkedBrowser, [url], url => {
+ let video1 = content.document.createElement("video");
+ video1.id = "will-be-clicked";
+ video1.controls = true;
+ video1.src = url;
+
+ let video2 = content.document.createElement("video");
+ video2.controls = true;
+ video2.src = url;
+
+ content.document.body.appendChild(video1);
+ content.document.body.appendChild(video2);
+ });
+}
diff --git a/dom/media/autoplay/test/browser/browser_autoplay_policy_detection_global_and_site_sticky.js b/dom/media/autoplay/test/browser/browser_autoplay_policy_detection_global_and_site_sticky.js
new file mode 100644
index 0000000000..828b3e0986
--- /dev/null
+++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_detection_global_and_site_sticky.js
@@ -0,0 +1,168 @@
+/**
+ * This test checks whether Autoplay Policy Detection API works correctly under
+ * different situations of having global permission set for block autoplay
+ * along with different site permission setting. This test only checks the
+ * sticky user gesture blocking model.
+ */
+"use strict";
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+// We can't set site permission on 'about:blank' so we use an empty page.
+const PAGE_URL = GetTestWebBasedURL("file_empty.html");
+
+add_setup(async function setSharedPrefs() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.media.autoplay-policy-detection.enabled", true],
+ ["media.autoplay.blocking_policy", 0],
+ ["media.autoplay.block-webaudio", true],
+ ],
+ });
+});
+
+add_task(async function testGlobalPermissionIsAllowed() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.ALLOWED]],
+ });
+ let tab = await createTabAndSetupPolicyAssertFunc(PAGE_URL);
+ PermissionTestUtils.add(
+ tab.linkedBrowser.currentURI,
+ "autoplay-media",
+ Services.perms.DENY_ACTION
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ info("site permission blocks audible autoplay");
+ content.assertAutoplayPolicy({
+ resultForElementType: "allowed-muted",
+ resultForElement: "allowed-muted",
+ resultForContextType: "disallowed",
+ resultForContext: "disallowed",
+ });
+ });
+ PermissionTestUtils.add(
+ tab.linkedBrowser.currentURI,
+ "autoplay-media",
+ Ci.nsIAutoplay.BLOCKED_ALL
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ info("site permission blocks all autoplay");
+ content.assertAutoplayPolicy({
+ resultForElementType: "disallowed",
+ resultForElement: "disallowed",
+ resultForContextType: "disallowed",
+ resultForContext: "disallowed",
+ });
+
+ info(
+ "activate document by using user gesture, all autoplay will be allowed"
+ );
+ content.document.notifyUserGestureActivation();
+ content.assertAutoplayPolicy({
+ resultForElementType: "allowed",
+ resultForElement: "allowed",
+ resultForContextType: "allowed",
+ resultForContext: "allowed",
+ });
+ });
+ PermissionTestUtils.remove(tab.linkedBrowser.currentURI, "autoplay-media");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testGlobalPermissionIsBlocked() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED]],
+ });
+ let tab = await createTabAndSetupPolicyAssertFunc(PAGE_URL);
+ PermissionTestUtils.add(
+ tab.linkedBrowser.currentURI,
+ "autoplay-media",
+ Services.perms.ALLOW_ACTION
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ info("site permission allows all autoplay");
+ content.assertAutoplayPolicy({
+ resultForElementType: "allowed",
+ resultForElement: "allowed",
+ resultForContextType: "allowed",
+ resultForContext: "allowed",
+ });
+ });
+ PermissionTestUtils.add(
+ tab.linkedBrowser.currentURI,
+ "autoplay-media",
+ Ci.nsIAutoplay.BLOCKED_ALL
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ info("site permission blocks all autoplay");
+ content.assertAutoplayPolicy({
+ resultForElementType: "disallowed",
+ resultForElement: "disallowed",
+ resultForContextType: "disallowed",
+ resultForContext: "disallowed",
+ });
+
+ info(
+ "activate document by using user gesture, all autoplay will be allowed"
+ );
+ content.document.notifyUserGestureActivation();
+ content.assertAutoplayPolicy({
+ resultForElementType: "allowed",
+ resultForElement: "allowed",
+ resultForContextType: "allowed",
+ resultForContext: "allowed",
+ });
+ });
+ PermissionTestUtils.remove(tab.linkedBrowser.currentURI, "autoplay-media");
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testGlobalPermissionIsBlockedAll() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED_ALL]],
+ });
+ let tab = await createTabAndSetupPolicyAssertFunc(PAGE_URL);
+ PermissionTestUtils.add(
+ tab.linkedBrowser.currentURI,
+ "autoplay-media",
+ Services.perms.ALLOW_ACTION
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ info("site permission allows all autoplay");
+ content.assertAutoplayPolicy({
+ resultForElementType: "allowed",
+ resultForElement: "allowed",
+ resultForContextType: "allowed",
+ resultForContext: "allowed",
+ });
+ });
+ PermissionTestUtils.add(
+ tab.linkedBrowser.currentURI,
+ "autoplay-media",
+ Services.perms.DENY_ACTION
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ info("site permission blocks audible autoplay");
+ content.assertAutoplayPolicy({
+ resultForElementType: "allowed-muted",
+ resultForElement: "allowed-muted",
+ resultForContextType: "disallowed",
+ resultForContext: "disallowed",
+ });
+
+ info(
+ "activate document by using user gesture, all autoplay will be allowed"
+ );
+ content.document.notifyUserGestureActivation();
+ content.assertAutoplayPolicy({
+ resultForElementType: "allowed",
+ resultForElement: "allowed",
+ resultForContextType: "allowed",
+ resultForContext: "allowed",
+ });
+ });
+ PermissionTestUtils.remove(tab.linkedBrowser.currentURI, "autoplay-media");
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/dom/media/autoplay/test/browser/browser_autoplay_policy_detection_global_sticky.js b/dom/media/autoplay/test/browser/browser_autoplay_policy_detection_global_sticky.js
new file mode 100644
index 0000000000..ab9fe6b418
--- /dev/null
+++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_detection_global_sticky.js
@@ -0,0 +1,105 @@
+/**
+ * This test checks whether Autoplay Policy Detection API works correctly under
+ * different situations of having global permission set for block autoplay.
+ * This test only checks the sticky user gesture blocking model.
+ */
+"use strict";
+
+add_setup(async function setSharedPrefs() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.media.autoplay-policy-detection.enabled", true],
+ ["media.autoplay.blocking_policy", 0],
+ ["media.autoplay.block-webaudio", true],
+ ],
+ });
+});
+
+add_task(async function testGlobalPermissionIsAllowed() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.ALLOWED]],
+ });
+ let tab = await createTabAndSetupPolicyAssertFunc("about:blank");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ info("global setting allows any autoplay");
+ content.assertAutoplayPolicy({
+ resultForElementType: "allowed",
+ resultForElement: "allowed",
+ resultForContextType: "allowed",
+ resultForContext: "allowed",
+ });
+ });
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testGlobalPermissionIsBlocked() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED]],
+ });
+ let tab = await createTabAndSetupPolicyAssertFunc("about:blank");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ info(
+ "global setting allows inaudible autoplay but audible autoplay is still not allowed"
+ );
+ content.assertAutoplayPolicy({
+ resultForElementType: "allowed-muted",
+ resultForElement: "allowed-muted",
+ resultForContextType: "disallowed",
+ resultForContext: "disallowed",
+ });
+
+ info("tweaking video's muted attribute won't change the result");
+ content.video.muted = true;
+ is(
+ "allowed-muted",
+ content.navigator.getAutoplayPolicy(content.video),
+ "getAutoplayPolicy(video) returns correct value"
+ );
+ content.video.muted = false;
+ is(
+ "allowed-muted",
+ content.navigator.getAutoplayPolicy(content.video),
+ "getAutoplayPolicy(video) returns correct value"
+ );
+
+ info(
+ "activate document by using user gesture, all autoplay will be allowed"
+ );
+ content.document.notifyUserGestureActivation();
+ content.assertAutoplayPolicy({
+ resultForElementType: "allowed",
+ resultForElement: "allowed",
+ resultForContextType: "allowed",
+ resultForContext: "allowed",
+ });
+ });
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testGlobalPermissionIsBlockedAll() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED_ALL]],
+ });
+ let tab = await createTabAndSetupPolicyAssertFunc("about:blank");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ info("global setting doesn't allow any autoplay");
+ content.assertAutoplayPolicy({
+ resultForElementType: "disallowed",
+ resultForElement: "disallowed",
+ resultForContextType: "disallowed",
+ resultForContext: "disallowed",
+ });
+
+ info(
+ "activate document by using user gesture, all autoplay will be allowed"
+ );
+ content.document.notifyUserGestureActivation();
+ content.assertAutoplayPolicy({
+ resultForElementType: "allowed",
+ resultForElement: "allowed",
+ resultForContextType: "allowed",
+ resultForContext: "allowed",
+ });
+ });
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/dom/media/autoplay/test/browser/browser_autoplay_policy_play_twice.js b/dom/media/autoplay/test/browser/browser_autoplay_policy_play_twice.js
new file mode 100644
index 0000000000..7130e6e781
--- /dev/null
+++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_play_twice.js
@@ -0,0 +1,54 @@
+const VIDEO_PAGE = GetTestWebBasedURL("file_video.html");
+
+function setup_test_preference(enableUserGesture) {
+ let state = enableUserGesture ? "enable" : "disable";
+ info(`- set pref : ${state} user gesture -`);
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED],
+ ["media.autoplay.blocking_policy", enableUserGesture ? 0 : 1],
+ ],
+ });
+}
+
+async function allow_play_for_played_video() {
+ info("- open new tab -");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "about:blank"
+ );
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, VIDEO_PAGE);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ info("- simulate user-click to start video -");
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#v",
+ { button: 0 },
+ tab.linkedBrowser
+ );
+
+ async function play_video_again() {
+ let video = content.document.getElementById("v");
+ ok(!video.paused, "video is playing");
+
+ info("- call video play() again -");
+ try {
+ await video.play();
+ ok(true, "success to resolve play promise");
+ } catch (e) {
+ ok(false, "promise should not be rejected");
+ }
+ }
+ await SpecialPowers.spawn(tab.linkedBrowser, [], play_video_again);
+
+ info("- remove tab -");
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function start_test() {
+ await setup_test_preference(true);
+ await allow_play_for_played_video();
+
+ await setup_test_preference(false);
+ await allow_play_for_played_video();
+});
diff --git a/dom/media/autoplay/test/browser/browser_autoplay_policy_request_permission.js b/dom/media/autoplay/test/browser/browser_autoplay_policy_request_permission.js
new file mode 100644
index 0000000000..5cdb3ffe6c
--- /dev/null
+++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_request_permission.js
@@ -0,0 +1,269 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+const VIDEO_PAGE_URI = GetTestWebBasedURL("file_empty.html");
+const SAME_ORIGIN_FRAME_URI = GetTestWebBasedURL(
+ "file_mediaplayback_frame.html"
+);
+const DIFFERENT_ORIGIN_FRAME_URI = GetTestWebBasedURL(
+ "file_mediaplayback_frame.html",
+ { crossOrigin: true }
+);
+
+const gPermissionName = "autoplay-media";
+
+function setTestingPreferences(defaultSetting) {
+ info(`set default autoplay setting to '${defaultSetting}'`);
+ let defaultValue =
+ defaultSetting == "blocked"
+ ? SpecialPowers.Ci.nsIAutoplay.BLOCKED
+ : SpecialPowers.Ci.nsIAutoplay.ALLOWED;
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.autoplay.default", defaultValue],
+ ["media.autoplay.blocking_policy", 0],
+ ["media.autoplay.block-event.enabled", true],
+ ],
+ });
+}
+
+async function testAutoplayExistingPermission(args) {
+ info("- Starting '" + args.name + "' -");
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: VIDEO_PAGE_URI,
+ },
+ async browser => {
+ let promptShowing = () =>
+ PopupNotifications.getNotification("autoplay-media", browser);
+
+ PermissionTestUtils.add(
+ browser.currentURI,
+ "autoplay-media",
+ args.permission
+ );
+ ok(!promptShowing(), "Should not be showing permission prompt yet");
+
+ await loadAutoplayVideo(browser, args);
+ await checkVideoDidPlay(browser, args);
+
+ // Reset permission.
+ PermissionTestUtils.remove(browser.currentURI, "autoplay-media");
+
+ info("- Finished '" + args.name + "' -");
+ }
+ );
+}
+
+async function testAutoplayExistingPermissionAgainstDefaultSetting(args) {
+ await setTestingPreferences(args.defaultSetting);
+ await testAutoplayExistingPermission(args);
+}
+
+// Test the simple ALLOW/BLOCK cases; when permission is already set to ALLOW,
+// we shoud be able to autoplay via calling play(), or via the autoplay attribute,
+// and when it's set to BLOCK, we should not.
+add_task(async () => {
+ await setTestingPreferences("blocked" /* default setting */);
+ await testAutoplayExistingPermission({
+ name: "Prexisting allow permission autoplay attribute",
+ permission: Services.perms.ALLOW_ACTION,
+ shouldPlay: true,
+ mode: "autoplay attribute",
+ });
+ await testAutoplayExistingPermission({
+ name: "Prexisting allow permission call play",
+ permission: Services.perms.ALLOW_ACTION,
+ shouldPlay: true,
+ mode: "call play",
+ });
+ await testAutoplayExistingPermission({
+ name: "Prexisting block permission autoplay attribute",
+ permission: Services.perms.DENY_ACTION,
+ shouldPlay: false,
+ mode: "autoplay attribute",
+ });
+ await testAutoplayExistingPermission({
+ name: "Prexisting block permission call play",
+ permission: Services.perms.DENY_ACTION,
+ shouldPlay: false,
+ mode: "call play",
+ });
+});
+
+/**
+ * These tests are used to ensure the autoplay setting for specific site can
+ * always override the default autoplay setting.
+ */
+add_task(async () => {
+ await testAutoplayExistingPermissionAgainstDefaultSetting({
+ name: "Site has prexisting allow permission but default setting is 'blocked'",
+ permission: Services.perms.ALLOW_ACTION,
+ defaultSetting: "blocked",
+ shouldPlay: true,
+ mode: "autoplay attribute",
+ });
+ await testAutoplayExistingPermissionAgainstDefaultSetting({
+ name: "Site has prexisting block permission but default setting is 'allowed'",
+ permission: Services.perms.DENY_ACTION,
+ defaultSetting: "allowed",
+ shouldPlay: false,
+ mode: "autoplay attribute",
+ });
+});
+
+/**
+ * The permission of the main page's domain would determine the final autoplay
+ * result when a page contains multiple iframes which are in the different
+ * domain from the main pages's.
+ * That means we would not check the permission of iframe's domain, even if it
+ * has been set.
+ */
+add_task(async function testExistingPermissionForIframe() {
+ await setTestingPreferences("blocked" /* default setting */);
+ await testAutoplayExistingPermissionForIframe({
+ name: "Prexisting ALLOW for main page with same origin iframe",
+ permissionForParent: Services.perms.ALLOW_ACTION,
+ isIframeDifferentOrgin: true,
+ shouldPlay: true,
+ });
+
+ await testAutoplayExistingPermissionForIframe({
+ name: "Prexisting ALLOW for main page with different origin iframe",
+ permissionForParent: Services.perms.ALLOW_ACTION,
+ isIframeDifferentOrgin: false,
+ shouldPlay: true,
+ });
+
+ await testAutoplayExistingPermissionForIframe({
+ name: "Prexisting ALLOW for main page, prexisting DENY for different origin iframe",
+ permissionForParent: Services.perms.ALLOW_ACTION,
+ permissionForChild: Services.perms.DENY_ACTION,
+ isIframeDifferentOrgin: false,
+ shouldPlay: true,
+ });
+
+ await testAutoplayExistingPermissionForIframe({
+ name: "Prexisting DENY for main page, prexisting ALLOW for different origin iframe",
+ permissionForParent: Services.perms.DENY_ACTION,
+ permissionForChild: Services.perms.ALLOW_ACTION,
+ isIframeDifferentOrgin: false,
+ shouldPlay: false,
+ });
+});
+
+/**
+ * The following are helper functions.
+ */
+async function testAutoplayExistingPermissionForIframe(args) {
+ info(`Start test : ${args.name}`);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: VIDEO_PAGE_URI,
+ },
+ async browser => {
+ setupSitesPermission(browser, args);
+
+ await createIframe(browser, args);
+ await checkAutplayInIframe(browser, args);
+
+ clearSitesPermission(browser, args);
+ }
+ );
+ info(`Finish test : ${args.name}`);
+}
+
+function setupSitesPermission(
+ browser,
+ {
+ isIframeDifferentOrgin,
+ permissionForParent,
+ permissionForChild = Services.perms.UNKNOWN_ACTION,
+ }
+) {
+ info(`setupSitesPermission`);
+ // Set permission for the main page's domain
+ setPermissionForBrowser(browser, browser.currentURI, permissionForParent);
+ if (isIframeDifferentOrgin) {
+ // Set permission for different domain of the iframe
+ setPermissionForBrowser(
+ browser,
+ DIFFERENT_ORIGIN_FRAME_URI,
+ permissionForChild
+ );
+ }
+}
+
+function clearSitesPermission(browser, { isIframeDifferentOrgin }) {
+ info(`clearSitesPermission`);
+ // Clear permission for the main page's domain
+ setPermissionForBrowser(
+ browser,
+ browser.currentURI,
+ Services.perms.UNKNOWN_ACTION
+ );
+ if (isIframeDifferentOrgin) {
+ // Clear permission for different domain of the iframe
+ setPermissionForBrowser(
+ browser,
+ DIFFERENT_ORIGIN_FRAME_URI,
+ Services.perms.UNKNOWN_ACTION
+ );
+ }
+}
+
+function setPermissionForBrowser(browser, uri, permValue) {
+ const promptShowing = () =>
+ PopupNotifications.getNotification(gPermissionName, browser);
+ PermissionTestUtils.add(uri, gPermissionName, permValue);
+ ok(!promptShowing(), "Should not be showing permission prompt yet");
+ is(
+ PermissionTestUtils.testExactPermission(uri, gPermissionName),
+ permValue,
+ "Set permission correctly"
+ );
+}
+
+function createIframe(browser, { isIframeDifferentOrgin }) {
+ const iframeURL = isIframeDifferentOrgin
+ ? DIFFERENT_ORIGIN_FRAME_URI
+ : SAME_ORIGIN_FRAME_URI;
+ return SpecialPowers.spawn(browser, [iframeURL], async url => {
+ info(`Create iframe and wait until it finsihes loading`);
+ const iframe = content.document.createElement("iframe");
+ iframe.src = url;
+ content.document.body.appendChild(iframe);
+ await new Promise(r => (iframe.onload = r));
+ });
+}
+
+function checkAutplayInIframe(browser, args) {
+ return SpecialPowers.spawn(browser, [args], async ({ shouldPlay }) => {
+ info(`check if media in iframe can start playing`);
+ const iframe = content.document.getElementsByTagName("iframe")[0];
+ if (!iframe) {
+ ok(false, `can not get the iframe!`);
+ return;
+ }
+ iframe.contentWindow.postMessage("play", "*");
+ await new Promise(r => {
+ content.onmessage = event => {
+ if (shouldPlay) {
+ is(event.data, "played", `played media in iframe`);
+ } else {
+ is(event.data, "blocked", `blocked media in iframe`);
+ }
+ r();
+ };
+ });
+ });
+}
diff --git a/dom/media/autoplay/test/browser/browser_autoplay_policy_touchScroll.js b/dom/media/autoplay/test/browser/browser_autoplay_policy_touchScroll.js
new file mode 100644
index 0000000000..fa28bf2943
--- /dev/null
+++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_touchScroll.js
@@ -0,0 +1,103 @@
+/**
+ * This test is used to ensure that touch in point can activate document and
+ * allow autoplay, but touch scroll can't activate document.
+ */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+"use strict";
+
+const PAGE = GetTestWebBasedURL("file_nonAutoplayAudio.html");
+
+function checkMediaPlayingState(isPlaying) {
+ let audio = content.document.getElementById("testAudio");
+ if (!audio) {
+ ok(false, "can't get the audio element!");
+ }
+
+ is(!audio.paused, isPlaying, "media playing state is correct.");
+}
+
+async function callMediaPlay(shouldStartPlaying) {
+ let audio = content.document.getElementById("testAudio");
+ if (!audio) {
+ ok(false, "can't get the audio element!");
+ }
+
+ info(`call media.play().`);
+ let playPromise = new Promise((resolve, reject) => {
+ audio.play().then(() => {
+ audio.isPlayStarted = true;
+ resolve();
+ });
+ content.setTimeout(() => {
+ if (audio.isPlayStarted) {
+ return;
+ }
+ reject();
+ }, 3000);
+ });
+
+ let isStartPlaying = await playPromise.then(
+ () => true,
+ () => false
+ );
+ is(
+ isStartPlaying,
+ shouldStartPlaying,
+ "media is " + (isStartPlaying ? "" : "not ") + "playing."
+ );
+}
+
+async function synthesizeTouchScroll(target, browser) {
+ const offset = 100;
+ await BrowserTestUtils.synthesizeTouch(
+ target,
+ 0,
+ 0,
+ { type: "touchstart" },
+ browser
+ );
+ await BrowserTestUtils.synthesizeTouch(
+ target,
+ offset / 2,
+ offset / 2,
+ { type: "touchmove" },
+ browser
+ );
+ await BrowserTestUtils.synthesizeTouch(
+ target,
+ offset,
+ offset,
+ { type: "touchend" },
+ browser
+ );
+}
+
+add_task(async function setup_test_preference() {
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED],
+ ["media.autoplay.blocking_policy", 0],
+ ],
+ });
+});
+
+add_task(async function testTouchScroll() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE,
+ },
+ async browser => {
+ info(`- media should not start playing -`);
+ await SpecialPowers.spawn(browser, [false], checkMediaPlayingState);
+
+ info(`- simulate touch scroll which should not activate document -`);
+ await synthesizeTouchScroll("#testAudio", browser);
+ await SpecialPowers.spawn(browser, [false], callMediaPlay);
+
+ info(`- simulate touch at a point which should activate document -`);
+ await BrowserTestUtils.synthesizeTouch("#testAudio", 0, 0, {}, browser);
+ await SpecialPowers.spawn(browser, [true], callMediaPlay);
+ }
+ );
+});
diff --git a/dom/media/autoplay/test/browser/browser_autoplay_policy_user_gestures.js b/dom/media/autoplay/test/browser/browser_autoplay_policy_user_gestures.js
new file mode 100644
index 0000000000..2b9d8c1158
--- /dev/null
+++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_user_gestures.js
@@ -0,0 +1,277 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+const VIDEO_PAGE = GetTestWebBasedURL("file_video.html");
+
+const UserGestures = {
+ MOUSE_CLICK: "mouse-click",
+ MOUSE_MOVE: "mouse-move",
+ KEYBOARD_PRESS: "keyboard-press",
+};
+
+const UserGestureTests = [
+ { type: UserGestures.MOUSE_CLICK, isActivationGesture: true },
+ { type: UserGestures.MOUSE_MOVE, isActivationGesture: false },
+ // test different keycode here. printable key, non-printable key and other
+ // special keys.
+ {
+ type: UserGestures.KEYBOARD_PRESS,
+ isActivationGesture: true,
+ keyCode: "a",
+ },
+ {
+ type: UserGestures.KEYBOARD_PRESS,
+ isActivationGesture: false,
+ keyCode: "VK_ESCAPE",
+ },
+ {
+ type: UserGestures.KEYBOARD_PRESS,
+ isActivationGesture: true,
+ keyCode: "VK_RETURN",
+ },
+ {
+ type: UserGestures.KEYBOARD_PRESS,
+ isActivationGesture: true,
+ keyCode: "VK_SPACE",
+ },
+];
+
+/**
+ * This test is used to ensure we would stop blocking autoplay after document
+ * has been activated by user gestures. We would treat mouse clicking, key board
+ * pressing (printable keys or carriage return) as valid user gesture input.
+ */
+add_task(async function startTestUserGestureInput() {
+ info("- setup test preference -");
+ await setupTestPreferences();
+
+ info("- test play when page doesn't be activated -");
+ await testPlayWithoutUserGesture();
+
+ info("- test play after page got user gesture -");
+ for (let idx = 0; idx < UserGestureTests.length; idx++) {
+ info("- test play after page got user gesture -");
+ await testPlayWithUserGesture(UserGestureTests[idx]);
+
+ info("- test web audio with user gesture -");
+ await testWebAudioWithUserGesture(UserGestureTests[idx]);
+ }
+});
+
+/**
+ * testing helper functions
+ */
+function setupTestPreferences() {
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED],
+ ["media.autoplay.blocking_policy", 0],
+ ["media.autoplay.block-event.enabled", true],
+ ["media.autoplay.block-webaudio", true],
+ ],
+ });
+}
+
+function simulateUserGesture(gesture, targetBrowser) {
+ info(`- simulate ${gesture.type} event -`);
+ switch (gesture.type) {
+ case UserGestures.MOUSE_CLICK:
+ return BrowserTestUtils.synthesizeMouseAtCenter(
+ "body",
+ { button: 0 },
+ targetBrowser
+ );
+ case UserGestures.MOUSE_MOVE:
+ return BrowserTestUtils.synthesizeMouseAtCenter(
+ "body",
+ { type: "mousemove" },
+ targetBrowser
+ );
+ case UserGestures.KEYBOARD_PRESS:
+ info(`- keycode=${gesture.keyCode} -`);
+ return BrowserTestUtils.synthesizeKey(gesture.keyCode, {}, targetBrowser);
+ default:
+ ok(false, "undefined user gesture");
+ return false;
+ }
+}
+
+async function testPlayWithoutUserGesture() {
+ info("- open new tab -");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "about:blank"
+ );
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, VIDEO_PAGE);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ async function checkAutoplayKeyword() {
+ info("- create an new autoplay video -");
+ let video = content.document.createElement("video");
+ video.src = "gizmo.mp4";
+ video.autoplay = true;
+ let canplayPromise = new Promise(function (resolve) {
+ video.addEventListener(
+ "canplaythrough",
+ function () {
+ resolve();
+ },
+ { once: true }
+ );
+ });
+ content.document.body.appendChild(video);
+
+ info("- can't autoplay without user activation -");
+ await canplayPromise;
+ ok(video.paused, "video can't start without user input.");
+ }
+ await SpecialPowers.spawn(tab.linkedBrowser, [], checkAutoplayKeyword);
+
+ async function playVideo() {
+ let video = content.document.getElementById("v");
+ info("- call play() without user activation -");
+ await video.play().catch(function () {
+ ok(video.paused, "video can't start play without user input.");
+ });
+ }
+ await SpecialPowers.spawn(tab.linkedBrowser, [], playVideo);
+
+ info("- remove tab -");
+ BrowserTestUtils.removeTab(tab);
+}
+
+async function testPlayWithUserGesture(gesture) {
+ info("- open new tab -");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "about:blank"
+ );
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, VIDEO_PAGE);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ info("- simulate user gesture -");
+ await simulateUserGesture(gesture, tab.linkedBrowser);
+
+ info("- call play() -");
+ async function playVideo(gesture) {
+ let video = content.document.getElementById("v");
+ try {
+ await video.play();
+ ok(gesture.isActivationGesture, "user gesture can activate the page");
+ ok(!video.paused, "video starts playing.");
+ } catch (e) {
+ ok(
+ !gesture.isActivationGesture,
+ "user gesture can not activate the page"
+ );
+ ok(video.paused, "video can not start playing.");
+ }
+ }
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [gesture], playVideo);
+
+ info("- remove tab -");
+ BrowserTestUtils.removeTab(tab);
+}
+
+function createAudioContext() {
+ content.ac = new content.AudioContext();
+ let ac = content.ac;
+ ac.resumePromises = [];
+ ac.stateChangePromise = new Promise(resolve => {
+ ac.addEventListener(
+ "statechange",
+ function () {
+ resolve();
+ },
+ { once: true }
+ );
+ });
+ ac.notAllowedToStart = new Promise(resolve => {
+ ac.addEventListener(
+ "blocked",
+ function () {
+ resolve();
+ },
+ { once: true }
+ );
+ });
+}
+
+function resumeWithoutExpectedSuccess() {
+ let ac = content.ac;
+ let promise = ac.resume();
+ ac.resumePromises.push(promise);
+ return new Promise((resolve, reject) => {
+ content.setTimeout(() => {
+ if (ac.state == "suspended") {
+ ok(true, "audio context is still suspended");
+ resolve();
+ } else {
+ reject("audio context should not be allowed to start");
+ }
+ }, 2000);
+ });
+}
+
+function resumeWithExpectedSuccess() {
+ let ac = content.ac;
+ ac.resumePromises.push(ac.resume());
+ return Promise.all(ac.resumePromises).then(() => {
+ ok(ac.state == "running", "audio context starts running");
+ });
+}
+
+async function testWebAudioWithUserGesture(gesture) {
+ info("- open new tab -");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "about:blank"
+ );
+ info("- create audio context -");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ content.ac = new content.AudioContext();
+ let ac = content.ac;
+ ac.resumePromises = [];
+ return new Promise(resolve => {
+ ac.addEventListener(
+ "blocked",
+ function () {
+ Assert.equal(
+ ac.state,
+ "suspended",
+ `AudioContext is not started yet.`
+ );
+ resolve();
+ },
+ { once: true }
+ );
+ });
+ });
+
+ info("- calling resume() -");
+ try {
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ resumeWithoutExpectedSuccess
+ );
+ } catch (error) {
+ ok(false, error.toString());
+ }
+
+ info("- simulate user gesture -");
+ await simulateUserGesture(gesture, tab.linkedBrowser);
+
+ info("- calling resume() again");
+ try {
+ let resumeFunc = gesture.isActivationGesture
+ ? resumeWithExpectedSuccess
+ : resumeWithoutExpectedSuccess;
+ await SpecialPowers.spawn(tab.linkedBrowser, [], resumeFunc);
+ } catch (error) {
+ ok(false, error.toString());
+ }
+
+ info("- remove tab -");
+ await BrowserTestUtils.removeTab(tab);
+}
diff --git a/dom/media/autoplay/test/browser/browser_autoplay_policy_webRTC_permission.js b/dom/media/autoplay/test/browser/browser_autoplay_policy_webRTC_permission.js
new file mode 100644
index 0000000000..8afae4d08e
--- /dev/null
+++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_webRTC_permission.js
@@ -0,0 +1,67 @@
+/**
+ * This test is used to ensure site which has granted 'camera' or 'microphone'
+ * or 'screen' permission could be allowed to autoplay.
+ */
+"use strict";
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+const VIDEO_PAGE = GetTestWebBasedURL("file_empty.html");
+
+add_task(() => {
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED],
+ ["media.autoplay.blocking_policy", 0],
+ ["media.autoplay.block-event.enabled", true],
+ ],
+ });
+});
+
+async function testAutoplayWebRTCPermission(args) {
+ info(`- Starting ${args.name} -`);
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: VIDEO_PAGE,
+ },
+ async browser => {
+ PermissionTestUtils.add(
+ browser.currentURI,
+ args.permission,
+ Services.perms.ALLOW_ACTION
+ );
+
+ await loadAutoplayVideo(browser, args);
+ await checkVideoDidPlay(browser, args);
+
+ // Reset permission.
+ PermissionTestUtils.remove(browser.currentURI, args.permission);
+
+ info(`- Finished ${args.name} -`);
+ }
+ );
+}
+
+add_task(async function start_test() {
+ await testAutoplayWebRTCPermission({
+ name: "Site with camera permission",
+ permission: "camera",
+ shouldPlay: true,
+ mode: "call play",
+ });
+ await testAutoplayWebRTCPermission({
+ name: "Site with microphone permission",
+ permission: "microphone",
+ shouldPlay: true,
+ mode: "call play",
+ });
+ await testAutoplayWebRTCPermission({
+ name: "Site with screen permission",
+ permission: "screen",
+ shouldPlay: true,
+ mode: "call play",
+ });
+});
diff --git a/dom/media/autoplay/test/browser/browser_autoplay_policy_web_audio.js b/dom/media/autoplay/test/browser/browser_autoplay_policy_web_audio.js
new file mode 100644
index 0000000000..b6e1133dd1
--- /dev/null
+++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_web_audio.js
@@ -0,0 +1,217 @@
+/**
+ * This test is used for testing whether WebAudio can be started correctly in
+ * different scenarios, such as
+ * 1) site has existing 'autoplay-media' permission for allowing autoplay
+ * 2) site has existing 'autoplay-media' permission for blocking autoplay
+ * 3) site doesn't have permission, start audio context by calling resume() or
+ * AudioScheduledNode.start() after granting user activation.
+ */
+"use strict";
+
+const { PermissionTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PermissionTestUtils.sys.mjs"
+);
+
+const PAGE = GetTestWebBasedURL("file_empty.html");
+
+function setup_test_preference() {
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED],
+ ["media.autoplay.blocking_policy", 0],
+ ["media.autoplay.block-webaudio", true],
+ ["media.autoplay.block-event.enabled", true],
+ ],
+ });
+}
+
+function createAudioContext() {
+ content.ac = new content.AudioContext();
+ const ac = content.ac;
+
+ ac.allowedToStart = new Promise(resolve => {
+ ac.addEventListener(
+ "statechange",
+ function () {
+ if (ac.state === "running") {
+ resolve();
+ }
+ },
+ { once: true }
+ );
+ });
+
+ ac.notAllowedToStart = new Promise(resolve => {
+ ac.addEventListener(
+ "blocked",
+ function () {
+ resolve();
+ },
+ { once: true }
+ );
+ });
+}
+
+async function checkIfAudioContextIsAllowedToStart(isAllowedToStart) {
+ const ac = content.ac;
+ if (isAllowedToStart) {
+ await ac.allowedToStart;
+ ok(ac.state === "running", `AudioContext is running.`);
+ } else {
+ await ac.notAllowedToStart;
+ ok(ac.state === "suspended", `AudioContext is not started yet.`);
+ }
+}
+
+async function resumeAudioContext(isAllowedToStart) {
+ const ac = content.ac;
+ const resumePromise = ac.resume();
+ const blockedPromise = new Promise(resolve => {
+ ac.addEventListener(
+ "blocked",
+ function () {
+ resolve();
+ },
+ { once: true }
+ );
+ });
+
+ if (isAllowedToStart) {
+ await resumePromise;
+ ok(true, `successfully resume AudioContext.`);
+ } else {
+ await blockedPromise;
+ ok(true, `resume is blocked because AudioContext is not allowed to start.`);
+ }
+}
+
+function startAudioContext(method) {
+ const ac = content.ac;
+ if (method == "AudioContext") {
+ info(`using AudioContext.resume() to start AudioContext`);
+ ac.resume();
+ return;
+ }
+ info(`using ${method}.start() to start AudioContext`);
+ let node;
+ switch (method) {
+ case "AudioBufferSourceNode":
+ node = ac.createBufferSource();
+ break;
+ case "ConstantSourceNode":
+ node = ac.createConstantSource();
+ break;
+ case "OscillatorNode":
+ node = ac.createOscillator();
+ break;
+ default:
+ ok(false, "undefined AudioScheduledSourceNode type");
+ return;
+ }
+ node.connect(ac.destination);
+ node.start();
+}
+
+async function testAutoplayExistingPermission({ name, permission }) {
+ info(`- starting \"${name}\" -`);
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ PAGE
+ );
+ const browser = tab.linkedBrowser;
+
+ info(`- set the 'autoplay-media' permission -`);
+ const promptShow = () =>
+ PopupNotifications.getNotification("autoplay-media", browser);
+ PermissionTestUtils.add(browser.currentURI, "autoplay-media", permission);
+ ok(!promptShow(), `should not be showing permission prompt yet`);
+
+ info(`- create audio context -`);
+ await SpecialPowers.spawn(browser, [], createAudioContext);
+
+ info(`- check AudioContext status -`);
+ const isAllowedToStart = permission === Services.perms.ALLOW_ACTION;
+ await SpecialPowers.spawn(
+ browser,
+ [isAllowedToStart],
+ checkIfAudioContextIsAllowedToStart
+ );
+ await SpecialPowers.spawn(browser, [isAllowedToStart], resumeAudioContext);
+
+ info(`- remove tab -`);
+ PermissionTestUtils.remove(browser.currentURI, "autoplay-media");
+ await BrowserTestUtils.removeTab(tab);
+}
+
+async function testAutoplayUnknownPermission({ name, method }) {
+ info(`- starting \"${name}\" -`);
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ PAGE
+ );
+ const browser = tab.linkedBrowser;
+
+ info(`- set the 'autoplay-media' permission to UNKNOWN -`);
+ const promptShow = () =>
+ PopupNotifications.getNotification("autoplay-media", browser);
+ PermissionTestUtils.add(
+ browser.currentURI,
+ "autoplay-media",
+ Services.perms.UNKNOWN_ACTION
+ );
+ ok(!promptShow(), `should not be showing permission prompt yet`);
+
+ info(`- create AudioContext which should not start -`);
+ await SpecialPowers.spawn(browser, [], createAudioContext);
+ await SpecialPowers.spawn(
+ browser,
+ [false],
+ checkIfAudioContextIsAllowedToStart
+ );
+
+ info(`- simulate user activate the page -`);
+ await SpecialPowers.spawn(browser, [], () => {
+ content.document.notifyUserGestureActivation();
+ });
+
+ info(`- try to start AudioContext -`);
+ await SpecialPowers.spawn(browser, [method], startAudioContext);
+
+ info(`- check AudioContext status -`);
+ await SpecialPowers.spawn(
+ browser,
+ [true],
+ checkIfAudioContextIsAllowedToStart
+ );
+ await SpecialPowers.spawn(browser, [true], resumeAudioContext);
+
+ info(`- remove tab -`);
+ PermissionTestUtils.remove(browser.currentURI, "autoplay-media");
+ await BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function start_tests() {
+ info("- setup test preference -");
+ await setup_test_preference();
+
+ await testAutoplayExistingPermission({
+ name: "Prexisting allow permission",
+ permission: Services.perms.ALLOW_ACTION,
+ });
+ await testAutoplayExistingPermission({
+ name: "Prexisting block permission",
+ permission: Services.perms.DENY_ACTION,
+ });
+ const startMethods = [
+ "AudioContext",
+ "AudioBufferSourceNode",
+ "ConstantSourceNode",
+ "OscillatorNode",
+ ];
+ for (let method of startMethods) {
+ await testAutoplayUnknownPermission({
+ name: "Unknown permission and start AudioContext after granting user activation",
+ method,
+ });
+ }
+});
diff --git a/dom/media/autoplay/test/browser/browser_autoplay_policy_web_audio_with_gum.js b/dom/media/autoplay/test/browser/browser_autoplay_policy_web_audio_with_gum.js
new file mode 100644
index 0000000000..45d5c71a52
--- /dev/null
+++ b/dom/media/autoplay/test/browser/browser_autoplay_policy_web_audio_with_gum.js
@@ -0,0 +1,174 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+/**
+ * This test is used to ensure web audio can be allowed to start when we have
+ * GUM permission.
+ */
+add_task(async function startTestingWebAudioWithGUM() {
+ info("- setup test preference -");
+ await setupTestPreferences();
+
+ info("- test web audio with gUM success -");
+ await testWebAudioWithGUM({
+ constraints: { audio: true },
+ shouldAllowStartingContext: true,
+ });
+ await testWebAudioWithGUM({
+ constraints: { video: true },
+ shouldAllowStartingContext: true,
+ });
+ await testWebAudioWithGUM({
+ constraints: { video: true, audio: true },
+ shouldAllowStartingContext: true,
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.navigator.permission.force", true]],
+ }).then(async function () {
+ info("- test web audio with gUM denied -");
+ await testWebAudioWithGUM({
+ constraints: { video: true },
+ shouldAllowStartingContext: false,
+ });
+ await testWebAudioWithGUM({
+ constraints: { audio: true },
+ shouldAllowStartingContext: false,
+ });
+ await testWebAudioWithGUM({
+ constraints: { video: true, audio: true },
+ shouldAllowStartingContext: false,
+ });
+ });
+});
+
+/**
+ * testing helper functions
+ */
+function setupTestPreferences() {
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED],
+ ["media.autoplay.blocking_policy", 0],
+ ["media.autoplay.block-event.enabled", true],
+ ["media.autoplay.block-webaudio", true],
+ ["media.navigator.permission.fake", true],
+ ],
+ });
+}
+
+function createAudioContext() {
+ content.ac = new content.AudioContext();
+ let ac = content.ac;
+ ac.resumePromises = [];
+ ac.stateChangePromise = new Promise(resolve => {
+ ac.addEventListener(
+ "statechange",
+ function () {
+ resolve();
+ },
+ { once: true }
+ );
+ });
+ ac.notAllowedToStart = new Promise(resolve => {
+ ac.addEventListener(
+ "blocked",
+ function () {
+ resolve();
+ },
+ { once: true }
+ );
+ });
+}
+
+async function checkingAudioContextRunningState() {
+ let ac = content.ac;
+ await ac.notAllowedToStart;
+ ok(ac.state === "suspended", `AudioContext is not started yet.`);
+}
+
+function resumeWithoutExpectedSuccess() {
+ let ac = content.ac;
+ let promise = ac.resume();
+ ac.resumePromises.push(promise);
+ return new Promise((resolve, reject) => {
+ content.setTimeout(() => {
+ if (ac.state == "suspended") {
+ ok(true, "audio context is still suspended");
+ resolve();
+ } else {
+ reject("audio context should not be allowed to start");
+ }
+ }, 2000);
+ });
+}
+
+function resumeWithExpectedSuccess() {
+ let ac = content.ac;
+ ac.resumePromises.push(ac.resume());
+ return Promise.all(ac.resumePromises).then(() => {
+ ok(ac.state == "running", "audio context starts running");
+ });
+}
+
+async function callGUM(testParameters) {
+ info("- calling gum with " + JSON.stringify(testParameters.constraints));
+ if (testParameters.shouldAllowStartingContext) {
+ // Because of the prefs we've set and passed, this is going to allow the
+ // window to start an AudioContext synchronously.
+ testParameters.constraints.fake = true;
+ await content.navigator.mediaDevices.getUserMedia(
+ testParameters.constraints
+ );
+ return;
+ }
+
+ // Call gUM, without sucess: we've made it so that only fake requests
+ // succeed without permission, and this is requesting non-fake-devices. Return
+ // a resolved promise so that the test continues, but the getUserMedia Promise
+ // will never be resolved.
+ // We do this to check that it's not merely calling gUM that allows starting
+ // an AudioContext, it's having the Promise it return resolved successfuly,
+ // because of saved permissions for an origin or explicit user consent using
+ // the prompt.
+ content.navigator.mediaDevices.getUserMedia(testParameters.constraints);
+}
+
+async function testWebAudioWithGUM(testParameters) {
+ info("- open new tab -");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "https://example.com"
+ );
+ info("- create audio context -");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], createAudioContext);
+
+ info("- check whether audio context starts running -");
+ try {
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [],
+ checkingAudioContextRunningState
+ );
+ } catch (error) {
+ ok(false, error.toString());
+ }
+
+ try {
+ await SpecialPowers.spawn(tab.linkedBrowser, [testParameters], callGUM);
+ } catch (error) {
+ ok(false, error.toString());
+ }
+
+ info("- calling resume() again");
+ try {
+ let resumeFunc = testParameters.shouldAllowStartingContext
+ ? resumeWithExpectedSuccess
+ : resumeWithoutExpectedSuccess;
+ await SpecialPowers.spawn(tab.linkedBrowser, [], resumeFunc);
+ } catch (error) {
+ ok(false, error.toString());
+ }
+
+ info("- remove tab -");
+ await BrowserTestUtils.removeTab(tab);
+}
diff --git a/dom/media/autoplay/test/browser/browser_autoplay_videoDocument.js b/dom/media/autoplay/test/browser/browser_autoplay_videoDocument.js
new file mode 100644
index 0000000000..77ce4ddbc1
--- /dev/null
+++ b/dom/media/autoplay/test/browser/browser_autoplay_videoDocument.js
@@ -0,0 +1,80 @@
+"use strict";
+
+const PAGE = GetTestWebBasedURL("audio.ogg");
+
+function setup_test_preference() {
+ return SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED],
+ ["media.autoplay.blocking_policy", 0],
+ ],
+ });
+}
+
+async function checkIsVideoDocumentAutoplay(browser) {
+ const played = await SpecialPowers.spawn(browser, [], async () => {
+ const video = content.document.getElementsByTagName("video")[0];
+ const played =
+ video &&
+ (await video.play().then(
+ () => true,
+ () => false
+ ));
+ return played;
+ });
+ ok(played, "Should be able to play in video document.");
+}
+
+async function checkIsIframeVideoDocumentAutoplay(browser) {
+ info("- create iframe video document -");
+ const iframeBC = await SpecialPowers.spawn(browser, [PAGE], async pageURL => {
+ const iframe = content.document.createElement("iframe");
+ iframe.src = pageURL;
+ content.document.body.appendChild(iframe);
+ const iframeLoaded = new Promise((resolve, reject) => {
+ iframe.addEventListener("load", e => resolve(), { once: true });
+ });
+ await iframeLoaded;
+ return iframe.browsingContext;
+ });
+
+ info("- check whether iframe video document starts playing -");
+ const [paused, playedLength] = await SpecialPowers.spawn(iframeBC, [], () => {
+ const video = content.document.querySelector("video");
+ return [video.paused, video.played.length];
+ });
+ ok(paused, "Subdoc video should not have played");
+ is(playedLength, 0, "Should have empty played ranges");
+}
+
+add_task(async () => {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: PAGE,
+ },
+ async browser => {
+ info("- setup test preference -");
+ await setup_test_preference();
+
+ info(`- check whether video document is autoplay -`);
+ await checkIsVideoDocumentAutoplay(browser);
+ }
+ );
+});
+
+add_task(async () => {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: "about:blank",
+ },
+ async browser => {
+ info("- setup test preference -");
+ await setup_test_preference();
+
+ info(`- check whether video document in iframe is autoplay -`);
+ await checkIsIframeVideoDocumentAutoplay(browser);
+ }
+ );
+});
diff --git a/dom/media/autoplay/test/browser/file_empty.html b/dom/media/autoplay/test/browser/file_empty.html
new file mode 100644
index 0000000000..d2b0361f09
--- /dev/null
+++ b/dom/media/autoplay/test/browser/file_empty.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Page left intentionally blank...</title>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/dom/media/autoplay/test/browser/file_mediaplayback_frame.html b/dom/media/autoplay/test/browser/file_mediaplayback_frame.html
new file mode 100644
index 0000000000..b5685c07b3
--- /dev/null
+++ b/dom/media/autoplay/test/browser/file_mediaplayback_frame.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Non-Autoplay page being used in Iframe</title>
+</head>
+<body>
+<video id="video" src="gizmo.mp4" loop></video>
+<script type="text/javascript">
+
+const video = document.getElementById("video");
+const w = window.opener || window.parent;
+
+window.onmessage = async event => {
+ if (event.data == "play") {
+ let rv = await video.play().then(() => true, () => false);
+ w.postMessage(rv ? "played" : "blocked", "*");
+ }
+}
+</script>
+</body>
+</html>
diff --git a/dom/media/autoplay/test/browser/file_nonAutoplayAudio.html b/dom/media/autoplay/test/browser/file_nonAutoplayAudio.html
new file mode 100644
index 0000000000..4d2641021a
--- /dev/null
+++ b/dom/media/autoplay/test/browser/file_nonAutoplayAudio.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<head>
+ <meta content="text/html;charset=utf-8" http-equiv="Content-Type">
+ <meta content="utf-8" http-equiv="encoding">
+</head>
+<body>
+<audio id="testAudio" src="audio.ogg" loop></audio>
diff --git a/dom/media/autoplay/test/browser/file_video.html b/dom/media/autoplay/test/browser/file_video.html
new file mode 100644
index 0000000000..3c70268fbb
--- /dev/null
+++ b/dom/media/autoplay/test/browser/file_video.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>video</title>
+</head>
+<body>
+<video id="v" src="gizmo.mp4" controls loop></video>
+</body>
+</html>
diff --git a/dom/media/autoplay/test/browser/head.js b/dom/media/autoplay/test/browser/head.js
new file mode 100644
index 0000000000..c84850900a
--- /dev/null
+++ b/dom/media/autoplay/test/browser/head.js
@@ -0,0 +1,149 @@
+/**
+ * Return a web-based URL for a given file based on the testing directory.
+ * @param {String} fileName
+ * file that caller wants its web-based url
+ * @param {Boolean} crossOrigin [optional]
+ * if set, then return a url with different origin. The default value is
+ * false.
+ */
+function GetTestWebBasedURL(fileName, { crossOrigin = false } = {}) {
+ const origin = crossOrigin ? "http://example.org" : "http://example.com";
+ return (
+ getRootDirectory(gTestPath).replace("chrome://mochitests/content", origin) +
+ fileName
+ );
+}
+
+/**
+ * Runs a content script that creates an autoplay video.
+ * @param {browserElement} browser
+ * the browser to run the script in
+ * @param {object} args
+ * test case definition, required members
+ * {
+ * mode: String, "autoplay attribute" or "call play".
+ * }
+ */
+function loadAutoplayVideo(browser, args) {
+ return SpecialPowers.spawn(browser, [args], async args => {
+ info("- create a new autoplay video -");
+ let video = content.document.createElement("video");
+ video.id = "v1";
+ video.didPlayPromise = new Promise((resolve, reject) => {
+ video.addEventListener(
+ "playing",
+ e => {
+ video.didPlay = true;
+ resolve();
+ },
+ { once: true }
+ );
+ video.addEventListener(
+ "blocked",
+ e => {
+ video.didPlay = false;
+ resolve();
+ },
+ { once: true }
+ );
+ });
+ if (args.mode == "autoplay attribute") {
+ info("autoplay attribute set to true");
+ video.autoplay = true;
+ } else if (args.mode == "call play") {
+ info("will call play() when reached loadedmetadata");
+ video.addEventListener(
+ "loadedmetadata",
+ e => {
+ video.play().then(
+ () => {
+ info("video play() resolved");
+ },
+ () => {
+ info("video play() rejected");
+ }
+ );
+ },
+ { once: true }
+ );
+ } else {
+ ok(false, "Invalid 'mode' arg");
+ }
+ video.src = "gizmo.mp4";
+ content.document.body.appendChild(video);
+ });
+}
+
+/**
+ * Runs a content script that checks whether the video created by
+ * loadAutoplayVideo() started playing.
+ * @param {browserElement} browser
+ * the browser to run the script in
+ * @param {object} args
+ * test case definition, required members
+ * {
+ * name: String, description of test.
+ * mode: String, "autoplay attribute" or "call play".
+ * shouldPlay: boolean, whether video should play.
+ * }
+ */
+function checkVideoDidPlay(browser, args) {
+ return SpecialPowers.spawn(browser, [args], async args => {
+ let video = content.document.getElementById("v1");
+ await video.didPlayPromise;
+ is(
+ video.didPlay,
+ args.shouldPlay,
+ args.name +
+ " should " +
+ (!args.shouldPlay ? "not " : "") +
+ "be able to autoplay"
+ );
+ video.src = "";
+ content.document.body.remove(video);
+ });
+}
+
+/**
+ * Create a tab that will load the given url, and define an autoplay policy
+ * check function inside the content window in that tab. This function should
+ * only be used when `dom.media.autoplay-policy-detection.enabled` is true.
+ * @param {url} url
+ * the url which the created tab should load
+ */
+async function createTabAndSetupPolicyAssertFunc(url) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(window.gBrowser, url);
+ await SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ content.video = content.document.createElement("video");
+ content.ac = new content.AudioContext();
+ content.assertAutoplayPolicy = ({
+ resultForElementType,
+ resultForElement,
+ resultForContextType,
+ resultForContext,
+ }) => {
+ is(
+ content.navigator.getAutoplayPolicy("mediaelement"),
+ resultForElementType,
+ "getAutoplayPolicy('mediaelement') returns correct value"
+ );
+ is(
+ content.navigator.getAutoplayPolicy(content.video),
+ resultForElement,
+ "getAutoplayPolicy(content.video) returns correct value"
+ );
+ // note, per spec "allowed-muted" won't be used for audio context.
+ is(
+ content.navigator.getAutoplayPolicy("audiocontext"),
+ resultForContextType,
+ "getAutoplayPolicy('audiocontext') returns correct value"
+ );
+ is(
+ content.navigator.getAutoplayPolicy(content.ac),
+ resultForContext,
+ "getAutoplayPolicy(content.ac) returns correct value"
+ );
+ };
+ });
+ return tab;
+}
diff --git a/dom/media/autoplay/test/mochitest/AutoplayTestUtils.js b/dom/media/autoplay/test/mochitest/AutoplayTestUtils.js
new file mode 100644
index 0000000000..aa8990c9d9
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/AutoplayTestUtils.js
@@ -0,0 +1,46 @@
+/* import-globals-from ../../../test/manifest.js */
+
+function playAndPostResult(muted, parent_window) {
+ let element = document.createElement("video");
+ element.preload = "auto";
+ element.muted = muted;
+ element.src = "short.mp4";
+ element.id = "video";
+ document.body.appendChild(element);
+ element.play().then(
+ () => {
+ parent_window.postMessage(
+ { played: true, allowedToPlay: element.allowedToPlay },
+ "*"
+ );
+ },
+ () => {
+ parent_window.postMessage(
+ { played: false, allowedToPlay: element.allowedToPlay },
+ "*"
+ );
+ }
+ );
+}
+
+function nextWindowMessage() {
+ return nextEvent(window, "message");
+}
+
+function log(msg) {
+ var log_pane = document.body;
+ log_pane.appendChild(document.createTextNode(msg));
+ log_pane.appendChild(document.createElement("br"));
+}
+
+const autoplayPermission = "autoplay-media";
+
+async function pushAutoplayAllowedPermission() {
+ return SpecialPowers.pushPermissions([
+ {
+ type: autoplayPermission,
+ allow: true,
+ context: document,
+ },
+ ]);
+}
diff --git a/dom/media/autoplay/test/mochitest/file_autoplay_gv_play_request_frame.html b/dom/media/autoplay/test/mochitest/file_autoplay_gv_play_request_frame.html
new file mode 100644
index 0000000000..de5ad1989f
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/file_autoplay_gv_play_request_frame.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>GV autoplay play request test</title>
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="AutoplayTestUtils.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+</head>
+<body>
+<script>
+
+window.addEventListener("message",
+ (event) => {
+ // Here we just want to test if media can start from iframe correctly, and
+ // we don't really care about if it's audible or not.
+ const isMuted = false;
+ playAndPostResult(isMuted, event.source);
+ });
+let w = window.opener || window.parent;
+w.postMessage("ready", "*");
+
+</script>
+</body>
+</html>
diff --git a/dom/media/autoplay/test/mochitest/file_autoplay_gv_play_request_window.html b/dom/media/autoplay/test/mochitest/file_autoplay_gv_play_request_window.html
new file mode 100644
index 0000000000..56e4e1031c
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/file_autoplay_gv_play_request_window.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>GV autoplay play request test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="AutoplayTestUtils.js"></script>
+ </head>
+<body>
+<script>
+/**
+ * The test info sent from the parent window will determine what kinds of media
+ * should start, where it should start, the result of the play request and
+ * whether the document is activated by user gesture.
+ */
+nextWindowMessage().then(
+ async (event) => {
+ let testInfo = event.data;
+ testInfo.parentWindow = event.source;
+ await setupTestEnvironment(testInfo);
+ await startPlaybackAndReturnMessage(testInfo);
+ });
+
+/**
+ * The following are helper functions.
+ */
+async function setupTestEnvironment(testInfo) {
+ if (testInfo.activatedDocument != undefined) {
+ info(`activate document`);
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ }
+ if (testInfo.iframe != undefined) {
+ info(`create child frame`);
+ testInfo.childFrame = await createChildFrame(testInfo);
+ }
+}
+
+async function createChildFrame(testInfo) {
+ let frame = document.createElement("iframe");
+ let origin = testInfo.iframe == "same-orgin"
+ ? "http://mochi.test:8888" : "http://example.org";
+ frame.src = origin + "/tests/dom/media/autoplay/test/mochitest/file_autoplay_gv_play_request_frame.html";
+ document.body.appendChild(frame);
+ info(`waiting for iframe loading`);
+ is((await nextWindowMessage()).data, "ready", "iframe has finished loading");
+ return frame;
+}
+
+async function startPlaybackAndReturnMessage({muted, iframe, parentWindow, childFrame}) {
+ if (iframe == undefined) {
+ info(`start playback`);
+ playAndPostResult(muted, parentWindow);
+ } else {
+ info("start autoplay from " + (iframe == "same-origin" ? "same" : "cross") + " origin child frame");
+ childFrame.contentWindow.postMessage("play", "*");
+ info(`waiting for media calling play from child frame`);
+ let result = await nextWindowMessage();
+ parentWindow.postMessage(result.data, "*");
+ }
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/media/autoplay/test/mochitest/file_autoplay_policy_activation_frame.html b/dom/media/autoplay/test/mochitest/file_autoplay_policy_activation_frame.html
new file mode 100644
index 0000000000..5dfb3da862
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/file_autoplay_policy_activation_frame.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Autoplay policy frame</title>
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="AutoplayTestUtils.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <style>
+ video {
+ width: 50%;
+ height: 50%;
+ }
+ </style>
+ </head>
+ <body>
+ <script>
+ window.addEventListener("message",
+ (event) => {
+ if (event.data == "click") {
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ event.source.postMessage("activated", "*");
+ } else if (event.data == "play-audible") {
+ playAndPostResult(false, event.source);
+ } else if (event.data == "play-muted") {
+ playAndPostResult(true, event.source);
+ }
+ });
+ let w = window.opener || window.parent;
+ w.postMessage("ready", "*");
+ </script>
+ </body>
+</html>
diff --git a/dom/media/autoplay/test/mochitest/file_autoplay_policy_activation_window.html b/dom/media/autoplay/test/mochitest/file_autoplay_policy_activation_window.html
new file mode 100644
index 0000000000..60c5a0cec1
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/file_autoplay_policy_activation_window.html
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Autoplay policy window</title>
+ <style>
+ video {
+ width: 50%;
+ height: 50%;
+ }
+ </style>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="AutoplayTestUtils.js"></script>
+ </head>
+ <body>
+ <pre id="test">
+ <script>
+
+ async function createChildFrame(testInfo) {
+ let frame = document.createElement("iframe");
+ let origin = testInfo.same_origin_child
+ ? "http://mochi.test:8888" : "http://example.org";
+ frame.src = origin + "/tests/dom/media/autoplay/test/mochitest/file_autoplay_policy_activation_frame.html";
+ // Wait for it to load...
+ document.body.appendChild(frame);
+ is((await nextWindowMessage()).data, "ready", "Expected a 'ready' message");
+ return frame;
+ }
+
+ async function activateDocument(testInfo) {
+ // Click the window to activate if appropriate.
+ if (testInfo.activated_from == "parent") {
+ info(`activate parent's document`);
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ } else if (testInfo.activated_from == "child") {
+ info(`activate child's document`);
+ testInfo.childFrame.contentWindow.postMessage("click", "*");
+ is((await nextWindowMessage()).data, "activated", "has activated child frame.");
+ }
+ }
+
+ function testAutoplayInWindow(testInfo) {
+ info(`start autoplay from parent frame`);
+ playAndPostResult(testInfo.muted, testInfo.parentWindow);
+ }
+
+ async function testAutoplayInChildFrame(testInfo) {
+ info("start autoplay from " + (testInfo.same_origin_child ? "same" : "cross") + " origin child frame");
+ // Ask the child iframe to try to play video.
+ let play_message = testInfo.muted ? "play-muted" : "play-audible";
+ testInfo.childFrame.contentWindow.postMessage(play_message, "*");
+ // Wait for the iframe to tell us whether it could play video.
+ let result = await nextWindowMessage();
+ // Report whether the iframe could play to the parent.
+ testInfo.parentWindow.postMessage(result.data, "*");
+ }
+
+ nextWindowMessage().then(
+ async (event) => {
+ let testInfo = event.data;
+ testInfo.parentWindow = event.source;
+ testInfo.childFrame = await createChildFrame(testInfo);
+
+ await activateDocument(testInfo);
+ switch (testInfo.play_from) {
+ case "parent":
+ testAutoplayInWindow(testInfo);
+ break;
+ case "child":
+ testAutoplayInChildFrame(testInfo);
+ break;
+ default:
+ ok(false, "Incorrect 'play_from' value!")
+ }
+ });
+ </script>
+ </pre>
+ </body>
+</html>
diff --git a/dom/media/autoplay/test/mochitest/file_autoplay_policy_eventdown_activation.html b/dom/media/autoplay/test/mochitest/file_autoplay_policy_eventdown_activation.html
new file mode 100644
index 0000000000..e25b6401d1
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/file_autoplay_policy_eventdown_activation.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <title>Autoplay policy window</title>
+ <style>
+ video {
+ width: 50%;
+ height: 50%;
+ }
+ </style>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="AutoplayTestUtils.js"></script>
+</head>
+
+<body>
+ <pre id="test">
+ <script>
+
+ window.ok = window.opener.ok;
+ window.is = window.opener.is;
+ window.info = window.opener.info;
+
+ async function testEventDownActivates(eventNames, activator) {
+ let element = document.createElement("video");
+ element.preload = "auto";
+ element.src = "short.mp4";
+ document.body.appendChild(element);
+
+ await once(element, "loadedmetadata");
+
+ let played = await element.play().then(() => true, () => false);
+ ok(!played, "Document should start out not activated, with playback blocked.");
+
+ let x = eventNames.map(
+ (eventName) => {
+ return new Promise(function (resolve, reject) {
+ window.addEventListener(eventName, async function (event) {
+ let p = await element.play().then(() => true, () => false);
+ ok(p, "Expect to be activated already in " + eventName);
+ resolve();
+ });
+ });
+ });
+
+ activator();
+
+ await Promise.all(x);
+
+ removeNodeAndSource(element);
+ }
+
+ nextWindowMessage().then(
+ async (event) => {
+ try {
+ if (event.data == "run keydown test") {
+ await testEventDownActivates(["keydown", "keypress", "keyup"], () => {
+ document.body.focus();
+ synthesizeKey(" ");
+ });
+ } else if (event.data == "run mousedown test") {
+ let events = ["mousedown", "mouseup", "click"];
+ if (getAndroidVersion() < 0) {
+ // Non-Android, also listen on pointer events.
+ events.push("pointerdown", "pointerup");
+ }
+ await testEventDownActivates(events, () => {
+ synthesizeMouseAtCenter(document.body, {});
+ });
+ } else {
+ ok(false, "unexpected message");
+ }
+ } catch (e) {
+ ok(false, "Caught exception " + e + " " + e.message + " " + e.stackTrace);
+ }
+ event.source.postMessage("done", "*");
+ });
+
+ </script>
+ </pre>
+</body>
+
+</html>
diff --git a/dom/media/autoplay/test/mochitest/file_autoplay_policy_key_blacklist.html b/dom/media/autoplay/test/mochitest/file_autoplay_policy_key_blacklist.html
new file mode 100644
index 0000000000..c9982f932a
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/file_autoplay_policy_key_blacklist.html
@@ -0,0 +1,148 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <title>Autoplay policy window</title>
+ <style>
+ video {
+ width: 50%;
+ height: 50%;
+ }
+ :focus {
+ background-color: blue;
+ }
+ </style>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="AutoplayTestUtils.js"></script>
+</head>
+
+<body>
+ <div id="x">This is a div with id=x.</div>
+ <pre id="test">
+ <input type="text" id="text-input"/>
+ <script>
+
+ window.ok = window.opener.ok;
+ window.is = window.opener.is;
+ window.info = window.opener.info;
+
+ // Keys that are expected to be not considered interaction with the page, and
+ // so not gesture activate the document.
+ let blacklistKeyPresses = [
+ "Tab",
+ "CapsLock",
+ "NumLock",
+ "ScrollLock",
+ "FnLock",
+ "Meta",
+ "OS",
+ "Hyper",
+ "Super",
+ "ContextMenu",
+ "ArrowUp",
+ "ArrowDown",
+ "ArrowLeft",
+ "ArrowRight",
+ "PageUp",
+ "PageDown",
+ "Home",
+ "End",
+ "Backspace",
+ "Fn",
+ "Alt",
+ "AltGraph",
+ "Control",
+ "Shift",
+ "Escape",
+ ];
+
+ let modifiedKeys = [
+ { key: "V", modifiers: { altKey: true, shiftKey: true } },
+ { key: "a", modifiers: { altKey: true } },
+ { key: "a", modifiers: { ctrlKey: true } },
+ { key: "KEY_ArrowRight", modifiers: { metaKey: true } },
+ { key: "KEY_ArrowRight", modifiers: { altKey: true } },
+ ];
+
+ async function sendInput(element, name, input) {
+ synthesizeMouseAtCenter(input, {});
+ let played = await element.play().then(() => true, () => false);
+ ok(!played, "Clicking " + name + " should not activate document and should not unblock play");
+
+ synthesizeCompositionChange({
+ composition: {
+ string: "\u30E9\u30FC\u30E1\u30F3",
+ clauses: [
+ { length: 4, attr: COMPOSITION_ATTR_RAW_CLAUSE }
+ ]
+ },
+ caret: { start: 4, length: 0 }
+ });
+ synthesizeComposition({ type: "compositioncommitasis" });
+ played = await element.play().then(() => true, () => false);
+ ok(!played, "Entering text to " + name + " via IME should not activate document and should not unblock play");
+
+ input.focus();
+ sendString("ascii text");
+ played = await element.play().then(() => true, () => false);
+ ok(!played, "Entering ASCII text into " + name + " should not activate document and should not unblock play");
+
+ input.blur();
+ }
+
+ async function testAutoplayKeyBlacklist(testCase, parent_window) {
+ let element = document.createElement("video");
+ element.preload = "auto";
+ element.src = "short.mp4";
+ document.body.appendChild(element);
+
+ await once(element, "loadedmetadata");
+
+ let played = await element.play().then(() => true, () => false);
+ is(played, false, "Document should start out not activated, with playback blocked.");
+
+ // Try pressing all the keys in the blacklist, then playing.
+ // Document should not be activated, so play should fail.
+
+ for (let key of blacklistKeyPresses) {
+ document.body.focus();
+ synthesizeKey("KEY_" + key);
+ played = await element.play().then(() => true, () => false);
+ is(played, false, "Key " + key + " should not activate document and should not unblock play");
+ }
+
+ // Try pressing some keys with modifiers.
+ let keyNames = (m) => Object.keys(m).join("+");
+ for (let x of modifiedKeys) {
+ document.body.focus();
+ synthesizeKey(x.key, x.modifiers);
+ played = await element.play().then(() => true, () => false);
+ is(played, false, "Key (" + x.key + "+" + keyNames(x.modifiers) + ") should not activate document and should not unblock play");
+ }
+
+ // Try pressing a key not in the blacklist, then playing.
+ // Document should be activated, and media should play.
+ synthesizeKey(" ");
+ played = await element.play().then(() => true, () => false);
+ is(played, true, "Space key should activate document and should unblock play");
+
+ removeNodeAndSource(element);
+ }
+
+ nextWindowMessage().then(
+ async (event) => {
+ try {
+ await testAutoplayKeyBlacklist(event.data, event.source);
+ } catch (e) {
+ ok(false, "Caught exception " + e + " " + e.message + " " + e.stackTrace);
+ }
+ event.source.postMessage("done", "*");
+ });
+
+ </script>
+ </pre>
+</body>
+
+</html>
diff --git a/dom/media/autoplay/test/mochitest/file_autoplay_policy_play_before_loadedmetadata.html b/dom/media/autoplay/test/mochitest/file_autoplay_policy_play_before_loadedmetadata.html
new file mode 100644
index 0000000000..3594d0f236
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/file_autoplay_policy_play_before_loadedmetadata.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <title>Autoplay policy window</title>
+ <style>
+ video {
+ width: 50%;
+ height: 50%;
+ }
+ </style>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="AutoplayTestUtils.js"></script>
+</head>
+
+<body>
+ <pre id="test">
+ <script>
+
+ window.is = window.opener.is;
+ window.info = window.opener.info;
+
+ async function testPlayBeforeLoadedMetata(testCase, parent_window) {
+ info("testPlayBeforeLoadedMetata: " + testCase.resource);
+
+ let element = document.createElement("video");
+ element.preload = "auto";
+ element.muted = testCase.muted;
+ element.src = testCase.resource;
+ document.body.appendChild(element);
+
+ is(element.paused, true, testCase.resource + " - should start out paused.");
+
+ let playEventFired = false;
+ once(element, "play").then(() => { playEventFired = true; });
+ let playingEventFired = false;
+ once(element, "playing").then(() => { playingEventFired = true;});
+ let pauseEventFired = false;
+ once(element, "pause").then(() => { pauseEventFired = true; });
+
+ let played = await element.play().then(() => true, () => false);
+
+ let playMsg = testCase.resource + " should " + (!testCase.shouldPlay ? "not " : "") + "play";
+ is(played, testCase.shouldPlay, playMsg);
+ is(playEventFired, testCase.shouldPlay, testCase.resource + " - should get play event if we played");
+ is(playingEventFired, testCase.shouldPlay, testCase.resource + "- should get playing event if we played");
+ is(pauseEventFired, false, testCase.resource + " - should not get pause event if we played");
+ removeNodeAndSource(element);
+ }
+
+ nextWindowMessage().then(
+ async (event) => {
+ await testPlayBeforeLoadedMetata(event.data, event.source);
+ event.source.postMessage("done", "*");
+ });
+
+ </script>
+ </pre>
+</body>
+
+</html>
diff --git a/dom/media/autoplay/test/mochitest/file_autoplay_policy_unmute_pauses.html b/dom/media/autoplay/test/mochitest/file_autoplay_policy_unmute_pauses.html
new file mode 100644
index 0000000000..125ee156b6
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/file_autoplay_policy_unmute_pauses.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <title>Autoplay policy window</title>
+ <style>
+ video {
+ width: 50%;
+ height: 50%;
+ }
+ </style>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="AutoplayTestUtils.js"></script>
+</head>
+
+<body>
+ <pre id="test">
+ <script>
+
+ window.is = window.opener.is;
+ window.info = window.opener.info;
+
+ function testAutoplayUnmutePauses(testCase, parent_window) {
+ return new Promise(function (resolve, reject) {
+
+ info("testAutoplayUnmutePauses: " + testCase.property);
+
+ let element = document.createElement("video");
+ element.preload = "auto";
+
+ // Make inaudible.
+ element[testCase.property] = testCase.inaudible;
+
+ // Once we've loaded, play, then make audible.
+ // Assert that the media is paused when we make it audible.
+ element.addEventListener("loadeddata", () => {
+ info("loadeddata");
+ element.play();
+ is(element.paused, false, testCase.property + "=" + testCase.inaudible + " - should be playing");
+ element[testCase.property] = testCase.audible;
+ is(element.paused, true, testCase.property + "=" + testCase.audible + " - should be paused.");
+ resolve();
+ });
+
+ element.src = "short.mp4";
+ element.id = "video";
+ document.body.appendChild(element);
+ });
+ }
+
+ nextWindowMessage().then(
+ (event) => {
+ testAutoplayUnmutePauses(event.data, event.source)
+ .then(() => {
+ event.source.postMessage("done", "*");
+ });
+ });
+
+ </script>
+ </pre>
+</body>
+
+</html>
diff --git a/dom/media/autoplay/test/mochitest/mochitest.ini b/dom/media/autoplay/test/mochitest/mochitest.ini
new file mode 100644
index 0000000000..f25b12e953
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/mochitest.ini
@@ -0,0 +1,54 @@
+[DEFAULT]
+subsuite = media
+tags = autoplay
+support-files =
+ ../../../test/manifest.js
+ ../../../test/320x240.ogv
+ ../../../test/bogus.duh
+ ../../../test/detodos-short.opus
+ ../../../test/flac-s24.flac
+ ../../../test/gizmo.mp4
+ ../../../test/gizmo.webm
+ ../../../test/gizmo-noaudio.mp4
+ ../../../test/gizmo-noaudio.webm
+ ../../../test/gizmo-short.mp4
+ ../../../test/r11025_s16_c1-short.wav
+ ../../../test/sample.3g2
+ ../../../test/sample.3gp
+ ../../../test/short.mp4
+ ../../../test/seek-short.webm
+ ../../../test/small-shot.flac
+ ../../../test/small-shot.m4a
+ ../../../test/small-shot.mp3
+ ../../../test/small-shot-mp3.mp4
+ ../../../test/small-shot.ogg
+ ../../../test/vp9-short.webm
+ AutoplayTestUtils.js
+ file_autoplay_gv_play_request_frame.html
+ file_autoplay_gv_play_request_window.html
+ file_autoplay_policy_activation_frame.html
+ file_autoplay_policy_activation_window.html
+ file_autoplay_policy_eventdown_activation.html
+ file_autoplay_policy_play_before_loadedmetadata.html
+ file_autoplay_policy_unmute_pauses.html
+ file_autoplay_policy_key_blacklist.html
+
+[test_autoplay.html]
+[test_autoplay_contentEditable.html]
+[test_autoplay_gv_play_request.html]
+skip-if = toolkit != 'android'
+[test_autoplay_policy.html]
+[test_autoplay_policy_activation.html]
+[test_autoplay_policy_play_before_loadedmetadata.html]
+skip-if = toolkit == 'android' # bug 1591121
+[test_autoplay_policy_eventdown_activation.html]
+[test_autoplay_policy_permission.html]
+[test_autoplay_policy_unmute_pauses.html]
+[test_autoplay_policy_key_blacklist.html]
+skip-if = (verify && debug && (os == 'win')) # bug 1424903
+[test_autoplay_policy_web_audio_notResumePageInvokedSuspendedAudioContext.html]
+[test_autoplay_policy_web_audio_mediaElementAudioSourceNode.html]
+[test_autoplay_policy_web_audio_AudioParamStream.html]
+[test_autoplay_policy_web_audio_createMediaStreamSource.html]
+[test_streams_autoplay.html]
+tags=mtg capturestream
diff --git a/dom/media/autoplay/test/mochitest/test_autoplay.html b/dom/media/autoplay/test/mochitest/test_autoplay.html
new file mode 100644
index 0000000000..aa936f976d
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/test_autoplay.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: autoplay attribute</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<video id='v1'"></video><audio id='a1'></audio>
+<video id='v2' autoplay></video><audio id='a2' autoplay></audio>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+/* import-globals-from ../../../test/manifest.js */
+var v1 = document.getElementById('v1');
+var a1 = document.getElementById('a1');
+var v2 = document.getElementById('v2');
+var a2 = document.getElementById('a2');
+ok(!v1.autoplay, "v1.autoplay should be false by default");
+ok(!a1.autoplay, "v1.autoplay should be false by default");
+ok(v2.autoplay, "v2.autoplay should be true");
+ok(a2.autoplay, "v2.autoplay should be true");
+
+v1.autoplay = true;
+a1.autoplay = true;
+ok(v1.autoplay, "video.autoplay not true");
+ok(a1.autoplay, "audio.autoplay not true");
+is(v1.getAttribute("autoplay"), "", "video autoplay attribute not set");
+is(a1.getAttribute("autoplay"), "", "video autoplay attribute not set");
+
+mediaTestCleanup();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/autoplay/test/mochitest/test_autoplay_contentEditable.html b/dom/media/autoplay/test/mochitest/test_autoplay_contentEditable.html
new file mode 100644
index 0000000000..0c0ec31797
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/test_autoplay_contentEditable.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: play() method</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body contenteditable="true">
+<pre id="test">
+
+<script>
+/* import-globals-from ../../../test/manifest.js */
+var manager = new MediaTestManager;
+
+var tokens = {
+ 0: ["canplay"],
+ "canplay": ["canplay", "canplaythrough"],
+ "canplaythrough": ["canplay", "canplaythrough"]
+};
+
+var eventList = ["play", "canplay", "playing", "canplaythrough", "ended"];
+
+function gotPlayEvent(event) {
+ var v = event.target;
+ ok(tokens[v._state].includes(event.type),
+ "Check expected event got " + event.type +
+ " at " + v._state + " for " + v._name);
+ v._state = event.type;
+ if (event.type == 'canplaythrough') {
+ // Remove all event listeners to avoid running tests after finishing test case.
+ eventList.forEach(function (e) {
+ v.removeEventListener(e, gotPlayEvent);
+ });
+ v.pause();
+ goToNext(v);
+ }
+}
+
+function goToNext(v) {
+ v.remove();
+ manager.finished(v.token);
+}
+
+function initTest(test, token) {
+ var v = document.createElement('video');
+ v.preload = "metadata";
+ v.token = token;
+ manager.started(token);
+ v._state = 0;
+
+ eventList.forEach(function (e) {
+ v.addEventListener(e, gotPlayEvent);
+ });
+
+ v.src = test.name;
+ v._name = test.name;
+ v.autoplay = true;
+ document.body.appendChild(v); // Causes load.
+}
+
+manager.runTests(gSmallTests, initTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/autoplay/test/mochitest/test_autoplay_gv_play_request.html b/dom/media/autoplay/test/mochitest/test_autoplay_gv_play_request.html
new file mode 100644
index 0000000000..760c452592
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/test_autoplay_gv_play_request.html
@@ -0,0 +1,221 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>GV Autoplay policy test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="AutoplayTestUtils.js"></script>
+ </head>
+<body>
+<script>
+
+/**
+ * On GeckoView, we have a different autoplay policy check than the one on other
+ * platforms, which would send a request to the embedding app to ask if the
+ * media can be allowed to play. We use a testing pref to simulate the response
+ * from the request.
+ *
+ * The request has two types, audible and inaudible request. The result of the
+ * audible request would only take effect on audible media, and the result of
+ * inaudible request would only take effect on inaudible media.
+ *
+ * User activation policy still work on GeckoView, so once the page has been
+ * activated, then we won't have to send the request and would allow all media
+ * in that page to play.
+ *
+ * The following test cases contain the information which would be applied in
+ * test, and the expected result of the test. For example, the following info
+ * indicates that, play an [inaudible] media in the environment with [allowed]
+ * [audible] request, and we expect to see it plays successfully.
+ * - muted: false,
+ * - requestType: "audible",
+ * - requestResult: "allowed",
+ * - expectedPlayResult: true,
+ */
+const testCases = [
+ // (1) testing audible playback
+ {
+ name: "[audible] playback and [allowed audible request] -> allowed",
+ muted: false,
+ requestType: "audible",
+ requestResult: "allowed",
+ expectedPlayResult: true,
+ },
+ {
+ name: "[audible] playback and [denied audible request] -> blocked",
+ muted: false,
+ requestType: "audible",
+ requestResult: "denied",
+ expectedPlayResult: false,
+ },
+ {
+ name: "[audible] playback and [allowed inaudible request] -> blocked",
+ muted: false,
+ requestType: "inaudible",
+ requestResult: "allowed",
+ expectedPlayResult: false,
+ },
+ {
+ name: "[audible] playback and [denied inaudible request] -> blocked",
+ muted: false,
+ requestType: "inaudible",
+ requestResult: "denied",
+ expectedPlayResult: false,
+ },
+ {
+ name: "[audible] playback with [pending request] in [activated document] -> allowed",
+ muted: false,
+ requestType: "all",
+ requestResult: "pending",
+ activatedDocument: true,
+ expectedPlayResult: true,
+ },
+ {
+ name: "[audible] playback with [denied audible request] in [activated document] -> allowed",
+ muted: false,
+ requestType: "audible",
+ requestResult: "allowed",
+ activatedDocument: true,
+ expectedPlayResult: true,
+ },
+ {
+ name: "[audible] playback with [pending request] in [unactivated document] -> blocked",
+ muted: false,
+ requestType: "all",
+ requestResult: "pending",
+ expectedPlayResult: false,
+ },
+ // (2) testing inaudible playback
+ {
+ name: "[inaudible] playback and [allowed audible request] -> blocked",
+ muted: true,
+ requestType: "audible",
+ requestResult: "allowed",
+ expectedPlayResult: false,
+ },
+ {
+ name: "[inaudible] playback and [denied audible request] -> blocked",
+ muted: true,
+ requestType: "audible",
+ requestResult: "denied",
+ expectedPlayResult: false,
+ },
+ {
+ name: "[inaudible] playback and [allowed inaudible request] -> allowed",
+ muted: true,
+ requestType: "inaudible",
+ requestResult: "allowed",
+ expectedPlayResult: true,
+ },
+ {
+ name: "[inaudible] playback and [denied inaudible request] -> blocked",
+ muted: true,
+ requestType: "inaudible",
+ requestResult: "denied",
+ expectedPlayResult: false,
+ },
+ {
+ name: "[inaudible] playback without [pending request] in [activated document] -> allowed",
+ muted: true,
+ requestType: "all",
+ requestResult: "pending",
+ activatedDocument: true,
+ expectedPlayResult: true,
+ },
+ {
+ name: "[inaudible] playback without [denied inaudible request] in [activated document] -> allowed",
+ muted: true,
+ requestType: "inaudible",
+ requestResult: "denied",
+ activatedDocument: true,
+ expectedPlayResult: true,
+ },
+ {
+ name: "[inaudible] playback without [pending request] in [unactivated document] -> blocked",
+ muted: true,
+ requestType: "all",
+ requestResult: "pending",
+ expectedPlayResult: false,
+ },
+ // (3) testing playback from iframe
+ {
+ name: "playback from [same origin] iframe and [allowed all request]-> allowed",
+ requestType: "all",
+ requestResult: "allowed",
+ iframe: "same-origin",
+ expectedPlayResult: true,
+ },
+ {
+ name: "playback from [same origin] iframe and [denied all request]-> blocked",
+ requestType: "all",
+ requestResult: "denied",
+ iframe: "same-origin",
+ expectedPlayResult: false,
+ },
+ {
+ name: "playback from [cross origin] iframe and [allowed all request]-> allowed",
+ requestType: "all",
+ requestResult: "allowed",
+ iframe: "cross-origin",
+ expectedPlayResult: true,
+ },
+ {
+ name: "playback from [cross origin] iframe and [denied all request]-> blocked",
+ requestType: "all",
+ requestResult: "denied",
+ iframe: "cross-origin",
+ expectedPlayResult: false,
+ },
+];
+
+const pageURL = "file_autoplay_gv_play_request_window.html";
+
+SimpleTest.waitForExplicitFinish();
+
+(async function startTest() {
+ for (const testCase of testCases) {
+ info(`- start running test '${testCase.name}'-`);
+ await setTestingPrefs(testCase);
+
+ // Run each test in a new window to ensure they won't interfere each other
+ const testPage = window.open(pageURL, "", "width=500,height=500");
+ await once(testPage, "load");
+ testPage.postMessage(testCase, window.origin);
+ let result = await nextWindowMessage();
+ is(result.data.allowedToPlay, testCase.expectedPlayResult, `allowed - ${testCase.name}`);
+ is(result.data.played, testCase.expectedPlayResult, `played - ${testCase.name}`);
+ testPage.close();
+ }
+ SimpleTest.finish();
+})();
+
+/**
+ * This function would set which type of request would be explicitly allowed,
+ * and the type of request we don't mention about would be pending forever.
+ * E.g. `setTestingPrefs({"audible", "allow"})` will allow the audible request
+ * and leave the inaudible request pending forever.
+ */
+async function setTestingPrefs({requestType, requestResult}) {
+ let prefVal = 0;
+ if (requestType == "all") {
+ if (requestResult == "pending") {
+ prefVal = 7;
+ } else {
+ prefVal = requestResult == "allowed" ? 1 : 2;
+ }
+ } else if (requestType == "audible") {
+ prefVal = requestResult == "allowed" ? 3 : 4;
+ } else if (requestType == "inaudible") {
+ prefVal = requestResult == "allowed" ? 5 : 6;
+ }
+ info(`set testing pref to ${prefVal}`);
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.geckoview.autoplay.request.testing", prefVal],
+ ["media.geckoview.autoplay.request", true]],
+ });
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/media/autoplay/test/mochitest/test_autoplay_policy.html b/dom/media/autoplay/test/mochitest/test_autoplay_policy.html
new file mode 100644
index 0000000000..dae388b21d
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/test_autoplay_policy.html
@@ -0,0 +1,174 @@
+
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Autoplay policy test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script>
+/* import-globals-from ../../../test/manifest.js */
+let manager = new MediaTestManager;
+
+gTestPrefs.push(["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED],
+ ["media.autoplay.blocking_policy", 0]);
+
+window.info = function(msg, token) {
+ SimpleTest.info(msg + ", token=" + token);
+}
+
+window.is = function(valA, valB, msg, token) {
+ SimpleTest.is(valA, valB, msg + ", token=" + token);
+}
+
+window.ok = function(val, msg, token) {
+ SimpleTest.ok(val, msg + ", token=" + token);
+}
+
+/**
+ * test files and paremeters
+ */
+var autoplayTests = [
+ /* video */
+ { name: "gizmo.mp4", type: "video/mp4", hasAudio:true },
+ { name: "gizmo-noaudio.mp4", type: "video/mp4", hasAudio:false },
+ { name: "gizmo.webm", type: "video/webm", hasAudio:true },
+ { name: "gizmo-noaudio.webm", type: "video/webm", hasAudio:false },
+ /* audio */
+ { name: "small-shot.ogg", type: "audio/ogg", hasAudio:true },
+ { name: "small-shot.m4a", type: "audio/mp4", hasAudio:true },
+ { name: "small-shot.mp3", type: "audio/mpeg", hasAudio:true },
+ { name: "small-shot.flac", type: "audio/flac", hasAudio:true },
+];
+
+var autoplayParams = [
+ { volume: 1.0, muted: false, preload: "none" },
+ { volume: 0.0, muted: false, preload: "none" },
+ { volume: 1.0, muted: true, preload: "none" },
+ { volume: 0.0, muted: true, preload: "none" },
+ { volume: 1.0, muted: false, preload: "metadata" },
+ { volume: 0.0, muted: false, preload: "metadata" },
+ { volume: 1.0, muted: true, preload: "metadata" },
+ { volume: 0.0, muted: true, preload: "metadata" },
+];
+
+function createTestArray()
+{
+ var tests = [];
+ for (let test of autoplayTests) {
+ for (let param of autoplayParams) {
+ tests.push({
+ name: test.name,
+ type: test.type,
+ hasAudio: test.hasAudio,
+ volume: param.volume,
+ muted: param.muted,
+ preload: param.preload,
+ });
+ }
+ }
+ return tests;
+}
+
+/**
+ * Main test function for different autoplay cases without user interaction.
+ *
+ * When the pref "media.autoplay.default" is 1 and the pref
+ * "media.autoplay.blocking_policy" is 0, we only allow
+ * audible media to autoplay after the website has been activated by specific
+ * user gestures. However, inaudible media won't be restricted.
+ *
+ * Audible means the volume is not zero, or muted is not true for the video with
+ * audio track. For media without loading metadata, we can't know whether it has
+ * audio track or not, so we would also regard it as audible media.
+ *
+ * Inaudible means the volume is zero, or the muted is true, or the video without
+ * audio track.
+ */
+async function runTest(test, token) {
+ manager.started(token);
+
+ await testPlay(test, token);
+ await testAutoplayKeyword(test, token);
+
+ manager.finished(token);
+}
+
+manager.runTests(createTestArray(), runTest);
+
+/**
+ * Different test scenarios
+ */
+async function testPlay(test, token) {
+ info("### start testPlay", token);
+ info(`volume=${test.volume}, muted=${test.muted}, ` +
+ `preload=${test.preload}, hasAudio=${test.hasAudio}`, token);
+
+ let element = document.createElement(getMajorMimeType(test.type));
+ element.volume = test.volume;
+ element.muted = test.muted;
+ element.src = test.name;
+ document.body.appendChild(element);
+
+ // Only need to test preload when calling play(), because media with 'autoplay'
+ // keyword always starts after loading enough data.
+ const preLoadNone = test.preload == "none";
+ if (!preLoadNone) {
+ info("### wait for loading metadata", token);
+ await once(element, "loadedmetadata");
+ }
+
+ let isAudible = (preLoadNone || test.hasAudio) &&
+ test.volume != 0.0 &&
+ !test.muted;
+ let state = isAudible? "audible" : "non-audible";
+ info(`### calling play() for ${state} media`, token);
+ let promise = element.play();
+ if (isAudible) {
+ await promise.catch(function(error) {
+ ok(element.paused, `${state} media fail to start via play()`, token);
+ is(error.name, "NotAllowedError", "rejected play promise", token);
+ });
+ } else {
+ // since we just want to check the value of 'paused', we don't need to wait
+ // resolved play promise. (it's equal to wait for 'playing' event)
+ await once(element, "play");
+ ok(!element.paused, `${state} media start via play()`, token);
+ }
+
+ removeNodeAndSource(element);
+}
+
+async function testAutoplayKeyword(test, token) {
+ info("### start testAutoplayKeyword", token);
+ info(`volume=${test.volume}, muted=${test.muted}, ` +
+ `hasAudio=${test.hasAudio}`, token);
+
+ let element = document.createElement(getMajorMimeType(test.type));
+ element.autoplay = true;
+ element.volume = test.volume;
+ element.muted = test.muted;
+ element.src = test.name;
+ document.body.appendChild(element);
+
+ let isAudible = test.hasAudio &&
+ test.volume != 0.0 &&
+ !test.muted;
+ let state = isAudible? "audible" : "non-audible";
+ info(`### wait to autoplay for ${state} media`, token);
+ if (isAudible) {
+ await once(element, "canplay");
+ ok(element.paused, `can not start with 'autoplay' keyword for ${state} media`, token);
+ } else {
+ await once(element, "play");
+ ok(!element.paused, `start with 'autoplay' keyword for ${state} media`, token);
+ }
+
+ removeNodeAndSource(element);
+}
+
+</script>
diff --git a/dom/media/autoplay/test/mochitest/test_autoplay_policy_activation.html b/dom/media/autoplay/test/mochitest/test_autoplay_policy_activation.html
new file mode 100644
index 0000000000..eae266030e
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/test_autoplay_policy_activation.html
@@ -0,0 +1,180 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Autoplay policy test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="AutoplayTestUtils.js"></script>
+ </head>
+ <body>
+ <pre id="test">
+ <script>
+
+ // Tests that videos can only play audibly in windows/frames
+ // which have been activated by same-origin user gesture.
+
+ gTestPrefs.push(["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED],
+ ["media.autoplay.blocking_policy", 0]);
+
+ SpecialPowers.pushPrefEnv({'set': gTestPrefs}, () => {
+ runTest();
+ });
+
+ let test_cases = [
+ {
+ name: "inaudible playback in unactivated same-origin iframe in activated parent -> allowed",
+ muted: true,
+ same_origin_child: true,
+ activated_from: "parent",
+ play_from: "child",
+ should_play: true,
+ },
+
+ {
+ name: "inaudible playback in unactivated same-origin iframe in unactivated parent -> allowed",
+ muted: true,
+ same_origin_child: true,
+ activated_from: "none",
+ play_from: "child",
+ should_play: true,
+ },
+
+ {
+ name: "audible playback in unactivated same-origin iframe in activated parent -> allowed",
+ muted: false,
+ same_origin_child: true,
+ activated_from: "parent",
+ play_from: "child",
+ should_play: true,
+ },
+
+ {
+ name: "audible playback in unactivated same-origin iframe in unactivated parent -> blocked",
+ muted: false,
+ same_origin_child: true,
+ activated_from: "none",
+ play_from: "child",
+ should_play: false,
+ },
+
+ {
+ name: "inaudible playback in unactivated cross-origin iframe in activated parent -> allowed",
+ muted: true,
+ same_origin_child: false,
+ activated_from: "parent",
+ play_from: "child",
+ should_play: true,
+ },
+
+ {
+ name: "inaudible playback in unactivated cross-origin iframe in unactivated parent -> allowed",
+ muted: true,
+ same_origin_child: false,
+ activated_from: "none",
+ play_from: "child",
+ should_play: true,
+ },
+
+ {
+ name: "audible playback in unactivated cross-origin iframe in activated parent -> allowed",
+ muted: false,
+ same_origin_child: false,
+ activated_from: "parent",
+ play_from: "child",
+ should_play: true,
+ },
+
+ {
+ name: "audible playback in unactivated cross-origin iframe in unactivated parent -> blocked",
+ muted: false,
+ same_origin_child: false,
+ activated_from: "none",
+ play_from: "child",
+ should_play: false,
+ },
+
+ {
+ name: "audible playback in activated cross-origin iframe -> allowed",
+ muted: false,
+ same_origin_child: false,
+ activated_from: "child",
+ play_from: "child",
+ should_play: true,
+ },
+
+ {
+ name: "audible playback in activated document -> allowed",
+ muted: false,
+ activated_from: "parent",
+ play_from: "parent",
+ should_play: true,
+ },
+
+ {
+ name: "audible playback in unactivated document -> blocked",
+ muted: false,
+ activated_from: "none",
+ play_from: "parent",
+ should_play: false,
+ },
+
+ {
+ name: "audible playback in activated document (via cross-origin child) -> allowed",
+ muted: false,
+ same_origin_child: false,
+ activated_from: "child",
+ play_from: "parent",
+ should_play: true,
+ },
+
+ {
+ name: "audible playback in activated document (via same-origin child) -> allowed",
+ muted: false,
+ same_origin_child: true,
+ activated_from: "child",
+ play_from: "parent",
+ should_play: true,
+ },
+
+ {
+ name: "inaudible playback in activated document -> allowed",
+ muted: true,
+ activated_from: "parent",
+ play_from: "parent",
+ should_play: true,
+ },
+
+ {
+ name: "inaudible playback in unactivated document -> allowed",
+ muted: true,
+ activated_from: "none",
+ play_from: "parent",
+ should_play: true,
+ },
+
+ ];
+
+ let child_url = "file_autoplay_policy_activation_window.html";
+
+ async function runTest() {
+ for (const test_case of test_cases) {
+ // Run each test in a new window, to ensure its user gesture
+ // activation state isn't tainted by preceeding tests.
+ let child = window.open(child_url, "", "width=500,height=500");
+ await once(child, "load");
+ child.postMessage(test_case, window.origin);
+ let result = await nextWindowMessage();
+ SimpleTest.is(result.data.allowedToPlay, test_case.should_play, "allowed - " + test_case.name);
+ SimpleTest.is(result.data.played, test_case.should_play, "played - " + test_case.name);
+ child.close();
+ }
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+
+ </script>
+ </pre>
+ </body>
+</html>
diff --git a/dom/media/autoplay/test/mochitest/test_autoplay_policy_eventdown_activation.html b/dom/media/autoplay/test/mochitest/test_autoplay_policy_eventdown_activation.html
new file mode 100644
index 0000000000..878f996ec5
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/test_autoplay_policy_eventdown_activation.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <title>Autoplay policy test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="AutoplayTestUtils.js"></script>
+</head>
+
+<body>
+ <pre id="test">
+ <script>
+
+ // Tests that we gesture activate on mousedown and keydown.
+
+ gTestPrefs.push(["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED],
+ ["media.autoplay.blocking_policy", 0]);
+
+ SpecialPowers.pushPrefEnv({ 'set': gTestPrefs }, () => {
+ runTest();
+ });
+
+ let child_url = "file_autoplay_policy_eventdown_activation.html";
+
+ async function runTest() {
+ // Run test in a new window, to ensure its user gesture
+ // activation state isn't tainted by preceeding tests.
+ {
+ let child = window.open(child_url, "", "width=500,height=500");
+ await once(child, "load");
+ child.postMessage("run keydown test", window.origin);
+ await nextWindowMessage();
+ child.close();
+ }
+
+ {
+ let child = window.open(child_url, "", "width=500,height=500");
+ await once(child, "load");
+ child.postMessage("run mousedown test", window.origin);
+ await nextWindowMessage();
+ child.close();
+ }
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+
+ </script>
+ </pre>
+</body>
+
+</html>
diff --git a/dom/media/autoplay/test/mochitest/test_autoplay_policy_key_blacklist.html b/dom/media/autoplay/test/mochitest/test_autoplay_policy_key_blacklist.html
new file mode 100644
index 0000000000..a85c63713a
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/test_autoplay_policy_key_blacklist.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <title>Autoplay policy test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="AutoplayTestUtils.js"></script>
+</head>
+
+<body>
+ <pre id="test">
+ <script>
+
+ // Tests that keypresses for non-printable characters,
+ // and mouse/keyboard interaction with editable elements,
+ // don't gesture activate documents, and don't unblock
+ // audible autoplay.
+
+ gTestPrefs.push(["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED],
+ ["media.autoplay.blocking_policy", 0]);
+
+ SpecialPowers.pushPrefEnv({ 'set': gTestPrefs }, () => {
+ runTest();
+ });
+
+ let child_url = "file_autoplay_policy_key_blacklist.html";
+
+ async function runTest() {
+ // Run test in a new window, to ensure its user gesture
+ // activation state isn't tainted by preceeding tests.
+ let child = window.open(child_url, "", "width=500,height=500");
+ await once(child, "load");
+ child.postMessage("run test", window.origin);
+ await nextWindowMessage();
+ child.close();
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+
+ </script>
+ </pre>
+</body>
+
+</html>
diff --git a/dom/media/autoplay/test/mochitest/test_autoplay_policy_permission.html b/dom/media/autoplay/test/mochitest/test_autoplay_policy_permission.html
new file mode 100644
index 0000000000..b91e4d7be8
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/test_autoplay_policy_permission.html
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <title>Autoplay policy test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="AutoplayTestUtils.js"></script>
+</head>
+
+<body>
+ <pre id="test">
+ <script>
+
+ // Tests that origins with "autoplay-media" permission can autoplay.
+
+ gTestPrefs.push(["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED],
+ ["media.autoplay.blocking_policy", 0]);
+
+ SpecialPowers.pushPrefEnv({ 'set': gTestPrefs }, () => {
+ runTest();
+ });
+
+ async function testPlayInOrigin(testCase) {
+ // Run test in a new window, to ensure its user gesture
+ // activation state isn't tainted by preceeding tests.
+ let url = testCase.origin + "/tests/dom/media/autoplay/test/mochitest/file_autoplay_policy_activation_frame.html";
+ let child = window.open(url, "", "width=500,height=500");
+ is((await nextWindowMessage()).data, "ready", "Expected a 'ready' message");
+ child.postMessage("play-audible", testCase.origin);
+ // Wait for the window to tell us whether it could play video.
+ let result = await nextWindowMessage();
+ is(result.data.allowedToPlay, testCase.shouldPlay, "allowedToPlay - " + testCase.message);
+ is(result.data.played, testCase.shouldPlay, "played - " + testCase.message);
+ child.close();
+ }
+
+ async function runTest() {
+ // First verify that we can't play in a document unwhitelisted.
+ is(window.origin, "http://mochi.test:8888", "Origin should be as we assume, otherwise the rest of the test is bogus!");
+
+ await testPlayInOrigin({
+ origin: "http://mochi.test:8888",
+ shouldPlay: false,
+ message: "Should not be able to play unwhitelisted."
+ });
+
+ // Add our origin to the whitelist.
+ await pushAutoplayAllowedPermission();
+
+ // Now we should be able to play...
+ await testPlayInOrigin({
+ origin: "http://mochi.test:8888",
+ shouldPlay: true,
+ message: "Should be able to play since whitelisted."
+ });
+
+ // But sub-origins should not.
+ await testPlayInOrigin({
+ origin: "http://test1.mochi.test:8888",
+ shouldPlay: false,
+ message: "Sub origin should not count as whitelisted."
+ });
+ await testPlayInOrigin({
+ origin: "http://sub1.test1.mochi.test:8888",
+ shouldPlay: false,
+ message: "Sub-sub-origins should not count as whitelisted."
+ });
+
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+
+ </script>
+ </pre>
+</body>
+
+</html>
diff --git a/dom/media/autoplay/test/mochitest/test_autoplay_policy_play_before_loadedmetadata.html b/dom/media/autoplay/test/mochitest/test_autoplay_policy_play_before_loadedmetadata.html
new file mode 100644
index 0000000000..b5f70be227
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/test_autoplay_policy_play_before_loadedmetadata.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <title>Autoplay policy test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="AutoplayTestUtils.js"></script>
+</head>
+
+<body>
+ <pre id="test">
+ <script>
+
+ window.is = SimpleTest.is;
+ window.info = SimpleTest.info;
+
+ // Tests that videos which have no audio track will play if play()
+ // is called before the video has reached readyState >= HAVE_METADATA.
+
+ gTestPrefs.push(["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED],
+ ["media.autoplay.blocking_policy", 0]);
+
+ SpecialPowers.pushPrefEnv({ 'set': gTestPrefs }, () => {
+ runTest();
+ });
+
+ let testCases = [
+ {
+ resource: "320x240.ogv", // Only video track.
+ shouldPlay: false,
+ muted: false,
+ },
+ {
+ resource: "320x240.ogv", // Only video track.
+ shouldPlay: true,
+ muted: true,
+ },
+ {
+ resource: "short.mp4", // Audio and video track.
+ shouldPlay: false,
+ muted: false,
+ },
+ {
+ resource: "short.mp4", // Audio and video track.
+ shouldPlay: true,
+ muted: true,
+ },
+ ];
+
+ let child_url = "file_autoplay_policy_play_before_loadedmetadata.html";
+
+ async function runTest() {
+ for (const testCase of testCases) {
+ // Run each test in a new window, to ensure its user gesture
+ // activation state isn't tainted by preceeding tests.
+ let child = window.open(child_url, "", "width=500,height=500");
+ await once(child, "load");
+ child.postMessage(testCase, window.origin);
+ await nextWindowMessage();
+ child.close();
+ }
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+
+ </script>
+ </pre>
+</body>
+
+</html>
diff --git a/dom/media/autoplay/test/mochitest/test_autoplay_policy_unmute_pauses.html b/dom/media/autoplay/test/mochitest/test_autoplay_policy_unmute_pauses.html
new file mode 100644
index 0000000000..29ce4b801f
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/test_autoplay_policy_unmute_pauses.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <title>Autoplay policy test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="AutoplayTestUtils.js"></script>
+</head>
+
+<body>
+ <pre id="test">
+ <script>
+
+ window.is = SimpleTest.is;
+ window.info = SimpleTest.info;
+
+ // Tests that videos can only play audibly in windows/frames
+ // which have been activated by same-origin user gesture.
+
+ gTestPrefs.push(["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED],
+ ["media.autoplay.blocking_policy", 0]);
+
+ SpecialPowers.pushPrefEnv({ 'set': gTestPrefs }, () => {
+ runTest();
+ });
+
+ let testCases = [
+ {
+ property: "muted",
+ inaudible: true,
+ audible: false,
+ },
+
+ {
+ property: "volume",
+ inaudible: 0.0,
+ audible: 1.0,
+ },
+ ];
+
+ let child_url = "file_autoplay_policy_unmute_pauses.html";
+
+ async function runTest() {
+ for (const testCase of testCases) {
+ // Run each test in a new window, to ensure its user gesture
+ // activation state isn't tainted by preceeding tests.
+ let child = window.open(child_url, "", "width=500,height=500");
+ await once(child, "load");
+ child.postMessage(testCase, window.origin);
+ await nextWindowMessage();
+ child.close();
+ }
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+
+ </script>
+ </pre>
+</body>
+
+</html>
diff --git a/dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_AudioParamStream.html b/dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_AudioParamStream.html
new file mode 100644
index 0000000000..27dfa5388f
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_AudioParamStream.html
@@ -0,0 +1,171 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Autoplay policy test : suspend/resume the AudioParam's stream</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<script>
+
+/**
+ * This test is used to ensure the AudioParam's stream can be suspended/resumed
+ * by AudioContext.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+(async function testSuspendAndResumeAudioParamStreams() {
+ await setupTestPreferences();
+
+ info(`- create the AudioContext -`);
+ createAudioContext();
+
+ info(`- the AudioContext is not allowed to start in beginning -`);
+ await audioContextShouldBeBlocked();
+
+ info(`- connect AudioScheduledSourceNode to the AudioParam and start AudioScheduledSourceNode, the AudioParam's stream should be suspended in the beginning -`)
+ let audioParamsArr = await connectAudioScheduledSourceNodeToAudioParams();
+
+ info(`- the AudioContext and the AudioParam's stream should be resumed -`);
+ await audioContextAndAudioParamStreamsShouldBeResumed(audioParamsArr);
+
+ info(`- suspend the AudioContext which should also suspend the AudioParam's stream -`);
+ await suspendAudioContextAndAudioParamStreams(audioParamsArr);
+
+ endTest();
+})();
+
+/**
+ * Test utility functions
+ */
+
+function setupTestPreferences() {
+ return SpecialPowers.pushPrefEnv({"set": [
+ ["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED],
+ ["media.autoplay.blocking_policy", 0],
+ ["media.autoplay.block-webaudio", true],
+ ["media.autoplay.block-event.enabled", true],
+ ]});
+}
+
+function createAudioContext() {
+ /* global ac */
+ window.ac = new AudioContext();
+
+ ac.allowedToStart = new Promise(resolve => {
+ ac.addEventListener("statechange", function() {
+ if (ac.state === "running") {
+ resolve();
+ }
+ }, {once: true});
+ });
+
+ ac.notAllowedToStart = new Promise(resolve => {
+ ac.addEventListener("blocked", async function() {
+ resolve();
+ }, {once: true});
+ });
+}
+
+async function audioContextShouldBeBlocked() {
+ await ac.notAllowedToStart;
+ is(ac.state, "suspended", `AudioContext is blocked.`);
+}
+
+function createAudioParams(nodeType) {
+ switch (nodeType) {
+ case "audioBufferSource":
+ let buffer = ac.createBufferSource();
+ return [buffer.playbackRate, buffer.detune];
+ case "biquadFilter":
+ let bf = ac.createBiquadFilter();
+ return [bf.frequency, bf.detune, bf.Q, bf.gain];
+ case "constantSource":
+ return [ac.createConstantSource().offset];
+ case "dynamicsCompressor":
+ let dc = ac.createDynamicsCompressor();
+ return [dc.threshold, dc.knee, dc.ratio, dc.attack, dc.release];
+ case "delay":
+ return [ac.createDelay(5.0).delayTime];
+ case "gain":
+ return [ac.createGain().gain];
+ case "oscillator":
+ let osc = ac.createOscillator();
+ return [osc.frequency, osc.detune];
+ case "panner":
+ let panner = ac.createPanner();
+ return [panner.positionX, panner.positionY, panner.positionZ,
+ panner.orientationX, panner.orientationY, panner.orientationZ];
+ case "stereoPanner":
+ return [ac.createStereoPanner().pan];
+ default:
+ ok(false, `non-defined node type ${nodeType}.`);
+ return [];
+ }
+}
+
+function createAudioParamArrWithName(nodeType) {
+ let audioParamsArr = createAudioParams(nodeType);
+ for (let audioParam of audioParamsArr) {
+ audioParam.name = nodeType;
+ }
+ return audioParamsArr;
+}
+
+function createAllAudioParamsFromDifferentAudioNode() {
+ const NodesWithAudioParam =
+ ["audioBufferSource", "biquadFilter", "constantSource", "delay",
+ "dynamicsCompressor", "gain", "oscillator", "panner", "stereoPanner"];
+ let audioParamsArr = [];
+ for (let nodeType of NodesWithAudioParam) {
+ audioParamsArr = audioParamsArr.concat(createAudioParamArrWithName(nodeType));
+ }
+ ok(audioParamsArr.length >= NodesWithAudioParam.length,
+ `Length of AudioParam array (${audioParamsArr.length}) is longer than the "
+ "length of node type array (${NodesWithAudioParam.length}).`);
+ return audioParamsArr;
+}
+
+function connectAudioScheduledSourceNodeToAudioParams() {
+ let osc = ac.createOscillator();
+ let audioParamsArr = createAllAudioParamsFromDifferentAudioNode();
+ for (let audioParam of audioParamsArr) {
+ osc.connect(audioParam);
+ ok(SpecialPowers.wrap(audioParam).isTrackSuspended,
+ `(${audioParam.name}) audioParam's stream has been suspended.`);
+ }
+
+ // simulate user gesture in order to start video.
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ osc.start();
+ return audioParamsArr;
+}
+
+async function audioContextAndAudioParamStreamsShouldBeResumed(audioParamsArr) {
+ await ac.allowedToStart;
+ is(ac.state, "running", `AudioContext is allowed to start.`);
+ for (let audioParam of audioParamsArr) {
+ ok(!SpecialPowers.wrap(audioParam).isTrackSuspended,
+ `(${audioParam.name}) audioParam's stream has been resumed.`);;
+ }
+}
+
+async function suspendAudioContextAndAudioParamStreams(audioParamsArr) {
+ await ac.suspend();
+ is(ac.state, "suspended", `AudioContext is suspended.`);
+ for (let audioParam of audioParamsArr) {
+ ok(SpecialPowers.wrap(audioParam).isTrackSuspended,
+ `(${audioParam.name}) audioParam's stream has been suspended.`);;
+ }
+}
+
+function endTest() {
+ // reset the activation flag in order not to interfere following test in the
+ // verify mode which would run the test using same document couple times.
+ SpecialPowers.wrap(document).clearUserGestureActivation();
+ SimpleTest.finish();
+}
+
+</script>
diff --git a/dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_createMediaStreamSource.html b/dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_createMediaStreamSource.html
new file mode 100644
index 0000000000..5fe9aa64fc
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_createMediaStreamSource.html
@@ -0,0 +1,119 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Autoplay policy test : createMediaStreamSource with active stream</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<script>
+
+/**
+ * This test is used to ensure that we would try to start the blocked AudioContext
+ * which is blocked by autoplay policy, when it creates a MediaStreamAudioSourceNode
+ * which has a active input stream.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+(async function testStartAudioContextWhenCreatingMediaStreamAudioSourceWithActiveStream() {
+ await setupTestPreferences();
+
+ info(`- create 2 AudioContext, one is used to generate active stream, another one is used to test whether it would be resumed after starting MediaStreamAudioSource with active stream -`);
+ createAudioContexts();
+
+ info(`- both AudioContext are not allowed to start in beginning -`);
+ await audioContextsShouldBeBlocked();
+
+ info(`- using AudioContext2 to create a MediaStreamAudioSourceNode with active stream, which should resume AudioContext2 -`);
+ await createMediaStreamAudioSourceByAudioContext2();
+
+ endTest();
+})();
+
+/**
+ * Test utility functions
+ */
+
+function setupTestPreferences() {
+ return SpecialPowers.pushPrefEnv({"set": [
+ ["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED],
+ ["media.autoplay.blocking_policy", 0],
+ ["media.autoplay.block-webaudio", true],
+ ["media.autoplay.block-event.enabled", true],
+ ]});
+}
+
+function createAudioContexts() {
+ /* global ac1, ac2 */
+ window.ac1 = new AudioContext();
+ window.ac2 = new AudioContext();
+
+ ac1.allowedToStart = new Promise(resolve => {
+ ac1.addEventListener("statechange", function() {
+ if (ac1.state === "running") {
+ resolve();
+ }
+ }, {once: true});
+ });
+
+ ac1.notAllowedToStart = new Promise(resolve => {
+ ac1.addEventListener("blocked", async function() {
+ resolve();
+ }, {once: true});
+ });
+
+
+ ac2.allowedToStart = new Promise(resolve => {
+ ac2.addEventListener("statechange", function() {
+ if (ac2.state === "running") {
+ resolve();
+ }
+ }, {once: true});
+ });
+
+ ac2.notAllowedToStart = new Promise(resolve => {
+ ac2.addEventListener("blocked", async function() {
+ resolve();
+ }, {once: true});
+ });
+}
+
+async function audioContextsShouldBeBlocked() {
+ await ac1.notAllowedToStart;
+ await ac2.notAllowedToStart;
+ is(ac1.state, "suspended", `AudioContext1 is blocked.`);
+ is(ac2.state, "suspended", `AudioContext2 is blocked.`);
+}
+
+async function startAudioContext1() {
+ // simulate user gesture in order to start video.
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ ok(await ac1.resume().then(() => true, () => false), `resumed AudioContext1.`);
+ await ac1.allowedToStart;
+ is(ac1.state, "running", `AudioContext1 is running.`);
+}
+
+async function getActiveStream() {
+ await startAudioContext1();
+ // As AudioContext1 has been resumed, we can use it to create active stream.
+ return ac1.createMediaStreamDestination().stream;
+}
+
+async function createMediaStreamAudioSourceByAudioContext2() {
+ is(ac2.state, "suspended", `AudioContext2 is suspended.`);
+ let source = ac2.createMediaStreamSource(await getActiveStream());
+ source.connect(ac2.destination);
+ await ac2.allowedToStart;
+ is(ac2.state, "running", `AudioContext2 is running.`);
+}
+
+function endTest() {
+ // reset the activation flag in order not to interfere following test in the
+ // verify mode which would run the test using same document couple times.
+ SpecialPowers.wrap(document).clearUserGestureActivation();
+ SimpleTest.finish();
+}
+
+</script>
diff --git a/dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_mediaElementAudioSourceNode.html b/dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_mediaElementAudioSourceNode.html
new file mode 100644
index 0000000000..41fab54133
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_mediaElementAudioSourceNode.html
@@ -0,0 +1,105 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Autoplay policy test : use media element as source for web audio</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<script>
+/* import-globals-from ../../../test/manifest.js */
+/**
+ * This test is used to ensure blocked AudioContext would be resumed when the
+ * source media element of MediaElementAudioSouceNode which has been created and
+ * connected to destinationnode starts.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+(async function testResumeAudioContextWhenMediaElementSourceStarted() {
+ await setupTestPreferences();
+
+ info(`- create audio context -`);
+ createAudioContext();
+
+ info(`- AudioContext is not allowed to start in beginning -`);
+ await audioContextShouldBeBlocked();
+
+ info(`- create a source for web audio and start the source -`);
+ await useMediaElementAsSourceAndPlayMediaElement();
+
+ info(`- AudioContext should be allowed to start after MediaElementAudioSourceNode started -`);
+ await audioContextShouldBeAllowedToStart();
+
+ endTest();
+})();
+
+/**
+ * Test utility functions
+ */
+
+function setupTestPreferences() {
+ return SpecialPowers.pushPrefEnv({"set": [
+ ["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED],
+ ["media.autoplay.blocking_policy", 0],
+ ["media.autoplay.block-webaudio", true],
+ ["media.autoplay.block-event.enabled", true],
+ ]});
+}
+
+function createAudioContext() {
+ /* global ac */
+ window.ac = new AudioContext();
+
+ ac.allowedToStart = new Promise(resolve => {
+ ac.addEventListener("statechange", function() {
+ if (ac.state === "running") {
+ resolve();
+ }
+ }, {once: true});
+ });
+
+ ac.notAllowedToStart = new Promise(resolve => {
+ ac.addEventListener("blocked", async function() {
+ resolve();
+ }, {once: true});
+ });
+}
+
+async function audioContextShouldBeBlocked() {
+ await ac.notAllowedToStart;
+ is(ac.state, "suspended", `AudioContext is blocked.`);
+}
+
+async function useMediaElementAsSourceAndPlayMediaElement() {
+ let video = document.createElement('video');
+ video.src = "gizmo.mp4";
+
+ let source = ac.createMediaElementSource(video);
+ source.connect(ac.destination);
+ // simulate user gesture in order to start video.
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ await playVideo(video);
+}
+
+async function playVideo(video) {
+ video.play();
+ await once(video, "play");
+ ok(true, `video started.`);
+ removeNodeAndSource(video);
+}
+
+async function audioContextShouldBeAllowedToStart() {
+ await ac.allowedToStart;
+ is(ac.state, "running", `AudioContext is allowed to start.`);
+}
+
+function endTest() {
+ // reset the activation flag in order not to interfere following test in the
+ // verify mode which would run the test using same document couple times.
+ SpecialPowers.wrap(document).clearUserGestureActivation();
+ SimpleTest.finish();
+}
+
+</script>
diff --git a/dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_notResumePageInvokedSuspendedAudioContext.html b/dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_notResumePageInvokedSuspendedAudioContext.html
new file mode 100644
index 0000000000..46df256391
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/test_autoplay_policy_web_audio_notResumePageInvokedSuspendedAudioContext.html
@@ -0,0 +1,96 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Autoplay policy test : do not resume AudioContext which is suspended by page</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<script>
+/* import-globals-from ../../../test/manifest.js */
+/**
+ * This test is used to ensure we won't resume AudioContext which is suspended
+ * by page (it means calling suspend() explicitly) when calling
+ * `AudioScheduledSourceNode.start()`.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+(async function testNotResumeUserInvokedSuspendedAudioContext() {
+ await setupTestPreferences();
+
+ const nodeTypes = ["AudioBufferSourceNode", "ConstantSourceNode", "OscillatorNode"];
+ for (let nodeType of nodeTypes) {
+ info(`- create an audio context which should not be allowed to start, it's allowed to be created, but it's forbidden to start -`);
+ await createAudioContext();
+
+ info(`- explicitly suspend the AudioContext in the page -`);
+ suspendAudioContext();
+
+ info(`- start an 'AudioScheduledSourceNode', and check that the AudioContext does not start, because it has been explicitly suspended -`);
+ await createAndStartAudioScheduledSourceNode(nodeType);
+ }
+
+ SimpleTest.finish();
+})();
+
+/**
+ * Test utility functions
+ */
+
+function setupTestPreferences() {
+ return SpecialPowers.pushPrefEnv({"set": [
+ ["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED],
+ ["media.autoplay.blocking_policy", 0],
+ ["media.autoplay.block-webaudio", true],
+ ["media.autoplay.block-event.enabled", true],
+ ]});
+}
+
+async function createAudioContext() {
+ /* global ac */
+ window.ac = new AudioContext();
+ await once(ac, "blocked");
+ is(ac.state, "suspended", `AudioContext is blocked.`);
+}
+
+function suspendAudioContext() {
+ try {
+ ac.suspend();
+ } catch(e) {
+ ok(false, `AudioContext suspend failed!`);
+ }
+}
+
+async function createAndStartAudioScheduledSourceNode(nodeType) {
+ let node;
+ info(`- create ${nodeType} -`);
+ switch (nodeType) {
+ case "AudioBufferSourceNode":
+ node = ac.createBufferSource();
+ break;
+ case "ConstantSourceNode":
+ node = ac.createConstantSource();
+ break;
+ case "OscillatorNode":
+ node = ac.createOscillator();
+ break;
+ default:
+ ok(false, "undefined AudioScheduledSourceNode type");
+ return;
+ }
+ node.connect(ac.destination);
+
+ // activate the document in order to allow autoplay.
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ node.start();
+
+ await once(ac, "blocked");
+ is(ac.state, "suspended", `AudioContext should not be resumed.`);
+
+ // reset the activation flag of the document in order not to interfere next test.
+ SpecialPowers.wrap(document).clearUserGestureActivation();
+}
+
+</script>
diff --git a/dom/media/autoplay/test/mochitest/test_streams_autoplay.html b/dom/media/autoplay/test/mochitest/test_streams_autoplay.html
new file mode 100644
index 0000000000..0b8630a323
--- /dev/null
+++ b/dom/media/autoplay/test/mochitest/test_streams_autoplay.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test that a MediaStream source triggers autoplay</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+/* import-globals-from ../../../test/manifest.js */
+SimpleTest.waitForExplicitFinish();
+
+var media = getPlayableVideo(gSmallTests);
+
+if (media == null) {
+ todo(false, "No media supported.");
+ SimpleTest.finish();
+} else {
+ function startTest() {
+ var v1 = document.createElement('video');
+ var v2 = document.createElement('video');
+ v1.preload = 'metadata';
+ v2.autoplay = true;
+ document.body.appendChild(v1);
+ document.body.appendChild(v2);
+
+ v1.src = media.name;
+ v1.onloadedmetadata = function() {
+ v2.srcObject = v1.mozCaptureStream();
+ v1.play();
+ };
+
+ v2.addEventListener('playing', function() {
+ ok(true, "playback started");
+ SimpleTest.finish();
+ }, {once: true});
+ }
+
+ startTest();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/benchmark/sample b/dom/media/benchmark/sample
new file mode 100755
index 0000000000..4f4f7a56fb
--- /dev/null
+++ b/dom/media/benchmark/sample
@@ -0,0 +1,7 @@
+#!/bin/bash -e
+wget http://people.mozilla.org/~ajones/frame-drop-test/bbb-31.webm -O bbb-31.webm
+ffmpeg -i bbb-31.webm -frames:v 8 -codec copy -map 0:0 sWebMSample.webm -y
+mv sWebMSample.webm sWebMSample
+xxd -i sWebMSample |head --lines=-1 | sed 's/unsigned char/static const uint8_t/' > ../WebMSample.h
+rm sWebMSample
+rm bbb-31.webm
diff --git a/dom/media/bridge/IPeerConnection.idl b/dom/media/bridge/IPeerConnection.idl
new file mode 100644
index 0000000000..2986ef8b81
--- /dev/null
+++ b/dom/media/bridge/IPeerConnection.idl
@@ -0,0 +1,58 @@
+#include "nsIThread.idl"
+#include "nsIDOMWindow.idl"
+#include "nsIPropertyBag2.idl"
+
+/* Do not confuse with nsIDOMRTCPeerConnection. This interface is purely for
+ * communication between the PeerConnection JS DOM binding and the C++
+ * implementation in SIPCC.
+ *
+ * See media/webrtc/signaling/include/PeerConnectionImpl.h
+ */
+[scriptable, uuid(d7dfe148-0416-446b-a128-66a7c71ae8d3)]
+interface IPeerConnectionObserver : nsISupports
+{
+};
+
+[scriptable, uuid(14afc8e7-e421-4d0c-99a5-69308d871481)]
+interface IPeerConnection : nsISupports
+{
+ const unsigned long kHintAudio = 0x00000001;
+ const unsigned long kHintVideo = 0x00000002;
+
+ const long kActionNone = -1;
+ const long kActionOffer = 0;
+ const long kActionAnswer = 1;
+ const long kActionPRAnswer = 2;
+ const long kActionRollback = 3;
+
+ const long kIceGathering = 0;
+ const long kIceWaiting = 1;
+ const long kIceChecking = 2;
+ const long kIceConnected = 3;
+ const long kIceFailed = 4;
+
+ /* for readyState on Peer Connection */
+ const long kNew = 0;
+ const long kNegotiating = 1;
+ const long kActive = 2;
+ const long kClosing = 3;
+ const long kClosed = 4;
+
+ /* for 'type' in DataChannelInit dictionary */
+ const unsigned short kDataChannelReliable = 0;
+ const unsigned short kDataChannelPartialReliableRexmit = 1;
+ const unsigned short kDataChannelPartialReliableTimed = 2;
+
+ /* Constants for 'name' in error callbacks */
+ const unsigned long kNoError = 0; // Test driver only
+ const unsigned long kInvalidCandidate = 2;
+ const unsigned long kInvalidMediastreamTrack = 3;
+ const unsigned long kInvalidState = 4;
+ const unsigned long kInvalidSessionDescription = 5;
+ const unsigned long kIncompatibleSessionDescription = 6;
+ const unsigned long kIncompatibleMediaStreamTrack = 8;
+ const unsigned long kInternalError = 9;
+ const unsigned long kTypeError = 10;
+ const unsigned long kOperationError = 11;
+ const unsigned long kMaxErrorType = 11; // Same as final error
+};
diff --git a/dom/media/bridge/MediaModule.cpp b/dom/media/bridge/MediaModule.cpp
new file mode 100644
index 0000000000..ff67751f10
--- /dev/null
+++ b/dom/media/bridge/MediaModule.cpp
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/Components.h"
+
+#ifdef MOZ_WEBRTC
+# include "PeerConnectionImpl.h"
+
+using namespace mozilla;
+
+NS_IMPL_COMPONENT_FACTORY(mozilla::PeerConnectionImpl) {
+ return do_AddRef(new PeerConnectionImpl()).downcast<nsISupports>();
+}
+
+#endif
diff --git a/dom/media/bridge/components.conf b/dom/media/bridge/components.conf
new file mode 100644
index 0000000000..258d111752
--- /dev/null
+++ b/dom/media/bridge/components.conf
@@ -0,0 +1,25 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+ {
+ 'cid': '{b93af7a1-3411-44a8-bd0a-8af3dde4d8d8}',
+ 'contract_ids': ['@mozilla.org/peerconnection;1'],
+ 'type': 'mozilla::PeerConnectionImpl',
+ },
+ {
+ 'cid': '{9fea635a-2fc2-4d08-9721-d238d3f52f92}',
+ 'contract_ids': ['@mozilla.org/network/tcp-filter-handler;1?name=stun'],
+ 'type': 'nsStunTCPSocketFilterHandler',
+ 'headers': ['transport/stun_socket_filter.h'],
+ },
+ {
+ 'cid': '{3e43ee93-829e-4ea6-a34e-62d9e4c9f993}',
+ 'contract_ids': ['@mozilla.org/network/udp-filter-handler;1?name=stun'],
+ 'type': 'nsStunUDPSocketFilterHandler',
+ 'headers': ['transport/stun_socket_filter.h'],
+ },
+]
diff --git a/dom/media/bridge/moz.build b/dom/media/bridge/moz.build
new file mode 100644
index 0000000000..969953ef2f
--- /dev/null
+++ b/dom/media/bridge/moz.build
@@ -0,0 +1,36 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+XPIDL_SOURCES += [
+ "IPeerConnection.idl",
+]
+
+XPIDL_MODULE = "peerconnection"
+
+SOURCES += [
+ "MediaModule.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "!/ipc/ipdl/_ipdlheaders",
+ "/dom/media/webrtc/",
+ "/dom/media/webrtc/common/time_profiling",
+ "/dom/media/webrtc/jsapi",
+ "/dom/media/webrtc/libwebrtcglue",
+ "/dom/media/webrtc/transport",
+ "/dom/media/webrtc/transportbridge",
+ "/ipc/chromium/src",
+ "/media/webrtc/",
+ "/third_party/libwebrtc",
+ "/third_party/libwebrtc/third_party/abseil-cpp",
+]
+
+if CONFIG["MOZ_WEBRTC"]:
+ XPCOM_MANIFESTS += [
+ "components.conf",
+ ]
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/components.conf b/dom/media/components.conf
new file mode 100644
index 0000000000..ba84bacbcf
--- /dev/null
+++ b/dom/media/components.conf
@@ -0,0 +1,74 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+ {
+ 'cid': '{bdc2e533-b308-4708-ac8e-a8bfade6d851}',
+ 'contract_ids': ['@mozilla.org/dom/peerconnection;1'],
+ 'esModule': 'resource://gre/modules/media/PeerConnection.sys.mjs',
+ 'constructor': 'RTCPeerConnection',
+ },
+ {
+ 'cid': '{d1748d4c-7f6a-4dc5-add6-d55b7678537e}',
+ 'contract_ids': ['@mozilla.org/dom/peerconnectionobserver;1'],
+ 'esModule': 'resource://gre/modules/media/PeerConnection.sys.mjs',
+ 'constructor': 'PeerConnectionObserver',
+ },
+ {
+ 'cid': '{3610C242-654E-11E6-8EC0-6D1BE389A607}',
+ 'contract_ids': ['@mozilla.org/dom/rtcdtmfsender;1'],
+ 'esModule': 'resource://gre/modules/media/PeerConnection.sys.mjs',
+ 'constructor': 'RTCDTMFSender',
+ },
+ {
+ 'cid': '{02b9970c-433d-4cc2-923d-f7028ac66073}',
+ 'contract_ids': ['@mozilla.org/dom/rtcicecandidate;1'],
+ 'esModule': 'resource://gre/modules/media/PeerConnection.sys.mjs',
+ 'constructor': 'RTCIceCandidate',
+ },
+ {
+ 'cid': '{1775081b-b62d-4954-8ffe-a067bbf508a7}',
+ 'contract_ids': ['@mozilla.org/dom/rtcsessiondescription;1'],
+ 'esModule': 'resource://gre/modules/media/PeerConnection.sys.mjs',
+ 'constructor': 'RTCSessionDescription',
+ },
+ {
+ 'cid': '{7293e901-2be3-4c02-b4bd-cbef6fc24f78}',
+ 'contract_ids': ['@mozilla.org/dom/peerconnectionmanager;1'],
+ 'esModule': 'resource://gre/modules/media/PeerConnection.sys.mjs',
+ 'constructor': 'GlobalPCList',
+ },
+ {
+ 'cid': '{7fe6e18b-0da3-4056-bf3b-440ef3809e06}',
+ 'contract_ids': ['@mozilla.org/dom/rtcstatsreport;1'],
+ 'esModule': 'resource://gre/modules/media/PeerConnection.sys.mjs',
+ 'constructor': 'RTCStatsReport',
+ },
+ {
+ 'cid': '{0fb47c47-a205-4583-a9fc-cbadf8c95880}',
+ 'contract_ids': ['@mozilla.org/dom/peerconnectionstatic;1'],
+ 'esModule': 'resource://gre/modules/media/PeerConnection.sys.mjs',
+ 'constructor': 'RTCPeerConnectionStatic',
+ },
+ {
+ 'cid': '{4fff5d46-d827-4cd4-a970-8fd53977440e}',
+ 'contract_ids': ['@mozilla.org/dom/rtpsender;1'],
+ 'esModule': 'resource://gre/modules/media/PeerConnection.sys.mjs',
+ 'constructor': 'RTCRtpSender',
+ },
+ {
+ 'cid': '{d974b814-8fde-411c-8c45-b86791b81030}',
+ 'contract_ids': ['@mozilla.org/dom/rtpreceiver;1'],
+ 'esModule': 'resource://gre/modules/media/PeerConnection.sys.mjs',
+ 'constructor': 'RTCRtpReceiver',
+ },
+ {
+ 'cid': '{74b2122d-65a8-4824-aa9e-3d664cb75dc2}',
+ 'contract_ids': ['@mozilla.org/dom/createofferrequest;1'],
+ 'esModule': 'resource://gre/modules/media/PeerConnection.sys.mjs',
+ 'constructor': 'CreateOfferRequest',
+ },
+]
diff --git a/dom/media/doctor/DDLifetime.cpp b/dom/media/doctor/DDLifetime.cpp
new file mode 100644
index 0000000000..2d4c6cb966
--- /dev/null
+++ b/dom/media/doctor/DDLifetime.cpp
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DDLifetime.h"
+
+namespace mozilla {
+
+void DDLifetime::AppendPrintf(nsCString& aString) const {
+ if (!mDerivedObject.Pointer()) {
+ mObject.AppendPrintf(aString);
+ aString.AppendPrintf("#%" PRIi32, mTag);
+ } else {
+ mDerivedObject.AppendPrintf(aString);
+ aString.AppendPrintf("#%" PRIi32 " (as ", mTag);
+ if (mObject.Pointer() == mDerivedObject.Pointer()) {
+ aString.Append(mObject.TypeName());
+ } else {
+ mObject.AppendPrintf(aString);
+ }
+ aString.Append(")");
+ }
+}
+
+nsCString DDLifetime::Printf() const {
+ nsCString s;
+ AppendPrintf(s);
+ return s;
+}
+
+} // namespace mozilla
diff --git a/dom/media/doctor/DDLifetime.h b/dom/media/doctor/DDLifetime.h
new file mode 100644
index 0000000000..3c97c064d8
--- /dev/null
+++ b/dom/media/doctor/DDLifetime.h
@@ -0,0 +1,72 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DDLifetime_h_
+#define DDLifetime_h_
+
+#include "DDLogObject.h"
+#include "DDMessageIndex.h"
+#include "DDTimeStamp.h"
+
+namespace mozilla {
+
+namespace dom {
+class HTMLMediaElement;
+} // namespace dom
+
+// This struct records the lifetime of one C++ object.
+// Note that multiple objects may have the same address and type (at different
+// times), so the recorded construction/destruction times should be used to
+// distinguish them.
+struct DDLifetime {
+ const DDLogObject mObject;
+ const DDMessageIndex mConstructionIndex;
+ const DDTimeStamp mConstructionTimeStamp;
+ // Only valid when mDestructionTimeStamp is not null.
+ DDMessageIndex mDestructionIndex;
+ DDTimeStamp mDestructionTimeStamp;
+ // Associated HTMLMediaElement, initially nullptr until this object can be
+ // linked to its HTMLMediaElement.
+ const dom::HTMLMediaElement* mMediaElement;
+ // If not null, derived object for which this DDLifetime is a base class.
+ // This is used to link messages from the same object, even when they
+ // originate from a method on a base class.
+ // Note: We assume a single-inheritance hierarchy.
+ DDLogObject mDerivedObject;
+ DDMessageIndex mDerivedObjectLinkingIndex;
+ // Unique tag used to identify objects in a log, easier to read than object
+ // pointers.
+ // Negative and unique for unassociated objects.
+ // Positive for associated objects, and unique for that HTMLMediaElement
+ // group.
+ int32_t mTag;
+
+ DDLifetime(DDLogObject aObject, DDMessageIndex aConstructionIndex,
+ DDTimeStamp aConstructionTimeStamp, int32_t aTag)
+ : mObject(aObject),
+ mConstructionIndex(aConstructionIndex),
+ mConstructionTimeStamp(aConstructionTimeStamp),
+ mDestructionIndex(0),
+ mMediaElement(nullptr),
+ mDerivedObjectLinkingIndex(0),
+ mTag(aTag) {}
+
+ // Is this lifetime alive at the given index?
+ // I.e.: Constructed before, and destroyed later or not yet.
+ bool IsAliveAt(DDMessageIndex aIndex) const {
+ return aIndex >= mConstructionIndex &&
+ (!mDestructionTimeStamp || aIndex <= mDestructionIndex);
+ }
+
+ // Print the object's pointer, tag and class name (and derived class). E.g.:
+ // "dom::HTMLVideoElement[134073800]#1 (as dom::HTMLMediaElement)"
+ void AppendPrintf(nsCString& aString) const;
+ nsCString Printf() const;
+};
+
+} // namespace mozilla
+
+#endif // DDLifetime_h_
diff --git a/dom/media/doctor/DDLifetimes.cpp b/dom/media/doctor/DDLifetimes.cpp
new file mode 100644
index 0000000000..ba14e33eac
--- /dev/null
+++ b/dom/media/doctor/DDLifetimes.cpp
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DDLifetimes.h"
+
+#include "DDLogUtils.h"
+
+namespace mozilla {
+
+DDLifetime* DDLifetimes::FindLifetime(const DDLogObject& aObject,
+ const DDMessageIndex& aIndex) {
+ LifetimesForObject* lifetimes = mLifetimes.Get(aObject);
+ if (lifetimes) {
+ for (DDLifetime& lifetime : *lifetimes) {
+ if (lifetime.mObject == aObject && lifetime.IsAliveAt(aIndex)) {
+ return &lifetime;
+ }
+ }
+ }
+ return nullptr;
+}
+
+const DDLifetime* DDLifetimes::FindLifetime(
+ const DDLogObject& aObject, const DDMessageIndex& aIndex) const {
+ const LifetimesForObject* lifetimes = mLifetimes.Get(aObject);
+ if (lifetimes) {
+ for (const DDLifetime& lifetime : *lifetimes) {
+ if (lifetime.mObject == aObject && lifetime.IsAliveAt(aIndex)) {
+ return &lifetime;
+ }
+ }
+ }
+ return nullptr;
+}
+
+DDLifetime& DDLifetimes::CreateLifetime(
+ const DDLogObject& aObject, DDMessageIndex aIndex,
+ const DDTimeStamp& aConstructionTimeStamp) {
+ // Use negative tags for yet-unclassified messages.
+ static int32_t sTag = 0;
+ if (--sTag > 0) {
+ sTag = -1;
+ }
+ LifetimesForObject* lifetimes = mLifetimes.GetOrInsertNew(aObject, 1);
+ DDLifetime& lifetime = *lifetimes->AppendElement(
+ DDLifetime(aObject, aIndex, aConstructionTimeStamp, sTag));
+ return lifetime;
+}
+
+void DDLifetimes::RemoveLifetime(const DDLifetime* aLifetime) {
+ LifetimesForObject* lifetimes = mLifetimes.Get(aLifetime->mObject);
+ MOZ_ASSERT(lifetimes);
+ DDL_LOG(aLifetime->mMediaElement ? mozilla::LogLevel::Debug
+ : mozilla::LogLevel::Warning,
+ "Remove lifetime %s", aLifetime->Printf().get());
+ // We should have been given a pointer inside this lifetimes array.
+ auto arrayIndex = aLifetime - lifetimes->Elements();
+ MOZ_ASSERT(static_cast<size_t>(arrayIndex) < lifetimes->Length());
+ lifetimes->RemoveElementAt(arrayIndex);
+}
+
+void DDLifetimes::RemoveLifetimesFor(
+ const dom::HTMLMediaElement* aMediaElement) {
+ for (const auto& lifetimes : mLifetimes.Values()) {
+ lifetimes->RemoveElementsBy([aMediaElement](const DDLifetime& lifetime) {
+ return lifetime.mMediaElement == aMediaElement;
+ });
+ }
+}
+
+void DDLifetimes::Clear() { mLifetimes.Clear(); }
+
+size_t DDLifetimes::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t size = mLifetimes.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (const auto& lifetimes : mLifetimes.Values()) {
+ size += lifetimes->ShallowSizeOfExcludingThis(aMallocSizeOf);
+ }
+ return size;
+}
+
+} // namespace mozilla
diff --git a/dom/media/doctor/DDLifetimes.h b/dom/media/doctor/DDLifetimes.h
new file mode 100644
index 0000000000..3cfcafc995
--- /dev/null
+++ b/dom/media/doctor/DDLifetimes.h
@@ -0,0 +1,130 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DDLifetimes_h_
+#define DDLifetimes_h_
+
+#include <type_traits>
+
+#include "DDLifetime.h"
+#include "DDLoggedTypeTraits.h"
+#include "nsClassHashtable.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+
+// Managed list of lifetimes.
+class DDLifetimes {
+ public:
+ // DDLifetime for a given aObject, that exists at the aIndex time;
+ // otherwise nullptr.
+ DDLifetime* FindLifetime(const DDLogObject& aObject,
+ const DDMessageIndex& aIndex);
+
+ // DDLifetime for a given aObject, that exists at the aIndex time;
+ // otherwise nullptr.
+ const DDLifetime* FindLifetime(const DDLogObject& aObject,
+ const DDMessageIndex& aIndex) const;
+
+ // Create a lifetime with the given object and construction index&time.
+ DDLifetime& CreateLifetime(const DDLogObject& aObject, DDMessageIndex aIndex,
+ const DDTimeStamp& aConstructionTimeStamp);
+
+ // Remove an existing lifetime (assumed to just have been found by
+ // FindLifetime()).
+ void RemoveLifetime(const DDLifetime* aLifetime);
+
+ // Remove all lifetimes associated with the given HTMLMediaElement.
+ void RemoveLifetimesFor(const dom::HTMLMediaElement* aMediaElement);
+
+ // Remove all lifetimes.
+ void Clear();
+
+ // Visit all lifetimes associated with an HTMLMediaElement and run
+ // `aF(const DDLifetime&)` on each one.
+ // If aOnlyHTMLMediaElement is true, only run aF once of that element.
+ template <typename F>
+ void Visit(const dom::HTMLMediaElement* aMediaElement, F&& aF,
+ bool aOnlyHTMLMediaElement = false) const {
+ for (const auto& lifetimes : mLifetimes.Values()) {
+ for (const DDLifetime& lifetime : *lifetimes) {
+ if (lifetime.mMediaElement == aMediaElement) {
+ if (aOnlyHTMLMediaElement) {
+ if (lifetime.mObject.Pointer() == aMediaElement &&
+ lifetime.mObject.TypeName() ==
+ DDLoggedTypeTraits<dom::HTMLMediaElement>::Name()) {
+ aF(lifetime);
+ break;
+ }
+ continue;
+ }
+ static_assert(std::is_same_v<decltype(aF(lifetime)), void>, "");
+ aF(lifetime);
+ }
+ }
+ }
+ }
+
+ // Visit all lifetimes associated with an HTMLMediaElement and run
+ // `aF(const DDLifetime&)` on each one.
+ // If aF() returns false, the loop continues.
+ // If aF() returns true, the loop stops, and true is returned immediately.
+ // If all aF() calls have returned false, false is returned at the end.
+ template <typename F>
+ bool VisitBreakable(const dom::HTMLMediaElement* aMediaElement,
+ F&& aF) const {
+ for (const auto& lifetimes : mLifetimes.Values()) {
+ for (const DDLifetime& lifetime : *lifetimes) {
+ if (lifetime.mMediaElement == aMediaElement) {
+ static_assert(std::is_same_v<decltype(aF(lifetime)), bool>, "");
+ if (aF(lifetime)) {
+ return true;
+ }
+ }
+ }
+ }
+ return false;
+ }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const;
+
+ private:
+ // Hashtable key to use for each DDLogObject.
+ class DDLogObjectHashKey : public PLDHashEntryHdr {
+ public:
+ typedef const DDLogObject& KeyType;
+ typedef const DDLogObject* KeyTypePointer;
+
+ explicit DDLogObjectHashKey(KeyTypePointer aKey) : mValue(*aKey) {}
+ DDLogObjectHashKey(const DDLogObjectHashKey& aToCopy)
+ : mValue(aToCopy.mValue) {}
+ ~DDLogObjectHashKey() = default;
+
+ KeyType GetKey() const { return mValue; }
+ bool KeyEquals(KeyTypePointer aKey) const { return *aKey == mValue; }
+
+ static KeyTypePointer KeyToPointer(KeyType aKey) { return &aKey; }
+ static PLDHashNumber HashKey(KeyTypePointer aKey) {
+ return HashBytes(aKey, sizeof(DDLogObject));
+ }
+ enum { ALLOW_MEMMOVE = true };
+
+ private:
+ const DDLogObject mValue;
+ };
+
+ // Array of all DDLifetimes for a given DDLogObject; they should be
+ // distinguished by their construction&destruction times.
+ using LifetimesForObject = nsTArray<DDLifetime>;
+
+ // For each DDLogObject, we store an array of all objects that have used this
+ // pointer and type.
+ nsClassHashtable<DDLogObjectHashKey, LifetimesForObject> mLifetimes;
+};
+
+} // namespace mozilla
+
+#endif // DDLifetimes_h_
diff --git a/dom/media/doctor/DDLogCategory.cpp b/dom/media/doctor/DDLogCategory.cpp
new file mode 100644
index 0000000000..76fce1f4ae
--- /dev/null
+++ b/dom/media/doctor/DDLogCategory.cpp
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DDLogCategory.h"
+
+namespace mozilla {
+
+const char* const kDDLogCategoryShortStrings[kDDLogCategoryCount] = {
+ "con", "dcn", "des", "lnk", "ulk", "prp", "evt",
+ "api", "log", "mze", "mzw", "mzi", "mzd", "mzv"};
+const char* const kDDLogCategoryLongStrings[kDDLogCategoryCount] = {
+ "Construction",
+ "Derived Construction",
+ "Destruction",
+ "Link",
+ "Unlink",
+ "Property",
+ "Event",
+ "API",
+ "Log",
+ "MozLog-Error",
+ "MozLog-Warning",
+ "MozLog-Info",
+ "MozLog-Debug",
+ "MozLog-Verbose"};
+
+} // namespace mozilla
diff --git a/dom/media/doctor/DDLogCategory.h b/dom/media/doctor/DDLogCategory.h
new file mode 100644
index 0000000000..2c8494b690
--- /dev/null
+++ b/dom/media/doctor/DDLogCategory.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DDLogCategory_h_
+#define DDLogCategory_h_
+
+#include "mozilla/Assertions.h"
+#include "mozilla/DefineEnum.h"
+
+namespace mozilla {
+
+// Enum used to categorize log messages.
+// Those starting with '_' are for internal use only.
+MOZ_DEFINE_ENUM_CLASS(DDLogCategory,
+ (_Construction, _DerivedConstruction, _Destruction, _Link,
+ _Unlink, Property, Event, API, Log, MozLogError,
+ MozLogWarning, MozLogInfo, MozLogDebug, MozLogVerbose));
+
+// Corresponding short strings, used as JSON property names when logs are
+// retrieved.
+extern const char* const kDDLogCategoryShortStrings[kDDLogCategoryCount];
+
+inline const char* ToShortString(DDLogCategory aCategory) {
+ MOZ_ASSERT(static_cast<size_t>(aCategory) < kDDLogCategoryCount);
+ return kDDLogCategoryShortStrings[static_cast<size_t>(aCategory)];
+}
+
+// Corresponding long strings, for use in descriptive UI.
+extern const char* const kDDLogCategoryLongStrings[kDDLogCategoryCount];
+
+inline const char* ToLongString(DDLogCategory aCategory) {
+ MOZ_ASSERT(static_cast<size_t>(aCategory) < kDDLogCategoryCount);
+ return kDDLogCategoryLongStrings[static_cast<size_t>(aCategory)];
+}
+
+} // namespace mozilla
+
+#endif // DDLogCategory_h_
diff --git a/dom/media/doctor/DDLogMessage.cpp b/dom/media/doctor/DDLogMessage.cpp
new file mode 100644
index 0000000000..488c39216a
--- /dev/null
+++ b/dom/media/doctor/DDLogMessage.cpp
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DDLogMessage.h"
+
+#include "DDLifetimes.h"
+
+namespace mozilla {
+
+nsCString DDLogMessage::Print() const {
+ nsCString str;
+ str.AppendPrintf("%" PRImi " | %f | %s[%p] | %s | %s | ", mIndex.Value(),
+ ToSeconds(mTimeStamp), mObject.TypeName(), mObject.Pointer(),
+ ToShortString(mCategory), mLabel);
+ AppendToString(mValue, str);
+ return str;
+}
+
+nsCString DDLogMessage::Print(const DDLifetimes& aLifetimes) const {
+ nsCString str;
+ const DDLifetime* lifetime = aLifetimes.FindLifetime(mObject, mIndex);
+ str.AppendPrintf("%" PRImi " | %f | ", mIndex.Value(), ToSeconds(mTimeStamp));
+ lifetime->AppendPrintf(str);
+ str.AppendPrintf(" | %s | %s | ", ToShortString(mCategory), mLabel);
+ if (!mValue.is<DDLogObject>()) {
+ AppendToString(mValue, str);
+ } else {
+ const DDLifetime* lifetime2 =
+ aLifetimes.FindLifetime(mValue.as<DDLogObject>(), mIndex);
+ if (lifetime2) {
+ lifetime2->AppendPrintf(str);
+ } else {
+ AppendToString(mValue, str);
+ }
+ }
+ return str;
+}
+
+} // namespace mozilla
diff --git a/dom/media/doctor/DDLogMessage.h b/dom/media/doctor/DDLogMessage.h
new file mode 100644
index 0000000000..5470e01da5
--- /dev/null
+++ b/dom/media/doctor/DDLogMessage.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DDLogMessage_h_
+#define DDLogMessage_h_
+
+#include "DDLogCategory.h"
+#include "DDLogObject.h"
+#include "DDLogValue.h"
+#include "DDMessageIndex.h"
+#include "DDTimeStamp.h"
+#include "mozilla/Atomics.h"
+#include "nsString.h"
+
+namespace mozilla {
+
+class DDLifetimes;
+
+// Structure containing all the information needed in each log message
+// (before and after processing).
+struct DDLogMessage {
+ DDMessageIndex mIndex;
+ DDTimeStamp mTimeStamp;
+ DDLogObject mObject;
+ DDLogCategory mCategory;
+ const char* mLabel;
+ DDLogValue mValue = DDLogValue{DDNoValue{}};
+
+ // Print the message. Format:
+ // "index | timestamp | object | category | label | value". E.g.:
+ // "29 | 5.047547 | dom::HTMLMediaElement[134073800] | lnk | decoder |
+ // MediaDecoder[136078200]"
+ nsCString Print() const;
+
+ // Print the message, using object information from aLifetimes. Format:
+ // "index | timestamp | object | category | label | value". E.g.:
+ // "29 | 5.047547 | dom::HTMLVideoElement[134073800]#1 (as
+ // dom::HTMLMediaElement) | lnk | decoder | MediaSourceDecoder[136078200]#5
+ // (as MediaDecoder)"
+ nsCString Print(const DDLifetimes& aLifetimes) const;
+};
+
+} // namespace mozilla
+
+#endif // DDLogMessage_h_
diff --git a/dom/media/doctor/DDLogObject.cpp b/dom/media/doctor/DDLogObject.cpp
new file mode 100644
index 0000000000..7a8b3341de
--- /dev/null
+++ b/dom/media/doctor/DDLogObject.cpp
@@ -0,0 +1,22 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DDLogObject.h"
+
+namespace mozilla {
+
+void DDLogObject::AppendPrintf(nsCString& mString) const {
+ MOZ_ASSERT(mTypeName);
+ mString.AppendPrintf("%s[%p]", mTypeName, mPointer);
+}
+
+nsCString DDLogObject::Printf() const {
+ nsCString s;
+ AppendPrintf(s);
+ return s;
+}
+
+} // namespace mozilla
diff --git a/dom/media/doctor/DDLogObject.h b/dom/media/doctor/DDLogObject.h
new file mode 100644
index 0000000000..d16d601eb2
--- /dev/null
+++ b/dom/media/doctor/DDLogObject.h
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DDLogObject_h_
+#define DDLogObject_h_
+
+#include "nsString.h"
+
+namespace mozilla {
+
+// DDLogObject identifies a C++ object by its pointer and its class name (as
+// provided in a DDLoggedTypeTrait.)
+// Note that a DDLogObject could have the exact same pointer&type as a previous
+// one, so extra information is needed to distinguish them, see DDLifetime.
+class DDLogObject {
+ public:
+ // Default-initialization with null pointer.
+ DDLogObject() : mTypeName("<unset>"), mPointer(nullptr) {}
+
+ // Construction with given non-null type name and pointer.
+ DDLogObject(const char* aTypeName, const void* aPointer)
+ : mTypeName(aTypeName), mPointer(aPointer) {
+ MOZ_ASSERT(aTypeName);
+ MOZ_ASSERT(aPointer);
+ }
+
+ // Sets this DDLogObject to an actual object.
+ void Set(const char* aTypeName, const void* aPointer) {
+ MOZ_ASSERT(aTypeName);
+ MOZ_ASSERT(aPointer);
+ mTypeName = aTypeName;
+ mPointer = aPointer;
+ }
+
+ // Object pointer, used for identification purposes only.
+ const void* Pointer() const { return mPointer; }
+
+ // Type name. Should only be accessed after non-null pointer initialization.
+ const char* TypeName() const {
+ MOZ_ASSERT(mPointer);
+ return mTypeName;
+ }
+
+ bool operator==(const DDLogObject& a) const {
+ return mPointer == a.mPointer && (!mPointer || mTypeName == a.mTypeName);
+ }
+
+ // Print the type name and pointer, e.g.: "MediaDecoder[136078200]".
+ void AppendPrintf(nsCString& mString) const;
+ nsCString Printf() const;
+
+ private:
+ const char* mTypeName;
+ const void* mPointer;
+};
+
+} // namespace mozilla
+
+#endif // DDLogObject_h_
diff --git a/dom/media/doctor/DDLogUtils.cpp b/dom/media/doctor/DDLogUtils.cpp
new file mode 100644
index 0000000000..50f0b676d0
--- /dev/null
+++ b/dom/media/doctor/DDLogUtils.cpp
@@ -0,0 +1,11 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DDLogUtils.h"
+
+mozilla::LazyLogModule sDecoderDoctorLoggerLog("DDLogger");
+
+mozilla::LazyLogModule sDecoderDoctorLoggerEndLog("DDLoggerEnd");
diff --git a/dom/media/doctor/DDLogUtils.h b/dom/media/doctor/DDLogUtils.h
new file mode 100644
index 0000000000..256aed1e3a
--- /dev/null
+++ b/dom/media/doctor/DDLogUtils.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DDLogUtils_h_
+#define DDLogUtils_h_
+
+#include "mozilla/Logging.h"
+
+// Logging for the DecoderDoctorLoggger code.
+extern mozilla::LazyLogModule sDecoderDoctorLoggerLog;
+#define DDL_LOG(level, arg, ...) \
+ MOZ_LOG(sDecoderDoctorLoggerLog, level, (arg, ##__VA_ARGS__))
+#define DDL_DEBUG(arg, ...) \
+ DDL_LOG(mozilla::LogLevel::Debug, arg, ##__VA_ARGS__)
+#define DDL_INFO(arg, ...) DDL_LOG(mozilla::LogLevel::Info, arg, ##__VA_ARGS__)
+#define DDL_WARN(arg, ...) \
+ DDL_LOG(mozilla::LogLevel::Warning, arg, ##__VA_ARGS__)
+
+// Output at shutdown the log given to DecoderDoctorLogger.
+extern mozilla::LazyLogModule sDecoderDoctorLoggerEndLog;
+#define DDLE_LOG(level, arg, ...) \
+ MOZ_LOG(sDecoderDoctorLoggerEndLog, level, (arg, ##__VA_ARGS__))
+#define DDLE_DEBUG(arg, ...) \
+ DDLE_LOG(mozilla::LogLevel::Debug, arg, ##__VA_ARGS__)
+#define DDLE_INFO(arg, ...) \
+ DDLE_LOG(mozilla::LogLevel::Info, arg, ##__VA_ARGS__)
+#define DDLE_WARN(arg, ...) \
+ DDLE_LOG(mozilla::LogLevel::Warning, arg, ##__VA_ARGS__)
+
+#endif // DDLogUtils_h_
diff --git a/dom/media/doctor/DDLogValue.cpp b/dom/media/doctor/DDLogValue.cpp
new file mode 100644
index 0000000000..74044ca05a
--- /dev/null
+++ b/dom/media/doctor/DDLogValue.cpp
@@ -0,0 +1,120 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DDLogValue.h"
+
+#include "mozilla/JSONWriter.h"
+
+namespace mozilla {
+
+struct LogValueMatcher {
+ nsCString& mString;
+
+ void operator()(const DDNoValue&) const {}
+ void operator()(const DDLogObject& a) const { a.AppendPrintf(mString); }
+ void operator()(const char* a) const { mString.AppendPrintf(R"("%s")", a); }
+ void operator()(const nsCString& a) const {
+ mString.AppendPrintf(R"(nsCString("%s"))", a.Data());
+ }
+ void operator()(bool a) const { mString.AppendPrintf(a ? "true" : "false"); }
+ void operator()(int8_t a) const {
+ mString.AppendPrintf("int8_t(%" PRIi8 ")", a);
+ }
+ void operator()(uint8_t a) const {
+ mString.AppendPrintf("uint8_t(%" PRIu8 ")", a);
+ }
+ void operator()(int16_t a) const {
+ mString.AppendPrintf("int16_t(%" PRIi16 ")", a);
+ }
+ void operator()(uint16_t a) const {
+ mString.AppendPrintf("uint16_t(%" PRIu16 ")", a);
+ }
+ void operator()(int32_t a) const {
+ mString.AppendPrintf("int32_t(%" PRIi32 ")", a);
+ }
+ void operator()(uint32_t a) const {
+ mString.AppendPrintf("uint32_t(%" PRIu32 ")", a);
+ }
+ void operator()(int64_t a) const {
+ mString.AppendPrintf("int64_t(%" PRIi64 ")", a);
+ }
+ void operator()(uint64_t a) const {
+ mString.AppendPrintf("uint64_t(%" PRIu64 ")", a);
+ }
+ void operator()(double a) const { mString.AppendPrintf("double(%f)", a); }
+ void operator()(const DDRange& a) const {
+ mString.AppendPrintf("%" PRIi64 "<=(%" PRIi64 "B)<%" PRIi64 "", a.mOffset,
+ a.mBytes, a.mOffset + a.mBytes);
+ }
+ void operator()(const nsresult& a) const {
+ nsCString name;
+ GetErrorName(a, name);
+ mString.AppendPrintf("nsresult(%s =0x%08" PRIx32 ")", name.get(),
+ static_cast<uint32_t>(a));
+ }
+ void operator()(const MediaResult& a) const {
+ nsCString name;
+ GetErrorName(a.Code(), name);
+ mString.AppendPrintf("MediaResult(%s =0x%08" PRIx32 ", \"%s\")", name.get(),
+ static_cast<uint32_t>(a.Code()), a.Message().get());
+ }
+};
+
+void AppendToString(const DDLogValue& aValue, nsCString& aString) {
+ aValue.match(LogValueMatcher{aString});
+}
+
+struct LogValueMatcherJson {
+ JSONWriter& mJW;
+ const Span<const char> mPropertyName;
+
+ void operator()(const DDNoValue&) const { mJW.NullProperty(mPropertyName); }
+ void operator()(const DDLogObject& a) const {
+ nsPrintfCString s(R"("%s[%p]")", a.TypeName(), a.Pointer());
+ mJW.StringProperty(mPropertyName, s);
+ }
+ void operator()(const char* a) const {
+ mJW.StringProperty(mPropertyName, MakeStringSpan(a));
+ }
+ void operator()(const nsCString& a) const {
+ mJW.StringProperty(mPropertyName, a);
+ }
+ void operator()(bool a) const { mJW.BoolProperty(mPropertyName, a); }
+ void operator()(int8_t a) const { mJW.IntProperty(mPropertyName, a); }
+ void operator()(uint8_t a) const { mJW.IntProperty(mPropertyName, a); }
+ void operator()(int16_t a) const { mJW.IntProperty(mPropertyName, a); }
+ void operator()(uint16_t a) const { mJW.IntProperty(mPropertyName, a); }
+ void operator()(int32_t a) const { mJW.IntProperty(mPropertyName, a); }
+ void operator()(uint32_t a) const { mJW.IntProperty(mPropertyName, a); }
+ void operator()(int64_t a) const { mJW.IntProperty(mPropertyName, a); }
+ void operator()(uint64_t a) const { mJW.DoubleProperty(mPropertyName, a); }
+ void operator()(double a) const { mJW.DoubleProperty(mPropertyName, a); }
+ void operator()(const DDRange& a) const {
+ mJW.StartArrayProperty(mPropertyName);
+ mJW.IntElement(a.mOffset);
+ mJW.IntElement(a.mOffset + a.mBytes);
+ mJW.EndArray();
+ }
+ void operator()(const nsresult& a) const {
+ nsCString name;
+ GetErrorName(a, name);
+ mJW.StringProperty(mPropertyName, name);
+ }
+ void operator()(const MediaResult& a) const {
+ nsCString name;
+ GetErrorName(a.Code(), name);
+ mJW.StringProperty(mPropertyName,
+ nsPrintfCString(R"lit("MediaResult(%s, %s)")lit",
+ name.get(), a.Message().get()));
+ }
+};
+
+void ToJSON(const DDLogValue& aValue, JSONWriter& aJSONWriter,
+ const char* aPropertyName) {
+ aValue.match(LogValueMatcherJson{aJSONWriter, MakeStringSpan(aPropertyName)});
+}
+
+} // namespace mozilla
diff --git a/dom/media/doctor/DDLogValue.h b/dom/media/doctor/DDLogValue.h
new file mode 100644
index 0000000000..9a8253916f
--- /dev/null
+++ b/dom/media/doctor/DDLogValue.h
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DDLogValue_h_
+#define DDLogValue_h_
+
+#include "DDLogObject.h"
+#include "MediaResult.h"
+#include "mozilla/Variant.h"
+#include "nsString.h"
+
+namespace mozilla {
+
+// Placeholder when no value is needed.
+struct DDNoValue {};
+
+// Value capturing a range (typically an offset and a number of bytes in a
+// resource).
+struct DDRange {
+ const int64_t mOffset;
+ const int64_t mBytes;
+ DDRange(int64_t aOffset, int64_t aBytes) : mOffset(aOffset), mBytes(aBytes) {}
+};
+
+// Value associated with a log message.
+using DDLogValue = Variant<DDNoValue, DDLogObject,
+ const char*, // Assumed to be a literal string.
+ const nsCString, bool, int8_t, uint8_t, int16_t,
+ uint16_t, int32_t, uint32_t, int64_t, uint64_t,
+ double, DDRange, nsresult, MediaResult>;
+
+void AppendToString(const DDLogValue& aValue, nsCString& aString);
+
+class JSONWriter;
+void ToJSON(const DDLogValue& aValue, JSONWriter& aJSONWriter,
+ const char* aPropertyName);
+
+} // namespace mozilla
+
+#endif // DDLogValue_h_
diff --git a/dom/media/doctor/DDLoggedTypeTraits.h b/dom/media/doctor/DDLoggedTypeTraits.h
new file mode 100644
index 0000000000..2c9de4dbc5
--- /dev/null
+++ b/dom/media/doctor/DDLoggedTypeTraits.h
@@ -0,0 +1,104 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DDLoggedTypeTraits_h_
+#define DDLoggedTypeTraits_h_
+
+#include <type_traits>
+
+namespace mozilla {
+
+// Templated struct carrying compile-time information about C++ types that may
+// log messages (including about their lifetime and links to other objects.)
+// Classes should declare a specialization by using one of the macros below.
+template <typename T>
+struct DDLoggedTypeTraits;
+
+#define DDLoggedTypeName(TYPE) \
+ template <> \
+ struct DDLoggedTypeTraits<TYPE> { \
+ using Type = TYPE; \
+ static constexpr const char* Name() { return #TYPE; } \
+ using HasBase = std::false_type; \
+ using BaseType = TYPE; \
+ static constexpr const char* BaseTypeName() { return ""; } \
+ }
+
+#define DDLoggedTypeNameAndBase(TYPE, BASE) \
+ template <> \
+ struct DDLoggedTypeTraits<TYPE> { \
+ using Type = TYPE; \
+ static constexpr const char* Name() { return #TYPE; } \
+ using HasBase = std::true_type; \
+ using BaseType = BASE; \
+ static constexpr const char* BaseTypeName() { \
+ return DDLoggedTypeTraits<BASE>::Name(); \
+ } \
+ }
+
+#define DDLoggedTypeCustomName(TYPE, NAME) \
+ template <> \
+ struct DDLoggedTypeTraits<TYPE> { \
+ using Type = TYPE; \
+ static constexpr const char* Name() { return #NAME; } \
+ using HasBase = std::false_type; \
+ using BaseType = TYPE; \
+ static constexpr const char* BaseTypeName() { return ""; } \
+ }
+
+#define DDLoggedTypeCustomNameAndBase(TYPE, NAME, BASE) \
+ template <> \
+ struct DDLoggedTypeTraits<TYPE> { \
+ using Type = TYPE; \
+ static constexpr const char* Name() { return #NAME; } \
+ using HasBase = std::true_type; \
+ using BaseType = BASE; \
+ static constexpr const char* BaseTypeName() { \
+ return DDLoggedTypeTraits<BASE>::Name(); \
+ } \
+ }
+
+// Variants with built-in forward-declaration, will only work
+// - in mozilla namespace,
+// - with unqualified mozilla-namespace type names.
+#define DDLoggedTypeDeclName(TYPE) \
+ class TYPE; \
+ DDLoggedTypeName(TYPE);
+#define DDLoggedTypeDeclNameAndBase(TYPE, BASE) \
+ class TYPE; \
+ DDLoggedTypeNameAndBase(TYPE, BASE);
+#define DDLoggedTypeDeclCustomName(TYPE, NAME) \
+ class TYPE; \
+ DDLoggedTypeCustomName(TYPE, NAME);
+#define DDLoggedTypeDeclCustomNameAndBase(TYPE, NAME, BASE) \
+ class TYPE; \
+ DDLoggedTypeCustomNameAndBase(TYPE, NAME, BASE);
+
+} // namespace mozilla
+
+// Some essential types that live outside of the media stack.
+class nsPIDOMWindowInner;
+class nsPIDOMWindowOuter;
+
+namespace mozilla {
+
+namespace dom {
+class Document;
+class HTMLAudioElement;
+class HTMLMediaElement;
+class HTMLVideoElement;
+} // namespace dom
+
+DDLoggedTypeName(nsPIDOMWindowInner);
+DDLoggedTypeName(nsPIDOMWindowOuter);
+DDLoggedTypeName(dom::Document);
+DDLoggedTypeName(dom::HTMLMediaElement);
+DDLoggedTypeNameAndBase(dom::HTMLAudioElement, dom::HTMLMediaElement);
+DDLoggedTypeNameAndBase(dom::HTMLVideoElement, dom::HTMLMediaElement);
+
+} // namespace mozilla
+
+#endif // DDLoggedTypeTraits_h_
diff --git a/dom/media/doctor/DDMediaLog.cpp b/dom/media/doctor/DDMediaLog.cpp
new file mode 100644
index 0000000000..786d1096ff
--- /dev/null
+++ b/dom/media/doctor/DDMediaLog.cpp
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DDMediaLog.h"
+
+namespace mozilla {
+
+size_t DDMediaLog::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t size = mMessages.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (const DDLogMessage& message : mMessages) {
+ if (message.mValue.is<const nsCString>()) {
+ size +=
+ message.mValue.as<const nsCString>().SizeOfExcludingThisIfUnshared(
+ aMallocSizeOf);
+ } else if (message.mValue.is<MediaResult>()) {
+ size += message.mValue.as<MediaResult>()
+ .Message()
+ .SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ }
+ }
+ return size;
+}
+
+} // namespace mozilla
diff --git a/dom/media/doctor/DDMediaLog.h b/dom/media/doctor/DDMediaLog.h
new file mode 100644
index 0000000000..78037d1975
--- /dev/null
+++ b/dom/media/doctor/DDMediaLog.h
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DDMediaLog_h_
+#define DDMediaLog_h_
+
+#include "DDLogMessage.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+
+namespace dom {
+class HTMLMediaElement;
+} // namespace dom
+
+class DDLifetimes;
+
+// Container of processed messages corresponding to an HTMLMediaElement (or
+// not yet).
+struct DDMediaLog {
+ // Associated HTMLMediaElement, or nullptr for the DDMediaLog containing
+ // messages for yet-unassociated objects.
+ // TODO: Should use a DDLogObject instead, to distinguish between elements
+ // at the same address.
+ // Not critical: At worst we will combine logs for two elements.
+ const dom::HTMLMediaElement* mMediaElement;
+
+ // Number of lifetimes associated with this log. Managed by DDMediaLogs.
+ int32_t mLifetimeCount = 0;
+
+ using LogMessages = nsTArray<DDLogMessage>;
+ LogMessages mMessages;
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const;
+};
+
+} // namespace mozilla
+
+#endif // DDMediaLog_h_
diff --git a/dom/media/doctor/DDMediaLogs.cpp b/dom/media/doctor/DDMediaLogs.cpp
new file mode 100644
index 0000000000..442e03d3fa
--- /dev/null
+++ b/dom/media/doctor/DDMediaLogs.cpp
@@ -0,0 +1,667 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DDMediaLogs.h"
+
+#include "DDLogUtils.h"
+#include "nsIThread.h"
+#include "nsIThreadManager.h"
+#include "mozilla/JSONStringWriteFuncs.h"
+
+namespace mozilla {
+
+/* static */ DDMediaLogs::ConstructionResult DDMediaLogs::New() {
+ nsCOMPtr<nsIThread> mThread;
+ nsresult rv =
+ NS_NewNamedThread("DDMediaLogs", getter_AddRefs(mThread), nullptr,
+ {.stackSize = nsIThreadManager::kThreadPoolStackSize});
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return {rv, nullptr};
+ }
+
+ return {rv, new DDMediaLogs(std::move(mThread))};
+}
+
+DDMediaLogs::DDMediaLogs(nsCOMPtr<nsIThread>&& aThread)
+ : mMediaLogs(1), mMutex("DDMediaLogs"), mThread(std::move(aThread)) {
+ mMediaLogs.SetLength(1);
+ mMediaLogs[0].mMediaElement = nullptr;
+ DDL_INFO("DDMediaLogs constructed, processing thread: %p", mThread.get());
+}
+
+DDMediaLogs::~DDMediaLogs() {
+ // Perform end-of-life processing, ensure the processing thread is shutdown.
+ Shutdown(/* aPanic = */ false);
+}
+
+void DDMediaLogs::Panic() { Shutdown(/* aPanic = */ true); }
+
+void DDMediaLogs::Shutdown(bool aPanic) {
+ nsCOMPtr<nsIThread> thread;
+ {
+ MutexAutoLock lock(mMutex);
+ thread.swap(mThread);
+ }
+ if (!thread) {
+ // Already shutdown, nothing more to do.
+ return;
+ }
+
+ DDL_INFO("DDMediaLogs::Shutdown will shutdown thread: %p", thread.get());
+ // Will block until pending tasks have completed, and thread is dead.
+ thread->Shutdown();
+
+ if (aPanic) {
+ mMessagesQueue.PopAll([](const DDLogMessage&) {});
+ MutexAutoLock lock(mMutex);
+ mLifetimes.Clear();
+ mMediaLogs.Clear();
+ mObjectLinks.Clear();
+ mPendingPromises.Clear();
+ return;
+ }
+
+ // Final processing is only necessary to output to MOZ_LOG=DDLoggerEnd,
+ // so there's no point doing any of it if that MOZ_LOG is not enabled.
+ if (MOZ_LOG_TEST(sDecoderDoctorLoggerEndLog, mozilla::LogLevel::Info)) {
+ DDL_DEBUG("Perform final DDMediaLogs processing...");
+ // The processing thread is dead, so we can safely call ProcessLog()
+ // directly from this thread.
+ ProcessLog();
+
+ for (const DDMediaLog& mediaLog : mMediaLogs) {
+ if (mediaLog.mMediaElement) {
+ DDLE_INFO("---");
+ }
+ DDLE_INFO("--- Log for HTMLMediaElement[%p] ---", mediaLog.mMediaElement);
+ for (const DDLogMessage& message : mediaLog.mMessages) {
+ DDLE_LOG(message.mCategory <= DDLogCategory::_Unlink
+ ? mozilla::LogLevel::Debug
+ : mozilla::LogLevel::Info,
+ "%s", message.Print(mLifetimes).get());
+ }
+ DDLE_DEBUG("--- End log for HTMLMediaElement[%p] ---",
+ mediaLog.mMediaElement);
+ }
+ }
+}
+
+DDMediaLog& DDMediaLogs::LogForUnassociatedMessages() {
+ MOZ_ASSERT(!mThread || mThread.get() == NS_GetCurrentThread());
+ return mMediaLogs[0];
+}
+const DDMediaLog& DDMediaLogs::LogForUnassociatedMessages() const {
+ MOZ_ASSERT(!mThread || mThread.get() == NS_GetCurrentThread());
+ return mMediaLogs[0];
+}
+
+DDMediaLog* DDMediaLogs::GetLogFor(const dom::HTMLMediaElement* aMediaElement) {
+ MOZ_ASSERT(!mThread || mThread.get() == NS_GetCurrentThread());
+ if (!aMediaElement) {
+ return &LogForUnassociatedMessages();
+ }
+ for (DDMediaLog& log : mMediaLogs) {
+ if (log.mMediaElement == aMediaElement) {
+ return &log;
+ }
+ }
+ return nullptr;
+}
+
+DDMediaLog& DDMediaLogs::LogFor(const dom::HTMLMediaElement* aMediaElement) {
+ MOZ_ASSERT(!mThread || mThread.get() == NS_GetCurrentThread());
+ DDMediaLog* log = GetLogFor(aMediaElement);
+ if (!log) {
+ log = mMediaLogs.AppendElement();
+ log->mMediaElement = aMediaElement;
+ }
+ return *log;
+}
+
+void DDMediaLogs::SetMediaElement(DDLifetime& aLifetime,
+ const dom::HTMLMediaElement* aMediaElement) {
+ MOZ_ASSERT(!mThread || mThread.get() == NS_GetCurrentThread());
+ DDMediaLog& log = LogFor(aMediaElement);
+
+ // List of lifetimes that are to be linked to aMediaElement.
+ nsTArray<DDLifetime*> lifetimes;
+ // We start with the given lifetime.
+ lifetimes.AppendElement(&aLifetime);
+ for (size_t i = 0; i < lifetimes.Length(); ++i) {
+ DDLifetime& lifetime = *lifetimes[i];
+ // Link the lifetime to aMediaElement.
+ lifetime.mMediaElement = aMediaElement;
+ // Classified lifetime's tag is a positive index from the DDMediaLog.
+ lifetime.mTag = ++log.mLifetimeCount;
+ DDL_DEBUG("%s -> HTMLMediaElement[%p]", lifetime.Printf().get(),
+ aMediaElement);
+
+ // Go through the lifetime's existing linked lifetimes, if any is not
+ // already linked to aMediaElement, add it to the list so it will get
+ // linked in a later loop.
+ for (auto& link : mObjectLinks) {
+ if (lifetime.IsAliveAt(link.mLinkingIndex)) {
+ if (lifetime.mObject == link.mParent) {
+ DDLifetime* childLifetime =
+ mLifetimes.FindLifetime(link.mChild, link.mLinkingIndex);
+ if (childLifetime && !childLifetime->mMediaElement &&
+ !lifetimes.Contains(childLifetime)) {
+ lifetimes.AppendElement(childLifetime);
+ }
+ } else if (lifetime.mObject == link.mChild) {
+ DDLifetime* parentLifetime =
+ mLifetimes.FindLifetime(link.mParent, link.mLinkingIndex);
+ if (parentLifetime && !parentLifetime->mMediaElement &&
+ !lifetimes.Contains(parentLifetime)) {
+ lifetimes.AppendElement(parentLifetime);
+ }
+ }
+ }
+ }
+ }
+
+ // Now we need to move yet-unclassified messages related to the just-set
+ // elements, to the appropriate MediaElement list.
+ DDMediaLog::LogMessages& messages = log.mMessages;
+ DDMediaLog::LogMessages& messages0 = LogForUnassociatedMessages().mMessages;
+ for (size_t i = 0; i < messages0.Length();
+ /* increment inside the loop */) {
+ DDLogMessage& message = messages0[i];
+ bool found = false;
+ for (const DDLifetime* lifetime : lifetimes) {
+ if (lifetime->mObject == message.mObject) {
+ found = true;
+ break;
+ }
+ }
+ if (found) {
+ messages.AppendElement(std::move(message));
+ messages0.RemoveElementAt(i);
+ // No increment, as we've removed this element; next element is now at
+ // the same index.
+ } else {
+ // Not touching this element, increment index to go to next element.
+ ++i;
+ }
+ }
+}
+
+DDLifetime& DDMediaLogs::FindOrCreateLifetime(const DDLogObject& aObject,
+ DDMessageIndex aIndex,
+ const DDTimeStamp& aTimeStamp) {
+ MOZ_ASSERT(!mThread || mThread.get() == NS_GetCurrentThread());
+ // Try to find lifetime corresponding to message object.
+ DDLifetime* lifetime = mLifetimes.FindLifetime(aObject, aIndex);
+ if (!lifetime) {
+ // No lifetime yet, create one.
+ lifetime = &(mLifetimes.CreateLifetime(aObject, aIndex, aTimeStamp));
+ if (MOZ_UNLIKELY(aObject.TypeName() ==
+ DDLoggedTypeTraits<dom::HTMLMediaElement>::Name())) {
+ const dom::HTMLMediaElement* mediaElement =
+ static_cast<const dom::HTMLMediaElement*>(aObject.Pointer());
+ SetMediaElement(*lifetime, mediaElement);
+ DDL_DEBUG("%s -> new lifetime: %s with MediaElement %p",
+ aObject.Printf().get(), lifetime->Printf().get(), mediaElement);
+ } else {
+ DDL_DEBUG("%s -> new lifetime: %s", aObject.Printf().get(),
+ lifetime->Printf().get());
+ }
+ }
+
+ return *lifetime;
+}
+
+void DDMediaLogs::LinkLifetimes(DDLifetime& aParentLifetime,
+ const char* aLinkName,
+ DDLifetime& aChildLifetime,
+ DDMessageIndex aIndex) {
+ MOZ_ASSERT(!mThread || mThread.get() == NS_GetCurrentThread());
+ mObjectLinks.AppendElement(DDObjectLink{
+ aParentLifetime.mObject, aChildLifetime.mObject, aLinkName, aIndex});
+ if (aParentLifetime.mMediaElement) {
+ if (!aChildLifetime.mMediaElement) {
+ SetMediaElement(aChildLifetime, aParentLifetime.mMediaElement);
+ }
+ } else if (aChildLifetime.mMediaElement) {
+ if (!aParentLifetime.mMediaElement) {
+ SetMediaElement(aParentLifetime, aChildLifetime.mMediaElement);
+ }
+ }
+}
+
+void DDMediaLogs::UnlinkLifetime(DDLifetime& aLifetime, DDMessageIndex aIndex) {
+ MOZ_ASSERT(!mThread || mThread.get() == NS_GetCurrentThread());
+ for (DDObjectLink& link : mObjectLinks) {
+ if ((link.mParent == aLifetime.mObject ||
+ link.mChild == aLifetime.mObject) &&
+ aLifetime.IsAliveAt(link.mLinkingIndex) && !link.mUnlinkingIndex) {
+ link.mUnlinkingIndex = Some(aIndex);
+ }
+ }
+};
+
+void DDMediaLogs::UnlinkLifetimes(DDLifetime& aParentLifetime,
+ DDLifetime& aChildLifetime,
+ DDMessageIndex aIndex) {
+ MOZ_ASSERT(!mThread || mThread.get() == NS_GetCurrentThread());
+ for (DDObjectLink& link : mObjectLinks) {
+ if ((link.mParent == aParentLifetime.mObject &&
+ link.mChild == aChildLifetime.mObject) &&
+ aParentLifetime.IsAliveAt(link.mLinkingIndex) &&
+ aChildLifetime.IsAliveAt(link.mLinkingIndex) && !link.mUnlinkingIndex) {
+ link.mUnlinkingIndex = Some(aIndex);
+ }
+ }
+}
+
+void DDMediaLogs::DestroyLifetimeLinks(const DDLifetime& aLifetime) {
+ mObjectLinks.RemoveElementsBy([&](DDObjectLink& link) {
+ return (link.mParent == aLifetime.mObject ||
+ link.mChild == aLifetime.mObject) &&
+ aLifetime.IsAliveAt(link.mLinkingIndex);
+ });
+}
+
+size_t DDMediaLogs::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t size = aMallocSizeOf(this) +
+ // This will usually be called after processing, so negligible
+ // external data should still be present in the queue.
+ mMessagesQueue.ShallowSizeOfExcludingThis(aMallocSizeOf) +
+ mLifetimes.SizeOfExcludingThis(aMallocSizeOf) +
+ mMediaLogs.ShallowSizeOfExcludingThis(aMallocSizeOf) +
+ mObjectLinks.ShallowSizeOfExcludingThis(aMallocSizeOf) +
+ mPendingPromises.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (const DDMediaLog& log : mMediaLogs) {
+ size += log.SizeOfExcludingThis(aMallocSizeOf);
+ }
+ return size;
+}
+
+void DDMediaLogs::ProcessBuffer() {
+ MOZ_ASSERT(!mThread || mThread.get() == NS_GetCurrentThread());
+
+ mMessagesQueue.PopAll([this](const DDLogMessage& message) {
+ DDL_DEBUG("Processing: %s", message.Print().Data());
+
+ // Either this message will carry a new object for which to create a
+ // lifetime, or we'll find an existing one.
+ DDLifetime& lifetime = FindOrCreateLifetime(message.mObject, message.mIndex,
+ message.mTimeStamp);
+
+ // Copy the message contents (without the mValid flag) to the
+ // appropriate MediaLog corresponding to the message's object lifetime.
+ LogFor(lifetime.mMediaElement)
+ .mMessages.AppendElement(static_cast<const DDLogMessage&>(message));
+
+ switch (message.mCategory) {
+ case DDLogCategory::_Construction:
+ // The FindOrCreateLifetime above will have set a construction time,
+ // so there's nothing more we need to do here.
+ MOZ_ASSERT(lifetime.mConstructionTimeStamp);
+ break;
+
+ case DDLogCategory::_DerivedConstruction:
+ // The FindOrCreateLifetime above will have set a construction time.
+ MOZ_ASSERT(lifetime.mConstructionTimeStamp);
+ // A derived construction must come with the base object.
+ MOZ_ASSERT(message.mValue.is<DDLogObject>());
+ {
+ const DDLogObject& base = message.mValue.as<DDLogObject>();
+ DDLifetime& baseLifetime =
+ FindOrCreateLifetime(base, message.mIndex, message.mTimeStamp);
+ // FindOrCreateLifetime could have moved `lifetime`.
+ DDLifetime* lifetime2 =
+ mLifetimes.FindLifetime(message.mObject, message.mIndex);
+ MOZ_ASSERT(lifetime2);
+ // Assume there's no multiple-inheritance (at least for the types
+ // we're watching.)
+ if (baseLifetime.mDerivedObject.Pointer()) {
+ DDL_WARN(
+ "base '%s' was already derived as '%s', now deriving as '%s'",
+ baseLifetime.Printf().get(),
+ baseLifetime.mDerivedObject.Printf().get(),
+ lifetime2->Printf().get());
+ }
+ baseLifetime.mDerivedObject = lifetime2->mObject;
+ baseLifetime.mDerivedObjectLinkingIndex = message.mIndex;
+ // Link the base and derived objects, to ensure they go to the same
+ // log.
+ LinkLifetimes(*lifetime2, "is-a", baseLifetime, message.mIndex);
+ }
+ break;
+
+ case DDLogCategory::_Destruction:
+ lifetime.mDestructionIndex = message.mIndex;
+ lifetime.mDestructionTimeStamp = message.mTimeStamp;
+ UnlinkLifetime(lifetime, message.mIndex);
+ break;
+
+ case DDLogCategory::_Link:
+ MOZ_ASSERT(message.mValue.is<DDLogObject>());
+ {
+ const DDLogObject& child = message.mValue.as<DDLogObject>();
+ DDLifetime& childLifetime =
+ FindOrCreateLifetime(child, message.mIndex, message.mTimeStamp);
+ // FindOrCreateLifetime could have moved `lifetime`.
+ DDLifetime* lifetime2 =
+ mLifetimes.FindLifetime(message.mObject, message.mIndex);
+ MOZ_ASSERT(lifetime2);
+ LinkLifetimes(*lifetime2, message.mLabel, childLifetime,
+ message.mIndex);
+ }
+ break;
+
+ case DDLogCategory::_Unlink:
+ MOZ_ASSERT(message.mValue.is<DDLogObject>());
+ {
+ const DDLogObject& child = message.mValue.as<DDLogObject>();
+ DDLifetime& childLifetime =
+ FindOrCreateLifetime(child, message.mIndex, message.mTimeStamp);
+ // FindOrCreateLifetime could have moved `lifetime`.
+ DDLifetime* lifetime2 =
+ mLifetimes.FindLifetime(message.mObject, message.mIndex);
+ MOZ_ASSERT(lifetime2);
+ UnlinkLifetimes(*lifetime2, childLifetime, message.mIndex);
+ }
+ break;
+
+ default:
+ // Anything else: Nothing more to do.
+ break;
+ }
+ });
+}
+
+void DDMediaLogs::FulfillPromises() {
+ MOZ_ASSERT(!mThread || mThread.get() == NS_GetCurrentThread());
+
+ MozPromiseHolder<LogMessagesPromise> promiseHolder;
+ const dom::HTMLMediaElement* mediaElement = nullptr;
+ {
+ // Grab the first pending promise (if any).
+ // Note that we don't pop it yet, so we don't potentially leave the list
+ // empty and therefore allow another processing task to be dispatched.
+ MutexAutoLock lock(mMutex);
+ if (mPendingPromises.IsEmpty()) {
+ return;
+ }
+ promiseHolder = std::move(mPendingPromises[0].mPromiseHolder);
+ mediaElement = mPendingPromises[0].mMediaElement;
+ }
+ for (;;) {
+ DDMediaLog* log = GetLogFor(mediaElement);
+ if (!log) {
+ // No such media element -> Reject this promise.
+ DDL_INFO("Rejecting promise for HTMLMediaElement[%p] - Cannot find log",
+ mediaElement);
+ promiseHolder.Reject(NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR, __func__);
+ // Pop this rejected promise, fetch next one.
+ MutexAutoLock lock(mMutex);
+ mPendingPromises.RemoveElementAt(0);
+ if (mPendingPromises.IsEmpty()) {
+ break;
+ }
+ promiseHolder = std::move(mPendingPromises[0].mPromiseHolder);
+ mediaElement = mPendingPromises[0].mMediaElement;
+ continue;
+ }
+
+ JSONStringWriteFunc<nsCString> json;
+ JSONWriter jw{json};
+ jw.Start();
+ jw.StartArrayProperty("messages");
+ for (const DDLogMessage& message : log->mMessages) {
+ jw.StartObjectElement(JSONWriter::SingleLineStyle);
+ jw.IntProperty("i", message.mIndex.Value());
+ jw.DoubleProperty("ts", ToSeconds(message.mTimeStamp));
+ DDLifetime* lifetime =
+ mLifetimes.FindLifetime(message.mObject, message.mIndex);
+ if (lifetime) {
+ jw.IntProperty("ob", lifetime->mTag);
+ } else {
+ jw.StringProperty(
+ "ob", nsPrintfCString(R"("%s[%p]")", message.mObject.TypeName(),
+ message.mObject.Pointer()));
+ }
+ jw.StringProperty("cat",
+ MakeStringSpan(ToShortString(message.mCategory)));
+ if (message.mLabel && message.mLabel[0] != '\0') {
+ jw.StringProperty("lbl", MakeStringSpan(message.mLabel));
+ }
+ if (!message.mValue.is<DDNoValue>()) {
+ if (message.mValue.is<DDLogObject>()) {
+ const DDLogObject& ob2 = message.mValue.as<DDLogObject>();
+ DDLifetime* lifetime2 = mLifetimes.FindLifetime(ob2, message.mIndex);
+ if (lifetime2) {
+ jw.IntProperty("val", lifetime2->mTag);
+ } else {
+ ToJSON(message.mValue, jw, "val");
+ }
+ } else {
+ ToJSON(message.mValue, jw, "val");
+ }
+ }
+ jw.EndObject();
+ }
+ jw.EndArray();
+ jw.StartObjectProperty("objects");
+ mLifetimes.Visit(
+ mediaElement,
+ [&](const DDLifetime& lifetime) {
+ jw.StartObjectProperty(nsPrintfCString("%" PRIi32, lifetime.mTag),
+ JSONWriter::SingleLineStyle);
+ jw.IntProperty("tag", lifetime.mTag);
+ jw.StringProperty("cls", MakeStringSpan(lifetime.mObject.TypeName()));
+ jw.StringProperty("ptr",
+ nsPrintfCString("%p", lifetime.mObject.Pointer()));
+ jw.IntProperty("con", lifetime.mConstructionIndex.Value());
+ jw.DoubleProperty("con_ts",
+ ToSeconds(lifetime.mConstructionTimeStamp));
+ if (lifetime.mDestructionTimeStamp) {
+ jw.IntProperty("des", lifetime.mDestructionIndex.Value());
+ jw.DoubleProperty("des_ts",
+ ToSeconds(lifetime.mDestructionTimeStamp));
+ }
+ if (lifetime.mDerivedObject.Pointer()) {
+ DDLifetime* derived = mLifetimes.FindLifetime(
+ lifetime.mDerivedObject, lifetime.mDerivedObjectLinkingIndex);
+ if (derived) {
+ jw.IntProperty("drvd", derived->mTag);
+ }
+ }
+ jw.EndObject();
+ },
+ // If there were no (new) messages, only give the main HTMLMediaElement
+ // object (used to identify this log against the correct element.)
+ log->mMessages.IsEmpty());
+ jw.EndObject();
+ jw.End();
+ DDL_DEBUG("RetrieveMessages(%p) ->\n%s", mediaElement,
+ json.StringCRef().get());
+
+ // This log exists (new messages or not) -> Resolve this promise.
+ DDL_INFO("Resolving promise for HTMLMediaElement[%p] with messages %" PRImi
+ "-%" PRImi,
+ mediaElement,
+ log->mMessages.IsEmpty() ? 0 : log->mMessages[0].mIndex.Value(),
+ log->mMessages.IsEmpty()
+ ? 0
+ : log->mMessages[log->mMessages.Length() - 1].mIndex.Value());
+ promiseHolder.Resolve(std::move(json).StringRRef(), __func__);
+
+ // Remove exported messages.
+ log->mMessages.Clear();
+
+ // Pop this resolved promise, fetch next one.
+ MutexAutoLock lock(mMutex);
+ mPendingPromises.RemoveElementAt(0);
+ if (mPendingPromises.IsEmpty()) {
+ break;
+ }
+ promiseHolder = std::move(mPendingPromises[0].mPromiseHolder);
+ mediaElement = mPendingPromises[0].mMediaElement;
+ }
+}
+
+void DDMediaLogs::CleanUpLogs() {
+ MOZ_ASSERT(!mThread || mThread.get() == NS_GetCurrentThread());
+
+ DDTimeStamp now = DDNow();
+
+ // Keep up to 30s of unclassified messages (if a message doesn't get
+ // classified this quickly, it probably never will be.)
+ static const double sMaxAgeUnclassifiedMessages_s = 30.0;
+ // Keep "dead" log (video element and dependents were destroyed) for up to
+ // 2 minutes, in case the user wants to look at it after the facts.
+ static const double sMaxAgeDeadLog_s = 120.0;
+ // Keep old messages related to a live video for up to 5 minutes.
+ static const double sMaxAgeClassifiedMessages_s = 300.0;
+
+ for (size_t logIndexPlus1 = mMediaLogs.Length(); logIndexPlus1 != 0;
+ --logIndexPlus1) {
+ DDMediaLog& log = mMediaLogs[logIndexPlus1 - 1];
+ if (log.mMediaElement) {
+ // Remove logs for which no lifetime still existed some time ago.
+ bool used = mLifetimes.VisitBreakable(
+ log.mMediaElement, [&](const DDLifetime& lifetime) {
+ // Do we still have a lifetime that existed recently enough?
+ return !lifetime.mDestructionTimeStamp ||
+ (now - lifetime.mDestructionTimeStamp).ToSeconds() <=
+ sMaxAgeDeadLog_s;
+ });
+ if (!used) {
+ DDL_INFO("Removed old log for media element %p", log.mMediaElement);
+ mLifetimes.Visit(log.mMediaElement, [&](const DDLifetime& lifetime) {
+ DestroyLifetimeLinks(lifetime);
+ });
+ mLifetimes.RemoveLifetimesFor(log.mMediaElement);
+ mMediaLogs.RemoveElementAt(logIndexPlus1 - 1);
+ continue;
+ }
+ }
+
+ // Remove old messages.
+ size_t old = 0;
+ const size_t len = log.mMessages.Length();
+ while (old < len &&
+ (now - log.mMessages[old].mTimeStamp).ToSeconds() >
+ (log.mMediaElement ? sMaxAgeClassifiedMessages_s
+ : sMaxAgeUnclassifiedMessages_s)) {
+ ++old;
+ }
+ if (old != 0) {
+ // We are going to remove `old` messages.
+ // First, remove associated destroyed lifetimes that are not used after
+ // these old messages. (We want to keep non-destroyed lifetimes, in
+ // case they get used later on.)
+ size_t removedLifetimes = 0;
+ for (size_t i = 0; i < old; ++i) {
+ auto RemoveDestroyedUnusedLifetime = [&](DDLifetime* lifetime) {
+ if (!lifetime->mDestructionTimeStamp) {
+ // Lifetime is still alive, keep it.
+ return;
+ }
+ bool used = false;
+ for (size_t after = old; after < len; ++after) {
+ const DDLogMessage message = log.mMessages[i];
+ if (!lifetime->IsAliveAt(message.mIndex)) {
+ // Lifetime is already dead, and not used yet -> kill it.
+ break;
+ }
+ const DDLogObject& ob = message.mObject;
+ if (lifetime->mObject == ob) {
+ used = true;
+ break;
+ }
+ if (message.mValue.is<DDLogObject>()) {
+ if (lifetime->mObject == message.mValue.as<DDLogObject>()) {
+ used = true;
+ break;
+ }
+ }
+ }
+ if (!used) {
+ DestroyLifetimeLinks(*lifetime);
+ mLifetimes.RemoveLifetime(lifetime);
+ ++removedLifetimes;
+ }
+ };
+
+ const DDLogMessage message = log.mMessages[i];
+ const DDLogObject& ob = message.mObject;
+
+ DDLifetime* lifetime1 = mLifetimes.FindLifetime(ob, message.mIndex);
+ if (lifetime1) {
+ RemoveDestroyedUnusedLifetime(lifetime1);
+ }
+
+ if (message.mValue.is<DDLogObject>()) {
+ DDLifetime* lifetime2 = mLifetimes.FindLifetime(
+ message.mValue.as<DDLogObject>(), message.mIndex);
+ if (lifetime2) {
+ RemoveDestroyedUnusedLifetime(lifetime2);
+ }
+ }
+ }
+ DDL_INFO("Removed %zu messages (#%" PRImi " %f - #%" PRImi
+ " %f) and %zu lifetimes from log for media element %p",
+ old, log.mMessages[0].mIndex.Value(),
+ ToSeconds(log.mMessages[0].mTimeStamp),
+ log.mMessages[old - 1].mIndex.Value(),
+ ToSeconds(log.mMessages[old - 1].mTimeStamp), removedLifetimes,
+ log.mMediaElement);
+ log.mMessages.RemoveElementsAt(0, old);
+ }
+ }
+}
+
+void DDMediaLogs::ProcessLog() {
+ MOZ_ASSERT(!mThread || mThread.get() == NS_GetCurrentThread());
+ ProcessBuffer();
+ FulfillPromises();
+ CleanUpLogs();
+ DDL_INFO("ProcessLog() completed - DDMediaLog size: %zu",
+ SizeOfIncludingThis(moz_malloc_size_of));
+}
+
+nsresult DDMediaLogs::DispatchProcessLog(const MutexAutoLock& aProofOfLock) {
+ if (!mThread) {
+ return NS_ERROR_SERVICE_NOT_AVAILABLE;
+ }
+ return mThread->Dispatch(
+ NS_NewRunnableFunction("ProcessLog", [this] { ProcessLog(); }),
+ NS_DISPATCH_NORMAL);
+}
+
+nsresult DDMediaLogs::DispatchProcessLog() {
+ DDL_INFO("DispatchProcessLog() - Yet-unprocessed message buffers: %d",
+ mMessagesQueue.LiveBuffersStats().mCount);
+ MutexAutoLock lock(mMutex);
+ return DispatchProcessLog(lock);
+}
+
+RefPtr<DDMediaLogs::LogMessagesPromise> DDMediaLogs::RetrieveMessages(
+ const dom::HTMLMediaElement* aMediaElement) {
+ MozPromiseHolder<LogMessagesPromise> holder;
+ RefPtr<LogMessagesPromise> promise = holder.Ensure(__func__);
+ {
+ MutexAutoLock lock(mMutex);
+ // If there were unfulfilled promises, we know processing has already
+ // been requested.
+ if (mPendingPromises.IsEmpty()) {
+ // But if we're the first one, start processing.
+ nsresult rv = DispatchProcessLog(lock);
+ if (NS_FAILED(rv)) {
+ holder.Reject(rv, __func__);
+ }
+ }
+ mPendingPromises.AppendElement(
+ PendingPromise{std::move(holder), aMediaElement});
+ }
+ return promise;
+}
+
+} // namespace mozilla
diff --git a/dom/media/doctor/DDMediaLogs.h b/dom/media/doctor/DDMediaLogs.h
new file mode 100644
index 0000000000..ef5bbe98f9
--- /dev/null
+++ b/dom/media/doctor/DDMediaLogs.h
@@ -0,0 +1,193 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DDMediaLogs_h_
+#define DDMediaLogs_h_
+
+#include "DDLifetimes.h"
+#include "DDMediaLog.h"
+#include "mozilla/MozPromise.h"
+#include "MultiWriterQueue.h"
+
+namespace mozilla {
+
+// Main object managing all processed logs, and yet-unprocessed messages.
+struct DDMediaLogs {
+ public:
+ // Construct a DDMediaLogs object if possible.
+ struct ConstructionResult {
+ nsresult mRv;
+ DDMediaLogs* mMediaLogs;
+ };
+ static ConstructionResult New();
+
+ // If not already shutdown, performs normal end-of-life processing, and shuts
+ // down the processing thread (blocking).
+ ~DDMediaLogs();
+
+ // Shutdown the processing thread (blocking), and free as much memory as
+ // possible.
+ void Panic();
+
+ inline void Log(const char* aSubjectTypeName, const void* aSubjectPointer,
+ DDLogCategory aCategory, const char* aLabel,
+ DDLogValue&& aValue) {
+ if (mMessagesQueue.PushF(
+ [&](DDLogMessage& aMessage, MessagesQueue::Index i) {
+ aMessage.mIndex = i;
+ aMessage.mTimeStamp = DDNow();
+ aMessage.mObject.Set(aSubjectTypeName, aSubjectPointer);
+ aMessage.mCategory = aCategory;
+ aMessage.mLabel = aLabel;
+ aMessage.mValue = std::move(aValue);
+ })) {
+ // Filled a buffer-full of messages, process it in another thread.
+ DispatchProcessLog();
+ }
+ }
+
+ // Process the log right now; should only be used on the processing thread,
+ // or after shutdown for end-of-life log retrieval. Work includes:
+ // - Processing incoming buffers, to update object lifetimes and links;
+ // - Resolve pending promises that requested logs;
+ // - Clean-up old logs from memory.
+ void ProcessLog();
+
+ using LogMessagesPromise =
+ MozPromise<nsCString, nsresult, /* IsExclusive = */ true>;
+
+ // Retrieve all messages associated with an HTMLMediaElement.
+ // This will trigger an async processing run (to ensure most recent messages
+ // get retrieved too), and the returned promise will be resolved with all
+ // found log messages.
+ RefPtr<LogMessagesPromise> RetrieveMessages(
+ const dom::HTMLMediaElement* aMediaElement);
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const;
+
+ private:
+ // Constructor, takes the given thread to use for log processing.
+ explicit DDMediaLogs(nsCOMPtr<nsIThread>&& aThread);
+
+ // Shutdown the processing thread, blocks until that thread exits.
+ // If aPanic is true, just free as much memory as possible.
+ // Otherwise, perform a final processing run, output end-logs (if enabled).
+ void Shutdown(bool aPanic);
+
+ // Get the log of yet-unassociated messages.
+ DDMediaLog& LogForUnassociatedMessages();
+ const DDMediaLog& LogForUnassociatedMessages() const;
+
+ // Get the log for the given HTMLMediaElement. Returns nullptr if there is no
+ // such log yet.
+ DDMediaLog* GetLogFor(const dom::HTMLMediaElement* aMediaElement);
+
+ // Get the log for the given HTMLMediaElement.
+ // A new log is created if that element didn't already have one.
+ DDMediaLog& LogFor(const dom::HTMLMediaElement* aMediaElement);
+
+ // Associate a lifetime, and all its already-linked lifetimes, with an
+ // HTMLMediaElement.
+ // All messages involving the modified lifetime(s) are moved to the
+ // corresponding log.
+ void SetMediaElement(DDLifetime& aLifetime,
+ const dom::HTMLMediaElement* aMediaElement);
+
+ // Find the lifetime corresponding to an object (known type and pointer) that
+ // was known to be alive at aIndex.
+ // If there is no such lifetime yet, create it with aTimeStamp as implicit
+ // construction timestamp.
+ // If the object is of type HTMLMediaElement, run SetMediaElement() on it.
+ DDLifetime& FindOrCreateLifetime(const DDLogObject& aObject,
+ DDMessageIndex aIndex,
+ const DDTimeStamp& aTimeStamp);
+
+ // Link two lifetimes together (at a given time corresponding to aIndex).
+ // If only one is associated with an HTMLMediaElement, run SetMediaElement on
+ // the other one.
+ void LinkLifetimes(DDLifetime& aParentLifetime, const char* aLinkName,
+ DDLifetime& aChildLifetime, DDMessageIndex aIndex);
+
+ // Unlink all lifetimes linked to aLifetime; only used to know when links
+ // expire, so that they won't be used after this time.
+ void UnlinkLifetime(DDLifetime& aLifetime, DDMessageIndex aIndex);
+
+ // Unlink two lifetimes; only used to know when a link expires, so that it
+ // won't be used after this time.
+ void UnlinkLifetimes(DDLifetime& aParentLifetime, DDLifetime& aChildLifetime,
+ DDMessageIndex aIndex);
+
+ // Remove all links involving aLifetime from the database.
+ void DestroyLifetimeLinks(const DDLifetime& aLifetime);
+
+ // Process all incoming log messages.
+ // This will create the appropriate DDLifetime and links objects, and then
+ // move processed messages to logs associated with different
+ // HTMLMediaElements.
+ void ProcessBuffer();
+
+ // Pending promises (added by RetrieveMessages) are resolved with all new
+ // log messages corresponding to requested HTMLMediaElements -- These
+ // messages are removed from our logs.
+ void FulfillPromises();
+
+ // Remove processed messages that have a low chance of being requested,
+ // based on the assumption that users/scripts will regularly call
+ // RetrieveMessages for HTMLMediaElements they are interested in.
+ void CleanUpLogs();
+
+ // Request log-processing on the processing thread. Thread-safe.
+ nsresult DispatchProcessLog();
+
+ // Request log-processing on the processing thread.
+ nsresult DispatchProcessLog(const MutexAutoLock& aProofOfLock);
+
+ using MessagesQueue =
+ MultiWriterQueue<DDLogMessage, MultiWriterQueueDefaultBufferSize,
+ MultiWriterQueueReaderLocking_None>;
+ MessagesQueue mMessagesQueue;
+
+ DDLifetimes mLifetimes;
+
+ // mMediaLogs[0] contains unsorted message (with mMediaElement=nullptr).
+ // mMediaLogs[1+] contains sorted messages for each media element.
+ nsTArray<DDMediaLog> mMediaLogs;
+
+ struct DDObjectLink {
+ const DDLogObject mParent;
+ const DDLogObject mChild;
+ const char* const mLinkName;
+ const DDMessageIndex mLinkingIndex;
+ Maybe<DDMessageIndex> mUnlinkingIndex;
+
+ DDObjectLink(DDLogObject aParent, DDLogObject aChild, const char* aLinkName,
+ DDMessageIndex aLinkingIndex)
+ : mParent(aParent),
+ mChild(aChild),
+ mLinkName(aLinkName),
+ mLinkingIndex(aLinkingIndex),
+ mUnlinkingIndex(Nothing{}) {}
+ };
+ // Links between live objects, updated while messages are processed.
+ nsTArray<DDObjectLink> mObjectLinks;
+
+ // Protects members below.
+ Mutex mMutex MOZ_UNANNOTATED;
+
+ // Processing thread.
+ nsCOMPtr<nsIThread> mThread;
+
+ struct PendingPromise {
+ MozPromiseHolder<LogMessagesPromise> mPromiseHolder;
+ const dom::HTMLMediaElement* mMediaElement;
+ };
+ // Most cases should have 1 media panel requesting 1 promise at a time.
+ AutoTArray<PendingPromise, 2> mPendingPromises;
+};
+
+} // namespace mozilla
+
+#endif // DDMediaLogs_h_
diff --git a/dom/media/doctor/DDMessageIndex.h b/dom/media/doctor/DDMessageIndex.h
new file mode 100644
index 0000000000..e2b5c7274c
--- /dev/null
+++ b/dom/media/doctor/DDMessageIndex.h
@@ -0,0 +1,26 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DDMessageIndex_h_
+#define DDMessageIndex_h_
+
+#include "RollingNumber.h"
+
+namespace mozilla {
+
+// Type used to uniquely identify and sort log messages.
+// We assume that a given media element won't live for more than the time taken
+// to log 2^31 messages (per process); for 10,000 messages per seconds, that's
+// about 2.5 days
+using DDMessageIndex = RollingNumber<uint32_t>;
+
+// Printf string constant to use when printing a DDMessageIndex, e.g.:
+// `printf("index=%" PRImi, index);`
+#define PRImi PRIu32
+
+} // namespace mozilla
+
+#endif // DDMessageIndex_h_
diff --git a/dom/media/doctor/DDTimeStamp.cpp b/dom/media/doctor/DDTimeStamp.cpp
new file mode 100644
index 0000000000..b440c559d7
--- /dev/null
+++ b/dom/media/doctor/DDTimeStamp.cpp
@@ -0,0 +1,20 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DDTimeStamp.h"
+
+namespace mozilla {
+
+double ToSeconds(const DDTimeStamp& aTimeStamp) {
+ // Timestamp at first call, used internally to convert log timestamps
+ // to a duration from this timestamp.
+ // What's important is the relative time between log messages.
+ static const DDTimeStamp sInitialTimeStamp = TimeStamp::Now();
+
+ return (aTimeStamp - sInitialTimeStamp).ToSeconds();
+}
+
+} // namespace mozilla
diff --git a/dom/media/doctor/DDTimeStamp.h b/dom/media/doctor/DDTimeStamp.h
new file mode 100644
index 0000000000..71cbfb8101
--- /dev/null
+++ b/dom/media/doctor/DDTimeStamp.h
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DDTimeStamp_h_
+#define DDTimeStamp_h_
+
+#include "mozilla/TimeStamp.h"
+
+namespace mozilla {
+
+// Simply using mozilla::TimeStamp as our timestamp type.
+using DDTimeStamp = TimeStamp;
+
+inline DDTimeStamp DDNow() { return TimeStamp::Now(); }
+
+// Convert a timestamp to the number of seconds since the process start.
+double ToSeconds(const DDTimeStamp& aTimeStamp);
+
+} // namespace mozilla
+
+#endif // DDTimeStamp_h_
diff --git a/dom/media/doctor/DecoderDoctorDiagnostics.cpp b/dom/media/doctor/DecoderDoctorDiagnostics.cpp
new file mode 100644
index 0000000000..164c614547
--- /dev/null
+++ b/dom/media/doctor/DecoderDoctorDiagnostics.cpp
@@ -0,0 +1,1319 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DecoderDoctorDiagnostics.h"
+
+#include <string.h>
+
+#include "VideoUtils.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/dom/Document.h"
+#include "nsContentUtils.h"
+#include "nsGkAtoms.h"
+#include "nsIObserverService.h"
+#include "nsIScriptError.h"
+#include "nsITimer.h"
+#include "nsPluginHost.h"
+#include "nsPrintfCString.h"
+
+#if defined(MOZ_FFMPEG)
+# include "FFmpegRuntimeLinker.h"
+#endif
+
+static mozilla::LazyLogModule sDecoderDoctorLog("DecoderDoctor");
+#define DD_LOG(level, arg, ...) \
+ MOZ_LOG(sDecoderDoctorLog, level, (arg, ##__VA_ARGS__))
+#define DD_DEBUG(arg, ...) DD_LOG(mozilla::LogLevel::Debug, arg, ##__VA_ARGS__)
+#define DD_INFO(arg, ...) DD_LOG(mozilla::LogLevel::Info, arg, ##__VA_ARGS__)
+#define DD_WARN(arg, ...) DD_LOG(mozilla::LogLevel::Warning, arg, ##__VA_ARGS__)
+
+namespace mozilla {
+
+// Class that collects a sequence of diagnostics from the same document over a
+// small period of time, in order to provide a synthesized analysis.
+//
+// Referenced by the document through a nsINode property, mTimer, and
+// inter-task captures.
+// When notified that the document is dead, or when the timer expires but
+// nothing new happened, StopWatching() will remove the document property and
+// timer (if present), so no more work will happen and the watcher will be
+// destroyed once all references are gone.
+class DecoderDoctorDocumentWatcher : public nsITimerCallback, public nsINamed {
+ public:
+ static already_AddRefed<DecoderDoctorDocumentWatcher> RetrieveOrCreate(
+ dom::Document* aDocument);
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSITIMERCALLBACK
+ NS_DECL_NSINAMED
+
+ void AddDiagnostics(DecoderDoctorDiagnostics&& aDiagnostics,
+ const char* aCallSite);
+
+ private:
+ explicit DecoderDoctorDocumentWatcher(dom::Document* aDocument);
+ virtual ~DecoderDoctorDocumentWatcher();
+
+ // This will prevent further work from happening, watcher will deregister
+ // itself from document (if requested) and cancel any timer, and soon die.
+ void StopWatching(bool aRemoveProperty);
+
+ // Remove property from document; will call DestroyPropertyCallback.
+ void RemovePropertyFromDocument();
+ // Callback for property destructor, will be automatically called when the
+ // document (in aObject) is being destroyed.
+ static void DestroyPropertyCallback(void* aObject, nsAtom* aPropertyName,
+ void* aPropertyValue, void* aData);
+
+ static const uint32_t sAnalysisPeriod_ms = 1000;
+ void EnsureTimerIsStarted();
+
+ void SynthesizeAnalysis();
+ // This is used for testing and will generate an analysis based on the value
+ // set in `media.decoder-doctor.testing.fake-error`.
+ void SynthesizeFakeAnalysis();
+ bool ShouldSynthesizeFakeAnalysis() const;
+
+ // Raw pointer to a Document.
+ // Must be non-null during construction.
+ // Nulled when we want to stop watching, because either:
+ // 1. The document has been destroyed (notified through
+ // DestroyPropertyCallback).
+ // 2. We have not received new diagnostic information within a short time
+ // period, so we just stop watching.
+ // Once nulled, no more actual work will happen, and the watcher will be
+ // destroyed soon.
+ dom::Document* mDocument;
+
+ struct Diagnostics {
+ Diagnostics(DecoderDoctorDiagnostics&& aDiagnostics, const char* aCallSite,
+ mozilla::TimeStamp aTimeStamp)
+ : mDecoderDoctorDiagnostics(std::move(aDiagnostics)),
+ mCallSite(aCallSite),
+ mTimeStamp(aTimeStamp) {}
+ Diagnostics(const Diagnostics&) = delete;
+ Diagnostics(Diagnostics&& aOther)
+ : mDecoderDoctorDiagnostics(
+ std::move(aOther.mDecoderDoctorDiagnostics)),
+ mCallSite(std::move(aOther.mCallSite)),
+ mTimeStamp(aOther.mTimeStamp) {}
+
+ const DecoderDoctorDiagnostics mDecoderDoctorDiagnostics;
+ const nsCString mCallSite;
+ const mozilla::TimeStamp mTimeStamp;
+ };
+ typedef nsTArray<Diagnostics> DiagnosticsSequence;
+ DiagnosticsSequence mDiagnosticsSequence;
+
+ nsCOMPtr<nsITimer> mTimer; // Keep timer alive until we run.
+ DiagnosticsSequence::size_type mDiagnosticsHandled = 0;
+};
+
+NS_IMPL_ISUPPORTS(DecoderDoctorDocumentWatcher, nsITimerCallback, nsINamed)
+
+// static
+already_AddRefed<DecoderDoctorDocumentWatcher>
+DecoderDoctorDocumentWatcher::RetrieveOrCreate(dom::Document* aDocument) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aDocument);
+ RefPtr<DecoderDoctorDocumentWatcher> watcher =
+ static_cast<DecoderDoctorDocumentWatcher*>(
+ aDocument->GetProperty(nsGkAtoms::decoderDoctor));
+ if (!watcher) {
+ watcher = new DecoderDoctorDocumentWatcher(aDocument);
+ if (NS_WARN_IF(NS_FAILED(aDocument->SetProperty(
+ nsGkAtoms::decoderDoctor, watcher.get(), DestroyPropertyCallback,
+ /*transfer*/ false)))) {
+ DD_WARN(
+ "DecoderDoctorDocumentWatcher::RetrieveOrCreate(doc=%p) - Could not "
+ "set property in document, will destroy new watcher[%p]",
+ aDocument, watcher.get());
+ return nullptr;
+ }
+ // Document owns watcher through this property.
+ // Released in DestroyPropertyCallback().
+ NS_ADDREF(watcher.get());
+ }
+ return watcher.forget();
+}
+
+DecoderDoctorDocumentWatcher::DecoderDoctorDocumentWatcher(
+ dom::Document* aDocument)
+ : mDocument(aDocument) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mDocument);
+ DD_DEBUG(
+ "DecoderDoctorDocumentWatcher[%p]::DecoderDoctorDocumentWatcher(doc=%p)",
+ this, mDocument);
+}
+
+DecoderDoctorDocumentWatcher::~DecoderDoctorDocumentWatcher() {
+ MOZ_ASSERT(NS_IsMainThread());
+ DD_DEBUG(
+ "DecoderDoctorDocumentWatcher[%p, doc=%p <- expect "
+ "0]::~DecoderDoctorDocumentWatcher()",
+ this, mDocument);
+ // mDocument should have been reset through StopWatching()!
+ MOZ_ASSERT(!mDocument);
+}
+
+void DecoderDoctorDocumentWatcher::RemovePropertyFromDocument() {
+ MOZ_ASSERT(NS_IsMainThread());
+ DecoderDoctorDocumentWatcher* watcher =
+ static_cast<DecoderDoctorDocumentWatcher*>(
+ mDocument->GetProperty(nsGkAtoms::decoderDoctor));
+ if (!watcher) {
+ return;
+ }
+ DD_DEBUG(
+ "DecoderDoctorDocumentWatcher[%p, "
+ "doc=%p]::RemovePropertyFromDocument()\n",
+ watcher, watcher->mDocument);
+ // This will call our DestroyPropertyCallback.
+ mDocument->RemoveProperty(nsGkAtoms::decoderDoctor);
+}
+
+// Callback for property destructors. |aObject| is the object
+// the property is being removed for, |aPropertyName| is the property
+// being removed, |aPropertyValue| is the value of the property, and |aData|
+// is the opaque destructor data that was passed to SetProperty().
+// static
+void DecoderDoctorDocumentWatcher::DestroyPropertyCallback(
+ void* aObject, nsAtom* aPropertyName, void* aPropertyValue, void*) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aPropertyName == nsGkAtoms::decoderDoctor);
+ DecoderDoctorDocumentWatcher* watcher =
+ static_cast<DecoderDoctorDocumentWatcher*>(aPropertyValue);
+ MOZ_ASSERT(watcher);
+#ifdef DEBUG
+ auto* document = static_cast<dom::Document*>(aObject);
+ MOZ_ASSERT(watcher->mDocument == document);
+#endif
+ DD_DEBUG(
+ "DecoderDoctorDocumentWatcher[%p, doc=%p]::DestroyPropertyCallback()\n",
+ watcher, watcher->mDocument);
+ // 'false': StopWatching should not try and remove the property.
+ watcher->StopWatching(false);
+ NS_RELEASE(watcher);
+}
+
+void DecoderDoctorDocumentWatcher::StopWatching(bool aRemoveProperty) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // StopWatching() shouldn't be called twice.
+ MOZ_ASSERT(mDocument);
+
+ if (aRemoveProperty) {
+ RemovePropertyFromDocument();
+ }
+
+ // Forget document now, this will prevent more work from being started.
+ mDocument = nullptr;
+
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+}
+
+void DecoderDoctorDocumentWatcher::EnsureTimerIsStarted() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mTimer) {
+ NS_NewTimerWithCallback(getter_AddRefs(mTimer), this, sAnalysisPeriod_ms,
+ nsITimer::TYPE_ONE_SHOT);
+ }
+}
+
+enum class ReportParam : uint8_t {
+ // Marks the end of the parameter list.
+ // Keep this zero! (For implicit zero-inits when used in definitions below.)
+ None = 0,
+
+ Formats,
+ DecodeIssue,
+ DocURL,
+ ResourceURL
+};
+
+struct NotificationAndReportStringId {
+ // Notification type, handled by DecoderDoctorChild.sys.mjs and
+ // DecoderDoctorParent.sys.mjs.
+ dom::DecoderDoctorNotificationType mNotificationType;
+ // Console message id. Key in dom/locales/.../chrome/dom/dom.properties.
+ const char* mReportStringId;
+ static const int maxReportParams = 4;
+ ReportParam mReportParams[maxReportParams];
+};
+
+// Note: ReportStringIds are limited to alphanumeric only.
+static const NotificationAndReportStringId sMediaWidevineNoWMF = {
+ dom::DecoderDoctorNotificationType::Platform_decoder_not_found,
+ "MediaWidevineNoWMF",
+ {ReportParam::None}};
+static const NotificationAndReportStringId sMediaWMFNeeded = {
+ dom::DecoderDoctorNotificationType::Platform_decoder_not_found,
+ "MediaWMFNeeded",
+ {ReportParam::Formats}};
+static const NotificationAndReportStringId sMediaFFMpegNotFound = {
+ dom::DecoderDoctorNotificationType::Platform_decoder_not_found,
+ "MediaPlatformDecoderNotFound",
+ {ReportParam::Formats}};
+static const NotificationAndReportStringId sMediaCannotPlayNoDecoders = {
+ dom::DecoderDoctorNotificationType::Cannot_play,
+ "MediaCannotPlayNoDecoders",
+ {ReportParam::Formats}};
+static const NotificationAndReportStringId sMediaNoDecoders = {
+ dom::DecoderDoctorNotificationType::Can_play_but_some_missing_decoders,
+ "MediaNoDecoders",
+ {ReportParam::Formats}};
+static const NotificationAndReportStringId sCannotInitializePulseAudio = {
+ dom::DecoderDoctorNotificationType::Cannot_initialize_pulseaudio,
+ "MediaCannotInitializePulseAudio",
+ {ReportParam::None}};
+static const NotificationAndReportStringId sUnsupportedLibavcodec = {
+ dom::DecoderDoctorNotificationType::Unsupported_libavcodec,
+ "MediaUnsupportedLibavcodec",
+ {ReportParam::None}};
+static const NotificationAndReportStringId sMediaDecodeError = {
+ dom::DecoderDoctorNotificationType::Decode_error,
+ "MediaDecodeError",
+ {ReportParam::ResourceURL, ReportParam::DecodeIssue}};
+static const NotificationAndReportStringId sMediaDecodeWarning = {
+ dom::DecoderDoctorNotificationType::Decode_warning,
+ "MediaDecodeWarning",
+ {ReportParam::ResourceURL, ReportParam::DecodeIssue}};
+
+static const NotificationAndReportStringId* const
+ sAllNotificationsAndReportStringIds[] = {
+ &sMediaWidevineNoWMF, &sMediaWMFNeeded,
+ &sMediaFFMpegNotFound, &sMediaCannotPlayNoDecoders,
+ &sMediaNoDecoders, &sCannotInitializePulseAudio,
+ &sUnsupportedLibavcodec, &sMediaDecodeError,
+ &sMediaDecodeWarning};
+
+// Create a webcompat-friendly description of a MediaResult.
+static nsString MediaResultDescription(const MediaResult& aResult,
+ bool aIsError) {
+ nsCString name;
+ GetErrorName(aResult.Code(), name);
+ return NS_ConvertUTF8toUTF16(nsPrintfCString(
+ "%s Code: %s (0x%08" PRIx32 ")%s%s", aIsError ? "Error" : "Warning",
+ name.get(), static_cast<uint32_t>(aResult.Code()),
+ aResult.Message().IsEmpty() ? "" : "\nDetails: ",
+ aResult.Message().get()));
+}
+
+static bool IsNotificationAllowedOnPlatform(
+ const NotificationAndReportStringId& aNotification) {
+ // Allow all notifications during testing.
+ if (StaticPrefs::media_decoder_doctor_testing()) {
+ return true;
+ }
+ // These notifications are platform independent.
+ if (aNotification.mNotificationType ==
+ dom::DecoderDoctorNotificationType::Cannot_play ||
+ aNotification.mNotificationType ==
+ dom::DecoderDoctorNotificationType::
+ Can_play_but_some_missing_decoders ||
+ aNotification.mNotificationType ==
+ dom::DecoderDoctorNotificationType::Decode_error ||
+ aNotification.mNotificationType ==
+ dom::DecoderDoctorNotificationType::Decode_warning) {
+ return true;
+ }
+#if defined(XP_WIN)
+ if (aNotification.mNotificationType ==
+ dom::DecoderDoctorNotificationType::Platform_decoder_not_found) {
+ return strcmp(sMediaWMFNeeded.mReportStringId,
+ aNotification.mReportStringId) == 0 ||
+ strcmp(sMediaWidevineNoWMF.mReportStringId,
+ aNotification.mReportStringId) == 0;
+ }
+#endif
+#if defined(MOZ_FFMPEG)
+ if (aNotification.mNotificationType ==
+ dom::DecoderDoctorNotificationType::Platform_decoder_not_found) {
+ return strcmp(sMediaFFMpegNotFound.mReportStringId,
+ aNotification.mReportStringId) == 0;
+ }
+ if (aNotification.mNotificationType ==
+ dom::DecoderDoctorNotificationType::Unsupported_libavcodec) {
+ return strcmp(sUnsupportedLibavcodec.mReportStringId,
+ aNotification.mReportStringId) == 0;
+ }
+#endif
+#ifdef MOZ_PULSEAUDIO
+ if (aNotification.mNotificationType ==
+ dom::DecoderDoctorNotificationType::Cannot_initialize_pulseaudio) {
+ return strcmp(sCannotInitializePulseAudio.mReportStringId,
+ aNotification.mReportStringId) == 0;
+ }
+#endif
+ return false;
+}
+
+static void DispatchNotification(
+ nsISupports* aSubject, const NotificationAndReportStringId& aNotification,
+ bool aIsSolved, const nsAString& aFormats, const nsAString& aDecodeIssue,
+ const nsACString& aDocURL, const nsAString& aResourceURL) {
+ if (!aSubject) {
+ return;
+ }
+ dom::DecoderDoctorNotification data;
+ data.mType = aNotification.mNotificationType;
+ data.mIsSolved = aIsSolved;
+ data.mDecoderDoctorReportId.Assign(
+ NS_ConvertUTF8toUTF16(aNotification.mReportStringId));
+ if (!aFormats.IsEmpty()) {
+ data.mFormats.Construct(aFormats);
+ }
+ if (!aDecodeIssue.IsEmpty()) {
+ data.mDecodeIssue.Construct(aDecodeIssue);
+ }
+ if (!aDocURL.IsEmpty()) {
+ data.mDocURL.Construct(NS_ConvertUTF8toUTF16(aDocURL));
+ }
+ if (!aResourceURL.IsEmpty()) {
+ data.mResourceURL.Construct(aResourceURL);
+ }
+ nsAutoString json;
+ data.ToJSON(json);
+ if (json.IsEmpty()) {
+ DD_WARN(
+ "DecoderDoctorDiagnostics/DispatchEvent() - Could not create json for "
+ "dispatch");
+ // No point in dispatching this notification without data, the front-end
+ // wouldn't know what to display.
+ return;
+ }
+ DD_DEBUG("DecoderDoctorDiagnostics/DispatchEvent() %s",
+ NS_ConvertUTF16toUTF8(json).get());
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ if (obs) {
+ obs->NotifyObservers(aSubject, "decoder-doctor-notification", json.get());
+ }
+}
+
+static void ReportToConsole(dom::Document* aDocument,
+ const char* aConsoleStringId,
+ const nsTArray<nsString>& aParams) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aDocument);
+
+ DD_DEBUG(
+ "DecoderDoctorDiagnostics.cpp:ReportToConsole(doc=%p) ReportToConsole"
+ " - aMsg='%s' params={%s%s%s%s}",
+ aDocument, aConsoleStringId,
+ aParams.IsEmpty() ? "<no params>"
+ : NS_ConvertUTF16toUTF8(aParams[0]).get(),
+ (aParams.Length() < 1 || aParams[0].IsEmpty()) ? "" : ", ",
+ (aParams.Length() < 1 || aParams[0].IsEmpty())
+ ? ""
+ : NS_ConvertUTF16toUTF8(aParams[0]).get(),
+ aParams.Length() < 2 ? "" : ", ...");
+ if (StaticPrefs::media_decoder_doctor_testing()) {
+ Unused << nsContentUtils::DispatchTrustedEvent(
+ aDocument, ToSupports(aDocument), u"mozreportmediaerror"_ns,
+ CanBubble::eNo, Cancelable::eNo);
+ }
+ nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "Media"_ns,
+ aDocument, nsContentUtils::eDOM_PROPERTIES,
+ aConsoleStringId, aParams);
+}
+
+static bool AllowNotification(
+ const NotificationAndReportStringId& aNotification) {
+ // "media.decoder-doctor.notifications-allowed" controls which notifications
+ // may be dispatched to the front-end. It either contains:
+ // - '*' -> Allow everything.
+ // - Comma-separater list of ids -> Allow if aReportStringId (from
+ // dom.properties) is one of them.
+ // - Nothing (missing or empty) -> Disable everything.
+ nsAutoCString filter;
+ Preferences::GetCString("media.decoder-doctor.notifications-allowed", filter);
+ return filter.EqualsLiteral("*") ||
+ StringListContains(filter, aNotification.mReportStringId);
+}
+
+static bool AllowDecodeIssue(const MediaResult& aDecodeIssue,
+ bool aDecodeIssueIsError) {
+ if (aDecodeIssue == NS_OK) {
+ // 'NS_OK' means we are not actually reporting a decode issue, so we
+ // allow the report.
+ return true;
+ }
+
+ // "media.decoder-doctor.decode-{errors,warnings}-allowed" controls which
+ // decode issues may be dispatched to the front-end. It either contains:
+ // - '*' -> Allow everything.
+ // - Comma-separater list of ids -> Allow if the issue name is one of them.
+ // - Nothing (missing or empty) -> Disable everything.
+ nsAutoCString filter;
+ Preferences::GetCString(aDecodeIssueIsError
+ ? "media.decoder-doctor.decode-errors-allowed"
+ : "media.decoder-doctor.decode-warnings-allowed",
+ filter);
+ if (filter.EqualsLiteral("*")) {
+ return true;
+ }
+
+ nsCString decodeIssueName;
+ GetErrorName(aDecodeIssue.Code(), static_cast<nsACString&>(decodeIssueName));
+ return StringListContains(filter, decodeIssueName);
+}
+
+static void ReportAnalysis(dom::Document* aDocument,
+ const NotificationAndReportStringId& aNotification,
+ bool aIsSolved, const nsAString& aFormats = u""_ns,
+ const MediaResult& aDecodeIssue = NS_OK,
+ bool aDecodeIssueIsError = true,
+ const nsACString& aDocURL = ""_ns,
+ const nsAString& aResourceURL = u""_ns) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!aDocument) {
+ return;
+ }
+
+ // Some errors should only appear on the specific platform. Eg. WMF related
+ // error only happens on Windows.
+ if (!IsNotificationAllowedOnPlatform(aNotification)) {
+ DD_WARN("Platform doesn't support '%s'!", aNotification.mReportStringId);
+ return;
+ }
+
+ nsString decodeIssueDescription;
+ if (aDecodeIssue != NS_OK) {
+ decodeIssueDescription.Assign(
+ MediaResultDescription(aDecodeIssue, aDecodeIssueIsError));
+ }
+
+ // Report non-solved issues to console.
+ if (!aIsSolved) {
+ // Build parameter array needed by console message.
+ AutoTArray<nsString, NotificationAndReportStringId::maxReportParams> params;
+ for (int i = 0; i < NotificationAndReportStringId::maxReportParams; ++i) {
+ if (aNotification.mReportParams[i] == ReportParam::None) {
+ break;
+ }
+ switch (aNotification.mReportParams[i]) {
+ case ReportParam::Formats:
+ params.AppendElement(aFormats);
+ break;
+ case ReportParam::DecodeIssue:
+ params.AppendElement(decodeIssueDescription);
+ break;
+ case ReportParam::DocURL:
+ params.AppendElement(NS_ConvertUTF8toUTF16(aDocURL));
+ break;
+ case ReportParam::ResourceURL:
+ params.AppendElement(aResourceURL);
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Bad notification parameter choice");
+ break;
+ }
+ }
+ ReportToConsole(aDocument, aNotification.mReportStringId, params);
+ }
+
+ const bool allowNotification = AllowNotification(aNotification);
+ const bool allowDecodeIssue =
+ AllowDecodeIssue(aDecodeIssue, aDecodeIssueIsError);
+ DD_INFO(
+ "ReportAnalysis for %s (decodeResult=%s) [AllowNotification=%d, "
+ "AllowDecodeIssue=%d]",
+ aNotification.mReportStringId, aDecodeIssue.ErrorName().get(),
+ allowNotification, allowDecodeIssue);
+ if (allowNotification && allowDecodeIssue) {
+ DispatchNotification(aDocument->GetInnerWindow(), aNotification, aIsSolved,
+ aFormats, decodeIssueDescription, aDocURL,
+ aResourceURL);
+ }
+}
+
+static nsString CleanItemForFormatsList(const nsAString& aItem) {
+ nsString item(aItem);
+ // Remove commas from item, as commas are used to separate items. It's fine
+ // to have a one-way mapping, it's only used for comparisons and in
+ // console display (where formats shouldn't contain commas in the first place)
+ item.ReplaceChar(',', ' ');
+ item.CompressWhitespace();
+ return item;
+}
+
+static void AppendToFormatsList(nsAString& aList, const nsAString& aItem) {
+ if (!aList.IsEmpty()) {
+ aList += u", "_ns;
+ }
+ aList += CleanItemForFormatsList(aItem);
+}
+
+static bool FormatsListContains(const nsAString& aList,
+ const nsAString& aItem) {
+ return StringListContains(aList, CleanItemForFormatsList(aItem));
+}
+
+static const char* GetLinkStatusLibraryName() {
+#if defined(MOZ_FFMPEG)
+ return FFmpegRuntimeLinker::LinkStatusLibraryName();
+#else
+ return "no library (ffmpeg disabled during build)";
+#endif
+}
+
+static const char* GetLinkStatusString() {
+#if defined(MOZ_FFMPEG)
+ return FFmpegRuntimeLinker::LinkStatusString();
+#else
+ return "no link (ffmpeg disabled during build)";
+#endif
+}
+
+void DecoderDoctorDocumentWatcher::SynthesizeFakeAnalysis() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsAutoCString errorType;
+ Preferences::GetCString("media.decoder-doctor.testing.fake-error", errorType);
+ MOZ_ASSERT(!errorType.IsEmpty());
+ for (const auto& id : sAllNotificationsAndReportStringIds) {
+ if (strcmp(id->mReportStringId, errorType.get()) == 0) {
+ if (id->mNotificationType ==
+ dom::DecoderDoctorNotificationType::Decode_error) {
+ ReportAnalysis(mDocument, *id, false /* isSolved */, u"*"_ns,
+ NS_ERROR_DOM_MEDIA_DECODE_ERR, true /* IsDecodeError */);
+ } else {
+ ReportAnalysis(mDocument, *id, false /* isSolved */, u"*"_ns, NS_OK,
+ false /* IsDecodeError */);
+ }
+ return;
+ }
+ }
+}
+
+void DecoderDoctorDocumentWatcher::SynthesizeAnalysis() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsAutoString playableFormats;
+ nsAutoString unplayableFormats;
+ // Subsets of unplayableFormats that require a specific platform decoder:
+ nsAutoString formatsRequiringWMF;
+ nsAutoString formatsRequiringFFMpeg;
+ nsAutoString formatsLibAVCodecUnsupported;
+ nsAutoString supportedKeySystems;
+ nsAutoString unsupportedKeySystems;
+ DecoderDoctorDiagnostics::KeySystemIssue lastKeySystemIssue =
+ DecoderDoctorDiagnostics::eUnset;
+ // Only deal with one decode error per document (the first one found).
+ const MediaResult* firstDecodeError = nullptr;
+ const nsString* firstDecodeErrorMediaSrc = nullptr;
+ // Only deal with one decode warning per document (the first one found).
+ const MediaResult* firstDecodeWarning = nullptr;
+ const nsString* firstDecodeWarningMediaSrc = nullptr;
+
+ for (const auto& diag : mDiagnosticsSequence) {
+ switch (diag.mDecoderDoctorDiagnostics.Type()) {
+ case DecoderDoctorDiagnostics::eFormatSupportCheck:
+ if (diag.mDecoderDoctorDiagnostics.CanPlay()) {
+ AppendToFormatsList(playableFormats,
+ diag.mDecoderDoctorDiagnostics.Format());
+ } else {
+ AppendToFormatsList(unplayableFormats,
+ diag.mDecoderDoctorDiagnostics.Format());
+ if (diag.mDecoderDoctorDiagnostics.DidWMFFailToLoad()) {
+ AppendToFormatsList(formatsRequiringWMF,
+ diag.mDecoderDoctorDiagnostics.Format());
+ } else if (diag.mDecoderDoctorDiagnostics.DidFFmpegNotFound()) {
+ AppendToFormatsList(formatsRequiringFFMpeg,
+ diag.mDecoderDoctorDiagnostics.Format());
+ } else if (diag.mDecoderDoctorDiagnostics.IsLibAVCodecUnsupported()) {
+ AppendToFormatsList(formatsLibAVCodecUnsupported,
+ diag.mDecoderDoctorDiagnostics.Format());
+ }
+ }
+ break;
+ case DecoderDoctorDiagnostics::eMediaKeySystemAccessRequest:
+ if (diag.mDecoderDoctorDiagnostics.IsKeySystemSupported()) {
+ AppendToFormatsList(supportedKeySystems,
+ diag.mDecoderDoctorDiagnostics.KeySystem());
+ } else {
+ AppendToFormatsList(unsupportedKeySystems,
+ diag.mDecoderDoctorDiagnostics.KeySystem());
+ DecoderDoctorDiagnostics::KeySystemIssue issue =
+ diag.mDecoderDoctorDiagnostics.GetKeySystemIssue();
+ if (issue != DecoderDoctorDiagnostics::eUnset) {
+ lastKeySystemIssue = issue;
+ }
+ }
+ break;
+ case DecoderDoctorDiagnostics::eEvent:
+ MOZ_ASSERT_UNREACHABLE("Events shouldn't be stored for processing.");
+ break;
+ case DecoderDoctorDiagnostics::eDecodeError:
+ if (!firstDecodeError) {
+ firstDecodeError = &diag.mDecoderDoctorDiagnostics.DecodeIssue();
+ firstDecodeErrorMediaSrc =
+ &diag.mDecoderDoctorDiagnostics.DecodeIssueMediaSrc();
+ }
+ break;
+ case DecoderDoctorDiagnostics::eDecodeWarning:
+ if (!firstDecodeWarning) {
+ firstDecodeWarning = &diag.mDecoderDoctorDiagnostics.DecodeIssue();
+ firstDecodeWarningMediaSrc =
+ &diag.mDecoderDoctorDiagnostics.DecodeIssueMediaSrc();
+ }
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unhandled DecoderDoctorDiagnostics type");
+ break;
+ }
+ }
+
+ // Check if issues have been solved, by finding if some now-playable
+ // key systems or formats were previously recorded as having issues.
+ if (!supportedKeySystems.IsEmpty() || !playableFormats.IsEmpty()) {
+ DD_DEBUG(
+ "DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - "
+ "supported key systems '%s', playable formats '%s'; See if they show "
+ "issues have been solved...",
+ this, mDocument, NS_ConvertUTF16toUTF8(supportedKeySystems).Data(),
+ NS_ConvertUTF16toUTF8(playableFormats).get());
+ const nsAString* workingFormatsArray[] = {&supportedKeySystems,
+ &playableFormats};
+ // For each type of notification, retrieve the pref that contains formats/
+ // key systems with issues.
+ for (const NotificationAndReportStringId* id :
+ sAllNotificationsAndReportStringIds) {
+ nsAutoCString formatsPref("media.decoder-doctor.");
+ formatsPref += id->mReportStringId;
+ formatsPref += ".formats";
+ nsAutoString formatsWithIssues;
+ Preferences::GetString(formatsPref.Data(), formatsWithIssues);
+ if (formatsWithIssues.IsEmpty()) {
+ continue;
+ }
+ // See if that list of formats-with-issues contains any formats that are
+ // now playable/supported.
+ bool solved = false;
+ for (const nsAString* workingFormats : workingFormatsArray) {
+ for (const auto& workingFormat : MakeStringListRange(*workingFormats)) {
+ if (FormatsListContains(formatsWithIssues, workingFormat)) {
+ // This now-working format used not to work -> Report solved issue.
+ DD_INFO(
+ "DecoderDoctorDocumentWatcher[%p, "
+ "doc=%p]::SynthesizeAnalysis() - %s solved ('%s' now works, it "
+ "was in pref(%s)='%s')",
+ this, mDocument, id->mReportStringId,
+ NS_ConvertUTF16toUTF8(workingFormat).get(), formatsPref.Data(),
+ NS_ConvertUTF16toUTF8(formatsWithIssues).get());
+ ReportAnalysis(mDocument, *id, true, workingFormat);
+ // This particular Notification&ReportId has been solved, no need
+ // to keep looking at other keysys/formats that might solve it too.
+ solved = true;
+ break;
+ }
+ }
+ if (solved) {
+ break;
+ }
+ }
+ if (!solved) {
+ DD_DEBUG(
+ "DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - "
+ "%s not solved (pref(%s)='%s')",
+ this, mDocument, id->mReportStringId, formatsPref.Data(),
+ NS_ConvertUTF16toUTF8(formatsWithIssues).get());
+ }
+ }
+ }
+
+ // Look at Key System issues first, as they take precedence over format
+ // checks.
+ if (!unsupportedKeySystems.IsEmpty() && supportedKeySystems.IsEmpty()) {
+ // No supported key systems!
+ switch (lastKeySystemIssue) {
+ case DecoderDoctorDiagnostics::eWidevineWithNoWMF:
+ DD_INFO(
+ "DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - "
+ "unsupported key systems: %s, Widevine without WMF",
+ this, mDocument,
+ NS_ConvertUTF16toUTF8(unsupportedKeySystems).get());
+ ReportAnalysis(mDocument, sMediaWidevineNoWMF, false,
+ unsupportedKeySystems);
+ return;
+ default:
+ break;
+ }
+ }
+
+ // Next, check playability of requested formats.
+ if (!unplayableFormats.IsEmpty()) {
+ // Some requested formats cannot be played.
+ if (playableFormats.IsEmpty()) {
+ // No requested formats can be played. See if we can help the user, by
+ // going through expected decoders from most to least desirable.
+ if (!formatsRequiringWMF.IsEmpty()) {
+ DD_INFO(
+ "DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - "
+ "unplayable formats: %s -> Cannot play media because WMF was not "
+ "found",
+ this, mDocument, NS_ConvertUTF16toUTF8(formatsRequiringWMF).get());
+ ReportAnalysis(mDocument, sMediaWMFNeeded, false, formatsRequiringWMF);
+ return;
+ }
+ if (!formatsRequiringFFMpeg.IsEmpty()) {
+ MOZ_DIAGNOSTIC_ASSERT(formatsLibAVCodecUnsupported.IsEmpty());
+ DD_INFO(
+ "DecoderDoctorDocumentWatcher[%p, "
+ "doc=%p]::SynthesizeAnalysis() - unplayable formats: %s -> "
+ "Cannot play media because ffmpeg was not found (Reason: %s)",
+ this, mDocument,
+ NS_ConvertUTF16toUTF8(formatsRequiringFFMpeg).get(),
+ GetLinkStatusString());
+ ReportAnalysis(mDocument, sMediaFFMpegNotFound, false,
+ formatsRequiringFFMpeg);
+ return;
+ }
+ if (!formatsLibAVCodecUnsupported.IsEmpty()) {
+ MOZ_DIAGNOSTIC_ASSERT(formatsRequiringFFMpeg.IsEmpty());
+ DD_INFO(
+ "DecoderDoctorDocumentWatcher[%p, "
+ "doc=%p]::SynthesizeAnalysis() - unplayable formats: %s -> "
+ "Cannot play media because of unsupported %s (Reason: %s)",
+ this, mDocument,
+ NS_ConvertUTF16toUTF8(formatsLibAVCodecUnsupported).get(),
+ GetLinkStatusLibraryName(), GetLinkStatusString());
+ ReportAnalysis(mDocument, sUnsupportedLibavcodec, false,
+ formatsLibAVCodecUnsupported);
+ return;
+ }
+ DD_INFO(
+ "DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - "
+ "Cannot play media, unplayable formats: %s",
+ this, mDocument, NS_ConvertUTF16toUTF8(unplayableFormats).get());
+ ReportAnalysis(mDocument, sMediaCannotPlayNoDecoders, false,
+ unplayableFormats);
+ return;
+ }
+
+ DD_INFO(
+ "DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - Can "
+ "play media, but no decoders for some requested formats: %s",
+ this, mDocument, NS_ConvertUTF16toUTF8(unplayableFormats).get());
+ if (Preferences::GetBool("media.decoder-doctor.verbose", false)) {
+ ReportAnalysis(mDocument, sMediaNoDecoders, false, unplayableFormats);
+ }
+ return;
+ }
+
+ if (firstDecodeError) {
+ DD_INFO(
+ "DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - "
+ "Decode error: %s",
+ this, mDocument, firstDecodeError->Description().get());
+ ReportAnalysis(mDocument, sMediaDecodeError, false, u""_ns,
+ *firstDecodeError,
+ true, // aDecodeIssueIsError=true
+ mDocument->GetDocumentURI()->GetSpecOrDefault(),
+ *firstDecodeErrorMediaSrc);
+ return;
+ }
+
+ if (firstDecodeWarning) {
+ DD_INFO(
+ "DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - "
+ "Decode warning: %s",
+ this, mDocument, firstDecodeWarning->Description().get());
+ ReportAnalysis(mDocument, sMediaDecodeWarning, false, u""_ns,
+ *firstDecodeWarning,
+ false, // aDecodeIssueIsError=false
+ mDocument->GetDocumentURI()->GetSpecOrDefault(),
+ *firstDecodeWarningMediaSrc);
+ return;
+ }
+
+ DD_DEBUG(
+ "DecoderDoctorDocumentWatcher[%p, doc=%p]::SynthesizeAnalysis() - Can "
+ "play media, decoders available for all requested formats",
+ this, mDocument);
+}
+
+void DecoderDoctorDocumentWatcher::AddDiagnostics(
+ DecoderDoctorDiagnostics&& aDiagnostics, const char* aCallSite) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aDiagnostics.Type() != DecoderDoctorDiagnostics::eEvent);
+
+ if (!mDocument) {
+ return;
+ }
+
+ const mozilla::TimeStamp now = mozilla::TimeStamp::Now();
+
+ constexpr size_t MaxDiagnostics = 128;
+ constexpr double MaxSeconds = 10.0;
+ while (
+ mDiagnosticsSequence.Length() > MaxDiagnostics ||
+ (!mDiagnosticsSequence.IsEmpty() &&
+ (now - mDiagnosticsSequence[0].mTimeStamp).ToSeconds() > MaxSeconds)) {
+ // Too many, or too old.
+ mDiagnosticsSequence.RemoveElementAt(0);
+ if (mDiagnosticsHandled != 0) {
+ // Make sure Notify picks up the new element added below.
+ --mDiagnosticsHandled;
+ }
+ }
+
+ DD_DEBUG(
+ "DecoderDoctorDocumentWatcher[%p, "
+ "doc=%p]::AddDiagnostics(DecoderDoctorDiagnostics{%s}, call site '%s')",
+ this, mDocument, aDiagnostics.GetDescription().Data(), aCallSite);
+ mDiagnosticsSequence.AppendElement(
+ Diagnostics(std::move(aDiagnostics), aCallSite, now));
+ EnsureTimerIsStarted();
+}
+
+bool DecoderDoctorDocumentWatcher::ShouldSynthesizeFakeAnalysis() const {
+ if (!StaticPrefs::media_decoder_doctor_testing()) {
+ return false;
+ }
+ nsAutoCString errorType;
+ Preferences::GetCString("media.decoder-doctor.testing.fake-error", errorType);
+ return !errorType.IsEmpty();
+}
+
+NS_IMETHODIMP
+DecoderDoctorDocumentWatcher::Notify(nsITimer* timer) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(timer == mTimer);
+
+ // Forget timer. (Assuming timer keeps itself and us alive during this call.)
+ mTimer = nullptr;
+
+ if (!mDocument) {
+ return NS_OK;
+ }
+
+ if (mDiagnosticsSequence.Length() > mDiagnosticsHandled) {
+ // We have new diagnostic data.
+ mDiagnosticsHandled = mDiagnosticsSequence.Length();
+
+ if (ShouldSynthesizeFakeAnalysis()) {
+ SynthesizeFakeAnalysis();
+ } else {
+ SynthesizeAnalysis();
+ }
+
+ // Restart timer, to redo analysis or stop watching this document,
+ // depending on whether anything new happens.
+ EnsureTimerIsStarted();
+ } else {
+ DD_DEBUG(
+ "DecoderDoctorDocumentWatcher[%p, doc=%p]::Notify() - No new "
+ "diagnostics to analyze -> Stop watching",
+ this, mDocument);
+ // Stop watching this document, we don't expect more diagnostics for now.
+ // If more diagnostics come in, we'll treat them as another burst,
+ // separately. 'true' to remove the property from the document.
+ StopWatching(true);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+DecoderDoctorDocumentWatcher::GetName(nsACString& aName) {
+ aName.AssignLiteral("DecoderDoctorDocumentWatcher_timer");
+ return NS_OK;
+}
+
+void DecoderDoctorDiagnostics::StoreFormatDiagnostics(dom::Document* aDocument,
+ const nsAString& aFormat,
+ bool aCanPlay,
+ const char* aCallSite) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // Make sure we only store once.
+ MOZ_ASSERT(mDiagnosticsType == eUnsaved);
+ mDiagnosticsType = eFormatSupportCheck;
+
+ if (NS_WARN_IF(aFormat.Length() > 2048)) {
+ DD_WARN(
+ "DecoderDoctorDiagnostics[%p]::StoreFormatDiagnostics(Document* "
+ "aDocument=%p, format= TOO LONG! '%s', can-play=%d, call site '%s')",
+ aDocument, this, NS_ConvertUTF16toUTF8(aFormat).get(), aCanPlay,
+ aCallSite);
+ return;
+ }
+
+ if (NS_WARN_IF(!aDocument)) {
+ DD_WARN(
+ "DecoderDoctorDiagnostics[%p]::StoreFormatDiagnostics(Document* "
+ "aDocument=nullptr, format='%s', can-play=%d, call site '%s')",
+ this, NS_ConvertUTF16toUTF8(aFormat).get(), aCanPlay, aCallSite);
+ return;
+ }
+ if (NS_WARN_IF(aFormat.IsEmpty())) {
+ DD_WARN(
+ "DecoderDoctorDiagnostics[%p]::StoreFormatDiagnostics(Document* "
+ "aDocument=%p, format=<empty>, can-play=%d, call site '%s')",
+ this, aDocument, aCanPlay, aCallSite);
+ return;
+ }
+
+ RefPtr<DecoderDoctorDocumentWatcher> watcher =
+ DecoderDoctorDocumentWatcher::RetrieveOrCreate(aDocument);
+
+ if (NS_WARN_IF(!watcher)) {
+ DD_WARN(
+ "DecoderDoctorDiagnostics[%p]::StoreFormatDiagnostics(Document* "
+ "aDocument=%p, format='%s', can-play=%d, call site '%s') - Could not "
+ "create document watcher",
+ this, aDocument, NS_ConvertUTF16toUTF8(aFormat).get(), aCanPlay,
+ aCallSite);
+ return;
+ }
+
+ mFormat = aFormat;
+ if (aCanPlay) {
+ mFlags += Flags::CanPlay;
+ } else {
+ mFlags -= Flags::CanPlay;
+ }
+
+ // StoreDiagnostics should only be called once, after all data is available,
+ // so it is safe to std::move() from this object.
+ watcher->AddDiagnostics(std::move(*this), aCallSite);
+ // Even though it's moved-from, the type should stay set
+ // (Only used to ensure that we do store only once.)
+ MOZ_ASSERT(mDiagnosticsType == eFormatSupportCheck);
+}
+
+void DecoderDoctorDiagnostics::StoreMediaKeySystemAccess(
+ dom::Document* aDocument, const nsAString& aKeySystem, bool aIsSupported,
+ const char* aCallSite) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // Make sure we only store once.
+ MOZ_ASSERT(mDiagnosticsType == eUnsaved);
+ mDiagnosticsType = eMediaKeySystemAccessRequest;
+
+ if (NS_WARN_IF(aKeySystem.Length() > 2048)) {
+ DD_WARN(
+ "DecoderDoctorDiagnostics[%p]::StoreMediaKeySystemAccess(Document* "
+ "aDocument=%p, keysystem= TOO LONG! '%s', supported=%d, call site "
+ "'%s')",
+ aDocument, this, NS_ConvertUTF16toUTF8(aKeySystem).get(), aIsSupported,
+ aCallSite);
+ return;
+ }
+
+ if (NS_WARN_IF(!aDocument)) {
+ DD_WARN(
+ "DecoderDoctorDiagnostics[%p]::StoreMediaKeySystemAccess(Document* "
+ "aDocument=nullptr, keysystem='%s', supported=%d, call site '%s')",
+ this, NS_ConvertUTF16toUTF8(aKeySystem).get(), aIsSupported, aCallSite);
+ return;
+ }
+ if (NS_WARN_IF(aKeySystem.IsEmpty())) {
+ DD_WARN(
+ "DecoderDoctorDiagnostics[%p]::StoreMediaKeySystemAccess(Document* "
+ "aDocument=%p, keysystem=<empty>, supported=%d, call site '%s')",
+ this, aDocument, aIsSupported, aCallSite);
+ return;
+ }
+
+ RefPtr<DecoderDoctorDocumentWatcher> watcher =
+ DecoderDoctorDocumentWatcher::RetrieveOrCreate(aDocument);
+
+ if (NS_WARN_IF(!watcher)) {
+ DD_WARN(
+ "DecoderDoctorDiagnostics[%p]::StoreMediaKeySystemAccess(Document* "
+ "aDocument=%p, keysystem='%s', supported=%d, call site '%s') - Could "
+ "not create document watcher",
+ this, aDocument, NS_ConvertUTF16toUTF8(aKeySystem).get(), aIsSupported,
+ aCallSite);
+ return;
+ }
+
+ mKeySystem = aKeySystem;
+ mIsKeySystemSupported = aIsSupported;
+
+ // StoreMediaKeySystemAccess should only be called once, after all data is
+ // available, so it is safe to std::move() from this object.
+ watcher->AddDiagnostics(std::move(*this), aCallSite);
+ // Even though it's moved-from, the type should stay set
+ // (Only used to ensure that we do store only once.)
+ MOZ_ASSERT(mDiagnosticsType == eMediaKeySystemAccessRequest);
+}
+
+void DecoderDoctorDiagnostics::StoreEvent(dom::Document* aDocument,
+ const DecoderDoctorEvent& aEvent,
+ const char* aCallSite) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // Make sure we only store once.
+ MOZ_ASSERT(mDiagnosticsType == eUnsaved);
+ mDiagnosticsType = eEvent;
+ mEvent = aEvent;
+
+ if (NS_WARN_IF(!aDocument)) {
+ DD_WARN(
+ "DecoderDoctorDiagnostics[%p]::StoreEvent(Document* "
+ "aDocument=nullptr, aEvent=%s, call site '%s')",
+ this, GetDescription().get(), aCallSite);
+ return;
+ }
+
+ // Don't keep events for later processing, just handle them now.
+ switch (aEvent.mDomain) {
+ case DecoderDoctorEvent::eAudioSinkStartup:
+ if (aEvent.mResult == NS_ERROR_DOM_MEDIA_CUBEB_INITIALIZATION_ERR) {
+ DD_INFO(
+ "DecoderDoctorDocumentWatcher[%p, doc=%p]::AddDiagnostics() - "
+ "unable to initialize PulseAudio",
+ this, aDocument);
+ ReportAnalysis(aDocument, sCannotInitializePulseAudio, false, u"*"_ns);
+ } else if (aEvent.mResult == NS_OK) {
+ DD_INFO(
+ "DecoderDoctorDocumentWatcher[%p, doc=%p]::AddDiagnostics() - now "
+ "able to initialize PulseAudio",
+ this, aDocument);
+ ReportAnalysis(aDocument, sCannotInitializePulseAudio, true, u"*"_ns);
+ }
+ break;
+ }
+}
+
+void DecoderDoctorDiagnostics::StoreDecodeError(dom::Document* aDocument,
+ const MediaResult& aError,
+ const nsString& aMediaSrc,
+ const char* aCallSite) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // Make sure we only store once.
+ MOZ_ASSERT(mDiagnosticsType == eUnsaved);
+ mDiagnosticsType = eDecodeError;
+
+ if (NS_WARN_IF(aError.Message().Length() > 2048)) {
+ DD_WARN(
+ "DecoderDoctorDiagnostics[%p]::StoreDecodeError(Document* "
+ "aDocument=%p, aError= TOO LONG! '%s', aMediaSrc=<provided>, call site "
+ "'%s')",
+ aDocument, this, aError.Description().get(), aCallSite);
+ return;
+ }
+
+ if (NS_WARN_IF(aMediaSrc.Length() > 2048)) {
+ DD_WARN(
+ "DecoderDoctorDiagnostics[%p]::StoreDecodeError(Document* "
+ "aDocument=%p, aError=%s, aMediaSrc= TOO LONG! <provided>, call site "
+ "'%s')",
+ aDocument, this, aError.Description().get(), aCallSite);
+ return;
+ }
+
+ if (NS_WARN_IF(!aDocument)) {
+ DD_WARN(
+ "DecoderDoctorDiagnostics[%p]::StoreDecodeError("
+ "Document* aDocument=nullptr, aError=%s,"
+ " aMediaSrc=<provided>, call site '%s')",
+ this, aError.Description().get(), aCallSite);
+ return;
+ }
+
+ RefPtr<DecoderDoctorDocumentWatcher> watcher =
+ DecoderDoctorDocumentWatcher::RetrieveOrCreate(aDocument);
+
+ if (NS_WARN_IF(!watcher)) {
+ DD_WARN(
+ "DecoderDoctorDiagnostics[%p]::StoreDecodeError("
+ "Document* aDocument=%p, aError='%s', aMediaSrc=<provided>,"
+ " call site '%s') - Could not create document watcher",
+ this, aDocument, aError.Description().get(), aCallSite);
+ return;
+ }
+
+ mDecodeIssue = aError;
+ mDecodeIssueMediaSrc = aMediaSrc;
+
+ // StoreDecodeError should only be called once, after all data is
+ // available, so it is safe to std::move() from this object.
+ watcher->AddDiagnostics(std::move(*this), aCallSite);
+ // Even though it's moved-from, the type should stay set
+ // (Only used to ensure that we do store only once.)
+ MOZ_ASSERT(mDiagnosticsType == eDecodeError);
+}
+
+void DecoderDoctorDiagnostics::StoreDecodeWarning(dom::Document* aDocument,
+ const MediaResult& aWarning,
+ const nsString& aMediaSrc,
+ const char* aCallSite) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // Make sure we only store once.
+ MOZ_ASSERT(mDiagnosticsType == eUnsaved);
+ mDiagnosticsType = eDecodeWarning;
+
+ if (NS_WARN_IF(!aDocument)) {
+ DD_WARN(
+ "DecoderDoctorDiagnostics[%p]::StoreDecodeWarning("
+ "Document* aDocument=nullptr, aWarning=%s,"
+ " aMediaSrc=<provided>, call site '%s')",
+ this, aWarning.Description().get(), aCallSite);
+ return;
+ }
+
+ RefPtr<DecoderDoctorDocumentWatcher> watcher =
+ DecoderDoctorDocumentWatcher::RetrieveOrCreate(aDocument);
+
+ if (NS_WARN_IF(!watcher)) {
+ DD_WARN(
+ "DecoderDoctorDiagnostics[%p]::StoreDecodeWarning("
+ "Document* aDocument=%p, aWarning='%s', aMediaSrc=<provided>,"
+ " call site '%s') - Could not create document watcher",
+ this, aDocument, aWarning.Description().get(), aCallSite);
+ return;
+ }
+
+ mDecodeIssue = aWarning;
+ mDecodeIssueMediaSrc = aMediaSrc;
+
+ // StoreDecodeWarning should only be called once, after all data is
+ // available, so it is safe to std::move() from this object.
+ watcher->AddDiagnostics(std::move(*this), aCallSite);
+ // Even though it's moved-from, the type should stay set
+ // (Only used to ensure that we do store only once.)
+ MOZ_ASSERT(mDiagnosticsType == eDecodeWarning);
+}
+
+static const char* EventDomainString(DecoderDoctorEvent::Domain aDomain) {
+ switch (aDomain) {
+ case DecoderDoctorEvent::eAudioSinkStartup:
+ return "audio-sink-startup";
+ }
+ return "?";
+}
+
+nsCString DecoderDoctorDiagnostics::GetDescription() const {
+ nsCString s;
+ switch (mDiagnosticsType) {
+ case eUnsaved:
+ s = "Unsaved diagnostics, cannot get accurate description";
+ break;
+ case eFormatSupportCheck:
+ s = "format='";
+ s += NS_ConvertUTF16toUTF8(mFormat).get();
+ s += mFlags.contains(Flags::CanPlay) ? "', can play" : "', cannot play";
+ if (mFlags.contains(Flags::VideoNotSupported)) {
+ s += ", but video format not supported";
+ }
+ if (mFlags.contains(Flags::AudioNotSupported)) {
+ s += ", but audio format not supported";
+ }
+ if (mFlags.contains(Flags::WMFFailedToLoad)) {
+ s += ", Windows platform decoder failed to load";
+ }
+ if (mFlags.contains(Flags::FFmpegNotFound)) {
+ s += ", Linux platform decoder not found";
+ }
+ if (mFlags.contains(Flags::GMPPDMFailedToStartup)) {
+ s += ", GMP PDM failed to startup";
+ } else if (!mGMP.IsEmpty()) {
+ s += ", Using GMP '";
+ s += mGMP;
+ s += "'";
+ }
+ break;
+ case eMediaKeySystemAccessRequest:
+ s = "key system='";
+ s += NS_ConvertUTF16toUTF8(mKeySystem).get();
+ s += mIsKeySystemSupported ? "', supported" : "', not supported";
+ switch (mKeySystemIssue) {
+ case eUnset:
+ break;
+ case eWidevineWithNoWMF:
+ s += ", Widevine with no WMF";
+ break;
+ }
+ break;
+ case eEvent:
+ s = nsPrintfCString("event domain %s result=%" PRIu32,
+ EventDomainString(mEvent.mDomain),
+ static_cast<uint32_t>(mEvent.mResult));
+ break;
+ case eDecodeError:
+ s = "decode error: ";
+ s += mDecodeIssue.Description();
+ s += ", src='";
+ s += mDecodeIssueMediaSrc.IsEmpty() ? "<none>" : "<provided>";
+ s += "'";
+ break;
+ case eDecodeWarning:
+ s = "decode warning: ";
+ s += mDecodeIssue.Description();
+ s += ", src='";
+ s += mDecodeIssueMediaSrc.IsEmpty() ? "<none>" : "<provided>";
+ s += "'";
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unexpected DiagnosticsType");
+ s = "?";
+ break;
+ }
+ return s;
+}
+
+static const char* ToDecoderDoctorReportTypeStr(
+ const dom::DecoderDoctorReportType& aType) {
+ switch (aType) {
+ case dom::DecoderDoctorReportType::Mediawidevinenowmf:
+ return sMediaWidevineNoWMF.mReportStringId;
+ case dom::DecoderDoctorReportType::Mediawmfneeded:
+ return sMediaWMFNeeded.mReportStringId;
+ case dom::DecoderDoctorReportType::Mediaplatformdecodernotfound:
+ return sMediaFFMpegNotFound.mReportStringId;
+ case dom::DecoderDoctorReportType::Mediacannotplaynodecoders:
+ return sMediaCannotPlayNoDecoders.mReportStringId;
+ case dom::DecoderDoctorReportType::Medianodecoders:
+ return sMediaNoDecoders.mReportStringId;
+ case dom::DecoderDoctorReportType::Mediacannotinitializepulseaudio:
+ return sCannotInitializePulseAudio.mReportStringId;
+ case dom::DecoderDoctorReportType::Mediaunsupportedlibavcodec:
+ return sUnsupportedLibavcodec.mReportStringId;
+ case dom::DecoderDoctorReportType::Mediadecodeerror:
+ return sMediaDecodeError.mReportStringId;
+ case dom::DecoderDoctorReportType::Mediadecodewarning:
+ return sMediaDecodeWarning.mReportStringId;
+ default:
+ DD_DEBUG("Invalid report type to str");
+ return "invalid-report-type";
+ }
+}
+
+void DecoderDoctorDiagnostics::SetDecoderDoctorReportType(
+ const dom::DecoderDoctorReportType& aType) {
+ DD_INFO("Set report type %s", ToDecoderDoctorReportTypeStr(aType));
+ switch (aType) {
+ case dom::DecoderDoctorReportType::Mediawmfneeded:
+ SetWMFFailedToLoad();
+ return;
+ case dom::DecoderDoctorReportType::Mediaplatformdecodernotfound:
+ SetFFmpegNotFound();
+ return;
+ case dom::DecoderDoctorReportType::Mediaunsupportedlibavcodec:
+ SetLibAVCodecUnsupported();
+ return;
+ case dom::DecoderDoctorReportType::Mediacannotplaynodecoders:
+ case dom::DecoderDoctorReportType::Medianodecoders:
+ // Do nothing, because these type are related with can-play, which would
+ // be handled in `StoreFormatDiagnostics()` when sending `false` in the
+ // parameter for the canplay.
+ return;
+ default:
+ DD_DEBUG("Not supported type");
+ return;
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/doctor/DecoderDoctorDiagnostics.h b/dom/media/doctor/DecoderDoctorDiagnostics.h
new file mode 100644
index 0000000000..dee63a6f1a
--- /dev/null
+++ b/dom/media/doctor/DecoderDoctorDiagnostics.h
@@ -0,0 +1,167 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DecoderDoctorDiagnostics_h_
+#define DecoderDoctorDiagnostics_h_
+
+#include "MediaResult.h"
+#include "mozilla/DefineEnum.h"
+#include "mozilla/EnumSet.h"
+#include "mozilla/EnumTypeTraits.h"
+#include "mozilla/dom/DecoderDoctorNotificationBinding.h"
+#include "nsString.h"
+
+namespace mozilla {
+
+namespace dom {
+class Document;
+}
+
+struct DecoderDoctorEvent {
+ enum Domain {
+ eAudioSinkStartup,
+ } mDomain;
+ nsresult mResult;
+};
+
+// DecoderDoctorDiagnostics class, used to gather data from PDMs/DecoderTraits,
+// and then notify the user about issues preventing (or worsening) playback.
+//
+// The expected usage is:
+// 1. Instantiate a DecoderDoctorDiagnostics in a function (close to the point
+// where a webpage is trying to know whether some MIME types can be played,
+// or trying to play a media file).
+// 2. Pass a pointer to the DecoderDoctorDiagnostics structure to one of the
+// CanPlayStatus/IsTypeSupported/(others?). During that call, some PDMs may
+// add relevant diagnostic information.
+// 3. Analyze the collected diagnostics, and optionally dispatch an event to the
+// UX, to notify the user about potential playback issues and how to resolve
+// them.
+//
+// This class' methods must be called from the main thread.
+
+class DecoderDoctorDiagnostics {
+ friend struct IPC::ParamTraits<mozilla::DecoderDoctorDiagnostics>;
+
+ public:
+ // Store the diagnostic information collected so far on a document for a
+ // given format. All diagnostics for a document will be analyzed together
+ // within a short timeframe.
+ // Should only be called once.
+ void StoreFormatDiagnostics(dom::Document* aDocument,
+ const nsAString& aFormat, bool aCanPlay,
+ const char* aCallSite);
+
+ void StoreMediaKeySystemAccess(dom::Document* aDocument,
+ const nsAString& aKeySystem, bool aIsSupported,
+ const char* aCallSite);
+
+ void StoreEvent(dom::Document* aDocument, const DecoderDoctorEvent& aEvent,
+ const char* aCallSite);
+
+ void StoreDecodeError(dom::Document* aDocument, const MediaResult& aError,
+ const nsString& aMediaSrc, const char* aCallSite);
+
+ void StoreDecodeWarning(dom::Document* aDocument, const MediaResult& aWarning,
+ const nsString& aMediaSrc, const char* aCallSite);
+
+ enum DiagnosticsType {
+ eUnsaved,
+ eFormatSupportCheck,
+ eMediaKeySystemAccessRequest,
+ eEvent,
+ eDecodeError,
+ eDecodeWarning
+ };
+ DiagnosticsType Type() const { return mDiagnosticsType; }
+
+ // Description string, for logging purposes; only call on stored diags.
+ nsCString GetDescription() const;
+
+ // Methods to record diagnostic information:
+
+ MOZ_DEFINE_ENUM_CLASS_AT_CLASS_SCOPE(
+ Flags, (CanPlay, WMFFailedToLoad, FFmpegNotFound, LibAVCodecUnsupported,
+ GMPPDMFailedToStartup, VideoNotSupported, AudioNotSupported));
+ using FlagsSet = mozilla::EnumSet<Flags>;
+
+ const nsAString& Format() const { return mFormat; }
+ bool CanPlay() const { return mFlags.contains(Flags::CanPlay); }
+
+ void SetFailureFlags(const FlagsSet& aFlags) { mFlags = aFlags; }
+ void SetWMFFailedToLoad() { mFlags += Flags::WMFFailedToLoad; }
+ bool DidWMFFailToLoad() const {
+ return mFlags.contains(Flags::WMFFailedToLoad);
+ }
+
+ void SetFFmpegNotFound() { mFlags += Flags::FFmpegNotFound; }
+ bool DidFFmpegNotFound() const {
+ return mFlags.contains(Flags::FFmpegNotFound);
+ }
+
+ void SetLibAVCodecUnsupported() { mFlags += Flags::LibAVCodecUnsupported; }
+ bool IsLibAVCodecUnsupported() const {
+ return mFlags.contains(Flags::LibAVCodecUnsupported);
+ }
+
+ void SetGMPPDMFailedToStartup() { mFlags += Flags::GMPPDMFailedToStartup; }
+ bool DidGMPPDMFailToStartup() const {
+ return mFlags.contains(Flags::GMPPDMFailedToStartup);
+ }
+
+ void SetVideoNotSupported() { mFlags += Flags::VideoNotSupported; }
+ void SetAudioNotSupported() { mFlags += Flags::AudioNotSupported; }
+
+ void SetGMP(const nsACString& aGMP) { mGMP = aGMP; }
+ const nsACString& GMP() const { return mGMP; }
+
+ const nsAString& KeySystem() const { return mKeySystem; }
+ bool IsKeySystemSupported() const { return mIsKeySystemSupported; }
+ enum KeySystemIssue { eUnset, eWidevineWithNoWMF };
+ void SetKeySystemIssue(KeySystemIssue aKeySystemIssue) {
+ mKeySystemIssue = aKeySystemIssue;
+ }
+ KeySystemIssue GetKeySystemIssue() const { return mKeySystemIssue; }
+
+ DecoderDoctorEvent event() const { return mEvent; }
+
+ const MediaResult& DecodeIssue() const { return mDecodeIssue; }
+ const nsString& DecodeIssueMediaSrc() const { return mDecodeIssueMediaSrc; }
+
+ // This method is only used for testing.
+ void SetDecoderDoctorReportType(const dom::DecoderDoctorReportType& aType);
+
+ private:
+ // Currently-known type of diagnostics. Set from one of the 'Store...'
+ // methods. This helps ensure diagnostics are only stored once, and makes it
+ // easy to know what information they contain.
+ DiagnosticsType mDiagnosticsType = eUnsaved;
+
+ nsString mFormat;
+ FlagsSet mFlags;
+ nsCString mGMP;
+
+ nsString mKeySystem;
+ bool mIsKeySystemSupported = false;
+ KeySystemIssue mKeySystemIssue = eUnset;
+
+ DecoderDoctorEvent mEvent;
+
+ MediaResult mDecodeIssue = NS_OK;
+ nsString mDecodeIssueMediaSrc;
+};
+
+// Used for IPDL serialization.
+// The 'value' have to be the biggest enum from DecoderDoctorDiagnostics::Flags.
+template <>
+struct MaxEnumValue<::mozilla::DecoderDoctorDiagnostics::Flags> {
+ static constexpr unsigned int value =
+ static_cast<unsigned int>(DecoderDoctorDiagnostics::sFlagsCount);
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/doctor/DecoderDoctorLogger.cpp b/dom/media/doctor/DecoderDoctorLogger.cpp
new file mode 100644
index 0000000000..927650babc
--- /dev/null
+++ b/dom/media/doctor/DecoderDoctorLogger.cpp
@@ -0,0 +1,176 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DecoderDoctorLogger.h"
+
+#include "DDLogUtils.h"
+#include "DDMediaLogs.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/Unused.h"
+
+namespace mozilla {
+
+/* static */ Atomic<DecoderDoctorLogger::LogState, ReleaseAcquire>
+ DecoderDoctorLogger::sLogState{DecoderDoctorLogger::scDisabled};
+
+/* static */ const char* DecoderDoctorLogger::sShutdownReason = nullptr;
+
+static DDMediaLogs* sMediaLogs;
+
+/* static */
+void DecoderDoctorLogger::Init() {
+ MOZ_ASSERT(static_cast<LogState>(sLogState) == scDisabled);
+ if (MOZ_LOG_TEST(sDecoderDoctorLoggerLog, LogLevel::Error) ||
+ MOZ_LOG_TEST(sDecoderDoctorLoggerEndLog, LogLevel::Error)) {
+ EnableLogging();
+ }
+}
+
+// First DDLogShutdowner sets sLogState to scShutdown, to prevent further
+// logging.
+struct DDLogShutdowner {
+ ~DDLogShutdowner() {
+ DDL_INFO("Shutting down");
+ // Prevent further logging, some may racily seep in, it's fine as the
+ // logging infrastructure would still be alive until DDLogDeleter runs.
+ DecoderDoctorLogger::ShutdownLogging();
+ }
+};
+static UniquePtr<DDLogShutdowner> sDDLogShutdowner;
+
+// Later DDLogDeleter will delete the message queue and media logs.
+struct DDLogDeleter {
+ ~DDLogDeleter() {
+ if (sMediaLogs) {
+ DDL_INFO("Final processing of collected logs");
+ delete sMediaLogs;
+ sMediaLogs = nullptr;
+ }
+ }
+};
+static UniquePtr<DDLogDeleter> sDDLogDeleter;
+
+/* static */
+void DecoderDoctorLogger::PanicInternal(const char* aReason, bool aDontBlock) {
+ for (;;) {
+ const LogState state = static_cast<LogState>(sLogState);
+ if (state == scEnabling && !aDontBlock) {
+ // Wait for the end of the enabling process (unless we're in it, in which
+ // case we don't want to block.)
+ continue;
+ }
+ if (state == scShutdown) {
+ // Already shutdown, nothing more to do.
+ break;
+ }
+ if (sLogState.compareExchange(state, scShutdown)) {
+ // We are the one performing the first shutdown -> Record reason.
+ sShutdownReason = aReason;
+ // Free as much memory as possible.
+ if (sMediaLogs) {
+ // Shutdown the medialogs processing thread, and free as much memory
+ // as possible.
+ sMediaLogs->Panic();
+ }
+ // sMediaLogs and sQueue will be deleted by DDLogDeleter.
+ // We don't want to delete them right now, because there could be a race
+ // where another thread started logging or retrieving logs before we
+ // changed the state to scShutdown, but has been delayed before actually
+ // trying to write or read log messages, thereby causing a UAF.
+ }
+ // If someone else changed the state, we'll just loop around, and either
+ // shutdown already happened elsewhere, or we'll try to shutdown again.
+ }
+}
+
+/* static */
+bool DecoderDoctorLogger::EnsureLogIsEnabled() {
+#ifdef RELEASE_OR_BETA
+ // Just refuse to enable DDLogger on release&beta because it makes it too easy
+ // to trigger an OOM. See bug 1571648.
+ return false;
+#else
+ for (;;) {
+ LogState state = static_cast<LogState>(sLogState);
+ switch (state) {
+ case scDisabled:
+ // Currently disabled, try to be the one to enable.
+ if (sLogState.compareExchange(scDisabled, scEnabling)) {
+ // We are the one to enable logging, state won't change (except for
+ // possible shutdown.)
+ // Create DDMediaLogs singleton, which will process the message queue.
+ DDMediaLogs::ConstructionResult mediaLogsConstruction =
+ DDMediaLogs::New();
+ if (NS_FAILED(mediaLogsConstruction.mRv)) {
+ PanicInternal("Failed to enable logging", /* aDontBlock */ true);
+ return false;
+ }
+ MOZ_ASSERT(mediaLogsConstruction.mMediaLogs);
+ sMediaLogs = mediaLogsConstruction.mMediaLogs;
+ // Setup shutdown-time clean-up.
+ MOZ_ALWAYS_SUCCEEDS(SchedulerGroup::Dispatch(
+ TaskCategory::Other,
+ NS_NewRunnableFunction("DDLogger shutdown setup", [] {
+ sDDLogShutdowner = MakeUnique<DDLogShutdowner>();
+ ClearOnShutdown(&sDDLogShutdowner,
+ ShutdownPhase::XPCOMShutdown);
+ sDDLogDeleter = MakeUnique<DDLogDeleter>();
+ ClearOnShutdown(&sDDLogDeleter,
+ ShutdownPhase::XPCOMShutdownThreads);
+ })));
+
+ // Nobody else should change the state when *we* are enabling logging.
+ MOZ_ASSERT(sLogState == scEnabling);
+ sLogState = scEnabled;
+ DDL_INFO("Logging enabled");
+ return true;
+ }
+ // Someone else changed the state before our compareExchange, just loop
+ // around to examine the new situation.
+ break;
+ case scEnabled:
+ return true;
+ case scEnabling:
+ // Someone else is currently enabling logging, actively wait by just
+ // looping, until the state changes.
+ break;
+ case scShutdown:
+ // Shutdown is non-recoverable, we cannot enable logging again.
+ return false;
+ }
+ // Not returned yet, loop around to examine the new situation.
+ }
+#endif
+}
+
+/* static */
+void DecoderDoctorLogger::EnableLogging() { Unused << EnsureLogIsEnabled(); }
+
+/* static */ RefPtr<DecoderDoctorLogger::LogMessagesPromise>
+DecoderDoctorLogger::RetrieveMessages(
+ const dom::HTMLMediaElement* aMediaElement) {
+ if (MOZ_UNLIKELY(!EnsureLogIsEnabled())) {
+ DDL_WARN("Request (for %p) but there are no logs", aMediaElement);
+ return DecoderDoctorLogger::LogMessagesPromise::CreateAndReject(
+ NS_ERROR_DOM_MEDIA_ABORT_ERR, __func__);
+ }
+ return sMediaLogs->RetrieveMessages(aMediaElement);
+}
+
+/* static */
+void DecoderDoctorLogger::Log(const char* aSubjectTypeName,
+ const void* aSubjectPointer,
+ DDLogCategory aCategory, const char* aLabel,
+ DDLogValue&& aValue) {
+ if (IsDDLoggingEnabled()) {
+ MOZ_ASSERT(sMediaLogs);
+ sMediaLogs->Log(aSubjectTypeName, aSubjectPointer, aCategory, aLabel,
+ std::move(aValue));
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/doctor/DecoderDoctorLogger.h b/dom/media/doctor/DecoderDoctorLogger.h
new file mode 100644
index 0000000000..88a8c0c87f
--- /dev/null
+++ b/dom/media/doctor/DecoderDoctorLogger.h
@@ -0,0 +1,472 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DecoderDoctorLogger_h_
+#define DecoderDoctorLogger_h_
+
+#include "DDLoggedTypeTraits.h"
+#include "DDLogCategory.h"
+#include "DDLogValue.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/DefineEnum.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/NonDereferenceable.h"
+#include "nsString.h"
+
+namespace mozilla {
+
+// Main class used to capture log messages from the media stack, and to
+// retrieve processed messages associated with an HTMLMediaElement.
+//
+// The logging APIs are designed to work as fast as possible (in most cases
+// only checking a couple of atomic variables, and not allocating memory), so
+// as not to introduce perceptible latency.
+// Consider using DDLOG...() macros, and IsDDLoggingEnabled(), to avoid any
+// unneeded work when logging is not enabled.
+//
+// Structural logging messages are used to determine when objects are created
+// and destroyed, and to link objects that depend on each other, ultimately
+// tying groups of objects and their messages to HTMLMediaElement objects.
+//
+// A separate thread processes log messages, and can asynchronously retrieve
+// processed messages that correspond to a given HTMLMediaElement.
+// That thread is also responsible for removing dated messages, so as not to
+// take too much memory.
+class DecoderDoctorLogger {
+ public:
+ // Called by nsLayoutStatics::Initialize() before any other media work.
+ // Pre-enables logging if MOZ_LOG requires DDLogger.
+ static void Init();
+
+ // Is logging currently enabled? This is tested anyway in all public `Log...`
+ // functions, but it may be used to prevent logging-only work in clients.
+ static inline bool IsDDLoggingEnabled() {
+ return MOZ_UNLIKELY(static_cast<LogState>(sLogState) == scEnabled);
+ }
+
+ // Shutdown logging. This will prevent more messages to be queued, but the
+ // already-queued messages may still get processed.
+ static void ShutdownLogging() { sLogState = scShutdown; }
+
+ // Something went horribly wrong, stop all logging and log processing.
+ static void Panic(const char* aReason) {
+ PanicInternal(aReason, /* aDontBlock */ false);
+ }
+
+ // Logging functions.
+ //
+ // All logging functions take:
+ // - The object that produces the message, either as a template type (for
+ // which a specialized DDLoggedTypeTraits exists), or a pointer and a type
+ // name (needed for inner classes that cannot specialize
+ // DDLoggedTypeTraits.)
+ // - A DDLogCategory defining the type of log message; some are used
+ // internally for capture the lifetime and linking of C++ objects, others
+ // are used to split messages into different domains.
+ // - A label (string literal).
+ // - An optional Variant value, see DDLogValue for the accepted types.
+ //
+ // The following `EagerLog...` functions always cause their arguments to be
+ // pre-evaluated even if logging is disabled, in which case runtime could be
+ // wasted. Consider using `DDLOG...` macros instead, or test
+ // `IsDDLoggingEnabled()` first.
+
+ template <typename Value>
+ static void EagerLogValue(const char* aSubjectTypeName,
+ const void* aSubjectPointer,
+ DDLogCategory aCategory, const char* aLabel,
+ Value&& aValue) {
+ Log(aSubjectTypeName, aSubjectPointer, aCategory, aLabel,
+ DDLogValue{std::forward<Value>(aValue)});
+ }
+
+ template <typename Subject, typename Value>
+ static void EagerLogValue(const Subject* aSubject, DDLogCategory aCategory,
+ const char* aLabel, Value&& aValue) {
+ EagerLogValue(DDLoggedTypeTraits<Subject>::Name(), aSubject, aCategory,
+ aLabel, std::forward<Value>(aValue));
+ }
+
+ // EagerLogValue that can explicitly take strings, as the templated function
+ // above confuses Variant when forwarding string literals.
+ static void EagerLogValue(const char* aSubjectTypeName,
+ const void* aSubjectPointer,
+ DDLogCategory aCategory, const char* aLabel,
+ const char* aValue) {
+ Log(aSubjectTypeName, aSubjectPointer, aCategory, aLabel,
+ DDLogValue{aValue});
+ }
+
+ template <typename Subject>
+ static void EagerLogValue(const Subject* aSubject, DDLogCategory aCategory,
+ const char* aLabel, const char* aValue) {
+ EagerLogValue(DDLoggedTypeTraits<Subject>::Name(), aSubject, aCategory,
+ aLabel, aValue);
+ }
+
+ static void EagerLogPrintf(const char* aSubjectTypeName,
+ const void* aSubjectPointer,
+ DDLogCategory aCategory, const char* aLabel,
+ const char* aString) {
+ Log(aSubjectTypeName, aSubjectPointer, aCategory, aLabel,
+ DDLogValue{nsCString{aString}});
+ }
+
+ template <typename... Args>
+ static void EagerLogPrintf(const char* aSubjectTypeName,
+ const void* aSubjectPointer,
+ DDLogCategory aCategory, const char* aLabel,
+ const char* aFormat, Args&&... aArgs) {
+ Log(aSubjectTypeName, aSubjectPointer, aCategory, aLabel,
+ DDLogValue{
+ nsCString{nsPrintfCString(aFormat, std::forward<Args>(aArgs)...)}});
+ }
+
+ template <typename Subject>
+ static void EagerLogPrintf(const Subject* aSubject, DDLogCategory aCategory,
+ const char* aLabel, const char* aString) {
+ EagerLogPrintf(DDLoggedTypeTraits<Subject>::Name(), aSubject, aCategory,
+ aLabel, aString);
+ }
+
+ template <typename Subject, typename... Args>
+ static void EagerLogPrintf(const Subject* aSubject, DDLogCategory aCategory,
+ const char* aLabel, const char* aFormat,
+ Args&&... aArgs) {
+ EagerLogPrintf(DDLoggedTypeTraits<Subject>::Name(), aSubject, aCategory,
+ aLabel, aFormat, std::forward<Args>(aArgs)...);
+ }
+
+ static void MozLogPrintf(const char* aSubjectTypeName,
+ const void* aSubjectPointer,
+ const LogModule* aLogModule, LogLevel aLogLevel,
+ const char* aString) {
+ Log(aSubjectTypeName, aSubjectPointer, CategoryForMozLogLevel(aLogLevel),
+ aLogModule->Name(), // LogModule name as label.
+ DDLogValue{nsCString{aString}});
+ MOZ_LOG(aLogModule, aLogLevel,
+ ("%s[%p] %s", aSubjectTypeName, aSubjectPointer, aString));
+ }
+
+ template <typename... Args>
+ static void MozLogPrintf(const char* aSubjectTypeName,
+ const void* aSubjectPointer,
+ const LogModule* aLogModule, LogLevel aLogLevel,
+ const char* aFormat, Args&&... aArgs) {
+ nsCString printed = nsPrintfCString(aFormat, std::forward<Args>(aArgs)...);
+ Log(aSubjectTypeName, aSubjectPointer, CategoryForMozLogLevel(aLogLevel),
+ aLogModule->Name(), // LogModule name as label.
+ DDLogValue{printed});
+ MOZ_LOG(aLogModule, aLogLevel,
+ ("%s[%p] %s", aSubjectTypeName, aSubjectPointer, printed.get()));
+ }
+
+ template <typename Subject>
+ static void MozLogPrintf(const Subject* aSubject, const LogModule* aLogModule,
+ LogLevel aLogLevel, const char* aString) {
+ MozLogPrintf(DDLoggedTypeTraits<Subject>::Name(), aSubject, aLogModule,
+ aLogLevel, aString);
+ }
+
+ template <typename Subject, typename... Args>
+ static void MozLogPrintf(const Subject* aSubject, const LogModule* aLogModule,
+ LogLevel aLogLevel, const char* aFormat,
+ Args&&... aArgs) {
+ MozLogPrintf(DDLoggedTypeTraits<Subject>::Name(), aSubject, aLogModule,
+ aLogLevel, aFormat, std::forward<Args>(aArgs)...);
+ }
+
+ // Special logging functions. Consider using DecoderDoctorLifeLogger to
+ // automatically capture constructions & destructions.
+
+ static void LogConstruction(const char* aSubjectTypeName,
+ const void* aSubjectPointer) {
+ Log(aSubjectTypeName, aSubjectPointer, DDLogCategory::_Construction, "",
+ DDLogValue{DDNoValue{}});
+ }
+
+ static void LogConstructionAndBase(const char* aSubjectTypeName,
+ const void* aSubjectPointer,
+ const char* aBaseTypeName,
+ const void* aBasePointer) {
+ Log(aSubjectTypeName, aSubjectPointer, DDLogCategory::_DerivedConstruction,
+ "", DDLogValue{DDLogObject{aBaseTypeName, aBasePointer}});
+ }
+
+ template <typename B>
+ static void LogConstructionAndBase(const char* aSubjectTypeName,
+ const void* aSubjectPointer,
+ const B* aBase) {
+ Log(aSubjectTypeName, aSubjectPointer, DDLogCategory::_DerivedConstruction,
+ "", DDLogValue{DDLogObject{DDLoggedTypeTraits<B>::Name(), aBase}});
+ }
+
+ template <typename Subject>
+ static void LogConstruction(NonDereferenceable<const Subject> aSubject) {
+ using Traits = DDLoggedTypeTraits<Subject>;
+ if (!Traits::HasBase::value) {
+ Log(DDLoggedTypeTraits<Subject>::Name(),
+ reinterpret_cast<const void*>(aSubject.value()),
+ DDLogCategory::_Construction, "", DDLogValue{DDNoValue{}});
+ } else {
+ Log(DDLoggedTypeTraits<Subject>::Name(),
+ reinterpret_cast<const void*>(aSubject.value()),
+ DDLogCategory::_DerivedConstruction, "",
+ DDLogValue{DDLogObject{
+ DDLoggedTypeTraits<typename Traits::BaseType>::Name(),
+ reinterpret_cast<const void*>(
+ NonDereferenceable<const typename Traits::BaseType>(aSubject)
+ .value())}});
+ }
+ }
+
+ template <typename Subject>
+ static void LogConstruction(const Subject* aSubject) {
+ LogConstruction(NonDereferenceable<const Subject>(aSubject));
+ }
+
+ static void LogDestruction(const char* aSubjectTypeName,
+ const void* aSubjectPointer) {
+ Log(aSubjectTypeName, aSubjectPointer, DDLogCategory::_Destruction, "",
+ DDLogValue{DDNoValue{}});
+ }
+
+ template <typename Subject>
+ static void LogDestruction(NonDereferenceable<const Subject> aSubject) {
+ Log(DDLoggedTypeTraits<Subject>::Name(),
+ reinterpret_cast<const void*>(aSubject.value()),
+ DDLogCategory::_Destruction, "", DDLogValue{DDNoValue{}});
+ }
+
+ template <typename Subject>
+ static void LogDestruction(const Subject* aSubject) {
+ LogDestruction(NonDereferenceable<const Subject>(aSubject));
+ }
+
+ template <typename P, typename C>
+ static void LinkParentAndChild(const P* aParent, const char* aLinkName,
+ const C* aChild) {
+ if (aChild) {
+ Log(DDLoggedTypeTraits<P>::Name(), aParent, DDLogCategory::_Link,
+ aLinkName,
+ DDLogValue{DDLogObject{DDLoggedTypeTraits<C>::Name(), aChild}});
+ }
+ }
+
+ template <typename C>
+ static void LinkParentAndChild(const char* aParentTypeName,
+ const void* aParentPointer,
+ const char* aLinkName, const C* aChild) {
+ if (aChild) {
+ Log(aParentTypeName, aParentPointer, DDLogCategory::_Link, aLinkName,
+ DDLogValue{DDLogObject{DDLoggedTypeTraits<C>::Name(), aChild}});
+ }
+ }
+
+ template <typename P>
+ static void LinkParentAndChild(const P* aParent, const char* aLinkName,
+ const char* aChildTypeName,
+ const void* aChildPointer) {
+ if (aChildPointer) {
+ Log(DDLoggedTypeTraits<P>::Name(), aParent, DDLogCategory::_Link,
+ aLinkName, DDLogValue{DDLogObject{aChildTypeName, aChildPointer}});
+ }
+ }
+
+ template <typename C>
+ static void UnlinkParentAndChild(const char* aParentTypeName,
+ const void* aParentPointer,
+ const C* aChild) {
+ if (aChild) {
+ Log(aParentTypeName, aParentPointer, DDLogCategory::_Unlink, "",
+ DDLogValue{DDLogObject{DDLoggedTypeTraits<C>::Name(), aChild}});
+ }
+ }
+
+ template <typename P, typename C>
+ static void UnlinkParentAndChild(const P* aParent, const C* aChild) {
+ if (aChild) {
+ Log(DDLoggedTypeTraits<P>::Name(), aParent, DDLogCategory::_Unlink, "",
+ DDLogValue{DDLogObject{DDLoggedTypeTraits<C>::Name(), aChild}});
+ }
+ }
+
+ // Retrieval functions.
+
+ // Enable logging, if not done already. No effect otherwise.
+ static void EnableLogging();
+
+ using LogMessagesPromise =
+ MozPromise<nsCString, nsresult, /* IsExclusive = */ true>;
+
+ // Retrieve all messages related to a given HTMLMediaElement object.
+ // This call will trigger a processing run (to ensure the most recent data
+ // will be returned), and the returned promise will be resolved with all
+ // relevant log messages and object lifetimes in a JSON string.
+ // The first call will enable logging, until shutdown.
+ static RefPtr<LogMessagesPromise> RetrieveMessages(
+ const dom::HTMLMediaElement* aMediaElement);
+
+ private:
+ // If logging is not enabled yet, initiate it, return true.
+ // If logging has been shutdown, don't start it, return false.
+ // Otherwise return true.
+ static bool EnsureLogIsEnabled();
+
+ // Note that this call may block while the state is scEnabling;
+ // set aDontBlock to true to avoid blocking, most importantly when the
+ // caller is the one doing the enabling, this would cause an endless loop.
+ static void PanicInternal(const char* aReason, bool aDontBlock);
+
+ static void Log(const char* aSubjectTypeName, const void* aSubjectPointer,
+ DDLogCategory aCategory, const char* aLabel,
+ DDLogValue&& aValue);
+
+ static void Log(const char* aSubjectTypeName, const void* aSubjectPointer,
+ const LogModule* aLogModule, LogLevel aLogLevel,
+ DDLogValue&& aValue);
+
+ static DDLogCategory CategoryForMozLogLevel(LogLevel aLevel) {
+ switch (aLevel) {
+ default:
+ case LogLevel::Error:
+ return DDLogCategory::MozLogError;
+ case LogLevel::Warning:
+ return DDLogCategory::MozLogWarning;
+ case LogLevel::Info:
+ return DDLogCategory::MozLogInfo;
+ case LogLevel::Debug:
+ return DDLogCategory::MozLogDebug;
+ case LogLevel::Verbose:
+ return DDLogCategory::MozLogVerbose;
+ }
+ }
+
+ using LogState = int;
+ // Currently disabled, may be enabled on request.
+ static constexpr LogState scDisabled = 0;
+ // Currently enabled (logging happens), may be shutdown.
+ static constexpr LogState scEnabled = 1;
+ // Still disabled, but one thread is working on enabling it, nobody else
+ // should interfere during this time.
+ static constexpr LogState scEnabling = 2;
+ // Shutdown, cannot be re-enabled.
+ static constexpr LogState scShutdown = 3;
+ // Current state.
+ // "ReleaseAcquire" because when changing to scEnabled, the just-created
+ // sMediaLogs must be accessible to consumers that see scEnabled.
+ static Atomic<LogState, ReleaseAcquire> sLogState;
+
+ // If non-null, reason for an abnormal shutdown.
+ static const char* sShutdownReason;
+};
+
+// Base class to automatically record a class lifetime. Usage:
+// class SomeClass : public DecoderDoctorLifeLogger<SomeClass>
+// {
+// ...
+template <typename T>
+class DecoderDoctorLifeLogger {
+ protected:
+ DecoderDoctorLifeLogger() {
+ DecoderDoctorLogger::LogConstruction(NonDereferenceable<const T>(this));
+ }
+ ~DecoderDoctorLifeLogger() {
+ DecoderDoctorLogger::LogDestruction(NonDereferenceable<const T>(this));
+ }
+};
+
+// Macros to help lazily-evaluate arguments, only after we have checked that
+// logging is enabled.
+
+// Log a single value; see DDLogValue for allowed types.
+#define DDLOG(_category, _label, _arg) \
+ do { \
+ if (DecoderDoctorLogger::IsDDLoggingEnabled()) { \
+ DecoderDoctorLogger::EagerLogValue(this, _category, _label, _arg); \
+ } \
+ } while (0)
+// Log a single value, with an EXplicit `this`.
+#define DDLOGEX(_this, _category, _label, _arg) \
+ do { \
+ if (DecoderDoctorLogger::IsDDLoggingEnabled()) { \
+ DecoderDoctorLogger::EagerLogValue(_this, _category, _label, _arg); \
+ } \
+ } while (0)
+// Log a single value, with EXplicit type name and `this`.
+#define DDLOGEX2(_typename, _this, _category, _label, _arg) \
+ do { \
+ if (DecoderDoctorLogger::IsDDLoggingEnabled()) { \
+ DecoderDoctorLogger::EagerLogValue(_typename, _this, _category, _label, \
+ _arg); \
+ } \
+ } while (0)
+
+#ifdef DEBUG
+// Do a printf format check in DEBUG, with the downside that side-effects (from
+// evaluating the arguments) may happen twice! Who would do that anyway?
+static void inline MOZ_FORMAT_PRINTF(1, 2) DDLOGPRCheck(const char*, ...) {}
+# define DDLOGPR_CHECK(_fmt, ...) DDLOGPRCheck(_fmt, ##__VA_ARGS__)
+#else
+# define DDLOGPR_CHECK(_fmt, ...)
+#endif
+
+// Log a printf'd string. Discouraged, please try using DDLOG instead.
+#define DDLOGPR(_category, _label, _format, ...) \
+ do { \
+ if (DecoderDoctorLogger::IsDDLoggingEnabled()) { \
+ DDLOGPR_CHECK(_format, ##__VA_ARGS__); \
+ DecoderDoctorLogger::EagerLogPrintf(this, _category, _label, _format, \
+ ##__VA_ARGS__); \
+ } \
+ } while (0)
+
+// Link a child object.
+#define DDLINKCHILD(...) \
+ do { \
+ if (DecoderDoctorLogger::IsDDLoggingEnabled()) { \
+ DecoderDoctorLogger::LinkParentAndChild(this, __VA_ARGS__); \
+ } \
+ } while (0)
+
+// Unlink a child object.
+#define DDUNLINKCHILD(...) \
+ do { \
+ if (DecoderDoctorLogger::IsDDLoggingEnabled()) { \
+ DecoderDoctorLogger::UnlinkParentAndChild(this, __VA_ARGS__); \
+ } \
+ } while (0)
+
+// Log a printf'd string to DDLogger and/or MOZ_LOG, with an EXplicit `this`.
+// Don't even call MOZ_LOG on Android non-release/beta; See Logging.h.
+#if !defined(ANDROID) || !defined(RELEASE_OR_BETA)
+# define DDMOZ_LOGEX(_this, _logModule, _logLevel, _format, ...) \
+ do { \
+ if (DecoderDoctorLogger::IsDDLoggingEnabled() || \
+ MOZ_LOG_TEST(_logModule, _logLevel)) { \
+ DDLOGPR_CHECK(_format, ##__VA_ARGS__); \
+ DecoderDoctorLogger::MozLogPrintf(_this, _logModule, _logLevel, \
+ _format, ##__VA_ARGS__); \
+ } \
+ } while (0)
+#else
+# define DDMOZ_LOGEX(_this, _logModule, _logLevel, _format, ...) \
+ do { \
+ if (DecoderDoctorLogger::IsDDLoggingEnabled()) { \
+ DDLOGPR_CHECK(_format, ##__VA_ARGS__); \
+ DecoderDoctorLogger::MozLogPrintf(_this, _logModule, _logLevel, \
+ _format, ##__VA_ARGS__); \
+ } \
+ } while (0)
+#endif
+
+// Log a printf'd string to DDLogger and/or MOZ_LOG.
+#define DDMOZ_LOG(_logModule, _logLevel, _format, ...) \
+ DDMOZ_LOGEX(this, _logModule, _logLevel, _format, ##__VA_ARGS__)
+
+} // namespace mozilla
+
+#endif // DecoderDoctorLogger_h_
diff --git a/dom/media/doctor/MultiWriterQueue.h b/dom/media/doctor/MultiWriterQueue.h
new file mode 100644
index 0000000000..b19c0039ba
--- /dev/null
+++ b/dom/media/doctor/MultiWriterQueue.h
@@ -0,0 +1,523 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_MultiWriterQueue_h_
+#define mozilla_MultiWriterQueue_h_
+
+#include <cstdint>
+#include <utility>
+
+#include "RollingNumber.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/Mutex.h"
+#include "prthread.h"
+
+namespace mozilla {
+
+// Default reader locking strategy, using a mutex to ensure that concurrent
+// PopAll calls won't overlap.
+class MOZ_CAPABILITY("mutex") MultiWriterQueueReaderLocking_Mutex {
+ public:
+ MultiWriterQueueReaderLocking_Mutex()
+ : mMutex("MultiWriterQueueReaderLocking_Mutex") {}
+ void Lock() MOZ_CAPABILITY_ACQUIRE(mMutex) { mMutex.Lock(); };
+ void Unlock() MOZ_CAPABILITY_RELEASE(mMutex) { mMutex.Unlock(); };
+
+ private:
+ Mutex mMutex;
+};
+
+// Reader non-locking strategy, trusting that PopAll will never be called
+// concurrently (e.g., by only calling it from a specific thread).
+class MOZ_CAPABILITY("dummy lock") MultiWriterQueueReaderLocking_None {
+ public:
+#ifndef DEBUG
+ void Lock() MOZ_CAPABILITY_ACQUIRE(){};
+ void Unlock() MOZ_CAPABILITY_RELEASE(){};
+#else
+ // DEBUG-mode checks to catch concurrent misuses.
+ void Lock() MOZ_CAPABILITY_ACQUIRE() {
+ MOZ_ASSERT(mLocked.compareExchange(false, true));
+ };
+ void Unlock() MOZ_CAPABILITY_RELEASE() {
+ MOZ_ASSERT(mLocked.compareExchange(true, false));
+ };
+
+ private:
+ Atomic<bool> mLocked{false};
+#endif
+};
+
+static constexpr uint32_t MultiWriterQueueDefaultBufferSize = 8192;
+
+// Multi-writer, single-reader queue of elements of type `T`.
+// Elements are bunched together in buffers of `BufferSize` elements.
+//
+// This queue is heavily optimized for pushing. In most cases pushes will only
+// cost a couple of atomic reads and a few non-atomic reads. Worst cases:
+// - Once per buffer, a push will allocate or reuse a buffer for later pushes;
+// - During the above new-buffer push, other pushes will be blocked.
+//
+// By default, popping is protected by mutex; it may be disabled if popping is
+// guaranteed never to be concurrent.
+// In any case, popping will never negatively impact pushes.
+// (However, *not* popping will add runtime costs, as unread buffers will not
+// be freed, or made available to future pushes; Push functions provide
+// feedback as to when popping would be most efficient.)
+template <typename T, uint32_t BufferSize = MultiWriterQueueDefaultBufferSize,
+ typename ReaderLocking = MultiWriterQueueReaderLocking_Mutex>
+class MultiWriterQueue {
+ static_assert(BufferSize > 0, "0-sized MultiWriterQueue buffer");
+
+ public:
+ // Constructor.
+ // Allocates the initial buffer that will receive the first `BufferSize`
+ // elements. Also allocates one reusable buffer, which will definitely be
+ // needed after the first `BufferSize` elements have been pushed.
+ // Ideally (if the reader can process each buffer quickly enough), there
+ // won't be a need for more buffer allocations.
+ MultiWriterQueue()
+ : mBuffersCoverAtLeastUpTo(BufferSize - 1),
+ mMostRecentBuffer(new Buffer{}),
+ mReusableBuffers(new Buffer{}),
+ mOldestBuffer(static_cast<Buffer*>(mMostRecentBuffer)),
+ mLiveBuffersStats(1),
+ mReusableBuffersStats(1),
+ mAllocatedBuffersStats(2) {}
+
+ ~MultiWriterQueue() {
+ auto DestroyList = [](Buffer* aBuffer) {
+ while (aBuffer) {
+ Buffer* older = aBuffer->Older();
+ delete aBuffer;
+ aBuffer = older;
+ }
+ };
+ DestroyList(mMostRecentBuffer);
+ DestroyList(mReusableBuffers);
+ }
+
+ // We need the index to be order-resistant to overflow, i.e., numbers before
+ // an overflow should test smaller-than numbers after the overflow.
+ // This is because we keep pushing elements with increasing Index, and this
+ // Index is used to find the appropriate buffer based on a range; and this
+ // need to work smoothly when crossing the overflow boundary.
+ using Index = RollingNumber<uint32_t>;
+
+ // Pushes indicate whether they have just reached the end of a buffer.
+ using DidReachEndOfBuffer = bool;
+
+ // Push new element and call aF on it.
+ // Element may be in just-created state, or recycled after a PopAll call.
+ // Atomically thread-safe; in the worst case some pushes may be blocked
+ // while a new buffer is created/reused for them.
+ // Returns whether that push reached the end of a buffer; useful if caller
+ // wants to trigger processing regularly at the most efficient time.
+ template <typename F>
+ DidReachEndOfBuffer PushF(F&& aF) {
+ // Atomically claim ownership of the next available element.
+ const Index index{mNextElementToWrite++};
+ // And now go and set that element.
+ for (;;) {
+ Index lastIndex{mBuffersCoverAtLeastUpTo};
+
+ if (MOZ_UNLIKELY(index == lastIndex)) {
+ // We have claimed the last element in the current head -> Allocate a
+ // new head in advance of more pushes. Make it point at the current
+ // most-recent buffer.
+ // This whole process is effectively guarded:
+ // - Later pushes will wait until mBuffersCoverAtLeastUpTo changes to
+ // one that can accept their claimed index.
+ // - Readers will stop until the last element is marked as valid.
+ Buffer* ourBuffer = mMostRecentBuffer;
+ Buffer* newBuffer = NewBuffer(ourBuffer, index + 1);
+ // Because we have claimed this very specific index, we should be the
+ // only one touching the most-recent buffer pointer.
+ MOZ_ASSERT(mMostRecentBuffer == ourBuffer);
+ // Just pivot the most-recent buffer pointer to our new buffer.
+ mMostRecentBuffer = newBuffer;
+ // Because we have claimed this very specific index, we should be the
+ // only one touching the buffer coverage watermark.
+ MOZ_ASSERT(mBuffersCoverAtLeastUpTo == lastIndex.Value());
+ // Update it to include the just-added most-recent buffer.
+ mBuffersCoverAtLeastUpTo = index.Value() + BufferSize;
+ // We know for sure that `ourBuffer` is the correct one for this index.
+ ourBuffer->SetAndValidateElement(aF, index);
+ // And indicate that we have reached the end of a buffer.
+ return true;
+ }
+
+ if (MOZ_UNLIKELY(index > lastIndex)) {
+ // We have claimed an element in a yet-unavailable buffer, wait for our
+ // target buffer to be created (see above).
+ while (Index(mBuffersCoverAtLeastUpTo) < index) {
+ PR_Sleep(PR_INTERVAL_NO_WAIT); // Yield
+ }
+ // Then loop to examine the new situation.
+ continue;
+ }
+
+ // Here, we have claimed a number that is covered by current buffers.
+ // These buffers cannot be destroyed, because our buffer is not filled
+ // yet (we haven't written in it yet), therefore the reader thread will
+ // have to stop there (or before) and won't destroy our buffer or more
+ // recent ones.
+ MOZ_ASSERT(index < lastIndex);
+ Buffer* ourBuffer = mMostRecentBuffer;
+
+ // In rare situations, another thread may have had the time to create a
+ // new more-recent buffer, in which case we need to find our older buffer.
+ while (MOZ_UNLIKELY(index < ourBuffer->Origin())) {
+ // We assume that older buffers with still-invalid elements (e.g., the
+ // one we have just claimed) cannot be destroyed.
+ MOZ_ASSERT(ourBuffer->Older());
+ ourBuffer = ourBuffer->Older();
+ }
+
+ // Now we can set&validate the claimed element, and indicate that we have
+ // not reached the end of a buffer.
+ ourBuffer->SetAndValidateElement(aF, index);
+ return false;
+ }
+ }
+
+ // Push new element and assign it a value.
+ // Atomically thread-safe; in the worst case some pushes may be blocked
+ // while a new buffer is created/reused for them.
+ // Returns whether that push reached the end of a buffer; useful if caller
+ // wants to trigger processing regularly at the most efficient time.
+ DidReachEndOfBuffer Push(const T& aT) {
+ return PushF([&aT](T& aElement, Index) { aElement = aT; });
+ }
+
+ // Push new element and move-assign it a value.
+ // Atomically thread-safe; in the worst case some pushes may be blocked
+ // while a new buffer is created/reused for them.
+ // Returns whether that push reached the end of a buffer; useful if caller
+ // wants to trigger processing regularly at the most efficient time.
+ DidReachEndOfBuffer Push(T&& aT) {
+ return PushF([&aT](T& aElement, Index) { aElement = std::move(aT); });
+ }
+
+ // Pop all elements before the first invalid one, running aF on each of them
+ // in FIFO order.
+ // Thread-safety with other PopAll calls is controlled by the `Locking`
+ // template argument.
+ // Concurrent pushes are always allowed, because:
+ // - PopAll won't read elements until valid,
+ // - Pushes do not interfere with pop-related members -- except for
+ // mReusableBuffers, which is accessed atomically.
+ template <typename F>
+ void PopAll(F&& aF) {
+ mReaderLocking.Lock();
+ // Destroy every second fully-read buffer.
+ // TODO: Research a better algorithm, probably based on stats.
+ bool destroy = false;
+ for (;;) {
+ Buffer* b = mOldestBuffer;
+ MOZ_ASSERT(!b->Older());
+ // The next element to pop must be in that oldest buffer.
+ MOZ_ASSERT(mNextElementToPop >= b->Origin());
+ MOZ_ASSERT(mNextElementToPop < b->Origin() + BufferSize);
+
+ // Start reading each element.
+ if (!b->ReadAndInvalidateAll(aF, mNextElementToPop)) {
+ // Found an invalid element, stop popping.
+ mReaderLocking.Unlock();
+ return;
+ }
+
+ // Reached the end of this oldest buffer
+ MOZ_ASSERT(mNextElementToPop == b->Origin() + BufferSize);
+ // Delete this oldest buffer.
+ // Since the last element was valid, it must mean that there is a newer
+ // buffer.
+ MOZ_ASSERT(b->Newer());
+ MOZ_ASSERT(mNextElementToPop == b->Newer()->Origin());
+ StopUsing(b, destroy);
+ destroy = !destroy;
+
+ // We will loop and start reading the now-oldest buffer.
+ }
+ }
+
+ // Size of all buffers (used, or recyclable), excluding external data.
+ size_t ShallowSizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ return mAllocatedBuffersStats.Count() * sizeof(Buffer);
+ }
+
+ struct CountAndWatermark {
+ int mCount;
+ int mWatermark;
+ };
+
+ CountAndWatermark LiveBuffersStats() const { return mLiveBuffersStats.Get(); }
+ CountAndWatermark ReusableBuffersStats() const {
+ return mReusableBuffersStats.Get();
+ }
+ CountAndWatermark AllocatedBuffersStats() const {
+ return mAllocatedBuffersStats.Get();
+ }
+
+ private:
+ // Structure containing the element to be stored, and a validity-marker.
+ class BufferedElement {
+ public:
+ // Run aF on an invalid element, and mark it as valid.
+ template <typename F>
+ void SetAndValidate(F&& aF, Index aIndex) {
+ MOZ_ASSERT(!mValid);
+ aF(mT, aIndex);
+ mValid = true;
+ }
+
+ // Run aF on a valid element and mark it as invalid, return true.
+ // Return false if element was invalid.
+ template <typename F>
+ bool ReadAndInvalidate(F&& aF) {
+ if (!mValid) {
+ return false;
+ }
+ aF(mT);
+ mValid = false;
+ return true;
+ }
+
+ private:
+ T mT;
+ // mValid should be atomically changed to true *after* mT has been written,
+ // so that the reader can only see valid data.
+ // ReleaseAcquire, because when set to `true`, we want the just-written mT
+ // to be visible to the thread reading this `true`; and when set to `false`,
+ // we want the previous reads to have completed.
+ Atomic<bool, ReleaseAcquire> mValid{false};
+ };
+
+ // Buffer contains a sequence of BufferedElements starting at a specific
+ // index, and it points to the next-older buffer (if any).
+ class Buffer {
+ public:
+ // Constructor of the very first buffer.
+ Buffer() : mOlder(nullptr), mNewer(nullptr), mOrigin(0) {}
+
+ // Constructor of later buffers.
+ Buffer(Buffer* aOlder, Index aOrigin)
+ : mOlder(aOlder), mNewer(nullptr), mOrigin(aOrigin) {
+ MOZ_ASSERT(aOlder);
+ aOlder->mNewer = this;
+ }
+
+ Buffer* Older() const { return mOlder; }
+ void SetOlder(Buffer* aOlder) { mOlder = aOlder; }
+
+ Buffer* Newer() const { return mNewer; }
+ void SetNewer(Buffer* aNewer) { mNewer = aNewer; }
+
+ Index Origin() const { return mOrigin; }
+ void SetOrigin(Index aOrigin) { mOrigin = aOrigin; }
+
+ // Run aF on a yet-invalid element.
+ // Not thread-safe by itself, but nothing else should write this element,
+ // and reader won't access it until after it becomes valid.
+ template <typename F>
+ void SetAndValidateElement(F&& aF, Index aIndex) {
+ MOZ_ASSERT(aIndex >= Origin());
+ MOZ_ASSERT(aIndex < Origin() + BufferSize);
+ mElements[aIndex - Origin()].SetAndValidate(aF, aIndex);
+ }
+
+ using DidReadLastElement = bool;
+
+ // Read all valid elements starting at aIndex, marking them invalid and
+ // updating aIndex.
+ // Returns true if we ended up reading the last element in this buffer.
+ // Accessing the validity bit is thread-safe (as it's atomic), but once
+ // an element is valid, the reading itself is not thread-safe and should be
+ // guarded.
+ template <typename F>
+ DidReadLastElement ReadAndInvalidateAll(F&& aF, Index& aIndex) {
+ MOZ_ASSERT(aIndex >= Origin());
+ MOZ_ASSERT(aIndex < Origin() + BufferSize);
+ for (; aIndex < Origin() + BufferSize; ++aIndex) {
+ if (!mElements[aIndex - Origin()].ReadAndInvalidate(aF)) {
+ // Found an invalid element, stop here. (aIndex will not be updated
+ // past it, so we will start from here next time.)
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private:
+ Buffer* mOlder;
+ Buffer* mNewer;
+ Index mOrigin;
+ BufferedElement mElements[BufferSize];
+ };
+
+ // Reuse a buffer, or create a new one.
+ // All buffered elements will be invalid.
+ Buffer* NewBuffer(Buffer* aOlder, Index aOrigin) {
+ MOZ_ASSERT(aOlder);
+ for (;;) {
+ Buffer* head = mReusableBuffers;
+ if (!head) {
+ ++mAllocatedBuffersStats;
+ ++mLiveBuffersStats;
+ Buffer* buffer = new Buffer(aOlder, aOrigin);
+ return buffer;
+ }
+ Buffer* older = head->Older();
+ // Try to pivot the reusable-buffer pointer from the current head to the
+ // next buffer in line.
+ if (mReusableBuffers.compareExchange(head, older)) {
+ // Success! The reusable-buffer pointer now points at the older buffer,
+ // so we can recycle this ex-head.
+ --mReusableBuffersStats;
+ ++mLiveBuffersStats;
+ head->SetOlder(aOlder);
+ aOlder->SetNewer(head);
+ // We will be the newest; newer-pointer should already be null.
+ MOZ_ASSERT(!head->Newer());
+ head->SetOrigin(aOrigin);
+ return head;
+ }
+ // Failure, someone else must have touched the list, loop to try again.
+ }
+ }
+
+ // Discard a fully-read buffer.
+ // If aDestroy is true, delete it.
+ // If aDestroy is false, move the buffer to a reusable-buffer stack.
+ void StopUsing(Buffer* aBuffer, bool aDestroy) {
+ --mLiveBuffersStats;
+
+ // We should only stop using the oldest buffer.
+ MOZ_ASSERT(!aBuffer->Older());
+ // The newest buffer should not be modified here.
+ MOZ_ASSERT(aBuffer->Newer());
+ MOZ_ASSERT(aBuffer->Newer()->Older() == aBuffer);
+ // Detach from the second-oldest buffer.
+ aBuffer->Newer()->SetOlder(nullptr);
+ // Make the second-oldest buffer the now-oldest buffer.
+ mOldestBuffer = aBuffer->Newer();
+
+ if (aDestroy) {
+ --mAllocatedBuffersStats;
+ delete aBuffer;
+ } else {
+ ++mReusableBuffersStats;
+ // The recycling stack only uses mOlder; mNewer is not needed.
+ aBuffer->SetNewer(nullptr);
+
+ // Make the given buffer the new head of reusable buffers.
+ for (;;) {
+ Buffer* head = mReusableBuffers;
+ aBuffer->SetOlder(head);
+ if (mReusableBuffers.compareExchange(head, aBuffer)) {
+ break;
+ }
+ }
+ }
+ }
+
+ // Index of the next element to write. Modified when an element index is
+ // claimed for a push. If the last element of a buffer is claimed, that push
+ // will be responsible for adding a new head buffer.
+ // Relaxed, because there is no synchronization based on this variable, each
+ // thread just needs to get a different value, and will then write different
+ // things (which themselves have some atomic validation before they may be
+ // read elsewhere, independent of this `mNextElementToWrite`.)
+ Atomic<Index::ValueType, Relaxed> mNextElementToWrite{0};
+
+ // Index that a live recent buffer reaches. If a push claims a lesser-or-
+ // equal number, the corresponding buffer is guaranteed to still be alive:
+ // - It will have been created before this index was updated,
+ // - It will not be destroyed until all its values have been written,
+ // including the one that just claimed a position within it.
+ // Also, the push that claims this exact number is responsible for adding the
+ // next buffer and updating this value accordingly.
+ // ReleaseAcquire, because when set to a certain value, the just-created
+ // buffer covering the new range must be visible to readers.
+ Atomic<Index::ValueType, ReleaseAcquire> mBuffersCoverAtLeastUpTo;
+
+ // Pointer to the most recent buffer. Never null.
+ // This is the most recent of a deque of yet-unread buffers.
+ // Only modified when adding a new head buffer.
+ // ReleaseAcquire, because when modified, the just-created new buffer must be
+ // visible to readers.
+ Atomic<Buffer*, ReleaseAcquire> mMostRecentBuffer;
+
+ // Stack of reusable buffers.
+ // ReleaseAcquire, because when modified, the just-added buffer must be
+ // visible to readers.
+ Atomic<Buffer*, ReleaseAcquire> mReusableBuffers;
+
+ // Template-provided locking mechanism to protect PopAll()-only member
+ // variables below.
+ ReaderLocking mReaderLocking;
+
+ // Pointer to the oldest buffer, which contains the new element to be popped.
+ // Never null.
+ Buffer* mOldestBuffer;
+
+ // Index of the next element to be popped.
+ Index mNextElementToPop{0};
+
+ // Stats.
+ class AtomicCountAndWatermark {
+ public:
+ explicit AtomicCountAndWatermark(int aCount)
+ : mCount(aCount), mWatermark(aCount) {}
+
+ int Count() const { return int(mCount); }
+
+ CountAndWatermark Get() const {
+ return CountAndWatermark{int(mCount), int(mWatermark)};
+ }
+
+ int operator++() {
+ int count = int(++mCount);
+ // Update watermark.
+ for (;;) {
+ int watermark = int(mWatermark);
+ if (watermark >= count) {
+ // printf("++[%p] -=> %d-%d\n", this, count, watermark);
+ break;
+ }
+ if (mWatermark.compareExchange(watermark, count)) {
+ // printf("++[%p] -x> %d-(was %d now %d)\n", this, count, watermark,
+ // count);
+ break;
+ }
+ }
+ return count;
+ }
+
+ int operator--() {
+ int count = int(--mCount);
+ // printf("--[%p] -> %d\n", this, count);
+ return count;
+ }
+
+ private:
+ // Relaxed, as these are just gathering stats, so consistency is not
+ // critical.
+ Atomic<int, Relaxed> mCount;
+ Atomic<int, Relaxed> mWatermark;
+ };
+ // All buffers in the mMostRecentBuffer deque.
+ AtomicCountAndWatermark mLiveBuffersStats;
+ // All buffers in the mReusableBuffers stack.
+ AtomicCountAndWatermark mReusableBuffersStats;
+ // All allocated buffers (sum of above).
+ AtomicCountAndWatermark mAllocatedBuffersStats;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_MultiWriterQueue_h_
diff --git a/dom/media/doctor/RollingNumber.h b/dom/media/doctor/RollingNumber.h
new file mode 100644
index 0000000000..a04296f8bc
--- /dev/null
+++ b/dom/media/doctor/RollingNumber.h
@@ -0,0 +1,163 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_RollingNumber_h_
+#define mozilla_RollingNumber_h_
+
+#include "mozilla/Assertions.h"
+#include "mozilla/Attributes.h"
+#include <limits>
+
+namespace mozilla {
+
+// Unsigned number suited to index elements in a never-ending queue, as
+// order-comparison behaves nicely around the overflow.
+//
+// Additive operators work the same as for the underlying value type, but
+// expect "small" jumps, as should normally happen when manipulating indices.
+//
+// Comparison functions are different, they keep the ordering based on the
+// distance between numbers, modulo the value type range:
+// If the distance is less than half the range of the value type, the usual
+// ordering stays.
+// 0 < 1, 2^23 < 2^24
+// However if the distance is more than half the range, we assume that we are
+// continuing along the queue, and therefore consider the smaller number to
+// actually be greater!
+// uint(-1) < 0.
+//
+// The expected usage is to always work on nearby rolling numbers: slowly
+// incrementing/decrementing them, and translating&comparing them within a
+// small window.
+// To enforce this usage during development, debug-build assertions catch API
+// calls involving distances of more than a *quarter* of the type range.
+// In non-debug builds, all APIs will still work as consistently as possible
+// without crashing, but performing operations on "distant" nunbers could lead
+// to unexpected results.
+template <typename T>
+class RollingNumber {
+ static_assert(!std::numeric_limits<T>::is_signed,
+ "RollingNumber only accepts unsigned number types");
+
+ public:
+ using ValueType = T;
+
+ RollingNumber() : mIndex(0) {}
+
+ explicit RollingNumber(ValueType aIndex) : mIndex(aIndex) {}
+
+ RollingNumber(const RollingNumber&) = default;
+ RollingNumber& operator=(const RollingNumber&) = default;
+
+ ValueType Value() const { return mIndex; }
+
+ // Normal increments/decrements.
+
+ RollingNumber& operator++() {
+ ++mIndex;
+ return *this;
+ }
+
+ RollingNumber operator++(int) { return RollingNumber{mIndex++}; }
+
+ RollingNumber& operator--() {
+ --mIndex;
+ return *this;
+ }
+
+ RollingNumber operator--(int) { return RollingNumber{mIndex--}; }
+
+ RollingNumber& operator+=(const ValueType& aIncrement) {
+ MOZ_ASSERT(aIncrement <= MaxDiff);
+ mIndex += aIncrement;
+ return *this;
+ }
+
+ RollingNumber operator+(const ValueType& aIncrement) const {
+ RollingNumber n = *this;
+ return n += aIncrement;
+ }
+
+ RollingNumber& operator-=(const ValueType& aDecrement) {
+ MOZ_ASSERT(aDecrement <= MaxDiff);
+ mIndex -= aDecrement;
+ return *this;
+ }
+
+ // Translate a RollingNumber by a negative value.
+ RollingNumber operator-(const ValueType& aDecrement) const {
+ RollingNumber n = *this;
+ return n -= aDecrement;
+ }
+
+ // Distance between two RollingNumbers, giving a value.
+ ValueType operator-(const RollingNumber& aOther) const {
+ ValueType diff = mIndex - aOther.mIndex;
+ MOZ_ASSERT(diff <= MaxDiff);
+ return diff;
+ }
+
+ // Normal (in)equality operators.
+
+ bool operator==(const RollingNumber& aOther) const {
+ return mIndex == aOther.mIndex;
+ }
+ bool operator!=(const RollingNumber& aOther) const {
+ return !(*this == aOther);
+ }
+
+ // Modified comparison operators.
+
+ bool operator<(const RollingNumber& aOther) const {
+ const T& a = mIndex;
+ const T& b = aOther.mIndex;
+ // static_cast needed because of possible integer promotion
+ // (e.g., from uint8_t to int, which would make the test useless).
+ const bool lessThanOther = static_cast<ValueType>(a - b) > MidWay;
+ MOZ_ASSERT((lessThanOther ? (b - a) : (a - b)) <= MaxDiff);
+ return lessThanOther;
+ }
+
+ bool operator<=(const RollingNumber& aOther) const {
+ const T& a = mIndex;
+ const T& b = aOther.mIndex;
+ const bool lessishThanOther = static_cast<ValueType>(b - a) <= MidWay;
+ MOZ_ASSERT((lessishThanOther ? (b - a) : (a - b)) <= MaxDiff);
+ return lessishThanOther;
+ }
+
+ bool operator>=(const RollingNumber& aOther) const {
+ const T& a = mIndex;
+ const T& b = aOther.mIndex;
+ const bool greaterishThanOther = static_cast<ValueType>(a - b) <= MidWay;
+ MOZ_ASSERT((greaterishThanOther ? (a - b) : (b - a)) <= MaxDiff);
+ return greaterishThanOther;
+ }
+
+ bool operator>(const RollingNumber& aOther) const {
+ const T& a = mIndex;
+ const T& b = aOther.mIndex;
+ const bool greaterThanOther = static_cast<ValueType>(b - a) > MidWay;
+ MOZ_ASSERT((greaterThanOther ? (a - b) : (b - a)) <= MaxDiff);
+ return greaterThanOther;
+ }
+
+ private:
+ // MidWay is used to split the type range in two, to decide how two numbers
+ // are ordered.
+ static const T MidWay = std::numeric_limits<T>::max() / 2;
+#ifdef DEBUG
+ // MaxDiff is the expected maximum difference between two numbers, either
+ // during comparisons, or when adding/subtracting.
+ // This is only used during debugging, to highlight algorithmic issues.
+ static const T MaxDiff = std::numeric_limits<T>::max() / 4;
+#endif
+ ValueType mIndex;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_RollingNumber_h_
diff --git a/dom/media/doctor/moz.build b/dom/media/doctor/moz.build
new file mode 100644
index 0000000000..75ea07e8f6
--- /dev/null
+++ b/dom/media/doctor/moz.build
@@ -0,0 +1,40 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+TEST_DIRS += [
+ "test/gtest",
+]
+
+# Needed for plugin IPC types required by nsPluginHost
+include("/ipc/chromium/chromium-config.mozbuild")
+
+EXPORTS += [
+ "DDLogCategory.h",
+ "DDLoggedTypeTraits.h",
+ "DDLogObject.h",
+ "DDLogValue.h",
+ "DecoderDoctorDiagnostics.h",
+ "DecoderDoctorLogger.h",
+]
+
+UNIFIED_SOURCES += [
+ "DDLifetime.cpp",
+ "DDLifetimes.cpp",
+ "DDLogCategory.cpp",
+ "DDLogMessage.cpp",
+ "DDLogObject.cpp",
+ "DDLogUtils.cpp",
+ "DDLogValue.cpp",
+ "DDMediaLog.cpp",
+ "DDMediaLogs.cpp",
+ "DDTimeStamp.cpp",
+ "DecoderDoctorDiagnostics.cpp",
+ "DecoderDoctorLogger.cpp",
+]
+
+BROWSER_CHROME_MANIFESTS += ["test/browser/browser.ini"]
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/doctor/test/browser/browser.ini b/dom/media/doctor/test/browser/browser.ini
new file mode 100644
index 0000000000..3dda42c4e5
--- /dev/null
+++ b/dom/media/doctor/test/browser/browser.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+subsuite = media-bc
+tags = decoderdoctor
+support-files =
+
+[browser_decoderDoctor.js]
+[browser_doctor_notification.js]
diff --git a/dom/media/doctor/test/browser/browser_decoderDoctor.js b/dom/media/doctor/test/browser/browser_decoderDoctor.js
new file mode 100644
index 0000000000..c502131fec
--- /dev/null
+++ b/dom/media/doctor/test/browser/browser_decoderDoctor.js
@@ -0,0 +1,356 @@
+"use strict";
+
+// 'data' contains the notification data object:
+// - data.type must be provided.
+// - data.isSolved and data.decoderDoctorReportId will be added if not provided
+// (false and "testReportId" resp.)
+// - Other fields (e.g.: data.formats) may be provided as needed.
+// 'notificationMessage': Expected message in the notification bar.
+// Falsy if nothing is expected after the notification is sent, in which case
+// we won't have further checks, so the following parameters are not needed.
+// 'label': Expected button label. Falsy if no button is expected, in which case
+// we won't have further checks, so the following parameters are not needed.
+// 'accessKey': Expected access key for the button.
+// 'tabChecker': function(openedTab) called with the opened tab that resulted
+// from clicking the button.
+async function test_decoder_doctor_notification(
+ data,
+ notificationMessage,
+ label,
+ accessKey,
+ isLink,
+ tabChecker
+) {
+ const TEST_URL = "https://example.org";
+ // A helper closure to test notifications in same or different origins.
+ // 'test_cross_origin' is used to determine if the observers used in the test
+ // are notified in the same frame (when false) or in a cross origin iframe
+ // (when true).
+ async function create_tab_and_test(test_cross_origin) {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: TEST_URL },
+ async function (browser) {
+ let awaitNotificationBar;
+ if (notificationMessage) {
+ awaitNotificationBar = BrowserTestUtils.waitForNotificationBar(
+ gBrowser,
+ browser,
+ "decoder-doctor-notification"
+ );
+ }
+
+ await SpecialPowers.spawn(
+ browser,
+ [data, test_cross_origin],
+ /* eslint-disable-next-line no-shadow */
+ async function (data, test_cross_origin) {
+ if (!test_cross_origin) {
+ // Notify in the same origin.
+ Services.obs.notifyObservers(
+ content.window,
+ "decoder-doctor-notification",
+ JSON.stringify(data)
+ );
+ return;
+ // Done notifying in the same origin.
+ }
+
+ // Notify in a different origin.
+ const CROSS_ORIGIN_URL = "https://example.com";
+ let frame = content.document.createElement("iframe");
+ frame.src = CROSS_ORIGIN_URL;
+ await new Promise(resolve => {
+ frame.addEventListener("load", () => {
+ resolve();
+ });
+ content.document.body.appendChild(frame);
+ });
+
+ await content.SpecialPowers.spawn(
+ frame,
+ [data],
+ async function (
+ /* eslint-disable-next-line no-shadow */
+ data
+ ) {
+ Services.obs.notifyObservers(
+ content.window,
+ "decoder-doctor-notification",
+ JSON.stringify(data)
+ );
+ }
+ );
+ // Done notifying in a different origin.
+ }
+ );
+
+ if (!notificationMessage) {
+ ok(
+ true,
+ "Tested notifying observers with a nonsensical message, no effects expected"
+ );
+ return;
+ }
+
+ let notification;
+ try {
+ notification = await awaitNotificationBar;
+ } catch (ex) {
+ ok(false, ex);
+ return;
+ }
+ ok(notification, "Got decoder-doctor-notification notification");
+ if (label?.l10nId) {
+ // Without the following statement, the
+ // test_cannot_initialize_pulseaudio
+ // will permanently fail on Linux.
+ if (label.l10nId === "moz-support-link-text") {
+ MozXULElement.insertFTLIfNeeded(
+ "browser/components/mozSupportLink.ftl"
+ );
+ }
+ label = await document.l10n.formatValue(label.l10nId);
+ }
+ if (isLink) {
+ let link = notification.messageText.querySelector("a");
+ if (link) {
+ // Seems to be a Windows specific quirk, but without this
+ // mutation observer the notification.messageText.textContent
+ // will not be updated. This will cause consistent failures
+ // on Windows.
+ await BrowserTestUtils.waitForMutationCondition(
+ link,
+ { childList: true },
+ () => link.textContent.trim()
+ );
+ }
+ }
+ is(
+ notification.messageText.textContent,
+ notificationMessage + (isLink && label ? ` ${label}` : ""),
+ "notification message should match expectation"
+ );
+
+ let button = notification.buttonContainer.querySelector("button");
+ let link = notification.messageText.querySelector("a");
+ if (!label) {
+ ok(!button, "There should not be a button");
+ ok(!link, "There should not be a link");
+ return;
+ }
+
+ if (isLink) {
+ ok(!button, "There should not be a button");
+ is(link.innerText, label, `notification link should be '${label}'`);
+ ok(
+ !link.hasAttribute("accesskey"),
+ "notification link should not have accesskey"
+ );
+ } else {
+ ok(!link, "There should not be a link");
+ is(
+ button.getAttribute("label"),
+ label,
+ `notification button should be '${label}'`
+ );
+ is(
+ button.getAttribute("accesskey"),
+ accessKey,
+ "notification button should have accesskey"
+ );
+ }
+
+ if (!tabChecker) {
+ ok(false, "Test implementation error: Missing tabChecker");
+ return;
+ }
+ let awaitNewTab = BrowserTestUtils.waitForNewTab(gBrowser);
+ if (button) {
+ button.click();
+ } else {
+ link.click();
+ }
+ let openedTab = await awaitNewTab;
+ tabChecker(openedTab);
+ BrowserTestUtils.removeTab(openedTab);
+ }
+ );
+ }
+
+ if (typeof data.type === "undefined") {
+ ok(false, "Test implementation error: data.type must be provided");
+ return;
+ }
+ data.isSolved = data.isSolved || false;
+ if (typeof data.decoderDoctorReportId === "undefined") {
+ data.decoderDoctorReportId = "testReportId";
+ }
+
+ // Test same origin.
+ await create_tab_and_test(false);
+ // Test cross origin.
+ await create_tab_and_test(true);
+}
+
+function tab_checker_for_sumo(expectedPath) {
+ return function (openedTab) {
+ let baseURL = Services.urlFormatter.formatURLPref("app.support.baseURL");
+ let url = baseURL + expectedPath;
+ is(
+ openedTab.linkedBrowser.currentURI.spec,
+ url,
+ `Expected '${url}' in new tab`
+ );
+ };
+}
+
+function tab_checker_for_webcompat(expectedParams) {
+ return function (openedTab) {
+ let urlString = openedTab.linkedBrowser.currentURI.spec;
+ let endpoint = Services.prefs.getStringPref(
+ "media.decoder-doctor.new-issue-endpoint",
+ ""
+ );
+ ok(
+ urlString.startsWith(endpoint),
+ `Expected URL starting with '${endpoint}', got '${urlString}'`
+ );
+ let params = new URL(urlString).searchParams;
+ for (let k in expectedParams) {
+ if (!params.has(k)) {
+ ok(false, `Expected ${k} in webcompat URL`);
+ } else {
+ is(
+ params.get(k),
+ expectedParams[k],
+ `Expected ${k}='${expectedParams[k]}' in webcompat URL`
+ );
+ }
+ }
+ };
+}
+
+add_task(async function test_platform_decoder_not_found() {
+ let message = "";
+ let decoderDoctorReportId = "";
+ let isLinux = AppConstants.platform == "linux";
+ if (isLinux) {
+ message = gNavigatorBundle.getString("decoder.noCodecsLinux.message");
+ decoderDoctorReportId = "MediaPlatformDecoderNotFound";
+ } else if (AppConstants.platform == "win") {
+ message = gNavigatorBundle.getString("decoder.noHWAcceleration.message");
+ decoderDoctorReportId = "MediaWMFNeeded";
+ }
+
+ await test_decoder_doctor_notification(
+ {
+ type: "platform-decoder-not-found",
+ decoderDoctorReportId,
+ formats: "testFormat",
+ },
+ message,
+ isLinux ? "" : { l10nId: "moz-support-link-text" },
+ isLinux ? "" : gNavigatorBundle.getString("decoder.noCodecs.accesskey"),
+ true,
+ tab_checker_for_sumo("fix-video-audio-problems-firefox-windows")
+ );
+});
+
+add_task(async function test_cannot_initialize_pulseaudio() {
+ let message = "";
+ // This is only sent on Linux.
+ if (AppConstants.platform == "linux") {
+ message = gNavigatorBundle.getString("decoder.noPulseAudio.message");
+ }
+
+ await test_decoder_doctor_notification(
+ { type: "cannot-initialize-pulseaudio", formats: "testFormat" },
+ message,
+ { l10nId: "moz-support-link-text" },
+ gNavigatorBundle.getString("decoder.noCodecs.accesskey"),
+ true,
+ tab_checker_for_sumo("fix-common-audio-and-video-issues")
+ );
+});
+
+add_task(async function test_unsupported_libavcodec() {
+ let message = "";
+ // This is only sent on Linux.
+ if (AppConstants.platform == "linux") {
+ message = gNavigatorBundle.getString(
+ "decoder.unsupportedLibavcodec.message"
+ );
+ }
+
+ await test_decoder_doctor_notification(
+ { type: "unsupported-libavcodec", formats: "testFormat" },
+ message
+ );
+});
+
+add_task(async function test_decode_error() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "media.decoder-doctor.new-issue-endpoint",
+ "http://example.com/webcompat",
+ ],
+ ["browser.fixup.fallback-to-https", false],
+ ],
+ });
+ let message = gNavigatorBundle.getString("decoder.decodeError.message");
+ await test_decoder_doctor_notification(
+ {
+ type: "decode-error",
+ decodeIssue: "DecodeIssue",
+ docURL: "DocURL",
+ resourceURL: "ResURL",
+ },
+ message,
+ gNavigatorBundle.getString("decoder.decodeError.button"),
+ gNavigatorBundle.getString("decoder.decodeError.accesskey"),
+ false,
+ tab_checker_for_webcompat({
+ url: "DocURL",
+ label: "type-media",
+ problem_type: "video_bug",
+ details: JSON.stringify({
+ "Technical Information:": "DecodeIssue",
+ "Resource:": "ResURL",
+ }),
+ })
+ );
+});
+
+add_task(async function test_decode_warning() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [
+ "media.decoder-doctor.new-issue-endpoint",
+ "http://example.com/webcompat",
+ ],
+ ],
+ });
+ let message = gNavigatorBundle.getString("decoder.decodeWarning.message");
+ await test_decoder_doctor_notification(
+ {
+ type: "decode-warning",
+ decodeIssue: "DecodeIssue",
+ docURL: "DocURL",
+ resourceURL: "ResURL",
+ },
+ message,
+ gNavigatorBundle.getString("decoder.decodeError.button"),
+ gNavigatorBundle.getString("decoder.decodeError.accesskey"),
+ false,
+ tab_checker_for_webcompat({
+ url: "DocURL",
+ label: "type-media",
+ problem_type: "video_bug",
+ details: JSON.stringify({
+ "Technical Information:": "DecodeIssue",
+ "Resource:": "ResURL",
+ }),
+ })
+ );
+});
diff --git a/dom/media/doctor/test/browser/browser_doctor_notification.js b/dom/media/doctor/test/browser/browser_doctor_notification.js
new file mode 100644
index 0000000000..5789622e23
--- /dev/null
+++ b/dom/media/doctor/test/browser/browser_doctor_notification.js
@@ -0,0 +1,265 @@
+/**
+ * This test is used to test whether the decoder doctor would report the error
+ * on the notification banner (checking that by observing message) or on the web
+ * console (checking that by listening to the test event).
+ * Error should be reported after calling `DecoderDoctorDiagnostics::StoreXXX`
+ * methods.
+ * - StoreFormatDiagnostics() [for checking if type is supported]
+ * - StoreDecodeError() [when decode error occurs]
+ * - StoreEvent() [for reporting audio sink error]
+ */
+
+// Only types being listed here would be allowed to display on a
+// notification banner. Otherwise, the error would only be showed on the
+// web console.
+var gAllowedNotificationTypes =
+ "MediaWMFNeeded,MediaFFMpegNotFound,MediaUnsupportedLibavcodec,MediaDecodeError,MediaCannotInitializePulseAudio,";
+
+// Used to check if the mime type in the notification is equal to what we set
+// before. This mime type doesn't reflect the real world siutation, i.e. not
+// every error listed in this test would happen on this type. An example, ffmpeg
+// not found would only happen on H264/AAC media.
+const gMimeType = "video/mp4";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.decoder-doctor.testing", true],
+ ["media.decoder-doctor.verbose", true],
+ ["media.decoder-doctor.notifications-allowed", gAllowedNotificationTypes],
+ ],
+ });
+ // transfer types to lower cases in order to match with `DecoderDoctorReportType`
+ gAllowedNotificationTypes = gAllowedNotificationTypes.toLowerCase();
+});
+
+add_task(async function testWMFIsNeeded() {
+ const tab = await createTab("about:blank");
+ await setFormatDiagnosticsReportForMimeType(tab, {
+ type: "platform-decoder-not-found",
+ decoderDoctorReportId: "mediawmfneeded",
+ formats: gMimeType,
+ });
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testFFMpegNotFound() {
+ const tab = await createTab("about:blank");
+ await setFormatDiagnosticsReportForMimeType(tab, {
+ type: "platform-decoder-not-found",
+ decoderDoctorReportId: "mediaplatformdecodernotfound",
+ formats: gMimeType,
+ });
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testLibAVCodecUnsupported() {
+ const tab = await createTab("about:blank");
+ await setFormatDiagnosticsReportForMimeType(tab, {
+ type: "unsupported-libavcodec",
+ decoderDoctorReportId: "mediaunsupportedlibavcodec",
+ formats: gMimeType,
+ });
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testCanNotPlayNoDecoder() {
+ const tab = await createTab("about:blank");
+ await setFormatDiagnosticsReportForMimeType(tab, {
+ type: "cannot-play",
+ decoderDoctorReportId: "mediacannotplaynodecoders",
+ formats: gMimeType,
+ });
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function testNoDecoder() {
+ const tab = await createTab("about:blank");
+ await setFormatDiagnosticsReportForMimeType(tab, {
+ type: "can-play-but-some-missing-decoders",
+ decoderDoctorReportId: "medianodecoders",
+ formats: gMimeType,
+ });
+ BrowserTestUtils.removeTab(tab);
+});
+
+const gErrorList = [
+ "NS_ERROR_DOM_MEDIA_ABORT_ERR",
+ "NS_ERROR_DOM_MEDIA_NOT_ALLOWED_ERR",
+ "NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR",
+ "NS_ERROR_DOM_MEDIA_DECODE_ERR",
+ "NS_ERROR_DOM_MEDIA_FATAL_ERR",
+ "NS_ERROR_DOM_MEDIA_METADATA_ERR",
+ "NS_ERROR_DOM_MEDIA_OVERFLOW_ERR",
+ "NS_ERROR_DOM_MEDIA_MEDIASINK_ERR",
+ "NS_ERROR_DOM_MEDIA_DEMUXER_ERR",
+ "NS_ERROR_DOM_MEDIA_CDM_ERR",
+ "NS_ERROR_DOM_MEDIA_CUBEB_INITIALIZATION_ERR",
+];
+
+add_task(async function testDecodeError() {
+ const type = "decode-error";
+ const decoderDoctorReportId = "mediadecodeerror";
+ for (let error of gErrorList) {
+ const tab = await createTab("about:blank");
+ info(`first to try if the error is not allowed to be reported`);
+ // No error is allowed to be reported in the notification banner.
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.decoder-doctor.decode-errors-allowed", ""]],
+ });
+ await setDecodeError(tab, {
+ type,
+ decoderDoctorReportId,
+ error,
+ shouldReportNotification: false,
+ });
+
+ // If the notification type is `MediaDecodeError` and the error type is
+ // listed in the pref, then the error would be reported to the
+ // notification banner.
+ info(`Then to try if the error is allowed to be reported`);
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.decoder-doctor.decode-errors-allowed", error]],
+ });
+ await setDecodeError(tab, {
+ type,
+ decoderDoctorReportId,
+ error,
+ shouldReportNotification: true,
+ });
+ BrowserTestUtils.removeTab(tab);
+ }
+});
+
+add_task(async function testAudioSinkFailedStartup() {
+ const tab = await createTab("about:blank");
+ await setAudioSinkFailedStartup(tab, {
+ type: "cannot-initialize-pulseaudio",
+ decoderDoctorReportId: "mediacannotinitializepulseaudio",
+ // This error comes with `*`, see `DecoderDoctorDiagnostics::StoreEvent`
+ formats: "*",
+ });
+ BrowserTestUtils.removeTab(tab);
+});
+
+/**
+ * Following are helper functions
+ */
+async function createTab(url) {
+ let tab = await BrowserTestUtils.openNewForegroundTab(window.gBrowser, url);
+ // Create observer in the content process in order to check the decoder
+ // doctor's notification that would be sent when an error occurs.
+ await SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ content._notificationName = "decoder-doctor-notification";
+ content._obs = {
+ observe(subject, topic, data) {
+ let { type, decoderDoctorReportId, formats } = JSON.parse(data);
+ decoderDoctorReportId = decoderDoctorReportId.toLowerCase();
+ info(`received '${type}:${decoderDoctorReportId}:${formats}'`);
+ if (!this._resolve) {
+ ok(false, "receive unexpected notification?");
+ }
+ if (
+ type == this._type &&
+ decoderDoctorReportId == this._decoderDoctorReportId &&
+ formats == this._formats
+ ) {
+ ok(true, `received correct notification`);
+ Services.obs.removeObserver(content._obs, content._notificationName);
+ this._resolve();
+ this._resolve = null;
+ }
+ },
+ // Return a promise that will be resolved once receiving a notification
+ // which has equal data with the input parameters.
+ waitFor({ type, decoderDoctorReportId, formats }) {
+ if (this._resolve) {
+ ok(false, "already has a pending promise!");
+ return Promise.reject();
+ }
+ Services.obs.addObserver(content._obs, content._notificationName);
+ return new Promise(resolve => {
+ info(`waiting for '${type}:${decoderDoctorReportId}:${formats}'`);
+ this._resolve = resolve;
+ this._type = type;
+ this._decoderDoctorReportId = decoderDoctorReportId;
+ this._formats = formats;
+ });
+ },
+ };
+ content._waitForReport = (params, shouldReportNotification) => {
+ const reportToConsolePromise = new Promise(r => {
+ content.document.addEventListener(
+ "mozreportmediaerror",
+ _ => {
+ r();
+ },
+ { once: true }
+ );
+ });
+ const reportToNotificationBannerPromise = shouldReportNotification
+ ? content._obs.waitFor(params)
+ : Promise.resolve();
+ info(
+ `waitForConsole=true, waitForNotificationBanner=${shouldReportNotification}`
+ );
+ return Promise.all([
+ reportToConsolePromise,
+ reportToNotificationBannerPromise,
+ ]);
+ };
+ });
+ return tab;
+}
+
+async function setFormatDiagnosticsReportForMimeType(tab, params) {
+ const shouldReportNotification = gAllowedNotificationTypes.includes(
+ params.decoderDoctorReportId
+ );
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [params, shouldReportNotification],
+ async (params, shouldReportNotification) => {
+ const video = content.document.createElement("video");
+ SpecialPowers.wrap(video).setFormatDiagnosticsReportForMimeType(
+ params.formats,
+ params.decoderDoctorReportId
+ );
+ await content._waitForReport(params, shouldReportNotification);
+ }
+ );
+ ok(true, `finished check for ${params.decoderDoctorReportId}`);
+}
+
+async function setDecodeError(tab, params) {
+ info(`start check for ${params.error}`);
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [params],
+ async (params, shouldReportNotification) => {
+ const video = content.document.createElement("video");
+ SpecialPowers.wrap(video).setDecodeError(params.error);
+ await content._waitForReport(params, params.shouldReportNotification);
+ }
+ );
+ ok(true, `finished check for ${params.error}`);
+}
+
+async function setAudioSinkFailedStartup(tab, params) {
+ const shouldReportNotification = gAllowedNotificationTypes.includes(
+ params.decoderDoctorReportId
+ );
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [params, shouldReportNotification],
+ async (params, shouldReportNotification) => {
+ const video = content.document.createElement("video");
+ const waitPromise = content._waitForReport(
+ params,
+ shouldReportNotification
+ );
+ SpecialPowers.wrap(video).setAudioSinkFailedStartup();
+ await waitPromise;
+ }
+ );
+}
diff --git a/dom/media/doctor/test/gtest/TestMultiWriterQueue.cpp b/dom/media/doctor/test/gtest/TestMultiWriterQueue.cpp
new file mode 100644
index 0000000000..35a89c9267
--- /dev/null
+++ b/dom/media/doctor/test/gtest/TestMultiWriterQueue.cpp
@@ -0,0 +1,382 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MultiWriterQueue.h"
+
+#include "DDTimeStamp.h"
+#include "mozilla/gtest/MozAssertions.h"
+#include "mozilla/Assertions.h"
+#include "nsDeque.h"
+#include "nsIThread.h"
+#include "nsThreadUtils.h"
+
+#include <gtest/gtest.h>
+#include <type_traits>
+
+using mozilla::MultiWriterQueue;
+using mozilla::MultiWriterQueueDefaultBufferSize;
+using mozilla::MultiWriterQueueReaderLocking_Mutex;
+using mozilla::MultiWriterQueueReaderLocking_None;
+
+template <size_t BufferSize>
+static void TestMultiWriterQueueST(const int loops) {
+ using Q = MultiWriterQueue<int, BufferSize>;
+ Q q;
+
+ int pushes = 0;
+ // Go through 2 cycles of pushes&pops, to exercize reusable buffers.
+ for (int max = loops; max <= loops * 2; max *= 2) {
+ // Push all numbers.
+ for (int i = 1; i <= max; ++i) {
+ bool newBuffer = q.Push(i);
+ // A new buffer should be added at the last push of each buffer.
+ EXPECT_EQ(++pushes % BufferSize == 0, newBuffer);
+ }
+
+ // Pop numbers, should be FIFO.
+ int x = 0;
+ q.PopAll([&](int& i) { EXPECT_EQ(++x, i); });
+
+ // We should have got all numbers.
+ EXPECT_EQ(max, x);
+
+ // Nothing left.
+ q.PopAll([&](int&) { EXPECT_TRUE(false); });
+ }
+}
+
+TEST(MultiWriterQueue, SingleThreaded)
+{
+ TestMultiWriterQueueST<1>(10);
+ TestMultiWriterQueueST<2>(10);
+ TestMultiWriterQueueST<4>(10);
+
+ TestMultiWriterQueueST<10>(9);
+ TestMultiWriterQueueST<10>(10);
+ TestMultiWriterQueueST<10>(11);
+ TestMultiWriterQueueST<10>(19);
+ TestMultiWriterQueueST<10>(20);
+ TestMultiWriterQueueST<10>(21);
+ TestMultiWriterQueueST<10>(999);
+ TestMultiWriterQueueST<10>(1000);
+ TestMultiWriterQueueST<10>(1001);
+
+ TestMultiWriterQueueST<8192>(8192 * 4 + 1);
+}
+
+template <typename Q>
+static void TestMultiWriterQueueMT(int aWriterThreads, int aReaderThreads,
+ int aTotalLoops, const char* aPrintPrefix) {
+ Q q;
+
+ const int threads = aWriterThreads + aReaderThreads;
+ const int loops = aTotalLoops / aWriterThreads;
+
+ nsIThread** array = new nsIThread*[threads];
+
+ mozilla::Atomic<int> pushThreadsCompleted{0};
+ int pops = 0;
+
+ nsCOMPtr<nsIRunnable> popper = NS_NewRunnableFunction("MWQPopper", [&]() {
+ // int popsBefore = pops;
+ // int allocsBefore = q.AllocatedBuffersStats().mCount;
+ q.PopAll([&pops](const int& i) { ++pops; });
+ // if (pops != popsBefore ||
+ // q.AllocatedBuffersStats().mCount != allocsBefore) {
+ // printf("%s threads=1+%d loops/thread=%d pops=%d "
+ // "buffers: live=%d (w %d) reusable=%d (w %d) "
+ // "alloc=%d (w %d)\n",
+ // aPrintPrefix,
+ // aWriterThreads,
+ // loops,
+ // pops,
+ // q.LiveBuffersStats().mCount,
+ // q.LiveBuffersStats().mWatermark,
+ // q.ReusableBuffersStats().mCount,
+ // q.ReusableBuffersStats().mWatermark,
+ // q.AllocatedBuffersStats().mCount,
+ // q.AllocatedBuffersStats().mWatermark);
+ // }
+ });
+ // Cycle through reader threads.
+ mozilla::Atomic<size_t> readerThread{0};
+
+ double start = mozilla::ToSeconds(mozilla::DDNow());
+
+ for (int k = 0; k < threads; k++) {
+ // First `aReaderThreads` threads to pop, all others to push.
+ if (k < aReaderThreads) {
+ nsCOMPtr<nsIThread> t;
+ nsresult rv = NS_NewNamedThread("MWQThread", getter_AddRefs(t));
+ EXPECT_NS_SUCCEEDED(rv);
+ NS_ADDREF(array[k] = t);
+ } else {
+ nsCOMPtr<nsIThread> t;
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction("MWQPusher", [&, k]() {
+ // Give a bit of breathing space to construct other threads.
+ PR_Sleep(PR_MillisecondsToInterval(100));
+
+ for (int i = 0; i < loops; ++i) {
+ if (q.Push(k * threads + i) && aReaderThreads != 0) {
+ // Run a popper task every time we push the last element of a
+ // buffer.
+ array[++readerThread % aReaderThreads]->Dispatch(
+ popper, nsIThread::DISPATCH_NORMAL);
+ }
+ }
+ ++pushThreadsCompleted;
+ });
+ nsresult rv = NS_NewNamedThread("MWQThread", getter_AddRefs(t), r);
+ EXPECT_NS_SUCCEEDED(rv);
+ NS_ADDREF(array[k] = t);
+ }
+ }
+
+ for (int k = threads - 1; k >= 0; k--) {
+ array[k]->Shutdown();
+ NS_RELEASE(array[k]);
+ }
+ delete[] array;
+
+ // There may be a few more elements that haven't been read yet.
+ q.PopAll([&pops](const int& i) { ++pops; });
+ const int pushes = aWriterThreads * loops;
+ EXPECT_EQ(pushes, pops);
+ q.PopAll([](const int& i) { EXPECT_TRUE(false); });
+
+ double duration = mozilla::ToSeconds(mozilla::DDNow()) - start - 0.1;
+ printf(
+ "%s threads=%dw+%dr loops/thread=%d pushes=pops=%d duration=%fs "
+ "pushes/s=%f buffers: live=%d (w %d) reusable=%d (w %d) "
+ "alloc=%d (w %d)\n",
+ aPrintPrefix, aWriterThreads, aReaderThreads, loops, pushes, duration,
+ pushes / duration, q.LiveBuffersStats().mCount,
+ q.LiveBuffersStats().mWatermark, q.ReusableBuffersStats().mCount,
+ q.ReusableBuffersStats().mWatermark, q.AllocatedBuffersStats().mCount,
+ q.AllocatedBuffersStats().mWatermark);
+}
+
+// skip test on windows10-aarch64 due to unexpected test timeout at
+// MultiWriterSingleReader, bug 1526001
+#if !defined(_M_ARM64)
+TEST(MultiWriterQueue, MultiWriterSingleReader)
+{
+ // Small BufferSize, to exercize the buffer management code.
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_None>>(
+ 1, 0, 2 * 1024 * 1024, "MultiWriterQueue<int, 10, Locking_None>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_None>>(
+ 1, 1, 2 * 1024 * 1024, "MultiWriterQueue<int, 10, Locking_None>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_None>>(
+ 2, 1, 2 * 1024 * 1024, "MultiWriterQueue<int, 10, Locking_None>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_None>>(
+ 3, 1, 2 * 1024 * 1024, "MultiWriterQueue<int, 10, Locking_None>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_None>>(
+ 4, 1, 2 * 1024 * 1024, "MultiWriterQueue<int, 10, Locking_None>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_None>>(
+ 5, 1, 2 * 1024 * 1024, "MultiWriterQueue<int, 10, Locking_None>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_None>>(
+ 6, 1, 2 * 1024 * 1024, "MultiWriterQueue<int, 10, Locking_None>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_None>>(
+ 7, 1, 2 * 1024 * 1024, "MultiWriterQueue<int, 10, Locking_None>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_None>>(
+ 8, 1, 2 * 1024 * 1024, "MultiWriterQueue<int, 10, Locking_None>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_None>>(
+ 9, 1, 2 * 1024 * 1024, "MultiWriterQueue<int, 10, Locking_None>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_None>>(
+ 10, 1, 2 * 1024 * 1024, "MultiWriterQueue<int, 10, Locking_None>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_None>>(
+ 16, 1, 2 * 1024 * 1024, "MultiWriterQueue<int, 10, Locking_None>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_None>>(
+ 32, 1, 2 * 1024 * 1024, "MultiWriterQueue<int, 10, Locking_None>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_None>>(
+ 64, 1, 2 * 1024 * 1024, "MultiWriterQueue<int, 10, Locking_None>");
+
+ // A more real-life buffer size.
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, MultiWriterQueueDefaultBufferSize,
+ MultiWriterQueueReaderLocking_None>>(
+ 64, 1, 2 * 1024 * 1024,
+ "MultiWriterQueue<int, DefaultBufferSize, Locking_None>");
+
+ // DEBUG-mode thread-safety checks should make the following (multi-reader
+ // with no locking) crash; uncomment to verify.
+ // TestMultiWriterQueueMT<
+ // MultiWriterQueue<int, MultiWriterQueueDefaultBufferSize,
+ // MultiWriterQueueReaderLocking_None>>(64, 2, 2*1024*1024);
+}
+#endif
+
+// skip test on windows10-aarch64 due to unexpected test timeout at
+// MultiWriterMultiReade, bug 1526001
+#if !defined(_M_ARM64)
+TEST(MultiWriterQueue, MultiWriterMultiReader)
+{
+ static_assert(
+ std::is_same_v<
+ MultiWriterQueue<int, 10>,
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_Mutex>>,
+ "MultiWriterQueue reader locking should use Mutex by default");
+
+ // Small BufferSize, to exercize the buffer management code.
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_Mutex>>(
+ 1, 2, 1024 * 1024, "MultiWriterQueue<int, 10, Locking_Mutex>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_Mutex>>(
+ 2, 2, 1024 * 1024, "MultiWriterQueue<int, 10, Locking_Mutex>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_Mutex>>(
+ 3, 2, 1024 * 1024, "MultiWriterQueue<int, 10, Locking_Mutex>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_Mutex>>(
+ 4, 2, 1024 * 1024, "MultiWriterQueue<int, 10, Locking_Mutex>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_Mutex>>(
+ 5, 2, 1024 * 1024, "MultiWriterQueue<int, 10, Locking_Mutex>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_Mutex>>(
+ 6, 2, 1024 * 1024, "MultiWriterQueue<int, 10, Locking_Mutex>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_Mutex>>(
+ 7, 2, 1024 * 1024, "MultiWriterQueue<int, 10, Locking_Mutex>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_Mutex>>(
+ 8, 2, 1024 * 1024, "MultiWriterQueue<int, 10, Locking_Mutex>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_Mutex>>(
+ 9, 2, 1024 * 1024, "MultiWriterQueue<int, 10, Locking_Mutex>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_Mutex>>(
+ 10, 4, 1024 * 1024, "MultiWriterQueue<int, 10, Locking_Mutex>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_Mutex>>(
+ 16, 8, 1024 * 1024, "MultiWriterQueue<int, 10, Locking_Mutex>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_Mutex>>(
+ 32, 16, 1024 * 1024, "MultiWriterQueue<int, 10, Locking_Mutex>");
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, 10, MultiWriterQueueReaderLocking_Mutex>>(
+ 64, 32, 1024 * 1024, "MultiWriterQueue<int, 10, Locking_Mutex>");
+
+ // A more real-life buffer size.
+ TestMultiWriterQueueMT<
+ MultiWriterQueue<int, MultiWriterQueueDefaultBufferSize,
+ MultiWriterQueueReaderLocking_Mutex>>(
+ 64, 32, 1024 * 1024,
+ "MultiWriterQueue<int, DefaultBufferSize, Locking_Mutex>");
+}
+#endif
+
+// Single-threaded use only.
+struct DequeWrapperST {
+ nsDeque<void> mDQ;
+
+ bool Push(int i) {
+ mDQ.PushFront(reinterpret_cast<void*>(static_cast<uintptr_t>(i)));
+ return true;
+ }
+ template <typename F>
+ void PopAll(F&& aF) {
+ while (mDQ.GetSize() != 0) {
+ int i = static_cast<int>(reinterpret_cast<uintptr_t>(mDQ.Pop()));
+ aF(i);
+ }
+ }
+
+ struct CountAndWatermark {
+ int mCount = 0;
+ int mWatermark = 0;
+ } mLiveBuffersStats, mReusableBuffersStats, mAllocatedBuffersStats;
+
+ CountAndWatermark LiveBuffersStats() const { return mLiveBuffersStats; }
+ CountAndWatermark ReusableBuffersStats() const {
+ return mReusableBuffersStats;
+ }
+ CountAndWatermark AllocatedBuffersStats() const {
+ return mAllocatedBuffersStats;
+ }
+};
+
+// Multi-thread (atomic) writes allowed, make sure you don't pop unless writes
+// can't happen.
+struct DequeWrapperAW : DequeWrapperST {
+ mozilla::Atomic<bool> mWriting{false};
+
+ bool Push(int i) {
+ while (!mWriting.compareExchange(false, true)) {
+ }
+ mDQ.PushFront(reinterpret_cast<void*>(static_cast<uintptr_t>(i)));
+ mWriting = false;
+ return true;
+ }
+};
+
+// Multi-thread writes allowed, make sure you don't pop unless writes can't
+// happen.
+struct DequeWrapperMW : DequeWrapperST {
+ mozilla::Mutex mMutex MOZ_UNANNOTATED;
+
+ DequeWrapperMW() : mMutex("DequeWrapperMW/MT") {}
+
+ bool Push(int i) {
+ mozilla::MutexAutoLock lock(mMutex);
+ mDQ.PushFront(reinterpret_cast<void*>(static_cast<uintptr_t>(i)));
+ return true;
+ }
+};
+
+// Multi-thread read&writes allowed.
+struct DequeWrapperMT : DequeWrapperMW {
+ template <typename F>
+ void PopAll(F&& aF) {
+ while (mDQ.GetSize() != 0) {
+ int i;
+ {
+ mozilla::MutexAutoLock lock(mMutex);
+ i = static_cast<int>(reinterpret_cast<uintptr_t>(mDQ.Pop()));
+ }
+ aF(i);
+ }
+ }
+};
+
+TEST(MultiWriterQueue, nsDequeBenchmark)
+{
+ TestMultiWriterQueueMT<DequeWrapperST>(1, 0, 2 * 1024 * 1024,
+ "DequeWrapperST ");
+
+ TestMultiWriterQueueMT<DequeWrapperAW>(1, 0, 2 * 1024 * 1024,
+ "DequeWrapperAW ");
+ TestMultiWriterQueueMT<DequeWrapperMW>(1, 0, 2 * 1024 * 1024,
+ "DequeWrapperMW ");
+ TestMultiWriterQueueMT<DequeWrapperMT>(1, 0, 2 * 1024 * 1024,
+ "DequeWrapperMT ");
+ TestMultiWriterQueueMT<DequeWrapperMT>(1, 1, 2 * 1024 * 1024,
+ "DequeWrapperMT ");
+
+ TestMultiWriterQueueMT<DequeWrapperAW>(8, 0, 2 * 1024 * 1024,
+ "DequeWrapperAW ");
+ TestMultiWriterQueueMT<DequeWrapperMW>(8, 0, 2 * 1024 * 1024,
+ "DequeWrapperMW ");
+ TestMultiWriterQueueMT<DequeWrapperMT>(8, 0, 2 * 1024 * 1024,
+ "DequeWrapperMT ");
+ TestMultiWriterQueueMT<DequeWrapperMT>(8, 1, 2 * 1024 * 1024,
+ "DequeWrapperMT ");
+}
diff --git a/dom/media/doctor/test/gtest/TestRollingNumber.cpp b/dom/media/doctor/test/gtest/TestRollingNumber.cpp
new file mode 100644
index 0000000000..cce06ae9ba
--- /dev/null
+++ b/dom/media/doctor/test/gtest/TestRollingNumber.cpp
@@ -0,0 +1,146 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "RollingNumber.h"
+
+#include "mozilla/Assertions.h"
+
+#include <cstdint>
+#include <gtest/gtest.h>
+#include <type_traits>
+
+using RN8 = mozilla::RollingNumber<uint8_t>;
+
+TEST(RollingNumber, Value)
+{
+ // Value type should reflect template argument.
+ static_assert(std::is_same_v<RN8::ValueType, uint8_t>);
+
+ // Default init to 0.
+ const RN8 n;
+ // Access through Value().
+ EXPECT_EQ(0, n.Value());
+
+ // Conversion constructor.
+ RN8 n42{42};
+ EXPECT_EQ(42, n42.Value());
+
+ // Copy Constructor.
+ RN8 n42Copied{n42};
+ EXPECT_EQ(42, n42Copied.Value());
+
+ // Assignment construction.
+ RN8 n42Assigned = n42;
+ EXPECT_EQ(42, n42Assigned.Value());
+
+ // Assignment.
+ n42 = n;
+ EXPECT_EQ(0, n42.Value());
+}
+
+TEST(RollingNumber, Operations)
+{
+ RN8 n;
+ EXPECT_EQ(0, n.Value());
+
+ RN8 nPreInc = ++n;
+ EXPECT_EQ(1, n.Value());
+ EXPECT_EQ(1, nPreInc.Value());
+
+ RN8 nPostInc = n++;
+ EXPECT_EQ(2, n.Value());
+ EXPECT_EQ(1, nPostInc.Value());
+
+ RN8 nPreDec = --n;
+ EXPECT_EQ(1, n.Value());
+ EXPECT_EQ(1, nPreDec.Value());
+
+ RN8 nPostDec = n--;
+ EXPECT_EQ(0, n.Value());
+ EXPECT_EQ(1, nPostDec.Value());
+
+ RN8 nPlus = n + 10;
+ EXPECT_EQ(0, n.Value());
+ EXPECT_EQ(10, nPlus.Value());
+
+ n += 20;
+ EXPECT_EQ(20, n.Value());
+
+ RN8 nMinus = n - 2;
+ EXPECT_EQ(20, n.Value());
+ EXPECT_EQ(18, nMinus.Value());
+
+ n -= 5;
+ EXPECT_EQ(15, n.Value());
+
+ uint8_t diff = nMinus - n;
+ EXPECT_EQ(3, diff);
+
+ // Overflows.
+ n = RN8(0);
+ EXPECT_EQ(0, n.Value());
+ n--;
+ EXPECT_EQ(255, n.Value());
+ n++;
+ EXPECT_EQ(0, n.Value());
+ n -= 10;
+ EXPECT_EQ(246, n.Value());
+ n += 20;
+ EXPECT_EQ(10, n.Value());
+}
+
+TEST(RollingNumber, Comparisons)
+{
+ uint8_t i = 0;
+ do {
+ RN8 n{i};
+ EXPECT_EQ(i, n.Value());
+ EXPECT_TRUE(n == n);
+ EXPECT_FALSE(n != n);
+ EXPECT_FALSE(n < n);
+ EXPECT_TRUE(n <= n);
+ EXPECT_FALSE(n > n);
+ EXPECT_TRUE(n >= n);
+
+ RN8 same = n;
+ EXPECT_TRUE(n == same);
+ EXPECT_FALSE(n != same);
+ EXPECT_FALSE(n < same);
+ EXPECT_TRUE(n <= same);
+ EXPECT_FALSE(n > same);
+ EXPECT_TRUE(n >= same);
+
+#ifdef DEBUG
+ // In debug builds, we are only allowed a quarter of the type range.
+ const uint8_t maxDiff = 255 / 4;
+#else
+ // In non-debug builds, we can go half-way up or down the type range, and
+ // still conserve the expected ordering.
+ const uint8_t maxDiff = 255 / 2;
+#endif
+ for (uint8_t add = 1; add <= maxDiff; ++add) {
+ RN8 bigger = n + add;
+ EXPECT_FALSE(n == bigger);
+ EXPECT_TRUE(n != bigger);
+ EXPECT_TRUE(n < bigger);
+ EXPECT_TRUE(n <= bigger);
+ EXPECT_FALSE(n > bigger);
+ EXPECT_FALSE(n >= bigger);
+ }
+
+ for (uint8_t sub = 1; sub <= maxDiff; ++sub) {
+ RN8 smaller = n - sub;
+ EXPECT_FALSE(n == smaller);
+ EXPECT_TRUE(n != smaller);
+ EXPECT_FALSE(n < smaller);
+ EXPECT_FALSE(n <= smaller);
+ EXPECT_TRUE(n > smaller);
+ EXPECT_TRUE(n >= smaller);
+ }
+
+ ++i;
+ } while (i != 0);
+}
diff --git a/dom/media/doctor/test/gtest/moz.build b/dom/media/doctor/test/gtest/moz.build
new file mode 100644
index 0000000000..7ae9eae130
--- /dev/null
+++ b/dom/media/doctor/test/gtest/moz.build
@@ -0,0 +1,19 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+if CONFIG["OS_TARGET"] != "Android":
+ UNIFIED_SOURCES += [
+ "TestMultiWriterQueue.cpp",
+ "TestRollingNumber.cpp",
+ ]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+LOCAL_INCLUDES += [
+ "/dom/media/doctor",
+]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/dom/media/eme/CDMCaps.cpp b/dom/media/eme/CDMCaps.cpp
new file mode 100644
index 0000000000..2752ada476
--- /dev/null
+++ b/dom/media/eme/CDMCaps.cpp
@@ -0,0 +1,112 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/CDMCaps.h"
+#include "mozilla/EMEUtils.h"
+#include "nsThreadUtils.h"
+#include "SamplesWaitingForKey.h"
+
+namespace mozilla {
+
+CDMCaps::CDMCaps() = default;
+
+CDMCaps::~CDMCaps() = default;
+
+// Keys with MediaKeyStatus::Usable, MediaKeyStatus::Output_downscaled,
+// or MediaKeyStatus::Output_restricted status can be used by the CDM
+// to decrypt or decrypt-and-decode samples.
+static bool IsUsableStatus(dom::MediaKeyStatus aStatus) {
+ return aStatus == dom::MediaKeyStatus::Usable ||
+ aStatus == dom::MediaKeyStatus::Output_restricted ||
+ aStatus == dom::MediaKeyStatus::Output_downscaled;
+}
+
+bool CDMCaps::IsKeyUsable(const CencKeyId& aKeyId) {
+ for (const KeyStatus& keyStatus : mKeyStatuses) {
+ if (keyStatus.mId == aKeyId) {
+ return IsUsableStatus(keyStatus.mStatus);
+ }
+ }
+ return false;
+}
+
+bool CDMCaps::SetKeyStatus(const CencKeyId& aKeyId, const nsString& aSessionId,
+ const dom::Optional<dom::MediaKeyStatus>& aStatus) {
+ if (!aStatus.WasPassed()) {
+ // Called from ForgetKeyStatus.
+ // Return true if the element is found to notify key changes.
+ return mKeyStatuses.RemoveElement(
+ KeyStatus(aKeyId, aSessionId, dom::MediaKeyStatus::Internal_error));
+ }
+
+ KeyStatus key(aKeyId, aSessionId, aStatus.Value());
+ auto index = mKeyStatuses.IndexOf(key);
+ if (index != mKeyStatuses.NoIndex) {
+ if (mKeyStatuses[index].mStatus == aStatus.Value()) {
+ // No change.
+ return false;
+ }
+ auto oldStatus = mKeyStatuses[index].mStatus;
+ mKeyStatuses[index].mStatus = aStatus.Value();
+ // The old key status was one for which we can decrypt media. We don't
+ // need to do the "notify usable" step below, as it should be impossible
+ // for us to have anything waiting on this key to become usable, since it
+ // was already usable.
+ if (IsUsableStatus(oldStatus)) {
+ return true;
+ }
+ } else {
+ mKeyStatuses.AppendElement(key);
+ }
+
+ // Only call NotifyUsable() for a key when we are going from non-usable
+ // to usable state.
+ if (!IsUsableStatus(aStatus.Value())) {
+ return true;
+ }
+
+ auto& waiters = mWaitForKeys;
+ size_t i = 0;
+ while (i < waiters.Length()) {
+ auto& w = waiters[i];
+ if (w.mKeyId == aKeyId) {
+ w.mListener->NotifyUsable(aKeyId);
+ waiters.RemoveElementAt(i);
+ } else {
+ i++;
+ }
+ }
+ return true;
+}
+
+void CDMCaps::NotifyWhenKeyIdUsable(const CencKeyId& aKey,
+ SamplesWaitingForKey* aListener) {
+ MOZ_ASSERT(!IsKeyUsable(aKey));
+ MOZ_ASSERT(aListener);
+ mWaitForKeys.AppendElement(WaitForKeys(aKey, aListener));
+}
+
+void CDMCaps::GetKeyStatusesForSession(const nsAString& aSessionId,
+ nsTArray<KeyStatus>& aOutKeyStatuses) {
+ for (const KeyStatus& keyStatus : mKeyStatuses) {
+ if (keyStatus.mSessionId.Equals(aSessionId)) {
+ aOutKeyStatuses.AppendElement(keyStatus);
+ }
+ }
+}
+
+bool CDMCaps::RemoveKeysForSession(const nsString& aSessionId) {
+ bool changed = false;
+ nsTArray<KeyStatus> statuses;
+ GetKeyStatusesForSession(aSessionId, statuses);
+ for (const KeyStatus& status : statuses) {
+ changed |= SetKeyStatus(status.mId, aSessionId,
+ dom::Optional<dom::MediaKeyStatus>());
+ }
+ return changed;
+}
+
+} // namespace mozilla
diff --git a/dom/media/eme/CDMCaps.h b/dom/media/eme/CDMCaps.h
new file mode 100644
index 0000000000..b16c5153b1
--- /dev/null
+++ b/dom/media/eme/CDMCaps.h
@@ -0,0 +1,82 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef CDMCaps_h_
+#define CDMCaps_h_
+
+#include "nsTArray.h"
+#include "nsString.h"
+#include "SamplesWaitingForKey.h"
+
+#include "mozilla/Monitor.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/MediaKeyStatusMapBinding.h" // For MediaKeyStatus
+#include "mozilla/dom/BindingDeclarations.h" // For Optional
+
+namespace mozilla {
+
+// CDM capabilities; what keys a CDMProxy can use.
+// Must be locked to access state.
+class CDMCaps {
+ public:
+ CDMCaps();
+ ~CDMCaps();
+
+ struct KeyStatus {
+ KeyStatus(const CencKeyId& aId, const nsString& aSessionId,
+ dom::MediaKeyStatus aStatus)
+ : mId(aId.Clone()), mSessionId(aSessionId), mStatus(aStatus) {}
+ KeyStatus(const KeyStatus& aOther)
+ : mId(aOther.mId.Clone()),
+ mSessionId(aOther.mSessionId),
+ mStatus(aOther.mStatus) {}
+ bool operator==(const KeyStatus& aOther) const {
+ return mId == aOther.mId && mSessionId == aOther.mSessionId;
+ };
+
+ CencKeyId mId;
+ nsString mSessionId;
+ dom::MediaKeyStatus mStatus;
+ };
+
+ bool IsKeyUsable(const CencKeyId& aKeyId);
+
+ // Returns true if key status changed,
+ // i.e. the key status changed from usable to expired.
+ bool SetKeyStatus(const CencKeyId& aKeyId, const nsString& aSessionId,
+ const dom::Optional<dom::MediaKeyStatus>& aStatus);
+
+ void GetKeyStatusesForSession(const nsAString& aSessionId,
+ nsTArray<KeyStatus>& aOutKeyStatuses);
+
+ // Ensures all keys for a session are marked as 'unknown', i.e. removed.
+ // Returns true if a key status was changed.
+ bool RemoveKeysForSession(const nsString& aSessionId);
+
+ // Notifies the SamplesWaitingForKey when key become usable.
+ void NotifyWhenKeyIdUsable(const CencKeyId& aKey,
+ SamplesWaitingForKey* aSamplesWaiting);
+
+ private:
+ struct WaitForKeys {
+ WaitForKeys(const CencKeyId& aKeyId, SamplesWaitingForKey* aListener)
+ : mKeyId(aKeyId.Clone()), mListener(aListener) {}
+ CencKeyId mKeyId;
+ RefPtr<SamplesWaitingForKey> mListener;
+ };
+
+ nsTArray<KeyStatus> mKeyStatuses;
+
+ nsTArray<WaitForKeys> mWaitForKeys;
+
+ // It is not safe to copy this object.
+ CDMCaps(const CDMCaps&) = delete;
+ CDMCaps& operator=(const CDMCaps&) = delete;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/eme/CDMProxy.h b/dom/media/eme/CDMProxy.h
new file mode 100644
index 0000000000..e638be3358
--- /dev/null
+++ b/dom/media/eme/CDMProxy.h
@@ -0,0 +1,323 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef CDMProxy_h_
+#define CDMProxy_h_
+
+#include "mozilla/CDMCaps.h"
+#include "mozilla/DataMutex.h"
+#include "mozilla/MozPromise.h"
+
+#include "mozilla/dom/MediaKeyMessageEvent.h"
+#include "mozilla/dom/MediaKeys.h"
+
+#include "nsIThread.h"
+
+namespace mozilla {
+class ErrorResult;
+class MediaRawData;
+class ChromiumCDMProxy;
+#ifdef MOZ_WMF_CDM
+class WMFCDMProxy;
+#endif
+
+namespace eme {
+enum DecryptStatus {
+ Ok = 0,
+ GenericErr = 1,
+ NoKeyErr = 2,
+ AbortedErr = 3,
+};
+}
+
+using eme::DecryptStatus;
+
+struct DecryptResult {
+ DecryptResult(DecryptStatus aStatus, MediaRawData* aSample)
+ : mStatus(aStatus), mSample(aSample) {}
+ DecryptStatus mStatus;
+ RefPtr<MediaRawData> mSample;
+};
+
+typedef MozPromise<DecryptResult, DecryptResult, /* IsExclusive = */ true>
+ DecryptPromise;
+
+class CDMKeyInfo {
+ public:
+ explicit CDMKeyInfo(const nsTArray<uint8_t>& aKeyId)
+ : mKeyId(aKeyId.Clone()), mStatus() {}
+
+ CDMKeyInfo(const nsTArray<uint8_t>& aKeyId,
+ const dom::Optional<dom::MediaKeyStatus>& aStatus)
+ : mKeyId(aKeyId.Clone()), mStatus(aStatus.Value()) {}
+
+ // The copy-ctor and copy-assignment operator for Optional<T> are declared as
+ // delete, so override CDMKeyInfo copy-ctor for nsTArray operations.
+ CDMKeyInfo(const CDMKeyInfo& aKeyInfo) {
+ mKeyId = aKeyInfo.mKeyId.Clone();
+ if (aKeyInfo.mStatus.WasPassed()) {
+ mStatus.Construct(aKeyInfo.mStatus.Value());
+ }
+ }
+
+ nsTArray<uint8_t> mKeyId;
+ dom::Optional<dom::MediaKeyStatus> mStatus;
+};
+
+// Time is defined as the number of milliseconds since the
+// Epoch (00:00:00 UTC, January 1, 1970).
+typedef int64_t UnixTime;
+
+// Proxies calls CDM, and proxies calls back.
+// Note: Promises are passed in via a PromiseId, so that the ID can be
+// passed via IPC to the CDM, which can then signal when to reject or
+// resolve the promise using its PromiseId.
+class CDMProxy {
+ protected:
+ typedef dom::PromiseId PromiseId;
+ typedef dom::MediaKeySessionType MediaKeySessionType;
+
+ public:
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+ // Main thread only.
+ // Loads the CDM corresponding to mKeySystem.
+ // Calls MediaKeys::OnCDMCreated() when the CDM is created.
+ virtual void Init(PromiseId aPromiseId, const nsAString& aOrigin,
+ const nsAString& aTopLevelOrigin,
+ const nsAString& aName) = 0;
+
+ // Main thread only.
+ // Uses the CDM to create a key session.
+ // Calls MediaKeys::OnSessionActivated() when session is created.
+ // Assumes ownership of (std::move()s) aInitData's contents.
+ virtual void CreateSession(uint32_t aCreateSessionToken,
+ MediaKeySessionType aSessionType,
+ PromiseId aPromiseId,
+ const nsAString& aInitDataType,
+ nsTArray<uint8_t>& aInitData) = 0;
+
+ // Main thread only.
+ // Uses the CDM to load a presistent session stored on disk.
+ // Calls MediaKeys::OnSessionActivated() when session is loaded.
+ virtual void LoadSession(PromiseId aPromiseId,
+ dom::MediaKeySessionType aSessionType,
+ const nsAString& aSessionId) = 0;
+
+ // Main thread only.
+ // Sends a new certificate to the CDM.
+ // Calls MediaKeys->ResolvePromise(aPromiseId) after the CDM has
+ // processed the request.
+ // Assumes ownership of (std::move()s) aCert's contents.
+ virtual void SetServerCertificate(PromiseId aPromiseId,
+ nsTArray<uint8_t>& aCert) = 0;
+
+ // Main thread only.
+ // Sends an update to the CDM.
+ // Calls MediaKeys->ResolvePromise(aPromiseId) after the CDM has
+ // processed the request.
+ // Assumes ownership of (std::move()s) aResponse's contents.
+ virtual void UpdateSession(const nsAString& aSessionId, PromiseId aPromiseId,
+ nsTArray<uint8_t>& aResponse) = 0;
+
+ // Main thread only.
+ // Calls MediaKeys->ResolvePromise(aPromiseId) after the CDM has
+ // processed the request.
+ // If processing this operation results in the session actually closing,
+ // we also call MediaKeySession::OnClosed(), which in turn calls
+ // MediaKeys::OnSessionClosed().
+ virtual void CloseSession(const nsAString& aSessionId,
+ PromiseId aPromiseId) = 0;
+
+ // Main thread only.
+ // Removes all data for a persisent session.
+ // Calls MediaKeys->ResolvePromise(aPromiseId) after the CDM has
+ // processed the request.
+ virtual void RemoveSession(const nsAString& aSessionId,
+ PromiseId aPromiseId) = 0;
+
+ // Main thread only.
+ // Called to signal a request for output protection information from the CDM.
+ // This should forward the call up the stack where the query should be
+ // performed and then responded to via `NotifyOutputProtectionStatus`.
+ virtual void QueryOutputProtectionStatus() = 0;
+
+ // NotifyOutputProtectionStatus enums. Explicit values are specified to make
+ // it easy to match values in logs.
+ enum class OutputProtectionCheckStatus : uint8_t {
+ CheckFailed = 0,
+ CheckSuccessful = 1,
+ };
+
+ enum class OutputProtectionCaptureStatus : uint8_t {
+ CapturePossilbe = 0,
+ CaptureNotPossible = 1,
+ Unused = 2,
+ };
+ // End NotifyOutputProtectionStatus enums
+
+ // Main thread only.
+ // Notifies this proxy of the protection status for the media the CDM is
+ // associated with. This can be called in response to
+ // `QueryOutputProtectionStatus`, but can also be called without an
+ // associated query. In both cases the information will be forwarded to
+ // the CDM host machinery and used to handle requests from the CDM.
+ // @param aCheckStatus did the check succeed or not.
+ // @param aCaptureStatus if the check succeeded, this reflects if capture
+ // of media could take place. This doesn't mean capture is taking place.
+ // Callers should be conservative with this value such that it's okay to pass
+ // CapturePossilbe even if capture is not happening, but should never pass
+ // CaptureNotPossible if it could happen. If the check failed, this value is
+ // not used, and callers should pass Unused to indicate this.
+ virtual void NotifyOutputProtectionStatus(
+ OutputProtectionCheckStatus aCheckStatus,
+ OutputProtectionCaptureStatus aCaptureStatus) = 0;
+
+ // Main thread only.
+ virtual void Shutdown() = 0;
+
+ // Main thread only.
+ virtual void Terminated() = 0;
+
+ // Threadsafe.
+ const nsCString& GetNodeId() const { return mNodeId; };
+
+ // Main thread only.
+ virtual void OnSetSessionId(uint32_t aCreateSessionToken,
+ const nsAString& aSessionId) = 0;
+
+ // Main thread only.
+ virtual void OnResolveLoadSessionPromise(uint32_t aPromiseId,
+ bool aSuccess) = 0;
+
+ // Main thread only.
+ virtual void OnSessionMessage(const nsAString& aSessionId,
+ dom::MediaKeyMessageType aMessageType,
+ const nsTArray<uint8_t>& aMessage) = 0;
+
+ // Main thread only.
+ virtual void OnExpirationChange(const nsAString& aSessionId,
+ UnixTime aExpiryTime) = 0;
+
+ // Main thread only.
+ virtual void OnSessionClosed(const nsAString& aSessionId) = 0;
+
+ // Main thread only.
+ virtual void OnSessionError(const nsAString& aSessionId, nsresult aException,
+ uint32_t aSystemCode, const nsAString& aMsg) = 0;
+
+ // Main thread only.
+ virtual void OnRejectPromise(uint32_t aPromiseId, ErrorResult&& aException,
+ const nsCString& aMsg) = 0;
+
+ virtual RefPtr<DecryptPromise> Decrypt(MediaRawData* aSample) = 0;
+
+ // Owner thread only.
+ virtual void OnDecrypted(uint32_t aId, DecryptStatus aResult,
+ const nsTArray<uint8_t>& aDecryptedData) = 0;
+
+ // Reject promise with the given ErrorResult.
+ //
+ // Can be called from any thread.
+ virtual void RejectPromise(PromiseId aId, ErrorResult&& aException,
+ const nsCString& aReason) = 0;
+
+ // Resolves promise with "undefined".
+ // Can be called from any thread.
+ virtual void ResolvePromise(PromiseId aId) = 0;
+
+ // Threadsafe.
+ const nsString& KeySystem() const { return mKeySystem; };
+
+ DataMutex<CDMCaps>& Capabilites() { return mCapabilites; };
+
+ // Main thread only.
+ virtual void OnKeyStatusesChange(const nsAString& aSessionId) = 0;
+
+ // Main thread only.
+ // Calls MediaKeys->ResolvePromiseWithKeyStatus(aPromiseId, aKeyStatus) after
+ // the CDM has processed the request.
+ virtual void GetStatusForPolicy(PromiseId aPromiseId,
+ const nsAString& aMinHdcpVersion) = 0;
+
+#ifdef DEBUG
+ virtual bool IsOnOwnerThread() = 0;
+#endif
+
+ virtual ChromiumCDMProxy* AsChromiumCDMProxy() { return nullptr; }
+
+#ifdef MOZ_WMF_CDM
+ virtual WMFCDMProxy* AsWMFCDMProxy() { return nullptr; }
+#endif
+
+ protected:
+ // Main thread only.
+ CDMProxy(dom::MediaKeys* aKeys, const nsAString& aKeySystem,
+ bool aDistinctiveIdentifierRequired, bool aPersistentStateRequired)
+ : mKeys(aKeys),
+ mKeySystem(aKeySystem),
+ mCapabilites("CDMProxy::mCDMCaps"),
+ mDistinctiveIdentifierRequired(aDistinctiveIdentifierRequired),
+ mPersistentStateRequired(aPersistentStateRequired),
+ mMainThread(GetMainThreadSerialEventTarget()) {
+ MOZ_ASSERT(NS_IsMainThread());
+ }
+
+ virtual ~CDMProxy() {}
+
+ // Helper to enforce that a raw pointer is only accessed on the main thread.
+ template <class Type>
+ class MainThreadOnlyRawPtr {
+ public:
+ explicit MainThreadOnlyRawPtr(Type* aPtr) : mPtr(aPtr) {
+ MOZ_ASSERT(NS_IsMainThread());
+ }
+
+ bool IsNull() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return !mPtr;
+ }
+
+ void Clear() {
+ MOZ_ASSERT(NS_IsMainThread());
+ mPtr = nullptr;
+ }
+
+ Type* operator->() const MOZ_NO_ADDREF_RELEASE_ON_RETURN {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mPtr;
+ }
+
+ private:
+ Type* mPtr;
+ };
+
+ // Our reference back to the MediaKeys object.
+ // WARNING: This is a non-owning reference that is cleared by MediaKeys
+ // destructor. only use on main thread, and always nullcheck before using!
+ MainThreadOnlyRawPtr<dom::MediaKeys> mKeys;
+
+ const nsString mKeySystem;
+
+ // Onwer specified thread. e.g. Gecko Media Plugin thread.
+ // All interactions with the out-of-process EME plugin must come from this
+ // thread.
+ RefPtr<nsIThread> mOwnerThread;
+
+ nsCString mNodeId;
+
+ DataMutex<CDMCaps> mCapabilites;
+
+ const bool mDistinctiveIdentifierRequired;
+ const bool mPersistentStateRequired;
+
+ // The main thread associated with the root document.
+ const nsCOMPtr<nsISerialEventTarget> mMainThread;
+};
+
+} // namespace mozilla
+
+#endif // CDMProxy_h_
diff --git a/dom/media/eme/DecryptorProxyCallback.h b/dom/media/eme/DecryptorProxyCallback.h
new file mode 100644
index 0000000000..88acdd7967
--- /dev/null
+++ b/dom/media/eme/DecryptorProxyCallback.h
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DecryptorProxyCallback_h_
+#define DecryptorProxyCallback_h_
+
+#include "mozilla/dom/MediaKeyStatusMapBinding.h" // For MediaKeyStatus
+#include "mozilla/dom/MediaKeyMessageEventBinding.h" // For MediaKeyMessageType
+#include "mozilla/CDMProxy.h"
+
+namespace mozilla {
+class ErrorResult;
+}
+
+class DecryptorProxyCallback {
+ public:
+ virtual ~DecryptorProxyCallback() {}
+
+ virtual void SetSessionId(uint32_t aCreateSessionId,
+ const nsCString& aSessionId) = 0;
+
+ virtual void ResolveLoadSessionPromise(uint32_t aPromiseId,
+ bool aSuccess) = 0;
+
+ virtual void ResolvePromise(uint32_t aPromiseId) = 0;
+
+ virtual void RejectPromise(uint32_t aPromiseId,
+ mozilla::ErrorResult&& aException,
+ const nsCString& aSessionId) = 0;
+
+ virtual void SessionMessage(const nsCString& aSessionId,
+ mozilla::dom::MediaKeyMessageType aMessageType,
+ const nsTArray<uint8_t>& aMessage) = 0;
+
+ virtual void ExpirationChange(const nsCString& aSessionId,
+ mozilla::UnixTime aExpiryTime) = 0;
+
+ virtual void SessionClosed(const nsCString& aSessionId) = 0;
+
+ virtual void SessionError(const nsCString& aSessionId, nsresult aException,
+ uint32_t aSystemCode,
+ const nsCString& aMessage) = 0;
+
+ virtual void Decrypted(uint32_t aId, mozilla::DecryptStatus aResult,
+ const nsTArray<uint8_t>& aDecryptedData) = 0;
+
+ virtual void BatchedKeyStatusChanged(
+ const nsCString& aSessionId,
+ const nsTArray<mozilla::CDMKeyInfo>& aKeyInfos) = 0;
+};
+
+#endif
diff --git a/dom/media/eme/DetailedPromise.cpp b/dom/media/eme/DetailedPromise.cpp
new file mode 100644
index 0000000000..0692a1dd07
--- /dev/null
+++ b/dom/media/eme/DetailedPromise.cpp
@@ -0,0 +1,87 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DetailedPromise.h"
+
+#include "VideoUtils.h"
+#include "mozilla/dom/DOMException.h"
+#include "nsPrintfCString.h"
+
+namespace mozilla::dom {
+
+DetailedPromise::DetailedPromise(nsIGlobalObject* aGlobal,
+ const nsACString& aName)
+ : Promise(aGlobal),
+ mName(aName),
+ mResponded(false),
+ mStartTime(TimeStamp::Now()) {}
+
+DetailedPromise::DetailedPromise(nsIGlobalObject* aGlobal,
+ const nsACString& aName,
+ Telemetry::HistogramID aSuccessLatencyProbe,
+ Telemetry::HistogramID aFailureLatencyProbe)
+ : DetailedPromise(aGlobal, aName) {
+ mSuccessLatencyProbe.Construct(aSuccessLatencyProbe);
+ mFailureLatencyProbe.Construct(aFailureLatencyProbe);
+}
+
+DetailedPromise::~DetailedPromise() {
+ // It would be nice to assert that mResponded is identical to
+ // GetPromiseState() == PromiseState::Rejected. But by now we've been
+ // unlinked, so don't have a reference to our actual JS Promise object
+ // anymore.
+ MaybeReportTelemetry(kFailed);
+}
+
+void DetailedPromise::LogRejectionReason(uint32_t aErrorCode,
+ const nsACString& aReason) {
+ nsPrintfCString msg("%s promise rejected 0x%" PRIx32 " '%s'", mName.get(),
+ aErrorCode, PromiseFlatCString(aReason).get());
+ EME_LOG("%s", msg.get());
+
+ MaybeReportTelemetry(kFailed);
+
+ LogToBrowserConsole(NS_ConvertUTF8toUTF16(msg));
+}
+
+void DetailedPromise::MaybeReject(nsresult aArg, const nsACString& aReason) {
+ LogRejectionReason(static_cast<uint32_t>(aArg), aReason);
+
+ Promise::MaybeRejectWithDOMException(aArg, aReason);
+}
+
+void DetailedPromise::MaybeReject(ErrorResult&& aArg,
+ const nsACString& aReason) {
+ LogRejectionReason(aArg.ErrorCodeAsInt(), aReason);
+ Promise::MaybeReject(std::move(aArg));
+}
+
+/* static */
+already_AddRefed<DetailedPromise> DetailedPromise::Create(
+ nsIGlobalObject* aGlobal, ErrorResult& aRv, const nsACString& aName) {
+ RefPtr<DetailedPromise> promise = new DetailedPromise(aGlobal, aName);
+ promise->CreateWrapper(aRv);
+ return aRv.Failed() ? nullptr : promise.forget();
+}
+
+void DetailedPromise::MaybeReportTelemetry(eStatus aStatus) {
+ if (mResponded) {
+ return;
+ }
+ mResponded = true;
+ if (!mSuccessLatencyProbe.WasPassed() || !mFailureLatencyProbe.WasPassed()) {
+ return;
+ }
+ uint32_t latency = (TimeStamp::Now() - mStartTime).ToMilliseconds();
+ EME_LOG("%s %s latency %ums reported via telemetry", mName.get(),
+ ((aStatus == kSucceeded) ? "succcess" : "failure"), latency);
+ Telemetry::HistogramID tid = (aStatus == kSucceeded)
+ ? mSuccessLatencyProbe.Value()
+ : mFailureLatencyProbe.Value();
+ Telemetry::Accumulate(tid, latency);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/eme/DetailedPromise.h b/dom/media/eme/DetailedPromise.h
new file mode 100644
index 0000000000..02c774755f
--- /dev/null
+++ b/dom/media/eme/DetailedPromise.h
@@ -0,0 +1,104 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef __DetailedPromise_h__
+#define __DetailedPromise_h__
+
+#include "mozilla/dom/Promise.h"
+#include "mozilla/Telemetry.h"
+#include "EMEUtils.h"
+
+namespace mozilla::dom {
+
+/*
+ * This is pretty horrible; bug 1160445.
+ * Extend Promise to add custom DOMException messages on rejection.
+ * Get rid of this once we've ironed out EME errors in the wild.
+ */
+class DetailedPromise : public Promise {
+ public:
+ static already_AddRefed<DetailedPromise> Create(nsIGlobalObject* aGlobal,
+ ErrorResult& aRv,
+ const nsACString& aName);
+
+ template <typename T>
+ void MaybeResolve(T&& aArg) {
+ EME_LOG("%s promise resolved", mName.get());
+ MaybeReportTelemetry(eStatus::kSucceeded);
+ Promise::MaybeResolve(std::forward<T>(aArg));
+ }
+
+ void MaybeReject(nsresult aArg) = delete;
+ void MaybeReject(nsresult aArg, const nsACString& aReason);
+
+ void MaybeReject(ErrorResult&& aArg) = delete;
+ void MaybeReject(ErrorResult&& aArg, const nsACString& aReason);
+
+ // Facilities for rejecting with various spec-defined exception values.
+#define DOMEXCEPTION(name, err) \
+ inline void MaybeRejectWith##name(const nsACString& aMessage) { \
+ LogRejectionReason(static_cast<uint32_t>(err), aMessage); \
+ Promise::MaybeRejectWith##name(aMessage); \
+ } \
+ template <int N> \
+ void MaybeRejectWith##name(const char(&aMessage)[N]) { \
+ MaybeRejectWith##name(nsLiteralCString(aMessage)); \
+ }
+
+#include "mozilla/dom/DOMExceptionNames.h"
+
+#undef DOMEXCEPTION
+
+ template <ErrNum errorNumber, typename... Ts>
+ void MaybeRejectWithTypeError(Ts&&... aMessageArgs) = delete;
+
+ inline void MaybeRejectWithTypeError(const nsACString& aMessage) {
+ ErrorResult res;
+ res.ThrowTypeError(aMessage);
+ MaybeReject(std::move(res), aMessage);
+ }
+
+ template <int N>
+ void MaybeRejectWithTypeError(const char (&aMessage)[N]) {
+ MaybeRejectWithTypeError(nsLiteralCString(aMessage));
+ }
+
+ template <ErrNum errorNumber, typename... Ts>
+ void MaybeRejectWithRangeError(Ts&&... aMessageArgs) = delete;
+
+ inline void MaybeRejectWithRangeError(const nsACString& aMessage) {
+ ErrorResult res;
+ res.ThrowRangeError(aMessage);
+ MaybeReject(std::move(res), aMessage);
+ }
+
+ template <int N>
+ void MaybeRejectWithRangeError(const char (&aMessage)[N]) {
+ MaybeRejectWithRangeError(nsLiteralCString(aMessage));
+ }
+
+ private:
+ explicit DetailedPromise(nsIGlobalObject* aGlobal, const nsACString& aName);
+
+ explicit DetailedPromise(nsIGlobalObject* aGlobal, const nsACString& aName,
+ Telemetry::HistogramID aSuccessLatencyProbe,
+ Telemetry::HistogramID aFailureLatencyProbe);
+ virtual ~DetailedPromise();
+
+ enum eStatus { kSucceeded, kFailed };
+ void MaybeReportTelemetry(eStatus aStatus);
+ void LogRejectionReason(uint32_t aErrorCode, const nsACString& aReason);
+
+ nsCString mName;
+ bool mResponded;
+ TimeStamp mStartTime;
+ Optional<Telemetry::HistogramID> mSuccessLatencyProbe;
+ Optional<Telemetry::HistogramID> mFailureLatencyProbe;
+};
+
+} // namespace mozilla::dom
+
+#endif // __DetailedPromise_h__
diff --git a/dom/media/eme/EMEUtils.cpp b/dom/media/eme/EMEUtils.cpp
new file mode 100644
index 0000000000..0f1630589e
--- /dev/null
+++ b/dom/media/eme/EMEUtils.cpp
@@ -0,0 +1,119 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/EMEUtils.h"
+
+#include "jsfriendapi.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/dom/KeySystemNames.h"
+#include "mozilla/dom/UnionTypes.h"
+
+namespace mozilla {
+
+LogModule* GetEMELog() {
+ static LazyLogModule log("EME");
+ return log;
+}
+
+LogModule* GetEMEVerboseLog() {
+ static LazyLogModule log("EMEV");
+ return log;
+}
+
+ArrayData GetArrayBufferViewOrArrayBufferData(
+ const dom::ArrayBufferViewOrArrayBuffer& aBufferOrView) {
+ MOZ_ASSERT(aBufferOrView.IsArrayBuffer() ||
+ aBufferOrView.IsArrayBufferView());
+ JS::AutoCheckCannotGC nogc;
+ if (aBufferOrView.IsArrayBuffer()) {
+ const dom::ArrayBuffer& buffer = aBufferOrView.GetAsArrayBuffer();
+ buffer.ComputeState();
+ return ArrayData(buffer.Data(), buffer.Length());
+ } else if (aBufferOrView.IsArrayBufferView()) {
+ const dom::ArrayBufferView& bufferview =
+ aBufferOrView.GetAsArrayBufferView();
+ bufferview.ComputeState();
+ return ArrayData(bufferview.Data(), bufferview.Length());
+ }
+ return ArrayData(nullptr, 0);
+}
+
+void CopyArrayBufferViewOrArrayBufferData(
+ const dom::ArrayBufferViewOrArrayBuffer& aBufferOrView,
+ nsTArray<uint8_t>& aOutData) {
+ JS::AutoCheckCannotGC nogc;
+ ArrayData data = GetArrayBufferViewOrArrayBufferData(aBufferOrView);
+ aOutData.Clear();
+ if (!data.IsValid()) {
+ return;
+ }
+ aOutData.AppendElements(data.mData, data.mLength);
+}
+
+bool IsClearkeyKeySystem(const nsAString& aKeySystem) {
+ if (StaticPrefs::media_clearkey_test_key_systems_enabled()) {
+ return aKeySystem.EqualsLiteral(kClearKeyKeySystemName) ||
+ aKeySystem.EqualsLiteral(kClearKeyWithProtectionQueryKeySystemName);
+ }
+ return aKeySystem.EqualsLiteral(kClearKeyKeySystemName);
+}
+
+bool IsWidevineKeySystem(const nsAString& aKeySystem) {
+ return aKeySystem.EqualsLiteral(kWidevineKeySystemName);
+}
+
+#ifdef MOZ_WMF_CDM
+bool IsPlayReadyKeySystemAndSupported(const nsAString& aKeySystem) {
+ if (!StaticPrefs::media_eme_playready_enabled()) {
+ return false;
+ }
+ // 1=enabled encrypted and clear, 2=enabled encrytped.
+ if (StaticPrefs::media_wmf_media_engine_enabled() != 1 &&
+ StaticPrefs::media_wmf_media_engine_enabled() != 2) {
+ return false;
+ }
+ return aKeySystem.EqualsLiteral(kPlayReadyKeySystemName) ||
+ aKeySystem.EqualsLiteral(kPlayReadyKeySystemHardware);
+}
+#endif
+
+nsString KeySystemToProxyName(const nsAString& aKeySystem) {
+ if (IsClearkeyKeySystem(aKeySystem)) {
+ return u"gmp-clearkey"_ns;
+ }
+ if (IsWidevineKeySystem(aKeySystem)) {
+ return u"gmp-widevinecdm"_ns;
+ }
+#ifdef MOZ_WMF_CDM
+ if (IsPlayReadyKeySystemAndSupported(aKeySystem)) {
+ return u"mfcdm-playready"_ns;
+ }
+#endif
+ MOZ_ASSERT_UNREACHABLE("Not supported key system!");
+ return u""_ns;
+}
+
+#define ENUM_TO_STR(enumVal) \
+ case enumVal: \
+ return #enumVal
+
+const char* ToMediaKeyStatusStr(dom::MediaKeyStatus aStatus) {
+ switch (aStatus) {
+ ENUM_TO_STR(dom::MediaKeyStatus::Usable);
+ ENUM_TO_STR(dom::MediaKeyStatus::Expired);
+ ENUM_TO_STR(dom::MediaKeyStatus::Released);
+ ENUM_TO_STR(dom::MediaKeyStatus::Output_restricted);
+ ENUM_TO_STR(dom::MediaKeyStatus::Output_downscaled);
+ ENUM_TO_STR(dom::MediaKeyStatus::Status_pending);
+ ENUM_TO_STR(dom::MediaKeyStatus::Internal_error);
+ default:
+ return "Undefined MediaKeyStatus!";
+ }
+}
+
+#undef ENUM_TO_STR
+
+} // namespace mozilla
diff --git a/dom/media/eme/EMEUtils.h b/dom/media/eme/EMEUtils.h
new file mode 100644
index 0000000000..2a9af09d58
--- /dev/null
+++ b/dom/media/eme/EMEUtils.h
@@ -0,0 +1,106 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef EME_LOG_H_
+#define EME_LOG_H_
+
+#include "mozilla/Logging.h"
+#include "mozilla/dom/MediaKeyStatusMapBinding.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+
+namespace dom {
+class ArrayBufferViewOrArrayBuffer;
+}
+
+#ifndef EME_LOG
+LogModule* GetEMELog();
+# define EME_LOG(...) \
+ MOZ_LOG(GetEMELog(), mozilla::LogLevel::Debug, (__VA_ARGS__))
+# define EME_LOG_ENABLED() MOZ_LOG_TEST(GetEMELog(), mozilla::LogLevel::Debug)
+#endif
+
+#ifndef EME_VERBOSE_LOG
+LogModule* GetEMEVerboseLog();
+# define EME_VERBOSE_LOG(...) \
+ MOZ_LOG(GetEMEVerboseLog(), mozilla::LogLevel::Debug, (__VA_ARGS__))
+#else
+# ifndef EME_LOG
+# define EME_LOG(...)
+# endif
+
+# ifndef EME_VERBOSE_LOG
+# define EME_VERBOSE_LOG(...)
+# endif
+#endif
+
+// Helper function to extract a copy of data coming in from JS in an
+// (ArrayBuffer or ArrayBufferView) IDL typed function argument.
+//
+// Only call this on a properly initialized ArrayBufferViewOrArrayBuffer.
+void CopyArrayBufferViewOrArrayBufferData(
+ const dom::ArrayBufferViewOrArrayBuffer& aBufferOrView,
+ nsTArray<uint8_t>& aOutData);
+
+struct ArrayData {
+ explicit ArrayData(const uint8_t* aData, size_t aLength)
+ : mData(aData), mLength(aLength) {}
+ const uint8_t* mData;
+ const size_t mLength;
+ bool IsValid() const { return mData != nullptr && mLength != 0; }
+ bool operator==(const nsTArray<uint8_t>& aOther) const {
+ return mLength == aOther.Length() &&
+ memcmp(mData, aOther.Elements(), mLength) == 0;
+ }
+};
+
+// Helper function to extract data coming in from JS in an
+// (ArrayBuffer or ArrayBufferView) IDL typed function argument.
+//
+// Be *very* careful with this!
+//
+// Only use returned ArrayData inside the lifetime of the
+// ArrayBufferViewOrArrayBuffer; the ArrayData struct does not contain
+// a copy of the data!
+//
+// And do *not* call out to anything that could call into JavaScript,
+// while the ArrayData is live, as then all bets about the data not changing
+// are off! No calls into JS, no calls into JS-implemented WebIDL or XPIDL,
+// nothing. Beware!
+//
+// Only call this on a properly initialized ArrayBufferViewOrArrayBuffer.
+ArrayData GetArrayBufferViewOrArrayBufferData(
+ const dom::ArrayBufferViewOrArrayBuffer& aBufferOrView);
+
+nsString KeySystemToProxyName(const nsAString& aKeySystem);
+
+bool IsClearkeyKeySystem(const nsAString& aKeySystem);
+
+bool IsWidevineKeySystem(const nsAString& aKeySystem);
+
+#ifdef MOZ_WMF_CDM
+bool IsPlayReadyKeySystemAndSupported(const nsAString& aKeySystem);
+#endif
+
+// Note: Primetime is now unsupported, but we leave it in the enum so
+// that the telemetry enum values are not changed; doing so would break
+// existing telemetry probes.
+enum CDMType {
+ eClearKey = 0,
+ ePrimetime = 1, // Note: Unsupported.
+ eWidevine = 2,
+ eUnknown = 3
+};
+
+CDMType ToCDMTypeTelemetryEnum(const nsString& aKeySystem);
+
+const char* ToMediaKeyStatusStr(dom::MediaKeyStatus aStatus);
+
+} // namespace mozilla
+
+#endif // EME_LOG_H_
diff --git a/dom/media/eme/KeySystemConfig.cpp b/dom/media/eme/KeySystemConfig.cpp
new file mode 100644
index 0000000000..b8eacc7f9d
--- /dev/null
+++ b/dom/media/eme/KeySystemConfig.cpp
@@ -0,0 +1,213 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "KeySystemConfig.h"
+
+#include "EMEUtils.h"
+#include "GMPUtils.h"
+#include "KeySystemNames.h"
+#include "mozilla/StaticPrefs_media.h"
+
+#ifdef XP_WIN
+# include "WMFDecoderModule.h"
+#endif
+#ifdef MOZ_WIDGET_ANDROID
+# include "AndroidDecoderModule.h"
+# include "mozilla/java/MediaDrmProxyWrappers.h"
+# include "nsMimeTypes.h"
+#endif
+
+#ifdef MOZ_WMF_CDM
+# include "mediafoundation/WMFCDMImpl.h"
+#endif
+
+namespace mozilla {
+
+/* static */
+bool KeySystemConfig::Supports(const nsAString& aKeySystem) {
+ nsCString api = nsLiteralCString(CHROMIUM_CDM_API);
+ nsCString name = NS_ConvertUTF16toUTF8(aKeySystem);
+
+ if (HaveGMPFor(api, {name})) {
+ return true;
+ }
+#ifdef MOZ_WIDGET_ANDROID
+ // Check if we can use MediaDrm for this keysystem.
+ if (mozilla::java::MediaDrmProxy::IsSchemeSupported(name)) {
+ return true;
+ }
+#endif
+#if MOZ_WMF_CDM
+ if (IsPlayReadyKeySystemAndSupported(aKeySystem) &&
+ WMFCDMImpl::Supports(aKeySystem)) {
+ return true;
+ }
+#endif
+ return false;
+}
+
+/* static */
+bool KeySystemConfig::GetConfig(const nsAString& aKeySystem,
+ KeySystemConfig& aConfig) {
+ if (!Supports(aKeySystem)) {
+ return false;
+ }
+
+ if (IsClearkeyKeySystem(aKeySystem)) {
+ aConfig.mKeySystem = aKeySystem;
+ aConfig.mInitDataTypes.AppendElement(u"cenc"_ns);
+ aConfig.mInitDataTypes.AppendElement(u"keyids"_ns);
+ aConfig.mInitDataTypes.AppendElement(u"webm"_ns);
+ aConfig.mPersistentState = Requirement::Optional;
+ aConfig.mDistinctiveIdentifier = Requirement::NotAllowed;
+ aConfig.mSessionTypes.AppendElement(SessionType::Temporary);
+ aConfig.mEncryptionSchemes.AppendElement(u"cenc"_ns);
+ aConfig.mEncryptionSchemes.AppendElement(u"cbcs"_ns);
+ aConfig.mEncryptionSchemes.AppendElement(u"cbcs-1-9"_ns);
+ if (StaticPrefs::media_clearkey_persistent_license_enabled()) {
+ aConfig.mSessionTypes.AppendElement(SessionType::PersistentLicense);
+ }
+#if defined(XP_WIN)
+ // Clearkey CDM uses WMF's H.264 decoder on Windows.
+ if (WMFDecoderModule::CanCreateMFTDecoder(WMFStreamType::H264)) {
+ aConfig.mMP4.SetCanDecryptAndDecode(EME_CODEC_H264);
+ } else {
+ aConfig.mMP4.SetCanDecrypt(EME_CODEC_H264);
+ }
+#else
+ aConfig.mMP4.SetCanDecrypt(EME_CODEC_H264);
+#endif
+ aConfig.mMP4.SetCanDecrypt(EME_CODEC_AAC);
+ aConfig.mMP4.SetCanDecrypt(EME_CODEC_FLAC);
+ aConfig.mMP4.SetCanDecrypt(EME_CODEC_OPUS);
+ aConfig.mMP4.SetCanDecrypt(EME_CODEC_VP9);
+ aConfig.mWebM.SetCanDecrypt(EME_CODEC_VORBIS);
+ aConfig.mWebM.SetCanDecrypt(EME_CODEC_OPUS);
+ aConfig.mWebM.SetCanDecrypt(EME_CODEC_VP8);
+ aConfig.mWebM.SetCanDecrypt(EME_CODEC_VP9);
+ return true;
+ }
+ if (IsWidevineKeySystem(aKeySystem)) {
+ aConfig.mKeySystem = aKeySystem;
+ aConfig.mInitDataTypes.AppendElement(u"cenc"_ns);
+ aConfig.mInitDataTypes.AppendElement(u"keyids"_ns);
+ aConfig.mInitDataTypes.AppendElement(u"webm"_ns);
+ aConfig.mPersistentState = Requirement::Optional;
+ aConfig.mDistinctiveIdentifier = Requirement::NotAllowed;
+ aConfig.mSessionTypes.AppendElement(SessionType::Temporary);
+#ifdef MOZ_WIDGET_ANDROID
+ aConfig.mSessionTypes.AppendElement(SessionType::PersistentLicense);
+#endif
+ aConfig.mAudioRobustness.AppendElement(u"SW_SECURE_CRYPTO"_ns);
+ aConfig.mVideoRobustness.AppendElement(u"SW_SECURE_CRYPTO"_ns);
+ aConfig.mVideoRobustness.AppendElement(u"SW_SECURE_DECODE"_ns);
+ aConfig.mEncryptionSchemes.AppendElement(u"cenc"_ns);
+ aConfig.mEncryptionSchemes.AppendElement(u"cbcs"_ns);
+ aConfig.mEncryptionSchemes.AppendElement(u"cbcs-1-9"_ns);
+
+#if defined(MOZ_WIDGET_ANDROID)
+ // MediaDrm.isCryptoSchemeSupported only allows passing
+ // "video/mp4" or "video/webm" for mimetype string.
+ // See
+ // https://developer.android.com/reference/android/media/MediaDrm.html#isCryptoSchemeSupported(java.util.UUID,
+ // java.lang.String) for more detail.
+ typedef struct {
+ const nsCString& mMimeType;
+ const nsCString& mEMECodecType;
+ const char16_t* mCodecType;
+ KeySystemConfig::ContainerSupport* mSupportType;
+ } DataForValidation;
+
+ DataForValidation validationList[] = {
+ {nsCString(VIDEO_MP4), EME_CODEC_H264, java::MediaDrmProxy::AVC,
+ &aConfig.mMP4},
+ {nsCString(VIDEO_MP4), EME_CODEC_VP9, java::MediaDrmProxy::AVC,
+ &aConfig.mMP4},
+ {nsCString(AUDIO_MP4), EME_CODEC_AAC, java::MediaDrmProxy::AAC,
+ &aConfig.mMP4},
+ {nsCString(AUDIO_MP4), EME_CODEC_FLAC, java::MediaDrmProxy::FLAC,
+ &aConfig.mMP4},
+ {nsCString(AUDIO_MP4), EME_CODEC_OPUS, java::MediaDrmProxy::OPUS,
+ &aConfig.mMP4},
+ {nsCString(VIDEO_WEBM), EME_CODEC_VP8, java::MediaDrmProxy::VP8,
+ &aConfig.mWebM},
+ {nsCString(VIDEO_WEBM), EME_CODEC_VP9, java::MediaDrmProxy::VP9,
+ &aConfig.mWebM},
+ {nsCString(AUDIO_WEBM), EME_CODEC_VORBIS, java::MediaDrmProxy::VORBIS,
+ &aConfig.mWebM},
+ {nsCString(AUDIO_WEBM), EME_CODEC_OPUS, java::MediaDrmProxy::OPUS,
+ &aConfig.mWebM},
+ };
+
+ for (const auto& data : validationList) {
+ if (java::MediaDrmProxy::IsCryptoSchemeSupported(kWidevineKeySystemName,
+ data.mMimeType)) {
+ if (AndroidDecoderModule::SupportsMimeType(data.mMimeType) !=
+ media::DecodeSupport::Unsupported) {
+ data.mSupportType->SetCanDecryptAndDecode(data.mEMECodecType);
+ } else {
+ data.mSupportType->SetCanDecrypt(data.mEMECodecType);
+ }
+ }
+ }
+#else
+# if defined(XP_WIN)
+ // Widevine CDM doesn't include an AAC decoder. So if WMF can't
+ // decode AAC, and a codec wasn't specified, be conservative
+ // and reject the MediaKeys request, since we assume Widevine
+ // will be used with AAC.
+ if (WMFDecoderModule::CanCreateMFTDecoder(WMFStreamType::AAC)) {
+ aConfig.mMP4.SetCanDecrypt(EME_CODEC_AAC);
+ }
+# else
+ aConfig.mMP4.SetCanDecrypt(EME_CODEC_AAC);
+# endif
+ aConfig.mMP4.SetCanDecrypt(EME_CODEC_FLAC);
+ aConfig.mMP4.SetCanDecrypt(EME_CODEC_OPUS);
+ aConfig.mMP4.SetCanDecryptAndDecode(EME_CODEC_H264);
+ aConfig.mMP4.SetCanDecryptAndDecode(EME_CODEC_VP9);
+ aConfig.mWebM.SetCanDecrypt(EME_CODEC_VORBIS);
+ aConfig.mWebM.SetCanDecrypt(EME_CODEC_OPUS);
+ aConfig.mWebM.SetCanDecryptAndDecode(EME_CODEC_VP8);
+ aConfig.mWebM.SetCanDecryptAndDecode(EME_CODEC_VP9);
+#endif
+ return true;
+ }
+#ifdef MOZ_WMF_CDM
+ if (IsPlayReadyKeySystemAndSupported(aKeySystem)) {
+ RefPtr<WMFCDMImpl> cdm = MakeRefPtr<WMFCDMImpl>(aKeySystem);
+ return cdm->GetCapabilities(aConfig);
+ }
+#endif
+ return false;
+}
+
+KeySystemConfig::SessionType ConvertToKeySystemConfigSessionType(
+ dom::MediaKeySessionType aType) {
+ switch (aType) {
+ case dom::MediaKeySessionType::Temporary:
+ return KeySystemConfig::SessionType::Temporary;
+ case dom::MediaKeySessionType::Persistent_license:
+ return KeySystemConfig::SessionType::PersistentLicense;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid session type");
+ return KeySystemConfig::SessionType::Temporary;
+ }
+}
+
+const char* SessionTypeToStr(KeySystemConfig::SessionType aType) {
+ switch (aType) {
+ case KeySystemConfig::SessionType::Temporary:
+ return "Temporary";
+ case KeySystemConfig::SessionType::PersistentLicense:
+ return "PersistentLicense";
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid session type");
+ return "Invalid";
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/eme/KeySystemConfig.h b/dom/media/eme/KeySystemConfig.h
new file mode 100644
index 0000000000..bf813b44e9
--- /dev/null
+++ b/dom/media/eme/KeySystemConfig.h
@@ -0,0 +1,157 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_EME_KEYSYSTEMCONFIG_H_
+#define DOM_MEDIA_EME_KEYSYSTEMCONFIG_H_
+
+#include "nsString.h"
+#include "nsTArray.h"
+#include "mozilla/dom/MediaKeysBinding.h"
+
+namespace mozilla {
+
+struct KeySystemConfig {
+ public:
+ // EME MediaKeysRequirement:
+ // https://www.w3.org/TR/encrypted-media/#dom-mediakeysrequirement
+ enum class Requirement {
+ Required = 1,
+ Optional = 2,
+ NotAllowed = 3,
+ };
+
+ // EME MediaKeySessionType:
+ // https://www.w3.org/TR/encrypted-media/#dom-mediakeysessiontype
+ enum class SessionType {
+ Temporary = 1,
+ PersistentLicense = 2,
+ };
+
+ using EMECodecString = nsCString;
+ static constexpr auto EME_CODEC_AAC = "aac"_ns;
+ static constexpr auto EME_CODEC_OPUS = "opus"_ns;
+ static constexpr auto EME_CODEC_VORBIS = "vorbis"_ns;
+ static constexpr auto EME_CODEC_FLAC = "flac"_ns;
+ static constexpr auto EME_CODEC_H264 = "h264"_ns;
+ static constexpr auto EME_CODEC_VP8 = "vp8"_ns;
+ static constexpr auto EME_CODEC_VP9 = "vp9"_ns;
+
+ using EMEEncryptionSchemeString = nsCString;
+ static constexpr auto EME_ENCRYPTION_SCHEME_CENC = "cenc"_ns;
+ static constexpr auto EME_ENCRYPTION_SCHEME_CBCS = "cbcs"_ns;
+
+ // A codec can be decrypted-and-decoded by the CDM, or only decrypted
+ // by the CDM and decoded by Gecko. Not both.
+ struct ContainerSupport {
+ ContainerSupport() = default;
+ ~ContainerSupport() = default;
+ ContainerSupport(const ContainerSupport& aOther) {
+ mCodecsDecoded = aOther.mCodecsDecoded.Clone();
+ mCodecsDecrypted = aOther.mCodecsDecrypted.Clone();
+ }
+ ContainerSupport& operator=(const ContainerSupport& aOther) {
+ if (this == &aOther) {
+ return *this;
+ }
+ mCodecsDecoded = aOther.mCodecsDecoded.Clone();
+ mCodecsDecrypted = aOther.mCodecsDecrypted.Clone();
+ return *this;
+ }
+ ContainerSupport(ContainerSupport&& aOther) = default;
+ ContainerSupport& operator=(ContainerSupport&&) = default;
+
+ bool IsSupported() const {
+ return !mCodecsDecoded.IsEmpty() || !mCodecsDecrypted.IsEmpty();
+ }
+
+ // CDM decrypts and decodes using a DRM robust decoder, and passes decoded
+ // samples back to Gecko for rendering.
+ bool DecryptsAndDecodes(const EMECodecString& aCodec) const {
+ return mCodecsDecoded.Contains(aCodec);
+ }
+
+ // CDM decrypts and passes the decrypted samples back to Gecko for decoding.
+ bool Decrypts(const EMECodecString& aCodec) const {
+ return mCodecsDecrypted.Contains(aCodec);
+ }
+
+ void SetCanDecryptAndDecode(const EMECodecString& aCodec) {
+ // Can't both decrypt and decrypt-and-decode a codec.
+ MOZ_ASSERT(!Decrypts(aCodec));
+ // Prevent duplicates.
+ MOZ_ASSERT(!DecryptsAndDecodes(aCodec));
+ mCodecsDecoded.AppendElement(aCodec);
+ }
+
+ void SetCanDecrypt(const EMECodecString& aCodec) {
+ // Prevent duplicates.
+ MOZ_ASSERT(!Decrypts(aCodec));
+ // Can't both decrypt and decrypt-and-decode a codec.
+ MOZ_ASSERT(!DecryptsAndDecodes(aCodec));
+ mCodecsDecrypted.AppendElement(aCodec);
+ }
+
+ private:
+ nsTArray<EMECodecString> mCodecsDecoded;
+ nsTArray<EMECodecString> mCodecsDecrypted;
+ };
+
+ static bool Supports(const nsAString& aKeySystem);
+ static bool GetConfig(const nsAString& aKeySystem, KeySystemConfig& aConfig);
+
+ KeySystemConfig() = default;
+ ~KeySystemConfig() = default;
+ explicit KeySystemConfig(const KeySystemConfig& aOther) {
+ mKeySystem = aOther.mKeySystem;
+ mInitDataTypes = aOther.mInitDataTypes.Clone();
+ mPersistentState = aOther.mPersistentState;
+ mDistinctiveIdentifier = aOther.mDistinctiveIdentifier;
+ mSessionTypes = aOther.mSessionTypes.Clone();
+ mVideoRobustness = aOther.mVideoRobustness.Clone();
+ mAudioRobustness = aOther.mAudioRobustness.Clone();
+ mEncryptionSchemes = aOther.mEncryptionSchemes.Clone();
+ mMP4 = aOther.mMP4;
+ mWebM = aOther.mWebM;
+ }
+ KeySystemConfig& operator=(const KeySystemConfig& aOther) {
+ if (this == &aOther) {
+ return *this;
+ }
+ mKeySystem = aOther.mKeySystem;
+ mInitDataTypes = aOther.mInitDataTypes.Clone();
+ mPersistentState = aOther.mPersistentState;
+ mDistinctiveIdentifier = aOther.mDistinctiveIdentifier;
+ mSessionTypes = aOther.mSessionTypes.Clone();
+ mVideoRobustness = aOther.mVideoRobustness.Clone();
+ mAudioRobustness = aOther.mAudioRobustness.Clone();
+ mEncryptionSchemes = aOther.mEncryptionSchemes.Clone();
+ mMP4 = aOther.mMP4;
+ mWebM = aOther.mWebM;
+ return *this;
+ }
+ KeySystemConfig(KeySystemConfig&&) = default;
+ KeySystemConfig& operator=(KeySystemConfig&&) = default;
+
+ nsString mKeySystem;
+ nsTArray<nsString> mInitDataTypes;
+ Requirement mPersistentState = Requirement::NotAllowed;
+ Requirement mDistinctiveIdentifier = Requirement::NotAllowed;
+ nsTArray<SessionType> mSessionTypes;
+ nsTArray<nsString> mVideoRobustness;
+ nsTArray<nsString> mAudioRobustness;
+ nsTArray<nsString> mEncryptionSchemes;
+ ContainerSupport mMP4;
+ ContainerSupport mWebM;
+ bool mIsHW = false;
+};
+
+KeySystemConfig::SessionType ConvertToKeySystemConfigSessionType(
+ dom::MediaKeySessionType aType);
+const char* SessionTypeToStr(KeySystemConfig::SessionType aType);
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_EME_KEYSYSTEMCONFIG_H_
diff --git a/dom/media/eme/KeySystemNames.h b/dom/media/eme/KeySystemNames.h
new file mode 100644
index 0000000000..24b59f5d8e
--- /dev/null
+++ b/dom/media/eme/KeySystemNames.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_EME_KEY_SYSTEM_NAMES_H_
+#define DOM_MEDIA_EME_KEY_SYSTEM_NAMES_H_
+
+// Header for key system names. Keep these separate from some of our other
+// EME utils because want to use these strings in contexts where other utils
+// may not build correctly. Specifically at time of writing:
+// - The GMP doesn't have prefs available, so we want to avoid utils that
+// touch the pref service.
+// - The clear key CDM links a limited subset of what normal Fx does, so we
+// need to avoid any utils that touch things like XUL.
+
+namespace mozilla {
+// EME Key System Strings.
+inline constexpr char kClearKeyKeySystemName[] = "org.w3.clearkey";
+inline constexpr char kClearKeyWithProtectionQueryKeySystemName[] =
+ "org.mozilla.clearkey_with_protection_query";
+inline constexpr char kWidevineKeySystemName[] = "com.widevine.alpha";
+#ifdef MOZ_WMF_CDM
+// https://learn.microsoft.com/en-us/playready/overview/key-system-strings
+inline constexpr char kPlayReadyKeySystemName[] =
+ "com.microsoft.playready.recommendation";
+inline constexpr char kPlayReadyKeySystemHardware[] =
+ "com.microsoft.playready.recommendation.3000";
+#endif
+} // namespace mozilla
+
+#endif // DOM_MEDIA_EME_KEY_SYSTEM_NAMES_H_
diff --git a/dom/media/eme/MediaEncryptedEvent.cpp b/dom/media/eme/MediaEncryptedEvent.cpp
new file mode 100644
index 0000000000..b011715295
--- /dev/null
+++ b/dom/media/eme/MediaEncryptedEvent.cpp
@@ -0,0 +1,108 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaEncryptedEvent.h"
+#include "mozilla/dom/MediaEncryptedEventBinding.h"
+#include "nsContentUtils.h"
+#include "js/ArrayBuffer.h"
+#include "jsfriendapi.h"
+#include "nsINode.h"
+#include "mozilla/dom/MediaKeys.h"
+#include "mozilla/HoldDropJSObjects.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(MediaEncryptedEvent)
+
+NS_IMPL_ADDREF_INHERITED(MediaEncryptedEvent, Event)
+NS_IMPL_RELEASE_INHERITED(MediaEncryptedEvent, Event)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(MediaEncryptedEvent, Event)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(MediaEncryptedEvent, Event)
+ NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mInitData)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(MediaEncryptedEvent, Event)
+ mozilla::DropJSObjects(tmp);
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaEncryptedEvent)
+NS_INTERFACE_MAP_END_INHERITING(Event)
+
+MediaEncryptedEvent::MediaEncryptedEvent(EventTarget* aOwner)
+ : Event(aOwner, nullptr, nullptr) {
+ mozilla::HoldJSObjects(this);
+}
+
+MediaEncryptedEvent::~MediaEncryptedEvent() { mozilla::DropJSObjects(this); }
+
+JSObject* MediaEncryptedEvent::WrapObjectInternal(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return MediaEncryptedEvent_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+already_AddRefed<MediaEncryptedEvent> MediaEncryptedEvent::Constructor(
+ EventTarget* aOwner) {
+ RefPtr<MediaEncryptedEvent> e = new MediaEncryptedEvent(aOwner);
+ e->InitEvent(u"encrypted"_ns, CanBubble::eNo, Cancelable::eNo);
+ e->SetTrusted(true);
+ return e.forget();
+}
+
+already_AddRefed<MediaEncryptedEvent> MediaEncryptedEvent::Constructor(
+ EventTarget* aOwner, const nsAString& aInitDataType,
+ const nsTArray<uint8_t>& aInitData) {
+ RefPtr<MediaEncryptedEvent> e = new MediaEncryptedEvent(aOwner);
+ e->InitEvent(u"encrypted"_ns, CanBubble::eNo, Cancelable::eNo);
+ e->mInitDataType = aInitDataType;
+ e->mRawInitData = aInitData.Clone();
+ e->SetTrusted(true);
+ return e.forget();
+}
+
+already_AddRefed<MediaEncryptedEvent> MediaEncryptedEvent::Constructor(
+ const GlobalObject& aGlobal, const nsAString& aType,
+ const MediaKeyNeededEventInit& aEventInitDict, ErrorResult& aRv) {
+ nsCOMPtr<EventTarget> owner = do_QueryInterface(aGlobal.GetAsSupports());
+ RefPtr<MediaEncryptedEvent> e = new MediaEncryptedEvent(owner);
+ bool trusted = e->Init(owner);
+ e->InitEvent(aType, aEventInitDict.mBubbles, aEventInitDict.mCancelable);
+ e->mInitDataType = aEventInitDict.mInitDataType;
+ if (!aEventInitDict.mInitData.IsNull()) {
+ JS::Rooted<JSObject*> buffer(aGlobal.Context(),
+ aEventInitDict.mInitData.Value().Obj());
+ e->mInitData = JS::CopyArrayBuffer(aGlobal.Context(), buffer);
+ if (!e->mInitData) {
+ aRv.NoteJSContextException(aGlobal.Context());
+ return nullptr;
+ }
+ }
+ e->SetTrusted(trusted);
+ return e.forget();
+}
+
+void MediaEncryptedEvent::GetInitDataType(nsString& aRetVal) const {
+ aRetVal = mInitDataType;
+}
+
+void MediaEncryptedEvent::GetInitData(JSContext* cx,
+ JS::MutableHandle<JSObject*> aData,
+ ErrorResult& aRv) {
+ if (mRawInitData.Length()) {
+ mInitData = ArrayBuffer::Create(cx, this, mRawInitData.Length(),
+ mRawInitData.Elements());
+ if (!mInitData) {
+ aRv.NoteJSContextException(cx);
+ return;
+ }
+ mRawInitData.Clear();
+ }
+ aData.set(mInitData);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/eme/MediaEncryptedEvent.h b/dom/media/eme/MediaEncryptedEvent.h
new file mode 100644
index 0000000000..0fcb5db051
--- /dev/null
+++ b/dom/media/eme/MediaEncryptedEvent.h
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_MediaKeyNeededEvent_h__
+#define mozilla_dom_MediaKeyNeededEvent_h__
+
+#include <cstdint>
+#include "js/RootingAPI.h"
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/dom/Event.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupports.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+
+namespace mozilla::dom {
+struct MediaKeyNeededEventInit;
+
+class MediaEncryptedEvent final : public Event {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(MediaEncryptedEvent,
+ Event)
+ protected:
+ virtual ~MediaEncryptedEvent();
+ explicit MediaEncryptedEvent(EventTarget* aOwner);
+
+ nsString mInitDataType;
+ JS::Heap<JSObject*> mInitData;
+
+ public:
+ JSObject* WrapObjectInternal(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ static already_AddRefed<MediaEncryptedEvent> Constructor(EventTarget* aOwner);
+
+ static already_AddRefed<MediaEncryptedEvent> Constructor(
+ EventTarget* aOwner, const nsAString& aInitDataType,
+ const nsTArray<uint8_t>& aInitData);
+
+ static already_AddRefed<MediaEncryptedEvent> Constructor(
+ const GlobalObject& aGlobal, const nsAString& aType,
+ const MediaKeyNeededEventInit& aEventInitDict, ErrorResult& aRv);
+
+ void GetInitDataType(nsString& aRetVal) const;
+
+ void GetInitData(JSContext* cx, JS::MutableHandle<JSObject*> aData,
+ ErrorResult& aRv);
+
+ private:
+ nsTArray<uint8_t> mRawInitData;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_MediaKeyNeededEvent_h__
diff --git a/dom/media/eme/MediaKeyError.cpp b/dom/media/eme/MediaKeyError.cpp
new file mode 100644
index 0000000000..7172b0d05b
--- /dev/null
+++ b/dom/media/eme/MediaKeyError.cpp
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaKeyError.h"
+#include "mozilla/dom/MediaKeyErrorBinding.h"
+#include "nsContentUtils.h"
+
+namespace mozilla::dom {
+
+MediaKeyError::MediaKeyError(EventTarget* aOwner, uint32_t aSystemCode)
+ : Event(aOwner, nullptr, nullptr), mSystemCode(aSystemCode) {
+ InitEvent(u"error"_ns, CanBubble::eNo, Cancelable::eNo);
+}
+
+MediaKeyError::~MediaKeyError() = default;
+
+uint32_t MediaKeyError::SystemCode() const { return mSystemCode; }
+
+JSObject* MediaKeyError::WrapObjectInternal(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return MediaKeyError_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/eme/MediaKeyError.h b/dom/media/eme/MediaKeyError.h
new file mode 100644
index 0000000000..ef4ecf1375
--- /dev/null
+++ b/dom/media/eme/MediaKeyError.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_MediaKeyError_h
+#define mozilla_dom_MediaKeyError_h
+
+#include "mozilla/Attributes.h"
+#include "nsWrapperCache.h"
+#include "mozilla/dom/Event.h"
+#include "js/TypeDecls.h"
+
+namespace mozilla::dom {
+
+class MediaKeyError final : public Event {
+ public:
+ MediaKeyError(EventTarget* aOwner, uint32_t aSystemCode);
+ ~MediaKeyError();
+
+ JSObject* WrapObjectInternal(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ uint32_t SystemCode() const;
+
+ private:
+ uint32_t mSystemCode;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/eme/MediaKeyMessageEvent.cpp b/dom/media/eme/MediaKeyMessageEvent.cpp
new file mode 100644
index 0000000000..69ce5499f4
--- /dev/null
+++ b/dom/media/eme/MediaKeyMessageEvent.cpp
@@ -0,0 +1,103 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/MediaKeyMessageEvent.h"
+#include "mozilla/dom/MediaKeyMessageEventBinding.h"
+#include "js/ArrayBuffer.h"
+#include "js/RootingAPI.h"
+#include "jsfriendapi.h"
+#include "mozilla/dom/Nullable.h"
+#include "mozilla/dom/PrimitiveConversions.h"
+#include "mozilla/HoldDropJSObjects.h"
+#include "mozilla/dom/TypedArray.h"
+#include "nsContentUtils.h"
+#include "mozilla/dom/MediaKeys.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(MediaKeyMessageEvent)
+
+NS_IMPL_ADDREF_INHERITED(MediaKeyMessageEvent, Event)
+NS_IMPL_RELEASE_INHERITED(MediaKeyMessageEvent, Event)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(MediaKeyMessageEvent, Event)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(MediaKeyMessageEvent, Event)
+ NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mMessage)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(MediaKeyMessageEvent, Event)
+ mozilla::DropJSObjects(tmp);
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaKeyMessageEvent)
+NS_INTERFACE_MAP_END_INHERITING(Event)
+
+MediaKeyMessageEvent::MediaKeyMessageEvent(EventTarget* aOwner)
+ : Event(aOwner, nullptr, nullptr),
+ mMessageType(static_cast<MediaKeyMessageType>(0)) {
+ mozilla::HoldJSObjects(this);
+}
+
+MediaKeyMessageEvent::~MediaKeyMessageEvent() { mozilla::DropJSObjects(this); }
+
+MediaKeyMessageEvent* MediaKeyMessageEvent::AsMediaKeyMessageEvent() {
+ return this;
+}
+
+JSObject* MediaKeyMessageEvent::WrapObjectInternal(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return MediaKeyMessageEvent_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+already_AddRefed<MediaKeyMessageEvent> MediaKeyMessageEvent::Constructor(
+ EventTarget* aOwner, MediaKeyMessageType aMessageType,
+ const nsTArray<uint8_t>& aMessage) {
+ RefPtr<MediaKeyMessageEvent> e = new MediaKeyMessageEvent(aOwner);
+ e->InitEvent(u"message"_ns, false, false);
+ e->mMessageType = aMessageType;
+ e->mRawMessage = aMessage.Clone();
+ e->SetTrusted(true);
+ return e.forget();
+}
+
+already_AddRefed<MediaKeyMessageEvent> MediaKeyMessageEvent::Constructor(
+ const GlobalObject& aGlobal, const nsAString& aType,
+ const MediaKeyMessageEventInit& aEventInitDict, ErrorResult& aRv) {
+ nsCOMPtr<EventTarget> owner = do_QueryInterface(aGlobal.GetAsSupports());
+ RefPtr<MediaKeyMessageEvent> e = new MediaKeyMessageEvent(owner);
+ bool trusted = e->Init(owner);
+ e->InitEvent(aType, aEventInitDict.mBubbles, aEventInitDict.mCancelable);
+ JS::Rooted<JSObject*> buffer(aGlobal.Context(),
+ aEventInitDict.mMessage.Obj());
+ e->mMessage = JS::CopyArrayBuffer(aGlobal.Context(), buffer);
+ if (!e->mMessage) {
+ aRv.NoteJSContextException(aGlobal.Context());
+ return nullptr;
+ }
+ e->mMessageType = aEventInitDict.mMessageType;
+ e->SetTrusted(trusted);
+ e->SetComposed(aEventInitDict.mComposed);
+ return e.forget();
+}
+
+void MediaKeyMessageEvent::GetMessage(JSContext* cx,
+ JS::MutableHandle<JSObject*> aMessage,
+ ErrorResult& aRv) {
+ if (!mMessage) {
+ mMessage = ArrayBuffer::Create(cx, this, mRawMessage.Length(),
+ mRawMessage.Elements());
+ if (!mMessage) {
+ aRv.NoteJSContextException(cx);
+ return;
+ }
+ mRawMessage.Clear();
+ }
+ aMessage.set(mMessage);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/eme/MediaKeyMessageEvent.h b/dom/media/eme/MediaKeyMessageEvent.h
new file mode 100644
index 0000000000..400439b065
--- /dev/null
+++ b/dom/media/eme/MediaKeyMessageEvent.h
@@ -0,0 +1,64 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_MediaKeyMessageEvent_h__
+#define mozilla_dom_MediaKeyMessageEvent_h__
+
+#include "mozilla/Attributes.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsWrapperCache.h"
+#include "nsCOMPtr.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/TypedArray.h"
+#include "js/TypeDecls.h"
+#include "mozilla/dom/MediaKeyMessageEventBinding.h"
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+
+struct MediaKeyMessageEventInit;
+
+class MediaKeyMessageEvent final : public Event {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(MediaKeyMessageEvent,
+ Event)
+ protected:
+ virtual ~MediaKeyMessageEvent();
+ explicit MediaKeyMessageEvent(EventTarget* aOwner);
+
+ MediaKeyMessageType mMessageType;
+ JS::Heap<JSObject*> mMessage;
+
+ public:
+ virtual MediaKeyMessageEvent* AsMediaKeyMessageEvent();
+
+ JSObject* WrapObjectInternal(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ static already_AddRefed<MediaKeyMessageEvent> Constructor(
+ EventTarget* aOwner, MediaKeyMessageType aMessageType,
+ const nsTArray<uint8_t>& aMessage);
+
+ static already_AddRefed<MediaKeyMessageEvent> Constructor(
+ const GlobalObject& aGlobal, const nsAString& aType,
+ const MediaKeyMessageEventInit& aEventInitDict, ErrorResult& aRv);
+
+ MediaKeyMessageType MessageType() const { return mMessageType; }
+
+ void GetMessage(JSContext* cx, JS::MutableHandle<JSObject*> aMessage,
+ ErrorResult& aRv);
+
+ private:
+ nsTArray<uint8_t> mRawMessage;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_MediaKeyMessageEvent_h__
diff --git a/dom/media/eme/MediaKeySession.cpp b/dom/media/eme/MediaKeySession.cpp
new file mode 100644
index 0000000000..79190be710
--- /dev/null
+++ b/dom/media/eme/MediaKeySession.cpp
@@ -0,0 +1,622 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/MediaKeySession.h"
+
+#include <ctime>
+#include <utility>
+
+#include "GMPUtils.h"
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/CDMProxy.h"
+#include "mozilla/EMEUtils.h"
+#include "mozilla/Encoding.h"
+#include "mozilla/dom/HTMLMediaElement.h"
+#include "mozilla/dom/KeyIdsInitDataBinding.h"
+#include "mozilla/dom/MediaEncryptedEvent.h"
+#include "mozilla/dom/MediaKeyError.h"
+#include "mozilla/dom/MediaKeyMessageEvent.h"
+#include "mozilla/dom/MediaKeyStatusMap.h"
+#include "mozilla/dom/MediaKeySystemAccess.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsPrintfCString.h"
+#include "psshparser/PsshParser.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(MediaKeySession, DOMEventTargetHelper,
+ mMediaKeyError, mKeys, mKeyStatusMap,
+ mClosed)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaKeySession)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+NS_IMPL_ADDREF_INHERITED(MediaKeySession, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(MediaKeySession, DOMEventTargetHelper)
+
+// Count of number of instances. Used to give each instance a
+// unique token.
+static uint32_t sMediaKeySessionNum = 0;
+
+// Max length of keyId in EME "keyIds" or WebM init data format, as enforced
+// by web platform tests.
+static const uint32_t MAX_KEY_ID_LENGTH = 512;
+
+// Max length of CENC PSSH init data tolerated, as enforced by web
+// platform tests.
+static const uint32_t MAX_CENC_INIT_DATA_LENGTH = 64 * 1024;
+
+MediaKeySession::MediaKeySession(nsPIDOMWindowInner* aParent, MediaKeys* aKeys,
+ const nsAString& aKeySystem,
+ MediaKeySessionType aSessionType,
+ ErrorResult& aRv)
+ : DOMEventTargetHelper(aParent),
+ mKeys(aKeys),
+ mKeySystem(aKeySystem),
+ mSessionType(aSessionType),
+ mToken(sMediaKeySessionNum++),
+ mIsClosed(false),
+ mUninitialized(true),
+ mKeyStatusMap(new MediaKeyStatusMap(aParent)),
+ mExpiration(JS::GenericNaN()) {
+ EME_LOG("MediaKeySession[%p,''] ctor", this);
+
+ MOZ_ASSERT(aParent);
+ if (aRv.Failed()) {
+ return;
+ }
+ mClosed = MakePromise(aRv, "MediaKeys.createSession"_ns);
+}
+
+void MediaKeySession::SetSessionId(const nsAString& aSessionId) {
+ EME_LOG("MediaKeySession[%p,'%s'] session Id set", this,
+ NS_ConvertUTF16toUTF8(aSessionId).get());
+
+ if (NS_WARN_IF(!mSessionId.IsEmpty())) {
+ return;
+ }
+ mSessionId = aSessionId;
+ mKeys->OnSessionIdReady(this);
+}
+
+MediaKeySession::~MediaKeySession() {
+ EME_LOG("MediaKeySession[%p,'%s'] dtor", this,
+ NS_ConvertUTF16toUTF8(mSessionId).get());
+}
+
+MediaKeyError* MediaKeySession::GetError() const { return mMediaKeyError; }
+
+void MediaKeySession::GetSessionId(nsString& aSessionId) const {
+ aSessionId = GetSessionId();
+}
+
+const nsString& MediaKeySession::GetSessionId() const { return mSessionId; }
+
+JSObject* MediaKeySession::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return MediaKeySession_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+double MediaKeySession::Expiration() const { return mExpiration; }
+
+Promise* MediaKeySession::Closed() const { return mClosed; }
+
+void MediaKeySession::UpdateKeyStatusMap() {
+ MOZ_ASSERT(!IsClosed());
+ if (!mKeys->GetCDMProxy()) {
+ return;
+ }
+
+ nsTArray<CDMCaps::KeyStatus> keyStatuses;
+ {
+ auto caps = mKeys->GetCDMProxy()->Capabilites().Lock();
+ caps->GetKeyStatusesForSession(mSessionId, keyStatuses);
+ }
+
+ mKeyStatusMap->Update(keyStatuses);
+
+ if (EME_LOG_ENABLED()) {
+ nsAutoCString message(
+ nsPrintfCString("MediaKeySession[%p,'%s'] key statuses change {", this,
+ NS_ConvertUTF16toUTF8(mSessionId).get()));
+ for (const CDMCaps::KeyStatus& status : keyStatuses) {
+ message.Append(nsPrintfCString(
+ " (%s,%s)", ToHexString(status.mId).get(),
+ nsCString(MediaKeyStatusValues::GetString(status.mStatus)).get()));
+ }
+ message.AppendLiteral(" }");
+ // Use %s so we aren't exposing random strings to printf interpolation.
+ EME_LOG("%s", message.get());
+ }
+}
+
+MediaKeyStatusMap* MediaKeySession::KeyStatuses() const {
+ return mKeyStatusMap;
+}
+
+// The user agent MUST thoroughly validate the Initialization Data before
+// passing it to the CDM. This includes verifying that the length and
+// values of fields are reasonable, verifying that values are within
+// reasonable limits, and stripping irrelevant, unsupported, or unknown
+// data or fields. It is RECOMMENDED that user agents pre-parse, sanitize,
+// and/or generate a fully sanitized version of the Initialization Data.
+// If the Initialization Data format specified by initDataType supports
+// multiple entries, the user agent SHOULD remove entries that are not
+// needed by the CDM. The user agent MUST NOT re-order entries within
+// the Initialization Data.
+static bool ValidateInitData(const nsTArray<uint8_t>& aInitData,
+ const nsAString& aInitDataType) {
+ if (aInitDataType.LowerCaseEqualsLiteral("webm")) {
+ // WebM initData consists of a single keyId. Ensure it's of reasonable
+ // length.
+ return aInitData.Length() <= MAX_KEY_ID_LENGTH;
+ } else if (aInitDataType.LowerCaseEqualsLiteral("cenc")) {
+ // Limit initData to less than 64KB.
+ if (aInitData.Length() > MAX_CENC_INIT_DATA_LENGTH) {
+ return false;
+ }
+ std::vector<std::vector<uint8_t>> keyIds;
+ return ParseCENCInitData(aInitData.Elements(), aInitData.Length(), keyIds);
+ } else if (aInitDataType.LowerCaseEqualsLiteral("keyids")) {
+ if (aInitData.Length() > MAX_KEY_ID_LENGTH) {
+ return false;
+ }
+ // Ensure that init data matches the expected JSON format.
+ mozilla::dom::KeyIdsInitData keyIds;
+ nsString json;
+ nsDependentCSubstring raw(
+ reinterpret_cast<const char*>(aInitData.Elements()),
+ aInitData.Length());
+ if (NS_FAILED(UTF_8_ENCODING->DecodeWithBOMRemoval(raw, json))) {
+ return false;
+ }
+ if (!keyIds.Init(json)) {
+ return false;
+ }
+ if (keyIds.mKids.Length() == 0) {
+ return false;
+ }
+ for (const auto& kid : keyIds.mKids) {
+ if (kid.IsEmpty()) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+// Generates a license request based on the initData. A message of type
+// "license-request" or "individualization-request" will always be queued
+// if the algorithm succeeds and the promise is resolved.
+already_AddRefed<Promise> MediaKeySession::GenerateRequest(
+ const nsAString& aInitDataType,
+ const ArrayBufferViewOrArrayBuffer& aInitData, ErrorResult& aRv) {
+ RefPtr<DetailedPromise> promise(
+ MakePromise(aRv, "MediaKeySession.generateRequest"_ns));
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ // If this object is closed, return a promise rejected with an
+ // InvalidStateError.
+ if (IsClosed()) {
+ EME_LOG("MediaKeySession[%p,'%s'] GenerateRequest() failed, closed", this,
+ NS_ConvertUTF16toUTF8(mSessionId).get());
+ promise->MaybeRejectWithInvalidStateError(
+ "Session is closed in MediaKeySession.generateRequest()");
+ return promise.forget();
+ }
+
+ // If this object's uninitialized value is false, return a promise rejected
+ // with an InvalidStateError.
+ if (!mUninitialized) {
+ EME_LOG("MediaKeySession[%p,'%s'] GenerateRequest() failed, uninitialized",
+ this, NS_ConvertUTF16toUTF8(mSessionId).get());
+ promise->MaybeRejectWithInvalidStateError(
+ "Session is already initialized in MediaKeySession.generateRequest()");
+ return promise.forget();
+ }
+
+ // Let this object's uninitialized value be false.
+ mUninitialized = false;
+
+ // If initDataType is the empty string, return a promise rejected
+ // with a newly created TypeError.
+ if (aInitDataType.IsEmpty()) {
+ promise->MaybeRejectWithTypeError(
+ "Empty initDataType passed to MediaKeySession.generateRequest()");
+ EME_LOG(
+ "MediaKeySession[%p,'%s'] GenerateRequest() failed, empty initDataType",
+ this, NS_ConvertUTF16toUTF8(mSessionId).get());
+ return promise.forget();
+ }
+
+ // If initData is an empty array, return a promise rejected with
+ // a newly created TypeError.
+ nsTArray<uint8_t> data;
+ CopyArrayBufferViewOrArrayBufferData(aInitData, data);
+ if (data.IsEmpty()) {
+ promise->MaybeRejectWithTypeError(
+ "Empty initData passed to MediaKeySession.generateRequest()");
+ EME_LOG("MediaKeySession[%p,'%s'] GenerateRequest() failed, empty initData",
+ this, NS_ConvertUTF16toUTF8(mSessionId).get());
+ return promise.forget();
+ }
+
+ // If the Key System implementation represented by this object's
+ // cdm implementation value does not support initDataType as an
+ // Initialization Data Type, return a promise rejected with a
+ // NotSupportedError. String comparison is case-sensitive.
+ if (!MediaKeySystemAccess::KeySystemSupportsInitDataType(mKeySystem,
+ aInitDataType)) {
+ promise->MaybeRejectWithNotSupportedError(
+ "Unsupported initDataType passed to MediaKeySession.generateRequest()");
+ EME_LOG(
+ "MediaKeySession[%p,'%s'] GenerateRequest() failed, unsupported "
+ "initDataType",
+ this, NS_ConvertUTF16toUTF8(mSessionId).get());
+ return promise.forget();
+ }
+
+ // Let init data be a copy of the contents of the initData parameter.
+ // Note: Handled by the CopyArrayBufferViewOrArrayBufferData call above.
+
+ // Let session type be this object's session type.
+
+ // Let promise be a new promise.
+
+ // Run the following steps in parallel:
+
+ // If the init data is not valid for initDataType, reject promise with
+ // a newly created TypeError.
+ if (!ValidateInitData(data, aInitDataType)) {
+ // If the preceding step failed, reject promise with a newly created
+ // TypeError.
+ promise->MaybeRejectWithTypeError(
+ "initData sanitization failed in MediaKeySession.generateRequest()");
+ EME_LOG(
+ "MediaKeySession[%p,'%s'] GenerateRequest() initData sanitization "
+ "failed",
+ this, NS_ConvertUTF16toUTF8(mSessionId).get());
+ return promise.forget();
+ }
+
+ // Let sanitized init data be a validated and sanitized version of init data.
+
+ // If sanitized init data is empty, reject promise with a NotSupportedError.
+
+ // Note: Remaining steps of generateRequest method continue in CDM.
+
+ // Convert initData to hex for easier logging.
+ // Note: CreateSession() std::move()s the data out of the array, so we have
+ // to copy it here.
+ nsAutoCString hexInitData(ToHexString(data));
+ PromiseId pid = mKeys->StorePromise(promise);
+ mKeys->ConnectPendingPromiseIdWithToken(pid, Token());
+ mKeys->GetCDMProxy()->CreateSession(Token(), mSessionType, pid, aInitDataType,
+ data);
+
+ EME_LOG(
+ "MediaKeySession[%p,'%s'] GenerateRequest() sent, "
+ "promiseId=%d initData='%s' initDataType='%s'",
+ this, NS_ConvertUTF16toUTF8(mSessionId).get(), pid, hexInitData.get(),
+ NS_ConvertUTF16toUTF8(aInitDataType).get());
+
+ return promise.forget();
+}
+
+already_AddRefed<Promise> MediaKeySession::Load(const nsAString& aSessionId,
+ ErrorResult& aRv) {
+ RefPtr<DetailedPromise> promise(MakePromise(aRv, "MediaKeySession.load"_ns));
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ // 1. If this object is closed, return a promise rejected with an
+ // InvalidStateError.
+ if (IsClosed()) {
+ promise->MaybeRejectWithInvalidStateError(
+ "Session is closed in MediaKeySession.load()");
+ EME_LOG("MediaKeySession[%p,'%s'] Load() failed, closed", this,
+ NS_ConvertUTF16toUTF8(aSessionId).get());
+ return promise.forget();
+ }
+
+ // 2.If this object's uninitialized value is false, return a promise rejected
+ // with an InvalidStateError.
+ if (!mUninitialized) {
+ promise->MaybeRejectWithInvalidStateError(
+ "Session is already initialized in MediaKeySession.load()");
+ EME_LOG("MediaKeySession[%p,'%s'] Load() failed, uninitialized", this,
+ NS_ConvertUTF16toUTF8(aSessionId).get());
+ return promise.forget();
+ }
+
+ // 3.Let this object's uninitialized value be false.
+ mUninitialized = false;
+
+ // 4. If sessionId is the empty string, return a promise rejected with a newly
+ // created TypeError.
+ if (aSessionId.IsEmpty()) {
+ promise->MaybeRejectWithTypeError(
+ "Trying to load a session with empty session ID");
+ // "The sessionId parameter is empty."
+ EME_LOG("MediaKeySession[%p,''] Load() failed, no sessionId", this);
+ return promise.forget();
+ }
+
+ // 5. If the result of running the Is persistent session type? algorithm
+ // on this object's session type is false, return a promise rejected with
+ // a newly created TypeError.
+ if (mSessionType == MediaKeySessionType::Temporary) {
+ promise->MaybeRejectWithTypeError(
+ "Trying to load() into a non-persistent session");
+ EME_LOG(
+ "MediaKeySession[%p,''] Load() failed, can't load in a non-persistent "
+ "session",
+ this);
+ return promise.forget();
+ }
+
+ // Note: We don't support persistent sessions in any keysystem, so all calls
+ // to Load() should reject with a TypeError in the preceding check. Omitting
+ // implementing the rest of the specified MediaKeySession::Load() algorithm.
+
+ // We now know the sessionId being loaded into this session. Remove the
+ // session from its owning MediaKey's set of sessions awaiting a sessionId.
+ RefPtr<MediaKeySession> session(mKeys->GetPendingSession(Token()));
+ MOZ_ASSERT(session == this, "Session should be awaiting id on its own token");
+
+ // Associate with the known sessionId.
+ SetSessionId(aSessionId);
+
+ PromiseId pid = mKeys->StorePromise(promise);
+ mKeys->GetCDMProxy()->LoadSession(pid, mSessionType, aSessionId);
+
+ EME_LOG("MediaKeySession[%p,'%s'] Load() sent to CDM, promiseId=%d", this,
+ NS_ConvertUTF16toUTF8(mSessionId).get(), pid);
+
+ return promise.forget();
+}
+
+already_AddRefed<Promise> MediaKeySession::Update(
+ const ArrayBufferViewOrArrayBuffer& aResponse, ErrorResult& aRv) {
+ RefPtr<DetailedPromise> promise(
+ MakePromise(aRv, "MediaKeySession.update"_ns));
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ if (!IsCallable()) {
+ // If this object's callable value is false, return a promise rejected
+ // with a new DOMException whose name is InvalidStateError.
+ EME_LOG(
+ "MediaKeySession[%p,''] Update() called before sessionId set by CDM",
+ this);
+ promise->MaybeRejectWithInvalidStateError(
+ "MediaKeySession.Update() called before sessionId set by CDM");
+ return promise.forget();
+ }
+
+ nsTArray<uint8_t> data;
+ if (IsClosed() || !mKeys->GetCDMProxy()) {
+ promise->MaybeRejectWithInvalidStateError(
+ "Session is closed or was not properly initialized");
+ EME_LOG(
+ "MediaKeySession[%p,'%s'] Update() failed, session is closed or was "
+ "not properly initialised.",
+ this, NS_ConvertUTF16toUTF8(mSessionId).get());
+ return promise.forget();
+ }
+ CopyArrayBufferViewOrArrayBufferData(aResponse, data);
+ if (data.IsEmpty()) {
+ promise->MaybeRejectWithTypeError(
+ "Empty response buffer passed to MediaKeySession.update()");
+ EME_LOG("MediaKeySession[%p,'%s'] Update() failed, empty response buffer",
+ this, NS_ConvertUTF16toUTF8(mSessionId).get());
+ return promise.forget();
+ }
+
+ // Convert response to hex for easier logging.
+ // Note: UpdateSession() std::move()s the data out of the array, so we have
+ // to copy it here.
+ nsAutoCString hexResponse(ToHexString(data));
+
+ PromiseId pid = mKeys->StorePromise(promise);
+ mKeys->GetCDMProxy()->UpdateSession(mSessionId, pid, data);
+
+ EME_LOG(
+ "MediaKeySession[%p,'%s'] Update() sent to CDM, "
+ "promiseId=%d Response='%s'",
+ this, NS_ConvertUTF16toUTF8(mSessionId).get(), pid, hexResponse.get());
+
+ return promise.forget();
+}
+
+already_AddRefed<Promise> MediaKeySession::Close(ErrorResult& aRv) {
+ RefPtr<DetailedPromise> promise(MakePromise(aRv, "MediaKeySession.close"_ns));
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+ // 1. Let session be the associated MediaKeySession object.
+ // 2. If session is closed, return a resolved promise.
+ if (IsClosed()) {
+ EME_LOG("MediaKeySession[%p,'%s'] Close() already closed", this,
+ NS_ConvertUTF16toUTF8(mSessionId).get());
+ promise->MaybeResolveWithUndefined();
+ return promise.forget();
+ }
+ // 3. If session's callable value is false, return a promise rejected
+ // with an InvalidStateError.
+ if (!IsCallable()) {
+ EME_LOG("MediaKeySession[%p,''] Close() called before sessionId set by CDM",
+ this);
+ promise->MaybeRejectWithInvalidStateError(
+ "MediaKeySession.Close() called before sessionId set by CDM");
+ return promise.forget();
+ }
+ if (!mKeys->GetCDMProxy()) {
+ EME_LOG("MediaKeySession[%p,'%s'] Close() null CDMProxy", this,
+ NS_ConvertUTF16toUTF8(mSessionId).get());
+ promise->MaybeRejectWithInvalidStateError(
+ "MediaKeySession.Close() lost reference to CDM");
+ return promise.forget();
+ }
+ // 4. Let promise be a new promise.
+ PromiseId pid = mKeys->StorePromise(promise);
+ // 5. Run the following steps in parallel:
+ // 5.1 Let cdm be the CDM instance represented by session's cdm instance
+ // value. 5.2 Use cdm to close the session associated with session.
+ mKeys->GetCDMProxy()->CloseSession(mSessionId, pid);
+
+ EME_LOG("MediaKeySession[%p,'%s'] Close() sent to CDM, promiseId=%d", this,
+ NS_ConvertUTF16toUTF8(mSessionId).get(), pid);
+
+ // Session Closed algorithm is run when CDM causes us to run
+ // OnSessionClosed().
+
+ // 6. Return promise.
+ return promise.forget();
+}
+
+void MediaKeySession::OnClosed() {
+ if (IsClosed()) {
+ return;
+ }
+ EME_LOG("MediaKeySession[%p,'%s'] session close operation complete.", this,
+ NS_ConvertUTF16toUTF8(mSessionId).get());
+ mIsClosed = true;
+ mKeys->OnSessionClosed(this);
+ mKeys = nullptr;
+ mClosed->MaybeResolveWithUndefined();
+}
+
+bool MediaKeySession::IsClosed() const { return mIsClosed; }
+
+already_AddRefed<Promise> MediaKeySession::Remove(ErrorResult& aRv) {
+ RefPtr<DetailedPromise> promise(
+ MakePromise(aRv, "MediaKeySession.remove"_ns));
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+ if (!IsCallable()) {
+ // If this object's callable value is false, return a promise rejected
+ // with a new DOMException whose name is InvalidStateError.
+ EME_LOG(
+ "MediaKeySession[%p,''] Remove() called before sessionId set by CDM",
+ this);
+ promise->MaybeRejectWithInvalidStateError(
+ "MediaKeySession.Remove() called before sessionId set by CDM");
+ return promise.forget();
+ }
+ if (mSessionType != MediaKeySessionType::Persistent_license) {
+ promise->MaybeRejectWithInvalidAccessError(
+ "Calling MediaKeySession.remove() on non-persistent session");
+ // "The operation is not supported on session type sessions."
+ EME_LOG("MediaKeySession[%p,'%s'] Remove() failed, sesion not persisrtent.",
+ this, NS_ConvertUTF16toUTF8(mSessionId).get());
+ return promise.forget();
+ }
+ if (IsClosed() || !mKeys->GetCDMProxy()) {
+ promise->MaybeRejectWithInvalidStateError(
+ "MediaKeySession.remove() called but session is not active");
+ // "The session is closed."
+ EME_LOG("MediaKeySession[%p,'%s'] Remove() failed, already session closed.",
+ this, NS_ConvertUTF16toUTF8(mSessionId).get());
+ return promise.forget();
+ }
+ PromiseId pid = mKeys->StorePromise(promise);
+ mKeys->GetCDMProxy()->RemoveSession(mSessionId, pid);
+ EME_LOG("MediaKeySession[%p,'%s'] Remove() sent to CDM, promiseId=%d.", this,
+ NS_ConvertUTF16toUTF8(mSessionId).get(), pid);
+
+ return promise.forget();
+}
+
+void MediaKeySession::DispatchKeyMessage(MediaKeyMessageType aMessageType,
+ const nsTArray<uint8_t>& aMessage) {
+ if (EME_LOG_ENABLED()) {
+ EME_LOG(
+ "MediaKeySession[%p,'%s'] DispatchKeyMessage() type=%s message='%s'",
+ this, NS_ConvertUTF16toUTF8(mSessionId).get(),
+ nsCString(MediaKeyMessageTypeValues::GetString(aMessageType)).get(),
+ ToHexString(aMessage).get());
+ }
+
+ RefPtr<MediaKeyMessageEvent> event(
+ MediaKeyMessageEvent::Constructor(this, aMessageType, aMessage));
+ RefPtr<AsyncEventDispatcher> asyncDispatcher =
+ new AsyncEventDispatcher(this, event);
+ asyncDispatcher->PostDOMEvent();
+}
+
+void MediaKeySession::DispatchKeyError(uint32_t aSystemCode) {
+ EME_LOG("MediaKeySession[%p,'%s'] DispatchKeyError() systemCode=%u.", this,
+ NS_ConvertUTF16toUTF8(mSessionId).get(), aSystemCode);
+
+ RefPtr<MediaKeyError> event(new MediaKeyError(this, aSystemCode));
+ RefPtr<AsyncEventDispatcher> asyncDispatcher =
+ new AsyncEventDispatcher(this, event);
+ asyncDispatcher->PostDOMEvent();
+}
+
+void MediaKeySession::DispatchKeyStatusesChange() {
+ if (IsClosed()) {
+ return;
+ }
+
+ UpdateKeyStatusMap();
+
+ RefPtr<AsyncEventDispatcher> asyncDispatcher =
+ new AsyncEventDispatcher(this, u"keystatuseschange"_ns, CanBubble::eNo);
+ asyncDispatcher->PostDOMEvent();
+}
+
+uint32_t MediaKeySession::Token() const { return mToken; }
+
+already_AddRefed<DetailedPromise> MediaKeySession::MakePromise(
+ ErrorResult& aRv, const nsACString& aName) {
+ nsCOMPtr<nsIGlobalObject> global = GetParentObject();
+ if (!global) {
+ NS_WARNING("Passed non-global to MediaKeys ctor!");
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+ return DetailedPromise::Create(global, aRv, aName);
+}
+
+void MediaKeySession::SetExpiration(double aExpiration) {
+ EME_LOG("MediaKeySession[%p,'%s'] SetExpiry(%.12lf) (%.2lf hours from now)",
+ this, NS_ConvertUTF16toUTF8(mSessionId).get(), aExpiration,
+ (aExpiration - 1000.0 * double(time(0))) / (1000.0 * 60 * 60));
+ mExpiration = aExpiration;
+}
+
+EventHandlerNonNull* MediaKeySession::GetOnkeystatuseschange() {
+ return GetEventHandler(nsGkAtoms::onkeystatuseschange);
+}
+
+void MediaKeySession::SetOnkeystatuseschange(EventHandlerNonNull* aCallback) {
+ SetEventHandler(nsGkAtoms::onkeystatuseschange, aCallback);
+}
+
+EventHandlerNonNull* MediaKeySession::GetOnmessage() {
+ return GetEventHandler(nsGkAtoms::onmessage);
+}
+
+void MediaKeySession::SetOnmessage(EventHandlerNonNull* aCallback) {
+ SetEventHandler(nsGkAtoms::onmessage, aCallback);
+}
+
+nsCString ToCString(MediaKeySessionType aType) {
+ return nsCString(MediaKeySessionTypeValues::GetString(aType));
+}
+
+nsString ToString(MediaKeySessionType aType) {
+ return NS_ConvertUTF8toUTF16(ToCString(aType));
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/eme/MediaKeySession.h b/dom/media/eme/MediaKeySession.h
new file mode 100644
index 0000000000..e19488c311
--- /dev/null
+++ b/dom/media/eme/MediaKeySession.h
@@ -0,0 +1,142 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_MediaKeySession_h
+#define mozilla_dom_MediaKeySession_h
+
+#include "DecoderDoctorLogger.h"
+#include "mozilla/Attributes.h"
+#include "nsCycleCollectionParticipant.h"
+#include "mozilla/DOMEventTargetHelper.h"
+#include "nsCOMPtr.h"
+#include "mozilla/dom/TypedArray.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/DetailedPromise.h"
+#include "mozilla/dom/MediaKeySessionBinding.h"
+#include "mozilla/dom/MediaKeysBinding.h"
+#include "mozilla/dom/MediaKeyMessageEventBinding.h"
+
+struct JSContext;
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+class MediaKeySession;
+} // namespace dom
+DDLoggedTypeName(dom::MediaKeySession);
+
+namespace dom {
+
+class ArrayBufferViewOrArrayBuffer;
+class MediaKeyError;
+class MediaKeyStatusMap;
+
+nsCString ToCString(MediaKeySessionType aType);
+
+nsString ToString(MediaKeySessionType aType);
+
+class MediaKeySession final : public DOMEventTargetHelper,
+ public DecoderDoctorLifeLogger<MediaKeySession> {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MediaKeySession,
+ DOMEventTargetHelper)
+ public:
+ MediaKeySession(nsPIDOMWindowInner* aParent, MediaKeys* aKeys,
+ const nsAString& aKeySystem, MediaKeySessionType aSessionType,
+ ErrorResult& aRv);
+
+ void SetSessionId(const nsAString& aSessionId);
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // Mark this as resultNotAddRefed to return raw pointers
+ MediaKeyError* GetError() const;
+
+ MediaKeyStatusMap* KeyStatuses() const;
+
+ void GetSessionId(nsString& aRetval) const;
+
+ const nsString& GetSessionId() const;
+
+ // Number of ms since epoch at which expiration occurs, or NaN if unknown.
+ // TODO: The type of this attribute is still under contention.
+ // https://www.w3.org/Bugs/Public/show_bug.cgi?id=25902
+ double Expiration() const;
+
+ Promise* Closed() const;
+
+ already_AddRefed<Promise> GenerateRequest(
+ const nsAString& aInitDataType,
+ const ArrayBufferViewOrArrayBuffer& aInitData, ErrorResult& aRv);
+
+ already_AddRefed<Promise> Load(const nsAString& aSessionId, ErrorResult& aRv);
+
+ already_AddRefed<Promise> Update(const ArrayBufferViewOrArrayBuffer& response,
+ ErrorResult& aRv);
+
+ already_AddRefed<Promise> Close(ErrorResult& aRv);
+
+ already_AddRefed<Promise> Remove(ErrorResult& aRv);
+
+ void DispatchKeyMessage(MediaKeyMessageType aMessageType,
+ const nsTArray<uint8_t>& aMessage);
+
+ void DispatchKeyError(uint32_t system_code);
+
+ void DispatchKeyStatusesChange();
+
+ void OnClosed();
+
+ bool IsClosed() const;
+
+ void SetExpiration(double aExpiry);
+
+ mozilla::dom::EventHandlerNonNull* GetOnkeystatuseschange();
+ void SetOnkeystatuseschange(mozilla::dom::EventHandlerNonNull* aCallback);
+
+ mozilla::dom::EventHandlerNonNull* GetOnmessage();
+ void SetOnmessage(mozilla::dom::EventHandlerNonNull* aCallback);
+
+ // Process-unique identifier.
+ uint32_t Token() const;
+
+ private:
+ ~MediaKeySession();
+
+ void UpdateKeyStatusMap();
+
+ bool IsCallable() const {
+ // The EME spec sets the "callable value" to true whenever the CDM sets
+ // the sessionId. When the session is initialized, sessionId is empty and
+ // callable is thus false.
+ return !mSessionId.IsEmpty();
+ }
+
+ already_AddRefed<DetailedPromise> MakePromise(ErrorResult& aRv,
+ const nsACString& aName);
+
+ RefPtr<DetailedPromise> mClosed;
+
+ RefPtr<MediaKeyError> mMediaKeyError;
+ RefPtr<MediaKeys> mKeys;
+ const nsString mKeySystem;
+ nsString mSessionId;
+ const MediaKeySessionType mSessionType;
+ const uint32_t mToken;
+ bool mIsClosed;
+ bool mUninitialized;
+ RefPtr<MediaKeyStatusMap> mKeyStatusMap;
+ double mExpiration;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/eme/MediaKeyStatusMap.cpp b/dom/media/eme/MediaKeyStatusMap.cpp
new file mode 100644
index 0000000000..c53f516423
--- /dev/null
+++ b/dom/media/eme/MediaKeyStatusMap.cpp
@@ -0,0 +1,99 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/MediaKeyStatusMap.h"
+#include "nsPIDOMWindow.h"
+#include "mozilla/dom/UnionTypes.h"
+#include "mozilla/dom/ToJSValue.h"
+#include "mozilla/EMEUtils.h"
+#include "GMPUtils.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaKeyStatusMap)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaKeyStatusMap)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaKeyStatusMap)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(MediaKeyStatusMap, mParent)
+
+MediaKeyStatusMap::MediaKeyStatusMap(nsPIDOMWindowInner* aParent)
+ : mParent(aParent) {}
+
+MediaKeyStatusMap::~MediaKeyStatusMap() = default;
+
+JSObject* MediaKeyStatusMap::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return MediaKeyStatusMap_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsPIDOMWindowInner* MediaKeyStatusMap::GetParentObject() const {
+ return mParent;
+}
+
+void MediaKeyStatusMap::Get(const ArrayBufferViewOrArrayBuffer& aKey,
+ OwningMediaKeyStatusOrUndefined& aOutValue,
+ ErrorResult& aOutRv) const {
+ ArrayData keyId = GetArrayBufferViewOrArrayBufferData(aKey);
+ if (!keyId.IsValid()) {
+ aOutValue.SetUndefined();
+ return;
+ }
+ for (const KeyStatus& status : mStatuses) {
+ if (keyId == status.mKeyId) {
+ aOutValue.SetAsMediaKeyStatus() = status.mStatus;
+ return;
+ }
+ }
+ aOutValue.SetUndefined();
+}
+
+bool MediaKeyStatusMap::Has(const ArrayBufferViewOrArrayBuffer& aKey) const {
+ ArrayData keyId = GetArrayBufferViewOrArrayBufferData(aKey);
+ if (!keyId.IsValid()) {
+ return false;
+ }
+
+ for (const KeyStatus& status : mStatuses) {
+ if (keyId == status.mKeyId) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+uint32_t MediaKeyStatusMap::GetIterableLength() const {
+ return mStatuses.Length();
+}
+
+TypedArrayCreator<ArrayBuffer> MediaKeyStatusMap::GetKeyAtIndex(
+ uint32_t aIndex) const {
+ MOZ_ASSERT(aIndex < GetIterableLength());
+ return TypedArrayCreator<ArrayBuffer>(mStatuses[aIndex].mKeyId);
+}
+
+nsString MediaKeyStatusMap::GetKeyIDAsHexString(uint32_t aIndex) const {
+ MOZ_ASSERT(aIndex < GetIterableLength());
+ return NS_ConvertUTF8toUTF16(ToHexString(mStatuses[aIndex].mKeyId));
+}
+
+MediaKeyStatus MediaKeyStatusMap::GetValueAtIndex(uint32_t aIndex) const {
+ MOZ_ASSERT(aIndex < GetIterableLength());
+ return mStatuses[aIndex].mStatus;
+}
+
+uint32_t MediaKeyStatusMap::Size() const { return mStatuses.Length(); }
+
+void MediaKeyStatusMap::Update(const nsTArray<CDMCaps::KeyStatus>& aKeys) {
+ mStatuses.Clear();
+ for (const auto& key : aKeys) {
+ mStatuses.InsertElementSorted(KeyStatus(key.mId, key.mStatus));
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/eme/MediaKeyStatusMap.h b/dom/media/eme/MediaKeyStatusMap.h
new file mode 100644
index 0000000000..35027b25e2
--- /dev/null
+++ b/dom/media/eme/MediaKeyStatusMap.h
@@ -0,0 +1,92 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_MediaKeyStatuses_h
+#define mozilla_dom_MediaKeyStatuses_h
+
+#include "mozilla/Attributes.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsWrapperCache.h"
+
+#include "mozilla/dom/TypedArray.h"
+#include "mozilla/dom/MediaKeyStatusMapBinding.h"
+#include "mozilla/CDMCaps.h"
+
+class nsPIDOMWindowInner;
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+
+class ArrayBufferViewOrArrayBuffer;
+
+// The MediaKeyStatusMap WebIDL interface; maps a keyId to its status.
+// Note that the underlying "map" is stored in an array, since we assume
+// that a MediaKeySession won't have many key statuses to report.
+class MediaKeyStatusMap final : public nsISupports, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(MediaKeyStatusMap)
+
+ public:
+ explicit MediaKeyStatusMap(nsPIDOMWindowInner* aParent);
+
+ protected:
+ ~MediaKeyStatusMap();
+
+ public:
+ nsPIDOMWindowInner* GetParentObject() const;
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ void Get(const ArrayBufferViewOrArrayBuffer& aKey,
+ OwningMediaKeyStatusOrUndefined& aOutValue,
+ ErrorResult& aOutRv) const;
+ bool Has(const ArrayBufferViewOrArrayBuffer& aKey) const;
+ uint32_t Size() const;
+
+ uint32_t GetIterableLength() const;
+ TypedArrayCreator<ArrayBuffer> GetKeyAtIndex(uint32_t aIndex) const;
+ nsString GetKeyIDAsHexString(uint32_t aIndex) const;
+ MediaKeyStatus GetValueAtIndex(uint32_t aIndex) const;
+
+ void Update(const nsTArray<CDMCaps::KeyStatus>& keys);
+
+ private:
+ nsCOMPtr<nsPIDOMWindowInner> mParent;
+
+ struct KeyStatus {
+ KeyStatus(const nsTArray<uint8_t>& aKeyId, MediaKeyStatus aStatus)
+ : mKeyId(aKeyId.Clone()), mStatus(aStatus) {}
+ bool operator==(const KeyStatus& aOther) const {
+ return aOther.mKeyId == mKeyId;
+ }
+ bool operator<(const KeyStatus& aOther) const {
+ // Copy chromium and compare keys' bytes.
+ // Update once https://github.com/w3c/encrypted-media/issues/69
+ // is resolved.
+ const nsTArray<uint8_t>& other = aOther.mKeyId;
+ const nsTArray<uint8_t>& self = mKeyId;
+ size_t length = std::min<size_t>(other.Length(), self.Length());
+ int cmp = memcmp(self.Elements(), other.Elements(), length);
+ if (cmp != 0) {
+ return cmp < 0;
+ }
+ return self.Length() <= other.Length();
+ }
+ nsTArray<uint8_t> mKeyId;
+ MediaKeyStatus mStatus;
+ };
+
+ nsTArray<KeyStatus> mStatuses;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/eme/MediaKeySystemAccess.cpp b/dom/media/eme/MediaKeySystemAccess.cpp
new file mode 100644
index 0000000000..0d61226eb5
--- /dev/null
+++ b/dom/media/eme/MediaKeySystemAccess.cpp
@@ -0,0 +1,1086 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/MediaKeySystemAccess.h"
+
+#include <functional>
+
+#include "DecoderDoctorDiagnostics.h"
+#include "DecoderTraits.h"
+#include "KeySystemConfig.h"
+#include "MediaContainerType.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/dom/KeySystemNames.h"
+#include "mozilla/dom/MediaKeySystemAccessBinding.h"
+#include "mozilla/dom/MediaKeySession.h"
+#include "mozilla/dom/MediaSource.h"
+#include "mozilla/EMEUtils.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "nsDOMString.h"
+#include "nsIObserverService.h"
+#include "nsMimeTypes.h"
+#include "nsReadableUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsUnicharUtils.h"
+#include "WebMDecoder.h"
+
+#ifdef XP_WIN
+# include "WMFDecoderModule.h"
+#endif
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(MediaKeySystemAccess, mParent)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaKeySystemAccess)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaKeySystemAccess)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaKeySystemAccess)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+static nsCString ToCString(const MediaKeySystemConfiguration& aConfig);
+
+MediaKeySystemAccess::MediaKeySystemAccess(
+ nsPIDOMWindowInner* aParent, const nsAString& aKeySystem,
+ const MediaKeySystemConfiguration& aConfig)
+ : mParent(aParent), mKeySystem(aKeySystem), mConfig(aConfig) {
+ EME_LOG("Created MediaKeySystemAccess for keysystem=%s config=%s",
+ NS_ConvertUTF16toUTF8(mKeySystem).get(),
+ mozilla::dom::ToCString(mConfig).get());
+}
+
+MediaKeySystemAccess::~MediaKeySystemAccess() = default;
+
+JSObject* MediaKeySystemAccess::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return MediaKeySystemAccess_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsPIDOMWindowInner* MediaKeySystemAccess::GetParentObject() const {
+ return mParent;
+}
+
+void MediaKeySystemAccess::GetKeySystem(nsString& aOutKeySystem) const {
+ aOutKeySystem.Assign(mKeySystem);
+}
+
+void MediaKeySystemAccess::GetConfiguration(
+ MediaKeySystemConfiguration& aConfig) {
+ aConfig = mConfig;
+}
+
+already_AddRefed<Promise> MediaKeySystemAccess::CreateMediaKeys(
+ ErrorResult& aRv) {
+ RefPtr<MediaKeys> keys(new MediaKeys(mParent, mKeySystem, mConfig));
+ return keys->Init(aRv);
+}
+
+static MediaKeySystemStatus EnsureCDMInstalled(const nsAString& aKeySystem,
+ nsACString& aOutMessage) {
+ if (!KeySystemConfig::Supports(aKeySystem)) {
+ aOutMessage = "CDM is not installed"_ns;
+ return MediaKeySystemStatus::Cdm_not_installed;
+ }
+
+ return MediaKeySystemStatus::Available;
+}
+
+/* static */
+MediaKeySystemStatus MediaKeySystemAccess::GetKeySystemStatus(
+ const nsAString& aKeySystem, nsACString& aOutMessage) {
+ MOZ_ASSERT(StaticPrefs::media_eme_enabled() ||
+ IsClearkeyKeySystem(aKeySystem));
+
+ if (IsClearkeyKeySystem(aKeySystem)) {
+ return EnsureCDMInstalled(aKeySystem, aOutMessage);
+ }
+
+ if (IsWidevineKeySystem(aKeySystem)) {
+ if (Preferences::GetBool("media.gmp-widevinecdm.visible", false)) {
+ if (!Preferences::GetBool("media.gmp-widevinecdm.enabled", false)) {
+ aOutMessage = "Widevine EME disabled"_ns;
+ return MediaKeySystemStatus::Cdm_disabled;
+ }
+ return EnsureCDMInstalled(aKeySystem, aOutMessage);
+#ifdef MOZ_WIDGET_ANDROID
+ } else if (Preferences::GetBool("media.mediadrm-widevinecdm.visible",
+ false)) {
+ nsCString keySystem = NS_ConvertUTF16toUTF8(aKeySystem);
+ bool supported =
+ mozilla::java::MediaDrmProxy::IsSchemeSupported(keySystem);
+ if (!supported) {
+ aOutMessage = nsLiteralCString(
+ "KeySystem or Minimum API level not met for Widevine EME");
+ return MediaKeySystemStatus::Cdm_not_supported;
+ }
+ return MediaKeySystemStatus::Available;
+#endif
+ }
+ }
+
+#ifdef MOZ_WMF_CDM
+ if (IsPlayReadyKeySystemAndSupported(aKeySystem) &&
+ KeySystemConfig::Supports(aKeySystem)) {
+ return MediaKeySystemStatus::Available;
+ }
+#endif
+
+ return MediaKeySystemStatus::Cdm_not_supported;
+}
+
+static KeySystemConfig::EMECodecString ToEMEAPICodecString(
+ const nsString& aCodec) {
+ if (IsAACCodecString(aCodec)) {
+ return KeySystemConfig::EME_CODEC_AAC;
+ }
+ if (aCodec.EqualsLiteral("opus")) {
+ return KeySystemConfig::EME_CODEC_OPUS;
+ }
+ if (aCodec.EqualsLiteral("vorbis")) {
+ return KeySystemConfig::EME_CODEC_VORBIS;
+ }
+ if (aCodec.EqualsLiteral("flac")) {
+ return KeySystemConfig::EME_CODEC_FLAC;
+ }
+ if (IsH264CodecString(aCodec)) {
+ return KeySystemConfig::EME_CODEC_H264;
+ }
+ if (IsVP8CodecString(aCodec)) {
+ return KeySystemConfig::EME_CODEC_VP8;
+ }
+ if (IsVP9CodecString(aCodec)) {
+ return KeySystemConfig::EME_CODEC_VP9;
+ }
+ return ""_ns;
+}
+
+static nsTArray<KeySystemConfig> GetSupportedKeySystems() {
+ nsTArray<KeySystemConfig> keySystemConfigs;
+
+ const nsTArray<nsString> keySystemNames{
+ NS_ConvertUTF8toUTF16(kClearKeyKeySystemName),
+ NS_ConvertUTF8toUTF16(kWidevineKeySystemName),
+#ifdef MOZ_WMF_CDM
+ NS_ConvertUTF8toUTF16(kPlayReadyKeySystemName),
+ NS_ConvertUTF8toUTF16(kPlayReadyKeySystemHardware),
+#endif
+ };
+ for (const auto& name : keySystemNames) {
+ KeySystemConfig config;
+ if (KeySystemConfig::GetConfig(name, config)) {
+ if (IsClearkeyKeySystem(name) &&
+ StaticPrefs::media_clearkey_test_key_systems_enabled()) {
+ // Add testing key systems. These offer the same capabilities as the
+ // base clearkey system, so just clone clearkey and change the name.
+ KeySystemConfig clearkeyWithProtectionQuery{config};
+ clearkeyWithProtectionQuery.mKeySystem.AssignLiteral(
+ kClearKeyWithProtectionQueryKeySystemName);
+ keySystemConfigs.AppendElement(std::move(clearkeyWithProtectionQuery));
+ }
+ keySystemConfigs.AppendElement(std::move(config));
+ }
+ }
+
+ return keySystemConfigs;
+}
+
+static bool GetKeySystemConfig(const nsAString& aKeySystem,
+ KeySystemConfig& aOutKeySystemConfig) {
+ for (auto&& config : GetSupportedKeySystems()) {
+ if (config.mKeySystem.Equals(aKeySystem)) {
+ aOutKeySystemConfig = std::move(config);
+ return true;
+ }
+ }
+ // No matching key system found.
+ return false;
+}
+
+/* static */
+bool MediaKeySystemAccess::KeySystemSupportsInitDataType(
+ const nsAString& aKeySystem, const nsAString& aInitDataType) {
+ KeySystemConfig implementation;
+ return GetKeySystemConfig(aKeySystem, implementation) &&
+ implementation.mInitDataTypes.Contains(aInitDataType);
+}
+
+enum CodecType { Audio, Video, Invalid };
+
+static bool CanDecryptAndDecode(
+ const nsString& aKeySystem, const nsString& aContentType,
+ CodecType aCodecType,
+ const KeySystemConfig::ContainerSupport& aContainerSupport,
+ const nsTArray<KeySystemConfig::EMECodecString>& aCodecs,
+ DecoderDoctorDiagnostics* aDiagnostics) {
+ MOZ_ASSERT(aCodecType != Invalid);
+ for (const KeySystemConfig::EMECodecString& codec : aCodecs) {
+ MOZ_ASSERT(!codec.IsEmpty());
+
+ if (aContainerSupport.DecryptsAndDecodes(codec)) {
+ // GMP can decrypt-and-decode this codec.
+ continue;
+ }
+
+ if (aContainerSupport.Decrypts(codec)) {
+ IgnoredErrorResult rv;
+ MediaSource::IsTypeSupported(aContentType, aDiagnostics, rv);
+ if (!rv.Failed()) {
+ // GMP can decrypt and is allowed to return compressed samples to
+ // Gecko to decode, and Gecko has a decoder.
+ continue;
+ }
+ }
+
+ // Neither the GMP nor Gecko can both decrypt and decode. We don't
+ // support this codec.
+
+#if defined(XP_WIN)
+ // Widevine CDM doesn't include an AAC decoder. So if WMF can't
+ // decode AAC, and a codec wasn't specified, be conservative
+ // and reject the MediaKeys request, since we assume Widevine
+ // will be used with AAC.
+ if (codec == KeySystemConfig::EME_CODEC_AAC &&
+ IsWidevineKeySystem(aKeySystem) &&
+ !WMFDecoderModule::CanCreateMFTDecoder(WMFStreamType::AAC)) {
+ if (aDiagnostics) {
+ aDiagnostics->SetKeySystemIssue(
+ DecoderDoctorDiagnostics::eWidevineWithNoWMF);
+ }
+ }
+#endif
+ return false;
+ }
+ return true;
+}
+
+// Returns if an encryption scheme is supported per:
+// https://github.com/WICG/encrypted-media-encryption-scheme/blob/master/explainer.md
+// To be supported the scheme should be one of:
+// - null
+// - missing (which will result in the nsString being set to void and thus null)
+// - one of the schemes supported by the CDM
+// If the pref to enable this behavior is not set, then the value should be
+// empty/null, as the dict member will not be exposed. In this case we will
+// always report support as we would before this feature was implemented.
+static bool SupportsEncryptionScheme(
+ const nsString& aEncryptionScheme,
+ const nsTArray<nsString>& aSupportedEncryptionSchemes) {
+ MOZ_ASSERT(
+ DOMStringIsNull(aEncryptionScheme) ||
+ StaticPrefs::media_eme_encrypted_media_encryption_scheme_enabled(),
+ "Encryption scheme checking support must be preffed on for "
+ "encryptionScheme to be a non-null string");
+ if (DOMStringIsNull(aEncryptionScheme)) {
+ // "A missing or null value indicates that any encryption scheme is
+ // acceptable."
+ return true;
+ }
+ return aSupportedEncryptionSchemes.Contains(aEncryptionScheme);
+}
+
+static bool ToSessionType(const nsAString& aSessionType,
+ MediaKeySessionType& aOutType) {
+ if (aSessionType.Equals(ToString(MediaKeySessionType::Temporary))) {
+ aOutType = MediaKeySessionType::Temporary;
+ return true;
+ }
+ if (aSessionType.Equals(ToString(MediaKeySessionType::Persistent_license))) {
+ aOutType = MediaKeySessionType::Persistent_license;
+ return true;
+ }
+ return false;
+}
+
+// 5.1.1 Is persistent session type?
+static bool IsPersistentSessionType(MediaKeySessionType aSessionType) {
+ return aSessionType == MediaKeySessionType::Persistent_license;
+}
+
+static bool ContainsSessionType(
+ const nsTArray<KeySystemConfig::SessionType>& aTypes,
+ const MediaKeySessionType& aSessionType) {
+ return (aSessionType == MediaKeySessionType::Persistent_license &&
+ aTypes.Contains(KeySystemConfig::SessionType::PersistentLicense)) ||
+ (aSessionType == MediaKeySessionType::Temporary &&
+ aTypes.Contains(KeySystemConfig::SessionType::Temporary));
+}
+
+CodecType GetMajorType(const MediaMIMEType& aMIMEType) {
+ if (aMIMEType.HasAudioMajorType()) {
+ return Audio;
+ }
+ if (aMIMEType.HasVideoMajorType()) {
+ return Video;
+ }
+ return Invalid;
+}
+
+static CodecType GetCodecType(const KeySystemConfig::EMECodecString& aCodec) {
+ if (aCodec.Equals(KeySystemConfig::EME_CODEC_AAC) ||
+ aCodec.Equals(KeySystemConfig::EME_CODEC_OPUS) ||
+ aCodec.Equals(KeySystemConfig::EME_CODEC_VORBIS) ||
+ aCodec.Equals(KeySystemConfig::EME_CODEC_FLAC)) {
+ return Audio;
+ }
+ if (aCodec.Equals(KeySystemConfig::EME_CODEC_H264) ||
+ aCodec.Equals(KeySystemConfig::EME_CODEC_VP8) ||
+ aCodec.Equals(KeySystemConfig::EME_CODEC_VP9)) {
+ return Video;
+ }
+ return Invalid;
+}
+
+static bool AllCodecsOfType(
+ const nsTArray<KeySystemConfig::EMECodecString>& aCodecs,
+ const CodecType aCodecType) {
+ for (const KeySystemConfig::EMECodecString& codec : aCodecs) {
+ if (GetCodecType(codec) != aCodecType) {
+ return false;
+ }
+ }
+ return true;
+}
+
+static bool IsParameterUnrecognized(const nsAString& aContentType) {
+ nsAutoString contentType(aContentType);
+ contentType.StripWhitespace();
+
+ nsTArray<nsString> params;
+ nsAString::const_iterator start, end, semicolon, equalSign;
+ contentType.BeginReading(start);
+ contentType.EndReading(end);
+ semicolon = start;
+ // Find any substring between ';' & '='.
+ while (semicolon != end) {
+ if (FindCharInReadable(';', semicolon, end)) {
+ equalSign = ++semicolon;
+ if (FindCharInReadable('=', equalSign, end)) {
+ params.AppendElement(Substring(semicolon, equalSign));
+ semicolon = equalSign;
+ }
+ }
+ }
+
+ for (auto param : params) {
+ if (!param.LowerCaseEqualsLiteral("codecs") &&
+ !param.LowerCaseEqualsLiteral("profiles")) {
+ return true;
+ }
+ }
+ return false;
+}
+
+// 3.1.1.3 Get Supported Capabilities for Audio/Video Type
+static Sequence<MediaKeySystemMediaCapability> GetSupportedCapabilities(
+ const CodecType aCodecType,
+ const nsTArray<MediaKeySystemMediaCapability>& aRequestedCapabilities,
+ const MediaKeySystemConfiguration& aPartialConfig,
+ const KeySystemConfig& aKeySystem, DecoderDoctorDiagnostics* aDiagnostics,
+ const std::function<void(const char*)>& aDeprecationLogFn) {
+ // Let local accumulated configuration be a local copy of partial
+ // configuration. (Note: It's not necessary for us to maintain a local copy,
+ // as we don't need to test whether capabilites from previous calls to this
+ // algorithm work with the capabilities currently being considered in this
+ // call. )
+
+ // Let supported media capabilities be an empty sequence of
+ // MediaKeySystemMediaCapability dictionaries.
+ Sequence<MediaKeySystemMediaCapability> supportedCapabilities;
+
+ // For each requested media capability in requested media capabilities:
+ for (const MediaKeySystemMediaCapability& capabilities :
+ aRequestedCapabilities) {
+ // Let content type be requested media capability's contentType member.
+ const nsString& contentTypeString = capabilities.mContentType;
+ // Let robustness be requested media capability's robustness member.
+ const nsString& robustness = capabilities.mRobustness;
+ // Optional encryption scheme extension, see
+ // https://github.com/WICG/encrypted-media-encryption-scheme/blob/master/explainer.md
+ // This will only be exposed to JS if
+ // media.eme.encrypted-media-encryption-scheme.enabled is preffed on.
+ const nsString encryptionScheme = capabilities.mEncryptionScheme;
+ // If content type is the empty string, return null.
+ if (contentTypeString.IsEmpty()) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') "
+ "MediaKeySystemMediaCapability('%s','%s','%s') rejected; "
+ "audio or video capability has empty contentType.",
+ NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(),
+ NS_ConvertUTF16toUTF8(contentTypeString).get(),
+ NS_ConvertUTF16toUTF8(robustness).get(),
+ NS_ConvertUTF16toUTF8(encryptionScheme).get());
+ return Sequence<MediaKeySystemMediaCapability>();
+ }
+ // If content type is an invalid or unrecognized MIME type, continue
+ // to the next iteration.
+ Maybe<MediaContainerType> maybeContainerType =
+ MakeMediaContainerType(contentTypeString);
+ if (!maybeContainerType) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') "
+ "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; "
+ "failed to parse contentTypeString as MIME type.",
+ NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(),
+ NS_ConvertUTF16toUTF8(contentTypeString).get(),
+ NS_ConvertUTF16toUTF8(robustness).get(),
+ NS_ConvertUTF16toUTF8(encryptionScheme).get());
+ continue;
+ }
+ const MediaContainerType& containerType = *maybeContainerType;
+ bool invalid = false;
+ nsTArray<KeySystemConfig::EMECodecString> codecs;
+ for (const auto& codecString :
+ containerType.ExtendedType().Codecs().Range()) {
+ KeySystemConfig::EMECodecString emeCodec =
+ ToEMEAPICodecString(nsString(codecString));
+ if (emeCodec.IsEmpty()) {
+ invalid = true;
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') "
+ "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; "
+ "'%s' is an invalid codec string.",
+ NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(),
+ NS_ConvertUTF16toUTF8(contentTypeString).get(),
+ NS_ConvertUTF16toUTF8(robustness).get(),
+ NS_ConvertUTF16toUTF8(encryptionScheme).get(),
+ NS_ConvertUTF16toUTF8(codecString).get());
+ break;
+ }
+ codecs.AppendElement(emeCodec);
+ }
+ if (invalid) {
+ continue;
+ }
+
+ // If the user agent does not support container, continue to the next
+ // iteration. The case-sensitivity of string comparisons is determined by
+ // the appropriate RFC. (Note: Per RFC 6838 [RFC6838], "Both top-level type
+ // and subtype names are case-insensitive."'. We're using
+ // nsContentTypeParser and that is case-insensitive and converts all its
+ // parameter outputs to lower case.)
+ const bool isMP4 =
+ DecoderTraits::IsMP4SupportedType(containerType, aDiagnostics);
+ if (isMP4 && !aKeySystem.mMP4.IsSupported()) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') "
+ "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; "
+ "MP4 requested but unsupported.",
+ NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(),
+ NS_ConvertUTF16toUTF8(contentTypeString).get(),
+ NS_ConvertUTF16toUTF8(robustness).get(),
+ NS_ConvertUTF16toUTF8(encryptionScheme).get());
+ continue;
+ }
+ const bool isWebM = WebMDecoder::IsSupportedType(containerType);
+ if (isWebM && !aKeySystem.mWebM.IsSupported()) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') "
+ "MediaKeySystemMediaCapability('%s','%s,'%s') unsupported; "
+ "WebM requested but unsupported.",
+ NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(),
+ NS_ConvertUTF16toUTF8(contentTypeString).get(),
+ NS_ConvertUTF16toUTF8(robustness).get(),
+ NS_ConvertUTF16toUTF8(encryptionScheme).get());
+ continue;
+ }
+ if (!isMP4 && !isWebM) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') "
+ "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; "
+ "Unsupported or unrecognized container requested.",
+ NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(),
+ NS_ConvertUTF16toUTF8(contentTypeString).get(),
+ NS_ConvertUTF16toUTF8(robustness).get(),
+ NS_ConvertUTF16toUTF8(encryptionScheme).get());
+ continue;
+ }
+
+ // Let parameters be the RFC 6381[RFC6381] parameters, if any, specified by
+ // content type.
+ // If the user agent does not recognize one or more parameters, continue to
+ // the next iteration.
+ if (IsParameterUnrecognized(contentTypeString)) {
+ continue;
+ }
+
+ // Let media types be the set of codecs and codec constraints specified by
+ // parameters. The case-sensitivity of string comparisons is determined by
+ // the appropriate RFC or other specification.
+ // (Note: codecs array is 'parameter').
+
+ // If media types is empty:
+ if (codecs.IsEmpty()) {
+ // Log deprecation warning to encourage authors to not do this!
+ aDeprecationLogFn("MediaEMENoCodecsDeprecatedWarning");
+ // TODO: Remove this once we're sure it doesn't break the web.
+ // If container normatively implies a specific set of codecs and codec
+ // constraints: Let parameters be that set.
+ if (isMP4) {
+ if (aCodecType == Audio) {
+ codecs.AppendElement(KeySystemConfig::EME_CODEC_AAC);
+ } else if (aCodecType == Video) {
+ codecs.AppendElement(KeySystemConfig::EME_CODEC_H264);
+ }
+ } else if (isWebM) {
+ if (aCodecType == Audio) {
+ codecs.AppendElement(KeySystemConfig::EME_CODEC_VORBIS);
+ } else if (aCodecType == Video) {
+ codecs.AppendElement(KeySystemConfig::EME_CODEC_VP8);
+ }
+ }
+ // Otherwise: Continue to the next iteration.
+ // (Note: all containers we support have implied codecs, so don't continue
+ // here.)
+ }
+
+ // If container type is not strictly a audio/video type, continue to the
+ // next iteration.
+ const auto majorType = GetMajorType(containerType.Type());
+ if (majorType == Invalid) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') "
+ "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; "
+ "MIME type is not an audio or video MIME type.",
+ NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(),
+ NS_ConvertUTF16toUTF8(contentTypeString).get(),
+ NS_ConvertUTF16toUTF8(robustness).get(),
+ NS_ConvertUTF16toUTF8(encryptionScheme).get());
+ continue;
+ }
+ if (majorType != aCodecType || !AllCodecsOfType(codecs, aCodecType)) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') "
+ "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; "
+ "MIME type mixes audio codecs in video capabilities "
+ "or video codecs in audio capabilities.",
+ NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(),
+ NS_ConvertUTF16toUTF8(contentTypeString).get(),
+ NS_ConvertUTF16toUTF8(robustness).get(),
+ NS_ConvertUTF16toUTF8(encryptionScheme).get());
+ continue;
+ }
+ // If robustness is not the empty string and contains an unrecognized
+ // value or a value not supported by implementation, continue to the
+ // next iteration. String comparison is case-sensitive.
+ if (!robustness.IsEmpty()) {
+ if (majorType == Audio &&
+ !aKeySystem.mAudioRobustness.Contains(robustness)) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') "
+ "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; "
+ "unsupported robustness string.",
+ NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(),
+ NS_ConvertUTF16toUTF8(contentTypeString).get(),
+ NS_ConvertUTF16toUTF8(robustness).get(),
+ NS_ConvertUTF16toUTF8(encryptionScheme).get());
+ continue;
+ }
+ if (majorType == Video &&
+ !aKeySystem.mVideoRobustness.Contains(robustness)) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') "
+ "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; "
+ "unsupported robustness string.",
+ NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(),
+ NS_ConvertUTF16toUTF8(contentTypeString).get(),
+ NS_ConvertUTF16toUTF8(robustness).get(),
+ NS_ConvertUTF16toUTF8(encryptionScheme).get());
+ continue;
+ }
+ // Note: specified robustness requirements are satisfied.
+ }
+
+ // If preffed on: "In the Get Supported Capabilities for Audio/Video Type
+ // algorithm, implementations must skip capabilities specifying unsupported
+ // encryption schemes."
+ if (!SupportsEncryptionScheme(encryptionScheme,
+ aKeySystem.mEncryptionSchemes)) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') "
+ "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; "
+ "encryption scheme unsupported by CDM requested.",
+ NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(),
+ NS_ConvertUTF16toUTF8(contentTypeString).get(),
+ NS_ConvertUTF16toUTF8(robustness).get(),
+ NS_ConvertUTF16toUTF8(encryptionScheme).get());
+ continue;
+ }
+
+ // If the user agent and implementation definitely support playback of
+ // encrypted media data for the combination of container, media types,
+ // robustness and local accumulated configuration in combination with
+ // restrictions...
+ const auto& containerSupport = isMP4 ? aKeySystem.mMP4 : aKeySystem.mWebM;
+ if (!CanDecryptAndDecode(aKeySystem.mKeySystem, contentTypeString,
+ majorType, containerSupport, codecs,
+ aDiagnostics)) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') "
+ "MediaKeySystemMediaCapability('%s','%s','%s') unsupported; "
+ "codec unsupported by CDM requested.",
+ NS_ConvertUTF16toUTF8(aPartialConfig.mLabel).get(),
+ NS_ConvertUTF16toUTF8(contentTypeString).get(),
+ NS_ConvertUTF16toUTF8(robustness).get(),
+ NS_ConvertUTF16toUTF8(encryptionScheme).get());
+ continue;
+ }
+
+ // ... add requested media capability to supported media capabilities.
+ if (!supportedCapabilities.AppendElement(capabilities, mozilla::fallible)) {
+ NS_WARNING("GetSupportedCapabilities: Malloc failure");
+ return Sequence<MediaKeySystemMediaCapability>();
+ }
+
+ // Note: omitting steps 3.13.2, our robustness is not sophisticated enough
+ // to require considering all requirements together.
+ }
+ return supportedCapabilities;
+}
+
+// "Get Supported Configuration and Consent" algorithm, steps 4-7 for
+// distinctive identifier, and steps 8-11 for persistent state. The steps
+// are the same for both requirements/features, so we factor them out into
+// a single function.
+static bool CheckRequirement(
+ const MediaKeysRequirement aRequirement,
+ const KeySystemConfig::Requirement aKeySystemRequirement,
+ MediaKeysRequirement& aOutRequirement) {
+ // Let requirement be the value of candidate configuration's member.
+ MediaKeysRequirement requirement = aRequirement;
+ // If requirement is "optional" and feature is not allowed according to
+ // restrictions, set requirement to "not-allowed".
+ if (aRequirement == MediaKeysRequirement::Optional &&
+ aKeySystemRequirement == KeySystemConfig::Requirement::NotAllowed) {
+ requirement = MediaKeysRequirement::Not_allowed;
+ }
+
+ // Follow the steps for requirement from the following list:
+ switch (requirement) {
+ case MediaKeysRequirement::Required: {
+ // If the implementation does not support use of requirement in
+ // combination with accumulated configuration and restrictions, return
+ // NotSupported.
+ if (aKeySystemRequirement == KeySystemConfig::Requirement::NotAllowed) {
+ return false;
+ }
+ break;
+ }
+ case MediaKeysRequirement::Optional: {
+ // Continue with the following steps.
+ break;
+ }
+ case MediaKeysRequirement::Not_allowed: {
+ // If the implementation requires use of feature in combination with
+ // accumulated configuration and restrictions, return NotSupported.
+ if (aKeySystemRequirement == KeySystemConfig::Requirement::Required) {
+ return false;
+ }
+ break;
+ }
+ default: {
+ return false;
+ }
+ }
+
+ // Set the requirement member of accumulated configuration to equal
+ // calculated requirement.
+ aOutRequirement = requirement;
+
+ return true;
+}
+
+// 3.1.1.2, step 12
+// Follow the steps for the first matching condition from the following list:
+// If the sessionTypes member is present in candidate configuration.
+// Let session types be candidate configuration's sessionTypes member.
+// Otherwise let session types be ["temporary"].
+// Note: This returns an empty array on malloc failure.
+static Sequence<nsString> UnboxSessionTypes(
+ const Optional<Sequence<nsString>>& aSessionTypes) {
+ Sequence<nsString> sessionTypes;
+ if (aSessionTypes.WasPassed()) {
+ sessionTypes = aSessionTypes.Value();
+ } else {
+ // Note: fallible. Results in an empty array.
+ (void)sessionTypes.AppendElement(ToString(MediaKeySessionType::Temporary),
+ mozilla::fallible);
+ }
+ return sessionTypes;
+}
+
+// 3.1.1.2 Get Supported Configuration and Consent
+static bool GetSupportedConfig(
+ const KeySystemConfig& aKeySystem,
+ const MediaKeySystemConfiguration& aCandidate,
+ MediaKeySystemConfiguration& aOutConfig,
+ DecoderDoctorDiagnostics* aDiagnostics, bool aInPrivateBrowsing,
+ const std::function<void(const char*)>& aDeprecationLogFn) {
+ // Let accumulated configuration be a new MediaKeySystemConfiguration
+ // dictionary.
+ MediaKeySystemConfiguration config;
+ // Set the label member of accumulated configuration to equal the label member
+ // of candidate configuration.
+ config.mLabel = aCandidate.mLabel;
+ // If the initDataTypes member of candidate configuration is non-empty, run
+ // the following steps:
+ if (!aCandidate.mInitDataTypes.IsEmpty()) {
+ // Let supported types be an empty sequence of DOMStrings.
+ nsTArray<nsString> supportedTypes;
+ // For each value in candidate configuration's initDataTypes member:
+ for (const nsString& initDataType : aCandidate.mInitDataTypes) {
+ // Let initDataType be the value.
+ // If the implementation supports generating requests based on
+ // initDataType, add initDataType to supported types. String comparison is
+ // case-sensitive. The empty string is never supported.
+ if (aKeySystem.mInitDataTypes.Contains(initDataType)) {
+ supportedTypes.AppendElement(initDataType);
+ }
+ }
+ // If supported types is empty, return NotSupported.
+ if (supportedTypes.IsEmpty()) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') rejected; "
+ "no supported initDataTypes provided.",
+ NS_ConvertUTF16toUTF8(aCandidate.mLabel).get());
+ return false;
+ }
+ // Set the initDataTypes member of accumulated configuration to supported
+ // types.
+ if (!config.mInitDataTypes.Assign(supportedTypes)) {
+ return false;
+ }
+ }
+
+ if (!CheckRequirement(aCandidate.mDistinctiveIdentifier,
+ aKeySystem.mDistinctiveIdentifier,
+ config.mDistinctiveIdentifier)) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') rejected; "
+ "distinctiveIdentifier requirement not satisfied.",
+ NS_ConvertUTF16toUTF8(aCandidate.mLabel).get());
+ return false;
+ }
+
+ if (!CheckRequirement(aCandidate.mPersistentState,
+ aKeySystem.mPersistentState, config.mPersistentState)) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') rejected; "
+ "persistentState requirement not satisfied.",
+ NS_ConvertUTF16toUTF8(aCandidate.mLabel).get());
+ return false;
+ }
+
+ if (config.mPersistentState == MediaKeysRequirement::Required &&
+ aInPrivateBrowsing) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') rejected; "
+ "persistentState requested in Private Browsing window.",
+ NS_ConvertUTF16toUTF8(aCandidate.mLabel).get());
+ return false;
+ }
+
+ Sequence<nsString> sessionTypes(UnboxSessionTypes(aCandidate.mSessionTypes));
+ if (sessionTypes.IsEmpty()) {
+ // Malloc failure.
+ return false;
+ }
+
+ // For each value in session types:
+ for (const auto& sessionTypeString : sessionTypes) {
+ // Let session type be the value.
+ MediaKeySessionType sessionType;
+ if (!ToSessionType(sessionTypeString, sessionType)) {
+ // (Assume invalid sessionType is unsupported as per steps below).
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') rejected; "
+ "invalid session type specified.",
+ NS_ConvertUTF16toUTF8(aCandidate.mLabel).get());
+ return false;
+ }
+ // If accumulated configuration's persistentState value is "not-allowed"
+ // and the Is persistent session type? algorithm returns true for session
+ // type return NotSupported.
+ if (config.mPersistentState == MediaKeysRequirement::Not_allowed &&
+ IsPersistentSessionType(sessionType)) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') rejected; "
+ "persistent session requested but keysystem doesn't"
+ "support persistent state.",
+ NS_ConvertUTF16toUTF8(aCandidate.mLabel).get());
+ return false;
+ }
+ // If the implementation does not support session type in combination
+ // with accumulated configuration and restrictions for other reasons,
+ // return NotSupported.
+ if (!ContainsSessionType(aKeySystem.mSessionTypes, sessionType)) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') rejected; "
+ "session type '%s' unsupported by keySystem.",
+ NS_ConvertUTF16toUTF8(aCandidate.mLabel).get(),
+ NS_ConvertUTF16toUTF8(sessionTypeString).get());
+ return false;
+ }
+ // If accumulated configuration's persistentState value is "optional"
+ // and the result of running the Is persistent session type? algorithm
+ // on session type is true, change accumulated configuration's
+ // persistentState value to "required".
+ if (config.mPersistentState == MediaKeysRequirement::Optional &&
+ IsPersistentSessionType(sessionType)) {
+ config.mPersistentState = MediaKeysRequirement::Required;
+ }
+ }
+ // Set the sessionTypes member of accumulated configuration to session types.
+ config.mSessionTypes.Construct(std::move(sessionTypes));
+
+ // If the videoCapabilities and audioCapabilities members in candidate
+ // configuration are both empty, return NotSupported.
+ if (aCandidate.mAudioCapabilities.IsEmpty() &&
+ aCandidate.mVideoCapabilities.IsEmpty()) {
+ // TODO: Most sites using EME still don't pass capabilities, so we
+ // can't reject on it yet without breaking them. So add this later.
+ // Log deprecation warning to encourage authors to not do this!
+ aDeprecationLogFn("MediaEMENoCapabilitiesDeprecatedWarning");
+ }
+
+ // If the videoCapabilities member in candidate configuration is non-empty:
+ if (!aCandidate.mVideoCapabilities.IsEmpty()) {
+ // Let video capabilities be the result of executing the Get Supported
+ // Capabilities for Audio/Video Type algorithm on Video, candidate
+ // configuration's videoCapabilities member, accumulated configuration,
+ // and restrictions.
+ Sequence<MediaKeySystemMediaCapability> caps =
+ GetSupportedCapabilities(Video, aCandidate.mVideoCapabilities, config,
+ aKeySystem, aDiagnostics, aDeprecationLogFn);
+ // If video capabilities is null, return NotSupported.
+ if (caps.IsEmpty()) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') rejected; "
+ "no supported video capabilities.",
+ NS_ConvertUTF16toUTF8(aCandidate.mLabel).get());
+ return false;
+ }
+ // Set the videoCapabilities member of accumulated configuration to video
+ // capabilities.
+ config.mVideoCapabilities = std::move(caps);
+ } else {
+ // Otherwise:
+ // Set the videoCapabilities member of accumulated configuration to an empty
+ // sequence.
+ }
+
+ // If the audioCapabilities member in candidate configuration is non-empty:
+ if (!aCandidate.mAudioCapabilities.IsEmpty()) {
+ // Let audio capabilities be the result of executing the Get Supported
+ // Capabilities for Audio/Video Type algorithm on Audio, candidate
+ // configuration's audioCapabilities member, accumulated configuration, and
+ // restrictions.
+ Sequence<MediaKeySystemMediaCapability> caps =
+ GetSupportedCapabilities(Audio, aCandidate.mAudioCapabilities, config,
+ aKeySystem, aDiagnostics, aDeprecationLogFn);
+ // If audio capabilities is null, return NotSupported.
+ if (caps.IsEmpty()) {
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') rejected; "
+ "no supported audio capabilities.",
+ NS_ConvertUTF16toUTF8(aCandidate.mLabel).get());
+ return false;
+ }
+ // Set the audioCapabilities member of accumulated configuration to audio
+ // capabilities.
+ config.mAudioCapabilities = std::move(caps);
+ } else {
+ // Otherwise:
+ // Set the audioCapabilities member of accumulated configuration to an empty
+ // sequence.
+ }
+
+ // If accumulated configuration's distinctiveIdentifier value is "optional",
+ // follow the steps for the first matching condition from the following list:
+ if (config.mDistinctiveIdentifier == MediaKeysRequirement::Optional) {
+ // If the implementation requires use Distinctive Identifier(s) or
+ // Distinctive Permanent Identifier(s) for any of the combinations
+ // in accumulated configuration
+ if (aKeySystem.mDistinctiveIdentifier ==
+ KeySystemConfig::Requirement::Required) {
+ // Change accumulated configuration's distinctiveIdentifier value to
+ // "required".
+ config.mDistinctiveIdentifier = MediaKeysRequirement::Required;
+ } else {
+ // Otherwise, change accumulated configuration's distinctiveIdentifier
+ // value to "not-allowed".
+ config.mDistinctiveIdentifier = MediaKeysRequirement::Not_allowed;
+ }
+ }
+
+ // If accumulated configuration's persistentState value is "optional", follow
+ // the steps for the first matching condition from the following list:
+ if (config.mPersistentState == MediaKeysRequirement::Optional) {
+ // If the implementation requires persisting state for any of the
+ // combinations in accumulated configuration
+ if (aKeySystem.mPersistentState == KeySystemConfig::Requirement::Required) {
+ // Change accumulated configuration's persistentState value to "required".
+ config.mPersistentState = MediaKeysRequirement::Required;
+ } else {
+ // Otherwise, change accumulated configuration's persistentState
+ // value to "not-allowed".
+ config.mPersistentState = MediaKeysRequirement::Not_allowed;
+ }
+ }
+
+ // Note: Omitting steps 20-22. We don't ask for consent.
+
+#if defined(XP_WIN)
+ // Widevine CDM doesn't include an AAC decoder. So if WMF can't decode AAC,
+ // and a codec wasn't specified, be conservative and reject the MediaKeys
+ // request.
+ if (IsWidevineKeySystem(aKeySystem.mKeySystem) &&
+ (aCandidate.mAudioCapabilities.IsEmpty() ||
+ aCandidate.mVideoCapabilities.IsEmpty()) &&
+ !WMFDecoderModule::CanCreateMFTDecoder(WMFStreamType::AAC)) {
+ if (aDiagnostics) {
+ aDiagnostics->SetKeySystemIssue(
+ DecoderDoctorDiagnostics::eWidevineWithNoWMF);
+ }
+ EME_LOG(
+ "MediaKeySystemConfiguration (label='%s') rejected; "
+ "WMF required for Widevine decoding, but it's not available.",
+ NS_ConvertUTF16toUTF8(aCandidate.mLabel).get());
+ return false;
+ }
+#endif
+
+ // Return accumulated configuration.
+ aOutConfig = config;
+
+ return true;
+}
+
+/* static */
+bool MediaKeySystemAccess::GetSupportedConfig(
+ const nsAString& aKeySystem,
+ const Sequence<MediaKeySystemConfiguration>& aConfigs,
+ MediaKeySystemConfiguration& aOutConfig,
+ DecoderDoctorDiagnostics* aDiagnostics, bool aIsPrivateBrowsing,
+ const std::function<void(const char*)>& aDeprecationLogFn) {
+ KeySystemConfig implementation;
+ if (!GetKeySystemConfig(aKeySystem, implementation)) {
+ return false;
+ }
+ for (const MediaKeySystemConfiguration& candidate : aConfigs) {
+ if (mozilla::dom::GetSupportedConfig(implementation, candidate, aOutConfig,
+ aDiagnostics, aIsPrivateBrowsing,
+ aDeprecationLogFn)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/* static */
+void MediaKeySystemAccess::NotifyObservers(nsPIDOMWindowInner* aWindow,
+ const nsAString& aKeySystem,
+ MediaKeySystemStatus aStatus) {
+ RequestMediaKeySystemAccessNotification data;
+ data.mKeySystem = aKeySystem;
+ data.mStatus = aStatus;
+ nsAutoString json;
+ data.ToJSON(json);
+ EME_LOG("MediaKeySystemAccess::NotifyObservers() %s",
+ NS_ConvertUTF16toUTF8(json).get());
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ if (obs) {
+ obs->NotifyObservers(aWindow, MediaKeys::kMediaKeysRequestTopic,
+ json.get());
+ }
+}
+
+static nsCString ToCString(const nsString& aString) {
+ nsCString str("'");
+ str.Append(NS_ConvertUTF16toUTF8(aString));
+ str.AppendLiteral("'");
+ return str;
+}
+
+static nsCString ToCString(const MediaKeysRequirement aValue) {
+ nsCString str("'");
+ str.AppendASCII(MediaKeysRequirementValues::GetString(aValue));
+ str.AppendLiteral("'");
+ return str;
+}
+
+static nsCString ToCString(const MediaKeySystemMediaCapability& aValue) {
+ nsCString str;
+ str.AppendLiteral("{contentType=");
+ str.Append(ToCString(aValue.mContentType));
+ str.AppendLiteral(", robustness=");
+ str.Append(ToCString(aValue.mRobustness));
+ str.AppendLiteral(", encryptionScheme=");
+ str.Append(ToCString(aValue.mEncryptionScheme));
+ str.AppendLiteral("}");
+ return str;
+}
+
+template <class Type>
+static nsCString ToCString(const Sequence<Type>& aSequence) {
+ nsCString str;
+ str.AppendLiteral("[");
+ StringJoinAppend(str, ","_ns, aSequence,
+ [](nsACString& dest, const Type& element) {
+ dest.Append(ToCString(element));
+ });
+ str.AppendLiteral("]");
+ return str;
+}
+
+template <class Type>
+static nsCString ToCString(const Optional<Sequence<Type>>& aOptional) {
+ nsCString str;
+ if (aOptional.WasPassed()) {
+ str.Append(ToCString(aOptional.Value()));
+ } else {
+ str.AppendLiteral("[]");
+ }
+ return str;
+}
+
+static nsCString ToCString(const MediaKeySystemConfiguration& aConfig) {
+ nsCString str;
+ str.AppendLiteral("{label=");
+ str.Append(ToCString(aConfig.mLabel));
+
+ str.AppendLiteral(", initDataTypes=");
+ str.Append(ToCString(aConfig.mInitDataTypes));
+
+ str.AppendLiteral(", audioCapabilities=");
+ str.Append(ToCString(aConfig.mAudioCapabilities));
+
+ str.AppendLiteral(", videoCapabilities=");
+ str.Append(ToCString(aConfig.mVideoCapabilities));
+
+ str.AppendLiteral(", distinctiveIdentifier=");
+ str.Append(ToCString(aConfig.mDistinctiveIdentifier));
+
+ str.AppendLiteral(", persistentState=");
+ str.Append(ToCString(aConfig.mPersistentState));
+
+ str.AppendLiteral(", sessionTypes=");
+ str.Append(ToCString(aConfig.mSessionTypes));
+
+ str.AppendLiteral("}");
+
+ return str;
+}
+
+/* static */
+nsCString MediaKeySystemAccess::ToCString(
+ const Sequence<MediaKeySystemConfiguration>& aConfig) {
+ return mozilla::dom::ToCString(aConfig);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/eme/MediaKeySystemAccess.h b/dom/media/eme/MediaKeySystemAccess.h
new file mode 100644
index 0000000000..b7ad9086b1
--- /dev/null
+++ b/dom/media/eme/MediaKeySystemAccess.h
@@ -0,0 +1,81 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_MediaKeySystemAccess_h
+#define mozilla_dom_MediaKeySystemAccess_h
+
+#include "mozilla/Attributes.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsWrapperCache.h"
+
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/MediaKeySystemAccessBinding.h"
+#include "mozilla/dom/MediaKeysRequestStatusBinding.h"
+
+#include "js/TypeDecls.h"
+
+namespace mozilla {
+
+class DecoderDoctorDiagnostics;
+class ErrorResult;
+
+namespace dom {
+
+class MediaKeySystemAccess final : public nsISupports, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(MediaKeySystemAccess)
+
+ public:
+ explicit MediaKeySystemAccess(nsPIDOMWindowInner* aParent,
+ const nsAString& aKeySystem,
+ const MediaKeySystemConfiguration& aConfig);
+
+ protected:
+ ~MediaKeySystemAccess();
+
+ public:
+ nsPIDOMWindowInner* GetParentObject() const;
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ void GetKeySystem(nsString& aRetVal) const;
+
+ void GetConfiguration(MediaKeySystemConfiguration& aConfig);
+
+ already_AddRefed<Promise> CreateMediaKeys(ErrorResult& aRv);
+
+ static MediaKeySystemStatus GetKeySystemStatus(
+ const nsAString& aKeySystem, nsACString& aOutExceptionMessage);
+
+ static void NotifyObservers(nsPIDOMWindowInner* aWindow,
+ const nsAString& aKeySystem,
+ MediaKeySystemStatus aStatus);
+
+ static bool GetSupportedConfig(
+ const nsAString& aKeySystem,
+ const Sequence<MediaKeySystemConfiguration>& aConfigs,
+ MediaKeySystemConfiguration& aOutConfig,
+ DecoderDoctorDiagnostics* aDiagnostics, bool aIsPrivateBrowsing,
+ const std::function<void(const char*)>& aDeprecationLogFn);
+
+ static bool KeySystemSupportsInitDataType(const nsAString& aKeySystem,
+ const nsAString& aInitDataType);
+
+ static nsCString ToCString(
+ const Sequence<MediaKeySystemConfiguration>& aConfig);
+
+ private:
+ nsCOMPtr<nsPIDOMWindowInner> mParent;
+ const nsString mKeySystem;
+ const MediaKeySystemConfiguration mConfig;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_MediaKeySystemAccess_h
diff --git a/dom/media/eme/MediaKeySystemAccessManager.cpp b/dom/media/eme/MediaKeySystemAccessManager.cpp
new file mode 100644
index 0000000000..e6febfef54
--- /dev/null
+++ b/dom/media/eme/MediaKeySystemAccessManager.cpp
@@ -0,0 +1,684 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaKeySystemAccessManager.h"
+
+#include "DecoderDoctorDiagnostics.h"
+#include "MediaKeySystemAccessPermissionRequest.h"
+#include "VideoUtils.h"
+#include "mozilla/dom/BrowserChild.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/DetailedPromise.h"
+#include "mozilla/EMEUtils.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/Unused.h"
+#ifdef XP_WIN
+# include "mozilla/WindowsVersion.h"
+#endif
+#ifdef XP_MACOSX
+# include "nsCocoaFeatures.h"
+#endif
+#include "nsComponentManagerUtils.h"
+#include "nsContentUtils.h"
+#include "nsTHashMap.h"
+#include "nsIObserverService.h"
+#include "nsIScriptError.h"
+#include "nsPrintfCString.h"
+#include "nsServiceManagerUtils.h"
+
+namespace mozilla::dom {
+
+MediaKeySystemAccessManager::PendingRequest::PendingRequest(
+ DetailedPromise* aPromise, const nsAString& aKeySystem,
+ const Sequence<MediaKeySystemConfiguration>& aConfigs)
+ : mPromise(aPromise), mKeySystem(aKeySystem), mConfigs(aConfigs) {
+ MOZ_COUNT_CTOR(MediaKeySystemAccessManager::PendingRequest);
+}
+
+MediaKeySystemAccessManager::PendingRequest::~PendingRequest() {
+ MOZ_COUNT_DTOR(MediaKeySystemAccessManager::PendingRequest);
+}
+
+void MediaKeySystemAccessManager::PendingRequest::CancelTimer() {
+ if (mTimer) {
+ mTimer->Cancel();
+ mTimer = nullptr;
+ }
+}
+
+void MediaKeySystemAccessManager::PendingRequest::
+ RejectPromiseWithInvalidAccessError(const nsACString& aReason) {
+ if (mPromise) {
+ mPromise->MaybeRejectWithInvalidAccessError(aReason);
+ }
+}
+
+void MediaKeySystemAccessManager::PendingRequest::
+ RejectPromiseWithNotSupportedError(const nsACString& aReason) {
+ if (mPromise) {
+ mPromise->MaybeRejectWithNotSupportedError(aReason);
+ }
+}
+
+void MediaKeySystemAccessManager::PendingRequest::RejectPromiseWithTypeError(
+ const nsACString& aReason) {
+ if (mPromise) {
+ mPromise->MaybeRejectWithTypeError(aReason);
+ }
+}
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaKeySystemAccessManager)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver)
+ NS_INTERFACE_MAP_ENTRY(nsIObserver)
+ NS_INTERFACE_MAP_ENTRY(nsINamed)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaKeySystemAccessManager)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaKeySystemAccessManager)
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(MediaKeySystemAccessManager)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(MediaKeySystemAccessManager)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mWindow)
+ for (size_t i = 0; i < tmp->mPendingInstallRequests.Length(); i++) {
+ tmp->mPendingInstallRequests[i]->CancelTimer();
+ tmp->mPendingInstallRequests[i]->RejectPromiseWithInvalidAccessError(
+ nsLiteralCString(
+ "Promise still outstanding at MediaKeySystemAccessManager GC"));
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingInstallRequests[i]->mPromise)
+ }
+ tmp->mPendingInstallRequests.Clear();
+ for (size_t i = 0; i < tmp->mPendingAppApprovalRequests.Length(); i++) {
+ tmp->mPendingAppApprovalRequests[i]->RejectPromiseWithInvalidAccessError(
+ nsLiteralCString(
+ "Promise still outstanding at MediaKeySystemAccessManager GC"));
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingAppApprovalRequests[i]->mPromise)
+ }
+ tmp->mPendingAppApprovalRequests.Clear();
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(MediaKeySystemAccessManager)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow)
+ for (size_t i = 0; i < tmp->mPendingInstallRequests.Length(); i++) {
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingInstallRequests[i]->mPromise)
+ }
+ for (size_t i = 0; i < tmp->mPendingAppApprovalRequests.Length(); i++) {
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingAppApprovalRequests[i]->mPromise)
+ }
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+#define MKSAM_LOG_DEBUG(msg, ...) \
+ EME_LOG("MediaKeySystemAccessManager::%s " msg, __func__, ##__VA_ARGS__)
+
+MediaKeySystemAccessManager::MediaKeySystemAccessManager(
+ nsPIDOMWindowInner* aWindow)
+ : mWindow(aWindow) {
+ MOZ_ASSERT(NS_IsMainThread());
+}
+
+MediaKeySystemAccessManager::~MediaKeySystemAccessManager() {
+ MOZ_ASSERT(NS_IsMainThread());
+ Shutdown();
+}
+
+void MediaKeySystemAccessManager::Request(
+ DetailedPromise* aPromise, const nsAString& aKeySystem,
+ const Sequence<MediaKeySystemConfiguration>& aConfigs) {
+ MOZ_ASSERT(NS_IsMainThread());
+ CheckDoesWindowSupportProtectedMedia(
+ MakeUnique<PendingRequest>(aPromise, aKeySystem, aConfigs));
+}
+
+void MediaKeySystemAccessManager::CheckDoesWindowSupportProtectedMedia(
+ UniquePtr<PendingRequest> aRequest) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aRequest);
+ MKSAM_LOG_DEBUG("aRequest->mKeySystem=%s",
+ NS_ConvertUTF16toUTF8(aRequest->mKeySystem).get());
+
+ // In Windows OS, some Firefox windows that host content cannot support
+ // protected content, so check the status of support for this window.
+ // On other platforms windows should always support protected media.
+#ifdef XP_WIN
+ RefPtr<BrowserChild> browser(BrowserChild::GetFrom(mWindow));
+ if (!browser) {
+ if (!XRE_IsParentProcess() || XRE_IsE10sParentProcess()) {
+ // In this case, there is no browser because the Navigator object has
+ // been disconnected from its window. Thus, reject the promise.
+ aRequest->mPromise->MaybeRejectWithTypeError(
+ "Browsing context is no longer available");
+ } else {
+ // In this case, there is no browser because e10s is off. Proceed with
+ // the request with support since this scenario is always supported.
+ MKSAM_LOG_DEBUG("Allowing protected media on Windows with e10s off.");
+
+ OnDoesWindowSupportProtectedMedia(true, std::move(aRequest));
+ }
+
+ return;
+ }
+
+ RefPtr<MediaKeySystemAccessManager> self(this);
+
+ MKSAM_LOG_DEBUG(
+ "Checking with browser if this window supports protected media.");
+ browser->DoesWindowSupportProtectedMedia()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self, request = std::move(aRequest)](
+ const BrowserChild::IsWindowSupportingProtectedMediaPromise::
+ ResolveOrRejectValue& value) mutable {
+ if (value.IsResolve()) {
+ self->OnDoesWindowSupportProtectedMedia(value.ResolveValue(),
+ std::move(request));
+ } else {
+ EME_LOG(
+ "MediaKeySystemAccessManager::DoesWindowSupportProtectedMedia-"
+ "ResolveOrRejectLambda Failed to make IPC call to "
+ "IsWindowSupportingProtectedMedia: "
+ "reason=%d",
+ static_cast<int>(value.RejectValue()));
+ // Treat as failure.
+ self->OnDoesWindowSupportProtectedMedia(false, std::move(request));
+ }
+ });
+
+#else
+ // Non-Windows OS windows always support protected media.
+ MKSAM_LOG_DEBUG(
+ "Allowing protected media because all non-Windows OS windows support "
+ "protected media.");
+ OnDoesWindowSupportProtectedMedia(true, std::move(aRequest));
+#endif
+}
+
+void MediaKeySystemAccessManager::OnDoesWindowSupportProtectedMedia(
+ bool aIsSupportedInWindow, UniquePtr<PendingRequest> aRequest) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aRequest);
+ MKSAM_LOG_DEBUG("aIsSupportedInWindow=%s aRequest->mKeySystem=%s",
+ aIsSupportedInWindow ? "true" : "false",
+ NS_ConvertUTF16toUTF8(aRequest->mKeySystem).get());
+
+ if (!aIsSupportedInWindow) {
+ aRequest->RejectPromiseWithNotSupportedError(
+ "EME is not supported in this window"_ns);
+ return;
+ }
+
+ RequestMediaKeySystemAccess(std::move(aRequest));
+}
+
+void MediaKeySystemAccessManager::CheckDoesAppAllowProtectedMedia(
+ UniquePtr<PendingRequest> aRequest) {
+ // At time of writing, only GeckoView is expected to leverage the need to
+ // approve EME requests from the application. However, this functionality
+ // can be tested on all platforms by manipulating the
+ // media.eme.require-app-approval + test prefs associated with
+ // MediaKeySystemPermissionRequest.
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aRequest);
+ MKSAM_LOG_DEBUG("aRequest->mKeySystem=%s",
+ NS_ConvertUTF16toUTF8(aRequest->mKeySystem).get());
+
+ if (!StaticPrefs::media_eme_require_app_approval()) {
+ MKSAM_LOG_DEBUG(
+ "media.eme.require-app-approval is false, allowing request.");
+ // We don't require app approval as the pref is not set. Treat as app
+ // approving by passing true to the handler.
+ OnDoesAppAllowProtectedMedia(true, std::move(aRequest));
+ return;
+ }
+
+ if (mAppAllowsProtectedMediaPromiseRequest.Exists()) {
+ // We already have a pending permission request, we don't need to fire
+ // another. Just wait for the existing permission request to be handled
+ // and the result from that will be used to handle the current request.
+ MKSAM_LOG_DEBUG(
+ "mAppAllowsProtectedMediaPromiseRequest already exists. aRequest "
+ "addded to queue and will be handled when exising permission request "
+ "is serviced.");
+ mPendingAppApprovalRequests.AppendElement(std::move(aRequest));
+ return;
+ }
+
+ RefPtr<MediaKeySystemAccessPermissionRequest> appApprovalRequest =
+ MediaKeySystemAccessPermissionRequest::Create(mWindow);
+ if (!appApprovalRequest) {
+ MKSAM_LOG_DEBUG(
+ "Failed to create app approval request! Blocking eme request as "
+ "fallback.");
+ aRequest->RejectPromiseWithInvalidAccessError(nsLiteralCString(
+ "Failed to create approval request to send to app embedding Gecko."));
+ return;
+ }
+
+ // If we're not using testing prefs (which take precedence over cached
+ // values) and have a cached value, handle based on the cached value.
+ if (appApprovalRequest->CheckPromptPrefs() ==
+ MediaKeySystemAccessPermissionRequest::PromptResult::Pending &&
+ mAppAllowsProtectedMedia) {
+ MKSAM_LOG_DEBUG(
+ "Short circuiting based on mAppAllowsProtectedMedia cached value");
+ OnDoesAppAllowProtectedMedia(*mAppAllowsProtectedMedia,
+ std::move(aRequest));
+ return;
+ }
+
+ // Store the approval request, it will be handled when we get a response
+ // from the app.
+ mPendingAppApprovalRequests.AppendElement(std::move(aRequest));
+
+ RefPtr<MediaKeySystemAccessPermissionRequest::RequestPromise> p =
+ appApprovalRequest->GetPromise();
+ p->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ // Allow callback
+ [this,
+ self = RefPtr<MediaKeySystemAccessManager>(this)](bool aRequestResult) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aRequestResult, "Result should be true on allow callback!");
+ mAppAllowsProtectedMediaPromiseRequest.Complete();
+ // Cache result.
+ mAppAllowsProtectedMedia = Some(aRequestResult);
+ // For each pending request, handle it based on the app's response.
+ for (UniquePtr<PendingRequest>& approvalRequest :
+ mPendingAppApprovalRequests) {
+ OnDoesAppAllowProtectedMedia(*mAppAllowsProtectedMedia,
+ std::move(approvalRequest));
+ }
+ self->mPendingAppApprovalRequests.Clear();
+ },
+ // Cancel callback
+ [this,
+ self = RefPtr<MediaKeySystemAccessManager>(this)](bool aRequestResult) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!aRequestResult,
+ "Result should be false on cancel callback!");
+ mAppAllowsProtectedMediaPromiseRequest.Complete();
+ // Cache result.
+ mAppAllowsProtectedMedia = Some(aRequestResult);
+ // For each pending request, handle it based on the app's response.
+ for (UniquePtr<PendingRequest>& approvalRequest :
+ mPendingAppApprovalRequests) {
+ OnDoesAppAllowProtectedMedia(*mAppAllowsProtectedMedia,
+ std::move(approvalRequest));
+ }
+ self->mPendingAppApprovalRequests.Clear();
+ })
+ ->Track(mAppAllowsProtectedMediaPromiseRequest);
+
+ // Prefs not causing short circuit, no cached value, go ahead and request
+ // permission.
+ MKSAM_LOG_DEBUG("Dispatching async request for app approval");
+ if (NS_FAILED(appApprovalRequest->Start())) {
+ // This shouldn't happen unless we're shutting down or similar edge cases.
+ // If this is regularly being hit then something is wrong and should be
+ // investigated.
+ MKSAM_LOG_DEBUG(
+ "Failed to start app approval request! Eme approval will be left in "
+ "limbo!");
+ }
+}
+
+void MediaKeySystemAccessManager::OnDoesAppAllowProtectedMedia(
+ bool aIsAllowed, UniquePtr<PendingRequest> aRequest) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aRequest);
+ MKSAM_LOG_DEBUG("aIsAllowed=%s aRequest->mKeySystem=%s",
+ aIsAllowed ? "true" : "false",
+ NS_ConvertUTF16toUTF8(aRequest->mKeySystem).get());
+ if (!aIsAllowed) {
+ aRequest->RejectPromiseWithNotSupportedError(
+ nsLiteralCString("The application embedding this user agent has "
+ "blocked MediaKeySystemAccess"));
+ return;
+ }
+
+ ProvideAccess(std::move(aRequest));
+}
+
+void MediaKeySystemAccessManager::RequestMediaKeySystemAccess(
+ UniquePtr<PendingRequest> aRequest) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aRequest);
+ MKSAM_LOG_DEBUG("aIsSupportedInWindow=%s",
+ NS_ConvertUTF16toUTF8(aRequest->mKeySystem).get());
+
+ // 1. If keySystem is the empty string, return a promise rejected with a newly
+ // created TypeError.
+ if (aRequest->mKeySystem.IsEmpty()) {
+ aRequest->mPromise->MaybeRejectWithTypeError("Key system string is empty");
+ // Don't notify DecoderDoctor, as there's nothing we or the user can
+ // do to fix this situation; the site is using the API wrong.
+ return;
+ }
+ // 2. If supportedConfigurations is empty, return a promise rejected with a
+ // newly created TypeError.
+ if (aRequest->mConfigs.IsEmpty()) {
+ aRequest->mPromise->MaybeRejectWithTypeError(
+ "Candidate MediaKeySystemConfigs is empty");
+ // Don't notify DecoderDoctor, as there's nothing we or the user can
+ // do to fix this situation; the site is using the API wrong.
+ return;
+ }
+
+ // 3. Let document be the calling context's Document.
+ // 4. Let origin be the origin of document.
+ // 5. Let promise be a new promise.
+ // 6. Run the following steps in parallel:
+
+ DecoderDoctorDiagnostics diagnostics;
+
+ // 1. If keySystem is not one of the Key Systems supported by the user
+ // agent, reject promise with a NotSupportedError. String comparison is
+ // case-sensitive.
+ if (!IsWidevineKeySystem(aRequest->mKeySystem) &&
+#ifdef MOZ_WMF_CDM
+ !IsPlayReadyKeySystemAndSupported(aRequest->mKeySystem) &&
+#endif
+ !IsClearkeyKeySystem(aRequest->mKeySystem)) {
+ // Not to inform user, because nothing to do if the keySystem is not
+ // supported.
+ aRequest->RejectPromiseWithNotSupportedError(
+ "Key system is unsupported"_ns);
+ diagnostics.StoreMediaKeySystemAccess(
+ mWindow->GetExtantDoc(), aRequest->mKeySystem, false, __func__);
+ return;
+ }
+
+ if (!StaticPrefs::media_eme_enabled() &&
+ !IsClearkeyKeySystem(aRequest->mKeySystem)) {
+ // EME disabled by user, send notification to chrome so UI can inform user.
+ // Clearkey is allowed even when EME is disabled because we want the pref
+ // "media.eme.enabled" only taking effect on proprietary DRMs.
+ // We don't show the notification if the pref is locked.
+ if (!Preferences::IsLocked("media.eme.enabled")) {
+ MediaKeySystemAccess::NotifyObservers(mWindow, aRequest->mKeySystem,
+ MediaKeySystemStatus::Api_disabled);
+ }
+ aRequest->RejectPromiseWithNotSupportedError("EME has been preffed off"_ns);
+ diagnostics.StoreMediaKeySystemAccess(
+ mWindow->GetExtantDoc(), aRequest->mKeySystem, false, __func__);
+ return;
+ }
+
+ nsAutoCString message;
+ MediaKeySystemStatus status =
+ MediaKeySystemAccess::GetKeySystemStatus(aRequest->mKeySystem, message);
+
+ nsPrintfCString msg(
+ "MediaKeySystemAccess::GetKeySystemStatus(%s) "
+ "result=%s msg='%s'",
+ NS_ConvertUTF16toUTF8(aRequest->mKeySystem).get(),
+ nsCString(MediaKeySystemStatusValues::GetString(status)).get(),
+ message.get());
+ LogToBrowserConsole(NS_ConvertUTF8toUTF16(msg));
+
+ // We may need to install the CDM to continue.
+ if (status == MediaKeySystemStatus::Cdm_not_installed &&
+ IsWidevineKeySystem(aRequest->mKeySystem)) {
+ // These are cases which could be resolved by downloading a new(er) CDM.
+ // When we send the status to chrome, chrome's GMPProvider will attempt to
+ // download or update the CDM. In AwaitInstall() we add listeners to wait
+ // for the update to complete, and we'll call this function again with
+ // aType==Subsequent once the download has completed and the GMPService
+ // has had a new plugin added. AwaitInstall() sets a timer to fail if the
+ // update/download takes too long or fails.
+
+ if (aRequest->mRequestType != PendingRequest::RequestType::Initial) {
+ MOZ_ASSERT(aRequest->mRequestType ==
+ PendingRequest::RequestType::Subsequent);
+ // CDM is not installed, but this is a subsequent request. We've waited,
+ // but can't service this request! Give up. Chrome will still be showing a
+ // "I can't play, updating" notification.
+ aRequest->RejectPromiseWithNotSupportedError(
+ "Timed out while waiting for a CDM update"_ns);
+ diagnostics.StoreMediaKeySystemAccess(
+ mWindow->GetExtantDoc(), aRequest->mKeySystem, false, __func__);
+ return;
+ }
+
+ const nsString keySystem = aRequest->mKeySystem;
+ if (AwaitInstall(std::move(aRequest))) {
+ // Notify chrome that we're going to wait for the CDM to download/update.
+ MediaKeySystemAccess::NotifyObservers(mWindow, keySystem, status);
+ } else {
+ // Failed to await the install. Log failure and give up trying to service
+ // this request.
+ diagnostics.StoreMediaKeySystemAccess(mWindow->GetExtantDoc(), keySystem,
+ false, __func__);
+ }
+ return;
+ }
+ if (status != MediaKeySystemStatus::Available) {
+ // Failed due to user disabling something, send a notification to
+ // chrome, so we can show some UI to explain how the user can rectify
+ // the situation.
+ MediaKeySystemAccess::NotifyObservers(mWindow, aRequest->mKeySystem,
+ status);
+ aRequest->RejectPromiseWithNotSupportedError(message);
+ return;
+ }
+
+ nsCOMPtr<Document> doc = mWindow->GetExtantDoc();
+ nsTHashMap<nsCharPtrHashKey, bool> warnings;
+ std::function<void(const char*)> deprecationWarningLogFn =
+ [&](const char* aMsgName) {
+ EME_LOG(
+ "MediaKeySystemAccessManager::DeprecationWarningLambda Logging "
+ "deprecation warning '%s' to WebConsole.",
+ aMsgName);
+ warnings.InsertOrUpdate(aMsgName, true);
+ AutoTArray<nsString, 1> params;
+ nsString& uri = *params.AppendElement();
+ if (doc) {
+ Unused << doc->GetDocumentURI(uri);
+ }
+ nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "Media"_ns,
+ doc, nsContentUtils::eDOM_PROPERTIES,
+ aMsgName, params);
+ };
+
+ bool isPrivateBrowsing =
+ mWindow->GetExtantDoc() &&
+ mWindow->GetExtantDoc()->NodePrincipal()->GetPrivateBrowsingId() > 0;
+ // 2. Let implementation be the implementation of keySystem.
+ // 3. For each value in supportedConfigurations:
+ // 1. Let candidate configuration be the value.
+ // 2. Let supported configuration be the result of executing the Get
+ // Supported Configuration algorithm on implementation, candidate
+ // configuration, and origin.
+ // 3. If supported configuration is not NotSupported, run the following
+ // steps:
+ // 1. Let access be a new MediaKeySystemAccess object, and initialize it
+ // as follows:
+ // 1. Set the keySystem attribute to keySystem.
+ // 2. Let the configuration value be supported configuration.
+ // 3. Let the cdm implementation value be implementation.
+ // 2. Resolve promise with access and abort the parallel steps of this
+ // algorithm.
+ MediaKeySystemConfiguration config;
+ if (MediaKeySystemAccess::GetSupportedConfig(
+ aRequest->mKeySystem, aRequest->mConfigs, config, &diagnostics,
+ isPrivateBrowsing, deprecationWarningLogFn)) {
+ aRequest->mSupportedConfig = Some(config);
+ // The app gets the final say on if we provide access or not.
+ CheckDoesAppAllowProtectedMedia(std::move(aRequest));
+ return;
+ }
+ // 4. Reject promise with a NotSupportedError.
+
+ // Not to inform user, because nothing to do if the corresponding keySystem
+ // configuration is not supported.
+ aRequest->RejectPromiseWithNotSupportedError(
+ "Key system configuration is not supported"_ns);
+ diagnostics.StoreMediaKeySystemAccess(mWindow->GetExtantDoc(),
+ aRequest->mKeySystem, false, __func__);
+}
+
+void MediaKeySystemAccessManager::ProvideAccess(
+ UniquePtr<PendingRequest> aRequest) {
+ MOZ_ASSERT(aRequest);
+ MOZ_ASSERT(
+ aRequest->mSupportedConfig,
+ "The request needs a supported config if we're going to provide access!");
+ MKSAM_LOG_DEBUG("aRequest->mKeySystem=%s",
+ NS_ConvertUTF16toUTF8(aRequest->mKeySystem).get());
+
+ DecoderDoctorDiagnostics diagnostics;
+
+ RefPtr<MediaKeySystemAccess> access(new MediaKeySystemAccess(
+ mWindow, aRequest->mKeySystem, aRequest->mSupportedConfig.ref()));
+ aRequest->mPromise->MaybeResolve(access);
+ diagnostics.StoreMediaKeySystemAccess(mWindow->GetExtantDoc(),
+ aRequest->mKeySystem, true, __func__);
+}
+
+bool MediaKeySystemAccessManager::AwaitInstall(
+ UniquePtr<PendingRequest> aRequest) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aRequest);
+ MKSAM_LOG_DEBUG("aRequest->mKeySystem=%s",
+ NS_ConvertUTF16toUTF8(aRequest->mKeySystem).get());
+
+ if (!EnsureObserversAdded()) {
+ NS_WARNING("Failed to add pref observer");
+ aRequest->RejectPromiseWithNotSupportedError(nsLiteralCString(
+ "Failed trying to setup CDM update: failed adding observers"));
+ return false;
+ }
+
+ nsCOMPtr<nsITimer> timer;
+ NS_NewTimerWithObserver(getter_AddRefs(timer), this, 60 * 1000,
+ nsITimer::TYPE_ONE_SHOT);
+ if (!timer) {
+ NS_WARNING("Failed to create timer to await CDM install.");
+ aRequest->RejectPromiseWithNotSupportedError(nsLiteralCString(
+ "Failed trying to setup CDM update: failed timer creation"));
+ return false;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(
+ aRequest->mTimer == nullptr,
+ "Timer should not already be set on a request we're about to await");
+ aRequest->mTimer = timer;
+
+ mPendingInstallRequests.AppendElement(std::move(aRequest));
+ return true;
+}
+
+void MediaKeySystemAccessManager::RetryRequest(
+ UniquePtr<PendingRequest> aRequest) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aRequest);
+ MKSAM_LOG_DEBUG("aRequest->mKeySystem=%s",
+ NS_ConvertUTF16toUTF8(aRequest->mKeySystem).get());
+ // Cancel and null timer if it exists.
+ aRequest->CancelTimer();
+ // Indicate that this is a request that's being retried.
+ aRequest->mRequestType = PendingRequest::RequestType::Subsequent;
+ RequestMediaKeySystemAccess(std::move(aRequest));
+}
+
+nsresult MediaKeySystemAccessManager::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* aData) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MKSAM_LOG_DEBUG("%s", aTopic);
+
+ if (!strcmp(aTopic, "gmp-changed")) {
+ // Filter out the requests where the CDM's install-status is no longer
+ // "unavailable". This will be the CDMs which have downloaded since the
+ // initial request.
+ // Note: We don't have a way to communicate from chrome that the CDM has
+ // failed to download, so we'll just let the timeout fail us in that case.
+ nsTArray<UniquePtr<PendingRequest>> requests;
+ for (size_t i = mPendingInstallRequests.Length(); i-- > 0;) {
+ nsAutoCString message;
+ MediaKeySystemStatus status = MediaKeySystemAccess::GetKeySystemStatus(
+ mPendingInstallRequests[i]->mKeySystem, message);
+ if (status == MediaKeySystemStatus::Cdm_not_installed) {
+ // Not yet installed, don't retry. Keep waiting until timeout.
+ continue;
+ }
+ // Status has changed, retry request.
+ requests.AppendElement(std::move(mPendingInstallRequests[i]));
+ mPendingInstallRequests.RemoveElementAt(i);
+ }
+ // Retry all pending requests, but this time fail if the CDM is not
+ // installed.
+ for (size_t i = requests.Length(); i-- > 0;) {
+ RetryRequest(std::move(requests[i]));
+ }
+ } else if (!strcmp(aTopic, "timer-callback")) {
+ // Find the timer that expired and re-run the request for it.
+ nsCOMPtr<nsITimer> timer(do_QueryInterface(aSubject));
+ for (size_t i = 0; i < mPendingInstallRequests.Length(); i++) {
+ if (mPendingInstallRequests[i]->mTimer == timer) {
+ EME_LOG("MediaKeySystemAccessManager::AwaitInstall resuming request");
+ UniquePtr<PendingRequest> request =
+ std::move(mPendingInstallRequests[i]);
+ mPendingInstallRequests.RemoveElementAt(i);
+ RetryRequest(std::move(request));
+ break;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+nsresult MediaKeySystemAccessManager::GetName(nsACString& aName) {
+ aName.AssignLiteral("MediaKeySystemAccessManager");
+ return NS_OK;
+}
+
+bool MediaKeySystemAccessManager::EnsureObserversAdded() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mAddedObservers) {
+ return true;
+ }
+
+ nsCOMPtr<nsIObserverService> obsService =
+ mozilla::services::GetObserverService();
+ if (NS_WARN_IF(!obsService)) {
+ return false;
+ }
+ mAddedObservers =
+ NS_SUCCEEDED(obsService->AddObserver(this, "gmp-changed", false));
+ return mAddedObservers;
+}
+
+void MediaKeySystemAccessManager::Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MKSAM_LOG_DEBUG("");
+ for (const UniquePtr<PendingRequest>& installRequest :
+ mPendingInstallRequests) {
+ // Cancel all requests; we're shutting down.
+ installRequest->CancelTimer();
+ installRequest->RejectPromiseWithInvalidAccessError(nsLiteralCString(
+ "Promise still outstanding at MediaKeySystemAccessManager shutdown"));
+ }
+ mPendingInstallRequests.Clear();
+ for (const UniquePtr<PendingRequest>& approvalRequest :
+ mPendingAppApprovalRequests) {
+ approvalRequest->RejectPromiseWithInvalidAccessError(nsLiteralCString(
+ "Promise still outstanding at MediaKeySystemAccessManager shutdown"));
+ }
+ mPendingAppApprovalRequests.Clear();
+ mAppAllowsProtectedMediaPromiseRequest.DisconnectIfExists();
+ if (mAddedObservers) {
+ nsCOMPtr<nsIObserverService> obsService =
+ mozilla::services::GetObserverService();
+ if (obsService) {
+ obsService->RemoveObserver(this, "gmp-changed");
+ mAddedObservers = false;
+ }
+ }
+}
+
+} // namespace mozilla::dom
+
+#undef MKSAM_LOG_DEBUG
diff --git a/dom/media/eme/MediaKeySystemAccessManager.h b/dom/media/eme/MediaKeySystemAccessManager.h
new file mode 100644
index 0000000000..992de2a9e7
--- /dev/null
+++ b/dom/media/eme/MediaKeySystemAccessManager.h
@@ -0,0 +1,229 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_MEDIAKEYSYSTEMACCESSMANAGER_H_
+#define DOM_MEDIA_MEDIAKEYSYSTEMACCESSMANAGER_H_
+
+#include "mozilla/dom/MediaKeySystemAccess.h"
+#include "mozilla/MozPromise.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIObserver.h"
+#include "nsISupportsImpl.h"
+#include "nsITimer.h"
+
+namespace mozilla::dom {
+
+class DetailedPromise;
+class TestGMPVideoDecoder;
+
+/**
+ * MediaKeySystemAccessManager implements the functionality for
+ * Navigator.requestMediaKeySystemAccess(). The navigator may perform its own
+ * logic before passing the request to this class, but the majority of
+ * processing happens the MediaKeySystemAccessManager. The manager is expected
+ * to be run entirely on the main thread of the content process for whichever
+ * window it is associated with.
+ *
+ * As well as implementing the Navigator.requestMediaKeySystemAccess()
+ * algorithm, the manager performs Gecko specific logic. For example, the EME
+ * specification does not specify a process to check if a CDM is installed as
+ * part of requesting access, but that is an important part of obtaining access
+ * for Gecko, and is handled by the manager.
+ *
+ * A request made to the manager can be thought of as entering a pipeline.
+ * In this pipeline the request must pass through various stages that can
+ * reject the request and remove it from the pipeline. If a request is not
+ * rejected by the end of the pipeline it is approved/resolved.
+ *
+ * The pipeline is structured in such a way that each step should be executed
+ * even if it will quickly be exited. For example, the step that checks if a
+ * window supports protected media is an instant approve on non-Windows OSes,
+ * but we want to execute the function representing that step to ensure a
+ * deterministic execution and logging path. The hope is this reduces
+ * complexity for when we need to debug or change the code.
+ *
+ * While the pipeline metaphor generally holds, the implementation details of
+ * the manager mean that processing is not always linear: a request may be
+ * re-injected earlier into the pipeline for reprocessing. This can happen
+ * if the request was pending some other operation, e.g. CDM install, after
+ * which we wish to reprocess that request. However, we strive to keep it
+ * as linear as possible.
+ *
+ * A high level version of the happy path pipeline is depicted below. If a
+ * request were to fail any of the steps below it would be rejected and ejected
+ * from the pipeline.
+ *
+ * Request arrives from navigator
+ * +
+ * |
+ * v
+ * Check if window supports protected media
+ * +
+ * +<-------------------+
+ * v |
+ * Check request args are sane |
+ * + |
+ * | Wait for CDM and retry
+ * v |
+ * Check if CDM is installed |
+ * + |
+ * | |
+ * +--------------------+
+ * |
+ * v
+ * Check if CDM supports args
+ * +
+ * |
+ * v
+ * Check if app allows protected media
+ * (used by GeckoView)
+ * +
+ * |
+ * v
+ * Provide access
+ *
+ */
+
+class MediaKeySystemAccessManager final : public nsIObserver, public nsINamed {
+ public:
+ explicit MediaKeySystemAccessManager(nsPIDOMWindowInner* aWindow);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(MediaKeySystemAccessManager,
+ nsIObserver)
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSINAMED
+
+ // Entry point for the navigator to call into the manager.
+ void Request(DetailedPromise* aPromise, const nsAString& aKeySystem,
+ const Sequence<MediaKeySystemConfiguration>& aConfig);
+
+ void Shutdown();
+
+ private:
+ // Encapsulates the information for a Navigator.requestMediaKeySystemAccess()
+ // request that is being processed.
+ struct PendingRequest {
+ enum class RequestType { Initial, Subsequent };
+
+ PendingRequest(DetailedPromise* aPromise, const nsAString& aKeySystem,
+ const Sequence<MediaKeySystemConfiguration>& aConfigs);
+ ~PendingRequest();
+
+ // The JS promise associated with this request.
+ RefPtr<DetailedPromise> mPromise;
+ // The KeySystem passed for this request.
+ const nsString mKeySystem;
+ // The config(s) passed for this request.
+ const Sequence<MediaKeySystemConfiguration> mConfigs;
+
+ // If the request is
+ // - A first attempt request from JS: RequestType::Initial.
+ // - A request we're reprocessing due to a GMP being installed:
+ // RequestType::Subsequent.
+ RequestType mRequestType = RequestType::Initial;
+
+ // If we find a supported config for this request during processing it
+ // should be stored here. Only if we have a supported config should a
+ // request have access provided.
+ Maybe<MediaKeySystemConfiguration> mSupportedConfig;
+
+ // Will be set to trigger a timeout and re-processing of the request if the
+ // request is pending on some potentially time consuming operation, e.g.
+ // CDM install.
+ nsCOMPtr<nsITimer> mTimer = nullptr;
+
+ // Convenience methods to reject the wrapped promise.
+ void RejectPromiseWithInvalidAccessError(const nsACString& aReason);
+ void RejectPromiseWithNotSupportedError(const nsACString& aReason);
+ void RejectPromiseWithTypeError(const nsACString& aReason);
+
+ void CancelTimer();
+ };
+
+ // Check if the application (e.g. a GeckoView app) allows protected media in
+ // this window.
+ //
+ // This function is always expected to be executed as part of the pipeline of
+ // processing a request, but its behavior differs depending on prefs set.
+ //
+ // If the `media_eme_require_app_approval` pref is false, then the function
+ // assumes app approval and early returns. Otherwise the function will
+ // create a permission request to be approved by the embedding app. If the
+ // test prefs detailed in MediaKeySystemAccessPermissionRequest.h are set
+ // then they will control handling, otherwise it is up to the embedding
+ // app to handle the request.
+ //
+ // At the time of writing, only GeckoView based apps are expected to pref
+ // on this behavior.
+ //
+ // This function is expected to run late/last in the pipeline so that if we
+ // ask the app for permission we don't fail after the app okays the request.
+ // This is to avoid cases where a user may be prompted by the app to approve
+ // eme, this check then passes, but we fail later in the pipeline, leaving
+ // the user wondering why their approval didn't work.
+ void CheckDoesAppAllowProtectedMedia(UniquePtr<PendingRequest> aRequest);
+
+ // Handles the result of the app allowing or disallowing protected media.
+ // If there are pending requests in mPendingAppApprovalRequests then this
+ // needs to be called on each.
+ void OnDoesAppAllowProtectedMedia(bool aIsAllowed,
+ UniquePtr<PendingRequest> aRequest);
+
+ // Checks if the Window associated with this manager supports protected media
+ // and calls into OnDoesWindowSupportEncryptedMedia with the result.
+ void CheckDoesWindowSupportProtectedMedia(UniquePtr<PendingRequest> aRequest);
+
+ // Handle the result of checking if the window associated with this manager
+ // supports protected media. If the window doesn't support protected media
+ // this will reject the request, otherwise the request will continue to be
+ // processed.
+ void OnDoesWindowSupportProtectedMedia(bool aIsSupportedInWindow,
+ UniquePtr<PendingRequest> aRequest);
+
+ // Performs the 'requestMediaKeySystemAccess' algorithm detailed in the EME
+ // specification. Gecko may need to install a CDM to satisfy this check. If
+ // CDM install is needed this function may be called again for the same
+ // request once the CDM is installed or a timeout is reached.
+ void RequestMediaKeySystemAccess(UniquePtr<PendingRequest> aRequest);
+
+ // Approves aRequest and provides MediaKeySystemAccess by resolving the
+ // promise associated with the request.
+ void ProvideAccess(UniquePtr<PendingRequest> aRequest);
+
+ ~MediaKeySystemAccessManager();
+
+ bool EnsureObserversAdded();
+
+ bool AwaitInstall(UniquePtr<PendingRequest> aRequest);
+
+ void RetryRequest(UniquePtr<PendingRequest> aRequest);
+
+ // Requests waiting on approval from the application to be processed.
+ nsTArray<UniquePtr<PendingRequest>> mPendingAppApprovalRequests;
+
+ // Requests waiting on CDM installation to be processed.
+ nsTArray<UniquePtr<PendingRequest>> mPendingInstallRequests;
+
+ nsCOMPtr<nsPIDOMWindowInner> mWindow;
+ bool mAddedObservers = false;
+
+ // Has the app approved protected media playback? If it has we cache the
+ // value so we don't need to check again.
+ Maybe<bool> mAppAllowsProtectedMedia;
+
+ // If we're waiting for permission from the app to enable EME this holder
+ // should contain the request.
+ //
+ // Note the type in the holder should match
+ // MediaKeySystemAccessPermissionRequest::RequestPromise, but we can't
+ // include MediaKeySystemAccessPermissionRequest's header here without
+ // breaking the build, so we do this hack.
+ MozPromiseRequestHolder<MozPromise<bool, bool, true>>
+ mAppAllowsProtectedMediaPromiseRequest;
+};
+
+} // namespace mozilla::dom
+
+#endif // DOM_MEDIA_MEDIAKEYSYSTEMACCESSMANAGER_H_
diff --git a/dom/media/eme/MediaKeySystemAccessPermissionRequest.cpp b/dom/media/eme/MediaKeySystemAccessPermissionRequest.cpp
new file mode 100644
index 0000000000..063bf93f7e
--- /dev/null
+++ b/dom/media/eme/MediaKeySystemAccessPermissionRequest.cpp
@@ -0,0 +1,91 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaKeySystemAccessPermissionRequest.h"
+
+#include "nsGlobalWindowInner.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(MediaKeySystemAccessPermissionRequest,
+ ContentPermissionRequestBase)
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(
+ MediaKeySystemAccessPermissionRequest, ContentPermissionRequestBase)
+
+/* static */
+already_AddRefed<MediaKeySystemAccessPermissionRequest>
+MediaKeySystemAccessPermissionRequest::Create(nsPIDOMWindowInner* aWindow) {
+ // Could conceivably be created off main thread then used on main thread
+ // later. If we really need to do that at some point we could relax this
+ // assert.
+ AssertIsOnMainThread();
+ if (!aWindow) {
+ return nullptr;
+ }
+
+ nsGlobalWindowInner* innerWindow = nsGlobalWindowInner::Cast(aWindow);
+ if (!innerWindow->GetPrincipal()) {
+ return nullptr;
+ }
+
+ RefPtr<MediaKeySystemAccessPermissionRequest> request =
+ new MediaKeySystemAccessPermissionRequest(innerWindow);
+ return request.forget();
+}
+
+MediaKeySystemAccessPermissionRequest::MediaKeySystemAccessPermissionRequest(
+ nsGlobalWindowInner* aWindow)
+ : ContentPermissionRequestBase(aWindow->GetPrincipal(), aWindow,
+ "media.eme.require-app-approval"_ns,
+ "media-key-system-access"_ns) {}
+
+MediaKeySystemAccessPermissionRequest::
+ ~MediaKeySystemAccessPermissionRequest() {
+ AssertIsOnMainThread();
+ // If a request has not been serviced by the time it is destroyed, treat it
+ // as if the request was denied.
+ Cancel();
+}
+
+already_AddRefed<MediaKeySystemAccessPermissionRequest::RequestPromise>
+MediaKeySystemAccessPermissionRequest::GetPromise() {
+ return mPromiseHolder.Ensure(__func__);
+}
+
+nsresult MediaKeySystemAccessPermissionRequest::Start() {
+ // Check test prefs to see if we should short circuit. We want to do this
+ // before checking the cached value so we can have pref changes take effect
+ // without refreshing the page.
+ MediaKeySystemAccessPermissionRequest::PromptResult promptResult =
+ CheckPromptPrefs();
+ if (promptResult ==
+ MediaKeySystemAccessPermissionRequest::PromptResult::Granted) {
+ return Allow(JS::UndefinedHandleValue);
+ }
+ if (promptResult ==
+ MediaKeySystemAccessPermissionRequest::PromptResult::Denied) {
+ return Cancel();
+ }
+
+ return nsContentPermissionUtils::AskPermission(this, mWindow);
+}
+
+NS_IMETHODIMP
+MediaKeySystemAccessPermissionRequest::Allow(JS::Handle<JS::Value> aChoices) {
+ AssertIsOnMainThread();
+ mPromiseHolder.ResolveIfExists(true, __func__);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+MediaKeySystemAccessPermissionRequest::Cancel() {
+ AssertIsOnMainThread();
+ mPromiseHolder.RejectIfExists(false, __func__);
+ return NS_OK;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/eme/MediaKeySystemAccessPermissionRequest.h b/dom/media/eme/MediaKeySystemAccessPermissionRequest.h
new file mode 100644
index 0000000000..088cf50ab6
--- /dev/null
+++ b/dom/media/eme/MediaKeySystemAccessPermissionRequest.h
@@ -0,0 +1,74 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_EME_MEDIAKEYSYSTEMACCESSPERMISSIONREQUEST_H_
+#define DOM_MEDIA_EME_MEDIAKEYSYSTEMACCESSPERMISSIONREQUEST_H_
+
+#include "mozilla/MozPromise.h"
+#include "nsContentPermissionHelper.h"
+
+class nsGlobalWindowInner;
+
+namespace mozilla::dom {
+
+/**
+ * This class encapsulates a permission request to allow media key system
+ * access. The intention is not for this class to be used in all cases of EME,
+ * but only when we need to seek explicit approval from an application using
+ * Gecko, such as an application embedding via GeckoView.
+ *
+ * media.eme.require-app-approval should be used to gate this functionality in
+ * gecko code, and is also used as the testing pref for
+ * ContentPermissionRequestBase. I.e. CheckPromptPrefs() will respond to having
+ * `media.eme.require-app-approval.prompt.testing` and
+ * `media.eme.require-app-approval.prompt.testing.allow` being set to true or
+ * false and will return an appropriate value to allow for test code to short
+ * circuit showing a prompt. Note that the code using this class needs to call
+ * CheckPromptPrefs and implement test specific logic, it is *not* handled by
+ * this class or ContentPermissionRequestBase.
+ *
+ * Expects to be used on main thread as ContentPermissionRequestBase uses
+ * PContentPermissionRequest which is managed by PContent which is main thread
+ * to main thread communication.
+ */
+class MediaKeySystemAccessPermissionRequest
+ : public ContentPermissionRequestBase {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(
+ MediaKeySystemAccessPermissionRequest, ContentPermissionRequestBase)
+
+ using RequestPromise = MozPromise<bool, bool, true /* IsExclusive*/>;
+
+ // Create a MediaKeySystemAccessPermissionRequest.
+ // @param aWindow The window associated with this request.
+ static already_AddRefed<MediaKeySystemAccessPermissionRequest> Create(
+ nsPIDOMWindowInner* aWindow);
+
+ // Returns a promise that will be resolved if this request is allowed or
+ // rejected in the case the request is denied. If allowed the promise
+ // will resolve with true, otherwise it will resolve with false.
+ already_AddRefed<RequestPromise> GetPromise();
+
+ // Helper function that triggers the request. This function will check
+ // prefs and cancel or allow the request if the appropriate prefs are set,
+ // otherwise it will fire the request to the associated window.
+ nsresult Start();
+
+ // nsIContentPermissionRequest methods
+ NS_IMETHOD Cancel(void) override;
+ NS_IMETHOD Allow(JS::Handle<JS::Value> choices) override;
+
+ private:
+ explicit MediaKeySystemAccessPermissionRequest(nsGlobalWindowInner* aWindow);
+ ~MediaKeySystemAccessPermissionRequest();
+
+ MozPromiseHolder<RequestPromise> mPromiseHolder;
+};
+
+} // namespace mozilla::dom
+
+#endif // DOM_MEDIA_EME_MEDIAKEYSYSTEMACCESSPERMISSIONREQUEST_H_
diff --git a/dom/media/eme/MediaKeys.cpp b/dom/media/eme/MediaKeys.cpp
new file mode 100644
index 0000000000..0b256650a2
--- /dev/null
+++ b/dom/media/eme/MediaKeys.cpp
@@ -0,0 +1,844 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/MediaKeys.h"
+
+#include "ChromiumCDMProxy.h"
+#include "GMPCrashHelper.h"
+#include "mozilla/EMEUtils.h"
+#include "mozilla/JSONStringWriteFuncs.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/dom/DOMException.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/HTMLMediaElement.h"
+#include "mozilla/dom/MediaKeyError.h"
+#include "mozilla/dom/MediaKeyMessageEvent.h"
+#include "mozilla/dom/MediaKeySession.h"
+#include "mozilla/dom/MediaKeyStatusMap.h"
+#include "mozilla/dom/MediaKeySystemAccess.h"
+#include "mozilla/dom/MediaKeysBinding.h"
+#include "mozilla/dom/UnionTypes.h"
+#include "mozilla/dom/WindowContext.h"
+#include "mozilla/dom/WindowGlobalChild.h"
+#include "nsContentCID.h"
+#include "nsContentTypeParser.h"
+#include "nsContentUtils.h"
+#include "nsIScriptObjectPrincipal.h"
+#include "nsPrintfCString.h"
+#include "nsServiceManagerUtils.h"
+
+#ifdef MOZ_WIDGET_ANDROID
+# include "mozilla/MediaDrmCDMProxy.h"
+#endif
+#ifdef XP_WIN
+# include "mozilla/WindowsVersion.h"
+#endif
+#ifdef MOZ_WMF_CDM
+# include "mozilla/WMFCDMProxy.h"
+#endif
+
+namespace mozilla::dom {
+
+// We don't use NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE because we need to
+// disconnect our MediaKeys instances from the inner window (mparent) before
+// we unlink it.
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(MediaKeys)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(MediaKeys)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mElement)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParent)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mKeySessions)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPromises)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingSessions)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(MediaKeys)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mElement)
+ tmp->DisconnectInnerWindow();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mKeySessions)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mPromises)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingSessions)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_PTR
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaKeys)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaKeys)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaKeys)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+ NS_INTERFACE_MAP_ENTRY(nsIObserver)
+NS_INTERFACE_MAP_END
+
+MediaKeys::MediaKeys(nsPIDOMWindowInner* aParent, const nsAString& aKeySystem,
+ const MediaKeySystemConfiguration& aConfig)
+ : mParent(aParent),
+ mKeySystem(aKeySystem),
+ mCreatePromiseId(0),
+ mConfig(aConfig) {
+ EME_LOG("MediaKeys[%p] constructed keySystem=%s", this,
+ NS_ConvertUTF16toUTF8(mKeySystem).get());
+}
+
+MediaKeys::~MediaKeys() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ DisconnectInnerWindow();
+ Shutdown();
+ EME_LOG("MediaKeys[%p] destroyed", this);
+}
+
+NS_IMETHODIMP MediaKeys::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!strcmp(aTopic, kMediaKeysResponseTopic),
+ "Should only listen for responses to MediaKey requests");
+ EME_LOG("MediaKeys[%p] observing message with aTopic=%s aData=%s", this,
+ aTopic, NS_ConvertUTF16toUTF8(aData).get());
+ if (!strcmp(aTopic, kMediaKeysResponseTopic)) {
+ if (!mProxy) {
+ // This may happen if we're notified during shutdown or startup. If this
+ // is happening outside of those scenarios there's a bug.
+ EME_LOG(
+ "MediaKeys[%p] can't notify CDM of observed message as mProxy is "
+ "unset",
+ this);
+ return NS_OK;
+ }
+
+ if (u"capture-possible"_ns.Equals(aData)) {
+ mProxy->NotifyOutputProtectionStatus(
+ CDMProxy::OutputProtectionCheckStatus::CheckSuccessful,
+ CDMProxy::OutputProtectionCaptureStatus::CapturePossilbe);
+ } else if (u"capture-not-possible"_ns.Equals(aData)) {
+ mProxy->NotifyOutputProtectionStatus(
+ CDMProxy::OutputProtectionCheckStatus::CheckSuccessful,
+ CDMProxy::OutputProtectionCaptureStatus::CaptureNotPossible);
+ } else {
+ MOZ_ASSERT_UNREACHABLE("No code paths should lead to the failure case");
+ // This should be unreachable, but gracefully handle in case.
+ mProxy->NotifyOutputProtectionStatus(
+ CDMProxy::OutputProtectionCheckStatus::CheckFailed,
+ CDMProxy::OutputProtectionCaptureStatus::Unused);
+ }
+ }
+ return NS_OK;
+}
+
+void MediaKeys::ConnectInnerWindow() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsPIDOMWindowInner> innerWindowParent = GetParentObject();
+ MOZ_ASSERT(innerWindowParent,
+ "We should only be connecting when we have an inner window!");
+ innerWindowParent->AddMediaKeysInstance(this);
+}
+
+void MediaKeys::DisconnectInnerWindow() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!GetParentObject()) {
+ // We don't have a parent. We've been cycle collected, or the window
+ // already notified us of its destruction and we cleared the ref.
+ return;
+ }
+
+ GetParentObject()->RemoveMediaKeysInstance(this);
+}
+
+void MediaKeys::OnInnerWindowDestroy() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ EME_LOG("MediaKeys[%p] OnInnerWindowDestroy()", this);
+
+ // The InnerWindow should clear its reference to this object after this call,
+ // so we don't need to explicitly call DisconnectInnerWindow before nulling.
+ mParent = nullptr;
+
+ // Don't call shutdown directly because (at time of writing) mProxy can
+ // spin the event loop when it's shutdown. This can change the world state
+ // in the middle of window destruction, which we do not want.
+ GetMainThreadSerialEventTarget()->Dispatch(
+ NewRunnableMethod("MediaKeys::Shutdown", this, &MediaKeys::Shutdown));
+}
+
+void MediaKeys::Terminated() {
+ EME_LOG("MediaKeys[%p] CDM crashed unexpectedly", this);
+
+ KeySessionHashMap keySessions;
+ // Remove entries during iteration will screw it. Make a copy first.
+ for (const RefPtr<MediaKeySession>& session : mKeySessions.Values()) {
+ // XXX Could the RefPtr still be moved here?
+ keySessions.InsertOrUpdate(session->GetSessionId(), RefPtr{session});
+ }
+ for (const RefPtr<MediaKeySession>& session : keySessions.Values()) {
+ session->OnClosed();
+ }
+ keySessions.Clear();
+ MOZ_ASSERT(mKeySessions.Count() == 0);
+
+ // Notify the element about that CDM has terminated.
+ if (mElement) {
+ mElement->DecodeError(NS_ERROR_DOM_MEDIA_CDM_ERR);
+ }
+
+ Shutdown();
+}
+
+void MediaKeys::Shutdown() {
+ EME_LOG("MediaKeys[%p]::Shutdown()", this);
+ if (mProxy) {
+ mProxy->Shutdown();
+ mProxy = nullptr;
+ }
+
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService && mObserverAdded) {
+ observerService->RemoveObserver(this, kMediaKeysResponseTopic);
+ }
+
+ // Hold a self reference to keep us alive after we clear the self reference
+ // for each promise. This ensures we stay alive until we're done shutting
+ // down.
+ RefPtr<MediaKeys> selfReference = this;
+
+ for (const RefPtr<dom::DetailedPromise>& promise : mPromises.Values()) {
+ promise->MaybeRejectWithInvalidStateError(
+ "Promise still outstanding at MediaKeys shutdown");
+ Release();
+ }
+ mPromises.Clear();
+}
+
+nsPIDOMWindowInner* MediaKeys::GetParentObject() const { return mParent; }
+
+JSObject* MediaKeys::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return MediaKeys_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void MediaKeys::GetKeySystem(nsString& aOutKeySystem) const {
+ aOutKeySystem.Assign(mKeySystem);
+}
+
+already_AddRefed<DetailedPromise> MediaKeys::SetServerCertificate(
+ const ArrayBufferViewOrArrayBuffer& aCert, ErrorResult& aRv) {
+ RefPtr<DetailedPromise> promise(
+ MakePromise(aRv, "MediaKeys.setServerCertificate"_ns));
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ if (!mProxy) {
+ NS_WARNING("Tried to use a MediaKeys without a CDM");
+ promise->MaybeRejectWithInvalidStateError(
+ "Null CDM in MediaKeys.setServerCertificate()");
+ return promise.forget();
+ }
+
+ nsTArray<uint8_t> data;
+ CopyArrayBufferViewOrArrayBufferData(aCert, data);
+ if (data.IsEmpty()) {
+ promise->MaybeRejectWithTypeError(
+ "Empty certificate passed to MediaKeys.setServerCertificate()");
+ return promise.forget();
+ }
+
+ mProxy->SetServerCertificate(StorePromise(promise), data);
+ return promise.forget();
+}
+
+already_AddRefed<DetailedPromise> MediaKeys::MakePromise(
+ ErrorResult& aRv, const nsACString& aName) {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(GetParentObject());
+ if (!global) {
+ NS_WARNING("Passed non-global to MediaKeys ctor!");
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+ return DetailedPromise::Create(global, aRv, aName);
+}
+
+PromiseId MediaKeys::StorePromise(DetailedPromise* aPromise) {
+ static uint32_t sEMEPromiseCount = 1;
+ MOZ_ASSERT(aPromise);
+ uint32_t id = sEMEPromiseCount++;
+
+ EME_LOG("MediaKeys[%p]::StorePromise() id=%" PRIu32, this, id);
+
+ // Keep MediaKeys alive for the lifetime of its promises. Any still-pending
+ // promises are rejected in Shutdown().
+ EME_LOG("MediaKeys[%p]::StorePromise() calling AddRef()", this);
+ AddRef();
+
+#ifdef DEBUG
+ // We should not have already stored this promise!
+ for (const RefPtr<dom::DetailedPromise>& promise : mPromises.Values()) {
+ MOZ_ASSERT(promise != aPromise);
+ }
+#endif
+
+ mPromises.InsertOrUpdate(id, RefPtr{aPromise});
+ return id;
+}
+
+void MediaKeys::ConnectPendingPromiseIdWithToken(PromiseId aId,
+ uint32_t aToken) {
+ // Should only be called from MediaKeySession::GenerateRequest.
+ mPromiseIdToken.InsertOrUpdate(aId, aToken);
+ EME_LOG(
+ "MediaKeys[%p]::ConnectPendingPromiseIdWithToken() id=%u => token(%u)",
+ this, aId, aToken);
+}
+
+already_AddRefed<DetailedPromise> MediaKeys::RetrievePromise(PromiseId aId) {
+ EME_LOG("MediaKeys[%p]::RetrievePromise(aId=%" PRIu32 ")", this, aId);
+ if (!mPromises.Contains(aId)) {
+ EME_LOG("MediaKeys[%p]::RetrievePromise(aId=%" PRIu32
+ ") tried to retrieve non-existent promise!",
+ this, aId);
+ NS_WARNING(nsPrintfCString(
+ "Tried to retrieve a non-existent promise id=%" PRIu32, aId)
+ .get());
+ return nullptr;
+ }
+ RefPtr<DetailedPromise> promise;
+ mPromises.Remove(aId, getter_AddRefs(promise));
+ EME_LOG("MediaKeys[%p]::RetrievePromise(aId=%" PRIu32 ") calling Release()",
+ this, aId);
+ Release();
+ return promise.forget();
+}
+
+void MediaKeys::RejectPromise(PromiseId aId, ErrorResult&& aException,
+ const nsCString& aReason) {
+ uint32_t errorCodeAsInt = aException.ErrorCodeAsInt();
+ EME_LOG("MediaKeys[%p]::RejectPromise(%" PRIu32 ", 0x%" PRIx32 ")", this, aId,
+ errorCodeAsInt);
+
+ RefPtr<DetailedPromise> promise(RetrievePromise(aId));
+ if (!promise) {
+ EME_LOG("MediaKeys[%p]::RejectPromise(%" PRIu32 ", 0x%" PRIx32
+ ") couldn't retrieve promise! Bailing!",
+ this, aId, errorCodeAsInt);
+ return;
+ }
+
+ // This promise could be a createSession or loadSession promise,
+ // so we might have a pending session waiting to be resolved into
+ // the promise on success. We've been directed to reject to promise,
+ // so we can throw away the corresponding session object.
+ uint32_t token = 0;
+ if (mPromiseIdToken.Get(aId, &token)) {
+ MOZ_ASSERT(mPendingSessions.Contains(token));
+ mPendingSessions.Remove(token);
+ mPromiseIdToken.Remove(aId);
+ }
+
+ MOZ_ASSERT(aException.Failed());
+ promise->MaybeReject(std::move(aException), aReason);
+
+ if (mCreatePromiseId == aId) {
+ // Note: This will probably destroy the MediaKeys object!
+ EME_LOG("MediaKeys[%p]::RejectPromise(%" PRIu32 ", 0x%" PRIx32
+ ") calling Release()",
+ this, aId, errorCodeAsInt);
+ Release();
+ }
+}
+
+void MediaKeys::OnSessionIdReady(MediaKeySession* aSession) {
+ if (!aSession) {
+ NS_WARNING("Invalid MediaKeySession passed to OnSessionIdReady()");
+ return;
+ }
+ if (mKeySessions.Contains(aSession->GetSessionId())) {
+ NS_WARNING("MediaKeySession's made ready multiple times!");
+ return;
+ }
+ if (mPendingSessions.Contains(aSession->Token())) {
+ NS_WARNING(
+ "MediaKeySession made ready when it wasn't waiting to be ready!");
+ return;
+ }
+ if (aSession->GetSessionId().IsEmpty()) {
+ NS_WARNING(
+ "MediaKeySession with invalid sessionId passed to OnSessionIdReady()");
+ return;
+ }
+ mKeySessions.InsertOrUpdate(aSession->GetSessionId(), RefPtr{aSession});
+}
+
+void MediaKeys::ResolvePromise(PromiseId aId) {
+ EME_LOG("MediaKeys[%p]::ResolvePromise(%" PRIu32 ")", this, aId);
+
+ RefPtr<DetailedPromise> promise(RetrievePromise(aId));
+ MOZ_ASSERT(!mPromises.Contains(aId));
+ if (!promise) {
+ return;
+ }
+
+ uint32_t token = 0;
+ if (!mPromiseIdToken.Get(aId, &token)) {
+ promise->MaybeResolveWithUndefined();
+ return;
+ } else if (!mPendingSessions.Contains(token)) {
+ // Pending session for CreateSession() should be removed when sessionId
+ // is ready.
+ promise->MaybeResolveWithUndefined();
+ mPromiseIdToken.Remove(aId);
+ return;
+ }
+ mPromiseIdToken.Remove(aId);
+
+ // We should only resolve LoadSession calls via this path,
+ // not CreateSession() promises.
+ RefPtr<MediaKeySession> session;
+ mPendingSessions.Remove(token, getter_AddRefs(session));
+ if (!session || session->GetSessionId().IsEmpty()) {
+ NS_WARNING("Received activation for non-existent session!");
+ promise->MaybeRejectWithInvalidAccessError(
+ "CDM LoadSession() returned a different session ID than requested");
+ return;
+ }
+ mKeySessions.InsertOrUpdate(session->GetSessionId(), RefPtr{session});
+ promise->MaybeResolve(session);
+}
+
+class MediaKeysGMPCrashHelper : public GMPCrashHelper {
+ public:
+ explicit MediaKeysGMPCrashHelper(MediaKeys* aMediaKeys)
+ : mMediaKeys(aMediaKeys) {
+ MOZ_ASSERT(NS_IsMainThread()); // WeakPtr isn't thread safe.
+ }
+ already_AddRefed<nsPIDOMWindowInner> GetPluginCrashedEventTarget() override {
+ MOZ_ASSERT(NS_IsMainThread()); // WeakPtr isn't thread safe.
+ EME_LOG("MediaKeysGMPCrashHelper::GetPluginCrashedEventTarget()");
+ return (mMediaKeys && mMediaKeys->GetParentObject())
+ ? do_AddRef(mMediaKeys->GetParentObject())
+ : nullptr;
+ }
+
+ private:
+ WeakPtr<MediaKeys> mMediaKeys;
+};
+
+already_AddRefed<CDMProxy> MediaKeys::CreateCDMProxy() {
+ EME_LOG("MediaKeys[%p]::CreateCDMProxy()", this);
+ RefPtr<CDMProxy> proxy;
+#ifdef MOZ_WIDGET_ANDROID
+ if (IsWidevineKeySystem(mKeySystem)) {
+ proxy = new MediaDrmCDMProxy(
+ this, mKeySystem,
+ mConfig.mDistinctiveIdentifier == MediaKeysRequirement::Required,
+ mConfig.mPersistentState == MediaKeysRequirement::Required);
+ } else
+#endif
+#ifdef MOZ_WMF_CDM
+ if (IsPlayReadyKeySystemAndSupported(mKeySystem)) {
+ proxy = new WMFCDMProxy(this, mKeySystem, mConfig);
+ } else
+#endif
+ {
+ proxy = new ChromiumCDMProxy(
+ this, mKeySystem, new MediaKeysGMPCrashHelper(this),
+ mConfig.mDistinctiveIdentifier == MediaKeysRequirement::Required,
+ mConfig.mPersistentState == MediaKeysRequirement::Required);
+ }
+ return proxy.forget();
+}
+
+already_AddRefed<DetailedPromise> MediaKeys::Init(ErrorResult& aRv) {
+ EME_LOG("MediaKeys[%p]::Init()", this);
+ RefPtr<DetailedPromise> promise(MakePromise(aRv, "MediaKeys::Init()"_ns));
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ // Determine principal (at creation time) of the MediaKeys object.
+ nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(GetParentObject());
+ if (!sop) {
+ promise->MaybeRejectWithInvalidStateError(
+ "Couldn't get script principal in MediaKeys::Init");
+ return promise.forget();
+ }
+ mPrincipal = sop->GetPrincipal();
+
+ // Begin figuring out the top level principal.
+ nsCOMPtr<nsPIDOMWindowInner> window = GetParentObject();
+
+ // If we're in a top level document, getting the top level principal is easy.
+ // However, we're not in a top level doc this becomes more complicated. If
+ // we're not top level we need to get the top level principal, this can be
+ // done by reading the principal of the load info, which we can get of a
+ // document's channel.
+ //
+ // There is an edge case we need to watch out for here where this code can be
+ // run in an about:blank document before it has done its async load. In this
+ // case the document will not yet have a load info. We address this below by
+ // walking up a level in the window context chain. See
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1675360
+ // for more info.
+ Document* document = window->GetExtantDoc();
+ if (!document) {
+ NS_WARNING("Failed to get document when creating MediaKeys");
+ promise->MaybeRejectWithInvalidStateError(
+ "Couldn't get document in MediaKeys::Init");
+ return promise.forget();
+ }
+
+ WindowGlobalChild* windowGlobalChild = window->GetWindowGlobalChild();
+ if (!windowGlobalChild) {
+ NS_WARNING("Failed to get window global child when creating MediaKeys");
+ promise->MaybeRejectWithInvalidStateError(
+ "Couldn't get window global child in MediaKeys::Init");
+ return promise.forget();
+ }
+
+ if (windowGlobalChild->SameOriginWithTop()) {
+ // We're in the same origin as the top window context, so our principal
+ // is also the top principal.
+ mTopLevelPrincipal = mPrincipal;
+ } else {
+ // We have a different origin than the top doc, try and find the top level
+ // principal by looking it up via load info, which we read off a channel.
+ nsIChannel* channel = document->GetChannel();
+
+ WindowContext* windowContext = document->GetWindowContext();
+ if (!windowContext) {
+ NS_WARNING("Failed to get window context when creating MediaKeys");
+ promise->MaybeRejectWithInvalidStateError(
+ "Couldn't get window context in MediaKeys::Init");
+ return promise.forget();
+ }
+ while (!channel) {
+ // We don't have a channel, this can happen if we're in an about:blank
+ // page that hasn't yet had its async load performed. Try and get
+ // the channel from our parent doc. We should be able to do this because
+ // an about:blank is considered the same origin as its parent. We do this
+ // recursively to cover pages do silly things like nesting blank iframes
+ // and not waiting for loads.
+
+ // Move our window context up a level.
+ windowContext = windowContext->GetParentWindowContext();
+ if (!windowContext || !windowContext->GetExtantDoc()) {
+ NS_WARNING(
+ "Failed to get parent window context's document when creating "
+ "MediaKeys");
+ promise->MaybeRejectWithInvalidStateError(
+ "Couldn't get parent window context's document in "
+ "MediaKeys::Init (likely due to an nested about about:blank frame "
+ "that hasn't loaded yet)");
+ return promise.forget();
+ }
+
+ Document* parentDoc = windowContext->GetExtantDoc();
+ channel = parentDoc->GetChannel();
+ }
+
+ MOZ_RELEASE_ASSERT(
+ channel, "Should either have a channel or should have returned by now");
+
+ nsCOMPtr<nsILoadInfo> loadInfo = channel->LoadInfo();
+ MOZ_RELEASE_ASSERT(loadInfo, "Channels should always have LoadInfo");
+ mTopLevelPrincipal = loadInfo->GetTopLevelPrincipal();
+ if (!mTopLevelPrincipal) {
+ NS_WARNING("Failed to get top level principal when creating MediaKeys");
+ promise->MaybeRejectWithInvalidStateError(
+ "Couldn't get top level principal in MediaKeys::Init");
+ return promise.forget();
+ }
+ }
+
+ // We should have figured out our top level principal.
+ if (!mPrincipal || !mTopLevelPrincipal) {
+ NS_WARNING("Failed to get principals when creating MediaKeys");
+ promise->MaybeRejectWithInvalidStateError(
+ "Couldn't get principal(s) in MediaKeys::Init");
+ return promise.forget();
+ }
+
+ nsAutoCString origin;
+ nsresult rv = mPrincipal->GetOrigin(origin);
+ if (NS_FAILED(rv)) {
+ promise->MaybeRejectWithInvalidStateError(
+ "Couldn't get principal origin string in MediaKeys::Init");
+ return promise.forget();
+ }
+ nsAutoCString topLevelOrigin;
+ rv = mTopLevelPrincipal->GetOrigin(topLevelOrigin);
+ if (NS_FAILED(rv)) {
+ promise->MaybeRejectWithInvalidStateError(
+ "Couldn't get top-level principal origin string in MediaKeys::Init");
+ return promise.forget();
+ }
+
+ EME_LOG("MediaKeys[%p]::Create() (%s, %s)", this, origin.get(),
+ topLevelOrigin.get());
+
+ mProxy = CreateCDMProxy();
+
+ // The CDMProxy's initialization is asynchronous. The MediaKeys is
+ // refcounted, and its instance is returned to JS by promise once
+ // it's been initialized. No external refs exist to the MediaKeys while
+ // we're waiting for the promise to be resolved, so we must hold a
+ // reference to the new MediaKeys object until it's been created,
+ // or its creation has failed. Store the id of the promise returned
+ // here, and hold a self-reference until that promise is resolved or
+ // rejected.
+ MOZ_ASSERT(!mCreatePromiseId, "Should only be created once!");
+ mCreatePromiseId = StorePromise(promise);
+ EME_LOG("MediaKeys[%p]::Init() calling AddRef()", this);
+ AddRef();
+ mProxy->Init(mCreatePromiseId, NS_ConvertUTF8toUTF16(origin),
+ NS_ConvertUTF8toUTF16(topLevelOrigin),
+ KeySystemToProxyName(mKeySystem));
+
+ ConnectInnerWindow();
+
+ return promise.forget();
+}
+
+void MediaKeys::OnCDMCreated(PromiseId aId, const uint32_t aPluginId) {
+ EME_LOG("MediaKeys[%p]::OnCDMCreated(aId=%" PRIu32 ", aPluginId=%" PRIu32 ")",
+ this, aId, aPluginId);
+ RefPtr<DetailedPromise> promise(RetrievePromise(aId));
+ if (!promise) {
+ return;
+ }
+ RefPtr<MediaKeys> keys(this);
+
+ promise->MaybeResolve(keys);
+ if (mCreatePromiseId == aId) {
+ EME_LOG("MediaKeys[%p]::OnCDMCreated(aId=%" PRIu32 ", aPluginId=%" PRIu32
+ ") calling Release()",
+ this, aId, aPluginId);
+ Release();
+ }
+
+ MediaKeySystemAccess::NotifyObservers(mParent, mKeySystem,
+ MediaKeySystemStatus::Cdm_created);
+}
+
+static bool IsSessionTypeSupported(const MediaKeySessionType aSessionType,
+ const MediaKeySystemConfiguration& aConfig) {
+ if (aSessionType == MediaKeySessionType::Temporary) {
+ // Temporary is always supported.
+ return true;
+ }
+ if (!aConfig.mSessionTypes.WasPassed()) {
+ // No other session types supported.
+ return false;
+ }
+ return aConfig.mSessionTypes.Value().Contains(ToString(aSessionType));
+}
+
+already_AddRefed<MediaKeySession> MediaKeys::CreateSession(
+ MediaKeySessionType aSessionType, ErrorResult& aRv) {
+ EME_LOG("MediaKeys[%p]::CreateSession(aSessionType=%" PRIu8 ")", this,
+ static_cast<uint8_t>(aSessionType));
+ if (!IsSessionTypeSupported(aSessionType, mConfig)) {
+ EME_LOG("MediaKeys[%p]::CreateSession() failed, unsupported session type",
+ this);
+ aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+ return nullptr;
+ }
+
+ if (!mProxy) {
+ NS_WARNING("Tried to use a MediaKeys which lost its CDM");
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ EME_LOG("MediaKeys[%p] Creating session", this);
+
+ RefPtr<MediaKeySession> session = new MediaKeySession(
+ GetParentObject(), this, mKeySystem, aSessionType, aRv);
+
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+ DDLINKCHILD("session", session.get());
+
+ // Add session to the set of sessions awaiting their sessionId being ready.
+ EME_LOG("MediaKeys[%p]::CreateSession(aSessionType=%" PRIu8
+ ") putting session with token=%" PRIu32 " into mPendingSessions",
+ this, static_cast<uint8_t>(aSessionType), session->Token());
+ mPendingSessions.InsertOrUpdate(session->Token(), RefPtr{session});
+
+ return session.forget();
+}
+
+void MediaKeys::OnSessionLoaded(PromiseId aId, bool aSuccess) {
+ EME_LOG("MediaKeys[%p]::OnSessionLoaded() resolve promise id=%" PRIu32, this,
+ aId);
+
+ ResolvePromiseWithResult(aId, aSuccess);
+}
+
+void MediaKeys::OnSessionClosed(MediaKeySession* aSession) {
+ nsAutoString id;
+ aSession->GetSessionId(id);
+ mKeySessions.Remove(id);
+}
+
+already_AddRefed<MediaKeySession> MediaKeys::GetSession(
+ const nsAString& aSessionId) {
+ RefPtr<MediaKeySession> session;
+ mKeySessions.Get(aSessionId, getter_AddRefs(session));
+ return session.forget();
+}
+
+already_AddRefed<MediaKeySession> MediaKeys::GetPendingSession(
+ uint32_t aToken) {
+ EME_LOG("MediaKeys[%p]::GetPendingSession(aToken=%" PRIu32 ")", this, aToken);
+ RefPtr<MediaKeySession> session;
+ mPendingSessions.Get(aToken, getter_AddRefs(session));
+ mPendingSessions.Remove(aToken);
+ return session.forget();
+}
+
+bool MediaKeys::IsBoundToMediaElement() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mElement != nullptr;
+}
+
+nsresult MediaKeys::Bind(HTMLMediaElement* aElement) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (IsBoundToMediaElement()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mElement = aElement;
+
+ return NS_OK;
+}
+
+void MediaKeys::Unbind() {
+ MOZ_ASSERT(NS_IsMainThread());
+ mElement = nullptr;
+}
+
+void MediaKeys::CheckIsElementCapturePossible() {
+ MOZ_ASSERT(NS_IsMainThread());
+ EME_LOG("MediaKeys[%p]::IsElementCapturePossible()", this);
+ // Note, HTMLMediaElement prevents capture of its content via Capture APIs
+ // on the element if it has a media keys attached (see bug 1071482). So we
+ // don't need to check those cases here (they are covered by tests).
+
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+
+ if (!observerService) {
+ // This can happen if we're in shutdown which means we may be going away
+ // soon anyway, but respond saying capture is possible since we can't
+ // forward the check further.
+ if (mProxy) {
+ mProxy->NotifyOutputProtectionStatus(
+ CDMProxy::OutputProtectionCheckStatus::CheckFailed,
+ CDMProxy::OutputProtectionCaptureStatus::Unused);
+ }
+ return;
+ }
+ if (!mObserverAdded) {
+ nsresult rv =
+ observerService->AddObserver(this, kMediaKeysResponseTopic, false);
+ if (NS_FAILED(rv)) {
+ if (mProxy) {
+ mProxy->NotifyOutputProtectionStatus(
+ CDMProxy::OutputProtectionCheckStatus::CheckFailed,
+ CDMProxy::OutputProtectionCaptureStatus::Unused);
+ }
+ return;
+ }
+ mObserverAdded = true;
+ }
+
+ if (mCaptureCheckRequestJson.IsEmpty()) {
+ // Lazily populate the JSON the first time we need it.
+ JSONStringWriteFunc<nsAutoCString> json;
+ JSONWriter jw{json};
+ jw.Start();
+ jw.StringProperty("status", "is-capture-possible");
+ jw.StringProperty("keySystem", NS_ConvertUTF16toUTF8(mKeySystem));
+ jw.End();
+ mCaptureCheckRequestJson = NS_ConvertUTF8toUTF16(json.StringCRef());
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(!mCaptureCheckRequestJson.IsEmpty());
+ observerService->NotifyObservers(mParent.get(), kMediaKeysRequestTopic,
+ mCaptureCheckRequestJson.get());
+}
+
+void MediaKeys::GetSessionsInfo(nsString& sessionsInfo) {
+ for (const auto& keySession : mKeySessions.Values()) {
+ nsString sessionID;
+ keySession->GetSessionId(sessionID);
+ sessionsInfo.AppendLiteral("(sid=");
+ sessionsInfo.Append(sessionID);
+ MediaKeyStatusMap* keyStatusMap = keySession->KeyStatuses();
+ for (uint32_t i = 0; i < keyStatusMap->GetIterableLength(); i++) {
+ nsString keyID = keyStatusMap->GetKeyIDAsHexString(i);
+ sessionsInfo.AppendLiteral("(kid=");
+ sessionsInfo.Append(keyID);
+ sessionsInfo.AppendLiteral(" status=");
+ sessionsInfo.AppendASCII(
+ MediaKeyStatusValues::GetString(keyStatusMap->GetValueAtIndex(i)));
+ sessionsInfo.AppendLiteral(")");
+ }
+ sessionsInfo.AppendLiteral(")");
+ }
+}
+
+already_AddRefed<Promise> MediaKeys::GetStatusForPolicy(
+ const MediaKeysPolicy& aPolicy, ErrorResult& aRv) {
+ RefPtr<DetailedPromise> promise(
+ MakePromise(aRv, "MediaKeys::GetStatusForPolicy()"_ns));
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ // Currently, only widevine CDM supports for this API.
+ if (!IsWidevineKeySystem(mKeySystem)) {
+ EME_LOG(
+ "MediaKeys[%p]::GetStatusForPolicy() HDCP policy check on unsupported "
+ "keysystem ",
+ this);
+ NS_WARNING("Tried to query without a CDM");
+ promise->MaybeRejectWithNotSupportedError(
+ "HDCP policy check on unsupported keysystem");
+ return promise.forget();
+ }
+
+ if (!mProxy) {
+ NS_WARNING("Tried to use a MediaKeys without a CDM");
+ promise->MaybeRejectWithInvalidStateError(
+ "Null CDM in MediaKeys.GetStatusForPolicy()");
+ return promise.forget();
+ }
+
+ EME_LOG("GetStatusForPolicy minHdcpVersion = %s.",
+ NS_ConvertUTF16toUTF8(aPolicy.mMinHdcpVersion).get());
+ mProxy->GetStatusForPolicy(StorePromise(promise), aPolicy.mMinHdcpVersion);
+ return promise.forget();
+}
+
+void MediaKeys::ResolvePromiseWithKeyStatus(PromiseId aId,
+ MediaKeyStatus aMediaKeyStatus) {
+ RefPtr<DetailedPromise> promise(RetrievePromise(aId));
+ if (!promise) {
+ return;
+ }
+ RefPtr<MediaKeys> keys(this);
+ EME_LOG(
+ "MediaKeys[%p]::ResolvePromiseWithKeyStatus() resolve promise id=%" PRIu32
+ ", keystatus=%" PRIu8,
+ this, aId, static_cast<uint8_t>(aMediaKeyStatus));
+ promise->MaybeResolve(aMediaKeyStatus);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/eme/MediaKeys.h b/dom/media/eme/MediaKeys.h
new file mode 100644
index 0000000000..5a44b3c227
--- /dev/null
+++ b/dom/media/eme/MediaKeys.h
@@ -0,0 +1,236 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_mediakeys_h__
+#define mozilla_dom_mediakeys_h__
+
+#include "DecoderDoctorLogger.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/DetailedPromise.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/WeakPtr.h"
+#include "mozilla/dom/MediaKeyStatusMapBinding.h" // For MediaKeyStatus
+#include "mozilla/dom/MediaKeySystemAccessBinding.h"
+#include "mozilla/dom/MediaKeysBinding.h"
+#include "mozilla/dom/Promise.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIObserver.h"
+#include "nsRefPtrHashtable.h"
+#include "nsTHashMap.h"
+#include "nsWrapperCache.h"
+
+namespace mozilla {
+
+class CDMProxy;
+
+namespace dom {
+class MediaKeys;
+} // namespace dom
+DDLoggedTypeName(dom::MediaKeys);
+
+namespace dom {
+
+class ArrayBufferViewOrArrayBuffer;
+class MediaKeySession;
+struct MediaKeysPolicy;
+class HTMLMediaElement;
+
+typedef nsRefPtrHashtable<nsStringHashKey, MediaKeySession> KeySessionHashMap;
+typedef nsRefPtrHashtable<nsUint32HashKey, dom::DetailedPromise> PromiseHashMap;
+typedef nsRefPtrHashtable<nsUint32HashKey, MediaKeySession>
+ PendingKeySessionsHashMap;
+typedef nsTHashMap<nsUint32HashKey, uint32_t> PendingPromiseIdTokenHashMap;
+typedef uint32_t PromiseId;
+
+// This class is used on the main thread only.
+// Note: its addref/release is not (and can't be) thread safe!
+class MediaKeys final : public nsIObserver,
+ public nsWrapperCache,
+ public SupportsWeakPtr,
+ public DecoderDoctorLifeLogger<MediaKeys> {
+ ~MediaKeys();
+
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(MediaKeys)
+
+ NS_DECL_NSIOBSERVER
+
+ MediaKeys(nsPIDOMWindowInner* aParentWindow, const nsAString& aKeySystem,
+ const MediaKeySystemConfiguration& aConfig);
+
+ already_AddRefed<DetailedPromise> Init(ErrorResult& aRv);
+
+ nsPIDOMWindowInner* GetParentObject() const;
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ nsresult Bind(HTMLMediaElement* aElement);
+ void Unbind();
+
+ // Checks if there's any activity happening that could capture the media
+ // the keys are associated with and then expose that media outside of the
+ // origin it is in.
+ //
+ // This method does not return the results of the check, but the MediaKeys
+ // will notify mProxy of the results using `NotifyOutputProtectionStatus`.
+ void CheckIsElementCapturePossible();
+
+ // Javascript: readonly attribute DOMString keySystem;
+ void GetKeySystem(nsString& retval) const;
+
+ // JavaScript: MediaKeys.createSession()
+ already_AddRefed<MediaKeySession> CreateSession(
+ MediaKeySessionType aSessionType, ErrorResult& aRv);
+
+ // JavaScript: MediaKeys.SetServerCertificate()
+ already_AddRefed<DetailedPromise> SetServerCertificate(
+ const ArrayBufferViewOrArrayBuffer& aServerCertificate, ErrorResult& aRv);
+
+ already_AddRefed<MediaKeySession> GetSession(const nsAString& aSessionId);
+
+ // Removes and returns MediaKeySession from the set of sessions awaiting
+ // their sessionId to be assigned.
+ already_AddRefed<MediaKeySession> GetPendingSession(uint32_t aToken);
+
+ // Called once a Init() operation succeeds.
+ void OnCDMCreated(PromiseId aId, const uint32_t aPluginId);
+
+ // Called once the CDM generates a sessionId while servicing a
+ // MediaKeySession.generateRequest() or MediaKeySession.load() call,
+ // once the sessionId of a MediaKeySession is known.
+ void OnSessionIdReady(MediaKeySession* aSession);
+
+ // Called once a LoadSession succeeds.
+ void OnSessionLoaded(PromiseId aId, bool aSuccess);
+
+ // Called once a session has closed.
+ void OnSessionClosed(MediaKeySession* aSession);
+
+ CDMProxy* GetCDMProxy() { return mProxy; }
+
+ // Makes a new promise, or nullptr on failure.
+ already_AddRefed<DetailedPromise> MakePromise(ErrorResult& aRv,
+ const nsACString& aName);
+ // Stores promise in mPromises, returning an ID that can be used to retrieve
+ // it later. The ID is passed to the CDM, so that it can signal specific
+ // promises to be resolved.
+ PromiseId StorePromise(DetailedPromise* aPromise);
+
+ // Stores a map from promise id to pending session token. Using this
+ // mapping, when a promise is rejected via its ID, we can check if the
+ // promise corresponds to a pending session and retrieve that session
+ // via the mapped-to token, and remove the pending session from the
+ // list of sessions awaiting a session id.
+ void ConnectPendingPromiseIdWithToken(PromiseId aId, uint32_t aToken);
+
+ // Reject promise with the given exception.
+ void RejectPromise(PromiseId aId, ErrorResult&& aException,
+ const nsCString& aReason);
+ // Resolves promise with "undefined".
+ void ResolvePromise(PromiseId aId);
+
+ void Shutdown();
+
+ // Called by CDMProxy when CDM crashes or shuts down. It is different from
+ // Shutdown which is called from the script/dom side.
+ void Terminated();
+
+ // Returns true if this MediaKeys has been bound to a media element.
+ bool IsBoundToMediaElement() const;
+
+ // Indicates to a MediaKeys instance that the inner window parent of that
+ // instance is being destroyed, this should prompt the keys to shutdown.
+ void OnInnerWindowDestroy();
+
+ void GetSessionsInfo(nsString& sessionsInfo);
+
+ // JavaScript: MediaKeys.GetStatusForPolicy()
+ already_AddRefed<Promise> GetStatusForPolicy(const MediaKeysPolicy& aPolicy,
+ ErrorResult& aR);
+ // Called by CDMProxy when CDM successfully GetStatusForPolicy.
+ void ResolvePromiseWithKeyStatus(PromiseId aId,
+ dom::MediaKeyStatus aMediaKeyStatus);
+
+ template <typename T>
+ void ResolvePromiseWithResult(PromiseId aId, const T& aResult) {
+ RefPtr<DetailedPromise> promise(RetrievePromise(aId));
+ if (!promise) {
+ return;
+ }
+ promise->MaybeResolve(aResult);
+ }
+
+ // The topic used for requests related to mediakeys -- observe this to be
+ // notified of such requests.
+ constexpr static const char* kMediaKeysRequestTopic = "mediakeys-request";
+
+ private:
+ // Instantiate CDMProxy instance.
+ // It could be MediaDrmCDMProxy (Widevine on Fennec) or ChromiumCDMProxy (the
+ // rest).
+ already_AddRefed<CDMProxy> CreateCDMProxy();
+
+ // Removes promise from mPromises, and returns it.
+ already_AddRefed<DetailedPromise> RetrievePromise(PromiseId aId);
+
+ // Helpers to connect and disconnect to the parent inner window. An inner
+ // window should track (via weak ptr) MediaKeys created within it so we can
+ // ensure MediaKeys are shutdown if that window is destroyed.
+ void ConnectInnerWindow();
+ void DisconnectInnerWindow();
+
+ // Owning ref to proxy. The proxy has a weak reference back to the MediaKeys,
+ // and the MediaKeys destructor clears the proxy's reference to the MediaKeys.
+ RefPtr<CDMProxy> mProxy;
+
+ // The HTMLMediaElement the MediaKeys are associated with. Note that a
+ // MediaKeys instance may not be associated with any HTMLMediaElement so
+ // this can be null (we also cannot rely on a media element to drive shutdown
+ // for this reason).
+ RefPtr<HTMLMediaElement> mElement;
+
+ // The inner window associated with an instance of MediaKeys. We will
+ // shutdown the media keys when this Window is destroyed. We do this from the
+ // window rather than a document to address the case where media keys can be
+ // created in an about:blank document that then performs an async load -- this
+ // recreates the document, but the inner window is preserved in such a case.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1675360 for more info.
+ nsCOMPtr<nsPIDOMWindowInner> mParent;
+ const nsString mKeySystem;
+ KeySessionHashMap mKeySessions;
+ PromiseHashMap mPromises;
+ PendingKeySessionsHashMap mPendingSessions;
+ PromiseId mCreatePromiseId;
+
+ // The principal of the relevant settings object.
+ RefPtr<nsIPrincipal> mPrincipal;
+ // The principal of the top level page. This can differ from mPrincipal if
+ // we're in an iframe.
+ RefPtr<nsIPrincipal> mTopLevelPrincipal;
+
+ const MediaKeySystemConfiguration mConfig;
+
+ PendingPromiseIdTokenHashMap mPromiseIdToken;
+
+ // The topic a MediaKeys instance will observe to receive updates from
+ // EncryptedMediaChild.
+ constexpr static const char* kMediaKeysResponseTopic = "mediakeys-response";
+ // Tracks if we've added an observer for responses from the associated
+ // EncryptedMediaChild. When true an observer is already in place, otherwise
+ // the observer has not yet been added.
+ bool mObserverAdded = false;
+ // Stores the json request we will send to EncryptedMediaChild when querying
+ // output protection. Lazily populated upon first use.
+ nsString mCaptureCheckRequestJson;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_mediakeys_h__
diff --git a/dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.cpp b/dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.cpp
new file mode 100644
index 0000000000..e5431f50fd
--- /dev/null
+++ b/dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.cpp
@@ -0,0 +1,115 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaDrmCDMCallbackProxy.h"
+#include "MediaDrmCDMProxy.h"
+#include "nsString.h"
+#include "mozilla/dom/MediaKeys.h"
+#include "mozilla/dom/MediaKeySession.h"
+#include "nsContentCID.h"
+#include "nsServiceManagerUtils.h"
+#include "MainThreadUtils.h"
+#include "mozilla/EMEUtils.h"
+
+namespace mozilla {
+
+MediaDrmCDMCallbackProxy::MediaDrmCDMCallbackProxy(MediaDrmCDMProxy* aProxy)
+ : mProxy(aProxy) {}
+
+MediaDrmCDMCallbackProxy::~MediaDrmCDMCallbackProxy() = default;
+
+void MediaDrmCDMCallbackProxy::SetSessionId(uint32_t aToken,
+ const nsCString& aSessionId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mProxy->OnSetSessionId(aToken, NS_ConvertUTF8toUTF16(aSessionId));
+}
+
+void MediaDrmCDMCallbackProxy::ResolveLoadSessionPromise(uint32_t aPromiseId,
+ bool aSuccess) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mProxy->OnResolveLoadSessionPromise(aPromiseId, aSuccess);
+}
+
+void MediaDrmCDMCallbackProxy::ResolvePromise(uint32_t aPromiseId) {
+ // Note: CDMProxy proxies this from non-main threads to main thread.
+ mProxy->ResolvePromise(aPromiseId);
+}
+
+void MediaDrmCDMCallbackProxy::RejectPromise(uint32_t aPromiseId,
+ ErrorResult&& aException,
+ const nsCString& aMessage) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mProxy->OnRejectPromise(aPromiseId, std::move(aException), aMessage);
+}
+
+void MediaDrmCDMCallbackProxy::SessionMessage(
+ const nsCString& aSessionId, dom::MediaKeyMessageType aMessageType,
+ const nsTArray<uint8_t>& aMessage) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // For removing constness
+ nsTArray<uint8_t> message(aMessage.Clone());
+ mProxy->OnSessionMessage(NS_ConvertUTF8toUTF16(aSessionId), aMessageType,
+ message);
+}
+
+void MediaDrmCDMCallbackProxy::ExpirationChange(const nsCString& aSessionId,
+ UnixTime aExpiryTime) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mProxy->OnExpirationChange(NS_ConvertUTF8toUTF16(aSessionId), aExpiryTime);
+}
+
+void MediaDrmCDMCallbackProxy::SessionClosed(const nsCString& aSessionId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ bool keyStatusesChange = false;
+ {
+ auto caps = mProxy->Capabilites().Lock();
+ keyStatusesChange =
+ caps->RemoveKeysForSession(NS_ConvertUTF8toUTF16(aSessionId));
+ }
+ if (keyStatusesChange) {
+ mProxy->OnKeyStatusesChange(NS_ConvertUTF8toUTF16(aSessionId));
+ }
+ mProxy->OnSessionClosed(NS_ConvertUTF8toUTF16(aSessionId));
+}
+
+void MediaDrmCDMCallbackProxy::SessionError(const nsCString& aSessionId,
+ nsresult aException,
+ uint32_t aSystemCode,
+ const nsCString& aMessage) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mProxy->OnSessionError(NS_ConvertUTF8toUTF16(aSessionId), aException,
+ aSystemCode, NS_ConvertUTF8toUTF16(aMessage));
+}
+
+void MediaDrmCDMCallbackProxy::BatchedKeyStatusChanged(
+ const nsCString& aSessionId, const nsTArray<CDMKeyInfo>& aKeyInfos) {
+ MOZ_ASSERT(NS_IsMainThread());
+ BatchedKeyStatusChangedInternal(aSessionId, aKeyInfos);
+}
+
+void MediaDrmCDMCallbackProxy::BatchedKeyStatusChangedInternal(
+ const nsCString& aSessionId, const nsTArray<CDMKeyInfo>& aKeyInfos) {
+ bool keyStatusesChange = false;
+ {
+ auto caps = mProxy->Capabilites().Lock();
+ for (size_t i = 0; i < aKeyInfos.Length(); i++) {
+ keyStatusesChange |= caps->SetKeyStatus(aKeyInfos[i].mKeyId,
+ NS_ConvertUTF8toUTF16(aSessionId),
+ aKeyInfos[i].mStatus);
+ }
+ }
+ if (keyStatusesChange) {
+ mProxy->OnKeyStatusesChange(NS_ConvertUTF8toUTF16(aSessionId));
+ }
+}
+
+void MediaDrmCDMCallbackProxy::Decrypted(
+ uint32_t aId, DecryptStatus aResult,
+ const nsTArray<uint8_t>& aDecryptedData) {
+ MOZ_ASSERT_UNREACHABLE("Fennec could not handle decrypted event");
+}
+
+} // namespace mozilla
diff --git a/dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.h b/dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.h
new file mode 100644
index 0000000000..e963e21e11
--- /dev/null
+++ b/dom/media/eme/mediadrm/MediaDrmCDMCallbackProxy.h
@@ -0,0 +1,63 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MediaDrmCDMCallbackProxy_h_
+#define MediaDrmCDMCallbackProxy_h_
+
+#include "mozilla/CDMProxy.h"
+#include "mozilla/DecryptorProxyCallback.h"
+
+namespace mozilla {
+class CDMProxy;
+class ErrorResult;
+class MediaDrmCDMProxy;
+
+// Proxies call backs from the MediaDrmProxy -> MediaDrmProxySupport back to the
+// MediaKeys object on the main thread. We used annotation calledFrom = "gecko"
+// to ensure running on main thread.
+class MediaDrmCDMCallbackProxy final : public DecryptorProxyCallback {
+ public:
+ void SetSessionId(uint32_t aCreateSessionToken,
+ const nsCString& aSessionId) override;
+
+ void ResolveLoadSessionPromise(uint32_t aPromiseId, bool aSuccess) override;
+
+ void ResolvePromise(uint32_t aPromiseId) override;
+
+ void RejectPromise(uint32_t aPromiseId, ErrorResult&& aException,
+ const nsCString& aSessionId) override;
+
+ void SessionMessage(const nsCString& aSessionId,
+ dom::MediaKeyMessageType aMessageType,
+ const nsTArray<uint8_t>& aMessage) override;
+
+ void ExpirationChange(const nsCString& aSessionId,
+ UnixTime aExpiryTime) override;
+
+ void SessionClosed(const nsCString& aSessionId) override;
+
+ void SessionError(const nsCString& aSessionId, nsresult aException,
+ uint32_t aSystemCode, const nsCString& aMessage) override;
+
+ void Decrypted(uint32_t aId, DecryptStatus aResult,
+ const nsTArray<uint8_t>& aDecryptedData) override;
+
+ void BatchedKeyStatusChanged(const nsCString& aSessionId,
+ const nsTArray<CDMKeyInfo>& aKeyInfos) override;
+
+ ~MediaDrmCDMCallbackProxy();
+
+ private:
+ friend class MediaDrmCDMProxy;
+ explicit MediaDrmCDMCallbackProxy(MediaDrmCDMProxy* aProxy);
+
+ void BatchedKeyStatusChangedInternal(const nsCString& aSessionId,
+ const nsTArray<CDMKeyInfo>& aKeyInfos);
+ const RefPtr<MediaDrmCDMProxy> mProxy;
+};
+
+} // namespace mozilla
+#endif
diff --git a/dom/media/eme/mediadrm/MediaDrmCDMProxy.cpp b/dom/media/eme/mediadrm/MediaDrmCDMProxy.cpp
new file mode 100644
index 0000000000..07b7bfe3a0
--- /dev/null
+++ b/dom/media/eme/mediadrm/MediaDrmCDMProxy.cpp
@@ -0,0 +1,453 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/MediaKeySession.h"
+#include "mozilla/MediaDrmCDMProxy.h"
+#include "MediaDrmCDMCallbackProxy.h"
+
+namespace mozilla {
+
+MediaDrmSessionType ToMediaDrmSessionType(
+ dom::MediaKeySessionType aSessionType) {
+ switch (aSessionType) {
+ case dom::MediaKeySessionType::Temporary:
+ return kKeyStreaming;
+ case dom::MediaKeySessionType::Persistent_license:
+ return kKeyOffline;
+ default:
+ return kKeyStreaming;
+ };
+}
+
+MediaDrmCDMProxy::MediaDrmCDMProxy(dom::MediaKeys* aKeys,
+ const nsAString& aKeySystem,
+ bool aDistinctiveIdentifierRequired,
+ bool aPersistentStateRequired)
+ : CDMProxy(aKeys, aKeySystem, aDistinctiveIdentifierRequired,
+ aPersistentStateRequired),
+ mCDM(nullptr),
+ mShutdownCalled(false) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_COUNT_CTOR(MediaDrmCDMProxy);
+}
+
+MediaDrmCDMProxy::~MediaDrmCDMProxy() { MOZ_COUNT_DTOR(MediaDrmCDMProxy); }
+
+void MediaDrmCDMProxy::Init(PromiseId aPromiseId, const nsAString& aOrigin,
+ const nsAString& aTopLevelOrigin,
+ const nsAString& aName) {
+ MOZ_ASSERT(NS_IsMainThread());
+ NS_ENSURE_TRUE_VOID(!mKeys.IsNull());
+
+ EME_LOG("MediaDrmCDMProxy::Init (%s, %s) %s",
+ NS_ConvertUTF16toUTF8(aOrigin).get(),
+ NS_ConvertUTF16toUTF8(aTopLevelOrigin).get(),
+ NS_ConvertUTF16toUTF8(aName).get());
+
+ // Create a thread to work with cdm.
+ if (!mOwnerThread) {
+ nsresult rv =
+ NS_NewNamedThread("MDCDMThread", getter_AddRefs(mOwnerThread));
+ if (NS_FAILED(rv)) {
+ RejectPromiseWithStateError(
+ aPromiseId, nsLiteralCString(
+ "Couldn't create CDM thread MediaDrmCDMProxy::Init"));
+ return;
+ }
+ }
+
+ mCDM = mozilla::MakeUnique<MediaDrmProxySupport>(mKeySystem);
+ nsCOMPtr<nsIRunnable> task(
+ NewRunnableMethod<uint32_t>("MediaDrmCDMProxy::md_Init", this,
+ &MediaDrmCDMProxy::md_Init, aPromiseId));
+ mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL);
+}
+
+void MediaDrmCDMProxy::CreateSession(uint32_t aCreateSessionToken,
+ MediaKeySessionType aSessionType,
+ PromiseId aPromiseId,
+ const nsAString& aInitDataType,
+ nsTArray<uint8_t>& aInitData) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mOwnerThread);
+
+ UniquePtr<CreateSessionData> data(new CreateSessionData());
+ data->mSessionType = aSessionType;
+ data->mCreateSessionToken = aCreateSessionToken;
+ data->mPromiseId = aPromiseId;
+ data->mInitDataType = NS_ConvertUTF16toUTF8(aInitDataType);
+ data->mInitData = std::move(aInitData);
+
+ nsCOMPtr<nsIRunnable> task(NewRunnableMethod<UniquePtr<CreateSessionData>&&>(
+ "MediaDrmCDMProxy::md_CreateSession", this,
+ &MediaDrmCDMProxy::md_CreateSession, std::move(data)));
+ mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL);
+}
+
+void MediaDrmCDMProxy::LoadSession(PromiseId aPromiseId,
+ dom::MediaKeySessionType aSessionType,
+ const nsAString& aSessionId) {
+ // TODO: Implement LoadSession.
+ RejectPromiseWithStateError(
+ aPromiseId, "Currently Fennec does not support LoadSession"_ns);
+}
+
+void MediaDrmCDMProxy::SetServerCertificate(PromiseId aPromiseId,
+ nsTArray<uint8_t>& aCert) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mOwnerThread);
+
+ mOwnerThread->Dispatch(NewRunnableMethod<PromiseId, const nsTArray<uint8_t>>(
+ "MediaDrmCDMProxy::md_SetServerCertificate", this,
+ &MediaDrmCDMProxy::md_SetServerCertificate,
+ aPromiseId, std::move(aCert)),
+ NS_DISPATCH_NORMAL);
+}
+
+void MediaDrmCDMProxy::UpdateSession(const nsAString& aSessionId,
+ PromiseId aPromiseId,
+ nsTArray<uint8_t>& aResponse) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mOwnerThread);
+ NS_ENSURE_TRUE_VOID(!mKeys.IsNull());
+
+ UniquePtr<UpdateSessionData> data(new UpdateSessionData());
+ data->mPromiseId = aPromiseId;
+ data->mSessionId = NS_ConvertUTF16toUTF8(aSessionId);
+ data->mResponse = std::move(aResponse);
+
+ nsCOMPtr<nsIRunnable> task(NewRunnableMethod<UniquePtr<UpdateSessionData>&&>(
+ "MediaDrmCDMProxy::md_UpdateSession", this,
+ &MediaDrmCDMProxy::md_UpdateSession, std::move(data)));
+ mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL);
+}
+
+void MediaDrmCDMProxy::CloseSession(const nsAString& aSessionId,
+ PromiseId aPromiseId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mOwnerThread);
+ NS_ENSURE_TRUE_VOID(!mKeys.IsNull());
+
+ UniquePtr<SessionOpData> data(new SessionOpData());
+ data->mPromiseId = aPromiseId;
+ data->mSessionId = NS_ConvertUTF16toUTF8(aSessionId);
+
+ nsCOMPtr<nsIRunnable> task(NewRunnableMethod<UniquePtr<SessionOpData>&&>(
+ "MediaDrmCDMProxy::md_CloseSession", this,
+ &MediaDrmCDMProxy::md_CloseSession, std::move(data)));
+ mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL);
+}
+
+void MediaDrmCDMProxy::RemoveSession(const nsAString& aSessionId,
+ PromiseId aPromiseId) {
+ // TODO: Implement RemoveSession.
+ RejectPromiseWithStateError(
+ aPromiseId, "Currently Fennec does not support RemoveSession"_ns);
+}
+
+void MediaDrmCDMProxy::QueryOutputProtectionStatus() {
+ // TODO(bryce): determine if this is needed for Android and implement as
+ // needed. See also `NotifyOutputProtectionStatus`.
+}
+
+void MediaDrmCDMProxy::NotifyOutputProtectionStatus(
+ OutputProtectionCheckStatus aCheckStatus,
+ OutputProtectionCaptureStatus aCaptureStatus) {
+ // TODO(bryce): determine if this is needed for Android and implement as
+ // needed. See also `QueryOutputProtectionStatus`.
+}
+
+void MediaDrmCDMProxy::Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mOwnerThread);
+ nsCOMPtr<nsIRunnable> task(NewRunnableMethod(
+ "MediaDrmCDMProxy::md_Shutdown", this, &MediaDrmCDMProxy::md_Shutdown));
+
+ mOwnerThread->Dispatch(task, NS_DISPATCH_NORMAL);
+ mOwnerThread->Shutdown();
+ mOwnerThread = nullptr;
+ mKeys.Clear();
+}
+
+void MediaDrmCDMProxy::Terminated() {
+ // TODO: Implement Terminated.
+ // Should find a way to handle the case when remote side MediaDrm crashed.
+}
+
+void MediaDrmCDMProxy::OnSetSessionId(uint32_t aCreateSessionToken,
+ const nsAString& aSessionId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mKeys.IsNull()) {
+ return;
+ }
+
+ RefPtr<dom::MediaKeySession> session(
+ mKeys->GetPendingSession(aCreateSessionToken));
+ if (session) {
+ session->SetSessionId(aSessionId);
+ }
+}
+
+void MediaDrmCDMProxy::OnResolveLoadSessionPromise(uint32_t aPromiseId,
+ bool aSuccess) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mKeys.IsNull()) {
+ return;
+ }
+ mKeys->OnSessionLoaded(aPromiseId, aSuccess);
+}
+
+void MediaDrmCDMProxy::OnSessionMessage(const nsAString& aSessionId,
+ dom::MediaKeyMessageType aMessageType,
+ const nsTArray<uint8_t>& aMessage) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mKeys.IsNull()) {
+ return;
+ }
+ RefPtr<dom::MediaKeySession> session(mKeys->GetSession(aSessionId));
+ if (session) {
+ session->DispatchKeyMessage(aMessageType, aMessage);
+ }
+}
+
+void MediaDrmCDMProxy::OnExpirationChange(const nsAString& aSessionId,
+ UnixTime aExpiryTime) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mKeys.IsNull()) {
+ return;
+ }
+ RefPtr<dom::MediaKeySession> session(mKeys->GetSession(aSessionId));
+ if (session) {
+ session->SetExpiration(static_cast<double>(aExpiryTime));
+ }
+}
+
+void MediaDrmCDMProxy::OnSessionClosed(const nsAString& aSessionId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mKeys.IsNull()) {
+ return;
+ }
+ RefPtr<dom::MediaKeySession> session(mKeys->GetSession(aSessionId));
+ if (session) {
+ session->OnClosed();
+ }
+}
+
+void MediaDrmCDMProxy::OnSessionError(const nsAString& aSessionId,
+ nsresult aException, uint32_t aSystemCode,
+ const nsAString& aMsg) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mKeys.IsNull()) {
+ return;
+ }
+ RefPtr<dom::MediaKeySession> session(mKeys->GetSession(aSessionId));
+ if (session) {
+ session->DispatchKeyError(aSystemCode);
+ }
+}
+
+void MediaDrmCDMProxy::OnRejectPromise(uint32_t aPromiseId,
+ ErrorResult&& aException,
+ const nsCString& aMsg) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RejectPromise(aPromiseId, std::move(aException), aMsg);
+}
+
+RefPtr<DecryptPromise> MediaDrmCDMProxy::Decrypt(MediaRawData* aSample) {
+ MOZ_ASSERT_UNREACHABLE("Fennec could not handle decrypting individually");
+ return nullptr;
+}
+
+void MediaDrmCDMProxy::OnDecrypted(uint32_t aId, DecryptStatus aResult,
+ const nsTArray<uint8_t>& aDecryptedData) {
+ MOZ_ASSERT_UNREACHABLE("Fennec could not handle decrypted event");
+}
+
+void MediaDrmCDMProxy::RejectPromise(PromiseId aId, ErrorResult&& aException,
+ const nsCString& aReason) {
+ if (NS_IsMainThread()) {
+ if (!mKeys.IsNull()) {
+ mKeys->RejectPromise(aId, std::move(aException), aReason);
+ }
+ } else {
+ nsCOMPtr<nsIRunnable> task(
+ new RejectPromiseTask(this, aId, std::move(aException), aReason));
+ mMainThread->Dispatch(task.forget(), NS_DISPATCH_NORMAL);
+ }
+}
+
+void MediaDrmCDMProxy::RejectPromiseWithStateError(PromiseId aId,
+ const nsCString& aReason) {
+ ErrorResult rv;
+ rv.ThrowInvalidStateError(aReason);
+ RejectPromise(aId, std::move(rv), aReason);
+}
+
+void MediaDrmCDMProxy::ResolvePromise(PromiseId aId) {
+ if (NS_IsMainThread()) {
+ if (!mKeys.IsNull()) {
+ mKeys->ResolvePromise(aId);
+ } else {
+ NS_WARNING("MediaDrmCDMProxy unable to resolve promise!");
+ }
+ } else {
+ nsCOMPtr<nsIRunnable> task;
+ task =
+ NewRunnableMethod<PromiseId>("MediaDrmCDMProxy::ResolvePromise", this,
+ &MediaDrmCDMProxy::ResolvePromise, aId);
+ mMainThread->Dispatch(task.forget(), NS_DISPATCH_NORMAL);
+ }
+}
+
+template <typename T>
+void MediaDrmCDMProxy::ResolvePromiseWithResult(PromiseId aId,
+ const T& aResult) {
+ if (NS_IsMainThread()) {
+ if (!mKeys.IsNull()) {
+ mKeys->ResolvePromiseWithResult(aId, aResult);
+ } else {
+ NS_WARNING("MediaDrmCDMProxy unable to resolve promise!");
+ }
+ return;
+ }
+
+ nsCOMPtr<nsIRunnable> task;
+ task = NewRunnableMethod<PromiseId, T>(
+ "MediaDrmCDMProxy::ResolvePromiseWithResult", this,
+ &MediaDrmCDMProxy::ResolvePromiseWithResult<T>, aId, aResult);
+ mMainThread->Dispatch(task.forget(), NS_DISPATCH_NORMAL);
+}
+
+void MediaDrmCDMProxy::OnKeyStatusesChange(const nsAString& aSessionId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mKeys.IsNull()) {
+ return;
+ }
+ RefPtr<dom::MediaKeySession> session(mKeys->GetSession(aSessionId));
+ if (session) {
+ session->DispatchKeyStatusesChange();
+ }
+}
+
+void MediaDrmCDMProxy::GetStatusForPolicy(PromiseId aPromiseId,
+ const nsAString& aMinHdcpVersion) {
+ // TODO: Implement GetStatusForPolicy.
+ constexpr auto err =
+ "Currently Fennec does not support GetStatusForPolicy"_ns;
+
+ ErrorResult rv;
+ rv.ThrowNotSupportedError(err);
+ RejectPromise(aPromiseId, std::move(rv), err);
+}
+
+#ifdef DEBUG
+bool MediaDrmCDMProxy::IsOnOwnerThread() {
+ return NS_GetCurrentThread() == mOwnerThread;
+}
+#endif
+
+const nsString& MediaDrmCDMProxy::GetMediaDrmStubId() const {
+ MOZ_ASSERT(mCDM);
+ return mCDM->GetMediaDrmStubId();
+}
+
+void MediaDrmCDMProxy::OnCDMCreated(uint32_t aPromiseId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mKeys.IsNull()) {
+ return;
+ }
+
+ if (mCDM) {
+ mKeys->OnCDMCreated(aPromiseId, 0);
+ return;
+ }
+
+ // No CDM? Just reject the promise.
+ constexpr auto err = "Null CDM in OnCDMCreated()"_ns;
+ ErrorResult rv;
+ rv.ThrowInvalidStateError(err);
+ mKeys->RejectPromise(aPromiseId, std::move(rv), err);
+}
+
+void MediaDrmCDMProxy::md_Init(uint32_t aPromiseId) {
+ MOZ_ASSERT(IsOnOwnerThread());
+ MOZ_ASSERT(mCDM);
+
+ UniquePtr<MediaDrmCDMCallbackProxy> callback(
+ new MediaDrmCDMCallbackProxy(this));
+ mCDM->Init(std::move(callback));
+ nsCOMPtr<nsIRunnable> task(
+ NewRunnableMethod<uint32_t>("MediaDrmCDMProxy::OnCDMCreated", this,
+ &MediaDrmCDMProxy::OnCDMCreated, aPromiseId));
+ mMainThread->Dispatch(task.forget(), NS_DISPATCH_NORMAL);
+}
+
+void MediaDrmCDMProxy::md_CreateSession(UniquePtr<CreateSessionData>&& aData) {
+ MOZ_ASSERT(IsOnOwnerThread());
+
+ if (!mCDM) {
+ RejectPromiseWithStateError(aData->mPromiseId,
+ "Null CDM in md_CreateSession"_ns);
+ return;
+ }
+
+ mCDM->CreateSession(aData->mCreateSessionToken, aData->mPromiseId,
+ aData->mInitDataType, aData->mInitData,
+ ToMediaDrmSessionType(aData->mSessionType));
+}
+
+void MediaDrmCDMProxy::md_SetServerCertificate(PromiseId aPromiseId,
+ const nsTArray<uint8_t>& aCert) {
+ MOZ_ASSERT(IsOnOwnerThread());
+
+ if (!mCDM) {
+ RejectPromiseWithStateError(aPromiseId,
+ "Null CDM in md_SetServerCertificate"_ns);
+ return;
+ }
+
+ if (mCDM->SetServerCertificate(aCert)) {
+ ResolvePromiseWithResult(aPromiseId, true);
+ } else {
+ RejectPromiseWithStateError(
+ aPromiseId, "MediaDrmCDMProxy unable to set server certificate"_ns);
+ }
+}
+
+void MediaDrmCDMProxy::md_UpdateSession(UniquePtr<UpdateSessionData>&& aData) {
+ MOZ_ASSERT(IsOnOwnerThread());
+
+ if (!mCDM) {
+ RejectPromiseWithStateError(aData->mPromiseId,
+ "Null CDM in md_UpdateSession"_ns);
+ return;
+ }
+ mCDM->UpdateSession(aData->mPromiseId, aData->mSessionId, aData->mResponse);
+}
+
+void MediaDrmCDMProxy::md_CloseSession(UniquePtr<SessionOpData>&& aData) {
+ MOZ_ASSERT(IsOnOwnerThread());
+
+ if (!mCDM) {
+ RejectPromiseWithStateError(aData->mPromiseId,
+ "Null CDM in md_CloseSession"_ns);
+ return;
+ }
+ mCDM->CloseSession(aData->mPromiseId, aData->mSessionId);
+}
+
+void MediaDrmCDMProxy::md_Shutdown() {
+ MOZ_ASSERT(IsOnOwnerThread());
+ MOZ_ASSERT(mCDM);
+ if (mShutdownCalled) {
+ return;
+ }
+ mShutdownCalled = true;
+ mCDM->Shutdown();
+ mCDM = nullptr;
+}
+
+} // namespace mozilla
diff --git a/dom/media/eme/mediadrm/MediaDrmCDMProxy.h b/dom/media/eme/mediadrm/MediaDrmCDMProxy.h
new file mode 100644
index 0000000000..60a541ece9
--- /dev/null
+++ b/dom/media/eme/mediadrm/MediaDrmCDMProxy.h
@@ -0,0 +1,186 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MediaDrmCDMProxy_h_
+#define MediaDrmCDMProxy_h_
+
+#include <jni.h>
+#include "mozilla/jni/Types.h"
+#include "mozilla/CDMProxy.h"
+#include "mozilla/CDMCaps.h"
+#include "mozilla/dom/MediaKeys.h"
+#include "mozilla/dom/MediaKeySession.h"
+#include "mozilla/MediaDrmProxySupport.h"
+#include "mozilla/UniquePtr.h"
+
+#include "MediaCodec.h"
+#include "nsString.h"
+
+namespace mozilla {
+
+class MediaDrmCDMCallbackProxy;
+class MediaDrmCDMProxy final : public CDMProxy {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaDrmCDMProxy, override)
+
+ MediaDrmCDMProxy(dom::MediaKeys* aKeys, const nsAString& aKeySystem,
+ bool aDistinctiveIdentifierRequired,
+ bool aPersistentStateRequired);
+
+ void Init(PromiseId aPromiseId, const nsAString& aOrigin,
+ const nsAString& aTopLevelOrigin,
+ const nsAString& aGMPName) override;
+
+ void CreateSession(uint32_t aCreateSessionToken,
+ MediaKeySessionType aSessionType, PromiseId aPromiseId,
+ const nsAString& aInitDataType,
+ nsTArray<uint8_t>& aInitData) override;
+
+ void LoadSession(PromiseId aPromiseId, dom::MediaKeySessionType aSessionType,
+ const nsAString& aSessionId) override;
+
+ void SetServerCertificate(PromiseId aPromiseId,
+ nsTArray<uint8_t>& aCert) override;
+
+ void UpdateSession(const nsAString& aSessionId, PromiseId aPromiseId,
+ nsTArray<uint8_t>& aResponse) override;
+
+ void CloseSession(const nsAString& aSessionId, PromiseId aPromiseId) override;
+
+ void RemoveSession(const nsAString& aSessionId,
+ PromiseId aPromiseId) override;
+
+ void QueryOutputProtectionStatus() override;
+
+ void NotifyOutputProtectionStatus(
+ OutputProtectionCheckStatus aCheckStatus,
+ OutputProtectionCaptureStatus aCaptureStatus) override;
+
+ void Shutdown() override;
+
+ void Terminated() override;
+
+ void OnSetSessionId(uint32_t aCreateSessionToken,
+ const nsAString& aSessionId) override;
+
+ void OnResolveLoadSessionPromise(uint32_t aPromiseId, bool aSuccess) override;
+
+ void OnSessionMessage(const nsAString& aSessionId,
+ dom::MediaKeyMessageType aMessageType,
+ const nsTArray<uint8_t>& aMessage) override;
+
+ void OnExpirationChange(const nsAString& aSessionId,
+ UnixTime aExpiryTime) override;
+
+ void OnSessionClosed(const nsAString& aSessionId) override;
+
+ void OnSessionError(const nsAString& aSessionId, nsresult aException,
+ uint32_t aSystemCode, const nsAString& aMsg) override;
+
+ void OnRejectPromise(uint32_t aPromiseId, ErrorResult&& aException,
+ const nsCString& aMsg) override;
+
+ RefPtr<DecryptPromise> Decrypt(MediaRawData* aSample) override;
+ void OnDecrypted(uint32_t aId, DecryptStatus aResult,
+ const nsTArray<uint8_t>& aDecryptedData) override;
+
+ void RejectPromise(PromiseId aId, ErrorResult&& aException,
+ const nsCString& aReason) override;
+ // Reject promise with an InvalidStateError and the given message.
+ void RejectPromiseWithStateError(PromiseId aId, const nsCString& aReason);
+
+ // Resolves promise with "undefined".
+ // Can be called from any thread.
+ void ResolvePromise(PromiseId aId) override;
+
+ void OnKeyStatusesChange(const nsAString& aSessionId) override;
+
+ void GetStatusForPolicy(PromiseId aPromiseId,
+ const nsAString& aMinHdcpVersion) override;
+
+#ifdef DEBUG
+ bool IsOnOwnerThread() override;
+#endif
+
+ const nsString& GetMediaDrmStubId() const;
+
+ private:
+ virtual ~MediaDrmCDMProxy();
+
+ void OnCDMCreated(uint32_t aPromiseId);
+
+ template <typename T>
+ void ResolvePromiseWithResult(PromiseId aId, const T& aResult);
+
+ struct CreateSessionData {
+ MediaKeySessionType mSessionType;
+ uint32_t mCreateSessionToken;
+ PromiseId mPromiseId;
+ nsCString mInitDataType;
+ nsTArray<uint8_t> mInitData;
+ };
+
+ struct UpdateSessionData {
+ PromiseId mPromiseId;
+ nsCString mSessionId;
+ nsTArray<uint8_t> mResponse;
+ };
+
+ struct SessionOpData {
+ PromiseId mPromiseId;
+ nsCString mSessionId;
+ };
+
+ class RejectPromiseTask : public Runnable {
+ public:
+ RejectPromiseTask(MediaDrmCDMProxy* aProxy, PromiseId aId,
+ ErrorResult&& aException, const nsCString& aReason)
+ : Runnable("RejectPromiseTask"),
+ mProxy(aProxy),
+ mId(aId),
+ mException(std::move(aException)),
+ mReason(aReason) {}
+ NS_IMETHOD Run() override {
+ // Moving into or out of a non-copyable ErrorResult will assert that both
+ // ErorResults are from our current thread. Avoid the assertion by moving
+ // into a current-thread CopyableErrorResult first. Note that this is
+ // safe, because CopyableErrorResult never holds state that can't move
+ // across threads.
+ CopyableErrorResult rv(std::move(mException));
+ mProxy->RejectPromise(mId, std::move(rv), mReason);
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<MediaDrmCDMProxy> mProxy;
+ PromiseId mId;
+ // We use a CopyableErrorResult here, because we're going to dispatch to a
+ // different thread and normal ErrorResult doesn't support that.
+ // CopyableErrorResult ensures that it only stores values that are safe to
+ // move across threads.
+ CopyableErrorResult mException;
+ nsCString mReason;
+ };
+
+ nsCString mNodeId;
+ UniquePtr<MediaDrmProxySupport> mCDM;
+ bool mShutdownCalled;
+
+ // =====================================================================
+ // For MediaDrmProxySupport
+ void md_Init(uint32_t aPromiseId);
+ void md_CreateSession(UniquePtr<CreateSessionData>&& aData);
+ void md_SetServerCertificate(PromiseId aPromiseId,
+ const nsTArray<uint8_t>& aCert);
+ void md_UpdateSession(UniquePtr<UpdateSessionData>&& aData);
+ void md_CloseSession(UniquePtr<SessionOpData>&& aData);
+ void md_Shutdown();
+ // =====================================================================
+};
+
+} // namespace mozilla
+
+#endif // MediaDrmCDMProxy_h_
diff --git a/dom/media/eme/mediadrm/MediaDrmProxySupport.cpp b/dom/media/eme/mediadrm/MediaDrmProxySupport.cpp
new file mode 100644
index 0000000000..717a57df35
--- /dev/null
+++ b/dom/media/eme/mediadrm/MediaDrmProxySupport.cpp
@@ -0,0 +1,272 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaDrmProxySupport.h"
+#include "MediaDrmCDMCallbackProxy.h"
+#include "mozilla/EMEUtils.h"
+#include "mozilla/java/MediaDrmProxyNatives.h"
+#include "mozilla/java/SessionKeyInfoWrappers.h"
+#include "MediaCodec.h" // For MediaDrm::KeyStatus
+
+namespace mozilla {
+
+LogModule* GetMDRMNLog() {
+ static LazyLogModule log("MediaDrmProxySupport");
+ return log;
+}
+
+class MediaDrmJavaCallbacksSupport
+ : public java::MediaDrmProxy::NativeMediaDrmProxyCallbacks::Natives<
+ MediaDrmJavaCallbacksSupport> {
+ public:
+ typedef java::MediaDrmProxy::NativeMediaDrmProxyCallbacks::Natives<
+ MediaDrmJavaCallbacksSupport>
+ MediaDrmProxyNativeCallbacks;
+ using MediaDrmProxyNativeCallbacks::AttachNative;
+ using MediaDrmProxyNativeCallbacks::DisposeNative;
+
+ explicit MediaDrmJavaCallbacksSupport(
+ UniquePtr<MediaDrmCDMCallbackProxy>&& aDecryptorProxyCallback)
+ : mDecryptorProxyCallback(std::move(aDecryptorProxyCallback)) {
+ MOZ_ASSERT(mDecryptorProxyCallback);
+ }
+ /*
+ * Native implementation, called by Java.
+ */
+ void OnSessionCreated(int aCreateSessionToken, int aPromiseId,
+ jni::ByteArray::Param aSessionId,
+ jni::ByteArray::Param aRequest);
+
+ void OnSessionUpdated(int aPromiseId, jni::ByteArray::Param aSessionId);
+
+ void OnSessionClosed(int aPromiseId, jni::ByteArray::Param aSessionId);
+
+ void OnSessionMessage(
+ jni::ByteArray::Param aSessionId,
+ int /*mozilla::dom::MediaKeyMessageType*/ aSessionMessageType,
+ jni::ByteArray::Param aRequest);
+
+ void OnSessionError(jni::ByteArray::Param aSessionId,
+ jni::String::Param aMessage);
+
+ void OnSessionBatchedKeyChanged(jni::ByteArray::Param,
+ jni::ObjectArray::Param);
+
+ void OnRejectPromise(int aPromiseId, jni::String::Param aMessage);
+
+ private:
+ UniquePtr<MediaDrmCDMCallbackProxy> mDecryptorProxyCallback;
+}; // MediaDrmJavaCallbacksSupport
+
+void MediaDrmJavaCallbacksSupport::OnSessionCreated(
+ int aCreateSessionToken, int aPromiseId, jni::ByteArray::Param aSessionId,
+ jni::ByteArray::Param aRequest) {
+ MOZ_ASSERT(NS_IsMainThread());
+ auto reqDataArray = aRequest->GetElements();
+ nsCString sessionId(
+ reinterpret_cast<char*>(aSessionId->GetElements().Elements()),
+ aSessionId->Length());
+ MDRMN_LOG("SessionId(%s) closed", sessionId.get());
+
+ mDecryptorProxyCallback->SetSessionId(aCreateSessionToken, sessionId);
+ mDecryptorProxyCallback->ResolvePromise(aPromiseId);
+}
+
+void MediaDrmJavaCallbacksSupport::OnSessionUpdated(
+ int aPromiseId, jni::ByteArray::Param aSessionId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MDRMN_LOG(
+ "SessionId(%s) closed",
+ nsCString(reinterpret_cast<char*>(aSessionId->GetElements().Elements()),
+ aSessionId->Length())
+ .get());
+ mDecryptorProxyCallback->ResolvePromise(aPromiseId);
+}
+
+void MediaDrmJavaCallbacksSupport::OnSessionClosed(
+ int aPromiseId, jni::ByteArray::Param aSessionId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCString sessionId(
+ reinterpret_cast<char*>(aSessionId->GetElements().Elements()),
+ aSessionId->Length());
+ MDRMN_LOG("SessionId(%s) closed", sessionId.get());
+ mDecryptorProxyCallback->ResolvePromise(aPromiseId);
+ mDecryptorProxyCallback->SessionClosed(sessionId);
+}
+
+void MediaDrmJavaCallbacksSupport::OnSessionMessage(
+ jni::ByteArray::Param aSessionId,
+ int /*mozilla::dom::MediaKeyMessageType*/ aMessageType,
+ jni::ByteArray::Param aRequest) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCString sessionId(
+ reinterpret_cast<char*>(aSessionId->GetElements().Elements()),
+ aSessionId->Length());
+ auto reqDataArray = aRequest->GetElements();
+
+ nsTArray<uint8_t> retRequest;
+ retRequest.AppendElements(reinterpret_cast<uint8_t*>(reqDataArray.Elements()),
+ reqDataArray.Length());
+
+ mDecryptorProxyCallback->SessionMessage(
+ sessionId, static_cast<dom::MediaKeyMessageType>(aMessageType),
+ retRequest);
+}
+
+void MediaDrmJavaCallbacksSupport::OnSessionError(
+ jni::ByteArray::Param aSessionId, jni::String::Param aMessage) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCString sessionId(
+ reinterpret_cast<char*>(aSessionId->GetElements().Elements()),
+ aSessionId->Length());
+ nsCString errorMessage = aMessage->ToCString();
+ MDRMN_LOG("SessionId(%s)", sessionId.get());
+ // TODO: We cannot get system error code from media drm API.
+ // Currently use -1 as an error code.
+ mDecryptorProxyCallback->SessionError(
+ sessionId, NS_ERROR_DOM_INVALID_STATE_ERR, -1, errorMessage);
+}
+
+// TODO: MediaDrm.KeyStatus defined the status code not included
+// dom::MediaKeyStatus::Released and dom::MediaKeyStatus::Output_downscaled.
+// Should keep tracking for this if it will be changed in the future.
+static dom::MediaKeyStatus MediaDrmKeyStatusToMediaKeyStatus(int aStatusCode) {
+ using mozilla::java::sdk::MediaDrm;
+ switch (aStatusCode) {
+ case MediaDrm::KeyStatus::STATUS_USABLE:
+ return dom::MediaKeyStatus::Usable;
+ case MediaDrm::KeyStatus::STATUS_EXPIRED:
+ return dom::MediaKeyStatus::Expired;
+ case MediaDrm::KeyStatus::STATUS_OUTPUT_NOT_ALLOWED:
+ return dom::MediaKeyStatus::Output_restricted;
+ case MediaDrm::KeyStatus::STATUS_INTERNAL_ERROR:
+ return dom::MediaKeyStatus::Internal_error;
+ case MediaDrm::KeyStatus::STATUS_PENDING:
+ return dom::MediaKeyStatus::Status_pending;
+ default:
+ return dom::MediaKeyStatus::Internal_error;
+ }
+}
+
+void MediaDrmJavaCallbacksSupport::OnSessionBatchedKeyChanged(
+ jni::ByteArray::Param aSessionId, jni::ObjectArray::Param aKeyInfos) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCString sessionId(
+ reinterpret_cast<char*>(aSessionId->GetElements().Elements()),
+ aSessionId->Length());
+ nsTArray<jni::Object::LocalRef> keyInfosObjectArray(aKeyInfos->GetElements());
+
+ nsTArray<CDMKeyInfo> keyInfosArray;
+
+ for (auto&& keyInfoObject : keyInfosObjectArray) {
+ java::SessionKeyInfo::LocalRef keyInfo(std::move(keyInfoObject));
+ mozilla::jni::ByteArray::LocalRef keyIdByteArray = keyInfo->KeyId();
+ nsTArray<int8_t> keyIdInt8Array = keyIdByteArray->GetElements();
+ // Cast nsTArray<int8_t> to nsTArray<uint8_t>
+ nsTArray<uint8_t>* keyId =
+ reinterpret_cast<nsTArray<uint8_t>*>(&keyIdInt8Array);
+ auto keyStatus = keyInfo->Status(); // int32_t
+ keyInfosArray.AppendElement(
+ CDMKeyInfo(*keyId, dom::Optional<dom::MediaKeyStatus>(
+ MediaDrmKeyStatusToMediaKeyStatus(keyStatus))));
+ }
+
+ mDecryptorProxyCallback->BatchedKeyStatusChanged(sessionId, keyInfosArray);
+}
+
+void MediaDrmJavaCallbacksSupport::OnRejectPromise(
+ int aPromiseId, jni::String::Param aMessage) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCString reason = aMessage->ToCString();
+ MDRMN_LOG("OnRejectPromise aMessage(%s) ", reason.get());
+ // Current implementation assume all the reject from MediaDrm is due to
+ // invalid state. Other cases should be handled before calling into
+ // MediaDrmProxy API.
+ ErrorResult rv;
+ rv.ThrowInvalidStateError(reason);
+ mDecryptorProxyCallback->RejectPromise(aPromiseId, std::move(rv), reason);
+}
+
+MediaDrmProxySupport::MediaDrmProxySupport(const nsAString& aKeySystem)
+ : mKeySystem(aKeySystem), mDestroyed(false) {
+ mJavaCallbacks = java::MediaDrmProxy::NativeMediaDrmProxyCallbacks::New();
+
+ mBridgeProxy = java::MediaDrmProxy::Create(mKeySystem, mJavaCallbacks);
+
+ MOZ_ASSERT(mBridgeProxy, "mBridgeProxy should not be null");
+ mMediaDrmStubId = mBridgeProxy->GetStubId()->ToString();
+}
+
+MediaDrmProxySupport::~MediaDrmProxySupport() {
+ MOZ_ASSERT(mDestroyed, "Shutdown() should be called before !!");
+ MediaDrmJavaCallbacksSupport::DisposeNative(mJavaCallbacks);
+}
+
+nsresult MediaDrmProxySupport::Init(
+ UniquePtr<MediaDrmCDMCallbackProxy>&& aCallback) {
+ MOZ_ASSERT(mJavaCallbacks);
+
+ MediaDrmJavaCallbacksSupport::AttachNative(
+ mJavaCallbacks,
+ mozilla::MakeUnique<MediaDrmJavaCallbacksSupport>(std::move(aCallback)));
+ return mBridgeProxy != nullptr ? NS_OK : NS_ERROR_FAILURE;
+}
+
+void MediaDrmProxySupport::CreateSession(uint32_t aCreateSessionToken,
+ uint32_t aPromiseId,
+ const nsCString& aInitDataType,
+ const nsTArray<uint8_t>& aInitData,
+ MediaDrmSessionType aSessionType) {
+ MOZ_ASSERT(mBridgeProxy);
+
+ auto initDataBytes = mozilla::jni::ByteArray::New(
+ reinterpret_cast<const int8_t*>(&aInitData[0]), aInitData.Length());
+ // TODO: aSessionType is not used here.
+ // Refer to
+ // http://androidxref.com/5.1.1_r6/xref/external/chromium_org/media/base/android/java/src/org/chromium/media/MediaDrmBridge.java#420
+ // it is hard code to streaming type.
+ mBridgeProxy->CreateSession(aCreateSessionToken, aPromiseId,
+ NS_ConvertUTF8toUTF16(aInitDataType),
+ initDataBytes);
+}
+
+void MediaDrmProxySupport::UpdateSession(uint32_t aPromiseId,
+ const nsCString& aSessionId,
+ const nsTArray<uint8_t>& aResponse) {
+ MOZ_ASSERT(mBridgeProxy);
+
+ auto response = mozilla::jni::ByteArray::New(
+ reinterpret_cast<const int8_t*>(aResponse.Elements()),
+ aResponse.Length());
+ mBridgeProxy->UpdateSession(aPromiseId, NS_ConvertUTF8toUTF16(aSessionId),
+ response);
+}
+
+void MediaDrmProxySupport::CloseSession(uint32_t aPromiseId,
+ const nsCString& aSessionId) {
+ MOZ_ASSERT(mBridgeProxy);
+
+ mBridgeProxy->CloseSession(aPromiseId, NS_ConvertUTF8toUTF16(aSessionId));
+}
+
+void MediaDrmProxySupport::Shutdown() {
+ MOZ_ASSERT(mBridgeProxy);
+
+ if (mDestroyed) {
+ return;
+ }
+ mBridgeProxy->Destroy();
+ mDestroyed = true;
+}
+
+bool MediaDrmProxySupport::SetServerCertificate(
+ const nsTArray<uint8_t>& aCert) {
+ jni::ByteArray::LocalRef cert = jni::ByteArray::New(
+ reinterpret_cast<const int8_t*>(aCert.Elements()), aCert.Length());
+ return mBridgeProxy->SetServerCertificate(cert);
+}
+
+} // namespace mozilla
diff --git a/dom/media/eme/mediadrm/MediaDrmProxySupport.h b/dom/media/eme/mediadrm/MediaDrmProxySupport.h
new file mode 100644
index 0000000000..e994036f69
--- /dev/null
+++ b/dom/media/eme/mediadrm/MediaDrmProxySupport.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MediaDrmProxySupport_H
+#define MediaDrmProxySupport_H
+
+#include "mozilla/DecryptorProxyCallback.h"
+#include "mozilla/java/MediaDrmProxyWrappers.h"
+#include "mozilla/Logging.h"
+#include "mozilla/UniquePtr.h"
+#include "nsString.h"
+
+namespace mozilla {
+
+enum MediaDrmSessionType {
+ kKeyStreaming = 1,
+ kKeyOffline = 2,
+ kKeyRelease = 3,
+};
+
+#ifndef MDRMN_LOG
+LogModule* GetMDRMNLog();
+# define MDRMN_LOG(x, ...) \
+ MOZ_LOG(GetMDRMNLog(), mozilla::LogLevel::Debug, \
+ ("[MediaDrmProxySupport][%s]" x, __FUNCTION__, ##__VA_ARGS__))
+#endif
+
+class MediaDrmCDMCallbackProxy;
+
+class MediaDrmProxySupport final {
+ public:
+ explicit MediaDrmProxySupport(const nsAString& aKeySystem);
+ ~MediaDrmProxySupport();
+
+ /*
+ * APIs to act as GMPDecryptorAPI, discarding unnecessary calls.
+ */
+ nsresult Init(UniquePtr<MediaDrmCDMCallbackProxy>&& aCallback);
+
+ void CreateSession(uint32_t aCreateSessionToken, uint32_t aPromiseId,
+ const nsCString& aInitDataType,
+ const nsTArray<uint8_t>& aInitData,
+ MediaDrmSessionType aSessionType);
+
+ void UpdateSession(uint32_t aPromiseId, const nsCString& aSessionId,
+ const nsTArray<uint8_t>& aResponse);
+
+ void CloseSession(uint32_t aPromiseId, const nsCString& aSessionId);
+
+ void Shutdown();
+
+ const nsString& GetMediaDrmStubId() const { return mMediaDrmStubId; }
+
+ bool SetServerCertificate(const nsTArray<uint8_t>& aCert);
+
+ private:
+ const nsString mKeySystem;
+ java::MediaDrmProxy::GlobalRef mBridgeProxy;
+ java::MediaDrmProxy::NativeMediaDrmProxyCallbacks::GlobalRef mJavaCallbacks;
+ bool mDestroyed;
+ nsString mMediaDrmStubId;
+};
+
+} // namespace mozilla
+#endif // MediaDrmProxySupport_H
diff --git a/dom/media/eme/mediadrm/moz.build b/dom/media/eme/mediadrm/moz.build
new file mode 100644
index 0000000000..c8f433f25f
--- /dev/null
+++ b/dom/media/eme/mediadrm/moz.build
@@ -0,0 +1,19 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS.mozilla += [
+ "MediaDrmCDMCallbackProxy.h",
+ "MediaDrmCDMProxy.h",
+ "MediaDrmProxySupport.h",
+]
+
+UNIFIED_SOURCES += [
+ "MediaDrmCDMCallbackProxy.cpp",
+ "MediaDrmCDMProxy.cpp",
+ "MediaDrmProxySupport.cpp",
+]
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/eme/mediafoundation/WMFCDMImpl.cpp b/dom/media/eme/mediafoundation/WMFCDMImpl.cpp
new file mode 100644
index 0000000000..c2ca31bf15
--- /dev/null
+++ b/dom/media/eme/mediafoundation/WMFCDMImpl.cpp
@@ -0,0 +1,128 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WMFCDMImpl.h"
+
+#include "mozilla/dom/MediaKeySession.h"
+
+namespace mozilla {
+
+/* static */
+bool WMFCDMImpl::Supports(const nsAString& aKeySystem) {
+ static std::map<nsString, bool> supports;
+
+ nsString key(aKeySystem);
+ if (const auto& s = supports.find(key); s != supports.end()) {
+ return s->second;
+ }
+
+ RefPtr<WMFCDMImpl> cdm = MakeRefPtr<WMFCDMImpl>(aKeySystem);
+ KeySystemConfig c;
+ bool s = cdm->GetCapabilities(c);
+ supports[key] = s;
+
+ return s;
+}
+
+static void MFCDMCapabilitiesIPDLToKeySystemConfig(
+ const MFCDMCapabilitiesIPDL& aCDMConfig,
+ KeySystemConfig& aKeySystemConfig) {
+ aKeySystemConfig.mKeySystem = aCDMConfig.keySystem();
+
+ for (const auto& type : aCDMConfig.initDataTypes()) {
+ aKeySystemConfig.mInitDataTypes.AppendElement(type);
+ }
+
+ for (const auto& type : aCDMConfig.sessionTypes()) {
+ aKeySystemConfig.mSessionTypes.AppendElement(type);
+ }
+
+ for (const auto& c : aCDMConfig.videoCapabilities()) {
+ if (!c.robustness().IsEmpty() &&
+ !aKeySystemConfig.mVideoRobustness.Contains(c.robustness())) {
+ aKeySystemConfig.mVideoRobustness.AppendElement(c.robustness());
+ }
+ aKeySystemConfig.mMP4.SetCanDecryptAndDecode(
+ NS_ConvertUTF16toUTF8(c.contentType()));
+ }
+ for (const auto& c : aCDMConfig.audioCapabilities()) {
+ if (!c.robustness().IsEmpty() &&
+ !aKeySystemConfig.mAudioRobustness.Contains(c.robustness())) {
+ aKeySystemConfig.mAudioRobustness.AppendElement(c.robustness());
+ }
+ aKeySystemConfig.mMP4.SetCanDecryptAndDecode(
+ NS_ConvertUTF16toUTF8(c.contentType()));
+ }
+ aKeySystemConfig.mPersistentState = aCDMConfig.persistentState();
+ aKeySystemConfig.mDistinctiveIdentifier = aCDMConfig.distinctiveID();
+}
+
+static const char* EncryptionSchemeStr(const CryptoScheme aScheme) {
+ switch (aScheme) {
+ case CryptoScheme::None:
+ return "none";
+ case CryptoScheme::Cenc:
+ return "cenc";
+ case CryptoScheme::Cbcs:
+ return "cbcs";
+ }
+}
+
+bool WMFCDMImpl::GetCapabilities(KeySystemConfig& aConfig) {
+ nsCOMPtr<nsISerialEventTarget> backgroundTaskQueue;
+ NS_CreateBackgroundTaskQueue(__func__, getter_AddRefs(backgroundTaskQueue));
+
+ bool ok = false;
+ media::Await(
+ backgroundTaskQueue.forget(), mCDM->GetCapabilities(),
+ [&ok, &aConfig](const MFCDMCapabilitiesIPDL& capabilities) {
+ EME_LOG("capabilities: keySystem=%s",
+ NS_ConvertUTF16toUTF8(capabilities.keySystem()).get());
+ for (const auto& v : capabilities.videoCapabilities()) {
+ EME_LOG("capabilities: video=%s",
+ NS_ConvertUTF16toUTF8(v.contentType()).get());
+ }
+ for (const auto& a : capabilities.audioCapabilities()) {
+ EME_LOG("capabilities: audio=%s",
+ NS_ConvertUTF16toUTF8(a.contentType()).get());
+ }
+ for (const auto& v : capabilities.encryptionSchemes()) {
+ EME_LOG("capabilities: encryptionScheme=%s", EncryptionSchemeStr(v));
+ }
+ MFCDMCapabilitiesIPDLToKeySystemConfig(capabilities, aConfig);
+ ok = true;
+ },
+ [](nsresult rv) {
+ EME_LOG("Fail to get key system capabilities. rv=%x", rv);
+ });
+ return ok;
+}
+
+RefPtr<WMFCDMImpl::InitPromise> WMFCDMImpl::Init(
+ const WMFCDMImpl::InitParams& aParams) {
+ MOZ_ASSERT(mCDM);
+
+ RefPtr<WMFCDMImpl> self = this;
+ mCDM->Init(aParams.mOrigin, aParams.mInitDataTypes,
+ aParams.mPersistentStateRequired
+ ? KeySystemConfig::Requirement::Required
+ : KeySystemConfig::Requirement::Optional,
+ aParams.mDistinctiveIdentifierRequired
+ ? KeySystemConfig::Requirement::Required
+ : KeySystemConfig::Requirement::Optional,
+ aParams.mHWSecure, aParams.mProxyCallback)
+ ->Then(
+ mCDM->ManagerThread(), __func__,
+ [self, this](const MFCDMInitIPDL& init) {
+ mInitPromiseHolder.ResolveIfExists(true, __func__);
+ },
+ [self, this](const nsresult rv) {
+ mInitPromiseHolder.RejectIfExists(rv, __func__);
+ });
+ return mInitPromiseHolder.Ensure(__func__);
+}
+
+} // namespace mozilla
diff --git a/dom/media/eme/mediafoundation/WMFCDMImpl.h b/dom/media/eme/mediafoundation/WMFCDMImpl.h
new file mode 100644
index 0000000000..fc4ef840d6
--- /dev/null
+++ b/dom/media/eme/mediafoundation/WMFCDMImpl.h
@@ -0,0 +1,100 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_EME_MEDIAFOUNDATION_WMFCDMIMPL_H_
+#define DOM_MEDIA_EME_MEDIAFOUNDATION_WMFCDMIMPL_H_
+
+#include "MediaData.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/EMEUtils.h"
+#include "mozilla/KeySystemConfig.h"
+#include "mozilla/media/MediaUtils.h"
+#include "mozilla/MFCDMChild.h"
+#include "nsString.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+
+class WMFCDMProxyCallback;
+
+/**
+ * WMFCDMImpl is a helper class for MFCDM protocol clients. It creates, manages,
+ * and calls MFCDMChild object in the content process on behalf of the client,
+ * and performs conversion between EME and MFCDM types and constants.
+ */
+class WMFCDMImpl final {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WMFCDMImpl);
+
+ explicit WMFCDMImpl(const nsAString& aKeySystem)
+ : mCDM(MakeRefPtr<MFCDMChild>(aKeySystem)) {}
+
+ static bool Supports(const nsAString& aKeySystem);
+ // TODO: make this async?
+ bool GetCapabilities(KeySystemConfig& aConfig);
+
+ using InitPromise = GenericPromise;
+ struct InitParams {
+ nsString mOrigin;
+ CopyableTArray<nsString> mInitDataTypes;
+ bool mPersistentStateRequired;
+ bool mDistinctiveIdentifierRequired;
+ bool mHWSecure;
+ WMFCDMProxyCallback* mProxyCallback;
+ };
+
+ RefPtr<InitPromise> Init(const InitParams& aParams);
+
+ RefPtr<MFCDMChild::SessionPromise> CreateSession(
+ uint32_t aPromiseId, const KeySystemConfig::SessionType aSessionType,
+ const nsAString& aInitDataType, const nsTArray<uint8_t>& aInitData) {
+ return mCDM->CreateSessionAndGenerateRequest(aPromiseId, aSessionType,
+ aInitDataType, aInitData);
+ }
+
+ RefPtr<GenericPromise> LoadSession(
+ uint32_t aPromiseId, const KeySystemConfig::SessionType aSessionType,
+ const nsAString& aSessionId) {
+ return mCDM->LoadSession(aPromiseId, aSessionType, aSessionId);
+ }
+
+ RefPtr<GenericPromise> UpdateSession(uint32_t aPromiseId,
+ const nsAString& aSessionId,
+ nsTArray<uint8_t>& aResponse) {
+ return mCDM->UpdateSession(aPromiseId, aSessionId, aResponse);
+ }
+
+ RefPtr<GenericPromise> CloseSession(uint32_t aPromiseId,
+ const nsAString& aSessionId) {
+ return mCDM->CloseSession(aPromiseId, aSessionId);
+ }
+
+ RefPtr<GenericPromise> RemoveSession(uint32_t aPromiseId,
+ const nsAString& aSessionId) {
+ return mCDM->RemoveSession(aPromiseId, aSessionId);
+ }
+
+ uint64_t Id() {
+ MOZ_ASSERT(mCDM->Id() != 0,
+ "Should be called only after Init() is resolved");
+ return mCDM->Id();
+ }
+
+ private:
+ ~WMFCDMImpl() {
+ if (mCDM) {
+ mCDM->Shutdown();
+ }
+ };
+
+ const RefPtr<MFCDMChild> mCDM;
+
+ MozPromiseHolder<InitPromise> mInitPromiseHolder;
+};
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_EME_MEDIAFOUNDATION_WMFCDMIMPL_H_
diff --git a/dom/media/eme/mediafoundation/WMFCDMProxy.cpp b/dom/media/eme/mediafoundation/WMFCDMProxy.cpp
new file mode 100644
index 0000000000..1d691168d8
--- /dev/null
+++ b/dom/media/eme/mediafoundation/WMFCDMProxy.cpp
@@ -0,0 +1,306 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WMFCDMProxy.h"
+
+#include "mozilla/dom/MediaKeySession.h"
+#include "mozilla/WMFCDMProxyCallback.h"
+#include "WMFCDMImpl.h"
+#include "WMFCDMProxyCallback.h"
+
+namespace mozilla {
+
+#define LOG(msg, ...) \
+ EME_LOG("WMFCDMProxy[%p]@%s: " msg, this, __func__, ##__VA_ARGS__)
+
+#define RETURN_IF_SHUTDOWN() \
+ do { \
+ MOZ_ASSERT(NS_IsMainThread()); \
+ if (mIsShutdown) { \
+ return; \
+ } \
+ } while (false)
+
+#define PERFORM_ON_CDM(operation, promiseId, ...) \
+ do { \
+ mCDM->operation(promiseId, __VA_ARGS__) \
+ ->Then( \
+ mMainThread, __func__, \
+ [self = RefPtr{this}, this, promiseId]() { \
+ RETURN_IF_SHUTDOWN(); \
+ if (mKeys.IsNull()) { \
+ EME_LOG("WMFCDMProxy(this=%p, pid=%" PRIu32 \
+ ") : abort the " #operation " due to empty key", \
+ this, promiseId); \
+ return; \
+ } \
+ ResolvePromise(promiseId); \
+ }, \
+ [self = RefPtr{this}, this, promiseId]() { \
+ RETURN_IF_SHUTDOWN(); \
+ RejectPromiseWithStateError( \
+ promiseId, nsLiteralCString("WMFCDMProxy::" #operation ": " \
+ "failed to " #operation)); \
+ }); \
+ } while (false)
+
+WMFCDMProxy::WMFCDMProxy(dom::MediaKeys* aKeys, const nsAString& aKeySystem,
+ const dom::MediaKeySystemConfiguration& aConfig)
+ : CDMProxy(
+ aKeys, aKeySystem,
+ aConfig.mDistinctiveIdentifier == dom::MediaKeysRequirement::Required,
+ aConfig.mPersistentState == dom::MediaKeysRequirement::Required),
+ mConfig(aConfig) {
+ MOZ_ASSERT(NS_IsMainThread());
+}
+
+WMFCDMProxy::~WMFCDMProxy() {}
+
+void WMFCDMProxy::Init(PromiseId aPromiseId, const nsAString& aOrigin,
+ const nsAString& aTopLevelOrigin,
+ const nsAString& aName) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!aOrigin.IsEmpty());
+
+ MOZ_ASSERT(!mOwnerThread);
+ if (NS_FAILED(
+ NS_NewNamedThread("WMFCDMThread", getter_AddRefs(mOwnerThread)))) {
+ RejectPromiseWithStateError(
+ aPromiseId,
+ nsLiteralCString("WMFCDMProxy::Init: couldn't create CDM thread"));
+ return;
+ }
+
+ mCDM = MakeRefPtr<WMFCDMImpl>(mKeySystem);
+ mProxyCallback = new WMFCDMProxyCallback(this);
+ WMFCDMImpl::InitParams params{nsString(aOrigin),
+ mConfig.mInitDataTypes,
+ mPersistentStateRequired,
+ mDistinctiveIdentifierRequired,
+ false /* TODO : support HW secure */,
+ mProxyCallback};
+ mCDM->Init(params)->Then(
+ mMainThread, __func__,
+ [self = RefPtr{this}, this, aPromiseId](const bool) {
+ MOZ_ASSERT(mCDM->Id() > 0);
+ mKeys->OnCDMCreated(aPromiseId, mCDM->Id());
+ },
+ [self = RefPtr{this}, this, aPromiseId](const nsresult rv) {
+ RejectPromiseWithStateError(
+ aPromiseId,
+ nsLiteralCString("WMFCDMProxy::Init: WMFCDM init error"));
+ });
+}
+
+void WMFCDMProxy::ResolvePromise(PromiseId aId) {
+ auto resolve = [self = RefPtr{this}, this, aId]() {
+ RETURN_IF_SHUTDOWN();
+ EME_LOG("WMFCDMProxy::ResolvePromise(this=%p, pid=%" PRIu32 ")", this, aId);
+ if (!mKeys.IsNull()) {
+ mKeys->ResolvePromise(aId);
+ } else {
+ NS_WARNING("WMFCDMProxy unable to resolve promise!");
+ }
+ };
+
+ if (NS_IsMainThread()) {
+ resolve();
+ return;
+ }
+ mMainThread->Dispatch(
+ NS_NewRunnableFunction("WMFCDMProxy::ResolvePromise", resolve));
+}
+
+void WMFCDMProxy::RejectPromise(PromiseId aId, ErrorResult&& aException,
+ const nsCString& aReason) {
+ if (!NS_IsMainThread()) {
+ // Use CopyableErrorResult to store our exception in the runnable,
+ // because ErrorResult is not OK to move across threads.
+ mMainThread->Dispatch(
+ NewRunnableMethod<PromiseId, StoreCopyPassByRRef<CopyableErrorResult>,
+ nsCString>("WMFCDMProxy::RejectPromise", this,
+ &WMFCDMProxy::RejectPromiseOnMainThread,
+ aId, std::move(aException), aReason),
+ NS_DISPATCH_NORMAL);
+ return;
+ }
+ EME_LOG("WMFCDMProxy::RejectPromise(this=%p, pid=%" PRIu32
+ ", code=0x%x, "
+ "reason='%s')",
+ this, aId, aException.ErrorCodeAsInt(), aReason.get());
+ if (!mKeys.IsNull()) {
+ mKeys->RejectPromise(aId, std::move(aException), aReason);
+ } else {
+ // We don't have a MediaKeys object to pass the exception to, so silence
+ // the exception to avoid it asserting due to being unused.
+ aException.SuppressException();
+ }
+}
+
+void WMFCDMProxy::RejectPromiseOnMainThread(PromiseId aId,
+ CopyableErrorResult&& aException,
+ const nsCString& aReason) {
+ RETURN_IF_SHUTDOWN();
+ // Moving into or out of a non-copyable ErrorResult will assert that both
+ // ErorResults are from our current thread. Avoid the assertion by moving
+ // into a current-thread CopyableErrorResult first. Note that this is safe,
+ // because CopyableErrorResult never holds state that can't move across
+ // threads.
+ CopyableErrorResult rv(std::move(aException));
+ RejectPromise(aId, std::move(rv), aReason);
+}
+
+void WMFCDMProxy::RejectPromiseWithStateError(PromiseId aId,
+ const nsCString& aReason) {
+ ErrorResult rv;
+ rv.ThrowInvalidStateError(aReason);
+ RejectPromise(aId, std::move(rv), aReason);
+}
+
+void WMFCDMProxy::CreateSession(uint32_t aCreateSessionToken,
+ MediaKeySessionType aSessionType,
+ PromiseId aPromiseId,
+ const nsAString& aInitDataType,
+ nsTArray<uint8_t>& aInitData) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RETURN_IF_SHUTDOWN();
+ const auto sessionType = ConvertToKeySystemConfigSessionType(aSessionType);
+ EME_LOG("WMFCDMProxy::CreateSession(this=%p, pid=%" PRIu32
+ "), sessionType=%s",
+ this, aPromiseId, SessionTypeToStr(sessionType));
+ mCDM->CreateSession(aPromiseId, sessionType, aInitDataType, aInitData)
+ ->Then(
+ mMainThread, __func__,
+ [self = RefPtr{this}, this, aCreateSessionToken,
+ aPromiseId](nsString sessionID) {
+ RETURN_IF_SHUTDOWN();
+ if (mKeys.IsNull()) {
+ EME_LOG("WMFCDMProxy(this=%p, pid=%" PRIu32
+ ") : abort the create session due to "
+ "empty key",
+ this, aPromiseId);
+ return;
+ }
+ if (RefPtr<dom::MediaKeySession> session =
+ mKeys->GetPendingSession(aCreateSessionToken)) {
+ session->SetSessionId(std::move(sessionID));
+ }
+ ResolvePromise(aPromiseId);
+ },
+ [self = RefPtr{this}, this, aPromiseId]() {
+ RETURN_IF_SHUTDOWN();
+ RejectPromiseWithStateError(
+ aPromiseId,
+ nsLiteralCString(
+ "WMFCDMProxy::CreateSession: cannot create session"));
+ });
+}
+
+void WMFCDMProxy::LoadSession(PromiseId aPromiseId,
+ dom::MediaKeySessionType aSessionType,
+ const nsAString& aSessionId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RETURN_IF_SHUTDOWN();
+ const auto sessionType = ConvertToKeySystemConfigSessionType(aSessionType);
+ EME_LOG("WMFCDMProxy::LoadSession(this=%p, pid=%" PRIu32
+ "), sessionType=%s, sessionId=%s",
+ this, aPromiseId, SessionTypeToStr(sessionType),
+ NS_ConvertUTF16toUTF8(aSessionId).get());
+ PERFORM_ON_CDM(LoadSession, aPromiseId, sessionType, aSessionId);
+}
+
+void WMFCDMProxy::UpdateSession(const nsAString& aSessionId,
+ PromiseId aPromiseId,
+ nsTArray<uint8_t>& aResponse) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RETURN_IF_SHUTDOWN();
+ EME_LOG("WMFCDMProxy::UpdateSession(this=%p, pid=%" PRIu32
+ "), sessionId=%s, responseLen=%zu",
+ this, aPromiseId, NS_ConvertUTF16toUTF8(aSessionId).get(),
+ aResponse.Length());
+ PERFORM_ON_CDM(UpdateSession, aPromiseId, aSessionId, aResponse);
+}
+
+void WMFCDMProxy::CloseSession(const nsAString& aSessionId,
+ PromiseId aPromiseId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RETURN_IF_SHUTDOWN();
+ EME_LOG("WMFCDMProxy::CloseSession(this=%p, pid=%" PRIu32 "), sessionId=%s",
+ this, aPromiseId, NS_ConvertUTF16toUTF8(aSessionId).get());
+ PERFORM_ON_CDM(CloseSession, aPromiseId, aSessionId);
+}
+
+void WMFCDMProxy::RemoveSession(const nsAString& aSessionId,
+ PromiseId aPromiseId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RETURN_IF_SHUTDOWN();
+ EME_LOG("WMFCDMProxy::RemoveSession(this=%p, pid=%" PRIu32 "), sessionId=%s",
+ this, aPromiseId, NS_ConvertUTF16toUTF8(aSessionId).get());
+ PERFORM_ON_CDM(RemoveSession, aPromiseId, aSessionId);
+}
+
+void WMFCDMProxy::Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!mIsShutdown);
+ if (mProxyCallback) {
+ mProxyCallback->Shutdown();
+ mProxyCallback = nullptr;
+ }
+ mIsShutdown = true;
+}
+
+void WMFCDMProxy::OnSessionMessage(const nsAString& aSessionId,
+ dom::MediaKeyMessageType aMessageType,
+ const nsTArray<uint8_t>& aMessage) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RETURN_IF_SHUTDOWN();
+ if (mKeys.IsNull()) {
+ return;
+ }
+ if (RefPtr<dom::MediaKeySession> session = mKeys->GetSession(aSessionId)) {
+ LOG("Notify key message for session Id=%s",
+ NS_ConvertUTF16toUTF8(aSessionId).get());
+ session->DispatchKeyMessage(aMessageType, aMessage);
+ }
+}
+
+void WMFCDMProxy::OnKeyStatusesChange(const nsAString& aSessionId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RETURN_IF_SHUTDOWN();
+ if (mKeys.IsNull()) {
+ return;
+ }
+ if (RefPtr<dom::MediaKeySession> session = mKeys->GetSession(aSessionId)) {
+ LOG("Notify key statuses for session Id=%s",
+ NS_ConvertUTF16toUTF8(aSessionId).get());
+ session->DispatchKeyStatusesChange();
+ }
+}
+
+void WMFCDMProxy::OnExpirationChange(const nsAString& aSessionId,
+ UnixTime aExpiryTime) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RETURN_IF_SHUTDOWN();
+ if (mKeys.IsNull()) {
+ return;
+ }
+ if (RefPtr<dom::MediaKeySession> session = mKeys->GetSession(aSessionId)) {
+ LOG("Notify expiration for session Id=%s",
+ NS_ConvertUTF16toUTF8(aSessionId).get());
+ session->SetExpiration(static_cast<double>(aExpiryTime));
+ }
+}
+
+uint64_t WMFCDMProxy::GetCDMProxyId() const {
+ MOZ_DIAGNOSTIC_ASSERT(mCDM);
+ return mCDM->Id();
+}
+
+#undef LOG
+#undef RETURN_IF_SHUTDOWN
+#undef PERFORM_ON_CDM
+
+} // namespace mozilla
diff --git a/dom/media/eme/mediafoundation/WMFCDMProxy.h b/dom/media/eme/mediafoundation/WMFCDMProxy.h
new file mode 100644
index 0000000000..7766b73f21
--- /dev/null
+++ b/dom/media/eme/mediafoundation/WMFCDMProxy.h
@@ -0,0 +1,134 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_EME_MEDIAFOUNDATION_WMFCDMPROXY_H_
+#define DOM_MEDIA_EME_MEDIAFOUNDATION_WMFCDMPROXY_H_
+
+#include "mozilla/CDMProxy.h"
+#include "mozilla/CDMCaps.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/dom/MediaKeys.h"
+#include "mozilla/dom/MediaKeySession.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/WMFCDMImpl.h"
+
+#include "nsString.h"
+
+namespace mozilla {
+
+class WMFCDMProxyCallback;
+
+class WMFCDMProxy : public CDMProxy {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WMFCDMProxy, override)
+
+ WMFCDMProxy(dom::MediaKeys* aKeys, const nsAString& aKeySystem,
+ const dom::MediaKeySystemConfiguration& aConfig);
+
+ // CDMProxy interface
+ void Init(PromiseId aPromiseId, const nsAString& aOrigin,
+ const nsAString& aTopLevelOrigin, const nsAString& aName) override;
+
+ void CreateSession(uint32_t aCreateSessionToken,
+ MediaKeySessionType aSessionType, PromiseId aPromiseId,
+ const nsAString& aInitDataType,
+ nsTArray<uint8_t>& aInitData) override;
+
+ void LoadSession(PromiseId aPromiseId, dom::MediaKeySessionType aSessionType,
+ const nsAString& aSessionId) override;
+
+ void SetServerCertificate(PromiseId aPromiseId,
+ nsTArray<uint8_t>& aCert) override {}
+
+ void UpdateSession(const nsAString& aSessionId, PromiseId aPromiseId,
+ nsTArray<uint8_t>& aResponse) override;
+
+ void CloseSession(const nsAString& aSessionId, PromiseId aPromiseId) override;
+
+ void RemoveSession(const nsAString& aSessionId,
+ PromiseId aPromiseId) override;
+
+ void QueryOutputProtectionStatus() override {}
+
+ void NotifyOutputProtectionStatus(
+ OutputProtectionCheckStatus aCheckStatus,
+ OutputProtectionCaptureStatus aCaptureStatus) override {}
+
+ void Shutdown() override;
+
+ void Terminated() override {}
+
+ void OnSetSessionId(uint32_t aCreateSessionToken,
+ const nsAString& aSessionId) override {}
+
+ void OnResolveLoadSessionPromise(uint32_t aPromiseId,
+ bool aSuccess) override {}
+
+ void OnSessionMessage(const nsAString& aSessionId,
+ dom::MediaKeyMessageType aMessageType,
+ const nsTArray<uint8_t>& aMessage) override;
+
+ void OnExpirationChange(const nsAString& aSessionId,
+ UnixTime aExpiryTime) override;
+
+ void OnSessionClosed(const nsAString& aSessionId) override {}
+
+ void OnSessionError(const nsAString& aSessionId, nsresult aException,
+ uint32_t aSystemCode, const nsAString& aMsg) override {}
+
+ void OnRejectPromise(uint32_t aPromiseId, ErrorResult&& aException,
+ const nsCString& aMsg) override {}
+
+ RefPtr<DecryptPromise> Decrypt(MediaRawData* aSample) override {
+ return nullptr;
+ }
+ void OnDecrypted(uint32_t aId, DecryptStatus aResult,
+ const nsTArray<uint8_t>& aDecryptedData) override {}
+
+ void ResolvePromise(PromiseId aId) override;
+ void RejectPromise(PromiseId aId, ErrorResult&& aException,
+ const nsCString& aReason) override;
+
+ void OnKeyStatusesChange(const nsAString& aSessionId) override;
+
+ void GetStatusForPolicy(PromiseId aPromiseId,
+ const nsAString& aMinHdcpVersion) override {}
+
+#ifdef DEBUG
+ bool IsOnOwnerThread() override {
+ return NS_GetCurrentThread() == mOwnerThread;
+ }
+#endif
+
+ WMFCDMProxy* AsWMFCDMProxy() override { return this; }
+
+ // Can only be called after initialization succeeded.
+ uint64_t GetCDMProxyId() const;
+
+ private:
+ virtual ~WMFCDMProxy();
+
+ template <typename T>
+ void ResolvePromiseWithResult(PromiseId aId, const T& aResult) {}
+ void RejectPromiseOnMainThread(PromiseId aId,
+ CopyableErrorResult&& aException,
+ const nsCString& aReason);
+ // Reject promise with an InvalidStateError and the given message.
+ void RejectPromiseWithStateError(PromiseId aId, const nsCString& aReason);
+
+ RefPtr<WMFCDMImpl> mCDM;
+
+ const dom::MediaKeySystemConfiguration mConfig;
+
+ RefPtr<WMFCDMProxyCallback> mProxyCallback;
+
+ // It can only be used on the main thread.
+ bool mIsShutdown = false;
+};
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_EME_MEDIAFOUNDATION_WMFCDMPROXY_H_
diff --git a/dom/media/eme/mediafoundation/WMFCDMProxyCallback.cpp b/dom/media/eme/mediafoundation/WMFCDMProxyCallback.cpp
new file mode 100644
index 0000000000..6464cec4c7
--- /dev/null
+++ b/dom/media/eme/mediafoundation/WMFCDMProxyCallback.cpp
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WMFCDMProxyCallback.h"
+
+#include "mozilla/WMFCDMProxy.h"
+
+#define RETURN_IF_NULL(proxy) \
+ if (!proxy) { \
+ return; \
+ }
+
+namespace mozilla {
+
+WMFCDMProxyCallback::WMFCDMProxyCallback(WMFCDMProxy* aProxy) : mProxy(aProxy) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mProxy);
+}
+
+WMFCDMProxyCallback::~WMFCDMProxyCallback() { MOZ_ASSERT(!mProxy); }
+
+void WMFCDMProxyCallback::OnSessionMessage(const MFCDMKeyMessage& aMessage) {
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "WMFCDMProxyCallback::OnSessionMessage",
+ [self = RefPtr{this}, this, message = aMessage]() {
+ RETURN_IF_NULL(mProxy);
+ mProxy->OnSessionMessage(message.sessionId(), message.type(),
+ std::move(message.message()));
+ }));
+}
+
+void WMFCDMProxyCallback::OnSessionKeyStatusesChange(
+ const MFCDMKeyStatusChange& aKeyStatuses) {
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "WMFCDMProxyCallback::OnSessionKeyStatusesChange",
+ [self = RefPtr{this}, this, keyStatuses = aKeyStatuses]() {
+ RETURN_IF_NULL(mProxy);
+ bool keyStatusesChange = false;
+ {
+ auto caps = mProxy->Capabilites().Lock();
+ for (const auto& keyInfo : keyStatuses.keyInfo()) {
+ keyStatusesChange |= caps->SetKeyStatus(
+ keyInfo.keyId(), keyStatuses.sessionId(),
+ dom::Optional<dom::MediaKeyStatus>(keyInfo.status()));
+ }
+ }
+ if (keyStatusesChange) {
+ mProxy->OnKeyStatusesChange(keyStatuses.sessionId());
+ }
+ }));
+}
+
+void WMFCDMProxyCallback::OnSessionKeyExpiration(
+ const MFCDMKeyExpiration& aExpiration) {
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "WMFCDMProxyCallback::OnSessionKeyExpiration",
+ [self = RefPtr{this}, this, expiration = aExpiration]() {
+ RETURN_IF_NULL(mProxy);
+ mProxy->OnExpirationChange(
+ expiration.sessionId(),
+ expiration.expiredTimeMilliSecondsSinceEpoch());
+ }));
+}
+
+void WMFCDMProxyCallback::Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ mProxy = nullptr;
+}
+
+#undef RETURN_IF_NULL
+} // namespace mozilla
diff --git a/dom/media/eme/mediafoundation/WMFCDMProxyCallback.h b/dom/media/eme/mediafoundation/WMFCDMProxyCallback.h
new file mode 100644
index 0000000000..6e76f63ccc
--- /dev/null
+++ b/dom/media/eme/mediafoundation/WMFCDMProxyCallback.h
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_EME_MEDIAFOUNDATION_WMFCDMPROXYCALLBACK_H_
+#define DOM_MEDIA_EME_MEDIAFOUNDATION_WMFCDMPROXYCALLBACK_H_
+
+#include "mozilla/PMFCDM.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+
+class WMFCDMProxy;
+
+// This class is used to notify CDM related events called from MFCDMChild, and
+// it will forward the relative calls to WMFCDMProxy on the main thread.
+class WMFCDMProxyCallback final {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WMFCDMProxyCallback);
+
+ explicit WMFCDMProxyCallback(WMFCDMProxy* aProxy);
+
+ void OnSessionMessage(const MFCDMKeyMessage& aMessage);
+
+ void OnSessionKeyStatusesChange(const MFCDMKeyStatusChange& aKeyStatuses);
+
+ void OnSessionKeyExpiration(const MFCDMKeyExpiration& aExpiration);
+
+ void Shutdown();
+
+ private:
+ ~WMFCDMProxyCallback();
+ RefPtr<WMFCDMProxy> mProxy;
+};
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_EME_MEDIAFOUNDATION_WMFCDMPROXYCALLBACK_H_
diff --git a/dom/media/eme/mediafoundation/moz.build b/dom/media/eme/mediafoundation/moz.build
new file mode 100644
index 0000000000..8c54b381ff
--- /dev/null
+++ b/dom/media/eme/mediafoundation/moz.build
@@ -0,0 +1,21 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS.mozilla += [
+ "WMFCDMImpl.h",
+ "WMFCDMProxy.h",
+ "WMFCDMProxyCallback.h",
+]
+
+UNIFIED_SOURCES += [
+ "WMFCDMImpl.cpp",
+ "WMFCDMProxy.cpp",
+ "WMFCDMProxyCallback.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/eme/moz.build b/dom/media/eme/moz.build
new file mode 100644
index 0000000000..76059a2790
--- /dev/null
+++ b/dom/media/eme/moz.build
@@ -0,0 +1,54 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS.mozilla.dom += [
+ "KeySystemNames.h",
+ "MediaEncryptedEvent.h",
+ "MediaKeyError.h",
+ "MediaKeyMessageEvent.h",
+ "MediaKeys.h",
+ "MediaKeySession.h",
+ "MediaKeyStatusMap.h",
+ "MediaKeySystemAccess.h",
+ "MediaKeySystemAccessManager.h",
+ "MediaKeySystemAccessPermissionRequest.h",
+]
+
+EXPORTS.mozilla += [
+ "CDMCaps.h",
+ "CDMProxy.h",
+ "DecryptorProxyCallback.h",
+ "DetailedPromise.h",
+ "EMEUtils.h",
+ "KeySystemConfig.h",
+]
+
+UNIFIED_SOURCES += [
+ "CDMCaps.cpp",
+ "DetailedPromise.cpp",
+ "EMEUtils.cpp",
+ "KeySystemConfig.cpp",
+ "MediaEncryptedEvent.cpp",
+ "MediaKeyError.cpp",
+ "MediaKeyMessageEvent.cpp",
+ "MediaKeys.cpp",
+ "MediaKeySession.cpp",
+ "MediaKeyStatusMap.cpp",
+ "MediaKeySystemAccess.cpp",
+ "MediaKeySystemAccessManager.cpp",
+ "MediaKeySystemAccessPermissionRequest.cpp",
+]
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "android":
+ DIRS += ["mediadrm"]
+ LOCAL_INCLUDES += ["/dom/media/platforms/android"]
+
+if CONFIG["MOZ_WMF_CDM"]:
+ DIRS += ["mediafoundation"]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/encoder/ContainerWriter.h b/dom/media/encoder/ContainerWriter.h
new file mode 100644
index 0000000000..724c8b90c9
--- /dev/null
+++ b/dom/media/encoder/ContainerWriter.h
@@ -0,0 +1,75 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef ContainerWriter_h_
+#define ContainerWriter_h_
+
+#include "nsTArray.h"
+#include "EncodedFrame.h"
+#include "TrackMetadataBase.h"
+
+namespace mozilla {
+/**
+ * ContainerWriter packs encoded track data into a specific media container.
+ */
+class ContainerWriter {
+ public:
+ ContainerWriter() : mInitialized(false), mIsWritingComplete(false) {}
+ virtual ~ContainerWriter() {}
+ // Mapping to DOMMediaStream::TrackTypeHints
+ enum {
+ CREATE_AUDIO_TRACK = 1 << 0,
+ CREATE_VIDEO_TRACK = 1 << 1,
+ };
+ enum { END_OF_STREAM = 1 << 0 };
+
+ /**
+ * Writes encoded track data from aData into the internal stream of container
+ * writer. aFlags is used to signal the impl of different conditions
+ * such as END_OF_STREAM. Each impl may handle different flags, and should be
+ * documented accordingly. Currently, WriteEncodedTrack doesn't support
+ * explicit track specification, though each impl may provide logic to
+ * allocate frames into different tracks.
+ */
+ virtual nsresult WriteEncodedTrack(
+ const nsTArray<RefPtr<EncodedFrame>>& aData, uint32_t aFlags = 0) = 0;
+
+ /**
+ * Stores the metadata for all given tracks to the muxer.
+ *
+ * This method checks the integrity of aMetadata.
+ * If the metadata isn't well formatted, this method returns NS_ERROR_FAILURE.
+ * If the metadata is well formatted, it stores the metadata and returns
+ * NS_OK.
+ */
+ virtual nsresult SetMetadata(
+ const nsTArray<RefPtr<TrackMetadataBase>>& aMetadata) = 0;
+
+ /**
+ * Indicate if the writer has finished to output data
+ */
+ virtual bool IsWritingComplete() { return mIsWritingComplete; }
+
+ enum { FLUSH_NEEDED = 1 << 0, GET_HEADER = 1 << 1 };
+
+ /**
+ * Copies the final container data to a buffer if it has accumulated enough
+ * packets from WriteEncodedTrack. This buffer of data is appended to
+ * aOutputBufs, and existing elements of aOutputBufs should not be modified.
+ * aFlags is true with FLUSH_NEEDED will force OggWriter to flush an ogg page
+ * even it is not full, and copy these container data to a buffer for
+ * aOutputBufs to append.
+ */
+ virtual nsresult GetContainerData(nsTArray<nsTArray<uint8_t>>* aOutputBufs,
+ uint32_t aFlags = 0) = 0;
+
+ protected:
+ bool mInitialized;
+ bool mIsWritingComplete;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/encoder/EncodedFrame.h b/dom/media/encoder/EncodedFrame.h
new file mode 100644
index 0000000000..e76babef89
--- /dev/null
+++ b/dom/media/encoder/EncodedFrame.h
@@ -0,0 +1,64 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef EncodedFrame_h_
+#define EncodedFrame_h_
+
+#include "nsISupportsImpl.h"
+#include "mozilla/media/MediaUtils.h"
+#include "TimeUnits.h"
+#include "VideoUtils.h"
+
+namespace mozilla {
+
+// Represent an encoded frame emitted by an encoder
+class EncodedFrame final {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(EncodedFrame)
+ public:
+ enum FrameType {
+ VP8_I_FRAME, // VP8 intraframe
+ VP8_P_FRAME, // VP8 predicted frame
+ OPUS_AUDIO_FRAME, // Opus audio frame
+ UNKNOWN // FrameType not set
+ };
+ using ConstFrameData = const media::Refcountable<nsTArray<uint8_t>>;
+ using FrameData = media::Refcountable<nsTArray<uint8_t>>;
+ EncodedFrame(const media::TimeUnit& aTime, uint64_t aDuration,
+ uint64_t aDurationBase, FrameType aFrameType,
+ RefPtr<ConstFrameData> aData)
+ : mTime(aTime),
+ mDuration(aDuration),
+ mDurationBase(aDurationBase),
+ mFrameType(aFrameType),
+ mFrameData(std::move(aData)) {
+ MOZ_ASSERT(mFrameData);
+ MOZ_ASSERT_IF(mFrameType == VP8_I_FRAME, mDurationBase == PR_USEC_PER_SEC);
+ MOZ_ASSERT_IF(mFrameType == VP8_P_FRAME, mDurationBase == PR_USEC_PER_SEC);
+ MOZ_ASSERT_IF(mFrameType == OPUS_AUDIO_FRAME, mDurationBase == 48000);
+ }
+ // Timestamp in microseconds
+ const media::TimeUnit mTime;
+ // The playback duration of this packet in mDurationBase.
+ const uint64_t mDuration;
+ // The time base of mDuration.
+ const uint64_t mDurationBase;
+ // Represent what is in the FrameData
+ const FrameType mFrameType;
+ // Encoded data
+ const RefPtr<ConstFrameData> mFrameData;
+
+ // The end time of the frame in microseconds.
+ media::TimeUnit GetEndTime() const {
+ return mTime + media::TimeUnit(mDuration, mDurationBase);
+ }
+
+ private:
+ // Private destructor, to discourage deletion outside of Release():
+ ~EncodedFrame() = default;
+};
+
+} // namespace mozilla
+
+#endif // EncodedFrame_h_
diff --git a/dom/media/encoder/MediaEncoder.cpp b/dom/media/encoder/MediaEncoder.cpp
new file mode 100644
index 0000000000..4eca742c77
--- /dev/null
+++ b/dom/media/encoder/MediaEncoder.cpp
@@ -0,0 +1,1142 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaEncoder.h"
+
+#include <algorithm>
+#include "AudioNodeEngine.h"
+#include "AudioNodeTrack.h"
+#include "DriftCompensation.h"
+#include "MediaDecoder.h"
+#include "MediaTrackGraphImpl.h"
+#include "MediaTrackListener.h"
+#include "mozilla/dom/AudioNode.h"
+#include "mozilla/dom/AudioStreamTrack.h"
+#include "mozilla/dom/Blob.h"
+#include "mozilla/dom/BlobImpl.h"
+#include "mozilla/dom/MediaStreamTrack.h"
+#include "mozilla/dom/MutableBlobStorage.h"
+#include "mozilla/dom/VideoStreamTrack.h"
+#include "mozilla/gfx/Point.h" // IntSize
+#include "mozilla/Logging.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/TaskQueue.h"
+#include "mozilla/Unused.h"
+#include "Muxer.h"
+#include "nsMimeTypes.h"
+#include "nsThreadUtils.h"
+#include "OggWriter.h"
+#include "OpusTrackEncoder.h"
+#include "TimeUnits.h"
+#include "Tracing.h"
+
+#include "VP8TrackEncoder.h"
+#include "WebMWriter.h"
+
+mozilla::LazyLogModule gMediaEncoderLog("MediaEncoder");
+#define LOG(type, msg) MOZ_LOG(gMediaEncoderLog, type, msg)
+
+namespace mozilla {
+
+using namespace dom;
+using namespace media;
+
+namespace {
+class BlobStorer : public MutableBlobStorageCallback {
+ MozPromiseHolder<MediaEncoder::BlobPromise> mHolder;
+
+ virtual ~BlobStorer() = default;
+
+ public:
+ BlobStorer() = default;
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(BlobStorer, override)
+
+ void BlobStoreCompleted(MutableBlobStorage*, BlobImpl* aBlobImpl,
+ nsresult aRv) override {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (NS_FAILED(aRv)) {
+ mHolder.Reject(aRv, __func__);
+ return;
+ }
+
+ mHolder.Resolve(aBlobImpl, __func__);
+ }
+
+ RefPtr<MediaEncoder::BlobPromise> Promise() {
+ return mHolder.Ensure(__func__);
+ }
+};
+} // namespace
+
+class MediaEncoder::AudioTrackListener : public DirectMediaTrackListener {
+ public:
+ AudioTrackListener(RefPtr<DriftCompensator> aDriftCompensator,
+ RefPtr<MediaEncoder> aMediaEncoder)
+ : mDirectConnected(false),
+ mInitialized(false),
+ mRemoved(false),
+ mDriftCompensator(std::move(aDriftCompensator)),
+ mMediaEncoder(std::move(aMediaEncoder)),
+ mEncoderThread(mMediaEncoder->mEncoderThread),
+ mShutdownPromise(mShutdownHolder.Ensure(__func__)) {
+ MOZ_ASSERT(mMediaEncoder);
+ MOZ_ASSERT(mMediaEncoder->mAudioEncoder);
+ MOZ_ASSERT(mEncoderThread);
+ }
+
+ void NotifyDirectListenerInstalled(InstallationResult aResult) override {
+ if (aResult == InstallationResult::SUCCESS) {
+ LOG(LogLevel::Info, ("Audio track direct listener installed"));
+ mDirectConnected = true;
+ } else {
+ LOG(LogLevel::Info, ("Audio track failed to install direct listener"));
+ MOZ_ASSERT(!mDirectConnected);
+ }
+ }
+
+ void NotifyDirectListenerUninstalled() override {
+ mDirectConnected = false;
+
+ if (mRemoved) {
+ mMediaEncoder = nullptr;
+ mEncoderThread = nullptr;
+ }
+ }
+
+ void NotifyQueuedChanges(MediaTrackGraph* aGraph, TrackTime aTrackOffset,
+ const MediaSegment& aQueuedMedia) override {
+ TRACE_COMMENT("MediaEncoder::NotifyQueuedChanges", "%p",
+ mMediaEncoder.get());
+ MOZ_ASSERT(mMediaEncoder);
+ MOZ_ASSERT(mEncoderThread);
+
+ if (!mInitialized) {
+ mDriftCompensator->NotifyAudioStart(TimeStamp::Now());
+ mInitialized = true;
+ }
+
+ mDriftCompensator->NotifyAudio(aQueuedMedia.GetDuration());
+
+ const AudioSegment& audio = static_cast<const AudioSegment&>(aQueuedMedia);
+
+ AudioSegment copy;
+ copy.AppendSlice(audio, 0, audio.GetDuration());
+
+ nsresult rv = mEncoderThread->Dispatch(NS_NewRunnableFunction(
+ "mozilla::AudioTrackEncoder::AppendAudioSegment",
+ [encoder = mMediaEncoder, copy = std::move(copy)]() mutable {
+ encoder->mAudioEncoder->AppendAudioSegment(std::move(copy));
+ }));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ }
+
+ void NotifyEnded(MediaTrackGraph* aGraph) override {
+ MOZ_ASSERT(mMediaEncoder);
+ MOZ_ASSERT(mMediaEncoder->mAudioEncoder);
+ MOZ_ASSERT(mEncoderThread);
+
+ nsresult rv = mEncoderThread->Dispatch(
+ NS_NewRunnableFunction("mozilla::AudioTrackEncoder::NotifyEndOfStream",
+ [encoder = mMediaEncoder] {
+ encoder->mAudioEncoder->NotifyEndOfStream();
+ }));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ }
+
+ void NotifyRemoved(MediaTrackGraph* aGraph) override {
+ nsresult rv = mEncoderThread->Dispatch(
+ NS_NewRunnableFunction("mozilla::AudioTrackEncoder::NotifyEndOfStream",
+ [encoder = mMediaEncoder] {
+ encoder->mAudioEncoder->NotifyEndOfStream();
+ }));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+
+ mRemoved = true;
+
+ if (!mDirectConnected) {
+ mMediaEncoder = nullptr;
+ mEncoderThread = nullptr;
+ }
+
+ mShutdownHolder.Resolve(true, __func__);
+ }
+
+ const RefPtr<GenericNonExclusivePromise>& OnShutdown() const {
+ return mShutdownPromise;
+ }
+
+ private:
+ bool mDirectConnected;
+ bool mInitialized;
+ bool mRemoved;
+ const RefPtr<DriftCompensator> mDriftCompensator;
+ RefPtr<MediaEncoder> mMediaEncoder;
+ RefPtr<TaskQueue> mEncoderThread;
+ MozPromiseHolder<GenericNonExclusivePromise> mShutdownHolder;
+ const RefPtr<GenericNonExclusivePromise> mShutdownPromise;
+};
+
+class MediaEncoder::VideoTrackListener : public DirectMediaTrackListener {
+ public:
+ explicit VideoTrackListener(RefPtr<MediaEncoder> aMediaEncoder)
+ : mDirectConnected(false),
+ mInitialized(false),
+ mRemoved(false),
+ mPendingAdvanceCurrentTime(false),
+ mMediaEncoder(std::move(aMediaEncoder)),
+ mEncoderThread(mMediaEncoder->mEncoderThread),
+ mShutdownPromise(mShutdownHolder.Ensure(__func__)) {
+ MOZ_ASSERT(mMediaEncoder);
+ MOZ_ASSERT(mEncoderThread);
+ }
+
+ void NotifyDirectListenerInstalled(InstallationResult aResult) override {
+ if (aResult == InstallationResult::SUCCESS) {
+ LOG(LogLevel::Info, ("Video track direct listener installed"));
+ mDirectConnected = true;
+ } else {
+ LOG(LogLevel::Info, ("Video track failed to install direct listener"));
+ MOZ_ASSERT(!mDirectConnected);
+ return;
+ }
+ }
+
+ void NotifyDirectListenerUninstalled() override {
+ mDirectConnected = false;
+
+ if (mRemoved) {
+ mMediaEncoder = nullptr;
+ mEncoderThread = nullptr;
+ }
+ }
+
+ void NotifyQueuedChanges(MediaTrackGraph* aGraph, TrackTime aTrackOffset,
+ const MediaSegment& aQueuedMedia) override {
+ TRACE_COMMENT("MediaEncoder::NotifyQueuedChanges", "%p",
+ mMediaEncoder.get());
+ MOZ_ASSERT(mMediaEncoder);
+ MOZ_ASSERT(mMediaEncoder->mVideoEncoder);
+ MOZ_ASSERT(mEncoderThread);
+
+ mCurrentTime = TimeStamp::Now();
+ if (!mInitialized) {
+ nsresult rv = mEncoderThread->Dispatch(
+ NS_NewRunnableFunction("mozilla::VideoTrackEncoder::SetStartOffset",
+ [encoder = mMediaEncoder, now = mCurrentTime] {
+ encoder->mVideoEncoder->SetStartOffset(now);
+ }));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ mInitialized = true;
+ }
+
+ if (!mPendingAdvanceCurrentTime) {
+ mPendingAdvanceCurrentTime = true;
+ nsresult rv = mEncoderThread->Dispatch(NS_NewRunnableFunction(
+ "mozilla::VideoTrackEncoder::AdvanceCurrentTime",
+ [encoder = mMediaEncoder, now = mCurrentTime] {
+ encoder->mVideoListener->mPendingAdvanceCurrentTime = false;
+ encoder->mVideoEncoder->AdvanceCurrentTime(now);
+ }));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ }
+ }
+
+ void NotifyRealtimeTrackData(MediaTrackGraph* aGraph, TrackTime aTrackOffset,
+ const MediaSegment& aMedia) override {
+ TRACE_COMMENT("MediaEncoder::NotifyRealtimeTrackData", "%p",
+ mMediaEncoder.get());
+ MOZ_ASSERT(mMediaEncoder);
+ MOZ_ASSERT(mMediaEncoder->mVideoEncoder);
+ MOZ_ASSERT(mEncoderThread);
+ MOZ_ASSERT(aMedia.GetType() == MediaSegment::VIDEO);
+
+ const VideoSegment& video = static_cast<const VideoSegment&>(aMedia);
+ VideoSegment copy;
+ for (VideoSegment::ConstChunkIterator iter(video); !iter.IsEnded();
+ iter.Next()) {
+ copy.AppendFrame(do_AddRef(iter->mFrame.GetImage()),
+ iter->mFrame.GetIntrinsicSize(),
+ iter->mFrame.GetPrincipalHandle(),
+ iter->mFrame.GetForceBlack(), iter->mTimeStamp);
+ }
+
+ nsresult rv = mEncoderThread->Dispatch(NS_NewRunnableFunction(
+ "mozilla::VideoTrackEncoder::AppendVideoSegment",
+ [encoder = mMediaEncoder, copy = std::move(copy)]() mutable {
+ encoder->mVideoEncoder->AppendVideoSegment(std::move(copy));
+ }));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ }
+
+ void NotifyEnabledStateChanged(MediaTrackGraph* aGraph,
+ bool aEnabled) override {
+ MOZ_ASSERT(mMediaEncoder);
+ MOZ_ASSERT(mMediaEncoder->mVideoEncoder);
+ MOZ_ASSERT(mEncoderThread);
+
+ nsresult rv;
+ if (aEnabled) {
+ rv = mEncoderThread->Dispatch(NS_NewRunnableFunction(
+ "mozilla::VideoTrackEncoder::Enable",
+ [encoder = mMediaEncoder, now = TimeStamp::Now()] {
+ encoder->mVideoEncoder->Enable(now);
+ }));
+ } else {
+ rv = mEncoderThread->Dispatch(NS_NewRunnableFunction(
+ "mozilla::VideoTrackEncoder::Disable",
+ [encoder = mMediaEncoder, now = TimeStamp::Now()] {
+ encoder->mVideoEncoder->Disable(now);
+ }));
+ }
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ }
+
+ void NotifyEnded(MediaTrackGraph* aGraph) override {
+ MOZ_ASSERT(mMediaEncoder);
+ MOZ_ASSERT(mMediaEncoder->mVideoEncoder);
+ MOZ_ASSERT(mEncoderThread);
+
+ nsresult rv = mEncoderThread->Dispatch(NS_NewRunnableFunction(
+ "mozilla::VideoTrackEncoder::NotifyEndOfStream",
+ [encoder = mMediaEncoder, now = mCurrentTime] {
+ if (!now.IsNull()) {
+ encoder->mVideoEncoder->AdvanceCurrentTime(now);
+ }
+ encoder->mVideoEncoder->NotifyEndOfStream();
+ }));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ }
+
+ void NotifyRemoved(MediaTrackGraph* aGraph) override {
+ nsresult rv = mEncoderThread->Dispatch(NS_NewRunnableFunction(
+ "mozilla::VideoTrackEncoder::NotifyEndOfStream",
+ [encoder = mMediaEncoder, now = mCurrentTime] {
+ if (!now.IsNull()) {
+ encoder->mVideoEncoder->AdvanceCurrentTime(now);
+ }
+ encoder->mVideoEncoder->NotifyEndOfStream();
+ }));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+
+ mRemoved = true;
+
+ if (!mDirectConnected) {
+ mMediaEncoder = nullptr;
+ mEncoderThread = nullptr;
+ }
+
+ mShutdownHolder.Resolve(true, __func__);
+ }
+
+ const RefPtr<GenericNonExclusivePromise>& OnShutdown() const {
+ return mShutdownPromise;
+ }
+
+ private:
+ bool mDirectConnected;
+ bool mInitialized;
+ bool mRemoved;
+ TimeStamp mCurrentTime;
+ Atomic<bool> mPendingAdvanceCurrentTime;
+ RefPtr<MediaEncoder> mMediaEncoder;
+ RefPtr<TaskQueue> mEncoderThread;
+ MozPromiseHolder<GenericNonExclusivePromise> mShutdownHolder;
+ const RefPtr<GenericNonExclusivePromise> mShutdownPromise;
+};
+
+class MediaEncoder::EncoderListener : public TrackEncoderListener {
+ public:
+ EncoderListener(TaskQueue* aEncoderThread, MediaEncoder* aEncoder)
+ : mEncoderThread(aEncoderThread), mEncoder(aEncoder) {}
+
+ void Forget() {
+ MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
+ mEncoder = nullptr;
+ }
+
+ void Initialized(TrackEncoder* aTrackEncoder) override {
+ MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
+ MOZ_ASSERT(aTrackEncoder->IsInitialized());
+
+ if (!mEncoder) {
+ return;
+ }
+
+ mEncoder->UpdateInitialized();
+ }
+
+ void Started(TrackEncoder* aTrackEncoder) override {
+ MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
+ MOZ_ASSERT(aTrackEncoder->IsStarted());
+
+ if (!mEncoder) {
+ return;
+ }
+
+ mEncoder->UpdateStarted();
+ }
+
+ void Error(TrackEncoder* aTrackEncoder) override {
+ MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
+
+ if (!mEncoder) {
+ return;
+ }
+
+ mEncoder->SetError();
+ }
+
+ protected:
+ RefPtr<TaskQueue> mEncoderThread;
+ RefPtr<MediaEncoder> mEncoder;
+};
+
+MediaEncoder::MediaEncoder(
+ RefPtr<TaskQueue> aEncoderThread,
+ RefPtr<DriftCompensator> aDriftCompensator,
+ UniquePtr<ContainerWriter> aWriter,
+ UniquePtr<AudioTrackEncoder> aAudioEncoder,
+ UniquePtr<VideoTrackEncoder> aVideoEncoder,
+ UniquePtr<MediaQueue<EncodedFrame>> aEncodedAudioQueue,
+ UniquePtr<MediaQueue<EncodedFrame>> aEncodedVideoQueue,
+ TrackRate aTrackRate, const nsAString& aMimeType, uint64_t aMaxMemory,
+ TimeDuration aTimeslice)
+ : mMainThread(GetMainThreadSerialEventTarget()),
+ mEncoderThread(std::move(aEncoderThread)),
+ mEncodedAudioQueue(std::move(aEncodedAudioQueue)),
+ mEncodedVideoQueue(std::move(aEncodedVideoQueue)),
+ mMuxer(MakeUnique<Muxer>(std::move(aWriter), *mEncodedAudioQueue,
+ *mEncodedVideoQueue)),
+ mAudioEncoder(std::move(aAudioEncoder)),
+ mAudioListener(mAudioEncoder ? MakeAndAddRef<AudioTrackListener>(
+ std::move(aDriftCompensator), this)
+ : nullptr),
+ mVideoEncoder(std::move(aVideoEncoder)),
+ mVideoListener(mVideoEncoder ? MakeAndAddRef<VideoTrackListener>(this)
+ : nullptr),
+ mEncoderListener(MakeAndAddRef<EncoderListener>(mEncoderThread, this)),
+ mMimeType(aMimeType),
+ mMaxMemory(aMaxMemory),
+ mTimeslice(aTimeslice),
+ mStartTime(TimeStamp::Now()),
+ mInitialized(false),
+ mStarted(false),
+ mCompleted(false),
+ mError(false) {
+ if (mAudioEncoder) {
+ mAudioPushListener = mEncodedAudioQueue->PushEvent().Connect(
+ mEncoderThread, this, &MediaEncoder::OnEncodedAudioPushed);
+ mAudioFinishListener = mEncodedAudioQueue->FinishEvent().Connect(
+ mEncoderThread, this, &MediaEncoder::MaybeShutdown);
+ nsresult rv = mEncoderThread->Dispatch(NS_NewRunnableFunction(
+ "mozilla::AudioTrackEncoder::RegisterListener",
+ [self = RefPtr<MediaEncoder>(this), this] {
+ mAudioEncoder->RegisterListener(mEncoderListener);
+ }));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ } else {
+ mMuxedAudioEndTime = TimeUnit::FromInfinity();
+ mEncodedAudioQueue->Finish();
+ }
+ if (mVideoEncoder) {
+ mVideoPushListener = mEncodedVideoQueue->PushEvent().Connect(
+ mEncoderThread, this, &MediaEncoder::OnEncodedVideoPushed);
+ mVideoFinishListener = mEncodedVideoQueue->FinishEvent().Connect(
+ mEncoderThread, this, &MediaEncoder::MaybeShutdown);
+ nsresult rv = mEncoderThread->Dispatch(NS_NewRunnableFunction(
+ "mozilla::VideoTrackEncoder::RegisterListener",
+ [self = RefPtr<MediaEncoder>(this), this] {
+ mVideoEncoder->RegisterListener(mEncoderListener);
+ }));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ } else {
+ mMuxedVideoEndTime = TimeUnit::FromInfinity();
+ mEncodedVideoQueue->Finish();
+ }
+}
+
+MediaEncoder::~MediaEncoder() {
+ MOZ_ASSERT(!mAudioTrack);
+ MOZ_ASSERT(!mVideoTrack);
+ MOZ_ASSERT(!mAudioNode);
+ MOZ_ASSERT(!mInputPort);
+ MOZ_ASSERT(!mPipeTrack);
+}
+
+void MediaEncoder::EnsureGraphTrackFrom(MediaTrack* aTrack) {
+ if (mGraphTrack) {
+ return;
+ }
+ MOZ_DIAGNOSTIC_ASSERT(!aTrack->IsDestroyed());
+ mGraphTrack = MakeAndAddRef<SharedDummyTrack>(
+ aTrack->GraphImpl()->CreateSourceTrack(MediaSegment::VIDEO));
+}
+
+void MediaEncoder::RunOnGraph(already_AddRefed<Runnable> aRunnable) {
+ MOZ_ASSERT(mGraphTrack);
+ class Message : public ControlMessage {
+ public:
+ explicit Message(already_AddRefed<Runnable> aRunnable)
+ : ControlMessage(nullptr), mRunnable(aRunnable) {}
+ void Run() override {
+ TRACE("MediaEncoder::RunOnGraph");
+ mRunnable->Run();
+ }
+ const RefPtr<Runnable> mRunnable;
+ };
+ mGraphTrack->mTrack->GraphImpl()->AppendMessage(
+ MakeUnique<Message>(std::move(aRunnable)));
+}
+
+void MediaEncoder::Suspend() {
+ RunOnGraph(NS_NewRunnableFunction(
+ "MediaEncoder::Suspend (graph)",
+ [self = RefPtr<MediaEncoder>(this), this] {
+ if (NS_FAILED(mEncoderThread->Dispatch(
+ NS_NewRunnableFunction("MediaEncoder::Suspend (encoder)",
+ [self, this, now = TimeStamp::Now()] {
+ if (mAudioEncoder) {
+ mAudioEncoder->Suspend();
+ }
+ if (mVideoEncoder) {
+ mVideoEncoder->Suspend(now);
+ }
+ })))) {
+ // RunOnGraph added an extra async step, and now `thread` has shut
+ // down.
+ return;
+ }
+ }));
+}
+
+void MediaEncoder::Resume() {
+ RunOnGraph(NS_NewRunnableFunction(
+ "MediaEncoder::Resume (graph)",
+ [self = RefPtr<MediaEncoder>(this), this] {
+ if (NS_FAILED(mEncoderThread->Dispatch(
+ NS_NewRunnableFunction("MediaEncoder::Resume (encoder)",
+ [self, this, now = TimeStamp::Now()] {
+ if (mAudioEncoder) {
+ mAudioEncoder->Resume();
+ }
+ if (mVideoEncoder) {
+ mVideoEncoder->Resume(now);
+ }
+ })))) {
+ // RunOnGraph added an extra async step, and now `thread` has shut
+ // down.
+ return;
+ }
+ }));
+}
+
+void MediaEncoder::ConnectAudioNode(AudioNode* aNode, uint32_t aOutput) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mAudioNode) {
+ MOZ_ASSERT(false, "Only one audio node supported");
+ return;
+ }
+
+ // Only AudioNodeTrack of kind EXTERNAL_OUTPUT stores output audio data in
+ // the track (see AudioNodeTrack::AdvanceOutputSegment()). That means
+ // forwarding input track in recorder session won't be able to copy data from
+ // the track of non-destination node. Create a pipe track in this case.
+ if (aNode->NumberOfOutputs() > 0) {
+ AudioContext* ctx = aNode->Context();
+ AudioNodeEngine* engine = new AudioNodeEngine(nullptr);
+ AudioNodeTrack::Flags flags = AudioNodeTrack::EXTERNAL_OUTPUT |
+ AudioNodeTrack::NEED_MAIN_THREAD_ENDED;
+ mPipeTrack = AudioNodeTrack::Create(ctx, engine, flags, ctx->Graph());
+ AudioNodeTrack* ns = aNode->GetTrack();
+ if (ns) {
+ mInputPort = mPipeTrack->AllocateInputPort(aNode->GetTrack(), 0, aOutput);
+ }
+ }
+
+ mAudioNode = aNode;
+
+ if (mPipeTrack) {
+ mPipeTrack->AddListener(mAudioListener);
+ EnsureGraphTrackFrom(mPipeTrack);
+ } else {
+ mAudioNode->GetTrack()->AddListener(mAudioListener);
+ EnsureGraphTrackFrom(mAudioNode->GetTrack());
+ }
+}
+
+void MediaEncoder::ConnectMediaStreamTrack(MediaStreamTrack* aTrack) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (aTrack->Ended()) {
+ MOZ_ASSERT_UNREACHABLE("Cannot connect ended track");
+ return;
+ }
+
+ EnsureGraphTrackFrom(aTrack->GetTrack());
+
+ if (AudioStreamTrack* audio = aTrack->AsAudioStreamTrack()) {
+ if (!mAudioEncoder) {
+ // No audio encoder for this audio track. It could be disabled.
+ LOG(LogLevel::Warning, ("Cannot connect to audio track - no encoder"));
+ return;
+ }
+
+ MOZ_ASSERT(!mAudioTrack, "Only one audio track supported.");
+ MOZ_ASSERT(mAudioListener, "No audio listener for this audio track");
+
+ LOG(LogLevel::Info, ("Connected to audio track %p", aTrack));
+
+ mAudioTrack = audio;
+ audio->AddListener(mAudioListener);
+ } else if (VideoStreamTrack* video = aTrack->AsVideoStreamTrack()) {
+ if (!mVideoEncoder) {
+ // No video encoder for this video track. It could be disabled.
+ LOG(LogLevel::Warning, ("Cannot connect to video track - no encoder"));
+ return;
+ }
+
+ MOZ_ASSERT(!mVideoTrack, "Only one video track supported.");
+ MOZ_ASSERT(mVideoListener, "No video listener for this video track");
+
+ LOG(LogLevel::Info, ("Connected to video track %p", aTrack));
+
+ mVideoTrack = video;
+ video->AddDirectListener(mVideoListener);
+ video->AddListener(mVideoListener);
+ } else {
+ MOZ_ASSERT(false, "Unknown track type");
+ }
+}
+
+void MediaEncoder::RemoveMediaStreamTrack(MediaStreamTrack* aTrack) {
+ if (!aTrack) {
+ MOZ_ASSERT(false);
+ return;
+ }
+
+ if (AudioStreamTrack* audio = aTrack->AsAudioStreamTrack()) {
+ if (audio != mAudioTrack) {
+ MOZ_ASSERT(false, "Not connected to this audio track");
+ return;
+ }
+
+ if (mAudioListener) {
+ audio->RemoveDirectListener(mAudioListener);
+ audio->RemoveListener(mAudioListener);
+ }
+ mAudioTrack = nullptr;
+ } else if (VideoStreamTrack* video = aTrack->AsVideoStreamTrack()) {
+ if (video != mVideoTrack) {
+ MOZ_ASSERT(false, "Not connected to this video track");
+ return;
+ }
+
+ if (mVideoListener) {
+ video->RemoveDirectListener(mVideoListener);
+ video->RemoveListener(mVideoListener);
+ }
+ mVideoTrack = nullptr;
+ }
+}
+
+/* static */
+already_AddRefed<MediaEncoder> MediaEncoder::CreateEncoder(
+ RefPtr<TaskQueue> aEncoderThread, const nsAString& aMimeType,
+ uint32_t aAudioBitrate, uint32_t aVideoBitrate, uint8_t aTrackTypes,
+ TrackRate aTrackRate, uint64_t aMaxMemory, TimeDuration aTimeslice) {
+ AUTO_PROFILER_LABEL("MediaEncoder::CreateEncoder", OTHER);
+
+ UniquePtr<ContainerWriter> writer;
+ UniquePtr<AudioTrackEncoder> audioEncoder;
+ UniquePtr<VideoTrackEncoder> videoEncoder;
+ auto encodedAudioQueue = MakeUnique<MediaQueue<EncodedFrame>>();
+ auto encodedVideoQueue = MakeUnique<MediaQueue<EncodedFrame>>();
+ auto driftCompensator =
+ MakeRefPtr<DriftCompensator>(aEncoderThread, aTrackRate);
+
+ Maybe<MediaContainerType> mimeType = MakeMediaContainerType(aMimeType);
+ if (!mimeType) {
+ return nullptr;
+ }
+
+ for (const auto& codec : mimeType->ExtendedType().Codecs().Range()) {
+ if (codec.EqualsLiteral("opus")) {
+ MOZ_ASSERT(!audioEncoder);
+ audioEncoder =
+ MakeUnique<OpusTrackEncoder>(aTrackRate, *encodedAudioQueue);
+ } else if (codec.EqualsLiteral("vp8") || codec.EqualsLiteral("vp8.0")) {
+ MOZ_ASSERT(!videoEncoder);
+ if (Preferences::GetBool("media.recorder.video.frame_drops", true)) {
+ videoEncoder = MakeUnique<VP8TrackEncoder>(driftCompensator, aTrackRate,
+ *encodedVideoQueue,
+ FrameDroppingMode::ALLOW);
+ } else {
+ videoEncoder = MakeUnique<VP8TrackEncoder>(driftCompensator, aTrackRate,
+ *encodedVideoQueue,
+ FrameDroppingMode::DISALLOW);
+ }
+ } else {
+ MOZ_CRASH("Unknown codec");
+ }
+ }
+
+ if (mimeType->Type() == MEDIAMIMETYPE(VIDEO_WEBM) ||
+ mimeType->Type() == MEDIAMIMETYPE(AUDIO_WEBM)) {
+ MOZ_ASSERT_IF(mimeType->Type() == MEDIAMIMETYPE(AUDIO_WEBM), !videoEncoder);
+ writer = MakeUnique<WebMWriter>();
+ } else if (mimeType->Type() == MEDIAMIMETYPE(AUDIO_OGG)) {
+ MOZ_ASSERT(audioEncoder);
+ MOZ_ASSERT(!videoEncoder);
+ writer = MakeUnique<OggWriter>();
+ }
+ NS_ENSURE_TRUE(writer, nullptr);
+
+ LOG(LogLevel::Info,
+ ("Create encoder result:a[%p](%u bps) v[%p](%u bps) w[%p] mimeType = "
+ "%s.",
+ audioEncoder.get(), aAudioBitrate, videoEncoder.get(), aVideoBitrate,
+ writer.get(), NS_ConvertUTF16toUTF8(aMimeType).get()));
+
+ if (audioEncoder) {
+ audioEncoder->SetWorkerThread(aEncoderThread);
+ if (aAudioBitrate != 0) {
+ audioEncoder->SetBitrate(aAudioBitrate);
+ }
+ }
+ if (videoEncoder) {
+ videoEncoder->SetWorkerThread(aEncoderThread);
+ if (aVideoBitrate != 0) {
+ videoEncoder->SetBitrate(aVideoBitrate);
+ }
+ }
+ return MakeAndAddRef<MediaEncoder>(
+ std::move(aEncoderThread), std::move(driftCompensator), std::move(writer),
+ std::move(audioEncoder), std::move(videoEncoder),
+ std::move(encodedAudioQueue), std::move(encodedVideoQueue), aTrackRate,
+ aMimeType, aMaxMemory, aTimeslice);
+}
+
+nsresult MediaEncoder::GetEncodedData(
+ nsTArray<nsTArray<uint8_t>>* aOutputBufs) {
+ AUTO_PROFILER_LABEL("MediaEncoder::GetEncodedData", OTHER);
+
+ MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
+
+ LOG(LogLevel::Verbose,
+ ("GetEncodedData TimeStamp = %f", GetEncodeTimeStamp()));
+
+ if (!mInitialized) {
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ nsresult rv = mMuxer->GetData(aOutputBufs);
+ if (mMuxer->IsFinished()) {
+ mCompleted = true;
+ }
+
+ LOG(LogLevel::Verbose,
+ ("END GetEncodedData TimeStamp=%f "
+ "mCompleted=%d, aComplete=%d, vComplete=%d",
+ GetEncodeTimeStamp(), mCompleted,
+ !mAudioEncoder || mAudioEncoder->IsEncodingComplete(),
+ !mVideoEncoder || mVideoEncoder->IsEncodingComplete()));
+
+ return rv;
+}
+
+void MediaEncoder::MaybeShutdown() {
+ MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
+ if (!mEncodedAudioQueue->IsFinished()) {
+ LOG(LogLevel::Debug,
+ ("MediaEncoder %p not shutting down, audio is still live", this));
+ return;
+ }
+
+ if (!mEncodedVideoQueue->IsFinished()) {
+ LOG(LogLevel::Debug,
+ ("MediaEncoder %p not shutting down, video is still live", this));
+ return;
+ }
+
+ mShutdownEvent.Notify();
+
+ // Stop will Shutdown() gracefully.
+ Unused << InvokeAsync(mMainThread, this, __func__, &MediaEncoder::Stop);
+}
+
+RefPtr<GenericNonExclusivePromise> MediaEncoder::Shutdown() {
+ MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
+ if (mShutdownPromise) {
+ return mShutdownPromise;
+ }
+
+ LOG(LogLevel::Info, ("MediaEncoder is shutting down."));
+
+ AutoTArray<RefPtr<GenericNonExclusivePromise>, 2> shutdownPromises;
+ if (mAudioListener) {
+ shutdownPromises.AppendElement(mAudioListener->OnShutdown());
+ }
+ if (mVideoListener) {
+ shutdownPromises.AppendElement(mVideoListener->OnShutdown());
+ }
+
+ mShutdownPromise =
+ GenericNonExclusivePromise::All(mEncoderThread, shutdownPromises)
+ ->Then(mEncoderThread, __func__,
+ [](const GenericNonExclusivePromise::AllPromiseType::
+ ResolveOrRejectValue& aValue) {
+ if (aValue.IsResolve()) {
+ return GenericNonExclusivePromise::CreateAndResolve(
+ true, __func__);
+ }
+ return GenericNonExclusivePromise::CreateAndReject(
+ aValue.RejectValue(), __func__);
+ });
+
+ mShutdownPromise->Then(
+ mEncoderThread, __func__, [self = RefPtr<MediaEncoder>(this), this] {
+ if (mAudioEncoder) {
+ mAudioEncoder->UnregisterListener(mEncoderListener);
+ }
+ if (mVideoEncoder) {
+ mVideoEncoder->UnregisterListener(mEncoderListener);
+ }
+ mEncoderListener->Forget();
+ mMuxer->Disconnect();
+ mAudioPushListener.DisconnectIfExists();
+ mAudioFinishListener.DisconnectIfExists();
+ mVideoPushListener.DisconnectIfExists();
+ mVideoFinishListener.DisconnectIfExists();
+ });
+
+ return mShutdownPromise;
+}
+
+RefPtr<GenericNonExclusivePromise> MediaEncoder::Stop() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ LOG(LogLevel::Info, ("MediaEncoder %p Stop", this));
+
+ DisconnectTracks();
+
+ return InvokeAsync(mEncoderThread, this, __func__, &MediaEncoder::Shutdown);
+}
+
+RefPtr<GenericNonExclusivePromise> MediaEncoder::Cancel() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ LOG(LogLevel::Info, ("MediaEncoder %p Cancel", this));
+
+ DisconnectTracks();
+
+ return InvokeAsync(mEncoderThread, __func__,
+ [self = RefPtr<MediaEncoder>(this), this]() {
+ if (mAudioEncoder) {
+ mAudioEncoder->Cancel();
+ }
+ if (mVideoEncoder) {
+ mVideoEncoder->Cancel();
+ }
+ return Shutdown();
+ });
+}
+
+bool MediaEncoder::HasError() {
+ MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
+ return mError;
+}
+
+void MediaEncoder::SetError() {
+ MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
+
+ if (mError) {
+ return;
+ }
+
+ mError = true;
+ mErrorEvent.Notify();
+}
+
+auto MediaEncoder::RequestData() -> RefPtr<BlobPromise> {
+ MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
+ TimeUnit muxedEndTime = std::min(mMuxedAudioEndTime, mMuxedVideoEndTime);
+ mLastBlobTime = muxedEndTime;
+ mLastExtractTime = muxedEndTime;
+ return Extract()->Then(
+ mMainThread, __func__,
+ [this, self = RefPtr<MediaEncoder>(this)](
+ const GenericPromise::ResolveOrRejectValue& aValue) {
+ // Even if rejected, we want to gather what has already been
+ // extracted into the current blob and expose that.
+ Unused << NS_WARN_IF(aValue.IsReject());
+ return GatherBlob();
+ });
+}
+
+void MediaEncoder::MaybeCreateMutableBlobStorage() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!mMutableBlobStorage) {
+ mMutableBlobStorage = new MutableBlobStorage(
+ MutableBlobStorage::eCouldBeInTemporaryFile, nullptr, mMaxMemory);
+ }
+}
+
+void MediaEncoder::OnEncodedAudioPushed(const RefPtr<EncodedFrame>& aFrame) {
+ MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
+ mMuxedAudioEndTime = aFrame->GetEndTime();
+ MaybeExtractOrGatherBlob();
+}
+
+void MediaEncoder::OnEncodedVideoPushed(const RefPtr<EncodedFrame>& aFrame) {
+ MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
+ mMuxedVideoEndTime = aFrame->GetEndTime();
+ MaybeExtractOrGatherBlob();
+}
+
+void MediaEncoder::MaybeExtractOrGatherBlob() {
+ MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
+
+ TimeUnit muxedEndTime = std::min(mMuxedAudioEndTime, mMuxedVideoEndTime);
+ if ((muxedEndTime - mLastBlobTime).ToTimeDuration() >= mTimeslice) {
+ LOG(LogLevel::Verbose, ("MediaEncoder %p Muxed %.2fs of data since last "
+ "blob. Issuing new blob.",
+ this, (muxedEndTime - mLastBlobTime).ToSeconds()));
+ RequestData()->Then(mEncoderThread, __func__,
+ [this, self = RefPtr<MediaEncoder>(this)](
+ const BlobPromise::ResolveOrRejectValue& aValue) {
+ if (aValue.IsReject()) {
+ SetError();
+ return;
+ }
+ RefPtr<BlobImpl> blob = aValue.ResolveValue();
+ mDataAvailableEvent.Notify(std::move(blob));
+ });
+ }
+
+ if (muxedEndTime - mLastExtractTime > TimeUnit::FromSeconds(1)) {
+ // Extract data from the muxer at least every second.
+ LOG(LogLevel::Verbose,
+ ("MediaEncoder %p Muxed %.2fs of data since last "
+ "extract. Extracting more data into blob.",
+ this, (muxedEndTime - mLastExtractTime).ToSeconds()));
+ mLastExtractTime = muxedEndTime;
+ Unused << Extract();
+ }
+}
+
+// Pull encoded media data from MediaEncoder and put into MutableBlobStorage.
+RefPtr<GenericPromise> MediaEncoder::Extract() {
+ MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
+
+ LOG(LogLevel::Debug, ("MediaEncoder %p Extract", this));
+
+ AUTO_PROFILER_LABEL("MediaEncoder::Extract", OTHER);
+
+ // Pull encoded media data from MediaEncoder
+ nsTArray<nsTArray<uint8_t>> buffer;
+ nsresult rv = GetEncodedData(&buffer);
+ MOZ_ASSERT(rv != NS_ERROR_INVALID_ARG, "Invalid args can be prevented.");
+ if (NS_FAILED(rv)) {
+ MOZ_RELEASE_ASSERT(buffer.IsEmpty());
+ // Even if we failed to encode more data, it might be time to push a blob
+ // with already encoded data.
+ }
+
+ // To ensure Extract() promises are resolved in calling order, we always
+ // invoke the main thread. Even when the encoded buffer is empty.
+ return InvokeAsync(
+ mMainThread, __func__,
+ [self = RefPtr<MediaEncoder>(this), this, buffer = std::move(buffer)] {
+ MaybeCreateMutableBlobStorage();
+ for (const auto& part : buffer) {
+ if (part.IsEmpty()) {
+ continue;
+ }
+
+ nsresult rv =
+ mMutableBlobStorage->Append(part.Elements(), part.Length());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return GenericPromise::CreateAndReject(rv, __func__);
+ }
+ }
+ return GenericPromise::CreateAndResolve(true, __func__);
+ });
+}
+
+auto MediaEncoder::GatherBlob() -> RefPtr<BlobPromise> {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!mBlobPromise) {
+ return mBlobPromise = GatherBlobImpl();
+ }
+ return mBlobPromise = mBlobPromise->Then(mMainThread, __func__,
+ [self = RefPtr<MediaEncoder>(this)] {
+ return self->GatherBlobImpl();
+ });
+}
+
+auto MediaEncoder::GatherBlobImpl() -> RefPtr<BlobPromise> {
+ RefPtr<BlobStorer> storer = MakeAndAddRef<BlobStorer>();
+ MaybeCreateMutableBlobStorage();
+ mMutableBlobStorage->GetBlobImplWhenReady(NS_ConvertUTF16toUTF8(mMimeType),
+ storer);
+ mMutableBlobStorage = nullptr;
+
+ storer->Promise()->Then(
+ mMainThread, __func__,
+ [self = RefPtr<MediaEncoder>(this), p = storer->Promise()] {
+ if (self->mBlobPromise == p) {
+ // Reset BlobPromise.
+ self->mBlobPromise = nullptr;
+ }
+ });
+
+ return storer->Promise();
+}
+
+void MediaEncoder::DisconnectTracks() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mAudioNode) {
+ mAudioNode->GetTrack()->RemoveListener(mAudioListener);
+ if (mInputPort) {
+ mInputPort->Destroy();
+ mInputPort = nullptr;
+ }
+ if (mPipeTrack) {
+ mPipeTrack->RemoveListener(mAudioListener);
+ mPipeTrack->Destroy();
+ mPipeTrack = nullptr;
+ }
+ mAudioNode = nullptr;
+ }
+
+ if (mAudioTrack) {
+ RemoveMediaStreamTrack(mAudioTrack);
+ }
+
+ if (mVideoTrack) {
+ RemoveMediaStreamTrack(mVideoTrack);
+ }
+}
+
+bool MediaEncoder::IsWebMEncoderEnabled() {
+ return StaticPrefs::media_encoder_webm_enabled();
+}
+
+void MediaEncoder::UpdateInitialized() {
+ MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
+
+ if (mInitialized) {
+ // This could happen if an encoder re-inits due to a resolution change.
+ return;
+ }
+
+ if (mAudioEncoder && !mAudioEncoder->IsInitialized()) {
+ LOG(LogLevel::Debug,
+ ("MediaEncoder %p UpdateInitialized waiting for audio", this));
+ return;
+ }
+
+ if (mVideoEncoder && !mVideoEncoder->IsInitialized()) {
+ LOG(LogLevel::Debug,
+ ("MediaEncoder %p UpdateInitialized waiting for video", this));
+ return;
+ }
+
+ MOZ_ASSERT(mMuxer->NeedsMetadata());
+ nsTArray<RefPtr<TrackMetadataBase>> meta;
+ if (mAudioEncoder && !*meta.AppendElement(mAudioEncoder->GetMetadata())) {
+ LOG(LogLevel::Error, ("Audio metadata is null"));
+ SetError();
+ return;
+ }
+ if (mVideoEncoder && !*meta.AppendElement(mVideoEncoder->GetMetadata())) {
+ LOG(LogLevel::Error, ("Video metadata is null"));
+ SetError();
+ return;
+ }
+
+ if (NS_FAILED(mMuxer->SetMetadata(meta))) {
+ LOG(LogLevel::Error, ("SetMetadata failed"));
+ SetError();
+ return;
+ }
+
+ LOG(LogLevel::Info,
+ ("MediaEncoder %p UpdateInitialized set metadata in muxer", this));
+
+ mInitialized = true;
+}
+
+void MediaEncoder::UpdateStarted() {
+ MOZ_ASSERT(mEncoderThread->IsCurrentThreadIn());
+
+ if (mStarted) {
+ return;
+ }
+
+ if (mAudioEncoder && !mAudioEncoder->IsStarted()) {
+ return;
+ }
+
+ if (mVideoEncoder && !mVideoEncoder->IsStarted()) {
+ return;
+ }
+
+ mStarted = true;
+
+ // Start issuing timeslice-based blobs.
+ MOZ_ASSERT(mLastBlobTime == TimeUnit::Zero());
+
+ mStartedEvent.Notify();
+}
+
+/*
+ * SizeOfExcludingThis measures memory being used by the Media Encoder.
+ * Currently it measures the size of the Encoder buffer and memory occupied
+ * by mAudioEncoder, mVideoEncoder, and any current blob storage.
+ */
+auto MediaEncoder::SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf)
+ -> RefPtr<SizeOfPromise> {
+ MOZ_ASSERT(NS_IsMainThread());
+ size_t blobStorageSize =
+ mMutableBlobStorage ? mMutableBlobStorage->SizeOfCurrentMemoryBuffer()
+ : 0;
+
+ return InvokeAsync(
+ mEncoderThread, __func__,
+ [self = RefPtr<MediaEncoder>(this), this, blobStorageSize,
+ aMallocSizeOf]() {
+ size_t size = 0;
+ if (mAudioEncoder) {
+ size += mAudioEncoder->SizeOfExcludingThis(aMallocSizeOf);
+ }
+ if (mVideoEncoder) {
+ size += mVideoEncoder->SizeOfExcludingThis(aMallocSizeOf);
+ }
+ return SizeOfPromise::CreateAndResolve(blobStorageSize + size,
+ __func__);
+ });
+}
+
+} // namespace mozilla
+
+#undef LOG
diff --git a/dom/media/encoder/MediaEncoder.h b/dom/media/encoder/MediaEncoder.h
new file mode 100644
index 0000000000..dae887edc6
--- /dev/null
+++ b/dom/media/encoder/MediaEncoder.h
@@ -0,0 +1,400 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MediaEncoder_h_
+#define MediaEncoder_h_
+
+#include "ContainerWriter.h"
+#include "CubebUtils.h"
+#include "MediaQueue.h"
+#include "MediaTrackGraph.h"
+#include "MediaTrackListener.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/UniquePtr.h"
+#include "nsIMemoryReporter.h"
+#include "TrackEncoder.h"
+
+namespace mozilla {
+
+class DriftCompensator;
+class Muxer;
+class Runnable;
+class TaskQueue;
+
+namespace dom {
+class AudioNode;
+class AudioStreamTrack;
+class BlobImpl;
+class MediaStreamTrack;
+class MutableBlobStorage;
+class VideoStreamTrack;
+} // namespace dom
+
+class DriftCompensator;
+
+/**
+ * MediaEncoder is the framework of encoding module, it controls and manages
+ * procedures between Muxer, ContainerWriter and TrackEncoder. ContainerWriter
+ * writes the encoded track data into a specific container (e.g. ogg, webm).
+ * AudioTrackEncoder and VideoTrackEncoder are subclasses of TrackEncoder, and
+ * are responsible for encoding raw data coming from MediaStreamTracks.
+ *
+ * MediaEncoder solves threading issues by doing message passing to a TaskQueue
+ * (the "encoder thread") as passed in to the constructor. Each
+ * MediaStreamTrack to be recorded is set up with a MediaTrackListener.
+ * Typically there are a non-direct track listeners for audio, direct listeners
+ * for video, and there is always a non-direct listener on each track for
+ * time-keeping. The listeners forward data to their corresponding TrackEncoders
+ * on the encoder thread.
+ *
+ * The MediaEncoder listens to events from all TrackEncoders, and in turn
+ * signals events to interested parties. Typically a MediaRecorder::Session.
+ * The MediaEncoder automatically encodes incoming data, muxes it, writes it
+ * into a container and stores the container data into a MutableBlobStorage.
+ * It is timeslice-aware so that it can notify listeners when it's time to
+ * expose a blob due to filling the timeslice.
+ *
+ * MediaEncoder is designed to be a passive component, neither does it own or is
+ * in charge of managing threads. Instead this is done by its owner.
+ *
+ * For example, usage from MediaRecorder of this component would be:
+ * 1) Create an encoder with a valid MIME type. Note that there are more
+ * configuration options, see the docs on MediaEncoder::CreateEncoder.
+ * => encoder = MediaEncoder::CreateEncoder(aMIMEType);
+ * It then creates track encoders and the appropriate ContainerWriter
+ * according to the MIME type
+ *
+ * 2) Connect handlers through MediaEventListeners to the MediaEncoder's
+ * MediaEventSources, StartedEvent(), DataAvailableEvent(), ErrorEvent() and
+ * ShutdownEvent().
+ * => listener = encoder->DataAvailableEvent().Connect(mainThread, &OnBlob);
+ *
+ * 3) Connect the sources to be recorded. Either through:
+ * => encoder->ConnectAudioNode(node);
+ * or
+ * => encoder->ConnectMediaStreamTrack(track);
+ * These should not be mixed. When connecting MediaStreamTracks there is
+ * support for at most one of each kind.
+ *
+ * 4) MediaEncoder automatically encodes data from the connected tracks, muxes
+ * them and writes it all into a blob, including metadata. When the blob
+ * contains at least `timeslice` worth of data it notifies the
+ * DataAvailableEvent that was connected in step 2.
+ * => void OnBlob(RefPtr<BlobImpl> aBlob) {
+ * => DispatchBlobEvent(Blob::Create(GetOwnerGlobal(), aBlob));
+ * => };
+ *
+ * 5) To stop encoding, there are multiple options:
+ *
+ * 5.1) Stop() for a graceful stop.
+ * => encoder->Stop();
+ *
+ * 5.2) Cancel() for an immediate stop, if you don't need the data currently
+ * buffered.
+ * => encoder->Cancel();
+ *
+ * 5.3) When all input tracks end, the MediaEncoder will automatically stop
+ * and shut down.
+ */
+class MediaEncoder {
+ private:
+ class AudioTrackListener;
+ class VideoTrackListener;
+ class EncoderListener;
+
+ public:
+ using BlobPromise =
+ MozPromise<RefPtr<dom::BlobImpl>, nsresult, false /* IsExclusive */>;
+ using SizeOfPromise = MozPromise<size_t, size_t, true /* IsExclusive */>;
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaEncoder)
+
+ MediaEncoder(RefPtr<TaskQueue> aEncoderThread,
+ RefPtr<DriftCompensator> aDriftCompensator,
+ UniquePtr<ContainerWriter> aWriter,
+ UniquePtr<AudioTrackEncoder> aAudioEncoder,
+ UniquePtr<VideoTrackEncoder> aVideoEncoder,
+ UniquePtr<MediaQueue<EncodedFrame>> aEncodedAudioQueue,
+ UniquePtr<MediaQueue<EncodedFrame>> aEncodedVideoQueue,
+ TrackRate aTrackRate, const nsAString& aMIMEType,
+ uint64_t aMaxMemory, TimeDuration aTimeslice);
+
+ /**
+ * Called on main thread from MediaRecorder::Pause.
+ */
+ void Suspend();
+
+ /**
+ * Called on main thread from MediaRecorder::Resume.
+ */
+ void Resume();
+
+ /**
+ * Disconnects the input tracks, causing the encoding to stop.
+ */
+ void DisconnectTracks();
+
+ /**
+ * Connects an AudioNode with the appropriate encoder.
+ */
+ void ConnectAudioNode(dom::AudioNode* aNode, uint32_t aOutput);
+
+ /**
+ * Connects a MediaStreamTrack with the appropriate encoder.
+ */
+ void ConnectMediaStreamTrack(dom::MediaStreamTrack* aTrack);
+
+ /**
+ * Removes a connected MediaStreamTrack.
+ */
+ void RemoveMediaStreamTrack(dom::MediaStreamTrack* aTrack);
+
+ /**
+ * Creates an encoder with the given MIME type. This must be a valid MIME type
+ * or we will crash hard.
+ * Bitrates are given either explicit, or with 0 for defaults.
+ * aTrackRate is the rate in which data will be fed to the TrackEncoders.
+ * aMaxMemory is the maximum number of bytes of muxed data allowed in memory.
+ * Beyond that the blob is moved to a temporary file.
+ * aTimeslice is the minimum duration of muxed data we gather before
+ * automatically issuing a dataavailable event.
+ */
+ static already_AddRefed<MediaEncoder> CreateEncoder(
+ RefPtr<TaskQueue> aEncoderThread, const nsAString& aMimeType,
+ uint32_t aAudioBitrate, uint32_t aVideoBitrate, uint8_t aTrackTypes,
+ TrackRate aTrackRate, uint64_t aMaxMemory, TimeDuration aTimeslice);
+
+ /**
+ * Encodes raw data for all tracks to aOutputBufs. The buffer of container
+ * data is allocated in ContainerWriter::GetContainerData().
+ *
+ * On its first call, metadata is also encoded. TrackEncoders must have been
+ * initialized before this is called.
+ */
+ nsresult GetEncodedData(nsTArray<nsTArray<uint8_t>>* aOutputBufs);
+
+ /**
+ * Asserts that Shutdown() has been called. Reasons are encoding
+ * complete, encounter an error, or being canceled by its caller.
+ */
+ void AssertShutdownCalled() { MOZ_ASSERT(mShutdownPromise); }
+
+ /**
+ * Stops (encoding any data currently buffered) the encoding and shuts down
+ * the encoder using Shutdown().
+ */
+ RefPtr<GenericNonExclusivePromise> Stop();
+
+ /**
+ * Cancels (discarding any data currently buffered) the encoding and shuts
+ * down the encoder using Shutdown().
+ */
+ RefPtr<GenericNonExclusivePromise> Cancel();
+
+ bool HasError();
+
+ static bool IsWebMEncoderEnabled();
+
+ /**
+ * Updates internal state when track encoders are all initialized.
+ */
+ void UpdateInitialized();
+
+ /**
+ * Updates internal state when track encoders are all initialized, and
+ * notifies listeners that this MediaEncoder has been started.
+ */
+ void UpdateStarted();
+
+ MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf)
+ /*
+ * Measure the size of the buffer, and heap memory in bytes occupied by
+ * mAudioEncoder and mVideoEncoder.
+ */
+ RefPtr<SizeOfPromise> SizeOfExcludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf);
+
+ /**
+ * Encode, mux and store into blob storage what has been buffered until now,
+ * then return the blob backed by that storage.
+ */
+ RefPtr<BlobPromise> RequestData();
+
+ // Event that gets notified when all track encoders have received data.
+ MediaEventSource<void>& StartedEvent() { return mStartedEvent; }
+ // Event that gets notified when there was an error preventing continued
+ // recording somewhere in the MediaEncoder stack.
+ MediaEventSource<void>& ErrorEvent() { return mErrorEvent; }
+ // Event that gets notified when the MediaEncoder stack has been shut down.
+ MediaEventSource<void>& ShutdownEvent() { return mShutdownEvent; }
+ // Event that gets notified after we have muxed at least mTimeslice worth of
+ // data into the current blob storage.
+ MediaEventSource<RefPtr<dom::BlobImpl>>& DataAvailableEvent() {
+ return mDataAvailableEvent;
+ }
+
+ protected:
+ ~MediaEncoder();
+
+ private:
+ /**
+ * Sets mGraphTrack if not already set, using a new stream from aTrack's
+ * graph.
+ */
+ void EnsureGraphTrackFrom(MediaTrack* aTrack);
+
+ /**
+ * Takes a regular runnable and dispatches it to the graph wrapped in a
+ * ControlMessage.
+ */
+ void RunOnGraph(already_AddRefed<Runnable> aRunnable);
+
+ /**
+ * Shuts down gracefully if there is no remaining live track encoder.
+ */
+ void MaybeShutdown();
+
+ /**
+ * Waits for TrackEncoders to shut down, then shuts down the MediaEncoder and
+ * cleans up track encoders.
+ */
+ RefPtr<GenericNonExclusivePromise> Shutdown();
+
+ /**
+ * Sets mError to true, notifies listeners of the error if mError changed,
+ * and stops encoding.
+ */
+ void SetError();
+
+ /**
+ * Creates a new MutableBlobStorage if one doesn't exist.
+ */
+ void MaybeCreateMutableBlobStorage();
+
+ /**
+ * Called when an encoded audio frame has been pushed by the audio encoder.
+ */
+ void OnEncodedAudioPushed(const RefPtr<EncodedFrame>& aFrame);
+
+ /**
+ * Called when an encoded video frame has been pushed by the video encoder.
+ */
+ void OnEncodedVideoPushed(const RefPtr<EncodedFrame>& aFrame);
+
+ /**
+ * If enough data has been pushed to the muxer, extract it into the current
+ * blob storage. If more than mTimeslice data has been pushed to the muxer
+ * since the last DataAvailableEvent was notified, also gather the blob and
+ * notify MediaRecorder.
+ */
+ void MaybeExtractOrGatherBlob();
+
+ // Extracts encoded and muxed data into the current blob storage, creating one
+ // if it doesn't exist. The returned promise resolves when data has been
+ // stored into the blob.
+ RefPtr<GenericPromise> Extract();
+
+ // Stops gathering data into the current blob and resolves when the current
+ // blob is available. Future data will be stored in a new blob.
+ // Should a previous async GatherBlob() operation still be in progress, we'll
+ // wait for it to finish before starting this one.
+ RefPtr<BlobPromise> GatherBlob();
+
+ RefPtr<BlobPromise> GatherBlobImpl();
+
+ const RefPtr<nsISerialEventTarget> mMainThread;
+ const RefPtr<TaskQueue> mEncoderThread;
+ const RefPtr<DriftCompensator> mDriftCompensator;
+
+ const UniquePtr<MediaQueue<EncodedFrame>> mEncodedAudioQueue;
+ const UniquePtr<MediaQueue<EncodedFrame>> mEncodedVideoQueue;
+
+ const UniquePtr<Muxer> mMuxer;
+ const UniquePtr<AudioTrackEncoder> mAudioEncoder;
+ const RefPtr<AudioTrackListener> mAudioListener;
+ const UniquePtr<VideoTrackEncoder> mVideoEncoder;
+ const RefPtr<VideoTrackListener> mVideoListener;
+ const RefPtr<EncoderListener> mEncoderListener;
+
+ public:
+ const nsString mMimeType;
+
+ // Max memory to use for the MutableBlobStorage.
+ const uint64_t mMaxMemory;
+
+ // The interval of passing encoded data from MutableBlobStorage to
+ // onDataAvailable handler.
+ const TimeDuration mTimeslice;
+
+ private:
+ MediaEventListener mAudioPushListener;
+ MediaEventListener mAudioFinishListener;
+ MediaEventListener mVideoPushListener;
+ MediaEventListener mVideoFinishListener;
+
+ MediaEventProducer<void> mStartedEvent;
+ MediaEventProducer<void> mErrorEvent;
+ MediaEventProducer<void> mShutdownEvent;
+ MediaEventProducer<RefPtr<dom::BlobImpl>> mDataAvailableEvent;
+
+ // The AudioNode we are encoding.
+ // Will be null when input is media stream or destination node.
+ RefPtr<dom::AudioNode> mAudioNode;
+ // Pipe-track for allowing a track listener on a non-destination AudioNode.
+ // Will be null when input is media stream or destination node.
+ RefPtr<AudioNodeTrack> mPipeTrack;
+ // Input port that connect mAudioNode to mPipeTrack.
+ // Will be null when input is media stream or destination node.
+ RefPtr<MediaInputPort> mInputPort;
+ // An audio track that we are encoding. Will be null if the input stream
+ // doesn't contain audio on start() or if the input is an AudioNode.
+ RefPtr<dom::AudioStreamTrack> mAudioTrack;
+ // A video track that we are encoding. Will be null if the input stream
+ // doesn't contain video on start() or if the input is an AudioNode.
+ RefPtr<dom::VideoStreamTrack> mVideoTrack;
+
+ // A stream to keep the MediaTrackGraph alive while we're recording.
+ RefPtr<SharedDummyTrack> mGraphTrack;
+
+ // A buffer to cache muxed encoded data.
+ RefPtr<dom::MutableBlobStorage> mMutableBlobStorage;
+ // If set, is a promise for the latest GatherBlob() operation. Allows
+ // GatherBlob() operations to be serialized in order to avoid races.
+ RefPtr<BlobPromise> mBlobPromise;
+ // The end time of the muxed data in the last gathered blob. If more than one
+ // track is present, this is the end time of the track that ends the earliest
+ // in the last blob. Encoder thread only.
+ media::TimeUnit mLastBlobTime;
+ // The end time of the muxed data in the current blob storage. If more than
+ // one track is present, this is the end time of the track that ends the
+ // earliest in the current blob storage. Encoder thread only.
+ media::TimeUnit mLastExtractTime;
+ // The end time of encoded audio data sent to the muxer. Positive infinity if
+ // there is no audio encoder. Encoder thread only.
+ media::TimeUnit mMuxedAudioEndTime;
+ // The end time of encoded video data sent to the muxer. Positive infinity if
+ // there is no video encoder. Encoder thread only.
+ media::TimeUnit mMuxedVideoEndTime;
+
+ TimeStamp mStartTime;
+ bool mInitialized;
+ bool mStarted;
+ bool mCompleted;
+ bool mError;
+ // Set when shutdown starts.
+ RefPtr<GenericNonExclusivePromise> mShutdownPromise;
+ // Get duration from create encoder, for logging purpose
+ double GetEncodeTimeStamp() {
+ TimeDuration decodeTime;
+ decodeTime = TimeStamp::Now() - mStartTime;
+ return decodeTime.ToMilliseconds();
+ }
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/encoder/Muxer.cpp b/dom/media/encoder/Muxer.cpp
new file mode 100644
index 0000000000..8225062ee5
--- /dev/null
+++ b/dom/media/encoder/Muxer.cpp
@@ -0,0 +1,185 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "Muxer.h"
+
+#include "ContainerWriter.h"
+
+namespace mozilla {
+
+LazyLogModule gMuxerLog("Muxer");
+#define LOG(type, ...) MOZ_LOG(gMuxerLog, type, (__VA_ARGS__))
+
+Muxer::Muxer(UniquePtr<ContainerWriter> aWriter,
+ MediaQueue<EncodedFrame>& aEncodedAudioQueue,
+ MediaQueue<EncodedFrame>& aEncodedVideoQueue)
+ : mEncodedAudioQueue(aEncodedAudioQueue),
+ mEncodedVideoQueue(aEncodedVideoQueue),
+ mWriter(std::move(aWriter)) {}
+
+void Muxer::Disconnect() {
+ mAudioPushListener.DisconnectIfExists();
+ mAudioFinishListener.DisconnectIfExists();
+ mVideoPushListener.DisconnectIfExists();
+ mVideoFinishListener.DisconnectIfExists();
+}
+
+bool Muxer::IsFinished() { return mWriter->IsWritingComplete(); }
+
+nsresult Muxer::SetMetadata(
+ const nsTArray<RefPtr<TrackMetadataBase>>& aMetadata) {
+ MOZ_DIAGNOSTIC_ASSERT(!mMetadataSet);
+ MOZ_DIAGNOSTIC_ASSERT(!mHasAudio);
+ MOZ_DIAGNOSTIC_ASSERT(!mHasVideo);
+ nsresult rv = mWriter->SetMetadata(aMetadata);
+ if (NS_FAILED(rv)) {
+ LOG(LogLevel::Error, "%p Setting metadata failed, tracks=%zu", this,
+ aMetadata.Length());
+ return rv;
+ }
+
+ for (const auto& track : aMetadata) {
+ switch (track->GetKind()) {
+ case TrackMetadataBase::METADATA_OPUS:
+ case TrackMetadataBase::METADATA_VORBIS:
+ case TrackMetadataBase::METADATA_AAC:
+ case TrackMetadataBase::METADATA_AMR:
+ case TrackMetadataBase::METADATA_EVRC:
+ MOZ_ASSERT(!mHasAudio, "Only one audio track supported");
+ mHasAudio = true;
+ break;
+ case TrackMetadataBase::METADATA_VP8:
+ MOZ_ASSERT(!mHasVideo, "Only one video track supported");
+ mHasVideo = true;
+ break;
+ default:
+ MOZ_CRASH("Unknown codec metadata");
+ };
+ }
+ mMetadataSet = true;
+ MOZ_ASSERT(mHasAudio || mHasVideo);
+ LOG(LogLevel::Info, "%p Metadata set; audio=%d, video=%d", this, mHasAudio,
+ mHasVideo);
+ return NS_OK;
+}
+
+nsresult Muxer::GetData(nsTArray<nsTArray<uint8_t>>* aOutputBuffers) {
+ MOZ_ASSERT(mHasAudio || mHasVideo);
+
+ nsresult rv;
+ if (!mMetadataEncoded) {
+ rv = mWriter->GetContainerData(aOutputBuffers, ContainerWriter::GET_HEADER);
+ if (NS_FAILED(rv)) {
+ LOG(LogLevel::Error, "%p Failed getting metadata from writer", this);
+ return rv;
+ }
+ mMetadataEncoded = true;
+ }
+
+ if (mEncodedAudioQueue.GetSize() == 0 && !mEncodedAudioQueue.IsFinished() &&
+ mEncodedVideoQueue.GetSize() == 0 && !mEncodedVideoQueue.IsFinished()) {
+ // Nothing to mux.
+ return NS_OK;
+ }
+
+ rv = Mux();
+ if (NS_FAILED(rv)) {
+ LOG(LogLevel::Error, "%p Failed muxing data into writer", this);
+ return rv;
+ }
+
+ MOZ_ASSERT_IF(
+ mEncodedAudioQueue.IsFinished() && mEncodedVideoQueue.IsFinished(),
+ mEncodedAudioQueue.AtEndOfStream());
+ MOZ_ASSERT_IF(
+ mEncodedAudioQueue.IsFinished() && mEncodedVideoQueue.IsFinished(),
+ mEncodedVideoQueue.AtEndOfStream());
+ uint32_t flags =
+ mEncodedAudioQueue.AtEndOfStream() && mEncodedVideoQueue.AtEndOfStream()
+ ? ContainerWriter::FLUSH_NEEDED
+ : 0;
+
+ if (mEncodedAudioQueue.AtEndOfStream() &&
+ mEncodedVideoQueue.AtEndOfStream()) {
+ LOG(LogLevel::Info, "%p All data written", this);
+ }
+
+ return mWriter->GetContainerData(aOutputBuffers, flags);
+}
+
+nsresult Muxer::Mux() {
+ MOZ_ASSERT(mMetadataSet);
+ MOZ_ASSERT(mHasAudio || mHasVideo);
+
+ nsTArray<RefPtr<EncodedFrame>> frames;
+ // The times at which we expect our next video and audio frames. These are
+ // based on the time + duration (GetEndTime()) of the last seen frames.
+ // Assumes that the encoders write the correct duration for frames.;
+ media::TimeUnit expectedNextVideoTime;
+ media::TimeUnit expectedNextAudioTime;
+ // Interleave frames until we're out of audio or video
+ while (mEncodedVideoQueue.GetSize() > 0 && mEncodedAudioQueue.GetSize() > 0) {
+ RefPtr<EncodedFrame> videoFrame = mEncodedVideoQueue.PeekFront();
+ RefPtr<EncodedFrame> audioFrame = mEncodedAudioQueue.PeekFront();
+ // For any expected time our frames should occur at or after that time.
+ MOZ_ASSERT(videoFrame->mTime >= expectedNextVideoTime);
+ MOZ_ASSERT(audioFrame->mTime >= expectedNextAudioTime);
+ if (videoFrame->mTime <= audioFrame->mTime) {
+ expectedNextVideoTime = videoFrame->GetEndTime();
+ RefPtr<EncodedFrame> frame = mEncodedVideoQueue.PopFront();
+ frames.AppendElement(std::move(frame));
+ } else {
+ expectedNextAudioTime = audioFrame->GetEndTime();
+ RefPtr<EncodedFrame> frame = mEncodedAudioQueue.PopFront();
+ frames.AppendElement(std::move(frame));
+ }
+ }
+
+ // If we're out of audio we still may be able to add more video...
+ if (mEncodedAudioQueue.GetSize() == 0) {
+ while (mEncodedVideoQueue.GetSize() > 0) {
+ if (!mEncodedAudioQueue.AtEndOfStream() &&
+ mEncodedVideoQueue.PeekFront()->mTime > expectedNextAudioTime) {
+ // Audio encoding is not complete and since the video frame comes
+ // after our next audio frame we cannot safely add it.
+ break;
+ }
+ frames.AppendElement(mEncodedVideoQueue.PopFront());
+ }
+ }
+
+ // If we're out of video we still may be able to add more audio...
+ if (mEncodedVideoQueue.GetSize() == 0) {
+ while (mEncodedAudioQueue.GetSize() > 0) {
+ if (!mEncodedVideoQueue.AtEndOfStream() &&
+ mEncodedAudioQueue.PeekFront()->mTime > expectedNextVideoTime) {
+ // Video encoding is not complete and since the audio frame comes
+ // after our next video frame we cannot safely add it.
+ break;
+ }
+ frames.AppendElement(mEncodedAudioQueue.PopFront());
+ }
+ }
+
+ LOG(LogLevel::Debug,
+ "%p Muxed data, remaining-audio=%zu, remaining-video=%zu", this,
+ mEncodedAudioQueue.GetSize(), mEncodedVideoQueue.GetSize());
+
+ // If encoding is complete for both encoders we should signal end of stream,
+ // otherwise we keep going.
+ uint32_t flags =
+ mEncodedVideoQueue.AtEndOfStream() && mEncodedAudioQueue.AtEndOfStream()
+ ? ContainerWriter::END_OF_STREAM
+ : 0;
+ nsresult rv = mWriter->WriteEncodedTrack(frames, flags);
+ if (NS_FAILED(rv)) {
+ LOG(LogLevel::Error, "Error! Failed to write muxed data to the container");
+ }
+ return rv;
+}
+
+} // namespace mozilla
+
+#undef LOG
diff --git a/dom/media/encoder/Muxer.h b/dom/media/encoder/Muxer.h
new file mode 100644
index 0000000000..983e260230
--- /dev/null
+++ b/dom/media/encoder/Muxer.h
@@ -0,0 +1,71 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_ENCODER_MUXER_H_
+#define DOM_MEDIA_ENCODER_MUXER_H_
+
+#include "MediaQueue.h"
+#include "mozilla/media/MediaUtils.h"
+
+namespace mozilla {
+
+class ContainerWriter;
+class EncodedFrame;
+class TrackMetadataBase;
+
+// Generic Muxer class that helps pace the output from track encoders to the
+// ContainerWriter, so time never appears to go backwards.
+// Note that the entire class is written for single threaded access.
+class Muxer {
+ public:
+ Muxer(UniquePtr<ContainerWriter> aWriter,
+ MediaQueue<EncodedFrame>& aEncodedAudioQueue,
+ MediaQueue<EncodedFrame>& aEncodedVideoQueue);
+ ~Muxer() = default;
+
+ // Disconnects MediaQueues such that they will no longer be consumed.
+ // Idempotent.
+ void Disconnect();
+
+ // Returns true when all tracks have ended, and all data has been muxed and
+ // fetched.
+ bool IsFinished();
+
+ // Returns true if this muxer has not been given metadata yet.
+ bool NeedsMetadata() const { return !mMetadataSet; }
+
+ // Sets metadata for all tracks. This may only be called once.
+ nsresult SetMetadata(const nsTArray<RefPtr<TrackMetadataBase>>& aMetadata);
+
+ // Gets the data that has been muxed and written into the container so far.
+ nsresult GetData(nsTArray<nsTArray<uint8_t>>* aOutputBuffers);
+
+ private:
+ // Writes data in MediaQueues to the ContainerWriter.
+ nsresult Mux();
+
+ // Audio frames that have been encoded and are pending write to the muxer.
+ MediaQueue<EncodedFrame>& mEncodedAudioQueue;
+ // Video frames that have been encoded and are pending write to the muxer.
+ MediaQueue<EncodedFrame>& mEncodedVideoQueue;
+ // Listeners driving the muxing as encoded data gets produced.
+ MediaEventListener mAudioPushListener;
+ MediaEventListener mAudioFinishListener;
+ MediaEventListener mVideoPushListener;
+ MediaEventListener mVideoFinishListener;
+ // The writer for the specific container we're recording into.
+ UniquePtr<ContainerWriter> mWriter;
+ // True once metadata has been set in the muxer.
+ bool mMetadataSet = false;
+ // True once metadata has been written to file.
+ bool mMetadataEncoded = false;
+ // True if metadata is set and contains an audio track.
+ bool mHasAudio = false;
+ // True if metadata is set and contains a video track.
+ bool mHasVideo = false;
+};
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/encoder/OpusTrackEncoder.cpp b/dom/media/encoder/OpusTrackEncoder.cpp
new file mode 100644
index 0000000000..16b71d378e
--- /dev/null
+++ b/dom/media/encoder/OpusTrackEncoder.cpp
@@ -0,0 +1,454 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "OpusTrackEncoder.h"
+#include "nsString.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/ProfilerLabels.h"
+#include "VideoUtils.h"
+
+#include <opus/opus.h>
+
+#define LOG(args, ...)
+
+namespace mozilla {
+
+// The Opus format supports up to 8 channels, and supports multitrack audio up
+// to 255 channels, but the current implementation supports only mono and
+// stereo, and downmixes any more than that.
+constexpr int MAX_SUPPORTED_AUDIO_CHANNELS = 8;
+
+// http://www.opus-codec.org/docs/html_api-1.0.2/group__opus__encoder.html
+// In section "opus_encoder_init", channels must be 1 or 2 of input signal.
+constexpr int MAX_CHANNELS = 2;
+
+// A maximum data bytes for Opus to encode.
+constexpr int MAX_DATA_BYTES = 4096;
+
+// http://tools.ietf.org/html/draft-ietf-codec-oggopus-00#section-4
+// Second paragraph, " The granule position of an audio data page is in units
+// of PCM audio samples at a fixed rate of 48 kHz."
+constexpr int kOpusSamplingRate = 48000;
+
+// The duration of an Opus frame, and it must be 2.5, 5, 10, 20, 40 or 60 ms.
+constexpr int kFrameDurationMs = 20;
+
+// The supported sampling rate of input signal (Hz),
+// must be one of the following. Will resampled to 48kHz otherwise.
+constexpr int kOpusSupportedInputSamplingRates[] = {8000, 12000, 16000, 24000,
+ 48000};
+
+namespace {
+
+// An endian-neutral serialization of integers. Serializing T in little endian
+// format to aOutput, where T is a 16 bits or 32 bits integer.
+template <typename T>
+static void SerializeToBuffer(T aValue, nsTArray<uint8_t>* aOutput) {
+ for (uint32_t i = 0; i < sizeof(T); i++) {
+ aOutput->AppendElement((uint8_t)(0x000000ff & (aValue >> (i * 8))));
+ }
+}
+
+static inline void SerializeToBuffer(const nsCString& aComment,
+ nsTArray<uint8_t>* aOutput) {
+ // Format of serializing a string to buffer is, the length of string (32 bits,
+ // little endian), and the string.
+ SerializeToBuffer((uint32_t)(aComment.Length()), aOutput);
+ aOutput->AppendElements(aComment.get(), aComment.Length());
+}
+
+static void SerializeOpusIdHeader(uint8_t aChannelCount, uint16_t aPreskip,
+ uint32_t aInputSampleRate,
+ nsTArray<uint8_t>* aOutput) {
+ // The magic signature, null terminator has to be stripped off from strings.
+ constexpr uint8_t magic[] = "OpusHead";
+ aOutput->AppendElements(magic, sizeof(magic) - 1);
+
+ // The version must always be 1 (8 bits, unsigned).
+ aOutput->AppendElement(1);
+
+ // Number of output channels (8 bits, unsigned).
+ aOutput->AppendElement(aChannelCount);
+
+ // Number of samples (at 48 kHz) to discard from the decoder output when
+ // starting playback (16 bits, unsigned, little endian).
+ SerializeToBuffer(aPreskip, aOutput);
+
+ // The sampling rate of input source (32 bits, unsigned, little endian).
+ SerializeToBuffer(aInputSampleRate, aOutput);
+
+ // Output gain, an encoder should set this field to zero (16 bits, signed,
+ // little endian).
+ SerializeToBuffer((int16_t)0, aOutput);
+
+ // Channel mapping family. Family 0 allows only 1 or 2 channels (8 bits,
+ // unsigned).
+ aOutput->AppendElement(0);
+}
+
+static void SerializeOpusCommentHeader(const nsCString& aVendor,
+ const nsTArray<nsCString>& aComments,
+ nsTArray<uint8_t>* aOutput) {
+ // The magic signature, null terminator has to be stripped off.
+ constexpr uint8_t magic[] = "OpusTags";
+ aOutput->AppendElements(magic, sizeof(magic) - 1);
+
+ // The vendor; Should append in the following order:
+ // vendor string length (32 bits, unsigned, little endian)
+ // vendor string.
+ SerializeToBuffer(aVendor, aOutput);
+
+ // Add comments; Should append in the following order:
+ // comment list length (32 bits, unsigned, little endian)
+ // comment #0 string length (32 bits, unsigned, little endian)
+ // comment #0 string
+ // comment #1 string length (32 bits, unsigned, little endian)
+ // comment #1 string ...
+ SerializeToBuffer((uint32_t)aComments.Length(), aOutput);
+ for (uint32_t i = 0; i < aComments.Length(); ++i) {
+ SerializeToBuffer(aComments[i], aOutput);
+ }
+}
+
+bool IsSampleRateSupported(TrackRate aSampleRate) {
+ // According to www.opus-codec.org, creating an opus encoder requires the
+ // sampling rate of source signal be one of 8000, 12000, 16000, 24000, or
+ // 48000. If this constraint is not satisfied, we resample the input to 48kHz.
+ AutoTArray<int, 5> supportedSamplingRates;
+ supportedSamplingRates.AppendElements(
+ kOpusSupportedInputSamplingRates,
+ ArrayLength(kOpusSupportedInputSamplingRates));
+ return supportedSamplingRates.Contains(aSampleRate);
+}
+
+} // Anonymous namespace.
+
+OpusTrackEncoder::OpusTrackEncoder(TrackRate aTrackRate,
+ MediaQueue<EncodedFrame>& aEncodedDataQueue)
+ : AudioTrackEncoder(aTrackRate, aEncodedDataQueue),
+ mOutputSampleRate(IsSampleRateSupported(aTrackRate) ? aTrackRate
+ : kOpusSamplingRate),
+ mEncoder(nullptr),
+ mLookahead(0),
+ mLookaheadWritten(0),
+ mResampler(nullptr),
+ mNumOutputFrames(0) {}
+
+OpusTrackEncoder::~OpusTrackEncoder() {
+ if (mEncoder) {
+ opus_encoder_destroy(mEncoder);
+ }
+ if (mResampler) {
+ speex_resampler_destroy(mResampler);
+ mResampler = nullptr;
+ }
+}
+
+nsresult OpusTrackEncoder::Init(int aChannels) {
+ NS_ENSURE_TRUE((aChannels <= MAX_SUPPORTED_AUDIO_CHANNELS) && (aChannels > 0),
+ NS_ERROR_FAILURE);
+
+ // This version of encoder API only support 1 or 2 channels,
+ // So set the mChannels less or equal 2 and
+ // let InterleaveTrackData downmix pcm data.
+ mChannels = aChannels > MAX_CHANNELS ? MAX_CHANNELS : aChannels;
+
+ // Reject non-audio sample rates.
+ NS_ENSURE_TRUE(mTrackRate >= 8000, NS_ERROR_INVALID_ARG);
+ NS_ENSURE_TRUE(mTrackRate <= 192000, NS_ERROR_INVALID_ARG);
+
+ if (NeedsResampler()) {
+ int error;
+ mResampler = speex_resampler_init(mChannels, mTrackRate, kOpusSamplingRate,
+ SPEEX_RESAMPLER_QUALITY_DEFAULT, &error);
+
+ if (error != RESAMPLER_ERR_SUCCESS) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ int error = 0;
+ mEncoder = opus_encoder_create(mOutputSampleRate, mChannels,
+ OPUS_APPLICATION_AUDIO, &error);
+
+ if (error != OPUS_OK) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (mAudioBitrate) {
+ int bps = static_cast<int>(
+ std::min<uint32_t>(mAudioBitrate, std::numeric_limits<int>::max()));
+ error = opus_encoder_ctl(mEncoder, OPUS_SET_BITRATE(bps));
+ if (error != OPUS_OK) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // In the case of Opus we need to calculate the codec delay based on the
+ // pre-skip. For more information see:
+ // https://tools.ietf.org/html/rfc7845#section-4.2
+ error = opus_encoder_ctl(mEncoder, OPUS_GET_LOOKAHEAD(&mLookahead));
+ if (error != OPUS_OK) {
+ mLookahead = 0;
+ return NS_ERROR_FAILURE;
+ }
+
+ SetInitialized();
+
+ return NS_OK;
+}
+
+int OpusTrackEncoder::GetLookahead() const {
+ return mLookahead * kOpusSamplingRate / mOutputSampleRate;
+}
+
+int OpusTrackEncoder::NumInputFramesPerPacket() const {
+ return mTrackRate * kFrameDurationMs / 1000;
+}
+
+int OpusTrackEncoder::NumOutputFramesPerPacket() const {
+ return mOutputSampleRate * kFrameDurationMs / 1000;
+}
+
+bool OpusTrackEncoder::NeedsResampler() const {
+ // A resampler is needed when mTrackRate is not supported by the opus encoder.
+ // This is equivalent to !IsSampleRateSupported(mTrackRate) but less cycles.
+ return mTrackRate != mOutputSampleRate &&
+ mOutputSampleRate == kOpusSamplingRate;
+}
+
+already_AddRefed<TrackMetadataBase> OpusTrackEncoder::GetMetadata() {
+ AUTO_PROFILER_LABEL("OpusTrackEncoder::GetMetadata", OTHER);
+
+ MOZ_ASSERT(mInitialized);
+
+ if (!mInitialized) {
+ return nullptr;
+ }
+
+ RefPtr<OpusMetadata> meta = new OpusMetadata();
+ meta->mChannels = mChannels;
+ meta->mSamplingFrequency = mTrackRate;
+
+ // Ogg and Webm timestamps are always sampled at 48k for Opus.
+ SerializeOpusIdHeader(mChannels,
+ mLookahead * (kOpusSamplingRate / mOutputSampleRate),
+ mTrackRate, &meta->mIdHeader);
+
+ nsCString vendor;
+ vendor.AppendASCII(opus_get_version_string());
+
+ nsTArray<nsCString> comments;
+ comments.AppendElement(
+ nsLiteralCString("ENCODER=Mozilla" MOZ_APP_UA_VERSION));
+
+ SerializeOpusCommentHeader(vendor, comments, &meta->mCommentHeader);
+
+ return meta.forget();
+}
+
+nsresult OpusTrackEncoder::Encode(AudioSegment* aSegment) {
+ AUTO_PROFILER_LABEL("OpusTrackEncoder::Encode", OTHER);
+
+ MOZ_ASSERT(aSegment);
+ MOZ_ASSERT(mInitialized || mCanceled);
+
+ if (mCanceled || IsEncodingComplete()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!mInitialized) {
+ // calculation below depends on the truth that mInitialized is true.
+ return NS_ERROR_FAILURE;
+ }
+
+ int result = 0;
+ // Loop until we run out of packets of input data
+ while (result >= 0 && !IsEncodingComplete()) {
+ // re-sampled frames left last time which didn't fit into an Opus packet
+ // duration.
+ const int framesLeft = mResampledLeftover.Length() / mChannels;
+ MOZ_ASSERT(NumOutputFramesPerPacket() >= framesLeft);
+ // Fetch input frames such that there will be n frames where (n +
+ // framesLeft) >= NumOutputFramesPerPacket() after re-sampling.
+ const int framesToFetch = NumInputFramesPerPacket() -
+ (framesLeft * mTrackRate / kOpusSamplingRate) +
+ (NeedsResampler() ? 1 : 0);
+
+ if (!mEndOfStream && aSegment->GetDuration() < framesToFetch) {
+ // Not enough raw data
+ return NS_OK;
+ }
+
+ // Start encoding data.
+ AutoTArray<AudioDataValue, 9600> pcm;
+ pcm.SetLength(NumOutputFramesPerPacket() * mChannels);
+
+ int frameCopied = 0;
+
+ for (AudioSegment::ChunkIterator iter(*aSegment);
+ !iter.IsEnded() && frameCopied < framesToFetch; iter.Next()) {
+ AudioChunk chunk = *iter;
+
+ // Chunk to the required frame size.
+ TrackTime frameToCopy =
+ std::min(chunk.GetDuration(),
+ static_cast<TrackTime>(framesToFetch - frameCopied));
+
+ // Possible greatest value of framesToFetch = 3844: see
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1349421#c8. frameToCopy
+ // should not be able to exceed this value.
+ MOZ_ASSERT(frameToCopy <= 3844, "frameToCopy exceeded expected range");
+
+ if (!chunk.IsNull()) {
+ // Append the interleaved data to the end of pcm buffer.
+ AudioTrackEncoder::InterleaveTrackData(
+ chunk, frameToCopy, mChannels,
+ pcm.Elements() + frameCopied * mChannels);
+ } else {
+ CheckedInt<int> memsetLength =
+ CheckedInt<int>(frameToCopy) * mChannels * sizeof(AudioDataValue);
+ if (!memsetLength.isValid()) {
+ // This should never happen, but we use a defensive check because
+ // we really don't want a bad memset
+ MOZ_ASSERT_UNREACHABLE("memsetLength invalid!");
+ return NS_ERROR_FAILURE;
+ }
+ memset(pcm.Elements() + frameCopied * mChannels, 0,
+ memsetLength.value());
+ }
+
+ frameCopied += frameToCopy;
+ }
+
+ // Possible greatest value of framesToFetch = 3844: see
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1349421#c8. frameCopied
+ // should not be able to exceed this value.
+ MOZ_ASSERT(frameCopied <= 3844, "frameCopied exceeded expected range");
+
+ int framesInPCM = frameCopied;
+ if (mResampler) {
+ AutoTArray<AudioDataValue, 9600> resamplingDest;
+ uint32_t inframes = frameCopied;
+ uint32_t outframes = inframes * kOpusSamplingRate / mTrackRate + 1;
+
+ // We want to consume all the input data, so we slightly oversize the
+ // resampled data buffer so we can fit the output data in. We cannot
+ // really predict the output frame count at each call.
+ resamplingDest.SetLength(outframes * mChannels);
+
+#if MOZ_SAMPLE_TYPE_S16
+ short* in = reinterpret_cast<short*>(pcm.Elements());
+ short* out = reinterpret_cast<short*>(resamplingDest.Elements());
+ speex_resampler_process_interleaved_int(mResampler, in, &inframes, out,
+ &outframes);
+#else
+ float* in = reinterpret_cast<float*>(pcm.Elements());
+ float* out = reinterpret_cast<float*>(resamplingDest.Elements());
+ speex_resampler_process_interleaved_float(mResampler, in, &inframes, out,
+ &outframes);
+#endif
+
+ MOZ_ASSERT(pcm.Length() >= mResampledLeftover.Length());
+ PodCopy(pcm.Elements(), mResampledLeftover.Elements(),
+ mResampledLeftover.Length());
+
+ uint32_t outframesToCopy = std::min(
+ outframes,
+ static_cast<uint32_t>(NumOutputFramesPerPacket() - framesLeft));
+
+ MOZ_ASSERT(pcm.Length() - mResampledLeftover.Length() >=
+ outframesToCopy * mChannels);
+ PodCopy(pcm.Elements() + mResampledLeftover.Length(),
+ resamplingDest.Elements(), outframesToCopy * mChannels);
+ int frameLeftover = outframes - outframesToCopy;
+ mResampledLeftover.SetLength(frameLeftover * mChannels);
+ PodCopy(mResampledLeftover.Elements(),
+ resamplingDest.Elements() + outframesToCopy * mChannels,
+ mResampledLeftover.Length());
+ // This is always at 48000Hz.
+ framesInPCM = framesLeft + outframesToCopy;
+ }
+
+ // Remove the raw data which has been pulled to pcm buffer.
+ // The value of frameCopied should be equal to (or smaller than, if eos)
+ // NumOutputFramesPerPacket().
+ aSegment->RemoveLeading(frameCopied);
+
+ // Has reached the end of input stream and all queued data has pulled for
+ // encoding.
+ bool isFinalPacket = false;
+ if (aSegment->GetDuration() == 0 && mEndOfStream &&
+ framesInPCM < NumOutputFramesPerPacket()) {
+ // Pad |mLookahead| samples to the end of the track to prevent loss of
+ // original data.
+ const int toWrite = std::min(mLookahead - mLookaheadWritten,
+ NumOutputFramesPerPacket() - framesInPCM);
+ PodZero(pcm.Elements() + framesInPCM * mChannels, toWrite * mChannels);
+ mLookaheadWritten += toWrite;
+ framesInPCM += toWrite;
+ if (mLookaheadWritten == mLookahead) {
+ isFinalPacket = true;
+ }
+ }
+
+ MOZ_ASSERT_IF(!isFinalPacket, framesInPCM == NumOutputFramesPerPacket());
+
+ // Append null data to pcm buffer if the leftover data is not enough for
+ // opus encoder.
+ if (framesInPCM < NumOutputFramesPerPacket() && isFinalPacket) {
+ PodZero(pcm.Elements() + framesInPCM * mChannels,
+ (NumOutputFramesPerPacket() - framesInPCM) * mChannels);
+ }
+ auto frameData = MakeRefPtr<EncodedFrame::FrameData>();
+ // Encode the data with Opus Encoder.
+ frameData->SetLength(MAX_DATA_BYTES);
+ // result is returned as opus error code if it is negative.
+ result = 0;
+#ifdef MOZ_SAMPLE_TYPE_S16
+ const opus_int16* pcmBuf = static_cast<opus_int16*>(pcm.Elements());
+ result = opus_encode(mEncoder, pcmBuf, NumOutputFramesPerPacket(),
+ frameData->Elements(), MAX_DATA_BYTES);
+#else
+ const float* pcmBuf = static_cast<float*>(pcm.Elements());
+ result = opus_encode_float(mEncoder, pcmBuf, NumOutputFramesPerPacket(),
+ frameData->Elements(), MAX_DATA_BYTES);
+#endif
+ frameData->SetLength(result >= 0 ? result : 0);
+
+ if (result < 0) {
+ LOG("[Opus] Fail to encode data! Result: %s.", opus_strerror(result));
+ }
+ if (isFinalPacket) {
+ if (mResampler) {
+ speex_resampler_destroy(mResampler);
+ mResampler = nullptr;
+ }
+ mResampledLeftover.SetLength(0);
+ }
+
+ // timestamp should be the time of the first sample
+ mEncodedDataQueue.Push(MakeAndAddRef<EncodedFrame>(
+ media::TimeUnit(mNumOutputFrames + mLookahead, mOutputSampleRate),
+ static_cast<uint64_t>(framesInPCM) * kOpusSamplingRate /
+ mOutputSampleRate,
+ kOpusSamplingRate, EncodedFrame::OPUS_AUDIO_FRAME,
+ std::move(frameData)));
+
+ mNumOutputFrames += NumOutputFramesPerPacket();
+ LOG("[Opus] mOutputTimeStamp %.3f.",
+ media::TimeUnit(mNumOutputFrames, mOutputSampleRate).ToSeconds());
+
+ if (isFinalPacket) {
+ LOG("[Opus] Done encoding.");
+ mEncodedDataQueue.Finish();
+ }
+ }
+
+ return result >= 0 ? NS_OK : NS_ERROR_FAILURE;
+}
+
+} // namespace mozilla
+
+#undef LOG
diff --git a/dom/media/encoder/OpusTrackEncoder.h b/dom/media/encoder/OpusTrackEncoder.h
new file mode 100644
index 0000000000..5206944169
--- /dev/null
+++ b/dom/media/encoder/OpusTrackEncoder.h
@@ -0,0 +1,117 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef OpusTrackEncoder_h_
+#define OpusTrackEncoder_h_
+
+#include <stdint.h>
+#include <speex/speex_resampler.h>
+#include "TimeUnits.h"
+#include "TrackEncoder.h"
+
+struct OpusEncoder;
+
+namespace mozilla {
+
+// Opus meta data structure
+class OpusMetadata : public TrackMetadataBase {
+ public:
+ // The ID Header of OggOpus. refer to http://wiki.xiph.org/OggOpus.
+ nsTArray<uint8_t> mIdHeader;
+ // The Comment Header of OggOpus.
+ nsTArray<uint8_t> mCommentHeader;
+ int32_t mChannels;
+ float mSamplingFrequency;
+ MetadataKind GetKind() const override { return METADATA_OPUS; }
+};
+
+class OpusTrackEncoder : public AudioTrackEncoder {
+ public:
+ OpusTrackEncoder(TrackRate aTrackRate,
+ MediaQueue<EncodedFrame>& aEncodedDataQueue);
+ virtual ~OpusTrackEncoder();
+
+ already_AddRefed<TrackMetadataBase> GetMetadata() override;
+
+ /**
+ * The encoder lookahead at 48k rate.
+ */
+ int GetLookahead() const;
+
+ protected:
+ /**
+ * The number of frames, in the input rate mTrackRate, needed to fill an
+ * encoded opus packet. A frame is a sample per channel.
+ */
+ int NumInputFramesPerPacket() const override;
+
+ nsresult Init(int aChannels) override;
+
+ /**
+ * Encodes buffered data and pushes it to mEncodedDataQueue.
+ */
+ nsresult Encode(AudioSegment* aSegment) override;
+
+ /**
+ * The number of frames, in the output rate (see GetOutputSampleRate), needed
+ * to fill an encoded opus packet. A frame is a sample per channel.
+ */
+ int NumOutputFramesPerPacket() const;
+
+ /**
+ * True if the input needs to be resampled to be fed to the underlying opus
+ * encoder.
+ */
+ bool NeedsResampler() const;
+
+ public:
+ /**
+ * Get the samplerate of the data to be fed to the Opus encoder. This might be
+ * different from the input samplerate if resampling occurs.
+ */
+ const TrackRate mOutputSampleRate;
+
+ private:
+ /**
+ * The Opus encoder from libopus.
+ */
+ OpusEncoder* mEncoder;
+
+ /**
+ * Total samples of delay added by codec (in rate mOutputSampleRate), can
+ * be queried by the encoder. From the perspective of decoding, real data
+ * begins this many samples late, so the encoder needs to append this many
+ * null samples to the end of stream, in order to align the time of input and
+ * output.
+ */
+ int mLookahead;
+
+ /**
+ * Number of mLookahead samples that has been written. When non-zero and equal
+ * to mLookahead, encoding is complete.
+ */
+ int mLookaheadWritten;
+
+ /**
+ * If the input sample rate does not divide 48kHz evenly, the input data are
+ * resampled.
+ */
+ SpeexResamplerState* mResampler;
+
+ /**
+ * Store the resampled frames that don't fit into an Opus packet duration.
+ * They will be prepended to the resampled frames next encoding cycle.
+ */
+ nsTArray<AudioDataValue> mResampledLeftover;
+
+ /**
+ * Number of audio frames encoded, in kOpusSamplingRate.
+ */
+ uint64_t mNumOutputFrames;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/encoder/TrackEncoder.cpp b/dom/media/encoder/TrackEncoder.cpp
new file mode 100644
index 0000000000..8e03fd6fe3
--- /dev/null
+++ b/dom/media/encoder/TrackEncoder.cpp
@@ -0,0 +1,822 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TrackEncoder.h"
+
+#include "AudioChannelFormat.h"
+#include "DriftCompensation.h"
+#include "MediaTrackGraph.h"
+#include "MediaTrackListener.h"
+#include "mozilla/AbstractThread.h"
+#include "mozilla/Logging.h"
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/RollingMean.h"
+#include "VideoUtils.h"
+#include "mozilla/Telemetry.h"
+
+namespace mozilla {
+
+LazyLogModule gTrackEncoderLog("TrackEncoder");
+#define TRACK_LOG(type, msg) MOZ_LOG(gTrackEncoderLog, type, msg)
+
+constexpr int DEFAULT_CHANNELS = 1;
+constexpr int DEFAULT_FRAME_WIDTH = 640;
+constexpr int DEFAULT_FRAME_HEIGHT = 480;
+constexpr int DEFAULT_FRAME_RATE = 30;
+// 10 second threshold if the audio encoder cannot be initialized.
+constexpr int AUDIO_INIT_FAILED_DURATION = 10;
+// 30 second threshold if the video encoder cannot be initialized.
+constexpr int VIDEO_INIT_FAILED_DURATION = 30;
+constexpr int FRAMERATE_DETECTION_ROLLING_WINDOW = 3;
+constexpr size_t FRAMERATE_DETECTION_MIN_CHUNKS = 5;
+constexpr int FRAMERATE_DETECTION_MAX_DURATION_S = 6;
+
+TrackEncoder::TrackEncoder(TrackRate aTrackRate,
+ MediaQueue<EncodedFrame>& aEncodedDataQueue)
+ : mInitialized(false),
+ mStarted(false),
+ mEndOfStream(false),
+ mCanceled(false),
+ mInitCounter(0),
+ mSuspended(false),
+ mTrackRate(aTrackRate),
+ mEncodedDataQueue(aEncodedDataQueue) {}
+
+bool TrackEncoder::IsInitialized() {
+ MOZ_ASSERT(!mWorkerThread || mWorkerThread->IsCurrentThreadIn());
+ return mInitialized;
+}
+
+bool TrackEncoder::IsStarted() {
+ MOZ_ASSERT(!mWorkerThread || mWorkerThread->IsCurrentThreadIn());
+ return mStarted;
+}
+
+bool TrackEncoder::IsEncodingComplete() const {
+ MOZ_ASSERT(!mWorkerThread || mWorkerThread->IsCurrentThreadIn());
+ return mEncodedDataQueue.IsFinished();
+}
+
+void TrackEncoder::SetInitialized() {
+ MOZ_ASSERT(!mWorkerThread || mWorkerThread->IsCurrentThreadIn());
+
+ if (mInitialized) {
+ return;
+ }
+
+ mInitialized = true;
+
+ for (auto& l : mListeners.Clone()) {
+ l->Initialized(this);
+ }
+}
+
+void TrackEncoder::SetStarted() {
+ MOZ_ASSERT(!mWorkerThread || mWorkerThread->IsCurrentThreadIn());
+
+ if (mStarted) {
+ return;
+ }
+
+ mStarted = true;
+
+ for (auto& l : mListeners.Clone()) {
+ l->Started(this);
+ }
+}
+
+void TrackEncoder::OnError() {
+ MOZ_ASSERT(!mWorkerThread || mWorkerThread->IsCurrentThreadIn());
+
+ Cancel();
+
+ for (auto& l : mListeners.Clone()) {
+ l->Error(this);
+ }
+}
+
+void TrackEncoder::RegisterListener(TrackEncoderListener* aListener) {
+ MOZ_ASSERT(!mWorkerThread || mWorkerThread->IsCurrentThreadIn());
+ MOZ_ASSERT(!mListeners.Contains(aListener));
+ mListeners.AppendElement(aListener);
+}
+
+bool TrackEncoder::UnregisterListener(TrackEncoderListener* aListener) {
+ MOZ_ASSERT(!mWorkerThread || mWorkerThread->IsCurrentThreadIn());
+ return mListeners.RemoveElement(aListener);
+}
+
+void TrackEncoder::SetWorkerThread(AbstractThread* aWorkerThread) {
+ mWorkerThread = aWorkerThread;
+}
+
+void AudioTrackEncoder::Suspend() {
+ MOZ_ASSERT(!mWorkerThread || mWorkerThread->IsCurrentThreadIn());
+ TRACK_LOG(LogLevel::Info, ("[AudioTrackEncoder %p]: Suspend(), was %s", this,
+ mSuspended ? "suspended" : "live"));
+
+ if (mSuspended) {
+ return;
+ }
+
+ mSuspended = true;
+}
+
+void AudioTrackEncoder::Resume() {
+ MOZ_ASSERT(!mWorkerThread || mWorkerThread->IsCurrentThreadIn());
+ TRACK_LOG(LogLevel::Info, ("[AudioTrackEncoder %p]: Resume(), was %s", this,
+ mSuspended ? "suspended" : "live"));
+
+ if (!mSuspended) {
+ return;
+ }
+
+ mSuspended = false;
+}
+
+void AudioTrackEncoder::AppendAudioSegment(AudioSegment&& aSegment) {
+ MOZ_ASSERT(!mWorkerThread || mWorkerThread->IsCurrentThreadIn());
+ AUTO_PROFILER_LABEL("AudioTrackEncoder::AppendAudioSegment", OTHER);
+ TRACK_LOG(LogLevel::Verbose,
+ ("[AudioTrackEncoder %p]: AppendAudioSegment() duration=%" PRIu64,
+ this, aSegment.GetDuration()));
+
+ if (mCanceled) {
+ return;
+ }
+
+ if (mEndOfStream) {
+ return;
+ }
+
+ TryInit(mOutgoingBuffer, aSegment.GetDuration());
+
+ if (mSuspended) {
+ return;
+ }
+
+ SetStarted();
+ mOutgoingBuffer.AppendFrom(&aSegment);
+
+ if (!mInitialized) {
+ return;
+ }
+
+ if (NS_FAILED(Encode(&mOutgoingBuffer))) {
+ OnError();
+ return;
+ }
+
+ MOZ_ASSERT_IF(IsEncodingComplete(), mOutgoingBuffer.IsEmpty());
+}
+
+void AudioTrackEncoder::TryInit(const AudioSegment& aSegment,
+ TrackTime aDuration) {
+ MOZ_ASSERT(!mWorkerThread || mWorkerThread->IsCurrentThreadIn());
+
+ if (mInitialized) {
+ return;
+ }
+
+ mInitCounter++;
+ TRACK_LOG(LogLevel::Debug,
+ ("[AudioTrackEncoder %p]: Inited the audio encoder %d times", this,
+ mInitCounter));
+
+ for (AudioSegment::ConstChunkIterator iter(aSegment); !iter.IsEnded();
+ iter.Next()) {
+ // The number of channels is determined by the first non-null chunk, and
+ // thus the audio encoder is initialized at this time.
+ if (iter->IsNull()) {
+ continue;
+ }
+
+ nsresult rv = Init(iter->mChannelData.Length());
+
+ if (NS_SUCCEEDED(rv)) {
+ TRACK_LOG(LogLevel::Info,
+ ("[AudioTrackEncoder %p]: Successfully initialized!", this));
+ return;
+ } else {
+ TRACK_LOG(
+ LogLevel::Error,
+ ("[AudioTrackEncoder %p]: Failed to initialize the encoder!", this));
+ OnError();
+ return;
+ }
+ break;
+ }
+
+ mNotInitDuration += aDuration;
+ if (!mInitialized &&
+ ((mNotInitDuration - 1) / mTrackRate >= AUDIO_INIT_FAILED_DURATION) &&
+ mInitCounter > 1) {
+ // Perform a best effort initialization since we haven't gotten any
+ // data yet. Motivated by issues like Bug 1336367
+ TRACK_LOG(LogLevel::Warning,
+ ("[AudioTrackEncoder]: Initialize failed for %ds. Attempting to "
+ "init with %d (default) channels!",
+ AUDIO_INIT_FAILED_DURATION, DEFAULT_CHANNELS));
+ nsresult rv = Init(DEFAULT_CHANNELS);
+ if (NS_FAILED(rv)) {
+ TRACK_LOG(LogLevel::Error,
+ ("[AudioTrackEncoder %p]: Default-channel-init failed.", this));
+ OnError();
+ return;
+ }
+ }
+}
+
+void AudioTrackEncoder::Cancel() {
+ MOZ_ASSERT(!mWorkerThread || mWorkerThread->IsCurrentThreadIn());
+ TRACK_LOG(LogLevel::Info, ("[AudioTrackEncoder %p]: Cancel()", this));
+ mCanceled = true;
+ mEndOfStream = true;
+ mOutgoingBuffer.Clear();
+ mEncodedDataQueue.Finish();
+}
+
+void AudioTrackEncoder::NotifyEndOfStream() {
+ MOZ_ASSERT(!mWorkerThread || mWorkerThread->IsCurrentThreadIn());
+ TRACK_LOG(LogLevel::Info,
+ ("[AudioTrackEncoder %p]: NotifyEndOfStream()", this));
+
+ if (!mCanceled && !mInitialized) {
+ // If source audio track is completely silent till the end of encoding,
+ // initialize the encoder with a default channel count.
+ Init(DEFAULT_CHANNELS);
+ }
+
+ if (mEndOfStream) {
+ return;
+ }
+
+ mEndOfStream = true;
+
+ if (NS_FAILED(Encode(&mOutgoingBuffer))) {
+ mOutgoingBuffer.Clear();
+ OnError();
+ }
+
+ MOZ_ASSERT(mOutgoingBuffer.GetDuration() == 0);
+}
+
+/*static*/
+void AudioTrackEncoder::InterleaveTrackData(AudioChunk& aChunk,
+ int32_t aDuration,
+ uint32_t aOutputChannels,
+ AudioDataValue* aOutput) {
+ uint32_t numChannelsToCopy = std::min(
+ aOutputChannels, static_cast<uint32_t>(aChunk.mChannelData.Length()));
+ switch (aChunk.mBufferFormat) {
+ case AUDIO_FORMAT_S16: {
+ AutoTArray<const int16_t*, 2> array;
+ array.SetLength(numChannelsToCopy);
+ for (uint32_t i = 0; i < array.Length(); i++) {
+ array[i] = static_cast<const int16_t*>(aChunk.mChannelData[i]);
+ }
+ InterleaveTrackData(array, aDuration, aOutputChannels, aOutput,
+ aChunk.mVolume);
+ break;
+ }
+ case AUDIO_FORMAT_FLOAT32: {
+ AutoTArray<const float*, 2> array;
+ array.SetLength(numChannelsToCopy);
+ for (uint32_t i = 0; i < array.Length(); i++) {
+ array[i] = static_cast<const float*>(aChunk.mChannelData[i]);
+ }
+ InterleaveTrackData(array, aDuration, aOutputChannels, aOutput,
+ aChunk.mVolume);
+ break;
+ }
+ case AUDIO_FORMAT_SILENCE: {
+ MOZ_ASSERT(false, "To implement.");
+ }
+ };
+}
+
+/*static*/
+void AudioTrackEncoder::DeInterleaveTrackData(AudioDataValue* aInput,
+ int32_t aDuration,
+ int32_t aChannels,
+ AudioDataValue* aOutput) {
+ for (int32_t i = 0; i < aChannels; ++i) {
+ for (int32_t j = 0; j < aDuration; ++j) {
+ aOutput[i * aDuration + j] = aInput[i + j * aChannels];
+ }
+ }
+}
+
+size_t AudioTrackEncoder::SizeOfExcludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) {
+ MOZ_ASSERT(!mWorkerThread || mWorkerThread->IsCurrentThreadIn());
+ return mOutgoingBuffer.SizeOfExcludingThis(aMallocSizeOf);
+}
+
+VideoTrackEncoder::VideoTrackEncoder(
+ RefPtr<DriftCompensator> aDriftCompensator, TrackRate aTrackRate,
+ MediaQueue<EncodedFrame>& aEncodedDataQueue,
+ FrameDroppingMode aFrameDroppingMode)
+ : TrackEncoder(aTrackRate, aEncodedDataQueue),
+ mDriftCompensator(std::move(aDriftCompensator)),
+ mEncodedTicks(0),
+ mVideoBitrate(0),
+ mFrameDroppingMode(aFrameDroppingMode),
+ mEnabled(true) {
+ mLastChunk.mDuration = 0;
+}
+
+void VideoTrackEncoder::Suspend(const TimeStamp& aTime) {
+ MOZ_ASSERT(!mWorkerThread || mWorkerThread->IsCurrentThreadIn());
+ TRACK_LOG(LogLevel::Info,
+ ("[VideoTrackEncoder %p]: Suspend() at %.3fs, was %s", this,
+ mStartTime.IsNull() ? 0.0 : (aTime - mStartTime).ToSeconds(),
+ mSuspended ? "suspended" : "live"));
+
+ if (mSuspended) {
+ return;
+ }
+
+ mSuspended = true;
+ mSuspendTime = aTime;
+}
+
+void VideoTrackEncoder::Resume(const TimeStamp& aTime) {
+ MOZ_ASSERT(!mWorkerThread || mWorkerThread->IsCurrentThreadIn());
+
+ if (!mSuspended) {
+ return;
+ }
+
+ TRACK_LOG(
+ LogLevel::Info,
+ ("[VideoTrackEncoder %p]: Resume() after %.3fs, was %s", this,
+ (aTime - mSuspendTime).ToSeconds(), mSuspended ? "suspended" : "live"));
+
+ mSuspended = false;
+
+ TimeDuration suspendDuration = aTime - mSuspendTime;
+ if (!mLastChunk.mTimeStamp.IsNull()) {
+ VideoChunk* nextChunk = mIncomingBuffer.FindChunkContaining(aTime);
+ MOZ_ASSERT_IF(nextChunk, nextChunk->mTimeStamp <= aTime);
+ if (nextChunk) {
+ nextChunk->mTimeStamp = aTime;
+ }
+ mLastChunk.mTimeStamp += suspendDuration;
+ }
+ if (!mStartTime.IsNull()) {
+ mStartTime += suspendDuration;
+ }
+
+ mSuspendTime = TimeStamp();
+}
+
+void VideoTrackEncoder::Disable(const TimeStamp& aTime) {
+ MOZ_ASSERT(!mWorkerThread || mWorkerThread->IsCurrentThreadIn());
+ TRACK_LOG(LogLevel::Debug, ("[VideoTrackEncoder %p]: Disable()", this));
+
+ if (mStartTime.IsNull()) {
+ // We haven't started yet. No need to touch future frames.
+ mEnabled = false;
+ return;
+ }
+
+ // Advancing currentTime to process any frames in mIncomingBuffer between
+ // mCurrentTime and aTime.
+ AdvanceCurrentTime(aTime);
+ if (!mLastChunk.mTimeStamp.IsNull()) {
+ // Insert a black frame at t=aTime into mIncomingBuffer, to trigger the
+ // shift to black at the right moment.
+ VideoSegment tempSegment;
+ tempSegment.AppendFrom(&mIncomingBuffer);
+ mIncomingBuffer.AppendFrame(do_AddRef(mLastChunk.mFrame.GetImage()),
+ mLastChunk.mFrame.GetIntrinsicSize(),
+ mLastChunk.mFrame.GetPrincipalHandle(), true,
+ aTime);
+ mIncomingBuffer.AppendFrom(&tempSegment);
+ }
+ mEnabled = false;
+}
+
+void VideoTrackEncoder::Enable(const TimeStamp& aTime) {
+ MOZ_ASSERT(!mWorkerThread || mWorkerThread->IsCurrentThreadIn());
+ TRACK_LOG(LogLevel::Debug, ("[VideoTrackEncoder %p]: Enable()", this));
+
+ if (mStartTime.IsNull()) {
+ // We haven't started yet. No need to touch future frames.
+ mEnabled = true;
+ return;
+ }
+
+ // Advancing currentTime to process any frames in mIncomingBuffer between
+ // mCurrentTime and aTime.
+ AdvanceCurrentTime(aTime);
+ if (!mLastChunk.mTimeStamp.IsNull()) {
+ // Insert a real frame at t=aTime into mIncomingBuffer, to trigger the
+ // shift from black at the right moment.
+ VideoSegment tempSegment;
+ tempSegment.AppendFrom(&mIncomingBuffer);
+ mIncomingBuffer.AppendFrame(do_AddRef(mLastChunk.mFrame.GetImage()),
+ mLastChunk.mFrame.GetIntrinsicSize(),
+ mLastChunk.mFrame.GetPrincipalHandle(),
+ mLastChunk.mFrame.GetForceBlack(), aTime);
+ mIncomingBuffer.AppendFrom(&tempSegment);
+ }
+ mEnabled = true;
+}
+
+void VideoTrackEncoder::AppendVideoSegment(VideoSegment&& aSegment) {
+ MOZ_ASSERT(!mWorkerThread || mWorkerThread->IsCurrentThreadIn());
+ TRACK_LOG(LogLevel::Verbose,
+ ("[VideoTrackEncoder %p]: AppendVideoSegment()", this));
+
+ if (mCanceled) {
+ return;
+ }
+
+ if (mEndOfStream) {
+ return;
+ }
+
+ for (VideoSegment::ConstChunkIterator iter(aSegment); !iter.IsEnded();
+ iter.Next()) {
+ if (iter->IsNull()) {
+ // A null image was sent. This is a signal from the source that we should
+ // clear any images buffered in the future.
+ mIncomingBuffer.Clear();
+ continue; // Don't append iter, as it is null.
+ }
+ if (VideoChunk* c = mIncomingBuffer.GetLastChunk()) {
+ if (iter->mTimeStamp < c->mTimeStamp) {
+ // Time went backwards. This can happen when a MediaDecoder seeks.
+ // We need to handle this by removing any frames buffered in the future
+ // and start over at iter->mTimeStamp.
+ mIncomingBuffer.Clear();
+ }
+ }
+ SetStarted();
+ mIncomingBuffer.AppendFrame(do_AddRef(iter->mFrame.GetImage()),
+ iter->mFrame.GetIntrinsicSize(),
+ iter->mFrame.GetPrincipalHandle(),
+ iter->mFrame.GetForceBlack(), iter->mTimeStamp);
+ }
+ aSegment.Clear();
+}
+
+void VideoTrackEncoder::Init(const VideoSegment& aSegment,
+ const TimeStamp& aTime,
+ size_t aFrameRateDetectionMinChunks) {
+ MOZ_ASSERT(!mWorkerThread || mWorkerThread->IsCurrentThreadIn());
+ MOZ_ASSERT(!aTime.IsNull());
+
+ if (mInitialized) {
+ return;
+ }
+
+ mInitCounter++;
+ TRACK_LOG(LogLevel::Debug,
+ ("[VideoTrackEncoder %p]: Init the video encoder %d times", this,
+ mInitCounter));
+
+ Maybe<float> framerate;
+ if (!aSegment.IsEmpty()) {
+ // The number of whole frames, i.e., with known duration.
+ size_t frameCount = 0;
+ RollingMean<TimeDuration, TimeDuration> meanDuration(
+ FRAMERATE_DETECTION_ROLLING_WINDOW);
+ VideoSegment::ConstChunkIterator iter(aSegment);
+ TimeStamp previousChunkTime = iter->mTimeStamp;
+ iter.Next();
+ for (; !iter.IsEnded(); iter.Next(), ++frameCount) {
+ meanDuration.insert(iter->mTimeStamp - previousChunkTime);
+ previousChunkTime = iter->mTimeStamp;
+ }
+ TRACK_LOG(LogLevel::Debug, ("[VideoTrackEncoder %p]: Init() frameCount=%zu",
+ this, frameCount));
+ if (frameCount >= aFrameRateDetectionMinChunks) {
+ if (meanDuration.empty()) {
+ // No whole frames available, use aTime as end time.
+ framerate = Some(1.0f / (aTime - mStartTime).ToSeconds());
+ } else {
+ // We want some frames for estimating the framerate.
+ framerate = Some(1.0f / meanDuration.mean().ToSeconds());
+ }
+ } else if ((aTime - mStartTime).ToSeconds() >
+ FRAMERATE_DETECTION_MAX_DURATION_S) {
+ // Instead of failing init after the fail-timeout, we fallback to a very
+ // low rate.
+ framerate = Some(static_cast<float>(frameCount) /
+ (aTime - mStartTime).ToSeconds());
+ }
+ }
+
+ if (framerate) {
+ for (VideoSegment::ConstChunkIterator iter(aSegment); !iter.IsEnded();
+ iter.Next()) {
+ if (iter->IsNull()) {
+ continue;
+ }
+
+ gfx::IntSize imgsize = iter->mFrame.GetImage()->GetSize();
+ gfx::IntSize intrinsicSize = iter->mFrame.GetIntrinsicSize();
+ nsresult rv = Init(imgsize.width, imgsize.height, intrinsicSize.width,
+ intrinsicSize.height, *framerate);
+
+ if (NS_SUCCEEDED(rv)) {
+ TRACK_LOG(LogLevel::Info,
+ ("[VideoTrackEncoder %p]: Successfully initialized!", this));
+ return;
+ }
+
+ TRACK_LOG(
+ LogLevel::Error,
+ ("[VideoTrackEncoder %p]: Failed to initialize the encoder!", this));
+ OnError();
+ break;
+ }
+ }
+
+ if (((aTime - mStartTime).ToSeconds() > VIDEO_INIT_FAILED_DURATION) &&
+ mInitCounter > 1) {
+ TRACK_LOG(LogLevel::Warning,
+ ("[VideoTrackEncoder %p]: No successful init for %ds.", this,
+ VIDEO_INIT_FAILED_DURATION));
+ OnError();
+ return;
+ }
+}
+
+void VideoTrackEncoder::Cancel() {
+ MOZ_ASSERT(!mWorkerThread || mWorkerThread->IsCurrentThreadIn());
+ TRACK_LOG(LogLevel::Info, ("[VideoTrackEncoder %p]: Cancel()", this));
+ mCanceled = true;
+ mEndOfStream = true;
+ mIncomingBuffer.Clear();
+ mOutgoingBuffer.Clear();
+ mLastChunk.SetNull(0);
+ mEncodedDataQueue.Finish();
+}
+
+void VideoTrackEncoder::NotifyEndOfStream() {
+ MOZ_ASSERT(!mWorkerThread || mWorkerThread->IsCurrentThreadIn());
+
+ if (mCanceled) {
+ return;
+ }
+
+ if (mEndOfStream) {
+ // We have already been notified.
+ return;
+ }
+
+ mEndOfStream = true;
+ TRACK_LOG(LogLevel::Info,
+ ("[VideoTrackEncoder %p]: NotifyEndOfStream()", this));
+
+ if (!mLastChunk.IsNull()) {
+ RefPtr<layers::Image> lastImage = mLastChunk.mFrame.GetImage();
+ const TimeStamp now = TimeStamp::Now();
+ TimeStamp currentTime = mSuspended ? mSuspendTime : mCurrentTime;
+ currentTime = mDriftCompensator->GetVideoTime(now, currentTime);
+ TimeDuration absoluteEndTime = currentTime - mStartTime;
+ CheckedInt64 duration =
+ UsecsToFrames(absoluteEndTime.ToMicroseconds(), mTrackRate) -
+ mEncodedTicks;
+ if (duration.isValid() && duration.value() > 0) {
+ mEncodedTicks += duration.value();
+ TRACK_LOG(LogLevel::Debug,
+ ("[VideoTrackEncoder %p]: Appending last video frame %p at pos "
+ "%.3fs, "
+ "track-end=%.3fs",
+ this, lastImage.get(),
+ (mLastChunk.mTimeStamp - mStartTime).ToSeconds(),
+ absoluteEndTime.ToSeconds()));
+ mOutgoingBuffer.AppendFrame(
+ lastImage.forget(), mLastChunk.mFrame.GetIntrinsicSize(),
+ PRINCIPAL_HANDLE_NONE, mLastChunk.mFrame.GetForceBlack() || !mEnabled,
+ mLastChunk.mTimeStamp);
+ mOutgoingBuffer.ExtendLastFrameBy(duration.value());
+ }
+
+ if (!mInitialized) {
+ // Try to init without waiting for an accurate framerate.
+ Init(mOutgoingBuffer, currentTime, 0);
+ }
+ }
+
+ if (mCanceled) {
+ // Previous Init failed and we got canceled. Nothing to do here.
+ return;
+ }
+
+ mIncomingBuffer.Clear();
+ mLastChunk.SetNull(0);
+
+ if (NS_WARN_IF(!mInitialized)) {
+ // Still not initialized. There was probably no real frame at all, perhaps
+ // by muting. Initialize the encoder with default frame width, frame
+ // height, and frame rate.
+ Init(DEFAULT_FRAME_WIDTH, DEFAULT_FRAME_HEIGHT, DEFAULT_FRAME_WIDTH,
+ DEFAULT_FRAME_HEIGHT, DEFAULT_FRAME_RATE);
+ }
+
+ if (NS_FAILED(Encode(&mOutgoingBuffer))) {
+ OnError();
+ }
+
+ MOZ_ASSERT(mOutgoingBuffer.IsEmpty());
+}
+
+void VideoTrackEncoder::SetStartOffset(const TimeStamp& aStartOffset) {
+ MOZ_ASSERT(!mWorkerThread || mWorkerThread->IsCurrentThreadIn());
+ MOZ_ASSERT(mCurrentTime.IsNull());
+ TRACK_LOG(LogLevel::Info, ("[VideoTrackEncoder %p]: SetStartOffset()", this));
+ mStartTime = aStartOffset;
+ mCurrentTime = aStartOffset;
+}
+
+void VideoTrackEncoder::AdvanceCurrentTime(const TimeStamp& aTime) {
+ AUTO_PROFILER_LABEL("VideoTrackEncoder::AdvanceCurrentTime", OTHER);
+
+ MOZ_ASSERT(!mWorkerThread || mWorkerThread->IsCurrentThreadIn());
+ MOZ_ASSERT(!mStartTime.IsNull());
+ MOZ_ASSERT(!mCurrentTime.IsNull());
+
+ if (mCanceled) {
+ return;
+ }
+
+ if (mEndOfStream) {
+ return;
+ }
+
+ if (mSuspended) {
+ TRACK_LOG(
+ LogLevel::Verbose,
+ ("[VideoTrackEncoder %p]: AdvanceCurrentTime() suspended at %.3fs",
+ this, (mCurrentTime - mStartTime).ToSeconds()));
+ mCurrentTime = aTime;
+ mIncomingBuffer.ForgetUpToTime(mCurrentTime);
+ return;
+ }
+
+ TRACK_LOG(LogLevel::Verbose,
+ ("[VideoTrackEncoder %p]: AdvanceCurrentTime() to %.3fs", this,
+ (aTime - mStartTime).ToSeconds()));
+
+ // Grab frames within the currentTime range from the incoming buffer.
+ VideoSegment tempSegment;
+ {
+ VideoChunk* previousChunk = &mLastChunk;
+ auto appendDupes = [&](const TimeStamp& aUpTo) {
+ while ((aUpTo - previousChunk->mTimeStamp).ToSeconds() > 1.0) {
+ // We encode at least one frame per second, even if there are none
+ // flowing.
+ previousChunk->mTimeStamp += TimeDuration::FromSeconds(1.0);
+ tempSegment.AppendFrame(
+ do_AddRef(previousChunk->mFrame.GetImage()),
+ previousChunk->mFrame.GetIntrinsicSize(),
+ previousChunk->mFrame.GetPrincipalHandle(),
+ previousChunk->mFrame.GetForceBlack() || !mEnabled,
+ previousChunk->mTimeStamp);
+ TRACK_LOG(
+ LogLevel::Verbose,
+ ("[VideoTrackEncoder %p]: Duplicating video frame (%p) at pos %.3f",
+ this, previousChunk->mFrame.GetImage(),
+ (previousChunk->mTimeStamp - mStartTime).ToSeconds()));
+ }
+ };
+ for (VideoSegment::ChunkIterator iter(mIncomingBuffer); !iter.IsEnded();
+ iter.Next()) {
+ MOZ_ASSERT(!iter->IsNull());
+ if (!previousChunk->IsNull() &&
+ iter->mTimeStamp <= previousChunk->mTimeStamp) {
+ // This frame starts earlier than previousChunk. Skip.
+ continue;
+ }
+ if (iter->mTimeStamp >= aTime) {
+ // This frame starts in the future. Stop.
+ break;
+ }
+ if (!previousChunk->IsNull()) {
+ appendDupes(iter->mTimeStamp);
+ }
+ tempSegment.AppendFrame(
+ do_AddRef(iter->mFrame.GetImage()), iter->mFrame.GetIntrinsicSize(),
+ iter->mFrame.GetPrincipalHandle(),
+ iter->mFrame.GetForceBlack() || !mEnabled, iter->mTimeStamp);
+ TRACK_LOG(LogLevel::Verbose,
+ ("[VideoTrackEncoder %p]: Taking video frame (%p) at pos %.3f",
+ this, iter->mFrame.GetImage(),
+ (iter->mTimeStamp - mStartTime).ToSeconds()));
+ previousChunk = &*iter;
+ }
+ if (!previousChunk->IsNull()) {
+ appendDupes(aTime);
+ }
+ }
+ mCurrentTime = aTime;
+ mIncomingBuffer.ForgetUpToTime(mCurrentTime);
+
+ // Convert tempSegment timestamps to durations and add chunks with known
+ // duration to mOutgoingBuffer.
+ const TimeStamp now = TimeStamp::Now();
+ for (VideoSegment::ConstChunkIterator iter(tempSegment); !iter.IsEnded();
+ iter.Next()) {
+ VideoChunk chunk = *iter;
+
+ if (mLastChunk.mTimeStamp.IsNull()) {
+ // This is the first real chunk in the track. Make it start at the
+ // beginning of the track.
+ MOZ_ASSERT(!iter->mTimeStamp.IsNull());
+
+ TRACK_LOG(
+ LogLevel::Verbose,
+ ("[VideoTrackEncoder %p]: Got the first video frame (%p) at pos %.3f "
+ "(moving it to beginning)",
+ this, iter->mFrame.GetImage(),
+ (iter->mTimeStamp - mStartTime).ToSeconds()));
+
+ mLastChunk = *iter;
+ mLastChunk.mTimeStamp = mStartTime;
+ continue;
+ }
+
+ MOZ_ASSERT(!mLastChunk.IsNull());
+ MOZ_ASSERT(!chunk.IsNull());
+
+ TimeDuration absoluteEndTime =
+ mDriftCompensator->GetVideoTime(now, chunk.mTimeStamp) - mStartTime;
+ TRACK_LOG(LogLevel::Verbose,
+ ("[VideoTrackEncoder %p]: Appending video frame %p, at pos %.3fs "
+ "until %.3fs",
+ this, mLastChunk.mFrame.GetImage(),
+ (mDriftCompensator->GetVideoTime(now, mLastChunk.mTimeStamp) -
+ mStartTime)
+ .ToSeconds(),
+ absoluteEndTime.ToSeconds()));
+ CheckedInt64 duration =
+ UsecsToFrames(absoluteEndTime.ToMicroseconds(), mTrackRate) -
+ mEncodedTicks;
+ if (!duration.isValid()) {
+ NS_ERROR("Duration overflow");
+ return;
+ }
+
+ if (duration.value() <= 0) {
+ // A frame either started before the last frame (can happen when
+ // multiple frames are added before SetStartOffset), or
+ // two frames were so close together that they ended up at the same
+ // position. We handle both cases by ignoring the previous frame.
+
+ TRACK_LOG(LogLevel::Verbose,
+ ("[VideoTrackEncoder %p]: Duration from frame %p to frame %p "
+ "is %" PRId64 ". Ignoring %p",
+ this, mLastChunk.mFrame.GetImage(), iter->mFrame.GetImage(),
+ duration.value(), mLastChunk.mFrame.GetImage()));
+
+ TimeStamp t = mLastChunk.mTimeStamp;
+ mLastChunk = *iter;
+ mLastChunk.mTimeStamp = t;
+ continue;
+ }
+
+ mEncodedTicks += duration.value();
+ mOutgoingBuffer.AppendFrame(
+ do_AddRef(mLastChunk.mFrame.GetImage()),
+ mLastChunk.mFrame.GetIntrinsicSize(), PRINCIPAL_HANDLE_NONE,
+ mLastChunk.mFrame.GetForceBlack() || !mEnabled, mLastChunk.mTimeStamp);
+ mOutgoingBuffer.ExtendLastFrameBy(duration.value());
+ mLastChunk = chunk;
+ }
+
+ if (mOutgoingBuffer.IsEmpty()) {
+ return;
+ }
+
+ Init(mOutgoingBuffer, mCurrentTime, FRAMERATE_DETECTION_MIN_CHUNKS);
+
+ if (!mInitialized) {
+ return;
+ }
+
+ if (NS_FAILED(Encode(&mOutgoingBuffer))) {
+ OnError();
+ return;
+ }
+
+ MOZ_ASSERT(mOutgoingBuffer.IsEmpty());
+}
+
+size_t VideoTrackEncoder::SizeOfExcludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) {
+ MOZ_ASSERT(!mWorkerThread || mWorkerThread->IsCurrentThreadIn());
+ return mIncomingBuffer.SizeOfExcludingThis(aMallocSizeOf) +
+ mOutgoingBuffer.SizeOfExcludingThis(aMallocSizeOf);
+}
+
+} // namespace mozilla
+
+#undef TRACK_LOG
diff --git a/dom/media/encoder/TrackEncoder.h b/dom/media/encoder/TrackEncoder.h
new file mode 100644
index 0000000000..879949874f
--- /dev/null
+++ b/dom/media/encoder/TrackEncoder.h
@@ -0,0 +1,501 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef TrackEncoder_h_
+#define TrackEncoder_h_
+
+#include "AudioSegment.h"
+#include "EncodedFrame.h"
+#include "MediaQueue.h"
+#include "MediaTrackGraph.h"
+#include "TrackMetadataBase.h"
+#include "VideoSegment.h"
+
+namespace mozilla {
+
+class AbstractThread;
+class DriftCompensator;
+class TrackEncoder;
+
+class TrackEncoderListener {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(TrackEncoderListener)
+
+ /**
+ * Called when the TrackEncoder has received its first real data.
+ */
+ virtual void Started(TrackEncoder* aEncoder) = 0;
+
+ /**
+ * Called when the TrackEncoder's underlying encoder has been successfully
+ * initialized and there's non-null data ready to be encoded.
+ */
+ virtual void Initialized(TrackEncoder* aEncoder) = 0;
+
+ /**
+ * Called after the TrackEncoder hit an unexpected error, causing it to
+ * abort operation.
+ */
+ virtual void Error(TrackEncoder* aEncoder) = 0;
+
+ protected:
+ virtual ~TrackEncoderListener() = default;
+};
+
+/**
+ * Base class of AudioTrackEncoder and VideoTrackEncoder. Lifetime managed by
+ * MediaEncoder. All methods are to be called only on the worker thread.
+ *
+ * The control APIs are all called by MediaEncoder on its dedicated thread. Data
+ * is encoded as soon as it has been appended (and time has advanced past its
+ * end in case of video) and pushed to mEncodedDataQueue.
+ */
+class TrackEncoder {
+ public:
+ TrackEncoder(TrackRate aTrackRate,
+ MediaQueue<EncodedFrame>& aEncodedDataQueue);
+
+ /**
+ * Called by MediaEncoder to cancel the encoding.
+ */
+ virtual void Cancel() = 0;
+
+ /**
+ * Notifies us that we have reached the end of the stream and no more data
+ * will be appended.
+ */
+ virtual void NotifyEndOfStream() = 0;
+
+ /**
+ * Creates and sets up meta data for a specific codec, called on the worker
+ * thread.
+ */
+ virtual already_AddRefed<TrackMetadataBase> GetMetadata() = 0;
+
+ /**
+ * MediaQueue containing encoded data, that is pushed as soon as it's ready.
+ */
+ MediaQueue<EncodedFrame>& EncodedDataQueue() { return mEncodedDataQueue; }
+
+ /**
+ * Returns true once this TrackEncoder is initialized.
+ */
+ bool IsInitialized();
+
+ /**
+ * Returns true once this TrackEncoder has received some data.
+ */
+ bool IsStarted();
+
+ /**
+ * True if the track encoder has encoded all source segments coming from
+ * MediaTrackGraph. Call on the worker thread.
+ */
+ bool IsEncodingComplete() const;
+
+ /**
+ * Registers a listener to events from this TrackEncoder.
+ * We hold a strong reference to the listener.
+ */
+ void RegisterListener(TrackEncoderListener* aListener);
+
+ /**
+ * Unregisters a listener from events from this TrackEncoder.
+ * The listener will stop receiving events synchronously.
+ */
+ bool UnregisterListener(TrackEncoderListener* aListener);
+
+ virtual void SetBitrate(const uint32_t aBitrate) = 0;
+
+ /**
+ * It's optional to set the worker thread, but if you do we'll assert that
+ * we are in the worker thread in every method that gets called.
+ */
+ void SetWorkerThread(AbstractThread* aWorkerThread);
+
+ /**
+ * Measure size of internal buffers.
+ */
+ virtual size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) = 0;
+
+ protected:
+ virtual ~TrackEncoder() { MOZ_ASSERT(mListeners.IsEmpty()); }
+
+ /**
+ * If this TrackEncoder was not already initialized, it is set to initialized
+ * and listeners are notified.
+ */
+ void SetInitialized();
+
+ /**
+ * If this TrackEncoder was not already marked started, its started state is
+ * set and listeners are notified.
+ */
+ void SetStarted();
+
+ /**
+ * Called after an error. Cancels the encoding and notifies listeners.
+ */
+ void OnError();
+
+ /**
+ * True if the track encoder has been initialized successfully.
+ */
+ bool mInitialized;
+
+ /**
+ * True if the track encoder has received data.
+ */
+ bool mStarted;
+
+ /**
+ * True once all data until the end of the input track has been received.
+ */
+ bool mEndOfStream;
+
+ /**
+ * True once this encoding has been cancelled.
+ */
+ bool mCanceled;
+
+ // How many times we have tried to initialize the encoder.
+ uint32_t mInitCounter;
+
+ /**
+ * True if this TrackEncoder is currently suspended.
+ */
+ bool mSuspended;
+
+ /**
+ * The track rate of source media.
+ */
+ const TrackRate mTrackRate;
+
+ /**
+ * If set we assert that all methods are called on this thread.
+ */
+ RefPtr<AbstractThread> mWorkerThread;
+
+ /**
+ * MediaQueue where encoded data ends up. Note that metadata goes out of band.
+ */
+ MediaQueue<EncodedFrame>& mEncodedDataQueue;
+
+ nsTArray<RefPtr<TrackEncoderListener>> mListeners;
+};
+
+class AudioTrackEncoder : public TrackEncoder {
+ public:
+ AudioTrackEncoder(TrackRate aTrackRate,
+ MediaQueue<EncodedFrame>& aEncodedDataQueue)
+ : TrackEncoder(aTrackRate, aEncodedDataQueue),
+ mChannels(0),
+ mNotInitDuration(0),
+ mAudioBitrate(0) {}
+
+ /**
+ * Suspends encoding from now, i.e., all future audio data received through
+ * AppendAudioSegment() until the next Resume() will be dropped.
+ */
+ void Suspend();
+
+ /**
+ * Resumes encoding starting now, i.e., data from the next
+ * AppendAudioSegment() will get encoded.
+ */
+ void Resume();
+
+ /**
+ * Appends and consumes track data from aSegment.
+ */
+ void AppendAudioSegment(AudioSegment&& aSegment);
+
+ template <typename T>
+ static void InterleaveTrackData(nsTArray<const T*>& aInput, int32_t aDuration,
+ uint32_t aOutputChannels,
+ AudioDataValue* aOutput, float aVolume) {
+ if (aInput.Length() < aOutputChannels) {
+ // Up-mix. This might make the mChannelData have more than aChannels.
+ AudioChannelsUpMix(&aInput, aOutputChannels,
+ SilentChannel::ZeroChannel<T>());
+ }
+
+ if (aInput.Length() > aOutputChannels) {
+ DownmixAndInterleave(aInput, aDuration, aVolume, aOutputChannels,
+ aOutput);
+ } else {
+ InterleaveAndConvertBuffer(aInput.Elements(), aDuration, aVolume,
+ aOutputChannels, aOutput);
+ }
+ }
+
+ /**
+ * Interleaves the track data and stores the result into aOutput. Might need
+ * to up-mix or down-mix the channel data if the channels number of this chunk
+ * is different from aOutputChannels. The channel data from aChunk might be
+ * modified by up-mixing.
+ */
+ static void InterleaveTrackData(AudioChunk& aChunk, int32_t aDuration,
+ uint32_t aOutputChannels,
+ AudioDataValue* aOutput);
+
+ /**
+ * De-interleaves the aInput data and stores the result into aOutput.
+ * No up-mix or down-mix operations inside.
+ */
+ static void DeInterleaveTrackData(AudioDataValue* aInput, int32_t aDuration,
+ int32_t aChannels, AudioDataValue* aOutput);
+
+ /**
+ * Measure size of internal buffers.
+ */
+ size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) override;
+
+ void SetBitrate(const uint32_t aBitrate) override {
+ mAudioBitrate = aBitrate;
+ }
+
+ /**
+ * Tries to initiate the AudioEncoder based on data in aSegment.
+ * This can be re-called often, as it will exit early should we already be
+ * initiated. mInitiated will only be set if there was enough data in
+ * aSegment to infer metadata. If mInitiated gets set, listeners are notified.
+ *
+ * Not having enough data in aSegment to initiate the encoder for an
+ * accumulated aDuration of one second will make us initiate with a default
+ * number of channels.
+ *
+ * If we attempt to initiate the underlying encoder but fail, we Cancel() and
+ * notify listeners.
+ */
+ void TryInit(const AudioSegment& aSegment, TrackTime aDuration);
+
+ void Cancel() override;
+
+ /**
+ * Dispatched from MediaTrackGraph when we have finished feeding data to
+ * mOutgoingBuffer.
+ */
+ void NotifyEndOfStream() override;
+
+ protected:
+ /**
+ * Number of samples per channel in a pcm buffer. This is also the value of
+ * frame size required by audio encoder, and listeners will be notified when
+ * at least this much data has been added to mOutgoingBuffer.
+ */
+ virtual int NumInputFramesPerPacket() const { return 0; }
+
+ /**
+ * Initializes the audio encoder. The call of this method is delayed until we
+ * have received the first valid track from MediaTrackGraph.
+ */
+ virtual nsresult Init(int aChannels) = 0;
+
+ /**
+ * Encodes buffered data and pushes it to mEncodedDataQueue.
+ */
+ virtual nsresult Encode(AudioSegment* aSegment) = 0;
+
+ /**
+ * The number of channels are used for processing PCM data in the audio
+ * encoder. This value comes from the first valid audio chunk. If encoder
+ * can't support the channels in the chunk, downmix PCM stream can be
+ * performed. This value also be used to initialize the audio encoder.
+ */
+ int mChannels;
+
+ /**
+ * A segment queue of outgoing audio track data to the encoder.
+ * The contents of mOutgoingBuffer will always be what has been appended on
+ * the encoder thread but not yet consumed by the encoder sub class.
+ */
+ AudioSegment mOutgoingBuffer;
+
+ TrackTime mNotInitDuration;
+
+ uint32_t mAudioBitrate;
+};
+
+enum class FrameDroppingMode {
+ ALLOW, // Allowed to drop frames to keep up under load
+ DISALLOW, // Must not drop any frames, even if it means we will OOM
+};
+
+class VideoTrackEncoder : public TrackEncoder {
+ public:
+ VideoTrackEncoder(RefPtr<DriftCompensator> aDriftCompensator,
+ TrackRate aTrackRate,
+ MediaQueue<EncodedFrame>& aEncodedDataQueue,
+ FrameDroppingMode aFrameDroppingMode);
+
+ /**
+ * Suspends encoding from aTime, i.e., all video frame with a timestamp
+ * between aTime and the timestamp of the next Resume() will be dropped.
+ */
+ void Suspend(const TimeStamp& aTime);
+
+ /**
+ * Resumes encoding starting at aTime.
+ */
+ void Resume(const TimeStamp& aTime);
+
+ /**
+ * Makes the video black from aTime.
+ */
+ void Disable(const TimeStamp& aTime);
+
+ /**
+ * Makes the video non-black from aTime.
+ *
+ * NB that it could still be forced black for other reasons, like principals.
+ */
+ void Enable(const TimeStamp& aTime);
+
+ /**
+ * Appends source video frames to mIncomingBuffer. We only append the source
+ * chunk if the image is different from mLastChunk's image. Called on the
+ * MediaTrackGraph thread.
+ */
+ void AppendVideoSegment(VideoSegment&& aSegment);
+
+ /**
+ * Measure size of internal buffers.
+ */
+ size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) override;
+
+ void SetBitrate(const uint32_t aBitrate) override {
+ mVideoBitrate = aBitrate;
+ }
+
+ /**
+ * Tries to initiate the VideoEncoder based on data in aSegment.
+ * This can be re-called often, as it will exit early should we already be
+ * initiated. mInitiated will only be set if there was enough data in
+ * aSegment to infer metadata. If mInitiated gets set, listeners are notified.
+ * The amount of chunks needed can be controlled by
+ * aFrameRateDetectionMinChunks which denotes the minimum number of chunks
+ * needed to infer the framerate.
+ *
+ * Failing to initiate the encoder for an accumulated aDuration of 30 seconds
+ * is seen as an error and will cancel the current encoding.
+ */
+ void Init(const VideoSegment& aSegment, const TimeStamp& aTime,
+ size_t aFrameRateDetectionMinChunks);
+
+ TrackTime SecondsToMediaTime(double aS) const {
+ NS_ASSERTION(0 <= aS && aS <= TRACK_TICKS_MAX / TRACK_RATE_MAX,
+ "Bad seconds");
+ return mTrackRate * aS;
+ }
+
+ /**
+ * MediaTrackGraph notifies us about the time of the track's start.
+ * This gets called on the MediaEncoder thread after a dispatch.
+ */
+ void SetStartOffset(const TimeStamp& aStartOffset);
+
+ void Cancel() override;
+
+ /**
+ * Notifies us that we have reached the end of the stream and no more data
+ * will be appended to mIncomingBuffer.
+ */
+ void NotifyEndOfStream() override;
+
+ /**
+ * Dispatched from MediaTrackGraph when it has run an iteration so we can
+ * hand more data to the encoder.
+ */
+ void AdvanceCurrentTime(const TimeStamp& aTime);
+
+ protected:
+ /**
+ * Initialize the video encoder. In order to collect the value of width and
+ * height of source frames, this initialization is delayed until we have
+ * received the first valid video frame from MediaTrackGraph.
+ * Listeners will be notified after it has been successfully initialized.
+ */
+ virtual nsresult Init(int32_t aWidth, int32_t aHeight, int32_t aDisplayWidth,
+ int32_t aDisplayHeight, float aEstimatedFrameRate) = 0;
+
+ /**
+ * Encodes data in the outgoing buffer and pushes it to mEncodedDataQueue.
+ */
+ virtual nsresult Encode(VideoSegment* aSegment) = 0;
+
+ /**
+ * Drift compensator for re-clocking incoming video frame wall-clock
+ * timestamps to audio time.
+ */
+ const RefPtr<DriftCompensator> mDriftCompensator;
+
+ /**
+ * The last unique frame and duration so far handled by
+ * NotifyAdvanceCurrentTime. When a new frame is detected, mLastChunk is added
+ * to mOutgoingBuffer.
+ */
+ VideoChunk mLastChunk;
+
+ /**
+ * A segment queue of incoming video track data, from listeners.
+ * The duration of mIncomingBuffer is irrelevant as we only look at TimeStamps
+ * of frames. Consumed data is replaced by null data.
+ */
+ VideoSegment mIncomingBuffer;
+
+ /**
+ * A segment queue of outgoing video track data to the encoder.
+ * The contents of mOutgoingBuffer will always be what has been consumed from
+ * mIncomingBuffer (up to mCurrentTime) but not yet consumed by the encoder
+ * sub class. There won't be any null data at the beginning of mOutgoingBuffer
+ * unless explicitly pushed by the producer.
+ */
+ VideoSegment mOutgoingBuffer;
+
+ /**
+ * The number of mTrackRate ticks we have passed to mOutgoingBuffer.
+ */
+ TrackTime mEncodedTicks;
+
+ /**
+ * The time up to which we have forwarded data from mIncomingBuffer to
+ * mOutgoingBuffer.
+ */
+ TimeStamp mCurrentTime;
+
+ /**
+ * The time the video track started, so the start of the video track can be
+ * synced to the start of the audio track.
+ *
+ * Note that this time will progress during suspension, to make sure the
+ * incoming frames stay in sync with the output.
+ */
+ TimeStamp mStartTime;
+
+ /**
+ * The time Suspend was called on the MediaRecorder, so we can calculate the
+ * duration on the next Resume().
+ */
+ TimeStamp mSuspendTime;
+
+ uint32_t mVideoBitrate;
+
+ /**
+ * ALLOW to drop frames under load.
+ * DISALLOW to encode all frames, mainly for testing.
+ */
+ FrameDroppingMode mFrameDroppingMode;
+
+ /**
+ * True if the video MediaTrackTrack this VideoTrackEncoder is attached to is
+ * currently enabled. While false, we encode all frames as black.
+ */
+ bool mEnabled;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/encoder/TrackMetadataBase.h b/dom/media/encoder/TrackMetadataBase.h
new file mode 100644
index 0000000000..503b52e5ec
--- /dev/null
+++ b/dom/media/encoder/TrackMetadataBase.h
@@ -0,0 +1,76 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef TrackMetadataBase_h_
+#define TrackMetadataBase_h_
+
+#include "nsTArray.h"
+#include "nsCOMPtr.h"
+namespace mozilla {
+
+// A class represent meta data for various codec format. Only support one track
+// information.
+class TrackMetadataBase {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(TrackMetadataBase)
+ enum MetadataKind {
+ METADATA_OPUS, // Represent the Opus metadata
+ METADATA_VP8,
+ METADATA_VORBIS,
+ METADATA_AVC,
+ METADATA_AAC,
+ METADATA_AMR,
+ METADATA_EVRC,
+ METADATA_UNKNOWN // Metadata Kind not set
+ };
+ // Return the specific metadata kind
+ virtual MetadataKind GetKind() const = 0;
+
+ protected:
+ // Protected destructor, to discourage deletion outside of Release():
+ virtual ~TrackMetadataBase() = default;
+};
+
+// The base class for audio metadata.
+class AudioTrackMetadata : public TrackMetadataBase {
+ public:
+ // The duration of each sample set generated by encoder. (counted by samples)
+ // If the duration is variant, this value should return 0.
+ virtual uint32_t GetAudioFrameDuration() = 0;
+
+ // The size of each sample set generated by encoder. (counted by byte)
+ // If the size is variant, this value should return 0.
+ virtual uint32_t GetAudioFrameSize() = 0;
+
+ // AudioSampleRate is the number of audio sample per second.
+ virtual uint32_t GetAudioSampleRate() = 0;
+
+ virtual uint32_t GetAudioChannels() = 0;
+};
+
+// The base class for video metadata.
+class VideoTrackMetadata : public TrackMetadataBase {
+ public:
+ // VideoHeight and VideoWidth are the frame size of the elementary stream.
+ virtual uint32_t GetVideoHeight() = 0;
+ virtual uint32_t GetVideoWidth() = 0;
+
+ // VideoDisplayHeight and VideoDisplayWidth are the display frame size.
+ virtual uint32_t GetVideoDisplayHeight() = 0;
+ virtual uint32_t GetVideoDisplayWidth() = 0;
+
+ // VideoClockRate is the number of samples per second in video frame's
+ // timestamp.
+ // For example, if VideoClockRate is 90k Hz and VideoFrameRate is
+ // 30 fps, each frame's sample duration will be 3000 Hz.
+ virtual uint32_t GetVideoClockRate() = 0;
+
+ // VideoFrameRate is numner of frames per second.
+ virtual uint32_t GetVideoFrameRate() = 0;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/encoder/VP8TrackEncoder.cpp b/dom/media/encoder/VP8TrackEncoder.cpp
new file mode 100644
index 0000000000..6412592ed1
--- /dev/null
+++ b/dom/media/encoder/VP8TrackEncoder.cpp
@@ -0,0 +1,720 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "VP8TrackEncoder.h"
+
+#include "DriftCompensation.h"
+#include "ImageToI420.h"
+#include "mozilla/gfx/2D.h"
+#include "prsystem.h"
+#include "VideoSegment.h"
+#include "VideoUtils.h"
+#include "vpx/vp8cx.h"
+#include "vpx/vpx_encoder.h"
+#include "WebMWriter.h"
+#include "mozilla/media/MediaUtils.h"
+#include "mozilla/dom/ImageUtils.h"
+#include "mozilla/dom/ImageBitmapBinding.h"
+#include "mozilla/ProfilerLabels.h"
+
+namespace mozilla {
+
+LazyLogModule gVP8TrackEncoderLog("VP8TrackEncoder");
+#define VP8LOG(level, msg, ...) \
+ MOZ_LOG(gVP8TrackEncoderLog, level, (msg, ##__VA_ARGS__))
+
+constexpr int DEFAULT_BITRATE_BPS = 2500000;
+constexpr int DEFAULT_KEYFRAME_INTERVAL_MS = 10000;
+constexpr int DYNAMIC_MAXKFDIST_CHECK_INTERVAL = 5;
+constexpr float DYNAMIC_MAXKFDIST_DIFFACTOR = 0.4;
+constexpr float DYNAMIC_MAXKFDIST_KFINTERVAL_FACTOR = 0.75;
+constexpr int I420_STRIDE_ALIGN = 16;
+
+using namespace mozilla::gfx;
+using namespace mozilla::layers;
+using namespace mozilla::media;
+using namespace mozilla::dom;
+
+namespace {
+
+template <int N>
+static int Aligned(int aValue) {
+ if (aValue < N) {
+ return N;
+ }
+
+ // The `- 1` avoids overreaching when `aValue % N == 0`.
+ return (((aValue - 1) / N) + 1) * N;
+}
+
+template <int Alignment>
+size_t I420Size(int aWidth, int aHeight) {
+ int yStride = Aligned<Alignment>(aWidth);
+ int yHeight = aHeight;
+ size_t yPlaneSize = yStride * yHeight;
+
+ int uvStride = Aligned<Alignment>((aWidth + 1) / 2);
+ int uvHeight = (aHeight + 1) / 2;
+ size_t uvPlaneSize = uvStride * uvHeight;
+
+ return yPlaneSize + uvPlaneSize * 2;
+}
+
+nsresult CreateEncoderConfig(int32_t aWidth, int32_t aHeight,
+ uint32_t aVideoBitrate, TrackRate aTrackRate,
+ int32_t aMaxKeyFrameDistance,
+ vpx_codec_enc_cfg_t* config) {
+ // Encoder configuration structure.
+ memset(config, 0, sizeof(vpx_codec_enc_cfg_t));
+ if (vpx_codec_enc_config_default(vpx_codec_vp8_cx(), config, 0)) {
+ VP8LOG(LogLevel::Error, "Failed to get default configuration");
+ return NS_ERROR_FAILURE;
+ }
+
+ config->g_w = aWidth;
+ config->g_h = aHeight;
+ // TODO: Maybe we should have various aFrameRate bitrate pair for each
+ // devices? or for different platform
+
+ // rc_target_bitrate needs kbit/s
+ config->rc_target_bitrate = std::max(
+ 1U, (aVideoBitrate != 0 ? aVideoBitrate : DEFAULT_BITRATE_BPS) / 1000);
+
+ // Setting the time base of the codec
+ config->g_timebase.num = 1;
+ config->g_timebase.den = aTrackRate;
+
+ // No error resilience as this is not intended for UDP transports
+ config->g_error_resilient = 0;
+
+ // Allow some frame lagging for large timeslices (when low latency is not
+ // needed)
+ /*std::min(10U, mKeyFrameInterval / 200)*/
+ config->g_lag_in_frames = 0;
+
+ int32_t number_of_cores = PR_GetNumberOfProcessors();
+ if (aWidth * aHeight > 1920 * 1080 && number_of_cores >= 8) {
+ config->g_threads = 4; // 4 threads for > 1080p.
+ } else if (aWidth * aHeight > 1280 * 960 && number_of_cores >= 6) {
+ config->g_threads = 3; // 3 threads for 1080p.
+ } else if (aWidth * aHeight > 640 * 480 && number_of_cores >= 3) {
+ config->g_threads = 2; // 2 threads for qHD/HD.
+ } else {
+ config->g_threads = 1; // 1 thread for VGA or less
+ }
+
+ // rate control settings
+
+ // No frame dropping
+ config->rc_dropframe_thresh = 0;
+ // Variable bitrate
+ config->rc_end_usage = VPX_VBR;
+ // Single pass encoding
+ config->g_pass = VPX_RC_ONE_PASS;
+ // ffmpeg doesn't currently support streams that use resize.
+ // Therefore, for safety, we should turn it off until it does.
+ config->rc_resize_allowed = 0;
+ // Allows 100% under target bitrate to compensate for prior overshoot
+ config->rc_undershoot_pct = 100;
+ // Allows 15% over target bitrate to compensate for prior undershoot
+ config->rc_overshoot_pct = 15;
+ // Tells the decoding application to buffer 500ms before beginning playback
+ config->rc_buf_initial_sz = 500;
+ // The decoding application will try to keep 600ms of buffer during playback
+ config->rc_buf_optimal_sz = 600;
+ // The decoding application may buffer 1000ms worth of encoded data
+ config->rc_buf_sz = 1000;
+
+ // We set key frame interval to automatic and try to set kf_max_dist so that
+ // the encoder chooses to put keyframes slightly more often than
+ // mKeyFrameInterval (which will encode with VPX_EFLAG_FORCE_KF when reached).
+ config->kf_mode = VPX_KF_AUTO;
+ config->kf_max_dist = aMaxKeyFrameDistance;
+
+ return NS_OK;
+}
+} // namespace
+
+VP8TrackEncoder::VP8TrackEncoder(RefPtr<DriftCompensator> aDriftCompensator,
+ TrackRate aTrackRate,
+ MediaQueue<EncodedFrame>& aEncodedDataQueue,
+ FrameDroppingMode aFrameDroppingMode,
+ Maybe<float> aKeyFrameIntervalFactor)
+ : VideoTrackEncoder(std::move(aDriftCompensator), aTrackRate,
+ aEncodedDataQueue, aFrameDroppingMode),
+ mKeyFrameInterval(
+ TimeDuration::FromMilliseconds(DEFAULT_KEYFRAME_INTERVAL_MS)),
+ mKeyFrameIntervalFactor(aKeyFrameIntervalFactor.valueOr(
+ DYNAMIC_MAXKFDIST_KFINTERVAL_FACTOR)) {
+ MOZ_COUNT_CTOR(VP8TrackEncoder);
+ CalculateMaxKeyFrameDistance().apply(
+ [&](auto aKfd) { SetMaxKeyFrameDistance(aKfd); });
+}
+
+VP8TrackEncoder::~VP8TrackEncoder() {
+ Destroy();
+ MOZ_COUNT_DTOR(VP8TrackEncoder);
+}
+
+void VP8TrackEncoder::Destroy() {
+ if (mInitialized) {
+ vpx_codec_destroy(&mVPXContext);
+ }
+
+ mInitialized = false;
+}
+
+Maybe<int32_t> VP8TrackEncoder::CalculateMaxKeyFrameDistance(
+ Maybe<float> aEstimatedFrameRate /* = Nothing() */) const {
+ if (!aEstimatedFrameRate && mMeanFrameDuration.empty()) {
+ // Not enough data to make a new calculation.
+ return Nothing();
+ }
+
+ // Calculate an estimation of our current framerate
+ const float estimatedFrameRate = aEstimatedFrameRate.valueOrFrom(
+ [&] { return 1.0f / mMeanFrameDuration.mean().ToSeconds(); });
+ // Set a kf_max_dist that should avoid triggering the VPX_EFLAG_FORCE_KF flag
+ return Some(std::max(
+ 1, static_cast<int32_t>(estimatedFrameRate * mKeyFrameIntervalFactor *
+ mKeyFrameInterval.ToSeconds())));
+}
+
+void VP8TrackEncoder::SetMaxKeyFrameDistance(int32_t aMaxKeyFrameDistance) {
+ if (mInitialized) {
+ VP8LOG(
+ LogLevel::Debug,
+ "%p SetMaxKeyFrameDistance() set kf_max_dist to %d based on estimated "
+ "framerate %.2ffps keyframe-factor %.2f and keyframe-interval %.2fs",
+ this, aMaxKeyFrameDistance, 1 / mMeanFrameDuration.mean().ToSeconds(),
+ mKeyFrameIntervalFactor, mKeyFrameInterval.ToSeconds());
+ DebugOnly<nsresult> rv =
+ Reconfigure(mFrameWidth, mFrameHeight, aMaxKeyFrameDistance);
+ MOZ_ASSERT(
+ NS_SUCCEEDED(rv),
+ "Reconfig for new key frame distance with proven size should succeed");
+ } else {
+ VP8LOG(LogLevel::Debug, "%p SetMaxKeyFrameDistance() distance=%d", this,
+ aMaxKeyFrameDistance);
+ mMaxKeyFrameDistance = Some(aMaxKeyFrameDistance);
+ }
+}
+
+nsresult VP8TrackEncoder::Init(int32_t aWidth, int32_t aHeight,
+ int32_t aDisplayWidth, int32_t aDisplayHeight,
+ float aEstimatedFrameRate) {
+ if (aDisplayWidth < 1 || aDisplayHeight < 1) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (aEstimatedFrameRate <= 0) {
+ return NS_ERROR_FAILURE;
+ }
+
+ int32_t maxKeyFrameDistance =
+ *CalculateMaxKeyFrameDistance(Some(aEstimatedFrameRate));
+
+ nsresult rv = InitInternal(aWidth, aHeight, maxKeyFrameDistance);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ MOZ_ASSERT(!mI420Frame);
+ MOZ_ASSERT(mI420FrameSize == 0);
+ const size_t neededSize = I420Size<I420_STRIDE_ALIGN>(aWidth, aHeight);
+ mI420Frame.reset(new (fallible) uint8_t[neededSize]);
+ mI420FrameSize = mI420Frame ? neededSize : 0;
+ if (!mI420Frame) {
+ VP8LOG(LogLevel::Warning, "Allocating I420 frame of size %zu failed",
+ neededSize);
+ return NS_ERROR_FAILURE;
+ }
+ vpx_img_wrap(&mVPXImageWrapper, VPX_IMG_FMT_I420, aWidth, aHeight,
+ I420_STRIDE_ALIGN, mI420Frame.get());
+
+ if (!mMetadata) {
+ mMetadata = MakeAndAddRef<VP8Metadata>();
+ mMetadata->mWidth = aWidth;
+ mMetadata->mHeight = aHeight;
+ mMetadata->mDisplayWidth = aDisplayWidth;
+ mMetadata->mDisplayHeight = aDisplayHeight;
+
+ VP8LOG(LogLevel::Info,
+ "%p Init() created metadata. width=%d, height=%d, displayWidth=%d, "
+ "displayHeight=%d, framerate=%.2f",
+ this, mMetadata->mWidth, mMetadata->mHeight,
+ mMetadata->mDisplayWidth, mMetadata->mDisplayHeight,
+ aEstimatedFrameRate);
+
+ SetInitialized();
+ }
+
+ return NS_OK;
+}
+
+nsresult VP8TrackEncoder::InitInternal(int32_t aWidth, int32_t aHeight,
+ int32_t aMaxKeyFrameDistance) {
+ if (aWidth < 1 || aHeight < 1) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (mInitialized) {
+ MOZ_ASSERT(false);
+ return NS_ERROR_FAILURE;
+ }
+
+ VP8LOG(LogLevel::Debug,
+ "%p InitInternal(). width=%d, height=%d, kf_max_dist=%d", this, aWidth,
+ aHeight, aMaxKeyFrameDistance);
+
+ // Encoder configuration structure.
+ vpx_codec_enc_cfg_t config;
+ nsresult rv = CreateEncoderConfig(aWidth, aHeight, mVideoBitrate, mTrackRate,
+ aMaxKeyFrameDistance, &config);
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE);
+
+ vpx_codec_flags_t flags = 0;
+ flags |= VPX_CODEC_USE_OUTPUT_PARTITION;
+ if (vpx_codec_enc_init(&mVPXContext, vpx_codec_vp8_cx(), &config, flags)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ vpx_codec_control(&mVPXContext, VP8E_SET_STATIC_THRESHOLD, 1);
+ vpx_codec_control(&mVPXContext, VP8E_SET_CPUUSED, 15);
+ vpx_codec_control(&mVPXContext, VP8E_SET_TOKEN_PARTITIONS,
+ VP8_TWO_TOKENPARTITION);
+
+ mFrameWidth = aWidth;
+ mFrameHeight = aHeight;
+ mMaxKeyFrameDistance = Some(aMaxKeyFrameDistance);
+
+ return NS_OK;
+}
+
+nsresult VP8TrackEncoder::Reconfigure(int32_t aWidth, int32_t aHeight,
+ int32_t aMaxKeyFrameDistance) {
+ if (aWidth <= 0 || aHeight <= 0) {
+ MOZ_ASSERT(false);
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!mInitialized) {
+ MOZ_ASSERT(false);
+ return NS_ERROR_FAILURE;
+ }
+
+ bool needsReInit = aMaxKeyFrameDistance != *mMaxKeyFrameDistance;
+
+ if (aWidth != mFrameWidth || aHeight != mFrameHeight) {
+ VP8LOG(LogLevel::Info, "Dynamic resolution change (%dx%d -> %dx%d).",
+ mFrameWidth, mFrameHeight, aWidth, aHeight);
+ const size_t neededSize = I420Size<I420_STRIDE_ALIGN>(aWidth, aHeight);
+ if (neededSize > mI420FrameSize) {
+ needsReInit = true;
+ mI420Frame.reset(new (fallible) uint8_t[neededSize]);
+ mI420FrameSize = mI420Frame ? neededSize : 0;
+ }
+ if (!mI420Frame) {
+ VP8LOG(LogLevel::Warning, "Allocating I420 frame of size %zu failed",
+ neededSize);
+ return NS_ERROR_FAILURE;
+ }
+ vpx_img_wrap(&mVPXImageWrapper, VPX_IMG_FMT_I420, aWidth, aHeight,
+ I420_STRIDE_ALIGN, mI420Frame.get());
+ }
+
+ if (needsReInit) {
+ Destroy();
+ mMaxKeyFrameDistance = Some(aMaxKeyFrameDistance);
+ nsresult rv = InitInternal(aWidth, aHeight, aMaxKeyFrameDistance);
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE);
+ mInitialized = true;
+ return NS_OK;
+ }
+
+ // Encoder configuration structure.
+ vpx_codec_enc_cfg_t config;
+ nsresult rv = CreateEncoderConfig(aWidth, aHeight, mVideoBitrate, mTrackRate,
+ aMaxKeyFrameDistance, &config);
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE);
+ // Set new configuration
+ if (vpx_codec_enc_config_set(&mVPXContext, &config) != VPX_CODEC_OK) {
+ VP8LOG(LogLevel::Error, "Failed to set new configuration");
+ return NS_ERROR_FAILURE;
+ }
+
+ mFrameWidth = aWidth;
+ mFrameHeight = aHeight;
+
+ return NS_OK;
+}
+
+already_AddRefed<TrackMetadataBase> VP8TrackEncoder::GetMetadata() {
+ AUTO_PROFILER_LABEL("VP8TrackEncoder::GetMetadata", OTHER);
+
+ MOZ_ASSERT(mInitialized);
+
+ if (!mInitialized) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(mMetadata);
+ return do_AddRef(mMetadata);
+}
+
+Result<RefPtr<EncodedFrame>, nsresult> VP8TrackEncoder::ExtractEncodedData() {
+ vpx_codec_iter_t iter = nullptr;
+ EncodedFrame::FrameType frameType = EncodedFrame::VP8_P_FRAME;
+ auto frameData = MakeRefPtr<EncodedFrame::FrameData>();
+ const vpx_codec_cx_pkt_t* pkt = nullptr;
+ while ((pkt = vpx_codec_get_cx_data(&mVPXContext, &iter)) != nullptr) {
+ switch (pkt->kind) {
+ case VPX_CODEC_CX_FRAME_PKT: {
+ // Copy the encoded data from libvpx to frameData
+ frameData->AppendElements((uint8_t*)pkt->data.frame.buf,
+ pkt->data.frame.sz);
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+ // End of frame
+ if ((pkt->data.frame.flags & VPX_FRAME_IS_FRAGMENT) == 0) {
+ if (pkt->data.frame.flags & VPX_FRAME_IS_KEY) {
+ frameType = EncodedFrame::VP8_I_FRAME;
+ }
+ break;
+ }
+ }
+
+ if (frameData->IsEmpty()) {
+ return RefPtr<EncodedFrame>(nullptr);
+ }
+
+ if (!pkt) {
+ // This check silences a coverity warning about accessing a null pkt below.
+ return RefPtr<EncodedFrame>(nullptr);
+ }
+
+ if (pkt->data.frame.flags & VPX_FRAME_IS_KEY) {
+ // Update the since-last-keyframe counter, and account for this frame's
+ // time.
+ TrackTime frameTime = pkt->data.frame.pts;
+ DebugOnly<TrackTime> frameDuration = pkt->data.frame.duration;
+ MOZ_ASSERT(frameTime + frameDuration <= mEncodedTimestamp);
+ mDurationSinceLastKeyframe =
+ std::min(mDurationSinceLastKeyframe, mEncodedTimestamp - frameTime);
+ }
+
+ // Convert the timestamp and duration to Usecs.
+ media::TimeUnit timestamp = media::TimeUnit(pkt->data.frame.pts, mTrackRate);
+ if (!timestamp.IsValid()) {
+ NS_ERROR("Microsecond timestamp overflow");
+ return Err(NS_ERROR_DOM_MEDIA_OVERFLOW_ERR);
+ }
+
+ mExtractedDuration += pkt->data.frame.duration;
+ if (!mExtractedDuration.isValid()) {
+ NS_ERROR("Duration overflow");
+ return Err(NS_ERROR_DOM_MEDIA_OVERFLOW_ERR);
+ }
+
+ media::TimeUnit totalDuration =
+ media::TimeUnit(mExtractedDuration.value(), mTrackRate);
+ if (!totalDuration.IsValid()) {
+ NS_ERROR("Duration overflow");
+ return Err(NS_ERROR_DOM_MEDIA_OVERFLOW_ERR);
+ }
+
+ media::TimeUnit duration = totalDuration - mExtractedDurationUs;
+ if (!duration.IsValid()) {
+ NS_ERROR("Duration overflow");
+ return Err(NS_ERROR_DOM_MEDIA_OVERFLOW_ERR);
+ }
+
+ mExtractedDurationUs = totalDuration;
+
+ VP8LOG(LogLevel::Verbose,
+ "ExtractEncodedData TimeStamp %.2f, Duration %.2f, FrameType %d",
+ timestamp.ToSeconds(), duration.ToSeconds(), frameType);
+
+ if (static_cast<int>(totalDuration.ToSeconds()) /
+ DYNAMIC_MAXKFDIST_CHECK_INTERVAL >
+ static_cast<int>(mLastKeyFrameDistanceUpdate.ToSeconds()) /
+ DYNAMIC_MAXKFDIST_CHECK_INTERVAL) {
+ // The interval has passed since the last keyframe update. Update again.
+ mLastKeyFrameDistanceUpdate = totalDuration;
+ const int32_t maxKfDistance =
+ CalculateMaxKeyFrameDistance().valueOr(*mMaxKeyFrameDistance);
+ const float diffFactor =
+ static_cast<float>(maxKfDistance) / *mMaxKeyFrameDistance;
+ VP8LOG(LogLevel::Debug, "maxKfDistance: %d, factor: %.2f", maxKfDistance,
+ diffFactor);
+ if (std::abs(1.0 - diffFactor) > DYNAMIC_MAXKFDIST_DIFFACTOR) {
+ SetMaxKeyFrameDistance(maxKfDistance);
+ }
+ }
+
+ return MakeRefPtr<EncodedFrame>(timestamp, duration.ToMicroseconds(),
+ PR_USEC_PER_SEC, frameType,
+ std::move(frameData));
+}
+
+/**
+ * Encoding flow in Encode():
+ * 1: Assert valid state.
+ * 2: Encode the video chunks in mSourceSegment in a for-loop.
+ * 2.1: The duration is taken straight from the video chunk's duration.
+ * 2.2: Setup the video chunk with mVPXImageWrapper by PrepareRawFrame().
+ * 2.3: Pass frame to vp8 encoder by vpx_codec_encode().
+ * 2.4: Extract the encoded frame from encoder by ExtractEncodedData().
+ * 2.5: Set the nextEncodeOperation for the next frame.
+ * 2.6: If we are not skipping the next frame, add the encoded frame to
+ * mEncodedDataQueue. If we are skipping the next frame, extend the encoded
+ * frame's duration in the next run of the loop.
+ * 3. Clear aSegment.
+ */
+nsresult VP8TrackEncoder::Encode(VideoSegment* aSegment) {
+ MOZ_ASSERT(mInitialized);
+ MOZ_ASSERT(!IsEncodingComplete());
+
+ AUTO_PROFILER_LABEL("VP8TrackEncoder::Encode", OTHER);
+
+ EncodeOperation nextEncodeOperation = ENCODE_NORMAL_FRAME;
+
+ RefPtr<EncodedFrame> encodedFrame;
+ for (VideoSegment::ChunkIterator iter(*aSegment); !iter.IsEnded();
+ iter.Next()) {
+ VideoChunk& chunk = *iter;
+
+ VP8LOG(LogLevel::Verbose,
+ "nextEncodeOperation is %d for frame of duration %" PRId64,
+ nextEncodeOperation, chunk.GetDuration());
+
+ TimeStamp timebase = TimeStamp::Now();
+
+ // Encode frame.
+ if (nextEncodeOperation != SKIP_FRAME) {
+ MOZ_ASSERT(!encodedFrame);
+ nsresult rv = PrepareRawFrame(chunk);
+ NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE);
+
+ // Encode the data with VP8 encoder
+ int flags = 0;
+ if (nextEncodeOperation == ENCODE_I_FRAME) {
+ VP8LOG(LogLevel::Warning,
+ "MediaRecorder lagging behind. Encoding keyframe.");
+ flags |= VPX_EFLAG_FORCE_KF;
+ }
+
+ // Sum duration of non-key frames and force keyframe if exceeded the
+ // given keyframe interval
+ if (mKeyFrameInterval > TimeDuration::FromSeconds(0)) {
+ if (media::TimeUnit(mDurationSinceLastKeyframe, mTrackRate)
+ .ToTimeDuration() >= mKeyFrameInterval) {
+ VP8LOG(LogLevel::Warning,
+ "Reached mKeyFrameInterval without seeing a keyframe. Forcing "
+ "one. time: %.2f, interval: %.2f",
+ media::TimeUnit(mDurationSinceLastKeyframe, mTrackRate)
+ .ToSeconds(),
+ mKeyFrameInterval.ToSeconds());
+ mDurationSinceLastKeyframe = 0;
+ flags |= VPX_EFLAG_FORCE_KF;
+ }
+ mDurationSinceLastKeyframe += chunk.GetDuration();
+ }
+
+ if (vpx_codec_encode(&mVPXContext, &mVPXImageWrapper, mEncodedTimestamp,
+ (unsigned long)chunk.GetDuration(), flags,
+ VPX_DL_REALTIME)) {
+ VP8LOG(LogLevel::Error, "vpx_codec_encode failed to encode the frame.");
+ return NS_ERROR_FAILURE;
+ }
+
+ // Move forward the mEncodedTimestamp.
+ mEncodedTimestamp += chunk.GetDuration();
+
+ // Extract the encoded data from the underlying encoder and push it to
+ // mEncodedDataQueue.
+ auto result = ExtractEncodedData();
+ if (result.isErr()) {
+ VP8LOG(LogLevel::Error, "ExtractEncodedData failed.");
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_ASSERT(result.inspect(),
+ "We expected a frame here. EOS is handled explicitly later");
+ encodedFrame = result.unwrap();
+ } else {
+ // SKIP_FRAME
+
+ MOZ_DIAGNOSTIC_ASSERT(encodedFrame);
+
+ if (mKeyFrameInterval > TimeDuration::FromSeconds(0)) {
+ mDurationSinceLastKeyframe += chunk.GetDuration();
+ }
+
+ // Move forward the mEncodedTimestamp.
+ mEncodedTimestamp += chunk.GetDuration();
+
+ // Extend the duration of the last encoded frame in mEncodedDataQueue
+ // because this frame will be skipped.
+ VP8LOG(LogLevel::Warning,
+ "MediaRecorder lagging behind. Skipping a frame.");
+
+ mExtractedDuration += chunk.mDuration;
+ if (!mExtractedDuration.isValid()) {
+ NS_ERROR("skipped duration overflow");
+ return NS_ERROR_DOM_MEDIA_OVERFLOW_ERR;
+ }
+
+ media::TimeUnit totalDuration =
+ media::TimeUnit(mExtractedDuration.value(), mTrackRate);
+ media::TimeUnit skippedDuration = totalDuration - mExtractedDurationUs;
+ mExtractedDurationUs = totalDuration;
+ if (!skippedDuration.IsValid()) {
+ NS_ERROR("skipped duration overflow");
+ return NS_ERROR_DOM_MEDIA_OVERFLOW_ERR;
+ }
+
+ encodedFrame = MakeRefPtr<EncodedFrame>(
+ encodedFrame->mTime,
+ encodedFrame->mDuration + skippedDuration.ToMicroseconds(),
+ encodedFrame->mDurationBase, encodedFrame->mFrameType,
+ encodedFrame->mFrameData);
+ }
+
+ mMeanFrameEncodeDuration.insert(TimeStamp::Now() - timebase);
+ mMeanFrameDuration.insert(
+ media::TimeUnit(chunk.GetDuration(), mTrackRate).ToTimeDuration());
+ nextEncodeOperation = GetNextEncodeOperation(
+ mMeanFrameEncodeDuration.mean(), mMeanFrameDuration.mean());
+
+ if (nextEncodeOperation != SKIP_FRAME) {
+ // Note that the next operation might be SKIP_FRAME even if there is no
+ // next frame.
+ mEncodedDataQueue.Push(encodedFrame.forget());
+ }
+ }
+
+ if (encodedFrame) {
+ // Push now if we ended on a SKIP_FRAME before.
+ mEncodedDataQueue.Push(encodedFrame.forget());
+ }
+
+ // Remove the chunks we have processed.
+ aSegment->Clear();
+
+ if (mEndOfStream) {
+ // EOS: Extract the remaining frames from the underlying encoder.
+ VP8LOG(LogLevel::Debug, "mEndOfStream is true");
+ // No more frames will be encoded. Clearing temporary frames saves some
+ // memory.
+ if (mI420Frame) {
+ mI420Frame = nullptr;
+ mI420FrameSize = 0;
+ }
+ // mMuteFrame must be released before gfx shutdown. We do it now since it
+ // may be too late when this VP8TrackEncoder gets destroyed.
+ mMuteFrame = nullptr;
+ // Bug 1243611, keep calling vpx_codec_encode and vpx_codec_get_cx_data
+ // until vpx_codec_get_cx_data return null.
+ while (true) {
+ if (vpx_codec_encode(&mVPXContext, nullptr, mEncodedTimestamp, 0, 0,
+ VPX_DL_REALTIME)) {
+ return NS_ERROR_FAILURE;
+ }
+ auto result = ExtractEncodedData();
+ if (result.isErr()) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!result.inspect()) {
+ // Null means end-of-stream.
+ break;
+ }
+ mEncodedDataQueue.Push(result.unwrap().forget());
+ }
+ mEncodedDataQueue.Finish();
+ }
+
+ return NS_OK;
+}
+
+nsresult VP8TrackEncoder::PrepareRawFrame(VideoChunk& aChunk) {
+ gfx::IntSize intrinsicSize = aChunk.mFrame.GetIntrinsicSize();
+ RefPtr<Image> img;
+ if (aChunk.mFrame.GetForceBlack() || aChunk.IsNull()) {
+ if (!mMuteFrame || mMuteFrame->GetSize() != intrinsicSize) {
+ mMuteFrame = mozilla::VideoFrame::CreateBlackImage(intrinsicSize);
+ }
+ if (!mMuteFrame) {
+ VP8LOG(LogLevel::Warning, "Failed to allocate black image of size %dx%d",
+ intrinsicSize.width, intrinsicSize.height);
+ return NS_OK;
+ }
+ img = mMuteFrame;
+ } else {
+ img = aChunk.mFrame.GetImage();
+ }
+
+ gfx::IntSize imgSize = img->GetSize();
+ if (imgSize != IntSize(mFrameWidth, mFrameHeight)) {
+ nsresult rv =
+ Reconfigure(imgSize.width, imgSize.height, *mMaxKeyFrameDistance);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ MOZ_ASSERT(mFrameWidth == imgSize.width);
+ MOZ_ASSERT(mFrameHeight == imgSize.height);
+
+ nsresult rv = ConvertToI420(img, mVPXImageWrapper.planes[VPX_PLANE_Y],
+ mVPXImageWrapper.stride[VPX_PLANE_Y],
+ mVPXImageWrapper.planes[VPX_PLANE_U],
+ mVPXImageWrapper.stride[VPX_PLANE_U],
+ mVPXImageWrapper.planes[VPX_PLANE_V],
+ mVPXImageWrapper.stride[VPX_PLANE_V]);
+ if (NS_FAILED(rv)) {
+ VP8LOG(LogLevel::Error, "Converting to I420 failed");
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+// These two define value used in GetNextEncodeOperation to determine the
+// EncodeOperation for next target frame.
+#define I_FRAME_RATIO (0.85) // Effectively disabled, because perceived quality
+#define SKIP_FRAME_RATIO (0.85)
+
+/**
+ * Compares the elapsed time from the beginning of GetEncodedTrack and
+ * the processed frame duration in mSourceSegment
+ * in order to set the nextEncodeOperation for next target frame.
+ */
+VP8TrackEncoder::EncodeOperation VP8TrackEncoder::GetNextEncodeOperation(
+ TimeDuration aTimeElapsed, TimeDuration aProcessedDuration) {
+ if (mFrameDroppingMode == FrameDroppingMode::DISALLOW) {
+ return ENCODE_NORMAL_FRAME;
+ }
+
+ if (aTimeElapsed.ToSeconds() >
+ aProcessedDuration.ToSeconds() * SKIP_FRAME_RATIO) {
+ // The encoder is too slow.
+ // We should skip next frame to consume the mSourceSegment.
+ return SKIP_FRAME;
+ }
+
+ if (aTimeElapsed.ToSeconds() >
+ aProcessedDuration.ToSeconds() * I_FRAME_RATIO) {
+ // The encoder is a little slow.
+ // We force the encoder to encode an I-frame to accelerate.
+ return ENCODE_I_FRAME;
+ }
+
+ return ENCODE_NORMAL_FRAME;
+}
+
+} // namespace mozilla
+
+#undef VP8LOG
diff --git a/dom/media/encoder/VP8TrackEncoder.h b/dom/media/encoder/VP8TrackEncoder.h
new file mode 100644
index 0000000000..c0e0d3a929
--- /dev/null
+++ b/dom/media/encoder/VP8TrackEncoder.h
@@ -0,0 +1,167 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef VP8TrackEncoder_h_
+#define VP8TrackEncoder_h_
+
+#include "TrackEncoder.h"
+
+#include "mozilla/RollingMean.h"
+#include "TimeUnits.h"
+#include "vpx/vpx_codec.h"
+
+namespace mozilla {
+
+typedef struct vpx_codec_ctx vpx_codec_ctx_t;
+typedef struct vpx_codec_enc_cfg vpx_codec_enc_cfg_t;
+typedef struct vpx_image vpx_image_t;
+
+class VP8Metadata;
+
+/**
+ * VP8TrackEncoder implements VideoTrackEncoder by using the libvpx library.
+ * We implement a realtime and variable frame rate encoder. In order to achieve
+ * that, there is a frame-drop encoding policy implemented in Encode().
+ */
+class VP8TrackEncoder : public VideoTrackEncoder {
+ enum EncodeOperation {
+ ENCODE_NORMAL_FRAME, // VP8 track encoder works normally.
+ ENCODE_I_FRAME, // The next frame will be encoded as I-Frame.
+ SKIP_FRAME, // Skip the next frame.
+ };
+
+ public:
+ VP8TrackEncoder(RefPtr<DriftCompensator> aDriftCompensator,
+ TrackRate aTrackRate,
+ MediaQueue<EncodedFrame>& aEncodedDataQueue,
+ FrameDroppingMode aFrameDroppingMode,
+ Maybe<float> aKeyFrameIntervalFactor = Nothing());
+ virtual ~VP8TrackEncoder();
+
+ already_AddRefed<TrackMetadataBase> GetMetadata() final;
+
+ protected:
+ nsresult Init(int32_t aWidth, int32_t aHeight, int32_t aDisplayWidth,
+ int32_t aDisplayHeight, float aEstimatedFrameRate) final;
+
+ private:
+ // Initiates the underlying vpx encoder.
+ nsresult InitInternal(int32_t aWidth, int32_t aHeight,
+ int32_t aMaxKeyFrameDistance);
+
+ // Get the EncodeOperation for next target frame.
+ EncodeOperation GetNextEncodeOperation(TimeDuration aTimeElapsed,
+ TimeDuration aProcessedDuration);
+
+ // Extracts the encoded data from the underlying encoder and returns it.
+ // Return value: An EncodedFrame if a frame was extracted.
+ // nullptr if we reached end-of-stream or nothing was available
+ // from the underlying encoder.
+ // An error nsresult otherwise.
+ Result<RefPtr<EncodedFrame>, nsresult> ExtractEncodedData();
+
+ // Takes the data in aSegment, encodes it, extracts it, and pushes it to
+ // mEncodedDataQueue.
+ nsresult Encode(VideoSegment* aSegment) final;
+
+ // Prepare the input data to the mVPXImageWrapper for encoding.
+ nsresult PrepareRawFrame(VideoChunk& aChunk);
+
+ // Re-configures an existing encoder with a new frame size.
+ nsresult Reconfigure(int32_t aWidth, int32_t aHeight,
+ int32_t aMaxKeyFrameDistance);
+
+ // Destroys the context and image wrapper. Does not de-allocate the structs.
+ void Destroy();
+
+ // Helper that calculates the desired max keyframe distance (vp8 config's
+ // max_kf_dist) based on configured key frame interval and recent framerate.
+ // Returns Nothing if not enough input data is available.
+ Maybe<int32_t> CalculateMaxKeyFrameDistance(
+ Maybe<float> aEstimatedFrameRate = Nothing()) const;
+
+ void SetMaxKeyFrameDistance(int32_t aMaxKeyFrameDistance);
+
+ // VP8 Metadata, set on successfuly Init and never modified again.
+ RefPtr<VP8Metadata> mMetadata;
+
+ // The width the encoder is currently configured with. The input frames to the
+ // underlying encoder must match this width, i.e., the underlying encoder will
+ // not do any resampling.
+ int mFrameWidth = 0;
+
+ // The height the encoder is currently configured with. The input frames to
+ // the underlying encoder must match this height, i.e., the underlying encoder
+ // will not do any resampling.
+ int mFrameHeight = 0;
+
+ // Encoded timestamp.
+ TrackTime mEncodedTimestamp = 0;
+
+ // Total duration in mTrackRate extracted from the underlying encoder.
+ CheckedInt64 mExtractedDuration;
+
+ // Total duration extracted from the underlying encoder.
+ media::TimeUnit mExtractedDurationUs;
+
+ // Muted frame, we only create it once.
+ RefPtr<layers::Image> mMuteFrame;
+
+ // I420 frame, for converting to I420.
+ UniquePtr<uint8_t[]> mI420Frame;
+ size_t mI420FrameSize = 0;
+
+ /**
+ * A duration of non-key frames in mTrackRate.
+ */
+ TrackTime mDurationSinceLastKeyframe = 0;
+
+ /**
+ * The max interval at which a keyframe gets forced (causing video quality
+ * degradation). The encoder is configured to encode keyframes more often than
+ * this, though it can vary based on frame rate.
+ */
+ const TimeDuration mKeyFrameInterval;
+
+ /**
+ * A factor used to multiply the estimated key-frame-interval based on
+ * mKeyFrameInterval (ms) with when configuring kf_max_dist in the encoder.
+ * The goal is to set it a bit below 1.0 to avoid falling back to forcing
+ * keyframes.
+ * NB that for purposes of testing the mKeyFrameInterval fallback this may be
+ * set to values higher than 1.0.
+ */
+ float mKeyFrameIntervalFactor;
+
+ /**
+ * Time when we last updated the key-frame-distance.
+ */
+ media::TimeUnit mLastKeyFrameDistanceUpdate;
+
+ /**
+ * The frame duration value last used to configure kf_max_dist.
+ */
+ Maybe<int32_t> mMaxKeyFrameDistance;
+
+ /**
+ * The mean duration of recent frames.
+ */
+ RollingMean<TimeDuration, TimeDuration> mMeanFrameDuration{30};
+
+ /**
+ * The mean wall-clock time it took to encode recent frames.
+ */
+ RollingMean<TimeDuration, TimeDuration> mMeanFrameEncodeDuration{30};
+
+ // VP8 relative members.
+ // Codec context structure.
+ vpx_codec_ctx_t mVPXContext;
+ // Image Descriptor.
+ vpx_image_t mVPXImageWrapper;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/encoder/moz.build b/dom/media/encoder/moz.build
new file mode 100644
index 0000000000..f995ecdc1c
--- /dev/null
+++ b/dom/media/encoder/moz.build
@@ -0,0 +1,42 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("*"):
+ BUG_COMPONENT = ("Core", "Audio/Video: Recording")
+
+EXPORTS += [
+ "ContainerWriter.h",
+ "EncodedFrame.h",
+ "MediaEncoder.h",
+ "OpusTrackEncoder.h",
+ "TrackEncoder.h",
+ "TrackMetadataBase.h",
+ "VP8TrackEncoder.h",
+]
+
+UNIFIED_SOURCES += [
+ "MediaEncoder.cpp",
+ "Muxer.cpp",
+ "OpusTrackEncoder.cpp",
+ "TrackEncoder.cpp",
+ "VP8TrackEncoder.cpp",
+]
+
+FINAL_LIBRARY = "xul"
+
+LOCAL_INCLUDES += [
+ "/dom/media",
+ "/ipc/chromium/src",
+ "/media/libyuv/libyuv/include",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+# Suppress some GCC warnings being treated as errors:
+# - about attributes on forward declarations for types that are already
+# defined, which complains about an important MOZ_EXPORT for android::AString
+if CONFIG["CC_TYPE"] in ("clang", "gcc"):
+ CXXFLAGS += ["-Wno-error=attributes"]
diff --git a/dom/media/fake-cdm/cdm-fake.cpp b/dom/media/fake-cdm/cdm-fake.cpp
new file mode 100644
index 0000000000..40f62c30d4
--- /dev/null
+++ b/dom/media/fake-cdm/cdm-fake.cpp
@@ -0,0 +1,63 @@
+/*!
+ * \copy
+ * Copyright (c) 2009-2014, Cisco Systems
+ * Copyright (c) 2014, Mozilla
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ *
+ *************************************************************************************
+ */
+
+#include "cdm-test-decryptor.h"
+#include "content_decryption_module.h"
+#include "content_decryption_module_ext.h"
+
+extern "C" {
+
+CDM_API
+void INITIALIZE_CDM_MODULE() {}
+
+CDM_API
+void* CreateCdmInstance(int cdm_interface_version, const char* key_system,
+ uint32_t key_system_size,
+ GetCdmHostFunc get_cdm_host_func, void* user_data) {
+ if (cdm_interface_version != cdm::ContentDecryptionModule_10::kVersion) {
+ // Only support CDM version 10 currently.
+ return nullptr;
+ }
+ cdm::Host_10* host = static_cast<cdm::Host_10*>(
+ get_cdm_host_func(cdm_interface_version, user_data));
+ return new FakeDecryptor(host);
+}
+
+CDM_API
+bool VerifyCdmHost_0(const cdm::HostFile* aHostFiles, uint32_t aNumFiles) {
+ return true;
+}
+
+} // extern "C"
diff --git a/dom/media/fake-cdm/cdm-test-decryptor.cpp b/dom/media/fake-cdm/cdm-test-decryptor.cpp
new file mode 100644
index 0000000000..3029fccdc4
--- /dev/null
+++ b/dom/media/fake-cdm/cdm-test-decryptor.cpp
@@ -0,0 +1,433 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "cdm-test-decryptor.h"
+#include "cdm-test-storage.h"
+#include "cdm-test-output-protection.h"
+
+#include <mutex>
+#include <string>
+#include <vector>
+#include <istream>
+#include <iterator>
+#include <set>
+#include <sstream>
+
+#include "mozilla/Assertions.h"
+
+FakeDecryptor* FakeDecryptor::sInstance = nullptr;
+
+class TestManager {
+ public:
+ TestManager() = default;
+
+ // Register a test with the test manager.
+ void BeginTest(const std::string& aTestID) {
+ std::lock_guard<std::mutex> lock(mMutex);
+ auto found = mTestIDs.find(aTestID);
+ if (found == mTestIDs.end()) {
+ mTestIDs.insert(aTestID);
+ } else {
+ Error("FAIL BeginTest test already existed: " + aTestID);
+ }
+ }
+
+ // Notify the test manager that the test is finished. If all tests are done,
+ // test manager will send "test-storage complete" to notify the parent that
+ // all tests are finished and also delete itself.
+ void EndTest(const std::string& aTestID) {
+ bool isEmpty = false;
+ {
+ std::lock_guard<std::mutex> lock(mMutex);
+ auto found = mTestIDs.find(aTestID);
+ if (found != mTestIDs.end()) {
+ mTestIDs.erase(aTestID);
+ isEmpty = mTestIDs.empty();
+ } else {
+ Error("FAIL EndTest test not existed: " + aTestID);
+ return;
+ }
+ }
+ if (isEmpty) {
+ Finish();
+ delete this;
+ }
+ }
+
+ private:
+ ~TestManager() = default;
+
+ static void Error(const std::string& msg) { FakeDecryptor::Message(msg); }
+
+ static void Finish() { FakeDecryptor::Message("test-storage complete"); }
+
+ std::mutex mMutex;
+ std::set<std::string> mTestIDs;
+};
+
+FakeDecryptor::FakeDecryptor(cdm::Host_10* aHost) : mHost(aHost) {
+ MOZ_ASSERT(!sInstance);
+ sInstance = this;
+}
+
+void FakeDecryptor::Message(const std::string& aMessage) {
+ MOZ_ASSERT(sInstance);
+ const static std::string sid("fake-session-id");
+ sInstance->mHost->OnSessionMessage(sid.c_str(), sid.size(),
+ cdm::MessageType::kLicenseRequest,
+ aMessage.c_str(), aMessage.size());
+}
+
+std::vector<std::string> Tokenize(const std::string& aString) {
+ std::stringstream strstr(aString);
+ std::istream_iterator<std::string> it(strstr), end;
+ return std::vector<std::string>(it, end);
+}
+
+static const std::string TruncateRecordId = "truncate-record-id";
+static const std::string TruncateRecordData = "I will soon be truncated";
+
+template <class Continuation>
+class WriteRecordSuccessTask {
+ public:
+ WriteRecordSuccessTask(std::string aId, Continuation aThen)
+ : mId(aId), mThen(std::move(aThen)) {}
+
+ void operator()() { ReadRecord(FakeDecryptor::sInstance->mHost, mId, mThen); }
+
+ std::string mId;
+ Continuation mThen;
+};
+
+class WriteRecordFailureTask {
+ public:
+ explicit WriteRecordFailureTask(const std::string& aMessage,
+ TestManager* aTestManager = nullptr,
+ const std::string& aTestID = "")
+ : mMessage(aMessage), mTestmanager(aTestManager), mTestID(aTestID) {}
+
+ void operator()() {
+ FakeDecryptor::Message(mMessage);
+ if (mTestmanager) {
+ mTestmanager->EndTest(mTestID);
+ }
+ }
+
+ private:
+ std::string mMessage;
+ TestManager* const mTestmanager;
+ const std::string mTestID;
+};
+
+class TestEmptyContinuation : public ReadContinuation {
+ public:
+ TestEmptyContinuation(TestManager* aTestManager, const std::string& aTestID)
+ : mTestmanager(aTestManager), mTestID(aTestID) {}
+
+ virtual void operator()(bool aSuccess, const uint8_t* aData,
+ uint32_t aDataSize) override {
+ if (aDataSize) {
+ FakeDecryptor::Message(
+ "FAIL TestEmptyContinuation record was not truncated");
+ }
+ mTestmanager->EndTest(mTestID);
+ }
+
+ private:
+ TestManager* const mTestmanager;
+ const std::string mTestID;
+};
+
+class TruncateContinuation : public ReadContinuation {
+ public:
+ TruncateContinuation(const std::string& aID, TestManager* aTestManager,
+ const std::string& aTestID)
+ : mID(aID), mTestmanager(aTestManager), mTestID(aTestID) {}
+
+ virtual void operator()(bool aSuccess, const uint8_t* aData,
+ uint32_t aDataSize) override {
+ if (std::string(reinterpret_cast<const char*>(aData), aDataSize) !=
+ TruncateRecordData) {
+ FakeDecryptor::Message(
+ "FAIL TruncateContinuation read data doesn't match written data");
+ }
+ auto cont = TestEmptyContinuation(mTestmanager, mTestID);
+ auto msg = "FAIL in TruncateContinuation write.";
+ WriteRecord(FakeDecryptor::sInstance->mHost, mID, nullptr, 0,
+ WriteRecordSuccessTask<TestEmptyContinuation>(mID, cont),
+ WriteRecordFailureTask(msg, mTestmanager, mTestID));
+ }
+
+ private:
+ const std::string mID;
+ TestManager* const mTestmanager;
+ const std::string mTestID;
+};
+
+class VerifyAndFinishContinuation : public ReadContinuation {
+ public:
+ explicit VerifyAndFinishContinuation(std::string aValue,
+ TestManager* aTestManager,
+ const std::string& aTestID)
+ : mValue(aValue), mTestmanager(aTestManager), mTestID(aTestID) {}
+
+ virtual void operator()(bool aSuccess, const uint8_t* aData,
+ uint32_t aDataSize) override {
+ if (std::string(reinterpret_cast<const char*>(aData), aDataSize) !=
+ mValue) {
+ FakeDecryptor::Message(
+ "FAIL VerifyAndFinishContinuation read data doesn't match expected "
+ "data");
+ }
+ mTestmanager->EndTest(mTestID);
+ }
+
+ private:
+ std::string mValue;
+ TestManager* const mTestmanager;
+ const std::string mTestID;
+};
+
+class VerifyAndOverwriteContinuation : public ReadContinuation {
+ public:
+ VerifyAndOverwriteContinuation(std::string aId, std::string aValue,
+ std::string aOverwrite,
+ TestManager* aTestManager,
+ const std::string& aTestID)
+ : mId(aId),
+ mValue(aValue),
+ mOverwrite(aOverwrite),
+ mTestmanager(aTestManager),
+ mTestID(aTestID) {}
+
+ virtual void operator()(bool aSuccess, const uint8_t* aData,
+ uint32_t aDataSize) override {
+ if (std::string(reinterpret_cast<const char*>(aData), aDataSize) !=
+ mValue) {
+ FakeDecryptor::Message(
+ "FAIL VerifyAndOverwriteContinuation read data doesn't match "
+ "expected data");
+ }
+ auto cont = VerifyAndFinishContinuation(mOverwrite, mTestmanager, mTestID);
+ auto msg = "FAIL in VerifyAndOverwriteContinuation write.";
+ WriteRecord(FakeDecryptor::sInstance->mHost, mId, mOverwrite,
+ WriteRecordSuccessTask<VerifyAndFinishContinuation>(mId, cont),
+ WriteRecordFailureTask(msg, mTestmanager, mTestID));
+ }
+
+ private:
+ std::string mId;
+ std::string mValue;
+ std::string mOverwrite;
+ TestManager* const mTestmanager;
+ const std::string mTestID;
+};
+
+static const std::string OpenAgainRecordId = "open-again-record-id";
+
+class OpenedSecondTimeContinuation : public OpenContinuation {
+ public:
+ explicit OpenedSecondTimeContinuation(TestManager* aTestManager,
+ const std::string& aTestID)
+ : mTestmanager(aTestManager), mTestID(aTestID) {}
+
+ void operator()(bool aSuccess) override {
+ if (!aSuccess) {
+ FakeDecryptor::Message(
+ "FAIL OpenSecondTimeContinuation should not be able to re-open "
+ "record.");
+ }
+ // Succeeded, open should have failed.
+ mTestmanager->EndTest(mTestID);
+ }
+
+ private:
+ TestManager* const mTestmanager;
+ const std::string mTestID;
+};
+
+class OpenedFirstTimeContinuation : public OpenContinuation {
+ public:
+ OpenedFirstTimeContinuation(const std::string& aID, TestManager* aTestManager,
+ const std::string& aTestID)
+ : mID(aID), mTestmanager(aTestManager), mTestID(aTestID) {}
+
+ void operator()(bool aSuccess) override {
+ if (!aSuccess) {
+ FakeDecryptor::Message(
+ "FAIL OpenAgainContinuation to open record initially.");
+ mTestmanager->EndTest(mTestID);
+ return;
+ }
+
+ auto cont = OpenedSecondTimeContinuation(mTestmanager, mTestID);
+ OpenRecord(FakeDecryptor::sInstance->mHost, mID, cont);
+ }
+
+ private:
+ const std::string mID;
+ TestManager* const mTestmanager;
+ const std::string mTestID;
+};
+
+static void DoTestStorage(const std::string& aPrefix,
+ TestManager* aTestManager) {
+ MOZ_ASSERT(FakeDecryptor::sInstance->mHost,
+ "FakeDecryptor::sInstance->mHost should not be null");
+ // Basic I/O tests. We run three cases concurrently. The tests, like
+ // CDMStorage run asynchronously. When they've all passed, we send
+ // a message back to the parent process, or a failure message if not.
+
+ // Test 1: Basic I/O test, and test that writing 0 bytes in a record
+ // deletes record.
+ //
+ // Write data to truncate record, then
+ // read data, verify that we read what we wrote, then
+ // write 0 bytes to truncate record, then
+ // read data, verify that 0 bytes was read
+ const std::string id1 = aPrefix + TruncateRecordId;
+ const std::string testID1 = aPrefix + "write-test-1";
+ aTestManager->BeginTest(testID1);
+ auto cont1 = TruncateContinuation(id1, aTestManager, testID1);
+ auto msg1 = "FAIL in TestStorage writing TruncateRecord.";
+ WriteRecord(FakeDecryptor::sInstance->mHost, id1, TruncateRecordData,
+ WriteRecordSuccessTask<TruncateContinuation>(id1, cont1),
+ WriteRecordFailureTask(msg1, aTestManager, testID1));
+
+ // Test 2: Test that overwriting a record with a shorter record truncates
+ // the record to the shorter record.
+ //
+ // Write record, then
+ // read and verify record, then
+ // write a shorter record to same record.
+ // read and verify
+ std::string id2 = aPrefix + "record1";
+ std::string record1 = "This is the first write to a record.";
+ std::string overwrite = "A shorter record";
+ const std::string testID2 = aPrefix + "write-test-2";
+ aTestManager->BeginTest(testID2);
+ auto task2 = VerifyAndOverwriteContinuation(id2, record1, overwrite,
+ aTestManager, testID2);
+ auto msg2 = "FAIL in TestStorage writing record1.";
+ WriteRecord(
+ FakeDecryptor::sInstance->mHost, id2, record1,
+ WriteRecordSuccessTask<VerifyAndOverwriteContinuation>(id2, task2),
+ WriteRecordFailureTask(msg2, aTestManager, testID2));
+
+ // Test 3: Test that opening a record while it's already open fails.
+ //
+ // Open record1, then
+ // open record1, should fail.
+ // close record1
+ const std::string id3 = aPrefix + OpenAgainRecordId;
+ const std::string testID3 = aPrefix + "open-test-1";
+ aTestManager->BeginTest(testID3);
+ auto task3 = OpenedFirstTimeContinuation(id3, aTestManager, testID3);
+ OpenRecord(FakeDecryptor::sInstance->mHost, id3, task3);
+}
+
+void FakeDecryptor::TestStorage() {
+ auto* testManager = new TestManager();
+ // Main thread tests.
+ DoTestStorage("mt1-", testManager);
+ DoTestStorage("mt2-", testManager);
+
+ // Note: Once all tests finish, TestManager will dispatch "test-pass" message,
+ // which ends the test for the parent.
+}
+
+class ReportWritten {
+ public:
+ ReportWritten(const std::string& aRecordId, const std::string& aValue)
+ : mRecordId(aRecordId), mValue(aValue) {}
+ void operator()() {
+ FakeDecryptor::Message("stored " + mRecordId + " " + mValue);
+ }
+
+ const std::string mRecordId;
+ const std::string mValue;
+};
+
+class ReportReadStatusContinuation : public ReadContinuation {
+ public:
+ explicit ReportReadStatusContinuation(const std::string& aRecordId)
+ : mRecordId(aRecordId) {}
+ void operator()(bool aSuccess, const uint8_t* aData,
+ uint32_t aDataSize) override {
+ if (!aSuccess) {
+ FakeDecryptor::Message("retrieve " + mRecordId + " failed");
+ } else {
+ std::stringstream ss;
+ ss << aDataSize;
+ std::string len;
+ ss >> len;
+ FakeDecryptor::Message("retrieve " + mRecordId + " succeeded (length " +
+ len + " bytes)");
+ }
+ }
+ std::string mRecordId;
+};
+
+class ReportReadRecordContinuation : public ReadContinuation {
+ public:
+ explicit ReportReadRecordContinuation(const std::string& aRecordId)
+ : mRecordId(aRecordId) {}
+ void operator()(bool aSuccess, const uint8_t* aData,
+ uint32_t aDataSize) override {
+ if (!aSuccess) {
+ FakeDecryptor::Message("retrieved " + mRecordId + " failed");
+ } else {
+ FakeDecryptor::Message(
+ "retrieved " + mRecordId + " " +
+ std::string(reinterpret_cast<const char*>(aData), aDataSize));
+ }
+ }
+ std::string mRecordId;
+};
+
+enum ShutdownMode { ShutdownNormal, ShutdownTimeout, ShutdownStoreToken };
+
+static ShutdownMode sShutdownMode = ShutdownNormal;
+static std::string sShutdownToken;
+
+void FakeDecryptor::UpdateSession(uint32_t aPromiseId, const char* aSessionId,
+ uint32_t aSessionIdLength,
+ const uint8_t* aResponse,
+ uint32_t aResponseSize) {
+ MOZ_ASSERT(FakeDecryptor::sInstance->mHost,
+ "FakeDecryptor::sInstance->mHost should not be null");
+ std::string response((const char*)aResponse,
+ (const char*)(aResponse) + aResponseSize);
+ std::vector<std::string> tokens = Tokenize(response);
+ const std::string& task = tokens[0];
+ if (task == "test-storage") {
+ TestStorage();
+ } else if (task == "store") {
+ // send "stored record" message on complete.
+ const std::string& id = tokens[1];
+ const std::string& value = tokens[2];
+ WriteRecord(FakeDecryptor::sInstance->mHost, id, value,
+ ReportWritten(id, value),
+ WriteRecordFailureTask("FAIL in writing record."));
+ } else if (task == "retrieve") {
+ const std::string& id = tokens[1];
+ ReadRecord(FakeDecryptor::sInstance->mHost, id,
+ ReportReadStatusContinuation(id));
+ } else if (task == "shutdown-mode") {
+ const std::string& mode = tokens[1];
+ if (mode == "timeout") {
+ sShutdownMode = ShutdownTimeout;
+ } else if (mode == "token") {
+ sShutdownMode = ShutdownStoreToken;
+ sShutdownToken = tokens[2];
+ Message("shutdown-token received " + sShutdownToken);
+ }
+ } else if (task == "retrieve-shutdown-token") {
+ ReadRecord(FakeDecryptor::sInstance->mHost, "shutdown-token",
+ ReportReadRecordContinuation("shutdown-token"));
+ } else if (task == "test-op-apis") {
+ mozilla::cdmtest::TestOuputProtectionAPIs();
+ }
+}
diff --git a/dom/media/fake-cdm/cdm-test-decryptor.h b/dom/media/fake-cdm/cdm-test-decryptor.h
new file mode 100644
index 0000000000..60f9f494ff
--- /dev/null
+++ b/dom/media/fake-cdm/cdm-test-decryptor.h
@@ -0,0 +1,106 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef FAKE_DECRYPTOR_H__
+#define FAKE_DECRYPTOR_H__
+
+#include "content_decryption_module.h"
+#include <string>
+
+class FakeDecryptor : public cdm::ContentDecryptionModule_10 {
+ public:
+ explicit FakeDecryptor(cdm::Host_10* aHost);
+
+ void Initialize(bool aAllowDistinctiveIdentifier, bool aAllowPersistentState,
+ bool aUseHardwareSecureCodecs) override {
+ mHost->OnInitialized(true);
+ }
+
+ void GetStatusForPolicy(uint32_t aPromiseId,
+ const cdm::Policy& aPolicy) override {}
+
+ void SetServerCertificate(uint32_t aPromiseId,
+ const uint8_t* aServerCertificateData,
+ uint32_t aServerCertificateDataSize) override {}
+
+ void CreateSessionAndGenerateRequest(uint32_t aPromiseId,
+ cdm::SessionType aSessionType,
+ cdm::InitDataType aInitDataType,
+ const uint8_t* aInitData,
+ uint32_t aInitDataSize) override {}
+
+ void LoadSession(uint32_t aPromiseId, cdm::SessionType aSessionType,
+ const char* aSessionId, uint32_t aSessionIdSize) override {}
+
+ void UpdateSession(uint32_t aPromiseId, const char* aSessionId,
+ uint32_t aSessionIdSize, const uint8_t* aResponse,
+ uint32_t aResponseSize) override;
+
+ void CloseSession(uint32_t aPromiseId, const char* aSessionId,
+ uint32_t aSessionIdSize) override {}
+
+ void RemoveSession(uint32_t aPromiseId, const char* aSessionId,
+ uint32_t aSessionIdSize) override {}
+
+ void TimerExpired(void* aContext) override {}
+
+ cdm::Status Decrypt(const cdm::InputBuffer_2& aEncryptedBuffer,
+ cdm::DecryptedBlock* aDecryptedBuffer) override {
+ return cdm::Status::kDecodeError;
+ }
+
+ cdm::Status InitializeAudioDecoder(
+ const cdm::AudioDecoderConfig_2& aAudioDecoderConfig) override {
+ return cdm::Status::kDecodeError;
+ }
+
+ cdm::Status InitializeVideoDecoder(
+ const cdm::VideoDecoderConfig_2& aVideoDecoderConfig) override {
+ return cdm::Status::kDecodeError;
+ }
+
+ void DeinitializeDecoder(cdm::StreamType aDecoderType) override {}
+
+ void ResetDecoder(cdm::StreamType aDecoderType) override {}
+
+ cdm::Status DecryptAndDecodeFrame(const cdm::InputBuffer_2& aEncryptedBuffer,
+ cdm::VideoFrame* aVideoFrame) override {
+ return cdm::Status::kDecodeError;
+ }
+
+ cdm::Status DecryptAndDecodeSamples(
+ const cdm::InputBuffer_2& aEncryptedBuffer,
+ cdm::AudioFrames* aAudioFrame) override {
+ return cdm::Status::kDecodeError;
+ }
+
+ void OnPlatformChallengeResponse(
+ const cdm::PlatformChallengeResponse& aResponse) override {}
+
+ void OnQueryOutputProtectionStatus(cdm::QueryResult aResult,
+ uint32_t aLinkMask,
+ uint32_t aOutputProtectionMask) override {}
+
+ void OnStorageId(uint32_t aVersion, const uint8_t* aStorageId,
+ uint32_t aStorageIdSize) override {}
+
+ void Destroy() override {
+ delete this;
+ sInstance = nullptr;
+ }
+
+ static void Message(const std::string& aMessage);
+
+ cdm::Host_10* mHost;
+
+ static FakeDecryptor* sInstance;
+
+ private:
+ virtual ~FakeDecryptor() = default;
+
+ void TestStorage();
+};
+
+#endif
diff --git a/dom/media/fake-cdm/cdm-test-output-protection.h b/dom/media/fake-cdm/cdm-test-output-protection.h
new file mode 100644
index 0000000000..bde7613ac2
--- /dev/null
+++ b/dom/media/fake-cdm/cdm-test-output-protection.h
@@ -0,0 +1,126 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if defined(XP_WIN)
+# include <d3d9.h> // needed to prevent re-definition of enums
+# include <stdio.h>
+# include <string>
+# include <vector>
+# include <windows.h>
+
+# include "opmapi.h"
+#endif
+
+namespace mozilla::cdmtest {
+
+#if defined(XP_WIN)
+typedef HRESULT(STDAPICALLTYPE* OPMGetVideoOutputsFromHMONITORProc)(
+ HMONITOR, OPM_VIDEO_OUTPUT_SEMANTICS, ULONG*, IOPMVideoOutput***);
+
+static OPMGetVideoOutputsFromHMONITORProc sOPMGetVideoOutputsFromHMONITORProc =
+ nullptr;
+
+static BOOL CALLBACK EnumDisplayMonitorsCallback(HMONITOR hMonitor, HDC hdc,
+ LPRECT lprc, LPARAM pData) {
+ std::vector<std::string>* failureMsgs = (std::vector<std::string>*)pData;
+
+ MONITORINFOEXA miex;
+ ZeroMemory(&miex, sizeof(miex));
+ miex.cbSize = sizeof(miex);
+ if (!GetMonitorInfoA(hMonitor, &miex)) {
+ failureMsgs->push_back("FAIL GetMonitorInfoA call failed");
+ }
+
+ ULONG numVideoOutputs = 0;
+ IOPMVideoOutput** opmVideoOutputArray = nullptr;
+ HRESULT hr = sOPMGetVideoOutputsFromHMONITORProc(
+ hMonitor, OPM_VOS_OPM_SEMANTICS, &numVideoOutputs, &opmVideoOutputArray);
+ if (S_OK != hr) {
+ if ((HRESULT)0x8007001f != hr && (HRESULT)0x80070032 != hr &&
+ (HRESULT)0xc02625e5 != hr) {
+ char msg[100];
+ sprintf(
+ msg,
+ "FAIL OPMGetVideoOutputsFromHMONITOR call failed: HRESULT=0x%08lx",
+ hr);
+ failureMsgs->push_back(msg);
+ }
+ return true;
+ }
+
+ DISPLAY_DEVICEA dd;
+ ZeroMemory(&dd, sizeof(dd));
+ dd.cb = sizeof(dd);
+ if (!EnumDisplayDevicesA(miex.szDevice, 0, &dd, 1)) {
+ failureMsgs->push_back("FAIL EnumDisplayDevicesA call failed");
+ }
+
+ for (ULONG i = 0; i < numVideoOutputs; ++i) {
+ OPM_RANDOM_NUMBER opmRandomNumber;
+ BYTE* certificate = nullptr;
+ ULONG certificateLength = 0;
+ hr = opmVideoOutputArray[i]->StartInitialization(
+ &opmRandomNumber, &certificate, &certificateLength);
+ if (S_OK != hr) {
+ char msg[100];
+ sprintf(msg, "FAIL StartInitialization call failed: HRESULT=0x%08lx", hr);
+ failureMsgs->push_back(msg);
+ }
+
+ if (certificate) {
+ CoTaskMemFree(certificate);
+ }
+
+ opmVideoOutputArray[i]->Release();
+ }
+
+ if (opmVideoOutputArray) {
+ CoTaskMemFree(opmVideoOutputArray);
+ }
+
+ return true;
+}
+#endif
+
+static void RunOutputProtectionAPITests() {
+#if defined(XP_WIN)
+ // Get hold of OPMGetVideoOutputsFromHMONITOR function.
+ HMODULE hDvax2DLL = GetModuleHandleW(L"dxva2.dll");
+ if (!hDvax2DLL) {
+ FakeDecryptor::Message("FAIL GetModuleHandleW call failed for dxva2.dll");
+ return;
+ }
+
+ sOPMGetVideoOutputsFromHMONITORProc =
+ (OPMGetVideoOutputsFromHMONITORProc)GetProcAddress(
+ hDvax2DLL, "OPMGetVideoOutputsFromHMONITOR");
+ if (!sOPMGetVideoOutputsFromHMONITORProc) {
+ FakeDecryptor::Message(
+ "FAIL GetProcAddress call failed for OPMGetVideoOutputsFromHMONITOR");
+ return;
+ }
+
+ // Test EnumDisplayMonitors.
+ // Other APIs are tested in the callback function.
+ std::vector<std::string> failureMsgs;
+ if (!EnumDisplayMonitors(NULL, NULL, EnumDisplayMonitorsCallback,
+ (LPARAM)&failureMsgs)) {
+ FakeDecryptor::Message("FAIL EnumDisplayMonitors call failed");
+ }
+
+ // Report any failures in the callback function.
+ for (size_t i = 0; i < failureMsgs.size(); i++) {
+ FakeDecryptor::Message(failureMsgs[i]);
+ }
+#endif
+}
+
+static void TestOuputProtectionAPIs() {
+ RunOutputProtectionAPITests();
+ FakeDecryptor::Message("OP tests completed");
+}
+
+} // namespace mozilla::cdmtest
diff --git a/dom/media/fake-cdm/cdm-test-storage.cpp b/dom/media/fake-cdm/cdm-test-storage.cpp
new file mode 100644
index 0000000000..d0a343576f
--- /dev/null
+++ b/dom/media/fake-cdm/cdm-test-storage.cpp
@@ -0,0 +1,195 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "cdm-test-storage.h"
+#include <vector>
+
+using namespace cdm;
+
+class WriteRecordClient : public FileIOClient {
+ public:
+ WriteRecordClient(std::function<void()>&& aOnSuccess,
+ std::function<void()>&& aOnFailure, const uint8_t* aData,
+ uint32_t aDataSize)
+ : mOnSuccess(std::move(aOnSuccess)), mOnFailure(std::move(aOnFailure)) {
+ mData.insert(mData.end(), aData, aData + aDataSize);
+ }
+
+ void OnOpenComplete(Status aStatus) override {
+ // If we hit an error, fail.
+ if (aStatus != Status::kSuccess) {
+ Done(aStatus);
+ } else if (mFileIO) { // Otherwise, write our data to the file.
+ mFileIO->Write(mData.empty() ? nullptr : &mData.front(), mData.size());
+ }
+ }
+
+ void OnReadComplete(Status aStatus, const uint8_t* aData,
+ uint32_t aDataSize) override {}
+
+ void OnWriteComplete(Status aStatus) override { Done(aStatus); }
+
+ void Do(const std::string& aName, Host_10* aHost) {
+ // Initialize the FileIO.
+ mFileIO = aHost->CreateFileIO(this);
+ mFileIO->Open(aName.c_str(), aName.size());
+ }
+
+ private:
+ void Done(cdm::FileIOClient::Status aStatus) {
+ // Note: Call Close() before running continuation, in case the
+ // continuation tries to open the same record; if we call Close()
+ // after running the continuation, the Close() call will arrive
+ // just after the Open() call succeeds, immediately closing the
+ // record we just opened.
+ if (mFileIO) {
+ // will delete mFileIO inside Close.
+ mFileIO->Close();
+ }
+
+ if (IO_SUCCEEDED(aStatus)) {
+ mOnSuccess();
+ } else {
+ mOnFailure();
+ }
+
+ delete this;
+ }
+
+ FileIO* mFileIO = nullptr;
+ std::function<void()> mOnSuccess;
+ std::function<void()> mOnFailure;
+ std::vector<uint8_t> mData;
+};
+
+void WriteRecord(Host_10* aHost, const std::string& aRecordName,
+ const uint8_t* aData, uint32_t aNumBytes,
+ std::function<void()>&& aOnSuccess,
+ std::function<void()>&& aOnFailure) {
+ // client will be delete in WriteRecordClient::Done
+ WriteRecordClient* client = new WriteRecordClient(
+ std::move(aOnSuccess), std::move(aOnFailure), aData, aNumBytes);
+ client->Do(aRecordName, aHost);
+}
+
+void WriteRecord(Host_10* aHost, const std::string& aRecordName,
+ const std::string& aData, std::function<void()>&& aOnSuccess,
+ std::function<void()>&& aOnFailure) {
+ return WriteRecord(aHost, aRecordName, (const uint8_t*)aData.c_str(),
+ aData.size(), std::move(aOnSuccess),
+ std::move(aOnFailure));
+}
+
+class ReadRecordClient : public FileIOClient {
+ public:
+ explicit ReadRecordClient(
+ std::function<void(bool, const uint8_t*, uint32_t)>&& aOnReadComplete)
+ : mOnReadComplete(std::move(aOnReadComplete)) {}
+
+ void OnOpenComplete(Status aStatus) override {
+ auto err = aStatus;
+ if (aStatus != Status::kSuccess) {
+ Done(err, reinterpret_cast<const uint8_t*>(""), 0);
+ } else {
+ mFileIO->Read();
+ }
+ }
+
+ void OnReadComplete(Status aStatus, const uint8_t* aData,
+ uint32_t aDataSize) override {
+ Done(aStatus, aData, aDataSize);
+ }
+
+ void OnWriteComplete(Status aStatus) override {}
+
+ void Do(const std::string& aName, Host_10* aHost) {
+ mFileIO = aHost->CreateFileIO(this);
+ mFileIO->Open(aName.c_str(), aName.size());
+ }
+
+ private:
+ void Done(cdm::FileIOClient::Status aStatus, const uint8_t* aData,
+ uint32_t aDataSize) {
+ // Note: Call Close() before running continuation, in case the
+ // continuation tries to open the same record; if we call Close()
+ // after running the continuation, the Close() call will arrive
+ // just after the Open() call succeeds, immediately closing the
+ // record we just opened.
+ if (mFileIO) {
+ // will delete mFileIO inside Close.
+ mFileIO->Close();
+ }
+
+ if (IO_SUCCEEDED(aStatus)) {
+ mOnReadComplete(true, aData, aDataSize);
+ } else {
+ mOnReadComplete(false, reinterpret_cast<const uint8_t*>(""), 0);
+ }
+
+ delete this;
+ }
+
+ FileIO* mFileIO = nullptr;
+ std::function<void(bool, const uint8_t*, uint32_t)> mOnReadComplete;
+};
+
+void ReadRecord(
+ Host_10* aHost, const std::string& aRecordName,
+ std::function<void(bool, const uint8_t*, uint32_t)>&& aOnReadComplete) {
+ // client will be delete in ReadRecordClient::Done
+ ReadRecordClient* client = new ReadRecordClient(std::move(aOnReadComplete));
+ client->Do(aRecordName, aHost);
+}
+
+class OpenRecordClient : public FileIOClient {
+ public:
+ explicit OpenRecordClient(std::function<void(bool)>&& aOpenComplete)
+ : mOpenComplete(std::move(aOpenComplete)) {}
+
+ void OnOpenComplete(Status aStatus) override { Done(aStatus); }
+
+ void OnReadComplete(Status aStatus, const uint8_t* aData,
+ uint32_t aDataSize) override {}
+
+ void OnWriteComplete(Status aStatus) override {}
+
+ void Do(const std::string& aName, Host_10* aHost) {
+ // Initialize the FileIO.
+ mFileIO = aHost->CreateFileIO(this);
+ mFileIO->Open(aName.c_str(), aName.size());
+ }
+
+ private:
+ void Done(cdm::FileIOClient::Status aStatus) {
+ // Note: Call Close() before running continuation, in case the
+ // continuation tries to open the same record; if we call Close()
+ // after running the continuation, the Close() call will arrive
+ // just after the Open() call succeeds, immediately closing the
+ // record we just opened.
+ if (mFileIO) {
+ // will delete mFileIO inside Close.
+ mFileIO->Close();
+ }
+
+ if (IO_SUCCEEDED(aStatus)) {
+ mOpenComplete(true);
+ } else {
+ mOpenComplete(false);
+ }
+
+ delete this;
+ }
+
+ FileIO* mFileIO = nullptr;
+ std::function<void(bool)> mOpenComplete;
+ ;
+};
+
+void OpenRecord(Host_10* aHost, const std::string& aRecordName,
+ std::function<void(bool)>&& aOpenComplete) {
+ // client will be delete in OpenRecordClient::Done
+ OpenRecordClient* client = new OpenRecordClient(std::move(aOpenComplete));
+ client->Do(aRecordName, aHost);
+}
diff --git a/dom/media/fake-cdm/cdm-test-storage.h b/dom/media/fake-cdm/cdm-test-storage.h
new file mode 100644
index 0000000000..2c95cca6b5
--- /dev/null
+++ b/dom/media/fake-cdm/cdm-test-storage.h
@@ -0,0 +1,45 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef TEST_CDM_STORAGE_H__
+#define TEST_CDM_STORAGE_H__
+
+#include <functional>
+#include <string>
+#include <cstdint>
+#include "content_decryption_module.h"
+
+#define IO_SUCCEEDED(x) ((x) == cdm::FileIOClient::Status::kSuccess)
+#define IO_FAILED(x) ((x) != cdm::FileIOClient::Status::kSuccess)
+
+class ReadContinuation {
+ public:
+ virtual ~ReadContinuation() = default;
+ virtual void operator()(bool aSuccess, const uint8_t* aData,
+ uint32_t aDataSize) = 0;
+};
+
+void WriteRecord(cdm::Host_10* aHost, const std::string& aRecordName,
+ const std::string& aData, std::function<void()>&& aOnSuccess,
+ std::function<void()>&& aOnFailure);
+
+void WriteRecord(cdm::Host_10* aHost, const std::string& aRecordName,
+ const uint8_t* aData, uint32_t aNumBytes,
+ std::function<void()>&& aOnSuccess,
+ std::function<void()>&& aOnFailure);
+
+void ReadRecord(
+ cdm::Host_10* aHost, const std::string& aRecordName,
+ std::function<void(bool, const uint8_t*, uint32_t)>&& aOnReadComplete);
+
+class OpenContinuation {
+ public:
+ virtual ~OpenContinuation() = default;
+ virtual void operator()(bool aSuccess) = 0;
+};
+
+void OpenRecord(cdm::Host_10* aHost, const std::string& aRecordName,
+ std::function<void(bool)>&& aOpenComplete);
+#endif // TEST_CDM_STORAGE_H__
diff --git a/dom/media/fake-cdm/manifest.json b/dom/media/fake-cdm/manifest.json
new file mode 100644
index 0000000000..96823bac09
--- /dev/null
+++ b/dom/media/fake-cdm/manifest.json
@@ -0,0 +1,9 @@
+{
+ "name": "fake",
+ "description": "Fake CDM Plugin",
+ "version": "1",
+ "x-cdm-module-versions": "4",
+ "x-cdm-interface-versions": "10",
+ "x-cdm-host-versions": "10",
+ "x-cdm-codecs": ""
+}
diff --git a/dom/media/fake-cdm/moz.build b/dom/media/fake-cdm/moz.build
new file mode 100644
index 0000000000..6c7aafd9b4
--- /dev/null
+++ b/dom/media/fake-cdm/moz.build
@@ -0,0 +1,33 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+FINAL_TARGET = "dist/bin/gmp-fake/1.0"
+
+FINAL_TARGET_FILES += [
+ "manifest.json",
+]
+
+SOURCES += [
+ "cdm-fake.cpp",
+ "cdm-test-decryptor.cpp",
+ "cdm-test-storage.cpp",
+]
+
+DEFINES["CDM_IMPLEMENTATION"] = True
+
+SharedLibrary("fake")
+
+if CONFIG["OS_ARCH"] == "WINNT":
+ OS_LIBS += [
+ "ole32",
+ "user32",
+ ]
+
+USE_STATIC_LIBS = True
+NoVisibilityFlags()
+# Don't use STL wrappers; this isn't Gecko code
+DisableStlWrapping()
+NO_PGO = True
diff --git a/dom/media/flac/FlacDecoder.cpp b/dom/media/flac/FlacDecoder.cpp
new file mode 100644
index 0000000000..2f205c9aae
--- /dev/null
+++ b/dom/media/flac/FlacDecoder.cpp
@@ -0,0 +1,45 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FlacDecoder.h"
+#include "MediaContainerType.h"
+#include "mozilla/StaticPrefs_media.h"
+
+namespace mozilla {
+
+/* static */
+bool FlacDecoder::IsEnabled() {
+#ifdef MOZ_FFVPX
+ return StaticPrefs::media_flac_enabled();
+#else
+ return false;
+#endif
+}
+
+/* static */
+bool FlacDecoder::IsSupportedType(const MediaContainerType& aContainerType) {
+ return IsEnabled() &&
+ (aContainerType.Type() == MEDIAMIMETYPE("audio/flac") ||
+ aContainerType.Type() == MEDIAMIMETYPE("audio/x-flac") ||
+ aContainerType.Type() == MEDIAMIMETYPE("application/x-flac"));
+}
+
+/* static */
+nsTArray<UniquePtr<TrackInfo>> FlacDecoder::GetTracksInfo(
+ const MediaContainerType& aType) {
+ nsTArray<UniquePtr<TrackInfo>> tracks;
+ if (!IsSupportedType(aType)) {
+ return tracks;
+ }
+
+ tracks.AppendElement(
+ CreateTrackInfoWithMIMETypeAndContainerTypeExtraParameters(
+ "audio/flac"_ns, aType));
+
+ return tracks;
+}
+
+} // namespace mozilla
diff --git a/dom/media/flac/FlacDecoder.h b/dom/media/flac/FlacDecoder.h
new file mode 100644
index 0000000000..f40ae6c31a
--- /dev/null
+++ b/dom/media/flac/FlacDecoder.h
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef FLAC_DECODER_H_
+#define FLAC_DECODER_H_
+
+#include "mozilla/UniquePtr.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+
+class MediaContainerType;
+class TrackInfo;
+
+class FlacDecoder {
+ public:
+ // Returns true if the Flac backend is pref'ed on, and we're running on a
+ // platform that is likely to have decoders for the format.
+ static bool IsEnabled();
+ static bool IsSupportedType(const MediaContainerType& aContainerType);
+ static nsTArray<UniquePtr<TrackInfo>> GetTracksInfo(
+ const MediaContainerType& aType);
+};
+
+} // namespace mozilla
+
+#endif // !FLAC_DECODER_H_
diff --git a/dom/media/flac/FlacDemuxer.cpp b/dom/media/flac/FlacDemuxer.cpp
new file mode 100644
index 0000000000..fd6eea13c2
--- /dev/null
+++ b/dom/media/flac/FlacDemuxer.cpp
@@ -0,0 +1,1027 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FlacDemuxer.h"
+
+#include "mozilla/Maybe.h"
+#include "BitReader.h"
+#include "prenv.h"
+#include "FlacFrameParser.h"
+#include "VideoUtils.h"
+#include "TimeUnits.h"
+
+extern mozilla::LazyLogModule gMediaDemuxerLog;
+#define LOG(msg, ...) \
+ DDMOZ_LOG(gMediaDemuxerLog, LogLevel::Debug, msg, ##__VA_ARGS__)
+#define LOGV(msg, ...) \
+ DDMOZ_LOG(gMediaDemuxerLog, LogLevel::Verbose, msg, ##__VA_ARGS__)
+
+using namespace mozilla::media;
+
+namespace mozilla {
+namespace flac {
+
+// flac::FrameHeader - Holds the flac frame header and its parsing
+// state.
+
+class FrameHeader {
+ public:
+ const AudioInfo& Info() const { return mInfo; }
+
+ uint32_t Size() const { return mSize; }
+
+ bool IsValid() const { return mValid; }
+
+ // Return the index (in samples) from the beginning of the track.
+ int64_t Index() const { return mIndex; }
+
+ // Parse the current packet and check that it made a valid flac frame header.
+ // From https://xiph.org/flac/format.html#frame_header
+ // A valid header is one that can be decoded without error and that has a
+ // valid CRC.
+ bool Parse(const uint8_t* aPacket, size_t aBytes) {
+ BitReader br(aPacket, aBytes * 8);
+
+ // Frame sync code.
+ if ((br.ReadBits(15) & 0x7fff) != 0x7ffc) {
+ return false;
+ }
+
+ // Variable block size stream code.
+ mVariableBlockSize = br.ReadBit();
+
+ // Block size and sample rate codes.
+ int bs_code = br.ReadBits(4);
+ int sr_code = br.ReadBits(4);
+
+ // Channels and decorrelation.
+ uint32_t ch_mode = br.ReadBits(4);
+ if (ch_mode < FLAC_MAX_CHANNELS) {
+ mInfo.mChannels = ch_mode + 1;
+ } else if (ch_mode < FLAC_MAX_CHANNELS + FLAC_CHMODE_MID_SIDE) {
+ // This is a special flac channels, we can't handle those yet. Treat it
+ // as stereo.
+ mInfo.mChannels = 2;
+ } else {
+ // invalid channel mode
+ return false;
+ }
+
+ // Bits per sample.
+ int bps_code = br.ReadBits(3);
+ if (bps_code == 3 || bps_code == 7) {
+ // Invalid sample size code.
+ return false;
+ }
+ mInfo.mBitDepth = FlacSampleSizeTable[bps_code];
+
+ // Reserved bit, must be 0.
+ if (br.ReadBit()) {
+ // Broken stream, invalid padding.
+ return false;
+ }
+
+ // Sample or frame count.
+ uint64_t frame_or_sample_num = br.ReadUTF8();
+ if (frame_or_sample_num == UINT64_MAX) {
+ // Sample/frame number invalid.
+ return false;
+ }
+
+ // Blocksize
+ if (bs_code == 0) {
+ // reserved blocksize code
+ return false;
+ } else if (bs_code == 6) {
+ mBlocksize = br.ReadBits(8) + 1;
+ } else if (bs_code == 7) {
+ mBlocksize = br.ReadBits(16) + 1;
+ } else {
+ mBlocksize = FlacBlocksizeTable[bs_code];
+ }
+
+ // The sample index is either:
+ // 1- coded sample number if blocksize is variable or
+ // 2- coded frame number if blocksize is known.
+ // A frame is made of Blocksize sample.
+ mIndex = mVariableBlockSize ? frame_or_sample_num
+ : frame_or_sample_num * mBlocksize;
+ mFrameOrSampleNum = static_cast<uint64_t>(frame_or_sample_num);
+
+ // Sample rate.
+ if (sr_code < 12) {
+ mInfo.mRate = FlacSampleRateTable[sr_code];
+ } else if (sr_code == 12) {
+ mInfo.mRate = br.ReadBits(8) * 1000;
+ } else if (sr_code == 13) {
+ mInfo.mRate = br.ReadBits(16);
+ } else if (sr_code == 14) {
+ mInfo.mRate = br.ReadBits(16) * 10;
+ } else {
+ // Illegal sample rate code.
+ return false;
+ }
+
+ // Header CRC-8 check.
+ uint8_t crc = 0;
+ for (uint32_t i = 0; i < br.BitCount() / 8; i++) {
+ crc = CRC8Table[crc ^ aPacket[i]];
+ }
+ mValid =
+#ifdef FUZZING
+ true;
+#else
+ crc == br.ReadBits(8);
+#endif
+ mSize = br.BitCount() / 8;
+
+ if (mValid) {
+ // Set the mimetype to make it a valid AudioInfo.
+ mInfo.mMimeType = "audio/flac";
+ // Set the codec specific data to flac, but leave it empty since we don't
+ // have METADATA_BLOCK_STREAMINFO in the frame.
+ mInfo.mCodecSpecificConfig =
+ AudioCodecSpecificVariant{FlacCodecSpecificData{}};
+ }
+
+ return mValid;
+ }
+
+ private:
+ friend class Frame;
+ enum {
+ FLAC_CHMODE_INDEPENDENT = 0,
+ FLAC_CHMODE_LEFT_SIDE,
+ FLAC_CHMODE_RIGHT_SIDE,
+ FLAC_CHMODE_MID_SIDE,
+ };
+ AudioInfo mInfo;
+ // mFrameOrSampleNum is either:
+ // 1- coded sample number if blocksize is variable or
+ // 2- coded frame number if blocksize is fixed.
+ // A frame is made of Blocksize sample.
+ uint64_t mFrameOrSampleNum = 0;
+ // Index in samples from start;
+ int64_t mIndex = 0;
+ bool mVariableBlockSize = false;
+ uint32_t mBlocksize = 0;
+ uint32_t mSize = 0;
+ bool mValid = false;
+
+ static const uint32_t FlacSampleRateTable[16];
+ static const uint32_t FlacBlocksizeTable[16];
+ static const uint8_t FlacSampleSizeTable[8];
+ static const uint8_t CRC8Table[256];
+};
+
+const uint32_t FrameHeader::FlacSampleRateTable[16] = {
+ 0, 88200, 176400, 192000, 8000, 16000, 22050, 24000,
+ 32000, 44100, 48000, 96000, 0, 0, 0, 0};
+
+const uint32_t FrameHeader::FlacBlocksizeTable[16] = {
+ 0, 192, 576 << 0, 576 << 1, 576 << 2, 576 << 3,
+ 0, 0, 256 << 0, 256 << 1, 256 << 2, 256 << 3,
+ 256 << 4, 256 << 5, 256 << 6, 256 << 7};
+
+const uint8_t FrameHeader::FlacSampleSizeTable[8] = {0, 8, 12, 0,
+ 16, 20, 24, 0};
+
+const uint8_t FrameHeader::CRC8Table[256] = {
+ 0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15, 0x38, 0x3F, 0x36, 0x31,
+ 0x24, 0x23, 0x2A, 0x2D, 0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65,
+ 0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D, 0xE0, 0xE7, 0xEE, 0xE9,
+ 0xFC, 0xFB, 0xF2, 0xF5, 0xD8, 0xDF, 0xD6, 0xD1, 0xC4, 0xC3, 0xCA, 0xCD,
+ 0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85, 0xA8, 0xAF, 0xA6, 0xA1,
+ 0xB4, 0xB3, 0xBA, 0xBD, 0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2,
+ 0xFF, 0xF8, 0xF1, 0xF6, 0xE3, 0xE4, 0xED, 0xEA, 0xB7, 0xB0, 0xB9, 0xBE,
+ 0xAB, 0xAC, 0xA5, 0xA2, 0x8F, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A,
+ 0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32, 0x1F, 0x18, 0x11, 0x16,
+ 0x03, 0x04, 0x0D, 0x0A, 0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42,
+ 0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A, 0x89, 0x8E, 0x87, 0x80,
+ 0x95, 0x92, 0x9B, 0x9C, 0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4,
+ 0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, 0xEB, 0xEC, 0xC1, 0xC6, 0xCF, 0xC8,
+ 0xDD, 0xDA, 0xD3, 0xD4, 0x69, 0x6E, 0x67, 0x60, 0x75, 0x72, 0x7B, 0x7C,
+ 0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44, 0x19, 0x1E, 0x17, 0x10,
+ 0x05, 0x02, 0x0B, 0x0C, 0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34,
+ 0x4E, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5C, 0x5B, 0x76, 0x71, 0x78, 0x7F,
+ 0x6A, 0x6D, 0x64, 0x63, 0x3E, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B,
+ 0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13, 0xAE, 0xA9, 0xA0, 0xA7,
+ 0xB2, 0xB5, 0xBC, 0xBB, 0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83,
+ 0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB, 0xE6, 0xE1, 0xE8, 0xEF,
+ 0xFA, 0xFD, 0xF4, 0xF3};
+
+// flac::Frame - Frame meta container used to parse and hold a frame
+// header and side info.
+class Frame {
+ public:
+ // The FLAC signature is made of 14 bits set to 1; however the 15th bit is
+ // mandatorily set to 0, so we need to find either of 0xfffc or 0xfffd 2-bytes
+ // signature. We first use a bitmask to see if 0xfc or 0xfd is present. And if
+ // so we check for the whole signature.
+ int64_t FindNext(const uint8_t* aData, const uint32_t aLength) {
+ // The non-variable size of a FLAC header is 32 bits followed by variable
+ // size data and a 8 bits CRC.
+ // There's no need to read the last 4 bytes, it can never make a complete
+ // header.
+ if (aLength < 4) {
+ return -1;
+ }
+ uint32_t modOffset = aLength % 4;
+ uint32_t i, j;
+
+ for (i = 0; i < modOffset; i++) {
+ if ((BigEndian::readUint16(aData + i) & 0xfffe) == 0xfff8) {
+ if (mHeader.Parse(aData + i, aLength - i)) {
+ return i;
+ }
+ }
+ }
+
+ for (; i < aLength - 4; i += 4) {
+ uint32_t x = BigEndian::readUint32(aData + i);
+ if (((x & ~(x + 0x01010101)) & 0x80808080)) {
+ for (j = 0; j < 4; j++) {
+ if ((BigEndian::readUint16(aData + i + j) & 0xfffe) == 0xfff8) {
+ if (mHeader.Parse(aData + i + j, aLength - i - j)) {
+ return i + j;
+ }
+ }
+ }
+ }
+ }
+ return -1;
+ }
+
+ // Find the next frame start in the current resource.
+ // On exit return true, offset is set and resource points to the frame found.
+ bool FindNext(MediaResourceIndex& aResource) {
+ static const int BUFFER_SIZE = 4096;
+
+ Reset();
+
+ nsTArray<char> buffer;
+ uint64_t originalOffset = static_cast<uint64_t>(aResource.Tell());
+ uint64_t offset = originalOffset;
+ uint32_t innerOffset = 0;
+
+ do {
+ uint32_t read = 0;
+ buffer.SetLength(BUFFER_SIZE + innerOffset);
+ nsresult rv =
+ aResource.Read(buffer.Elements() + innerOffset, BUFFER_SIZE, &read);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ const size_t bufSize = read + innerOffset;
+ int64_t foundOffset =
+ FindNext(reinterpret_cast<uint8_t*>(buffer.Elements()), bufSize);
+
+ if (foundOffset >= 0) {
+ SetOffset(aResource, static_cast<uint64_t>(foundOffset) + offset);
+ return true;
+ }
+
+ if (read < BUFFER_SIZE) {
+ // Nothing more to try on as we had reached EOS during the previous
+ // read.
+ mEOS = true;
+ return false;
+ }
+
+ // Scan the next block;
+ // We rewind a bit to re-try what could have been an incomplete packet.
+ // The maximum size of a FLAC header being FLAC_MAX_FRAME_HEADER_SIZE so
+ // we need to retry just after that amount.
+ offset += bufSize - (FLAC_MAX_FRAME_HEADER_SIZE + 1);
+ buffer.RemoveElementsAt(0, bufSize - (FLAC_MAX_FRAME_HEADER_SIZE + 1));
+ innerOffset = buffer.Length();
+ } while (offset - originalOffset < FLAC_MAX_FRAME_SIZE);
+
+ return false;
+ }
+
+ uint64_t Offset() const { return mOffset; }
+
+ const AudioInfo& Info() const { return Header().Info(); }
+
+ void SetEndOffset(uint64_t aOffset) { mSize = aOffset - mOffset; }
+
+ void SetEndTime(int64_t aIndex) {
+ if (aIndex > Header().mIndex) {
+ mDuration = aIndex - Header().mIndex;
+ }
+ }
+
+ void ResetStartTimeIfNeeded(const Frame& aReferenceFrame) {
+ if (Header().mVariableBlockSize ||
+ aReferenceFrame.Header().mVariableBlockSize ||
+ aReferenceFrame.Header().mBlocksize <= Header().mBlocksize) {
+ // Not a fixed size frame, or nothing to adjust.
+ return;
+ }
+ mHeader.mIndex =
+ Header().mFrameOrSampleNum * aReferenceFrame.Header().mBlocksize;
+ }
+
+ uint32_t Size() const { return mSize; }
+
+ TimeUnit Time() const {
+ if (!IsValid()) {
+ return TimeUnit::Invalid();
+ }
+ MOZ_ASSERT(Header().Info().mRate, "Invalid Frame. Need Header");
+ return media::TimeUnit(Header().mIndex, Header().Info().mRate);
+ }
+
+ TimeUnit Duration() const {
+ if (!IsValid()) {
+ return TimeUnit();
+ }
+ MOZ_ASSERT(Header().Info().mRate, "Invalid Frame. Need Header");
+ return media::TimeUnit(mDuration, Header().Info().mRate);
+ }
+
+ // Returns the parsed frame header.
+ const FrameHeader& Header() const { return mHeader; }
+
+ bool IsValid() const { return mHeader.IsValid(); }
+
+ bool EOS() const { return mEOS; }
+
+ void SetRate(uint32_t aRate) { mHeader.mInfo.mRate = aRate; };
+
+ void SetBitDepth(uint32_t aBitDepth) { mHeader.mInfo.mBitDepth = aBitDepth; }
+
+ void SetInvalid() { mHeader.mValid = false; }
+
+ // Resets the frame header and data.
+ void Reset() { *this = Frame(); }
+
+ private:
+ void SetOffset(MediaResourceIndex& aResource, uint64_t aOffset) {
+ mOffset = aOffset;
+ aResource.Seek(SEEK_SET, mOffset);
+ }
+
+ // The offset to the start of the header.
+ uint64_t mOffset = 0;
+ uint32_t mSize = 0;
+ uint32_t mDuration = 0;
+ bool mEOS = false;
+
+ // The currently parsed frame header.
+ FrameHeader mHeader;
+};
+
+class FrameParser {
+ public:
+ // Returns the currently parsed frame. Reset via EndFrameSession.
+ const Frame& CurrentFrame() const { return mFrame; }
+
+ // Returns the first parsed frame.
+ const Frame& FirstFrame() const { return mFirstFrame; }
+
+ // Clear the last parsed frame to allow for next frame parsing
+ void EndFrameSession() {
+ mNextFrame.Reset();
+ mFrame.Reset();
+ }
+
+ // Attempt to find the next frame.
+ bool FindNextFrame(MediaResourceIndex& aResource) {
+ mFrame = mNextFrame;
+ if (GetNextFrame(aResource)) {
+ if (!mFrame.IsValid()) {
+ mFrame = mNextFrame;
+ // We need two frames to be able to start playing (or have reached EOS).
+ GetNextFrame(aResource);
+ }
+ }
+
+ if (mFrame.IsValid()) {
+ if (mNextFrame.EOS()) {
+ mFrame.SetEndOffset(static_cast<uint64_t>(aResource.Tell()));
+ // If the blocksize is fixed, the frame's starting sample number will be
+ // the frame number times the blocksize. However, the last block may
+ // have been incorrectly set as shorter than the stream blocksize.
+ // We recalculate the start time of this last sample using the first
+ // frame blocksize.
+ // TODO: should we use an overall counter of frames instead?
+ mFrame.ResetStartTimeIfNeeded(mFirstFrame);
+ } else if (mNextFrame.IsValid()) {
+ mFrame.SetEndOffset(mNextFrame.Offset());
+ mFrame.SetEndTime(mNextFrame.Header().Index());
+ }
+ }
+
+ if (!mFirstFrame.IsValid()) {
+ mFirstFrame = mFrame;
+ }
+ return mFrame.IsValid();
+ }
+
+ // Convenience methods to external FlacFrameParser ones.
+ bool IsHeaderBlock(const uint8_t* aPacket, size_t aLength) const {
+ auto res = mParser.IsHeaderBlock(aPacket, aLength);
+ return res.isOk() ? res.unwrap() : false;
+ }
+
+ uint32_t HeaderBlockLength(const uint8_t* aPacket) const {
+ return mParser.HeaderBlockLength(aPacket);
+ }
+
+ bool DecodeHeaderBlock(const uint8_t* aPacket, size_t aLength) {
+ return mParser.DecodeHeaderBlock(aPacket, aLength).isOk();
+ }
+
+ bool HasFullMetadata() const { return mParser.HasFullMetadata(); }
+
+ AudioInfo Info() const { return mParser.mInfo; }
+
+ // Return a hash table with tag metadata.
+ UniquePtr<MetadataTags> GetTags() const { return mParser.GetTags(); }
+
+ private:
+ bool GetNextFrame(MediaResourceIndex& aResource) {
+ while (mNextFrame.FindNext(aResource)) {
+ // Move our offset slightly, so that we don't find the same frame at the
+ // next FindNext call.
+ aResource.Seek(SEEK_CUR, mNextFrame.Header().Size());
+ if (mFrame.IsValid() &&
+ mNextFrame.Offset() - mFrame.Offset() < FLAC_MAX_FRAME_SIZE &&
+ !CheckCRC16AtOffset(mFrame.Offset(), mNextFrame.Offset(),
+ aResource)) {
+ // The frame doesn't match its CRC or would be too far, skip it..
+ continue;
+ }
+ CheckFrameData();
+ break;
+ }
+ return mNextFrame.IsValid();
+ }
+
+ bool CheckFrameData() {
+ if (mNextFrame.Header().Info().mRate == 0 ||
+ mNextFrame.Header().Info().mBitDepth == 0) {
+ if (!Info().IsValid()) {
+ // We can only use the STREAMINFO data if we have one.
+ mNextFrame.SetInvalid();
+ } else {
+ if (mNextFrame.Header().Info().mRate == 0) {
+ mNextFrame.SetRate(Info().mRate);
+ }
+ if (mNextFrame.Header().Info().mBitDepth == 0) {
+ mNextFrame.SetBitDepth(Info().mBitDepth);
+ }
+ }
+ }
+ return mNextFrame.IsValid();
+ }
+
+ bool CheckCRC16AtOffset(int64_t aStart, int64_t aEnd,
+ MediaResourceIndex& aResource) const {
+ int64_t size = aEnd - aStart;
+ if (size <= 0) {
+ return false;
+ }
+ UniquePtr<char[]> buffer(new char[static_cast<size_t>(size)]);
+ uint32_t read = 0;
+ if (NS_FAILED(aResource.ReadAt(aStart, buffer.get(), size, &read)) ||
+ read != size) {
+ NS_WARNING("Couldn't read frame content");
+ return false;
+ }
+
+ uint16_t crc = 0;
+ uint8_t* buf = reinterpret_cast<uint8_t*>(buffer.get());
+ const uint8_t* end = buf + size;
+ while (buf < end) {
+ crc = CRC16Table[((uint8_t)crc) ^ *buf++] ^ (crc >> 8);
+ }
+#ifdef FUZZING
+ return true;
+#else
+ return !crc;
+#endif
+ }
+
+ const uint16_t CRC16Table[256] = {
+ 0x0000, 0x0580, 0x0F80, 0x0A00, 0x1B80, 0x1E00, 0x1400, 0x1180, 0x3380,
+ 0x3600, 0x3C00, 0x3980, 0x2800, 0x2D80, 0x2780, 0x2200, 0x6380, 0x6600,
+ 0x6C00, 0x6980, 0x7800, 0x7D80, 0x7780, 0x7200, 0x5000, 0x5580, 0x5F80,
+ 0x5A00, 0x4B80, 0x4E00, 0x4400, 0x4180, 0xC380, 0xC600, 0xCC00, 0xC980,
+ 0xD800, 0xDD80, 0xD780, 0xD200, 0xF000, 0xF580, 0xFF80, 0xFA00, 0xEB80,
+ 0xEE00, 0xE400, 0xE180, 0xA000, 0xA580, 0xAF80, 0xAA00, 0xBB80, 0xBE00,
+ 0xB400, 0xB180, 0x9380, 0x9600, 0x9C00, 0x9980, 0x8800, 0x8D80, 0x8780,
+ 0x8200, 0x8381, 0x8601, 0x8C01, 0x8981, 0x9801, 0x9D81, 0x9781, 0x9201,
+ 0xB001, 0xB581, 0xBF81, 0xBA01, 0xAB81, 0xAE01, 0xA401, 0xA181, 0xE001,
+ 0xE581, 0xEF81, 0xEA01, 0xFB81, 0xFE01, 0xF401, 0xF181, 0xD381, 0xD601,
+ 0xDC01, 0xD981, 0xC801, 0xCD81, 0xC781, 0xC201, 0x4001, 0x4581, 0x4F81,
+ 0x4A01, 0x5B81, 0x5E01, 0x5401, 0x5181, 0x7381, 0x7601, 0x7C01, 0x7981,
+ 0x6801, 0x6D81, 0x6781, 0x6201, 0x2381, 0x2601, 0x2C01, 0x2981, 0x3801,
+ 0x3D81, 0x3781, 0x3201, 0x1001, 0x1581, 0x1F81, 0x1A01, 0x0B81, 0x0E01,
+ 0x0401, 0x0181, 0x0383, 0x0603, 0x0C03, 0x0983, 0x1803, 0x1D83, 0x1783,
+ 0x1203, 0x3003, 0x3583, 0x3F83, 0x3A03, 0x2B83, 0x2E03, 0x2403, 0x2183,
+ 0x6003, 0x6583, 0x6F83, 0x6A03, 0x7B83, 0x7E03, 0x7403, 0x7183, 0x5383,
+ 0x5603, 0x5C03, 0x5983, 0x4803, 0x4D83, 0x4783, 0x4203, 0xC003, 0xC583,
+ 0xCF83, 0xCA03, 0xDB83, 0xDE03, 0xD403, 0xD183, 0xF383, 0xF603, 0xFC03,
+ 0xF983, 0xE803, 0xED83, 0xE783, 0xE203, 0xA383, 0xA603, 0xAC03, 0xA983,
+ 0xB803, 0xBD83, 0xB783, 0xB203, 0x9003, 0x9583, 0x9F83, 0x9A03, 0x8B83,
+ 0x8E03, 0x8403, 0x8183, 0x8002, 0x8582, 0x8F82, 0x8A02, 0x9B82, 0x9E02,
+ 0x9402, 0x9182, 0xB382, 0xB602, 0xBC02, 0xB982, 0xA802, 0xAD82, 0xA782,
+ 0xA202, 0xE382, 0xE602, 0xEC02, 0xE982, 0xF802, 0xFD82, 0xF782, 0xF202,
+ 0xD002, 0xD582, 0xDF82, 0xDA02, 0xCB82, 0xCE02, 0xC402, 0xC182, 0x4382,
+ 0x4602, 0x4C02, 0x4982, 0x5802, 0x5D82, 0x5782, 0x5202, 0x7002, 0x7582,
+ 0x7F82, 0x7A02, 0x6B82, 0x6E02, 0x6402, 0x6182, 0x2002, 0x2582, 0x2F82,
+ 0x2A02, 0x3B82, 0x3E02, 0x3402, 0x3182, 0x1382, 0x1602, 0x1C02, 0x1982,
+ 0x0802, 0x0D82, 0x0782, 0x0202,
+ };
+
+ FlacFrameParser mParser;
+ // We keep the first parsed frame around for static info access
+ // and the currently parsed frame.
+ Frame mFirstFrame;
+ Frame mNextFrame;
+ Frame mFrame;
+};
+
+} // namespace flac
+
+// FlacDemuxer
+
+FlacDemuxer::FlacDemuxer(MediaResource* aSource) : mSource(aSource) {
+ DDLINKCHILD("source", aSource);
+}
+
+bool FlacDemuxer::InitInternal() {
+ if (!mTrackDemuxer) {
+ mTrackDemuxer = new FlacTrackDemuxer(mSource);
+ DDLINKCHILD("track demuxer", mTrackDemuxer.get());
+ }
+ return mTrackDemuxer->Init();
+}
+
+RefPtr<FlacDemuxer::InitPromise> FlacDemuxer::Init() {
+ if (!InitInternal()) {
+ LOG("Init() failure: waiting for data");
+
+ return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_DEMUXER_ERR,
+ __func__);
+ }
+
+ LOG("Init() successful");
+ return InitPromise::CreateAndResolve(NS_OK, __func__);
+}
+
+uint32_t FlacDemuxer::GetNumberTracks(TrackInfo::TrackType aType) const {
+ return (aType == TrackInfo::kAudioTrack) ? 1 : 0;
+}
+
+already_AddRefed<MediaTrackDemuxer> FlacDemuxer::GetTrackDemuxer(
+ TrackInfo::TrackType aType, uint32_t aTrackNumber) {
+ if (!mTrackDemuxer) {
+ return nullptr;
+ }
+
+ return RefPtr<FlacTrackDemuxer>(mTrackDemuxer).forget();
+}
+
+bool FlacDemuxer::IsSeekable() const {
+ return mTrackDemuxer && mTrackDemuxer->IsSeekable();
+}
+
+// FlacTrackDemuxer
+FlacTrackDemuxer::FlacTrackDemuxer(MediaResource* aSource)
+ : mSource(aSource), mParser(new flac::FrameParser()), mTotalFrameLen(0) {
+ DDLINKCHILD("source", aSource);
+ Reset();
+}
+
+FlacTrackDemuxer::~FlacTrackDemuxer() = default;
+
+bool FlacTrackDemuxer::Init() {
+ static const int BUFFER_SIZE = 4096;
+
+ // First check if we have a valid Flac start.
+ char buffer[BUFFER_SIZE];
+ const uint8_t* ubuffer = // only needed due to type constraints of ReadAt.
+ reinterpret_cast<uint8_t*>(buffer);
+ int64_t offset = 0;
+
+ do {
+ uint32_t read = 0;
+ nsresult ret = mSource.ReadAt(offset, buffer, BUFFER_SIZE, &read);
+ if (NS_FAILED(ret) || read < BUFFER_SIZE) {
+ // Assume that if we can't read that many bytes while parsing the header,
+ // that something is wrong.
+ return false;
+ }
+ if (!mParser->IsHeaderBlock(ubuffer, BUFFER_SIZE)) {
+ // Not a header and we haven't reached the end of the metadata blocks.
+ // Will fall back to using the frames header instead.
+ break;
+ }
+ uint32_t sizeHeader = mParser->HeaderBlockLength(ubuffer);
+ RefPtr<MediaByteBuffer> block = mSource.MediaReadAt(offset, sizeHeader);
+ if (!block || block->Length() != sizeHeader) {
+ break;
+ }
+ if (!mParser->DecodeHeaderBlock(block->Elements(), sizeHeader)) {
+ break;
+ }
+ offset += sizeHeader;
+ } while (!mParser->HasFullMetadata());
+
+ // First flac frame is found after the metadata.
+ // Can seek there immediately to avoid reparsing it all.
+ mSource.Seek(SEEK_SET, offset);
+
+ // Find the first frame to fully initialise our parser.
+ if (mParser->FindNextFrame(mSource)) {
+ // Ensure that the next frame returned will be the first.
+ mSource.Seek(SEEK_SET, mParser->FirstFrame().Offset());
+ mParser->EndFrameSession();
+ } else if (!mParser->Info().IsValid() || !mParser->FirstFrame().IsValid()) {
+ // We must find at least a frame to determine the metadata.
+ // We can't play this stream.
+ return false;
+ }
+
+ if (!mParser->Info().IsValid() || !mParser->Info().mDuration.IsPositive()) {
+ // Check if we can look at the last frame for the end time to determine the
+ // duration when we don't have any.
+ TimeAtEnd();
+ }
+
+ return true;
+}
+
+UniquePtr<TrackInfo> FlacTrackDemuxer::GetInfo() const {
+ if (mParser->Info().IsValid()) {
+ // We have a proper metadata header.
+ UniquePtr<TrackInfo> info = mParser->Info().Clone();
+ UniquePtr<MetadataTags> tags(mParser->GetTags());
+ if (tags) {
+ for (const auto& entry : *tags) {
+ info->mTags.AppendElement(MetadataTag(entry.GetKey(), entry.GetData()));
+ }
+ }
+ MOZ_ASSERT(info->IsAudio() &&
+ info->GetAsAudioInfo()
+ ->mCodecSpecificConfig.is<FlacCodecSpecificData>(),
+ "Should get flac specific data from parser");
+ return info;
+ } else if (mParser->FirstFrame().Info().IsValid()) {
+ // Use the first frame header.
+ UniquePtr<TrackInfo> info = mParser->FirstFrame().Info().Clone();
+ info->mDuration = Duration();
+ MOZ_ASSERT(info->IsAudio() &&
+ info->GetAsAudioInfo()
+ ->mCodecSpecificConfig.is<FlacCodecSpecificData>(),
+ "Should get flac specific data from parser");
+ return info;
+ }
+ return nullptr;
+}
+
+bool FlacTrackDemuxer::IsSeekable() const {
+ // For now we only allow seeking if a STREAMINFO block was found and with
+ // a known number of samples (duration is set).
+ return mParser->Info().IsValid() && mParser->Info().mDuration.IsPositive();
+}
+
+RefPtr<FlacTrackDemuxer::SeekPromise> FlacTrackDemuxer::Seek(
+ const TimeUnit& aTime) {
+ // Efficiently seek to the position.
+ FastSeek(aTime);
+ // Correct seek position by scanning the next frames.
+ const TimeUnit seekTime = ScanUntil(aTime);
+
+ return SeekPromise::CreateAndResolve(seekTime, __func__);
+}
+
+TimeUnit FlacTrackDemuxer::FastSeek(const TimeUnit& aTime) {
+ LOG("FastSeek(%f) avgFrameLen=%f mParsedFramesDuration=%f offset=%" PRId64,
+ aTime.ToSeconds(), AverageFrameLength(),
+ mParsedFramesDuration.ToSeconds(), GetResourceOffset());
+
+ // Invalidate current frames in the parser.
+ mParser->EndFrameSession();
+
+ if (!mParser->FirstFrame().IsValid()) {
+ // Something wrong, and there's nothing to seek to anyway, so we can
+ // do whatever here.
+ mSource.Seek(SEEK_SET, 0);
+ return TimeUnit();
+ }
+
+ if (aTime <= mParser->FirstFrame().Time()) {
+ // We're attempting to seek prior the first frame, return the first frame.
+ mSource.Seek(SEEK_SET, mParser->FirstFrame().Offset());
+ return mParser->FirstFrame().Time();
+ }
+
+ // We look for the seek position using a bisection search, starting where the
+ // estimated position might be using the average frame length.
+ // Typically, with flac such approximation is typically useless.
+
+ // Estimate where the position might be.
+ int64_t pivot =
+ aTime.ToSeconds() * AverageFrameLength() + mParser->FirstFrame().Offset();
+
+ // Time in seconds where we can stop seeking and will continue using
+ // ScanUntil.
+ static const int GAP_THRESHOLD = 5;
+ int64_t first = mParser->FirstFrame().Offset();
+ int64_t last = mSource.GetLength();
+ Maybe<uint64_t> lastFoundOffset;
+ uint32_t iterations = 0;
+ TimeUnit timeSeekedTo;
+
+ do {
+ iterations++;
+ mSource.Seek(SEEK_SET, pivot);
+ flac::Frame frame;
+ if (!frame.FindNext(mSource)) {
+ NS_WARNING("We should have found a point");
+ break;
+ }
+ timeSeekedTo = frame.Time();
+
+ LOGV("FastSeek: interation:%u found:%f @ %" PRIu64, iterations,
+ timeSeekedTo.ToSeconds(), frame.Offset());
+
+ if (lastFoundOffset && lastFoundOffset.ref() == frame.Offset()) {
+ // Same frame found twice. We're done.
+ break;
+ }
+ lastFoundOffset = Some(frame.Offset());
+
+ if (frame.Time() == aTime) {
+ break;
+ }
+ if (aTime > frame.Time() &&
+ aTime - frame.Time() <= TimeUnit::FromSeconds(GAP_THRESHOLD)) {
+ // We're close enough to the target, experimentation shows that bisection
+ // search doesn't help much after that.
+ break;
+ }
+ if (frame.Time() > aTime) {
+ last = pivot;
+ pivot -= (pivot - first) / 2;
+ } else {
+ first = pivot;
+ pivot += (last - pivot) / 2;
+ }
+ } while (true);
+
+ if (lastFoundOffset) {
+ mSource.Seek(SEEK_SET, lastFoundOffset.ref());
+ }
+
+ return timeSeekedTo;
+}
+
+TimeUnit FlacTrackDemuxer::ScanUntil(const TimeUnit& aTime) {
+ LOG("ScanUntil(%f avgFrameLen=%f mParsedFramesDuration=%f offset=%" PRId64,
+ aTime.ToSeconds(), AverageFrameLength(),
+ mParsedFramesDuration.ToSeconds(), mParser->CurrentFrame().Offset());
+
+ if (!mParser->FirstFrame().IsValid() ||
+ aTime <= mParser->FirstFrame().Time()) {
+ return FastSeek(aTime);
+ }
+
+ int64_t previousOffset = 0;
+ TimeUnit previousTime;
+ while (FindNextFrame().IsValid() && mParser->CurrentFrame().Time() < aTime) {
+ previousOffset = mParser->CurrentFrame().Offset();
+ previousTime = mParser->CurrentFrame().Time();
+ }
+
+ if (!mParser->CurrentFrame().IsValid()) {
+ // We reached EOS.
+ return Duration();
+ }
+
+ // Seek back to the last frame found prior the target.
+ mParser->EndFrameSession();
+ mSource.Seek(SEEK_SET, previousOffset);
+ return previousTime;
+}
+
+RefPtr<FlacTrackDemuxer::SamplesPromise> FlacTrackDemuxer::GetSamples(
+ int32_t aNumSamples) {
+ LOGV("GetSamples(%d) Begin offset=%" PRId64
+ " mParsedFramesDuration=%f"
+ " mTotalFrameLen=%" PRIu64,
+ aNumSamples, GetResourceOffset(), mParsedFramesDuration.ToSeconds(),
+ mTotalFrameLen);
+
+ if (!aNumSamples) {
+ return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_DEMUXER_ERR,
+ __func__);
+ }
+
+ RefPtr<SamplesHolder> frames = new SamplesHolder();
+
+ while (aNumSamples--) {
+ RefPtr<MediaRawData> frame(GetNextFrame(FindNextFrame()));
+ if (!frame) break;
+ if (!frame->HasValidTime()) {
+ return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_DEMUXER_ERR,
+ __func__);
+ }
+ frames->AppendSample(frame);
+ }
+
+ LOGV("GetSamples() End mSamples.Length=%zu aNumSamples=%d offset=%" PRId64
+ " mParsedFramesDuration=%f mTotalFrameLen=%" PRIu64,
+ frames->GetSamples().Length(), aNumSamples, GetResourceOffset(),
+ mParsedFramesDuration.ToSeconds(), mTotalFrameLen);
+
+ if (frames->GetSamples().IsEmpty()) {
+ return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_END_OF_STREAM,
+ __func__);
+ }
+
+ return SamplesPromise::CreateAndResolve(frames, __func__);
+}
+
+void FlacTrackDemuxer::Reset() {
+ LOG("Reset()");
+ MOZ_ASSERT(mParser);
+ if (mParser->FirstFrame().IsValid()) {
+ mSource.Seek(SEEK_SET, mParser->FirstFrame().Offset());
+ } else {
+ mSource.Seek(SEEK_SET, 0);
+ }
+ mParser->EndFrameSession();
+}
+
+RefPtr<FlacTrackDemuxer::SkipAccessPointPromise>
+FlacTrackDemuxer::SkipToNextRandomAccessPoint(const TimeUnit& aTimeThreshold) {
+ // Will not be called for audio-only resources.
+ return SkipAccessPointPromise::CreateAndReject(
+ SkipFailureHolder(NS_ERROR_DOM_MEDIA_DEMUXER_ERR, 0), __func__);
+}
+
+int64_t FlacTrackDemuxer::GetResourceOffset() const { return mSource.Tell(); }
+
+TimeIntervals FlacTrackDemuxer::GetBuffered() {
+ TimeUnit duration = Duration();
+
+ if (duration <= TimeUnit()) {
+ return TimeIntervals();
+ }
+
+ // We could simply parse the cached data instead and read the timestamps.
+ // However, for now this will do.
+ AutoPinned<MediaResource> stream(mSource.GetResource());
+ return GetEstimatedBufferedTimeRanges(stream, duration.ToMicroseconds());
+}
+
+const flac::Frame& FlacTrackDemuxer::FindNextFrame() {
+ LOGV("FindNext() Begin offset=%" PRId64
+ " mParsedFramesDuration=%f"
+ " mTotalFrameLen=%" PRIu64,
+ GetResourceOffset(), mParsedFramesDuration.ToSeconds(), mTotalFrameLen);
+
+ if (mParser->FindNextFrame(mSource)) {
+ // Update our current progress stats.
+ mParsedFramesDuration =
+ std::max(mParsedFramesDuration, mParser->CurrentFrame().Time() -
+ mParser->FirstFrame().Time() +
+ mParser->CurrentFrame().Duration());
+ mTotalFrameLen =
+ std::max<uint64_t>(mTotalFrameLen, mParser->CurrentFrame().Offset() -
+ mParser->FirstFrame().Offset() +
+ mParser->CurrentFrame().Size());
+
+ LOGV("FindNext() End time=%f offset=%" PRId64
+ " mParsedFramesDuration=%f"
+ " mTotalFrameLen=%" PRIu64,
+ mParser->CurrentFrame().Time().ToSeconds(), GetResourceOffset(),
+ mParsedFramesDuration.ToSeconds(), mTotalFrameLen);
+ }
+
+ return mParser->CurrentFrame();
+}
+
+already_AddRefed<MediaRawData> FlacTrackDemuxer::GetNextFrame(
+ const flac::Frame& aFrame) {
+ if (!aFrame.IsValid()) {
+ LOG("GetNextFrame() EOS");
+ return nullptr;
+ }
+
+ LOG("GetNextFrame() Begin(time=%f offset=%" PRId64 " size=%u)",
+ aFrame.Time().ToSeconds(), aFrame.Offset(), aFrame.Size());
+
+ const uint64_t offset = aFrame.Offset();
+ const uint32_t size = aFrame.Size();
+
+ RefPtr<MediaRawData> frame = new MediaRawData();
+ frame->mOffset = offset;
+
+ UniquePtr<MediaRawDataWriter> frameWriter(frame->CreateWriter());
+ if (!frameWriter->SetSize(size)) {
+ LOG("GetNext() Exit failed to allocated media buffer");
+ return nullptr;
+ }
+
+ const uint32_t read = Read(frameWriter->Data(), offset, size);
+ if (read != size) {
+ LOG("GetNextFrame() Exit read=%u frame->Size=%zu", read, frame->Size());
+ return nullptr;
+ }
+
+ frame->mTime = aFrame.Time();
+ frame->mDuration = aFrame.Duration();
+ frame->mTimecode = frame->mTime;
+ frame->mOffset = aFrame.Offset();
+ frame->mKeyframe = true;
+
+ MOZ_ASSERT(!frame->mTime.IsNegative());
+ MOZ_ASSERT(!frame->mDuration.IsNegative());
+
+ return frame.forget();
+}
+
+uint32_t FlacTrackDemuxer::Read(uint8_t* aBuffer, int64_t aOffset,
+ int32_t aSize) {
+ uint32_t read = 0;
+ const nsresult rv = mSource.ReadAt(aOffset, reinterpret_cast<char*>(aBuffer),
+ static_cast<uint32_t>(aSize), &read);
+ NS_ENSURE_SUCCESS(rv, 0);
+ return read;
+}
+
+double FlacTrackDemuxer::AverageFrameLength() const {
+ if (mParsedFramesDuration.ToMicroseconds()) {
+ return mTotalFrameLen / mParsedFramesDuration.ToSeconds();
+ }
+
+ return 0.0;
+}
+
+TimeUnit FlacTrackDemuxer::Duration() const {
+ return std::max(mParsedFramesDuration, mParser->Info().mDuration);
+}
+
+TimeUnit FlacTrackDemuxer::TimeAtEnd() {
+ // Scan the last 128kB if available to determine the last frame.
+ static const int OFFSET_FROM_END = 128 * 1024;
+
+ // Seek to the end of the file and attempt to find the last frame.
+ MediaResourceIndex source(mSource.GetResource());
+ TimeUnit previousDuration;
+ TimeUnit previousTime;
+
+ const int64_t streamLen = mSource.GetLength();
+ if (streamLen < 0) {
+ return TimeUnit::FromInfinity();
+ }
+
+ flac::FrameParser parser;
+
+ source.Seek(SEEK_SET, std::max<int64_t>(0LL, streamLen - OFFSET_FROM_END));
+ while (parser.FindNextFrame(source)) {
+ // FFmpeg flac muxer can generate a last frame with earlier than the others.
+ previousTime = std::max(previousTime, parser.CurrentFrame().Time());
+ if (parser.CurrentFrame().Duration() > TimeUnit()) {
+ // The last frame doesn't have a duration, so only update our duration
+ // if we do have one.
+ previousDuration = parser.CurrentFrame().Duration();
+ }
+ if (source.Tell() >= streamLen) {
+ // Limit the read, in case the length change half-way.
+ break;
+ }
+ }
+
+ // Update our current progress stats.
+ mParsedFramesDuration =
+ previousTime + previousDuration - mParser->FirstFrame().Time();
+
+ mTotalFrameLen =
+ static_cast<uint64_t>(streamLen) - mParser->FirstFrame().Offset();
+
+ return mParsedFramesDuration;
+}
+
+/* static */
+bool FlacDemuxer::FlacSniffer(const uint8_t* aData, const uint32_t aLength) {
+ if (aLength < FLAC_MIN_FRAME_SIZE) {
+ return false;
+ }
+
+ flac::Frame frame;
+ return frame.FindNext(aData, aLength) >= 0;
+}
+
+} // namespace mozilla
diff --git a/dom/media/flac/FlacDemuxer.h b/dom/media/flac/FlacDemuxer.h
new file mode 100644
index 0000000000..cfbbfb7c57
--- /dev/null
+++ b/dom/media/flac/FlacDemuxer.h
@@ -0,0 +1,112 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef FLAC_DEMUXER_H_
+#define FLAC_DEMUXER_H_
+
+#include "mozilla/Attributes.h"
+#include "MediaDataDemuxer.h"
+#include "MediaResource.h"
+namespace mozilla {
+
+namespace flac {
+class Frame;
+class FrameParser;
+} // namespace flac
+class FlacTrackDemuxer;
+
+DDLoggedTypeDeclNameAndBase(FlacDemuxer, MediaDataDemuxer);
+DDLoggedTypeNameAndBase(FlacTrackDemuxer, MediaTrackDemuxer);
+
+class FlacDemuxer : public MediaDataDemuxer,
+ public DecoderDoctorLifeLogger<FlacDemuxer> {
+ public:
+ // MediaDataDemuxer interface.
+ explicit FlacDemuxer(MediaResource* aSource);
+ RefPtr<InitPromise> Init() override;
+ uint32_t GetNumberTracks(TrackInfo::TrackType aType) const override;
+ already_AddRefed<MediaTrackDemuxer> GetTrackDemuxer(
+ TrackInfo::TrackType aType, uint32_t aTrackNumber) override;
+ bool IsSeekable() const override;
+
+ // Return true if a valid flac frame header could be found.
+ static bool FlacSniffer(const uint8_t* aData, const uint32_t aLength);
+
+ private:
+ bool InitInternal();
+
+ RefPtr<MediaResource> mSource;
+ RefPtr<FlacTrackDemuxer> mTrackDemuxer;
+};
+
+class FlacTrackDemuxer : public MediaTrackDemuxer,
+ public DecoderDoctorLifeLogger<FlacTrackDemuxer> {
+ public:
+ explicit FlacTrackDemuxer(MediaResource* aSource);
+
+ // Initializes the track demuxer by reading the first frame for meta data.
+ // Returns initialization success state.
+ bool Init();
+
+ // MediaTrackDemuxer interface.
+ UniquePtr<TrackInfo> GetInfo() const override;
+ RefPtr<SeekPromise> Seek(const media::TimeUnit& aTime) override;
+ RefPtr<SamplesPromise> GetSamples(int32_t aNumSamples = 1) override;
+ void Reset() override;
+ int64_t GetResourceOffset() const override;
+ media::TimeIntervals GetBuffered() override;
+ RefPtr<SkipAccessPointPromise> SkipToNextRandomAccessPoint(
+ const media::TimeUnit& aTimeThreshold) override;
+
+ bool IsSeekable() const;
+
+ private:
+ // Destructor.
+ ~FlacTrackDemuxer();
+
+ // Returns the estimated stream duration, or a 0-duration if unknown.
+ media::TimeUnit Duration() const;
+ media::TimeUnit TimeAtEnd();
+
+ // Fast approximate seeking to given time.
+ media::TimeUnit FastSeek(const media::TimeUnit& aTime);
+
+ // Seeks by scanning the stream up to the given time for more accurate
+ // results.
+ media::TimeUnit ScanUntil(const media::TimeUnit& aTime);
+
+ // Finds the next valid frame and return it.
+ const flac::Frame& FindNextFrame();
+
+ // Returns the next ADTS frame, if available.
+ already_AddRefed<MediaRawData> GetNextFrame(const flac::Frame& aFrame);
+
+ // Reads aSize bytes into aBuffer from the source starting at aOffset.
+ // Returns the actual size read.
+ uint32_t Read(uint8_t* aBuffer, int64_t aOffset, int32_t aSize);
+
+ // Returns the average frame length derived from the previously parsed frames.
+ double AverageFrameLength() const;
+
+ // The (hopefully) Flac resource.
+ MediaResourceIndex mSource;
+
+ // Flac frame parser used to detect frames and extract side info.
+ UniquePtr<flac::FrameParser> mParser;
+
+ // Total duration of parsed frames.
+ media::TimeUnit mParsedFramesDuration;
+
+ // Sum of parsed frames' lengths in bytes.
+ uint64_t mTotalFrameLen;
+
+ // Audio track config info.
+ UniquePtr<AudioInfo> mInfo;
+};
+
+} // namespace mozilla
+
+#endif // !FLAC_DEMUXER_H_
diff --git a/dom/media/flac/FlacFrameParser.cpp b/dom/media/flac/FlacFrameParser.cpp
new file mode 100644
index 0000000000..c042a7d334
--- /dev/null
+++ b/dom/media/flac/FlacFrameParser.cpp
@@ -0,0 +1,244 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FlacFrameParser.h"
+#include "nsTArray.h"
+#include "OggCodecState.h"
+#include "OpusParser.h"
+#include "VideoUtils.h"
+#include "BufferReader.h"
+#include "mozilla/ResultExtensions.h"
+
+namespace mozilla {
+
+#define OGG_FLAC_METADATA_TYPE_STREAMINFO 0x7F
+#define FLAC_STREAMINFO_SIZE 34
+
+#define BITMASK(x) ((1ULL << x) - 1)
+
+enum {
+ FLAC_METADATA_TYPE_STREAMINFO = 0,
+ FLAC_METADATA_TYPE_PADDING,
+ FLAC_METADATA_TYPE_APPLICATION,
+ FLAC_METADATA_TYPE_SEEKTABLE,
+ FLAC_METADATA_TYPE_VORBIS_COMMENT,
+ FLAC_METADATA_TYPE_CUESHEET,
+ FLAC_METADATA_TYPE_PICTURE,
+ FLAC_METADATA_TYPE_INVALID = 127
+};
+
+FlacFrameParser::FlacFrameParser()
+ : mMinBlockSize(0),
+ mMaxBlockSize(0),
+ mMinFrameSize(0),
+ mMaxFrameSize(0),
+ mNumFrames(0),
+ mFullMetadata(false),
+ mPacketCount(0){};
+
+FlacFrameParser::~FlacFrameParser() = default;
+
+uint32_t FlacFrameParser::HeaderBlockLength(const uint8_t* aPacket) const {
+ uint32_t extra = 4;
+ if (aPacket[0] == 'f') {
+ // This must be the first block read, which contains the fLaC signature.
+ aPacket += 4;
+ extra += 4;
+ }
+ return (BigEndian::readUint32(aPacket) & BITMASK(24)) + extra;
+}
+
+Result<Ok, nsresult> FlacFrameParser::DecodeHeaderBlock(const uint8_t* aPacket,
+ size_t aLength) {
+ if (aLength < 4 || aPacket[0] == 0xff) {
+ // Not a header block.
+ return Err(NS_ERROR_FAILURE);
+ }
+ BufferReader br(aPacket, aLength);
+
+ mPacketCount++;
+
+ if (aPacket[0] == 'f') {
+ if (mPacketCount != 1 || memcmp(br.Read(4), "fLaC", 4) ||
+ br.Remaining() != FLAC_STREAMINFO_SIZE + 4) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ }
+ uint8_t blockHeader;
+ MOZ_TRY_VAR(blockHeader, br.ReadU8());
+ // blockType is a misnomer as it could indicate here either a packet type
+ // should it points to the start of a Flac in Ogg metadata, or an actual
+ // block type as per the flac specification.
+ uint32_t blockType = blockHeader & 0x7f;
+ bool lastBlock = blockHeader & 0x80;
+
+ if (blockType == OGG_FLAC_METADATA_TYPE_STREAMINFO) {
+ if (mPacketCount != 1 || memcmp(br.Read(4), "FLAC", 4) ||
+ br.Remaining() != FLAC_STREAMINFO_SIZE + 12) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ uint32_t major;
+ MOZ_TRY_VAR(major, br.ReadU8());
+ if (major != 1) {
+ // unsupported version;
+ return Err(NS_ERROR_FAILURE);
+ }
+ MOZ_TRY(br.ReadU8()); // minor version
+ uint32_t header;
+ MOZ_TRY_VAR(header, br.ReadU16());
+ mNumHeaders = Some(header);
+ br.Read(4); // fLaC
+ MOZ_TRY_VAR(blockType, br.ReadU8());
+ blockType &= BITMASK(7);
+ // First METADATA_BLOCK_STREAMINFO
+ if (blockType != FLAC_METADATA_TYPE_STREAMINFO) {
+ // First block must be a stream info.
+ return Err(NS_ERROR_FAILURE);
+ }
+ }
+
+ uint32_t blockDataSize;
+ MOZ_TRY_VAR(blockDataSize, br.ReadU24());
+ const uint8_t* blockDataStart = br.Peek(blockDataSize);
+ if (!blockDataStart) {
+ // Incomplete block.
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ switch (blockType) {
+ case FLAC_METADATA_TYPE_STREAMINFO: {
+ if (mPacketCount != 1 || blockDataSize != FLAC_STREAMINFO_SIZE) {
+ // STREAMINFO must be the first metadata block found, and its size
+ // is constant.
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ MOZ_TRY_VAR(mMinBlockSize, br.ReadU16());
+ MOZ_TRY_VAR(mMaxBlockSize, br.ReadU16());
+ MOZ_TRY_VAR(mMinFrameSize, br.ReadU24());
+ MOZ_TRY_VAR(mMaxFrameSize, br.ReadU24());
+
+ uint64_t blob;
+ MOZ_TRY_VAR(blob, br.ReadU64());
+ uint32_t sampleRate = (blob >> 44) & BITMASK(20);
+ if (!sampleRate) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ uint32_t numChannels = ((blob >> 41) & BITMASK(3)) + 1;
+ if (numChannels > FLAC_MAX_CHANNELS) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ uint32_t bps = ((blob >> 36) & BITMASK(5)) + 1;
+ if (bps > 24) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ mNumFrames = blob & BITMASK(36);
+
+ mInfo.mMimeType = "audio/flac";
+ mInfo.mRate = sampleRate;
+ mInfo.mChannels = numChannels;
+ mInfo.mBitDepth = bps;
+ FlacCodecSpecificData flacCodecSpecificData;
+ flacCodecSpecificData.mStreamInfoBinaryBlob->AppendElements(
+ blockDataStart, blockDataSize);
+ mInfo.mCodecSpecificConfig =
+ AudioCodecSpecificVariant{std::move(flacCodecSpecificData)};
+ auto duration = media::TimeUnit(mNumFrames, sampleRate);
+ mInfo.mDuration = duration.IsValid() ? duration : media::TimeUnit::Zero();
+ mParser = MakeUnique<OpusParser>();
+ break;
+ }
+ case FLAC_METADATA_TYPE_VORBIS_COMMENT: {
+ if (!mParser) {
+ // We must have seen a valid streaminfo first.
+ return Err(NS_ERROR_FAILURE);
+ }
+ nsTArray<uint8_t> comments(blockDataSize + 8);
+ comments.AppendElements("OpusTags", 8);
+ comments.AppendElements(blockDataStart, blockDataSize);
+ if (!mParser->DecodeTags(comments.Elements(), comments.Length())) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ if (mNumHeaders && mPacketCount > mNumHeaders.ref() + 1) {
+ // Received too many header block. assuming invalid.
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ if (lastBlock || (mNumHeaders && mNumHeaders.ref() + 1 == mPacketCount)) {
+ mFullMetadata = true;
+ }
+
+ return Ok();
+}
+
+int64_t FlacFrameParser::BlockDuration(const uint8_t* aPacket,
+ size_t aLength) const {
+ if (!mInfo.IsValid()) {
+ return -1;
+ }
+ if (mMinBlockSize == mMaxBlockSize) {
+ // block size is fixed, use this instead of looking at the frame header.
+ return mMinBlockSize;
+ }
+ // TODO
+ return 0;
+}
+
+Result<bool, nsresult> FlacFrameParser::IsHeaderBlock(const uint8_t* aPacket,
+ size_t aLength) const {
+ // Ogg Flac header
+ // The one-byte packet type 0x7F
+ // The four-byte ASCII signature "FLAC", i.e. 0x46, 0x4C, 0x41, 0x43
+
+ // Flac header:
+ // "fLaC", the FLAC stream marker in ASCII, meaning byte 0 of the stream is
+ // 0x66, followed by 0x4C 0x61 0x43
+
+ // If we detect either a ogg or plain flac header, then it must be valid.
+ if (aLength < 4 || aPacket[0] == 0xff) {
+ // A header is at least 4 bytes.
+ return false;
+ }
+ if (aPacket[0] == 0x7f) {
+ // Ogg packet
+ BufferReader br(aPacket + 1, aLength - 1);
+ const uint8_t* signature = br.Read(4);
+ return signature && !memcmp(signature, "FLAC", 4);
+ }
+ BufferReader br(aPacket, aLength - 1);
+ const uint8_t* signature = br.Read(4);
+ if (signature && !memcmp(signature, "fLaC", 4)) {
+ // Flac start header, must have STREAMINFO as first metadata block;
+ uint32_t blockType;
+ MOZ_TRY_VAR(blockType, br.ReadU8());
+ blockType &= 0x7f;
+ return blockType == FLAC_METADATA_TYPE_STREAMINFO;
+ }
+ char type = aPacket[0] & 0x7f;
+ return type >= 1 && type <= 6;
+}
+
+UniquePtr<MetadataTags> FlacFrameParser::GetTags() const {
+ if (!mParser) {
+ return nullptr;
+ }
+
+ auto tags = MakeUnique<MetadataTags>();
+ for (uint32_t i = 0; i < mParser->mTags.Length(); i++) {
+ OggCodecState::AddVorbisComment(tags, mParser->mTags[i].Data(),
+ mParser->mTags[i].Length());
+ }
+
+ return tags;
+}
+
+} // namespace mozilla
diff --git a/dom/media/flac/FlacFrameParser.h b/dom/media/flac/FlacFrameParser.h
new file mode 100644
index 0000000000..2bd457ea03
--- /dev/null
+++ b/dom/media/flac/FlacFrameParser.h
@@ -0,0 +1,72 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef FLAC_FRAME_PARSER_H_
+#define FLAC_FRAME_PARSER_H_
+
+#include "mozilla/Maybe.h"
+#include "mozilla/Result.h"
+#include "MediaDecoder.h" // For MetadataTags
+#include "MediaInfo.h"
+#include "MediaResource.h"
+
+namespace mozilla {
+
+#define FLAC_MAX_CHANNELS 8
+#define FLAC_MIN_BLOCKSIZE 16
+#define FLAC_MAX_BLOCKSIZE 65535
+#define FLAC_MIN_FRAME_SIZE 11
+#define FLAC_MAX_FRAME_HEADER_SIZE 16
+#define FLAC_MAX_FRAME_SIZE \
+ (FLAC_MAX_FRAME_HEADER_SIZE + FLAC_MAX_BLOCKSIZE * FLAC_MAX_CHANNELS * 3)
+
+class OpusParser;
+
+// Decode a Flac Metadata block contained in either a ogg packet
+// (https://xiph.org/flac/ogg_mapping.html) or in flac container
+// (https://xiph.org/flac/format.html#frame_header)
+
+class FlacFrameParser {
+ public:
+ FlacFrameParser();
+ ~FlacFrameParser();
+
+ Result<bool, nsresult> IsHeaderBlock(const uint8_t* aPacket,
+ size_t aLength) const;
+ // Return the length of the block header (METADATA_BLOCK_HEADER+
+ // METADATA_BLOCK_DATA), aPacket must point to at least 4
+ // bytes and to a valid block header start (as determined by IsHeaderBlock).
+ uint32_t HeaderBlockLength(const uint8_t* aPacket) const;
+ Result<Ok, nsresult> DecodeHeaderBlock(const uint8_t* aPacket,
+ size_t aLength);
+ bool HasFullMetadata() const { return mFullMetadata; }
+ // Return the duration in frames found in the block. -1 if error
+ // such as invalid packet.
+ int64_t BlockDuration(const uint8_t* aPacket, size_t aLength) const;
+
+ // Return a hash table with tag metadata.
+ UniquePtr<MetadataTags> GetTags() const;
+
+ AudioInfo mInfo;
+
+ private:
+ bool ReconstructFlacGranulepos(void);
+ Maybe<uint32_t> mNumHeaders;
+ uint32_t mMinBlockSize;
+ uint32_t mMaxBlockSize;
+ uint32_t mMinFrameSize;
+ uint32_t mMaxFrameSize;
+ uint64_t mNumFrames;
+ bool mFullMetadata;
+ uint32_t mPacketCount;
+
+ // Used to decode the vorbis comment metadata.
+ UniquePtr<OpusParser> mParser;
+};
+
+} // namespace mozilla
+
+#endif // FLAC_FRAME_PARSER_H_
diff --git a/dom/media/flac/moz.build b/dom/media/flac/moz.build
new file mode 100644
index 0000000000..0e3c8eae61
--- /dev/null
+++ b/dom/media/flac/moz.build
@@ -0,0 +1,24 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS += [
+ "FlacDecoder.h",
+ "FlacDemuxer.h",
+ "FlacFrameParser.h",
+]
+
+UNIFIED_SOURCES += [
+ "FlacDecoder.cpp",
+ "FlacDemuxer.cpp",
+ "FlacFrameParser.cpp",
+]
+
+CXXFLAGS += CONFIG["MOZ_LIBVPX_CFLAGS"]
+
+FINAL_LIBRARY = "xul"
+
+# Add libFuzzer configuration directives
+include("/tools/fuzzing/libfuzzer-config.mozbuild")
diff --git a/dom/media/fuzz/FuzzMedia.cpp b/dom/media/fuzz/FuzzMedia.cpp
new file mode 100644
index 0000000000..add6e14486
--- /dev/null
+++ b/dom/media/fuzz/FuzzMedia.cpp
@@ -0,0 +1,66 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ADTSDemuxer.h"
+#include "Benchmark.h"
+#include "BufferMediaResource.h"
+#include "FlacDemuxer.h"
+#include "FuzzingInterface.h"
+#include "mozilla/AbstractThread.h"
+#include "mozilla/SpinEventLoopUntil.h"
+#include "MP3Demuxer.h"
+#include "MP4Demuxer.h"
+#include "OggDemuxer.h"
+#include "systemservices/MediaUtils.h"
+#include "WaveDemuxer.h"
+#include "WebMDemuxer.h"
+
+using namespace mozilla;
+
+class FuzzRunner {
+ public:
+ explicit FuzzRunner(Benchmark* aBenchmark) : mBenchmark(aBenchmark) {}
+
+ void Run() {
+ // Assert we're on the main thread, otherwise `done` must be synchronized.
+ MOZ_ASSERT(NS_IsMainThread());
+ bool done = false;
+
+ mBenchmark->Init();
+ mBenchmark->Run()->Then(
+ // Non DocGroup-version of AbstractThread::MainThread() is fine for
+ // testing.
+ AbstractThread::MainThread(), __func__,
+ [&](uint32_t aDecodeFps) { done = true; }, [&]() { done = true; });
+
+ // Wait until benchmark completes.
+ SpinEventLoopUntil("FuzzRunner::Run"_ns, [&]() { return done; });
+ return;
+ }
+
+ private:
+ RefPtr<Benchmark> mBenchmark;
+};
+
+#define MOZ_MEDIA_FUZZER(_name) \
+ static int FuzzingRunMedia##_name(const uint8_t* data, size_t size) { \
+ if (!size) { \
+ return 0; \
+ } \
+ RefPtr<BufferMediaResource> resource = \
+ new BufferMediaResource(data, size); \
+ FuzzRunner runner(new Benchmark(new _name##Demuxer(resource))); \
+ runner.Run(); \
+ return 0; \
+ } \
+ MOZ_FUZZING_INTERFACE_RAW(nullptr, FuzzingRunMedia##_name, Media##_name);
+
+MOZ_MEDIA_FUZZER(ADTS);
+MOZ_MEDIA_FUZZER(Flac);
+MOZ_MEDIA_FUZZER(MP3);
+MOZ_MEDIA_FUZZER(MP4);
+MOZ_MEDIA_FUZZER(Ogg);
+MOZ_MEDIA_FUZZER(WAV);
+MOZ_MEDIA_FUZZER(WebM);
diff --git a/dom/media/fuzz/moz.build b/dom/media/fuzz/moz.build
new file mode 100644
index 0000000000..f672bbf93c
--- /dev/null
+++ b/dom/media/fuzz/moz.build
@@ -0,0 +1,29 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Library("FuzzingMedia")
+
+SOURCES += [
+ "FuzzMedia.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+LOCAL_INCLUDES += [
+ "/dom/media",
+ "/dom/media/encoder",
+ "/dom/media/gmp",
+ "/dom/media/hls",
+ "/dom/media/mp4",
+ "/dom/media/ogg",
+ "/dom/media/platforms",
+ "/dom/media/platforms/agnostic",
+]
+
+FINAL_LIBRARY = "xul-gtest"
+
+# Add libFuzzer configuration directives
+include("/tools/fuzzing/libfuzzer-config.mozbuild")
diff --git a/dom/media/gmp-plugin-openh264/fakeopenh264.info b/dom/media/gmp-plugin-openh264/fakeopenh264.info
new file mode 100644
index 0000000000..814a96be32
--- /dev/null
+++ b/dom/media/gmp-plugin-openh264/fakeopenh264.info
@@ -0,0 +1,4 @@
+Name: fakeopenh264
+Description: Fake GMP Plugin
+Version: 1.0
+APIs: encode-video[h264:fake], decode-video[h264:fake]
diff --git a/dom/media/gmp-plugin-openh264/gmp-fake-openh264.cpp b/dom/media/gmp-plugin-openh264/gmp-fake-openh264.cpp
new file mode 100644
index 0000000000..4c5e95643e
--- /dev/null
+++ b/dom/media/gmp-plugin-openh264/gmp-fake-openh264.cpp
@@ -0,0 +1,406 @@
+/*!
+ * \copy
+ * Copyright (c) 2009-2014, Cisco Systems
+ * Copyright (c) 2014, Mozilla
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
+ * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
+ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ *
+ *************************************************************************************
+ */
+
+#include <stdint.h>
+#include <cstring>
+#include <iostream>
+#include <assert.h>
+
+#include "gmp-platform.h"
+#include "gmp-video-host.h"
+#include "gmp-video-encode.h"
+#include "gmp-video-decode.h"
+#include "gmp-video-frame-i420.h"
+#include "gmp-video-frame-encoded.h"
+
+#include "mozilla/PodOperations.h"
+
+#if defined(_MSC_VER)
+# define PUBLIC_FUNC __declspec(dllexport)
+#else
+# define PUBLIC_FUNC
+#endif
+
+#define BIG_FRAME 10000
+
+#define GL_CRIT 0
+#define GL_ERROR 1
+#define GL_INFO 2
+#define GL_DEBUG 3
+
+const char* kLogStrings[] = {"Critical", "Error", "Info", "Debug"};
+
+static int g_log_level = GL_CRIT;
+
+#define GMPLOG(l, x) \
+ do { \
+ if (l <= g_log_level) { \
+ const char* log_string = "unknown"; \
+ if ((l >= 0) && (l <= 3)) { \
+ log_string = kLogStrings[l]; \
+ } \
+ std::cerr << log_string << ": " << x << std::endl; \
+ } \
+ } while (0)
+
+class FakeVideoEncoder;
+class FakeVideoDecoder;
+
+// Turn off padding for this structure. Having extra bytes can result in
+// bitstream parsing problems.
+#pragma pack(push, 1)
+struct EncodedFrame {
+ struct SPSNalu {
+ uint32_t size_;
+ uint8_t payload[14];
+ } sps_nalu;
+ struct PPSNalu {
+ uint32_t size_;
+ uint8_t payload[4];
+ } pps_nalu;
+ struct IDRNalu {
+ uint32_t size_;
+ uint8_t h264_compat_;
+ uint32_t magic_;
+ uint32_t width_;
+ uint32_t height_;
+ uint8_t y_;
+ uint8_t u_;
+ uint8_t v_;
+ uint32_t timestamp_;
+ } idr_nalu;
+};
+#pragma pack(pop)
+
+#define ENCODED_FRAME_MAGIC 0x004000b8
+
+template <typename T>
+class SelfDestruct {
+ public:
+ explicit SelfDestruct(T* t) : t_(t) {}
+ ~SelfDestruct() {
+ if (t_) {
+ t_->Destroy();
+ }
+ }
+
+ private:
+ T* t_;
+};
+
+class FakeVideoEncoder : public GMPVideoEncoder {
+ public:
+ explicit FakeVideoEncoder(GMPVideoHost* hostAPI) : host_(hostAPI) {}
+
+ void InitEncode(const GMPVideoCodec& codecSettings,
+ const uint8_t* aCodecSpecific, uint32_t aCodecSpecificSize,
+ GMPVideoEncoderCallback* callback, int32_t numberOfCores,
+ uint32_t maxPayloadSize) override {
+ callback_ = callback;
+ frame_size_ = (maxPayloadSize > 0 && maxPayloadSize < BIG_FRAME)
+ ? maxPayloadSize
+ : BIG_FRAME;
+ frame_size_ -= 24 + 40;
+ // default header+extension size is 24, but let's leave extra room if
+ // we enable more extensions.
+ // XXX -- why isn't the size passed in based on the size minus extensions?
+
+ const char* env = getenv("GMP_LOGGING");
+ if (env) {
+ g_log_level = atoi(env);
+ }
+ GMPLOG(GL_INFO, "Initialized encoder");
+ }
+
+ void SendFrame(GMPVideoi420Frame* inputImage, GMPVideoFrameType frame_type,
+ int nal_type) {
+ // Encode this in a frame that looks a little bit like H.264.
+ // Send SPS/PPS/IDR to avoid confusing people
+ // Copy the data. This really should convert this to network byte order.
+ EncodedFrame eframe;
+
+ // These values were chosen to force a SPS id of 0
+ eframe.sps_nalu = {sizeof(EncodedFrame::SPSNalu) - sizeof(uint32_t),
+ {0x67, 0x42, 0xc0, 0xd, 0x8c, 0x8d, 0x40, 0xa0, 0xf9,
+ 0x0, 0xf0, 0x88, 0x46, 0xa0}};
+
+ // These values were chosen to force a PPS id of 0
+ eframe.pps_nalu = {sizeof(EncodedFrame::PPSNalu) - sizeof(uint32_t),
+ {0x68, 0xce, 0x3c, 0x80}};
+
+ eframe.idr_nalu.size_ = sizeof(EncodedFrame::IDRNalu) - sizeof(uint32_t);
+ // We force IFrame here - if we send PFrames, the webrtc.org code gets
+ // tripped up attempting to find non-existent previous IFrames.
+ eframe.idr_nalu.h264_compat_ =
+ nal_type; // 5 = IFrame/IDR slice, 1=PFrame/slice
+ eframe.idr_nalu.magic_ = ENCODED_FRAME_MAGIC;
+ eframe.idr_nalu.width_ = inputImage->Width();
+ eframe.idr_nalu.height_ = inputImage->Height();
+ eframe.idr_nalu.y_ = AveragePlane(inputImage->Buffer(kGMPYPlane),
+ inputImage->AllocatedSize(kGMPYPlane));
+ eframe.idr_nalu.u_ = AveragePlane(inputImage->Buffer(kGMPUPlane),
+ inputImage->AllocatedSize(kGMPUPlane));
+ eframe.idr_nalu.v_ = AveragePlane(inputImage->Buffer(kGMPVPlane),
+ inputImage->AllocatedSize(kGMPVPlane));
+
+ eframe.idr_nalu.timestamp_ = inputImage->Timestamp();
+
+ // Now return the encoded data back to the parent.
+ GMPVideoFrame* ftmp;
+ GMPErr err = host_->CreateFrame(kGMPEncodedVideoFrame, &ftmp);
+ if (err != GMPNoErr) {
+ GMPLOG(GL_ERROR, "Error creating encoded frame");
+ return;
+ }
+
+ GMPVideoEncodedFrame* f = static_cast<GMPVideoEncodedFrame*>(ftmp);
+
+ err = f->CreateEmptyFrame(sizeof(eframe));
+ if (err != GMPNoErr) {
+ GMPLOG(GL_ERROR, "Error allocating frame data");
+ f->Destroy();
+ return;
+ }
+ memcpy(f->Buffer(), &eframe, sizeof(eframe));
+ f->SetEncodedWidth(eframe.idr_nalu.width_);
+ f->SetEncodedHeight(eframe.idr_nalu.height_);
+ f->SetTimeStamp(eframe.idr_nalu.timestamp_);
+ f->SetFrameType(frame_type);
+ f->SetCompleteFrame(true);
+ f->SetBufferType(GMP_BufferLength32);
+
+ GMPLOG(GL_DEBUG, "Encoding complete. type= "
+ << f->FrameType()
+ << " NAL_type=" << (int)eframe.idr_nalu.h264_compat_
+ << " length=" << f->Size()
+ << " timestamp=" << f->TimeStamp()
+ << " width/height=" << eframe.idr_nalu.width_ << "x"
+ << eframe.idr_nalu.height_);
+
+ // Return the encoded frame.
+ GMPCodecSpecificInfo info;
+ mozilla::PodZero(&info);
+ info.mCodecType = kGMPVideoCodecH264;
+ info.mBufferType = GMP_BufferLength32;
+ info.mCodecSpecific.mH264.mSimulcastIdx = 0;
+ GMPLOG(GL_DEBUG, "Calling callback");
+ callback_->Encoded(f, reinterpret_cast<uint8_t*>(&info), sizeof(info));
+ GMPLOG(GL_DEBUG, "Callback called");
+ }
+
+ void Encode(GMPVideoi420Frame* inputImage, const uint8_t* aCodecSpecificInfo,
+ uint32_t aCodecSpecificInfoLength,
+ const GMPVideoFrameType* aFrameTypes,
+ uint32_t aFrameTypesLength) override {
+ GMPLOG(GL_DEBUG, __FUNCTION__ << " size=" << inputImage->Width() << "x"
+ << inputImage->Height());
+
+ assert(aFrameTypesLength != 0);
+ GMPVideoFrameType frame_type = aFrameTypes[0];
+
+ SelfDestruct<GMPVideoi420Frame> ifd(inputImage);
+
+ if (frame_type == kGMPKeyFrame) {
+ if (!inputImage) return;
+ }
+ if (!inputImage) {
+ GMPLOG(GL_ERROR, "no input image");
+ return;
+ }
+
+ if (frame_type == kGMPKeyFrame ||
+ frames_encoded_++ % 10 == 0) { // periodically send iframes anyways
+ // 5 = IFrame/IDR slice
+ SendFrame(inputImage, kGMPKeyFrame, 5);
+ } else {
+ // 1 = PFrame/slice
+ SendFrame(inputImage, frame_type, 1);
+ }
+ }
+
+ void SetChannelParameters(uint32_t aPacketLoss, uint32_t aRTT) override {}
+
+ void SetRates(uint32_t aNewBitRate, uint32_t aFrameRate) override {}
+
+ void SetPeriodicKeyFrames(bool aEnable) override {}
+
+ void EncodingComplete() override { delete this; }
+
+ private:
+ uint8_t AveragePlane(uint8_t* ptr, size_t len) {
+ uint64_t val = 0;
+
+ for (size_t i = 0; i < len; ++i) {
+ val += ptr[i];
+ }
+
+ return (val / len) % 0xff;
+ }
+
+ GMPVideoHost* host_;
+ GMPVideoEncoderCallback* callback_ = nullptr;
+ uint32_t frame_size_ = BIG_FRAME;
+ uint32_t frames_encoded_ = 0;
+};
+
+class FakeVideoDecoder : public GMPVideoDecoder {
+ public:
+ explicit FakeVideoDecoder(GMPVideoHost* hostAPI)
+ : host_(hostAPI), callback_(nullptr) {}
+
+ ~FakeVideoDecoder() override = default;
+
+ void InitDecode(const GMPVideoCodec& codecSettings,
+ const uint8_t* aCodecSpecific, uint32_t aCodecSpecificSize,
+ GMPVideoDecoderCallback* callback,
+ int32_t coreCount) override {
+ GMPLOG(GL_INFO, "InitDecode");
+
+ const char* env = getenv("GMP_LOGGING");
+ if (env) {
+ g_log_level = atoi(env);
+ }
+ callback_ = callback;
+ }
+
+ void Decode(GMPVideoEncodedFrame* inputFrame, bool missingFrames,
+ const uint8_t* aCodecSpecificInfo,
+ uint32_t aCodecSpecificInfoLength,
+ int64_t renderTimeMs = -1) override {
+ GMPLOG(GL_DEBUG, __FUNCTION__
+ << "Decoding frame size=" << inputFrame->Size()
+ << " timestamp=" << inputFrame->TimeStamp());
+
+ // Attach a self-destructor so that the input frame is destroyed on return.
+ SelfDestruct<GMPVideoEncodedFrame> ifd(inputFrame);
+
+ EncodedFrame* eframe;
+ eframe = reinterpret_cast<EncodedFrame*>(inputFrame->Buffer());
+ GMPLOG(GL_DEBUG, "magic=" << eframe->idr_nalu.magic_ << " h264_compat="
+ << (int)eframe->idr_nalu.h264_compat_
+ << " width=" << eframe->idr_nalu.width_
+ << " height=" << eframe->idr_nalu.height_
+ << " timestamp=" << inputFrame->TimeStamp()
+ << " y/u/v=" << (int)eframe->idr_nalu.y_ << ":"
+ << (int)eframe->idr_nalu.u_ << ":"
+ << (int)eframe->idr_nalu.v_);
+ if (inputFrame->Size() != (sizeof(*eframe))) {
+ GMPLOG(GL_ERROR, "Couldn't decode frame. Size=" << inputFrame->Size());
+ return;
+ }
+
+ if (eframe->idr_nalu.magic_ != ENCODED_FRAME_MAGIC) {
+ GMPLOG(GL_ERROR,
+ "Couldn't decode frame. Magic=" << eframe->idr_nalu.magic_);
+ return;
+ }
+ if (eframe->idr_nalu.h264_compat_ != 5 &&
+ eframe->idr_nalu.h264_compat_ != 1) {
+ // only return video for iframes or pframes
+ GMPLOG(GL_DEBUG, "Not a video frame: NAL type "
+ << (int)eframe->idr_nalu.h264_compat_);
+ return;
+ }
+
+ int width = eframe->idr_nalu.width_;
+ int height = eframe->idr_nalu.height_;
+ int ystride = eframe->idr_nalu.width_;
+ int uvstride = eframe->idr_nalu.width_ / 2;
+
+ GMPLOG(GL_DEBUG, "Video frame ready for display "
+ << width << "x" << height
+ << " timestamp=" << inputFrame->TimeStamp());
+
+ GMPVideoFrame* ftmp = nullptr;
+
+ // Translate the image.
+ GMPErr err = host_->CreateFrame(kGMPI420VideoFrame, &ftmp);
+ if (err != GMPNoErr) {
+ GMPLOG(GL_ERROR, "Couldn't allocate empty I420 frame");
+ return;
+ }
+
+ GMPVideoi420Frame* frame = static_cast<GMPVideoi420Frame*>(ftmp);
+ err = frame->CreateEmptyFrame(width, height, ystride, uvstride, uvstride);
+ if (err != GMPNoErr) {
+ GMPLOG(GL_ERROR, "Couldn't make decoded frame");
+ return;
+ }
+
+ memset(frame->Buffer(kGMPYPlane), eframe->idr_nalu.y_,
+ frame->AllocatedSize(kGMPYPlane));
+ memset(frame->Buffer(kGMPUPlane), eframe->idr_nalu.u_,
+ frame->AllocatedSize(kGMPUPlane));
+ memset(frame->Buffer(kGMPVPlane), eframe->idr_nalu.v_,
+ frame->AllocatedSize(kGMPVPlane));
+
+ GMPLOG(GL_DEBUG, "Allocated size = " << frame->AllocatedSize(kGMPYPlane));
+ frame->SetTimestamp(inputFrame->TimeStamp());
+ frame->SetDuration(inputFrame->Duration());
+ callback_->Decoded(frame);
+ }
+
+ void Reset() override {}
+
+ void Drain() override {}
+
+ void DecodingComplete() override { delete this; }
+
+ GMPVideoHost* host_;
+ GMPVideoDecoderCallback* callback_;
+};
+
+extern "C" {
+
+PUBLIC_FUNC GMPErr GMPInit(const GMPPlatformAPI* aPlatformAPI) {
+ return GMPNoErr;
+}
+
+PUBLIC_FUNC GMPErr GMPGetAPI(const char* aApiName, void* aHostAPI,
+ void** aPluginApi) {
+ if (!strcmp(aApiName, GMP_API_VIDEO_DECODER)) {
+ *aPluginApi = new FakeVideoDecoder(static_cast<GMPVideoHost*>(aHostAPI));
+ return GMPNoErr;
+ }
+ if (!strcmp(aApiName, GMP_API_VIDEO_ENCODER)) {
+ *aPluginApi = new FakeVideoEncoder(static_cast<GMPVideoHost*>(aHostAPI));
+ return GMPNoErr;
+ }
+ return GMPGenericErr;
+}
+
+PUBLIC_FUNC void GMPShutdown(void) {}
+
+} // extern "C"
diff --git a/dom/media/gmp-plugin-openh264/moz.build b/dom/media/gmp-plugin-openh264/moz.build
new file mode 100644
index 0000000000..e85a50673e
--- /dev/null
+++ b/dom/media/gmp-plugin-openh264/moz.build
@@ -0,0 +1,25 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# largely a copy of dom/media/gmp-fake/moz.build
+
+FINAL_TARGET = "dist/bin/gmp-fakeopenh264/1.0"
+
+FINAL_TARGET_FILES += [
+ "fakeopenh264.info",
+]
+
+SOURCES += [
+ "gmp-fake-openh264.cpp",
+]
+
+SharedLibrary("fakeopenh264")
+
+USE_STATIC_LIBS = True
+NoVisibilityFlags()
+# Don't use STL wrappers; this isn't Gecko code
+DisableStlWrapping()
+NO_PGO = True
diff --git a/dom/media/gmp/CDMStorageIdProvider.cpp b/dom/media/gmp/CDMStorageIdProvider.cpp
new file mode 100644
index 0000000000..52255879b3
--- /dev/null
+++ b/dom/media/gmp/CDMStorageIdProvider.cpp
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CDMStorageIdProvider.h"
+#include "GMPLog.h"
+#include "nsCOMPtr.h"
+#include "nsComponentManagerUtils.h"
+#include "nsICryptoHash.h"
+
+#ifdef SUPPORT_STORAGE_ID
+# include "rlz/lib/machine_id.h"
+#endif
+
+namespace mozilla {
+
+/*static*/
+nsCString CDMStorageIdProvider::ComputeStorageId(const nsCString& aOriginSalt) {
+#ifndef SUPPORT_STORAGE_ID
+ return ""_ns;
+#else
+ GMP_LOG_DEBUG("CDMStorageIdProvider::ComputeStorageId");
+
+ std::string machineId;
+ if (!rlz_lib::GetMachineId(&machineId)) {
+ GMP_LOG_DEBUG(
+ "CDMStorageIdProvider::ComputeStorageId: get machineId failed.");
+ return ""_ns;
+ }
+
+ std::string originSalt(aOriginSalt.BeginReading(), aOriginSalt.Length());
+ std::string input =
+ machineId + originSalt + CDMStorageIdProvider::kBrowserIdentifier;
+ nsCOMPtr<nsICryptoHash> hasher;
+ nsresult rv = NS_NewCryptoHash(nsICryptoHash::SHA256, getter_AddRefs(hasher));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ GMP_LOG_DEBUG(
+ "CDMStorageIdProvider::ComputeStorageId: failed to initialize "
+ "hash(0x%08" PRIx32 ")",
+ static_cast<uint32_t>(rv));
+ return ""_ns;
+ }
+
+ rv = hasher->Update(reinterpret_cast<const uint8_t*>(input.c_str()),
+ input.size());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ GMP_LOG_DEBUG(
+ "CDMStorageIdProvider::ComputeStorageId: failed to update "
+ "hash(0x%08" PRIx32 ")",
+ static_cast<uint32_t>(rv));
+ return ""_ns;
+ }
+
+ nsCString storageId;
+ rv = hasher->Finish(false, storageId);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ GMP_LOG_DEBUG(
+ "CDMStorageIdProvider::ComputeStorageId: failed to get the final hash "
+ "result(0x%08" PRIx32 ")",
+ static_cast<uint32_t>(rv));
+ return ""_ns;
+ }
+ return storageId;
+#endif
+}
+
+} // namespace mozilla
diff --git a/dom/media/gmp/CDMStorageIdProvider.h b/dom/media/gmp/CDMStorageIdProvider.h
new file mode 100644
index 0000000000..6904792607
--- /dev/null
+++ b/dom/media/gmp/CDMStorageIdProvider.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef CDMStorageIdProvider_h_
+#define CDMStorageIdProvider_h_
+
+/**
+ * CDM will try to request a latest version(0) of storage id.
+ * If the storage id computation algorithm changed, we should increase the
+ * kCurrentVersion.
+ */
+
+#include <string>
+
+#include "nsString.h"
+
+namespace mozilla {
+
+class CDMStorageIdProvider {
+ static constexpr const char* kBrowserIdentifier = "mozilla_firefox_gecko";
+
+ public:
+ // Should increase the value when the storage id algorithm changed.
+ static constexpr int kCurrentVersion = 1;
+ static constexpr int kCDMRequestLatestVersion = 0;
+
+ // Return empty string when
+ // 1. Call on unsupported storageid platform.
+ // 2. Failed to compute the storage id.
+ // This function only provide the storage id for kCurrentVersion=1.
+ // If you want to change the algorithm or output of storageid,
+ // you should keep the version 1 storage id and consider to provide
+ // higher version storage id in another function.
+ static nsCString ComputeStorageId(const nsCString& aOriginSalt);
+};
+
+} // namespace mozilla
+
+#endif // CDMStorageIdProvider_h_
diff --git a/dom/media/gmp/ChromiumCDMAdapter.cpp b/dom/media/gmp/ChromiumCDMAdapter.cpp
new file mode 100644
index 0000000000..a5e866f83e
--- /dev/null
+++ b/dom/media/gmp/ChromiumCDMAdapter.cpp
@@ -0,0 +1,307 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ChromiumCDMAdapter.h"
+
+#include <utility>
+
+#include "GMPLog.h"
+#include "WidevineUtils.h"
+#include "content_decryption_module.h"
+#include "content_decryption_module_ext.h"
+#include "gmp-api/gmp-entrypoints.h"
+#include "gmp-api/gmp-video-codec.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/HelperMacros.h"
+#include "mozilla/dom/KeySystemNames.h"
+
+#ifdef XP_WIN
+# include "WinUtils.h"
+# include "nsWindowsDllInterceptor.h"
+# include <windows.h>
+# include <strsafe.h>
+# include <unordered_map>
+# include <vector>
+#else
+# include <sys/types.h>
+# include <sys/stat.h>
+# include <unistd.h>
+# include <fcntl.h>
+#endif
+
+const GMPPlatformAPI* sPlatform = nullptr;
+
+namespace mozilla {
+
+#ifdef XP_WIN
+static void InitializeHooks();
+#endif
+
+ChromiumCDMAdapter::ChromiumCDMAdapter(
+ nsTArray<std::pair<nsCString, nsCString>>&& aHostPathPairs) {
+#ifdef XP_WIN
+ InitializeHooks();
+#endif
+ PopulateHostFiles(std::move(aHostPathPairs));
+}
+
+void ChromiumCDMAdapter::SetAdaptee(PRLibrary* aLib) { mLib = aLib; }
+
+void* ChromiumCdmHost(int aHostInterfaceVersion, void* aUserData) {
+ GMP_LOG_DEBUG("ChromiumCdmHostFunc(%d, %p)", aHostInterfaceVersion,
+ aUserData);
+ if (aHostInterfaceVersion != cdm::Host_10::kVersion) {
+ return nullptr;
+ }
+ return aUserData;
+}
+
+#ifdef MOZILLA_OFFICIAL
+static cdm::HostFile TakeToCDMHostFile(HostFileData& aHostFileData) {
+ return cdm::HostFile(aHostFileData.mBinary.Path().get(),
+ aHostFileData.mBinary.TakePlatformFile(),
+ aHostFileData.mSig.TakePlatformFile());
+}
+#endif
+
+GMPErr ChromiumCDMAdapter::GMPInit(const GMPPlatformAPI* aPlatformAPI) {
+ GMP_LOG_DEBUG("ChromiumCDMAdapter::GMPInit");
+ sPlatform = aPlatformAPI;
+ if (NS_WARN_IF(!mLib)) {
+ MOZ_CRASH("Missing library!");
+ return GMPGenericErr;
+ }
+
+#ifdef MOZILLA_OFFICIAL
+ // Note: we must call the VerifyCdmHost_0 function if it's present before
+ // we call the initialize function.
+ auto verify = reinterpret_cast<decltype(::VerifyCdmHost_0)*>(
+ PR_FindFunctionSymbol(mLib, MOZ_STRINGIFY(VerifyCdmHost_0)));
+ if (verify) {
+ nsTArray<cdm::HostFile> files;
+ for (HostFileData& hostFile : mHostFiles) {
+ files.AppendElement(TakeToCDMHostFile(hostFile));
+ }
+ bool result = verify(files.Elements(), files.Length());
+ GMP_LOG_DEBUG("%s VerifyCdmHost_0 returned %d", __func__, result);
+ MOZ_DIAGNOSTIC_ASSERT(result, "Verification failed!");
+ }
+#endif
+
+ auto init = reinterpret_cast<decltype(::INITIALIZE_CDM_MODULE)*>(
+ PR_FindFunctionSymbol(mLib, MOZ_STRINGIFY(INITIALIZE_CDM_MODULE)));
+ if (!init) {
+ MOZ_CRASH("Missing init method!");
+ return GMPGenericErr;
+ }
+
+ GMP_LOG_DEBUG(MOZ_STRINGIFY(INITIALIZE_CDM_MODULE) "()");
+ init();
+
+ return GMPNoErr;
+}
+
+GMPErr ChromiumCDMAdapter::GMPGetAPI(const char* aAPIName, void* aHostAPI,
+ void** aPluginAPI,
+ const nsACString& aKeySystem) {
+ MOZ_ASSERT(
+ aKeySystem.EqualsLiteral(kWidevineKeySystemName) ||
+ aKeySystem.EqualsLiteral(kClearKeyKeySystemName) ||
+ aKeySystem.EqualsLiteral(kClearKeyWithProtectionQueryKeySystemName) ||
+ aKeySystem.EqualsLiteral("fake"),
+ "Should not get an unrecognized key system. Why didn't it get "
+ "blocked by MediaKeySystemAccess?");
+ GMP_LOG_DEBUG("ChromiumCDMAdapter::GMPGetAPI(%s, 0x%p, 0x%p, %s) this=0x%p",
+ aAPIName, aHostAPI, aPluginAPI,
+ PromiseFlatCString(aKeySystem).get(), this);
+ bool isCdm10 = !strcmp(aAPIName, CHROMIUM_CDM_API);
+
+ if (!isCdm10) {
+ MOZ_ASSERT_UNREACHABLE("We only support and expect cdm10!");
+ GMP_LOG_DEBUG(
+ "ChromiumCDMAdapter::GMPGetAPI(%s, 0x%p, 0x%p) this=0x%p got "
+ "unsupported CDM version!",
+ aAPIName, aHostAPI, aPluginAPI, this);
+ return GMPGenericErr;
+ }
+ auto create = reinterpret_cast<decltype(::CreateCdmInstance)*>(
+ PR_FindFunctionSymbol(mLib, "CreateCdmInstance"));
+ if (!create) {
+ GMP_LOG_DEBUG(
+ "ChromiumCDMAdapter::GMPGetAPI(%s, 0x%p, 0x%p) this=0x%p "
+ "FAILED to find CreateCdmInstance",
+ aAPIName, aHostAPI, aPluginAPI, this);
+ return GMPGenericErr;
+ }
+
+ const int version = cdm::ContentDecryptionModule_10::kVersion;
+ void* cdm = create(version, aKeySystem.BeginReading(), aKeySystem.Length(),
+ &ChromiumCdmHost, aHostAPI);
+ if (!cdm) {
+ GMP_LOG_DEBUG(
+ "ChromiumCDMAdapter::GMPGetAPI(%s, 0x%p, 0x%p) this=0x%p "
+ "FAILED to create cdm version %d",
+ aAPIName, aHostAPI, aPluginAPI, this, version);
+ return GMPGenericErr;
+ }
+ GMP_LOG_DEBUG("cdm: 0x%p, version: %d", cdm, version);
+ *aPluginAPI = cdm;
+
+ return *aPluginAPI ? GMPNoErr : GMPNotImplementedErr;
+}
+
+void ChromiumCDMAdapter::GMPShutdown() {
+ GMP_LOG_DEBUG("ChromiumCDMAdapter::GMPShutdown()");
+
+ decltype(::DeinitializeCdmModule)* deinit;
+ deinit =
+ (decltype(deinit))(PR_FindFunctionSymbol(mLib, "DeinitializeCdmModule"));
+ if (deinit) {
+ GMP_LOG_DEBUG("DeinitializeCdmModule()");
+ deinit();
+ }
+}
+
+/* static */
+bool ChromiumCDMAdapter::Supports(int32_t aModuleVersion,
+ int32_t aInterfaceVersion,
+ int32_t aHostVersion) {
+ return aModuleVersion == CDM_MODULE_VERSION &&
+ aInterfaceVersion == cdm::ContentDecryptionModule_10::kVersion &&
+ aHostVersion == cdm::Host_10::kVersion;
+}
+
+#ifdef XP_WIN
+
+static WindowsDllInterceptor sKernel32Intercept;
+
+typedef DWORD(WINAPI* QueryDosDeviceWFnPtr)(_In_opt_ LPCWSTR lpDeviceName,
+ _Out_ LPWSTR lpTargetPath,
+ _In_ DWORD ucchMax);
+
+static WindowsDllInterceptor::FuncHookType<QueryDosDeviceWFnPtr>
+ sOriginalQueryDosDeviceWFnPtr;
+
+static std::unordered_map<std::wstring, std::wstring>* sDeviceNames = nullptr;
+
+DWORD WINAPI QueryDosDeviceWHook(LPCWSTR lpDeviceName, LPWSTR lpTargetPath,
+ DWORD ucchMax) {
+ if (!sDeviceNames) {
+ return 0;
+ }
+ std::wstring name = std::wstring(lpDeviceName);
+ auto iter = sDeviceNames->find(name);
+ if (iter == sDeviceNames->end()) {
+ return 0;
+ }
+ const std::wstring& device = iter->second;
+ if (device.size() + 1 > ucchMax) {
+ return 0;
+ }
+ PodCopy(lpTargetPath, device.c_str(), device.size());
+ lpTargetPath[device.size()] = 0;
+ GMP_LOG_DEBUG("QueryDosDeviceWHook %S -> %S", lpDeviceName, lpTargetPath);
+ return device.size();
+}
+
+static std::vector<std::wstring> GetDosDeviceNames() {
+ std::vector<std::wstring> v;
+ std::vector<wchar_t> buf;
+ buf.resize(1024);
+ DWORD rv = GetLogicalDriveStringsW(buf.size(), buf.data());
+ if (rv == 0 || rv > buf.size()) {
+ return v;
+ }
+
+ // buf will be a list of null terminated strings, with the last string
+ // being 0 length.
+ const wchar_t* p = buf.data();
+ const wchar_t* end = &buf.back();
+ size_t l;
+ while (p < end && (l = wcsnlen_s(p, end - p)) > 0) {
+ // The string is of the form "C:\". We need to strip off the trailing
+ // backslash.
+ std::wstring drive = std::wstring(p, p + l);
+ if (drive.back() == '\\') {
+ drive.erase(drive.end() - 1);
+ }
+ v.push_back(std::move(drive));
+ p += l + 1;
+ }
+ return v;
+}
+
+static std::wstring GetDeviceMapping(const std::wstring& aDosDeviceName) {
+ wchar_t buf[MAX_PATH] = {0};
+ DWORD rv = QueryDosDeviceW(aDosDeviceName.c_str(), buf, MAX_PATH);
+ if (rv == 0) {
+ return std::wstring(L"");
+ }
+ return std::wstring(buf, buf + rv);
+}
+
+static void InitializeHooks() {
+ static bool initialized = false;
+ if (initialized) {
+ return;
+ }
+ initialized = true;
+ sDeviceNames = new std::unordered_map<std::wstring, std::wstring>();
+ for (const std::wstring& name : GetDosDeviceNames()) {
+ sDeviceNames->emplace(name, GetDeviceMapping(name));
+ }
+
+ sKernel32Intercept.Init("kernelbase.dll");
+ sOriginalQueryDosDeviceWFnPtr.Set(sKernel32Intercept, "QueryDosDeviceW",
+ &QueryDosDeviceWHook);
+}
+#endif
+
+HostFile::HostFile(HostFile&& aOther)
+ : mPath(aOther.mPath), mFile(aOther.TakePlatformFile()) {}
+
+HostFile::~HostFile() {
+ if (mFile != cdm::kInvalidPlatformFile) {
+#ifdef XP_WIN
+ CloseHandle(mFile);
+#else
+ close(mFile);
+#endif
+ mFile = cdm::kInvalidPlatformFile;
+ }
+}
+
+#ifdef XP_WIN
+HostFile::HostFile(const nsCString& aPath)
+ : mPath(NS_ConvertUTF8toUTF16(aPath)) {
+ HANDLE handle =
+ CreateFileW(mPath.get(), GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
+ NULL, OPEN_EXISTING, 0, NULL);
+ mFile = (handle == INVALID_HANDLE_VALUE) ? cdm::kInvalidPlatformFile : handle;
+}
+#endif
+
+#ifndef XP_WIN
+HostFile::HostFile(const nsCString& aPath) : mPath(aPath) {
+ // Note: open() returns -1 on failure; i.e. kInvalidPlatformFile.
+ mFile = open(aPath.get(), O_RDONLY);
+}
+#endif
+
+cdm::PlatformFile HostFile::TakePlatformFile() {
+ cdm::PlatformFile f = mFile;
+ mFile = cdm::kInvalidPlatformFile;
+ return f;
+}
+
+void ChromiumCDMAdapter::PopulateHostFiles(
+ nsTArray<std::pair<nsCString, nsCString>>&& aHostPathPairs) {
+ for (const auto& pair : aHostPathPairs) {
+ mHostFiles.AppendElement(HostFileData(mozilla::HostFile(pair.first),
+ mozilla::HostFile(pair.second)));
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/gmp/ChromiumCDMAdapter.h b/dom/media/gmp/ChromiumCDMAdapter.h
new file mode 100644
index 0000000000..747345fb27
--- /dev/null
+++ b/dom/media/gmp/ChromiumCDMAdapter.h
@@ -0,0 +1,81 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef ChromiumAdapter_h_
+#define ChromiumAdapter_h_
+
+#include "GMPLoader.h"
+#include "prlink.h"
+#include "GMPUtils.h"
+#include "nsTArray.h"
+#include "content_decryption_module_ext.h"
+#include "nsString.h"
+
+#include <utility>
+
+struct GMPPlatformAPI;
+
+namespace mozilla {
+
+#if defined(XP_WIN)
+typedef nsString HostFileString;
+#else
+typedef nsCString HostFileString;
+#endif
+
+class HostFile {
+ public:
+ explicit HostFile(const nsCString& aPath);
+ HostFile(HostFile&& aOther);
+ ~HostFile();
+
+ const HostFileString& Path() const { return mPath; }
+ cdm::PlatformFile TakePlatformFile();
+
+ private:
+ const HostFileString mPath;
+ cdm::PlatformFile mFile = cdm::kInvalidPlatformFile;
+};
+
+struct HostFileData {
+ HostFileData(HostFile&& aBinary, HostFile&& aSig)
+ : mBinary(std::move(aBinary)), mSig(std::move(aSig)) {}
+
+ HostFileData(HostFileData&& aOther)
+ : mBinary(std::move(aOther.mBinary)), mSig(std::move(aOther.mSig)) {}
+
+ ~HostFileData() = default;
+
+ HostFile mBinary;
+ HostFile mSig;
+};
+
+class ChromiumCDMAdapter : public gmp::GMPAdapter {
+ public:
+ explicit ChromiumCDMAdapter(
+ nsTArray<std::pair<nsCString, nsCString>>&& aHostPathPairs);
+
+ void SetAdaptee(PRLibrary* aLib) override;
+
+ // These are called in place of the corresponding GMP API functions.
+ GMPErr GMPInit(const GMPPlatformAPI* aPlatformAPI) override;
+ GMPErr GMPGetAPI(const char* aAPIName, void* aHostAPI, void** aPluginAPI,
+ const nsACString& aKeySystem) override;
+ void GMPShutdown() override;
+
+ static bool Supports(int32_t aModuleVersion, int32_t aInterfaceVersion,
+ int32_t aHostVersion);
+
+ private:
+ void PopulateHostFiles(
+ nsTArray<std::pair<nsCString, nsCString>>&& aHostFilePaths);
+
+ PRLibrary* mLib = nullptr;
+ nsTArray<HostFileData> mHostFiles;
+};
+
+} // namespace mozilla
+
+#endif // ChromiumAdapter_h_
diff --git a/dom/media/gmp/ChromiumCDMCallback.h b/dom/media/gmp/ChromiumCDMCallback.h
new file mode 100644
index 0000000000..3f62634933
--- /dev/null
+++ b/dom/media/gmp/ChromiumCDMCallback.h
@@ -0,0 +1,56 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef ChromiumCDMCallback_h_
+#define ChromiumCDMCallback_h_
+
+#include "mozilla/CDMProxy.h"
+#include "mozilla/dom/MediaKeyStatusMapBinding.h" // For MediaKeyStatus
+#include "mozilla/dom/MediaKeyMessageEventBinding.h" // For MediaKeyMessageType
+#include "mozilla/gmp/GMPTypes.h" // For CDMKeyInformation
+
+namespace mozilla {
+class ErrorResult;
+}
+
+class ChromiumCDMCallback {
+ public:
+ virtual ~ChromiumCDMCallback() = default;
+
+ virtual void SetSessionId(uint32_t aPromiseId,
+ const nsCString& aSessionId) = 0;
+
+ virtual void ResolveLoadSessionPromise(uint32_t aPromiseId,
+ bool aSuccessful) = 0;
+
+ virtual void ResolvePromiseWithKeyStatus(uint32_t aPromiseId,
+ uint32_t aKeyStatus) = 0;
+
+ virtual void ResolvePromise(uint32_t aPromiseId) = 0;
+
+ virtual void RejectPromise(uint32_t aPromiseId, mozilla::ErrorResult&& aError,
+ const nsCString& aErrorMessage) = 0;
+
+ virtual void SessionMessage(const nsACString& aSessionId,
+ uint32_t aMessageType,
+ nsTArray<uint8_t>&& aMessage) = 0;
+
+ virtual void SessionKeysChange(
+ const nsCString& aSessionId,
+ nsTArray<mozilla::gmp::CDMKeyInformation>&& aKeysInfo) = 0;
+
+ virtual void ExpirationChange(const nsCString& aSessionId,
+ double aSecondsSinceEpoch) = 0;
+
+ virtual void SessionClosed(const nsCString& aSessionId) = 0;
+
+ virtual void QueryOutputProtectionStatus() = 0;
+
+ virtual void Terminated() = 0;
+
+ virtual void Shutdown() = 0;
+};
+
+#endif
diff --git a/dom/media/gmp/ChromiumCDMCallbackProxy.cpp b/dom/media/gmp/ChromiumCDMCallbackProxy.cpp
new file mode 100644
index 0000000000..481534c0e4
--- /dev/null
+++ b/dom/media/gmp/ChromiumCDMCallbackProxy.cpp
@@ -0,0 +1,159 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ChromiumCDMCallbackProxy.h"
+
+#include <type_traits>
+
+#include "ChromiumCDMProxy.h"
+#include "content_decryption_module.h"
+
+namespace mozilla {
+
+template <class Func, class... Args>
+void ChromiumCDMCallbackProxy::DispatchToMainThread(const char* const aLabel,
+ Func aFunc,
+ Args&&... aArgs) {
+ mMainThread->Dispatch(
+ // Use decay to ensure all the types are passed by value not by reference.
+ NewRunnableMethod<std::decay_t<Args>...>(aLabel, mProxy, aFunc,
+ std::forward<Args>(aArgs)...),
+ NS_DISPATCH_NORMAL);
+}
+
+void ChromiumCDMCallbackProxy::SetSessionId(uint32_t aPromiseId,
+ const nsCString& aSessionId) {
+ DispatchToMainThread("ChromiumCDMProxy::OnSetSessionId",
+ &ChromiumCDMProxy::OnSetSessionId, aPromiseId,
+ NS_ConvertUTF8toUTF16(aSessionId));
+}
+
+void ChromiumCDMCallbackProxy::ResolveLoadSessionPromise(uint32_t aPromiseId,
+ bool aSuccessful) {
+ DispatchToMainThread("ChromiumCDMProxy::OnResolveLoadSessionPromise",
+ &ChromiumCDMProxy::OnResolveLoadSessionPromise,
+ aPromiseId, aSuccessful);
+}
+
+void ChromiumCDMCallbackProxy::ResolvePromise(uint32_t aPromiseId) {
+ DispatchToMainThread("ChromiumCDMProxy::ResolvePromise",
+ &ChromiumCDMProxy::ResolvePromise, aPromiseId);
+}
+
+void ChromiumCDMCallbackProxy::RejectPromise(uint32_t aPromiseId,
+ ErrorResult&& aException,
+ const nsCString& aErrorMessage) {
+ // Use CopyableErrorResult to store our exception in the runnable,
+ // because ErrorResult is not OK to move across threads.
+ DispatchToMainThread<decltype(&ChromiumCDMProxy::RejectPromiseOnMainThread),
+ int32_t, StoreCopyPassByRRef<CopyableErrorResult>,
+ const nsCString&>(
+ "ChromiumCDMProxy::RejectPromise",
+ &ChromiumCDMProxy::RejectPromiseOnMainThread, aPromiseId,
+ std::move(aException), aErrorMessage);
+}
+
+static dom::MediaKeyMessageType ToDOMMessageType(uint32_t aMessageType) {
+ switch (static_cast<cdm::MessageType>(aMessageType)) {
+ case cdm::kLicenseRequest:
+ return dom::MediaKeyMessageType::License_request;
+ case cdm::kLicenseRenewal:
+ return dom::MediaKeyMessageType::License_renewal;
+ case cdm::kLicenseRelease:
+ return dom::MediaKeyMessageType::License_release;
+ case cdm::kIndividualizationRequest:
+ return dom::MediaKeyMessageType::Individualization_request;
+ }
+ MOZ_ASSERT_UNREACHABLE("Invalid cdm::MessageType enum value.");
+ return dom::MediaKeyMessageType::License_request;
+}
+
+void ChromiumCDMCallbackProxy::SessionMessage(const nsACString& aSessionId,
+ uint32_t aMessageType,
+ nsTArray<uint8_t>&& aMessage) {
+ DispatchToMainThread("ChromiumCDMProxy::OnSessionMessage",
+ &ChromiumCDMProxy::OnSessionMessage,
+ NS_ConvertUTF8toUTF16(aSessionId),
+ ToDOMMessageType(aMessageType), std::move(aMessage));
+}
+
+static dom::MediaKeyStatus ToDOMMediaKeyStatus(uint32_t aStatus) {
+ switch (static_cast<cdm::KeyStatus>(aStatus)) {
+ case cdm::kUsable:
+ return dom::MediaKeyStatus::Usable;
+ case cdm::kInternalError:
+ return dom::MediaKeyStatus::Internal_error;
+ case cdm::kExpired:
+ return dom::MediaKeyStatus::Expired;
+ case cdm::kOutputRestricted:
+ return dom::MediaKeyStatus::Output_restricted;
+ case cdm::kOutputDownscaled:
+ return dom::MediaKeyStatus::Output_downscaled;
+ case cdm::kStatusPending:
+ return dom::MediaKeyStatus::Status_pending;
+ case cdm::kReleased:
+ return dom::MediaKeyStatus::Released;
+ }
+ MOZ_ASSERT_UNREACHABLE("Invalid cdm::KeyStatus enum value.");
+ return dom::MediaKeyStatus::Internal_error;
+}
+
+void ChromiumCDMCallbackProxy::ResolvePromiseWithKeyStatus(
+ uint32_t aPromiseId, uint32_t aKeyStatus) {
+ DispatchToMainThread("ChromiumCDMProxy::OnResolvePromiseWithKeyStatus",
+ &ChromiumCDMProxy::OnResolvePromiseWithKeyStatus,
+ aPromiseId, ToDOMMediaKeyStatus(aKeyStatus));
+}
+
+void ChromiumCDMCallbackProxy::SessionKeysChange(
+ const nsCString& aSessionId,
+ nsTArray<mozilla::gmp::CDMKeyInformation>&& aKeysInfo) {
+ bool keyStatusesChange = false;
+ {
+ auto caps = mProxy->Capabilites().Lock();
+ for (const auto& keyInfo : aKeysInfo) {
+ keyStatusesChange |= caps->SetKeyStatus(
+ keyInfo.mKeyId(), NS_ConvertUTF8toUTF16(aSessionId),
+ dom::Optional<dom::MediaKeyStatus>(
+ ToDOMMediaKeyStatus(keyInfo.mStatus())));
+ }
+ }
+ if (keyStatusesChange) {
+ DispatchToMainThread("ChromiumCDMProxy::OnKeyStatusesChange",
+ &ChromiumCDMProxy::OnKeyStatusesChange,
+ NS_ConvertUTF8toUTF16(aSessionId));
+ }
+}
+
+void ChromiumCDMCallbackProxy::ExpirationChange(const nsCString& aSessionId,
+ double aSecondsSinceEpoch) {
+ DispatchToMainThread("ChromiumCDMProxy::OnExpirationChange",
+ &ChromiumCDMProxy::OnExpirationChange,
+ NS_ConvertUTF8toUTF16(aSessionId),
+ UnixTime(aSecondsSinceEpoch * 1000));
+}
+
+void ChromiumCDMCallbackProxy::SessionClosed(const nsCString& aSessionId) {
+ DispatchToMainThread("ChromiumCDMProxy::OnSessionClosed",
+ &ChromiumCDMProxy::OnSessionClosed,
+ NS_ConvertUTF8toUTF16(aSessionId));
+}
+
+void ChromiumCDMCallbackProxy::QueryOutputProtectionStatus() {
+ DispatchToMainThread("ChromiumCDMProxy::QueryOutputProtectionStatus",
+ &ChromiumCDMProxy::QueryOutputProtectionStatus);
+}
+
+void ChromiumCDMCallbackProxy::Terminated() {
+ DispatchToMainThread("ChromiumCDMProxy::Terminated",
+ &ChromiumCDMProxy::Terminated);
+}
+
+void ChromiumCDMCallbackProxy::Shutdown() {
+ DispatchToMainThread("ChromiumCDMProxy::Shutdown",
+ &ChromiumCDMProxy::Shutdown);
+}
+
+} // namespace mozilla
diff --git a/dom/media/gmp/ChromiumCDMCallbackProxy.h b/dom/media/gmp/ChromiumCDMCallbackProxy.h
new file mode 100644
index 0000000000..5b240f9102
--- /dev/null
+++ b/dom/media/gmp/ChromiumCDMCallbackProxy.h
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef ChromiumCDMCallbackProxy_h_
+#define ChromiumCDMCallbackProxy_h_
+
+#include "ChromiumCDMCallback.h"
+#include "ChromiumCDMProxy.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+
+class ChromiumCDMCallbackProxy : public ChromiumCDMCallback {
+ public:
+ ChromiumCDMCallbackProxy(ChromiumCDMProxy* aProxy,
+ nsIEventTarget* aMainThread)
+ : mProxy(aProxy), mMainThread(aMainThread) {}
+
+ void SetSessionId(uint32_t aPromiseId, const nsCString& aSessionId) override;
+
+ void ResolveLoadSessionPromise(uint32_t aPromiseId,
+ bool aSuccessful) override;
+
+ void ResolvePromiseWithKeyStatus(uint32_t aPromiseId,
+ uint32_t aKeyStatus) override;
+
+ void ResolvePromise(uint32_t aPromiseId) override;
+
+ void RejectPromise(uint32_t aPromiseId, ErrorResult&& aException,
+ const nsCString& aErrorMessage) override;
+
+ void SessionMessage(const nsACString& aSessionId, uint32_t aMessageType,
+ nsTArray<uint8_t>&& aMessage) override;
+
+ void SessionKeysChange(
+ const nsCString& aSessionId,
+ nsTArray<mozilla::gmp::CDMKeyInformation>&& aKeysInfo) override;
+
+ void ExpirationChange(const nsCString& aSessionId,
+ double aSecondsSinceEpoch) override;
+
+ void SessionClosed(const nsCString& aSessionId) override;
+
+ void QueryOutputProtectionStatus() override;
+
+ void Terminated() override;
+
+ void Shutdown() override;
+
+ private:
+ template <class Func, class... Args>
+ void DispatchToMainThread(const char* const aLabel, Func aFunc,
+ Args&&... aArgs);
+ // Warning: Weak ref.
+ ChromiumCDMProxy* mProxy;
+ const nsCOMPtr<nsIEventTarget> mMainThread;
+};
+
+} // namespace mozilla
+#endif
diff --git a/dom/media/gmp/ChromiumCDMChild.cpp b/dom/media/gmp/ChromiumCDMChild.cpp
new file mode 100644
index 0000000000..4592f2e291
--- /dev/null
+++ b/dom/media/gmp/ChromiumCDMChild.cpp
@@ -0,0 +1,866 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ChromiumCDMChild.h"
+#include "GMPContentChild.h"
+#include "WidevineUtils.h"
+#include "WidevineFileIO.h"
+#include "WidevineVideoFrame.h"
+#include "GMPLog.h"
+#include "GMPPlatform.h"
+#include "mozilla/Unused.h"
+#include "nsPrintfCString.h"
+#include "base/time.h"
+#include "GMPUtils.h"
+#include "mozilla/ScopeExit.h"
+#include "CDMStorageIdProvider.h"
+#include "nsReadableUtils.h"
+
+#include <type_traits>
+
+namespace mozilla::gmp {
+
+ChromiumCDMChild::ChromiumCDMChild(GMPContentChild* aPlugin)
+ : mPlugin(aPlugin) {
+ MOZ_ASSERT(IsOnMessageLoopThread());
+ GMP_LOG_DEBUG("ChromiumCDMChild:: ctor this=%p", this);
+}
+
+void ChromiumCDMChild::Init(cdm::ContentDecryptionModule_10* aCDM,
+ const nsACString& aStorageId) {
+ MOZ_ASSERT(IsOnMessageLoopThread());
+ mCDM = aCDM;
+ MOZ_ASSERT(mCDM);
+ mStorageId = aStorageId;
+}
+
+void ChromiumCDMChild::TimerExpired(void* aContext) {
+ MOZ_ASSERT(IsOnMessageLoopThread());
+ GMP_LOG_DEBUG("ChromiumCDMChild::TimerExpired(context=0x%p)", aContext);
+ if (mCDM) {
+ mCDM->TimerExpired(aContext);
+ }
+}
+
+class CDMShmemBuffer : public CDMBuffer {
+ public:
+ CDMShmemBuffer(ChromiumCDMChild* aProtocol, ipc::Shmem aShmem)
+ : mProtocol(aProtocol), mSize(aShmem.Size<uint8_t>()), mShmem(aShmem) {
+ GMP_LOG_DEBUG("CDMShmemBuffer(size=%" PRIu32 ") created", Size());
+ // Note: Chrome initializes the size of a buffer to it capacity. We do the
+ // same.
+ }
+
+ CDMShmemBuffer(ChromiumCDMChild* aProtocol, ipc::Shmem aShmem,
+ WidevineBuffer* aLocalBuffer)
+ : CDMShmemBuffer(aProtocol, aShmem) {
+ MOZ_ASSERT(aLocalBuffer->Size() == Size());
+ memcpy(Data(), aLocalBuffer->Data(),
+ std::min<uint32_t>(aLocalBuffer->Size(), Size()));
+ }
+
+ ~CDMShmemBuffer() override {
+ GMP_LOG_DEBUG("CDMShmemBuffer(size=%" PRIu32 ") destructed writable=%d",
+ Size(), mShmem.IsWritable());
+ if (mShmem.IsWritable()) {
+ // The shmem wasn't extracted to send its data back up to the parent
+ // process, so we can reuse the shmem.
+ mProtocol->GiveBuffer(std::move(mShmem));
+ }
+ }
+
+ void Destroy() override {
+ GMP_LOG_DEBUG("CDMShmemBuffer::Destroy(size=%" PRIu32 ")", Size());
+ delete this;
+ }
+ uint32_t Capacity() const override { return mShmem.Size<uint8_t>(); }
+
+ uint8_t* Data() override { return mShmem.get<uint8_t>(); }
+
+ void SetSize(uint32_t aSize) override {
+ MOZ_ASSERT(aSize <= Capacity());
+ // Note: We can't use the shmem's size member after ExtractShmem(),
+ // has been called, so we track the size exlicitly so that we can use
+ // Size() in logging after we've called ExtractShmem().
+ GMP_LOG_DEBUG("CDMShmemBuffer::SetSize(size=%" PRIu32 ")", Size());
+ mSize = aSize;
+ }
+
+ uint32_t Size() const override { return mSize; }
+
+ ipc::Shmem ExtractShmem() {
+ ipc::Shmem shmem = mShmem;
+ mShmem = ipc::Shmem();
+ return shmem;
+ }
+
+ CDMShmemBuffer* AsShmemBuffer() override { return this; }
+
+ private:
+ RefPtr<ChromiumCDMChild> mProtocol;
+ uint32_t mSize;
+ mozilla::ipc::Shmem mShmem;
+ CDMShmemBuffer(const CDMShmemBuffer&);
+ void operator=(const CDMShmemBuffer&);
+};
+
+static auto ToString(const nsTArray<ipc::Shmem>& aBuffers) {
+ return StringJoin(","_ns, aBuffers, [](auto& s, const ipc::Shmem& shmem) {
+ s.AppendInt(static_cast<uint32_t>(shmem.Size<uint8_t>()));
+ });
+}
+
+cdm::Buffer* ChromiumCDMChild::Allocate(uint32_t aCapacity) {
+ GMP_LOG_DEBUG("ChromiumCDMChild::Allocate(capacity=%" PRIu32
+ ") bufferSizes={%s}",
+ aCapacity, ToString(mBuffers).get());
+ MOZ_ASSERT(IsOnMessageLoopThread());
+
+ if (mBuffers.IsEmpty()) {
+ Unused << SendIncreaseShmemPoolSize();
+ }
+
+ // Find the shmem with the least amount of wasted space if we were to
+ // select it for this sized allocation. We need to do this because shmems
+ // for decrypted audio as well as video frames are both stored in this
+ // list, and we don't want to use the video frame shmems for audio samples.
+ const size_t invalid = std::numeric_limits<size_t>::max();
+ size_t best = invalid;
+ auto wastedSpace = [this, aCapacity](size_t index) {
+ return mBuffers[index].Size<uint8_t>() - aCapacity;
+ };
+ for (size_t i = 0; i < mBuffers.Length(); i++) {
+ if (mBuffers[i].Size<uint8_t>() >= aCapacity &&
+ (best == invalid || wastedSpace(i) < wastedSpace(best))) {
+ best = i;
+ }
+ }
+ if (best == invalid) {
+ // The parent process should have bestowed upon us a shmem of appropriate
+ // size, but did not! Do a "dive and catch", and create an non-shared
+ // memory buffer. The parent will detect this and send us an extra shmem
+ // so future frames can be in shmems, i.e. returned on the fast path.
+ return new WidevineBuffer(aCapacity);
+ }
+ ipc::Shmem shmem = mBuffers[best];
+ mBuffers.RemoveElementAt(best);
+ return new CDMShmemBuffer(this, shmem);
+}
+
+void ChromiumCDMChild::SetTimer(int64_t aDelayMs, void* aContext) {
+ MOZ_ASSERT(IsOnMessageLoopThread());
+ GMP_LOG_DEBUG("ChromiumCDMChild::SetTimer(delay=%" PRId64 ", context=0x%p)",
+ aDelayMs, aContext);
+ RefPtr<ChromiumCDMChild> self(this);
+ SetTimerOnMainThread(
+ NewGMPTask([self, aContext]() { self->TimerExpired(aContext); }),
+ aDelayMs);
+}
+
+cdm::Time ChromiumCDMChild::GetCurrentWallTime() {
+ return base::Time::Now().ToDoubleT();
+}
+
+template <typename MethodType, typename... ParamType>
+void ChromiumCDMChild::CallMethod(MethodType aMethod, ParamType&&... aParams) {
+ MOZ_ASSERT(IsOnMessageLoopThread());
+ // Avoid calling member function after destroy.
+ if (!mDestroyed) {
+ Unused << (this->*aMethod)(std::forward<ParamType>(aParams)...);
+ }
+}
+
+template <typename MethodType, typename... ParamType>
+void ChromiumCDMChild::CallOnMessageLoopThread(const char* const aName,
+ MethodType aMethod,
+ ParamType&&... aParams) {
+ if (NS_WARN_IF(!mPlugin)) {
+ return;
+ }
+
+ if (IsOnMessageLoopThread()) {
+ CallMethod(aMethod, std::forward<ParamType>(aParams)...);
+ } else {
+ auto m = &ChromiumCDMChild::CallMethod<
+ decltype(aMethod), const std::remove_reference_t<ParamType>&...>;
+ RefPtr<mozilla::Runnable> t =
+ NewRunnableMethod<decltype(aMethod),
+ const std::remove_reference_t<ParamType>...>(
+ aName, this, m, aMethod, std::forward<ParamType>(aParams)...);
+ mPlugin->GMPMessageLoop()->PostTask(t.forget());
+ }
+}
+
+void ChromiumCDMChild::OnResolveKeyStatusPromise(uint32_t aPromiseId,
+ cdm::KeyStatus aKeyStatus) {
+ GMP_LOG_DEBUG("ChromiumCDMChild::OnResolveKeyStatusPromise(pid=%" PRIu32
+ "keystatus=%d)",
+ aPromiseId, aKeyStatus);
+ CallOnMessageLoopThread("gmp::ChromiumCDMChild::OnResolveKeyStatusPromise",
+ &ChromiumCDMChild::SendOnResolvePromiseWithKeyStatus,
+ aPromiseId, static_cast<uint32_t>(aKeyStatus));
+}
+
+bool ChromiumCDMChild::OnResolveNewSessionPromiseInternal(
+ uint32_t aPromiseId, const nsACString& aSessionId) {
+ MOZ_ASSERT(IsOnMessageLoopThread());
+ if (mLoadSessionPromiseIds.Contains(aPromiseId)) {
+ // As laid out in the Chromium CDM API, if the CDM fails to load
+ // a session it calls OnResolveNewSessionPromise with nullptr as the
+ // sessionId. We can safely assume this means that we have failed to load a
+ // session as the other methods specify calling 'OnRejectPromise' when they
+ // fail.
+ bool loadSuccessful = !aSessionId.IsEmpty();
+ GMP_LOG_DEBUG(
+ "ChromiumCDMChild::OnResolveNewSessionPromise(pid=%u, sid=%s) "
+ "resolving %s load session ",
+ aPromiseId, PromiseFlatCString(aSessionId).get(),
+ (loadSuccessful ? "successful" : "failed"));
+ mLoadSessionPromiseIds.RemoveElement(aPromiseId);
+ return SendResolveLoadSessionPromise(aPromiseId, loadSuccessful);
+ }
+
+ return SendOnResolveNewSessionPromise(aPromiseId, aSessionId);
+}
+void ChromiumCDMChild::OnResolveNewSessionPromise(uint32_t aPromiseId,
+ const char* aSessionId,
+ uint32_t aSessionIdSize) {
+ GMP_LOG_DEBUG("ChromiumCDMChild::OnResolveNewSessionPromise(pid=%" PRIu32
+ ", sid=%s)",
+ aPromiseId, aSessionId);
+ CallOnMessageLoopThread("gmp::ChromiumCDMChild::OnResolveNewSessionPromise",
+ &ChromiumCDMChild::OnResolveNewSessionPromiseInternal,
+ aPromiseId, nsCString(aSessionId, aSessionIdSize));
+}
+
+void ChromiumCDMChild::OnResolvePromise(uint32_t aPromiseId) {
+ GMP_LOG_DEBUG("ChromiumCDMChild::OnResolvePromise(pid=%" PRIu32 ")",
+ aPromiseId);
+ CallOnMessageLoopThread("gmp::ChromiumCDMChild::OnResolvePromise",
+ &ChromiumCDMChild::SendOnResolvePromise, aPromiseId);
+}
+
+void ChromiumCDMChild::OnRejectPromise(uint32_t aPromiseId,
+ cdm::Exception aException,
+ uint32_t aSystemCode,
+ const char* aErrorMessage,
+ uint32_t aErrorMessageSize) {
+ GMP_LOG_DEBUG("ChromiumCDMChild::OnRejectPromise(pid=%" PRIu32
+ ", err=%" PRIu32 " code=%" PRIu32 ", msg='%s')",
+ aPromiseId, aException, aSystemCode, aErrorMessage);
+ CallOnMessageLoopThread("gmp::ChromiumCDMChild::OnRejectPromise",
+ &ChromiumCDMChild::SendOnRejectPromise, aPromiseId,
+ static_cast<uint32_t>(aException), aSystemCode,
+ nsCString(aErrorMessage, aErrorMessageSize));
+}
+
+void ChromiumCDMChild::OnSessionMessage(const char* aSessionId,
+ uint32_t aSessionIdSize,
+ cdm::MessageType aMessageType,
+ const char* aMessage,
+ uint32_t aMessageSize) {
+ GMP_LOG_DEBUG("ChromiumCDMChild::OnSessionMessage(sid=%s, type=%" PRIu32
+ " size=%" PRIu32 ")",
+ aSessionId, aMessageType, aMessageSize);
+ CopyableTArray<uint8_t> message;
+ message.AppendElements(aMessage, aMessageSize);
+ CallOnMessageLoopThread("gmp::ChromiumCDMChild::OnSessionMessage",
+ &ChromiumCDMChild::SendOnSessionMessage,
+ nsCString(aSessionId, aSessionIdSize),
+ static_cast<uint32_t>(aMessageType), message);
+}
+
+static auto ToString(const cdm::KeyInformation* aKeysInfo,
+ uint32_t aKeysInfoCount) {
+ return StringJoin(","_ns, Span{aKeysInfo, aKeysInfoCount},
+ [](auto& str, const cdm::KeyInformation& key) {
+ str.Append(ToHexString(key.key_id, key.key_id_size));
+ str.AppendLiteral("=");
+ str.AppendInt(key.status);
+ });
+}
+
+void ChromiumCDMChild::OnSessionKeysChange(const char* aSessionId,
+ uint32_t aSessionIdSize,
+ bool aHasAdditionalUsableKey,
+ const cdm::KeyInformation* aKeysInfo,
+ uint32_t aKeysInfoCount) {
+ GMP_LOG_DEBUG("ChromiumCDMChild::OnSessionKeysChange(sid=%s) keys={%s}",
+ aSessionId, ToString(aKeysInfo, aKeysInfoCount).get());
+
+ CopyableTArray<CDMKeyInformation> keys;
+ keys.SetCapacity(aKeysInfoCount);
+ for (uint32_t i = 0; i < aKeysInfoCount; i++) {
+ const cdm::KeyInformation& key = aKeysInfo[i];
+ nsTArray<uint8_t> kid;
+ kid.AppendElements(key.key_id, key.key_id_size);
+ keys.AppendElement(CDMKeyInformation(kid, key.status, key.system_code));
+ }
+ CallOnMessageLoopThread("gmp::ChromiumCDMChild::OnSessionMessage",
+ &ChromiumCDMChild::SendOnSessionKeysChange,
+ nsCString(aSessionId, aSessionIdSize), keys);
+}
+
+void ChromiumCDMChild::OnExpirationChange(const char* aSessionId,
+ uint32_t aSessionIdSize,
+ cdm::Time aNewExpiryTime) {
+ GMP_LOG_DEBUG("ChromiumCDMChild::OnExpirationChange(sid=%s, time=%lf)",
+ aSessionId, aNewExpiryTime);
+ CallOnMessageLoopThread("gmp::ChromiumCDMChild::OnExpirationChange",
+ &ChromiumCDMChild::SendOnExpirationChange,
+ nsCString(aSessionId, aSessionIdSize),
+ aNewExpiryTime);
+}
+
+void ChromiumCDMChild::OnSessionClosed(const char* aSessionId,
+ uint32_t aSessionIdSize) {
+ GMP_LOG_DEBUG("ChromiumCDMChild::OnSessionClosed(sid=%s)", aSessionId);
+ CallOnMessageLoopThread("gmp::ChromiumCDMChild::OnSessionClosed",
+ &ChromiumCDMChild::SendOnSessionClosed,
+ nsCString(aSessionId, aSessionIdSize));
+}
+
+void ChromiumCDMChild::QueryOutputProtectionStatus() {
+ GMP_LOG_DEBUG("ChromiumCDMChild::QueryOutputProtectionStatus()");
+ // We'll handle the response in `CompleteQueryOutputProtectionStatus`.
+ CallOnMessageLoopThread("gmp::ChromiumCDMChild::QueryOutputProtectionStatus",
+ &ChromiumCDMChild::SendOnQueryOutputProtectionStatus);
+}
+
+void ChromiumCDMChild::OnInitialized(bool aSuccess) {
+ MOZ_ASSERT(!mInitPromise.IsEmpty(),
+ "mInitPromise should exist during init callback!");
+ if (!aSuccess) {
+ mInitPromise.RejectIfExists(NS_ERROR_FAILURE, __func__);
+ }
+ mInitPromise.ResolveIfExists(true, __func__);
+}
+
+cdm::FileIO* ChromiumCDMChild::CreateFileIO(cdm::FileIOClient* aClient) {
+ MOZ_ASSERT(IsOnMessageLoopThread());
+ GMP_LOG_DEBUG("ChromiumCDMChild::CreateFileIO()");
+ if (!mPersistentStateAllowed) {
+ return nullptr;
+ }
+ return new WidevineFileIO(aClient);
+}
+
+void ChromiumCDMChild::RequestStorageId(uint32_t aVersion) {
+ MOZ_ASSERT(IsOnMessageLoopThread());
+ GMP_LOG_DEBUG("ChromiumCDMChild::RequestStorageId() aVersion = %u", aVersion);
+ // aVersion >= 0x80000000 are reserved.
+ if (aVersion >= 0x80000000) {
+ mCDM->OnStorageId(aVersion, nullptr, 0);
+ return;
+ }
+ if (aVersion > CDMStorageIdProvider::kCurrentVersion) {
+ mCDM->OnStorageId(aVersion, nullptr, 0);
+ return;
+ }
+
+ mCDM->OnStorageId(CDMStorageIdProvider::kCurrentVersion,
+ !mStorageId.IsEmpty()
+ ? reinterpret_cast<const uint8_t*>(mStorageId.get())
+ : nullptr,
+ mStorageId.Length());
+}
+
+ChromiumCDMChild::~ChromiumCDMChild() {
+ GMP_LOG_DEBUG("ChromiumCDMChild:: dtor this=%p", this);
+}
+
+bool ChromiumCDMChild::IsOnMessageLoopThread() {
+ return mPlugin && mPlugin->GMPMessageLoop() == MessageLoop::current();
+}
+
+void ChromiumCDMChild::ActorDestroy(ActorDestroyReason aReason) {
+ mPlugin = nullptr;
+}
+
+void ChromiumCDMChild::PurgeShmems() {
+ for (ipc::Shmem& shmem : mBuffers) {
+ DeallocShmem(shmem);
+ }
+ mBuffers.Clear();
+}
+
+ipc::IPCResult ChromiumCDMChild::RecvPurgeShmems() {
+ PurgeShmems();
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ChromiumCDMChild::RecvInit(
+ const bool& aAllowDistinctiveIdentifier, const bool& aAllowPersistentState,
+ InitResolver&& aResolver) {
+ MOZ_ASSERT(IsOnMessageLoopThread());
+ GMP_LOG_DEBUG(
+ "ChromiumCDMChild::RecvInit(distinctiveId=%s, persistentState=%s)",
+ aAllowDistinctiveIdentifier ? "true" : "false",
+ aAllowPersistentState ? "true" : "false");
+ mPersistentStateAllowed = aAllowPersistentState;
+
+ RefPtr<ChromiumCDMChild::InitPromise> promise = mInitPromise.Ensure(__func__);
+ promise->Then(
+ mPlugin->GMPMessageLoop()->SerialEventTarget(), __func__,
+ [aResolver](bool /* unused */) { aResolver(true); },
+ [aResolver](nsresult rv) {
+ GMP_LOG_DEBUG(
+ "ChromiumCDMChild::RecvInit() init promise rejected with "
+ "rv=%" PRIu32,
+ static_cast<uint32_t>(rv));
+ aResolver(false);
+ });
+
+ if (mCDM) {
+ // Once the CDM is initialized we expect it to resolve mInitPromise via
+ // ChromiumCDMChild::OnInitialized
+ mCDM->Initialize(aAllowDistinctiveIdentifier, aAllowPersistentState,
+ // We do not yet support hardware secure codecs
+ false);
+ } else {
+ GMP_LOG_DEBUG(
+ "ChromiumCDMChild::RecvInit() mCDM not set! Is GMP shutting down?");
+ mInitPromise.RejectIfExists(NS_ERROR_FAILURE, __func__);
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ChromiumCDMChild::RecvSetServerCertificate(
+ const uint32_t& aPromiseId, nsTArray<uint8_t>&& aServerCert)
+
+{
+ MOZ_ASSERT(IsOnMessageLoopThread());
+ GMP_LOG_DEBUG("ChromiumCDMChild::RecvSetServerCertificate() certlen=%zu",
+ aServerCert.Length());
+ if (mCDM) {
+ mCDM->SetServerCertificate(aPromiseId, aServerCert.Elements(),
+ aServerCert.Length());
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ChromiumCDMChild::RecvCreateSessionAndGenerateRequest(
+ const uint32_t& aPromiseId, const uint32_t& aSessionType,
+ const uint32_t& aInitDataType, nsTArray<uint8_t>&& aInitData) {
+ MOZ_ASSERT(IsOnMessageLoopThread());
+ GMP_LOG_DEBUG(
+ "ChromiumCDMChild::RecvCreateSessionAndGenerateRequest("
+ "pid=%" PRIu32 ", sessionType=%" PRIu32 ", initDataType=%" PRIu32
+ ") initDataLen=%zu",
+ aPromiseId, aSessionType, aInitDataType, aInitData.Length());
+ MOZ_ASSERT(aSessionType <= cdm::SessionType::kPersistentUsageRecord);
+ MOZ_ASSERT(aInitDataType <= cdm::InitDataType::kWebM);
+ if (mCDM) {
+ mCDM->CreateSessionAndGenerateRequest(
+ aPromiseId, static_cast<cdm::SessionType>(aSessionType),
+ static_cast<cdm::InitDataType>(aInitDataType), aInitData.Elements(),
+ aInitData.Length());
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ChromiumCDMChild::RecvLoadSession(
+ const uint32_t& aPromiseId, const uint32_t& aSessionType,
+ const nsACString& aSessionId) {
+ MOZ_ASSERT(IsOnMessageLoopThread());
+ GMP_LOG_DEBUG(
+ "ChromiumCDMChild::RecvLoadSession(pid=%u, type=%u, sessionId=%s)",
+ aPromiseId, aSessionType, PromiseFlatCString(aSessionId).get());
+ if (mCDM) {
+ mLoadSessionPromiseIds.AppendElement(aPromiseId);
+ mCDM->LoadSession(aPromiseId, static_cast<cdm::SessionType>(aSessionType),
+ aSessionId.BeginReading(), aSessionId.Length());
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ChromiumCDMChild::RecvUpdateSession(
+ const uint32_t& aPromiseId, const nsACString& aSessionId,
+ nsTArray<uint8_t>&& aResponse) {
+ MOZ_ASSERT(IsOnMessageLoopThread());
+ GMP_LOG_DEBUG("ChromiumCDMChild::RecvUpdateSession(pid=%" PRIu32
+ ", sid=%s) responseLen=%zu",
+ aPromiseId, PromiseFlatCString(aSessionId).get(),
+ aResponse.Length());
+ if (mCDM) {
+ mCDM->UpdateSession(aPromiseId, aSessionId.BeginReading(),
+ aSessionId.Length(), aResponse.Elements(),
+ aResponse.Length());
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ChromiumCDMChild::RecvCloseSession(
+ const uint32_t& aPromiseId, const nsACString& aSessionId) {
+ MOZ_ASSERT(IsOnMessageLoopThread());
+ GMP_LOG_DEBUG("ChromiumCDMChild::RecvCloseSession(pid=%" PRIu32 ", sid=%s)",
+ aPromiseId, PromiseFlatCString(aSessionId).get());
+ if (mCDM) {
+ mCDM->CloseSession(aPromiseId, aSessionId.BeginReading(),
+ aSessionId.Length());
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ChromiumCDMChild::RecvRemoveSession(
+ const uint32_t& aPromiseId, const nsACString& aSessionId) {
+ MOZ_ASSERT(IsOnMessageLoopThread());
+ GMP_LOG_DEBUG("ChromiumCDMChild::RecvRemoveSession(pid=%" PRIu32 ", sid=%s)",
+ aPromiseId, PromiseFlatCString(aSessionId).get());
+ if (mCDM) {
+ mCDM->RemoveSession(aPromiseId, aSessionId.BeginReading(),
+ aSessionId.Length());
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
+ChromiumCDMChild::RecvCompleteQueryOutputProtectionStatus(
+ const bool& aSuccess, const uint32_t& aLinkMask,
+ const uint32_t& aProtectionMask) {
+ MOZ_ASSERT(IsOnMessageLoopThread());
+ GMP_LOG_DEBUG(
+ "ChromiumCDMChild::RecvCompleteQueryOutputProtectionStatus(aSuccess=%s, "
+ "aLinkMask=%" PRIu32 ", aProtectionMask=%" PRIu32 ")",
+ aSuccess ? "true" : "false", aLinkMask, aProtectionMask);
+
+ if (mCDM) {
+ cdm::QueryResult queryResult = aSuccess ? cdm::QueryResult::kQuerySucceeded
+ : cdm::QueryResult::kQueryFailed;
+ mCDM->OnQueryOutputProtectionStatus(queryResult, aLinkMask,
+ aProtectionMask);
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ChromiumCDMChild::RecvGetStatusForPolicy(
+ const uint32_t& aPromiseId, const cdm::HdcpVersion& aMinHdcpVersion) {
+ MOZ_ASSERT(IsOnMessageLoopThread());
+ GMP_LOG_DEBUG("ChromiumCDMChild::RecvGetStatusForPolicy(pid=%" PRIu32
+ ", MinHdcpVersion=%" PRIu32 ")",
+ aPromiseId, static_cast<uint32_t>(aMinHdcpVersion));
+ if (mCDM) {
+ cdm::Policy policy;
+ policy.min_hdcp_version = aMinHdcpVersion;
+ mCDM->GetStatusForPolicy(aPromiseId, policy);
+ }
+ return IPC_OK();
+}
+
+static void InitInputBuffer(const CDMInputBuffer& aBuffer,
+ nsTArray<cdm::SubsampleEntry>& aSubSamples,
+ cdm::InputBuffer_2& aInputBuffer) {
+ aInputBuffer.data = aBuffer.mData().get<uint8_t>();
+ aInputBuffer.data_size = aBuffer.mData().Size<uint8_t>();
+
+ if (aBuffer.mEncryptionScheme() != cdm::EncryptionScheme::kUnencrypted) {
+ MOZ_ASSERT(aBuffer.mEncryptionScheme() == cdm::EncryptionScheme::kCenc ||
+ aBuffer.mEncryptionScheme() == cdm::EncryptionScheme::kCbcs);
+ aInputBuffer.key_id = aBuffer.mKeyId().Elements();
+ aInputBuffer.key_id_size = aBuffer.mKeyId().Length();
+
+ aInputBuffer.iv = aBuffer.mIV().Elements();
+ aInputBuffer.iv_size = aBuffer.mIV().Length();
+
+ aSubSamples.SetCapacity(aBuffer.mClearBytes().Length());
+ for (size_t i = 0; i < aBuffer.mCipherBytes().Length(); i++) {
+ aSubSamples.AppendElement(cdm::SubsampleEntry{aBuffer.mClearBytes()[i],
+ aBuffer.mCipherBytes()[i]});
+ }
+ aInputBuffer.subsamples = aSubSamples.Elements();
+ aInputBuffer.num_subsamples = aSubSamples.Length();
+ aInputBuffer.encryption_scheme = aBuffer.mEncryptionScheme();
+ }
+ aInputBuffer.pattern.crypt_byte_block = aBuffer.mCryptByteBlock();
+ aInputBuffer.pattern.skip_byte_block = aBuffer.mSkipByteBlock();
+ aInputBuffer.timestamp = aBuffer.mTimestamp();
+}
+
+bool ChromiumCDMChild::HasShmemOfSize(size_t aSize) const {
+ for (const ipc::Shmem& shmem : mBuffers) {
+ if (shmem.Size<uint8_t>() == aSize) {
+ return true;
+ }
+ }
+ return false;
+}
+
+mozilla::ipc::IPCResult ChromiumCDMChild::RecvDecrypt(
+ const uint32_t& aId, const CDMInputBuffer& aBuffer) {
+ MOZ_ASSERT(IsOnMessageLoopThread());
+ GMP_LOG_DEBUG("ChromiumCDMChild::RecvDecrypt()");
+
+ // Parent should have already gifted us a shmem to use as output.
+ size_t outputShmemSize = aBuffer.mData().Size<uint8_t>();
+ MOZ_ASSERT(HasShmemOfSize(outputShmemSize));
+
+ // Ensure we deallocate the shmem used to send input.
+ RefPtr<ChromiumCDMChild> self = this;
+ auto autoDeallocateInputShmem =
+ MakeScopeExit([&, self] { self->DeallocShmem(aBuffer.mData()); });
+
+ // On failure, we need to ensure that the shmem that the parent sent
+ // for the CDM to use to return output back to the parent is deallocated.
+ // Otherwise, it will leak.
+ auto autoDeallocateOutputShmem = MakeScopeExit([self, outputShmemSize] {
+ self->mBuffers.RemoveElementsBy(
+ [outputShmemSize, self](ipc::Shmem& aShmem) {
+ if (aShmem.Size<uint8_t>() != outputShmemSize) {
+ return false;
+ }
+ self->DeallocShmem(aShmem);
+ return true;
+ });
+ });
+
+ if (!mCDM) {
+ GMP_LOG_DEBUG("ChromiumCDMChild::RecvDecrypt() no CDM");
+ Unused << SendDecryptFailed(aId, cdm::kDecryptError);
+ return IPC_OK();
+ }
+ if (aBuffer.mClearBytes().Length() != aBuffer.mCipherBytes().Length()) {
+ GMP_LOG_DEBUG(
+ "ChromiumCDMChild::RecvDecrypt() clear/cipher bytes length doesn't "
+ "match");
+ Unused << SendDecryptFailed(aId, cdm::kDecryptError);
+ return IPC_OK();
+ }
+
+ cdm::InputBuffer_2 input = {};
+ nsTArray<cdm::SubsampleEntry> subsamples;
+ InitInputBuffer(aBuffer, subsamples, input);
+
+ WidevineDecryptedBlock output;
+ cdm::Status status = mCDM->Decrypt(input, &output);
+
+ // CDM should have allocated a cdm::Buffer for output.
+ CDMShmemBuffer* buffer =
+ output.DecryptedBuffer()
+ ? static_cast<CDMShmemBuffer*>(output.DecryptedBuffer())
+ : nullptr;
+ MOZ_ASSERT_IF(buffer, buffer->AsShmemBuffer());
+ if (status != cdm::kSuccess || !buffer) {
+ Unused << SendDecryptFailed(aId, status);
+ return IPC_OK();
+ }
+
+ // Success! Return the decrypted sample to parent.
+ MOZ_ASSERT(!HasShmemOfSize(outputShmemSize));
+ ipc::Shmem shmem = buffer->ExtractShmem();
+ if (SendDecrypted(aId, cdm::kSuccess, std::move(shmem))) {
+ // No need to deallocate the output shmem; it should have been returned
+ // to the content process.
+ autoDeallocateOutputShmem.release();
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ChromiumCDMChild::RecvInitializeVideoDecoder(
+ const CDMVideoDecoderConfig& aConfig) {
+ MOZ_ASSERT(IsOnMessageLoopThread());
+ MOZ_ASSERT(!mDecoderInitialized);
+ if (!mCDM) {
+ GMP_LOG_DEBUG("ChromiumCDMChild::RecvInitializeVideoDecoder() no CDM");
+ Unused << SendOnDecoderInitDone(cdm::kInitializationError);
+ return IPC_OK();
+ }
+ cdm::VideoDecoderConfig_2 config = {};
+ config.codec = static_cast<cdm::VideoCodec>(aConfig.mCodec());
+ config.profile = static_cast<cdm::VideoCodecProfile>(aConfig.mProfile());
+ config.format = static_cast<cdm::VideoFormat>(aConfig.mFormat());
+ config.coded_size =
+ mCodedSize = {aConfig.mImageWidth(), aConfig.mImageHeight()};
+ nsTArray<uint8_t> extraData(aConfig.mExtraData().Clone());
+ config.extra_data = extraData.Elements();
+ config.extra_data_size = extraData.Length();
+ config.encryption_scheme = aConfig.mEncryptionScheme();
+ cdm::Status status = mCDM->InitializeVideoDecoder(config);
+ GMP_LOG_DEBUG("ChromiumCDMChild::RecvInitializeVideoDecoder() status=%u",
+ status);
+ Unused << SendOnDecoderInitDone(status);
+ mDecoderInitialized = status == cdm::kSuccess;
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ChromiumCDMChild::RecvDeinitializeVideoDecoder() {
+ MOZ_ASSERT(IsOnMessageLoopThread());
+ GMP_LOG_DEBUG("ChromiumCDMChild::RecvDeinitializeVideoDecoder()");
+ MOZ_ASSERT(mDecoderInitialized);
+ if (mDecoderInitialized && mCDM) {
+ mCDM->DeinitializeDecoder(cdm::kStreamTypeVideo);
+ }
+ mDecoderInitialized = false;
+ PurgeShmems();
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ChromiumCDMChild::RecvResetVideoDecoder() {
+ MOZ_ASSERT(IsOnMessageLoopThread());
+ GMP_LOG_DEBUG("ChromiumCDMChild::RecvResetVideoDecoder()");
+ if (mDecoderInitialized && mCDM) {
+ mCDM->ResetDecoder(cdm::kStreamTypeVideo);
+ }
+ Unused << SendResetVideoDecoderComplete();
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ChromiumCDMChild::RecvDecryptAndDecodeFrame(
+ const CDMInputBuffer& aBuffer) {
+ MOZ_ASSERT(IsOnMessageLoopThread());
+ GMP_LOG_DEBUG("ChromiumCDMChild::RecvDecryptAndDecodeFrame() t=%" PRId64 ")",
+ aBuffer.mTimestamp());
+ MOZ_ASSERT(mDecoderInitialized);
+
+ if (!mCDM) {
+ GMP_LOG_DEBUG("ChromiumCDMChild::RecvDecryptAndDecodeFrame() no CDM");
+ Unused << SendDecodeFailed(cdm::kDecodeError);
+ return IPC_OK();
+ }
+
+ RefPtr<ChromiumCDMChild> self = this;
+ auto autoDeallocateShmem =
+ MakeScopeExit([&, self] { self->DeallocShmem(aBuffer.mData()); });
+
+ // The output frame may not have the same timestamp as the frame we put in.
+ // We may need to input a number of frames before we receive output. The
+ // CDM's decoder reorders to ensure frames output are in presentation order.
+ // So we need to store the durations of the frames input, and retrieve them
+ // on output.
+ mFrameDurations.Insert(aBuffer.mTimestamp(), aBuffer.mDuration());
+
+ cdm::InputBuffer_2 input = {};
+ nsTArray<cdm::SubsampleEntry> subsamples;
+ InitInputBuffer(aBuffer, subsamples, input);
+
+ WidevineVideoFrame frame;
+ cdm::Status rv = mCDM->DecryptAndDecodeFrame(input, &frame);
+ GMP_LOG_DEBUG("ChromiumCDMChild::RecvDecryptAndDecodeFrame() t=%" PRId64
+ " CDM decoder rv=%d",
+ aBuffer.mTimestamp(), rv);
+
+ switch (rv) {
+ case cdm::kNeedMoreData:
+ Unused << SendDecodeFailed(rv);
+ break;
+ case cdm::kNoKey:
+ GMP_LOG_DEBUG("NoKey for sample at time=%" PRId64 "!", input.timestamp);
+ // Somehow our key became unusable. Typically this would happen when
+ // a stream requires output protection, and the configuration changed
+ // such that output protection is no longer available. For example, a
+ // non-compliant monitor was attached. The JS player should notice the
+ // key status changing to "output-restricted", and is supposed to switch
+ // to a stream that doesn't require OP. In order to keep the playback
+ // pipeline rolling, just output a black frame. See bug 1343140.
+ if (!frame.InitToBlack(mCodedSize.width, mCodedSize.height,
+ input.timestamp)) {
+ Unused << SendDecodeFailed(cdm::kDecodeError);
+ break;
+ }
+ [[fallthrough]];
+ case cdm::kSuccess:
+ if (frame.FrameBuffer()) {
+ ReturnOutput(frame);
+ break;
+ }
+ // CDM didn't set a frame buffer on the sample, report it as an error.
+ [[fallthrough]];
+ default:
+ Unused << SendDecodeFailed(rv);
+ break;
+ }
+
+ return IPC_OK();
+}
+
+void ChromiumCDMChild::ReturnOutput(WidevineVideoFrame& aFrame) {
+ MOZ_ASSERT(IsOnMessageLoopThread());
+ MOZ_ASSERT(aFrame.FrameBuffer());
+ gmp::CDMVideoFrame output;
+ output.mFormat() = static_cast<cdm::VideoFormat>(aFrame.Format());
+ output.mImageWidth() = aFrame.Size().width;
+ output.mImageHeight() = aFrame.Size().height;
+ output.mYPlane() = {aFrame.PlaneOffset(cdm::VideoPlane::kYPlane),
+ aFrame.Stride(cdm::VideoPlane::kYPlane)};
+ output.mUPlane() = {aFrame.PlaneOffset(cdm::VideoPlane::kUPlane),
+ aFrame.Stride(cdm::VideoPlane::kUPlane)};
+ output.mVPlane() = {aFrame.PlaneOffset(cdm::VideoPlane::kVPlane),
+ aFrame.Stride(cdm::VideoPlane::kVPlane)};
+ output.mTimestamp() = aFrame.Timestamp();
+
+ uint64_t duration = 0;
+ if (mFrameDurations.Find(aFrame.Timestamp(), duration)) {
+ output.mDuration() = duration;
+ }
+
+ CDMBuffer* base = reinterpret_cast<CDMBuffer*>(aFrame.FrameBuffer());
+ if (base->AsShmemBuffer()) {
+ ipc::Shmem shmem = base->AsShmemBuffer()->ExtractShmem();
+ Unused << SendDecodedShmem(output, std::move(shmem));
+ } else {
+ MOZ_ASSERT(base->AsArrayBuffer());
+ Unused << SendDecodedData(output, base->AsArrayBuffer()->ExtractBuffer());
+ }
+}
+
+mozilla::ipc::IPCResult ChromiumCDMChild::RecvDrain() {
+ MOZ_ASSERT(IsOnMessageLoopThread());
+ if (!mCDM) {
+ GMP_LOG_DEBUG("ChromiumCDMChild::RecvDrain() no CDM");
+ Unused << SendDrainComplete();
+ return IPC_OK();
+ }
+ WidevineVideoFrame frame;
+ cdm::InputBuffer_2 sample = {};
+ cdm::Status rv = mCDM->DecryptAndDecodeFrame(sample, &frame);
+ GMP_LOG_DEBUG("ChromiumCDMChild::RecvDrain(); DecryptAndDecodeFrame() rv=%d",
+ rv);
+ if (rv == cdm::kSuccess) {
+ MOZ_ASSERT(frame.Format() != cdm::kUnknownVideoFormat);
+ ReturnOutput(frame);
+ } else {
+ Unused << SendDrainComplete();
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ChromiumCDMChild::RecvDestroy() {
+ MOZ_ASSERT(IsOnMessageLoopThread());
+ GMP_LOG_DEBUG("ChromiumCDMChild::RecvDestroy()");
+
+ MOZ_ASSERT(!mDecoderInitialized);
+
+ mInitPromise.RejectIfExists(NS_ERROR_ABORT, __func__);
+
+ if (mCDM) {
+ mCDM->Destroy();
+ mCDM = nullptr;
+ }
+ mDestroyed = true;
+
+ Unused << Send__delete__(this);
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ChromiumCDMChild::RecvGiveBuffer(ipc::Shmem&& aBuffer) {
+ MOZ_ASSERT(IsOnMessageLoopThread());
+
+ GiveBuffer(std::move(aBuffer));
+ return IPC_OK();
+}
+
+void ChromiumCDMChild::GiveBuffer(ipc::Shmem&& aBuffer) {
+ MOZ_ASSERT(IsOnMessageLoopThread());
+ size_t sz = aBuffer.Size<uint8_t>();
+ mBuffers.AppendElement(std::move(aBuffer));
+ GMP_LOG_DEBUG(
+ "ChromiumCDMChild::RecvGiveBuffer(capacity=%zu"
+ ") bufferSizes={%s} mDecoderInitialized=%d",
+ sz, ToString(mBuffers).get(), mDecoderInitialized);
+}
+
+} // namespace mozilla::gmp
diff --git a/dom/media/gmp/ChromiumCDMChild.h b/dom/media/gmp/ChromiumCDMChild.h
new file mode 100644
index 0000000000..1bf153a5d5
--- /dev/null
+++ b/dom/media/gmp/ChromiumCDMChild.h
@@ -0,0 +1,148 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef ChromiumCDMChild_h_
+#define ChromiumCDMChild_h_
+
+#include "content_decryption_module.h"
+#include "mozilla/gmp/PChromiumCDMChild.h"
+#include "SimpleMap.h"
+#include "WidevineVideoFrame.h"
+
+namespace mozilla::gmp {
+
+class GMPContentChild;
+
+class ChromiumCDMChild : public PChromiumCDMChild, public cdm::Host_10 {
+ public:
+ // Mark AddRef and Release as `final`, as they overload pure virtual
+ // implementations in PChromiumCDMChild.
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ChromiumCDMChild, final);
+
+ explicit ChromiumCDMChild(GMPContentChild* aPlugin);
+
+ void Init(cdm::ContentDecryptionModule_10* aCDM,
+ const nsACString& aStorageId);
+
+ void TimerExpired(void* aContext);
+
+ // cdm::Host_10 implementation
+ cdm::Buffer* Allocate(uint32_t aCapacity) override;
+ void SetTimer(int64_t aDelayMs, void* aContext) override;
+ cdm::Time GetCurrentWallTime() override;
+ void OnResolveKeyStatusPromise(uint32_t aPromiseId,
+ cdm::KeyStatus aKeyStatus) override;
+ void OnResolveNewSessionPromise(uint32_t aPromiseId, const char* aSessionId,
+ uint32_t aSessionIdSize) override;
+ void OnResolvePromise(uint32_t aPromiseId) override;
+ void OnRejectPromise(uint32_t aPromiseId, cdm::Exception aException,
+ uint32_t aSystemCode, const char* aErrorMessage,
+ uint32_t aErrorMessageSize) override;
+ void OnSessionMessage(const char* aSessionId, uint32_t aSessionIdSize,
+ cdm::MessageType aMessageType, const char* aMessage,
+ uint32_t aMessageSize) override;
+ void OnSessionKeysChange(const char* aSessionId, uint32_t aSessionIdSize,
+ bool aHasAdditionalUsableKey,
+ const cdm::KeyInformation* aKeysInfo,
+ uint32_t aKeysInfoCount) override;
+ void OnExpirationChange(const char* aSessionId, uint32_t aSessionIdSize,
+ cdm::Time aNewExpiryTime) override;
+ void OnSessionClosed(const char* aSessionId,
+ uint32_t aSessionIdSize) override;
+ void SendPlatformChallenge(const char* aServiceId, uint32_t aServiceIdSize,
+ const char* aChallenge,
+ uint32_t aChallengeSize) override {}
+ void EnableOutputProtection(uint32_t aDesiredProtectionMask) override {}
+ void QueryOutputProtectionStatus() override;
+ void OnDeferredInitializationDone(cdm::StreamType aStreamType,
+ cdm::Status aDecoderStatus) override {}
+ void RequestStorageId(uint32_t aVersion) override;
+ cdm::FileIO* CreateFileIO(cdm::FileIOClient* aClient) override;
+ void OnInitialized(bool aSuccess) override;
+ // end cdm::Host_10 specific methods
+
+ void GiveBuffer(ipc::Shmem&& aBuffer);
+
+ protected:
+ ~ChromiumCDMChild();
+
+ bool OnResolveNewSessionPromiseInternal(uint32_t aPromiseId,
+ const nsACString& aSessionId);
+
+ bool IsOnMessageLoopThread();
+
+ void ActorDestroy(ActorDestroyReason aReason) override;
+
+ ipc::IPCResult RecvGiveBuffer(ipc::Shmem&& aShmem) override;
+ ipc::IPCResult RecvPurgeShmems() override;
+ void PurgeShmems();
+ ipc::IPCResult RecvInit(const bool& aAllowDistinctiveIdentifier,
+ const bool& aAllowPersistentState,
+ InitResolver&& aResolver) override;
+ ipc::IPCResult RecvSetServerCertificate(
+ const uint32_t& aPromiseId, nsTArray<uint8_t>&& aServerCert) override;
+ ipc::IPCResult RecvCreateSessionAndGenerateRequest(
+ const uint32_t& aPromiseId, const uint32_t& aSessionType,
+ const uint32_t& aInitDataType, nsTArray<uint8_t>&& aInitData) override;
+ ipc::IPCResult RecvLoadSession(const uint32_t& aPromiseId,
+ const uint32_t& aSessionType,
+ const nsACString& aSessionId) override;
+ ipc::IPCResult RecvUpdateSession(const uint32_t& aPromiseId,
+ const nsACString& aSessionId,
+ nsTArray<uint8_t>&& aResponse) override;
+ ipc::IPCResult RecvCloseSession(const uint32_t& aPromiseId,
+ const nsACString& aSessionId) override;
+ ipc::IPCResult RecvRemoveSession(const uint32_t& aPromiseId,
+ const nsACString& aSessionId) override;
+ ipc::IPCResult RecvCompleteQueryOutputProtectionStatus(
+ const bool& aSuccess, const uint32_t& aLinkMask,
+ const uint32_t& aProtectionMask) override;
+ ipc::IPCResult RecvGetStatusForPolicy(
+ const uint32_t& aPromiseId,
+ const cdm::HdcpVersion& aMinHdcpVersion) override;
+ ipc::IPCResult RecvDecrypt(const uint32_t& aId,
+ const CDMInputBuffer& aBuffer) override;
+ ipc::IPCResult RecvInitializeVideoDecoder(
+ const CDMVideoDecoderConfig& aConfig) override;
+ ipc::IPCResult RecvDeinitializeVideoDecoder() override;
+ ipc::IPCResult RecvResetVideoDecoder() override;
+ ipc::IPCResult RecvDecryptAndDecodeFrame(
+ const CDMInputBuffer& aBuffer) override;
+ ipc::IPCResult RecvDrain() override;
+ ipc::IPCResult RecvDestroy() override;
+
+ void ReturnOutput(WidevineVideoFrame& aFrame);
+ bool HasShmemOfSize(size_t aSize) const;
+
+ template <typename MethodType, typename... ParamType>
+ void CallMethod(MethodType, ParamType&&...);
+
+ template <typename MethodType, typename... ParamType>
+ void CallOnMessageLoopThread(const char* const, MethodType, ParamType&&...);
+
+ GMPContentChild* mPlugin = nullptr;
+ cdm::ContentDecryptionModule_10* mCDM = nullptr;
+
+ typedef SimpleMap<uint64_t> DurationMap;
+ DurationMap mFrameDurations;
+ nsTArray<uint32_t> mLoadSessionPromiseIds;
+
+ cdm::Size mCodedSize = {0, 0};
+ nsTArray<ipc::Shmem> mBuffers;
+
+ bool mDecoderInitialized = false;
+ bool mPersistentStateAllowed = false;
+ bool mDestroyed = false;
+ nsCString mStorageId;
+
+ typedef MozPromise<bool, nsresult, /* IsExclusive = */ true> InitPromise;
+ // Created when we RecvInit, should be resolved once the CDM is initialized
+ // or rejected if init fails.
+ MozPromiseHolder<InitPromise> mInitPromise;
+};
+
+} // namespace mozilla::gmp
+
+#endif // ChromiumCDMChild_h_
diff --git a/dom/media/gmp/ChromiumCDMParent.cpp b/dom/media/gmp/ChromiumCDMParent.cpp
new file mode 100644
index 0000000000..ddb1e38d33
--- /dev/null
+++ b/dom/media/gmp/ChromiumCDMParent.cpp
@@ -0,0 +1,1319 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ChromiumCDMParent.h"
+
+#include "ChromiumCDMCallback.h"
+#include "ChromiumCDMCallbackProxy.h"
+#include "ChromiumCDMProxy.h"
+#include "content_decryption_module.h"
+#include "GMPContentChild.h"
+#include "GMPContentParent.h"
+#include "GMPLog.h"
+#include "GMPService.h"
+#include "GMPUtils.h"
+#include "VideoUtils.h"
+#include "mozilla/dom/MediaKeyMessageEventBinding.h"
+#include "mozilla/gmp/GMPTypes.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/Unused.h"
+#include "AnnexB.h"
+#include "H264.h"
+
+#define NS_DispatchToMainThread(...) CompileError_UseAbstractMainThreadInstead
+
+namespace mozilla::gmp {
+
+using namespace eme;
+
+ChromiumCDMParent::ChromiumCDMParent(GMPContentParent* aContentParent,
+ uint32_t aPluginId)
+ : mPluginId(aPluginId),
+ mContentParent(aContentParent),
+ mVideoShmemLimit(StaticPrefs::media_eme_chromium_api_video_shmems())
+#ifdef DEBUG
+ ,
+ mGMPThread(aContentParent->GMPEventTarget())
+#endif
+{
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG(
+ "ChromiumCDMParent::ChromiumCDMParent(this=%p, contentParent=%p, "
+ "id=%" PRIu32 ")",
+ this, aContentParent, aPluginId);
+}
+
+RefPtr<ChromiumCDMParent::InitPromise> ChromiumCDMParent::Init(
+ ChromiumCDMCallback* aCDMCallback, bool aAllowDistinctiveIdentifier,
+ bool aAllowPersistentState, nsIEventTarget* aMainThread) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG(
+ "ChromiumCDMParent::Init(this=%p) shutdown=%s abormalShutdown=%s "
+ "actorDestroyed=%s",
+ this, mIsShutdown ? "true" : "false",
+ mAbnormalShutdown ? "true" : "false", mActorDestroyed ? "true" : "false");
+ if (!aCDMCallback || !aMainThread) {
+ GMP_LOG_DEBUG(
+ "ChromiumCDMParent::Init(this=%p) failed "
+ "nullCallback=%s nullMainThread=%s",
+ this, !aCDMCallback ? "true" : "false",
+ !aMainThread ? "true" : "false");
+
+ return ChromiumCDMParent::InitPromise::CreateAndReject(
+ MediaResult(NS_ERROR_FAILURE,
+ nsPrintfCString("ChromiumCDMParent::Init() failed "
+ "nullCallback=%s nullMainThread=%s",
+ !aCDMCallback ? "true" : "false",
+ !aMainThread ? "true" : "false")),
+ __func__);
+ }
+
+ RefPtr<ChromiumCDMParent::InitPromise> promise =
+ mInitPromise.Ensure(__func__);
+ RefPtr<ChromiumCDMParent> self = this;
+ SendInit(aAllowDistinctiveIdentifier, aAllowPersistentState)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self, aCDMCallback](bool aSuccess) {
+ if (!aSuccess) {
+ GMP_LOG_DEBUG(
+ "ChromiumCDMParent::Init() failed with callback from "
+ "child indicating CDM failed init");
+ self->mInitPromise.RejectIfExists(
+ MediaResult(NS_ERROR_FAILURE,
+ "ChromiumCDMParent::Init() failed with callback "
+ "from child indicating CDM failed init"),
+ __func__);
+ return;
+ }
+ GMP_LOG_DEBUG(
+ "ChromiumCDMParent::Init() succeeded with callback from child");
+ self->mCDMCallback = aCDMCallback;
+ self->mInitPromise.ResolveIfExists(true /* unused */, __func__);
+ },
+ [self](ResponseRejectReason&& aReason) {
+ RefPtr<gmp::GeckoMediaPluginService> service =
+ gmp::GeckoMediaPluginService::GetGeckoMediaPluginService();
+ bool xpcomWillShutdown =
+ service && service->XPCOMWillShutdownReceived();
+ GMP_LOG_DEBUG(
+ "ChromiumCDMParent::Init(this=%p) failed "
+ "shutdown=%s cdmCrash=%s actorDestroyed=%s "
+ "browserShutdown=%s promiseRejectReason=%d",
+ self.get(), self->mIsShutdown ? "true" : "false",
+ self->mAbnormalShutdown ? "true" : "false",
+ self->mActorDestroyed ? "true" : "false",
+ xpcomWillShutdown ? "true" : "false",
+ static_cast<int>(aReason));
+ self->mInitPromise.RejectIfExists(
+ MediaResult(
+ NS_ERROR_FAILURE,
+ nsPrintfCString("ChromiumCDMParent::Init() failed "
+ "shutdown=%s cdmCrash=%s actorDestroyed=%s "
+ "browserShutdown=%s promiseRejectReason=%d",
+ self->mIsShutdown ? "true" : "false",
+ self->mAbnormalShutdown ? "true" : "false",
+ self->mActorDestroyed ? "true" : "false",
+ xpcomWillShutdown ? "true" : "false",
+ static_cast<int>(aReason))),
+ __func__);
+ });
+ return promise;
+}
+
+void ChromiumCDMParent::CreateSession(uint32_t aCreateSessionToken,
+ uint32_t aSessionType,
+ uint32_t aInitDataType,
+ uint32_t aPromiseId,
+ const nsTArray<uint8_t>& aInitData) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG("ChromiumCDMParent::CreateSession(this=%p)", this);
+ if (mIsShutdown) {
+ RejectPromiseShutdown(aPromiseId);
+ return;
+ }
+ if (!SendCreateSessionAndGenerateRequest(aPromiseId, aSessionType,
+ aInitDataType, aInitData)) {
+ RejectPromiseWithStateError(
+ aPromiseId, "Failed to send generateRequest to CDM process."_ns);
+ return;
+ }
+ mPromiseToCreateSessionToken.InsertOrUpdate(aPromiseId, aCreateSessionToken);
+}
+
+void ChromiumCDMParent::LoadSession(uint32_t aPromiseId, uint32_t aSessionType,
+ nsString aSessionId) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG("ChromiumCDMParent::LoadSession(this=%p, pid=%" PRIu32
+ ", type=%" PRIu32 ", sid=%s)",
+ this, aPromiseId, aSessionType,
+ NS_ConvertUTF16toUTF8(aSessionId).get());
+ if (mIsShutdown) {
+ RejectPromiseShutdown(aPromiseId);
+ return;
+ }
+ if (!SendLoadSession(aPromiseId, aSessionType,
+ NS_ConvertUTF16toUTF8(aSessionId))) {
+ RejectPromiseWithStateError(
+ aPromiseId, "Failed to send loadSession to CDM process."_ns);
+ return;
+ }
+}
+
+void ChromiumCDMParent::SetServerCertificate(uint32_t aPromiseId,
+ const nsTArray<uint8_t>& aCert) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG("ChromiumCDMParent::SetServerCertificate(this=%p)", this);
+ if (mIsShutdown) {
+ RejectPromiseShutdown(aPromiseId);
+ return;
+ }
+ if (!SendSetServerCertificate(aPromiseId, aCert)) {
+ RejectPromiseWithStateError(
+ aPromiseId, "Failed to send setServerCertificate to CDM process"_ns);
+ }
+}
+
+void ChromiumCDMParent::UpdateSession(const nsCString& aSessionId,
+ uint32_t aPromiseId,
+ const nsTArray<uint8_t>& aResponse) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG("ChromiumCDMParent::UpdateSession(this=%p)", this);
+ if (mIsShutdown) {
+ RejectPromiseShutdown(aPromiseId);
+ return;
+ }
+ if (!SendUpdateSession(aPromiseId, aSessionId, aResponse)) {
+ RejectPromiseWithStateError(
+ aPromiseId, "Failed to send updateSession to CDM process"_ns);
+ }
+}
+
+void ChromiumCDMParent::CloseSession(const nsCString& aSessionId,
+ uint32_t aPromiseId) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG("ChromiumCDMParent::CloseSession(this=%p)", this);
+ if (mIsShutdown) {
+ RejectPromiseShutdown(aPromiseId);
+ return;
+ }
+ if (!SendCloseSession(aPromiseId, aSessionId)) {
+ RejectPromiseWithStateError(
+ aPromiseId, "Failed to send closeSession to CDM process"_ns);
+ }
+}
+
+void ChromiumCDMParent::RemoveSession(const nsCString& aSessionId,
+ uint32_t aPromiseId) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG("ChromiumCDMParent::RemoveSession(this=%p)", this);
+ if (mIsShutdown) {
+ RejectPromiseShutdown(aPromiseId);
+ return;
+ }
+ if (!SendRemoveSession(aPromiseId, aSessionId)) {
+ RejectPromiseWithStateError(
+ aPromiseId, "Failed to send removeSession to CDM process"_ns);
+ }
+}
+
+void ChromiumCDMParent::NotifyOutputProtectionStatus(bool aSuccess,
+ uint32_t aLinkMask,
+ uint32_t aProtectionMask) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG("ChromiumCDMParent::NotifyOutputProtectionStatus(this=%p)",
+ this);
+ if (mIsShutdown) {
+ return;
+ }
+ const bool haveCachedValue = mOutputProtectionLinkMask.isSome();
+ if (mAwaitingOutputProtectionInformation && !aSuccess) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ !haveCachedValue,
+ "Should not have a cached value if we're still awaiting infomation");
+ // We're awaiting info and don't yet have a cached value, and the check
+ // failed, don't cache the result, just forward the failure.
+ CompleteQueryOutputProtectionStatus(false, aLinkMask, aProtectionMask);
+ return;
+ }
+ if (!mAwaitingOutputProtectionInformation && haveCachedValue && !aSuccess) {
+ // We're not awaiting info, already have a cached value, and the check
+ // failed. Ignore this, we'll update our info from any future successful
+ // checks.
+ return;
+ }
+ MOZ_ASSERT(aSuccess, "Failed checks should be handled by this point");
+ // Update our protection information.
+ mOutputProtectionLinkMask = Some(aLinkMask);
+
+ if (mAwaitingOutputProtectionInformation) {
+ // If we have an outstanding query, service that query with this
+ // information.
+ mAwaitingOutputProtectionInformation = false;
+ MOZ_ASSERT(!haveCachedValue,
+ "If we were waiting on information, we shouldn't have yet "
+ "cached a value");
+ CompleteQueryOutputProtectionStatus(true, mOutputProtectionLinkMask.value(),
+ aProtectionMask);
+ }
+}
+
+void ChromiumCDMParent::CompleteQueryOutputProtectionStatus(
+ bool aSuccess, uint32_t aLinkMask, uint32_t aProtectionMask) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG(
+ "ChromiumCDMParent::CompleteQueryOutputProtectionStatus(this=%p) "
+ "mIsShutdown=%s aSuccess=%s aLinkMask=%" PRIu32,
+ this, mIsShutdown ? "true" : "false", aSuccess ? "true" : "false",
+ aLinkMask);
+ if (mIsShutdown) {
+ return;
+ }
+ Unused << SendCompleteQueryOutputProtectionStatus(aSuccess, aLinkMask,
+ aProtectionMask);
+}
+
+// See
+// https://cs.chromium.org/chromium/src/media/blink/webcontentdecryptionmodule_impl.cc?l=33-66&rcl=d49aa59ac8c2925d5bec229f3f1906537b6b4547
+static Result<cdm::HdcpVersion, nsresult> ToCDMHdcpVersion(
+ const nsCString& aMinHdcpVersion) {
+ if (aMinHdcpVersion.IsEmpty()) {
+ return cdm::HdcpVersion::kHdcpVersionNone;
+ }
+ if (aMinHdcpVersion.EqualsIgnoreCase("1.0")) {
+ return cdm::HdcpVersion::kHdcpVersion1_0;
+ }
+ if (aMinHdcpVersion.EqualsIgnoreCase("1.1")) {
+ return cdm::HdcpVersion::kHdcpVersion1_1;
+ }
+ if (aMinHdcpVersion.EqualsIgnoreCase("1.2")) {
+ return cdm::HdcpVersion::kHdcpVersion1_2;
+ }
+ if (aMinHdcpVersion.EqualsIgnoreCase("1.3")) {
+ return cdm::HdcpVersion::kHdcpVersion1_3;
+ }
+ if (aMinHdcpVersion.EqualsIgnoreCase("1.4")) {
+ return cdm::HdcpVersion::kHdcpVersion1_4;
+ }
+ if (aMinHdcpVersion.EqualsIgnoreCase("2.0")) {
+ return cdm::HdcpVersion::kHdcpVersion2_0;
+ }
+ if (aMinHdcpVersion.EqualsIgnoreCase("2.1")) {
+ return cdm::HdcpVersion::kHdcpVersion2_1;
+ }
+ if (aMinHdcpVersion.EqualsIgnoreCase("2.2")) {
+ return cdm::HdcpVersion::kHdcpVersion2_2;
+ }
+ // When adding another version remember to update GMPMessageUtils so that we
+ // can serialize it correctly and have correct bounds on the enum!
+
+ // Invalid hdcp version string.
+ return Err(NS_ERROR_INVALID_ARG);
+}
+
+void ChromiumCDMParent::GetStatusForPolicy(uint32_t aPromiseId,
+ const nsCString& aMinHdcpVersion) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG("ChromiumCDMParent::GetStatusForPolicy(this=%p)", this);
+ if (mIsShutdown) {
+ RejectPromiseShutdown(aPromiseId);
+ return;
+ }
+ auto hdcpVersionResult = ToCDMHdcpVersion(aMinHdcpVersion);
+ if (hdcpVersionResult.isErr()) {
+ ErrorResult rv;
+ // XXXbz there's no spec for this yet, and
+ // <https://github.com/WICG/hdcp-detection/blob/master/explainer.md>
+ // does not define what exceptions get thrown. Let's assume
+ // TypeError for invalid args, as usual.
+ constexpr auto err =
+ "getStatusForPolicy failed due to bad hdcp version argument"_ns;
+ rv.ThrowTypeError(err);
+ RejectPromise(aPromiseId, std::move(rv), err);
+ return;
+ }
+
+ if (!SendGetStatusForPolicy(aPromiseId, hdcpVersionResult.unwrap())) {
+ RejectPromiseWithStateError(
+ aPromiseId, "Failed to send getStatusForPolicy to CDM process"_ns);
+ }
+}
+
+bool ChromiumCDMParent::InitCDMInputBuffer(gmp::CDMInputBuffer& aBuffer,
+ MediaRawData* aSample) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ const CryptoSample& crypto = aSample->mCrypto;
+ if (crypto.mEncryptedSizes.Length() != crypto.mPlainSizes.Length()) {
+ GMP_LOG_DEBUG("InitCDMInputBuffer clear/cipher subsamples don't match");
+ return false;
+ }
+
+ Shmem shmem;
+ if (!AllocShmem(aSample->Size(), &shmem)) {
+ return false;
+ }
+ memcpy(shmem.get<uint8_t>(), aSample->Data(), aSample->Size());
+ cdm::EncryptionScheme encryptionScheme = cdm::EncryptionScheme::kUnencrypted;
+ switch (crypto.mCryptoScheme) {
+ case CryptoScheme::None:
+ break; // Default to none
+ case CryptoScheme::Cenc:
+ encryptionScheme = cdm::EncryptionScheme::kCenc;
+ break;
+ case CryptoScheme::Cbcs:
+ encryptionScheme = cdm::EncryptionScheme::kCbcs;
+ break;
+ default:
+ GMP_LOG_DEBUG(
+ "InitCDMInputBuffer got unexpected encryption scheme with "
+ "value of %" PRIu8 ". Treating as no encryption.",
+ static_cast<uint8_t>(crypto.mCryptoScheme));
+ MOZ_ASSERT_UNREACHABLE("Should not have unrecognized encryption type");
+ break;
+ }
+
+ const nsTArray<uint8_t>& iv = encryptionScheme != cdm::EncryptionScheme::kCbcs
+ ? crypto.mIV
+ : crypto.mConstantIV;
+ aBuffer = gmp::CDMInputBuffer(
+ shmem, crypto.mKeyId, iv, aSample->mTime.ToMicroseconds(),
+ aSample->mDuration.ToMicroseconds(), crypto.mPlainSizes,
+ crypto.mEncryptedSizes, crypto.mCryptByteBlock, crypto.mSkipByteBlock,
+ encryptionScheme);
+ return true;
+}
+
+bool ChromiumCDMParent::SendBufferToCDM(uint32_t aSizeInBytes) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG("ChromiumCDMParent::SendBufferToCDM() size=%" PRIu32,
+ aSizeInBytes);
+ Shmem shmem;
+ if (!AllocShmem(aSizeInBytes, &shmem)) {
+ return false;
+ }
+ if (!SendGiveBuffer(std::move(shmem))) {
+ DeallocShmem(shmem);
+ return false;
+ }
+ return true;
+}
+
+RefPtr<DecryptPromise> ChromiumCDMParent::Decrypt(MediaRawData* aSample) {
+ if (mIsShutdown) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ return DecryptPromise::CreateAndReject(DecryptResult(GenericErr, aSample),
+ __func__);
+ }
+ CDMInputBuffer buffer;
+ if (!InitCDMInputBuffer(buffer, aSample)) {
+ return DecryptPromise::CreateAndReject(DecryptResult(GenericErr, aSample),
+ __func__);
+ }
+ // Send a buffer to the CDM to store the output. The CDM will either fill
+ // it with the decrypted sample and return it, or deallocate it on failure.
+ if (!SendBufferToCDM(aSample->Size())) {
+ DeallocShmem(buffer.mData());
+ return DecryptPromise::CreateAndReject(DecryptResult(GenericErr, aSample),
+ __func__);
+ }
+
+ RefPtr<DecryptJob> job = new DecryptJob(aSample);
+ if (!SendDecrypt(job->mId, buffer)) {
+ GMP_LOG_DEBUG(
+ "ChromiumCDMParent::Decrypt(this=%p) failed to send decrypt message",
+ this);
+ DeallocShmem(buffer.mData());
+ return DecryptPromise::CreateAndReject(DecryptResult(GenericErr, aSample),
+ __func__);
+ }
+ RefPtr<DecryptPromise> promise = job->Ensure();
+ mDecrypts.AppendElement(job);
+ return promise;
+}
+
+ipc::IPCResult ChromiumCDMParent::Recv__delete__() {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ MOZ_ASSERT(mIsShutdown);
+ GMP_LOG_DEBUG("ChromiumCDMParent::Recv__delete__(this=%p)", this);
+ if (mContentParent) {
+ mContentParent->ChromiumCDMDestroyed(this);
+ mContentParent = nullptr;
+ }
+ return IPC_OK();
+}
+
+ipc::IPCResult ChromiumCDMParent::RecvOnResolvePromiseWithKeyStatus(
+ const uint32_t& aPromiseId, const uint32_t& aKeyStatus) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG(
+ "ChromiumCDMParent::RecvOnResolvePromiseWithKeyStatus(this=%p, "
+ "pid=%" PRIu32 ", keystatus=%" PRIu32 ")",
+ this, aPromiseId, aKeyStatus);
+ if (!mCDMCallback || mIsShutdown) {
+ return IPC_OK();
+ }
+
+ mCDMCallback->ResolvePromiseWithKeyStatus(aPromiseId, aKeyStatus);
+
+ return IPC_OK();
+}
+
+ipc::IPCResult ChromiumCDMParent::RecvOnResolveNewSessionPromise(
+ const uint32_t& aPromiseId, const nsCString& aSessionId) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG(
+ "ChromiumCDMParent::RecvOnResolveNewSessionPromise(this=%p, pid=%" PRIu32
+ ", sid=%s)",
+ this, aPromiseId, aSessionId.get());
+ if (!mCDMCallback || mIsShutdown) {
+ return IPC_OK();
+ }
+
+ Maybe<uint32_t> token = mPromiseToCreateSessionToken.Extract(aPromiseId);
+ if (token.isNothing()) {
+ RejectPromiseWithStateError(aPromiseId,
+ "Lost session token for new session."_ns);
+ return IPC_OK();
+ }
+
+ mCDMCallback->SetSessionId(token.value(), aSessionId);
+
+ ResolvePromise(aPromiseId);
+
+ return IPC_OK();
+}
+
+ipc::IPCResult ChromiumCDMParent::RecvResolveLoadSessionPromise(
+ const uint32_t& aPromiseId, const bool& aSuccessful) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG(
+ "ChromiumCDMParent::RecvResolveLoadSessionPromise(this=%p, pid=%" PRIu32
+ ", successful=%d)",
+ this, aPromiseId, aSuccessful);
+ if (!mCDMCallback || mIsShutdown) {
+ return IPC_OK();
+ }
+
+ mCDMCallback->ResolveLoadSessionPromise(aPromiseId, aSuccessful);
+
+ return IPC_OK();
+}
+
+void ChromiumCDMParent::ResolvePromise(uint32_t aPromiseId) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG("ChromiumCDMParent::ResolvePromise(this=%p, pid=%" PRIu32 ")",
+ this, aPromiseId);
+
+ // Note: The MediaKeys rejects all pending DOM promises when it
+ // initiates shutdown.
+ if (!mCDMCallback || mIsShutdown) {
+ return;
+ }
+
+ mCDMCallback->ResolvePromise(aPromiseId);
+}
+
+ipc::IPCResult ChromiumCDMParent::RecvOnResolvePromise(
+ const uint32_t& aPromiseId) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ ResolvePromise(aPromiseId);
+ return IPC_OK();
+}
+
+void ChromiumCDMParent::RejectPromise(uint32_t aPromiseId,
+ ErrorResult&& aException,
+ const nsCString& aErrorMessage) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG("ChromiumCDMParent::RejectPromise(this=%p, pid=%" PRIu32 ")",
+ this, aPromiseId);
+ // Note: The MediaKeys rejects all pending DOM promises when it
+ // initiates shutdown.
+ if (!mCDMCallback || mIsShutdown) {
+ // Suppress the exception as it will not be explicitly handled due to the
+ // early return.
+ aException.SuppressException();
+ return;
+ }
+
+ mCDMCallback->RejectPromise(aPromiseId, std::move(aException), aErrorMessage);
+}
+
+void ChromiumCDMParent::RejectPromiseShutdown(uint32_t aPromiseId) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ RejectPromiseWithStateError(aPromiseId, "CDM is shutdown"_ns);
+}
+
+void ChromiumCDMParent::RejectPromiseWithStateError(
+ uint32_t aPromiseId, const nsCString& aErrorMessage) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ ErrorResult rv;
+ rv.ThrowInvalidStateError(aErrorMessage);
+ RejectPromise(aPromiseId, std::move(rv), aErrorMessage);
+}
+
+static ErrorResult ToErrorResult(uint32_t aException,
+ const nsCString& aErrorMessage) {
+ // XXXbz could we have a CopyableErrorResult sent to us with a better error
+ // message?
+ ErrorResult rv;
+ switch (static_cast<cdm::Exception>(aException)) {
+ case cdm::Exception::kExceptionNotSupportedError:
+ rv.ThrowNotSupportedError(aErrorMessage);
+ break;
+ case cdm::Exception::kExceptionInvalidStateError:
+ rv.ThrowInvalidStateError(aErrorMessage);
+ break;
+ case cdm::Exception::kExceptionTypeError:
+ rv.ThrowTypeError(aErrorMessage);
+ break;
+ case cdm::Exception::kExceptionQuotaExceededError:
+ rv.ThrowQuotaExceededError(aErrorMessage);
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid cdm::Exception enum value.");
+ // Note: Unique placeholder.
+ rv.ThrowTimeoutError(aErrorMessage);
+ };
+ return rv;
+}
+
+ipc::IPCResult ChromiumCDMParent::RecvOnRejectPromise(
+ const uint32_t& aPromiseId, const uint32_t& aException,
+ const uint32_t& aSystemCode, const nsCString& aErrorMessage) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ RejectPromise(aPromiseId, ToErrorResult(aException, aErrorMessage),
+ aErrorMessage);
+ return IPC_OK();
+}
+
+ipc::IPCResult ChromiumCDMParent::RecvOnSessionMessage(
+ const nsCString& aSessionId, const uint32_t& aMessageType,
+ nsTArray<uint8_t>&& aMessage) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG("ChromiumCDMParent::RecvOnSessionMessage(this=%p, sid=%s)",
+ this, aSessionId.get());
+ if (!mCDMCallback || mIsShutdown) {
+ return IPC_OK();
+ }
+
+ mCDMCallback->SessionMessage(aSessionId, aMessageType, std::move(aMessage));
+ return IPC_OK();
+}
+
+ipc::IPCResult ChromiumCDMParent::RecvOnSessionKeysChange(
+ const nsCString& aSessionId, nsTArray<CDMKeyInformation>&& aKeysInfo) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG("ChromiumCDMParent::RecvOnSessionKeysChange(this=%p)", this);
+ if (!mCDMCallback || mIsShutdown) {
+ return IPC_OK();
+ }
+
+ mCDMCallback->SessionKeysChange(aSessionId, std::move(aKeysInfo));
+ return IPC_OK();
+}
+
+ipc::IPCResult ChromiumCDMParent::RecvOnExpirationChange(
+ const nsCString& aSessionId, const double& aSecondsSinceEpoch) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG("ChromiumCDMParent::RecvOnExpirationChange(this=%p) time=%lf",
+ this, aSecondsSinceEpoch);
+ if (!mCDMCallback || mIsShutdown) {
+ return IPC_OK();
+ }
+
+ mCDMCallback->ExpirationChange(aSessionId, aSecondsSinceEpoch);
+ return IPC_OK();
+}
+
+ipc::IPCResult ChromiumCDMParent::RecvOnSessionClosed(
+ const nsCString& aSessionId) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG("ChromiumCDMParent::RecvOnSessionClosed(this=%p)", this);
+ if (!mCDMCallback || mIsShutdown) {
+ return IPC_OK();
+ }
+
+ mCDMCallback->SessionClosed(aSessionId);
+ return IPC_OK();
+}
+
+ipc::IPCResult ChromiumCDMParent::RecvOnQueryOutputProtectionStatus() {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG(
+ "ChromiumCDMParent::RecvOnQueryOutputProtectionStatus(this=%p) "
+ "mIsShutdown=%s mCDMCallback=%s mAwaitingOutputProtectionInformation=%s",
+ this, mIsShutdown ? "true" : "false", mCDMCallback ? "true" : "false",
+ mAwaitingOutputProtectionInformation ? "true" : "false");
+ if (mIsShutdown) {
+ // We're shutdown, don't try to service the query.
+ return IPC_OK();
+ }
+ if (!mCDMCallback) {
+ // We don't have a callback. We're not yet outputting anything so can report
+ // we're safe.
+ CompleteQueryOutputProtectionStatus(true, uint32_t{}, uint32_t{});
+ return IPC_OK();
+ }
+
+ if (mOutputProtectionLinkMask.isSome()) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ !mAwaitingOutputProtectionInformation,
+ "If we have a cached value we should not be awaiting information");
+ // We have a cached value, use that.
+ CompleteQueryOutputProtectionStatus(true, mOutputProtectionLinkMask.value(),
+ uint32_t{});
+ return IPC_OK();
+ }
+
+ // We need to call up the stack to get the info. The CDM proxy will finish
+ // the request via `NotifyOutputProtectionStatus`.
+ mAwaitingOutputProtectionInformation = true;
+ mCDMCallback->QueryOutputProtectionStatus();
+ return IPC_OK();
+}
+
+DecryptStatus ToDecryptStatus(uint32_t aStatus) {
+ switch (static_cast<cdm::Status>(aStatus)) {
+ case cdm::kSuccess:
+ return DecryptStatus::Ok;
+ case cdm::kNoKey:
+ return DecryptStatus::NoKeyErr;
+ default:
+ return DecryptStatus::GenericErr;
+ }
+}
+
+ipc::IPCResult ChromiumCDMParent::RecvDecryptFailed(const uint32_t& aId,
+ const uint32_t& aStatus) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG("ChromiumCDMParent::RecvDecryptFailed(this=%p, id=%" PRIu32
+ ", status=%" PRIu32 ")",
+ this, aId, aStatus);
+
+ if (mIsShutdown) {
+ MOZ_ASSERT(mDecrypts.IsEmpty());
+ return IPC_OK();
+ }
+
+ for (size_t i = 0; i < mDecrypts.Length(); i++) {
+ if (mDecrypts[i]->mId == aId) {
+ mDecrypts[i]->PostResult(ToDecryptStatus(aStatus));
+ mDecrypts.RemoveElementAt(i);
+ break;
+ }
+ }
+ return IPC_OK();
+}
+
+ipc::IPCResult ChromiumCDMParent::RecvDecrypted(const uint32_t& aId,
+ const uint32_t& aStatus,
+ ipc::Shmem&& aShmem) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG("ChromiumCDMParent::RecvDecrypted(this=%p, id=%" PRIu32
+ ", status=%" PRIu32 ")",
+ this, aId, aStatus);
+
+ // We must deallocate the shmem once we've copied the result out of it
+ // in PostResult below.
+ auto autoDeallocateShmem = MakeScopeExit([&, this] { DeallocShmem(aShmem); });
+
+ if (mIsShutdown) {
+ MOZ_ASSERT(mDecrypts.IsEmpty());
+ return IPC_OK();
+ }
+ for (size_t i = 0; i < mDecrypts.Length(); i++) {
+ if (mDecrypts[i]->mId == aId) {
+ mDecrypts[i]->PostResult(
+ ToDecryptStatus(aStatus),
+ Span<const uint8_t>(aShmem.get<uint8_t>(), aShmem.Size<uint8_t>()));
+ mDecrypts.RemoveElementAt(i);
+ break;
+ }
+ }
+ return IPC_OK();
+}
+
+ipc::IPCResult ChromiumCDMParent::RecvIncreaseShmemPoolSize() {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG("%s(this=%p) limit=%" PRIu32 " active=%" PRIu32, __func__, this,
+ mVideoShmemLimit, mVideoShmemsActive);
+
+ // Put an upper limit on the number of shmems we tolerate the CDM asking
+ // for, to prevent a memory blow-out. In practice, we expect the CDM to
+ // need less than 5, but some encodings require more.
+ // We'd expect CDMs to not have video frames larger than 720p-1080p
+ // (due to DRM robustness requirements), which is about 1.5MB-3MB per
+ // frame.
+ if (mVideoShmemLimit > 50) {
+ mDecodePromise.RejectIfExists(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Failled to ensure CDM has enough shmems.")),
+ __func__);
+ Shutdown();
+ return IPC_OK();
+ }
+ mVideoShmemLimit++;
+
+ EnsureSufficientShmems(mVideoFrameBufferSize);
+
+ return IPC_OK();
+}
+
+bool ChromiumCDMParent::PurgeShmems() {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG(
+ "ChromiumCDMParent::PurgeShmems(this=%p) frame_size=%zu"
+ " limit=%" PRIu32 " active=%" PRIu32,
+ this, mVideoFrameBufferSize, mVideoShmemLimit, mVideoShmemsActive);
+
+ if (mVideoShmemsActive == 0) {
+ // We haven't allocated any shmems, nothing to do here.
+ return true;
+ }
+ if (!SendPurgeShmems()) {
+ return false;
+ }
+ mVideoShmemsActive = 0;
+ return true;
+}
+
+bool ChromiumCDMParent::EnsureSufficientShmems(size_t aVideoFrameSize) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG(
+ "ChromiumCDMParent::EnsureSufficientShmems(this=%p) "
+ "size=%zu expected_size=%zu limit=%" PRIu32 " active=%" PRIu32,
+ this, aVideoFrameSize, mVideoFrameBufferSize, mVideoShmemLimit,
+ mVideoShmemsActive);
+
+ // The Chromium CDM API requires us to implement a synchronous
+ // interface to supply buffers to the CDM for it to write decrypted samples
+ // into. We want our buffers to be backed by shmems, in order to reduce
+ // the overhead of transferring decoded frames. However due to sandboxing
+ // restrictions, the CDM process cannot allocate shmems itself.
+ // We don't want to be doing synchronous IPC to request shmems from the
+ // content process, nor do we want to have to do intr IPC or make async
+ // IPC conform to the sync allocation interface. So instead we have the
+ // content process pre-allocate a set of shmems and give them to the CDM
+ // process in advance of them being needed.
+ //
+ // When the CDM needs to allocate a buffer for storing a decoded video
+ // frame, the CDM host gives it one of these shmems' buffers. When this
+ // is sent back to the content process, we upload it to a GPU surface,
+ // and send the shmem back to the CDM process so it can reuse it.
+ //
+ // Normally the CDM won't allocate more than one buffer at once, but
+ // we've seen cases where it allocates multiple buffers, returns one and
+ // holds onto the rest. So we need to ensure we have several extra
+ // shmems pre-allocated for the CDM. This threshold is set by the pref
+ // media.eme.chromium-api.video-shmems.
+ //
+ // We also have a failure recovery mechanism; if the CDM asks for more
+ // buffers than we have shmem's available, ChromiumCDMChild gives the
+ // CDM a non-shared memory buffer, and returns the frame to the parent
+ // in an nsTArray<uint8_t> instead of a shmem. The child then sends a
+ // message to the parent asking it to increase the number of shmems in
+ // the pool. Via this mechanism we should recover from incorrectly
+ // predicting how many shmems to pre-allocate.
+ //
+ // At decoder start up, we guess how big the shmems need to be based on
+ // the video frame dimensions. If we guess wrong, the CDM will follow
+ // the non-shmem path, and we'll re-create the shmems of the correct size.
+ // This meanns we can recover from guessing the shmem size wrong.
+ // We must re-take this path after every decoder de-init/re-init, as the
+ // frame sizes should change every time we switch video stream.
+
+ if (mVideoFrameBufferSize < aVideoFrameSize) {
+ if (!PurgeShmems()) {
+ return false;
+ }
+ mVideoFrameBufferSize = aVideoFrameSize;
+ }
+
+ while (mVideoShmemsActive < mVideoShmemLimit) {
+ if (!SendBufferToCDM(mVideoFrameBufferSize)) {
+ return false;
+ }
+ mVideoShmemsActive++;
+ }
+
+ return true;
+}
+
+ipc::IPCResult ChromiumCDMParent::RecvDecodedData(const CDMVideoFrame& aFrame,
+ nsTArray<uint8_t>&& aData) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG("ChromiumCDMParent::RecvDecodedData(this=%p) time=%" PRId64,
+ this, aFrame.mTimestamp());
+
+ if (mIsShutdown || mDecodePromise.IsEmpty()) {
+ return IPC_OK();
+ }
+
+ if (!EnsureSufficientShmems(aData.Length())) {
+ mDecodePromise.RejectIfExists(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Failled to ensure CDM has enough shmems.")),
+ __func__);
+ return IPC_OK();
+ }
+
+ RefPtr<VideoData> v = CreateVideoFrame(aFrame, aData);
+ if (!v) {
+ mDecodePromise.RejectIfExists(
+ MediaResult(NS_ERROR_OUT_OF_MEMORY,
+ RESULT_DETAIL("Can't create VideoData")),
+ __func__);
+ return IPC_OK();
+ }
+
+ ReorderAndReturnOutput(std::move(v));
+
+ return IPC_OK();
+}
+
+ipc::IPCResult ChromiumCDMParent::RecvDecodedShmem(const CDMVideoFrame& aFrame,
+ ipc::Shmem&& aShmem) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG("ChromiumCDMParent::RecvDecodedShmem(this=%p) time=%" PRId64
+ " duration=%" PRId64,
+ this, aFrame.mTimestamp(), aFrame.mDuration());
+
+ // On failure we need to deallocate the shmem we're to return to the
+ // CDM. On success we return it to the CDM to be reused.
+ auto autoDeallocateShmem =
+ MakeScopeExit([&, this] { this->DeallocShmem(aShmem); });
+
+ if (mIsShutdown || mDecodePromise.IsEmpty()) {
+ return IPC_OK();
+ }
+
+ RefPtr<VideoData> v = CreateVideoFrame(
+ aFrame, Span<uint8_t>(aShmem.get<uint8_t>(), aShmem.Size<uint8_t>()));
+ if (!v) {
+ mDecodePromise.RejectIfExists(
+ MediaResult(NS_ERROR_OUT_OF_MEMORY,
+ RESULT_DETAIL("Can't create VideoData")),
+ __func__);
+ return IPC_OK();
+ }
+
+ // Return the shmem to the CDM so the shmem can be reused to send us
+ // another frame.
+ if (!SendGiveBuffer(std::move(aShmem))) {
+ mDecodePromise.RejectIfExists(
+ MediaResult(NS_ERROR_OUT_OF_MEMORY,
+ RESULT_DETAIL("Can't return shmem to CDM process")),
+ __func__);
+ return IPC_OK();
+ }
+
+ // Don't need to deallocate the shmem since the CDM process is responsible
+ // for it again.
+ autoDeallocateShmem.release();
+
+ ReorderAndReturnOutput(std::move(v));
+
+ return IPC_OK();
+}
+
+void ChromiumCDMParent::ReorderAndReturnOutput(RefPtr<VideoData>&& aFrame) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ if (mMaxRefFrames == 0) {
+ mDecodePromise.ResolveIfExists(
+ MediaDataDecoder::DecodedData({std::move(aFrame)}), __func__);
+ return;
+ }
+ mReorderQueue.Push(std::move(aFrame));
+ MediaDataDecoder::DecodedData results;
+ while (mReorderQueue.Length() > mMaxRefFrames) {
+ results.AppendElement(mReorderQueue.Pop());
+ }
+ mDecodePromise.Resolve(std::move(results), __func__);
+}
+
+already_AddRefed<VideoData> ChromiumCDMParent::CreateVideoFrame(
+ const CDMVideoFrame& aFrame, Span<uint8_t> aData) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ VideoData::YCbCrBuffer b;
+ MOZ_ASSERT(aData.Length() > 0);
+
+ // Since we store each plane separately we can just roll the offset
+ // into our pointer to that plane and store that.
+ b.mPlanes[0].mData = aData.Elements() + aFrame.mYPlane().mPlaneOffset();
+ b.mPlanes[0].mWidth = aFrame.mImageWidth();
+ b.mPlanes[0].mHeight = aFrame.mImageHeight();
+ b.mPlanes[0].mStride = aFrame.mYPlane().mStride();
+ b.mPlanes[0].mSkip = 0;
+
+ b.mPlanes[1].mData = aData.Elements() + aFrame.mUPlane().mPlaneOffset();
+ b.mPlanes[1].mWidth = (aFrame.mImageWidth() + 1) / 2;
+ b.mPlanes[1].mHeight = (aFrame.mImageHeight() + 1) / 2;
+ b.mPlanes[1].mStride = aFrame.mUPlane().mStride();
+ b.mPlanes[1].mSkip = 0;
+
+ b.mPlanes[2].mData = aData.Elements() + aFrame.mVPlane().mPlaneOffset();
+ b.mPlanes[2].mWidth = (aFrame.mImageWidth() + 1) / 2;
+ b.mPlanes[2].mHeight = (aFrame.mImageHeight() + 1) / 2;
+ b.mPlanes[2].mStride = aFrame.mVPlane().mStride();
+ b.mPlanes[2].mSkip = 0;
+
+ b.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT;
+
+ // We unfortunately can't know which colorspace the video is using at this
+ // stage.
+ b.mYUVColorSpace =
+ DefaultColorSpace({aFrame.mImageWidth(), aFrame.mImageHeight()});
+
+ gfx::IntRect pictureRegion(0, 0, aFrame.mImageWidth(), aFrame.mImageHeight());
+ RefPtr<VideoData> v = VideoData::CreateAndCopyData(
+ mVideoInfo, mImageContainer, mLastStreamOffset,
+ media::TimeUnit::FromMicroseconds(aFrame.mTimestamp()),
+ media::TimeUnit::FromMicroseconds(aFrame.mDuration()), b, false,
+ media::TimeUnit::FromMicroseconds(-1), pictureRegion, mKnowsCompositor);
+
+ if (!v || !v->mImage) {
+ NS_WARNING("Failed to decode video frame.");
+ return v.forget();
+ }
+
+ // This is a DRM image.
+ v->mImage->SetIsDRM(true);
+
+ return v.forget();
+}
+
+ipc::IPCResult ChromiumCDMParent::RecvDecodeFailed(const uint32_t& aStatus) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG("ChromiumCDMParent::RecvDecodeFailed(this=%p status=%" PRIu32
+ ")",
+ this, aStatus);
+ if (mIsShutdown) {
+ MOZ_ASSERT(mDecodePromise.IsEmpty());
+ return IPC_OK();
+ }
+
+ if (aStatus == cdm::kNeedMoreData) {
+ mDecodePromise.ResolveIfExists(nsTArray<RefPtr<MediaData>>(), __func__);
+ return IPC_OK();
+ }
+
+ mDecodePromise.RejectIfExists(
+ MediaResult(
+ NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL(
+ "ChromiumCDMParent::RecvDecodeFailed with status %s (%" PRIu32
+ ")",
+ CdmStatusToString(aStatus), aStatus)),
+ __func__);
+ return IPC_OK();
+}
+
+ipc::IPCResult ChromiumCDMParent::RecvShutdown() {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG("ChromiumCDMParent::RecvShutdown(this=%p)", this);
+ Shutdown();
+ return IPC_OK();
+}
+
+void ChromiumCDMParent::ActorDestroy(ActorDestroyReason aWhy) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG("ChromiumCDMParent::ActorDestroy(this=%p, reason=%d)", this,
+ aWhy);
+ MOZ_ASSERT(!mActorDestroyed);
+ mActorDestroyed = true;
+ // Shutdown() will clear mCDMCallback, so let's keep a reference for later
+ // use.
+ auto callback = mCDMCallback;
+ if (!mIsShutdown) {
+ // Plugin crash.
+ MOZ_ASSERT(aWhy == AbnormalShutdown);
+ Shutdown();
+ }
+ MOZ_ASSERT(mIsShutdown);
+ RefPtr<ChromiumCDMParent> kungFuDeathGrip(this);
+ if (mContentParent) {
+ mContentParent->ChromiumCDMDestroyed(this);
+ mContentParent = nullptr;
+ }
+ mAbnormalShutdown = (aWhy == AbnormalShutdown);
+ if (mAbnormalShutdown && callback) {
+ callback->Terminated();
+ }
+ MaybeDisconnect(mAbnormalShutdown);
+}
+
+RefPtr<MediaDataDecoder::InitPromise> ChromiumCDMParent::InitializeVideoDecoder(
+ const gmp::CDMVideoDecoderConfig& aConfig, const VideoInfo& aInfo,
+ RefPtr<layers::ImageContainer> aImageContainer,
+ RefPtr<layers::KnowsCompositor> aKnowsCompositor) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ if (mIsShutdown) {
+ return MediaDataDecoder::InitPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("ChromiumCDMParent is shutdown")),
+ __func__);
+ }
+
+ // The Widevine CDM version 1.4.8.970 and above contain a video decoder that
+ // does not optimally allocate video frames; it requests buffers much larger
+ // than required. The exact formula the CDM uses to calculate their frame
+ // sizes isn't obvious, but they normally request around or slightly more
+ // than 1.5X the optimal amount. So pad the size of buffers we allocate so
+ // that we're likely to have buffers big enough to accomodate the CDM's weird
+ // frame size calculation.
+ const size_t bufferSize =
+ 1.7 * I420FrameBufferSizePadded(aInfo.mImage.width, aInfo.mImage.height);
+ if (bufferSize <= 0) {
+ return MediaDataDecoder::InitPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Video frame buffer size is invalid.")),
+ __func__);
+ }
+
+ if (!EnsureSufficientShmems(bufferSize)) {
+ return MediaDataDecoder::InitPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Failed to init shmems for video decoder")),
+ __func__);
+ }
+
+ if (!SendInitializeVideoDecoder(aConfig)) {
+ return MediaDataDecoder::InitPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Failed to send init video decoder to CDM")),
+ __func__);
+ }
+
+ mMaxRefFrames = (aConfig.mCodec() == cdm::VideoCodec::kCodecH264)
+ ? H264::HasSPS(aInfo.mExtraData)
+ ? H264::ComputeMaxRefFrames(aInfo.mExtraData)
+ : 16
+ : 0;
+
+ mVideoDecoderInitialized = true;
+ mImageContainer = aImageContainer;
+ mKnowsCompositor = aKnowsCompositor;
+ mVideoInfo = aInfo;
+ mVideoFrameBufferSize = bufferSize;
+
+ return mInitVideoDecoderPromise.Ensure(__func__);
+}
+
+ipc::IPCResult ChromiumCDMParent::RecvOnDecoderInitDone(
+ const uint32_t& aStatus) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG(
+ "ChromiumCDMParent::RecvOnDecoderInitDone(this=%p, status=%" PRIu32 ")",
+ this, aStatus);
+ if (mIsShutdown) {
+ MOZ_ASSERT(mInitVideoDecoderPromise.IsEmpty());
+ return IPC_OK();
+ }
+ if (aStatus == static_cast<uint32_t>(cdm::kSuccess)) {
+ mInitVideoDecoderPromise.ResolveIfExists(TrackInfo::kVideoTrack, __func__);
+ } else {
+ mVideoDecoderInitialized = false;
+ mInitVideoDecoderPromise.RejectIfExists(
+ MediaResult(
+ NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("CDM init decode failed with status %s (%" PRIu32 ")",
+ CdmStatusToString(aStatus), aStatus)),
+ __func__);
+ }
+ return IPC_OK();
+}
+
+RefPtr<MediaDataDecoder::DecodePromise>
+ChromiumCDMParent::DecryptAndDecodeFrame(MediaRawData* aSample) {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ if (mIsShutdown) {
+ return MediaDataDecoder::DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("ChromiumCDMParent is shutdown")),
+ __func__);
+ }
+
+ GMP_LOG_DEBUG("ChromiumCDMParent::DecryptAndDecodeFrame t=%" PRId64,
+ aSample->mTime.ToMicroseconds());
+
+ CDMInputBuffer buffer;
+
+ if (!InitCDMInputBuffer(buffer, aSample)) {
+ return MediaDataDecoder::DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, "Failed to init CDM buffer."),
+ __func__);
+ }
+
+ mLastStreamOffset = aSample->mOffset;
+
+ if (!SendDecryptAndDecodeFrame(buffer)) {
+ GMP_LOG_DEBUG(
+ "ChromiumCDMParent::Decrypt(this=%p) failed to send decrypt message.",
+ this);
+ DeallocShmem(buffer.mData());
+ return MediaDataDecoder::DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ "Failed to send decrypt to CDM process."),
+ __func__);
+ }
+
+ return mDecodePromise.Ensure(__func__);
+}
+
+RefPtr<MediaDataDecoder::FlushPromise> ChromiumCDMParent::FlushVideoDecoder() {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ if (mIsShutdown) {
+ MOZ_ASSERT(mReorderQueue.IsEmpty());
+ return MediaDataDecoder::FlushPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("ChromiumCDMParent is shutdown")),
+ __func__);
+ }
+
+ mReorderQueue.Clear();
+
+ mDecodePromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ if (!SendResetVideoDecoder()) {
+ return MediaDataDecoder::FlushPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ "Failed to send flush to CDM."),
+ __func__);
+ }
+ return mFlushDecoderPromise.Ensure(__func__);
+}
+
+ipc::IPCResult ChromiumCDMParent::RecvResetVideoDecoderComplete() {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ MOZ_ASSERT(mReorderQueue.IsEmpty());
+ if (mIsShutdown) {
+ MOZ_ASSERT(mFlushDecoderPromise.IsEmpty());
+ return IPC_OK();
+ }
+ mFlushDecoderPromise.ResolveIfExists(true, __func__);
+ return IPC_OK();
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> ChromiumCDMParent::Drain() {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ MOZ_ASSERT(mDecodePromise.IsEmpty(), "Must wait for decoding to complete");
+ if (mIsShutdown) {
+ return MediaDataDecoder::DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("ChromiumCDMParent is shutdown")),
+ __func__);
+ }
+
+ RefPtr<MediaDataDecoder::DecodePromise> p = mDecodePromise.Ensure(__func__);
+ if (!SendDrain()) {
+ mDecodePromise.Resolve(MediaDataDecoder::DecodedData(), __func__);
+ }
+ return p;
+}
+
+ipc::IPCResult ChromiumCDMParent::RecvDrainComplete() {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ if (mIsShutdown) {
+ MOZ_ASSERT(mDecodePromise.IsEmpty());
+ return IPC_OK();
+ }
+
+ MediaDataDecoder::DecodedData samples;
+ while (!mReorderQueue.IsEmpty()) {
+ samples.AppendElement(mReorderQueue.Pop());
+ }
+
+ mDecodePromise.ResolveIfExists(std::move(samples), __func__);
+ return IPC_OK();
+}
+RefPtr<ShutdownPromise> ChromiumCDMParent::ShutdownVideoDecoder() {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ if (mIsShutdown || !mVideoDecoderInitialized) {
+ return ShutdownPromise::CreateAndResolve(true, __func__);
+ }
+ mInitVideoDecoderPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED,
+ __func__);
+ mDecodePromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ MOZ_ASSERT(mFlushDecoderPromise.IsEmpty());
+ if (!SendDeinitializeVideoDecoder()) {
+ return ShutdownPromise::CreateAndResolve(true, __func__);
+ }
+ mVideoDecoderInitialized = false;
+
+ GMP_LOG_DEBUG("ChromiumCDMParent::~ShutdownVideoDecoder(this=%p) ", this);
+
+ // The ChromiumCDMChild will purge its shmems, so if the decoder is
+ // reinitialized the shmems need to be re-allocated, and they may need
+ // to be a different size.
+ mVideoShmemsActive = 0;
+ mVideoFrameBufferSize = 0;
+ return ShutdownPromise::CreateAndResolve(true, __func__);
+}
+
+void ChromiumCDMParent::Shutdown() {
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+ GMP_LOG_DEBUG("ChromiumCDMParent::Shutdown(this=%p)", this);
+
+ if (mIsShutdown) {
+ return;
+ }
+ mIsShutdown = true;
+
+ // If we're shutting down due to the plugin shutting down due to application
+ // shutdown, we should tell the CDM proxy to also shutdown. Otherwise the
+ // proxy will shutdown when the owning MediaKeys is destroyed during cycle
+ // collection, and that will not shut down cleanly as the GMP thread will be
+ // shutdown by then.
+ if (mCDMCallback) {
+ mCDMCallback->Shutdown();
+ }
+
+ // We may be called from a task holding the last reference to the CDM
+ // callback, so let's clear our local weak pointer to ensure it will not be
+ // used afterward (including from an already-queued task, e.g.: ActorDestroy).
+ mCDMCallback = nullptr;
+
+ mReorderQueue.Clear();
+
+ for (RefPtr<DecryptJob>& decrypt : mDecrypts) {
+ decrypt->PostResult(eme::AbortedErr);
+ }
+ mDecrypts.Clear();
+
+ if (mVideoDecoderInitialized && !mActorDestroyed) {
+ Unused << SendDeinitializeVideoDecoder();
+ mVideoDecoderInitialized = false;
+ }
+
+ // Note: MediaKeys rejects all outstanding promises when it initiates
+ // shutdown.
+ mPromiseToCreateSessionToken.Clear();
+
+ mInitPromise.RejectIfExists(
+ MediaResult(NS_ERROR_DOM_ABORT_ERR,
+ RESULT_DETAIL("ChromiumCDMParent is shutdown")),
+ __func__);
+
+ mInitVideoDecoderPromise.RejectIfExists(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("ChromiumCDMParent is shutdown")),
+ __func__);
+ mDecodePromise.RejectIfExists(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("ChromiumCDMParent is shutdown")),
+ __func__);
+ mFlushDecoderPromise.RejectIfExists(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("ChromiumCDMParent is shutdown")),
+ __func__);
+
+ if (!mActorDestroyed) {
+ Unused << SendDestroy();
+ }
+}
+
+} // namespace mozilla::gmp
+
+#undef NS_DispatchToMainThread
diff --git a/dom/media/gmp/ChromiumCDMParent.h b/dom/media/gmp/ChromiumCDMParent.h
new file mode 100644
index 0000000000..b6c28fb1c5
--- /dev/null
+++ b/dom/media/gmp/ChromiumCDMParent.h
@@ -0,0 +1,232 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef ChromiumCDMParent_h_
+#define ChromiumCDMParent_h_
+
+#include "DecryptJob.h"
+#include "GMPCrashHelper.h"
+#include "GMPCrashHelperHolder.h"
+#include "GMPMessageUtils.h"
+#include "mozilla/gmp/PChromiumCDMParent.h"
+#include "mozilla/RefPtr.h"
+#include "nsTHashMap.h"
+#include "PlatformDecoderModule.h"
+#include "ImageContainer.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Span.h"
+#include "ReorderQueue.h"
+
+class ChromiumCDMCallback;
+
+namespace mozilla {
+
+class ErrorResult;
+class MediaRawData;
+class ChromiumCDMProxy;
+
+namespace gmp {
+
+class GMPContentParent;
+
+/**
+ * ChromiumCDMParent is the content process IPC actor used to communicate with a
+ * CDM in the GMP process (where ChromiumCDMChild lives). All non-static
+ * members of this class are GMP thread only.
+ */
+class ChromiumCDMParent final : public PChromiumCDMParent,
+ public GMPCrashHelperHolder {
+ friend class PChromiumCDMParent;
+
+ public:
+ typedef MozPromise<bool, MediaResult, /* IsExclusive = */ true> InitPromise;
+
+ // Mark AddRef and Release as `final`, as they overload pure virtual
+ // implementations in PChromiumCDMParent.
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ChromiumCDMParent, final)
+
+ ChromiumCDMParent(GMPContentParent* aContentParent, uint32_t aPluginId);
+
+ uint32_t PluginId() const { return mPluginId; }
+
+ RefPtr<InitPromise> Init(ChromiumCDMCallback* aCDMCallback,
+ bool aAllowDistinctiveIdentifier,
+ bool aAllowPersistentState,
+ nsIEventTarget* aMainThread);
+
+ void CreateSession(uint32_t aCreateSessionToken, uint32_t aSessionType,
+ uint32_t aInitDataType, uint32_t aPromiseId,
+ const nsTArray<uint8_t>& aInitData);
+
+ void LoadSession(uint32_t aPromiseId, uint32_t aSessionType,
+ nsString aSessionId);
+
+ void SetServerCertificate(uint32_t aPromiseId,
+ const nsTArray<uint8_t>& aCert);
+
+ void UpdateSession(const nsCString& aSessionId, uint32_t aPromiseId,
+ const nsTArray<uint8_t>& aResponse);
+
+ void CloseSession(const nsCString& aSessionId, uint32_t aPromiseId);
+
+ void RemoveSession(const nsCString& aSessionId, uint32_t aPromiseId);
+
+ // Notifies this parent of the current output protection status. This will
+ // update cached status and resolve outstanding queries from the CDM if one
+ // exists.
+ void NotifyOutputProtectionStatus(bool aSuccess, uint32_t aLinkMask,
+ uint32_t aProtectionMask);
+
+ void GetStatusForPolicy(uint32_t aPromiseId,
+ const nsCString& aMinHdcpVersion);
+
+ RefPtr<DecryptPromise> Decrypt(MediaRawData* aSample);
+
+ // TODO: Add functions for clients to send data to CDM, and
+ // a Close() function.
+ RefPtr<MediaDataDecoder::InitPromise> InitializeVideoDecoder(
+ const gmp::CDMVideoDecoderConfig& aConfig, const VideoInfo& aInfo,
+ RefPtr<layers::ImageContainer> aImageContainer,
+ RefPtr<layers::KnowsCompositor> aKnowsCompositor);
+
+ RefPtr<MediaDataDecoder::DecodePromise> DecryptAndDecodeFrame(
+ MediaRawData* aSample);
+
+ RefPtr<MediaDataDecoder::FlushPromise> FlushVideoDecoder();
+
+ RefPtr<MediaDataDecoder::DecodePromise> Drain();
+
+ RefPtr<ShutdownPromise> ShutdownVideoDecoder();
+
+ void Shutdown();
+
+ protected:
+ ~ChromiumCDMParent() = default;
+
+ ipc::IPCResult Recv__delete__() override;
+ ipc::IPCResult RecvOnResolvePromiseWithKeyStatus(const uint32_t& aPromiseId,
+ const uint32_t& aKeyStatus);
+ ipc::IPCResult RecvOnResolveNewSessionPromise(const uint32_t& aPromiseId,
+ const nsCString& aSessionId);
+ ipc::IPCResult RecvResolveLoadSessionPromise(const uint32_t& aPromiseId,
+ const bool& aSuccessful);
+ ipc::IPCResult RecvOnResolvePromise(const uint32_t& aPromiseId);
+ ipc::IPCResult RecvOnRejectPromise(const uint32_t& aPromiseId,
+ const uint32_t& aError,
+ const uint32_t& aSystemCode,
+ const nsCString& aErrorMessage);
+ ipc::IPCResult RecvOnSessionMessage(const nsCString& aSessionId,
+ const uint32_t& aMessageType,
+ nsTArray<uint8_t>&& aMessage);
+ ipc::IPCResult RecvOnSessionKeysChange(
+ const nsCString& aSessionId, nsTArray<CDMKeyInformation>&& aKeysInfo);
+ ipc::IPCResult RecvOnExpirationChange(const nsCString& aSessionId,
+ const double& aSecondsSinceEpoch);
+ ipc::IPCResult RecvOnSessionClosed(const nsCString& aSessionId);
+ ipc::IPCResult RecvOnQueryOutputProtectionStatus();
+ ipc::IPCResult RecvDecrypted(const uint32_t& aId, const uint32_t& aStatus,
+ ipc::Shmem&& aData);
+ ipc::IPCResult RecvDecryptFailed(const uint32_t& aId,
+ const uint32_t& aStatus);
+ ipc::IPCResult RecvOnDecoderInitDone(const uint32_t& aStatus);
+ ipc::IPCResult RecvDecodedShmem(const CDMVideoFrame& aFrame,
+ ipc::Shmem&& aShmem);
+ ipc::IPCResult RecvDecodedData(const CDMVideoFrame& aFrame,
+ nsTArray<uint8_t>&& aData);
+ ipc::IPCResult RecvDecodeFailed(const uint32_t& aStatus);
+ ipc::IPCResult RecvShutdown();
+ ipc::IPCResult RecvResetVideoDecoderComplete();
+ ipc::IPCResult RecvDrainComplete();
+ ipc::IPCResult RecvIncreaseShmemPoolSize();
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+ bool SendBufferToCDM(uint32_t aSizeInBytes);
+
+ void ReorderAndReturnOutput(RefPtr<VideoData>&& aFrame);
+
+ void RejectPromise(uint32_t aPromiseId, ErrorResult&& aException,
+ const nsCString& aErrorMessage);
+
+ void ResolvePromise(uint32_t aPromiseId);
+ // Helpers to reject our promise if we are shut down.
+ void RejectPromiseShutdown(uint32_t aPromiseId);
+ // Helper to reject our promise with an InvalidStateError and the given
+ // message.
+ void RejectPromiseWithStateError(uint32_t aPromiseId,
+ const nsCString& aErrorMessage);
+
+ // Complete the CDMs request for us to check protection status by responding
+ // to the CDM child with the requested info.
+ void CompleteQueryOutputProtectionStatus(bool aSuccess, uint32_t aLinkMask,
+ uint32_t aProtectionMask);
+
+ bool InitCDMInputBuffer(gmp::CDMInputBuffer& aBuffer, MediaRawData* aSample);
+
+ bool PurgeShmems();
+ bool EnsureSufficientShmems(size_t aVideoFrameSize);
+ already_AddRefed<VideoData> CreateVideoFrame(const CDMVideoFrame& aFrame,
+ Span<uint8_t> aData);
+
+ const uint32_t mPluginId;
+ GMPContentParent* mContentParent;
+ // Note: this pointer is a weak reference as ChromiumCDMProxy has a strong
+ // reference to the ChromiumCDMCallback.
+ ChromiumCDMCallback* mCDMCallback = nullptr;
+ nsTHashMap<nsUint32HashKey, uint32_t> mPromiseToCreateSessionToken;
+ nsTArray<RefPtr<DecryptJob>> mDecrypts;
+
+ MozPromiseHolder<InitPromise> mInitPromise;
+
+ MozPromiseHolder<MediaDataDecoder::InitPromise> mInitVideoDecoderPromise;
+ MozPromiseHolder<MediaDataDecoder::DecodePromise> mDecodePromise;
+
+ RefPtr<layers::ImageContainer> mImageContainer;
+ RefPtr<layers::KnowsCompositor> mKnowsCompositor;
+ VideoInfo mVideoInfo;
+ uint64_t mLastStreamOffset = 0;
+
+ MozPromiseHolder<MediaDataDecoder::FlushPromise> mFlushDecoderPromise;
+
+ size_t mVideoFrameBufferSize = 0;
+
+ // Count of the number of shmems in the set used to return decoded video
+ // frames from the CDM to Gecko.
+ uint32_t mVideoShmemsActive = 0;
+ // Maximum number of shmems to use to return decoded video frames.
+ uint32_t mVideoShmemLimit;
+
+ // Tracks if we have an outstanding request for output protection information.
+ // This will be set to true if the CDM requests the information and we haven't
+ // yet received it from up the stack and need to query up.
+ bool mAwaitingOutputProtectionInformation = false;
+ // The cached link mask for QueryOutputProtectionStatus related calls. If
+ // this isn't set we'll call up the stack to MediaKeys to request the
+ // information, otherwise we'll use the cached value and rely on MediaKeys
+ // to notify us if the mask changes.
+ Maybe<uint32_t> mOutputProtectionLinkMask;
+
+ bool mIsShutdown = false;
+ bool mVideoDecoderInitialized = false;
+ bool mActorDestroyed = false;
+ bool mAbnormalShutdown = false;
+
+ // The H.264 decoder in Widevine CDM versions 970 and later output in decode
+ // order rather than presentation order, so we reorder in presentation order
+ // before presenting. mMaxRefFrames is non-zero if we have an initialized
+ // decoder and we are decoding H.264. If so, it stores the maximum length of
+ // the reorder queue that we need. Note we may have multiple decoders for the
+ // life time of this object, but never more than one active at once.
+ uint32_t mMaxRefFrames = 0;
+ ReorderQueue mReorderQueue;
+
+#ifdef DEBUG
+ // The GMP thread. Used to MOZ_ASSERT methods run on the GMP thread.
+ const nsCOMPtr<nsISerialEventTarget> mGMPThread;
+#endif
+};
+
+} // namespace gmp
+} // namespace mozilla
+
+#endif // ChromiumCDMParent_h_
diff --git a/dom/media/gmp/ChromiumCDMProxy.cpp b/dom/media/gmp/ChromiumCDMProxy.cpp
new file mode 100644
index 0000000000..26d0475ad8
--- /dev/null
+++ b/dom/media/gmp/ChromiumCDMProxy.cpp
@@ -0,0 +1,636 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ChromiumCDMProxy.h"
+#include "ChromiumCDMCallbackProxy.h"
+#include "MediaResult.h"
+#include "mozilla/dom/MediaKeySession.h"
+#include "GMPUtils.h"
+#include "nsPrintfCString.h"
+#include "GMPService.h"
+#include "content_decryption_module.h"
+
+#define NS_DispatchToMainThread(...) CompileError_UseAbstractMainThreadInstead
+
+namespace mozilla {
+
+ChromiumCDMProxy::ChromiumCDMProxy(dom::MediaKeys* aKeys,
+ const nsAString& aKeySystem,
+ GMPCrashHelper* aCrashHelper,
+ bool aDistinctiveIdentifierRequired,
+ bool aPersistentStateRequired)
+ : CDMProxy(aKeys, aKeySystem, aDistinctiveIdentifierRequired,
+ aPersistentStateRequired),
+ mCrashHelper(aCrashHelper),
+ mCDMMutex("ChromiumCDMProxy"),
+ mGMPThread(GetGMPThread()) {
+ MOZ_ASSERT(NS_IsMainThread());
+}
+
+ChromiumCDMProxy::~ChromiumCDMProxy() {
+ EME_LOG("ChromiumCDMProxy::~ChromiumCDMProxy(this=%p)", this);
+}
+
+void ChromiumCDMProxy::Init(PromiseId aPromiseId, const nsAString& aOrigin,
+ const nsAString& aTopLevelOrigin,
+ const nsAString& aGMPName) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<GMPCrashHelper> helper(std::move(mCrashHelper));
+
+ NS_ENSURE_TRUE_VOID(!mKeys.IsNull());
+
+ EME_LOG("ChromiumCDMProxy::Init(this=%p, pid=%" PRIu32
+ ", origin=%s, topLevelOrigin=%s, "
+ "gmp=%s)",
+ this, aPromiseId, NS_ConvertUTF16toUTF8(aOrigin).get(),
+ NS_ConvertUTF16toUTF8(aTopLevelOrigin).get(),
+ NS_ConvertUTF16toUTF8(aGMPName).get());
+
+ if (!mGMPThread) {
+ RejectPromiseWithStateError(
+ aPromiseId, "Couldn't get GMP thread ChromiumCDMProxy::Init"_ns);
+ return;
+ }
+
+ if (aGMPName.IsEmpty()) {
+ RejectPromiseWithStateError(
+ aPromiseId, nsPrintfCString("Unknown GMP for keysystem '%s'",
+ NS_ConvertUTF16toUTF8(mKeySystem).get()));
+ return;
+ }
+
+ gmp::NodeIdParts nodeIdParts{nsString(aOrigin), nsString(aTopLevelOrigin),
+ nsString(aGMPName)};
+ nsCOMPtr<nsISerialEventTarget> thread = mGMPThread;
+ RefPtr<ChromiumCDMProxy> self(this);
+ nsCString keySystem = NS_ConvertUTF16toUTF8(mKeySystem);
+ RefPtr<Runnable> task(NS_NewRunnableFunction(
+ "ChromiumCDMProxy::Init",
+ [self, nodeIdParts, helper, aPromiseId, thread, keySystem]() -> void {
+ MOZ_ASSERT(self->IsOnOwnerThread());
+
+ RefPtr<gmp::GeckoMediaPluginService> service =
+ gmp::GeckoMediaPluginService::GetGeckoMediaPluginService();
+ if (!service) {
+ self->RejectPromiseWithStateError(
+ aPromiseId,
+ nsLiteralCString("Couldn't get GeckoMediaPluginService in "
+ "ChromiumCDMProxy::Init"));
+ return;
+ }
+ RefPtr<gmp::GetCDMParentPromise> promise =
+ service->GetCDM(nodeIdParts, keySystem, helper);
+ promise->Then(
+ thread, __func__,
+ [self, aPromiseId, thread](RefPtr<gmp::ChromiumCDMParent> cdm) {
+ // service->GetCDM succeeded
+ self->mCallback =
+ MakeUnique<ChromiumCDMCallbackProxy>(self, self->mMainThread);
+ cdm->Init(self->mCallback.get(),
+ self->mDistinctiveIdentifierRequired,
+ self->mPersistentStateRequired, self->mMainThread)
+ ->Then(
+ self->mMainThread, __func__,
+ [self, aPromiseId, cdm](bool /* unused */) {
+ // CDM init succeeded
+ {
+ MutexAutoLock lock(self->mCDMMutex);
+ self->mCDM = cdm;
+ }
+ if (self->mIsShutdown) {
+ self->RejectPromiseWithStateError(
+ aPromiseId, nsLiteralCString(
+ "ChromiumCDMProxy shutdown "
+ "during ChromiumCDMProxy::Init"));
+ // If shutdown happened while waiting to init, we
+ // need to explicitly shutdown the CDM to avoid it
+ // referencing this proxy which is on its way out.
+ self->ShutdownCDMIfExists();
+ return;
+ }
+ self->OnCDMCreated(aPromiseId);
+ },
+ [self, aPromiseId](MediaResult aResult) {
+ // CDM init failed.
+ ErrorResult rv;
+ // XXXbz MediaResult should really store a
+ // CopyableErrorResult or something. See
+ // <https://bugzilla.mozilla.org/show_bug.cgi?id=1612216>.
+ rv.Throw(aResult.Code());
+ self->RejectPromise(aPromiseId, std::move(rv),
+ aResult.Message());
+ });
+ },
+ [self, aPromiseId](MediaResult rv) {
+ // service->GetCDM failed
+ ErrorResult result;
+ // XXXbz MediaResult should really store a CopyableErrorResult or
+ // something. See
+ // <https://bugzilla.mozilla.org/show_bug.cgi?id=1612216>.
+ result.Throw(rv.Code());
+ self->RejectPromise(aPromiseId, std::move(result),
+ rv.Description());
+ });
+ }));
+
+ mGMPThread->Dispatch(task.forget());
+}
+
+void ChromiumCDMProxy::OnCDMCreated(uint32_t aPromiseId) {
+ EME_LOG("ChromiumCDMProxy::OnCDMCreated(this=%p, pid=%" PRIu32
+ ") isMainThread=%d",
+ this, aPromiseId, NS_IsMainThread());
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mKeys.IsNull()) {
+ return;
+ }
+ RefPtr<gmp::ChromiumCDMParent> cdm = GetCDMParent();
+ // This should only be called once the CDM has been created.
+ MOZ_ASSERT(cdm);
+ if (cdm) {
+ mKeys->OnCDMCreated(aPromiseId, cdm->PluginId());
+ } else {
+ // No CDM? Shouldn't be possible, but reject the promise anyway...
+ constexpr auto err = "Null CDM in OnCDMCreated()"_ns;
+ ErrorResult rv;
+ rv.ThrowInvalidStateError(err);
+ mKeys->RejectPromise(aPromiseId, std::move(rv), err);
+ }
+}
+
+void ChromiumCDMProxy::ShutdownCDMIfExists() {
+ EME_LOG(
+ "ChromiumCDMProxy::ShutdownCDMIfExists(this=%p) mCDM=%p, mIsShutdown=%s",
+ this, mCDM.get(), mIsShutdown ? "true" : "false");
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mGMPThread);
+ MOZ_ASSERT(mIsShutdown,
+ "Should only shutdown the CDM if the proxy is shutting down");
+ RefPtr<gmp::ChromiumCDMParent> cdm;
+ {
+ MutexAutoLock lock(mCDMMutex);
+ cdm.swap(mCDM);
+ }
+ if (cdm) {
+ // We need to keep this proxy alive until the parent has finished its
+ // Shutdown (as it may still try to use the proxy until then).
+ RefPtr<ChromiumCDMProxy> self(this);
+ nsCOMPtr<nsIRunnable> task = NS_NewRunnableFunction(
+ "ChromiumCDMProxy::Shutdown", [self, cdm]() { cdm->Shutdown(); });
+ mGMPThread->Dispatch(task.forget());
+ }
+}
+
+#ifdef DEBUG
+bool ChromiumCDMProxy::IsOnOwnerThread() {
+ return mGMPThread && mGMPThread->IsOnCurrentThread();
+}
+#endif
+
+static uint32_t ToCDMSessionType(dom::MediaKeySessionType aSessionType) {
+ switch (aSessionType) {
+ case dom::MediaKeySessionType::Temporary:
+ return static_cast<uint32_t>(cdm::kTemporary);
+ case dom::MediaKeySessionType::Persistent_license:
+ return static_cast<uint32_t>(cdm::kPersistentLicense);
+ default:
+ return static_cast<uint32_t>(cdm::kTemporary);
+ };
+};
+
+static uint32_t ToCDMInitDataType(const nsAString& aInitDataType) {
+ if (aInitDataType.EqualsLiteral("cenc")) {
+ return static_cast<uint32_t>(cdm::kCenc);
+ }
+ if (aInitDataType.EqualsLiteral("webm")) {
+ return static_cast<uint32_t>(cdm::kWebM);
+ }
+ if (aInitDataType.EqualsLiteral("keyids")) {
+ return static_cast<uint32_t>(cdm::kKeyIds);
+ }
+ return static_cast<uint32_t>(cdm::kCenc);
+}
+
+void ChromiumCDMProxy::CreateSession(uint32_t aCreateSessionToken,
+ dom::MediaKeySessionType aSessionType,
+ PromiseId aPromiseId,
+ const nsAString& aInitDataType,
+ nsTArray<uint8_t>& aInitData) {
+ MOZ_ASSERT(NS_IsMainThread());
+ EME_LOG("ChromiumCDMProxy::CreateSession(this=%p, token=%" PRIu32
+ ", type=%d, pid=%" PRIu32
+ ") "
+ "initDataLen=%zu",
+ this, aCreateSessionToken, (int)aSessionType, aPromiseId,
+ aInitData.Length());
+
+ uint32_t sessionType = ToCDMSessionType(aSessionType);
+ uint32_t initDataType = ToCDMInitDataType(aInitDataType);
+
+ RefPtr<gmp::ChromiumCDMParent> cdm = GetCDMParent();
+ if (!cdm) {
+ RejectPromiseWithStateError(aPromiseId, "Null CDM in CreateSession"_ns);
+ return;
+ }
+
+ mGMPThread->Dispatch(NewRunnableMethod<uint32_t, uint32_t, uint32_t, uint32_t,
+ nsTArray<uint8_t>>(
+ "gmp::ChromiumCDMParent::CreateSession", cdm,
+ &gmp::ChromiumCDMParent::CreateSession, aCreateSessionToken, sessionType,
+ initDataType, aPromiseId, std::move(aInitData)));
+}
+
+void ChromiumCDMProxy::LoadSession(PromiseId aPromiseId,
+ dom::MediaKeySessionType aSessionType,
+ const nsAString& aSessionId) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<gmp::ChromiumCDMParent> cdm = GetCDMParent();
+ if (!cdm) {
+ RejectPromiseWithStateError(aPromiseId, "Null CDM in LoadSession"_ns);
+ return;
+ }
+
+ mGMPThread->Dispatch(NewRunnableMethod<uint32_t, uint32_t, nsString>(
+ "gmp::ChromiumCDMParent::LoadSession", cdm,
+ &gmp::ChromiumCDMParent::LoadSession, aPromiseId,
+ ToCDMSessionType(aSessionType), aSessionId));
+}
+
+void ChromiumCDMProxy::SetServerCertificate(PromiseId aPromiseId,
+ nsTArray<uint8_t>& aCert) {
+ MOZ_ASSERT(NS_IsMainThread());
+ EME_LOG("ChromiumCDMProxy::SetServerCertificate(this=%p, pid=%" PRIu32
+ ") certLen=%zu",
+ this, aPromiseId, aCert.Length());
+
+ RefPtr<gmp::ChromiumCDMParent> cdm = GetCDMParent();
+ if (!cdm) {
+ RejectPromiseWithStateError(aPromiseId,
+ "Null CDM in SetServerCertificate"_ns);
+ return;
+ }
+
+ mGMPThread->Dispatch(NewRunnableMethod<uint32_t, nsTArray<uint8_t>>(
+ "gmp::ChromiumCDMParent::SetServerCertificate", cdm,
+ &gmp::ChromiumCDMParent::SetServerCertificate, aPromiseId,
+ std::move(aCert)));
+}
+
+void ChromiumCDMProxy::UpdateSession(const nsAString& aSessionId,
+ PromiseId aPromiseId,
+ nsTArray<uint8_t>& aResponse) {
+ MOZ_ASSERT(NS_IsMainThread());
+ EME_LOG("ChromiumCDMProxy::UpdateSession(this=%p, sid='%s', pid=%" PRIu32
+ ") "
+ "responseLen=%zu",
+ this, NS_ConvertUTF16toUTF8(aSessionId).get(), aPromiseId,
+ aResponse.Length());
+
+ RefPtr<gmp::ChromiumCDMParent> cdm = GetCDMParent();
+ if (!cdm) {
+ RejectPromiseWithStateError(aPromiseId, "Null CDM in UpdateSession"_ns);
+ return;
+ }
+ mGMPThread->Dispatch(
+ NewRunnableMethod<nsCString, uint32_t, nsTArray<uint8_t>>(
+ "gmp::ChromiumCDMParent::UpdateSession", cdm,
+ &gmp::ChromiumCDMParent::UpdateSession,
+ NS_ConvertUTF16toUTF8(aSessionId), aPromiseId, std::move(aResponse)));
+}
+
+void ChromiumCDMProxy::CloseSession(const nsAString& aSessionId,
+ PromiseId aPromiseId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ EME_LOG("ChromiumCDMProxy::CloseSession(this=%p, sid='%s', pid=%" PRIu32 ")",
+ this, NS_ConvertUTF16toUTF8(aSessionId).get(), aPromiseId);
+
+ RefPtr<gmp::ChromiumCDMParent> cdm = GetCDMParent();
+ if (!cdm) {
+ RejectPromiseWithStateError(aPromiseId, "Null CDM in CloseSession"_ns);
+ return;
+ }
+ mGMPThread->Dispatch(NewRunnableMethod<nsCString, uint32_t>(
+ "gmp::ChromiumCDMParent::CloseSession", cdm,
+ &gmp::ChromiumCDMParent::CloseSession, NS_ConvertUTF16toUTF8(aSessionId),
+ aPromiseId));
+}
+
+void ChromiumCDMProxy::RemoveSession(const nsAString& aSessionId,
+ PromiseId aPromiseId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ EME_LOG("ChromiumCDMProxy::RemoveSession(this=%p, sid='%s', pid=%" PRIu32 ")",
+ this, NS_ConvertUTF16toUTF8(aSessionId).get(), aPromiseId);
+
+ RefPtr<gmp::ChromiumCDMParent> cdm = GetCDMParent();
+ if (!cdm) {
+ RejectPromiseWithStateError(aPromiseId, "Null CDM in RemoveSession"_ns);
+ return;
+ }
+ mGMPThread->Dispatch(NewRunnableMethod<nsCString, uint32_t>(
+ "gmp::ChromiumCDMParent::RemoveSession", cdm,
+ &gmp::ChromiumCDMParent::RemoveSession, NS_ConvertUTF16toUTF8(aSessionId),
+ aPromiseId));
+}
+
+void ChromiumCDMProxy::QueryOutputProtectionStatus() {
+ MOZ_ASSERT(NS_IsMainThread());
+ EME_LOG("ChromiumCDMProxy::QueryOutputProtectionStatus(this=%p)", this);
+
+ if (mKeys.IsNull()) {
+ EME_LOG(
+ "ChromiumCDMProxy::QueryOutputProtectionStatus(this=%p), mKeys "
+ "missing!",
+ this);
+ // If we can't get mKeys, we're probably in shutdown. But do our best to
+ // respond to the request and indicate the check failed.
+ NotifyOutputProtectionStatus(OutputProtectionCheckStatus::CheckFailed,
+ OutputProtectionCaptureStatus::Unused);
+ return;
+ }
+ // The keys will call back via `NotifyOutputProtectionStatus` to notify the
+ // result of the check.
+ mKeys->CheckIsElementCapturePossible();
+}
+
+void ChromiumCDMProxy::NotifyOutputProtectionStatus(
+ OutputProtectionCheckStatus aCheckStatus,
+ OutputProtectionCaptureStatus aCaptureStatus) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // If the check failed aCaptureStatus should be unused, otherwise not.
+ MOZ_ASSERT_IF(aCheckStatus == OutputProtectionCheckStatus::CheckFailed,
+ aCaptureStatus == OutputProtectionCaptureStatus::Unused);
+ MOZ_ASSERT_IF(aCheckStatus == OutputProtectionCheckStatus::CheckSuccessful,
+ aCaptureStatus != OutputProtectionCaptureStatus::Unused);
+ EME_LOG(
+ "ChromiumCDMProxy::NotifyOutputProtectionStatus(this=%p) "
+ "aCheckStatus=%" PRIu8 " aCaptureStatus=%" PRIu8,
+ this, static_cast<uint8_t>(aCheckStatus),
+ static_cast<uint8_t>(aCaptureStatus));
+
+ RefPtr<gmp::ChromiumCDMParent> cdm = GetCDMParent();
+ if (!cdm) {
+ // If we're in shutdown the CDM may have been cleared while a notification
+ // is in flight. If this happens outside of shutdown we have a bug.
+ MOZ_ASSERT(mIsShutdown);
+ return;
+ }
+
+ uint32_t linkMask{};
+ uint32_t protectionMask{}; // Unused/always zeroed.
+ if (aCheckStatus == OutputProtectionCheckStatus::CheckSuccessful &&
+ aCaptureStatus == OutputProtectionCaptureStatus::CapturePossilbe) {
+ // The result indicates the capture is possible, so set the mask
+ // to indicate this.
+ linkMask |= cdm::OutputLinkTypes::kLinkTypeNetwork;
+ }
+ mGMPThread->Dispatch(NewRunnableMethod<bool, uint32_t, uint32_t>(
+ "gmp::ChromiumCDMParent::NotifyOutputProtectionStatus", cdm,
+ &gmp::ChromiumCDMParent::NotifyOutputProtectionStatus,
+ aCheckStatus == OutputProtectionCheckStatus::CheckSuccessful, linkMask,
+ protectionMask));
+}
+
+void ChromiumCDMProxy::Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ EME_LOG("ChromiumCDMProxy::Shutdown(this=%p) mCDM=%p, mIsShutdown=%s", this,
+ mCDM.get(), mIsShutdown ? "true" : "false");
+ if (mIsShutdown) {
+ return;
+ }
+ mIsShutdown = true;
+ mKeys.Clear();
+ ShutdownCDMIfExists();
+}
+
+void ChromiumCDMProxy::RejectPromise(PromiseId aId, ErrorResult&& aException,
+ const nsCString& aReason) {
+ if (!NS_IsMainThread()) {
+ // Use CopyableErrorResult to store our exception in the runnable,
+ // because ErrorResult is not OK to move across threads.
+ mMainThread->Dispatch(
+ NewRunnableMethod<PromiseId, StoreCopyPassByRRef<CopyableErrorResult>,
+ nsCString>(
+ "ChromiumCDMProxy::RejectPromise", this,
+ &ChromiumCDMProxy::RejectPromiseOnMainThread, aId,
+ std::move(aException), aReason),
+ NS_DISPATCH_NORMAL);
+ return;
+ }
+ EME_LOG("ChromiumCDMProxy::RejectPromise(this=%p, pid=%" PRIu32
+ ", code=0x%x, "
+ "reason='%s')",
+ this, aId, aException.ErrorCodeAsInt(), aReason.get());
+ if (!mKeys.IsNull()) {
+ mKeys->RejectPromise(aId, std::move(aException), aReason);
+ } else {
+ // We don't have a MediaKeys object to pass the exception to, so silence
+ // the exception to avoid it asserting due to being unused.
+ aException.SuppressException();
+ }
+}
+
+void ChromiumCDMProxy::RejectPromiseWithStateError(PromiseId aId,
+ const nsCString& aReason) {
+ ErrorResult rv;
+ rv.ThrowInvalidStateError(aReason);
+ RejectPromise(aId, std::move(rv), aReason);
+}
+
+void ChromiumCDMProxy::RejectPromiseOnMainThread(
+ PromiseId aId, CopyableErrorResult&& aException, const nsCString& aReason) {
+ // Moving into or out of a non-copyable ErrorResult will assert that both
+ // ErorResults are from our current thread. Avoid the assertion by moving
+ // into a current-thread CopyableErrorResult first. Note that this is safe,
+ // because CopyableErrorResult never holds state that can't move across
+ // threads.
+ CopyableErrorResult rv(std::move(aException));
+ RejectPromise(aId, std::move(rv), aReason);
+}
+
+void ChromiumCDMProxy::ResolvePromise(PromiseId aId) {
+ if (!NS_IsMainThread()) {
+ mMainThread->Dispatch(
+ NewRunnableMethod<PromiseId>("ChromiumCDMProxy::ResolvePromise", this,
+ &ChromiumCDMProxy::ResolvePromise, aId),
+ NS_DISPATCH_NORMAL);
+ return;
+ }
+
+ EME_LOG("ChromiumCDMProxy::ResolvePromise(this=%p, pid=%" PRIu32 ")", this,
+ aId);
+ if (!mKeys.IsNull()) {
+ mKeys->ResolvePromise(aId);
+ } else {
+ NS_WARNING("ChromiumCDMProxy unable to resolve promise!");
+ }
+}
+
+void ChromiumCDMProxy::OnSetSessionId(uint32_t aCreateSessionToken,
+ const nsAString& aSessionId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ EME_LOG("ChromiumCDMProxy::OnSetSessionId(this=%p, token=%" PRIu32
+ ", sid='%s')",
+ this, aCreateSessionToken, NS_ConvertUTF16toUTF8(aSessionId).get());
+
+ if (mKeys.IsNull()) {
+ return;
+ }
+ RefPtr<dom::MediaKeySession> session(
+ mKeys->GetPendingSession(aCreateSessionToken));
+ if (session) {
+ session->SetSessionId(aSessionId);
+ }
+}
+
+void ChromiumCDMProxy::OnResolveLoadSessionPromise(uint32_t aPromiseId,
+ bool aSuccess) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mKeys.IsNull()) {
+ return;
+ }
+ mKeys->OnSessionLoaded(aPromiseId, aSuccess);
+}
+
+void ChromiumCDMProxy::OnResolvePromiseWithKeyStatus(
+ uint32_t aPromiseId, dom::MediaKeyStatus aKeyStatus) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mKeys.IsNull()) {
+ return;
+ }
+ mKeys->ResolvePromiseWithKeyStatus(aPromiseId, aKeyStatus);
+}
+
+void ChromiumCDMProxy::OnSessionMessage(const nsAString& aSessionId,
+ dom::MediaKeyMessageType aMessageType,
+ const nsTArray<uint8_t>& aMessage) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mKeys.IsNull()) {
+ return;
+ }
+ RefPtr<dom::MediaKeySession> session(mKeys->GetSession(aSessionId));
+ if (session) {
+ session->DispatchKeyMessage(aMessageType, aMessage);
+ }
+}
+
+void ChromiumCDMProxy::OnKeyStatusesChange(const nsAString& aSessionId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mKeys.IsNull()) {
+ return;
+ }
+ RefPtr<dom::MediaKeySession> session(mKeys->GetSession(aSessionId));
+ if (session) {
+ session->DispatchKeyStatusesChange();
+ }
+}
+
+void ChromiumCDMProxy::OnExpirationChange(const nsAString& aSessionId,
+ UnixTime aExpiryTime) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mKeys.IsNull()) {
+ return;
+ }
+ RefPtr<dom::MediaKeySession> session(mKeys->GetSession(aSessionId));
+ if (session) {
+ // Expiry of 0 is interpreted as "never expire". See bug 1345341.
+ double t = (aExpiryTime == 0) ? std::numeric_limits<double>::quiet_NaN()
+ : static_cast<double>(aExpiryTime);
+ session->SetExpiration(t);
+ }
+}
+
+void ChromiumCDMProxy::OnSessionClosed(const nsAString& aSessionId) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ bool keyStatusesChange = false;
+ {
+ auto caps = Capabilites().Lock();
+ keyStatusesChange = caps->RemoveKeysForSession(nsString(aSessionId));
+ }
+ if (keyStatusesChange) {
+ OnKeyStatusesChange(aSessionId);
+ }
+ if (mKeys.IsNull()) {
+ return;
+ }
+ RefPtr<dom::MediaKeySession> session(mKeys->GetSession(aSessionId));
+ if (session) {
+ session->OnClosed();
+ }
+}
+
+void ChromiumCDMProxy::OnDecrypted(uint32_t aId, DecryptStatus aResult,
+ const nsTArray<uint8_t>& aDecryptedData) {}
+
+void ChromiumCDMProxy::OnSessionError(const nsAString& aSessionId,
+ nsresult aException, uint32_t aSystemCode,
+ const nsAString& aMsg) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mKeys.IsNull()) {
+ return;
+ }
+ RefPtr<dom::MediaKeySession> session(mKeys->GetSession(aSessionId));
+ if (session) {
+ session->DispatchKeyError(aSystemCode);
+ }
+ LogToConsole(aMsg);
+}
+
+void ChromiumCDMProxy::OnRejectPromise(uint32_t aPromiseId,
+ ErrorResult&& aException,
+ const nsCString& aMsg) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RejectPromise(aPromiseId, std::move(aException), aMsg);
+}
+
+RefPtr<DecryptPromise> ChromiumCDMProxy::Decrypt(MediaRawData* aSample) {
+ RefPtr<gmp::ChromiumCDMParent> cdm = GetCDMParent();
+ if (!cdm) {
+ return DecryptPromise::CreateAndReject(
+ DecryptResult(eme::AbortedErr, aSample), __func__);
+ }
+ RefPtr<MediaRawData> sample = aSample;
+ return InvokeAsync(mGMPThread, __func__,
+ [cdm, sample]() { return cdm->Decrypt(sample); });
+}
+
+void ChromiumCDMProxy::GetStatusForPolicy(PromiseId aPromiseId,
+ const nsAString& aMinHdcpVersion) {
+ MOZ_ASSERT(NS_IsMainThread());
+ EME_LOG("ChromiumCDMProxy::GetStatusForPolicy(this=%p, pid=%" PRIu32
+ ") minHdcpVersion=%s",
+ this, aPromiseId, NS_ConvertUTF16toUTF8(aMinHdcpVersion).get());
+
+ RefPtr<gmp::ChromiumCDMParent> cdm = GetCDMParent();
+ if (!cdm) {
+ RejectPromiseWithStateError(aPromiseId,
+ "Null CDM in GetStatusForPolicy"_ns);
+ return;
+ }
+
+ mGMPThread->Dispatch(NewRunnableMethod<uint32_t, nsCString>(
+ "gmp::ChromiumCDMParent::GetStatusForPolicy", cdm,
+ &gmp::ChromiumCDMParent::GetStatusForPolicy, aPromiseId,
+ NS_ConvertUTF16toUTF8(aMinHdcpVersion)));
+}
+
+void ChromiumCDMProxy::Terminated() {
+ if (!mKeys.IsNull()) {
+ mKeys->Terminated();
+ }
+}
+
+already_AddRefed<gmp::ChromiumCDMParent> ChromiumCDMProxy::GetCDMParent() {
+ MutexAutoLock lock(mCDMMutex);
+ RefPtr<gmp::ChromiumCDMParent> cdm = mCDM;
+ return cdm.forget();
+}
+
+} // namespace mozilla
+
+#undef NS_DispatchToMainThread
diff --git a/dom/media/gmp/ChromiumCDMProxy.h b/dom/media/gmp/ChromiumCDMProxy.h
new file mode 100644
index 0000000000..7f60153ae7
--- /dev/null
+++ b/dom/media/gmp/ChromiumCDMProxy.h
@@ -0,0 +1,136 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef ChromiumCDMProxy_h_
+#define ChromiumCDMProxy_h_
+
+#include "mozilla/AbstractThread.h"
+#include "mozilla/CDMProxy.h"
+#include "ChromiumCDMParent.h"
+
+namespace mozilla {
+
+class ErrorResult;
+class MediaRawData;
+class DecryptJob;
+class ChromiumCDMCallbackProxy;
+class ChromiumCDMProxy : public CDMProxy {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ChromiumCDMProxy, override)
+
+ ChromiumCDMProxy(dom::MediaKeys* aKeys, const nsAString& aKeySystem,
+ GMPCrashHelper* aCrashHelper,
+ bool aAllowDistinctiveIdentifier,
+ bool aAllowPersistentState);
+
+ void Init(PromiseId aPromiseId, const nsAString& aOrigin,
+ const nsAString& aTopLevelOrigin,
+ const nsAString& aGMPName) override;
+
+ void CreateSession(uint32_t aCreateSessionToken,
+ dom::MediaKeySessionType aSessionType,
+ PromiseId aPromiseId, const nsAString& aInitDataType,
+ nsTArray<uint8_t>& aInitData) override;
+
+ void LoadSession(PromiseId aPromiseId, dom::MediaKeySessionType aSessionType,
+ const nsAString& aSessionId) override;
+
+ void SetServerCertificate(PromiseId aPromiseId,
+ nsTArray<uint8_t>& aCert) override;
+
+ void UpdateSession(const nsAString& aSessionId, PromiseId aPromiseId,
+ nsTArray<uint8_t>& aResponse) override;
+
+ void CloseSession(const nsAString& aSessionId, PromiseId aPromiseId) override;
+
+ void RemoveSession(const nsAString& aSessionId,
+ PromiseId aPromiseId) override;
+
+ void QueryOutputProtectionStatus() override;
+
+ void NotifyOutputProtectionStatus(
+ OutputProtectionCheckStatus aCheckStatus,
+ OutputProtectionCaptureStatus aCaptureStatus) override;
+
+ void Shutdown() override;
+
+ void Terminated() override;
+
+ void OnSetSessionId(uint32_t aCreateSessionToken,
+ const nsAString& aSessionId) override;
+
+ void OnResolveLoadSessionPromise(uint32_t aPromiseId, bool aSuccess) override;
+
+ void OnSessionMessage(const nsAString& aSessionId,
+ dom::MediaKeyMessageType aMessageType,
+ const nsTArray<uint8_t>& aMessage) override;
+
+ void OnExpirationChange(const nsAString& aSessionId,
+ UnixTime aExpiryTime) override;
+
+ void OnSessionClosed(const nsAString& aSessionId) override;
+
+ void OnSessionError(const nsAString& aSessionId, nsresult aException,
+ uint32_t aSystemCode, const nsAString& aMsg) override;
+
+ void OnRejectPromise(uint32_t aPromiseId, ErrorResult&& aException,
+ const nsCString& aMsg) override;
+
+ RefPtr<DecryptPromise> Decrypt(MediaRawData* aSample) override;
+
+ void OnDecrypted(uint32_t aId, DecryptStatus aResult,
+ const nsTArray<uint8_t>& aDecryptedData) override;
+
+ void RejectPromise(PromiseId aId, ErrorResult&& aException,
+ const nsCString& aReason) override;
+ // Reject promise with an InvalidStateError and the given message.
+ void RejectPromiseWithStateError(PromiseId aId, const nsCString& aReason);
+ // For use for moving rejections from off-main thread.
+ void RejectPromiseOnMainThread(PromiseId aId,
+ CopyableErrorResult&& aException,
+ const nsCString& aReason);
+
+ void ResolvePromise(PromiseId aId) override;
+
+ void OnKeyStatusesChange(const nsAString& aSessionId) override;
+
+ void GetStatusForPolicy(PromiseId aPromiseId,
+ const nsAString& aMinHdcpVersion) override;
+
+#ifdef DEBUG
+ bool IsOnOwnerThread() override;
+#endif
+
+ ChromiumCDMProxy* AsChromiumCDMProxy() override { return this; }
+
+ // Threadsafe. Note this may return a reference to a shutdown
+ // CDM, which will fail on all operations.
+ already_AddRefed<gmp::ChromiumCDMParent> GetCDMParent();
+
+ void OnResolvePromiseWithKeyStatus(uint32_t aPromiseId,
+ dom::MediaKeyStatus aKeyStatus);
+
+ private:
+ void OnCDMCreated(uint32_t aPromiseId);
+ void ShutdownCDMIfExists();
+
+ ~ChromiumCDMProxy();
+
+ // True if Shutdown() has been called. Should only be read and written on
+ // main thread.
+ bool mIsShutdown = false;
+
+ RefPtr<GMPCrashHelper> mCrashHelper;
+
+ Mutex mCDMMutex MOZ_UNANNOTATED;
+ RefPtr<gmp::ChromiumCDMParent> mCDM;
+ nsCOMPtr<nsISerialEventTarget> mGMPThread;
+ UniquePtr<ChromiumCDMCallbackProxy> mCallback;
+};
+
+} // namespace mozilla
+
+#endif // ChromiumCDMProxy_h_
diff --git a/dom/media/gmp/DecryptJob.cpp b/dom/media/gmp/DecryptJob.cpp
new file mode 100644
index 0000000000..affda3668f
--- /dev/null
+++ b/dom/media/gmp/DecryptJob.cpp
@@ -0,0 +1,46 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DecryptJob.h"
+#include "mozilla/Atomics.h"
+
+namespace mozilla {
+
+static Atomic<uint32_t> sDecryptJobInstanceCount(0u);
+
+DecryptJob::DecryptJob(MediaRawData* aSample)
+ : mId(++sDecryptJobInstanceCount), mSample(aSample) {}
+
+RefPtr<DecryptPromise> DecryptJob::Ensure() {
+ return mPromise.Ensure(__func__);
+}
+
+void DecryptJob::PostResult(DecryptStatus aResult) {
+ nsTArray<uint8_t> empty;
+ PostResult(aResult, empty);
+}
+
+void DecryptJob::PostResult(DecryptStatus aResult,
+ Span<const uint8_t> aDecryptedData) {
+ if (aDecryptedData.Length() != mSample->Size()) {
+ NS_WARNING("CDM returned incorrect number of decrypted bytes");
+ }
+ if (aResult == eme::Ok) {
+ UniquePtr<MediaRawDataWriter> writer(mSample->CreateWriter());
+ PodCopy(writer->Data(), aDecryptedData.Elements(),
+ std::min<size_t>(aDecryptedData.Length(), mSample->Size()));
+ } else if (aResult == eme::NoKeyErr) {
+ NS_WARNING("CDM returned NoKeyErr");
+ // We still have the encrypted sample, so we can re-enqueue it to be
+ // decrypted again once the key is usable again.
+ } else {
+ nsAutoCString str("CDM returned decode failure DecryptStatus=");
+ str.AppendInt(aResult);
+ NS_WARNING(str.get());
+ }
+ mPromise.Resolve(DecryptResult(aResult, mSample), __func__);
+}
+
+} // namespace mozilla
diff --git a/dom/media/gmp/DecryptJob.h b/dom/media/gmp/DecryptJob.h
new file mode 100644
index 0000000000..c4f25206e3
--- /dev/null
+++ b/dom/media/gmp/DecryptJob.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DecryptJob_h_
+#define DecryptJob_h_
+
+#include "mozilla/CDMProxy.h"
+#include "mozilla/Span.h"
+
+namespace mozilla {
+
+class DecryptJob {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DecryptJob)
+
+ explicit DecryptJob(MediaRawData* aSample);
+
+ void PostResult(DecryptStatus aResult, Span<const uint8_t> aDecryptedData);
+ void PostResult(DecryptStatus aResult);
+
+ RefPtr<DecryptPromise> Ensure();
+
+ const uint32_t mId;
+ RefPtr<MediaRawData> mSample;
+
+ private:
+ ~DecryptJob() = default;
+ MozPromiseHolder<DecryptPromise> mPromise;
+};
+
+} // namespace mozilla
+
+#endif // DecryptJob_h_
diff --git a/dom/media/gmp/GMPCallbackBase.h b/dom/media/gmp/GMPCallbackBase.h
new file mode 100644
index 0000000000..7284f18e86
--- /dev/null
+++ b/dom/media/gmp/GMPCallbackBase.h
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GMPCallbackBase_h_
+#define GMPCallbackBase_h_
+
+#include "nsISupportsImpl.h"
+
+class GMPCallbackBase {
+ public:
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+ virtual ~GMPCallbackBase() = default;
+
+ // The GMP code will call this if the codec crashes or shuts down. It's
+ // expected that the consumer (destination of this callback) will respond
+ // by dropping their reference to the proxy, allowing the proxy/parent to
+ // be destroyed.
+ virtual void Terminated() = 0;
+};
+
+#endif
diff --git a/dom/media/gmp/GMPChild.cpp b/dom/media/gmp/GMPChild.cpp
new file mode 100644
index 0000000000..0d485345bc
--- /dev/null
+++ b/dom/media/gmp/GMPChild.cpp
@@ -0,0 +1,657 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GMPChild.h"
+
+#include "base/command_line.h"
+#include "base/task.h"
+#include "ChildProfilerController.h"
+#include "ChromiumCDMAdapter.h"
+#ifdef XP_LINUX
+# include "dlfcn.h"
+#endif
+#include "gmp-video-decode.h"
+#include "gmp-video-encode.h"
+#include "GMPContentChild.h"
+#include "GMPLoader.h"
+#include "GMPLog.h"
+#include "GMPPlatform.h"
+#include "GMPProcessChild.h"
+#include "GMPProcessParent.h"
+#include "GMPUtils.h"
+#include "GMPVideoDecoderChild.h"
+#include "GMPVideoEncoderChild.h"
+#include "GMPVideoHost.h"
+#include "mozilla/Algorithm.h"
+#include "mozilla/FOGIPC.h"
+#include "mozilla/glean/GleanMetrics.h"
+#include "mozilla/ipc/CrashReporterClient.h"
+#include "mozilla/ipc/Endpoint.h"
+#include "mozilla/ipc/ProcessChild.h"
+#include "mozilla/TextUtils.h"
+#include "nsDebugImpl.h"
+#include "nsExceptionHandler.h"
+#include "nsIFile.h"
+#include "nsReadableUtils.h"
+#include "nsThreadManager.h"
+#include "nsXULAppAPI.h"
+#include "nsIXULRuntime.h"
+#include "prio.h"
+#ifdef XP_WIN
+# include <stdlib.h> // for _exit()
+# include "WinUtils.h"
+#else
+# include <unistd.h> // for _exit()
+#endif
+
+using namespace mozilla::ipc;
+
+namespace mozilla {
+
+#define GMP_CHILD_LOG_DEBUG(x, ...) \
+ GMP_LOG_DEBUG("GMPChild[pid=%d] " x, (int)base::GetCurrentProcId(), \
+ ##__VA_ARGS__)
+
+namespace gmp {
+
+GMPChild::GMPChild()
+ : mGMPMessageLoop(MessageLoop::current()), mGMPLoader(nullptr) {
+ GMP_CHILD_LOG_DEBUG("GMPChild ctor");
+ nsDebugImpl::SetMultiprocessMode("GMP");
+}
+
+GMPChild::~GMPChild() {
+ GMP_CHILD_LOG_DEBUG("GMPChild dtor");
+#ifdef XP_LINUX
+ for (auto& libHandle : mLibHandles) {
+ dlclose(libHandle);
+ }
+#endif
+}
+
+bool GMPChild::Init(const nsAString& aPluginPath,
+ mozilla::ipc::UntypedEndpoint&& aEndpoint) {
+ GMP_CHILD_LOG_DEBUG("%s pluginPath=%s", __FUNCTION__,
+ NS_ConvertUTF16toUTF8(aPluginPath).get());
+
+ // GMPChild needs nsThreadManager to create the ProfilerChild thread.
+ // It is also used on debug builds for the sandbox tests.
+ if (NS_WARN_IF(NS_FAILED(nsThreadManager::get().Init()))) {
+ return false;
+ }
+
+ if (NS_WARN_IF(!aEndpoint.Bind(this))) {
+ return false;
+ }
+
+ CrashReporterClient::InitSingleton(this);
+
+ mPluginPath = aPluginPath;
+
+ return true;
+}
+
+mozilla::ipc::IPCResult GMPChild::RecvProvideStorageId(
+ const nsCString& aStorageId) {
+ GMP_CHILD_LOG_DEBUG("%s", __FUNCTION__);
+ mStorageId = aStorageId;
+ return IPC_OK();
+}
+
+GMPErr GMPChild::GetAPI(const char* aAPIName, void* aHostAPI, void** aPluginAPI,
+ const nsACString& aKeySystem) {
+ if (!mGMPLoader) {
+ return GMPGenericErr;
+ }
+ return mGMPLoader->GetAPI(aAPIName, aHostAPI, aPluginAPI, aKeySystem);
+}
+
+mozilla::ipc::IPCResult GMPChild::RecvPreloadLibs(const nsCString& aLibs) {
+ // Pre-load libraries that may need to be used by the EME plugin but that
+ // can't be loaded after the sandbox has started.
+#ifdef XP_WIN
+ // Items in this must be lowercase!
+ constexpr static const char16_t* whitelist[] = {
+ u"dxva2.dll", // Get monitor information
+ u"evr.dll", // MFGetStrideForBitmapInfoHeader
+ u"freebl3.dll", // NSS for clearkey CDM
+ u"mfplat.dll", // MFCreateSample, MFCreateAlignedMemoryBuffer,
+ // MFCreateMediaType
+ u"msmpeg2vdec.dll", // H.264 decoder
+ u"nss3.dll", // NSS for clearkey CDM
+ u"ole32.dll", // required for OPM
+ u"oleaut32.dll", // For _bstr_t use in libwebrtc, see bug 1788592
+ u"psapi.dll", // For GetMappedFileNameW, see bug 1383611
+ u"softokn3.dll", // NSS for clearkey CDM
+ u"winmm.dll", // Dependency for widevine
+ };
+ constexpr static bool (*IsASCII)(const char16_t*) =
+ IsAsciiNullTerminated<char16_t>;
+ static_assert(AllOf(std::begin(whitelist), std::end(whitelist), IsASCII),
+ "Items in the whitelist must not contain non-ASCII "
+ "characters!");
+
+ nsTArray<nsCString> libs;
+ SplitAt(", ", aLibs, libs);
+ for (nsCString lib : libs) {
+ ToLowerCase(lib);
+ for (const char16_t* whiteListedLib : whitelist) {
+ if (nsDependentString(whiteListedLib)
+ .EqualsASCII(lib.Data(), lib.Length())) {
+ LoadLibraryW(char16ptr_t(whiteListedLib));
+ break;
+ }
+ }
+ }
+#elif defined(XP_LINUX)
+ constexpr static const char* whitelist[] = {
+ // NSS libraries used by clearkey.
+ "libfreeblpriv3.so",
+ "libsoftokn3.so",
+ // glibc libraries merged into libc.so.6; see bug 1725828 and
+ // the corresponding code in GMPParent.cpp.
+ "libdl.so.2",
+ "libpthread.so.0",
+ "librt.so.1",
+ };
+
+ nsTArray<nsCString> libs;
+ SplitAt(", ", aLibs, libs);
+ for (const nsCString& lib : libs) {
+ for (const char* whiteListedLib : whitelist) {
+ if (lib.EqualsASCII(whiteListedLib)) {
+ auto libHandle = dlopen(whiteListedLib, RTLD_NOW | RTLD_GLOBAL);
+ if (libHandle) {
+ mLibHandles.AppendElement(libHandle);
+ } else {
+ // TODO(bug 1698718): remove the logging once we've identified
+ // the cause of the load failure.
+ const char* error = dlerror();
+ if (error) {
+ // We should always have an error, but gracefully handle just in
+ // case.
+ nsAutoCString nsError{error};
+ CrashReporter::AppendAppNotesToCrashReport(nsError);
+ }
+ // End bug 1698718 logging.
+
+ MOZ_CRASH("Couldn't load lib needed by media plugin");
+ }
+ }
+ }
+ }
+#endif
+ return IPC_OK();
+}
+
+bool GMPChild::GetUTF8LibPath(nsACString& aOutLibPath) {
+ nsCOMPtr<nsIFile> libFile;
+
+#define GMP_PATH_CRASH(explain) \
+ do { \
+ nsAutoString path; \
+ if (!libFile || NS_FAILED(libFile->GetPath(path))) { \
+ path = mPluginPath; \
+ } \
+ CrashReporter::AnnotateCrashReport( \
+ CrashReporter::Annotation::GMPLibraryPath, \
+ NS_ConvertUTF16toUTF8(path)); \
+ MOZ_CRASH(explain); \
+ } while (false)
+
+ nsresult rv = NS_NewLocalFile(mPluginPath, true, getter_AddRefs(libFile));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ GMP_PATH_CRASH("Failed to create file for plugin path");
+ return false;
+ }
+
+ nsCOMPtr<nsIFile> parent;
+ rv = libFile->GetParent(getter_AddRefs(parent));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ GMP_PATH_CRASH("Failed to get parent file for plugin file");
+ return false;
+ }
+
+ nsAutoString parentLeafName;
+ rv = parent->GetLeafName(parentLeafName);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ GMP_PATH_CRASH("Failed to get leaf for plugin file");
+ return false;
+ }
+
+ nsAutoString baseName;
+ baseName = Substring(parentLeafName, 4, parentLeafName.Length() - 1);
+
+#if defined(XP_MACOSX)
+ nsAutoString binaryName = u"lib"_ns + baseName + u".dylib"_ns;
+#elif defined(OS_POSIX)
+ nsAutoString binaryName = u"lib"_ns + baseName + u".so"_ns;
+#elif defined(XP_WIN)
+ nsAutoString binaryName = baseName + u".dll"_ns;
+#else
+# error not defined
+#endif
+ rv = libFile->AppendRelativePath(binaryName);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ GMP_PATH_CRASH("Failed to append lib to plugin file");
+ return false;
+ }
+
+ if (NS_WARN_IF(!FileExists(libFile))) {
+ GMP_PATH_CRASH("Plugin file does not exist");
+ return false;
+ }
+
+ nsAutoString path;
+ rv = libFile->GetPath(path);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ GMP_PATH_CRASH("Failed to get path for plugin file");
+ return false;
+ }
+
+ CopyUTF16toUTF8(path, aOutLibPath);
+ return true;
+}
+
+static nsCOMPtr<nsIFile> AppendFile(nsCOMPtr<nsIFile>&& aFile,
+ const nsString& aStr) {
+ return (aFile && NS_SUCCEEDED(aFile->Append(aStr))) ? aFile : nullptr;
+}
+
+static nsCOMPtr<nsIFile> CloneFile(const nsCOMPtr<nsIFile>& aFile) {
+ nsCOMPtr<nsIFile> clone;
+ return (aFile && NS_SUCCEEDED(aFile->Clone(getter_AddRefs(clone)))) ? clone
+ : nullptr;
+}
+
+static nsCOMPtr<nsIFile> GetParentFile(const nsCOMPtr<nsIFile>& aFile) {
+ nsCOMPtr<nsIFile> parent;
+ return (aFile && NS_SUCCEEDED(aFile->GetParent(getter_AddRefs(parent))))
+ ? parent
+ : nullptr;
+}
+
+#if defined(XP_WIN)
+static bool IsFileLeafEqualToASCII(const nsCOMPtr<nsIFile>& aFile,
+ const char* aStr) {
+ nsAutoString leafName;
+ return aFile && NS_SUCCEEDED(aFile->GetLeafName(leafName)) &&
+ leafName.EqualsASCII(aStr);
+}
+#endif
+
+#if defined(XP_WIN)
+# define FIREFOX_FILE u"firefox.exe"_ns
+# define XUL_LIB_FILE u"xul.dll"_ns
+#elif defined(XP_MACOSX)
+# define FIREFOX_FILE u"firefox"_ns
+# define XUL_LIB_FILE u"XUL"_ns
+#else
+# define FIREFOX_FILE u"firefox"_ns
+# define XUL_LIB_FILE u"libxul.so"_ns
+#endif
+
+static nsCOMPtr<nsIFile> GetFirefoxAppPath(
+ nsCOMPtr<nsIFile> aPluginContainerPath) {
+ MOZ_ASSERT(aPluginContainerPath);
+#if defined(XP_MACOSX)
+ // On MacOS the firefox binary is a few parent directories up from
+ // plugin-container.
+ // aPluginContainerPath will end with something like:
+ // xxxx/NightlyDebug.app/Contents/MacOS/plugin-container.app/Contents/MacOS/plugin-container
+ nsCOMPtr<nsIFile> path = aPluginContainerPath;
+ for (int i = 0; i < 4; i++) {
+ path = GetParentFile(path);
+ }
+ return path;
+#else
+ nsCOMPtr<nsIFile> parent = GetParentFile(aPluginContainerPath);
+# if XP_WIN
+ if (IsFileLeafEqualToASCII(parent, "i686")) {
+ // We must be on Windows on ARM64, where the plugin-container path will
+ // be in the 'i686' subdir. The firefox.exe is in the parent directory.
+ parent = GetParentFile(parent);
+ }
+# endif
+ return parent;
+#endif
+}
+
+#if defined(XP_MACOSX)
+static bool GetSigPath(const int aRelativeLayers,
+ const nsString& aTargetSigFileName,
+ nsCOMPtr<nsIFile> aExecutablePath,
+ nsCOMPtr<nsIFile>& aOutSigPath) {
+ // The sig file will be located in
+ // xxxx/NightlyDebug.app/Contents/Resources/XUL.sig
+ // xxxx/NightlyDebug.app/Contents/Resources/firefox.sig
+ // xxxx/NightlyDebug.app/Contents/MacOS/plugin-container.app/Contents/Resources/plugin-container.sig
+ // On MacOS the sig file is a few parent directories up from
+ // its executable file.
+ // Start to search the path from the path of the executable file we provided.
+ MOZ_ASSERT(aExecutablePath);
+ nsCOMPtr<nsIFile> path = aExecutablePath;
+ for (int i = 0; i < aRelativeLayers; i++) {
+ nsCOMPtr<nsIFile> parent;
+ if (NS_WARN_IF(NS_FAILED(path->GetParent(getter_AddRefs(parent))))) {
+ return false;
+ }
+ path = parent;
+ }
+ MOZ_ASSERT(path);
+ aOutSigPath = path;
+ return NS_SUCCEEDED(path->Append(u"Resources"_ns)) &&
+ NS_SUCCEEDED(path->Append(aTargetSigFileName));
+}
+#endif
+
+static bool AppendHostPath(nsCOMPtr<nsIFile>& aFile,
+ nsTArray<std::pair<nsCString, nsCString>>& aPaths) {
+ nsString str;
+ if (!FileExists(aFile) || NS_FAILED(aFile->GetPath(str))) {
+ return false;
+ }
+
+ nsCString filePath = NS_ConvertUTF16toUTF8(str);
+ nsCString sigFilePath;
+#if defined(XP_MACOSX)
+ nsAutoString binary;
+ if (NS_FAILED(aFile->GetLeafName(binary))) {
+ return false;
+ }
+ binary.Append(u".sig"_ns);
+ nsCOMPtr<nsIFile> sigFile;
+ if (GetSigPath(2, binary, aFile, sigFile) &&
+ NS_SUCCEEDED(sigFile->GetPath(str))) {
+ CopyUTF16toUTF8(str, sigFilePath);
+ } else {
+ // Cannot successfully get the sig file path.
+ // Assume it is located at the same place as plugin-container
+ // alternatively.
+ sigFilePath = nsCString(NS_ConvertUTF16toUTF8(str) + ".sig"_ns);
+ }
+#else
+ sigFilePath = nsCString(NS_ConvertUTF16toUTF8(str) + ".sig"_ns);
+#endif
+ aPaths.AppendElement(
+ std::make_pair(std::move(filePath), std::move(sigFilePath)));
+ return true;
+}
+
+nsTArray<std::pair<nsCString, nsCString>>
+GMPChild::MakeCDMHostVerificationPaths(const nsACString& aPluginLibPath) {
+ // Record the file path and its sig file path.
+ nsTArray<std::pair<nsCString, nsCString>> paths;
+ // Plugin binary path.
+ paths.AppendElement(
+ std::make_pair(nsCString(aPluginLibPath), aPluginLibPath + ".sig"_ns));
+
+ // Plugin-container binary path.
+ // Note: clang won't let us initialize an nsString from a wstring, so we
+ // need to go through UTF8 to get to an nsString.
+ const std::string pluginContainer =
+ WideToUTF8(CommandLine::ForCurrentProcess()->program());
+ nsString str;
+
+ CopyUTF8toUTF16(nsDependentCString(pluginContainer.c_str()), str);
+ nsCOMPtr<nsIFile> path;
+ if (NS_FAILED(NS_NewLocalFile(str, true, /* aFollowLinks */
+ getter_AddRefs(path))) ||
+ !AppendHostPath(path, paths)) {
+ // Without successfully determining plugin-container's path, we can't
+ // determine libxul's or Firefox's. So give up.
+ return paths;
+ }
+
+#if defined(XP_WIN)
+ // On Windows on ARM64, we should also append the x86 plugin-container's
+ // xul.dll.
+ const bool isWindowsOnARM64 =
+ IsFileLeafEqualToASCII(GetParentFile(path), "i686");
+ if (isWindowsOnARM64) {
+ nsCOMPtr<nsIFile> x86XulPath =
+ AppendFile(GetParentFile(path), XUL_LIB_FILE);
+ if (!AppendHostPath(x86XulPath, paths)) {
+ return paths;
+ }
+ }
+#endif
+
+ // Firefox application binary path.
+ nsCOMPtr<nsIFile> appDir = GetFirefoxAppPath(path);
+ path = AppendFile(CloneFile(appDir), FIREFOX_FILE);
+ if (!AppendHostPath(path, paths)) {
+ return paths;
+ }
+
+ // Libxul path. Note: re-using 'appDir' var here, as we assume libxul is in
+ // the same directory as Firefox executable.
+ appDir->GetPath(str);
+ path = AppendFile(CloneFile(appDir), XUL_LIB_FILE);
+ if (!AppendHostPath(path, paths)) {
+ return paths;
+ }
+
+ return paths;
+}
+
+static auto ToCString(const nsTArray<std::pair<nsCString, nsCString>>& aPairs) {
+ return StringJoin(","_ns, aPairs, [](nsACString& dest, const auto& p) {
+ dest.AppendPrintf("(%s,%s)", p.first.get(), p.second.get());
+ });
+}
+
+mozilla::ipc::IPCResult GMPChild::RecvStartPlugin(const nsString& aAdapter) {
+ GMP_CHILD_LOG_DEBUG("%s", __FUNCTION__);
+
+ nsAutoCString libPath;
+ if (!GetUTF8LibPath(libPath)) {
+ CrashReporter::AnnotateCrashReport(
+ CrashReporter::Annotation::GMPLibraryPath,
+ NS_ConvertUTF16toUTF8(mPluginPath));
+#ifdef XP_WIN
+ return IPC_FAIL(this,
+ nsPrintfCString("Failed to get lib path with error(%lu).",
+ GetLastError())
+ .get());
+#else
+ return IPC_FAIL(this, "Failed to get lib path.");
+#endif
+ }
+
+ auto platformAPI = new GMPPlatformAPI();
+ InitPlatformAPI(*platformAPI, this);
+
+ mGMPLoader = MakeUnique<GMPLoader>();
+#if defined(MOZ_SANDBOX) && !defined(XP_MACOSX)
+ if (!mGMPLoader->CanSandbox()) {
+ GMP_CHILD_LOG_DEBUG("%s Can't sandbox GMP, failing", __FUNCTION__);
+ delete platformAPI;
+ return IPC_FAIL(this, "Can't sandbox GMP.");
+ }
+#endif
+
+ GMPAdapter* adapter = nullptr;
+ if (aAdapter.EqualsLiteral("chromium")) {
+ auto&& paths = MakeCDMHostVerificationPaths(libPath);
+ GMP_CHILD_LOG_DEBUG("%s CDM host paths=%s", __func__,
+ ToCString(paths).get());
+ adapter = new ChromiumCDMAdapter(std::move(paths));
+ }
+
+ if (!mGMPLoader->Load(libPath.get(), libPath.Length(), platformAPI,
+ adapter)) {
+ NS_WARNING("Failed to load GMP");
+ delete platformAPI;
+ CrashReporter::AnnotateCrashReport(
+ CrashReporter::Annotation::GMPLibraryPath,
+ NS_ConvertUTF16toUTF8(mPluginPath));
+
+#ifdef XP_WIN
+ return IPC_FAIL(this, nsPrintfCString("Failed to load GMP with error(%lu).",
+ GetLastError())
+ .get());
+#else
+ return IPC_FAIL(this, "Failed to load GMP.");
+#endif
+ }
+
+ return IPC_OK();
+}
+
+MessageLoop* GMPChild::GMPMessageLoop() { return mGMPMessageLoop; }
+
+void GMPChild::ActorDestroy(ActorDestroyReason aWhy) {
+ GMP_CHILD_LOG_DEBUG("%s reason=%d", __FUNCTION__, aWhy);
+
+ for (uint32_t i = mGMPContentChildren.Length(); i > 0; i--) {
+ MOZ_ASSERT_IF(aWhy == NormalShutdown,
+ !mGMPContentChildren[i - 1]->IsUsed());
+ mGMPContentChildren[i - 1]->Close();
+ }
+
+ if (mGMPLoader) {
+ mGMPLoader->Shutdown();
+ }
+ if (AbnormalShutdown == aWhy) {
+ NS_WARNING("Abnormal shutdown of GMP process!");
+ ProcessChild::QuickExit();
+ }
+
+ // Send the last bits of Glean data over to the main process.
+ glean::FlushFOGData(
+ [](ByteBuf&& aBuf) { glean::SendFOGData(std::move(aBuf)); });
+
+ if (mProfilerController) {
+ mProfilerController->Shutdown();
+ mProfilerController = nullptr;
+ }
+
+ CrashReporterClient::DestroySingleton();
+
+ XRE_ShutdownChildProcess();
+}
+
+void GMPChild::ProcessingError(Result aCode, const char* aReason) {
+ switch (aCode) {
+ case MsgDropped:
+ _exit(0); // Don't trigger a crash report.
+ case MsgNotKnown:
+ MOZ_CRASH("aborting because of MsgNotKnown");
+ case MsgNotAllowed:
+ MOZ_CRASH("aborting because of MsgNotAllowed");
+ case MsgPayloadError:
+ MOZ_CRASH("aborting because of MsgPayloadError");
+ case MsgProcessingError:
+ MOZ_CRASH("aborting because of MsgProcessingError");
+ case MsgRouteError:
+ MOZ_CRASH("aborting because of MsgRouteError");
+ case MsgValueError:
+ MOZ_CRASH("aborting because of MsgValueError");
+ default:
+ MOZ_CRASH("not reached");
+ }
+}
+
+PGMPTimerChild* GMPChild::AllocPGMPTimerChild() {
+ return new GMPTimerChild(this);
+}
+
+bool GMPChild::DeallocPGMPTimerChild(PGMPTimerChild* aActor) {
+ MOZ_ASSERT(mTimerChild == static_cast<GMPTimerChild*>(aActor));
+ mTimerChild = nullptr;
+ return true;
+}
+
+GMPTimerChild* GMPChild::GetGMPTimers() {
+ if (!mTimerChild) {
+ PGMPTimerChild* sc = SendPGMPTimerConstructor();
+ if (!sc) {
+ return nullptr;
+ }
+ mTimerChild = static_cast<GMPTimerChild*>(sc);
+ }
+ return mTimerChild;
+}
+
+PGMPStorageChild* GMPChild::AllocPGMPStorageChild() {
+ return new GMPStorageChild(this);
+}
+
+bool GMPChild::DeallocPGMPStorageChild(PGMPStorageChild* aActor) {
+ mStorage = nullptr;
+ return true;
+}
+
+GMPStorageChild* GMPChild::GetGMPStorage() {
+ if (!mStorage) {
+ PGMPStorageChild* sc = SendPGMPStorageConstructor();
+ if (!sc) {
+ return nullptr;
+ }
+ mStorage = static_cast<GMPStorageChild*>(sc);
+ }
+ return mStorage;
+}
+
+mozilla::ipc::IPCResult GMPChild::RecvCrashPluginNow() {
+ MOZ_CRASH();
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPChild::RecvCloseActive() {
+ for (uint32_t i = mGMPContentChildren.Length(); i > 0; i--) {
+ mGMPContentChildren[i - 1]->CloseActive();
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPChild::RecvInitGMPContentChild(
+ Endpoint<PGMPContentChild>&& aEndpoint) {
+ GMPContentChild* child =
+ mGMPContentChildren.AppendElement(new GMPContentChild(this))->get();
+ aEndpoint.Bind(child);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPChild::RecvFlushFOGData(
+ FlushFOGDataResolver&& aResolver) {
+ GMP_CHILD_LOG_DEBUG("GMPChild RecvFlushFOGData");
+ glean::FlushFOGData(std::move(aResolver));
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPChild::RecvTestTriggerMetrics(
+ TestTriggerMetricsResolver&& aResolve) {
+ GMP_CHILD_LOG_DEBUG("GMPChild RecvTestTriggerMetrics");
+ mozilla::glean::test_only_ipc::a_counter.Add(
+ nsIXULRuntime::PROCESS_TYPE_GMPLUGIN);
+ aResolve(true);
+ return IPC_OK();
+}
+
+void GMPChild::GMPContentChildActorDestroy(GMPContentChild* aGMPContentChild) {
+ for (uint32_t i = mGMPContentChildren.Length(); i > 0; i--) {
+ RefPtr<GMPContentChild>& destroyedActor = mGMPContentChildren[i - 1];
+ if (destroyedActor.get() == aGMPContentChild) {
+ SendPGMPContentChildDestroyed();
+ mGMPContentChildren.RemoveElementAt(i - 1);
+ break;
+ }
+ }
+}
+
+mozilla::ipc::IPCResult GMPChild::RecvInitProfiler(
+ Endpoint<PProfilerChild>&& aEndpoint) {
+ mProfilerController =
+ mozilla::ChildProfilerController::Create(std::move(aEndpoint));
+ return IPC_OK();
+}
+
+} // namespace gmp
+} // namespace mozilla
+
+#undef GMP_CHILD_LOG_DEBUG
+#undef __CLASS__
diff --git a/dom/media/gmp/GMPChild.h b/dom/media/gmp/GMPChild.h
new file mode 100644
index 0000000000..7201833501
--- /dev/null
+++ b/dom/media/gmp/GMPChild.h
@@ -0,0 +1,106 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GMPChild_h_
+#define GMPChild_h_
+
+#include "mozilla/gmp/PGMPChild.h"
+#include "GMPTimerChild.h"
+#include "GMPStorageChild.h"
+#include "GMPLoader.h"
+#include "gmp-entrypoints.h"
+#include "prlink.h"
+
+namespace mozilla {
+
+class ChildProfilerController;
+
+namespace gmp {
+
+class GMPContentChild;
+
+class GMPChild : public PGMPChild {
+ friend class PGMPChild;
+
+ public:
+ NS_INLINE_DECL_REFCOUNTING(GMPChild, override)
+
+ GMPChild();
+
+ bool Init(const nsAString& aPluginPath,
+ mozilla::ipc::UntypedEndpoint&& aEndpoint);
+ MessageLoop* GMPMessageLoop();
+
+ // Main thread only.
+ GMPTimerChild* GetGMPTimers();
+ GMPStorageChild* GetGMPStorage();
+
+#if defined(XP_MACOSX) && defined(MOZ_SANDBOX)
+ bool SetMacSandboxInfo(bool aAllowWindowServer);
+#endif
+
+ private:
+ friend class GMPContentChild;
+
+ virtual ~GMPChild();
+
+ bool GetUTF8LibPath(nsACString& aOutLibPath);
+
+ mozilla::ipc::IPCResult RecvProvideStorageId(const nsCString& aStorageId);
+
+ mozilla::ipc::IPCResult RecvStartPlugin(const nsString& aAdapter);
+ mozilla::ipc::IPCResult RecvPreloadLibs(const nsCString& aLibs);
+
+ PGMPTimerChild* AllocPGMPTimerChild();
+ bool DeallocPGMPTimerChild(PGMPTimerChild* aActor);
+
+ PGMPStorageChild* AllocPGMPStorageChild();
+ bool DeallocPGMPStorageChild(PGMPStorageChild* aActor);
+
+ void GMPContentChildActorDestroy(GMPContentChild* aGMPContentChild);
+
+ mozilla::ipc::IPCResult RecvCrashPluginNow();
+ mozilla::ipc::IPCResult RecvCloseActive();
+
+ mozilla::ipc::IPCResult RecvInitGMPContentChild(
+ Endpoint<PGMPContentChild>&& aEndpoint);
+
+ mozilla::ipc::IPCResult RecvFlushFOGData(FlushFOGDataResolver&& aResolver);
+
+ mozilla::ipc::IPCResult RecvTestTriggerMetrics(
+ TestTriggerMetricsResolver&& aResolve);
+
+ mozilla::ipc::IPCResult RecvInitProfiler(
+ Endpoint<mozilla::PProfilerChild>&& aEndpoint);
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+ void ProcessingError(Result aCode, const char* aReason) override;
+
+ GMPErr GetAPI(const char* aAPIName, void* aHostAPI, void** aPluginAPI,
+ const nsACString& aKeySystem = ""_ns);
+
+ nsTArray<std::pair<nsCString, nsCString>> MakeCDMHostVerificationPaths(
+ const nsACString& aPluginLibPath);
+
+ nsTArray<RefPtr<GMPContentChild>> mGMPContentChildren;
+
+ RefPtr<GMPTimerChild> mTimerChild;
+ RefPtr<GMPStorageChild> mStorage;
+
+ RefPtr<ChildProfilerController> mProfilerController;
+
+ MessageLoop* mGMPMessageLoop;
+ nsString mPluginPath;
+ nsCString mStorageId;
+ UniquePtr<GMPLoader> mGMPLoader;
+#ifdef XP_LINUX
+ nsTArray<void*> mLibHandles;
+#endif
+};
+
+} // namespace gmp
+} // namespace mozilla
+
+#endif // GMPChild_h_
diff --git a/dom/media/gmp/GMPContentChild.cpp b/dom/media/gmp/GMPContentChild.cpp
new file mode 100644
index 0000000000..85fc6e7a70
--- /dev/null
+++ b/dom/media/gmp/GMPContentChild.cpp
@@ -0,0 +1,131 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GMPContentChild.h"
+#include "GMPChild.h"
+#include "GMPVideoDecoderChild.h"
+#include "GMPVideoEncoderChild.h"
+#include "ChromiumCDMChild.h"
+#include "base/task.h"
+#include "GMPUtils.h"
+
+namespace mozilla::gmp {
+
+MessageLoop* GMPContentChild::GMPMessageLoop() {
+ return mGMPChild->GMPMessageLoop();
+}
+
+void GMPContentChild::CheckThread() {
+ MOZ_ASSERT(mGMPChild->mGMPMessageLoop == MessageLoop::current());
+}
+
+#if defined(MOZ_SANDBOX) && defined(MOZ_DEBUG) && defined(ENABLE_TESTS)
+mozilla::ipc::IPCResult GMPContentChild::RecvInitSandboxTesting(
+ Endpoint<PSandboxTestingChild>&& aEndpoint) {
+ if (!SandboxTestingChild::Initialize(std::move(aEndpoint))) {
+ return IPC_FAIL(
+ this, "InitSandboxTesting failed to initialise the child process.");
+ }
+ return IPC_OK();
+}
+#endif
+
+void GMPContentChild::ActorDestroy(ActorDestroyReason aWhy) {
+ mGMPChild->GMPContentChildActorDestroy(this);
+}
+
+void GMPContentChild::ProcessingError(Result aCode, const char* aReason) {
+ mGMPChild->ProcessingError(aCode, aReason);
+}
+
+already_AddRefed<PGMPVideoDecoderChild>
+GMPContentChild::AllocPGMPVideoDecoderChild() {
+ return MakeAndAddRef<GMPVideoDecoderChild>(this);
+}
+
+already_AddRefed<PGMPVideoEncoderChild>
+GMPContentChild::AllocPGMPVideoEncoderChild() {
+ return MakeAndAddRef<GMPVideoEncoderChild>(this);
+}
+
+already_AddRefed<PChromiumCDMChild> GMPContentChild::AllocPChromiumCDMChild(
+ const nsACString& aKeySystem) {
+ return MakeAndAddRef<ChromiumCDMChild>(this);
+}
+
+mozilla::ipc::IPCResult GMPContentChild::RecvPGMPVideoDecoderConstructor(
+ PGMPVideoDecoderChild* aActor) {
+ auto vdc = static_cast<GMPVideoDecoderChild*>(aActor);
+
+ void* vd = nullptr;
+ GMPErr err = mGMPChild->GetAPI(GMP_API_VIDEO_DECODER, &vdc->Host(), &vd);
+ if (err != GMPNoErr || !vd) {
+ return IPC_FAIL(this, "GMPGetAPI call failed trying to construct decoder.");
+ }
+
+ vdc->Init(static_cast<GMPVideoDecoder*>(vd));
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPContentChild::RecvPGMPVideoEncoderConstructor(
+ PGMPVideoEncoderChild* aActor) {
+ auto vec = static_cast<GMPVideoEncoderChild*>(aActor);
+
+ void* ve = nullptr;
+ GMPErr err = mGMPChild->GetAPI(GMP_API_VIDEO_ENCODER, &vec->Host(), &ve);
+ if (err != GMPNoErr || !ve) {
+ return IPC_FAIL(this, "GMPGetAPI call failed trying to construct encoder.");
+ }
+
+ vec->Init(static_cast<GMPVideoEncoder*>(ve));
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPContentChild::RecvPChromiumCDMConstructor(
+ PChromiumCDMChild* aActor, const nsACString& aKeySystem) {
+ ChromiumCDMChild* child = static_cast<ChromiumCDMChild*>(aActor);
+ cdm::Host_10* host10 = child;
+
+ void* cdm = nullptr;
+ GMPErr err = mGMPChild->GetAPI(CHROMIUM_CDM_API, host10, &cdm, aKeySystem);
+ if (err != GMPNoErr || !cdm) {
+ return IPC_FAIL(this, "GMPGetAPI call failed trying to get CDM.");
+ }
+
+ child->Init(static_cast<cdm::ContentDecryptionModule_10*>(cdm),
+ mGMPChild->mStorageId);
+
+ return IPC_OK();
+}
+
+void GMPContentChild::CloseActive() {
+ // Invalidate and remove any remaining API objects.
+ const ManagedContainer<PGMPVideoDecoderChild>& videoDecoders =
+ ManagedPGMPVideoDecoderChild();
+ for (const auto& key : videoDecoders) {
+ key->SendShutdown();
+ }
+
+ const ManagedContainer<PGMPVideoEncoderChild>& videoEncoders =
+ ManagedPGMPVideoEncoderChild();
+ for (const auto& key : videoEncoders) {
+ key->SendShutdown();
+ }
+
+ const ManagedContainer<PChromiumCDMChild>& cdms = ManagedPChromiumCDMChild();
+ for (const auto& key : cdms) {
+ key->SendShutdown();
+ }
+}
+
+bool GMPContentChild::IsUsed() {
+ return !ManagedPGMPVideoDecoderChild().IsEmpty() ||
+ !ManagedPGMPVideoEncoderChild().IsEmpty() ||
+ !ManagedPChromiumCDMChild().IsEmpty();
+}
+
+} // namespace mozilla::gmp
diff --git a/dom/media/gmp/GMPContentChild.h b/dom/media/gmp/GMPContentChild.h
new file mode 100644
index 0000000000..a31b48b00f
--- /dev/null
+++ b/dom/media/gmp/GMPContentChild.h
@@ -0,0 +1,66 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GMPContentChild_h_
+#define GMPContentChild_h_
+
+#include "mozilla/gmp/PGMPContentChild.h"
+#include "GMPSharedMemManager.h"
+
+#if defined(MOZ_SANDBOX) && defined(MOZ_DEBUG) && defined(ENABLE_TESTS)
+# include "mozilla/SandboxTestingChild.h"
+#endif
+
+namespace mozilla::gmp {
+
+class GMPChild;
+
+class GMPContentChild : public PGMPContentChild, public GMPSharedMem {
+ public:
+ // Mark AddRef and Release as `final`, as they overload pure virtual
+ // implementations in PGMPContentChild.
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GMPContentChild, final)
+
+ explicit GMPContentChild(GMPChild* aChild) : mGMPChild(aChild) {}
+
+ MessageLoop* GMPMessageLoop();
+
+ mozilla::ipc::IPCResult RecvPGMPVideoDecoderConstructor(
+ PGMPVideoDecoderChild* aActor) override;
+ mozilla::ipc::IPCResult RecvPGMPVideoEncoderConstructor(
+ PGMPVideoEncoderChild* aActor) override;
+ mozilla::ipc::IPCResult RecvPChromiumCDMConstructor(
+ PChromiumCDMChild* aActor, const nsACString& aKeySystem) override;
+
+ already_AddRefed<PGMPVideoDecoderChild> AllocPGMPVideoDecoderChild();
+
+ already_AddRefed<PGMPVideoEncoderChild> AllocPGMPVideoEncoderChild();
+
+ already_AddRefed<PChromiumCDMChild> AllocPChromiumCDMChild(
+ const nsACString& aKeySystem);
+
+#if defined(MOZ_SANDBOX) && defined(MOZ_DEBUG) && defined(ENABLE_TESTS)
+ mozilla::ipc::IPCResult RecvInitSandboxTesting(
+ Endpoint<PSandboxTestingChild>&& aEndpoint);
+#endif
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+ void ProcessingError(Result aCode, const char* aReason) override;
+
+ // GMPSharedMem
+ void CheckThread() override;
+
+ void CloseActive();
+ bool IsUsed();
+
+ GMPChild* mGMPChild;
+
+ private:
+ ~GMPContentChild() = default;
+};
+
+} // namespace mozilla::gmp
+
+#endif // GMPContentChild_h_
diff --git a/dom/media/gmp/GMPContentParent.cpp b/dom/media/gmp/GMPContentParent.cpp
new file mode 100644
index 0000000000..153d4da7fe
--- /dev/null
+++ b/dom/media/gmp/GMPContentParent.cpp
@@ -0,0 +1,204 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GMPContentParent.h"
+#include "GMPLog.h"
+#include "GMPParent.h"
+#include "GMPServiceChild.h"
+#include "GMPVideoDecoderParent.h"
+#include "GMPVideoEncoderParent.h"
+#include "ChromiumCDMParent.h"
+#include "mozIGeckoMediaPluginService.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Unused.h"
+#include "base/task.h"
+
+namespace mozilla::gmp {
+
+static const char* GetBoolString(bool aBool) {
+ return aBool ? "true" : "false";
+}
+
+GMPContentParent::GMPContentParent(GMPParent* aParent)
+ : mParent(aParent), mPluginId(0) {
+ GMP_LOG_DEBUG("GMPContentParent::GMPContentParent(this=%p), aParent=%p", this,
+ aParent);
+ if (mParent) {
+ SetDisplayName(mParent->GetDisplayName());
+ SetPluginId(mParent->GetPluginId());
+ SetPluginType(mParent->GetPluginType());
+ }
+}
+
+GMPContentParent::~GMPContentParent() {
+ GMP_LOG_DEBUG(
+ "GMPContentParent::~GMPContentParent(this=%p) mVideoDecoders.IsEmpty=%s, "
+ "mVideoEncoders.IsEmpty=%s, mChromiumCDMs.IsEmpty=%s, "
+ "mCloseBlockerCount=%" PRIu32,
+ this, GetBoolString(mVideoDecoders.IsEmpty()),
+ GetBoolString(mVideoEncoders.IsEmpty()),
+ GetBoolString(mChromiumCDMs.IsEmpty()), mCloseBlockerCount);
+}
+
+void GMPContentParent::ActorDestroy(ActorDestroyReason aWhy) {
+ GMP_LOG_DEBUG("GMPContentParent::ActorDestroy(this=%p, aWhy=%d)", this,
+ static_cast<int>(aWhy));
+ MOZ_ASSERT(mVideoDecoders.IsEmpty() && mVideoEncoders.IsEmpty() &&
+ mChromiumCDMs.IsEmpty());
+}
+
+void GMPContentParent::CheckThread() {
+ MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread());
+}
+
+void GMPContentParent::ChromiumCDMDestroyed(ChromiumCDMParent* aCDM) {
+ GMP_LOG_DEBUG("GMPContentParent::ChromiumCDMDestroyed(this=%p, aCDM=%p)",
+ this, aCDM);
+ MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread());
+
+ MOZ_ALWAYS_TRUE(mChromiumCDMs.RemoveElement(aCDM));
+ CloseIfUnused();
+}
+
+void GMPContentParent::VideoDecoderDestroyed(GMPVideoDecoderParent* aDecoder) {
+ GMP_LOG_DEBUG("GMPContentParent::VideoDecoderDestroyed(this=%p, aDecoder=%p)",
+ this, aDecoder);
+ MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread());
+
+ // If the constructor fails, we'll get called before it's added
+ Unused << NS_WARN_IF(!mVideoDecoders.RemoveElement(aDecoder));
+ CloseIfUnused();
+}
+
+void GMPContentParent::VideoEncoderDestroyed(GMPVideoEncoderParent* aEncoder) {
+ GMP_LOG_DEBUG("GMPContentParent::VideoEncoderDestroyed(this=%p, aEncoder=%p)",
+ this, aEncoder);
+ MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread());
+
+ // If the constructor fails, we'll get called before it's added
+ Unused << NS_WARN_IF(!mVideoEncoders.RemoveElement(aEncoder));
+ CloseIfUnused();
+}
+
+void GMPContentParent::AddCloseBlocker() {
+ MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread());
+ ++mCloseBlockerCount;
+ GMP_LOG_DEBUG(
+ "GMPContentParent::AddCloseBlocker(this=%p) mCloseBlockerCount=%" PRIu32,
+ this, mCloseBlockerCount);
+}
+
+void GMPContentParent::RemoveCloseBlocker() {
+ MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread());
+ --mCloseBlockerCount;
+ GMP_LOG_DEBUG(
+ "GMPContentParent::RemoveCloseBlocker(this=%p) "
+ "mCloseBlockerCount=%" PRIu32,
+ this, mCloseBlockerCount);
+ CloseIfUnused();
+}
+
+void GMPContentParent::CloseIfUnused() {
+ MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread());
+ GMP_LOG_DEBUG(
+ "GMPContentParent::CloseIfUnused(this=%p) mVideoDecoders.IsEmpty=%s, "
+ "mVideoEncoders.IsEmpty=%s, mChromiumCDMs.IsEmpty=%s, "
+ "mCloseBlockerCount=%" PRIu32,
+ this, GetBoolString(mVideoDecoders.IsEmpty()),
+ GetBoolString(mVideoEncoders.IsEmpty()),
+ GetBoolString(mChromiumCDMs.IsEmpty()), mCloseBlockerCount);
+ if (mVideoDecoders.IsEmpty() && mVideoEncoders.IsEmpty() &&
+ mChromiumCDMs.IsEmpty() && mCloseBlockerCount == 0) {
+ RefPtr<GMPContentParent> toClose;
+ if (mParent) {
+ toClose = mParent->ForgetGMPContentParent();
+ } else {
+ toClose = this;
+ RefPtr<GeckoMediaPluginServiceChild> gmp(
+ GeckoMediaPluginServiceChild::GetSingleton());
+ if (gmp) {
+ gmp->RemoveGMPContentParent(toClose);
+ }
+ }
+ NS_DispatchToCurrentThread(NewRunnableMethod(
+ "gmp::GMPContentParent::Close", toClose, &GMPContentParent::Close));
+ }
+}
+
+nsCOMPtr<nsISerialEventTarget> GMPContentParent::GMPEventTarget() {
+ if (!mGMPEventTarget) {
+ GMP_LOG_DEBUG("GMPContentParent::GMPEventTarget(this=%p)", this);
+ nsCOMPtr<mozIGeckoMediaPluginService> mps =
+ do_GetService("@mozilla.org/gecko-media-plugin-service;1");
+ MOZ_ASSERT(mps);
+ if (!mps) {
+ return nullptr;
+ }
+ // Not really safe if we just grab to the mGMPEventTarget, as we don't know
+ // what thread we're running on and other threads may be trying to
+ // access this without locks! However, debug only, and primary failure
+ // mode outside of compiler-helped TSAN is a leak. But better would be
+ // to use swap() under a lock.
+ nsCOMPtr<nsIThread> gmpThread;
+ mps->GetThread(getter_AddRefs(gmpThread));
+ MOZ_ASSERT(gmpThread);
+
+ mGMPEventTarget = gmpThread;
+ }
+
+ return mGMPEventTarget;
+}
+
+already_AddRefed<ChromiumCDMParent> GMPContentParent::GetChromiumCDM(
+ const nsCString& aKeySystem) {
+ GMP_LOG_DEBUG("GMPContentParent::GetChromiumCDM(this=%p aKeySystem=%s)", this,
+ aKeySystem.get());
+
+ RefPtr<ChromiumCDMParent> parent = new ChromiumCDMParent(this, GetPluginId());
+ if (!SendPChromiumCDMConstructor(parent, aKeySystem)) {
+ return nullptr;
+ }
+
+ // TODO: Remove parent from mChromiumCDMs in ChromiumCDMParent::Destroy().
+ mChromiumCDMs.AppendElement(parent);
+
+ return parent.forget();
+}
+
+nsresult GMPContentParent::GetGMPVideoDecoder(GMPVideoDecoderParent** aGMPVD) {
+ GMP_LOG_DEBUG("GMPContentParent::GetGMPVideoDecoder(this=%p)", this);
+
+ RefPtr<GMPVideoDecoderParent> vdp = new GMPVideoDecoderParent(this);
+ if (!SendPGMPVideoDecoderConstructor(vdp)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // This addref corresponds to the Proxy pointer the consumer is returned.
+ // It's dropped by calling Close() on the interface.
+ vdp.get()->AddRef();
+ *aGMPVD = vdp;
+ mVideoDecoders.AppendElement(vdp);
+
+ return NS_OK;
+}
+
+nsresult GMPContentParent::GetGMPVideoEncoder(GMPVideoEncoderParent** aGMPVE) {
+ GMP_LOG_DEBUG("GMPContentParent::GetGMPVideoEncoder(this=%p)", this);
+
+ RefPtr<GMPVideoEncoderParent> vep = new GMPVideoEncoderParent(this);
+ if (!SendPGMPVideoEncoderConstructor(vep)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // This addref corresponds to the Proxy pointer the consumer is returned.
+ // It's dropped by calling Close() on the interface.
+ vep.get()->AddRef();
+ *aGMPVE = vep;
+ mVideoEncoders.AppendElement(vep);
+
+ return NS_OK;
+}
+
+} // namespace mozilla::gmp
diff --git a/dom/media/gmp/GMPContentParent.h b/dom/media/gmp/GMPContentParent.h
new file mode 100644
index 0000000000..eb68e0fd4a
--- /dev/null
+++ b/dom/media/gmp/GMPContentParent.h
@@ -0,0 +1,94 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GMPContentParent_h_
+#define GMPContentParent_h_
+
+#include "mozilla/gmp/PGMPContentParent.h"
+#include "GMPSharedMemManager.h"
+#include "GMPNativeTypes.h"
+#include "nsISupportsImpl.h"
+
+namespace mozilla::gmp {
+
+class GMPParent;
+class GMPVideoDecoderParent;
+class GMPVideoEncoderParent;
+class ChromiumCDMParent;
+
+class GMPContentParent final : public PGMPContentParent, public GMPSharedMem {
+ friend class PGMPContentParent;
+
+ public:
+ // Mark AddRef and Release as `final`, as they overload pure virtual
+ // implementations in PGMPContentParent.
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GMPContentParent, final)
+
+ explicit GMPContentParent(GMPParent* aParent = nullptr);
+
+ nsresult GetGMPVideoDecoder(GMPVideoDecoderParent** aGMPVD);
+ void VideoDecoderDestroyed(GMPVideoDecoderParent* aDecoder);
+
+ nsresult GetGMPVideoEncoder(GMPVideoEncoderParent** aGMPVE);
+ void VideoEncoderDestroyed(GMPVideoEncoderParent* aEncoder);
+
+ already_AddRefed<ChromiumCDMParent> GetChromiumCDM(
+ const nsCString& aKeySystem);
+ void ChromiumCDMDestroyed(ChromiumCDMParent* aCDM);
+
+ nsCOMPtr<nsISerialEventTarget> GMPEventTarget();
+
+ // GMPSharedMem
+ void CheckThread() override;
+
+ void SetDisplayName(const nsCString& aDisplayName) {
+ mDisplayName = aDisplayName;
+ }
+ const nsCString& GetDisplayName() const { return mDisplayName; }
+ void SetPluginId(const uint32_t aPluginId) { mPluginId = aPluginId; }
+ uint32_t GetPluginId() const { return mPluginId; }
+ void SetPluginType(GMPPluginType aPluginType) { mPluginType = aPluginType; }
+ GMPPluginType GetPluginType() const { return mPluginType; }
+
+ class CloseBlocker {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CloseBlocker)
+
+ explicit CloseBlocker(GMPContentParent* aParent) : mParent(aParent) {
+ mParent->AddCloseBlocker();
+ }
+ RefPtr<GMPContentParent> mParent;
+
+ private:
+ ~CloseBlocker() { mParent->RemoveCloseBlocker(); }
+ };
+
+ private:
+ void AddCloseBlocker();
+ void RemoveCloseBlocker();
+
+ ~GMPContentParent();
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ void CloseIfUnused();
+ // Needed because NewRunnableMethod tried to use the class that the method
+ // lives on to store the receiver, but PGMPContentParent isn't refcounted.
+ void Close() { PGMPContentParent::Close(); }
+
+ nsTArray<RefPtr<GMPVideoDecoderParent>> mVideoDecoders;
+ nsTArray<RefPtr<GMPVideoEncoderParent>> mVideoEncoders;
+ nsTArray<RefPtr<ChromiumCDMParent>> mChromiumCDMs;
+ nsCOMPtr<nsISerialEventTarget> mGMPEventTarget;
+ RefPtr<GMPParent> mParent;
+ nsCString mDisplayName;
+ uint32_t mPluginId;
+ GMPPluginType mPluginType = GMPPluginType::Unknown;
+ uint32_t mCloseBlockerCount = 0;
+};
+
+} // namespace mozilla::gmp
+
+#endif // GMPParent_h_
diff --git a/dom/media/gmp/GMPCrashHelper.h b/dom/media/gmp/GMPCrashHelper.h
new file mode 100644
index 0000000000..0034d54ee7
--- /dev/null
+++ b/dom/media/gmp/GMPCrashHelper.h
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(GMPCrashHelper_h_)
+# define GMPCrashHelper_h_
+
+# include "MainThreadUtils.h"
+# include "nsISupportsImpl.h"
+
+class nsPIDOMWindowInner;
+
+namespace mozilla {
+
+// For every GMP actor requested, the caller can specify a crash helper,
+// which is an object which supplies the nsPIDOMWindowInner to which we'll
+// dispatch the PluginCrashed event if the GMP crashes.
+// GMPCrashHelper has threadsafe refcounting. Its release method ensures
+// that instances are destroyed on the main thread.
+class GMPCrashHelper {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING_WITH_DELETE_ON_MAIN_THREAD(
+ GMPCrashHelper);
+
+ // Called on the main thread.
+ virtual already_AddRefed<nsPIDOMWindowInner>
+ GetPluginCrashedEventTarget() = 0;
+
+ protected:
+ virtual ~GMPCrashHelper() { MOZ_ASSERT(NS_IsMainThread()); }
+};
+
+} // namespace mozilla
+
+#endif // GMPCrashHelper_h_
diff --git a/dom/media/gmp/GMPCrashHelperHolder.cpp b/dom/media/gmp/GMPCrashHelperHolder.cpp
new file mode 100644
index 0000000000..1c0182155b
--- /dev/null
+++ b/dom/media/gmp/GMPCrashHelperHolder.cpp
@@ -0,0 +1,31 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GMPCrashHelperHolder.h"
+#include "GMPService.h"
+#include "mozilla/RefPtr.h"
+#include "nsPIDOMWindow.h"
+#include "mozilla/ipc/ProtocolUtils.h"
+
+namespace mozilla {
+
+void GMPCrashHelperHolder::SetCrashHelper(GMPCrashHelper* aHelper) {
+ mCrashHelper = aHelper;
+}
+
+GMPCrashHelper* GMPCrashHelperHolder::GetCrashHelper() { return mCrashHelper; }
+
+void GMPCrashHelperHolder::MaybeDisconnect(bool aAbnormalShutdown) {
+ if (!aAbnormalShutdown) {
+ RefPtr<gmp::GeckoMediaPluginService> service(
+ gmp::GeckoMediaPluginService::GetGeckoMediaPluginService());
+ if (service) {
+ service->DisconnectCrashHelper(GetCrashHelper());
+ }
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/gmp/GMPCrashHelperHolder.h b/dom/media/gmp/GMPCrashHelperHolder.h
new file mode 100644
index 0000000000..d8c63234da
--- /dev/null
+++ b/dom/media/gmp/GMPCrashHelperHolder.h
@@ -0,0 +1,63 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GMPCrashHelperHolder_h_
+#define GMPCrashHelperHolder_h_
+
+#include "mozilla/RefPtr.h"
+#include "GMPCrashHelper.h"
+
+namespace mozilla {
+
+// Disconnecting the GMPCrashHelpers at the right time is hard. We need to
+// ensure that in the crashing case that we stay connected until the
+// "gmp-plugin-crashed" message is processed in the content process.
+//
+// We have two channels connecting to the GMP; PGMP which connects from
+// chrome to GMP process, and PGMPContent, which bridges between the content
+// and GMP process. If the GMP crashes both PGMP and PGMPContent receive
+// ActorDestroy messages and begin to shutdown at the same time.
+//
+// However the crash report mini dump must be generated in the chrome
+// process' ActorDestroy, before the "gmp-plugin-crashed" message can be sent
+// to the content process. We fire the "PluginCrashed" event when we handle
+// the "gmp-plugin-crashed" message in the content process, and we need the
+// crash helpers to do that.
+//
+// The PGMPContent's managed actors' ActorDestroy messages are the only shutdown
+// notification we get in the content process, but we can't disconnect the
+// crash handlers there in the crashing case, as ActorDestroy happens before
+// we've received the "gmp-plugin-crashed" message and had a chance for the
+// crash helpers to generate the window to dispatch PluginCrashed to initiate
+// the crash report submission notification box.
+//
+// So we need to ensure that in the content process, the GMPCrashHelpers stay
+// connected to the GMPService until after ActorDestroy messages are received
+// if there's an abnormal shutdown. In the case where the GMP doesn't crash,
+// we do actually want to disconnect GMPCrashHandlers in ActorDestroy, since
+// we don't have any other signal that a GMP actor is shutting down. If we don't
+// disconnect the crash helper there in the normal shutdown case, the helper
+// will stick around forever and leak.
+//
+// In the crashing case, the GMPCrashHelpers are deallocated when the crash
+// report is processed in GeckoMediaPluginService::RunPluginCrashCallbacks().
+//
+// It's a bit yuck that we have to have two paths for disconnecting the crash
+// helpers, but there aren't really any better options.
+class GMPCrashHelperHolder {
+ public:
+ void SetCrashHelper(GMPCrashHelper* aHelper);
+
+ GMPCrashHelper* GetCrashHelper();
+
+ void MaybeDisconnect(bool aAbnormalShutdown);
+
+ private:
+ RefPtr<GMPCrashHelper> mCrashHelper;
+};
+
+} // namespace mozilla
+
+#endif // GMPCrashHelperHolder_h_
diff --git a/dom/media/gmp/GMPDiskStorage.cpp b/dom/media/gmp/GMPDiskStorage.cpp
new file mode 100644
index 0000000000..5f7a023b9b
--- /dev/null
+++ b/dom/media/gmp/GMPDiskStorage.cpp
@@ -0,0 +1,454 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "plhash.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "GMPParent.h"
+#include "gmp-storage.h"
+#include "mozilla/Unused.h"
+#include "mozilla/EndianUtils.h"
+#include "nsClassHashtable.h"
+#include "prio.h"
+#include "nsContentCID.h"
+#include "nsServiceManagerUtils.h"
+
+namespace mozilla::gmp {
+
+// We store the records for a given GMP as files in the profile dir.
+// $profileDir/gmp/$platform/$gmpName/storage/$nodeId/
+static nsresult GetGMPStorageDir(nsIFile** aTempDir, const nsAString& aGMPName,
+ const nsACString& aNodeId) {
+ if (NS_WARN_IF(!aTempDir)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsCOMPtr<mozIGeckoMediaPluginChromeService> mps =
+ do_GetService("@mozilla.org/gecko-media-plugin-service;1");
+ if (NS_WARN_IF(!mps)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIFile> tmpFile;
+ nsresult rv = mps->GetStorageDir(getter_AddRefs(tmpFile));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = tmpFile->Append(aGMPName);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = tmpFile->AppendNative("storage"_ns);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = tmpFile->AppendNative(aNodeId);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = tmpFile->Create(nsIFile::DIRECTORY_TYPE, 0700);
+ if (rv != NS_ERROR_FILE_ALREADY_EXISTS && NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ tmpFile.forget(aTempDir);
+
+ return NS_OK;
+}
+
+// Disk-backed GMP storage. Records are stored in files on disk in
+// the profile directory. The record name is a hash of the filename,
+// and we resolve hash collisions by just adding 1 to the hash code.
+// The format of records on disk is:
+// 4 byte, uint32_t $recordNameLength, in little-endian byte order,
+// record name (i.e. $recordNameLength bytes, no null terminator)
+// record bytes (entire remainder of file)
+class GMPDiskStorage : public GMPStorage {
+ public:
+ explicit GMPDiskStorage(const nsACString& aNodeId, const nsAString& aGMPName)
+ : mNodeId(aNodeId), mGMPName(aGMPName) {}
+
+ ~GMPDiskStorage() {
+ // Close all open file handles.
+ for (const auto& record : mRecords.Values()) {
+ if (record->mFileDesc) {
+ PR_Close(record->mFileDesc);
+ record->mFileDesc = nullptr;
+ }
+ }
+ }
+
+ nsresult Init() {
+ // Build our index of records on disk.
+ nsCOMPtr<nsIFile> storageDir;
+ nsresult rv =
+ GetGMPStorageDir(getter_AddRefs(storageDir), mGMPName, mNodeId);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ DirectoryEnumerator iter(storageDir, DirectoryEnumerator::FilesAndDirs);
+ for (nsCOMPtr<nsIFile> dirEntry; (dirEntry = iter.Next()) != nullptr;) {
+ PRFileDesc* fd = nullptr;
+ if (NS_WARN_IF(
+ NS_FAILED(dirEntry->OpenNSPRFileDesc(PR_RDONLY, 0, &fd)))) {
+ continue;
+ }
+ int32_t recordLength = 0;
+ nsCString recordName;
+ nsresult err = ReadRecordMetadata(fd, recordLength, recordName);
+ PR_Close(fd);
+ if (NS_WARN_IF(NS_FAILED(err))) {
+ // File is not a valid storage file. Don't index it. Delete the file,
+ // to make our indexing faster in future.
+ dirEntry->Remove(false);
+ continue;
+ }
+
+ nsAutoString filename;
+ rv = dirEntry->GetLeafName(filename);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ mRecords.InsertOrUpdate(recordName,
+ MakeUnique<Record>(filename, recordName));
+ }
+
+ return NS_OK;
+ }
+
+ GMPErr Open(const nsACString& aRecordName) override {
+ MOZ_ASSERT(!IsOpen(aRecordName));
+
+ Record* const record =
+ mRecords.WithEntryHandle(aRecordName, [&](auto&& entry) -> Record* {
+ if (!entry) {
+ // New file.
+ nsAutoString filename;
+ nsresult rv = GetUnusedFilename(aRecordName, filename);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+ return entry.Insert(MakeUnique<Record>(filename, aRecordName))
+ .get();
+ }
+
+ return entry->get();
+ });
+ if (!record) {
+ return GMPGenericErr;
+ }
+
+ MOZ_ASSERT(record);
+ if (record->mFileDesc) {
+ NS_WARNING("Tried to open already open record");
+ return GMPRecordInUse;
+ }
+
+ nsresult rv =
+ OpenStorageFile(record->mFilename, ReadWrite, &record->mFileDesc);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return GMPGenericErr;
+ }
+
+ MOZ_ASSERT(IsOpen(aRecordName));
+
+ return GMPNoErr;
+ }
+
+ bool IsOpen(const nsACString& aRecordName) const override {
+ // We are open if we have a record indexed, and it has a valid
+ // file descriptor.
+ const Record* record = mRecords.Get(aRecordName);
+ return record && !!record->mFileDesc;
+ }
+
+ GMPErr Read(const nsACString& aRecordName,
+ nsTArray<uint8_t>& aOutBytes) override {
+ if (!IsOpen(aRecordName)) {
+ return GMPClosedErr;
+ }
+
+ Record* record = nullptr;
+ mRecords.Get(aRecordName, &record);
+ MOZ_ASSERT(record && !!record->mFileDesc); // IsOpen() guarantees this.
+
+ // Our error strategy is to report records with invalid contents as
+ // containing 0 bytes. Zero length records are considered "deleted" by
+ // the GMPStorage API.
+ aOutBytes.SetLength(0);
+
+ int32_t recordLength = 0;
+ nsCString recordName;
+ nsresult err =
+ ReadRecordMetadata(record->mFileDesc, recordLength, recordName);
+ if (NS_WARN_IF(NS_FAILED(err) || recordLength == 0)) {
+ // We failed to read the record metadata. Or the record is 0 length.
+ // Treat damaged records as empty.
+ // ReadRecordMetadata() could fail if the GMP opened a new record and
+ // tried to read it before anything was written to it..
+ return GMPNoErr;
+ }
+
+ if (!aRecordName.Equals(recordName)) {
+ NS_WARNING("Record file contains some other record's contents!");
+ return GMPRecordCorrupted;
+ }
+
+ // After calling ReadRecordMetadata, we should be ready to read the
+ // record data.
+ if (PR_Available(record->mFileDesc) != recordLength) {
+ NS_WARNING("Record file length mismatch!");
+ return GMPRecordCorrupted;
+ }
+
+ aOutBytes.SetLength(recordLength);
+ int32_t bytesRead =
+ PR_Read(record->mFileDesc, aOutBytes.Elements(), recordLength);
+ return (bytesRead == recordLength) ? GMPNoErr : GMPRecordCorrupted;
+ }
+
+ GMPErr Write(const nsACString& aRecordName,
+ const nsTArray<uint8_t>& aBytes) override {
+ if (!IsOpen(aRecordName)) {
+ return GMPClosedErr;
+ }
+
+ Record* record = nullptr;
+ mRecords.Get(aRecordName, &record);
+ MOZ_ASSERT(record && !!record->mFileDesc); // IsOpen() guarantees this.
+
+ // Write operations overwrite the entire record. So close it now.
+ PR_Close(record->mFileDesc);
+ record->mFileDesc = nullptr;
+
+ // Writing 0 bytes means removing (deleting) the file.
+ if (aBytes.Length() == 0) {
+ nsresult rv = RemoveStorageFile(record->mFilename);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ // Could not delete file -> Continue with trying to erase the contents.
+ } else {
+ return GMPNoErr;
+ }
+ }
+
+ // Write operations overwrite the entire record. So re-open the file
+ // in truncate mode, to clear its contents.
+ if (NS_WARN_IF(NS_FAILED(OpenStorageFile(record->mFilename, Truncate,
+ &record->mFileDesc)))) {
+ return GMPGenericErr;
+ }
+
+ // Store the length of the record name followed by the record name
+ // at the start of the file.
+ int32_t bytesWritten = 0;
+ char buf[sizeof(uint32_t)] = {0};
+ LittleEndian::writeUint32(buf, aRecordName.Length());
+ bytesWritten = PR_Write(record->mFileDesc, buf, MOZ_ARRAY_LENGTH(buf));
+ if (bytesWritten != MOZ_ARRAY_LENGTH(buf)) {
+ NS_WARNING("Failed to write GMPStorage record name length.");
+ return GMPRecordCorrupted;
+ }
+ bytesWritten = PR_Write(record->mFileDesc, aRecordName.BeginReading(),
+ aRecordName.Length());
+ if (bytesWritten != (int32_t)aRecordName.Length()) {
+ NS_WARNING("Failed to write GMPStorage record name.");
+ return GMPRecordCorrupted;
+ }
+
+ bytesWritten =
+ PR_Write(record->mFileDesc, aBytes.Elements(), aBytes.Length());
+ if (bytesWritten != (int32_t)aBytes.Length()) {
+ NS_WARNING("Failed to write GMPStorage record data.");
+ return GMPRecordCorrupted;
+ }
+
+ // Try to sync the file to disk, so that in the event of a crash,
+ // the record is less likely to be corrupted.
+ PR_Sync(record->mFileDesc);
+
+ return GMPNoErr;
+ }
+
+ void Close(const nsACString& aRecordName) override {
+ Record* record = nullptr;
+ mRecords.Get(aRecordName, &record);
+ if (record && !!record->mFileDesc) {
+ PR_Close(record->mFileDesc);
+ record->mFileDesc = nullptr;
+ }
+ MOZ_ASSERT(!IsOpen(aRecordName));
+ }
+
+ private:
+ // We store records in a file which is a hash of the record name.
+ // If there is a hash collision, we just keep adding 1 to the hash
+ // code, until we find a free slot.
+ nsresult GetUnusedFilename(const nsACString& aRecordName,
+ nsString& aOutFilename) {
+ nsCOMPtr<nsIFile> storageDir;
+ nsresult rv =
+ GetGMPStorageDir(getter_AddRefs(storageDir), mGMPName, mNodeId);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ uint64_t recordNameHash = HashString(PromiseFlatCString(aRecordName).get());
+ for (int i = 0; i < 1000000; i++) {
+ nsCOMPtr<nsIFile> f;
+ rv = storageDir->Clone(getter_AddRefs(f));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ nsAutoString hashStr;
+ hashStr.AppendInt(recordNameHash);
+ rv = f->Append(hashStr);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ bool exists = false;
+ f->Exists(&exists);
+ if (!exists) {
+ // Filename not in use, we can write into this file.
+ aOutFilename = hashStr;
+ return NS_OK;
+ } else {
+ // Hash collision; just increment the hash name and try that again.
+ ++recordNameHash;
+ continue;
+ }
+ }
+ // Somehow, we've managed to completely fail to find a vacant file name.
+ // Give up.
+ NS_WARNING("GetUnusedFilename had extreme hash collision!");
+ return NS_ERROR_FAILURE;
+ }
+
+ enum OpenFileMode { ReadWrite, Truncate };
+
+ nsresult OpenStorageFile(const nsAString& aFileLeafName,
+ const OpenFileMode aMode, PRFileDesc** aOutFD) {
+ MOZ_ASSERT(aOutFD);
+
+ nsCOMPtr<nsIFile> f;
+ nsresult rv = GetGMPStorageDir(getter_AddRefs(f), mGMPName, mNodeId);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ f->Append(aFileLeafName);
+
+ auto mode = PR_RDWR | PR_CREATE_FILE;
+ if (aMode == Truncate) {
+ mode |= PR_TRUNCATE;
+ }
+
+ return f->OpenNSPRFileDesc(mode, PR_IRWXU, aOutFD);
+ }
+
+ nsresult ReadRecordMetadata(PRFileDesc* aFd, int32_t& aOutRecordLength,
+ nsACString& aOutRecordName) {
+ int32_t offset = PR_Seek(aFd, 0, PR_SEEK_END);
+ PR_Seek(aFd, 0, PR_SEEK_SET);
+
+ if (offset < 0 || offset > GMP_MAX_RECORD_SIZE) {
+ // Refuse to read big records, or records where we can't get a length.
+ return NS_ERROR_FAILURE;
+ }
+ const uint32_t fileLength = static_cast<uint32_t>(offset);
+
+ // At the start of the file the length of the record name is stored in a
+ // uint32_t (little endian byte order) followed by the record name at the
+ // start of the file. The record name is not null terminated. The remainder
+ // of the file is the record's data.
+
+ if (fileLength < sizeof(uint32_t)) {
+ // Record file doesn't have enough contents to store the record name
+ // length. Fail.
+ return NS_ERROR_FAILURE;
+ }
+
+ // Read length, and convert to host byte order.
+ uint32_t recordNameLength = 0;
+ char buf[sizeof(recordNameLength)] = {0};
+ int32_t bytesRead = PR_Read(aFd, &buf, sizeof(recordNameLength));
+ recordNameLength = LittleEndian::readUint32(buf);
+ if (sizeof(recordNameLength) != bytesRead || recordNameLength == 0 ||
+ recordNameLength + sizeof(recordNameLength) > fileLength ||
+ recordNameLength > GMP_MAX_RECORD_NAME_SIZE) {
+ // Record file has invalid contents. Fail.
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCString recordName;
+ recordName.SetLength(recordNameLength);
+ bytesRead = PR_Read(aFd, recordName.BeginWriting(), recordNameLength);
+ if ((uint32_t)bytesRead != recordNameLength) {
+ // Read failed.
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_ASSERT(fileLength >= sizeof(recordNameLength) + recordNameLength);
+ int32_t recordLength =
+ fileLength - (sizeof(recordNameLength) + recordNameLength);
+
+ aOutRecordLength = recordLength;
+ aOutRecordName = recordName;
+
+ // Read cursor should be positioned after the record name, before the record
+ // contents.
+ if (PR_Seek(aFd, 0, PR_SEEK_CUR) !=
+ (int32_t)(sizeof(recordNameLength) + recordNameLength)) {
+ NS_WARNING("Read cursor mismatch after ReadRecordMetadata()");
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+ }
+
+ nsresult RemoveStorageFile(const nsAString& aFilename) {
+ nsCOMPtr<nsIFile> f;
+ nsresult rv = GetGMPStorageDir(getter_AddRefs(f), mGMPName, mNodeId);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ rv = f->Append(aFilename);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ return f->Remove(/* bool recursive= */ false);
+ }
+
+ struct Record {
+ Record(const nsAString& aFilename, const nsACString& aRecordName)
+ : mFilename(aFilename), mRecordName(aRecordName), mFileDesc(0) {}
+ ~Record() { MOZ_ASSERT(!mFileDesc); }
+ nsString mFilename;
+ nsCString mRecordName;
+ PRFileDesc* mFileDesc;
+ };
+
+ // Hash record name to record data.
+ nsClassHashtable<nsCStringHashKey, Record> mRecords;
+ const nsCString mNodeId;
+ const nsString mGMPName;
+};
+
+already_AddRefed<GMPStorage> CreateGMPDiskStorage(const nsACString& aNodeId,
+ const nsAString& aGMPName) {
+ RefPtr<GMPDiskStorage> storage(new GMPDiskStorage(aNodeId, aGMPName));
+ if (NS_FAILED(storage->Init())) {
+ NS_WARNING("Failed to initialize on disk GMP storage");
+ return nullptr;
+ }
+ return storage.forget();
+}
+
+} // namespace mozilla::gmp
diff --git a/dom/media/gmp/GMPLoader.cpp b/dom/media/gmp/GMPLoader.cpp
new file mode 100644
index 0000000000..9fa7756165
--- /dev/null
+++ b/dom/media/gmp/GMPLoader.cpp
@@ -0,0 +1,203 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=4 et :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GMPLoader.h"
+#include <stdio.h>
+#include "mozilla/Attributes.h"
+#include "nsExceptionHandler.h"
+#include "gmp-entrypoints.h"
+#include "prlink.h"
+#include "prenv.h"
+#include "prerror.h"
+#if defined(XP_WIN) && defined(MOZ_SANDBOX)
+# include "mozilla/sandboxTarget.h"
+# include "mozilla/sandboxing/SandboxInitialization.h"
+# include "mozilla/sandboxing/sandboxLogging.h"
+#endif
+#if defined(XP_LINUX) && defined(MOZ_SANDBOX)
+# include "mozilla/Sandbox.h"
+# include "mozilla/SandboxInfo.h"
+#endif
+
+#include <string>
+
+#ifdef XP_WIN
+# include <windows.h>
+#endif
+
+namespace mozilla::gmp {
+class PassThroughGMPAdapter : public GMPAdapter {
+ public:
+ ~PassThroughGMPAdapter() override {
+ // Ensure we're always shutdown, even if caller forgets to call
+ // GMPShutdown().
+ GMPShutdown();
+ }
+
+ void SetAdaptee(PRLibrary* aLib) override { mLib = aLib; }
+
+ GMPErr GMPInit(const GMPPlatformAPI* aPlatformAPI) override {
+ if (NS_WARN_IF(!mLib)) {
+ MOZ_CRASH("Missing library!");
+ return GMPGenericErr;
+ }
+ GMPInitFunc initFunc =
+ reinterpret_cast<GMPInitFunc>(PR_FindFunctionSymbol(mLib, "GMPInit"));
+ if (!initFunc) {
+ MOZ_CRASH("Missing init method!");
+ return GMPNotImplementedErr;
+ }
+ return initFunc(aPlatformAPI);
+ }
+
+ GMPErr GMPGetAPI(const char* aAPIName, void* aHostAPI, void** aPluginAPI,
+ const nsACString& /* aKeySystem */) override {
+ if (!mLib) {
+ return GMPGenericErr;
+ }
+ GMPGetAPIFunc getapiFunc = reinterpret_cast<GMPGetAPIFunc>(
+ PR_FindFunctionSymbol(mLib, "GMPGetAPI"));
+ if (!getapiFunc) {
+ return GMPNotImplementedErr;
+ }
+ return getapiFunc(aAPIName, aHostAPI, aPluginAPI);
+ }
+
+ void GMPShutdown() override {
+ if (mLib) {
+ GMPShutdownFunc shutdownFunc = reinterpret_cast<GMPShutdownFunc>(
+ PR_FindFunctionSymbol(mLib, "GMPShutdown"));
+ if (shutdownFunc) {
+ shutdownFunc();
+ }
+ PR_UnloadLibrary(mLib);
+ mLib = nullptr;
+ }
+ }
+
+ private:
+ PRLibrary* mLib = nullptr;
+};
+
+bool GMPLoader::Load(const char* aUTF8LibPath, uint32_t aUTF8LibPathLen,
+ const GMPPlatformAPI* aPlatformAPI, GMPAdapter* aAdapter) {
+ CrashReporter::AutoAnnotateCrashReport autoLibPath(
+ CrashReporter::Annotation::GMPLibraryPath,
+ nsDependentCString(aUTF8LibPath));
+
+ if (!getenv("MOZ_DISABLE_GMP_SANDBOX") && mSandboxStarter &&
+ !mSandboxStarter->Start(aUTF8LibPath)) {
+ MOZ_CRASH("Cannot start sandbox!");
+ return false;
+ }
+
+ // Load the GMP.
+ PRLibSpec libSpec;
+#ifdef XP_WIN
+ int pathLen = MultiByteToWideChar(CP_UTF8, 0, aUTF8LibPath, -1, nullptr, 0);
+ if (pathLen == 0) {
+ MOZ_CRASH("Cannot get path length as wide char!");
+ return false;
+ }
+
+ auto widePath = MakeUnique<wchar_t[]>(pathLen);
+ if (MultiByteToWideChar(CP_UTF8, 0, aUTF8LibPath, -1, widePath.get(),
+ pathLen) == 0) {
+ MOZ_CRASH("Cannot convert path to wide char!");
+ return false;
+ }
+
+ libSpec.value.pathname_u = widePath.get();
+ libSpec.type = PR_LibSpec_PathnameU;
+#else
+ libSpec.value.pathname = aUTF8LibPath;
+ libSpec.type = PR_LibSpec_Pathname;
+#endif
+ PRLibrary* lib = PR_LoadLibraryWithFlags(libSpec, 0);
+ if (!lib) {
+ MOZ_CRASH_UNSAFE_PRINTF("Cannot load plugin as library %d %d",
+ PR_GetError(), PR_GetOSError());
+ return false;
+ }
+
+ mAdapter.reset((!aAdapter) ? new PassThroughGMPAdapter() : aAdapter);
+ mAdapter->SetAdaptee(lib);
+
+ if (mAdapter->GMPInit(aPlatformAPI) != GMPNoErr) {
+ MOZ_CRASH("Cannot initialize plugin adapter!");
+ return false;
+ }
+
+ return true;
+}
+
+GMPErr GMPLoader::GetAPI(const char* aAPIName, void* aHostAPI,
+ void** aPluginAPI, const nsACString& aKeySystem) {
+ return mAdapter->GMPGetAPI(aAPIName, aHostAPI, aPluginAPI, aKeySystem);
+}
+
+void GMPLoader::Shutdown() {
+ if (mAdapter) {
+ mAdapter->GMPShutdown();
+ }
+}
+
+#if defined(XP_WIN) && defined(MOZ_SANDBOX)
+class WinSandboxStarter : public mozilla::gmp::SandboxStarter {
+ public:
+ bool Start(const char* aLibPath) override {
+ // Cause advapi32 to load before the sandbox is turned on, as
+ // Widevine version 970 and later require it and the sandbox
+ // blocks it on Win7.
+ unsigned int dummy_rand;
+ rand_s(&dummy_rand);
+
+ mozilla::SandboxTarget::Instance()->StartSandbox();
+ return true;
+ }
+};
+#endif
+
+#if defined(XP_LINUX) && defined(MOZ_SANDBOX)
+namespace {
+class LinuxSandboxStarter : public mozilla::gmp::SandboxStarter {
+ private:
+ LinuxSandboxStarter() = default;
+ friend mozilla::detail::UniqueSelector<LinuxSandboxStarter>::SingleObject
+ mozilla::MakeUnique<LinuxSandboxStarter>();
+
+ public:
+ static UniquePtr<SandboxStarter> Make() {
+ if (mozilla::SandboxInfo::Get().CanSandboxMedia()) {
+ return MakeUnique<LinuxSandboxStarter>();
+ }
+ // Sandboxing isn't possible, but the parent has already
+ // checked that this plugin doesn't require it. (Bug 1074561)
+ return nullptr;
+ }
+ bool Start(const char* aLibPath) override {
+ mozilla::SetMediaPluginSandbox(aLibPath);
+ return true;
+ }
+};
+} // anonymous namespace
+#endif // XP_LINUX && MOZ_SANDBOX
+
+static UniquePtr<SandboxStarter> MakeSandboxStarter() {
+#if defined(XP_WIN) && defined(MOZ_SANDBOX)
+ return mozilla::MakeUnique<WinSandboxStarter>();
+#elif defined(XP_LINUX) && defined(MOZ_SANDBOX)
+ return LinuxSandboxStarter::Make();
+#else
+ return nullptr;
+#endif
+}
+
+GMPLoader::GMPLoader() : mSandboxStarter(MakeSandboxStarter()) {}
+
+bool GMPLoader::CanSandbox() const { return !!mSandboxStarter; }
+
+} // namespace mozilla::gmp
diff --git a/dom/media/gmp/GMPLoader.h b/dom/media/gmp/GMPLoader.h
new file mode 100644
index 0000000000..a0e1513d34
--- /dev/null
+++ b/dom/media/gmp/GMPLoader.h
@@ -0,0 +1,80 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=4 et :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GMP_LOADER_H__
+#define GMP_LOADER_H__
+
+#include <stdint.h>
+#include "prlink.h"
+#include "gmp-entrypoints.h"
+#include "mozilla/UniquePtr.h"
+#include "nsString.h"
+
+#if defined(XP_MACOSX) && defined(MOZ_SANDBOX)
+# include "mozilla/Sandbox.h"
+#endif
+
+namespace mozilla::gmp {
+
+class SandboxStarter {
+ public:
+ virtual ~SandboxStarter() = default;
+ virtual bool Start(const char* aLibPath) = 0;
+};
+
+// Interface that adapts a plugin to the GMP API.
+class GMPAdapter {
+ public:
+ virtual ~GMPAdapter() = default;
+ // Sets the adapted to plugin library module.
+ // Note: the GMPAdapter is responsible for calling PR_UnloadLibrary on aLib
+ // when it's finished with it.
+ virtual void SetAdaptee(PRLibrary* aLib) = 0;
+
+ // These are called in place of the corresponding GMP API functions.
+ virtual GMPErr GMPInit(const GMPPlatformAPI* aPlatformAPI) = 0;
+ // The `aKeySystem` arg is used to specify the key system when loading CDMs,
+ // and will be ignored by non-CDM GMPs. It is not part of the public GMP API
+ // Gecko exposes.
+ virtual GMPErr GMPGetAPI(const char* aAPIName, void* aHostAPI,
+ void** aPluginAPI, const nsACString& aKeySystem) = 0;
+ virtual void GMPShutdown() = 0;
+};
+
+// Encapsulates activating the sandbox, and loading the GMP.
+// Load() takes an optional GMPAdapter which can be used to adapt non-GMPs
+// to adhere to the GMP API.
+class GMPLoader {
+ public:
+ GMPLoader();
+
+ // Activates the sandbox, then loads the GMP library. If aAdapter is
+ // non-null, the lib path is assumed to be a non-GMP, and the adapter
+ // is initialized with the lib and the adapter is used to interact with
+ // the plugin.
+ bool Load(const char* aUTF8LibPath, uint32_t aLibPathLen,
+ const GMPPlatformAPI* aPlatformAPI, GMPAdapter* aAdapter = nullptr);
+
+ // Retrieves an interface pointer from the GMP. If the GMP is loading a CDM,
+ // aKeySystem is passed to the CDM to allow for key system specific
+ // configuration by the CDM.
+ GMPErr GetAPI(const char* aAPIName, void* aHostAPI, void** aPluginAPI,
+ const nsACString& aKeySystem);
+
+ // Calls the GMPShutdown function exported by the GMP lib, and unloads the
+ // plugin library.
+ void Shutdown();
+
+ bool CanSandbox() const;
+
+ private:
+ UniquePtr<SandboxStarter> mSandboxStarter;
+ UniquePtr<GMPAdapter> mAdapter;
+};
+
+} // namespace mozilla::gmp
+
+#endif // GMP_LOADER_H__
diff --git a/dom/media/gmp/GMPLog.h b/dom/media/gmp/GMPLog.h
new file mode 100644
index 0000000000..94e797e827
--- /dev/null
+++ b/dom/media/gmp/GMPLog.h
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_GMPLOG_H_
+#define DOM_MEDIA_GMPLOG_H_
+
+#include "content_decryption_module.h"
+#include "gmp-video-codec.h"
+#include "mozilla/Logging.h"
+
+namespace mozilla {
+
+extern LogModule* GetGMPLog();
+extern LogModule* GetGMPLibraryLog();
+extern GMPLogLevel GetGMPLibraryLogLevel();
+
+#define GMP_LOG_ERROR(msg, ...) \
+ MOZ_LOG(GetGMPLog(), LogLevel::Error, (msg, ##__VA_ARGS__))
+#define GMP_LOG_WARNING(msg, ...) \
+ MOZ_LOG(GetGMPLog(), LogLevel::Warning, (msg, ##__VA_ARGS__))
+#define GMP_LOG_INFO(msg, ...) \
+ MOZ_LOG(GetGMPLog(), LogLevel::Info, (msg, ##__VA_ARGS__))
+#define GMP_LOG_DEBUG(msg, ...) \
+ MOZ_LOG(GetGMPLog(), LogLevel::Debug, (msg, ##__VA_ARGS__))
+#define GMP_LOG_VERBOSE(msg, ...) \
+ MOZ_LOG(GetGMPLog(), LogLevel::Verbose, (msg, ##__VA_ARGS__))
+
+// Helpers
+
+inline const char* CdmStatusToString(cdm::Status aStatus) {
+ switch (aStatus) {
+ case cdm::Status::kSuccess:
+ return "success";
+ case cdm::Status::kNeedMoreData:
+ return "need more data";
+ case cdm::Status::kNoKey:
+ return "no key";
+ case cdm::Status::kInitializationError:
+ return "initialization error";
+ case cdm::Status::kDecryptError:
+ return "decrypt error";
+ case cdm::Status::kDecodeError:
+ return "decode error";
+ case cdm::Status::kDeferredInitialization:
+ return "deferred initialization";
+ default:
+ MOZ_ASSERT_UNREACHABLE("Should have coverage of entire enum");
+ return "unexpected status code"; // Gracefully handle disabled asserts.
+ }
+}
+
+inline const char* CdmStatusToString(uint32_t aStatus) {
+ return CdmStatusToString(cdm::Status(aStatus));
+}
+
+// End helpers
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_GMPLOG_H_
diff --git a/dom/media/gmp/GMPMemoryStorage.cpp b/dom/media/gmp/GMPMemoryStorage.cpp
new file mode 100644
index 0000000000..fe8cfee5f8
--- /dev/null
+++ b/dom/media/gmp/GMPMemoryStorage.cpp
@@ -0,0 +1,75 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GMPStorage.h"
+#include "nsClassHashtable.h"
+
+namespace mozilla::gmp {
+
+class GMPMemoryStorage : public GMPStorage {
+ public:
+ GMPErr Open(const nsACString& aRecordName) override {
+ MOZ_ASSERT(!IsOpen(aRecordName));
+
+ Record* record = mRecords.GetOrInsertNew(aRecordName);
+ record->mIsOpen = true;
+ return GMPNoErr;
+ }
+
+ bool IsOpen(const nsACString& aRecordName) const override {
+ const Record* record = mRecords.Get(aRecordName);
+ if (!record) {
+ return false;
+ }
+ return record->mIsOpen;
+ }
+
+ GMPErr Read(const nsACString& aRecordName,
+ nsTArray<uint8_t>& aOutBytes) override {
+ const Record* record = mRecords.Get(aRecordName);
+ if (!record) {
+ return GMPGenericErr;
+ }
+ aOutBytes = record->mData.Clone();
+ return GMPNoErr;
+ }
+
+ GMPErr Write(const nsACString& aRecordName,
+ const nsTArray<uint8_t>& aBytes) override {
+ Record* record = nullptr;
+ if (!mRecords.Get(aRecordName, &record)) {
+ return GMPClosedErr;
+ }
+ record->mData = aBytes.Clone();
+ return GMPNoErr;
+ }
+
+ void Close(const nsACString& aRecordName) override {
+ Record* record = nullptr;
+ if (!mRecords.Get(aRecordName, &record)) {
+ return;
+ }
+ if (!record->mData.Length()) {
+ // Record is empty, delete.
+ mRecords.Remove(aRecordName);
+ } else {
+ record->mIsOpen = false;
+ }
+ }
+
+ private:
+ struct Record {
+ nsTArray<uint8_t> mData;
+ bool mIsOpen = false;
+ };
+
+ nsClassHashtable<nsCStringHashKey, Record> mRecords;
+};
+
+already_AddRefed<GMPStorage> CreateGMPMemoryStorage() {
+ return RefPtr<GMPStorage>(new GMPMemoryStorage()).forget();
+}
+
+} // namespace mozilla::gmp
diff --git a/dom/media/gmp/GMPMessageUtils.h b/dom/media/gmp/GMPMessageUtils.h
new file mode 100644
index 0000000000..29740a49a6
--- /dev/null
+++ b/dom/media/gmp/GMPMessageUtils.h
@@ -0,0 +1,190 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GMPMessageUtils_h_
+#define GMPMessageUtils_h_
+
+#include "gmp-video-codec.h"
+#include "gmp-video-frame-encoded.h"
+#include "ipc/EnumSerializer.h"
+#include "ipc/IPCMessageUtilsSpecializations.h"
+#include "GMPNativeTypes.h"
+#include "GMPSanitizedExports.h"
+
+namespace IPC {
+
+template <>
+struct ParamTraits<GMPPluginType>
+ : public ContiguousEnumSerializerInclusive<
+ GMPPluginType, GMPPluginType::Unknown, GMPPluginType::Widevine> {};
+
+template <>
+struct ParamTraits<GMPErr>
+ : public ContiguousEnumSerializer<GMPErr, GMPNoErr, GMPLastErr> {};
+
+template <>
+struct ParamTraits<GMPVideoFrameType>
+ : public ContiguousEnumSerializer<GMPVideoFrameType, kGMPKeyFrame,
+ kGMPVideoFrameInvalid> {};
+
+template <>
+struct ParamTraits<GMPVideoCodecComplexity>
+ : public ContiguousEnumSerializer<GMPVideoCodecComplexity,
+ kGMPComplexityNormal,
+ kGMPComplexityInvalid> {};
+
+template <>
+struct ParamTraits<GMPVP8ResilienceMode>
+ : public ContiguousEnumSerializer<GMPVP8ResilienceMode, kResilienceOff,
+ kResilienceInvalid> {};
+
+template <>
+struct ParamTraits<GMPVideoCodecType>
+ : public ContiguousEnumSerializer<GMPVideoCodecType, kGMPVideoCodecVP8,
+ kGMPVideoCodecInvalid> {};
+
+template <>
+struct ParamTraits<GMPVideoCodecMode>
+ : public ContiguousEnumSerializer<GMPVideoCodecMode, kGMPRealtimeVideo,
+ kGMPCodecModeInvalid> {};
+
+template <>
+struct ParamTraits<GMPLogLevel>
+ : public ContiguousEnumSerializerInclusive<GMPLogLevel, kGMPLogDefault,
+ kGMPLogInvalid> {};
+
+template <>
+struct ParamTraits<GMPBufferType>
+ : public ContiguousEnumSerializer<GMPBufferType, GMP_BufferSingle,
+ GMP_BufferInvalid> {};
+
+template <>
+struct ParamTraits<cdm::EncryptionScheme>
+ : public ContiguousEnumSerializerInclusive<
+ cdm::EncryptionScheme, cdm::EncryptionScheme::kUnencrypted,
+ cdm::EncryptionScheme::kCbcs> {};
+
+template <>
+struct ParamTraits<cdm::HdcpVersion>
+ : public ContiguousEnumSerializerInclusive<
+ cdm::HdcpVersion, cdm::HdcpVersion::kHdcpVersionNone,
+ cdm::HdcpVersion::kHdcpVersion2_2> {};
+
+template <>
+struct ParamTraits<GMPSimulcastStream> {
+ typedef GMPSimulcastStream paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mWidth);
+ WriteParam(aWriter, aParam.mHeight);
+ WriteParam(aWriter, aParam.mNumberOfTemporalLayers);
+ WriteParam(aWriter, aParam.mMaxBitrate);
+ WriteParam(aWriter, aParam.mTargetBitrate);
+ WriteParam(aWriter, aParam.mMinBitrate);
+ WriteParam(aWriter, aParam.mQPMax);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ if (ReadParam(aReader, &(aResult->mWidth)) &&
+ ReadParam(aReader, &(aResult->mHeight)) &&
+ ReadParam(aReader, &(aResult->mNumberOfTemporalLayers)) &&
+ ReadParam(aReader, &(aResult->mMaxBitrate)) &&
+ ReadParam(aReader, &(aResult->mTargetBitrate)) &&
+ ReadParam(aReader, &(aResult->mMinBitrate)) &&
+ ReadParam(aReader, &(aResult->mQPMax))) {
+ return true;
+ }
+ return false;
+ }
+};
+
+template <>
+struct ParamTraits<GMPVideoCodec> {
+ typedef GMPVideoCodec paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mGMPApiVersion);
+ WriteParam(aWriter, aParam.mCodecType);
+ WriteParam(aWriter, static_cast<const nsCString&>(
+ nsDependentCString(aParam.mPLName)));
+ WriteParam(aWriter, aParam.mPLType);
+ WriteParam(aWriter, aParam.mWidth);
+ WriteParam(aWriter, aParam.mHeight);
+ WriteParam(aWriter, aParam.mStartBitrate);
+ WriteParam(aWriter, aParam.mMaxBitrate);
+ WriteParam(aWriter, aParam.mMinBitrate);
+ WriteParam(aWriter, aParam.mMaxFramerate);
+ WriteParam(aWriter, aParam.mFrameDroppingOn);
+ WriteParam(aWriter, aParam.mKeyFrameInterval);
+ WriteParam(aWriter, aParam.mQPMax);
+ WriteParam(aWriter, aParam.mNumberOfSimulcastStreams);
+ for (uint32_t i = 0; i < aParam.mNumberOfSimulcastStreams; i++) {
+ WriteParam(aWriter, aParam.mSimulcastStream[i]);
+ }
+ WriteParam(aWriter, aParam.mMode);
+ WriteParam(aWriter, aParam.mUseThreadedDecode);
+ WriteParam(aWriter, aParam.mLogLevel);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ // NOTE: make sure this matches any versions supported
+ if (!ReadParam(aReader, &(aResult->mGMPApiVersion)) ||
+ (aResult->mGMPApiVersion != kGMPVersion33 &&
+ aResult->mGMPApiVersion != kGMPVersion34)) {
+ return false;
+ }
+ if (!ReadParam(aReader, &(aResult->mCodecType))) {
+ return false;
+ }
+
+ nsAutoCString plName;
+ if (!ReadParam(aReader, &plName) ||
+ plName.Length() > kGMPPayloadNameSize - 1) {
+ return false;
+ }
+ memcpy(aResult->mPLName, plName.get(), plName.Length());
+ memset(aResult->mPLName + plName.Length(), 0,
+ kGMPPayloadNameSize - plName.Length());
+
+ if (!ReadParam(aReader, &(aResult->mPLType)) ||
+ !ReadParam(aReader, &(aResult->mWidth)) ||
+ !ReadParam(aReader, &(aResult->mHeight)) ||
+ !ReadParam(aReader, &(aResult->mStartBitrate)) ||
+ !ReadParam(aReader, &(aResult->mMaxBitrate)) ||
+ !ReadParam(aReader, &(aResult->mMinBitrate)) ||
+ !ReadParam(aReader, &(aResult->mMaxFramerate)) ||
+ !ReadParam(aReader, &(aResult->mFrameDroppingOn)) ||
+ !ReadParam(aReader, &(aResult->mKeyFrameInterval))) {
+ return false;
+ }
+
+ if (!ReadParam(aReader, &(aResult->mQPMax)) ||
+ !ReadParam(aReader, &(aResult->mNumberOfSimulcastStreams))) {
+ return false;
+ }
+
+ if (aResult->mNumberOfSimulcastStreams > kGMPMaxSimulcastStreams) {
+ return false;
+ }
+
+ for (uint32_t i = 0; i < aResult->mNumberOfSimulcastStreams; i++) {
+ if (!ReadParam(aReader, &(aResult->mSimulcastStream[i]))) {
+ return false;
+ }
+ }
+
+ if (!ReadParam(aReader, &(aResult->mMode)) ||
+ !ReadParam(aReader, &(aResult->mUseThreadedDecode)) ||
+ !ReadParam(aReader, &(aResult->mLogLevel))) {
+ return false;
+ }
+
+ return true;
+ }
+};
+
+} // namespace IPC
+
+#endif // GMPMessageUtils_h_
diff --git a/dom/media/gmp/GMPNativeTypes.h b/dom/media/gmp/GMPNativeTypes.h
new file mode 100644
index 0000000000..39cd6ddc05
--- /dev/null
+++ b/dom/media/gmp/GMPNativeTypes.h
@@ -0,0 +1,17 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GMPNativeTypes_h_
+#define GMPNativeTypes_h_
+
+enum class GMPPluginType {
+ Unknown,
+ Fake,
+ Clearkey,
+ OpenH264,
+ Widevine,
+};
+
+#endif
diff --git a/dom/media/gmp/GMPParent.cpp b/dom/media/gmp/GMPParent.cpp
new file mode 100644
index 0000000000..6c1acc3a20
--- /dev/null
+++ b/dom/media/gmp/GMPParent.cpp
@@ -0,0 +1,1209 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GMPParent.h"
+
+#include "CDMStorageIdProvider.h"
+#include "ChromiumCDMAdapter.h"
+#include "GMPContentParent.h"
+#include "GMPLog.h"
+#include "GMPTimerParent.h"
+#include "MediaResult.h"
+#include "mozIGeckoMediaPluginService.h"
+#include "mozilla/dom/KeySystemNames.h"
+#include "mozilla/dom/WidevineCDMManifestBinding.h"
+#include "mozilla/FOGIPC.h"
+#include "mozilla/ipc/CrashReporterHost.h"
+#include "mozilla/ipc/Endpoint.h"
+#include "mozilla/ipc/GeckoChildProcessHost.h"
+#if defined(XP_LINUX) && defined(MOZ_SANDBOX)
+# include "mozilla/SandboxInfo.h"
+# include "base/shared_memory.h"
+#endif
+#include "mozilla/Services.h"
+#include "mozilla/SSE.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/SyncRunnable.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/Unused.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIRunnable.h"
+#include "nsIObserverService.h"
+#include "nsIWritablePropertyBag2.h"
+#include "nsPrintfCString.h"
+#include "nsThreadUtils.h"
+#include "ProfilerParent.h"
+#include "runnable_utils.h"
+#ifdef XP_WIN
+# include "mozilla/FileUtilsWin.h"
+# include "WMFDecoderModule.h"
+#endif
+#if defined(MOZ_WIDGET_ANDROID)
+# include "mozilla/java/GeckoProcessManagerWrappers.h"
+# include "mozilla/java/GeckoProcessTypeWrappers.h"
+#endif // defined(MOZ_WIDGET_ANDROID)
+#if defined(XP_MACOSX)
+# include "nsMacUtilsImpl.h"
+# include "base/process_util.h"
+#endif // defined(XP_MACOSX)
+
+using mozilla::ipc::GeckoChildProcessHost;
+
+using CrashReporter::AnnotationTable;
+
+namespace mozilla::gmp {
+
+#define GMP_PARENT_LOG_DEBUG(x, ...) \
+ GMP_LOG_DEBUG("GMPParent[%p|childPid=%d] " x, this, mChildPid, ##__VA_ARGS__)
+
+#ifdef __CLASS__
+# undef __CLASS__
+#endif
+#define __CLASS__ "GMPParent"
+
+GMPParent::GMPParent()
+ : mState(GMPState::NotLoaded),
+ mPluginId(GeckoChildProcessHost::GetUniqueID()),
+ mProcess(nullptr),
+ mDeleteProcessOnlyOnUnload(false),
+ mAbnormalShutdownInProgress(false),
+ mIsBlockingDeletion(false),
+ mCanDecrypt(false),
+ mGMPContentChildCount(0),
+ mChildPid(0),
+#ifdef ALLOW_GECKO_CHILD_PROCESS_ARCH
+ mChildLaunchArch(base::PROCESS_ARCH_INVALID),
+#endif
+ mMainThread(GetMainThreadSerialEventTarget()) {
+ MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread());
+ GMP_PARENT_LOG_DEBUG("GMPParent ctor id=%u", mPluginId);
+}
+
+GMPParent::~GMPParent() {
+ // This method is not restricted to a specific thread.
+ GMP_PARENT_LOG_DEBUG("GMPParent dtor id=%u", mPluginId);
+ MOZ_ASSERT(!mProcess);
+}
+
+void GMPParent::CloneFrom(const GMPParent* aOther) {
+ MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread());
+ MOZ_ASSERT(aOther->mDirectory && aOther->mService, "null plugin directory");
+
+ mService = aOther->mService;
+ mDirectory = aOther->mDirectory;
+ mName = aOther->mName;
+ mVersion = aOther->mVersion;
+ mDescription = aOther->mDescription;
+ mDisplayName = aOther->mDisplayName;
+ mPluginType = aOther->mPluginType;
+#if defined(XP_WIN) || defined(XP_LINUX)
+ mLibs = aOther->mLibs;
+#endif
+ for (const GMPCapability& cap : aOther->mCapabilities) {
+ mCapabilities.AppendElement(cap);
+ }
+ mAdapter = aOther->mAdapter;
+
+#ifdef ALLOW_GECKO_CHILD_PROCESS_ARCH
+ mChildLaunchArch = aOther->mChildLaunchArch;
+#endif
+}
+
+#if defined(XP_WIN) || defined(XP_MACOSX)
+nsresult GMPParent::GetPluginFileArch(nsIFile* aPluginDir,
+ const nsString& aBaseName,
+ uint32_t& aArchSet) {
+ // Build up the plugin filename
+# if defined(XP_MACOSX)
+ nsAutoString pluginFileName = u"lib"_ns + aBaseName + u".dylib"_ns;
+# elif defined(XP_WIN)
+ nsAutoString pluginFileName = aBaseName + u".dll"_ns;
+# endif
+ GMP_PARENT_LOG_DEBUG("%s: pluginFileName: %s", __FUNCTION__,
+ NS_LossyConvertUTF16toASCII(pluginFileName).get());
+
+ // Create an nsIFile representing the plugin
+ nsCOMPtr<nsIFile> pluginFile;
+ nsresult rv = aPluginDir->Clone(getter_AddRefs(pluginFile));
+ NS_ENSURE_SUCCESS(rv, rv);
+ pluginFile->AppendRelativePath(pluginFileName);
+
+# if defined(XP_MACOSX)
+ // Get the full plugin path
+ nsAutoCString pluginPath;
+ rv = pluginFile->GetNativePath(pluginPath);
+ NS_ENSURE_SUCCESS(rv, rv);
+ GMP_PARENT_LOG_DEBUG("%s: pluginPath: %s", __FUNCTION__, pluginPath.get());
+
+ rv = nsMacUtilsImpl::GetArchitecturesForBinary(pluginPath.get(), &aArchSet);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+# if defined(__aarch64__)
+ mPluginFilePath = pluginPath;
+# endif
+# elif defined(XP_WIN)
+ // Get the full plugin path
+ nsAutoString pluginPath;
+ rv = pluginFile->GetTarget(pluginPath);
+ NS_ENSURE_SUCCESS(rv, rv);
+ GMP_PARENT_LOG_DEBUG("%s: pluginPath: %s", __FUNCTION__,
+ NS_LossyConvertUTF16toASCII(pluginPath).get());
+
+ aArchSet = GetExecutableArchitecture(pluginPath.get());
+ if (aArchSet == base::PROCESS_ARCH_INVALID) {
+ return NS_ERROR_FAILURE;
+ }
+# endif
+
+ return NS_OK;
+}
+#endif // defined(XP_WIN) || defined(XP_MACOSX)
+
+RefPtr<GenericPromise> GMPParent::Init(GeckoMediaPluginServiceParent* aService,
+ nsIFile* aPluginDir) {
+ MOZ_ASSERT(aPluginDir);
+ MOZ_ASSERT(aService);
+ MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread());
+
+ mService = aService;
+ mDirectory = aPluginDir;
+
+ // aPluginDir is <profile-dir>/<gmp-plugin-id>/<version>
+ // where <gmp-plugin-id> should be gmp-gmpopenh264
+ nsCOMPtr<nsIFile> parent;
+ nsresult rv = aPluginDir->GetParent(getter_AddRefs(parent));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return GenericPromise::CreateAndReject(rv, __func__);
+ }
+ nsAutoString parentLeafName;
+ rv = parent->GetLeafName(parentLeafName);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return GenericPromise::CreateAndReject(rv, __func__);
+ }
+ GMP_PARENT_LOG_DEBUG("%s: for %s", __FUNCTION__,
+ NS_LossyConvertUTF16toASCII(parentLeafName).get());
+
+ MOZ_ASSERT(parentLeafName.Length() > 4);
+ mName = Substring(parentLeafName, 4);
+
+#if defined(XP_WIN) || defined(XP_MACOSX)
+ uint32_t pluginArch = base::PROCESS_ARCH_INVALID;
+ rv = GetPluginFileArch(aPluginDir, mName, pluginArch);
+ if (NS_FAILED(rv)) {
+ GMP_PARENT_LOG_DEBUG("%s: Plugin arch error: %d", __FUNCTION__, rv);
+ } else {
+ GMP_PARENT_LOG_DEBUG("%s: Plugin arch: 0x%x", __FUNCTION__, pluginArch);
+ }
+
+ const uint32_t x86 = base::PROCESS_ARCH_X86_64 | base::PROCESS_ARCH_I386;
+# ifdef ALLOW_GECKO_CHILD_PROCESS_ARCH
+ const uint32_t arm64 = base::PROCESS_ARCH_ARM_64;
+
+ mChildLaunchArch = pluginArch;
+ // When executing in an ARM64 process, if the library is x86 or x64,
+ // set |mChildLaunchArch| to x64 and allow the library to be used as long
+ // as this process is a universal binary.
+ if (!(pluginArch & arm64) && (pluginArch & x86)) {
+ bool isWidevine = parentLeafName.Find(u"widevine") != kNotFound;
+ bool isWidevineAllowed =
+ StaticPrefs::media_gmp_widevinecdm_allow_x64_plugin_on_arm64();
+ bool isH264 = parentLeafName.Find(u"openh264") != kNotFound;
+ bool isH264Allowed =
+ StaticPrefs::media_gmp_gmpopenh264_allow_x64_plugin_on_arm64();
+ bool isClearkey = parentLeafName.Find(u"clearkey") != kNotFound;
+ bool isClearkeyAllowed =
+ StaticPrefs::media_gmp_gmpclearkey_allow_x64_plugin_on_arm64();
+
+ // Only allow x64 child GMP processes for Widevine, OpenH264 and Clearkey
+ if (!isWidevine && !isH264 && !isClearkey) {
+ return GenericPromise::CreateAndReject(NS_ERROR_NOT_IMPLEMENTED,
+ __func__);
+ }
+ // And only if prefs permit it.
+ if ((isWidevine && !isWidevineAllowed) || (isH264 && !isH264Allowed) ||
+ (isClearkey && !isClearkeyAllowed)) {
+ return GenericPromise::CreateAndReject(NS_ERROR_PLUGIN_DISABLED,
+ __func__);
+ }
+
+# ifdef XP_MACOSX
+ // We have an x64 library. Get the bundle architecture to determine
+ // if we are a universal binary and hence if we can launch an x64
+ // child process to host this plugin.
+ uint32_t bundleArch = base::PROCESS_ARCH_INVALID;
+ rv = nsMacUtilsImpl::GetArchitecturesForBundle(&bundleArch);
+ if (NS_FAILED(rv)) {
+ // If we fail here, continue as if this is not a univeral binary.
+ GMP_PARENT_LOG_DEBUG("%s: Bundle arch error: %d", __FUNCTION__, rv);
+ } else {
+ GMP_PARENT_LOG_DEBUG("%s: Bundle arch: 0x%x", __FUNCTION__, bundleArch);
+ }
+
+ bool isUniversalBinary = (bundleArch & base::PROCESS_ARCH_X86_64) &&
+ (bundleArch & base::PROCESS_ARCH_ARM_64);
+ if (isUniversalBinary) {
+ mChildLaunchArch = base::PROCESS_ARCH_X86_64;
+ PreTranslateBins();
+ } else {
+ return GenericPromise::CreateAndReject(NS_ERROR_NOT_IMPLEMENTED,
+ __func__);
+ }
+# endif
+ }
+# else
+ // When executing in a non-ARM process, if the library is not x86 or x64,
+ // remove it and return an error. This prevents a child process crash due
+ // to loading an incompatible library and forces a new plugin version to be
+ // downloaded when the check is next performed. This could occur if a profile
+ // is moved from an ARM64 system to an x64 system.
+ if ((pluginArch & x86) == 0) {
+ GMP_PARENT_LOG_DEBUG("%s: Removing plugin directory", __FUNCTION__);
+ aPluginDir->Remove(true);
+ return GenericPromise::CreateAndReject(NS_ERROR_NOT_IMPLEMENTED, __func__);
+ }
+# endif // defined(ALLOW_GECKO_CHILD_PROCESS_ARCH)
+#endif // defined(XP_WIN) || defined(XP_MACOSX)
+
+ return ReadGMPMetaData();
+}
+
+void GMPParent::Crash() {
+ if (mState != GMPState::NotLoaded) {
+ Unused << SendCrashPluginNow();
+ }
+}
+
+class NotifyGMPProcessLoadedTask : public Runnable {
+ public:
+ explicit NotifyGMPProcessLoadedTask(const ::base::ProcessId aProcessId,
+ GMPParent* aGMPParent)
+ : Runnable("NotifyGMPProcessLoadedTask"),
+ mProcessId(aProcessId),
+ mGMPParent(aGMPParent) {}
+
+ NS_IMETHOD Run() override {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ bool canProfile = true;
+
+#if defined(XP_LINUX) && defined(MOZ_SANDBOX)
+ if (SandboxInfo::Get().Test(SandboxInfo::kEnabledForMedia) &&
+ base::SharedMemory::UsingPosixShm()) {
+ canProfile = false;
+ }
+#endif
+
+ if (canProfile) {
+ nsCOMPtr<nsISerialEventTarget> gmpEventTarget =
+ mGMPParent->GMPEventTarget();
+ if (!gmpEventTarget) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ipc::Endpoint<PProfilerChild> profilerParent(
+ ProfilerParent::CreateForProcess(mProcessId));
+
+ gmpEventTarget->Dispatch(
+ NewRunnableMethod<ipc::Endpoint<mozilla::PProfilerChild>&&>(
+ "GMPParent::SendInitProfiler", mGMPParent,
+ &GMPParent::SendInitProfiler, std::move(profilerParent)));
+ }
+
+ return NS_OK;
+ }
+
+ ::base::ProcessId mProcessId;
+ const RefPtr<GMPParent> mGMPParent;
+};
+
+nsresult GMPParent::LoadProcess() {
+ MOZ_ASSERT(mDirectory, "Plugin directory cannot be NULL!");
+ MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread());
+ MOZ_ASSERT(mState == GMPState::NotLoaded);
+
+ nsAutoString path;
+ if (NS_WARN_IF(NS_FAILED(mDirectory->GetPath(path)))) {
+ return NS_ERROR_FAILURE;
+ }
+ GMP_PARENT_LOG_DEBUG("%s: for %s", __FUNCTION__,
+ NS_ConvertUTF16toUTF8(path).get());
+
+ if (!mProcess) {
+ mProcess = new GMPProcessParent(NS_ConvertUTF16toUTF8(path).get());
+#if defined(XP_MACOSX) && defined(MOZ_SANDBOX)
+ mProcess->SetRequiresWindowServer(mAdapter.EqualsLiteral("chromium"));
+#endif
+
+#ifdef ALLOW_GECKO_CHILD_PROCESS_ARCH
+ mProcess->SetLaunchArchitecture(mChildLaunchArch);
+#endif
+
+ if (!mProcess->Launch(30 * 1000)) {
+ GMP_PARENT_LOG_DEBUG("%s: Failed to launch new child process",
+ __FUNCTION__);
+ mProcess->Delete();
+ mProcess = nullptr;
+ return NS_ERROR_FAILURE;
+ }
+
+ mChildPid = mProcess->GetChildProcessId();
+ GMP_PARENT_LOG_DEBUG("%s: Launched new child process", __FUNCTION__);
+
+ bool opened = mProcess->TakeInitialEndpoint().Bind(this);
+ if (!opened) {
+ GMP_PARENT_LOG_DEBUG("%s: Failed to open channel to new child process",
+ __FUNCTION__);
+ mProcess->Delete();
+ mProcess = nullptr;
+ return NS_ERROR_FAILURE;
+ }
+ GMP_PARENT_LOG_DEBUG("%s: Opened channel to new child process",
+ __FUNCTION__);
+
+ // ComputeStorageId may return empty string, we leave the error handling to
+ // CDM. The CDM will reject the promise once we provide a empty string of
+ // storage id.
+ bool ok =
+ SendProvideStorageId(CDMStorageIdProvider::ComputeStorageId(mNodeId));
+ if (!ok) {
+ GMP_PARENT_LOG_DEBUG("%s: Failed to send storage id to child process",
+ __FUNCTION__);
+ return NS_ERROR_FAILURE;
+ }
+ GMP_PARENT_LOG_DEBUG("%s: Sent storage id to child process", __FUNCTION__);
+
+#if defined(XP_WIN) || defined(XP_LINUX)
+ if (!mLibs.IsEmpty()) {
+ bool ok = SendPreloadLibs(mLibs);
+ if (!ok) {
+ GMP_PARENT_LOG_DEBUG("%s: Failed to send preload-libs to child process",
+ __FUNCTION__);
+ return NS_ERROR_FAILURE;
+ }
+ GMP_PARENT_LOG_DEBUG("%s: Sent preload-libs ('%s') to child process",
+ __FUNCTION__, mLibs.get());
+ }
+#endif
+
+ NS_DispatchToMainThread(new NotifyGMPProcessLoadedTask(OtherPid(), this));
+
+ // Intr call to block initialization on plugin load.
+ if (!SendStartPlugin(mAdapter)) {
+ GMP_PARENT_LOG_DEBUG("%s: Failed to send start to child process",
+ __FUNCTION__);
+ return NS_ERROR_FAILURE;
+ }
+ GMP_PARENT_LOG_DEBUG("%s: Sent StartPlugin to child process", __FUNCTION__);
+ }
+
+ mState = GMPState::Loaded;
+
+ return NS_OK;
+}
+
+mozilla::ipc::IPCResult GMPParent::RecvPGMPContentChildDestroyed() {
+ --mGMPContentChildCount;
+ if (!IsUsed()) {
+ CloseIfUnused();
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPParent::RecvFOGData(ByteBuf&& aBuf) {
+ GMP_PARENT_LOG_DEBUG("GMPParent RecvFOGData");
+ glean::FOGData(std::move(aBuf));
+ return IPC_OK();
+}
+
+void GMPParent::CloseIfUnused() {
+ MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread());
+ GMP_PARENT_LOG_DEBUG("%s", __FUNCTION__);
+
+ if ((mDeleteProcessOnlyOnUnload || mState == GMPState::Loaded ||
+ mState == GMPState::Unloading) &&
+ !IsUsed()) {
+ // Ensure all timers are killed.
+ for (uint32_t i = mTimers.Length(); i > 0; i--) {
+ mTimers[i - 1]->Shutdown();
+ }
+
+ // Shutdown GMPStorage. Given that all protocol actors must be shutdown
+ // (!Used() is true), all storage operations should be complete.
+ for (size_t i = mStorage.Length(); i > 0; i--) {
+ mStorage[i - 1]->Shutdown();
+ }
+ Shutdown();
+ }
+}
+
+void GMPParent::CloseActive(bool aDieWhenUnloaded) {
+ MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread());
+ GMP_PARENT_LOG_DEBUG("%s: state %u", __FUNCTION__,
+ uint32_t(GMPState(mState)));
+
+ if (aDieWhenUnloaded) {
+ mDeleteProcessOnlyOnUnload = true; // don't allow this to go back...
+ }
+ if (mState == GMPState::Loaded) {
+ mState = GMPState::Unloading;
+ }
+ if (mState != GMPState::NotLoaded && IsUsed()) {
+ Unused << SendCloseActive();
+ CloseIfUnused();
+ }
+}
+
+void GMPParent::MarkForDeletion() {
+ mDeleteProcessOnlyOnUnload = true;
+ mIsBlockingDeletion = true;
+}
+
+bool GMPParent::IsMarkedForDeletion() { return mIsBlockingDeletion; }
+
+void GMPParent::Shutdown() {
+ MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread());
+ GMP_PARENT_LOG_DEBUG("%s", __FUNCTION__);
+
+ if (mAbnormalShutdownInProgress) {
+ return;
+ }
+
+ MOZ_ASSERT(!IsUsed());
+ if (mState == GMPState::NotLoaded || mState == GMPState::Closing) {
+ return;
+ }
+
+ RefPtr<GMPParent> self(this);
+ DeleteProcess();
+
+ // XXX Get rid of mDeleteProcessOnlyOnUnload and this code when
+ // Bug 1043671 is fixed
+ if (!mDeleteProcessOnlyOnUnload) {
+ // Destroy ourselves and rise from the fire to save memory
+ mService->ReAddOnGMPThread(self);
+ } // else we've been asked to die and stay dead
+ MOZ_ASSERT(mState == GMPState::NotLoaded);
+}
+
+class NotifyGMPShutdownTask : public Runnable {
+ public:
+ explicit NotifyGMPShutdownTask(const nsAString& aNodeId)
+ : Runnable("NotifyGMPShutdownTask"), mNodeId(aNodeId) {}
+ NS_IMETHOD Run() override {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCOMPtr<nsIObserverService> obsService =
+ mozilla::services::GetObserverService();
+ MOZ_ASSERT(obsService);
+ if (obsService) {
+ obsService->NotifyObservers(nullptr, "gmp-shutdown", mNodeId.get());
+ }
+ return NS_OK;
+ }
+ nsString mNodeId;
+};
+
+void GMPParent::ChildTerminated() {
+ RefPtr<GMPParent> self(this);
+ nsCOMPtr<nsISerialEventTarget> gmpEventTarget = GMPEventTarget();
+
+ if (!gmpEventTarget) {
+ // Bug 1163239 - this can happen on shutdown.
+ // PluginTerminated removes the GMP from the GMPService.
+ // On shutdown we can have this case where it is already been
+ // removed so there is no harm in not trying to remove it again.
+ GMP_PARENT_LOG_DEBUG("%s::%s: GMPEventTarget() returned nullptr.",
+ __CLASS__, __FUNCTION__);
+ } else {
+ gmpEventTarget->Dispatch(
+ NewRunnableMethod<RefPtr<GMPParent>>(
+ "gmp::GeckoMediaPluginServiceParent::PluginTerminated", mService,
+ &GeckoMediaPluginServiceParent::PluginTerminated, self),
+ NS_DISPATCH_NORMAL);
+ }
+}
+
+void GMPParent::DeleteProcess() {
+ MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread());
+ GMP_PARENT_LOG_DEBUG("%s", __FUNCTION__);
+
+ if (mState != GMPState::Closing) {
+ // Don't Close() twice!
+ // Probably remove when bug 1043671 is resolved
+ mState = GMPState::Closing;
+ Close();
+ }
+ mProcess->Delete(NewRunnableMethod("gmp::GMPParent::ChildTerminated", this,
+ &GMPParent::ChildTerminated));
+ GMP_PARENT_LOG_DEBUG("%s: Shut down process", __FUNCTION__);
+ mProcess = nullptr;
+
+#if defined(MOZ_WIDGET_ANDROID)
+ if (mState != GMPState::NotLoaded) {
+ nsCOMPtr<nsIEventTarget> launcherThread(GetIPCLauncher());
+ MOZ_ASSERT(launcherThread);
+
+ auto procType = java::GeckoProcessType::GMPLUGIN();
+ auto selector =
+ java::GeckoProcessManager::Selector::New(procType, OtherPid());
+
+ launcherThread->Dispatch(NS_NewRunnableFunction(
+ "GMPParent::DeleteProcess",
+ [selector =
+ java::GeckoProcessManager::Selector::GlobalRef(selector)]() {
+ java::GeckoProcessManager::ShutdownProcess(selector);
+ }));
+ }
+#endif // defined(MOZ_WIDGET_ANDROID)
+
+ mState = GMPState::NotLoaded;
+
+ nsCOMPtr<nsIRunnable> r =
+ new NotifyGMPShutdownTask(NS_ConvertUTF8toUTF16(mNodeId));
+ mMainThread->Dispatch(r.forget());
+}
+
+GMPState GMPParent::State() const { return mState; }
+
+nsCOMPtr<nsISerialEventTarget> GMPParent::GMPEventTarget() {
+ nsCOMPtr<mozIGeckoMediaPluginService> mps =
+ do_GetService("@mozilla.org/gecko-media-plugin-service;1");
+ MOZ_ASSERT(mps);
+ if (!mps) {
+ return nullptr;
+ }
+ // Note: GeckoMediaPluginService::GetThread() is threadsafe, and returns
+ // nullptr if the GeckoMediaPluginService has started shutdown.
+ nsCOMPtr<nsIThread> gmpThread;
+ mps->GetThread(getter_AddRefs(gmpThread));
+ return gmpThread;
+}
+
+/* static */
+bool GMPCapability::Supports(const nsTArray<GMPCapability>& aCapabilities,
+ const nsACString& aAPI,
+ const nsTArray<nsCString>& aTags) {
+ for (const nsCString& tag : aTags) {
+ if (!GMPCapability::Supports(aCapabilities, aAPI, tag)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+/* static */
+bool GMPCapability::Supports(const nsTArray<GMPCapability>& aCapabilities,
+ const nsACString& aAPI, const nsCString& aTag) {
+ for (const GMPCapability& capabilities : aCapabilities) {
+ if (!capabilities.mAPIName.Equals(aAPI)) {
+ continue;
+ }
+ for (const nsCString& tag : capabilities.mAPITags) {
+ if (tag.Equals(aTag)) {
+#ifdef XP_WIN
+ // Clearkey on Windows advertises that it can decode in its GMP info
+ // file, but uses Windows Media Foundation to decode. That's not present
+ // on Windows XP, and on some Vista, Windows N, and KN variants without
+ // certain services packs.
+ if (tag.EqualsLiteral(kClearKeyKeySystemName)) {
+ if (capabilities.mAPIName.EqualsLiteral(GMP_API_VIDEO_DECODER)) {
+ if (!WMFDecoderModule::CanCreateMFTDecoder(WMFStreamType::H264)) {
+ continue;
+ }
+ }
+ }
+#endif
+ return true;
+ }
+ }
+ }
+ return false;
+}
+
+bool GMPParent::EnsureProcessLoaded() {
+ switch (mState) {
+ case GMPState::NotLoaded:
+ return NS_SUCCEEDED(LoadProcess());
+ case GMPState::Loaded:
+ return true;
+ case GMPState::Unloading:
+ case GMPState::Closing:
+ return false;
+ }
+
+ MOZ_ASSERT_UNREACHABLE("Unhandled GMPState!");
+ return false;
+}
+
+void GMPParent::AddCrashAnnotations() {
+ if (mCrashReporter) {
+ mCrashReporter->AddAnnotation(CrashReporter::Annotation::GMPPlugin, true);
+ mCrashReporter->AddAnnotation(CrashReporter::Annotation::PluginFilename,
+ NS_ConvertUTF16toUTF8(mName));
+ mCrashReporter->AddAnnotation(CrashReporter::Annotation::PluginName,
+ mDisplayName);
+ mCrashReporter->AddAnnotation(CrashReporter::Annotation::PluginVersion,
+ mVersion);
+ }
+}
+
+void GMPParent::GetCrashID(nsString& aResult) {
+ AddCrashAnnotations();
+ GenerateCrashReport(OtherPid(), &aResult);
+}
+
+static void GMPNotifyObservers(const uint32_t aPluginID,
+ const nsACString& aPluginName,
+ const nsAString& aPluginDumpID) {
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ nsCOMPtr<nsIWritablePropertyBag2> propbag =
+ do_CreateInstance("@mozilla.org/hash-property-bag;1");
+ if (obs && propbag) {
+ propbag->SetPropertyAsUint32(u"pluginID"_ns, aPluginID);
+ propbag->SetPropertyAsACString(u"pluginName"_ns, aPluginName);
+ propbag->SetPropertyAsAString(u"pluginDumpID"_ns, aPluginDumpID);
+ obs->NotifyObservers(propbag, "gmp-plugin-crash", nullptr);
+ }
+
+ RefPtr<gmp::GeckoMediaPluginService> service =
+ gmp::GeckoMediaPluginService::GetGeckoMediaPluginService();
+ if (service) {
+ service->RunPluginCrashCallbacks(aPluginID, aPluginName);
+ }
+}
+
+void GMPParent::ActorDestroy(ActorDestroyReason aWhy) {
+ MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread());
+ GMP_PARENT_LOG_DEBUG("%s: (%d)", __FUNCTION__, (int)aWhy);
+
+ if (AbnormalShutdown == aWhy) {
+ Telemetry::Accumulate(Telemetry::SUBPROCESS_ABNORMAL_ABORT, "gmplugin"_ns,
+ 1);
+ nsString dumpID;
+ GetCrashID(dumpID);
+ if (dumpID.IsEmpty()) {
+ NS_WARNING("GMP crash without crash report");
+ dumpID = mName;
+ dumpID += '-';
+ AppendUTF8toUTF16(mVersion, dumpID);
+ }
+
+ // NotifyObservers is mainthread-only
+ nsCOMPtr<nsIRunnable> r =
+ WrapRunnableNM(&GMPNotifyObservers, mPluginId, mDisplayName, dumpID);
+ mMainThread->Dispatch(r.forget());
+ }
+
+ // warn us off trying to close again
+ mState = GMPState::Closing;
+ mAbnormalShutdownInProgress = true;
+ CloseActive(false);
+
+ // Normal Shutdown() will delete the process on unwind.
+ if (AbnormalShutdown == aWhy) {
+ RefPtr<GMPParent> self(this);
+ // Must not call Close() again in DeleteProcess(), as we'll recurse
+ // infinitely if we do.
+ MOZ_ASSERT(mState == GMPState::Closing);
+ DeleteProcess();
+ // Note: final destruction will be Dispatched to ourself
+ mService->ReAddOnGMPThread(self);
+ }
+}
+
+PGMPStorageParent* GMPParent::AllocPGMPStorageParent() {
+ GMPStorageParent* p = new GMPStorageParent(mNodeId, this);
+ mStorage.AppendElement(p); // Addrefs, released in DeallocPGMPStorageParent.
+ return p;
+}
+
+bool GMPParent::DeallocPGMPStorageParent(PGMPStorageParent* aActor) {
+ GMPStorageParent* p = static_cast<GMPStorageParent*>(aActor);
+ p->Shutdown();
+ mStorage.RemoveElement(p);
+ return true;
+}
+
+mozilla::ipc::IPCResult GMPParent::RecvPGMPStorageConstructor(
+ PGMPStorageParent* aActor) {
+ GMPStorageParent* p = (GMPStorageParent*)aActor;
+ if (NS_FAILED(p->Init())) {
+ // TODO: Verify if this is really a good reason to IPC_FAIL.
+ // There might be shutdown edge cases here.
+ return IPC_FAIL(this,
+ "GMPParent::RecvPGMPStorageConstructor: p->Init() failed.");
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPParent::RecvPGMPTimerConstructor(
+ PGMPTimerParent* actor) {
+ return IPC_OK();
+}
+
+PGMPTimerParent* GMPParent::AllocPGMPTimerParent() {
+ nsCOMPtr<nsISerialEventTarget> target = GMPEventTarget();
+ GMPTimerParent* p = new GMPTimerParent(target);
+ mTimers.AppendElement(
+ p); // Released in DeallocPGMPTimerParent, or on shutdown.
+ return p;
+}
+
+bool GMPParent::DeallocPGMPTimerParent(PGMPTimerParent* aActor) {
+ GMPTimerParent* p = static_cast<GMPTimerParent*>(aActor);
+ p->Shutdown();
+ mTimers.RemoveElement(p);
+ return true;
+}
+
+bool ReadInfoField(GMPInfoFileParser& aParser, const nsCString& aKey,
+ nsACString& aOutValue) {
+ if (!aParser.Contains(aKey) || aParser.Get(aKey).IsEmpty()) {
+ return false;
+ }
+ aOutValue = aParser.Get(aKey);
+ return true;
+}
+
+RefPtr<GenericPromise> GMPParent::ReadGMPMetaData() {
+ MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread());
+ MOZ_ASSERT(mDirectory, "Plugin directory cannot be NULL!");
+ MOZ_ASSERT(!mName.IsEmpty(), "Plugin mName cannot be empty!");
+
+ nsCOMPtr<nsIFile> infoFile;
+ nsresult rv = mDirectory->Clone(getter_AddRefs(infoFile));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return GenericPromise::CreateAndReject(rv, __func__);
+ }
+ infoFile->AppendRelativePath(mName + u".info"_ns);
+
+ if (FileExists(infoFile)) {
+ return ReadGMPInfoFile(infoFile);
+ }
+
+ // Maybe this is the Widevine adapted plugin?
+ nsCOMPtr<nsIFile> manifestFile;
+ rv = mDirectory->Clone(getter_AddRefs(manifestFile));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return GenericPromise::CreateAndReject(rv, __func__);
+ }
+ manifestFile->AppendRelativePath(u"manifest.json"_ns);
+ return ReadChromiumManifestFile(manifestFile);
+}
+
+#if defined(XP_LINUX)
+static void ApplyGlibcWorkaround(nsCString& aLibs) {
+ // These glibc libraries were merged into libc.so.6 as of glibc
+ // 2.34; they now exist only as stub libraries for compatibility and
+ // newly linked code won't depend on them, so we need to ensure
+ // they're loaded for plugins that may have been linked against a
+ // different version of glibc. (See also bug 1725828.)
+ if (!aLibs.IsEmpty()) {
+ aLibs.AppendLiteral(", ");
+ }
+ aLibs.AppendLiteral("libdl.so.2, libpthread.so.0, librt.so.1");
+}
+#endif
+
+#if defined(XP_WIN)
+static void ApplyOleaut32(nsCString& aLibs) {
+ // In the libwebrtc update in bug 1766646 an include of comdef.h for using
+ // _bstr_t was introduced. This resulted in a dependency on comsupp.lib which
+ // contains a `_variant_t vtMissing` that would get cleared in an exit
+ // handler. VariantClear is defined in oleaut32.dll, and so we'd try to load
+ // oleaut32.dll on exit but get denied by the sandbox.
+ // Note that we had includes of comdef.h before bug 1766646 but it is the use
+ // of _bstr_t that triggers the vtMissing exit handler.
+ // See bug 1788592 for details.
+ if (!aLibs.IsEmpty()) {
+ aLibs.AppendLiteral(", ");
+ }
+ aLibs.AppendLiteral("oleaut32.dll");
+}
+#endif
+
+RefPtr<GenericPromise> GMPParent::ReadGMPInfoFile(nsIFile* aFile) {
+ MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread());
+ GMPInfoFileParser parser;
+ if (!parser.Init(aFile)) {
+ return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+
+ nsAutoCString apis;
+ if (!ReadInfoField(parser, "name"_ns, mDisplayName) ||
+ !ReadInfoField(parser, "description"_ns, mDescription) ||
+ !ReadInfoField(parser, "version"_ns, mVersion) ||
+ !ReadInfoField(parser, "apis"_ns, apis)) {
+ return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+
+#if defined(XP_WIN) || defined(XP_LINUX)
+ // "Libraries" field is optional.
+ ReadInfoField(parser, "libraries"_ns, mLibs);
+#endif
+
+ UpdatePluginType();
+
+#ifdef XP_LINUX
+ // The glibc workaround (see above) isn't needed for clearkey
+ // because it's built along with the browser.
+ if (mPluginType != GMPPluginType::Clearkey) {
+ ApplyGlibcWorkaround(mLibs);
+ }
+#endif
+
+#ifdef XP_WIN
+ ApplyOleaut32(mLibs);
+#endif
+
+ nsTArray<nsCString> apiTokens;
+ SplitAt(", ", apis, apiTokens);
+ for (nsCString api : apiTokens) {
+ int32_t tagsStart = api.FindChar('[');
+ if (tagsStart == 0) {
+ // Not allowed to be the first character.
+ // API name must be at least one character.
+ continue;
+ }
+
+ GMPCapability cap;
+ if (tagsStart == -1) {
+ // No tags.
+ cap.mAPIName.Assign(api);
+ } else {
+ auto tagsEnd = api.FindChar(']');
+ if (tagsEnd == -1 || tagsEnd < tagsStart) {
+ // Invalid syntax, skip whole capability.
+ continue;
+ }
+
+ cap.mAPIName.Assign(Substring(api, 0, tagsStart));
+
+ if ((tagsEnd - tagsStart) > 1) {
+ const nsDependentCSubstring ts(
+ Substring(api, tagsStart + 1, tagsEnd - tagsStart - 1));
+ nsTArray<nsCString> tagTokens;
+ SplitAt(":", ts, tagTokens);
+ for (nsCString tag : tagTokens) {
+ cap.mAPITags.AppendElement(tag);
+ }
+ }
+ }
+
+ mCapabilities.AppendElement(std::move(cap));
+ }
+
+ if (mCapabilities.IsEmpty()) {
+ return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+
+ return GenericPromise::CreateAndResolve(true, __func__);
+}
+
+RefPtr<GenericPromise> GMPParent::ReadChromiumManifestFile(nsIFile* aFile) {
+ MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread());
+ nsAutoCString json;
+ if (!ReadIntoString(aFile, json, 5 * 1024)) {
+ return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+
+ // DOM JSON parsing needs to run on the main thread.
+ return InvokeAsync(mMainThread, this, __func__,
+ &GMPParent::ParseChromiumManifest,
+ NS_ConvertUTF8toUTF16(json));
+}
+
+static bool IsCDMAPISupported(
+ const mozilla::dom::WidevineCDMManifest& aManifest) {
+ nsresult ignored; // Note: ToInteger returns 0 on failure.
+ int32_t moduleVersion = aManifest.mX_cdm_module_versions.ToInteger(&ignored);
+ int32_t interfaceVersion =
+ aManifest.mX_cdm_interface_versions.ToInteger(&ignored);
+ int32_t hostVersion = aManifest.mX_cdm_host_versions.ToInteger(&ignored);
+ return ChromiumCDMAdapter::Supports(moduleVersion, interfaceVersion,
+ hostVersion);
+}
+
+RefPtr<GenericPromise> GMPParent::ParseChromiumManifest(
+ const nsAString& aJSON) {
+ GMP_PARENT_LOG_DEBUG("%s: for '%s'", __FUNCTION__,
+ NS_LossyConvertUTF16toASCII(aJSON).get());
+
+ MOZ_ASSERT(NS_IsMainThread());
+ mozilla::dom::WidevineCDMManifest m;
+ if (!m.Init(aJSON)) {
+ GMP_PARENT_LOG_DEBUG("%s: Failed to initialize json parser, failing.",
+ __FUNCTION__);
+ return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+
+ if (!IsCDMAPISupported(m)) {
+ GMP_PARENT_LOG_DEBUG("%s: CDM API not supported, failing.", __FUNCTION__);
+ return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+
+ CopyUTF16toUTF8(m.mName, mDisplayName);
+ CopyUTF16toUTF8(m.mDescription, mDescription);
+ CopyUTF16toUTF8(m.mVersion, mVersion);
+
+#if defined(XP_LINUX) && defined(MOZ_SANDBOX)
+ if (!mozilla::SandboxInfo::Get().CanSandboxMedia()) {
+ nsPrintfCString msg(
+ "GMPParent::ParseChromiumManifest: Plugin \"%s\" is an EME CDM"
+ " but this system can't sandbox it; not loading.",
+ mDisplayName.get());
+ printf_stderr("%s\n", msg.get());
+ GMP_PARENT_LOG_DEBUG("%s", msg.get());
+ return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+#endif
+
+ UpdatePluginType();
+
+ GMPCapability video;
+
+ // We hard code a few of the settings because they can't be stored in the
+ // widevine manifest without making our API different to widevine's.
+ switch (mPluginType) {
+ case GMPPluginType::Clearkey:
+ video.mAPITags.AppendElement(nsCString{kClearKeyKeySystemName});
+ video.mAPITags.AppendElement(
+ nsCString{kClearKeyWithProtectionQueryKeySystemName});
+#if XP_WIN
+ mLibs = nsLiteralCString(
+ "dxva2.dll, evr.dll, freebl3.dll, mfh264dec.dll, mfplat.dll, "
+ "msmpeg2vdec.dll, nss3.dll, softokn3.dll");
+#elif XP_LINUX
+ mLibs = "libfreeblpriv3.so, libsoftokn3.so"_ns;
+#endif
+ break;
+ case GMPPluginType::Widevine:
+ video.mAPITags.AppendElement(nsCString{kWidevineKeySystemName});
+#if XP_WIN
+ // psapi.dll added for GetMappedFileNameW, which could possibly be avoided
+ // in future versions, see bug 1383611 for details.
+ mLibs = "dxva2.dll, ole32.dll, psapi.dll, winmm.dll"_ns;
+#endif
+ break;
+ case GMPPluginType::Fake:
+ // The fake CDM just exposes a key system with id "fake".
+ video.mAPITags.AppendElement(nsCString{"fake"});
+#if XP_WIN
+ mLibs = "dxva2.dll, ole32.dll"_ns;
+#endif
+ break;
+ default:
+ GMP_PARENT_LOG_DEBUG("%s: Unrecognized key system: %s, failing.",
+ __FUNCTION__, mDisplayName.get());
+ return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+
+#ifdef XP_LINUX
+ ApplyGlibcWorkaround(mLibs);
+#endif
+
+#ifdef XP_WIN
+ ApplyOleaut32(mLibs);
+#endif
+
+ nsCString codecsString = NS_ConvertUTF16toUTF8(m.mX_cdm_codecs);
+ nsTArray<nsCString> codecs;
+ SplitAt(",", codecsString, codecs);
+
+ // Parse the codec strings in the manifest and map them to strings used
+ // internally by Gecko for capability recognition.
+ //
+ // Google's code to parse manifests can be used as a reference for strings
+ // the manifest may contain
+ // https://source.chromium.org/chromium/chromium/src/+/master:components/cdm/common/cdm_manifest.cc;l=74;drc=775880ced8a989191281e93854c7f2201f25068f
+ //
+ // Gecko's internal strings can be found at
+ // https://searchfox.org/mozilla-central/rev/ea63a0888d406fae720cf24f4727d87569a8cab5/dom/media/eme/MediaKeySystemAccess.cpp#149-155
+ for (const nsCString& chromiumCodec : codecs) {
+ nsCString codec;
+ if (chromiumCodec.EqualsASCII("vp8")) {
+ codec = "vp8"_ns;
+ } else if (chromiumCodec.EqualsASCII("vp9.0") || // Legacy string.
+ chromiumCodec.EqualsASCII("vp09")) {
+ codec = "vp9"_ns;
+ } else if (chromiumCodec.EqualsASCII("avc1")) {
+ codec = "h264"_ns;
+ } else if (chromiumCodec.EqualsASCII("av01")) {
+ codec = "av1"_ns;
+ } else {
+ GMP_PARENT_LOG_DEBUG("%s: Unrecognized codec: %s.", __FUNCTION__,
+ chromiumCodec.get());
+ MOZ_ASSERT_UNREACHABLE(
+ "Unhandled codec string! Need to add it to the parser.");
+ continue;
+ }
+
+ video.mAPITags.AppendElement(codec);
+ }
+
+ video.mAPIName = nsLiteralCString(CHROMIUM_CDM_API);
+ mAdapter = u"chromium"_ns;
+
+ mCapabilities.AppendElement(std::move(video));
+
+ GMP_PARENT_LOG_DEBUG("%s: Successfully parsed manifest.", __FUNCTION__);
+ return GenericPromise::CreateAndResolve(true, __func__);
+}
+
+bool GMPParent::CanBeSharedCrossNodeIds() const {
+ return mNodeId.IsEmpty() &&
+ // XXX bug 1159300 hack -- maybe remove after openh264 1.4
+ // We don't want to use CDM decoders for non-encrypted playback
+ // just yet; especially not for WebRTC. Don't allow CDMs to be used
+ // without a node ID.
+ !mCanDecrypt;
+}
+
+bool GMPParent::CanBeUsedFrom(const nsACString& aNodeId) const {
+ return mNodeId == aNodeId;
+}
+
+void GMPParent::SetNodeId(const nsACString& aNodeId) {
+ MOZ_ASSERT(!aNodeId.IsEmpty());
+ mNodeId = aNodeId;
+}
+
+void GMPParent::UpdatePluginType() {
+ if (mDisplayName.EqualsLiteral("WidevineCdm")) {
+ mPluginType = GMPPluginType::Widevine;
+ } else if (mDisplayName.EqualsLiteral("gmpopenh264")) {
+ mPluginType = GMPPluginType::OpenH264;
+ } else if (mDisplayName.EqualsLiteral("clearkey")) {
+ mPluginType = GMPPluginType::Clearkey;
+ } else if (mDisplayName.EqualsLiteral("fake")) {
+ mPluginType = GMPPluginType::Fake;
+ } else {
+ mPluginType = GMPPluginType::Unknown;
+ }
+}
+
+const nsCString& GMPParent::GetDisplayName() const { return mDisplayName; }
+
+const nsCString& GMPParent::GetVersion() const { return mVersion; }
+
+uint32_t GMPParent::GetPluginId() const { return mPluginId; }
+
+void GMPParent::ResolveGetContentParentPromises() {
+ nsTArray<UniquePtr<MozPromiseHolder<GetGMPContentParentPromise>>> promises =
+ std::move(mGetContentParentPromises);
+ MOZ_ASSERT(mGetContentParentPromises.IsEmpty());
+ RefPtr<GMPContentParent::CloseBlocker> blocker(
+ new GMPContentParent::CloseBlocker(mGMPContentParent));
+ for (auto& holder : promises) {
+ holder->Resolve(blocker, __func__);
+ }
+}
+
+bool GMPParent::OpenPGMPContent() {
+ MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread());
+ MOZ_ASSERT(!mGMPContentParent);
+
+ Endpoint<PGMPContentParent> parent;
+ Endpoint<PGMPContentChild> child;
+ if (NS_WARN_IF(NS_FAILED(PGMPContent::CreateEndpoints(
+ base::GetCurrentProcId(), OtherPid(), &parent, &child)))) {
+ return false;
+ }
+
+ mGMPContentParent = new GMPContentParent(this);
+
+ if (!parent.Bind(mGMPContentParent)) {
+ return false;
+ }
+
+ if (!SendInitGMPContentChild(std::move(child))) {
+ return false;
+ }
+
+ ResolveGetContentParentPromises();
+
+ return true;
+}
+
+void GMPParent::RejectGetContentParentPromises() {
+ nsTArray<UniquePtr<MozPromiseHolder<GetGMPContentParentPromise>>> promises =
+ std::move(mGetContentParentPromises);
+ MOZ_ASSERT(mGetContentParentPromises.IsEmpty());
+ for (auto& holder : promises) {
+ holder->Reject(NS_ERROR_FAILURE, __func__);
+ }
+}
+
+void GMPParent::GetGMPContentParent(
+ UniquePtr<MozPromiseHolder<GetGMPContentParentPromise>>&& aPromiseHolder) {
+ MOZ_ASSERT(GMPEventTarget()->IsOnCurrentThread());
+ GMP_PARENT_LOG_DEBUG("%s %p", __FUNCTION__, this);
+
+ if (mGMPContentParent) {
+ RefPtr<GMPContentParent::CloseBlocker> blocker(
+ new GMPContentParent::CloseBlocker(mGMPContentParent));
+ aPromiseHolder->Resolve(blocker, __func__);
+ } else {
+ mGetContentParentPromises.AppendElement(std::move(aPromiseHolder));
+ // If we don't have a GMPContentParent and we try to get one for the first
+ // time (mGetContentParentPromises.Length() == 1) then call
+ // PGMPContent::Open. If more calls to GetGMPContentParent happen before
+ // mGMPContentParent has been set then we should just store them, so that
+ // they get called when we set mGMPContentParent as a result of the
+ // PGMPContent::Open call.
+ if (mGetContentParentPromises.Length() == 1) {
+ if (!EnsureProcessLoaded() || !OpenPGMPContent()) {
+ RejectGetContentParentPromises();
+ return;
+ }
+ // We want to increment this as soon as possible, to avoid that we'd try
+ // to shut down the GMP process while we're still trying to get a
+ // PGMPContentParent actor.
+ ++mGMPContentChildCount;
+ }
+ }
+}
+
+already_AddRefed<GMPContentParent> GMPParent::ForgetGMPContentParent() {
+ MOZ_ASSERT(mGetContentParentPromises.IsEmpty());
+ return mGMPContentParent.forget();
+}
+
+bool GMPParent::EnsureProcessLoaded(base::ProcessId* aID) {
+ if (!EnsureProcessLoaded()) {
+ return false;
+ }
+ *aID = OtherPid();
+ return true;
+}
+
+void GMPParent::IncrementGMPContentChildCount() { ++mGMPContentChildCount; }
+
+nsString GMPParent::GetPluginBaseName() const { return u"gmp-"_ns + mName; }
+
+#if defined(XP_MACOSX) && defined(__aarch64__)
+void GMPParent::PreTranslateBins() {
+ nsCOMPtr<nsIRunnable> event = mozilla::NewRunnableMethod(
+ "RosettaTranslation", this, &GMPParent::PreTranslateBinsWorker);
+
+ DebugOnly<nsresult> rv =
+ NS_DispatchBackgroundTask(event.forget(), NS_DISPATCH_EVENT_MAY_BLOCK);
+
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+}
+
+void GMPParent::PreTranslateBinsWorker() {
+ int rv = nsMacUtilsImpl::PreTranslateXUL();
+ GMP_PARENT_LOG_DEBUG("%s: XUL translation result: %d", __FUNCTION__, rv);
+
+ rv = nsMacUtilsImpl::PreTranslateBinary(mPluginFilePath);
+ GMP_PARENT_LOG_DEBUG("%s: %s translation result: %d", __FUNCTION__,
+ mPluginFilePath.get(), rv);
+}
+#endif
+
+} // namespace mozilla::gmp
+
+#undef GMP_PARENT_LOG_DEBUG
+#undef __CLASS__
diff --git a/dom/media/gmp/GMPParent.h b/dom/media/gmp/GMPParent.h
new file mode 100644
index 0000000000..25fe1f88d1
--- /dev/null
+++ b/dom/media/gmp/GMPParent.h
@@ -0,0 +1,244 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GMPParent_h_
+#define GMPParent_h_
+
+#include "GMPNativeTypes.h"
+#include "GMPProcessParent.h"
+#include "GMPServiceParent.h"
+#include "GMPVideoDecoderParent.h"
+#include "GMPVideoEncoderParent.h"
+#include "GMPTimerParent.h"
+#include "GMPStorageParent.h"
+#include "mozilla/gmp/PGMPParent.h"
+#include "mozilla/ipc/CrashReporterHelper.h"
+#include "nsCOMPtr.h"
+#include "nscore.h"
+#include "nsISupports.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsIFile.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/MozPromise.h"
+
+namespace mozilla::gmp {
+
+class GMPCapability {
+ public:
+ explicit GMPCapability() = default;
+ GMPCapability(GMPCapability&& aOther)
+ : mAPIName(std::move(aOther.mAPIName)),
+ mAPITags(std::move(aOther.mAPITags)) {}
+ explicit GMPCapability(const nsACString& aAPIName) : mAPIName(aAPIName) {}
+ explicit GMPCapability(const GMPCapability& aOther) = default;
+ nsCString mAPIName;
+ CopyableTArray<nsCString> mAPITags;
+
+ static bool Supports(const nsTArray<GMPCapability>& aCapabilities,
+ const nsACString& aAPI,
+ const nsTArray<nsCString>& aTags);
+
+ static bool Supports(const nsTArray<GMPCapability>& aCapabilities,
+ const nsACString& aAPI, const nsCString& aTag);
+};
+
+enum class GMPState : uint32_t { NotLoaded, Loaded, Unloading, Closing };
+
+class GMPContentParent;
+
+class GMPParent final
+ : public PGMPParent,
+ public ipc::CrashReporterHelper<GeckoProcessType_GMPlugin> {
+ friend class PGMPParent;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GMPParent, final)
+
+ GMPParent();
+
+ RefPtr<GenericPromise> Init(GeckoMediaPluginServiceParent* aService,
+ nsIFile* aPluginDir);
+ void CloneFrom(const GMPParent* aOther);
+
+ void Crash();
+
+ nsresult LoadProcess();
+
+ // Called internally to close this if we don't need it
+ void CloseIfUnused();
+
+ // Notify all active de/encoders that we are closing, either because of
+ // normal shutdown or unexpected shutdown/crash.
+ void CloseActive(bool aDieWhenUnloaded);
+
+ // Tell the plugin to die after shutdown.
+ void MarkForDeletion();
+ bool IsMarkedForDeletion();
+
+ // Called by the GMPService to forcibly close active de/encoders at shutdown
+ void Shutdown();
+
+ // This must not be called while we're in the middle of abnormal ActorDestroy
+ void DeleteProcess();
+
+ GMPState State() const;
+ nsCOMPtr<nsISerialEventTarget> GMPEventTarget();
+
+ // A GMP can either be a single instance shared across all NodeIds (like
+ // in the OpenH264 case), or we can require a new plugin instance for every
+ // NodeIds running the plugin (as in the EME plugin case).
+ //
+ // A NodeId is a hash of the ($urlBarOrigin, $ownerDocOrigin) pair.
+ //
+ // Plugins are associated with an NodeIds by calling SetNodeId() before
+ // loading.
+ //
+ // If a plugin has no NodeId specified and it is loaded, it is assumed to
+ // be shared across NodeIds.
+
+ // Specifies that a GMP can only work with the specified NodeIds.
+ void SetNodeId(const nsACString& aNodeId);
+ const nsACString& GetNodeId() const { return mNodeId; }
+
+ const nsCString& GetDisplayName() const;
+ const nsCString& GetVersion() const;
+ uint32_t GetPluginId() const;
+ GMPPluginType GetPluginType() const { return mPluginType; }
+ nsString GetPluginBaseName() const;
+
+ // Returns true if a plugin can be or is being used across multiple NodeIds.
+ bool CanBeSharedCrossNodeIds() const;
+
+ // A GMP can be used from a NodeId if it's already been set to work with
+ // that NodeId, or if it's not been set to work with any NodeId and has
+ // not yet been loaded (i.e. it's not shared across NodeIds).
+ bool CanBeUsedFrom(const nsACString& aNodeId) const;
+
+ already_AddRefed<nsIFile> GetDirectory() {
+ return nsCOMPtr<nsIFile>(mDirectory).forget();
+ }
+
+ void AbortAsyncShutdown();
+
+ // Called when the child process has died.
+ void ChildTerminated();
+
+ bool OpenPGMPContent();
+
+ void GetGMPContentParent(
+ UniquePtr<MozPromiseHolder<GetGMPContentParentPromise>>&& aPromiseHolder);
+ already_AddRefed<GMPContentParent> ForgetGMPContentParent();
+
+ bool EnsureProcessLoaded(base::ProcessId* aID);
+
+ void IncrementGMPContentChildCount();
+
+ const nsTArray<GMPCapability>& GetCapabilities() const {
+ return mCapabilities;
+ }
+
+ private:
+ ~GMPParent();
+ void UpdatePluginType();
+
+ RefPtr<GeckoMediaPluginServiceParent> mService;
+ bool EnsureProcessLoaded();
+ RefPtr<GenericPromise> ReadGMPMetaData();
+ RefPtr<GenericPromise> ReadGMPInfoFile(nsIFile* aFile);
+ RefPtr<GenericPromise> ParseChromiumManifest(
+ const nsAString& aJSON); // Worker thread.
+ RefPtr<GenericPromise> ReadChromiumManifestFile(
+ nsIFile* aFile); // GMP thread.
+ void AddCrashAnnotations();
+ void GetCrashID(nsString& aResult);
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ mozilla::ipc::IPCResult RecvPGMPStorageConstructor(
+ PGMPStorageParent* actor) override;
+ PGMPStorageParent* AllocPGMPStorageParent();
+ bool DeallocPGMPStorageParent(PGMPStorageParent* aActor);
+
+ mozilla::ipc::IPCResult RecvPGMPTimerConstructor(
+ PGMPTimerParent* actor) override;
+ PGMPTimerParent* AllocPGMPTimerParent();
+ bool DeallocPGMPTimerParent(PGMPTimerParent* aActor);
+
+ mozilla::ipc::IPCResult RecvPGMPContentChildDestroyed();
+
+ mozilla::ipc::IPCResult RecvFOGData(ByteBuf&& aBuf);
+
+ bool IsUsed() {
+ return mGMPContentChildCount > 0 || !mGetContentParentPromises.IsEmpty();
+ }
+
+ void ResolveGetContentParentPromises();
+ void RejectGetContentParentPromises();
+
+#if defined(XP_MACOSX) && defined(__aarch64__)
+ // We pre-translate XUL and our plugin file to avoid x64 child process
+ // startup delays caused by translation for instances when the child
+ // process binary translations have not already been cached. i.e., the
+ // first time we launch an x64 child process after installation or
+ // update. Measured by binary size of a recent XUL and Widevine plugin,
+ // this makes up 94% of the translation needed. Re-translating the
+ // same binary does not cause translation to occur again.
+ void PreTranslateBins();
+ void PreTranslateBinsWorker();
+#endif
+
+#if defined(XP_WIN) || defined(XP_MACOSX)
+ nsresult GetPluginFileArch(nsIFile* aPluginDir, const nsString& aBaseName,
+ uint32_t& aArchSet);
+#endif
+
+ Atomic<GMPState> mState;
+ nsCOMPtr<nsIFile> mDirectory; // plugin directory on disk
+ nsString mName; // base name of plugin on disk, UTF-16 because used for paths
+ nsCString mDisplayName; // name of plugin displayed to users
+ nsCString mDescription; // description of plugin for display to users
+ nsCString mVersion;
+#if defined(XP_WIN) || defined(XP_LINUX)
+ nsCString mLibs;
+#endif
+ nsString mAdapter;
+ const uint32_t mPluginId;
+ GMPPluginType mPluginType = GMPPluginType::Unknown;
+ nsTArray<GMPCapability> mCapabilities;
+ GMPProcessParent* mProcess;
+ bool mDeleteProcessOnlyOnUnload;
+ bool mAbnormalShutdownInProgress;
+ bool mIsBlockingDeletion;
+
+ bool mCanDecrypt;
+
+ nsTArray<RefPtr<GMPTimerParent>> mTimers;
+ nsTArray<RefPtr<GMPStorageParent>> mStorage;
+ // NodeId the plugin is assigned to, or empty if the the plugin is not
+ // assigned to a NodeId.
+ nsCString mNodeId;
+ // This is used for GMP content in the parent, there may be more of these in
+ // the content processes.
+ RefPtr<GMPContentParent> mGMPContentParent;
+ nsTArray<UniquePtr<MozPromiseHolder<GetGMPContentParentPromise>>>
+ mGetContentParentPromises;
+ uint32_t mGMPContentChildCount;
+
+ int mChildPid;
+
+#ifdef ALLOW_GECKO_CHILD_PROCESS_ARCH
+ // The child process architecture to use.
+ uint32_t mChildLaunchArch;
+#endif
+#if defined(XP_MACOSX) && defined(__aarch64__)
+ nsCString mPluginFilePath;
+#endif
+
+ const nsCOMPtr<nsISerialEventTarget> mMainThread;
+};
+
+} // namespace mozilla::gmp
+
+#endif // GMPParent_h_
diff --git a/dom/media/gmp/GMPPlatform.cpp b/dom/media/gmp/GMPPlatform.cpp
new file mode 100644
index 0000000000..1a76015ed4
--- /dev/null
+++ b/dom/media/gmp/GMPPlatform.cpp
@@ -0,0 +1,281 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GMPPlatform.h"
+#include "GMPStorageChild.h"
+#include "GMPTimerChild.h"
+#include "mozilla/Monitor.h"
+#include "GMPChild.h"
+#include "mozilla/Mutex.h"
+#include "base/thread.h"
+#include "base/time.h"
+#include "mozilla/ReentrantMonitor.h"
+
+#include <ctime>
+
+namespace mozilla::gmp {
+
+static MessageLoop* sMainLoop = nullptr;
+static GMPChild* sChild = nullptr;
+
+static bool IsOnChildMainThread() {
+ return sMainLoop && sMainLoop == MessageLoop::current();
+}
+
+// We just need a refcounted wrapper for GMPTask objects.
+class GMPRunnable final {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GMPRunnable)
+
+ explicit GMPRunnable(GMPTask* aTask) : mTask(aTask) { MOZ_ASSERT(mTask); }
+
+ void Run() {
+ mTask->Run();
+ mTask->Destroy();
+ mTask = nullptr;
+ }
+
+ private:
+ ~GMPRunnable() = default;
+
+ GMPTask* mTask;
+};
+
+class GMPSyncRunnable final {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GMPSyncRunnable)
+
+ GMPSyncRunnable(GMPTask* aTask, MessageLoop* aMessageLoop)
+ : mDone(false),
+ mTask(aTask),
+ mMessageLoop(aMessageLoop),
+ mMonitor("GMPSyncRunnable") {
+ MOZ_ASSERT(mTask);
+ MOZ_ASSERT(mMessageLoop);
+ }
+
+ void Post() {
+ // We assert here for two reasons.
+ // 1) Nobody should be blocking the main thread.
+ // 2) This prevents deadlocks when doing sync calls to main which if the
+ // main thread tries to do a sync call back to the calling thread.
+ MOZ_ASSERT(!IsOnChildMainThread());
+
+ mMessageLoop->PostTask(NewRunnableMethod("gmp::GMPSyncRunnable::Run", this,
+ &GMPSyncRunnable::Run));
+ MonitorAutoLock lock(mMonitor);
+ while (!mDone) {
+ lock.Wait();
+ }
+ }
+
+ void Run() {
+ mTask->Run();
+ mTask->Destroy();
+ mTask = nullptr;
+ MonitorAutoLock lock(mMonitor);
+ mDone = true;
+ lock.Notify();
+ }
+
+ private:
+ ~GMPSyncRunnable() = default;
+
+ bool mDone;
+ GMPTask* mTask;
+ MessageLoop* mMessageLoop;
+ Monitor mMonitor MOZ_UNANNOTATED;
+};
+
+class GMPThreadImpl : public GMPThread {
+ public:
+ GMPThreadImpl();
+ virtual ~GMPThreadImpl();
+
+ // GMPThread
+ void Post(GMPTask* aTask) override;
+ void Join() override;
+
+ private:
+ Mutex mMutex MOZ_UNANNOTATED;
+ base::Thread mThread;
+};
+
+GMPErr CreateThread(GMPThread** aThread) {
+ if (!aThread) {
+ return GMPGenericErr;
+ }
+
+ *aThread = new GMPThreadImpl();
+
+ return GMPNoErr;
+}
+
+GMPErr RunOnMainThread(GMPTask* aTask) {
+ if (!aTask || !sMainLoop) {
+ return GMPGenericErr;
+ }
+
+ RefPtr<GMPRunnable> r = new GMPRunnable(aTask);
+ sMainLoop->PostTask(
+ NewRunnableMethod("gmp::GMPRunnable::Run", r, &GMPRunnable::Run));
+
+ return GMPNoErr;
+}
+
+GMPErr SyncRunOnMainThread(GMPTask* aTask) {
+ if (!aTask || !sMainLoop || IsOnChildMainThread()) {
+ return GMPGenericErr;
+ }
+
+ RefPtr<GMPSyncRunnable> r = new GMPSyncRunnable(aTask, sMainLoop);
+
+ r->Post();
+
+ return GMPNoErr;
+}
+
+class GMPMutexImpl : public GMPMutex {
+ public:
+ GMPMutexImpl();
+ virtual ~GMPMutexImpl();
+
+ // GMPMutex
+ void Acquire() override;
+ void Release() override;
+ void Destroy() override;
+
+ private:
+ ReentrantMonitor mMonitor MOZ_UNANNOTATED;
+};
+
+GMPErr CreateMutex(GMPMutex** aMutex) {
+ if (!aMutex) {
+ return GMPGenericErr;
+ }
+
+ *aMutex = new GMPMutexImpl();
+
+ return GMPNoErr;
+}
+
+GMPErr CreateRecord(const char* aRecordName, uint32_t aRecordNameSize,
+ GMPRecord** aOutRecord, GMPRecordClient* aClient) {
+ if (aRecordNameSize > GMP_MAX_RECORD_NAME_SIZE || aRecordNameSize == 0) {
+ NS_WARNING("GMP tried to CreateRecord with too long or 0 record name");
+ return GMPGenericErr;
+ }
+ GMPStorageChild* storage = sChild->GetGMPStorage();
+ if (!storage) {
+ return GMPGenericErr;
+ }
+ MOZ_ASSERT(storage);
+ return storage->CreateRecord(nsDependentCString(aRecordName, aRecordNameSize),
+ aOutRecord, aClient);
+}
+
+GMPErr SetTimerOnMainThread(GMPTask* aTask, int64_t aTimeoutMS) {
+ if (!aTask || !sMainLoop || !IsOnChildMainThread()) {
+ return GMPGenericErr;
+ }
+ GMPTimerChild* timers = sChild->GetGMPTimers();
+ NS_ENSURE_TRUE(timers, GMPGenericErr);
+ return timers->SetTimer(aTask, aTimeoutMS);
+}
+
+GMPErr GetClock(GMPTimestamp* aOutTime) {
+ if (!aOutTime) {
+ return GMPGenericErr;
+ }
+ *aOutTime = base::Time::Now().ToDoubleT() * 1000.0;
+ return GMPNoErr;
+}
+
+void InitPlatformAPI(GMPPlatformAPI& aPlatformAPI, GMPChild* aChild) {
+ if (!sMainLoop) {
+ sMainLoop = MessageLoop::current();
+ }
+ if (!sChild) {
+ sChild = aChild;
+ }
+
+ aPlatformAPI.version = 0;
+ aPlatformAPI.createthread = &CreateThread;
+ aPlatformAPI.runonmainthread = &RunOnMainThread;
+ aPlatformAPI.syncrunonmainthread = &SyncRunOnMainThread;
+ aPlatformAPI.createmutex = &CreateMutex;
+ aPlatformAPI.createrecord = &CreateRecord;
+ aPlatformAPI.settimer = &SetTimerOnMainThread;
+ aPlatformAPI.getcurrenttime = &GetClock;
+}
+
+void SendFOGData(ipc::ByteBuf&& buf) {
+ if (sChild) {
+ sChild->SendFOGData(std::move(buf));
+ }
+}
+
+GMPThreadImpl::GMPThreadImpl() : mMutex("GMPThreadImpl"), mThread("GMPThread") {
+ MOZ_COUNT_CTOR(GMPThread);
+}
+
+GMPThreadImpl::~GMPThreadImpl() { MOZ_COUNT_DTOR(GMPThread); }
+
+void GMPThreadImpl::Post(GMPTask* aTask) {
+ MutexAutoLock lock(mMutex);
+
+ if (!mThread.IsRunning()) {
+ bool started = mThread.Start();
+ if (!started) {
+ NS_WARNING("Unable to start GMPThread!");
+ return;
+ }
+ }
+
+ RefPtr<GMPRunnable> r = new GMPRunnable(aTask);
+ mThread.message_loop()->PostTask(
+ NewRunnableMethod("gmp::GMPRunnable::Run", r.get(), &GMPRunnable::Run));
+}
+
+void GMPThreadImpl::Join() {
+ {
+ MutexAutoLock lock(mMutex);
+ if (mThread.IsRunning()) {
+ mThread.Stop();
+ }
+ }
+ delete this;
+}
+
+GMPMutexImpl::GMPMutexImpl() : mMonitor("gmp-mutex") {
+ MOZ_COUNT_CTOR(GMPMutexImpl);
+}
+
+GMPMutexImpl::~GMPMutexImpl() { MOZ_COUNT_DTOR(GMPMutexImpl); }
+
+void GMPMutexImpl::Destroy() { delete this; }
+
+MOZ_PUSH_IGNORE_THREAD_SAFETY
+void GMPMutexImpl::Acquire() { mMonitor.Enter(); }
+
+void GMPMutexImpl::Release() { mMonitor.Exit(); }
+MOZ_POP_THREAD_SAFETY
+
+GMPTask* NewGMPTask(std::function<void()>&& aFunction) {
+ class Task : public GMPTask {
+ public:
+ explicit Task(std::function<void()>&& aFunction)
+ : mFunction(std::move(aFunction)) {}
+ void Destroy() override { delete this; }
+ ~Task() override = default;
+ void Run() override { mFunction(); }
+
+ private:
+ std::function<void()> mFunction;
+ };
+ return new Task(std::move(aFunction));
+}
+
+} // namespace mozilla::gmp
diff --git a/dom/media/gmp/GMPPlatform.h b/dom/media/gmp/GMPPlatform.h
new file mode 100644
index 0000000000..ee4571d856
--- /dev/null
+++ b/dom/media/gmp/GMPPlatform.h
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GMPPlatform_h_
+#define GMPPlatform_h_
+
+#include "gmp-platform.h"
+#include <functional>
+
+namespace mozilla::ipc {
+class ByteBuf;
+} // namespace mozilla::ipc
+
+namespace mozilla::gmp {
+
+class GMPChild;
+
+void InitPlatformAPI(GMPPlatformAPI& aPlatformAPI, GMPChild* aChild);
+
+GMPErr RunOnMainThread(GMPTask* aTask);
+
+GMPTask* NewGMPTask(std::function<void()>&& aFunction);
+
+GMPErr SetTimerOnMainThread(GMPTask* aTask, int64_t aTimeoutMS);
+
+void SendFOGData(ipc::ByteBuf&& buf);
+
+} // namespace mozilla::gmp
+
+#endif // GMPPlatform_h_
diff --git a/dom/media/gmp/GMPProcessChild.cpp b/dom/media/gmp/GMPProcessChild.cpp
new file mode 100644
index 0000000000..17c70079cb
--- /dev/null
+++ b/dom/media/gmp/GMPProcessChild.cpp
@@ -0,0 +1,45 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GMPProcessChild.h"
+
+#include "base/command_line.h"
+#include "base/string_util.h"
+#include "mozilla/ipc/IOThreadChild.h"
+#include "mozilla/BackgroundHangMonitor.h"
+
+using mozilla::ipc::IOThreadChild;
+
+namespace mozilla::gmp {
+
+GMPProcessChild::~GMPProcessChild() = default;
+
+bool GMPProcessChild::Init(int aArgc, char* aArgv[]) {
+ nsAutoString pluginFilename;
+
+#if defined(OS_POSIX)
+ // NB: need to be very careful in ensuring that the first arg
+ // (after the binary name) here is indeed the plugin module path.
+ // Keep in sync with dom/plugins/PluginModuleParent.
+ std::vector<std::string> values = CommandLine::ForCurrentProcess()->argv();
+ MOZ_ASSERT(values.size() >= 2, "not enough args");
+ CopyUTF8toUTF16(nsDependentCString(values[1].c_str()), pluginFilename);
+#elif defined(OS_WIN)
+ std::vector<std::wstring> values =
+ CommandLine::ForCurrentProcess()->GetLooseValues();
+ MOZ_ASSERT(values.size() >= 1, "not enough loose args");
+ pluginFilename = nsDependentString(values[0].c_str());
+#else
+# error Not implemented
+#endif
+
+ BackgroundHangMonitor::Startup();
+
+ return mPlugin->Init(pluginFilename, TakeInitialEndpoint());
+}
+
+void GMPProcessChild::CleanUp() { BackgroundHangMonitor::Shutdown(); }
+
+} // namespace mozilla::gmp
diff --git a/dom/media/gmp/GMPProcessChild.h b/dom/media/gmp/GMPProcessChild.h
new file mode 100644
index 0000000000..88d0141979
--- /dev/null
+++ b/dom/media/gmp/GMPProcessChild.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GMPProcessChild_h_
+#define GMPProcessChild_h_
+
+#include "mozilla/ipc/ProcessChild.h"
+#include "GMPChild.h"
+
+namespace mozilla::gmp {
+
+class GMPLoader;
+
+class GMPProcessChild final : public mozilla::ipc::ProcessChild {
+ protected:
+ typedef mozilla::ipc::ProcessChild ProcessChild;
+
+ public:
+ using ProcessChild::ProcessChild;
+ ~GMPProcessChild();
+
+ bool Init(int aArgc, char* aArgv[]) override;
+ void CleanUp() override;
+
+ private:
+ const RefPtr<GMPChild> mPlugin = new GMPChild;
+};
+
+} // namespace mozilla::gmp
+
+#endif // GMPProcessChild_h_
diff --git a/dom/media/gmp/GMPProcessParent.cpp b/dom/media/gmp/GMPProcessParent.cpp
new file mode 100644
index 0000000000..f5ffafcce4
--- /dev/null
+++ b/dom/media/gmp/GMPProcessParent.cpp
@@ -0,0 +1,297 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 et :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GMPProcessParent.h"
+#include "GMPUtils.h"
+#include "nsIRunnable.h"
+#ifdef XP_WIN
+# include "WinUtils.h"
+#endif
+#include "GMPLog.h"
+
+#include "base/string_util.h"
+#include "base/process_util.h"
+
+#include <string>
+
+#if defined(XP_MACOSX) && defined(MOZ_SANDBOX)
+# include "mozilla/Omnijar.h"
+# include "mozilla/Preferences.h"
+# include "mozilla/Sandbox.h"
+# include "mozilla/SandboxSettings.h"
+# include "nsMacUtilsImpl.h"
+#endif
+
+using std::string;
+using std::vector;
+
+using mozilla::gmp::GMPProcessParent;
+using mozilla::ipc::GeckoChildProcessHost;
+
+#ifdef MOZ_WIDGET_ANDROID
+static const int kInvalidFd = -1;
+#endif
+
+namespace mozilla::gmp {
+
+#if defined(XP_MACOSX) && defined(MOZ_SANDBOX)
+bool GMPProcessParent::sLaunchWithMacSandbox = true;
+bool GMPProcessParent::sMacSandboxGMPLogging = false;
+# if defined(DEBUG)
+bool GMPProcessParent::sIsMainThreadInitDone = false;
+# endif
+#endif
+
+#if defined(XP_MACOSX) && defined(MOZ_SANDBOX)
+/* static */
+void GMPProcessParent::InitStaticMainThread() {
+ // The GMPProcessParent constructor is called off the
+ // main thread. Do main thread initialization here.
+ MOZ_ASSERT(NS_IsMainThread());
+ sMacSandboxGMPLogging =
+ Preferences::GetBool("security.sandbox.logging.enabled") ||
+ PR_GetEnv("MOZ_SANDBOX_GMP_LOGGING") || PR_GetEnv("MOZ_SANDBOX_LOGGING");
+ GMP_LOG_DEBUG("GMPProcessParent::InitStaticMainThread: sandbox logging=%s",
+ sMacSandboxGMPLogging ? "true" : "false");
+# if defined(DEBUG)
+ sIsMainThreadInitDone = true;
+# endif
+}
+#endif
+
+GMPProcessParent::GMPProcessParent(const std::string& aGMPPath)
+ : GeckoChildProcessHost(GeckoProcessType_GMPlugin),
+ mGMPPath(aGMPPath)
+#if defined(XP_MACOSX) && defined(MOZ_SANDBOX)
+ ,
+ mRequiresWindowServer(false)
+#endif
+{
+ MOZ_COUNT_CTOR(GMPProcessParent);
+#if defined(XP_MACOSX) && defined(MOZ_SANDBOX)
+ MOZ_ASSERT(sIsMainThreadInitDone == true);
+ mDisableOSActivityMode = sLaunchWithMacSandbox;
+#endif
+}
+
+GMPProcessParent::~GMPProcessParent() { MOZ_COUNT_DTOR(GMPProcessParent); }
+
+bool GMPProcessParent::Launch(int32_t aTimeoutMs) {
+ vector<string> args;
+
+#ifdef ALLOW_GECKO_CHILD_PROCESS_ARCH
+ GMP_LOG_DEBUG("GMPProcessParent::Launch() mLaunchArch: %d", mLaunchArch);
+# if defined(XP_MACOSX)
+ mLaunchOptions->arch = mLaunchArch;
+ if (mLaunchArch == base::PROCESS_ARCH_X86_64) {
+ mLaunchOptions->env_map["MOZ_SHMEM_PAGESIZE_16K"] = 1;
+ }
+# endif
+#endif
+
+ // Resolve symlinks in the plugin path. The sandbox prevents
+ // resolving symlinks in the child process if access to link
+ // source file is denied.
+#ifdef XP_WIN
+ nsAutoString normalizedPath;
+#else
+ nsAutoCString normalizedPath;
+#endif
+ nsresult rv = NormalizePath(mGMPPath.c_str(), normalizedPath);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ GMP_LOG_DEBUG(
+ "GMPProcessParent::Launch: "
+ "plugin path normaliziation failed for path: %s",
+ mGMPPath.c_str());
+ }
+
+#ifdef XP_WIN
+ std::wstring wGMPPath;
+ if (NS_SUCCEEDED(rv)) {
+ wGMPPath = normalizedPath.get();
+ } else {
+ wGMPPath = UTF8ToWide(mGMPPath.c_str());
+ }
+
+ // The sandbox doesn't allow file system rules where the paths contain
+ // symbolic links or junction points. Sometimes the Users folder has been
+ // moved to another drive using a junction point, so allow for this specific
+ // case. See bug 1236680 for details.
+ if (NS_WARN_IF(
+ !widget::WinUtils::ResolveJunctionPointsAndSymLinks(wGMPPath))) {
+ GMP_LOG_DEBUG("ResolveJunctionPointsAndSymLinks failed for GMP path=%S",
+ wGMPPath.c_str());
+ return false;
+ }
+ GMP_LOG_DEBUG("GMPProcessParent::Launch() resolved path to %S",
+ wGMPPath.c_str());
+
+# ifdef MOZ_SANDBOX
+ // If the GMP path is a network path that is not mapped to a drive letter,
+ // then we need to fix the path format for the sandbox rule.
+ wchar_t volPath[MAX_PATH];
+ if (::GetVolumePathNameW(wGMPPath.c_str(), volPath, MAX_PATH) &&
+ ::GetDriveTypeW(volPath) == DRIVE_REMOTE &&
+ wGMPPath.compare(0, 2, L"\\\\") == 0) {
+ std::wstring sandboxGMPPath(wGMPPath);
+ sandboxGMPPath.insert(1, L"??\\UNC");
+ mAllowedFilesRead.push_back(sandboxGMPPath + L"\\*");
+ } else {
+ mAllowedFilesRead.push_back(wGMPPath + L"\\*");
+ }
+# endif
+
+ args.push_back(WideToUTF8(wGMPPath));
+#else
+ if (NS_SUCCEEDED(rv)) {
+ args.push_back(normalizedPath.get());
+ } else {
+ args.push_back(mGMPPath);
+ }
+#endif
+
+#ifdef MOZ_WIDGET_ANDROID
+ // Add dummy values for pref and pref map to the file descriptors remapping
+ // table. See bug 1440207 and 1481139.
+ AddFdToRemap(kInvalidFd, kInvalidFd);
+ AddFdToRemap(kInvalidFd, kInvalidFd);
+#endif
+ return SyncLaunch(args, aTimeoutMs);
+}
+
+void GMPProcessParent::Delete(nsCOMPtr<nsIRunnable> aCallback) {
+ mDeletedCallback = aCallback;
+ XRE_GetIOMessageLoop()->PostTask(NewNonOwningRunnableMethod(
+ "gmp::GMPProcessParent::DoDelete", this, &GMPProcessParent::DoDelete));
+}
+
+void GMPProcessParent::DoDelete() {
+ MOZ_ASSERT(MessageLoop::current() == XRE_GetIOMessageLoop());
+
+ if (mDeletedCallback) {
+ mDeletedCallback->Run();
+ }
+
+ Destroy();
+}
+
+#if defined(XP_MACOSX) && defined(MOZ_SANDBOX)
+bool GMPProcessParent::IsMacSandboxLaunchEnabled() {
+ return sLaunchWithMacSandbox;
+}
+
+void GMPProcessParent::SetRequiresWindowServer(bool aRequiresWindowServer) {
+ mRequiresWindowServer = aRequiresWindowServer;
+}
+
+bool GMPProcessParent::FillMacSandboxInfo(MacSandboxInfo& aInfo) {
+ aInfo.type = MacSandboxType_GMP;
+ aInfo.hasWindowServer = mRequiresWindowServer;
+ aInfo.shouldLog = (aInfo.shouldLog || sMacSandboxGMPLogging);
+ nsAutoCString appPath;
+ if (!nsMacUtilsImpl::GetAppPath(appPath)) {
+ GMP_LOG_DEBUG(
+ "GMPProcessParent::FillMacSandboxInfo: failed to get app path");
+ return false;
+ }
+ aInfo.appPath.assign(appPath.get());
+
+ GMP_LOG_DEBUG(
+ "GMPProcessParent::FillMacSandboxInfo: "
+ "plugin dir path: %s",
+ mGMPPath.c_str());
+ nsCOMPtr<nsIFile> pluginDir;
+ nsresult rv = NS_NewLocalFile(NS_ConvertUTF8toUTF16(mGMPPath.c_str()), true,
+ getter_AddRefs(pluginDir));
+ if (NS_FAILED(rv)) {
+ GMP_LOG_DEBUG(
+ "GMPProcessParent::FillMacSandboxInfo: "
+ "NS_NewLocalFile failed for plugin dir, rv=%d",
+ rv);
+ return false;
+ }
+
+ rv = pluginDir->Normalize();
+ if (NS_FAILED(rv)) {
+ GMP_LOG_DEBUG(
+ "GMPProcessParent::FillMacSandboxInfo: "
+ "failed to normalize plugin dir path, rv=%d",
+ rv);
+ return false;
+ }
+
+ nsAutoCString resolvedPluginPath;
+ pluginDir->GetNativePath(resolvedPluginPath);
+ aInfo.pluginPath.assign(resolvedPluginPath.get());
+ GMP_LOG_DEBUG(
+ "GMPProcessParent::FillMacSandboxInfo: "
+ "resolved plugin dir path: %s",
+ resolvedPluginPath.get());
+
+ if (!mozilla::IsPackagedBuild()) {
+ GMP_LOG_DEBUG(
+ "GMPProcessParent::FillMacSandboxInfo: IsPackagedBuild()=false");
+
+ // Repo dir
+ nsCOMPtr<nsIFile> repoDir;
+ rv = nsMacUtilsImpl::GetRepoDir(getter_AddRefs(repoDir));
+ if (NS_FAILED(rv)) {
+ GMP_LOG_DEBUG(
+ "GMPProcessParent::FillMacSandboxInfo: failed to get repo dir");
+ return false;
+ }
+ nsCString repoDirPath;
+ Unused << repoDir->GetNativePath(repoDirPath);
+ aInfo.testingReadPath1 = repoDirPath.get();
+ GMP_LOG_DEBUG(
+ "GMPProcessParent::FillMacSandboxInfo: "
+ "repo dir path: %s",
+ repoDirPath.get());
+
+ // Object dir
+ nsCOMPtr<nsIFile> objDir;
+ rv = nsMacUtilsImpl::GetObjDir(getter_AddRefs(objDir));
+ if (NS_FAILED(rv)) {
+ GMP_LOG_DEBUG(
+ "GMPProcessParent::FillMacSandboxInfo: failed to get object dir");
+ return false;
+ }
+ nsCString objDirPath;
+ Unused << objDir->GetNativePath(objDirPath);
+ aInfo.testingReadPath2 = objDirPath.get();
+ GMP_LOG_DEBUG(
+ "GMPProcessParent::FillMacSandboxInfo: "
+ "object dir path: %s",
+ objDirPath.get());
+ }
+ return true;
+}
+#endif
+
+nsresult GMPProcessParent::NormalizePath(const char* aPath,
+ PathString& aNormalizedPath) {
+ nsCOMPtr<nsIFile> fileOrDir;
+ nsresult rv = NS_NewLocalFile(NS_ConvertUTF8toUTF16(aPath), true,
+ getter_AddRefs(fileOrDir));
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = fileOrDir->Normalize();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+#ifdef XP_WIN
+ return fileOrDir->GetTarget(aNormalizedPath);
+#else
+ bool isLink = false;
+ rv = fileOrDir->IsSymlink(&isLink);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (isLink) {
+ return fileOrDir->GetNativeTarget(aNormalizedPath);
+ }
+ return fileOrDir->GetNativePath(aNormalizedPath);
+#endif
+}
+
+} // namespace mozilla::gmp
diff --git a/dom/media/gmp/GMPProcessParent.h b/dom/media/gmp/GMPProcessParent.h
new file mode 100644
index 0000000000..efe6bff17f
--- /dev/null
+++ b/dom/media/gmp/GMPProcessParent.h
@@ -0,0 +1,103 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=4 et :
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GMPProcessParent_h
+#define GMPProcessParent_h 1
+
+#include "mozilla/Attributes.h"
+#include "base/basictypes.h"
+#include "base/file_path.h"
+#include "base/thread.h"
+#include "chrome/common/child_process_host.h"
+#include "mozilla/ipc/GeckoChildProcessHost.h"
+#include "nsIFile.h"
+
+class nsIRunnable;
+
+namespace mozilla::gmp {
+
+class GMPProcessParent final : public mozilla::ipc::GeckoChildProcessHost {
+ public:
+ explicit GMPProcessParent(const std::string& aGMPPath);
+
+ // Synchronously launch the plugin process. If the process fails to launch
+ // after timeoutMs, this method will return false.
+ bool Launch(int32_t aTimeoutMs);
+
+ void Delete(nsCOMPtr<nsIRunnable> aCallback = nullptr);
+
+ bool CanShutdown() override { return true; }
+ const std::string& GetPluginFilePath() { return mGMPPath; }
+
+#if defined(XP_MACOSX) && defined(MOZ_SANDBOX)
+ // Init static members on the main thread
+ static void InitStaticMainThread();
+
+ // Read prefs and environment variables to determine
+ // when and if to start the Mac sandbox for the child
+ // process. Starting the sandbox at launch is the new
+ // preferred method. Code to support starting the sandbox
+ // later at plugin start time should be removed once
+ // starting at launch is stable and shipping.
+ bool IsMacSandboxLaunchEnabled() override;
+
+ // For process sandboxing purposes, set whether or not this
+ // instance of the GMP process requires access to the macOS
+ // window server. At present, Widevine requires window server
+ // access, but OpenH264 decoding does not.
+ void SetRequiresWindowServer(bool aRequiresWindowServer);
+
+ // Return the sandbox type to be used with this process type.
+ static MacSandboxType GetMacSandboxType() { return MacSandboxType_GMP; };
+#endif
+
+ using mozilla::ipc::GeckoChildProcessHost::GetChannel;
+ using mozilla::ipc::GeckoChildProcessHost::GetChildProcessHandle;
+
+ private:
+ ~GMPProcessParent();
+
+ void DoDelete();
+
+ std::string mGMPPath;
+ nsCOMPtr<nsIRunnable> mDeletedCallback;
+
+#if defined(XP_MACOSX) && defined(MOZ_SANDBOX)
+ // Indicates whether we'll start the Mac GMP sandbox during
+ // process launch (earlyinit) which is the new preferred method
+ // or later in the process lifetime.
+ static bool sLaunchWithMacSandbox;
+
+ // Whether or not Mac sandbox violation logging is enabled.
+ static bool sMacSandboxGMPLogging;
+
+ // Override so we can set GMP-specific sandbox parameters
+ bool FillMacSandboxInfo(MacSandboxInfo& aInfo) override;
+
+ // Controls whether or not the sandbox will be configured with
+ // window service access.
+ bool mRequiresWindowServer;
+
+# if defined(DEBUG)
+ // Used to assert InitStaticMainThread() is called before the constructor.
+ static bool sIsMainThreadInitDone;
+# endif
+#endif
+
+ // For normalizing paths to be compatible with sandboxing.
+ // We use normalized paths to generate the sandbox ruleset. Once
+ // the sandbox has been started, resolving symlinks that point to
+ // allowed directories could require reading paths not allowed by
+ // the sandbox, so we should only attempt to load plugin libraries
+ // using normalized paths.
+ static nsresult NormalizePath(const char* aPath, PathString& aNormalizedPath);
+
+ DISALLOW_COPY_AND_ASSIGN(GMPProcessParent);
+};
+
+} // namespace mozilla::gmp
+
+#endif // ifndef GMPProcessParent_h
diff --git a/dom/media/gmp/GMPSanitizedExports.h b/dom/media/gmp/GMPSanitizedExports.h
new file mode 100644
index 0000000000..76c00dba9b
--- /dev/null
+++ b/dom/media/gmp/GMPSanitizedExports.h
@@ -0,0 +1,22 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+// This file exposes symbols from the CDM headers + undefines macros which
+// X11 defines and which clash with the CDM headers.
+//
+// Ideally we can prune where X11 headers are included so that we don't need
+// this, but this band aid is useful until then.
+
+#ifndef DOM_MEDIA_GMP_GMP_API_GMP_SANITIZED_CDM_EXPORTS_H_
+#define DOM_MEDIA_GMP_GMP_API_GMP_SANITIZED_CDM_EXPORTS_H_
+
+// If Status is defined undef it so we don't break the CDM headers.
+#ifdef Status
+# undef Status
+#endif // Status
+#include "content_decryption_module.h"
+
+#endif // DOM_MEDIA_GMP_GMP_API_GMP_SANITIZED_CDM_EXPORTS_H_
diff --git a/dom/media/gmp/GMPService.cpp b/dom/media/gmp/GMPService.cpp
new file mode 100644
index 0000000000..1d2444a4aa
--- /dev/null
+++ b/dom/media/gmp/GMPService.cpp
@@ -0,0 +1,570 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GMPService.h"
+
+#include "ChromiumCDMParent.h"
+#include "GMPLog.h"
+#include "GMPParent.h"
+#include "GMPProcessParent.h"
+#include "GMPServiceChild.h"
+#include "GMPServiceParent.h"
+#include "GMPVideoDecoderParent.h"
+#include "mozilla/ipc/GeckoChildProcessHost.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/PluginCrashedEvent.h"
+#include "nsThreadUtils.h"
+#if defined(XP_LINUX) && defined(MOZ_SANDBOX)
+# include "mozilla/SandboxInfo.h"
+#endif
+#include "VideoUtils.h"
+#include "mozilla/Services.h"
+#include "mozilla/SyncRunnable.h"
+#include "mozilla/Unused.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsComponentManagerUtils.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsHashKeys.h"
+#include "nsIObserverService.h"
+#include "nsIXULAppInfo.h"
+#include "nsNativeCharsetUtils.h"
+#include "nsXPCOMPrivate.h"
+#include "prio.h"
+#include "runnable_utils.h"
+
+namespace mozilla {
+
+LogModule* GetGMPLog() {
+ static LazyLogModule sLog("GMP");
+ return sLog;
+}
+
+LogModule* GetGMPLibraryLog() {
+ static LazyLogModule sLog("GMPLibrary");
+ return sLog;
+}
+
+GMPLogLevel GetGMPLibraryLogLevel() {
+ switch (GetGMPLibraryLog()->Level()) {
+ case LogLevel::Disabled:
+ return kGMPLogQuiet;
+ case LogLevel::Error:
+ return kGMPLogError;
+ case LogLevel::Warning:
+ return kGMPLogWarning;
+ case LogLevel::Info:
+ return kGMPLogInfo;
+ case LogLevel::Debug:
+ return kGMPLogDebug;
+ case LogLevel::Verbose:
+ return kGMPLogDetail;
+ }
+ return kGMPLogInvalid;
+}
+
+#ifdef __CLASS__
+# undef __CLASS__
+#endif
+#define __CLASS__ "GMPService"
+
+namespace gmp {
+
+static StaticRefPtr<GeckoMediaPluginService> sSingletonService;
+
+class GMPServiceCreateHelper final : public mozilla::Runnable {
+ RefPtr<GeckoMediaPluginService> mService;
+
+ public:
+ static already_AddRefed<GeckoMediaPluginService> GetOrCreate() {
+ RefPtr<GeckoMediaPluginService> service;
+
+ if (NS_IsMainThread()) {
+ service = GetOrCreateOnMainThread();
+ } else {
+ RefPtr<GMPServiceCreateHelper> createHelper =
+ new GMPServiceCreateHelper();
+
+ mozilla::SyncRunnable::DispatchToThread(GetMainThreadSerialEventTarget(),
+ createHelper, true);
+
+ service = std::move(createHelper->mService);
+ }
+
+ return service.forget();
+ }
+
+ private:
+ GMPServiceCreateHelper() : Runnable("GMPServiceCreateHelper") {}
+
+ ~GMPServiceCreateHelper() { MOZ_ASSERT(!mService); }
+
+ static already_AddRefed<GeckoMediaPluginService> GetOrCreateOnMainThread() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!sSingletonService) {
+ if (XRE_IsParentProcess()) {
+ RefPtr<GeckoMediaPluginServiceParent> service =
+ new GeckoMediaPluginServiceParent();
+ if (NS_WARN_IF(NS_FAILED(service->Init()))) {
+ return nullptr;
+ }
+ sSingletonService = service;
+#if defined(XP_MACOSX) && defined(MOZ_SANDBOX)
+ // GMPProcessParent should only be instantiated in the parent
+ // so initialization only needs to be done in the parent.
+ GMPProcessParent::InitStaticMainThread();
+#endif
+ } else {
+ RefPtr<GeckoMediaPluginServiceChild> service =
+ new GeckoMediaPluginServiceChild();
+ if (NS_WARN_IF(NS_FAILED(service->Init()))) {
+ return nullptr;
+ }
+ sSingletonService = service;
+ }
+ ClearOnShutdown(&sSingletonService);
+ }
+
+ RefPtr<GeckoMediaPluginService> service = sSingletonService.get();
+ return service.forget();
+ }
+
+ NS_IMETHOD
+ Run() override {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mService = GetOrCreateOnMainThread();
+ return NS_OK;
+ }
+};
+
+already_AddRefed<GeckoMediaPluginService>
+GeckoMediaPluginService::GetGeckoMediaPluginService() {
+ return GMPServiceCreateHelper::GetOrCreate();
+}
+
+NS_IMPL_ISUPPORTS(GeckoMediaPluginService, mozIGeckoMediaPluginService,
+ nsIObserver)
+
+GeckoMediaPluginService::GeckoMediaPluginService()
+ : mMutex("GeckoMediaPluginService::mMutex"),
+ mMainThread(GetMainThreadSerialEventTarget()),
+ mGMPThreadShutdown(false),
+ mShuttingDownOnGMPThread(false),
+ mXPCOMWillShutdown(false) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIXULAppInfo> appInfo =
+ do_GetService("@mozilla.org/xre/app-info;1");
+ if (appInfo) {
+ nsAutoCString version;
+ nsAutoCString buildID;
+ if (NS_SUCCEEDED(appInfo->GetVersion(version)) &&
+ NS_SUCCEEDED(appInfo->GetAppBuildID(buildID))) {
+ GMP_LOG_DEBUG(
+ "GeckoMediaPluginService created; Gecko version=%s buildID=%s",
+ version.get(), buildID.get());
+ }
+ }
+}
+
+GeckoMediaPluginService::~GeckoMediaPluginService() = default;
+
+NS_IMETHODIMP
+GeckoMediaPluginService::RunPluginCrashCallbacks(
+ uint32_t aPluginId, const nsACString& aPluginName) {
+ MOZ_ASSERT(NS_IsMainThread());
+ GMP_LOG_DEBUG("%s::%s(%i)", __CLASS__, __FUNCTION__, aPluginId);
+
+ mozilla::UniquePtr<nsTArray<RefPtr<GMPCrashHelper>>> helpers;
+ {
+ MutexAutoLock lock(mMutex);
+ mPluginCrashHelpers.Remove(aPluginId, &helpers);
+ }
+ if (!helpers) {
+ GMP_LOG_DEBUG("%s::%s(%i) No crash helpers, not handling crash.", __CLASS__,
+ __FUNCTION__, aPluginId);
+ return NS_OK;
+ }
+
+ for (const auto& helper : *helpers) {
+ nsCOMPtr<nsPIDOMWindowInner> window = helper->GetPluginCrashedEventTarget();
+ if (NS_WARN_IF(!window)) {
+ continue;
+ }
+ RefPtr<dom::Document> document(window->GetExtantDoc());
+ if (NS_WARN_IF(!document)) {
+ continue;
+ }
+
+ dom::PluginCrashedEventInit init;
+ init.mPluginID = aPluginId;
+ init.mBubbles = true;
+ init.mCancelable = true;
+ init.mGmpPlugin = true;
+ CopyUTF8toUTF16(aPluginName, init.mPluginName);
+ init.mSubmittedCrashReport = false;
+ RefPtr<dom::PluginCrashedEvent> event =
+ dom::PluginCrashedEvent::Constructor(document, u"PluginCrashed"_ns,
+ init);
+ event->SetTrusted(true);
+ event->WidgetEventPtr()->mFlags.mOnlyChromeDispatch = true;
+
+ EventDispatcher::DispatchDOMEvent(window, nullptr, event, nullptr, nullptr);
+ }
+
+ return NS_OK;
+}
+
+nsresult GeckoMediaPluginService::Init() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIObserverService> obsService =
+ mozilla::services::GetObserverService();
+ MOZ_ASSERT(obsService);
+ MOZ_ALWAYS_SUCCEEDS(obsService->AddObserver(
+ this, NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID, false));
+
+ // Kick off scanning for plugins
+ nsCOMPtr<nsIThread> thread;
+ return GetThread(getter_AddRefs(thread));
+}
+
+RefPtr<GetCDMParentPromise> GeckoMediaPluginService::GetCDM(
+ const NodeIdParts& aNodeIdParts, const nsACString& aKeySystem,
+ GMPCrashHelper* aHelper) {
+ AssertOnGMPThread();
+
+ if (mShuttingDownOnGMPThread || aKeySystem.IsEmpty()) {
+ nsPrintfCString reason(
+ "%s::%s failed, aKeySystem.IsEmpty() = %d, mShuttingDownOnGMPThread = "
+ "%d.",
+ __CLASS__, __FUNCTION__, aKeySystem.IsEmpty(),
+ mShuttingDownOnGMPThread);
+ return GetCDMParentPromise::CreateAndReject(
+ MediaResult(NS_ERROR_FAILURE, reason.get()), __func__);
+ }
+
+ typedef MozPromiseHolder<GetCDMParentPromise> PromiseHolder;
+ PromiseHolder* rawHolder(new PromiseHolder());
+ RefPtr<GetCDMParentPromise> promise = rawHolder->Ensure(__func__);
+ nsCOMPtr<nsISerialEventTarget> thread(GetGMPThread());
+ RefPtr<GMPCrashHelper> helper(aHelper);
+ nsTArray<nsCString> tags{nsCString{aKeySystem}};
+ GetContentParent(aHelper, NodeIdVariant{aNodeIdParts},
+ nsLiteralCString(CHROMIUM_CDM_API), tags)
+ ->Then(
+ thread, __func__,
+ [rawHolder, helper, keySystem = nsCString{aKeySystem}](
+ RefPtr<GMPContentParent::CloseBlocker> wrapper) {
+ RefPtr<GMPContentParent> parent = wrapper->mParent;
+ MOZ_ASSERT(
+ parent,
+ "Wrapper should wrap a valid parent if we're in this path.");
+ UniquePtr<PromiseHolder> holder(rawHolder);
+ RefPtr<ChromiumCDMParent> cdm = parent->GetChromiumCDM(keySystem);
+ if (!cdm) {
+ nsPrintfCString reason(
+ "%s::%s failed since GetChromiumCDM returns nullptr.",
+ __CLASS__, __FUNCTION__);
+ holder->Reject(MediaResult(NS_ERROR_FAILURE, reason.get()),
+ __func__);
+ return;
+ }
+ if (helper) {
+ cdm->SetCrashHelper(helper);
+ }
+ holder->Resolve(cdm, __func__);
+ },
+ [rawHolder](MediaResult result) {
+ nsPrintfCString reason(
+ "%s::%s failed since GetContentParent rejects the promise with "
+ "reason %s.",
+ __CLASS__, __FUNCTION__, result.Description().get());
+ UniquePtr<PromiseHolder> holder(rawHolder);
+ holder->Reject(MediaResult(NS_ERROR_FAILURE, reason.get()),
+ __func__);
+ });
+
+ return promise;
+}
+
+#if defined(MOZ_SANDBOX) && defined(MOZ_DEBUG) && defined(ENABLE_TESTS)
+RefPtr<GetGMPContentParentPromise>
+GeckoMediaPluginService::GetContentParentForTest() {
+ AssertOnGMPThread();
+
+ nsTArray<nsCString> tags;
+ tags.AppendElement("fake"_ns);
+
+ const nsString origin1 = u"http://example1.com"_ns;
+ const nsString origin2 = u"http://example2.org"_ns;
+ const nsString gmpName = u"gmp-fake"_ns;
+
+ NodeIdParts nodeIdParts = NodeIdParts{origin1, origin2, gmpName};
+
+ if (mShuttingDownOnGMPThread) {
+ nsPrintfCString reason("%s::%s failed, mShuttingDownOnGMPThread = %d.",
+ __CLASS__, __FUNCTION__, mShuttingDownOnGMPThread);
+ return GetGMPContentParentPromise::CreateAndReject(
+ MediaResult(NS_ERROR_FAILURE, reason.get()), __func__);
+ }
+
+ using PromiseHolder = MozPromiseHolder<GetGMPContentParentPromise>;
+ PromiseHolder* rawHolder(new PromiseHolder());
+ RefPtr<GetGMPContentParentPromise> promise = rawHolder->Ensure(__func__);
+ nsCOMPtr<nsISerialEventTarget> thread(GetGMPThread());
+ GetContentParent(nullptr, NodeIdVariant{nodeIdParts},
+ nsLiteralCString(CHROMIUM_CDM_API), tags)
+ ->Then(
+ thread, __func__,
+ [rawHolder](const RefPtr<GMPContentParent::CloseBlocker>& wrapper) {
+ RefPtr<GMPContentParent> parent = wrapper->mParent;
+ MOZ_ASSERT(
+ parent,
+ "Wrapper should wrap a valid parent if we're in this path.");
+ UniquePtr<PromiseHolder> holder(rawHolder);
+ if (!parent) {
+ nsPrintfCString reason("%s::%s failed since no GMPContentParent.",
+ __CLASS__, __FUNCTION__);
+ holder->Reject(MediaResult(NS_ERROR_FAILURE, reason.get()),
+ __func__);
+ return;
+ }
+ holder->Resolve(wrapper, __func__);
+ },
+ [rawHolder](const MediaResult& result) {
+ nsPrintfCString reason(
+ "%s::%s failed since GetContentParent rejects the promise with "
+ "reason %s.",
+ __CLASS__, __FUNCTION__, result.Description().get());
+ UniquePtr<PromiseHolder> holder(rawHolder);
+ holder->Reject(MediaResult(NS_ERROR_FAILURE, reason.get()),
+ __func__);
+ });
+
+ return promise;
+}
+#endif
+
+void GeckoMediaPluginService::ShutdownGMPThread() {
+ GMP_LOG_DEBUG("%s::%s", __CLASS__, __FUNCTION__);
+ nsCOMPtr<nsIThread> gmpThread;
+ {
+ MutexAutoLock lock(mMutex);
+ mGMPThreadShutdown = true;
+ mGMPThread.swap(gmpThread);
+ }
+
+ if (gmpThread) {
+ gmpThread->Shutdown();
+ }
+}
+
+/* static */
+nsCOMPtr<nsIAsyncShutdownClient> GeckoMediaPluginService::GetShutdownBarrier() {
+ nsCOMPtr<nsIAsyncShutdownService> svc = services::GetAsyncShutdownService();
+ if (NS_WARN_IF(!svc)) {
+ MOZ_ASSERT_UNREACHABLE("No async shutdown service!");
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIAsyncShutdownClient> barrier;
+ nsresult rv = svc->GetXpcomWillShutdown(getter_AddRefs(barrier));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_ASSERT_UNREACHABLE("Could not create shutdown barrier!");
+ return nullptr;
+ }
+
+ MOZ_RELEASE_ASSERT(barrier);
+ return barrier;
+}
+
+nsresult GeckoMediaPluginService::GMPDispatch(nsIRunnable* event,
+ uint32_t flags) {
+ nsCOMPtr<nsIRunnable> r(event);
+ return GMPDispatch(r.forget(), flags);
+}
+
+nsresult GeckoMediaPluginService::GMPDispatch(
+ already_AddRefed<nsIRunnable> event, uint32_t flags) {
+ nsCOMPtr<nsIRunnable> r(event);
+ nsCOMPtr<nsIThread> thread;
+ nsresult rv = GetThread(getter_AddRefs(thread));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ return thread->Dispatch(r, flags);
+}
+
+// always call with getter_AddRefs, because it does
+NS_IMETHODIMP
+GeckoMediaPluginService::GetThread(nsIThread** aThread) {
+ MOZ_ASSERT(aThread);
+
+ // This can be called from any thread.
+ MutexAutoLock lock(mMutex);
+
+ return GetThreadLocked(aThread);
+}
+
+// always call with getter_AddRefs, because it does
+nsresult GeckoMediaPluginService::GetThreadLocked(nsIThread** aThread) {
+ MOZ_ASSERT(aThread);
+
+ mMutex.AssertCurrentThreadOwns();
+
+ if (!mGMPThread) {
+ // Don't allow the thread to be created after shutdown has started.
+ if (mGMPThreadShutdown) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv = NS_NewNamedThread("GMPThread", getter_AddRefs(mGMPThread));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // Tell the thread to initialize plugins
+ InitializePlugins(mGMPThread);
+ }
+
+ nsCOMPtr<nsIThread> copy = mGMPThread;
+ copy.forget(aThread);
+
+ return NS_OK;
+}
+
+already_AddRefed<nsISerialEventTarget> GeckoMediaPluginService::GetGMPThread() {
+ nsCOMPtr<nsISerialEventTarget> thread;
+ {
+ MutexAutoLock lock(mMutex);
+ thread = mGMPThread;
+ }
+ return thread.forget();
+}
+
+NS_IMETHODIMP
+GeckoMediaPluginService::GetGMPVideoDecoder(
+ GMPCrashHelper* aHelper, nsTArray<nsCString>* aTags,
+ const nsACString& aNodeId,
+ UniquePtr<GetGMPVideoDecoderCallback>&& aCallback) {
+ AssertOnGMPThread();
+ NS_ENSURE_ARG(aTags && aTags->Length() > 0);
+ NS_ENSURE_ARG(aCallback);
+
+ if (mShuttingDownOnGMPThread) {
+ return NS_ERROR_FAILURE;
+ }
+
+ GetGMPVideoDecoderCallback* rawCallback = aCallback.release();
+ nsCOMPtr<nsISerialEventTarget> thread(GetGMPThread());
+ RefPtr<GMPCrashHelper> helper(aHelper);
+ GetContentParent(aHelper, NodeIdVariant{nsCString(aNodeId)},
+ nsLiteralCString(GMP_API_VIDEO_DECODER), *aTags)
+ ->Then(
+ thread, __func__,
+ [rawCallback,
+ helper](RefPtr<GMPContentParent::CloseBlocker> wrapper) {
+ RefPtr<GMPContentParent> parent = wrapper->mParent;
+ UniquePtr<GetGMPVideoDecoderCallback> callback(rawCallback);
+ GMPVideoDecoderParent* actor = nullptr;
+ GMPVideoHostImpl* host = nullptr;
+ if (parent && NS_SUCCEEDED(parent->GetGMPVideoDecoder(&actor))) {
+ host = &(actor->Host());
+ actor->SetCrashHelper(helper);
+ }
+ callback->Done(actor, host);
+ },
+ [rawCallback] {
+ UniquePtr<GetGMPVideoDecoderCallback> callback(rawCallback);
+ callback->Done(nullptr, nullptr);
+ });
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GeckoMediaPluginService::GetGMPVideoEncoder(
+ GMPCrashHelper* aHelper, nsTArray<nsCString>* aTags,
+ const nsACString& aNodeId,
+ UniquePtr<GetGMPVideoEncoderCallback>&& aCallback) {
+ AssertOnGMPThread();
+ NS_ENSURE_ARG(aTags && aTags->Length() > 0);
+ NS_ENSURE_ARG(aCallback);
+
+ if (mShuttingDownOnGMPThread) {
+ return NS_ERROR_FAILURE;
+ }
+
+ GetGMPVideoEncoderCallback* rawCallback = aCallback.release();
+ nsCOMPtr<nsISerialEventTarget> thread(GetGMPThread());
+ RefPtr<GMPCrashHelper> helper(aHelper);
+ GetContentParent(aHelper, NodeIdVariant{nsCString(aNodeId)},
+ nsLiteralCString(GMP_API_VIDEO_ENCODER), *aTags)
+ ->Then(
+ thread, __func__,
+ [rawCallback,
+ helper](RefPtr<GMPContentParent::CloseBlocker> wrapper) {
+ RefPtr<GMPContentParent> parent = wrapper->mParent;
+ UniquePtr<GetGMPVideoEncoderCallback> callback(rawCallback);
+ GMPVideoEncoderParent* actor = nullptr;
+ GMPVideoHostImpl* host = nullptr;
+ if (parent && NS_SUCCEEDED(parent->GetGMPVideoEncoder(&actor))) {
+ host = &(actor->Host());
+ actor->SetCrashHelper(helper);
+ }
+ callback->Done(actor, host);
+ },
+ [rawCallback] {
+ UniquePtr<GetGMPVideoEncoderCallback> callback(rawCallback);
+ callback->Done(nullptr, nullptr);
+ });
+
+ return NS_OK;
+}
+
+void GeckoMediaPluginService::ConnectCrashHelper(uint32_t aPluginId,
+ GMPCrashHelper* aHelper) {
+ if (!aHelper) {
+ return;
+ }
+
+ MutexAutoLock lock(mMutex);
+ mPluginCrashHelpers.WithEntryHandle(aPluginId, [&](auto&& entry) {
+ if (!entry) {
+ entry.Insert(MakeUnique<nsTArray<RefPtr<GMPCrashHelper>>>());
+ } else if (entry.Data()->Contains(aHelper)) {
+ return;
+ }
+ entry.Data()->AppendElement(aHelper);
+ });
+}
+
+void GeckoMediaPluginService::DisconnectCrashHelper(GMPCrashHelper* aHelper) {
+ if (!aHelper) {
+ return;
+ }
+ MutexAutoLock lock(mMutex);
+ for (auto iter = mPluginCrashHelpers.Iter(); !iter.Done(); iter.Next()) {
+ nsTArray<RefPtr<GMPCrashHelper>>* helpers = iter.UserData();
+ if (!helpers->Contains(aHelper)) {
+ continue;
+ }
+ helpers->RemoveElement(aHelper);
+ MOZ_ASSERT(!helpers->Contains(aHelper)); // Ensure there aren't duplicates.
+ if (helpers->IsEmpty()) {
+ iter.Remove();
+ }
+ }
+}
+
+} // namespace gmp
+} // namespace mozilla
+
+#undef __CLASS__
diff --git a/dom/media/gmp/GMPService.h b/dom/media/gmp/GMPService.h
new file mode 100644
index 0000000000..8f4271812e
--- /dev/null
+++ b/dom/media/gmp/GMPService.h
@@ -0,0 +1,130 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GMPService_h_
+#define GMPService_h_
+
+#include "GMPContentParent.h"
+#include "GMPCrashHelper.h"
+#include "gmp-video-codec.h"
+#include "mozIGeckoMediaPluginService.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/gmp/GMPTypes.h"
+#include "mozilla/MozPromise.h"
+#include "nsCOMPtr.h"
+#include "nsClassHashtable.h"
+#include "nsIObserver.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+class nsIAsyncShutdownClient;
+class nsIRunnable;
+class nsISerialEventTarget;
+class nsIThread;
+
+template <class>
+struct already_AddRefed;
+
+namespace mozilla {
+
+class GMPCrashHelper;
+class MediaResult;
+
+extern LogModule* GetGMPLog();
+extern LogModule* GetGMPLibraryLog();
+extern GMPLogLevel GetGMPLibraryLogLevel();
+
+namespace gmp {
+
+typedef MozPromise<RefPtr<GMPContentParent::CloseBlocker>, MediaResult,
+ /* IsExclusive = */ true>
+ GetGMPContentParentPromise;
+typedef MozPromise<RefPtr<ChromiumCDMParent>, MediaResult,
+ /* IsExclusive = */ true>
+ GetCDMParentPromise;
+
+class GeckoMediaPluginService : public mozIGeckoMediaPluginService,
+ public nsIObserver {
+ public:
+ static already_AddRefed<GeckoMediaPluginService> GetGeckoMediaPluginService();
+
+ virtual nsresult Init();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ RefPtr<GetCDMParentPromise> GetCDM(const NodeIdParts& aNodeIdParts,
+ const nsACString& aKeySystem,
+ GMPCrashHelper* aHelper);
+
+#if defined(MOZ_SANDBOX) && defined(MOZ_DEBUG) && defined(ENABLE_TESTS)
+ RefPtr<GetGMPContentParentPromise> GetContentParentForTest();
+#endif
+
+ // mozIGeckoMediaPluginService
+ NS_IMETHOD GetThread(nsIThread** aThread) override;
+ nsresult GetThreadLocked(nsIThread** aThread);
+ NS_IMETHOD GetGMPVideoDecoder(
+ GMPCrashHelper* aHelper, nsTArray<nsCString>* aTags,
+ const nsACString& aNodeId,
+ UniquePtr<GetGMPVideoDecoderCallback>&& aCallback) override;
+ NS_IMETHOD GetGMPVideoEncoder(
+ GMPCrashHelper* aHelper, nsTArray<nsCString>* aTags,
+ const nsACString& aNodeId,
+ UniquePtr<GetGMPVideoEncoderCallback>&& aCallback) override;
+
+ // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230)
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD RunPluginCrashCallbacks(
+ uint32_t aPluginId, const nsACString& aPluginName) override;
+
+ already_AddRefed<nsISerialEventTarget> GetGMPThread();
+
+ void ConnectCrashHelper(uint32_t aPluginId, GMPCrashHelper* aHelper);
+ void DisconnectCrashHelper(GMPCrashHelper* aHelper);
+
+ bool XPCOMWillShutdownReceived() const { return mXPCOMWillShutdown; }
+
+ protected:
+ GeckoMediaPluginService();
+ virtual ~GeckoMediaPluginService();
+
+ void AssertOnGMPThread() {
+#ifdef DEBUG
+ MutexAutoLock lock(mMutex);
+ MOZ_ASSERT(mGMPThread->IsOnCurrentThread());
+#endif
+ }
+
+ virtual void InitializePlugins(nsISerialEventTarget* aGMPThread) = 0;
+
+ virtual RefPtr<GetGMPContentParentPromise> GetContentParent(
+ GMPCrashHelper* aHelper, const NodeIdVariant& aNodeIdVariant,
+ const nsACString& aAPI, const nsTArray<nsCString>& aTags) = 0;
+
+ nsresult GMPDispatch(nsIRunnable* event, uint32_t flags = NS_DISPATCH_NORMAL);
+ nsresult GMPDispatch(already_AddRefed<nsIRunnable> event,
+ uint32_t flags = NS_DISPATCH_NORMAL);
+ void ShutdownGMPThread();
+
+ static nsCOMPtr<nsIAsyncShutdownClient> GetShutdownBarrier();
+
+ Mutex mMutex MOZ_UNANNOTATED; // Protects mGMPThread, mPluginCrashHelpers,
+ // mGMPThreadShutdown and some members in
+ // derived classes.
+
+ const nsCOMPtr<nsISerialEventTarget> mMainThread;
+
+ nsCOMPtr<nsIThread> mGMPThread;
+ bool mGMPThreadShutdown;
+ bool mShuttingDownOnGMPThread;
+ Atomic<bool> mXPCOMWillShutdown;
+
+ nsClassHashtable<nsUint32HashKey, nsTArray<RefPtr<GMPCrashHelper>>>
+ mPluginCrashHelpers;
+};
+
+} // namespace gmp
+} // namespace mozilla
+
+#endif // GMPService_h_
diff --git a/dom/media/gmp/GMPServiceChild.cpp b/dom/media/gmp/GMPServiceChild.cpp
new file mode 100644
index 0000000000..ebcb9dc84c
--- /dev/null
+++ b/dom/media/gmp/GMPServiceChild.cpp
@@ -0,0 +1,586 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GMPServiceChild.h"
+
+#include "GMPContentParent.h"
+#include "GMPLog.h"
+#include "GMPParent.h"
+#include "base/task.h"
+#include "mozIGeckoMediaPluginChromeService.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/SyncRunnable.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/ipc/Endpoint.h"
+#include "nsCOMPtr.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIObserverService.h"
+#include "nsReadableUtils.h"
+#include "nsXPCOMPrivate.h"
+#include "runnable_utils.h"
+
+namespace mozilla::gmp {
+
+#ifdef __CLASS__
+# undef __CLASS__
+#endif
+#define __CLASS__ "GMPServiceChild"
+
+already_AddRefed<GeckoMediaPluginServiceChild>
+GeckoMediaPluginServiceChild::GetSingleton() {
+ MOZ_ASSERT(!XRE_IsParentProcess());
+ RefPtr<GeckoMediaPluginService> service(
+ GeckoMediaPluginService::GetGeckoMediaPluginService());
+#ifdef DEBUG
+ if (service) {
+ nsCOMPtr<mozIGeckoMediaPluginChromeService> chromeService;
+ CallQueryInterface(service.get(), getter_AddRefs(chromeService));
+ MOZ_ASSERT(!chromeService);
+ }
+#endif
+ return service.forget().downcast<GeckoMediaPluginServiceChild>();
+}
+
+nsresult GeckoMediaPluginServiceChild::Init() {
+ MOZ_ASSERT(NS_IsMainThread());
+ GMP_LOG_DEBUG("%s::%s", __CLASS__, __FUNCTION__);
+
+ nsresult rv = AddShutdownBlocker();
+ if (NS_FAILED(rv)) {
+ MOZ_ASSERT_UNREACHABLE(
+ "We expect xpcom to be live when calling this, so we should be able to "
+ "add a blocker");
+ GMP_LOG_DEBUG("%s::%s failed to add shutdown blocker!", __CLASS__,
+ __FUNCTION__);
+ return rv;
+ }
+
+ return GeckoMediaPluginService::Init();
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(GeckoMediaPluginServiceChild,
+ GeckoMediaPluginService, nsIAsyncShutdownBlocker)
+
+// Used to identify blockers that we put in place.
+static const nsLiteralString kShutdownBlockerName =
+ u"GeckoMediaPluginServiceChild: shutdown"_ns;
+
+// nsIAsyncShutdownBlocker members
+NS_IMETHODIMP
+GeckoMediaPluginServiceChild::GetName(nsAString& aName) {
+ aName = kShutdownBlockerName;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GeckoMediaPluginServiceChild::GetState(nsIPropertyBag**) { return NS_OK; }
+
+NS_IMETHODIMP
+GeckoMediaPluginServiceChild::BlockShutdown(nsIAsyncShutdownClient*) {
+ MOZ_ASSERT(NS_IsMainThread());
+ GMP_LOG_DEBUG("%s::%s", __CLASS__, __FUNCTION__);
+
+ mXPCOMWillShutdown = true;
+
+ MutexAutoLock lock(mMutex);
+ Unused << NS_WARN_IF(NS_FAILED(mGMPThread->Dispatch(
+ NewRunnableMethod("GeckoMediaPluginServiceChild::BeginShutdown", this,
+ &GeckoMediaPluginServiceChild::BeginShutdown))));
+ return NS_OK;
+}
+// End nsIAsyncShutdownBlocker members
+
+GeckoMediaPluginServiceChild::~GeckoMediaPluginServiceChild() {
+ MOZ_ASSERT(!mServiceChild);
+}
+
+RefPtr<GetGMPContentParentPromise>
+GeckoMediaPluginServiceChild::GetContentParent(
+ GMPCrashHelper* aHelper, const NodeIdVariant& aNodeIdVariant,
+ const nsACString& aAPI, const nsTArray<nsCString>& aTags) {
+ AssertOnGMPThread();
+ MOZ_ASSERT(!mShuttingDownOnGMPThread,
+ "Should not be called if GMPThread is shutting down!");
+
+ MozPromiseHolder<GetGMPContentParentPromise>* rawHolder =
+ new MozPromiseHolder<GetGMPContentParentPromise>();
+ RefPtr<GetGMPContentParentPromise> promise = rawHolder->Ensure(__func__);
+ nsCOMPtr<nsISerialEventTarget> thread(GetGMPThread());
+
+ nsCString api(aAPI);
+ RefPtr<GMPCrashHelper> helper(aHelper);
+ RefPtr<GeckoMediaPluginServiceChild> self(this);
+
+ mPendingGetContentParents += 1;
+
+ GetServiceChild()->Then(
+ thread, __func__,
+ [nodeIdVariant = aNodeIdVariant, self, api, tags = aTags.Clone(), helper,
+ rawHolder](GMPServiceChild* child) {
+ UniquePtr<MozPromiseHolder<GetGMPContentParentPromise>> holder(
+ rawHolder);
+ nsresult rv;
+
+ nsTArray<base::ProcessId> alreadyBridgedTo;
+ child->GetAlreadyBridgedTo(alreadyBridgedTo);
+
+ base::ProcessId otherProcess;
+ nsCString displayName;
+ uint32_t pluginId = 0;
+ GMPPluginType pluginType = GMPPluginType::Unknown;
+ ipc::Endpoint<PGMPContentParent> endpoint;
+ nsCString errorDescription;
+
+ bool ok = child->SendLaunchGMP(
+ nodeIdVariant, api, tags, alreadyBridgedTo, &pluginId, &pluginType,
+ &otherProcess, &displayName, &endpoint, &rv, &errorDescription);
+
+ if (helper && pluginId) {
+ // Note: Even if the launch failed, we need to connect the crash
+ // helper so that if the launch failed due to the plugin crashing, we
+ // can report the crash via the crash reporter. The crash handling
+ // notification will arrive shortly if the launch failed due to the
+ // plugin crashing.
+ self->ConnectCrashHelper(pluginId, helper);
+ }
+
+ if (!ok || NS_FAILED(rv)) {
+ MediaResult error(
+ rv, nsPrintfCString(
+ "GeckoMediaPluginServiceChild::GetContentParent "
+ "SendLaunchGMPForNodeId failed with description (%s)",
+ errorDescription.get()));
+
+ GMP_LOG_DEBUG("%s failed to launch GMP with error: %s", __CLASS__,
+ error.Description().get());
+ self->mPendingGetContentParents -= 1;
+ self->RemoveShutdownBlockerIfNeeded();
+
+ holder->Reject(error, __func__);
+ return;
+ }
+
+ RefPtr<GMPContentParent> parent = child->GetBridgedGMPContentParent(
+ otherProcess, std::move(endpoint));
+ if (!alreadyBridgedTo.Contains(otherProcess)) {
+ parent->SetDisplayName(displayName);
+ parent->SetPluginId(pluginId);
+ parent->SetPluginType(pluginType);
+ }
+
+ // The content parent is no longer pending.
+ self->mPendingGetContentParents -= 1;
+ MOZ_ASSERT(child->HaveContentParents(),
+ "We should have at least one content parent!");
+ // We don't check if we need to remove the shutdown blocker here as
+ // we should always have at least one live content parent.
+
+ RefPtr<GMPContentParent::CloseBlocker> blocker(
+ new GMPContentParent::CloseBlocker(parent));
+ holder->Resolve(blocker, __func__);
+ },
+ [self, rawHolder](MediaResult result) {
+ self->mPendingGetContentParents -= 1;
+ self->RemoveShutdownBlockerIfNeeded();
+ UniquePtr<MozPromiseHolder<GetGMPContentParentPromise>> holder(
+ rawHolder);
+ holder->Reject(result, __func__);
+ });
+
+ return promise;
+}
+
+typedef mozilla::dom::GMPCapabilityData GMPCapabilityData;
+typedef mozilla::dom::GMPAPITags GMPAPITags;
+
+struct GMPCapabilityAndVersion {
+ explicit GMPCapabilityAndVersion(const GMPCapabilityData& aCapabilities)
+ : mName(aCapabilities.name()), mVersion(aCapabilities.version()) {
+ for (const GMPAPITags& tags : aCapabilities.capabilities()) {
+ GMPCapability cap;
+ cap.mAPIName = tags.api();
+ for (const nsACString& tag : tags.tags()) {
+ cap.mAPITags.AppendElement(tag);
+ }
+ mCapabilities.AppendElement(std::move(cap));
+ }
+ }
+
+ nsCString ToString() const {
+ nsCString s;
+ s.Append(mName);
+ s.AppendLiteral(" version=");
+ s.Append(mVersion);
+ s.AppendLiteral(" tags=[");
+ StringJoinAppend(s, " "_ns, mCapabilities,
+ [](auto& tags, const GMPCapability& cap) {
+ tags.Append(cap.mAPIName);
+ for (const nsACString& tag : cap.mAPITags) {
+ tags.AppendLiteral(":");
+ tags.Append(tag);
+ }
+ });
+ s.AppendLiteral("]");
+ return s;
+ }
+
+ nsCString mName;
+ nsCString mVersion;
+ nsTArray<GMPCapability> mCapabilities;
+};
+
+StaticMutex sGMPCapabilitiesMutex;
+StaticAutoPtr<nsTArray<GMPCapabilityAndVersion>> sGMPCapabilities;
+
+static auto GMPCapabilitiesToString() {
+ return StringJoin(", "_ns, *sGMPCapabilities,
+ [](nsACString& dest, const GMPCapabilityAndVersion& gmp) {
+ dest.Append(gmp.ToString());
+ });
+}
+
+/* static */
+void GeckoMediaPluginServiceChild::UpdateGMPCapabilities(
+ nsTArray<GMPCapabilityData>&& aCapabilities) {
+ {
+ // The mutex should unlock before sending the "gmp-changed" observer
+ // service notification.
+ StaticMutexAutoLock lock(sGMPCapabilitiesMutex);
+ if (!sGMPCapabilities) {
+ sGMPCapabilities = new nsTArray<GMPCapabilityAndVersion>();
+ ClearOnShutdown(&sGMPCapabilities);
+ }
+ sGMPCapabilities->Clear();
+ for (const GMPCapabilityData& plugin : aCapabilities) {
+ sGMPCapabilities->AppendElement(GMPCapabilityAndVersion(plugin));
+ }
+
+ GMP_LOG_DEBUG("%s::%s {%s}", __CLASS__, __FUNCTION__,
+ GMPCapabilitiesToString().get());
+ }
+
+ // Fire a notification so that any MediaKeySystemAccess
+ // requests waiting on a CDM to download will retry.
+ nsCOMPtr<nsIObserverService> obsService =
+ mozilla::services::GetObserverService();
+ MOZ_ASSERT(obsService);
+ if (obsService) {
+ obsService->NotifyObservers(nullptr, "gmp-changed", nullptr);
+ }
+}
+
+void GeckoMediaPluginServiceChild::BeginShutdown() {
+ AssertOnGMPThread();
+ GMP_LOG_DEBUG("%s::%s: mServiceChild=%p,", __CLASS__, __FUNCTION__,
+ mServiceChild.get());
+ // It's possible this gets called twice if the parent sends us a message to
+ // shutdown and we block shutdown in content in close proximity.
+ mShuttingDownOnGMPThread = true;
+ RemoveShutdownBlockerIfNeeded();
+}
+
+NS_IMETHODIMP
+GeckoMediaPluginServiceChild::HasPluginForAPI(const nsACString& aAPI,
+ const nsTArray<nsCString>& aTags,
+ bool* aHasPlugin) {
+ StaticMutexAutoLock lock(sGMPCapabilitiesMutex);
+ if (!sGMPCapabilities) {
+ *aHasPlugin = false;
+ return NS_OK;
+ }
+
+ nsCString api(aAPI);
+ for (const GMPCapabilityAndVersion& plugin : *sGMPCapabilities) {
+ if (GMPCapability::Supports(plugin.mCapabilities, api, aTags)) {
+ *aHasPlugin = true;
+ return NS_OK;
+ }
+ }
+
+ *aHasPlugin = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GeckoMediaPluginServiceChild::GetNodeId(
+ const nsAString& aOrigin, const nsAString& aTopLevelOrigin,
+ const nsAString& aGMPName, UniquePtr<GetNodeIdCallback>&& aCallback) {
+ AssertOnGMPThread();
+
+ GetNodeIdCallback* rawCallback = aCallback.release();
+ nsCOMPtr<nsISerialEventTarget> thread(GetGMPThread());
+ nsString origin(aOrigin);
+ nsString topLevelOrigin(aTopLevelOrigin);
+ nsString gmpName(aGMPName);
+ GetServiceChild()->Then(
+ thread, __func__,
+ [rawCallback, origin, topLevelOrigin, gmpName](GMPServiceChild* child) {
+ UniquePtr<GetNodeIdCallback> callback(rawCallback);
+ nsCString outId;
+ if (!child->SendGetGMPNodeId(origin, topLevelOrigin, gmpName, &outId)) {
+ callback->Done(NS_ERROR_FAILURE, ""_ns);
+ return;
+ }
+
+ callback->Done(NS_OK, outId);
+ },
+ [rawCallback](nsresult rv) {
+ UniquePtr<GetNodeIdCallback> callback(rawCallback);
+ callback->Done(NS_ERROR_FAILURE, ""_ns);
+ });
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GeckoMediaPluginServiceChild::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aSomeData) {
+ MOZ_ASSERT(NS_IsMainThread());
+ GMP_LOG_DEBUG("%s::%s: aTopic=%s", __CLASS__, __FUNCTION__, aTopic);
+ if (!strcmp(NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID, aTopic)) {
+ if (mServiceChild) {
+ MutexAutoLock lock(mMutex);
+ mozilla::SyncRunnable::DispatchToThread(
+ mGMPThread,
+ WrapRunnable(mServiceChild.get(), &PGMPServiceChild::Close));
+ mServiceChild = nullptr;
+ }
+ ShutdownGMPThread();
+ }
+
+ return NS_OK;
+}
+
+RefPtr<GeckoMediaPluginServiceChild::GetServiceChildPromise>
+GeckoMediaPluginServiceChild::GetServiceChild() {
+ AssertOnGMPThread();
+
+ if (!mServiceChild) {
+ if (mShuttingDownOnGMPThread) {
+ // We have begun shutdown. Don't allow a new connection to the main
+ // process to be instantiated. This also prevents new plugins being
+ // instantiated.
+ return GetServiceChildPromise::CreateAndReject(NS_ERROR_FAILURE,
+ __func__);
+ }
+ dom::ContentChild* contentChild = dom::ContentChild::GetSingleton();
+ if (!contentChild) {
+ return GetServiceChildPromise::CreateAndReject(NS_ERROR_FAILURE,
+ __func__);
+ }
+ MozPromiseHolder<GetServiceChildPromise>* holder =
+ mGetServiceChildPromises.AppendElement();
+ RefPtr<GetServiceChildPromise> promise = holder->Ensure(__func__);
+ if (mGetServiceChildPromises.Length() == 1) {
+ nsCOMPtr<nsIRunnable> r =
+ WrapRunnable(contentChild, &dom::ContentChild::SendCreateGMPService);
+ SchedulerGroup::Dispatch(TaskCategory::Other, r.forget());
+ }
+ return promise;
+ }
+ return GetServiceChildPromise::CreateAndResolve(mServiceChild.get(),
+ __func__);
+}
+
+void GeckoMediaPluginServiceChild::SetServiceChild(
+ RefPtr<GMPServiceChild>&& aServiceChild) {
+ AssertOnGMPThread();
+ MOZ_ASSERT(!mServiceChild, "Should not already have service child!");
+ GMP_LOG_DEBUG("%s::%s: aServiceChild=%p", __CLASS__, __FUNCTION__,
+ aServiceChild.get());
+
+ mServiceChild = std::move(aServiceChild);
+
+ nsTArray<MozPromiseHolder<GetServiceChildPromise>> holders =
+ std::move(mGetServiceChildPromises);
+ for (MozPromiseHolder<GetServiceChildPromise>& holder : holders) {
+ holder.Resolve(mServiceChild.get(), __func__);
+ }
+}
+
+void GeckoMediaPluginServiceChild::RemoveGMPContentParent(
+ GMPContentParent* aGMPContentParent) {
+ AssertOnGMPThread();
+ GMP_LOG_DEBUG(
+ "%s::%s: aGMPContentParent=%p, mServiceChild=%p, "
+ "mShuttingDownOnGMPThread=%s",
+ __CLASS__, __FUNCTION__, aGMPContentParent, mServiceChild.get(),
+ mShuttingDownOnGMPThread ? "true" : "false");
+
+ if (mServiceChild) {
+ mServiceChild->RemoveGMPContentParent(aGMPContentParent);
+ GMP_LOG_DEBUG(
+ "%s::%s: aGMPContentParent removed, "
+ "mServiceChild->HaveContentParents()=%s",
+ __CLASS__, __FUNCTION__,
+ mServiceChild->HaveContentParents() ? "true" : "false");
+ RemoveShutdownBlockerIfNeeded();
+ }
+}
+
+nsresult GeckoMediaPluginServiceChild::AddShutdownBlocker() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!mShuttingDownOnGMPThread,
+ "No call paths should add blockers once we're shutting down!");
+ MOZ_ASSERT(!mShutdownBlockerAdded, "Should only add blocker once!");
+ GMP_LOG_DEBUG("%s::%s ", __CLASS__, __FUNCTION__);
+
+ nsCOMPtr<nsIAsyncShutdownClient> barrier = GetShutdownBarrier();
+ if (NS_WARN_IF(!barrier)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nsresult rv =
+ barrier->AddBlocker(this, NS_LITERAL_STRING_FROM_CSTRING(__FILE__),
+ __LINE__, kShutdownBlockerName);
+#ifdef DEBUG
+ mShutdownBlockerAdded = NS_SUCCEEDED(rv);
+#endif
+ return rv;
+}
+
+void GeckoMediaPluginServiceChild::RemoveShutdownBlocker() {
+ AssertOnGMPThread();
+ MOZ_ASSERT(mShuttingDownOnGMPThread,
+ "We should only remove blockers once we're "
+ "shutting down!");
+ GMP_LOG_DEBUG("%s::%s ", __CLASS__, __FUNCTION__);
+ nsresult rv = mMainThread->Dispatch(NS_NewRunnableFunction(
+ "GeckoMediaPluginServiceChild::"
+ "RemoveShutdownBlocker",
+ [this, self = RefPtr<GeckoMediaPluginServiceChild>(this)]() {
+ nsCOMPtr<nsIAsyncShutdownClient> barrier = GetShutdownBarrier();
+ if (NS_WARN_IF(!barrier)) {
+ return;
+ }
+
+ nsresult rv = barrier->RemoveBlocker(this);
+ MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
+ }));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_ASSERT_UNREACHABLE(
+ "Main thread should always be alive when we "
+ "call this!");
+ }
+}
+
+void GeckoMediaPluginServiceChild::RemoveShutdownBlockerIfNeeded() {
+ AssertOnGMPThread();
+ GMP_LOG_DEBUG(
+ "%s::%s mPendingGetContentParents=%" PRIu32
+ " mServiceChild->HaveContentParents()=%s "
+ "mShuttingDownOnGMPThread=%s",
+ __CLASS__, __FUNCTION__, mPendingGetContentParents,
+ mServiceChild && mServiceChild->HaveContentParents() ? "true" : "false",
+ mShuttingDownOnGMPThread ? "true" : "false");
+
+ bool haveOneOrMoreContentParents =
+ mPendingGetContentParents > 0 ||
+ (mServiceChild && mServiceChild->HaveContentParents());
+
+ if (!mShuttingDownOnGMPThread || haveOneOrMoreContentParents) {
+ return;
+ }
+ RemoveShutdownBlocker();
+}
+
+already_AddRefed<GMPContentParent> GMPServiceChild::GetBridgedGMPContentParent(
+ ProcessId aOtherPid, ipc::Endpoint<PGMPContentParent>&& endpoint) {
+ return do_AddRef(mContentParents.LookupOrInsertWith(aOtherPid, [&] {
+ MOZ_ASSERT(aOtherPid == endpoint.OtherPid());
+
+ auto parent = MakeRefPtr<GMPContentParent>();
+
+ DebugOnly<bool> ok = endpoint.Bind(parent);
+ MOZ_ASSERT(ok);
+
+ return parent;
+ }));
+}
+
+void GMPServiceChild::RemoveGMPContentParent(
+ GMPContentParent* aGMPContentParent) {
+ for (auto iter = mContentParents.Iter(); !iter.Done(); iter.Next()) {
+ RefPtr<GMPContentParent>& parent = iter.Data();
+ if (parent == aGMPContentParent) {
+ iter.Remove();
+ break;
+ }
+ }
+}
+
+void GMPServiceChild::GetAlreadyBridgedTo(
+ nsTArray<base::ProcessId>& aAlreadyBridgedTo) {
+ AppendToArray(aAlreadyBridgedTo, mContentParents.Keys());
+}
+
+class OpenPGMPServiceChild : public mozilla::Runnable {
+ public:
+ OpenPGMPServiceChild(RefPtr<GMPServiceChild>&& aGMPServiceChild,
+ ipc::Endpoint<PGMPServiceChild>&& aEndpoint)
+ : Runnable("gmp::OpenPGMPServiceChild"),
+ mGMPServiceChild(std::move(aGMPServiceChild)),
+ mEndpoint(std::move(aEndpoint)) {}
+
+ NS_IMETHOD Run() override {
+ RefPtr<GeckoMediaPluginServiceChild> gmp =
+ GeckoMediaPluginServiceChild::GetSingleton();
+ MOZ_RELEASE_ASSERT(gmp);
+ MOZ_ASSERT(!gmp->mServiceChild);
+ if (mEndpoint.Bind(mGMPServiceChild.get())) {
+ gmp->SetServiceChild(std::move(mGMPServiceChild));
+ } else {
+ gmp->SetServiceChild(nullptr);
+ }
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<GMPServiceChild> mGMPServiceChild;
+ ipc::Endpoint<PGMPServiceChild> mEndpoint;
+};
+
+/* static */
+bool GMPServiceChild::Create(Endpoint<PGMPServiceChild>&& aGMPService) {
+ RefPtr<GeckoMediaPluginServiceChild> gmp =
+ GeckoMediaPluginServiceChild::GetSingleton();
+ if (NS_WARN_IF(!gmp)) {
+ return false;
+ }
+
+ MOZ_ASSERT(!gmp->mServiceChild);
+
+ RefPtr<GMPServiceChild> serviceChild(new GMPServiceChild());
+
+ nsCOMPtr<nsIThread> gmpThread;
+ nsresult rv = gmp->GetThread(getter_AddRefs(gmpThread));
+ NS_ENSURE_SUCCESS(rv, false);
+
+ rv = gmpThread->Dispatch(
+ new OpenPGMPServiceChild(std::move(serviceChild), std::move(aGMPService)),
+ NS_DISPATCH_NORMAL);
+ return NS_SUCCEEDED(rv);
+}
+
+ipc::IPCResult GMPServiceChild::RecvBeginShutdown() {
+ RefPtr<GeckoMediaPluginServiceChild> service =
+ GeckoMediaPluginServiceChild::GetSingleton();
+ MOZ_ASSERT(service && service->mServiceChild.get() == this);
+ if (service) {
+ service->BeginShutdown();
+ }
+ return IPC_OK();
+}
+
+bool GMPServiceChild::HaveContentParents() const {
+ return mContentParents.Count() > 0;
+}
+
+} // namespace mozilla::gmp
+
+#undef __CLASS__
diff --git a/dom/media/gmp/GMPServiceChild.h b/dom/media/gmp/GMPServiceChild.h
new file mode 100644
index 0000000000..2775a83489
--- /dev/null
+++ b/dom/media/gmp/GMPServiceChild.h
@@ -0,0 +1,164 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GMPServiceChild_h_
+#define GMPServiceChild_h_
+
+#include "GMPService.h"
+#include "MediaResult.h"
+#include "base/process.h"
+#include "mozilla/dom/PContent.h"
+#include "mozilla/gmp/PGMPServiceChild.h"
+#include "mozilla/MozPromise.h"
+#include "nsIAsyncShutdown.h"
+#include "nsRefPtrHashtable.h"
+
+namespace mozilla::gmp {
+
+class GMPContentParent;
+class GMPServiceChild;
+
+class GeckoMediaPluginServiceChild : public GeckoMediaPluginService,
+ public nsIAsyncShutdownBlocker {
+ friend class GMPServiceChild;
+
+ public:
+ static already_AddRefed<GeckoMediaPluginServiceChild> GetSingleton();
+ nsresult Init() override;
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIASYNCSHUTDOWNBLOCKER
+
+ NS_IMETHOD HasPluginForAPI(const nsACString& aAPI,
+ const nsTArray<nsCString>& aTags,
+ bool* aRetVal) override;
+ NS_IMETHOD GetNodeId(const nsAString& aOrigin,
+ const nsAString& aTopLevelOrigin,
+ const nsAString& aGMPName,
+ UniquePtr<GetNodeIdCallback>&& aCallback) override;
+
+ NS_DECL_NSIOBSERVER
+
+ void SetServiceChild(RefPtr<GMPServiceChild>&& aServiceChild);
+
+ void RemoveGMPContentParent(GMPContentParent* aGMPContentParent);
+
+ static void UpdateGMPCapabilities(
+ nsTArray<mozilla::dom::GMPCapabilityData>&& aCapabilities);
+
+ void BeginShutdown();
+
+ protected:
+ void InitializePlugins(nsISerialEventTarget*) override {
+ // Nothing to do here.
+ }
+
+ RefPtr<GetGMPContentParentPromise> GetContentParent(
+ GMPCrashHelper* aHelper, const NodeIdVariant& aNodeIdVariant,
+ const nsACString& aAPI, const nsTArray<nsCString>& aTags) override;
+
+ private:
+ friend class OpenPGMPServiceChild;
+
+ ~GeckoMediaPluginServiceChild() override;
+
+ typedef MozPromise<GMPServiceChild*, MediaResult, /* IsExclusive = */ true>
+ GetServiceChildPromise;
+ RefPtr<GetServiceChildPromise> GetServiceChild();
+
+ nsTArray<MozPromiseHolder<GetServiceChildPromise>> mGetServiceChildPromises;
+ RefPtr<GMPServiceChild> mServiceChild;
+
+ // Shutdown blocker management. A shutdown blocker is used to ensure that
+ // we do not shutdown until all content parents are removed from
+ // GMPServiceChild::mContentParents.
+ //
+ // The following rules let us shutdown block (and thus we shouldn't
+ // violate them):
+ // - We will only call GetContentParent if mShuttingDownOnGMPThread is false.
+ // - mShuttingDownOnGMPThread will become true once profile teardown is
+ // observed in the parent process (and once GMPServiceChild receives a
+ // message from GMPServiceParent) or if we block shutdown (which implies
+ // we're in shutdown).
+ // - If we currently have mPendingGetContentParents > 0 or parents in
+ // GMPServiceChild::mContentParents we should block shutdown so such
+ // parents can be cleanly shutdown.
+ // therefore
+ // - We can block shutdown at xpcom-will-shutdown until our content parents
+ // are handled.
+ // - Because once mShuttingDownOnGMPThread is true we cannot add new content
+ // parents, we know that when mShuttingDownOnGMPThread && all content
+ // parents are handled we'll never add more and it's safe to stop blocking
+ // shutdown.
+ // this relies on
+ // - Once we're shutting down, we need to ensure all content parents are
+ // shutdown and removed. Failure to do so will result in a shutdown stall.
+
+ // Note that at the time of writing there are significant differences in how
+ // xpcom shutdown is handled in release and non-release builds. For example,
+ // in release builds content processes are exited early, so xpcom shutdown
+ // is not observed (and not blocked by blockers). This is important to keep
+ // in mind when testing the shutdown blocking machinery (you won't see most
+ // of it be invoked in release).
+
+ // All of these members should only be used on the GMP thread unless
+ // otherwise noted!
+
+ // Add a shutdown blocker. Main thread only. Should only be called once when
+ // we init the service.
+ nsresult AddShutdownBlocker();
+ // Remove a shutdown blocker. Should be called once at most and only when
+ // mShuttingDownOnGMPThread. Prefer RemoveShutdownBlockerIfNeeded unless
+ // absolutely certain you need to call this directly.
+ void RemoveShutdownBlocker();
+ // Remove shutdown blocker if the following conditions are met:
+ // - mShuttingDownOnGMPThread.
+ // - !mServiceChild->HaveContentParents.
+ // - mPendingGetContentParents == 0.
+ // - mShutdownBlockerHasBeenAdded.
+ void RemoveShutdownBlockerIfNeeded();
+
+#ifdef DEBUG
+ // Track if we've added a shutdown blocker for sanity checking. Main thread
+ // only.
+ bool mShutdownBlockerAdded = false;
+#endif // DEBUG
+ // The number of GetContentParent calls that have not yet been resolved or
+ // rejected. We use this value to help determine if we need to block
+ // shutdown. Should only be used on GMP thread to avoid races.
+ uint32_t mPendingGetContentParents = 0;
+ // End shutdown blocker management.
+};
+
+class GMPServiceChild : public PGMPServiceChild {
+ public:
+ // Mark AddRef and Release as `final`, as they overload pure virtual
+ // implementations in PGMPServiceChild.
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GMPServiceChild, final)
+
+ explicit GMPServiceChild() = default;
+
+ already_AddRefed<GMPContentParent> GetBridgedGMPContentParent(
+ ProcessId aOtherPid, ipc::Endpoint<PGMPContentParent>&& endpoint);
+
+ void RemoveGMPContentParent(GMPContentParent* aGMPContentParent);
+
+ void GetAlreadyBridgedTo(nsTArray<ProcessId>& aAlreadyBridgedTo);
+
+ static bool Create(Endpoint<PGMPServiceChild>&& aGMPService);
+
+ ipc::IPCResult RecvBeginShutdown() override;
+
+ bool HaveContentParents() const;
+
+ private:
+ ~GMPServiceChild() = default;
+
+ nsRefPtrHashtable<nsUint64HashKey, GMPContentParent> mContentParents;
+};
+
+} // namespace mozilla::gmp
+
+#endif // GMPServiceChild_h_
diff --git a/dom/media/gmp/GMPServiceParent.cpp b/dom/media/gmp/GMPServiceParent.cpp
new file mode 100644
index 0000000000..d5d72a3655
--- /dev/null
+++ b/dom/media/gmp/GMPServiceParent.cpp
@@ -0,0 +1,1943 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GMPServiceParent.h"
+
+#include <limits>
+
+#include "GMPDecoderModule.h"
+#include "GMPLog.h"
+#include "GMPParent.h"
+#include "GMPVideoDecoderParent.h"
+#include "mozilla/ipc/GeckoChildProcessHost.h"
+#include "base/task.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/ipc/Endpoint.h"
+#include "nsThreadUtils.h"
+#if defined(XP_LINUX) && defined(MOZ_SANDBOX)
+# include "mozilla/SandboxInfo.h"
+#endif
+#include "VideoUtils.h"
+#include "mozilla/AppShutdown.h"
+#include "mozilla/Services.h"
+#include "mozilla/SpinEventLoopUntil.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/SyncRunnable.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/Unused.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsComponentManagerUtils.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsNetUtil.h"
+#include "nsHashKeys.h"
+#include "nsIFile.h"
+#include "nsIObserverService.h"
+#include "nsIXULRuntime.h"
+#include "nsNativeCharsetUtils.h"
+#include "nsXPCOMPrivate.h"
+#include "prio.h"
+#include "runnable_utils.h"
+
+#ifdef DEBUG
+# include "mozilla/dom/MediaKeys.h" // MediaKeys::kMediaKeysRequestTopic
+#endif
+
+namespace mozilla::gmp {
+
+#ifdef __CLASS__
+# undef __CLASS__
+#endif
+#define __CLASS__ "GMPServiceParent"
+
+static const uint32_t NodeIdSaltLength = 32;
+
+already_AddRefed<GeckoMediaPluginServiceParent>
+GeckoMediaPluginServiceParent::GetSingleton() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ RefPtr<GeckoMediaPluginService> service(
+ GeckoMediaPluginServiceParent::GetGeckoMediaPluginService());
+#ifdef DEBUG
+ if (service) {
+ nsCOMPtr<mozIGeckoMediaPluginChromeService> chromeService;
+ CallQueryInterface(service.get(), getter_AddRefs(chromeService));
+ MOZ_ASSERT(chromeService);
+ }
+#endif
+ return service.forget().downcast<GeckoMediaPluginServiceParent>();
+}
+
+NS_IMPL_ISUPPORTS_INHERITED(GeckoMediaPluginServiceParent,
+ GeckoMediaPluginService,
+ mozIGeckoMediaPluginChromeService,
+ nsIAsyncShutdownBlocker)
+
+GeckoMediaPluginServiceParent::GeckoMediaPluginServiceParent()
+ : mScannedPluginOnDisk(false),
+ mShuttingDown(false),
+ mWaitingForPluginsSyncShutdown(false),
+ mInitPromiseMonitor("GeckoMediaPluginServiceParent::mInitPromiseMonitor"),
+ mInitPromise(&mInitPromiseMonitor),
+ mLoadPluginsFromDiskComplete(false) {
+ MOZ_ASSERT(NS_IsMainThread());
+}
+
+GeckoMediaPluginServiceParent::~GeckoMediaPluginServiceParent() {
+ MOZ_ASSERT(mPlugins.IsEmpty());
+}
+
+nsresult GeckoMediaPluginServiceParent::Init() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (AppShutdown::GetCurrentShutdownPhase() != ShutdownPhase::NotInShutdown) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIObserverService> obsService =
+ mozilla::services::GetObserverService();
+ MOZ_ASSERT(obsService);
+
+ MOZ_ALWAYS_SUCCEEDS(
+ obsService->AddObserver(this, "profile-change-teardown", false));
+ MOZ_ALWAYS_SUCCEEDS(
+ obsService->AddObserver(this, "last-pb-context-exited", false));
+ MOZ_ALWAYS_SUCCEEDS(
+ obsService->AddObserver(this, "browser:purge-session-history", false));
+ MOZ_ALWAYS_SUCCEEDS(
+ obsService->AddObserver(this, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID, false));
+
+#ifdef DEBUG
+ MOZ_ALWAYS_SUCCEEDS(obsService->AddObserver(
+ this, dom::MediaKeys::kMediaKeysRequestTopic, false));
+#endif
+
+ nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+ if (prefs) {
+ prefs->AddObserver("media.gmp.plugin.crash", this, false);
+ }
+
+ nsresult rv = InitStorage();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // Kick off scanning for plugins
+ nsCOMPtr<nsIThread> thread;
+ rv = GetThread(getter_AddRefs(thread));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // Detect if GMP storage has an incompatible version, and if so nuke it.
+ int32_t version =
+ Preferences::GetInt("media.gmp.storage.version.observed", 0);
+ int32_t expected =
+ Preferences::GetInt("media.gmp.storage.version.expected", 0);
+ if (version != expected) {
+ Preferences::SetInt("media.gmp.storage.version.observed", expected);
+ return GMPDispatch(
+ NewRunnableMethod("gmp::GeckoMediaPluginServiceParent::ClearStorage",
+ this, &GeckoMediaPluginServiceParent::ClearStorage));
+ }
+ return NS_OK;
+}
+
+already_AddRefed<nsIFile> CloneAndAppend(nsIFile* aFile,
+ const nsAString& aDir) {
+ nsCOMPtr<nsIFile> f;
+ nsresult rv = aFile->Clone(getter_AddRefs(f));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+
+ rv = f->Append(aDir);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+ return f.forget();
+}
+
+static nsresult GMPPlatformString(nsAString& aOutPlatform) {
+ // Append the OS and arch so that we don't reuse the storage if the profile is
+ // copied or used under a different bit-ness, or copied to another platform.
+ nsCOMPtr<nsIXULRuntime> runtime = do_GetService("@mozilla.org/xre/runtime;1");
+ if (!runtime) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsAutoCString OS;
+ nsresult rv = runtime->GetOS(OS);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsAutoCString arch;
+ rv = runtime->GetXPCOMABI(arch);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsCString platform;
+ platform.Append(OS);
+ platform.AppendLiteral("_");
+ platform.Append(arch);
+
+ CopyUTF8toUTF16(platform, aOutPlatform);
+
+ return NS_OK;
+}
+
+nsresult GeckoMediaPluginServiceParent::InitStorage() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // GMP storage should be used in the chrome process only.
+ if (!XRE_IsParentProcess()) {
+ return NS_OK;
+ }
+
+ // Directory service is main thread only, so cache the profile dir here
+ // so that we can use it off main thread.
+ nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(mStorageBaseDir));
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = mStorageBaseDir->AppendNative("gmp"_ns);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsAutoString platform;
+ rv = GMPPlatformString(platform);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = mStorageBaseDir->Append(platform);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ return GeckoMediaPluginService::Init();
+}
+
+NS_IMETHODIMP
+GeckoMediaPluginServiceParent::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* aSomeData) {
+ GMP_LOG_DEBUG("%s::%s topic='%s' data='%s'", __CLASS__, __FUNCTION__, aTopic,
+ NS_ConvertUTF16toUTF8(aSomeData).get());
+ if (!strcmp(aTopic, NS_PREFBRANCH_PREFCHANGE_TOPIC_ID)) {
+ nsCOMPtr<nsIPrefBranch> branch(do_QueryInterface(aSubject));
+ if (branch) {
+ bool crashNow = false;
+ if (u"media.gmp.plugin.crash"_ns.Equals(aSomeData)) {
+ branch->GetBoolPref("media.gmp.plugin.crash", &crashNow);
+ }
+ if (crashNow) {
+ nsCOMPtr<nsIThread> gmpThread;
+ {
+ MutexAutoLock lock(mMutex);
+ gmpThread = mGMPThread;
+ }
+ if (gmpThread) {
+ // Note: the GeckoMediaPluginServiceParent singleton is kept alive by
+ // a static refptr that is only cleared in the final stage of shutdown
+ // after everything else is shutdown, so this RefPtr<> is not strictly
+ // necessary so long as that is true, but it's safer.
+ gmpThread->Dispatch(
+ WrapRunnable(RefPtr<GeckoMediaPluginServiceParent>(this),
+ &GeckoMediaPluginServiceParent::CrashPlugins),
+ NS_DISPATCH_NORMAL);
+ }
+ }
+ }
+ } else if (!strcmp("profile-change-teardown", aTopic)) {
+ mWaitingForPluginsSyncShutdown = true;
+
+ nsCOMPtr<nsIThread> gmpThread;
+ DebugOnly<bool> plugins_empty;
+ {
+ MutexAutoLock lock(mMutex);
+ MOZ_ASSERT(!mShuttingDown);
+ mShuttingDown = true;
+ gmpThread = mGMPThread;
+#ifdef DEBUG
+ plugins_empty = mPlugins.IsEmpty();
+#endif
+ }
+
+ if (gmpThread) {
+ GMP_LOG_DEBUG(
+ "%s::%s Starting to unload plugins, waiting for sync shutdown...",
+ __CLASS__, __FUNCTION__);
+ gmpThread->Dispatch(
+ NewRunnableMethod("gmp::GeckoMediaPluginServiceParent::UnloadPlugins",
+ this,
+ &GeckoMediaPluginServiceParent::UnloadPlugins),
+ NS_DISPATCH_NORMAL);
+
+ // Wait for UnloadPlugins() to do sync shutdown...
+ SpinEventLoopUntil(
+ "GeckoMediaPluginServiceParent::Observe "
+ "WaitingForPluginsSyncShutdown"_ns,
+ [&]() { return !mWaitingForPluginsSyncShutdown; });
+ } else {
+ // GMP thread has already shutdown.
+ MOZ_ASSERT(plugins_empty);
+ mWaitingForPluginsSyncShutdown = false;
+ }
+
+ } else if (!strcmp(NS_XPCOM_SHUTDOWN_THREADS_OBSERVER_ID, aTopic)) {
+#ifdef DEBUG
+ MOZ_ASSERT(mShuttingDown);
+#endif
+ ShutdownGMPThread();
+ } else if (!strcmp(NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID, aTopic)) {
+ mXPCOMWillShutdown = true;
+ } else if (!strcmp("last-pb-context-exited", aTopic)) {
+ // When Private Browsing mode exits, all we need to do is clear
+ // mTempNodeIds. This drops all the node ids we've cached in memory
+ // for PB origin-pairs. If we try to open an origin-pair for non-PB
+ // mode, we'll get the NodeId salt stored on-disk, and if we try to
+ // open a PB mode origin-pair, we'll re-generate new salt.
+ mTempNodeIds.Clear();
+ } else if (!strcmp("browser:purge-session-history", aTopic)) {
+ // Clear everything!
+ if (!aSomeData || nsDependentString(aSomeData).IsEmpty()) {
+ return GMPDispatch(NewRunnableMethod(
+ "gmp::GeckoMediaPluginServiceParent::ClearStorage", this,
+ &GeckoMediaPluginServiceParent::ClearStorage));
+ }
+
+ // Clear nodeIds/records modified after |t|.
+ nsresult rv;
+ PRTime t = nsDependentString(aSomeData).ToInteger64(&rv, 10);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ return GMPDispatch(NewRunnableMethod<PRTime>(
+ "gmp::GeckoMediaPluginServiceParent::ClearRecentHistoryOnGMPThread",
+ this, &GeckoMediaPluginServiceParent::ClearRecentHistoryOnGMPThread,
+ t));
+ }
+
+ return NS_OK;
+}
+
+RefPtr<GenericPromise> GeckoMediaPluginServiceParent::EnsureInitialized() {
+ MonitorAutoLock lock(mInitPromiseMonitor);
+ if (mLoadPluginsFromDiskComplete) {
+ return GenericPromise::CreateAndResolve(true, __func__);
+ }
+ // We should have an init promise in flight.
+ MOZ_ASSERT(!mInitPromise.IsEmpty());
+ return mInitPromise.Ensure(__func__);
+}
+
+RefPtr<GetGMPContentParentPromise>
+GeckoMediaPluginServiceParent::GetContentParent(
+ GMPCrashHelper* aHelper, const NodeIdVariant& aNodeIdVariant,
+ const nsACString& aAPI, const nsTArray<nsCString>& aTags) {
+ AssertOnGMPThread();
+
+ nsCOMPtr<nsISerialEventTarget> thread(GetGMPThread());
+ if (!thread) {
+ MOZ_ASSERT_UNREACHABLE(
+ "We should always be called on GMP thread, so it should be live");
+ return GetGMPContentParentPromise::CreateAndReject(NS_ERROR_FAILURE,
+ __func__);
+ }
+
+ nsCString nodeIdString;
+ nsresult rv = GetNodeId(aNodeIdVariant, nodeIdString);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return GetGMPContentParentPromise::CreateAndReject(NS_ERROR_FAILURE,
+ __func__);
+ }
+
+ auto holder = MakeUnique<MozPromiseHolder<GetGMPContentParentPromise>>();
+ RefPtr<GetGMPContentParentPromise> promise = holder->Ensure(__func__);
+ EnsureInitialized()->Then(
+ thread, __func__,
+ [self = RefPtr<GeckoMediaPluginServiceParent>(this),
+ nodeIdString = std::move(nodeIdString), api = nsCString(aAPI),
+ tags = aTags.Clone(), helper = RefPtr<GMPCrashHelper>(aHelper),
+ holder = std::move(holder)](
+ const GenericPromise::ResolveOrRejectValue& aValue) mutable -> void {
+ if (aValue.IsReject()) {
+ NS_WARNING("GMPService::EnsureInitialized failed.");
+ holder->Reject(NS_ERROR_FAILURE, __func__);
+ return;
+ }
+ RefPtr<GMPParent> gmp =
+ self->SelectPluginForAPI(nodeIdString, api, tags);
+ GMP_LOG_DEBUG("%s: %p returning %p for api %s", __FUNCTION__,
+ self.get(), gmp.get(), api.get());
+ if (!gmp) {
+ NS_WARNING(
+ "GeckoMediaPluginServiceParent::GetContentParentFrom failed");
+ holder->Reject(NS_ERROR_FAILURE, __func__);
+ return;
+ }
+ self->ConnectCrashHelper(gmp->GetPluginId(), helper);
+ gmp->GetGMPContentParent(std::move(holder));
+ });
+
+ return promise;
+}
+
+void GeckoMediaPluginServiceParent::InitializePlugins(
+ nsISerialEventTarget* aGMPThread) {
+ MOZ_ASSERT(aGMPThread);
+ MonitorAutoLock lock(mInitPromiseMonitor);
+ if (mLoadPluginsFromDiskComplete) {
+ return;
+ }
+
+ RefPtr<GeckoMediaPluginServiceParent> self(this);
+ RefPtr<GenericPromise> p = mInitPromise.Ensure(__func__);
+ InvokeAsync(aGMPThread, this, __func__,
+ &GeckoMediaPluginServiceParent::LoadFromEnvironment)
+ ->Then(
+ aGMPThread, __func__,
+ [self]() -> void {
+ MonitorAutoLock lock(self->mInitPromiseMonitor);
+ self->mLoadPluginsFromDiskComplete = true;
+ self->mInitPromise.Resolve(true, __func__);
+ },
+ [self]() -> void {
+ MonitorAutoLock lock(self->mInitPromiseMonitor);
+ self->mLoadPluginsFromDiskComplete = true;
+ self->mInitPromise.Reject(NS_ERROR_FAILURE, __func__);
+ });
+}
+
+void GeckoMediaPluginServiceParent::NotifySyncShutdownComplete() {
+ MOZ_ASSERT(NS_IsMainThread());
+ mWaitingForPluginsSyncShutdown = false;
+}
+
+bool GeckoMediaPluginServiceParent::IsShuttingDown() {
+ AssertOnGMPThread();
+ return mShuttingDownOnGMPThread;
+}
+
+void GeckoMediaPluginServiceParent::UnloadPlugins() {
+ AssertOnGMPThread();
+ MOZ_ASSERT(!mShuttingDownOnGMPThread);
+ mShuttingDownOnGMPThread = true;
+
+ nsTArray<RefPtr<GMPParent>> plugins;
+ {
+ MutexAutoLock lock(mMutex);
+ // Move all plugins references to a local array. This way mMutex won't be
+ // locked when calling CloseActive (to avoid inter-locking).
+ std::swap(plugins, mPlugins);
+
+ for (GMPServiceParent* parent : mServiceParents) {
+ Unused << parent->SendBeginShutdown();
+ }
+
+ GMP_LOG_DEBUG("%s::%s plugins:%zu", __CLASS__, __FUNCTION__,
+ plugins.Length());
+#ifdef DEBUG
+ for (const auto& plugin : plugins) {
+ GMP_LOG_DEBUG("%s::%s plugin: '%s'", __CLASS__, __FUNCTION__,
+ plugin->GetDisplayName().get());
+ }
+#endif
+ }
+
+ // Note: CloseActive may be async; it could actually finish
+ // shutting down when all the plugins have unloaded.
+ for (const auto& plugin : plugins) {
+ plugin->CloseActive(true);
+ }
+
+ nsCOMPtr<nsIRunnable> task = NewRunnableMethod(
+ "GeckoMediaPluginServiceParent::NotifySyncShutdownComplete", this,
+ &GeckoMediaPluginServiceParent::NotifySyncShutdownComplete);
+ mMainThread->Dispatch(task.forget());
+}
+
+void GeckoMediaPluginServiceParent::CrashPlugins() {
+ GMP_LOG_DEBUG("%s::%s", __CLASS__, __FUNCTION__);
+ AssertOnGMPThread();
+
+ MutexAutoLock lock(mMutex);
+ for (size_t i = 0; i < mPlugins.Length(); i++) {
+ mPlugins[i]->Crash();
+ }
+}
+
+RefPtr<GenericPromise> GeckoMediaPluginServiceParent::LoadFromEnvironment() {
+ AssertOnGMPThread();
+ nsCOMPtr<nsISerialEventTarget> thread(GetGMPThread());
+ if (!thread) {
+ return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+
+ const char* env = PR_GetEnv("MOZ_GMP_PATH");
+ if (!env || !*env) {
+ return GenericPromise::CreateAndResolve(true, __func__);
+ }
+
+ nsString allpaths;
+ if (NS_WARN_IF(NS_FAILED(
+ NS_CopyNativeToUnicode(nsDependentCString(env), allpaths)))) {
+ return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+
+ nsTArray<RefPtr<GenericPromise>> promises;
+ uint32_t pos = 0;
+ while (pos < allpaths.Length()) {
+ // Loop over multiple path entries separated by colons (*nix) or
+ // semicolons (Windows)
+ int32_t next = allpaths.FindChar(XPCOM_ENV_PATH_SEPARATOR[0], pos);
+ if (next == -1) {
+ promises.AppendElement(
+ AddOnGMPThread(nsString(Substring(allpaths, pos))));
+ break;
+ } else {
+ promises.AppendElement(
+ AddOnGMPThread(nsString(Substring(allpaths, pos, next - pos))));
+ pos = next + 1;
+ }
+ }
+
+ mScannedPluginOnDisk = true;
+ return GenericPromise::All(thread, promises)
+ ->Then(
+ thread, __func__,
+ []() { return GenericPromise::CreateAndResolve(true, __func__); },
+ []() {
+ return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ });
+}
+
+class NotifyObserversTask final : public mozilla::Runnable {
+ public:
+ explicit NotifyObserversTask(const char* aTopic, nsString aData = u""_ns)
+ : Runnable(aTopic), mTopic(aTopic), mData(aData) {}
+ NS_IMETHOD Run() override {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCOMPtr<nsIObserverService> obsService =
+ mozilla::services::GetObserverService();
+ MOZ_ASSERT(obsService);
+ if (obsService) {
+ obsService->NotifyObservers(nullptr, mTopic, mData.get());
+ }
+ return NS_OK;
+ }
+
+ private:
+ ~NotifyObserversTask() = default;
+ const char* mTopic;
+ const nsString mData;
+};
+
+NS_IMETHODIMP
+GeckoMediaPluginServiceParent::PathRunnable::Run() {
+ mService->RemoveOnGMPThread(mPath, mOperation == REMOVE_AND_DELETE_FROM_DISK,
+ mDefer);
+
+ mService->UpdateContentProcessGMPCapabilities();
+ return NS_OK;
+}
+
+void GeckoMediaPluginServiceParent::UpdateContentProcessGMPCapabilities(
+ ContentParent* aContentProcess) {
+ if (!NS_IsMainThread()) {
+ nsCOMPtr<nsIRunnable> task = NewRunnableMethod<ContentParent*>(
+ "GeckoMediaPluginServiceParent::UpdateContentProcessGMPCapabilities",
+ this,
+ &GeckoMediaPluginServiceParent::UpdateContentProcessGMPCapabilities,
+ aContentProcess);
+ mMainThread->Dispatch(task.forget());
+ return;
+ }
+
+ typedef mozilla::dom::GMPCapabilityData GMPCapabilityData;
+ typedef mozilla::dom::GMPAPITags GMPAPITags;
+ typedef mozilla::dom::ContentParent ContentParent;
+
+ const uint32_t NO_H264 = 0;
+ const uint32_t HAS_H264 = 1;
+ const uint32_t NO_H264_1_DIR = 2;
+ const uint32_t NO_H264_2_PLUS_DIRS = 3;
+ const uint32_t NO_H264_DIR_IN_PROGRESS = 4;
+ uint32_t hasH264 = NO_H264;
+ if (mDirectoriesAdded == 1) {
+ hasH264 = NO_H264_1_DIR;
+ } else if (mDirectoriesAdded > 1) {
+ hasH264 = NO_H264_2_PLUS_DIRS;
+ }
+ if (mDirectoriesInProgress) {
+ hasH264 = NO_H264_DIR_IN_PROGRESS;
+ }
+ nsTArray<GMPCapabilityData> caps;
+ {
+ MutexAutoLock lock(mMutex);
+ for (const RefPtr<GMPParent>& gmp : mPlugins) {
+ // We have multiple instances of a GMPParent for a given GMP in the
+ // list, one per origin. So filter the list so that we don't include
+ // the same GMP's capabilities twice.
+ NS_ConvertUTF16toUTF8 name(gmp->GetPluginBaseName());
+ bool found = false;
+ for (const GMPCapabilityData& cap : caps) {
+ if (cap.name().Equals(name)) {
+ found = true;
+ break;
+ }
+ }
+ if (found) {
+ continue;
+ }
+ GMPCapabilityData x;
+ x.name() = name;
+ x.version() = gmp->GetVersion();
+ for (const GMPCapability& tag : gmp->GetCapabilities()) {
+ x.capabilities().AppendElement(GMPAPITags(tag.mAPIName, tag.mAPITags));
+ if (tag.mAPIName == nsLiteralCString(GMP_API_VIDEO_ENCODER) &&
+ tag.mAPITags.Contains("h264"_ns)) {
+ hasH264 = HAS_H264;
+ }
+ }
+ caps.AppendElement(std::move(x));
+ }
+ }
+
+ Telemetry::Accumulate(Telemetry::MEDIA_GMP_UPDATE_CONTENT_PROCESS_HAS_H264,
+ hasH264);
+
+ if (aContentProcess) {
+ Unused << aContentProcess->SendGMPsChanged(caps);
+ return;
+ }
+
+ for (auto* cp : ContentParent::AllProcesses(ContentParent::eLive)) {
+ Unused << cp->SendGMPsChanged(caps);
+ }
+
+ // For non-e10s, we must fire a notification so that any MediaKeySystemAccess
+ // requests waiting on a CDM to download will retry.
+ nsCOMPtr<nsIObserverService> obsService =
+ mozilla::services::GetObserverService();
+ MOZ_ASSERT(obsService);
+ if (obsService) {
+ obsService->NotifyObservers(nullptr, "gmp-changed", nullptr);
+ }
+}
+
+void GeckoMediaPluginServiceParent::SendFlushFOGData(
+ nsTArray<RefPtr<FlushFOGDataPromise>>& promises) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MutexAutoLock lock(mMutex);
+
+ for (const RefPtr<GMPParent>& gmp : mPlugins) {
+ if (gmp->State() != GMPState::Loaded) {
+ // Plugins that are not in the Loaded state have no process attached to
+ // them, and any IPC we would attempt to send them would be ignored (or
+ // result in a warning on debug builds).
+ continue;
+ }
+ RefPtr<FlushFOGDataPromise::Private> promise =
+ new FlushFOGDataPromise::Private(__func__);
+ // Direct dispatch will resolve the promise on the same thread, which is
+ // faster; FOGIPC will move execution back to the main thread.
+ promise->UseDirectTaskDispatch(__func__);
+ promises.EmplaceBack(promise);
+
+ mGMPThread->Dispatch(
+ NewRunnableMethod<ipc::ResolveCallback<ipc::ByteBuf>&&,
+ ipc::RejectCallback&&>(
+ "GMPParent::SendFlushFOGData", gmp,
+ static_cast<void (GMPParent::*)(
+ mozilla::ipc::ResolveCallback<ipc::ByteBuf>&& aResolve,
+ mozilla::ipc::RejectCallback&& aReject)>(
+ &GMPParent::SendFlushFOGData),
+
+ [promise](ipc::ByteBuf&& aValue) {
+ promise->Resolve(std::move(aValue), __func__);
+ },
+ [promise](ipc::ResponseRejectReason&& aReason) {
+ promise->Reject(std::move(aReason), __func__);
+ }),
+ NS_DISPATCH_NORMAL);
+ }
+}
+
+RefPtr<PGMPParent::TestTriggerMetricsPromise>
+GeckoMediaPluginServiceParent::TestTriggerMetrics() {
+ MOZ_ASSERT(NS_IsMainThread());
+ {
+ MutexAutoLock lock(mMutex);
+ for (const RefPtr<GMPParent>& gmp : mPlugins) {
+ if (gmp->State() != GMPState::Loaded) {
+ // Plugins that are not in the Loaded state have no process attached to
+ // them, and any IPC we would attempt to send them would be ignored (or
+ // result in a warning on debug builds).
+ continue;
+ }
+
+ RefPtr<PGMPParent::TestTriggerMetricsPromise::Private> promise =
+ new PGMPParent::TestTriggerMetricsPromise::Private(__func__);
+ // Direct dispatch will resolve the promise on the same thread, which is
+ // faster; FOGIPC will move execution back to the main thread.
+ promise->UseDirectTaskDispatch(__func__);
+
+ mGMPThread->Dispatch(
+ NewRunnableMethod<ipc::ResolveCallback<bool>&&,
+ ipc::RejectCallback&&>(
+ "GMPParent::SendTestTriggerMetrics", gmp,
+ static_cast<void (GMPParent::*)(
+ mozilla::ipc::ResolveCallback<bool>&& aResolve,
+ mozilla::ipc::RejectCallback&& aReject)>(
+ &PGMPParent::SendTestTriggerMetrics),
+
+ [promise](bool aValue) {
+ promise->Resolve(std::move(aValue), __func__);
+ },
+ [promise](ipc::ResponseRejectReason&& aReason) {
+ promise->Reject(std::move(aReason), __func__);
+ }),
+ NS_DISPATCH_NORMAL);
+
+ return promise;
+ }
+ }
+
+ return PGMPParent::TestTriggerMetricsPromise::CreateAndReject(
+ ipc::ResponseRejectReason::SendError, __func__);
+}
+
+RefPtr<GenericPromise> GeckoMediaPluginServiceParent::AsyncAddPluginDirectory(
+ const nsAString& aDirectory) {
+ nsCOMPtr<nsISerialEventTarget> thread(GetGMPThread());
+ if (!thread) {
+ return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+
+ mDirectoriesAdded++;
+ mDirectoriesInProgress++;
+
+ nsString dir(aDirectory);
+ RefPtr<GeckoMediaPluginServiceParent> self = this;
+ return InvokeAsync(thread, this, __func__,
+ &GeckoMediaPluginServiceParent::AddOnGMPThread, dir)
+ ->Then(
+ mMainThread, __func__,
+ [dir, self](bool aVal) {
+ GMP_LOG_DEBUG(
+ "GeckoMediaPluginServiceParent::AsyncAddPluginDirectory %s "
+ "succeeded",
+ NS_ConvertUTF16toUTF8(dir).get());
+ MOZ_ASSERT(NS_IsMainThread());
+ self->mDirectoriesInProgress--;
+ self->UpdateContentProcessGMPCapabilities();
+ return GenericPromise::CreateAndResolve(aVal, __func__);
+ },
+ [dir, self](nsresult aResult) {
+ GMP_LOG_DEBUG(
+ "GeckoMediaPluginServiceParent::AsyncAddPluginDirectory %s "
+ "failed",
+ NS_ConvertUTF16toUTF8(dir).get());
+ self->mDirectoriesInProgress--;
+ return GenericPromise::CreateAndReject(aResult, __func__);
+ });
+}
+
+NS_IMETHODIMP
+GeckoMediaPluginServiceParent::AddPluginDirectory(const nsAString& aDirectory) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RefPtr<GenericPromise> p = AsyncAddPluginDirectory(aDirectory);
+ Unused << p;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GeckoMediaPluginServiceParent::RemovePluginDirectory(
+ const nsAString& aDirectory) {
+ MOZ_ASSERT(NS_IsMainThread());
+ return GMPDispatch(
+ new PathRunnable(this, aDirectory, PathRunnable::EOperation::REMOVE));
+}
+
+NS_IMETHODIMP
+GeckoMediaPluginServiceParent::RemoveAndDeletePluginDirectory(
+ const nsAString& aDirectory, const bool aDefer) {
+ MOZ_ASSERT(NS_IsMainThread());
+ return GMPDispatch(new PathRunnable(
+ this, aDirectory, PathRunnable::EOperation::REMOVE_AND_DELETE_FROM_DISK,
+ aDefer));
+}
+
+NS_IMETHODIMP
+GeckoMediaPluginServiceParent::HasPluginForAPI(const nsACString& aAPI,
+ const nsTArray<nsCString>& aTags,
+ bool* aHasPlugin) {
+ NS_ENSURE_ARG(!aTags.IsEmpty());
+ NS_ENSURE_ARG(aHasPlugin);
+
+ nsresult rv = EnsurePluginsOnDiskScanned();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to load GMPs from disk.");
+ return rv;
+ }
+
+ {
+ MutexAutoLock lock(mMutex);
+ nsCString api(aAPI);
+ size_t index = 0;
+ RefPtr<GMPParent> gmp = FindPluginForAPIFrom(index, api, aTags, &index);
+ *aHasPlugin = !!gmp;
+ }
+
+ return NS_OK;
+}
+
+nsresult GeckoMediaPluginServiceParent::EnsurePluginsOnDiskScanned() {
+ const char* env = nullptr;
+ if (!mScannedPluginOnDisk && (env = PR_GetEnv("MOZ_GMP_PATH")) && *env) {
+ // We have a MOZ_GMP_PATH environment variable which may specify the
+ // location of plugins to load, and we haven't yet scanned the disk to
+ // see if there are plugins there. Get the GMP thread, which will
+ // cause an event to be dispatched to which scans for plugins. We
+ // dispatch a sync event to the GMP thread here in order to wait until
+ // after the GMP thread has scanned any paths in MOZ_GMP_PATH.
+ nsCOMPtr<nsIThread> thread;
+ nsresult rv = GetThread(getter_AddRefs(thread));
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = NS_DispatchAndSpinEventLoopUntilComplete(
+ "GeckoMediaPluginServiceParent::EnsurePluginsOnDiskScanned"_ns, thread,
+ MakeAndAddRef<mozilla::Runnable>("GMPDummyRunnable"));
+ NS_ENSURE_SUCCESS(rv, rv);
+ MOZ_ASSERT(mScannedPluginOnDisk, "Should have scanned MOZ_GMP_PATH by now");
+ }
+
+ return NS_OK;
+}
+
+already_AddRefed<GMPParent> GeckoMediaPluginServiceParent::FindPluginForAPIFrom(
+ size_t aSearchStartIndex, const nsACString& aAPI,
+ const nsTArray<nsCString>& aTags, size_t* aOutPluginIndex) {
+ mMutex.AssertCurrentThreadOwns();
+ for (size_t i = aSearchStartIndex; i < mPlugins.Length(); i++) {
+ RefPtr<GMPParent> gmp = mPlugins[i];
+ if (!GMPCapability::Supports(gmp->GetCapabilities(), aAPI, aTags)) {
+ continue;
+ }
+ if (aOutPluginIndex) {
+ *aOutPluginIndex = i;
+ }
+ return gmp.forget();
+ }
+ return nullptr;
+}
+
+already_AddRefed<GMPParent> GeckoMediaPluginServiceParent::SelectPluginForAPI(
+ const nsACString& aNodeId, const nsACString& aAPI,
+ const nsTArray<nsCString>& aTags) {
+ AssertOnGMPThread();
+
+ GMPParent* gmpToClone = nullptr;
+ {
+ MutexAutoLock lock(mMutex);
+ size_t index = 0;
+ RefPtr<GMPParent> gmp;
+ while ((gmp = FindPluginForAPIFrom(index, aAPI, aTags, &index))) {
+ if (aNodeId.IsEmpty()) {
+ if (gmp->CanBeSharedCrossNodeIds()) {
+ return gmp.forget();
+ }
+ } else if (gmp->CanBeUsedFrom(aNodeId)) {
+ return gmp.forget();
+ }
+
+ if (!gmpToClone ||
+ (gmpToClone->IsMarkedForDeletion() && !gmp->IsMarkedForDeletion())) {
+ // This GMP has the correct type but has the wrong nodeId; hold on to it
+ // in case we need to clone it.
+ // Prefer GMPs in-use for the case where an upgraded plugin version is
+ // waiting for the old one to die. If the old plugin is in use, we
+ // should continue using it so that any persistent state remains
+ // consistent. Otherwise, just check that the plugin isn't scheduled
+ // for deletion.
+ gmpToClone = gmp;
+ }
+ // Loop around and try the next plugin; it may be usable from aNodeId.
+ index++;
+ }
+ }
+
+ // Plugin exists, but we can't use it due to cross-origin separation. Create a
+ // new one.
+ if (gmpToClone) {
+ RefPtr<GMPParent> clone = ClonePlugin(gmpToClone);
+ {
+ MutexAutoLock lock(mMutex);
+ mPlugins.AppendElement(clone);
+ }
+ if (!aNodeId.IsEmpty()) {
+ clone->SetNodeId(aNodeId);
+ }
+ return clone.forget();
+ }
+
+ return nullptr;
+}
+
+static already_AddRefed<GMPParent> CreateGMPParent() {
+ // Should run on the GMP thread.
+#if defined(XP_LINUX) && defined(MOZ_SANDBOX)
+ if (!SandboxInfo::Get().CanSandboxMedia()) {
+ if (!StaticPrefs::media_gmp_insecure_allow()) {
+ NS_WARNING("Denying media plugin load due to lack of sandboxing.");
+ return nullptr;
+ }
+ NS_WARNING("Loading media plugin despite lack of sandboxing.");
+ }
+#endif
+ RefPtr<GMPParent> gmpParent = new GMPParent();
+ return gmpParent.forget();
+}
+
+already_AddRefed<GMPParent> GeckoMediaPluginServiceParent::ClonePlugin(
+ const GMPParent* aOriginal) {
+ AssertOnGMPThread();
+ MOZ_ASSERT(aOriginal);
+
+ RefPtr<GMPParent> gmp = CreateGMPParent();
+ if (!gmp) {
+ NS_WARNING("Can't Create GMPParent");
+ return nullptr;
+ }
+
+ gmp->CloneFrom(aOriginal);
+ return gmp.forget();
+}
+
+RefPtr<GenericPromise> GeckoMediaPluginServiceParent::AddOnGMPThread(
+ nsString aDirectory) {
+#ifdef XP_WIN
+ // On Windows our various test harnesses often pass paths with UNIX dir
+ // separators, or a mix of dir separators. NS_NewLocalFile() can't handle
+ // that, so fixup to match the platform's expected format. This makes us
+ // more robust in the face of bad input and test harnesses changing...
+ std::replace(aDirectory.BeginWriting(), aDirectory.EndWriting(), '/', '\\');
+#endif
+
+ AssertOnGMPThread();
+ nsCString dir = NS_ConvertUTF16toUTF8(aDirectory);
+ nsCOMPtr<nsISerialEventTarget> thread(GetGMPThread());
+ if (!thread) {
+ GMP_LOG_DEBUG("%s::%s: %s No GMP Thread", __CLASS__, __FUNCTION__,
+ dir.get());
+ return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+ GMP_LOG_DEBUG("%s::%s: %s", __CLASS__, __FUNCTION__, dir.get());
+
+ nsCOMPtr<nsIFile> directory;
+ nsresult rv = NS_NewLocalFile(aDirectory, false, getter_AddRefs(directory));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ GMP_LOG_DEBUG("%s::%s: failed to create nsIFile for dir=%s rv=%" PRIx32,
+ __CLASS__, __FUNCTION__, dir.get(),
+ static_cast<uint32_t>(rv));
+ return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+
+ RefPtr<GMPParent> gmp = CreateGMPParent();
+ if (!gmp) {
+ NS_WARNING("Can't Create GMPParent");
+ return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+
+ RefPtr<GeckoMediaPluginServiceParent> self(this);
+ return gmp->Init(this, directory)
+ ->Then(
+ thread, __func__,
+ [gmp, self, dir](bool aVal) {
+ GMP_LOG_DEBUG("%s::%s: %s Succeeded", __CLASS__, __FUNCTION__,
+ dir.get());
+ {
+ MutexAutoLock lock(self->mMutex);
+ self->mPlugins.AppendElement(gmp);
+ }
+ return GenericPromise::CreateAndResolve(aVal, __func__);
+ },
+ [dir](nsresult aResult) {
+ GMP_LOG_DEBUG("%s::%s: %s Failed", __CLASS__, __FUNCTION__,
+ dir.get());
+ return GenericPromise::CreateAndReject(aResult, __func__);
+ });
+}
+
+void GeckoMediaPluginServiceParent::RemoveOnGMPThread(
+ const nsAString& aDirectory, const bool aDeleteFromDisk,
+ const bool aCanDefer) {
+ AssertOnGMPThread();
+ GMP_LOG_DEBUG("%s::%s: %s", __CLASS__, __FUNCTION__,
+ NS_LossyConvertUTF16toASCII(aDirectory).get());
+
+ nsCOMPtr<nsIFile> directory;
+ nsresult rv = NS_NewLocalFile(aDirectory, false, getter_AddRefs(directory));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ // Plugin destruction can modify |mPlugins|. Put them aside for now and
+ // destroy them once we're done with |mPlugins|.
+ nsTArray<RefPtr<GMPParent>> deadPlugins;
+
+ bool inUse = false;
+ MutexAutoLock lock(mMutex);
+ for (size_t i = mPlugins.Length(); i-- > 0;) {
+ nsCOMPtr<nsIFile> pluginpath = mPlugins[i]->GetDirectory();
+ bool equals;
+ if (NS_FAILED(directory->Equals(pluginpath, &equals)) || !equals) {
+ continue;
+ }
+
+ RefPtr<GMPParent> gmp = mPlugins[i];
+ if (aDeleteFromDisk && gmp->State() != GMPState::NotLoaded) {
+ // We have to wait for the child process to release its lib handle
+ // before we can delete the GMP.
+ inUse = true;
+ gmp->MarkForDeletion();
+
+ if (!mPluginsWaitingForDeletion.Contains(aDirectory)) {
+ mPluginsWaitingForDeletion.AppendElement(aDirectory);
+ }
+ }
+
+ if (gmp->State() == GMPState::NotLoaded || !aCanDefer) {
+ // GMP not in use or shutdown is being forced; can shut it down now.
+ deadPlugins.AppendElement(gmp);
+ mPlugins.RemoveElementAt(i);
+ }
+ }
+
+ {
+ MutexAutoUnlock unlock(mMutex);
+ for (auto& gmp : deadPlugins) {
+ gmp->CloseActive(true);
+ }
+ }
+
+ if (aDeleteFromDisk && !inUse) {
+ // Ensure the GMP dir and all files in it are writable, so we have
+ // permission to delete them.
+ directory->SetPermissions(0700);
+ DirectoryEnumerator iter(directory, DirectoryEnumerator::FilesAndDirs);
+ for (nsCOMPtr<nsIFile> dirEntry; (dirEntry = iter.Next()) != nullptr;) {
+ dirEntry->SetPermissions(0700);
+ }
+ if (NS_SUCCEEDED(directory->Remove(true))) {
+ mPluginsWaitingForDeletion.RemoveElement(aDirectory);
+ nsCOMPtr<nsIRunnable> task = new NotifyObserversTask(
+ "gmp-directory-deleted", nsString(aDirectory));
+ mMainThread->Dispatch(task.forget());
+ }
+ }
+}
+
+// May remove when Bug 1043671 is fixed
+static void Dummy(RefPtr<GMPParent> aOnDeathsDoor) {
+ // exists solely to do nothing and let the Runnable kill the GMPParent
+ // when done.
+}
+
+void GeckoMediaPluginServiceParent::PluginTerminated(
+ const RefPtr<GMPParent>& aPlugin) {
+ AssertOnGMPThread();
+
+ if (aPlugin->IsMarkedForDeletion()) {
+ nsString path;
+ RefPtr<nsIFile> dir = aPlugin->GetDirectory();
+ nsresult rv = dir->GetPath(path);
+ NS_ENSURE_SUCCESS_VOID(rv);
+ if (mPluginsWaitingForDeletion.Contains(path)) {
+ RemoveOnGMPThread(path, true /* delete */, true /* can defer */);
+ }
+ }
+}
+
+void GeckoMediaPluginServiceParent::ReAddOnGMPThread(
+ const RefPtr<GMPParent>& aOld) {
+ AssertOnGMPThread();
+ GMP_LOG_DEBUG("%s::%s: %p", __CLASS__, __FUNCTION__, (void*)aOld);
+
+ RefPtr<GMPParent> gmp;
+ if (!mShuttingDownOnGMPThread) {
+ // We're not shutting down, so replace the old plugin in the list with a
+ // clone which is in a pristine state. Note: We place the plugin in
+ // the same slot in the array as a hack to ensure if we re-request with
+ // the same capabilities we get an instance of the same plugin.
+ gmp = ClonePlugin(aOld);
+ MutexAutoLock lock(mMutex);
+ MOZ_ASSERT(mPlugins.Contains(aOld));
+ if (mPlugins.Contains(aOld)) {
+ mPlugins[mPlugins.IndexOf(aOld)] = gmp;
+ }
+ } else {
+ // We're shutting down; don't re-add plugin, let the old plugin die.
+ MutexAutoLock lock(mMutex);
+ mPlugins.RemoveElement(aOld);
+ }
+ // Schedule aOld to be destroyed. We can't destroy it from here since we
+ // may be inside ActorDestroyed() for it.
+ NS_DispatchToCurrentThread(WrapRunnableNM(&Dummy, aOld));
+}
+
+NS_IMETHODIMP
+GeckoMediaPluginServiceParent::GetStorageDir(nsIFile** aOutFile) {
+ if (NS_WARN_IF(!mStorageBaseDir)) {
+ return NS_ERROR_FAILURE;
+ }
+ return mStorageBaseDir->Clone(aOutFile);
+}
+
+nsresult WriteToFile(nsIFile* aPath, const nsACString& aFileName,
+ const nsACString& aData) {
+ nsCOMPtr<nsIFile> path;
+ nsresult rv = aPath->Clone(getter_AddRefs(path));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = path->AppendNative(aFileName);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ PRFileDesc* f = nullptr;
+ rv = path->OpenNSPRFileDesc(PR_WRONLY | PR_CREATE_FILE, PR_IRWXU, &f);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ int32_t len = PR_Write(f, aData.BeginReading(), aData.Length());
+ PR_Close(f);
+ if (NS_WARN_IF(len < 0 || (size_t)len != aData.Length())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+static nsresult ReadFromFile(nsIFile* aPath, const nsACString& aFileName,
+ nsACString& aOutData, int32_t aMaxLength) {
+ nsCOMPtr<nsIFile> path;
+ nsresult rv = aPath->Clone(getter_AddRefs(path));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = path->AppendNative(aFileName);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ PRFileDesc* f = nullptr;
+ rv = path->OpenNSPRFileDesc(PR_RDONLY | PR_CREATE_FILE, PR_IRWXU, &f);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ auto size = PR_Seek(f, 0, PR_SEEK_END);
+ PR_Seek(f, 0, PR_SEEK_SET);
+
+ if (size > aMaxLength) {
+ return NS_ERROR_FAILURE;
+ }
+ aOutData.SetLength(size);
+
+ auto len = PR_Read(f, aOutData.BeginWriting(), size);
+ PR_Close(f);
+ if (NS_WARN_IF(len != size)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+nsresult ReadSalt(nsIFile* aPath, nsACString& aOutData) {
+ return ReadFromFile(aPath, "salt"_ns, aOutData, NodeIdSaltLength);
+}
+
+already_AddRefed<GMPStorage> GeckoMediaPluginServiceParent::GetMemoryStorageFor(
+ const nsACString& aNodeId) {
+ return do_AddRef(mTempGMPStorage.LookupOrInsertWith(
+ aNodeId, [] { return CreateGMPMemoryStorage(); }));
+}
+
+NS_IMETHODIMP
+GeckoMediaPluginServiceParent::IsPersistentStorageAllowed(
+ const nsACString& aNodeId, bool* aOutAllowed) {
+ AssertOnGMPThread();
+ NS_ENSURE_ARG(aOutAllowed);
+ // We disallow persistent storage for the NodeId used for shared GMP
+ // decoding, to prevent GMP decoding being used to track what a user
+ // watches somehow.
+ *aOutAllowed = !aNodeId.Equals(SHARED_GMP_DECODING_NODE_ID) &&
+ mPersistentStorageAllowed.Get(aNodeId);
+ return NS_OK;
+}
+
+nsresult GeckoMediaPluginServiceParent::GetNodeId(
+ const nsAString& aOrigin, const nsAString& aTopLevelOrigin,
+ const nsAString& aGMPName, nsACString& aOutId) {
+ AssertOnGMPThread();
+ GMP_LOG_DEBUG("%s::%s: (%s, %s)", __CLASS__, __FUNCTION__,
+ NS_ConvertUTF16toUTF8(aOrigin).get(),
+ NS_ConvertUTF16toUTF8(aTopLevelOrigin).get());
+
+ nsresult rv;
+
+ if (aOrigin.EqualsLiteral("null") || aOrigin.IsEmpty() ||
+ aTopLevelOrigin.EqualsLiteral("null") || aTopLevelOrigin.IsEmpty()) {
+ // (origin, topLevelOrigin) is null or empty; this is for an anonymous
+ // origin, probably a local file, for which we don't provide persistent
+ // storage. Generate a random node id, and don't store it so that the GMP's
+ // storage is temporary and the process for this GMP is not shared with GMP
+ // instances that have the same nodeId.
+ nsAutoCString salt;
+ rv = GenerateRandomPathName(salt, NodeIdSaltLength);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ aOutId = salt;
+ mPersistentStorageAllowed.InsertOrUpdate(salt, false);
+ return NS_OK;
+ }
+
+ const uint32_t hash =
+ AddToHash(HashString(aOrigin), HashString(aTopLevelOrigin));
+
+ if (OriginAttributes::IsPrivateBrowsing(NS_ConvertUTF16toUTF8(aOrigin))) {
+ // For PB mode, we store the node id, indexed by the origin pair and GMP
+ // name, so that if the same origin pair is opened for the same GMP in this
+ // session, it gets the same node id.
+ const uint32_t pbHash = AddToHash(HashString(aGMPName), hash);
+ return mTempNodeIds.WithEntryHandle(pbHash, [&](auto&& entry) {
+ if (!entry) {
+ // No salt stored, generate and temporarily store some for this id.
+ nsAutoCString newSalt;
+ rv = GenerateRandomPathName(newSalt, NodeIdSaltLength);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ auto salt = MakeUnique<nsCString>(newSalt);
+
+ mPersistentStorageAllowed.InsertOrUpdate(*salt, false);
+
+ entry.Insert(std::move(salt));
+ }
+
+ aOutId = *entry.Data();
+ return NS_OK;
+ });
+ }
+
+ // Otherwise, try to see if we've previously generated and stored salt
+ // for this origin pair.
+ nsCOMPtr<nsIFile> path; // $profileDir/gmp/$platform/
+ rv = GetStorageDir(getter_AddRefs(path));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // $profileDir/gmp/$platform/$gmpName/
+ rv = path->Append(aGMPName);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // $profileDir/gmp/$platform/$gmpName/id/
+ rv = path->AppendNative("id"_ns);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsAutoCString hashStr;
+ hashStr.AppendInt((int64_t)hash);
+
+ // $profileDir/gmp/$platform/$gmpName/id/$hash
+ rv = path->AppendNative(hashStr);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = path->Create(nsIFile::DIRECTORY_TYPE, 0700);
+ if (rv != NS_ERROR_FILE_ALREADY_EXISTS && NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIFile> saltFile;
+ rv = path->Clone(getter_AddRefs(saltFile));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = saltFile->AppendNative("salt"_ns);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsAutoCString salt;
+ bool exists = false;
+ rv = saltFile->Exists(&exists);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ if (!exists) {
+ // No stored salt for this origin. Generate salt, and store it and
+ // the origin on disk.
+ nsresult rv = GenerateRandomPathName(salt, NodeIdSaltLength);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ MOZ_ASSERT(salt.Length() == NodeIdSaltLength);
+
+ // $profileDir/gmp/$platform/$gmpName/id/$hash/salt
+ rv = WriteToFile(path, "salt"_ns, salt);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // $profileDir/gmp/$platform/$gmpName/id/$hash/origin
+ rv = WriteToFile(path, "origin"_ns, NS_ConvertUTF16toUTF8(aOrigin));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // $profileDir/gmp/$platform/$gmpName/id/$hash/topLevelOrigin
+ rv = WriteToFile(path, "topLevelOrigin"_ns,
+ NS_ConvertUTF16toUTF8(aTopLevelOrigin));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ } else {
+ rv = ReadSalt(path, salt);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ }
+
+ aOutId = salt;
+ mPersistentStorageAllowed.InsertOrUpdate(salt, true);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GeckoMediaPluginServiceParent::GetNodeId(
+ const nsAString& aOrigin, const nsAString& aTopLevelOrigin,
+ const nsAString& aGMPName, UniquePtr<GetNodeIdCallback>&& aCallback) {
+ nsCString nodeId;
+ nsresult rv = GetNodeId(aOrigin, aTopLevelOrigin, aGMPName, nodeId);
+ aCallback->Done(rv, nodeId);
+ return rv;
+}
+
+nsresult GeckoMediaPluginServiceParent::GetNodeId(
+ const NodeIdVariant& aNodeIdVariant, nsACString& aOutId) {
+ if (aNodeIdVariant.type() == NodeIdVariant::TnsCString) {
+ // The union already contains a node ID as a string, use that.
+ aOutId = aNodeIdVariant.get_nsCString();
+ return NS_OK;
+ }
+ // The union contains a NodeIdParts, convert it to a node ID string.
+ NodeIdParts nodeIdParts{aNodeIdVariant.get_NodeIdParts()};
+ return GetNodeId(nodeIdParts.mOrigin(), nodeIdParts.mTopLevelOrigin(),
+ nodeIdParts.mGMPName(), aOutId);
+}
+
+static bool ExtractHostName(const nsACString& aOrigin, nsACString& aOutData) {
+ nsCString str;
+ str.Assign(aOrigin);
+ int begin = str.Find("://");
+ // The scheme is missing!
+ if (begin == -1) {
+ return false;
+ }
+
+ int end = str.RFind(":");
+ // Remove the port number
+ if (end != begin) {
+ str.SetLength(end);
+ }
+
+ nsDependentCSubstring host(str, begin + 3);
+ aOutData.Assign(host);
+ return true;
+}
+
+constexpr uint32_t kMaxDomainLength = 253;
+// http://en.wikipedia.org/wiki/Domain_Name_System#Domain_name_syntax
+
+constexpr std::array<nsLiteralCString, 2> kFileNames = {"origin"_ns,
+ "topLevelOrigin"_ns};
+
+bool MatchOrigin(nsIFile* aPath, const nsACString& aSite,
+ const mozilla::OriginAttributesPattern& aPattern) {
+ nsresult rv;
+ nsCString str;
+ nsCString originNoSuffix;
+ for (const auto& fileName : kFileNames) {
+ rv = ReadFromFile(aPath, fileName, str, kMaxDomainLength);
+ mozilla::OriginAttributes originAttributes;
+ if (NS_FAILED(rv) ||
+ !originAttributes.PopulateFromOrigin(str, originNoSuffix)) {
+ // Fails on parsing the originAttributes, treat this as a non-match.
+ return false;
+ }
+
+ if (ExtractHostName(originNoSuffix, str) && str.Equals(aSite) &&
+ aPattern.Matches(originAttributes)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool MatchBaseDomain(nsIFile* aPath, const nsACString& aBaseDomain) {
+ nsresult rv;
+ nsCString fileContent;
+ nsCString originNoSuffix;
+ for (const auto& fileName : kFileNames) {
+ rv = ReadFromFile(aPath, fileName, fileContent, kMaxDomainLength);
+ mozilla::OriginAttributes originAttributes;
+ if (NS_FAILED(rv) ||
+ !originAttributes.PopulateFromOrigin(fileContent, originNoSuffix)) {
+ // Fails on parsing the originAttributes, treat this as a non-match.
+ return false;
+ }
+ nsCString originHostname;
+ if (!ExtractHostName(originNoSuffix, originHostname)) {
+ return false;
+ }
+ bool success;
+ rv = net::HasRootDomain(originHostname, aBaseDomain, &success);
+ if (NS_SUCCEEDED(rv) && success) {
+ return true;
+ }
+ }
+ return false;
+}
+
+template <typename T>
+static void KillPlugins(const nsTArray<RefPtr<GMPParent>>& aPlugins,
+ Mutex& aMutex, T&& aFilter) {
+ // Shutdown the plugins when |aFilter| evaluates to true.
+ // After we clear storage data, node IDs will become invalid and shouldn't be
+ // used anymore. We need to kill plugins with such nodeIDs.
+ // Note: we can't shut them down while holding the lock,
+ // as the lock is not re-entrant and shutdown requires taking the lock.
+ // The plugin list is only edited on the GMP thread, so this should be OK.
+ nsTArray<RefPtr<GMPParent>> pluginsToKill;
+ {
+ MutexAutoLock lock(aMutex);
+ for (size_t i = 0; i < aPlugins.Length(); i++) {
+ RefPtr<GMPParent> parent(aPlugins[i]);
+ if (aFilter(parent)) {
+ pluginsToKill.AppendElement(parent);
+ }
+ }
+ }
+
+ for (size_t i = 0; i < pluginsToKill.Length(); i++) {
+ pluginsToKill[i]->CloseActive(false);
+ }
+}
+
+struct NodeFilter {
+ explicit NodeFilter(const nsTArray<nsCString>& nodeIDs) : mNodeIDs(nodeIDs) {}
+ bool operator()(GMPParent* aParent) {
+ return mNodeIDs.Contains(aParent->GetNodeId());
+ }
+
+ private:
+ const nsTArray<nsCString>& mNodeIDs;
+};
+
+void GeckoMediaPluginServiceParent::ClearNodeIdAndPlugin(
+ DirectoryFilter& aFilter) {
+ // $profileDir/gmp/$platform/
+ nsCOMPtr<nsIFile> path;
+ nsresult rv = GetStorageDir(getter_AddRefs(path));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ // Iterate all sub-folders of $profileDir/gmp/$platform/, i.e. the dirs in
+ // which specific GMPs store their data.
+ DirectoryEnumerator iter(path, DirectoryEnumerator::DirsOnly);
+ for (nsCOMPtr<nsIFile> pluginDir; (pluginDir = iter.Next()) != nullptr;) {
+ ClearNodeIdAndPlugin(pluginDir, aFilter);
+ }
+}
+
+void GeckoMediaPluginServiceParent::ClearNodeIdAndPlugin(
+ nsIFile* aPluginStorageDir, DirectoryFilter& aFilter) {
+ // $profileDir/gmp/$platform/$gmpName/id/
+ nsCOMPtr<nsIFile> path = CloneAndAppend(aPluginStorageDir, u"id"_ns);
+ if (!path) {
+ return;
+ }
+
+ // Iterate all sub-folders of $profileDir/gmp/$platform/$gmpName/id/
+ nsTArray<nsCString> nodeIDsToClear;
+ DirectoryEnumerator iter(path, DirectoryEnumerator::DirsOnly);
+ for (nsCOMPtr<nsIFile> dirEntry; (dirEntry = iter.Next()) != nullptr;) {
+ // dirEntry is the hash of origins, i.e.:
+ // $profileDir/gmp/$platform/$gmpName/id/$originHash/
+ if (!aFilter(dirEntry)) {
+ continue;
+ }
+ nsAutoCString salt;
+ if (NS_SUCCEEDED(ReadSalt(dirEntry, salt))) {
+ // Keep node IDs to clear data/plugins associated with them later.
+ nodeIDsToClear.AppendElement(salt);
+ // Also remove node IDs from the table.
+ mPersistentStorageAllowed.Remove(salt);
+ }
+ // Now we can remove the directory for the origin pair.
+ if (NS_FAILED(dirEntry->Remove(true))) {
+ NS_WARNING("Failed to delete the directory for the origin pair");
+ }
+ }
+
+ // Kill plugin instances that have node IDs being cleared.
+ KillPlugins(mPlugins, mMutex, NodeFilter(nodeIDsToClear));
+
+ // Clear all storage in $profileDir/gmp/$platform/$gmpName/storage/$nodeId/
+ path = CloneAndAppend(aPluginStorageDir, u"storage"_ns);
+ if (!path) {
+ return;
+ }
+
+ for (const nsACString& nodeId : nodeIDsToClear) {
+ nsCOMPtr<nsIFile> dirEntry;
+ nsresult rv = path->Clone(getter_AddRefs(dirEntry));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ rv = dirEntry->AppendNative(nodeId);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ if (NS_FAILED(dirEntry->Remove(true))) {
+ NS_WARNING("Failed to delete GMP storage directory for the node");
+ }
+ }
+}
+
+void GeckoMediaPluginServiceParent::ForgetThisSiteOnGMPThread(
+ const nsACString& aSite, const mozilla::OriginAttributesPattern& aPattern) {
+ AssertOnGMPThread();
+ GMP_LOG_DEBUG("%s::%s: origin=%s", __CLASS__, __FUNCTION__, aSite.Data());
+
+ struct OriginFilter : public DirectoryFilter {
+ explicit OriginFilter(const nsACString& aSite,
+ const mozilla::OriginAttributesPattern& aPattern)
+ : mSite(aSite), mPattern(aPattern) {}
+ bool operator()(nsIFile* aPath) override {
+ return MatchOrigin(aPath, mSite, mPattern);
+ }
+
+ private:
+ const nsACString& mSite;
+ const mozilla::OriginAttributesPattern& mPattern;
+ } filter(aSite, aPattern);
+
+ ClearNodeIdAndPlugin(filter);
+}
+
+void GeckoMediaPluginServiceParent::ForgetThisBaseDomainOnGMPThread(
+ const nsACString& aBaseDomain) {
+ AssertOnGMPThread();
+ GMP_LOG_DEBUG("%s::%s: baseDomain=%s", __CLASS__, __FUNCTION__,
+ aBaseDomain.Data());
+
+ struct BaseDomainFilter : public DirectoryFilter {
+ explicit BaseDomainFilter(const nsACString& aBaseDomain)
+ : mBaseDomain(aBaseDomain) {}
+ bool operator()(nsIFile* aPath) override {
+ return MatchBaseDomain(aPath, mBaseDomain);
+ }
+
+ private:
+ const nsACString& mBaseDomain;
+ } filter(aBaseDomain);
+
+ ClearNodeIdAndPlugin(filter);
+}
+
+void GeckoMediaPluginServiceParent::ClearRecentHistoryOnGMPThread(
+ PRTime aSince) {
+ AssertOnGMPThread();
+ GMP_LOG_DEBUG("%s::%s: since=%" PRId64, __CLASS__, __FUNCTION__,
+ (int64_t)aSince);
+
+ struct MTimeFilter : public DirectoryFilter {
+ explicit MTimeFilter(PRTime aSince) : mSince(aSince) {}
+
+ // Return true if any files under aPath is modified after |mSince|.
+ bool IsModifiedAfter(nsIFile* aPath) {
+ PRTime lastModified;
+ nsresult rv = aPath->GetLastModifiedTime(&lastModified);
+ if (NS_SUCCEEDED(rv) && lastModified >= mSince) {
+ return true;
+ }
+ DirectoryEnumerator iter(aPath, DirectoryEnumerator::FilesAndDirs);
+ for (nsCOMPtr<nsIFile> dirEntry; (dirEntry = iter.Next()) != nullptr;) {
+ if (IsModifiedAfter(dirEntry)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // |aPath| is $profileDir/gmp/$platform/$gmpName/id/$originHash/
+ bool operator()(nsIFile* aPath) override {
+ if (IsModifiedAfter(aPath)) {
+ return true;
+ }
+
+ nsAutoCString salt;
+ if (NS_WARN_IF(NS_FAILED(ReadSalt(aPath, salt)))) {
+ return false;
+ }
+
+ // $profileDir/gmp/$platform/$gmpName/id/
+ nsCOMPtr<nsIFile> idDir;
+ if (NS_WARN_IF(NS_FAILED(aPath->GetParent(getter_AddRefs(idDir))))) {
+ return false;
+ }
+ // $profileDir/gmp/$platform/$gmpName/
+ nsCOMPtr<nsIFile> temp;
+ if (NS_WARN_IF(NS_FAILED(idDir->GetParent(getter_AddRefs(temp))))) {
+ return false;
+ }
+
+ // $profileDir/gmp/$platform/$gmpName/storage/
+ if (NS_WARN_IF(NS_FAILED(temp->Append(u"storage"_ns)))) {
+ return false;
+ }
+ // $profileDir/gmp/$platform/$gmpName/storage/$originSalt
+ return NS_SUCCEEDED(temp->AppendNative(salt)) && IsModifiedAfter(temp);
+ }
+
+ private:
+ const PRTime mSince;
+ } filter(aSince);
+
+ ClearNodeIdAndPlugin(filter);
+
+ nsCOMPtr<nsIRunnable> task =
+ new NotifyObserversTask("gmp-clear-storage-complete");
+ mMainThread->Dispatch(task.forget());
+}
+
+NS_IMETHODIMP
+GeckoMediaPluginServiceParent::ForgetThisSite(const nsAString& aSite,
+ const nsAString& aPattern) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mozilla::OriginAttributesPattern pattern;
+
+ if (!pattern.Init(aPattern)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ return ForgetThisSiteNative(aSite, pattern);
+}
+
+nsresult GeckoMediaPluginServiceParent::ForgetThisSiteNative(
+ const nsAString& aSite, const mozilla::OriginAttributesPattern& aPattern) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ return GMPDispatch(
+ NewRunnableMethod<nsCString, mozilla::OriginAttributesPattern>(
+ "gmp::GeckoMediaPluginServiceParent::ForgetThisSiteOnGMPThread", this,
+ &GeckoMediaPluginServiceParent::ForgetThisSiteOnGMPThread,
+ NS_ConvertUTF16toUTF8(aSite), aPattern));
+}
+
+NS_IMETHODIMP
+GeckoMediaPluginServiceParent::ForgetThisBaseDomain(
+ const nsAString& aBaseDomain) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ return ForgetThisBaseDomainNative(aBaseDomain);
+}
+
+nsresult GeckoMediaPluginServiceParent::ForgetThisBaseDomainNative(
+ const nsAString& aBaseDomain) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ return GMPDispatch(NewRunnableMethod<nsCString>(
+ "gmp::GeckoMediaPluginServiceParent::ForgetThisBaseDomainOnGMPThread",
+ this, &GeckoMediaPluginServiceParent::ForgetThisBaseDomainOnGMPThread,
+ NS_ConvertUTF16toUTF8(aBaseDomain)));
+}
+
+static bool IsNodeIdValid(GMPParent* aParent) {
+ return !aParent->GetNodeId().IsEmpty();
+}
+
+NS_IMETHODIMP
+GeckoMediaPluginServiceParent::GetName(nsAString& aName) {
+ aName = u"GeckoMediaPluginServiceParent: shutdown"_ns;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+GeckoMediaPluginServiceParent::GetState(nsIPropertyBag**) { return NS_OK; }
+
+NS_IMETHODIMP
+GeckoMediaPluginServiceParent::BlockShutdown(nsIAsyncShutdownClient*) {
+ return NS_OK;
+}
+
+// Called from GMPServiceParent::Create() which holds the lock
+void GeckoMediaPluginServiceParent::ServiceUserCreated(
+ GMPServiceParent* aServiceParent) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mMutex.AssertCurrentThreadOwns();
+
+ MOZ_ASSERT(!mServiceParents.Contains(aServiceParent));
+ mServiceParents.AppendElement(aServiceParent);
+ if (mServiceParents.Length() == 1) {
+ nsCOMPtr<nsIAsyncShutdownClient> barrier = GetShutdownBarrier();
+ MOZ_RELEASE_ASSERT(barrier);
+
+ nsresult rv = barrier->AddBlocker(
+ this, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__,
+ u"GeckoMediaPluginServiceParent shutdown"_ns);
+ MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
+ }
+}
+
+void GeckoMediaPluginServiceParent::ServiceUserDestroyed(
+ GMPServiceParent* aServiceParent) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MutexAutoLock lock(mMutex);
+ MOZ_ASSERT(mServiceParents.Length() > 0);
+ MOZ_ASSERT(mServiceParents.Contains(aServiceParent));
+ mServiceParents.RemoveElement(aServiceParent);
+ if (mServiceParents.IsEmpty()) {
+ nsCOMPtr<nsIAsyncShutdownClient> barrier = GetShutdownBarrier();
+ MOZ_RELEASE_ASSERT(barrier);
+ nsresult rv = barrier->RemoveBlocker(this);
+ MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
+ }
+}
+
+void GeckoMediaPluginServiceParent::ClearStorage() {
+ AssertOnGMPThread();
+ GMP_LOG_DEBUG("%s::%s", __CLASS__, __FUNCTION__);
+
+ // Kill plugins with valid nodeIDs.
+ KillPlugins(mPlugins, mMutex, &IsNodeIdValid);
+
+ nsCOMPtr<nsIFile> path; // $profileDir/gmp/$platform/
+ nsresult rv = GetStorageDir(getter_AddRefs(path));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ if (NS_FAILED(path->Remove(true))) {
+ NS_WARNING("Failed to delete GMP storage directory");
+ }
+
+ // Clear private-browsing storage.
+ mTempGMPStorage.Clear();
+
+ nsCOMPtr<nsIRunnable> task =
+ new NotifyObserversTask("gmp-clear-storage-complete");
+ mMainThread->Dispatch(task.forget());
+}
+
+already_AddRefed<GMPParent> GeckoMediaPluginServiceParent::GetById(
+ uint32_t aPluginId) {
+ MutexAutoLock lock(mMutex);
+ for (const RefPtr<GMPParent>& gmp : mPlugins) {
+ if (gmp->GetPluginId() == aPluginId) {
+ return do_AddRef(gmp);
+ }
+ }
+ return nullptr;
+}
+
+GMPServiceParent::GMPServiceParent(GeckoMediaPluginServiceParent* aService)
+ : mService(aService) {
+ MOZ_ASSERT(NS_IsMainThread(), "Should be constructed on the main thread");
+ MOZ_ASSERT(mService);
+ mService->ServiceUserCreated(this);
+}
+
+GMPServiceParent::~GMPServiceParent() {
+ MOZ_ASSERT(NS_IsMainThread(), "Should be destroyted on the main thread");
+ MOZ_ASSERT(mService);
+ mService->ServiceUserDestroyed(this);
+}
+
+mozilla::ipc::IPCResult GMPServiceParent::RecvLaunchGMP(
+ const NodeIdVariant& aNodeIdVariant, const nsACString& aAPI,
+ nsTArray<nsCString>&& aTags, nsTArray<ProcessId>&& aAlreadyBridgedTo,
+ uint32_t* aOutPluginId, GMPPluginType* aOutPluginType,
+ ProcessId* aOutProcessId, nsCString* aOutDisplayName,
+ Endpoint<PGMPContentParent>* aOutEndpoint, nsresult* aOutRv,
+ nsCString* aOutErrorDescription) {
+ if (mService->IsShuttingDown()) {
+ *aOutRv = NS_ERROR_ILLEGAL_DURING_SHUTDOWN;
+ *aOutErrorDescription = "Service is shutting down."_ns;
+ *aOutPluginId = 0;
+ *aOutPluginType = GMPPluginType::Unknown;
+ return IPC_OK();
+ }
+
+ nsCString nodeIdString;
+ nsresult rv = mService->GetNodeId(aNodeIdVariant, nodeIdString);
+ if (!NS_SUCCEEDED(rv)) {
+ *aOutRv = rv;
+ *aOutErrorDescription = "GetNodeId failed."_ns;
+ *aOutPluginId = 0;
+ *aOutPluginType = GMPPluginType::Unknown;
+ return IPC_OK();
+ }
+
+ RefPtr<GMPParent> gmp =
+ mService->SelectPluginForAPI(nodeIdString, aAPI, aTags);
+ if (gmp) {
+ *aOutPluginId = gmp->GetPluginId();
+ *aOutPluginType = gmp->GetPluginType();
+ } else {
+ *aOutRv = NS_ERROR_FAILURE;
+ *aOutErrorDescription = "SelectPluginForAPI returns nullptr."_ns;
+ *aOutPluginId = 0;
+ *aOutPluginType = GMPPluginType::Unknown;
+ return IPC_OK();
+ }
+
+ if (!gmp->EnsureProcessLoaded(aOutProcessId)) {
+ *aOutRv = NS_ERROR_FAILURE;
+ *aOutErrorDescription = "Process has not loaded."_ns;
+ return IPC_OK();
+ }
+
+ *aOutDisplayName = gmp->GetDisplayName();
+
+ if (aAlreadyBridgedTo.Contains(*aOutProcessId)) {
+ *aOutRv = NS_OK;
+ return IPC_OK();
+ }
+
+ Endpoint<PGMPContentParent> parent;
+ Endpoint<PGMPContentChild> child;
+ rv =
+ PGMPContent::CreateEndpoints(OtherPid(), *aOutProcessId, &parent, &child);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ *aOutRv = rv;
+ *aOutErrorDescription = "PGMPContent::CreateEndpoints failed."_ns;
+ return IPC_OK();
+ }
+
+ *aOutEndpoint = std::move(parent);
+
+ if (!gmp->SendInitGMPContentChild(std::move(child))) {
+ *aOutRv = NS_ERROR_FAILURE;
+ *aOutErrorDescription = "SendInitGMPContentChild failed."_ns;
+ return IPC_OK();
+ }
+
+ gmp->IncrementGMPContentChildCount();
+
+ *aOutRv = NS_OK;
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPServiceParent::RecvGetGMPNodeId(
+ const nsAString& aOrigin, const nsAString& aTopLevelOrigin,
+ const nsAString& aGMPName, nsCString* aID) {
+ nsresult rv = mService->GetNodeId(aOrigin, aTopLevelOrigin, aGMPName, *aID);
+ if (!NS_SUCCEEDED(rv)) {
+ return IPC_FAIL(
+ this,
+ "GMPServiceParent::RecvGetGMPNodeId: mService->GetNodeId failed.");
+ }
+ return IPC_OK();
+}
+
+class OpenPGMPServiceParent : public mozilla::Runnable {
+ public:
+ OpenPGMPServiceParent(RefPtr<GMPServiceParent>&& aGMPServiceParent,
+ ipc::Endpoint<PGMPServiceParent>&& aEndpoint,
+ bool* aResult)
+ : Runnable("gmp::OpenPGMPServiceParent"),
+ mGMPServiceParent(aGMPServiceParent),
+ mEndpoint(std::move(aEndpoint)),
+ mResult(aResult) {}
+
+ NS_IMETHOD Run() override {
+ *mResult = mEndpoint.Bind(mGMPServiceParent);
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<GMPServiceParent> mGMPServiceParent;
+ ipc::Endpoint<PGMPServiceParent> mEndpoint;
+ bool* mResult;
+};
+
+/* static */
+bool GMPServiceParent::Create(Endpoint<PGMPServiceParent>&& aGMPService) {
+ RefPtr<GeckoMediaPluginServiceParent> gmp =
+ GeckoMediaPluginServiceParent::GetSingleton();
+
+ if (!gmp || gmp->mShuttingDown) {
+ // Shutdown is initiated. There is no point creating a new actor.
+ return false;
+ }
+
+ nsCOMPtr<nsIThread> gmpThread;
+ RefPtr<GMPServiceParent> serviceParent;
+ {
+ MutexAutoLock lock(gmp->mMutex);
+ nsresult rv = gmp->GetThreadLocked(getter_AddRefs(gmpThread));
+ NS_ENSURE_SUCCESS(rv, false);
+ serviceParent = new GMPServiceParent(gmp);
+ }
+ bool ok;
+ nsresult rv = NS_DispatchAndSpinEventLoopUntilComplete(
+ "GMPServiceParent::Create"_ns, gmpThread,
+ do_AddRef(new OpenPGMPServiceParent(std::move(serviceParent),
+ std::move(aGMPService), &ok)));
+
+ if (NS_WARN_IF(NS_FAILED(rv) || !ok)) {
+ return false;
+ }
+
+ // Now that the service parent is set up, it will be released by IPC
+ // refcounting, so we don't need to hold any more references here.
+
+ return true;
+}
+
+} // namespace mozilla::gmp
+
+#undef __CLASS__
diff --git a/dom/media/gmp/GMPServiceParent.h b/dom/media/gmp/GMPServiceParent.h
new file mode 100644
index 0000000000..0a897d6a39
--- /dev/null
+++ b/dom/media/gmp/GMPServiceParent.h
@@ -0,0 +1,281 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GMPServiceParent_h_
+#define GMPServiceParent_h_
+
+#include "GMPService.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/gmp/PGMPServiceParent.h"
+#include "mozIGeckoMediaPluginChromeService.h"
+#include "nsClassHashtable.h"
+#include "nsTHashMap.h"
+#include "mozilla/Atomics.h"
+#include "nsNetUtil.h"
+#include "nsIAsyncShutdown.h"
+#include "nsRefPtrHashtable.h"
+#include "nsThreadUtils.h"
+#include "mozilla/gmp/PGMPParent.h"
+#include "mozilla/MozPromise.h"
+#include "GMPStorage.h"
+
+template <class>
+struct already_AddRefed;
+using FlushFOGDataPromise = mozilla::dom::ContentParent::FlushFOGDataPromise;
+using ContentParent = mozilla::dom::ContentParent;
+
+namespace mozilla {
+class OriginAttributesPattern;
+
+namespace gmp {
+
+class GMPParent;
+class GMPServiceParent;
+
+class GeckoMediaPluginServiceParent final
+ : public GeckoMediaPluginService,
+ public mozIGeckoMediaPluginChromeService,
+ public nsIAsyncShutdownBlocker {
+ public:
+ static already_AddRefed<GeckoMediaPluginServiceParent> GetSingleton();
+
+ GeckoMediaPluginServiceParent();
+ nsresult Init() override;
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSIASYNCSHUTDOWNBLOCKER
+
+ // mozIGeckoMediaPluginService
+ NS_IMETHOD HasPluginForAPI(const nsACString& aAPI,
+ const nsTArray<nsCString>& aTags,
+ bool* aRetVal) override;
+ NS_IMETHOD GetNodeId(const nsAString& aOrigin,
+ const nsAString& aTopLevelOrigin,
+ const nsAString& aGMPName,
+ UniquePtr<GetNodeIdCallback>&& aCallback) override;
+
+ NS_DECL_MOZIGECKOMEDIAPLUGINCHROMESERVICE
+ NS_DECL_NSIOBSERVER
+
+ RefPtr<GenericPromise> EnsureInitialized();
+ RefPtr<GenericPromise> AsyncAddPluginDirectory(const nsAString& aDirectory);
+
+ // GMP thread access only
+ bool IsShuttingDown();
+
+ already_AddRefed<GMPStorage> GetMemoryStorageFor(const nsACString& aNodeId);
+ nsresult ForgetThisSiteNative(
+ const nsAString& aSite, const mozilla::OriginAttributesPattern& aPattern);
+
+ nsresult ForgetThisBaseDomainNative(const nsAString& aBaseDomain);
+
+ // Notifies that some user of this class is created/destroyed.
+ void ServiceUserCreated(GMPServiceParent* aServiceParent);
+ void ServiceUserDestroyed(GMPServiceParent* aServiceParent);
+
+ // If aContentProcess is specified, this will only update GMP caps in that
+ // content process, otherwise will update all content processes.
+ void UpdateContentProcessGMPCapabilities(
+ ContentParent* aContentProcess = nullptr);
+
+ void SendFlushFOGData(nsTArray<RefPtr<FlushFOGDataPromise>>& promises);
+
+ /*
+ * ** Test-only Method **
+ *
+ * Trigger GMP-process test metric instrumentation.
+ */
+ RefPtr<PGMPParent::TestTriggerMetricsPromise> TestTriggerMetrics();
+
+ private:
+ friend class GMPServiceParent;
+
+ virtual ~GeckoMediaPluginServiceParent();
+
+ void ClearStorage();
+
+ already_AddRefed<GMPParent> SelectPluginForAPI(
+ const nsACString& aNodeId, const nsACString& aAPI,
+ const nsTArray<nsCString>& aTags);
+
+ already_AddRefed<GMPParent> FindPluginForAPIFrom(
+ size_t aSearchStartIndex, const nsACString& aAPI,
+ const nsTArray<nsCString>& aTags, size_t* aOutPluginIndex);
+
+ nsresult GetNodeId(const nsAString& aOrigin, const nsAString& aTopLevelOrigin,
+ const nsAString& aGMPName, nsACString& aOutId);
+
+ void UnloadPlugins();
+ void CrashPlugins();
+ void NotifySyncShutdownComplete();
+
+ void RemoveOnGMPThread(const nsAString& aDirectory,
+ const bool aDeleteFromDisk, const bool aCanDefer);
+
+ struct DirectoryFilter {
+ virtual bool operator()(nsIFile* aPath) = 0;
+ ~DirectoryFilter() = default;
+ };
+ void ClearNodeIdAndPlugin(DirectoryFilter& aFilter);
+ void ClearNodeIdAndPlugin(nsIFile* aPluginStorageDir,
+ DirectoryFilter& aFilter);
+ void ForgetThisSiteOnGMPThread(
+ const nsACString& aSite,
+ const mozilla::OriginAttributesPattern& aPattern);
+ void ForgetThisBaseDomainOnGMPThread(const nsACString& aBaseDomain);
+ void ClearRecentHistoryOnGMPThread(PRTime aSince);
+
+ already_AddRefed<GMPParent> GetById(uint32_t aPluginId);
+
+ protected:
+ friend class GMPParent;
+ void ReAddOnGMPThread(const RefPtr<GMPParent>& aOld);
+ void PluginTerminated(const RefPtr<GMPParent>& aOld);
+ void InitializePlugins(nsISerialEventTarget* GMPThread) override;
+ RefPtr<GenericPromise> LoadFromEnvironment();
+ RefPtr<GenericPromise> AddOnGMPThread(nsString aDirectory);
+
+ RefPtr<GetGMPContentParentPromise> GetContentParent(
+ GMPCrashHelper* aHelper, const NodeIdVariant& aNodeIdVariant,
+ const nsACString& aAPI, const nsTArray<nsCString>& aTags) override;
+
+ private:
+ // Creates a copy of aOriginal. Note that the caller is responsible for
+ // adding this to GeckoMediaPluginServiceParent::mPlugins.
+ already_AddRefed<GMPParent> ClonePlugin(const GMPParent* aOriginal);
+ nsresult EnsurePluginsOnDiskScanned();
+ nsresult InitStorage();
+
+ // Get a string based node ID from a NodeIdVariant. This will
+ // either fetch the internal string, or convert the internal NodeIdParts to a
+ // string. The conversion process is fallible, so the return value should be
+ // checked.
+ nsresult GetNodeId(const NodeIdVariant& aNodeIdVariant, nsACString& aOutId);
+
+ class PathRunnable : public Runnable {
+ public:
+ enum EOperation {
+ REMOVE,
+ REMOVE_AND_DELETE_FROM_DISK,
+ };
+
+ PathRunnable(GeckoMediaPluginServiceParent* aService,
+ const nsAString& aPath, EOperation aOperation,
+ bool aDefer = false)
+ : Runnable("gmp::GeckoMediaPluginServiceParent::PathRunnable"),
+ mService(aService),
+ mPath(aPath),
+ mOperation(aOperation),
+ mDefer(aDefer) {}
+
+ NS_DECL_NSIRUNNABLE
+
+ private:
+ RefPtr<GeckoMediaPluginServiceParent> mService;
+ nsString mPath;
+ EOperation mOperation;
+ bool mDefer;
+ };
+
+ // Protected by mMutex from the base class.
+ nsTArray<RefPtr<GMPParent>> mPlugins;
+
+ // True if we've inspected MOZ_GMP_PATH on the GMP thread and loaded any
+ // plugins found there into mPlugins.
+ Atomic<bool> mScannedPluginOnDisk;
+
+ template <typename T>
+ class MainThreadOnly {
+ public:
+ MOZ_IMPLICIT MainThreadOnly(T aValue) : mValue(aValue) {}
+ operator T&() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mValue;
+ }
+
+ private:
+ T mValue;
+ };
+
+ MainThreadOnly<bool> mShuttingDown;
+ MainThreadOnly<bool> mWaitingForPluginsSyncShutdown;
+
+ nsTArray<nsString> mPluginsWaitingForDeletion;
+
+ nsCOMPtr<nsIFile> mStorageBaseDir;
+
+ // Hashes of (origin,topLevelOrigin) to the node id for
+ // non-persistent sessions.
+ nsClassHashtable<nsUint32HashKey, nsCString> mTempNodeIds;
+
+ // Hashes node id to whether that node id is allowed to store data
+ // persistently on disk.
+ nsTHashMap<nsCStringHashKey, bool> mPersistentStorageAllowed;
+
+ // Synchronization for barrier that ensures we've loaded GMPs from
+ // MOZ_GMP_PATH before allowing GetContentParentFrom() to proceed.
+ Monitor mInitPromiseMonitor MOZ_UNANNOTATED;
+ MozMonitoredPromiseHolder<GenericPromise> mInitPromise;
+ bool mLoadPluginsFromDiskComplete;
+
+ // Hashes nodeId to the hashtable of storage for that nodeId.
+ nsRefPtrHashtable<nsCStringHashKey, GMPStorage> mTempGMPStorage;
+
+ // Tracks how many IPC connections to GMPServices running in content
+ // processes we have. When this is empty we can safely shut down.
+ // Synchronized across thread via mMutex in base class.
+ nsTArray<GMPServiceParent*> mServiceParents;
+
+ uint32_t mDirectoriesAdded = 0;
+ uint32_t mDirectoriesInProgress = 0;
+};
+
+nsresult WriteToFile(nsIFile* aPath, const nsACString& aFileName,
+ const nsACString& aData);
+nsresult ReadSalt(nsIFile* aPath, nsACString& aOutData);
+bool MatchOrigin(nsIFile* aPath, const nsACString& aSite,
+ const mozilla::OriginAttributesPattern& aPattern);
+bool MatchBaseDomain(nsIFile* aPath, const nsACString& aBaseDomain);
+
+class GMPServiceParent final : public PGMPServiceParent {
+ public:
+ explicit GMPServiceParent(GeckoMediaPluginServiceParent* aService);
+
+ // Our refcounting is thread safe, and when our refcount drops to zero
+ // we dispatch an event to the main thread to delete the GMPServiceParent.
+ // Note that this means it's safe for references to this object to be
+ // released on a non main thread, but the destructor will always run on
+ // the main thread.
+
+ // Mark AddRef and Release as `final`, as they overload pure virtual
+ // implementations in PGMPServiceParent.
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING_WITH_DELETE_ON_MAIN_THREAD(
+ GMPServiceParent, final);
+
+ ipc::IPCResult RecvGetGMPNodeId(const nsAString& aOrigin,
+ const nsAString& aTopLevelOrigin,
+ const nsAString& aGMPName,
+ nsCString* aID) override;
+
+ static bool Create(Endpoint<PGMPServiceParent>&& aGMPService);
+
+ ipc::IPCResult RecvLaunchGMP(
+ const NodeIdVariant& aNodeIdVariant, const nsACString& aAPI,
+ nsTArray<nsCString>&& aTags, nsTArray<ProcessId>&& aAlreadyBridgedTo,
+ uint32_t* aOutPluginId, GMPPluginType* aOutPluginType,
+ ProcessId* aOutProcessId, nsCString* aOutDisplayName,
+ Endpoint<PGMPContentParent>* aOutEndpoint, nsresult* aOutRv,
+ nsCString* aOutErrorDescription) override;
+
+ private:
+ ~GMPServiceParent();
+
+ RefPtr<GeckoMediaPluginServiceParent> mService;
+};
+
+} // namespace gmp
+} // namespace mozilla
+
+#endif // GMPServiceParent_h_
diff --git a/dom/media/gmp/GMPSharedMemManager.cpp b/dom/media/gmp/GMPSharedMemManager.cpp
new file mode 100644
index 0000000000..5da04247cb
--- /dev/null
+++ b/dom/media/gmp/GMPSharedMemManager.cpp
@@ -0,0 +1,89 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GMPSharedMemManager.h"
+#include "GMPMessageUtils.h"
+#include "mozilla/ipc/SharedMemory.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/ClearOnShutdown.h"
+
+namespace mozilla::gmp {
+
+// Really one set of pools on each side of the plugin API.
+
+// YUV buffers go from Encoder parent to child; pool there, and then return
+// with Decoded() frames to the Decoder parent and goes into the parent pool.
+// Compressed (encoded) data goes from the Decoder parent to the child;
+// pool there, and then return with Encoded() frames and goes into the parent
+// pool.
+bool GMPSharedMemManager::MgrAllocShmem(GMPSharedMem::GMPMemoryClasses aClass,
+ size_t aSize, ipc::Shmem* aMem) {
+ mData->CheckThread();
+
+ // first look to see if we have a free buffer large enough
+ for (uint32_t i = 0; i < GetGmpFreelist(aClass).Length(); i++) {
+ MOZ_ASSERT(GetGmpFreelist(aClass)[i].IsWritable());
+ if (aSize <= GetGmpFreelist(aClass)[i].Size<uint8_t>()) {
+ *aMem = GetGmpFreelist(aClass)[i];
+ GetGmpFreelist(aClass).RemoveElementAt(i);
+ return true;
+ }
+ }
+
+ // Didn't find a buffer free with enough space; allocate one
+ size_t pagesize = ipc::SharedMemory::SystemPageSize();
+ aSize = (aSize + (pagesize - 1)) & ~(pagesize - 1); // round up to page size
+ bool retval = Alloc(aSize, aMem);
+ if (retval) {
+ // The allocator (or NeedsShmem call) should never return less than we ask
+ // for...
+ MOZ_ASSERT(aMem->Size<uint8_t>() >= aSize);
+ mData->mGmpAllocated[aClass]++;
+ }
+ return retval;
+}
+
+bool GMPSharedMemManager::MgrDeallocShmem(GMPSharedMem::GMPMemoryClasses aClass,
+ ipc::Shmem& aMem) {
+ mData->CheckThread();
+
+ size_t size = aMem.Size<uint8_t>();
+
+ // XXX Bug NNNNNNN Until we put better guards on ipc::shmem, verify we
+ // weren't fed an shmem we already had.
+ for (uint32_t i = 0; i < GetGmpFreelist(aClass).Length(); i++) {
+ if (NS_WARN_IF(aMem == GetGmpFreelist(aClass)[i])) {
+ // Safest to crash in this case; should never happen in normal
+ // operation.
+ MOZ_CRASH("Deallocating Shmem we already have in our cache!");
+ // return true;
+ }
+ }
+
+ // XXX This works; there are better pool algorithms. We need to avoid
+ // "falling off a cliff" with too low a number
+ if (GetGmpFreelist(aClass).Length() > 10) {
+ Dealloc(std::move(GetGmpFreelist(aClass)[0]));
+ GetGmpFreelist(aClass).RemoveElementAt(0);
+ // The allocation numbers will be fubar on the Child!
+ mData->mGmpAllocated[aClass]--;
+ }
+ for (uint32_t i = 0; i < GetGmpFreelist(aClass).Length(); i++) {
+ MOZ_ASSERT(GetGmpFreelist(aClass)[i].IsWritable());
+ if (size < GetGmpFreelist(aClass)[i].Size<uint8_t>()) {
+ GetGmpFreelist(aClass).InsertElementAt(i, aMem);
+ return true;
+ }
+ }
+ GetGmpFreelist(aClass).AppendElement(aMem);
+
+ return true;
+}
+
+uint32_t GMPSharedMemManager::NumInUse(GMPSharedMem::GMPMemoryClasses aClass) {
+ return mData->mGmpAllocated[aClass] - GetGmpFreelist(aClass).Length();
+}
+
+} // namespace mozilla::gmp
diff --git a/dom/media/gmp/GMPSharedMemManager.h b/dom/media/gmp/GMPSharedMemManager.h
new file mode 100644
index 0000000000..1a04bdde14
--- /dev/null
+++ b/dom/media/gmp/GMPSharedMemManager.h
@@ -0,0 +1,78 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GMPSharedMemManager_h_
+#define GMPSharedMemManager_h_
+
+#include "mozilla/ipc/Shmem.h"
+#include "nsTArray.h"
+
+namespace mozilla::gmp {
+
+class GMPSharedMemManager;
+
+class GMPSharedMem {
+ public:
+ typedef enum {
+ kGMPFrameData = 0,
+ kGMPEncodedData,
+ kGMPNumTypes
+ } GMPMemoryClasses;
+
+ // This is a heuristic - max of 10 free in the Child pool, plus those
+ // in-use for the encoder and decoder at the given moment and not yet
+ // returned to the parent pool (which is not included). If more than
+ // this are needed, we presume the client has either crashed or hung
+ // (perhaps temporarily).
+ static const uint32_t kGMPBufLimit = 40;
+
+ GMPSharedMem() {
+ for (size_t i = 0; i < sizeof(mGmpAllocated) / sizeof(mGmpAllocated[0]);
+ i++) {
+ mGmpAllocated[i] = 0;
+ }
+ }
+ virtual ~GMPSharedMem() = default;
+
+ // Parent and child impls will differ here
+ virtual void CheckThread() = 0;
+
+ protected:
+ friend class GMPSharedMemManager;
+
+ nsTArray<ipc::Shmem> mGmpFreelist[GMPSharedMem::kGMPNumTypes];
+ uint32_t mGmpAllocated[GMPSharedMem::kGMPNumTypes];
+};
+
+class GMPSharedMemManager {
+ public:
+ explicit GMPSharedMemManager(GMPSharedMem* aData) : mData(aData) {}
+ virtual ~GMPSharedMemManager() = default;
+
+ virtual bool MgrAllocShmem(GMPSharedMem::GMPMemoryClasses aClass,
+ size_t aSize, ipc::Shmem* aMem);
+ virtual bool MgrDeallocShmem(GMPSharedMem::GMPMemoryClasses aClass,
+ ipc::Shmem& aMem);
+
+ // So we can know if data is "piling up" for the plugin - I.e. it's hung or
+ // crashed
+ virtual uint32_t NumInUse(GMPSharedMem::GMPMemoryClasses aClass);
+
+ // These have to be implemented using the AllocShmem/etc provided by the
+ // IPDL-generated interfaces, so have the Parent/Child implement them.
+ virtual bool Alloc(size_t aSize, ipc::Shmem* aMem) = 0;
+ virtual void Dealloc(ipc::Shmem&& aMem) = 0;
+
+ private:
+ nsTArray<ipc::Shmem>& GetGmpFreelist(GMPSharedMem::GMPMemoryClasses aTypes) {
+ return mData->mGmpFreelist[aTypes];
+ }
+
+ GMPSharedMem* mData;
+};
+
+} // namespace mozilla::gmp
+
+#endif // GMPSharedMemManager_h_
diff --git a/dom/media/gmp/GMPStorage.h b/dom/media/gmp/GMPStorage.h
new file mode 100644
index 0000000000..aca8ca3f26
--- /dev/null
+++ b/dom/media/gmp/GMPStorage.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GMPStorage_h_
+#define GMPStorage_h_
+
+#include "gmp-storage.h"
+#include "mozilla/AlreadyAddRefed.h"
+#include "nsISupportsImpl.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+namespace mozilla::gmp {
+
+class GMPStorage {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GMPStorage)
+
+ virtual GMPErr Open(const nsACString& aRecordName) = 0;
+ virtual bool IsOpen(const nsACString& aRecordName) const = 0;
+ virtual GMPErr Read(const nsACString& aRecordName,
+ nsTArray<uint8_t>& aOutBytes) = 0;
+ virtual GMPErr Write(const nsACString& aRecordName,
+ const nsTArray<uint8_t>& aBytes) = 0;
+ virtual void Close(const nsACString& aRecordName) = 0;
+
+ protected:
+ virtual ~GMPStorage() = default;
+};
+
+already_AddRefed<GMPStorage> CreateGMPMemoryStorage();
+already_AddRefed<GMPStorage> CreateGMPDiskStorage(const nsACString& aNodeId,
+ const nsAString& aGMPName);
+
+} // namespace mozilla::gmp
+
+#endif
diff --git a/dom/media/gmp/GMPStorageChild.cpp b/dom/media/gmp/GMPStorageChild.cpp
new file mode 100644
index 0000000000..b08356e0d6
--- /dev/null
+++ b/dom/media/gmp/GMPStorageChild.cpp
@@ -0,0 +1,244 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GMPStorageChild.h"
+#include "GMPChild.h"
+#include "gmp-storage.h"
+#include "base/task.h"
+
+#define ON_GMP_THREAD() (mPlugin->GMPMessageLoop() == MessageLoop::current())
+
+#define CALL_ON_GMP_THREAD(_func, ...) \
+ do { \
+ if (ON_GMP_THREAD()) { \
+ _func(__VA_ARGS__); \
+ } else { \
+ mPlugin->GMPMessageLoop()->PostTask( \
+ dont_add_new_uses_of_this::NewRunnableMethod( \
+ this, &GMPStorageChild::_func, ##__VA_ARGS__)); \
+ } \
+ } while (false)
+
+namespace mozilla::gmp {
+
+static nsTArray<uint8_t> ToArray(const uint8_t* aData, uint32_t aDataSize) {
+ nsTArray<uint8_t> data;
+ data.AppendElements(aData, aDataSize);
+ return data;
+}
+
+GMPRecordImpl::GMPRecordImpl(GMPStorageChild* aOwner, const nsCString& aName,
+ GMPRecordClient* aClient)
+ : mName(aName), mClient(aClient), mOwner(aOwner) {}
+
+GMPErr GMPRecordImpl::Open() { return mOwner->Open(this); }
+
+void GMPRecordImpl::OpenComplete(GMPErr aStatus) {
+ mClient->OpenComplete(aStatus);
+}
+
+GMPErr GMPRecordImpl::Read() { return mOwner->Read(this); }
+
+void GMPRecordImpl::ReadComplete(GMPErr aStatus, const uint8_t* aBytes,
+ uint32_t aLength) {
+ mClient->ReadComplete(aStatus, aBytes, aLength);
+}
+
+GMPErr GMPRecordImpl::Write(const uint8_t* aData, uint32_t aDataSize) {
+ return mOwner->Write(this, aData, aDataSize);
+}
+
+void GMPRecordImpl::WriteComplete(GMPErr aStatus) {
+ mClient->WriteComplete(aStatus);
+}
+
+GMPErr GMPRecordImpl::Close() {
+ RefPtr<GMPRecordImpl> kungfuDeathGrip(this);
+ // Delete our self reference.
+ Release();
+ mOwner->Close(this->Name());
+ return GMPNoErr;
+}
+
+GMPStorageChild::GMPStorageChild(GMPChild* aPlugin)
+ : mMonitor("GMPStorageChild"), mPlugin(aPlugin), mShutdown(false) {
+ MOZ_ASSERT(ON_GMP_THREAD());
+}
+
+GMPErr GMPStorageChild::CreateRecord(const nsCString& aRecordName,
+ GMPRecord** aOutRecord,
+ GMPRecordClient* aClient) {
+ MonitorAutoLock lock(mMonitor);
+
+ if (mShutdown) {
+ NS_WARNING("GMPStorage used after it's been shutdown!");
+ return GMPClosedErr;
+ }
+
+ MOZ_ASSERT(aRecordName.Length() && aOutRecord);
+
+ if (HasRecord(aRecordName)) {
+ return GMPRecordInUse;
+ }
+
+ RefPtr<GMPRecordImpl> record(new GMPRecordImpl(this, aRecordName, aClient));
+ mRecords.InsertOrUpdate(aRecordName, RefPtr{record}); // Addrefs
+
+ // The GMPRecord holds a self reference until the GMP calls Close() on
+ // it. This means the object is always valid (even if neutered) while
+ // the GMP expects it to be.
+ record.forget(aOutRecord);
+
+ return GMPNoErr;
+}
+
+bool GMPStorageChild::HasRecord(const nsCString& aRecordName) {
+ mMonitor.AssertCurrentThreadOwns();
+ return mRecords.Contains(aRecordName);
+}
+
+already_AddRefed<GMPRecordImpl> GMPStorageChild::GetRecord(
+ const nsCString& aRecordName) {
+ MonitorAutoLock lock(mMonitor);
+ RefPtr<GMPRecordImpl> record;
+ mRecords.Get(aRecordName, getter_AddRefs(record));
+ return record.forget();
+}
+
+GMPErr GMPStorageChild::Open(GMPRecordImpl* aRecord) {
+ MonitorAutoLock lock(mMonitor);
+
+ if (mShutdown) {
+ NS_WARNING("GMPStorage used after it's been shutdown!");
+ return GMPClosedErr;
+ }
+
+ if (!HasRecord(aRecord->Name())) {
+ // Trying to re-open a record that has already been closed.
+ return GMPClosedErr;
+ }
+
+ CALL_ON_GMP_THREAD(SendOpen, aRecord->Name());
+
+ return GMPNoErr;
+}
+
+GMPErr GMPStorageChild::Read(GMPRecordImpl* aRecord) {
+ MonitorAutoLock lock(mMonitor);
+
+ if (mShutdown) {
+ NS_WARNING("GMPStorage used after it's been shutdown!");
+ return GMPClosedErr;
+ }
+
+ if (!HasRecord(aRecord->Name())) {
+ // Record not opened.
+ return GMPClosedErr;
+ }
+
+ CALL_ON_GMP_THREAD(SendRead, aRecord->Name());
+
+ return GMPNoErr;
+}
+
+GMPErr GMPStorageChild::Write(GMPRecordImpl* aRecord, const uint8_t* aData,
+ uint32_t aDataSize) {
+ if (aDataSize > GMP_MAX_RECORD_SIZE) {
+ return GMPQuotaExceededErr;
+ }
+
+ MonitorAutoLock lock(mMonitor);
+
+ if (mShutdown) {
+ NS_WARNING("GMPStorage used after it's been shutdown!");
+ return GMPClosedErr;
+ }
+
+ if (!HasRecord(aRecord->Name())) {
+ // Record not opened.
+ return GMPClosedErr;
+ }
+
+ CALL_ON_GMP_THREAD(SendWrite, aRecord->Name(), ToArray(aData, aDataSize));
+
+ return GMPNoErr;
+}
+
+GMPErr GMPStorageChild::Close(const nsCString& aRecordName) {
+ MonitorAutoLock lock(mMonitor);
+
+ if (!HasRecord(aRecordName)) {
+ // Already closed.
+ return GMPClosedErr;
+ }
+
+ mRecords.Remove(aRecordName);
+
+ if (!mShutdown) {
+ CALL_ON_GMP_THREAD(SendClose, aRecordName);
+ }
+
+ return GMPNoErr;
+}
+
+mozilla::ipc::IPCResult GMPStorageChild::RecvOpenComplete(
+ const nsCString& aRecordName, const GMPErr& aStatus) {
+ // We don't need a lock to read |mShutdown| since it is only changed in
+ // the GMP thread.
+ if (mShutdown) {
+ return IPC_OK();
+ }
+ RefPtr<GMPRecordImpl> record = GetRecord(aRecordName);
+ if (!record) {
+ // Not fatal.
+ return IPC_OK();
+ }
+ record->OpenComplete(aStatus);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPStorageChild::RecvReadComplete(
+ const nsCString& aRecordName, const GMPErr& aStatus,
+ nsTArray<uint8_t>&& aBytes) {
+ if (mShutdown) {
+ return IPC_OK();
+ }
+ RefPtr<GMPRecordImpl> record = GetRecord(aRecordName);
+ if (!record) {
+ // Not fatal.
+ return IPC_OK();
+ }
+ record->ReadComplete(aStatus, aBytes.Elements(), aBytes.Length());
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPStorageChild::RecvWriteComplete(
+ const nsCString& aRecordName, const GMPErr& aStatus) {
+ if (mShutdown) {
+ return IPC_OK();
+ }
+ RefPtr<GMPRecordImpl> record = GetRecord(aRecordName);
+ if (!record) {
+ // Not fatal.
+ return IPC_OK();
+ }
+ record->WriteComplete(aStatus);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPStorageChild::RecvShutdown() {
+ // Block any new storage requests, and thus any messages back to the
+ // parent. We don't delete any objects here, as that may invalidate
+ // GMPRecord pointers held by the GMP.
+ MonitorAutoLock lock(mMonitor);
+ mShutdown = true;
+ return IPC_OK();
+}
+
+} // namespace mozilla::gmp
+
+// avoid redefined macro in unified build
+#undef ON_GMP_THREAD
+#undef CALL_ON_GMP_THREAD
diff --git a/dom/media/gmp/GMPStorageChild.h b/dom/media/gmp/GMPStorageChild.h
new file mode 100644
index 0000000000..97c61a5007
--- /dev/null
+++ b/dom/media/gmp/GMPStorageChild.h
@@ -0,0 +1,94 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GMPStorageChild_h_
+#define GMPStorageChild_h_
+
+#include "mozilla/gmp/PGMPStorageChild.h"
+#include "gmp-storage.h"
+#include "nsTHashtable.h"
+#include "nsRefPtrHashtable.h"
+#include "gmp-platform.h"
+
+#include <queue>
+
+namespace mozilla::gmp {
+
+class GMPChild;
+class GMPStorageChild;
+
+class GMPRecordImpl : public GMPRecord {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GMPRecordImpl)
+
+ GMPRecordImpl(GMPStorageChild* aOwner, const nsCString& aName,
+ GMPRecordClient* aClient);
+
+ // GMPRecord.
+ GMPErr Open() override;
+ GMPErr Read() override;
+ GMPErr Write(const uint8_t* aData, uint32_t aDataSize) override;
+ GMPErr Close() override;
+
+ const nsCString& Name() const { return mName; }
+
+ void OpenComplete(GMPErr aStatus);
+ void ReadComplete(GMPErr aStatus, const uint8_t* aBytes, uint32_t aLength);
+ void WriteComplete(GMPErr aStatus);
+
+ private:
+ ~GMPRecordImpl() = default;
+ const nsCString mName;
+ GMPRecordClient* const mClient;
+ GMPStorageChild* const mOwner;
+};
+
+class GMPStorageChild : public PGMPStorageChild {
+ friend class PGMPStorageChild;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GMPStorageChild)
+
+ explicit GMPStorageChild(GMPChild* aPlugin);
+
+ GMPErr CreateRecord(const nsCString& aRecordName, GMPRecord** aOutRecord,
+ GMPRecordClient* aClient);
+
+ GMPErr Open(GMPRecordImpl* aRecord);
+
+ GMPErr Read(GMPRecordImpl* aRecord);
+
+ GMPErr Write(GMPRecordImpl* aRecord, const uint8_t* aData,
+ uint32_t aDataSize);
+
+ GMPErr Close(const nsCString& aRecordName);
+
+ private:
+ bool HasRecord(const nsCString& aRecordName);
+ already_AddRefed<GMPRecordImpl> GetRecord(const nsCString& aRecordName);
+
+ protected:
+ ~GMPStorageChild() = default;
+
+ // PGMPStorageChild
+ mozilla::ipc::IPCResult RecvOpenComplete(const nsCString& aRecordName,
+ const GMPErr& aStatus);
+ mozilla::ipc::IPCResult RecvReadComplete(const nsCString& aRecordName,
+ const GMPErr& aStatus,
+ nsTArray<uint8_t>&& aBytes);
+ mozilla::ipc::IPCResult RecvWriteComplete(const nsCString& aRecordName,
+ const GMPErr& aStatus);
+ mozilla::ipc::IPCResult RecvShutdown();
+
+ private:
+ Monitor mMonitor MOZ_UNANNOTATED;
+ nsRefPtrHashtable<nsCStringHashKey, GMPRecordImpl> mRecords;
+ GMPChild* mPlugin;
+ bool mShutdown;
+};
+
+} // namespace mozilla::gmp
+
+#endif // GMPStorageChild_h_
diff --git a/dom/media/gmp/GMPStorageParent.cpp b/dom/media/gmp/GMPStorageParent.cpp
new file mode 100644
index 0000000000..d153f5cace
--- /dev/null
+++ b/dom/media/gmp/GMPStorageParent.cpp
@@ -0,0 +1,194 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GMPStorageParent.h"
+#include "GMPParent.h"
+#include "gmp-storage.h"
+#include "mozilla/Unused.h"
+
+namespace mozilla {
+
+#ifdef LOG
+# undef LOG
+#endif
+
+extern LogModule* GetGMPLog();
+
+#define LOGD(msg) MOZ_LOG(GetGMPLog(), mozilla::LogLevel::Debug, msg)
+#define LOG(level, msg) MOZ_LOG(GetGMPLog(), (level), msg)
+
+namespace gmp {
+
+GMPStorageParent::GMPStorageParent(const nsACString& aNodeId,
+ GMPParent* aPlugin)
+ : mNodeId(aNodeId), mPlugin(aPlugin), mShutdown(true) {}
+
+nsresult GMPStorageParent::Init() {
+ LOGD(("GMPStorageParent[%p]::Init()", this));
+
+ if (NS_WARN_IF(mNodeId.IsEmpty())) {
+ return NS_ERROR_FAILURE;
+ }
+ RefPtr<GeckoMediaPluginServiceParent> mps(
+ GeckoMediaPluginServiceParent::GetSingleton());
+ if (NS_WARN_IF(!mps)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ bool persistent = false;
+ if (NS_WARN_IF(
+ NS_FAILED(mps->IsPersistentStorageAllowed(mNodeId, &persistent)))) {
+ return NS_ERROR_FAILURE;
+ }
+ if (persistent) {
+ mStorage = CreateGMPDiskStorage(mNodeId, mPlugin->GetPluginBaseName());
+ } else {
+ mStorage = mps->GetMemoryStorageFor(mNodeId);
+ }
+ if (!mStorage) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mShutdown = false;
+ return NS_OK;
+}
+
+mozilla::ipc::IPCResult GMPStorageParent::RecvOpen(
+ const nsACString& aRecordName) {
+ LOGD(("GMPStorageParent[%p]::RecvOpen(record='%s')", this,
+ PromiseFlatCString(aRecordName).get()));
+
+ if (mShutdown) {
+ // Shutdown is an expected state, so we do not IPC_FAIL.
+ return IPC_OK();
+ }
+
+ if (mNodeId.EqualsLiteral("null")) {
+ // Refuse to open storage if the page is opened from local disk,
+ // or shared across origin.
+ LOGD(("GMPStorageParent[%p]::RecvOpen(record='%s') failed; null nodeId",
+ this, PromiseFlatCString(aRecordName).get()));
+ Unused << SendOpenComplete(aRecordName, GMPGenericErr);
+ return IPC_OK();
+ }
+
+ if (aRecordName.IsEmpty()) {
+ LOGD((
+ "GMPStorageParent[%p]::RecvOpen(record='%s') failed; record name empty",
+ this, PromiseFlatCString(aRecordName).get()));
+ Unused << SendOpenComplete(aRecordName, GMPGenericErr);
+ return IPC_OK();
+ }
+
+ if (mStorage->IsOpen(aRecordName)) {
+ LOGD(("GMPStorageParent[%p]::RecvOpen(record='%s') failed; record in use",
+ this, PromiseFlatCString(aRecordName).get()));
+ Unused << SendOpenComplete(aRecordName, GMPRecordInUse);
+ return IPC_OK();
+ }
+
+ auto err = mStorage->Open(aRecordName);
+ MOZ_ASSERT(GMP_FAILED(err) || mStorage->IsOpen(aRecordName));
+ LOGD(("GMPStorageParent[%p]::RecvOpen(record='%s') complete; rv=%d", this,
+ PromiseFlatCString(aRecordName).get(), err));
+ Unused << SendOpenComplete(aRecordName, err);
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPStorageParent::RecvRead(
+ const nsACString& aRecordName) {
+ LOGD(("GMPStorageParent[%p]::RecvRead(record='%s')", this,
+ PromiseFlatCString(aRecordName).get()));
+
+ if (mShutdown) {
+ // Shutdown is an expected state, so we do not IPC_FAIL.
+ return IPC_OK();
+ }
+
+ nsTArray<uint8_t> data;
+ if (!mStorage->IsOpen(aRecordName)) {
+ LOGD(("GMPStorageParent[%p]::RecvRead(record='%s') failed; record not open",
+ this, PromiseFlatCString(aRecordName).get()));
+ Unused << SendReadComplete(aRecordName, GMPClosedErr, data);
+ } else {
+ GMPErr rv = mStorage->Read(aRecordName, data);
+ LOGD(
+ ("GMPStorageParent[%p]::RecvRead(record='%s') read %zu bytes "
+ "rv=%" PRIu32,
+ this, PromiseFlatCString(aRecordName).get(), data.Length(),
+ static_cast<uint32_t>(rv)));
+ Unused << SendReadComplete(aRecordName, rv, data);
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPStorageParent::RecvWrite(
+ const nsACString& aRecordName, nsTArray<uint8_t>&& aBytes) {
+ LOGD(("GMPStorageParent[%p]::RecvWrite(record='%s') %zu bytes", this,
+ PromiseFlatCString(aRecordName).get(), aBytes.Length()));
+
+ if (mShutdown) {
+ // Shutdown is an expected state, so we do not IPC_FAIL.
+ return IPC_OK();
+ }
+
+ if (!mStorage->IsOpen(aRecordName)) {
+ LOGD(("GMPStorageParent[%p]::RecvWrite(record='%s') failed record not open",
+ this, PromiseFlatCString(aRecordName).get()));
+ Unused << SendWriteComplete(aRecordName, GMPClosedErr);
+ return IPC_OK();
+ }
+
+ if (aBytes.Length() > GMP_MAX_RECORD_SIZE) {
+ LOGD(("GMPStorageParent[%p]::RecvWrite(record='%s') failed record too big",
+ this, PromiseFlatCString(aRecordName).get()));
+ Unused << SendWriteComplete(aRecordName, GMPQuotaExceededErr);
+ return IPC_OK();
+ }
+
+ GMPErr rv = mStorage->Write(aRecordName, aBytes);
+ LOGD(("GMPStorageParent[%p]::RecvWrite(record='%s') write complete rv=%d",
+ this, PromiseFlatCString(aRecordName).get(), rv));
+
+ Unused << SendWriteComplete(aRecordName, rv);
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPStorageParent::RecvClose(
+ const nsACString& aRecordName) {
+ LOGD(("GMPStorageParent[%p]::RecvClose(record='%s')", this,
+ PromiseFlatCString(aRecordName).get()));
+
+ if (mShutdown) {
+ return IPC_OK();
+ }
+
+ mStorage->Close(aRecordName);
+
+ return IPC_OK();
+}
+
+void GMPStorageParent::ActorDestroy(ActorDestroyReason aWhy) {
+ LOGD(("GMPStorageParent[%p]::ActorDestroy(reason=%d)", this, aWhy));
+ Shutdown();
+}
+
+void GMPStorageParent::Shutdown() {
+ LOGD(("GMPStorageParent[%p]::Shutdown()", this));
+
+ if (mShutdown) {
+ return;
+ }
+ mShutdown = true;
+ Unused << SendShutdown();
+
+ mStorage = nullptr;
+}
+
+} // namespace gmp
+} // namespace mozilla
diff --git a/dom/media/gmp/GMPStorageParent.h b/dom/media/gmp/GMPStorageParent.h
new file mode 100644
index 0000000000..b0cbba14c1
--- /dev/null
+++ b/dom/media/gmp/GMPStorageParent.h
@@ -0,0 +1,47 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GMPStorageParent_h_
+#define GMPStorageParent_h_
+
+#include "mozilla/gmp/PGMPStorageParent.h"
+#include "GMPStorage.h"
+
+namespace mozilla::gmp {
+
+class GMPParent;
+
+class GMPStorageParent : public PGMPStorageParent {
+ friend class PGMPStorageParent;
+
+ public:
+ NS_INLINE_DECL_REFCOUNTING(GMPStorageParent)
+ GMPStorageParent(const nsACString& aNodeId, GMPParent* aPlugin);
+
+ nsresult Init();
+ void Shutdown();
+
+ protected:
+ mozilla::ipc::IPCResult RecvOpen(const nsACString& aRecordName) override;
+ mozilla::ipc::IPCResult RecvRead(const nsACString& aRecordName) override;
+ mozilla::ipc::IPCResult RecvWrite(const nsACString& aRecordName,
+ nsTArray<uint8_t>&& aBytes) override;
+ mozilla::ipc::IPCResult RecvClose(const nsACString& aRecordName) override;
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ private:
+ ~GMPStorageParent() = default;
+
+ RefPtr<GMPStorage> mStorage;
+
+ const nsCString mNodeId;
+ RefPtr<GMPParent> mPlugin;
+ // True after Shutdown(), or if Init() has not completed successfully.
+ bool mShutdown;
+};
+
+} // namespace mozilla::gmp
+
+#endif // GMPStorageParent_h_
diff --git a/dom/media/gmp/GMPTimerChild.cpp b/dom/media/gmp/GMPTimerChild.cpp
new file mode 100644
index 0000000000..8858f5f87f
--- /dev/null
+++ b/dom/media/gmp/GMPTimerChild.cpp
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GMPTimerChild.h"
+#include "GMPPlatform.h"
+#include "GMPChild.h"
+
+#define MAX_NUM_TIMERS 1000
+
+namespace mozilla::gmp {
+
+GMPTimerChild::GMPTimerChild(GMPChild* aPlugin)
+ : mTimerCount(1), mPlugin(aPlugin) {
+ MOZ_ASSERT(mPlugin->GMPMessageLoop() == MessageLoop::current());
+}
+
+GMPTimerChild::~GMPTimerChild() {
+ MOZ_ASSERT(mPlugin->GMPMessageLoop() == MessageLoop::current());
+}
+
+GMPErr GMPTimerChild::SetTimer(GMPTask* aTask, int64_t aTimeoutMS) {
+ if (!aTask) {
+ NS_WARNING("Tried to set timer with null task!");
+ return GMPGenericErr;
+ }
+
+ if (mPlugin->GMPMessageLoop() != MessageLoop::current()) {
+ NS_WARNING("Tried to set GMP timer on non-main thread.");
+ return GMPGenericErr;
+ }
+
+ if (mTimers.Count() > MAX_NUM_TIMERS) {
+ return GMPQuotaExceededErr;
+ }
+ uint32_t timerId = mTimerCount;
+ mTimers.InsertOrUpdate(timerId, aTask);
+ mTimerCount++;
+
+ if (!SendSetTimer(timerId, aTimeoutMS)) {
+ return GMPGenericErr;
+ }
+ return GMPNoErr;
+}
+
+mozilla::ipc::IPCResult GMPTimerChild::RecvTimerExpired(
+ const uint32_t& aTimerId) {
+ MOZ_ASSERT(mPlugin->GMPMessageLoop() == MessageLoop::current());
+
+ GMPTask* task = mTimers.Get(aTimerId);
+ mTimers.Remove(aTimerId);
+ if (task) {
+ RunOnMainThread(task);
+ }
+ return IPC_OK();
+}
+
+} // namespace mozilla::gmp
diff --git a/dom/media/gmp/GMPTimerChild.h b/dom/media/gmp/GMPTimerChild.h
new file mode 100644
index 0000000000..f81f796e12
--- /dev/null
+++ b/dom/media/gmp/GMPTimerChild.h
@@ -0,0 +1,45 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GMPTimerChild_h_
+#define GMPTimerChild_h_
+
+#include "mozilla/gmp/PGMPTimerChild.h"
+#include "mozilla/Monitor.h"
+#include "nsTHashMap.h"
+#include "nsHashKeys.h"
+#include "gmp-errors.h"
+#include "gmp-platform.h"
+
+namespace mozilla::gmp {
+
+class GMPChild;
+
+class GMPTimerChild : public PGMPTimerChild {
+ friend class PGMPTimerChild;
+
+ public:
+ NS_INLINE_DECL_REFCOUNTING(GMPTimerChild)
+
+ explicit GMPTimerChild(GMPChild* aPlugin);
+
+ GMPErr SetTimer(GMPTask* aTask, int64_t aTimeoutMS);
+
+ protected:
+ // GMPTimerChild
+ mozilla::ipc::IPCResult RecvTimerExpired(const uint32_t& aTimerId);
+
+ private:
+ ~GMPTimerChild();
+
+ nsTHashMap<nsUint32HashKey, GMPTask*> mTimers;
+ uint32_t mTimerCount;
+
+ GMPChild* mPlugin;
+};
+
+} // namespace mozilla::gmp
+
+#endif // GMPTimerChild_h_
diff --git a/dom/media/gmp/GMPTimerParent.cpp b/dom/media/gmp/GMPTimerParent.cpp
new file mode 100644
index 0000000000..31b17ef214
--- /dev/null
+++ b/dom/media/gmp/GMPTimerParent.cpp
@@ -0,0 +1,105 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GMPTimerParent.h"
+
+#include "GMPLog.h"
+#include "mozilla/Unused.h"
+#include "nsComponentManagerUtils.h"
+
+namespace mozilla {
+
+extern LogModule* GetGMPLog();
+
+#ifdef __CLASS__
+# undef __CLASS__
+#endif
+#define __CLASS__ "GMPTimerParent"
+
+namespace gmp {
+
+GMPTimerParent::GMPTimerParent(nsISerialEventTarget* aGMPEventTarget)
+ : mGMPEventTarget(aGMPEventTarget), mIsOpen(true) {}
+
+mozilla::ipc::IPCResult GMPTimerParent::RecvSetTimer(
+ const uint32_t& aTimerId, const uint32_t& aTimeoutMs) {
+ GMP_LOG_DEBUG("%s::%s: %p mIsOpen=%d", __CLASS__, __FUNCTION__, this,
+ mIsOpen);
+
+ MOZ_ASSERT(mGMPEventTarget->IsOnCurrentThread());
+
+ if (!mIsOpen) {
+ return IPC_OK();
+ }
+
+ nsresult rv;
+ UniquePtr<Context> ctx(new Context());
+
+ rv = NS_NewTimerWithFuncCallback(
+ getter_AddRefs(ctx->mTimer), &GMPTimerParent::GMPTimerExpired, ctx.get(),
+ aTimeoutMs, nsITimer::TYPE_ONE_SHOT, "gmp::GMPTimerParent::RecvSetTimer",
+ mGMPEventTarget);
+ NS_ENSURE_SUCCESS(rv, IPC_OK());
+
+ ctx->mId = aTimerId;
+ ctx->mParent = this;
+
+ mTimers.Insert(ctx.release());
+
+ return IPC_OK();
+}
+
+void GMPTimerParent::Shutdown() {
+ GMP_LOG_DEBUG("%s::%s: %p mIsOpen=%d", __CLASS__, __FUNCTION__, this,
+ mIsOpen);
+
+ MOZ_ASSERT(mGMPEventTarget->IsOnCurrentThread());
+
+ for (Context* context : mTimers) {
+ context->mTimer->Cancel();
+ delete context;
+ }
+
+ mTimers.Clear();
+ mIsOpen = false;
+}
+
+void GMPTimerParent::ActorDestroy(ActorDestroyReason aWhy) {
+ GMP_LOG_DEBUG("%s::%s: %p mIsOpen=%d", __CLASS__, __FUNCTION__, this,
+ mIsOpen);
+
+ Shutdown();
+}
+
+/* static */
+void GMPTimerParent::GMPTimerExpired(nsITimer* aTimer, void* aClosure) {
+ MOZ_ASSERT(aClosure);
+ UniquePtr<Context> ctx(static_cast<Context*>(aClosure));
+ MOZ_ASSERT(ctx->mParent);
+ if (ctx->mParent) {
+ ctx->mParent->TimerExpired(ctx.get());
+ }
+}
+
+void GMPTimerParent::TimerExpired(Context* aContext) {
+ GMP_LOG_DEBUG("%s::%s: %p mIsOpen=%d", __CLASS__, __FUNCTION__, this,
+ mIsOpen);
+ MOZ_ASSERT(mGMPEventTarget->IsOnCurrentThread());
+
+ if (!mIsOpen) {
+ return;
+ }
+
+ uint32_t id = aContext->mId;
+ mTimers.Remove(aContext);
+ if (id) {
+ Unused << SendTimerExpired(id);
+ }
+}
+
+} // namespace gmp
+} // namespace mozilla
+
+#undef __CLASS__
diff --git a/dom/media/gmp/GMPTimerParent.h b/dom/media/gmp/GMPTimerParent.h
new file mode 100644
index 0000000000..dae1d9a1fb
--- /dev/null
+++ b/dom/media/gmp/GMPTimerParent.h
@@ -0,0 +1,56 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GMPTimerParent_h_
+#define GMPTimerParent_h_
+
+#include "mozilla/gmp/PGMPTimerParent.h"
+#include "nsITimer.h"
+#include "nsCOMPtr.h"
+#include "nsTHashSet.h"
+#include "mozilla/Monitor.h"
+
+namespace mozilla::gmp {
+
+class GMPTimerParent : public PGMPTimerParent {
+ friend class PGMPTimerParent;
+
+ public:
+ NS_INLINE_DECL_REFCOUNTING(GMPTimerParent)
+ explicit GMPTimerParent(nsISerialEventTarget* aGMPEventTarget);
+
+ void Shutdown();
+
+ protected:
+ mozilla::ipc::IPCResult RecvSetTimer(const uint32_t& aTimerId,
+ const uint32_t& aTimeoutMs);
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ private:
+ ~GMPTimerParent() = default;
+
+ static void GMPTimerExpired(nsITimer* aTimer, void* aClosure);
+
+ struct Context {
+ Context() : mId(0) { MOZ_COUNT_CTOR(Context); }
+ MOZ_COUNTED_DTOR(Context)
+ nsCOMPtr<nsITimer> mTimer;
+ RefPtr<GMPTimerParent>
+ mParent; // Note: live timers keep the GMPTimerParent alive.
+ uint32_t mId;
+ };
+
+ void TimerExpired(Context* aContext);
+
+ nsTHashSet<Context*> mTimers;
+
+ nsCOMPtr<nsISerialEventTarget> mGMPEventTarget;
+
+ bool mIsOpen;
+};
+
+} // namespace mozilla::gmp
+
+#endif // GMPTimerParent_h_
diff --git a/dom/media/gmp/GMPTypes.ipdlh b/dom/media/gmp/GMPTypes.ipdlh
new file mode 100644
index 0000000000..6cb37b6068
--- /dev/null
+++ b/dom/media/gmp/GMPTypes.ipdlh
@@ -0,0 +1,116 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include "GMPMessageUtils.h";
+
+using cdm::EncryptionScheme from "GMPSanitizedExports.h";
+using GMPBufferType from "gmp-video-codec.h";
+
+namespace mozilla {
+namespace gmp {
+
+// GMP processes are associated with a specific node ID, so all GMP requests
+// which have the same node ID will use the same GMP process. Depending on the
+// use case, the node ID may be represented by a string (such as when used for
+// WebRTC) or by this structure, which is populated according to the origin
+// initiating the request. This structure will eventually be converted to a
+// string representing a node ID. For this structure, the process ensures the
+// strings are unique for the combination of origin, top level origin and GMP
+// name.
+struct NodeIdParts {
+ nsString mOrigin;
+ nsString mTopLevelOrigin;
+ nsString mGMPName;
+};
+
+// A NodeIdVariant should contain either
+// - A string representing an already computed node ID.
+// - A NodeIdParts representing a node ID that still needs to be computed by
+// processing those parts.
+// This union is used to simplify passing of node ID information. Some
+// GMP use cases can hard code their node ID, while others need to compute
+// the node ID later. This lets us avoid having overloads to handle
+// the two different paths.
+union NodeIdVariant {
+ nsCString;
+ NodeIdParts;
+};
+
+struct GMPVideoEncodedFrameData {
+ uint32_t mEncodedWidth;
+ uint32_t mEncodedHeight;
+ uint64_t mTimestamp; // microseconds
+ uint64_t mDuration; // microseconds
+ uint32_t mFrameType;
+ uint32_t mSize;
+ GMPBufferType mBufferType;
+ Shmem mBuffer;
+ bool mCompleteFrame;
+};
+
+struct GMPPlaneData {
+ int32_t mSize;
+ int32_t mStride;
+ Shmem mBuffer;
+};
+
+struct GMPVideoi420FrameData {
+ GMPPlaneData mYPlane;
+ GMPPlaneData mUPlane;
+ GMPPlaneData mVPlane;
+ int32_t mWidth;
+ int32_t mHeight;
+ uint64_t mTimestamp; // microseconds
+ uint64_t? mUpdatedTimestamp; // microseconds
+ uint64_t mDuration; // microseconds
+};
+
+struct CDMInputBuffer {
+ Shmem mData;
+ uint8_t[] mKeyId;
+ uint8_t[] mIV;
+ int64_t mTimestamp;
+ int64_t mDuration;
+ uint32_t[] mClearBytes;
+ uint32_t[] mCipherBytes;
+ uint8_t mCryptByteBlock;
+ uint8_t mSkipByteBlock;
+ EncryptionScheme mEncryptionScheme;
+};
+
+struct CDMVideoDecoderConfig {
+ uint32_t mCodec;
+ uint32_t mProfile;
+ uint32_t mFormat;
+ int32_t mImageWidth;
+ int32_t mImageHeight;
+ uint8_t[] mExtraData;
+ EncryptionScheme mEncryptionScheme;
+};
+
+struct CDMKeyInformation {
+ uint8_t[] mKeyId;
+ uint32_t mStatus;
+ uint32_t mSystemCode;
+};
+
+struct CDMVideoPlane {
+ uint32_t mPlaneOffset;
+ uint32_t mStride;
+};
+
+struct CDMVideoFrame {
+ uint32_t mFormat;
+ int32_t mImageWidth;
+ int32_t mImageHeight;
+ CDMVideoPlane mYPlane;
+ CDMVideoPlane mUPlane;
+ CDMVideoPlane mVPlane;
+ int64_t mTimestamp;
+ int64_t mDuration;
+};
+
+} // namespace gmp
+} // namespace mozilla
diff --git a/dom/media/gmp/GMPUtils.cpp b/dom/media/gmp/GMPUtils.cpp
new file mode 100644
index 0000000000..1bc02d7027
--- /dev/null
+++ b/dom/media/gmp/GMPUtils.cpp
@@ -0,0 +1,228 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GMPUtils.h"
+
+#include "GMPService.h"
+#include "VideoLimits.h"
+#include "mozIGeckoMediaPluginService.h"
+#include "mozilla/Base64.h"
+#include "nsCOMPtr.h"
+#include "nsCRTGlue.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsIConsoleService.h"
+#include "nsIFile.h"
+#include "nsLiteralString.h"
+#include "prio.h"
+
+namespace mozilla {
+
+void SplitAt(const char* aDelims, const nsACString& aInput,
+ nsTArray<nsCString>& aOutTokens) {
+ nsAutoCString str(aInput);
+ char* end = str.BeginWriting();
+ const char* start = nullptr;
+ while (!!(start = NS_strtok(aDelims, &end))) {
+ aOutTokens.AppendElement(nsCString(start));
+ }
+}
+
+nsCString ToHexString(const uint8_t* aBytes, uint32_t aLength) {
+ static const char hex[] = {'0', '1', '2', '3', '4', '5', '6', '7',
+ '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
+ nsCString str;
+ for (uint32_t i = 0; i < aLength; i++) {
+ char buf[3];
+ buf[0] = hex[(aBytes[i] & 0xf0) >> 4];
+ buf[1] = hex[aBytes[i] & 0x0f];
+ buf[2] = 0;
+ str.AppendASCII(buf);
+ }
+ return str;
+}
+
+nsCString ToHexString(const nsTArray<uint8_t>& aBytes) {
+ return ToHexString(aBytes.Elements(), aBytes.Length());
+}
+
+bool FileExists(nsIFile* aFile) {
+ bool exists = false;
+ return aFile && NS_SUCCEEDED(aFile->Exists(&exists)) && exists;
+}
+
+DirectoryEnumerator::DirectoryEnumerator(nsIFile* aPath, Mode aMode)
+ : mMode(aMode) {
+ aPath->GetDirectoryEntries(getter_AddRefs(mIter));
+}
+
+already_AddRefed<nsIFile> DirectoryEnumerator::Next() {
+ if (!mIter) {
+ return nullptr;
+ }
+ bool hasMore = false;
+ while (NS_SUCCEEDED(mIter->HasMoreElements(&hasMore)) && hasMore) {
+ nsCOMPtr<nsISupports> supports;
+ nsresult rv = mIter->GetNext(getter_AddRefs(supports));
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+
+ nsCOMPtr<nsIFile> path(do_QueryInterface(supports, &rv));
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+
+ if (mMode == DirsOnly) {
+ bool isDirectory = false;
+ rv = path->IsDirectory(&isDirectory);
+ if (NS_FAILED(rv) || !isDirectory) {
+ continue;
+ }
+ }
+ return path.forget();
+ }
+ return nullptr;
+}
+
+bool ReadIntoArray(nsIFile* aFile, nsTArray<uint8_t>& aOutDst,
+ size_t aMaxLength) {
+ if (!FileExists(aFile)) {
+ return false;
+ }
+
+ PRFileDesc* fd = nullptr;
+ nsresult rv = aFile->OpenNSPRFileDesc(PR_RDONLY, 0, &fd);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ int32_t length = PR_Seek(fd, 0, PR_SEEK_END);
+ PR_Seek(fd, 0, PR_SEEK_SET);
+
+ if (length < 0 || (size_t)length > aMaxLength) {
+ NS_WARNING("EME file is longer than maximum allowed length");
+ PR_Close(fd);
+ return false;
+ }
+ aOutDst.SetLength(length);
+ int32_t bytesRead = PR_Read(fd, aOutDst.Elements(), length);
+ PR_Close(fd);
+ return (bytesRead == length);
+}
+
+bool ReadIntoString(nsIFile* aFile, nsCString& aOutDst, size_t aMaxLength) {
+ nsTArray<uint8_t> buf;
+ bool rv = ReadIntoArray(aFile, buf, aMaxLength);
+ if (rv) {
+ buf.AppendElement(0); // Append null terminator, required by nsC*String.
+ aOutDst = nsDependentCString((const char*)buf.Elements(), buf.Length() - 1);
+ }
+ return rv;
+}
+
+bool GMPInfoFileParser::Init(nsIFile* aInfoFile) {
+ nsTArray<nsCString> lines;
+ static const size_t MAX_GMP_INFO_FILE_LENGTH = 5 * 1024;
+
+ nsAutoCString info;
+ if (!ReadIntoString(aInfoFile, info, MAX_GMP_INFO_FILE_LENGTH)) {
+ NS_WARNING("Failed to read info file in GMP process.");
+ return false;
+ }
+
+ // Note: we pass "\r\n" to SplitAt so that we'll split lines delimited
+ // by \n (Unix), \r\n (Windows) and \r (old MacOSX).
+ SplitAt("\r\n", info, lines);
+
+ for (nsCString line : lines) {
+ // Field name is the string up to but not including the first ':'
+ // character on the line.
+ int32_t colon = line.FindChar(':');
+ if (colon <= 0) {
+ // Not allowed to be the first character.
+ // Info field name must be at least one character.
+ continue;
+ }
+ nsAutoCString key(Substring(line, 0, colon));
+ ToLowerCase(key);
+ key.Trim(" ");
+
+ auto value = MakeUnique<nsCString>(Substring(line, colon + 1));
+ value->Trim(" ");
+ mValues.InsertOrUpdate(
+ key,
+ std::move(value)); // Hashtable assumes ownership of value.
+ }
+
+ return true;
+}
+
+bool GMPInfoFileParser::Contains(const nsCString& aKey) const {
+ nsCString key(aKey);
+ ToLowerCase(key);
+ return mValues.Contains(key);
+}
+
+nsCString GMPInfoFileParser::Get(const nsCString& aKey) const {
+ MOZ_ASSERT(Contains(aKey));
+ nsCString key(aKey);
+ ToLowerCase(key);
+ nsCString* p = nullptr;
+ if (mValues.Get(key, &p)) {
+ return nsCString(*p);
+ }
+ return ""_ns;
+}
+
+bool HaveGMPFor(const nsACString& aAPI, const nsTArray<nsCString>& aTags) {
+ nsCOMPtr<mozIGeckoMediaPluginService> mps =
+ do_GetService("@mozilla.org/gecko-media-plugin-service;1");
+ if (NS_WARN_IF(!mps)) {
+ return false;
+ }
+
+ bool hasPlugin = false;
+ if (NS_FAILED(mps->HasPluginForAPI(aAPI, aTags, &hasPlugin))) {
+ return false;
+ }
+ return hasPlugin;
+}
+
+void LogToConsole(const nsAString& aMsg) {
+ nsCOMPtr<nsIConsoleService> console(
+ do_GetService("@mozilla.org/consoleservice;1"));
+ if (!console) {
+ NS_WARNING("Failed to log message to console.");
+ return;
+ }
+ nsAutoString msg(aMsg);
+ console->LogStringMessage(msg.get());
+}
+
+already_AddRefed<nsISerialEventTarget> GetGMPThread() {
+ RefPtr<gmp::GeckoMediaPluginService> service =
+ gmp::GeckoMediaPluginService::GetGeckoMediaPluginService();
+ nsCOMPtr<nsISerialEventTarget> thread =
+ service ? service->GetGMPThread() : nullptr;
+ return thread.forget();
+}
+
+static size_t Align16(size_t aNumber) {
+ const size_t mask = 15; // Alignment - 1.
+ return (aNumber + mask) & ~mask;
+}
+
+size_t I420FrameBufferSizePadded(int32_t aWidth, int32_t aHeight) {
+ if (aWidth <= 0 || aHeight <= 0 || aWidth > MAX_VIDEO_WIDTH ||
+ aHeight > MAX_VIDEO_HEIGHT) {
+ return 0;
+ }
+
+ size_t ySize = Align16(aWidth) * Align16(aHeight);
+ return ySize + (ySize / 4) * 2;
+}
+
+} // namespace mozilla
diff --git a/dom/media/gmp/GMPUtils.h b/dom/media/gmp/GMPUtils.h
new file mode 100644
index 0000000000..0ac097420c
--- /dev/null
+++ b/dom/media/gmp/GMPUtils.h
@@ -0,0 +1,83 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GMPUtils_h_
+#define GMPUtils_h_
+
+#include "mozilla/AbstractThread.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "nsCOMPtr.h"
+#include "nsClassHashtable.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+
+#define CHROMIUM_CDM_API_BACKWARD_COMPAT "chromium-cdm9-host4"
+#define CHROMIUM_CDM_API "chromium-cdm10-host4"
+
+class nsIFile;
+class nsIDirectoryEnumerator;
+
+namespace mozilla {
+
+template <typename T>
+struct DestroyPolicy {
+ void operator()(T* aGMPObject) const { aGMPObject->Destroy(); }
+};
+
+template <typename T>
+using GMPUniquePtr = mozilla::UniquePtr<T, DestroyPolicy<T>>;
+
+void SplitAt(const char* aDelims, const nsACString& aInput,
+ nsTArray<nsCString>& aOutTokens);
+
+nsCString ToHexString(const nsTArray<uint8_t>& aBytes);
+
+nsCString ToHexString(const uint8_t* aBytes, uint32_t aLength);
+
+bool FileExists(nsIFile* aFile);
+
+// Enumerate directory entries for a specified path.
+class DirectoryEnumerator {
+ public:
+ enum Mode {
+ DirsOnly, // Enumeration only includes directories.
+ FilesAndDirs // Enumeration includes directories and non-directory files.
+ };
+
+ DirectoryEnumerator(nsIFile* aPath, Mode aMode);
+
+ already_AddRefed<nsIFile> Next();
+
+ private:
+ Mode mMode;
+ nsCOMPtr<nsIDirectoryEnumerator> mIter;
+};
+
+class GMPInfoFileParser {
+ public:
+ bool Init(nsIFile* aFile);
+ bool Contains(const nsCString& aKey) const;
+ nsCString Get(const nsCString& aKey) const;
+
+ private:
+ nsClassHashtable<nsCStringHashKey, nsCString> mValues;
+};
+
+bool ReadIntoString(nsIFile* aFile, nsCString& aOutDst, size_t aMaxLength);
+
+bool HaveGMPFor(const nsACString& aAPI, const nsTArray<nsCString>& aTags);
+
+void LogToConsole(const nsAString& aMsg);
+
+already_AddRefed<nsISerialEventTarget> GetGMPThread();
+
+// Returns the number of bytes required to store an aWidth x aHeight image in
+// I420 format, padded so that the width and height are multiples of 16.
+size_t I420FrameBufferSizePadded(int32_t aWidth, int32_t aHeight);
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/gmp/GMPVideoDecoderChild.cpp b/dom/media/gmp/GMPVideoDecoderChild.cpp
new file mode 100644
index 0000000000..0f605cca9b
--- /dev/null
+++ b/dom/media/gmp/GMPVideoDecoderChild.cpp
@@ -0,0 +1,210 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GMPVideoDecoderChild.h"
+#include "GMPVideoi420FrameImpl.h"
+#include "GMPContentChild.h"
+#include <stdio.h>
+#include "mozilla/Unused.h"
+#include "GMPVideoEncodedFrameImpl.h"
+#include "runnable_utils.h"
+
+namespace mozilla::gmp {
+
+GMPVideoDecoderChild::GMPVideoDecoderChild(GMPContentChild* aPlugin)
+ : GMPSharedMemManager(aPlugin),
+ mPlugin(aPlugin),
+ mVideoDecoder(nullptr),
+ mVideoHost(this),
+ mNeedShmemIntrCount(0),
+ mPendingDecodeComplete(false) {
+ MOZ_ASSERT(mPlugin);
+}
+
+GMPVideoDecoderChild::~GMPVideoDecoderChild() {
+ MOZ_ASSERT(!mNeedShmemIntrCount);
+}
+
+void GMPVideoDecoderChild::Init(GMPVideoDecoder* aDecoder) {
+ MOZ_ASSERT(aDecoder,
+ "Cannot initialize video decoder child without a video decoder!");
+ mVideoDecoder = aDecoder;
+}
+
+GMPVideoHostImpl& GMPVideoDecoderChild::Host() { return mVideoHost; }
+
+void GMPVideoDecoderChild::Decoded(GMPVideoi420Frame* aDecodedFrame) {
+ MOZ_ASSERT(mPlugin->GMPMessageLoop() == MessageLoop::current());
+
+ if (!aDecodedFrame) {
+ MOZ_CRASH("Not given a decoded frame!");
+ }
+
+ auto df = static_cast<GMPVideoi420FrameImpl*>(aDecodedFrame);
+
+ GMPVideoi420FrameData frameData;
+ df->InitFrameData(frameData);
+ SendDecoded(frameData);
+
+ aDecodedFrame->Destroy();
+}
+
+void GMPVideoDecoderChild::ReceivedDecodedReferenceFrame(
+ const uint64_t aPictureId) {
+ MOZ_ASSERT(mPlugin->GMPMessageLoop() == MessageLoop::current());
+
+ SendReceivedDecodedReferenceFrame(aPictureId);
+}
+
+void GMPVideoDecoderChild::ReceivedDecodedFrame(const uint64_t aPictureId) {
+ MOZ_ASSERT(mPlugin->GMPMessageLoop() == MessageLoop::current());
+
+ SendReceivedDecodedFrame(aPictureId);
+}
+
+void GMPVideoDecoderChild::InputDataExhausted() {
+ MOZ_ASSERT(mPlugin->GMPMessageLoop() == MessageLoop::current());
+
+ SendInputDataExhausted();
+}
+
+void GMPVideoDecoderChild::DrainComplete() {
+ MOZ_ASSERT(mPlugin->GMPMessageLoop() == MessageLoop::current());
+
+ SendDrainComplete();
+}
+
+void GMPVideoDecoderChild::ResetComplete() {
+ MOZ_ASSERT(mPlugin->GMPMessageLoop() == MessageLoop::current());
+
+ SendResetComplete();
+}
+
+void GMPVideoDecoderChild::Error(GMPErr aError) {
+ MOZ_ASSERT(mPlugin->GMPMessageLoop() == MessageLoop::current());
+
+ SendError(aError);
+}
+
+mozilla::ipc::IPCResult GMPVideoDecoderChild::RecvInitDecode(
+ const GMPVideoCodec& aCodecSettings, nsTArray<uint8_t>&& aCodecSpecific,
+ const int32_t& aCoreCount) {
+ if (!mVideoDecoder) {
+ return IPC_FAIL(this, "!mVideoDecoder");
+ }
+
+ // Ignore any return code. It is OK for this to fail without killing the
+ // process.
+ mVideoDecoder->InitDecode(aCodecSettings, aCodecSpecific.Elements(),
+ aCodecSpecific.Length(), this, aCoreCount);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPVideoDecoderChild::RecvDecode(
+ const GMPVideoEncodedFrameData& aInputFrame, const bool& aMissingFrames,
+ nsTArray<uint8_t>&& aCodecSpecificInfo, const int64_t& aRenderTimeMs) {
+ if (!mVideoDecoder) {
+ return IPC_FAIL(this, "!mVideoDecoder");
+ }
+
+ auto f = new GMPVideoEncodedFrameImpl(aInputFrame, &mVideoHost);
+
+ // Ignore any return code. It is OK for this to fail without killing the
+ // process.
+ mVideoDecoder->Decode(f, aMissingFrames, aCodecSpecificInfo.Elements(),
+ aCodecSpecificInfo.Length(), aRenderTimeMs);
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPVideoDecoderChild::RecvChildShmemForPool(
+ Shmem&& aFrameBuffer) {
+ if (aFrameBuffer.IsWritable()) {
+ mVideoHost.SharedMemMgr()->MgrDeallocShmem(GMPSharedMem::kGMPFrameData,
+ aFrameBuffer);
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPVideoDecoderChild::RecvReset() {
+ if (!mVideoDecoder) {
+ return IPC_FAIL(this, "!mVideoDecoder");
+ }
+
+ // Ignore any return code. It is OK for this to fail without killing the
+ // process.
+ mVideoDecoder->Reset();
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPVideoDecoderChild::RecvDrain() {
+ if (!mVideoDecoder) {
+ return IPC_FAIL(this, "!mVideoDecoder");
+ }
+
+ // Ignore any return code. It is OK for this to fail without killing the
+ // process.
+ mVideoDecoder->Drain();
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPVideoDecoderChild::RecvDecodingComplete() {
+ MOZ_ASSERT(mPlugin->GMPMessageLoop() == MessageLoop::current());
+
+ if (mNeedShmemIntrCount) {
+ // There's a GMP blocked in Alloc() waiting for the CallNeedShem() to
+ // return a frame they can use. Don't call the GMP's DecodingComplete()
+ // now and don't delete the GMPVideoDecoderChild, defer processing the
+ // DecodingComplete() until once the Alloc() finishes.
+ mPendingDecodeComplete = true;
+ return IPC_OK();
+ }
+ if (mVideoDecoder) {
+ // Ignore any return code. It is OK for this to fail without killing the
+ // process.
+ mVideoDecoder->DecodingComplete();
+ mVideoDecoder = nullptr;
+ }
+
+ mVideoHost.DoneWithAPI();
+
+ mPlugin = nullptr;
+
+ Unused << Send__delete__(this);
+
+ return IPC_OK();
+}
+
+bool GMPVideoDecoderChild::Alloc(size_t aSize, Shmem* aMem) {
+ MOZ_ASSERT(mPlugin->GMPMessageLoop() == MessageLoop::current());
+
+ bool rv;
+#ifndef SHMEM_ALLOC_IN_CHILD
+ ++mNeedShmemIntrCount;
+ rv = SendNeedShmem(aSize, aMem);
+ --mNeedShmemIntrCount;
+ if (mPendingDecodeComplete && mNeedShmemIntrCount == 0) {
+ mPendingDecodeComplete = false;
+ mPlugin->GMPMessageLoop()->PostTask(
+ NewRunnableMethod("gmp::GMPVideoDecoderChild::RecvDecodingComplete",
+ this, &GMPVideoDecoderChild::RecvDecodingComplete));
+ }
+#else
+ rv = AllocShmem(aSize, aType, aMem);
+#endif
+ return rv;
+}
+
+void GMPVideoDecoderChild::Dealloc(Shmem&& aMem) {
+#ifndef SHMEM_ALLOC_IN_CHILD
+ SendParentShmemForPool(std::move(aMem));
+#else
+ DeallocShmem(aMem);
+#endif
+}
+
+} // namespace mozilla::gmp
diff --git a/dom/media/gmp/GMPVideoDecoderChild.h b/dom/media/gmp/GMPVideoDecoderChild.h
new file mode 100644
index 0000000000..3c74e5f02c
--- /dev/null
+++ b/dom/media/gmp/GMPVideoDecoderChild.h
@@ -0,0 +1,75 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GMPVideoDecoderChild_h_
+#define GMPVideoDecoderChild_h_
+
+#include "nsString.h"
+#include "mozilla/gmp/PGMPVideoDecoderChild.h"
+#include "gmp-video-decode.h"
+#include "GMPSharedMemManager.h"
+#include "GMPVideoHost.h"
+#include "mozilla/gmp/GMPTypes.h"
+
+namespace mozilla::gmp {
+
+class GMPContentChild;
+
+class GMPVideoDecoderChild : public PGMPVideoDecoderChild,
+ public GMPVideoDecoderCallback,
+ public GMPSharedMemManager {
+ friend class PGMPVideoDecoderChild;
+
+ public:
+ // Mark AddRef and Release as `final`, as they overload pure virtual
+ // implementations in PGMPVideoDecoderChild.
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GMPVideoDecoderChild, final);
+
+ explicit GMPVideoDecoderChild(GMPContentChild* aPlugin);
+
+ void Init(GMPVideoDecoder* aDecoder);
+ GMPVideoHostImpl& Host();
+
+ // GMPVideoDecoderCallback
+ void Decoded(GMPVideoi420Frame* decodedFrame) override;
+ void ReceivedDecodedReferenceFrame(const uint64_t pictureId) override;
+ void ReceivedDecodedFrame(const uint64_t pictureId) override;
+ void InputDataExhausted() override;
+ void DrainComplete() override;
+ void ResetComplete() override;
+ void Error(GMPErr aError) override;
+
+ // GMPSharedMemManager
+ bool Alloc(size_t aSize, Shmem* aMem) override;
+ void Dealloc(Shmem&& aMem) override;
+
+ private:
+ virtual ~GMPVideoDecoderChild();
+
+ // PGMPVideoDecoderChild
+ mozilla::ipc::IPCResult RecvInitDecode(const GMPVideoCodec& aCodecSettings,
+ nsTArray<uint8_t>&& aCodecSpecific,
+ const int32_t& aCoreCount);
+ mozilla::ipc::IPCResult RecvDecode(
+ const GMPVideoEncodedFrameData& aInputFrame, const bool& aMissingFrames,
+ nsTArray<uint8_t>&& aCodecSpecificInfo, const int64_t& aRenderTimeMs);
+ mozilla::ipc::IPCResult RecvChildShmemForPool(Shmem&& aFrameBuffer);
+ mozilla::ipc::IPCResult RecvReset();
+ mozilla::ipc::IPCResult RecvDrain();
+ mozilla::ipc::IPCResult RecvDecodingComplete();
+
+ GMPContentChild* mPlugin;
+ GMPVideoDecoder* mVideoDecoder;
+ GMPVideoHostImpl mVideoHost;
+
+ // Non-zero when a GMP is blocked spinning the IPC message loop while
+ // waiting on an NeedShmem to complete.
+ int mNeedShmemIntrCount;
+ bool mPendingDecodeComplete;
+};
+
+} // namespace mozilla::gmp
+
+#endif // GMPVideoDecoderChild_h_
diff --git a/dom/media/gmp/GMPVideoDecoderParent.cpp b/dom/media/gmp/GMPVideoDecoderParent.cpp
new file mode 100644
index 0000000000..dfe7182c41
--- /dev/null
+++ b/dom/media/gmp/GMPVideoDecoderParent.cpp
@@ -0,0 +1,461 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GMPVideoDecoderParent.h"
+
+#include "GMPContentParent.h"
+#include "GMPUtils.h"
+#include "GMPLog.h"
+#include "GMPMessageUtils.h"
+#include "GMPVideoEncodedFrameImpl.h"
+#include "GMPVideoi420FrameImpl.h"
+#include "mozilla/gmp/GMPTypes.h"
+#include "mozilla/Unused.h"
+#include "nsAutoRef.h"
+#include "nsPrintfCString.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla::gmp {
+
+// States:
+// Initial: mIsOpen == false
+// on InitDecode success -> Open
+// on Shutdown -> Dead
+// Open: mIsOpen == true
+// on Close -> Dead
+// on ActorDestroy -> Dead
+// on Shutdown -> Dead
+// Dead: mIsOpen == false
+
+GMPVideoDecoderParent::GMPVideoDecoderParent(GMPContentParent* aPlugin)
+ : GMPSharedMemManager(aPlugin),
+ mIsOpen(false),
+ mShuttingDown(false),
+ mActorDestroyed(false),
+ mIsAwaitingResetComplete(false),
+ mIsAwaitingDrainComplete(false),
+ mPlugin(aPlugin),
+ mCallback(nullptr),
+ mVideoHost(this),
+ mPluginId(aPlugin->GetPluginId()),
+ mPluginType(aPlugin->GetPluginType()),
+ mFrameCount(0) {
+ MOZ_ASSERT(mPlugin);
+}
+
+GMPVideoDecoderParent::~GMPVideoDecoderParent() = default;
+
+GMPVideoHostImpl& GMPVideoDecoderParent::Host() { return mVideoHost; }
+
+// Note: may be called via Terminated()
+void GMPVideoDecoderParent::Close() {
+ GMP_LOG_DEBUG("GMPVideoDecoderParent[%p]::Close()", this);
+ MOZ_ASSERT(!mPlugin || mPlugin->GMPEventTarget()->IsOnCurrentThread());
+
+ // Ensure if we've received a Close while waiting for a ResetComplete
+ // or DrainComplete notification, we'll unblock the caller before processing
+ // the close. This seems unlikely to happen, but better to be careful.
+ UnblockResetAndDrain();
+
+ // Consumer is done with us; we can shut down. No more callbacks should
+ // be made to mCallback. Note: do this before Shutdown()!
+ mCallback = nullptr;
+ // Let Shutdown mark us as dead so it knows if we had been alive
+
+ // In case this is the last reference
+ RefPtr<GMPVideoDecoderParent> kungfudeathgrip(this);
+ Release();
+ Shutdown();
+}
+
+nsresult GMPVideoDecoderParent::InitDecode(
+ const GMPVideoCodec& aCodecSettings,
+ const nsTArray<uint8_t>& aCodecSpecific,
+ GMPVideoDecoderCallbackProxy* aCallback, int32_t aCoreCount) {
+ GMP_LOG_DEBUG("GMPVideoDecoderParent[%p]::InitDecode()", this);
+
+ if (mActorDestroyed) {
+ NS_WARNING("Trying to use a destroyed GMP video decoder!");
+ return NS_ERROR_FAILURE;
+ }
+ if (mIsOpen) {
+ NS_WARNING("Trying to re-init an in-use GMP video decoder!");
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_ASSERT(mPlugin->GMPEventTarget()->IsOnCurrentThread());
+
+ if (!aCallback) {
+ return NS_ERROR_FAILURE;
+ }
+ mCallback = aCallback;
+
+ if (!SendInitDecode(aCodecSettings, aCodecSpecific, aCoreCount)) {
+ return NS_ERROR_FAILURE;
+ }
+ mIsOpen = true;
+
+ // Async IPC, we don't have access to a return value.
+ return NS_OK;
+}
+
+nsresult GMPVideoDecoderParent::Decode(
+ GMPUniquePtr<GMPVideoEncodedFrame> aInputFrame, bool aMissingFrames,
+ const nsTArray<uint8_t>& aCodecSpecificInfo, int64_t aRenderTimeMs) {
+ GMP_LOG_VERBOSE(
+ "GMPVideoDecoderParent[%p]::Decode() timestamp=%" PRId64 " keyframe=%d",
+ this, aInputFrame->TimeStamp(), aInputFrame->FrameType() == kGMPKeyFrame);
+
+ if (!mIsOpen) {
+ GMP_LOG_ERROR(
+ "GMPVideoDecoderParent[%p]::Decode() ERROR; dead GMPVideoDecoder",
+ this);
+ NS_WARNING("Trying to use an dead GMP video decoder");
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_ASSERT(mPlugin->GMPEventTarget()->IsOnCurrentThread());
+
+ GMPUniquePtr<GMPVideoEncodedFrameImpl> inputFrameImpl(
+ static_cast<GMPVideoEncodedFrameImpl*>(aInputFrame.release()));
+
+ // Very rough kill-switch if the plugin stops processing. If it's merely
+ // hung and continues, we'll come back to life eventually.
+ // 3* is because we're using 3 buffers per frame for i420 data for now.
+ if ((NumInUse(GMPSharedMem::kGMPFrameData) >
+ 3 * GMPSharedMem::kGMPBufLimit) ||
+ (NumInUse(GMPSharedMem::kGMPEncodedData) > GMPSharedMem::kGMPBufLimit)) {
+ GMP_LOG_ERROR(
+ "GMPVideoDecoderParent[%p]::Decode() ERROR; shmem buffer limit hit "
+ "frame=%d encoded=%d",
+ this, NumInUse(GMPSharedMem::kGMPFrameData),
+ NumInUse(GMPSharedMem::kGMPEncodedData));
+ return NS_ERROR_FAILURE;
+ }
+
+ GMPVideoEncodedFrameData frameData;
+ inputFrameImpl->RelinquishFrameData(frameData);
+
+ if (!SendDecode(frameData, aMissingFrames, aCodecSpecificInfo,
+ aRenderTimeMs)) {
+ GMP_LOG_ERROR(
+ "GMPVideoDecoderParent[%p]::Decode() ERROR; SendDecode() failure.",
+ this);
+ return NS_ERROR_FAILURE;
+ }
+ mFrameCount++;
+
+ // Async IPC, we don't have access to a return value.
+ return NS_OK;
+}
+
+nsresult GMPVideoDecoderParent::Reset() {
+ GMP_LOG_DEBUG("GMPVideoDecoderParent[%p]::Reset()", this);
+
+ if (!mIsOpen) {
+ NS_WARNING("Trying to use an dead GMP video decoder");
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_ASSERT(mPlugin->GMPEventTarget()->IsOnCurrentThread());
+
+ if (!SendReset()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mIsAwaitingResetComplete = true;
+
+ RefPtr<GMPVideoDecoderParent> self(this);
+ nsCOMPtr<nsIRunnable> task = NS_NewRunnableFunction(
+ "gmp::GMPVideoDecoderParent::Reset", [self]() -> void {
+ GMP_LOG_DEBUG(
+ "GMPVideoDecoderParent[%p]::ResetCompleteTimeout() timed out "
+ "waiting for ResetComplete",
+ self.get());
+ self->mResetCompleteTimeout = nullptr;
+ LogToBrowserConsole(nsLiteralString(
+ u"GMPVideoDecoderParent timed out waiting for ResetComplete()"));
+ });
+ CancelResetCompleteTimeout();
+ nsCOMPtr<nsISerialEventTarget> target = mPlugin->GMPEventTarget();
+ mResetCompleteTimeout = SimpleTimer::Create(task, 5000, target);
+
+ // Async IPC, we don't have access to a return value.
+ return NS_OK;
+}
+
+void GMPVideoDecoderParent::CancelResetCompleteTimeout() {
+ if (mResetCompleteTimeout) {
+ mResetCompleteTimeout->Cancel();
+ mResetCompleteTimeout = nullptr;
+ }
+}
+
+nsresult GMPVideoDecoderParent::Drain() {
+ GMP_LOG_DEBUG("GMPVideoDecoderParent[%p]::Drain() frameCount=%d", this,
+ mFrameCount);
+
+ if (!mIsOpen) {
+ NS_WARNING("Trying to use an dead GMP video decoder");
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_ASSERT(mPlugin->GMPEventTarget()->IsOnCurrentThread());
+
+ if (!SendDrain()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mIsAwaitingDrainComplete = true;
+
+ // Async IPC, we don't have access to a return value.
+ return NS_OK;
+}
+
+nsCString GMPVideoDecoderParent::GetDisplayName() const {
+ if (NS_WARN_IF(!mIsOpen)) {
+ return ""_ns;
+ }
+
+ MOZ_ASSERT(mPlugin->GMPEventTarget()->IsOnCurrentThread());
+ return mPlugin->GetDisplayName();
+}
+
+// Note: Consider keeping ActorDestroy sync'd up when making changes here.
+nsresult GMPVideoDecoderParent::Shutdown() {
+ GMP_LOG_DEBUG("GMPVideoDecoderParent[%p]::Shutdown()", this);
+ MOZ_ASSERT(!mPlugin || mPlugin->GMPEventTarget()->IsOnCurrentThread());
+
+ if (mShuttingDown) {
+ return NS_OK;
+ }
+ mShuttingDown = true;
+
+ // Ensure if we've received a shutdown while waiting for a ResetComplete
+ // or DrainComplete notification, we'll unblock the caller before processing
+ // the shutdown.
+ UnblockResetAndDrain();
+
+ // Notify client we're gone! Won't occur after Close()
+ if (mCallback) {
+ mCallback->Terminated();
+ mCallback = nullptr;
+ }
+
+ mIsOpen = false;
+ if (!mActorDestroyed) {
+ Unused << SendDecodingComplete();
+ }
+
+ return NS_OK;
+}
+
+// Note: Keep this sync'd up with Shutdown
+void GMPVideoDecoderParent::ActorDestroy(ActorDestroyReason aWhy) {
+ GMP_LOG_DEBUG("GMPVideoDecoderParent[%p]::ActorDestroy reason=%d", this,
+ aWhy);
+
+ mIsOpen = false;
+ mActorDestroyed = true;
+
+ // Ensure if we've received a destroy while waiting for a ResetComplete
+ // or DrainComplete notification, we'll unblock the caller before processing
+ // the error.
+ UnblockResetAndDrain();
+
+ if (mCallback) {
+ // May call Close() (and Shutdown()) immediately or with a delay
+ mCallback->Terminated();
+ mCallback = nullptr;
+ }
+ if (mPlugin) {
+ // Ignore any return code. It is OK for this to fail without killing the
+ // process.
+ mPlugin->VideoDecoderDestroyed(this);
+ mPlugin = nullptr;
+ }
+ mVideoHost.ActorDestroyed();
+ MaybeDisconnect(aWhy == AbnormalShutdown);
+}
+
+mozilla::ipc::IPCResult GMPVideoDecoderParent::RecvDecoded(
+ const GMPVideoi420FrameData& aDecodedFrame) {
+ --mFrameCount;
+ if (aDecodedFrame.mUpdatedTimestamp() &&
+ aDecodedFrame.mUpdatedTimestamp().value() != aDecodedFrame.mTimestamp()) {
+ GMP_LOG_VERBOSE(
+ "GMPVideoDecoderParent[%p]::RecvDecoded() timestamp=[%" PRId64
+ " -> %" PRId64 "] frameCount=%d",
+ this, aDecodedFrame.mTimestamp(),
+ aDecodedFrame.mUpdatedTimestamp().value(), mFrameCount);
+ } else {
+ GMP_LOG_VERBOSE(
+ "GMPVideoDecoderParent[%p]::RecvDecoded() timestamp=%" PRId64
+ " frameCount=%d",
+ this, aDecodedFrame.mTimestamp(), mFrameCount);
+ }
+
+ if (mCallback) {
+ if (GMPVideoi420FrameImpl::CheckFrameData(aDecodedFrame)) {
+ auto f = new GMPVideoi420FrameImpl(aDecodedFrame, &mVideoHost);
+
+ mCallback->Decoded(f);
+ } else {
+ GMP_LOG_ERROR(
+ "GMPVideoDecoderParent[%p]::RecvDecoded() "
+ "timestamp=%" PRId64 " decoded frame corrupt, ignoring",
+ this, aDecodedFrame.mTimestamp());
+ // TODO: Verify if we should take more serious the arrival of
+ // a corrupted frame, see bug 1750506.
+ }
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
+GMPVideoDecoderParent::RecvReceivedDecodedReferenceFrame(
+ const uint64_t& aPictureId) {
+ if (mCallback) {
+ mCallback->ReceivedDecodedReferenceFrame(aPictureId);
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPVideoDecoderParent::RecvReceivedDecodedFrame(
+ const uint64_t& aPictureId) {
+ if (mCallback) {
+ mCallback->ReceivedDecodedFrame(aPictureId);
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPVideoDecoderParent::RecvInputDataExhausted() {
+ GMP_LOG_VERBOSE("GMPVideoDecoderParent[%p]::RecvInputDataExhausted()", this);
+
+ if (mCallback) {
+ mCallback->InputDataExhausted();
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPVideoDecoderParent::RecvDrainComplete() {
+ GMP_LOG_DEBUG("GMPVideoDecoderParent[%p]::RecvDrainComplete() frameCount=%d",
+ this, mFrameCount);
+ nsAutoString msg;
+ msg.AppendLiteral(
+ "GMPVideoDecoderParent::RecvDrainComplete() outstanding frames=");
+ msg.AppendInt(mFrameCount);
+ LogToBrowserConsole(msg);
+
+ if (mCallback && mIsAwaitingDrainComplete) {
+ mIsAwaitingDrainComplete = false;
+
+ mCallback->DrainComplete();
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPVideoDecoderParent::RecvResetComplete() {
+ GMP_LOG_DEBUG("GMPVideoDecoderParent[%p]::RecvResetComplete()", this);
+
+ CancelResetCompleteTimeout();
+
+ if (mCallback && mIsAwaitingResetComplete) {
+ mIsAwaitingResetComplete = false;
+ mFrameCount = 0;
+
+ mCallback->ResetComplete();
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPVideoDecoderParent::RecvError(const GMPErr& aError) {
+ GMP_LOG_DEBUG("GMPVideoDecoderParent[%p]::RecvError(error=%d)", this, aError);
+
+ if (mCallback) {
+ // Ensure if we've received an error while waiting for a ResetComplete
+ // or DrainComplete notification, we'll unblock the caller before processing
+ // the error.
+ UnblockResetAndDrain();
+
+ mCallback->Error(aError);
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPVideoDecoderParent::RecvShutdown() {
+ GMP_LOG_DEBUG("GMPVideoDecoderParent[%p]::RecvShutdown()", this);
+
+ Shutdown();
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPVideoDecoderParent::RecvParentShmemForPool(
+ Shmem&& aEncodedBuffer) {
+ if (aEncodedBuffer.IsWritable()) {
+ mVideoHost.SharedMemMgr()->MgrDeallocShmem(GMPSharedMem::kGMPEncodedData,
+ aEncodedBuffer);
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPVideoDecoderParent::RecvNeedShmem(
+ const uint32_t& aFrameBufferSize, Shmem* aMem) {
+ ipc::Shmem mem;
+
+ if (!mVideoHost.SharedMemMgr()->MgrAllocShmem(GMPSharedMem::kGMPFrameData,
+ aFrameBufferSize, &mem)) {
+ GMP_LOG_ERROR("%s: Failed to get a shared mem buffer for Child! size %u",
+ __FUNCTION__, aFrameBufferSize);
+ return IPC_FAIL(this, "Failed to get a shared mem buffer for Child!");
+ }
+ *aMem = mem;
+ mem = ipc::Shmem();
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPVideoDecoderParent::Recv__delete__() {
+ GMP_LOG_DEBUG("GMPVideoDecoderParent[%p]::Recv__delete__()", this);
+
+ if (mPlugin) {
+ // Ignore any return code. It is OK for this to fail without killing the
+ // process.
+ mPlugin->VideoDecoderDestroyed(this);
+ mPlugin = nullptr;
+ }
+
+ return IPC_OK();
+}
+
+void GMPVideoDecoderParent::UnblockResetAndDrain() {
+ GMP_LOG_DEBUG(
+ "GMPVideoDecoderParent[%p]::UnblockResetAndDrain() "
+ "awaitingResetComplete=%d awaitingDrainComplete=%d",
+ this, mIsAwaitingResetComplete, mIsAwaitingDrainComplete);
+
+ if (!mCallback) {
+ MOZ_ASSERT(!mIsAwaitingResetComplete);
+ MOZ_ASSERT(!mIsAwaitingDrainComplete);
+ return;
+ }
+ if (mIsAwaitingResetComplete) {
+ mIsAwaitingResetComplete = false;
+ mCallback->ResetComplete();
+ }
+ if (mIsAwaitingDrainComplete) {
+ mIsAwaitingDrainComplete = false;
+ mCallback->DrainComplete();
+ }
+ CancelResetCompleteTimeout();
+}
+
+} // namespace mozilla::gmp
diff --git a/dom/media/gmp/GMPVideoDecoderParent.h b/dom/media/gmp/GMPVideoDecoderParent.h
new file mode 100644
index 0000000000..9265202a63
--- /dev/null
+++ b/dom/media/gmp/GMPVideoDecoderParent.h
@@ -0,0 +1,103 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GMPVideoDecoderParent_h_
+#define GMPVideoDecoderParent_h_
+
+#include "mozilla/RefPtr.h"
+#include "gmp-video-decode.h"
+#include "mozilla/gmp/PGMPVideoDecoderParent.h"
+#include "GMPMessageUtils.h"
+#include "GMPSharedMemManager.h"
+#include "GMPUtils.h"
+#include "GMPVideoHost.h"
+#include "GMPVideoDecoderProxy.h"
+#include "VideoUtils.h"
+#include "GMPCrashHelperHolder.h"
+
+namespace mozilla::gmp {
+
+class GMPContentParent;
+
+class GMPVideoDecoderParent final : public PGMPVideoDecoderParent,
+ public GMPVideoDecoderProxy,
+ public GMPSharedMemManager,
+ public GMPCrashHelperHolder {
+ friend class PGMPVideoDecoderParent;
+
+ public:
+ // Mark AddRef and Release as `final`, as they overload pure virtual
+ // implementations in PGMPVideoDecoderParent.
+ NS_INLINE_DECL_REFCOUNTING(GMPVideoDecoderParent, final)
+
+ explicit GMPVideoDecoderParent(GMPContentParent* aPlugin);
+
+ GMPVideoHostImpl& Host();
+ nsresult Shutdown();
+
+ // GMPVideoDecoder
+ void Close() override;
+ nsresult InitDecode(const GMPVideoCodec& aCodecSettings,
+ const nsTArray<uint8_t>& aCodecSpecific,
+ GMPVideoDecoderCallbackProxy* aCallback,
+ int32_t aCoreCount) override;
+ nsresult Decode(GMPUniquePtr<GMPVideoEncodedFrame> aInputFrame,
+ bool aMissingFrames,
+ const nsTArray<uint8_t>& aCodecSpecificInfo,
+ int64_t aRenderTimeMs = -1) override;
+ nsresult Reset() override;
+ nsresult Drain() override;
+ uint32_t GetPluginId() const override { return mPluginId; }
+ GMPPluginType GetPluginType() const override { return mPluginType; }
+ nsCString GetDisplayName() const override;
+
+ // GMPSharedMemManager
+ bool Alloc(size_t aSize, Shmem* aMem) override {
+ return AllocShmem(aSize, aMem);
+ }
+ void Dealloc(Shmem&& aMem) override { DeallocShmem(aMem); }
+
+ private:
+ ~GMPVideoDecoderParent();
+
+ // PGMPVideoDecoderParent
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+ mozilla::ipc::IPCResult RecvDecoded(
+ const GMPVideoi420FrameData& aDecodedFrame) override;
+ mozilla::ipc::IPCResult RecvReceivedDecodedReferenceFrame(
+ const uint64_t& aPictureId) override;
+ mozilla::ipc::IPCResult RecvReceivedDecodedFrame(
+ const uint64_t& aPictureId) override;
+ mozilla::ipc::IPCResult RecvInputDataExhausted() override;
+ mozilla::ipc::IPCResult RecvDrainComplete() override;
+ mozilla::ipc::IPCResult RecvResetComplete() override;
+ mozilla::ipc::IPCResult RecvError(const GMPErr& aError) override;
+ mozilla::ipc::IPCResult RecvShutdown() override;
+ mozilla::ipc::IPCResult RecvParentShmemForPool(
+ Shmem&& aEncodedBuffer) override;
+ mozilla::ipc::IPCResult RecvNeedShmem(const uint32_t& aFrameBufferSize,
+ Shmem* aMem) override;
+ mozilla::ipc::IPCResult Recv__delete__() override;
+
+ void UnblockResetAndDrain();
+ void CancelResetCompleteTimeout();
+
+ bool mIsOpen;
+ bool mShuttingDown;
+ bool mActorDestroyed;
+ bool mIsAwaitingResetComplete;
+ bool mIsAwaitingDrainComplete;
+ RefPtr<GMPContentParent> mPlugin;
+ RefPtr<GMPVideoDecoderCallbackProxy> mCallback;
+ GMPVideoHostImpl mVideoHost;
+ const uint32_t mPluginId;
+ GMPPluginType mPluginType = GMPPluginType::Unknown;
+ int32_t mFrameCount;
+ RefPtr<SimpleTimer> mResetCompleteTimeout;
+};
+
+} // namespace mozilla::gmp
+
+#endif // GMPVideoDecoderParent_h_
diff --git a/dom/media/gmp/GMPVideoDecoderProxy.h b/dom/media/gmp/GMPVideoDecoderProxy.h
new file mode 100644
index 0000000000..e12668755c
--- /dev/null
+++ b/dom/media/gmp/GMPVideoDecoderProxy.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GMPVideoDecoderProxy_h_
+#define GMPVideoDecoderProxy_h_
+
+#include "nsTArray.h"
+#include "gmp-video-decode.h"
+#include "gmp-video-frame-i420.h"
+#include "gmp-video-frame-encoded.h"
+
+#include "GMPCallbackBase.h"
+#include "GMPNativeTypes.h"
+#include "GMPUtils.h"
+
+class GMPVideoDecoderCallbackProxy : public GMPCallbackBase,
+ public GMPVideoDecoderCallback {
+ public:
+ virtual ~GMPVideoDecoderCallbackProxy() = default;
+};
+
+// A proxy to GMPVideoDecoder in the child process.
+// GMPVideoDecoderParent exposes this to users the GMP.
+// This enables Gecko to pass nsTArrays to the child GMP and avoid
+// an extra copy when doing so.
+
+// The consumer must call Close() when done with the codec, or when
+// Terminated() is called by the GMP plugin indicating an abnormal shutdown
+// of the underlying plugin. After calling Close(), the consumer must
+// not access this again.
+
+// This interface is not thread-safe and must only be used from GMPThread.
+class GMPVideoDecoderProxy {
+ public:
+ virtual nsresult InitDecode(const GMPVideoCodec& aCodecSettings,
+ const nsTArray<uint8_t>& aCodecSpecific,
+ GMPVideoDecoderCallbackProxy* aCallback,
+ int32_t aCoreCount) = 0;
+ virtual nsresult Decode(
+ mozilla::GMPUniquePtr<GMPVideoEncodedFrame> aInputFrame,
+ bool aMissingFrames, const nsTArray<uint8_t>& aCodecSpecificInfo,
+ int64_t aRenderTimeMs = -1) = 0;
+ virtual nsresult Reset() = 0;
+ virtual nsresult Drain() = 0;
+ virtual uint32_t GetPluginId() const = 0;
+ virtual GMPPluginType GetPluginType() const = 0;
+
+ // Call to tell GMP/plugin the consumer will no longer use this
+ // interface/codec.
+ virtual void Close() = 0;
+
+ virtual nsCString GetDisplayName() const = 0;
+};
+
+#endif
diff --git a/dom/media/gmp/GMPVideoEncodedFrameImpl.cpp b/dom/media/gmp/GMPVideoEncodedFrameImpl.cpp
new file mode 100644
index 0000000000..c0ed87df4d
--- /dev/null
+++ b/dom/media/gmp/GMPVideoEncodedFrameImpl.cpp
@@ -0,0 +1,226 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GMPVideoEncodedFrameImpl.h"
+#include "GMPVideoHost.h"
+#include "mozilla/gmp/GMPTypes.h"
+#include "GMPSharedMemManager.h"
+
+namespace mozilla::gmp {
+
+GMPVideoEncodedFrameImpl::GMPVideoEncodedFrameImpl(GMPVideoHostImpl* aHost)
+ : mEncodedWidth(0),
+ mEncodedHeight(0),
+ mTimeStamp(0ll),
+ mDuration(0ll),
+ mFrameType(kGMPDeltaFrame),
+ mSize(0),
+ mCompleteFrame(false),
+ mHost(aHost),
+ mBufferType(GMP_BufferSingle) {
+ MOZ_ASSERT(aHost);
+ aHost->EncodedFrameCreated(this);
+}
+
+GMPVideoEncodedFrameImpl::GMPVideoEncodedFrameImpl(
+ const GMPVideoEncodedFrameData& aFrameData, GMPVideoHostImpl* aHost)
+ : mEncodedWidth(aFrameData.mEncodedWidth()),
+ mEncodedHeight(aFrameData.mEncodedHeight()),
+ mTimeStamp(aFrameData.mTimestamp()),
+ mDuration(aFrameData.mDuration()),
+ mFrameType(static_cast<GMPVideoFrameType>(aFrameData.mFrameType())),
+ mSize(aFrameData.mSize()),
+ mCompleteFrame(aFrameData.mCompleteFrame()),
+ mHost(aHost),
+ mBuffer(aFrameData.mBuffer()),
+ mBufferType(aFrameData.mBufferType()) {
+ MOZ_ASSERT(aHost);
+ aHost->EncodedFrameCreated(this);
+}
+
+GMPVideoEncodedFrameImpl::~GMPVideoEncodedFrameImpl() {
+ DestroyBuffer();
+ if (mHost) {
+ mHost->EncodedFrameDestroyed(this);
+ }
+}
+
+GMPVideoFrameFormat GMPVideoEncodedFrameImpl::GetFrameFormat() {
+ return kGMPEncodedVideoFrame;
+}
+
+void GMPVideoEncodedFrameImpl::DoneWithAPI() {
+ DestroyBuffer();
+
+ // Do this after destroying the buffer because destruction
+ // involves deallocation, which requires a host.
+ mHost = nullptr;
+}
+
+void GMPVideoEncodedFrameImpl::ActorDestroyed() {
+ // Simply clear out Shmem reference, do not attempt to
+ // properly free it. It has already been freed.
+ mBuffer = ipc::Shmem();
+ // No more host.
+ mHost = nullptr;
+}
+
+bool GMPVideoEncodedFrameImpl::RelinquishFrameData(
+ GMPVideoEncodedFrameData& aFrameData) {
+ aFrameData.mEncodedWidth() = mEncodedWidth;
+ aFrameData.mEncodedHeight() = mEncodedHeight;
+ aFrameData.mTimestamp() = mTimeStamp;
+ aFrameData.mDuration() = mDuration;
+ aFrameData.mFrameType() = mFrameType;
+ aFrameData.mSize() = mSize;
+ aFrameData.mCompleteFrame() = mCompleteFrame;
+ aFrameData.mBuffer() = mBuffer;
+ aFrameData.mBufferType() = mBufferType;
+
+ // This method is called right before Shmem is sent to another process.
+ // We need to effectively zero out our member copy so that we don't
+ // try to delete Shmem we don't own later.
+ mBuffer = ipc::Shmem();
+
+ return true;
+}
+
+void GMPVideoEncodedFrameImpl::DestroyBuffer() {
+ if (mHost && mBuffer.IsWritable()) {
+ mHost->SharedMemMgr()->MgrDeallocShmem(GMPSharedMem::kGMPEncodedData,
+ mBuffer);
+ }
+ mBuffer = ipc::Shmem();
+}
+
+GMPErr GMPVideoEncodedFrameImpl::CreateEmptyFrame(uint32_t aSize) {
+ if (aSize == 0) {
+ DestroyBuffer();
+ } else if (aSize > AllocatedSize()) {
+ DestroyBuffer();
+ if (!mHost->SharedMemMgr()->MgrAllocShmem(GMPSharedMem::kGMPEncodedData,
+ aSize, &mBuffer) ||
+ !Buffer()) {
+ return GMPAllocErr;
+ }
+ }
+ mSize = aSize;
+
+ return GMPNoErr;
+}
+
+GMPErr GMPVideoEncodedFrameImpl::CopyFrame(const GMPVideoEncodedFrame& aFrame) {
+ auto& f = static_cast<const GMPVideoEncodedFrameImpl&>(aFrame);
+
+ if (f.mSize != 0) {
+ GMPErr err = CreateEmptyFrame(f.mSize);
+ if (err != GMPNoErr) {
+ return err;
+ }
+ memcpy(Buffer(), f.Buffer(), f.mSize);
+ }
+ mEncodedWidth = f.mEncodedWidth;
+ mEncodedHeight = f.mEncodedHeight;
+ mTimeStamp = f.mTimeStamp;
+ mDuration = f.mDuration;
+ mFrameType = f.mFrameType;
+ mSize = f.mSize; // already set...
+ mCompleteFrame = f.mCompleteFrame;
+ mBufferType = f.mBufferType;
+ // Don't copy host, that should have been set properly on object creation via
+ // host.
+
+ return GMPNoErr;
+}
+
+void GMPVideoEncodedFrameImpl::SetEncodedWidth(uint32_t aEncodedWidth) {
+ mEncodedWidth = aEncodedWidth;
+}
+
+uint32_t GMPVideoEncodedFrameImpl::EncodedWidth() { return mEncodedWidth; }
+
+void GMPVideoEncodedFrameImpl::SetEncodedHeight(uint32_t aEncodedHeight) {
+ mEncodedHeight = aEncodedHeight;
+}
+
+uint32_t GMPVideoEncodedFrameImpl::EncodedHeight() { return mEncodedHeight; }
+
+void GMPVideoEncodedFrameImpl::SetTimeStamp(uint64_t aTimeStamp) {
+ mTimeStamp = aTimeStamp;
+}
+
+uint64_t GMPVideoEncodedFrameImpl::TimeStamp() { return mTimeStamp; }
+
+void GMPVideoEncodedFrameImpl::SetDuration(uint64_t aDuration) {
+ mDuration = aDuration;
+}
+
+uint64_t GMPVideoEncodedFrameImpl::Duration() const { return mDuration; }
+
+void GMPVideoEncodedFrameImpl::SetFrameType(GMPVideoFrameType aFrameType) {
+ mFrameType = aFrameType;
+}
+
+GMPVideoFrameType GMPVideoEncodedFrameImpl::FrameType() { return mFrameType; }
+
+void GMPVideoEncodedFrameImpl::SetAllocatedSize(uint32_t aNewSize) {
+ if (aNewSize <= AllocatedSize()) {
+ return;
+ }
+
+ if (!mHost) {
+ return;
+ }
+
+ ipc::Shmem new_mem;
+ if (!mHost->SharedMemMgr()->MgrAllocShmem(GMPSharedMem::kGMPEncodedData,
+ aNewSize, &new_mem) ||
+ !new_mem.get<uint8_t>()) {
+ return;
+ }
+
+ if (mBuffer.IsReadable()) {
+ memcpy(new_mem.get<uint8_t>(), Buffer(), mSize);
+ }
+
+ DestroyBuffer();
+
+ mBuffer = new_mem;
+}
+
+uint32_t GMPVideoEncodedFrameImpl::AllocatedSize() {
+ if (mBuffer.IsWritable()) {
+ return mBuffer.Size<uint8_t>();
+ }
+ return 0;
+}
+
+void GMPVideoEncodedFrameImpl::SetSize(uint32_t aSize) { mSize = aSize; }
+
+uint32_t GMPVideoEncodedFrameImpl::Size() { return mSize; }
+
+void GMPVideoEncodedFrameImpl::SetCompleteFrame(bool aCompleteFrame) {
+ mCompleteFrame = aCompleteFrame;
+}
+
+bool GMPVideoEncodedFrameImpl::CompleteFrame() { return mCompleteFrame; }
+
+const uint8_t* GMPVideoEncodedFrameImpl::Buffer() const {
+ return mBuffer.get<uint8_t>();
+}
+
+uint8_t* GMPVideoEncodedFrameImpl::Buffer() { return mBuffer.get<uint8_t>(); }
+
+void GMPVideoEncodedFrameImpl::Destroy() { delete this; }
+
+GMPBufferType GMPVideoEncodedFrameImpl::BufferType() const {
+ return mBufferType;
+}
+
+void GMPVideoEncodedFrameImpl::SetBufferType(GMPBufferType aBufferType) {
+ mBufferType = aBufferType;
+}
+
+} // namespace mozilla::gmp
diff --git a/dom/media/gmp/GMPVideoEncodedFrameImpl.h b/dom/media/gmp/GMPVideoEncodedFrameImpl.h
new file mode 100644
index 0000000000..fbb5f92146
--- /dev/null
+++ b/dom/media/gmp/GMPVideoEncodedFrameImpl.h
@@ -0,0 +1,116 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* Copyright (c) 2014, Mozilla Corporation
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * 2. Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following
+ * disclaimer in the documentation and/or other materials provided
+ * with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
+ * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
+ * COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
+ * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+ * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
+ * OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef GMPVideoEncodedFrameImpl_h_
+#define GMPVideoEncodedFrameImpl_h_
+
+#include "gmp-errors.h"
+#include "gmp-video-frame.h"
+#include "gmp-video-frame-encoded.h"
+#include "mozilla/ipc/Shmem.h"
+
+namespace mozilla {
+class CryptoSample;
+
+namespace gmp {
+
+class GMPVideoHostImpl;
+class GMPVideoEncodedFrameData;
+
+class GMPVideoEncodedFrameImpl : public GMPVideoEncodedFrame {
+ friend struct IPC::ParamTraits<mozilla::gmp::GMPVideoEncodedFrameImpl>;
+
+ public:
+ explicit GMPVideoEncodedFrameImpl(GMPVideoHostImpl* aHost);
+ GMPVideoEncodedFrameImpl(const GMPVideoEncodedFrameData& aFrameData,
+ GMPVideoHostImpl* aHost);
+ virtual ~GMPVideoEncodedFrameImpl();
+
+ // This is called during a normal destroy sequence, which is
+ // when a consumer is finished or during XPCOM shutdown.
+ void DoneWithAPI();
+ // Does not attempt to release Shmem, as the Shmem has already been released.
+ void ActorDestroyed();
+
+ bool RelinquishFrameData(GMPVideoEncodedFrameData& aFrameData);
+
+ // GMPVideoFrame
+ GMPVideoFrameFormat GetFrameFormat() override;
+ void Destroy() override;
+
+ // GMPVideoEncodedFrame
+ GMPErr CreateEmptyFrame(uint32_t aSize) override;
+ GMPErr CopyFrame(const GMPVideoEncodedFrame& aFrame) override;
+ void SetEncodedWidth(uint32_t aEncodedWidth) override;
+ uint32_t EncodedWidth() override;
+ void SetEncodedHeight(uint32_t aEncodedHeight) override;
+ uint32_t EncodedHeight() override;
+ // Microseconds
+ void SetTimeStamp(uint64_t aTimeStamp) override;
+ uint64_t TimeStamp() override;
+ // Set frame duration (microseconds)
+ // NOTE: next-frame's Timestamp() != this-frame's TimeStamp()+Duration()
+ // depending on rounding to avoid having to track roundoff errors
+ // and dropped/missing frames(!) (which may leave a large gap)
+ void SetDuration(uint64_t aDuration) override;
+ uint64_t Duration() const override;
+ void SetFrameType(GMPVideoFrameType aFrameType) override;
+ GMPVideoFrameType FrameType() override;
+ void SetAllocatedSize(uint32_t aNewSize) override;
+ uint32_t AllocatedSize() override;
+ void SetSize(uint32_t aSize) override;
+ uint32_t Size() override;
+ void SetCompleteFrame(bool aCompleteFrame) override;
+ bool CompleteFrame() override;
+ const uint8_t* Buffer() const override;
+ uint8_t* Buffer() override;
+ GMPBufferType BufferType() const override;
+ void SetBufferType(GMPBufferType aBufferType) override;
+
+ private:
+ void DestroyBuffer();
+
+ uint32_t mEncodedWidth;
+ uint32_t mEncodedHeight;
+ uint64_t mTimeStamp;
+ uint64_t mDuration;
+ GMPVideoFrameType mFrameType;
+ uint32_t mSize;
+ bool mCompleteFrame;
+ GMPVideoHostImpl* mHost;
+ ipc::Shmem mBuffer;
+ GMPBufferType mBufferType;
+};
+
+} // namespace gmp
+
+} // namespace mozilla
+
+#endif // GMPVideoEncodedFrameImpl_h_
diff --git a/dom/media/gmp/GMPVideoEncoderChild.cpp b/dom/media/gmp/GMPVideoEncoderChild.cpp
new file mode 100644
index 0000000000..19a96b5efe
--- /dev/null
+++ b/dom/media/gmp/GMPVideoEncoderChild.cpp
@@ -0,0 +1,203 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GMPVideoEncoderChild.h"
+#include "GMPContentChild.h"
+#include <stdio.h>
+#include "mozilla/Unused.h"
+#include "GMPVideoEncodedFrameImpl.h"
+#include "GMPVideoi420FrameImpl.h"
+#include "runnable_utils.h"
+
+namespace mozilla::gmp {
+
+GMPVideoEncoderChild::GMPVideoEncoderChild(GMPContentChild* aPlugin)
+ : GMPSharedMemManager(aPlugin),
+ mPlugin(aPlugin),
+ mVideoEncoder(nullptr),
+ mVideoHost(this),
+ mNeedShmemIntrCount(0),
+ mPendingEncodeComplete(false) {
+ MOZ_ASSERT(mPlugin);
+}
+
+GMPVideoEncoderChild::~GMPVideoEncoderChild() {
+ MOZ_ASSERT(!mNeedShmemIntrCount);
+}
+
+void GMPVideoEncoderChild::Init(GMPVideoEncoder* aEncoder) {
+ MOZ_ASSERT(aEncoder,
+ "Cannot initialize video encoder child without a video encoder!");
+ mVideoEncoder = aEncoder;
+}
+
+GMPVideoHostImpl& GMPVideoEncoderChild::Host() { return mVideoHost; }
+
+void GMPVideoEncoderChild::Encoded(GMPVideoEncodedFrame* aEncodedFrame,
+ const uint8_t* aCodecSpecificInfo,
+ uint32_t aCodecSpecificInfoLength) {
+ MOZ_ASSERT(mPlugin->GMPMessageLoop() == MessageLoop::current());
+
+ auto ef = static_cast<GMPVideoEncodedFrameImpl*>(aEncodedFrame);
+
+ GMPVideoEncodedFrameData frameData;
+ ef->RelinquishFrameData(frameData);
+
+ nsTArray<uint8_t> codecSpecific;
+ codecSpecific.AppendElements(aCodecSpecificInfo, aCodecSpecificInfoLength);
+ SendEncoded(frameData, codecSpecific);
+
+ aEncodedFrame->Destroy();
+}
+
+void GMPVideoEncoderChild::Error(GMPErr aError) {
+ MOZ_ASSERT(mPlugin->GMPMessageLoop() == MessageLoop::current());
+
+ SendError(aError);
+}
+
+mozilla::ipc::IPCResult GMPVideoEncoderChild::RecvInitEncode(
+ const GMPVideoCodec& aCodecSettings, nsTArray<uint8_t>&& aCodecSpecific,
+ const int32_t& aNumberOfCores, const uint32_t& aMaxPayloadSize) {
+ if (!mVideoEncoder) {
+ return IPC_FAIL(this, "!mVideoDecoder");
+ }
+
+ // Ignore any return code. It is OK for this to fail without killing the
+ // process.
+ mVideoEncoder->InitEncode(aCodecSettings, aCodecSpecific.Elements(),
+ aCodecSpecific.Length(), this, aNumberOfCores,
+ aMaxPayloadSize);
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPVideoEncoderChild::RecvEncode(
+ const GMPVideoi420FrameData& aInputFrame,
+ nsTArray<uint8_t>&& aCodecSpecificInfo,
+ nsTArray<GMPVideoFrameType>&& aFrameTypes) {
+ if (!mVideoEncoder) {
+ return IPC_FAIL(this, "!mVideoDecoder");
+ }
+
+ auto f = new GMPVideoi420FrameImpl(aInputFrame, &mVideoHost);
+
+ // Ignore any return code. It is OK for this to fail without killing the
+ // process.
+ mVideoEncoder->Encode(f, aCodecSpecificInfo.Elements(),
+ aCodecSpecificInfo.Length(), aFrameTypes.Elements(),
+ aFrameTypes.Length());
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPVideoEncoderChild::RecvChildShmemForPool(
+ Shmem&& aEncodedBuffer) {
+ if (aEncodedBuffer.IsWritable()) {
+ mVideoHost.SharedMemMgr()->MgrDeallocShmem(GMPSharedMem::kGMPEncodedData,
+ aEncodedBuffer);
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPVideoEncoderChild::RecvSetChannelParameters(
+ const uint32_t& aPacketLoss, const uint32_t& aRTT) {
+ if (!mVideoEncoder) {
+ return IPC_FAIL(this, "!mVideoDecoder");
+ }
+
+ // Ignore any return code. It is OK for this to fail without killing the
+ // process.
+ mVideoEncoder->SetChannelParameters(aPacketLoss, aRTT);
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPVideoEncoderChild::RecvSetRates(
+ const uint32_t& aNewBitRate, const uint32_t& aFrameRate) {
+ if (!mVideoEncoder) {
+ return IPC_FAIL(this, "!mVideoDecoder");
+ }
+
+ // Ignore any return code. It is OK for this to fail without killing the
+ // process.
+ mVideoEncoder->SetRates(aNewBitRate, aFrameRate);
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPVideoEncoderChild::RecvSetPeriodicKeyFrames(
+ const bool& aEnable) {
+ if (!mVideoEncoder) {
+ return IPC_FAIL(this, "!mVideoDecoder");
+ }
+
+ // Ignore any return code. It is OK for this to fail without killing the
+ // process.
+ mVideoEncoder->SetPeriodicKeyFrames(aEnable);
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPVideoEncoderChild::RecvEncodingComplete() {
+ MOZ_ASSERT(mPlugin->GMPMessageLoop() == MessageLoop::current());
+
+ if (mNeedShmemIntrCount) {
+ // There's a GMP blocked in Alloc() waiting for the CallNeedShem() to
+ // return a frame they can use. Don't call the GMP's EncodingComplete()
+ // now and don't delete the GMPVideoEncoderChild, defer processing the
+ // EncodingComplete() until once the Alloc() finishes.
+ mPendingEncodeComplete = true;
+ return IPC_OK();
+ }
+
+ if (!mVideoEncoder) {
+ // There is not much to clean up anymore.
+ Unused << Send__delete__(this);
+ return IPC_OK();
+ }
+
+ // Ignore any return code. It is OK for this to fail without killing the
+ // process.
+ mVideoEncoder->EncodingComplete();
+
+ mVideoHost.DoneWithAPI();
+
+ mPlugin = nullptr;
+
+ Unused << Send__delete__(this);
+
+ return IPC_OK();
+}
+
+bool GMPVideoEncoderChild::Alloc(size_t aSize, Shmem* aMem) {
+ MOZ_ASSERT(mPlugin->GMPMessageLoop() == MessageLoop::current());
+
+ bool rv;
+#ifndef SHMEM_ALLOC_IN_CHILD
+ ++mNeedShmemIntrCount;
+ rv = SendNeedShmem(aSize, aMem);
+ --mNeedShmemIntrCount;
+ if (mPendingEncodeComplete && mNeedShmemIntrCount == 0) {
+ mPendingEncodeComplete = false;
+ mPlugin->GMPMessageLoop()->PostTask(
+ NewRunnableMethod("gmp::GMPVideoEncoderChild::RecvEncodingComplete",
+ this, &GMPVideoEncoderChild::RecvEncodingComplete));
+ }
+#else
+ rv = AllocShmem(aSize, aMem);
+#endif
+ return rv;
+}
+
+void GMPVideoEncoderChild::Dealloc(Shmem&& aMem) {
+#ifndef SHMEM_ALLOC_IN_CHILD
+ SendParentShmemForPool(std::move(aMem));
+#else
+ DeallocShmem(aMem);
+#endif
+}
+
+} // namespace mozilla::gmp
diff --git a/dom/media/gmp/GMPVideoEncoderChild.h b/dom/media/gmp/GMPVideoEncoderChild.h
new file mode 100644
index 0000000000..dd3c0fdf37
--- /dev/null
+++ b/dom/media/gmp/GMPVideoEncoderChild.h
@@ -0,0 +1,75 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GMPVideoEncoderChild_h_
+#define GMPVideoEncoderChild_h_
+
+#include "nsString.h"
+#include "mozilla/gmp/PGMPVideoEncoderChild.h"
+#include "gmp-video-encode.h"
+#include "GMPSharedMemManager.h"
+#include "GMPVideoHost.h"
+
+namespace mozilla::gmp {
+
+class GMPContentChild;
+
+class GMPVideoEncoderChild : public PGMPVideoEncoderChild,
+ public GMPVideoEncoderCallback,
+ public GMPSharedMemManager {
+ friend class PGMPVideoEncoderChild;
+
+ public:
+ // Mark AddRef and Release as `final`, as they overload pure virtual
+ // implementations in PGMPVideoEncoderChild.
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GMPVideoEncoderChild, final);
+
+ explicit GMPVideoEncoderChild(GMPContentChild* aPlugin);
+
+ void Init(GMPVideoEncoder* aEncoder);
+ GMPVideoHostImpl& Host();
+
+ // GMPVideoEncoderCallback
+ void Encoded(GMPVideoEncodedFrame* aEncodedFrame,
+ const uint8_t* aCodecSpecificInfo,
+ uint32_t aCodecSpecificInfoLength) override;
+ void Error(GMPErr aError) override;
+
+ // GMPSharedMemManager
+ bool Alloc(size_t aSize, Shmem* aMem) override;
+ void Dealloc(Shmem&& aMem) override;
+
+ private:
+ virtual ~GMPVideoEncoderChild();
+
+ // PGMPVideoEncoderChild
+ mozilla::ipc::IPCResult RecvInitEncode(const GMPVideoCodec& aCodecSettings,
+ nsTArray<uint8_t>&& aCodecSpecific,
+ const int32_t& aNumberOfCores,
+ const uint32_t& aMaxPayloadSize);
+ mozilla::ipc::IPCResult RecvEncode(const GMPVideoi420FrameData& aInputFrame,
+ nsTArray<uint8_t>&& aCodecSpecificInfo,
+ nsTArray<GMPVideoFrameType>&& aFrameTypes);
+ mozilla::ipc::IPCResult RecvChildShmemForPool(Shmem&& aEncodedBuffer);
+ mozilla::ipc::IPCResult RecvSetChannelParameters(const uint32_t& aPacketLoss,
+ const uint32_t& aRTT);
+ mozilla::ipc::IPCResult RecvSetRates(const uint32_t& aNewBitRate,
+ const uint32_t& aFrameRate);
+ mozilla::ipc::IPCResult RecvSetPeriodicKeyFrames(const bool& aEnable);
+ mozilla::ipc::IPCResult RecvEncodingComplete();
+
+ GMPContentChild* mPlugin;
+ GMPVideoEncoder* mVideoEncoder;
+ GMPVideoHostImpl mVideoHost;
+
+ // Non-zero when a GMP is blocked spinning the IPC message loop while
+ // waiting on an NeedShmem to complete.
+ int mNeedShmemIntrCount;
+ bool mPendingEncodeComplete;
+};
+
+} // namespace mozilla::gmp
+
+#endif // GMPVideoEncoderChild_h_
diff --git a/dom/media/gmp/GMPVideoEncoderParent.cpp b/dom/media/gmp/GMPVideoEncoderParent.cpp
new file mode 100644
index 0000000000..21c86ff9ab
--- /dev/null
+++ b/dom/media/gmp/GMPVideoEncoderParent.cpp
@@ -0,0 +1,307 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GMPVideoEncoderParent.h"
+
+#include "GMPContentParent.h"
+#include "GMPCrashHelper.h"
+#include "GMPLog.h"
+#include "GMPMessageUtils.h"
+#include "GMPVideoEncodedFrameImpl.h"
+#include "GMPVideoi420FrameImpl.h"
+#include "mozilla/gmp/GMPTypes.h"
+#include "mozilla/Unused.h"
+#include "nsAutoRef.h"
+#include "nsThread.h"
+#include "nsThreadUtils.h"
+#include "runnable_utils.h"
+
+namespace mozilla::gmp {
+
+#ifdef __CLASS__
+# undef __CLASS__
+#endif
+#define __CLASS__ "GMPVideoEncoderParent"
+
+// States:
+// Initial: mIsOpen == false
+// on InitDecode success -> Open
+// on Shutdown -> Dead
+// Open: mIsOpen == true
+// on Close -> Dead
+// on ActorDestroy -> Dead
+// on Shutdown -> Dead
+// Dead: mIsOpen == false
+
+GMPVideoEncoderParent::GMPVideoEncoderParent(GMPContentParent* aPlugin)
+ : GMPSharedMemManager(aPlugin),
+ mIsOpen(false),
+ mShuttingDown(false),
+ mActorDestroyed(false),
+ mPlugin(aPlugin),
+ mCallback(nullptr),
+ mVideoHost(this),
+ mPluginId(aPlugin->GetPluginId()) {
+ MOZ_ASSERT(mPlugin);
+}
+
+GMPVideoHostImpl& GMPVideoEncoderParent::Host() { return mVideoHost; }
+
+// Note: may be called via Terminated()
+void GMPVideoEncoderParent::Close() {
+ GMP_LOG_DEBUG("%s::%s: %p", __CLASS__, __FUNCTION__, this);
+ MOZ_ASSERT(mPlugin->GMPEventTarget()->IsOnCurrentThread());
+ // Consumer is done with us; we can shut down. No more callbacks should
+ // be made to mCallback. Note: do this before Shutdown()!
+ mCallback = nullptr;
+
+ // Let Shutdown mark us as dead so it knows if we had been alive
+
+ // In case this is the last reference
+ RefPtr<GMPVideoEncoderParent> kungfudeathgrip(this);
+ Release();
+ Shutdown();
+}
+
+GMPErr GMPVideoEncoderParent::InitEncode(
+ const GMPVideoCodec& aCodecSettings,
+ const nsTArray<uint8_t>& aCodecSpecific,
+ GMPVideoEncoderCallbackProxy* aCallback, int32_t aNumberOfCores,
+ uint32_t aMaxPayloadSize) {
+ GMP_LOG_DEBUG("%s::%s: %p", __CLASS__, __FUNCTION__, this);
+ if (mIsOpen) {
+ NS_WARNING("Trying to re-init an in-use GMP video encoder!");
+ return GMPGenericErr;
+ ;
+ }
+
+ MOZ_ASSERT(mPlugin->GMPEventTarget()->IsOnCurrentThread());
+ MOZ_ASSERT(!mCallback);
+
+ if (!aCallback) {
+ return GMPGenericErr;
+ }
+ mCallback = aCallback;
+
+ if (!SendInitEncode(aCodecSettings, aCodecSpecific, aNumberOfCores,
+ aMaxPayloadSize)) {
+ return GMPGenericErr;
+ }
+ mIsOpen = true;
+
+ // Async IPC, we don't have access to a return value.
+ return GMPNoErr;
+}
+
+GMPErr GMPVideoEncoderParent::Encode(
+ GMPUniquePtr<GMPVideoi420Frame> aInputFrame,
+ const nsTArray<uint8_t>& aCodecSpecificInfo,
+ const nsTArray<GMPVideoFrameType>& aFrameTypes) {
+ if (!mIsOpen) {
+ NS_WARNING("Trying to use an dead GMP video encoder");
+ return GMPGenericErr;
+ }
+
+ MOZ_ASSERT(mPlugin->GMPEventTarget()->IsOnCurrentThread());
+
+ GMPUniquePtr<GMPVideoi420FrameImpl> inputFrameImpl(
+ static_cast<GMPVideoi420FrameImpl*>(aInputFrame.release()));
+
+ // Very rough kill-switch if the plugin stops processing. If it's merely
+ // hung and continues, we'll come back to life eventually.
+ // 3* is because we're using 3 buffers per frame for i420 data for now.
+ if ((NumInUse(GMPSharedMem::kGMPFrameData) >
+ 3 * GMPSharedMem::kGMPBufLimit) ||
+ (NumInUse(GMPSharedMem::kGMPEncodedData) > GMPSharedMem::kGMPBufLimit)) {
+ GMP_LOG_ERROR(
+ "%s::%s: Out of mem buffers. Frame Buffers:%lu Max:%lu, Encoded "
+ "Buffers: %lu Max: %lu",
+ __CLASS__, __FUNCTION__,
+ static_cast<unsigned long>(NumInUse(GMPSharedMem::kGMPFrameData)),
+ static_cast<unsigned long>(3 * GMPSharedMem::kGMPBufLimit),
+ static_cast<unsigned long>(NumInUse(GMPSharedMem::kGMPEncodedData)),
+ static_cast<unsigned long>(GMPSharedMem::kGMPBufLimit));
+ return GMPGenericErr;
+ }
+
+ GMPVideoi420FrameData frameData;
+ inputFrameImpl->InitFrameData(frameData);
+
+ if (!SendEncode(frameData, aCodecSpecificInfo, aFrameTypes)) {
+ GMP_LOG_ERROR("%s::%s: failed to send encode", __CLASS__, __FUNCTION__);
+ return GMPGenericErr;
+ }
+
+ // Async IPC, we don't have access to a return value.
+ return GMPNoErr;
+}
+
+GMPErr GMPVideoEncoderParent::SetChannelParameters(uint32_t aPacketLoss,
+ uint32_t aRTT) {
+ if (!mIsOpen) {
+ NS_WARNING("Trying to use an invalid GMP video encoder!");
+ return GMPGenericErr;
+ }
+
+ MOZ_ASSERT(mPlugin->GMPEventTarget()->IsOnCurrentThread());
+
+ if (!SendSetChannelParameters(aPacketLoss, aRTT)) {
+ return GMPGenericErr;
+ }
+
+ // Async IPC, we don't have access to a return value.
+ return GMPNoErr;
+}
+
+GMPErr GMPVideoEncoderParent::SetRates(uint32_t aNewBitRate,
+ uint32_t aFrameRate) {
+ if (!mIsOpen) {
+ NS_WARNING("Trying to use an dead GMP video decoder");
+ return GMPGenericErr;
+ }
+
+ MOZ_ASSERT(mPlugin->GMPEventTarget()->IsOnCurrentThread());
+
+ if (!SendSetRates(aNewBitRate, aFrameRate)) {
+ return GMPGenericErr;
+ }
+
+ // Async IPC, we don't have access to a return value.
+ return GMPNoErr;
+}
+
+GMPErr GMPVideoEncoderParent::SetPeriodicKeyFrames(bool aEnable) {
+ if (!mIsOpen) {
+ NS_WARNING("Trying to use an invalid GMP video encoder!");
+ return GMPGenericErr;
+ }
+
+ MOZ_ASSERT(mPlugin->GMPEventTarget()->IsOnCurrentThread());
+
+ if (!SendSetPeriodicKeyFrames(aEnable)) {
+ return GMPGenericErr;
+ }
+
+ // Async IPC, we don't have access to a return value.
+ return GMPNoErr;
+}
+
+// Note: Consider keeping ActorDestroy sync'd up when making changes here.
+void GMPVideoEncoderParent::Shutdown() {
+ GMP_LOG_DEBUG("%s::%s: %p", __CLASS__, __FUNCTION__, this);
+ MOZ_ASSERT(mPlugin->GMPEventTarget()->IsOnCurrentThread());
+
+ if (mShuttingDown) {
+ return;
+ }
+ mShuttingDown = true;
+
+ // Notify client we're gone! Won't occur after Close()
+ if (mCallback) {
+ mCallback->Terminated();
+ mCallback = nullptr;
+ }
+
+ mIsOpen = false;
+ if (!mActorDestroyed) {
+ Unused << SendEncodingComplete();
+ }
+}
+
+// Note: Keep this sync'd up with Shutdown
+void GMPVideoEncoderParent::ActorDestroy(ActorDestroyReason aWhy) {
+ GMP_LOG_DEBUG("%s::%s: %p (%d)", __CLASS__, __FUNCTION__, this, (int)aWhy);
+ mIsOpen = false;
+ mActorDestroyed = true;
+ if (mCallback) {
+ // May call Close() (and Shutdown()) immediately or with a delay
+ mCallback->Terminated();
+ mCallback = nullptr;
+ }
+ if (mPlugin) {
+ // Ignore any return code. It is OK for this to fail without killing the
+ // process.
+ mPlugin->VideoEncoderDestroyed(this);
+ mPlugin = nullptr;
+ }
+ mVideoHost.ActorDestroyed(); // same as DoneWithAPI
+ MaybeDisconnect(aWhy == AbnormalShutdown);
+}
+
+mozilla::ipc::IPCResult GMPVideoEncoderParent::RecvEncoded(
+ const GMPVideoEncodedFrameData& aEncodedFrame,
+ nsTArray<uint8_t>&& aCodecSpecificInfo) {
+ if (mCallback) {
+ auto f = new GMPVideoEncodedFrameImpl(aEncodedFrame, &mVideoHost);
+ mCallback->Encoded(f, aCodecSpecificInfo);
+ f->Destroy();
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPVideoEncoderParent::RecvError(const GMPErr& aError) {
+ if (mCallback) {
+ mCallback->Error(aError);
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPVideoEncoderParent::RecvShutdown() {
+ Shutdown();
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPVideoEncoderParent::RecvParentShmemForPool(
+ Shmem&& aFrameBuffer) {
+ if (aFrameBuffer.IsWritable()) {
+ // This test may be paranoia now that we don't shut down the VideoHost
+ // in ::Shutdown, but doesn't hurt
+ if (mVideoHost.SharedMemMgr()) {
+ mVideoHost.SharedMemMgr()->MgrDeallocShmem(GMPSharedMem::kGMPFrameData,
+ aFrameBuffer);
+ } else {
+ GMP_LOG_DEBUG(
+ "%s::%s: %p Called in shutdown, ignoring and freeing directly",
+ __CLASS__, __FUNCTION__, this);
+ DeallocShmem(aFrameBuffer);
+ }
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPVideoEncoderParent::RecvNeedShmem(
+ const uint32_t& aEncodedBufferSize, Shmem* aMem) {
+ ipc::Shmem mem;
+
+ // This test may be paranoia now that we don't shut down the VideoHost
+ // in ::Shutdown, but doesn't hurt
+ if (!mVideoHost.SharedMemMgr() ||
+ !mVideoHost.SharedMemMgr()->MgrAllocShmem(GMPSharedMem::kGMPEncodedData,
+ aEncodedBufferSize, &mem)) {
+ GMP_LOG_ERROR(
+ "%s::%s: Failed to get a shared mem buffer for Child! size %u",
+ __CLASS__, __FUNCTION__, aEncodedBufferSize);
+ return IPC_FAIL(this, "Failed to get a shared mem buffer for Child!");
+ }
+ *aMem = mem;
+ mem = ipc::Shmem();
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GMPVideoEncoderParent::Recv__delete__() {
+ if (mPlugin) {
+ // Ignore any return code. It is OK for this to fail without killing the
+ // process.
+ mPlugin->VideoEncoderDestroyed(this);
+ mPlugin = nullptr;
+ }
+
+ return IPC_OK();
+}
+
+} // namespace mozilla::gmp
+
+#undef __CLASS__
diff --git a/dom/media/gmp/GMPVideoEncoderParent.h b/dom/media/gmp/GMPVideoEncoderParent.h
new file mode 100644
index 0000000000..c5c81194ec
--- /dev/null
+++ b/dom/media/gmp/GMPVideoEncoderParent.h
@@ -0,0 +1,85 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GMPVideoEncoderParent_h_
+#define GMPVideoEncoderParent_h_
+
+#include "mozilla/RefPtr.h"
+#include "gmp-video-encode.h"
+#include "mozilla/gmp/PGMPVideoEncoderParent.h"
+#include "GMPMessageUtils.h"
+#include "GMPSharedMemManager.h"
+#include "GMPUtils.h"
+#include "GMPVideoHost.h"
+#include "GMPVideoEncoderProxy.h"
+#include "GMPCrashHelperHolder.h"
+
+namespace mozilla::gmp {
+
+class GMPContentParent;
+
+class GMPVideoEncoderParent : public GMPVideoEncoderProxy,
+ public PGMPVideoEncoderParent,
+ public GMPSharedMemManager,
+ public GMPCrashHelperHolder {
+ friend class PGMPVideoEncoderParent;
+
+ public:
+ // Mark AddRef and Release as `final`, as they overload pure virtual
+ // implementations in PGMPVideoEncoderParent.
+ NS_INLINE_DECL_REFCOUNTING(GMPVideoEncoderParent, final)
+
+ explicit GMPVideoEncoderParent(GMPContentParent* aPlugin);
+
+ GMPVideoHostImpl& Host();
+ void Shutdown();
+
+ // GMPVideoEncoderProxy
+ void Close() override;
+ GMPErr InitEncode(const GMPVideoCodec& aCodecSettings,
+ const nsTArray<uint8_t>& aCodecSpecific,
+ GMPVideoEncoderCallbackProxy* aCallback,
+ int32_t aNumberOfCores, uint32_t aMaxPayloadSize) override;
+ GMPErr Encode(GMPUniquePtr<GMPVideoi420Frame> aInputFrame,
+ const nsTArray<uint8_t>& aCodecSpecificInfo,
+ const nsTArray<GMPVideoFrameType>& aFrameTypes) override;
+ GMPErr SetChannelParameters(uint32_t aPacketLoss, uint32_t aRTT) override;
+ GMPErr SetRates(uint32_t aNewBitRate, uint32_t aFrameRate) override;
+ GMPErr SetPeriodicKeyFrames(bool aEnable) override;
+ uint32_t GetPluginId() const override { return mPluginId; }
+
+ // GMPSharedMemManager
+ bool Alloc(size_t aSize, Shmem* aMem) override {
+ return AllocShmem(aSize, aMem);
+ }
+ void Dealloc(Shmem&& aMem) override { DeallocShmem(aMem); }
+
+ private:
+ virtual ~GMPVideoEncoderParent() = default;
+
+ // PGMPVideoEncoderParent
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+ mozilla::ipc::IPCResult RecvEncoded(
+ const GMPVideoEncodedFrameData& aEncodedFrame,
+ nsTArray<uint8_t>&& aCodecSpecificInfo) override;
+ mozilla::ipc::IPCResult RecvError(const GMPErr& aError) override;
+ mozilla::ipc::IPCResult RecvShutdown() override;
+ mozilla::ipc::IPCResult RecvParentShmemForPool(Shmem&& aFrameBuffer) override;
+ mozilla::ipc::IPCResult RecvNeedShmem(const uint32_t& aEncodedBufferSize,
+ Shmem* aMem) override;
+ mozilla::ipc::IPCResult Recv__delete__() override;
+
+ bool mIsOpen;
+ bool mShuttingDown;
+ bool mActorDestroyed;
+ RefPtr<GMPContentParent> mPlugin;
+ RefPtr<GMPVideoEncoderCallbackProxy> mCallback;
+ GMPVideoHostImpl mVideoHost;
+ const uint32_t mPluginId;
+};
+
+} // namespace mozilla::gmp
+
+#endif // GMPVideoEncoderParent_h_
diff --git a/dom/media/gmp/GMPVideoEncoderProxy.h b/dom/media/gmp/GMPVideoEncoderProxy.h
new file mode 100644
index 0000000000..ad638ebb3a
--- /dev/null
+++ b/dom/media/gmp/GMPVideoEncoderProxy.h
@@ -0,0 +1,56 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GMPVideoEncoderProxy_h_
+#define GMPVideoEncoderProxy_h_
+
+#include "nsTArray.h"
+#include "gmp-video-encode.h"
+#include "gmp-video-frame-i420.h"
+#include "gmp-video-frame-encoded.h"
+
+#include "GMPCallbackBase.h"
+#include "GMPUtils.h"
+
+class GMPVideoEncoderCallbackProxy : public GMPCallbackBase {
+ public:
+ virtual ~GMPVideoEncoderCallbackProxy() = default;
+ virtual void Encoded(GMPVideoEncodedFrame* aEncodedFrame,
+ const nsTArray<uint8_t>& aCodecSpecificInfo) = 0;
+ virtual void Error(GMPErr aError) = 0;
+};
+
+// A proxy to GMPVideoEncoder in the child process.
+// GMPVideoEncoderParent exposes this to users the GMP.
+// This enables Gecko to pass nsTArrays to the child GMP and avoid
+// an extra copy when doing so.
+
+// The consumer must call Close() when done with the codec, or when
+// Terminated() is called by the GMP plugin indicating an abnormal shutdown
+// of the underlying plugin. After calling Close(), the consumer must
+// not access this again.
+
+// This interface is not thread-safe and must only be used from GMPThread.
+class GMPVideoEncoderProxy {
+ public:
+ virtual GMPErr InitEncode(const GMPVideoCodec& aCodecSettings,
+ const nsTArray<uint8_t>& aCodecSpecific,
+ GMPVideoEncoderCallbackProxy* aCallback,
+ int32_t aNumberOfCores,
+ uint32_t aMaxPayloadSize) = 0;
+ virtual GMPErr Encode(mozilla::GMPUniquePtr<GMPVideoi420Frame> aInputFrame,
+ const nsTArray<uint8_t>& aCodecSpecificInfo,
+ const nsTArray<GMPVideoFrameType>& aFrameTypes) = 0;
+ virtual GMPErr SetChannelParameters(uint32_t aPacketLoss, uint32_t aRTT) = 0;
+ virtual GMPErr SetRates(uint32_t aNewBitRate, uint32_t aFrameRate) = 0;
+ virtual GMPErr SetPeriodicKeyFrames(bool aEnable) = 0;
+ virtual uint32_t GetPluginId() const = 0;
+
+ // Call to tell GMP/plugin the consumer will no longer use this
+ // interface/codec.
+ virtual void Close() = 0;
+};
+
+#endif // GMPVideoEncoderProxy_h_
diff --git a/dom/media/gmp/GMPVideoHost.cpp b/dom/media/gmp/GMPVideoHost.cpp
new file mode 100644
index 0000000000..7ed6c5abc2
--- /dev/null
+++ b/dom/media/gmp/GMPVideoHost.cpp
@@ -0,0 +1,94 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GMPVideoHost.h"
+#include "mozilla/Assertions.h"
+#include "GMPVideoi420FrameImpl.h"
+#include "GMPVideoEncodedFrameImpl.h"
+
+namespace mozilla::gmp {
+
+GMPVideoHostImpl::GMPVideoHostImpl(GMPSharedMemManager* aSharedMemMgr)
+ : mSharedMemMgr(aSharedMemMgr) {}
+
+GMPVideoHostImpl::~GMPVideoHostImpl() = default;
+
+GMPErr GMPVideoHostImpl::CreateFrame(GMPVideoFrameFormat aFormat,
+ GMPVideoFrame** aFrame) {
+ if (!mSharedMemMgr) {
+ return GMPGenericErr;
+ }
+
+ if (!aFrame) {
+ return GMPGenericErr;
+ }
+ *aFrame = nullptr;
+
+ switch (aFormat) {
+ case kGMPI420VideoFrame:
+ *aFrame = new GMPVideoi420FrameImpl(this);
+ return GMPNoErr;
+ case kGMPEncodedVideoFrame:
+ *aFrame = new GMPVideoEncodedFrameImpl(this);
+ return GMPNoErr;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unknown frame format!");
+ }
+
+ return GMPGenericErr;
+}
+
+GMPErr GMPVideoHostImpl::CreatePlane(GMPPlane** aPlane) {
+ if (!mSharedMemMgr) {
+ return GMPGenericErr;
+ }
+
+ if (!aPlane) {
+ return GMPGenericErr;
+ }
+ *aPlane = nullptr;
+
+ auto p = new GMPPlaneImpl(this);
+
+ *aPlane = p;
+
+ return GMPNoErr;
+}
+
+GMPSharedMemManager* GMPVideoHostImpl::SharedMemMgr() { return mSharedMemMgr; }
+
+// XXX This should merge with ActorDestroyed
+void GMPVideoHostImpl::DoneWithAPI() { ActorDestroyed(); }
+
+void GMPVideoHostImpl::ActorDestroyed() {
+ for (uint32_t i = mPlanes.Length(); i > 0; i--) {
+ mPlanes[i - 1]->DoneWithAPI();
+ mPlanes.RemoveElementAt(i - 1);
+ }
+ for (uint32_t i = mEncodedFrames.Length(); i > 0; i--) {
+ mEncodedFrames[i - 1]->DoneWithAPI();
+ mEncodedFrames.RemoveElementAt(i - 1);
+ }
+ mSharedMemMgr = nullptr;
+}
+
+void GMPVideoHostImpl::PlaneCreated(GMPPlaneImpl* aPlane) {
+ mPlanes.AppendElement(aPlane);
+}
+
+void GMPVideoHostImpl::PlaneDestroyed(GMPPlaneImpl* aPlane) {
+ MOZ_ALWAYS_TRUE(mPlanes.RemoveElement(aPlane));
+}
+
+void GMPVideoHostImpl::EncodedFrameCreated(
+ GMPVideoEncodedFrameImpl* aEncodedFrame) {
+ mEncodedFrames.AppendElement(aEncodedFrame);
+}
+
+void GMPVideoHostImpl::EncodedFrameDestroyed(GMPVideoEncodedFrameImpl* aFrame) {
+ MOZ_ALWAYS_TRUE(mEncodedFrames.RemoveElement(aFrame));
+}
+
+} // namespace mozilla::gmp
diff --git a/dom/media/gmp/GMPVideoHost.h b/dom/media/gmp/GMPVideoHost.h
new file mode 100644
index 0000000000..8b75ff28fe
--- /dev/null
+++ b/dom/media/gmp/GMPVideoHost.h
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GMPVideoHost_h_
+#define GMPVideoHost_h_
+
+#include "gmp-video-host.h"
+#include "gmp-video-plane.h"
+#include "gmp-video-frame.h"
+#include "nsTArray.h"
+
+namespace mozilla::gmp {
+
+class GMPSharedMemManager;
+class GMPPlaneImpl;
+class GMPVideoEncodedFrameImpl;
+
+class GMPVideoHostImpl : public GMPVideoHost {
+ public:
+ explicit GMPVideoHostImpl(GMPSharedMemManager* aSharedMemMgr);
+ virtual ~GMPVideoHostImpl();
+
+ // Used for shared memory allocation and deallocation.
+ GMPSharedMemManager* SharedMemMgr();
+ void DoneWithAPI();
+ void ActorDestroyed();
+ void PlaneCreated(GMPPlaneImpl* aPlane);
+ void PlaneDestroyed(GMPPlaneImpl* aPlane);
+ void EncodedFrameCreated(GMPVideoEncodedFrameImpl* aEncodedFrame);
+ void EncodedFrameDestroyed(GMPVideoEncodedFrameImpl* aFrame);
+
+ // GMPVideoHost
+ GMPErr CreateFrame(GMPVideoFrameFormat aFormat,
+ GMPVideoFrame** aFrame) override;
+ GMPErr CreatePlane(GMPPlane** aPlane) override;
+
+ private:
+ // All shared memory allocations have to be made by an IPDL actor.
+ // This is a reference to the owning actor. If this reference is
+ // null then the actor has died and all allocations must fail.
+ GMPSharedMemManager* mSharedMemMgr;
+
+ // We track all of these things because they need to handle further
+ // allocations through us and we need to notify them when they
+ // can't use us any more.
+ nsTArray<GMPPlaneImpl*> mPlanes;
+ nsTArray<GMPVideoEncodedFrameImpl*> mEncodedFrames;
+};
+
+} // namespace mozilla::gmp
+
+#endif // GMPVideoHost_h_
diff --git a/dom/media/gmp/GMPVideoPlaneImpl.cpp b/dom/media/gmp/GMPVideoPlaneImpl.cpp
new file mode 100644
index 0000000000..6c24b5b4a6
--- /dev/null
+++ b/dom/media/gmp/GMPVideoPlaneImpl.cpp
@@ -0,0 +1,179 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GMPVideoPlaneImpl.h"
+#include "mozilla/gmp/GMPTypes.h"
+#include "GMPVideoHost.h"
+#include "GMPSharedMemManager.h"
+
+namespace mozilla::gmp {
+
+GMPPlaneImpl::GMPPlaneImpl(GMPVideoHostImpl* aHost)
+ : mSize(0), mStride(0), mHost(aHost) {
+ MOZ_ASSERT(mHost);
+ mHost->PlaneCreated(this);
+}
+
+GMPPlaneImpl::GMPPlaneImpl(const GMPPlaneData& aPlaneData,
+ GMPVideoHostImpl* aHost)
+ : mBuffer(aPlaneData.mBuffer()),
+ mSize(aPlaneData.mSize()),
+ mStride(aPlaneData.mStride()),
+ mHost(aHost) {
+ MOZ_ASSERT(mHost);
+ mHost->PlaneCreated(this);
+}
+
+GMPPlaneImpl::~GMPPlaneImpl() {
+ DestroyBuffer();
+ if (mHost) {
+ mHost->PlaneDestroyed(this);
+ }
+}
+
+void GMPPlaneImpl::DoneWithAPI() {
+ DestroyBuffer();
+
+ // Do this after destroying the buffer because destruction
+ // involves deallocation, which requires a host.
+ mHost = nullptr;
+}
+
+void GMPPlaneImpl::ActorDestroyed() {
+ // Simply clear out Shmem reference, do not attempt to
+ // properly free it. It has already been freed.
+ mBuffer = ipc::Shmem();
+ // No more host.
+ mHost = nullptr;
+}
+
+bool GMPPlaneImpl::InitPlaneData(GMPPlaneData& aPlaneData) {
+ aPlaneData.mBuffer() = mBuffer;
+ aPlaneData.mSize() = mSize;
+ aPlaneData.mStride() = mStride;
+
+ // This method is called right before Shmem is sent to another process.
+ // We need to effectively zero out our member copy so that we don't
+ // try to delete memory we don't own later.
+ mBuffer = ipc::Shmem();
+
+ return true;
+}
+
+GMPErr GMPPlaneImpl::MaybeResize(int32_t aNewSize) {
+ if (aNewSize <= AllocatedSize()) {
+ return GMPNoErr;
+ }
+
+ if (!mHost) {
+ return GMPGenericErr;
+ }
+
+ ipc::Shmem new_mem;
+ if (!mHost->SharedMemMgr()->MgrAllocShmem(GMPSharedMem::kGMPFrameData,
+ aNewSize, &new_mem) ||
+ !new_mem.get<uint8_t>()) {
+ return GMPAllocErr;
+ }
+
+ if (mBuffer.IsReadable()) {
+ memcpy(new_mem.get<uint8_t>(), Buffer(), mSize);
+ }
+
+ DestroyBuffer();
+
+ mBuffer = new_mem;
+
+ return GMPNoErr;
+}
+
+void GMPPlaneImpl::DestroyBuffer() {
+ if (mHost && mBuffer.IsWritable()) {
+ mHost->SharedMemMgr()->MgrDeallocShmem(GMPSharedMem::kGMPFrameData,
+ mBuffer);
+ }
+ mBuffer = ipc::Shmem();
+}
+
+GMPErr GMPPlaneImpl::CreateEmptyPlane(int32_t aAllocatedSize, int32_t aStride,
+ int32_t aPlaneSize) {
+ if (aAllocatedSize < 1 || aStride < 1 || aPlaneSize < 1) {
+ return GMPGenericErr;
+ }
+
+ GMPErr err = MaybeResize(aAllocatedSize);
+ if (err != GMPNoErr) {
+ return err;
+ }
+
+ mSize = aPlaneSize;
+ mStride = aStride;
+
+ return GMPNoErr;
+}
+
+GMPErr GMPPlaneImpl::Copy(const GMPPlane& aPlane) {
+ auto& planeimpl = static_cast<const GMPPlaneImpl&>(aPlane);
+
+ GMPErr err = MaybeResize(planeimpl.mSize);
+ if (err != GMPNoErr) {
+ return err;
+ }
+
+ if (planeimpl.Buffer() && planeimpl.mSize > 0) {
+ memcpy(Buffer(), planeimpl.Buffer(), mSize);
+ }
+
+ mSize = planeimpl.mSize;
+ mStride = planeimpl.mStride;
+
+ return GMPNoErr;
+}
+
+GMPErr GMPPlaneImpl::Copy(int32_t aSize, int32_t aStride,
+ const uint8_t* aBuffer) {
+ GMPErr err = MaybeResize(aSize);
+ if (err != GMPNoErr) {
+ return err;
+ }
+
+ if (aBuffer && aSize > 0) {
+ memcpy(Buffer(), aBuffer, aSize);
+ }
+
+ mSize = aSize;
+ mStride = aStride;
+
+ return GMPNoErr;
+}
+
+void GMPPlaneImpl::Swap(GMPPlane& aPlane) {
+ auto& planeimpl = static_cast<GMPPlaneImpl&>(aPlane);
+
+ std::swap(mStride, planeimpl.mStride);
+ std::swap(mSize, planeimpl.mSize);
+ std::swap(mBuffer, planeimpl.mBuffer);
+}
+
+int32_t GMPPlaneImpl::AllocatedSize() const {
+ if (mBuffer.IsWritable()) {
+ return mBuffer.Size<uint8_t>();
+ }
+ return 0;
+}
+
+void GMPPlaneImpl::ResetSize() { mSize = 0; }
+
+bool GMPPlaneImpl::IsZeroSize() const { return (mSize == 0); }
+
+int32_t GMPPlaneImpl::Stride() const { return mStride; }
+
+const uint8_t* GMPPlaneImpl::Buffer() const { return mBuffer.get<uint8_t>(); }
+
+uint8_t* GMPPlaneImpl::Buffer() { return mBuffer.get<uint8_t>(); }
+
+void GMPPlaneImpl::Destroy() { delete this; }
+
+} // namespace mozilla::gmp
diff --git a/dom/media/gmp/GMPVideoPlaneImpl.h b/dom/media/gmp/GMPVideoPlaneImpl.h
new file mode 100644
index 0000000000..d0c1de1cd3
--- /dev/null
+++ b/dom/media/gmp/GMPVideoPlaneImpl.h
@@ -0,0 +1,61 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GMPVideoPlaneImpl_h_
+#define GMPVideoPlaneImpl_h_
+
+#include "gmp-video-plane.h"
+#include "mozilla/ipc/Shmem.h"
+
+namespace mozilla::gmp {
+
+class GMPVideoHostImpl;
+class GMPPlaneData;
+
+class GMPPlaneImpl : public GMPPlane {
+ friend struct IPC::ParamTraits<mozilla::gmp::GMPPlaneImpl>;
+
+ public:
+ explicit GMPPlaneImpl(GMPVideoHostImpl* aHost);
+ GMPPlaneImpl(const GMPPlaneData& aPlaneData, GMPVideoHostImpl* aHost);
+ virtual ~GMPPlaneImpl();
+
+ // This is called during a normal destroy sequence, which is
+ // when a consumer is finished or during XPCOM shutdown.
+ void DoneWithAPI();
+ // This is called when something has gone wrong - specicifically,
+ // a child process has crashed. Does not attempt to release Shmem,
+ // as the Shmem has already been released.
+ void ActorDestroyed();
+
+ bool InitPlaneData(GMPPlaneData& aPlaneData);
+
+ // GMPPlane
+ GMPErr CreateEmptyPlane(int32_t aAllocatedSize, int32_t aStride,
+ int32_t aPlaneSize) override;
+ GMPErr Copy(const GMPPlane& aPlane) override;
+ GMPErr Copy(int32_t aSize, int32_t aStride, const uint8_t* aBuffer) override;
+ void Swap(GMPPlane& aPlane) override;
+ int32_t AllocatedSize() const override;
+ void ResetSize() override;
+ bool IsZeroSize() const override;
+ int32_t Stride() const override;
+ const uint8_t* Buffer() const override;
+ uint8_t* Buffer() override;
+ void Destroy() override;
+
+ private:
+ GMPErr MaybeResize(int32_t aNewSize);
+ void DestroyBuffer();
+
+ ipc::Shmem mBuffer;
+ int32_t mSize;
+ int32_t mStride;
+ GMPVideoHostImpl* mHost;
+};
+
+} // namespace mozilla::gmp
+
+#endif // GMPVideoPlaneImpl_h_
diff --git a/dom/media/gmp/GMPVideoi420FrameImpl.cpp b/dom/media/gmp/GMPVideoi420FrameImpl.cpp
new file mode 100644
index 0000000000..1ca3e8123f
--- /dev/null
+++ b/dom/media/gmp/GMPVideoi420FrameImpl.cpp
@@ -0,0 +1,328 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GMPVideoi420FrameImpl.h"
+#include "mozilla/gmp/GMPTypes.h"
+#include "mozilla/CheckedInt.h"
+
+namespace mozilla::gmp {
+
+GMPVideoi420FrameImpl::GMPVideoi420FrameImpl(GMPVideoHostImpl* aHost)
+ : mYPlane(aHost),
+ mUPlane(aHost),
+ mVPlane(aHost),
+ mWidth(0),
+ mHeight(0),
+ mTimestamp(0ll),
+ mDuration(0ll) {
+ MOZ_ASSERT(aHost);
+}
+
+GMPVideoi420FrameImpl::GMPVideoi420FrameImpl(
+ const GMPVideoi420FrameData& aFrameData, GMPVideoHostImpl* aHost)
+ : mYPlane(aFrameData.mYPlane(), aHost),
+ mUPlane(aFrameData.mUPlane(), aHost),
+ mVPlane(aFrameData.mVPlane(), aHost),
+ mWidth(aFrameData.mWidth()),
+ mHeight(aFrameData.mHeight()),
+ mTimestamp(aFrameData.mTimestamp()),
+ mUpdatedTimestamp(aFrameData.mUpdatedTimestamp()),
+ mDuration(aFrameData.mDuration()) {
+ MOZ_ASSERT(aHost);
+}
+
+GMPVideoi420FrameImpl::~GMPVideoi420FrameImpl() = default;
+
+bool GMPVideoi420FrameImpl::InitFrameData(GMPVideoi420FrameData& aFrameData) {
+ mYPlane.InitPlaneData(aFrameData.mYPlane());
+ mUPlane.InitPlaneData(aFrameData.mUPlane());
+ mVPlane.InitPlaneData(aFrameData.mVPlane());
+ aFrameData.mWidth() = mWidth;
+ aFrameData.mHeight() = mHeight;
+ aFrameData.mTimestamp() = mTimestamp;
+ aFrameData.mUpdatedTimestamp() = mUpdatedTimestamp;
+ aFrameData.mDuration() = mDuration;
+ return true;
+}
+
+GMPVideoFrameFormat GMPVideoi420FrameImpl::GetFrameFormat() {
+ return kGMPI420VideoFrame;
+}
+
+void GMPVideoi420FrameImpl::Destroy() { delete this; }
+
+/* static */
+bool GMPVideoi420FrameImpl::CheckFrameData(
+ const GMPVideoi420FrameData& aFrameData) {
+ // We may be passed the "wrong" shmem (one smaller than the actual size).
+ // This implies a bug or serious error on the child size. Ignore this frame
+ // if so. Note: Size() greater than expected is also an error, but with no
+ // negative consequences
+ int32_t half_width = (aFrameData.mWidth() + 1) / 2;
+ if ((aFrameData.mYPlane().mStride() <= 0) ||
+ (aFrameData.mYPlane().mSize() <= 0) ||
+ (aFrameData.mUPlane().mStride() <= 0) ||
+ (aFrameData.mUPlane().mSize() <= 0) ||
+ (aFrameData.mVPlane().mStride() <= 0) ||
+ (aFrameData.mVPlane().mSize() <= 0) ||
+ (aFrameData.mYPlane().mSize() >
+ (int32_t)aFrameData.mYPlane().mBuffer().Size<uint8_t>()) ||
+ (aFrameData.mUPlane().mSize() >
+ (int32_t)aFrameData.mUPlane().mBuffer().Size<uint8_t>()) ||
+ (aFrameData.mVPlane().mSize() >
+ (int32_t)aFrameData.mVPlane().mBuffer().Size<uint8_t>()) ||
+ (aFrameData.mYPlane().mStride() < aFrameData.mWidth()) ||
+ (aFrameData.mUPlane().mStride() < half_width) ||
+ (aFrameData.mVPlane().mStride() < half_width) ||
+ (aFrameData.mYPlane().mSize() <
+ aFrameData.mYPlane().mStride() * aFrameData.mHeight()) ||
+ (aFrameData.mUPlane().mSize() <
+ aFrameData.mUPlane().mStride() * ((aFrameData.mHeight() + 1) / 2)) ||
+ (aFrameData.mVPlane().mSize() <
+ aFrameData.mVPlane().mStride() * ((aFrameData.mHeight() + 1) / 2))) {
+ return false;
+ }
+ return true;
+}
+
+bool GMPVideoi420FrameImpl::CheckDimensions(int32_t aWidth, int32_t aHeight,
+ int32_t aStride_y,
+ int32_t aStride_u,
+ int32_t aStride_v) {
+ int32_t half_width = (aWidth + 1) / 2;
+ if (aWidth < 1 || aHeight < 1 || aStride_y < aWidth ||
+ aStride_u < half_width || aStride_v < half_width ||
+ !(CheckedInt<int32_t>(aHeight) * aStride_y +
+ ((CheckedInt<int32_t>(aHeight) + 1) / 2) *
+ (CheckedInt<int32_t>(aStride_u) + aStride_v))
+ .isValid()) {
+ return false;
+ }
+ return true;
+}
+
+const GMPPlaneImpl* GMPVideoi420FrameImpl::GetPlane(GMPPlaneType aType) const {
+ switch (aType) {
+ case kGMPYPlane:
+ return &mYPlane;
+ case kGMPUPlane:
+ return &mUPlane;
+ case kGMPVPlane:
+ return &mVPlane;
+ default:
+ MOZ_CRASH("Unknown plane type!");
+ }
+ return nullptr;
+}
+
+GMPPlaneImpl* GMPVideoi420FrameImpl::GetPlane(GMPPlaneType aType) {
+ switch (aType) {
+ case kGMPYPlane:
+ return &mYPlane;
+ case kGMPUPlane:
+ return &mUPlane;
+ case kGMPVPlane:
+ return &mVPlane;
+ default:
+ MOZ_CRASH("Unknown plane type!");
+ }
+ return nullptr;
+}
+
+GMPErr GMPVideoi420FrameImpl::CreateEmptyFrame(int32_t aWidth, int32_t aHeight,
+ int32_t aStride_y,
+ int32_t aStride_u,
+ int32_t aStride_v) {
+ if (!CheckDimensions(aWidth, aHeight, aStride_y, aStride_u, aStride_v)) {
+ return GMPGenericErr;
+ }
+
+ int32_t size_y = aStride_y * aHeight;
+ int32_t half_height = (aHeight + 1) / 2;
+ int32_t size_u = aStride_u * half_height;
+ int32_t size_v = aStride_v * half_height;
+
+ GMPErr err = mYPlane.CreateEmptyPlane(size_y, aStride_y, size_y);
+ if (err != GMPNoErr) {
+ return err;
+ }
+ err = mUPlane.CreateEmptyPlane(size_u, aStride_u, size_u);
+ if (err != GMPNoErr) {
+ return err;
+ }
+ err = mVPlane.CreateEmptyPlane(size_v, aStride_v, size_v);
+ if (err != GMPNoErr) {
+ return err;
+ }
+
+ mWidth = aWidth;
+ mHeight = aHeight;
+ mTimestamp = 0ll;
+ mUpdatedTimestamp.reset();
+ mDuration = 0ll;
+
+ return GMPNoErr;
+}
+
+GMPErr GMPVideoi420FrameImpl::CreateFrame(
+ int32_t aSize_y, const uint8_t* aBuffer_y, int32_t aSize_u,
+ const uint8_t* aBuffer_u, int32_t aSize_v, const uint8_t* aBuffer_v,
+ int32_t aWidth, int32_t aHeight, int32_t aStride_y, int32_t aStride_u,
+ int32_t aStride_v) {
+ MOZ_ASSERT(aBuffer_y);
+ MOZ_ASSERT(aBuffer_u);
+ MOZ_ASSERT(aBuffer_v);
+
+ if (aSize_y < 1 || aSize_u < 1 || aSize_v < 1) {
+ return GMPGenericErr;
+ }
+
+ if (!CheckDimensions(aWidth, aHeight, aStride_y, aStride_u, aStride_v)) {
+ return GMPGenericErr;
+ }
+
+ GMPErr err = mYPlane.Copy(aSize_y, aStride_y, aBuffer_y);
+ if (err != GMPNoErr) {
+ return err;
+ }
+ err = mUPlane.Copy(aSize_u, aStride_u, aBuffer_u);
+ if (err != GMPNoErr) {
+ return err;
+ }
+ err = mVPlane.Copy(aSize_v, aStride_v, aBuffer_v);
+ if (err != GMPNoErr) {
+ return err;
+ }
+
+ mWidth = aWidth;
+ mHeight = aHeight;
+
+ return GMPNoErr;
+}
+
+GMPErr GMPVideoi420FrameImpl::CopyFrame(const GMPVideoi420Frame& aFrame) {
+ auto& f = static_cast<const GMPVideoi420FrameImpl&>(aFrame);
+
+ GMPErr err = mYPlane.Copy(f.mYPlane);
+ if (err != GMPNoErr) {
+ return err;
+ }
+
+ err = mUPlane.Copy(f.mUPlane);
+ if (err != GMPNoErr) {
+ return err;
+ }
+
+ err = mVPlane.Copy(f.mVPlane);
+ if (err != GMPNoErr) {
+ return err;
+ }
+
+ mWidth = f.mWidth;
+ mHeight = f.mHeight;
+ mTimestamp = f.mTimestamp;
+ mUpdatedTimestamp = f.mUpdatedTimestamp;
+ mDuration = f.mDuration;
+
+ return GMPNoErr;
+}
+
+void GMPVideoi420FrameImpl::SwapFrame(GMPVideoi420Frame* aFrame) {
+ auto f = static_cast<GMPVideoi420FrameImpl*>(aFrame);
+ mYPlane.Swap(f->mYPlane);
+ mUPlane.Swap(f->mUPlane);
+ mVPlane.Swap(f->mVPlane);
+ std::swap(mWidth, f->mWidth);
+ std::swap(mHeight, f->mHeight);
+ std::swap(mTimestamp, f->mTimestamp);
+ std::swap(mUpdatedTimestamp, f->mUpdatedTimestamp);
+ std::swap(mDuration, f->mDuration);
+}
+
+uint8_t* GMPVideoi420FrameImpl::Buffer(GMPPlaneType aType) {
+ GMPPlane* p = GetPlane(aType);
+ if (p) {
+ return p->Buffer();
+ }
+ return nullptr;
+}
+
+const uint8_t* GMPVideoi420FrameImpl::Buffer(GMPPlaneType aType) const {
+ const GMPPlane* p = GetPlane(aType);
+ if (p) {
+ return p->Buffer();
+ }
+ return nullptr;
+}
+
+int32_t GMPVideoi420FrameImpl::AllocatedSize(GMPPlaneType aType) const {
+ const GMPPlane* p = GetPlane(aType);
+ if (p) {
+ return p->AllocatedSize();
+ }
+ return -1;
+}
+
+int32_t GMPVideoi420FrameImpl::Stride(GMPPlaneType aType) const {
+ const GMPPlane* p = GetPlane(aType);
+ if (p) {
+ return p->Stride();
+ }
+ return -1;
+}
+
+GMPErr GMPVideoi420FrameImpl::SetWidth(int32_t aWidth) {
+ if (!CheckDimensions(aWidth, mHeight, mYPlane.Stride(), mUPlane.Stride(),
+ mVPlane.Stride())) {
+ return GMPGenericErr;
+ }
+ mWidth = aWidth;
+ return GMPNoErr;
+}
+
+GMPErr GMPVideoi420FrameImpl::SetHeight(int32_t aHeight) {
+ if (!CheckDimensions(mWidth, aHeight, mYPlane.Stride(), mUPlane.Stride(),
+ mVPlane.Stride())) {
+ return GMPGenericErr;
+ }
+ mHeight = aHeight;
+ return GMPNoErr;
+}
+
+int32_t GMPVideoi420FrameImpl::Width() const { return mWidth; }
+
+int32_t GMPVideoi420FrameImpl::Height() const { return mHeight; }
+
+void GMPVideoi420FrameImpl::SetTimestamp(uint64_t aTimestamp) {
+ mTimestamp = aTimestamp;
+}
+
+uint64_t GMPVideoi420FrameImpl::Timestamp() const { return mTimestamp; }
+
+void GMPVideoi420FrameImpl::SetUpdatedTimestamp(uint64_t aTimestamp) {
+ mUpdatedTimestamp = Some(aTimestamp);
+}
+
+uint64_t GMPVideoi420FrameImpl::UpdatedTimestamp() const {
+ return mUpdatedTimestamp ? *mUpdatedTimestamp : mTimestamp;
+}
+
+void GMPVideoi420FrameImpl::SetDuration(uint64_t aDuration) {
+ mDuration = aDuration;
+}
+
+uint64_t GMPVideoi420FrameImpl::Duration() const { return mDuration; }
+
+bool GMPVideoi420FrameImpl::IsZeroSize() const {
+ return (mYPlane.IsZeroSize() && mUPlane.IsZeroSize() && mVPlane.IsZeroSize());
+}
+
+void GMPVideoi420FrameImpl::ResetSize() {
+ mYPlane.ResetSize();
+ mUPlane.ResetSize();
+ mVPlane.ResetSize();
+}
+
+} // namespace mozilla::gmp
diff --git a/dom/media/gmp/GMPVideoi420FrameImpl.h b/dom/media/gmp/GMPVideoi420FrameImpl.h
new file mode 100644
index 0000000000..53377beb8d
--- /dev/null
+++ b/dom/media/gmp/GMPVideoi420FrameImpl.h
@@ -0,0 +1,80 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GMPVideoi420FrameImpl_h_
+#define GMPVideoi420FrameImpl_h_
+
+#include "gmp-video-frame-i420.h"
+#include "mozilla/ipc/Shmem.h"
+#include "GMPVideoPlaneImpl.h"
+#include "mozilla/Maybe.h"
+
+namespace mozilla::gmp {
+
+class GMPVideoi420FrameData;
+
+class GMPVideoi420FrameImpl : public GMPVideoi420Frame {
+ friend struct IPC::ParamTraits<mozilla::gmp::GMPVideoi420FrameImpl>;
+
+ public:
+ explicit GMPVideoi420FrameImpl(GMPVideoHostImpl* aHost);
+ GMPVideoi420FrameImpl(const GMPVideoi420FrameData& aFrameData,
+ GMPVideoHostImpl* aHost);
+ virtual ~GMPVideoi420FrameImpl();
+
+ static bool CheckFrameData(const GMPVideoi420FrameData& aFrameData);
+
+ bool InitFrameData(GMPVideoi420FrameData& aFrameData);
+ const GMPPlaneImpl* GetPlane(GMPPlaneType aType) const;
+ GMPPlaneImpl* GetPlane(GMPPlaneType aType);
+
+ // GMPVideoFrame
+ GMPVideoFrameFormat GetFrameFormat() override;
+ void Destroy() override;
+
+ // GMPVideoi420Frame
+ GMPErr CreateEmptyFrame(int32_t aWidth, int32_t aHeight, int32_t aStride_y,
+ int32_t aStride_u, int32_t aStride_v) override;
+ GMPErr CreateFrame(int32_t aSize_y, const uint8_t* aBuffer_y, int32_t aSize_u,
+ const uint8_t* aBuffer_u, int32_t aSize_v,
+ const uint8_t* aBuffer_v, int32_t aWidth, int32_t aHeight,
+ int32_t aStride_y, int32_t aStride_u,
+ int32_t aStride_v) override;
+ GMPErr CopyFrame(const GMPVideoi420Frame& aFrame) override;
+ void SwapFrame(GMPVideoi420Frame* aFrame) override;
+ uint8_t* Buffer(GMPPlaneType aType) override;
+ const uint8_t* Buffer(GMPPlaneType aType) const override;
+ int32_t AllocatedSize(GMPPlaneType aType) const override;
+ int32_t Stride(GMPPlaneType aType) const override;
+ GMPErr SetWidth(int32_t aWidth) override;
+ GMPErr SetHeight(int32_t aHeight) override;
+ int32_t Width() const override;
+ int32_t Height() const override;
+ void SetTimestamp(uint64_t aTimestamp) override;
+ uint64_t Timestamp() const override;
+ void SetUpdatedTimestamp(uint64_t aTimestamp) override;
+ uint64_t UpdatedTimestamp() const override;
+ void SetDuration(uint64_t aDuration) override;
+ uint64_t Duration() const override;
+ bool IsZeroSize() const override;
+ void ResetSize() override;
+
+ private:
+ bool CheckDimensions(int32_t aWidth, int32_t aHeight, int32_t aStride_y,
+ int32_t aStride_u, int32_t aStride_v);
+
+ GMPPlaneImpl mYPlane;
+ GMPPlaneImpl mUPlane;
+ GMPPlaneImpl mVPlane;
+ int32_t mWidth;
+ int32_t mHeight;
+ uint64_t mTimestamp;
+ Maybe<uint64_t> mUpdatedTimestamp;
+ uint64_t mDuration;
+};
+
+} // namespace mozilla::gmp
+
+#endif // GMPVideoi420FrameImpl_h_
diff --git a/dom/media/gmp/PChromiumCDM.ipdl b/dom/media/gmp/PChromiumCDM.ipdl
new file mode 100644
index 0000000000..2355839c17
--- /dev/null
+++ b/dom/media/gmp/PChromiumCDM.ipdl
@@ -0,0 +1,129 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PGMPContent;
+include GMPTypes;
+
+include "GMPMessageUtils.h";
+include "ChromiumCDMParent.h";
+
+using cdm::HdcpVersion from "GMPSanitizedExports.h";
+
+namespace mozilla {
+namespace gmp {
+
+[ChildImpl=virtual, ParentImpl="ChromiumCDMParent"]
+async protocol PChromiumCDM
+{
+ manager PGMPContent;
+child:
+
+ // cdm::ContentDecryptionModule_10
+ async Init(bool aAllowDistinctiveIdentifier,
+ bool aAllowPersistentState) returns (bool aSuccess);
+
+ async GetStatusForPolicy(uint32_t aPromiseId,
+ HdcpVersion aMinHdcpVersion);
+
+ async SetServerCertificate(uint32_t aPromiseId,
+ uint8_t[] aServerCert);
+
+ async CreateSessionAndGenerateRequest(uint32_t aPromiseId,
+ uint32_t aSessionType,
+ uint32_t aInitDataType,
+ uint8_t[] aInitData);
+
+ async LoadSession(uint32_t aPromiseId,
+ uint32_t aSessionType,
+ nsCString aSessionId);
+
+ async UpdateSession(uint32_t aPromiseId,
+ nsCString aSessionId,
+ uint8_t[] aResponse);
+
+ async CloseSession(uint32_t aPromiseId,
+ nsCString aSessionId);
+
+ async RemoveSession(uint32_t aPromiseId,
+ nsCString aSessionId);
+
+ // This is called after the parent has verified the protection status and
+ // round tripped the call back to the child.
+ async CompleteQueryOutputProtectionStatus(bool aSuccess,
+ uint32_t aLinkMask,
+ uint32_t aProtectionMask);
+
+ async Decrypt(uint32_t aId, CDMInputBuffer aBuffer);
+
+ async InitializeVideoDecoder(CDMVideoDecoderConfig aConfig);
+
+ async DeinitializeVideoDecoder();
+
+ async ResetVideoDecoder();
+
+ async DecryptAndDecodeFrame(CDMInputBuffer aBuffer);
+
+ async Drain();
+
+ async Destroy();
+
+ async GiveBuffer(Shmem aShmem);
+
+ async PurgeShmems();
+
+
+parent:
+ async __delete__();
+
+ // cdm::Host_10
+ async OnResolvePromiseWithKeyStatus(uint32_t aPromiseId, uint32_t aKeyStatus);
+
+ async OnResolveNewSessionPromise(uint32_t aPromiseId, nsCString aSessionId);
+
+ async OnResolvePromise(uint32_t aPromiseId);
+
+ async OnRejectPromise(uint32_t aPromiseId,
+ uint32_t aException,
+ uint32_t aSystemCode,
+ nsCString aErrorMessage);
+
+ async OnSessionMessage(nsCString aSessionId,
+ uint32_t aMessageType,
+ uint8_t[] aMessage);
+
+ async OnSessionKeysChange(nsCString aSessionId,
+ CDMKeyInformation[] aKeysInfo);
+
+ async OnExpirationChange(nsCString aSessionId,
+ double aSecondsSinceEpoch);
+
+ async OnSessionClosed(nsCString aSessionId);
+
+ async OnQueryOutputProtectionStatus();
+
+ async ResolveLoadSessionPromise(uint32_t aPromiseId, bool aSuccessful);
+
+ // Return values of cdm::ContentDecryptionModule_10::Decrypt
+ async Decrypted(uint32_t aId, uint32_t aStatus, Shmem aDecryptedData);
+ async DecryptFailed(uint32_t aId, uint32_t aStatus);
+
+ async OnDecoderInitDone(uint32_t aStatus);
+
+ // Return values of cdm::ContentDecryptionModule_10::DecryptAndDecodeFrame
+ async DecodedShmem(CDMVideoFrame aFrame, Shmem aData);
+ async DecodedData(CDMVideoFrame aFrame, uint8_t[] aData);
+ async DecodeFailed(uint32_t aStatus);
+
+ async ResetVideoDecoderComplete();
+
+ async DrainComplete();
+
+ async Shutdown();
+
+ async IncreaseShmemPoolSize();
+};
+
+} // namespace gmp
+} // namespace mozilla
diff --git a/dom/media/gmp/PGMP.ipdl b/dom/media/gmp/PGMP.ipdl
new file mode 100644
index 0000000000..076592f49b
--- /dev/null
+++ b/dom/media/gmp/PGMP.ipdl
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PGMPContent;
+include protocol PGMPTimer;
+include protocol PGMPStorage;
+include protocol PProfiler;
+
+include "mozilla/ipc/ByteBufUtils.h";
+include "GMPParent.h";
+include "GMPChild.h";
+
+using mozilla::dom::NativeThreadId from "mozilla/dom/NativeThreadId.h";
+
+namespace mozilla {
+namespace gmp {
+
+[NeedsOtherPid, NestedUpTo=inside_sync, ChildImpl="GMPChild", ParentImpl="GMPParent"]
+sync protocol PGMP
+{
+ manages PGMPTimer;
+ manages PGMPStorage;
+
+parent:
+ async InitCrashReporter(NativeThreadId threadId);
+ async PGMPTimer();
+ async PGMPStorage();
+
+ async PGMPContentChildDestroyed();
+
+ // Sent from time-to-time to limit the amount of telemetry vulnerable to loss
+ // Buffer contains bincoded Rust structs.
+ // https://firefox-source-docs.mozilla.org/toolkit/components/glean/dev/ipc.html
+ async FOGData(ByteBuf buf);
+
+child:
+ async CrashPluginNow();
+ [Nested=inside_sync] sync StartPlugin(nsString adapter);
+ async ProvideStorageId(nsCString storageId);
+ async PreloadLibs(nsCString libs);
+ async CloseActive();
+ async InitGMPContentChild(Endpoint<PGMPContentChild> endpoint);
+ async InitProfiler(Endpoint<PProfilerChild> endpoint);
+
+ // Tells the GMP process to flush any pending telemetry.
+ // Used in tests and ping assembly. Buffer contains bincoded Rust structs.
+ // https://firefox-source-docs.mozilla.org/toolkit/components/glean/dev/ipc.html
+ async FlushFOGData() returns (ByteBuf buf);
+
+ // Test-only method.
+ // Asks the GMP process to trigger test-only instrumentation.
+ // The unused returned value is to have a promise we can await.
+ async TestTriggerMetrics() returns (bool unused);
+};
+
+} // namespace gmp
+} // namespace mozilla
diff --git a/dom/media/gmp/PGMPContent.ipdl b/dom/media/gmp/PGMPContent.ipdl
new file mode 100644
index 0000000000..db47aadc62
--- /dev/null
+++ b/dom/media/gmp/PGMPContent.ipdl
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PGMPVideoDecoder;
+include protocol PGMPVideoEncoder;
+include protocol PChromiumCDM;
+
+#if defined(MOZ_SANDBOX) && defined(MOZ_DEBUG) && defined(ENABLE_TESTS)
+include protocol PSandboxTesting;
+#endif
+
+include "GMPContentChild.h";
+
+namespace mozilla {
+namespace gmp {
+
+[NeedsOtherPid, ChildImpl="GMPContentChild", ParentImpl=virtual]
+sync protocol PGMPContent
+{
+ manages PGMPVideoDecoder;
+ manages PGMPVideoEncoder;
+ manages PChromiumCDM;
+
+child:
+ async PGMPVideoDecoder();
+ async PGMPVideoEncoder();
+ async PChromiumCDM(nsCString aKeySystem);
+
+#if defined(MOZ_SANDBOX) && defined(MOZ_DEBUG) && defined(ENABLE_TESTS)
+ async InitSandboxTesting(Endpoint<PSandboxTestingChild> aEndpoint);
+#endif
+};
+
+} // namespace gmp
+} // namespace mozilla
diff --git a/dom/media/gmp/PGMPService.ipdl b/dom/media/gmp/PGMPService.ipdl
new file mode 100644
index 0000000000..e186aaee80
--- /dev/null
+++ b/dom/media/gmp/PGMPService.ipdl
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PGMPContent;
+include GMPTypes;
+
+using base::ProcessId from "base/process.h";
+using GMPPluginType from "GMPNativeTypes.h";
+
+namespace mozilla {
+namespace gmp {
+
+[NeedsOtherPid, ChildImpl=virtual, ParentImpl=virtual]
+sync protocol PGMPService
+{
+parent:
+ sync LaunchGMP(NodeIdVariant nodeIdVariant,
+ nsCString api,
+ nsCString[] tags,
+ ProcessId[] alreadyBridgedTo)
+ returns (uint32_t pluginId,
+ GMPPluginType pluginType,
+ ProcessId id,
+ nsCString displayName,
+ Endpoint<PGMPContentParent> endpoint,
+ nsresult aResult,
+ nsCString aErrorDescription);
+
+ sync GetGMPNodeId(nsString origin, nsString topLevelOrigin, nsString gmpName)
+ returns (nsCString id);
+child:
+ async BeginShutdown();
+};
+
+} // namespace gmp
+} // namespace mozilla
diff --git a/dom/media/gmp/PGMPStorage.ipdl b/dom/media/gmp/PGMPStorage.ipdl
new file mode 100644
index 0000000000..6efe7fbb23
--- /dev/null
+++ b/dom/media/gmp/PGMPStorage.ipdl
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PGMP;
+include GMPTypes;
+
+include "GMPStorageChild.h";
+
+using GMPErr from "gmp-errors.h";
+
+namespace mozilla {
+namespace gmp {
+
+[ManualDealloc, ChildImpl="GMPStorageChild", ParentImpl=virtual]
+async protocol PGMPStorage
+{
+ manager PGMP;
+
+child:
+ async OpenComplete(nsCString aRecordName, GMPErr aStatus);
+ async ReadComplete(nsCString aRecordName, GMPErr aStatus, uint8_t[] aBytes);
+ async WriteComplete(nsCString aRecordName, GMPErr aStatus);
+ async Shutdown();
+
+parent:
+ async Open(nsCString aRecordName);
+ async Read(nsCString aRecordName);
+ async Write(nsCString aRecordName, uint8_t[] aBytes);
+ async Close(nsCString aRecordName);
+ async __delete__();
+
+};
+
+} // namespace gmp
+} // namespace mozilla
diff --git a/dom/media/gmp/PGMPTimer.ipdl b/dom/media/gmp/PGMPTimer.ipdl
new file mode 100644
index 0000000000..c6c8170b43
--- /dev/null
+++ b/dom/media/gmp/PGMPTimer.ipdl
@@ -0,0 +1,26 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PGMP;
+
+include "GMPTimerParent.h";
+include "GMPTimerChild.h";
+
+namespace mozilla {
+namespace gmp {
+
+[ManualDealloc, ChildImpl="GMPTimerChild", ParentImpl="GMPTimerParent"]
+async protocol PGMPTimer
+{
+ manager PGMP;
+child:
+ async TimerExpired(uint32_t aTimerId);
+parent:
+ async SetTimer(uint32_t aTimerId, uint32_t aTimeoutMs);
+ async __delete__();
+};
+
+} // namespace gmp
+} // namespace mozilla
diff --git a/dom/media/gmp/PGMPVideoDecoder.ipdl b/dom/media/gmp/PGMPVideoDecoder.ipdl
new file mode 100644
index 0000000000..e0c7a21a64
--- /dev/null
+++ b/dom/media/gmp/PGMPVideoDecoder.ipdl
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PGMPContent;
+include GMPTypes;
+
+using GMPVideoCodec from "gmp-video-codec.h";
+using GMPErr from "gmp-errors.h";
+
+include "GMPMessageUtils.h";
+include "GMPVideoDecoderChild.h";
+
+namespace mozilla {
+namespace gmp {
+
+[ChildImpl="GMPVideoDecoderChild", ParentImpl=virtual]
+sync protocol PGMPVideoDecoder
+{
+ manager PGMPContent;
+child:
+ async InitDecode(GMPVideoCodec aCodecSettings,
+ uint8_t[] aCodecSpecific,
+ int32_t aCoreCount);
+ async Decode(GMPVideoEncodedFrameData aInputFrame,
+ bool aMissingFrames,
+ uint8_t[] aCodecSpecificInfo,
+ int64_t aRenderTimeMs);
+ async Reset();
+ async Drain();
+ async DecodingComplete();
+ async ChildShmemForPool(Shmem aFrameBuffer);
+
+parent:
+ async __delete__();
+ async Decoded(GMPVideoi420FrameData aDecodedFrame);
+ async ReceivedDecodedReferenceFrame(uint64_t aPictureId);
+ async ReceivedDecodedFrame(uint64_t aPictureId);
+ async InputDataExhausted();
+ async DrainComplete();
+ async ResetComplete();
+ async Error(GMPErr aErr);
+ async Shutdown();
+ async ParentShmemForPool(Shmem aEncodedBuffer);
+ sync NeedShmem(uint32_t aFrameBufferSize) returns (Shmem aMem);
+};
+
+} // namespace gmp
+} // namespace mozilla
diff --git a/dom/media/gmp/PGMPVideoEncoder.ipdl b/dom/media/gmp/PGMPVideoEncoder.ipdl
new file mode 100644
index 0000000000..afd4f7ce9a
--- /dev/null
+++ b/dom/media/gmp/PGMPVideoEncoder.ipdl
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PGMPContent;
+include GMPTypes;
+
+using GMPVideoCodec from "gmp-video-codec.h";
+using GMPVideoFrameType from "gmp-video-frame-encoded.h";
+using GMPErr from "gmp-errors.h";
+
+include "GMPMessageUtils.h";
+include "GMPVideoEncoderChild.h";
+
+namespace mozilla {
+namespace gmp {
+
+[ChildImpl="GMPVideoEncoderChild", ParentImpl=virtual]
+sync protocol PGMPVideoEncoder
+{
+ manager PGMPContent;
+child:
+ async InitEncode(GMPVideoCodec aCodecSettings,
+ uint8_t[] aCodecSpecific,
+ int32_t aNumberOfCores,
+ uint32_t aMaxPayloadSize);
+ async Encode(GMPVideoi420FrameData aInputFrame,
+ uint8_t[] aCodecSpecificInfo,
+ GMPVideoFrameType[] aFrameTypes);
+ async SetChannelParameters(uint32_t aPacketLoss, uint32_t aRTT);
+ async SetRates(uint32_t aNewBitRate, uint32_t aFrameRate);
+ async SetPeriodicKeyFrames(bool aEnable);
+ async EncodingComplete();
+ async ChildShmemForPool(Shmem aEncodedBuffer);
+
+parent:
+ async __delete__();
+ async Encoded(GMPVideoEncodedFrameData aEncodedFrame,
+ uint8_t[] aCodecSpecificInfo);
+ async Error(GMPErr aErr);
+ async Shutdown();
+ async ParentShmemForPool(Shmem aFrameBuffer);
+ sync NeedShmem(uint32_t aEncodedBufferSize) returns (Shmem aMem);
+};
+
+} // namespace gmp
+} // namespace mozilla
diff --git a/dom/media/gmp/README.txt b/dom/media/gmp/README.txt
new file mode 100644
index 0000000000..189cf3b302
--- /dev/null
+++ b/dom/media/gmp/README.txt
@@ -0,0 +1 @@
+This directory contains code supporting Gecko Media Plugins (GMPs). The GMP API is not the same thing as the Media Plugin API (MPAPI).
diff --git a/dom/media/gmp/gmp-api/gmp-entrypoints.h b/dom/media/gmp/gmp-api/gmp-entrypoints.h
new file mode 100644
index 0000000000..fd2457e9be
--- /dev/null
+++ b/dom/media/gmp/gmp-api/gmp-entrypoints.h
@@ -0,0 +1,73 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* Copyright (c) 2011, The WebRTC project authors. All rights reserved.
+ * Copyright (c) 2014, Mozilla
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ ** Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ ** Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ ** Neither the name of Google nor the names of its contributors may
+ * be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef GMP_ENTRYPOINTS_h_
+#define GMP_ENTRYPOINTS_h_
+
+#include "gmp-errors.h"
+#include "gmp-platform.h"
+
+/* C functions exposed by Gecko Media Plugin shared library. */
+
+// GMPInit
+// - Called once after plugin library is loaded, before GMPGetAPI or GMPShutdown
+// are called.
+// - Called on main thread.
+// - 'aPlatformAPI' is a structure containing platform-provided APIs. It is
+// valid until
+// 'GMPShutdown' is called. Owned and must be deleted by plugin.
+typedef GMPErr (*GMPInitFunc)(const GMPPlatformAPI* aPlatformAPI);
+
+// GMPGetAPI
+// - Called when host wants to use an API.
+// - Called on main thread.
+// - 'aAPIName' is a string indicating the API being requested. This should
+// match one of the GMP_API_* macros. Subsequent iterations of the GMP_APIs
+// may change the value of the GMP_API_* macros when ABI changes occur. So
+// make sure you compare aAPIName against the corresponding GMP_API_* macro!
+// - 'aHostAPI' is the host API which is specific to the API being requested
+// from the plugin. It is valid so long as the API object requested from the
+// plugin is valid. It is owned by the host, plugin should not attempt to
+// delete. May be null.
+// - 'aPluginAPI' is for returning the requested API. Destruction of the
+// requsted
+// API object is defined by the API.
+typedef GMPErr (*GMPGetAPIFunc)(const char* aAPIName, void* aHostAPI,
+ void** aPluginAPI);
+
+// GMPShutdown
+// - Called once before exiting process (unloading library).
+// - Called on main thread.
+typedef void (*GMPShutdownFunc)(void);
+
+#endif // GMP_ENTRYPOINTS_h_
diff --git a/dom/media/gmp/gmp-api/gmp-errors.h b/dom/media/gmp/gmp-api/gmp-errors.h
new file mode 100644
index 0000000000..a95db1f7a5
--- /dev/null
+++ b/dom/media/gmp/gmp-api/gmp-errors.h
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* Copyright (c) 2014, Mozilla
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ ** Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ ** Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ ** Neither the name of Google nor the names of its contributors may
+ * be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef GMP_ERRORS_h_
+#define GMP_ERRORS_h_
+
+typedef enum {
+ GMPNoErr = 0,
+ GMPGenericErr = 1,
+ GMPClosedErr = 2,
+ GMPAllocErr = 3,
+ GMPNotImplementedErr = 4,
+ GMPRecordInUse = 5,
+ GMPQuotaExceededErr = 6,
+ GMPDecodeErr = 7,
+ GMPEncodeErr = 8,
+ GMPNoKeyErr = 9,
+ GMPCryptoErr = 10,
+ GMPEndOfEnumeration = 11,
+ GMPInvalidArgErr = 12,
+ GMPAbortedErr = 13,
+ GMPRecordCorrupted = 14,
+ GMPLastErr // Placeholder, must be last. This enum's values must remain
+ // consecutive!
+} GMPErr;
+
+#define GMP_SUCCEEDED(x) ((x) == GMPNoErr)
+#define GMP_FAILED(x) ((x) != GMPNoErr)
+
+#endif // GMP_ERRORS_h_
diff --git a/dom/media/gmp/gmp-api/gmp-platform.h b/dom/media/gmp/gmp-api/gmp-platform.h
new file mode 100644
index 0000000000..530abcbbfe
--- /dev/null
+++ b/dom/media/gmp/gmp-api/gmp-platform.h
@@ -0,0 +1,102 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* Copyright (c) 2014, Mozilla
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ ** Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ ** Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ ** Neither the name of Google nor the names of its contributors may
+ * be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef GMP_PLATFORM_h_
+#define GMP_PLATFORM_h_
+
+#include "gmp-errors.h"
+#include "gmp-storage.h"
+#include <stdint.h>
+
+/* Platform helper API. */
+
+class GMPTask {
+ public:
+ virtual void Destroy() = 0; // Deletes object.
+ virtual ~GMPTask() = default;
+ virtual void Run() = 0;
+};
+
+class GMPThread {
+ public:
+ virtual ~GMPThread() = default;
+ virtual void Post(GMPTask* aTask) = 0;
+ virtual void Join() = 0; // Deletes object after join completes.
+};
+
+// A re-entrant monitor; can be locked from the same thread multiple times.
+// Must be unlocked the same number of times it's locked.
+class GMPMutex {
+ public:
+ virtual ~GMPMutex() = default;
+ virtual void Acquire() = 0;
+ virtual void Release() = 0;
+ virtual void Destroy() = 0; // Deletes object.
+};
+
+// Time is defined as the number of milliseconds since the
+// Epoch (00:00:00 UTC, January 1, 1970).
+typedef int64_t GMPTimestamp;
+
+typedef GMPErr (*GMPCreateThreadPtr)(GMPThread** aThread);
+typedef GMPErr (*GMPRunOnMainThreadPtr)(GMPTask* aTask);
+typedef GMPErr (*GMPSyncRunOnMainThreadPtr)(GMPTask* aTask);
+typedef GMPErr (*GMPCreateMutexPtr)(GMPMutex** aMutex);
+
+// Call on main thread only.
+typedef GMPErr (*GMPCreateRecordPtr)(const char* aRecordName,
+ uint32_t aRecordNameSize,
+ GMPRecord** aOutRecord,
+ GMPRecordClient* aClient);
+
+// Call on main thread only.
+typedef GMPErr (*GMPSetTimerOnMainThreadPtr)(GMPTask* aTask,
+ int64_t aTimeoutMS);
+typedef GMPErr (*GMPGetCurrentTimePtr)(GMPTimestamp* aOutTime);
+
+struct GMPPlatformAPI {
+ // Increment the version when things change. Can only add to the struct,
+ // do not change what already exists. Pointers to functions may be NULL
+ // when passed to plugins, but beware backwards compat implications of
+ // doing that.
+ uint16_t version; // Currently version 0
+
+ GMPCreateThreadPtr createthread;
+ GMPRunOnMainThreadPtr runonmainthread;
+ GMPSyncRunOnMainThreadPtr syncrunonmainthread;
+ GMPCreateMutexPtr createmutex;
+ GMPCreateRecordPtr createrecord;
+ GMPSetTimerOnMainThreadPtr settimer;
+ GMPGetCurrentTimePtr getcurrenttime;
+};
+
+#endif // GMP_PLATFORM_h_
diff --git a/dom/media/gmp/gmp-api/gmp-storage.h b/dom/media/gmp/gmp-api/gmp-storage.h
new file mode 100644
index 0000000000..af53256d43
--- /dev/null
+++ b/dom/media/gmp/gmp-api/gmp-storage.h
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2013, Mozilla Foundation and contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef GMP_STORAGE_h_
+#define GMP_STORAGE_h_
+
+#include "gmp-errors.h"
+#include <stdint.h>
+
+// Maximum size of a record, in bytes; 10 megabytes.
+#define GMP_MAX_RECORD_SIZE (10 * 1024 * 1024)
+
+// Maximum length of a record name in bytes.
+#define GMP_MAX_RECORD_NAME_SIZE 2000
+
+// Provides basic per-origin storage for CDMs. GMPRecord instances can be
+// retrieved by calling GMPPlatformAPI->openstorage. Multiple GMPRecords
+// with different names can be open at once, but a single record can only
+// be opened by one client at a time. This interface is asynchronous, with
+// results being returned via callbacks to the GMPRecordClient pointer
+// provided to the GMPPlatformAPI->openstorage call, on the main thread.
+//
+// Lifecycle: Once opened, the GMPRecord object remains allocated until
+// GMPRecord::Close() is called. If any GMPRecord function, either
+// synchronously or asynchronously through a GMPRecordClient callback,
+// returns an error, the GMP is responsible for calling Close() on the
+// GMPRecord to delete the GMPRecord object's memory. If your GMP does not
+// call Close(), the GMPRecord's memory will leak.
+class GMPRecord {
+ public:
+ // Opens the record. Calls OpenComplete() once the record is open.
+ // Note: Only work when GMP is loading content from a webserver.
+ // Does not work for web pages on loaded from disk.
+ // Note: OpenComplete() is only called if this returns GMPNoErr.
+ virtual GMPErr Open() = 0;
+
+ // Reads the entire contents of the record, and calls
+ // GMPRecordClient::ReadComplete() once the operation is complete.
+ // Note: ReadComplete() is only called if this returns GMPNoErr.
+ virtual GMPErr Read() = 0;
+
+ // Writes aDataSize bytes of aData into the record, overwriting the
+ // contents of the record, truncating it to aDataSize length.
+ // Overwriting with 0 bytes "deletes" the record.
+ // Note: WriteComplete is only called if this returns GMPNoErr.
+ virtual GMPErr Write(const uint8_t* aData, uint32_t aDataSize) = 0;
+
+ // Closes a record, deletes the GMPRecord object. The GMPRecord object
+ // must not be used after this is called, request a new one with
+ // GMPPlatformAPI->openstorage to re-open this record. Cancels all
+ // callbacks.
+ virtual GMPErr Close() = 0;
+
+ virtual ~GMPRecord() = default;
+};
+
+// Callback object that receives the results of GMPRecord calls. Callbacks
+// run asynchronously to the GMPRecord call, on the main thread.
+class GMPRecordClient {
+ public:
+ // Response to a GMPRecord::Open() call with the open |status|.
+ // aStatus values:
+ // - GMPNoErr - Record opened successfully. Record may be empty.
+ // - GMPRecordInUse - This record is in use by another client.
+ // - GMPGenericErr - Unspecified error.
+ // If aStatus is not GMPNoErr, the GMPRecord is unusable, and you must
+ // call Close() on the GMPRecord to dispose of it.
+ virtual void OpenComplete(GMPErr aStatus) = 0;
+
+ // Response to a GMPRecord::Read() call, where aData is the record contents,
+ // of length aDataSize.
+ // aData is only valid for the duration of the call to ReadComplete.
+ // Copy it if you want to hang onto it!
+ // aStatus values:
+ // - GMPNoErr - Record contents read successfully, aDataSize 0 means record
+ // is empty.
+ // - GMPRecordInUse - There are other operations or clients in use on
+ // this record.
+ // - GMPGenericErr - Unspecified error.
+ // If aStatus is not GMPNoErr, the GMPRecord is unusable, and you must
+ // call Close() on the GMPRecord to dispose of it.
+ virtual void ReadComplete(GMPErr aStatus, const uint8_t* aData,
+ uint32_t aDataSize) = 0;
+
+ // Response to a GMPRecord::Write() call.
+ // - GMPNoErr - File contents written successfully.
+ // - GMPRecordInUse - There are other operations or clients in use on
+ // this record.
+ // - GMPGenericErr - Unspecified error.
+ // If aStatus is not GMPNoErr, the GMPRecord is unusable, and you must
+ // call Close() on the GMPRecord to dispose of it.
+ virtual void WriteComplete(GMPErr aStatus) = 0;
+
+ virtual ~GMPRecordClient() = default;
+};
+
+#endif // GMP_STORAGE_h_
diff --git a/dom/media/gmp/gmp-api/gmp-video-codec.h b/dom/media/gmp/gmp-api/gmp-video-codec.h
new file mode 100644
index 0000000000..1fb4930a8b
--- /dev/null
+++ b/dom/media/gmp/gmp-api/gmp-video-codec.h
@@ -0,0 +1,302 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* Copyright (c) 2011, The WebRTC project authors. All rights reserved.
+ * Copyright (c) 2014, Mozilla
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ ** Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ ** Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ ** Neither the name of Google nor the names of its contributors may
+ * be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef GMP_VIDEO_CODEC_h_
+#define GMP_VIDEO_CODEC_h_
+
+#include <stdint.h>
+#include <stddef.h>
+
+enum { kGMPPayloadNameSize = 32 };
+enum { kGMPMaxSimulcastStreams = 4 };
+
+enum GMPVideoCodecComplexity {
+ kGMPComplexityNormal = 0,
+ kGMPComplexityHigh = 1,
+ kGMPComplexityHigher = 2,
+ kGMPComplexityMax = 3,
+ kGMPComplexityInvalid // Should always be last
+};
+
+enum GMPVP8ResilienceMode {
+ kResilienceOff, // The stream produced by the encoder requires a
+ // recovery frame (typically a key frame) to be
+ // decodable after a packet loss.
+ kResilientStream, // A stream produced by the encoder is resilient to
+ // packet losses, but packets within a frame subsequent
+ // to a loss can't be decoded.
+ kResilientFrames, // Same as kResilientStream but with added resilience
+ // within a frame.
+ kResilienceInvalid // Should always be last.
+};
+
+// VP8 specific
+struct GMPVideoCodecVP8 {
+ bool mPictureLossIndicationOn;
+ bool mFeedbackModeOn;
+ GMPVideoCodecComplexity mComplexity;
+ GMPVP8ResilienceMode mResilience;
+ uint32_t mNumberOfTemporalLayers;
+ bool mDenoisingOn;
+ bool mErrorConcealmentOn;
+ bool mAutomaticResizeOn;
+};
+
+// H264 specific
+
+// Needs to match a binary spec for this structure.
+// Note: the mSPS at the end of this structure is variable length.
+struct GMPVideoCodecH264AVCC {
+ uint8_t mVersion; // == 0x01
+ uint8_t mProfile; // these 3 are profile_level_id
+ uint8_t mConstraints;
+ uint8_t mLevel;
+ uint8_t mLengthSizeMinusOne; // lower 2 bits (== GMPBufferType-1). Top 6
+ // reserved (1's)
+
+ // SPS/PPS will not generally be present for interactive use unless SDP
+ // parameter-sets are used.
+ uint8_t mNumSPS; // lower 5 bits; top 5 reserved (1's)
+
+ /*** uint8_t mSPS[]; (Not defined due to compiler warnings and
+ * warnings-as-errors ...) **/
+ // Following mNumSPS is a variable number of bytes, which is the SPS and PPS.
+ // Each SPS == 16 bit size, ("N"), then "N" bytes,
+ // then uint8_t mNumPPS, then each PPS == 16 bit size ("N"), then "N" bytes.
+};
+
+// Codec specific data for H.264 decoding/encoding.
+// Cast the "aCodecSpecific" parameter of GMPVideoDecoder::InitDecode() and
+// GMPVideoEncoder::InitEncode() to this structure.
+struct GMPVideoCodecH264 {
+ uint8_t mPacketizationMode; // 0 or 1
+ struct GMPVideoCodecH264AVCC
+ mAVCC; // holds a variable-sized struct GMPVideoCodecH264AVCC mAVCC;
+};
+
+enum GMPVideoCodecType {
+ kGMPVideoCodecVP8,
+
+ // Encoded frames are in AVCC format; NAL length field of 4 bytes, followed
+ // by frame data. May be multiple NALUs per sample. Codec specific extra data
+ // is the AVCC extra data (in AVCC format).
+ kGMPVideoCodecH264,
+ kGMPVideoCodecVP9,
+ kGMPVideoCodecInvalid // Should always be last.
+};
+
+// Simulcast is when the same stream is encoded multiple times with different
+// settings such as resolution.
+struct GMPSimulcastStream {
+ uint32_t mWidth;
+ uint32_t mHeight;
+ uint32_t mNumberOfTemporalLayers;
+ uint32_t mMaxBitrate; // kilobits/sec.
+ uint32_t mTargetBitrate; // kilobits/sec.
+ uint32_t mMinBitrate; // kilobits/sec.
+ uint32_t mQPMax; // minimum quality
+};
+
+enum GMPVideoCodecMode {
+ kGMPRealtimeVideo,
+ kGMPScreensharing,
+ kGMPStreamingVideo,
+ kGMPNonRealtimeVideo,
+ kGMPCodecModeInvalid // Should always be last.
+};
+
+enum GMPLogLevel {
+ kGMPLogDefault,
+ kGMPLogQuiet,
+ kGMPLogError,
+ kGMPLogWarning,
+ kGMPLogInfo,
+ kGMPLogDebug,
+ kGMPLogDetail,
+ kGMPLogInvalid // Should always be last.
+};
+
+enum GMPProfile {
+ kGMPH264ProfileUnknown,
+ kGMPH264ProfileBaseline,
+ kGMPH264ProfileMain,
+ kGMPH264ProfileExtended,
+ kGMPH264ProfileHigh,
+ kGMPH264ProfileHigh10,
+ kGMPH264ProfileHigh422,
+ kGMPH264ProfileHigh444,
+ kGMPH264ProfileCavlc444,
+ kGMPH264ProfileScalableBaseline,
+ kGMPH264ProfileScalableHigh
+};
+
+enum GMPLevel {
+ kGMPH264LevelUnknown,
+ kGMPH264Level1_0,
+ kGMPH264Level1_B,
+ kGMPH264Level1_1,
+ kGMPH264Level1_2,
+ kGMPH264Level1_3,
+ kGMPH264Level2_0,
+ kGMPH264Level2_1,
+ kGMPH264Level2_2,
+ kGMPH264Level3_0,
+ kGMPH264Level3_1,
+ kGMPH264Level3_2,
+ kGMPH264Level4_0,
+ kGMPH264Level4_1,
+ kGMPH264Level4_2,
+ kGMPH264Level5_0,
+ kGMPH264Level5_1,
+ kGMPH264Level5_2
+};
+
+enum GMPRateControlMode {
+ kGMPRateControlUnknown,
+ kGMPRateControlBitrate,
+ kGMPRateControlQuality,
+ kGMPRateControlBufferBased,
+ kGMPRateControlTimestamp,
+ kGMPRateControlBitratePostskip,
+ kGMPRateControlOff
+};
+
+enum GMPSliceMode {
+ kGMPSliceUnknown,
+ kGMPSliceSingle,
+ kGMPSliceFixedSlcNum,
+ kGMPSliceRaster,
+ kGMPSliceSizeLimited
+};
+
+enum GMPApiVersion {
+ kGMPVersion32 =
+ 1, // leveraging that V32 had mCodecType first, and only supported H264
+ kGMPVersion33 = 33,
+
+ // Adds GMPVideoi420Frame::SetUpdatedTimestamp/UpdatedTimestamp
+ kGMPVersion34 = 34,
+
+ // Adds new options for encoding
+ kGMPVersion35 = 35,
+};
+
+struct GMPVideoCodec {
+ uint32_t mGMPApiVersion;
+
+ GMPVideoCodecType mCodecType;
+ char mPLName[kGMPPayloadNameSize]; // Must be NULL-terminated!
+ uint32_t mPLType;
+
+ uint32_t mWidth;
+ uint32_t mHeight;
+
+ uint32_t mStartBitrate; // kilobits/sec.
+ uint32_t mMaxBitrate; // kilobits/sec.
+ uint32_t mMinBitrate; // kilobits/sec.
+ uint32_t mMaxFramerate;
+
+ bool mFrameDroppingOn;
+ int32_t mKeyFrameInterval;
+
+ uint32_t mQPMax;
+ uint32_t mNumberOfSimulcastStreams;
+ GMPSimulcastStream mSimulcastStream[kGMPMaxSimulcastStreams];
+
+ GMPVideoCodecMode mMode;
+
+ // Since GMP version 34
+ bool mUseThreadedDecode;
+ GMPLogLevel mLogLevel;
+
+ // Since GMP version 35
+ GMPLevel mLevel;
+ GMPProfile mProfile;
+ GMPRateControlMode mRateControlMode;
+ GMPSliceMode mSliceMode;
+ bool mUseThreadedEncode;
+};
+
+// Either single encoded unit, or multiple units separated by 8/16/24/32
+// bit lengths, all with the same timestamp. Note there is no final 0-length
+// entry; one should check the overall end-of-buffer against where the next
+// length would be.
+enum GMPBufferType {
+ GMP_BufferSingle = 0,
+ GMP_BufferLength8,
+ GMP_BufferLength16,
+ GMP_BufferLength24,
+ GMP_BufferLength32,
+ GMP_BufferInvalid,
+};
+
+struct GMPCodecSpecificInfoGeneric {
+ uint8_t mSimulcastIdx;
+};
+
+struct GMPCodecSpecificInfoH264 {
+ uint8_t mSimulcastIdx;
+};
+
+// Note: if any pointers are added to this struct, it must be fitted
+// with a copy-constructor. See below.
+struct GMPCodecSpecificInfoVP8 {
+ bool mHasReceivedSLI;
+ uint8_t mPictureIdSLI;
+ bool mHasReceivedRPSI;
+ uint64_t mPictureIdRPSI;
+ int16_t mPictureId; // negative value to skip pictureId
+ bool mNonReference;
+ uint8_t mSimulcastIdx;
+ uint8_t mTemporalIdx;
+ bool mLayerSync;
+ int32_t mTL0PicIdx; // negative value to skip tl0PicIdx
+ int8_t mKeyIdx; // negative value to skip keyIdx
+};
+
+union GMPCodecSpecificInfoUnion {
+ GMPCodecSpecificInfoGeneric mGeneric;
+ GMPCodecSpecificInfoVP8 mVP8;
+ GMPCodecSpecificInfoH264 mH264;
+};
+
+// Note: if any pointers are added to this struct or its sub-structs, it
+// must be fitted with a copy-constructor. This is because it is copied
+// in the copy-constructor of VCMEncodedFrame.
+struct GMPCodecSpecificInfo {
+ GMPVideoCodecType mCodecType;
+ GMPBufferType mBufferType;
+ GMPCodecSpecificInfoUnion mCodecSpecific;
+};
+
+#endif // GMP_VIDEO_CODEC_h_
diff --git a/dom/media/gmp/gmp-api/gmp-video-decode.h b/dom/media/gmp/gmp-api/gmp-video-decode.h
new file mode 100644
index 0000000000..561e841d39
--- /dev/null
+++ b/dom/media/gmp/gmp-api/gmp-video-decode.h
@@ -0,0 +1,125 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* Copyright (c) 2011, The WebRTC project authors. All rights reserved.
+ * Copyright (c) 2014, Mozilla
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ ** Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ ** Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ ** Neither the name of Google nor the names of its contributors may
+ * be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef GMP_VIDEO_DECODE_h_
+#define GMP_VIDEO_DECODE_h_
+
+#include "gmp-errors.h"
+#include "gmp-video-frame-i420.h"
+#include "gmp-video-frame-encoded.h"
+#include "gmp-video-codec.h"
+#include <stdint.h>
+
+// ALL METHODS MUST BE CALLED ON THE MAIN THREAD
+class GMPVideoDecoderCallback {
+ public:
+ virtual ~GMPVideoDecoderCallback() {}
+
+ virtual void Decoded(GMPVideoi420Frame* aDecodedFrame) = 0;
+
+ virtual void ReceivedDecodedReferenceFrame(const uint64_t aPictureId) = 0;
+
+ virtual void ReceivedDecodedFrame(const uint64_t aPictureId) = 0;
+
+ virtual void InputDataExhausted() = 0;
+
+ virtual void DrainComplete() = 0;
+
+ virtual void ResetComplete() = 0;
+
+ // Called when the decoder encounters a catestrophic error and cannot
+ // continue. Gecko will not send any more input for decoding.
+ virtual void Error(GMPErr aError) = 0;
+};
+
+#define GMP_API_VIDEO_DECODER "decode-video"
+
+// Video decoding for a single stream. A GMP may be asked to create multiple
+// decoders concurrently.
+//
+// API name macro: GMP_API_VIDEO_DECODER
+// Host API: GMPVideoHost
+//
+// ALL METHODS MUST BE CALLED ON THE MAIN THREAD
+class GMPVideoDecoder {
+ public:
+ virtual ~GMPVideoDecoder() {}
+
+ // - aCodecSettings: Details of decoder to create.
+ // - aCodecSpecific: codec specific data, cast to a GMPVideoCodecXXX struct
+ // to get codec specific config data.
+ // - aCodecSpecificLength: number of bytes in aCodecSpecific.
+ // - aCallback: Subclass should retain reference to it until DecodingComplete
+ // is called. Do not attempt to delete it, host retains
+ // ownership.
+ // aCoreCount: number of CPU cores.
+ virtual void InitDecode(const GMPVideoCodec& aCodecSettings,
+ const uint8_t* aCodecSpecific,
+ uint32_t aCodecSpecificLength,
+ GMPVideoDecoderCallback* aCallback,
+ int32_t aCoreCount) = 0;
+
+ // Decode encoded frame (as a part of a video stream). The decoded frame
+ // will be returned to the user through the decode complete callback.
+ //
+ // - aInputFrame: Frame to decode. Call Destroy() on frame when it's decoded.
+ // - aMissingFrames: True if one or more frames have been lost since the
+ // previous decode call.
+ // - aCodecSpecificInfo : codec specific data, pointer to a
+ // GMPCodecSpecificInfo structure appropriate for
+ // this codec type.
+ // - aCodecSpecificInfoLength : number of bytes in aCodecSpecificInfo
+ // - renderTimeMs : System time to render in milliseconds. Only used by
+ // decoders with internal rendering.
+ virtual void Decode(GMPVideoEncodedFrame* aInputFrame, bool aMissingFrames,
+ const uint8_t* aCodecSpecificInfo,
+ uint32_t aCodecSpecificInfoLength,
+ int64_t aRenderTimeMs = -1) = 0;
+
+ // Reset decoder state and prepare for a new call to Decode(...).
+ // Flushes the decoder pipeline.
+ // The decoder should enqueue a task to run ResetComplete() on the main
+ // thread once the reset has finished.
+ virtual void Reset() = 0;
+
+ // Output decoded frames for any data in the pipeline, regardless of ordering.
+ // All remaining decoded frames should be immediately returned via callback.
+ // The decoder should enqueue a task to run DrainComplete() on the main
+ // thread once the reset has finished.
+ virtual void Drain() = 0;
+
+ // May free decoder memory.
+ virtual void DecodingComplete() = 0;
+};
+
+#endif // GMP_VIDEO_DECODE_h_
diff --git a/dom/media/gmp/gmp-api/gmp-video-encode.h b/dom/media/gmp/gmp-api/gmp-video-encode.h
new file mode 100644
index 0000000000..850d34abd1
--- /dev/null
+++ b/dom/media/gmp/gmp-api/gmp-video-encode.h
@@ -0,0 +1,133 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* Copyright (c) 2011, The WebRTC project authors. All rights reserved.
+ * Copyright (c) 2014, Mozilla
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ ** Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ ** Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ ** Neither the name of Google nor the names of its contributors may
+ * be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef GMP_VIDEO_ENCODE_h_
+#define GMP_VIDEO_ENCODE_h_
+
+#include <vector>
+#include <stdint.h>
+
+#include "gmp-errors.h"
+#include "gmp-video-frame-i420.h"
+#include "gmp-video-frame-encoded.h"
+#include "gmp-video-codec.h"
+
+// ALL METHODS MUST BE CALLED ON THE MAIN THREAD
+class GMPVideoEncoderCallback {
+ public:
+ virtual ~GMPVideoEncoderCallback() {}
+
+ virtual void Encoded(GMPVideoEncodedFrame* aEncodedFrame,
+ const uint8_t* aCodecSpecificInfo,
+ uint32_t aCodecSpecificInfoLength) = 0;
+
+ // Called when the encoder encounters a catestrophic error and cannot
+ // continue. Gecko will not send any more input for encoding.
+ virtual void Error(GMPErr aError) = 0;
+};
+
+#define GMP_API_VIDEO_ENCODER "encode-video"
+
+// Video encoding for a single stream. A GMP may be asked to create multiple
+// encoders concurrently.
+//
+// API name macro: GMP_API_VIDEO_ENCODER
+// Host API: GMPVideoHost
+//
+// ALL METHODS MUST BE CALLED ON THE MAIN THREAD
+class GMPVideoEncoder {
+ public:
+ virtual ~GMPVideoEncoder() {}
+
+ // Initialize the encoder with the information from the VideoCodec.
+ //
+ // Input:
+ // - codecSettings : Codec settings
+ // - aCodecSpecific : codec specific data, pointer to a
+ // GMPCodecSpecific structure appropriate for
+ // this codec type.
+ // - aCodecSpecificLength : number of bytes in aCodecSpecific
+ // - aCallback: Subclass should retain reference to it until EncodingComplete
+ // is called. Do not attempt to delete it, host retains
+ // ownership.
+ // - aNnumberOfCores : Number of cores available for the encoder
+ // - aMaxPayloadSize : The maximum size each payload is allowed
+ // to have. Usually MTU - overhead.
+ virtual void InitEncode(const GMPVideoCodec& aCodecSettings,
+ const uint8_t* aCodecSpecific,
+ uint32_t aCodecSpecificLength,
+ GMPVideoEncoderCallback* aCallback,
+ int32_t aNumberOfCores, uint32_t aMaxPayloadSize) = 0;
+
+ // Encode an I420 frame (as a part of a video stream). The encoded frame
+ // will be returned to the user through the encode complete callback.
+ //
+ // Input:
+ // - aInputFrame : Frame to be encoded
+ // - aCodecSpecificInfo : codec specific data, pointer to a
+ // GMPCodecSpecificInfo structure appropriate for
+ // this codec type.
+ // - aCodecSpecificInfoLength : number of bytes in aCodecSpecific
+ // - aFrameTypes : The frame type to encode
+ // - aFrameTypesLength : The number of elements in aFrameTypes array.
+ virtual void Encode(GMPVideoi420Frame* aInputFrame,
+ const uint8_t* aCodecSpecificInfo,
+ uint32_t aCodecSpecificInfoLength,
+ const GMPVideoFrameType* aFrameTypes,
+ uint32_t aFrameTypesLength) = 0;
+
+ // Inform the encoder about the packet loss and round trip time on the
+ // network used to decide the best pattern and signaling.
+ //
+ // - packetLoss : Fraction lost (loss rate in percent =
+ // 100 * packetLoss / 255)
+ // - rtt : Round-trip time in milliseconds
+ virtual void SetChannelParameters(uint32_t aPacketLoss, uint32_t aRTT) = 0;
+
+ // Inform the encoder about the new target bit rate.
+ //
+ // - newBitRate : New target bit rate
+ // - frameRate : The target frame rate
+ virtual void SetRates(uint32_t aNewBitRate, uint32_t aFrameRate) = 0;
+
+ // Use this function to enable or disable periodic key frames. Can be useful
+ // for codecs which have other ways of stopping error propagation.
+ //
+ // - enable : Enable or disable periodic key frames
+ virtual void SetPeriodicKeyFrames(bool aEnable) = 0;
+
+ // May free Encoder memory.
+ virtual void EncodingComplete() = 0;
+};
+
+#endif // GMP_VIDEO_ENCODE_h_
diff --git a/dom/media/gmp/gmp-api/gmp-video-frame-encoded.h b/dom/media/gmp/gmp-api/gmp-video-frame-encoded.h
new file mode 100644
index 0000000000..42683aa129
--- /dev/null
+++ b/dom/media/gmp/gmp-api/gmp-video-frame-encoded.h
@@ -0,0 +1,92 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* Copyright (c) 2011, The WebRTC project authors. All rights reserved.
+ * Copyright (c) 2014, Mozilla
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ ** Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ ** Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ ** Neither the name of Google nor the names of its contributors may
+ * be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef GMP_VIDEO_FRAME_ENCODED_h_
+#define GMP_VIDEO_FRAME_ENCODED_h_
+
+#include <stdint.h>
+#include "gmp-video-frame.h"
+#include "gmp-video-codec.h"
+
+enum GMPVideoFrameType {
+ kGMPKeyFrame = 0,
+ kGMPDeltaFrame = 1,
+ kGMPGoldenFrame = 2,
+ kGMPAltRefFrame = 3,
+ kGMPSkipFrame = 4,
+ kGMPVideoFrameInvalid = 5 // Must always be last.
+};
+
+// The implementation backing this interface uses shared memory for the
+// buffer(s). This means it can only be used by the "owning" process.
+// At first the process which created the object owns it. When the object
+// is passed to an interface the creator loses ownership and must Destroy()
+// the object. Further attempts to use it may fail due to not being able to
+// access the underlying buffer(s).
+//
+// Methods that create or destroy shared memory must be called on the main
+// thread. They are marked below.
+class GMPVideoEncodedFrame : public GMPVideoFrame {
+ public:
+ // MAIN THREAD ONLY
+ virtual GMPErr CreateEmptyFrame(uint32_t aSize) = 0;
+ // MAIN THREAD ONLY
+ virtual GMPErr CopyFrame(const GMPVideoEncodedFrame& aVideoFrame) = 0;
+ virtual void SetEncodedWidth(uint32_t aEncodedWidth) = 0;
+ virtual uint32_t EncodedWidth() = 0;
+ virtual void SetEncodedHeight(uint32_t aEncodedHeight) = 0;
+ virtual uint32_t EncodedHeight() = 0;
+ // Microseconds
+ virtual void SetTimeStamp(uint64_t aTimeStamp) = 0;
+ virtual uint64_t TimeStamp() = 0;
+ // Set frame duration (microseconds)
+ // NOTE: next-frame's Timestamp() != this-frame's TimeStamp()+Duration()
+ // depending on rounding to avoid having to track roundoff errors
+ // and dropped/missing frames(!) (which may leave a large gap)
+ virtual void SetDuration(uint64_t aDuration) = 0;
+ virtual uint64_t Duration() const = 0;
+ virtual void SetFrameType(GMPVideoFrameType aFrameType) = 0;
+ virtual GMPVideoFrameType FrameType() = 0;
+ virtual void SetAllocatedSize(uint32_t aNewSize) = 0;
+ virtual uint32_t AllocatedSize() = 0;
+ virtual void SetSize(uint32_t aSize) = 0;
+ virtual uint32_t Size() = 0;
+ virtual void SetCompleteFrame(bool aCompleteFrame) = 0;
+ virtual bool CompleteFrame() = 0;
+ virtual const uint8_t* Buffer() const = 0;
+ virtual uint8_t* Buffer() = 0;
+ virtual GMPBufferType BufferType() const = 0;
+ virtual void SetBufferType(GMPBufferType aBufferType) = 0;
+};
+
+#endif // GMP_VIDEO_FRAME_ENCODED_h_
diff --git a/dom/media/gmp/gmp-api/gmp-video-frame-i420.h b/dom/media/gmp/gmp-api/gmp-video-frame-i420.h
new file mode 100644
index 0000000000..bf64b90903
--- /dev/null
+++ b/dom/media/gmp/gmp-api/gmp-video-frame-i420.h
@@ -0,0 +1,142 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* Copyright (c) 2011, The WebRTC project authors. All rights reserved.
+ * Copyright (c) 2014, Mozilla
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ ** Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ ** Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ ** Neither the name of Google nor the names of its contributors may
+ * be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef GMP_VIDEO_FRAME_I420_h_
+#define GMP_VIDEO_FRAME_I420_h_
+
+#include "gmp-errors.h"
+#include "gmp-video-frame.h"
+#include "gmp-video-plane.h"
+
+#include <stdint.h>
+
+enum GMPPlaneType {
+ kGMPYPlane = 0,
+ kGMPUPlane = 1,
+ kGMPVPlane = 2,
+ kGMPNumOfPlanes = 3
+};
+
+// The implementation backing this interface uses shared memory for the
+// buffer(s). This means it can only be used by the "owning" process.
+// At first the process which created the object owns it. When the object
+// is passed to an interface the creator loses ownership and must Destroy()
+// the object. Further attempts to use it may fail due to not being able to
+// access the underlying buffer(s).
+//
+// Methods that create or destroy shared memory must be called on the main
+// thread. They are marked below.
+class GMPVideoi420Frame : public GMPVideoFrame {
+ public:
+ // MAIN THREAD ONLY
+ // CreateEmptyFrame: Sets frame dimensions and allocates buffers based
+ // on set dimensions - height and plane stride.
+ // If required size is bigger than the allocated one, new buffers of adequate
+ // size will be allocated.
+ virtual GMPErr CreateEmptyFrame(int32_t aWidth, int32_t aHeight,
+ int32_t aStride_y, int32_t aStride_u,
+ int32_t aStride_v) = 0;
+
+ // MAIN THREAD ONLY
+ // CreateFrame: Sets the frame's members and buffers. If required size is
+ // bigger than allocated one, new buffers of adequate size will be allocated.
+ virtual GMPErr CreateFrame(int32_t aSize_y, const uint8_t* aBuffer_y,
+ int32_t aSize_u, const uint8_t* aBuffer_u,
+ int32_t aSize_v, const uint8_t* aBuffer_v,
+ int32_t aWidth, int32_t aHeight, int32_t aStride_y,
+ int32_t aStride_u, int32_t aStride_v) = 0;
+
+ // MAIN THREAD ONLY
+ // Copy frame: If required size is bigger than allocated one, new buffers of
+ // adequate size will be allocated.
+ virtual GMPErr CopyFrame(const GMPVideoi420Frame& aVideoFrame) = 0;
+
+ // Swap Frame.
+ virtual void SwapFrame(GMPVideoi420Frame* aVideoFrame) = 0;
+
+ // Get pointer to buffer per plane.
+ virtual uint8_t* Buffer(GMPPlaneType aType) = 0;
+
+ // Overloading with const.
+ virtual const uint8_t* Buffer(GMPPlaneType aType) const = 0;
+
+ // Get allocated size per plane.
+ virtual int32_t AllocatedSize(GMPPlaneType aType) const = 0;
+
+ // Get allocated stride per plane.
+ virtual int32_t Stride(GMPPlaneType aType) const = 0;
+
+ // Set frame width.
+ virtual GMPErr SetWidth(int32_t aWidth) = 0;
+
+ // Set frame height.
+ virtual GMPErr SetHeight(int32_t aHeight) = 0;
+
+ // Get frame width.
+ virtual int32_t Width() const = 0;
+
+ // Get frame height.
+ virtual int32_t Height() const = 0;
+
+ // Set frame timestamp (microseconds)
+ virtual void SetTimestamp(uint64_t aTimestamp) = 0;
+
+ // Get frame timestamp (microseconds)
+ virtual uint64_t Timestamp() const = 0;
+
+ // Set frame duration (microseconds)
+ // NOTE: next-frame's Timestamp() != this-frame's TimeStamp()+Duration()
+ // depending on rounding to avoid having to track roundoff errors
+ // and dropped/missing frames(!) (which may leave a large gap)
+ virtual void SetDuration(uint64_t aDuration) = 0;
+
+ // Get frame duration (microseconds)
+ virtual uint64_t Duration() const = 0;
+
+ // Return true if underlying plane buffers are of zero size, false if not.
+ virtual bool IsZeroSize() const = 0;
+
+ // Reset underlying plane buffers sizes to 0. This function doesn't clear
+ // memory.
+ virtual void ResetSize() = 0;
+
+ // -- These methods have been added in kGMPVersion34 --
+
+ // Set an updated frame timestamp (microseconds) from decoder
+ virtual void SetUpdatedTimestamp(uint64_t aTimestamp) = 0;
+
+ // Get an updated frame timestamp (microseconds) from decoder
+ virtual uint64_t UpdatedTimestamp() const = 0;
+};
+
+#endif // GMP_VIDEO_FRAME_I420_h_
diff --git a/dom/media/gmp/gmp-api/gmp-video-frame.h b/dom/media/gmp/gmp-api/gmp-video-frame.h
new file mode 100644
index 0000000000..8f202e1196
--- /dev/null
+++ b/dom/media/gmp/gmp-api/gmp-video-frame.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* Copyright (c) 2011, The WebRTC project authors. All rights reserved.
+ * Copyright (c) 2014, Mozilla
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ ** Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ ** Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ ** Neither the name of Google nor the names of its contributors may
+ * be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef GMP_VIDEO_FRAME_h_
+#define GMP_VIDEO_FRAME_h_
+
+#include "gmp-video-plane.h"
+
+enum GMPVideoFrameFormat { kGMPEncodedVideoFrame = 0, kGMPI420VideoFrame = 1 };
+
+class GMPVideoFrame {
+ public:
+ virtual GMPVideoFrameFormat GetFrameFormat() = 0;
+ // MAIN THREAD ONLY IF OWNING PROCESS
+ virtual void Destroy() = 0;
+};
+
+#endif // GMP_VIDEO_FRAME_h_
diff --git a/dom/media/gmp/gmp-api/gmp-video-host.h b/dom/media/gmp/gmp-api/gmp-video-host.h
new file mode 100644
index 0000000000..e1a6c742aa
--- /dev/null
+++ b/dom/media/gmp/gmp-api/gmp-video-host.h
@@ -0,0 +1,53 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* Copyright (c) 2011, The WebRTC project authors. All rights reserved.
+ * Copyright (c) 2014, Mozilla
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ ** Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ ** Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ ** Neither the name of Google nor the names of its contributors may
+ * be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef GMP_VIDEO_HOST_h_
+#define GMP_VIDEO_HOST_h_
+
+#include "gmp-errors.h"
+#include "gmp-video-frame-i420.h"
+#include "gmp-video-frame-encoded.h"
+#include "gmp-video-codec.h"
+
+// This interface must be called on the main thread only.
+class GMPVideoHost {
+ public:
+ // Construct various video API objects. Host does not retain reference,
+ // caller is owner and responsible for deleting.
+ // MAIN THREAD ONLY
+ virtual GMPErr CreateFrame(GMPVideoFrameFormat aFormat,
+ GMPVideoFrame** aFrame) = 0;
+ virtual GMPErr CreatePlane(GMPPlane** aPlane) = 0;
+};
+
+#endif // GMP_VIDEO_HOST_h_
diff --git a/dom/media/gmp/gmp-api/gmp-video-plane.h b/dom/media/gmp/gmp-api/gmp-video-plane.h
new file mode 100644
index 0000000000..58ca1041ca
--- /dev/null
+++ b/dom/media/gmp/gmp-api/gmp-video-plane.h
@@ -0,0 +1,94 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* Copyright (c) 2011, The WebRTC project authors. All rights reserved.
+ * Copyright (c) 2014, Mozilla
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ ** Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ ** Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ ** Neither the name of Google nor the names of its contributors may
+ * be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef GMP_VIDEO_PLANE_h_
+#define GMP_VIDEO_PLANE_h_
+
+#include "gmp-errors.h"
+#include <stdint.h>
+
+// The implementation backing this interface uses shared memory for the
+// buffer(s). This means it can only be used by the "owning" process.
+// At first the process which created the object owns it. When the object
+// is passed to an interface the creator loses ownership and must Destroy()
+// the object. Further attempts to use it may fail due to not being able to
+// access the underlying buffer(s).
+//
+// Methods that create or destroy shared memory must be called on the main
+// thread. They are marked below.
+class GMPPlane {
+ public:
+ // MAIN THREAD ONLY
+ // CreateEmptyPlane - set allocated size, actual plane size and stride:
+ // If current size is smaller than current size, then a buffer of sufficient
+ // size will be allocated.
+ virtual GMPErr CreateEmptyPlane(int32_t aAllocatedSize, int32_t aStride,
+ int32_t aPlaneSize) = 0;
+
+ // MAIN THREAD ONLY
+ // Copy the entire plane data.
+ virtual GMPErr Copy(const GMPPlane& aPlane) = 0;
+
+ // MAIN THREAD ONLY
+ // Copy buffer: If current size is smaller
+ // than current size, then a buffer of sufficient size will be allocated.
+ virtual GMPErr Copy(int32_t aSize, int32_t aStride,
+ const uint8_t* aBuffer) = 0;
+
+ // Swap plane data.
+ virtual void Swap(GMPPlane& aPlane) = 0;
+
+ // Get allocated size.
+ virtual int32_t AllocatedSize() const = 0;
+
+ // Set actual size.
+ virtual void ResetSize() = 0;
+
+ // Return true is plane size is zero, false if not.
+ virtual bool IsZeroSize() const = 0;
+
+ // Get stride value.
+ virtual int32_t Stride() const = 0;
+
+ // Return data pointer.
+ virtual const uint8_t* Buffer() const = 0;
+
+ // Overloading with non-const.
+ virtual uint8_t* Buffer() = 0;
+
+ // MAIN THREAD ONLY IF OWNING PROCESS
+ // Call this when done with the object. This may delete it.
+ virtual void Destroy() = 0;
+};
+
+#endif // GMP_VIDEO_PLANE_h_
diff --git a/dom/media/gmp/moz.build b/dom/media/gmp/moz.build
new file mode 100644
index 0000000000..4557793b4a
--- /dev/null
+++ b/dom/media/gmp/moz.build
@@ -0,0 +1,149 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+XPIDL_MODULE = "content_geckomediaplugins"
+
+XPIDL_SOURCES += [
+ "mozIGeckoMediaPluginChromeService.idl",
+ "mozIGeckoMediaPluginService.idl",
+]
+
+EXPORTS += [
+ "ChromiumCDMCallback.h",
+ "ChromiumCDMParent.h",
+ "ChromiumCDMProxy.h",
+ "DecryptJob.h",
+ "gmp-api/gmp-entrypoints.h",
+ "gmp-api/gmp-errors.h",
+ "gmp-api/gmp-platform.h",
+ "gmp-api/gmp-storage.h",
+ "gmp-api/gmp-video-codec.h",
+ "gmp-api/gmp-video-decode.h",
+ "gmp-api/gmp-video-encode.h",
+ "gmp-api/gmp-video-frame-encoded.h",
+ "gmp-api/gmp-video-frame-i420.h",
+ "gmp-api/gmp-video-frame.h",
+ "gmp-api/gmp-video-host.h",
+ "gmp-api/gmp-video-plane.h",
+ "GMPCallbackBase.h",
+ "GMPChild.h",
+ "GMPContentChild.h",
+ "GMPContentParent.h",
+ "GMPCrashHelper.h",
+ "GMPCrashHelperHolder.h",
+ "GMPLoader.h",
+ "GMPMessageUtils.h",
+ "GMPNativeTypes.h",
+ "GMPParent.h",
+ "GMPPlatform.h",
+ "GMPProcessChild.h",
+ "GMPProcessParent.h",
+ "GMPSanitizedExports.h",
+ "GMPService.h",
+ "GMPServiceChild.h",
+ "GMPServiceParent.h",
+ "GMPSharedMemManager.h",
+ "GMPStorage.h",
+ "GMPStorageChild.h",
+ "GMPStorageParent.h",
+ "GMPTimerChild.h",
+ "GMPTimerParent.h",
+ "GMPUtils.h",
+ "GMPVideoDecoderChild.h",
+ "GMPVideoDecoderParent.h",
+ "GMPVideoDecoderProxy.h",
+ "GMPVideoEncodedFrameImpl.h",
+ "GMPVideoEncoderChild.h",
+ "GMPVideoEncoderParent.h",
+ "GMPVideoEncoderProxy.h",
+ "GMPVideoHost.h",
+ "GMPVideoi420FrameImpl.h",
+ "GMPVideoPlaneImpl.h",
+ "widevine-adapter/content_decryption_module.h",
+ "widevine-adapter/content_decryption_module_export.h",
+ "widevine-adapter/content_decryption_module_ext.h",
+ "widevine-adapter/content_decryption_module_proxy.h",
+]
+
+UNIFIED_SOURCES += [
+ "CDMStorageIdProvider.cpp",
+ "ChromiumCDMAdapter.cpp",
+ "ChromiumCDMCallbackProxy.cpp",
+ "ChromiumCDMChild.cpp",
+ "ChromiumCDMParent.cpp",
+ "ChromiumCDMProxy.cpp",
+ "DecryptJob.cpp",
+ "GMPChild.cpp",
+ "GMPContentChild.cpp",
+ "GMPContentParent.cpp",
+ "GMPCrashHelperHolder.cpp",
+ "GMPDiskStorage.cpp",
+ "GMPLoader.cpp",
+ "GMPMemoryStorage.cpp",
+ "GMPParent.cpp",
+ "GMPPlatform.cpp",
+ "GMPProcessChild.cpp",
+ "GMPProcessParent.cpp",
+ "GMPService.cpp",
+ "GMPServiceChild.cpp",
+ "GMPServiceParent.cpp",
+ "GMPSharedMemManager.cpp",
+ "GMPStorageChild.cpp",
+ "GMPStorageParent.cpp",
+ "GMPTimerChild.cpp",
+ "GMPTimerParent.cpp",
+ "GMPUtils.cpp",
+ "GMPVideoDecoderChild.cpp",
+ "GMPVideoDecoderParent.cpp",
+ "GMPVideoEncodedFrameImpl.cpp",
+ "GMPVideoEncoderChild.cpp",
+ "GMPVideoEncoderParent.cpp",
+ "GMPVideoHost.cpp",
+ "GMPVideoi420FrameImpl.cpp",
+ "GMPVideoPlaneImpl.cpp",
+]
+
+DIRS += [
+ "rlz",
+ "widevine-adapter",
+]
+
+IPDL_SOURCES += [
+ "GMPTypes.ipdlh",
+ "PChromiumCDM.ipdl",
+ "PGMP.ipdl",
+ "PGMPService.ipdl",
+ "PGMPStorage.ipdl",
+ "PGMPTimer.ipdl",
+ "PGMPVideoDecoder.ipdl",
+ "PGMPVideoEncoder.ipdl",
+]
+
+PREPROCESSED_IPDL_SOURCES += [
+ "PGMPContent.ipdl",
+]
+
+if CONFIG["OS_TARGET"] in ["WINNT", "Darwin"]:
+ DEFINES["SUPPORT_STORAGE_ID"] = 1
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+if CONFIG["MOZ_SANDBOX"]:
+ # For sandbox includes and the include dependencies those have
+ LOCAL_INCLUDES += [
+ "/security/sandbox/chromium",
+ "/security/sandbox/chromium-shim",
+ ]
+
+
+FINAL_LIBRARY = "xul"
+# dom/media/webrtc/transport so we work with --disable-webrtc
+LOCAL_INCLUDES += [
+ "/dom/media/webrtc/transport",
+ "/xpcom/base",
+ "/xpcom/build",
+ "/xpcom/threads",
+]
diff --git a/dom/media/gmp/mozIGeckoMediaPluginChromeService.idl b/dom/media/gmp/mozIGeckoMediaPluginChromeService.idl
new file mode 100644
index 0000000000..51dc545092
--- /dev/null
+++ b/dom/media/gmp/mozIGeckoMediaPluginChromeService.idl
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "nsIFile.idl"
+
+[scriptable, uuid(32d35d21-181f-4630-8caa-a431e2ebad72)]
+interface mozIGeckoMediaPluginChromeService : nsISupports
+{
+ /**
+ * Add a directory to scan for gecko media plugins.
+ * @note Main-thread API.
+ */
+ void addPluginDirectory(in AString directory);
+
+ /**
+ * Remove a directory for gecko media plugins.
+ * @note Main-thread API.
+ */
+ void removePluginDirectory(in AString directory);
+
+ /**
+ * Remove a directory for gecko media plugins and delete it from disk.
+ * If |defer| is true, wait until the plugin is unused before removing.
+ * @note Main-thread API.
+ */
+ void removeAndDeletePluginDirectory(in AString directory,
+ [optional] in bool defer);
+
+ /**
+ * Clears storage data associated with the site and the originAttributes
+ * pattern in JSON format.
+ */
+ void forgetThisSite(in AString site,
+ in AString aPattern);
+
+ /**
+ * Clears storage data associated with the base domain
+ * This means cleaning any storage that is associated
+ * either by origin or top level origin with the base domain
+ */
+ void forgetThisBaseDomain(in AString baseDomain);
+
+ /**
+ * Returns true if the given node id is allowed to store things
+ * persistently on disk. Private Browsing and local content are not
+ * allowed to store persistent data.
+ */
+ bool isPersistentStorageAllowed(in ACString nodeId);
+
+ /**
+ * Returns the directory to use as the base for storing data about GMPs.
+ */
+ nsIFile getStorageDir();
+
+};
diff --git a/dom/media/gmp/mozIGeckoMediaPluginService.idl b/dom/media/gmp/mozIGeckoMediaPluginService.idl
new file mode 100644
index 0000000000..1691e82ac7
--- /dev/null
+++ b/dom/media/gmp/mozIGeckoMediaPluginService.idl
@@ -0,0 +1,122 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "nsIThread.idl"
+
+%{C++
+#include "mozilla/UniquePtr.h"
+#include "nsTArray.h"
+#include "nsString.h"
+class GMPDecryptorProxy;
+class GMPVideoDecoderProxy;
+class GMPVideoEncoderProxy;
+class GMPVideoHost;
+
+namespace mozilla {
+class GMPCrashHelper;
+}
+
+template<class T>
+class GMPGetterCallback
+{
+public:
+ GMPGetterCallback() { MOZ_COUNT_CTOR(GMPGetterCallback<T>); }
+ virtual ~GMPGetterCallback() { MOZ_COUNT_DTOR(GMPGetterCallback<T>); }
+ virtual void Done(T*) = 0;
+};
+template<class T>
+class GMPVideoGetterCallback
+{
+public:
+ GMPVideoGetterCallback() { MOZ_COUNT_CTOR(GMPVideoGetterCallback<T>); }
+ virtual ~GMPVideoGetterCallback() { MOZ_COUNT_DTOR(GMPVideoGetterCallback<T>); }
+ virtual void Done(T*, GMPVideoHost*) = 0;
+};
+typedef GMPGetterCallback<GMPDecryptorProxy> GetGMPDecryptorCallback;
+typedef GMPVideoGetterCallback<GMPVideoDecoderProxy> GetGMPVideoDecoderCallback;
+typedef GMPVideoGetterCallback<GMPVideoEncoderProxy> GetGMPVideoEncoderCallback;
+class GetNodeIdCallback
+{
+public:
+ MOZ_COUNTED_DEFAULT_CTOR(GetNodeIdCallback)
+ MOZ_COUNTED_DTOR_VIRTUAL(GetNodeIdCallback)
+ virtual void Done(nsresult aResult, const nsACString& aNodeId) = 0;
+};
+%}
+
+[ptr] native TagArray(nsTArray<nsCString>);
+[ref] native ConstTagArrayRef(const nsTArray<nsCString>);
+native GetGMPDecryptorCallback(mozilla::UniquePtr<GetGMPDecryptorCallback>&&);
+native GetGMPVideoDecoderCallback(mozilla::UniquePtr<GetGMPVideoDecoderCallback>&&);
+native GetGMPVideoEncoderCallback(mozilla::UniquePtr<GetGMPVideoEncoderCallback>&&);
+native GetNodeIdCallback(mozilla::UniquePtr<GetNodeIdCallback>&&);
+native GMPCrashHelperPtr(mozilla::GMPCrashHelper*);
+
+[scriptable, uuid(44d362ae-937a-4803-bee6-f2512a0149d1)]
+interface mozIGeckoMediaPluginService : nsISupports
+{
+
+ /**
+ * The GMP thread. Callable from any thread.
+ */
+ readonly attribute nsIThread thread;
+
+ /**
+ * Run through windows registered registered for pluginId, sending
+ * 'PluginCrashed' chrome-only event
+ */
+ void RunPluginCrashCallbacks(in unsigned long pluginId, in ACString pluginName);
+
+ /**
+ * Get a plugin that supports the specified tags.
+ * Callable on any thread
+ */
+ [noscript]
+ boolean hasPluginForAPI(in ACString api, in ConstTagArrayRef tags);
+
+ /**
+ * Get a video decoder that supports the specified tags.
+ * The array of tags should at least contain a codec tag, and optionally
+ * other tags such as for EME keysystem.
+ * Callable only on GMP thread.
+ * This is an asynchronous operation, the Done method of the callback object
+ * will be called on the GMP thread with the result (which might be null in
+ * the case of failure). This method always takes ownership of the callback
+ * object, but if this method returns an error then the Done method of the
+ * callback object will not be called at all.
+ */
+ [noscript]
+ void getGMPVideoDecoder(in GMPCrashHelperPtr helper,
+ in TagArray tags,
+ [optional] in ACString nodeId,
+ in GetGMPVideoDecoderCallback callback);
+
+ /**
+ * Get a video encoder that supports the specified tags.
+ * The array of tags should at least contain a codec tag, and optionally
+ * other tags.
+ * Callable only on GMP thread.
+ * This is an asynchronous operation, the Done method of the callback object
+ * will be called on the GMP thread with the result (which might be null in
+ * the case of failure). This method always takes ownership of the callback
+ * object, but if this method returns an error then the Done method of the
+ * callback object will not be called at all.
+ */
+ [noscript]
+ void getGMPVideoEncoder(in GMPCrashHelperPtr helper,
+ in TagArray tags,
+ [optional] in ACString nodeId,
+ in GetGMPVideoEncoderCallback callback);
+
+ /**
+ * Gets the NodeId for a (origin, urlbarOrigin) pair.
+ */
+ [noscript]
+ void getNodeId(in AString origin,
+ in AString topLevelOrigin,
+ in AString gmpName,
+ in GetNodeIdCallback callback);
+};
diff --git a/dom/media/gmp/rlz/OWNERS b/dom/media/gmp/rlz/OWNERS
new file mode 100644
index 0000000000..66f24ebb37
--- /dev/null
+++ b/dom/media/gmp/rlz/OWNERS
@@ -0,0 +1,4 @@
+rogerta@chromium.org
+thakis@chromium.org
+
+# COMPONENT: Internals>Core
diff --git a/dom/media/gmp/rlz/README.mozilla b/dom/media/gmp/rlz/README.mozilla
new file mode 100644
index 0000000000..4408737a5e
--- /dev/null
+++ b/dom/media/gmp/rlz/README.mozilla
@@ -0,0 +1,4 @@
+Code taken from rlz project in Chromium repository: https://chromium.googlesource.com/chromium/src.git/+/6f3478dfd7d29b9872871718bd08493ed0c8bc8e
+
+Note: base/ contains wrappers/dummies to provide implementations of the
+Chromium APIs that this code relies upon.
diff --git a/dom/media/gmp/rlz/lib/assert.h b/dom/media/gmp/rlz/lib/assert.h
new file mode 100644
index 0000000000..c64dd7ff82
--- /dev/null
+++ b/dom/media/gmp/rlz/lib/assert.h
@@ -0,0 +1,14 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef FAKE_ASSERT_H_
+#define FAKE_ASSERT_H_
+
+#include <assert.h>
+
+#define ASSERT_STRING(x) { assert(false); }
+#define VERIFY(x) { assert(x); };
+
+#endif
diff --git a/dom/media/gmp/rlz/lib/crc8.cc b/dom/media/gmp/rlz/lib/crc8.cc
new file mode 100644
index 0000000000..fe02eabf1b
--- /dev/null
+++ b/dom/media/gmp/rlz/lib/crc8.cc
@@ -0,0 +1,90 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "rlz/lib/crc8.h"
+
+namespace {
+
+// The CRC lookup table used for ATM HES (Polynomial = 0x07).
+// These are 256 unique 8-bit values.
+const unsigned char kCrcTable[256] = {
+ 0x00, 0x07, 0x0E, 0x09, 0x1C, 0x1B, 0x12, 0x15,
+ 0x38, 0x3F, 0x36, 0x31, 0x24, 0x23, 0x2A, 0x2D,
+ 0x70, 0x77, 0x7E, 0x79, 0x6C, 0x6B, 0x62, 0x65,
+ 0x48, 0x4F, 0x46, 0x41, 0x54, 0x53, 0x5A, 0x5D,
+ 0xE0, 0xE7, 0xEE, 0xE9, 0xFC, 0xFB, 0xF2, 0xF5,
+ 0xD8, 0xDF, 0xD6, 0xD1, 0xC4, 0xC3, 0xCA, 0xCD,
+ 0x90, 0x97, 0x9E, 0x99, 0x8C, 0x8B, 0x82, 0x85,
+ 0xA8, 0xAF, 0xA6, 0xA1, 0xB4, 0xB3, 0xBA, 0xBD,
+ 0xC7, 0xC0, 0xC9, 0xCE, 0xDB, 0xDC, 0xD5, 0xD2,
+ 0xFF, 0xF8, 0xF1, 0xF6, 0xE3, 0xE4, 0xED, 0xEA,
+ 0xB7, 0xB0, 0xB9, 0xBE, 0xAB, 0xAC, 0xA5, 0xA2,
+ 0x8F, 0x88, 0x81, 0x86, 0x93, 0x94, 0x9D, 0x9A,
+ 0x27, 0x20, 0x29, 0x2E, 0x3B, 0x3C, 0x35, 0x32,
+ 0x1F, 0x18, 0x11, 0x16, 0x03, 0x04, 0x0D, 0x0A,
+ 0x57, 0x50, 0x59, 0x5E, 0x4B, 0x4C, 0x45, 0x42,
+ 0x6F, 0x68, 0x61, 0x66, 0x73, 0x74, 0x7D, 0x7A,
+ 0x89, 0x8E, 0x87, 0x80, 0x95, 0x92, 0x9B, 0x9C,
+ 0xB1, 0xB6, 0xBF, 0xB8, 0xAD, 0xAA, 0xA3, 0xA4,
+ 0xF9, 0xFE, 0xF7, 0xF0, 0xE5, 0xE2, 0xEB, 0xEC,
+ 0xC1, 0xC6, 0xCF, 0xC8, 0xDD, 0xDA, 0xD3, 0xD4,
+ 0x69, 0x6E, 0x67, 0x60, 0x75, 0x72, 0x7B, 0x7C,
+ 0x51, 0x56, 0x5F, 0x58, 0x4D, 0x4A, 0x43, 0x44,
+ 0x19, 0x1E, 0x17, 0x10, 0x05, 0x02, 0x0B, 0x0C,
+ 0x21, 0x26, 0x2F, 0x28, 0x3D, 0x3A, 0x33, 0x34,
+ 0x4E, 0x49, 0x40, 0x47, 0x52, 0x55, 0x5C, 0x5B,
+ 0x76, 0x71, 0x78, 0x7F, 0x6A, 0x6D, 0x64, 0x63,
+ 0x3E, 0x39, 0x30, 0x37, 0x22, 0x25, 0x2C, 0x2B,
+ 0x06, 0x01, 0x08, 0x0F, 0x1A, 0x1D, 0x14, 0x13,
+ 0xAE, 0xA9, 0xA0, 0xA7, 0xB2, 0xB5, 0xBC, 0xBB,
+ 0x96, 0x91, 0x98, 0x9F, 0x8A, 0x8D, 0x84, 0x83,
+ 0xDE, 0xD9, 0xD0, 0xD7, 0xC2, 0xC5, 0xCC, 0xCB,
+ 0xE6, 0xE1, 0xE8, 0xEF, 0xFA, 0xFD, 0xF4, 0xF3
+};
+
+} // namespace anonymous
+
+
+namespace rlz_lib {
+
+bool Crc8::Generate(const unsigned char *data, int length,
+ unsigned char* check_sum) {
+ if (!check_sum)
+ return false;
+
+ *check_sum = 0;
+ if (!data)
+ return false;
+
+ // The inital and final constants are as used in the ATM HEC.
+ static const unsigned char kInitial = 0x00;
+ static const unsigned char kFinal = 0x55;
+ unsigned char crc = kInitial;
+ for (int i = 0; i < length; ++i) {
+ crc = kCrcTable[(data[i] ^ crc) & 0xFFU];
+ }
+
+ *check_sum = crc ^ kFinal;
+ return true;
+}
+
+bool Crc8::Verify(const unsigned char* data, int length,
+ unsigned char check_sum, bool* matches) {
+ if (!matches)
+ return false;
+
+ *matches = false;
+ if (!data)
+ return false;
+
+ unsigned char calculated_crc;
+ if (!Generate(data, length, &calculated_crc))
+ return false;
+
+ *matches = check_sum == calculated_crc;
+
+ return true;
+}
+
+} // namespace rlz_lib
diff --git a/dom/media/gmp/rlz/lib/crc8.h b/dom/media/gmp/rlz/lib/crc8.h
new file mode 100644
index 0000000000..6c3c84859b
--- /dev/null
+++ b/dom/media/gmp/rlz/lib/crc8.h
@@ -0,0 +1,24 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// Crc8 utility functions.
+
+#ifndef RLZ_LIB_CRC8_H_
+#define RLZ_LIB_CRC8_H_
+
+namespace rlz_lib {
+// CRC-8 methods:
+class Crc8 {
+ public:
+ static bool Generate(const unsigned char* data,
+ int length,
+ unsigned char* check_sum);
+ static bool Verify(const unsigned char* data,
+ int length,
+ unsigned char checksum,
+ bool * matches);
+};
+}; // namespace rlz_lib
+
+#endif // RLZ_LIB_CRC8_H_
diff --git a/dom/media/gmp/rlz/lib/machine_id.cc b/dom/media/gmp/rlz/lib/machine_id.cc
new file mode 100644
index 0000000000..b2943f90ff
--- /dev/null
+++ b/dom/media/gmp/rlz/lib/machine_id.cc
@@ -0,0 +1,93 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "rlz/lib/machine_id.h"
+
+#include <stddef.h>
+
+#include "rlz/lib/assert.h"
+#include "rlz/lib/crc8.h"
+#include "rlz/lib/string_utils.h"
+
+// Note: The original machine_id.cc code depends on Chromium's sha1 implementation.
+// Using Mozilla's implmentation as replacement to reduce the dependency of
+// some external files.
+#include "mozilla/SHA1.h"
+
+namespace rlz_lib {
+
+bool GetMachineId(std::string* machine_id) {
+ if (!machine_id)
+ return false;
+
+ static std::string calculated_id;
+ static bool calculated = false;
+ if (calculated) {
+ *machine_id = calculated_id;
+ return true;
+ }
+
+ std::vector<uint8_t> sid_bytes;
+ int volume_id;
+ if (!GetRawMachineId(&sid_bytes, &volume_id))
+ return false;
+
+ if (!testing::GetMachineIdImpl(sid_bytes, volume_id, machine_id))
+ return false;
+
+ calculated = true;
+ calculated_id = *machine_id;
+ return true;
+}
+
+namespace testing {
+
+bool GetMachineIdImpl(const std::vector<uint8_t>& sid_bytes,
+ int volume_id,
+ std::string* machine_id) {
+ machine_id->clear();
+
+ // The ID should be the SID hash + the Hard Drive SNo. + checksum byte.
+ static const int kSizeWithoutChecksum = mozilla::SHA1Sum::kHashSize + sizeof(int);
+ std::basic_string<unsigned char> id_binary(kSizeWithoutChecksum + 1, 0);
+
+ if (!sid_bytes.empty()) {
+ // In order to be compatible with the old version of RLZ, the hash of the
+ // SID must be done with all the original bytes from the unicode string.
+ // However, the chromebase SHA1 hash function takes only an std::string as
+ // input, so the unicode string needs to be converted to std::string
+ // "as is".
+ size_t byte_count = sid_bytes.size() * sizeof(std::vector<uint8_t>::value_type);
+ const char* buffer = reinterpret_cast<const char*>(sid_bytes.data());
+
+ // Note that digest can have embedded nulls.
+ mozilla::SHA1Sum SHA1;
+ mozilla::SHA1Sum::Hash hash;
+ SHA1.update(buffer, byte_count);
+ SHA1.finish(hash);
+ std::string digest(reinterpret_cast<char*>(hash), mozilla::SHA1Sum::kHashSize);
+ VERIFY(digest.size() == mozilla::SHA1Sum::kHashSize);
+ std::copy(digest.begin(), digest.end(), id_binary.begin());
+ }
+
+ // Convert from int to binary (makes big-endian).
+ for (size_t i = 0; i < sizeof(int); i++) {
+ int shift_bits = 8 * (sizeof(int) - i - 1);
+ id_binary[mozilla::SHA1Sum::kHashSize + i] = static_cast<unsigned char>(
+ (volume_id >> shift_bits) & 0xFF);
+ }
+
+ // Append the checksum byte.
+ if (!sid_bytes.empty() || (0 != volume_id))
+ rlz_lib::Crc8::Generate(id_binary.c_str(),
+ kSizeWithoutChecksum,
+ &id_binary[kSizeWithoutChecksum]);
+
+ return rlz_lib::BytesToString(
+ id_binary.c_str(), kSizeWithoutChecksum + 1, machine_id);
+}
+
+} // namespace testing
+
+} // namespace rlz_lib
diff --git a/dom/media/gmp/rlz/lib/machine_id.h b/dom/media/gmp/rlz/lib/machine_id.h
new file mode 100644
index 0000000000..5fa343efa3
--- /dev/null
+++ b/dom/media/gmp/rlz/lib/machine_id.h
@@ -0,0 +1,33 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef RLZ_LIB_MACHINE_ID_H_
+#define RLZ_LIB_MACHINE_ID_H_
+
+#include <string>
+#include <vector>
+
+namespace rlz_lib {
+
+// Gets the unique ID for the machine used for RLZ tracking purposes. On
+// Windows, this ID is derived from the Windows machine SID, and is the string
+// representation of a 20 byte hash + 4 bytes volum id + a 1 byte checksum.
+// Included in financial pings with events, unless explicitly forbidden by the
+// calling application.
+bool GetMachineId(std::string* machine_id);
+
+// Retrieves a raw machine identifier string and a machine-specific
+// 4 byte value. GetMachineId() will SHA1 |data|, append |more_data|, compute
+// the Crc8 of that, and return a hex-encoded string of that data.
+bool GetRawMachineId(std::vector<uint8_t>* data, int* more_data);
+
+namespace testing {
+bool GetMachineIdImpl(const std::vector<uint8_t>& sid_bytes,
+ int volume_id,
+ std::string* machine_id);
+} // namespace testing
+
+} // namespace rlz_lib
+
+#endif // RLZ_LIB_MACHINE_ID_H_
diff --git a/dom/media/gmp/rlz/lib/string_utils.cc b/dom/media/gmp/rlz/lib/string_utils.cc
new file mode 100644
index 0000000000..6da7323823
--- /dev/null
+++ b/dom/media/gmp/rlz/lib/string_utils.cc
@@ -0,0 +1,34 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// String manipulation functions used in the RLZ library.
+
+#include "rlz/lib/string_utils.h"
+
+namespace rlz_lib {
+
+bool BytesToString(const unsigned char* data,
+ int data_len,
+ std::string* string) {
+ if (!string)
+ return false;
+
+ string->clear();
+ if (data_len < 1 || !data)
+ return false;
+
+ static const char kHex[] = "0123456789ABCDEF";
+
+ // Fix the buffer size to begin with to avoid repeated re-allocation.
+ string->resize(data_len * 2);
+ int index = data_len;
+ while (index--) {
+ string->at(2 * index) = kHex[data[index] >> 4]; // high digit
+ string->at(2 * index + 1) = kHex[data[index] & 0x0F]; // low digit
+ }
+
+ return true;
+}
+
+} // namespace rlz_lib
diff --git a/dom/media/gmp/rlz/lib/string_utils.h b/dom/media/gmp/rlz/lib/string_utils.h
new file mode 100644
index 0000000000..500133ade1
--- /dev/null
+++ b/dom/media/gmp/rlz/lib/string_utils.h
@@ -0,0 +1,20 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+//
+// String manipulation functions used in the RLZ library.
+
+#ifndef RLZ_LIB_STRING_UTILS_H_
+#define RLZ_LIB_STRING_UTILS_H_
+
+#include <string>
+
+namespace rlz_lib {
+
+bool BytesToString(const unsigned char* data,
+ int data_len,
+ std::string* string);
+
+}; // namespace
+
+#endif // RLZ_LIB_STRING_UTILS_H_
diff --git a/dom/media/gmp/rlz/mac/lib/machine_id_mac.cc b/dom/media/gmp/rlz/mac/lib/machine_id_mac.cc
new file mode 100644
index 0000000000..8c0bae22e8
--- /dev/null
+++ b/dom/media/gmp/rlz/mac/lib/machine_id_mac.cc
@@ -0,0 +1,322 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <CoreFoundation/CoreFoundation.h>
+#include <IOKit/IOKitLib.h>
+#include <IOKit/network/IOEthernetController.h>
+#include <IOKit/network/IOEthernetInterface.h>
+#include <IOKit/network/IONetworkInterface.h>
+#include <stddef.h>
+#include <stdint.h>
+
+#include <vector>
+#include <string>
+// Note: The original machine_id_mac.cc code is in namespace rlz_lib below.
+// It depends on some external files, which would bring in a log of Chromium
+// code if imported as well.
+// Instead only the necessary code has been extracted from the relevant files,
+// and further combined and reduced to limit the maintenance burden.
+
+// [Extracted from base/logging.h]
+#define DCHECK assert
+
+namespace base {
+
+// [Extracted from base/mac/scoped_typeref.h and base/mac/scoped_cftyperef.h]
+template<typename T>
+class ScopedCFTypeRef {
+ public:
+ typedef T element_type;
+
+ explicit ScopedCFTypeRef(T object)
+ : object_(object) {
+ }
+
+ ScopedCFTypeRef(const ScopedCFTypeRef<T>& that) = delete;
+ ScopedCFTypeRef(ScopedCFTypeRef<T>&& that) = delete;
+
+ ~ScopedCFTypeRef() {
+ if (object_)
+ CFRelease(object_);
+ }
+
+ ScopedCFTypeRef& operator=(const ScopedCFTypeRef<T>& that) = delete;
+ ScopedCFTypeRef& operator=(ScopedCFTypeRef<T>&& that) = delete;
+
+ operator T() const {
+ return object_;
+ }
+
+ // ScopedCFTypeRef<>::release() is like scoped_ptr<>::release. It is NOT
+ // a wrapper for CFRelease().
+ T release() {
+ T temp = object_;
+ object_ = NULL;
+ return temp;
+ }
+
+ private:
+ T object_;
+};
+
+namespace mac {
+
+// [Extracted from base/mac/scoped_ioobject.h]
+// Just like ScopedCFTypeRef but for io_object_t and subclasses.
+template<typename IOT>
+class ScopedIOObject {
+ public:
+ typedef IOT element_type;
+
+ explicit ScopedIOObject(IOT object = IO_OBJECT_NULL)
+ : object_(object) {
+ }
+
+ ~ScopedIOObject() {
+ if (object_)
+ IOObjectRelease(object_);
+ }
+
+ ScopedIOObject(const ScopedIOObject&) = delete;
+ void operator=(const ScopedIOObject&) = delete;
+
+ void reset(IOT object = IO_OBJECT_NULL) {
+ if (object_)
+ IOObjectRelease(object_);
+ object_ = object;
+ }
+
+ operator IOT() const {
+ return object_;
+ }
+
+ private:
+ IOT object_;
+};
+
+// [Extracted from base/mac/foundation_util.h]
+template<typename T>
+T CFCast(const CFTypeRef& cf_val);
+
+template<>
+CFDataRef
+CFCast<CFDataRef>(const CFTypeRef& cf_val) {
+ if (cf_val == NULL) {
+ return NULL;
+ }
+ if (CFGetTypeID(cf_val) == CFDataGetTypeID()) {
+ return (CFDataRef)(cf_val);
+ }
+ return NULL;
+}
+
+template<>
+CFStringRef
+CFCast<CFStringRef>(const CFTypeRef& cf_val) {
+ if (cf_val == NULL) {
+ return NULL;
+ }
+ if (CFGetTypeID(cf_val) == CFStringGetTypeID()) {
+ return (CFStringRef)(cf_val);
+ }
+ return NULL;
+}
+
+} // namespace mac
+
+// [Extracted from base/strings/sys_string_conversions_mac.mm]
+static const CFStringEncoding kNarrowStringEncoding = kCFStringEncodingUTF8;
+
+template<typename StringType>
+static StringType CFStringToSTLStringWithEncodingT(CFStringRef cfstring,
+ CFStringEncoding encoding) {
+ CFIndex length = CFStringGetLength(cfstring);
+ if (length == 0)
+ return StringType();
+
+ CFRange whole_string = CFRangeMake(0, length);
+ CFIndex out_size;
+ CFIndex converted = CFStringGetBytes(cfstring,
+ whole_string,
+ encoding,
+ 0, // lossByte
+ false, // isExternalRepresentation
+ NULL, // buffer
+ 0, // maxBufLen
+ &out_size);
+ if (converted == 0 || out_size == 0)
+ return StringType();
+
+ // out_size is the number of UInt8-sized units needed in the destination.
+ // A buffer allocated as UInt8 units might not be properly aligned to
+ // contain elements of StringType::value_type. Use a container for the
+ // proper value_type, and convert out_size by figuring the number of
+ // value_type elements per UInt8. Leave room for a NUL terminator.
+ typename StringType::size_type elements =
+ out_size * sizeof(UInt8) / sizeof(typename StringType::value_type) + 1;
+
+ std::vector<typename StringType::value_type> out_buffer(elements);
+ converted = CFStringGetBytes(cfstring,
+ whole_string,
+ encoding,
+ 0, // lossByte
+ false, // isExternalRepresentation
+ reinterpret_cast<UInt8*>(&out_buffer[0]),
+ out_size,
+ NULL); // usedBufLen
+ if (converted == 0)
+ return StringType();
+
+ out_buffer[elements - 1] = '\0';
+ return StringType(&out_buffer[0], elements - 1);
+}
+
+std::string SysCFStringRefToUTF8(CFStringRef ref)
+{
+ return CFStringToSTLStringWithEncodingT<std::string>(ref,
+ kNarrowStringEncoding);
+}
+
+} // namespace base
+
+namespace rlz_lib {
+
+namespace {
+
+// See http://developer.apple.com/library/mac/#technotes/tn1103/_index.html
+
+// The caller is responsible for freeing |matching_services|.
+bool FindEthernetInterfaces(io_iterator_t* matching_services) {
+ base::ScopedCFTypeRef<CFMutableDictionaryRef> matching_dict(
+ IOServiceMatching(kIOEthernetInterfaceClass));
+ if (!matching_dict)
+ return false;
+
+ base::ScopedCFTypeRef<CFMutableDictionaryRef> primary_interface(
+ CFDictionaryCreateMutable(kCFAllocatorDefault,
+ 0,
+ &kCFTypeDictionaryKeyCallBacks,
+ &kCFTypeDictionaryValueCallBacks));
+ if (!primary_interface)
+ return false;
+
+ CFDictionarySetValue(
+ primary_interface, CFSTR(kIOPrimaryInterface), kCFBooleanTrue);
+ CFDictionarySetValue(
+ matching_dict, CFSTR(kIOPropertyMatchKey), primary_interface);
+
+ kern_return_t kern_result = IOServiceGetMatchingServices(
+ kIOMasterPortDefault, matching_dict.release(), matching_services);
+
+ return kern_result == KERN_SUCCESS;
+}
+
+bool GetMACAddressFromIterator(io_iterator_t primary_interface_iterator,
+ uint8_t* buffer, size_t buffer_size) {
+ if (buffer_size < kIOEthernetAddressSize)
+ return false;
+
+ bool success = false;
+
+ bzero(buffer, buffer_size);
+ base::mac::ScopedIOObject<io_object_t> primary_interface;
+ for (primary_interface.reset(IOIteratorNext(primary_interface_iterator));
+ primary_interface;
+ primary_interface.reset(IOIteratorNext(primary_interface_iterator))) {
+ io_object_t primary_interface_parent;
+ kern_return_t kern_result = IORegistryEntryGetParentEntry(
+ primary_interface, kIOServicePlane, &primary_interface_parent);
+ base::mac::ScopedIOObject<io_object_t> primary_interface_parent_deleter(
+ primary_interface_parent);
+ success = kern_result == KERN_SUCCESS;
+
+ if (!success)
+ continue;
+
+ base::ScopedCFTypeRef<CFTypeRef> mac_data(
+ IORegistryEntryCreateCFProperty(primary_interface_parent,
+ CFSTR(kIOMACAddress),
+ kCFAllocatorDefault,
+ 0));
+ CFDataRef mac_data_data = base::mac::CFCast<CFDataRef>(mac_data);
+ if (mac_data_data) {
+ CFDataGetBytes(
+ mac_data_data, CFRangeMake(0, kIOEthernetAddressSize), buffer);
+ }
+ }
+
+ return success;
+}
+
+bool GetMacAddress(unsigned char* buffer, size_t size) {
+ io_iterator_t primary_interface_iterator;
+ if (!FindEthernetInterfaces(&primary_interface_iterator))
+ return false;
+ bool result = GetMACAddressFromIterator(
+ primary_interface_iterator, buffer, size);
+ IOObjectRelease(primary_interface_iterator);
+ return result;
+}
+
+CFStringRef CopySerialNumber() {
+ base::mac::ScopedIOObject<io_service_t> expert_device(
+ IOServiceGetMatchingService(kIOMasterPortDefault,
+ IOServiceMatching("IOPlatformExpertDevice")));
+ if (!expert_device)
+ return NULL;
+
+ base::ScopedCFTypeRef<CFTypeRef> serial_number(
+ IORegistryEntryCreateCFProperty(expert_device,
+ CFSTR(kIOPlatformSerialNumberKey),
+ kCFAllocatorDefault,
+ 0));
+ CFStringRef serial_number_cfstring =
+ base::mac::CFCast<CFStringRef>(serial_number.release());
+ if (!serial_number_cfstring)
+ return NULL;
+
+ return serial_number_cfstring;
+}
+
+} // namespace
+
+bool GetRawMachineId(std::vector<uint8_t>* data, int* more_data) {
+ uint8_t mac_address[kIOEthernetAddressSize];
+
+ std::string id;
+ if (GetMacAddress(mac_address, sizeof(mac_address))) {
+ id += "mac:";
+ static const char hex[] =
+ { '0', '1', '2', '3', '4', '5', '6', '7',
+ '8', '9', 'a', 'b', 'c', 'd', 'e', 'f' };
+ for (int i = 0; i < kIOEthernetAddressSize; ++i) {
+ uint8_t byte = mac_address[i];
+ id += hex[byte >> 4];
+ id += hex[byte & 0xF];
+ }
+ }
+
+ // A MAC address is enough to uniquely identify a machine, but it's only 6
+ // bytes, 3 of which are manufacturer-determined. To make brute-forcing the
+ // SHA1 of this harder, also append the system's serial number.
+ CFStringRef serial = CopySerialNumber();
+ if (serial) {
+ if (!id.empty()) {
+ id += ' ';
+ }
+ id += "serial:";
+ id += base::SysCFStringRefToUTF8(serial);
+ CFRelease(serial);
+ }
+
+ // Get the contents of the string 'id' as a bunch of bytes.
+ data->assign(&id[0], &id[id.size()]);
+
+ // On windows, this is set to the volume id. Since it's not scrambled before
+ // being sent, just set it to 1.
+ *more_data = 1;
+ return true;
+}
+
+} // namespace rlz_lib
diff --git a/dom/media/gmp/rlz/moz.build b/dom/media/gmp/rlz/moz.build
new file mode 100644
index 0000000000..8e2d9ea5d1
--- /dev/null
+++ b/dom/media/gmp/rlz/moz.build
@@ -0,0 +1,34 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Note: build rlz in its own moz.build, so it doesn't pickup any of
+# Chromium IPC's headers used in the moz.build of the parent file.
+
+FINAL_LIBRARY = 'xul'
+
+if CONFIG['OS_TARGET'] in ['WINNT', 'Darwin']:
+ UNIFIED_SOURCES += [
+ 'lib/crc8.cc',
+ 'lib/machine_id.cc',
+ 'lib/string_utils.cc',
+ ]
+
+if CONFIG['OS_TARGET'] == 'WINNT':
+ UNIFIED_SOURCES += [
+ 'win/lib/machine_id_win.cc',
+ ]
+
+if CONFIG['OS_TARGET'] == 'Darwin':
+ UNIFIED_SOURCES += [
+ 'mac/lib/machine_id_mac.cc',
+ ]
+ OS_LIBS += [
+ '-framework IOKit',
+ ]
+
+LOCAL_INCLUDES += [
+ '..',
+]
diff --git a/dom/media/gmp/rlz/win/lib/machine_id_win.cc b/dom/media/gmp/rlz/win/lib/machine_id_win.cc
new file mode 100644
index 0000000000..85f1d6cf54
--- /dev/null
+++ b/dom/media/gmp/rlz/win/lib/machine_id_win.cc
@@ -0,0 +1,136 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <windows.h>
+#include <sddl.h> // For ConvertSidToStringSidW.
+
+#include <memory>
+#include <string>
+#include <vector>
+
+#include "mozilla/ArrayUtils.h"
+
+#include "rlz/lib/assert.h"
+
+namespace rlz_lib {
+
+namespace {
+
+bool GetSystemVolumeSerialNumber(int* number) {
+ if (!number)
+ return false;
+
+ *number = 0;
+
+ // Find the system root path (e.g: C:\).
+ wchar_t system_path[MAX_PATH + 1];
+ if (!GetSystemDirectoryW(system_path, MAX_PATH))
+ return false;
+
+ wchar_t* first_slash = wcspbrk(system_path, L"\\/");
+ if (first_slash != NULL)
+ *(first_slash + 1) = 0;
+
+ DWORD number_local = 0;
+ if (!GetVolumeInformationW(system_path, NULL, 0, &number_local, NULL, NULL,
+ NULL, 0))
+ return false;
+
+ *number = (int)number_local;
+ return true;
+}
+
+bool GetComputerSid(const wchar_t* account_name, SID* sid, DWORD sid_size) {
+ static const DWORD kStartDomainLength = 128; // reasonable to start with
+
+ std::unique_ptr<wchar_t[]> domain_buffer(new wchar_t[kStartDomainLength]);
+ DWORD domain_size = kStartDomainLength;
+ DWORD sid_dword_size = sid_size;
+ SID_NAME_USE sid_name_use;
+
+ BOOL success = ::LookupAccountNameW(NULL, account_name, sid,
+ &sid_dword_size, domain_buffer.get(),
+ &domain_size, &sid_name_use);
+ if (!success && ::GetLastError() == ERROR_INSUFFICIENT_BUFFER) {
+ // We could have gotten the insufficient buffer error because
+ // one or both of sid and szDomain was too small. Check for that
+ // here.
+ if (sid_dword_size > sid_size)
+ return false;
+
+ if (domain_size > kStartDomainLength)
+ domain_buffer.reset(new wchar_t[domain_size]);
+
+ success = ::LookupAccountNameW(NULL, account_name, sid, &sid_dword_size,
+ domain_buffer.get(), &domain_size,
+ &sid_name_use);
+ }
+
+ return success != FALSE;
+}
+
+std::vector<uint8_t> ConvertSidToBytes(SID* sid) {
+ std::wstring sid_string;
+#if _WIN32_WINNT >= 0x500
+ wchar_t* sid_buffer = NULL;
+ if (ConvertSidToStringSidW(sid, &sid_buffer)) {
+ sid_string = sid_buffer;
+ LocalFree(sid_buffer);
+ }
+#else
+ SID_IDENTIFIER_AUTHORITY* sia = ::GetSidIdentifierAuthority(sid);
+
+ if(sia->Value[0] || sia->Value[1]) {
+ base::SStringPrintf(
+ &sid_string, L"S-%d-0x%02hx%02hx%02hx%02hx%02hx%02hx",
+ SID_REVISION, (USHORT)sia->Value[0], (USHORT)sia->Value[1],
+ (USHORT)sia->Value[2], (USHORT)sia->Value[3], (USHORT)sia->Value[4],
+ (USHORT)sia->Value[5]);
+ } else {
+ ULONG authority = 0;
+ for (int i = 2; i < 6; ++i) {
+ authority <<= 8;
+ authority |= sia->Value[i];
+ }
+ base::SStringPrintf(&sid_string, L"S-%d-%lu", SID_REVISION, authority);
+ }
+
+ int sub_auth_count = *::GetSidSubAuthorityCount(sid);
+ for(int i = 0; i < sub_auth_count; ++i)
+ base::StringAppendF(&sid_string, L"-%lu", *::GetSidSubAuthority(sid, i));
+#endif
+
+ // Get the contents of the string as a bunch of bytes.
+ return std::vector<uint8_t>(
+ reinterpret_cast<uint8_t*>(&sid_string[0]),
+ reinterpret_cast<uint8_t*>(&sid_string[sid_string.size()]));
+}
+
+} // namespace
+
+bool GetRawMachineId(std::vector<uint8_t>* sid_bytes, int* volume_id) {
+ // Calculate the Windows SID.
+
+ wchar_t computer_name[MAX_COMPUTERNAME_LENGTH + 1] = {0};
+ DWORD size = mozilla::ArrayLength(computer_name);
+
+ if (GetComputerNameW(computer_name, &size)) {
+ char sid_buffer[SECURITY_MAX_SID_SIZE];
+ SID* sid = reinterpret_cast<SID*>(sid_buffer);
+ if (GetComputerSid(computer_name, sid, SECURITY_MAX_SID_SIZE)) {
+ *sid_bytes = ConvertSidToBytes(sid);
+ }
+ }
+
+ // Get the system drive volume serial number.
+ *volume_id = 0;
+ if (!GetSystemVolumeSerialNumber(volume_id)) {
+ ASSERT_STRING("GetMachineId: Failed to retrieve volume serial number");
+ *volume_id = 0;
+ }
+
+ return true;
+}
+
+} // namespace rlz_lib
diff --git a/dom/media/gmp/widevine-adapter/WidevineFileIO.cpp b/dom/media/gmp/widevine-adapter/WidevineFileIO.cpp
new file mode 100644
index 0000000000..913f6ba288
--- /dev/null
+++ b/dom/media/gmp/widevine-adapter/WidevineFileIO.cpp
@@ -0,0 +1,100 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WidevineFileIO.h"
+#include "GMPLog.h"
+#include "WidevineUtils.h"
+
+#include "gmp-api/gmp-platform.h"
+
+// Declared in ChromiumCDMAdapter.cpp.
+extern const GMPPlatformAPI* sPlatform;
+
+namespace mozilla {
+
+void WidevineFileIO::Open(const char* aFilename, uint32_t aFilenameLength) {
+ mName = std::string(aFilename, aFilename + aFilenameLength);
+ GMPRecord* record = nullptr;
+ GMPErr err = sPlatform->createrecord(aFilename, aFilenameLength, &record,
+ static_cast<GMPRecordClient*>(this));
+ if (GMP_FAILED(err)) {
+ GMP_LOG_DEBUG("WidevineFileIO::Open() '%s' GMPCreateRecord failed",
+ mName.c_str());
+ mClient->OnOpenComplete(cdm::FileIOClient::Status::kError);
+ return;
+ }
+ if (GMP_FAILED(record->Open())) {
+ GMP_LOG_DEBUG("WidevineFileIO::Open() '%s' record open failed",
+ mName.c_str());
+ mClient->OnOpenComplete(cdm::FileIOClient::Status::kError);
+ return;
+ }
+
+ GMP_LOG_DEBUG("WidevineFileIO::Open() '%s'", mName.c_str());
+ mRecord = record;
+}
+
+void WidevineFileIO::Read() {
+ if (!mRecord) {
+ GMP_LOG_DEBUG("WidevineFileIO::Read() '%s' used uninitialized!",
+ mName.c_str());
+ mClient->OnReadComplete(cdm::FileIOClient::Status::kError, nullptr, 0);
+ return;
+ }
+ GMP_LOG_DEBUG("WidevineFileIO::Read() '%s'", mName.c_str());
+ mRecord->Read();
+}
+
+void WidevineFileIO::Write(const uint8_t* aData, uint32_t aDataSize) {
+ if (!mRecord) {
+ GMP_LOG_DEBUG("WidevineFileIO::Write() '%s' used uninitialized!",
+ mName.c_str());
+ mClient->OnWriteComplete(cdm::FileIOClient::Status::kError);
+ return;
+ }
+ mRecord->Write(aData, aDataSize);
+}
+
+void WidevineFileIO::Close() {
+ GMP_LOG_DEBUG("WidevineFileIO::Close() '%s'", mName.c_str());
+ if (mRecord) {
+ mRecord->Close();
+ mRecord = nullptr;
+ }
+ delete this;
+}
+
+static cdm::FileIOClient::Status GMPToWidevineFileStatus(GMPErr aStatus) {
+ switch (aStatus) {
+ case GMPRecordInUse:
+ return cdm::FileIOClient::Status::kInUse;
+ case GMPNoErr:
+ return cdm::FileIOClient::Status::kSuccess;
+ default:
+ return cdm::FileIOClient::Status::kError;
+ }
+}
+
+void WidevineFileIO::OpenComplete(GMPErr aStatus) {
+ GMP_LOG_DEBUG("WidevineFileIO::OpenComplete() '%s' status=%d", mName.c_str(),
+ aStatus);
+ mClient->OnOpenComplete(GMPToWidevineFileStatus(aStatus));
+}
+
+void WidevineFileIO::ReadComplete(GMPErr aStatus, const uint8_t* aData,
+ uint32_t aDataSize) {
+ GMP_LOG_DEBUG("WidevineFileIO::OnReadComplete() '%s' status=%d",
+ mName.c_str(), aStatus);
+ mClient->OnReadComplete(GMPToWidevineFileStatus(aStatus), aData, aDataSize);
+}
+
+void WidevineFileIO::WriteComplete(GMPErr aStatus) {
+ GMP_LOG_DEBUG("WidevineFileIO::WriteComplete() '%s' status=%d", mName.c_str(),
+ aStatus);
+ mClient->OnWriteComplete(GMPToWidevineFileStatus(aStatus));
+}
+
+} // namespace mozilla
diff --git a/dom/media/gmp/widevine-adapter/WidevineFileIO.h b/dom/media/gmp/widevine-adapter/WidevineFileIO.h
new file mode 100644
index 0000000000..8c36f032be
--- /dev/null
+++ b/dom/media/gmp/widevine-adapter/WidevineFileIO.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef WidevineFileIO_h_
+#define WidevineFileIO_h_
+
+#include <stddef.h>
+#include "content_decryption_module.h"
+#include "gmp-api/gmp-storage.h"
+#include <string>
+
+namespace mozilla {
+
+class WidevineFileIO : public cdm::FileIO, public GMPRecordClient {
+ public:
+ explicit WidevineFileIO(cdm::FileIOClient* aClient)
+ : mClient(aClient), mRecord(nullptr) {}
+
+ // cdm::FileIO
+ void Open(const char* aFilename, uint32_t aFilenameLength) override;
+ void Read() override;
+ void Write(const uint8_t* aData, uint32_t aDataSize) override;
+ void Close() override;
+
+ // GMPRecordClient
+ void OpenComplete(GMPErr aStatus) override;
+ void ReadComplete(GMPErr aStatus, const uint8_t* aData,
+ uint32_t aDataSize) override;
+ void WriteComplete(GMPErr aStatus) override;
+
+ private:
+ cdm::FileIOClient* mClient;
+ GMPRecord* mRecord;
+ std::string mName;
+};
+
+} // namespace mozilla
+
+#endif // WidevineFileIO_h_
diff --git a/dom/media/gmp/widevine-adapter/WidevineUtils.cpp b/dom/media/gmp/widevine-adapter/WidevineUtils.cpp
new file mode 100644
index 0000000000..d0551406b2
--- /dev/null
+++ b/dom/media/gmp/widevine-adapter/WidevineUtils.cpp
@@ -0,0 +1,61 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WidevineUtils.h"
+#include "GMPLog.h"
+#include "gmp-api/gmp-errors.h"
+#include <stdarg.h>
+#include <stdio.h>
+#include <inttypes.h>
+
+namespace mozilla {
+
+WidevineBuffer::WidevineBuffer(size_t aSize) {
+ GMP_LOG_DEBUG("WidevineBuffer(size=%zu) created", aSize);
+ mBuffer.SetLength(aSize);
+}
+
+WidevineBuffer::~WidevineBuffer() {
+ GMP_LOG_DEBUG("WidevineBuffer(size=%" PRIu32 ") destroyed", Size());
+}
+
+void WidevineBuffer::Destroy() { delete this; }
+
+uint32_t WidevineBuffer::Capacity() const { return mBuffer.Length(); }
+
+uint8_t* WidevineBuffer::Data() { return mBuffer.Elements(); }
+
+void WidevineBuffer::SetSize(uint32_t aSize) { mBuffer.SetLength(aSize); }
+
+uint32_t WidevineBuffer::Size() const { return mBuffer.Length(); }
+
+nsTArray<uint8_t> WidevineBuffer::ExtractBuffer() {
+ nsTArray<uint8_t> out = std::move(mBuffer);
+ return out;
+}
+
+WidevineDecryptedBlock::WidevineDecryptedBlock()
+ : mBuffer(nullptr), mTimestamp(0) {}
+
+WidevineDecryptedBlock::~WidevineDecryptedBlock() {
+ if (mBuffer) {
+ mBuffer->Destroy();
+ mBuffer = nullptr;
+ }
+}
+
+void WidevineDecryptedBlock::SetDecryptedBuffer(cdm::Buffer* aBuffer) {
+ mBuffer = aBuffer;
+}
+
+cdm::Buffer* WidevineDecryptedBlock::DecryptedBuffer() { return mBuffer; }
+
+void WidevineDecryptedBlock::SetTimestamp(int64_t aTimestamp) {
+ mTimestamp = aTimestamp;
+}
+
+int64_t WidevineDecryptedBlock::Timestamp() const { return mTimestamp; }
+
+} // namespace mozilla
diff --git a/dom/media/gmp/widevine-adapter/WidevineUtils.h b/dom/media/gmp/widevine-adapter/WidevineUtils.h
new file mode 100644
index 0000000000..1502621451
--- /dev/null
+++ b/dom/media/gmp/widevine-adapter/WidevineUtils.h
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef WidevineUtils_h_
+#define WidevineUtils_h_
+
+#include "stddef.h"
+#include "content_decryption_module.h"
+#include "nsISupportsImpl.h"
+#include "nsTArray.h"
+#include "mozilla/Logging.h"
+
+namespace mozilla {
+
+#define ENSURE_TRUE(condition, rv) \
+ { \
+ if (!(condition)) { \
+ GMP_LOG_DEBUG("ENSURE_TRUE FAILED %s:%d", __FILE__, __LINE__); \
+ return rv; \
+ } \
+ }
+
+#define ENSURE_GMP_SUCCESS(err, rv) \
+ { \
+ if (GMP_FAILED(err)) { \
+ GMP_LOG_DEBUG("ENSURE_GMP_SUCCESS FAILED %s:%d", __FILE__, __LINE__); \
+ return rv; \
+ } \
+ }
+
+namespace gmp {
+class CDMShmemBuffer;
+}
+class WidevineBuffer;
+
+// Base class for our cdm::Buffer implementations, so we can tell at runtime
+// whether the buffer is a Shmem or non-Shmem buffer.
+class CDMBuffer : public cdm::Buffer {
+ public:
+ virtual WidevineBuffer* AsArrayBuffer() { return nullptr; }
+ virtual gmp::CDMShmemBuffer* AsShmemBuffer() { return nullptr; }
+};
+
+class WidevineBuffer : public CDMBuffer {
+ public:
+ explicit WidevineBuffer(size_t aSize);
+ ~WidevineBuffer() override;
+ void Destroy() override;
+ uint32_t Capacity() const override;
+ uint8_t* Data() override;
+ void SetSize(uint32_t aSize) override;
+ uint32_t Size() const override;
+
+ // Moves contents of buffer out into temporary.
+ // Note: This empties the buffer.
+ nsTArray<uint8_t> ExtractBuffer();
+
+ WidevineBuffer* AsArrayBuffer() override { return this; }
+
+ private:
+ nsTArray<uint8_t> mBuffer;
+ WidevineBuffer(const WidevineBuffer&);
+ void operator=(const WidevineBuffer&);
+};
+
+class WidevineDecryptedBlock : public cdm::DecryptedBlock {
+ public:
+ WidevineDecryptedBlock();
+ ~WidevineDecryptedBlock() override;
+ void SetDecryptedBuffer(cdm::Buffer* aBuffer) override;
+ cdm::Buffer* DecryptedBuffer() override;
+ void SetTimestamp(int64_t aTimestamp) override;
+ int64_t Timestamp() const override;
+
+ private:
+ cdm::Buffer* mBuffer;
+ int64_t mTimestamp;
+};
+
+} // namespace mozilla
+
+#endif // WidevineUtils_h_
diff --git a/dom/media/gmp/widevine-adapter/WidevineVideoFrame.cpp b/dom/media/gmp/widevine-adapter/WidevineVideoFrame.cpp
new file mode 100644
index 0000000000..3f136c4ac8
--- /dev/null
+++ b/dom/media/gmp/widevine-adapter/WidevineVideoFrame.cpp
@@ -0,0 +1,142 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WidevineVideoFrame.h"
+#include "GMPLog.h"
+#include "WidevineUtils.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/IntegerPrintfMacros.h"
+
+namespace mozilla {
+
+WidevineVideoFrame::WidevineVideoFrame()
+ : mFormat(cdm::VideoFormat::kUnknownVideoFormat),
+ mSize{0, 0},
+ mBuffer(nullptr),
+ mTimestamp(0) {
+ MOZ_ASSERT(mSize.height == 0 && mSize.width == 0, "Size should be zeroed");
+ GMP_LOG_DEBUG("WidevineVideoFrame::WidevineVideoFrame() this=%p", this);
+ memset(mPlaneOffsets, 0, sizeof(mPlaneOffsets));
+ memset(mPlaneStrides, 0, sizeof(mPlaneStrides));
+}
+
+WidevineVideoFrame::WidevineVideoFrame(WidevineVideoFrame&& aOther)
+ : mFormat(aOther.mFormat),
+ mSize(aOther.mSize),
+ mBuffer(aOther.mBuffer),
+ mTimestamp(aOther.mTimestamp) {
+ GMP_LOG_DEBUG(
+ "WidevineVideoFrame::WidevineVideoFrame(WidevineVideoFrame&&) "
+ "this=%p, other=%p",
+ this, &aOther);
+ memcpy(mPlaneOffsets, aOther.mPlaneOffsets, sizeof(mPlaneOffsets));
+ memcpy(mPlaneStrides, aOther.mPlaneStrides, sizeof(mPlaneStrides));
+ aOther.mBuffer = nullptr;
+}
+
+WidevineVideoFrame::~WidevineVideoFrame() {
+ if (mBuffer) {
+ mBuffer->Destroy();
+ mBuffer = nullptr;
+ }
+}
+
+void WidevineVideoFrame::SetFormat(cdm::VideoFormat aFormat) {
+ GMP_LOG_DEBUG("WidevineVideoFrame::SetFormat(%d) this=%p", aFormat, this);
+ mFormat = aFormat;
+}
+
+cdm::VideoFormat WidevineVideoFrame::Format() const { return mFormat; }
+
+void WidevineVideoFrame::SetSize(cdm::Size aSize) {
+ GMP_LOG_DEBUG("WidevineVideoFrame::SetSize(%d,%d) this=%p", aSize.width,
+ aSize.height, this);
+ mSize.width = aSize.width;
+ mSize.height = aSize.height;
+}
+
+cdm::Size WidevineVideoFrame::Size() const { return mSize; }
+
+void WidevineVideoFrame::SetFrameBuffer(cdm::Buffer* aFrameBuffer) {
+ GMP_LOG_DEBUG("WidevineVideoFrame::SetFrameBuffer(%p) this=%p", aFrameBuffer,
+ this);
+ MOZ_ASSERT(!mBuffer);
+ mBuffer = aFrameBuffer;
+}
+
+cdm::Buffer* WidevineVideoFrame::FrameBuffer() { return mBuffer; }
+
+void WidevineVideoFrame::SetPlaneOffset(cdm::VideoPlane aPlane,
+ uint32_t aOffset) {
+ GMP_LOG_DEBUG("WidevineVideoFrame::SetPlaneOffset(%d, %" PRIu32 ") this=%p",
+ aPlane, aOffset, this);
+ mPlaneOffsets[aPlane] = aOffset;
+}
+
+uint32_t WidevineVideoFrame::PlaneOffset(cdm::VideoPlane aPlane) {
+ return mPlaneOffsets[aPlane];
+}
+
+void WidevineVideoFrame::SetStride(cdm::VideoPlane aPlane, uint32_t aStride) {
+ GMP_LOG_DEBUG("WidevineVideoFrame::SetStride(%d, %" PRIu32 ") this=%p",
+ aPlane, aStride, this);
+ mPlaneStrides[aPlane] = aStride;
+}
+
+uint32_t WidevineVideoFrame::Stride(cdm::VideoPlane aPlane) {
+ return mPlaneStrides[aPlane];
+}
+
+void WidevineVideoFrame::SetTimestamp(int64_t timestamp) {
+ GMP_LOG_DEBUG("WidevineVideoFrame::SetTimestamp(%" PRId64 ") this=%p",
+ timestamp, this);
+ mTimestamp = timestamp;
+}
+
+int64_t WidevineVideoFrame::Timestamp() const { return mTimestamp; }
+
+bool WidevineVideoFrame::InitToBlack(int32_t aWidth, int32_t aHeight,
+ int64_t aTimeStamp) {
+ if (NS_WARN_IF(aWidth < 0 || aHeight < 0)) {
+ MOZ_ASSERT_UNREACHABLE("Frame dimensions should be positive");
+ return false;
+ }
+
+ const uint32_t halfWidth = (uint32_t(aWidth) + 1) / 2;
+ CheckedInt<size_t> ySizeChk = aWidth;
+ ySizeChk *= aHeight;
+ CheckedInt<size_t> uSizeChk = halfWidth;
+ uSizeChk *= (uint32_t(aHeight) + 1) / 2;
+ CheckedInt<size_t> yuSizeChk = ySizeChk + uSizeChk;
+ if (NS_WARN_IF(!yuSizeChk.isValid())) {
+ return false;
+ }
+
+ WidevineBuffer* buffer = new WidevineBuffer(yuSizeChk.value());
+ const size_t ySize = ySizeChk.value();
+ const size_t uSize = uSizeChk.value();
+ // Black in YCbCr is (0,128,128).
+ memset(buffer->Data(), 0, ySize);
+ memset(buffer->Data() + ySize, 128, uSize);
+ if (mBuffer) {
+ mBuffer->Destroy();
+ mBuffer = nullptr;
+ }
+ SetFormat(cdm::VideoFormat::kI420);
+ SetSize(cdm::Size{aWidth, aHeight});
+ SetFrameBuffer(buffer);
+ SetPlaneOffset(cdm::VideoPlane::kYPlane, 0);
+ SetStride(cdm::VideoPlane::kYPlane, aWidth);
+ // Note: U and V planes are stored at the same place in order to
+ // save memory since their contents are the same.
+ SetPlaneOffset(cdm::VideoPlane::kUPlane, ySize);
+ SetStride(cdm::VideoPlane::kUPlane, halfWidth);
+ SetPlaneOffset(cdm::VideoPlane::kVPlane, ySize);
+ SetStride(cdm::VideoPlane::kVPlane, halfWidth);
+ SetTimestamp(aTimeStamp);
+ return true;
+}
+
+} // namespace mozilla
diff --git a/dom/media/gmp/widevine-adapter/WidevineVideoFrame.h b/dom/media/gmp/widevine-adapter/WidevineVideoFrame.h
new file mode 100644
index 0000000000..0ae65ac1e5
--- /dev/null
+++ b/dom/media/gmp/widevine-adapter/WidevineVideoFrame.h
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef WidevineVideoFrame_h_
+#define WidevineVideoFrame_h_
+
+#include "stddef.h"
+#include "content_decryption_module.h"
+#include "mozilla/Attributes.h"
+#include <vector>
+
+namespace mozilla {
+
+class WidevineVideoFrame : public cdm::VideoFrame {
+ public:
+ WidevineVideoFrame();
+ WidevineVideoFrame(WidevineVideoFrame&& other);
+ ~WidevineVideoFrame();
+
+ void SetFormat(cdm::VideoFormat aFormat) override;
+ cdm::VideoFormat Format() const override;
+
+ void SetSize(cdm::Size aSize) override;
+ cdm::Size Size() const override;
+
+ void SetFrameBuffer(cdm::Buffer* aFrameBuffer) override;
+ cdm::Buffer* FrameBuffer() override;
+
+ void SetPlaneOffset(cdm::VideoPlane aPlane, uint32_t aOffset) override;
+ uint32_t PlaneOffset(cdm::VideoPlane aPlane) override;
+
+ void SetStride(cdm::VideoPlane aPlane, uint32_t aStride) override;
+ uint32_t Stride(cdm::VideoPlane aPlane) override;
+
+ void SetTimestamp(int64_t aTimestamp) override;
+ int64_t Timestamp() const override;
+
+ [[nodiscard]] bool InitToBlack(int32_t aWidth, int32_t aHeight,
+ int64_t aTimeStamp);
+
+ protected:
+ cdm::VideoFormat mFormat;
+ cdm::Size mSize;
+ cdm::Buffer* mBuffer;
+ uint32_t mPlaneOffsets[cdm::VideoPlane::kMaxPlanes];
+ uint32_t mPlaneStrides[cdm::VideoPlane::kMaxPlanes];
+ int64_t mTimestamp;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/gmp/widevine-adapter/content_decryption_module.h b/dom/media/gmp/widevine-adapter/content_decryption_module.h
new file mode 100644
index 0000000000..68fee35195
--- /dev/null
+++ b/dom/media/gmp/widevine-adapter/content_decryption_module.h
@@ -0,0 +1,1359 @@
+// Copyright (c) 2012 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CDM_CONTENT_DECRYPTION_MODULE_H_
+#define CDM_CONTENT_DECRYPTION_MODULE_H_
+
+#include <type_traits>
+
+#include "content_decryption_module_export.h"
+#include "content_decryption_module_proxy.h"
+
+#if defined(_MSC_VER)
+typedef unsigned char uint8_t;
+typedef unsigned int uint32_t;
+typedef int int32_t;
+typedef __int64 int64_t;
+#else
+# include <stdint.h>
+#endif
+
+// The version number must be rolled when the exported functions are updated!
+// If the CDM and the adapter use different versions of these functions, the
+// adapter will fail to load or crash!
+#define CDM_MODULE_VERSION 4
+
+// Build the versioned entrypoint name.
+// The extra macros are necessary to expand version to an actual value.
+#define INITIALIZE_CDM_MODULE \
+ BUILD_ENTRYPOINT(InitializeCdmModule, CDM_MODULE_VERSION)
+#define BUILD_ENTRYPOINT(name, version) \
+ BUILD_ENTRYPOINT_NO_EXPANSION(name, version)
+#define BUILD_ENTRYPOINT_NO_EXPANSION(name, version) name##_##version
+
+// Macro to check that |type| does the following:
+// 1. is a standard layout.
+// 2. is trivial.
+// 3. sizeof(type) matches the expected size in bytes. As some types contain
+// pointers, the size is specified for both 32 and 64 bit.
+#define CHECK_TYPE(type, size_32, size_64) \
+ static_assert(std::is_standard_layout<type>(), \
+ #type " not standard_layout"); \
+ static_assert(std::is_trivial<type>(), #type " not trivial"); \
+ static_assert((sizeof(void*) == 4 && sizeof(type) == size_32) || \
+ (sizeof(void*) == 8 && sizeof(type) == size_64), \
+ #type " size mismatch")
+
+extern "C" {
+
+CDM_API void INITIALIZE_CDM_MODULE();
+
+CDM_API void DeinitializeCdmModule();
+
+// Returns a pointer to the requested CDM Host interface upon success.
+// Returns NULL if the requested CDM Host interface is not supported.
+// The caller should cast the returned pointer to the type matching
+// |host_interface_version|.
+typedef void* (*GetCdmHostFunc)(int host_interface_version, void* user_data);
+
+// Returns a pointer to the requested CDM upon success.
+// Returns NULL if an error occurs or the requested |cdm_interface_version| or
+// |key_system| is not supported or another error occurs.
+// The caller should cast the returned pointer to the type matching
+// |cdm_interface_version|.
+// Caller retains ownership of arguments and must call Destroy() on the returned
+// object.
+CDM_API void* CreateCdmInstance(int cdm_interface_version,
+ const char* key_system,
+ uint32_t key_system_size,
+ GetCdmHostFunc get_cdm_host_func,
+ void* user_data);
+
+CDM_API const char* GetCdmVersion();
+
+} // extern "C"
+
+namespace cdm {
+
+enum Status : uint32_t {
+ kSuccess = 0,
+ kNeedMoreData, // Decoder needs more data to produce a decoded frame/sample.
+ kNoKey, // The required decryption key is not available.
+ kInitializationError, // Initialization error.
+ kDecryptError, // Decryption failed.
+ kDecodeError, // Error decoding audio or video.
+ kDeferredInitialization // Decoder is not ready for initialization.
+};
+CHECK_TYPE(Status, 4, 4);
+
+// Exceptions used by the CDM to reject promises.
+// https://w3c.github.io/encrypted-media/#exceptions
+enum Exception : uint32_t {
+ kExceptionTypeError,
+ kExceptionNotSupportedError,
+ kExceptionInvalidStateError,
+ kExceptionQuotaExceededError
+};
+CHECK_TYPE(Exception, 4, 4);
+
+// The encryption scheme. The definitions are from ISO/IEC 23001-7:2016.
+enum class EncryptionScheme : uint32_t {
+ kUnencrypted = 0,
+ kCenc, // 'cenc' subsample encryption using AES-CTR mode.
+ kCbcs // 'cbcs' pattern encryption using AES-CBC mode.
+};
+CHECK_TYPE(EncryptionScheme, 4, 4);
+
+// The pattern used for pattern encryption. Note that ISO/IEC 23001-7:2016
+// defines each block to be 16-bytes.
+struct Pattern {
+ uint32_t crypt_byte_block; // Count of the encrypted blocks.
+ uint32_t skip_byte_block; // Count of the unencrypted blocks.
+};
+CHECK_TYPE(Pattern, 8, 8);
+
+enum class ColorRange : uint8_t {
+ kInvalid,
+ kLimited, // 709 color range with RGB values ranging from 16 to 235.
+ kFull, // Full RGB color range with RGB values from 0 to 255.
+ kDerived // Range is defined by |transfer_id| and |matrix_id|.
+};
+CHECK_TYPE(ColorRange, 1, 1);
+
+// Described in ISO 23001-8:2016, section 7. All the IDs are in the range
+// [0, 255] so 8-bit integer is sufficient. An unspecified ColorSpace should be
+// {2, 2, 2, ColorRange::kInvalid}, where value 2 means "Unspecified" for all
+// the IDs, as defined by the spec.
+struct ColorSpace {
+ uint8_t primary_id; // 7.1 colour primaries, table 2
+ uint8_t transfer_id; // 7.2 transfer characteristics, table 3
+ uint8_t matrix_id; // 7.3 matrix coefficients, table 4
+ ColorRange range;
+};
+CHECK_TYPE(ColorSpace, 4, 4);
+
+// Time is defined as the number of seconds since the Epoch
+// (00:00:00 UTC, January 1, 1970), not including any added leap second.
+// Also see Time definition in spec: https://w3c.github.io/encrypted-media/#time
+// Note that Time is defined in millisecond accuracy in the spec but in second
+// accuracy here.
+typedef double Time;
+
+// An input buffer can be split into several continuous subsamples.
+// A SubsampleEntry specifies the number of clear and cipher bytes in each
+// subsample. For example, the following buffer has three subsamples:
+//
+// |<----- subsample1 ----->|<----- subsample2 ----->|<----- subsample3 ----->|
+// | clear1 | cipher1 | clear2 | cipher2 | clear3 | cipher3 |
+//
+// For decryption, all of the cipher bytes in a buffer should be concatenated
+// (in the subsample order) into a single logical stream. The clear bytes should
+// not be considered as part of decryption.
+//
+// Stream to decrypt: | cipher1 | cipher2 | cipher3 |
+// Decrypted stream: | decrypted1| decrypted2 | decrypted3 |
+//
+// After decryption, the decrypted bytes should be copied over the position
+// of the corresponding cipher bytes in the original buffer to form the output
+// buffer. Following the above example, the decrypted buffer should be:
+//
+// |<----- subsample1 ----->|<----- subsample2 ----->|<----- subsample3 ----->|
+// | clear1 | decrypted1| clear2 | decrypted2 | clear3 | decrypted3 |
+//
+struct SubsampleEntry {
+ uint32_t clear_bytes;
+ uint32_t cipher_bytes;
+};
+CHECK_TYPE(SubsampleEntry, 8, 8);
+
+// Represents an input buffer to be decrypted (and possibly decoded). It does
+// not own any pointers in this struct. If |encryption_scheme| = kUnencrypted,
+// the data is unencrypted.
+// Note that this struct is organized so that sizeof(InputBuffer_2)
+// equals the sum of sizeof() all members in both 32-bit and 64-bit compiles.
+// Padding has been added to keep the fields aligned.
+struct InputBuffer_2 {
+ const uint8_t* data; // Pointer to the beginning of the input data.
+ uint32_t data_size; // Size (in bytes) of |data|.
+
+ EncryptionScheme encryption_scheme;
+
+ const uint8_t* key_id; // Key ID to identify the decryption key.
+ uint32_t key_id_size; // Size (in bytes) of |key_id|.
+ uint32_t : 32; // Padding.
+
+ const uint8_t* iv; // Initialization vector.
+ uint32_t iv_size; // Size (in bytes) of |iv|.
+ uint32_t : 32; // Padding.
+
+ const struct SubsampleEntry* subsamples;
+ uint32_t num_subsamples; // Number of subsamples in |subsamples|.
+ uint32_t : 32; // Padding.
+
+ // |pattern| is required if |encryption_scheme| specifies pattern encryption.
+ Pattern pattern;
+
+ int64_t timestamp; // Presentation timestamp in microseconds.
+};
+CHECK_TYPE(InputBuffer_2, 64, 80);
+
+enum AudioCodec : uint32_t { kUnknownAudioCodec = 0, kCodecVorbis, kCodecAac };
+CHECK_TYPE(AudioCodec, 4, 4);
+
+struct AudioDecoderConfig_2 {
+ AudioCodec codec;
+ int32_t channel_count;
+ int32_t bits_per_channel;
+ int32_t samples_per_second;
+
+ // Optional byte data required to initialize audio decoders, such as the
+ // vorbis setup header.
+ uint8_t* extra_data;
+ uint32_t extra_data_size;
+
+ // Encryption scheme.
+ EncryptionScheme encryption_scheme;
+};
+CHECK_TYPE(AudioDecoderConfig_2, 28, 32);
+
+// Supported sample formats for AudioFrames.
+enum AudioFormat : uint32_t {
+ kUnknownAudioFormat = 0, // Unknown format value. Used for error reporting.
+ kAudioFormatU8, // Interleaved unsigned 8-bit w/ bias of 128.
+ kAudioFormatS16, // Interleaved signed 16-bit.
+ kAudioFormatS32, // Interleaved signed 32-bit.
+ kAudioFormatF32, // Interleaved float 32-bit.
+ kAudioFormatPlanarS16, // Signed 16-bit planar.
+ kAudioFormatPlanarF32, // Float 32-bit planar.
+};
+CHECK_TYPE(AudioFormat, 4, 4);
+
+// Surface formats based on FOURCC labels, see: http://www.fourcc.org/yuv.php
+// Values are chosen to be consistent with Chromium's VideoPixelFormat values.
+enum VideoFormat : uint32_t {
+ kUnknownVideoFormat = 0, // Unknown format value. Used for error reporting.
+ kYv12 = 1, // 12bpp YVU planar 1x1 Y, 2x2 VU samples.
+ kI420 = 2, // 12bpp YUV planar 1x1 Y, 2x2 UV samples.
+
+ // In the following formats, each sample uses 16-bit in storage, while the
+ // sample value is stored in the least significant N bits where N is
+ // specified by the number after "P". For example, for YUV420P9, each Y, U,
+ // and V sample is stored in the least significant 9 bits in a 2-byte block.
+ kYUV420P9 = 16,
+ kYUV420P10 = 17,
+ kYUV422P9 = 18,
+ kYUV422P10 = 19,
+ kYUV444P9 = 20,
+ kYUV444P10 = 21,
+ kYUV420P12 = 22,
+ kYUV422P12 = 23,
+ kYUV444P12 = 24,
+};
+CHECK_TYPE(VideoFormat, 4, 4);
+
+struct Size {
+ int32_t width;
+ int32_t height;
+};
+CHECK_TYPE(Size, 8, 8);
+
+enum VideoCodec : uint32_t {
+ kUnknownVideoCodec = 0,
+ kCodecVp8,
+ kCodecH264,
+ kCodecVp9,
+ kCodecAv1
+};
+CHECK_TYPE(VideoCodec, 4, 4);
+
+enum VideoCodecProfile : uint32_t {
+ kUnknownVideoCodecProfile = 0,
+ kProfileNotNeeded,
+ kH264ProfileBaseline,
+ kH264ProfileMain,
+ kH264ProfileExtended,
+ kH264ProfileHigh,
+ kH264ProfileHigh10,
+ kH264ProfileHigh422,
+ kH264ProfileHigh444Predictive,
+ kVP9Profile0,
+ kVP9Profile1,
+ kVP9Profile2,
+ kVP9Profile3,
+ kAv1ProfileMain,
+ kAv1ProfileHigh,
+ kAv1ProfilePro
+};
+CHECK_TYPE(VideoCodecProfile, 4, 4);
+
+// Deprecated: New CDM implementations should use VideoDecoderConfig_3.
+// Note that this struct is organized so that sizeof(VideoDecoderConfig_2)
+// equals the sum of sizeof() all members in both 32-bit and 64-bit compiles.
+// Padding has been added to keep the fields aligned.
+struct VideoDecoderConfig_2 {
+ VideoCodec codec;
+ VideoCodecProfile profile;
+ VideoFormat format;
+ uint32_t : 32; // Padding.
+
+ // Width and height of video frame immediately post-decode. Not all pixels
+ // in this region are valid.
+ Size coded_size;
+
+ // Optional byte data required to initialize video decoders, such as H.264
+ // AAVC data.
+ uint8_t* extra_data;
+ uint32_t extra_data_size;
+
+ // Encryption scheme.
+ EncryptionScheme encryption_scheme;
+};
+CHECK_TYPE(VideoDecoderConfig_2, 36, 40);
+
+struct VideoDecoderConfig_3 {
+ VideoCodec codec;
+ VideoCodecProfile profile;
+ VideoFormat format;
+ ColorSpace color_space;
+
+ // Width and height of video frame immediately post-decode. Not all pixels
+ // in this region are valid.
+ Size coded_size;
+
+ // Optional byte data required to initialize video decoders, such as H.264
+ // AAVC data.
+ uint8_t* extra_data;
+ uint32_t extra_data_size;
+
+ EncryptionScheme encryption_scheme;
+};
+CHECK_TYPE(VideoDecoderConfig_3, 36, 40);
+
+enum StreamType : uint32_t { kStreamTypeAudio = 0, kStreamTypeVideo = 1 };
+CHECK_TYPE(StreamType, 4, 4);
+
+// Structure provided to ContentDecryptionModule::OnPlatformChallengeResponse()
+// after a platform challenge was initiated via Host::SendPlatformChallenge().
+// All values will be NULL / zero in the event of a challenge failure.
+struct PlatformChallengeResponse {
+ // |challenge| provided during Host::SendPlatformChallenge() combined with
+ // nonce data and signed with the platform's private key.
+ const uint8_t* signed_data;
+ uint32_t signed_data_length;
+
+ // RSASSA-PKCS1-v1_5-SHA256 signature of the |signed_data| block.
+ const uint8_t* signed_data_signature;
+ uint32_t signed_data_signature_length;
+
+ // X.509 device specific certificate for the |service_id| requested.
+ const uint8_t* platform_key_certificate;
+ uint32_t platform_key_certificate_length;
+};
+CHECK_TYPE(PlatformChallengeResponse, 24, 48);
+
+// The current status of the associated key. The valid types are defined in the
+// spec: https://w3c.github.io/encrypted-media/#dom-mediakeystatus
+enum KeyStatus : uint32_t {
+ kUsable = 0,
+ kInternalError = 1,
+ kExpired = 2,
+ kOutputRestricted = 3,
+ kOutputDownscaled = 4,
+ kStatusPending = 5,
+ kReleased = 6
+};
+CHECK_TYPE(KeyStatus, 4, 4);
+
+// Used when passing arrays of key information. Does not own the referenced
+// data. |system_code| is an additional error code for unusable keys and
+// should be 0 when |status| == kUsable.
+struct KeyInformation {
+ const uint8_t* key_id;
+ uint32_t key_id_size;
+ KeyStatus status;
+ uint32_t system_code;
+};
+CHECK_TYPE(KeyInformation, 16, 24);
+
+// Supported output protection methods for use with EnableOutputProtection() and
+// returned by OnQueryOutputProtectionStatus().
+enum OutputProtectionMethods : uint32_t {
+ kProtectionNone = 0,
+ kProtectionHDCP = 1 << 0
+};
+CHECK_TYPE(OutputProtectionMethods, 4, 4);
+
+// Connected output link types returned by OnQueryOutputProtectionStatus().
+enum OutputLinkTypes : uint32_t {
+ kLinkTypeNone = 0,
+ kLinkTypeUnknown = 1 << 0,
+ kLinkTypeInternal = 1 << 1,
+ kLinkTypeVGA = 1 << 2,
+ kLinkTypeHDMI = 1 << 3,
+ kLinkTypeDVI = 1 << 4,
+ kLinkTypeDisplayPort = 1 << 5,
+ kLinkTypeNetwork = 1 << 6
+};
+CHECK_TYPE(OutputLinkTypes, 4, 4);
+
+// Result of the QueryOutputProtectionStatus() call.
+enum QueryResult : uint32_t { kQuerySucceeded = 0, kQueryFailed };
+CHECK_TYPE(QueryResult, 4, 4);
+
+// The Initialization Data Type. The valid types are defined in the spec:
+// https://w3c.github.io/encrypted-media/format-registry/initdata/index.html#registry
+enum InitDataType : uint32_t { kCenc = 0, kKeyIds = 1, kWebM = 2 };
+CHECK_TYPE(InitDataType, 4, 4);
+
+// The type of session to create. The valid types are defined in the spec:
+// https://w3c.github.io/encrypted-media/#dom-mediakeysessiontype
+enum SessionType : uint32_t {
+ kTemporary = 0,
+ kPersistentLicense = 1,
+ kPersistentUsageRecord = 2
+};
+CHECK_TYPE(SessionType, 4, 4);
+
+// The type of the message event. The valid types are defined in the spec:
+// https://w3c.github.io/encrypted-media/#dom-mediakeymessagetype
+enum MessageType : uint32_t {
+ kLicenseRequest = 0,
+ kLicenseRenewal = 1,
+ kLicenseRelease = 2,
+ kIndividualizationRequest = 3
+};
+CHECK_TYPE(MessageType, 4, 4);
+
+enum HdcpVersion : uint32_t {
+ kHdcpVersionNone,
+ kHdcpVersion1_0,
+ kHdcpVersion1_1,
+ kHdcpVersion1_2,
+ kHdcpVersion1_3,
+ kHdcpVersion1_4,
+ kHdcpVersion2_0,
+ kHdcpVersion2_1,
+ kHdcpVersion2_2,
+ kHdcpVersion2_3
+};
+CHECK_TYPE(HdcpVersion, 4, 4);
+
+struct Policy {
+ HdcpVersion min_hdcp_version;
+};
+CHECK_TYPE(Policy, 4, 4);
+
+// Represents a buffer created by Allocator implementations.
+class CDM_CLASS_API Buffer {
+ public:
+ // Destroys the buffer in the same context as it was created.
+ virtual void Destroy() = 0;
+
+ virtual uint32_t Capacity() const = 0;
+ virtual uint8_t* Data() = 0;
+ virtual void SetSize(uint32_t size) = 0;
+ virtual uint32_t Size() const = 0;
+
+ protected:
+ Buffer() {}
+ virtual ~Buffer() {}
+
+ private:
+ Buffer(const Buffer&);
+ void operator=(const Buffer&);
+};
+
+// Represents a decrypted block that has not been decoded.
+class CDM_CLASS_API DecryptedBlock {
+ public:
+ virtual void SetDecryptedBuffer(Buffer* buffer) = 0;
+ virtual Buffer* DecryptedBuffer() = 0;
+
+ // TODO(tomfinegan): Figure out if timestamp is really needed. If it is not,
+ // we can just pass Buffer pointers around.
+ virtual void SetTimestamp(int64_t timestamp) = 0;
+ virtual int64_t Timestamp() const = 0;
+
+ protected:
+ DecryptedBlock() {}
+ virtual ~DecryptedBlock() {}
+};
+
+enum VideoPlane : uint32_t {
+ kYPlane = 0,
+ kUPlane = 1,
+ kVPlane = 2,
+ kMaxPlanes = 3,
+};
+CHECK_TYPE(VideoPlane, 4, 4);
+
+class CDM_CLASS_API VideoFrame {
+ public:
+ virtual void SetFormat(VideoFormat format) = 0;
+ virtual VideoFormat Format() const = 0;
+
+ virtual void SetSize(cdm::Size size) = 0;
+ virtual cdm::Size Size() const = 0;
+
+ virtual void SetFrameBuffer(Buffer* frame_buffer) = 0;
+ virtual Buffer* FrameBuffer() = 0;
+
+ virtual void SetPlaneOffset(VideoPlane plane, uint32_t offset) = 0;
+ virtual uint32_t PlaneOffset(VideoPlane plane) = 0;
+
+ virtual void SetStride(VideoPlane plane, uint32_t stride) = 0;
+ virtual uint32_t Stride(VideoPlane plane) = 0;
+
+ // Sets and gets the presentation timestamp which is in microseconds.
+ virtual void SetTimestamp(int64_t timestamp) = 0;
+ virtual int64_t Timestamp() const = 0;
+
+ protected:
+ VideoFrame() {}
+ virtual ~VideoFrame() {}
+};
+
+// Represents a decoded video frame. The CDM should call the interface methods
+// to set the frame attributes. See DecryptAndDecodeFrame().
+class CDM_CLASS_API VideoFrame_2 {
+ public:
+ virtual void SetFormat(VideoFormat format) = 0;
+ virtual void SetSize(cdm::Size size) = 0;
+ virtual void SetFrameBuffer(Buffer* frame_buffer) = 0;
+ virtual void SetPlaneOffset(VideoPlane plane, uint32_t offset) = 0;
+ virtual void SetStride(VideoPlane plane, uint32_t stride) = 0;
+ // Sets the presentation timestamp which is in microseconds.
+ virtual void SetTimestamp(int64_t timestamp) = 0;
+ virtual void SetColorSpace(ColorSpace color_space) = 0;
+
+ protected:
+ VideoFrame_2() {}
+ virtual ~VideoFrame_2() {}
+};
+
+// Represents decrypted and decoded audio frames. AudioFrames can contain
+// multiple audio output buffers, which are serialized into this format:
+//
+// |<------------------- serialized audio buffer ------------------->|
+// | int64_t timestamp | int64_t length | length bytes of audio data |
+//
+// For example, with three audio output buffers, the AudioFrames will look
+// like this:
+//
+// |<----------------- AudioFrames ------------------>|
+// | audio buffer 0 | audio buffer 1 | audio buffer 2 |
+class CDM_CLASS_API AudioFrames {
+ public:
+ virtual void SetFrameBuffer(Buffer* buffer) = 0;
+ virtual Buffer* FrameBuffer() = 0;
+
+ // The CDM must call this method, providing a valid format, when providing
+ // frame buffers. Planar data should be stored end to end; e.g.,
+ // |ch1 sample1||ch1 sample2|....|ch1 sample_last||ch2 sample1|...
+ virtual void SetFormat(AudioFormat format) = 0;
+ virtual AudioFormat Format() const = 0;
+
+ protected:
+ AudioFrames() {}
+ virtual ~AudioFrames() {}
+};
+
+// FileIO interface provides a way for the CDM to store data in a file in
+// persistent storage. This interface aims only at providing basic read/write
+// capabilities and should not be used as a full fledged file IO API.
+// Each CDM and origin (e.g. HTTPS, "foo.example.com", 443) combination has
+// its own persistent storage. All instances of a given CDM associated with a
+// given origin share the same persistent storage.
+// Note to implementors of this interface:
+// Per-origin storage and the ability for users to clear it are important.
+// See http://www.w3.org/TR/encrypted-media/#privacy-storedinfo.
+class CDM_CLASS_API FileIO {
+ public:
+ // Opens the file with |file_name| for read and write.
+ // FileIOClient::OnOpenComplete() will be called after the opening
+ // operation finishes.
+ // - When the file is opened by a CDM instance, it will be classified as "in
+ // use". In this case other CDM instances in the same domain may receive
+ // kInUse status when trying to open it.
+ // - |file_name| must only contain letters (A-Za-z), digits(0-9), or "._-".
+ // It must not start with an underscore ('_'), and must be at least 1
+ // character and no more than 256 characters long.
+ virtual void Open(const char* file_name, uint32_t file_name_size) = 0;
+
+ // Reads the contents of the file. FileIOClient::OnReadComplete() will be
+ // called with the read status. Read() should not be called if a previous
+ // Read() or Write() call is still pending; otherwise OnReadComplete() will
+ // be called with kInUse.
+ virtual void Read() = 0;
+
+ // Writes |data_size| bytes of |data| into the file.
+ // FileIOClient::OnWriteComplete() will be called with the write status.
+ // All existing contents in the file will be overwritten. Calling Write() with
+ // NULL |data| will clear all contents in the file. Write() should not be
+ // called if a previous Write() or Read() call is still pending; otherwise
+ // OnWriteComplete() will be called with kInUse.
+ virtual void Write(const uint8_t* data, uint32_t data_size) = 0;
+
+ // Closes the file if opened, destroys this FileIO object and releases any
+ // resources allocated. The CDM must call this method when it finished using
+ // this object. A FileIO object must not be used after Close() is called.
+ virtual void Close() = 0;
+
+ protected:
+ FileIO() {}
+ virtual ~FileIO() {}
+};
+
+// Responses to FileIO calls. All responses will be called asynchronously.
+// When kError is returned, the FileIO object could be in an error state. All
+// following calls (other than Close()) could return kError. The CDM should
+// still call Close() to destroy the FileIO object.
+class CDM_CLASS_API FileIOClient {
+ public:
+ enum class Status : uint32_t { kSuccess = 0, kInUse, kError };
+
+ // Response to a FileIO::Open() call with the open |status|.
+ virtual void OnOpenComplete(Status status) = 0;
+
+ // Response to a FileIO::Read() call to provide |data_size| bytes of |data|
+ // read from the file.
+ // - kSuccess indicates that all contents of the file has been successfully
+ // read. In this case, 0 |data_size| means that the file is empty.
+ // - kInUse indicates that there are other read/write operations pending.
+ // - kError indicates read failure, e.g. the storage is not open or cannot be
+ // fully read.
+ virtual void OnReadComplete(Status status, const uint8_t* data,
+ uint32_t data_size) = 0;
+
+ // Response to a FileIO::Write() call.
+ // - kSuccess indicates that all the data has been written into the file
+ // successfully.
+ // - kInUse indicates that there are other read/write operations pending.
+ // - kError indicates write failure, e.g. the storage is not open or cannot be
+ // fully written. Upon write failure, the contents of the file should be
+ // regarded as corrupt and should not used.
+ virtual void OnWriteComplete(Status status) = 0;
+
+ protected:
+ FileIOClient() {}
+ virtual ~FileIOClient() {}
+};
+
+class CDM_CLASS_API Host_10;
+class CDM_CLASS_API Host_11;
+
+// ContentDecryptionModule interface that all CDMs need to implement.
+// The interface is versioned for backward compatibility.
+// Note: ContentDecryptionModule implementations must use the allocator
+// provided in CreateCdmInstance() to allocate any Buffer that needs to
+// be passed back to the caller. Implementations must call Buffer::Destroy()
+// when a Buffer is created that will never be returned to the caller.
+class CDM_CLASS_API ContentDecryptionModule_10 {
+ public:
+ static const int kVersion = 10;
+ static const bool kIsStable = true;
+ typedef Host_10 Host;
+
+ // Initializes the CDM instance, providing information about permitted
+ // functionalities. The CDM must respond by calling Host::OnInitialized()
+ // with whether the initialization succeeded. No other calls will be made by
+ // the host before Host::OnInitialized() returns.
+ // If |allow_distinctive_identifier| is false, messages from the CDM,
+ // such as message events, must not contain a Distinctive Identifier,
+ // even in an encrypted form.
+ // If |allow_persistent_state| is false, the CDM must not attempt to
+ // persist state. Calls to CreateFileIO() will fail.
+ // If |use_hw_secure_codecs| is true, the CDM must ensure the decryption key
+ // and video buffers (compressed and uncompressed) are securely protected by
+ // hardware.
+ virtual void Initialize(bool allow_distinctive_identifier,
+ bool allow_persistent_state,
+ bool use_hw_secure_codecs) = 0;
+
+ // Gets the key status if the CDM has a hypothetical key with the |policy|.
+ // The CDM must respond by calling either Host::OnResolveKeyStatusPromise()
+ // with the result key status or Host::OnRejectPromise() if an unexpected
+ // error happened or this method is not supported.
+ virtual void GetStatusForPolicy(uint32_t promise_id,
+ const Policy& policy) = 0;
+
+ // SetServerCertificate(), CreateSessionAndGenerateRequest(), LoadSession(),
+ // UpdateSession(), CloseSession(), and RemoveSession() all accept a
+ // |promise_id|, which must be passed to the completion Host method
+ // (e.g. Host::OnResolveNewSessionPromise()).
+
+ // Provides a server certificate to be used to encrypt messages to the
+ // license server. The CDM must respond by calling either
+ // Host::OnResolvePromise() or Host::OnRejectPromise().
+ // If the CDM does not support server certificates, the promise should be
+ // rejected with kExceptionNotSupportedError. If |server_certificate_data|
+ // is empty, reject with kExceptionTypeError. Any other error should be
+ // rejected with kExceptionInvalidStateError or kExceptionQuotaExceededError.
+ // TODO(crbug.com/796417): Add support for the promise to return true or
+ // false, rather than using kExceptionNotSupportedError to mean false.
+ virtual void SetServerCertificate(uint32_t promise_id,
+ const uint8_t* server_certificate_data,
+ uint32_t server_certificate_data_size) = 0;
+
+ // Creates a session given |session_type|, |init_data_type|, and |init_data|.
+ // The CDM must respond by calling either Host::OnResolveNewSessionPromise()
+ // or Host::OnRejectPromise().
+ virtual void CreateSessionAndGenerateRequest(uint32_t promise_id,
+ SessionType session_type,
+ InitDataType init_data_type,
+ const uint8_t* init_data,
+ uint32_t init_data_size) = 0;
+
+ // Loads the session of type |session_type| specified by |session_id|.
+ // The CDM must respond by calling either Host::OnResolveNewSessionPromise()
+ // or Host::OnRejectPromise(). If the session is not found, call
+ // Host::OnResolveNewSessionPromise() with session_id = NULL.
+ virtual void LoadSession(uint32_t promise_id, SessionType session_type,
+ const char* session_id,
+ uint32_t session_id_size) = 0;
+
+ // Updates the session with |response|. The CDM must respond by calling
+ // either Host::OnResolvePromise() or Host::OnRejectPromise().
+ virtual void UpdateSession(uint32_t promise_id, const char* session_id,
+ uint32_t session_id_size, const uint8_t* response,
+ uint32_t response_size) = 0;
+
+ // Requests that the CDM close the session. The CDM must respond by calling
+ // either Host::OnResolvePromise() or Host::OnRejectPromise() when the request
+ // has been processed. This may be before the session is closed. Once the
+ // session is closed, Host::OnSessionClosed() must also be called.
+ virtual void CloseSession(uint32_t promise_id, const char* session_id,
+ uint32_t session_id_size) = 0;
+
+ // Removes any stored session data associated with this session. Will only be
+ // called for persistent sessions. The CDM must respond by calling either
+ // Host::OnResolvePromise() or Host::OnRejectPromise() when the request has
+ // been processed.
+ virtual void RemoveSession(uint32_t promise_id, const char* session_id,
+ uint32_t session_id_size) = 0;
+
+ // Performs scheduled operation with |context| when the timer fires.
+ virtual void TimerExpired(void* context) = 0;
+
+ // Decrypts the |encrypted_buffer|.
+ //
+ // Returns kSuccess if decryption succeeded, in which case the callee
+ // should have filled the |decrypted_buffer| and passed the ownership of
+ // |data| in |decrypted_buffer| to the caller.
+ // Returns kNoKey if the CDM did not have the necessary decryption key
+ // to decrypt.
+ // Returns kDecryptError if any other error happened.
+ // If the return value is not kSuccess, |decrypted_buffer| should be ignored
+ // by the caller.
+ virtual Status Decrypt(const InputBuffer_2& encrypted_buffer,
+ DecryptedBlock* decrypted_buffer) = 0;
+
+ // Initializes the CDM audio decoder with |audio_decoder_config|. This
+ // function must be called before DecryptAndDecodeSamples() is called.
+ //
+ // Returns kSuccess if the |audio_decoder_config| is supported and the CDM
+ // audio decoder is successfully initialized.
+ // Returns kInitializationError if |audio_decoder_config| is not supported.
+ // The CDM may still be able to do Decrypt().
+ // Returns kDeferredInitialization if the CDM is not ready to initialize the
+ // decoder at this time. Must call Host::OnDeferredInitializationDone() once
+ // initialization is complete.
+ virtual Status InitializeAudioDecoder(
+ const AudioDecoderConfig_2& audio_decoder_config) = 0;
+
+ // Initializes the CDM video decoder with |video_decoder_config|. This
+ // function must be called before DecryptAndDecodeFrame() is called.
+ //
+ // Returns kSuccess if the |video_decoder_config| is supported and the CDM
+ // video decoder is successfully initialized.
+ // Returns kInitializationError if |video_decoder_config| is not supported.
+ // The CDM may still be able to do Decrypt().
+ // Returns kDeferredInitialization if the CDM is not ready to initialize the
+ // decoder at this time. Must call Host::OnDeferredInitializationDone() once
+ // initialization is complete.
+ virtual Status InitializeVideoDecoder(
+ const VideoDecoderConfig_2& video_decoder_config) = 0;
+
+ // De-initializes the CDM decoder and sets it to an uninitialized state. The
+ // caller can initialize the decoder again after this call to re-initialize
+ // it. This can be used to reconfigure the decoder if the configuration
+ // changes.
+ virtual void DeinitializeDecoder(StreamType decoder_type) = 0;
+
+ // Resets the CDM decoder to an initialized clean state. All internal buffers
+ // MUST be flushed.
+ virtual void ResetDecoder(StreamType decoder_type) = 0;
+
+ // Decrypts the |encrypted_buffer| and decodes the decrypted buffer into a
+ // |video_frame|. Upon end-of-stream, the caller should call this function
+ // repeatedly with empty |encrypted_buffer| (|data| == NULL) until
+ // kNeedMoreData is returned.
+ //
+ // Returns kSuccess if decryption and decoding both succeeded, in which case
+ // the callee will have filled the |video_frame| and passed the ownership of
+ // |frame_buffer| in |video_frame| to the caller.
+ // Returns kNoKey if the CDM did not have the necessary decryption key
+ // to decrypt.
+ // Returns kNeedMoreData if more data was needed by the decoder to generate
+ // a decoded frame (e.g. during initialization and end-of-stream).
+ // Returns kDecryptError if any decryption error happened.
+ // Returns kDecodeError if any decoding error happened.
+ // If the return value is not kSuccess, |video_frame| should be ignored by
+ // the caller.
+ virtual Status DecryptAndDecodeFrame(const InputBuffer_2& encrypted_buffer,
+ VideoFrame* video_frame) = 0;
+
+ // Decrypts the |encrypted_buffer| and decodes the decrypted buffer into
+ // |audio_frames|. Upon end-of-stream, the caller should call this function
+ // repeatedly with empty |encrypted_buffer| (|data| == NULL) until only empty
+ // |audio_frames| is produced.
+ //
+ // Returns kSuccess if decryption and decoding both succeeded, in which case
+ // the callee will have filled |audio_frames| and passed the ownership of
+ // |data| in |audio_frames| to the caller.
+ // Returns kNoKey if the CDM did not have the necessary decryption key
+ // to decrypt.
+ // Returns kNeedMoreData if more data was needed by the decoder to generate
+ // audio samples (e.g. during initialization and end-of-stream).
+ // Returns kDecryptError if any decryption error happened.
+ // Returns kDecodeError if any decoding error happened.
+ // If the return value is not kSuccess, |audio_frames| should be ignored by
+ // the caller.
+ virtual Status DecryptAndDecodeSamples(const InputBuffer_2& encrypted_buffer,
+ AudioFrames* audio_frames) = 0;
+
+ // Called by the host after a platform challenge was initiated via
+ // Host::SendPlatformChallenge().
+ virtual void OnPlatformChallengeResponse(
+ const PlatformChallengeResponse& response) = 0;
+
+ // Called by the host after a call to Host::QueryOutputProtectionStatus(). The
+ // |link_mask| is a bit mask of OutputLinkTypes and |output_protection_mask|
+ // is a bit mask of OutputProtectionMethods. If |result| is kQueryFailed,
+ // then |link_mask| and |output_protection_mask| are undefined and should
+ // be ignored.
+ virtual void OnQueryOutputProtectionStatus(
+ QueryResult result, uint32_t link_mask,
+ uint32_t output_protection_mask) = 0;
+
+ // Called by the host after a call to Host::RequestStorageId(). If the
+ // version of the storage ID requested is available, |storage_id| and
+ // |storage_id_size| are set appropriately. |version| will be the same as
+ // what was requested, unless 0 (latest) was requested, in which case
+ // |version| will be the actual version number for the |storage_id| returned.
+ // If the requested version is not available, null/zero will be provided as
+ // |storage_id| and |storage_id_size|, respectively, and |version| should be
+ // ignored.
+ virtual void OnStorageId(uint32_t version, const uint8_t* storage_id,
+ uint32_t storage_id_size) = 0;
+
+ // Destroys the object in the same context as it was created.
+ virtual void Destroy() = 0;
+
+ protected:
+ ContentDecryptionModule_10() {}
+ virtual ~ContentDecryptionModule_10() {}
+};
+
+// ----- Note: CDM interface(s) below still in development and not stable! -----
+
+// ContentDecryptionModule interface that all CDMs need to implement.
+// The interface is versioned for backward compatibility.
+// Note: ContentDecryptionModule implementations must use the allocator
+// provided in CreateCdmInstance() to allocate any Buffer that needs to
+// be passed back to the caller. Implementations must call Buffer::Destroy()
+// when a Buffer is created that will never be returned to the caller.
+class CDM_CLASS_API ContentDecryptionModule_11 {
+ public:
+ static const int kVersion = 11;
+ static const bool kIsStable = false;
+ typedef Host_11 Host;
+
+ // Initializes the CDM instance, providing information about permitted
+ // functionalities. The CDM must respond by calling Host::OnInitialized()
+ // with whether the initialization succeeded. No other calls will be made by
+ // the host before Host::OnInitialized() returns.
+ // If |allow_distinctive_identifier| is false, messages from the CDM,
+ // such as message events, must not contain a Distinctive Identifier,
+ // even in an encrypted form.
+ // If |allow_persistent_state| is false, the CDM must not attempt to
+ // persist state. Calls to CreateFileIO() will fail.
+ // If |use_hw_secure_codecs| is true, the CDM must ensure the decryption key
+ // and video buffers (compressed and uncompressed) are securely protected by
+ // hardware.
+ virtual void Initialize(bool allow_distinctive_identifier,
+ bool allow_persistent_state,
+ bool use_hw_secure_codecs) = 0;
+
+ // Gets the key status if the CDM has a hypothetical key with the |policy|.
+ // The CDM must respond by calling either Host::OnResolveKeyStatusPromise()
+ // with the result key status or Host::OnRejectPromise() if an unexpected
+ // error happened or this method is not supported.
+ virtual void GetStatusForPolicy(uint32_t promise_id,
+ const Policy& policy) = 0;
+
+ // SetServerCertificate(), CreateSessionAndGenerateRequest(), LoadSession(),
+ // UpdateSession(), CloseSession(), and RemoveSession() all accept a
+ // |promise_id|, which must be passed to the completion Host method
+ // (e.g. Host::OnResolveNewSessionPromise()).
+
+ // Provides a server certificate to be used to encrypt messages to the
+ // license server. The CDM must respond by calling either
+ // Host::OnResolvePromise() or Host::OnRejectPromise().
+ // If the CDM does not support server certificates, the promise should be
+ // rejected with kExceptionNotSupportedError. If |server_certificate_data|
+ // is empty, reject with kExceptionTypeError. Any other error should be
+ // rejected with kExceptionInvalidStateError or kExceptionQuotaExceededError.
+ // TODO(crbug.com/796417): Add support for the promise to return true or
+ // false, rather than using kExceptionNotSupportedError to mean false.
+ virtual void SetServerCertificate(uint32_t promise_id,
+ const uint8_t* server_certificate_data,
+ uint32_t server_certificate_data_size) = 0;
+
+ // Creates a session given |session_type|, |init_data_type|, and |init_data|.
+ // The CDM must respond by calling either Host::OnResolveNewSessionPromise()
+ // or Host::OnRejectPromise().
+ virtual void CreateSessionAndGenerateRequest(uint32_t promise_id,
+ SessionType session_type,
+ InitDataType init_data_type,
+ const uint8_t* init_data,
+ uint32_t init_data_size) = 0;
+
+ // Loads the session of type |session_type| specified by |session_id|.
+ // The CDM must respond by calling either Host::OnResolveNewSessionPromise()
+ // or Host::OnRejectPromise(). If the session is not found, call
+ // Host::OnResolveNewSessionPromise() with session_id = NULL.
+ virtual void LoadSession(uint32_t promise_id, SessionType session_type,
+ const char* session_id,
+ uint32_t session_id_size) = 0;
+
+ // Updates the session with |response|. The CDM must respond by calling
+ // either Host::OnResolvePromise() or Host::OnRejectPromise().
+ virtual void UpdateSession(uint32_t promise_id, const char* session_id,
+ uint32_t session_id_size, const uint8_t* response,
+ uint32_t response_size) = 0;
+
+ // Requests that the CDM close the session. The CDM must respond by calling
+ // either Host::OnResolvePromise() or Host::OnRejectPromise() when the request
+ // has been processed. This may be before the session is closed. Once the
+ // session is closed, Host::OnSessionClosed() must also be called.
+ virtual void CloseSession(uint32_t promise_id, const char* session_id,
+ uint32_t session_id_size) = 0;
+
+ // Removes any stored session data associated with this session. Removes all
+ // license(s) and key(s) associated with the session, whether they are in
+ // memory, persistent store, or both. For persistent session types, other
+ // session data (e.g. record of license destruction) will be cleared as
+ // defined for each session type once a release message acknowledgment is
+ // processed by UpdateSession(). The CDM must respond by calling either
+ // Host::OnResolvePromise() or Host::OnRejectPromise() when the request has
+ // been processed.
+ virtual void RemoveSession(uint32_t promise_id, const char* session_id,
+ uint32_t session_id_size) = 0;
+
+ // Performs scheduled operation with |context| when the timer fires.
+ virtual void TimerExpired(void* context) = 0;
+
+ // Decrypts the |encrypted_buffer|.
+ //
+ // Returns kSuccess if decryption succeeded, in which case the callee
+ // should have filled the |decrypted_buffer| and passed the ownership of
+ // |data| in |decrypted_buffer| to the caller.
+ // Returns kNoKey if the CDM did not have the necessary decryption key
+ // to decrypt.
+ // Returns kDecryptError if any other error happened.
+ // If the return value is not kSuccess, |decrypted_buffer| should be ignored
+ // by the caller.
+ virtual Status Decrypt(const InputBuffer_2& encrypted_buffer,
+ DecryptedBlock* decrypted_buffer) = 0;
+
+ // Initializes the CDM audio decoder with |audio_decoder_config|. This
+ // function must be called before DecryptAndDecodeSamples() is called.
+ //
+ // Returns kSuccess if the |audio_decoder_config| is supported and the CDM
+ // audio decoder is successfully initialized.
+ // Returns kInitializationError if |audio_decoder_config| is not supported.
+ // The CDM may still be able to do Decrypt().
+ // Returns kDeferredInitialization if the CDM is not ready to initialize the
+ // decoder at this time. Must call Host::OnDeferredInitializationDone() once
+ // initialization is complete.
+ virtual Status InitializeAudioDecoder(
+ const AudioDecoderConfig_2& audio_decoder_config) = 0;
+
+ // Initializes the CDM video decoder with |video_decoder_config|. This
+ // function must be called before DecryptAndDecodeFrame() is called.
+ //
+ // Returns kSuccess if the |video_decoder_config| is supported and the CDM
+ // video decoder is successfully initialized.
+ // Returns kInitializationError if |video_decoder_config| is not supported.
+ // The CDM may still be able to do Decrypt().
+ // Returns kDeferredInitialization if the CDM is not ready to initialize the
+ // decoder at this time. Must call Host::OnDeferredInitializationDone() once
+ // initialization is complete.
+ virtual Status InitializeVideoDecoder(
+ const VideoDecoderConfig_3& video_decoder_config) = 0;
+
+ // De-initializes the CDM decoder and sets it to an uninitialized state. The
+ // caller can initialize the decoder again after this call to re-initialize
+ // it. This can be used to reconfigure the decoder if the configuration
+ // changes.
+ virtual void DeinitializeDecoder(StreamType decoder_type) = 0;
+
+ // Resets the CDM decoder to an initialized clean state. All internal buffers
+ // MUST be flushed.
+ virtual void ResetDecoder(StreamType decoder_type) = 0;
+
+ // Decrypts the |encrypted_buffer| and decodes the decrypted buffer into a
+ // |video_frame|. Upon end-of-stream, the caller should call this function
+ // repeatedly with empty |encrypted_buffer| (|data| == NULL) until
+ // kNeedMoreData is returned.
+ //
+ // Returns kSuccess if decryption and decoding both succeeded, in which case
+ // the callee will have filled the |video_frame| and passed the ownership of
+ // |frame_buffer| in |video_frame| to the caller.
+ // Returns kNoKey if the CDM did not have the necessary decryption key
+ // to decrypt.
+ // Returns kNeedMoreData if more data was needed by the decoder to generate
+ // a decoded frame (e.g. during initialization and end-of-stream).
+ // Returns kDecryptError if any decryption error happened.
+ // Returns kDecodeError if any decoding error happened.
+ // If the return value is not kSuccess, |video_frame| should be ignored by
+ // the caller.
+ virtual Status DecryptAndDecodeFrame(const InputBuffer_2& encrypted_buffer,
+ VideoFrame_2* video_frame) = 0;
+
+ // Decrypts the |encrypted_buffer| and decodes the decrypted buffer into
+ // |audio_frames|. Upon end-of-stream, the caller should call this function
+ // repeatedly with empty |encrypted_buffer| (|data| == NULL) until only empty
+ // |audio_frames| is produced.
+ //
+ // Returns kSuccess if decryption and decoding both succeeded, in which case
+ // the callee will have filled |audio_frames| and passed the ownership of
+ // |data| in |audio_frames| to the caller.
+ // Returns kNoKey if the CDM did not have the necessary decryption key
+ // to decrypt.
+ // Returns kNeedMoreData if more data was needed by the decoder to generate
+ // audio samples (e.g. during initialization and end-of-stream).
+ // Returns kDecryptError if any decryption error happened.
+ // Returns kDecodeError if any decoding error happened.
+ // If the return value is not kSuccess, |audio_frames| should be ignored by
+ // the caller.
+ virtual Status DecryptAndDecodeSamples(const InputBuffer_2& encrypted_buffer,
+ AudioFrames* audio_frames) = 0;
+
+ // Called by the host after a platform challenge was initiated via
+ // Host::SendPlatformChallenge().
+ virtual void OnPlatformChallengeResponse(
+ const PlatformChallengeResponse& response) = 0;
+
+ // Called by the host after a call to Host::QueryOutputProtectionStatus(). The
+ // |link_mask| is a bit mask of OutputLinkTypes and |output_protection_mask|
+ // is a bit mask of OutputProtectionMethods. If |result| is kQueryFailed,
+ // then |link_mask| and |output_protection_mask| are undefined and should
+ // be ignored.
+ virtual void OnQueryOutputProtectionStatus(
+ QueryResult result, uint32_t link_mask,
+ uint32_t output_protection_mask) = 0;
+
+ // Called by the host after a call to Host::RequestStorageId(). If the
+ // version of the storage ID requested is available, |storage_id| and
+ // |storage_id_size| are set appropriately. |version| will be the same as
+ // what was requested, unless 0 (latest) was requested, in which case
+ // |version| will be the actual version number for the |storage_id| returned.
+ // If the requested version is not available, null/zero will be provided as
+ // |storage_id| and |storage_id_size|, respectively, and |version| should be
+ // ignored.
+ virtual void OnStorageId(uint32_t version, const uint8_t* storage_id,
+ uint32_t storage_id_size) = 0;
+
+ // Destroys the object in the same context as it was created.
+ virtual void Destroy() = 0;
+
+ protected:
+ ContentDecryptionModule_11() {}
+ virtual ~ContentDecryptionModule_11() {}
+};
+
+class CDM_CLASS_API Host_10 {
+ public:
+ static const int kVersion = 10;
+
+ // Returns a Buffer* containing non-zero members upon success, or NULL on
+ // failure. The caller owns the Buffer* after this call. The buffer is not
+ // guaranteed to be zero initialized. The capacity of the allocated Buffer
+ // is guaranteed to be not less than |capacity|.
+ virtual Buffer* Allocate(uint32_t capacity) = 0;
+
+ // Requests the host to call ContentDecryptionModule::TimerFired() |delay_ms|
+ // from now with |context|.
+ virtual void SetTimer(int64_t delay_ms, void* context) = 0;
+
+ // Returns the current wall time.
+ virtual Time GetCurrentWallTime() = 0;
+
+ // Called by the CDM with the result after the CDM instance was initialized.
+ virtual void OnInitialized(bool success) = 0;
+
+ // Called by the CDM when a key status is available in response to
+ // GetStatusForPolicy().
+ virtual void OnResolveKeyStatusPromise(uint32_t promise_id,
+ KeyStatus key_status) = 0;
+
+ // Called by the CDM when a session is created or loaded and the value for the
+ // MediaKeySession's sessionId attribute is available (|session_id|).
+ // This must be called before OnSessionMessage() or
+ // OnSessionKeysChange() is called for the same session. |session_id_size|
+ // should not include null termination.
+ // When called in response to LoadSession(), the |session_id| must be the
+ // same as the |session_id| passed in LoadSession(), or NULL if the
+ // session could not be loaded.
+ virtual void OnResolveNewSessionPromise(uint32_t promise_id,
+ const char* session_id,
+ uint32_t session_id_size) = 0;
+
+ // Called by the CDM when a session is updated or released.
+ virtual void OnResolvePromise(uint32_t promise_id) = 0;
+
+ // Called by the CDM when an error occurs as a result of one of the
+ // ContentDecryptionModule calls that accept a |promise_id|.
+ // |exception| must be specified. |error_message| and |system_code|
+ // are optional. |error_message_size| should not include null termination.
+ virtual void OnRejectPromise(uint32_t promise_id, Exception exception,
+ uint32_t system_code, const char* error_message,
+ uint32_t error_message_size) = 0;
+
+ // Called by the CDM when it has a message for session |session_id|.
+ // Size parameters should not include null termination.
+ virtual void OnSessionMessage(const char* session_id,
+ uint32_t session_id_size,
+ MessageType message_type, const char* message,
+ uint32_t message_size) = 0;
+
+ // Called by the CDM when there has been a change in keys or their status for
+ // session |session_id|. |has_additional_usable_key| should be set if a
+ // key is newly usable (e.g. new key available, previously expired key has
+ // been renewed, etc.) and the browser should attempt to resume playback.
+ // |keys_info| is the list of key IDs for this session along with their
+ // current status. |keys_info_count| is the number of entries in |keys_info|.
+ // Size parameter for |session_id| should not include null termination.
+ virtual void OnSessionKeysChange(const char* session_id,
+ uint32_t session_id_size,
+ bool has_additional_usable_key,
+ const KeyInformation* keys_info,
+ uint32_t keys_info_count) = 0;
+
+ // Called by the CDM when there has been a change in the expiration time for
+ // session |session_id|. This can happen as the result of an Update() call
+ // or some other event. If this happens as a result of a call to Update(),
+ // it must be called before resolving the Update() promise. |new_expiry_time|
+ // represents the time after which the key(s) in the session will no longer
+ // be usable for decryption. It can be 0 if no such time exists or if the
+ // license explicitly never expires. Size parameter should not include null
+ // termination.
+ virtual void OnExpirationChange(const char* session_id,
+ uint32_t session_id_size,
+ Time new_expiry_time) = 0;
+
+ // Called by the CDM when session |session_id| is closed. Size
+ // parameter should not include null termination.
+ virtual void OnSessionClosed(const char* session_id,
+ uint32_t session_id_size) = 0;
+
+ // The following are optional methods that may not be implemented on all
+ // platforms.
+
+ // Sends a platform challenge for the given |service_id|. |challenge| is at
+ // most 256 bits of data to be signed. Once the challenge has been completed,
+ // the host will call ContentDecryptionModule::OnPlatformChallengeResponse()
+ // with the signed challenge response and platform certificate. Size
+ // parameters should not include null termination.
+ virtual void SendPlatformChallenge(const char* service_id,
+ uint32_t service_id_size,
+ const char* challenge,
+ uint32_t challenge_size) = 0;
+
+ // Attempts to enable output protection (e.g. HDCP) on the display link. The
+ // |desired_protection_mask| is a bit mask of OutputProtectionMethods. No
+ // status callback is issued, the CDM must call QueryOutputProtectionStatus()
+ // periodically to ensure the desired protections are applied.
+ virtual void EnableOutputProtection(uint32_t desired_protection_mask) = 0;
+
+ // Requests the current output protection status. Once the host has the status
+ // it will call ContentDecryptionModule::OnQueryOutputProtectionStatus().
+ virtual void QueryOutputProtectionStatus() = 0;
+
+ // Must be called by the CDM if it returned kDeferredInitialization during
+ // InitializeAudioDecoder() or InitializeVideoDecoder().
+ virtual void OnDeferredInitializationDone(StreamType stream_type,
+ Status decoder_status) = 0;
+
+ // Creates a FileIO object from the host to do file IO operation. Returns NULL
+ // if a FileIO object cannot be obtained. Once a valid FileIO object is
+ // returned, |client| must be valid until FileIO::Close() is called. The
+ // CDM can call this method multiple times to operate on different files.
+ virtual FileIO* CreateFileIO(FileIOClient* client) = 0;
+
+ // Requests a specific version of the storage ID. A storage ID is a stable,
+ // device specific ID used by the CDM to securely store persistent data. The
+ // ID will be returned by the host via ContentDecryptionModule::OnStorageId().
+ // If |version| is 0, the latest version will be returned. All |version|s
+ // that are greater than or equal to 0x80000000 are reserved for the CDM and
+ // should not be supported or returned by the host. The CDM must not expose
+ // the ID outside the client device, even in encrypted form.
+ virtual void RequestStorageId(uint32_t version) = 0;
+
+ protected:
+ Host_10() {}
+ virtual ~Host_10() {}
+};
+
+class CDM_CLASS_API Host_11 {
+ public:
+ static const int kVersion = 11;
+
+ // Returns a Buffer* containing non-zero members upon success, or NULL on
+ // failure. The caller owns the Buffer* after this call. The buffer is not
+ // guaranteed to be zero initialized. The capacity of the allocated Buffer
+ // is guaranteed to be not less than |capacity|.
+ virtual Buffer* Allocate(uint32_t capacity) = 0;
+
+ // Requests the host to call ContentDecryptionModule::TimerFired() |delay_ms|
+ // from now with |context|.
+ virtual void SetTimer(int64_t delay_ms, void* context) = 0;
+
+ // Returns the current wall time.
+ virtual Time GetCurrentWallTime() = 0;
+
+ // Called by the CDM with the result after the CDM instance was initialized.
+ virtual void OnInitialized(bool success) = 0;
+
+ // Called by the CDM when a key status is available in response to
+ // GetStatusForPolicy().
+ virtual void OnResolveKeyStatusPromise(uint32_t promise_id,
+ KeyStatus key_status) = 0;
+
+ // Called by the CDM when a session is created or loaded and the value for the
+ // MediaKeySession's sessionId attribute is available (|session_id|).
+ // This must be called before OnSessionMessage() or
+ // OnSessionKeysChange() is called for the same session. |session_id_size|
+ // should not include null termination.
+ // When called in response to LoadSession(), the |session_id| must be the
+ // same as the |session_id| passed in LoadSession(), or NULL if the
+ // session could not be loaded.
+ virtual void OnResolveNewSessionPromise(uint32_t promise_id,
+ const char* session_id,
+ uint32_t session_id_size) = 0;
+
+ // Called by the CDM when a session is updated or released.
+ virtual void OnResolvePromise(uint32_t promise_id) = 0;
+
+ // Called by the CDM when an error occurs as a result of one of the
+ // ContentDecryptionModule calls that accept a |promise_id|.
+ // |exception| must be specified. |error_message| and |system_code|
+ // are optional. |error_message_size| should not include null termination.
+ virtual void OnRejectPromise(uint32_t promise_id, Exception exception,
+ uint32_t system_code, const char* error_message,
+ uint32_t error_message_size) = 0;
+
+ // Called by the CDM when it has a message for session |session_id|.
+ // Size parameters should not include null termination.
+ virtual void OnSessionMessage(const char* session_id,
+ uint32_t session_id_size,
+ MessageType message_type, const char* message,
+ uint32_t message_size) = 0;
+
+ // Called by the CDM when there has been a change in keys or their status for
+ // session |session_id|. |has_additional_usable_key| should be set if a
+ // key is newly usable (e.g. new key available, previously expired key has
+ // been renewed, etc.) and the browser should attempt to resume playback.
+ // |keys_info| is the list of key IDs for this session along with their
+ // current status. |keys_info_count| is the number of entries in |keys_info|.
+ // Size parameter for |session_id| should not include null termination.
+ virtual void OnSessionKeysChange(const char* session_id,
+ uint32_t session_id_size,
+ bool has_additional_usable_key,
+ const KeyInformation* keys_info,
+ uint32_t keys_info_count) = 0;
+
+ // Called by the CDM when there has been a change in the expiration time for
+ // session |session_id|. This can happen as the result of an Update() call
+ // or some other event. If this happens as a result of a call to Update(),
+ // it must be called before resolving the Update() promise. |new_expiry_time|
+ // represents the time after which the key(s) in the session will no longer
+ // be usable for decryption. It can be 0 if no such time exists or if the
+ // license explicitly never expires. Size parameter should not include null
+ // termination.
+ virtual void OnExpirationChange(const char* session_id,
+ uint32_t session_id_size,
+ Time new_expiry_time) = 0;
+
+ // Called by the CDM when session |session_id| is closed. Size
+ // parameter should not include null termination.
+ virtual void OnSessionClosed(const char* session_id,
+ uint32_t session_id_size) = 0;
+
+ // The following are optional methods that may not be implemented on all
+ // platforms.
+
+ // Sends a platform challenge for the given |service_id|. |challenge| is at
+ // most 256 bits of data to be signed. Once the challenge has been completed,
+ // the host will call ContentDecryptionModule::OnPlatformChallengeResponse()
+ // with the signed challenge response and platform certificate. Size
+ // parameters should not include null termination.
+ virtual void SendPlatformChallenge(const char* service_id,
+ uint32_t service_id_size,
+ const char* challenge,
+ uint32_t challenge_size) = 0;
+
+ // Attempts to enable output protection (e.g. HDCP) on the display link. The
+ // |desired_protection_mask| is a bit mask of OutputProtectionMethods. No
+ // status callback is issued, the CDM must call QueryOutputProtectionStatus()
+ // periodically to ensure the desired protections are applied.
+ virtual void EnableOutputProtection(uint32_t desired_protection_mask) = 0;
+
+ // Requests the current output protection status. Once the host has the status
+ // it will call ContentDecryptionModule::OnQueryOutputProtectionStatus().
+ virtual void QueryOutputProtectionStatus() = 0;
+
+ // Must be called by the CDM if it returned kDeferredInitialization during
+ // InitializeAudioDecoder() or InitializeVideoDecoder().
+ virtual void OnDeferredInitializationDone(StreamType stream_type,
+ Status decoder_status) = 0;
+
+ // Creates a FileIO object from the host to do file IO operation. Returns NULL
+ // if a FileIO object cannot be obtained. Once a valid FileIO object is
+ // returned, |client| must be valid until FileIO::Close() is called. The
+ // CDM can call this method multiple times to operate on different files.
+ virtual FileIO* CreateFileIO(FileIOClient* client) = 0;
+
+ // Requests a CdmProxy that proxies part of CDM functionalities to a different
+ // entity, e.g. a hardware CDM module. A CDM instance can have at most one
+ // CdmProxy throughout its lifetime, which must be requested and initialized
+ // during CDM instance initialization time, i.e. in or after CDM::Initialize()
+ // and before OnInitialized() is called, to ensure proper connection of the
+ // CdmProxy and the media player (e.g. hardware decoder). The CdmProxy is
+ // owned by the host and is guaranteed to be valid throughout the CDM
+ // instance's lifetime. The CDM must ensure that the |client| remain valid
+ // before the CDM instance is destroyed. Returns null if CdmProxy is not
+ // supported, called before CDM::Initialize(), RequestCdmProxy() is called
+ // more than once, or called after the CDM instance has been initialized.
+ virtual CdmProxy* RequestCdmProxy(CdmProxyClient* client) = 0;
+
+ // Requests a specific version of the storage ID. A storage ID is a stable,
+ // device specific ID used by the CDM to securely store persistent data. The
+ // ID will be returned by the host via ContentDecryptionModule::OnStorageId().
+ // If |version| is 0, the latest version will be returned. All |version|s
+ // that are greater than or equal to 0x80000000 are reserved for the CDM and
+ // should not be supported or returned by the host. The CDM must not expose
+ // the ID outside the client device, even in encrypted form.
+ virtual void RequestStorageId(uint32_t version) = 0;
+
+ protected:
+ Host_11() {}
+ virtual ~Host_11() {}
+};
+
+} // namespace cdm
+
+#endif // CDM_CONTENT_DECRYPTION_MODULE_H_
diff --git a/dom/media/gmp/widevine-adapter/content_decryption_module_export.h b/dom/media/gmp/widevine-adapter/content_decryption_module_export.h
new file mode 100644
index 0000000000..932708deba
--- /dev/null
+++ b/dom/media/gmp/widevine-adapter/content_decryption_module_export.h
@@ -0,0 +1,38 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CDM_CONTENT_DECRYPTION_MODULE_EXPORT_H_
+#define CDM_CONTENT_DECRYPTION_MODULE_EXPORT_H_
+
+// Define CDM_API so that functionality implemented by the CDM module
+// can be exported to consumers.
+#if defined(_WIN32)
+
+#if defined(CDM_IMPLEMENTATION)
+#define CDM_API __declspec(dllexport)
+#else
+#define CDM_API __declspec(dllimport)
+#endif // defined(CDM_IMPLEMENTATION)
+
+#else // defined(_WIN32)
+#define CDM_API __attribute__((visibility("default")))
+#endif // defined(_WIN32)
+
+// Define CDM_CLASS_API to export class types. We have to add visibility
+// attributes to make sure virtual tables in CDM consumer and CDM implementation
+// are the same. Generally, it was always a good idea, as there're no guarantees
+// about that for the internal symbols, but it has only become a practical issue
+// after introduction of LTO devirtualization. See more details on
+// https://crbug.com/609564#c35
+#if defined(_WIN32)
+#if defined(__clang__)
+#define CDM_CLASS_API [[clang::lto_visibility_public]]
+#else
+#define CDM_CLASS_API
+#endif
+#else // defined(_WIN32)
+#define CDM_CLASS_API __attribute__((visibility("default")))
+#endif // defined(_WIN32)
+
+#endif // CDM_CONTENT_DECRYPTION_MODULE_EXPORT_H_
diff --git a/dom/media/gmp/widevine-adapter/content_decryption_module_ext.h b/dom/media/gmp/widevine-adapter/content_decryption_module_ext.h
new file mode 100644
index 0000000000..5df8344e60
--- /dev/null
+++ b/dom/media/gmp/widevine-adapter/content_decryption_module_ext.h
@@ -0,0 +1,64 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CDM_CONTENT_DECRYPTION_MODULE_EXT_H_
+#define CDM_CONTENT_DECRYPTION_MODULE_EXT_H_
+
+#if defined(_WIN32)
+#include <windows.h>
+#endif
+
+#include "content_decryption_module_export.h"
+
+#if defined(_MSC_VER)
+typedef unsigned int uint32_t;
+#else
+#include <stdint.h>
+#endif
+
+namespace cdm {
+
+#if defined(_WIN32)
+typedef wchar_t FilePathCharType;
+typedef HANDLE PlatformFile;
+const PlatformFile kInvalidPlatformFile = INVALID_HANDLE_VALUE;
+#else
+typedef char FilePathCharType;
+typedef int PlatformFile;
+const PlatformFile kInvalidPlatformFile = -1;
+#endif // defined(_WIN32)
+
+struct HostFile {
+ HostFile(const FilePathCharType* file_path,
+ PlatformFile file,
+ PlatformFile sig_file)
+ : file_path(file_path), file(file), sig_file(sig_file) {}
+
+ // File that is part of the host of the CDM.
+ const FilePathCharType* file_path = nullptr;
+ PlatformFile file = kInvalidPlatformFile;
+
+ // Signature file for |file|.
+ PlatformFile sig_file = kInvalidPlatformFile;
+};
+
+} // namespace cdm
+
+extern "C" {
+
+// Functions in this file are dynamically retrieved by their versioned function
+// names. Increment the version number for any backward incompatible API
+// changes.
+
+// Verifies CDM host. All files in |host_files| are opened in read-only mode.
+//
+// Returns false and closes all files if there is an immediate failure.
+// Otherwise returns true as soon as possible and processes the files
+// asynchronously. All files MUST be closed by the CDM after this one-time
+// processing is finished.
+CDM_API bool VerifyCdmHost_0(const cdm::HostFile* host_files,
+ uint32_t num_files);
+}
+
+#endif // CDM_CONTENT_DECRYPTION_MODULE_EXT_H_
diff --git a/dom/media/gmp/widevine-adapter/content_decryption_module_proxy.h b/dom/media/gmp/widevine-adapter/content_decryption_module_proxy.h
new file mode 100644
index 0000000000..d3edff8b37
--- /dev/null
+++ b/dom/media/gmp/widevine-adapter/content_decryption_module_proxy.h
@@ -0,0 +1,121 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef CDM_CONTENT_DECRYPTION_MODULE_PROXY_H_
+#define CDM_CONTENT_DECRYPTION_MODULE_PROXY_H_
+
+#include "content_decryption_module_export.h"
+
+#if defined(_MSC_VER)
+typedef unsigned char uint8_t;
+typedef unsigned int uint32_t;
+typedef unsigned __int64 uint64_t;
+#else
+# include <stdint.h>
+#endif
+
+namespace cdm {
+
+class CDM_CLASS_API CdmProxyClient;
+
+// A proxy class for the CDM.
+// In general, the interpretation of the CdmProxy and CdmProxyClient method
+// parameters are protocol dependent. For enum parameters, values outside the
+// enum range may not work.
+class CDM_CLASS_API CdmProxy {
+ public:
+ enum Function : uint32_t {
+ // For Intel Negotiate Crypto SessionKey Exchange (CSME) path to call
+ // ID3D11VideoContext::NegotiateCryptoSessionKeyExchange.
+ kIntelNegotiateCryptoSessionKeyExchange = 1,
+ // There will be more values in the future e.g. for D3D11 RSA method.
+ };
+
+ enum KeyType : uint32_t {
+ kDecryptOnly = 0,
+ kDecryptAndDecode = 1,
+ };
+
+ // Initializes the proxy. The results will be returned in
+ // CdmProxyClient::OnInitialized().
+ virtual void Initialize() = 0;
+
+ // Processes and updates the state of the proxy.
+ // |output_data_size| is required by some protocol to set up the output data.
+ // The operation may fail if the |output_data_size| is wrong. The results will
+ // be returned in CdmProxyClient::OnProcessed().
+ virtual void Process(Function function, uint32_t crypto_session_id,
+ const uint8_t* input_data, uint32_t input_data_size,
+ uint32_t output_data_size) = 0;
+
+ // Creates a crypto session for handling media.
+ // If extra data has to be passed to further setup the media crypto session,
+ // pass the data as |input_data|. The results will be returned in
+ // CdmProxyClient::OnMediaCryptoSessionCreated().
+ virtual void CreateMediaCryptoSession(const uint8_t* input_data,
+ uint32_t input_data_size) = 0;
+
+ // Sets a key for the session identified by |crypto_session_id|.
+ virtual void SetKey(uint32_t crypto_session_id, const uint8_t* key_id,
+ uint32_t key_id_size, KeyType key_type,
+ const uint8_t* key_blob, uint32_t key_blob_size) = 0;
+
+ // Removes a key for the session identified by |crypto_session_id|.
+ virtual void RemoveKey(uint32_t crypto_session_id, const uint8_t* key_id,
+ uint32_t key_id_size) = 0;
+
+ protected:
+ CdmProxy() {}
+ virtual ~CdmProxy() {}
+};
+
+// Responses to CdmProxy calls. All responses will be called asynchronously.
+class CDM_CLASS_API CdmProxyClient {
+ public:
+ enum Status : uint32_t {
+ kOk,
+ kFail,
+ };
+
+ enum Protocol : uint32_t {
+ kNone = 0, // No protocol supported. Can be used in failure cases.
+ kIntel, // Method using Intel CSME.
+ // There will be more values in the future e.g. kD3D11RsaHardware,
+ // kD3D11RsaSoftware to use the D3D11 RSA method.
+ };
+
+ // Callback for Initialize(). If the proxy created a crypto session, then the
+ // ID for the crypto session is |crypto_session_id|.
+ virtual void OnInitialized(Status status, Protocol protocol,
+ uint32_t crypto_session_id) = 0;
+
+ // Callback for Process(). |output_data| is the output of processing.
+ virtual void OnProcessed(Status status, const uint8_t* output_data,
+ uint32_t output_data_size) = 0;
+
+ // Callback for CreateMediaCryptoSession(). On success:
+ // - |crypto_session_id| is the ID for the created crypto session.
+ // - |output_data| is extra value, if any.
+ // Otherwise, |crypto_session_id| and |output_data| should be ignored.
+ virtual void OnMediaCryptoSessionCreated(Status status,
+ uint32_t crypto_session_id,
+ uint64_t output_data) = 0;
+
+ // Callback for SetKey().
+ virtual void OnKeySet(Status status) = 0;
+
+ // Callback for RemoveKey().
+ virtual void OnKeyRemoved(Status status) = 0;
+
+ // Called when there is a hardware reset and all the hardware context is lost.
+ virtual void NotifyHardwareReset() = 0;
+
+ protected:
+ CdmProxyClient() {}
+ virtual ~CdmProxyClient() {}
+};
+
+} // namespace cdm
+
+#endif // CDM_CONTENT_DECRYPTION_MODULE_PROXY_H_
diff --git a/dom/media/gmp/widevine-adapter/moz.build b/dom/media/gmp/widevine-adapter/moz.build
new file mode 100644
index 0000000000..b1b7582407
--- /dev/null
+++ b/dom/media/gmp/widevine-adapter/moz.build
@@ -0,0 +1,21 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+SOURCES += [
+ "WidevineFileIO.cpp",
+ "WidevineUtils.cpp",
+ "WidevineVideoFrame.cpp",
+]
+
+EXPORTS += ["WidevineFileIO.h", "WidevineUtils.h", "WidevineVideoFrame.h"]
+
+FINAL_LIBRARY = "xul"
+
+LOCAL_INCLUDES += [
+ "/dom/media/gmp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
diff --git a/dom/media/gtest/AudioGenerator.h b/dom/media/gtest/AudioGenerator.h
new file mode 100644
index 0000000000..da7a31b9dc
--- /dev/null
+++ b/dom/media/gtest/AudioGenerator.h
@@ -0,0 +1,64 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_GTEST_AUDIO_GENERATOR_H_
+#define DOM_MEDIA_GTEST_AUDIO_GENERATOR_H_
+
+#include "AudioSegment.h"
+#include "prtime.h"
+#include "SineWaveGenerator.h"
+
+namespace mozilla {
+
+template <typename Sample>
+class AudioGenerator {
+ public:
+ AudioGenerator(uint32_t aChannels, uint32_t aSampleRate,
+ uint32_t aFrequency = 1000)
+ : mSampleRate(aSampleRate),
+ mFrequency(aFrequency),
+ mChannelCount(aChannels),
+ mGenerator(aSampleRate, aFrequency) {}
+
+ void Generate(mozilla::AudioSegment& aSegment, uint32_t aFrameCount) {
+ CheckedInt<size_t> bufferSize(sizeof(Sample));
+ bufferSize *= aFrameCount;
+ RefPtr<SharedBuffer> buffer = SharedBuffer::Create(bufferSize);
+ Sample* dest = static_cast<Sample*>(buffer->Data());
+ mGenerator.generate(dest, aFrameCount);
+ AutoTArray<const Sample*, 1> channels;
+ for (uint32_t i = 0; i < mChannelCount; ++i) {
+ channels.AppendElement(dest);
+ }
+ aSegment.AppendFrames(buffer.forget(), channels, aFrameCount,
+ PRINCIPAL_HANDLE_NONE);
+ }
+
+ void GenerateInterleaved(Sample* aSamples, uint32_t aFrameCount) {
+ mGenerator.generate(aSamples, aFrameCount, mChannelCount);
+ }
+
+ void SetChannelsCount(uint32_t aChannelCount) {
+ mChannelCount = aChannelCount;
+ }
+
+ uint32_t ChannelCount() const { return mChannelCount; }
+
+ static float Amplitude() {
+ return mozilla::SineWaveGenerator<Sample>::Amplitude();
+ }
+
+ const uint32_t mSampleRate;
+ const uint32_t mFrequency;
+
+ private:
+ uint32_t mChannelCount;
+ mozilla::SineWaveGenerator<Sample> mGenerator;
+};
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_GTEST_AUDIO_GENERATOR_H_
diff --git a/dom/media/gtest/AudioVerifier.h b/dom/media/gtest/AudioVerifier.h
new file mode 100644
index 0000000000..e50c812f63
--- /dev/null
+++ b/dom/media/gtest/AudioVerifier.h
@@ -0,0 +1,135 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_GTEST_AUDIOVERIFIER_H_
+#define DOM_MEDIA_GTEST_AUDIOVERIFIER_H_
+
+#include "AudioGenerator.h"
+
+namespace mozilla {
+
+template <typename Sample>
+class AudioVerifier {
+ public:
+ explicit AudioVerifier(uint32_t aRate, uint32_t aFrequency)
+ : mRate(aRate), mFrequency(aFrequency) {}
+
+ // Only the mono channel is taken into account.
+ void AppendData(const AudioSegment& segment) {
+ for (AudioSegment::ConstChunkIterator iter(segment); !iter.IsEnded();
+ iter.Next()) {
+ const AudioChunk& c = *iter;
+ if (c.IsNull()) {
+ for (int i = 0; i < c.GetDuration(); ++i) {
+ CheckSample(0);
+ }
+ } else {
+ const Sample* buffer = c.ChannelData<Sample>()[0];
+ for (int i = 0; i < c.GetDuration(); ++i) {
+ CheckSample(buffer[i]);
+ }
+ }
+ }
+ }
+
+ void AppendDataInterleaved(const Sample* aBuffer, uint32_t aFrames,
+ uint32_t aChannels) {
+ for (uint32_t i = 0; i < aFrames * aChannels; i += aChannels) {
+ CheckSample(aBuffer[i]);
+ }
+ }
+
+ float EstimatedFreq() const {
+ if (mTotalFramesSoFar == PreSilenceSamples()) {
+ return 0;
+ }
+ if (mSumPeriodInSamples == 0) {
+ return 0;
+ }
+ if (mZeroCrossCount <= 1) {
+ return 0;
+ }
+ return mRate /
+ (static_cast<float>(mSumPeriodInSamples) / (mZeroCrossCount - 1));
+ }
+
+ // Returns the maximum difference in value between two adjacent samples along
+ // the sine curve.
+ Sample MaxMagnitudeDifference() {
+ return static_cast<Sample>(AudioGenerator<Sample>::Amplitude() *
+ sin(2 * M_PI * mFrequency / mRate));
+ }
+
+ bool PreSilenceEnded() const {
+ return mTotalFramesSoFar > mPreSilenceSamples;
+ }
+ uint64_t PreSilenceSamples() const { return mPreSilenceSamples; }
+ uint32_t CountDiscontinuities() const { return mDiscontinuitiesCount; }
+
+ private:
+ void CheckSample(Sample aCurrentSample) {
+ ++mTotalFramesSoFar;
+ // Avoid pre-silence
+ if (!CountPreSilence(aCurrentSample)) {
+ CountZeroCrossing(aCurrentSample);
+ CountDiscontinuities(aCurrentSample);
+ }
+
+ mPrevious = aCurrentSample;
+ }
+
+ bool CountPreSilence(Sample aCurrentSample) {
+ if (IsZero(aCurrentSample) && mPreSilenceSamples == mTotalFramesSoFar - 1) {
+ ++mPreSilenceSamples;
+ return true;
+ }
+ if (IsZero(mPrevious) && aCurrentSample > 0 &&
+ aCurrentSample < 2 * MaxMagnitudeDifference() &&
+ mPreSilenceSamples == mTotalFramesSoFar - 1) {
+ // Previous zero considered the first sample of the waveform.
+ --mPreSilenceSamples;
+ }
+ return false;
+ }
+
+ // Positive to negative direction
+ void CountZeroCrossing(Sample aCurrentSample) {
+ if (mPrevious > 0 && aCurrentSample <= 0) {
+ if (mZeroCrossCount++) {
+ MOZ_ASSERT(mZeroCrossCount > 1);
+ mSumPeriodInSamples += mTotalFramesSoFar - mLastZeroCrossPosition;
+ }
+ mLastZeroCrossPosition = mTotalFramesSoFar;
+ }
+ }
+
+ void CountDiscontinuities(Sample aCurrentSample) {
+ mDiscontinuitiesCount += fabs(fabs(aCurrentSample) - fabs(mPrevious)) >
+ 3 * MaxMagnitudeDifference();
+ }
+
+ bool IsZero(float aValue) { return fabs(aValue) < 1e-8; }
+ bool IsZero(short aValue) { return aValue == 0; }
+
+ private:
+ const uint32_t mRate;
+ const uint32_t mFrequency;
+
+ uint32_t mZeroCrossCount = 0;
+ uint64_t mLastZeroCrossPosition = 0;
+ uint64_t mSumPeriodInSamples = 0;
+
+ uint64_t mTotalFramesSoFar = 0;
+ uint64_t mPreSilenceSamples = 0;
+
+ uint32_t mDiscontinuitiesCount = 0;
+ // This is needed to connect the previous buffers.
+ Sample mPrevious = {};
+};
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_GTEST_AUDIOVERIFIER_H_
diff --git a/dom/media/gtest/Cargo.toml b/dom/media/gtest/Cargo.toml
new file mode 100644
index 0000000000..a9318c24f6
--- /dev/null
+++ b/dom/media/gtest/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "mp4parse-gtest"
+version = "0.1.0"
+authors = ["The Mozilla Project Developers"]
+license = "MPL-2.0"
+
+[lib]
+path = "hello.rs"
diff --git a/dom/media/gtest/GMPTestMonitor.h b/dom/media/gtest/GMPTestMonitor.h
new file mode 100644
index 0000000000..226d5c77a9
--- /dev/null
+++ b/dom/media/gtest/GMPTestMonitor.h
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef __GMPTestMonitor_h__
+#define __GMPTestMonitor_h__
+
+#include "nsThreadUtils.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/SpinEventLoopUntil.h"
+
+class GMPTestMonitor {
+ public:
+ GMPTestMonitor() : mFinished(false) {}
+
+ void AwaitFinished() {
+ MOZ_ASSERT(NS_IsMainThread());
+ mozilla::SpinEventLoopUntil("GMPTestMonitor::AwaitFinished"_ns,
+ [&]() { return mFinished; });
+ mFinished = false;
+ }
+
+ private:
+ void MarkFinished() {
+ MOZ_ASSERT(NS_IsMainThread());
+ mFinished = true;
+ }
+
+ public:
+ void SetFinished() {
+ mozilla::SchedulerGroup::Dispatch(mozilla::TaskCategory::Other,
+ mozilla::NewNonOwningRunnableMethod(
+ "GMPTestMonitor::MarkFinished", this,
+ &GMPTestMonitor::MarkFinished));
+ }
+
+ private:
+ bool mFinished;
+};
+
+#endif // __GMPTestMonitor_h__
diff --git a/dom/media/gtest/MockCubeb.cpp b/dom/media/gtest/MockCubeb.cpp
new file mode 100644
index 0000000000..10422c7757
--- /dev/null
+++ b/dom/media/gtest/MockCubeb.cpp
@@ -0,0 +1,673 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MockCubeb.h"
+
+#include "gtest/gtest.h"
+
+namespace mozilla {
+
+void PrintDevice(cubeb_device_info aInfo) {
+ printf(
+ "id: %zu\n"
+ "device_id: %s\n"
+ "friendly_name: %s\n"
+ "group_id: %s\n"
+ "vendor_name: %s\n"
+ "type: %d\n"
+ "state: %d\n"
+ "preferred: %d\n"
+ "format: %d\n"
+ "default_format: %d\n"
+ "max_channels: %d\n"
+ "default_rate: %d\n"
+ "max_rate: %d\n"
+ "min_rate: %d\n"
+ "latency_lo: %d\n"
+ "latency_hi: %d\n",
+ reinterpret_cast<uintptr_t>(aInfo.devid), aInfo.device_id,
+ aInfo.friendly_name, aInfo.group_id, aInfo.vendor_name, aInfo.type,
+ aInfo.state, aInfo.preferred, aInfo.format, aInfo.default_format,
+ aInfo.max_channels, aInfo.default_rate, aInfo.max_rate, aInfo.min_rate,
+ aInfo.latency_lo, aInfo.latency_hi);
+}
+
+void PrintDevice(AudioDeviceInfo* aInfo) {
+ cubeb_devid id;
+ nsString name;
+ nsString groupid;
+ nsString vendor;
+ uint16_t type;
+ uint16_t state;
+ uint16_t preferred;
+ uint16_t supportedFormat;
+ uint16_t defaultFormat;
+ uint32_t maxChannels;
+ uint32_t defaultRate;
+ uint32_t maxRate;
+ uint32_t minRate;
+ uint32_t maxLatency;
+ uint32_t minLatency;
+
+ id = aInfo->DeviceID();
+ aInfo->GetName(name);
+ aInfo->GetGroupId(groupid);
+ aInfo->GetVendor(vendor);
+ aInfo->GetType(&type);
+ aInfo->GetState(&state);
+ aInfo->GetPreferred(&preferred);
+ aInfo->GetSupportedFormat(&supportedFormat);
+ aInfo->GetDefaultFormat(&defaultFormat);
+ aInfo->GetMaxChannels(&maxChannels);
+ aInfo->GetDefaultRate(&defaultRate);
+ aInfo->GetMaxRate(&maxRate);
+ aInfo->GetMinRate(&minRate);
+ aInfo->GetMinLatency(&minLatency);
+ aInfo->GetMaxLatency(&maxLatency);
+
+ printf(
+ "device id: %zu\n"
+ "friendly_name: %s\n"
+ "group_id: %s\n"
+ "vendor_name: %s\n"
+ "type: %d\n"
+ "state: %d\n"
+ "preferred: %d\n"
+ "format: %d\n"
+ "default_format: %d\n"
+ "max_channels: %d\n"
+ "default_rate: %d\n"
+ "max_rate: %d\n"
+ "min_rate: %d\n"
+ "latency_lo: %d\n"
+ "latency_hi: %d\n",
+ reinterpret_cast<uintptr_t>(id), NS_LossyConvertUTF16toASCII(name).get(),
+ NS_LossyConvertUTF16toASCII(groupid).get(),
+ NS_LossyConvertUTF16toASCII(vendor).get(), type, state, preferred,
+ supportedFormat, defaultFormat, maxChannels, defaultRate, maxRate,
+ minRate, minLatency, maxLatency);
+}
+
+cubeb_device_info DeviceTemplate(cubeb_devid aId, cubeb_device_type aType,
+ const char* name) {
+ // A fake input device
+ cubeb_device_info device;
+ device.devid = aId;
+ device.device_id = "nice name";
+ device.friendly_name = name;
+ device.group_id = "the physical device";
+ device.vendor_name = "mozilla";
+ device.type = aType;
+ device.state = CUBEB_DEVICE_STATE_ENABLED;
+ device.preferred = CUBEB_DEVICE_PREF_NONE;
+ device.format = CUBEB_DEVICE_FMT_F32NE;
+ device.default_format = CUBEB_DEVICE_FMT_F32NE;
+ device.max_channels = 2;
+ device.default_rate = 44100;
+ device.max_rate = 44100;
+ device.min_rate = 16000;
+ device.latency_lo = 256;
+ device.latency_hi = 1024;
+
+ return device;
+}
+
+cubeb_device_info DeviceTemplate(cubeb_devid aId, cubeb_device_type aType) {
+ return DeviceTemplate(aId, aType, "nice name");
+}
+
+void AddDevices(MockCubeb* mock, uint32_t device_count,
+ cubeb_device_type deviceType) {
+ mock->ClearDevices(deviceType);
+ // Add a few input devices (almost all the same but it does not really
+ // matter as long as they have distinct IDs and only one is the default
+ // devices)
+ for (uintptr_t i = 0; i < device_count; i++) {
+ cubeb_device_info device =
+ DeviceTemplate(reinterpret_cast<void*>(i + 1), deviceType);
+ // Make it so that the last device is the default input device.
+ if (i == device_count - 1) {
+ device.preferred = CUBEB_DEVICE_PREF_ALL;
+ }
+ mock->AddDevice(device);
+ }
+}
+
+void cubeb_mock_destroy(cubeb* context) { delete MockCubeb::AsMock(context); }
+
+MockCubebStream::MockCubebStream(cubeb* aContext, cubeb_devid aInputDevice,
+ cubeb_stream_params* aInputStreamParams,
+ cubeb_devid aOutputDevice,
+ cubeb_stream_params* aOutputStreamParams,
+ cubeb_data_callback aDataCallback,
+ cubeb_state_callback aStateCallback,
+ void* aUserPtr, SmartMockCubebStream* aSelf,
+ bool aFrozenStart)
+ : context(aContext),
+ mUserPtr(aUserPtr),
+ mHasInput(aInputStreamParams),
+ mHasOutput(aOutputStreamParams),
+ mSelf(aSelf),
+ mFrozenStartMonitor("MockCubebStream::mFrozenStartMonitor"),
+ mFrozenStart(aFrozenStart),
+ mDataCallback(aDataCallback),
+ mStateCallback(aStateCallback),
+ mInputDeviceID(aInputDevice),
+ mOutputDeviceID(aOutputDevice),
+ mAudioGenerator(aInputStreamParams ? aInputStreamParams->channels
+ : MAX_INPUT_CHANNELS,
+ aInputStreamParams ? aInputStreamParams->rate
+ : aOutputStreamParams->rate,
+ 100 /* aFrequency */),
+ mAudioVerifier(aInputStreamParams ? aInputStreamParams->rate
+ : aOutputStreamParams->rate,
+ 100 /* aFrequency */) {
+ MOZ_ASSERT(mAudioGenerator.ChannelCount() <= MAX_INPUT_CHANNELS,
+ "mInputBuffer has no enough space to hold generated data");
+ if (aInputStreamParams) {
+ mInputParams = *aInputStreamParams;
+ }
+ if (aOutputStreamParams) {
+ mOutputParams = *aOutputStreamParams;
+ }
+}
+
+MockCubebStream::~MockCubebStream() = default;
+
+int MockCubebStream::Start() {
+ NotifyStateChanged(CUBEB_STATE_STARTED);
+ mStreamStop = false;
+ if (mFrozenStart) {
+ // We need to grab mFrozenStartMonitor before returning to avoid races in
+ // the calling code -- it controls when to mFrozenStartMonitor.Notify().
+ // TempData helps facilitate this by holding what's needed to block the
+ // calling thread until the background thread has grabbed the lock.
+ struct TempData {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(TempData)
+ static_assert(HasThreadSafeRefCnt::value,
+ "Silence a -Wunused-local-typedef warning");
+ Monitor mMonitor{"MockCubebStream::Start::TempData::mMonitor"};
+ bool mFinished = false;
+
+ private:
+ ~TempData() = default;
+ };
+ auto temp = MakeRefPtr<TempData>();
+ MonitorAutoLock lock(temp->mMonitor);
+ NS_DispatchBackgroundTask(NS_NewRunnableFunction(
+ "MockCubebStream::WaitForThawBeforeStart",
+ [temp, this, self = RefPtr<SmartMockCubebStream>(mSelf)]() mutable {
+ MonitorAutoLock lock(mFrozenStartMonitor);
+ {
+ // Unblock MockCubebStream::Start now that we have locked the frozen
+ // start monitor.
+ MonitorAutoLock tempLock(temp->mMonitor);
+ temp->mFinished = true;
+ temp->mMonitor.Notify();
+ temp = nullptr;
+ }
+ while (mFrozenStart) {
+ mFrozenStartMonitor.Wait();
+ }
+ if (!mStreamStop) {
+ MockCubeb::AsMock(context)->StartStream(mSelf);
+ }
+ }));
+ while (!temp->mFinished) {
+ temp->mMonitor.Wait();
+ }
+ return CUBEB_OK;
+ }
+ MockCubeb::AsMock(context)->StartStream(this);
+ return CUBEB_OK;
+}
+
+int MockCubebStream::Stop() {
+ mOutputVerificationEvent.Notify(std::make_tuple(
+ mAudioVerifier.PreSilenceSamples(), mAudioVerifier.EstimatedFreq(),
+ mAudioVerifier.CountDiscontinuities()));
+ int rv = MockCubeb::AsMock(context)->StopStream(this);
+ mStreamStop = true;
+ if (rv == CUBEB_OK) {
+ NotifyStateChanged(CUBEB_STATE_STOPPED);
+ }
+ return rv;
+}
+
+void MockCubebStream::Destroy() {
+ // Dispatch an extra STOPPED state change as produced with audioipc.
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1801190#c1
+ NotifyStateChanged(CUBEB_STATE_STOPPED);
+
+ MockCubeb::AsMock(context)->StreamDestroy(AsCubebStream());
+}
+
+int MockCubebStream::RegisterDeviceChangedCallback(
+ cubeb_device_changed_callback aDeviceChangedCallback) {
+ if (mDeviceChangedCallback && aDeviceChangedCallback) {
+ return CUBEB_ERROR_INVALID_PARAMETER;
+ }
+ mDeviceChangedCallback = aDeviceChangedCallback;
+ return CUBEB_OK;
+}
+
+cubeb_stream* MockCubebStream::AsCubebStream() {
+ return reinterpret_cast<cubeb_stream*>(this);
+}
+
+MockCubebStream* MockCubebStream::AsMock(cubeb_stream* aStream) {
+ return reinterpret_cast<MockCubebStream*>(aStream);
+}
+
+cubeb_devid MockCubebStream::GetInputDeviceID() const { return mInputDeviceID; }
+
+cubeb_devid MockCubebStream::GetOutputDeviceID() const {
+ return mOutputDeviceID;
+}
+
+uint32_t MockCubebStream::InputChannels() const {
+ return mAudioGenerator.ChannelCount();
+}
+
+uint32_t MockCubebStream::OutputChannels() const {
+ return mOutputParams.channels;
+}
+
+uint32_t MockCubebStream::InputSampleRate() const {
+ return mAudioGenerator.mSampleRate;
+}
+
+uint32_t MockCubebStream::InputFrequency() const {
+ return mAudioGenerator.mFrequency;
+}
+
+nsTArray<AudioDataValue>&& MockCubebStream::TakeRecordedOutput() {
+ return std::move(mRecordedOutput);
+}
+
+nsTArray<AudioDataValue>&& MockCubebStream::TakeRecordedInput() {
+ return std::move(mRecordedInput);
+}
+
+void MockCubebStream::SetDriftFactor(float aDriftFactor) {
+ mDriftFactor = aDriftFactor;
+}
+
+void MockCubebStream::ForceError() { mForceErrorState = true; }
+
+void MockCubebStream::ForceDeviceChanged() { mForceDeviceChanged = true; };
+
+void MockCubebStream::Thaw() {
+ MonitorAutoLock l(mFrozenStartMonitor);
+ mFrozenStart = false;
+ mFrozenStartMonitor.Notify();
+}
+
+void MockCubebStream::SetOutputRecordingEnabled(bool aEnabled) {
+ mOutputRecordingEnabled = aEnabled;
+}
+
+void MockCubebStream::SetInputRecordingEnabled(bool aEnabled) {
+ mInputRecordingEnabled = aEnabled;
+}
+
+MediaEventSource<cubeb_state>& MockCubebStream::StateEvent() {
+ return mStateEvent;
+}
+
+MediaEventSource<uint32_t>& MockCubebStream::FramesProcessedEvent() {
+ return mFramesProcessedEvent;
+}
+
+MediaEventSource<uint32_t>& MockCubebStream::FramesVerifiedEvent() {
+ return mFramesVerifiedEvent;
+}
+
+MediaEventSource<std::tuple<uint64_t, float, uint32_t>>&
+MockCubebStream::OutputVerificationEvent() {
+ return mOutputVerificationEvent;
+}
+
+MediaEventSource<void>& MockCubebStream::ErrorForcedEvent() {
+ return mErrorForcedEvent;
+}
+
+MediaEventSource<void>& MockCubebStream::ErrorStoppedEvent() {
+ return mErrorStoppedEvent;
+}
+
+MediaEventSource<void>& MockCubebStream::DeviceChangeForcedEvent() {
+ return mDeviceChangedForcedEvent;
+}
+
+void MockCubebStream::Process10Ms() {
+ if (mStreamStop) {
+ return;
+ }
+
+ uint32_t rate = mHasOutput ? mOutputParams.rate : mInputParams.rate;
+ const long nrFrames =
+ static_cast<long>(static_cast<float>(rate * 10) * mDriftFactor) /
+ PR_MSEC_PER_SEC;
+ if (mInputParams.rate) {
+ mAudioGenerator.GenerateInterleaved(mInputBuffer, nrFrames);
+ }
+ cubeb_stream* stream = AsCubebStream();
+ const long outframes =
+ mDataCallback(stream, mUserPtr, mHasInput ? mInputBuffer : nullptr,
+ mHasOutput ? mOutputBuffer : nullptr, nrFrames);
+
+ if (mInputRecordingEnabled && mHasInput) {
+ mRecordedInput.AppendElements(mInputBuffer, outframes * InputChannels());
+ }
+ if (mOutputRecordingEnabled && mHasOutput) {
+ mRecordedOutput.AppendElements(mOutputBuffer, outframes * OutputChannels());
+ }
+ mAudioVerifier.AppendDataInterleaved(mOutputBuffer, outframes,
+ MAX_OUTPUT_CHANNELS);
+
+ mFramesProcessedEvent.Notify(outframes);
+ if (mAudioVerifier.PreSilenceEnded()) {
+ mFramesVerifiedEvent.Notify(outframes);
+ }
+
+ if (outframes < nrFrames) {
+ NotifyStateChanged(CUBEB_STATE_DRAINED);
+ mStreamStop = true;
+ return;
+ }
+ if (mForceErrorState) {
+ mForceErrorState = false;
+ // Let the audio thread (this thread!) run to completion before
+ // being released, by joining and releasing on main.
+ NS_DispatchBackgroundTask(NS_NewRunnableFunction(
+ __func__, [cubeb = MockCubeb::AsMock(context), this,
+ self = RefPtr<SmartMockCubebStream>(mSelf)] {
+ cubeb->StopStream(this);
+ self->mErrorStoppedEvent.Notify();
+ }));
+ NotifyStateChanged(CUBEB_STATE_ERROR);
+ mErrorForcedEvent.Notify();
+ mStreamStop = true;
+ return;
+ }
+ if (mForceDeviceChanged) {
+ mForceDeviceChanged = false;
+ // The device-changed callback is not necessary to be run in the
+ // audio-callback thread. It's up to the platform APIs. We don't have any
+ // control over them. Fire the device-changed callback in another thread to
+ // simulate this.
+ NS_DispatchBackgroundTask(NS_NewRunnableFunction(
+ __func__, [this, self = RefPtr<SmartMockCubebStream>(mSelf)] {
+ mDeviceChangedCallback(this->mUserPtr);
+ mDeviceChangedForcedEvent.Notify();
+ }));
+ }
+}
+
+void MockCubebStream::NotifyStateChanged(cubeb_state aState) {
+ mStateCallback(AsCubebStream(), mUserPtr, aState);
+ mStateEvent.Notify(aState);
+}
+
+MockCubeb::MockCubeb() : ops(&mock_ops) {}
+
+MockCubeb::~MockCubeb() { MOZ_ASSERT(!mFakeAudioThread); };
+
+cubeb* MockCubeb::AsCubebContext() { return reinterpret_cast<cubeb*>(this); }
+
+MockCubeb* MockCubeb::AsMock(cubeb* aContext) {
+ return reinterpret_cast<MockCubeb*>(aContext);
+}
+
+int MockCubeb::EnumerateDevices(cubeb_device_type aType,
+ cubeb_device_collection* aCollection) {
+#ifdef ANDROID
+ EXPECT_TRUE(false) << "This is not to be called on Android.";
+#endif
+ size_t count = 0;
+ if (aType & CUBEB_DEVICE_TYPE_INPUT) {
+ count += mInputDevices.Length();
+ }
+ if (aType & CUBEB_DEVICE_TYPE_OUTPUT) {
+ count += mOutputDevices.Length();
+ }
+ aCollection->device = new cubeb_device_info[count];
+ aCollection->count = count;
+
+ uint32_t collection_index = 0;
+ if (aType & CUBEB_DEVICE_TYPE_INPUT) {
+ for (auto& device : mInputDevices) {
+ aCollection->device[collection_index] = device;
+ collection_index++;
+ }
+ }
+ if (aType & CUBEB_DEVICE_TYPE_OUTPUT) {
+ for (auto& device : mOutputDevices) {
+ aCollection->device[collection_index] = device;
+ collection_index++;
+ }
+ }
+
+ return CUBEB_OK;
+}
+
+int MockCubeb::DestroyDeviceCollection(cubeb_device_collection* aCollection) {
+ delete[] aCollection->device;
+ aCollection->count = 0;
+ return CUBEB_OK;
+}
+
+int MockCubeb::RegisterDeviceCollectionChangeCallback(
+ cubeb_device_type aDevType,
+ cubeb_device_collection_changed_callback aCallback, void* aUserPtr) {
+ if (!mSupportsDeviceCollectionChangedCallback) {
+ return CUBEB_ERROR;
+ }
+
+ if (aDevType & CUBEB_DEVICE_TYPE_INPUT) {
+ mInputDeviceCollectionChangeCallback = aCallback;
+ mInputDeviceCollectionChangeUserPtr = aUserPtr;
+ }
+ if (aDevType & CUBEB_DEVICE_TYPE_OUTPUT) {
+ mOutputDeviceCollectionChangeCallback = aCallback;
+ mOutputDeviceCollectionChangeUserPtr = aUserPtr;
+ }
+
+ return CUBEB_OK;
+}
+
+void MockCubeb::AddDevice(cubeb_device_info aDevice) {
+ if (aDevice.type == CUBEB_DEVICE_TYPE_INPUT) {
+ mInputDevices.AppendElement(aDevice);
+ } else if (aDevice.type == CUBEB_DEVICE_TYPE_OUTPUT) {
+ mOutputDevices.AppendElement(aDevice);
+ } else {
+ MOZ_CRASH("bad device type when adding a device in mock cubeb backend");
+ }
+
+ bool isInput = aDevice.type & CUBEB_DEVICE_TYPE_INPUT;
+ if (isInput && mInputDeviceCollectionChangeCallback) {
+ mInputDeviceCollectionChangeCallback(AsCubebContext(),
+ mInputDeviceCollectionChangeUserPtr);
+ }
+ if (!isInput && mOutputDeviceCollectionChangeCallback) {
+ mOutputDeviceCollectionChangeCallback(AsCubebContext(),
+ mOutputDeviceCollectionChangeUserPtr);
+ }
+}
+
+bool MockCubeb::RemoveDevice(cubeb_devid aId) {
+ bool foundInput = false;
+ bool foundOutput = false;
+ mInputDevices.RemoveElementsBy(
+ [aId, &foundInput](cubeb_device_info& aDeviceInfo) {
+ bool foundThisTime = aDeviceInfo.devid == aId;
+ foundInput |= foundThisTime;
+ return foundThisTime;
+ });
+ mOutputDevices.RemoveElementsBy(
+ [aId, &foundOutput](cubeb_device_info& aDeviceInfo) {
+ bool foundThisTime = aDeviceInfo.devid == aId;
+ foundOutput |= foundThisTime;
+ return foundThisTime;
+ });
+
+ if (foundInput && mInputDeviceCollectionChangeCallback) {
+ mInputDeviceCollectionChangeCallback(AsCubebContext(),
+ mInputDeviceCollectionChangeUserPtr);
+ }
+ if (foundOutput && mOutputDeviceCollectionChangeCallback) {
+ mOutputDeviceCollectionChangeCallback(AsCubebContext(),
+ mOutputDeviceCollectionChangeUserPtr);
+ }
+ // If the device removed was a default device, set another device as the
+ // default, if there are still devices available.
+ bool foundDefault = false;
+ for (uint32_t i = 0; i < mInputDevices.Length(); i++) {
+ foundDefault |= mInputDevices[i].preferred != CUBEB_DEVICE_PREF_NONE;
+ }
+
+ if (!foundDefault) {
+ if (!mInputDevices.IsEmpty()) {
+ mInputDevices[mInputDevices.Length() - 1].preferred =
+ CUBEB_DEVICE_PREF_ALL;
+ }
+ }
+
+ foundDefault = false;
+ for (uint32_t i = 0; i < mOutputDevices.Length(); i++) {
+ foundDefault |= mOutputDevices[i].preferred != CUBEB_DEVICE_PREF_NONE;
+ }
+
+ if (!foundDefault) {
+ if (!mOutputDevices.IsEmpty()) {
+ mOutputDevices[mOutputDevices.Length() - 1].preferred =
+ CUBEB_DEVICE_PREF_ALL;
+ }
+ }
+
+ return foundInput | foundOutput;
+}
+
+void MockCubeb::ClearDevices(cubeb_device_type aType) {
+ mInputDevices.Clear();
+ mOutputDevices.Clear();
+}
+
+void MockCubeb::SetSupportDeviceChangeCallback(bool aSupports) {
+ mSupportsDeviceCollectionChangedCallback = aSupports;
+}
+
+void MockCubeb::SetStreamStartFreezeEnabled(bool aEnabled) {
+ mStreamStartFreezeEnabled = aEnabled;
+}
+
+auto MockCubeb::ForceAudioThread() -> RefPtr<ForcedAudioThreadPromise> {
+ RefPtr<ForcedAudioThreadPromise> p =
+ mForcedAudioThreadPromise.Ensure(__func__);
+ mForcedAudioThread = true;
+ StartStream(nullptr);
+ return p;
+}
+
+void MockCubeb::UnforceAudioThread() {
+ mForcedAudioThread = false;
+ StopStream(nullptr);
+}
+
+int MockCubeb::StreamInit(cubeb* aContext, cubeb_stream** aStream,
+ cubeb_devid aInputDevice,
+ cubeb_stream_params* aInputStreamParams,
+ cubeb_devid aOutputDevice,
+ cubeb_stream_params* aOutputStreamParams,
+ cubeb_data_callback aDataCallback,
+ cubeb_state_callback aStateCallback, void* aUserPtr) {
+ auto mockStream = MakeRefPtr<SmartMockCubebStream>(
+ aContext, aInputDevice, aInputStreamParams, aOutputDevice,
+ aOutputStreamParams, aDataCallback, aStateCallback, aUserPtr,
+ mStreamStartFreezeEnabled);
+ *aStream = mockStream->AsCubebStream();
+ mStreamInitEvent.Notify(mockStream);
+ // AddRef the stream to keep it alive. StreamDestroy releases it.
+ Unused << mockStream.forget().take();
+ return CUBEB_OK;
+}
+
+void MockCubeb::StreamDestroy(cubeb_stream* aStream) {
+ RefPtr<SmartMockCubebStream> mockStream =
+ dont_AddRef(MockCubebStream::AsMock(aStream)->mSelf);
+ mStreamDestroyEvent.Notify(mockStream);
+}
+
+void MockCubeb::GoFaster() { mFastMode = true; }
+
+void MockCubeb::DontGoFaster() { mFastMode = false; }
+
+MediaEventSource<RefPtr<SmartMockCubebStream>>& MockCubeb::StreamInitEvent() {
+ return mStreamInitEvent;
+}
+
+MediaEventSource<RefPtr<SmartMockCubebStream>>&
+MockCubeb::StreamDestroyEvent() {
+ return mStreamDestroyEvent;
+}
+
+void MockCubeb::StartStream(MockCubebStream* aStream) {
+ auto streams = mLiveStreams.Lock();
+ MOZ_ASSERT_IF(!aStream, mForcedAudioThread);
+ // Forcing an audio thread must happen before starting streams
+ MOZ_ASSERT_IF(!aStream, streams->IsEmpty());
+ if (aStream) {
+ MOZ_ASSERT(!streams->Contains(aStream->mSelf));
+ streams->AppendElement(aStream->mSelf);
+ }
+ if (!mFakeAudioThread) {
+ mFakeAudioThread = WrapUnique(new std::thread(ThreadFunction_s, this));
+ }
+}
+
+int MockCubeb::StopStream(MockCubebStream* aStream) {
+ UniquePtr<std::thread> audioThread;
+ {
+ auto streams = mLiveStreams.Lock();
+ if (aStream) {
+ if (!streams->Contains(aStream->mSelf)) {
+ return CUBEB_ERROR;
+ }
+ streams->RemoveElement(aStream->mSelf);
+ }
+ MOZ_ASSERT(mFakeAudioThread);
+ if (streams->IsEmpty() && !mForcedAudioThread) {
+ audioThread = std::move(mFakeAudioThread);
+ }
+ }
+ if (audioThread) {
+ audioThread->join();
+ }
+ return CUBEB_OK;
+}
+
+void MockCubeb::ThreadFunction() {
+ if (mForcedAudioThread) {
+ mForcedAudioThreadPromise.Resolve(MakeRefPtr<AudioThreadAutoUnforcer>(this),
+ __func__);
+ }
+ while (true) {
+ {
+ auto streams = mLiveStreams.Lock();
+ for (auto& stream : *streams) {
+ stream->Process10Ms();
+ }
+ if (streams->IsEmpty() && !mForcedAudioThread) {
+ break;
+ }
+ }
+ std::this_thread::sleep_for(
+ std::chrono::microseconds(mFastMode ? 0 : 10 * PR_USEC_PER_MSEC));
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/gtest/MockCubeb.h b/dom/media/gtest/MockCubeb.h
new file mode 100644
index 0000000000..4adaa4e02b
--- /dev/null
+++ b/dom/media/gtest/MockCubeb.h
@@ -0,0 +1,527 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef MOCKCUBEB_H_
+#define MOCKCUBEB_H_
+
+#include "AudioDeviceInfo.h"
+#include "AudioGenerator.h"
+#include "AudioVerifier.h"
+#include "MediaEventSource.h"
+#include "mozilla/DataMutex.h"
+#include "mozilla/ThreadSafeWeakPtr.h"
+#include "nsTArray.h"
+
+#include <thread>
+#include <atomic>
+#include <chrono>
+
+namespace mozilla {
+const uint32_t MAX_OUTPUT_CHANNELS = 2;
+const uint32_t MAX_INPUT_CHANNELS = 2;
+
+struct cubeb_ops {
+ int (*init)(cubeb** context, char const* context_name);
+ char const* (*get_backend_id)(cubeb* context);
+ int (*get_max_channel_count)(cubeb* context, uint32_t* max_channels);
+ int (*get_min_latency)(cubeb* context, cubeb_stream_params params,
+ uint32_t* latency_ms);
+ int (*get_preferred_sample_rate)(cubeb* context, uint32_t* rate);
+ int (*enumerate_devices)(cubeb* context, cubeb_device_type type,
+ cubeb_device_collection* collection);
+ int (*device_collection_destroy)(cubeb* context,
+ cubeb_device_collection* collection);
+ void (*destroy)(cubeb* context);
+ int (*stream_init)(cubeb* context, cubeb_stream** stream,
+ char const* stream_name, cubeb_devid input_device,
+ cubeb_stream_params* input_stream_params,
+ cubeb_devid output_device,
+ cubeb_stream_params* output_stream_params,
+ unsigned int latency, cubeb_data_callback data_callback,
+ cubeb_state_callback state_callback, void* user_ptr);
+ void (*stream_destroy)(cubeb_stream* stream);
+ int (*stream_start)(cubeb_stream* stream);
+ int (*stream_stop)(cubeb_stream* stream);
+ int (*stream_get_position)(cubeb_stream* stream, uint64_t* position);
+ int (*stream_get_latency)(cubeb_stream* stream, uint32_t* latency);
+ int (*stream_get_input_latency)(cubeb_stream* stream, uint32_t* latency);
+ int (*stream_set_volume)(cubeb_stream* stream, float volumes);
+ int (*stream_set_name)(cubeb_stream* stream, char const* stream_name);
+ int (*stream_get_current_device)(cubeb_stream* stream,
+ cubeb_device** const device);
+ int (*stream_device_destroy)(cubeb_stream* stream, cubeb_device* device);
+ int (*stream_register_device_changed_callback)(
+ cubeb_stream* stream,
+ cubeb_device_changed_callback device_changed_callback);
+ int (*register_device_collection_changed)(
+ cubeb* context, cubeb_device_type devtype,
+ cubeb_device_collection_changed_callback callback, void* user_ptr);
+};
+
+// Keep those and the struct definition in sync with cubeb.h and
+// cubeb-internal.h
+void cubeb_mock_destroy(cubeb* context);
+static int cubeb_mock_enumerate_devices(cubeb* context, cubeb_device_type type,
+ cubeb_device_collection* out);
+
+static int cubeb_mock_device_collection_destroy(
+ cubeb* context, cubeb_device_collection* collection);
+
+static int cubeb_mock_register_device_collection_changed(
+ cubeb* context, cubeb_device_type devtype,
+ cubeb_device_collection_changed_callback callback, void* user_ptr);
+
+static int cubeb_mock_stream_init(
+ cubeb* context, cubeb_stream** stream, char const* stream_name,
+ cubeb_devid input_device, cubeb_stream_params* input_stream_params,
+ cubeb_devid output_device, cubeb_stream_params* output_stream_params,
+ unsigned int latency, cubeb_data_callback data_callback,
+ cubeb_state_callback state_callback, void* user_ptr);
+
+static int cubeb_mock_stream_start(cubeb_stream* stream);
+
+static int cubeb_mock_stream_stop(cubeb_stream* stream);
+
+static void cubeb_mock_stream_destroy(cubeb_stream* stream);
+
+static char const* cubeb_mock_get_backend_id(cubeb* context);
+
+static int cubeb_mock_stream_set_volume(cubeb_stream* stream, float volume);
+
+static int cubeb_mock_stream_set_name(cubeb_stream* stream,
+ char const* stream_name);
+
+static int cubeb_mock_stream_register_device_changed_callback(
+ cubeb_stream* stream,
+ cubeb_device_changed_callback device_changed_callback);
+
+static int cubeb_mock_get_min_latency(cubeb* context,
+ cubeb_stream_params params,
+ uint32_t* latency_ms);
+
+static int cubeb_mock_get_preferred_sample_rate(cubeb* context, uint32_t* rate);
+
+static int cubeb_mock_get_max_channel_count(cubeb* context,
+ uint32_t* max_channels);
+
+// Mock cubeb impl, only supports device enumeration for now.
+cubeb_ops const mock_ops = {
+ /*.init =*/NULL,
+ /*.get_backend_id =*/cubeb_mock_get_backend_id,
+ /*.get_max_channel_count =*/cubeb_mock_get_max_channel_count,
+ /*.get_min_latency =*/cubeb_mock_get_min_latency,
+ /*.get_preferred_sample_rate =*/cubeb_mock_get_preferred_sample_rate,
+ /*.enumerate_devices =*/cubeb_mock_enumerate_devices,
+ /*.device_collection_destroy =*/cubeb_mock_device_collection_destroy,
+ /*.destroy =*/cubeb_mock_destroy,
+ /*.stream_init =*/cubeb_mock_stream_init,
+ /*.stream_destroy =*/cubeb_mock_stream_destroy,
+ /*.stream_start =*/cubeb_mock_stream_start,
+ /*.stream_stop =*/cubeb_mock_stream_stop,
+ /*.stream_get_position =*/NULL,
+ /*.stream_get_latency =*/NULL,
+ /*.stream_get_input_latency =*/NULL,
+ /*.stream_set_volume =*/cubeb_mock_stream_set_volume,
+ /*.stream_set_name =*/cubeb_mock_stream_set_name,
+ /*.stream_get_current_device =*/NULL,
+ /*.stream_device_destroy =*/NULL,
+ /*.stream_register_device_changed_callback =*/
+ cubeb_mock_stream_register_device_changed_callback,
+ /*.register_device_collection_changed =*/
+
+ cubeb_mock_register_device_collection_changed};
+
+class SmartMockCubebStream;
+
+// Represents the fake cubeb_stream. The context instance is needed to
+// provide access on cubeb_ops struct.
+class MockCubebStream {
+ // These members need to have the exact same memory layout as a real
+ // cubeb_stream, so that AsMock() returns a pointer to this that can be used
+ // as a cubeb_stream.
+ cubeb* context;
+ void* mUserPtr;
+
+ public:
+ MockCubebStream(cubeb* aContext, cubeb_devid aInputDevice,
+ cubeb_stream_params* aInputStreamParams,
+ cubeb_devid aOutputDevice,
+ cubeb_stream_params* aOutputStreamParams,
+ cubeb_data_callback aDataCallback,
+ cubeb_state_callback aStateCallback, void* aUserPtr,
+ SmartMockCubebStream* aSelf, bool aFrozenStart);
+
+ ~MockCubebStream();
+
+ int Start();
+ int Stop();
+ void Destroy();
+ int RegisterDeviceChangedCallback(
+ cubeb_device_changed_callback aDeviceChangedCallback);
+
+ cubeb_stream* AsCubebStream();
+ static MockCubebStream* AsMock(cubeb_stream* aStream);
+
+ cubeb_devid GetInputDeviceID() const;
+ cubeb_devid GetOutputDeviceID() const;
+
+ uint32_t InputChannels() const;
+ uint32_t OutputChannels() const;
+ uint32_t InputSampleRate() const;
+ uint32_t InputFrequency() const;
+
+ void SetDriftFactor(float aDriftFactor);
+ void ForceError();
+ void ForceDeviceChanged();
+ void Thaw();
+
+ // Enable input recording for this driver. This is best called before
+ // the thread is running, but is safe to call whenever.
+ void SetOutputRecordingEnabled(bool aEnabled);
+ // Enable input recording for this driver. This is best called before
+ // the thread is running, but is safe to call whenever.
+ void SetInputRecordingEnabled(bool aEnabled);
+ // Get the recorded output from this stream. This doesn't copy, and therefore
+ // only works once.
+ nsTArray<AudioDataValue>&& TakeRecordedOutput();
+ // Get the recorded input from this stream. This doesn't copy, and therefore
+ // only works once.
+ nsTArray<AudioDataValue>&& TakeRecordedInput();
+
+ MediaEventSource<cubeb_state>& StateEvent();
+ MediaEventSource<uint32_t>& FramesProcessedEvent();
+ MediaEventSource<uint32_t>& FramesVerifiedEvent();
+ MediaEventSource<std::tuple<uint64_t, float, uint32_t>>&
+ OutputVerificationEvent();
+ MediaEventSource<void>& ErrorForcedEvent();
+ MediaEventSource<void>& ErrorStoppedEvent();
+ MediaEventSource<void>& DeviceChangeForcedEvent();
+
+ void Process10Ms();
+
+ public:
+ const bool mHasInput;
+ const bool mHasOutput;
+ SmartMockCubebStream* const mSelf;
+
+ private:
+ void NotifyStateChanged(cubeb_state aState);
+
+ // Monitor used to block start until mFrozenStart is false.
+ Monitor mFrozenStartMonitor MOZ_UNANNOTATED;
+ // Whether this stream should wait for an explicit start request before
+ // starting. Protected by FrozenStartMonitor.
+ bool mFrozenStart;
+ // Signal to the audio thread that stream is stopped.
+ std::atomic_bool mStreamStop{true};
+ // Whether or not the output-side of this stream (what is written from the
+ // callback output buffer) is recorded in an internal buffer. The data is then
+ // available via `GetRecordedOutput`.
+ std::atomic_bool mOutputRecordingEnabled{false};
+ // Whether or not the input-side of this stream (what is written from the
+ // callback input buffer) is recorded in an internal buffer. The data is then
+ // available via `TakeRecordedInput`.
+ std::atomic_bool mInputRecordingEnabled{false};
+ // The audio buffer used on data callback.
+ AudioDataValue mOutputBuffer[MAX_OUTPUT_CHANNELS * 1920] = {};
+ AudioDataValue mInputBuffer[MAX_INPUT_CHANNELS * 1920] = {};
+ // The audio callback
+ cubeb_data_callback mDataCallback = nullptr;
+ // The stream state callback
+ cubeb_state_callback mStateCallback = nullptr;
+ // The device changed callback
+ cubeb_device_changed_callback mDeviceChangedCallback = nullptr;
+ // The stream params
+ cubeb_stream_params mOutputParams = {};
+ cubeb_stream_params mInputParams = {};
+ /* Device IDs */
+ cubeb_devid mInputDeviceID;
+ cubeb_devid mOutputDeviceID;
+
+ std::atomic<float> mDriftFactor{1.0};
+ std::atomic_bool mFastMode{false};
+ std::atomic_bool mForceErrorState{false};
+ std::atomic_bool mForceDeviceChanged{false};
+ AudioGenerator<AudioDataValue> mAudioGenerator;
+ AudioVerifier<AudioDataValue> mAudioVerifier;
+
+ MediaEventProducer<cubeb_state> mStateEvent;
+ MediaEventProducer<uint32_t> mFramesProcessedEvent;
+ MediaEventProducer<uint32_t> mFramesVerifiedEvent;
+ MediaEventProducer<std::tuple<uint64_t, float, uint32_t>>
+ mOutputVerificationEvent;
+ MediaEventProducer<void> mErrorForcedEvent;
+ MediaEventProducer<void> mErrorStoppedEvent;
+ MediaEventProducer<void> mDeviceChangedForcedEvent;
+ // The recorded data, copied from the output_buffer of the callback.
+ // Interleaved.
+ nsTArray<AudioDataValue> mRecordedOutput;
+ // The recorded data, copied from the input buffer of the callback.
+ // Interleaved.
+ nsTArray<AudioDataValue> mRecordedInput;
+};
+
+class SmartMockCubebStream
+ : public MockCubebStream,
+ public SupportsThreadSafeWeakPtr<SmartMockCubebStream> {
+ public:
+ MOZ_DECLARE_REFCOUNTED_TYPENAME(SmartMockCubebStream)
+ SmartMockCubebStream(cubeb* aContext, cubeb_devid aInputDevice,
+ cubeb_stream_params* aInputStreamParams,
+ cubeb_devid aOutputDevice,
+ cubeb_stream_params* aOutputStreamParams,
+ cubeb_data_callback aDataCallback,
+ cubeb_state_callback aStateCallback, void* aUserPtr,
+ bool aFrozenStart)
+ : MockCubebStream(aContext, aInputDevice, aInputStreamParams,
+ aOutputDevice, aOutputStreamParams, aDataCallback,
+ aStateCallback, aUserPtr, this, aFrozenStart) {}
+};
+
+// This class has two facets: it is both a fake cubeb backend that is intended
+// to be used for testing, and passed to Gecko code that expects a normal
+// backend, but is also controllable by the test code to decide what the backend
+// should do, depending on what is being tested.
+class MockCubeb {
+ public:
+ MockCubeb();
+ ~MockCubeb();
+ // Cubeb backend implementation
+ // This allows passing this class as a cubeb* instance.
+ cubeb* AsCubebContext();
+ static MockCubeb* AsMock(cubeb* aContext);
+ // Fill in the collection parameter with all devices of aType.
+ int EnumerateDevices(cubeb_device_type aType,
+ cubeb_device_collection* aCollection);
+ // Clear the collection parameter and deallocate its related memory space.
+ int DestroyDeviceCollection(cubeb_device_collection* aCollection);
+
+ // For a given device type, add a callback, called with a user pointer, when
+ // the device collection for this backend changes (i.e. a device has been
+ // removed or added).
+ int RegisterDeviceCollectionChangeCallback(
+ cubeb_device_type aDevType,
+ cubeb_device_collection_changed_callback aCallback, void* aUserPtr);
+
+ // Control API
+
+ // Add an input or output device to this backend. This calls the device
+ // collection invalidation callback if needed.
+ void AddDevice(cubeb_device_info aDevice);
+ // Remove a specific input or output device to this backend, returns true if
+ // a device was removed. This calls the device collection invalidation
+ // callback if needed.
+ bool RemoveDevice(cubeb_devid aId);
+ // Remove all input or output devices from this backend, without calling the
+ // callback. This is meant to clean up in between tests.
+ void ClearDevices(cubeb_device_type aType);
+
+ // This allows simulating a backend that does not support setting a device
+ // collection invalidation callback, to be able to test the fallback path.
+ void SetSupportDeviceChangeCallback(bool aSupports);
+
+ // Makes MockCubebStreams starting after this point wait for AllowStart().
+ // Callers must ensure they get a hold of the stream through StreamInitEvent
+ // to be able to start them.
+ void SetStreamStartFreezeEnabled(bool aEnabled);
+
+ // Helper class that automatically unforces a forced audio thread on release.
+ class AudioThreadAutoUnforcer {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AudioThreadAutoUnforcer)
+
+ public:
+ explicit AudioThreadAutoUnforcer(MockCubeb* aContext)
+ : mContext(aContext) {}
+
+ protected:
+ virtual ~AudioThreadAutoUnforcer() { mContext->UnforceAudioThread(); }
+ MockCubeb* mContext;
+ };
+
+ // Creates the audio thread if one is not available. The audio thread remains
+ // forced until UnforceAudioThread is called. The returned promise is resolved
+ // when the audio thread is running. With this, a test can ensure starting
+ // audio streams is deterministically fast across platforms for more accurate
+ // results.
+ using ForcedAudioThreadPromise =
+ MozPromise<RefPtr<AudioThreadAutoUnforcer>, nsresult, false>;
+ RefPtr<ForcedAudioThreadPromise> ForceAudioThread();
+
+ // Allows a forced audio thread to stop.
+ void UnforceAudioThread();
+
+ int StreamInit(cubeb* aContext, cubeb_stream** aStream,
+ cubeb_devid aInputDevice,
+ cubeb_stream_params* aInputStreamParams,
+ cubeb_devid aOutputDevice,
+ cubeb_stream_params* aOutputStreamParams,
+ cubeb_data_callback aDataCallback,
+ cubeb_state_callback aStateCallback, void* aUserPtr);
+
+ void StreamDestroy(cubeb_stream* aStream);
+
+ void GoFaster();
+ void DontGoFaster();
+
+ MediaEventSource<RefPtr<SmartMockCubebStream>>& StreamInitEvent();
+ MediaEventSource<RefPtr<SmartMockCubebStream>>& StreamDestroyEvent();
+
+ // MockCubeb specific API
+ void StartStream(MockCubebStream* aStream);
+ int StopStream(MockCubebStream* aStream);
+
+ // Simulates the audio thread. The thread is created at Start and destroyed
+ // at Stop. At next StreamStart a new thread is created.
+ static void ThreadFunction_s(MockCubeb* aContext) {
+ aContext->ThreadFunction();
+ }
+
+ void ThreadFunction();
+
+ private:
+ // This needs to have the exact same memory layout as a real cubeb backend.
+ // It's very important for this `ops` member to be the very first member of
+ // the class, and to not have any virtual members (to avoid having a
+ // vtable).
+ const cubeb_ops* ops;
+ // The callback to call when the device list has been changed.
+ cubeb_device_collection_changed_callback
+ mInputDeviceCollectionChangeCallback = nullptr;
+ cubeb_device_collection_changed_callback
+ mOutputDeviceCollectionChangeCallback = nullptr;
+ // The pointer to pass in the callback.
+ void* mInputDeviceCollectionChangeUserPtr = nullptr;
+ void* mOutputDeviceCollectionChangeUserPtr = nullptr;
+ void* mUserPtr = nullptr;
+ // Whether or not this backend supports device collection change
+ // notification via a system callback. If not, Gecko is expected to re-query
+ // the list every time.
+ bool mSupportsDeviceCollectionChangedCallback = true;
+ // Whether new MockCubebStreams should be frozen on start.
+ Atomic<bool> mStreamStartFreezeEnabled{false};
+ // Whether the audio thread is forced, i.e., whether it remains active even
+ // with no live streams.
+ Atomic<bool> mForcedAudioThread{false};
+ MozPromiseHolder<ForcedAudioThreadPromise> mForcedAudioThreadPromise;
+ // Our input and output devices.
+ nsTArray<cubeb_device_info> mInputDevices;
+ nsTArray<cubeb_device_info> mOutputDevices;
+
+ // The streams that are currently running.
+ DataMutex<nsTArray<RefPtr<SmartMockCubebStream>>> mLiveStreams{
+ "MockCubeb::mLiveStreams"};
+ // Thread that simulates the audio thread, shared across MockCubebStreams to
+ // avoid unintended drift. This is set together with mLiveStreams, under the
+ // mLiveStreams DataMutex.
+ UniquePtr<std::thread> mFakeAudioThread;
+ // Whether to run the fake audio thread in fast mode, not caring about wall
+ // clock time. false is default and means data is processed every 10ms. When
+ // true we sleep(0) between iterations instead of 10ms.
+ std::atomic<bool> mFastMode{false};
+
+ MediaEventProducer<RefPtr<SmartMockCubebStream>> mStreamInitEvent;
+ MediaEventProducer<RefPtr<SmartMockCubebStream>> mStreamDestroyEvent;
+};
+
+int cubeb_mock_enumerate_devices(cubeb* context, cubeb_device_type type,
+ cubeb_device_collection* out) {
+ return MockCubeb::AsMock(context)->EnumerateDevices(type, out);
+}
+
+int cubeb_mock_device_collection_destroy(cubeb* context,
+ cubeb_device_collection* collection) {
+ return MockCubeb::AsMock(context)->DestroyDeviceCollection(collection);
+}
+
+int cubeb_mock_register_device_collection_changed(
+ cubeb* context, cubeb_device_type devtype,
+ cubeb_device_collection_changed_callback callback, void* user_ptr) {
+ return MockCubeb::AsMock(context)->RegisterDeviceCollectionChangeCallback(
+ devtype, callback, user_ptr);
+}
+
+int cubeb_mock_stream_init(
+ cubeb* context, cubeb_stream** stream, char const* stream_name,
+ cubeb_devid input_device, cubeb_stream_params* input_stream_params,
+ cubeb_devid output_device, cubeb_stream_params* output_stream_params,
+ unsigned int latency, cubeb_data_callback data_callback,
+ cubeb_state_callback state_callback, void* user_ptr) {
+ return MockCubeb::AsMock(context)->StreamInit(
+ context, stream, input_device, input_stream_params, output_device,
+ output_stream_params, data_callback, state_callback, user_ptr);
+}
+
+int cubeb_mock_stream_start(cubeb_stream* stream) {
+ return MockCubebStream::AsMock(stream)->Start();
+}
+
+int cubeb_mock_stream_stop(cubeb_stream* stream) {
+ return MockCubebStream::AsMock(stream)->Stop();
+}
+
+void cubeb_mock_stream_destroy(cubeb_stream* stream) {
+ MockCubebStream::AsMock(stream)->Destroy();
+}
+
+static char const* cubeb_mock_get_backend_id(cubeb* context) {
+#if defined(XP_MACOSX)
+ return "audiounit";
+#elif defined(XP_WIN)
+ return "wasapi";
+#elif defined(ANDROID)
+ return "opensl";
+#elif defined(__OpenBSD__)
+ return "sndio";
+#else
+ return "pulse";
+#endif
+}
+
+static int cubeb_mock_stream_set_volume(cubeb_stream* stream, float volume) {
+ return CUBEB_OK;
+}
+
+static int cubeb_mock_stream_set_name(cubeb_stream* stream,
+ char const* stream_name) {
+ return CUBEB_OK;
+}
+
+int cubeb_mock_stream_register_device_changed_callback(
+ cubeb_stream* stream,
+ cubeb_device_changed_callback device_changed_callback) {
+ return MockCubebStream::AsMock(stream)->RegisterDeviceChangedCallback(
+ device_changed_callback);
+}
+
+int cubeb_mock_get_min_latency(cubeb* context, cubeb_stream_params params,
+ uint32_t* latency_ms) {
+ *latency_ms = 10;
+ return CUBEB_OK;
+}
+
+int cubeb_mock_get_preferred_sample_rate(cubeb* context, uint32_t* rate) {
+ *rate = 44100;
+ return CUBEB_OK;
+}
+
+int cubeb_mock_get_max_channel_count(cubeb* context, uint32_t* max_channels) {
+ *max_channels = MAX_OUTPUT_CHANNELS;
+ return CUBEB_OK;
+}
+
+void PrintDevice(cubeb_device_info aInfo);
+
+void PrintDevice(AudioDeviceInfo* aInfo);
+
+cubeb_device_info DeviceTemplate(cubeb_devid aId, cubeb_device_type aType,
+ const char* name);
+
+cubeb_device_info DeviceTemplate(cubeb_devid aId, cubeb_device_type aType);
+
+void AddDevices(MockCubeb* mock, uint32_t device_count,
+ cubeb_device_type deviceType);
+
+} // namespace mozilla
+
+#endif // MOCKCUBEB_H_
diff --git a/dom/media/gtest/MockMediaResource.cpp b/dom/media/gtest/MockMediaResource.cpp
new file mode 100644
index 0000000000..8811af7c0b
--- /dev/null
+++ b/dom/media/gtest/MockMediaResource.cpp
@@ -0,0 +1,91 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MockMediaResource.h"
+
+#include <sys/types.h>
+#include <sys/stat.h>
+
+namespace mozilla {
+
+MockMediaResource::MockMediaResource(const char* aFileName)
+ : mFileHandle(nullptr), mFileName(aFileName) {}
+
+nsresult MockMediaResource::Open() {
+ mFileHandle = fopen(mFileName, "rb");
+ if (mFileHandle == nullptr) {
+ printf_stderr("Can't open %s\n", mFileName);
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+MockMediaResource::~MockMediaResource() {
+ if (mFileHandle != nullptr) {
+ fclose(mFileHandle);
+ }
+}
+
+nsresult MockMediaResource::ReadAt(int64_t aOffset, char* aBuffer,
+ uint32_t aCount, uint32_t* aBytes) {
+ if (mFileHandle == nullptr) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Make it fail if we're re-entrant
+ if (mEntry++) {
+ MOZ_ASSERT(false);
+ return NS_ERROR_FAILURE;
+ }
+
+ fseek(mFileHandle, aOffset, SEEK_SET);
+ *aBytes = fread(aBuffer, 1, aCount, mFileHandle);
+
+ mEntry--;
+
+ return ferror(mFileHandle) ? NS_ERROR_FAILURE : NS_OK;
+}
+
+int64_t MockMediaResource::GetLength() {
+ if (mFileHandle == nullptr) {
+ return -1;
+ }
+ fseek(mFileHandle, 0, SEEK_END);
+ return ftell(mFileHandle);
+}
+
+void MockMediaResource::MockClearBufferedRanges() { mRanges.Clear(); }
+
+void MockMediaResource::MockAddBufferedRange(int64_t aStart, int64_t aEnd) {
+ mRanges += MediaByteRange(aStart, aEnd);
+}
+
+int64_t MockMediaResource::GetNextCachedData(int64_t aOffset) {
+ if (!aOffset) {
+ return mRanges.Length() ? mRanges[0].mStart : -1;
+ }
+ for (size_t i = 0; i < mRanges.Length(); i++) {
+ if (aOffset == mRanges[i].mStart) {
+ ++i;
+ return i < mRanges.Length() ? mRanges[i].mStart : -1;
+ }
+ }
+ return -1;
+}
+
+int64_t MockMediaResource::GetCachedDataEnd(int64_t aOffset) {
+ for (size_t i = 0; i < mRanges.Length(); i++) {
+ if (aOffset == mRanges[i].mStart) {
+ return mRanges[i].mEnd;
+ }
+ }
+ return aOffset;
+}
+
+nsresult MockMediaResource::GetCachedRanges(MediaByteRangeSet& aRanges) {
+ aRanges = mRanges;
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/dom/media/gtest/MockMediaResource.h b/dom/media/gtest/MockMediaResource.h
new file mode 100644
index 0000000000..9ec2a884a0
--- /dev/null
+++ b/dom/media/gtest/MockMediaResource.h
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOCK_MEDIA_RESOURCE_H_
+#define MOCK_MEDIA_RESOURCE_H_
+
+#include "MediaResource.h"
+#include "nsTArray.h"
+#include "mozilla/Atomics.h"
+
+namespace mozilla {
+
+DDLoggedTypeDeclNameAndBase(MockMediaResource, MediaResource);
+
+class MockMediaResource : public MediaResource,
+ public DecoderDoctorLifeLogger<MockMediaResource> {
+ public:
+ explicit MockMediaResource(const char* aFileName);
+ nsresult ReadAt(int64_t aOffset, char* aBuffer, uint32_t aCount,
+ uint32_t* aBytes) override;
+ // Data stored in file, caching recommended.
+ bool ShouldCacheReads() override { return true; }
+ void Pin() override {}
+ void Unpin() override {}
+ int64_t GetLength() override;
+ int64_t GetNextCachedData(int64_t aOffset) override;
+ int64_t GetCachedDataEnd(int64_t aOffset) override;
+ bool IsDataCachedToEndOfResource(int64_t aOffset) override { return false; }
+ nsresult ReadFromCache(char* aBuffer, int64_t aOffset,
+ uint32_t aCount) override {
+ uint32_t bytesRead = 0;
+ nsresult rv = ReadAt(aOffset, aBuffer, aCount, &bytesRead);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return bytesRead == aCount ? NS_OK : NS_ERROR_FAILURE;
+ }
+
+ nsresult Open();
+ nsresult GetCachedRanges(MediaByteRangeSet& aRanges) override;
+
+ void MockClearBufferedRanges();
+ void MockAddBufferedRange(int64_t aStart, int64_t aEnd);
+
+ protected:
+ virtual ~MockMediaResource();
+
+ private:
+ FILE* mFileHandle;
+ const char* mFileName;
+ MediaByteRangeSet mRanges;
+ Atomic<int> mEntry;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/gtest/TestAudioBuffers.cpp b/dom/media/gtest/TestAudioBuffers.cpp
new file mode 100644
index 0000000000..2de1e646fb
--- /dev/null
+++ b/dom/media/gtest/TestAudioBuffers.cpp
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <stdint.h>
+#include "AudioBufferUtils.h"
+#include "gtest/gtest.h"
+#include <vector>
+
+const uint32_t FRAMES = 256;
+
+void test_for_number_of_channels(const uint32_t channels) {
+ const uint32_t samples = channels * FRAMES;
+
+ mozilla::AudioCallbackBufferWrapper<float> mBuffer(channels);
+ mozilla::SpillBuffer<float, 128> b(channels);
+ std::vector<float> fromCallback(samples, 0.0);
+ std::vector<float> other(samples, 1.0);
+
+ // Set the buffer in the wrapper from the callback
+ mBuffer.SetBuffer(fromCallback.data(), FRAMES);
+
+ // Fill the SpillBuffer with data.
+ ASSERT_TRUE(b.Fill(other.data(), 15) == 15);
+ ASSERT_TRUE(b.Fill(other.data(), 17) == 17);
+ for (uint32_t i = 0; i < 32 * channels; i++) {
+ other[i] = 0.0;
+ }
+
+ // Empty it in the AudioCallbackBufferWrapper
+ ASSERT_TRUE(b.Empty(mBuffer) == 32);
+
+ // Check available return something reasonnable
+ ASSERT_TRUE(mBuffer.Available() == FRAMES - 32);
+
+ // Fill the buffer with the rest of the data
+ mBuffer.WriteFrames(other.data() + 32 * channels, FRAMES - 32);
+
+ // Check the buffer is now full
+ ASSERT_TRUE(mBuffer.Available() == 0);
+
+ for (uint32_t i = 0; i < samples; i++) {
+ ASSERT_TRUE(fromCallback[i] == 1.0)
+ << "Difference at " << i << " (" << fromCallback[i] << " != " << 1.0
+ << ")\n";
+ }
+
+ ASSERT_TRUE(b.Fill(other.data(), FRAMES) == 128);
+ ASSERT_TRUE(b.Fill(other.data(), FRAMES) == 0);
+ ASSERT_TRUE(b.Empty(mBuffer) == 0);
+}
+
+TEST(AudioBuffers, Test)
+{
+ for (uint32_t ch = 1; ch <= 8; ++ch) {
+ test_for_number_of_channels(ch);
+ }
+}
diff --git a/dom/media/gtest/TestAudioCallbackDriver.cpp b/dom/media/gtest/TestAudioCallbackDriver.cpp
new file mode 100644
index 0000000000..afde478150
--- /dev/null
+++ b/dom/media/gtest/TestAudioCallbackDriver.cpp
@@ -0,0 +1,224 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CubebUtils.h"
+#include "GraphDriver.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest-printers.h"
+#include "gtest/gtest.h"
+
+#include "MediaTrackGraphImpl.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/UniquePtr.h"
+#include "nsTArray.h"
+
+#include "MockCubeb.h"
+#include "WaitFor.h"
+
+using namespace mozilla;
+using IterationResult = GraphInterface::IterationResult;
+using ::testing::NiceMock;
+
+class MockGraphInterface : public GraphInterface {
+ NS_DECL_THREADSAFE_ISUPPORTS
+ explicit MockGraphInterface(TrackRate aSampleRate)
+ : mSampleRate(aSampleRate) {}
+ MOCK_METHOD4(NotifyOutputData,
+ void(AudioDataValue*, size_t, TrackRate, uint32_t));
+ MOCK_METHOD0(NotifyInputStopped, void());
+ MOCK_METHOD5(NotifyInputData, void(const AudioDataValue*, size_t, TrackRate,
+ uint32_t, uint32_t));
+ MOCK_METHOD0(DeviceChanged, void());
+ /* OneIteration cannot be mocked because IterationResult is non-memmovable and
+ * cannot be passed as a parameter, which GMock does internally. */
+ IterationResult OneIteration(GraphTime aStateComputedTime, GraphTime,
+ AudioMixer* aMixer) {
+ GraphDriver* driver = mCurrentDriver;
+ if (aMixer) {
+ aMixer->StartMixing();
+ aMixer->Mix(nullptr,
+ driver->AsAudioCallbackDriver()->OutputChannelCount(),
+ aStateComputedTime - mStateComputedTime, mSampleRate);
+ aMixer->FinishMixing();
+ }
+ if (aStateComputedTime != mStateComputedTime) {
+ mFramesIteratedEvent.Notify(aStateComputedTime - mStateComputedTime);
+ ++mIterationCount;
+ }
+ mStateComputedTime = aStateComputedTime;
+ if (!mKeepProcessing) {
+ return IterationResult::CreateStop(
+ NS_NewRunnableFunction(__func__, [] {}));
+ }
+ GraphDriver* next = mNextDriver.exchange(nullptr);
+ if (next) {
+ return IterationResult::CreateSwitchDriver(
+ next, NS_NewRunnableFunction(__func__, [] {}));
+ }
+ if (mEnsureNextIteration) {
+ driver->EnsureNextIteration();
+ }
+ return IterationResult::CreateStillProcessing();
+ }
+ void SetEnsureNextIteration(bool aEnsure) { mEnsureNextIteration = aEnsure; }
+
+#ifdef DEBUG
+ bool InDriverIteration(const GraphDriver* aDriver) const override {
+ return aDriver->OnThread();
+ }
+#endif
+
+ size_t IterationCount() const { return mIterationCount; }
+
+ GraphTime StateComputedTime() const { return mStateComputedTime; }
+ void SetCurrentDriver(GraphDriver* aDriver) { mCurrentDriver = aDriver; }
+
+ void StopIterating() { mKeepProcessing = false; }
+
+ void SwitchTo(GraphDriver* aDriver) { mNextDriver = aDriver; }
+ const TrackRate mSampleRate;
+
+ MediaEventSource<uint32_t>& FramesIteratedEvent() {
+ return mFramesIteratedEvent;
+ }
+
+ protected:
+ Atomic<size_t> mIterationCount{0};
+ Atomic<GraphTime> mStateComputedTime{0};
+ Atomic<GraphDriver*> mCurrentDriver{nullptr};
+ Atomic<bool> mEnsureNextIteration{false};
+ Atomic<bool> mKeepProcessing{true};
+ Atomic<GraphDriver*> mNextDriver{nullptr};
+ MediaEventProducer<uint32_t> mFramesIteratedEvent;
+ virtual ~MockGraphInterface() = default;
+};
+
+NS_IMPL_ISUPPORTS0(MockGraphInterface)
+
+TEST(TestAudioCallbackDriver, StartStop)
+MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION {
+ const TrackRate rate = 44100;
+ MockCubeb* cubeb = new MockCubeb();
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+
+ RefPtr<AudioCallbackDriver> driver;
+ auto graph = MakeRefPtr<NiceMock<MockGraphInterface>>(rate);
+ EXPECT_CALL(*graph, NotifyInputStopped).Times(0);
+ ON_CALL(*graph, NotifyOutputData)
+ .WillByDefault([&](AudioDataValue*, size_t, TrackRate, uint32_t) {});
+
+ driver = MakeRefPtr<AudioCallbackDriver>(graph, nullptr, rate, 2, 0, nullptr,
+ nullptr, AudioInputType::Unknown);
+ EXPECT_FALSE(driver->ThreadRunning()) << "Verify thread is not running";
+ EXPECT_FALSE(driver->IsStarted()) << "Verify thread is not started";
+
+ graph->SetCurrentDriver(driver);
+ driver->Start();
+ // Allow some time to "play" audio.
+ std::this_thread::sleep_for(std::chrono::milliseconds(200));
+ EXPECT_TRUE(driver->ThreadRunning()) << "Verify thread is running";
+ EXPECT_TRUE(driver->IsStarted()) << "Verify thread is started";
+
+ // This will block untill all events have been executed.
+ MOZ_KnownLive(driver)->Shutdown();
+ EXPECT_FALSE(driver->ThreadRunning()) << "Verify thread is not running";
+ EXPECT_FALSE(driver->IsStarted()) << "Verify thread is not started";
+}
+
+void TestSlowStart(const TrackRate aRate) MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION {
+ std::cerr << "TestSlowStart with rate " << aRate << std::endl;
+
+ MockCubeb* cubeb = new MockCubeb();
+ cubeb->SetStreamStartFreezeEnabled(true);
+ auto unforcer = WaitFor(cubeb->ForceAudioThread()).unwrap();
+ Unused << unforcer;
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+
+ RefPtr<AudioCallbackDriver> driver;
+ auto graph = MakeRefPtr<NiceMock<MockGraphInterface>>(aRate);
+ EXPECT_CALL(*graph, NotifyInputStopped).Times(0);
+
+ Maybe<int64_t> audioStart;
+ Maybe<uint32_t> alreadyBuffered;
+ int64_t inputFrameCount = 0;
+ int64_t outputFrameCount = 0;
+ int64_t processedFrameCount = 0;
+ ON_CALL(*graph, NotifyInputData)
+ .WillByDefault([&](const AudioDataValue*, size_t aFrames, TrackRate,
+ uint32_t, uint32_t aAlreadyBuffered) {
+ if (!audioStart) {
+ audioStart = Some(graph->StateComputedTime());
+ alreadyBuffered = Some(aAlreadyBuffered);
+ }
+ EXPECT_NEAR(inputFrameCount,
+ static_cast<int64_t>(graph->StateComputedTime() -
+ *audioStart + *alreadyBuffered),
+ WEBAUDIO_BLOCK_SIZE)
+ << "Input should be behind state time, due to the delayed start. "
+ "stateComputedTime="
+ << graph->StateComputedTime() << ", audioStartTime=" << *audioStart
+ << ", alreadyBuffered=" << *alreadyBuffered;
+ inputFrameCount += aFrames;
+ });
+ ON_CALL(*graph, NotifyOutputData)
+ .WillByDefault([&](AudioDataValue*, size_t aFrames, TrackRate aRate,
+ uint32_t) { outputFrameCount += aFrames; });
+
+ driver = MakeRefPtr<AudioCallbackDriver>(graph, nullptr, aRate, 2, 2, nullptr,
+ (void*)1, AudioInputType::Voice);
+ EXPECT_FALSE(driver->ThreadRunning()) << "Verify thread is not running";
+ EXPECT_FALSE(driver->IsStarted()) << "Verify thread is not started";
+
+ graph->SetCurrentDriver(driver);
+ graph->SetEnsureNextIteration(true);
+
+ driver->Start();
+ RefPtr<SmartMockCubebStream> stream = WaitFor(cubeb->StreamInitEvent());
+ cubeb->SetStreamStartFreezeEnabled(false);
+
+ const size_t fallbackIterations = 3;
+ WaitUntil(graph->FramesIteratedEvent(), [&](uint32_t aFrames) {
+ const GraphTime tenMillis = aRate / 100;
+ // An iteration is always rounded upwards to the next full block.
+ const GraphTime tenMillisIteration =
+ MediaTrackGraphImpl::RoundUpToEndOfAudioBlock(tenMillis);
+ // The iteration may be smaller because up to an extra block may have been
+ // processed and buffered.
+ const GraphTime tenMillisMinIteration =
+ tenMillisIteration - WEBAUDIO_BLOCK_SIZE;
+ // An iteration must be at least one audio block.
+ const GraphTime minIteration =
+ std::max<GraphTime>(WEBAUDIO_BLOCK_SIZE, tenMillisMinIteration);
+ EXPECT_GE(aFrames, minIteration)
+ << "Fallback driver iteration >= 10ms, modulo an audio block";
+ EXPECT_LT(aFrames, static_cast<size_t>(aRate))
+ << "Fallback driver iteration <1s (sanity)";
+ return graph->IterationCount() >= fallbackIterations;
+ });
+ stream->Thaw();
+
+ // Wait for at least 100ms of audio data.
+ WaitUntil(stream->FramesProcessedEvent(), [&](uint32_t aFrames) {
+ processedFrameCount += aFrames;
+ return processedFrameCount >= aRate / 10;
+ });
+
+ // This will block untill all events have been executed.
+ MOZ_KnownLive(driver)->Shutdown();
+
+ EXPECT_EQ(inputFrameCount, outputFrameCount);
+ EXPECT_NEAR(graph->StateComputedTime() - *audioStart,
+ inputFrameCount + *alreadyBuffered, WEBAUDIO_BLOCK_SIZE)
+ << "Graph progresses while audio driver runs. stateComputedTime="
+ << graph->StateComputedTime() << ", inputFrameCount=" << inputFrameCount;
+}
+
+TEST(TestAudioCallbackDriver, SlowStart)
+MOZ_CAN_RUN_SCRIPT_FOR_DEFINITION {
+ TestSlowStart(1000); // 10ms = 10 <<< 128 samples
+ TestSlowStart(8000); // 10ms = 80 < 128 samples
+ TestSlowStart(44100); // 10ms = 441 > 128 samples
+}
diff --git a/dom/media/gtest/TestAudioCompactor.cpp b/dom/media/gtest/TestAudioCompactor.cpp
new file mode 100644
index 0000000000..8c37a98ddf
--- /dev/null
+++ b/dom/media/gtest/TestAudioCompactor.cpp
@@ -0,0 +1,131 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "gtest/gtest.h"
+#include "AudioCompactor.h"
+#include "nsDeque.h"
+#include "nsIMemoryReporter.h"
+
+using mozilla::AudioCompactor;
+using mozilla::AudioData;
+using mozilla::AudioDataValue;
+using mozilla::MediaQueue;
+
+class MemoryFunctor : public nsDequeFunctor<AudioData> {
+ public:
+ MemoryFunctor() : mSize(0) {}
+ MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf);
+
+ void operator()(AudioData* aObject) override {
+ mSize += aObject->SizeOfIncludingThis(MallocSizeOf);
+ }
+
+ size_t mSize;
+};
+
+class TestCopy {
+ public:
+ TestCopy(uint32_t aFrames, uint32_t aChannels, uint32_t& aCallCount,
+ uint32_t& aFrameCount)
+ : mFrames(aFrames),
+ mChannels(aChannels),
+ mCallCount(aCallCount),
+ mFrameCount(aFrameCount) {}
+
+ uint32_t operator()(AudioDataValue* aBuffer, uint32_t aSamples) {
+ mCallCount += 1;
+ uint32_t frames = std::min(mFrames - mFrameCount, aSamples / mChannels);
+ mFrameCount += frames;
+ return frames;
+ }
+
+ private:
+ const uint32_t mFrames;
+ const uint32_t mChannels;
+ uint32_t& mCallCount;
+ uint32_t& mFrameCount;
+};
+
+static void TestAudioCompactor(size_t aBytes) {
+ MediaQueue<AudioData> queue;
+ AudioCompactor compactor(queue);
+
+ uint64_t offset = 0;
+ uint64_t time = 0;
+ uint32_t sampleRate = 44000;
+ uint32_t channels = 2;
+ uint32_t frames = aBytes / (channels * sizeof(AudioDataValue));
+ size_t maxSlop = aBytes / AudioCompactor::MAX_SLOP_DIVISOR;
+
+ uint32_t callCount = 0;
+ uint32_t frameCount = 0;
+
+ compactor.Push(offset, time, sampleRate, frames, channels,
+ TestCopy(frames, channels, callCount, frameCount));
+
+ EXPECT_GT(callCount, 0U) << "copy functor never called";
+ EXPECT_EQ(frames, frameCount) << "incorrect number of frames copied";
+
+ MemoryFunctor memoryFunc;
+ queue.LockedForEach(memoryFunc);
+ size_t allocSize = memoryFunc.mSize - (callCount * sizeof(AudioData));
+ size_t slop = allocSize - aBytes;
+ EXPECT_LE(slop, maxSlop) << "allowed too much allocation slop";
+}
+
+TEST(Media, AudioCompactor_4000)
+{ TestAudioCompactor(4000); }
+
+TEST(Media, AudioCompactor_4096)
+{ TestAudioCompactor(4096); }
+
+TEST(Media, AudioCompactor_5000)
+{ TestAudioCompactor(5000); }
+
+TEST(Media, AudioCompactor_5256)
+{ TestAudioCompactor(5256); }
+
+TEST(Media, AudioCompactor_NativeCopy)
+{
+ const uint32_t channels = 2;
+ const size_t srcBytes = 32;
+ const uint32_t srcSamples = srcBytes / sizeof(AudioDataValue);
+ const uint32_t srcFrames = srcSamples / channels;
+ uint8_t src[srcBytes];
+
+ for (uint32_t i = 0; i < srcBytes; ++i) {
+ src[i] = i;
+ }
+
+ AudioCompactor::NativeCopy copy(src, srcBytes, channels);
+
+ const uint32_t dstSamples = srcSamples * 2;
+ AudioDataValue dst[dstSamples];
+
+ const AudioDataValue notCopied = 0xffff;
+ for (uint32_t i = 0; i < dstSamples; ++i) {
+ dst[i] = notCopied;
+ }
+
+ const uint32_t copyCount = 8;
+ uint32_t copiedFrames = 0;
+ uint32_t nextSample = 0;
+ for (uint32_t i = 0; i < copyCount; ++i) {
+ uint32_t copySamples = dstSamples / copyCount;
+ copiedFrames += copy(dst + nextSample, copySamples);
+ nextSample += copySamples;
+ }
+
+ EXPECT_EQ(srcFrames, copiedFrames) << "copy exact number of source frames";
+
+ // Verify that the only the correct bytes were copied.
+ for (uint32_t i = 0; i < dstSamples; ++i) {
+ if (i < srcSamples) {
+ EXPECT_NE(notCopied, dst[i]) << "should have copied over these bytes";
+ } else {
+ EXPECT_EQ(notCopied, dst[i]) << "should not have copied over these bytes";
+ }
+ }
+}
diff --git a/dom/media/gtest/TestAudioDecoderInputTrack.cpp b/dom/media/gtest/TestAudioDecoderInputTrack.cpp
new file mode 100644
index 0000000000..850d23accc
--- /dev/null
+++ b/dom/media/gtest/TestAudioDecoderInputTrack.cpp
@@ -0,0 +1,447 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <utility>
+
+#include "AudioDecoderInputTrack.h"
+#include "gmock/gmock.h"
+#include "GraphDriver.h"
+#include "gtest/gtest.h"
+#include "MediaInfo.h"
+#include "MediaTrackGraphImpl.h"
+#include "nsThreadUtils.h"
+#include "VideoUtils.h"
+#include "WaitFor.h"
+
+using namespace mozilla;
+using namespace mozilla::media;
+using testing::AssertionResult;
+using testing::NiceMock;
+using testing::Return;
+
+constexpr uint32_t kNoFlags = 0;
+constexpr TrackRate kRate = 44100;
+constexpr uint32_t kChannels = 2;
+
+class MockTestGraph : public MediaTrackGraphImpl {
+ public:
+ MockTestGraph(TrackRate aRate, uint32_t aChannels)
+ : MediaTrackGraphImpl(OFFLINE_THREAD_DRIVER, DIRECT_DRIVER, aRate,
+ aChannels, nullptr, NS_GetCurrentThread()) {
+ ON_CALL(*this, OnGraphThread).WillByDefault(Return(true));
+ // We have to call `Destroy()` manually in order to break the reference.
+ // The reason we don't assign a null driver is because we would add a track
+ // to the graph, then it would trigger graph's `EnsureNextIteration()` that
+ // requires a non-null driver.
+ SetCurrentDriver(new NiceMock<MockDriver>());
+ }
+
+ MOCK_CONST_METHOD0(OnGraphThread, bool());
+ MOCK_METHOD1(AppendMessage, void(UniquePtr<ControlMessage>));
+
+ protected:
+ ~MockTestGraph() = default;
+
+ class MockDriver : public GraphDriver {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MockDriver, override);
+
+ MockDriver() : GraphDriver(nullptr, nullptr, 0) {
+ ON_CALL(*this, OnThread).WillByDefault(Return(true));
+ ON_CALL(*this, ThreadRunning).WillByDefault(Return(true));
+ }
+
+ MOCK_METHOD0(Start, void());
+ MOCK_METHOD0(Shutdown, void());
+ MOCK_METHOD0(IterationDuration, uint32_t());
+ MOCK_METHOD0(EnsureNextIteration, void());
+ MOCK_CONST_METHOD0(OnThread, bool());
+ MOCK_CONST_METHOD0(ThreadRunning, bool());
+
+ protected:
+ ~MockDriver() = default;
+ };
+
+ bool mEnableFakeAppend = false;
+};
+
+AudioData* CreateAudioDataFromInfo(uint32_t aFrames, const AudioInfo& aInfo) {
+ AlignedAudioBuffer samples(aFrames * aInfo.mChannels);
+ return new AudioData(0, TimeUnit::Zero(), std::move(samples), aInfo.mChannels,
+ aInfo.mRate);
+}
+
+AudioDecoderInputTrack* CreateTrack(MediaTrackGraph* aGraph,
+ nsISerialEventTarget* aThread,
+ const AudioInfo& aInfo,
+ float aPlaybackRate = 1.0,
+ float aVolume = 1.0,
+ bool aPreservesPitch = true) {
+ return AudioDecoderInputTrack::Create(aGraph, aThread, aInfo, aPlaybackRate,
+ aVolume, aPreservesPitch);
+}
+
+class TestAudioDecoderInputTrack : public testing::Test {
+ protected:
+ void SetUp() override {
+ mGraph = MakeRefPtr<NiceMock<MockTestGraph>>(kRate, kChannels);
+
+ mInfo.mRate = kRate;
+ mInfo.mChannels = kChannels;
+ mTrack = CreateTrack(mGraph, NS_GetCurrentThread(), mInfo);
+ EXPECT_FALSE(mTrack->Ended());
+ }
+
+ void TearDown() override {
+ // This simulates the normal usage where the `Close()` is always be called
+ // before the `Destroy()`.
+ mTrack->Close();
+ mTrack->Destroy();
+ // Remove the reference of the track from the mock graph, and then release
+ // the self-reference of mock graph.
+ mGraph->RemoveTrackGraphThread(mTrack);
+ mGraph->Destroy();
+ }
+
+ AudioData* CreateAudioData(uint32_t aFrames) {
+ return CreateAudioDataFromInfo(aFrames, mInfo);
+ }
+
+ AudioSegment* GetTrackSegment() { return mTrack->GetData<AudioSegment>(); }
+
+ AssertionResult ExpectSegmentNonSilence(const char* aStartExpr,
+ const char* aEndExpr,
+ TrackTime aStart, TrackTime aEnd) {
+ AudioSegment checkedRange;
+ checkedRange.AppendSlice(*mTrack->GetData(), aStart, aEnd);
+ if (!checkedRange.IsNull()) {
+ return testing::AssertionSuccess();
+ }
+ return testing::AssertionFailure()
+ << "segment [" << aStart << ":" << aEnd << "] should be non-silence";
+ }
+
+ AssertionResult ExpectSegmentSilence(const char* aStartExpr,
+ const char* aEndExpr, TrackTime aStart,
+ TrackTime aEnd) {
+ AudioSegment checkedRange;
+ checkedRange.AppendSlice(*mTrack->GetData(), aStart, aEnd);
+ if (checkedRange.IsNull()) {
+ return testing::AssertionSuccess();
+ }
+ return testing::AssertionFailure()
+ << "segment [" << aStart << ":" << aEnd << "] should be silence";
+ }
+
+ RefPtr<MockTestGraph> mGraph;
+ RefPtr<AudioDecoderInputTrack> mTrack;
+ AudioInfo mInfo;
+};
+
+TEST_F(TestAudioDecoderInputTrack, BasicAppendData) {
+ // Start from [0:10] and each time we move the time by 10ms.
+ // Expected: outputDuration=10, outputFrames=0, outputSilence=10
+ TrackTime start = 0;
+ TrackTime end = 10;
+ mTrack->ProcessInput(start, end, kNoFlags);
+ EXPECT_EQ(mTrack->GetEnd(), end);
+ EXPECT_PRED_FORMAT2(ExpectSegmentSilence, start, end);
+
+ // Expected: outputDuration=20, outputFrames=5, outputSilence=15
+ RefPtr<AudioData> audio1 = CreateAudioData(5);
+ mTrack->AppendData(audio1, nullptr);
+ start = end;
+ end += 10;
+ mTrack->ProcessInput(start, end, kNoFlags);
+ EXPECT_EQ(mTrack->GetEnd(), end);
+ EXPECT_PRED_FORMAT2(ExpectSegmentNonSilence, start, start + audio1->Frames());
+ EXPECT_PRED_FORMAT2(ExpectSegmentSilence, start + audio1->Frames(), end);
+
+ // Expected: outputDuration=30, outputFrames=15, outputSilence=15
+ RefPtr<AudioData> audio2 = CreateAudioData(10);
+ mTrack->AppendData(audio2, nullptr);
+ start = end;
+ end += 10;
+ mTrack->ProcessInput(start, end, kNoFlags);
+ EXPECT_PRED_FORMAT2(ExpectSegmentNonSilence, start, end);
+ EXPECT_EQ(mTrack->GetEnd(), end);
+
+ // Expected : sent all data, track should be ended in the next iteration and
+ // fill slience in this iteration.
+ mTrack->NotifyEndOfStream();
+ start = end;
+ end += 10;
+ mTrack->ProcessInput(start, end, ProcessedMediaTrack::ALLOW_END);
+ EXPECT_PRED_FORMAT2(ExpectSegmentSilence, start, end);
+ EXPECT_EQ(mTrack->GetEnd(), end);
+ EXPECT_FALSE(mTrack->Ended());
+
+ // Expected : track ended
+ start = end;
+ end += 10;
+ mTrack->ProcessInput(start, end, ProcessedMediaTrack::ALLOW_END);
+ EXPECT_EQ(mTrack->WrittenFrames(), audio1->Frames() + audio2->Frames());
+}
+
+TEST_F(TestAudioDecoderInputTrack, ClearFuture) {
+ // Start from [0:10] and each time we move the time by 10ms.
+ // Expected: appended=30, expected duration=10
+ RefPtr<AudioData> audio1 = CreateAudioData(30);
+ mTrack->AppendData(audio1, nullptr);
+ TrackTime start = 0;
+ TrackTime end = 10;
+ mTrack->ProcessInput(start, end, kNoFlags);
+ EXPECT_PRED_FORMAT2(ExpectSegmentNonSilence, start, end);
+
+ // In next iteration [10:20], we would consume the remaining data that was
+ // appended in the previous iteration.
+ start = end;
+ end += 10;
+ mTrack->ProcessInput(start, end, kNoFlags);
+ EXPECT_PRED_FORMAT2(ExpectSegmentNonSilence, start, end);
+
+ // Clear future data which is the remaining 10 frames so the track would
+ // only output silence.
+ mTrack->ClearFutureData();
+ start = end;
+ end += 10;
+ mTrack->ProcessInput(start, end, kNoFlags);
+ EXPECT_PRED_FORMAT2(ExpectSegmentSilence, start, end);
+
+ // Test appending data again, to see if we can append data correctly after
+ // calling `ClearFutureData()`.
+ RefPtr<AudioData> audio2 = CreateAudioData(10);
+ mTrack->AppendData(audio2, nullptr);
+ start = end;
+ end += 10;
+ mTrack->ProcessInput(start, end, kNoFlags);
+ EXPECT_PRED_FORMAT2(ExpectSegmentNonSilence, start, end);
+
+ // Run another iteration that should only contains silence because the data
+ // we appended only enough for one iteration.
+ start = end;
+ end += 10;
+ mTrack->ProcessInput(start, end, kNoFlags);
+ EXPECT_PRED_FORMAT2(ExpectSegmentSilence, start, end);
+
+ // Clear future data would also remove the EOS.
+ mTrack->NotifyEndOfStream();
+ mTrack->ClearFutureData();
+ start = end;
+ end += 10;
+ mTrack->ProcessInput(start, end, ProcessedMediaTrack::ALLOW_END);
+ EXPECT_PRED_FORMAT2(ExpectSegmentSilence, start, end);
+ EXPECT_FALSE(mTrack->Ended());
+
+ // As EOS has been removed, in next iteration the track would still be
+ // running.
+ start = end;
+ end += 10;
+ mTrack->ProcessInput(start, end, ProcessedMediaTrack::ALLOW_END);
+ EXPECT_PRED_FORMAT2(ExpectSegmentSilence, start, end);
+ EXPECT_FALSE(mTrack->Ended());
+ EXPECT_EQ(mTrack->WrittenFrames(),
+ (audio1->Frames() - 10 /* got clear */) + audio2->Frames());
+}
+
+TEST_F(TestAudioDecoderInputTrack, InputRateChange) {
+ // Start from [0:10] and each time we move the time by 10ms.
+ // Expected: appended=10, expected duration=10
+ RefPtr<AudioData> audio1 = CreateAudioData(10);
+ mTrack->AppendData(audio1, nullptr);
+ TrackTime start = 0;
+ TrackTime end = 10;
+ mTrack->ProcessInput(start, end, kNoFlags);
+ EXPECT_PRED_FORMAT2(ExpectSegmentNonSilence, start, end);
+
+ // Change input sample rate to the half, input data should be resampled and
+ // its duration would become longer.
+ // Expected: appended=10 + 5,
+ // expected duration=10 + 5*2 (resampled)
+ mInfo.mRate = kRate / 2;
+ RefPtr<AudioData> audioHalfSampleRate = CreateAudioData(5);
+ mTrack->AppendData(audioHalfSampleRate, nullptr);
+ start = end;
+ end += 10;
+ mTrack->ProcessInput(start, end, kNoFlags);
+ EXPECT_PRED_FORMAT2(ExpectSegmentNonSilence, start, end);
+
+ // Change input sample rate to the double, input data should be resampled and
+ // its duration would become shorter.
+ // Expected: appended=10 + 10 + 10,
+ // expected duration=10 + 10 + 10/2(resampled) + 5(silence)
+ mInfo.mRate = kRate * 2;
+ RefPtr<AudioData> audioDoubleSampleRate = CreateAudioData(10);
+ TrackTime expectedDuration = audioDoubleSampleRate->Frames() / 2;
+ mTrack->AppendData(audioDoubleSampleRate, nullptr);
+ start = end;
+ end += 10;
+ mTrack->ProcessInput(start, end, kNoFlags);
+ EXPECT_PRED_FORMAT2(ExpectSegmentNonSilence, start, start + expectedDuration);
+ EXPECT_PRED_FORMAT2(ExpectSegmentSilence, start + expectedDuration, end);
+ EXPECT_EQ(mTrack->WrittenFrames(), audio1->Frames() +
+ audioHalfSampleRate->Frames() * 2 +
+ audioDoubleSampleRate->Frames() / 2);
+}
+
+TEST_F(TestAudioDecoderInputTrack, ChannelChange) {
+ // Start from [0:10] and each time we move the time by 10ms.
+ // Track was initialized in stero.
+ EXPECT_EQ(mTrack->NumberOfChannels(), uint32_t(2));
+
+ // But first audio data is mono, so the `NumberOfChannels()` changes to
+ // reflect the maximum channel in the audio segment.
+ mInfo.mChannels = 1;
+ RefPtr<AudioData> audioMono = CreateAudioData(10);
+ mTrack->AppendData(audioMono, nullptr);
+ TrackTime start = 0;
+ TrackTime end = 10;
+ mTrack->ProcessInput(start, end, kNoFlags);
+ EXPECT_PRED_FORMAT2(ExpectSegmentNonSilence, start, end);
+ EXPECT_EQ(mTrack->NumberOfChannels(), audioMono->mChannels);
+
+ // Then append audio data with 5 channels.
+ mInfo.mChannels = 5;
+ RefPtr<AudioData> audioWithFiveChannels = CreateAudioData(10);
+ mTrack->AppendData(audioWithFiveChannels, nullptr);
+ start = end;
+ end += 10;
+ mTrack->ProcessInput(start, end, kNoFlags);
+ EXPECT_PRED_FORMAT2(ExpectSegmentNonSilence, start, end);
+ EXPECT_EQ(mTrack->NumberOfChannels(), audioWithFiveChannels->mChannels);
+ EXPECT_EQ(mTrack->WrittenFrames(),
+ audioMono->Frames() + audioWithFiveChannels->Frames());
+}
+
+TEST_F(TestAudioDecoderInputTrack, VolumeChange) {
+ // In order to run the volume change directly without using a real graph.
+ // one for setting the track's volume, another for the track destruction.
+ EXPECT_CALL(*mGraph, AppendMessage)
+ .Times(2)
+ .WillOnce([](UniquePtr<ControlMessage> aMessage) { aMessage->Run(); })
+ .WillOnce([](UniquePtr<ControlMessage> aMessage) {});
+
+ // The default volume is 1.0.
+ float expectedVolume = 1.0;
+ RefPtr<AudioData> audio = CreateAudioData(20);
+ TrackTime start = 0;
+ TrackTime end = 10;
+ mTrack->AppendData(audio, nullptr);
+ mTrack->ProcessInput(start, end, kNoFlags);
+ EXPECT_PRED_FORMAT2(ExpectSegmentNonSilence, start, end);
+ EXPECT_TRUE(GetTrackSegment()->GetLastChunk()->mVolume == expectedVolume);
+
+ // After setting volume on the track, the data in the output chunk should be
+ // changed as well.
+ expectedVolume = 0.1;
+ mTrack->SetVolume(expectedVolume);
+ SpinEventLoopUntil<ProcessFailureBehavior::IgnoreAndContinue>(
+ "TEST_F(TestAudioDecoderInputTrack, VolumeChange)"_ns,
+ [&] { return mTrack->Volume() == expectedVolume; });
+ start = end;
+ end += 10;
+ mTrack->ProcessInput(start, end, kNoFlags);
+ EXPECT_PRED_FORMAT2(ExpectSegmentNonSilence, start, end);
+ EXPECT_TRUE(GetTrackSegment()->GetLastChunk()->mVolume == expectedVolume);
+}
+
+TEST_F(TestAudioDecoderInputTrack, BatchedData) {
+ uint32_t appendedFrames = 0;
+ RefPtr<AudioData> audio = CreateAudioData(10);
+ for (size_t idx = 0; idx < 50; idx++) {
+ mTrack->AppendData(audio, nullptr);
+ appendedFrames += audio->Frames();
+ }
+
+ // First we need to call `ProcessInput` at least once to drain the track's
+ // SPSC queue, otherwise we're not able to push the batched data later.
+ TrackTime start = 0;
+ TrackTime end = 10;
+ uint32_t expectedFrames = end - start;
+ mTrack->ProcessInput(start, end, kNoFlags);
+ EXPECT_PRED_FORMAT2(ExpectSegmentNonSilence, start, end);
+
+ // The batched data would be pushed to the graph thread in around 10ms after
+ // the track first time started to batch data, which we can't control here.
+ // Therefore, we need to wait until the batched data gets cleared.
+ SpinEventLoopUntil<ProcessFailureBehavior::IgnoreAndContinue>(
+ "TEST_F(TestAudioDecoderInputTrack, BatchedData)"_ns,
+ [&] { return !mTrack->HasBatchedData(); });
+
+ // Check that we received all the remainging data previously appended.
+ start = end;
+ end = start + (appendedFrames - expectedFrames);
+ mTrack->ProcessInput(start, end, kNoFlags);
+ EXPECT_PRED_FORMAT2(ExpectSegmentNonSilence, start, end);
+
+ // Check that we received no more data than previously appended.
+ start = end;
+ end += 10;
+ mTrack->ProcessInput(start, end, kNoFlags);
+ EXPECT_PRED_FORMAT2(ExpectSegmentSilence, start, end);
+ EXPECT_EQ(mTrack->WrittenFrames(), appendedFrames);
+}
+
+TEST_F(TestAudioDecoderInputTrack, OutputAndEndEvent) {
+ // Append an audio and EOS, the output event should notify the amount of
+ // frames that is equal to the amount of audio we appended.
+ RefPtr<AudioData> audio = CreateAudioData(10);
+ MozPromiseHolder<GenericPromise> holder;
+ RefPtr<GenericPromise> p = holder.Ensure(__func__);
+ MediaEventListener outputListener =
+ mTrack->OnOutput().Connect(NS_GetCurrentThread(), [&](TrackTime aFrame) {
+ EXPECT_EQ(aFrame, audio->Frames());
+ holder.Resolve(true, __func__);
+ });
+ mTrack->AppendData(audio, nullptr);
+ mTrack->NotifyEndOfStream();
+ TrackTime start = 0;
+ TrackTime end = 10;
+ mTrack->ProcessInput(start, end, ProcessedMediaTrack::ALLOW_END);
+ Unused << WaitFor(p);
+
+ // Track should end in this iteration, so the end event should be notified.
+ p = holder.Ensure(__func__);
+ MediaEventListener endListener = mTrack->OnEnd().Connect(
+ NS_GetCurrentThread(), [&]() { holder.Resolve(true, __func__); });
+ start = end;
+ end += 10;
+ mTrack->ProcessInput(start, end, ProcessedMediaTrack::ALLOW_END);
+ Unused << WaitFor(p);
+ outputListener.Disconnect();
+ endListener.Disconnect();
+}
+
+TEST_F(TestAudioDecoderInputTrack, PlaybackRateChange) {
+ // In order to run the playback change directly without using a real graph.
+ // one for setting the track's playback, another for the track destruction.
+ EXPECT_CALL(*mGraph, AppendMessage)
+ .Times(2)
+ .WillOnce([](UniquePtr<ControlMessage> aMessage) { aMessage->Run(); })
+ .WillOnce([](UniquePtr<ControlMessage> aMessage) {});
+
+ // Changing the playback rate.
+ float expectedPlaybackRate = 2.0;
+ mTrack->SetPlaybackRate(expectedPlaybackRate);
+ SpinEventLoopUntil<ProcessFailureBehavior::IgnoreAndContinue>(
+ "TEST_F(TestAudioDecoderInputTrack, PlaybackRateChange)"_ns,
+ [&] { return mTrack->PlaybackRate() == expectedPlaybackRate; });
+
+ // Time stretcher in the track would usually need certain amount of data
+ // before it outputs the time-stretched result. As we're in testing, we would
+ // only append data once, so signal an EOS after appending data, in order to
+ // ask the track to flush all samples from the time strecther.
+ RefPtr<AudioData> audio = CreateAudioData(100);
+ mTrack->AppendData(audio, nullptr);
+ mTrack->NotifyEndOfStream();
+
+ // Playback rate is 2x, so we should only get 1/2x sample frames, another 1/2
+ // should be silence.
+ TrackTime start = 0;
+ TrackTime end = audio->Frames();
+ mTrack->ProcessInput(start, end, kNoFlags);
+ EXPECT_PRED_FORMAT2(ExpectSegmentNonSilence, start, audio->Frames() / 2);
+ EXPECT_PRED_FORMAT2(ExpectSegmentSilence, start + audio->Frames() / 2, end);
+}
diff --git a/dom/media/gtest/TestAudioDeviceEnumerator.cpp b/dom/media/gtest/TestAudioDeviceEnumerator.cpp
new file mode 100644
index 0000000000..7e0ffcc4af
--- /dev/null
+++ b/dom/media/gtest/TestAudioDeviceEnumerator.cpp
@@ -0,0 +1,271 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#define ENABLE_SET_CUBEB_BACKEND 1
+#include "CubebDeviceEnumerator.h"
+#include "gtest/gtest-printers.h"
+#include "gtest/gtest.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/media/MediaUtils.h"
+#include "nsTArray.h"
+
+#include "MockCubeb.h"
+
+using namespace mozilla;
+using AudioDeviceSet = CubebDeviceEnumerator::AudioDeviceSet;
+
+const bool DEBUG_PRINTS = false;
+
+enum DeviceOperation { ADD, REMOVE };
+
+void TestEnumeration(MockCubeb* aMock, uint32_t aExpectedDeviceCount,
+ DeviceOperation aOperation, cubeb_device_type aType) {
+ RefPtr<CubebDeviceEnumerator> enumerator =
+ CubebDeviceEnumerator::GetInstance();
+
+ RefPtr<const AudioDeviceSet> devices;
+
+ if (aType == CUBEB_DEVICE_TYPE_INPUT) {
+ devices = enumerator->EnumerateAudioInputDevices();
+ }
+
+ if (aType == CUBEB_DEVICE_TYPE_OUTPUT) {
+ devices = enumerator->EnumerateAudioOutputDevices();
+ }
+
+ EXPECT_EQ(devices->Length(), aExpectedDeviceCount)
+ << "Device count is correct when enumerating";
+
+ if (DEBUG_PRINTS) {
+ for (const auto& deviceInfo : *devices) {
+ printf("=== Before removal\n");
+ PrintDevice(deviceInfo);
+ }
+ }
+
+ if (aOperation == DeviceOperation::REMOVE) {
+ aMock->RemoveDevice(reinterpret_cast<cubeb_devid>(1));
+ } else {
+ aMock->AddDevice(DeviceTemplate(reinterpret_cast<cubeb_devid>(123), aType));
+ }
+
+ if (aType == CUBEB_DEVICE_TYPE_INPUT) {
+ devices = enumerator->EnumerateAudioInputDevices();
+ }
+
+ if (aType == CUBEB_DEVICE_TYPE_OUTPUT) {
+ devices = enumerator->EnumerateAudioOutputDevices();
+ }
+
+ uint32_t newExpectedDeviceCount = aOperation == DeviceOperation::REMOVE
+ ? aExpectedDeviceCount - 1
+ : aExpectedDeviceCount + 1;
+
+ EXPECT_EQ(devices->Length(), newExpectedDeviceCount)
+ << "Device count is correct when enumerating after operation";
+
+ if (DEBUG_PRINTS) {
+ for (const auto& deviceInfo : *devices) {
+ printf("=== After removal\n");
+ PrintDevice(deviceInfo);
+ }
+ }
+}
+
+#ifndef ANDROID
+TEST(CubebDeviceEnumerator, EnumerateSimple)
+{
+ // It looks like we're leaking this object, but in fact it will be freed by
+ // gecko sometime later: `cubeb_destroy` is called when layout statics are
+ // shutdown and we cast back to a MockCubeb* and call the dtor.
+ MockCubeb* mock = new MockCubeb();
+ mozilla::CubebUtils::ForceSetCubebContext(mock->AsCubebContext());
+
+ // We want to test whether CubebDeviceEnumerator works with and without a
+ // backend that can notify of a device collection change via callback.
+ // Additionally, we're testing that both adding and removing a device
+ // invalidates the list correctly.
+ bool supportsDeviceChangeCallback[2] = {true, false};
+ DeviceOperation operations[2] = {DeviceOperation::ADD,
+ DeviceOperation::REMOVE};
+
+ for (bool supports : supportsDeviceChangeCallback) {
+ // Shutdown for `supports` to take effect
+ CubebDeviceEnumerator::Shutdown();
+ mock->SetSupportDeviceChangeCallback(supports);
+ for (DeviceOperation op : operations) {
+ uint32_t device_count = 4;
+
+ cubeb_device_type deviceType = CUBEB_DEVICE_TYPE_INPUT;
+ AddDevices(mock, device_count, deviceType);
+ TestEnumeration(mock, device_count, op, deviceType);
+
+ deviceType = CUBEB_DEVICE_TYPE_OUTPUT;
+ AddDevices(mock, device_count, deviceType);
+ TestEnumeration(mock, device_count, op, deviceType);
+ }
+ }
+ // Shutdown to clean up the last `supports` effect
+ CubebDeviceEnumerator::Shutdown();
+}
+
+TEST(CubebDeviceEnumerator, ZeroChannelDevices)
+{
+ MockCubeb* mock = new MockCubeb();
+ mozilla::CubebUtils::ForceSetCubebContext(mock->AsCubebContext());
+
+ // Create devices with different channel count, including 0-channel
+
+ cubeb_device_info dev1 = DeviceTemplate(reinterpret_cast<cubeb_devid>(1),
+ CUBEB_DEVICE_TYPE_INPUT, "dev 1");
+ dev1.max_channels = 1;
+ mock->AddDevice(dev1);
+
+ cubeb_device_info dev2 = DeviceTemplate(reinterpret_cast<cubeb_devid>(2),
+ CUBEB_DEVICE_TYPE_INPUT, "dev 2");
+ dev2.max_channels = 0;
+ mock->AddDevice(dev2);
+
+ cubeb_device_info dev3 = DeviceTemplate(reinterpret_cast<cubeb_devid>(3),
+ CUBEB_DEVICE_TYPE_OUTPUT, "dev 3");
+ dev3.max_channels = 2;
+ mock->AddDevice(dev3);
+
+ cubeb_device_info dev4 = DeviceTemplate(reinterpret_cast<cubeb_devid>(4),
+ CUBEB_DEVICE_TYPE_OUTPUT, "dev 4");
+ dev4.max_channels = 0;
+ mock->AddDevice(dev4);
+
+ // Make sure the devices are added to cubeb.
+
+ cubeb_device_collection inputCollection = {nullptr, 0};
+ mock->EnumerateDevices(CUBEB_DEVICE_TYPE_INPUT, &inputCollection);
+ EXPECT_EQ(inputCollection.count, 2U);
+ EXPECT_EQ(inputCollection.device[0].devid, dev1.devid);
+ EXPECT_EQ(inputCollection.device[1].devid, dev2.devid);
+ mock->DestroyDeviceCollection(&inputCollection);
+ EXPECT_EQ(inputCollection.count, 0U);
+
+ cubeb_device_collection outputCollection = {nullptr, 0};
+ mock->EnumerateDevices(CUBEB_DEVICE_TYPE_OUTPUT, &outputCollection);
+ EXPECT_EQ(outputCollection.count, 2U);
+ EXPECT_EQ(outputCollection.device[0].devid, dev3.devid);
+ EXPECT_EQ(outputCollection.device[1].devid, dev4.devid);
+ mock->DestroyDeviceCollection(&outputCollection);
+ EXPECT_EQ(outputCollection.count, 0U);
+
+ // Enumerate the devices. The result should exclude the 0-channel devices.
+
+ RefPtr<CubebDeviceEnumerator> enumerator =
+ CubebDeviceEnumerator::GetInstance();
+
+ RefPtr<const AudioDeviceSet> inputDevices =
+ enumerator->EnumerateAudioInputDevices();
+ EXPECT_EQ(inputDevices->Length(), 1U);
+ EXPECT_EQ(inputDevices->ElementAt(0)->DeviceID(), dev1.devid);
+ EXPECT_EQ(inputDevices->ElementAt(0)->MaxChannels(), dev1.max_channels);
+
+ RefPtr<const AudioDeviceSet> outputDevices =
+ enumerator->EnumerateAudioOutputDevices();
+ EXPECT_EQ(outputDevices->Length(), 1U);
+ EXPECT_EQ(outputDevices->ElementAt(0)->DeviceID(), dev3.devid);
+ EXPECT_EQ(outputDevices->ElementAt(0)->MaxChannels(), dev3.max_channels);
+}
+
+#else // building for Android, which has no device enumeration support
+TEST(CubebDeviceEnumerator, EnumerateAndroid)
+{
+ MockCubeb* mock = new MockCubeb();
+ mozilla::CubebUtils::ForceSetCubebContext(mock->AsCubebContext());
+
+ RefPtr<CubebDeviceEnumerator> enumerator =
+ CubebDeviceEnumerator::GetInstance();
+
+ RefPtr<const AudioDeviceSet> inputDevices =
+ enumerator->EnumerateAudioInputDevices();
+ EXPECT_EQ(inputDevices->Length(), 1u)
+ << "Android always exposes a single input device.";
+ EXPECT_EQ((*inputDevices)[0]->MaxChannels(), 1u) << "With a single channel.";
+ EXPECT_EQ((*inputDevices)[0]->DeviceID(), nullptr)
+ << "It's always the default input device.";
+ EXPECT_TRUE((*inputDevices)[0]->Preferred())
+ << "it's always the prefered input device.";
+
+ RefPtr<const AudioDeviceSet> outputDevices =
+ enumerator->EnumerateAudioOutputDevices();
+ EXPECT_EQ(outputDevices->Length(), 1u)
+ << "Android always exposes a single output device.";
+ EXPECT_EQ((*outputDevices)[0]->MaxChannels(), 2u) << "With stereo channels.";
+ EXPECT_EQ((*outputDevices)[0]->DeviceID(), nullptr)
+ << "It's always the default output device.";
+ EXPECT_TRUE((*outputDevices)[0]->Preferred())
+ << "it's always the prefered output device.";
+}
+#endif
+
+TEST(CubebDeviceEnumerator, ForceNullCubebContext)
+{
+ mozilla::CubebUtils::ForceSetCubebContext(nullptr);
+ RefPtr<CubebDeviceEnumerator> enumerator =
+ CubebDeviceEnumerator::GetInstance();
+
+ RefPtr inputDevices = enumerator->EnumerateAudioInputDevices();
+ EXPECT_EQ(inputDevices->Length(), 0u)
+ << "Enumeration must fail, input device list must be empty.";
+
+ RefPtr outputDevices = enumerator->EnumerateAudioOutputDevices();
+ EXPECT_EQ(outputDevices->Length(), 0u)
+ << "Enumeration must fail, output device list must be empty.";
+
+ // Shutdown to clean up the null context effect
+ CubebDeviceEnumerator::Shutdown();
+}
+
+TEST(CubebDeviceEnumerator, DeviceInfoFromName)
+{
+ MockCubeb* mock = new MockCubeb();
+ mozilla::CubebUtils::ForceSetCubebContext(mock->AsCubebContext());
+
+ cubeb_device_type deviceTypes[2] = {CUBEB_DEVICE_TYPE_INPUT,
+ CUBEB_DEVICE_TYPE_OUTPUT};
+
+ bool supportsDeviceChangeCallback[2] = {true, false};
+ for (bool supports : supportsDeviceChangeCallback) {
+ // Shutdown for `supports` to take effect
+ CubebDeviceEnumerator::Shutdown();
+ mock->SetSupportDeviceChangeCallback(supports);
+ for (cubeb_device_type& deviceType : deviceTypes) {
+ cubeb_devid id_1 = reinterpret_cast<cubeb_devid>(1);
+ mock->AddDevice(DeviceTemplate(id_1, deviceType, "device name 1"));
+ cubeb_devid id_2 = reinterpret_cast<cubeb_devid>(2);
+ nsCString device_name = "device name 2"_ns;
+ mock->AddDevice(DeviceTemplate(id_2, deviceType, device_name.get()));
+ cubeb_devid id_3 = reinterpret_cast<cubeb_devid>(3);
+ mock->AddDevice(DeviceTemplate(id_3, deviceType, "device name 3"));
+
+ RefPtr<CubebDeviceEnumerator> enumerator =
+ CubebDeviceEnumerator::GetInstance();
+
+ EnumeratorSide side = (deviceType == CUBEB_DEVICE_TYPE_INPUT)
+ ? EnumeratorSide::INPUT
+ : EnumeratorSide::OUTPUT;
+ RefPtr<AudioDeviceInfo> devInfo = enumerator->DeviceInfoFromName(
+ NS_ConvertUTF8toUTF16(device_name), side);
+ EXPECT_TRUE(devInfo) << "the device exist";
+ EXPECT_EQ(devInfo->Name(), NS_ConvertUTF8toUTF16(device_name))
+ << "verify the device";
+
+ mock->RemoveDevice(id_2);
+
+ devInfo = enumerator->DeviceInfoFromName(
+ NS_ConvertUTF8toUTF16(device_name), side);
+ EXPECT_FALSE(devInfo) << "the device does not exist any more";
+ }
+ }
+ // Shutdown for `supports` to take effect
+ CubebDeviceEnumerator::Shutdown();
+}
+#undef ENABLE_SET_CUBEB_BACKEND
diff --git a/dom/media/gtest/TestAudioDriftCorrection.cpp b/dom/media/gtest/TestAudioDriftCorrection.cpp
new file mode 100644
index 0000000000..e7ae95b658
--- /dev/null
+++ b/dom/media/gtest/TestAudioDriftCorrection.cpp
@@ -0,0 +1,436 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioDriftCorrection.h"
+#include "AudioGenerator.h"
+#include "AudioVerifier.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "nsContentUtils.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest-printers.h"
+#include "gtest/gtest.h"
+
+using namespace mozilla;
+
+// Runs UpdateClock() and checks that the reported correction level doesn't
+// change for enough time to trigger a correction update on the first
+// following UpdateClock(). Returns the first reported correction level.
+static float RunUntilCorrectionUpdate(ClockDrift& aC, uint32_t aSource,
+ uint32_t aTarget, uint32_t aBuffering,
+ uint32_t aSaturation,
+ uint32_t aSourceOffset = 0,
+ uint32_t aTargetOffset = 0) {
+ Maybe<float> correction;
+ for (uint32_t s = aSourceOffset, t = aTargetOffset;
+ s < aC.mSourceRate && t < aC.mTargetRate; s += aSource, t += aTarget) {
+ aC.UpdateClock(aSource, aTarget, aBuffering, aSaturation);
+ if (correction) {
+ EXPECT_FLOAT_EQ(aC.GetCorrection(), *correction)
+ << "s=" << s << "; t=" << t;
+ } else {
+ correction = Some(aC.GetCorrection());
+ }
+ }
+ return *correction;
+};
+
+TEST(TestClockDrift, Basic)
+{
+ // Keep buffered frames to the wanted level in order to not affect that test.
+ const uint32_t buffered = 5 * 480;
+
+ ClockDrift c(48000, 48000, buffered);
+ EXPECT_EQ(c.GetCorrection(), 1.0);
+
+ EXPECT_FLOAT_EQ(RunUntilCorrectionUpdate(c, 480, 480, buffered, buffered),
+ 1.0);
+ EXPECT_FLOAT_EQ(
+ RunUntilCorrectionUpdate(c, 480, 480 + 48, buffered, buffered), 1.0);
+ EXPECT_FLOAT_EQ(RunUntilCorrectionUpdate(c, 480, 480, buffered, buffered),
+ 1.06);
+ EXPECT_FLOAT_EQ(
+ RunUntilCorrectionUpdate(c, 480 + 48, 480, buffered, buffered), 1.024);
+
+ c.UpdateClock(0, 0, 5 * 480, 5 * 480);
+ EXPECT_FLOAT_EQ(c.GetCorrection(), 0.95505452);
+}
+
+TEST(TestClockDrift, BasicResampler)
+{
+ // Keep buffered frames to the wanted level in order to not affect that test.
+ const uint32_t buffered = 5 * 240;
+
+ ClockDrift c(24000, 48000, buffered);
+
+ // Keep buffered frames to the wanted level in order to not affect that test.
+ EXPECT_FLOAT_EQ(RunUntilCorrectionUpdate(c, 240, 480, buffered, buffered),
+ 1.0);
+
+ // +10%
+ EXPECT_FLOAT_EQ(
+ RunUntilCorrectionUpdate(c, 240, 480 + 48, buffered, buffered), 1.0);
+
+ // +10%
+ EXPECT_FLOAT_EQ(
+ RunUntilCorrectionUpdate(c, 240 + 24, 480, buffered, buffered), 1.06);
+
+ // -10%
+ EXPECT_FLOAT_EQ(
+ RunUntilCorrectionUpdate(c, 240, 480 - 48, buffered, buffered),
+ 0.96945453);
+
+ // +5%, -5%
+ EXPECT_FLOAT_EQ(
+ RunUntilCorrectionUpdate(c, 240 + 12, 480 - 24, buffered, buffered),
+ 0.92778182);
+
+ c.UpdateClock(0, 0, buffered, buffered);
+ EXPECT_FLOAT_EQ(c.GetCorrection(), 0.91396987);
+}
+
+TEST(TestClockDrift, BufferedInput)
+{
+ ClockDrift c(48000, 48000, 5 * 480);
+ EXPECT_EQ(c.GetCorrection(), 1.0);
+
+ EXPECT_FLOAT_EQ(RunUntilCorrectionUpdate(c, 480, 480, 5 * 480, 8 * 480), 1.0);
+
+ c.UpdateClock(480, 480, 0, 10 * 480); // 0 buffered when updating correction
+ EXPECT_FLOAT_EQ(c.GetCorrection(), 1.0473685);
+ EXPECT_FLOAT_EQ(
+ RunUntilCorrectionUpdate(c, 480, 480, 3 * 480, 7 * 480, 480, 480),
+ 1.0473685);
+
+ c.UpdateClock(480, 480, 3 * 480, 7 * 480);
+ EXPECT_FLOAT_EQ(c.GetCorrection(), 1.0311923);
+ EXPECT_FLOAT_EQ(
+ RunUntilCorrectionUpdate(c, 480, 480, 5 * 480, 5 * 480, 480, 480),
+ 1.0311923);
+
+ c.UpdateClock(480, 480, 5 * 480, 5 * 480);
+ EXPECT_FLOAT_EQ(c.GetCorrection(), 1.0124769);
+ EXPECT_FLOAT_EQ(
+ RunUntilCorrectionUpdate(c, 480, 480, 7 * 480, 3 * 480, 480, 480),
+ 1.0124769);
+
+ c.UpdateClock(480, 480, 7 * 480, 3 * 480);
+ EXPECT_FLOAT_EQ(c.GetCorrection(), 0.99322605);
+}
+
+TEST(TestClockDrift, BufferedInputWithResampling)
+{
+ ClockDrift c(24000, 48000, 5 * 240);
+ EXPECT_EQ(c.GetCorrection(), 1.0);
+
+ EXPECT_FLOAT_EQ(RunUntilCorrectionUpdate(c, 240, 480, 5 * 240, 5 * 240), 1.0);
+
+ c.UpdateClock(240, 480, 0, 10 * 240); // 0 buffered when updating correction
+ EXPECT_FLOAT_EQ(c.GetCorrection(), 1.0473685);
+ EXPECT_FLOAT_EQ(
+ RunUntilCorrectionUpdate(c, 240, 480, 3 * 240, 7 * 240, 240, 480),
+ 1.0473685);
+
+ c.UpdateClock(240, 480, 3 * 240, 7 * 240);
+ EXPECT_FLOAT_EQ(c.GetCorrection(), 1.0311923);
+ EXPECT_FLOAT_EQ(
+ RunUntilCorrectionUpdate(c, 240, 480, 5 * 240, 5 * 240, 240, 480),
+ 1.0311923);
+
+ c.UpdateClock(240, 480, 5 * 240, 5 * 240);
+ EXPECT_FLOAT_EQ(c.GetCorrection(), 1.0124769);
+ EXPECT_FLOAT_EQ(
+ RunUntilCorrectionUpdate(c, 240, 480, 7 * 240, 3 * 240, 240, 480),
+ 1.0124769);
+
+ c.UpdateClock(240, 480, 7 * 240, 3 * 240);
+ EXPECT_FLOAT_EQ(c.GetCorrection(), 0.99322605);
+}
+
+TEST(TestClockDrift, Clamp)
+{
+ // Keep buffered frames to the wanted level in order to not affect that test.
+ const uint32_t buffered = 5 * 480;
+
+ ClockDrift c(48000, 48000, buffered);
+
+ // +30%
+ EXPECT_FLOAT_EQ(
+ RunUntilCorrectionUpdate(c, 480, 480 + 3 * 48, buffered, buffered), 1.0);
+
+ // -30%
+ EXPECT_FLOAT_EQ(
+ RunUntilCorrectionUpdate(c, 480, 480 - 3 * 48, buffered, buffered), 1.1);
+
+ c.UpdateClock(0, 0, buffered, buffered);
+ EXPECT_FLOAT_EQ(c.GetCorrection(), 0.9);
+}
+
+TEST(TestClockDrift, SmallDiff)
+{
+ // Keep buffered frames to the wanted level in order to not affect that test.
+ const uint32_t buffered = 5 * 480;
+
+ ClockDrift c(48000, 48000, buffered);
+
+ EXPECT_FLOAT_EQ(RunUntilCorrectionUpdate(c, 480 + 4, 480, buffered, buffered),
+ 1.0);
+ EXPECT_FLOAT_EQ(RunUntilCorrectionUpdate(c, 480 + 5, 480, buffered, buffered),
+ 0.99504131);
+ EXPECT_FLOAT_EQ(RunUntilCorrectionUpdate(c, 480, 480, buffered, buffered),
+ 0.991831);
+ EXPECT_FLOAT_EQ(RunUntilCorrectionUpdate(c, 480, 480 + 4, buffered, buffered),
+ 0.99673241);
+ c.UpdateClock(0, 0, buffered, buffered);
+ EXPECT_FLOAT_EQ(c.GetCorrection(), 1.003693);
+}
+
+TEST(TestClockDrift, SmallBufferedFrames)
+{
+ ClockDrift c(48000, 48000, 5 * 480);
+
+ EXPECT_FLOAT_EQ(c.GetCorrection(), 1.0);
+ for (uint32_t i = 0; i < 10; ++i) {
+ c.UpdateClock(480, 480, 5 * 480, 5 * 480);
+ }
+ EXPECT_FLOAT_EQ(c.GetCorrection(), 1.0);
+ c.UpdateClock(480, 480, 0, 10 * 480);
+ EXPECT_FLOAT_EQ(c.GetCorrection(), 1.1);
+
+ EXPECT_FLOAT_EQ(
+ RunUntilCorrectionUpdate(c, 480, 480, 5 * 480, 5 * 480, 24000, 24000),
+ 1.1);
+ c.UpdateClock(480, 480, 0, 10 * 480);
+ EXPECT_FLOAT_EQ(c.GetCorrection(), 1.1);
+}
+
+// Print the mono channel of a segment.
+void printAudioSegment(const AudioSegment& segment) {
+ for (AudioSegment::ConstChunkIterator iter(segment); !iter.IsEnded();
+ iter.Next()) {
+ const AudioChunk& c = *iter;
+ for (uint32_t i = 0; i < c.GetDuration(); ++i) {
+ if (c.mBufferFormat == AUDIO_FORMAT_FLOAT32) {
+ printf("%f\n", c.ChannelData<float>()[0][i]);
+ } else {
+ printf("%d\n", c.ChannelData<int16_t>()[0][i]);
+ }
+ }
+ }
+}
+
+template <class T>
+AudioChunk CreateAudioChunk(uint32_t aFrames, uint32_t aChannels,
+ AudioSampleFormat aSampleFormat);
+
+void testAudioCorrection(int32_t aSourceRate, int32_t aTargetRate) {
+ const uint32_t sampleRateTransmitter = aSourceRate;
+ const uint32_t sampleRateReceiver = aTargetRate;
+ const uint32_t frequency = 100;
+ const uint32_t buffering = StaticPrefs::media_clockdrift_buffering();
+ const PrincipalHandle testPrincipal =
+ MakePrincipalHandle(nsContentUtils::GetSystemPrincipal());
+ AudioDriftCorrection ad(sampleRateTransmitter, sampleRateReceiver, buffering,
+ testPrincipal);
+
+ AudioGenerator<AudioDataValue> tone(1, sampleRateTransmitter, frequency);
+ AudioVerifier<AudioDataValue> inToneVerifier(sampleRateTransmitter,
+ frequency);
+ AudioVerifier<AudioDataValue> outToneVerifier(sampleRateReceiver, frequency);
+
+ uint32_t sourceFrames;
+ const uint32_t targetFrames = sampleRateReceiver / 100;
+
+ // Run for some time: 3 * 1050 = 3150 iterations
+ for (uint32_t j = 0; j < 3; ++j) {
+ // apply some drift
+ if (j % 2 == 0) {
+ sourceFrames =
+ sampleRateTransmitter * /*1.02*/ 102 / 100 / /*1s->10ms*/ 100;
+ } else {
+ sourceFrames =
+ sampleRateTransmitter * /*0.98*/ 98 / 100 / /*1s->10ms*/ 100;
+ }
+
+ // 10.5 seconds, allows for at least 10 correction changes, to stabilize
+ // around the desired buffer.
+ for (uint32_t n = 0; n < 1050; ++n) {
+ // Create the input (sine tone)
+ AudioSegment inSegment;
+ tone.Generate(inSegment, sourceFrames);
+ inToneVerifier.AppendData(inSegment);
+ // Print the input for debugging
+ // printAudioSegment(inSegment);
+
+ // Get the output of the correction
+ AudioSegment outSegment = ad.RequestFrames(inSegment, targetFrames);
+ EXPECT_EQ(outSegment.GetDuration(), targetFrames);
+ for (AudioSegment::ConstChunkIterator ci(outSegment); !ci.IsEnded();
+ ci.Next()) {
+ EXPECT_EQ(ci->mPrincipalHandle, testPrincipal);
+ }
+ // Print the output for debugging
+ // printAudioSegment(outSegment);
+ outToneVerifier.AppendData(outSegment);
+ }
+ }
+
+ const int32_t expectedBuffering =
+ ad.mDesiredBuffering - sampleRateTransmitter / 100 /* 10ms */;
+ EXPECT_NEAR(ad.CurrentBuffering(), expectedBuffering, 512);
+
+ EXPECT_NEAR(inToneVerifier.EstimatedFreq(), tone.mFrequency, 1.0f);
+ EXPECT_EQ(inToneVerifier.PreSilenceSamples(), 0U);
+ EXPECT_EQ(inToneVerifier.CountDiscontinuities(), 0U);
+
+ EXPECT_NEAR(outToneVerifier.EstimatedFreq(), tone.mFrequency, 1.0f);
+ // The expected pre-silence is 50ms plus the resampling.
+ EXPECT_GE(outToneVerifier.PreSilenceSamples(), aTargetRate * 50 / 1000U);
+ EXPECT_EQ(outToneVerifier.CountDiscontinuities(), 0U);
+}
+
+TEST(TestAudioDriftCorrection, Basic)
+{
+ printf("Testing AudioCorrection 48 -> 48\n");
+ testAudioCorrection(48000, 48000);
+ printf("Testing AudioCorrection 48 -> 44.1\n");
+ testAudioCorrection(48000, 44100);
+ printf("Testing AudioCorrection 44.1 -> 48\n");
+ testAudioCorrection(44100, 48000);
+ printf("Testing AudioCorrection 23458 -> 25113\n");
+ testAudioCorrection(23458, 25113);
+}
+
+void testMonoToStereoInput(uint32_t aSourceRate, uint32_t aTargetRate) {
+ const uint32_t frequency = 100;
+ const uint32_t sampleRateTransmitter = aSourceRate;
+ const uint32_t sampleRateReceiver = aTargetRate;
+ const uint32_t buffering = StaticPrefs::media_clockdrift_buffering();
+ const PrincipalHandle testPrincipal =
+ MakePrincipalHandle(nsContentUtils::GetSystemPrincipal());
+ AudioDriftCorrection ad(sampleRateTransmitter, sampleRateReceiver, buffering,
+ testPrincipal);
+
+ AudioGenerator<AudioDataValue> tone(1, sampleRateTransmitter, frequency);
+ AudioVerifier<AudioDataValue> inToneVerify(sampleRateTransmitter, frequency);
+ AudioVerifier<AudioDataValue> outToneVerify(sampleRateReceiver, frequency);
+
+ uint32_t sourceFrames;
+ const uint32_t targetFrames = sampleRateReceiver / 100;
+
+ // Run for some time: 6 * 250 = 1500 iterations
+ for (uint32_t j = 0; j < 6; ++j) {
+ // apply some drift
+ if (j % 2 == 0) {
+ sourceFrames = sampleRateTransmitter / 100 + 10;
+ } else {
+ sourceFrames = sampleRateTransmitter / 100 - 10;
+ }
+
+ for (uint32_t n = 0; n < 250; ++n) {
+ // Create the input (sine tone) of two chunks.
+ AudioSegment inSegment;
+ tone.Generate(inSegment, sourceFrames / 2);
+ tone.SetChannelsCount(2);
+ tone.Generate(inSegment, sourceFrames / 2);
+ tone.SetChannelsCount(1);
+ inToneVerify.AppendData(inSegment);
+ // Print the input for debugging
+ // printAudioSegment(inSegment);
+
+ // Get the output of the correction
+ AudioSegment outSegment = ad.RequestFrames(inSegment, targetFrames);
+ EXPECT_EQ(outSegment.GetDuration(), targetFrames);
+ for (AudioSegment::ConstChunkIterator ci(outSegment); !ci.IsEnded();
+ ci.Next()) {
+ EXPECT_EQ(ci->mPrincipalHandle, testPrincipal);
+ }
+ // Print the output for debugging
+ // printAudioSegment(outSegment);
+ outToneVerify.AppendData(outSegment);
+ }
+ }
+ EXPECT_EQ(inToneVerify.EstimatedFreq(), frequency);
+ EXPECT_EQ(inToneVerify.PreSilenceSamples(), 0U);
+ EXPECT_EQ(inToneVerify.CountDiscontinuities(), 0U);
+
+ EXPECT_GT(outToneVerify.CountDiscontinuities(), 0U)
+ << "Expect discontinuities";
+ EXPECT_NE(outToneVerify.EstimatedFreq(), frequency)
+ << "Estimation is not accurate due to discontinuities";
+ // The expected pre-silence is 50ms plus the resampling. However, due to
+ // discontinuities pre-silence is expected only in the first iteration which
+ // is routhly a little more than 400 frames for the chosen sample rates.
+ EXPECT_GT(outToneVerify.PreSilenceSamples(), 400U);
+}
+
+TEST(TestAudioDriftCorrection, MonoToStereoInput)
+{
+ testMonoToStereoInput(48000, 48000);
+ testMonoToStereoInput(48000, 44100);
+ testMonoToStereoInput(44100, 48000);
+}
+
+TEST(TestAudioDriftCorrection, NotEnoughFrames)
+{
+ const uint32_t sampleRateTransmitter = 48000;
+ const uint32_t sampleRateReceiver = 48000;
+ const uint32_t buffering = StaticPrefs::media_clockdrift_buffering();
+ const PrincipalHandle testPrincipal =
+ MakePrincipalHandle(nsContentUtils::GetSystemPrincipal());
+ AudioDriftCorrection ad(sampleRateTransmitter, sampleRateReceiver, buffering,
+ testPrincipal);
+ const uint32_t targetFrames = sampleRateReceiver / 100;
+
+ for (uint32_t i = 0; i < 7; ++i) {
+ // Input is something small, 10 frames here, in order to dry out fast,
+ // after 4 iterations
+ AudioChunk chunk = CreateAudioChunk<float>(10, 1, AUDIO_FORMAT_FLOAT32);
+ AudioSegment inSegment;
+ inSegment.AppendAndConsumeChunk(std::move(chunk));
+
+ AudioSegment outSegment = ad.RequestFrames(inSegment, targetFrames);
+ EXPECT_EQ(outSegment.GetDuration(), targetFrames);
+ if (i < 5) {
+ EXPECT_FALSE(outSegment.IsNull());
+ for (AudioSegment::ConstChunkIterator ci(outSegment); !ci.IsEnded();
+ ci.Next()) {
+ EXPECT_EQ(ci->mPrincipalHandle, testPrincipal);
+ }
+ } else {
+ // Last 2 iterations, the 5th and 6th, will be null. It has used all
+ // buffered data so the output is silence.
+ EXPECT_TRUE(outSegment.IsNull());
+ }
+ }
+}
+
+TEST(TestAudioDriftCorrection, CrashInAudioResampler)
+{
+ const uint32_t sampleRateTransmitter = 48000;
+ const uint32_t sampleRateReceiver = 48000;
+ const uint32_t buffering = StaticPrefs::media_clockdrift_buffering();
+ const PrincipalHandle testPrincipal =
+ MakePrincipalHandle(nsContentUtils::GetSystemPrincipal());
+ AudioDriftCorrection ad(sampleRateTransmitter, sampleRateReceiver, buffering,
+ testPrincipal);
+ const uint32_t targetFrames = sampleRateReceiver / 100;
+
+ for (uint32_t i = 0; i < 100; ++i) {
+ AudioChunk chunk = CreateAudioChunk<float>(sampleRateTransmitter / 1000, 1,
+ AUDIO_FORMAT_FLOAT32);
+ AudioSegment inSegment;
+ inSegment.AppendAndConsumeChunk(std::move(chunk));
+
+ AudioSegment outSegment = ad.RequestFrames(inSegment, targetFrames);
+ EXPECT_EQ(outSegment.GetDuration(), targetFrames);
+ if (!outSegment.IsNull()) { // Don't check the data if ad is dried out.
+ for (AudioSegment::ConstChunkIterator ci(outSegment); !ci.IsEnded();
+ ci.Next()) {
+ EXPECT_EQ(ci->mPrincipalHandle, testPrincipal);
+ }
+ }
+ }
+}
diff --git a/dom/media/gtest/TestAudioInputProcessing.cpp b/dom/media/gtest/TestAudioInputProcessing.cpp
new file mode 100644
index 0000000000..1dd5723acf
--- /dev/null
+++ b/dom/media/gtest/TestAudioInputProcessing.cpp
@@ -0,0 +1,386 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+#include "AudioGenerator.h"
+#include "MediaEngineWebRTCAudio.h"
+#include "MediaTrackGraphImpl.h"
+#include "PrincipalHandle.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/NullPrincipal.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Unused.h"
+#include "nsContentUtils.h"
+#include "nsTArray.h"
+
+using namespace mozilla;
+using testing::NiceMock;
+using testing::Return;
+
+class MockGraph : public MediaTrackGraphImpl {
+ public:
+ MockGraph(TrackRate aRate, uint32_t aChannels)
+ : MediaTrackGraphImpl(OFFLINE_THREAD_DRIVER, DIRECT_DRIVER, aRate,
+ aChannels, nullptr, AbstractThread::MainThread()) {
+ ON_CALL(*this, OnGraphThread).WillByDefault(Return(true));
+ // Remove this graph's driver since it holds a ref. If no AppendMessage
+ // takes place, the driver never starts. This will also make sure no-one
+ // tries to use it. We are still kept alive by the self-ref. Destroy() must
+ // be called to break that cycle.
+ SetCurrentDriver(nullptr);
+ }
+
+ MOCK_CONST_METHOD0(OnGraphThread, bool());
+
+ protected:
+ ~MockGraph() = default;
+};
+
+// AudioInputProcessing will put extra frames as pre-buffering data to avoid
+// glitchs in non pass-through mode. The main goal of the test is to check how
+// many frames left in the AudioInputProcessing's mSegment in various situations
+// after input data has been processed.
+TEST(TestAudioInputProcessing, Buffering)
+{
+ const TrackRate rate = 8000; // So packet size is 80
+ const uint32_t channels = 1;
+ auto graph = MakeRefPtr<NiceMock<MockGraph>>(rate, channels);
+ auto aip = MakeRefPtr<AudioInputProcessing>(channels);
+
+ const size_t frames = 72;
+
+ AudioGenerator<AudioDataValue> generator(channels, rate);
+ GraphTime processedTime;
+ GraphTime nextTime;
+ AudioSegment output;
+
+ // Toggle pass-through mode without starting
+ {
+ EXPECT_EQ(aip->PassThrough(graph), false);
+ EXPECT_EQ(aip->NumBufferedFrames(graph), 0);
+
+ aip->SetPassThrough(graph, true);
+ EXPECT_EQ(aip->NumBufferedFrames(graph), 0);
+
+ aip->SetPassThrough(graph, false);
+ EXPECT_EQ(aip->NumBufferedFrames(graph), 0);
+
+ aip->SetPassThrough(graph, true);
+ EXPECT_EQ(aip->NumBufferedFrames(graph), 0);
+ }
+
+ {
+ // Need (nextTime - processedTime) = 128 - 0 = 128 frames this round.
+ // aip has not started and set to processing mode yet, so output will be
+ // filled with silence data directly.
+ processedTime = 0;
+ nextTime = MediaTrackGraphImpl::RoundUpToEndOfAudioBlock(frames);
+
+ AudioSegment input;
+ generator.Generate(input, nextTime - processedTime);
+
+ aip->Process(graph, processedTime, nextTime, &input, &output);
+ EXPECT_EQ(input.GetDuration(), nextTime - processedTime);
+ EXPECT_EQ(output.GetDuration(), nextTime);
+ EXPECT_EQ(aip->NumBufferedFrames(graph), 0);
+ }
+
+ // Set aip to processing/non-pass-through mode
+ aip->SetPassThrough(graph, false);
+ {
+ // Need (nextTime - processedTime) = 256 - 128 = 128 frames this round.
+ // aip has not started yet, so output will be filled with silence data
+ // directly.
+ processedTime = nextTime;
+ nextTime = MediaTrackGraphImpl::RoundUpToEndOfAudioBlock(2 * frames);
+
+ AudioSegment input;
+ generator.Generate(input, nextTime - processedTime);
+
+ aip->Process(graph, processedTime, nextTime, &input, &output);
+ EXPECT_EQ(input.GetDuration(), nextTime - processedTime);
+ EXPECT_EQ(output.GetDuration(), nextTime);
+ EXPECT_EQ(aip->NumBufferedFrames(graph), 0);
+ }
+
+ // aip has been started and set to processing mode so it will insert 80 frames
+ // into aip's internal buffer as pre-buffering.
+ aip->Start(graph);
+ {
+ // Need (nextTime - processedTime) = 256 - 256 = 0 frames this round.
+ // The Process() aip will take 0 frames from input, packetize and process
+ // these frames into 0 80-frame packet(0 frames left in packetizer), insert
+ // packets into aip's internal buffer, then move 0 frames the internal
+ // buffer to output, leaving 80 + 0 - 0 = 80 frames in aip's internal
+ // buffer.
+ processedTime = nextTime;
+ nextTime = MediaTrackGraphImpl::RoundUpToEndOfAudioBlock(3 * frames);
+
+ AudioSegment input;
+ generator.Generate(input, nextTime - processedTime);
+
+ aip->Process(graph, processedTime, nextTime, &input, &output);
+ EXPECT_EQ(input.GetDuration(), nextTime - processedTime);
+ EXPECT_EQ(output.GetDuration(), nextTime);
+ EXPECT_EQ(aip->NumBufferedFrames(graph), 80);
+ }
+
+ {
+ // Need (nextTime - processedTime) = 384 - 256 = 128 frames this round.
+ // The Process() aip will take 128 frames from input, packetize and process
+ // these frames into floor(128/80) = 1 80-frame packet (48 frames left in
+ // packetizer), insert packets into aip's internal buffer, then move 128
+ // frames the internal buffer to output, leaving 80 + 80 - 128 = 32 frames
+ // in aip's internal buffer.
+ processedTime = nextTime;
+ nextTime = MediaTrackGraphImpl::RoundUpToEndOfAudioBlock(4 * frames);
+
+ AudioSegment input;
+ generator.Generate(input, nextTime - processedTime);
+
+ aip->Process(graph, processedTime, nextTime, &input, &output);
+ EXPECT_EQ(input.GetDuration(), nextTime - processedTime);
+ EXPECT_EQ(output.GetDuration(), nextTime);
+ EXPECT_EQ(aip->NumBufferedFrames(graph), 32);
+ }
+
+ {
+ // Need (nextTime - processedTime) = 384 - 384 = 0 frames this round.
+ processedTime = nextTime;
+ nextTime = MediaTrackGraphImpl::RoundUpToEndOfAudioBlock(5 * frames);
+
+ AudioSegment input;
+ generator.Generate(input, nextTime - processedTime);
+
+ aip->Process(graph, processedTime, nextTime, &input, &output);
+ EXPECT_EQ(input.GetDuration(), nextTime - processedTime);
+ EXPECT_EQ(output.GetDuration(), nextTime);
+ EXPECT_EQ(aip->NumBufferedFrames(graph), 32);
+ }
+
+ {
+ // Need (nextTime - processedTime) = 512 - 384 = 128 frames this round.
+ // The Process() aip will take 128 frames from input, packetize and process
+ // these frames into floor(128+48/80) = 2 80-frame packet (16 frames left in
+ // packetizer), insert packets into aip's internal buffer, then move 128
+ // frames the internal buffer to output, leaving 32 + 2*80 - 128 = 64 frames
+ // in aip's internal buffer.
+ processedTime = nextTime;
+ nextTime = MediaTrackGraphImpl::RoundUpToEndOfAudioBlock(6 * frames);
+
+ AudioSegment input;
+ generator.Generate(input, nextTime - processedTime);
+
+ aip->Process(graph, processedTime, nextTime, &input, &output);
+ EXPECT_EQ(input.GetDuration(), nextTime - processedTime);
+ EXPECT_EQ(output.GetDuration(), nextTime);
+ EXPECT_EQ(aip->NumBufferedFrames(graph), 64);
+ }
+
+ aip->SetPassThrough(graph, true);
+ {
+ // Need (nextTime - processedTime) = 512 - 512 = 0 frames this round.
+ // No buffering in pass-through mode
+ processedTime = nextTime;
+ nextTime = MediaTrackGraphImpl::RoundUpToEndOfAudioBlock(7 * frames);
+
+ AudioSegment input;
+ generator.Generate(input, nextTime - processedTime);
+
+ aip->Process(graph, processedTime, nextTime, &input, &output);
+ EXPECT_EQ(input.GetDuration(), nextTime - processedTime);
+ EXPECT_EQ(output.GetDuration(), processedTime);
+ EXPECT_EQ(aip->NumBufferedFrames(graph), 0);
+ }
+
+ aip->Stop(graph);
+ graph->Destroy();
+}
+
+TEST(TestAudioInputProcessing, ProcessDataWithDifferentPrincipals)
+{
+ const TrackRate rate = 48000; // so # of output frames from packetizer is 480
+ const uint32_t channels = 2;
+ auto graph = MakeRefPtr<NiceMock<MockGraph>>(rate, channels);
+ auto aip = MakeRefPtr<AudioInputProcessing>(channels);
+ AudioGenerator<AudioDataValue> generator(channels, rate);
+
+ RefPtr<nsIPrincipal> dummy_principal =
+ NullPrincipal::CreateWithoutOriginAttributes();
+ const PrincipalHandle principal1 = MakePrincipalHandle(dummy_principal.get());
+ const PrincipalHandle principal2 =
+ MakePrincipalHandle(nsContentUtils::GetSystemPrincipal());
+
+ // Total 4800 frames. It's easier to test with frames of multiples of 480.
+ nsTArray<std::pair<TrackTime, PrincipalHandle>> framesWithPrincipal = {
+ {100, principal1},
+ {200, PRINCIPAL_HANDLE_NONE},
+ {300, principal2},
+ {400, principal1},
+ {440, PRINCIPAL_HANDLE_NONE},
+ // 3 packet-size above.
+ {480, principal1},
+ {480, principal2},
+ {480, PRINCIPAL_HANDLE_NONE},
+ // 3 packet-size above.
+ {500, principal2},
+ {490, principal1},
+ {600, principal1},
+ {330, principal1}
+ // 4 packet-size above.
+ };
+
+ // Generate 4800 frames of data with different principals.
+ AudioSegment input;
+ {
+ for (const auto& [duration, principal] : framesWithPrincipal) {
+ AudioSegment data;
+ generator.Generate(data, duration);
+ for (AudioSegment::ChunkIterator it(data); !it.IsEnded(); it.Next()) {
+ it->mPrincipalHandle = principal;
+ }
+
+ input.AppendFrom(&data);
+ }
+ }
+
+ auto verifyPrincipals = [&](const AudioSegment& data) {
+ TrackTime start = 0;
+ for (const auto& [duration, principal] : framesWithPrincipal) {
+ const TrackTime end = start + duration;
+
+ AudioSegment slice;
+ slice.AppendSlice(data, start, end);
+ start = end;
+
+ for (AudioSegment::ChunkIterator it(slice); !it.IsEnded(); it.Next()) {
+ EXPECT_EQ(it->mPrincipalHandle, principal);
+ }
+ }
+ };
+
+ // Check the principals in audio-processing mode.
+ EXPECT_EQ(aip->PassThrough(graph), false);
+ aip->Start(graph);
+ {
+ EXPECT_EQ(aip->NumBufferedFrames(graph), 480);
+ AudioSegment output;
+ {
+ // Trim the prebuffering silence.
+
+ AudioSegment data;
+ aip->Process(graph, 0, 4800, &input, &data);
+ EXPECT_EQ(input.GetDuration(), 4800);
+ EXPECT_EQ(data.GetDuration(), 4800);
+
+ AudioSegment dummy;
+ dummy.AppendNullData(480);
+ aip->Process(graph, 0, 480, &dummy, &data);
+ EXPECT_EQ(dummy.GetDuration(), 480);
+ EXPECT_EQ(data.GetDuration(), 480 + 4800);
+
+ // Ignore the pre-buffering data
+ output.AppendSlice(data, 480, 480 + 4800);
+ }
+
+ verifyPrincipals(output);
+ }
+
+ // Check the principals in pass-through mode.
+ aip->SetPassThrough(graph, true);
+ {
+ AudioSegment output;
+ aip->Process(graph, 0, 4800, &input, &output);
+ EXPECT_EQ(input.GetDuration(), 4800);
+ EXPECT_EQ(output.GetDuration(), 4800);
+
+ verifyPrincipals(output);
+ }
+
+ aip->Stop(graph);
+ graph->Destroy();
+}
+
+TEST(TestAudioInputProcessing, Downmixing)
+{
+ const TrackRate rate = 44100;
+ const uint32_t channels = 4;
+ auto graph = MakeRefPtr<NiceMock<MockGraph>>(rate, channels);
+ auto aip = MakeRefPtr<AudioInputProcessing>(channels);
+
+ const size_t frames = 44100;
+
+ AudioGenerator<AudioDataValue> generator(channels, rate);
+ GraphTime processedTime;
+ GraphTime nextTime;
+
+ aip->SetPassThrough(graph, false);
+ aip->Start(graph);
+
+ processedTime = 0;
+ nextTime = MediaTrackGraphImpl::RoundUpToEndOfAudioBlock(frames);
+
+ {
+ AudioSegment input;
+ AudioSegment output;
+ generator.Generate(input, nextTime - processedTime);
+
+ // Intentionally reduce the amplitude of the generated sine wave so there's
+ // no chance the max amplitude reaches 1.0, but not enough so that 4
+ // channels summed together won't clip.
+ input.ApplyVolume(0.9);
+
+ // Process is going to see that it has 4 channels of input, and is going to
+ // downmix to mono, scaling the input by 1/4 in the process.
+ // We can't compare the input and output signal because the sine is going to
+ // be mangledui
+ aip->Process(graph, processedTime, nextTime, &input, &output);
+ EXPECT_EQ(input.GetDuration(), nextTime - processedTime);
+ EXPECT_EQ(output.GetDuration(), nextTime);
+ EXPECT_EQ(output.MaxChannelCount(), 1u);
+
+ // Verify that it doesn't clip: the input signal has likely been mangled by
+ // the various processing passes, but at least it shouldn't clip. We know we
+ // always have floating point audio here, regardless of the sample-type used
+ // by Gecko.
+ for (AudioSegment::ChunkIterator iterOutput(output); !iterOutput.IsEnded();
+ iterOutput.Next()) {
+ const float* const output = iterOutput->ChannelData<float>()[0];
+ for (uint32_t i = 0; i < iterOutput->GetDuration(); i++) {
+ // Very conservative here, it's likely that the AGC lowers the volume a
+ // lot.
+ EXPECT_LE(std::abs(output[i]), 0.95);
+ }
+ }
+ }
+
+ // Now, repeat the test, checking we get the unmodified 4 channels.
+ aip->SetPassThrough(graph, true);
+
+ AudioSegment input, output;
+ processedTime = nextTime;
+ nextTime += MediaTrackGraphImpl::RoundUpToEndOfAudioBlock(frames);
+ generator.Generate(input, nextTime - processedTime);
+
+ aip->Process(graph, processedTime, nextTime, &input, &output);
+ EXPECT_EQ(input.GetDuration(), nextTime - processedTime);
+ EXPECT_EQ(output.GetDuration(), nextTime - processedTime);
+ // This time, no downmix: 4 channels of input, 4 channels of output
+ EXPECT_EQ(output.MaxChannelCount(), 4u);
+
+ nsTArray<AudioDataValue> inputLinearized, outputLinearized;
+ input.WriteToInterleavedBuffer(inputLinearized, input.MaxChannelCount());
+ output.WriteToInterleavedBuffer(outputLinearized, output.MaxChannelCount());
+
+ // The data should be passed through, and exactly equal.
+ for (uint32_t i = 0; i < frames * channels; i++) {
+ EXPECT_EQ(inputLinearized[i], outputLinearized[i]);
+ }
+
+ aip->Stop(graph);
+ graph->Destroy();
+}
diff --git a/dom/media/gtest/TestAudioInputSource.cpp b/dom/media/gtest/TestAudioInputSource.cpp
new file mode 100644
index 0000000000..c5b820065b
--- /dev/null
+++ b/dom/media/gtest/TestAudioInputSource.cpp
@@ -0,0 +1,275 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#include "AudioInputSource.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+#include "MockCubeb.h"
+#include "WaitFor.h"
+#include "nsContentUtils.h"
+
+using namespace mozilla;
+
+namespace {
+#define DispatchFunction(f) \
+ NS_DispatchToCurrentThread(NS_NewRunnableFunction(__func__, f))
+} // namespace
+
+class MockEventListener : public AudioInputSource::EventListener {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MockEventListener, override);
+ MOCK_METHOD1(AudioDeviceChanged, void(AudioInputSource::Id));
+ MOCK_METHOD2(AudioStateCallback,
+ void(AudioInputSource::Id,
+ AudioInputSource::EventListener::State));
+
+ private:
+ ~MockEventListener() = default;
+};
+
+TEST(TestAudioInputSource, StartAndStop)
+{
+ MockCubeb* cubeb = new MockCubeb();
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+
+ const AudioInputSource::Id sourceId = 1;
+ const CubebUtils::AudioDeviceID deviceId = (CubebUtils::AudioDeviceID)1;
+ const uint32_t channels = 2;
+ const PrincipalHandle testPrincipal =
+ MakePrincipalHandle(nsContentUtils::GetSystemPrincipal());
+ const TrackRate sourceRate = 44100;
+ const TrackRate targetRate = 48000;
+ const uint32_t bufferingMs = 50; // ms
+
+ auto listener = MakeRefPtr<MockEventListener>();
+ EXPECT_CALL(*listener,
+ AudioStateCallback(
+ sourceId, AudioInputSource::EventListener::State::Started))
+ .Times(2);
+ EXPECT_CALL(*listener,
+ AudioStateCallback(
+ sourceId, AudioInputSource::EventListener::State::Stopped))
+ .Times(4);
+
+ RefPtr<AudioInputSource> ais = MakeRefPtr<AudioInputSource>(
+ std::move(listener), sourceId, deviceId, channels, true, testPrincipal,
+ sourceRate, targetRate, bufferingMs);
+ ASSERT_TRUE(ais);
+
+ // Make sure start and stop works.
+ {
+ DispatchFunction([&] { ais->Start(); });
+ RefPtr<SmartMockCubebStream> stream = WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(stream->mHasInput);
+ EXPECT_FALSE(stream->mHasOutput);
+ EXPECT_EQ(stream->GetInputDeviceID(), deviceId);
+ EXPECT_EQ(stream->InputChannels(), channels);
+ EXPECT_EQ(stream->InputSampleRate(), static_cast<uint32_t>(sourceRate));
+
+ Unused << WaitFor(stream->FramesProcessedEvent());
+
+ DispatchFunction([&] { ais->Stop(); });
+ Unused << WaitFor(cubeb->StreamDestroyEvent());
+ }
+
+ // Make sure restart is ok.
+ {
+ DispatchFunction([&] { ais->Start(); });
+ RefPtr<SmartMockCubebStream> stream = WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(stream->mHasInput);
+ EXPECT_FALSE(stream->mHasOutput);
+ EXPECT_EQ(stream->GetInputDeviceID(), deviceId);
+ EXPECT_EQ(stream->InputChannels(), channels);
+ EXPECT_EQ(stream->InputSampleRate(), static_cast<uint32_t>(sourceRate));
+
+ Unused << WaitFor(stream->FramesProcessedEvent());
+
+ DispatchFunction([&] { ais->Stop(); });
+ Unused << WaitFor(cubeb->StreamDestroyEvent());
+ }
+
+ ais = nullptr; // Drop the SharedThreadPool here.
+}
+
+TEST(TestAudioInputSource, DataOutputBeforeStartAndAfterStop)
+{
+ MockCubeb* cubeb = new MockCubeb();
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+
+ const AudioInputSource::Id sourceId = 1;
+ const CubebUtils::AudioDeviceID deviceId = (CubebUtils::AudioDeviceID)1;
+ const uint32_t channels = 2;
+ const PrincipalHandle testPrincipal =
+ MakePrincipalHandle(nsContentUtils::GetSystemPrincipal());
+ const TrackRate sourceRate = 44100;
+ const TrackRate targetRate = 48000;
+ const uint32_t bufferingMs = 50; // ms
+
+ const TrackTime requestFrames = 2 * WEBAUDIO_BLOCK_SIZE;
+
+ auto listener = MakeRefPtr<MockEventListener>();
+ EXPECT_CALL(*listener,
+ AudioStateCallback(
+ sourceId, AudioInputSource::EventListener::State::Started));
+ EXPECT_CALL(*listener,
+ AudioStateCallback(
+ sourceId, AudioInputSource::EventListener::State::Stopped))
+ .Times(2);
+
+ RefPtr<AudioInputSource> ais = MakeRefPtr<AudioInputSource>(
+ std::move(listener), sourceId, deviceId, channels, true, testPrincipal,
+ sourceRate, targetRate, bufferingMs);
+ ASSERT_TRUE(ais);
+
+ // It's ok to call GetAudioSegment before starting
+ {
+ AudioSegment data =
+ ais->GetAudioSegment(requestFrames, AudioInputSource::Consumer::Same);
+ EXPECT_EQ(data.GetDuration(), requestFrames);
+ EXPECT_TRUE(data.IsNull());
+ }
+
+ DispatchFunction([&] { ais->Start(); });
+ RefPtr<SmartMockCubebStream> stream = WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(stream->mHasInput);
+ EXPECT_FALSE(stream->mHasOutput);
+ EXPECT_EQ(stream->InputChannels(), channels);
+
+ stream->SetInputRecordingEnabled(true);
+
+ Unused << WaitFor(stream->FramesProcessedEvent());
+
+ DispatchFunction([&] { ais->Stop(); });
+ Unused << WaitFor(cubeb->StreamDestroyEvent());
+
+ // Check the data output
+ {
+ nsTArray<AudioDataValue> record = stream->TakeRecordedInput();
+ size_t frames = record.Length() / channels;
+ AudioSegment deinterleaved;
+ deinterleaved.AppendFromInterleavedBuffer(record.Elements(), frames,
+ channels, testPrincipal);
+ AudioDriftCorrection driftCorrector(sourceRate, targetRate, bufferingMs,
+ testPrincipal);
+ AudioSegment expectedSegment = driftCorrector.RequestFrames(
+ deinterleaved, static_cast<uint32_t>(requestFrames));
+
+ nsTArray<AudioDataValue> expected;
+ size_t expectedSamples =
+ expectedSegment.WriteToInterleavedBuffer(expected, channels);
+
+ AudioSegment actualSegment =
+ ais->GetAudioSegment(requestFrames, AudioInputSource::Consumer::Same);
+ EXPECT_EQ(actualSegment.GetDuration(), requestFrames);
+ nsTArray<AudioDataValue> actual;
+ size_t actualSamples =
+ actualSegment.WriteToInterleavedBuffer(actual, channels);
+
+ EXPECT_EQ(actualSamples, expectedSamples);
+ EXPECT_EQ(actualSamples / channels, static_cast<size_t>(requestFrames));
+ EXPECT_EQ(actual, expected);
+ }
+
+ ais = nullptr; // Drop the SharedThreadPool here.
+}
+
+TEST(TestAudioInputSource, ErrorCallback)
+{
+ MockCubeb* cubeb = new MockCubeb();
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+
+ const AudioInputSource::Id sourceId = 1;
+ const CubebUtils::AudioDeviceID deviceId = (CubebUtils::AudioDeviceID)1;
+ const uint32_t channels = 2;
+ const PrincipalHandle testPrincipal =
+ MakePrincipalHandle(nsContentUtils::GetSystemPrincipal());
+ const TrackRate sourceRate = 44100;
+ const TrackRate targetRate = 48000;
+ const uint32_t bufferingMs = 50; // ms
+
+ auto listener = MakeRefPtr<MockEventListener>();
+ EXPECT_CALL(*listener,
+ AudioStateCallback(
+ sourceId, AudioInputSource::EventListener::State::Started));
+ EXPECT_CALL(*listener,
+ AudioStateCallback(
+ sourceId, AudioInputSource::EventListener::State::Error));
+ EXPECT_CALL(*listener,
+ AudioStateCallback(
+ sourceId, AudioInputSource::EventListener::State::Stopped));
+
+ RefPtr<AudioInputSource> ais = MakeRefPtr<AudioInputSource>(
+ std::move(listener), sourceId, deviceId, channels, true, testPrincipal,
+ sourceRate, targetRate, bufferingMs);
+ ASSERT_TRUE(ais);
+
+ DispatchFunction([&] { ais->Start(); });
+ RefPtr<SmartMockCubebStream> stream = WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(stream->mHasInput);
+ EXPECT_FALSE(stream->mHasOutput);
+ EXPECT_EQ(stream->InputChannels(), channels);
+
+ Unused << WaitFor(stream->FramesProcessedEvent());
+
+ DispatchFunction([&] { stream->ForceError(); });
+ WaitFor(stream->ErrorForcedEvent());
+ // Make sure the stream has been stopped by the error-state's backgroud thread
+ // task, to avoid getting a stopped state callback by `ais->Stop` below.
+ WaitFor(stream->ErrorStoppedEvent());
+
+ DispatchFunction([&] { ais->Stop(); });
+ Unused << WaitFor(cubeb->StreamDestroyEvent());
+
+ ais = nullptr; // Drop the SharedThreadPool here.
+}
+
+TEST(TestAudioInputSource, DeviceChangedCallback)
+{
+ MockCubeb* cubeb = new MockCubeb();
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+
+ const AudioInputSource::Id sourceId = 1;
+ const CubebUtils::AudioDeviceID deviceId = (CubebUtils::AudioDeviceID)1;
+ const uint32_t channels = 2;
+ const PrincipalHandle testPrincipal =
+ MakePrincipalHandle(nsContentUtils::GetSystemPrincipal());
+ const TrackRate sourceRate = 44100;
+ const TrackRate targetRate = 48000;
+ const uint32_t bufferingMs = 50; // ms
+
+ auto listener = MakeRefPtr<MockEventListener>();
+ EXPECT_CALL(*listener, AudioDeviceChanged(sourceId));
+ EXPECT_CALL(*listener,
+ AudioStateCallback(
+ sourceId, AudioInputSource::EventListener::State::Started));
+ EXPECT_CALL(*listener,
+ AudioStateCallback(
+ sourceId, AudioInputSource::EventListener::State::Stopped))
+ .Times(2);
+
+ RefPtr<AudioInputSource> ais = MakeRefPtr<AudioInputSource>(
+ std::move(listener), sourceId, deviceId, channels, true, testPrincipal,
+ sourceRate, targetRate, bufferingMs);
+ ASSERT_TRUE(ais);
+
+ DispatchFunction([&] { ais->Start(); });
+ RefPtr<SmartMockCubebStream> stream = WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(stream->mHasInput);
+ EXPECT_FALSE(stream->mHasOutput);
+ EXPECT_EQ(stream->InputChannels(), channels);
+
+ Unused << WaitFor(stream->FramesProcessedEvent());
+
+ DispatchFunction([&] { stream->ForceDeviceChanged(); });
+ WaitFor(stream->DeviceChangeForcedEvent());
+
+ DispatchFunction([&] { ais->Stop(); });
+ Unused << WaitFor(cubeb->StreamDestroyEvent());
+
+ ais = nullptr; // Drop the SharedThreadPool here.
+}
diff --git a/dom/media/gtest/TestAudioMixer.cpp b/dom/media/gtest/TestAudioMixer.cpp
new file mode 100644
index 0000000000..017ac960eb
--- /dev/null
+++ b/dom/media/gtest/TestAudioMixer.cpp
@@ -0,0 +1,174 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioMixer.h"
+#include "gtest/gtest.h"
+
+using mozilla::AudioDataValue;
+using mozilla::AudioSampleFormat;
+
+namespace audio_mixer {
+
+struct MixerConsumer : public mozilla::MixerCallbackReceiver {
+ /* In this test, the different audio stream and channels are always created to
+ * cancel each other. */
+ void MixerCallback(AudioDataValue* aData, AudioSampleFormat aFormat,
+ uint32_t aChannels, uint32_t aFrames,
+ uint32_t aSampleRate) {
+ bool silent = true;
+ for (uint32_t i = 0; i < aChannels * aFrames; i++) {
+ if (aData[i] != 0.0) {
+ if (aFormat == mozilla::AUDIO_FORMAT_S16) {
+ fprintf(stderr, "Sample at %d is not silent: %d\n", i,
+ (short)aData[i]);
+ } else {
+ fprintf(stderr, "Sample at %d is not silent: %f\n", i,
+ (float)aData[i]);
+ }
+ silent = false;
+ }
+ }
+ ASSERT_TRUE(silent);
+ }
+};
+
+/* Helper function to give us the maximum and minimum value that don't clip,
+ * for a given sample format (integer or floating-point). */
+template <typename T>
+T GetLowValue();
+
+template <typename T>
+T GetHighValue();
+
+template <>
+float GetLowValue<float>() {
+ return -1.0;
+}
+
+template <>
+short GetLowValue<short>() {
+ return -INT16_MAX;
+}
+
+template <>
+float GetHighValue<float>() {
+ return 1.0;
+}
+
+template <>
+short GetHighValue<short>() {
+ return INT16_MAX;
+}
+
+void FillBuffer(AudioDataValue* aBuffer, uint32_t aLength,
+ AudioDataValue aValue) {
+ AudioDataValue* end = aBuffer + aLength;
+ while (aBuffer != end) {
+ *aBuffer++ = aValue;
+ }
+}
+
+TEST(AudioMixer, Test)
+{
+ const uint32_t CHANNEL_LENGTH = 256;
+ const uint32_t AUDIO_RATE = 44100;
+ MixerConsumer consumer;
+ AudioDataValue a[CHANNEL_LENGTH * 2];
+ AudioDataValue b[CHANNEL_LENGTH * 2];
+ FillBuffer(a, CHANNEL_LENGTH, GetLowValue<AudioDataValue>());
+ FillBuffer(a + CHANNEL_LENGTH, CHANNEL_LENGTH,
+ GetHighValue<AudioDataValue>());
+ FillBuffer(b, CHANNEL_LENGTH, GetHighValue<AudioDataValue>());
+ FillBuffer(b + CHANNEL_LENGTH, CHANNEL_LENGTH, GetLowValue<AudioDataValue>());
+
+ {
+ int iterations = 2;
+ mozilla::AudioMixer mixer;
+ mixer.AddCallback(WrapNotNull(&consumer));
+
+ fprintf(stderr, "Test AudioMixer constant buffer length.\n");
+
+ while (iterations--) {
+ mixer.Mix(a, 2, CHANNEL_LENGTH, AUDIO_RATE);
+ mixer.Mix(b, 2, CHANNEL_LENGTH, AUDIO_RATE);
+ mixer.FinishMixing();
+ }
+ }
+
+ {
+ mozilla::AudioMixer mixer;
+ mixer.AddCallback(WrapNotNull(&consumer));
+
+ fprintf(stderr, "Test AudioMixer variable buffer length.\n");
+
+ FillBuffer(a, CHANNEL_LENGTH / 2, GetLowValue<AudioDataValue>());
+ FillBuffer(a + CHANNEL_LENGTH / 2, CHANNEL_LENGTH / 2,
+ GetLowValue<AudioDataValue>());
+ FillBuffer(b, CHANNEL_LENGTH / 2, GetHighValue<AudioDataValue>());
+ FillBuffer(b + CHANNEL_LENGTH / 2, CHANNEL_LENGTH / 2,
+ GetHighValue<AudioDataValue>());
+ mixer.Mix(a, 2, CHANNEL_LENGTH / 2, AUDIO_RATE);
+ mixer.Mix(b, 2, CHANNEL_LENGTH / 2, AUDIO_RATE);
+ mixer.FinishMixing();
+ FillBuffer(a, CHANNEL_LENGTH, GetLowValue<AudioDataValue>());
+ FillBuffer(a + CHANNEL_LENGTH, CHANNEL_LENGTH,
+ GetHighValue<AudioDataValue>());
+ FillBuffer(b, CHANNEL_LENGTH, GetHighValue<AudioDataValue>());
+ FillBuffer(b + CHANNEL_LENGTH, CHANNEL_LENGTH,
+ GetLowValue<AudioDataValue>());
+ mixer.Mix(a, 2, CHANNEL_LENGTH, AUDIO_RATE);
+ mixer.Mix(b, 2, CHANNEL_LENGTH, AUDIO_RATE);
+ mixer.FinishMixing();
+ FillBuffer(a, CHANNEL_LENGTH / 2, GetLowValue<AudioDataValue>());
+ FillBuffer(a + CHANNEL_LENGTH / 2, CHANNEL_LENGTH / 2,
+ GetLowValue<AudioDataValue>());
+ FillBuffer(b, CHANNEL_LENGTH / 2, GetHighValue<AudioDataValue>());
+ FillBuffer(b + CHANNEL_LENGTH / 2, CHANNEL_LENGTH / 2,
+ GetHighValue<AudioDataValue>());
+ mixer.Mix(a, 2, CHANNEL_LENGTH / 2, AUDIO_RATE);
+ mixer.Mix(b, 2, CHANNEL_LENGTH / 2, AUDIO_RATE);
+ mixer.FinishMixing();
+ }
+
+ FillBuffer(a, CHANNEL_LENGTH, GetLowValue<AudioDataValue>());
+ FillBuffer(b, CHANNEL_LENGTH, GetHighValue<AudioDataValue>());
+
+ {
+ mozilla::AudioMixer mixer;
+ mixer.AddCallback(WrapNotNull(&consumer));
+
+ fprintf(stderr, "Test AudioMixer variable channel count.\n");
+
+ mixer.Mix(a, 1, CHANNEL_LENGTH, AUDIO_RATE);
+ mixer.Mix(b, 1, CHANNEL_LENGTH, AUDIO_RATE);
+ mixer.FinishMixing();
+ mixer.Mix(a, 1, CHANNEL_LENGTH, AUDIO_RATE);
+ mixer.Mix(b, 1, CHANNEL_LENGTH, AUDIO_RATE);
+ mixer.FinishMixing();
+ mixer.Mix(a, 1, CHANNEL_LENGTH, AUDIO_RATE);
+ mixer.Mix(b, 1, CHANNEL_LENGTH, AUDIO_RATE);
+ mixer.FinishMixing();
+ }
+
+ {
+ mozilla::AudioMixer mixer;
+ mixer.AddCallback(WrapNotNull(&consumer));
+ fprintf(stderr, "Test AudioMixer variable stream count.\n");
+
+ mixer.Mix(a, 2, CHANNEL_LENGTH, AUDIO_RATE);
+ mixer.Mix(b, 2, CHANNEL_LENGTH, AUDIO_RATE);
+ mixer.FinishMixing();
+ mixer.Mix(a, 2, CHANNEL_LENGTH, AUDIO_RATE);
+ mixer.Mix(b, 2, CHANNEL_LENGTH, AUDIO_RATE);
+ mixer.Mix(a, 2, CHANNEL_LENGTH, AUDIO_RATE);
+ mixer.Mix(b, 2, CHANNEL_LENGTH, AUDIO_RATE);
+ mixer.FinishMixing();
+ mixer.Mix(a, 2, CHANNEL_LENGTH, AUDIO_RATE);
+ mixer.Mix(b, 2, CHANNEL_LENGTH, AUDIO_RATE);
+ mixer.FinishMixing();
+ }
+}
+
+} // namespace audio_mixer
diff --git a/dom/media/gtest/TestAudioPacketizer.cpp b/dom/media/gtest/TestAudioPacketizer.cpp
new file mode 100644
index 0000000000..6c3275d82a
--- /dev/null
+++ b/dom/media/gtest/TestAudioPacketizer.cpp
@@ -0,0 +1,163 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <stdint.h>
+#include <math.h>
+#include <memory>
+#include "../AudioPacketizer.h"
+#include "gtest/gtest.h"
+
+using namespace mozilla;
+
+template <typename T>
+class AutoBuffer {
+ public:
+ explicit AutoBuffer(size_t aLength) { mStorage = new T[aLength]; }
+ ~AutoBuffer() { delete[] mStorage; }
+ T* Get() { return mStorage; }
+
+ private:
+ T* mStorage;
+};
+
+int16_t Sequence(int16_t* aBuffer, uint32_t aSize, uint32_t aStart = 0) {
+ uint32_t i;
+ for (i = 0; i < aSize; i++) {
+ aBuffer[i] = aStart + i;
+ }
+ return aStart + i;
+}
+
+void IsSequence(std::unique_ptr<int16_t[]> aBuffer, uint32_t aSize,
+ uint32_t aStart = 0) {
+ for (uint32_t i = 0; i < aSize; i++) {
+ ASSERT_TRUE(aBuffer[i] == static_cast<int64_t>(aStart + i))
+ << "Buffer is not a sequence at offset " << i << std::endl;
+ }
+ // Buffer is a sequence.
+}
+
+void Zero(std::unique_ptr<int16_t[]> aBuffer, uint32_t aSize) {
+ for (uint32_t i = 0; i < aSize; i++) {
+ ASSERT_TRUE(aBuffer[i] == 0)
+ << "Buffer is not null at offset " << i << std::endl;
+ }
+}
+
+double sine(uint32_t aPhase) { return sin(aPhase * 2 * M_PI * 440 / 44100); }
+
+TEST(AudioPacketizer, Test)
+{
+ for (int16_t channels = 1; channels < 2; channels++) {
+ // Test that the packetizer returns zero on underrun
+ {
+ AudioPacketizer<int16_t, int16_t> ap(441, channels);
+ for (int16_t i = 0; i < 10; i++) {
+ std::unique_ptr<int16_t[]> out(ap.Output());
+ Zero(std::move(out), 441);
+ }
+ }
+ // Simple test, with input/output buffer size aligned on the packet size,
+ // alternating Input and Output calls.
+ {
+ AudioPacketizer<int16_t, int16_t> ap(441, channels);
+ int16_t seqEnd = 0;
+ for (int16_t i = 0; i < 10; i++) {
+ AutoBuffer<int16_t> b(441 * channels);
+ int16_t prevEnd = seqEnd;
+ seqEnd = Sequence(b.Get(), channels * 441, prevEnd);
+ ap.Input(b.Get(), 441);
+ std::unique_ptr<int16_t[]> out(ap.Output());
+ IsSequence(std::move(out), 441 * channels, prevEnd);
+ }
+ }
+ // Simple test, with input/output buffer size aligned on the packet size,
+ // alternating two Input and Output calls.
+ {
+ AudioPacketizer<int16_t, int16_t> ap(441, channels);
+ int16_t seqEnd = 0;
+ for (int16_t i = 0; i < 10; i++) {
+ AutoBuffer<int16_t> b(441 * channels);
+ AutoBuffer<int16_t> b1(441 * channels);
+ int16_t prevEnd0 = seqEnd;
+ seqEnd = Sequence(b.Get(), 441 * channels, prevEnd0);
+ int16_t prevEnd1 = seqEnd;
+ seqEnd = Sequence(b1.Get(), 441 * channels, seqEnd);
+ ap.Input(b.Get(), 441);
+ ap.Input(b1.Get(), 441);
+ std::unique_ptr<int16_t[]> out(ap.Output());
+ std::unique_ptr<int16_t[]> out2(ap.Output());
+ IsSequence(std::move(out), 441 * channels, prevEnd0);
+ IsSequence(std::move(out2), 441 * channels, prevEnd1);
+ }
+ }
+ // Input/output buffer size not aligned on the packet size,
+ // alternating two Input and Output calls.
+ {
+ AudioPacketizer<int16_t, int16_t> ap(441, channels);
+ int16_t prevEnd = 0;
+ int16_t prevSeq = 0;
+ for (int16_t i = 0; i < 10; i++) {
+ AutoBuffer<int16_t> b(480 * channels);
+ AutoBuffer<int16_t> b1(480 * channels);
+ prevSeq = Sequence(b.Get(), 480 * channels, prevSeq);
+ prevSeq = Sequence(b1.Get(), 480 * channels, prevSeq);
+ ap.Input(b.Get(), 480);
+ ap.Input(b1.Get(), 480);
+ std::unique_ptr<int16_t[]> out(ap.Output());
+ std::unique_ptr<int16_t[]> out2(ap.Output());
+ IsSequence(std::move(out), 441 * channels, prevEnd);
+ prevEnd += 441 * channels;
+ IsSequence(std::move(out2), 441 * channels, prevEnd);
+ prevEnd += 441 * channels;
+ }
+ printf("Available: %d\n", ap.PacketsAvailable());
+ }
+
+ // "Real-life" test case: streaming a sine wave through a packetizer, and
+ // checking that we have the right output.
+ // 128 is, for example, the size of a Web Audio API block, and 441 is the
+ // size of a webrtc.org packet when the sample rate is 44100 (10ms)
+ {
+ AudioPacketizer<int16_t, int16_t> ap(441, channels);
+ AutoBuffer<int16_t> b(128 * channels);
+ uint32_t phase = 0;
+ uint32_t outPhase = 0;
+ for (int16_t i = 0; i < 1000; i++) {
+ for (int32_t j = 0; j < 128; j++) {
+ for (int32_t c = 0; c < channels; c++) {
+ // int16_t sinewave at 440Hz/44100Hz sample rate
+ b.Get()[j * channels + c] = (2 << 14) * sine(phase);
+ }
+ phase++;
+ }
+ ap.Input(b.Get(), 128);
+ while (ap.PacketsAvailable()) {
+ std::unique_ptr<int16_t[]> packet(ap.Output());
+ for (uint32_t k = 0; k < ap.mPacketSize; k++) {
+ for (int32_t c = 0; c < channels; c++) {
+ ASSERT_TRUE(packet[k * channels + c] ==
+ static_cast<int16_t>(((2 << 14) * sine(outPhase))));
+ }
+ outPhase++;
+ }
+ }
+ }
+ }
+ // Test that clearing the packetizer empties it and starts returning zeros.
+ {
+ AudioPacketizer<int16_t, int16_t> ap(441, channels);
+ AutoBuffer<int16_t> b(440 * channels);
+ Sequence(b.Get(), 440 * channels);
+ ap.Input(b.Get(), 440);
+ EXPECT_EQ(ap.FramesAvailable(), 440U);
+ ap.Clear();
+ EXPECT_EQ(ap.FramesAvailable(), 0U);
+ EXPECT_TRUE(ap.Empty());
+ std::unique_ptr<int16_t[]> out(ap.Output());
+ Zero(std::move(out), 441);
+ }
+ }
+}
diff --git a/dom/media/gtest/TestAudioRingBuffer.cpp b/dom/media/gtest/TestAudioRingBuffer.cpp
new file mode 100644
index 0000000000..1eb33df384
--- /dev/null
+++ b/dom/media/gtest/TestAudioRingBuffer.cpp
@@ -0,0 +1,993 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioRingBuffer.h"
+
+#include "gtest/gtest.h"
+#include "mozilla/PodOperations.h"
+
+using namespace mozilla;
+
+TEST(TestAudioRingBuffer, BasicFloat)
+{
+ AudioRingBuffer ringBuffer(11 * sizeof(float));
+ ringBuffer.SetSampleFormat(AUDIO_FORMAT_FLOAT32);
+
+ EXPECT_TRUE(ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 10u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 0u);
+
+ uint32_t rv = ringBuffer.WriteSilence(4);
+ EXPECT_EQ(rv, 4u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 6u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 4u);
+
+ float in[4] = {.1, .2, .3, .4};
+ rv = ringBuffer.Write(Span(in, 4));
+ EXPECT_EQ(rv, 4u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 2u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 8u);
+
+ rv = ringBuffer.WriteSilence(4);
+ EXPECT_EQ(rv, 2u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 0u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 10u);
+
+ rv = ringBuffer.Write(Span(in, 4));
+ EXPECT_EQ(rv, 0u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 0u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 10u);
+
+ float out[4] = {};
+ rv = ringBuffer.Read(Span(out, 4));
+ EXPECT_EQ(rv, 4u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 4u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 6u);
+ for (float f : out) {
+ EXPECT_FLOAT_EQ(f, 0.0);
+ }
+
+ rv = ringBuffer.Read(Span(out, 4));
+ EXPECT_EQ(rv, 4u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 8u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 2u);
+ for (uint32_t i = 0; i < 4; ++i) {
+ EXPECT_FLOAT_EQ(in[i], out[i]);
+ }
+
+ rv = ringBuffer.Read(Span(out, 4));
+ EXPECT_EQ(rv, 2u);
+ EXPECT_TRUE(ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 10u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 0u);
+ for (uint32_t i = 0; i < 2; ++i) {
+ EXPECT_FLOAT_EQ(out[i], 0.0);
+ }
+
+ rv = ringBuffer.Clear();
+ EXPECT_EQ(rv, 0u);
+ EXPECT_TRUE(ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 10u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 0u);
+}
+
+TEST(TestAudioRingBuffer, BasicShort)
+{
+ AudioRingBuffer ringBuffer(11 * sizeof(short));
+ ringBuffer.SetSampleFormat(AUDIO_FORMAT_S16);
+
+ EXPECT_TRUE(ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 10u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 0u);
+
+ uint32_t rv = ringBuffer.WriteSilence(4);
+ EXPECT_EQ(rv, 4u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 6u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 4u);
+
+ short in[4] = {1, 2, 3, 4};
+ rv = ringBuffer.Write(Span(in, 4));
+ EXPECT_EQ(rv, 4u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 2u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 8u);
+
+ rv = ringBuffer.WriteSilence(4);
+ EXPECT_EQ(rv, 2u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 0u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 10u);
+
+ rv = ringBuffer.Write(Span(in, 4));
+ EXPECT_EQ(rv, 0u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 0u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 10u);
+
+ short out[4] = {};
+ rv = ringBuffer.Read(Span(out, 4));
+ EXPECT_EQ(rv, 4u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 4u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 6u);
+ for (float f : out) {
+ EXPECT_EQ(f, 0);
+ }
+
+ rv = ringBuffer.Read(Span(out, 4));
+ EXPECT_EQ(rv, 4u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 8u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 2u);
+ for (uint32_t i = 0; i < 4; ++i) {
+ EXPECT_EQ(in[i], out[i]);
+ }
+
+ rv = ringBuffer.Read(Span(out, 4));
+ EXPECT_EQ(rv, 2u);
+ EXPECT_TRUE(ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 10u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 0u);
+ for (uint32_t i = 0; i < 2; ++i) {
+ EXPECT_EQ(out[i], 0);
+ }
+
+ rv = ringBuffer.Clear();
+ EXPECT_EQ(rv, 0u);
+ EXPECT_TRUE(ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 10u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 0u);
+}
+
+TEST(TestAudioRingBuffer, BasicFloat2)
+{
+ AudioRingBuffer ringBuffer(11 * sizeof(float));
+ ringBuffer.SetSampleFormat(AUDIO_FORMAT_FLOAT32);
+
+ EXPECT_TRUE(ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 10u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 0u);
+
+ float in[4] = {.1, .2, .3, .4};
+ uint32_t rv = ringBuffer.Write(Span(in, 4));
+ EXPECT_EQ(rv, 4u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 6u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 4u);
+
+ rv = ringBuffer.Write(Span(in, 4));
+ EXPECT_EQ(rv, 4u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 2u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 8u);
+
+ float out[4] = {};
+ rv = ringBuffer.Read(Span(out, 4));
+ EXPECT_EQ(rv, 4u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 6u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 4u);
+ for (uint32_t i = 0; i < 4; ++i) {
+ EXPECT_FLOAT_EQ(in[i], out[i]);
+ }
+
+ // WriteIndex = 12
+ rv = ringBuffer.Write(Span(in, 4));
+ EXPECT_EQ(rv, 4u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 2u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 8u);
+
+ rv = ringBuffer.Read(Span(out, 4));
+ EXPECT_EQ(rv, 4u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 6u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 4u);
+ for (uint32_t i = 0; i < 4; ++i) {
+ EXPECT_FLOAT_EQ(in[i], out[i]);
+ }
+
+ rv = ringBuffer.Read(Span(out, 8));
+ EXPECT_EQ(rv, 4u);
+ EXPECT_TRUE(ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 10u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 0u);
+ for (uint32_t i = 0; i < 4; ++i) {
+ EXPECT_FLOAT_EQ(in[i], out[i]);
+ }
+
+ rv = ringBuffer.Read(Span(out, 8));
+ EXPECT_EQ(rv, 0u);
+ EXPECT_TRUE(ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 10u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 0u);
+ for (uint32_t i = 0; i < 4; ++i) {
+ EXPECT_FLOAT_EQ(in[i], out[i]);
+ }
+
+ // WriteIndex = 16
+ rv = ringBuffer.Write(Span(in, 4));
+ EXPECT_EQ(rv, 4u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 6u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 4u);
+
+ rv = ringBuffer.Write(Span(in, 4));
+ EXPECT_EQ(rv, 4u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 2u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 8u);
+
+ rv = ringBuffer.Write(Span(in, 4));
+ EXPECT_EQ(rv, 2u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 0u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 10u);
+
+ rv = ringBuffer.Write(Span(in, 4));
+ EXPECT_EQ(rv, 0u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 0u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 10u);
+}
+
+TEST(TestAudioRingBuffer, BasicShort2)
+{
+ AudioRingBuffer ringBuffer(11 * sizeof(int16_t));
+ ringBuffer.SetSampleFormat(AUDIO_FORMAT_S16);
+
+ EXPECT_TRUE(ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 10u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 0u);
+
+ int16_t in[4] = {1, 2, 3, 4};
+ uint32_t rv = ringBuffer.Write(Span(in, 4));
+ EXPECT_EQ(rv, 4u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 6u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 4u);
+
+ rv = ringBuffer.Write(Span(in, 4));
+ EXPECT_EQ(rv, 4u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 2u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 8u);
+
+ int16_t out[4] = {};
+ rv = ringBuffer.Read(Span(out, 4));
+ EXPECT_EQ(rv, 4u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 6u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 4u);
+ for (uint32_t i = 0; i < 4; ++i) {
+ EXPECT_EQ(in[i], out[i]);
+ }
+
+ // WriteIndex = 12
+ rv = ringBuffer.Write(Span(in, 4));
+ EXPECT_EQ(rv, 4u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 2u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 8u);
+
+ rv = ringBuffer.Read(Span(out, 4));
+ EXPECT_EQ(rv, 4u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 6u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 4u);
+ for (uint32_t i = 0; i < 4; ++i) {
+ EXPECT_EQ(in[i], out[i]);
+ }
+
+ rv = ringBuffer.Read(Span(out, 8));
+ EXPECT_EQ(rv, 4u);
+ EXPECT_TRUE(ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 10u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 0u);
+ for (uint32_t i = 0; i < 4; ++i) {
+ EXPECT_EQ(in[i], out[i]);
+ }
+
+ rv = ringBuffer.Read(Span(out, 8));
+ EXPECT_EQ(rv, 0u);
+ EXPECT_TRUE(ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 10u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 0u);
+ for (uint32_t i = 0; i < 4; ++i) {
+ EXPECT_EQ(in[i], out[i]);
+ }
+
+ // WriteIndex = 16
+ rv = ringBuffer.Write(Span(in, 4));
+ EXPECT_EQ(rv, 4u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 6u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 4u);
+
+ rv = ringBuffer.Write(Span(in, 4));
+ EXPECT_EQ(rv, 4u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 2u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 8u);
+
+ rv = ringBuffer.Write(Span(in, 4));
+ EXPECT_EQ(rv, 2u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 0u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 10u);
+
+ rv = ringBuffer.Write(Span(in, 4));
+ EXPECT_EQ(rv, 0u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 0u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 10u);
+}
+
+TEST(TestAudioRingBuffer, NoCopyFloat)
+{
+ AudioRingBuffer ringBuffer(11 * sizeof(float));
+ ringBuffer.SetSampleFormat(AUDIO_FORMAT_FLOAT32);
+
+ float in[8] = {.0, .1, .2, .3, .4, .5, .6, .7};
+ ringBuffer.Write(Span(in, 6));
+ // v ReadIndex
+ // [x0: .0, x1: .1, x2: .2, x3: .3, x4: .4,
+ // x5: .5, x6: .0, x7: .0, x8: .0, x9: .0, x10: .0]
+
+ float out[10] = {};
+ float* out_ptr = out;
+
+ uint32_t rv =
+ ringBuffer.ReadNoCopy([&out_ptr](const Span<const float> aInBuffer) {
+ PodMove(out_ptr, aInBuffer.data(), aInBuffer.Length());
+ out_ptr += aInBuffer.Length();
+ return aInBuffer.Length();
+ });
+ EXPECT_EQ(rv, 6u);
+ EXPECT_TRUE(ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 10u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 0u);
+ for (uint32_t i = 0; i < rv; ++i) {
+ EXPECT_FLOAT_EQ(out[i], in[i]);
+ }
+
+ ringBuffer.Write(Span(in, 8));
+ // Now the buffer contains:
+ // [x0: .5, x1: .6, x2: .2, x3: .3, x4: .4,
+ // x5: .5, x6: .0, x7: .1, x8: .2, x9: .3, x10: .4
+ // ^ ReadIndex
+ out_ptr = out; // reset the pointer before lambdas reuse
+ rv = ringBuffer.ReadNoCopy([&out_ptr](const Span<const float> aInBuffer) {
+ PodMove(out_ptr, aInBuffer.data(), aInBuffer.Length());
+ out_ptr += aInBuffer.Length();
+ return aInBuffer.Length();
+ });
+ EXPECT_EQ(rv, 8u);
+ EXPECT_TRUE(ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 10u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 0u);
+ for (uint32_t i = 0; i < rv; ++i) {
+ EXPECT_FLOAT_EQ(out[i], in[i]);
+ }
+}
+
+TEST(TestAudioRingBuffer, NoCopyShort)
+{
+ AudioRingBuffer ringBuffer(11 * sizeof(short));
+ ringBuffer.SetSampleFormat(AUDIO_FORMAT_S16);
+
+ short in[8] = {0, 1, 2, 3, 4, 5, 6, 7};
+ ringBuffer.Write(Span(in, 6));
+ // v ReadIndex
+ // [x0: 0, x1: 1, x2: 2, x3: 3, x4: 4,
+ // x5: 5, x6: 0, x7: 0, x8: 0, x9: 0, x10: 0]
+
+ short out[10] = {};
+ short* out_ptr = out;
+
+ uint32_t rv =
+ ringBuffer.ReadNoCopy([&out_ptr](const Span<const short> aInBuffer) {
+ PodMove(out_ptr, aInBuffer.data(), aInBuffer.Length());
+ out_ptr += aInBuffer.Length();
+ return aInBuffer.Length();
+ });
+ EXPECT_EQ(rv, 6u);
+ EXPECT_TRUE(ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 10u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 0u);
+ for (uint32_t i = 0; i < rv; ++i) {
+ EXPECT_EQ(out[i], in[i]);
+ }
+
+ ringBuffer.Write(Span(in, 8));
+ // Now the buffer contains:
+ // [x0: 5, x1: 6, x2: 2, x3: 3, x4: 4,
+ // x5: 5, x6: 0, x7: 1, x8: 2, x9: 3, x10: 4
+ // ^ ReadIndex
+ out_ptr = out; // reset the pointer before lambdas reuse
+ rv = ringBuffer.ReadNoCopy([&out_ptr](const Span<const short> aInBuffer) {
+ PodMove(out_ptr, aInBuffer.data(), aInBuffer.Length());
+ out_ptr += aInBuffer.Length();
+ return aInBuffer.Length();
+ });
+ EXPECT_EQ(rv, 8u);
+ EXPECT_TRUE(ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 10u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 0u);
+ for (uint32_t i = 0; i < rv; ++i) {
+ EXPECT_EQ(out[i], in[i]);
+ }
+}
+
+TEST(TestAudioRingBuffer, NoCopyFloat2)
+{
+ AudioRingBuffer ringBuffer(11 * sizeof(float));
+ ringBuffer.SetSampleFormat(AUDIO_FORMAT_FLOAT32);
+
+ float in[8] = {.0, .1, .2, .3, .4, .5, .6, .7};
+ ringBuffer.Write(Span(in, 6));
+ // v ReadIndex
+ // [x0: .0, x1: .1, x2: .2, x3: .3, x4: .4,
+ // x5: .5, x6: .0, x7: .0, x8: .0, x9: .0, x10: .0]
+
+ float out[10] = {};
+ float* out_ptr = out;
+ uint32_t total_frames = 3;
+
+ uint32_t rv = ringBuffer.ReadNoCopy(
+ [&out_ptr, &total_frames](const Span<const float>& aInBuffer) {
+ uint32_t inFramesUsed =
+ std::min<uint32_t>(total_frames, aInBuffer.Length());
+ PodMove(out_ptr, aInBuffer.data(), inFramesUsed);
+ out_ptr += inFramesUsed;
+ total_frames -= inFramesUsed;
+ return inFramesUsed;
+ });
+ // v ReadIndex
+ // [x0: .0, x1: .1, x2: .2, x3: .3, x4: .4,
+ // x5: .5, x6: .0, x7: .0, x8: .0, x9: .0, x10: .0]
+ EXPECT_EQ(rv, 3u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 7u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 3u);
+ for (uint32_t i = 0; i < rv; ++i) {
+ EXPECT_FLOAT_EQ(out[i], in[i]);
+ }
+
+ total_frames = 3;
+ rv = ringBuffer.ReadNoCopy(
+ [&out_ptr, &total_frames](const Span<const float>& aInBuffer) {
+ uint32_t inFramesUsed =
+ std::min<uint32_t>(total_frames, aInBuffer.Length());
+ PodMove(out_ptr, aInBuffer.data(), inFramesUsed);
+ out_ptr += inFramesUsed;
+ total_frames -= inFramesUsed;
+ return inFramesUsed;
+ });
+ // [x0: .0, x1: .1, x2: .2, x3: .3, x4: .4,
+ // x5: .5, x6: .0, x7: .0, x8: .0, x9: .0, x10: .0]
+ // ^ ReadIndex
+ EXPECT_EQ(rv, 3u);
+ EXPECT_TRUE(ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 10u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 0u);
+ for (uint32_t i = 0; i < rv; ++i) {
+ EXPECT_FLOAT_EQ(out[i + 3], in[i + 3]);
+ }
+
+ ringBuffer.Write(Span(in, 8));
+ // Now the buffer contains:
+ // [x0: .5, x1: .6, x2: .7, x3: .3, x4: .4,
+ // x5: .5, x6: .0, x7: .1, x8: .2, x9: .3, x10: .4
+ // ^ ReadIndex
+
+ // reset the pointer before lambdas reuse
+ out_ptr = out;
+ total_frames = 3;
+ rv = ringBuffer.ReadNoCopy(
+ [&out_ptr, &total_frames](const Span<const float>& aInBuffer) {
+ uint32_t inFramesUsed =
+ std::min<uint32_t>(total_frames, aInBuffer.Length());
+ PodMove(out_ptr, aInBuffer.data(), inFramesUsed);
+ out_ptr += inFramesUsed;
+ total_frames -= inFramesUsed;
+ return inFramesUsed;
+ });
+ // Now the buffer contains:
+ // [x0: .5, x1: .6, x2: .2, x3: .3, x4: .4,
+ // x5: .5, x6: .0, x7: .1, x8: .2, x9: .3, x10: .4
+ // ^ ReadIndex
+ EXPECT_EQ(rv, 3u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 5u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 5u);
+ for (uint32_t i = 0; i < rv; ++i) {
+ EXPECT_FLOAT_EQ(out[i], in[i]);
+ }
+
+ total_frames = 3;
+ rv = ringBuffer.ReadNoCopy(
+ [&out_ptr, &total_frames](const Span<const float>& aInBuffer) {
+ uint32_t inFramesUsed =
+ std::min<uint32_t>(total_frames, aInBuffer.Length());
+ PodMove(out_ptr, aInBuffer.data(), inFramesUsed);
+ out_ptr += inFramesUsed;
+ total_frames -= inFramesUsed;
+ return inFramesUsed;
+ });
+ // Now the buffer contains:
+ // v ReadIndex
+ // [x0: .5, x1: .6, x2: .7, x3: .3, x4: .4,
+ // x5: .5, x6: .0, x7: .1, x8: .2, x9: .3, x10: .4
+ EXPECT_EQ(rv, 3u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 8u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 2u);
+ for (uint32_t i = 0; i < rv; ++i) {
+ EXPECT_FLOAT_EQ(out[i + 3], in[i + 3]);
+ }
+
+ total_frames = 3;
+ rv = ringBuffer.ReadNoCopy(
+ [&out_ptr, &total_frames](const Span<const float>& aInBuffer) {
+ uint32_t inFramesUsed =
+ std::min<uint32_t>(total_frames, aInBuffer.Length());
+ PodMove(out_ptr, aInBuffer.data(), inFramesUsed);
+ out_ptr += inFramesUsed;
+ total_frames -= inFramesUsed;
+ return inFramesUsed;
+ });
+ // Now the buffer contains:
+ // v ReadIndex
+ // [x0: .5, x1: .6, x2: .7, x3: .3, x4: .4,
+ // x5: .5, x6: .0, x7: .1, x8: .2, x9: .3, x10: .4
+ EXPECT_EQ(rv, 2u);
+ EXPECT_EQ(total_frames, 1u);
+ EXPECT_TRUE(ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 10u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 0u);
+ for (uint32_t i = 0; i < rv; ++i) {
+ EXPECT_FLOAT_EQ(out[i + 6], in[i + 6]);
+ }
+}
+
+TEST(TestAudioRingBuffer, NoCopyShort2)
+{
+ AudioRingBuffer ringBuffer(11 * sizeof(short));
+ ringBuffer.SetSampleFormat(AUDIO_FORMAT_S16);
+
+ short in[8] = {0, 1, 2, 3, 4, 5, 6, 7};
+ ringBuffer.Write(Span(in, 6));
+ // v ReadIndex
+ // [x0: 0, x1: 1, x2: 2, x3: 3, x4: 4,
+ // x5: 5, x6: 0, x7: 0, x8: 0, x9: 0, x10: 0]
+
+ short out[10] = {};
+ short* out_ptr = out;
+ uint32_t total_frames = 3;
+
+ uint32_t rv = ringBuffer.ReadNoCopy(
+ [&out_ptr, &total_frames](const Span<const short>& aInBuffer) {
+ uint32_t inFramesUsed =
+ std::min<uint32_t>(total_frames, aInBuffer.Length());
+ PodMove(out_ptr, aInBuffer.data(), inFramesUsed);
+ out_ptr += inFramesUsed;
+ total_frames -= inFramesUsed;
+ return inFramesUsed;
+ });
+ // v ReadIndex
+ // [x0: 0, x1: 1, x2: 2, x3: 3, x4: 4,
+ // x5: 5, x6: 0, x7: 0, x8: 0, x9: 0, x10: 0]
+ EXPECT_EQ(rv, 3u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 7u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 3u);
+ for (uint32_t i = 0; i < rv; ++i) {
+ EXPECT_EQ(out[i], in[i]);
+ }
+
+ total_frames = 3;
+ rv = ringBuffer.ReadNoCopy(
+ [&out_ptr, &total_frames](const Span<const short>& aInBuffer) {
+ uint32_t inFramesUsed =
+ std::min<uint32_t>(total_frames, aInBuffer.Length());
+ PodMove(out_ptr, aInBuffer.data(), inFramesUsed);
+ out_ptr += inFramesUsed;
+ total_frames -= inFramesUsed;
+ return inFramesUsed;
+ });
+ // [x0: 0, x1: 1, x2: 2, x3: 3, x4: 4,
+ // x5: 5, x6: 0, x7: 0, x8: 0, x9: 0, x10: .0]
+ // ^ ReadIndex
+ EXPECT_EQ(rv, 3u);
+ EXPECT_TRUE(ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 10u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 0u);
+ for (uint32_t i = 0; i < rv; ++i) {
+ EXPECT_EQ(out[i + 3], in[i + 3]);
+ }
+
+ ringBuffer.Write(Span(in, 8));
+ // Now the buffer contains:
+ // [x0: 5, x1: 6, x2: 7, x3: 3, x4: 4,
+ // x5: 5, x6: 0, x7: 1, x8: 2, x9: 3, x10: 4
+ // ^ ReadIndex
+
+ // reset the pointer before lambdas reuse
+ out_ptr = out;
+ total_frames = 3;
+ rv = ringBuffer.ReadNoCopy(
+ [&out_ptr, &total_frames](const Span<const short>& aInBuffer) {
+ uint32_t inFramesUsed =
+ std::min<uint32_t>(total_frames, aInBuffer.Length());
+ PodMove(out_ptr, aInBuffer.data(), inFramesUsed);
+ out_ptr += inFramesUsed;
+ total_frames -= inFramesUsed;
+ return inFramesUsed;
+ });
+ // Now the buffer contains:
+ // [x0: 5, x1: 6, x2: 2, x3: 3, x4: 4,
+ // x5: 5, x6: 0, x7: 1, x8: 2, x9: 3, x10: 4
+ // ^ ReadIndex
+ EXPECT_EQ(rv, 3u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 5u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 5u);
+ for (uint32_t i = 0; i < rv; ++i) {
+ EXPECT_EQ(out[i], in[i]);
+ }
+
+ total_frames = 3;
+ rv = ringBuffer.ReadNoCopy(
+ [&out_ptr, &total_frames](const Span<const short>& aInBuffer) {
+ uint32_t inFramesUsed =
+ std::min<uint32_t>(total_frames, aInBuffer.Length());
+ PodMove(out_ptr, aInBuffer.data(), inFramesUsed);
+ out_ptr += inFramesUsed;
+ total_frames -= inFramesUsed;
+ return inFramesUsed;
+ });
+ // Now the buffer contains:
+ // v ReadIndex
+ // [x0: 5, x1: 6, x2: 7, x3: 3, x4: 4,
+ // x5: 5, x6: 0, x7: 1, x8: 2, x9: 3, x10: 4
+ EXPECT_EQ(rv, 3u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 8u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 2u);
+ for (uint32_t i = 0; i < rv; ++i) {
+ EXPECT_EQ(out[i + 3], in[i + 3]);
+ }
+
+ total_frames = 3;
+ rv = ringBuffer.ReadNoCopy(
+ [&out_ptr, &total_frames](const Span<const short>& aInBuffer) {
+ uint32_t inFramesUsed =
+ std::min<uint32_t>(total_frames, aInBuffer.Length());
+ PodMove(out_ptr, aInBuffer.data(), inFramesUsed);
+ out_ptr += inFramesUsed;
+ total_frames -= inFramesUsed;
+ return inFramesUsed;
+ });
+ // Now the buffer contains:
+ // v ReadIndex
+ // [x0: 5, x1: 6, x2: 7, x3: 3, x4: 4,
+ // x5: 5, x6: 0, x7: 1, x8: 2, x9: 3, x10: 4
+ EXPECT_EQ(rv, 2u);
+ EXPECT_EQ(total_frames, 1u);
+ EXPECT_TRUE(ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 10u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 0u);
+ for (uint32_t i = 0; i < rv; ++i) {
+ EXPECT_EQ(out[i + 6], in[i + 6]);
+ }
+}
+
+TEST(TestAudioRingBuffer, DiscardFloat)
+{
+ AudioRingBuffer ringBuffer(11 * sizeof(float));
+ ringBuffer.SetSampleFormat(AUDIO_FORMAT_FLOAT32);
+
+ float in[8] = {.0, .1, .2, .3, .4, .5, .6, .7};
+ ringBuffer.Write(Span(in, 8));
+
+ uint32_t rv = ringBuffer.Discard(3);
+ EXPECT_EQ(rv, 3u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 5u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 5u);
+
+ float out[8] = {};
+ rv = ringBuffer.Read(Span(out, 3));
+ EXPECT_EQ(rv, 3u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 8u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 2u);
+ for (uint32_t i = 0; i < rv; ++i) {
+ EXPECT_FLOAT_EQ(out[i], in[i + 3]);
+ }
+
+ rv = ringBuffer.Discard(3);
+ EXPECT_EQ(rv, 2u);
+ EXPECT_TRUE(ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 10u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 0u);
+
+ ringBuffer.WriteSilence(4);
+ rv = ringBuffer.Discard(6);
+ EXPECT_EQ(rv, 4u);
+ EXPECT_TRUE(ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 10u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 0u);
+}
+
+TEST(TestAudioRingBuffer, DiscardShort)
+{
+ AudioRingBuffer ringBuffer(11 * sizeof(short));
+ ringBuffer.SetSampleFormat(AUDIO_FORMAT_S16);
+
+ short in[8] = {0, 1, 2, 3, 4, 5, 6, 7};
+ ringBuffer.Write(Span(in, 8));
+
+ uint32_t rv = ringBuffer.Discard(3);
+ EXPECT_EQ(rv, 3u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 5u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 5u);
+
+ short out[8] = {};
+ rv = ringBuffer.Read(Span(out, 3));
+ EXPECT_EQ(rv, 3u);
+ EXPECT_TRUE(!ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 8u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 2u);
+ for (uint32_t i = 0; i < rv; ++i) {
+ EXPECT_EQ(out[i], in[i + 3]);
+ }
+
+ rv = ringBuffer.Discard(3);
+ EXPECT_EQ(rv, 2u);
+ EXPECT_TRUE(ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 10u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 0u);
+
+ ringBuffer.WriteSilence(4);
+ rv = ringBuffer.Discard(6);
+ EXPECT_EQ(rv, 4u);
+ EXPECT_TRUE(ringBuffer.IsEmpty());
+ EXPECT_TRUE(!ringBuffer.IsFull());
+ EXPECT_EQ(ringBuffer.AvailableWrite(), 10u);
+ EXPECT_EQ(ringBuffer.AvailableRead(), 0u);
+}
+
+TEST(TestRingBuffer, WriteFromRing1)
+{
+ AudioRingBuffer ringBuffer1(11 * sizeof(float));
+ ringBuffer1.SetSampleFormat(AUDIO_FORMAT_FLOAT32);
+ AudioRingBuffer ringBuffer2(11 * sizeof(float));
+ ringBuffer2.SetSampleFormat(AUDIO_FORMAT_FLOAT32);
+
+ float in[4] = {.1, .2, .3, .4};
+ uint32_t rv = ringBuffer1.Write(Span<const float>(in, 4));
+ EXPECT_EQ(rv, 4u);
+
+ EXPECT_EQ(ringBuffer2.AvailableRead(), 0u);
+ rv = ringBuffer2.Write(ringBuffer1, 4);
+ EXPECT_EQ(rv, 4u);
+ EXPECT_EQ(ringBuffer2.AvailableRead(), 4u);
+
+ float out[4] = {};
+ rv = ringBuffer2.Read(Span<float>(out, 4));
+ EXPECT_EQ(rv, 4u);
+ for (uint32_t i = 0; i < 4; ++i) {
+ EXPECT_FLOAT_EQ(in[i], out[i]);
+ }
+}
+
+TEST(TestRingBuffer, WriteFromRing2)
+{
+ AudioRingBuffer ringBuffer1(11 * sizeof(float));
+ ringBuffer1.SetSampleFormat(AUDIO_FORMAT_FLOAT32);
+ AudioRingBuffer ringBuffer2(11 * sizeof(float));
+ ringBuffer2.SetSampleFormat(AUDIO_FORMAT_FLOAT32);
+
+ // Advance the index
+ ringBuffer2.WriteSilence(8);
+ ringBuffer2.Clear();
+
+ float in[4] = {.1, .2, .3, .4};
+ uint32_t rv = ringBuffer1.Write(Span<const float>(in, 4));
+ EXPECT_EQ(rv, 4u);
+ rv = ringBuffer2.Write(ringBuffer1, 4);
+ EXPECT_EQ(rv, 4u);
+ EXPECT_EQ(ringBuffer2.AvailableRead(), 4u);
+
+ float out[4] = {};
+ rv = ringBuffer2.Read(Span<float>(out, 4));
+ EXPECT_EQ(rv, 4u);
+ for (uint32_t i = 0; i < 4; ++i) {
+ EXPECT_FLOAT_EQ(in[i], out[i]);
+ }
+}
+
+TEST(TestRingBuffer, WriteFromRing3)
+{
+ AudioRingBuffer ringBuffer1(11 * sizeof(float));
+ ringBuffer1.SetSampleFormat(AUDIO_FORMAT_FLOAT32);
+ AudioRingBuffer ringBuffer2(11 * sizeof(float));
+ ringBuffer2.SetSampleFormat(AUDIO_FORMAT_FLOAT32);
+
+ // Advance the index
+ ringBuffer2.WriteSilence(8);
+ ringBuffer2.Clear();
+ ringBuffer2.WriteSilence(4);
+ ringBuffer2.Clear();
+
+ float in[4] = {.1, .2, .3, .4};
+ uint32_t rv = ringBuffer1.Write(Span<const float>(in, 4));
+ EXPECT_EQ(rv, 4u);
+ rv = ringBuffer2.Write(ringBuffer1, 4);
+ EXPECT_EQ(rv, 4u);
+ EXPECT_EQ(ringBuffer2.AvailableRead(), 4u);
+
+ float out[4] = {};
+ rv = ringBuffer2.Read(Span<float>(out, 4));
+ EXPECT_EQ(rv, 4u);
+ for (uint32_t i = 0; i < 4; ++i) {
+ EXPECT_FLOAT_EQ(in[i], out[i]);
+ }
+}
+
+TEST(TestAudioRingBuffer, WriteFromRingShort)
+{
+ AudioRingBuffer ringBuffer1(11 * sizeof(short));
+ ringBuffer1.SetSampleFormat(AUDIO_FORMAT_S16);
+
+ short in[8] = {0, 1, 2, 3, 4, 5, 6, 7};
+ uint32_t rv = ringBuffer1.Write(Span(in, 8));
+ EXPECT_EQ(rv, 8u);
+
+ AudioRingBuffer ringBuffer2(11 * sizeof(short));
+ ringBuffer2.SetSampleFormat(AUDIO_FORMAT_S16);
+
+ rv = ringBuffer2.Write(ringBuffer1, 4);
+ EXPECT_EQ(rv, 4u);
+ EXPECT_EQ(ringBuffer2.AvailableRead(), 4u);
+ EXPECT_EQ(ringBuffer1.AvailableRead(), 8u);
+
+ short out[4] = {};
+ rv = ringBuffer2.Read(Span(out, 4));
+ for (uint32_t i = 0; i < rv; ++i) {
+ EXPECT_EQ(out[i], in[i]);
+ }
+
+ rv = ringBuffer2.Write(ringBuffer1, 4);
+ EXPECT_EQ(rv, 4u);
+ EXPECT_EQ(ringBuffer2.AvailableRead(), 4u);
+ EXPECT_EQ(ringBuffer1.AvailableRead(), 8u);
+
+ ringBuffer1.Discard(4);
+ rv = ringBuffer2.Write(ringBuffer1, 4);
+ EXPECT_EQ(rv, 4u);
+ EXPECT_EQ(ringBuffer2.AvailableRead(), 8u);
+ EXPECT_EQ(ringBuffer1.AvailableRead(), 4u);
+
+ short out2[8] = {};
+ rv = ringBuffer2.Read(Span(out2, 8));
+ for (uint32_t i = 0; i < rv; ++i) {
+ EXPECT_EQ(out2[i], in[i]);
+ }
+}
+
+TEST(TestAudioRingBuffer, WriteFromRingFloat)
+{
+ AudioRingBuffer ringBuffer1(11 * sizeof(float));
+ ringBuffer1.SetSampleFormat(AUDIO_FORMAT_FLOAT32);
+
+ float in[8] = {.0, .1, .2, .3, .4, .5, .6, .7};
+ uint32_t rv = ringBuffer1.Write(Span(in, 8));
+ EXPECT_EQ(rv, 8u);
+
+ AudioRingBuffer ringBuffer2(11 * sizeof(float));
+ ringBuffer2.SetSampleFormat(AUDIO_FORMAT_FLOAT32);
+
+ rv = ringBuffer2.Write(ringBuffer1, 4);
+ EXPECT_EQ(rv, 4u);
+ EXPECT_EQ(ringBuffer2.AvailableRead(), 4u);
+ EXPECT_EQ(ringBuffer1.AvailableRead(), 8u);
+
+ float out[4] = {};
+ rv = ringBuffer2.Read(Span(out, 4));
+ for (uint32_t i = 0; i < rv; ++i) {
+ EXPECT_FLOAT_EQ(out[i], in[i]);
+ }
+
+ rv = ringBuffer2.Write(ringBuffer1, 4);
+ EXPECT_EQ(rv, 4u);
+ EXPECT_EQ(ringBuffer2.AvailableRead(), 4u);
+ EXPECT_EQ(ringBuffer1.AvailableRead(), 8u);
+
+ ringBuffer1.Discard(4);
+ rv = ringBuffer2.Write(ringBuffer1, 4);
+ EXPECT_EQ(rv, 4u);
+ EXPECT_EQ(ringBuffer2.AvailableRead(), 8u);
+ EXPECT_EQ(ringBuffer1.AvailableRead(), 4u);
+
+ float out2[8] = {};
+ rv = ringBuffer2.Read(Span(out2, 8));
+ for (uint32_t i = 0; i < rv; ++i) {
+ EXPECT_FLOAT_EQ(out2[i], in[i]);
+ }
+}
diff --git a/dom/media/gtest/TestAudioSegment.cpp b/dom/media/gtest/TestAudioSegment.cpp
new file mode 100644
index 0000000000..64366045ca
--- /dev/null
+++ b/dom/media/gtest/TestAudioSegment.cpp
@@ -0,0 +1,470 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioSegment.h"
+#include <iostream>
+#include "gtest/gtest.h"
+
+#include "AudioGenerator.h"
+
+using namespace mozilla;
+
+namespace audio_segment {
+
+/* Helper function to give us the maximum and minimum value that don't clip,
+ * for a given sample format (integer or floating-point). */
+template <typename T>
+T GetLowValue();
+
+template <typename T>
+T GetHighValue();
+
+template <typename T>
+T GetSilentValue();
+
+template <>
+float GetLowValue<float>() {
+ return -1.0;
+}
+
+template <>
+int16_t GetLowValue<short>() {
+ return -INT16_MAX;
+}
+
+template <>
+float GetHighValue<float>() {
+ return 1.0;
+}
+
+template <>
+int16_t GetHighValue<short>() {
+ return INT16_MAX;
+}
+
+template <>
+float GetSilentValue() {
+ return 0.0;
+}
+
+template <>
+int16_t GetSilentValue() {
+ return 0;
+}
+
+// Get an array of planar audio buffers that has the inverse of the index of the
+// channel (1-indexed) as samples.
+template <typename T>
+const T* const* GetPlanarChannelArray(size_t aChannels, size_t aSize) {
+ T** channels = new T*[aChannels];
+ for (size_t c = 0; c < aChannels; c++) {
+ channels[c] = new T[aSize];
+ for (size_t i = 0; i < aSize; i++) {
+ channels[c][i] = FloatToAudioSample<T>(1. / (c + 1));
+ }
+ }
+ return channels;
+}
+
+template <typename T>
+void DeletePlanarChannelsArray(const T* const* aArrays, size_t aChannels) {
+ for (size_t channel = 0; channel < aChannels; channel++) {
+ delete[] aArrays[channel];
+ }
+ delete[] aArrays;
+}
+
+template <typename T>
+T** GetPlanarArray(size_t aChannels, size_t aSize) {
+ T** channels = new T*[aChannels];
+ for (size_t c = 0; c < aChannels; c++) {
+ channels[c] = new T[aSize];
+ for (size_t i = 0; i < aSize; i++) {
+ channels[c][i] = 0.0f;
+ }
+ }
+ return channels;
+}
+
+template <typename T>
+void DeletePlanarArray(T** aArrays, size_t aChannels) {
+ for (size_t channel = 0; channel < aChannels; channel++) {
+ delete[] aArrays[channel];
+ }
+ delete[] aArrays;
+}
+
+// Get an array of audio samples that have the inverse of the index of the
+// channel (1-indexed) as samples.
+template <typename T>
+const T* GetInterleavedChannelArray(size_t aChannels, size_t aSize) {
+ size_t sampleCount = aChannels * aSize;
+ T* samples = new T[sampleCount];
+ for (size_t i = 0; i < sampleCount; i++) {
+ uint32_t channel = (i % aChannels) + 1;
+ samples[i] = FloatToAudioSample<T>(1. / channel);
+ }
+ return samples;
+}
+
+template <typename T>
+void DeleteInterleavedChannelArray(const T* aArray) {
+ delete[] aArray;
+}
+
+bool FuzzyEqual(float aLhs, float aRhs) { return std::abs(aLhs - aRhs) < 0.01; }
+
+template <typename SrcT, typename DstT>
+void TestInterleaveAndConvert() {
+ size_t arraySize = 1024;
+ size_t maxChannels = 8; // 7.1
+ for (uint32_t channels = 1; channels < maxChannels; channels++) {
+ const SrcT* const* src = GetPlanarChannelArray<SrcT>(channels, arraySize);
+ DstT* dst = new DstT[channels * arraySize];
+
+ InterleaveAndConvertBuffer(src, arraySize, 1.0, channels, dst);
+
+ uint32_t channelIndex = 0;
+ for (size_t i = 0; i < arraySize * channels; i++) {
+ ASSERT_TRUE(FuzzyEqual(
+ dst[i], FloatToAudioSample<DstT>(1. / (channelIndex + 1))));
+ channelIndex++;
+ channelIndex %= channels;
+ }
+
+ DeletePlanarChannelsArray(src, channels);
+ delete[] dst;
+ }
+}
+
+template <typename SrcT, typename DstT>
+void TestDeinterleaveAndConvert() {
+ size_t arraySize = 1024;
+ size_t maxChannels = 8; // 7.1
+ for (uint32_t channels = 1; channels < maxChannels; channels++) {
+ const SrcT* src = GetInterleavedChannelArray<SrcT>(channels, arraySize);
+ DstT** dst = GetPlanarArray<DstT>(channels, arraySize);
+
+ DeinterleaveAndConvertBuffer(src, arraySize, channels, dst);
+
+ for (size_t channel = 0; channel < channels; channel++) {
+ for (size_t i = 0; i < arraySize; i++) {
+ ASSERT_TRUE(FuzzyEqual(dst[channel][i],
+ FloatToAudioSample<DstT>(1. / (channel + 1))));
+ }
+ }
+
+ DeleteInterleavedChannelArray(src);
+ DeletePlanarArray(dst, channels);
+ }
+}
+
+uint8_t gSilence[4096] = {0};
+
+template <typename T>
+T* SilentChannel() {
+ return reinterpret_cast<T*>(gSilence);
+}
+
+template <typename T>
+void TestUpmixStereo() {
+ size_t arraySize = 1024;
+ nsTArray<T*> channels;
+ nsTArray<const T*> channelsptr;
+
+ channels.SetLength(1);
+ channelsptr.SetLength(1);
+
+ channels[0] = new T[arraySize];
+
+ for (size_t i = 0; i < arraySize; i++) {
+ channels[0][i] = GetHighValue<T>();
+ }
+ channelsptr[0] = channels[0];
+
+ AudioChannelsUpMix(&channelsptr, 2, SilentChannel<T>());
+
+ for (size_t channel = 0; channel < 2; channel++) {
+ for (size_t i = 0; i < arraySize; i++) {
+ ASSERT_TRUE(channelsptr[channel][i] == GetHighValue<T>());
+ }
+ }
+ delete[] channels[0];
+}
+
+template <typename T>
+void TestDownmixStereo() {
+ const size_t arraySize = 1024;
+ nsTArray<const T*> inputptr;
+ nsTArray<T*> input;
+ T** output;
+
+ output = new T*[1];
+ output[0] = new T[arraySize];
+
+ input.SetLength(2);
+ inputptr.SetLength(2);
+
+ for (size_t channel = 0; channel < input.Length(); channel++) {
+ input[channel] = new T[arraySize];
+ for (size_t i = 0; i < arraySize; i++) {
+ input[channel][i] = channel == 0 ? GetLowValue<T>() : GetHighValue<T>();
+ }
+ inputptr[channel] = input[channel];
+ }
+
+ AudioChannelsDownMix(inputptr, output, 1, arraySize);
+
+ for (size_t i = 0; i < arraySize; i++) {
+ ASSERT_TRUE(output[0][i] == GetSilentValue<T>());
+ ASSERT_TRUE(output[0][i] == GetSilentValue<T>());
+ }
+
+ delete[] output[0];
+ delete[] output;
+}
+
+TEST(AudioSegment, Test)
+{
+ TestInterleaveAndConvert<float, float>();
+ TestInterleaveAndConvert<float, int16_t>();
+ TestInterleaveAndConvert<int16_t, float>();
+ TestInterleaveAndConvert<int16_t, int16_t>();
+ TestDeinterleaveAndConvert<float, float>();
+ TestDeinterleaveAndConvert<float, int16_t>();
+ TestDeinterleaveAndConvert<int16_t, float>();
+ TestDeinterleaveAndConvert<int16_t, int16_t>();
+ TestUpmixStereo<float>();
+ TestUpmixStereo<int16_t>();
+ TestDownmixStereo<float>();
+ TestDownmixStereo<int16_t>();
+}
+
+template <class T, uint32_t Channels>
+void fillChunk(AudioChunk* aChunk, int aDuration) {
+ static_assert(Channels != 0, "Filling 0 channels is a no-op");
+
+ aChunk->mDuration = aDuration;
+
+ AutoTArray<nsTArray<T>, Channels> buffer;
+ buffer.SetLength(Channels);
+ aChunk->mChannelData.ClearAndRetainStorage();
+ aChunk->mChannelData.SetCapacity(Channels);
+ for (nsTArray<T>& channel : buffer) {
+ T* ch = channel.AppendElements(aDuration);
+ for (int i = 0; i < aDuration; ++i) {
+ ch[i] = GetHighValue<T>();
+ }
+ aChunk->mChannelData.AppendElement(ch);
+ }
+
+ aChunk->mBuffer = new mozilla::SharedChannelArrayBuffer<T>(std::move(buffer));
+ aChunk->mBufferFormat = AudioSampleTypeToFormat<T>::Format;
+}
+
+TEST(AudioSegment, FlushAfter_ZeroDuration)
+{
+ AudioChunk c;
+ fillChunk<float, 2>(&c, 10);
+
+ AudioSegment s;
+ s.AppendAndConsumeChunk(std::move(c));
+ s.FlushAfter(0);
+ EXPECT_EQ(s.GetDuration(), 0);
+}
+
+TEST(AudioSegment, FlushAfter_SmallerDuration)
+{
+ // It was crashing when the first chunk was silence (null) and FlushAfter
+ // was called for a duration, smaller or equal to the duration of the
+ // first chunk.
+ TrackTime duration = 10;
+ TrackTime smaller_duration = 8;
+ AudioChunk c1;
+ c1.SetNull(duration);
+ AudioChunk c2;
+ fillChunk<float, 2>(&c2, duration);
+
+ AudioSegment s;
+ s.AppendAndConsumeChunk(std::move(c1));
+ s.AppendAndConsumeChunk(std::move(c2));
+ s.FlushAfter(smaller_duration);
+ EXPECT_EQ(s.GetDuration(), smaller_duration) << "Check new duration";
+
+ TrackTime chunkByChunkDuration = 0;
+ for (AudioSegment::ChunkIterator iter(s); !iter.IsEnded(); iter.Next()) {
+ chunkByChunkDuration += iter->GetDuration();
+ }
+ EXPECT_EQ(s.GetDuration(), chunkByChunkDuration)
+ << "Confirm duration chunk by chunk";
+}
+
+TEST(AudioSegment, MemoizedOutputChannelCount)
+{
+ AudioSegment s;
+ EXPECT_EQ(s.MaxChannelCount(), 0U) << "0 channels on init";
+
+ s.AppendNullData(1);
+ EXPECT_EQ(s.MaxChannelCount(), 0U) << "Null data has 0 channels";
+
+ s.Clear();
+ EXPECT_EQ(s.MaxChannelCount(), 0U) << "Still 0 after clearing";
+
+ AudioChunk c1;
+ fillChunk<float, 1>(&c1, 1);
+ s.AppendAndConsumeChunk(std::move(c1));
+ EXPECT_EQ(s.MaxChannelCount(), 1U) << "A single chunk's channel count";
+
+ AudioChunk c2;
+ fillChunk<float, 2>(&c2, 1);
+ s.AppendAndConsumeChunk(std::move(c2));
+ EXPECT_EQ(s.MaxChannelCount(), 2U) << "The max of two chunks' channel count";
+
+ s.ForgetUpTo(2);
+ EXPECT_EQ(s.MaxChannelCount(), 2U) << "Memoized value with null chunks";
+
+ s.Clear();
+ EXPECT_EQ(s.MaxChannelCount(), 2U) << "Still memoized after clearing";
+
+ AudioChunk c3;
+ fillChunk<float, 1>(&c3, 1);
+ s.AppendAndConsumeChunk(std::move(c3));
+ EXPECT_EQ(s.MaxChannelCount(), 1U) << "Real chunk trumps memoized value";
+
+ s.Clear();
+ EXPECT_EQ(s.MaxChannelCount(), 1U) << "Memoized value was updated";
+}
+
+TEST(AudioSegment, AppendAndConsumeChunk)
+{
+ AudioChunk c;
+ fillChunk<float, 2>(&c, 10);
+ AudioChunk temp(c);
+ EXPECT_TRUE(c.mBuffer->IsShared());
+
+ AudioSegment s;
+ s.AppendAndConsumeChunk(std::move(temp));
+ EXPECT_FALSE(s.IsEmpty());
+ EXPECT_TRUE(c.mBuffer->IsShared());
+
+ s.Clear();
+ EXPECT_FALSE(c.mBuffer->IsShared());
+}
+
+TEST(AudioSegment, AppendAndConsumeEmptyChunk)
+{
+ AudioChunk c;
+ AudioSegment s;
+ s.AppendAndConsumeChunk(std::move(c));
+ EXPECT_TRUE(s.IsEmpty());
+}
+
+TEST(AudioSegment, AppendAndConsumeNonEmptyZeroDurationChunk)
+{
+ AudioChunk c;
+ fillChunk<float, 2>(&c, 0);
+ AudioChunk temp(c);
+ EXPECT_TRUE(c.mBuffer->IsShared());
+
+ AudioSegment s;
+ s.AppendAndConsumeChunk(std::move(temp));
+ EXPECT_TRUE(s.IsEmpty());
+ EXPECT_FALSE(c.mBuffer->IsShared());
+}
+
+TEST(AudioSegment, CombineChunksInAppendAndConsumeChunk)
+{
+ AudioChunk source;
+ fillChunk<float, 2>(&source, 10);
+
+ auto checkChunks = [&](const AudioSegment& aSegement,
+ const nsTArray<TrackTime>& aDurations) {
+ size_t i = 0;
+ for (AudioSegment::ConstChunkIterator iter(aSegement); !iter.IsEnded();
+ iter.Next()) {
+ EXPECT_EQ(iter->GetDuration(), aDurations[i++]);
+ }
+ EXPECT_EQ(i, aDurations.Length());
+ };
+
+ // The chunks can be merged if their duration are adjacent.
+ {
+ AudioChunk c1(source);
+ c1.SliceTo(2, 5);
+
+ AudioChunk c2(source);
+ c2.SliceTo(5, 9);
+
+ AudioSegment s;
+ s.AppendAndConsumeChunk(std::move(c1));
+ EXPECT_EQ(s.GetDuration(), 3);
+
+ s.AppendAndConsumeChunk(std::move(c2));
+ EXPECT_EQ(s.GetDuration(), 7);
+
+ checkChunks(s, {7});
+ }
+ // Otherwise, they cannot be merged.
+ {
+ // If durations of chunks are overlapped, they cannot be merged.
+ AudioChunk c1(source);
+ c1.SliceTo(2, 5);
+
+ AudioChunk c2(source);
+ c2.SliceTo(4, 9);
+
+ AudioSegment s;
+ s.AppendAndConsumeChunk(std::move(c1));
+ EXPECT_EQ(s.GetDuration(), 3);
+
+ s.AppendAndConsumeChunk(std::move(c2));
+ EXPECT_EQ(s.GetDuration(), 8);
+
+ checkChunks(s, {3, 5});
+ }
+ {
+ // If durations of chunks are discontinuous, they cannot be merged.
+ AudioChunk c1(source);
+ c1.SliceTo(2, 4);
+
+ AudioChunk c2(source);
+ c2.SliceTo(5, 9);
+
+ AudioSegment s;
+ s.AppendAndConsumeChunk(std::move(c1));
+ EXPECT_EQ(s.GetDuration(), 2);
+
+ s.AppendAndConsumeChunk(std::move(c2));
+ EXPECT_EQ(s.GetDuration(), 6);
+
+ checkChunks(s, {2, 4});
+ }
+}
+
+TEST(AudioSegment, ConvertFromAndToInterleaved)
+{
+ const uint32_t channels = 2;
+ const uint32_t rate = 44100;
+ AudioGenerator<AudioDataValue> generator(channels, rate);
+
+ const size_t frames = 10;
+ const size_t bufferSize = frames * channels;
+ nsTArray<AudioDataValue> buffer(bufferSize);
+ buffer.AppendElements(bufferSize);
+
+ generator.GenerateInterleaved(buffer.Elements(), frames);
+
+ AudioSegment data;
+ data.AppendFromInterleavedBuffer(buffer.Elements(), frames, channels,
+ PRINCIPAL_HANDLE_NONE);
+
+ nsTArray<AudioDataValue> interleaved;
+ size_t sampleCount = data.WriteToInterleavedBuffer(interleaved, channels);
+
+ EXPECT_EQ(sampleCount, bufferSize);
+ EXPECT_EQ(interleaved, buffer);
+}
+
+} // namespace audio_segment
diff --git a/dom/media/gtest/TestAudioTrackEncoder.cpp b/dom/media/gtest/TestAudioTrackEncoder.cpp
new file mode 100644
index 0000000000..e0bfa6a696
--- /dev/null
+++ b/dom/media/gtest/TestAudioTrackEncoder.cpp
@@ -0,0 +1,298 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "OpusTrackEncoder.h"
+
+#include "AudioGenerator.h"
+#include "AudioSampleFormat.h"
+
+using namespace mozilla;
+
+class TestOpusTrackEncoder : public OpusTrackEncoder {
+ public:
+ TestOpusTrackEncoder(TrackRate aTrackRate,
+ MediaQueue<EncodedFrame>& aEncodedDataQueue)
+ : OpusTrackEncoder(aTrackRate, aEncodedDataQueue) {}
+
+ // Return true if it has successfully initialized the Opus encoder.
+ bool TestOpusRawCreation(int aChannels) {
+ if (Init(aChannels) == NS_OK) {
+ if (IsInitialized()) {
+ return true;
+ }
+ }
+ return false;
+ }
+};
+
+static bool TestOpusInit(int aChannels, TrackRate aSamplingRate) {
+ MediaQueue<EncodedFrame> frames;
+ TestOpusTrackEncoder encoder(aSamplingRate, frames);
+ return encoder.TestOpusRawCreation(aChannels);
+}
+
+TEST(OpusAudioTrackEncoder, InitRaw)
+{
+ // Expect false with 0 or negative channels of input signal.
+ EXPECT_FALSE(TestOpusInit(0, 16000));
+ EXPECT_FALSE(TestOpusInit(-1, 16000));
+
+ // The Opus format supports up to 8 channels, and supports multitrack audio up
+ // to 255 channels, but the current implementation supports only mono and
+ // stereo, and downmixes any more than that.
+ // Expect false with channels of input signal exceed the max supported number.
+ EXPECT_FALSE(TestOpusInit(8 + 1, 16000));
+
+ // Should accept channels within valid range.
+ for (int i = 1; i <= 8; i++) {
+ EXPECT_TRUE(TestOpusInit(i, 16000));
+ }
+
+ // Expect false with 0 or negative sampling rate of input signal.
+ EXPECT_FALSE(TestOpusInit(1, 0));
+ EXPECT_FALSE(TestOpusInit(1, -1));
+
+ // Verify sample rate bounds checking.
+ EXPECT_FALSE(TestOpusInit(2, 2000));
+ EXPECT_FALSE(TestOpusInit(2, 4000));
+ EXPECT_FALSE(TestOpusInit(2, 7999));
+ EXPECT_TRUE(TestOpusInit(2, 8000));
+ EXPECT_TRUE(TestOpusInit(2, 192000));
+ EXPECT_FALSE(TestOpusInit(2, 192001));
+ EXPECT_FALSE(TestOpusInit(2, 200000));
+}
+
+TEST(OpusAudioTrackEncoder, Init)
+{
+ {
+ // The encoder does not normally recieve enough info from null data to
+ // init. However, multiple attempts to do so, with sufficiently long
+ // duration segments, should result in a default-init. The first attempt
+ // should never do this though, even if the duration is long:
+ MediaQueue<EncodedFrame> frames;
+ OpusTrackEncoder encoder(48000, frames);
+ AudioSegment segment;
+ segment.AppendNullData(48000 * 100);
+ encoder.TryInit(segment, segment.GetDuration());
+ EXPECT_FALSE(encoder.IsInitialized());
+
+ // Multiple init attempts should result in best effort init:
+ encoder.TryInit(segment, segment.GetDuration());
+ EXPECT_TRUE(encoder.IsInitialized());
+ }
+
+ {
+ // For non-null segments we should init immediately
+ MediaQueue<EncodedFrame> frames;
+ OpusTrackEncoder encoder(48000, frames);
+ AudioSegment segment;
+ AudioGenerator<AudioDataValue> generator(2, 48000);
+ generator.Generate(segment, 1);
+ encoder.TryInit(segment, segment.GetDuration());
+ EXPECT_TRUE(encoder.IsInitialized());
+ }
+
+ {
+ // Test low sample rate bound
+ MediaQueue<EncodedFrame> frames;
+ OpusTrackEncoder encoder(7999, frames);
+ AudioSegment segment;
+ AudioGenerator<AudioDataValue> generator(2, 7999);
+ generator.Generate(segment, 1);
+ encoder.TryInit(segment, segment.GetDuration());
+ EXPECT_FALSE(encoder.IsInitialized());
+ }
+
+ {
+ // Test low sample rate bound
+ MediaQueue<EncodedFrame> frames;
+ OpusTrackEncoder encoder(8000, frames);
+ AudioSegment segment;
+ AudioGenerator<AudioDataValue> generator(2, 8000);
+ generator.Generate(segment, 1);
+ encoder.TryInit(segment, segment.GetDuration());
+ EXPECT_TRUE(encoder.IsInitialized());
+ }
+
+ {
+ // Test high sample rate bound
+ MediaQueue<EncodedFrame> frames;
+ OpusTrackEncoder encoder(192001, frames);
+ AudioSegment segment;
+ AudioGenerator<AudioDataValue> generator(2, 192001);
+ generator.Generate(segment, 1);
+ encoder.TryInit(segment, segment.GetDuration());
+ EXPECT_FALSE(encoder.IsInitialized());
+ }
+
+ {
+ // Test high sample rate bound
+ MediaQueue<EncodedFrame> frames;
+ OpusTrackEncoder encoder(192000, frames);
+ AudioSegment segment;
+ AudioGenerator<AudioDataValue> generator(2, 192000);
+ generator.Generate(segment, 1);
+ encoder.TryInit(segment, segment.GetDuration());
+ EXPECT_TRUE(encoder.IsInitialized());
+ }
+
+ {
+ // Test that it takes 10s to trigger default-init.
+ MediaQueue<EncodedFrame> frames;
+ OpusTrackEncoder encoder(48000, frames);
+ AudioSegment longSegment;
+ longSegment.AppendNullData(48000 * 10 - 1);
+ AudioSegment shortSegment;
+ shortSegment.AppendNullData(1);
+ encoder.TryInit(longSegment, longSegment.GetDuration());
+ EXPECT_FALSE(encoder.IsInitialized());
+ encoder.TryInit(shortSegment, shortSegment.GetDuration());
+ EXPECT_FALSE(encoder.IsInitialized());
+ encoder.TryInit(shortSegment, shortSegment.GetDuration());
+ EXPECT_TRUE(encoder.IsInitialized());
+ }
+}
+
+static int TestOpusResampler(TrackRate aSamplingRate) {
+ MediaQueue<EncodedFrame> frames;
+ OpusTrackEncoder encoder(aSamplingRate, frames);
+ return encoder.mOutputSampleRate;
+}
+
+TEST(OpusAudioTrackEncoder, Resample)
+{
+ // Sampling rates of data to be fed to Opus encoder, should remain unchanged
+ // if it is one of Opus supported rates (8000, 12000, 16000, 24000 and 48000
+ // (kHz)) at initialization.
+ EXPECT_TRUE(TestOpusResampler(8000) == 8000);
+ EXPECT_TRUE(TestOpusResampler(12000) == 12000);
+ EXPECT_TRUE(TestOpusResampler(16000) == 16000);
+ EXPECT_TRUE(TestOpusResampler(24000) == 24000);
+ EXPECT_TRUE(TestOpusResampler(48000) == 48000);
+
+ // Otherwise, it should be resampled to 48kHz by resampler.
+ EXPECT_TRUE(TestOpusResampler(9600) == 48000);
+ EXPECT_TRUE(TestOpusResampler(44100) == 48000);
+}
+
+TEST(OpusAudioTrackEncoder, FetchMetadata)
+{
+ const int32_t channels = 1;
+ const TrackRate sampleRate = 44100;
+ MediaQueue<EncodedFrame> frames;
+ TestOpusTrackEncoder encoder(sampleRate, frames);
+ EXPECT_TRUE(encoder.TestOpusRawCreation(channels));
+
+ RefPtr<TrackMetadataBase> metadata = encoder.GetMetadata();
+ ASSERT_EQ(TrackMetadataBase::METADATA_OPUS, metadata->GetKind());
+
+ RefPtr<OpusMetadata> opusMeta = static_cast<OpusMetadata*>(metadata.get());
+ EXPECT_EQ(channels, opusMeta->mChannels);
+ EXPECT_EQ(sampleRate, opusMeta->mSamplingFrequency);
+}
+
+TEST(OpusAudioTrackEncoder, FrameEncode)
+{
+ const int32_t channels = 1;
+ const TrackRate sampleRate = 44100;
+ MediaQueue<EncodedFrame> frames;
+ TestOpusTrackEncoder encoder(sampleRate, frames);
+ EXPECT_TRUE(encoder.TestOpusRawCreation(channels));
+
+ // Generate five seconds of raw audio data.
+ AudioGenerator<AudioDataValue> generator(channels, sampleRate);
+ AudioSegment segment;
+ const int32_t samples = sampleRate * 5;
+ generator.Generate(segment, samples);
+
+ encoder.AppendAudioSegment(std::move(segment));
+ encoder.NotifyEndOfStream();
+
+ EXPECT_TRUE(encoder.IsEncodingComplete());
+ EXPECT_TRUE(frames.IsFinished());
+
+ // Verify that encoded data is 5 seconds long.
+ uint64_t totalDuration = 0;
+ while (RefPtr<EncodedFrame> frame = frames.PopFront()) {
+ totalDuration += frame->mDuration;
+ }
+ // 44100 as used above gets resampled to 48000 for opus.
+ const uint64_t five = 48000 * 5;
+ EXPECT_EQ(five + encoder.GetLookahead(), totalDuration);
+}
+
+TEST(OpusAudioTrackEncoder, DefaultInitDuration)
+{
+ const TrackRate rate = 44100;
+ MediaQueue<EncodedFrame> frames;
+ OpusTrackEncoder encoder(rate, frames);
+ AudioGenerator<AudioDataValue> generator(2, rate);
+ AudioSegment segment;
+ // 15 seconds should trigger the default-init rate.
+ // The default-init timeout is evaluated once per chunk, so keep chunks
+ // reasonably short.
+ for (int i = 0; i < 150; ++i) {
+ generator.Generate(segment, rate / 10);
+ }
+ encoder.AppendAudioSegment(std::move(segment));
+ encoder.NotifyEndOfStream();
+
+ EXPECT_TRUE(encoder.IsEncodingComplete());
+ EXPECT_TRUE(frames.IsFinished());
+
+ // Verify that encoded data is 15 seconds long.
+ uint64_t totalDuration = 0;
+ while (RefPtr<EncodedFrame> frame = frames.PopFront()) {
+ totalDuration += frame->mDuration;
+ }
+ // 44100 as used above gets resampled to 48000 for opus.
+ const uint64_t fifteen = 48000 * 15;
+ EXPECT_EQ(totalDuration, fifteen + encoder.GetLookahead());
+}
+
+uint64_t TestSampleRate(TrackRate aSampleRate, uint64_t aInputFrames) {
+ MediaQueue<EncodedFrame> frames;
+ OpusTrackEncoder encoder(aSampleRate, frames);
+ AudioGenerator<AudioDataValue> generator(2, aSampleRate);
+ AudioSegment segment;
+ const uint64_t chunkSize = aSampleRate / 10;
+ const uint64_t chunks = aInputFrames / chunkSize;
+ // 15 seconds should trigger the default-init rate.
+ // The default-init timeout is evaluated once per chunk, so keep chunks
+ // reasonably short.
+ for (size_t i = 0; i < chunks; ++i) {
+ generator.Generate(segment, chunkSize);
+ }
+ generator.Generate(segment, aInputFrames % chunks);
+ encoder.AppendAudioSegment(std::move(segment));
+ encoder.NotifyEndOfStream();
+
+ EXPECT_TRUE(encoder.IsEncodingComplete());
+ EXPECT_TRUE(frames.IsFinished());
+
+ // Verify that encoded data is 15 seconds long.
+ uint64_t totalDuration = 0;
+ while (RefPtr<EncodedFrame> frame = frames.PopFront()) {
+ totalDuration += frame->mDuration;
+ }
+ return totalDuration - encoder.GetLookahead();
+}
+
+TEST(OpusAudioTrackEncoder, DurationSampleRates)
+{
+ // Factors of 48k
+ EXPECT_EQ(TestSampleRate(48000, 48000 * 3 / 2), 48000U * 3 / 2);
+ EXPECT_EQ(TestSampleRate(24000, 24000 * 3 / 2), 48000U * 3 / 2);
+ EXPECT_EQ(TestSampleRate(16000, 16000 * 3 / 2), 48000U * 3 / 2);
+ EXPECT_EQ(TestSampleRate(12000, 12000 * 3 / 2), 48000U * 3 / 2);
+ EXPECT_EQ(TestSampleRate(8000, 8000 * 3 / 2), 48000U * 3 / 2);
+
+ // Non-factors of 48k, resampled
+ EXPECT_EQ(TestSampleRate(44100, 44100 * 3 / 2), 48000U * 3 / 2);
+ EXPECT_EQ(TestSampleRate(32000, 32000 * 3 / 2), 48000U * 3 / 2);
+ EXPECT_EQ(TestSampleRate(96000, 96000 * 3 / 2), 48000U * 3 / 2);
+ EXPECT_EQ(TestSampleRate(33330, 33330 * 3 / 2), 48000U * 3 / 2);
+}
diff --git a/dom/media/gtest/TestAudioTrackGraph.cpp b/dom/media/gtest/TestAudioTrackGraph.cpp
new file mode 100644
index 0000000000..c001c57c9f
--- /dev/null
+++ b/dom/media/gtest/TestAudioTrackGraph.cpp
@@ -0,0 +1,2537 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaTrackGraphImpl.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest-printers.h"
+#include "gtest/gtest.h"
+
+#include "CrossGraphPort.h"
+#include "DeviceInputTrack.h"
+#ifdef MOZ_WEBRTC
+# include "MediaEngineWebRTCAudio.h"
+#endif // MOZ_WEBRTC
+#include "MockCubeb.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/SpinEventLoopUntil.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "WaitFor.h"
+#include "WavDumper.h"
+
+#define DRIFT_BUFFERING_PREF "media.clockdrift.buffering"
+
+using namespace mozilla;
+
+namespace {
+// Short-hand for InvokeAsync on the current thread.
+#define Invoke(f) InvokeAsync(GetCurrentSerialEventTarget(), __func__, f)
+
+// Short-hand for DispatchToCurrentThread with a function.
+#define DispatchFunction(f) \
+ NS_DispatchToCurrentThread(NS_NewRunnableFunction(__func__, f))
+
+// Short-hand for DispatchToCurrentThread with a method with arguments
+#define DispatchMethod(t, m, args...) \
+ NS_DispatchToCurrentThread(NewRunnableMethod(__func__, t, m, ##args))
+
+#ifdef MOZ_WEBRTC
+/*
+ * Common ControlMessages
+ */
+struct StartInputProcessing : public ControlMessage {
+ const RefPtr<AudioProcessingTrack> mProcessingTrack;
+ const RefPtr<AudioInputProcessing> mInputProcessing;
+
+ StartInputProcessing(AudioProcessingTrack* aTrack,
+ AudioInputProcessing* aInputProcessing)
+ : ControlMessage(aTrack),
+ mProcessingTrack(aTrack),
+ mInputProcessing(aInputProcessing) {}
+ void Run() override { mInputProcessing->Start(mTrack->GraphImpl()); }
+};
+
+struct StopInputProcessing : public ControlMessage {
+ const RefPtr<AudioInputProcessing> mInputProcessing;
+
+ explicit StopInputProcessing(AudioProcessingTrack* aTrack,
+ AudioInputProcessing* aInputProcessing)
+ : ControlMessage(aTrack), mInputProcessing(aInputProcessing) {}
+ void Run() override { mInputProcessing->Stop(mTrack->GraphImpl()); }
+};
+
+struct SetPassThrough : public ControlMessage {
+ const RefPtr<AudioInputProcessing> mInputProcessing;
+ const bool mPassThrough;
+
+ SetPassThrough(MediaTrack* aTrack, AudioInputProcessing* aInputProcessing,
+ bool aPassThrough)
+ : ControlMessage(aTrack),
+ mInputProcessing(aInputProcessing),
+ mPassThrough(aPassThrough) {}
+ void Run() override {
+ EXPECT_EQ(mInputProcessing->PassThrough(mTrack->GraphImpl()),
+ !mPassThrough);
+ mInputProcessing->SetPassThrough(mTrack->GraphImpl(), mPassThrough);
+ }
+};
+
+struct SetRequestedInputChannelCount : public ControlMessage {
+ const CubebUtils::AudioDeviceID mDeviceId;
+ const RefPtr<AudioInputProcessing> mInputProcessing;
+ const uint32_t mChannelCount;
+
+ SetRequestedInputChannelCount(MediaTrack* aTrack,
+ CubebUtils::AudioDeviceID aDeviceId,
+ AudioInputProcessing* aInputProcessing,
+ uint32_t aChannelCount)
+ : ControlMessage(aTrack),
+ mDeviceId(aDeviceId),
+ mInputProcessing(aInputProcessing),
+ mChannelCount(aChannelCount) {}
+ void Run() override {
+ mInputProcessing->SetRequestedInputChannelCount(mTrack->GraphImpl(),
+ mDeviceId, mChannelCount);
+ }
+};
+#endif // MOZ_WEBRTC
+
+class GoFaster : public ControlMessage {
+ MockCubeb* mCubeb;
+
+ public:
+ explicit GoFaster(MockCubeb* aCubeb)
+ : ControlMessage(nullptr), mCubeb(aCubeb) {}
+ void Run() override { mCubeb->GoFaster(); }
+};
+
+struct StartNonNativeInput : public ControlMessage {
+ const RefPtr<NonNativeInputTrack> mInputTrack;
+ RefPtr<AudioInputSource> mInputSource;
+
+ StartNonNativeInput(NonNativeInputTrack* aInputTrack,
+ RefPtr<AudioInputSource>&& aInputSource)
+ : ControlMessage(aInputTrack),
+ mInputTrack(aInputTrack),
+ mInputSource(std::move(aInputSource)) {}
+ void Run() override { mInputTrack->StartAudio(std::move(mInputSource)); }
+};
+
+struct StopNonNativeInput : public ControlMessage {
+ const RefPtr<NonNativeInputTrack> mInputTrack;
+
+ explicit StopNonNativeInput(NonNativeInputTrack* aInputTrack)
+ : ControlMessage(aInputTrack), mInputTrack(aInputTrack) {}
+ void Run() override { mInputTrack->StopAudio(); }
+};
+
+} // namespace
+
+/*
+ * The set of tests here are a bit special. In part because they're async and
+ * depends on the graph thread to function. In part because they depend on main
+ * thread stable state to send messages to the graph.
+ *
+ * Any message sent from the main thread to the graph through the graph's
+ * various APIs are scheduled to run in stable state. Stable state occurs after
+ * a task in the main thread eventloop has run to completion.
+ *
+ * Since gtests are generally sync and on main thread, calling into the graph
+ * may schedule a stable state runnable but with no task in the eventloop to
+ * trigger stable state. Therefore care must be taken to always call into the
+ * graph from a task, typically via InvokeAsync or a dispatch to main thread.
+ */
+
+TEST(TestAudioTrackGraph, DifferentDeviceIDs)
+{
+ MockCubeb* cubeb = new MockCubeb();
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+
+ MediaTrackGraph* g1 = MediaTrackGraphImpl::GetInstance(
+ MediaTrackGraph::AUDIO_THREAD_DRIVER, /*Window ID*/ 1,
+ /* aShouldResistFingerprinting */ false,
+ MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE,
+ /*OutputDeviceID*/ nullptr, GetMainThreadSerialEventTarget());
+
+ MediaTrackGraph* g2 = MediaTrackGraphImpl::GetInstance(
+ MediaTrackGraph::AUDIO_THREAD_DRIVER, /*Window ID*/ 1,
+ /* aShouldResistFingerprinting */ false,
+ MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE,
+ /*OutputDeviceID*/ reinterpret_cast<cubeb_devid>(1),
+ GetMainThreadSerialEventTarget());
+
+ MediaTrackGraph* g1_2 = MediaTrackGraphImpl::GetInstance(
+ MediaTrackGraph::AUDIO_THREAD_DRIVER, /*Window ID*/ 1,
+ /* aShouldResistFingerprinting */ false,
+ MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE,
+ /*OutputDeviceID*/ nullptr, GetMainThreadSerialEventTarget());
+
+ MediaTrackGraph* g2_2 = MediaTrackGraphImpl::GetInstance(
+ MediaTrackGraph::AUDIO_THREAD_DRIVER, /*Window ID*/ 1,
+ /* aShouldResistFingerprinting */ false,
+ MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE,
+ /*OutputDeviceID*/ reinterpret_cast<cubeb_devid>(1),
+ GetMainThreadSerialEventTarget());
+
+ EXPECT_NE(g1, g2) << "Different graphs due to different device ids";
+ EXPECT_EQ(g1, g1_2) << "Same graphs for same device ids";
+ EXPECT_EQ(g2, g2_2) << "Same graphs for same device ids";
+
+ for (MediaTrackGraph* g : {g1, g2}) {
+ // Dummy track to make graph rolling. Add it and remove it to remove the
+ // graph from the global hash table and let it shutdown.
+
+ using SourceTrackPromise = MozPromise<SourceMediaTrack*, nsresult, true>;
+ auto p = Invoke([g] {
+ return SourceTrackPromise::CreateAndResolve(
+ g->CreateSourceTrack(MediaSegment::AUDIO), __func__);
+ });
+
+ WaitFor(cubeb->StreamInitEvent());
+ RefPtr<SourceMediaTrack> dummySource = WaitFor(p).unwrap();
+
+ DispatchMethod(dummySource, &SourceMediaTrack::Destroy);
+
+ WaitFor(cubeb->StreamDestroyEvent());
+ }
+}
+
+TEST(TestAudioTrackGraph, SetOutputDeviceID)
+{
+ MockCubeb* cubeb = new MockCubeb();
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+
+ // Set the output device id in GetInstance method confirm that it is the one
+ // used in cubeb_stream_init.
+ MediaTrackGraph* graph = MediaTrackGraphImpl::GetInstance(
+ MediaTrackGraph::AUDIO_THREAD_DRIVER, /*Window ID*/ 1,
+ /* aShouldResistFingerprinting */ false,
+ MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE,
+ /*OutputDeviceID*/ reinterpret_cast<cubeb_devid>(2),
+ GetMainThreadSerialEventTarget());
+
+ // Dummy track to make graph rolling. Add it and remove it to remove the
+ // graph from the global hash table and let it shutdown.
+ RefPtr<SourceMediaTrack> dummySource;
+ DispatchFunction(
+ [&] { dummySource = graph->CreateSourceTrack(MediaSegment::AUDIO); });
+
+ RefPtr<SmartMockCubebStream> stream = WaitFor(cubeb->StreamInitEvent());
+
+ EXPECT_EQ(stream->GetOutputDeviceID(), reinterpret_cast<cubeb_devid>(2))
+ << "After init confirm the expected output device id";
+
+ // Test has finished, destroy the track to shutdown the MTG.
+ DispatchMethod(dummySource, &SourceMediaTrack::Destroy);
+ WaitFor(cubeb->StreamDestroyEvent());
+}
+
+TEST(TestAudioTrackGraph, NotifyDeviceStarted)
+{
+ MockCubeb* cubeb = new MockCubeb();
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+
+ MediaTrackGraph* graph = MediaTrackGraphImpl::GetInstance(
+ MediaTrackGraph::AUDIO_THREAD_DRIVER, /*Window ID*/ 1,
+ /* aShouldResistFingerprinting */ false,
+ MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE, nullptr,
+ GetMainThreadSerialEventTarget());
+
+ RefPtr<SourceMediaTrack> dummySource;
+ Unused << WaitFor(Invoke([&] {
+ // Dummy track to make graph rolling. Add it and remove it to remove the
+ // graph from the global hash table and let it shutdown.
+ dummySource = graph->CreateSourceTrack(MediaSegment::AUDIO);
+
+ return graph->NotifyWhenDeviceStarted(dummySource);
+ }));
+
+ {
+ MediaTrackGraphImpl* graph = dummySource->GraphImpl();
+ MonitorAutoLock lock(graph->GetMonitor());
+ EXPECT_TRUE(graph->CurrentDriver()->AsAudioCallbackDriver());
+ EXPECT_TRUE(graph->CurrentDriver()->ThreadRunning());
+ }
+
+ // Test has finished, destroy the track to shutdown the MTG.
+ DispatchMethod(dummySource, &SourceMediaTrack::Destroy);
+ WaitFor(cubeb->StreamDestroyEvent());
+}
+
+TEST(TestAudioTrackGraph, NonNativeInputTrackStartAndStop)
+{
+ MockCubeb* cubeb = new MockCubeb();
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+
+ MediaTrackGraph* graph = MediaTrackGraphImpl::GetInstance(
+ MediaTrackGraph::SYSTEM_THREAD_DRIVER, /*Window ID*/ 1,
+ /* aShouldResistFingerprinting */ false,
+ MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE, nullptr,
+ GetMainThreadSerialEventTarget());
+
+ const CubebUtils::AudioDeviceID deviceId = (CubebUtils::AudioDeviceID)1;
+
+ // Add a NonNativeInputTrack to graph, making graph create an output-only
+ // AudioCallbackDriver since NonNativeInputTrack is an audio-type MediaTrack.
+ RefPtr<NonNativeInputTrack> track;
+ auto started = Invoke([&] {
+ track = new NonNativeInputTrack(graph->GraphRate(), deviceId,
+ PRINCIPAL_HANDLE_NONE);
+ graph->AddTrack(track);
+ return graph->NotifyWhenDeviceStarted(track);
+ });
+
+ RefPtr<SmartMockCubebStream> driverStream = WaitFor(cubeb->StreamInitEvent());
+ Result<bool, nsresult> rv = WaitFor(started);
+ EXPECT_TRUE(rv.unwrapOr(false));
+ EXPECT_FALSE(driverStream->mHasInput);
+ EXPECT_TRUE(driverStream->mHasOutput);
+
+ // Main test below:
+ {
+ const AudioInputSource::Id sourceId = 1;
+ const uint32_t channels = 2;
+ const TrackRate rate = 48000;
+ const uint32_t bufferingMs = StaticPrefs::media_clockdrift_buffering();
+
+ // Start and stop the audio in NonNativeInputTrack.
+ {
+ struct DeviceInfo {
+ uint32_t mChannelCount;
+ AudioInputType mType;
+ };
+ using DeviceQueryPromise =
+ MozPromise<DeviceInfo, nsresult, /* IsExclusive = */ true>;
+
+ struct DeviceQueryMessage : public ControlMessage {
+ const NonNativeInputTrack* mInputTrack;
+ MozPromiseHolder<DeviceQueryPromise> mHolder;
+
+ DeviceQueryMessage(NonNativeInputTrack* aInputTrack,
+ MozPromiseHolder<DeviceQueryPromise>&& aHolder)
+ : ControlMessage(aInputTrack),
+ mInputTrack(aInputTrack),
+ mHolder(std::move(aHolder)) {}
+ void Run() override {
+ DeviceInfo info = {mInputTrack->NumberOfChannels(),
+ mInputTrack->DevicePreference()};
+ // mHolder.Resolve(info, __func__);
+ mTrack->GraphImpl()->Dispatch(NS_NewRunnableFunction(
+ "TestAudioTrackGraph::DeviceQueryMessage",
+ [holder = std::move(mHolder), devInfo = info]() mutable {
+ holder.Resolve(devInfo, __func__);
+ }));
+ }
+ };
+
+ // No input channels and device preference before start.
+ {
+ MozPromiseHolder<DeviceQueryPromise> h;
+ RefPtr<DeviceQueryPromise> p = h.Ensure(__func__);
+ DispatchFunction([&] {
+ track->GraphImpl()->AppendMessage(
+ MakeUnique<DeviceQueryMessage>(track.get(), std::move(h)));
+ });
+ Result<DeviceInfo, nsresult> r = WaitFor(p);
+ ASSERT_TRUE(r.isOk());
+ DeviceInfo info = r.unwrap();
+
+ EXPECT_EQ(info.mChannelCount, 0U);
+ EXPECT_EQ(info.mType, AudioInputType::Unknown);
+ }
+
+ DispatchFunction([&] {
+ track->GraphImpl()->AppendMessage(MakeUnique<StartNonNativeInput>(
+ track.get(),
+ MakeRefPtr<AudioInputSource>(
+ MakeRefPtr<AudioInputSourceListener>(track.get()), sourceId,
+ deviceId, channels, true /* voice */, PRINCIPAL_HANDLE_NONE,
+ rate, graph->GraphRate(), bufferingMs)));
+ });
+ RefPtr<SmartMockCubebStream> nonNativeStream =
+ WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(nonNativeStream->mHasInput);
+ EXPECT_FALSE(nonNativeStream->mHasOutput);
+ EXPECT_EQ(nonNativeStream->GetInputDeviceID(), deviceId);
+ EXPECT_EQ(nonNativeStream->InputChannels(), channels);
+ EXPECT_EQ(nonNativeStream->InputSampleRate(),
+ static_cast<uint32_t>(rate));
+
+ // Input channels and device preference should be set after start.
+ {
+ MozPromiseHolder<DeviceQueryPromise> h;
+ RefPtr<DeviceQueryPromise> p = h.Ensure(__func__);
+ DispatchFunction([&] {
+ track->GraphImpl()->AppendMessage(
+ MakeUnique<DeviceQueryMessage>(track.get(), std::move(h)));
+ });
+ Result<DeviceInfo, nsresult> r = WaitFor(p);
+ ASSERT_TRUE(r.isOk());
+ DeviceInfo info = r.unwrap();
+
+ EXPECT_EQ(info.mChannelCount, channels);
+ EXPECT_EQ(info.mType, AudioInputType::Voice);
+ }
+
+ Unused << WaitFor(nonNativeStream->FramesProcessedEvent());
+
+ DispatchFunction([&] {
+ track->GraphImpl()->AppendMessage(
+ MakeUnique<StopNonNativeInput>(track.get()));
+ });
+ RefPtr<SmartMockCubebStream> destroyedStream =
+ WaitFor(cubeb->StreamDestroyEvent());
+ EXPECT_EQ(destroyedStream.get(), nonNativeStream.get());
+
+ // No input channels and device preference after stop.
+ {
+ MozPromiseHolder<DeviceQueryPromise> h;
+ RefPtr<DeviceQueryPromise> p = h.Ensure(__func__);
+ DispatchFunction([&] {
+ track->GraphImpl()->AppendMessage(
+ MakeUnique<DeviceQueryMessage>(track.get(), std::move(h)));
+ });
+ Result<DeviceInfo, nsresult> r = WaitFor(p);
+ ASSERT_TRUE(r.isOk());
+ DeviceInfo info = r.unwrap();
+
+ EXPECT_EQ(info.mChannelCount, 0U);
+ EXPECT_EQ(info.mType, AudioInputType::Unknown);
+ }
+ }
+
+ // Make sure the NonNativeInputTrack can restart and stop its audio.
+ {
+ DispatchFunction([&] {
+ track->GraphImpl()->AppendMessage(MakeUnique<StartNonNativeInput>(
+ track.get(),
+ MakeRefPtr<AudioInputSource>(
+ MakeRefPtr<AudioInputSourceListener>(track.get()), sourceId,
+ deviceId, channels, true, PRINCIPAL_HANDLE_NONE, rate,
+ graph->GraphRate(), bufferingMs)));
+ });
+ RefPtr<SmartMockCubebStream> nonNativeStream =
+ WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(nonNativeStream->mHasInput);
+ EXPECT_FALSE(nonNativeStream->mHasOutput);
+ EXPECT_EQ(nonNativeStream->GetInputDeviceID(), deviceId);
+ EXPECT_EQ(nonNativeStream->InputChannels(), channels);
+ EXPECT_EQ(nonNativeStream->InputSampleRate(),
+ static_cast<uint32_t>(rate));
+
+ Unused << WaitFor(nonNativeStream->FramesProcessedEvent());
+
+ DispatchFunction([&] {
+ track->GraphImpl()->AppendMessage(
+ MakeUnique<StopNonNativeInput>(track.get()));
+ });
+ RefPtr<SmartMockCubebStream> destroyedStream =
+ WaitFor(cubeb->StreamDestroyEvent());
+ EXPECT_EQ(destroyedStream.get(), nonNativeStream.get());
+ }
+ }
+
+ // Clean up.
+ DispatchFunction([&] { track->Destroy(); });
+ RefPtr<SmartMockCubebStream> destroyedStream =
+ WaitFor(cubeb->StreamDestroyEvent());
+ EXPECT_EQ(destroyedStream.get(), driverStream.get());
+}
+
+TEST(TestAudioTrackGraph, NonNativeInputTrackErrorCallback)
+{
+ MockCubeb* cubeb = new MockCubeb();
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+
+ MediaTrackGraph* graph = MediaTrackGraphImpl::GetInstance(
+ MediaTrackGraph::SYSTEM_THREAD_DRIVER, /*Window ID*/ 1,
+ /* aShouldResistFingerprinting */ false,
+ MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE, nullptr,
+ GetMainThreadSerialEventTarget());
+
+ const CubebUtils::AudioDeviceID deviceId = (CubebUtils::AudioDeviceID)1;
+
+ // Add a NonNativeInputTrack to graph, making graph create an output-only
+ // AudioCallbackDriver since NonNativeInputTrack is an audio-type MediaTrack.
+ RefPtr<NonNativeInputTrack> track;
+ auto started = Invoke([&] {
+ track = new NonNativeInputTrack(graph->GraphRate(), deviceId,
+ PRINCIPAL_HANDLE_NONE);
+ graph->AddTrack(track);
+ return graph->NotifyWhenDeviceStarted(track);
+ });
+
+ RefPtr<SmartMockCubebStream> driverStream = WaitFor(cubeb->StreamInitEvent());
+ Result<bool, nsresult> rv = WaitFor(started);
+ EXPECT_TRUE(rv.unwrapOr(false));
+ EXPECT_FALSE(driverStream->mHasInput);
+ EXPECT_TRUE(driverStream->mHasOutput);
+
+ // Main test below:
+ {
+ const AudioInputSource::Id sourceId = 1;
+ const uint32_t channels = 2;
+ const TrackRate rate = 48000;
+ const uint32_t bufferingMs = StaticPrefs::media_clockdrift_buffering();
+
+ // Launch and start the non-native audio stream.
+ DispatchFunction([&] {
+ track->GraphImpl()->AppendMessage(MakeUnique<StartNonNativeInput>(
+ track.get(),
+ MakeRefPtr<AudioInputSource>(
+ MakeRefPtr<AudioInputSourceListener>(track.get()), sourceId,
+ deviceId, channels, true, PRINCIPAL_HANDLE_NONE, rate,
+ graph->GraphRate(), bufferingMs)));
+ });
+ RefPtr<SmartMockCubebStream> nonNativeStream =
+ WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(nonNativeStream->mHasInput);
+ EXPECT_FALSE(nonNativeStream->mHasOutput);
+ EXPECT_EQ(nonNativeStream->GetInputDeviceID(), deviceId);
+ EXPECT_EQ(nonNativeStream->InputChannels(), channels);
+ EXPECT_EQ(nonNativeStream->InputSampleRate(), static_cast<uint32_t>(rate));
+
+ // Make sure the audio stream is running.
+ Unused << WaitFor(nonNativeStream->FramesProcessedEvent());
+
+ // Force an error. This results in the audio stream destroying.
+ DispatchFunction([&] { nonNativeStream->ForceError(); });
+ WaitFor(nonNativeStream->ErrorForcedEvent());
+
+ RefPtr<SmartMockCubebStream> destroyedStream =
+ WaitFor(cubeb->StreamDestroyEvent());
+ EXPECT_EQ(destroyedStream.get(), nonNativeStream.get());
+ }
+
+ // Make sure it's ok to call audio stop again.
+ DispatchFunction([&] {
+ track->GraphImpl()->AppendMessage(
+ MakeUnique<StopNonNativeInput>(track.get()));
+ });
+
+ // Clean up.
+ DispatchFunction([&] { track->Destroy(); });
+ RefPtr<SmartMockCubebStream> destroyedStream =
+ WaitFor(cubeb->StreamDestroyEvent());
+ EXPECT_EQ(destroyedStream.get(), driverStream.get());
+}
+
+class TestDeviceInputConsumerTrack : public DeviceInputConsumerTrack {
+ public:
+ static TestDeviceInputConsumerTrack* Create(MediaTrackGraph* aGraph) {
+ MOZ_ASSERT(NS_IsMainThread());
+ TestDeviceInputConsumerTrack* track =
+ new TestDeviceInputConsumerTrack(aGraph->GraphRate());
+ aGraph->AddTrack(track);
+ return track;
+ }
+
+ void Destroy() {
+ MOZ_ASSERT(NS_IsMainThread());
+ DisconnectDeviceInput();
+ DeviceInputConsumerTrack::Destroy();
+ }
+
+ void ProcessInput(GraphTime aFrom, GraphTime aTo, uint32_t aFlags) override {
+ if (aFrom >= aTo) {
+ return;
+ }
+
+ if (mInputs.IsEmpty()) {
+ GetData<AudioSegment>()->AppendNullData(aTo - aFrom);
+ } else {
+ MOZ_ASSERT(mInputs.Length() == 1);
+ AudioSegment data;
+ DeviceInputConsumerTrack::GetInputSourceData(data, mInputs[0], aFrom,
+ aTo);
+ GetData<AudioSegment>()->AppendFrom(&data);
+ }
+ };
+
+ uint32_t NumberOfChannels() const override {
+ if (mInputs.IsEmpty()) {
+ return 0;
+ }
+ DeviceInputTrack* t = mInputs[0]->GetSource()->AsDeviceInputTrack();
+ MOZ_ASSERT(t);
+ return t->NumberOfChannels();
+ }
+
+ private:
+ explicit TestDeviceInputConsumerTrack(TrackRate aSampleRate)
+ : DeviceInputConsumerTrack(aSampleRate) {}
+};
+
+TEST(TestAudioTrackGraph, DeviceChangedCallback)
+{
+ MockCubeb* cubeb = new MockCubeb();
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+
+ MediaTrackGraphImpl* graphImpl = MediaTrackGraphImpl::GetInstance(
+ MediaTrackGraph::SYSTEM_THREAD_DRIVER, /*Window ID*/ 1,
+ /* aShouldResistFingerprinting */ false,
+ MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE, nullptr,
+ GetMainThreadSerialEventTarget());
+
+ class TestAudioDataListener : public AudioDataListener {
+ public:
+ TestAudioDataListener(uint32_t aChannelCount, bool aIsVoice)
+ : mChannelCount(aChannelCount),
+ mIsVoice(aIsVoice),
+ mDeviceChangedCount(0) {}
+
+ uint32_t RequestedInputChannelCount(MediaTrackGraphImpl* aGraph) override {
+ return mChannelCount;
+ }
+ bool IsVoiceInput(MediaTrackGraphImpl* aGraph) const override {
+ return mIsVoice;
+ };
+ void DeviceChanged(MediaTrackGraphImpl* aGraph) override {
+ ++mDeviceChangedCount;
+ }
+ void Disconnect(MediaTrackGraphImpl* aGraph) override{/* Ignored */};
+ uint32_t DeviceChangedCount() { return mDeviceChangedCount; }
+
+ private:
+ ~TestAudioDataListener() = default;
+ const uint32_t mChannelCount;
+ const bool mIsVoice;
+ std::atomic<uint32_t> mDeviceChangedCount;
+ };
+
+ // Create a full-duplex AudioCallbackDriver by creating a NativeInputTrack.
+ const CubebUtils::AudioDeviceID device1 = (CubebUtils::AudioDeviceID)1;
+ RefPtr<TestAudioDataListener> listener1 = new TestAudioDataListener(1, false);
+ RefPtr<TestDeviceInputConsumerTrack> track1 =
+ TestDeviceInputConsumerTrack::Create(graphImpl);
+ track1->ConnectDeviceInput(device1, listener1.get(), PRINCIPAL_HANDLE_NONE);
+
+ EXPECT_TRUE(track1->ConnectToNativeDevice());
+ EXPECT_FALSE(track1->ConnectToNonNativeDevice());
+ auto started =
+ Invoke([&] { return graphImpl->NotifyWhenDeviceStarted(track1); });
+ RefPtr<SmartMockCubebStream> stream1 = WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(stream1->mHasInput);
+ EXPECT_TRUE(stream1->mHasOutput);
+ EXPECT_EQ(stream1->GetInputDeviceID(), device1);
+ Unused << WaitFor(started);
+
+ // Create a NonNativeInputTrack, and make sure its DeviceChangeCallback works.
+ const CubebUtils::AudioDeviceID device2 = (CubebUtils::AudioDeviceID)2;
+ RefPtr<TestAudioDataListener> listener2 = new TestAudioDataListener(2, true);
+ RefPtr<TestDeviceInputConsumerTrack> track2 =
+ TestDeviceInputConsumerTrack::Create(graphImpl);
+ track2->ConnectDeviceInput(device2, listener2.get(), PRINCIPAL_HANDLE_NONE);
+
+ EXPECT_FALSE(track2->ConnectToNativeDevice());
+ EXPECT_TRUE(track2->ConnectToNonNativeDevice());
+ RefPtr<SmartMockCubebStream> stream2 = WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(stream2->mHasInput);
+ EXPECT_FALSE(stream2->mHasOutput);
+ EXPECT_EQ(stream2->GetInputDeviceID(), device2);
+
+ // Produce a device-changed event for the NonNativeInputTrack.
+ DispatchFunction([&] { stream2->ForceDeviceChanged(); });
+ WaitFor(stream2->DeviceChangeForcedEvent());
+
+ // Produce a device-changed event for the NativeInputTrack.
+ DispatchFunction([&] { stream1->ForceDeviceChanged(); });
+ WaitFor(stream1->DeviceChangeForcedEvent());
+
+ // Destroy the NonNativeInputTrack.
+ DispatchFunction([&] {
+ track2->DisconnectDeviceInput();
+ track2->Destroy();
+ });
+ RefPtr<SmartMockCubebStream> destroyedStream =
+ WaitFor(cubeb->StreamDestroyEvent());
+ EXPECT_EQ(destroyedStream.get(), stream2.get());
+
+ // Make sure we only have one device-changed event for the NativeInputTrack.
+ EXPECT_EQ(listener2->DeviceChangedCount(), 1U);
+
+ // Destroy the NativeInputTrack.
+ DispatchFunction([&] {
+ track1->DisconnectDeviceInput();
+ track1->Destroy();
+ });
+ destroyedStream = WaitFor(cubeb->StreamDestroyEvent());
+ EXPECT_EQ(destroyedStream.get(), stream1.get());
+
+ // Make sure we only have one device-changed event for the NativeInputTrack.
+ EXPECT_EQ(listener1->DeviceChangedCount(), 1U);
+}
+
+// The native audio stream (a.k.a. GraphDriver) and the non-native audio stream
+// should always be the same as the max requested input channel of its paired
+// DeviceInputTracks. This test checks if the audio stream paired with the
+// DeviceInputTrack will follow the max requested input channel or not.
+//
+// The main focus for this test is to make sure DeviceInputTrack::OpenAudio and
+// ::CloseAudio works as what we expect. Besides, This test also confirms
+// MediaTrackGraphImpl::ReevaluateInputDevice works correctly by using a
+// test-only AudioDataListener.
+//
+// This test is pretty similar to RestartAudioIfProcessingMaxChannelCountChanged
+// below, which tests the same thing but using AudioProcessingTrack.
+// AudioProcessingTrack is the consumer of the DeviceInputTrack used in wild.
+// It has its own customized AudioDataListener. However, it only tests when
+// MOZ_WEBRTC is defined.
+TEST(TestAudioTrackGraph, RestartAudioIfMaxChannelCountChanged)
+{
+ MockCubeb* cubeb = new MockCubeb();
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+ auto unforcer = WaitFor(cubeb->ForceAudioThread()).unwrap();
+ Unused << unforcer;
+
+ MediaTrackGraphImpl* graphImpl = MediaTrackGraphImpl::GetInstance(
+ MediaTrackGraph::SYSTEM_THREAD_DRIVER, /*Window ID*/ 1,
+ /* aShouldResistFingerprinting */ false,
+ MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE, nullptr,
+ GetMainThreadSerialEventTarget());
+
+ // A test-only AudioDataListener that simulates AudioInputProcessing's setter
+ // and getter for the input channel count.
+ class TestAudioDataListener : public AudioDataListener {
+ public:
+ TestAudioDataListener(uint32_t aChannelCount, bool aIsVoice)
+ : mChannelCount(aChannelCount), mIsVoice(aIsVoice) {}
+ // Main thread API
+ void SetInputChannelCount(MediaTrackGraphImpl* aGraph,
+ CubebUtils::AudioDeviceID aDevice,
+ uint32_t aChannelCount) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ struct Message : public ControlMessage {
+ MediaTrackGraphImpl* mGraph;
+ TestAudioDataListener* mListener;
+ CubebUtils::AudioDeviceID mDevice;
+ uint32_t mChannelCount;
+
+ Message(MediaTrackGraphImpl* aGraph, TestAudioDataListener* aListener,
+ CubebUtils::AudioDeviceID aDevice, uint32_t aChannelCount)
+ : ControlMessage(nullptr),
+ mGraph(aGraph),
+ mListener(aListener),
+ mDevice(aDevice),
+ mChannelCount(aChannelCount) {}
+ void Run() override {
+ mListener->mChannelCount = mChannelCount;
+ mGraph->ReevaluateInputDevice(mDevice);
+ }
+ };
+
+ aGraph->AppendMessage(
+ MakeUnique<Message>(aGraph, this, aDevice, aChannelCount));
+ }
+ // Graph thread APIs: AudioDataListenerInterface implementations.
+ uint32_t RequestedInputChannelCount(MediaTrackGraphImpl* aGraph) override {
+ MOZ_ASSERT(aGraph->OnGraphThread());
+ return mChannelCount;
+ }
+ bool IsVoiceInput(MediaTrackGraphImpl* aGraph) const override {
+ return mIsVoice;
+ };
+ void DeviceChanged(MediaTrackGraphImpl* aGraph) override { /* Ignored */
+ }
+ void Disconnect(MediaTrackGraphImpl* aGraph) override{/* Ignored */};
+
+ private:
+ ~TestAudioDataListener() = default;
+
+ // Graph thread-only.
+ uint32_t mChannelCount;
+ // Any thread.
+ const bool mIsVoice;
+ };
+
+ // Request a new input channel count and expect to have a new stream.
+ auto setNewChannelCount = [&](const RefPtr<TestAudioDataListener>& aListener,
+ RefPtr<SmartMockCubebStream>& aStream,
+ uint32_t aChannelCount) {
+ ASSERT_TRUE(!!aListener);
+ ASSERT_TRUE(!!aStream);
+ ASSERT_TRUE(aStream->mHasInput);
+ ASSERT_NE(aChannelCount, 0U);
+
+ const CubebUtils::AudioDeviceID device = aStream->GetInputDeviceID();
+
+ bool destroyed = false;
+ MediaEventListener destroyListener = cubeb->StreamDestroyEvent().Connect(
+ AbstractThread::GetCurrent(),
+ [&](const RefPtr<SmartMockCubebStream>& aDestroyed) {
+ destroyed = aDestroyed.get() == aStream.get();
+ });
+
+ RefPtr<SmartMockCubebStream> newStream;
+ MediaEventListener restartListener = cubeb->StreamInitEvent().Connect(
+ AbstractThread::GetCurrent(),
+ [&](const RefPtr<SmartMockCubebStream>& aCreated) {
+ newStream = aCreated;
+ });
+
+ DispatchFunction([&] {
+ aListener->SetInputChannelCount(graphImpl, device, aChannelCount);
+ });
+
+ SpinEventLoopUntil<ProcessFailureBehavior::IgnoreAndContinue>(
+ "TEST(TestAudioTrackGraph, RestartAudioIfMaxChannelCountChanged) #1"_ns,
+ [&] { return destroyed && newStream; });
+
+ destroyListener.Disconnect();
+ restartListener.Disconnect();
+
+ aStream = newStream;
+ };
+
+ // Open a new track and expect to have a new stream.
+ auto openTrack = [&](RefPtr<SmartMockCubebStream>& aCurrentStream,
+ RefPtr<TestDeviceInputConsumerTrack>& aTrack,
+ const RefPtr<TestAudioDataListener>& aListener,
+ CubebUtils::AudioDeviceID aDevice) {
+ ASSERT_TRUE(!!aCurrentStream);
+ ASSERT_TRUE(aCurrentStream->mHasInput);
+ ASSERT_TRUE(!aTrack);
+ ASSERT_TRUE(!!aListener);
+
+ bool destroyed = false;
+ MediaEventListener destroyListener = cubeb->StreamDestroyEvent().Connect(
+ AbstractThread::GetCurrent(),
+ [&](const RefPtr<SmartMockCubebStream>& aDestroyed) {
+ destroyed = aDestroyed.get() == aCurrentStream.get();
+ });
+
+ RefPtr<SmartMockCubebStream> newStream;
+ MediaEventListener restartListener = cubeb->StreamInitEvent().Connect(
+ AbstractThread::GetCurrent(),
+ [&](const RefPtr<SmartMockCubebStream>& aCreated) {
+ newStream = aCreated;
+ });
+
+ aTrack = TestDeviceInputConsumerTrack::Create(graphImpl);
+ aTrack->ConnectDeviceInput(aDevice, aListener.get(), PRINCIPAL_HANDLE_NONE);
+
+ SpinEventLoopUntil<ProcessFailureBehavior::IgnoreAndContinue>(
+ "TEST(TestAudioTrackGraph, RestartAudioIfMaxChannelCountChanged) #2"_ns,
+ [&] { return destroyed && newStream; });
+
+ destroyListener.Disconnect();
+ restartListener.Disconnect();
+
+ aCurrentStream = newStream;
+ };
+
+ // Test for the native input device first then non-native device. The
+ // non-native device will be destroyed before the native device in case of
+ // causing a driver switching.
+
+ // Test for the native device.
+ const CubebUtils::AudioDeviceID nativeDevice = (CubebUtils::AudioDeviceID)1;
+ RefPtr<TestDeviceInputConsumerTrack> track1;
+ RefPtr<TestAudioDataListener> listener1;
+ RefPtr<SmartMockCubebStream> nativeStream;
+ RefPtr<TestDeviceInputConsumerTrack> track2;
+ RefPtr<TestAudioDataListener> listener2;
+ {
+ // Open a 1-channel NativeInputTrack.
+ listener1 = new TestAudioDataListener(1, false);
+ track1 = TestDeviceInputConsumerTrack::Create(graphImpl);
+ track1->ConnectDeviceInput(nativeDevice, listener1.get(),
+ PRINCIPAL_HANDLE_NONE);
+
+ EXPECT_TRUE(track1->ConnectToNativeDevice());
+ EXPECT_FALSE(track1->ConnectToNonNativeDevice());
+ auto started =
+ Invoke([&] { return graphImpl->NotifyWhenDeviceStarted(track1); });
+ nativeStream = WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(nativeStream->mHasInput);
+ EXPECT_TRUE(nativeStream->mHasOutput);
+ EXPECT_EQ(nativeStream->GetInputDeviceID(), nativeDevice);
+ Unused << WaitFor(started);
+
+ // Open a 2-channel NativeInputTrack and wait for a new driver since the
+ // max-channel for the native device becomes 2 now.
+ listener2 = new TestAudioDataListener(2, false);
+ openTrack(nativeStream, track2, listener2, nativeDevice);
+ EXPECT_EQ(nativeStream->InputChannels(), 2U);
+
+ // Set the second NativeInputTrack to 1-channel and wait for a new driver
+ // since the max-channel for the native device becomes 1 now.
+ setNewChannelCount(listener2, nativeStream, 1);
+ EXPECT_EQ(nativeStream->InputChannels(), 1U);
+
+ // Set the first NativeInputTrack to 2-channel and wait for a new driver
+ // since the max input channel for the native device becomes 2 now.
+ setNewChannelCount(listener1, nativeStream, 2);
+ EXPECT_EQ(nativeStream->InputChannels(), 2U);
+ }
+
+ // Test for the non-native device.
+ {
+ const CubebUtils::AudioDeviceID nonNativeDevice =
+ (CubebUtils::AudioDeviceID)2;
+
+ // Open a 1-channel NonNativeInputTrack.
+ RefPtr<TestAudioDataListener> listener3 =
+ new TestAudioDataListener(1, false);
+ RefPtr<TestDeviceInputConsumerTrack> track3 =
+ TestDeviceInputConsumerTrack::Create(graphImpl);
+ track3->ConnectDeviceInput(nonNativeDevice, listener3.get(),
+ PRINCIPAL_HANDLE_NONE);
+ EXPECT_FALSE(track3->ConnectToNativeDevice());
+ EXPECT_TRUE(track3->ConnectToNonNativeDevice());
+
+ RefPtr<SmartMockCubebStream> nonNativeStream =
+ WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(nonNativeStream->mHasInput);
+ EXPECT_FALSE(nonNativeStream->mHasOutput);
+ EXPECT_EQ(nonNativeStream->GetInputDeviceID(), nonNativeDevice);
+ EXPECT_EQ(nonNativeStream->InputChannels(), 1U);
+
+ // Open a 2-channel NonNativeInputTrack and wait for a new stream since
+ // the max-channel for the non-native device becomes 2 now.
+ RefPtr<TestAudioDataListener> listener4 =
+ new TestAudioDataListener(2, false);
+ RefPtr<TestDeviceInputConsumerTrack> track4;
+ openTrack(nonNativeStream, track4, listener4, nonNativeDevice);
+ EXPECT_EQ(nonNativeStream->InputChannels(), 2U);
+ EXPECT_EQ(nonNativeStream->GetInputDeviceID(), nonNativeDevice);
+
+ // Set the second NonNativeInputTrack to 1-channel and wait for a new
+ // driver since the max-channel for the non-native device becomes 1 now.
+ setNewChannelCount(listener4, nonNativeStream, 1);
+ EXPECT_EQ(nonNativeStream->InputChannels(), 1U);
+
+ // Set the first NonNativeInputTrack to 2-channel and wait for a new
+ // driver since the max input channel for the non-native device becomes 2
+ // now.
+ setNewChannelCount(listener3, nonNativeStream, 2);
+ EXPECT_EQ(nonNativeStream->InputChannels(), 2U);
+
+ // Close the second NonNativeInputTrack (1-channel) then the first one
+ // (2-channel) so we won't result in another stream creation.
+ DispatchFunction([&] {
+ track4->DisconnectDeviceInput();
+ track4->Destroy();
+ });
+ DispatchFunction([&] {
+ track3->DisconnectDeviceInput();
+ track3->Destroy();
+ });
+ RefPtr<SmartMockCubebStream> destroyedStream =
+ WaitFor(cubeb->StreamDestroyEvent());
+ EXPECT_EQ(destroyedStream.get(), nonNativeStream.get());
+ }
+
+ // Tear down for the native device.
+ {
+ // Close the second NativeInputTrack (1-channel) then the first one
+ // (2-channel) so we won't have driver switching.
+ DispatchFunction([&] {
+ track2->DisconnectDeviceInput();
+ track2->Destroy();
+ });
+ DispatchFunction([&] {
+ track1->DisconnectDeviceInput();
+ track1->Destroy();
+ });
+ RefPtr<SmartMockCubebStream> destroyedStream =
+ WaitFor(cubeb->StreamDestroyEvent());
+ EXPECT_EQ(destroyedStream.get(), nativeStream.get());
+ }
+}
+
+// This test is pretty similar to SwitchNativeAudioProcessingTrack below, which
+// tests the same thing but using AudioProcessingTrack. AudioProcessingTrack is
+// the consumer of the DeviceInputTrack used in wild. It has its own customized
+// AudioDataListener. However, it only tests when MOZ_WEBRTC is defined.
+TEST(TestAudioTrackGraph, SwitchNativeInputDevice)
+{
+ class TestAudioDataListener : public AudioDataListener {
+ public:
+ TestAudioDataListener(uint32_t aChannelCount, bool aIsVoice)
+ : mChannelCount(aChannelCount),
+ mIsVoice(aIsVoice),
+ mDeviceChangedCount(0) {}
+
+ uint32_t RequestedInputChannelCount(MediaTrackGraphImpl* aGraph) override {
+ return mChannelCount;
+ }
+ bool IsVoiceInput(MediaTrackGraphImpl* aGraph) const override {
+ return mIsVoice;
+ };
+ void DeviceChanged(MediaTrackGraphImpl* aGraph) override {
+ ++mDeviceChangedCount;
+ }
+ void Disconnect(MediaTrackGraphImpl* aGraph) override{/* Ignored */};
+ uint32_t DeviceChangedCount() { return mDeviceChangedCount; }
+
+ private:
+ ~TestAudioDataListener() = default;
+ const uint32_t mChannelCount;
+ const bool mIsVoice;
+ std::atomic<uint32_t> mDeviceChangedCount;
+ };
+
+ MockCubeb* cubeb = new MockCubeb();
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+
+ MediaTrackGraphImpl* graph = MediaTrackGraphImpl::GetInstance(
+ MediaTrackGraph::SYSTEM_THREAD_DRIVER, /*Window ID*/ 1,
+ /* aShouldResistFingerprinting */ false,
+ MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE, nullptr,
+ GetMainThreadSerialEventTarget());
+
+ auto switchNativeDevice =
+ [&](RefPtr<SmartMockCubebStream>&& aCurrentNativeStream,
+ RefPtr<TestDeviceInputConsumerTrack>& aCurrentNativeTrack,
+ RefPtr<SmartMockCubebStream>& aNextNativeStream,
+ RefPtr<TestDeviceInputConsumerTrack>& aNextNativeTrack) {
+ ASSERT_TRUE(aCurrentNativeStream->mHasInput);
+ ASSERT_TRUE(aCurrentNativeStream->mHasOutput);
+ ASSERT_TRUE(aNextNativeStream->mHasInput);
+ ASSERT_FALSE(aNextNativeStream->mHasOutput);
+
+ std::cerr << "Switching native input from device "
+ << aCurrentNativeStream->GetInputDeviceID() << " to "
+ << aNextNativeStream->GetInputDeviceID() << std::endl;
+
+ uint32_t destroyed = 0;
+ MediaEventListener destroyListener =
+ cubeb->StreamDestroyEvent().Connect(
+ AbstractThread::GetCurrent(),
+ [&](const RefPtr<SmartMockCubebStream>& aDestroyed) {
+ if (aDestroyed.get() == aCurrentNativeStream.get() ||
+ aDestroyed.get() == aNextNativeStream.get()) {
+ std::cerr << "cubeb stream " << aDestroyed.get()
+ << " (device " << aDestroyed->GetInputDeviceID()
+ << ") has been destroyed" << std::endl;
+ destroyed += 1;
+ }
+ });
+
+ RefPtr<SmartMockCubebStream> newStream;
+ MediaEventListener restartListener = cubeb->StreamInitEvent().Connect(
+ AbstractThread::GetCurrent(),
+ [&](const RefPtr<SmartMockCubebStream>& aCreated) {
+ // Make sure new stream has input, to prevent from getting a
+ // temporary output-only AudioCallbackDriver after closing current
+ // native device but before setting a new native input.
+ if (aCreated->mHasInput) {
+ ASSERT_TRUE(aCreated->mHasOutput);
+ newStream = aCreated;
+ }
+ });
+
+ std::cerr << "Close device " << aCurrentNativeStream->GetInputDeviceID()
+ << std::endl;
+ DispatchFunction([&] {
+ aCurrentNativeTrack->DisconnectDeviceInput();
+ aCurrentNativeTrack->Destroy();
+ });
+
+ std::cerr << "Wait for the switching" << std::endl;
+ SpinEventLoopUntil<ProcessFailureBehavior::IgnoreAndContinue>(
+ "TEST(TestAudioTrackGraph, SwitchNativeInputDevice)"_ns,
+ [&] { return destroyed >= 2 && newStream; });
+
+ destroyListener.Disconnect();
+ restartListener.Disconnect();
+
+ aCurrentNativeStream = nullptr;
+ aNextNativeStream = newStream;
+
+ std::cerr << "Now the native input is device "
+ << aNextNativeStream->GetInputDeviceID() << std::endl;
+ };
+
+ // Open a DeviceInputConsumerTrack for device 1.
+ const CubebUtils::AudioDeviceID device1 = (CubebUtils::AudioDeviceID)1;
+ RefPtr<TestDeviceInputConsumerTrack> track1 =
+ TestDeviceInputConsumerTrack::Create(graph);
+ RefPtr<TestAudioDataListener> listener1 = new TestAudioDataListener(1, false);
+ track1->ConnectDeviceInput(device1, listener1, PRINCIPAL_HANDLE_NONE);
+ EXPECT_EQ(track1->DeviceId().value(), device1);
+
+ auto started = Invoke([&] { return graph->NotifyWhenDeviceStarted(track1); });
+
+ RefPtr<SmartMockCubebStream> stream1 = WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(stream1->mHasInput);
+ EXPECT_TRUE(stream1->mHasOutput);
+ EXPECT_EQ(stream1->InputChannels(), 1U);
+ EXPECT_EQ(stream1->GetInputDeviceID(), device1);
+ Unused << WaitFor(started);
+ std::cerr << "Device " << device1 << " is opened (stream " << stream1.get()
+ << ")" << std::endl;
+
+ // Open a DeviceInputConsumerTrack for device 2.
+ const CubebUtils::AudioDeviceID device2 = (CubebUtils::AudioDeviceID)2;
+ RefPtr<TestDeviceInputConsumerTrack> track2 =
+ TestDeviceInputConsumerTrack::Create(graph);
+ RefPtr<TestAudioDataListener> listener2 = new TestAudioDataListener(2, false);
+ track2->ConnectDeviceInput(device2, listener2, PRINCIPAL_HANDLE_NONE);
+ EXPECT_EQ(track2->DeviceId().value(), device2);
+
+ RefPtr<SmartMockCubebStream> stream2 = WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(stream2->mHasInput);
+ EXPECT_FALSE(stream2->mHasOutput);
+ EXPECT_EQ(stream2->InputChannels(), 2U);
+ EXPECT_EQ(stream2->GetInputDeviceID(), device2);
+ std::cerr << "Device " << device2 << " is opened (stream " << stream2.get()
+ << ")" << std::endl;
+
+ // Open a DeviceInputConsumerTrack for device 3.
+ const CubebUtils::AudioDeviceID device3 = (CubebUtils::AudioDeviceID)3;
+ RefPtr<TestDeviceInputConsumerTrack> track3 =
+ TestDeviceInputConsumerTrack::Create(graph);
+ RefPtr<TestAudioDataListener> listener3 = new TestAudioDataListener(1, false);
+ track3->ConnectDeviceInput(device3, listener3, PRINCIPAL_HANDLE_NONE);
+ EXPECT_EQ(track3->DeviceId().value(), device3);
+
+ RefPtr<SmartMockCubebStream> stream3 = WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(stream3->mHasInput);
+ EXPECT_FALSE(stream3->mHasOutput);
+ EXPECT_EQ(stream3->InputChannels(), 1U);
+ EXPECT_EQ(stream3->GetInputDeviceID(), device3);
+ std::cerr << "Device " << device3 << " is opened (stream " << stream3.get()
+ << ")" << std::endl;
+
+ // Close device 1, so the native input device is switched from device 1 to
+ // device 2.
+ switchNativeDevice(std::move(stream1), track1, stream2, track2);
+ EXPECT_TRUE(stream2->mHasInput);
+ EXPECT_TRUE(stream2->mHasOutput);
+ EXPECT_EQ(stream2->InputChannels(), 2U);
+ EXPECT_EQ(stream2->GetInputDeviceID(), device2);
+ {
+ NativeInputTrack* native =
+ track2->GraphImpl()->GetNativeInputTrackMainThread();
+ ASSERT_TRUE(!!native);
+ EXPECT_EQ(native->mDeviceId, device2);
+ }
+
+ // Close device 2, so the native input device is switched from device 2 to
+ // device 3.
+ switchNativeDevice(std::move(stream2), track2, stream3, track3);
+ EXPECT_TRUE(stream3->mHasInput);
+ EXPECT_TRUE(stream3->mHasOutput);
+ EXPECT_EQ(stream3->InputChannels(), 1U);
+ EXPECT_EQ(stream3->GetInputDeviceID(), device3);
+ {
+ NativeInputTrack* native =
+ track3->GraphImpl()->GetNativeInputTrackMainThread();
+ ASSERT_TRUE(!!native);
+ EXPECT_EQ(native->mDeviceId, device3);
+ }
+
+ // Clean up.
+ std::cerr << "Close device " << device3 << std::endl;
+ DispatchFunction([&] {
+ track3->DisconnectDeviceInput();
+ track3->Destroy();
+ });
+ RefPtr<SmartMockCubebStream> destroyedStream =
+ WaitFor(cubeb->StreamDestroyEvent());
+ EXPECT_EQ(destroyedStream.get(), stream3.get());
+ {
+ auto* graphImpl = static_cast<MediaTrackGraphImpl*>(graph);
+ NativeInputTrack* native = graphImpl->GetNativeInputTrackMainThread();
+ ASSERT_TRUE(!native);
+ }
+ std::cerr << "No native input now" << std::endl;
+}
+
+#ifdef MOZ_WEBRTC
+TEST(TestAudioTrackGraph, ErrorCallback)
+{
+ MockCubeb* cubeb = new MockCubeb();
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+
+ MediaTrackGraph* graph = MediaTrackGraphImpl::GetInstance(
+ MediaTrackGraph::SYSTEM_THREAD_DRIVER, /*Window ID*/ 1,
+ /* aShouldResistFingerprinting */ false,
+ MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE, nullptr,
+ GetMainThreadSerialEventTarget());
+
+ const CubebUtils::AudioDeviceID deviceId = (CubebUtils::AudioDeviceID)1;
+
+ // Dummy track to make graph rolling. Add it and remove it to remove the
+ // graph from the global hash table and let it shutdown.
+ //
+ // We open an input through this track so that there's something triggering
+ // EnsureNextIteration on the fallback driver after the callback driver has
+ // gotten the error, and to check that a replacement cubeb_stream receives
+ // output from the graph.
+ RefPtr<AudioProcessingTrack> processingTrack;
+ RefPtr<AudioInputProcessing> listener;
+ auto started = Invoke([&] {
+ processingTrack = AudioProcessingTrack::Create(graph);
+ listener = new AudioInputProcessing(2);
+ processingTrack->GraphImpl()->AppendMessage(
+ MakeUnique<SetPassThrough>(processingTrack, listener, true));
+ processingTrack->SetInputProcessing(listener);
+ processingTrack->GraphImpl()->AppendMessage(
+ MakeUnique<StartInputProcessing>(processingTrack, listener));
+ processingTrack->ConnectDeviceInput(deviceId, listener,
+ PRINCIPAL_HANDLE_NONE);
+ EXPECT_EQ(processingTrack->DeviceId().value(), deviceId);
+ processingTrack->AddAudioOutput(reinterpret_cast<void*>(1));
+ return graph->NotifyWhenDeviceStarted(processingTrack);
+ });
+
+ RefPtr<SmartMockCubebStream> stream = WaitFor(cubeb->StreamInitEvent());
+ Result<bool, nsresult> rv = WaitFor(started);
+ EXPECT_TRUE(rv.unwrapOr(false));
+
+ // Force a cubeb state_callback error and see that we don't crash.
+ DispatchFunction([&] { stream->ForceError(); });
+
+ // Wait for the error to take effect, and the driver to restart and receive
+ // output.
+ bool errored = false;
+ MediaEventListener errorListener = stream->ErrorForcedEvent().Connect(
+ AbstractThread::GetCurrent(), [&] { errored = true; });
+ stream = WaitFor(cubeb->StreamInitEvent());
+ WaitFor(stream->FramesVerifiedEvent());
+ // The error event is notified after CUBEB_STATE_ERROR triggers other
+ // threads to init a new cubeb_stream, so there is a theoretical chance that
+ // `errored` might not be set when `stream` is set.
+ errorListener.Disconnect();
+ EXPECT_TRUE(errored);
+
+ // Clean up.
+ DispatchFunction([&] {
+ processingTrack->GraphImpl()->AppendMessage(
+ MakeUnique<StopInputProcessing>(processingTrack, listener));
+ processingTrack->DisconnectDeviceInput();
+ processingTrack->Destroy();
+ });
+ WaitFor(cubeb->StreamDestroyEvent());
+}
+
+TEST(TestAudioTrackGraph, AudioProcessingTrack)
+{
+ MockCubeb* cubeb = new MockCubeb();
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+ auto unforcer = WaitFor(cubeb->ForceAudioThread()).unwrap();
+ Unused << unforcer;
+
+ // Start on a system clock driver, then switch to full-duplex in one go. If we
+ // did output-then-full-duplex we'd risk a second NotifyWhenDeviceStarted
+ // resolving early after checking the first audio driver only.
+ MediaTrackGraph* graph = MediaTrackGraphImpl::GetInstance(
+ MediaTrackGraph::SYSTEM_THREAD_DRIVER, /*Window ID*/ 1,
+ /* aShouldResistFingerprinting */ false,
+ MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE, nullptr,
+ GetMainThreadSerialEventTarget());
+
+ const CubebUtils::AudioDeviceID deviceId = (CubebUtils::AudioDeviceID)1;
+
+ RefPtr<AudioProcessingTrack> processingTrack;
+ RefPtr<ProcessedMediaTrack> outputTrack;
+ RefPtr<MediaInputPort> port;
+ RefPtr<AudioInputProcessing> listener;
+ auto p = Invoke([&] {
+ processingTrack = AudioProcessingTrack::Create(graph);
+ outputTrack = graph->CreateForwardedInputTrack(MediaSegment::AUDIO);
+ outputTrack->QueueSetAutoend(false);
+ outputTrack->AddAudioOutput(reinterpret_cast<void*>(1));
+ port = outputTrack->AllocateInputPort(processingTrack);
+ /* Primary graph: Open Audio Input through SourceMediaTrack */
+ listener = new AudioInputProcessing(2);
+ processingTrack->GraphImpl()->AppendMessage(
+ MakeUnique<SetPassThrough>(processingTrack, listener, true));
+ processingTrack->SetInputProcessing(listener);
+ processingTrack->GraphImpl()->AppendMessage(
+ MakeUnique<StartInputProcessing>(processingTrack, listener));
+ // Device id does not matter. Ignore.
+ processingTrack->ConnectDeviceInput(deviceId, listener,
+ PRINCIPAL_HANDLE_NONE);
+ return graph->NotifyWhenDeviceStarted(processingTrack);
+ });
+
+ RefPtr<SmartMockCubebStream> stream = WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(stream->mHasInput);
+ Unused << WaitFor(p);
+
+ // Wait for a second worth of audio data. GoFaster is dispatched through a
+ // ControlMessage so that it is called in the first audio driver iteration.
+ // Otherwise the audio driver might be going very fast while the fallback
+ // system clock driver is still in an iteration.
+ DispatchFunction([&] {
+ processingTrack->GraphImpl()->AppendMessage(MakeUnique<GoFaster>(cubeb));
+ });
+ uint32_t totalFrames = 0;
+ WaitUntil(stream->FramesVerifiedEvent(), [&](uint32_t aFrames) {
+ totalFrames += aFrames;
+ return totalFrames > static_cast<uint32_t>(graph->GraphRate());
+ });
+ cubeb->DontGoFaster();
+
+ // Clean up.
+ DispatchFunction([&] {
+ outputTrack->RemoveAudioOutput((void*)1);
+ outputTrack->Destroy();
+ port->Destroy();
+ processingTrack->GraphImpl()->AppendMessage(
+ MakeUnique<StopInputProcessing>(processingTrack, listener));
+ processingTrack->DisconnectDeviceInput();
+ processingTrack->Destroy();
+ });
+
+ uint32_t inputRate = stream->InputSampleRate();
+ uint32_t inputFrequency = stream->InputFrequency();
+ uint64_t preSilenceSamples;
+ uint32_t estimatedFreq;
+ uint32_t nrDiscontinuities;
+ std::tie(preSilenceSamples, estimatedFreq, nrDiscontinuities) =
+ WaitFor(stream->OutputVerificationEvent());
+
+ EXPECT_EQ(estimatedFreq, inputFrequency);
+ std::cerr << "PreSilence: " << preSilenceSamples << std::endl;
+ // We buffer 128 frames. See DeviceInputTrack::ProcessInput.
+ EXPECT_GE(preSilenceSamples, 128U);
+ // If the fallback system clock driver is doing a graph iteration before the
+ // first audio driver iteration comes in, that iteration is ignored and
+ // results in zeros. It takes one fallback driver iteration *after* the audio
+ // driver has started to complete the switch, *usually* resulting two
+ // 10ms-iterations of silence; sometimes only one.
+ EXPECT_LE(preSilenceSamples, 128U + 2 * inputRate / 100 /* 2*10ms */);
+ // The waveform from AudioGenerator starts at 0, but we don't control its
+ // ending, so we expect a discontinuity there.
+ EXPECT_LE(nrDiscontinuities, 1U);
+}
+
+TEST(TestAudioTrackGraph, ReConnectDeviceInput)
+{
+ MockCubeb* cubeb = new MockCubeb();
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+
+ // 48k is a native processing rate, and avoids a resampling pass compared
+ // to 44.1k. The resampler may add take a few frames to stabilize, which show
+ // as unexected discontinuities in the test.
+ const TrackRate rate = 48000;
+
+ MediaTrackGraph* graph = MediaTrackGraphImpl::GetInstance(
+ MediaTrackGraph::SYSTEM_THREAD_DRIVER, /*Window ID*/ 1,
+ /* aShouldResistFingerprinting */ false, rate, nullptr,
+ GetMainThreadSerialEventTarget());
+
+ const CubebUtils::AudioDeviceID deviceId = (CubebUtils::AudioDeviceID)1;
+
+ RefPtr<AudioProcessingTrack> processingTrack;
+ RefPtr<ProcessedMediaTrack> outputTrack;
+ RefPtr<MediaInputPort> port;
+ RefPtr<AudioInputProcessing> listener;
+ auto p = Invoke([&] {
+ processingTrack = AudioProcessingTrack::Create(graph);
+ outputTrack = graph->CreateForwardedInputTrack(MediaSegment::AUDIO);
+ outputTrack->QueueSetAutoend(false);
+ outputTrack->AddAudioOutput(reinterpret_cast<void*>(1));
+ port = outputTrack->AllocateInputPort(processingTrack);
+ listener = new AudioInputProcessing(2);
+ processingTrack->SetInputProcessing(listener);
+ processingTrack->GraphImpl()->AppendMessage(
+ MakeUnique<StartInputProcessing>(processingTrack, listener));
+ processingTrack->ConnectDeviceInput(deviceId, listener,
+ PRINCIPAL_HANDLE_NONE);
+ return graph->NotifyWhenDeviceStarted(processingTrack);
+ });
+
+ RefPtr<SmartMockCubebStream> stream = WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(stream->mHasInput);
+ Unused << WaitFor(p);
+
+ // Set a drift factor so that we don't dont produce perfect 10ms-chunks. This
+ // will exercise whatever buffers are in the audio processing pipeline, and
+ // the bookkeeping surrounding them.
+ stream->SetDriftFactor(1.111);
+
+ // Wait for a second worth of audio data. GoFaster is dispatched through a
+ // ControlMessage so that it is called in the first audio driver iteration.
+ // Otherwise the audio driver might be going very fast while the fallback
+ // system clock driver is still in an iteration.
+ DispatchFunction([&] {
+ processingTrack->GraphImpl()->AppendMessage(MakeUnique<GoFaster>(cubeb));
+ });
+ {
+ uint32_t totalFrames = 0;
+ WaitUntil(stream->FramesProcessedEvent(), [&](uint32_t aFrames) {
+ totalFrames += aFrames;
+ return totalFrames > static_cast<uint32_t>(graph->GraphRate());
+ });
+ }
+ cubeb->DontGoFaster();
+
+ // Close the input to see that no asserts go off due to bad state.
+ DispatchFunction([&] { processingTrack->DisconnectDeviceInput(); });
+
+ stream = WaitFor(cubeb->StreamInitEvent());
+ EXPECT_FALSE(stream->mHasInput);
+ Unused << WaitFor(
+ Invoke([&] { return graph->NotifyWhenDeviceStarted(processingTrack); }));
+
+ // Output-only. Wait for another second before unmuting.
+ DispatchFunction([&] {
+ processingTrack->GraphImpl()->AppendMessage(MakeUnique<GoFaster>(cubeb));
+ });
+ {
+ uint32_t totalFrames = 0;
+ WaitUntil(stream->FramesProcessedEvent(), [&](uint32_t aFrames) {
+ totalFrames += aFrames;
+ return totalFrames > static_cast<uint32_t>(graph->GraphRate());
+ });
+ }
+ cubeb->DontGoFaster();
+
+ // Re-open the input to again see that no asserts go off due to bad state.
+ DispatchFunction([&] {
+ // Device id does not matter. Ignore.
+ processingTrack->ConnectDeviceInput(deviceId, listener,
+ PRINCIPAL_HANDLE_NONE);
+ });
+
+ stream = WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(stream->mHasInput);
+ Unused << WaitFor(
+ Invoke([&] { return graph->NotifyWhenDeviceStarted(processingTrack); }));
+
+ // Full-duplex. Wait for another second before finishing.
+ DispatchFunction([&] {
+ processingTrack->GraphImpl()->AppendMessage(MakeUnique<GoFaster>(cubeb));
+ });
+ {
+ uint32_t totalFrames = 0;
+ WaitUntil(stream->FramesProcessedEvent(), [&](uint32_t aFrames) {
+ totalFrames += aFrames;
+ return totalFrames > static_cast<uint32_t>(graph->GraphRate());
+ });
+ }
+ cubeb->DontGoFaster();
+
+ // Clean up.
+ DispatchFunction([&] {
+ outputTrack->RemoveAudioOutput((void*)1);
+ outputTrack->Destroy();
+ port->Destroy();
+ processingTrack->GraphImpl()->AppendMessage(
+ MakeUnique<StopInputProcessing>(processingTrack, listener));
+ processingTrack->DisconnectDeviceInput();
+ processingTrack->Destroy();
+ });
+
+ uint32_t inputRate = stream->InputSampleRate();
+ uint32_t inputFrequency = stream->InputFrequency();
+ uint64_t preSilenceSamples;
+ uint32_t estimatedFreq;
+ uint32_t nrDiscontinuities;
+ std::tie(preSilenceSamples, estimatedFreq, nrDiscontinuities) =
+ WaitFor(stream->OutputVerificationEvent());
+
+ EXPECT_EQ(estimatedFreq, inputFrequency);
+ std::cerr << "PreSilence: " << preSilenceSamples << std::endl;
+ // We buffer 10ms worth of frames in non-passthrough mode, plus up to 128
+ // frames as we round up to the nearest block. See
+ // AudioInputProcessing::Process and DeviceInputTrack::PrcoessInput.
+ EXPECT_GE(preSilenceSamples, 128U + inputRate / 100);
+ // If the fallback system clock driver is doing a graph iteration before the
+ // first audio driver iteration comes in, that iteration is ignored and
+ // results in zeros. It takes one fallback driver iteration *after* the audio
+ // driver has started to complete the switch, *usually* resulting two
+ // 10ms-iterations of silence; sometimes only one.
+ EXPECT_LE(preSilenceSamples, 128U + 3 * inputRate / 100 /* 3*10ms */);
+ // The waveform from AudioGenerator starts at 0, but we don't control its
+ // ending, so we expect a discontinuity there. Note that this check is only
+ // for the waveform on the stream *after* re-opening the input.
+ EXPECT_LE(nrDiscontinuities, 1U);
+}
+
+// Sum the signal to mono and compute the root mean square, in float32,
+// regardless of the input format.
+float rmsf32(AudioDataValue* aSamples, uint32_t aChannels, uint32_t aFrames) {
+ float downmixed;
+ float rms = 0.;
+ uint32_t readIdx = 0;
+ for (uint32_t i = 0; i < aFrames; i++) {
+ downmixed = 0.;
+ for (uint32_t j = 0; j < aChannels; j++) {
+ downmixed += AudioSampleToFloat(aSamples[readIdx++]);
+ }
+ rms += downmixed * downmixed;
+ }
+ rms = rms / aFrames;
+ return sqrt(rms);
+}
+
+TEST(TestAudioTrackGraph, AudioProcessingTrackDisabling)
+{
+ MockCubeb* cubeb = new MockCubeb();
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+
+ MediaTrackGraph* graph = MediaTrackGraphImpl::GetInstance(
+ MediaTrackGraph::SYSTEM_THREAD_DRIVER, /*Window ID*/ 1,
+ /* aShouldResistFingerprinting */ false,
+ MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE, nullptr,
+ GetMainThreadSerialEventTarget());
+
+ const CubebUtils::AudioDeviceID deviceId = (CubebUtils::AudioDeviceID)1;
+
+ RefPtr<AudioProcessingTrack> processingTrack;
+ RefPtr<ProcessedMediaTrack> outputTrack;
+ RefPtr<MediaInputPort> port;
+ RefPtr<AudioInputProcessing> listener;
+ auto p = Invoke([&] {
+ processingTrack = AudioProcessingTrack::Create(graph);
+ outputTrack = graph->CreateForwardedInputTrack(MediaSegment::AUDIO);
+ outputTrack->QueueSetAutoend(false);
+ outputTrack->AddAudioOutput(reinterpret_cast<void*>(1));
+ port = outputTrack->AllocateInputPort(processingTrack);
+ /* Primary graph: Open Audio Input through SourceMediaTrack */
+ listener = new AudioInputProcessing(2);
+ processingTrack->GraphImpl()->AppendMessage(
+ MakeUnique<SetPassThrough>(processingTrack, listener, true));
+ processingTrack->SetInputProcessing(listener);
+ processingTrack->ConnectDeviceInput(deviceId, listener,
+ PRINCIPAL_HANDLE_NONE);
+ processingTrack->GraphImpl()->AppendMessage(
+ MakeUnique<StartInputProcessing>(processingTrack, listener));
+ return graph->NotifyWhenDeviceStarted(processingTrack);
+ });
+
+ RefPtr<SmartMockCubebStream> stream = WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(stream->mHasInput);
+ Unused << WaitFor(p);
+
+ stream->SetOutputRecordingEnabled(true);
+
+ // Wait for a second worth of audio data.
+ uint32_t totalFrames = 0;
+ WaitUntil(stream->FramesProcessedEvent(), [&](uint32_t aFrames) {
+ totalFrames += aFrames;
+ return totalFrames > static_cast<uint32_t>(graph->GraphRate());
+ });
+
+ const uint32_t ITERATION_COUNT = 5;
+ uint32_t iterations = ITERATION_COUNT;
+ DisabledTrackMode currentMode = DisabledTrackMode::SILENCE_BLACK;
+ while (iterations--) {
+ // toggle the track enabled mode, wait a second, do this ITERATION_COUNT
+ // times
+ DispatchFunction([&] {
+ processingTrack->SetDisabledTrackMode(currentMode);
+ if (currentMode == DisabledTrackMode::SILENCE_BLACK) {
+ currentMode = DisabledTrackMode::ENABLED;
+ } else {
+ currentMode = DisabledTrackMode::SILENCE_BLACK;
+ }
+ });
+
+ totalFrames = 0;
+ WaitUntil(stream->FramesProcessedEvent(), [&](uint32_t aFrames) {
+ totalFrames += aFrames;
+ return totalFrames > static_cast<uint32_t>(graph->GraphRate());
+ });
+ }
+
+ // Clean up.
+ DispatchFunction([&] {
+ outputTrack->RemoveAudioOutput((void*)1);
+ outputTrack->Destroy();
+ port->Destroy();
+ processingTrack->GraphImpl()->AppendMessage(
+ MakeUnique<StopInputProcessing>(processingTrack, listener));
+ processingTrack->DisconnectDeviceInput();
+ processingTrack->Destroy();
+ });
+
+ uint64_t preSilenceSamples;
+ uint32_t estimatedFreq;
+ uint32_t nrDiscontinuities;
+ std::tie(preSilenceSamples, estimatedFreq, nrDiscontinuities) =
+ WaitFor(stream->OutputVerificationEvent());
+
+ auto data = stream->TakeRecordedOutput();
+
+ // check that there is non-silence and silence at the expected time in the
+ // stereo recording, while allowing for a bit of scheduling uncertainty, by
+ // checking half a second after the theoretical muting/unmuting.
+ // non-silence starts around: 0s, 2s, 4s
+ // silence start around: 1s, 3s, 5s
+ // To detect silence or non-silence, we compute the RMS of the signal for
+ // 100ms.
+ float noisyTime_s[] = {0.5, 2.5, 4.5};
+ float silenceTime_s[] = {1.5, 3.5, 5.5};
+
+ uint32_t rate = graph->GraphRate();
+ for (float& time : noisyTime_s) {
+ uint32_t startIdx = time * rate * 2 /* stereo */;
+ EXPECT_NE(rmsf32(&(data[startIdx]), 2, rate / 10), 0.0);
+ }
+
+ for (float& time : silenceTime_s) {
+ uint32_t startIdx = time * rate * 2 /* stereo */;
+ EXPECT_EQ(rmsf32(&(data[startIdx]), 2, rate / 10), 0.0);
+ }
+}
+
+TEST(TestAudioTrackGraph, SetRequestedInputChannelCount)
+{
+ MockCubeb* cubeb = new MockCubeb();
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+
+ MediaTrackGraph* graph = MediaTrackGraphImpl::GetInstance(
+ MediaTrackGraph::SYSTEM_THREAD_DRIVER, /*Window ID*/ 1,
+ /* aShouldResistFingerprinting */ false,
+ MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE, nullptr,
+ GetMainThreadSerialEventTarget());
+
+ // Open a 2-channel native input stream.
+ const CubebUtils::AudioDeviceID device1 = (CubebUtils::AudioDeviceID)1;
+ RefPtr<AudioProcessingTrack> track1 = AudioProcessingTrack::Create(graph);
+ RefPtr<AudioInputProcessing> listener1 = new AudioInputProcessing(2);
+ track1->SetInputProcessing(listener1);
+ track1->GraphImpl()->AppendMessage(
+ MakeUnique<SetPassThrough>(track1, listener1, true));
+ track1->GraphImpl()->AppendMessage(
+ MakeUnique<StartInputProcessing>(track1, listener1));
+ track1->ConnectDeviceInput(device1, listener1, PRINCIPAL_HANDLE_NONE);
+ EXPECT_EQ(track1->DeviceId().value(), device1);
+
+ auto started = Invoke([&] { return graph->NotifyWhenDeviceStarted(track1); });
+
+ RefPtr<SmartMockCubebStream> stream1 = WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(stream1->mHasInput);
+ EXPECT_TRUE(stream1->mHasOutput);
+ EXPECT_EQ(stream1->InputChannels(), 2U);
+ EXPECT_EQ(stream1->GetInputDeviceID(), device1);
+ Unused << WaitFor(started);
+
+ // Open a 1-channel non-native input stream.
+ const CubebUtils::AudioDeviceID device2 = (CubebUtils::AudioDeviceID)2;
+ RefPtr<AudioProcessingTrack> track2 = AudioProcessingTrack::Create(graph);
+ RefPtr<AudioInputProcessing> listener2 = new AudioInputProcessing(1);
+ track2->SetInputProcessing(listener2);
+ track2->GraphImpl()->AppendMessage(
+ MakeUnique<SetPassThrough>(track2, listener2, true));
+ track2->GraphImpl()->AppendMessage(
+ MakeUnique<StartInputProcessing>(track2, listener2));
+ track2->ConnectDeviceInput(device2, listener2, PRINCIPAL_HANDLE_NONE);
+ EXPECT_EQ(track2->DeviceId().value(), device2);
+
+ RefPtr<SmartMockCubebStream> stream2 = WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(stream2->mHasInput);
+ EXPECT_FALSE(stream2->mHasOutput);
+ EXPECT_EQ(stream2->InputChannels(), 1U);
+ EXPECT_EQ(stream2->GetInputDeviceID(), device2);
+
+ // Request a new input channel count. This should re-create new input stream
+ // accordingly.
+ auto setNewChannelCount = [&](const RefPtr<AudioProcessingTrack> aTrack,
+ const RefPtr<AudioInputProcessing>& aListener,
+ RefPtr<SmartMockCubebStream>& aStream,
+ uint32_t aChannelCount) {
+ bool destroyed = false;
+ MediaEventListener destroyListener = cubeb->StreamDestroyEvent().Connect(
+ AbstractThread::GetCurrent(),
+ [&](const RefPtr<SmartMockCubebStream>& aDestroyed) {
+ destroyed = aDestroyed.get() == aStream.get();
+ });
+
+ RefPtr<SmartMockCubebStream> newStream;
+ MediaEventListener restartListener = cubeb->StreamInitEvent().Connect(
+ AbstractThread::GetCurrent(),
+ [&](const RefPtr<SmartMockCubebStream>& aCreated) {
+ newStream = aCreated;
+ });
+
+ DispatchFunction([&] {
+ aTrack->GraphImpl()->AppendMessage(
+ MakeUnique<SetRequestedInputChannelCount>(aTrack, *aTrack->DeviceId(),
+ aListener, aChannelCount));
+ });
+
+ SpinEventLoopUntil<ProcessFailureBehavior::IgnoreAndContinue>(
+ "TEST(TestAudioTrackGraph, SetRequestedInputChannelCount)"_ns,
+ [&] { return destroyed && newStream; });
+
+ destroyListener.Disconnect();
+ restartListener.Disconnect();
+
+ aStream = newStream;
+ };
+
+ // Set the native input stream's input channel count to 1.
+ setNewChannelCount(track1, listener1, stream1, 1);
+ EXPECT_TRUE(stream1->mHasInput);
+ EXPECT_TRUE(stream1->mHasOutput);
+ EXPECT_EQ(stream1->InputChannels(), 1U);
+ EXPECT_EQ(stream1->GetInputDeviceID(), device1);
+
+ // Set the non-native input stream's input channel count to 2.
+ setNewChannelCount(track2, listener2, stream2, 2);
+ EXPECT_TRUE(stream2->mHasInput);
+ EXPECT_FALSE(stream2->mHasOutput);
+ EXPECT_EQ(stream2->InputChannels(), 2U);
+ EXPECT_EQ(stream2->GetInputDeviceID(), device2);
+
+ // Close the non-native input stream.
+ DispatchFunction([&] {
+ track2->GraphImpl()->AppendMessage(
+ MakeUnique<StopInputProcessing>(track2, listener2));
+ track2->DisconnectDeviceInput();
+ track2->Destroy();
+ });
+ RefPtr<SmartMockCubebStream> destroyed = WaitFor(cubeb->StreamDestroyEvent());
+ EXPECT_EQ(destroyed.get(), stream2.get());
+
+ // Close the native input stream.
+ DispatchFunction([&] {
+ track1->GraphImpl()->AppendMessage(
+ MakeUnique<StopInputProcessing>(track1, listener1));
+ track1->DisconnectDeviceInput();
+ track1->Destroy();
+ });
+ destroyed = WaitFor(cubeb->StreamDestroyEvent());
+ EXPECT_EQ(destroyed.get(), stream1.get());
+}
+
+// The native audio stream (a.k.a. GraphDriver) and the non-native audio stream
+// should always be the same as the max requested input channel of its paired
+// AudioProcessingTracks. This test checks if the audio stream paired with the
+// AudioProcessingTrack will follow the max requested input channel or not.
+//
+// This test is pretty similar to RestartAudioIfMaxChannelCountChanged above,
+// which makes sure the related DeviceInputTrack operations for the test here
+// works correctly. Instead of using a test-only AudioDataListener, we use
+// AudioInputProcessing here to simulate the real world use case.
+TEST(TestAudioTrackGraph, RestartAudioIfProcessingMaxChannelCountChanged)
+{
+ MockCubeb* cubeb = new MockCubeb();
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+ auto unforcer = WaitFor(cubeb->ForceAudioThread()).unwrap();
+ Unused << unforcer;
+
+ MediaTrackGraph* graph = MediaTrackGraphImpl::GetInstance(
+ MediaTrackGraph::SYSTEM_THREAD_DRIVER, /*Window ID*/ 1,
+ /* aShouldResistFingerprinting */ false,
+ MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE, nullptr,
+ GetMainThreadSerialEventTarget());
+
+ // Request a new input channel count and expect to have a new stream.
+ auto setNewChannelCount = [&](const RefPtr<AudioProcessingTrack>& aTrack,
+ const RefPtr<AudioInputProcessing>& aListener,
+ RefPtr<SmartMockCubebStream>& aStream,
+ uint32_t aChannelCount) {
+ ASSERT_TRUE(!!aTrack);
+ ASSERT_TRUE(!!aListener);
+ ASSERT_TRUE(!!aStream);
+ ASSERT_TRUE(aStream->mHasInput);
+ ASSERT_NE(aChannelCount, 0U);
+
+ const CubebUtils::AudioDeviceID device = *aTrack->DeviceId();
+
+ bool destroyed = false;
+ MediaEventListener destroyListener = cubeb->StreamDestroyEvent().Connect(
+ AbstractThread::GetCurrent(),
+ [&](const RefPtr<SmartMockCubebStream>& aDestroyed) {
+ destroyed = aDestroyed.get() == aStream.get();
+ });
+
+ RefPtr<SmartMockCubebStream> newStream;
+ MediaEventListener restartListener = cubeb->StreamInitEvent().Connect(
+ AbstractThread::GetCurrent(),
+ [&](const RefPtr<SmartMockCubebStream>& aCreated) {
+ newStream = aCreated;
+ });
+
+ DispatchFunction([&] {
+ aTrack->GraphImpl()->AppendMessage(
+ MakeUnique<SetRequestedInputChannelCount>(aTrack, device, aListener,
+ aChannelCount));
+ });
+
+ SpinEventLoopUntil<ProcessFailureBehavior::IgnoreAndContinue>(
+ "TEST(TestAudioTrackGraph, RestartAudioIfProcessingMaxChannelCountChanged) #1"_ns,
+ [&] { return destroyed && newStream; });
+
+ destroyListener.Disconnect();
+ restartListener.Disconnect();
+
+ aStream = newStream;
+ };
+
+ // Open a new track and expect to have a new stream.
+ auto openTrack = [&](RefPtr<SmartMockCubebStream>& aCurrentStream,
+ RefPtr<AudioProcessingTrack>& aTrack,
+ RefPtr<AudioInputProcessing>& aListener,
+ CubebUtils::AudioDeviceID aDevice,
+ uint32_t aChannelCount) {
+ ASSERT_TRUE(!!aCurrentStream);
+ ASSERT_TRUE(aCurrentStream->mHasInput);
+ ASSERT_TRUE(aChannelCount > aCurrentStream->InputChannels());
+ ASSERT_TRUE(!aTrack);
+ ASSERT_TRUE(!aListener);
+
+ bool destroyed = false;
+ MediaEventListener destroyListener = cubeb->StreamDestroyEvent().Connect(
+ AbstractThread::GetCurrent(),
+ [&](const RefPtr<SmartMockCubebStream>& aDestroyed) {
+ destroyed = aDestroyed.get() == aCurrentStream.get();
+ });
+
+ RefPtr<SmartMockCubebStream> newStream;
+ MediaEventListener restartListener = cubeb->StreamInitEvent().Connect(
+ AbstractThread::GetCurrent(),
+ [&](const RefPtr<SmartMockCubebStream>& aCreated) {
+ newStream = aCreated;
+ });
+
+ aTrack = AudioProcessingTrack::Create(graph);
+ aListener = new AudioInputProcessing(aChannelCount);
+ aTrack->SetInputProcessing(aListener);
+ aTrack->GraphImpl()->AppendMessage(
+ MakeUnique<SetPassThrough>(aTrack, aListener, true));
+ aTrack->GraphImpl()->AppendMessage(
+ MakeUnique<StartInputProcessing>(aTrack, aListener));
+
+ DispatchFunction([&] {
+ aTrack->ConnectDeviceInput(aDevice, aListener, PRINCIPAL_HANDLE_NONE);
+ });
+
+ SpinEventLoopUntil<ProcessFailureBehavior::IgnoreAndContinue>(
+ "TEST(TestAudioTrackGraph, RestartAudioIfProcessingMaxChannelCountChanged) #2"_ns,
+ [&] { return destroyed && newStream; });
+
+ destroyListener.Disconnect();
+ restartListener.Disconnect();
+
+ aCurrentStream = newStream;
+ };
+
+ // Test for the native input device first then non-native device. The
+ // non-native device will be destroyed before the native device in case of
+ // causing a native-device-switching.
+
+ // Test for the native device.
+ const CubebUtils::AudioDeviceID nativeDevice = (CubebUtils::AudioDeviceID)1;
+ RefPtr<AudioProcessingTrack> track1;
+ RefPtr<AudioInputProcessing> listener1;
+ RefPtr<SmartMockCubebStream> nativeStream;
+ RefPtr<AudioProcessingTrack> track2;
+ RefPtr<AudioInputProcessing> listener2;
+ {
+ // Open a 1-channel AudioProcessingTrack for the native device.
+ track1 = AudioProcessingTrack::Create(graph);
+ listener1 = new AudioInputProcessing(1);
+ track1->SetInputProcessing(listener1);
+ track1->GraphImpl()->AppendMessage(
+ MakeUnique<SetPassThrough>(track1, listener1, true));
+ track1->GraphImpl()->AppendMessage(
+ MakeUnique<StartInputProcessing>(track1, listener1));
+ track1->ConnectDeviceInput(nativeDevice, listener1, PRINCIPAL_HANDLE_NONE);
+ EXPECT_EQ(track1->DeviceId().value(), nativeDevice);
+
+ auto started =
+ Invoke([&] { return graph->NotifyWhenDeviceStarted(track1); });
+
+ nativeStream = WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(nativeStream->mHasInput);
+ EXPECT_TRUE(nativeStream->mHasOutput);
+ EXPECT_EQ(nativeStream->InputChannels(), 1U);
+ EXPECT_EQ(nativeStream->GetInputDeviceID(), nativeDevice);
+ Unused << WaitFor(started);
+
+ // Open a 2-channel AudioProcessingTrack for the native device and wait for
+ // a new driver since the max-channel for the native device becomes 2 now.
+ openTrack(nativeStream, track2, listener2, nativeDevice, 2);
+ EXPECT_EQ(nativeStream->InputChannels(), 2U);
+
+ // Set the second AudioProcessingTrack for the native device to 1-channel
+ // and wait for a new driver since the max-channel for the native device
+ // becomes 1 now.
+ setNewChannelCount(track2, listener2, nativeStream, 1);
+ EXPECT_EQ(nativeStream->InputChannels(), 1U);
+
+ // Set the first AudioProcessingTrack for the native device to 2-channel and
+ // wait for a new driver since the max input channel for the native device
+ // becomes 2 now.
+ setNewChannelCount(track1, listener1, nativeStream, 2);
+ EXPECT_EQ(nativeStream->InputChannels(), 2U);
+ }
+
+ // Test for the non-native device.
+ {
+ const CubebUtils::AudioDeviceID nonNativeDevice =
+ (CubebUtils::AudioDeviceID)2;
+
+ // Open a 1-channel AudioProcessingTrack for the non-native device.
+ RefPtr<AudioProcessingTrack> track3 = AudioProcessingTrack::Create(graph);
+ RefPtr<AudioInputProcessing> listener3 = new AudioInputProcessing(1);
+ track3->SetInputProcessing(listener3);
+ track3->GraphImpl()->AppendMessage(
+ MakeUnique<SetPassThrough>(track3, listener3, true));
+ track3->GraphImpl()->AppendMessage(
+ MakeUnique<StartInputProcessing>(track3, listener3));
+ track3->ConnectDeviceInput(nonNativeDevice, listener3,
+ PRINCIPAL_HANDLE_NONE);
+ EXPECT_EQ(track3->DeviceId().value(), nonNativeDevice);
+
+ RefPtr<SmartMockCubebStream> nonNativeStream =
+ WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(nonNativeStream->mHasInput);
+ EXPECT_FALSE(nonNativeStream->mHasOutput);
+ EXPECT_EQ(nonNativeStream->InputChannels(), 1U);
+ EXPECT_EQ(nonNativeStream->GetInputDeviceID(), nonNativeDevice);
+
+ // Open a 2-channel AudioProcessingTrack for the non-native device and wait
+ // for a new stream since the max-channel for the non-native device becomes
+ // 2 now.
+ RefPtr<AudioProcessingTrack> track4;
+ RefPtr<AudioInputProcessing> listener4;
+ openTrack(nonNativeStream, track4, listener4, nonNativeDevice, 2);
+ EXPECT_EQ(nonNativeStream->InputChannels(), 2U);
+ EXPECT_EQ(nonNativeStream->GetInputDeviceID(), nonNativeDevice);
+
+ // Set the second AudioProcessingTrack for the non-native to 1-channel and
+ // wait for a new driver since the max-channel for the non-native device
+ // becomes 1 now.
+ setNewChannelCount(track4, listener4, nonNativeStream, 1);
+ EXPECT_EQ(nonNativeStream->InputChannels(), 1U);
+ EXPECT_EQ(nonNativeStream->GetInputDeviceID(), nonNativeDevice);
+
+ // Set the first AudioProcessingTrack for the non-native device to 2-channel
+ // and wait for a new driver since the max input channel for the non-native
+ // device becomes 2 now.
+ setNewChannelCount(track3, listener3, nonNativeStream, 2);
+ EXPECT_EQ(nonNativeStream->InputChannels(), 2U);
+ EXPECT_EQ(nonNativeStream->GetInputDeviceID(), nonNativeDevice);
+
+ // Close the second AudioProcessingTrack (1-channel) for the non-native
+ // device then the first one (2-channel) so we won't result in another
+ // stream creation.
+ DispatchFunction([&] {
+ track4->GraphImpl()->AppendMessage(
+ MakeUnique<StopInputProcessing>(track4, listener4));
+ track4->DisconnectDeviceInput();
+ track4->Destroy();
+ });
+ DispatchFunction([&] {
+ track3->GraphImpl()->AppendMessage(
+ MakeUnique<StopInputProcessing>(track3, listener3));
+ track3->DisconnectDeviceInput();
+ track3->Destroy();
+ });
+ RefPtr<SmartMockCubebStream> destroyedStream =
+ WaitFor(cubeb->StreamDestroyEvent());
+ EXPECT_EQ(destroyedStream.get(), nonNativeStream.get());
+ }
+
+ // Tear down for the native device.
+ {
+ // Close the second AudioProcessingTrack (1-channel) for the native device
+ // then the first one (2-channel) so we won't have driver switching.
+ DispatchFunction([&] {
+ track2->GraphImpl()->AppendMessage(
+ MakeUnique<StopInputProcessing>(track2, listener2));
+ track2->DisconnectDeviceInput();
+ track2->Destroy();
+ });
+ DispatchFunction([&] {
+ track1->GraphImpl()->AppendMessage(
+ MakeUnique<StopInputProcessing>(track1, listener1));
+ track1->DisconnectDeviceInput();
+ track1->Destroy();
+ });
+ RefPtr<SmartMockCubebStream> destroyedStream =
+ WaitFor(cubeb->StreamDestroyEvent());
+ EXPECT_EQ(destroyedStream.get(), nativeStream.get());
+ }
+}
+
+TEST(TestAudioTrackGraph, SetInputChannelCountBeforeAudioCallbackDriver)
+{
+ MockCubeb* cubeb = new MockCubeb();
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+
+ MediaTrackGraph* graph = MediaTrackGraphImpl::GetInstance(
+ MediaTrackGraph::SYSTEM_THREAD_DRIVER, /*Window ID*/ 1,
+ /* aShouldResistFingerprinting */ false,
+ MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE, nullptr,
+ GetMainThreadSerialEventTarget());
+
+ // Set the input channel count of AudioInputProcessing, which will force
+ // MediaTrackGraph to re-evaluate input device, when the MediaTrackGraph is
+ // driven by the SystemClockDriver.
+
+ const CubebUtils::AudioDeviceID deviceId = (CubebUtils::AudioDeviceID)1;
+ RefPtr<AudioProcessingTrack> track;
+ RefPtr<AudioInputProcessing> listener;
+ {
+ MozPromiseHolder<GenericPromise> h;
+ RefPtr<GenericPromise> p = h.Ensure(__func__);
+
+ struct GuardMessage : public ControlMessage {
+ MozPromiseHolder<GenericPromise> mHolder;
+
+ GuardMessage(MediaTrack* aTrack,
+ MozPromiseHolder<GenericPromise>&& aHolder)
+ : ControlMessage(aTrack), mHolder(std::move(aHolder)) {}
+ void Run() override {
+ mTrack->GraphImpl()->Dispatch(NS_NewRunnableFunction(
+ "TestAudioTrackGraph::SetInputChannel::Message::Resolver",
+ [holder = std::move(mHolder)]() mutable {
+ holder.Resolve(true, __func__);
+ }));
+ }
+ };
+
+ DispatchFunction([&] {
+ track = AudioProcessingTrack::Create(graph);
+ listener = new AudioInputProcessing(2);
+ track->GraphImpl()->AppendMessage(
+ MakeUnique<SetPassThrough>(track, listener, true));
+ track->SetInputProcessing(listener);
+ track->GraphImpl()->AppendMessage(
+ MakeUnique<SetRequestedInputChannelCount>(track, deviceId, listener,
+ 1));
+ track->GraphImpl()->AppendMessage(
+ MakeUnique<GuardMessage>(track, std::move(h)));
+ });
+
+ Unused << WaitFor(p);
+ }
+
+ // Open a full-duplex AudioCallbackDriver.
+
+ RefPtr<MediaInputPort> port;
+ DispatchFunction([&] {
+ track->GraphImpl()->AppendMessage(
+ MakeUnique<StartInputProcessing>(track, listener));
+ track->ConnectDeviceInput(deviceId, listener, PRINCIPAL_HANDLE_NONE);
+ });
+
+ // MediaTrackGraph will create a output-only AudioCallbackDriver in
+ // CheckDriver before we open an audio input above, since AudioProcessingTrack
+ // is a audio-type MediaTrack, so we need to wait here until the duplex
+ // AudioCallbackDriver is created.
+ RefPtr<SmartMockCubebStream> stream;
+ SpinEventLoopUntil<ProcessFailureBehavior::IgnoreAndContinue>(
+ "TEST(TestAudioTrackGraph, SetInputChannelCountBeforeAudioCallbackDriver)"_ns,
+ [&] {
+ stream = WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(stream->mHasOutput);
+ return stream->mHasInput;
+ });
+ EXPECT_EQ(stream->InputChannels(), 1U);
+
+ Unused << WaitFor(
+ Invoke([&] { return graph->NotifyWhenDeviceStarted(track); }));
+
+ // Clean up.
+ DispatchFunction([&] {
+ track->GraphImpl()->AppendMessage(
+ MakeUnique<StopInputProcessing>(track, listener));
+ track->DisconnectDeviceInput();
+ track->Destroy();
+ });
+ Unused << WaitFor(cubeb->StreamDestroyEvent());
+}
+
+TEST(TestAudioTrackGraph, StartAudioDeviceBeforeStartingAudioProcessing)
+{
+ MockCubeb* cubeb = new MockCubeb();
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+
+ MediaTrackGraph* graph = MediaTrackGraphImpl::GetInstance(
+ MediaTrackGraph::SYSTEM_THREAD_DRIVER, /*Window ID*/ 1,
+ /* aShouldResistFingerprinting */ false,
+ MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE, nullptr,
+ GetMainThreadSerialEventTarget());
+
+ // Create a duplex AudioCallbackDriver
+ const CubebUtils::AudioDeviceID deviceId = (CubebUtils::AudioDeviceID)1;
+ RefPtr<AudioProcessingTrack> track;
+ RefPtr<AudioInputProcessing> listener;
+ auto started = Invoke([&] {
+ track = AudioProcessingTrack::Create(graph);
+ listener = new AudioInputProcessing(2);
+ track->GraphImpl()->AppendMessage(
+ MakeUnique<SetPassThrough>(track, listener, true));
+ track->SetInputProcessing(listener);
+ // Start audio device without starting audio processing.
+ track->ConnectDeviceInput(deviceId, listener, PRINCIPAL_HANDLE_NONE);
+ return graph->NotifyWhenDeviceStarted(track);
+ });
+
+ RefPtr<SmartMockCubebStream> stream = WaitFor(cubeb->StreamInitEvent());
+ Result<bool, nsresult> rv = WaitFor(started);
+ EXPECT_TRUE(rv.unwrapOr(false));
+ EXPECT_TRUE(stream->mHasInput);
+ EXPECT_TRUE(stream->mHasOutput);
+
+ // Wait for a second to make sure audio output callback has been fired.
+ DispatchFunction(
+ [&] { track->GraphImpl()->AppendMessage(MakeUnique<GoFaster>(cubeb)); });
+ {
+ uint32_t totalFrames = 0;
+ WaitUntil(stream->FramesProcessedEvent(), [&](uint32_t aFrames) {
+ totalFrames += aFrames;
+ return totalFrames > static_cast<uint32_t>(graph->GraphRate());
+ });
+ }
+ cubeb->DontGoFaster();
+
+ // Start the audio processing.
+ DispatchFunction([&] {
+ track->GraphImpl()->AppendMessage(
+ MakeUnique<StartInputProcessing>(track, listener));
+ });
+
+ // Wait for a second to make sure audio output callback has been fired.
+ DispatchFunction(
+ [&] { track->GraphImpl()->AppendMessage(MakeUnique<GoFaster>(cubeb)); });
+ {
+ uint32_t totalFrames = 0;
+ WaitUntil(stream->FramesProcessedEvent(), [&](uint32_t aFrames) {
+ totalFrames += aFrames;
+ return totalFrames > static_cast<uint32_t>(graph->GraphRate());
+ });
+ }
+ cubeb->DontGoFaster();
+
+ // Clean up.
+ DispatchFunction([&] {
+ track->DisconnectDeviceInput();
+ track->Destroy();
+ });
+ Unused << WaitFor(cubeb->StreamDestroyEvent());
+}
+
+TEST(TestAudioTrackGraph, StopAudioProcessingBeforeStoppingAudioDevice)
+{
+ MockCubeb* cubeb = new MockCubeb();
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+
+ MediaTrackGraph* graph = MediaTrackGraphImpl::GetInstance(
+ MediaTrackGraph::SYSTEM_THREAD_DRIVER, /*Window ID*/ 1,
+ /* aShouldResistFingerprinting */ false,
+ MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE, nullptr,
+ GetMainThreadSerialEventTarget());
+
+ // Create a duplex AudioCallbackDriver
+ const CubebUtils::AudioDeviceID deviceId = (CubebUtils::AudioDeviceID)1;
+ RefPtr<AudioProcessingTrack> track;
+ RefPtr<AudioInputProcessing> listener;
+ auto started = Invoke([&] {
+ track = AudioProcessingTrack::Create(graph);
+ listener = new AudioInputProcessing(2);
+ track->GraphImpl()->AppendMessage(
+ MakeUnique<SetPassThrough>(track, listener, true));
+ track->SetInputProcessing(listener);
+ track->GraphImpl()->AppendMessage(
+ MakeUnique<StartInputProcessing>(track, listener));
+ track->ConnectDeviceInput(deviceId, listener, PRINCIPAL_HANDLE_NONE);
+ return graph->NotifyWhenDeviceStarted(track);
+ });
+
+ RefPtr<SmartMockCubebStream> stream = WaitFor(cubeb->StreamInitEvent());
+ Result<bool, nsresult> rv = WaitFor(started);
+ EXPECT_TRUE(rv.unwrapOr(false));
+ EXPECT_TRUE(stream->mHasInput);
+ EXPECT_TRUE(stream->mHasOutput);
+
+ // Wait for a second to make sure audio output callback has been fired.
+ DispatchFunction(
+ [&] { track->GraphImpl()->AppendMessage(MakeUnique<GoFaster>(cubeb)); });
+ {
+ uint32_t totalFrames = 0;
+ WaitUntil(stream->FramesProcessedEvent(), [&](uint32_t aFrames) {
+ totalFrames += aFrames;
+ return totalFrames > static_cast<uint32_t>(graph->GraphRate());
+ });
+ }
+ cubeb->DontGoFaster();
+
+ // Stop the audio processing
+ DispatchFunction([&] {
+ track->GraphImpl()->AppendMessage(
+ MakeUnique<StopInputProcessing>(track, listener));
+ });
+
+ // Wait for a second to make sure audio output callback has been fired.
+ DispatchFunction(
+ [&] { track->GraphImpl()->AppendMessage(MakeUnique<GoFaster>(cubeb)); });
+ {
+ uint32_t totalFrames = 0;
+ WaitUntil(stream->FramesProcessedEvent(), [&](uint32_t aFrames) {
+ totalFrames += aFrames;
+ return totalFrames > static_cast<uint32_t>(graph->GraphRate());
+ });
+ }
+ cubeb->DontGoFaster();
+
+ // Clean up.
+ DispatchFunction([&] {
+ track->DisconnectDeviceInput();
+ track->Destroy();
+ });
+ Unused << WaitFor(cubeb->StreamDestroyEvent());
+}
+
+// This test is pretty similar to SwitchNativeInputDevice above, which makes
+// sure the related DeviceInputTrack operations for the test here works
+// correctly. Instead of using a test-only DeviceInputTrack consumer, we use
+// AudioProcessingTrack here to simulate the real world use case.
+TEST(TestAudioTrackGraph, SwitchNativeAudioProcessingTrack)
+{
+ MockCubeb* cubeb = new MockCubeb();
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+
+ MediaTrackGraph* graph = MediaTrackGraphImpl::GetInstance(
+ MediaTrackGraph::SYSTEM_THREAD_DRIVER, /*Window ID*/ 1,
+ /* aShouldResistFingerprinting */ false,
+ MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE, nullptr,
+ GetMainThreadSerialEventTarget());
+
+ auto switchNativeDevice =
+ [&](RefPtr<SmartMockCubebStream>&& aCurrentNativeStream,
+ RefPtr<AudioProcessingTrack>& aCurrentNativeTrack,
+ RefPtr<AudioInputProcessing>& aCurrentNativeListener,
+ RefPtr<SmartMockCubebStream>& aNextNativeStream,
+ RefPtr<AudioProcessingTrack>& aNextNativeTrack) {
+ ASSERT_TRUE(aCurrentNativeStream->mHasInput);
+ ASSERT_TRUE(aCurrentNativeStream->mHasOutput);
+ ASSERT_TRUE(aNextNativeStream->mHasInput);
+ ASSERT_FALSE(aNextNativeStream->mHasOutput);
+
+ std::cerr << "Switching native input from device "
+ << aCurrentNativeStream->GetInputDeviceID() << " to "
+ << aNextNativeStream->GetInputDeviceID() << std::endl;
+
+ uint32_t destroyed = 0;
+ MediaEventListener destroyListener =
+ cubeb->StreamDestroyEvent().Connect(
+ AbstractThread::GetCurrent(),
+ [&](const RefPtr<SmartMockCubebStream>& aDestroyed) {
+ if (aDestroyed.get() == aCurrentNativeStream.get() ||
+ aDestroyed.get() == aNextNativeStream.get()) {
+ std::cerr << "cubeb stream " << aDestroyed.get()
+ << " (device " << aDestroyed->GetInputDeviceID()
+ << ") has been destroyed" << std::endl;
+ destroyed += 1;
+ }
+ });
+
+ RefPtr<SmartMockCubebStream> newStream;
+ MediaEventListener restartListener = cubeb->StreamInitEvent().Connect(
+ AbstractThread::GetCurrent(),
+ [&](const RefPtr<SmartMockCubebStream>& aCreated) {
+ // Make sure new stream has input, to prevent from getting a
+ // temporary output-only AudioCallbackDriver after closing current
+ // native device but before setting a new native input.
+ if (aCreated->mHasInput) {
+ ASSERT_TRUE(aCreated->mHasOutput);
+ newStream = aCreated;
+ }
+ });
+
+ std::cerr << "Close device " << aCurrentNativeStream->GetInputDeviceID()
+ << std::endl;
+ DispatchFunction([&] {
+ aCurrentNativeTrack->GraphImpl()->AppendMessage(
+ MakeUnique<StopInputProcessing>(aCurrentNativeTrack,
+ aCurrentNativeListener));
+ aCurrentNativeTrack->DisconnectDeviceInput();
+ aCurrentNativeTrack->Destroy();
+ });
+
+ std::cerr << "Wait for the switching" << std::endl;
+ SpinEventLoopUntil<ProcessFailureBehavior::IgnoreAndContinue>(
+ "TEST(TestAudioTrackGraph, SwitchNativeAudioProcessingTrack)"_ns,
+ [&] { return destroyed >= 2 && newStream; });
+
+ destroyListener.Disconnect();
+ restartListener.Disconnect();
+
+ aCurrentNativeStream = nullptr;
+ aNextNativeStream = newStream;
+
+ std::cerr << "Now the native input is device "
+ << aNextNativeStream->GetInputDeviceID() << std::endl;
+ };
+
+ // Open a AudioProcessingTrack for device 1.
+ const CubebUtils::AudioDeviceID device1 = (CubebUtils::AudioDeviceID)1;
+ RefPtr<AudioProcessingTrack> track1 = AudioProcessingTrack::Create(graph);
+ RefPtr<AudioInputProcessing> listener1 = new AudioInputProcessing(1);
+ track1->SetInputProcessing(listener1);
+ track1->GraphImpl()->AppendMessage(
+ MakeUnique<SetPassThrough>(track1, listener1, true));
+ track1->GraphImpl()->AppendMessage(
+ MakeUnique<StartInputProcessing>(track1, listener1));
+ track1->ConnectDeviceInput(device1, listener1, PRINCIPAL_HANDLE_NONE);
+ EXPECT_EQ(track1->DeviceId().value(), device1);
+
+ auto started = Invoke([&] { return graph->NotifyWhenDeviceStarted(track1); });
+
+ RefPtr<SmartMockCubebStream> stream1 = WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(stream1->mHasInput);
+ EXPECT_TRUE(stream1->mHasOutput);
+ EXPECT_EQ(stream1->InputChannels(), 1U);
+ EXPECT_EQ(stream1->GetInputDeviceID(), device1);
+ Unused << WaitFor(started);
+ std::cerr << "Device " << device1 << " is opened (stream " << stream1.get()
+ << ")" << std::endl;
+
+ // Open a AudioProcessingTrack for device 2.
+ const CubebUtils::AudioDeviceID device2 = (CubebUtils::AudioDeviceID)2;
+ RefPtr<AudioProcessingTrack> track2 = AudioProcessingTrack::Create(graph);
+ RefPtr<AudioInputProcessing> listener2 = new AudioInputProcessing(2);
+ track2->SetInputProcessing(listener2);
+ track2->GraphImpl()->AppendMessage(
+ MakeUnique<SetPassThrough>(track2, listener2, true));
+ track2->GraphImpl()->AppendMessage(
+ MakeUnique<StartInputProcessing>(track2, listener2));
+ track2->ConnectDeviceInput(device2, listener2, PRINCIPAL_HANDLE_NONE);
+ EXPECT_EQ(track2->DeviceId().value(), device2);
+
+ RefPtr<SmartMockCubebStream> stream2 = WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(stream2->mHasInput);
+ EXPECT_FALSE(stream2->mHasOutput);
+ EXPECT_EQ(stream2->InputChannels(), 2U);
+ EXPECT_EQ(stream2->GetInputDeviceID(), device2);
+ std::cerr << "Device " << device2 << " is opened (stream " << stream2.get()
+ << ")" << std::endl;
+
+ // Open a AudioProcessingTrack for device 3.
+ const CubebUtils::AudioDeviceID device3 = (CubebUtils::AudioDeviceID)3;
+ RefPtr<AudioProcessingTrack> track3 = AudioProcessingTrack::Create(graph);
+ RefPtr<AudioInputProcessing> listener3 = new AudioInputProcessing(1);
+ track3->SetInputProcessing(listener3);
+ track3->GraphImpl()->AppendMessage(
+ MakeUnique<SetPassThrough>(track3, listener3, true));
+ track3->GraphImpl()->AppendMessage(
+ MakeUnique<StartInputProcessing>(track3, listener3));
+ track3->ConnectDeviceInput(device3, listener3, PRINCIPAL_HANDLE_NONE);
+ EXPECT_EQ(track3->DeviceId().value(), device3);
+
+ RefPtr<SmartMockCubebStream> stream3 = WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(stream3->mHasInput);
+ EXPECT_FALSE(stream3->mHasOutput);
+ EXPECT_EQ(stream3->InputChannels(), 1U);
+ EXPECT_EQ(stream3->GetInputDeviceID(), device3);
+ std::cerr << "Device " << device3 << " is opened (stream " << stream3.get()
+ << ")" << std::endl;
+
+ // Close device 1, so the native input device is switched from device 1 to
+ // device 2.
+ switchNativeDevice(std::move(stream1), track1, listener1, stream2, track2);
+ EXPECT_TRUE(stream2->mHasInput);
+ EXPECT_TRUE(stream2->mHasOutput);
+ EXPECT_EQ(stream2->InputChannels(), 2U);
+ EXPECT_EQ(stream2->GetInputDeviceID(), device2);
+ {
+ NativeInputTrack* native =
+ track2->GraphImpl()->GetNativeInputTrackMainThread();
+ ASSERT_TRUE(!!native);
+ EXPECT_EQ(native->mDeviceId, device2);
+ }
+
+ // Close device 2, so the native input device is switched from device 2 to
+ // device 3.
+ switchNativeDevice(std::move(stream2), track2, listener2, stream3, track3);
+ EXPECT_TRUE(stream3->mHasInput);
+ EXPECT_TRUE(stream3->mHasOutput);
+ EXPECT_EQ(stream3->InputChannels(), 1U);
+ EXPECT_EQ(stream3->GetInputDeviceID(), device3);
+ {
+ NativeInputTrack* native =
+ track3->GraphImpl()->GetNativeInputTrackMainThread();
+ ASSERT_TRUE(!!native);
+ EXPECT_EQ(native->mDeviceId, device3);
+ }
+
+ // Clean up.
+ std::cerr << "Close device " << device3 << std::endl;
+ DispatchFunction([&] {
+ track3->GraphImpl()->AppendMessage(
+ MakeUnique<StopInputProcessing>(track3, listener3));
+ track3->DisconnectDeviceInput();
+ track3->Destroy();
+ });
+ RefPtr<SmartMockCubebStream> destroyedStream =
+ WaitFor(cubeb->StreamDestroyEvent());
+ EXPECT_EQ(destroyedStream.get(), stream3.get());
+ {
+ auto* graphImpl = static_cast<MediaTrackGraphImpl*>(graph);
+ NativeInputTrack* native = graphImpl->GetNativeInputTrackMainThread();
+ ASSERT_TRUE(!native);
+ }
+ std::cerr << "No native input now" << std::endl;
+}
+
+void TestCrossGraphPort(uint32_t aInputRate, uint32_t aOutputRate,
+ float aDriftFactor, uint32_t aBufferMs = 50) {
+ std::cerr << "TestCrossGraphPort input: " << aInputRate
+ << ", output: " << aOutputRate << ", driftFactor: " << aDriftFactor
+ << std::endl;
+
+ MockCubeb* cubeb = new MockCubeb();
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+ auto unforcer = WaitFor(cubeb->ForceAudioThread()).unwrap();
+ Unused << unforcer;
+
+ cubeb->SetStreamStartFreezeEnabled(true);
+
+ /* Primary graph: Create the graph. */
+ MediaTrackGraph* primary = MediaTrackGraphImpl::GetInstance(
+ MediaTrackGraph::SYSTEM_THREAD_DRIVER,
+ /*Window ID*/ 1, /* aShouldResistFingerprinting */ false, aInputRate,
+ nullptr, GetMainThreadSerialEventTarget());
+
+ /* Partner graph: Create the graph. */
+ MediaTrackGraph* partner = MediaTrackGraphImpl::GetInstance(
+ MediaTrackGraph::SYSTEM_THREAD_DRIVER, /*Window ID*/ 1,
+ /* aShouldResistFingerprinting */ false, aOutputRate,
+ /*OutputDeviceID*/ reinterpret_cast<cubeb_devid>(1),
+ GetMainThreadSerialEventTarget());
+
+ const CubebUtils::AudioDeviceID deviceId = (CubebUtils::AudioDeviceID)1;
+
+ RefPtr<AudioProcessingTrack> processingTrack;
+ RefPtr<AudioInputProcessing> listener;
+ auto primaryStarted = Invoke([&] {
+ /* Primary graph: Create input track and open it */
+ processingTrack = AudioProcessingTrack::Create(primary);
+ listener = new AudioInputProcessing(2);
+ processingTrack->GraphImpl()->AppendMessage(
+ MakeUnique<SetPassThrough>(processingTrack, listener, true));
+ processingTrack->SetInputProcessing(listener);
+ processingTrack->GraphImpl()->AppendMessage(
+ MakeUnique<StartInputProcessing>(processingTrack, listener));
+ processingTrack->ConnectDeviceInput(deviceId, listener,
+ PRINCIPAL_HANDLE_NONE);
+ return primary->NotifyWhenDeviceStarted(processingTrack);
+ });
+
+ RefPtr<SmartMockCubebStream> inputStream = WaitFor(cubeb->StreamInitEvent());
+
+ RefPtr<CrossGraphTransmitter> transmitter;
+ RefPtr<MediaInputPort> port;
+ RefPtr<CrossGraphReceiver> receiver;
+ auto partnerStarted = Invoke([&] {
+ /* Partner graph: Create CrossGraphReceiver */
+ receiver = partner->CreateCrossGraphReceiver(primary->GraphRate());
+
+ /* Primary graph: Create CrossGraphTransmitter */
+ transmitter = primary->CreateCrossGraphTransmitter(receiver);
+
+ /* How the input track connects to another ProcessedMediaTrack.
+ * Check in MediaManager how it is connected to AudioStreamTrack. */
+ port = transmitter->AllocateInputPort(processingTrack);
+ receiver->AddAudioOutput((void*)1);
+ return partner->NotifyWhenDeviceStarted(receiver);
+ });
+
+ RefPtr<SmartMockCubebStream> partnerStream =
+ WaitFor(cubeb->StreamInitEvent());
+ partnerStream->SetDriftFactor(aDriftFactor);
+
+ cubeb->SetStreamStartFreezeEnabled(false);
+
+ // One source of non-determinism in this type of test is that inputStream
+ // and partnerStream are started in sequence by the CubebOperation thread pool
+ // (of size 1). To minimize the chance that the stream that starts first sees
+ // an iteration before the other has started - this is a source of pre-silence
+ // - we freeze both on start and thaw them together here.
+ // Note that another source of non-determinism is the fallback driver. Handing
+ // over from the fallback to the audio driver requires first an audio callback
+ // (deterministic with the fake audio thread), then a fallback driver
+ // iteration (non-deterministic, since each graph has its own fallback driver,
+ // each with its own dedicated thread, which we have no control over). This
+ // non-determinism is worrisome, but both fallback drivers are likely to
+ // exhibit similar characteristics, hopefully keeping the level of
+ // non-determinism down sufficiently for this test to pass.
+ inputStream->Thaw();
+ partnerStream->Thaw();
+
+ Unused << WaitFor(primaryStarted);
+ Unused << WaitFor(partnerStarted);
+
+ // Wait for 3s worth of audio data on the receiver stream.
+ DispatchFunction([&] {
+ processingTrack->GraphImpl()->AppendMessage(MakeUnique<GoFaster>(cubeb));
+ });
+ uint32_t totalFrames = 0;
+ WaitUntil(partnerStream->FramesVerifiedEvent(), [&](uint32_t aFrames) {
+ totalFrames += aFrames;
+ return totalFrames > static_cast<uint32_t>(partner->GraphRate() * 3);
+ });
+ cubeb->DontGoFaster();
+
+ DispatchFunction([&] {
+ // Clean up on MainThread
+ receiver->RemoveAudioOutput((void*)1);
+ receiver->Destroy();
+ transmitter->Destroy();
+ port->Destroy();
+ processingTrack->GraphImpl()->AppendMessage(
+ MakeUnique<StopInputProcessing>(processingTrack, listener));
+ processingTrack->DisconnectDeviceInput();
+ processingTrack->Destroy();
+ });
+
+ uint32_t inputFrequency = inputStream->InputFrequency();
+ uint32_t partnerRate = partnerStream->InputSampleRate();
+
+ uint64_t preSilenceSamples;
+ float estimatedFreq;
+ uint32_t nrDiscontinuities;
+ std::tie(preSilenceSamples, estimatedFreq, nrDiscontinuities) =
+ WaitFor(partnerStream->OutputVerificationEvent());
+
+ EXPECT_NEAR(estimatedFreq, inputFrequency / aDriftFactor, 5);
+ uint32_t expectedPreSilence =
+ static_cast<uint32_t>(partnerRate * aDriftFactor / 1000 * aBufferMs);
+ uint32_t margin = partnerRate / 20 /* +/- 50ms */;
+ EXPECT_NEAR(preSilenceSamples, expectedPreSilence, margin);
+ // The waveform from AudioGenerator starts at 0, but we don't control its
+ // ending, so we expect a discontinuity there.
+ EXPECT_LE(nrDiscontinuities, 1U);
+}
+
+TEST(TestAudioTrackGraph, CrossGraphPort)
+{
+ TestCrossGraphPort(44100, 44100, 1);
+ TestCrossGraphPort(44100, 44100, 1.08);
+ TestCrossGraphPort(44100, 44100, 0.92);
+
+ TestCrossGraphPort(48000, 44100, 1);
+ TestCrossGraphPort(48000, 44100, 1.08);
+ TestCrossGraphPort(48000, 44100, 0.92);
+
+ TestCrossGraphPort(44100, 48000, 1);
+ TestCrossGraphPort(44100, 48000, 1.08);
+ TestCrossGraphPort(44100, 48000, 0.92);
+
+ TestCrossGraphPort(52110, 17781, 1);
+ TestCrossGraphPort(52110, 17781, 1.08);
+ TestCrossGraphPort(52110, 17781, 0.92);
+}
+
+TEST(TestAudioTrackGraph, CrossGraphPortLargeBuffer)
+{
+ const int32_t oldBuffering = Preferences::GetInt(DRIFT_BUFFERING_PREF);
+ const int32_t longBuffering = 5000;
+ Preferences::SetInt(DRIFT_BUFFERING_PREF, longBuffering);
+
+ TestCrossGraphPort(44100, 44100, 1.02, longBuffering);
+ TestCrossGraphPort(48000, 44100, 1.08, longBuffering);
+ TestCrossGraphPort(44100, 48000, 0.95, longBuffering);
+ TestCrossGraphPort(52110, 17781, 0.92, longBuffering);
+
+ Preferences::SetInt(DRIFT_BUFFERING_PREF, oldBuffering);
+}
+#endif // MOZ_WEBRTC
diff --git a/dom/media/gtest/TestBenchmarkStorage.cpp b/dom/media/gtest/TestBenchmarkStorage.cpp
new file mode 100644
index 0000000000..0f1eb7e4c4
--- /dev/null
+++ b/dom/media/gtest/TestBenchmarkStorage.cpp
@@ -0,0 +1,92 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/BenchmarkStorageParent.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest-printers.h"
+#include "gtest/gtest.h"
+
+using ::testing::Return;
+using namespace mozilla;
+
+TEST(BenchmarkStorage, MovingAverage)
+{
+ int32_t av = 0;
+ int32_t win = 0;
+ int32_t val = 100;
+ BenchmarkStorageParent::MovingAverage(av, win, val);
+ EXPECT_EQ(av, val) << "1st average";
+ EXPECT_EQ(win, 1) << "1st window";
+
+ av = 50;
+ win = 1;
+ val = 100;
+ BenchmarkStorageParent::MovingAverage(av, win, val);
+ EXPECT_EQ(av, 75) << "2nd average";
+ EXPECT_EQ(win, 2) << "2nd window";
+
+ av = 100;
+ win = 9;
+ val = 90;
+ BenchmarkStorageParent::MovingAverage(av, win, val);
+ EXPECT_EQ(av, 99) << "9th average";
+ EXPECT_EQ(win, 10) << "9th window";
+
+ av = 90;
+ win = 19;
+ val = 90;
+ BenchmarkStorageParent::MovingAverage(av, win, val);
+ EXPECT_EQ(av, 90) << "19th average";
+ EXPECT_EQ(win, 20) << "19th window";
+
+ av = 90;
+ win = 20;
+ val = 100;
+ BenchmarkStorageParent::MovingAverage(av, win, val);
+ EXPECT_EQ(av, 91) << "20th average";
+ EXPECT_EQ(win, 20) << "20th window";
+}
+
+TEST(BenchmarkStorage, ParseStoredValue)
+{
+ int32_t win = 0;
+ int32_t score = BenchmarkStorageParent::ParseStoredValue(1100, win);
+ EXPECT_EQ(win, 1) << "Window";
+ EXPECT_EQ(score, 100) << "Score/Percentage";
+
+ win = 0;
+ score = BenchmarkStorageParent::ParseStoredValue(10099, win);
+ EXPECT_EQ(win, 10) << "Window";
+ EXPECT_EQ(score, 99) << "Score/Percentage";
+
+ win = 0;
+ score = BenchmarkStorageParent::ParseStoredValue(15038, win);
+ EXPECT_EQ(win, 15) << "Window";
+ EXPECT_EQ(score, 38) << "Score/Percentage";
+
+ win = 0;
+ score = BenchmarkStorageParent::ParseStoredValue(20099, win);
+ EXPECT_EQ(win, 20) << "Window";
+ EXPECT_EQ(score, 99) << "Score/Percentage";
+}
+
+TEST(BenchmarkStorage, PrepareStoredValue)
+{
+ int32_t stored_value = BenchmarkStorageParent::PrepareStoredValue(80, 1);
+ EXPECT_EQ(stored_value, 1080) << "Window";
+
+ stored_value = BenchmarkStorageParent::PrepareStoredValue(100, 6);
+ EXPECT_EQ(stored_value, 6100) << "Window";
+
+ stored_value = BenchmarkStorageParent::PrepareStoredValue(1, 10);
+ EXPECT_EQ(stored_value, 10001) << "Window";
+
+ stored_value = BenchmarkStorageParent::PrepareStoredValue(88, 13);
+ EXPECT_EQ(stored_value, 13088) << "Window";
+
+ stored_value = BenchmarkStorageParent::PrepareStoredValue(100, 20);
+ EXPECT_EQ(stored_value, 20100) << "Window";
+}
diff --git a/dom/media/gtest/TestBitWriter.cpp b/dom/media/gtest/TestBitWriter.cpp
new file mode 100644
index 0000000000..8c6b4f7ebf
--- /dev/null
+++ b/dom/media/gtest/TestBitWriter.cpp
@@ -0,0 +1,97 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "BitReader.h"
+#include "BitWriter.h"
+#include "H264.h"
+
+using namespace mozilla;
+
+TEST(BitWriter, BitWriter)
+{
+ RefPtr<MediaByteBuffer> test = new MediaByteBuffer();
+ BitWriter b(test);
+ b.WriteBit(false);
+ b.WriteBits(~1ULL, 1); // ensure that extra bits don't modify byte buffer.
+ b.WriteBits(3, 1);
+ b.WriteUE(1280 / 16 - 1);
+ b.WriteUE(720 / 16 - 1);
+ b.WriteUE(1280);
+ b.WriteUE(720);
+ b.WriteBit(true);
+ b.WriteBit(false);
+ b.WriteBit(true);
+ b.WriteU8(7);
+ b.WriteU32(16356);
+ b.WriteU64(116356);
+ b.WriteBits(~(0ULL) & ~1ULL, 16);
+ b.WriteULEB128(16ULL);
+ b.WriteULEB128(31895793ULL);
+ b.WriteULEB128(426894039235654ULL);
+ const uint32_t length = b.BitCount();
+ b.CloseWithRbspTrailing();
+
+ BitReader c(test);
+
+ EXPECT_EQ(c.ReadBit(), false);
+ EXPECT_EQ(c.ReadBit(), false);
+ EXPECT_EQ(c.ReadBit(), true);
+ EXPECT_EQ(c.ReadUE(), 1280u / 16 - 1);
+ EXPECT_EQ(c.ReadUE(), 720u / 16 - 1);
+ EXPECT_EQ(c.ReadUE(), 1280u);
+ EXPECT_EQ(c.ReadUE(), 720u);
+ EXPECT_EQ(c.ReadBit(), true);
+ EXPECT_EQ(c.ReadBit(), false);
+ EXPECT_EQ(c.ReadBit(), true);
+ EXPECT_EQ(c.ReadBits(8), 7u);
+ EXPECT_EQ(c.ReadU32(), 16356u);
+ EXPECT_EQ(c.ReadU64(), 116356u);
+ EXPECT_EQ(c.ReadBits(16), 0xfffeu);
+ EXPECT_EQ(c.ReadULEB128(), 16ull);
+ EXPECT_EQ(c.ReadULEB128(), 31895793ull);
+ EXPECT_EQ(c.ReadULEB128(), 426894039235654ull);
+ EXPECT_EQ(length, BitReader::GetBitLength(test));
+}
+
+TEST(BitWriter, SPS)
+{
+ uint8_t sps_pps[] = {0x01, 0x4d, 0x40, 0x0c, 0xff, 0xe1, 0x00, 0x1b, 0x67,
+ 0x4d, 0x40, 0x0c, 0xe8, 0x80, 0x80, 0x9d, 0x80, 0xb5,
+ 0x01, 0x01, 0x01, 0x40, 0x00, 0x00, 0x03, 0x00, 0x40,
+ 0x00, 0x00, 0x0f, 0x03, 0xc5, 0x0a, 0x44, 0x80, 0x01,
+ 0x00, 0x04, 0x68, 0xeb, 0xef, 0x20};
+
+ RefPtr<MediaByteBuffer> extraData = new MediaByteBuffer();
+ extraData->AppendElements(sps_pps, sizeof(sps_pps));
+ SPSData spsdata1;
+ bool success = H264::DecodeSPSFromExtraData(extraData, spsdata1);
+ EXPECT_EQ(success, true);
+
+ auto testOutput = [&](uint8_t aProfile, uint8_t aConstraints, uint8_t aLevel,
+ gfx::IntSize aSize, char const* aDesc) {
+ RefPtr<MediaByteBuffer> extraData =
+ H264::CreateExtraData(aProfile, aConstraints, aLevel, aSize);
+ SPSData spsData;
+ success = H264::DecodeSPSFromExtraData(extraData, spsData);
+ EXPECT_EQ(success, true) << aDesc;
+ EXPECT_EQ(spsData.profile_idc, aProfile) << aDesc;
+ EXPECT_EQ(spsData.constraint_set0_flag, (aConstraints >> 7) & 1) << aDesc;
+ EXPECT_EQ(spsData.constraint_set1_flag, (aConstraints >> 6) & 1) << aDesc;
+ EXPECT_EQ(spsData.constraint_set2_flag, (aConstraints >> 5) & 1) << aDesc;
+ EXPECT_EQ(spsData.constraint_set3_flag, (aConstraints >> 4) & 1) << aDesc;
+ EXPECT_EQ(spsData.constraint_set4_flag, (aConstraints >> 3) & 1) << aDesc;
+ EXPECT_EQ(spsData.constraint_set5_flag, (aConstraints >> 2) & 1) << aDesc;
+
+ EXPECT_EQ(spsData.level_idc, aLevel) << aDesc;
+ EXPECT_TRUE(!aSize.IsEmpty());
+ EXPECT_EQ(spsData.pic_width, static_cast<uint32_t>(aSize.width)) << aDesc;
+ EXPECT_EQ(spsData.pic_height, static_cast<uint32_t>(aSize.height)) << aDesc;
+ };
+
+ testOutput(0x42, 0x40, 0x1E, {1920, 1080}, "Constrained Baseline Profile");
+ testOutput(0x4D, 0x00, 0x0B, {300, 300}, "Main Profile");
+ testOutput(0x64, 0x0C, 0x33, {1280, 720}, "Constrained High Profile");
+}
diff --git a/dom/media/gtest/TestBlankVideoDataCreator.cpp b/dom/media/gtest/TestBlankVideoDataCreator.cpp
new file mode 100644
index 0000000000..b30f1cecbe
--- /dev/null
+++ b/dom/media/gtest/TestBlankVideoDataCreator.cpp
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "BlankDecoderModule.h"
+#include "ImageContainer.h"
+
+using namespace mozilla;
+
+TEST(BlankVideoDataCreator, ShouldNotOverflow)
+{
+ RefPtr<MediaRawData> mrd = new MediaRawData();
+ const uint32_t width = 1;
+ const uint32_t height = 1;
+ BlankVideoDataCreator creater(width, height, nullptr);
+ RefPtr<MediaData> data = creater.Create(mrd);
+ EXPECT_NE(data.get(), nullptr);
+}
+
+TEST(BlankVideoDataCreator, ShouldOverflow)
+{
+ RefPtr<MediaRawData> mrd = new MediaRawData();
+ const uint32_t width = UINT_MAX;
+ const uint32_t height = UINT_MAX;
+ BlankVideoDataCreator creater(width, height, nullptr);
+ RefPtr<MediaData> data = creater.Create(mrd);
+ EXPECT_EQ(data.get(), nullptr);
+}
diff --git a/dom/media/gtest/TestBufferReader.cpp b/dom/media/gtest/TestBufferReader.cpp
new file mode 100644
index 0000000000..827e55335d
--- /dev/null
+++ b/dom/media/gtest/TestBufferReader.cpp
@@ -0,0 +1,53 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "BufferReader.h"
+
+using namespace mozilla;
+
+TEST(BufferReader, ReaderCursor)
+{
+ // Allocate a buffer and create a BufferReader.
+ const size_t BUFFER_SIZE = 10;
+ uint8_t buffer[BUFFER_SIZE] = {0};
+
+ const uint8_t* const HEAD = reinterpret_cast<uint8_t*>(buffer);
+ const uint8_t* const TAIL = HEAD + BUFFER_SIZE;
+
+ BufferReader reader(HEAD, BUFFER_SIZE);
+ ASSERT_EQ(reader.Offset(), static_cast<size_t>(0));
+ ASSERT_EQ(reader.Peek(BUFFER_SIZE), HEAD);
+
+ // Keep reading to the end, and make sure the final read failed.
+ const size_t READ_SIZE = 4;
+ ASSERT_NE(BUFFER_SIZE % READ_SIZE, static_cast<size_t>(0));
+ for (const uint8_t* ptr = reader.Peek(0); ptr != nullptr;
+ ptr = reader.Read(READ_SIZE)) {
+ }
+
+ // Check the reading cursor of the BufferReader is correct
+ // after reading and seeking.
+ const uint8_t* tail = reader.Peek(0);
+ const uint8_t* head = reader.Seek(0);
+
+ EXPECT_EQ(head, HEAD);
+ EXPECT_EQ(tail, TAIL);
+}
+
+TEST(BufferReader, UnalignedRead)
+{
+ // Allocate a buffer and create a BufferReader.
+ const size_t BUFFER_SIZE = 5;
+ uint8_t buffer[BUFFER_SIZE] = {0};
+
+ const uint8_t* const HEAD = reinterpret_cast<uint8_t*>(buffer);
+
+ BufferReader reader(HEAD, BUFFER_SIZE);
+ // adjust the offset so that it's unaligned
+ reader.Read(1);
+ // read an int which needs 4 byte alignment
+ reader.ReadType<uint32_t>();
+}
diff --git a/dom/media/gtest/TestCDMStorage.cpp b/dom/media/gtest/TestCDMStorage.cpp
new file mode 100644
index 0000000000..489fc93921
--- /dev/null
+++ b/dom/media/gtest/TestCDMStorage.cpp
@@ -0,0 +1,1347 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ChromiumCDMCallback.h"
+#include "ChromiumCDMParent.h"
+#include "GMPServiceParent.h"
+#include "GMPTestMonitor.h"
+#include "MediaResult.h"
+#include "gtest/gtest.h"
+#include "mozilla/gtest/MozAssertions.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/SpinEventLoopUntil.h"
+#include "nsIFile.h"
+#include "nsCRTGlue.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsNSSComponent.h" //For EnsureNSSInitializedChromeOrContent
+#include "nsThreadUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::gmp;
+
+static already_AddRefed<nsIThread> GetGMPThread() {
+ RefPtr<GeckoMediaPluginService> service =
+ GeckoMediaPluginService::GetGeckoMediaPluginService();
+ nsCOMPtr<nsIThread> thread;
+ EXPECT_NS_SUCCEEDED(service->GetThread(getter_AddRefs(thread)));
+ return thread.forget();
+}
+
+/**
+ * Enumerate files under |aPath| (non-recursive).
+ */
+template <typename T>
+static nsresult EnumerateDir(nsIFile* aPath, T&& aDirIter) {
+ nsCOMPtr<nsIDirectoryEnumerator> iter;
+ nsresult rv = aPath->GetDirectoryEntries(getter_AddRefs(iter));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIFile> entry;
+ while (NS_SUCCEEDED(iter->GetNextFile(getter_AddRefs(entry))) && entry) {
+ aDirIter(entry);
+ }
+ return NS_OK;
+}
+
+/**
+ * Enumerate files under $profileDir/gmp/$platform/gmp-fake/$aDir/
+ * (non-recursive).
+ */
+template <typename T>
+static nsresult EnumerateCDMStorageDir(const nsACString& aDir, T&& aDirIter) {
+ RefPtr<GeckoMediaPluginServiceParent> service =
+ GeckoMediaPluginServiceParent::GetSingleton();
+ MOZ_ASSERT(service);
+
+ // $profileDir/gmp/$platform/
+ nsCOMPtr<nsIFile> path;
+ nsresult rv = service->GetStorageDir(getter_AddRefs(path));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // $profileDir/gmp/$platform/gmp-fake/
+ rv = path->Append(u"gmp-fake"_ns);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // $profileDir/gmp/$platform/gmp-fake/$aDir/
+ rv = path->AppendNative(aDir);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ return EnumerateDir(path, aDirIter);
+}
+
+class GMPShutdownObserver : public nsIRunnable, public nsIObserver {
+ public:
+ GMPShutdownObserver(already_AddRefed<nsIRunnable> aShutdownTask,
+ already_AddRefed<nsIRunnable> Continuation,
+ const nsACString& aNodeId)
+ : mShutdownTask(aShutdownTask),
+ mContinuation(Continuation),
+ mNodeId(NS_ConvertUTF8toUTF16(aNodeId)) {}
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ NS_IMETHOD Run() override {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ EXPECT_TRUE(observerService);
+ observerService->AddObserver(this, "gmp-shutdown", false);
+
+ nsCOMPtr<nsIThread> thread(GetGMPThread());
+ thread->Dispatch(mShutdownTask, NS_DISPATCH_NORMAL);
+ return NS_OK;
+ }
+
+ NS_IMETHOD Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aSomeData) override {
+ if (!strcmp(aTopic, "gmp-shutdown") &&
+ mNodeId.Equals(nsDependentString(aSomeData))) {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ EXPECT_TRUE(observerService);
+ observerService->RemoveObserver(this, "gmp-shutdown");
+ nsCOMPtr<nsIThread> thread(GetGMPThread());
+ thread->Dispatch(mContinuation, NS_DISPATCH_NORMAL);
+ }
+ return NS_OK;
+ }
+
+ private:
+ virtual ~GMPShutdownObserver() = default;
+ nsCOMPtr<nsIRunnable> mShutdownTask;
+ nsCOMPtr<nsIRunnable> mContinuation;
+ const nsString mNodeId;
+};
+
+NS_IMPL_ISUPPORTS(GMPShutdownObserver, nsIRunnable, nsIObserver)
+
+class NotifyObserversTask : public Runnable {
+ public:
+ explicit NotifyObserversTask(const char* aTopic)
+ : mozilla::Runnable("NotifyObserversTask"), mTopic(aTopic) {}
+ NS_IMETHOD Run() override {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ if (observerService) {
+ observerService->NotifyObservers(nullptr, mTopic, nullptr);
+ }
+ return NS_OK;
+ }
+ const char* mTopic;
+};
+
+class ClearCDMStorageTask : public nsIRunnable, public nsIObserver {
+ public:
+ ClearCDMStorageTask(already_AddRefed<nsIRunnable> Continuation,
+ nsIThread* aTarget, PRTime aSince)
+ : mContinuation(Continuation), mTarget(aTarget), mSince(aSince) {}
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ NS_IMETHOD Run() override {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ EXPECT_TRUE(observerService);
+ observerService->AddObserver(this, "gmp-clear-storage-complete", false);
+ if (observerService) {
+ nsAutoString str;
+ if (mSince >= 0) {
+ str.AppendInt(static_cast<int64_t>(mSince));
+ }
+ observerService->NotifyObservers(nullptr, "browser:purge-session-history",
+ str.Data());
+ }
+ return NS_OK;
+ }
+
+ NS_IMETHOD Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aSomeData) override {
+ if (!strcmp(aTopic, "gmp-clear-storage-complete")) {
+ nsCOMPtr<nsIObserverService> observerService =
+ mozilla::services::GetObserverService();
+ EXPECT_TRUE(observerService);
+ observerService->RemoveObserver(this, "gmp-clear-storage-complete");
+ mTarget->Dispatch(mContinuation, NS_DISPATCH_NORMAL);
+ }
+ return NS_OK;
+ }
+
+ private:
+ virtual ~ClearCDMStorageTask() = default;
+ nsCOMPtr<nsIRunnable> mContinuation;
+ nsCOMPtr<nsIThread> mTarget;
+ const PRTime mSince;
+};
+
+NS_IMPL_ISUPPORTS(ClearCDMStorageTask, nsIRunnable, nsIObserver)
+
+static void ClearCDMStorage(already_AddRefed<nsIRunnable> aContinuation,
+ nsIThread* aTarget, PRTime aSince = -1) {
+ RefPtr<ClearCDMStorageTask> task(
+ new ClearCDMStorageTask(std::move(aContinuation), aTarget, aSince));
+ SchedulerGroup::Dispatch(TaskCategory::Other, task.forget());
+}
+
+static void SimulatePBModeExit() {
+ NS_DispatchAndSpinEventLoopUntilComplete(
+ "SimulatePBModeExit"_ns, GetMainThreadSerialEventTarget(),
+ MakeAndAddRef<NotifyObserversTask>("last-pb-context-exited"));
+}
+
+class TestGetNodeIdCallback : public GetNodeIdCallback {
+ public:
+ TestGetNodeIdCallback(nsCString& aNodeId, nsresult& aResult)
+ : mNodeId(aNodeId), mResult(aResult) {}
+
+ void Done(nsresult aResult, const nsACString& aNodeId) {
+ mResult = aResult;
+ mNodeId = aNodeId;
+ }
+
+ private:
+ nsCString& mNodeId;
+ nsresult& mResult;
+};
+
+static NodeIdParts GetNodeIdParts(const nsAString& aOrigin,
+ const nsAString& aTopLevelOrigin,
+ const nsAString& aGmpName, bool aInPBMode) {
+ OriginAttributes attrs;
+ attrs.mPrivateBrowsingId = aInPBMode ? 1 : 0;
+
+ nsAutoCString suffix;
+ attrs.CreateSuffix(suffix);
+
+ nsAutoString origin;
+ origin.Assign(aOrigin);
+ origin.Append(NS_ConvertUTF8toUTF16(suffix));
+
+ nsAutoString topLevelOrigin;
+ topLevelOrigin.Assign(aTopLevelOrigin);
+ topLevelOrigin.Append(NS_ConvertUTF8toUTF16(suffix));
+ return NodeIdParts{origin, topLevelOrigin, nsString(aGmpName)};
+}
+
+static nsCString GetNodeId(const nsAString& aOrigin,
+ const nsAString& aTopLevelOrigin, bool aInPBMode) {
+ RefPtr<GeckoMediaPluginServiceParent> service =
+ GeckoMediaPluginServiceParent::GetSingleton();
+ EXPECT_TRUE(service);
+ nsCString nodeId;
+ nsresult result;
+ UniquePtr<GetNodeIdCallback> callback(
+ new TestGetNodeIdCallback(nodeId, result));
+
+ OriginAttributes attrs;
+ attrs.mPrivateBrowsingId = aInPBMode ? 1 : 0;
+
+ nsAutoCString suffix;
+ attrs.CreateSuffix(suffix);
+
+ nsAutoString origin;
+ origin.Assign(aOrigin);
+ origin.Append(NS_ConvertUTF8toUTF16(suffix));
+
+ nsAutoString topLevelOrigin;
+ topLevelOrigin.Assign(aTopLevelOrigin);
+ topLevelOrigin.Append(NS_ConvertUTF8toUTF16(suffix));
+
+ // We rely on the fact that the GetNodeId implementation for
+ // GeckoMediaPluginServiceParent is synchronous.
+ nsresult rv = service->GetNodeId(origin, topLevelOrigin, u"gmp-fake"_ns,
+ std::move(callback));
+ EXPECT_TRUE(NS_SUCCEEDED(rv) && NS_SUCCEEDED(result));
+ return nodeId;
+}
+
+static bool IsCDMStorageIsEmpty() {
+ RefPtr<GeckoMediaPluginServiceParent> service =
+ GeckoMediaPluginServiceParent::GetSingleton();
+ MOZ_ASSERT(service);
+ nsCOMPtr<nsIFile> storage;
+ nsresult rv = service->GetStorageDir(getter_AddRefs(storage));
+ EXPECT_NS_SUCCEEDED(rv);
+ bool exists = false;
+ if (storage) {
+ storage->Exists(&exists);
+ }
+ return !exists;
+}
+
+static void AssertIsOnGMPThread() {
+ RefPtr<GeckoMediaPluginService> service =
+ GeckoMediaPluginService::GetGeckoMediaPluginService();
+ MOZ_ASSERT(service);
+ nsCOMPtr<nsIThread> thread;
+ service->GetThread(getter_AddRefs(thread));
+ MOZ_ASSERT(thread);
+ nsCOMPtr<nsIThread> currentThread;
+ DebugOnly<nsresult> rv = NS_GetCurrentThread(getter_AddRefs(currentThread));
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ MOZ_ASSERT(currentThread == thread);
+}
+
+class CDMStorageTest {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CDMStorageTest)
+
+ void DoTest(void (CDMStorageTest::*aTestMethod)()) {
+ EnsureNSSInitializedChromeOrContent();
+ nsCOMPtr<nsIThread> thread(GetGMPThread());
+ ClearCDMStorage(
+ NewRunnableMethod("CDMStorageTest::DoTest", this, aTestMethod), thread);
+ AwaitFinished();
+ }
+
+ CDMStorageTest() : mMonitor("CDMStorageTest"), mFinished(false) {}
+
+ void Update(const nsCString& aMessage) {
+ nsTArray<uint8_t> msg;
+ msg.AppendElements(aMessage.get(), aMessage.Length());
+ mCDM->UpdateSession("fake-session-id"_ns, 1, msg);
+ }
+
+ void TestGetNodeId() {
+ AssertIsOnGMPThread();
+
+ EXPECT_TRUE(IsCDMStorageIsEmpty());
+
+ const nsString origin1 = u"http://example1.com"_ns;
+ const nsString origin2 = u"http://example2.org"_ns;
+
+ nsCString PBnodeId1 = GetNodeId(origin1, origin2, true);
+ nsCString PBnodeId2 = GetNodeId(origin1, origin2, true);
+
+ // Node ids for the same origins should be the same in PB mode.
+ EXPECT_TRUE(PBnodeId1.Equals(PBnodeId2));
+
+ nsCString PBnodeId3 = GetNodeId(origin2, origin1, true);
+
+ // Node ids with origin and top level origin swapped should be different.
+ EXPECT_TRUE(!PBnodeId3.Equals(PBnodeId1));
+
+ // Getting node ids in PB mode should not result in the node id being
+ // stored.
+ EXPECT_TRUE(IsCDMStorageIsEmpty());
+
+ nsCString nodeId1 = GetNodeId(origin1, origin2, false);
+ nsCString nodeId2 = GetNodeId(origin1, origin2, false);
+
+ // NodeIds for the same origin pair in non-pb mode should be the same.
+ EXPECT_TRUE(nodeId1.Equals(nodeId2));
+
+ // Node ids for a given origin pair should be different for the PB origins
+ // should be the same in PB mode.
+ EXPECT_TRUE(!PBnodeId1.Equals(nodeId1));
+ EXPECT_TRUE(!PBnodeId2.Equals(nodeId2));
+
+ nsCOMPtr<nsIThread> thread(GetGMPThread());
+ ClearCDMStorage(NewRunnableMethod<nsCString>(
+ "CDMStorageTest::TestGetNodeId_Continuation", this,
+ &CDMStorageTest::TestGetNodeId_Continuation, nodeId1),
+ thread);
+ }
+
+ void TestGetNodeId_Continuation(nsCString aNodeId1) {
+ EXPECT_TRUE(IsCDMStorageIsEmpty());
+
+ // Once we clear storage, the node ids generated for the same origin-pair
+ // should be different.
+ const nsString origin1 = u"http://example1.com"_ns;
+ const nsString origin2 = u"http://example2.org"_ns;
+ nsCString nodeId3 = GetNodeId(origin1, origin2, false);
+ EXPECT_TRUE(!aNodeId1.Equals(nodeId3));
+
+ SetFinished();
+ }
+
+ void CreateDecryptor(const nsAString& aOrigin,
+ const nsAString& aTopLevelOrigin, bool aInPBMode,
+ const nsCString& aUpdate) {
+ nsTArray<nsCString> updates;
+ updates.AppendElement(aUpdate);
+ CreateDecryptor(aOrigin, aTopLevelOrigin, aInPBMode, std::move(updates));
+ }
+
+ void CreateDecryptor(const nsAString& aOrigin,
+ const nsAString& aTopLevelOrigin, bool aInPBMode,
+ nsTArray<nsCString>&& aUpdates) {
+ CreateDecryptor(
+ GetNodeIdParts(aOrigin, aTopLevelOrigin, u"gmp-fake"_ns, aInPBMode),
+ std::move(aUpdates));
+ }
+
+ void CreateDecryptor(const NodeIdParts& aNodeId,
+ nsTArray<nsCString>&& aUpdates) {
+ RefPtr<GeckoMediaPluginService> service =
+ GeckoMediaPluginService::GetGeckoMediaPluginService();
+ EXPECT_TRUE(service);
+
+ nsCString keySystem{"fake"_ns};
+
+ RefPtr<CDMStorageTest> self = this;
+ RefPtr<gmp::GetCDMParentPromise> promise =
+ service->GetCDM(aNodeId, keySystem, nullptr);
+ nsCOMPtr<nsISerialEventTarget> thread = GetGMPThread();
+ promise->Then(
+ thread, __func__,
+ [self, updates = std::move(aUpdates),
+ thread](RefPtr<gmp::ChromiumCDMParent> cdm) mutable {
+ self->mCDM = cdm;
+ EXPECT_TRUE(!!self->mCDM);
+ self->mCallback.reset(new CallbackProxy(self));
+ nsCString failureReason;
+ self->mCDM
+ ->Init(self->mCallback.get(), false, true,
+ GetMainThreadSerialEventTarget())
+ ->Then(
+ thread, __func__,
+ [self, updates = std::move(updates)] {
+ for (const auto& update : updates) {
+ self->Update(update);
+ }
+ },
+ [](MediaResult rv) { EXPECT_TRUE(false); });
+ },
+ [](MediaResult rv) { EXPECT_TRUE(false); });
+ }
+
+ void TestBasicStorage() {
+ AssertIsOnGMPThread();
+ EXPECT_TRUE(IsCDMStorageIsEmpty());
+
+ RefPtr<GeckoMediaPluginService> service =
+ GeckoMediaPluginService::GetGeckoMediaPluginService();
+
+ // Send a message to the fake GMP for it to run its own tests internally.
+ // It sends us a "test-storage complete" message when its passed, or
+ // some other message if its tests fail.
+ Expect("test-storage complete"_ns,
+ NewRunnableMethod("CDMStorageTest::SetFinished", this,
+ &CDMStorageTest::SetFinished));
+
+ CreateDecryptor(u"http://example1.com"_ns, u"http://example2.com"_ns, false,
+ "test-storage"_ns);
+ }
+
+ /**
+ * 1. Generate storage data for some sites.
+ * 2. Forget about one of the sites.
+ * 3. Check if the storage data for the forgotten site are erased correctly.
+ * 4. Check if the storage data for other sites remain unchanged.
+ */
+ void TestForgetThisSite() {
+ AssertIsOnGMPThread();
+ EXPECT_TRUE(IsCDMStorageIsEmpty());
+
+ // Generate storage data for some site.
+ nsCOMPtr<nsIRunnable> r = NewRunnableMethod(
+ "CDMStorageTest::TestForgetThisSite_AnotherSite", this,
+ &CDMStorageTest::TestForgetThisSite_AnotherSite);
+ Expect("test-storage complete"_ns, r.forget());
+
+ CreateDecryptor(u"http://example1.com"_ns, u"http://example2.com"_ns, false,
+ "test-storage"_ns);
+ }
+
+ void TestForgetThisSite_AnotherSite() {
+ Shutdown();
+
+ // Generate storage data for another site.
+ nsCOMPtr<nsIRunnable> r = NewRunnableMethod(
+ "CDMStorageTest::TestForgetThisSite_CollectSiteInfo", this,
+ &CDMStorageTest::TestForgetThisSite_CollectSiteInfo);
+ Expect("test-storage complete"_ns, r.forget());
+
+ CreateDecryptor(u"http://example3.com"_ns, u"http://example4.com"_ns, false,
+ "test-storage"_ns);
+ }
+
+ struct NodeInfo {
+ explicit NodeInfo(const nsACString& aSite,
+ const mozilla::OriginAttributesPattern& aPattern)
+ : siteToForget(aSite), mPattern(aPattern) {}
+ nsCString siteToForget;
+ mozilla::OriginAttributesPattern mPattern;
+ nsTArray<nsCString> mExpectedRemainingNodeIds;
+ };
+
+ class NodeIdCollector {
+ public:
+ explicit NodeIdCollector(NodeInfo* aInfo) : mNodeInfo(aInfo) {}
+ void operator()(nsIFile* aFile) {
+ nsCString salt;
+ nsresult rv = ReadSalt(aFile, salt);
+ ASSERT_NS_SUCCEEDED(rv);
+ if (!MatchOrigin(aFile, mNodeInfo->siteToForget, mNodeInfo->mPattern)) {
+ mNodeInfo->mExpectedRemainingNodeIds.AppendElement(salt);
+ }
+ }
+
+ private:
+ NodeInfo* mNodeInfo;
+ };
+
+ void TestForgetThisSite_CollectSiteInfo() {
+ mozilla::OriginAttributesPattern pattern;
+
+ UniquePtr<NodeInfo> siteInfo(
+ new NodeInfo("http://example1.com"_ns, pattern));
+ // Collect nodeIds that are expected to remain for later comparison.
+ EnumerateCDMStorageDir("id"_ns, NodeIdCollector(siteInfo.get()));
+ // Invoke "Forget this site" on the main thread.
+ SchedulerGroup::Dispatch(
+ TaskCategory::Other,
+ NewRunnableMethod<UniquePtr<NodeInfo>&&>(
+ "CDMStorageTest::TestForgetThisSite_Forget", this,
+ &CDMStorageTest::TestForgetThisSite_Forget, std::move(siteInfo)));
+ }
+
+ void TestForgetThisSite_Forget(UniquePtr<NodeInfo>&& aSiteInfo) {
+ RefPtr<GeckoMediaPluginServiceParent> service =
+ GeckoMediaPluginServiceParent::GetSingleton();
+ service->ForgetThisSiteNative(
+ NS_ConvertUTF8toUTF16(aSiteInfo->siteToForget), aSiteInfo->mPattern);
+
+ nsCOMPtr<nsIThread> thread;
+ service->GetThread(getter_AddRefs(thread));
+
+ nsCOMPtr<nsIRunnable> r = NewRunnableMethod<UniquePtr<NodeInfo>&&>(
+ "CDMStorageTest::TestForgetThisSite_Verify", this,
+ &CDMStorageTest::TestForgetThisSite_Verify, std::move(aSiteInfo));
+ thread->Dispatch(r, NS_DISPATCH_NORMAL);
+
+ nsCOMPtr<nsIRunnable> f = NewRunnableMethod(
+ "CDMStorageTest::SetFinished", this, &CDMStorageTest::SetFinished);
+ thread->Dispatch(f, NS_DISPATCH_NORMAL);
+ }
+
+ class NodeIdVerifier {
+ public:
+ explicit NodeIdVerifier(const NodeInfo* aInfo)
+ : mNodeInfo(aInfo),
+ mExpectedRemainingNodeIds(aInfo->mExpectedRemainingNodeIds.Clone()) {}
+ void operator()(nsIFile* aFile) {
+ nsCString salt;
+ nsresult rv = ReadSalt(aFile, salt);
+ ASSERT_NS_SUCCEEDED(rv);
+ // Shouldn't match the origin if we clear correctly.
+ EXPECT_FALSE(
+ MatchOrigin(aFile, mNodeInfo->siteToForget, mNodeInfo->mPattern))
+ << "Found files persisted that match against a site that should "
+ "have been removed!";
+ // Check if remaining nodeIDs are as expected.
+ EXPECT_TRUE(mExpectedRemainingNodeIds.RemoveElement(salt))
+ << "Failed to remove salt from expected remaining node ids. This "
+ "indicates storage that should be forgotten is still persisted!";
+ }
+ ~NodeIdVerifier() {
+ EXPECT_TRUE(mExpectedRemainingNodeIds.IsEmpty())
+ << "Some expected remaining node ids were not checked against. This "
+ "indicates that data we expected to find in storage was missing!";
+ }
+
+ private:
+ const NodeInfo* mNodeInfo;
+ nsTArray<nsCString> mExpectedRemainingNodeIds;
+ };
+
+ class StorageVerifier {
+ public:
+ explicit StorageVerifier(const NodeInfo* aInfo)
+ : mExpectedRemainingNodeIds(aInfo->mExpectedRemainingNodeIds.Clone()) {}
+ void operator()(nsIFile* aFile) {
+ nsCString salt;
+ nsresult rv = aFile->GetNativeLeafName(salt);
+ ASSERT_NS_SUCCEEDED(rv);
+ EXPECT_TRUE(mExpectedRemainingNodeIds.RemoveElement(salt))
+ << "Failed to remove salt from expected remaining node ids. This "
+ "indicates storage that should be forgotten is still persisted!";
+ }
+ ~StorageVerifier() {
+ EXPECT_TRUE(mExpectedRemainingNodeIds.IsEmpty())
+ << "Some expected remaining node ids were not checked against. This "
+ "indicates that data we expected to find in storage was missing!";
+ }
+
+ private:
+ nsTArray<nsCString> mExpectedRemainingNodeIds;
+ };
+
+ void TestForgetThisSite_Verify(UniquePtr<NodeInfo>&& aSiteInfo) {
+ nsresult rv =
+ EnumerateCDMStorageDir("id"_ns, NodeIdVerifier(aSiteInfo.get()));
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = EnumerateCDMStorageDir("storage"_ns, StorageVerifier(aSiteInfo.get()));
+ EXPECT_NS_SUCCEEDED(rv);
+ }
+
+ /**
+ * 1. Generate storage data for some sites.
+ * 2. Forget about base domain example1.com
+ * 3. Check if the storage data for the forgotten site are erased correctly.
+ * 4. Check if the storage data for other sites remain unchanged.
+ */
+ void TestForgetThisBaseDomain() {
+ AssertIsOnGMPThread();
+ EXPECT_TRUE(IsCDMStorageIsEmpty());
+
+ // Generate storage data for some site.
+ nsCOMPtr<nsIRunnable> r = NewRunnableMethod(
+ "CDMStorageTest::TestForgetThisBaseDomain_SecondSite", this,
+ &CDMStorageTest::TestForgetThisBaseDomain_SecondSite);
+ Expect("test-storage complete"_ns, r.forget());
+
+ CreateDecryptor(u"http://media.example1.com"_ns,
+ u"http://tld.example2.com"_ns, false, "test-storage"_ns);
+ }
+
+ void TestForgetThisBaseDomain_SecondSite() {
+ Shutdown();
+
+ // Generate storage data for another site.
+ nsCOMPtr<nsIRunnable> r = NewRunnableMethod(
+ "CDMStorageTest::TestForgetThisBaseDomain_ThirdSite", this,
+ &CDMStorageTest::TestForgetThisBaseDomain_ThirdSite);
+ Expect("test-storage complete"_ns, r.forget());
+
+ CreateDecryptor(u"http://media.somewhereelse.com"_ns,
+ u"http://home.example1.com"_ns, false, "test-storage"_ns);
+ }
+
+ void TestForgetThisBaseDomain_ThirdSite() {
+ Shutdown();
+
+ // Generate storage data for another site.
+ nsCOMPtr<nsIRunnable> r = NewRunnableMethod(
+ "CDMStorageTest::TestForgetThisBaseDomain_CollectSiteInfo", this,
+ &CDMStorageTest::TestForgetThisBaseDomain_CollectSiteInfo);
+ Expect("test-storage complete"_ns, r.forget());
+
+ CreateDecryptor(u"http://media.example3.com"_ns,
+ u"http://tld.long-example1.com"_ns, false,
+ "test-storage"_ns);
+ }
+
+ struct BaseDomainNodeInfo {
+ explicit BaseDomainNodeInfo(const nsACString& aBaseDomain)
+ : baseDomainToForget(aBaseDomain) {}
+ nsCString baseDomainToForget;
+
+ nsTArray<nsCString> mExpectedRemainingNodeIds;
+ };
+
+ class BaseDomainNodeIdCollector {
+ public:
+ explicit BaseDomainNodeIdCollector(BaseDomainNodeInfo* aInfo)
+ : mNodeInfo(aInfo) {}
+ void operator()(nsIFile* aFile) {
+ nsCString salt;
+ nsresult rv = ReadSalt(aFile, salt);
+ ASSERT_NS_SUCCEEDED(rv);
+ if (!MatchBaseDomain(aFile, mNodeInfo->baseDomainToForget)) {
+ mNodeInfo->mExpectedRemainingNodeIds.AppendElement(salt);
+ }
+ }
+
+ private:
+ BaseDomainNodeInfo* mNodeInfo;
+ };
+
+ void TestForgetThisBaseDomain_CollectSiteInfo() {
+ UniquePtr<BaseDomainNodeInfo> siteInfo(
+ new BaseDomainNodeInfo("example1.com"_ns));
+ // Collect nodeIds that are expected to remain for later comparison.
+ EnumerateCDMStorageDir("id"_ns, BaseDomainNodeIdCollector(siteInfo.get()));
+ // Invoke "ForgetThisBaseDomain" on the main thread.
+ SchedulerGroup::Dispatch(
+ TaskCategory::Other,
+ NewRunnableMethod<UniquePtr<BaseDomainNodeInfo>&&>(
+ "CDMStorageTest::TestForgetThisBaseDomain_Forget", this,
+ &CDMStorageTest::TestForgetThisBaseDomain_Forget,
+ std::move(siteInfo)));
+ }
+
+ void TestForgetThisBaseDomain_Forget(
+ UniquePtr<BaseDomainNodeInfo>&& aSiteInfo) {
+ RefPtr<GeckoMediaPluginServiceParent> service =
+ GeckoMediaPluginServiceParent::GetSingleton();
+ service->ForgetThisBaseDomain(
+ NS_ConvertUTF8toUTF16(aSiteInfo->baseDomainToForget));
+
+ nsCOMPtr<nsIThread> thread;
+ service->GetThread(getter_AddRefs(thread));
+
+ nsCOMPtr<nsIRunnable> r =
+ NewRunnableMethod<UniquePtr<BaseDomainNodeInfo>&&>(
+ "CDMStorageTest::TestForgetThisBaseDomain_Verify", this,
+ &CDMStorageTest::TestForgetThisBaseDomain_Verify,
+ std::move(aSiteInfo));
+ thread->Dispatch(r, NS_DISPATCH_NORMAL);
+
+ nsCOMPtr<nsIRunnable> f = NewRunnableMethod(
+ "CDMStorageTest::SetFinished", this, &CDMStorageTest::SetFinished);
+ thread->Dispatch(f, NS_DISPATCH_NORMAL);
+ }
+
+ class BaseDomainNodeIdVerifier {
+ public:
+ explicit BaseDomainNodeIdVerifier(const BaseDomainNodeInfo* aInfo)
+ : mNodeInfo(aInfo),
+ mExpectedRemainingNodeIds(aInfo->mExpectedRemainingNodeIds.Clone()) {}
+ void operator()(nsIFile* aFile) {
+ nsCString salt;
+ nsresult rv = ReadSalt(aFile, salt);
+ ASSERT_NS_SUCCEEDED(rv);
+ // Shouldn't match the origin if we clear correctly.
+ EXPECT_FALSE(MatchBaseDomain(aFile, mNodeInfo->baseDomainToForget))
+ << "Found files persisted that match against a domain that should "
+ "have been removed!";
+ // Check if remaining nodeIDs are as expected.
+ EXPECT_TRUE(mExpectedRemainingNodeIds.RemoveElement(salt))
+ << "Failed to remove salt from expected remaining node ids. This "
+ "indicates storage that should be forgotten is still persisted!";
+ }
+ ~BaseDomainNodeIdVerifier() {
+ EXPECT_TRUE(mExpectedRemainingNodeIds.IsEmpty())
+ << "Some expected remaining node ids were not checked against. This "
+ "indicates that data we expected to find in storage was missing!";
+ }
+
+ private:
+ const BaseDomainNodeInfo* mNodeInfo;
+ nsTArray<nsCString> mExpectedRemainingNodeIds;
+ };
+
+ class BaseDomainStorageVerifier {
+ public:
+ explicit BaseDomainStorageVerifier(const BaseDomainNodeInfo* aInfo)
+ : mExpectedRemainingNodeIds(aInfo->mExpectedRemainingNodeIds.Clone()) {}
+ void operator()(nsIFile* aFile) {
+ nsCString salt;
+ nsresult rv = aFile->GetNativeLeafName(salt);
+ ASSERT_NS_SUCCEEDED(rv);
+ EXPECT_TRUE(mExpectedRemainingNodeIds.RemoveElement(salt))
+ << "Failed to remove salt from expected remaining node ids. This "
+ "indicates storage that should be forgotten is still persisted!";
+ ;
+ }
+ ~BaseDomainStorageVerifier() {
+ EXPECT_TRUE(mExpectedRemainingNodeIds.IsEmpty())
+ << "Some expected remaining node ids were not checked against. This "
+ "indicates that data we expected to find in storage was missing!";
+ ;
+ }
+
+ private:
+ nsTArray<nsCString> mExpectedRemainingNodeIds;
+ };
+
+ void TestForgetThisBaseDomain_Verify(
+ UniquePtr<BaseDomainNodeInfo>&& aSiteInfo) {
+ nsresult rv = EnumerateCDMStorageDir(
+ "id"_ns, BaseDomainNodeIdVerifier(aSiteInfo.get()));
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = EnumerateCDMStorageDir("storage"_ns,
+ BaseDomainStorageVerifier(aSiteInfo.get()));
+ EXPECT_NS_SUCCEEDED(rv);
+ }
+
+ /**
+ * 1. Generate some storage data.
+ * 2. Find the max mtime |t| in $profileDir/gmp/$platform/gmp-fake/id/.
+ * 3. Pass |t| to clear recent history.
+ * 4. Check if all directories in $profileDir/gmp/$platform/gmp-fake/id/ and
+ * $profileDir/gmp/$platform/gmp-fake/storage are removed.
+ */
+ void TestClearRecentHistory1() {
+ AssertIsOnGMPThread();
+ EXPECT_TRUE(IsCDMStorageIsEmpty());
+
+ // Generate storage data for some site.
+ nsCOMPtr<nsIRunnable> r =
+ NewRunnableMethod("CDMStorageTest::TestClearRecentHistory1_Clear", this,
+ &CDMStorageTest::TestClearRecentHistory1_Clear);
+ Expect("test-storage complete"_ns, r.forget());
+
+ CreateDecryptor(u"http://example1.com"_ns, u"http://example2.com"_ns, false,
+ "test-storage"_ns);
+ }
+
+ /**
+ * 1. Generate some storage data.
+ * 2. Find the max mtime |t| in $profileDir/gmp/$platform/gmp-fake/storage/.
+ * 3. Pass |t| to clear recent history.
+ * 4. Check if all directories in $profileDir/gmp/$platform/gmp-fake/id/ and
+ * $profileDir/gmp/$platform/gmp-fake/storage are removed.
+ */
+ void TestClearRecentHistory2() {
+ AssertIsOnGMPThread();
+ EXPECT_TRUE(IsCDMStorageIsEmpty());
+
+ // Generate storage data for some site.
+ nsCOMPtr<nsIRunnable> r =
+ NewRunnableMethod("CDMStorageTest::TestClearRecentHistory2_Clear", this,
+ &CDMStorageTest::TestClearRecentHistory2_Clear);
+ Expect("test-storage complete"_ns, r.forget());
+
+ CreateDecryptor(u"http://example1.com"_ns, u"http://example2.com"_ns, false,
+ "test-storage"_ns);
+ }
+
+ /**
+ * 1. Generate some storage data.
+ * 2. Find the max mtime |t| in $profileDir/gmp/$platform/gmp-fake/storage/.
+ * 3. Pass |t+1| to clear recent history.
+ * 4. Check if all directories in $profileDir/gmp/$platform/gmp-fake/id/ and
+ * $profileDir/gmp/$platform/gmp-fake/storage remain unchanged.
+ */
+ void TestClearRecentHistory3() {
+ AssertIsOnGMPThread();
+ EXPECT_TRUE(IsCDMStorageIsEmpty());
+
+ // Generate storage data for some site.
+ nsCOMPtr<nsIRunnable> r =
+ NewRunnableMethod("CDMStorageTest::TestClearRecentHistory3_Clear", this,
+ &CDMStorageTest::TestClearRecentHistory3_Clear);
+ Expect("test-storage complete"_ns, r.forget());
+
+ CreateDecryptor(u"http://example1.com"_ns, u"http://example2.com"_ns, false,
+ "test-storage"_ns);
+ }
+
+ class MaxMTimeFinder {
+ public:
+ MaxMTimeFinder() : mMaxTime(0) {}
+ void operator()(nsIFile* aFile) {
+ PRTime lastModified;
+ nsresult rv = aFile->GetLastModifiedTime(&lastModified);
+ if (NS_SUCCEEDED(rv) && lastModified > mMaxTime) {
+ mMaxTime = lastModified;
+ }
+ EnumerateDir(aFile, *this);
+ }
+ PRTime GetResult() const { return mMaxTime; }
+
+ private:
+ PRTime mMaxTime;
+ };
+
+ void TestClearRecentHistory1_Clear() {
+ MaxMTimeFinder f;
+ nsresult rv = EnumerateCDMStorageDir("id"_ns, f);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ nsCOMPtr<nsIRunnable> r = NewRunnableMethod(
+ "CDMStorageTest::TestClearRecentHistory_CheckEmpty", this,
+ &CDMStorageTest::TestClearRecentHistory_CheckEmpty);
+ nsCOMPtr<nsIThread> t(GetGMPThread());
+ ClearCDMStorage(r.forget(), t, f.GetResult());
+ }
+
+ void TestClearRecentHistory2_Clear() {
+ MaxMTimeFinder f;
+ nsresult rv = EnumerateCDMStorageDir("storage"_ns, f);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ nsCOMPtr<nsIRunnable> r = NewRunnableMethod(
+ "CDMStorageTest::TestClearRecentHistory_CheckEmpty", this,
+ &CDMStorageTest::TestClearRecentHistory_CheckEmpty);
+ nsCOMPtr<nsIThread> t(GetGMPThread());
+ ClearCDMStorage(r.forget(), t, f.GetResult());
+ }
+
+ void TestClearRecentHistory3_Clear() {
+ MaxMTimeFinder f;
+ nsresult rv = EnumerateCDMStorageDir("storage"_ns, f);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ nsCOMPtr<nsIRunnable> r = NewRunnableMethod(
+ "CDMStorageTest::TestClearRecentHistory_CheckNonEmpty", this,
+ &CDMStorageTest::TestClearRecentHistory_CheckNonEmpty);
+ nsCOMPtr<nsIThread> t(GetGMPThread());
+ ClearCDMStorage(r.forget(), t, f.GetResult() + 1);
+ }
+
+ class FileCounter {
+ public:
+ FileCounter() : mCount(0) {}
+ void operator()(nsIFile* aFile) { ++mCount; }
+ int GetCount() const { return mCount; }
+
+ private:
+ int mCount;
+ };
+
+ void TestClearRecentHistory_CheckEmpty() {
+ FileCounter c1;
+ nsresult rv = EnumerateCDMStorageDir("id"_ns, c1);
+ EXPECT_NS_SUCCEEDED(rv);
+ // There should be no files under $profileDir/gmp/$platform/gmp-fake/id/
+ EXPECT_EQ(c1.GetCount(), 0);
+
+ FileCounter c2;
+ rv = EnumerateCDMStorageDir("storage"_ns, c2);
+ EXPECT_NS_SUCCEEDED(rv);
+ // There should be no files under
+ // $profileDir/gmp/$platform/gmp-fake/storage/
+ EXPECT_EQ(c2.GetCount(), 0);
+
+ SetFinished();
+ }
+
+ void TestClearRecentHistory_CheckNonEmpty() {
+ FileCounter c1;
+ nsresult rv = EnumerateCDMStorageDir("id"_ns, c1);
+ EXPECT_NS_SUCCEEDED(rv);
+ // There should be one directory under
+ // $profileDir/gmp/$platform/gmp-fake/id/
+ EXPECT_EQ(c1.GetCount(), 1);
+
+ FileCounter c2;
+ rv = EnumerateCDMStorageDir("storage"_ns, c2);
+ EXPECT_NS_SUCCEEDED(rv);
+ // There should be one directory under
+ // $profileDir/gmp/$platform/gmp-fake/storage/
+ EXPECT_EQ(c2.GetCount(), 1);
+
+ SetFinished();
+ }
+
+ void TestCrossOriginStorage() {
+ EXPECT_TRUE(!mCDM);
+
+ // Send the decryptor the message "store recordid $time"
+ // Wait for the decrytor to send us "stored recordid $time"
+ auto t = time(0);
+ nsCString response("stored crossOriginTestRecordId ");
+ response.AppendInt((int64_t)t);
+ Expect(
+ response,
+ NewRunnableMethod(
+ "CDMStorageTest::TestCrossOriginStorage_RecordStoredContinuation",
+ this,
+ &CDMStorageTest::TestCrossOriginStorage_RecordStoredContinuation));
+
+ nsCString update("store crossOriginTestRecordId ");
+ update.AppendInt((int64_t)t);
+
+ // Open decryptor on one, origin, write a record, and test that that
+ // record can't be read on another origin.
+ CreateDecryptor(u"http://example3.com"_ns, u"http://example4.com"_ns, false,
+ update);
+ }
+
+ void TestCrossOriginStorage_RecordStoredContinuation() {
+ // Close the old decryptor, and create a new one on a different origin,
+ // and try to read the record.
+ Shutdown();
+
+ Expect(nsLiteralCString(
+ "retrieve crossOriginTestRecordId succeeded (length 0 bytes)"),
+ NewRunnableMethod("CDMStorageTest::SetFinished", this,
+ &CDMStorageTest::SetFinished));
+
+ CreateDecryptor(u"http://example5.com"_ns, u"http://example6.com"_ns, false,
+ "retrieve crossOriginTestRecordId"_ns);
+ }
+
+ void TestPBStorage() {
+ // Send the decryptor the message "store recordid $time"
+ // Wait for the decrytor to send us "stored recordid $time"
+ nsCString response("stored pbdata test-pb-data");
+ Expect(response,
+ NewRunnableMethod(
+ "CDMStorageTest::TestPBStorage_RecordStoredContinuation", this,
+ &CDMStorageTest::TestPBStorage_RecordStoredContinuation));
+
+ // Open decryptor on one, origin, write a record, close decryptor,
+ // open another, and test that record can be read, close decryptor,
+ // then send pb-last-context-closed notification, then open decryptor
+ // and check that it can't read that data; it should have been purged.
+ CreateDecryptor(u"http://pb1.com"_ns, u"http://pb2.com"_ns, true,
+ "store pbdata test-pb-data"_ns);
+ }
+
+ void TestPBStorage_RecordStoredContinuation() {
+ Shutdown();
+
+ Expect(
+ "retrieve pbdata succeeded (length 12 bytes)"_ns,
+ NewRunnableMethod(
+ "CDMStorageTest::TestPBStorage_RecordRetrievedContinuation", this,
+ &CDMStorageTest::TestPBStorage_RecordRetrievedContinuation));
+
+ CreateDecryptor(u"http://pb1.com"_ns, u"http://pb2.com"_ns, true,
+ "retrieve pbdata"_ns);
+ }
+
+ void TestPBStorage_RecordRetrievedContinuation() {
+ Shutdown();
+ SimulatePBModeExit();
+
+ Expect("retrieve pbdata succeeded (length 0 bytes)"_ns,
+ NewRunnableMethod("CDMStorageTest::SetFinished", this,
+ &CDMStorageTest::SetFinished));
+
+ CreateDecryptor(u"http://pb1.com"_ns, u"http://pb2.com"_ns, true,
+ "retrieve pbdata"_ns);
+ }
+
+#if defined(XP_WIN)
+ void TestOutputProtection() {
+ Shutdown();
+
+ Expect("OP tests completed"_ns,
+ NewRunnableMethod("CDMStorageTest::SetFinished", this,
+ &CDMStorageTest::SetFinished));
+
+ CreateDecryptor(u"http://example15.com"_ns, u"http://example16.com"_ns,
+ false, "test-op-apis"_ns);
+ }
+#endif
+
+ void TestLongRecordNames() {
+ constexpr auto longRecordName =
+ "A_"
+ "very_very_very_very_very_very_very_very_very_"
+ "very_very_very_very_very_very_"
+ "very_very_very_very_very_very_very_very_very_"
+ "very_very_very_very_very_very_"
+ "very_very_very_very_very_very_very_very_very_"
+ "very_very_very_very_very_very_"
+ "very_very_very_very_very_very_very_very_very_"
+ "very_very_very_very_very_very_"
+ "very_very_very_very_very_very_very_very_very_"
+ "very_very_very_very_very_very_"
+ "very_very_very_very_very_very_very_very_very_"
+ "very_very_very_very_very_very_"
+ "very_very_very_very_very_very_very_very_very_"
+ "very_very_very_very_very_very_"
+ "very_very_very_very_very_very_very_very_very_"
+ "very_very_very_very_very_very_"
+ "very_very_very_very_very_very_very_very_very_"
+ "very_very_very_very_very_very_"
+ "very_very_very_very_very_very_very_very_very_"
+ "very_very_very_very_very_very_"
+ "very_very_very_very_very_very_very_very_very_"
+ "very_very_very_very_very_very_"
+ "very_very_very_very_very_very_very_very_very_"
+ "very_very_very_very_very_very_"
+ "very_very_very_very_very_very_very_very_very_"
+ "very_very_very_very_very_very_"
+ "very_very_very_very_very_very_very_very_very_"
+ "very_very_very_very_very_very_"
+ "very_very_very_very_very_very_very_very_very_"
+ "very_very_very_very_very_very_"
+ "long_record_name"_ns;
+
+ constexpr auto data = "Just_some_arbitrary_data."_ns;
+
+ MOZ_ASSERT(longRecordName.Length() < GMP_MAX_RECORD_NAME_SIZE);
+ MOZ_ASSERT(longRecordName.Length() > 260); // Windows MAX_PATH
+
+ nsCString response("stored ");
+ response.Append(longRecordName);
+ response.AppendLiteral(" ");
+ response.Append(data);
+ Expect(response, NewRunnableMethod("CDMStorageTest::SetFinished", this,
+ &CDMStorageTest::SetFinished));
+
+ nsCString update("store ");
+ update.Append(longRecordName);
+ update.AppendLiteral(" ");
+ update.Append(data);
+ CreateDecryptor(u"http://fuz.com"_ns, u"http://baz.com"_ns, false, update);
+ }
+
+ void Expect(const nsCString& aMessage,
+ already_AddRefed<nsIRunnable> aContinuation) {
+ mExpected.AppendElement(
+ ExpectedMessage(aMessage, std::move(aContinuation)));
+ }
+
+ void AwaitFinished() {
+ mozilla::SpinEventLoopUntil("CDMStorageTest::AwaitFinished"_ns,
+ [&]() -> bool { return mFinished; });
+ mFinished = false;
+ }
+
+ void ShutdownThen(already_AddRefed<nsIRunnable> aContinuation) {
+ EXPECT_TRUE(!!mCDM);
+ if (!mCDM) {
+ return;
+ }
+ EXPECT_FALSE(mNodeId.IsEmpty());
+ RefPtr<GMPShutdownObserver> task(new GMPShutdownObserver(
+ NewRunnableMethod("CDMStorageTest::Shutdown", this,
+ &CDMStorageTest::Shutdown),
+ std::move(aContinuation), mNodeId));
+ SchedulerGroup::Dispatch(TaskCategory::Other, task.forget());
+ }
+
+ void Shutdown() {
+ if (mCDM) {
+ mCDM->Shutdown();
+ mCDM = nullptr;
+ mNodeId.Truncate();
+ }
+ }
+
+ void Dummy() {}
+
+ void SetFinished() {
+ mFinished = true;
+ Shutdown();
+ nsCOMPtr<nsIRunnable> task = NewRunnableMethod(
+ "CDMStorageTest::Dummy", this, &CDMStorageTest::Dummy);
+ SchedulerGroup::Dispatch(TaskCategory::Other, task.forget());
+ }
+
+ void SessionMessage(const nsACString& aSessionId, uint32_t aMessageType,
+ const nsTArray<uint8_t>& aMessage) {
+ MonitorAutoLock mon(mMonitor);
+
+ nsCString msg((const char*)aMessage.Elements(), aMessage.Length());
+ EXPECT_TRUE(mExpected.Length() > 0);
+ bool matches = mExpected[0].mMessage.Equals(msg);
+ EXPECT_STREQ(mExpected[0].mMessage.get(), msg.get());
+ if (mExpected.Length() > 0 && matches) {
+ nsCOMPtr<nsIRunnable> continuation = mExpected[0].mContinuation;
+ mExpected.RemoveElementAt(0);
+ if (continuation) {
+ NS_DispatchToCurrentThread(continuation);
+ }
+ }
+ }
+
+ void Terminated() {
+ if (mCDM) {
+ mCDM->Shutdown();
+ mCDM = nullptr;
+ }
+ }
+
+ private:
+ ~CDMStorageTest() = default;
+
+ struct ExpectedMessage {
+ ExpectedMessage(const nsCString& aMessage,
+ already_AddRefed<nsIRunnable> aContinuation)
+ : mMessage(aMessage), mContinuation(aContinuation) {}
+ nsCString mMessage;
+ nsCOMPtr<nsIRunnable> mContinuation;
+ };
+
+ nsTArray<ExpectedMessage> mExpected;
+
+ RefPtr<gmp::ChromiumCDMParent> mCDM;
+ Monitor mMonitor MOZ_UNANNOTATED;
+ Atomic<bool> mFinished;
+ nsCString mNodeId;
+
+ class CallbackProxy : public ChromiumCDMCallback {
+ public:
+ explicit CallbackProxy(CDMStorageTest* aRunner) : mRunner(aRunner) {}
+
+ void SetSessionId(uint32_t aPromiseId,
+ const nsCString& aSessionId) override {}
+
+ void ResolveLoadSessionPromise(uint32_t aPromiseId,
+ bool aSuccessful) override {}
+
+ void ResolvePromiseWithKeyStatus(uint32_t aPromiseId,
+ uint32_t aKeyStatus) override {}
+
+ void ResolvePromise(uint32_t aPromiseId) override {}
+
+ void RejectPromise(uint32_t aPromiseId, ErrorResult&& aError,
+ const nsCString& aErrorMessage) override {}
+
+ void SessionMessage(const nsACString& aSessionId, uint32_t aMessageType,
+ nsTArray<uint8_t>&& aMessage) override {
+ mRunner->SessionMessage(aSessionId, aMessageType, std::move(aMessage));
+ }
+
+ void SessionKeysChange(
+ const nsCString& aSessionId,
+ nsTArray<mozilla::gmp::CDMKeyInformation>&& aKeysInfo) override {}
+
+ void ExpirationChange(const nsCString& aSessionId,
+ double aSecondsSinceEpoch) override {}
+
+ void SessionClosed(const nsCString& aSessionId) override {}
+
+ void QueryOutputProtectionStatus() override {}
+
+ void Terminated() override { mRunner->Terminated(); }
+
+ void Shutdown() override { mRunner->Shutdown(); }
+
+ private:
+ // Warning: Weak ref.
+ CDMStorageTest* mRunner;
+ };
+
+ UniquePtr<CallbackProxy> mCallback;
+}; // class CDMStorageTest
+
+static nsresult CreateTestDirectory(nsCOMPtr<nsIFile>& aOut) {
+ nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(aOut));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ nsCString dirName;
+ dirName.SetLength(32);
+ NS_MakeRandomString(dirName.BeginWriting(), 32);
+ aOut->Append(NS_ConvertUTF8toUTF16(dirName));
+ rv = aOut->Create(nsIFile::DIRECTORY_TYPE, 0755);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ return NS_OK;
+}
+
+void TestMatchBaseDomain_MatchOrigin() {
+ nsCOMPtr<nsIFile> testDir;
+ nsresult rv = CreateTestDirectory(testDir);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = WriteToFile(testDir, "origin"_ns,
+ "https://video.subdomain.removeme.github.io"_ns);
+ EXPECT_NS_SUCCEEDED(rv);
+ rv = WriteToFile(testDir, "topLevelOrigin"_ns,
+ "https://embedder.example.com"_ns);
+ EXPECT_NS_SUCCEEDED(rv);
+ bool result = MatchBaseDomain(testDir, "removeme.github.io"_ns);
+ EXPECT_TRUE(result);
+ testDir->Remove(true);
+}
+
+void TestMatchBaseDomain_MatchTLD() {
+ nsCOMPtr<nsIFile> testDir;
+ nsresult rv = CreateTestDirectory(testDir);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = WriteToFile(testDir, "origin"_ns,
+ "https://video.example.com^userContextId=4"_ns);
+ EXPECT_NS_SUCCEEDED(rv);
+ rv = WriteToFile(testDir, "topLevelOrigin"_ns,
+ "https://evil.web.megacorp.co.uk^privateBrowsingId=1"_ns);
+ EXPECT_NS_SUCCEEDED(rv);
+ bool result = MatchBaseDomain(testDir, "megacorp.co.uk"_ns);
+ EXPECT_TRUE(result);
+ testDir->Remove(true);
+}
+
+void TestMatchBaseDomain_NoMatch() {
+ nsCOMPtr<nsIFile> testDir;
+ nsresult rv = CreateTestDirectory(testDir);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ rv = WriteToFile(testDir, "origin"_ns,
+ "https://video.example.com^userContextId=4"_ns);
+ EXPECT_NS_SUCCEEDED(rv);
+ rv = WriteToFile(testDir, "topLevelOrigin"_ns,
+ "https://evil.web.megacorp.co.uk^privateBrowsingId=1"_ns);
+ EXPECT_NS_SUCCEEDED(rv);
+ bool result = MatchBaseDomain(testDir, "longer-example.com"_ns);
+ EXPECT_FALSE(result);
+ testDir->Remove(true);
+}
+
+TEST(GeckoMediaPlugins, MatchBaseDomain_MatchOrigin)
+{ TestMatchBaseDomain_MatchOrigin(); }
+
+TEST(GeckoMediaPlugins, MatchBaseDomain_MatchTLD)
+{ TestMatchBaseDomain_MatchTLD(); }
+
+TEST(GeckoMediaPlugins, MatchBaseDomain_NoMatch)
+{ TestMatchBaseDomain_NoMatch(); }
+
+// Bug 1776767 - Skip all GMP tests on Windows ASAN
+#if !(defined(XP_WIN) && defined(MOZ_ASAN))
+TEST(GeckoMediaPlugins, CDMStorageGetNodeId)
+{
+ RefPtr<CDMStorageTest> runner = new CDMStorageTest();
+ runner->DoTest(&CDMStorageTest::TestGetNodeId);
+}
+
+TEST(GeckoMediaPlugins, CDMStorageBasic)
+{
+ RefPtr<CDMStorageTest> runner = new CDMStorageTest();
+ runner->DoTest(&CDMStorageTest::TestBasicStorage);
+}
+
+TEST(GeckoMediaPlugins, CDMStorageForgetThisSite)
+{
+ RefPtr<CDMStorageTest> runner = new CDMStorageTest();
+ runner->DoTest(&CDMStorageTest::TestForgetThisSite);
+}
+
+TEST(GeckoMediaPlugins, CDMStorageForgetThisBaseDomain)
+{
+ RefPtr<CDMStorageTest> runner = new CDMStorageTest();
+ runner->DoTest(&CDMStorageTest::TestForgetThisBaseDomain);
+}
+
+TEST(GeckoMediaPlugins, CDMStorageClearRecentHistory1)
+{
+ RefPtr<CDMStorageTest> runner = new CDMStorageTest();
+ runner->DoTest(&CDMStorageTest::TestClearRecentHistory1);
+}
+
+TEST(GeckoMediaPlugins, CDMStorageClearRecentHistory2)
+{
+ RefPtr<CDMStorageTest> runner = new CDMStorageTest();
+ runner->DoTest(&CDMStorageTest::TestClearRecentHistory2);
+}
+
+TEST(GeckoMediaPlugins, CDMStorageClearRecentHistory3)
+{
+ RefPtr<CDMStorageTest> runner = new CDMStorageTest();
+ runner->DoTest(&CDMStorageTest::TestClearRecentHistory3);
+}
+
+TEST(GeckoMediaPlugins, CDMStorageCrossOrigin)
+{
+ RefPtr<CDMStorageTest> runner = new CDMStorageTest();
+ runner->DoTest(&CDMStorageTest::TestCrossOriginStorage);
+}
+
+TEST(GeckoMediaPlugins, CDMStoragePrivateBrowsing)
+{
+ RefPtr<CDMStorageTest> runner = new CDMStorageTest();
+ runner->DoTest(&CDMStorageTest::TestPBStorage);
+}
+
+TEST(GeckoMediaPlugins, CDMStorageLongRecordNames)
+{
+ RefPtr<CDMStorageTest> runner = new CDMStorageTest();
+ runner->DoTest(&CDMStorageTest::TestLongRecordNames);
+}
+
+# if defined(XP_WIN)
+TEST(GeckoMediaPlugins, GMPOutputProtection)
+{
+ RefPtr<CDMStorageTest> runner = new CDMStorageTest();
+ runner->DoTest(&CDMStorageTest::TestOutputProtection);
+}
+# endif // defined(XP_WIN)
+#endif // !(defined(XP_WIN) && defined(MOZ_ASAN))
diff --git a/dom/media/gtest/TestCubebInputStream.cpp b/dom/media/gtest/TestCubebInputStream.cpp
new file mode 100644
index 0000000000..0488c2be1a
--- /dev/null
+++ b/dom/media/gtest/TestCubebInputStream.cpp
@@ -0,0 +1,188 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#include "CubebInputStream.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+#include "MockCubeb.h"
+#include "WaitFor.h"
+
+using namespace mozilla;
+
+namespace {
+#define DispatchFunction(f) \
+ NS_DispatchToCurrentThread(NS_NewRunnableFunction(__func__, f))
+} // namespace
+
+class MockListener : public CubebInputStream::Listener {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MockListener, override);
+ MOCK_METHOD2(DataCallback, long(const void* aBuffer, long aFrames));
+ MOCK_METHOD1(StateCallback, void(cubeb_state aState));
+ MOCK_METHOD0(DeviceChangedCallback, void());
+
+ private:
+ ~MockListener() = default;
+};
+
+TEST(TestCubebInputStream, DataCallback)
+{
+ using ::testing::Ne;
+ using ::testing::NotNull;
+
+ MockCubeb* cubeb = new MockCubeb();
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+
+ const CubebUtils::AudioDeviceID deviceId = nullptr;
+ const uint32_t channels = 2;
+
+ uint32_t rate = 0;
+ ASSERT_EQ(cubeb_get_preferred_sample_rate(cubeb->AsCubebContext(), &rate),
+ CUBEB_OK);
+
+ nsTArray<AudioDataValue> data;
+ auto listener = MakeRefPtr<MockListener>();
+ EXPECT_CALL(*listener, DataCallback(NotNull(), Ne(0)))
+ .WillRepeatedly([&](const void* aBuffer, long aFrames) {
+ const AudioDataValue* source =
+ reinterpret_cast<const AudioDataValue*>(aBuffer);
+ size_t sampleCount =
+ static_cast<size_t>(aFrames) * static_cast<size_t>(channels);
+ data.AppendElements(source, sampleCount);
+ return aFrames;
+ });
+
+ EXPECT_CALL(*listener, StateCallback(CUBEB_STATE_STARTED));
+ EXPECT_CALL(*listener, StateCallback(CUBEB_STATE_STOPPED)).Times(2);
+
+ EXPECT_CALL(*listener, DeviceChangedCallback).Times(0);
+
+ UniquePtr<CubebInputStream> cis;
+ DispatchFunction([&] {
+ cis = CubebInputStream::Create(deviceId, channels, rate, true,
+ listener.get());
+ ASSERT_TRUE(cis);
+ });
+ RefPtr<SmartMockCubebStream> stream = WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(stream->mHasInput);
+
+ stream->SetInputRecordingEnabled(true);
+
+ DispatchFunction([&] { ASSERT_EQ(cis->Start(), CUBEB_OK); });
+ WaitFor(stream->FramesProcessedEvent());
+
+ DispatchFunction([&] { ASSERT_EQ(cis->Stop(), CUBEB_OK); });
+ WaitFor(stream->OutputVerificationEvent());
+
+ nsTArray<AudioDataValue> record = stream->TakeRecordedInput();
+
+ DispatchFunction([&] { cis = nullptr; });
+ WaitFor(cubeb->StreamDestroyEvent());
+
+ ASSERT_EQ(data, record);
+}
+
+TEST(TestCubebInputStream, ErrorCallback)
+{
+ using ::testing::Ne;
+ using ::testing::NotNull;
+ using ::testing::ReturnArg;
+
+ MockCubeb* cubeb = new MockCubeb();
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+
+ const CubebUtils::AudioDeviceID deviceId = nullptr;
+ const uint32_t channels = 2;
+
+ uint32_t rate = 0;
+ ASSERT_EQ(cubeb_get_preferred_sample_rate(cubeb->AsCubebContext(), &rate),
+ CUBEB_OK);
+
+ auto listener = MakeRefPtr<MockListener>();
+ EXPECT_CALL(*listener, DataCallback(NotNull(), Ne(0)))
+ .WillRepeatedly(ReturnArg<1>());
+
+ EXPECT_CALL(*listener, StateCallback(CUBEB_STATE_STARTED));
+ EXPECT_CALL(*listener, StateCallback(CUBEB_STATE_ERROR));
+ EXPECT_CALL(*listener, StateCallback(CUBEB_STATE_STOPPED));
+
+ EXPECT_CALL(*listener, DeviceChangedCallback).Times(0);
+
+ UniquePtr<CubebInputStream> cis;
+ DispatchFunction([&] {
+ cis = CubebInputStream::Create(deviceId, channels, rate, true,
+ listener.get());
+ ASSERT_TRUE(cis);
+ });
+ RefPtr<SmartMockCubebStream> stream = WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(stream->mHasInput);
+
+ DispatchFunction([&] { ASSERT_EQ(cis->Start(), CUBEB_OK); });
+ WaitFor(stream->FramesProcessedEvent());
+
+ DispatchFunction([&] { stream->ForceError(); });
+ WaitFor(stream->ErrorForcedEvent());
+
+ // If stream ran into an error state, then it should be stopped.
+
+ DispatchFunction([&] { cis = nullptr; });
+ WaitFor(cubeb->StreamDestroyEvent());
+}
+
+TEST(TestCubebInputStream, DeviceChangedCallback)
+{
+ using ::testing::Ne;
+ using ::testing::NotNull;
+ using ::testing::ReturnArg;
+
+ MockCubeb* cubeb = new MockCubeb();
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+
+ const CubebUtils::AudioDeviceID deviceId = nullptr;
+ const uint32_t channels = 2;
+
+ uint32_t rate = 0;
+ ASSERT_EQ(cubeb_get_preferred_sample_rate(cubeb->AsCubebContext(), &rate),
+ CUBEB_OK);
+
+ auto listener = MakeRefPtr<MockListener>();
+ EXPECT_CALL(*listener, DataCallback(NotNull(), Ne(0)))
+ .WillRepeatedly(ReturnArg<1>());
+
+ // In real world, the stream might run into an error state when the
+ // device-changed event is fired (e.g., the last default output device is
+ // unplugged). But it's fine to not check here since we can control how
+ // MockCubeb behaves.
+ EXPECT_CALL(*listener, StateCallback(CUBEB_STATE_STARTED));
+ EXPECT_CALL(*listener, StateCallback(CUBEB_STATE_STOPPED)).Times(2);
+
+ EXPECT_CALL(*listener, DeviceChangedCallback);
+
+ UniquePtr<CubebInputStream> cis;
+ DispatchFunction([&] {
+ cis = CubebInputStream::Create(deviceId, channels, rate, true,
+ listener.get());
+ ASSERT_TRUE(cis);
+ });
+ RefPtr<SmartMockCubebStream> stream = WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(stream->mHasInput);
+
+ DispatchFunction([&] { ASSERT_EQ(cis->Start(), CUBEB_OK); });
+ WaitFor(stream->FramesProcessedEvent());
+
+ DispatchFunction([&] { stream->ForceDeviceChanged(); });
+ WaitFor(stream->DeviceChangeForcedEvent());
+
+ // The stream can keep running when its device is changed.
+ DispatchFunction([&] { ASSERT_EQ(cis->Stop(), CUBEB_OK); });
+ cubeb_state state = WaitFor(stream->StateEvent());
+ EXPECT_EQ(state, CUBEB_STATE_STOPPED);
+
+ DispatchFunction([&] { cis = nullptr; });
+ WaitFor(cubeb->StreamDestroyEvent());
+}
diff --git a/dom/media/gtest/TestDataMutex.cpp b/dom/media/gtest/TestDataMutex.cpp
new file mode 100644
index 0000000000..11f3e395c9
--- /dev/null
+++ b/dom/media/gtest/TestDataMutex.cpp
@@ -0,0 +1,46 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "mozilla/DataMutex.h"
+#include "nsTArray.h"
+
+using mozilla::DataMutex;
+
+struct A {
+ void Set(int a) { mValue = a; }
+ int mValue;
+};
+
+TEST(DataMutex, Basic)
+{
+ {
+ DataMutex<uint32_t> i(1, "1");
+ i.Mutex().AssertNotCurrentThreadOwns();
+ {
+ auto x = i.Lock();
+ i.Mutex().AssertCurrentThreadOwns();
+ *x = 4;
+ ASSERT_EQ(*x, 4u);
+ }
+ i.Mutex().AssertNotCurrentThreadOwns();
+ }
+ {
+ DataMutex<A> a({4}, "StructA");
+ auto x = a.Lock();
+ ASSERT_EQ(x->mValue, 4);
+ x->Set(8);
+ ASSERT_EQ(x->mValue, 8);
+ }
+ {
+ DataMutex<nsTArray<uint32_t>> _a("array");
+ auto a = _a.Lock();
+ auto& x = a.ref();
+ ASSERT_EQ(x.Length(), 0u);
+ x.AppendElement(1u);
+ ASSERT_EQ(x.Length(), 1u);
+ ASSERT_EQ(x[0], 1u);
+ }
+}
diff --git a/dom/media/gtest/TestDecoderBenchmark.cpp b/dom/media/gtest/TestDecoderBenchmark.cpp
new file mode 100644
index 0000000000..85091a4946
--- /dev/null
+++ b/dom/media/gtest/TestDecoderBenchmark.cpp
@@ -0,0 +1,66 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DecoderBenchmark.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest-printers.h"
+#include "gtest/gtest.h"
+
+using ::testing::Return;
+using namespace mozilla;
+
+TEST(DecoderBenchmark, CreateKey)
+{
+ DecoderBenchmarkInfo info{"video/av1"_ns, 1, 1, 1, 8};
+ EXPECT_EQ(KeyUtil::CreateKey(info),
+ "ResolutionLevel0-FrameRateLevel0-8bit"_ns)
+ << "Min level";
+
+ DecoderBenchmarkInfo info1{"video/av1"_ns, 5000, 5000, 100, 8};
+ EXPECT_EQ(KeyUtil::CreateKey(info1),
+ "ResolutionLevel7-FrameRateLevel4-8bit"_ns)
+ << "Max level";
+
+ DecoderBenchmarkInfo info2{"video/av1"_ns, 854, 480, 30, 8};
+ EXPECT_EQ(KeyUtil::CreateKey(info2),
+ "ResolutionLevel3-FrameRateLevel2-8bit"_ns)
+ << "On the top of 4th resolution level";
+
+ DecoderBenchmarkInfo info3{"video/av1"_ns, 1270, 710, 24, 8};
+ EXPECT_EQ(KeyUtil::CreateKey(info3),
+ "ResolutionLevel4-FrameRateLevel1-8bit"_ns)
+ << "Closer to 5th resolution level - bellow";
+
+ DecoderBenchmarkInfo info4{"video/av1"_ns, 1290, 730, 24, 8};
+ EXPECT_EQ(KeyUtil::CreateKey(info4),
+ "ResolutionLevel4-FrameRateLevel1-8bit"_ns)
+ << "Closer to 5th resolution level - above";
+
+ DecoderBenchmarkInfo info5{"video/av1"_ns, 854, 480, 20, 8};
+ EXPECT_EQ(KeyUtil::CreateKey(info5),
+ "ResolutionLevel3-FrameRateLevel1-8bit"_ns)
+ << "Closer to 2nd frame rate level - bellow";
+
+ DecoderBenchmarkInfo info6{"video/av1"_ns, 854, 480, 26, 8};
+ EXPECT_EQ(KeyUtil::CreateKey(info6),
+ "ResolutionLevel3-FrameRateLevel1-8bit"_ns)
+ << "Closer to 2nd frame rate level - above";
+
+ DecoderBenchmarkInfo info7{"video/av1"_ns, 1280, 720, 24, 10};
+ EXPECT_EQ(KeyUtil::CreateKey(info7),
+ "ResolutionLevel4-FrameRateLevel1-non8bit"_ns)
+ << "Bit depth 10 bits";
+
+ DecoderBenchmarkInfo info8{"video/av1"_ns, 1280, 720, 24, 12};
+ EXPECT_EQ(KeyUtil::CreateKey(info8),
+ "ResolutionLevel4-FrameRateLevel1-non8bit"_ns)
+ << "Bit depth 12 bits";
+
+ DecoderBenchmarkInfo info9{"video/av1"_ns, 1280, 720, 24, 16};
+ EXPECT_EQ(KeyUtil::CreateKey(info9),
+ "ResolutionLevel4-FrameRateLevel1-non8bit"_ns)
+ << "Bit depth 16 bits";
+}
diff --git a/dom/media/gtest/TestDeviceInputTrack.cpp b/dom/media/gtest/TestDeviceInputTrack.cpp
new file mode 100644
index 0000000000..ada330437d
--- /dev/null
+++ b/dom/media/gtest/TestDeviceInputTrack.cpp
@@ -0,0 +1,563 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#include "DeviceInputTrack.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+
+#include "AudioGenerator.h"
+#include "MediaTrackGraphImpl.h"
+#include "MockCubeb.h"
+#include "WaitFor.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "nsContentUtils.h"
+
+using namespace mozilla;
+using testing::NiceMock;
+using testing::Return;
+
+namespace {
+#define DispatchFunction(f) \
+ NS_DispatchToCurrentThread(NS_NewRunnableFunction(__func__, f))
+} // namespace
+
+class MockGraphImpl : public MediaTrackGraphImpl {
+ public:
+ MockGraphImpl(TrackRate aRate, uint32_t aChannels)
+ : MediaTrackGraphImpl(OFFLINE_THREAD_DRIVER, DIRECT_DRIVER, aRate,
+ aChannels, nullptr, NS_GetCurrentThread()) {
+ ON_CALL(*this, OnGraphThread).WillByDefault(Return(true));
+ // We have to call `Destroy()` manually in order to break the reference.
+ // The reason we don't assign a null driver is because we would add a track
+ // to the graph, then it would trigger graph's `EnsureNextIteration()` that
+ // requires a non-null driver.
+ SetCurrentDriver(new NiceMock<MockDriver>());
+ }
+
+ MOCK_CONST_METHOD0(OnGraphThread, bool());
+ MOCK_METHOD1(AppendMessage, void(UniquePtr<ControlMessage>));
+
+ protected:
+ ~MockGraphImpl() = default;
+
+ class MockDriver : public GraphDriver {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MockDriver, override);
+
+ MockDriver() : GraphDriver(nullptr, nullptr, 0) {
+ ON_CALL(*this, OnThread).WillByDefault(Return(true));
+ ON_CALL(*this, ThreadRunning).WillByDefault(Return(true));
+ }
+
+ MOCK_METHOD0(Start, void());
+ MOCK_METHOD0(Shutdown, void());
+ MOCK_METHOD0(IterationDuration, uint32_t());
+ MOCK_METHOD0(EnsureNextIteration, void());
+ MOCK_CONST_METHOD0(OnThread, bool());
+ MOCK_CONST_METHOD0(ThreadRunning, bool());
+
+ protected:
+ ~MockDriver() = default;
+ };
+};
+
+class TestDeviceInputTrack : public testing::Test {
+ protected:
+ TestDeviceInputTrack() : mChannels(2), mRate(44100) {}
+
+ void SetUp() override {
+ mGraph = MakeRefPtr<NiceMock<MockGraphImpl>>(mRate, mChannels);
+ }
+
+ void TearDown() override { mGraph->Destroy(); }
+
+ const uint32_t mChannels;
+ const TrackRate mRate;
+ RefPtr<MockGraphImpl> mGraph;
+};
+
+TEST_F(TestDeviceInputTrack, DeviceInputConsumerTrack) {
+ class TestDeviceInputConsumerTrack : public DeviceInputConsumerTrack {
+ public:
+ static TestDeviceInputConsumerTrack* Create(MediaTrackGraph* aGraph) {
+ MOZ_ASSERT(NS_IsMainThread());
+ TestDeviceInputConsumerTrack* track =
+ new TestDeviceInputConsumerTrack(aGraph->GraphRate());
+ aGraph->AddTrack(track);
+ return track;
+ }
+
+ void Destroy() {
+ MOZ_ASSERT(NS_IsMainThread());
+ DisconnectDeviceInput();
+ DeviceInputConsumerTrack::Destroy();
+ }
+
+ void ProcessInput(GraphTime aFrom, GraphTime aTo,
+ uint32_t aFlags) override{/* Ignored */};
+
+ uint32_t NumberOfChannels() const override {
+ if (mInputs.IsEmpty()) {
+ return 0;
+ }
+ DeviceInputTrack* t = mInputs[0]->GetSource()->AsDeviceInputTrack();
+ MOZ_ASSERT(t);
+ return t->NumberOfChannels();
+ }
+
+ private:
+ explicit TestDeviceInputConsumerTrack(TrackRate aSampleRate)
+ : DeviceInputConsumerTrack(aSampleRate) {}
+ };
+
+ class TestAudioDataListener : public AudioDataListener {
+ public:
+ TestAudioDataListener(uint32_t aChannelCount, bool aIsVoice)
+ : mChannelCount(aChannelCount), mIsVoice(aIsVoice) {}
+ // Graph thread APIs: AudioDataListenerInterface implementations.
+ uint32_t RequestedInputChannelCount(MediaTrackGraphImpl* aGraph) override {
+ MOZ_ASSERT(aGraph->OnGraphThread());
+ return mChannelCount;
+ }
+ bool IsVoiceInput(MediaTrackGraphImpl* aGraph) const override {
+ return mIsVoice;
+ };
+ void DeviceChanged(MediaTrackGraphImpl* aGraph) override { /* Ignored */
+ }
+ void Disconnect(MediaTrackGraphImpl* aGraph) override{/* Ignored */};
+
+ private:
+ ~TestAudioDataListener() = default;
+
+ // Graph thread-only.
+ uint32_t mChannelCount;
+ // Any thread.
+ const bool mIsVoice;
+ };
+
+ const PrincipalHandle testPrincipal =
+ MakePrincipalHandle(nsContentUtils::GetSystemPrincipal());
+
+ const CubebUtils::AudioDeviceID device1 = (void*)1;
+ RefPtr<TestAudioDataListener> listener1 = new TestAudioDataListener(1, false);
+ RefPtr<TestDeviceInputConsumerTrack> track1 =
+ TestDeviceInputConsumerTrack::Create(mGraph);
+ track1->ConnectDeviceInput(device1, listener1.get(), testPrincipal);
+ EXPECT_TRUE(track1->ConnectToNativeDevice());
+ EXPECT_FALSE(track1->ConnectToNonNativeDevice());
+
+ const CubebUtils::AudioDeviceID device2 = (void*)2;
+ RefPtr<TestAudioDataListener> listener2 = new TestAudioDataListener(2, false);
+ RefPtr<TestDeviceInputConsumerTrack> track2 =
+ TestDeviceInputConsumerTrack::Create(mGraph);
+ track2->ConnectDeviceInput(device2, listener2.get(), testPrincipal);
+ EXPECT_FALSE(track2->ConnectToNativeDevice());
+ EXPECT_TRUE(track2->ConnectToNonNativeDevice());
+
+ track2->Destroy();
+ mGraph->RemoveTrackGraphThread(track2);
+
+ track1->Destroy();
+ mGraph->RemoveTrackGraphThread(track1);
+}
+
+TEST_F(TestDeviceInputTrack, NativeInputTrackData) {
+ const uint32_t flags = 0;
+ const CubebUtils::AudioDeviceID deviceId = (void*)1;
+
+ AudioGenerator<AudioDataValue> generator(mChannels, mRate);
+ const size_t nrFrames = 10;
+ const size_t bufferSize = nrFrames * mChannels;
+ nsTArray<AudioDataValue> buffer(bufferSize);
+ buffer.AppendElements(bufferSize);
+
+ const PrincipalHandle testPrincipal =
+ MakePrincipalHandle(nsContentUtils::GetSystemPrincipal());
+
+ // Setup: Create a NativeInputTrack and add it to mGraph
+ RefPtr<NativeInputTrack> track =
+ new NativeInputTrack(mGraph->GraphRate(), deviceId, testPrincipal);
+ mGraph->AddTrack(track);
+
+ // Main test below:
+
+ generator.GenerateInterleaved(buffer.Elements(), nrFrames);
+ track->NotifyInputData(mGraph.get(), buffer.Elements(), nrFrames, mRate,
+ mChannels, 0);
+
+ track->ProcessInput(0, WEBAUDIO_BLOCK_SIZE + nrFrames, flags);
+ EXPECT_EQ(static_cast<size_t>(track->GetEnd()),
+ static_cast<size_t>(WEBAUDIO_BLOCK_SIZE) + nrFrames);
+
+ // Check pre-buffering: null data with PRINCIPAL_HANDLE_NONE principal
+ AudioSegment preBuffering;
+ preBuffering.AppendSlice(*track->GetData(), 0, WEBAUDIO_BLOCK_SIZE);
+ EXPECT_TRUE(preBuffering.IsNull());
+ for (AudioSegment::ConstChunkIterator iter(preBuffering); !iter.IsEnded();
+ iter.Next()) {
+ const AudioChunk& chunk = *iter;
+ EXPECT_EQ(chunk.mPrincipalHandle, PRINCIPAL_HANDLE_NONE);
+ }
+
+ // Check rest of the data
+ AudioSegment data;
+ data.AppendSlice(*track->GetData(), WEBAUDIO_BLOCK_SIZE,
+ WEBAUDIO_BLOCK_SIZE + nrFrames);
+ nsTArray<AudioDataValue> interleaved;
+ size_t sampleCount = data.WriteToInterleavedBuffer(interleaved, mChannels);
+ EXPECT_EQ(sampleCount, bufferSize);
+ EXPECT_EQ(interleaved, buffer);
+
+ // Check principal in data
+ for (AudioSegment::ConstChunkIterator iter(data); !iter.IsEnded();
+ iter.Next()) {
+ const AudioChunk& chunk = *iter;
+ EXPECT_EQ(chunk.mPrincipalHandle, testPrincipal);
+ }
+
+ // Tear down: Destroy the NativeInputTrack and remove it from mGraph.
+ track->Destroy();
+ mGraph->RemoveTrackGraphThread(track);
+}
+
+class MockEventListener : public AudioInputSource::EventListener {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MockEventListener, override);
+ MOCK_METHOD1(AudioDeviceChanged, void(AudioInputSource::Id));
+ MOCK_METHOD2(AudioStateCallback,
+ void(AudioInputSource::Id,
+ AudioInputSource::EventListener::State));
+
+ private:
+ ~MockEventListener() = default;
+};
+
+TEST_F(TestDeviceInputTrack, StartAndStop) {
+ MockCubeb* cubeb = new MockCubeb();
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+
+ // Non native input settings
+ const AudioInputSource::Id sourceId = 1;
+ const CubebUtils::AudioDeviceID deviceId = (CubebUtils::AudioDeviceID)1;
+ const uint32_t channels = 2;
+ const PrincipalHandle testPrincipal =
+ MakePrincipalHandle(nsContentUtils::GetSystemPrincipal());
+ const TrackRate rate = 48000;
+ const uint32_t bufferingMs = StaticPrefs::media_clockdrift_buffering();
+
+ // Setup: Create a NonNativeInputTrack and add it to mGraph.
+ RefPtr<NonNativeInputTrack> track =
+ new NonNativeInputTrack(mGraph->GraphRate(), deviceId, testPrincipal);
+ mGraph->AddTrack(track);
+
+ // Main test below:
+
+ // Make sure the NonNativeInputTrack can start and stop its audio correctly.
+ {
+ auto listener = MakeRefPtr<MockEventListener>();
+ EXPECT_CALL(*listener,
+ AudioStateCallback(
+ sourceId, AudioInputSource::EventListener::State::Started));
+ EXPECT_CALL(*listener,
+ AudioStateCallback(
+ sourceId, AudioInputSource::EventListener::State::Stopped))
+ .Times(2);
+
+ // No input channels and device preference before start.
+ EXPECT_EQ(track->NumberOfChannels(), 0U);
+ EXPECT_EQ(track->DevicePreference(), AudioInputType::Unknown);
+
+ DispatchFunction([&] {
+ track->StartAudio(MakeRefPtr<AudioInputSource>(
+ std::move(listener), sourceId, deviceId, channels, true /* voice */,
+ testPrincipal, rate, mGraph->GraphRate(), bufferingMs));
+ });
+
+ // Wait for stream creation.
+ RefPtr<SmartMockCubebStream> stream = WaitFor(cubeb->StreamInitEvent());
+
+ // Make sure the audio stream and the track's settings are correct.
+ EXPECT_TRUE(stream->mHasInput);
+ EXPECT_FALSE(stream->mHasOutput);
+ EXPECT_EQ(stream->GetInputDeviceID(), deviceId);
+ EXPECT_EQ(stream->InputChannels(), channels);
+ EXPECT_EQ(stream->InputSampleRate(), static_cast<uint32_t>(rate));
+ EXPECT_EQ(track->NumberOfChannels(), channels);
+ EXPECT_EQ(track->DevicePreference(), AudioInputType::Voice);
+
+ // Wait for stream callbacks.
+ Unused << WaitFor(stream->FramesProcessedEvent());
+
+ DispatchFunction([&] { track->StopAudio(); });
+
+ // Wait for stream destroy.
+ Unused << WaitFor(cubeb->StreamDestroyEvent());
+
+ // No input channels and device preference after stop.
+ EXPECT_EQ(track->NumberOfChannels(), 0U);
+ EXPECT_EQ(track->DevicePreference(), AudioInputType::Unknown);
+ }
+
+ // Make sure the NonNativeInputTrack can restart its audio correctly.
+ {
+ auto listener = MakeRefPtr<MockEventListener>();
+ EXPECT_CALL(*listener,
+ AudioStateCallback(
+ sourceId, AudioInputSource::EventListener::State::Started));
+ EXPECT_CALL(*listener,
+ AudioStateCallback(
+ sourceId, AudioInputSource::EventListener::State::Stopped))
+ .Times(2);
+
+ DispatchFunction([&] {
+ track->StartAudio(MakeRefPtr<AudioInputSource>(
+ std::move(listener), sourceId, deviceId, channels, true,
+ testPrincipal, rate, mGraph->GraphRate(), bufferingMs));
+ });
+
+ // Wait for stream creation.
+ RefPtr<SmartMockCubebStream> stream = WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(stream->mHasInput);
+ EXPECT_FALSE(stream->mHasOutput);
+ EXPECT_EQ(stream->GetInputDeviceID(), deviceId);
+ EXPECT_EQ(stream->InputChannels(), channels);
+ EXPECT_EQ(stream->InputSampleRate(), static_cast<uint32_t>(rate));
+
+ // Wait for stream callbacks.
+ Unused << WaitFor(stream->FramesProcessedEvent());
+
+ DispatchFunction([&] { track->StopAudio(); });
+
+ // Wait for stream destroy.
+ Unused << WaitFor(cubeb->StreamDestroyEvent());
+ }
+
+ // Tear down: Destroy the NativeInputTrack and remove it from mGraph.
+ track->Destroy();
+ mGraph->RemoveTrackGraphThread(track);
+}
+
+TEST_F(TestDeviceInputTrack, NonNativeInputTrackData) {
+ MockCubeb* cubeb = new MockCubeb();
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+
+ // Graph settings
+ const uint32_t flags = 0;
+ const GraphTime frames = 440;
+
+ // Non native input settings
+ const AudioInputSource::Id sourceId = 1;
+ const CubebUtils::AudioDeviceID deviceId = (CubebUtils::AudioDeviceID)1;
+ const uint32_t channels = 2;
+ const PrincipalHandle testPrincipal =
+ MakePrincipalHandle(nsContentUtils::GetSystemPrincipal());
+ const TrackRate rate = 48000;
+ const uint32_t bufferingMs =
+ static_cast<uint32_t>(StaticPrefs::media_clockdrift_buffering());
+
+ // Setup: Create a NonNativeInputTrack and add it to mGraph.
+ RefPtr<NonNativeInputTrack> track =
+ new NonNativeInputTrack(mGraph->GraphRate(), deviceId, testPrincipal);
+ mGraph->AddTrack(track);
+
+ // Main test below:
+
+ // Make sure we get null data if the track is not started yet.
+ GraphTime current = 0;
+ GraphTime next = MediaTrackGraphImpl::RoundUpToEndOfAudioBlock(frames);
+ ASSERT_NE(current, next); // Make sure we have data produced in ProcessInput.
+
+ track->ProcessInput(current, next, flags);
+ {
+ AudioSegment data;
+ data.AppendSegment(track->GetData<AudioSegment>());
+ EXPECT_TRUE(data.IsNull());
+ }
+
+ // Make sure we get the AudioInputSource's data once we start the track.
+
+ current = next;
+ next = MediaTrackGraphImpl::RoundUpToEndOfAudioBlock(2 * frames);
+ ASSERT_NE(current, next); // Make sure we have data produced in ProcessInput.
+
+ auto listener = MakeRefPtr<MockEventListener>();
+ EXPECT_CALL(*listener,
+ AudioStateCallback(
+ sourceId, AudioInputSource::EventListener::State::Started));
+ EXPECT_CALL(*listener,
+ AudioStateCallback(
+ sourceId, AudioInputSource::EventListener::State::Stopped))
+ .Times(2);
+
+ DispatchFunction([&] {
+ track->StartAudio(MakeRefPtr<AudioInputSource>(
+ std::move(listener), sourceId, deviceId, channels, true, testPrincipal,
+ rate, mGraph->GraphRate(), bufferingMs));
+ });
+ RefPtr<SmartMockCubebStream> stream = WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(stream->mHasInput);
+ EXPECT_FALSE(stream->mHasOutput);
+ EXPECT_EQ(stream->GetInputDeviceID(), deviceId);
+ EXPECT_EQ(stream->InputChannels(), channels);
+ EXPECT_EQ(stream->InputSampleRate(), static_cast<uint32_t>(rate));
+
+ // Check audio data.
+ Unused << WaitFor(stream->FramesProcessedEvent());
+ track->ProcessInput(current, next, flags);
+ {
+ AudioSegment data;
+ data.AppendSlice(*track->GetData<AudioSegment>(), current, next);
+ EXPECT_FALSE(data.IsNull());
+ for (AudioSegment::ConstChunkIterator iter(data); !iter.IsEnded();
+ iter.Next()) {
+ EXPECT_EQ(iter->mChannelData.Length(), channels);
+ EXPECT_EQ(iter->mPrincipalHandle, testPrincipal);
+ }
+ }
+
+ // Stop the track and make sure it produces null data again.
+ current = next;
+ next = MediaTrackGraphImpl::RoundUpToEndOfAudioBlock(3 * frames);
+ ASSERT_NE(current, next); // Make sure we have data produced in ProcessInput.
+
+ DispatchFunction([&] { track->StopAudio(); });
+ Unused << WaitFor(cubeb->StreamDestroyEvent());
+
+ track->ProcessInput(current, next, flags);
+ {
+ AudioSegment data;
+ data.AppendSlice(*track->GetData<AudioSegment>(), current, next);
+ EXPECT_TRUE(data.IsNull());
+ }
+
+ // Tear down: Destroy the NonNativeInputTrack and remove it from mGraph.
+ track->Destroy();
+ mGraph->RemoveTrackGraphThread(track);
+}
+
+TEST_F(TestDeviceInputTrack, NonNativeDeviceChangedCallback) {
+ MockCubeb* cubeb = new MockCubeb();
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+
+ // Non native input settings
+ const AudioInputSource::Id sourceId = 1;
+ const CubebUtils::AudioDeviceID deviceId = (CubebUtils::AudioDeviceID)1;
+ const uint32_t channels = 2;
+ const PrincipalHandle testPrincipal =
+ MakePrincipalHandle(nsContentUtils::GetSystemPrincipal());
+ const TrackRate rate = 48000;
+ const uint32_t bufferingMs = StaticPrefs::media_clockdrift_buffering();
+
+ // Setup: Create a NonNativeInputTrack and add it to mGraph.
+ RefPtr<NonNativeInputTrack> track =
+ new NonNativeInputTrack(mGraph->GraphRate(), deviceId, testPrincipal);
+ mGraph->AddTrack(track);
+
+ // Main test below:
+
+ auto listener = MakeRefPtr<MockEventListener>();
+ EXPECT_CALL(*listener, AudioDeviceChanged(sourceId));
+ EXPECT_CALL(*listener,
+ AudioStateCallback(
+ sourceId, AudioInputSource::EventListener::State::Started));
+ EXPECT_CALL(*listener,
+ AudioStateCallback(
+ sourceId, AudioInputSource::EventListener::State::Stopped))
+ .Times(2);
+
+ // Launch and start an audio stream.
+ DispatchFunction([&] {
+ track->StartAudio(MakeRefPtr<AudioInputSource>(
+ std::move(listener), sourceId, deviceId, channels, true, testPrincipal,
+ rate, mGraph->GraphRate(), bufferingMs));
+ });
+ RefPtr<SmartMockCubebStream> stream = WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(stream->mHasInput);
+ EXPECT_FALSE(stream->mHasOutput);
+ EXPECT_EQ(stream->GetInputDeviceID(), deviceId);
+ EXPECT_EQ(stream->InputChannels(), channels);
+ EXPECT_EQ(stream->InputSampleRate(), static_cast<uint32_t>(rate));
+
+ // Make sure the stream is running.
+ Unused << WaitFor(stream->FramesProcessedEvent());
+
+ // Fire a device-changed callback.
+ DispatchFunction([&] { stream->ForceDeviceChanged(); });
+ WaitFor(stream->DeviceChangeForcedEvent());
+
+ // Stop and destroy the stream.
+ DispatchFunction([&] { track->StopAudio(); });
+ Unused << WaitFor(cubeb->StreamDestroyEvent());
+
+ // Tear down: Destroy the NonNativeInputTrack and remove it from mGraph.
+ track->Destroy();
+ mGraph->RemoveTrackGraphThread(track);
+}
+
+TEST_F(TestDeviceInputTrack, NonNativeErrorCallback) {
+ MockCubeb* cubeb = new MockCubeb();
+ CubebUtils::ForceSetCubebContext(cubeb->AsCubebContext());
+
+ // Non native input settings
+ const AudioInputSource::Id sourceId = 1;
+ const CubebUtils::AudioDeviceID deviceId = (CubebUtils::AudioDeviceID)1;
+ const uint32_t channels = 2;
+ const PrincipalHandle testPrincipal =
+ MakePrincipalHandle(nsContentUtils::GetSystemPrincipal());
+ const TrackRate rate = 48000;
+ const uint32_t bufferingMs = StaticPrefs::media_clockdrift_buffering();
+
+ // Setup: Create a NonNativeInputTrack and add it to mGraph.
+ RefPtr<NonNativeInputTrack> track =
+ new NonNativeInputTrack(mGraph->GraphRate(), deviceId, testPrincipal);
+ mGraph->AddTrack(track);
+
+ // Main test below:
+
+ auto listener = MakeRefPtr<MockEventListener>();
+ EXPECT_CALL(*listener,
+ AudioStateCallback(
+ sourceId, AudioInputSource::EventListener::State::Started));
+ EXPECT_CALL(*listener,
+ AudioStateCallback(
+ sourceId, AudioInputSource::EventListener::State::Error));
+ EXPECT_CALL(*listener,
+ AudioStateCallback(
+ sourceId, AudioInputSource::EventListener::State::Stopped));
+
+ // Launch and start an audio stream.
+ DispatchFunction([&] {
+ track->StartAudio(MakeRefPtr<AudioInputSource>(
+ std::move(listener), sourceId, deviceId, channels, true, testPrincipal,
+ rate, mGraph->GraphRate(), bufferingMs));
+ });
+ RefPtr<SmartMockCubebStream> stream = WaitFor(cubeb->StreamInitEvent());
+ EXPECT_TRUE(stream->mHasInput);
+ EXPECT_FALSE(stream->mHasOutput);
+ EXPECT_EQ(stream->GetInputDeviceID(), deviceId);
+ EXPECT_EQ(stream->InputChannels(), channels);
+ EXPECT_EQ(stream->InputSampleRate(), static_cast<uint32_t>(rate));
+
+ // Make sure the stream is running.
+ Unused << WaitFor(stream->FramesProcessedEvent());
+
+ // Force an error in the MockCubeb.
+ DispatchFunction([&] { stream->ForceError(); });
+ WaitFor(stream->ErrorForcedEvent());
+
+ // Make sure the stream has been stopped by the error-state's backgroud thread
+ // task, to avoid getting a stopped state callback by `track->StopAudio`
+ // below.
+ WaitFor(stream->ErrorStoppedEvent());
+
+ // Stop and destroy the stream.
+ DispatchFunction([&] { track->StopAudio(); });
+ Unused << WaitFor(cubeb->StreamDestroyEvent());
+
+ // Tear down: Destroy the NonNativeInputTrack and remove it from mGraph.
+ track->Destroy();
+ mGraph->RemoveTrackGraphThread(track);
+}
diff --git a/dom/media/gtest/TestDriftCompensation.cpp b/dom/media/gtest/TestDriftCompensation.cpp
new file mode 100644
index 0000000000..055a74ff5f
--- /dev/null
+++ b/dom/media/gtest/TestDriftCompensation.cpp
@@ -0,0 +1,86 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "DriftCompensation.h"
+#include "mozilla/SpinEventLoopUntil.h"
+
+using namespace mozilla;
+
+class DriftCompensatorTest : public ::testing::Test {
+ public:
+ const TrackRate mRate = 44100;
+ const TimeStamp mStart;
+ const RefPtr<DriftCompensator> mComp;
+
+ DriftCompensatorTest()
+ : mStart(TimeStamp::Now()),
+ mComp(MakeRefPtr<DriftCompensator>(GetCurrentSerialEventTarget(),
+ mRate)) {
+ mComp->NotifyAudioStart(mStart);
+ // NotifyAudioStart dispatched a runnable to update the audio mStart time on
+ // the video thread. Because this is a test, the video thread is the current
+ // thread. We spin the event loop until we know the mStart time is updated.
+ {
+ bool updated = false;
+ NS_DispatchToCurrentThread(
+ NS_NewRunnableFunction(__func__, [&] { updated = true; }));
+ SpinEventLoopUntil("DriftCompensatorTest::DriftCompensatorTest"_ns,
+ [&] { return updated; });
+ }
+ }
+
+ // Past() is half as far from `mStart` as `aNow`.
+ TimeStamp Past(TimeStamp aNow) {
+ return mStart + (aNow - mStart) / (int64_t)2;
+ }
+
+ // Future() is twice as far from `mStart` as `aNow`.
+ TimeStamp Future(TimeStamp aNow) { return mStart + (aNow - mStart) * 2; }
+};
+
+TEST_F(DriftCompensatorTest, Initialized) {
+ EXPECT_EQ(mComp->GetVideoTime(mStart, mStart), mStart);
+}
+
+TEST_F(DriftCompensatorTest, SlowerAudio) {
+ // 10s of audio took 20 seconds of wall clock to play out
+ mComp->NotifyAudio(mRate * 10);
+ TimeStamp now = mStart + TimeDuration::FromSeconds(20);
+ EXPECT_EQ((mComp->GetVideoTime(now, mStart) - mStart).ToSeconds(), 0.0);
+ EXPECT_EQ((mComp->GetVideoTime(now, Past(now)) - mStart).ToSeconds(), 5.0);
+ EXPECT_EQ((mComp->GetVideoTime(now, now) - mStart).ToSeconds(), 10.0);
+ EXPECT_EQ((mComp->GetVideoTime(now, Future(now)) - mStart).ToSeconds(), 20.0);
+}
+
+TEST_F(DriftCompensatorTest, NoDrift) {
+ // 10s of audio took 10 seconds of wall clock to play out
+ mComp->NotifyAudio(mRate * 10);
+ TimeStamp now = mStart + TimeDuration::FromSeconds(10);
+ EXPECT_EQ((mComp->GetVideoTime(now, mStart) - mStart).ToSeconds(), 0.0);
+ EXPECT_EQ((mComp->GetVideoTime(now, Past(now)) - mStart).ToSeconds(), 5.0);
+ EXPECT_EQ((mComp->GetVideoTime(now, now) - mStart).ToSeconds(), 10.0);
+ EXPECT_EQ((mComp->GetVideoTime(now, Future(now)) - mStart).ToSeconds(), 20.0);
+}
+
+TEST_F(DriftCompensatorTest, NoProgress) {
+ // 10s of audio took 0 seconds of wall clock to play out
+ mComp->NotifyAudio(mRate * 10);
+ TimeStamp now = mStart;
+ TimeStamp future = mStart + TimeDuration::FromSeconds(5);
+ EXPECT_EQ((mComp->GetVideoTime(now, mStart) - mStart).ToSeconds(), 0.0);
+ EXPECT_EQ((mComp->GetVideoTime(now, future) - mStart).ToSeconds(), 5.0);
+}
+
+TEST_F(DriftCompensatorTest, FasterAudio) {
+ // 20s of audio took 10 seconds of wall clock to play out
+ mComp->NotifyAudio(mRate * 20);
+ TimeStamp now = mStart + TimeDuration::FromSeconds(10);
+ EXPECT_EQ((mComp->GetVideoTime(now, mStart) - mStart).ToSeconds(), 0.0);
+ EXPECT_EQ((mComp->GetVideoTime(now, Past(now)) - mStart).ToSeconds(), 10.0);
+ EXPECT_EQ((mComp->GetVideoTime(now, now) - mStart).ToSeconds(), 20.0);
+ EXPECT_EQ((mComp->GetVideoTime(now, Future(now)) - mStart).ToSeconds(), 40.0);
+}
diff --git a/dom/media/gtest/TestDynamicResampler.cpp b/dom/media/gtest/TestDynamicResampler.cpp
new file mode 100644
index 0000000000..4db0bd4854
--- /dev/null
+++ b/dom/media/gtest/TestDynamicResampler.cpp
@@ -0,0 +1,1556 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gmock/gmock.h"
+#include "gtest/gtest-printers.h"
+#include "gtest/gtest.h"
+
+#include "DynamicResampler.h"
+
+#include "nsContentUtils.h"
+
+using namespace mozilla;
+
+TEST(TestDynamicResampler, SameRates_Float1)
+{
+ const uint32_t in_frames = 100;
+ const uint32_t out_frames = 100;
+ uint32_t channels = 2;
+ uint32_t in_rate = 44100;
+ uint32_t out_rate = 44100;
+
+ DynamicResampler dr(in_rate, out_rate);
+ dr.SetSampleFormat(AUDIO_FORMAT_FLOAT32);
+ EXPECT_EQ(dr.GetOutRate(), out_rate);
+ EXPECT_EQ(dr.GetChannels(), channels);
+
+ // float in_ch1[] = {.1, .2, .3, .4, .5, .6, .7, .8, .9, 1.0};
+ // float in_ch2[] = {.1, .2, .3, .4, .5, .6, .7, .8, .9, 1.0};
+ float in_ch1[in_frames] = {};
+ float in_ch2[in_frames] = {};
+ AutoTArray<const float*, 2> in_buffer;
+ in_buffer.AppendElements(channels);
+ in_buffer[0] = in_ch1;
+ in_buffer[1] = in_ch2;
+
+ float out_ch1[out_frames] = {};
+ float out_ch2[out_frames] = {};
+
+ // Warm up with zeros
+ dr.AppendInput(in_buffer, in_frames);
+ uint32_t out_frames_used = out_frames;
+ bool rv = dr.Resample(out_ch1, &out_frames_used, 0);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames_used, out_frames);
+ rv = dr.Resample(out_ch2, &out_frames_used, 1);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames_used, out_frames);
+ for (uint32_t i = 0; i < out_frames; ++i) {
+ EXPECT_FLOAT_EQ(in_ch1[i], out_ch1[i]);
+ EXPECT_FLOAT_EQ(in_ch2[i], out_ch2[i]);
+ }
+
+ // Continue with non zero
+ for (uint32_t i = 0; i < in_frames; ++i) {
+ in_ch1[i] = in_ch2[i] = 0.01f * i;
+ }
+ dr.AppendInput(in_buffer, in_frames);
+ out_frames_used = out_frames;
+ rv = dr.Resample(out_ch1, &out_frames_used, 0);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames_used, out_frames);
+ rv = dr.Resample(out_ch2, &out_frames_used, 1);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames_used, out_frames);
+ for (uint32_t i = 0; i < out_frames; ++i) {
+ EXPECT_FLOAT_EQ(in_ch1[i], out_ch1[i]);
+ EXPECT_FLOAT_EQ(in_ch2[i], out_ch2[i]);
+ }
+
+ // No more frames in the input buffer
+ rv = dr.Resample(out_ch1, &out_frames_used, 0);
+ EXPECT_FALSE(rv);
+ EXPECT_EQ(out_frames_used, 0u);
+ out_frames_used = 2;
+ rv = dr.Resample(out_ch2, &out_frames_used, 1);
+ EXPECT_FALSE(rv);
+ EXPECT_EQ(out_frames_used, 0u);
+}
+
+TEST(TestDynamicResampler, SameRates_Short1)
+{
+ uint32_t in_frames = 2;
+ uint32_t out_frames = 2;
+ uint32_t channels = 2;
+ uint32_t in_rate = 44100;
+ uint32_t out_rate = 44100;
+
+ DynamicResampler dr(in_rate, out_rate);
+ dr.SetSampleFormat(AUDIO_FORMAT_S16);
+ EXPECT_EQ(dr.GetOutRate(), out_rate);
+ EXPECT_EQ(dr.GetChannels(), channels);
+
+ short in_ch1[] = {1, 2, 3};
+ short in_ch2[] = {4, 5, 6};
+ AutoTArray<const short*, 2> in_buffer;
+ in_buffer.AppendElements(channels);
+ in_buffer[0] = in_ch1;
+ in_buffer[1] = in_ch2;
+
+ short out_ch1[3] = {};
+ short out_ch2[3] = {};
+
+ dr.AppendInput(in_buffer, in_frames);
+ bool rv = dr.Resample(out_ch1, &out_frames, 0);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 2u);
+ rv = dr.Resample(out_ch2, &out_frames, 1);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 2u);
+ for (uint32_t i = 0; i < out_frames; ++i) {
+ EXPECT_EQ(in_ch1[i], out_ch1[i]);
+ EXPECT_EQ(in_ch2[i], out_ch2[i]);
+ }
+
+ // No more frames in the input buffer
+ rv = dr.Resample(out_ch1, &out_frames, 0);
+ EXPECT_FALSE(rv);
+ EXPECT_EQ(out_frames, 0u);
+ out_frames = 2;
+ rv = dr.Resample(out_ch2, &out_frames, 1);
+ EXPECT_FALSE(rv);
+ EXPECT_EQ(out_frames, 0u);
+}
+
+TEST(TestDynamicResampler, SameRates_Float2)
+{
+ uint32_t in_frames = 3;
+ uint32_t out_frames = 2;
+ uint32_t channels = 2;
+ uint32_t in_rate = 44100;
+ uint32_t out_rate = 44100;
+
+ DynamicResampler dr(in_rate, out_rate);
+ dr.SetSampleFormat(AUDIO_FORMAT_FLOAT32);
+
+ float in_ch1[] = {0.1, 0.2, 0.3};
+ float in_ch2[] = {0.4, 0.5, 0.6};
+ AutoTArray<const float*, 2> in_buffer;
+ in_buffer.AppendElements(channels);
+ in_buffer[0] = in_ch1;
+ in_buffer[1] = in_ch2;
+
+ float out_ch1[3] = {};
+ float out_ch2[3] = {};
+
+ dr.AppendInput(in_buffer, in_frames);
+ bool rv = dr.Resample(out_ch1, &out_frames, 0);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 2u);
+ rv = dr.Resample(out_ch2, &out_frames, 1);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 2u);
+ for (uint32_t i = 0; i < out_frames; ++i) {
+ EXPECT_FLOAT_EQ(in_ch1[i], out_ch1[i]);
+ EXPECT_FLOAT_EQ(in_ch2[i], out_ch2[i]);
+ }
+
+ out_frames = 1;
+ rv = dr.Resample(out_ch1, &out_frames, 0);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 1u);
+ rv = dr.Resample(out_ch2, &out_frames, 1);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 1u);
+ for (uint32_t i = 0; i < out_frames; ++i) {
+ EXPECT_FLOAT_EQ(in_ch1[i + 2], out_ch1[i]);
+ EXPECT_FLOAT_EQ(in_ch2[i + 2], out_ch2[i]);
+ }
+
+ // No more frames, the input buffer has drained
+ rv = dr.Resample(out_ch1, &out_frames, 0);
+ EXPECT_FALSE(rv);
+ EXPECT_EQ(out_frames, 0u);
+ out_frames = 1;
+ rv = dr.Resample(out_ch2, &out_frames, 1);
+ EXPECT_FALSE(rv);
+ EXPECT_EQ(out_frames, 0u);
+}
+
+TEST(TestDynamicResampler, SameRates_Short2)
+{
+ uint32_t in_frames = 3;
+ uint32_t out_frames = 2;
+ uint32_t channels = 2;
+ uint32_t in_rate = 44100;
+ uint32_t out_rate = 44100;
+
+ DynamicResampler dr(in_rate, out_rate);
+ dr.SetSampleFormat(AUDIO_FORMAT_S16);
+
+ short in_ch1[] = {1, 2, 3};
+ short in_ch2[] = {4, 5, 6};
+ AutoTArray<const short*, 2> in_buffer;
+ in_buffer.AppendElements(channels);
+ in_buffer[0] = in_ch1;
+ in_buffer[1] = in_ch2;
+
+ short out_ch1[3] = {};
+ short out_ch2[3] = {};
+
+ dr.AppendInput(in_buffer, in_frames);
+ bool rv = dr.Resample(out_ch1, &out_frames, 0);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 2u);
+ rv = dr.Resample(out_ch2, &out_frames, 1);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 2u);
+ for (uint32_t i = 0; i < out_frames; ++i) {
+ EXPECT_EQ(in_ch1[i], out_ch1[i]);
+ EXPECT_EQ(in_ch2[i], out_ch2[i]);
+ }
+
+ out_frames = 1;
+ rv = dr.Resample(out_ch1, &out_frames, 0);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 1u);
+ rv = dr.Resample(out_ch2, &out_frames, 1);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 1u);
+ for (uint32_t i = 0; i < out_frames; ++i) {
+ EXPECT_EQ(in_ch1[i + 2], out_ch1[i]);
+ EXPECT_EQ(in_ch2[i + 2], out_ch2[i]);
+ }
+
+ // No more frames, the input buffer has drained
+ rv = dr.Resample(out_ch1, &out_frames, 0);
+ EXPECT_FALSE(rv);
+ EXPECT_EQ(out_frames, 0u);
+ out_frames = 1;
+ rv = dr.Resample(out_ch2, &out_frames, 1);
+ EXPECT_FALSE(rv);
+ EXPECT_EQ(out_frames, 0u);
+}
+
+TEST(TestDynamicResampler, SameRates_Float3)
+{
+ uint32_t in_frames = 2;
+ uint32_t out_frames = 3;
+ uint32_t channels = 2;
+ uint32_t in_rate = 44100;
+ uint32_t out_rate = 44100;
+
+ DynamicResampler dr(in_rate, out_rate);
+ dr.SetSampleFormat(AUDIO_FORMAT_FLOAT32);
+
+ float in_ch1[] = {0.1, 0.2, 0.3};
+ float in_ch2[] = {0.4, 0.5, 0.6};
+ AutoTArray<const float*, 2> in_buffer;
+ in_buffer.AppendElements(channels);
+ in_buffer[0] = in_ch1;
+ in_buffer[1] = in_ch2;
+
+ float out_ch1[3] = {};
+ float out_ch2[3] = {};
+
+ // Not enough frames in the input buffer
+ dr.AppendInput(in_buffer, in_frames);
+ bool rv = dr.Resample(out_ch1, &out_frames, 0);
+ EXPECT_FALSE(rv);
+ EXPECT_EQ(out_frames, 0u);
+ out_frames = 3;
+ rv = dr.Resample(out_ch2, &out_frames, 1);
+ EXPECT_FALSE(rv);
+ EXPECT_EQ(out_frames, 0u);
+
+ // Add one more frame
+ in_buffer[0] = in_ch1 + 2;
+ in_buffer[1] = in_ch2 + 2;
+ dr.AppendInput(in_buffer, 1);
+ out_frames = 3;
+ rv = dr.Resample(out_ch1, &out_frames, 0);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 3u);
+ rv = dr.Resample(out_ch2, &out_frames, 1);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 3u);
+ for (uint32_t i = 0; i < out_frames; ++i) {
+ EXPECT_FLOAT_EQ(in_ch1[i], out_ch1[i]);
+ EXPECT_FLOAT_EQ(in_ch2[i], out_ch2[i]);
+ }
+}
+
+TEST(TestDynamicResampler, SameRates_Short3)
+{
+ uint32_t in_frames = 2;
+ uint32_t out_frames = 3;
+ uint32_t channels = 2;
+ uint32_t in_rate = 44100;
+ uint32_t out_rate = 44100;
+
+ DynamicResampler dr(in_rate, out_rate);
+ dr.SetSampleFormat(AUDIO_FORMAT_S16);
+
+ short in_ch1[] = {1, 2, 3};
+ short in_ch2[] = {4, 5, 6};
+ AutoTArray<const short*, 2> in_buffer;
+ in_buffer.AppendElements(channels);
+ in_buffer[0] = in_ch1;
+ in_buffer[1] = in_ch2;
+
+ short out_ch1[3] = {};
+ short out_ch2[3] = {};
+
+ // Not enough frames in the input buffer
+ dr.AppendInput(in_buffer, in_frames);
+ bool rv = dr.Resample(out_ch1, &out_frames, 0);
+ EXPECT_FALSE(rv);
+ EXPECT_EQ(out_frames, 0u);
+ out_frames = 3;
+ rv = dr.Resample(out_ch2, &out_frames, 1);
+ EXPECT_FALSE(rv);
+ EXPECT_EQ(out_frames, 0u);
+
+ // Add one more frame
+ in_buffer[0] = in_ch1 + 2;
+ in_buffer[1] = in_ch2 + 2;
+ dr.AppendInput(in_buffer, 1);
+ out_frames = 3;
+ rv = dr.Resample(out_ch1, &out_frames, 0);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 3u);
+ rv = dr.Resample(out_ch2, &out_frames, 1);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 3u);
+ for (uint32_t i = 0; i < out_frames; ++i) {
+ EXPECT_EQ(in_ch1[i], out_ch1[i]);
+ EXPECT_EQ(in_ch2[i], out_ch2[i]);
+ }
+}
+
+TEST(TestDynamicResampler, UpdateOutRate_Float)
+{
+ uint32_t in_frames = 10;
+ uint32_t out_frames = 40;
+ uint32_t channels = 2;
+ uint32_t in_rate = 24000;
+ uint32_t out_rate = 48000;
+
+ uint32_t pre_buffer = 20;
+
+ DynamicResampler dr(in_rate, out_rate, pre_buffer);
+ dr.SetSampleFormat(AUDIO_FORMAT_FLOAT32);
+ EXPECT_EQ(dr.GetOutRate(), out_rate);
+ EXPECT_EQ(dr.GetChannels(), channels);
+
+ float in_ch1[10] = {};
+ float in_ch2[10] = {};
+ for (uint32_t i = 0; i < in_frames; ++i) {
+ in_ch1[i] = in_ch2[i] = 0.01f * i;
+ }
+ AutoTArray<const float*, 2> in_buffer;
+ in_buffer.AppendElements(channels);
+ in_buffer[0] = in_ch1;
+ in_buffer[1] = in_ch2;
+
+ float out_ch1[40] = {};
+ float out_ch2[40] = {};
+
+ dr.AppendInput(in_buffer, in_frames);
+ bool rv = dr.Resample(out_ch1, &out_frames, 0);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 40u);
+ rv = dr.Resample(out_ch2, &out_frames, 1);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 40u);
+ for (uint32_t i = 0; i < out_frames; ++i) {
+ // Only pre buffered data reach output
+ EXPECT_FLOAT_EQ(out_ch1[i], 0.0);
+ EXPECT_FLOAT_EQ(out_ch2[i], 0.0);
+ }
+
+ // Update out rate
+ out_rate = 44100;
+ dr.UpdateResampler(out_rate, channels);
+ EXPECT_EQ(dr.GetOutRate(), out_rate);
+ EXPECT_EQ(dr.GetChannels(), channels);
+ out_frames = in_frames * out_rate / in_rate;
+ EXPECT_EQ(out_frames, 18u);
+ // Even if we provide no input if we have enough buffered input, we can create
+ // output
+ rv = dr.Resample(out_ch1, &out_frames, 0);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 18u);
+ rv = dr.Resample(out_ch2, &out_frames, 1);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 18u);
+}
+
+TEST(TestDynamicResampler, UpdateOutRate_Short)
+{
+ uint32_t in_frames = 10;
+ uint32_t out_frames = 40;
+ uint32_t channels = 2;
+ uint32_t in_rate = 24000;
+ uint32_t out_rate = 48000;
+
+ uint32_t pre_buffer = 20;
+
+ DynamicResampler dr(in_rate, out_rate, pre_buffer);
+ dr.SetSampleFormat(AUDIO_FORMAT_S16);
+ EXPECT_EQ(dr.GetOutRate(), out_rate);
+ EXPECT_EQ(dr.GetChannels(), channels);
+
+ short in_ch1[10] = {};
+ short in_ch2[10] = {};
+ for (uint32_t i = 0; i < in_frames; ++i) {
+ in_ch1[i] = in_ch2[i] = i;
+ }
+ AutoTArray<const short*, 2> in_buffer;
+ in_buffer.AppendElements(channels);
+ in_buffer[0] = in_ch1;
+ in_buffer[1] = in_ch2;
+
+ short out_ch1[40] = {};
+ short out_ch2[40] = {};
+
+ dr.AppendInput(in_buffer, in_frames);
+ bool rv = dr.Resample(out_ch1, &out_frames, 0);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 40u);
+ rv = dr.Resample(out_ch2, &out_frames, 1);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 40u);
+ for (uint32_t i = 0; i < out_frames; ++i) {
+ // Only pre buffered data reach output
+ EXPECT_EQ(out_ch1[i], 0.0);
+ EXPECT_EQ(out_ch2[i], 0.0);
+ }
+
+ // Update out rate
+ out_rate = 44100;
+ dr.UpdateResampler(out_rate, channels);
+ EXPECT_EQ(dr.GetOutRate(), out_rate);
+ EXPECT_EQ(dr.GetChannels(), channels);
+ out_frames = in_frames * out_rate / in_rate;
+ EXPECT_EQ(out_frames, 18u);
+ // Even if we provide no input if we have enough buffered input, we can create
+ // output
+ rv = dr.Resample(out_ch1, &out_frames, 0);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 18u);
+ rv = dr.Resample(out_ch2, &out_frames, 1);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 18u);
+}
+
+TEST(TestDynamicResampler, BigRangeOutRates_Float)
+{
+ uint32_t in_frames = 10;
+ uint32_t out_frames = 10;
+ uint32_t channels = 2;
+ uint32_t in_rate = 44100;
+ uint32_t out_rate = 44100;
+ uint32_t pre_buffer = 20;
+
+ DynamicResampler dr(in_rate, out_rate, pre_buffer);
+ dr.SetSampleFormat(AUDIO_FORMAT_FLOAT32);
+
+ const uint32_t in_capacity = 40;
+ float in_ch1[in_capacity] = {};
+ float in_ch2[in_capacity] = {};
+ for (uint32_t i = 0; i < in_capacity; ++i) {
+ in_ch1[i] = in_ch2[i] = 0.01f * i;
+ }
+ AutoTArray<const float*, 2> in_buffer;
+ in_buffer.AppendElements(channels);
+ in_buffer[0] = in_ch1;
+ in_buffer[1] = in_ch2;
+
+ const uint32_t out_capacity = 1000;
+ float out_ch1[out_capacity] = {};
+ float out_ch2[out_capacity] = {};
+
+ for (uint32_t rate = 10000; rate < 90000; ++rate) {
+ out_rate = rate;
+ dr.UpdateResampler(out_rate, channels);
+ EXPECT_EQ(dr.GetOutRate(), out_rate);
+ EXPECT_EQ(dr.GetChannels(), channels);
+ in_frames = 20; // more than we need
+ out_frames = in_frames * out_rate / in_rate;
+ uint32_t expected_out_frames = out_frames;
+ for (uint32_t y = 0; y < 2; ++y) {
+ dr.AppendInput(in_buffer, in_frames);
+ bool rv = dr.Resample(out_ch1, &out_frames, 0);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, expected_out_frames);
+ rv = dr.Resample(out_ch2, &out_frames, 1);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, expected_out_frames);
+ }
+ }
+}
+
+TEST(TestDynamicResampler, BigRangeOutRates_Short)
+{
+ uint32_t in_frames = 10;
+ uint32_t out_frames = 10;
+ uint32_t channels = 2;
+ uint32_t in_rate = 44100;
+ uint32_t out_rate = 44100;
+ uint32_t pre_buffer = 20;
+
+ DynamicResampler dr(in_rate, out_rate, pre_buffer);
+ dr.SetSampleFormat(AUDIO_FORMAT_S16);
+
+ const uint32_t in_capacity = 40;
+ short in_ch1[in_capacity] = {};
+ short in_ch2[in_capacity] = {};
+ for (uint32_t i = 0; i < in_capacity; ++i) {
+ in_ch1[i] = in_ch2[i] = i;
+ }
+ AutoTArray<const short*, 2> in_buffer;
+ in_buffer.AppendElements(channels);
+ in_buffer[0] = in_ch1;
+ in_buffer[1] = in_ch2;
+
+ const uint32_t out_capacity = 1000;
+ short out_ch1[out_capacity] = {};
+ short out_ch2[out_capacity] = {};
+
+ for (uint32_t rate = 10000; rate < 90000; ++rate) {
+ out_rate = rate;
+ dr.UpdateResampler(out_rate, channels);
+ in_frames = 20; // more than we need
+ out_frames = in_frames * out_rate / in_rate;
+ uint32_t expected_out_frames = out_frames;
+ for (uint32_t y = 0; y < 2; ++y) {
+ dr.AppendInput(in_buffer, in_frames);
+ bool rv = dr.Resample(out_ch1, &out_frames, 0);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, expected_out_frames);
+ rv = dr.Resample(out_ch2, &out_frames, 1);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, expected_out_frames);
+ }
+ }
+}
+
+TEST(TestDynamicResampler, UpdateChannels_Float)
+{
+ uint32_t in_frames = 10;
+ uint32_t out_frames = 10;
+ uint32_t channels = 2;
+ uint32_t in_rate = 44100;
+ uint32_t out_rate = 48000;
+
+ DynamicResampler dr(in_rate, out_rate);
+ dr.SetSampleFormat(AUDIO_FORMAT_FLOAT32);
+
+ float in_ch1[10] = {};
+ float in_ch2[10] = {};
+ for (uint32_t i = 0; i < in_frames; ++i) {
+ in_ch1[i] = in_ch2[i] = 0.01f * i;
+ }
+ AutoTArray<const float*, 2> in_buffer;
+ in_buffer.AppendElements(channels);
+ in_buffer[0] = in_ch1;
+ in_buffer[1] = in_ch2;
+
+ float out_ch1[10] = {};
+ float out_ch2[10] = {};
+
+ dr.AppendInput(in_buffer, in_frames);
+ bool rv = dr.Resample(out_ch1, &out_frames, 0);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 10u);
+ rv = dr.Resample(out_ch2, &out_frames, 1);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 10u);
+
+ // Add 3rd channel
+ dr.UpdateResampler(out_rate, 3);
+ EXPECT_EQ(dr.GetOutRate(), out_rate);
+ EXPECT_EQ(dr.GetChannels(), 3u);
+
+ float in_ch3[10] = {};
+ for (uint32_t i = 0; i < in_frames; ++i) {
+ in_ch3[i] = 0.01f * i;
+ }
+ in_buffer.AppendElement();
+ in_buffer[2] = in_ch3;
+ float out_ch3[10] = {};
+
+ dr.AppendInput(in_buffer, in_frames);
+
+ rv = dr.Resample(out_ch1, &out_frames, 0);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 10u);
+ rv = dr.Resample(out_ch2, &out_frames, 1);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 10u);
+ rv = dr.Resample(out_ch3, &out_frames, 2);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 10u);
+
+ float in_ch4[10] = {};
+ for (uint32_t i = 0; i < in_frames; ++i) {
+ in_ch3[i] = 0.01f * i;
+ }
+ in_buffer.AppendElement();
+ in_buffer[3] = in_ch4;
+ float out_ch4[10] = {};
+
+ dr.UpdateResampler(out_rate, 4);
+ EXPECT_EQ(dr.GetOutRate(), out_rate);
+ EXPECT_EQ(dr.GetChannels(), 4u);
+ dr.AppendInput(in_buffer, in_frames);
+
+ rv = dr.Resample(out_ch1, &out_frames, 0);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 10u);
+ rv = dr.Resample(out_ch2, &out_frames, 1);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 10u);
+ rv = dr.Resample(out_ch3, &out_frames, 2);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 10u);
+ rv = dr.Resample(out_ch4, &out_frames, 3);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 10u);
+}
+
+TEST(TestDynamicResampler, UpdateChannels_Short)
+{
+ uint32_t in_frames = 10;
+ uint32_t out_frames = 10;
+ uint32_t channels = 2;
+ uint32_t in_rate = 44100;
+ uint32_t out_rate = 48000;
+
+ DynamicResampler dr(in_rate, out_rate);
+ dr.SetSampleFormat(AUDIO_FORMAT_S16);
+
+ short in_ch1[10] = {};
+ short in_ch2[10] = {};
+ for (uint32_t i = 0; i < in_frames; ++i) {
+ in_ch1[i] = in_ch2[i] = i;
+ }
+ AutoTArray<const short*, 2> in_buffer;
+ in_buffer.AppendElements(channels);
+ in_buffer[0] = in_ch1;
+ in_buffer[1] = in_ch2;
+
+ short out_ch1[10] = {};
+ short out_ch2[10] = {};
+
+ dr.AppendInput(in_buffer, in_frames);
+ bool rv = dr.Resample(out_ch1, &out_frames, 0);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 10u);
+ rv = dr.Resample(out_ch2, &out_frames, 1);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 10u);
+
+ // Add 3rd channel
+ dr.UpdateResampler(out_rate, 3);
+ EXPECT_EQ(dr.GetOutRate(), out_rate);
+ EXPECT_EQ(dr.GetChannels(), 3u);
+
+ short in_ch3[10] = {};
+ for (uint32_t i = 0; i < in_frames; ++i) {
+ in_ch3[i] = i;
+ }
+ in_buffer.AppendElement();
+ in_buffer[2] = in_ch3;
+ short out_ch3[10] = {};
+
+ dr.AppendInput(in_buffer, in_frames);
+
+ rv = dr.Resample(out_ch1, &out_frames, 0);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 10u);
+ rv = dr.Resample(out_ch2, &out_frames, 1);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 10u);
+ rv = dr.Resample(out_ch3, &out_frames, 2);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 10u);
+
+ // Check update with AudioSegment
+ short in_ch4[10] = {};
+ for (uint32_t i = 0; i < in_frames; ++i) {
+ in_ch3[i] = i;
+ }
+ in_buffer.AppendElement();
+ in_buffer[3] = in_ch4;
+ short out_ch4[10] = {};
+
+ dr.UpdateResampler(out_rate, 4);
+ EXPECT_EQ(dr.GetOutRate(), out_rate);
+ EXPECT_EQ(dr.GetChannels(), 4u);
+ dr.AppendInput(in_buffer, in_frames);
+
+ rv = dr.Resample(out_ch1, &out_frames, 0);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 10u);
+ rv = dr.Resample(out_ch2, &out_frames, 1);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 10u);
+ rv = dr.Resample(out_ch3, &out_frames, 2);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 10u);
+ rv = dr.Resample(out_ch4, &out_frames, 3);
+ EXPECT_TRUE(rv);
+ EXPECT_EQ(out_frames, 10u);
+}
+
+TEST(TestAudioChunkList, Basic1)
+{
+ const PrincipalHandle testPrincipal =
+ MakePrincipalHandle(nsContentUtils::GetSystemPrincipal());
+ AudioChunkList list(256, 2, testPrincipal);
+ list.SetSampleFormat(AUDIO_FORMAT_FLOAT32);
+ EXPECT_EQ(list.ChunkCapacity(), 128u);
+ EXPECT_EQ(list.TotalCapacity(), 256u);
+
+ AudioChunk& c1 = list.GetNext();
+ float* c1_ch1 = c1.ChannelDataForWrite<float>(0);
+ float* c1_ch2 = c1.ChannelDataForWrite<float>(1);
+ EXPECT_EQ(c1.mPrincipalHandle, testPrincipal);
+ EXPECT_EQ(c1.mBufferFormat, AUDIO_FORMAT_FLOAT32);
+ for (uint32_t i = 0; i < list.ChunkCapacity(); ++i) {
+ c1_ch1[i] = c1_ch2[i] = 0.01f * static_cast<float>(i);
+ }
+ AudioChunk& c2 = list.GetNext();
+ EXPECT_EQ(c2.mPrincipalHandle, testPrincipal);
+ EXPECT_EQ(c2.mBufferFormat, AUDIO_FORMAT_FLOAT32);
+ EXPECT_NE(c1.mBuffer.get(), c2.mBuffer.get());
+ AudioChunk& c3 = list.GetNext();
+ EXPECT_EQ(c3.mPrincipalHandle, testPrincipal);
+ EXPECT_EQ(c3.mBufferFormat, AUDIO_FORMAT_FLOAT32);
+ // Cycle
+ EXPECT_EQ(c1.mBuffer.get(), c3.mBuffer.get());
+ float* c3_ch1 = c3.ChannelDataForWrite<float>(0);
+ float* c3_ch2 = c3.ChannelDataForWrite<float>(1);
+ for (uint32_t i = 0; i < list.ChunkCapacity(); ++i) {
+ EXPECT_FLOAT_EQ(c1_ch1[i], c3_ch1[i]);
+ EXPECT_FLOAT_EQ(c1_ch2[i], c3_ch2[i]);
+ }
+}
+
+TEST(TestAudioChunkList, Basic2)
+{
+ const PrincipalHandle testPrincipal =
+ MakePrincipalHandle(nsContentUtils::GetSystemPrincipal());
+ AudioChunkList list(256, 2, testPrincipal);
+ list.SetSampleFormat(AUDIO_FORMAT_S16);
+ EXPECT_EQ(list.ChunkCapacity(), 256u);
+ EXPECT_EQ(list.TotalCapacity(), 512u);
+
+ AudioChunk& c1 = list.GetNext();
+ EXPECT_EQ(c1.mPrincipalHandle, testPrincipal);
+ EXPECT_EQ(c1.mBufferFormat, AUDIO_FORMAT_S16);
+ short* c1_ch1 = c1.ChannelDataForWrite<short>(0);
+ short* c1_ch2 = c1.ChannelDataForWrite<short>(1);
+ for (uint32_t i = 0; i < list.ChunkCapacity(); ++i) {
+ c1_ch1[i] = c1_ch2[i] = static_cast<short>(i);
+ }
+ AudioChunk& c2 = list.GetNext();
+ EXPECT_EQ(c2.mPrincipalHandle, testPrincipal);
+ EXPECT_EQ(c2.mBufferFormat, AUDIO_FORMAT_S16);
+ EXPECT_NE(c1.mBuffer.get(), c2.mBuffer.get());
+ AudioChunk& c3 = list.GetNext();
+ EXPECT_EQ(c3.mPrincipalHandle, testPrincipal);
+ EXPECT_EQ(c3.mBufferFormat, AUDIO_FORMAT_S16);
+ AudioChunk& c4 = list.GetNext();
+ EXPECT_EQ(c4.mPrincipalHandle, testPrincipal);
+ EXPECT_EQ(c4.mBufferFormat, AUDIO_FORMAT_S16);
+ // Cycle
+ AudioChunk& c5 = list.GetNext();
+ EXPECT_EQ(c5.mPrincipalHandle, testPrincipal);
+ EXPECT_EQ(c5.mBufferFormat, AUDIO_FORMAT_S16);
+ EXPECT_EQ(c1.mBuffer.get(), c5.mBuffer.get());
+ short* c5_ch1 = c5.ChannelDataForWrite<short>(0);
+ short* c5_ch2 = c5.ChannelDataForWrite<short>(1);
+ for (uint32_t i = 0; i < list.ChunkCapacity(); ++i) {
+ EXPECT_EQ(c1_ch1[i], c5_ch1[i]);
+ EXPECT_EQ(c1_ch2[i], c5_ch2[i]);
+ }
+}
+
+TEST(TestAudioChunkList, Basic3)
+{
+ AudioChunkList list(260, 2, PRINCIPAL_HANDLE_NONE);
+ list.SetSampleFormat(AUDIO_FORMAT_FLOAT32);
+ EXPECT_EQ(list.ChunkCapacity(), 128u);
+ EXPECT_EQ(list.TotalCapacity(), 256u + 128u);
+
+ AudioChunk& c1 = list.GetNext();
+ AudioChunk& c2 = list.GetNext();
+ EXPECT_NE(c1.mBuffer.get(), c2.mBuffer.get());
+ AudioChunk& c3 = list.GetNext();
+ EXPECT_NE(c1.mBuffer.get(), c3.mBuffer.get());
+ AudioChunk& c4 = list.GetNext();
+ EXPECT_EQ(c1.mBuffer.get(), c4.mBuffer.get());
+}
+
+TEST(TestAudioChunkList, Basic4)
+{
+ AudioChunkList list(260, 2, PRINCIPAL_HANDLE_NONE);
+ list.SetSampleFormat(AUDIO_FORMAT_S16);
+ EXPECT_EQ(list.ChunkCapacity(), 256u);
+ EXPECT_EQ(list.TotalCapacity(), 512u + 256u);
+
+ AudioChunk& c1 = list.GetNext();
+ AudioChunk& c2 = list.GetNext();
+ EXPECT_NE(c1.mBuffer.get(), c2.mBuffer.get());
+ AudioChunk& c3 = list.GetNext();
+ EXPECT_NE(c1.mBuffer.get(), c3.mBuffer.get());
+ AudioChunk& c4 = list.GetNext();
+ EXPECT_EQ(c1.mBuffer.get(), c4.mBuffer.get());
+}
+
+TEST(TestAudioChunkList, UpdateChannels)
+{
+ AudioChunkList list(256, 2, PRINCIPAL_HANDLE_NONE);
+ list.SetSampleFormat(AUDIO_FORMAT_FLOAT32);
+
+ AudioChunk& c1 = list.GetNext();
+ AudioChunk& c2 = list.GetNext();
+ EXPECT_EQ(c1.ChannelCount(), 2u);
+ EXPECT_EQ(c2.ChannelCount(), 2u);
+
+ // Update to Quad
+ list.Update(4);
+
+ AudioChunk& c3 = list.GetNext();
+ AudioChunk& c4 = list.GetNext();
+ EXPECT_EQ(c3.ChannelCount(), 4u);
+ EXPECT_EQ(c4.ChannelCount(), 4u);
+}
+
+TEST(TestAudioChunkList, UpdateBetweenMonoAndStereo)
+{
+ AudioChunkList list(256, 2, PRINCIPAL_HANDLE_NONE);
+ list.SetSampleFormat(AUDIO_FORMAT_FLOAT32);
+
+ AudioChunk& c1 = list.GetNext();
+ float* c1_ch1 = c1.ChannelDataForWrite<float>(0);
+ float* c1_ch2 = c1.ChannelDataForWrite<float>(1);
+ for (uint32_t i = 0; i < list.ChunkCapacity(); ++i) {
+ c1_ch1[i] = c1_ch2[i] = 0.01f * static_cast<float>(i);
+ }
+
+ AudioChunk& c2 = list.GetNext();
+ EXPECT_EQ(c1.ChannelCount(), 2u);
+ EXPECT_EQ(c2.ChannelCount(), 2u);
+
+ // Downmix to mono
+ list.Update(1);
+
+ AudioChunk& c3 = list.GetNext();
+ float* c3_ch1 = c3.ChannelDataForWrite<float>(0);
+ for (uint32_t i = 0; i < list.ChunkCapacity(); ++i) {
+ EXPECT_FLOAT_EQ(c3_ch1[i], c1_ch1[i]);
+ }
+
+ AudioChunk& c4 = list.GetNext();
+ EXPECT_EQ(c3.ChannelCount(), 1u);
+ EXPECT_EQ(c4.ChannelCount(), 1u);
+ EXPECT_EQ(static_cast<SharedChannelArrayBuffer<float>*>(c3.mBuffer.get())
+ ->mBuffers[0]
+ .Length(),
+ list.ChunkCapacity());
+
+ // Upmix to stereo
+ list.Update(2);
+
+ AudioChunk& c5 = list.GetNext();
+ AudioChunk& c6 = list.GetNext();
+ EXPECT_EQ(c5.ChannelCount(), 2u);
+ EXPECT_EQ(c6.ChannelCount(), 2u);
+ EXPECT_EQ(static_cast<SharedChannelArrayBuffer<float>*>(c5.mBuffer.get())
+ ->mBuffers[0]
+ .Length(),
+ list.ChunkCapacity());
+ EXPECT_EQ(static_cast<SharedChannelArrayBuffer<float>*>(c5.mBuffer.get())
+ ->mBuffers[1]
+ .Length(),
+ list.ChunkCapacity());
+
+ // Downmix to mono
+ list.Update(1);
+
+ AudioChunk& c7 = list.GetNext();
+ float* c7_ch1 = c7.ChannelDataForWrite<float>(0);
+ for (uint32_t i = 0; i < list.ChunkCapacity(); ++i) {
+ EXPECT_FLOAT_EQ(c7_ch1[i], c1_ch1[i]);
+ }
+
+ AudioChunk& c8 = list.GetNext();
+ EXPECT_EQ(c7.ChannelCount(), 1u);
+ EXPECT_EQ(c8.ChannelCount(), 1u);
+ EXPECT_EQ(static_cast<SharedChannelArrayBuffer<float>*>(c7.mBuffer.get())
+ ->mBuffers[0]
+ .Length(),
+ list.ChunkCapacity());
+}
+
+TEST(TestAudioChunkList, ConsumeAndForget)
+{
+ AudioSegment s;
+ AudioChunkList list(256, 2, PRINCIPAL_HANDLE_NONE);
+ list.SetSampleFormat(AUDIO_FORMAT_FLOAT32);
+
+ AudioChunk& c1 = list.GetNext();
+ AudioChunk tmp1 = c1;
+ s.AppendAndConsumeChunk(std::move(tmp1));
+ EXPECT_FALSE(c1.mBuffer.get() == nullptr);
+ EXPECT_EQ(c1.ChannelData<float>().Length(), 2u);
+
+ AudioChunk& c2 = list.GetNext();
+ AudioChunk tmp2 = c2;
+ s.AppendAndConsumeChunk(std::move(tmp2));
+ EXPECT_FALSE(c2.mBuffer.get() == nullptr);
+ EXPECT_EQ(c2.ChannelData<float>().Length(), 2u);
+
+ s.ForgetUpTo(256);
+ list.GetNext();
+ list.GetNext();
+}
+
+template <class T>
+AudioChunk CreateAudioChunk(uint32_t aFrames, uint32_t aChannels,
+ AudioSampleFormat aSampleFormat) {
+ AudioChunk chunk;
+ nsTArray<nsTArray<T>> buffer;
+ buffer.AppendElements(aChannels);
+
+ nsTArray<const T*> bufferPtrs;
+ bufferPtrs.AppendElements(aChannels);
+
+ for (uint32_t i = 0; i < aChannels; ++i) {
+ T* ptr = buffer[i].AppendElements(aFrames);
+ bufferPtrs[i] = ptr;
+ for (uint32_t j = 0; j < aFrames; ++j) {
+ if (aSampleFormat == AUDIO_FORMAT_FLOAT32) {
+ ptr[j] = 0.01 * j;
+ } else {
+ ptr[j] = j;
+ }
+ }
+ }
+
+ chunk.mBuffer = new mozilla::SharedChannelArrayBuffer(std::move(buffer));
+ chunk.mBufferFormat = aSampleFormat;
+ chunk.mChannelData.AppendElements(aChannels);
+ for (uint32_t i = 0; i < aChannels; ++i) {
+ chunk.mChannelData[i] = bufferPtrs[i];
+ }
+ chunk.mDuration = aFrames;
+ return chunk;
+}
+
+template <class T>
+AudioSegment CreateAudioSegment(uint32_t aFrames, uint32_t aChannels,
+ AudioSampleFormat aSampleFormat) {
+ AudioSegment segment;
+ AudioChunk chunk = CreateAudioChunk<T>(aFrames, aChannels, aSampleFormat);
+ segment.AppendAndConsumeChunk(std::move(chunk));
+ return segment;
+}
+
+TEST(TestAudioResampler, OutAudioSegment_Float)
+{
+ const PrincipalHandle testPrincipal =
+ MakePrincipalHandle(nsContentUtils::GetSystemPrincipal());
+
+ uint32_t in_frames = 10;
+ uint32_t out_frames = 40;
+ uint32_t channels = 2;
+ uint32_t in_rate = 24000;
+ uint32_t out_rate = 48000;
+
+ uint32_t pre_buffer = 21;
+
+ AudioResampler dr(in_rate, out_rate, pre_buffer, testPrincipal);
+
+ AudioSegment inSegment =
+ CreateAudioSegment<float>(in_frames, channels, AUDIO_FORMAT_FLOAT32);
+ dr.AppendInput(inSegment);
+
+ AudioSegment s = dr.Resample(out_frames);
+ EXPECT_EQ(s.GetDuration(), 40);
+ EXPECT_EQ(s.GetType(), MediaSegment::AUDIO);
+ EXPECT_TRUE(!s.IsNull());
+ EXPECT_TRUE(!s.IsEmpty());
+
+ for (AudioSegment::ChunkIterator ci(s); !ci.IsEnded(); ci.Next()) {
+ AudioChunk& c = *ci;
+ EXPECT_EQ(c.mPrincipalHandle, testPrincipal);
+ EXPECT_EQ(c.ChannelCount(), 2u);
+ for (uint32_t i = 0; i < out_frames; ++i) {
+ // Only pre buffered data reach output
+ EXPECT_FLOAT_EQ(c.ChannelData<float>()[0][i], 0.0);
+ EXPECT_FLOAT_EQ(c.ChannelData<float>()[1][i], 0.0);
+ }
+ }
+
+ // Update out rate
+ out_rate = 44100;
+ dr.UpdateOutRate(out_rate);
+ out_frames = in_frames * out_rate / in_rate;
+ EXPECT_EQ(out_frames, 18u);
+ // Even if we provide no input if we have enough buffered input, we can create
+ // output
+ AudioSegment s1 = dr.Resample(out_frames);
+ EXPECT_EQ(s1.GetDuration(), out_frames);
+ EXPECT_EQ(s1.GetType(), MediaSegment::AUDIO);
+ EXPECT_TRUE(!s1.IsNull());
+ EXPECT_TRUE(!s1.IsEmpty());
+ for (AudioSegment::ConstChunkIterator ci(s1); !ci.IsEnded(); ci.Next()) {
+ EXPECT_EQ(ci->mPrincipalHandle, testPrincipal);
+ }
+}
+
+TEST(TestAudioResampler, OutAudioSegment_Short)
+{
+ const PrincipalHandle testPrincipal =
+ MakePrincipalHandle(nsContentUtils::GetSystemPrincipal());
+
+ uint32_t in_frames = 10;
+ uint32_t out_frames = 40;
+ uint32_t channels = 2;
+ uint32_t in_rate = 24000;
+ uint32_t out_rate = 48000;
+
+ uint32_t pre_buffer = 21;
+
+ AudioResampler dr(in_rate, out_rate, pre_buffer, testPrincipal);
+
+ AudioSegment inSegment =
+ CreateAudioSegment<short>(in_frames, channels, AUDIO_FORMAT_S16);
+ dr.AppendInput(inSegment);
+
+ AudioSegment s = dr.Resample(out_frames);
+ EXPECT_EQ(s.GetDuration(), 40);
+ EXPECT_EQ(s.GetType(), MediaSegment::AUDIO);
+ EXPECT_TRUE(!s.IsNull());
+ EXPECT_TRUE(!s.IsEmpty());
+
+ for (AudioSegment::ChunkIterator ci(s); !ci.IsEnded(); ci.Next()) {
+ AudioChunk& c = *ci;
+ EXPECT_EQ(c.mPrincipalHandle, testPrincipal);
+ EXPECT_EQ(c.ChannelCount(), 2u);
+ for (uint32_t i = 0; i < out_frames; ++i) {
+ // Only pre buffered data reach output
+ EXPECT_FLOAT_EQ(c.ChannelData<short>()[0][i], 0.0);
+ EXPECT_FLOAT_EQ(c.ChannelData<short>()[1][i], 0.0);
+ }
+ }
+
+ // Update out rate
+ out_rate = 44100;
+ dr.UpdateOutRate(out_rate);
+ out_frames = in_frames * out_rate / in_rate;
+ EXPECT_EQ(out_frames, 18u);
+ // Even if we provide no input if we have enough buffered input, we can create
+ // output
+ AudioSegment s1 = dr.Resample(out_frames);
+ EXPECT_EQ(s1.GetDuration(), out_frames);
+ EXPECT_EQ(s1.GetType(), MediaSegment::AUDIO);
+ EXPECT_TRUE(!s1.IsNull());
+ EXPECT_TRUE(!s1.IsEmpty());
+ for (AudioSegment::ConstChunkIterator ci(s1); !ci.IsEnded(); ci.Next()) {
+ EXPECT_EQ(ci->mPrincipalHandle, testPrincipal);
+ }
+}
+
+TEST(TestAudioResampler, OutAudioSegmentFail_Float)
+{
+ const uint32_t in_frames = 130;
+ const uint32_t out_frames = 300;
+ uint32_t channels = 2;
+ uint32_t in_rate = 24000;
+ uint32_t out_rate = 48000;
+
+ uint32_t pre_buffer = 5;
+
+ AudioResampler dr(in_rate, out_rate, pre_buffer, PRINCIPAL_HANDLE_NONE);
+ AudioSegment inSegment =
+ CreateAudioSegment<float>(in_frames, channels, AUDIO_FORMAT_FLOAT32);
+ dr.AppendInput(inSegment);
+
+ AudioSegment s = dr.Resample(out_frames);
+ EXPECT_EQ(s.GetDuration(), 0);
+ EXPECT_EQ(s.GetType(), MediaSegment::AUDIO);
+ EXPECT_TRUE(s.IsNull());
+ EXPECT_TRUE(s.IsEmpty());
+}
+
+TEST(TestAudioResampler, InAudioSegment_Float)
+{
+ const PrincipalHandle testPrincipal =
+ MakePrincipalHandle(nsContentUtils::GetSystemPrincipal());
+
+ uint32_t in_frames = 10;
+ uint32_t out_frames = 40;
+ uint32_t channels = 2;
+ uint32_t in_rate = 24000;
+ uint32_t out_rate = 48000;
+
+ uint32_t pre_buffer = 10;
+ AudioResampler dr(in_rate, out_rate, pre_buffer, testPrincipal);
+
+ AudioSegment inSegment;
+
+ AudioChunk chunk1;
+ chunk1.SetNull(in_frames / 2);
+ inSegment.AppendAndConsumeChunk(std::move(chunk1));
+
+ AudioChunk chunk2;
+ nsTArray<nsTArray<float>> buffer;
+ buffer.AppendElements(channels);
+
+ nsTArray<const float*> bufferPtrs;
+ bufferPtrs.AppendElements(channels);
+
+ for (uint32_t i = 0; i < channels; ++i) {
+ float* ptr = buffer[i].AppendElements(5);
+ bufferPtrs[i] = ptr;
+ for (uint32_t j = 0; j < 5; ++j) {
+ ptr[j] = 0.01f * j;
+ }
+ }
+
+ chunk2.mBuffer = new mozilla::SharedChannelArrayBuffer(std::move(buffer));
+ chunk2.mBufferFormat = AUDIO_FORMAT_FLOAT32;
+ chunk2.mChannelData.AppendElements(channels);
+ for (uint32_t i = 0; i < channels; ++i) {
+ chunk2.mChannelData[i] = bufferPtrs[i];
+ }
+ chunk2.mDuration = in_frames / 2;
+ inSegment.AppendAndConsumeChunk(std::move(chunk2));
+
+ dr.AppendInput(inSegment);
+ AudioSegment outSegment = dr.Resample(out_frames);
+ // Faild because the first chunk is ignored
+ EXPECT_EQ(outSegment.GetDuration(), 0u);
+ EXPECT_EQ(outSegment.MaxChannelCount(), 0u);
+
+ // Add the 5 more frames that are missing
+ dr.AppendInput(inSegment);
+ AudioSegment outSegment2 = dr.Resample(out_frames);
+ EXPECT_EQ(outSegment2.GetDuration(), 40u);
+ EXPECT_EQ(outSegment2.MaxChannelCount(), 2u);
+ for (AudioSegment::ConstChunkIterator ci(outSegment2); !ci.IsEnded();
+ ci.Next()) {
+ EXPECT_EQ(ci->mPrincipalHandle, testPrincipal);
+ }
+}
+
+TEST(TestAudioResampler, InAudioSegment_Short)
+{
+ const PrincipalHandle testPrincipal =
+ MakePrincipalHandle(nsContentUtils::GetSystemPrincipal());
+
+ uint32_t in_frames = 10;
+ uint32_t out_frames = 40;
+ uint32_t channels = 2;
+ uint32_t in_rate = 24000;
+ uint32_t out_rate = 48000;
+
+ uint32_t pre_buffer = 10;
+ AudioResampler dr(in_rate, out_rate, pre_buffer, testPrincipal);
+
+ AudioSegment inSegment;
+
+ // The null chunk at the beginning will be ignored.
+ AudioChunk chunk1;
+ chunk1.SetNull(in_frames / 2);
+ inSegment.AppendAndConsumeChunk(std::move(chunk1));
+
+ AudioChunk chunk2;
+ nsTArray<nsTArray<short>> buffer;
+ buffer.AppendElements(channels);
+
+ nsTArray<const short*> bufferPtrs;
+ bufferPtrs.AppendElements(channels);
+
+ for (uint32_t i = 0; i < channels; ++i) {
+ short* ptr = buffer[i].AppendElements(5);
+ bufferPtrs[i] = ptr;
+ for (uint32_t j = 0; j < 5; ++j) {
+ ptr[j] = j;
+ }
+ }
+
+ chunk2.mBuffer = new mozilla::SharedChannelArrayBuffer(std::move(buffer));
+ chunk2.mBufferFormat = AUDIO_FORMAT_S16;
+ chunk2.mChannelData.AppendElements(channels);
+ for (uint32_t i = 0; i < channels; ++i) {
+ chunk2.mChannelData[i] = bufferPtrs[i];
+ }
+ chunk2.mDuration = in_frames / 2;
+ inSegment.AppendAndConsumeChunk(std::move(chunk2));
+
+ dr.AppendInput(inSegment);
+ AudioSegment outSegment = dr.Resample(out_frames);
+ // Faild because the first chunk is ignored
+ EXPECT_EQ(outSegment.GetDuration(), 0u);
+ EXPECT_EQ(outSegment.MaxChannelCount(), 0u);
+
+ dr.AppendInput(inSegment);
+ AudioSegment outSegment2 = dr.Resample(out_frames);
+ EXPECT_EQ(outSegment2.GetDuration(), 40u);
+ EXPECT_EQ(outSegment2.MaxChannelCount(), 2u);
+ for (AudioSegment::ConstChunkIterator ci(outSegment2); !ci.IsEnded();
+ ci.Next()) {
+ EXPECT_EQ(ci->mPrincipalHandle, testPrincipal);
+ }
+}
+
+TEST(TestAudioResampler, ChannelChange_MonoToStereo)
+{
+ const PrincipalHandle testPrincipal =
+ MakePrincipalHandle(nsContentUtils::GetSystemPrincipal());
+
+ uint32_t in_frames = 10;
+ uint32_t out_frames = 40;
+ // uint32_t channels = 2;
+ uint32_t in_rate = 24000;
+ uint32_t out_rate = 48000;
+
+ uint32_t pre_buffer = 0;
+
+ AudioResampler dr(in_rate, out_rate, pre_buffer, testPrincipal);
+
+ AudioChunk monoChunk =
+ CreateAudioChunk<float>(in_frames, 1, AUDIO_FORMAT_FLOAT32);
+ AudioChunk stereoChunk =
+ CreateAudioChunk<float>(in_frames, 2, AUDIO_FORMAT_FLOAT32);
+
+ AudioSegment inSegment;
+ inSegment.AppendAndConsumeChunk(std::move(monoChunk));
+ inSegment.AppendAndConsumeChunk(std::move(stereoChunk));
+ dr.AppendInput(inSegment);
+
+ AudioSegment s = dr.Resample(out_frames);
+ EXPECT_EQ(s.GetDuration(), 40);
+ EXPECT_EQ(s.GetType(), MediaSegment::AUDIO);
+ EXPECT_TRUE(!s.IsNull());
+ EXPECT_TRUE(!s.IsEmpty());
+ EXPECT_EQ(s.MaxChannelCount(), 2u);
+ for (AudioSegment::ConstChunkIterator ci(s); !ci.IsEnded(); ci.Next()) {
+ EXPECT_EQ(ci->mPrincipalHandle, testPrincipal);
+ }
+}
+
+TEST(TestAudioResampler, ChannelChange_StereoToMono)
+{
+ const PrincipalHandle testPrincipal =
+ MakePrincipalHandle(nsContentUtils::GetSystemPrincipal());
+
+ uint32_t in_frames = 10;
+ uint32_t out_frames = 40;
+ // uint32_t channels = 2;
+ uint32_t in_rate = 24000;
+ uint32_t out_rate = 48000;
+
+ uint32_t pre_buffer = 0;
+
+ AudioResampler dr(in_rate, out_rate, pre_buffer, testPrincipal);
+
+ AudioChunk monoChunk =
+ CreateAudioChunk<float>(in_frames, 1, AUDIO_FORMAT_FLOAT32);
+ AudioChunk stereoChunk =
+ CreateAudioChunk<float>(in_frames, 2, AUDIO_FORMAT_FLOAT32);
+
+ AudioSegment inSegment;
+ inSegment.AppendAndConsumeChunk(std::move(stereoChunk));
+ inSegment.AppendAndConsumeChunk(std::move(monoChunk));
+ dr.AppendInput(inSegment);
+
+ AudioSegment s = dr.Resample(out_frames);
+ EXPECT_EQ(s.GetDuration(), 40);
+ EXPECT_EQ(s.GetType(), MediaSegment::AUDIO);
+ EXPECT_TRUE(!s.IsNull());
+ EXPECT_TRUE(!s.IsEmpty());
+ EXPECT_EQ(s.MaxChannelCount(), 1u);
+ for (AudioSegment::ConstChunkIterator ci(s); !ci.IsEnded(); ci.Next()) {
+ EXPECT_EQ(ci->mPrincipalHandle, testPrincipal);
+ }
+}
+
+TEST(TestAudioResampler, ChannelChange_StereoToQuad)
+{
+ const PrincipalHandle testPrincipal =
+ MakePrincipalHandle(nsContentUtils::GetSystemPrincipal());
+
+ uint32_t in_frames = 10;
+ uint32_t out_frames = 40;
+ // uint32_t channels = 2;
+ uint32_t in_rate = 24000;
+ uint32_t out_rate = 48000;
+
+ uint32_t pre_buffer = 0;
+
+ AudioResampler dr(in_rate, out_rate, pre_buffer, testPrincipal);
+
+ AudioChunk stereoChunk =
+ CreateAudioChunk<float>(in_frames, 2, AUDIO_FORMAT_FLOAT32);
+ AudioChunk quadChunk =
+ CreateAudioChunk<float>(in_frames, 4, AUDIO_FORMAT_FLOAT32);
+
+ AudioSegment inSegment;
+ inSegment.AppendAndConsumeChunk(std::move(stereoChunk));
+ inSegment.AppendAndConsumeChunk(std::move(quadChunk));
+ dr.AppendInput(inSegment);
+
+ AudioSegment s = dr.Resample(out_frames);
+ EXPECT_EQ(s.GetDuration(), 0);
+ EXPECT_EQ(s.GetType(), MediaSegment::AUDIO);
+ EXPECT_TRUE(s.IsNull());
+ EXPECT_TRUE(s.IsEmpty());
+
+ AudioSegment s2 = dr.Resample(out_frames / 2);
+ EXPECT_EQ(s2.GetDuration(), out_frames / 2);
+ EXPECT_EQ(s2.GetType(), MediaSegment::AUDIO);
+ EXPECT_TRUE(!s2.IsNull());
+ EXPECT_TRUE(!s2.IsEmpty());
+ for (AudioSegment::ConstChunkIterator ci(s2); !ci.IsEnded(); ci.Next()) {
+ EXPECT_EQ(ci->mPrincipalHandle, testPrincipal);
+ }
+}
+
+TEST(TestAudioResampler, ChannelChange_QuadToStereo)
+{
+ const PrincipalHandle testPrincipal =
+ MakePrincipalHandle(nsContentUtils::GetSystemPrincipal());
+
+ uint32_t in_frames = 10;
+ uint32_t out_frames = 40;
+ // uint32_t channels = 2;
+ uint32_t in_rate = 24000;
+ uint32_t out_rate = 48000;
+
+ AudioResampler dr(in_rate, out_rate, 0, testPrincipal);
+
+ AudioChunk stereoChunk =
+ CreateAudioChunk<float>(in_frames, 2, AUDIO_FORMAT_FLOAT32);
+ AudioChunk quadChunk =
+ CreateAudioChunk<float>(in_frames, 4, AUDIO_FORMAT_FLOAT32);
+
+ AudioSegment inSegment;
+ inSegment.AppendAndConsumeChunk(std::move(quadChunk));
+ inSegment.AppendAndConsumeChunk(std::move(stereoChunk));
+ dr.AppendInput(inSegment);
+
+ AudioSegment s = dr.Resample(out_frames);
+ EXPECT_EQ(s.GetDuration(), 0);
+ EXPECT_EQ(s.GetType(), MediaSegment::AUDIO);
+ EXPECT_TRUE(s.IsNull());
+ EXPECT_TRUE(s.IsEmpty());
+
+ AudioSegment s2 = dr.Resample(out_frames / 2);
+ EXPECT_EQ(s2.GetDuration(), out_frames / 2);
+ EXPECT_EQ(s2.GetType(), MediaSegment::AUDIO);
+ EXPECT_TRUE(!s2.IsNull());
+ EXPECT_TRUE(!s2.IsEmpty());
+ for (AudioSegment::ConstChunkIterator ci(s2); !ci.IsEnded(); ci.Next()) {
+ EXPECT_EQ(ci->mPrincipalHandle, testPrincipal);
+ }
+}
+
+void printAudioSegment(const AudioSegment& segment);
+
+TEST(TestAudioResampler, ChannelChange_Discontinuity)
+{
+ const PrincipalHandle testPrincipal =
+ MakePrincipalHandle(nsContentUtils::GetSystemPrincipal());
+
+ uint32_t in_rate = 24000;
+ uint32_t out_rate = 48000;
+
+ const float amplitude = 0.5;
+ const float frequency = 200;
+ const float phase = 0.0;
+ float time = 0.0;
+ const float deltaTime = 1.0f / static_cast<float>(in_rate);
+
+ uint32_t in_frames = in_rate / 100;
+ uint32_t out_frames = out_rate / 100;
+ AudioResampler dr(in_rate, out_rate, 0, testPrincipal);
+
+ AudioChunk monoChunk =
+ CreateAudioChunk<float>(in_frames, 1, AUDIO_FORMAT_FLOAT32);
+ for (uint32_t i = 0; i < monoChunk.GetDuration(); ++i) {
+ double value = amplitude * sin(2 * M_PI * frequency * time + phase);
+ monoChunk.ChannelDataForWrite<float>(0)[i] = static_cast<float>(value);
+ time += deltaTime;
+ }
+ AudioChunk stereoChunk =
+ CreateAudioChunk<float>(in_frames, 2, AUDIO_FORMAT_FLOAT32);
+ for (uint32_t i = 0; i < stereoChunk.GetDuration(); ++i) {
+ double value = amplitude * sin(2 * M_PI * frequency * time + phase);
+ stereoChunk.ChannelDataForWrite<float>(0)[i] = static_cast<float>(value);
+ if (stereoChunk.ChannelCount() == 2) {
+ stereoChunk.ChannelDataForWrite<float>(1)[i] = value;
+ }
+ time += deltaTime;
+ }
+
+ AudioSegment inSegment;
+ inSegment.AppendAndConsumeChunk(std::move(stereoChunk));
+ // printAudioSegment(inSegment);
+
+ dr.AppendInput(inSegment);
+ AudioSegment s = dr.Resample(out_frames);
+ // printAudioSegment(s);
+
+ AudioSegment inSegment2;
+ inSegment2.AppendAndConsumeChunk(std::move(monoChunk));
+ // The resampler here is updated due to the channel change and that creates
+ // discontinuity.
+ dr.AppendInput(inSegment2);
+ AudioSegment s2 = dr.Resample(out_frames);
+ // printAudioSegment(s2);
+
+ EXPECT_EQ(s2.GetDuration(), 480);
+ EXPECT_EQ(s2.GetType(), MediaSegment::AUDIO);
+ EXPECT_TRUE(!s2.IsNull());
+ EXPECT_TRUE(!s2.IsEmpty());
+ EXPECT_EQ(s2.MaxChannelCount(), 1u);
+ for (AudioSegment::ConstChunkIterator ci(s2); !ci.IsEnded(); ci.Next()) {
+ EXPECT_EQ(ci->mPrincipalHandle, testPrincipal);
+ }
+}
+
+TEST(TestAudioResampler, ChannelChange_Discontinuity2)
+{
+ const PrincipalHandle testPrincipal =
+ MakePrincipalHandle(nsContentUtils::GetSystemPrincipal());
+
+ uint32_t in_rate = 24000;
+ uint32_t out_rate = 48000;
+
+ const float amplitude = 0.5;
+ const float frequency = 200;
+ const float phase = 0.0;
+ float time = 0.0;
+ const float deltaTime = 1.0f / static_cast<float>(in_rate);
+
+ uint32_t in_frames = in_rate / 100;
+ uint32_t out_frames = out_rate / 100;
+ AudioResampler dr(in_rate, out_rate, 10, testPrincipal);
+
+ AudioChunk monoChunk =
+ CreateAudioChunk<float>(in_frames / 2, 1, AUDIO_FORMAT_FLOAT32);
+ for (uint32_t i = 0; i < monoChunk.GetDuration(); ++i) {
+ double value = amplitude * sin(2 * M_PI * frequency * time + phase);
+ monoChunk.ChannelDataForWrite<float>(0)[i] = static_cast<float>(value);
+ time += deltaTime;
+ }
+ AudioChunk stereoChunk =
+ CreateAudioChunk<float>(in_frames / 2, 2, AUDIO_FORMAT_FLOAT32);
+ for (uint32_t i = 0; i < stereoChunk.GetDuration(); ++i) {
+ double value = amplitude * sin(2 * M_PI * frequency * time + phase);
+ stereoChunk.ChannelDataForWrite<float>(0)[i] = static_cast<float>(value);
+ if (stereoChunk.ChannelCount() == 2) {
+ stereoChunk.ChannelDataForWrite<float>(1)[i] = value;
+ }
+ time += deltaTime;
+ }
+
+ AudioSegment inSegment;
+ inSegment.AppendAndConsumeChunk(std::move(monoChunk));
+ inSegment.AppendAndConsumeChunk(std::move(stereoChunk));
+ // printAudioSegment(inSegment);
+
+ dr.AppendInput(inSegment);
+ AudioSegment s1 = dr.Resample(out_frames);
+ // printAudioSegment(s1);
+
+ EXPECT_EQ(s1.GetDuration(), 480);
+ EXPECT_EQ(s1.GetType(), MediaSegment::AUDIO);
+ EXPECT_TRUE(!s1.IsNull());
+ EXPECT_TRUE(!s1.IsEmpty());
+ EXPECT_EQ(s1.MaxChannelCount(), 2u);
+ for (AudioSegment::ConstChunkIterator ci(s1); !ci.IsEnded(); ci.Next()) {
+ EXPECT_EQ(ci->mPrincipalHandle, testPrincipal);
+ }
+
+ // The resampler here is updated due to the channel change and that creates
+ // discontinuity.
+ dr.AppendInput(inSegment);
+ AudioSegment s2 = dr.Resample(out_frames);
+ // printAudioSegment(s2);
+
+ EXPECT_EQ(s2.GetDuration(), 480);
+ EXPECT_EQ(s2.GetType(), MediaSegment::AUDIO);
+ EXPECT_TRUE(!s2.IsNull());
+ EXPECT_TRUE(!s2.IsEmpty());
+ EXPECT_EQ(s2.MaxChannelCount(), 2u);
+ for (AudioSegment::ConstChunkIterator ci(s2); !ci.IsEnded(); ci.Next()) {
+ EXPECT_EQ(ci->mPrincipalHandle, testPrincipal);
+ }
+}
+
+TEST(TestAudioResampler, ChannelChange_Discontinuity3)
+{
+ const PrincipalHandle testPrincipal =
+ MakePrincipalHandle(nsContentUtils::GetSystemPrincipal());
+
+ uint32_t in_rate = 48000;
+ uint32_t out_rate = 48000;
+
+ const float amplitude = 0.5;
+ const float frequency = 200;
+ const float phase = 0.0;
+ float time = 0.0;
+ const float deltaTime = 1.0f / static_cast<float>(in_rate);
+
+ uint32_t in_frames = in_rate / 100;
+ uint32_t out_frames = out_rate / 100;
+ AudioResampler dr(in_rate, out_rate, 10, testPrincipal);
+
+ AudioChunk stereoChunk =
+ CreateAudioChunk<float>(in_frames, 2, AUDIO_FORMAT_FLOAT32);
+ for (uint32_t i = 0; i < stereoChunk.GetDuration(); ++i) {
+ double value = amplitude * sin(2 * M_PI * frequency * time + phase);
+ stereoChunk.ChannelDataForWrite<float>(0)[i] = static_cast<float>(value);
+ if (stereoChunk.ChannelCount() == 2) {
+ stereoChunk.ChannelDataForWrite<float>(1)[i] = value;
+ }
+ time += deltaTime;
+ }
+
+ AudioSegment inSegment;
+ inSegment.AppendAndConsumeChunk(std::move(stereoChunk));
+ // printAudioSegment(inSegment);
+
+ dr.AppendInput(inSegment);
+ AudioSegment s = dr.Resample(out_frames);
+ // printAudioSegment(s);
+
+ // The resampler here is updated due to the rate change. This is because the
+ // in and out rate was the same so a pass through logice was used. By updating
+ // the out rate to something different than the in rate, the resampler will
+ // start being use dand discontinuity will exist.
+ dr.UpdateOutRate(out_rate + 100);
+ dr.AppendInput(inSegment);
+ AudioSegment s2 = dr.Resample(out_frames);
+ // printAudioSegment(s2);
+
+ EXPECT_EQ(s2.GetDuration(), 480);
+ EXPECT_EQ(s2.GetType(), MediaSegment::AUDIO);
+ EXPECT_TRUE(!s2.IsNull());
+ EXPECT_TRUE(!s2.IsEmpty());
+ EXPECT_EQ(s2.MaxChannelCount(), 2u);
+ for (AudioSegment::ConstChunkIterator ci(s2); !ci.IsEnded(); ci.Next()) {
+ EXPECT_EQ(ci->mPrincipalHandle, testPrincipal);
+ }
+}
diff --git a/dom/media/gtest/TestGMPCrossOrigin.cpp b/dom/media/gtest/TestGMPCrossOrigin.cpp
new file mode 100644
index 0000000000..8abb7694ac
--- /dev/null
+++ b/dom/media/gtest/TestGMPCrossOrigin.cpp
@@ -0,0 +1,212 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "mozilla/gtest/MozAssertions.h"
+#include "mozilla/StaticPtr.h"
+#include "GMPTestMonitor.h"
+#include "GMPVideoDecoderProxy.h"
+#include "GMPVideoEncoderProxy.h"
+#include "GMPServiceParent.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/DebugOnly.h"
+#include "nsThreadUtils.h"
+
+using namespace mozilla;
+using namespace mozilla::gmp;
+
+struct GMPTestRunner {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GMPTestRunner)
+
+ GMPTestRunner() = default;
+ void DoTest(void (GMPTestRunner::*aTestMethod)(GMPTestMonitor&));
+ void RunTestGMPTestCodec1(GMPTestMonitor& aMonitor);
+ void RunTestGMPTestCodec2(GMPTestMonitor& aMonitor);
+ void RunTestGMPTestCodec3(GMPTestMonitor& aMonitor);
+ void RunTestGMPCrossOrigin1(GMPTestMonitor& aMonitor);
+ void RunTestGMPCrossOrigin2(GMPTestMonitor& aMonitor);
+ void RunTestGMPCrossOrigin3(GMPTestMonitor& aMonitor);
+ void RunTestGMPCrossOrigin4(GMPTestMonitor& aMonitor);
+
+ private:
+ ~GMPTestRunner() = default;
+};
+
+template <class T, class Base,
+ nsresult (NS_STDCALL GeckoMediaPluginService::*Getter)(
+ GMPCrashHelper*, nsTArray<nsCString>*, const nsACString&,
+ UniquePtr<Base>&&)>
+class RunTestGMPVideoCodec : public Base {
+ public:
+ void Done(T* aGMP, GMPVideoHost* aHost) override {
+ EXPECT_TRUE(aGMP);
+ EXPECT_TRUE(aHost);
+ if (aGMP) {
+ aGMP->Close();
+ }
+ mMonitor.SetFinished();
+ }
+
+ static void Run(GMPTestMonitor& aMonitor, const nsCString& aOrigin) {
+ UniquePtr<GMPCallbackType> callback(new RunTestGMPVideoCodec(aMonitor));
+ Get(aOrigin, std::move(callback));
+ }
+
+ protected:
+ typedef T GMPCodecType;
+ typedef Base GMPCallbackType;
+
+ explicit RunTestGMPVideoCodec(GMPTestMonitor& aMonitor)
+ : mMonitor(aMonitor) {}
+
+ static nsresult Get(const nsACString& aNodeId, UniquePtr<Base>&& aCallback) {
+ nsTArray<nsCString> tags;
+ tags.AppendElement("h264"_ns);
+ tags.AppendElement("fake"_ns);
+
+ RefPtr<GeckoMediaPluginService> service =
+ GeckoMediaPluginService::GetGeckoMediaPluginService();
+ return ((*service).*Getter)(nullptr, &tags, aNodeId, std::move(aCallback));
+ }
+
+ GMPTestMonitor& mMonitor;
+};
+
+typedef RunTestGMPVideoCodec<GMPVideoDecoderProxy, GetGMPVideoDecoderCallback,
+ &GeckoMediaPluginService::GetGMPVideoDecoder>
+ RunTestGMPVideoDecoder;
+typedef RunTestGMPVideoCodec<GMPVideoEncoderProxy, GetGMPVideoEncoderCallback,
+ &GeckoMediaPluginService::GetGMPVideoEncoder>
+ RunTestGMPVideoEncoder;
+
+void GMPTestRunner::RunTestGMPTestCodec1(GMPTestMonitor& aMonitor) {
+ RunTestGMPVideoDecoder::Run(aMonitor, "o"_ns);
+}
+
+void GMPTestRunner::RunTestGMPTestCodec2(GMPTestMonitor& aMonitor) {
+ RunTestGMPVideoDecoder::Run(aMonitor, ""_ns);
+}
+
+void GMPTestRunner::RunTestGMPTestCodec3(GMPTestMonitor& aMonitor) {
+ RunTestGMPVideoEncoder::Run(aMonitor, ""_ns);
+}
+
+template <class Base>
+class RunTestGMPCrossOrigin : public Base {
+ public:
+ void Done(typename Base::GMPCodecType* aGMP, GMPVideoHost* aHost) override {
+ EXPECT_TRUE(aGMP);
+
+ UniquePtr<typename Base::GMPCallbackType> callback(
+ new Step2(Base::mMonitor, aGMP, mShouldBeEqual));
+ nsresult rv = Base::Get(mOrigin2, std::move(callback));
+ EXPECT_NS_SUCCEEDED(rv);
+ if (NS_FAILED(rv)) {
+ Base::mMonitor.SetFinished();
+ }
+ }
+
+ static void Run(GMPTestMonitor& aMonitor, const nsCString& aOrigin1,
+ const nsCString& aOrigin2) {
+ UniquePtr<typename Base::GMPCallbackType> callback(
+ new RunTestGMPCrossOrigin<Base>(aMonitor, aOrigin1, aOrigin2));
+ nsresult rv = Base::Get(aOrigin1, std::move(callback));
+ EXPECT_NS_SUCCEEDED(rv);
+ if (NS_FAILED(rv)) {
+ aMonitor.SetFinished();
+ }
+ }
+
+ private:
+ RunTestGMPCrossOrigin(GMPTestMonitor& aMonitor, const nsCString& aOrigin1,
+ const nsCString& aOrigin2)
+ : Base(aMonitor),
+ mGMP(nullptr),
+ mOrigin2(aOrigin2),
+ mShouldBeEqual(aOrigin1.Equals(aOrigin2)) {}
+
+ class Step2 : public Base {
+ public:
+ Step2(GMPTestMonitor& aMonitor, typename Base::GMPCodecType* aGMP,
+ bool aShouldBeEqual)
+ : Base(aMonitor), mGMP(aGMP), mShouldBeEqual(aShouldBeEqual) {}
+ void Done(typename Base::GMPCodecType* aGMP, GMPVideoHost* aHost) override {
+ EXPECT_TRUE(aGMP);
+ if (aGMP) {
+ EXPECT_TRUE(mGMP && (mGMP->GetPluginId() == aGMP->GetPluginId()) ==
+ mShouldBeEqual);
+ }
+ if (mGMP) {
+ mGMP->Close();
+ }
+ Base::Done(aGMP, aHost);
+ }
+
+ private:
+ typename Base::GMPCodecType* mGMP;
+ bool mShouldBeEqual;
+ };
+
+ typename Base::GMPCodecType* mGMP;
+ nsCString mOrigin2;
+ bool mShouldBeEqual;
+};
+
+typedef RunTestGMPCrossOrigin<RunTestGMPVideoDecoder>
+ RunTestGMPVideoDecoderCrossOrigin;
+typedef RunTestGMPCrossOrigin<RunTestGMPVideoEncoder>
+ RunTestGMPVideoEncoderCrossOrigin;
+
+void GMPTestRunner::RunTestGMPCrossOrigin1(GMPTestMonitor& aMonitor) {
+ RunTestGMPVideoDecoderCrossOrigin::Run(aMonitor, "origin1"_ns, "origin2"_ns);
+}
+
+void GMPTestRunner::RunTestGMPCrossOrigin2(GMPTestMonitor& aMonitor) {
+ RunTestGMPVideoEncoderCrossOrigin::Run(aMonitor, "origin1"_ns, "origin2"_ns);
+}
+
+void GMPTestRunner::RunTestGMPCrossOrigin3(GMPTestMonitor& aMonitor) {
+ RunTestGMPVideoDecoderCrossOrigin::Run(aMonitor, "origin1"_ns, "origin1"_ns);
+}
+
+void GMPTestRunner::RunTestGMPCrossOrigin4(GMPTestMonitor& aMonitor) {
+ RunTestGMPVideoEncoderCrossOrigin::Run(aMonitor, "origin1"_ns, "origin1"_ns);
+}
+
+void GMPTestRunner::DoTest(
+ void (GMPTestRunner::*aTestMethod)(GMPTestMonitor&)) {
+ RefPtr<GeckoMediaPluginService> service =
+ GeckoMediaPluginService::GetGeckoMediaPluginService();
+ nsCOMPtr<nsIThread> thread;
+ EXPECT_NS_SUCCEEDED(service->GetThread(getter_AddRefs(thread)));
+
+ GMPTestMonitor monitor;
+ thread->Dispatch(NewRunnableMethod<GMPTestMonitor&>(
+ "GMPTestRunner::DoTest", this, aTestMethod, monitor),
+ NS_DISPATCH_NORMAL);
+ monitor.AwaitFinished();
+}
+
+// Bug 1776767 - Skip all GMP tests on Windows ASAN
+#if !(defined(XP_WIN) && defined(MOZ_ASAN))
+TEST(GeckoMediaPlugins, GMPTestCodec)
+{
+ RefPtr<GMPTestRunner> runner = new GMPTestRunner();
+ runner->DoTest(&GMPTestRunner::RunTestGMPTestCodec1);
+ runner->DoTest(&GMPTestRunner::RunTestGMPTestCodec2);
+ runner->DoTest(&GMPTestRunner::RunTestGMPTestCodec3);
+}
+
+TEST(GeckoMediaPlugins, GMPCrossOrigin)
+{
+ RefPtr<GMPTestRunner> runner = new GMPTestRunner();
+ runner->DoTest(&GMPTestRunner::RunTestGMPCrossOrigin1);
+ runner->DoTest(&GMPTestRunner::RunTestGMPCrossOrigin2);
+ runner->DoTest(&GMPTestRunner::RunTestGMPCrossOrigin3);
+ runner->DoTest(&GMPTestRunner::RunTestGMPCrossOrigin4);
+}
+#endif // !(defined(XP_WIN) && defined(MOZ_ASAN))
diff --git a/dom/media/gtest/TestGMPRemoveAndDelete.cpp b/dom/media/gtest/TestGMPRemoveAndDelete.cpp
new file mode 100644
index 0000000000..b969027c6e
--- /dev/null
+++ b/dom/media/gtest/TestGMPRemoveAndDelete.cpp
@@ -0,0 +1,472 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GMPService.h"
+#include "GMPServiceParent.h"
+#include "GMPTestMonitor.h"
+#include "GMPUtils.h"
+#include "GMPVideoDecoderProxy.h"
+#include "gmp-api/gmp-video-host.h"
+#include "gtest/gtest.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPtr.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsIObserverService.h"
+
+#define GMP_DIR_NAME u"gmp-fakeopenh264"_ns
+#define GMP_OLD_VERSION u"1.0"_ns
+#define GMP_NEW_VERSION u"1.1"_ns
+
+#define GMP_DELETED_TOPIC "gmp-directory-deleted"
+
+#define EXPECT_OK(X) EXPECT_TRUE(NS_SUCCEEDED(X))
+
+using namespace mozilla;
+using namespace mozilla::gmp;
+
+class GMPRemoveTest : public nsIObserver, public GMPVideoDecoderCallbackProxy {
+ public:
+ GMPRemoveTest();
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ // Called when a GMP plugin directory has been successfully deleted.
+ // |aData| will contain the directory path.
+ NS_IMETHOD Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) override;
+
+ // Create a new GMP plugin directory that we can trash and add it to the GMP
+ // service. Remove the original plugin directory. Original plugin directory
+ // gets re-added at destruction.
+ void Setup();
+
+ bool CreateVideoDecoder(nsCString aNodeId = ""_ns);
+ void CloseVideoDecoder();
+
+ void DeletePluginDirectory(bool aCanDefer);
+
+ // Decode a dummy frame.
+ GMPErr Decode();
+
+ // Wait until TestMonitor has been signaled.
+ void Wait();
+
+ // Did we get a Terminated() callback from the plugin?
+ bool IsTerminated();
+
+ // From GMPVideoDecoderCallbackProxy
+ // Set mDecodeResult; unblock TestMonitor.
+ virtual void Decoded(GMPVideoi420Frame* aDecodedFrame) override;
+ virtual void Error(GMPErr aError) override;
+
+ // From GMPVideoDecoderCallbackProxy
+ // We expect this to be called when a plugin has been forcibly closed.
+ virtual void Terminated() override;
+
+ // Ignored GMPVideoDecoderCallbackProxy members
+ virtual void ReceivedDecodedReferenceFrame(
+ const uint64_t aPictureId) override {}
+ virtual void ReceivedDecodedFrame(const uint64_t aPictureId) override {}
+ virtual void InputDataExhausted() override {}
+ virtual void DrainComplete() override {}
+ virtual void ResetComplete() override {}
+
+ private:
+ virtual ~GMPRemoveTest();
+
+ void gmp_Decode();
+ void gmp_GetVideoDecoder(nsCString aNodeId,
+ GMPVideoDecoderProxy** aOutDecoder,
+ GMPVideoHost** aOutHost);
+ void GeneratePlugin();
+
+ GMPTestMonitor mTestMonitor;
+ nsCOMPtr<nsIThread> mGMPThread;
+
+ bool mIsTerminated;
+
+ // Path to the cloned GMP we have created.
+ nsString mTmpPath;
+ nsCOMPtr<nsIFile> mTmpDir;
+
+ // Path to the original GMP. Store so that we can re-add it after we're done
+ // testing.
+ nsString mOriginalPath;
+
+ GMPVideoDecoderProxy* mDecoder;
+ GMPVideoHost* mHost;
+ GMPErr mDecodeResult;
+};
+
+/*
+ * Simple test that the plugin is deleted when forcibly removed and deleted.
+ */
+TEST(GeckoMediaPlugins, RemoveAndDeleteForcedSimple)
+{
+ RefPtr<GMPRemoveTest> test(new GMPRemoveTest());
+
+ test->Setup();
+ test->DeletePluginDirectory(false /* force immediate */);
+ test->Wait();
+}
+
+/*
+ * Simple test that the plugin is deleted when deferred deletion is allowed.
+ */
+TEST(GeckoMediaPlugins, RemoveAndDeleteDeferredSimple)
+{
+ RefPtr<GMPRemoveTest> test(new GMPRemoveTest());
+
+ test->Setup();
+ test->DeletePluginDirectory(true /* can defer */);
+ test->Wait();
+}
+
+/*
+ * Test that the plugin is unavailable immediately after a forced
+ * RemoveAndDelete, and that the plugin is deleted afterwards.
+ */
+// Bug 1115253 - disable test in win64 to reduce failure rate
+#if !defined(_WIN64)
+TEST(GeckoMediaPlugins, RemoveAndDeleteForcedInUse)
+{
+ RefPtr<GMPRemoveTest> test(new GMPRemoveTest());
+
+ test->Setup();
+ EXPECT_TRUE(test->CreateVideoDecoder("thisOrigin"_ns));
+
+ // Test that we can decode a frame.
+ GMPErr err = test->Decode();
+ EXPECT_EQ(err, GMPNoErr);
+
+ test->DeletePluginDirectory(false /* force immediate */);
+ test->Wait();
+
+ // Test that the VideoDecoder is no longer available.
+ EXPECT_FALSE(test->CreateVideoDecoder("thisOrigin"_ns));
+
+ // Test that we were notified of the plugin's destruction.
+ EXPECT_TRUE(test->IsTerminated());
+}
+
+/*
+ * Test that the plugin is still usable after a deferred RemoveAndDelete, and
+ * that the plugin is deleted afterwards.
+ */
+TEST(GeckoMediaPlugins, RemoveAndDeleteDeferredInUse)
+{
+ RefPtr<GMPRemoveTest> test(new GMPRemoveTest());
+
+ test->Setup();
+ EXPECT_TRUE(test->CreateVideoDecoder("thisOrigin"_ns));
+
+ // Make sure decoding works before we do anything.
+ GMPErr err = test->Decode();
+ EXPECT_EQ(err, GMPNoErr);
+
+ test->DeletePluginDirectory(true /* can defer */);
+
+ // Test that decoding still works.
+ err = test->Decode();
+ EXPECT_EQ(err, GMPNoErr);
+
+ // Test that this origin is still able to fetch the video decoder.
+ EXPECT_TRUE(test->CreateVideoDecoder("thisOrigin"_ns));
+
+ test->CloseVideoDecoder();
+ test->Wait();
+}
+#endif
+
+static StaticRefPtr<GeckoMediaPluginService> gService;
+static StaticRefPtr<GeckoMediaPluginServiceParent> gServiceParent;
+
+static GeckoMediaPluginService* GetService() {
+ if (!gService) {
+ RefPtr<GeckoMediaPluginService> service =
+ GeckoMediaPluginService::GetGeckoMediaPluginService();
+ gService = service;
+ }
+
+ return gService.get();
+}
+
+static GeckoMediaPluginServiceParent* GetServiceParent() {
+ if (!gServiceParent) {
+ RefPtr<GeckoMediaPluginServiceParent> parent =
+ GeckoMediaPluginServiceParent::GetSingleton();
+ gServiceParent = parent;
+ }
+
+ return gServiceParent.get();
+}
+
+NS_IMPL_ISUPPORTS(GMPRemoveTest, nsIObserver)
+
+GMPRemoveTest::GMPRemoveTest()
+ : mIsTerminated(false), mDecoder(nullptr), mHost(nullptr) {}
+
+GMPRemoveTest::~GMPRemoveTest() {
+ bool exists;
+ EXPECT_TRUE(NS_SUCCEEDED(mTmpDir->Exists(&exists)) && !exists);
+
+ EXPECT_OK(GetServiceParent()->AddPluginDirectory(mOriginalPath));
+}
+
+void GMPRemoveTest::Setup() {
+ GeneratePlugin();
+ GetService()->GetThread(getter_AddRefs(mGMPThread));
+
+ // Spin the event loop until the GMP service has had a chance to complete
+ // adding GMPs from MOZ_GMP_PATH. Otherwise, the RemovePluginDirectory()
+ // below may complete before we're finished adding GMPs from MOZ_GMP_PATH,
+ // and we'll end up not removing the GMP, and the test will fail.
+ nsCOMPtr<nsISerialEventTarget> thread(GetServiceParent()->GetGMPThread());
+ EXPECT_TRUE(thread);
+ GMPTestMonitor* mon = &mTestMonitor;
+ GetServiceParent()->EnsureInitialized()->Then(
+ thread, __func__, [mon]() { mon->SetFinished(); },
+ [mon]() { mon->SetFinished(); });
+ mTestMonitor.AwaitFinished();
+
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ obs->AddObserver(this, GMP_DELETED_TOPIC, false /* strong ref */);
+ EXPECT_OK(GetServiceParent()->RemovePluginDirectory(mOriginalPath));
+
+ GetServiceParent()->AsyncAddPluginDirectory(mTmpPath)->Then(
+ thread, __func__, [mon]() { mon->SetFinished(); },
+ [mon]() { mon->SetFinished(); });
+ mTestMonitor.AwaitFinished();
+}
+
+bool GMPRemoveTest::CreateVideoDecoder(nsCString aNodeId) {
+ GMPVideoHost* host;
+ GMPVideoDecoderProxy* decoder = nullptr;
+
+ mGMPThread->Dispatch(
+ NewNonOwningRunnableMethod<nsCString, GMPVideoDecoderProxy**,
+ GMPVideoHost**>(
+ "GMPRemoveTest::gmp_GetVideoDecoder", this,
+ &GMPRemoveTest::gmp_GetVideoDecoder, aNodeId, &decoder, &host),
+ NS_DISPATCH_NORMAL);
+
+ mTestMonitor.AwaitFinished();
+
+ if (!decoder) {
+ return false;
+ }
+
+ GMPVideoCodec codec;
+ memset(&codec, 0, sizeof(codec));
+ codec.mGMPApiVersion = 33;
+
+ nsTArray<uint8_t> empty;
+ NS_DispatchAndSpinEventLoopUntilComplete(
+ "GMPVideoDecoderProxy::InitDecode"_ns, mGMPThread,
+ NewNonOwningRunnableMethod<const GMPVideoCodec&, const nsTArray<uint8_t>&,
+ GMPVideoDecoderCallbackProxy*, int32_t>(
+ "GMPVideoDecoderProxy::InitDecode", decoder,
+ &GMPVideoDecoderProxy::InitDecode, codec, empty, this,
+ 1 /* core count */));
+
+ if (mDecoder) {
+ CloseVideoDecoder();
+ }
+
+ mDecoder = decoder;
+ mHost = host;
+
+ return true;
+}
+
+void GMPRemoveTest::gmp_GetVideoDecoder(nsCString aNodeId,
+ GMPVideoDecoderProxy** aOutDecoder,
+ GMPVideoHost** aOutHost) {
+ nsTArray<nsCString> tags;
+ tags.AppendElement("h264"_ns);
+ tags.AppendElement("fake"_ns);
+
+ class Callback : public GetGMPVideoDecoderCallback {
+ public:
+ Callback(GMPTestMonitor* aMonitor, GMPVideoDecoderProxy** aDecoder,
+ GMPVideoHost** aHost)
+ : mMonitor(aMonitor), mDecoder(aDecoder), mHost(aHost) {}
+ virtual void Done(GMPVideoDecoderProxy* aDecoder,
+ GMPVideoHost* aHost) override {
+ *mDecoder = aDecoder;
+ *mHost = aHost;
+ mMonitor->SetFinished();
+ }
+
+ private:
+ GMPTestMonitor* mMonitor;
+ GMPVideoDecoderProxy** mDecoder;
+ GMPVideoHost** mHost;
+ };
+
+ UniquePtr<GetGMPVideoDecoderCallback> cb(
+ new Callback(&mTestMonitor, aOutDecoder, aOutHost));
+
+ if (NS_FAILED(GetService()->GetGMPVideoDecoder(nullptr, &tags, aNodeId,
+ std::move(cb)))) {
+ mTestMonitor.SetFinished();
+ }
+}
+
+void GMPRemoveTest::CloseVideoDecoder() {
+ NS_DispatchAndSpinEventLoopUntilComplete(
+ "GMPVideoDecoderProxy::Close"_ns, mGMPThread,
+ NewNonOwningRunnableMethod("GMPVideoDecoderProxy::Close", mDecoder,
+ &GMPVideoDecoderProxy::Close));
+
+ mDecoder = nullptr;
+ mHost = nullptr;
+}
+
+void GMPRemoveTest::DeletePluginDirectory(bool aCanDefer) {
+ GetServiceParent()->RemoveAndDeletePluginDirectory(mTmpPath, aCanDefer);
+}
+
+GMPErr GMPRemoveTest::Decode() {
+ mGMPThread->Dispatch(
+ NewNonOwningRunnableMethod("GMPRemoveTest::gmp_Decode", this,
+ &GMPRemoveTest::gmp_Decode),
+ NS_DISPATCH_NORMAL);
+
+ mTestMonitor.AwaitFinished();
+ return mDecodeResult;
+}
+
+void GMPRemoveTest::gmp_Decode() {
+// from gmp-fake.cpp
+#pragma pack(push, 1)
+ struct EncodedFrame {
+ struct SPSNalu {
+ uint32_t size_;
+ uint8_t payload[14];
+ } sps_nalu;
+ struct PPSNalu {
+ uint32_t size_;
+ uint8_t payload[4];
+ } pps_nalu;
+ struct IDRNalu {
+ uint32_t size_;
+ uint8_t h264_compat_;
+ uint32_t magic_;
+ uint32_t width_;
+ uint32_t height_;
+ uint8_t y_;
+ uint8_t u_;
+ uint8_t v_;
+ uint32_t timestamp_;
+ } idr_nalu;
+ };
+#pragma pack(pop)
+
+ GMPVideoFrame* absFrame;
+ GMPErr err = mHost->CreateFrame(kGMPEncodedVideoFrame, &absFrame);
+ EXPECT_EQ(err, GMPNoErr);
+
+ GMPUniquePtr<GMPVideoEncodedFrame> frame(
+ static_cast<GMPVideoEncodedFrame*>(absFrame));
+ err = frame->CreateEmptyFrame(sizeof(EncodedFrame) /* size */);
+ EXPECT_EQ(err, GMPNoErr);
+
+ EncodedFrame* frameData = reinterpret_cast<EncodedFrame*>(frame->Buffer());
+ frameData->sps_nalu.size_ = sizeof(EncodedFrame::SPSNalu) - sizeof(uint32_t);
+ frameData->pps_nalu.size_ = sizeof(EncodedFrame::PPSNalu) - sizeof(uint32_t);
+ frameData->idr_nalu.size_ = sizeof(EncodedFrame::IDRNalu) - sizeof(uint32_t);
+ frameData->idr_nalu.h264_compat_ = 5;
+ frameData->idr_nalu.magic_ = 0x004000b8;
+ frameData->idr_nalu.width_ = frameData->idr_nalu.height_ = 16;
+
+ nsTArray<uint8_t> empty;
+ nsresult rv =
+ mDecoder->Decode(std::move(frame), false /* aMissingFrames */, empty);
+ EXPECT_OK(rv);
+}
+
+void GMPRemoveTest::Wait() { mTestMonitor.AwaitFinished(); }
+
+bool GMPRemoveTest::IsTerminated() { return mIsTerminated; }
+
+// nsIObserver
+NS_IMETHODIMP
+GMPRemoveTest::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ EXPECT_TRUE(!strcmp(GMP_DELETED_TOPIC, aTopic));
+
+ nsString data(aData);
+ if (mTmpPath.Equals(data)) {
+ mTestMonitor.SetFinished();
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ obs->RemoveObserver(this, GMP_DELETED_TOPIC);
+ }
+
+ return NS_OK;
+}
+
+// GMPVideoDecoderCallbackProxy
+void GMPRemoveTest::Decoded(GMPVideoi420Frame* aDecodedFrame) {
+ aDecodedFrame->Destroy();
+ mDecodeResult = GMPNoErr;
+ mTestMonitor.SetFinished();
+}
+
+// GMPVideoDecoderCallbackProxy
+void GMPRemoveTest::Error(GMPErr aError) {
+ mDecodeResult = aError;
+ mTestMonitor.SetFinished();
+}
+
+// GMPVideoDecoderCallbackProxy
+void GMPRemoveTest::Terminated() {
+ mIsTerminated = true;
+ if (mDecoder) {
+ mDecoder->Close();
+ mDecoder = nullptr;
+ }
+}
+
+void GMPRemoveTest::GeneratePlugin() {
+ nsresult rv;
+ nsCOMPtr<nsIFile> gmpDir;
+ nsCOMPtr<nsIFile> origDir;
+ nsCOMPtr<nsIFile> tmpDir;
+
+ rv = NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(gmpDir));
+ EXPECT_OK(rv);
+ rv = gmpDir->Append(GMP_DIR_NAME);
+ EXPECT_OK(rv);
+
+ rv = gmpDir->Clone(getter_AddRefs(origDir));
+ EXPECT_OK(rv);
+ rv = origDir->Append(GMP_OLD_VERSION);
+ EXPECT_OK(rv);
+
+ rv = gmpDir->Clone(getter_AddRefs(tmpDir));
+ EXPECT_OK(rv);
+ rv = tmpDir->Append(GMP_NEW_VERSION);
+ EXPECT_OK(rv);
+ bool exists = false;
+ rv = tmpDir->Exists(&exists);
+ EXPECT_OK(rv);
+ if (exists) {
+ rv = tmpDir->Remove(true);
+ EXPECT_OK(rv);
+ }
+ rv = origDir->CopyTo(gmpDir, GMP_NEW_VERSION);
+ EXPECT_OK(rv);
+
+ rv = gmpDir->Clone(getter_AddRefs(tmpDir));
+ EXPECT_OK(rv);
+ rv = tmpDir->Append(GMP_NEW_VERSION);
+ EXPECT_OK(rv);
+
+ EXPECT_OK(origDir->GetPath(mOriginalPath));
+ EXPECT_OK(tmpDir->GetPath(mTmpPath));
+ mTmpDir = tmpDir;
+}
diff --git a/dom/media/gtest/TestGMPUtils.cpp b/dom/media/gtest/TestGMPUtils.cpp
new file mode 100644
index 0000000000..589b47b581
--- /dev/null
+++ b/dom/media/gtest/TestGMPUtils.cpp
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "GMPUtils.h"
+#include "mozilla/ArrayUtils.h"
+#include "nsString.h"
+
+#include <string>
+#include <vector>
+
+using namespace mozilla;
+
+void TestSplitAt(const char* aInput, const char* aDelims,
+ size_t aNumExpectedTokens, const char* aExpectedTokens[]) {
+ nsCString input(aInput);
+ nsTArray<nsCString> tokens;
+ SplitAt(aDelims, input, tokens);
+ EXPECT_EQ(tokens.Length(), aNumExpectedTokens)
+ << "Should get expected number of tokens";
+ for (size_t i = 0; i < tokens.Length(); i++) {
+ EXPECT_TRUE(tokens[i].EqualsASCII(aExpectedTokens[i]))
+ << "Tokenize fail; expected=" << aExpectedTokens[i]
+ << " got=" << tokens[i].BeginReading();
+ }
+}
+
+TEST(GeckoMediaPlugins, TestSplitAt)
+{
+ {
+ const char* input = "1,2,3,4";
+ const char* delims = ",";
+ const char* tokens[] = {"1", "2", "3", "4"};
+ TestSplitAt(input, delims, MOZ_ARRAY_LENGTH(tokens), tokens);
+ }
+
+ {
+ const char* input = "a simple, comma, seperated, list";
+ const char* delims = ",";
+ const char* tokens[] = {"a simple", " comma", " seperated", " list"};
+ TestSplitAt(input, delims, MOZ_ARRAY_LENGTH(tokens), tokens);
+ }
+
+ {
+ const char* input = // Various platform line endings...
+ "line1\r\n" // Windows
+ "line2\r" // Old MacOSX
+ "line3\n" // Unix
+ "line4";
+ const char* delims = "\r\n";
+ const char* tokens[] = {"line1", "line2", "line3", "line4"};
+ TestSplitAt(input, delims, MOZ_ARRAY_LENGTH(tokens), tokens);
+ }
+}
+
+TEST(GeckoMediaPlugins, ToHexString)
+{
+ struct Test {
+ nsTArray<uint8_t> bytes;
+ std::string hex;
+ };
+
+ static const Test tests[] = {
+ {{0x00, 0x00}, "0000"},
+ {{0xff, 0xff}, "ffff"},
+ {{0xff, 0x00}, "ff00"},
+ {{0x00, 0xff}, "00ff"},
+ {{0xf0, 0x10}, "f010"},
+ {{0x05, 0x50}, "0550"},
+ {{0xf}, "0f"},
+ {{0x10}, "10"},
+ {{}, ""},
+ {{0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88, 0x99, 0xaa, 0xbb,
+ 0xcc, 0xdd, 0xee, 0xff},
+ "00112233445566778899aabbccddeeff"},
+ };
+
+ for (const Test& test : tests) {
+ EXPECT_STREQ(test.hex.c_str(), ToHexString(test.bytes).get());
+ }
+}
diff --git a/dom/media/gtest/TestGroupId.cpp b/dom/media/gtest/TestGroupId.cpp
new file mode 100644
index 0000000000..efd0bae20a
--- /dev/null
+++ b/dom/media/gtest/TestGroupId.cpp
@@ -0,0 +1,302 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioDeviceInfo.h"
+#include "MediaManager.h"
+#include "gmock/gmock.h"
+#include "gtest/gtest-printers.h"
+#include "gtest/gtest.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/UniquePtr.h"
+#include "nsTArray.h"
+#include "webrtc/MediaEngineFake.h"
+
+using ::testing::Return;
+using namespace mozilla;
+
+void PrintTo(const nsString& aValue, ::std::ostream* aStream) {
+ NS_ConvertUTF16toUTF8 str(aValue);
+ (*aStream) << str.get();
+}
+void PrintTo(const nsCString& aValue, ::std::ostream* aStream) {
+ (*aStream) << aValue.get();
+}
+
+RefPtr<AudioDeviceInfo> MakeAudioDeviceInfo(const nsAString& aName,
+ const nsAString& aGroupId,
+ uint16_t aType) {
+ return MakeRefPtr<AudioDeviceInfo>(
+ nullptr, aName, aGroupId, u"Vendor"_ns, aType,
+ AudioDeviceInfo::STATE_ENABLED, AudioDeviceInfo::PREF_NONE,
+ AudioDeviceInfo::FMT_F32LE, AudioDeviceInfo::FMT_F32LE, 2u, 44100u,
+ 44100u, 44100u, 0, 0);
+}
+
+RefPtr<MediaDevice> MakeCameraDevice(const nsString& aName,
+ const nsString& aGroupId) {
+ return new MediaDevice(new MediaEngineFake(), dom::MediaSourceEnum::Camera,
+ aName, u""_ns, aGroupId, MediaDevice::IsScary::No,
+ MediaDevice::OsPromptable::No);
+}
+
+RefPtr<MediaDevice> MakeMicDevice(const nsString& aName,
+ const nsString& aGroupId) {
+ return new MediaDevice(
+ new MediaEngineFake(),
+ MakeAudioDeviceInfo(aName, aGroupId, AudioDeviceInfo::TYPE_INPUT),
+ u""_ns);
+}
+
+RefPtr<MediaDevice> MakeSpeakerDevice(const nsString& aName,
+ const nsString& aGroupId) {
+ return new MediaDevice(
+ new MediaEngineFake(),
+ MakeAudioDeviceInfo(aName, aGroupId, AudioDeviceInfo::TYPE_OUTPUT),
+ u"ID"_ns);
+}
+
+/* Verify that when an audio input device name contains the video input device
+ * name the video device group id is updated to become equal to the audio
+ * device group id. */
+TEST(TestGroupId, MatchInput_PartOfName)
+{
+ MediaManager::MediaDeviceSet devices;
+ MediaManager::MediaDeviceSet audios;
+
+ devices.AppendElement(
+ MakeCameraDevice(u"Vendor Model"_ns, u"Cam-Model-GroupId"_ns));
+
+ auto mic =
+ MakeMicDevice(u"Vendor Model Analog Stereo"_ns, u"Mic-Model-GroupId"_ns);
+ devices.AppendElement(mic);
+ audios.AppendElement(mic);
+
+ MediaManager::GuessVideoDeviceGroupIDs(devices, audios);
+
+ EXPECT_EQ(devices[0]->mRawGroupID, devices[1]->mRawGroupID)
+ << "Video group id is the same as audio input group id.";
+}
+
+/* Verify that when an audio input device name is the same as the video input
+ * device name the video device group id is updated to become equal to the audio
+ * device group id. */
+TEST(TestGroupId, MatchInput_FullName)
+{
+ MediaManager::MediaDeviceSet devices;
+ MediaManager::MediaDeviceSet audios;
+
+ devices.AppendElement(
+ MakeCameraDevice(u"Vendor Model"_ns, u"Cam-Model-GroupId"_ns));
+
+ auto mic = MakeMicDevice(u"Vendor Model"_ns, u"Mic-Model-GroupId"_ns);
+ devices.AppendElement(mic);
+ audios.AppendElement(mic);
+
+ MediaManager::GuessVideoDeviceGroupIDs(devices, audios);
+
+ EXPECT_EQ(devices[0]->mRawGroupID, devices[1]->mRawGroupID)
+ << "Video group id is the same as audio input group id.";
+}
+
+/* Verify that when an audio input device name does not contain the video input
+ * device name the video device group id does not change. */
+TEST(TestGroupId, NoMatchInput)
+{
+ MediaManager::MediaDeviceSet devices;
+ MediaManager::MediaDeviceSet audios;
+
+ nsString Cam_Model_GroupId = u"Cam-Model-GroupId"_ns;
+ devices.AppendElement(
+ MakeCameraDevice(u"Vendor Model"_ns, Cam_Model_GroupId));
+
+ audios.AppendElement(
+ MakeMicDevice(u"Model Analog Stereo"_ns, u"Mic-Model-GroupId"_ns));
+
+ MediaManager::GuessVideoDeviceGroupIDs(devices, audios);
+
+ EXPECT_EQ(devices[0]->mRawGroupID, Cam_Model_GroupId)
+ << "Video group id has not been updated.";
+ EXPECT_NE(devices[0]->mRawGroupID, audios[0]->mRawGroupID)
+ << "Video group id is different than audio input group id.";
+}
+
+/* Verify that when more that one audio input and more than one audio output
+ * device name contain the video input device name the video device group id
+ * does not change. */
+TEST(TestGroupId, NoMatch_TwoIdenticalDevices)
+{
+ MediaManager::MediaDeviceSet devices;
+ MediaManager::MediaDeviceSet audios;
+
+ nsString Cam_Model_GroupId = u"Cam-Model-GroupId"_ns;
+ devices.AppendElement(
+ MakeCameraDevice(u"Vendor Model"_ns, Cam_Model_GroupId));
+
+ audios.AppendElement(
+ MakeMicDevice(u"Vendor Model Analog Stereo"_ns, u"Mic-Model-GroupId"_ns));
+ audios.AppendElement(
+ MakeMicDevice(u"Vendor Model Analog Stereo"_ns, u"Mic-Model-GroupId"_ns));
+
+ audios.AppendElement(MakeSpeakerDevice(u"Vendor Model Analog Stereo"_ns,
+ u"Speaker-Model-GroupId"_ns));
+ audios.AppendElement(MakeSpeakerDevice(u"Vendor Model Analog Stereo"_ns,
+ u"Speaker-Model-GroupId"_ns));
+
+ MediaManager::GuessVideoDeviceGroupIDs(devices, audios);
+
+ EXPECT_EQ(devices[0]->mRawGroupID, Cam_Model_GroupId)
+ << "Video group id has not been updated.";
+ EXPECT_NE(devices[0]->mRawGroupID, audios[0]->mRawGroupID)
+ << "Video group id is different from audio input group id.";
+ EXPECT_NE(devices[0]->mRawGroupID, audios[2]->mRawGroupID)
+ << "Video group id is different from audio output group id.";
+}
+
+/* Verify that when more that one audio input device name contain the video
+ * input device name the video device group id is not updated by audio input
+ * device group id but it continues looking at audio output devices where it
+ * finds a match so video input group id is updated by audio output group id. */
+TEST(TestGroupId, Match_TwoIdenticalInputsMatchOutput)
+{
+ MediaManager::MediaDeviceSet devices;
+ MediaManager::MediaDeviceSet audios;
+
+ nsString Cam_Model_GroupId = u"Cam-Model-GroupId"_ns;
+ devices.AppendElement(
+ MakeCameraDevice(u"Vendor Model"_ns, Cam_Model_GroupId));
+
+ audios.AppendElement(
+ MakeMicDevice(u"Vendor Model Analog Stereo"_ns, u"Mic-Model-GroupId"_ns));
+ audios.AppendElement(
+ MakeMicDevice(u"Vendor Model Analog Stereo"_ns, u"Mic-Model-GroupId"_ns));
+
+ audios.AppendElement(MakeSpeakerDevice(u"Vendor Model Analog Stereo"_ns,
+ u"Speaker-Model-GroupId"_ns));
+
+ MediaManager::GuessVideoDeviceGroupIDs(devices, audios);
+
+ EXPECT_EQ(devices[0]->mRawGroupID, audios[2]->mRawGroupID)
+ << "Video group id is the same as audio output group id.";
+}
+
+/* Verify that when more that one audio input and more than one audio output
+ * device names contain the video input device name the video device group id
+ * does not change. */
+TEST(TestGroupId, NoMatch_ThreeIdenticalDevices)
+{
+ MediaManager::MediaDeviceSet devices;
+ MediaManager::MediaDeviceSet audios;
+
+ nsString Cam_Model_GroupId = u"Cam-Model-GroupId"_ns;
+ devices.AppendElement(
+ MakeCameraDevice(u"Vendor Model"_ns, Cam_Model_GroupId));
+
+ audios.AppendElement(
+ MakeMicDevice(u"Vendor Model Analog Stereo"_ns, u"Mic-Model-GroupId"_ns));
+ audios.AppendElement(
+ MakeMicDevice(u"Vendor Model Analog Stereo"_ns, u"Mic-Model-GroupId"_ns));
+ audios.AppendElement(
+ MakeMicDevice(u"Vendor Model Analog Stereo"_ns, u"Mic-Model-GroupId"_ns));
+
+ audios.AppendElement(MakeSpeakerDevice(u"Vendor Model Analog Stereo"_ns,
+ u"Speaker-Model-GroupId"_ns));
+ audios.AppendElement(MakeSpeakerDevice(u"Vendor Model Analog Stereo"_ns,
+ u"Speaker-Model-GroupId"_ns));
+ audios.AppendElement(MakeSpeakerDevice(u"Vendor Model Analog Stereo"_ns,
+ u"Speaker-Model-GroupId"_ns));
+
+ MediaManager::GuessVideoDeviceGroupIDs(devices, audios);
+
+ EXPECT_EQ(devices[0]->mRawGroupID, Cam_Model_GroupId)
+ << "Video group id has not been updated.";
+ EXPECT_NE(devices[0]->mRawGroupID, audios[0]->mRawGroupID)
+ << "Video group id is different from audio input group id.";
+ EXPECT_NE(devices[0]->mRawGroupID, audios[3]->mRawGroupID)
+ << "Video group id is different from audio output group id.";
+}
+
+/* Verify that when an audio output device name contains the video input device
+ * name the video device group id is updated to become equal to the audio
+ * device group id. */
+TEST(TestGroupId, MatchOutput)
+{
+ MediaManager::MediaDeviceSet devices;
+ MediaManager::MediaDeviceSet audios;
+
+ devices.AppendElement(
+ MakeCameraDevice(u"Vendor Model"_ns, u"Cam-Model-GroupId"_ns));
+
+ audios.AppendElement(
+ MakeMicDevice(u"Mic Analog Stereo"_ns, u"Mic-Model-GroupId"_ns));
+
+ audios.AppendElement(MakeSpeakerDevice(u"Vendor Model Analog Stereo"_ns,
+ u"Speaker-Model-GroupId"_ns));
+
+ MediaManager::GuessVideoDeviceGroupIDs(devices, audios);
+
+ EXPECT_EQ(devices[0]->mRawGroupID, audios[1]->mRawGroupID)
+ << "Video group id is the same as audio output group id.";
+}
+
+/* Verify that when an audio input device name is the same as audio output
+ * device and video input device name the video device group id is updated to
+ * become equal to the audio input device group id. */
+TEST(TestGroupId, InputOutputSameName)
+{
+ MediaManager::MediaDeviceSet devices;
+ MediaManager::MediaDeviceSet audios;
+
+ devices.AppendElement(
+ MakeCameraDevice(u"Vendor Model"_ns, u"Cam-Model-GroupId"_ns));
+
+ audios.AppendElement(
+ MakeMicDevice(u"Vendor Model"_ns, u"Mic-Model-GroupId"_ns));
+
+ audios.AppendElement(
+ MakeSpeakerDevice(u"Vendor Model"_ns, u"Speaker-Model-GroupId"_ns));
+
+ MediaManager::GuessVideoDeviceGroupIDs(devices, audios);
+
+ EXPECT_EQ(devices[0]->mRawGroupID, audios[0]->mRawGroupID)
+ << "Video input group id is the same as audio input group id.";
+}
+
+/* Verify that when an audio input device name contains the video input device
+ * and the audio input group id is an empty string, the video device group id
+ * is updated to become equal to the audio device group id. */
+TEST(TestGroupId, InputEmptyGroupId)
+{
+ MediaManager::MediaDeviceSet devices;
+ MediaManager::MediaDeviceSet audios;
+
+ devices.AppendElement(
+ MakeCameraDevice(u"Vendor Model"_ns, u"Cam-Model-GroupId"_ns));
+
+ audios.AppendElement(MakeMicDevice(u"Vendor Model"_ns, u""_ns));
+
+ MediaManager::GuessVideoDeviceGroupIDs(devices, audios);
+
+ EXPECT_EQ(devices[0]->mRawGroupID, audios[0]->mRawGroupID)
+ << "Video input group id is the same as audio input group id.";
+}
+
+/* Verify that when an audio output device name contains the video input device
+ * and the audio output group id is an empty string, the video device group id
+ * is updated to become equal to the audio output device group id. */
+TEST(TestGroupId, OutputEmptyGroupId)
+{
+ MediaManager::MediaDeviceSet devices;
+ MediaManager::MediaDeviceSet audios;
+
+ devices.AppendElement(
+ MakeCameraDevice(u"Vendor Model"_ns, u"Cam-Model-GroupId"_ns));
+
+ audios.AppendElement(MakeSpeakerDevice(u"Vendor Model"_ns, u""_ns));
+
+ MediaManager::GuessVideoDeviceGroupIDs(devices, audios);
+
+ EXPECT_EQ(devices[0]->mRawGroupID, audios[0]->mRawGroupID)
+ << "Video input group id is the same as audio output group id.";
+}
diff --git a/dom/media/gtest/TestIntervalSet.cpp b/dom/media/gtest/TestIntervalSet.cpp
new file mode 100644
index 0000000000..11d0428f6c
--- /dev/null
+++ b/dom/media/gtest/TestIntervalSet.cpp
@@ -0,0 +1,819 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/dom/TimeRanges.h"
+#include "TimeUnits.h"
+#include "Intervals.h"
+#include <algorithm>
+#include <type_traits>
+#include <vector>
+
+using namespace mozilla;
+
+typedef media::Interval<uint8_t> ByteInterval;
+typedef media::Interval<int> IntInterval;
+typedef media::IntervalSet<int> IntIntervals;
+
+ByteInterval CreateByteInterval(int32_t aStart, int32_t aEnd) {
+ ByteInterval test(aStart, aEnd);
+ return test;
+}
+
+media::IntervalSet<uint8_t> CreateByteIntervalSet(int32_t aStart,
+ int32_t aEnd) {
+ media::IntervalSet<uint8_t> test;
+ test += ByteInterval(aStart, aEnd);
+ return test;
+}
+
+TEST(IntervalSet, Constructors)
+{
+ const int32_t start = 1;
+ const int32_t end = 2;
+ const int32_t fuzz = 0;
+
+ // Compiler exercise.
+ ByteInterval test1(start, end);
+ ByteInterval test2(test1);
+ ByteInterval test3(start, end, fuzz);
+ ByteInterval test4(test3);
+ ByteInterval test5 = CreateByteInterval(start, end);
+
+ media::IntervalSet<uint8_t> blah1(test1);
+ media::IntervalSet<uint8_t> blah2 = blah1;
+ media::IntervalSet<uint8_t> blah3 = blah1 + test1;
+ media::IntervalSet<uint8_t> blah4 = test1 + blah1;
+ media::IntervalSet<uint8_t> blah5 = CreateByteIntervalSet(start, end);
+ (void)test1;
+ (void)test2;
+ (void)test3;
+ (void)test4;
+ (void)test5;
+ (void)blah1;
+ (void)blah2;
+ (void)blah3;
+ (void)blah4;
+ (void)blah5;
+}
+
+media::TimeInterval CreateTimeInterval(int32_t aStart, int32_t aEnd) {
+ // Copy constructor test
+ media::TimeUnit start = media::TimeUnit::FromMicroseconds(aStart);
+ media::TimeUnit end;
+ // operator= test
+ end = media::TimeUnit::FromMicroseconds(aEnd);
+ media::TimeInterval ti(start, end);
+ return ti;
+}
+
+media::TimeIntervals CreateTimeIntervals(int32_t aStart, int32_t aEnd) {
+ media::TimeIntervals test;
+ test += CreateTimeInterval(aStart, aEnd);
+ return test;
+}
+
+TEST(IntervalSet, TimeIntervalsConstructors)
+{
+ const auto start = media::TimeUnit::FromMicroseconds(1);
+ const auto end = media::TimeUnit::FromMicroseconds(2);
+ const media::TimeUnit fuzz;
+
+ // Compiler exercise.
+ media::TimeInterval test1(start, end);
+ media::TimeInterval test2(test1);
+ media::TimeInterval test3(start, end, fuzz);
+ media::TimeInterval test4(test3);
+ media::TimeInterval test5 =
+ CreateTimeInterval(start.ToMicroseconds(), end.ToMicroseconds());
+
+ media::TimeIntervals blah1(test1);
+ media::TimeIntervals blah2(blah1);
+ media::TimeIntervals blah3 = blah1 + test1;
+ media::TimeIntervals blah4 = test1 + blah1;
+ media::TimeIntervals blah5 =
+ CreateTimeIntervals(start.ToMicroseconds(), end.ToMicroseconds());
+ (void)test1;
+ (void)test2;
+ (void)test3;
+ (void)test4;
+ (void)test5;
+ (void)blah1;
+ (void)blah2;
+ (void)blah3;
+ (void)blah4;
+ (void)blah5;
+
+ media::TimeIntervals i0{media::TimeInterval(media::TimeUnit::FromSeconds(0),
+ media::TimeUnit::FromSeconds(0))};
+ EXPECT_TRUE(i0.IsEmpty()); // Constructing with an empty time interval.
+}
+
+TEST(IntervalSet, Length)
+{
+ IntInterval i(15, 25);
+ EXPECT_EQ(10, i.Length());
+}
+
+TEST(IntervalSet, Intersects)
+{
+ EXPECT_TRUE(IntInterval(1, 5).Intersects(IntInterval(3, 4)));
+ EXPECT_TRUE(IntInterval(1, 5).Intersects(IntInterval(3, 7)));
+ EXPECT_TRUE(IntInterval(1, 5).Intersects(IntInterval(-1, 3)));
+ EXPECT_TRUE(IntInterval(1, 5).Intersects(IntInterval(-1, 7)));
+ EXPECT_FALSE(IntInterval(1, 5).Intersects(IntInterval(6, 7)));
+ EXPECT_FALSE(IntInterval(1, 5).Intersects(IntInterval(-1, 0)));
+ // End boundary is exclusive of the interval.
+ EXPECT_FALSE(IntInterval(1, 5).Intersects(IntInterval(5, 7)));
+ EXPECT_FALSE(IntInterval(1, 5).Intersects(IntInterval(0, 1)));
+ // Empty identical interval do not intersect.
+ EXPECT_FALSE(IntInterval(1, 1).Intersects(IntInterval(1, 1)));
+ // Empty interval do not intersect.
+ EXPECT_FALSE(IntInterval(1, 1).Intersects(IntInterval(2, 2)));
+}
+
+TEST(IntervalSet, Intersection)
+{
+ IntInterval i0(10, 20);
+ IntInterval i1(15, 25);
+ IntInterval i = i0.Intersection(i1);
+ EXPECT_EQ(15, i.mStart);
+ EXPECT_EQ(20, i.mEnd);
+ IntInterval j0(10, 20);
+ IntInterval j1(20, 25);
+ IntInterval j = j0.Intersection(j1);
+ EXPECT_TRUE(j.IsEmpty());
+ IntInterval k0(2, 2);
+ IntInterval k1(2, 2);
+ IntInterval k = k0.Intersection(k1);
+ EXPECT_TRUE(k.IsEmpty());
+}
+
+TEST(IntervalSet, Equals)
+{
+ IntInterval i0(10, 20);
+ IntInterval i1(10, 20);
+ EXPECT_EQ(i0, i1);
+
+ IntInterval i2(5, 20);
+ EXPECT_NE(i0, i2);
+
+ IntInterval i3(10, 15);
+ EXPECT_NE(i0, i2);
+}
+
+TEST(IntervalSet, IntersectionIntervalSet)
+{
+ IntIntervals i0;
+ i0 += IntInterval(5, 10);
+ i0 += IntInterval(20, 25);
+ i0 += IntInterval(40, 60);
+
+ IntIntervals i1;
+ i1.Add(IntInterval(7, 15));
+ i1.Add(IntInterval(16, 27));
+ i1.Add(IntInterval(45, 50));
+ i1.Add(IntInterval(53, 57));
+
+ IntIntervals i = media::Intersection(i0, i1);
+
+ EXPECT_EQ(4u, i.Length());
+
+ EXPECT_EQ(7, i[0].mStart);
+ EXPECT_EQ(10, i[0].mEnd);
+
+ EXPECT_EQ(20, i[1].mStart);
+ EXPECT_EQ(25, i[1].mEnd);
+
+ EXPECT_EQ(45, i[2].mStart);
+ EXPECT_EQ(50, i[2].mEnd);
+
+ EXPECT_EQ(53, i[3].mStart);
+ EXPECT_EQ(57, i[3].mEnd);
+}
+
+template <typename T>
+static void Compare(const media::IntervalSet<T>& aI1,
+ const media::IntervalSet<T>& aI2) {
+ EXPECT_EQ(aI1.Length(), aI2.Length());
+ if (aI1.Length() != aI2.Length()) {
+ return;
+ }
+ for (uint32_t i = 0; i < aI1.Length(); i++) {
+ EXPECT_EQ(aI1[i].mStart, aI2[i].mStart);
+ EXPECT_EQ(aI1[i].mEnd, aI2[i].mEnd);
+ }
+}
+
+static void GeneratePermutations(const IntIntervals& aI1,
+ const IntIntervals& aI2) {
+ IntIntervals i_ref = media::Intersection(aI1, aI2);
+ // Test all permutations possible
+ std::vector<uint32_t> comb1;
+ for (uint32_t i = 0; i < aI1.Length(); i++) {
+ comb1.push_back(i);
+ }
+ std::vector<uint32_t> comb2;
+ for (uint32_t i = 0; i < aI2.Length(); i++) {
+ comb2.push_back(i);
+ }
+
+ do {
+ do {
+ // Create intervals according to new indexes.
+ IntIntervals i_0;
+ for (uint32_t i = 0; i < comb1.size(); i++) {
+ i_0 += aI1[comb1[i]];
+ }
+ // Test that intervals are always normalized.
+ Compare(aI1, i_0);
+ IntIntervals i_1;
+ for (uint32_t i = 0; i < comb2.size(); i++) {
+ i_1 += aI2[comb2[i]];
+ }
+ Compare(aI2, i_1);
+ // Check intersections yield the same result.
+ Compare(i_0.Intersection(i_1), i_ref);
+ } while (std::next_permutation(comb2.begin(), comb2.end()));
+ } while (std::next_permutation(comb1.begin(), comb1.end()));
+}
+
+TEST(IntervalSet, IntersectionNormalizedIntervalSet)
+{
+ IntIntervals i0;
+ i0 += IntInterval(5, 10);
+ i0 += IntInterval(20, 25);
+ i0 += IntInterval(40, 60);
+
+ IntIntervals i1;
+ i1.Add(IntInterval(7, 15));
+ i1.Add(IntInterval(16, 27));
+ i1.Add(IntInterval(45, 50));
+ i1.Add(IntInterval(53, 57));
+
+ GeneratePermutations(i0, i1);
+}
+
+TEST(IntervalSet, IntersectionUnorderedNonNormalizedIntervalSet)
+{
+ IntIntervals i0;
+ i0 += IntInterval(5, 10);
+ i0 += IntInterval(8, 25);
+ i0 += IntInterval(24, 60);
+
+ IntIntervals i1;
+ i1.Add(IntInterval(7, 15));
+ i1.Add(IntInterval(10, 27));
+ i1.Add(IntInterval(45, 50));
+ i1.Add(IntInterval(53, 57));
+
+ GeneratePermutations(i0, i1);
+}
+
+TEST(IntervalSet, IntersectionNonNormalizedInterval)
+{
+ IntIntervals i0;
+ i0 += IntInterval(5, 10);
+ i0 += IntInterval(8, 25);
+ i0 += IntInterval(30, 60);
+
+ media::Interval<int> i1(9, 15);
+ i0.Intersection(i1);
+ EXPECT_EQ(1u, i0.Length());
+ EXPECT_EQ(i0[0].mStart, i1.mStart);
+ EXPECT_EQ(i0[0].mEnd, i1.mEnd);
+}
+
+TEST(IntervalSet, IntersectionUnorderedNonNormalizedInterval)
+{
+ IntIntervals i0;
+ i0 += IntInterval(1, 3);
+ i0 += IntInterval(1, 10);
+ i0 += IntInterval(9, 12);
+ i0 += IntInterval(12, 15);
+ i0 += IntInterval(8, 25);
+ i0 += IntInterval(30, 60);
+ i0 += IntInterval(5, 10);
+ i0 += IntInterval(30, 60);
+
+ media::Interval<int> i1(9, 15);
+ i0.Intersection(i1);
+ EXPECT_EQ(1u, i0.Length());
+ EXPECT_EQ(i0[0].mStart, i1.mStart);
+ EXPECT_EQ(i0[0].mEnd, i1.mEnd);
+}
+
+static IntIntervals Duplicate(const IntIntervals& aValue) {
+ IntIntervals value(aValue);
+ return value;
+}
+
+TEST(IntervalSet, Normalize)
+{
+ IntIntervals i;
+ // Test IntervalSet<T> + Interval<T> operator.
+ i = i + IntInterval(20, 30);
+ // Test Internal<T> + IntervalSet<T> operator.
+ i = IntInterval(2, 7) + i;
+ // Test Interval<T> + IntervalSet<T> operator
+ i = IntInterval(1, 8) + i;
+ IntIntervals interval;
+ interval += IntInterval(5, 10);
+ // Test += with rval move.
+ i += Duplicate(interval);
+ // Test = with move and add with move.
+ i = Duplicate(interval) + i;
+
+ EXPECT_EQ(2u, i.Length());
+
+ EXPECT_EQ(1, i[0].mStart);
+ EXPECT_EQ(10, i[0].mEnd);
+
+ EXPECT_EQ(20, i[1].mStart);
+ EXPECT_EQ(30, i[1].mEnd);
+
+ media::TimeIntervals ti;
+ ti += media::TimeInterval(media::TimeUnit::FromSeconds(0.0),
+ media::TimeUnit::FromSeconds(3.203333));
+ ti += media::TimeInterval(media::TimeUnit::FromSeconds(3.203366),
+ media::TimeUnit::FromSeconds(10.010065));
+ EXPECT_EQ(2u, ti.Length());
+ ti += media::TimeInterval(ti.Start(0), ti.End(0),
+ media::TimeUnit::FromMicroseconds(35000));
+ EXPECT_EQ(1u, ti.Length());
+}
+
+TEST(IntervalSet, ContainValue)
+{
+ IntIntervals i0;
+ i0 += IntInterval(0, 10);
+ i0 += IntInterval(15, 20);
+ i0 += IntInterval(30, 50);
+ EXPECT_TRUE(i0.Contains(0)); // start is inclusive.
+ EXPECT_TRUE(i0.Contains(17));
+ EXPECT_FALSE(i0.Contains(20)); // end boundary is exclusive.
+ EXPECT_FALSE(i0.Contains(25));
+}
+
+TEST(IntervalSet, ContainValueWithFuzz)
+{
+ IntIntervals i0;
+ i0 += IntInterval(0, 10);
+ i0 += IntInterval(15, 20, 1);
+ i0 += IntInterval(30, 50);
+ EXPECT_TRUE(i0.Contains(0)); // start is inclusive.
+ EXPECT_TRUE(i0.Contains(17));
+ EXPECT_TRUE(
+ i0.Contains(20)); // end boundary is exclusive but we have a fuzz of 1.
+ EXPECT_FALSE(i0.Contains(25));
+}
+
+TEST(IntervalSet, ContainInterval)
+{
+ IntIntervals i0;
+ i0 += IntInterval(0, 10);
+ i0 += IntInterval(15, 20);
+ i0 += IntInterval(30, 50);
+ EXPECT_TRUE(i0.Contains(IntInterval(2, 8)));
+ EXPECT_TRUE(i0.Contains(IntInterval(31, 50)));
+ EXPECT_TRUE(i0.Contains(IntInterval(0, 10)));
+ EXPECT_FALSE(i0.Contains(IntInterval(0, 11)));
+ EXPECT_TRUE(i0.Contains(IntInterval(0, 5)));
+ EXPECT_FALSE(i0.Contains(IntInterval(8, 15)));
+ EXPECT_FALSE(i0.Contains(IntInterval(15, 30)));
+ EXPECT_FALSE(i0.Contains(IntInterval(30, 55)));
+}
+
+TEST(IntervalSet, ContainIntervalWithFuzz)
+{
+ IntIntervals i0;
+ i0 += IntInterval(0, 10);
+ i0 += IntInterval(15, 20);
+ i0 += IntInterval(30, 50);
+ EXPECT_TRUE(i0.Contains(IntInterval(2, 8)));
+ EXPECT_TRUE(i0.Contains(IntInterval(31, 50)));
+ EXPECT_TRUE(i0.Contains(IntInterval(0, 11, 1)));
+ EXPECT_TRUE(i0.Contains(IntInterval(0, 5)));
+ EXPECT_FALSE(i0.Contains(IntInterval(8, 15)));
+ EXPECT_FALSE(i0.Contains(IntInterval(15, 21)));
+ EXPECT_FALSE(i0.Contains(IntInterval(15, 30)));
+ EXPECT_FALSE(i0.Contains(IntInterval(30, 55)));
+
+ IntIntervals i1;
+ i1 += IntInterval(0, 10, 1);
+ i1 += IntInterval(15, 20, 1);
+ i1 += IntInterval(30, 50, 1);
+ EXPECT_TRUE(i1.Contains(IntInterval(2, 8)));
+ EXPECT_TRUE(i1.Contains(IntInterval(29, 51)));
+ EXPECT_TRUE(i1.Contains(IntInterval(0, 11, 1)));
+ EXPECT_TRUE(i1.Contains(IntInterval(15, 21)));
+}
+
+TEST(IntervalSet, Span)
+{
+ IntInterval i0(0, 10);
+ IntInterval i1(20, 30);
+ IntInterval i{i0.Span(i1)};
+
+ EXPECT_EQ(i.mStart, 0);
+ EXPECT_EQ(i.mEnd, 30);
+}
+
+TEST(IntervalSet, Union)
+{
+ IntIntervals i0;
+ i0 += IntInterval(5, 10);
+ i0 += IntInterval(20, 25);
+ i0 += IntInterval(40, 60);
+
+ IntIntervals i1;
+ i1.Add(IntInterval(7, 15));
+ i1.Add(IntInterval(16, 27));
+ i1.Add(IntInterval(45, 50));
+ i1.Add(IntInterval(53, 57));
+
+ IntIntervals i = media::Union(i0, i1);
+
+ EXPECT_EQ(3u, i.Length());
+
+ EXPECT_EQ(5, i[0].mStart);
+ EXPECT_EQ(15, i[0].mEnd);
+
+ EXPECT_EQ(16, i[1].mStart);
+ EXPECT_EQ(27, i[1].mEnd);
+
+ EXPECT_EQ(40, i[2].mStart);
+ EXPECT_EQ(60, i[2].mEnd);
+}
+
+TEST(IntervalSet, UnionNotOrdered)
+{
+ IntIntervals i0;
+ i0 += IntInterval(20, 25);
+ i0 += IntInterval(40, 60);
+ i0 += IntInterval(5, 10);
+
+ IntIntervals i1;
+ i1.Add(IntInterval(16, 27));
+ i1.Add(IntInterval(7, 15));
+ i1.Add(IntInterval(53, 57));
+ i1.Add(IntInterval(45, 50));
+
+ IntIntervals i = media::Union(i0, i1);
+
+ EXPECT_EQ(3u, i.Length());
+
+ EXPECT_EQ(5, i[0].mStart);
+ EXPECT_EQ(15, i[0].mEnd);
+
+ EXPECT_EQ(16, i[1].mStart);
+ EXPECT_EQ(27, i[1].mEnd);
+
+ EXPECT_EQ(40, i[2].mStart);
+ EXPECT_EQ(60, i[2].mEnd);
+}
+
+TEST(IntervalSet, NormalizeFuzz)
+{
+ IntIntervals i0;
+ i0 += IntInterval(11, 25, 0);
+ i0 += IntInterval(5, 10, 1);
+ i0 += IntInterval(40, 60, 1);
+
+ EXPECT_EQ(2u, i0.Length());
+
+ EXPECT_EQ(5, i0[0].mStart);
+ EXPECT_EQ(25, i0[0].mEnd);
+
+ EXPECT_EQ(40, i0[1].mStart);
+ EXPECT_EQ(60, i0[1].mEnd);
+}
+
+TEST(IntervalSet, UnionFuzz)
+{
+ IntIntervals i0;
+ i0 += IntInterval(5, 10, 1);
+ i0 += IntInterval(11, 25, 0);
+ i0 += IntInterval(40, 60, 1);
+ EXPECT_EQ(2u, i0.Length());
+ EXPECT_EQ(5, i0[0].mStart);
+ EXPECT_EQ(25, i0[0].mEnd);
+ EXPECT_EQ(40, i0[1].mStart);
+ EXPECT_EQ(60, i0[1].mEnd);
+
+ IntIntervals i1;
+ i1.Add(IntInterval(7, 15, 1));
+ i1.Add(IntInterval(16, 27, 1));
+ i1.Add(IntInterval(45, 50, 1));
+ i1.Add(IntInterval(53, 57, 1));
+ EXPECT_EQ(3u, i1.Length());
+ EXPECT_EQ(7, i1[0].mStart);
+ EXPECT_EQ(27, i1[0].mEnd);
+ EXPECT_EQ(45, i1[1].mStart);
+ EXPECT_EQ(50, i1[1].mEnd);
+ EXPECT_EQ(53, i1[2].mStart);
+ EXPECT_EQ(57, i1[2].mEnd);
+
+ IntIntervals i = media::Union(i0, i1);
+
+ EXPECT_EQ(2u, i.Length());
+
+ EXPECT_EQ(5, i[0].mStart);
+ EXPECT_EQ(27, i[0].mEnd);
+
+ EXPECT_EQ(40, i[1].mStart);
+ EXPECT_EQ(60, i[1].mEnd);
+}
+
+TEST(IntervalSet, Contiguous)
+{
+ EXPECT_FALSE(IntInterval(5, 10).Contiguous(IntInterval(11, 25)));
+ EXPECT_TRUE(IntInterval(5, 10).Contiguous(IntInterval(10, 25)));
+ EXPECT_TRUE(IntInterval(5, 10, 1).Contiguous(IntInterval(11, 25)));
+ EXPECT_TRUE(IntInterval(5, 10).Contiguous(IntInterval(11, 25, 1)));
+}
+
+TEST(IntervalSet, TimeRangesSeconds)
+{
+ media::TimeIntervals i0;
+ i0 += media::TimeInterval(media::TimeUnit::FromSeconds(20),
+ media::TimeUnit::FromSeconds(25));
+ i0 += media::TimeInterval(media::TimeUnit::FromSeconds(40),
+ media::TimeUnit::FromSeconds(60));
+ i0 += media::TimeInterval(media::TimeUnit::FromSeconds(5),
+ media::TimeUnit::FromSeconds(10));
+
+ media::TimeIntervals i1;
+ i1.Add(media::TimeInterval(media::TimeUnit::FromSeconds(16),
+ media::TimeUnit::FromSeconds(27)));
+ i1.Add(media::TimeInterval(media::TimeUnit::FromSeconds(7),
+ media::TimeUnit::FromSeconds(15)));
+ i1.Add(media::TimeInterval(media::TimeUnit::FromSeconds(53),
+ media::TimeUnit::FromSeconds(57)));
+ i1.Add(media::TimeInterval(media::TimeUnit::FromSeconds(45),
+ media::TimeUnit::FromSeconds(50)));
+
+ media::TimeIntervals i(i0 + i1);
+ RefPtr<dom::TimeRanges> tr = new dom::TimeRanges(i);
+ EXPECT_EQ(tr->Length(), i.Length());
+ for (dom::TimeRanges::index_type index = 0; index < tr->Length(); index++) {
+ ErrorResult rv;
+ EXPECT_EQ(tr->Start(index, rv), i[index].mStart.ToSeconds());
+ EXPECT_EQ(tr->Start(index, rv), i.Start(index).ToSeconds());
+ EXPECT_EQ(tr->End(index, rv), i[index].mEnd.ToSeconds());
+ EXPECT_EQ(tr->End(index, rv), i.End(index).ToSeconds());
+ }
+}
+
+static void CheckTimeRanges(dom::TimeRanges* aTr,
+ const media::TimeIntervals& aTi) {
+ RefPtr<dom::TimeRanges> tr = new dom::TimeRanges;
+ tr->Union(aTr, 0); // This will normalize the time range.
+ EXPECT_EQ(tr->Length(), aTi.Length());
+ for (dom::TimeRanges::index_type i = 0; i < tr->Length(); i++) {
+ ErrorResult rv;
+ EXPECT_EQ(tr->Start(i, rv), aTi[i].mStart.ToSeconds());
+ EXPECT_EQ(tr->Start(i, rv), aTi.Start(i).ToSeconds());
+ EXPECT_EQ(tr->End(i, rv), aTi[i].mEnd.ToSeconds());
+ EXPECT_EQ(tr->End(i, rv), aTi.End(i).ToSeconds());
+ }
+}
+
+TEST(IntervalSet, TimeRangesConversion)
+{
+ RefPtr<dom::TimeRanges> tr = new dom::TimeRanges();
+ tr->Add(20, 25);
+ tr->Add(40, 60);
+ tr->Add(5, 10);
+ tr->Add(16, 27);
+ tr->Add(53, 57);
+ tr->Add(45, 50);
+
+ // explicit copy constructor and ToTimeIntervals.
+ media::TimeIntervals i1(tr->ToTimeIntervals());
+ CheckTimeRanges(tr, i1);
+
+ // ctor(const TimeIntervals&)
+ RefPtr<dom::TimeRanges> tr2 = new dom::TimeRanges(tr->ToTimeIntervals());
+ CheckTimeRanges(tr2, i1);
+}
+
+TEST(IntervalSet, TimeRangesMicroseconds)
+{
+ media::TimeIntervals i0;
+
+ i0 += media::TimeInterval(media::TimeUnit::FromMicroseconds(20),
+ media::TimeUnit::FromMicroseconds(25));
+ i0 += media::TimeInterval(media::TimeUnit::FromMicroseconds(40),
+ media::TimeUnit::FromMicroseconds(60));
+ i0 += media::TimeInterval(media::TimeUnit::FromMicroseconds(5),
+ media::TimeUnit::FromMicroseconds(10));
+
+ media::TimeIntervals i1;
+ i1.Add(media::TimeInterval(media::TimeUnit::FromMicroseconds(16),
+ media::TimeUnit::FromMicroseconds(27)));
+ i1.Add(media::TimeInterval(media::TimeUnit::FromMicroseconds(7),
+ media::TimeUnit::FromMicroseconds(15)));
+ i1.Add(media::TimeInterval(media::TimeUnit::FromMicroseconds(53),
+ media::TimeUnit::FromMicroseconds(57)));
+ i1.Add(media::TimeInterval(media::TimeUnit::FromMicroseconds(45),
+ media::TimeUnit::FromMicroseconds(50)));
+
+ media::TimeIntervals i(i0 + i1);
+ RefPtr<dom::TimeRanges> tr = new dom::TimeRanges(i);
+ EXPECT_EQ(tr->Length(), i.Length());
+ for (dom::TimeRanges::index_type index = 0; index < tr->Length(); index++) {
+ ErrorResult rv;
+ EXPECT_EQ(tr->Start(index, rv), i[index].mStart.ToSeconds());
+ EXPECT_EQ(tr->Start(index, rv), i.Start(index).ToSeconds());
+ EXPECT_EQ(tr->End(index, rv), i[index].mEnd.ToSeconds());
+ EXPECT_EQ(tr->End(index, rv), i.End(index).ToSeconds());
+ }
+
+ tr->Normalize();
+ EXPECT_EQ(tr->Length(), i.Length());
+ for (dom::TimeRanges::index_type index = 0; index < tr->Length(); index++) {
+ ErrorResult rv;
+ EXPECT_EQ(tr->Start(index, rv), i[index].mStart.ToSeconds());
+ EXPECT_EQ(tr->Start(index, rv), i.Start(index).ToSeconds());
+ EXPECT_EQ(tr->End(index, rv), i[index].mEnd.ToSeconds());
+ EXPECT_EQ(tr->End(index, rv), i.End(index).ToSeconds());
+ }
+
+ // Check infinity values aren't lost in the conversion.
+ tr = new dom::TimeRanges();
+ tr->Add(0, 30);
+ tr->Add(50, std::numeric_limits<double>::infinity());
+ media::TimeIntervals i_oo = tr->ToTimeIntervals();
+ RefPtr<dom::TimeRanges> tr2 = new dom::TimeRanges(i_oo);
+ EXPECT_EQ(tr->Length(), tr2->Length());
+ for (dom::TimeRanges::index_type index = 0; index < tr->Length(); index++) {
+ ErrorResult rv;
+ EXPECT_EQ(tr->Start(index, rv), tr2->Start(index, rv));
+ EXPECT_EQ(tr->End(index, rv), tr2->End(index, rv));
+ }
+}
+
+template <typename T>
+class Foo {
+ public:
+ Foo() : mArg1(1), mArg2(2), mArg3(3) {}
+
+ Foo(T a1, T a2, T a3) : mArg1(a1), mArg2(a2), mArg3(a3) {}
+
+ Foo<T> operator+(const Foo<T>& aOther) const {
+ Foo<T> blah;
+ blah.mArg1 += aOther.mArg1;
+ blah.mArg2 += aOther.mArg2;
+ blah.mArg3 += aOther.mArg3;
+ return blah;
+ }
+ Foo<T> operator-(const Foo<T>& aOther) const {
+ Foo<T> blah;
+ blah.mArg1 -= aOther.mArg1;
+ blah.mArg2 -= aOther.mArg2;
+ blah.mArg3 -= aOther.mArg3;
+ return blah;
+ }
+ bool operator<(const Foo<T>& aOther) const { return mArg1 < aOther.mArg1; }
+ bool operator==(const Foo<T>& aOther) const { return mArg1 == aOther.mArg1; }
+ bool operator<=(const Foo<T>& aOther) const { return mArg1 <= aOther.mArg1; }
+
+ private:
+ int32_t mArg1;
+ int32_t mArg2;
+ int32_t mArg3;
+};
+
+TEST(IntervalSet, FooIntervalSet)
+{
+ media::Interval<Foo<int>> i(Foo<int>(), Foo<int>(4, 5, 6));
+ media::IntervalSet<Foo<int>> is;
+ is += i;
+ is += i;
+ is.Add(i);
+ is = is + i;
+ is = i + is;
+ EXPECT_EQ(1u, is.Length());
+ EXPECT_EQ(Foo<int>(), is[0].mStart);
+ EXPECT_EQ(Foo<int>(4, 5, 6), is[0].mEnd);
+}
+
+TEST(IntervalSet, StaticAssert)
+{
+ media::Interval<int> i;
+
+ static_assert(
+ std::is_same_v<nsTArray_RelocationStrategy<IntIntervals>::Type,
+ nsTArray_RelocateUsingMoveConstructor<IntIntervals>>,
+ "Must use copy constructor");
+ static_assert(
+ std::is_same_v<
+ nsTArray_RelocationStrategy<media::TimeIntervals>::Type,
+ nsTArray_RelocateUsingMoveConstructor<media::TimeIntervals>>,
+ "Must use copy constructor");
+}
+
+TEST(IntervalSet, Substraction)
+{
+ IntIntervals i0;
+ i0 += IntInterval(5, 10);
+ i0 += IntInterval(20, 25);
+ i0 += IntInterval(40, 60);
+
+ IntInterval i1(8, 15);
+ i0 -= i1;
+
+ EXPECT_EQ(3u, i0.Length());
+ EXPECT_EQ(5, i0[0].mStart);
+ EXPECT_EQ(8, i0[0].mEnd);
+ EXPECT_EQ(20, i0[1].mStart);
+ EXPECT_EQ(25, i0[1].mEnd);
+ EXPECT_EQ(40, i0[2].mStart);
+ EXPECT_EQ(60, i0[2].mEnd);
+
+ i0 = IntIntervals();
+ i0 += IntInterval(5, 10);
+ i0 += IntInterval(20, 25);
+ i0 += IntInterval(40, 60);
+ i1 = IntInterval(0, 60);
+ i0 -= i1;
+ EXPECT_TRUE(i0.IsEmpty());
+
+ i0 = IntIntervals();
+ i0 += IntInterval(5, 10);
+ i0 += IntInterval(20, 25);
+ i0 += IntInterval(40, 60);
+ i1 = IntInterval(0, 45);
+ i0 -= i1;
+ EXPECT_EQ(1u, i0.Length());
+ EXPECT_EQ(45, i0[0].mStart);
+ EXPECT_EQ(60, i0[0].mEnd);
+
+ i0 = IntIntervals();
+ i0 += IntInterval(5, 10);
+ i0 += IntInterval(20, 25);
+ i0 += IntInterval(40, 60);
+ i1 = IntInterval(8, 45);
+ i0 -= i1;
+ EXPECT_EQ(2u, i0.Length());
+ EXPECT_EQ(5, i0[0].mStart);
+ EXPECT_EQ(8, i0[0].mEnd);
+ EXPECT_EQ(45, i0[1].mStart);
+ EXPECT_EQ(60, i0[1].mEnd);
+
+ i0 = IntIntervals();
+ i0 += IntInterval(5, 10);
+ i0 += IntInterval(20, 25);
+ i0 += IntInterval(40, 60);
+ i1 = IntInterval(8, 70);
+ i0 -= i1;
+ EXPECT_EQ(1u, i0.Length());
+ EXPECT_EQ(5, i0[0].mStart);
+ EXPECT_EQ(8, i0[0].mEnd);
+
+ i0 = IntIntervals();
+ i0 += IntInterval(0, 10);
+ IntIntervals i2;
+ i2 += IntInterval(4, 6);
+ i0 -= i2;
+ EXPECT_EQ(2u, i0.Length());
+ EXPECT_EQ(0, i0[0].mStart);
+ EXPECT_EQ(4, i0[0].mEnd);
+ EXPECT_EQ(6, i0[1].mStart);
+ EXPECT_EQ(10, i0[1].mEnd);
+
+ i0 = IntIntervals();
+ i0 += IntInterval(0, 1);
+ i0 += IntInterval(3, 10);
+ EXPECT_EQ(2u, i0.Length());
+ // This fuzz should collapse i0 into [0,10).
+ i0.SetFuzz(1);
+ EXPECT_EQ(1u, i0.Length());
+ EXPECT_EQ(1, i0[0].mFuzz);
+ i2 = IntInterval(4, 6);
+ i0 -= i2;
+ EXPECT_EQ(2u, i0.Length());
+ EXPECT_EQ(0, i0[0].mStart);
+ EXPECT_EQ(4, i0[0].mEnd);
+ EXPECT_EQ(6, i0[1].mStart);
+ EXPECT_EQ(10, i0[1].mEnd);
+ EXPECT_EQ(1, i0[0].mFuzz);
+ EXPECT_EQ(1, i0[1].mFuzz);
+
+ i0 = IntIntervals();
+ i0 += IntInterval(0, 10);
+ // [4,6) with fuzz 1 used to fail because the complementary interval set
+ // [0,4)+[6,10) would collapse into [0,10).
+ i2 = IntInterval(4, 6);
+ i2.SetFuzz(1);
+ i0 -= i2;
+ EXPECT_EQ(2u, i0.Length());
+ EXPECT_EQ(0, i0[0].mStart);
+ EXPECT_EQ(4, i0[0].mEnd);
+ EXPECT_EQ(6, i0[1].mStart);
+ EXPECT_EQ(10, i0[1].mEnd);
+}
diff --git a/dom/media/gtest/TestKeyValueStorage.cpp b/dom/media/gtest/TestKeyValueStorage.cpp
new file mode 100644
index 0000000000..7ba65343e3
--- /dev/null
+++ b/dom/media/gtest/TestKeyValueStorage.cpp
@@ -0,0 +1,109 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/KeyValueStorage.h"
+
+#include "gmock/gmock.h"
+#include "gtest/gtest-printers.h"
+#include "gtest/gtest.h"
+
+#include "GMPTestMonitor.h"
+
+using ::testing::Return;
+using namespace mozilla;
+
+TEST(TestKeyValueStorage, BasicPutGet)
+{
+ auto kvs = MakeRefPtr<KeyValueStorage>();
+
+ nsCString name("database_name");
+ nsCString key("key1");
+ int32_t value = 100;
+
+ GMPTestMonitor mon;
+
+ kvs->Put(name, key, value)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&](bool) { return kvs->Get(name, key); },
+ [](nsresult rv) {
+ EXPECT_TRUE(false) << "Put promise has been rejected";
+ return KeyValueStorage::GetPromise::CreateAndReject(rv, __func__);
+ })
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&](int32_t aValue) {
+ EXPECT_EQ(aValue, value) << "Values are the same";
+ mon.SetFinished();
+ },
+ [&](nsresult rv) {
+ EXPECT_TRUE(false) << "Get Promise has been rejected";
+ mon.SetFinished();
+ });
+
+ mon.AwaitFinished();
+}
+
+TEST(TestKeyValueStorage, GetNonExistedKey)
+{
+ auto kvs = MakeRefPtr<KeyValueStorage>();
+
+ nsCString name("database_name");
+ nsCString key("NonExistedKey");
+
+ GMPTestMonitor mon;
+
+ kvs->Get(name, key)->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&mon](int32_t aValue) {
+ EXPECT_EQ(aValue, -1) << "When key does not exist return -1";
+ mon.SetFinished();
+ },
+ [&mon](nsresult rv) {
+ EXPECT_TRUE(false) << "Get Promise has been rejected";
+ mon.SetFinished();
+ });
+
+ mon.AwaitFinished();
+}
+
+TEST(TestKeyValueStorage, Clear)
+{
+ auto kvs = MakeRefPtr<KeyValueStorage>();
+
+ nsCString name("database_name");
+ nsCString key("key1");
+ int32_t value = 100;
+
+ GMPTestMonitor mon;
+
+ kvs->Put(name, key, value)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&](bool) { return kvs->Clear(name); },
+ [](nsresult rv) {
+ EXPECT_TRUE(false) << "Put promise has been rejected";
+ return GenericPromise::CreateAndReject(rv, __func__);
+ })
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&](bool) { return kvs->Get(name, key); },
+ [](nsresult rv) {
+ EXPECT_TRUE(false) << "Clear promise has been rejected";
+ return KeyValueStorage::GetPromise::CreateAndReject(rv, __func__);
+ })
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&](int32_t aValue) {
+ EXPECT_EQ(aValue, -1) << "After clear the key does not exist";
+ mon.SetFinished();
+ },
+ [&](nsresult rv) {
+ EXPECT_TRUE(false) << "Get Promise has been rejected";
+ mon.SetFinished();
+ });
+
+ mon.AwaitFinished();
+}
diff --git a/dom/media/gtest/TestMP3Demuxer.cpp b/dom/media/gtest/TestMP3Demuxer.cpp
new file mode 100644
index 0000000000..e015fe29dc
--- /dev/null
+++ b/dom/media/gtest/TestMP3Demuxer.cpp
@@ -0,0 +1,579 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <gtest/gtest.h>
+#include <vector>
+
+#include "MP3Demuxer.h"
+#include "mozilla/ArrayUtils.h"
+#include "MockMediaResource.h"
+
+class MockMP3MediaResource;
+class MockMP3StreamMediaResource;
+namespace mozilla {
+DDLoggedTypeNameAndBase(::MockMP3MediaResource, MockMediaResource);
+DDLoggedTypeNameAndBase(::MockMP3StreamMediaResource, MockMP3MediaResource);
+} // namespace mozilla
+
+using namespace mozilla;
+using media::TimeUnit;
+
+// Regular MP3 file mock resource.
+class MockMP3MediaResource
+ : public MockMediaResource,
+ public DecoderDoctorLifeLogger<MockMP3MediaResource> {
+ public:
+ explicit MockMP3MediaResource(const char* aFileName)
+ : MockMediaResource(aFileName) {}
+
+ protected:
+ virtual ~MockMP3MediaResource() = default;
+};
+
+// MP3 stream mock resource.
+class MockMP3StreamMediaResource
+ : public MockMP3MediaResource,
+ public DecoderDoctorLifeLogger<MockMP3StreamMediaResource> {
+ public:
+ explicit MockMP3StreamMediaResource(const char* aFileName)
+ : MockMP3MediaResource(aFileName) {}
+
+ int64_t GetLength() override { return -1; }
+
+ protected:
+ virtual ~MockMP3StreamMediaResource() = default;
+};
+
+struct MP3Resource {
+ enum class HeaderType { NONE, XING, VBRI };
+ struct Duration {
+ int64_t mMicroseconds;
+ float mTolerableRate;
+
+ Duration(int64_t aMicroseconds, float aTolerableRate)
+ : mMicroseconds(aMicroseconds), mTolerableRate(aTolerableRate) {}
+ int64_t Tolerance() const {
+ return AssertedCast<int64_t>(mTolerableRate *
+ static_cast<float>(mMicroseconds));
+ }
+ };
+
+ const char* mFilePath{};
+ bool mIsVBR{};
+ HeaderType mHeaderType{HeaderType::NONE};
+ int64_t mFileSize{};
+ uint32_t mMPEGLayer{};
+ uint32_t mMPEGVersion{};
+ uint8_t mID3MajorVersion{};
+ uint8_t mID3MinorVersion{};
+ uint8_t mID3Flags{};
+ uint32_t mID3Size{};
+
+ Maybe<Duration> mDuration;
+ float mSeekError{};
+ uint32_t mSampleRate{};
+ uint32_t mSamplesPerFrame{};
+ uint32_t mNumSamples{};
+ uint32_t mPadding{};
+ uint32_t mEncoderDelay{};
+ uint32_t mBitrate{};
+ uint32_t mSlotSize{};
+ int32_t mPrivate{};
+
+ // The first n frame offsets.
+ std::vector<int32_t> mSyncOffsets;
+ RefPtr<MockMP3MediaResource> mResource;
+ RefPtr<MP3TrackDemuxer> mDemuxer;
+};
+
+class MP3DemuxerTest : public ::testing::Test {
+ protected:
+ void SetUp() override {
+ {
+ MP3Resource res;
+ res.mFilePath = "noise.mp3";
+ res.mIsVBR = false;
+ res.mHeaderType = MP3Resource::HeaderType::NONE;
+ res.mFileSize = 965257;
+ res.mMPEGLayer = 3;
+ res.mMPEGVersion = 1;
+ res.mID3MajorVersion = 3;
+ res.mID3MinorVersion = 0;
+ res.mID3Flags = 0;
+ res.mID3Size = 2141;
+ // The tolerance comes from the fact that this file has ID3v1 information
+ // at the end, this trips our CBR duration calculation. The file has
+ // however the correct duration when decoded / demuxed completely.
+ res.mDuration = Some(MP3Resource::Duration{30093062, 0.00015f});
+ res.mSeekError = 0.02f;
+ res.mSampleRate = 44100;
+ res.mSamplesPerFrame = 1152;
+ res.mNumSamples = 1327104;
+ res.mPadding = 0;
+ res.mEncoderDelay = 0;
+ res.mBitrate = 256000;
+ res.mSlotSize = 1;
+ res.mPrivate = 0;
+ const int syncs[] = {2151, 2987, 3823, 4659, 5495, 6331};
+ res.mSyncOffsets.insert(res.mSyncOffsets.begin(), syncs, syncs + 6);
+
+ // No content length can be estimated for CBR stream resources.
+ MP3Resource streamRes = res;
+ streamRes.mFileSize = -1;
+ streamRes.mDuration = Nothing();
+
+ res.mResource = new MockMP3MediaResource(res.mFilePath);
+ res.mDemuxer = new MP3TrackDemuxer(res.mResource);
+ mTargets.push_back(res);
+
+ streamRes.mResource = new MockMP3StreamMediaResource(streamRes.mFilePath);
+ streamRes.mDemuxer = new MP3TrackDemuxer(streamRes.mResource);
+ mTargets.push_back(streamRes);
+ }
+
+ {
+ MP3Resource res;
+ // This file trips up the MP3 demuxer if ID3v2 tags aren't properly
+ // skipped. If skipping is not properly implemented, depending on the
+ // strictness of the MPEG frame parser a false sync will be detected
+ // somewhere within the metadata at or after 112087, or failing that, at
+ // the artificially added extraneous header at 114532.
+ res.mFilePath = "id3v2header.mp3";
+ res.mIsVBR = false;
+ res.mHeaderType = MP3Resource::HeaderType::NONE;
+ res.mFileSize = 191302;
+ res.mMPEGLayer = 3;
+ res.mMPEGVersion = 1;
+ res.mID3MajorVersion = 3;
+ res.mID3MinorVersion = 0;
+ res.mID3Flags = 0;
+ res.mID3Size = 115304;
+ // The tolerance comes from the fact that this file has ID3v1 information
+ // at the end, this trips our CBR duration calculation. The file has
+ // however the correct duration when decoded / demuxed completely.
+ res.mDuration = Some(MP3Resource::Duration{3160833, 0.0017f});
+ res.mSeekError = 0.02f;
+ res.mSampleRate = 44100;
+ res.mSamplesPerFrame = 1152;
+ res.mNumSamples = 139392;
+ res.mPadding = 0;
+ res.mEncoderDelay = 0;
+ res.mBitrate = 192000;
+ res.mSlotSize = 1;
+ res.mPrivate = 1;
+ const int syncs[] = {115314, 115941, 116568, 117195, 117822, 118449};
+ res.mSyncOffsets.insert(res.mSyncOffsets.begin(), syncs, syncs + 6);
+
+ // No content length can be estimated for CBR stream resources.
+ MP3Resource streamRes = res;
+ streamRes.mFileSize = -1;
+ streamRes.mDuration = Nothing();
+
+ res.mResource = new MockMP3MediaResource(res.mFilePath);
+ res.mDemuxer = new MP3TrackDemuxer(res.mResource);
+ mTargets.push_back(res);
+
+ streamRes.mResource = new MockMP3StreamMediaResource(streamRes.mFilePath);
+ streamRes.mDemuxer = new MP3TrackDemuxer(streamRes.mResource);
+ mTargets.push_back(streamRes);
+ }
+
+ {
+ MP3Resource res;
+ res.mFilePath = "noise_vbr.mp3";
+ res.mIsVBR = true;
+ res.mHeaderType = MP3Resource::HeaderType::XING;
+ res.mFileSize = 583679;
+ res.mMPEGLayer = 3;
+ res.mMPEGVersion = 1;
+ res.mID3MajorVersion = 3;
+ res.mID3MinorVersion = 0;
+ res.mID3Flags = 0;
+ res.mID3Size = 2221;
+ res.mDuration = Some(MP3Resource::Duration{30081065, 0.f});
+ res.mSeekError = 0.02f;
+ res.mSampleRate = 44100;
+ res.mSamplesPerFrame = 1152;
+ res.mNumSamples = 1326575;
+ res.mPadding = 576;
+ res.mEncoderDelay = 2257;
+ res.mBitrate = 154000;
+ res.mSlotSize = 1;
+ res.mPrivate = 0;
+ const int syncs[] = {2231, 2648, 2752, 3796, 4318, 4735};
+ res.mSyncOffsets.insert(res.mSyncOffsets.begin(), syncs, syncs + 6);
+
+ // VBR stream resources contain header info on total frames numbers, which
+ // is used to estimate the total duration.
+ MP3Resource streamRes = res;
+ streamRes.mFileSize = -1;
+
+ res.mResource = new MockMP3MediaResource(res.mFilePath);
+ res.mDemuxer = new MP3TrackDemuxer(res.mResource);
+ mTargets.push_back(res);
+
+ streamRes.mResource = new MockMP3StreamMediaResource(streamRes.mFilePath);
+ streamRes.mDemuxer = new MP3TrackDemuxer(streamRes.mResource);
+ mTargets.push_back(streamRes);
+ }
+
+ {
+ MP3Resource res;
+ res.mFilePath = "small-shot.mp3";
+ res.mIsVBR = true;
+ res.mHeaderType = MP3Resource::HeaderType::XING;
+ res.mFileSize = 6825;
+ res.mMPEGLayer = 3;
+ res.mMPEGVersion = 1;
+ res.mID3MajorVersion = 4;
+ res.mID3MinorVersion = 0;
+ res.mID3Flags = 0;
+ res.mID3Size = 24;
+ res.mDuration = Some(MP3Resource::Duration{301473, 0.f});
+ res.mSeekError = 0.2f;
+ res.mSampleRate = 44100;
+ res.mSamplesPerFrame = 1152;
+ res.mNumSamples = 12;
+ res.mPadding = 0;
+ res.mEncoderDelay = 1152 + 529;
+ res.mBitrate = 256000;
+ res.mSlotSize = 1;
+ res.mPrivate = 0;
+ const int syncs[] = {34, 556, 1078, 1601, 2123, 2646, 3168,
+ 3691, 4213, 4736, 5258, 5781, 6303};
+ res.mSyncOffsets.insert(res.mSyncOffsets.begin(), syncs, syncs + 13);
+
+ // No content length can be estimated for CBR stream resources.
+ MP3Resource streamRes = res;
+ streamRes.mFileSize = -1;
+
+ res.mResource = new MockMP3MediaResource(res.mFilePath);
+ res.mDemuxer = new MP3TrackDemuxer(res.mResource);
+ mTargets.push_back(res);
+
+ streamRes.mResource = new MockMP3StreamMediaResource(streamRes.mFilePath);
+ streamRes.mDemuxer = new MP3TrackDemuxer(streamRes.mResource);
+ mTargets.push_back(streamRes);
+ }
+
+ {
+ MP3Resource res;
+ // This file contains a false frame sync at 34, just after the ID3 tag,
+ // which should be identified as a false positive and skipped.
+ res.mFilePath = "small-shot-false-positive.mp3";
+ res.mIsVBR = true;
+ res.mHeaderType = MP3Resource::HeaderType::XING;
+ res.mFileSize = 6845;
+ res.mMPEGLayer = 3;
+ res.mMPEGVersion = 1;
+ res.mID3MajorVersion = 4;
+ res.mID3MinorVersion = 0;
+ res.mID3Flags = 0;
+ res.mID3Size = 24;
+ res.mDuration = Some(MP3Resource::Duration{301473, 0.f});
+ res.mSeekError = 0.2f;
+ res.mSampleRate = 44100;
+ res.mSamplesPerFrame = 1152;
+ res.mNumSamples = 12;
+ res.mPadding = 0;
+ res.mEncoderDelay = 1681;
+ res.mBitrate = 256000;
+ res.mSlotSize = 1;
+ res.mPrivate = 0;
+ const int syncs[] = {54, 576, 1098, 1621, 2143, 2666, 3188,
+ 3711, 4233, 4756, 5278, 5801, 6323};
+ res.mSyncOffsets.insert(res.mSyncOffsets.begin(), syncs, syncs + 13);
+
+ // No content length can be estimated for CBR stream resources.
+ MP3Resource streamRes = res;
+ streamRes.mFileSize = -1;
+
+ res.mResource = new MockMP3MediaResource(res.mFilePath);
+ res.mDemuxer = new MP3TrackDemuxer(res.mResource);
+ mTargets.push_back(res);
+
+ streamRes.mResource = new MockMP3StreamMediaResource(streamRes.mFilePath);
+ streamRes.mDemuxer = new MP3TrackDemuxer(streamRes.mResource);
+ mTargets.push_back(streamRes);
+ }
+
+ {
+ MP3Resource res;
+ res.mFilePath = "small-shot-partial-xing.mp3";
+ res.mIsVBR = true;
+ res.mHeaderType = MP3Resource::HeaderType::XING;
+ res.mFileSize = 6825;
+ res.mMPEGLayer = 3;
+ res.mMPEGVersion = 1;
+ res.mID3MajorVersion = 4;
+ res.mID3MinorVersion = 0;
+ res.mID3Flags = 0;
+ res.mID3Size = 24;
+ res.mDuration = Some(MP3Resource::Duration{301473, 0.f});
+ res.mSeekError = 0.2f;
+ res.mSampleRate = 44100;
+ res.mSamplesPerFrame = 1152;
+ res.mNumSamples = 12;
+ res.mPadding = 0;
+ res.mEncoderDelay = 1681;
+ res.mBitrate = 256000;
+ res.mSlotSize = 1;
+ res.mPrivate = 0;
+ const int syncs[] = {34, 556, 1078, 1601, 2123, 2646, 3168,
+ 3691, 4213, 4736, 5258, 5781, 6303};
+ res.mSyncOffsets.insert(res.mSyncOffsets.begin(), syncs, syncs + 13);
+
+ // No content length can be estimated for CBR stream resources.
+ MP3Resource streamRes = res;
+ streamRes.mFileSize = -1;
+
+ res.mResource = new MockMP3MediaResource(res.mFilePath);
+ res.mDemuxer = new MP3TrackDemuxer(res.mResource);
+ mTargets.push_back(res);
+
+ streamRes.mResource = new MockMP3StreamMediaResource(streamRes.mFilePath);
+ streamRes.mDemuxer = new MP3TrackDemuxer(streamRes.mResource);
+ mTargets.push_back(streamRes);
+ }
+
+ {
+ MP3Resource res;
+ res.mFilePath = "test_vbri.mp3";
+ res.mIsVBR = true;
+ res.mHeaderType = MP3Resource::HeaderType::VBRI;
+ res.mFileSize = 16519;
+ res.mMPEGLayer = 3;
+ res.mMPEGVersion = 1;
+ res.mID3MajorVersion = 3;
+ res.mID3MinorVersion = 0;
+ res.mID3Flags = 0;
+ res.mID3Size = 4202;
+ res.mDuration = Some(MP3Resource::Duration{731428, 0.f});
+ res.mSeekError = 0.02f;
+ res.mSampleRate = 44100;
+ res.mSamplesPerFrame = 1152;
+ res.mNumSamples = 29;
+ res.mPadding = 0;
+ res.mEncoderDelay = 1152;
+ res.mBitrate = 0;
+ res.mSlotSize = 1;
+ res.mPrivate = 0;
+ const int syncs[] = {4212, 4734, 5047, 5464, 5986, 6403};
+ res.mSyncOffsets.insert(res.mSyncOffsets.begin(), syncs, syncs + 6);
+
+ // VBR stream resources contain header info on total frames numbers, which
+ // is used to estimate the total duration.
+ MP3Resource streamRes = res;
+ streamRes.mFileSize = -1;
+
+ res.mResource = new MockMP3MediaResource(res.mFilePath);
+ res.mDemuxer = new MP3TrackDemuxer(res.mResource);
+ mTargets.push_back(res);
+
+ streamRes.mResource = new MockMP3StreamMediaResource(streamRes.mFilePath);
+ streamRes.mDemuxer = new MP3TrackDemuxer(streamRes.mResource);
+ mTargets.push_back(streamRes);
+ }
+
+ for (auto& target : mTargets) {
+ ASSERT_EQ(NS_OK, target.mResource->Open());
+ ASSERT_TRUE(target.mDemuxer->Init());
+ }
+ }
+
+ std::vector<MP3Resource> mTargets;
+};
+
+TEST_F(MP3DemuxerTest, ID3Tags) {
+ for (const auto& target : mTargets) {
+ RefPtr<MediaRawData> frame(target.mDemuxer->DemuxSample());
+ ASSERT_TRUE(frame);
+
+ const auto& id3 = target.mDemuxer->ID3Header();
+ ASSERT_TRUE(id3.IsValid());
+
+ EXPECT_EQ(target.mID3MajorVersion, id3.MajorVersion());
+ EXPECT_EQ(target.mID3MinorVersion, id3.MinorVersion());
+ EXPECT_EQ(target.mID3Flags, id3.Flags());
+ EXPECT_EQ(target.mID3Size, id3.Size());
+ }
+}
+
+TEST_F(MP3DemuxerTest, VBRHeader) {
+ for (const auto& target : mTargets) {
+ RefPtr<MediaRawData> frame(target.mDemuxer->DemuxSample());
+ ASSERT_TRUE(frame);
+
+ const auto& vbr = target.mDemuxer->VBRInfo();
+
+ if (target.mHeaderType == MP3Resource::HeaderType::XING) {
+ EXPECT_EQ(FrameParser::VBRHeader::XING, vbr.Type());
+ } else if (target.mHeaderType == MP3Resource::HeaderType::VBRI) {
+ EXPECT_TRUE(target.mIsVBR);
+ EXPECT_EQ(FrameParser::VBRHeader::VBRI, vbr.Type());
+ } else { // MP3Resource::HeaderType::NONE
+ EXPECT_EQ(FrameParser::VBRHeader::NONE, vbr.Type());
+ EXPECT_FALSE(vbr.NumAudioFrames());
+ }
+ }
+}
+
+TEST_F(MP3DemuxerTest, FrameParsing) {
+ for (const auto& target : mTargets) {
+ printf("Testing: %s\n", target.mFilePath);
+ RefPtr<MediaRawData> frameData(target.mDemuxer->DemuxSample());
+ ASSERT_TRUE(frameData);
+ EXPECT_EQ(target.mFileSize, target.mDemuxer->StreamLength());
+
+ const auto& id3 = target.mDemuxer->ID3Header();
+ ASSERT_TRUE(id3.IsValid());
+
+ int64_t parsedLength = id3.Size();
+ uint64_t bitrateSum = 0;
+ uint32_t numFrames = 0;
+ uint32_t numSamples = 0;
+
+ while (frameData) {
+ if (static_cast<int64_t>(target.mSyncOffsets.size()) > numFrames) {
+ // Test sync offsets.
+ EXPECT_EQ(target.mSyncOffsets[numFrames], frameData->mOffset);
+ }
+
+ ++numFrames;
+ parsedLength += AssertedCast<int64_t>(frameData->Size());
+
+ const auto& frame = target.mDemuxer->LastFrame();
+ const auto& header = frame.Header();
+ ASSERT_TRUE(header.IsValid());
+
+ numSamples += header.SamplesPerFrame();
+
+ EXPECT_EQ(target.mMPEGLayer, header.Layer());
+ EXPECT_EQ(target.mSampleRate, header.SampleRate());
+ EXPECT_EQ(target.mSamplesPerFrame, header.SamplesPerFrame());
+ EXPECT_EQ(target.mSlotSize, header.SlotSize());
+ EXPECT_EQ(target.mPrivate, header.Private());
+
+ if (target.mIsVBR) {
+ // Used to compute the average bitrate for VBR streams.
+ bitrateSum += target.mBitrate;
+ } else {
+ EXPECT_EQ(target.mBitrate, header.Bitrate());
+ }
+
+ frameData = target.mDemuxer->DemuxSample();
+ }
+
+ EXPECT_EQ(target.mPadding, target.mDemuxer->PaddingFrames());
+ EXPECT_EQ(target.mEncoderDelay, target.mDemuxer->EncoderDelayFrames());
+ EXPECT_GE(numSamples, 0u);
+
+ // There may be trailing headers which we don't parse, so the stream length
+ // is the upper bound.
+ if (target.mFileSize > 0) {
+ EXPECT_GE(target.mFileSize, parsedLength);
+ }
+
+ if (target.mIsVBR) {
+ ASSERT_TRUE(numFrames);
+ EXPECT_EQ(target.mBitrate, bitrateSum / numFrames);
+ }
+ }
+}
+
+TEST_F(MP3DemuxerTest, Duration) {
+ for (const auto& target : mTargets) {
+ printf("Testing: %s\n", target.mFilePath);
+ RefPtr<MediaRawData> frameData(target.mDemuxer->DemuxSample());
+ ASSERT_TRUE(frameData);
+ EXPECT_EQ(target.mFileSize, target.mDemuxer->StreamLength());
+
+ while (frameData) {
+ if (target.mDuration) {
+ ASSERT_TRUE(target.mDemuxer->Duration());
+ EXPECT_NEAR(target.mDuration->mMicroseconds,
+ target.mDemuxer->Duration()->ToMicroseconds(),
+ target.mDuration->Tolerance());
+ } else {
+ EXPECT_FALSE(target.mDemuxer->Duration());
+ }
+ frameData = target.mDemuxer->DemuxSample();
+ }
+ if (target.mDuration) {
+ // At the end, the durations should always be exact.
+ EXPECT_EQ(target.mDuration->mMicroseconds,
+ target.mDemuxer->Duration()->ToMicroseconds());
+ }
+ }
+
+ // Seek out of range tests.
+ for (const auto& target : mTargets) {
+ printf("Testing %s\n", target.mFilePath);
+ // Skip tests for stream media resources because of lacking duration.
+ if (target.mFileSize <= 0) {
+ continue;
+ }
+
+ target.mDemuxer->Reset();
+ RefPtr<MediaRawData> frameData(target.mDemuxer->DemuxSample());
+ ASSERT_TRUE(frameData);
+
+ ASSERT_TRUE(target.mDemuxer->Duration());
+ const auto duration = target.mDemuxer->Duration().value();
+ const auto pos = duration + TimeUnit::FromMicroseconds(1e6);
+
+ // Attempt to seek 1 second past the end of stream.
+ target.mDemuxer->Seek(pos);
+ // The seek should bring us to the end of the stream.
+ EXPECT_NEAR(duration.ToMicroseconds(),
+ target.mDemuxer->SeekPosition().ToMicroseconds(),
+ target.mSeekError * duration.ToMicroseconds());
+
+ // Since we're at the end of the stream, there should be no frames left.
+ frameData = target.mDemuxer->DemuxSample();
+ ASSERT_FALSE(frameData);
+ }
+}
+
+TEST_F(MP3DemuxerTest, Seek) {
+ for (const auto& target : mTargets) {
+ RefPtr<MediaRawData> frameData(target.mDemuxer->DemuxSample());
+ ASSERT_TRUE(frameData);
+
+ const auto seekTime = TimeUnit::FromSeconds(1);
+ auto pos = target.mDemuxer->SeekPosition();
+
+ while (frameData) {
+ EXPECT_NEAR(pos.ToMicroseconds(),
+ target.mDemuxer->SeekPosition().ToMicroseconds(),
+ target.mSeekError * pos.ToMicroseconds());
+
+ pos += seekTime;
+ target.mDemuxer->Seek(pos);
+ frameData = target.mDemuxer->DemuxSample();
+ }
+ }
+
+ // Seeking should work with in-between resets, too.
+ for (const auto& target : mTargets) {
+ target.mDemuxer->Reset();
+ RefPtr<MediaRawData> frameData(target.mDemuxer->DemuxSample());
+ ASSERT_TRUE(frameData);
+
+ const auto seekTime = TimeUnit::FromSeconds(1);
+ auto pos = target.mDemuxer->SeekPosition();
+
+ while (frameData) {
+ EXPECT_NEAR(pos.ToMicroseconds(),
+ target.mDemuxer->SeekPosition().ToMicroseconds(),
+ target.mSeekError * pos.ToMicroseconds());
+
+ pos += seekTime;
+ target.mDemuxer->Reset();
+ target.mDemuxer->Seek(pos);
+ frameData = target.mDemuxer->DemuxSample();
+ }
+ }
+}
diff --git a/dom/media/gtest/TestMP4Demuxer.cpp b/dom/media/gtest/TestMP4Demuxer.cpp
new file mode 100644
index 0000000000..43dfdf19a4
--- /dev/null
+++ b/dom/media/gtest/TestMP4Demuxer.cpp
@@ -0,0 +1,613 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "MP4Demuxer.h"
+#include "mozilla/MozPromise.h"
+#include "MediaDataDemuxer.h"
+#include "mozilla/SharedThreadPool.h"
+#include "mozilla/TaskQueue.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Unused.h"
+#include "MockMediaResource.h"
+#include "VideoUtils.h"
+
+using namespace mozilla;
+using media::TimeUnit;
+
+#define DO_FAIL \
+ [binding]() -> void { \
+ EXPECT_TRUE(false); \
+ binding->mTaskQueue->BeginShutdown(); \
+ }
+
+class MP4DemuxerBinding {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MP4DemuxerBinding);
+
+ RefPtr<MockMediaResource> resource;
+ RefPtr<MP4Demuxer> mDemuxer;
+ RefPtr<TaskQueue> mTaskQueue;
+ RefPtr<MediaTrackDemuxer> mAudioTrack;
+ RefPtr<MediaTrackDemuxer> mVideoTrack;
+ uint32_t mIndex;
+ nsTArray<RefPtr<MediaRawData>> mSamples;
+ nsTArray<int64_t> mKeyFrameTimecodes;
+ MozPromiseHolder<GenericPromise> mCheckTrackKeyFramePromise;
+ MozPromiseHolder<GenericPromise> mCheckTrackSamples;
+
+ explicit MP4DemuxerBinding(const char* aFileName = "dash_dashinit.mp4")
+ : resource(new MockMediaResource(aFileName)),
+ mDemuxer(new MP4Demuxer(resource)),
+ mTaskQueue(TaskQueue::Create(
+ GetMediaThreadPool(MediaThreadType::SUPERVISOR), "TestMP4Demuxer")),
+ mIndex(0) {
+ EXPECT_EQ(NS_OK, resource->Open());
+ }
+
+ template <typename Function>
+ void RunTestAndWait(const Function& aFunction) {
+ Function func(aFunction);
+ RefPtr<MP4DemuxerBinding> binding = this;
+ mDemuxer->Init()->Then(mTaskQueue, __func__, std::move(func), DO_FAIL);
+ mTaskQueue->AwaitShutdownAndIdle();
+ }
+
+ RefPtr<GenericPromise> CheckTrackKeyFrame(MediaTrackDemuxer* aTrackDemuxer) {
+ MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
+
+ RefPtr<MediaTrackDemuxer> track = aTrackDemuxer;
+ RefPtr<MP4DemuxerBinding> binding = this;
+
+ auto time = TimeUnit::Invalid();
+ while (mIndex < mSamples.Length()) {
+ uint32_t i = mIndex++;
+ if (mSamples[i]->mKeyframe) {
+ time = mSamples[i]->mTime;
+ break;
+ }
+ }
+
+ RefPtr<GenericPromise> p = mCheckTrackKeyFramePromise.Ensure(__func__);
+
+ if (!time.IsValid()) {
+ mCheckTrackKeyFramePromise.Resolve(true, __func__);
+ return p;
+ }
+
+ DispatchTask([track, time, binding]() {
+ track->Seek(time)->Then(
+ binding->mTaskQueue, __func__,
+ [track, time, binding]() {
+ track->GetSamples()->Then(
+ binding->mTaskQueue, __func__,
+ [track, time,
+ binding](RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples) {
+ EXPECT_EQ(time, aSamples->GetSamples()[0]->mTime);
+ binding->CheckTrackKeyFrame(track);
+ },
+ DO_FAIL);
+ },
+ DO_FAIL);
+ });
+
+ return p;
+ }
+
+ RefPtr<GenericPromise> CheckTrackSamples(MediaTrackDemuxer* aTrackDemuxer) {
+ MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
+
+ RefPtr<MediaTrackDemuxer> track = aTrackDemuxer;
+ RefPtr<MP4DemuxerBinding> binding = this;
+
+ RefPtr<GenericPromise> p = mCheckTrackSamples.Ensure(__func__);
+
+ DispatchTask([track, binding]() {
+ track->GetSamples()->Then(
+ binding->mTaskQueue, __func__,
+ [track, binding](RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples) {
+ if (aSamples->GetSamples().Length()) {
+ binding->mSamples.AppendElements(aSamples->GetSamples());
+ binding->CheckTrackSamples(track);
+ }
+ },
+ [binding](const MediaResult& aError) {
+ if (aError == NS_ERROR_DOM_MEDIA_END_OF_STREAM) {
+ EXPECT_TRUE(binding->mSamples.Length() > 1);
+ for (uint32_t i = 0; i < (binding->mSamples.Length() - 1); i++) {
+ EXPECT_LT(binding->mSamples[i]->mTimecode,
+ binding->mSamples[i + 1]->mTimecode);
+ if (binding->mSamples[i]->mKeyframe) {
+ binding->mKeyFrameTimecodes.AppendElement(
+ binding->mSamples[i]->mTimecode.ToMicroseconds());
+ }
+ }
+ binding->mCheckTrackSamples.Resolve(true, __func__);
+ } else {
+ EXPECT_TRUE(false);
+ binding->mCheckTrackSamples.Reject(aError, __func__);
+ }
+ });
+ });
+
+ return p;
+ }
+
+ private:
+ template <typename FunctionType>
+ void DispatchTask(FunctionType aFun) {
+ RefPtr<Runnable> r =
+ NS_NewRunnableFunction("MP4DemuxerBinding::DispatchTask", aFun);
+ Unused << mTaskQueue->Dispatch(r.forget());
+ }
+
+ virtual ~MP4DemuxerBinding() = default;
+};
+
+TEST(MP4Demuxer, Seek)
+{
+ RefPtr<MP4DemuxerBinding> binding = new MP4DemuxerBinding();
+
+ binding->RunTestAndWait([binding]() {
+ binding->mVideoTrack =
+ binding->mDemuxer->GetTrackDemuxer(TrackInfo::kVideoTrack, 0);
+ binding->CheckTrackSamples(binding->mVideoTrack)
+ ->Then(
+ binding->mTaskQueue, __func__,
+ [binding]() {
+ binding->CheckTrackKeyFrame(binding->mVideoTrack)
+ ->Then(
+ binding->mTaskQueue, __func__,
+ [binding]() { binding->mTaskQueue->BeginShutdown(); },
+ DO_FAIL);
+ },
+ DO_FAIL);
+ });
+}
+
+static nsCString ToCryptoString(const CryptoSample& aCrypto) {
+ nsCString res;
+ if (aCrypto.IsEncrypted()) {
+ res.AppendPrintf("%d ", aCrypto.mIVSize);
+ for (size_t i = 0; i < aCrypto.mKeyId.Length(); i++) {
+ res.AppendPrintf("%02x", aCrypto.mKeyId[i]);
+ }
+ res.AppendLiteral(" ");
+ for (size_t i = 0; i < aCrypto.mIV.Length(); i++) {
+ res.AppendPrintf("%02x", aCrypto.mIV[i]);
+ }
+ EXPECT_EQ(aCrypto.mPlainSizes.Length(), aCrypto.mEncryptedSizes.Length());
+ for (size_t i = 0; i < aCrypto.mPlainSizes.Length(); i++) {
+ res.AppendPrintf(" %d,%d", aCrypto.mPlainSizes[i],
+ aCrypto.mEncryptedSizes[i]);
+ }
+ } else {
+ res.AppendLiteral("no crypto");
+ }
+ return res;
+}
+
+TEST(MP4Demuxer, CENCFragVideo)
+{
+ const char* video[] = {
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000000 "
+ "5,684 5,16980",
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000450 "
+ "5,1826",
+ "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000004c3 "
+ "5,1215",
+ "16 7e571d037e571d037e571d037e571d03 0000000000000000000000000000050f "
+ "5,1302",
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000561 "
+ "5,939",
+ "16 7e571d037e571d037e571d037e571d03 0000000000000000000000000000059c "
+ "5,763",
+ "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000005cc "
+ "5,672",
+ "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000005f6 "
+ "5,748",
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000625 "
+ "5,1025",
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000666 "
+ "5,730",
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000694 "
+ "5,897",
+ "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000006cd "
+ "5,643",
+ "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000006f6 "
+ "5,556",
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000719 "
+ "5,527",
+ "16 7e571d037e571d037e571d037e571d03 0000000000000000000000000000073a "
+ "5,606",
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000760 "
+ "5,701",
+ "16 7e571d037e571d037e571d037e571d03 0000000000000000000000000000078c "
+ "5,531",
+ "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000007ae "
+ "5,562",
+ "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000007d2 "
+ "5,576",
+ "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000007f6 "
+ "5,514",
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000817 "
+ "5,404",
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000831 "
+ "5,635",
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000859 "
+ "5,433",
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000875 "
+ "5,478",
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000893 "
+ "5,474",
+ "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000008b1 "
+ "5,462",
+ "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000008ce "
+ "5,473",
+ "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000008ec "
+ "5,437",
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000908 "
+ "5,418",
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000923 "
+ "5,475",
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000941 "
+ "5,23133",
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000ee7 "
+ "5,475",
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000f05 "
+ "5,402",
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000f1f "
+ "5,415",
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000f39 "
+ "5,408",
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000f53 "
+ "5,442",
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000f6f "
+ "5,385",
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000f88 "
+ "5,368",
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000f9f "
+ "5,354",
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000fb6 "
+ "5,400",
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000fcf "
+ "5,399",
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000000fe8 "
+ "5,1098",
+ "16 7e571d037e571d037e571d037e571d03 0000000000000000000000000000102d "
+ "5,1508",
+ "16 7e571d037e571d037e571d037e571d03 0000000000000000000000000000108c "
+ "5,1345",
+ "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000010e1 "
+ "5,1945",
+ "16 7e571d037e571d037e571d037e571d03 0000000000000000000000000000115b "
+ "5,1824",
+ "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000011cd "
+ "5,2133",
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000001253 "
+ "5,2486",
+ "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000012ef "
+ "5,1739",
+ "16 7e571d037e571d037e571d037e571d03 0000000000000000000000000000135c "
+ "5,1836",
+ "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000013cf "
+ "5,2367",
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000001463 "
+ "5,2571",
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000001504 "
+ "5,3008",
+ "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000015c0 "
+ "5,3255",
+ "16 7e571d037e571d037e571d037e571d03 0000000000000000000000000000168c "
+ "5,3225",
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000001756 "
+ "5,3118",
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000001819 "
+ "5,2407",
+ "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000018b0 "
+ "5,2400",
+ "16 7e571d037e571d037e571d037e571d03 00000000000000000000000000001946 "
+ "5,2158",
+ "16 7e571d037e571d037e571d037e571d03 000000000000000000000000000019cd "
+ "5,2392",
+ };
+
+ RefPtr<MP4DemuxerBinding> binding = new MP4DemuxerBinding("gizmo-frag.mp4");
+
+ binding->RunTestAndWait([binding, video]() {
+ // grab all video samples.
+ binding->mVideoTrack =
+ binding->mDemuxer->GetTrackDemuxer(TrackInfo::kVideoTrack, 0);
+ binding->CheckTrackSamples(binding->mVideoTrack)
+ ->Then(
+ binding->mTaskQueue, __func__,
+ [binding, video]() {
+ for (uint32_t i = 0; i < binding->mSamples.Length(); i++) {
+ nsCString text = ToCryptoString(binding->mSamples[i]->mCrypto);
+ EXPECT_STREQ(video[i++], text.get());
+ }
+ EXPECT_EQ(ArrayLength(video), binding->mSamples.Length());
+ binding->mTaskQueue->BeginShutdown();
+ },
+ DO_FAIL);
+ });
+}
+
+TEST(MP4Demuxer, CENCFragAudio)
+{
+ const char* audio[] = {
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000000 "
+ "0,281",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000012 "
+ "0,257",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000023 "
+ "0,246",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000033 "
+ "0,257",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000044 "
+ "0,260",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000055 "
+ "0,260",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000066 "
+ "0,272",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000077 "
+ "0,280",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000089 "
+ "0,284",
+ "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000009b "
+ "0,290",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000000ae "
+ "0,278",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000000c0 "
+ "0,268",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000000d1 "
+ "0,307",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000000e5 "
+ "0,290",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000000f8 "
+ "0,304",
+ "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000010b "
+ "0,316",
+ "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000011f "
+ "0,308",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000133 "
+ "0,301",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000146 "
+ "0,318",
+ "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000015a "
+ "0,311",
+ "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000016e "
+ "0,303",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000181 "
+ "0,325",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000196 "
+ "0,334",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000001ab "
+ "0,344",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000001c1 "
+ "0,344",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000001d7 "
+ "0,387",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000001f0 "
+ "0,396",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000209 "
+ "0,368",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000220 "
+ "0,373",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000238 "
+ "0,425",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000253 "
+ "0,428",
+ "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000026e "
+ "0,426",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000289 "
+ "0,427",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000002a4 "
+ "0,424",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000002bf "
+ "0,447",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000002db "
+ "0,446",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000002f7 "
+ "0,442",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000313 "
+ "0,444",
+ "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000032f "
+ "0,374",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000347 "
+ "0,405",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000361 "
+ "0,372",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000379 "
+ "0,395",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000392 "
+ "0,435",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000003ae "
+ "0,426",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000003c9 "
+ "0,430",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000003e4 "
+ "0,390",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000003fd "
+ "0,335",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000412 "
+ "0,339",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000428 "
+ "0,352",
+ "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000043e "
+ "0,364",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000455 "
+ "0,398",
+ "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000046e "
+ "0,451",
+ "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000048b "
+ "0,448",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000004a7 "
+ "0,436",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000004c3 "
+ "0,424",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000004de "
+ "0,428",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000004f9 "
+ "0,413",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000513 "
+ "0,430",
+ "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000052e "
+ "0,450",
+ "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000054b "
+ "0,386",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000564 "
+ "0,320",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000578 "
+ "0,347",
+ "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000058e "
+ "0,382",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000005a6 "
+ "0,437",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000005c2 "
+ "0,387",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000005db "
+ "0,340",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000005f1 "
+ "0,337",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000607 "
+ "0,389",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000620 "
+ "0,428",
+ "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000063b "
+ "0,426",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000656 "
+ "0,446",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000672 "
+ "0,456",
+ "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000068f "
+ "0,468",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000006ad "
+ "0,468",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000006cb "
+ "0,463",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000006e8 "
+ "0,467",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000706 "
+ "0,460",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000723 "
+ "0,446",
+ "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000073f "
+ "0,453",
+ "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000075c "
+ "0,448",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000778 "
+ "0,446",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000794 "
+ "0,439",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000007b0 "
+ "0,436",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000007cc "
+ "0,441",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000007e8 "
+ "0,465",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000806 "
+ "0,448",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000822 "
+ "0,448",
+ "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000083e "
+ "0,469",
+ "16 7e571d047e571d047e571d047e571d04 0000000000000000000000000000085c "
+ "0,431",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000877 "
+ "0,437",
+ "16 7e571d047e571d047e571d047e571d04 00000000000000000000000000000893 "
+ "0,474",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000008b1 "
+ "0,436",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000008cd "
+ "0,433",
+ "16 7e571d047e571d047e571d047e571d04 000000000000000000000000000008e9 "
+ "0,481",
+ };
+
+ RefPtr<MP4DemuxerBinding> binding = new MP4DemuxerBinding("gizmo-frag.mp4");
+
+ binding->RunTestAndWait([binding, audio]() {
+ // grab all audio samples.
+ binding->mAudioTrack =
+ binding->mDemuxer->GetTrackDemuxer(TrackInfo::kAudioTrack, 0);
+ binding->CheckTrackSamples(binding->mAudioTrack)
+ ->Then(
+ binding->mTaskQueue, __func__,
+ [binding, audio]() {
+ EXPECT_TRUE(binding->mSamples.Length() > 1);
+ for (uint32_t i = 0; i < binding->mSamples.Length(); i++) {
+ nsCString text = ToCryptoString(binding->mSamples[i]->mCrypto);
+ EXPECT_STREQ(audio[i++], text.get());
+ }
+ EXPECT_EQ(ArrayLength(audio), binding->mSamples.Length());
+ binding->mTaskQueue->BeginShutdown();
+ },
+ DO_FAIL);
+ });
+}
+
+TEST(MP4Demuxer, GetNextKeyframe)
+{
+ RefPtr<MP4DemuxerBinding> binding = new MP4DemuxerBinding("gizmo-frag.mp4");
+
+ binding->RunTestAndWait([binding]() {
+ // Insert a [0,end] buffered range, to simulate Moof's being buffered
+ // via MSE.
+ auto len = binding->resource->GetLength();
+ binding->resource->MockAddBufferedRange(0, len);
+
+ // gizmp-frag has two keyframes; one at dts=cts=0, and another at
+ // dts=cts=1000000. Verify we get expected results.
+ TimeUnit time;
+ binding->mVideoTrack =
+ binding->mDemuxer->GetTrackDemuxer(TrackInfo::kVideoTrack, 0);
+ binding->mVideoTrack->Reset();
+ binding->mVideoTrack->GetNextRandomAccessPoint(&time);
+ EXPECT_EQ(time.ToMicroseconds(), 0);
+ binding->mVideoTrack->GetSamples()->Then(
+ binding->mTaskQueue, __func__,
+ [binding]() {
+ TimeUnit time;
+ binding->mVideoTrack->GetNextRandomAccessPoint(&time);
+ EXPECT_EQ(time.ToMicroseconds(), 1000000);
+ binding->mTaskQueue->BeginShutdown();
+ },
+ DO_FAIL);
+ });
+}
+
+TEST(MP4Demuxer, ZeroInLastMoov)
+{
+ RefPtr<MP4DemuxerBinding> binding =
+ new MP4DemuxerBinding("short-zero-in-moov.mp4");
+ binding->RunTestAndWait([binding]() {
+ // It demuxes without error. That is sufficient.
+ binding->mTaskQueue->BeginShutdown();
+ });
+}
+
+TEST(MP4Demuxer, ZeroInMoovQuickTime)
+{
+ RefPtr<MP4DemuxerBinding> binding =
+ new MP4DemuxerBinding("short-zero-inband.mov");
+ binding->RunTestAndWait([binding]() {
+ // It demuxes without error. That is sufficient.
+ binding->mTaskQueue->BeginShutdown();
+ });
+}
+
+TEST(MP4Demuxer, IgnoreMinus1Duration)
+{
+ RefPtr<MP4DemuxerBinding> binding =
+ new MP4DemuxerBinding("negative_duration.mp4");
+ binding->RunTestAndWait([binding]() {
+ // It demuxes without error. That is sufficient.
+ binding->mTaskQueue->BeginShutdown();
+ });
+}
+
+#undef DO_FAIL
diff --git a/dom/media/gtest/TestMediaCodecsSupport.cpp b/dom/media/gtest/TestMediaCodecsSupport.cpp
new file mode 100644
index 0000000000..86840cce09
--- /dev/null
+++ b/dom/media/gtest/TestMediaCodecsSupport.cpp
@@ -0,0 +1,157 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "MediaCodecsSupport.h"
+
+using namespace mozilla;
+using namespace media;
+
+// Test MCSInfo::GetDecodeSupportSet function.
+// This function is used to retrieve SW/HW support information for a
+// given codec from a MediaCodecsSupported EnumSet.
+// We validate that SW, HW, SW+HW, or lack of support information is
+// properly returned.
+TEST(MediaCodecsSupport, GetDecodeSupportSet)
+{
+ // Mock VP8 SW support, VP9 HW support, H264 SW+HW support
+ MediaCodecsSupported supported{MediaCodecsSupport::VP8SoftwareDecode,
+ MediaCodecsSupport::VP9HardwareDecode,
+ MediaCodecsSupport::H264SoftwareDecode,
+ MediaCodecsSupport::H264HardwareDecode};
+
+ MediaCodec codec; // Codec used to generate + filter results
+ DecodeSupportSet RV; // Return value to check for validity
+
+ // Check only SW support returned for VP8
+ codec = MediaCodec::VP8;
+ RV = MCSInfo::GetDecodeSupportSet(codec, supported);
+ EXPECT_TRUE(RV.contains(DecodeSupport::SoftwareDecode));
+ EXPECT_TRUE(RV.size() == 1);
+
+ // Check only HW support returned for VP9
+ codec = MediaCodec::VP9;
+ RV = MCSInfo::GetDecodeSupportSet(codec, supported);
+ EXPECT_TRUE(RV.contains(DecodeSupport::HardwareDecode));
+ EXPECT_TRUE(RV.size() == 1);
+
+ // Check for both SW/HW support returned for H264
+ codec = MediaCodec::H264;
+ RV = MCSInfo::GetDecodeSupportSet(codec, supported);
+ EXPECT_TRUE(RV.contains(DecodeSupport::SoftwareDecode));
+ EXPECT_TRUE(RV.contains(DecodeSupport::HardwareDecode));
+ EXPECT_TRUE(RV.size() == 2);
+
+ // Check empty return if codec not in list of codecs
+ codec = MediaCodec::AV1;
+ RV = MCSInfo::GetDecodeSupportSet(codec, supported);
+ EXPECT_TRUE(RV.size() == 0);
+}
+
+// Test MCSInfo::GetDecodeMediaCodecsSupported function.
+// This function is used to generate codec-specific SW/HW
+// support information from a generic codec identifier enum and
+// generic SW/HW support information.
+// We validate that SW, HW, SW+HW, or lack of support information is
+// properly returned.
+TEST(MediaCodecsSupport, GetDecodeMediaCodecsSupported)
+{
+ MediaCodec codec; // Codec used to generate / filter results
+ MediaCodecsSupported RV; // Return value to check for validity
+ DecodeSupportSet dss; // Non codec-specific SW / HW support information
+
+ // Check SW support returned for VP8
+ codec = MediaCodec::VP8;
+ dss = DecodeSupportSet{DecodeSupport::SoftwareDecode};
+ RV = MCSInfo::GetDecodeMediaCodecsSupported(codec, dss);
+ EXPECT_TRUE(RV.contains(MediaCodecsSupport::VP8SoftwareDecode));
+ EXPECT_TRUE(RV.size() == 1);
+
+ // Check HW support returned for AV1
+ codec = MediaCodec::AV1;
+ dss = DecodeSupportSet{DecodeSupport::HardwareDecode};
+ RV = MCSInfo::GetDecodeMediaCodecsSupported(codec, dss);
+ EXPECT_TRUE(RV.contains(MediaCodecsSupport::AV1HardwareDecode));
+ EXPECT_TRUE(RV.size() == 1);
+
+ // Check SW + HW support returned for VP9
+ codec = MediaCodec::VP9;
+ dss = DecodeSupportSet{DecodeSupport::SoftwareDecode,
+ DecodeSupport::HardwareDecode};
+ RV = MCSInfo::GetDecodeMediaCodecsSupported(codec, dss);
+ EXPECT_TRUE(RV.contains(MediaCodecsSupport::VP9SoftwareDecode));
+ EXPECT_TRUE(RV.contains(MediaCodecsSupport::VP9HardwareDecode));
+ EXPECT_TRUE(RV.size() == 2);
+
+ // Check empty return if codec not supported
+ codec = MediaCodec::AV1;
+ dss = DecodeSupportSet{};
+ RV = MCSInfo::GetDecodeMediaCodecsSupported(codec, dss);
+ EXPECT_TRUE(RV.size() == 0);
+}
+
+// Test MCSInfo::AddSupport function.
+// This function is used to store codec support data.
+// Incoming support data will be merged with any data that
+// has already been stored.
+TEST(MediaCodecsSupport, AddSupport)
+{
+ // Make sure we're not storing any existing support information.
+ MCSInfo::ResetSupport();
+ EXPECT_TRUE(MCSInfo::GetSupport().size() == 0);
+
+ // Add codec support one at a time via individual calls
+ MCSInfo::AddSupport(MediaCodecsSupport::AACSoftwareDecode);
+ MCSInfo::AddSupport(MediaCodecsSupport::VP9SoftwareDecode);
+ MCSInfo::AddSupport(MediaCodecsSupport::AV1HardwareDecode);
+
+ // Add multiple codec support via MediaCodecsSupported EnumSet
+ MCSInfo::AddSupport(
+ MediaCodecsSupported{MediaCodecsSupport::H264SoftwareDecode,
+ MediaCodecsSupport::H264HardwareDecode});
+
+ // Query MCSInfo for supported codecs
+ MediaCodecsSupported supported = MCSInfo::GetSupport();
+ DecodeSupportSet dss;
+
+ // AAC should only report software decode support
+ dss = MCSInfo::GetDecodeSupportSet(MediaCodec::AAC, supported);
+ EXPECT_TRUE(dss.size() == 1);
+ EXPECT_TRUE(dss.contains(DecodeSupport::SoftwareDecode));
+
+ // AV1 should only report hardware decode support
+ dss = MCSInfo::GetDecodeSupportSet(MediaCodec::AV1, supported);
+ EXPECT_TRUE(dss.size() == 1);
+ EXPECT_TRUE(dss.contains(DecodeSupport::HardwareDecode));
+
+ // H264 should report both SW + HW decode support
+ dss = MCSInfo::GetDecodeSupportSet(MediaCodec::H264, supported);
+ EXPECT_TRUE(dss.size() == 2);
+ EXPECT_TRUE(dss.contains(DecodeSupport::SoftwareDecode));
+ EXPECT_TRUE(dss.contains(DecodeSupport::HardwareDecode));
+
+ // Vorbis should report no decode support
+ dss = MCSInfo::GetDecodeSupportSet(MediaCodec::Vorbis, supported);
+ EXPECT_TRUE(dss.size() == 0);
+}
+
+// Test MCSInfo::GetMediaCodecsSupportedString function.
+// This function returns a human-readable string containing codec
+// names and SW/HW playback support information.
+TEST(MediaCodecsSupport, GetMediaCodecsSupportedString)
+{
+ // Make sure we're not storing any existing support information.
+ MCSInfo::ResetSupport();
+ EXPECT_TRUE(MCSInfo::GetSupport().size() == 0);
+
+ // Add H264 SW/HW support + VP8 Software decode support.
+ MCSInfo::AddSupport({MediaCodecsSupport::H264SoftwareDecode,
+ MediaCodecsSupport::H264HardwareDecode,
+ MediaCodecsSupport::VP8SoftwareDecode});
+
+ nsCString supportString;
+ MCSInfo::GetMediaCodecsSupportedString(supportString, MCSInfo::GetSupport());
+ EXPECT_TRUE(supportString.Equals("H264 SW\nH264 HW\nVP8 SW"_ns));
+}
diff --git a/dom/media/gtest/TestMediaDataDecoder.cpp b/dom/media/gtest/TestMediaDataDecoder.cpp
new file mode 100644
index 0000000000..2930dfb7a1
--- /dev/null
+++ b/dom/media/gtest/TestMediaDataDecoder.cpp
@@ -0,0 +1,98 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "Benchmark.h"
+#include "MockMediaResource.h"
+#include "DecoderTraits.h"
+#include "MediaContainerType.h"
+#include "MP4Demuxer.h"
+#include "WebMDecoder.h"
+#include "WebMDemuxer.h"
+#include "mozilla/AbstractThread.h"
+#include "mozilla/gtest/MozAssertions.h"
+#include "mozilla/SpinEventLoopUntil.h"
+#include "nsMimeTypes.h"
+
+using namespace mozilla;
+
+class BenchmarkRunner {
+ public:
+ explicit BenchmarkRunner(Benchmark* aBenchmark) : mBenchmark(aBenchmark) {}
+
+ uint32_t Run() {
+ bool done = false;
+ uint32_t result = 0;
+
+ mBenchmark->Init();
+ mBenchmark->Run()->Then(
+ // Non DocGroup-version of AbstractThread::MainThread() is fine for
+ // testing.
+ AbstractThread::MainThread(), __func__,
+ [&](uint32_t aDecodeFps) {
+ result = aDecodeFps;
+ done = true;
+ },
+ [&]() { done = true; });
+
+ // Wait until benchmark completes.
+ SpinEventLoopUntil("BenchmarkRunner::Run"_ns, [&]() { return done; });
+ return result;
+ }
+
+ private:
+ RefPtr<Benchmark> mBenchmark;
+};
+
+TEST(MediaDataDecoder, H264)
+{
+ if (!DecoderTraits::IsMP4SupportedType(
+ MediaContainerType(MEDIAMIMETYPE(VIDEO_MP4)),
+ /* DecoderDoctorDiagnostics* */ nullptr)) {
+ EXPECT_TRUE(true);
+ } else {
+ RefPtr<MockMediaResource> resource = new MockMediaResource("gizmo.mp4");
+ nsresult rv = resource->Open();
+ EXPECT_NS_SUCCEEDED(rv);
+
+ BenchmarkRunner runner(new Benchmark(new MP4Demuxer(resource)));
+ EXPECT_GT(runner.Run(), 0u);
+ }
+}
+
+// Decoding AV1 via. ffvpx is supported on Linux/Wayland only.
+#if defined(MOZ_AV1) && defined(MOZ_WAYLAND) && defined(MOZ_FFVPX) && \
+ !defined(MOZ_FFVPX_AUDIOONLY)
+TEST(MediaDataDecoder, AV1)
+{
+ if (!DecoderTraits::IsMP4SupportedType(
+ MediaContainerType(MEDIAMIMETYPE(VIDEO_MP4)),
+ /* DecoderDoctorDiagnostics* */ nullptr)) {
+ EXPECT_TRUE(true);
+ } else {
+ RefPtr<MockMediaResource> resource = new MockMediaResource("av1.mp4");
+ nsresult rv = resource->Open();
+ EXPECT_NS_SUCCEEDED(rv);
+
+ BenchmarkRunner runner(new Benchmark(new MP4Demuxer(resource)));
+ EXPECT_GT(runner.Run(), 0u);
+ }
+}
+#endif
+
+TEST(MediaDataDecoder, VP9)
+{
+ if (!WebMDecoder::IsSupportedType(
+ MediaContainerType(MEDIAMIMETYPE(VIDEO_WEBM)))) {
+ EXPECT_TRUE(true);
+ } else {
+ RefPtr<MockMediaResource> resource = new MockMediaResource("vp9cake.webm");
+ nsresult rv = resource->Open();
+ EXPECT_NS_SUCCEEDED(rv);
+
+ BenchmarkRunner runner(new Benchmark(new WebMDemuxer(resource)));
+ EXPECT_GT(runner.Run(), 0u);
+ }
+}
diff --git a/dom/media/gtest/TestMediaDataEncoder.cpp b/dom/media/gtest/TestMediaDataEncoder.cpp
new file mode 100644
index 0000000000..7bc975816f
--- /dev/null
+++ b/dom/media/gtest/TestMediaDataEncoder.cpp
@@ -0,0 +1,510 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+
+#include "AnnexB.h"
+#include "ImageContainer.h"
+#include "mozilla/AbstractThread.h"
+#include "mozilla/SpinEventLoopUntil.h"
+#include "mozilla/media/MediaUtils.h" // For media::Await
+#include "nsMimeTypes.h"
+#include "PEMFactory.h"
+#include "TimeUnits.h"
+#include "VideoUtils.h"
+#include "VPXDecoder.h"
+#include <algorithm>
+
+#include <fstream>
+
+#ifdef XP_WIN
+#include "mozilla/WindowsVersion.h"
+#endif
+
+#define RUN_IF_SUPPORTED(mimeType, test) \
+ do { \
+ if (!isWin7()) { \
+ RefPtr<PEMFactory> f(new PEMFactory()); \
+ if (f->SupportsMimeType(nsLiteralCString(mimeType))) { \
+ test(); \
+ } \
+ } \
+ } while (0)
+
+#define BLOCK_SIZE 64
+#define WIDTH 640
+#define HEIGHT 480
+#define NUM_FRAMES 150UL
+#define FRAME_RATE 30
+#define FRAME_DURATION (1000000 / FRAME_RATE)
+#define BIT_RATE (1000 * 1000) // 1Mbps
+#define KEYFRAME_INTERVAL FRAME_RATE // 1 keyframe per second
+#define VIDEO_VP8 "video/vp8"
+#define VIDEO_VP9 "video/vp9"
+
+using namespace mozilla;
+
+static gfx::IntSize kImageSize(WIDTH, HEIGHT);
+
+class MediaDataEncoderTest : public testing::Test {
+ protected:
+ void SetUp() override { mData.Init(kImageSize); }
+
+ void TearDown() override { mData.Deinit(); }
+
+ public:
+ struct FrameSource final {
+ layers::PlanarYCbCrData mYUV;
+ UniquePtr<uint8_t[]> mBuffer;
+ RefPtr<layers::BufferRecycleBin> mRecycleBin;
+ int16_t mColorStep = 4;
+
+ void Init(const gfx::IntSize& aSize) {
+ mYUV.mPictureRect = gfx::IntRect(0, 0, aSize.width, aSize.height);
+ mYUV.mYStride = aSize.width;
+ mYUV.mCbCrStride = (aSize.width + 1) / 2;
+ mYUV.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT;
+ auto ySize = mYUV.YDataSize();
+ auto cbcrSize = mYUV.CbCrDataSize();
+ size_t bufferSize =
+ mYUV.mYStride * ySize.height + 2 * mYUV.mCbCrStride * cbcrSize.height;
+ mBuffer = MakeUnique<uint8_t[]>(bufferSize);
+ std::fill_n(mBuffer.get(), bufferSize, 0x7F);
+ mYUV.mYChannel = mBuffer.get();
+ mYUV.mCbChannel = mYUV.mYChannel + mYUV.mYStride * ySize.height;
+ mYUV.mCrChannel = mYUV.mCbChannel + mYUV.mCbCrStride * cbcrSize.height;
+ mYUV.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT;
+ mRecycleBin = new layers::BufferRecycleBin();
+ }
+
+ void Deinit() {
+ mBuffer.reset();
+ mRecycleBin = nullptr;
+ }
+
+ already_AddRefed<MediaData> GetFrame(const size_t aIndex) {
+ Draw(aIndex);
+ RefPtr<layers::PlanarYCbCrImage> img =
+ new layers::RecyclingPlanarYCbCrImage(mRecycleBin);
+ img->CopyData(mYUV);
+ RefPtr<MediaData> frame = VideoData::CreateFromImage(
+ kImageSize, 0,
+ media::TimeUnit::FromMicroseconds(aIndex * FRAME_DURATION),
+ media::TimeUnit::FromMicroseconds(FRAME_DURATION), img,
+ (aIndex & 0xF) == 0,
+ media::TimeUnit::FromMicroseconds(aIndex * FRAME_DURATION));
+ return frame.forget();
+ }
+
+ void DrawChessboard(uint8_t* aAddr, const size_t aWidth,
+ const size_t aHeight, const size_t aOffset) {
+ uint8_t pixels[2][BLOCK_SIZE];
+ size_t x = aOffset % BLOCK_SIZE;
+ if ((aOffset / BLOCK_SIZE) & 1) {
+ x = BLOCK_SIZE - x;
+ }
+ for (size_t i = 0; i < x; i++) {
+ pixels[0][i] = 0x00;
+ pixels[1][i] = 0xFF;
+ }
+ for (size_t i = x; i < BLOCK_SIZE; i++) {
+ pixels[0][i] = 0xFF;
+ pixels[1][i] = 0x00;
+ }
+
+ uint8_t* p = aAddr;
+ for (size_t row = 0; row < aHeight; row++) {
+ for (size_t col = 0; col < aWidth; col += BLOCK_SIZE) {
+ memcpy(p, pixels[((row / BLOCK_SIZE) + (col / BLOCK_SIZE)) % 2],
+ BLOCK_SIZE);
+ p += BLOCK_SIZE;
+ }
+ }
+ }
+
+ void Draw(const size_t aIndex) {
+ auto ySize = mYUV.YDataSize();
+ DrawChessboard(mYUV.mYChannel, ySize.width, ySize.height, aIndex << 1);
+ int16_t color = mYUV.mCbChannel[0] + mColorStep;
+ if (color > 255 || color < 0) {
+ mColorStep = -mColorStep;
+ color = mYUV.mCbChannel[0] + mColorStep;
+ }
+
+ size_t size = (mYUV.mCrChannel - mYUV.mCbChannel);
+
+ std::fill_n(mYUV.mCbChannel, size, static_cast<uint8_t>(color));
+ std::fill_n(mYUV.mCrChannel, size, 0xFF - static_cast<uint8_t>(color));
+ }
+ };
+
+ public:
+ FrameSource mData;
+};
+
+template <typename T>
+already_AddRefed<MediaDataEncoder> CreateVideoEncoder(
+ const char* aMimeType, MediaDataEncoder::Usage aUsage,
+ MediaDataEncoder::PixelFormat aPixelFormat, int32_t aWidth, int32_t aHeight,
+ const Maybe<T>& aSpecific) {
+ RefPtr<PEMFactory> f(new PEMFactory());
+
+ if (!f->SupportsMimeType(nsCString(aMimeType))) {
+ return nullptr;
+ }
+
+ VideoInfo videoInfo(aWidth, aHeight);
+ videoInfo.mMimeType = nsCString(aMimeType);
+ const RefPtr<TaskQueue> taskQueue(
+ TaskQueue::Create(GetMediaThreadPool(MediaThreadType::PLATFORM_ENCODER),
+ "TestMediaDataEncoder"));
+
+ RefPtr<MediaDataEncoder> e;
+#ifdef MOZ_WIDGET_ANDROID
+ const bool hardwareNotAllowed = false;
+#else
+ const bool hardwareNotAllowed = true;
+#endif
+ if (aSpecific) {
+ e = f->CreateEncoder(
+ CreateEncoderParams(videoInfo /* track info */, aUsage, taskQueue,
+ aPixelFormat, FRAME_RATE /* FPS */,
+ KEYFRAME_INTERVAL /* keyframe interval */,
+ BIT_RATE /* bitrate */, aSpecific.value()),
+ hardwareNotAllowed);
+ } else {
+ e = f->CreateEncoder(
+ CreateEncoderParams(videoInfo /* track info */, aUsage, taskQueue,
+ aPixelFormat, FRAME_RATE /* FPS */,
+ KEYFRAME_INTERVAL /* keyframe interval */,
+ BIT_RATE /* bitrate */),
+ hardwareNotAllowed);
+ }
+
+ return e.forget();
+}
+
+static already_AddRefed<MediaDataEncoder> CreateH264Encoder(
+ MediaDataEncoder::Usage aUsage = MediaDataEncoder::Usage::Realtime,
+ MediaDataEncoder::PixelFormat aPixelFormat =
+ MediaDataEncoder::PixelFormat::YUV420P,
+ int32_t aWidth = WIDTH, int32_t aHeight = HEIGHT,
+ const Maybe<MediaDataEncoder::H264Specific>& aSpecific =
+ Some(MediaDataEncoder::H264Specific(
+ MediaDataEncoder::H264Specific::ProfileLevel::BaselineAutoLevel))) {
+ return CreateVideoEncoder(VIDEO_MP4, aUsage, aPixelFormat, aWidth, aHeight,
+ aSpecific);
+}
+
+void WaitForShutdown(RefPtr<MediaDataEncoder> aEncoder) {
+ MOZ_ASSERT(aEncoder);
+
+ Maybe<bool> result;
+ // media::Await() supports exclusive promises only, but ShutdownPromise is
+ // not.
+ aEncoder->Shutdown()->Then(
+ AbstractThread::MainThread(), __func__,
+ [&result](bool rv) {
+ EXPECT_TRUE(rv);
+ result = Some(true);
+ },
+ []() { FAIL() << "Shutdown should never be rejected"; });
+ SpinEventLoopUntil("TestMediaDataEncoder.cpp:WaitForShutdown"_ns,
+ [&result]() { return result; });
+}
+
+bool isWin7() {
+ #ifdef XP_WIN
+ if (!IsWin8OrLater()) {
+ return true;
+ }
+ #endif
+ return false;
+}
+
+TEST_F(MediaDataEncoderTest, H264Create) {
+ RUN_IF_SUPPORTED(VIDEO_MP4, []() {
+ RefPtr<MediaDataEncoder> e = CreateH264Encoder();
+ EXPECT_TRUE(e);
+ WaitForShutdown(e);
+ });
+}
+
+static bool EnsureInit(RefPtr<MediaDataEncoder> aEncoder) {
+ if (!aEncoder) {
+ return false;
+ }
+
+ bool succeeded;
+ media::Await(
+ GetMediaThreadPool(MediaThreadType::SUPERVISOR), aEncoder->Init(),
+ [&succeeded](TrackInfo::TrackType t) {
+ EXPECT_EQ(TrackInfo::TrackType::kVideoTrack, t);
+ succeeded = true;
+ },
+ [&succeeded](MediaResult r) { succeeded = false; });
+ return succeeded;
+}
+
+TEST_F(MediaDataEncoderTest, H264Inits) {
+ RUN_IF_SUPPORTED(VIDEO_MP4, []() {
+ // w/o codec specific.
+ RefPtr<MediaDataEncoder> e = CreateH264Encoder(
+ MediaDataEncoder::Usage::Realtime,
+ MediaDataEncoder::PixelFormat::YUV420P, WIDTH, HEIGHT, Nothing());
+ EXPECT_TRUE(EnsureInit(e));
+ WaitForShutdown(e);
+
+ // w/ codec specific
+ e = CreateH264Encoder();
+ EXPECT_TRUE(EnsureInit(e));
+ WaitForShutdown(e);
+ });
+}
+
+static MediaDataEncoder::EncodedData Encode(
+ const RefPtr<MediaDataEncoder> aEncoder, const size_t aNumFrames,
+ MediaDataEncoderTest::FrameSource& aSource) {
+ MediaDataEncoder::EncodedData output;
+ bool succeeded;
+ for (size_t i = 0; i < aNumFrames; i++) {
+ RefPtr<MediaData> frame = aSource.GetFrame(i);
+ media::Await(
+ GetMediaThreadPool(MediaThreadType::SUPERVISOR),
+ aEncoder->Encode(frame),
+ [&output, &succeeded](MediaDataEncoder::EncodedData encoded) {
+ output.AppendElements(std::move(encoded));
+ succeeded = true;
+ },
+ [&succeeded](MediaResult r) { succeeded = false; });
+ EXPECT_TRUE(succeeded);
+ if (!succeeded) {
+ return output;
+ }
+ }
+
+ size_t pending = 0;
+ do {
+ media::Await(
+ GetMediaThreadPool(MediaThreadType::SUPERVISOR), aEncoder->Drain(),
+ [&pending, &output, &succeeded](MediaDataEncoder::EncodedData encoded) {
+ pending = encoded.Length();
+ output.AppendElements(std::move(encoded));
+ succeeded = true;
+ },
+ [&succeeded](MediaResult r) { succeeded = false; });
+ EXPECT_TRUE(succeeded);
+ if (!succeeded) {
+ return output;
+ }
+ } while (pending > 0);
+
+ return output;
+}
+
+TEST_F(MediaDataEncoderTest, H264Encodes) {
+ RUN_IF_SUPPORTED(VIDEO_MP4, [this]() {
+ // Encode one frame and output in AnnexB format.
+ RefPtr<MediaDataEncoder> e = CreateH264Encoder();
+ EnsureInit(e);
+ MediaDataEncoder::EncodedData output = Encode(e, 1UL, mData);
+ EXPECT_EQ(output.Length(), 1UL);
+ EXPECT_TRUE(AnnexB::IsAnnexB(output[0]));
+ WaitForShutdown(e);
+
+ // Encode multiple frames and output in AnnexB format.
+ e = CreateH264Encoder();
+ EnsureInit(e);
+ output = Encode(e, NUM_FRAMES, mData);
+ EXPECT_EQ(output.Length(), NUM_FRAMES);
+ for (auto frame : output) {
+ EXPECT_TRUE(AnnexB::IsAnnexB(frame));
+ }
+ WaitForShutdown(e);
+
+ // Encode one frame and output in avcC format.
+ e = CreateH264Encoder(MediaDataEncoder::Usage::Record);
+ EnsureInit(e);
+ output = Encode(e, NUM_FRAMES, mData);
+ EXPECT_EQ(output.Length(), NUM_FRAMES);
+ AnnexB::IsAVCC(output[0]); // Only 1st frame has extra data.
+ for (auto frame : output) {
+ EXPECT_FALSE(AnnexB::IsAnnexB(frame));
+ }
+ WaitForShutdown(e);
+ });
+}
+
+#ifndef DEBUG // Zero width or height will assert/crash in debug builds.
+TEST_F(MediaDataEncoderTest, InvalidSize) {
+ RUN_IF_SUPPORTED(VIDEO_MP4, []() {
+ RefPtr<MediaDataEncoder> e0x0 =
+ CreateH264Encoder(MediaDataEncoder::Usage::Realtime,
+ MediaDataEncoder::PixelFormat::YUV420P, 0, 0);
+ EXPECT_NE(e0x0, nullptr);
+ EXPECT_FALSE(EnsureInit(e0x0));
+
+ RefPtr<MediaDataEncoder> e0x1 =
+ CreateH264Encoder(MediaDataEncoder::Usage::Realtime,
+ MediaDataEncoder::PixelFormat::YUV420P, 0, 1);
+ EXPECT_NE(e0x1, nullptr);
+ EXPECT_FALSE(EnsureInit(e0x1));
+
+ RefPtr<MediaDataEncoder> e1x0 =
+ CreateH264Encoder(MediaDataEncoder::Usage::Realtime,
+ MediaDataEncoder::PixelFormat::YUV420P, 1, 0);
+ EXPECT_NE(e1x0, nullptr);
+ EXPECT_FALSE(EnsureInit(e1x0));
+ });
+}
+#endif
+
+#ifdef MOZ_WIDGET_ANDROID
+TEST_F(MediaDataEncoderTest, AndroidNotSupportedSize) {
+ RUN_IF_SUPPORTED(VIDEO_MP4, []() {
+ RefPtr<MediaDataEncoder> e =
+ CreateH264Encoder(MediaDataEncoder::Usage::Realtime,
+ MediaDataEncoder::PixelFormat::YUV420P, 1, 1);
+ EXPECT_NE(e, nullptr);
+ EXPECT_FALSE(EnsureInit(e));
+ });
+}
+#endif
+
+static already_AddRefed<MediaDataEncoder> CreateVP8Encoder(
+ MediaDataEncoder::Usage aUsage = MediaDataEncoder::Usage::Realtime,
+ MediaDataEncoder::PixelFormat aPixelFormat =
+ MediaDataEncoder::PixelFormat::YUV420P,
+ int32_t aWidth = WIDTH, int32_t aHeight = HEIGHT,
+ const Maybe<MediaDataEncoder::VPXSpecific::VP8>& aSpecific =
+ Some(MediaDataEncoder::VPXSpecific::VP8())) {
+ return CreateVideoEncoder(VIDEO_VP8, aUsage, aPixelFormat, aWidth, aHeight,
+ aSpecific);
+}
+
+static already_AddRefed<MediaDataEncoder> CreateVP9Encoder(
+ MediaDataEncoder::Usage aUsage = MediaDataEncoder::Usage::Realtime,
+ MediaDataEncoder::PixelFormat aPixelFormat =
+ MediaDataEncoder::PixelFormat::YUV420P,
+ int32_t aWidth = WIDTH, int32_t aHeight = HEIGHT,
+ const Maybe<MediaDataEncoder::VPXSpecific::VP9>& aSpecific =
+ Some(MediaDataEncoder::VPXSpecific::VP9())) {
+ return CreateVideoEncoder(VIDEO_VP9, aUsage, aPixelFormat, aWidth, aHeight,
+ aSpecific);
+}
+
+TEST_F(MediaDataEncoderTest, VP8Create) {
+ RUN_IF_SUPPORTED(VIDEO_VP8, []() {
+ RefPtr<MediaDataEncoder> e = CreateVP8Encoder();
+ EXPECT_TRUE(e);
+ WaitForShutdown(e);
+ });
+}
+
+TEST_F(MediaDataEncoderTest, VP8Inits) {
+ RUN_IF_SUPPORTED(VIDEO_VP8, []() {
+ // w/o codec specific.
+ RefPtr<MediaDataEncoder> e = CreateVP8Encoder(
+ MediaDataEncoder::Usage::Realtime,
+ MediaDataEncoder::PixelFormat::YUV420P, WIDTH, HEIGHT, Nothing());
+ EXPECT_TRUE(EnsureInit(e));
+ WaitForShutdown(e);
+
+ // w/ codec specific
+ e = CreateVP8Encoder();
+ EXPECT_TRUE(EnsureInit(e));
+ WaitForShutdown(e);
+ });
+}
+
+TEST_F(MediaDataEncoderTest, VP8Encodes) {
+ RUN_IF_SUPPORTED(VIDEO_VP8, [this]() {
+ // Encode one VPX frame.
+ RefPtr<MediaDataEncoder> e = CreateVP8Encoder();
+ EnsureInit(e);
+ MediaDataEncoder::EncodedData output = Encode(e, 1UL, mData);
+ EXPECT_EQ(output.Length(), 1UL);
+ VPXDecoder::VPXStreamInfo info;
+ EXPECT_TRUE(
+ VPXDecoder::GetStreamInfo(*output[0], info, VPXDecoder::Codec::VP8));
+ EXPECT_EQ(info.mKeyFrame, output[0]->mKeyframe);
+ if (info.mKeyFrame) {
+ EXPECT_EQ(info.mImage, kImageSize);
+ }
+ WaitForShutdown(e);
+
+ // Encode multiple VPX frames.
+ e = CreateVP8Encoder();
+ EnsureInit(e);
+ output = Encode(e, NUM_FRAMES, mData);
+ EXPECT_EQ(output.Length(), NUM_FRAMES);
+ for (auto frame : output) {
+ VPXDecoder::VPXStreamInfo info;
+ EXPECT_TRUE(
+ VPXDecoder::GetStreamInfo(*frame, info, VPXDecoder::Codec::VP8));
+ EXPECT_EQ(info.mKeyFrame, frame->mKeyframe);
+ if (info.mKeyFrame) {
+ EXPECT_EQ(info.mImage, kImageSize);
+ }
+ }
+ WaitForShutdown(e);
+ });
+}
+
+TEST_F(MediaDataEncoderTest, VP9Create) {
+ RUN_IF_SUPPORTED(VIDEO_VP9, []() {
+ RefPtr<MediaDataEncoder> e = CreateVP9Encoder();
+ EXPECT_TRUE(e);
+ WaitForShutdown(e);
+ });
+}
+
+TEST_F(MediaDataEncoderTest, VP9Inits) {
+ RUN_IF_SUPPORTED(VIDEO_VP9, []() {
+ // w/o codec specific.
+ RefPtr<MediaDataEncoder> e = CreateVP9Encoder(
+ MediaDataEncoder::Usage::Realtime,
+ MediaDataEncoder::PixelFormat::YUV420P, WIDTH, HEIGHT, Nothing());
+ EXPECT_TRUE(EnsureInit(e));
+ WaitForShutdown(e);
+
+ // w/ codec specific
+ e = CreateVP9Encoder();
+ EXPECT_TRUE(EnsureInit(e));
+ WaitForShutdown(e);
+ });
+}
+
+TEST_F(MediaDataEncoderTest, VP9Encodes) {
+ RUN_IF_SUPPORTED(VIDEO_VP9, [this]() {
+ RefPtr<MediaDataEncoder> e = CreateVP9Encoder();
+ EnsureInit(e);
+ MediaDataEncoder::EncodedData output = Encode(e, 1UL, mData);
+ EXPECT_EQ(output.Length(), 1UL);
+ VPXDecoder::VPXStreamInfo info;
+ EXPECT_TRUE(
+ VPXDecoder::GetStreamInfo(*output[0], info, VPXDecoder::Codec::VP9));
+ EXPECT_EQ(info.mKeyFrame, output[0]->mKeyframe);
+ if (info.mKeyFrame) {
+ EXPECT_EQ(info.mImage, kImageSize);
+ }
+ WaitForShutdown(e);
+
+ e = CreateVP9Encoder();
+ EnsureInit(e);
+ output = Encode(e, NUM_FRAMES, mData);
+ EXPECT_EQ(output.Length(), NUM_FRAMES);
+ for (auto frame : output) {
+ VPXDecoder::VPXStreamInfo info;
+ EXPECT_TRUE(
+ VPXDecoder::GetStreamInfo(*frame, info, VPXDecoder::Codec::VP9));
+ EXPECT_EQ(info.mKeyFrame, frame->mKeyframe);
+ if (info.mKeyFrame) {
+ EXPECT_EQ(info.mImage, kImageSize);
+ }
+ }
+ WaitForShutdown(e);
+ });
+}
diff --git a/dom/media/gtest/TestMediaEventSource.cpp b/dom/media/gtest/TestMediaEventSource.cpp
new file mode 100644
index 0000000000..811e2bec9f
--- /dev/null
+++ b/dom/media/gtest/TestMediaEventSource.cpp
@@ -0,0 +1,490 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+
+#include "mozilla/SharedThreadPool.h"
+#include "mozilla/TaskQueue.h"
+#include "mozilla/UniquePtr.h"
+#include "MediaEventSource.h"
+#include "VideoUtils.h"
+
+using namespace mozilla;
+
+/*
+ * Test if listeners receive the event data correctly.
+ */
+TEST(MediaEventSource, SingleListener)
+{
+ RefPtr<TaskQueue> queue =
+ TaskQueue::Create(GetMediaThreadPool(MediaThreadType::SUPERVISOR),
+ "TestMediaEventSource SingleListener");
+
+ MediaEventProducer<int> source;
+ int i = 0;
+
+ auto func = [&](int j) { i += j; };
+ MediaEventListener listener = source.Connect(queue, func);
+
+ // Call Notify 3 times. The listener should be also called 3 times.
+ source.Notify(3);
+ source.Notify(5);
+ source.Notify(7);
+
+ queue->BeginShutdown();
+ queue->AwaitShutdownAndIdle();
+
+ // Verify the event data is passed correctly to the listener.
+ EXPECT_EQ(i, 15); // 3 + 5 + 7
+ listener.Disconnect();
+}
+
+TEST(MediaEventSource, MultiListener)
+{
+ RefPtr<TaskQueue> queue =
+ TaskQueue::Create(GetMediaThreadPool(MediaThreadType::SUPERVISOR),
+ "TestMediaEventSource MultiListener");
+
+ MediaEventProducer<int> source;
+ int i = 0;
+ int j = 0;
+
+ auto func1 = [&](int k) { i = k * 2; };
+ auto func2 = [&](int k) { j = k * 3; };
+ MediaEventListener listener1 = source.Connect(queue, func1);
+ MediaEventListener listener2 = source.Connect(queue, func2);
+
+ // Both listeners should receive the event.
+ source.Notify(11);
+
+ queue->BeginShutdown();
+ queue->AwaitShutdownAndIdle();
+
+ // Verify the event data is passed correctly to the listener.
+ EXPECT_EQ(i, 22); // 11 * 2
+ EXPECT_EQ(j, 33); // 11 * 3
+
+ listener1.Disconnect();
+ listener2.Disconnect();
+}
+
+/*
+ * Test if disconnecting a listener prevents events from coming.
+ */
+TEST(MediaEventSource, DisconnectAfterNotification)
+{
+ RefPtr<TaskQueue> queue =
+ TaskQueue::Create(GetMediaThreadPool(MediaThreadType::SUPERVISOR),
+ "TestMediaEventSource DisconnectAfterNotification");
+
+ MediaEventProducer<int> source;
+ int i = 0;
+
+ MediaEventListener listener;
+ auto func = [&](int j) {
+ i += j;
+ listener.Disconnect();
+ };
+ listener = source.Connect(queue, func);
+
+ // Call Notify() twice. Since we disconnect the listener when receiving
+ // the 1st event, the 2nd event should not reach the listener.
+ source.Notify(11);
+ source.Notify(11);
+
+ queue->BeginShutdown();
+ queue->AwaitShutdownAndIdle();
+
+ // Check only the 1st event is received.
+ EXPECT_EQ(i, 11);
+}
+
+TEST(MediaEventSource, DisconnectBeforeNotification)
+{
+ RefPtr<TaskQueue> queue =
+ TaskQueue::Create(GetMediaThreadPool(MediaThreadType::SUPERVISOR),
+ "TestMediaEventSource DisconnectBeforeNotification");
+
+ MediaEventProducer<int> source;
+ int i = 0;
+ int j = 0;
+
+ auto func1 = [&](int k) { i = k * 2; };
+ auto func2 = [&](int k) { j = k * 3; };
+ MediaEventListener listener1 = source.Connect(queue, func1);
+ MediaEventListener listener2 = source.Connect(queue, func2);
+
+ // Disconnect listener2 before notification. Only listener1 should receive
+ // the event.
+ listener2.Disconnect();
+ source.Notify(11);
+
+ queue->BeginShutdown();
+ queue->AwaitShutdownAndIdle();
+
+ EXPECT_EQ(i, 22); // 11 * 2
+ EXPECT_EQ(j, 0); // event not received
+
+ listener1.Disconnect();
+}
+
+/*
+ * Test we don't hit the assertion when calling Connect() and Disconnect()
+ * repeatedly.
+ */
+TEST(MediaEventSource, DisconnectAndConnect)
+{
+ RefPtr<TaskQueue> queue =
+ TaskQueue::Create(GetMediaThreadPool(MediaThreadType::SUPERVISOR),
+ "TestMediaEventSource DisconnectAndConnect");
+
+ MediaEventProducerExc<int> source;
+ MediaEventListener listener = source.Connect(queue, []() {});
+ listener.Disconnect();
+ listener = source.Connect(queue, []() {});
+ listener.Disconnect();
+}
+
+/*
+ * Test void event type.
+ */
+TEST(MediaEventSource, VoidEventType)
+{
+ RefPtr<TaskQueue> queue =
+ TaskQueue::Create(GetMediaThreadPool(MediaThreadType::SUPERVISOR),
+ "TestMediaEventSource VoidEventType");
+
+ MediaEventProducer<void> source;
+ int i = 0;
+
+ // Test function object.
+ auto func = [&]() { ++i; };
+ MediaEventListener listener1 = source.Connect(queue, func);
+
+ // Test member function.
+ struct Foo {
+ Foo() : j(1) {}
+ void OnNotify() { j *= 2; }
+ int j;
+ } foo;
+ MediaEventListener listener2 = source.Connect(queue, &foo, &Foo::OnNotify);
+
+ // Call Notify 2 times. The listener should be also called 2 times.
+ source.Notify();
+ source.Notify();
+
+ queue->BeginShutdown();
+ queue->AwaitShutdownAndIdle();
+
+ // Verify the event data is passed correctly to the listener.
+ EXPECT_EQ(i, 2); // ++i called twice
+ EXPECT_EQ(foo.j, 4); // |j *= 2| called twice
+ listener1.Disconnect();
+ listener2.Disconnect();
+}
+
+/*
+ * Test listeners can take various event types (T, T&&, const T& and void).
+ */
+TEST(MediaEventSource, ListenerType1)
+{
+ RefPtr<TaskQueue> queue =
+ TaskQueue::Create(GetMediaThreadPool(MediaThreadType::SUPERVISOR),
+ "TestMediaEventSource ListenerType1");
+
+ MediaEventProducer<int> source;
+ int i = 0;
+
+ // Test various argument types.
+ auto func1 = [&](int&& j) { i += j; };
+ auto func2 = [&](const int& j) { i += j; };
+ auto func3 = [&]() { i += 1; };
+ MediaEventListener listener1 = source.Connect(queue, func1);
+ MediaEventListener listener2 = source.Connect(queue, func2);
+ MediaEventListener listener3 = source.Connect(queue, func3);
+
+ source.Notify(1);
+
+ queue->BeginShutdown();
+ queue->AwaitShutdownAndIdle();
+
+ EXPECT_EQ(i, 3);
+
+ listener1.Disconnect();
+ listener2.Disconnect();
+ listener3.Disconnect();
+}
+
+TEST(MediaEventSource, ListenerType2)
+{
+ RefPtr<TaskQueue> queue =
+ TaskQueue::Create(GetMediaThreadPool(MediaThreadType::SUPERVISOR),
+ "TestMediaEventSource ListenerType2");
+
+ MediaEventProducer<int> source;
+
+ struct Foo {
+ Foo() : mInt(0) {}
+ void OnNotify1(int&& i) { mInt += i; }
+ void OnNotify2(const int& i) { mInt += i; }
+ void OnNotify3() { mInt += 1; }
+ void OnNotify4(int i) const { mInt += i; }
+ void OnNotify5(int i) volatile { mInt = mInt + i; }
+ mutable int mInt;
+ } foo;
+
+ // Test member functions which might be CV qualified.
+ MediaEventListener listener1 = source.Connect(queue, &foo, &Foo::OnNotify1);
+ MediaEventListener listener2 = source.Connect(queue, &foo, &Foo::OnNotify2);
+ MediaEventListener listener3 = source.Connect(queue, &foo, &Foo::OnNotify3);
+ MediaEventListener listener4 = source.Connect(queue, &foo, &Foo::OnNotify4);
+ MediaEventListener listener5 = source.Connect(queue, &foo, &Foo::OnNotify5);
+
+ source.Notify(1);
+
+ queue->BeginShutdown();
+ queue->AwaitShutdownAndIdle();
+
+ EXPECT_EQ(foo.mInt, 5);
+
+ listener1.Disconnect();
+ listener2.Disconnect();
+ listener3.Disconnect();
+ listener4.Disconnect();
+ listener5.Disconnect();
+}
+
+struct SomeEvent {
+ explicit SomeEvent(int& aCount) : mCount(aCount) {}
+ // Increment mCount when copy constructor is called to know how many times
+ // the event data is copied.
+ SomeEvent(const SomeEvent& aOther) : mCount(aOther.mCount) { ++mCount; }
+ SomeEvent(SomeEvent&& aOther) : mCount(aOther.mCount) {}
+ int& mCount;
+};
+
+/*
+ * Test we don't have unnecessary copies of the event data.
+ */
+TEST(MediaEventSource, CopyEvent1)
+{
+ RefPtr<TaskQueue> queue =
+ TaskQueue::Create(GetMediaThreadPool(MediaThreadType::SUPERVISOR),
+ "TestMediaEventSource CopyEvent1");
+
+ MediaEventProducer<SomeEvent> source;
+ int i = 0;
+
+ auto func = [](SomeEvent&& aEvent) {};
+ struct Foo {
+ void OnNotify(SomeEvent&& aEvent) {}
+ } foo;
+
+ MediaEventListener listener1 = source.Connect(queue, func);
+ MediaEventListener listener2 = source.Connect(queue, &foo, &Foo::OnNotify);
+
+ // We expect i to be 2 since SomeEvent should be copied only once when
+ // passing to each listener.
+ source.Notify(SomeEvent(i));
+
+ queue->BeginShutdown();
+ queue->AwaitShutdownAndIdle();
+ EXPECT_EQ(i, 2);
+ listener1.Disconnect();
+ listener2.Disconnect();
+}
+
+TEST(MediaEventSource, CopyEvent2)
+{
+ RefPtr<TaskQueue> queue =
+ TaskQueue::Create(GetMediaThreadPool(MediaThreadType::SUPERVISOR),
+ "TestMediaEventSource CopyEvent2");
+
+ MediaEventProducer<SomeEvent> source;
+ int i = 0;
+
+ auto func = []() {};
+ struct Foo {
+ void OnNotify() {}
+ } foo;
+
+ MediaEventListener listener1 = source.Connect(queue, func);
+ MediaEventListener listener2 = source.Connect(queue, &foo, &Foo::OnNotify);
+
+ // SomeEvent won't be copied at all since the listeners take no arguments.
+ source.Notify(SomeEvent(i));
+
+ queue->BeginShutdown();
+ queue->AwaitShutdownAndIdle();
+ EXPECT_EQ(i, 0);
+ listener1.Disconnect();
+ listener2.Disconnect();
+}
+
+/*
+ * Test move-only types.
+ */
+TEST(MediaEventSource, MoveOnly)
+{
+ RefPtr<TaskQueue> queue =
+ TaskQueue::Create(GetMediaThreadPool(MediaThreadType::SUPERVISOR),
+ "TestMediaEventSource MoveOnly");
+
+ MediaEventProducerExc<UniquePtr<int>> source;
+
+ auto func = [](UniquePtr<int>&& aEvent) { EXPECT_EQ(*aEvent, 20); };
+ MediaEventListener listener = source.Connect(queue, func);
+
+ // It is OK to pass an rvalue which is move-only.
+ source.Notify(UniquePtr<int>(new int(20)));
+ // It is an error to pass an lvalue which is move-only.
+ // UniquePtr<int> event(new int(30));
+ // source.Notify(event);
+
+ queue->BeginShutdown();
+ queue->AwaitShutdownAndIdle();
+ listener.Disconnect();
+}
+
+struct RefCounter {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RefCounter)
+ explicit RefCounter(int aVal) : mVal(aVal) {}
+ int mVal;
+
+ private:
+ ~RefCounter() = default;
+};
+
+/*
+ * Test we should copy instead of move in NonExclusive mode
+ * for each listener must get a copy.
+ */
+TEST(MediaEventSource, NoMove)
+{
+ RefPtr<TaskQueue> queue =
+ TaskQueue::Create(GetMediaThreadPool(MediaThreadType::SUPERVISOR),
+ "TestMediaEventSource NoMove");
+
+ MediaEventProducer<RefPtr<RefCounter>> source;
+
+ auto func1 = [](RefPtr<RefCounter>&& aEvent) { EXPECT_EQ(aEvent->mVal, 20); };
+ auto func2 = [](RefPtr<RefCounter>&& aEvent) { EXPECT_EQ(aEvent->mVal, 20); };
+ MediaEventListener listener1 = source.Connect(queue, func1);
+ MediaEventListener listener2 = source.Connect(queue, func2);
+
+ // We should copy this rvalue instead of move it in NonExclusive mode.
+ RefPtr<RefCounter> val = new RefCounter(20);
+ source.Notify(std::move(val));
+
+ queue->BeginShutdown();
+ queue->AwaitShutdownAndIdle();
+ listener1.Disconnect();
+ listener2.Disconnect();
+}
+
+/*
+ * Rvalue lambda should be moved instead of copied.
+ */
+TEST(MediaEventSource, MoveLambda)
+{
+ RefPtr<TaskQueue> queue =
+ TaskQueue::Create(GetMediaThreadPool(MediaThreadType::SUPERVISOR),
+ "TestMediaEventSource MoveLambda");
+
+ MediaEventProducer<void> source;
+
+ int counter = 0;
+ SomeEvent someEvent(counter);
+
+ auto func = [someEvent]() {};
+ // someEvent is copied when captured by the lambda.
+ EXPECT_EQ(someEvent.mCount, 1);
+
+ // someEvent should be copied for we pass |func| as an lvalue.
+ MediaEventListener listener1 = source.Connect(queue, func);
+ EXPECT_EQ(someEvent.mCount, 2);
+
+ // someEvent should be moved for we pass |func| as an rvalue.
+ MediaEventListener listener2 = source.Connect(queue, std::move(func));
+ EXPECT_EQ(someEvent.mCount, 2);
+
+ listener1.Disconnect();
+ listener2.Disconnect();
+}
+
+template <typename Bool>
+struct DestroyChecker {
+ explicit DestroyChecker(Bool* aIsDestroyed) : mIsDestroyed(aIsDestroyed) {
+ EXPECT_FALSE(*mIsDestroyed);
+ }
+ ~DestroyChecker() {
+ EXPECT_FALSE(*mIsDestroyed);
+ *mIsDestroyed = true;
+ }
+
+ private:
+ Bool* const mIsDestroyed;
+};
+
+class ClassForDestroyCheck final : private DestroyChecker<bool> {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ClassForDestroyCheck);
+
+ explicit ClassForDestroyCheck(bool* aIsDestroyed)
+ : DestroyChecker(aIsDestroyed) {}
+
+ int32_t RefCountNums() const { return mRefCnt; }
+
+ protected:
+ ~ClassForDestroyCheck() = default;
+};
+
+TEST(MediaEventSource, ResetFuncReferenceAfterDisconnect)
+{
+ const RefPtr<TaskQueue> queue = TaskQueue::Create(
+ GetMediaThreadPool(MediaThreadType::SUPERVISOR),
+ "TestMediaEventSource ResetFuncReferenceAfterDisconnect");
+ MediaEventProducer<void> source;
+
+ // Using a class that supports refcounting to check the object destruction.
+ bool isDestroyed = false;
+ auto object = MakeRefPtr<ClassForDestroyCheck>(&isDestroyed);
+ EXPECT_FALSE(isDestroyed);
+ EXPECT_EQ(object->RefCountNums(), 1);
+
+ // Function holds a strong reference to object.
+ MediaEventListener listener = source.Connect(queue, [ptr = object] {});
+ EXPECT_FALSE(isDestroyed);
+ EXPECT_EQ(object->RefCountNums(), 2);
+
+ // This should destroy the function and release the object reference from the
+ // function on the task queue,
+ listener.Disconnect();
+ queue->BeginShutdown();
+ queue->AwaitShutdownAndIdle();
+ EXPECT_FALSE(isDestroyed);
+ EXPECT_EQ(object->RefCountNums(), 1);
+
+ // No one is holding reference to object, it should be destroyed
+ // immediately.
+ object = nullptr;
+ EXPECT_TRUE(isDestroyed);
+}
+
+TEST(MediaEventSource, ResetTargetAfterDisconnect)
+{
+ RefPtr<TaskQueue> queue =
+ TaskQueue::Create(GetMediaThreadPool(MediaThreadType::SUPERVISOR),
+ "TestMediaEventSource ResetTargetAfterDisconnect");
+ MediaEventProducer<void> source;
+ MediaEventListener listener = source.Connect(queue, [] {});
+
+ // MediaEventListener::Disconnect eventually gives up its target
+ listener.Disconnect();
+ queue->AwaitIdle();
+
+ // `queue` should be the last reference to the TaskQueue, meaning that this
+ // Release destroys it.
+ EXPECT_EQ(queue.forget().take()->Release(), 0u);
+}
diff --git a/dom/media/gtest/TestMediaMIMETypes.cpp b/dom/media/gtest/TestMediaMIMETypes.cpp
new file mode 100644
index 0000000000..d36e3bf586
--- /dev/null
+++ b/dom/media/gtest/TestMediaMIMETypes.cpp
@@ -0,0 +1,284 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "MediaMIMETypes.h"
+#include "mozilla/Unused.h"
+
+using namespace mozilla;
+
+TEST(MediaMIMETypes, DependentMIMEType)
+{
+ static const struct {
+ const char* mString;
+ DependentMediaMIMEType mDependentMediaMIMEType;
+ } tests[] = {{"audio/mp4", MEDIAMIMETYPE("audio/mp4")},
+ {"video/mp4", MEDIAMIMETYPE("video/mp4")},
+ {"application/x-mp4", MEDIAMIMETYPE("application/x-mp4")}};
+ for (const auto& test : tests) {
+ EXPECT_TRUE(test.mDependentMediaMIMEType.AsDependentString().EqualsASCII(
+ test.mString));
+ MediaMIMEType mimetype(test.mDependentMediaMIMEType);
+ EXPECT_TRUE(mimetype.AsString().Equals(
+ test.mDependentMediaMIMEType.AsDependentString()));
+ EXPECT_EQ(mimetype, test.mDependentMediaMIMEType);
+ EXPECT_EQ(mimetype, MediaMIMEType(test.mDependentMediaMIMEType));
+ }
+}
+
+TEST(MediaMIMETypes, MakeMediaMIMEType_bad)
+{
+ static const char* tests[] = {"", " ", "/", "audio",
+ "audio/", "mp4", "/mp4", "a/b"};
+
+ for (const auto& test : tests) {
+ Maybe<MediaMIMEType> type = MakeMediaMIMEType(test);
+ EXPECT_TRUE(type.isNothing())
+ << "MakeMediaMIMEType(\"" << test << "\").isNothing()";
+ }
+}
+
+TEST(MediaMIMETypes, MediaMIMEType)
+{
+ static const struct {
+ const char* mTypeString;
+ const char* mAsString;
+ bool mApplication;
+ bool mAudio;
+ bool mVideo;
+ bool mEqualsLiteralVideoSlashMp4; // tests `== "video/mp4"`
+ } tests[] = {
+ // in AsString app audio video ==v/mp4
+ {"video/mp4", "video/mp4", false, false, true, true},
+ {"video/mp4; codecs=0", "video/mp4", false, false, true, true},
+ {"VIDEO/MP4", "video/mp4", false, false, true, true},
+ {"audio/mp4", "audio/mp4", false, true, false, false},
+ {"application/x", "application/x", true, false, false, false}};
+
+ for (const auto& test : tests) {
+ Maybe<MediaMIMEType> type = MakeMediaMIMEType(test.mTypeString);
+ EXPECT_TRUE(type.isSome())
+ << "MakeMediaMIMEType(\"" << test.mTypeString << "\").isSome()";
+ EXPECT_TRUE(type->AsString().EqualsASCII(test.mAsString))
+ << "MakeMediaMIMEType(\"" << test.mTypeString << "\")->AsString() == \""
+ << test.mAsString << "\"";
+ EXPECT_EQ(test.mApplication, type->HasApplicationMajorType())
+ << "MakeMediaMIMEType(\"" << test.mTypeString
+ << "\")->HasApplicationMajorType() == "
+ << (test.mApplication ? "true" : "false");
+ EXPECT_EQ(test.mAudio, type->HasAudioMajorType())
+ << "MakeMediaMIMEType(\"" << test.mTypeString
+ << "\")->HasAudioMajorType() == " << (test.mAudio ? "true" : "false");
+ EXPECT_EQ(test.mVideo, type->HasVideoMajorType())
+ << "MakeMediaMIMEType(\"" << test.mTypeString
+ << "\")->HasVideoMajorType() == " << (test.mVideo ? "true" : "false");
+ EXPECT_EQ(test.mEqualsLiteralVideoSlashMp4,
+ *type == MEDIAMIMETYPE("video/mp4"))
+ << "*MakeMediaMIMEType(\"" << test.mTypeString
+ << "\") == MEDIAMIMETYPE(\"video/mp4\")";
+ }
+}
+
+TEST(MediaMIMETypes, MediaCodecs)
+{
+ MediaCodecs empty("");
+ EXPECT_TRUE(empty.IsEmpty());
+ EXPECT_TRUE(empty.AsString().EqualsLiteral(""));
+ EXPECT_FALSE(empty.Contains(u""_ns));
+ EXPECT_FALSE(empty.Contains(u"c1"_ns));
+ EXPECT_FALSE(empty.ContainsPrefix(u""_ns));
+ EXPECT_FALSE(empty.ContainsPrefix(u"c1"_ns));
+ int iterations = 0;
+ for (const auto& codec : empty.Range()) {
+ ++iterations;
+ Unused << codec;
+ }
+ EXPECT_EQ(0, iterations);
+
+ MediaCodecs space(" ");
+ EXPECT_FALSE(space.IsEmpty());
+ EXPECT_TRUE(space.AsString().EqualsLiteral(" "));
+ EXPECT_TRUE(space.Contains(u""_ns));
+ EXPECT_FALSE(space.Contains(u"c1"_ns));
+ EXPECT_TRUE(space.ContainsPrefix(u""_ns));
+ EXPECT_FALSE(space.ContainsPrefix(u"c"_ns));
+ EXPECT_FALSE(space.ContainsPrefix(u"c1"_ns));
+ iterations = 0;
+ for (const auto& codec : space.Range()) {
+ ++iterations;
+ EXPECT_TRUE(codec.IsEmpty());
+ }
+ EXPECT_EQ(1, iterations);
+
+ MediaCodecs one(" c1 ");
+ EXPECT_FALSE(one.IsEmpty());
+ EXPECT_TRUE(one.AsString().EqualsLiteral(" c1 "));
+ EXPECT_FALSE(one.Contains(u""_ns));
+ EXPECT_TRUE(one.Contains(u"c1"_ns));
+ EXPECT_TRUE(one.ContainsPrefix(u""_ns));
+ EXPECT_TRUE(one.ContainsPrefix(u"c"_ns));
+ EXPECT_TRUE(one.ContainsPrefix(u"c1"_ns));
+ EXPECT_FALSE(one.ContainsPrefix(u"c1x"_ns));
+ EXPECT_FALSE(one.ContainsPrefix(u"c1 "_ns));
+ iterations = 0;
+ for (const auto& codec : one.Range()) {
+ ++iterations;
+ EXPECT_TRUE(codec.EqualsLiteral("c1"));
+ }
+ EXPECT_EQ(1, iterations);
+
+ MediaCodecs two(" c1 , c2 ");
+ EXPECT_FALSE(two.IsEmpty());
+ EXPECT_TRUE(two.AsString().EqualsLiteral(" c1 , c2 "));
+ EXPECT_FALSE(two.Contains(u""_ns));
+ EXPECT_TRUE(two.Contains(u"c1"_ns));
+ EXPECT_TRUE(two.Contains(u"c2"_ns));
+ EXPECT_TRUE(two.ContainsPrefix(u""_ns));
+ EXPECT_TRUE(two.ContainsPrefix(u"c"_ns));
+ EXPECT_FALSE(two.ContainsPrefix(u"1"_ns));
+ EXPECT_TRUE(two.ContainsPrefix(u"c1"_ns));
+ EXPECT_TRUE(two.ContainsPrefix(u"c2"_ns));
+ EXPECT_FALSE(two.ContainsPrefix(u"c1x"_ns));
+ EXPECT_FALSE(two.ContainsPrefix(u"c2x"_ns));
+ iterations = 0;
+ for (const auto& codec : two.Range()) {
+ ++iterations;
+ char buffer[] = "c0";
+ buffer[1] += iterations;
+ EXPECT_TRUE(codec.EqualsASCII(buffer));
+ }
+ EXPECT_EQ(2, iterations);
+
+ EXPECT_TRUE(two.ContainsAll(two));
+ EXPECT_TRUE(two.ContainsAll(one));
+ EXPECT_FALSE(one.ContainsAll(two));
+
+ // Check wide char case where both octets/bytes are relevant. Note we don't
+ // use `EqualsLiteral` here because at the time of writing it will place the
+ // literal into a narrow string which then doesn't compare correctly with
+ // the wide representation from MediaCodecs.
+ MediaCodecs euroSign(" € "); // U+20AC
+ EXPECT_FALSE(euroSign.IsEmpty());
+ EXPECT_TRUE(euroSign.AsString().Equals(u" € "_ns));
+ EXPECT_FALSE(euroSign.Contains(u""_ns));
+ EXPECT_TRUE(euroSign.Contains(u"€"_ns));
+ EXPECT_FALSE(euroSign.Contains(u"€€"_ns));
+ EXPECT_TRUE(euroSign.ContainsPrefix(u""_ns));
+ EXPECT_TRUE(euroSign.ContainsPrefix(u"€"_ns));
+ EXPECT_FALSE(euroSign.ContainsPrefix(
+ u"₭"_ns)); // U+20AD -- ensure second octet is compared
+ EXPECT_FALSE(euroSign.ContainsPrefix(
+ u"↬"_ns)); // U+21AC -- ensure first octet is compared
+ EXPECT_FALSE(euroSign.ContainsPrefix(u"€ "_ns));
+ iterations = 0;
+ for (const auto& codec : euroSign.Range()) {
+ ++iterations;
+ EXPECT_TRUE(codec.Equals(u"€"_ns));
+ }
+ EXPECT_EQ(1, iterations);
+}
+
+TEST(MediaMIMETypes, MakeMediaExtendedMIMEType_bad)
+{
+ static const char* tests[] = {"", " ", "/", "audio",
+ "audio/", "mp4", "/mp4", "a/b"};
+
+ for (const auto& test : tests) {
+ Maybe<MediaExtendedMIMEType> type = MakeMediaExtendedMIMEType(test);
+ EXPECT_TRUE(type.isNothing())
+ << "MakeMediaExtendedMIMEType(\"" << test << "\").isNothing()";
+ }
+}
+
+TEST(MediaMIMETypes, MediaExtendedMIMEType)
+{
+ // Some generic tests first.
+ static const struct {
+ const char* mTypeString;
+ const char* mTypeAsString;
+ bool mApplication;
+ bool mAudio;
+ bool mVideo;
+ bool mEqualsLiteralVideoSlashMp4; // tests `== "video/mp4"`
+ bool mHaveCodecs;
+ } tests[] = {
+ // in Type().AsString app audio video ==v/mp4
+ // codecs
+ {"video/mp4", "video/mp4", false, false, true, true, false},
+ {"video/mp4; codecs=0", "video/mp4", false, false, true, true, true},
+ {"VIDEO/MP4", "video/mp4", false, false, true, true, false},
+ {"audio/mp4", "audio/mp4", false, true, false, false, false},
+ {"video/webm", "video/webm", false, false, true, false, false},
+ {"audio/webm", "audio/webm", false, true, false, false, false},
+ {"application/x", "application/x", true, false, false, false, false}};
+
+ for (const auto& test : tests) {
+ Maybe<MediaExtendedMIMEType> type =
+ MakeMediaExtendedMIMEType(test.mTypeString);
+ EXPECT_TRUE(type.isSome())
+ << "MakeMediaExtendedMIMEType(\"" << test.mTypeString << "\").isSome()";
+ EXPECT_TRUE(type->OriginalString().EqualsASCII(test.mTypeString))
+ << "MakeMediaExtendedMIMEType(\"" << test.mTypeString
+ << "\")->AsString() == \"" << test.mTypeAsString << "\"";
+ EXPECT_TRUE(type->Type().AsString().EqualsASCII(test.mTypeAsString))
+ << "MakeMediaExtendedMIMEType(\"" << test.mTypeString
+ << "\")->AsString() == \"" << test.mTypeAsString << "\"";
+ EXPECT_EQ(test.mApplication, type->Type().HasApplicationMajorType())
+ << "MakeMediaExtendedMIMEType(\"" << test.mTypeString
+ << "\")->Type().HasApplicationMajorType() == "
+ << (test.mApplication ? "true" : "false");
+ EXPECT_EQ(test.mAudio, type->Type().HasAudioMajorType())
+ << "MakeMediaExtendedMIMEType(\"" << test.mTypeString
+ << "\")->Type().HasAudioMajorType() == "
+ << (test.mAudio ? "true" : "false");
+ EXPECT_EQ(test.mVideo, type->Type().HasVideoMajorType())
+ << "MakeMediaExtendedMIMEType(\"" << test.mTypeString
+ << "\")->Type().HasVideoMajorType() == "
+ << (test.mVideo ? "true" : "false");
+ EXPECT_EQ(test.mEqualsLiteralVideoSlashMp4,
+ type->Type() == MEDIAMIMETYPE("video/mp4"))
+ << "*MakeMediaExtendedMIMEType(\"" << test.mTypeString
+ << "\")->Type() == MEDIAMIMETYPE(\"video/mp4\")";
+ EXPECT_EQ(test.mHaveCodecs, type->HaveCodecs())
+ << "MakeMediaExtendedMIMEType(\"" << test.mTypeString
+ << "\")->HaveCodecs() == " << (test.mHaveCodecs ? "true" : "false");
+ EXPECT_NE(test.mHaveCodecs, type->Codecs().IsEmpty())
+ << "MakeMediaExtendedMIMEType(\"" << test.mTypeString
+ << "\")->Codecs.IsEmpty() != " << (test.mHaveCodecs ? "true" : "false");
+ EXPECT_FALSE(type->GetWidth()) << "MakeMediaExtendedMIMEType(\""
+ << test.mTypeString << "\")->GetWidth()";
+ EXPECT_FALSE(type->GetHeight()) << "MakeMediaExtendedMIMEType(\""
+ << test.mTypeString << "\")->GetHeight()";
+ EXPECT_FALSE(type->GetFramerate())
+ << "MakeMediaExtendedMIMEType(\"" << test.mTypeString
+ << "\")->GetFramerate()";
+ EXPECT_FALSE(type->GetBitrate()) << "MakeMediaExtendedMIMEType(\""
+ << test.mTypeString << "\")->GetBitrate()";
+ }
+
+ // Test all extra parameters.
+ Maybe<MediaExtendedMIMEType> type = MakeMediaExtendedMIMEType(
+ "video/mp4; codecs=\"a,b\"; width=1024; Height=768; FrameRate=60; "
+ "BITRATE=100000");
+ EXPECT_TRUE(type->HaveCodecs());
+ EXPECT_FALSE(type->Codecs().IsEmpty());
+ EXPECT_TRUE(type->Codecs().AsString().EqualsASCII("a,b"));
+ EXPECT_TRUE(type->Codecs() == "a,b");
+ EXPECT_TRUE(type->Codecs().Contains(u"a"_ns));
+ EXPECT_TRUE(type->Codecs().Contains(u"b"_ns));
+ EXPECT_TRUE(type->Codecs().ContainsPrefix(u"a"_ns));
+ EXPECT_TRUE(type->Codecs().ContainsPrefix(u"b"_ns));
+ EXPECT_FALSE(type->Codecs().ContainsPrefix(u"ab"_ns));
+ EXPECT_FALSE(type->Codecs().ContainsPrefix(u"ba"_ns));
+ EXPECT_FALSE(type->Codecs().ContainsPrefix(u"a,b"_ns));
+ EXPECT_TRUE(!!type->GetWidth());
+ EXPECT_EQ(1024, *type->GetWidth());
+ EXPECT_TRUE(!!type->GetHeight());
+ EXPECT_EQ(768, *type->GetHeight());
+ EXPECT_TRUE(!!type->GetFramerate());
+ EXPECT_EQ(60, *type->GetFramerate());
+ EXPECT_TRUE(!!type->GetBitrate());
+ EXPECT_EQ(100000, *type->GetBitrate());
+}
diff --git a/dom/media/gtest/TestMediaQueue.cpp b/dom/media/gtest/TestMediaQueue.cpp
new file mode 100644
index 0000000000..5b049dc7fe
--- /dev/null
+++ b/dom/media/gtest/TestMediaQueue.cpp
@@ -0,0 +1,288 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <gtest/gtest.h>
+
+#include "MediaData.h"
+#include "MediaQueue.h"
+
+using namespace mozilla;
+using mozilla::media::TimeUnit;
+
+MediaData* CreateDataRawPtr(
+ int64_t aStartTime, int64_t aEndTime,
+ MediaData::Type aType = MediaData::Type::NULL_DATA) {
+ const TimeUnit startTime = TimeUnit::FromMicroseconds(aStartTime);
+ const TimeUnit endTime = TimeUnit::FromMicroseconds(aEndTime);
+ MediaData* data;
+ if (aType == MediaData::Type::AUDIO_DATA) {
+ AlignedAudioBuffer samples;
+ data = new AudioData(0, startTime, std::move(samples), 2, 44100);
+ data->mDuration = endTime - startTime;
+ } else if (aType == MediaData::Type::VIDEO_DATA) {
+ data = new VideoData(0, startTime, endTime - startTime, true, startTime,
+ gfx::IntSize(), 0);
+ } else {
+ data = new NullData(0, startTime, endTime - startTime);
+ }
+ return data;
+}
+
+already_AddRefed<MediaData> CreateData(int64_t aStartTime, int64_t aEndTime) {
+ RefPtr<MediaData> data = CreateDataRawPtr(aStartTime, aEndTime);
+ return data.forget();
+}
+
+// Used to avoid the compile error `comparison of integers of different signs`
+// when comparing 'const unsigned long' and 'const int'.
+#define EXPECT_EQUAL_SIZE_T(lhs, rhs) EXPECT_EQ(size_t(lhs), size_t(rhs))
+
+TEST(MediaQueue, BasicPopOperations)
+{
+ MediaQueue<MediaData> queue;
+ EXPECT_EQUAL_SIZE_T(queue.GetSize(), 0);
+
+ // Test only one element
+ const RefPtr<MediaData> data = CreateDataRawPtr(0, 10);
+ queue.Push(data.get());
+ EXPECT_EQUAL_SIZE_T(queue.GetSize(), 1);
+
+ RefPtr<MediaData> rv = queue.PopFront();
+ EXPECT_EQ(rv, data);
+
+ queue.Push(data.get());
+ EXPECT_EQUAL_SIZE_T(queue.GetSize(), 1);
+
+ rv = queue.PopBack();
+ EXPECT_EQ(rv, data);
+ EXPECT_EQUAL_SIZE_T(queue.GetSize(), 0);
+
+ // Test multiple elements
+ const RefPtr<MediaData> data1 = CreateDataRawPtr(0, 10);
+ const RefPtr<MediaData> data2 = CreateDataRawPtr(11, 20);
+ const RefPtr<MediaData> data3 = CreateDataRawPtr(21, 30);
+ queue.Push(data1.get());
+ queue.Push(data2.get());
+ queue.Push(data3.get());
+ EXPECT_EQUAL_SIZE_T(queue.GetSize(), 3);
+
+ rv = queue.PopFront();
+ EXPECT_EQUAL_SIZE_T(queue.GetSize(), 2);
+ EXPECT_EQ(rv, data1);
+
+ rv = queue.PopBack();
+ EXPECT_EQUAL_SIZE_T(queue.GetSize(), 1);
+ EXPECT_EQ(rv, data3);
+
+ rv = queue.PopBack();
+ EXPECT_EQUAL_SIZE_T(queue.GetSize(), 0);
+ EXPECT_EQ(rv, data2);
+}
+
+TEST(MediaQueue, BasicPeekOperations)
+{
+ MediaQueue<MediaData> queue;
+ EXPECT_EQUAL_SIZE_T(queue.GetSize(), 0);
+
+ // Test only one element
+ const RefPtr<MediaData> data1 = CreateDataRawPtr(0, 10);
+ queue.Push(data1.get());
+ EXPECT_EQUAL_SIZE_T(queue.GetSize(), 1);
+
+ RefPtr<MediaData> rv = queue.PeekFront();
+ EXPECT_EQUAL_SIZE_T(queue.GetSize(), 1);
+ EXPECT_EQ(rv, data1);
+
+ rv = queue.PeekBack();
+ EXPECT_EQUAL_SIZE_T(queue.GetSize(), 1);
+ EXPECT_EQ(rv, data1);
+
+ // Test multiple elements
+ const RefPtr<MediaData> data2 = CreateDataRawPtr(11, 20);
+ const RefPtr<MediaData> data3 = CreateDataRawPtr(21, 30);
+ queue.Push(data2.get());
+ queue.Push(data3.get());
+ EXPECT_EQUAL_SIZE_T(queue.GetSize(), 3);
+
+ rv = queue.PeekFront();
+ EXPECT_EQUAL_SIZE_T(queue.GetSize(), 3);
+ EXPECT_EQ(rv, data1);
+
+ rv = queue.PeekBack();
+ EXPECT_EQUAL_SIZE_T(queue.GetSize(), 3);
+ EXPECT_EQ(rv, data3);
+}
+
+TEST(MediaQueue, FinishQueue)
+{
+ MediaQueue<MediaData> queue;
+ EXPECT_FALSE(queue.IsFinished());
+
+ queue.Finish();
+ EXPECT_TRUE(queue.IsFinished());
+}
+
+TEST(MediaQueue, EndOfStream)
+{
+ MediaQueue<MediaData> queue;
+ EXPECT_FALSE(queue.IsFinished());
+
+ queue.Push(CreateData(0, 10));
+ EXPECT_EQUAL_SIZE_T(queue.GetSize(), 1);
+
+ queue.Finish();
+ EXPECT_EQUAL_SIZE_T(queue.GetSize(), 1);
+ EXPECT_TRUE(queue.IsFinished());
+ EXPECT_FALSE(queue.AtEndOfStream());
+
+ RefPtr<MediaData> rv = queue.PopFront();
+ EXPECT_EQUAL_SIZE_T(queue.GetSize(), 0);
+ EXPECT_TRUE(queue.IsFinished());
+ EXPECT_TRUE(queue.AtEndOfStream());
+}
+
+TEST(MediaQueue, QueueDuration)
+{
+ MediaQueue<MediaData> queue;
+ EXPECT_EQUAL_SIZE_T(queue.GetSize(), 0);
+
+ queue.Push(CreateData(0, 10));
+ queue.Push(CreateData(11, 20));
+ queue.Push(CreateData(21, 30));
+ EXPECT_EQUAL_SIZE_T(queue.GetSize(), 3);
+
+ const int64_t rv = queue.Duration();
+ EXPECT_EQ(rv, 30);
+}
+
+TEST(MediaQueue, CallGetElementAfterOnSingleElement)
+{
+ MediaQueue<MediaData> queue;
+ queue.Push(CreateData(0, 10));
+
+ // Target time is earlier than data's end time
+ TimeUnit targetTime = TimeUnit::FromMicroseconds(5);
+ nsTArray<RefPtr<MediaData>> foundResult;
+ queue.GetElementsAfter(targetTime, &foundResult);
+ EXPECT_EQUAL_SIZE_T(foundResult.Length(), 1);
+ EXPECT_TRUE(foundResult[0]->GetEndTime() > targetTime);
+
+ // Target time is later than data's end time
+ targetTime = TimeUnit::FromMicroseconds(15);
+ nsTArray<RefPtr<MediaData>> emptyResult;
+ queue.GetElementsAfter(targetTime, &emptyResult);
+ EXPECT_TRUE(emptyResult.IsEmpty());
+}
+
+TEST(MediaQueue, CallGetElementAfterOnMultipleElements)
+{
+ MediaQueue<MediaData> queue;
+ queue.Push(CreateData(0, 10));
+ queue.Push(CreateData(11, 20));
+ queue.Push(CreateData(21, 30));
+ queue.Push(CreateData(31, 40));
+ queue.Push(CreateData(41, 50));
+
+ // Should find [21,30], [31,40] and [41,50]
+ TimeUnit targetTime = TimeUnit::FromMicroseconds(25);
+ nsTArray<RefPtr<MediaData>> foundResult;
+ queue.GetElementsAfter(targetTime, &foundResult);
+ EXPECT_EQUAL_SIZE_T(foundResult.Length(), 3);
+ for (const auto& data : foundResult) {
+ EXPECT_TRUE(data->GetEndTime() > targetTime);
+ }
+
+ // Should find [31,40] and [41,50]
+ targetTime = TimeUnit::FromMicroseconds(30);
+ foundResult.Clear();
+ queue.GetElementsAfter(targetTime, &foundResult);
+ EXPECT_EQUAL_SIZE_T(foundResult.Length(), 2);
+ for (const auto& data : foundResult) {
+ EXPECT_TRUE(data->GetEndTime() > targetTime);
+ }
+
+ // Should find no data.
+ targetTime = TimeUnit::FromMicroseconds(60);
+ nsTArray<RefPtr<MediaData>> emptyResult;
+ queue.GetElementsAfter(targetTime, &emptyResult);
+ EXPECT_TRUE(emptyResult.IsEmpty());
+}
+
+TEST(MediaQueue, TimestampAdjustmentForSupportDataType)
+{
+ const size_t kOffSet = 30;
+ {
+ MediaQueue<AudioData> audioQueue;
+ audioQueue.Push(
+ CreateDataRawPtr(0, 10, MediaData::Type::AUDIO_DATA)->As<AudioData>());
+ audioQueue.SetOffset(TimeUnit::FromMicroseconds(kOffSet));
+ audioQueue.Push(
+ CreateDataRawPtr(0, 10, MediaData::Type::AUDIO_DATA)->As<AudioData>());
+
+ // Data stored before setting the offset shouldn't be changed
+ RefPtr<AudioData> data = audioQueue.PopFront();
+ EXPECT_EQ(data->mTime, TimeUnit::FromMicroseconds(0));
+ EXPECT_EQ(data->GetEndTime(), TimeUnit::FromMicroseconds(10));
+
+ // Data stored after setting the offset should be changed
+ data = audioQueue.PopFront();
+ EXPECT_EQ(data->mTime, TimeUnit::FromMicroseconds(0 + kOffSet));
+ EXPECT_EQ(data->GetEndTime(), TimeUnit::FromMicroseconds(10 + kOffSet));
+
+ // Reset will clean the offset.
+ audioQueue.Reset();
+ audioQueue.Push(
+ CreateDataRawPtr(0, 10, MediaData::Type::AUDIO_DATA)->As<AudioData>());
+ data = audioQueue.PopFront();
+ EXPECT_EQ(data->mTime, TimeUnit::FromMicroseconds(0));
+ EXPECT_EQ(data->GetEndTime(), TimeUnit::FromMicroseconds(10));
+ }
+
+ // Check another supported type
+ MediaQueue<VideoData> videoQueue;
+ videoQueue.Push(
+ CreateDataRawPtr(0, 10, MediaData::Type::VIDEO_DATA)->As<VideoData>());
+ videoQueue.SetOffset(TimeUnit::FromMicroseconds(kOffSet));
+ videoQueue.Push(
+ CreateDataRawPtr(0, 10, MediaData::Type::VIDEO_DATA)->As<VideoData>());
+
+ // Data stored before setting the offset shouldn't be changed
+ RefPtr<VideoData> data = videoQueue.PopFront();
+ EXPECT_EQ(data->mTime, TimeUnit::FromMicroseconds(0));
+ EXPECT_EQ(data->GetEndTime(), TimeUnit::FromMicroseconds(10));
+
+ // Data stored after setting the offset should be changed
+ data = videoQueue.PopFront();
+ EXPECT_EQ(data->mTime, TimeUnit::FromMicroseconds(0 + kOffSet));
+ EXPECT_EQ(data->GetEndTime(), TimeUnit::FromMicroseconds(10 + kOffSet));
+
+ // Reset will clean the offset.
+ videoQueue.Reset();
+ videoQueue.Push(
+ CreateDataRawPtr(0, 10, MediaData::Type::VIDEO_DATA)->As<VideoData>());
+ data = videoQueue.PopFront();
+ EXPECT_EQ(data->mTime, TimeUnit::FromMicroseconds(0));
+ EXPECT_EQ(data->GetEndTime(), TimeUnit::FromMicroseconds(10));
+}
+
+TEST(MediaQueue, TimestampAdjustmentForNotSupportDataType)
+{
+ const size_t kOffSet = 30;
+
+ MediaQueue<MediaData> queue;
+ queue.Push(CreateDataRawPtr(0, 10));
+ queue.SetOffset(TimeUnit::FromMicroseconds(kOffSet));
+ queue.Push(CreateDataRawPtr(0, 10));
+
+ // Offset won't affect any data at all.
+ RefPtr<MediaData> data = queue.PopFront();
+ EXPECT_EQ(data->mTime, TimeUnit::FromMicroseconds(0));
+ EXPECT_EQ(data->GetEndTime(), TimeUnit::FromMicroseconds(10));
+
+ data = queue.PopFront();
+ EXPECT_EQ(data->mTime, TimeUnit::FromMicroseconds(0));
+ EXPECT_EQ(data->GetEndTime(), TimeUnit::FromMicroseconds(10));
+}
+
+#undef EXPECT_EQUAL_SIZE_T
diff --git a/dom/media/gtest/TestMediaSpan.cpp b/dom/media/gtest/TestMediaSpan.cpp
new file mode 100644
index 0000000000..e6edcb944b
--- /dev/null
+++ b/dom/media/gtest/TestMediaSpan.cpp
@@ -0,0 +1,110 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <gtest/gtest.h>
+#include <stdint.h>
+
+#include "MediaSpan.h"
+
+#include "mozilla/ArrayUtils.h"
+
+using namespace mozilla;
+
+already_AddRefed<MediaByteBuffer> makeBuffer(uint8_t aStart, uint8_t aEnd) {
+ RefPtr<MediaByteBuffer> buffer(new MediaByteBuffer);
+ for (uint8_t i = aStart; i <= aEnd; i++) {
+ buffer->AppendElement(i);
+ }
+ return buffer.forget();
+}
+
+bool IsRangeAt(const MediaSpan& aSpan, uint8_t aStart, uint8_t aEnd,
+ size_t aAt) {
+ size_t length = size_t(aEnd) - size_t(aStart) + 1;
+ if (aAt + length > aSpan.Length()) {
+ return false;
+ }
+ for (size_t i = 0; i < length; i++) {
+ if (aSpan[aAt + i] != uint8_t(aStart + i)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool IsRange(const MediaSpan& aSpan, uint8_t aStart, uint8_t aEnd) {
+ return IsRangeAt(aSpan, aStart, aEnd, 0);
+}
+
+TEST(MediaSpan, AppendToFromSpan)
+{
+ RefPtr<MediaByteBuffer> buffer1 = makeBuffer(0, 9);
+ MediaSpan span1 = MediaSpan(buffer1);
+ EXPECT_EQ(span1.Length(), size_t(10));
+ EXPECT_TRUE(IsRange(span1, 0, 9));
+
+ MediaSpan span2 = span1.From(5);
+
+ EXPECT_EQ(span2.Length(), size_t(5));
+ EXPECT_TRUE(IsRange(span2, 5, 9));
+ RefPtr<MediaByteBuffer> buffer2 = makeBuffer(10, 19);
+ EXPECT_EQ(buffer2->Length(), size_t(10));
+ span2.Append(buffer2);
+
+ // Span2 should be: [5...19]
+ EXPECT_EQ(span2.Length(), size_t(15));
+ EXPECT_TRUE(IsRange(span2, 5, 19));
+
+ // Span1 should not be modified by the append to span2.
+ EXPECT_EQ(span1.Length(), size_t(10));
+ EXPECT_TRUE(IsRange(span1, 0, 9));
+}
+
+TEST(MediaSpan, AppendToToSpan)
+{
+ RefPtr<MediaByteBuffer> buffer1 = makeBuffer(0, 9);
+ MediaSpan span1 = MediaSpan(buffer1);
+ EXPECT_EQ(span1.Length(), size_t(10));
+ EXPECT_TRUE(IsRange(span1, 0, 9));
+
+ MediaSpan span2 = span1.To(5);
+
+ // Span2 should be [0...4]
+ EXPECT_EQ(span2.Length(), size_t(5));
+ EXPECT_TRUE(IsRange(span2, 0, 4));
+ RefPtr<MediaByteBuffer> buffer2 = makeBuffer(10, 19);
+ EXPECT_EQ(buffer2->Length(), size_t(10));
+ span2.Append(buffer2);
+
+ // Span2 should be: [0...4][10...19]
+ EXPECT_EQ(span2.Length(), size_t(15));
+ EXPECT_TRUE(IsRangeAt(span2, 0, 4, 0));
+ EXPECT_TRUE(IsRangeAt(span2, 10, 19, 5));
+
+ // Span1 should not be modified by the append to span2.
+ EXPECT_EQ(span1.Length(), size_t(10));
+ EXPECT_TRUE(IsRange(span1, 0, 9));
+}
+
+TEST(MediaSpan, RemoveFront)
+{
+ RefPtr<MediaByteBuffer> buffer1 = makeBuffer(0, 9);
+ MediaSpan span1 = MediaSpan(buffer1);
+ EXPECT_EQ(span1.Length(), size_t(10));
+ EXPECT_TRUE(IsRange(span1, 0, 9));
+
+ MediaSpan span2(span1);
+ EXPECT_EQ(span2.Length(), size_t(10));
+
+ span2.RemoveFront(5);
+
+ // Span2 should now be [5...9]
+ EXPECT_EQ(span2.Length(), size_t(5));
+ EXPECT_TRUE(IsRange(span2, 5, 9));
+
+ // Span1 should be unaffected.
+ EXPECT_EQ(span1.Length(), size_t(10));
+ EXPECT_TRUE(IsRange(span1, 0, 9));
+}
diff --git a/dom/media/gtest/TestMediaUtils.cpp b/dom/media/gtest/TestMediaUtils.cpp
new file mode 100644
index 0000000000..33a32b7ea0
--- /dev/null
+++ b/dom/media/gtest/TestMediaUtils.cpp
@@ -0,0 +1,240 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+
+#include "MediaUtils.h"
+#include "mozilla/AppShutdown.h"
+#include "mozilla/SyncRunnable.h"
+#include "mozilla/gtest/MozHelpers.h"
+
+using namespace mozilla;
+using namespace mozilla::gtest;
+using namespace mozilla::media;
+
+// Spawning the death test child process aborts on Android.
+#if !defined(ANDROID)
+
+// Kept here for reference as it can be handy during development.
+# define DISABLE_CRASH_REPORTING \
+ gtest::DisableCrashReporter(); \
+ ZERO_GDB_SLEEP();
+
+void DoCreateTicketBeforeAppShutdownOnMain() {
+ auto reporter = ScopedTestResultReporter::Create(ExitMode::ExitOnDtor);
+
+ AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdownNetTeardown);
+ AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdownTeardown);
+
+ Monitor mon("TestMonitor");
+ bool pastAppShutdown = false;
+ bool backgroundTaskFinished = false;
+
+ UniquePtr ticket = ShutdownBlockingTicket::Create(
+ u"Test"_ns, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__);
+
+ MOZ_ALWAYS_SUCCEEDS(
+ NS_DispatchBackgroundTask(NS_NewRunnableFunction(__func__, [&] {
+ TimeStamp now = TimeStamp::Now();
+ TimeStamp end = now + TimeDuration::FromSeconds(0.2);
+ MonitorAutoLock lock(mon);
+ while (!pastAppShutdown && (end - now) > TimeDuration()) {
+ lock.Wait(end - now);
+ now = TimeStamp::Now();
+ }
+ EXPECT_FALSE(pastAppShutdown);
+ ticket = nullptr;
+ while (!pastAppShutdown) {
+ lock.Wait();
+ }
+ EXPECT_TRUE(pastAppShutdown);
+ backgroundTaskFinished = true;
+ lock.Notify();
+ })));
+
+ AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdown);
+
+ {
+ MonitorAutoLock lock(mon);
+ pastAppShutdown = true;
+ lock.Notify();
+ while (!backgroundTaskFinished) {
+ lock.Wait();
+ }
+ }
+
+ AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdownQM);
+ AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdownTelemetry);
+
+ NS_ShutdownXPCOM(nullptr);
+}
+
+void DoCreateTicketAfterAppShutdownOnMain() {
+ auto reporter = ScopedTestResultReporter::Create(ExitMode::ExitOnDtor);
+
+ AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdownNetTeardown);
+ AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdownTeardown);
+ AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdown);
+
+ auto ticket = ShutdownBlockingTicket::Create(
+ u"Test"_ns, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__);
+ EXPECT_FALSE(ticket);
+
+ AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdownQM);
+ AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdownTelemetry);
+
+ NS_ShutdownXPCOM(nullptr);
+}
+
+void DoCreateTicketBeforeAppShutdownOffMain() {
+ auto reporter = ScopedTestResultReporter::Create(ExitMode::ExitOnDtor);
+
+ AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdownNetTeardown);
+ AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdownTeardown);
+
+ Monitor mon("TestMonitor");
+ bool pastAppShutdown = false;
+ bool ticketCreated = false;
+ bool backgroundTaskFinished = false;
+
+ MOZ_ALWAYS_SUCCEEDS(
+ NS_DispatchBackgroundTask(NS_NewRunnableFunction(__func__, [&] {
+ MonitorAutoLock lock(mon);
+ auto ticket = ShutdownBlockingTicket::Create(
+ u"Test"_ns, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__);
+ EXPECT_TRUE(ticket);
+ ticketCreated = true;
+ lock.Notify();
+
+ TimeStamp now = TimeStamp::Now();
+ TimeStamp end = now + TimeDuration::FromSeconds(0.2);
+ while (!pastAppShutdown && (end - now) > TimeDuration()) {
+ lock.Wait(end - now);
+ now = TimeStamp::Now();
+ }
+ EXPECT_FALSE(pastAppShutdown);
+ ticket = nullptr;
+ while (!pastAppShutdown) {
+ lock.Wait();
+ }
+ EXPECT_TRUE(pastAppShutdown);
+ backgroundTaskFinished = true;
+ lock.Notify();
+ })));
+
+ {
+ MonitorAutoLock lock(mon);
+ while (!ticketCreated) {
+ lock.Wait();
+ }
+ }
+
+ AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdown);
+
+ MonitorAutoLock lock(mon);
+ pastAppShutdown = true;
+ lock.Notify();
+ while (!backgroundTaskFinished) {
+ lock.Wait();
+ }
+
+ AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdownQM);
+ AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdownTelemetry);
+
+ NS_ShutdownXPCOM(nullptr);
+}
+
+void DoCreateTicketAfterAppShutdownOffMain() {
+ auto reporter = ScopedTestResultReporter::Create(ExitMode::ExitOnDtor);
+
+ AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdownNetTeardown);
+ AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdownTeardown);
+ AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdown);
+
+ UniquePtr<ShutdownBlockingTicket> ticket;
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchBackgroundTask(
+ MakeAndAddRef<SyncRunnable>(NS_NewRunnableFunction(__func__, [&] {
+ ticket = ShutdownBlockingTicket::Create(
+ u"Test"_ns, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__);
+ }))));
+
+ EXPECT_FALSE(ticket);
+
+ AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdownQM);
+ AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdownTelemetry);
+
+ NS_ShutdownXPCOM(nullptr);
+}
+
+void DoTwoTicketsWithSameNameBothBlockShutdown() {
+ auto reporter = ScopedTestResultReporter::Create(ExitMode::ExitOnDtor);
+
+ const auto name = u"Test"_ns;
+ auto ticket1 = ShutdownBlockingTicket::Create(
+ name, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__);
+ EXPECT_TRUE(ticket1);
+ auto ticket2 = ShutdownBlockingTicket::Create(
+ name, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__);
+ EXPECT_TRUE(ticket2);
+
+ ticket1 = nullptr;
+
+ // A copyable holder for the std::function in NS_NewTimerWithCallback.
+ auto ticket2Holder =
+ MakeRefPtr<Refcountable<UniquePtr<ShutdownBlockingTicket>>>(
+ ticket2.release());
+
+ const auto waitBeforeDestroyingTicket = TimeDuration::FromMilliseconds(100);
+ TimeStamp before = TimeStamp::Now();
+ auto timerResult = NS_NewTimerWithCallback(
+ [t = std::move(ticket2Holder)](nsITimer* aTimer) {},
+ waitBeforeDestroyingTicket, nsITimer::TYPE_ONE_SHOT, __func__);
+ ASSERT_TRUE(timerResult.isOk());
+
+ AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdownNetTeardown);
+ AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdownTeardown);
+ AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdown);
+ AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdownQM);
+ AppShutdown::AdvanceShutdownPhase(ShutdownPhase::AppShutdownTelemetry);
+
+ NS_ShutdownXPCOM(nullptr);
+ TimeStamp after = TimeStamp::Now();
+ EXPECT_GT((after - before).ToMilliseconds(),
+ waitBeforeDestroyingTicket.ToMilliseconds());
+}
+
+TEST(ShutdownBlockingTicketDeathTest, CreateTicketBeforeAppShutdownOnMain)
+{
+ EXPECT_EXIT(DoCreateTicketBeforeAppShutdownOnMain(),
+ testing::ExitedWithCode(0), "");
+}
+
+TEST(ShutdownBlockingTicketDeathTest, CreateTicketAfterAppShutdownOnMain)
+{
+ EXPECT_EXIT(DoCreateTicketAfterAppShutdownOnMain(),
+ testing::ExitedWithCode(0), "");
+}
+
+TEST(ShutdownBlockingTicketDeathTest, CreateTicketBeforeAppShutdownOffMain)
+{
+ EXPECT_EXIT(DoCreateTicketBeforeAppShutdownOffMain(),
+ testing::ExitedWithCode(0), "");
+}
+
+TEST(ShutdownBlockingTicketDeathTest, CreateTicketAfterAppShutdownOffMain)
+{
+ EXPECT_EXIT(DoCreateTicketAfterAppShutdownOffMain(),
+ testing::ExitedWithCode(0), "");
+}
+
+TEST(ShutdownBlockingTicketDeathTest, TwoTicketsWithSameNameBothBlockShutdown)
+{
+ EXPECT_EXIT(DoTwoTicketsWithSameNameBothBlockShutdown(),
+ testing::ExitedWithCode(0), "");
+}
+
+# undef DISABLE_CRASH_REPORTING
+
+#endif
diff --git a/dom/media/gtest/TestMuxer.cpp b/dom/media/gtest/TestMuxer.cpp
new file mode 100644
index 0000000000..1c6c128eef
--- /dev/null
+++ b/dom/media/gtest/TestMuxer.cpp
@@ -0,0 +1,212 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <vector>
+
+#include "ContainerWriter.h"
+#include "EncodedFrame.h"
+#include "gtest/gtest.h"
+#include "gmock/gmock.h"
+#include "Muxer.h"
+#include "OpusTrackEncoder.h"
+#include "WebMWriter.h"
+
+using namespace mozilla;
+using media::TimeUnit;
+using testing::_;
+using testing::ElementsAre;
+using testing::Return;
+using testing::StaticAssertTypeEq;
+
+static RefPtr<TrackMetadataBase> CreateOpusMetadata(int32_t aChannels,
+ float aSamplingFrequency,
+ size_t aIdHeaderSize,
+ size_t aCommentHeaderSize) {
+ auto opusMetadata = MakeRefPtr<OpusMetadata>();
+ opusMetadata->mChannels = aChannels;
+ opusMetadata->mSamplingFrequency = aSamplingFrequency;
+ opusMetadata->mIdHeader.SetLength(aIdHeaderSize);
+ for (size_t i = 0; i < opusMetadata->mIdHeader.Length(); i++) {
+ opusMetadata->mIdHeader[i] = 0;
+ }
+ opusMetadata->mCommentHeader.SetLength(aCommentHeaderSize);
+ for (size_t i = 0; i < opusMetadata->mCommentHeader.Length(); i++) {
+ opusMetadata->mCommentHeader[i] = 0;
+ }
+ return opusMetadata;
+}
+
+static RefPtr<TrackMetadataBase> CreateVP8Metadata(int32_t aWidth,
+ int32_t aHeight) {
+ auto vp8Metadata = MakeRefPtr<VP8Metadata>();
+ vp8Metadata->mWidth = aWidth;
+ vp8Metadata->mDisplayWidth = aWidth;
+ vp8Metadata->mHeight = aHeight;
+ vp8Metadata->mDisplayHeight = aHeight;
+ return vp8Metadata;
+}
+
+static RefPtr<EncodedFrame> CreateFrame(EncodedFrame::FrameType aType,
+ const TimeUnit& aTime,
+ const TimeUnit& aDuration,
+ size_t aDataSize) {
+ auto data = MakeRefPtr<EncodedFrame::FrameData>();
+ data->SetLength(aDataSize);
+ if (aType == EncodedFrame::OPUS_AUDIO_FRAME) {
+ // Opus duration is in samples, so figure out how many samples will put us
+ // closest to aDurationUs without going over.
+ return MakeRefPtr<EncodedFrame>(aTime,
+ TimeUnitToFrames(aDuration, 48000).value(),
+ 48000, aType, std::move(data));
+ }
+ return MakeRefPtr<EncodedFrame>(
+ aTime, TimeUnitToFrames(aDuration, USECS_PER_S).value(), USECS_PER_S,
+ aType, std::move(data));
+}
+
+class MockContainerWriter : public ContainerWriter {
+ public:
+ MOCK_METHOD2(WriteEncodedTrack,
+ nsresult(const nsTArray<RefPtr<EncodedFrame>>&, uint32_t));
+ MOCK_METHOD1(SetMetadata,
+ nsresult(const nsTArray<RefPtr<TrackMetadataBase>>&));
+ MOCK_METHOD0(IsWritingComplete, bool());
+ MOCK_METHOD2(GetContainerData,
+ nsresult(nsTArray<nsTArray<uint8_t>>*, uint32_t));
+};
+
+TEST(MuxerTest, AudioOnly)
+{
+ MediaQueue<EncodedFrame> audioQueue;
+ MediaQueue<EncodedFrame> videoQueue;
+ videoQueue.Finish();
+ MockContainerWriter* writer = new MockContainerWriter();
+ Muxer muxer(WrapUnique<ContainerWriter>(writer), audioQueue, videoQueue);
+
+ // Prepare data
+
+ auto opusMeta = CreateOpusMetadata(1, 48000, 16, 16);
+ auto audioFrame =
+ CreateFrame(EncodedFrame::OPUS_AUDIO_FRAME, TimeUnit::FromSeconds(0),
+ TimeUnit::FromSeconds(0.2), 4096);
+
+ // Expectations
+
+ EXPECT_CALL(*writer, SetMetadata(ElementsAre(opusMeta)))
+ .WillOnce(Return(NS_OK));
+ EXPECT_CALL(*writer, WriteEncodedTrack(ElementsAre(audioFrame),
+ ContainerWriter::END_OF_STREAM))
+ .WillOnce(Return(NS_OK));
+ EXPECT_CALL(*writer, GetContainerData(_, ContainerWriter::GET_HEADER))
+ .WillOnce(Return(NS_OK));
+ EXPECT_CALL(*writer, GetContainerData(_, ContainerWriter::FLUSH_NEEDED))
+ .WillOnce(Return(NS_OK));
+ EXPECT_CALL(*writer, IsWritingComplete()).Times(0);
+
+ // Test
+
+ EXPECT_EQ(muxer.SetMetadata(nsTArray<RefPtr<TrackMetadataBase>>({opusMeta})),
+ NS_OK);
+ audioQueue.Push(audioFrame);
+ audioQueue.Finish();
+ nsTArray<nsTArray<uint8_t>> buffers;
+ EXPECT_EQ(muxer.GetData(&buffers), NS_OK);
+}
+
+TEST(MuxerTest, AudioVideo)
+{
+ MediaQueue<EncodedFrame> audioQueue;
+ MediaQueue<EncodedFrame> videoQueue;
+ MockContainerWriter* writer = new MockContainerWriter();
+ Muxer muxer(WrapUnique<ContainerWriter>(writer), audioQueue, videoQueue);
+
+ // Prepare data
+
+ auto opusMeta = CreateOpusMetadata(1, 48000, 16, 16);
+ auto vp8Meta = CreateVP8Metadata(640, 480);
+ auto audioFrame =
+ CreateFrame(EncodedFrame::OPUS_AUDIO_FRAME, TimeUnit::FromSeconds(0),
+ TimeUnit::FromSeconds(0.2), 4096);
+ auto videoFrame =
+ CreateFrame(EncodedFrame::VP8_I_FRAME, TimeUnit::FromSeconds(0),
+ TimeUnit::FromSeconds(0.05), 65536);
+
+ // Expectations
+
+ EXPECT_CALL(*writer, SetMetadata(ElementsAre(opusMeta, vp8Meta)))
+ .WillOnce(Return(NS_OK));
+ EXPECT_CALL(*writer, WriteEncodedTrack(ElementsAre(videoFrame, audioFrame),
+ ContainerWriter::END_OF_STREAM))
+ .WillOnce(Return(NS_OK));
+ EXPECT_CALL(*writer, GetContainerData(_, ContainerWriter::GET_HEADER))
+ .WillOnce(Return(NS_OK));
+ EXPECT_CALL(*writer, GetContainerData(_, ContainerWriter::FLUSH_NEEDED))
+ .WillOnce(Return(NS_OK));
+ EXPECT_CALL(*writer, IsWritingComplete()).Times(0);
+
+ // Test
+
+ EXPECT_EQ(muxer.SetMetadata(
+ nsTArray<RefPtr<TrackMetadataBase>>({opusMeta, vp8Meta})),
+ NS_OK);
+ audioQueue.Push(audioFrame);
+ audioQueue.Finish();
+ videoQueue.Push(videoFrame);
+ videoQueue.Finish();
+ nsTArray<nsTArray<uint8_t>> buffers;
+ EXPECT_EQ(muxer.GetData(&buffers), NS_OK);
+}
+
+TEST(MuxerTest, AudioVideoOutOfOrder)
+{
+ MediaQueue<EncodedFrame> audioQueue;
+ MediaQueue<EncodedFrame> videoQueue;
+ MockContainerWriter* writer = new MockContainerWriter();
+ Muxer muxer(WrapUnique<ContainerWriter>(writer), audioQueue, videoQueue);
+
+ // Prepare data
+
+ auto opusMeta = CreateOpusMetadata(1, 48000, 16, 16);
+ auto vp8Meta = CreateVP8Metadata(640, 480);
+ auto a0 =
+ CreateFrame(EncodedFrame::OPUS_AUDIO_FRAME, TimeUnit::FromMicroseconds(0),
+ TimeUnit::FromMicroseconds(48), 4096);
+ auto v0 =
+ CreateFrame(EncodedFrame::VP8_I_FRAME, TimeUnit::FromMicroseconds(0),
+ TimeUnit::FromMicroseconds(50), 65536);
+ auto a48 = CreateFrame(EncodedFrame::OPUS_AUDIO_FRAME,
+ TimeUnit::FromMicroseconds(48),
+ TimeUnit::FromMicroseconds(48), 4096);
+ auto v50 =
+ CreateFrame(EncodedFrame::VP8_I_FRAME, TimeUnit::FromMicroseconds(50),
+ TimeUnit::FromMicroseconds(50), 65536);
+
+ // Expectations
+
+ EXPECT_CALL(*writer, SetMetadata(ElementsAre(opusMeta, vp8Meta)))
+ .WillOnce(Return(NS_OK));
+ EXPECT_CALL(*writer, WriteEncodedTrack(ElementsAre(v0, a0, a48, v50),
+ ContainerWriter::END_OF_STREAM))
+ .WillOnce(Return(NS_OK));
+ EXPECT_CALL(*writer, GetContainerData(_, ContainerWriter::GET_HEADER))
+ .WillOnce(Return(NS_OK));
+ EXPECT_CALL(*writer, GetContainerData(_, ContainerWriter::FLUSH_NEEDED))
+ .WillOnce(Return(NS_OK));
+ EXPECT_CALL(*writer, IsWritingComplete()).Times(0);
+
+ // Test
+
+ EXPECT_EQ(muxer.SetMetadata(
+ nsTArray<RefPtr<TrackMetadataBase>>({opusMeta, vp8Meta})),
+ NS_OK);
+ audioQueue.Push(a0);
+ videoQueue.Push(v0);
+ videoQueue.Push(v50);
+ videoQueue.Finish();
+ audioQueue.Push(a48);
+ audioQueue.Finish();
+ nsTArray<nsTArray<uint8_t>> buffers;
+ EXPECT_EQ(muxer.GetData(&buffers), NS_OK);
+}
diff --git a/dom/media/gtest/TestOggWriter.cpp b/dom/media/gtest/TestOggWriter.cpp
new file mode 100644
index 0000000000..d9df697cfe
--- /dev/null
+++ b/dom/media/gtest/TestOggWriter.cpp
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "OggWriter.h"
+#include "OpusTrackEncoder.h"
+
+using namespace mozilla;
+
+// Writing multiple 4kB-pages should return all of them on getting.
+TEST(TestOggWriter, MultiPageInput)
+{
+ auto opusMeta = MakeRefPtr<OpusMetadata>();
+ opusMeta->mChannels = 1;
+ opusMeta->mSamplingFrequency = 48000;
+ opusMeta->mIdHeader.AppendElement(1);
+ opusMeta->mCommentHeader.AppendElement(1);
+ AutoTArray<RefPtr<TrackMetadataBase>, 1> metadata;
+ metadata.AppendElement(std::move(opusMeta));
+
+ OggWriter ogg;
+ MOZ_ALWAYS_SUCCEEDS(ogg.SetMetadata(metadata));
+ {
+ nsTArray<nsTArray<uint8_t>> buffer;
+ MOZ_ALWAYS_SUCCEEDS(
+ ogg.GetContainerData(&buffer, ContainerWriter::GET_HEADER));
+ }
+
+ size_t inputBytes = 0;
+ const size_t USECS_PER_MS = 1000;
+ auto frameData = MakeRefPtr<EncodedFrame::FrameData>();
+ frameData->SetLength(320); // 320B per 20ms == 128kbps
+ PodZero(frameData->Elements(), frameData->Length());
+ // 50 frames at 320B = 16kB = 4 4kB-pages
+ for (int i = 0; i < 50; ++i) {
+ auto frame = MakeRefPtr<EncodedFrame>(
+ media::TimeUnit::FromMicroseconds(20 * USECS_PER_MS * i),
+ 48000 / 1000 * 20 /* 20ms */, 48000, EncodedFrame::OPUS_AUDIO_FRAME,
+ frameData);
+ AutoTArray<RefPtr<EncodedFrame>, 1> frames;
+ frames.AppendElement(std::move(frame));
+ uint32_t flags = 0;
+ if (i == 49) {
+ flags |= ContainerWriter::END_OF_STREAM;
+ }
+ MOZ_ALWAYS_SUCCEEDS(ogg.WriteEncodedTrack(frames, flags));
+ inputBytes += frameData->Length();
+ }
+
+ nsTArray<nsTArray<uint8_t>> buffer;
+ MOZ_ALWAYS_SUCCEEDS(
+ ogg.GetContainerData(&buffer, ContainerWriter::FLUSH_NEEDED));
+ size_t outputBytes = 0;
+ for (const auto& b : buffer) {
+ outputBytes += b.Length();
+ }
+
+ EXPECT_EQ(inputBytes, 16000U);
+ EXPECT_EQ(outputBytes, 16208U);
+}
diff --git a/dom/media/gtest/TestOpusParser.cpp b/dom/media/gtest/TestOpusParser.cpp
new file mode 100644
index 0000000000..639fe7cfc0
--- /dev/null
+++ b/dom/media/gtest/TestOpusParser.cpp
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "OpusParser.h"
+#include <algorithm>
+
+using namespace mozilla;
+
+TEST(OpusParser, Mapping2)
+{
+ uint8_t validChannels[] = {1, 3, 4, 6, 9, 11, 16, 18, 25, 27,
+ 36, 38, 49, 51, 64, 66, 81, 83, 100, 102,
+ 121, 123, 144, 146, 169, 171, 196, 198, 225, 227};
+ for (uint8_t channels = 0; channels < 255; channels++) {
+ bool found = OpusParser::IsValidMapping2ChannelsCount(channels);
+ bool foundTable =
+ std::find(std::begin(validChannels), std::end(validChannels),
+ channels) != std::end(validChannels);
+ EXPECT_EQ(found, foundTable);
+ }
+}
diff --git a/dom/media/gtest/TestPacer.cpp b/dom/media/gtest/TestPacer.cpp
new file mode 100644
index 0000000000..5d21ce1215
--- /dev/null
+++ b/dom/media/gtest/TestPacer.cpp
@@ -0,0 +1,189 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "Pacer.h"
+#include "VideoUtils.h"
+#include "WaitFor.h"
+
+using namespace mozilla;
+
+template <typename T>
+class PacerTest {
+ protected:
+ explicit PacerTest(TimeDuration aDuplicationInterval)
+ : mTaskQueue(TaskQueue::Create(
+ GetMediaThreadPool(MediaThreadType::WEBRTC_WORKER), "PacerTest")),
+ mPacer(MakeRefPtr<Pacer<T>>(mTaskQueue, aDuplicationInterval)),
+ mInterval(aDuplicationInterval) {}
+
+ // Helper for calling `mPacer->Enqueue(...)`. Dispatches an event to the
+ // current thread which will enqueue the event to make sure that any listeners
+ // registered by a call to `WaitFor(...)` have been registered before events
+ // start being processed on a background queue.
+ void EnqueueSoon(T aItem, TimeStamp aTime) {
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchToCurrentThread(NS_NewRunnableFunction(
+ "PacerTest::EnqueueSoon",
+ [pacer = mPacer, aItem = std::move(aItem), aTime] {
+ pacer->Enqueue(std::move(aItem), aTime);
+ })));
+ }
+
+ void TearDown() {
+ mPacer->Shutdown()->Then(mTaskQueue, __func__,
+ [tq = mTaskQueue] { tq->BeginShutdown(); });
+ }
+
+ const RefPtr<TaskQueue> mTaskQueue;
+ const RefPtr<Pacer<T>> mPacer;
+ const TimeDuration mInterval;
+};
+
+class PacerTestInt : public PacerTest<int>, public ::testing::Test {
+ protected:
+ explicit PacerTestInt(TimeDuration aDuplicationInterval)
+ : PacerTest<int>(aDuplicationInterval) {}
+
+ void TearDown() override { PacerTest::TearDown(); }
+};
+
+class PacerTestIntLongDuplication : public PacerTestInt {
+ protected:
+ PacerTestIntLongDuplication() : PacerTestInt(TimeDuration::FromSeconds(10)) {}
+};
+
+class PacerTestIntTenMsDuplication : public PacerTestInt {
+ protected:
+ PacerTestIntTenMsDuplication()
+ : PacerTestInt(TimeDuration::FromMilliseconds(10)) {}
+};
+
+TEST_F(PacerTestIntLongDuplication, Single) {
+ auto now = TimeStamp::Now();
+ auto d1 = TimeDuration::FromMilliseconds(100);
+ EnqueueSoon(1, now + d1);
+
+ auto [i, time] = WaitFor(TakeN(mPacer->PacedItemEvent(), 1)).unwrap()[0];
+ EXPECT_GE(TimeStamp::Now() - now, d1);
+ EXPECT_EQ(i, 1);
+ EXPECT_EQ(time - now, d1);
+}
+
+TEST_F(PacerTestIntLongDuplication, Past) {
+ auto now = TimeStamp::Now();
+ auto d1 = TimeDuration::FromMilliseconds(100);
+ EnqueueSoon(1, now - d1);
+
+ auto [i, time] = WaitFor(TakeN(mPacer->PacedItemEvent(), 1)).unwrap()[0];
+ EXPECT_GE(TimeStamp::Now() - now, -d1);
+ EXPECT_EQ(i, 1);
+ EXPECT_EQ(time - now, -d1);
+}
+
+TEST_F(PacerTestIntLongDuplication, TimeReset) {
+ auto now = TimeStamp::Now();
+ auto d1 = TimeDuration::FromMilliseconds(100);
+ auto d2 = TimeDuration::FromMilliseconds(200);
+ auto d3 = TimeDuration::FromMilliseconds(300);
+ EnqueueSoon(1, now + d1);
+ EnqueueSoon(2, now + d3);
+ EnqueueSoon(3, now + d2);
+
+ auto items = WaitFor(TakeN(mPacer->PacedItemEvent(), 2)).unwrap();
+
+ {
+ auto [i, time] = items[0];
+ EXPECT_GE(TimeStamp::Now() - now, d1);
+ EXPECT_EQ(i, 1);
+ EXPECT_EQ(time - now, d1);
+ }
+ {
+ auto [i, time] = items[1];
+ EXPECT_GE(TimeStamp::Now() - now, d2);
+ EXPECT_EQ(i, 3);
+ EXPECT_EQ(time - now, d2);
+ }
+}
+
+TEST_F(PacerTestIntTenMsDuplication, SingleDuplication) {
+ auto now = TimeStamp::Now();
+ auto d1 = TimeDuration::FromMilliseconds(100);
+ EnqueueSoon(1, now + d1);
+
+ auto items = WaitFor(TakeN(mPacer->PacedItemEvent(), 2)).unwrap();
+
+ {
+ auto [i, time] = items[0];
+ EXPECT_GE(TimeStamp::Now() - now, d1);
+ EXPECT_EQ(i, 1);
+ EXPECT_EQ(time - now, d1);
+ }
+ {
+ auto [i, time] = items[1];
+ EXPECT_GE(TimeStamp::Now() - now, d1 + mInterval);
+ EXPECT_EQ(i, 1);
+ EXPECT_EQ(time - now, d1 + mInterval);
+ }
+}
+
+TEST_F(PacerTestIntTenMsDuplication, RacyDuplication1) {
+ auto now = TimeStamp::Now();
+ auto d1 = TimeDuration::FromMilliseconds(100);
+ auto d2 = d1 + mInterval - TimeDuration::FromMicroseconds(1);
+ EnqueueSoon(1, now + d1);
+ EnqueueSoon(2, now + d2);
+
+ auto items = WaitFor(TakeN(mPacer->PacedItemEvent(), 3)).unwrap();
+
+ {
+ auto [i, time] = items[0];
+ EXPECT_GE(TimeStamp::Now() - now, d1);
+ EXPECT_EQ(i, 1);
+ EXPECT_EQ(time - now, d1);
+ }
+ {
+ auto [i, time] = items[1];
+ EXPECT_GE(TimeStamp::Now() - now, d2);
+ EXPECT_EQ(i, 2);
+ EXPECT_EQ(time - now, d2);
+ }
+ {
+ auto [i, time] = items[2];
+ EXPECT_GE(TimeStamp::Now() - now, d2 + mInterval);
+ EXPECT_EQ(i, 2);
+ EXPECT_EQ(time - now, d2 + mInterval);
+ }
+}
+
+TEST_F(PacerTestIntTenMsDuplication, RacyDuplication2) {
+ auto now = TimeStamp::Now();
+ auto d1 = TimeDuration::FromMilliseconds(100);
+ auto d2 = d1 + mInterval + TimeDuration::FromMicroseconds(1);
+ EnqueueSoon(1, now + d1);
+ EnqueueSoon(2, now + d2);
+
+ auto items = WaitFor(TakeN(mPacer->PacedItemEvent(), 3)).unwrap();
+
+ {
+ auto [i, time] = items[0];
+ EXPECT_GE(TimeStamp::Now() - now, d1);
+ EXPECT_EQ(i, 1);
+ EXPECT_EQ(time - now, d1);
+ }
+ {
+ auto [i, time] = items[1];
+ EXPECT_GE(TimeStamp::Now() - now, d1 + mInterval);
+ EXPECT_EQ(i, 1);
+ EXPECT_EQ(time - now, d1 + mInterval);
+ }
+ {
+ auto [i, time] = items[2];
+ EXPECT_GE(TimeStamp::Now() - now, d2);
+ EXPECT_EQ(i, 2);
+ EXPECT_EQ(time - now, d2);
+ }
+}
diff --git a/dom/media/gtest/TestRTCStatsTimestampMaker.cpp b/dom/media/gtest/TestRTCStatsTimestampMaker.cpp
new file mode 100644
index 0000000000..7997c22d1f
--- /dev/null
+++ b/dom/media/gtest/TestRTCStatsTimestampMaker.cpp
@@ -0,0 +1,113 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <cmath>
+
+#include "gtest/gtest.h"
+#include "libwebrtcglue/SystemTime.h"
+
+using namespace mozilla;
+using namespace dom;
+
+static constexpr auto kWebrtcTimeOffset = webrtc::Timestamp::Seconds(123456789);
+
+TEST(RTCStatsTimestampMakerRealtimeClock, ConvertTimestampToNtpTime)
+{
+ auto maker = RTCStatsTimestampMaker::Create();
+ RTCStatsTimestampMakerRealtimeClock clock(maker);
+ constexpr auto ntpTo1Jan1970Ms = webrtc::kNtpJan1970 * 1000LL;
+ for (int i = 1000; i < 20000; i += 93) {
+ const auto t = kWebrtcTimeOffset + webrtc::TimeDelta::Micros(i);
+ const auto ntp = clock.ConvertTimestampToNtpTime(t);
+ // Because of precision differences, these round to a specific millisecond
+ // slightly differently.
+ EXPECT_NEAR(ntp.ToMs() - ntpTo1Jan1970Ms,
+ RTCStatsTimestamp::FromRealtime(maker, t).To1Jan1970().ms(),
+ 1.0)
+ << " for i=" << i;
+ }
+}
+
+TEST(RTCStatsTimestampMaker, ConvertNtpToDomTime)
+{
+ auto maker = RTCStatsTimestampMaker::Create();
+ RTCStatsTimestampMakerRealtimeClock clock(maker);
+ for (int i = 1000; i < 20000; i += 93) {
+ const auto t = kWebrtcTimeOffset + webrtc::TimeDelta::Micros(i);
+ const auto ntp = clock.ConvertTimestampToNtpTime(t);
+ const auto dom =
+ RTCStatsTimestamp::FromNtp(maker, webrtc::Timestamp::Millis(ntp.ToMs()))
+ .ToDom();
+ // Because of precision differences, these round to a specific millisecond
+ // slightly differently.
+ EXPECT_NEAR(std::lround(dom),
+ std::lround(RTCStatsTimestamp::FromRealtime(maker, t).ToDom()),
+ 1.0)
+ << " for i=" << i;
+ }
+}
+
+TEST(RTCStatsTimestampMaker, ConvertMozTime)
+{
+ auto maker = RTCStatsTimestampMaker::Create();
+ const auto start = TimeStamp::Now();
+ RTCStatsTimestampMakerRealtimeClock clock(maker);
+ for (int i = 1000; i < 20000; i += 93) {
+ const auto duration = TimeDuration::FromMicroseconds(i);
+ const auto time = RTCStatsTimestamp::FromMozTime(maker, start + duration);
+ EXPECT_EQ(duration.ToMicroseconds(),
+ (time.ToMozTime() - start).ToMicroseconds())
+ << " for i=" << i;
+ }
+}
+
+TEST(RTCStatsTimestampMaker, ConvertRealtime)
+{
+ auto maker = RTCStatsTimestampMaker::Create();
+ const auto start = kWebrtcTimeOffset;
+ RTCStatsTimestampMakerRealtimeClock clock(maker);
+ for (int i = 1000; i < 20000; i += 93) {
+ const auto duration = webrtc::TimeDelta::Micros(i);
+ const auto time = RTCStatsTimestamp::FromRealtime(maker, start + duration);
+ // Because of precision differences, these round to a specific Microsecond
+ // slightly differently.
+ EXPECT_NEAR(duration.us(), (time.ToRealtime() - start).us(), 1)
+ << " for i=" << i;
+ }
+}
+
+TEST(RTCStatsTimestampMaker, Convert1Jan1970)
+{
+ auto maker = RTCStatsTimestampMaker::Create();
+ const auto start =
+ kWebrtcTimeOffset +
+ webrtc::TimeDelta::Millis(PerformanceService::GetOrCreate()->TimeOrigin(
+ WebrtcSystemTimeBase()));
+ RTCStatsTimestampMakerRealtimeClock clock(maker);
+ for (int i = 1000; i < 20000; i += 93) {
+ const auto duration = webrtc::TimeDelta::Micros(i);
+ const auto time = RTCStatsTimestamp::From1Jan1970(maker, start + duration);
+ // Because of precision differences, these round to a specific Microsecond
+ // slightly differently.
+ EXPECT_NEAR(duration.us(), (time.To1Jan1970() - start).us(), 1)
+ << " for i=" << i;
+ }
+}
+
+TEST(RTCStatsTimestampMaker, ConvertDomRealtime)
+{
+ auto maker = RTCStatsTimestampMaker::Create();
+ const auto start = kWebrtcTimeOffset;
+ RTCStatsTimestampMakerRealtimeClock clock(maker);
+ for (int i = 1000; i < 20000; i += 93) {
+ const auto duration = webrtc::TimeDelta::Micros(i);
+ const auto time =
+ RTCStatsTimestamp::FromDomRealtime(maker, start + duration);
+ // Because of precision differences, these round to a specific Microsecond
+ // slightly differently.
+ EXPECT_NEAR(duration.us(), (time.ToDomRealtime() - start).us(), 1)
+ << " for i=" << i;
+ }
+}
diff --git a/dom/media/gtest/TestRust.cpp b/dom/media/gtest/TestRust.cpp
new file mode 100644
index 0000000000..059500767f
--- /dev/null
+++ b/dom/media/gtest/TestRust.cpp
@@ -0,0 +1,10 @@
+#include <stdint.h>
+#include "gtest/gtest.h"
+
+extern "C" uint8_t* test_rust();
+
+TEST(rust, CallFromCpp)
+{
+ auto greeting = test_rust();
+ EXPECT_STREQ(reinterpret_cast<char*>(greeting), "hello from rust.");
+}
diff --git a/dom/media/gtest/TestTimeUnit.cpp b/dom/media/gtest/TestTimeUnit.cpp
new file mode 100644
index 0000000000..7b199376cc
--- /dev/null
+++ b/dom/media/gtest/TestTimeUnit.cpp
@@ -0,0 +1,281 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include <algorithm>
+#include <vector>
+
+#include "TimeUnits.h"
+
+using namespace mozilla;
+using namespace mozilla::media;
+using TimeUnit = mozilla::media::TimeUnit;
+
+TEST(TimeUnit, BasicArithmetic)
+{
+ const TimeUnit a(1000, 44100);
+ {
+ TimeUnit b = a * 10;
+ EXPECT_EQ(b.mBase, 44100);
+ EXPECT_EQ(b.mTicks.value(), a.mTicks.value() * 10);
+ EXPECT_EQ(a * 10, b);
+ }
+ {
+ TimeUnit b = a / 10;
+ EXPECT_EQ(b.mBase, 44100);
+ EXPECT_EQ(b.mTicks.value(), a.mTicks.value() / 10);
+ EXPECT_EQ(a / 10, b);
+ }
+ {
+ TimeUnit b = TimeUnit(10, 44100);
+ b += a;
+ EXPECT_EQ(b.mBase, 44100);
+ EXPECT_EQ(b.mTicks.value(), a.mTicks.value() + 10);
+ EXPECT_EQ(b - a, TimeUnit(10, 44100));
+ }
+ {
+ TimeUnit b = TimeUnit(1010, 44100);
+ b -= a; // now 10
+ EXPECT_EQ(b.mBase, 44100);
+ EXPECT_EQ(b.mTicks.value(), 10);
+ EXPECT_EQ(a + b, TimeUnit(1010, 44100));
+ }
+ {
+ TimeUnit b = TimeUnit(4010, 44100);
+ TimeUnit c = b % a; // now 10
+ EXPECT_EQ(c.mBase, 44100);
+ EXPECT_EQ(c.mTicks.value(), 10);
+ }
+ {
+ // Adding 6s in nanoseconds (e.g. coming from script) to a typical number
+ // from an mp4, 9001 in base 90000
+ TimeUnit b = TimeUnit(6000000000, 1000000000);
+ TimeUnit c = TimeUnit(9001, 90000);
+ TimeUnit d = c + b;
+ EXPECT_EQ(d.mBase, 90000);
+ EXPECT_EQ(d.mTicks.value(), 549001);
+ }
+ {
+ // Subtracting 9001 in base 9000 from 6s in nanoseconds (e.g. coming from
+ // script), converting to back to base 9000.
+ TimeUnit b = TimeUnit(6000000000, 1000000000);
+ TimeUnit c = TimeUnit(9001, 90000);
+ TimeUnit d = (b - c).ToBase(90000);
+ EXPECT_EQ(d.mBase, 90000);
+ EXPECT_EQ(d.mTicks.value(), 530998);
+ }
+}
+
+TEST(TimeUnit, Base)
+{
+ {
+ TimeUnit a = TimeUnit::FromSeconds(1);
+ EXPECT_EQ(a.mTicks.value(), 1000000);
+ EXPECT_EQ(a.mBase, 1000000);
+ }
+ {
+ TimeUnit a = TimeUnit::FromMicroseconds(44100000000);
+ EXPECT_EQ(a.mTicks.value(), 44100000000);
+ EXPECT_EQ(a.mBase, 1000000);
+ }
+ {
+ TimeUnit a = TimeUnit::FromSeconds(6.0);
+ EXPECT_EQ(a.mTicks.value(), 6000000);
+ EXPECT_EQ(a.mBase, 1000000);
+ double error;
+ TimeUnit b = a.ToBase(90000, error);
+ EXPECT_EQ(error, 0);
+ EXPECT_EQ(b.mTicks.value(), 540000);
+ EXPECT_EQ(b.mBase, 90000);
+ }
+}
+
+TEST(TimeUnit, Rounding)
+{
+ int64_t usecs = 662617;
+ double seconds = TimeUnit::FromMicroseconds(usecs).ToSeconds();
+ TimeUnit fromSeconds = TimeUnit::FromSeconds(seconds);
+ EXPECT_EQ(fromSeconds.mTicks.value(), usecs);
+ // TimeUnit base is microseconds if not explicitly passed.
+ EXPECT_EQ(fromSeconds.mBase, 1000000);
+ EXPECT_EQ(fromSeconds.ToMicroseconds(), usecs);
+
+ seconds = 4.169470123;
+ int64_t nsecs = 4169470123;
+ EXPECT_EQ(TimeUnit::FromSeconds(seconds, 1e9).ToNanoseconds(), nsecs);
+ EXPECT_EQ(TimeUnit::FromSeconds(seconds, 1e9).ToMicroseconds(), nsecs / 1000);
+
+ seconds = 2312312.16947012;
+ nsecs = 2312312169470120;
+ EXPECT_EQ(TimeUnit::FromSeconds(seconds, 1e9).ToNanoseconds(), nsecs);
+ EXPECT_EQ(TimeUnit::FromSeconds(seconds, 1e9).ToMicroseconds(), nsecs / 1000);
+
+ seconds = 2312312.169470123;
+ nsecs = 2312312169470123;
+ // A double doesn't have enough precision to roundtrip this time value
+ // correctly in this base, but the number of microseconds is still correct.
+ // This value is about 142.5 days however.
+ // This particular calculation results in exactly 1ns of difference after
+ // roundtrip. Enable this after remoing the MOZ_CRASH in TimeUnit::FromSeconds
+ // EXPECT_EQ(TimeUnit::FromSeconds(seconds, 1e9).ToNanoseconds() - nsecs, 1);
+ EXPECT_EQ(TimeUnit::FromSeconds(seconds, 1e9).ToMicroseconds(), nsecs / 1000);
+}
+
+TEST(TimeUnit, Comparisons)
+{
+ TimeUnit a(0, 1e9);
+ TimeUnit b(1, 1e9);
+ TimeUnit c(1, 1e6);
+
+ EXPECT_GE(b, a);
+ EXPECT_GE(c, a);
+ EXPECT_GE(c, b);
+
+ EXPECT_GT(b, a);
+ EXPECT_GT(c, a);
+ EXPECT_GT(c, b);
+
+ EXPECT_LE(a, b);
+ EXPECT_LE(a, c);
+ EXPECT_LE(b, c);
+
+ EXPECT_LT(a, b);
+ EXPECT_LT(a, c);
+ EXPECT_LT(b, c);
+
+ // Equivalence of zero regardless of the base
+ TimeUnit d(0, 1);
+ TimeUnit e(0, 1000);
+ EXPECT_EQ(a, d);
+ EXPECT_EQ(a, e);
+
+ // Equivalence of time accross bases
+ TimeUnit f(1000, 1e9);
+ TimeUnit g(1, 1e6);
+ EXPECT_EQ(f, g);
+
+ // Comparisons with infinity, same base
+ TimeUnit h = TimeUnit::FromInfinity();
+ TimeUnit i = TimeUnit::Zero();
+ EXPECT_LE(i, h);
+ EXPECT_LT(i, h);
+ EXPECT_GE(h, i);
+ EXPECT_GT(h, i);
+
+ // Comparisons with infinity, different base
+ TimeUnit j = TimeUnit::FromInfinity();
+ TimeUnit k = TimeUnit::Zero(1000000);
+ EXPECT_LE(k, j);
+ EXPECT_LT(k, j);
+ EXPECT_GE(j, k);
+ EXPECT_GT(j, k);
+
+ // Comparison of very big numbers, different base that have a gcd that makes
+ // it easy to reduce, to test the fraction reduction code
+ TimeUnit l = TimeUnit(123123120000000, 1000000000);
+ TimeUnit m = TimeUnit(123123120000000, 1000);
+ EXPECT_LE(l, m);
+ EXPECT_LT(l, m);
+ EXPECT_GE(m, l);
+ EXPECT_GT(m, l);
+
+ // Comparison of very big numbers, different base that are co-prime: worst
+ // cast scenario.
+ TimeUnit n = TimeUnit(123123123123123, 1000000000);
+ TimeUnit o = TimeUnit(123123123123123, 1000000001);
+ EXPECT_LE(o, n);
+ EXPECT_LT(o, n);
+ EXPECT_GE(n, o);
+ EXPECT_GT(n, o);
+
+ // Values taken from a real website (this is about 53 years, Date.now() in
+ // 2023).
+ TimeUnit leftBound(74332508253360, 44100);
+ TimeUnit rightBound(74332508297392, 44100);
+ TimeUnit fuzz(250000, 1000000);
+ TimeUnit time(1685544404790205, 1000000);
+
+ EXPECT_LT(leftBound - fuzz, time);
+ EXPECT_GT(time, leftBound - fuzz);
+ EXPECT_GE(rightBound + fuzz, time);
+ EXPECT_LT(time, rightBound + fuzz);
+
+ TimeUnit zero = TimeUnit::Zero(); // default base 1e6
+ TimeUnit datenow(
+ 151737439364679,
+ 90000); // Also from `Date.now()` in a common base for an mp4
+ EXPECT_NE(zero, datenow);
+}
+
+TEST(TimeUnit, InfinityMath)
+{
+ // Operator plus/minus uses floating point behaviour for positive and
+ // negative infinity values, i.e.:
+ // posInf + posInf = inf
+ // posInf + negInf = -nan
+ // posInf + finite = inf
+ // posInf - posInf = -nan
+ // posInf - negInf = inf
+ // posInf - finite = inf
+ // negInf + negInf = -inf
+ // negInf + posInf = -nan
+ // negInf + finite = -inf
+ // negInf - negInf = -nan
+ // negInf - posInf = -inf
+ // negInf - finite = -inf
+ // finite + posInf = inf
+ // finite - posInf = -inf
+ // finite + negInf = -inf
+ // finite - negInf = inf
+
+ const TimeUnit posInf = TimeUnit::FromInfinity();
+ EXPECT_EQ(TimeUnit::FromSeconds(mozilla::PositiveInfinity<double>()), posInf);
+
+ const TimeUnit negInf = TimeUnit::FromNegativeInfinity();
+ EXPECT_EQ(TimeUnit::FromSeconds(mozilla::NegativeInfinity<double>()), negInf);
+
+ EXPECT_EQ(posInf + posInf, posInf);
+ EXPECT_FALSE((posInf + negInf).IsValid());
+ EXPECT_FALSE((posInf - posInf).IsValid());
+ EXPECT_EQ(posInf - negInf, posInf);
+ EXPECT_EQ(negInf + negInf, negInf);
+ EXPECT_FALSE((negInf + posInf).IsValid());
+ EXPECT_FALSE((negInf - negInf).IsValid());
+ EXPECT_EQ(negInf - posInf, negInf);
+
+ const TimeUnit finite = TimeUnit::FromSeconds(42.0);
+ EXPECT_EQ(posInf - finite, posInf);
+ EXPECT_EQ(posInf + finite, posInf);
+ EXPECT_EQ(negInf - finite, negInf);
+ EXPECT_EQ(negInf + finite, negInf);
+
+ EXPECT_EQ(finite + posInf, posInf);
+ EXPECT_EQ(finite - posInf, negInf);
+ EXPECT_EQ(finite + negInf, negInf);
+ EXPECT_EQ(finite - negInf, posInf);
+}
+
+TEST(TimeUnit, BaseConversion)
+{
+ const int64_t packetSize = 1024; // typical for AAC
+ int64_t sampleRates[] = {16000, 44100, 48000, 88200, 96000};
+ const double hnsPerSeconds = 10000000.;
+ for (auto sampleRate : sampleRates) {
+ int64_t frameCount = 0;
+ TimeUnit pts;
+ do {
+ // Compute a time in hundreds of nanoseconds based of frame count, typical
+ // on Windows platform, checking that it round trips properly.
+ int64_t hns = AssertedCast<int64_t>(
+ std::round(hnsPerSeconds * static_cast<double>(frameCount) /
+ static_cast<double>(sampleRate)));
+ pts = TimeUnit::FromHns(hns, sampleRate);
+ EXPECT_EQ(
+ AssertedCast<int64_t>(std::round(pts.ToSeconds() * hnsPerSeconds)),
+ hns);
+ frameCount += packetSize;
+ } while (pts.ToSeconds() < 36000);
+ }
+}
diff --git a/dom/media/gtest/TestVPXDecoding.cpp b/dom/media/gtest/TestVPXDecoding.cpp
new file mode 100644
index 0000000000..d58ca24cc7
--- /dev/null
+++ b/dom/media/gtest/TestVPXDecoding.cpp
@@ -0,0 +1,96 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "mozilla/ArrayUtils.h"
+#include "nsTArray.h"
+#include "VPXDecoder.h"
+
+#include <stdio.h>
+
+using namespace mozilla;
+
+static void ReadVPXFile(const char* aPath, nsTArray<uint8_t>& aBuffer) {
+ FILE* f = fopen(aPath, "rb");
+ ASSERT_NE(f, (FILE*)nullptr);
+
+ int r = fseek(f, 0, SEEK_END);
+ ASSERT_EQ(r, 0);
+
+ long size = ftell(f);
+ ASSERT_NE(size, -1);
+ aBuffer.SetLength(size);
+
+ r = fseek(f, 0, SEEK_SET);
+ ASSERT_EQ(r, 0);
+
+ size_t got = fread(aBuffer.Elements(), 1, size, f);
+ ASSERT_EQ(got, size_t(size));
+
+ r = fclose(f);
+ ASSERT_EQ(r, 0);
+}
+
+static vpx_codec_iface_t* ParseIVFConfig(nsTArray<uint8_t>& data,
+ vpx_codec_dec_cfg_t& config) {
+ if (data.Length() < 32 + 12) {
+ // Not enough data for file & first frame headers.
+ return nullptr;
+ }
+ if (data[0] != 'D' || data[1] != 'K' || data[2] != 'I' || data[3] != 'F') {
+ // Expect 'DKIP'
+ return nullptr;
+ }
+ if (data[4] != 0 || data[5] != 0) {
+ // Expect version==0.
+ return nullptr;
+ }
+ if (data[8] != 'V' || data[9] != 'P' ||
+ (data[10] != '8' && data[10] != '9') || data[11] != '0') {
+ // Expect 'VP80' or 'VP90'.
+ return nullptr;
+ }
+ config.w = uint32_t(data[12]) || (uint32_t(data[13]) << 8);
+ config.h = uint32_t(data[14]) || (uint32_t(data[15]) << 8);
+ vpx_codec_iface_t* codec =
+ (data[10] == '8') ? vpx_codec_vp8_dx() : vpx_codec_vp9_dx();
+ // Remove headers, to just leave raw VPx data to be decoded.
+ data.RemoveElementsAt(0, 32 + 12);
+ return codec;
+}
+
+struct TestFileData {
+ const char* mFilename;
+ vpx_codec_err_t mDecodeResult;
+};
+static const TestFileData testFiles[] = {
+ {"test_case_1224361.vp8.ivf", VPX_CODEC_OK},
+ {"test_case_1224363.vp8.ivf", VPX_CODEC_CORRUPT_FRAME},
+ {"test_case_1224369.vp8.ivf", VPX_CODEC_CORRUPT_FRAME}};
+
+TEST(libvpx, test_cases)
+{
+ for (size_t test = 0; test < ArrayLength(testFiles); ++test) {
+ nsTArray<uint8_t> data;
+ ReadVPXFile(testFiles[test].mFilename, data);
+ ASSERT_GT(data.Length(), 0u);
+
+ vpx_codec_dec_cfg_t config;
+ vpx_codec_iface_t* dx = ParseIVFConfig(data, config);
+ ASSERT_TRUE(dx);
+ config.threads = 2;
+
+ vpx_codec_ctx_t ctx;
+ PodZero(&ctx);
+ vpx_codec_err_t r = vpx_codec_dec_init(&ctx, dx, &config, 0);
+ ASSERT_EQ(VPX_CODEC_OK, r);
+
+ r = vpx_codec_decode(&ctx, data.Elements(), data.Length(), nullptr, 0);
+ // This test case is known to be corrupt.
+ EXPECT_EQ(testFiles[test].mDecodeResult, r);
+
+ r = vpx_codec_destroy(&ctx);
+ EXPECT_EQ(VPX_CODEC_OK, r);
+ }
+}
diff --git a/dom/media/gtest/TestVideoFrameConverter.cpp b/dom/media/gtest/TestVideoFrameConverter.cpp
new file mode 100644
index 0000000000..2c18077886
--- /dev/null
+++ b/dom/media/gtest/TestVideoFrameConverter.cpp
@@ -0,0 +1,504 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <iterator>
+
+#include "gtest/gtest.h"
+#include "libwebrtcglue/SystemTime.h"
+#include "MediaEventSource.h"
+#include "VideoFrameConverter.h"
+#include "WaitFor.h"
+#include "YUVBufferGenerator.h"
+
+using namespace mozilla;
+
+class VideoFrameConverterTest;
+
+class FrameListener {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FrameListener)
+
+ explicit FrameListener(MediaEventSourceExc<webrtc::VideoFrame>& aSource) {
+ mListener = aSource.Connect(AbstractThread::GetCurrent(), this,
+ &FrameListener::OnVideoFrameConverted);
+ }
+
+ void OnVideoFrameConverted(webrtc::VideoFrame aVideoFrame) {
+ mVideoFrameConvertedEvent.Notify(std::move(aVideoFrame), TimeStamp::Now());
+ }
+
+ MediaEventSource<webrtc::VideoFrame, TimeStamp>& VideoFrameConvertedEvent() {
+ return mVideoFrameConvertedEvent;
+ }
+
+ private:
+ ~FrameListener() { mListener.Disconnect(); }
+
+ MediaEventListener mListener;
+ MediaEventProducer<webrtc::VideoFrame, TimeStamp> mVideoFrameConvertedEvent;
+};
+
+class DebugVideoFrameConverter : public VideoFrameConverter {
+ public:
+ explicit DebugVideoFrameConverter(
+ const dom::RTCStatsTimestampMaker& aTimestampMaker)
+ : VideoFrameConverter(aTimestampMaker) {}
+ using VideoFrameConverter::QueueForProcessing;
+};
+
+class VideoFrameConverterTest : public ::testing::Test {
+ protected:
+ const dom::RTCStatsTimestampMaker mTimestampMaker;
+ RefPtr<DebugVideoFrameConverter> mConverter;
+ RefPtr<FrameListener> mListener;
+
+ VideoFrameConverterTest()
+ : mTimestampMaker(dom::RTCStatsTimestampMaker::Create()),
+ mConverter(MakeAndAddRef<DebugVideoFrameConverter>(mTimestampMaker)),
+ mListener(MakeAndAddRef<FrameListener>(
+ mConverter->VideoFrameConvertedEvent())) {}
+
+ void TearDown() override { mConverter->Shutdown(); }
+
+ RefPtr<TakeNPromise<webrtc::VideoFrame, TimeStamp>> TakeNConvertedFrames(
+ size_t aN) {
+ return TakeN(mListener->VideoFrameConvertedEvent(), aN);
+ }
+};
+
+static bool IsPlane(const uint8_t* aData, int aWidth, int aHeight, int aStride,
+ uint8_t aValue) {
+ for (int i = 0; i < aHeight; ++i) {
+ for (int j = 0; j < aWidth; ++j) {
+ if (aData[i * aStride + j] != aValue) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+static bool IsFrameBlack(const webrtc::VideoFrame& aFrame) {
+ RefPtr<webrtc::I420BufferInterface> buffer =
+ aFrame.video_frame_buffer()->ToI420().get();
+ return IsPlane(buffer->DataY(), buffer->width(), buffer->height(),
+ buffer->StrideY(), 0x00) &&
+ IsPlane(buffer->DataU(), buffer->ChromaWidth(), buffer->ChromaHeight(),
+ buffer->StrideU(), 0x80) &&
+ IsPlane(buffer->DataV(), buffer->ChromaWidth(), buffer->ChromaHeight(),
+ buffer->StrideV(), 0x80);
+}
+
+VideoChunk GenerateChunk(int32_t aWidth, int32_t aHeight, TimeStamp aTime) {
+ YUVBufferGenerator generator;
+ generator.Init(gfx::IntSize(aWidth, aHeight));
+ VideoFrame f(generator.GenerateI420Image(), gfx::IntSize(aWidth, aHeight));
+ VideoChunk c;
+ c.mFrame.TakeFrom(&f);
+ c.mTimeStamp = aTime;
+ c.mDuration = 0;
+ return c;
+}
+
+TEST_F(VideoFrameConverterTest, BasicConversion) {
+ auto framesPromise = TakeNConvertedFrames(1);
+ TimeStamp now = TimeStamp::Now();
+ VideoChunk chunk = GenerateChunk(640, 480, now);
+ mConverter->SetActive(true);
+ mConverter->QueueVideoChunk(chunk, false);
+ auto frames = WaitFor(framesPromise).unwrap();
+ ASSERT_EQ(frames.size(), 1U);
+ const auto& [frame, conversionTime] = frames[0];
+ EXPECT_EQ(frame.width(), 640);
+ EXPECT_EQ(frame.height(), 480);
+ EXPECT_FALSE(IsFrameBlack(frame));
+ EXPECT_GT(conversionTime - now, TimeDuration::FromMilliseconds(0));
+}
+
+TEST_F(VideoFrameConverterTest, BasicPacing) {
+ auto framesPromise = TakeNConvertedFrames(1);
+ TimeStamp now = TimeStamp::Now();
+ TimeStamp future = now + TimeDuration::FromMilliseconds(100);
+ VideoChunk chunk = GenerateChunk(640, 480, future);
+ mConverter->SetActive(true);
+ mConverter->QueueVideoChunk(chunk, false);
+ auto frames = WaitFor(framesPromise).unwrap();
+ EXPECT_GT(TimeStamp::Now() - now, future - now);
+ ASSERT_EQ(frames.size(), 1U);
+ const auto& [frame, conversionTime] = frames[0];
+ EXPECT_EQ(frame.width(), 640);
+ EXPECT_EQ(frame.height(), 480);
+ EXPECT_FALSE(IsFrameBlack(frame));
+ EXPECT_GT(conversionTime - now, future - now);
+}
+
+TEST_F(VideoFrameConverterTest, MultiPacing) {
+ auto framesPromise = TakeNConvertedFrames(2);
+ TimeStamp now = TimeStamp::Now();
+ TimeStamp future1 = now + TimeDuration::FromMilliseconds(100);
+ TimeStamp future2 = now + TimeDuration::FromMilliseconds(200);
+ VideoChunk chunk = GenerateChunk(640, 480, future1);
+ mConverter->SetActive(true);
+ mConverter->QueueVideoChunk(chunk, false);
+ chunk = GenerateChunk(640, 480, future2);
+ mConverter->QueueVideoChunk(chunk, false);
+ auto frames = WaitFor(framesPromise).unwrap();
+ EXPECT_GT(TimeStamp::Now(), future2);
+ ASSERT_EQ(frames.size(), 2U);
+ const auto& [frame0, conversionTime0] = frames[0];
+ EXPECT_EQ(frame0.width(), 640);
+ EXPECT_EQ(frame0.height(), 480);
+ EXPECT_FALSE(IsFrameBlack(frame0));
+ EXPECT_GT(conversionTime0 - now, future1 - now);
+
+ const auto& [frame1, conversionTime1] = frames[1];
+ EXPECT_EQ(frame1.width(), 640);
+ EXPECT_EQ(frame1.height(), 480);
+ EXPECT_FALSE(IsFrameBlack(frame1));
+ EXPECT_GT(conversionTime1, future2);
+ EXPECT_GT(conversionTime1 - now, conversionTime0 - now);
+}
+
+TEST_F(VideoFrameConverterTest, Duplication) {
+ auto framesPromise = TakeNConvertedFrames(2);
+ TimeStamp now = TimeStamp::Now();
+ TimeStamp future1 = now + TimeDuration::FromMilliseconds(100);
+ VideoChunk chunk = GenerateChunk(640, 480, future1);
+ mConverter->SetActive(true);
+ mConverter->QueueVideoChunk(chunk, false);
+ auto frames = WaitFor(framesPromise).unwrap();
+ EXPECT_GT(TimeStamp::Now() - now, TimeDuration::FromMilliseconds(1100));
+ ASSERT_EQ(frames.size(), 2U);
+ const auto& [frame0, conversionTime0] = frames[0];
+ EXPECT_EQ(frame0.width(), 640);
+ EXPECT_EQ(frame0.height(), 480);
+ EXPECT_FALSE(IsFrameBlack(frame0));
+ EXPECT_GT(conversionTime0, future1);
+
+ const auto& [frame1, conversionTime1] = frames[1];
+ EXPECT_EQ(frame1.width(), 640);
+ EXPECT_EQ(frame1.height(), 480);
+ EXPECT_FALSE(IsFrameBlack(frame1));
+ EXPECT_GT(conversionTime1 - now, TimeDuration::FromMilliseconds(1100));
+ EXPECT_EQ(frame1.timestamp_us() - frame0.timestamp_us(), USECS_PER_S);
+
+ // Check that we re-used the old buffer.
+ EXPECT_EQ(frame0.video_frame_buffer(), frame1.video_frame_buffer());
+}
+
+TEST_F(VideoFrameConverterTest, DropsOld) {
+ auto framesPromise = TakeNConvertedFrames(1);
+ TimeStamp now = TimeStamp::Now();
+ TimeStamp future1 = now + TimeDuration::FromMilliseconds(1000);
+ TimeStamp future2 = now + TimeDuration::FromMilliseconds(100);
+ mConverter->SetActive(true);
+ mConverter->QueueVideoChunk(GenerateChunk(800, 600, future1), false);
+ mConverter->QueueVideoChunk(GenerateChunk(640, 480, future2), false);
+ auto frames = WaitFor(framesPromise).unwrap();
+ EXPECT_GT(TimeStamp::Now(), future2);
+ ASSERT_EQ(frames.size(), 1U);
+ const auto& [frame, conversionTime] = frames[0];
+ EXPECT_EQ(frame.width(), 640);
+ EXPECT_EQ(frame.height(), 480);
+ EXPECT_FALSE(IsFrameBlack(frame));
+ EXPECT_GT(conversionTime - now, future2 - now);
+}
+
+// We check that the disabling code was triggered by sending multiple,
+// different, frames to the converter within one second. While black, it shall
+// treat all frames identical and issue one black frame per second.
+// This version disables before queuing a frame. A frame will have to be
+// invented.
+TEST_F(VideoFrameConverterTest, BlackOnDisableCreated) {
+ auto framesPromise = TakeNConvertedFrames(2);
+ TimeStamp now = TimeStamp::Now();
+ TimeStamp future1 = now + TimeDuration::FromMilliseconds(100);
+ TimeStamp future2 = now + TimeDuration::FromMilliseconds(200);
+ TimeStamp future3 = now + TimeDuration::FromMilliseconds(400);
+ mConverter->SetActive(true);
+ mConverter->SetTrackEnabled(false);
+ mConverter->QueueVideoChunk(GenerateChunk(800, 600, future1), false);
+ mConverter->QueueVideoChunk(GenerateChunk(800, 600, future2), false);
+ mConverter->QueueVideoChunk(GenerateChunk(800, 600, future3), false);
+ auto frames = WaitFor(framesPromise).unwrap();
+ EXPECT_GT(TimeStamp::Now() - now, TimeDuration::FromSeconds(1));
+ ASSERT_EQ(frames.size(), 2U);
+ // The first frame was created instantly by SetTrackEnabled().
+ const auto& [frame0, conversionTime0] = frames[0];
+ EXPECT_EQ(frame0.width(), 640);
+ EXPECT_EQ(frame0.height(), 480);
+ EXPECT_TRUE(IsFrameBlack(frame0));
+ EXPECT_GE(conversionTime0 - now, TimeDuration::FromSeconds(0));
+ // The second frame was created by the same-frame timer (after 1s).
+ const auto& [frame1, conversionTime1] = frames[1];
+ EXPECT_EQ(frame1.width(), 640);
+ EXPECT_EQ(frame1.height(), 480);
+ EXPECT_TRUE(IsFrameBlack(frame1));
+ EXPECT_GT(conversionTime1 - now, TimeDuration::FromSeconds(1));
+ // Check that the second frame comes 1s after the first.
+ EXPECT_EQ(frame1.timestamp_us(), frame0.timestamp_us() + PR_USEC_PER_SEC);
+}
+
+// We check that the disabling code was triggered by sending multiple,
+// different, frames to the converter within one second. While black, it shall
+// treat all frames identical and issue one black frame per second.
+// This version queues a frame before disabling.
+TEST_F(VideoFrameConverterTest, BlackOnDisableDuplicated) {
+ TimeStamp now = TimeStamp::Now();
+ TimeStamp future1 = now + TimeDuration::FromMilliseconds(100);
+ TimeStamp future2 = now + TimeDuration::FromMilliseconds(200);
+ TimeStamp future3 = now + TimeDuration::FromMilliseconds(400);
+ mConverter->SetActive(true);
+ mConverter->QueueVideoChunk(GenerateChunk(800, 600, future1), false);
+ mConverter->QueueVideoChunk(GenerateChunk(800, 600, future2), false);
+ mConverter->QueueVideoChunk(GenerateChunk(800, 600, future3), false);
+
+ const auto [frame0, conversionTime0] =
+ WaitFor(TakeNConvertedFrames(1)).unwrap()[0];
+ mConverter->SetTrackEnabled(false);
+ // The first frame was queued.
+ EXPECT_EQ(frame0.width(), 800);
+ EXPECT_EQ(frame0.height(), 600);
+ EXPECT_FALSE(IsFrameBlack(frame0));
+ EXPECT_GT(conversionTime0 - now, future1 - now);
+
+ auto frames = WaitFor(TakeNConvertedFrames(2)).unwrap();
+ ASSERT_EQ(frames.size(), 2U);
+ // The second frame was duplicated by SetTrackEnabled.
+ const auto& [frame1, conversionTime1] = frames[0];
+ EXPECT_EQ(frame1.width(), 800);
+ EXPECT_EQ(frame1.height(), 600);
+ EXPECT_TRUE(IsFrameBlack(frame1));
+ EXPECT_GT(conversionTime1 - now, future1 - now);
+ // The third frame was created by the same-frame timer (after 1s).
+ const auto& [frame2, conversionTime2] = frames[1];
+ EXPECT_EQ(frame2.width(), 800);
+ EXPECT_EQ(frame2.height(), 600);
+ EXPECT_TRUE(IsFrameBlack(frame2));
+ EXPECT_GT(conversionTime2 - now,
+ future1 - now + TimeDuration::FromSeconds(1));
+ // Check that the third frame comes 1s after the second.
+ EXPECT_EQ(frame2.timestamp_us(), frame1.timestamp_us() + PR_USEC_PER_SEC);
+}
+
+TEST_F(VideoFrameConverterTest, ClearFutureFramesOnJumpingBack) {
+ TimeStamp start = TimeStamp::Now();
+ TimeStamp future1 = start + TimeDuration::FromMilliseconds(100);
+
+ auto framesPromise = TakeNConvertedFrames(1);
+ mConverter->SetActive(true);
+ mConverter->QueueVideoChunk(GenerateChunk(640, 480, future1), false);
+ auto frames = WaitFor(framesPromise).unwrap();
+
+ // We are now at t=100ms+. Queue a future frame and jump back in time to
+ // signal a reset.
+
+ framesPromise = TakeNConvertedFrames(1);
+ TimeStamp step1 = TimeStamp::Now();
+ ASSERT_GT(step1 - start, future1 - start);
+ TimeStamp future2 = step1 + TimeDuration::FromMilliseconds(200);
+ TimeStamp future3 = step1 + TimeDuration::FromMilliseconds(100);
+ ASSERT_LT(future2 - start, future1 + TimeDuration::FromSeconds(1) - start);
+ mConverter->QueueVideoChunk(GenerateChunk(800, 600, future2), false);
+ VideoChunk nullChunk;
+ nullChunk.mFrame = VideoFrame(nullptr, gfx::IntSize(800, 600));
+ nullChunk.mTimeStamp = step1;
+ mConverter->QueueVideoChunk(nullChunk, false);
+
+ // We queue one more chunk after the reset so we don't have to wait a full
+ // second for the same-frame timer. It has a different time and resolution
+ // so we can differentiate them.
+ mConverter->QueueVideoChunk(GenerateChunk(320, 240, future3), false);
+
+ {
+ auto newFrames = WaitFor(framesPromise).unwrap();
+ frames.insert(frames.end(), std::make_move_iterator(newFrames.begin()),
+ std::make_move_iterator(newFrames.end()));
+ }
+ TimeStamp step2 = TimeStamp::Now();
+ EXPECT_GT(step2 - start, future3 - start);
+ ASSERT_EQ(frames.size(), 2U);
+ const auto& [frame0, conversionTime0] = frames[0];
+ EXPECT_EQ(frame0.width(), 640);
+ EXPECT_EQ(frame0.height(), 480);
+ EXPECT_FALSE(IsFrameBlack(frame0));
+ EXPECT_GT(conversionTime0 - start, future1 - start);
+ const auto& [frame1, conversionTime1] = frames[1];
+ EXPECT_EQ(frame1.width(), 320);
+ EXPECT_EQ(frame1.height(), 240);
+ EXPECT_FALSE(IsFrameBlack(frame1));
+ EXPECT_GT(conversionTime1 - start, future3 - start);
+}
+
+// We check that the no frame is converted while inactive, and that on
+// activating the most recently queued frame gets converted.
+TEST_F(VideoFrameConverterTest, NoConversionsWhileInactive) {
+ auto framesPromise = TakeNConvertedFrames(1);
+ TimeStamp now = TimeStamp::Now();
+ TimeStamp future1 = now - TimeDuration::FromMilliseconds(1);
+ TimeStamp future2 = now;
+ mConverter->QueueVideoChunk(GenerateChunk(640, 480, future1), false);
+ mConverter->QueueVideoChunk(GenerateChunk(800, 600, future2), false);
+
+ // SetActive needs to follow the same async path as the frames to be in sync.
+ auto q = TaskQueue::Create(GetMediaThreadPool(MediaThreadType::WEBRTC_WORKER),
+ "VideoFrameConverterTest");
+ auto timer = MakeRefPtr<MediaTimer>(false);
+ timer->WaitFor(TimeDuration::FromMilliseconds(100), __func__)
+ ->Then(q, __func__,
+ [converter = mConverter] { converter->SetActive(true); });
+
+ auto frames = WaitFor(framesPromise).unwrap();
+ ASSERT_EQ(frames.size(), 1U);
+ const auto& [frame, conversionTime] = frames[0];
+ Unused << conversionTime;
+ EXPECT_EQ(frame.width(), 800);
+ EXPECT_EQ(frame.height(), 600);
+ EXPECT_FALSE(IsFrameBlack(frame));
+}
+
+TEST_F(VideoFrameConverterTest, TimestampPropagation) {
+ auto framesPromise = TakeNConvertedFrames(2);
+ TimeStamp now = TimeStamp::Now();
+ TimeDuration d1 = TimeDuration::FromMilliseconds(1);
+ TimeDuration d2 = TimeDuration::FromMilliseconds(29);
+
+ mConverter->SetActive(true);
+ mConverter->QueueVideoChunk(GenerateChunk(640, 480, now + d1), false);
+ mConverter->QueueVideoChunk(GenerateChunk(800, 600, now + d2), false);
+
+ auto frames = WaitFor(framesPromise).unwrap();
+ ASSERT_EQ(frames.size(), 2U);
+ const auto& [frame0, conversionTime0] = frames[0];
+ EXPECT_EQ(frame0.width(), 640);
+ EXPECT_EQ(frame0.height(), 480);
+ EXPECT_FALSE(IsFrameBlack(frame0));
+ EXPECT_EQ(frame0.timestamp_us(),
+ dom::RTCStatsTimestamp::FromMozTime(mTimestampMaker, now + d1)
+ .ToRealtime()
+ .us());
+ EXPECT_GE(conversionTime0 - now, d1);
+
+ const auto& [frame1, conversionTime1] = frames[1];
+ EXPECT_EQ(frame1.width(), 800);
+ EXPECT_EQ(frame1.height(), 600);
+ EXPECT_FALSE(IsFrameBlack(frame1));
+ EXPECT_EQ(frame1.timestamp_us(),
+ dom::RTCStatsTimestamp::FromMozTime(mTimestampMaker, now + d2)
+ .ToRealtime()
+ .us());
+ EXPECT_GE(conversionTime1 - now, d2);
+}
+
+TEST_F(VideoFrameConverterTest, IgnoreOldFrames) {
+ TimeStamp now = TimeStamp::Now();
+ TimeDuration d1 = TimeDuration::FromMilliseconds(100);
+ TimeDuration d2 = d1 + TimeDuration::FromMicroseconds(1);
+
+ auto framesPromise = TakeNConvertedFrames(1);
+ mConverter->SetActive(true);
+ mConverter->QueueVideoChunk(GenerateChunk(640, 480, now + d1), false);
+ auto frames = WaitFor(framesPromise).unwrap();
+
+ framesPromise = TakeNConvertedFrames(2);
+
+ // Time is now ~t1. This processes an extra frame using t=now().
+ mConverter->SetActive(false);
+ mConverter->SetActive(true);
+
+ // This processes a new chunk with an earlier timestamp than the extra frame
+ // above. But it gets processed after the extra frame, so time will appear to
+ // go backwards. This simulates a frame from the pacer being in flight when we
+ // flip SetActive() above. This frame is expected to get ignored.
+ Unused << WaitFor(InvokeAsync(mConverter->mTaskQueue, __func__, [&] {
+ mConverter->QueueForProcessing(
+ GenerateChunk(800, 600, now + d2).mFrame.GetImage(), now + d2,
+ gfx::IntSize(800, 600), false);
+ return GenericPromise::CreateAndResolve(true, __func__);
+ }));
+
+ {
+ auto newFrames = WaitFor(framesPromise).unwrap();
+ frames.insert(frames.end(), std::make_move_iterator(newFrames.begin()),
+ std::make_move_iterator(newFrames.end()));
+ }
+ ASSERT_EQ(frames.size(), 3U);
+ const auto& [frame0, conversionTime0] = frames[0];
+ EXPECT_EQ(frame0.width(), 640);
+ EXPECT_EQ(frame0.height(), 480);
+ EXPECT_FALSE(IsFrameBlack(frame0));
+ EXPECT_EQ(frame0.timestamp_us(),
+ dom::RTCStatsTimestamp::FromMozTime(mTimestampMaker, now + d1)
+ .ToRealtime()
+ .us());
+ EXPECT_GE(conversionTime0 - now, d1);
+
+ const auto& [frame1, conversionTime1] = frames[1];
+ EXPECT_EQ(frame1.width(), 640);
+ EXPECT_EQ(frame1.height(), 480);
+ EXPECT_FALSE(IsFrameBlack(frame1));
+ EXPECT_GT(frame1.timestamp_us(),
+ dom::RTCStatsTimestamp::FromMozTime(mTimestampMaker, now + d2)
+ .ToRealtime()
+ .us());
+ EXPECT_GE(conversionTime1 - now, d2);
+
+ const auto& [frame2, conversionTime2] = frames[2];
+ EXPECT_EQ(frame2.width(), 640);
+ EXPECT_EQ(frame2.height(), 480);
+ EXPECT_FALSE(IsFrameBlack(frame2));
+ EXPECT_EQ(frame2.timestamp_us(), frame1.timestamp_us() + USECS_PER_S);
+ EXPECT_GE(conversionTime2 - now, d2 + TimeDuration::FromSeconds(1));
+}
+
+TEST_F(VideoFrameConverterTest, SameFrameTimerRacingWithPacing) {
+ TimeStamp now = TimeStamp::Now();
+ TimeDuration d1 = TimeDuration::FromMilliseconds(100);
+ TimeDuration d2 =
+ d1 + TimeDuration::FromSeconds(1) - TimeDuration::FromMicroseconds(1);
+
+ auto framesPromise = TakeNConvertedFrames(3);
+ mConverter->SetActive(true);
+ mConverter->QueueVideoChunk(GenerateChunk(640, 480, now + d1), false);
+ mConverter->QueueVideoChunk(GenerateChunk(640, 480, now + d2), false);
+ auto frames = WaitFor(framesPromise).unwrap();
+
+ // The expected order here (in timestamps) is t1, t2, t2+1s.
+ //
+ // If the same-frame timer doesn't check what is queued we could end up with
+ // t1, t1+1s, t2.
+
+ ASSERT_EQ(frames.size(), 3U);
+ const auto& [frame0, conversionTime0] = frames[0];
+ EXPECT_EQ(frame0.width(), 640);
+ EXPECT_EQ(frame0.height(), 480);
+ EXPECT_FALSE(IsFrameBlack(frame0));
+ EXPECT_EQ(frame0.timestamp_us(),
+ dom::RTCStatsTimestamp::FromMozTime(mTimestampMaker, now + d1)
+ .ToRealtime()
+ .us());
+ EXPECT_GE(conversionTime0 - now, d1);
+
+ const auto& [frame1, conversionTime1] = frames[1];
+ EXPECT_EQ(frame1.width(), 640);
+ EXPECT_EQ(frame1.height(), 480);
+ EXPECT_FALSE(IsFrameBlack(frame1));
+ EXPECT_EQ(frame1.timestamp_us(),
+ dom::RTCStatsTimestamp::FromMozTime(mTimestampMaker, now + d2)
+ .ToRealtime()
+ .us());
+ EXPECT_GE(conversionTime1 - now, d2);
+
+ const auto& [frame2, conversionTime2] = frames[2];
+ EXPECT_EQ(frame2.width(), 640);
+ EXPECT_EQ(frame2.height(), 480);
+ EXPECT_FALSE(IsFrameBlack(frame2));
+ EXPECT_EQ(frame2.timestamp_us(),
+ dom::RTCStatsTimestamp::FromMozTime(
+ mTimestampMaker, now + d2 + TimeDuration::FromSeconds(1))
+ .ToRealtime()
+ .us());
+ EXPECT_GE(conversionTime2 - now, d2 + TimeDuration::FromSeconds(1));
+}
diff --git a/dom/media/gtest/TestVideoSegment.cpp b/dom/media/gtest/TestVideoSegment.cpp
new file mode 100644
index 0000000000..fd9f5ed285
--- /dev/null
+++ b/dom/media/gtest/TestVideoSegment.cpp
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "VideoSegment.h"
+
+using namespace mozilla;
+
+namespace mozilla::layer {
+class Image;
+} // namespace mozilla::layer
+
+TEST(VideoSegment, TestAppendFrameForceBlack)
+{
+ RefPtr<layers::Image> testImage = nullptr;
+
+ VideoSegment segment;
+ segment.AppendFrame(testImage.forget(), mozilla::gfx::IntSize(640, 480),
+ PRINCIPAL_HANDLE_NONE, true);
+
+ VideoSegment::ChunkIterator iter(segment);
+ while (!iter.IsEnded()) {
+ VideoChunk chunk = *iter;
+ EXPECT_TRUE(chunk.mFrame.GetForceBlack());
+ iter.Next();
+ }
+}
+
+TEST(VideoSegment, TestAppendFrameNotForceBlack)
+{
+ RefPtr<layers::Image> testImage = nullptr;
+
+ VideoSegment segment;
+ segment.AppendFrame(testImage.forget(), mozilla::gfx::IntSize(640, 480),
+ PRINCIPAL_HANDLE_NONE);
+
+ VideoSegment::ChunkIterator iter(segment);
+ while (!iter.IsEnded()) {
+ VideoChunk chunk = *iter;
+ EXPECT_FALSE(chunk.mFrame.GetForceBlack());
+ iter.Next();
+ }
+}
diff --git a/dom/media/gtest/TestVideoTrackEncoder.cpp b/dom/media/gtest/TestVideoTrackEncoder.cpp
new file mode 100644
index 0000000000..ee39213961
--- /dev/null
+++ b/dom/media/gtest/TestVideoTrackEncoder.cpp
@@ -0,0 +1,1467 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <algorithm>
+
+#include "DriftCompensation.h"
+#include "MediaTrackGraph.h"
+#include "MediaTrackListener.h"
+#include "VP8TrackEncoder.h"
+#include "WebMWriter.h" // TODO: it's weird to include muxer header to get the class definition of VP8 METADATA
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "mozilla/ArrayUtils.h"
+#include "prtime.h"
+
+#include "YUVBufferGenerator.h"
+
+#define VIDEO_TRACK_RATE 90000
+
+using ::testing::_;
+using ::testing::Invoke;
+using ::testing::NiceMock;
+using ::testing::TestWithParam;
+using ::testing::Values;
+
+using namespace mozilla::layers;
+using namespace mozilla;
+
+struct InitParam {
+ bool mShouldSucceed; // This parameter should cause success or fail result
+ int mWidth; // frame width
+ int mHeight; // frame height
+};
+
+class MockDriftCompensator : public DriftCompensator {
+ public:
+ MockDriftCompensator()
+ : DriftCompensator(GetCurrentSerialEventTarget(), VIDEO_TRACK_RATE) {
+ ON_CALL(*this, GetVideoTime(_, _))
+ .WillByDefault(Invoke([](TimeStamp, TimeStamp t) { return t; }));
+ }
+
+ MOCK_METHOD2(GetVideoTime, TimeStamp(TimeStamp, TimeStamp));
+};
+
+class TestVP8TrackEncoder : public VP8TrackEncoder {
+ public:
+ explicit TestVP8TrackEncoder(Maybe<float> aKeyFrameIntervalFactor = Nothing())
+ : VP8TrackEncoder(MakeRefPtr<NiceMock<MockDriftCompensator>>(),
+ VIDEO_TRACK_RATE, mEncodedVideoQueue,
+ FrameDroppingMode::DISALLOW, aKeyFrameIntervalFactor) {}
+
+ MockDriftCompensator* DriftCompensator() {
+ return static_cast<MockDriftCompensator*>(mDriftCompensator.get());
+ }
+
+ ::testing::AssertionResult TestInit(const InitParam& aParam) {
+ nsresult result =
+ Init(aParam.mWidth, aParam.mHeight, aParam.mWidth, aParam.mHeight, 30);
+
+ if (((NS_FAILED(result) && aParam.mShouldSucceed)) ||
+ (NS_SUCCEEDED(result) && !aParam.mShouldSucceed)) {
+ return ::testing::AssertionFailure()
+ << " width = " << aParam.mWidth << " height = " << aParam.mHeight;
+ }
+
+ return ::testing::AssertionSuccess();
+ }
+
+ MediaQueue<EncodedFrame> mEncodedVideoQueue;
+};
+
+// Init test
+TEST(VP8VideoTrackEncoder, Initialization)
+{
+ InitParam params[] = {
+ // Failure cases.
+ {false, 0, 0}, // Height/ width should be larger than 1.
+ {false, 0, 1}, // Height/ width should be larger than 1.
+ {false, 1, 0}, // Height/ width should be larger than 1.
+
+ // Success cases
+ {true, 640, 480}, // Standard VGA
+ {true, 800, 480}, // Standard WVGA
+ {true, 960, 540}, // Standard qHD
+ {true, 1280, 720} // Standard HD
+ };
+
+ for (const InitParam& param : params) {
+ TestVP8TrackEncoder encoder;
+ EXPECT_TRUE(encoder.TestInit(param));
+ }
+}
+
+// Get MetaData test
+TEST(VP8VideoTrackEncoder, FetchMetaData)
+{
+ InitParam params[] = {
+ // Success cases
+ {true, 640, 480}, // Standard VGA
+ {true, 800, 480}, // Standard WVGA
+ {true, 960, 540}, // Standard qHD
+ {true, 1280, 720} // Standard HD
+ };
+
+ for (const InitParam& param : params) {
+ TestVP8TrackEncoder encoder;
+ EXPECT_TRUE(encoder.TestInit(param));
+
+ RefPtr<TrackMetadataBase> meta = encoder.GetMetadata();
+ RefPtr<VP8Metadata> vp8Meta(static_cast<VP8Metadata*>(meta.get()));
+
+ // METADATA should be depend on how to initiate encoder.
+ EXPECT_EQ(vp8Meta->mWidth, param.mWidth);
+ EXPECT_EQ(vp8Meta->mHeight, param.mHeight);
+ }
+}
+
+// Encode test
+TEST(VP8VideoTrackEncoder, FrameEncode)
+{
+ TestVP8TrackEncoder encoder;
+ TimeStamp now = TimeStamp::Now();
+
+ // Create YUV images as source.
+ nsTArray<RefPtr<Image>> images;
+ YUVBufferGenerator generator;
+ generator.Init(mozilla::gfx::IntSize(640, 480));
+ images.AppendElement(generator.GenerateI420Image());
+ images.AppendElement(generator.GenerateNV12Image());
+ images.AppendElement(generator.GenerateNV21Image());
+
+ // Put generated YUV frame into video segment.
+ // Duration of each frame is 1 second.
+ VideoSegment segment;
+ for (nsTArray<RefPtr<Image>>::size_type i = 0; i < images.Length(); i++) {
+ RefPtr<Image> image = images[i];
+ segment.AppendFrame(image.forget(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + TimeDuration::FromSeconds(i));
+ }
+
+ encoder.SetStartOffset(now);
+ encoder.AppendVideoSegment(std::move(segment));
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(images.Length()));
+ encoder.NotifyEndOfStream();
+
+ EXPECT_TRUE(encoder.IsEncodingComplete());
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.IsFinished());
+ EXPECT_FALSE(encoder.mEncodedVideoQueue.AtEndOfStream());
+}
+
+// Test that encoding a single frame gives useful output.
+TEST(VP8VideoTrackEncoder, SingleFrameEncode)
+{
+ TestVP8TrackEncoder encoder;
+ TimeStamp now = TimeStamp::Now();
+ YUVBufferGenerator generator;
+ generator.Init(mozilla::gfx::IntSize(640, 480));
+
+ // Pass a half-second frame to the encoder.
+ VideoSegment segment;
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false, now);
+
+ encoder.SetStartOffset(now);
+ encoder.AppendVideoSegment(std::move(segment));
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(0.5));
+ encoder.NotifyEndOfStream();
+
+ EXPECT_TRUE(encoder.IsEncodingComplete());
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.IsFinished());
+ EXPECT_FALSE(encoder.mEncodedVideoQueue.AtEndOfStream());
+
+ // Read out encoded data, and verify.
+ RefPtr<EncodedFrame> frame = encoder.mEncodedVideoQueue.PopFront();
+ EXPECT_EQ(EncodedFrame::VP8_I_FRAME, frame->mFrameType)
+ << "We only have one frame, so it should be a keyframe";
+
+ const uint64_t halfSecond = PR_USEC_PER_SEC / 2;
+ EXPECT_EQ(halfSecond, frame->mDuration);
+
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.AtEndOfStream());
+}
+
+// Test that encoding a couple of identical images gives useful output.
+TEST(VP8VideoTrackEncoder, SameFrameEncode)
+{
+ TestVP8TrackEncoder encoder;
+ TimeStamp now = TimeStamp::Now();
+ YUVBufferGenerator generator;
+ generator.Init(mozilla::gfx::IntSize(640, 480));
+
+ // Pass 15 100ms frames to the encoder.
+ RefPtr<Image> image = generator.GenerateI420Image();
+ VideoSegment segment;
+ for (uint32_t i = 0; i < 15; ++i) {
+ segment.AppendFrame(do_AddRef(image), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + TimeDuration::FromSeconds(i * 0.1));
+ }
+
+ encoder.SetStartOffset(now);
+ encoder.AppendVideoSegment(std::move(segment));
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(1.5));
+ encoder.NotifyEndOfStream();
+
+ EXPECT_TRUE(encoder.IsEncodingComplete());
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.IsFinished());
+ EXPECT_FALSE(encoder.mEncodedVideoQueue.AtEndOfStream());
+
+ // Verify total duration being 1.5s.
+ uint64_t totalDuration = 0;
+ while (RefPtr<EncodedFrame> frame = encoder.mEncodedVideoQueue.PopFront()) {
+ totalDuration += frame->mDuration;
+ }
+ const uint64_t oneAndAHalf = (PR_USEC_PER_SEC / 2) * 3;
+ EXPECT_EQ(oneAndAHalf, totalDuration);
+}
+
+// Test encoding a track that has to skip frames.
+TEST(VP8VideoTrackEncoder, SkippedFrames)
+{
+ TestVP8TrackEncoder encoder;
+ YUVBufferGenerator generator;
+ generator.Init(mozilla::gfx::IntSize(640, 480));
+ TimeStamp now = TimeStamp::Now();
+
+ // Pass 100 frames of the shortest possible duration where we don't get
+ // rounding errors between input/output rate.
+ VideoSegment segment;
+ for (uint32_t i = 0; i < 100; ++i) {
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + TimeDuration::FromMilliseconds(i));
+ }
+
+ encoder.SetStartOffset(now);
+ encoder.AppendVideoSegment(std::move(segment));
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromMilliseconds(100));
+ encoder.NotifyEndOfStream();
+
+ EXPECT_TRUE(encoder.IsEncodingComplete());
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.IsFinished());
+ EXPECT_FALSE(encoder.mEncodedVideoQueue.AtEndOfStream());
+
+ // Verify total duration being 100 * 1ms = 100ms.
+ uint64_t totalDuration = 0;
+ while (RefPtr<EncodedFrame> frame = encoder.mEncodedVideoQueue.PopFront()) {
+ totalDuration += frame->mDuration;
+ }
+ const uint64_t hundredMillis = PR_USEC_PER_SEC / 10;
+ EXPECT_EQ(hundredMillis, totalDuration);
+}
+
+// Test encoding a track with frames subject to rounding errors.
+TEST(VP8VideoTrackEncoder, RoundingErrorFramesEncode)
+{
+ TestVP8TrackEncoder encoder;
+ YUVBufferGenerator generator;
+ generator.Init(mozilla::gfx::IntSize(640, 480));
+ TimeStamp now = TimeStamp::Now();
+
+ // Pass nine frames with timestamps not expressable in 90kHz sample rate,
+ // then one frame to make the total duration close to one second.
+ VideoSegment segment;
+ uint32_t usPerFrame = 99999; // 99.999ms
+ for (uint32_t i = 0; i < 9; ++i) {
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + TimeDuration::FromMicroseconds(i * usPerFrame));
+ }
+
+ // This last frame has timestamp start + 0.9s and duration 0.1s.
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + TimeDuration::FromSeconds(0.9));
+
+ encoder.SetStartOffset(now);
+ encoder.AppendVideoSegment(std::move(segment));
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(1));
+ encoder.NotifyEndOfStream();
+
+ EXPECT_TRUE(encoder.IsEncodingComplete());
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.IsFinished());
+ EXPECT_FALSE(encoder.mEncodedVideoQueue.AtEndOfStream());
+
+ // Verify total duration being 1s.
+ uint64_t totalDuration = 0;
+ while (RefPtr<EncodedFrame> frame = encoder.mEncodedVideoQueue.PopFront()) {
+ totalDuration += frame->mDuration;
+ }
+ // Not exact, the stream is encoded in time base 90kHz.
+ const uint64_t oneSecond = PR_USEC_PER_SEC - 1;
+ EXPECT_EQ(oneSecond, totalDuration);
+}
+
+// Test that we're encoding timestamps rather than durations.
+TEST(VP8VideoTrackEncoder, TimestampFrameEncode)
+{
+ TestVP8TrackEncoder encoder;
+ YUVBufferGenerator generator;
+ generator.Init(mozilla::gfx::IntSize(640, 480));
+ TimeStamp now = TimeStamp::Now();
+
+ VideoSegment segment;
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false, now);
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + TimeDuration::FromSeconds(0.05));
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + TimeDuration::FromSeconds(0.2));
+
+ encoder.SetStartOffset(now);
+ encoder.AppendVideoSegment(std::move(segment));
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(0.3));
+ encoder.NotifyEndOfStream();
+
+ EXPECT_TRUE(encoder.IsEncodingComplete());
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.IsFinished());
+ EXPECT_FALSE(encoder.mEncodedVideoQueue.AtEndOfStream());
+
+ // Verify total duration being 0.3s and individual frames being [0.05s, 0.15s,
+ // 0.1s]
+ uint64_t expectedDurations[] = {(PR_USEC_PER_SEC / 10) / 2,
+ (PR_USEC_PER_SEC / 10) * 3 / 2,
+ (PR_USEC_PER_SEC / 10)};
+ uint64_t totalDuration = 0;
+ size_t i = 0;
+ while (RefPtr<EncodedFrame> frame = encoder.mEncodedVideoQueue.PopFront()) {
+ EXPECT_EQ(expectedDurations[i], frame->mDuration);
+ i++;
+ totalDuration += frame->mDuration;
+ }
+ const uint64_t pointThree = (PR_USEC_PER_SEC / 10) * 3;
+ EXPECT_EQ(pointThree, totalDuration);
+}
+
+// Test that we're compensating for drift when encoding.
+TEST(VP8VideoTrackEncoder, DriftingFrameEncode)
+{
+ TestVP8TrackEncoder encoder;
+ YUVBufferGenerator generator;
+ generator.Init(mozilla::gfx::IntSize(640, 480));
+ TimeStamp now = TimeStamp::Now();
+
+ // Set up major drift -- audio that goes twice as fast as video.
+ // This should make the given video durations double as they get encoded.
+ EXPECT_CALL(*encoder.DriftCompensator(), GetVideoTime(_, _))
+ .WillRepeatedly(Invoke(
+ [&](TimeStamp, TimeStamp aTime) { return now + (aTime - now) * 2; }));
+
+ VideoSegment segment;
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false, now);
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + TimeDuration::FromSeconds(0.05));
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + TimeDuration::FromSeconds(0.2));
+
+ encoder.SetStartOffset(now);
+ encoder.AppendVideoSegment(std::move(segment));
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(0.3));
+ encoder.NotifyEndOfStream();
+
+ EXPECT_TRUE(encoder.IsEncodingComplete());
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.IsFinished());
+ EXPECT_FALSE(encoder.mEncodedVideoQueue.AtEndOfStream());
+
+ // Verify total duration being 0.6s and individual frames being [0.1s, 0.3s,
+ // 0.2s]
+ uint64_t expectedDurations[] = {(PR_USEC_PER_SEC / 10),
+ (PR_USEC_PER_SEC / 10) * 3,
+ (PR_USEC_PER_SEC / 10) * 2};
+ uint64_t totalDuration = 0;
+ size_t i = 0;
+ while (RefPtr<EncodedFrame> frame = encoder.mEncodedVideoQueue.PopFront()) {
+ EXPECT_EQ(expectedDurations[i], frame->mDuration);
+ i++;
+ totalDuration += frame->mDuration;
+ }
+ const uint64_t pointSix = (PR_USEC_PER_SEC / 10) * 6;
+ EXPECT_EQ(pointSix, totalDuration);
+}
+
+// Test that suspending an encoding works.
+TEST(VP8VideoTrackEncoder, Suspended)
+{
+ TestVP8TrackEncoder encoder;
+ TimeStamp now = TimeStamp::Now();
+ YUVBufferGenerator generator;
+ generator.Init(mozilla::gfx::IntSize(640, 480));
+
+ // Pass 3 frames with duration 0.1s. We suspend before and resume after the
+ // second frame.
+
+ {
+ VideoSegment segment;
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false, now);
+
+ encoder.SetStartOffset(now);
+ encoder.AppendVideoSegment(std::move(segment));
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(0.1));
+ }
+
+ encoder.Suspend(now + TimeDuration::FromSeconds(0.1));
+
+ {
+ VideoSegment segment;
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + TimeDuration::FromSeconds(0.1));
+ encoder.AppendVideoSegment(std::move(segment));
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(0.2));
+ }
+
+ encoder.Resume(now + TimeDuration::FromSeconds(0.2));
+
+ {
+ VideoSegment segment;
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + TimeDuration::FromSeconds(0.2));
+ encoder.AppendVideoSegment(std::move(segment));
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(0.3));
+ }
+
+ encoder.NotifyEndOfStream();
+
+ EXPECT_TRUE(encoder.IsEncodingComplete());
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.IsFinished());
+ EXPECT_FALSE(encoder.mEncodedVideoQueue.AtEndOfStream());
+
+ // Verify that we have two encoded frames and a total duration of 0.2s.
+ uint64_t count = 0;
+ uint64_t totalDuration = 0;
+ while (RefPtr<EncodedFrame> frame = encoder.mEncodedVideoQueue.PopFront()) {
+ ++count;
+ totalDuration += frame->mDuration;
+ }
+ const uint64_t two = 2;
+ EXPECT_EQ(two, count);
+ const uint64_t pointTwo = (PR_USEC_PER_SEC / 10) * 2;
+ EXPECT_EQ(pointTwo, totalDuration);
+}
+
+// Test that ending a track while the video track encoder is suspended works.
+TEST(VP8VideoTrackEncoder, SuspendedUntilEnd)
+{
+ TestVP8TrackEncoder encoder;
+ YUVBufferGenerator generator;
+ generator.Init(mozilla::gfx::IntSize(640, 480));
+ TimeStamp now = TimeStamp::Now();
+
+ // Pass 2 frames with duration 0.1s. We suspend before the second frame.
+
+ {
+ VideoSegment segment;
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false, now);
+
+ encoder.SetStartOffset(now);
+ encoder.AppendVideoSegment(std::move(segment));
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(0.1));
+ }
+
+ encoder.Suspend(now + TimeDuration::FromSeconds(0.1));
+
+ {
+ VideoSegment segment;
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + TimeDuration::FromSeconds(0.1));
+ encoder.AppendVideoSegment(std::move(segment));
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(0.2));
+ }
+
+ encoder.NotifyEndOfStream();
+
+ EXPECT_TRUE(encoder.IsEncodingComplete());
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.IsFinished());
+ EXPECT_FALSE(encoder.mEncodedVideoQueue.AtEndOfStream());
+
+ // Verify that we have one encoded frames and a total duration of 0.1s.
+ uint64_t count = 0;
+ uint64_t totalDuration = 0;
+ while (RefPtr<EncodedFrame> frame = encoder.mEncodedVideoQueue.PopFront()) {
+ ++count;
+ totalDuration += frame->mDuration;
+ }
+ const uint64_t one = 1;
+ EXPECT_EQ(one, count);
+ const uint64_t pointOne = PR_USEC_PER_SEC / 10;
+ EXPECT_EQ(pointOne, totalDuration);
+}
+
+// Test that ending a track that was always suspended works.
+TEST(VP8VideoTrackEncoder, AlwaysSuspended)
+{
+ TestVP8TrackEncoder encoder;
+ YUVBufferGenerator generator;
+ generator.Init(mozilla::gfx::IntSize(640, 480));
+ TimeStamp now = TimeStamp::Now();
+
+ // Suspend and then pass a frame with duration 2s.
+
+ encoder.Suspend(now);
+
+ VideoSegment segment;
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false, now);
+
+ encoder.SetStartOffset(now);
+ encoder.AppendVideoSegment(std::move(segment));
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(2));
+
+ encoder.NotifyEndOfStream();
+
+ // Verify that we have no encoded frames.
+ EXPECT_TRUE(encoder.IsEncodingComplete());
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.IsFinished());
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.AtEndOfStream());
+}
+
+// Test that encoding a track that is suspended in the beginning works.
+TEST(VP8VideoTrackEncoder, SuspendedBeginning)
+{
+ TestVP8TrackEncoder encoder;
+ YUVBufferGenerator generator;
+ generator.Init(mozilla::gfx::IntSize(640, 480));
+ TimeStamp now = TimeStamp::Now();
+
+ // Suspend and pass a frame with duration 0.5s. Then resume and pass one more.
+ encoder.Suspend(now);
+
+ {
+ VideoSegment segment;
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false, now);
+
+ encoder.SetStartOffset(now);
+ encoder.AppendVideoSegment(std::move(segment));
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(0.5));
+ }
+
+ encoder.Resume(now + TimeDuration::FromSeconds(0.5));
+
+ {
+ VideoSegment segment;
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + TimeDuration::FromSeconds(0.5));
+ encoder.AppendVideoSegment(std::move(segment));
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(1));
+ }
+
+ encoder.NotifyEndOfStream();
+
+ EXPECT_TRUE(encoder.IsEncodingComplete());
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.IsFinished());
+ EXPECT_FALSE(encoder.mEncodedVideoQueue.AtEndOfStream());
+
+ // Verify that we have one encoded frames and a total duration of 0.1s.
+ uint64_t count = 0;
+ uint64_t totalDuration = 0;
+ while (RefPtr<EncodedFrame> frame = encoder.mEncodedVideoQueue.PopFront()) {
+ ++count;
+ totalDuration += frame->mDuration;
+ }
+ const uint64_t one = 1;
+ EXPECT_EQ(one, count);
+ const uint64_t half = PR_USEC_PER_SEC / 2;
+ EXPECT_EQ(half, totalDuration);
+}
+
+// Test that suspending and resuming in the middle of already pushed data
+// works.
+TEST(VP8VideoTrackEncoder, SuspendedOverlap)
+{
+ TestVP8TrackEncoder encoder;
+ YUVBufferGenerator generator;
+ generator.Init(mozilla::gfx::IntSize(640, 480));
+ TimeStamp now = TimeStamp::Now();
+
+ {
+ // Pass a 1s frame and suspend after 0.5s.
+ VideoSegment segment;
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false, now);
+
+ encoder.SetStartOffset(now);
+ encoder.AppendVideoSegment(std::move(segment));
+ }
+
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(0.5));
+ encoder.Suspend(now + TimeDuration::FromSeconds(0.5));
+
+ {
+ // Pass another 1s frame and resume after 0.3 of this new frame.
+ VideoSegment segment;
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + TimeDuration::FromSeconds(1));
+ encoder.AppendVideoSegment(std::move(segment));
+ }
+
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(1.3));
+ encoder.Resume(now + TimeDuration::FromSeconds(1.3));
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(2));
+ encoder.NotifyEndOfStream();
+
+ EXPECT_TRUE(encoder.IsEncodingComplete());
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.IsFinished());
+ EXPECT_FALSE(encoder.mEncodedVideoQueue.AtEndOfStream());
+
+ // Verify that we have two encoded frames and a total duration of 0.1s.
+ RefPtr<EncodedFrame> frame = encoder.mEncodedVideoQueue.PopFront();
+ const uint64_t pointFive = (PR_USEC_PER_SEC / 10) * 5;
+ EXPECT_EQ(pointFive, frame->mDuration);
+ frame = encoder.mEncodedVideoQueue.PopFront();
+ const uint64_t pointSeven = (PR_USEC_PER_SEC / 10) * 7;
+ EXPECT_EQ(pointSeven, frame->mDuration);
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.AtEndOfStream());
+}
+
+// Test that ending a track in the middle of already pushed data works.
+TEST(VP8VideoTrackEncoder, PrematureEnding)
+{
+ TestVP8TrackEncoder encoder;
+ YUVBufferGenerator generator;
+ generator.Init(mozilla::gfx::IntSize(640, 480));
+ TimeStamp now = TimeStamp::Now();
+
+ // Pass a 1s frame and end the track after 0.5s.
+ VideoSegment segment;
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false, now);
+
+ encoder.SetStartOffset(now);
+ encoder.AppendVideoSegment(std::move(segment));
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(0.5));
+ encoder.NotifyEndOfStream();
+
+ EXPECT_TRUE(encoder.IsEncodingComplete());
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.IsFinished());
+ EXPECT_FALSE(encoder.mEncodedVideoQueue.AtEndOfStream());
+
+ uint64_t totalDuration = 0;
+ while (RefPtr<EncodedFrame> frame = encoder.mEncodedVideoQueue.PopFront()) {
+ totalDuration += frame->mDuration;
+ }
+ const uint64_t half = PR_USEC_PER_SEC / 2;
+ EXPECT_EQ(half, totalDuration);
+}
+
+// Test that a track that starts at t > 0 works as expected.
+TEST(VP8VideoTrackEncoder, DelayedStart)
+{
+ TestVP8TrackEncoder encoder;
+ YUVBufferGenerator generator;
+ generator.Init(mozilla::gfx::IntSize(640, 480));
+ TimeStamp now = TimeStamp::Now();
+
+ // Pass a 2s frame, start (pass first CurrentTime) at 0.5s, end at 1s.
+ // Should result in a 0.5s encoding.
+ VideoSegment segment;
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false, now);
+
+ encoder.SetStartOffset(now + TimeDuration::FromSeconds(0.5));
+ encoder.AppendVideoSegment(std::move(segment));
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(1));
+ encoder.NotifyEndOfStream();
+
+ EXPECT_TRUE(encoder.IsEncodingComplete());
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.IsFinished());
+ EXPECT_FALSE(encoder.mEncodedVideoQueue.AtEndOfStream());
+
+ uint64_t totalDuration = 0;
+ while (RefPtr<EncodedFrame> frame = encoder.mEncodedVideoQueue.PopFront()) {
+ totalDuration += frame->mDuration;
+ }
+ const uint64_t half = PR_USEC_PER_SEC / 2;
+ EXPECT_EQ(half, totalDuration);
+}
+
+// Test that a track that starts at t > 0 works as expected, when
+// SetStartOffset comes after AppendVideoSegment.
+TEST(VP8VideoTrackEncoder, DelayedStartOtherEventOrder)
+{
+ TestVP8TrackEncoder encoder;
+ YUVBufferGenerator generator;
+ generator.Init(mozilla::gfx::IntSize(640, 480));
+ TimeStamp now = TimeStamp::Now();
+
+ // Pass a 2s frame, start (pass first CurrentTime) at 0.5s, end at 1s.
+ // Should result in a 0.5s encoding.
+ VideoSegment segment;
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false, now);
+
+ encoder.AppendVideoSegment(std::move(segment));
+ encoder.SetStartOffset(now + TimeDuration::FromSeconds(0.5));
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(1));
+ encoder.NotifyEndOfStream();
+
+ EXPECT_TRUE(encoder.IsEncodingComplete());
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.IsFinished());
+ EXPECT_FALSE(encoder.mEncodedVideoQueue.AtEndOfStream());
+
+ uint64_t totalDuration = 0;
+ while (RefPtr<EncodedFrame> frame = encoder.mEncodedVideoQueue.PopFront()) {
+ totalDuration += frame->mDuration;
+ }
+ const uint64_t half = PR_USEC_PER_SEC / 2;
+ EXPECT_EQ(half, totalDuration);
+}
+
+// Test that a track that starts at t >>> 0 works as expected.
+TEST(VP8VideoTrackEncoder, VeryDelayedStart)
+{
+ TestVP8TrackEncoder encoder;
+ YUVBufferGenerator generator;
+ generator.Init(mozilla::gfx::IntSize(640, 480));
+ TimeStamp now = TimeStamp::Now();
+
+ // Pass a 1s frame, start (pass first CurrentTime) at 10s, end at 10.5s.
+ // Should result in a 0.5s encoding.
+ VideoSegment segment;
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false, now);
+
+ encoder.SetStartOffset(now + TimeDuration::FromSeconds(10));
+ encoder.AppendVideoSegment(std::move(segment));
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(10.5));
+ encoder.NotifyEndOfStream();
+
+ EXPECT_TRUE(encoder.IsEncodingComplete());
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.IsFinished());
+ EXPECT_FALSE(encoder.mEncodedVideoQueue.AtEndOfStream());
+
+ uint64_t totalDuration = 0;
+ while (RefPtr<EncodedFrame> frame = encoder.mEncodedVideoQueue.PopFront()) {
+ totalDuration += frame->mDuration;
+ }
+ const uint64_t half = PR_USEC_PER_SEC / 2;
+ EXPECT_EQ(half, totalDuration);
+}
+
+// Test that a video frame that hangs around for a long time gets encoded
+// every second.
+TEST(VP8VideoTrackEncoder, LongFramesReEncoded)
+{
+ TestVP8TrackEncoder encoder;
+ YUVBufferGenerator generator;
+ generator.Init(mozilla::gfx::IntSize(640, 480));
+ TimeStamp now = TimeStamp::Now();
+
+ // Pass a frame at t=0 and start encoding.
+ // Advancing the current time by 6.5s should encode six 1s frames.
+ // Advancing the current time by another 5.5s should encode another five 1s
+ // frames.
+
+ VideoSegment segment;
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false, now);
+
+ encoder.SetStartOffset(now);
+ encoder.AppendVideoSegment(std::move(segment));
+
+ {
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(6.5));
+
+ EXPECT_FALSE(encoder.IsEncodingComplete());
+ EXPECT_FALSE(encoder.mEncodedVideoQueue.IsFinished());
+
+ uint64_t count = 0;
+ uint64_t totalDuration = 0;
+ while (RefPtr<EncodedFrame> frame = encoder.mEncodedVideoQueue.PopFront()) {
+ ++count;
+ totalDuration += frame->mDuration;
+ }
+ const uint64_t sixSec = 6 * PR_USEC_PER_SEC;
+ EXPECT_EQ(sixSec, totalDuration);
+ EXPECT_EQ(6U, count);
+ }
+
+ {
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(11));
+ encoder.NotifyEndOfStream();
+
+ EXPECT_TRUE(encoder.IsEncodingComplete());
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.IsFinished());
+
+ uint64_t count = 0;
+ uint64_t totalDuration = 0;
+ while (RefPtr<EncodedFrame> frame = encoder.mEncodedVideoQueue.PopFront()) {
+ ++count;
+ totalDuration += frame->mDuration;
+ }
+ const uint64_t fiveSec = 5 * PR_USEC_PER_SEC;
+ EXPECT_EQ(fiveSec, totalDuration);
+ EXPECT_EQ(5U, count);
+ }
+}
+
+// Test that an encoding with no defined key frame interval encodes keyframes
+// as expected. Default interval should be 10s.
+TEST(VP8VideoTrackEncoder, DefaultKeyFrameInterval)
+{
+ // Set the factor high to only test the keyframe-forcing logic
+ TestVP8TrackEncoder encoder(Some(2.0));
+ YUVBufferGenerator generator;
+ generator.Init(mozilla::gfx::IntSize(640, 480));
+ TimeStamp now = TimeStamp::Now();
+
+ // Pass a frame at t=0, and the frame-duplication logic will encode frames
+ // every second. Keyframes are expected at t=0, 10s and 20s.
+ VideoSegment segment;
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false, now);
+
+ encoder.SetStartOffset(now);
+ encoder.AppendVideoSegment(std::move(segment));
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromSeconds(21.5));
+ encoder.NotifyEndOfStream();
+
+ EXPECT_TRUE(encoder.IsEncodingComplete());
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.IsFinished());
+ EXPECT_FALSE(encoder.mEncodedVideoQueue.AtEndOfStream());
+
+ // Duplication logic ensures no frame duration is longer than 1 second.
+
+ // [0, 1000ms) - key-frame.
+ RefPtr<EncodedFrame> frame = encoder.mEncodedVideoQueue.PopFront();
+ EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 1000UL, frame->mDuration);
+ EXPECT_EQ(EncodedFrame::VP8_I_FRAME, frame->mFrameType);
+
+ // [1000ms, 10000ms) - non-key-frames
+ for (int i = 0; i < 9; ++i) {
+ frame = encoder.mEncodedVideoQueue.PopFront();
+ EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 1000UL, frame->mDuration)
+ << "Start time: " << frame->mTime.ToMicroseconds() << "us";
+ EXPECT_EQ(EncodedFrame::VP8_P_FRAME, frame->mFrameType)
+ << "Start time: " << frame->mTime.ToMicroseconds() << "us";
+ }
+
+ // [10000ms, 11000ms) - key-frame
+ frame = encoder.mEncodedVideoQueue.PopFront();
+ EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 1000UL, frame->mDuration);
+ EXPECT_EQ(EncodedFrame::VP8_I_FRAME, frame->mFrameType);
+
+ // [11000ms, 20000ms) - non-key-frames
+ for (int i = 0; i < 9; ++i) {
+ frame = encoder.mEncodedVideoQueue.PopFront();
+ EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 1000UL, frame->mDuration)
+ << "Start time: " << frame->mTime.ToMicroseconds() << "us";
+ EXPECT_EQ(EncodedFrame::VP8_P_FRAME, frame->mFrameType)
+ << "Start time: " << frame->mTime.ToMicroseconds() << "us";
+ }
+
+ // [20000ms, 21000ms) - key-frame
+ frame = encoder.mEncodedVideoQueue.PopFront();
+ EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 1000UL, frame->mDuration);
+ EXPECT_EQ(EncodedFrame::VP8_I_FRAME, frame->mFrameType);
+
+ // [21000ms, 21500ms) - non-key-frame
+ frame = encoder.mEncodedVideoQueue.PopFront();
+ EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 500UL, frame->mDuration);
+ EXPECT_EQ(EncodedFrame::VP8_P_FRAME, frame->mFrameType);
+
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.AtEndOfStream());
+}
+
+// Test that an encoding which is disabled on a frame timestamp encodes
+// frames as expected.
+TEST(VP8VideoTrackEncoder, DisableOnFrameTime)
+{
+ TestVP8TrackEncoder encoder;
+ YUVBufferGenerator generator;
+ generator.Init(mozilla::gfx::IntSize(640, 480));
+ TimeStamp now = TimeStamp::Now();
+
+ // Pass a frame in at t=0.
+ // Pass another frame in at t=100ms.
+ // Disable the track at t=100ms.
+ // Stop encoding at t=200ms.
+ // Should yield 2 frames, 1 real; 1 black.
+
+ VideoSegment segment;
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false, now);
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + TimeDuration::FromMilliseconds(100));
+
+ encoder.SetStartOffset(now);
+ encoder.AppendVideoSegment(std::move(segment));
+
+ // Advancing 100ms, for simplicity.
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromMilliseconds(100));
+
+ encoder.Disable(now + TimeDuration::FromMilliseconds(100));
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromMilliseconds(200));
+ encoder.NotifyEndOfStream();
+
+ EXPECT_TRUE(encoder.IsEncodingComplete());
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.IsFinished());
+ EXPECT_FALSE(encoder.mEncodedVideoQueue.AtEndOfStream());
+
+ // [0, 100ms)
+ RefPtr<EncodedFrame> frame = encoder.mEncodedVideoQueue.PopFront();
+ EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frame->mDuration);
+
+ // [100ms, 200ms)
+ frame = encoder.mEncodedVideoQueue.PopFront();
+ EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frame->mDuration);
+
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.AtEndOfStream());
+}
+
+// Test that an encoding which is disabled between two frame timestamps
+// encodes frames as expected.
+TEST(VP8VideoTrackEncoder, DisableBetweenFrames)
+{
+ TestVP8TrackEncoder encoder;
+ YUVBufferGenerator generator;
+ generator.Init(mozilla::gfx::IntSize(640, 480));
+ TimeStamp now = TimeStamp::Now();
+
+ // Pass a frame in at t=0.
+ // Disable the track at t=50ms.
+ // Pass another frame in at t=100ms.
+ // Stop encoding at t=200ms.
+ // Should yield 3 frames, 1 real [0, 50); 2 black [50, 100) and [100, 200).
+
+ VideoSegment segment;
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false, now);
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + TimeDuration::FromMilliseconds(100));
+
+ encoder.SetStartOffset(now);
+ encoder.AppendVideoSegment(std::move(segment));
+
+ encoder.Disable(now + TimeDuration::FromMilliseconds(50));
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromMilliseconds(200));
+ encoder.NotifyEndOfStream();
+
+ EXPECT_TRUE(encoder.IsEncodingComplete());
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.IsFinished());
+ EXPECT_FALSE(encoder.mEncodedVideoQueue.AtEndOfStream());
+
+ // [0, 50ms)
+ RefPtr<EncodedFrame> frame = encoder.mEncodedVideoQueue.PopFront();
+ EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 50UL, frame->mDuration);
+
+ // [50ms, 100ms)
+ frame = encoder.mEncodedVideoQueue.PopFront();
+ EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 50UL, frame->mDuration);
+
+ // [100ms, 200ms)
+ frame = encoder.mEncodedVideoQueue.PopFront();
+ EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frame->mDuration);
+
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.AtEndOfStream());
+}
+
+// Test that an encoding which is disabled before the first frame becomes
+// black immediately.
+TEST(VP8VideoTrackEncoder, DisableBeforeFirstFrame)
+{
+ TestVP8TrackEncoder encoder;
+ YUVBufferGenerator generator;
+ generator.Init(mozilla::gfx::IntSize(640, 480));
+ TimeStamp now = TimeStamp::Now();
+
+ // Disable the track at t=0.
+ // Pass a frame in at t=50ms.
+ // Enable the track at t=100ms.
+ // Stop encoding at t=200ms.
+ // Should yield 2 frames, 1 black [0, 100); 1 real [100, 200).
+
+ VideoSegment segment;
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + TimeDuration::FromMilliseconds(50));
+
+ encoder.SetStartOffset(now);
+ encoder.Disable(now);
+ encoder.AppendVideoSegment(std::move(segment));
+
+ encoder.Enable(now + TimeDuration::FromMilliseconds(100));
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromMilliseconds(200));
+ encoder.NotifyEndOfStream();
+
+ EXPECT_TRUE(encoder.IsEncodingComplete());
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.IsFinished());
+ EXPECT_FALSE(encoder.mEncodedVideoQueue.AtEndOfStream());
+
+ // [0, 100ms)
+ RefPtr<EncodedFrame> frame = encoder.mEncodedVideoQueue.PopFront();
+ EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frame->mDuration);
+
+ // [100ms, 200ms)
+ frame = encoder.mEncodedVideoQueue.PopFront();
+ EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frame->mDuration);
+
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.AtEndOfStream());
+}
+
+// Test that an encoding which is enabled on a frame timestamp encodes
+// frames as expected.
+TEST(VP8VideoTrackEncoder, EnableOnFrameTime)
+{
+ TestVP8TrackEncoder encoder;
+ YUVBufferGenerator generator;
+ generator.Init(mozilla::gfx::IntSize(640, 480));
+ TimeStamp now = TimeStamp::Now();
+
+ // Disable the track at t=0.
+ // Pass a frame in at t=0.
+ // Pass another frame in at t=100ms.
+ // Enable the track at t=100ms.
+ // Stop encoding at t=200ms.
+ // Should yield 2 frames, 1 black; 1 real.
+
+ VideoSegment segment;
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false, now);
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + TimeDuration::FromMilliseconds(100));
+
+ encoder.SetStartOffset(now);
+ encoder.Disable(now);
+ encoder.AppendVideoSegment(std::move(segment));
+
+ // Advancing 100ms, for simplicity.
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromMilliseconds(100));
+
+ encoder.Enable(now + TimeDuration::FromMilliseconds(100));
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromMilliseconds(200));
+ encoder.NotifyEndOfStream();
+
+ EXPECT_TRUE(encoder.IsEncodingComplete());
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.IsFinished());
+ EXPECT_FALSE(encoder.mEncodedVideoQueue.AtEndOfStream());
+
+ // [0, 100ms)
+ RefPtr<EncodedFrame> frame = encoder.mEncodedVideoQueue.PopFront();
+ EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frame->mDuration);
+
+ // [100ms, 200ms)
+ frame = encoder.mEncodedVideoQueue.PopFront();
+ EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frame->mDuration);
+
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.AtEndOfStream());
+}
+
+// Test that an encoding which is enabled between two frame timestamps encodes
+// frames as expected.
+TEST(VP8VideoTrackEncoder, EnableBetweenFrames)
+{
+ TestVP8TrackEncoder encoder;
+ YUVBufferGenerator generator;
+ generator.Init(mozilla::gfx::IntSize(640, 480));
+ TimeStamp now = TimeStamp::Now();
+
+ // Disable the track at t=0.
+ // Pass a frame in at t=0.
+ // Enable the track at t=50ms.
+ // Pass another frame in at t=100ms.
+ // Stop encoding at t=200ms.
+ // Should yield 3 frames, 1 black [0, 50); 2 real [50, 100) and [100, 200).
+
+ VideoSegment segment;
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false, now);
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + TimeDuration::FromMilliseconds(100));
+
+ encoder.SetStartOffset(now);
+ encoder.Disable(now);
+ encoder.AppendVideoSegment(std::move(segment));
+
+ encoder.Enable(now + TimeDuration::FromMilliseconds(50));
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromMilliseconds(200));
+ encoder.NotifyEndOfStream();
+
+ EXPECT_TRUE(encoder.IsEncodingComplete());
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.IsFinished());
+ EXPECT_FALSE(encoder.mEncodedVideoQueue.AtEndOfStream());
+
+ // [0, 50ms)
+ RefPtr<EncodedFrame> frame = encoder.mEncodedVideoQueue.PopFront();
+ EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 50UL, frame->mDuration);
+
+ // [50ms, 100ms)
+ frame = encoder.mEncodedVideoQueue.PopFront();
+ EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 50UL, frame->mDuration);
+
+ // [100ms, 200ms)
+ frame = encoder.mEncodedVideoQueue.PopFront();
+ EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frame->mDuration);
+
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.AtEndOfStream());
+}
+
+// Test that making time go backwards removes any future frames in the
+// encoder.
+TEST(VP8VideoTrackEncoder, BackwardsTimeResets)
+{
+ TestVP8TrackEncoder encoder;
+ YUVBufferGenerator generator;
+ generator.Init(mozilla::gfx::IntSize(640, 480));
+ TimeStamp now = TimeStamp::Now();
+
+ encoder.SetStartOffset(now);
+
+ // Pass frames in at t=0, t=100ms, t=200ms, t=300ms.
+ // Advance time to t=125ms.
+ // Pass frames in at t=150ms, t=250ms, t=350ms.
+ // Stop encoding at t=300ms.
+ // Should yield 4 frames, at t=0, t=100ms, t=150ms, t=250ms.
+
+ {
+ VideoSegment segment;
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false, now);
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + TimeDuration::FromMilliseconds(100));
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + TimeDuration::FromMilliseconds(200));
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + TimeDuration::FromMilliseconds(300));
+
+ encoder.AppendVideoSegment(std::move(segment));
+ }
+
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromMilliseconds(125));
+
+ {
+ VideoSegment segment;
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + TimeDuration::FromMilliseconds(150));
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + TimeDuration::FromMilliseconds(250));
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + TimeDuration::FromMilliseconds(350));
+
+ encoder.AppendVideoSegment(std::move(segment));
+ }
+
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromMilliseconds(300));
+ encoder.NotifyEndOfStream();
+
+ EXPECT_TRUE(encoder.IsEncodingComplete());
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.IsFinished());
+ EXPECT_FALSE(encoder.mEncodedVideoQueue.AtEndOfStream());
+
+ // [0, 100ms)
+ RefPtr<EncodedFrame> frame = encoder.mEncodedVideoQueue.PopFront();
+ EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frame->mDuration);
+
+ // [100ms, 150ms)
+ frame = encoder.mEncodedVideoQueue.PopFront();
+ EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 50UL, frame->mDuration);
+
+ // [150ms, 250ms)
+ frame = encoder.mEncodedVideoQueue.PopFront();
+ EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frame->mDuration);
+
+ // [250ms, 300ms)
+ frame = encoder.mEncodedVideoQueue.PopFront();
+ EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 50UL, frame->mDuration);
+
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.AtEndOfStream());
+}
+
+// Test that trying to encode a null image removes any future frames in the
+// encoder.
+TEST(VP8VideoTrackEncoder, NullImageResets)
+{
+ TestVP8TrackEncoder encoder;
+ YUVBufferGenerator generator;
+ generator.Init(mozilla::gfx::IntSize(640, 480));
+ TimeStamp now = TimeStamp::Now();
+
+ encoder.SetStartOffset(now);
+
+ // Pass frames in at t=0, t=100ms, t=200ms, t=300ms.
+ // Advance time to t=125ms.
+ // Pass in a null image at t=125ms.
+ // Pass frames in at t=250ms, t=350ms.
+ // Stop encoding at t=300ms.
+ // Should yield 3 frames, at t=0, t=100ms, t=250ms.
+
+ {
+ VideoSegment segment;
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false, now);
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + TimeDuration::FromMilliseconds(100));
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + TimeDuration::FromMilliseconds(200));
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + TimeDuration::FromMilliseconds(300));
+
+ encoder.AppendVideoSegment(std::move(segment));
+ }
+
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromMilliseconds(125));
+
+ {
+ VideoSegment segment;
+ segment.AppendFrame(nullptr, generator.GetSize(), PRINCIPAL_HANDLE_NONE,
+ false, now + TimeDuration::FromMilliseconds(125));
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + TimeDuration::FromMilliseconds(250));
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + TimeDuration::FromMilliseconds(350));
+
+ encoder.AppendVideoSegment(std::move(segment));
+ }
+
+ encoder.AdvanceCurrentTime(now + TimeDuration::FromMilliseconds(300));
+ encoder.NotifyEndOfStream();
+
+ EXPECT_TRUE(encoder.IsEncodingComplete());
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.IsFinished());
+ EXPECT_FALSE(encoder.mEncodedVideoQueue.AtEndOfStream());
+
+ // [0, 100ms)
+ RefPtr<EncodedFrame> frame = encoder.mEncodedVideoQueue.PopFront();
+ EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frame->mDuration);
+
+ // [100ms, 250ms)
+ frame = encoder.mEncodedVideoQueue.PopFront();
+ EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 150UL, frame->mDuration);
+
+ // [250ms, 300ms)
+ frame = encoder.mEncodedVideoQueue.PopFront();
+ EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 50UL, frame->mDuration);
+
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.AtEndOfStream());
+}
+
+TEST(VP8VideoTrackEncoder, MaxKeyFrameDistanceLowFramerate)
+{
+ TestVP8TrackEncoder encoder;
+ YUVBufferGenerator generator;
+ generator.Init(mozilla::gfx::IntSize(240, 180));
+ TimeStamp now = TimeStamp::Now();
+
+ encoder.SetStartOffset(now);
+
+ // Pass 10s worth of frames at 2 fps and verify that the key frame interval
+ // is ~7.5s.
+ const TimeDuration duration = TimeDuration::FromSeconds(10);
+ const uint32_t numFrames = 10 * 2;
+ const TimeDuration frameDuration = duration / static_cast<int64_t>(numFrames);
+
+ {
+ VideoSegment segment;
+ for (uint32_t i = 0; i < numFrames; ++i) {
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + frameDuration * i);
+ }
+ encoder.AppendVideoSegment(std::move(segment));
+ }
+
+ encoder.AdvanceCurrentTime(now + duration);
+ encoder.NotifyEndOfStream();
+
+ EXPECT_TRUE(encoder.IsEncodingComplete());
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.IsFinished());
+ EXPECT_FALSE(encoder.mEncodedVideoQueue.AtEndOfStream());
+
+ for (uint32_t i = 0; i < numFrames; ++i) {
+ const RefPtr<EncodedFrame> frame = encoder.mEncodedVideoQueue.PopFront();
+ EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 500UL, frame->mDuration)
+ << "Frame " << i << ", with start: " << frame->mTime.ToMicroseconds()
+ << "us";
+ // 7.5s key frame interval at 2 fps becomes the 15th frame.
+ EXPECT_EQ(
+ i % 15 == 0 ? EncodedFrame::VP8_I_FRAME : EncodedFrame::VP8_P_FRAME,
+ frame->mFrameType)
+ << "Frame " << i << ", with start: " << frame->mTime.ToMicroseconds()
+ << "us";
+ }
+
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.AtEndOfStream());
+}
+
+// This is "High" framerate, as in higher than the test for "Low" framerate.
+// We don't make it too high because the test takes considerably longer to
+// run.
+TEST(VP8VideoTrackEncoder, MaxKeyFrameDistanceHighFramerate)
+{
+ TestVP8TrackEncoder encoder;
+ YUVBufferGenerator generator;
+ generator.Init(mozilla::gfx::IntSize(240, 180));
+ TimeStamp now = TimeStamp::Now();
+
+ encoder.SetStartOffset(now);
+
+ // Pass 10s worth of frames at 8 fps and verify that the key frame interval
+ // is ~7.5s.
+ const TimeDuration duration = TimeDuration::FromSeconds(10);
+ const uint32_t numFrames = 10 * 8;
+ const TimeDuration frameDuration = duration / static_cast<int64_t>(numFrames);
+
+ {
+ VideoSegment segment;
+ for (uint32_t i = 0; i < numFrames; ++i) {
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + frameDuration * i);
+ }
+ encoder.AppendVideoSegment(std::move(segment));
+ }
+
+ encoder.AdvanceCurrentTime(now + duration);
+ encoder.NotifyEndOfStream();
+
+ EXPECT_TRUE(encoder.IsEncodingComplete());
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.IsFinished());
+ EXPECT_FALSE(encoder.mEncodedVideoQueue.AtEndOfStream());
+
+ for (uint32_t i = 0; i < numFrames; ++i) {
+ const RefPtr<EncodedFrame> frame = encoder.mEncodedVideoQueue.PopFront();
+ EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 125UL, frame->mDuration)
+ << "Frame " << i << ", with start: " << frame->mTime.ToMicroseconds()
+ << "us";
+ // 7.5s key frame interval at 8 fps becomes the 60th frame.
+ EXPECT_EQ(
+ i % 60 == 0 ? EncodedFrame::VP8_I_FRAME : EncodedFrame::VP8_P_FRAME,
+ frame->mFrameType)
+ << "Frame " << i << ", with start: " << frame->mTime.ToMicroseconds()
+ << "us";
+ }
+
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.AtEndOfStream());
+}
+
+TEST(VP8VideoTrackEncoder, MaxKeyFrameDistanceAdaptiveFramerate)
+{
+ TestVP8TrackEncoder encoder;
+ YUVBufferGenerator generator;
+ generator.Init(mozilla::gfx::IntSize(240, 180));
+ TimeStamp now = TimeStamp::Now();
+
+ encoder.SetStartOffset(now);
+
+ // Pass 11s worth of frames at 2 fps and verify that there is a key frame
+ // at 7.5s. Then pass 14s worth of frames at 10 fps and verify that there is
+ // a key frame at 15s (due to re-init) and then one at 22.5s.
+
+ const TimeDuration firstDuration = TimeDuration::FromSeconds(11);
+ const uint32_t firstNumFrames = 11 * 2;
+ const TimeDuration firstFrameDuration =
+ firstDuration / static_cast<int64_t>(firstNumFrames);
+ {
+ VideoSegment segment;
+ for (uint32_t i = 0; i < firstNumFrames; ++i) {
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + firstFrameDuration * i);
+ }
+ encoder.AppendVideoSegment(std::move(segment));
+ }
+ encoder.AdvanceCurrentTime(now + firstDuration);
+
+ const TimeDuration secondDuration = TimeDuration::FromSeconds(14);
+ const uint32_t secondNumFrames = 14 * 10;
+ const TimeDuration secondFrameDuration =
+ secondDuration / static_cast<int64_t>(secondNumFrames);
+ {
+ VideoSegment segment;
+ for (uint32_t i = 0; i < secondNumFrames; ++i) {
+ segment.AppendFrame(generator.GenerateI420Image(), generator.GetSize(),
+ PRINCIPAL_HANDLE_NONE, false,
+ now + firstDuration + secondFrameDuration * i);
+ }
+ encoder.AppendVideoSegment(std::move(segment));
+ }
+ encoder.AdvanceCurrentTime(now + firstDuration + secondDuration);
+
+ encoder.NotifyEndOfStream();
+
+ EXPECT_TRUE(encoder.IsEncodingComplete());
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.IsFinished());
+ EXPECT_FALSE(encoder.mEncodedVideoQueue.AtEndOfStream());
+
+ // [0, 11s) - keyframe distance is now 7.5s@2fps = 15.
+ for (uint32_t i = 0; i < 22; ++i) {
+ const RefPtr<EncodedFrame> frame = encoder.mEncodedVideoQueue.PopFront();
+ EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 500UL, frame->mDuration)
+ << "Frame " << i << ", with start: " << frame->mTime.ToMicroseconds()
+ << "us";
+ // 7.5s key frame interval at 2 fps becomes the 15th frame.
+ EXPECT_EQ(
+ i % 15 == 0 ? EncodedFrame::VP8_I_FRAME : EncodedFrame::VP8_P_FRAME,
+ frame->mFrameType)
+ << "Frame " << i << ", with start: " << frame->mTime.ToMicroseconds()
+ << "us";
+ }
+
+ // Input framerate is now 10fps.
+ // Framerate re-evaluation every 5s, so the keyframe distance changed at
+ // 15s.
+ for (uint32_t i = 22; i < 162; ++i) {
+ const RefPtr<EncodedFrame> frame = encoder.mEncodedVideoQueue.PopFront();
+ EXPECT_EQ(PR_USEC_PER_SEC / 1000 * 100UL, frame->mDuration)
+ << "Frame " << i << ", with start: " << frame->mTime.ToMicroseconds()
+ << "us";
+ if (i < 22 + 40) {
+ // [11s, 15s) - 40 frames at 10fps but with the 2fps keyframe distance.
+ EXPECT_EQ(
+ i % 15 == 0 ? EncodedFrame::VP8_I_FRAME : EncodedFrame::VP8_P_FRAME,
+ frame->mFrameType)
+ << "Frame " << i << ", with start: " << frame->mTime.ToMicroseconds()
+ << "us";
+ } else {
+ // [15s, 25s) - 100 frames at 10fps. Keyframe distance 75. Starts with
+ // keyframe due to re-init.
+ EXPECT_EQ((i - 22 - 40) % 75 == 0 ? EncodedFrame::VP8_I_FRAME
+ : EncodedFrame::VP8_P_FRAME,
+ frame->mFrameType)
+ << "Frame " << i << ", with start: " << frame->mTime.ToMicroseconds()
+ << "us";
+ }
+ }
+
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.AtEndOfStream());
+}
+
+// EOS test
+TEST(VP8VideoTrackEncoder, EncodeComplete)
+{
+ TestVP8TrackEncoder encoder;
+
+ // NotifyEndOfStream should wrap up the encoding immediately.
+ encoder.NotifyEndOfStream();
+ EXPECT_TRUE(encoder.mEncodedVideoQueue.IsFinished());
+}
diff --git a/dom/media/gtest/TestVideoUtils.cpp b/dom/media/gtest/TestVideoUtils.cpp
new file mode 100644
index 0000000000..d322d15d64
--- /dev/null
+++ b/dom/media/gtest/TestVideoUtils.cpp
@@ -0,0 +1,128 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "nsMimeTypes.h"
+#include "nsString.h"
+#include "VideoUtils.h"
+
+using namespace mozilla;
+
+TEST(MediaMIMETypes, IsMediaMIMEType)
+{
+ EXPECT_TRUE(IsMediaMIMEType(AUDIO_MP4));
+ EXPECT_TRUE(IsMediaMIMEType(VIDEO_MP4));
+ EXPECT_TRUE(IsMediaMIMEType("application/x-mp4"));
+
+ EXPECT_TRUE(IsMediaMIMEType("audio/m"));
+ EXPECT_FALSE(IsMediaMIMEType("audio/"));
+
+ EXPECT_FALSE(IsMediaMIMEType("vide/mp4"));
+ EXPECT_FALSE(IsMediaMIMEType("videos/mp4"));
+
+ // Expect lowercase only.
+ EXPECT_FALSE(IsMediaMIMEType("Video/mp4"));
+}
+
+TEST(StringListRange, MakeStringListRange)
+{
+ static const struct {
+ const char* mList;
+ const char* mExpectedSkipEmpties;
+ const char* mExpectedProcessAll;
+ const char* mExpectedProcessEmpties;
+ } tests[] = {
+ // string skip all empties
+ {"", "", "|", ""},
+ {" ", "", "|", "|"},
+ {",", "", "||", "||"},
+ {" , ", "", "||", "||"},
+ {"a", "a|", "a|", "a|"},
+ {" a ", "a|", "a|", "a|"},
+ {"a,", "a|", "a||", "a||"},
+ {"a, ", "a|", "a||", "a||"},
+ {",a", "a|", "|a|", "|a|"},
+ {" ,a", "a|", "|a|", "|a|"},
+ {"aa,bb", "aa|bb|", "aa|bb|", "aa|bb|"},
+ {" a a , b b ", "a a|b b|", "a a|b b|", "a a|b b|"},
+ {" , ,a 1,, ,b 2,", "a 1|b 2|", "||a 1|||b 2||", "||a 1|||b 2||"}};
+
+ for (const auto& test : tests) {
+ nsCString list(test.mList);
+ nsCString out;
+ for (const auto& item : MakeStringListRange(list)) {
+ out += item;
+ out += "|";
+ }
+ EXPECT_STREQ(test.mExpectedSkipEmpties, out.Data());
+ out.SetLength(0);
+
+ for (const auto& item :
+ MakeStringListRange<StringListRangeEmptyItems::ProcessAll>(list)) {
+ out += item;
+ out += "|";
+ }
+ EXPECT_STREQ(test.mExpectedProcessAll, out.Data());
+ out.SetLength(0);
+
+ for (const auto& item :
+ MakeStringListRange<StringListRangeEmptyItems::ProcessEmptyItems>(
+ list)) {
+ out += item;
+ out += "|";
+ }
+ EXPECT_STREQ(test.mExpectedProcessEmpties, out.Data());
+ }
+}
+
+TEST(StringListRange, StringListContains)
+{
+ static const struct {
+ const char* mList;
+ const char* mItemToSearch;
+ bool mExpectedSkipEmpties;
+ bool mExpectedProcessAll;
+ bool mExpectedProcessEmpties;
+ } tests[] = {// haystack needle skip all empties
+ {"", "", false, true, false},
+ {" ", "", false, true, true},
+ {"", "a", false, false, false},
+ {" ", "a", false, false, false},
+ {",", "a", false, false, false},
+ {" , ", "", false, true, true},
+ {" , ", "a", false, false, false},
+ {"a", "a", true, true, true},
+ {"a", "b", false, false, false},
+ {" a ", "a", true, true, true},
+ {"aa,bb", "aa", true, true, true},
+ {"aa,bb", "bb", true, true, true},
+ {"aa,bb", "cc", false, false, false},
+ {"aa,bb", " aa ", false, false, false},
+ {" a a , b b ", "a a", true, true, true},
+ {" , ,a 1,, ,b 2,", "a 1", true, true, true},
+ {" , ,a 1,, ,b 2,", "b 2", true, true, true},
+ {" , ,a 1,, ,b 2,", "", false, true, true},
+ {" , ,a 1,, ,b 2,", " ", false, false, false},
+ {" , ,a 1,, ,b 2,", "A 1", false, false, false},
+ {" , ,A 1,, ,b 2,", "a 1", false, false, false}};
+
+ for (const auto& test : tests) {
+ nsCString list(test.mList);
+ nsCString itemToSearch(test.mItemToSearch);
+ EXPECT_EQ(test.mExpectedSkipEmpties, StringListContains(list, itemToSearch))
+ << "trying to find \"" << itemToSearch.Data() << "\" in \""
+ << list.Data() << "\" (skipping empties)";
+ EXPECT_EQ(test.mExpectedProcessAll,
+ StringListContains<StringListRangeEmptyItems::ProcessAll>(
+ list, itemToSearch))
+ << "trying to find \"" << itemToSearch.Data() << "\" in \""
+ << list.Data() << "\" (processing everything)";
+ EXPECT_EQ(test.mExpectedProcessEmpties,
+ StringListContains<StringListRangeEmptyItems::ProcessEmptyItems>(
+ list, itemToSearch))
+ << "trying to find \"" << itemToSearch.Data() << "\" in \""
+ << list.Data() << "\" (processing empties)";
+ }
+}
diff --git a/dom/media/gtest/TestWebMBuffered.cpp b/dom/media/gtest/TestWebMBuffered.cpp
new file mode 100644
index 0000000000..35ba00fec7
--- /dev/null
+++ b/dom/media/gtest/TestWebMBuffered.cpp
@@ -0,0 +1,234 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "mozilla/ArrayUtils.h"
+#include <stdio.h>
+#include "nsTArray.h"
+#include "WebMBufferedParser.h"
+
+using namespace mozilla;
+
+std::ostream& operator<<(std::ostream& aStream, nsresult aResult) {
+ return aStream << GetStaticErrorName(aResult);
+}
+
+namespace mozilla {
+std::ostream& operator<<(std::ostream& aStream, const MediaResult& aResult) {
+ aStream << aResult.Code();
+ if (!aResult.Message().IsEmpty()) {
+ aStream << " (" << aResult.Message() << ")";
+ }
+ return aStream;
+}
+} // namespace mozilla
+
+// "test.webm" contains 8 SimpleBlocks in a single Cluster. The blocks with
+// timecodes 100000000 and are 133000000 skipped by WebMBufferedParser
+// because they occur after a block with timecode 160000000 and the parser
+// expects in-order timecodes per the WebM spec. The remaining 6
+// SimpleBlocks have the following attributes:
+static const uint64_t gTimecodes[] = {66000000, 160000000, 166000000,
+ 200000000, 233000000, 320000000};
+static const int64_t gEndOffsets[] = {466, 737, 1209, 1345, 1508, 1980};
+
+TEST(WebMBuffered, BasicTests)
+{
+ WebMBufferedParser parser(0);
+
+ nsTArray<WebMTimeDataOffset> mapping;
+ EXPECT_EQ(parser.Append(nullptr, 0, mapping), NS_OK);
+ EXPECT_TRUE(mapping.IsEmpty());
+ EXPECT_EQ(parser.mStartOffset, 0);
+ EXPECT_EQ(parser.mCurrentOffset, 0);
+
+ unsigned char buf[] = {0x1a, 0x45, 0xdf, 0xa3};
+ EXPECT_EQ(parser.Append(buf, ArrayLength(buf), mapping), NS_OK);
+ EXPECT_TRUE(mapping.IsEmpty());
+ EXPECT_EQ(parser.mStartOffset, 0);
+ EXPECT_EQ(parser.mCurrentOffset, 4);
+}
+
+static void ReadFile(const char* aPath, nsTArray<uint8_t>& aBuffer) {
+ FILE* f = fopen(aPath, "rb");
+ ASSERT_NE(f, (FILE*)nullptr);
+
+ int r = fseek(f, 0, SEEK_END);
+ ASSERT_EQ(r, 0);
+
+ long size = ftell(f);
+ ASSERT_NE(size, -1);
+ aBuffer.SetLength(size);
+
+ r = fseek(f, 0, SEEK_SET);
+ ASSERT_EQ(r, 0);
+
+ size_t got = fread(aBuffer.Elements(), 1, size, f);
+ ASSERT_EQ(got, size_t(size));
+
+ r = fclose(f);
+ ASSERT_EQ(r, 0);
+}
+
+TEST(WebMBuffered, RealData)
+{
+ WebMBufferedParser parser(0);
+
+ nsTArray<uint8_t> webmData;
+ ReadFile("test.webm", webmData);
+
+ nsTArray<WebMTimeDataOffset> mapping;
+ EXPECT_EQ(parser.Append(webmData.Elements(), webmData.Length(), mapping),
+ NS_OK);
+ EXPECT_EQ(mapping.Length(), 6u);
+ EXPECT_EQ(parser.mStartOffset, 0);
+ EXPECT_EQ(parser.mCurrentOffset, int64_t(webmData.Length()));
+ EXPECT_EQ(parser.GetTimecodeScale(), 500000u);
+
+ for (uint32_t i = 0; i < mapping.Length(); ++i) {
+ EXPECT_EQ(mapping[i].mEndOffset, gEndOffsets[i]);
+ EXPECT_EQ(mapping[i].mSyncOffset, 326);
+ EXPECT_EQ(mapping[i].mTimecode, gTimecodes[i]);
+ }
+}
+
+TEST(WebMBuffered, RealDataAppend)
+{
+ WebMBufferedParser parser(0);
+ nsTArray<WebMTimeDataOffset> mapping;
+
+ nsTArray<uint8_t> webmData;
+ ReadFile("test.webm", webmData);
+
+ uint32_t arrayEntries = mapping.Length();
+ size_t offset = 0;
+ while (offset < webmData.Length()) {
+ EXPECT_EQ(parser.Append(webmData.Elements() + offset, 1, mapping), NS_OK);
+ offset += 1;
+ EXPECT_EQ(parser.mCurrentOffset, int64_t(offset));
+ if (mapping.Length() != arrayEntries) {
+ arrayEntries = mapping.Length();
+ ASSERT_LE(arrayEntries, 6u);
+ uint32_t i = arrayEntries - 1;
+ EXPECT_EQ(mapping[i].mEndOffset, gEndOffsets[i]);
+ EXPECT_EQ(mapping[i].mSyncOffset, 326);
+ EXPECT_EQ(mapping[i].mTimecode, gTimecodes[i]);
+ EXPECT_EQ(parser.GetTimecodeScale(), 500000u);
+ }
+ }
+ EXPECT_EQ(mapping.Length(), 6u);
+ EXPECT_EQ(parser.mStartOffset, 0);
+ EXPECT_EQ(parser.mCurrentOffset, int64_t(webmData.Length()));
+ EXPECT_EQ(parser.GetTimecodeScale(), 500000u);
+
+ for (uint32_t i = 0; i < mapping.Length(); ++i) {
+ EXPECT_EQ(mapping[i].mEndOffset, gEndOffsets[i]);
+ EXPECT_EQ(mapping[i].mSyncOffset, 326);
+ EXPECT_EQ(mapping[i].mTimecode, gTimecodes[i]);
+ }
+}
+
+TEST(WebMBuffered, InvalidEBMLMaxIdLength)
+{
+ WebMBufferedParser parser(0);
+
+ nsTArray<uint8_t> webmData;
+ // This file contains EBMLMaxIdLength=3, but a Segment element (and maybe
+ // others) whose Id VInt has length 4.
+ ReadFile("test_InvalidElementId.webm", webmData);
+
+ nsTArray<WebMTimeDataOffset> mapping;
+ EXPECT_EQ(parser.Append(webmData.Elements(), webmData.Length(), mapping),
+ NS_ERROR_FAILURE);
+}
+
+TEST(WebMBuffered, InvalidLargeElementIdLength)
+{
+ WebMBufferedParser parser(0);
+
+ nsTArray<uint8_t> webmData;
+ // This file contains EBMLMaxIdLength=4, but a dummy element whose Id VInt has
+ // length 5.
+ ReadFile("test_InvalidLargeElementId.webm", webmData);
+
+ nsTArray<WebMTimeDataOffset> mapping;
+ EXPECT_EQ(parser.Append(webmData.Elements(), webmData.Length(), mapping),
+ NS_ERROR_FAILURE);
+}
+
+TEST(WebMBuffered, InvalidSmallEBMLMaxIdLength)
+{
+ WebMBufferedParser parser(0);
+
+ nsTArray<uint8_t> webmData;
+ // This file contains EBMLMaxIdLength=3.
+ // Per draft-ietf-cellar-matroska-13 EBMLMaxIdLength MUST be 4. But element
+ // ids can also be between 1 and 5 octets long. 5 only if EBMLMaxIdLength
+ // specifies it. At least 3 is too short.
+ ReadFile("test_InvalidSmallEBMLMaxIdLength.webm", webmData);
+
+ nsTArray<WebMTimeDataOffset> mapping;
+ EXPECT_EQ(parser.Append(webmData.Elements(), webmData.Length(), mapping),
+ NS_ERROR_FAILURE);
+}
+
+TEST(WebMBuffered, ValidLargeEBMLMaxIdLength)
+{
+ WebMBufferedParser parser(0);
+
+ nsTArray<uint8_t> webmData;
+ // This file contains EBMLMaxIdLength=5 and a dummy element with a 5 octet
+ // long id. Per draft-ietf-cellar-matroska-13 EBMLMaxIdLength MUST be 4. But
+ // element ids can also be between 1 and 5 octets long. 5 only if
+ // EBMLMaxIdLength specifies it. We better tolerate this.
+ ReadFile("test_ValidLargeEBMLMaxIdLength.webm", webmData);
+
+ nsTArray<WebMTimeDataOffset> mapping;
+ EXPECT_EQ(parser.Append(webmData.Elements(), webmData.Length(), mapping),
+ NS_OK);
+}
+
+TEST(WebMBuffered, InvalidLargeEBMLMaxIdLength)
+{
+ WebMBufferedParser parser(0);
+
+ nsTArray<uint8_t> webmData;
+ // This file contains EBMLMaxIdLength=6.
+ // Per draft-ietf-cellar-matroska-13 EBMLMaxIdLength MUST be 4. But
+ // element ids can also be between 1 and 5 octets long. 5 only if
+ // EBMLMaxIdLength specifies it. At least 6 is too long.
+ ReadFile("test_InvalidLargeEBMLMaxIdLength.webm", webmData);
+
+ nsTArray<WebMTimeDataOffset> mapping;
+ EXPECT_EQ(parser.Append(webmData.Elements(), webmData.Length(), mapping),
+ NS_ERROR_FAILURE);
+}
+
+TEST(WebMBuffered, ValidSmallEBMLMaxSizeLength)
+{
+ WebMBufferedParser parser(0);
+
+ nsTArray<uint8_t> webmData;
+ // This file contains EBMLMaxSizeLength=7 and no element with an element size
+ // longer than 7 bytes.
+ ReadFile("test_ValidSmallEBMLMaxSizeLength.webm", webmData);
+
+ nsTArray<WebMTimeDataOffset> mapping;
+ EXPECT_EQ(parser.Append(webmData.Elements(), webmData.Length(), mapping),
+ NS_OK);
+}
+
+TEST(WebMBuffered, InvalidEBMLMaxSizeLength)
+{
+ WebMBufferedParser parser(0);
+
+ nsTArray<uint8_t> webmData;
+ // This file contains EBMLMaxSizeLength=7, but the Segment element size VInt
+ // has length 8.
+ ReadFile("test_InvalidElementSize.webm", webmData);
+
+ nsTArray<WebMTimeDataOffset> mapping;
+ EXPECT_EQ(parser.Append(webmData.Elements(), webmData.Length(), mapping),
+ NS_ERROR_FAILURE);
+}
diff --git a/dom/media/gtest/TestWebMWriter.cpp b/dom/media/gtest/TestWebMWriter.cpp
new file mode 100644
index 0000000000..ee1f387311
--- /dev/null
+++ b/dom/media/gtest/TestWebMWriter.cpp
@@ -0,0 +1,388 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/MathAlgorithms.h"
+#include "nestegg/nestegg.h"
+#include "DriftCompensation.h"
+#include "OpusTrackEncoder.h"
+#include "VP8TrackEncoder.h"
+#include "WebMWriter.h"
+
+using namespace mozilla;
+
+class WebMOpusTrackEncoder : public OpusTrackEncoder {
+ public:
+ explicit WebMOpusTrackEncoder(TrackRate aTrackRate)
+ : OpusTrackEncoder(aTrackRate, mEncodedAudioQueue) {}
+ bool TestOpusCreation(int aChannels) {
+ if (NS_SUCCEEDED(Init(aChannels))) {
+ return true;
+ }
+ return false;
+ }
+ MediaQueue<EncodedFrame> mEncodedAudioQueue;
+};
+
+class WebMVP8TrackEncoder : public VP8TrackEncoder {
+ public:
+ explicit WebMVP8TrackEncoder(TrackRate aTrackRate = 90000)
+ : VP8TrackEncoder(nullptr, aTrackRate, mEncodedVideoQueue,
+ FrameDroppingMode::DISALLOW) {}
+
+ bool TestVP8Creation(int32_t aWidth, int32_t aHeight, int32_t aDisplayWidth,
+ int32_t aDisplayHeight) {
+ if (NS_SUCCEEDED(
+ Init(aWidth, aHeight, aDisplayWidth, aDisplayHeight, 30))) {
+ return true;
+ }
+ return false;
+ }
+ MediaQueue<EncodedFrame> mEncodedVideoQueue;
+};
+
+static void GetOpusMetadata(int aChannels, TrackRate aTrackRate,
+ nsTArray<RefPtr<TrackMetadataBase>>& aMeta) {
+ WebMOpusTrackEncoder opusEncoder(aTrackRate);
+ EXPECT_TRUE(opusEncoder.TestOpusCreation(aChannels));
+ aMeta.AppendElement(opusEncoder.GetMetadata());
+}
+
+static void GetVP8Metadata(int32_t aWidth, int32_t aHeight,
+ int32_t aDisplayWidth, int32_t aDisplayHeight,
+ TrackRate aTrackRate,
+ nsTArray<RefPtr<TrackMetadataBase>>& aMeta) {
+ WebMVP8TrackEncoder vp8Encoder;
+ EXPECT_TRUE(vp8Encoder.TestVP8Creation(aWidth, aHeight, aDisplayWidth,
+ aDisplayHeight));
+ aMeta.AppendElement(vp8Encoder.GetMetadata());
+}
+
+const uint64_t FIXED_DURATION = 1000000;
+const uint32_t FIXED_FRAMESIZE = 500;
+
+class TestWebMWriter : public WebMWriter {
+ public:
+ TestWebMWriter() : WebMWriter() {}
+
+ // When we append an I-Frame into WebM muxer, the muxer will treat previous
+ // data as "a cluster".
+ // In these test cases, we will call the function many times to enclose the
+ // previous cluster so that we can retrieve data by |GetContainerData|.
+ void AppendDummyFrame(EncodedFrame::FrameType aFrameType,
+ uint64_t aDuration) {
+ nsTArray<RefPtr<EncodedFrame>> encodedVideoData;
+ auto frameData = MakeRefPtr<EncodedFrame::FrameData>();
+ // Create dummy frame data.
+ frameData->SetLength(FIXED_FRAMESIZE);
+ encodedVideoData.AppendElement(
+ MakeRefPtr<EncodedFrame>(mTimestamp, aDuration, PR_USEC_PER_SEC,
+ aFrameType, std::move(frameData)));
+ WriteEncodedTrack(encodedVideoData, 0);
+ mTimestamp += media::TimeUnit::FromMicroseconds(aDuration);
+ }
+
+ bool HaveValidCluster() {
+ nsTArray<nsTArray<uint8_t>> encodedBuf;
+ GetContainerData(&encodedBuf, 0);
+ return !encodedBuf.IsEmpty();
+ }
+
+ // Timestamp accumulator that increased by AppendDummyFrame.
+ // Keep it public that we can do some testcases about it.
+ media::TimeUnit mTimestamp;
+};
+
+TEST(WebMWriter, Metadata)
+{
+ TestWebMWriter writer;
+
+ // The output should be empty since we didn't set any metadata in writer.
+ nsTArray<nsTArray<uint8_t>> encodedBuf;
+ writer.GetContainerData(&encodedBuf, ContainerWriter::GET_HEADER);
+ EXPECT_TRUE(encodedBuf.Length() == 0);
+ writer.GetContainerData(&encodedBuf, ContainerWriter::FLUSH_NEEDED);
+ EXPECT_TRUE(encodedBuf.Length() == 0);
+
+ nsTArray<RefPtr<TrackMetadataBase>> meta;
+
+ TrackRate trackRate = 44100;
+
+ // Get opus metadata.
+ int channel = 1;
+ GetOpusMetadata(channel, trackRate, meta);
+
+ // Get vp8 metadata
+ int32_t width = 640;
+ int32_t height = 480;
+ int32_t displayWidth = 640;
+ int32_t displayHeight = 480;
+ GetVP8Metadata(width, height, displayWidth, displayHeight, trackRate, meta);
+
+ // Set metadata
+ writer.SetMetadata(meta);
+
+ writer.GetContainerData(&encodedBuf, ContainerWriter::GET_HEADER);
+ EXPECT_TRUE(encodedBuf.Length() > 0);
+}
+
+TEST(WebMWriter, Cluster)
+{
+ TestWebMWriter writer;
+ nsTArray<RefPtr<TrackMetadataBase>> meta;
+ TrackRate trackRate = 48000;
+ // Get opus metadata.
+ int channel = 1;
+ GetOpusMetadata(channel, trackRate, meta);
+ // Get vp8 metadata
+ int32_t width = 320;
+ int32_t height = 240;
+ int32_t displayWidth = 320;
+ int32_t displayHeight = 240;
+ GetVP8Metadata(width, height, displayWidth, displayHeight, trackRate, meta);
+ writer.SetMetadata(meta);
+
+ nsTArray<nsTArray<uint8_t>> encodedBuf;
+ writer.GetContainerData(&encodedBuf, ContainerWriter::GET_HEADER);
+ EXPECT_TRUE(encodedBuf.Length() > 0);
+ encodedBuf.Clear();
+
+ // write the first I-Frame.
+ writer.AppendDummyFrame(EncodedFrame::VP8_I_FRAME, FIXED_DURATION);
+ EXPECT_TRUE(writer.HaveValidCluster());
+
+ // The second I-Frame.
+ writer.AppendDummyFrame(EncodedFrame::VP8_I_FRAME, FIXED_DURATION);
+ EXPECT_TRUE(writer.HaveValidCluster());
+
+ // P-Frame.
+ writer.AppendDummyFrame(EncodedFrame::VP8_P_FRAME, FIXED_DURATION);
+ EXPECT_TRUE(writer.HaveValidCluster());
+
+ // The third I-Frame.
+ writer.AppendDummyFrame(EncodedFrame::VP8_I_FRAME, FIXED_DURATION);
+ EXPECT_TRUE(writer.HaveValidCluster());
+}
+
+TEST(WebMWriter, FLUSH_NEEDED)
+{
+ TestWebMWriter writer;
+ nsTArray<RefPtr<TrackMetadataBase>> meta;
+ TrackRate trackRate = 44100;
+ // Get opus metadata.
+ int channel = 2;
+ GetOpusMetadata(channel, trackRate, meta);
+ // Get vp8 metadata
+ int32_t width = 176;
+ int32_t height = 352;
+ int32_t displayWidth = 176;
+ int32_t displayHeight = 352;
+ GetVP8Metadata(width, height, displayWidth, displayHeight, trackRate, meta);
+ writer.SetMetadata(meta);
+ // Have data because the metadata is finished.
+ EXPECT_TRUE(writer.HaveValidCluster());
+
+ // write the first I-Frame.
+ writer.AppendDummyFrame(EncodedFrame::VP8_I_FRAME, FIXED_DURATION);
+
+ // P-Frame
+ writer.AppendDummyFrame(EncodedFrame::VP8_P_FRAME, FIXED_DURATION);
+ // Have data because frames were written.
+ EXPECT_TRUE(writer.HaveValidCluster());
+ // No data because the previous check emptied it.
+ EXPECT_FALSE(writer.HaveValidCluster());
+
+ nsTArray<nsTArray<uint8_t>> encodedBuf;
+ // No data because the flag ContainerWriter::FLUSH_NEEDED does nothing.
+ writer.GetContainerData(&encodedBuf, ContainerWriter::FLUSH_NEEDED);
+ EXPECT_TRUE(encodedBuf.IsEmpty());
+ encodedBuf.Clear();
+
+ // P-Frame
+ writer.AppendDummyFrame(EncodedFrame::VP8_P_FRAME, FIXED_DURATION);
+ // Have data because we continue the previous cluster.
+ EXPECT_TRUE(writer.HaveValidCluster());
+
+ // I-Frame
+ writer.AppendDummyFrame(EncodedFrame::VP8_I_FRAME, FIXED_DURATION);
+ // Have data with a new cluster.
+ EXPECT_TRUE(writer.HaveValidCluster());
+
+ // I-Frame
+ writer.AppendDummyFrame(EncodedFrame::VP8_I_FRAME, FIXED_DURATION);
+ // Have data with a new cluster.
+ EXPECT_TRUE(writer.HaveValidCluster());
+}
+
+struct WebMioData {
+ nsTArray<uint8_t> data;
+ CheckedInt<size_t> offset;
+};
+
+static int webm_read(void* aBuffer, size_t aLength, void* aUserData) {
+ NS_ASSERTION(aUserData, "aUserData must point to a valid WebMioData");
+ WebMioData* ioData = static_cast<WebMioData*>(aUserData);
+
+ // Check the read length.
+ if (aLength > ioData->data.Length()) {
+ return 0;
+ }
+
+ // Check eos.
+ if (ioData->offset.value() >= ioData->data.Length()) {
+ return 0;
+ }
+
+ size_t oldOffset = ioData->offset.value();
+ ioData->offset += aLength;
+ if (!ioData->offset.isValid() ||
+ (ioData->offset.value() > ioData->data.Length())) {
+ return -1;
+ }
+ memcpy(aBuffer, ioData->data.Elements() + oldOffset, aLength);
+ return 1;
+}
+
+static int webm_seek(int64_t aOffset, int aWhence, void* aUserData) {
+ NS_ASSERTION(aUserData, "aUserData must point to a valid WebMioData");
+ WebMioData* ioData = static_cast<WebMioData*>(aUserData);
+
+ if (Abs(aOffset) > ioData->data.Length()) {
+ NS_ERROR("Invalid aOffset");
+ return -1;
+ }
+
+ switch (aWhence) {
+ case NESTEGG_SEEK_END: {
+ CheckedInt<size_t> tempOffset = ioData->data.Length();
+ ioData->offset = tempOffset + aOffset;
+ break;
+ }
+ case NESTEGG_SEEK_CUR:
+ ioData->offset += aOffset;
+ break;
+ case NESTEGG_SEEK_SET:
+ ioData->offset = aOffset;
+ break;
+ default:
+ NS_ERROR("Unknown whence");
+ return -1;
+ }
+
+ if (!ioData->offset.isValid()) {
+ NS_ERROR("Invalid offset");
+ return -1;
+ }
+
+ return 0;
+}
+
+static int64_t webm_tell(void* aUserData) {
+ NS_ASSERTION(aUserData, "aUserData must point to a valid WebMioData");
+ WebMioData* ioData = static_cast<WebMioData*>(aUserData);
+ return ioData->offset.isValid() ? ioData->offset.value() : -1;
+}
+
+TEST(WebMWriter, bug970774_aspect_ratio)
+{
+ TestWebMWriter writer;
+ nsTArray<RefPtr<TrackMetadataBase>> meta;
+ TrackRate trackRate = 44100;
+ // Get opus metadata.
+ int channel = 1;
+ GetOpusMetadata(channel, trackRate, meta);
+ // Set vp8 metadata
+ int32_t width = 640;
+ int32_t height = 480;
+ int32_t displayWidth = 1280;
+ int32_t displayHeight = 960;
+ GetVP8Metadata(width, height, displayWidth, displayHeight, trackRate, meta);
+ writer.SetMetadata(meta);
+
+ // write the first I-Frame.
+ writer.AppendDummyFrame(EncodedFrame::VP8_I_FRAME, FIXED_DURATION);
+
+ // write the second I-Frame.
+ writer.AppendDummyFrame(EncodedFrame::VP8_I_FRAME, FIXED_DURATION);
+
+ // Get the metadata and the first cluster.
+ nsTArray<nsTArray<uint8_t>> encodedBuf;
+ writer.GetContainerData(&encodedBuf, 0);
+ // Flatten the encodedBuf.
+ WebMioData ioData;
+ ioData.offset = 0;
+ for (uint32_t i = 0; i < encodedBuf.Length(); ++i) {
+ ioData.data.AppendElements(encodedBuf[i]);
+ }
+
+ // Use nestegg to verify the information in metadata.
+ nestegg* context = nullptr;
+ nestegg_io io;
+ io.read = webm_read;
+ io.seek = webm_seek;
+ io.tell = webm_tell;
+ io.userdata = static_cast<void*>(&ioData);
+ int rv = nestegg_init(&context, io, nullptr, -1);
+ EXPECT_EQ(rv, 0);
+ unsigned int ntracks = 0;
+ rv = nestegg_track_count(context, &ntracks);
+ EXPECT_EQ(rv, 0);
+ EXPECT_EQ(ntracks, (unsigned int)2);
+ for (unsigned int track = 0; track < ntracks; ++track) {
+ int id = nestegg_track_codec_id(context, track);
+ EXPECT_NE(id, -1);
+ int type = nestegg_track_type(context, track);
+ if (type == NESTEGG_TRACK_VIDEO) {
+ nestegg_video_params params;
+ rv = nestegg_track_video_params(context, track, &params);
+ EXPECT_EQ(rv, 0);
+ EXPECT_EQ(width, static_cast<int32_t>(params.width));
+ EXPECT_EQ(height, static_cast<int32_t>(params.height));
+ EXPECT_EQ(displayWidth, static_cast<int32_t>(params.display_width));
+ EXPECT_EQ(displayHeight, static_cast<int32_t>(params.display_height));
+ } else if (type == NESTEGG_TRACK_AUDIO) {
+ nestegg_audio_params params;
+ rv = nestegg_track_audio_params(context, track, &params);
+ EXPECT_EQ(rv, 0);
+ EXPECT_EQ(channel, static_cast<int>(params.channels));
+ EXPECT_EQ(static_cast<double>(trackRate), params.rate);
+ }
+ }
+ if (context) {
+ nestegg_destroy(context);
+ }
+}
+
+/**
+ * Test that we don't crash when writing two video frames that are too far apart
+ * to fit in the same cluster (>32767ms).
+ */
+TEST(WebMWriter, LongVideoGap)
+{
+ TestWebMWriter writer;
+ nsTArray<RefPtr<TrackMetadataBase>> meta;
+ TrackRate trackRate = 44100;
+ // Set vp8 metadata
+ int32_t width = 640;
+ int32_t height = 480;
+ int32_t displayWidth = 640;
+ int32_t displayHeight = 480;
+ GetVP8Metadata(width, height, displayWidth, displayHeight, trackRate, meta);
+ writer.SetMetadata(meta);
+
+ // write the first I-Frame.
+ writer.AppendDummyFrame(EncodedFrame::VP8_I_FRAME,
+ media::TimeUnit::FromSeconds(33).ToMicroseconds());
+
+ // write the second I-Frame.
+ writer.AppendDummyFrame(EncodedFrame::VP8_I_FRAME,
+ media::TimeUnit::FromSeconds(0.33).ToMicroseconds());
+
+ nsTArray<nsTArray<uint8_t>> encodedBuf;
+ writer.GetContainerData(&encodedBuf, ContainerWriter::GET_HEADER);
+ // metadata + 2 frames
+ EXPECT_EQ(encodedBuf.Length(), 3U);
+}
diff --git a/dom/media/gtest/WaitFor.cpp b/dom/media/gtest/WaitFor.cpp
new file mode 100644
index 0000000000..ab96fa9ae5
--- /dev/null
+++ b/dom/media/gtest/WaitFor.cpp
@@ -0,0 +1,19 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WaitFor.h"
+
+namespace mozilla {
+
+void WaitFor(MediaEventSource<void>& aEvent) {
+ bool done = false;
+ MediaEventListener listener =
+ aEvent.Connect(AbstractThread::GetCurrent(), [&] { done = true; });
+ SpinEventLoopUntil<ProcessFailureBehavior::IgnoreAndContinue>(
+ "WaitFor(MediaEventSource<void>& aEvent)"_ns, [&] { return done; });
+ listener.Disconnect();
+}
+
+} // namespace mozilla
diff --git a/dom/media/gtest/WaitFor.h b/dom/media/gtest/WaitFor.h
new file mode 100644
index 0000000000..00e979b408
--- /dev/null
+++ b/dom/media/gtest/WaitFor.h
@@ -0,0 +1,134 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef WAITFOR_H_
+#define WAITFOR_H_
+
+#include "MediaEventSource.h"
+#include "MediaUtils.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/ResultVariant.h"
+#include "mozilla/SpinEventLoopUntil.h"
+
+namespace mozilla {
+
+/**
+ * Waits for an occurrence of aEvent on the current thread (by blocking it,
+ * except tasks added to the event loop may run) and returns the event's
+ * templated value, if it's non-void.
+ *
+ * The caller must be wary of eventloop issues, in
+ * particular cases where we rely on a stable state runnable, but there is never
+ * a task to trigger stable state. In such cases it is the responsibility of the
+ * caller to create the needed tasks, as JS would. A noteworthy API that relies
+ * on stable state is MediaTrackGraph::GetInstance.
+ */
+template <typename T>
+T WaitFor(MediaEventSource<T>& aEvent) {
+ Maybe<T> value;
+ MediaEventListener listener = aEvent.Connect(
+ AbstractThread::GetCurrent(), [&](T aValue) { value = Some(aValue); });
+ SpinEventLoopUntil<ProcessFailureBehavior::IgnoreAndContinue>(
+ "WaitFor(MediaEventSource<T>& aEvent)"_ns,
+ [&] { return value.isSome(); });
+ listener.Disconnect();
+ return value.value();
+}
+
+/**
+ * Specialization of WaitFor<T> for void.
+ */
+void WaitFor(MediaEventSource<void>& aEvent);
+
+/**
+ * Variant of WaitFor that blocks the caller until a MozPromise has either been
+ * resolved or rejected.
+ */
+template <typename R, typename E, bool Exc>
+Result<R, E> WaitFor(const RefPtr<MozPromise<R, E, Exc>>& aPromise) {
+ Maybe<R> success;
+ Maybe<E> error;
+ aPromise->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [&](R aResult) { success = Some(aResult); },
+ [&](E aError) { error = Some(aError); });
+ SpinEventLoopUntil<ProcessFailureBehavior::IgnoreAndContinue>(
+ "WaitFor(const RefPtr<MozPromise<R, E, Exc>>& aPromise)"_ns,
+ [&] { return success.isSome() || error.isSome(); });
+ if (success.isSome()) {
+ return success.extract();
+ }
+ return Err(error.extract());
+}
+
+/**
+ * A variation of WaitFor that takes a callback to be called each time aEvent is
+ * raised. Blocks the caller until the callback function returns true.
+ */
+template <typename... Args, typename CallbackFunction>
+void WaitUntil(MediaEventSource<Args...>& aEvent, CallbackFunction&& aF) {
+ bool done = false;
+ MediaEventListener listener =
+ aEvent.Connect(AbstractThread::GetCurrent(), [&](Args... aValue) {
+ if (!done) {
+ done = aF(std::forward<Args>(aValue)...);
+ }
+ });
+ SpinEventLoopUntil<ProcessFailureBehavior::IgnoreAndContinue>(
+ "WaitUntil(MediaEventSource<Args...>& aEvent, CallbackFunction&& aF)"_ns,
+ [&] { return done; });
+ listener.Disconnect();
+}
+
+template <typename... Args>
+using TakeNPromise = MozPromise<std::vector<std::tuple<Args...>>, bool, true>;
+
+template <ListenerPolicy Lp, typename... Args>
+auto TakeN(MediaEventSourceImpl<Lp, Args...>& aEvent, size_t aN)
+ -> RefPtr<TakeNPromise<Args...>> {
+ using Storage = std::vector<std::tuple<Args...>>;
+ using Promise = TakeNPromise<Args...>;
+ using Values = media::Refcountable<Storage>;
+ using Listener = media::Refcountable<MediaEventListener>;
+ RefPtr<Values> values = MakeRefPtr<Values>();
+ values->reserve(aN);
+ RefPtr<Listener> listener = MakeRefPtr<Listener>();
+ auto promise = InvokeAsync(
+ AbstractThread::GetCurrent(), __func__, [values, aN]() mutable {
+ SpinEventLoopUntil<ProcessFailureBehavior::IgnoreAndContinue>(
+ "TakeN(MediaEventSourceImpl<Lp, Args...>& aEvent, size_t aN)"_ns,
+ [&] { return values->size() == aN; });
+ return Promise::CreateAndResolve(std::move(*values), __func__);
+ });
+ *listener = aEvent.Connect(AbstractThread::GetCurrent(),
+ [values, listener, aN](Args... aValue) {
+ values->push_back({aValue...});
+ if (values->size() == aN) {
+ listener->Disconnect();
+ }
+ });
+ return promise;
+}
+
+/**
+ * Helper that, given that canonicals have just been updated on the current
+ * thread, will block its execution until mirrors and their watchers have
+ * executed on aTarget.
+ */
+inline void WaitForMirrors(const RefPtr<nsISerialEventTarget>& aTarget) {
+ Unused << WaitFor(InvokeAsync(aTarget, __func__, [] {
+ return GenericPromise::CreateAndResolve(true, "WaitForMirrors resolver");
+ }));
+}
+
+/**
+ * Short form of WaitForMirrors that assumes mirrors are on the current thread
+ * (like canonicals).
+ */
+inline void WaitForMirrors() { WaitForMirrors(GetCurrentSerialEventTarget()); }
+
+} // namespace mozilla
+
+#endif // WAITFOR_H_
diff --git a/dom/media/gtest/YUVBufferGenerator.cpp b/dom/media/gtest/YUVBufferGenerator.cpp
new file mode 100644
index 0000000000..60c8c6adcb
--- /dev/null
+++ b/dom/media/gtest/YUVBufferGenerator.cpp
@@ -0,0 +1,144 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "YUVBufferGenerator.h"
+
+#include "VideoUtils.h"
+
+using namespace mozilla::layers;
+using namespace mozilla;
+
+void YUVBufferGenerator::Init(const mozilla::gfx::IntSize& aSize) {
+ mImageSize = aSize;
+
+ int yPlaneLen = aSize.width * aSize.height;
+ int cbcrPlaneLen = (yPlaneLen + 1) / 2;
+ int frameLen = yPlaneLen + cbcrPlaneLen;
+
+ // Generate source buffer.
+ mSourceBuffer.SetLength(frameLen);
+
+ // Fill Y plane.
+ memset(mSourceBuffer.Elements(), 0x10, yPlaneLen);
+
+ // Fill Cb/Cr planes.
+ memset(mSourceBuffer.Elements() + yPlaneLen, 0x80, cbcrPlaneLen);
+}
+
+mozilla::gfx::IntSize YUVBufferGenerator::GetSize() const { return mImageSize; }
+
+already_AddRefed<Image> YUVBufferGenerator::GenerateI420Image() {
+ return do_AddRef(CreateI420Image());
+}
+
+already_AddRefed<Image> YUVBufferGenerator::GenerateNV12Image() {
+ return do_AddRef(CreateNV12Image());
+}
+
+already_AddRefed<Image> YUVBufferGenerator::GenerateNV21Image() {
+ return do_AddRef(CreateNV21Image());
+}
+
+Image* YUVBufferGenerator::CreateI420Image() {
+ PlanarYCbCrImage* image =
+ new RecyclingPlanarYCbCrImage(new BufferRecycleBin());
+ PlanarYCbCrData data;
+ data.mPictureRect = gfx::IntRect(0, 0, mImageSize.width, mImageSize.height);
+
+ const uint32_t yPlaneSize = mImageSize.width * mImageSize.height;
+ const uint32_t halfWidth = (mImageSize.width + 1) / 2;
+ const uint32_t halfHeight = (mImageSize.height + 1) / 2;
+ const uint32_t uvPlaneSize = halfWidth * halfHeight;
+
+ // Y plane.
+ uint8_t* y = mSourceBuffer.Elements();
+ data.mYChannel = y;
+ data.mYStride = mImageSize.width;
+ data.mYSkip = 0;
+
+ // Cr plane (aka V).
+ uint8_t* cr = y + yPlaneSize + uvPlaneSize;
+ data.mCrChannel = cr;
+ data.mCrSkip = 0;
+
+ // Cb plane (aka U).
+ uint8_t* cb = y + yPlaneSize;
+ data.mCbChannel = cb;
+ data.mCbSkip = 0;
+
+ // CrCb plane vectors.
+ data.mCbCrStride = halfWidth;
+ data.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT;
+
+ data.mYUVColorSpace = DefaultColorSpace(mImageSize);
+
+ image->CopyData(data);
+ return image;
+}
+
+Image* YUVBufferGenerator::CreateNV12Image() {
+ NVImage* image = new NVImage();
+ PlanarYCbCrData data;
+ data.mPictureRect = gfx::IntRect(0, 0, mImageSize.width, mImageSize.height);
+
+ const uint32_t yPlaneSize = mImageSize.width * mImageSize.height;
+
+ // Y plane.
+ uint8_t* y = mSourceBuffer.Elements();
+ data.mYChannel = y;
+ data.mYStride = mImageSize.width;
+ data.mYSkip = 0;
+
+ // Cb plane (aka U).
+ uint8_t* cb = y + yPlaneSize;
+ data.mCbChannel = cb;
+ data.mCbSkip = 1;
+
+ // Cr plane (aka V).
+ uint8_t* cr = y + yPlaneSize + 1;
+ data.mCrChannel = cr;
+ data.mCrSkip = 1;
+
+ // 4:2:0.
+ data.mCbCrStride = mImageSize.width;
+ data.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT;
+
+ image->SetData(data);
+ return image;
+}
+
+Image* YUVBufferGenerator::CreateNV21Image() {
+ NVImage* image = new NVImage();
+ PlanarYCbCrData data;
+ data.mPictureRect = gfx::IntRect(0, 0, mImageSize.width, mImageSize.height);
+
+ const uint32_t yPlaneSize = mImageSize.width * mImageSize.height;
+
+ // Y plane.
+ uint8_t* y = mSourceBuffer.Elements();
+ data.mYChannel = y;
+ data.mYStride = mImageSize.width;
+ data.mYSkip = 0;
+
+ // Cb plane (aka U).
+ uint8_t* cb = y + yPlaneSize + 1;
+ data.mCbChannel = cb;
+ data.mCbSkip = 1;
+
+ // Cr plane (aka V).
+ uint8_t* cr = y + yPlaneSize;
+ data.mCrChannel = cr;
+ data.mCrSkip = 1;
+
+ // 4:2:0.
+ data.mCbCrStride = mImageSize.width;
+ data.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT;
+
+ data.mYUVColorSpace = DefaultColorSpace(mImageSize);
+
+ image->SetData(data);
+ return image;
+}
diff --git a/dom/media/gtest/YUVBufferGenerator.h b/dom/media/gtest/YUVBufferGenerator.h
new file mode 100644
index 0000000000..cb6ed6b220
--- /dev/null
+++ b/dom/media/gtest/YUVBufferGenerator.h
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef YUVBufferGenerator_h
+#define YUVBufferGenerator_h
+
+#include "ImageContainer.h"
+#include "mozilla/AlreadyAddRefed.h"
+#include "nsTArray.h"
+#include "Point.h" // mozilla::gfx::IntSize
+
+// A helper object to generate of different YUV planes.
+class YUVBufferGenerator {
+ public:
+ void Init(const mozilla::gfx::IntSize& aSize);
+ mozilla::gfx::IntSize GetSize() const;
+ already_AddRefed<mozilla::layers::Image> GenerateI420Image();
+ already_AddRefed<mozilla::layers::Image> GenerateNV12Image();
+ already_AddRefed<mozilla::layers::Image> GenerateNV21Image();
+
+ private:
+ mozilla::layers::Image* CreateI420Image();
+ mozilla::layers::Image* CreateNV12Image();
+ mozilla::layers::Image* CreateNV21Image();
+ mozilla::gfx::IntSize mImageSize;
+ nsTArray<uint8_t> mSourceBuffer;
+};
+
+#endif // YUVBufferGenerator_h
diff --git a/dom/media/gtest/dash_dashinit.mp4 b/dom/media/gtest/dash_dashinit.mp4
new file mode 100644
index 0000000000..d19068f36d
--- /dev/null
+++ b/dom/media/gtest/dash_dashinit.mp4
Binary files differ
diff --git a/dom/media/gtest/hello.rs b/dom/media/gtest/hello.rs
new file mode 100644
index 0000000000..af1308eee6
--- /dev/null
+++ b/dom/media/gtest/hello.rs
@@ -0,0 +1,6 @@
+#[no_mangle]
+pub extern "C" fn test_rust() -> *const u8 {
+ // NB: rust &str aren't null terminated.
+ let greeting = "hello from rust.\0";
+ greeting.as_ptr()
+}
diff --git a/dom/media/gtest/id3v2header.mp3 b/dom/media/gtest/id3v2header.mp3
new file mode 100644
index 0000000000..2f5585d02e
--- /dev/null
+++ b/dom/media/gtest/id3v2header.mp3
Binary files differ
diff --git a/dom/media/gtest/moz.build b/dom/media/gtest/moz.build
new file mode 100644
index 0000000000..4654d82593
--- /dev/null
+++ b/dom/media/gtest/moz.build
@@ -0,0 +1,148 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+include("/dom/media/webrtc/third_party_build/webrtc.mozbuild")
+
+DEFINES["ENABLE_SET_CUBEB_BACKEND"] = True
+DEFINES["VISIBLE_TIMEUNIT_INTERNALS"] = True
+
+LOCAL_INCLUDES += [
+ "/dom/media/mediasink",
+ "/dom/media/systemservices",
+ "/dom/media/webrtc",
+ "/dom/media/webrtc/common",
+ "/third_party/libwebrtc",
+ "/third_party/libwebrtc/third_party/abseil-cpp",
+]
+
+UNIFIED_SOURCES += [
+ "MockCubeb.cpp",
+ "MockMediaResource.cpp",
+ "TestAudioBuffers.cpp",
+ "TestAudioCallbackDriver.cpp",
+ "TestAudioCompactor.cpp",
+ "TestAudioDecoderInputTrack.cpp",
+ "TestAudioDriftCorrection.cpp",
+ "TestAudioInputSource.cpp",
+ "TestAudioMixer.cpp",
+ "TestAudioPacketizer.cpp",
+ "TestAudioRingBuffer.cpp",
+ "TestAudioSegment.cpp",
+ "TestAudioTrackEncoder.cpp",
+ "TestAudioTrackGraph.cpp",
+ "TestBenchmarkStorage.cpp",
+ "TestBitWriter.cpp",
+ "TestBlankVideoDataCreator.cpp",
+ "TestBufferReader.cpp",
+ "TestCubebInputStream.cpp",
+ "TestDataMutex.cpp",
+ "TestDecoderBenchmark.cpp",
+ "TestDeviceInputTrack.cpp",
+ "TestDriftCompensation.cpp",
+ "TestDynamicResampler.cpp",
+ "TestGMPUtils.cpp",
+ "TestGroupId.cpp",
+ "TestIntervalSet.cpp",
+ "TestKeyValueStorage.cpp",
+ "TestMediaCodecsSupport.cpp",
+ "TestMediaDataDecoder.cpp",
+ "TestMediaDataEncoder.cpp",
+ "TestMediaEventSource.cpp",
+ "TestMediaMIMETypes.cpp",
+ "TestMediaQueue.cpp",
+ "TestMediaSpan.cpp",
+ "TestMediaUtils.cpp",
+ "TestMP3Demuxer.cpp",
+ "TestMP4Demuxer.cpp",
+ "TestMuxer.cpp",
+ "TestOggWriter.cpp",
+ "TestOpusParser.cpp",
+ "TestPacer.cpp",
+ "TestRust.cpp",
+ "TestTimeUnit.cpp",
+ "TestVideoSegment.cpp",
+ "TestVideoTrackEncoder.cpp",
+ "TestVideoUtils.cpp",
+ "TestVPXDecoding.cpp",
+ "TestWebMBuffered.cpp",
+ "TestWebMWriter.cpp",
+ "WaitFor.cpp",
+ "YUVBufferGenerator.cpp",
+]
+
+if CONFIG["MOZ_WEBRTC"]:
+ UNIFIED_SOURCES += [
+ "TestAudioInputProcessing.cpp",
+ "TestRTCStatsTimestampMaker.cpp",
+ ]
+
+if CONFIG["OS_TARGET"] != "Android":
+ UNIFIED_SOURCES += [
+ "TestCDMStorage.cpp",
+ "TestGMPCrossOrigin.cpp",
+ "TestGMPRemoveAndDelete.cpp",
+ ]
+
+if CONFIG["MOZ_WEBRTC"] and CONFIG["OS_TARGET"] != "Android":
+ UNIFIED_SOURCES += [
+ "TestAudioDeviceEnumerator.cpp",
+ "TestVideoFrameConverter.cpp",
+ ]
+
+TEST_HARNESS_FILES.gtest += [
+ "../test/av1.mp4",
+ "../test/gizmo-frag.mp4",
+ "../test/gizmo.mp4",
+ "../test/vp9cake.webm",
+ "dash_dashinit.mp4",
+ "id3v2header.mp3",
+ "negative_duration.mp4",
+ "noise.mp3",
+ "noise_vbr.mp3",
+ "short-zero-in-moov.mp4",
+ "short-zero-inband.mov",
+ "small-shot-false-positive.mp3",
+ "small-shot-partial-xing.mp3",
+ "small-shot.mp3",
+ "test.webm",
+ "test_case_1224361.vp8.ivf",
+ "test_case_1224363.vp8.ivf",
+ "test_case_1224369.vp8.ivf",
+ "test_InvalidElementId.webm",
+ "test_InvalidElementSize.webm",
+ "test_InvalidLargeEBMLMaxIdLength.webm",
+ "test_InvalidLargeElementId.webm",
+ "test_InvalidSmallEBMLMaxIdLength.webm",
+ "test_ValidLargeEBMLMaxIdLength.webm",
+ "test_ValidSmallEBMLMaxSizeLength.webm",
+ "test_vbri.mp3",
+]
+
+TEST_DIRS += [
+ "mp4_demuxer",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+LOCAL_INCLUDES += [
+ "/dom/media",
+ "/dom/media/encoder",
+ "/dom/media/gmp",
+ "/dom/media/mp4",
+ "/dom/media/platforms",
+ "/dom/media/platforms/agnostic",
+ "/dom/media/webrtc",
+ "/gfx/2d/",
+ "/security/certverifier",
+]
+
+FINAL_LIBRARY = "xul-gtest"
+
+if CONFIG["CC_TYPE"] in ("clang", "clang-cl"):
+ CXXFLAGS += [
+ "-Wno-inconsistent-missing-override",
+ "-Wno-unused-private-field",
+ ]
diff --git a/dom/media/gtest/mp4_demuxer/TestInterval.cpp b/dom/media/gtest/mp4_demuxer/TestInterval.cpp
new file mode 100644
index 0000000000..2572b1c392
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/TestInterval.cpp
@@ -0,0 +1,88 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "MP4Interval.h"
+
+using mozilla::MP4Interval;
+
+TEST(MP4Interval, Length)
+{
+ MP4Interval<int> i(15, 25);
+ EXPECT_EQ(10, i.Length());
+}
+
+TEST(MP4Interval, Intersection)
+{
+ MP4Interval<int> i0(10, 20);
+ MP4Interval<int> i1(15, 25);
+ MP4Interval<int> i = i0.Intersection(i1);
+ EXPECT_EQ(15, i.start);
+ EXPECT_EQ(20, i.end);
+}
+
+TEST(MP4Interval, Equals)
+{
+ MP4Interval<int> i0(10, 20);
+ MP4Interval<int> i1(10, 20);
+ EXPECT_EQ(i0, i1);
+
+ MP4Interval<int> i2(5, 20);
+ EXPECT_NE(i0, i2);
+
+ MP4Interval<int> i3(10, 15);
+ EXPECT_NE(i0, i2);
+}
+
+TEST(MP4Interval, IntersectionVector)
+{
+ nsTArray<MP4Interval<int>> i0;
+ i0.AppendElement(MP4Interval<int>(5, 10));
+ i0.AppendElement(MP4Interval<int>(20, 25));
+ i0.AppendElement(MP4Interval<int>(40, 60));
+
+ nsTArray<MP4Interval<int>> i1;
+ i1.AppendElement(MP4Interval<int>(7, 15));
+ i1.AppendElement(MP4Interval<int>(16, 27));
+ i1.AppendElement(MP4Interval<int>(45, 50));
+ i1.AppendElement(MP4Interval<int>(53, 57));
+
+ nsTArray<MP4Interval<int>> i;
+ MP4Interval<int>::Intersection(i0, i1, &i);
+
+ EXPECT_EQ(4u, i.Length());
+
+ EXPECT_EQ(7, i[0].start);
+ EXPECT_EQ(10, i[0].end);
+
+ EXPECT_EQ(20, i[1].start);
+ EXPECT_EQ(25, i[1].end);
+
+ EXPECT_EQ(45, i[2].start);
+ EXPECT_EQ(50, i[2].end);
+
+ EXPECT_EQ(53, i[3].start);
+ EXPECT_EQ(57, i[3].end);
+}
+
+TEST(MP4Interval, Normalize)
+{
+ nsTArray<MP4Interval<int>> i;
+ i.AppendElement(MP4Interval<int>(20, 30));
+ i.AppendElement(MP4Interval<int>(1, 8));
+ i.AppendElement(MP4Interval<int>(5, 10));
+ i.AppendElement(MP4Interval<int>(2, 7));
+
+ nsTArray<MP4Interval<int>> o;
+ MP4Interval<int>::Normalize(i, &o);
+
+ EXPECT_EQ(2u, o.Length());
+
+ EXPECT_EQ(1, o[0].start);
+ EXPECT_EQ(10, o[0].end);
+
+ EXPECT_EQ(20, o[1].start);
+ EXPECT_EQ(30, o[1].end);
+}
diff --git a/dom/media/gtest/mp4_demuxer/TestMP4.cpp b/dom/media/gtest/mp4_demuxer/TestMP4.cpp
new file mode 100644
index 0000000000..df58ec42e2
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/TestMP4.cpp
@@ -0,0 +1,133 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "mp4parse.h"
+
+#include <stdint.h>
+#include <stdio.h>
+#include <string.h>
+#include <algorithm>
+#include <vector>
+
+static intptr_t error_reader(uint8_t* buffer, uintptr_t size, void* userdata) {
+ return -1;
+}
+
+struct read_vector {
+ explicit read_vector(FILE* file, size_t length);
+ explicit read_vector(size_t length);
+
+ size_t location;
+ std::vector<uint8_t> buffer;
+};
+
+read_vector::read_vector(FILE* file, size_t length) : location(0) {
+ buffer.resize(length);
+ size_t read = fread(buffer.data(), sizeof(decltype(buffer)::value_type),
+ buffer.size(), file);
+ buffer.resize(read);
+}
+
+read_vector::read_vector(size_t length) : location(0) {
+ buffer.resize(length, 0);
+}
+
+static intptr_t vector_reader(uint8_t* buffer, uintptr_t size, void* userdata) {
+ if (!buffer || !userdata) {
+ return -1;
+ }
+
+ auto source = reinterpret_cast<read_vector*>(userdata);
+ if (source->location > source->buffer.size()) {
+ return -1;
+ }
+ uintptr_t available =
+ source->buffer.data() ? source->buffer.size() - source->location : 0;
+ uintptr_t length = std::min(available, size);
+ if (length) {
+ memcpy(buffer, source->buffer.data() + source->location, length);
+ source->location += length;
+ }
+ return length;
+}
+
+TEST(rust, MP4MetadataEmpty)
+{
+ Mp4parseStatus rv;
+ Mp4parseIo io;
+ Mp4parseParser* parser = nullptr;
+
+ // Shouldn't be able to read with no context.
+ rv = mp4parse_new(nullptr, nullptr);
+ EXPECT_EQ(rv, MP4PARSE_STATUS_BAD_ARG);
+
+ // Shouldn't be able to wrap an Mp4parseIo with null members.
+ io = {nullptr, nullptr};
+ rv = mp4parse_new(&io, &parser);
+ EXPECT_EQ(rv, MP4PARSE_STATUS_BAD_ARG);
+ EXPECT_EQ(parser, nullptr);
+
+ io = {nullptr, &io};
+ rv = mp4parse_new(&io, &parser);
+ EXPECT_EQ(rv, MP4PARSE_STATUS_BAD_ARG);
+ EXPECT_EQ(parser, nullptr);
+
+ // FIXME: this should probably be accepted.
+ io = {error_reader, nullptr};
+ rv = mp4parse_new(&io, &parser);
+ EXPECT_EQ(rv, MP4PARSE_STATUS_BAD_ARG);
+ EXPECT_EQ(parser, nullptr);
+
+ // Read method errors should propagate.
+ io = {error_reader, &io};
+ rv = mp4parse_new(&io, &parser);
+ ASSERT_EQ(parser, nullptr);
+ EXPECT_EQ(rv, MP4PARSE_STATUS_IO);
+
+ // Short buffers should fail.
+ read_vector buf(0);
+ io = {vector_reader, &buf};
+ rv = mp4parse_new(&io, &parser);
+ ASSERT_EQ(parser, nullptr);
+ EXPECT_EQ(rv, MP4PARSE_STATUS_MOOV_MISSING);
+
+ buf.buffer.reserve(4097);
+ rv = mp4parse_new(&io, &parser);
+ ASSERT_EQ(parser, nullptr);
+ EXPECT_EQ(rv, MP4PARSE_STATUS_MOOV_MISSING);
+
+ // Empty buffers should fail.
+ buf.buffer.resize(4097, 0);
+ rv = mp4parse_new(&io, &parser);
+ ASSERT_EQ(parser, nullptr);
+ EXPECT_EQ(rv, MP4PARSE_STATUS_UNSUPPORTED);
+}
+
+TEST(rust, MP4Metadata)
+{
+ FILE* f = fopen("street.mp4", "rb");
+ ASSERT_TRUE(f != nullptr);
+ // Read just the moov header to work around the parser
+ // treating mid-box eof as an error.
+ // read_vector reader = read_vector(f, 1061);
+ struct stat s;
+ ASSERT_EQ(0, fstat(fileno(f), &s));
+ read_vector reader = read_vector(f, s.st_size);
+ fclose(f);
+
+ Mp4parseIo io = {vector_reader, &reader};
+ Mp4parseParser* parser = nullptr;
+ Mp4parseStatus rv = mp4parse_new(&io, &parser);
+ ASSERT_NE(nullptr, parser);
+ EXPECT_EQ(MP4PARSE_STATUS_OK, rv);
+
+ uint32_t tracks = 0;
+ rv = mp4parse_get_track_count(parser, &tracks);
+ EXPECT_EQ(MP4PARSE_STATUS_OK, rv);
+ EXPECT_EQ(2U, tracks);
+
+ mp4parse_free(parser);
+}
diff --git a/dom/media/gtest/mp4_demuxer/TestParser.cpp b/dom/media/gtest/mp4_demuxer/TestParser.cpp
new file mode 100644
index 0000000000..4c71a6469f
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/TestParser.cpp
@@ -0,0 +1,1019 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "js/Conversions.h"
+#include "MediaData.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/gtest/MozAssertions.h"
+#include "mozilla/Preferences.h"
+
+#include "BufferStream.h"
+#include "MP4Metadata.h"
+#include "MoofParser.h"
+#include "TelemetryFixture.h"
+#include "TelemetryTestHelpers.h"
+
+class TestStream;
+namespace mozilla {
+DDLoggedTypeNameAndBase(::TestStream, ByteStream);
+} // namespace mozilla
+
+using namespace mozilla;
+
+static const uint32_t E = MP4Metadata::NumberTracksError();
+
+class TestStream : public ByteStream,
+ public DecoderDoctorLifeLogger<TestStream> {
+ public:
+ TestStream(const uint8_t* aBuffer, size_t aSize)
+ : mHighestSuccessfulEndOffset(0), mBuffer(aBuffer), mSize(aSize) {}
+ bool ReadAt(int64_t aOffset, void* aData, size_t aLength,
+ size_t* aBytesRead) override {
+ if (aOffset < 0 || aOffset > static_cast<int64_t>(mSize)) {
+ return false;
+ }
+ // After the test, 0 <= aOffset <= mSize <= SIZE_MAX, so it's safe to cast
+ // to size_t.
+ size_t offset = static_cast<size_t>(aOffset);
+ // Don't read past the end (but it's not an error to try).
+ if (aLength > mSize - offset) {
+ aLength = mSize - offset;
+ }
+ // Now, 0 <= offset <= offset + aLength <= mSize <= SIZE_MAX.
+ *aBytesRead = aLength;
+ memcpy(aData, mBuffer + offset, aLength);
+ if (mHighestSuccessfulEndOffset < offset + aLength) {
+ mHighestSuccessfulEndOffset = offset + aLength;
+ }
+ return true;
+ }
+ bool CachedReadAt(int64_t aOffset, void* aData, size_t aLength,
+ size_t* aBytesRead) override {
+ return ReadAt(aOffset, aData, aLength, aBytesRead);
+ }
+ bool Length(int64_t* aLength) override {
+ *aLength = mSize;
+ return true;
+ }
+ void DiscardBefore(int64_t aOffset) override {}
+
+ // Offset past the last character ever read. 0 when nothing read yet.
+ size_t mHighestSuccessfulEndOffset;
+
+ protected:
+ virtual ~TestStream() = default;
+
+ const uint8_t* mBuffer;
+ size_t mSize;
+};
+
+TEST(MP4Metadata, EmptyStream)
+{
+ RefPtr<ByteStream> stream = new TestStream(nullptr, 0);
+
+ MP4Metadata::ResultAndByteBuffer metadataBuffer =
+ MP4Metadata::Metadata(stream);
+ EXPECT_TRUE(NS_OK != metadataBuffer.Result());
+ EXPECT_FALSE(static_cast<bool>(metadataBuffer.Ref()));
+
+ MP4Metadata metadata(stream);
+ EXPECT_TRUE(0u ==
+ metadata.GetNumberTracks(TrackInfo::kUndefinedTrack).Ref() ||
+ E == metadata.GetNumberTracks(TrackInfo::kUndefinedTrack).Ref());
+ EXPECT_TRUE(0u == metadata.GetNumberTracks(TrackInfo::kAudioTrack).Ref() ||
+ E == metadata.GetNumberTracks(TrackInfo::kAudioTrack).Ref());
+ EXPECT_TRUE(0u == metadata.GetNumberTracks(TrackInfo::kVideoTrack).Ref() ||
+ E == metadata.GetNumberTracks(TrackInfo::kVideoTrack).Ref());
+ EXPECT_TRUE(0u == metadata.GetNumberTracks(TrackInfo::kTextTrack).Ref() ||
+ E == metadata.GetNumberTracks(TrackInfo::kTextTrack).Ref());
+ EXPECT_FALSE(metadata.GetTrackInfo(TrackInfo::kAudioTrack, 0).Ref());
+ EXPECT_FALSE(metadata.GetTrackInfo(TrackInfo::kVideoTrack, 0).Ref());
+ EXPECT_FALSE(metadata.GetTrackInfo(TrackInfo::kTextTrack, 0).Ref());
+ // We can seek anywhere in any MPEG4.
+ EXPECT_TRUE(metadata.CanSeek());
+ EXPECT_FALSE(metadata.Crypto().Ref()->valid);
+}
+
+TEST(MoofParser, EmptyStream)
+{
+ RefPtr<ByteStream> stream = new TestStream(nullptr, 0);
+
+ MoofParser parser(stream, AsVariant(ParseAllTracks{}), false);
+ EXPECT_EQ(0u, parser.mOffset);
+ EXPECT_TRUE(parser.ReachedEnd());
+
+ MediaByteRangeSet byteRanges;
+ EXPECT_FALSE(parser.RebuildFragmentedIndex(byteRanges));
+
+ EXPECT_TRUE(parser.GetCompositionRange(byteRanges).IsNull());
+ EXPECT_TRUE(parser.mInitRange.IsEmpty());
+ EXPECT_EQ(0u, parser.mOffset);
+ EXPECT_TRUE(parser.ReachedEnd());
+ RefPtr<MediaByteBuffer> metadataBuffer = parser.Metadata();
+ EXPECT_FALSE(metadataBuffer);
+ EXPECT_TRUE(parser.FirstCompleteMediaSegment().IsEmpty());
+ EXPECT_TRUE(parser.FirstCompleteMediaHeader().IsEmpty());
+}
+
+nsTArray<uint8_t> ReadTestFile(const char* aFilename) {
+ if (!aFilename) {
+ return {};
+ }
+ FILE* f = fopen(aFilename, "rb");
+ if (!f) {
+ return {};
+ }
+
+ if (fseek(f, 0, SEEK_END) != 0) {
+ fclose(f);
+ return {};
+ }
+ long position = ftell(f);
+ // I know EOF==-1, so this test is made obsolete by '<0', but I don't want
+ // the code to rely on that.
+ if (position == 0 || position == EOF || position < 0) {
+ fclose(f);
+ return {};
+ }
+ if (fseek(f, 0, SEEK_SET) != 0) {
+ fclose(f);
+ return {};
+ }
+
+ size_t len = static_cast<size_t>(position);
+ nsTArray<uint8_t> buffer(len);
+ buffer.SetLength(len);
+ size_t read = fread(buffer.Elements(), 1, len, f);
+ fclose(f);
+ if (read != len) {
+ return {};
+ }
+
+ return buffer;
+}
+
+struct TestFileData {
+ const char* mFilename;
+ bool mParseResult;
+ uint32_t mNumberVideoTracks;
+ bool mHasVideoIndice;
+ double mVideoDuration; // For first video track, -1 if N/A, in seconds.
+ int32_t mWidth;
+ int32_t mHeight;
+ uint32_t mNumberAudioTracks;
+ double mAudioDuration; // For first audio track, -1 if N/A, in seconds.
+ bool mHasCrypto; // Note, MP4Metadata only considers pssh box for crypto.
+ uint64_t mMoofReachedOffset; // or 0 for the end.
+ bool mValidMoof;
+ int8_t mAudioProfile;
+};
+
+static const TestFileData testFiles[] = {
+ // filename parses? #V hasVideoIndex vDur w h #A aDur hasCrypto? moofOffset
+ // validMoof? audio_profile
+ {"test_case_1156505.mp4", false, 0, false, -1, 0, 0, 0, -1., false, 152,
+ false, 0}, // invalid ''trak box
+ {"test_case_1181213.mp4", true, 1, true, 0.41666666, 320, 240, 1,
+ 0.47746032, true, 0, false, 2},
+ {"test_case_1181215.mp4", true, 0, false, -1, 0, 0, 0, -1, false, 0, false,
+ 0},
+ {"test_case_1181223.mp4", false, 0, false, 0.41666666, 320, 240, 0, -1,
+ false, 0, false, 0},
+ {"test_case_1181719.mp4", false, 0, false, -1, 0, 0, 0, -1, false, 0, false,
+ 0},
+ {"test_case_1185230.mp4", true, 2, true, 0.41666666, 320, 240, 2,
+ 0.0000059754907, false, 0, false, 2},
+ {"test_case_1187067.mp4", true, 1, true, 0.080000, 160, 90, 0, -1, false, 0,
+ false, 0},
+ {"test_case_1200326.mp4", false, 0, false, -1, 0, 0, 0, -1, false, 0, false,
+ 0},
+ {"test_case_1204580.mp4", true, 1, true, 0.502500, 320, 180, 0, -1, false,
+ 0, false, 0},
+ {"test_case_1216748.mp4", false, 0, false, -1, 0, 0, 0, -1, false, 152,
+ false, 0}, // invalid 'trak' box
+ {"test_case_1296473.mp4", false, 0, false, -1, 0, 0, 0, -1, false, 0, false,
+ 0},
+ {"test_case_1296532.mp4", true, 1, true, 5.589333, 560, 320, 1, 5.589333,
+ true, 0, true, 2},
+ {"test_case_1301065.mp4", true, 0, false, -1, 0, 0, 1, 100079991719, false,
+ 0, false, 2},
+ {"test_case_1301065-u32max.mp4", true, 0, false, -1, 0, 0, 1, 97391.548639,
+ false, 0, false, 2},
+ {"test_case_1301065-max-ez.mp4", true, 0, false, -1, 0, 0, 1,
+ 209146758.205306, false, 0, false, 2},
+ {"test_case_1301065-harder.mp4", true, 0, false, -1, 0, 0, 1,
+ 209146758.205328, false, 0, false, 2},
+ {"test_case_1301065-max-ok.mp4", true, 0, false, -1, 0, 0, 1,
+ 9223372036854.775, false, 0, false, 2},
+ // The duration is overflow for int64_t in TestFileData, parser uses
+ // uint64_t so
+ // this file is ignore.
+ //{ "test_case_1301065-overfl.mp4", 0, -1, 0, 0, 1, 9223372036854775827,
+ // false, 0,
+ // false, 2
+ // },
+ {"test_case_1301065-i64max.mp4", true, 0, false, -1, 0, 0, 1,
+ std::numeric_limits<double>::infinity(), false, 0, false, 2},
+ {"test_case_1301065-i64min.mp4", true, 0, false, -1, 0, 0, 1,
+ -std::numeric_limits<double>::infinity(), false, 0, false, 2},
+ {"test_case_1301065-u64max.mp4", true, 0, false, -1, 0, 0, 1, 0, false, 0,
+ false, 2},
+ {"test_case_1329061.mov", false, 0, false, -1, 0, 0, 1, 234567981, false, 0,
+ false, 2},
+ {"test_case_1351094.mp4", true, 0, false, -1, 0, 0, 0, -1, false, 0, true,
+ 0},
+ {"test_case_1389299.mp4", true, 1, true, 5.589333, 560, 320, 1, 5.589333,
+ true, 0, true, 2},
+
+ {"test_case_1389527.mp4", true, 1, false, 5.005000, 80, 128, 1, 4.992000,
+ false, 0, false, 2},
+ {"test_case_1395244.mp4", true, 1, true, 0.41666666, 320, 240, 1,
+ 0.47746032, false, 0, false, 2},
+ {"test_case_1388991.mp4", true, 0, false, -1, 0, 0, 1, 30.000181, false, 0,
+ false, 2},
+ {"test_case_1410565.mp4", false, 0, false, 0, 0, 0, 0, 0, false, 955100,
+ true, 2}, // negative 'timescale'
+ {"test_case_1513651-2-sample-description-entries.mp4", true, 1, true,
+ 9.843344, 400, 300, 0, -1, true, 0, false, 0},
+ {"test_case_1519617-cenc-init-with-track_id-0.mp4", true, 1, true, 0, 1272,
+ 530, 0, -1, false, 0, false,
+ 0}, // Uses bad track id 0 and has a sinf but no pssh
+ {"test_case_1519617-track2-trafs-removed.mp4", true, 1, true, 10.032000,
+ 400, 300, 1, 10.032000, false, 0, true, 2},
+ {"test_case_1519617-video-has-track_id-0.mp4", true, 1, true, 10.032000,
+ 400, 300, 1, 10.032000, false, 0, true, 2}, // Uses bad track id 0
+ // The following file has multiple sample description entries with the same
+ // crypto information. This does not cover multiple entries with different
+ // crypto information which is tracked by
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1714626
+ {"test_case_1714125-2-sample-description-entires-with-identical-crypto.mp4",
+ true, 1, true, 0, 1920, 1080, 0, 0, true, 0, false, 0},
+};
+
+TEST(MP4Metadata, test_case_mp4)
+{
+ const TestFileData* tests = nullptr;
+ size_t length = 0;
+
+ tests = testFiles;
+ length = ArrayLength(testFiles);
+
+ for (size_t test = 0; test < length; ++test) {
+ nsTArray<uint8_t> buffer = ReadTestFile(tests[test].mFilename);
+ ASSERT_FALSE(buffer.IsEmpty());
+ RefPtr<ByteStream> stream =
+ new TestStream(buffer.Elements(), buffer.Length());
+
+ MP4Metadata::ResultAndByteBuffer metadataBuffer =
+ MP4Metadata::Metadata(stream);
+ EXPECT_EQ(NS_OK, metadataBuffer.Result());
+ EXPECT_TRUE(metadataBuffer.Ref());
+
+ MP4Metadata metadata(stream);
+ nsresult res = metadata.Parse();
+ EXPECT_EQ(tests[test].mParseResult, NS_SUCCEEDED(res))
+ << tests[test].mFilename;
+ if (!tests[test].mParseResult) {
+ continue;
+ }
+
+ EXPECT_EQ(tests[test].mNumberAudioTracks,
+ metadata.GetNumberTracks(TrackInfo::kAudioTrack).Ref())
+ << tests[test].mFilename;
+ EXPECT_EQ(tests[test].mNumberVideoTracks,
+ metadata.GetNumberTracks(TrackInfo::kVideoTrack).Ref())
+ << tests[test].mFilename;
+ // If there is an error, we should expect an error code instead of zero
+ // for non-Audio/Video tracks.
+ const uint32_t None = (tests[test].mNumberVideoTracks == E) ? E : 0;
+ EXPECT_EQ(None, metadata.GetNumberTracks(TrackInfo::kUndefinedTrack).Ref())
+ << tests[test].mFilename;
+ EXPECT_EQ(None, metadata.GetNumberTracks(TrackInfo::kTextTrack).Ref())
+ << tests[test].mFilename;
+ EXPECT_FALSE(metadata.GetTrackInfo(TrackInfo::kUndefinedTrack, 0).Ref());
+ MP4Metadata::ResultAndTrackInfo trackInfo =
+ metadata.GetTrackInfo(TrackInfo::kVideoTrack, 0);
+ if (!!tests[test].mNumberVideoTracks) {
+ ASSERT_TRUE(!!trackInfo.Ref());
+ const VideoInfo* videoInfo = trackInfo.Ref()->GetAsVideoInfo();
+ ASSERT_TRUE(!!videoInfo);
+ EXPECT_TRUE(videoInfo->IsValid()) << tests[test].mFilename;
+ EXPECT_TRUE(videoInfo->IsVideo()) << tests[test].mFilename;
+ if (std::isinf(tests[test].mVideoDuration)) {
+ ASSERT_TRUE(std::isinf(videoInfo->mDuration.ToSeconds()));
+ } else {
+ EXPECT_FLOAT_EQ(tests[test].mVideoDuration,
+ videoInfo->mDuration.ToSeconds())
+ << tests[test].mFilename;
+ }
+ EXPECT_EQ(tests[test].mWidth, videoInfo->mDisplay.width)
+ << tests[test].mFilename;
+ EXPECT_EQ(tests[test].mHeight, videoInfo->mDisplay.height)
+ << tests[test].mFilename;
+
+ MP4Metadata::ResultAndIndice indices =
+ metadata.GetTrackIndice(videoInfo->mTrackId);
+ EXPECT_EQ(!!indices.Ref(), tests[test].mHasVideoIndice)
+ << tests[test].mFilename;
+ if (tests[test].mHasVideoIndice) {
+ for (size_t i = 0; i < indices.Ref()->Length(); i++) {
+ MP4SampleIndex::Indice data;
+ EXPECT_TRUE(indices.Ref()->GetIndice(i, data))
+ << tests[test].mFilename;
+ EXPECT_TRUE(data.start_offset <= data.end_offset)
+ << tests[test].mFilename;
+ EXPECT_TRUE(data.start_composition <= data.end_composition)
+ << tests[test].mFilename;
+ }
+ }
+ }
+ trackInfo = metadata.GetTrackInfo(TrackInfo::kAudioTrack, 0);
+ if (tests[test].mNumberAudioTracks == 0 ||
+ tests[test].mNumberAudioTracks == E) {
+ EXPECT_TRUE(!trackInfo.Ref()) << tests[test].mFilename;
+ } else {
+ ASSERT_TRUE(!!trackInfo.Ref());
+ const AudioInfo* audioInfo = trackInfo.Ref()->GetAsAudioInfo();
+ ASSERT_TRUE(!!audioInfo);
+ EXPECT_TRUE(audioInfo->IsValid()) << tests[test].mFilename;
+ EXPECT_TRUE(audioInfo->IsAudio()) << tests[test].mFilename;
+ if (std::isinf(tests[test].mAudioDuration)) {
+ ASSERT_TRUE(std::isinf(audioInfo->mDuration.ToSeconds()))
+ << tests[test].mFilename;
+ } else {
+ EXPECT_FLOAT_EQ(tests[test].mAudioDuration,
+ audioInfo->mDuration.ToSeconds())
+ << tests[test].mFilename;
+ }
+ EXPECT_EQ(tests[test].mAudioProfile, audioInfo->mProfile)
+ << tests[test].mFilename;
+
+ MP4Metadata::ResultAndIndice indices =
+ metadata.GetTrackIndice(audioInfo->mTrackId);
+ EXPECT_TRUE(!!indices.Ref()) << tests[test].mFilename;
+ for (size_t i = 0; i < indices.Ref()->Length(); i++) {
+ MP4SampleIndex::Indice data;
+ EXPECT_TRUE(indices.Ref()->GetIndice(i, data)) << tests[test].mFilename;
+ EXPECT_TRUE(data.start_offset <= data.end_offset)
+ << tests[test].mFilename;
+ EXPECT_TRUE(int64_t(data.start_composition) <=
+ int64_t(data.end_composition))
+ << tests[test].mFilename;
+ }
+ }
+ EXPECT_FALSE(metadata.GetTrackInfo(TrackInfo::kTextTrack, 0).Ref())
+ << tests[test].mFilename;
+ // We can see anywhere in any MPEG4.
+ EXPECT_TRUE(metadata.CanSeek()) << tests[test].mFilename;
+ EXPECT_EQ(tests[test].mHasCrypto, metadata.Crypto().Ref()->valid)
+ << tests[test].mFilename;
+ }
+}
+
+// This test was disabled by Bug 1224019 for producing way too much output.
+// This test no longer produces such output, as we've moved away from
+// stagefright, but it does take a long time to run. I can be useful to enable
+// as a sanity check on changes to the parser, but is too taxing to run as part
+// of normal test execution.
+#if 0
+TEST(MP4Metadata, test_case_mp4_subsets) {
+ static const size_t step = 1u;
+ for (size_t test = 0; test < ArrayLength(testFiles); ++test) {
+ nsTArray<uint8_t> buffer = ReadTestFile(testFiles[test].mFilename);
+ ASSERT_FALSE(buffer.IsEmpty());
+ ASSERT_LE(step, buffer.Length());
+ // Just exercizing the parser starting at different points through the file,
+ // making sure it doesn't crash.
+ // No checks because results would differ for each position.
+ for (size_t offset = 0; offset < buffer.Length() - step; offset += step) {
+ size_t size = buffer.Length() - offset;
+ while (size > 0) {
+ RefPtr<TestStream> stream =
+ new TestStream(buffer.Elements() + offset, size);
+
+ MP4Metadata::ResultAndByteBuffer metadataBuffer =
+ MP4Metadata::Metadata(stream);
+ MP4Metadata metadata(stream);
+
+ if (stream->mHighestSuccessfulEndOffset <= 0) {
+ // No successful reads -> Cutting down the size won't change anything.
+ break;
+ }
+ if (stream->mHighestSuccessfulEndOffset < size) {
+ // Read up to a point before the end -> Resize down to that point.
+ size = stream->mHighestSuccessfulEndOffset;
+ } else {
+ // Read up to the end (or after?!) -> Just cut 1 byte.
+ size -= 1;
+ }
+ }
+ }
+ }
+}
+#endif
+
+#if !defined(XP_WIN) || !defined(MOZ_ASAN) // OOMs on Windows ASan
+TEST(MoofParser, test_case_mp4)
+{
+ const TestFileData* tests = nullptr;
+ size_t length = 0;
+
+ tests = testFiles;
+ length = ArrayLength(testFiles);
+
+ for (size_t test = 0; test < length; ++test) {
+ nsTArray<uint8_t> buffer = ReadTestFile(tests[test].mFilename);
+ ASSERT_FALSE(buffer.IsEmpty());
+ RefPtr<ByteStream> stream =
+ new TestStream(buffer.Elements(), buffer.Length());
+
+ MoofParser parser(stream, AsVariant(ParseAllTracks{}), false);
+ EXPECT_EQ(0u, parser.mOffset) << tests[test].mFilename;
+ EXPECT_FALSE(parser.ReachedEnd()) << tests[test].mFilename;
+ EXPECT_TRUE(parser.mInitRange.IsEmpty()) << tests[test].mFilename;
+
+ RefPtr<MediaByteBuffer> metadataBuffer = parser.Metadata();
+ EXPECT_TRUE(metadataBuffer) << tests[test].mFilename;
+
+ EXPECT_FALSE(parser.mInitRange.IsEmpty()) << tests[test].mFilename;
+ const MediaByteRangeSet byteRanges(
+ MediaByteRange(0, int64_t(buffer.Length())));
+ EXPECT_EQ(tests[test].mValidMoof, parser.RebuildFragmentedIndex(byteRanges))
+ << tests[test].mFilename;
+ if (tests[test].mMoofReachedOffset == 0) {
+ EXPECT_EQ(buffer.Length(), parser.mOffset) << tests[test].mFilename;
+ EXPECT_TRUE(parser.ReachedEnd()) << tests[test].mFilename;
+ } else {
+ EXPECT_EQ(tests[test].mMoofReachedOffset, parser.mOffset)
+ << tests[test].mFilename;
+ EXPECT_FALSE(parser.ReachedEnd()) << tests[test].mFilename;
+ }
+
+ EXPECT_FALSE(parser.mInitRange.IsEmpty()) << tests[test].mFilename;
+ EXPECT_TRUE(parser.GetCompositionRange(byteRanges).IsNull())
+ << tests[test].mFilename;
+ EXPECT_TRUE(parser.FirstCompleteMediaSegment().IsEmpty())
+ << tests[test].mFilename;
+ // If we expect a valid moof we should have that moof's range stored.
+ EXPECT_EQ(tests[test].mValidMoof,
+ !parser.FirstCompleteMediaHeader().IsEmpty())
+ << tests[test].mFilename;
+ }
+}
+
+TEST(MoofParser, test_case_sample_description_entries)
+{
+ const TestFileData* tests = testFiles;
+ size_t length = ArrayLength(testFiles);
+
+ for (size_t test = 0; test < length; ++test) {
+ nsTArray<uint8_t> buffer = ReadTestFile(tests[test].mFilename);
+ ASSERT_FALSE(buffer.IsEmpty());
+ RefPtr<ByteStream> stream =
+ new TestStream(buffer.Elements(), buffer.Length());
+
+ // Parse the first track. Treating it as audio is hacky, but this doesn't
+ // affect how we read the sample description entries.
+ uint32_t trackNumber = 1;
+ MoofParser parser(stream, AsVariant(trackNumber), false);
+ EXPECT_EQ(0u, parser.mOffset) << tests[test].mFilename;
+ EXPECT_FALSE(parser.ReachedEnd()) << tests[test].mFilename;
+ EXPECT_TRUE(parser.mInitRange.IsEmpty()) << tests[test].mFilename;
+
+ // Explicitly don't call parser.Metadata() so that the parser itself will
+ // read the metadata as if we're in a fragmented case. Otherwise the parser
+ // won't read the sample description table.
+
+ const MediaByteRangeSet byteRanges(
+ MediaByteRange(0, int64_t(buffer.Length())));
+ EXPECT_EQ(tests[test].mValidMoof, parser.RebuildFragmentedIndex(byteRanges))
+ << tests[test].mFilename;
+
+ // We only care about crypto data from the samples descriptions right now.
+ // This test should be expanded should we read further information.
+ if (tests[test].mHasCrypto) {
+ uint32_t numEncryptedEntries = 0;
+ // It's possible to have multiple sample description entries. Bug
+ // 1714626 tracks more robust handling of multiple entries, for now just
+ // check that we have at least one.
+ for (SampleDescriptionEntry entry : parser.mSampleDescriptions) {
+ if (entry.mIsEncryptedEntry) {
+ numEncryptedEntries++;
+ }
+ }
+ EXPECT_GE(numEncryptedEntries, 1u) << tests[test].mFilename;
+ }
+ }
+}
+#endif // !defined(XP_WIN) || !defined(MOZ_ASAN)
+
+// We should gracefully handle track_id 0 since Bug 1519617. We'd previously
+// used id 0 to trigger special handling in the MoofParser to read multiple
+// track metadata, but since muxers use track id 0 in the wild, we want to
+// make sure they can't accidentally trigger such handling.
+TEST(MoofParser, test_case_track_id_0_does_not_read_multitracks)
+{
+ const char* zeroTrackIdFileName =
+ "test_case_1519617-video-has-track_id-0.mp4";
+ nsTArray<uint8_t> buffer = ReadTestFile(zeroTrackIdFileName);
+
+ ASSERT_FALSE(buffer.IsEmpty());
+ RefPtr<ByteStream> stream =
+ new TestStream(buffer.Elements(), buffer.Length());
+
+ // Parse track id 0. We expect to only get metadata from that track, not the
+ // other track with id 2.
+ const uint32_t videoTrackId = 0;
+ MoofParser parser(stream, AsVariant(videoTrackId), false);
+
+ // Explicitly don't call parser.Metadata() so that the parser itself will
+ // read the metadata as if we're in a fragmented case. Otherwise we won't
+ // read the trak data.
+
+ const MediaByteRangeSet byteRanges(
+ MediaByteRange(0, int64_t(buffer.Length())));
+ EXPECT_TRUE(parser.RebuildFragmentedIndex(byteRanges))
+ << "MoofParser should find a valid moof as the file contains one!";
+
+ // Verify we only have data from track 0, if we parsed multiple tracks we'd
+ // find some of the audio track metadata here. Only check for values that
+ // differ between tracks.
+ const uint32_t videoTimescale = 90000;
+ const uint32_t videoSampleDuration = 3000;
+ const uint32_t videoSampleFlags = 0x10000;
+ const uint32_t videoNumSampleDescriptionEntries = 1;
+ EXPECT_EQ(videoTimescale, parser.mMdhd.mTimescale)
+ << "Wrong timescale for video track! If value is 22050, we've read from "
+ "the audio track!";
+ EXPECT_EQ(videoTrackId, parser.mTrex.mTrackId)
+ << "Wrong track id for video track! If value is 2, we've read from the "
+ "audio track!";
+ EXPECT_EQ(videoSampleDuration, parser.mTrex.mDefaultSampleDuration)
+ << "Wrong sample duration for video track! If value is 1024, we've read "
+ "from the audio track!";
+ EXPECT_EQ(videoSampleFlags, parser.mTrex.mDefaultSampleFlags)
+ << "Wrong sample flags for video track! If value is 0x2000000 (note "
+ "that's hex), we've read from the audio track!";
+ EXPECT_EQ(videoNumSampleDescriptionEntries,
+ parser.mSampleDescriptions.Length())
+ << "Wrong number of sample descriptions for video track! If value is 2, "
+ "then we've read sample description information from video and audio "
+ "tracks!";
+}
+
+// We should gracefully handle track_id 0 since Bug 1519617. This includes
+// handling crypto data from the sinf box in the MoofParser. Note, as of the
+// time of writing, MP4Metadata uses the presence of a pssh box to determine
+// if its crypto member is valid. However, even on files where the pssh isn't
+// in the init segment, the MoofParser should still read the sinf, as in this
+// testcase.
+TEST(MoofParser, test_case_track_id_0_reads_crypto_metadata)
+{
+ const char* zeroTrackIdFileName =
+ "test_case_1519617-cenc-init-with-track_id-0.mp4";
+ nsTArray<uint8_t> buffer = ReadTestFile(zeroTrackIdFileName);
+
+ ASSERT_FALSE(buffer.IsEmpty());
+ RefPtr<ByteStream> stream =
+ new TestStream(buffer.Elements(), buffer.Length());
+
+ // Parse track id 0. We expect to only get metadata from that track, not the
+ // other track with id 2.
+ const uint32_t videoTrackId = 0;
+ MoofParser parser(stream, AsVariant(videoTrackId), false);
+
+ // Explicitly don't call parser.Metadata() so that the parser itself will
+ // read the metadata as if we're in a fragmented case. Otherwise we won't
+ // read the trak data.
+
+ const MediaByteRangeSet byteRanges(
+ MediaByteRange(0, int64_t(buffer.Length())));
+ EXPECT_FALSE(parser.RebuildFragmentedIndex(byteRanges))
+ << "MoofParser should not find a valid moof, this is just an init "
+ "segment!";
+
+ // Verify we only have data from track 0, if we parsed multiple tracks we'd
+ // find some of the audio track metadata here. Only check for values that
+ // differ between tracks.
+ const size_t numSampleDescriptionEntries = 1;
+ const uint32_t defaultPerSampleIVSize = 8;
+ const size_t keyIdLength = 16;
+ const uint32_t defaultKeyId[keyIdLength] = {
+ 0x43, 0xbe, 0x13, 0xd0, 0x26, 0xc9, 0x41, 0x54,
+ 0x8f, 0xed, 0xf9, 0x54, 0x1a, 0xef, 0x6b, 0x0e};
+ EXPECT_TRUE(parser.mSinf.IsValid())
+ << "Should have a sinf that has crypto data!";
+ EXPECT_EQ(defaultPerSampleIVSize, parser.mSinf.mDefaultIVSize)
+ << "Wrong default per sample IV size for track! If 0 indicates we failed "
+ "to parse some crypto info!";
+ for (size_t i = 0; i < keyIdLength; i++) {
+ EXPECT_EQ(defaultKeyId[i], parser.mSinf.mDefaultKeyID[i])
+ << "Mismatched default key ID byte at index " << i
+ << " indicates we failed to parse some crypto info!";
+ }
+ ASSERT_EQ(numSampleDescriptionEntries, parser.mSampleDescriptions.Length())
+ << "Wrong number of sample descriptions for track! If 0, indicates we "
+ "failed to parse some expected crypto!";
+ EXPECT_TRUE(parser.mSampleDescriptions[0].mIsEncryptedEntry)
+ << "Sample description should be marked as encrypted!";
+}
+
+// The MoofParser may be asked to parse metadata for multiple tracks, but then
+// be presented with fragments/moofs that contain data for only a subset of
+// those tracks. I.e. metadata contains information for tracks with ids 1 and 2,
+// but then the moof parser only receives moofs with data for track id 1. We
+// should parse such fragmented media. In this test the metadata contains info
+// for track ids 1 and 2, but track 2's track fragment headers (traf) have been
+// over written with free space boxes (free).
+TEST(MoofParser, test_case_moofs_missing_trafs)
+{
+ const char* noTrafsForTrack2MoofsFileName =
+ "test_case_1519617-track2-trafs-removed.mp4";
+ nsTArray<uint8_t> buffer = ReadTestFile(noTrafsForTrack2MoofsFileName);
+
+ ASSERT_FALSE(buffer.IsEmpty());
+ RefPtr<ByteStream> stream =
+ new TestStream(buffer.Elements(), buffer.Length());
+
+ // Create parser that will read metadata from all tracks.
+ MoofParser parser(stream, AsVariant(ParseAllTracks{}), false);
+
+ // Explicitly don't call parser.Metadata() so that the parser itself will
+ // read the metadata as if we're in a fragmented case. Otherwise we won't
+ // read the trak data.
+
+ const MediaByteRangeSet byteRanges(
+ MediaByteRange(0, int64_t(buffer.Length())));
+ EXPECT_TRUE(parser.RebuildFragmentedIndex(byteRanges))
+ << "MoofParser should find a valid moof, there's 2 in the file!";
+
+ // Verify we've found 2 moofs and that the parser was able to parse them.
+ const size_t numMoofs = 2;
+ EXPECT_EQ(numMoofs, parser.Moofs().Length())
+ << "File has 2 moofs, we should have read both";
+ for (size_t i = 0; i < parser.Moofs().Length(); i++) {
+ EXPECT_TRUE(parser.Moofs()[i].IsValid()) << "All moofs should be valid";
+ }
+}
+
+// This test was disabled by Bug 1224019 for producing way too much output.
+// This test no longer produces such output, as we've moved away from
+// stagefright, but it does take a long time to run. I can be useful to enable
+// as a sanity check on changes to the parser, but is too taxing to run as part
+// of normal test execution.
+#if 0
+TEST(MoofParser, test_case_mp4_subsets) {
+ const size_t step = 1u;
+ for (size_t test = 0; test < ArrayLength(testFiles); ++test) {
+ nsTArray<uint8_t> buffer = ReadTestFile(testFiles[test].mFilename);
+ ASSERT_FALSE(buffer.IsEmpty());
+ ASSERT_LE(step, buffer.Length());
+ // Just exercizing the parser starting at different points through the file,
+ // making sure it doesn't crash.
+ // No checks because results would differ for each position.
+ for (size_t offset = 0; offset < buffer.Length() - step; offset += step) {
+ size_t size = buffer.Length() - offset;
+ while (size > 0) {
+ RefPtr<TestStream> stream =
+ new TestStream(buffer.Elements() + offset, size);
+
+ MoofParser parser(stream, AsVariant(ParseAllTracks{}), false);
+ MediaByteRangeSet byteRanges;
+ EXPECT_FALSE(parser.RebuildFragmentedIndex(byteRanges));
+ parser.GetCompositionRange(byteRanges);
+ RefPtr<MediaByteBuffer> metadataBuffer = parser.Metadata();
+ parser.FirstCompleteMediaSegment();
+ parser.FirstCompleteMediaHeader();
+
+ if (stream->mHighestSuccessfulEndOffset <= 0) {
+ // No successful reads -> Cutting down the size won't change anything.
+ break;
+ }
+ if (stream->mHighestSuccessfulEndOffset < size) {
+ // Read up to a point before the end -> Resize down to that point.
+ size = stream->mHighestSuccessfulEndOffset;
+ } else {
+ // Read up to the end (or after?!) -> Just cut 1 byte.
+ size -= 1;
+ }
+ }
+ }
+ }
+}
+#endif
+
+uint8_t media_gtest_video_init_mp4[] = {
+ 0x00, 0x00, 0x00, 0x18, 0x66, 0x74, 0x79, 0x70, 0x69, 0x73, 0x6f, 0x6d,
+ 0x00, 0x00, 0x00, 0x01, 0x69, 0x73, 0x6f, 0x6d, 0x61, 0x76, 0x63, 0x31,
+ 0x00, 0x00, 0x02, 0xd1, 0x6d, 0x6f, 0x6f, 0x76, 0x00, 0x00, 0x00, 0x6c,
+ 0x6d, 0x76, 0x68, 0x64, 0x00, 0x00, 0x00, 0x00, 0xc8, 0x49, 0x73, 0xf8,
+ 0xc8, 0x4a, 0xc5, 0x7a, 0x00, 0x00, 0x02, 0x58, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x01, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x18,
+ 0x69, 0x6f, 0x64, 0x73, 0x00, 0x00, 0x00, 0x00, 0x10, 0x80, 0x80, 0x80,
+ 0x07, 0x00, 0x4f, 0xff, 0xff, 0x29, 0x15, 0xff, 0x00, 0x00, 0x02, 0x0d,
+ 0x74, 0x72, 0x61, 0x6b, 0x00, 0x00, 0x00, 0x5c, 0x74, 0x6b, 0x68, 0x64,
+ 0x00, 0x00, 0x00, 0x01, 0xc8, 0x49, 0x73, 0xf8, 0xc8, 0x49, 0x73, 0xf9,
+ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x40, 0x00, 0x00, 0x00, 0x02, 0x80, 0x00, 0x00, 0x01, 0x68, 0x00, 0x00,
+ 0x00, 0x00, 0x01, 0xa9, 0x6d, 0x64, 0x69, 0x61, 0x00, 0x00, 0x00, 0x20,
+ 0x6d, 0x64, 0x68, 0x64, 0x00, 0x00, 0x00, 0x00, 0xc8, 0x49, 0x73, 0xf8,
+ 0xc8, 0x49, 0x73, 0xf9, 0x00, 0x00, 0x75, 0x30, 0x00, 0x00, 0x00, 0x00,
+ 0x55, 0xc4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x68, 0x64, 0x6c, 0x72,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x76, 0x69, 0x64, 0x65,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x47, 0x50, 0x41, 0x43, 0x20, 0x49, 0x53, 0x4f, 0x20, 0x56, 0x69, 0x64,
+ 0x65, 0x6f, 0x20, 0x48, 0x61, 0x6e, 0x64, 0x6c, 0x65, 0x72, 0x00, 0x00,
+ 0x00, 0x00, 0x01, 0x49, 0x6d, 0x69, 0x6e, 0x66, 0x00, 0x00, 0x00, 0x14,
+ 0x76, 0x6d, 0x68, 0x64, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x64, 0x69, 0x6e, 0x66,
+ 0x00, 0x00, 0x00, 0x1c, 0x64, 0x72, 0x65, 0x66, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0c, 0x75, 0x72, 0x6c, 0x20,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x09, 0x73, 0x74, 0x62, 0x6c,
+ 0x00, 0x00, 0x00, 0xad, 0x73, 0x74, 0x73, 0x64, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x9d, 0x61, 0x76, 0x63, 0x31,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x02, 0x80, 0x01, 0x68, 0x00, 0x48, 0x00, 0x00, 0x00, 0x48, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x18, 0xff, 0xff, 0x00, 0x00, 0x00, 0x33, 0x61, 0x76,
+ 0x63, 0x43, 0x01, 0x64, 0x00, 0x1f, 0xff, 0xe1, 0x00, 0x1b, 0x67, 0x64,
+ 0x00, 0x1f, 0xac, 0x2c, 0xc5, 0x02, 0x80, 0xbf, 0xe5, 0xc0, 0x44, 0x00,
+ 0x00, 0x03, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0xf2, 0x3c, 0x60, 0xc6,
+ 0x58, 0x01, 0x00, 0x05, 0x68, 0xe9, 0x2b, 0x2c, 0x8b, 0x00, 0x00, 0x00,
+ 0x14, 0x62, 0x74, 0x72, 0x74, 0x00, 0x01, 0x5a, 0xc2, 0x00, 0x24, 0x74,
+ 0x38, 0x00, 0x09, 0x22, 0x00, 0x00, 0x00, 0x00, 0x10, 0x73, 0x74, 0x74,
+ 0x73, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x10, 0x63, 0x74, 0x74, 0x73, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x10, 0x73, 0x74, 0x73, 0x63, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x14, 0x73, 0x74, 0x73,
+ 0x7a, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x10, 0x73, 0x74, 0x63, 0x6f, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x38, 0x6d, 0x76, 0x65,
+ 0x78, 0x00, 0x00, 0x00, 0x10, 0x6d, 0x65, 0x68, 0x64, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x05, 0x76, 0x18, 0x00, 0x00, 0x00, 0x20, 0x74, 0x72, 0x65,
+ 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
+ 0x01, 0x00, 0x00, 0x03, 0xe8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00,
+ 0x00};
+
+const uint32_t media_gtest_video_init_mp4_len = 745;
+
+TEST(MP4Metadata, EmptyCTTS)
+{
+ RefPtr<MediaByteBuffer> buffer =
+ new MediaByteBuffer(media_gtest_video_init_mp4_len);
+ buffer->AppendElements(media_gtest_video_init_mp4,
+ media_gtest_video_init_mp4_len);
+ RefPtr<BufferStream> stream = new BufferStream(buffer);
+
+ MP4Metadata::ResultAndByteBuffer metadataBuffer =
+ MP4Metadata::Metadata(stream);
+ EXPECT_EQ(NS_OK, metadataBuffer.Result());
+ EXPECT_TRUE(metadataBuffer.Ref());
+
+ MP4Metadata metadata(stream);
+ EXPECT_EQ(metadata.Parse(), NS_OK);
+ EXPECT_EQ(1u, metadata.GetNumberTracks(TrackInfo::kVideoTrack).Ref());
+ MP4Metadata::ResultAndTrackInfo track =
+ metadata.GetTrackInfo(TrackInfo::kVideoTrack, 0);
+ EXPECT_TRUE(track.Ref() != nullptr);
+ // We can seek anywhere in any MPEG4.
+ EXPECT_TRUE(metadata.CanSeek());
+ EXPECT_FALSE(metadata.Crypto().Ref()->valid);
+}
+
+// Fixture so we test telemetry probes.
+class MP4MetadataTelemetryFixture : public TelemetryTestFixture {};
+
+TEST_F(MP4MetadataTelemetryFixture, Telemetry) {
+ // Helper to fetch the metadata from a file and send telemetry in the process.
+ auto UpdateMetadataAndHistograms = [](const char* testFileName) {
+ nsTArray<uint8_t> buffer = ReadTestFile(testFileName);
+ ASSERT_FALSE(buffer.IsEmpty());
+ RefPtr<ByteStream> stream =
+ new TestStream(buffer.Elements(), buffer.Length());
+
+ MP4Metadata::ResultAndByteBuffer metadataBuffer =
+ MP4Metadata::Metadata(stream);
+ EXPECT_EQ(NS_OK, metadataBuffer.Result());
+ EXPECT_TRUE(metadataBuffer.Ref());
+
+ MP4Metadata metadata(stream);
+ nsresult res = metadata.Parse();
+ EXPECT_NS_SUCCEEDED(res);
+ auto audioTrackCount = metadata.GetNumberTracks(TrackInfo::kAudioTrack);
+ ASSERT_NE(audioTrackCount.Ref(), MP4Metadata::NumberTracksError());
+ auto videoTrackCount = metadata.GetNumberTracks(TrackInfo::kVideoTrack);
+ ASSERT_NE(videoTrackCount.Ref(), MP4Metadata::NumberTracksError());
+
+ // Need to read the track data to get telemetry to fire.
+ for (uint32_t i = 0; i < audioTrackCount.Ref(); i++) {
+ metadata.GetTrackInfo(TrackInfo::kAudioTrack, i);
+ }
+ for (uint32_t i = 0; i < videoTrackCount.Ref(); i++) {
+ metadata.GetTrackInfo(TrackInfo::kVideoTrack, i);
+ }
+ };
+
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ // Checks the current state of the histograms relating to sample description
+ // entries and verifies they're in an expected state.
+ // aExpectedMultipleCodecCounts is a tuple where the first value represents
+ // the number of expected 'false' count, and the second the expected 'true'
+ // count for the sample description entries have multiple codecs histogram.
+ // aExpectedMultipleCryptoCounts is the same, but for the sample description
+ // entires have multiple crypto histogram.
+ // aExpectedSampleDescriptionEntryCounts is a tuple with 6 values, each is
+ // the expected number of sample description seen. I.e, the first value in the
+ // tuple is the number of tracks we've seen with 0 sample descriptions, the
+ // second value with 1 sample description, and so on up to 5 sample
+ // descriptions. aFileName is the name of the most recent file we've parsed,
+ // and is used to log if our telem counts are not in an expected state.
+ auto CheckHistograms =
+ [this, &cx](
+ const std::tuple<uint32_t, uint32_t>& aExpectedMultipleCodecCounts,
+ const std::tuple<uint32_t, uint32_t>& aExpectedMultipleCryptoCounts,
+ const std::tuple<uint32_t, uint32_t, uint32_t, uint32_t, uint32_t,
+ uint32_t>& aExpectedSampleDescriptionEntryCounts,
+ const char* aFileName) {
+ // Get a snapshot of the current histograms
+ JS::Rooted<JS::Value> snapshot(cx.GetJSContext());
+ TelemetryTestHelpers::GetSnapshots(cx.GetJSContext(), mTelemetry,
+ "" /* this string is unused */,
+ &snapshot, false /* is_keyed */);
+
+ // We'll use these to pull values out of the histograms.
+ JS::Rooted<JS::Value> values(cx.GetJSContext());
+ JS::Rooted<JS::Value> value(cx.GetJSContext());
+
+ // Verify our multiple codecs count histogram.
+ JS::Rooted<JS::Value> multipleCodecsHistogram(cx.GetJSContext());
+ TelemetryTestHelpers::GetProperty(
+ cx.GetJSContext(),
+ "MEDIA_MP4_PARSE_SAMPLE_DESCRIPTION_ENTRIES_HAVE_MULTIPLE_CODECS",
+ snapshot, &multipleCodecsHistogram);
+ ASSERT_TRUE(multipleCodecsHistogram.isObject())
+ << "Multiple codecs histogram should exist!";
+
+ TelemetryTestHelpers::GetProperty(cx.GetJSContext(), "values",
+ multipleCodecsHistogram, &values);
+ // False count.
+ TelemetryTestHelpers::GetElement(cx.GetJSContext(), 0, values, &value);
+ uint32_t uValue = 0;
+ JS::ToUint32(cx.GetJSContext(), value, &uValue);
+ EXPECT_EQ(std::get<0>(aExpectedMultipleCodecCounts), uValue)
+ << "Unexpected number of false multiple codecs after parsing "
+ << aFileName;
+ // True count.
+ TelemetryTestHelpers::GetElement(cx.GetJSContext(), 1, values, &value);
+ JS::ToUint32(cx.GetJSContext(), value, &uValue);
+ EXPECT_EQ(std::get<1>(aExpectedMultipleCodecCounts), uValue)
+ << "Unexpected number of true multiple codecs after parsing "
+ << aFileName;
+
+ // Verify our multiple crypto count histogram.
+ JS::Rooted<JS::Value> multipleCryptoHistogram(cx.GetJSContext());
+ TelemetryTestHelpers::GetProperty(
+ cx.GetJSContext(),
+ "MEDIA_MP4_PARSE_SAMPLE_DESCRIPTION_ENTRIES_HAVE_MULTIPLE_CRYPTO",
+ snapshot, &multipleCryptoHistogram);
+ ASSERT_TRUE(multipleCryptoHistogram.isObject())
+ << "Multiple crypto histogram should exist!";
+
+ TelemetryTestHelpers::GetProperty(cx.GetJSContext(), "values",
+ multipleCryptoHistogram, &values);
+ // False count.
+ TelemetryTestHelpers::GetElement(cx.GetJSContext(), 0, values, &value);
+ JS::ToUint32(cx.GetJSContext(), value, &uValue);
+ EXPECT_EQ(std::get<0>(aExpectedMultipleCryptoCounts), uValue)
+ << "Unexpected number of false multiple cryptos after parsing "
+ << aFileName;
+ // True count.
+ TelemetryTestHelpers::GetElement(cx.GetJSContext(), 1, values, &value);
+ JS::ToUint32(cx.GetJSContext(), value, &uValue);
+ EXPECT_EQ(std::get<1>(aExpectedMultipleCryptoCounts), uValue)
+ << "Unexpected number of true multiple cryptos after parsing "
+ << aFileName;
+
+ // Verify our sample description entry count histogram.
+ JS::Rooted<JS::Value> numSamplesHistogram(cx.GetJSContext());
+ TelemetryTestHelpers::GetProperty(
+ cx.GetJSContext(), "MEDIA_MP4_PARSE_NUM_SAMPLE_DESCRIPTION_ENTRIES",
+ snapshot, &numSamplesHistogram);
+ ASSERT_TRUE(numSamplesHistogram.isObject())
+ << "Num sample description entries histogram should exist!";
+
+ TelemetryTestHelpers::GetProperty(cx.GetJSContext(), "values",
+ numSamplesHistogram, &values);
+
+ TelemetryTestHelpers::GetElement(cx.GetJSContext(), 0, values, &value);
+ JS::ToUint32(cx.GetJSContext(), value, &uValue);
+ EXPECT_EQ(std::get<0>(aExpectedSampleDescriptionEntryCounts), uValue)
+ << "Unexpected number of 0 sample entry descriptions after parsing "
+ << aFileName;
+ TelemetryTestHelpers::GetElement(cx.GetJSContext(), 1, values, &value);
+ JS::ToUint32(cx.GetJSContext(), value, &uValue);
+ EXPECT_EQ(std::get<1>(aExpectedSampleDescriptionEntryCounts), uValue)
+ << "Unexpected number of 1 sample entry descriptions after parsing "
+ << aFileName;
+ TelemetryTestHelpers::GetElement(cx.GetJSContext(), 2, values, &value);
+ JS::ToUint32(cx.GetJSContext(), value, &uValue);
+ EXPECT_EQ(std::get<2>(aExpectedSampleDescriptionEntryCounts), uValue)
+ << "Unexpected number of 2 sample entry descriptions after parsing "
+ << aFileName;
+ TelemetryTestHelpers::GetElement(cx.GetJSContext(), 3, values, &value);
+ JS::ToUint32(cx.GetJSContext(), value, &uValue);
+ EXPECT_EQ(std::get<3>(aExpectedSampleDescriptionEntryCounts), uValue)
+ << "Unexpected number of 3 sample entry descriptions after parsing "
+ << aFileName;
+ TelemetryTestHelpers::GetElement(cx.GetJSContext(), 4, values, &value);
+ JS::ToUint32(cx.GetJSContext(), value, &uValue);
+ EXPECT_EQ(std::get<4>(aExpectedSampleDescriptionEntryCounts), uValue)
+ << "Unexpected number of 4 sample entry descriptions after parsing "
+ << aFileName;
+ TelemetryTestHelpers::GetElement(cx.GetJSContext(), 5, values, &value);
+ JS::ToUint32(cx.GetJSContext(), value, &uValue);
+ EXPECT_EQ(std::get<5>(aExpectedSampleDescriptionEntryCounts), uValue)
+ << "Unexpected number of 5 sample entry descriptions after parsing "
+ << aFileName;
+ };
+
+ // Clear histograms
+ TelemetryTestHelpers::GetAndClearHistogram(
+ cx.GetJSContext(), mTelemetry,
+ nsLiteralCString(
+ "MEDIA_MP4_PARSE_SAMPLE_DESCRIPTION_ENTRIES_HAVE_MULTIPLE_CODECS"),
+ false /* is_keyed */);
+
+ TelemetryTestHelpers::GetAndClearHistogram(
+ cx.GetJSContext(), mTelemetry,
+ nsLiteralCString(
+ "MEDIA_MP4_PARSE_SAMPLE_DESCRIPTION_ENTRIES_HAVE_MULTIPLE_CRYPTO"),
+ false /* is_keyed */);
+
+ TelemetryTestHelpers::GetAndClearHistogram(
+ cx.GetJSContext(), mTelemetry,
+ "MEDIA_MP4_PARSE_NUM_SAMPLE_DESCRIPTION_ENTRIES"_ns,
+ false /* is_keyed */);
+
+ // The snapshot won't have any data in it until we populate our histograms, so
+ // we don't check for a baseline here. Just read out first MP4 metadata.
+
+ // Grab one of the test cases we know should parse and parse it, this should
+ // trigger telemetry gathering.
+
+ // This file contains 2 moovs, each with a video and audio track with one
+ // sample description entry. So we should see 4 tracks, each with a single
+ // codec, no crypto, and a single sample description entry.
+ UpdateMetadataAndHistograms("test_case_1185230.mp4");
+
+ // Verify our histograms are updated.
+ CheckHistograms(std::make_tuple<uint32_t, uint32_t>(4, 0),
+ std::make_tuple<uint32_t, uint32_t>(4, 0),
+ std::make_tuple<uint32_t, uint32_t, uint32_t, uint32_t,
+ uint32_t, uint32_t>(0, 4, 0, 0, 0, 0),
+ "test_case_1185230.mp4");
+
+ // Parse another test case. This one has a single moov with a single video
+ // track. However, the track has two sample description entries, and our
+ // updated telemetry should reflect that.
+ UpdateMetadataAndHistograms(
+ "test_case_1513651-2-sample-description-entries.mp4");
+
+ // Verify our histograms are updated.
+ CheckHistograms(std::make_tuple<uint32_t, uint32_t>(5, 0),
+ std::make_tuple<uint32_t, uint32_t>(5, 0),
+ std::make_tuple<uint32_t, uint32_t, uint32_t, uint32_t,
+ uint32_t, uint32_t>(0, 4, 1, 0, 0, 0),
+ "test_case_1513651-2-sample-description-entries.mp4");
+
+ // Parse another test case. This one has 2 sample decription entries, both
+ // with crypto information, which should be reflected in our telemetry.
+ UpdateMetadataAndHistograms(
+ "test_case_1714125-2-sample-description-entires-with-identical-crypto."
+ "mp4");
+
+ // Verify our histograms are updated.
+ CheckHistograms(
+ std::make_tuple<uint32_t, uint32_t>(6, 0),
+ std::make_tuple<uint32_t, uint32_t>(5, 1),
+ std::make_tuple<uint32_t, uint32_t, uint32_t, uint32_t, uint32_t,
+ uint32_t>(0, 4, 2, 0, 0, 0),
+ "test_case_1714125-2-sample-description-entires-with-identical-crypto."
+ "mp4");
+}
diff --git a/dom/media/gtest/mp4_demuxer/moz.build b/dom/media/gtest/mp4_demuxer/moz.build
new file mode 100644
index 0000000000..dc0946b7a0
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/moz.build
@@ -0,0 +1,66 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Library("mp4_demuxer_gtest")
+
+if CONFIG["OS_TARGET"] != "Android":
+ SOURCES += [
+ "TestParser.cpp",
+ ]
+
+SOURCES += [
+ "TestInterval.cpp",
+]
+
+TEST_HARNESS_FILES.gtest += [
+ "test_case_1156505.mp4",
+ "test_case_1181213.mp4",
+ "test_case_1181215.mp4",
+ "test_case_1181223.mp4",
+ "test_case_1181719.mp4",
+ "test_case_1185230.mp4",
+ "test_case_1187067.mp4",
+ "test_case_1200326.mp4",
+ "test_case_1204580.mp4",
+ "test_case_1216748.mp4",
+ "test_case_1296473.mp4",
+ "test_case_1296532.mp4",
+ "test_case_1301065-harder.mp4",
+ "test_case_1301065-i64max.mp4",
+ "test_case_1301065-i64min.mp4",
+ "test_case_1301065-max-ez.mp4",
+ "test_case_1301065-max-ok.mp4",
+ "test_case_1301065-overfl.mp4",
+ "test_case_1301065-u32max.mp4",
+ "test_case_1301065-u64max.mp4",
+ "test_case_1301065.mp4",
+ "test_case_1329061.mov",
+ "test_case_1351094.mp4",
+ "test_case_1388991.mp4",
+ "test_case_1389299.mp4",
+ "test_case_1389527.mp4",
+ "test_case_1395244.mp4",
+ "test_case_1410565.mp4",
+ "test_case_1513651-2-sample-description-entries.mp4",
+ "test_case_1519617-cenc-init-with-track_id-0.mp4",
+ "test_case_1519617-track2-trafs-removed.mp4",
+ "test_case_1519617-video-has-track_id-0.mp4",
+ "test_case_1714125-2-sample-description-entires-with-identical-crypto.mp4",
+]
+
+UNIFIED_SOURCES += [
+ "TestMP4.cpp",
+]
+
+TEST_HARNESS_FILES.gtest += [
+ "../../test/street.mp4",
+]
+LOCAL_INCLUDES += [
+ "../../mp4",
+ "/toolkit/components/telemetry/tests/gtest",
+]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1156505.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1156505.mp4
new file mode 100644
index 0000000000..687b06ee1f
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1156505.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1181213.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1181213.mp4
new file mode 100644
index 0000000000..e2326edb4e
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1181213.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1181215.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1181215.mp4
new file mode 100644
index 0000000000..7adba3836f
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1181215.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1181223.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1181223.mp4
new file mode 100644
index 0000000000..2aa2d5abfd
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1181223.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1181719.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1181719.mp4
new file mode 100644
index 0000000000..6846edd6ed
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1181719.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1185230.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1185230.mp4
new file mode 100644
index 0000000000..ac5cbdbe85
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1185230.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1187067.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1187067.mp4
new file mode 100644
index 0000000000..fdb396eeb3
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1187067.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1200326.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1200326.mp4
new file mode 100644
index 0000000000..5b8b27d508
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1200326.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1204580.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1204580.mp4
new file mode 100644
index 0000000000..4e55b05719
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1204580.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1216748.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1216748.mp4
new file mode 100644
index 0000000000..7072f53bec
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1216748.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1296473.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1296473.mp4
new file mode 100644
index 0000000000..109eb51064
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1296473.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1296532.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1296532.mp4
new file mode 100644
index 0000000000..5a5669bb89
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1296532.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1301065-harder.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1301065-harder.mp4
new file mode 100644
index 0000000000..7d678b7c66
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1301065-harder.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1301065-i64max.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1301065-i64max.mp4
new file mode 100644
index 0000000000..5a3572f88c
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1301065-i64max.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1301065-i64min.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1301065-i64min.mp4
new file mode 100644
index 0000000000..4d3eb366e1
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1301065-i64min.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1301065-max-ez.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1301065-max-ez.mp4
new file mode 100644
index 0000000000..17fbf411ed
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1301065-max-ez.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1301065-max-ok.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1301065-max-ok.mp4
new file mode 100644
index 0000000000..a5e1e4610d
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1301065-max-ok.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1301065-overfl.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1301065-overfl.mp4
new file mode 100644
index 0000000000..1ef24e932b
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1301065-overfl.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1301065-u32max.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1301065-u32max.mp4
new file mode 100644
index 0000000000..b1d8b6ce7e
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1301065-u32max.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1301065-u64max.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1301065-u64max.mp4
new file mode 100644
index 0000000000..419dcba2c1
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1301065-u64max.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1301065.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1301065.mp4
new file mode 100644
index 0000000000..543a4fba3e
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1301065.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1329061.mov b/dom/media/gtest/mp4_demuxer/test_case_1329061.mov
new file mode 100644
index 0000000000..4246b8f716
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1329061.mov
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1351094.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1351094.mp4
new file mode 100644
index 0000000000..2dfd4c35ce
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1351094.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1388991.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1388991.mp4
new file mode 100644
index 0000000000..deb7aae33a
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1388991.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1389299.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1389299.mp4
new file mode 100644
index 0000000000..78dc390a3d
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1389299.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1389527.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1389527.mp4
new file mode 100644
index 0000000000..6406fcb8f8
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1389527.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1395244.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1395244.mp4
new file mode 100644
index 0000000000..da43d017ed
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1395244.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1410565.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1410565.mp4
new file mode 100644
index 0000000000..ebeaa08354
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1410565.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1513651-2-sample-description-entries.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1513651-2-sample-description-entries.mp4
new file mode 100644
index 0000000000..2f8f235a9b
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1513651-2-sample-description-entries.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1519617-cenc-init-with-track_id-0.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1519617-cenc-init-with-track_id-0.mp4
new file mode 100644
index 0000000000..e76e9f0894
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1519617-cenc-init-with-track_id-0.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1519617-track2-trafs-removed.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1519617-track2-trafs-removed.mp4
new file mode 100644
index 0000000000..55bd57c7db
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1519617-track2-trafs-removed.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1519617-video-has-track_id-0.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1519617-video-has-track_id-0.mp4
new file mode 100644
index 0000000000..8cb4dcc212
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1519617-video-has-track_id-0.mp4
Binary files differ
diff --git a/dom/media/gtest/mp4_demuxer/test_case_1714125-2-sample-description-entires-with-identical-crypto.mp4 b/dom/media/gtest/mp4_demuxer/test_case_1714125-2-sample-description-entires-with-identical-crypto.mp4
new file mode 100644
index 0000000000..4356259e68
--- /dev/null
+++ b/dom/media/gtest/mp4_demuxer/test_case_1714125-2-sample-description-entires-with-identical-crypto.mp4
Binary files differ
diff --git a/dom/media/gtest/negative_duration.mp4 b/dom/media/gtest/negative_duration.mp4
new file mode 100644
index 0000000000..de86bf497c
--- /dev/null
+++ b/dom/media/gtest/negative_duration.mp4
Binary files differ
diff --git a/dom/media/gtest/noise.mp3 b/dom/media/gtest/noise.mp3
new file mode 100644
index 0000000000..e76b503502
--- /dev/null
+++ b/dom/media/gtest/noise.mp3
Binary files differ
diff --git a/dom/media/gtest/noise_vbr.mp3 b/dom/media/gtest/noise_vbr.mp3
new file mode 100644
index 0000000000..284ebe40bf
--- /dev/null
+++ b/dom/media/gtest/noise_vbr.mp3
Binary files differ
diff --git a/dom/media/gtest/short-zero-in-moov.mp4 b/dom/media/gtest/short-zero-in-moov.mp4
new file mode 100644
index 0000000000..577318c8fa
--- /dev/null
+++ b/dom/media/gtest/short-zero-in-moov.mp4
Binary files differ
diff --git a/dom/media/gtest/short-zero-inband.mov b/dom/media/gtest/short-zero-inband.mov
new file mode 100644
index 0000000000..9c18642865
--- /dev/null
+++ b/dom/media/gtest/short-zero-inband.mov
Binary files differ
diff --git a/dom/media/gtest/small-shot-false-positive.mp3 b/dom/media/gtest/small-shot-false-positive.mp3
new file mode 100644
index 0000000000..2f1e794051
--- /dev/null
+++ b/dom/media/gtest/small-shot-false-positive.mp3
Binary files differ
diff --git a/dom/media/gtest/small-shot-partial-xing.mp3 b/dom/media/gtest/small-shot-partial-xing.mp3
new file mode 100644
index 0000000000..99d68e3cbe
--- /dev/null
+++ b/dom/media/gtest/small-shot-partial-xing.mp3
Binary files differ
diff --git a/dom/media/gtest/small-shot.mp3 b/dom/media/gtest/small-shot.mp3
new file mode 100644
index 0000000000..f9397a5106
--- /dev/null
+++ b/dom/media/gtest/small-shot.mp3
Binary files differ
diff --git a/dom/media/gtest/test.webm b/dom/media/gtest/test.webm
new file mode 100644
index 0000000000..fc9e991270
--- /dev/null
+++ b/dom/media/gtest/test.webm
Binary files differ
diff --git a/dom/media/gtest/test_InvalidElementId.webm b/dom/media/gtest/test_InvalidElementId.webm
new file mode 100644
index 0000000000..74e24d2093
--- /dev/null
+++ b/dom/media/gtest/test_InvalidElementId.webm
Binary files differ
diff --git a/dom/media/gtest/test_InvalidElementSize.webm b/dom/media/gtest/test_InvalidElementSize.webm
new file mode 100644
index 0000000000..420a1452ce
--- /dev/null
+++ b/dom/media/gtest/test_InvalidElementSize.webm
Binary files differ
diff --git a/dom/media/gtest/test_InvalidLargeEBMLMaxIdLength.webm b/dom/media/gtest/test_InvalidLargeEBMLMaxIdLength.webm
new file mode 100644
index 0000000000..fc2d9ce88e
--- /dev/null
+++ b/dom/media/gtest/test_InvalidLargeEBMLMaxIdLength.webm
Binary files differ
diff --git a/dom/media/gtest/test_InvalidLargeElementId.webm b/dom/media/gtest/test_InvalidLargeElementId.webm
new file mode 100644
index 0000000000..ceac160d9d
--- /dev/null
+++ b/dom/media/gtest/test_InvalidLargeElementId.webm
Binary files differ
diff --git a/dom/media/gtest/test_InvalidSmallEBMLMaxIdLength.webm b/dom/media/gtest/test_InvalidSmallEBMLMaxIdLength.webm
new file mode 100644
index 0000000000..ca38a258c8
--- /dev/null
+++ b/dom/media/gtest/test_InvalidSmallEBMLMaxIdLength.webm
Binary files differ
diff --git a/dom/media/gtest/test_ValidLargeEBMLMaxIdLength.webm b/dom/media/gtest/test_ValidLargeEBMLMaxIdLength.webm
new file mode 100644
index 0000000000..44bca6101e
--- /dev/null
+++ b/dom/media/gtest/test_ValidLargeEBMLMaxIdLength.webm
Binary files differ
diff --git a/dom/media/gtest/test_ValidSmallEBMLMaxSizeLength.webm b/dom/media/gtest/test_ValidSmallEBMLMaxSizeLength.webm
new file mode 100644
index 0000000000..23fd2b36a6
--- /dev/null
+++ b/dom/media/gtest/test_ValidSmallEBMLMaxSizeLength.webm
Binary files differ
diff --git a/dom/media/gtest/test_case_1224361.vp8.ivf b/dom/media/gtest/test_case_1224361.vp8.ivf
new file mode 100644
index 0000000000..e2fe942f0e
--- /dev/null
+++ b/dom/media/gtest/test_case_1224361.vp8.ivf
Binary files differ
diff --git a/dom/media/gtest/test_case_1224363.vp8.ivf b/dom/media/gtest/test_case_1224363.vp8.ivf
new file mode 100644
index 0000000000..6d2e4e0206
--- /dev/null
+++ b/dom/media/gtest/test_case_1224363.vp8.ivf
Binary files differ
diff --git a/dom/media/gtest/test_case_1224369.vp8.ivf b/dom/media/gtest/test_case_1224369.vp8.ivf
new file mode 100644
index 0000000000..2f8deb1148
--- /dev/null
+++ b/dom/media/gtest/test_case_1224369.vp8.ivf
Binary files differ
diff --git a/dom/media/gtest/test_vbri.mp3 b/dom/media/gtest/test_vbri.mp3
new file mode 100644
index 0000000000..efd7450338
--- /dev/null
+++ b/dom/media/gtest/test_vbri.mp3
Binary files differ
diff --git a/dom/media/hls/HLSDecoder.cpp b/dom/media/hls/HLSDecoder.cpp
new file mode 100644
index 0000000000..a992f52f71
--- /dev/null
+++ b/dom/media/hls/HLSDecoder.cpp
@@ -0,0 +1,312 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HLSDecoder.h"
+#include "AndroidBridge.h"
+#include "base/process_util.h"
+#include "DecoderTraits.h"
+#include "HLSDemuxer.h"
+#include "HLSUtils.h"
+#include "JavaBuiltins.h"
+#include "MediaContainerType.h"
+#include "MediaDecoderStateMachine.h"
+#include "MediaFormatReader.h"
+#include "MediaShutdownManager.h"
+#include "mozilla/java/GeckoHLSResourceWrapperNatives.h"
+#include "nsContentUtils.h"
+#include "nsIChannel.h"
+#include "nsNetUtil.h"
+#include "nsThreadUtils.h"
+#include "mozilla/dom/HTMLMediaElement.h"
+#include "mozilla/NullPrincipal.h"
+#include "mozilla/StaticPrefs_media.h"
+
+namespace mozilla {
+
+class HLSResourceCallbacksSupport
+ : public java::GeckoHLSResourceWrapper::Callbacks::Natives<
+ HLSResourceCallbacksSupport> {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(HLSResourceCallbacksSupport)
+ public:
+ typedef java::GeckoHLSResourceWrapper::Callbacks::Natives<
+ HLSResourceCallbacksSupport>
+ NativeCallbacks;
+ using NativeCallbacks::AttachNative;
+ using NativeCallbacks::DisposeNative;
+
+ explicit HLSResourceCallbacksSupport(HLSDecoder* aResource);
+ void Detach();
+ void OnLoad(jni::String::Param aUrl);
+ void OnDataArrived();
+ void OnError(int aErrorCode);
+
+ private:
+ ~HLSResourceCallbacksSupport() {}
+ Mutex mMutex MOZ_UNANNOTATED;
+ HLSDecoder* mDecoder;
+};
+
+HLSResourceCallbacksSupport::HLSResourceCallbacksSupport(HLSDecoder* aDecoder)
+ : mMutex("HLSResourceCallbacksSupport"), mDecoder(aDecoder) {
+ MOZ_ASSERT(mDecoder);
+}
+
+void HLSResourceCallbacksSupport::Detach() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MutexAutoLock lock(mMutex);
+ mDecoder = nullptr;
+}
+
+void HLSResourceCallbacksSupport::OnLoad(jni::String::Param aUrl) {
+ MutexAutoLock lock(mMutex);
+ if (!mDecoder) {
+ return;
+ }
+ RefPtr<HLSResourceCallbacksSupport> self = this;
+ jni::String::GlobalRef url = std::move(aUrl);
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "HLSResourceCallbacksSupport::OnLoad", [self, url]() -> void {
+ if (self->mDecoder) {
+ self->mDecoder->NotifyLoad(url->ToCString());
+ }
+ }));
+}
+
+void HLSResourceCallbacksSupport::OnDataArrived() {
+ HLS_DEBUG("HLSResourceCallbacksSupport", "OnDataArrived.");
+ MutexAutoLock lock(mMutex);
+ if (!mDecoder) {
+ return;
+ }
+ RefPtr<HLSResourceCallbacksSupport> self = this;
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "HLSResourceCallbacksSupport::OnDataArrived", [self]() -> void {
+ if (self->mDecoder) {
+ self->mDecoder->NotifyDataArrived();
+ }
+ }));
+}
+
+void HLSResourceCallbacksSupport::OnError(int aErrorCode) {
+ HLS_DEBUG("HLSResourceCallbacksSupport", "onError(%d)", aErrorCode);
+ MutexAutoLock lock(mMutex);
+ if (!mDecoder) {
+ return;
+ }
+ RefPtr<HLSResourceCallbacksSupport> self = this;
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "HLSResourceCallbacksSupport::OnError", [self]() -> void {
+ if (self->mDecoder) {
+ // Since HLS source should be from the Internet, we treat all resource
+ // errors from GeckoHlsPlayer as network errors.
+ self->mDecoder->NetworkError(
+ MediaResult(NS_ERROR_FAILURE, "HLS error"));
+ }
+ }));
+}
+
+size_t HLSDecoder::sAllocatedInstances = 0;
+
+// static
+RefPtr<HLSDecoder> HLSDecoder::Create(MediaDecoderInit& aInit) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ return sAllocatedInstances < StaticPrefs::media_hls_max_allocations()
+ ? new HLSDecoder(aInit)
+ : nullptr;
+}
+
+HLSDecoder::HLSDecoder(MediaDecoderInit& aInit) : MediaDecoder(aInit) {
+ MOZ_ASSERT(NS_IsMainThread());
+ sAllocatedInstances++;
+ HLS_DEBUG("HLSDecoder", "HLSDecoder(): allocated=%zu", sAllocatedInstances);
+}
+
+HLSDecoder::~HLSDecoder() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(sAllocatedInstances > 0);
+ sAllocatedInstances--;
+ HLS_DEBUG("HLSDecoder", "~HLSDecoder(): allocated=%zu", sAllocatedInstances);
+}
+
+MediaDecoderStateMachineBase* HLSDecoder::CreateStateMachine(
+ bool aDisableExternalEngine) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ MediaFormatReaderInit init;
+ init.mVideoFrameContainer = GetVideoFrameContainer();
+ init.mKnowsCompositor = GetCompositor();
+ init.mCrashHelper = GetOwner()->CreateGMPCrashHelper();
+ init.mFrameStats = mFrameStats;
+ init.mMediaDecoderOwnerID = mOwner;
+ static Atomic<uint32_t> sTrackingIdCounter(0);
+ init.mTrackingId =
+ Some(TrackingId(TrackingId::Source::HLSDecoder, sTrackingIdCounter++,
+ TrackingId::TrackAcrossProcesses::Yes));
+ mReader = new MediaFormatReader(
+ init, new HLSDemuxer(mHLSResourceWrapper->GetPlayerId()));
+
+ return new MediaDecoderStateMachine(this, mReader);
+}
+
+bool HLSDecoder::IsEnabled() {
+ return StaticPrefs::media_hls_enabled() && (jni::GetAPIVersion() >= 16);
+}
+
+bool HLSDecoder::IsSupportedType(const MediaContainerType& aContainerType) {
+ return IsEnabled() && DecoderTraits::IsHttpLiveStreamingType(aContainerType);
+}
+
+nsresult HLSDecoder::Load(nsIChannel* aChannel) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsresult rv = NS_GetFinalChannelURI(aChannel, getter_AddRefs(mURI));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ mChannel = aChannel;
+ nsCString spec;
+ Unused << mURI->GetSpec(spec);
+ ;
+ HLSResourceCallbacksSupport::Init();
+ mJavaCallbacks = java::GeckoHLSResourceWrapper::Callbacks::New();
+ mCallbackSupport = new HLSResourceCallbacksSupport(this);
+ HLSResourceCallbacksSupport::AttachNative(mJavaCallbacks, mCallbackSupport);
+ mHLSResourceWrapper = java::GeckoHLSResourceWrapper::Create(
+ NS_ConvertUTF8toUTF16(spec), mJavaCallbacks);
+ MOZ_ASSERT(mHLSResourceWrapper);
+
+ rv = MediaShutdownManager::Instance().Register(this);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ return CreateAndInitStateMachine(false);
+}
+
+void HLSDecoder::AddSizeOfResources(ResourceSizes* aSizes) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // TODO: track JAVA wrappers.
+}
+
+already_AddRefed<nsIPrincipal> HLSDecoder::GetCurrentPrincipal() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return do_AddRef(mContentPrincipal);
+}
+
+bool HLSDecoder::HadCrossOriginRedirects() {
+ MOZ_ASSERT(NS_IsMainThread());
+ // Bug 1478843
+ return false;
+}
+
+void HLSDecoder::Play() {
+ MOZ_ASSERT(NS_IsMainThread());
+ HLS_DEBUG("HLSDecoder", "MediaElement called Play");
+ mHLSResourceWrapper->Play();
+ return MediaDecoder::Play();
+}
+
+void HLSDecoder::Pause() {
+ MOZ_ASSERT(NS_IsMainThread());
+ HLS_DEBUG("HLSDecoder", "MediaElement called Pause");
+ mHLSResourceWrapper->Pause();
+ return MediaDecoder::Pause();
+}
+
+void HLSDecoder::Suspend() {
+ MOZ_ASSERT(NS_IsMainThread());
+ HLS_DEBUG("HLSDecoder", "Should suspend the resource fetching.");
+ mHLSResourceWrapper->Suspend();
+}
+
+void HLSDecoder::Resume() {
+ MOZ_ASSERT(NS_IsMainThread());
+ HLS_DEBUG("HLSDecoder", "Should resume the resource fetching.");
+ mHLSResourceWrapper->Resume();
+}
+
+void HLSDecoder::Shutdown() {
+ HLS_DEBUG("HLSDecoder", "Shutdown");
+ if (mCallbackSupport) {
+ mCallbackSupport->Detach();
+ }
+ if (mHLSResourceWrapper) {
+ mHLSResourceWrapper->Destroy();
+ mHLSResourceWrapper = nullptr;
+ }
+ if (mJavaCallbacks) {
+ HLSResourceCallbacksSupport::DisposeNative(mJavaCallbacks);
+ mJavaCallbacks = nullptr;
+ }
+ MediaDecoder::Shutdown();
+}
+
+void HLSDecoder::NotifyDataArrived() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
+ NotifyReaderDataArrived();
+ GetOwner()->DownloadProgressed();
+}
+
+void HLSDecoder::NotifyLoad(nsCString aMediaUrl) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
+ UpdateCurrentPrincipal(aMediaUrl);
+}
+
+// Should be called when the decoder loads media from a URL to ensure the
+// principal of the media element is appropriately set for CORS.
+void HLSDecoder::UpdateCurrentPrincipal(nsCString aMediaUrl) {
+ nsCOMPtr<nsIPrincipal> principal = GetContentPrincipal(aMediaUrl);
+ MOZ_DIAGNOSTIC_ASSERT(principal);
+
+ // Check the subsumption of old and new principals. Should be either
+ // equal or disjoint.
+ if (!mContentPrincipal) {
+ mContentPrincipal = principal;
+ } else if (principal->Equals(mContentPrincipal)) {
+ return;
+ } else if (!principal->Subsumes(mContentPrincipal) &&
+ !mContentPrincipal->Subsumes(principal)) {
+ // Principals are disjoint -- no access.
+ mContentPrincipal = NullPrincipal::Create(OriginAttributes());
+ } else {
+ MOZ_DIAGNOSTIC_ASSERT(false, "non-equal principals should be disjoint");
+ mContentPrincipal = nullptr;
+ }
+ MediaDecoder::NotifyPrincipalChanged();
+}
+
+already_AddRefed<nsIPrincipal> HLSDecoder::GetContentPrincipal(
+ nsCString aMediaUrl) {
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), aMediaUrl.Data());
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ RefPtr<dom::HTMLMediaElement> element = GetOwner()->GetMediaElement();
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ nsSecurityFlags securityFlags =
+ element->ShouldCheckAllowOrigin()
+ ? nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT
+ : nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_INHERITS_SEC_CONTEXT;
+ if (element->GetCORSMode() == CORS_USE_CREDENTIALS) {
+ securityFlags |= nsILoadInfo::SEC_COOKIES_INCLUDE;
+ }
+ nsCOMPtr<nsIChannel> channel;
+ rv = NS_NewChannel(getter_AddRefs(channel), uri,
+ static_cast<dom::Element*>(element), securityFlags,
+ nsIContentPolicy::TYPE_INTERNAL_VIDEO);
+ NS_ENSURE_SUCCESS(rv, nullptr);
+ nsCOMPtr<nsIPrincipal> principal;
+ nsIScriptSecurityManager* secMan = nsContentUtils::GetSecurityManager();
+ if (!secMan) {
+ return nullptr;
+ }
+ secMan->GetChannelResultPrincipal(channel, getter_AddRefs(principal));
+ return principal.forget();
+}
+
+} // namespace mozilla
diff --git a/dom/media/hls/HLSDecoder.h b/dom/media/hls/HLSDecoder.h
new file mode 100644
index 0000000000..0f65457765
--- /dev/null
+++ b/dom/media/hls/HLSDecoder.h
@@ -0,0 +1,79 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef HLSDecoder_h_
+#define HLSDecoder_h_
+
+#include "MediaDecoder.h"
+#include "mozilla/java/GeckoHLSResourceWrapperWrappers.h"
+
+namespace mozilla {
+
+class HLSResourceCallbacksSupport;
+
+class HLSDecoder final : public MediaDecoder {
+ public:
+ static RefPtr<HLSDecoder> Create(MediaDecoderInit& aInit);
+
+ // Returns true if the HLS backend is pref'ed on.
+ static bool IsEnabled();
+
+ // Returns true if aContainerType is an HLS type that we think we can render
+ // with the a platform decoder backend.
+ // If provided, codecs are checked for support.
+ static bool IsSupportedType(const MediaContainerType& aContainerType);
+
+ nsresult Load(nsIChannel* aChannel);
+
+ // MediaDecoder interface.
+ void Play() override;
+
+ void Pause() override;
+
+ void AddSizeOfResources(ResourceSizes* aSizes) override;
+ already_AddRefed<nsIPrincipal> GetCurrentPrincipal() override;
+ bool HadCrossOriginRedirects() override;
+ bool IsTransportSeekable() override { return true; }
+ void Suspend() override;
+ void Resume() override;
+ void Shutdown() override;
+
+ // Called as data arrives on the underlying HLS player. Main thread only.
+ void NotifyDataArrived();
+
+ // Called when Exoplayer start to load media. Main thread only.
+ void NotifyLoad(nsCString aMediaUrl);
+
+ private:
+ friend class HLSResourceCallbacksSupport;
+
+ explicit HLSDecoder(MediaDecoderInit& aInit);
+ ~HLSDecoder();
+ MediaDecoderStateMachineBase* CreateStateMachine(
+ bool aDisableExternalEngine) override;
+
+ bool CanPlayThroughImpl() final {
+ // TODO: We don't know how to estimate 'canplaythrough' for this decoder.
+ // For now we just return true for 'autoplay' can work.
+ return true;
+ }
+
+ void UpdateCurrentPrincipal(nsCString aMediaUrl);
+ already_AddRefed<nsIPrincipal> GetContentPrincipal(nsCString aMediaUrl);
+
+ static size_t sAllocatedInstances; // Access only in the main thread.
+
+ nsCOMPtr<nsIChannel> mChannel;
+ nsCOMPtr<nsIURI> mURI;
+ java::GeckoHLSResourceWrapper::GlobalRef mHLSResourceWrapper;
+ java::GeckoHLSResourceWrapper::Callbacks::GlobalRef mJavaCallbacks;
+ RefPtr<HLSResourceCallbacksSupport> mCallbackSupport;
+ nsCOMPtr<nsIPrincipal> mContentPrincipal;
+};
+
+} // namespace mozilla
+
+#endif /* HLSDecoder_h_ */
diff --git a/dom/media/hls/HLSDemuxer.cpp b/dom/media/hls/HLSDemuxer.cpp
new file mode 100644
index 0000000000..d8b951c6e7
--- /dev/null
+++ b/dom/media/hls/HLSDemuxer.cpp
@@ -0,0 +1,628 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HLSDemuxer.h"
+
+#include <algorithm>
+#include <limits>
+#include <stdint.h>
+
+#include "HLSUtils.h"
+#include "MediaCodec.h"
+#include "mozilla/java/GeckoAudioInfoWrappers.h"
+#include "mozilla/java/GeckoHLSDemuxerWrapperNatives.h"
+#include "mozilla/java/GeckoVideoInfoWrappers.h"
+#include "mozilla/Unused.h"
+#include "nsPrintfCString.h"
+
+namespace mozilla {
+
+static Atomic<uint32_t> sStreamSourceID(0u);
+
+typedef TrackInfo::TrackType TrackType;
+using media::TimeInterval;
+using media::TimeIntervals;
+using media::TimeUnit;
+
+static VideoInfo::Rotation getVideoInfoRotation(int aRotation) {
+ switch (aRotation) {
+ case 0:
+ return VideoInfo::Rotation::kDegree_0;
+ case 90:
+ return VideoInfo::Rotation::kDegree_90;
+ case 180:
+ return VideoInfo::Rotation::kDegree_180;
+ case 270:
+ return VideoInfo::Rotation::kDegree_270;
+ default:
+ return VideoInfo::Rotation::kDegree_0;
+ }
+}
+
+static mozilla::StereoMode getStereoMode(int aMode) {
+ switch (aMode) {
+ case 0:
+ return mozilla::StereoMode::MONO;
+ case 1:
+ return mozilla::StereoMode::TOP_BOTTOM;
+ case 2:
+ return mozilla::StereoMode::LEFT_RIGHT;
+ default:
+ return mozilla::StereoMode::MONO;
+ }
+}
+
+// HLSDemuxerCallbacksSupport is a native implemented callback class for
+// Callbacks in GeckoHLSDemuxerWrapper.java.
+// The callback functions will be invoked by JAVA-side thread.
+// Should dispatch the task to the demuxer's task queue.
+// We ensure the callback will never be invoked after
+// HLSDemuxerCallbacksSupport::DisposeNative has been called in ~HLSDemuxer.
+class HLSDemuxer::HLSDemuxerCallbacksSupport
+ : public java::GeckoHLSDemuxerWrapper::Callbacks::Natives<
+ HLSDemuxerCallbacksSupport> {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(HLSDemuxerCallbacksSupport)
+ public:
+ typedef java::GeckoHLSDemuxerWrapper::Callbacks::Natives<
+ HLSDemuxerCallbacksSupport>
+ NativeCallbacks;
+ using NativeCallbacks::AttachNative;
+ using NativeCallbacks::DisposeNative;
+
+ explicit HLSDemuxerCallbacksSupport(HLSDemuxer* aDemuxer)
+ : mMutex("HLSDemuxerCallbacksSupport"), mDemuxer(aDemuxer) {
+ MOZ_ASSERT(mDemuxer);
+ }
+
+ void OnInitialized(bool aHasAudio, bool aHasVideo) {
+ HLS_DEBUG("HLSDemuxerCallbacksSupport", "OnInitialized");
+ MutexAutoLock lock(mMutex);
+ if (!mDemuxer) {
+ return;
+ }
+ RefPtr<HLSDemuxerCallbacksSupport> self = this;
+ nsresult rv = mDemuxer->GetTaskQueue()->Dispatch(NS_NewRunnableFunction(
+ "HLSDemuxer::HLSDemuxerCallbacksSupport::OnInitialized", [=]() {
+ MutexAutoLock lock(self->mMutex);
+ if (self->mDemuxer) {
+ self->mDemuxer->OnInitialized(aHasAudio, aHasVideo);
+ }
+ }));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ }
+
+ void OnError(int aErrorCode) {
+ HLS_DEBUG("HLSDemuxerCallbacksSupport", "Got error(%d) from java side",
+ aErrorCode);
+ MutexAutoLock lock(mMutex);
+ if (!mDemuxer) {
+ return;
+ }
+ RefPtr<HLSDemuxerCallbacksSupport> self = this;
+ nsresult rv = mDemuxer->GetTaskQueue()->Dispatch(NS_NewRunnableFunction(
+ "HLSDemuxer::HLSDemuxerCallbacksSupport::OnError", [=]() {
+ MutexAutoLock lock(self->mMutex);
+ if (self->mDemuxer) {
+ self->mDemuxer->OnError(aErrorCode);
+ }
+ }));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ }
+
+ void Detach() {
+ MutexAutoLock lock(mMutex);
+ mDemuxer = nullptr;
+ }
+
+ Mutex mMutex MOZ_UNANNOTATED;
+
+ private:
+ ~HLSDemuxerCallbacksSupport() {}
+ HLSDemuxer* mDemuxer;
+};
+
+HLSDemuxer::HLSDemuxer(int aPlayerId)
+ : mTaskQueue(TaskQueue::Create(
+ GetMediaThreadPool(MediaThreadType::SUPERVISOR), "HLSDemuxer",
+ /* aSupportsTailDispatch = */ false)) {
+ MOZ_ASSERT(NS_IsMainThread());
+ HLSDemuxerCallbacksSupport::Init();
+ mJavaCallbacks = java::GeckoHLSDemuxerWrapper::Callbacks::New();
+ MOZ_ASSERT(mJavaCallbacks);
+
+ mCallbackSupport = new HLSDemuxerCallbacksSupport(this);
+ HLSDemuxerCallbacksSupport::AttachNative(mJavaCallbacks, mCallbackSupport);
+
+ mHLSDemuxerWrapper =
+ java::GeckoHLSDemuxerWrapper::Create(aPlayerId, mJavaCallbacks);
+ MOZ_ASSERT(mHLSDemuxerWrapper);
+}
+
+void HLSDemuxer::OnInitialized(bool aHasAudio, bool aHasVideo) {
+ MOZ_ASSERT(OnTaskQueue());
+
+ if (aHasAudio) {
+ mAudioDemuxer = new HLSTrackDemuxer(this, TrackInfo::TrackType::kAudioTrack,
+ MakeUnique<AudioInfo>());
+ }
+ if (aHasVideo) {
+ mVideoDemuxer = new HLSTrackDemuxer(this, TrackInfo::TrackType::kVideoTrack,
+ MakeUnique<VideoInfo>());
+ }
+
+ mInitPromise.ResolveIfExists(NS_OK, __func__);
+}
+
+void HLSDemuxer::OnError(int aErrorCode) {
+ MOZ_ASSERT(OnTaskQueue());
+ mInitPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+}
+
+RefPtr<HLSDemuxer::InitPromise> HLSDemuxer::Init() {
+ RefPtr<HLSDemuxer> self = this;
+ return InvokeAsync(GetTaskQueue(), __func__, [self]() {
+ RefPtr<InitPromise> p = self->mInitPromise.Ensure(__func__);
+ return p;
+ });
+}
+
+void HLSDemuxer::NotifyDataArrived() {
+ HLS_DEBUG("HLSDemuxer", "NotifyDataArrived");
+}
+
+uint32_t HLSDemuxer::GetNumberTracks(TrackType aType) const {
+ switch (aType) {
+ case TrackType::kAudioTrack:
+ return mHLSDemuxerWrapper->GetNumberOfTracks(TrackType::kAudioTrack);
+ case TrackType::kVideoTrack:
+ return mHLSDemuxerWrapper->GetNumberOfTracks(TrackType::kVideoTrack);
+ default:
+ return 0;
+ }
+}
+
+already_AddRefed<MediaTrackDemuxer> HLSDemuxer::GetTrackDemuxer(
+ TrackType aType, uint32_t aTrackNumber) {
+ RefPtr<HLSTrackDemuxer> e = nullptr;
+ if (aType == TrackInfo::TrackType::kAudioTrack) {
+ e = mAudioDemuxer;
+ } else {
+ e = mVideoDemuxer;
+ }
+ return e.forget();
+}
+
+bool HLSDemuxer::IsSeekable() const {
+ return !mHLSDemuxerWrapper->IsLiveStream();
+}
+
+UniquePtr<EncryptionInfo> HLSDemuxer::GetCrypto() {
+ // TODO: Currently, our HLS implementation doesn't support encrypted content.
+ // Return null at this stage.
+ return nullptr;
+}
+
+TimeUnit HLSDemuxer::GetNextKeyFrameTime() {
+ MOZ_ASSERT(mHLSDemuxerWrapper);
+ return TimeUnit::FromMicroseconds(mHLSDemuxerWrapper->GetNextKeyFrameTime());
+}
+
+bool HLSDemuxer::OnTaskQueue() const { return mTaskQueue->IsCurrentThreadIn(); }
+
+HLSDemuxer::~HLSDemuxer() {
+ HLS_DEBUG("HLSDemuxer", "~HLSDemuxer()");
+ mCallbackSupport->Detach();
+ if (mHLSDemuxerWrapper) {
+ mHLSDemuxerWrapper->Destroy();
+ mHLSDemuxerWrapper = nullptr;
+ }
+ if (mJavaCallbacks) {
+ HLSDemuxerCallbacksSupport::DisposeNative(mJavaCallbacks);
+ mJavaCallbacks = nullptr;
+ }
+ mInitPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+}
+
+HLSTrackDemuxer::HLSTrackDemuxer(HLSDemuxer* aParent,
+ TrackInfo::TrackType aType,
+ UniquePtr<TrackInfo> aTrackInfo)
+ : mParent(aParent),
+ mType(aType),
+ mMutex("HLSTrackDemuxer"),
+ mTrackInfo(std::move(aTrackInfo)) {
+ // Only support audio and video track currently.
+ MOZ_ASSERT(mType == TrackInfo::kVideoTrack ||
+ mType == TrackInfo::kAudioTrack);
+ UpdateMediaInfo(0);
+}
+
+UniquePtr<TrackInfo> HLSTrackDemuxer::GetInfo() const {
+ MutexAutoLock lock(mMutex);
+ return mTrackInfo->Clone();
+}
+
+RefPtr<HLSTrackDemuxer::SeekPromise> HLSTrackDemuxer::Seek(
+ const TimeUnit& aTime) {
+ MOZ_ASSERT(mParent, "Called after BreackCycle()");
+ return InvokeAsync<TimeUnit&&>(mParent->GetTaskQueue(), this, __func__,
+ &HLSTrackDemuxer::DoSeek, aTime);
+}
+
+RefPtr<HLSTrackDemuxer::SeekPromise> HLSTrackDemuxer::DoSeek(
+ const TimeUnit& aTime) {
+ MOZ_ASSERT(mParent, "Called after BreackCycle()");
+ MOZ_ASSERT(mParent->OnTaskQueue());
+ mQueuedSample = nullptr;
+ int64_t seekTimeUs = aTime.ToMicroseconds();
+ bool result = mParent->mHLSDemuxerWrapper->Seek(seekTimeUs);
+ if (!result) {
+ return SeekPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA,
+ __func__);
+ }
+ TimeUnit seekTime = TimeUnit::FromMicroseconds(seekTimeUs);
+ return SeekPromise::CreateAndResolve(seekTime, __func__);
+}
+
+RefPtr<HLSTrackDemuxer::SamplesPromise> HLSTrackDemuxer::GetSamples(
+ int32_t aNumSamples) {
+ MOZ_ASSERT(mParent, "Called after BreackCycle()");
+ return InvokeAsync(mParent->GetTaskQueue(), this, __func__,
+ &HLSTrackDemuxer::DoGetSamples, aNumSamples);
+}
+
+RefPtr<HLSTrackDemuxer::SamplesPromise> HLSTrackDemuxer::DoGetSamples(
+ int32_t aNumSamples) {
+ MOZ_ASSERT(mParent, "Called after BreackCycle()");
+ MOZ_ASSERT(mParent->OnTaskQueue());
+ if (!aNumSamples) {
+ return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_DEMUXER_ERR,
+ __func__);
+ }
+ RefPtr<SamplesHolder> samples = new SamplesHolder;
+ if (mQueuedSample) {
+ if (mQueuedSample->mEOS) {
+ return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_END_OF_STREAM,
+ __func__);
+ }
+ MOZ_ASSERT(mQueuedSample->mKeyframe, "mQueuedSample must be a keyframe");
+ samples->AppendSample(mQueuedSample);
+ mQueuedSample = nullptr;
+ aNumSamples--;
+ }
+ if (aNumSamples == 0) {
+ // Return the queued sample.
+ return SamplesPromise::CreateAndResolve(samples, __func__);
+ }
+ mozilla::jni::ObjectArray::LocalRef demuxedSamples =
+ (mType == TrackInfo::kAudioTrack)
+ ? mParent->mHLSDemuxerWrapper->GetSamples(TrackInfo::kAudioTrack,
+ aNumSamples)
+ : mParent->mHLSDemuxerWrapper->GetSamples(TrackInfo::kVideoTrack,
+ aNumSamples);
+ nsTArray<jni::Object::LocalRef> sampleObjectArray(
+ demuxedSamples->GetElements());
+
+ if (sampleObjectArray.IsEmpty()) {
+ return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA,
+ __func__);
+ }
+
+ for (auto&& demuxedSample : sampleObjectArray) {
+ java::GeckoHLSSample::LocalRef sample(std::move(demuxedSample));
+ if (sample->IsEOS()) {
+ HLS_DEBUG("HLSTrackDemuxer", "Met BUFFER_FLAG_END_OF_STREAM.");
+ if (samples->GetSamples().IsEmpty()) {
+ return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_END_OF_STREAM,
+ __func__);
+ }
+ mQueuedSample = new MediaRawData();
+ mQueuedSample->mEOS = true;
+ break;
+ }
+ RefPtr<MediaRawData> mrd = ConvertToMediaRawData(sample);
+ if (!mrd) {
+ return SamplesPromise::CreateAndReject(NS_ERROR_OUT_OF_MEMORY, __func__);
+ }
+ if (!mrd->HasValidTime()) {
+ return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_DEMUXER_ERR,
+ __func__);
+ }
+ samples->AppendSample(mrd);
+ }
+ if (mType == TrackInfo::kVideoTrack &&
+ (mNextKeyframeTime.isNothing() ||
+ samples->GetSamples().LastElement()->mTime >=
+ mNextKeyframeTime.value())) {
+ // Only need to find NextKeyFrame for Video
+ UpdateNextKeyFrameTime();
+ }
+
+ return SamplesPromise::CreateAndResolve(samples, __func__);
+}
+
+void HLSTrackDemuxer::UpdateMediaInfo(int index) {
+ MOZ_ASSERT(mParent->OnTaskQueue());
+ MOZ_ASSERT(mParent->mHLSDemuxerWrapper);
+ MutexAutoLock lock(mMutex);
+ jni::Object::LocalRef infoObj = nullptr;
+ if (mType == TrackType::kAudioTrack) {
+ infoObj = mParent->mHLSDemuxerWrapper->GetAudioInfo(index);
+ if (!infoObj) {
+ NS_WARNING("Failed to get audio info from Java wrapper");
+ return;
+ }
+ auto* audioInfo = mTrackInfo->GetAsAudioInfo();
+ MOZ_ASSERT(audioInfo != nullptr);
+ HLS_DEBUG("HLSTrackDemuxer", "Update audio info (%d)", index);
+ java::GeckoAudioInfo::LocalRef audioInfoObj(std::move(infoObj));
+ audioInfo->mRate = audioInfoObj->Rate();
+ audioInfo->mChannels = audioInfoObj->Channels();
+ audioInfo->mProfile = audioInfoObj->Profile();
+ audioInfo->mBitDepth = audioInfoObj->BitDepth();
+ audioInfo->mMimeType =
+ NS_ConvertUTF16toUTF8(audioInfoObj->MimeType()->ToString());
+ audioInfo->mDuration = TimeUnit::FromMicroseconds(audioInfoObj->Duration());
+ jni::ByteArray::LocalRef csdBytes = audioInfoObj->CodecSpecificData();
+ if (csdBytes) {
+ auto&& csd = csdBytes->GetElements();
+ AudioCodecSpecificBinaryBlob blob;
+ blob.mBinaryBlob->AppendElements(reinterpret_cast<uint8_t*>(&csd[0]),
+ csd.Length());
+ audioInfo->mCodecSpecificConfig =
+ AudioCodecSpecificVariant{std::move(blob)};
+ }
+ } else {
+ infoObj = mParent->mHLSDemuxerWrapper->GetVideoInfo(index);
+ if (!infoObj) {
+ NS_WARNING("Failed to get video info from Java wrapper");
+ return;
+ }
+ auto* videoInfo = mTrackInfo->GetAsVideoInfo();
+ MOZ_ASSERT(videoInfo != nullptr);
+ java::GeckoVideoInfo::LocalRef videoInfoObj(std::move(infoObj));
+ videoInfo->mStereoMode = getStereoMode(videoInfoObj->StereoMode());
+ videoInfo->mRotation = getVideoInfoRotation(videoInfoObj->Rotation());
+ videoInfo->mImage.width = videoInfoObj->DisplayWidth();
+ videoInfo->mImage.height = videoInfoObj->DisplayHeight();
+ videoInfo->mDisplay.width = videoInfoObj->PictureWidth();
+ videoInfo->mDisplay.height = videoInfoObj->PictureHeight();
+ videoInfo->mMimeType =
+ NS_ConvertUTF16toUTF8(videoInfoObj->MimeType()->ToString());
+ videoInfo->mDuration = TimeUnit::FromMicroseconds(videoInfoObj->Duration());
+ HLS_DEBUG("HLSTrackDemuxer", "Update video info (%d) / I(%dx%d) / D(%dx%d)",
+ index, videoInfo->mImage.width, videoInfo->mImage.height,
+ videoInfo->mDisplay.width, videoInfo->mDisplay.height);
+ }
+}
+
+CryptoSample HLSTrackDemuxer::ExtractCryptoSample(
+ size_t aSampleSize,
+ java::sdk::MediaCodec::CryptoInfo::LocalRef aCryptoInfo) {
+ if (!aCryptoInfo) {
+ return CryptoSample{};
+ }
+ // Extract Crypto information
+ CryptoSample crypto;
+ char const* msg = "";
+ do {
+ HLS_DEBUG("HLSTrackDemuxer", "Sample has Crypto Info");
+
+ int32_t mode = 0;
+ if (NS_FAILED(aCryptoInfo->Mode(&mode))) {
+ msg = "Error when extracting encryption mode.";
+ break;
+ }
+ // We currently only handle ctr mode.
+ if (mode != java::sdk::MediaCodec::CRYPTO_MODE_AES_CTR) {
+ msg = "Error: unexpected encryption mode.";
+ break;
+ }
+
+ crypto.mCryptoScheme = CryptoScheme::Cenc;
+
+ mozilla::jni::ByteArray::LocalRef ivData;
+ if (NS_FAILED(aCryptoInfo->Iv(&ivData))) {
+ msg = "Error when extracting encryption IV.";
+ break;
+ }
+ // Data in mIV is uint8_t and jbyte is signed char
+ auto&& ivArr = ivData->GetElements();
+ crypto.mIV.AppendElements(reinterpret_cast<uint8_t*>(&ivArr[0]),
+ ivArr.Length());
+ crypto.mIVSize = ivArr.Length();
+ mozilla::jni::ByteArray::LocalRef keyData;
+ if (NS_FAILED(aCryptoInfo->Key(&keyData))) {
+ msg = "Error when extracting encryption key.";
+ break;
+ }
+ auto&& keyArr = keyData->GetElements();
+ // Data in mKeyId is uint8_t and jbyte is signed char
+ crypto.mKeyId.AppendElements(reinterpret_cast<uint8_t*>(&keyArr[0]),
+ keyArr.Length());
+
+ mozilla::jni::IntArray::LocalRef clearData;
+ if (NS_FAILED(aCryptoInfo->NumBytesOfClearData(&clearData))) {
+ msg = "Error when extracting clear data.";
+ break;
+ }
+ auto&& clearArr = clearData->GetElements();
+ // Data in mPlainSizes is uint32_t, NumBytesOfClearData is int32_t
+ crypto.mPlainSizes.AppendElements(reinterpret_cast<uint32_t*>(&clearArr[0]),
+ clearArr.Length());
+
+ mozilla::jni::IntArray::LocalRef encryptedData;
+ if (NS_FAILED(aCryptoInfo->NumBytesOfEncryptedData(&encryptedData))) {
+ msg = "Error when extracting encrypted data.";
+ break;
+ }
+ auto&& encryptedArr = encryptedData->GetElements();
+ // Data in mEncryptedSizes is uint32_t, NumBytesOfEncryptedData is int32_t
+ crypto.mEncryptedSizes.AppendElements(
+ reinterpret_cast<uint32_t*>(&encryptedArr[0]), encryptedArr.Length());
+ int subSamplesNum = 0;
+ if (NS_FAILED(aCryptoInfo->NumSubSamples(&subSamplesNum))) {
+ msg = "Error when extracting subsamples.";
+ break;
+ }
+ crypto.mPlainSizes[0] -= (aSampleSize - subSamplesNum);
+
+ return crypto;
+ } while (false);
+
+ HLS_DEBUG("HLSTrackDemuxer", "%s", msg);
+ return CryptoSample{};
+}
+
+RefPtr<MediaRawData> HLSTrackDemuxer::ConvertToMediaRawData(
+ java::GeckoHLSSample::LocalRef aSample) {
+ java::sdk::MediaCodec::BufferInfo::LocalRef info = aSample->Info();
+ // Currently extract PTS, Size and Data without Crypto information.
+ // Transform java Sample into MediaRawData
+ RefPtr<MediaRawData> mrd = new MediaRawData();
+ int64_t presentationTimeUs = 0;
+ bool ok = NS_SUCCEEDED(info->PresentationTimeUs(&presentationTimeUs));
+ mrd->mTime = TimeUnit::FromMicroseconds(presentationTimeUs);
+ mrd->mTimecode = TimeUnit::FromMicroseconds(presentationTimeUs);
+ mrd->mKeyframe = aSample->IsKeyFrame();
+ mrd->mDuration = (mType == TrackInfo::kVideoTrack)
+ ? TimeUnit::FromMicroseconds(aSample->Duration())
+ : TimeUnit::Zero();
+
+ int32_t size = 0;
+ ok &= NS_SUCCEEDED(info->Size(&size));
+ if (!ok) {
+ HLS_DEBUG("HLSTrackDemuxer",
+ "Error occurred during extraction from Sample java object.");
+ return nullptr;
+ }
+
+ // Update A/V stream souce ID & Audio/VideoInfo for MFR.
+ auto sampleFormatIndex = aSample->FormatIndex();
+ if (mLastFormatIndex != sampleFormatIndex) {
+ mLastFormatIndex = sampleFormatIndex;
+ UpdateMediaInfo(mLastFormatIndex);
+ MutexAutoLock lock(mMutex);
+ mrd->mTrackInfo = new TrackInfoSharedPtr(*mTrackInfo, ++sStreamSourceID);
+ }
+
+ // Write payload into MediaRawData
+ UniquePtr<MediaRawDataWriter> writer(mrd->CreateWriter());
+ if (!writer->SetSize(size)) {
+ HLS_DEBUG("HLSTrackDemuxer", "Exit failed to allocate media buffer");
+ return nullptr;
+ }
+ jni::ByteBuffer::LocalRef dest =
+ jni::ByteBuffer::New(writer->Data(), writer->Size());
+ aSample->WriteToByteBuffer(dest);
+
+ writer->mCrypto = ExtractCryptoSample(writer->Size(), aSample->CryptoInfo());
+ return mrd;
+}
+
+void HLSTrackDemuxer::Reset() {
+ MOZ_ASSERT(mParent, "Called after BreackCycle()");
+ mQueuedSample = nullptr;
+}
+
+void HLSTrackDemuxer::UpdateNextKeyFrameTime() {
+ MOZ_ASSERT(mParent, "Called after BreackCycle()");
+ TimeUnit nextKeyFrameTime = mParent->GetNextKeyFrameTime();
+ if (nextKeyFrameTime != mNextKeyframeTime.refOr(TimeUnit::FromInfinity())) {
+ HLS_DEBUG("HLSTrackDemuxer", "Update mNextKeyframeTime to %" PRId64,
+ nextKeyFrameTime.ToMicroseconds());
+ mNextKeyframeTime = Some(nextKeyFrameTime);
+ }
+}
+
+nsresult HLSTrackDemuxer::GetNextRandomAccessPoint(TimeUnit* aTime) {
+ if (mNextKeyframeTime.isNothing()) {
+ // There's no next key frame.
+ *aTime = TimeUnit::FromInfinity();
+ } else {
+ *aTime = mNextKeyframeTime.value();
+ }
+ return NS_OK;
+}
+
+RefPtr<HLSTrackDemuxer::SkipAccessPointPromise>
+HLSTrackDemuxer::SkipToNextRandomAccessPoint(const TimeUnit& aTimeThreshold) {
+ return InvokeAsync(mParent->GetTaskQueue(), this, __func__,
+ &HLSTrackDemuxer::DoSkipToNextRandomAccessPoint,
+ aTimeThreshold);
+}
+
+RefPtr<HLSTrackDemuxer::SkipAccessPointPromise>
+HLSTrackDemuxer::DoSkipToNextRandomAccessPoint(const TimeUnit& aTimeThreshold) {
+ MOZ_ASSERT(mParent, "Called after BreackCycle()");
+ MOZ_ASSERT(mParent->OnTaskQueue());
+ mQueuedSample = nullptr;
+ uint32_t parsed = 0;
+ bool found = false;
+ MediaResult result = NS_ERROR_DOM_MEDIA_END_OF_STREAM;
+ do {
+ mozilla::jni::ObjectArray::LocalRef demuxedSamples =
+ mParent->mHLSDemuxerWrapper->GetSamples(mType, 1);
+ nsTArray<jni::Object::LocalRef> sampleObjectArray(
+ demuxedSamples->GetElements());
+ if (sampleObjectArray.IsEmpty()) {
+ result = NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA;
+ break;
+ }
+ parsed++;
+ java::GeckoHLSSample::LocalRef sample(std::move(sampleObjectArray[0]));
+ if (sample->IsEOS()) {
+ result = NS_ERROR_DOM_MEDIA_END_OF_STREAM;
+ break;
+ }
+ if (sample->IsKeyFrame()) {
+ java::sdk::MediaCodec::BufferInfo::LocalRef info = sample->Info();
+ int64_t presentationTimeUs = 0;
+ if (NS_SUCCEEDED(info->PresentationTimeUs(&presentationTimeUs)) &&
+ TimeUnit::FromMicroseconds(presentationTimeUs) >= aTimeThreshold) {
+ RefPtr<MediaRawData> rawData = ConvertToMediaRawData(sample);
+ if (!rawData) {
+ result = NS_ERROR_OUT_OF_MEMORY;
+ break;
+ }
+ if (!rawData->HasValidTime()) {
+ result = NS_ERROR_DOM_MEDIA_DEMUXER_ERR;
+ break;
+ }
+ found = true;
+ mQueuedSample = rawData;
+ break;
+ }
+ }
+ } while (true);
+
+ if (!found) {
+ return SkipAccessPointPromise::CreateAndReject(
+ SkipFailureHolder(result, parsed), __func__);
+ }
+ return SkipAccessPointPromise::CreateAndResolve(parsed, __func__);
+}
+
+TimeIntervals HLSTrackDemuxer::GetBuffered() {
+ MOZ_ASSERT(mParent, "Called after BreackCycle()");
+ int64_t bufferedTime = mParent->mHLSDemuxerWrapper->GetBuffered(); // us
+ return TimeIntervals(
+ TimeInterval(TimeUnit(), TimeUnit::FromMicroseconds(bufferedTime)));
+}
+
+void HLSTrackDemuxer::BreakCycles() {
+ RefPtr<HLSTrackDemuxer> self = this;
+ nsCOMPtr<nsIRunnable> task = NS_NewRunnableFunction(
+ "HLSTrackDemuxer::BreakCycles", [self]() { self->mParent = nullptr; });
+ nsresult rv = mParent->GetTaskQueue()->Dispatch(task.forget());
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+}
+
+HLSTrackDemuxer::~HLSTrackDemuxer() {}
+
+} // namespace mozilla
diff --git a/dom/media/hls/HLSDemuxer.h b/dom/media/hls/HLSDemuxer.h
new file mode 100644
index 0000000000..679faa759f
--- /dev/null
+++ b/dom/media/hls/HLSDemuxer.h
@@ -0,0 +1,137 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(HLSDemuxer_h_)
+# define HLSDemuxer_h_
+
+# include "MediaCodec.h"
+# include "MediaDataDemuxer.h"
+# include "MediaDecoder.h"
+# include "mozilla/Atomics.h"
+# include "mozilla/java/GeckoHLSDemuxerWrapperWrappers.h"
+# include "mozilla/java/GeckoHLSSampleWrappers.h"
+# include "mozilla/Maybe.h"
+# include "mozilla/Mutex.h"
+# include "mozilla/TaskQueue.h"
+
+# include "VideoUtils.h"
+
+namespace mozilla {
+
+class AbstractThread;
+class MediaResult;
+class HLSTrackDemuxer;
+
+DDLoggedTypeDeclNameAndBase(HLSDemuxer, MediaDataDemuxer);
+DDLoggedTypeNameAndBase(HLSTrackDemuxer, MediaTrackDemuxer);
+
+class HLSDemuxer final : public MediaDataDemuxer,
+ public DecoderDoctorLifeLogger<HLSDemuxer> {
+ class HLSDemuxerCallbacksSupport;
+
+ public:
+ explicit HLSDemuxer(int aPlayerId);
+
+ RefPtr<InitPromise> Init() override;
+
+ uint32_t GetNumberTracks(TrackInfo::TrackType aType) const override;
+
+ already_AddRefed<MediaTrackDemuxer> GetTrackDemuxer(
+ TrackInfo::TrackType aType, uint32_t aTrackNumber) override;
+
+ bool IsSeekable() const override;
+
+ UniquePtr<EncryptionInfo> GetCrypto() override;
+
+ bool ShouldComputeStartTime() const override { return true; }
+
+ void NotifyDataArrived() override;
+
+ TaskQueue* GetTaskQueue() const { return mTaskQueue; }
+ void OnInitialized(bool aHasAudio, bool aHasVideo);
+ void OnError(int aErrorCode);
+
+ private:
+ media::TimeUnit GetNextKeyFrameTime();
+
+ bool OnTaskQueue() const;
+ ~HLSDemuxer();
+ friend class HLSTrackDemuxer;
+
+ const RefPtr<TaskQueue> mTaskQueue;
+ RefPtr<HLSTrackDemuxer> mAudioDemuxer;
+ RefPtr<HLSTrackDemuxer> mVideoDemuxer;
+
+ MozPromiseHolder<InitPromise> mInitPromise;
+ RefPtr<HLSDemuxerCallbacksSupport> mCallbackSupport;
+
+ java::GeckoHLSDemuxerWrapper::Callbacks::GlobalRef mJavaCallbacks;
+ java::GeckoHLSDemuxerWrapper::GlobalRef mHLSDemuxerWrapper;
+};
+
+class HLSTrackDemuxer : public MediaTrackDemuxer,
+ public DecoderDoctorLifeLogger<HLSTrackDemuxer> {
+ public:
+ HLSTrackDemuxer(HLSDemuxer* aParent, TrackInfo::TrackType aType,
+ UniquePtr<TrackInfo> aTrackInfo);
+ ~HLSTrackDemuxer();
+ UniquePtr<TrackInfo> GetInfo() const override;
+
+ RefPtr<SeekPromise> Seek(const media::TimeUnit& aTime) override;
+
+ RefPtr<SamplesPromise> GetSamples(int32_t aNumSamples = 1) override;
+
+ void UpdateMediaInfo(int index);
+
+ void Reset() override;
+
+ nsresult GetNextRandomAccessPoint(media::TimeUnit* aTime) override;
+
+ RefPtr<SkipAccessPointPromise> SkipToNextRandomAccessPoint(
+ const media::TimeUnit& aTimeThreshold) override;
+
+ media::TimeIntervals GetBuffered() override;
+
+ void BreakCycles() override;
+
+ bool GetSamplesMayBlock() const override { return false; }
+
+ bool IsTrackValid() const {
+ MutexAutoLock lock(mMutex);
+ return mTrackInfo->IsValid();
+ }
+
+ private:
+ // Update the timestamp of the next keyframe if there's one.
+ void UpdateNextKeyFrameTime();
+
+ // Runs on HLSDemuxer's task queue.
+ RefPtr<SeekPromise> DoSeek(const media::TimeUnit& aTime);
+ RefPtr<SamplesPromise> DoGetSamples(int32_t aNumSamples);
+ RefPtr<SkipAccessPointPromise> DoSkipToNextRandomAccessPoint(
+ const media::TimeUnit& aTimeThreshold);
+
+ CryptoSample ExtractCryptoSample(
+ size_t aSampleSize,
+ java::sdk::MediaCodec::CryptoInfo::LocalRef aCryptoInfo);
+ RefPtr<MediaRawData> ConvertToMediaRawData(
+ java::GeckoHLSSample::LocalRef aSample);
+
+ RefPtr<HLSDemuxer> mParent;
+ TrackInfo::TrackType mType;
+ Maybe<media::TimeUnit> mNextKeyframeTime;
+ int32_t mLastFormatIndex = -1;
+ // Queued samples extracted by the demuxer, but not yet returned.
+ RefPtr<MediaRawData> mQueuedSample;
+
+ // Mutex to protect members below across multiple threads.
+ mutable Mutex mMutex MOZ_UNANNOTATED;
+ UniquePtr<TrackInfo> mTrackInfo;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/hls/HLSUtils.cpp b/dom/media/hls/HLSUtils.cpp
new file mode 100644
index 0000000000..872de1ced6
--- /dev/null
+++ b/dom/media/hls/HLSUtils.cpp
@@ -0,0 +1,12 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "HLSUtils.h"
+
+mozilla::LogModule* GetHLSLog() {
+ static mozilla::LazyLogModule sLogModule("HLS");
+ return sLogModule;
+}
diff --git a/dom/media/hls/HLSUtils.h b/dom/media/hls/HLSUtils.h
new file mode 100644
index 0000000000..d5505fbd1d
--- /dev/null
+++ b/dom/media/hls/HLSUtils.h
@@ -0,0 +1,21 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef HLSUtils_h_
+#define HLSUtils_h_
+
+#include "mozilla/Logging.h"
+// Logger
+mozilla::LogModule* GetHLSLog();
+
+#define HLS_DEBUG(TAG, format, ...) \
+ MOZ_LOG(GetHLSLog(), mozilla::LogLevel::Debug, \
+ (TAG "(%p)::%s: " format, this, __func__, ##__VA_ARGS__))
+#define HLS_DEBUG_NON_MEMBER(TAG, format, ...) \
+ MOZ_LOG(GetHLSLog(), mozilla::LogLevel::Debug, \
+ (TAG " %s: " format, __func__, ##__VA_ARGS__))
+
+#endif // HLSUtils_h_
diff --git a/dom/media/hls/moz.build b/dom/media/hls/moz.build
new file mode 100644
index 0000000000..8a6c176974
--- /dev/null
+++ b/dom/media/hls/moz.build
@@ -0,0 +1,24 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+EXPORTS += [
+ "HLSDecoder.h",
+ "HLSDemuxer.h",
+ "HLSUtils.h",
+]
+
+UNIFIED_SOURCES += [
+ "HLSDecoder.cpp",
+ "HLSDemuxer.cpp",
+ "HLSUtils.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+
+# Add libFuzzer configuration directives
+include("/tools/fuzzing/libfuzzer-config.mozbuild")
diff --git a/dom/media/imagecapture/CaptureTask.cpp b/dom/media/imagecapture/CaptureTask.cpp
new file mode 100644
index 0000000000..d83de76f5e
--- /dev/null
+++ b/dom/media/imagecapture/CaptureTask.cpp
@@ -0,0 +1,197 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CaptureTask.h"
+#include "gfxUtils.h"
+#include "mozilla/dom/BlobImpl.h"
+#include "mozilla/dom/ImageCapture.h"
+#include "mozilla/dom/ImageCaptureError.h"
+#include "mozilla/dom/ImageEncoder.h"
+#include "mozilla/dom/MediaStreamTrack.h"
+#include "mozilla/dom/VideoStreamTrack.h"
+#include "mozilla/SchedulerGroup.h"
+#include "nsThreadUtils.h"
+#include "VideoSegment.h"
+
+namespace mozilla {
+
+class CaptureTask::MediaTrackEventListener : public MediaTrackListener {
+ public:
+ explicit MediaTrackEventListener(CaptureTask* aCaptureTask)
+ : mCaptureTask(aCaptureTask){};
+
+ // MediaTrackListener methods.
+ void NotifyEnded(MediaTrackGraph* aGraph) override {
+ mCaptureTask->PostTrackEndEvent();
+ }
+
+ private:
+ CaptureTask* mCaptureTask;
+};
+
+CaptureTask::CaptureTask(dom::ImageCapture* aImageCapture)
+ : mImageCapture(aImageCapture),
+ mEventListener(new MediaTrackEventListener(this)),
+ mImageGrabbedOrTrackEnd(false),
+ mPrincipalChanged(false) {}
+
+nsresult CaptureTask::TaskComplete(already_AddRefed<dom::BlobImpl> aBlobImpl,
+ nsresult aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ DetachTrack();
+
+ nsresult rv;
+ RefPtr<dom::BlobImpl> blobImpl(aBlobImpl);
+
+ // We have to set the parent because the blob has been generated with a valid
+ // one.
+ RefPtr<dom::Blob> blob;
+ if (blobImpl) {
+ blob = dom::Blob::Create(mImageCapture->GetOwnerGlobal(), blobImpl);
+ if (NS_WARN_IF(!blob)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ if (mPrincipalChanged) {
+ aRv = NS_ERROR_DOM_SECURITY_ERR;
+ IC_LOG("MediaStream principal should not change during TakePhoto().");
+ }
+
+ if (NS_SUCCEEDED(aRv)) {
+ rv = mImageCapture->PostBlobEvent(blob);
+ } else {
+ rv =
+ mImageCapture->PostErrorEvent(dom::ImageCaptureError::PHOTO_ERROR, aRv);
+ }
+
+ // Ensure ImageCapture dereference on main thread here because the TakePhoto()
+ // sequences stopped here.
+ mImageCapture = nullptr;
+
+ return rv;
+}
+
+void CaptureTask::AttachTrack() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ dom::MediaStreamTrack* track = mImageCapture->GetVideoStreamTrack();
+ track->AddPrincipalChangeObserver(this);
+ track->AddListener(mEventListener.get());
+ track->AddDirectListener(this);
+}
+
+void CaptureTask::DetachTrack() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ dom::MediaStreamTrack* track = mImageCapture->GetVideoStreamTrack();
+ track->RemovePrincipalChangeObserver(this);
+ track->RemoveListener(mEventListener.get());
+ track->RemoveDirectListener(this);
+}
+
+void CaptureTask::PrincipalChanged(dom::MediaStreamTrack* aMediaStreamTrack) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mPrincipalChanged = true;
+}
+
+void CaptureTask::NotifyRealtimeTrackData(MediaTrackGraph* aGraph,
+ TrackTime aTrackOffset,
+ const MediaSegment& aMedia) {
+ MOZ_ASSERT(aMedia.GetType() == MediaSegment::VIDEO);
+ const VideoSegment& video = static_cast<const VideoSegment&>(aMedia);
+
+ // Callback for encoding complete, it calls on main thread.
+ class EncodeComplete : public dom::EncodeCompleteCallback {
+ public:
+ explicit EncodeComplete(CaptureTask* aTask) : mTask(aTask) {}
+
+ bool CanBeDeletedOnAnyThread() override {
+ // EncodeComplete is used from the main thread only.
+ return false;
+ }
+
+ nsresult ReceiveBlobImpl(
+ already_AddRefed<dom::BlobImpl> aBlobImpl) override {
+ MOZ_ASSERT(NS_IsMainThread());
+ RefPtr<dom::BlobImpl> blobImpl(aBlobImpl);
+ mTask->TaskComplete(blobImpl.forget(), NS_OK);
+ mTask = nullptr;
+ return NS_OK;
+ }
+
+ protected:
+ RefPtr<CaptureTask> mTask;
+ };
+
+ for (VideoSegment::ConstChunkIterator iter(video); !iter.IsEnded();
+ iter.Next()) {
+ VideoChunk chunk = *iter;
+
+ // Extract the first valid video frame.
+ VideoFrame frame;
+ if (chunk.IsNull()) {
+ continue;
+ }
+
+ RefPtr<layers::Image> image;
+ if (chunk.mFrame.GetForceBlack()) {
+ // Create a black image.
+ image = VideoFrame::CreateBlackImage(chunk.mFrame.GetIntrinsicSize());
+ } else {
+ image = chunk.mFrame.GetImage();
+ }
+ if (!image) {
+ MOZ_ASSERT(image);
+ continue;
+ }
+
+ bool wasGrabbed = mImageGrabbedOrTrackEnd.exchange(true);
+ if (wasGrabbed) {
+ return;
+ }
+
+ // Encode image.
+ nsresult rv;
+ nsAutoString type(u"image/jpeg"_ns);
+ nsAutoString options;
+ rv = dom::ImageEncoder::ExtractDataFromLayersImageAsync(
+ type, options, false, image, false, new EncodeComplete(this));
+ if (NS_FAILED(rv)) {
+ PostTrackEndEvent();
+ }
+ }
+}
+
+void CaptureTask::PostTrackEndEvent() {
+ bool wasGrabbed = mImageGrabbedOrTrackEnd.exchange(true);
+ if (wasGrabbed) {
+ return;
+ }
+
+ // Got track end or finish event, stop the task.
+ class TrackEndRunnable : public Runnable {
+ public:
+ explicit TrackEndRunnable(CaptureTask* aTask)
+ : mozilla::Runnable("TrackEndRunnable"), mTask(aTask) {}
+
+ NS_IMETHOD Run() override {
+ mTask->TaskComplete(nullptr, NS_ERROR_FAILURE);
+ mTask = nullptr;
+ return NS_OK;
+ }
+
+ protected:
+ RefPtr<CaptureTask> mTask;
+ };
+
+ IC_LOG("Got MediaTrack track removed or finished event.");
+ nsCOMPtr<nsIRunnable> event = new TrackEndRunnable(this);
+ SchedulerGroup::Dispatch(TaskCategory::Other, event.forget());
+}
+
+} // namespace mozilla
diff --git a/dom/media/imagecapture/CaptureTask.h b/dom/media/imagecapture/CaptureTask.h
new file mode 100644
index 0000000000..efcd0e84b2
--- /dev/null
+++ b/dom/media/imagecapture/CaptureTask.h
@@ -0,0 +1,90 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef CAPTURETASK_H
+#define CAPTURETASK_H
+
+#include "MediaTrackGraph.h"
+#include "MediaTrackListener.h"
+#include "PrincipalChangeObserver.h"
+
+namespace mozilla {
+
+namespace dom {
+class BlobImpl;
+class ImageCapture;
+class MediaStreamTrack;
+} // namespace dom
+
+/**
+ * CaptureTask retrieves image from MediaTrack and encodes the image to jpeg in
+ * ImageEncoder. The whole procedures start at AttachTrack(), it will add this
+ * class into MediaTrack and retrieves an image in MediaTrackGraph thread.
+ * Once the image is retrieved, it will be sent to ImageEncoder and the encoded
+ * blob will be sent out via encoder callback in main thread.
+ *
+ * CaptureTask holds a reference of ImageCapture to ensure ImageCapture won't be
+ * released during the period of the capturing process described above.
+ */
+class CaptureTask : public DirectMediaTrackListener,
+ public dom::PrincipalChangeObserver<dom::MediaStreamTrack> {
+ public:
+ class MediaTrackEventListener;
+
+ // DirectMediaTrackListener methods
+ void NotifyRealtimeTrackData(MediaTrackGraph* aGraph, TrackTime aTrackOffset,
+ const MediaSegment& aMedia) override;
+
+ // PrincipalChangeObserver<MediaStreamTrack> methods
+ void PrincipalChanged(dom::MediaStreamTrack* aMediaStreamTrack) override;
+
+ // CaptureTask methods.
+
+ // It is called when aBlob is ready to post back to script in company with
+ // aRv == NS_OK. If aRv is not NS_OK, it will post an error event to script.
+ //
+ // Note:
+ // this function should be called on main thread.
+ nsresult TaskComplete(already_AddRefed<dom::BlobImpl> aBlobImpl,
+ nsresult aRv);
+
+ // Add listeners into MediaStreamTrack and PrincipalChangeObserver.
+ // It should be on main thread only.
+ void AttachTrack();
+
+ // Remove listeners from MediaStreamTrack and PrincipalChangeObserver.
+ // It should be on main thread only.
+ void DetachTrack();
+
+ // CaptureTask should be created on main thread.
+ explicit CaptureTask(dom::ImageCapture* aImageCapture);
+
+ protected:
+ virtual ~CaptureTask() = default;
+
+ // Post a runnable on main thread to end this task and call TaskComplete to
+ // post error event to script. It is called off-main-thread.
+ void PostTrackEndEvent();
+
+ // The ImageCapture associates with this task. This reference count should not
+ // change in this class unless it clears this reference after a blob or error
+ // event to script.
+ RefPtr<dom::ImageCapture> mImageCapture;
+
+ RefPtr<MediaTrackEventListener> mEventListener;
+
+ // True when an image is retrieved from the video track, or MediaTrackGraph
+ // sends a track finish, end, or removed event. Any thread.
+ Atomic<bool> mImageGrabbedOrTrackEnd;
+
+ // True after MediaStreamTrack principal changes while waiting for a photo
+ // to finish and we should raise a security error.
+ bool mPrincipalChanged;
+};
+
+} // namespace mozilla
+
+#endif // CAPTURETASK_H
diff --git a/dom/media/imagecapture/ImageCapture.cpp b/dom/media/imagecapture/ImageCapture.cpp
new file mode 100644
index 0000000000..d32c22d054
--- /dev/null
+++ b/dom/media/imagecapture/ImageCapture.cpp
@@ -0,0 +1,212 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ImageCapture.h"
+#include "mozilla/dom/BlobEvent.h"
+#include "mozilla/dom/DOMException.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/File.h"
+#include "mozilla/dom/ImageCaptureError.h"
+#include "mozilla/dom/ImageCaptureErrorEvent.h"
+#include "mozilla/dom/ImageCaptureErrorEventBinding.h"
+#include "mozilla/dom/VideoStreamTrack.h"
+#include "mozilla/dom/Document.h"
+#include "CaptureTask.h"
+#include "MediaEngineSource.h"
+
+namespace mozilla {
+
+LogModule* GetICLog() {
+ static LazyLogModule log("ImageCapture");
+ return log;
+}
+
+namespace dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(ImageCapture, DOMEventTargetHelper, mTrack)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ImageCapture)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+NS_IMPL_ADDREF_INHERITED(ImageCapture, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(ImageCapture, DOMEventTargetHelper)
+
+ImageCapture::ImageCapture(VideoStreamTrack* aTrack,
+ nsPIDOMWindowInner* aOwnerWindow)
+ : DOMEventTargetHelper(aOwnerWindow), mTrack(aTrack) {
+ MOZ_ASSERT(aOwnerWindow);
+ MOZ_ASSERT(aTrack);
+}
+
+ImageCapture::~ImageCapture() { MOZ_ASSERT(NS_IsMainThread()); }
+
+already_AddRefed<ImageCapture> ImageCapture::Constructor(
+ const GlobalObject& aGlobal, MediaStreamTrack& aTrack, ErrorResult& aRv) {
+ nsCOMPtr<nsPIDOMWindowInner> win = do_QueryInterface(aGlobal.GetAsSupports());
+ if (!win) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ if (!aTrack.AsVideoStreamTrack()) {
+ aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+ return nullptr;
+ }
+
+ RefPtr<ImageCapture> object =
+ new ImageCapture(aTrack.AsVideoStreamTrack(), win);
+
+ return object.forget();
+}
+
+MediaStreamTrack* ImageCapture::GetVideoStreamTrack() const { return mTrack; }
+
+nsresult ImageCapture::TakePhotoByMediaEngine() {
+ // Callback for TakPhoto(), it also monitor the principal. If principal
+ // changes, it returns PHOTO_ERROR with security error.
+ class TakePhotoCallback : public MediaEnginePhotoCallback,
+ public PrincipalChangeObserver<MediaStreamTrack> {
+ public:
+ TakePhotoCallback(VideoStreamTrack* aVideoTrack,
+ ImageCapture* aImageCapture)
+ : mVideoTrack(aVideoTrack),
+ mImageCapture(aImageCapture),
+ mPrincipalChanged(false) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mVideoTrack->AddPrincipalChangeObserver(this);
+ }
+
+ void PrincipalChanged(MediaStreamTrack* aMediaStream) override {
+ mPrincipalChanged = true;
+ }
+
+ nsresult PhotoComplete(already_AddRefed<Blob> aBlob) override {
+ RefPtr<Blob> blob = aBlob;
+
+ if (mPrincipalChanged) {
+ return PhotoError(NS_ERROR_DOM_SECURITY_ERR);
+ }
+ return mImageCapture->PostBlobEvent(blob);
+ }
+
+ nsresult PhotoError(nsresult aRv) override {
+ return mImageCapture->PostErrorEvent(ImageCaptureError::PHOTO_ERROR, aRv);
+ }
+
+ protected:
+ ~TakePhotoCallback() {
+ MOZ_ASSERT(NS_IsMainThread());
+ mVideoTrack->RemovePrincipalChangeObserver(this);
+ }
+
+ const RefPtr<VideoStreamTrack> mVideoTrack;
+ const RefPtr<ImageCapture> mImageCapture;
+ bool mPrincipalChanged;
+ };
+
+ RefPtr<MediaEnginePhotoCallback> callback =
+ new TakePhotoCallback(mTrack, this);
+ return mTrack->GetSource().TakePhoto(callback);
+}
+
+void ImageCapture::TakePhoto(ErrorResult& aResult) {
+ // According to spec, MediaStreamTrack.readyState must be "live"; however
+ // gecko doesn't implement it yet (bug 910249). Instead of readyState, we
+ // check MediaStreamTrack.enable before bug 910249 is fixed.
+ // The error code should be INVALID_TRACK, but spec doesn't define it in
+ // ImageCaptureError. So it returns PHOTO_ERROR here before spec updates.
+ if (!mTrack->Enabled()) {
+ PostErrorEvent(ImageCaptureError::PHOTO_ERROR, NS_ERROR_FAILURE);
+ return;
+ }
+
+ // Try if MediaEngine supports taking photo.
+ nsresult rv = TakePhotoByMediaEngine();
+
+ // It falls back to MediaTrackGraph image capture if MediaEngine doesn't
+ // support TakePhoto().
+ if (rv == NS_ERROR_NOT_IMPLEMENTED) {
+ IC_LOG(
+ "MediaEngine doesn't support TakePhoto(), it falls back to "
+ "MediaTrackGraph.");
+ RefPtr<CaptureTask> task = new CaptureTask(this);
+
+ // It adds itself into MediaTrackGraph, so ImageCapture doesn't need to
+ // hold the reference.
+ task->AttachTrack();
+ }
+}
+
+nsresult ImageCapture::PostBlobEvent(Blob* aBlob) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!CheckPrincipal()) {
+ // Media is not same-origin, don't allow the data out.
+ return PostErrorEvent(ImageCaptureError::PHOTO_ERROR,
+ NS_ERROR_DOM_SECURITY_ERR);
+ }
+
+ BlobEventInit init;
+ init.mBubbles = false;
+ init.mCancelable = false;
+ init.mData = aBlob;
+
+ RefPtr<BlobEvent> blob_event =
+ BlobEvent::Constructor(this, u"photo"_ns, init);
+
+ return DispatchTrustedEvent(blob_event);
+}
+
+nsresult ImageCapture::PostErrorEvent(uint16_t aErrorCode, nsresult aReason) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsresult rv = CheckCurrentGlobalCorrectness();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ nsString errorMsg;
+ if (NS_FAILED(aReason)) {
+ nsCString name, message;
+ rv = NS_GetNameAndMessageForDOMNSResult(aReason, name, message);
+ if (NS_SUCCEEDED(rv)) {
+ CopyASCIItoUTF16(message, errorMsg);
+ }
+ }
+
+ RefPtr<ImageCaptureError> error = new ImageCaptureError(
+ static_cast<EventTarget*>(this), aErrorCode, errorMsg);
+
+ ImageCaptureErrorEventInit init;
+ init.mBubbles = false;
+ init.mCancelable = false;
+ init.mImageCaptureError = error;
+
+ RefPtr<Event> event =
+ ImageCaptureErrorEvent::Constructor(this, u"error"_ns, init);
+
+ return DispatchTrustedEvent(event);
+}
+
+bool ImageCapture::CheckPrincipal() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIPrincipal> principal = mTrack->GetPrincipal();
+
+ if (!GetOwner()) {
+ return false;
+ }
+ nsCOMPtr<Document> doc = GetOwner()->GetExtantDoc();
+ if (!doc || !principal) {
+ return false;
+ }
+
+ bool subsumes;
+ if (NS_FAILED(doc->NodePrincipal()->Subsumes(principal, &subsumes))) {
+ return false;
+ }
+
+ return subsumes;
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/media/imagecapture/ImageCapture.h b/dom/media/imagecapture/ImageCapture.h
new file mode 100644
index 0000000000..957a3ffccc
--- /dev/null
+++ b/dom/media/imagecapture/ImageCapture.h
@@ -0,0 +1,95 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef IMAGECAPTURE_H
+#define IMAGECAPTURE_H
+
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/dom/ImageCaptureBinding.h"
+#include "mozilla/Logging.h"
+
+namespace mozilla {
+
+#ifndef IC_LOG
+LogModule* GetICLog();
+# define IC_LOG(...) \
+ MOZ_LOG(GetICLog(), mozilla::LogLevel::Debug, (__VA_ARGS__))
+#endif
+
+namespace dom {
+
+class Blob;
+class MediaStreamTrack;
+class VideoStreamTrack;
+
+/**
+ * Implementation of https://dvcs.w3.org/hg/dap/raw-file/default/media-stream-
+ * capture/ImageCapture.html.
+ * The ImageCapture accepts a video MediaStreamTrack as input source. The image
+ * will be sent back as a JPG format via Blob event.
+ *
+ * All the functions in ImageCapture are run in main thread.
+ *
+ * There are two ways to capture image, MediaEngineSource and MediaTrackGraph.
+ * When the implementation of MediaEngineSource supports TakePhoto(),
+ * it uses the platform camera to grab image. Otherwise, it falls back
+ * to the MediaTrackGraph way.
+ */
+
+class ImageCapture final : public DOMEventTargetHelper {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ImageCapture, DOMEventTargetHelper)
+
+ IMPL_EVENT_HANDLER(photo)
+ IMPL_EVENT_HANDLER(error)
+
+ // WebIDL members.
+ void TakePhoto(ErrorResult& aResult);
+
+ // The MediaStreamTrack passed into the constructor.
+ MediaStreamTrack* GetVideoStreamTrack() const;
+
+ // nsWrapperCache member
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override {
+ return ImageCapture_Binding::Wrap(aCx, this, aGivenProto);
+ }
+
+ // ImageCapture class members
+ nsPIDOMWindowInner* GetParentObject() { return GetOwner(); }
+
+ static already_AddRefed<ImageCapture> Constructor(const GlobalObject& aGlobal,
+ MediaStreamTrack& aTrack,
+ ErrorResult& aRv);
+
+ ImageCapture(VideoStreamTrack* aTrack, nsPIDOMWindowInner* aOwnerWindow);
+
+ // Post a Blob event to script.
+ nsresult PostBlobEvent(Blob* aBlob);
+
+ // Post an error event to script.
+ // aErrorCode should be one of error codes defined in ImageCaptureError.h.
+ // aReason is the nsresult which maps to a error string in
+ // dom/base/domerr.msg.
+ nsresult PostErrorEvent(uint16_t aErrorCode, nsresult aReason = NS_OK);
+
+ bool CheckPrincipal();
+
+ protected:
+ virtual ~ImageCapture();
+
+ // Capture image by MediaEngine. If it's not support taking photo, this
+ // function should return NS_ERROR_NOT_IMPLEMENTED.
+ nsresult TakePhotoByMediaEngine();
+
+ RefPtr<VideoStreamTrack> mTrack;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // IMAGECAPTURE_H
diff --git a/dom/media/imagecapture/moz.build b/dom/media/imagecapture/moz.build
new file mode 100644
index 0000000000..fdbc73430d
--- /dev/null
+++ b/dom/media/imagecapture/moz.build
@@ -0,0 +1,16 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS.mozilla.dom += ["ImageCapture.h"]
+
+UNIFIED_SOURCES += [
+ "CaptureTask.cpp",
+ "ImageCapture.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/ipc/MFCDMChild.cpp b/dom/media/ipc/MFCDMChild.cpp
new file mode 100644
index 0000000000..292d0cd6bd
--- /dev/null
+++ b/dom/media/ipc/MFCDMChild.cpp
@@ -0,0 +1,468 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MFCDMChild.h"
+
+#include "mozilla/EMEUtils.h"
+#include "mozilla/KeySystemConfig.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/WindowsVersion.h"
+#include "mozilla/WMFCDMProxyCallback.h"
+#include "nsString.h"
+#include "RemoteDecoderManagerChild.h"
+
+namespace mozilla {
+
+#define LOG(msg, ...) \
+ EME_LOG("MFCDMChild[%p]@%s: " msg, this, __func__, ##__VA_ARGS__)
+#define SLOG(msg, ...) EME_LOG("MFCDMChild@%s: " msg, __func__, ##__VA_ARGS__)
+
+MFCDMChild::MFCDMChild(const nsAString& aKeySystem)
+ : mKeySystem(aKeySystem),
+ mManagerThread(RemoteDecoderManagerChild::GetManagerThread()),
+ mState(NS_ERROR_NOT_INITIALIZED),
+ mShutdown(false) {
+ mRemotePromise = EnsureRemote();
+}
+
+MFCDMChild::~MFCDMChild() {}
+
+RefPtr<MFCDMChild::RemotePromise> MFCDMChild::EnsureRemote() {
+ if (!mManagerThread) {
+ LOG("no manager thread");
+ mState = NS_ERROR_NOT_AVAILABLE;
+ return RemotePromise::CreateAndReject(mState, __func__);
+ }
+
+ if (!IsWin10OrLater()) {
+ LOG("only support MF CDM on Windows 10+");
+ mState = NS_ERROR_NOT_AVAILABLE;
+ return RemotePromise::CreateAndReject(mState, __func__);
+ }
+
+ RefPtr<MFCDMChild> self = this;
+ RemoteDecoderManagerChild::LaunchUtilityProcessIfNeeded(
+ RemoteDecodeIn::UtilityProcess_MFMediaEngineCDM)
+ ->Then(
+ mManagerThread, __func__,
+ [self, this](bool) {
+ mRemoteRequest.Complete();
+ RefPtr<RemoteDecoderManagerChild> manager =
+ RemoteDecoderManagerChild::GetSingleton(
+ RemoteDecodeIn::UtilityProcess_MFMediaEngineCDM);
+ if (!manager || !manager->CanSend()) {
+ LOG("manager not exists or can't send");
+ mState = NS_ERROR_NOT_AVAILABLE;
+ mRemotePromiseHolder.RejectIfExists(mState, __func__);
+ return;
+ }
+
+ mIPDLSelfRef = this;
+ Unused << manager->SendPMFCDMConstructor(this, mKeySystem);
+ mState = NS_OK;
+ mRemotePromiseHolder.ResolveIfExists(true, __func__);
+ },
+ [self, this](nsresult rv) {
+ mRemoteRequest.Complete();
+ LOG("fail to launch MFCDM process");
+ mState = rv;
+ mRemotePromiseHolder.RejectIfExists(rv, __func__);
+ })
+ ->Track(mRemoteRequest);
+ return mRemotePromiseHolder.Ensure(__func__);
+}
+
+void MFCDMChild::Shutdown() {
+ MOZ_ASSERT(!mShutdown);
+
+ mShutdown = true;
+ mProxyCallback = nullptr;
+
+ mRemoteRequest.DisconnectIfExists();
+ mInitRequest.DisconnectIfExists();
+
+ if (mState == NS_OK) {
+ mManagerThread->Dispatch(
+ NS_NewRunnableFunction(__func__, [self = RefPtr{this}, this]() {
+ for (auto& promise : mPendingSessionPromises) {
+ promise.second.RejectIfExists(NS_ERROR_ABORT, __func__);
+ }
+ mPendingSessionPromises.clear();
+
+ for (auto& promise : mPendingGenericPromises) {
+ promise.second.RejectIfExists(NS_ERROR_ABORT, __func__);
+ }
+ mPendingGenericPromises.clear();
+
+ mRemotePromiseHolder.RejectIfExists(NS_ERROR_ABORT, __func__);
+ mCapabilitiesPromiseHolder.RejectIfExists(NS_ERROR_ABORT, __func__);
+
+ Send__delete__(this);
+ }));
+ }
+}
+
+RefPtr<MFCDMChild::CapabilitiesPromise> MFCDMChild::GetCapabilities() {
+ MOZ_ASSERT(mManagerThread);
+
+ if (mShutdown) {
+ return CapabilitiesPromise::CreateAndReject(NS_ERROR_ABORT, __func__);
+ }
+
+ if (mState != NS_OK && mState != NS_ERROR_NOT_INITIALIZED) {
+ LOG("error=%x", nsresult(mState));
+ return CapabilitiesPromise::CreateAndReject(mState, __func__);
+ }
+
+ auto doSend = [self = RefPtr{this}, this]() {
+ SendGetCapabilities()->Then(
+ mManagerThread, __func__,
+ [self, this](MFCDMCapabilitiesResult&& aResult) {
+ if (aResult.type() == MFCDMCapabilitiesResult::Tnsresult) {
+ mCapabilitiesPromiseHolder.RejectIfExists(aResult.get_nsresult(),
+ __func__);
+ return;
+ }
+ mCapabilitiesPromiseHolder.ResolveIfExists(
+ std::move(aResult.get_MFCDMCapabilitiesIPDL()), __func__);
+ },
+ [self, this](const mozilla::ipc::ResponseRejectReason& aReason) {
+ mCapabilitiesPromiseHolder.RejectIfExists(NS_ERROR_FAILURE, __func__);
+ });
+ };
+
+ return InvokeAsync(doSend, __func__, mCapabilitiesPromiseHolder);
+}
+
+// Neither error nor shutdown.
+void MFCDMChild::AssertSendable() {
+ MOZ_ASSERT((mState == NS_ERROR_NOT_INITIALIZED || mState == NS_OK) &&
+ mShutdown == false);
+}
+
+template <typename PromiseType>
+already_AddRefed<PromiseType> MFCDMChild::InvokeAsync(
+ std::function<void()>&& aCall, const char* aCallerName,
+ MozPromiseHolder<PromiseType>& aPromise) {
+ AssertSendable();
+
+ if (mState == NS_OK) {
+ // mRemotePromise is resolved, send on manager thread.
+ mManagerThread->Dispatch(
+ NS_NewRunnableFunction(aCallerName, std::move(aCall)));
+ } else if (mState == NS_ERROR_NOT_INITIALIZED) {
+ // mRemotePromise is pending, chain to it.
+ mRemotePromise->Then(
+ mManagerThread, __func__, std::move(aCall),
+ [self = RefPtr{this}, this, &aPromise, aCallerName](nsresult rv) {
+ LOG("error=%x", rv);
+ mState = rv;
+ aPromise.RejectIfExists(rv, aCallerName);
+ });
+ }
+
+ return aPromise.Ensure(aCallerName);
+}
+
+RefPtr<MFCDMChild::InitPromise> MFCDMChild::Init(
+ const nsAString& aOrigin, const CopyableTArray<nsString>& aInitDataTypes,
+ const KeySystemConfig::Requirement aPersistentState,
+ const KeySystemConfig::Requirement aDistinctiveID, const bool aHWSecure,
+ WMFCDMProxyCallback* aProxyCallback) {
+ MOZ_ASSERT(mManagerThread);
+
+ if (mShutdown) {
+ return InitPromise::CreateAndReject(NS_ERROR_ABORT, __func__);
+ }
+
+ if (mState != NS_OK && mState != NS_ERROR_NOT_INITIALIZED) {
+ LOG("error=%x", nsresult(mState));
+ return InitPromise::CreateAndReject(mState, __func__);
+ }
+
+ mProxyCallback = aProxyCallback;
+ MFCDMInitParamsIPDL params{nsString(aOrigin), aInitDataTypes, aDistinctiveID,
+ aPersistentState, aHWSecure};
+ auto doSend = [self = RefPtr{this}, this, params]() {
+ SendInit(params)
+ ->Then(
+ mManagerThread, __func__,
+ [self, this](MFCDMInitResult&& aResult) {
+ mInitRequest.Complete();
+ if (aResult.type() == MFCDMInitResult::Tnsresult) {
+ nsresult rv = aResult.get_nsresult();
+ mInitPromiseHolder.RejectIfExists(rv, __func__);
+ return;
+ }
+ mId = aResult.get_MFCDMInitIPDL().id();
+ mInitPromiseHolder.ResolveIfExists(aResult.get_MFCDMInitIPDL(),
+ __func__);
+ },
+ [self, this](const mozilla::ipc::ResponseRejectReason& aReason) {
+ mInitRequest.Complete();
+ mInitPromiseHolder.RejectIfExists(NS_ERROR_FAILURE, __func__);
+ })
+ ->Track(mInitRequest);
+ };
+
+ return InvokeAsync(std::move(doSend), __func__, mInitPromiseHolder);
+}
+
+RefPtr<MFCDMChild::SessionPromise> MFCDMChild::CreateSessionAndGenerateRequest(
+ uint32_t aPromiseId, KeySystemConfig::SessionType aSessionType,
+ const nsAString& aInitDataType, const nsTArray<uint8_t>& aInitData) {
+ MOZ_ASSERT(mManagerThread);
+ MOZ_ASSERT(mId > 0, "Should call Init() first and wait for it");
+
+ if (mShutdown) {
+ return MFCDMChild::SessionPromise::CreateAndReject(NS_ERROR_ABORT,
+ __func__);
+ }
+
+ MOZ_ASSERT(mPendingSessionPromises.find(aPromiseId) ==
+ mPendingSessionPromises.end());
+ mPendingSessionPromises.emplace(aPromiseId,
+ MozPromiseHolder<SessionPromise>{});
+ mManagerThread->Dispatch(NS_NewRunnableFunction(
+ __func__, [self = RefPtr{this}, this,
+ params =
+ MFCDMCreateSessionParamsIPDL{
+ aSessionType, nsString{aInitDataType}, aInitData},
+ aPromiseId] {
+ SendCreateSessionAndGenerateRequest(params)->Then(
+ mManagerThread, __func__,
+ [self, aPromiseId, this](const MFCDMSessionResult& result) {
+ auto iter = mPendingSessionPromises.find(aPromiseId);
+ if (iter == mPendingSessionPromises.end()) {
+ return;
+ }
+ auto& promiseHolder = iter->second;
+ if (result.type() == MFCDMSessionResult::Tnsresult) {
+ promiseHolder.RejectIfExists(result.get_nsresult(), __func__);
+ } else {
+ LOG("session ID=[%zu]%s", result.get_nsString().Length(),
+ NS_ConvertUTF16toUTF8(result.get_nsString()).get());
+ promiseHolder.ResolveIfExists(result.get_nsString(), __func__);
+ }
+ mPendingSessionPromises.erase(iter);
+ },
+ [self, aPromiseId,
+ this](const mozilla::ipc::ResponseRejectReason& aReason) {
+ auto iter = mPendingSessionPromises.find(aPromiseId);
+ if (iter == mPendingSessionPromises.end()) {
+ return;
+ }
+ auto& promiseHolder = iter->second;
+ promiseHolder.RejectIfExists(NS_ERROR_FAILURE, __func__);
+ mPendingSessionPromises.erase(iter);
+ });
+ }));
+ return mPendingSessionPromises[aPromiseId].Ensure(__func__);
+}
+
+RefPtr<GenericPromise> MFCDMChild::LoadSession(
+ uint32_t aPromiseId, const KeySystemConfig::SessionType aSessionType,
+ const nsAString& aSessionId) {
+ MOZ_ASSERT(mManagerThread);
+ MOZ_ASSERT(mId > 0, "Should call Init() first and wait for it");
+
+ if (mShutdown) {
+ return GenericPromise::CreateAndReject(NS_ERROR_ABORT, __func__);
+ }
+
+ MOZ_ASSERT(mPendingGenericPromises.find(aPromiseId) ==
+ mPendingGenericPromises.end());
+ mPendingGenericPromises.emplace(aPromiseId,
+ MozPromiseHolder<GenericPromise>{});
+ mManagerThread->Dispatch(NS_NewRunnableFunction(
+ __func__, [self = RefPtr{this}, this, aSessionType,
+ sessionId = nsString{aSessionId}, aPromiseId] {
+ SendLoadSession(aSessionType, sessionId)
+ ->Then(mManagerThread, __func__,
+ [self, this, aPromiseId](
+ PMFCDMChild::LoadSessionPromise::ResolveOrRejectValue&&
+ aResult) {
+ auto iter = mPendingGenericPromises.find(aPromiseId);
+ if (iter == mPendingGenericPromises.end()) {
+ return;
+ }
+ auto& promiseHolder = iter->second;
+ if (aResult.IsResolve()) {
+ if (NS_SUCCEEDED(aResult.ResolveValue())) {
+ promiseHolder.ResolveIfExists(true, __func__);
+ } else {
+ promiseHolder.RejectIfExists(aResult.ResolveValue(),
+ __func__);
+ }
+ } else {
+ // IPC died
+ promiseHolder.RejectIfExists(NS_ERROR_FAILURE, __func__);
+ }
+ mPendingGenericPromises.erase(iter);
+ });
+ }));
+ return mPendingGenericPromises[aPromiseId].Ensure(__func__);
+}
+
+RefPtr<GenericPromise> MFCDMChild::UpdateSession(uint32_t aPromiseId,
+ const nsAString& aSessionId,
+ nsTArray<uint8_t>& aResponse) {
+ MOZ_ASSERT(mManagerThread);
+ MOZ_ASSERT(mId > 0, "Should call Init() first and wait for it");
+
+ if (mShutdown) {
+ return GenericPromise::CreateAndReject(NS_ERROR_ABORT, __func__);
+ }
+
+ MOZ_ASSERT(mPendingGenericPromises.find(aPromiseId) ==
+ mPendingGenericPromises.end());
+ mPendingGenericPromises.emplace(aPromiseId,
+ MozPromiseHolder<GenericPromise>{});
+ mManagerThread->Dispatch(NS_NewRunnableFunction(
+ __func__, [self = RefPtr{this}, this, sessionId = nsString{aSessionId},
+ response = std::move(aResponse), aPromiseId] {
+ SendUpdateSession(sessionId, response)
+ ->Then(mManagerThread, __func__,
+ [self, this, aPromiseId](
+ PMFCDMChild::UpdateSessionPromise::ResolveOrRejectValue&&
+ aResult) {
+ auto iter = mPendingGenericPromises.find(aPromiseId);
+ if (iter == mPendingGenericPromises.end()) {
+ return;
+ }
+ auto& promiseHolder = iter->second;
+ if (aResult.IsResolve()) {
+ if (NS_SUCCEEDED(aResult.ResolveValue())) {
+ promiseHolder.ResolveIfExists(true, __func__);
+ } else {
+ promiseHolder.RejectIfExists(aResult.ResolveValue(),
+ __func__);
+ }
+ } else {
+ // IPC died
+ promiseHolder.RejectIfExists(NS_ERROR_FAILURE, __func__);
+ }
+ mPendingGenericPromises.erase(iter);
+ });
+ }));
+ return mPendingGenericPromises[aPromiseId].Ensure(__func__);
+}
+
+RefPtr<GenericPromise> MFCDMChild::CloseSession(uint32_t aPromiseId,
+ const nsAString& aSessionId) {
+ MOZ_ASSERT(mManagerThread);
+ MOZ_ASSERT(mId > 0, "Should call Init() first and wait for it");
+
+ if (mShutdown) {
+ return GenericPromise::CreateAndReject(NS_ERROR_ABORT, __func__);
+ }
+
+ MOZ_ASSERT(mPendingGenericPromises.find(aPromiseId) ==
+ mPendingGenericPromises.end());
+ mPendingGenericPromises.emplace(aPromiseId,
+ MozPromiseHolder<GenericPromise>{});
+ mManagerThread->Dispatch(NS_NewRunnableFunction(
+ __func__, [self = RefPtr{this}, this, sessionId = nsString{aSessionId},
+ aPromiseId] {
+ SendCloseSession(sessionId)->Then(
+ mManagerThread, __func__,
+ [self, this, aPromiseId](
+ PMFCDMChild::CloseSessionPromise::ResolveOrRejectValue&&
+ aResult) {
+ auto iter = mPendingGenericPromises.find(aPromiseId);
+ if (iter == mPendingGenericPromises.end()) {
+ return;
+ }
+ auto& promiseHolder = iter->second;
+ if (aResult.IsResolve()) {
+ if (NS_SUCCEEDED(aResult.ResolveValue())) {
+ promiseHolder.ResolveIfExists(true, __func__);
+ } else {
+ promiseHolder.RejectIfExists(aResult.ResolveValue(),
+ __func__);
+ }
+ } else {
+ // IPC died
+ promiseHolder.RejectIfExists(NS_ERROR_FAILURE, __func__);
+ }
+ mPendingGenericPromises.erase(iter);
+ });
+ }));
+ return mPendingGenericPromises[aPromiseId].Ensure(__func__);
+}
+
+RefPtr<GenericPromise> MFCDMChild::RemoveSession(uint32_t aPromiseId,
+ const nsAString& aSessionId) {
+ MOZ_ASSERT(mManagerThread);
+ MOZ_ASSERT(mId > 0, "Should call Init() first and wait for it");
+
+ if (mShutdown) {
+ return GenericPromise::CreateAndReject(NS_ERROR_ABORT, __func__);
+ }
+
+ MOZ_ASSERT(mPendingGenericPromises.find(aPromiseId) ==
+ mPendingGenericPromises.end());
+ mPendingGenericPromises.emplace(aPromiseId,
+ MozPromiseHolder<GenericPromise>{});
+ mManagerThread->Dispatch(NS_NewRunnableFunction(
+ __func__, [self = RefPtr{this}, this, sessionId = nsString{aSessionId},
+ aPromiseId] {
+ SendRemoveSession(sessionId)->Then(
+ mManagerThread, __func__,
+ [self, this, aPromiseId](
+ PMFCDMChild::RemoveSessionPromise::ResolveOrRejectValue&&
+ aResult) {
+ auto iter = mPendingGenericPromises.find(aPromiseId);
+ if (iter == mPendingGenericPromises.end()) {
+ return;
+ }
+ auto& promiseHolder = iter->second;
+ if (aResult.IsResolve()) {
+ if (NS_SUCCEEDED(aResult.ResolveValue())) {
+ promiseHolder.ResolveIfExists(true, __func__);
+ } else {
+ promiseHolder.RejectIfExists(aResult.ResolveValue(),
+ __func__);
+ }
+ } else {
+ // IPC died
+ promiseHolder.RejectIfExists(NS_ERROR_FAILURE, __func__);
+ }
+ mPendingGenericPromises.erase(iter);
+ });
+ }));
+ return mPendingGenericPromises[aPromiseId].Ensure(__func__);
+}
+
+mozilla::ipc::IPCResult MFCDMChild::RecvOnSessionKeyMessage(
+ const MFCDMKeyMessage& aMessage) {
+ LOG("RecvOnSessionKeyMessage, sessionId=%s",
+ NS_ConvertUTF16toUTF8(aMessage.sessionId()).get());
+ MOZ_ASSERT(mProxyCallback);
+ mProxyCallback->OnSessionMessage(aMessage);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult MFCDMChild::RecvOnSessionKeyStatusesChanged(
+ const MFCDMKeyStatusChange& aKeyStatuses) {
+ LOG("RecvOnSessionKeyStatusesChanged, sessionId=%s",
+ NS_ConvertUTF16toUTF8(aKeyStatuses.sessionId()).get());
+ MOZ_ASSERT(mProxyCallback);
+ mProxyCallback->OnSessionKeyStatusesChange(aKeyStatuses);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult MFCDMChild::RecvOnSessionKeyExpiration(
+ const MFCDMKeyExpiration& aExpiration) {
+ LOG("RecvOnSessionKeyExpiration, sessionId=%s",
+ NS_ConvertUTF16toUTF8(aExpiration.sessionId()).get());
+ MOZ_ASSERT(mProxyCallback);
+ mProxyCallback->OnSessionKeyExpiration(aExpiration);
+ return IPC_OK();
+}
+
+#undef SLOG
+#undef LOG
+
+} // namespace mozilla
diff --git a/dom/media/ipc/MFCDMChild.h b/dom/media/ipc/MFCDMChild.h
new file mode 100644
index 0000000000..e022e67862
--- /dev/null
+++ b/dom/media/ipc/MFCDMChild.h
@@ -0,0 +1,146 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_IPC_MFCDMCHILD_H_
+#define DOM_MEDIA_IPC_MFCDMCHILD_H_
+
+#include <unordered_map>
+#include "mozilla/Atomics.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/PMFCDMChild.h"
+
+namespace mozilla {
+
+class WMFCDMProxyCallback;
+
+/**
+ * MFCDMChild is a content process proxy to MFCDMParent and the actual CDM
+ * running in utility process.
+ */
+class MFCDMChild final : public PMFCDMChild {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MFCDMChild);
+
+ explicit MFCDMChild(const nsAString& aKeySystem);
+
+ using CapabilitiesPromise = MozPromise<MFCDMCapabilitiesIPDL, nsresult, true>;
+ RefPtr<CapabilitiesPromise> GetCapabilities();
+
+ template <typename PromiseType>
+ already_AddRefed<PromiseType> InvokeAsync(
+ std::function<void()>&& aCall, const char* aCallerName,
+ MozPromiseHolder<PromiseType>& aPromise);
+
+ using InitPromise = MozPromise<MFCDMInitIPDL, nsresult, true>;
+ RefPtr<InitPromise> Init(const nsAString& aOrigin,
+ const CopyableTArray<nsString>& aInitDataTypes,
+ const KeySystemConfig::Requirement aPersistentState,
+ const KeySystemConfig::Requirement aDistinctiveID,
+ const bool aHWSecure,
+ WMFCDMProxyCallback* aProxyCallback);
+
+ using SessionPromise = MozPromise<nsString, nsresult, true>;
+ RefPtr<SessionPromise> CreateSessionAndGenerateRequest(
+ uint32_t aPromiseId, const KeySystemConfig::SessionType aSessionType,
+ const nsAString& aInitDataType, const nsTArray<uint8_t>& aInitData);
+
+ RefPtr<GenericPromise> LoadSession(
+ uint32_t aPromiseId, const KeySystemConfig::SessionType aSessionType,
+ const nsAString& aSessionId);
+
+ RefPtr<GenericPromise> UpdateSession(uint32_t aPromiseId,
+ const nsAString& aSessionId,
+ nsTArray<uint8_t>& aResponse);
+
+ RefPtr<GenericPromise> CloseSession(uint32_t aPromiseId,
+ const nsAString& aSessionId);
+
+ RefPtr<GenericPromise> RemoveSession(uint32_t aPromiseId,
+ const nsAString& aSessionId);
+
+ mozilla::ipc::IPCResult RecvOnSessionKeyMessage(
+ const MFCDMKeyMessage& aMessage);
+ mozilla::ipc::IPCResult RecvOnSessionKeyStatusesChanged(
+ const MFCDMKeyStatusChange& aKeyStatuses);
+ mozilla::ipc::IPCResult RecvOnSessionKeyExpiration(
+ const MFCDMKeyExpiration& aExpiration);
+
+ uint64_t Id() const { return mId; }
+
+ void IPDLActorDestroyed() {
+ AssertOnManagerThread();
+ mIPDLSelfRef = nullptr;
+ if (!mShutdown) {
+ // Remote crashed!
+ mState = NS_ERROR_NOT_AVAILABLE;
+ }
+ }
+ void Shutdown();
+
+ nsISerialEventTarget* ManagerThread() { return mManagerThread; }
+ void AssertOnManagerThread() const {
+ MOZ_ASSERT(mManagerThread->IsOnCurrentThread());
+ }
+
+ private:
+ ~MFCDMChild();
+
+ using RemotePromise = GenericNonExclusivePromise;
+ RefPtr<RemotePromise> EnsureRemote();
+
+ void AssertSendable();
+
+ const nsString mKeySystem;
+
+ const RefPtr<nsISerialEventTarget> mManagerThread;
+ RefPtr<MFCDMChild> mIPDLSelfRef;
+
+ RefPtr<RemotePromise> mRemotePromise;
+ MozPromiseHolder<RemotePromise> mRemotePromiseHolder;
+ MozPromiseRequestHolder<RemotePromise> mRemoteRequest;
+ // Before EnsureRemote(): NS_ERROR_NOT_INITIALIZED; After EnsureRemote():
+ // NS_OK or some error code.
+ Atomic<nsresult> mState;
+
+ MozPromiseHolder<CapabilitiesPromise> mCapabilitiesPromiseHolder;
+
+ Atomic<bool> mShutdown;
+
+ // This represents an unique Id to indentify the CDM in the remote process.
+ // 0(zero) means the CDM is not yet initialized.
+ // Modified on the manager thread, and read on other threads.
+ Atomic<uint64_t> mId;
+ MozPromiseHolder<InitPromise> mInitPromiseHolder;
+ using InitIPDLPromise = MozPromise<mozilla::MFCDMInitResult,
+ mozilla::ipc::ResponseRejectReason, true>;
+ MozPromiseRequestHolder<InitIPDLPromise> mInitRequest;
+
+ MozPromiseHolder<SessionPromise> mCreateSessionPromiseHolder;
+ MozPromiseRequestHolder<CreateSessionAndGenerateRequestPromise>
+ mCreateSessionRequest;
+
+ MozPromiseHolder<GenericPromise> mLoadSessionPromiseHolder;
+ MozPromiseRequestHolder<LoadSessionPromise> mLoadSessionRequest;
+
+ MozPromiseHolder<GenericPromise> mUpdateSessionPromiseHolder;
+ MozPromiseRequestHolder<UpdateSessionPromise> mUpdateSessionRequest;
+
+ MozPromiseHolder<GenericPromise> mCloseSessionPromiseHolder;
+ MozPromiseRequestHolder<CloseSessionPromise> mCloseSessionRequest;
+
+ MozPromiseHolder<GenericPromise> mRemoveSessionPromiseHolder;
+ MozPromiseRequestHolder<RemoveSessionPromise> mRemoveSessionRequest;
+
+ std::unordered_map<uint32_t, MozPromiseHolder<SessionPromise>>
+ mPendingSessionPromises;
+
+ std::unordered_map<uint32_t, MozPromiseHolder<GenericPromise>>
+ mPendingGenericPromises;
+
+ RefPtr<WMFCDMProxyCallback> mProxyCallback;
+};
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_IPC_MFCDMCHILD_H_
diff --git a/dom/media/ipc/MFCDMParent.cpp b/dom/media/ipc/MFCDMParent.cpp
new file mode 100644
index 0000000000..26dce82bc5
--- /dev/null
+++ b/dom/media/ipc/MFCDMParent.cpp
@@ -0,0 +1,632 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MFCDMParent.h"
+
+#include <mfmediaengine.h>
+#define INITGUID // Enable DEFINE_PROPERTYKEY()
+#include <propkeydef.h> // For DEFINE_PROPERTYKEY() definition
+#include <propvarutil.h> // For InitPropVariantFrom*()
+
+#include "mozilla/dom/KeySystemNames.h"
+#include "mozilla/EMEUtils.h"
+#include "mozilla/KeySystemConfig.h"
+#include "MFCDMProxy.h"
+#include "RemoteDecodeUtils.h" // For GetCurrentSandboxingKind()
+#include "SpecialSystemDirectory.h" // For temp dir
+
+using Microsoft::WRL::ComPtr;
+using Microsoft::WRL::MakeAndInitialize;
+
+namespace mozilla {
+
+// See
+// https://source.chromium.org/chromium/chromium/src/+/main:media/cdm/win/media_foundation_cdm_util.cc;l=26-40;drc=503535015a7b373cc6185c69c991e01fda5da571
+#ifndef EME_CONTENTDECRYPTIONMODULE_ORIGIN_ID
+DEFINE_PROPERTYKEY(EME_CONTENTDECRYPTIONMODULE_ORIGIN_ID, 0x1218a3e2, 0xcfb0,
+ 0x4c98, 0x90, 0xe5, 0x5f, 0x58, 0x18, 0xd4, 0xb6, 0x7e,
+ PID_FIRST_USABLE);
+#endif
+
+#define MFCDM_PARENT_LOG(msg, ...) \
+ EME_LOG("MFCDMParent[%p, Id=%" PRIu64 "]@%s: " msg, this, this->mId, \
+ __func__, ##__VA_ARGS__)
+#define MFCDM_PARENT_SLOG(msg, ...) \
+ EME_LOG("MFCDMParent@%s: " msg, __func__, ##__VA_ARGS__)
+
+#define MFCDM_RETURN_IF_FAILED(x) \
+ do { \
+ HRESULT rv = x; \
+ if (MOZ_UNLIKELY(FAILED(rv))) { \
+ MFCDM_PARENT_SLOG("(" #x ") failed, rv=%lx", rv); \
+ return rv; \
+ } \
+ } while (false)
+
+#define MFCDM_REJECT_IF(pred, rv) \
+ do { \
+ if (MOZ_UNLIKELY(pred)) { \
+ MFCDM_PARENT_LOG("reject for [" #pred "], rv=%x", rv); \
+ aResolver(rv); \
+ return IPC_OK(); \
+ } \
+ } while (false)
+
+#define MFCDM_REJECT_IF_FAILED(op, rv) \
+ do { \
+ HRESULT hr = op; \
+ if (MOZ_UNLIKELY(FAILED(hr))) { \
+ MFCDM_PARENT_LOG("(" #op ") failed(hr=%lx), rv=%x", hr, rv); \
+ aResolver(rv); \
+ return IPC_OK(); \
+ } \
+ } while (false)
+
+void MFCDMParent::Register() {
+ MOZ_ASSERT(!sRegisteredCDMs.Contains(this->mId));
+ sRegisteredCDMs.InsertOrUpdate(this->mId, this);
+ MFCDM_PARENT_LOG("Registered!");
+}
+
+void MFCDMParent::Unregister() {
+ MOZ_ASSERT(sRegisteredCDMs.Contains(this->mId));
+ sRegisteredCDMs.Remove(this->mId);
+ MFCDM_PARENT_LOG("Unregistered!");
+}
+
+MFCDMParent::MFCDMParent(const nsAString& aKeySystem,
+ RemoteDecoderManagerParent* aManager,
+ nsISerialEventTarget* aManagerThread)
+ : mKeySystem(aKeySystem),
+ mManager(aManager),
+ mManagerThread(aManagerThread),
+ mId(sNextId++),
+ mKeyMessageEvents(aManagerThread),
+ mKeyChangeEvents(aManagerThread),
+ mExpirationEvents(aManagerThread) {
+ // TODO: check Widevine too when it's ready.
+ MOZ_ASSERT(IsPlayReadyKeySystemAndSupported(aKeySystem));
+ MOZ_ASSERT(aManager);
+ MOZ_ASSERT(aManagerThread);
+ MOZ_ASSERT(XRE_IsUtilityProcess());
+ MOZ_ASSERT(GetCurrentSandboxingKind() ==
+ ipc::SandboxingKind::MF_MEDIA_ENGINE_CDM);
+
+ mIPDLSelfRef = this;
+ LoadFactory();
+ Register();
+
+ mKeyMessageListener = mKeyMessageEvents.Connect(
+ mManagerThread, this, &MFCDMParent::SendOnSessionKeyMessage);
+ mKeyChangeListener = mKeyChangeEvents.Connect(
+ mManagerThread, this, &MFCDMParent::SendOnSessionKeyStatusesChanged);
+ mExpirationListener = mExpirationEvents.Connect(
+ mManagerThread, this, &MFCDMParent::SendOnSessionKeyExpiration);
+}
+
+void MFCDMParent::Destroy() {
+ AssertOnManagerThread();
+ mKeyMessageEvents.DisconnectAll();
+ mKeyChangeEvents.DisconnectAll();
+ mExpirationEvents.DisconnectAll();
+ mKeyMessageListener.DisconnectIfExists();
+ mKeyChangeListener.DisconnectIfExists();
+ mExpirationListener.DisconnectIfExists();
+ mIPDLSelfRef = nullptr;
+}
+
+HRESULT MFCDMParent::LoadFactory() {
+ ComPtr<IMFMediaEngineClassFactory4> clsFactory;
+ MFCDM_RETURN_IF_FAILED(CoCreateInstance(CLSID_MFMediaEngineClassFactory,
+ nullptr, CLSCTX_INPROC_SERVER,
+ IID_PPV_ARGS(&clsFactory)));
+
+ ComPtr<IMFContentDecryptionModuleFactory> cdmFactory;
+ MFCDM_RETURN_IF_FAILED(clsFactory->CreateContentDecryptionModuleFactory(
+ mKeySystem.get(), IID_PPV_ARGS(&cdmFactory)));
+
+ mFactory.Swap(cdmFactory);
+ return S_OK;
+}
+
+// Use IMFContentDecryptionModuleFactory::IsTypeSupported() to get DRM
+// capabilities. It appears to be the same as
+// Windows.Media.Protection.ProtectionCapabilities.IsTypeSupported(). See
+// https://learn.microsoft.com/en-us/uwp/api/windows.media.protection.protectioncapabilities.istypesupported?view=winrt-19041
+static bool FactorySupports(
+ ComPtr<IMFContentDecryptionModuleFactory>& aFactory,
+ const nsString& aKeySystem,
+ const KeySystemConfig::EMECodecString& aVideoCodec,
+ const KeySystemConfig::EMECodecString& aAudioCodec =
+ KeySystemConfig::EMECodecString(""),
+ const nsString& aAdditionalFeature = nsString(u"")) {
+ // MP4 is the only container supported.
+ nsString mimeType(u"video/mp4;codecs=\"");
+ MOZ_ASSERT(!aVideoCodec.IsEmpty());
+ mimeType.AppendASCII(aVideoCodec);
+ if (!aAudioCodec.IsEmpty()) {
+ mimeType.AppendLiteral(u",");
+ mimeType.AppendASCII(aAudioCodec);
+ }
+ // These features are required to call IsTypeSupported(). We only care about
+ // codec and encryption scheme so hardcode the rest.
+ mimeType.AppendLiteral(
+ u"\";features=\"decode-bpp=8,"
+ "decode-res-x=1920,decode-res-y=1080,"
+ "decode-bitrate=10000000,decode-fps=30,");
+ if (!aAdditionalFeature.IsEmpty()) {
+ MOZ_ASSERT(aAdditionalFeature.Last() == u',');
+ mimeType.Append(aAdditionalFeature);
+ }
+ // TODO: add HW robustness setting too.
+ mimeType.AppendLiteral(u"encryption-robustness=SW_SECURE_DECODE\"");
+ return aFactory->IsTypeSupported(aKeySystem.get(), mimeType.get());
+}
+
+mozilla::ipc::IPCResult MFCDMParent::RecvGetCapabilities(
+ GetCapabilitiesResolver&& aResolver) {
+ MFCDM_REJECT_IF(!mFactory, NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+
+ MFCDMCapabilitiesIPDL capabilities;
+
+ // TODO : check HW CDM creation
+ // TODO : add HEVC support?
+ static nsTArray<KeySystemConfig::EMECodecString> kVideoCodecs({
+ KeySystemConfig::EME_CODEC_H264,
+ KeySystemConfig::EME_CODEC_VP8,
+ KeySystemConfig::EME_CODEC_VP9,
+ });
+ // Remember supported video codecs.
+ // It will be used when collecting audio codec and encryption scheme
+ // support.
+ nsTArray<KeySystemConfig::EMECodecString> supportedVideoCodecs;
+ for (auto& codec : kVideoCodecs) {
+ if (FactorySupports(mFactory, mKeySystem, codec)) {
+ MFCDMMediaCapability* c =
+ capabilities.videoCapabilities().AppendElement();
+ c->contentType() = NS_ConvertUTF8toUTF16(codec);
+ MFCDM_PARENT_LOG("%s: +video:%s", __func__, codec.get());
+ supportedVideoCodecs.AppendElement(codec);
+ }
+ }
+ if (supportedVideoCodecs.IsEmpty()) {
+ // Return empty capabilities when no video codec is supported.
+ aResolver(std::move(capabilities));
+ return IPC_OK();
+ }
+
+ static nsTArray<KeySystemConfig::EMECodecString> kAudioCodecs({
+ KeySystemConfig::EME_CODEC_AAC,
+ KeySystemConfig::EME_CODEC_FLAC,
+ KeySystemConfig::EME_CODEC_OPUS,
+ KeySystemConfig::EME_CODEC_VORBIS,
+ });
+ for (auto& codec : kAudioCodecs) {
+ if (FactorySupports(mFactory, mKeySystem, supportedVideoCodecs[0], codec)) {
+ MFCDMMediaCapability* c =
+ capabilities.audioCapabilities().AppendElement();
+ c->contentType() = NS_ConvertUTF8toUTF16(codec);
+ MFCDM_PARENT_LOG("%s: +audio:%s", __func__, codec.get());
+ }
+ }
+
+ // Collect schemes supported by all video codecs.
+ static nsTArray<std::pair<CryptoScheme, nsDependentString>> kSchemes = {
+ std::pair<CryptoScheme, nsDependentString>(
+ CryptoScheme::Cenc, u"encryption-type=cenc,encryption-iv-size=8,"),
+ std::pair<CryptoScheme, nsDependentString>(
+ CryptoScheme::Cbcs, u"encryption-type=cbcs,encryption-iv-size=16,")};
+ for (auto& scheme : kSchemes) {
+ bool ok = true;
+ for (auto& codec : supportedVideoCodecs) {
+ ok &= FactorySupports(mFactory, mKeySystem, codec,
+ KeySystemConfig::EMECodecString(""),
+ scheme.second /* additional feature */);
+ if (!ok) {
+ break;
+ }
+ }
+ if (ok) {
+ capabilities.encryptionSchemes().AppendElement(scheme.first);
+ MFCDM_PARENT_LOG("%s: +scheme:%s", __func__,
+ scheme.first == CryptoScheme::Cenc ? "cenc" : "cbcs");
+ }
+ }
+
+ // TODO: don't hardcode
+ capabilities.initDataTypes().AppendElement(u"keyids");
+ capabilities.initDataTypes().AppendElement(u"cenc");
+ capabilities.sessionTypes().AppendElement(
+ KeySystemConfig::SessionType::Temporary);
+ capabilities.sessionTypes().AppendElement(
+ KeySystemConfig::SessionType::PersistentLicense);
+ // WMF CDMs usually require these. See
+ // https://source.chromium.org/chromium/chromium/src/+/main:media/cdm/win/media_foundation_cdm_factory.cc;l=69-73;drc=b3ca5c09fa0aa07b7f9921501f75e43d80f3ba48
+ capabilities.persistentState() = KeySystemConfig::Requirement::Required;
+ capabilities.distinctiveID() = KeySystemConfig::Requirement::Required;
+
+ capabilities.keySystem() = mKeySystem;
+
+ aResolver(std::move(capabilities));
+ return IPC_OK();
+}
+
+// RAIIized PROPVARIANT. See
+// third_party/libwebrtc/modules/audio_device/win/core_audio_utility_win.h
+class AutoPropVar {
+ public:
+ AutoPropVar() { PropVariantInit(&mVar); }
+
+ ~AutoPropVar() { Reset(); }
+
+ AutoPropVar(const AutoPropVar&) = delete;
+ AutoPropVar& operator=(const AutoPropVar&) = delete;
+ bool operator==(const AutoPropVar&) const = delete;
+ bool operator!=(const AutoPropVar&) const = delete;
+
+ // Returns a pointer to the underlying PROPVARIANT for use as an out param
+ // in a function call.
+ PROPVARIANT* Receive() {
+ MOZ_ASSERT(mVar.vt == VT_EMPTY);
+ return &mVar;
+ }
+
+ // Clears the instance to prepare it for re-use (e.g., via Receive).
+ void Reset() {
+ if (mVar.vt != VT_EMPTY) {
+ HRESULT hr = PropVariantClear(&mVar);
+ MOZ_ASSERT(SUCCEEDED(hr));
+ Unused << hr;
+ }
+ }
+
+ const PROPVARIANT& get() const { return mVar; }
+ const PROPVARIANT* ptr() const { return &mVar; }
+
+ private:
+ PROPVARIANT mVar;
+};
+
+static MF_MEDIAKEYS_REQUIREMENT ToMFRequirement(
+ const KeySystemConfig::Requirement aRequirement) {
+ switch (aRequirement) {
+ case KeySystemConfig::Requirement::NotAllowed:
+ return MF_MEDIAKEYS_REQUIREMENT_NOT_ALLOWED;
+ case KeySystemConfig::Requirement::Optional:
+ return MF_MEDIAKEYS_REQUIREMENT_OPTIONAL;
+ case KeySystemConfig::Requirement::Required:
+ return MF_MEDIAKEYS_REQUIREMENT_REQUIRED;
+ }
+};
+
+static inline LPCWSTR InitDataTypeToString(const nsAString& aInitDataType) {
+ // The strings are defined in https://www.w3.org/TR/eme-initdata-registry/
+ if (aInitDataType.EqualsLiteral("webm")) {
+ return L"webm";
+ } else if (aInitDataType.EqualsLiteral("cenc")) {
+ return L"cenc";
+ } else if (aInitDataType.EqualsLiteral("keyids")) {
+ return L"keyids";
+ } else {
+ return L"unknown";
+ }
+}
+
+static HRESULT BuildCDMAccessConfig(const MFCDMInitParamsIPDL& aParams,
+ ComPtr<IPropertyStore>& aConfig) {
+ ComPtr<IPropertyStore> mksc; // EME MediaKeySystemConfiguration
+ MFCDM_RETURN_IF_FAILED(PSCreateMemoryPropertyStore(IID_PPV_ARGS(&mksc)));
+
+ // If we don't set `MF_EME_INITDATATYPES` then we won't be able to create
+ // CDM module on Windows 10, which is not documented officially.
+ BSTR* initDataTypeArray =
+ (BSTR*)CoTaskMemAlloc(sizeof(BSTR) * aParams.initDataTypes().Length());
+ for (size_t i = 0; i < aParams.initDataTypes().Length(); i++) {
+ initDataTypeArray[i] =
+ SysAllocString(InitDataTypeToString(aParams.initDataTypes()[i]));
+ }
+ AutoPropVar initDataTypes;
+ PROPVARIANT* var = initDataTypes.Receive();
+ var->vt = VT_VECTOR | VT_BSTR;
+ var->cabstr.cElems = static_cast<ULONG>(aParams.initDataTypes().Length());
+ var->cabstr.pElems = initDataTypeArray;
+ MFCDM_RETURN_IF_FAILED(
+ mksc->SetValue(MF_EME_INITDATATYPES, initDataTypes.get()));
+
+ // Empty 'audioCapabilities'.
+ AutoPropVar audioCapabilities;
+ var = audioCapabilities.Receive();
+ var->vt = VT_VARIANT | VT_VECTOR;
+ var->capropvar.cElems = 0;
+ MFCDM_RETURN_IF_FAILED(
+ mksc->SetValue(MF_EME_AUDIOCAPABILITIES, audioCapabilities.get()));
+
+ // 'videoCapabilites'.
+ ComPtr<IPropertyStore> mksmc; // EME MediaKeySystemMediaCapability
+ MFCDM_RETURN_IF_FAILED(PSCreateMemoryPropertyStore(IID_PPV_ARGS(&mksmc)));
+ if (aParams.hwSecure()) {
+ AutoPropVar robustness;
+ var = robustness.Receive();
+ var->vt = VT_BSTR;
+ var->bstrVal = SysAllocString(L"HW_SECURE_ALL");
+ MFCDM_RETURN_IF_FAILED(
+ mksmc->SetValue(MF_EME_ROBUSTNESS, robustness.get()));
+ }
+ // Store mksmc in a PROPVARIANT.
+ AutoPropVar videoCapability;
+ var = videoCapability.Receive();
+ var->vt = VT_UNKNOWN;
+ var->punkVal = mksmc.Detach();
+ // Insert element.
+ AutoPropVar videoCapabilities;
+ var = videoCapabilities.Receive();
+ var->vt = VT_VARIANT | VT_VECTOR;
+ var->capropvar.cElems = 1;
+ var->capropvar.pElems =
+ reinterpret_cast<PROPVARIANT*>(CoTaskMemAlloc(sizeof(PROPVARIANT)));
+ PropVariantCopy(var->capropvar.pElems, videoCapability.ptr());
+ MFCDM_RETURN_IF_FAILED(
+ mksc->SetValue(MF_EME_VIDEOCAPABILITIES, videoCapabilities.get()));
+
+ AutoPropVar persistState;
+ InitPropVariantFromUInt32(ToMFRequirement(aParams.persistentState()),
+ persistState.Receive());
+ MFCDM_RETURN_IF_FAILED(
+ mksc->SetValue(MF_EME_PERSISTEDSTATE, persistState.get()));
+
+ AutoPropVar distinctiveID;
+ InitPropVariantFromUInt32(ToMFRequirement(aParams.distinctiveID()),
+ distinctiveID.Receive());
+ MFCDM_RETURN_IF_FAILED(
+ mksc->SetValue(MF_EME_DISTINCTIVEID, distinctiveID.get()));
+
+ aConfig.Swap(mksc);
+ return S_OK;
+}
+
+static HRESULT BuildCDMProperties(const nsString& aOrigin,
+ ComPtr<IPropertyStore>& aProps) {
+ MOZ_ASSERT(!aOrigin.IsEmpty());
+
+ ComPtr<IPropertyStore> props;
+ MFCDM_RETURN_IF_FAILED(PSCreateMemoryPropertyStore(IID_PPV_ARGS(&props)));
+
+ AutoPropVar origin;
+ MFCDM_RETURN_IF_FAILED(
+ InitPropVariantFromString(aOrigin.get(), origin.Receive()));
+ MFCDM_RETURN_IF_FAILED(
+ props->SetValue(EME_CONTENTDECRYPTIONMODULE_ORIGIN_ID, origin.get()));
+
+ // TODO: support client token?
+
+ // TODO: CDM store path per profile?
+ nsCOMPtr<nsIFile> dir;
+ if (NS_FAILED(GetSpecialSystemDirectory(OS_TemporaryDirectory,
+ getter_AddRefs(dir)))) {
+ return E_ACCESSDENIED;
+ }
+ if (NS_FAILED(dir->AppendNative(nsDependentCString("mfcdm")))) {
+ return E_ACCESSDENIED;
+ }
+ nsresult rv = dir->Create(nsIFile::DIRECTORY_TYPE, 0700);
+ if (rv != NS_ERROR_FILE_ALREADY_EXISTS && NS_FAILED(rv)) {
+ return E_ACCESSDENIED;
+ }
+ nsAutoString cdmStorePath;
+ if (NS_FAILED(dir->GetPath(cdmStorePath))) {
+ return E_ACCESSDENIED;
+ }
+
+ AutoPropVar path;
+ MFCDM_RETURN_IF_FAILED(
+ InitPropVariantFromString(cdmStorePath.get(), path.Receive()));
+ MFCDM_RETURN_IF_FAILED(
+ props->SetValue(MF_CONTENTDECRYPTIONMODULE_STOREPATH, path.get()));
+
+ aProps.Swap(props);
+ return S_OK;
+}
+
+mozilla::ipc::IPCResult MFCDMParent::RecvInit(
+ const MFCDMInitParamsIPDL& aParams, InitResolver&& aResolver) {
+ static auto RequirementToStr = [](KeySystemConfig::Requirement aRequirement) {
+ switch (aRequirement) {
+ case KeySystemConfig::Requirement::Required:
+ return "Required";
+ case KeySystemConfig::Requirement::Optional:
+ return "Optional";
+ default:
+ return "NotAllowed";
+ }
+ };
+
+ MFCDM_PARENT_LOG(
+ "Creating a CDM (key-system=%s, origin=%s, distinctiveID=%s, "
+ "persistentState=%s, "
+ "hwSecure=%d)",
+ NS_ConvertUTF16toUTF8(mKeySystem).get(),
+ NS_ConvertUTF16toUTF8(aParams.origin()).get(),
+ RequirementToStr(aParams.distinctiveID()),
+ RequirementToStr(aParams.persistentState()), aParams.hwSecure());
+ MOZ_ASSERT(mFactory->IsTypeSupported(mKeySystem.get(), nullptr));
+
+ // Get access object to CDM.
+ ComPtr<IPropertyStore> accessConfig;
+ MFCDM_REJECT_IF_FAILED(BuildCDMAccessConfig(aParams, accessConfig),
+ NS_ERROR_FAILURE);
+
+ AutoTArray<IPropertyStore*, 1> configs = {accessConfig.Get()};
+ ComPtr<IMFContentDecryptionModuleAccess> cdmAccess;
+ MFCDM_REJECT_IF_FAILED(
+ mFactory->CreateContentDecryptionModuleAccess(
+ mKeySystem.get(), configs.Elements(), configs.Length(), &cdmAccess),
+ NS_ERROR_FAILURE);
+ // Get CDM.
+ ComPtr<IPropertyStore> cdmProps;
+ MFCDM_REJECT_IF_FAILED(BuildCDMProperties(aParams.origin(), cdmProps),
+ NS_ERROR_FAILURE);
+ ComPtr<IMFContentDecryptionModule> cdm;
+ MFCDM_REJECT_IF_FAILED(
+ cdmAccess->CreateContentDecryptionModule(cdmProps.Get(), &cdm),
+ NS_ERROR_FAILURE);
+
+ mCDM.Swap(cdm);
+ MFCDM_PARENT_LOG("Created a CDM!");
+
+ // TODO : for Widevine CDM, would we still need to do following steps?
+ ComPtr<IMFPMPHost> pmpHost;
+ ComPtr<IMFGetService> cdmService;
+ MFCDM_REJECT_IF_FAILED(mCDM.As(&cdmService), NS_ERROR_FAILURE);
+ MFCDM_REJECT_IF_FAILED(
+ cdmService->GetService(MF_CONTENTDECRYPTIONMODULE_SERVICE,
+ IID_PPV_ARGS(&pmpHost)),
+ NS_ERROR_FAILURE);
+ MFCDM_REJECT_IF_FAILED(
+ SUCCEEDED(MakeAndInitialize<MFPMPHostWrapper>(&mPMPHostWrapper, pmpHost)),
+ NS_ERROR_FAILURE);
+ MFCDM_REJECT_IF_FAILED(mCDM->SetPMPHostApp(mPMPHostWrapper.Get()),
+ NS_ERROR_FAILURE);
+ MFCDM_PARENT_LOG("Set PMPHostWrapper on CDM!");
+ aResolver(MFCDMInitIPDL{mId});
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult MFCDMParent::RecvCreateSessionAndGenerateRequest(
+ const MFCDMCreateSessionParamsIPDL& aParams,
+ CreateSessionAndGenerateRequestResolver&& aResolver) {
+ MOZ_ASSERT(mCDM, "RecvInit() must be called and waited on before this call");
+
+ static auto SessionTypeToStr = [](KeySystemConfig::SessionType aSessionType) {
+ switch (aSessionType) {
+ case KeySystemConfig::SessionType::Temporary:
+ return "temporary";
+ case KeySystemConfig::SessionType::PersistentLicense:
+ return "persistent-license";
+ default: {
+ MOZ_ASSERT_UNREACHABLE("Unsupported license type!");
+ return "invalid";
+ }
+ }
+ };
+ MFCDM_PARENT_LOG("Creating session for type '%s'",
+ SessionTypeToStr(aParams.sessionType()));
+ UniquePtr<MFCDMSession> session{
+ MFCDMSession::Create(aParams.sessionType(), mCDM.Get(), mManagerThread)};
+ if (!session) {
+ MFCDM_PARENT_LOG("Failed to create CDM session");
+ aResolver(NS_ERROR_DOM_MEDIA_CDM_NO_SESSION_ERR);
+ return IPC_OK();
+ }
+
+ MFCDM_REJECT_IF_FAILED(session->GenerateRequest(aParams.initDataType(),
+ aParams.initData().Elements(),
+ aParams.initData().Length()),
+ NS_ERROR_DOM_MEDIA_CDM_SESSION_OPERATION_ERR);
+ ConnectSessionEvents(session.get());
+
+ // TODO : now we assume all session ID is available after session is
+ // created, but this is not always true. Need to remove this assertion and
+ // handle cases where session Id is not available yet.
+ const auto& sessionId = session->SessionID();
+ MOZ_ASSERT(sessionId);
+ mSessions.emplace(*sessionId, std::move(session));
+ MFCDM_PARENT_LOG("Created a CDM session!");
+ aResolver(*sessionId);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult MFCDMParent::RecvLoadSession(
+ const KeySystemConfig::SessionType& aSessionType,
+ const nsString& aSessionId, LoadSessionResolver&& aResolver) {
+ MOZ_ASSERT(mCDM, "RecvInit() must be called and waited on before this call");
+
+ nsresult rv = NS_OK;
+ auto* session = GetSession(aSessionId);
+ if (!session) {
+ aResolver(NS_ERROR_DOM_MEDIA_CDM_NO_SESSION_ERR);
+ return IPC_OK();
+ }
+ MFCDM_REJECT_IF_FAILED(session->Load(aSessionId),
+ NS_ERROR_DOM_MEDIA_CDM_SESSION_OPERATION_ERR);
+ aResolver(rv);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult MFCDMParent::RecvUpdateSession(
+ const nsString& aSessionId, const CopyableTArray<uint8_t>& aResponse,
+ UpdateSessionResolver&& aResolver) {
+ MOZ_ASSERT(mCDM, "RecvInit() must be called and waited on before this call");
+ nsresult rv = NS_OK;
+ auto* session = GetSession(aSessionId);
+ if (!session) {
+ aResolver(NS_ERROR_DOM_MEDIA_CDM_NO_SESSION_ERR);
+ return IPC_OK();
+ }
+ MFCDM_REJECT_IF_FAILED(session->Update(aResponse),
+ NS_ERROR_DOM_MEDIA_CDM_SESSION_OPERATION_ERR);
+ aResolver(rv);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult MFCDMParent::RecvCloseSession(
+ const nsString& aSessionId, UpdateSessionResolver&& aResolver) {
+ MOZ_ASSERT(mCDM, "RecvInit() must be called and waited on before this call");
+ nsresult rv = NS_OK;
+ auto* session = GetSession(aSessionId);
+ if (!session) {
+ aResolver(NS_ERROR_DOM_MEDIA_CDM_NO_SESSION_ERR);
+ return IPC_OK();
+ }
+ MFCDM_REJECT_IF_FAILED(session->Close(),
+ NS_ERROR_DOM_MEDIA_CDM_SESSION_OPERATION_ERR);
+ aResolver(rv);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult MFCDMParent::RecvRemoveSession(
+ const nsString& aSessionId, UpdateSessionResolver&& aResolver) {
+ MOZ_ASSERT(mCDM, "RecvInit() must be called and waited on before this call");
+ nsresult rv = NS_OK;
+ auto* session = GetSession(aSessionId);
+ if (!session) {
+ aResolver(NS_ERROR_DOM_MEDIA_CDM_NO_SESSION_ERR);
+ return IPC_OK();
+ }
+ MFCDM_REJECT_IF_FAILED(session->Remove(),
+ NS_ERROR_DOM_MEDIA_CDM_SESSION_OPERATION_ERR);
+ aResolver(rv);
+ return IPC_OK();
+}
+
+void MFCDMParent::ConnectSessionEvents(MFCDMSession* aSession) {
+ // TODO : clear session's event source when the session gets removed.
+ mKeyMessageEvents.Forward(aSession->KeyMessageEvent());
+ mKeyChangeEvents.Forward(aSession->KeyChangeEvent());
+ mExpirationEvents.Forward(aSession->ExpirationEvent());
+}
+
+MFCDMSession* MFCDMParent::GetSession(const nsString& aSessionId) {
+ AssertOnManagerThread();
+ auto iter = mSessions.find(aSessionId);
+ if (iter == mSessions.end()) {
+ return nullptr;
+ }
+ return iter->second.get();
+}
+
+already_AddRefed<MFCDMProxy> MFCDMParent::GetMFCDMProxy() {
+ if (!mCDM) {
+ return nullptr;
+ }
+ RefPtr<MFCDMProxy> proxy = new MFCDMProxy(mCDM.Get());
+ return proxy.forget();
+}
+
+#undef MFCDM_REJECT_IF_FAILED
+#undef MFCDM_REJECT_IF
+#undef MFCDM_RETURN_IF_FAILED
+#undef MFCDM_PARENT_SLOG
+#undef MFCDM_PARENT_LOG
+
+} // namespace mozilla
diff --git a/dom/media/ipc/MFCDMParent.h b/dom/media/ipc/MFCDMParent.h
new file mode 100644
index 0000000000..c0696d5edc
--- /dev/null
+++ b/dom/media/ipc/MFCDMParent.h
@@ -0,0 +1,116 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_IPC_MFCDMPARENT_H_
+#define DOM_MEDIA_IPC_MFCDMPARENT_H_
+
+#include <wrl.h>
+
+#include "mozilla/Assertions.h"
+#include "mozilla/PMFCDMParent.h"
+#include "MFCDMExtra.h"
+#include "MFCDMSession.h"
+#include "MFPMPHostWrapper.h"
+#include "RemoteDecoderManagerParent.h"
+
+namespace mozilla {
+
+class MFCDMProxy;
+
+/**
+ * MFCDMParent is a wrapper class for the Media Foundation CDM in the utility
+ * process.
+ * It's responsible to create and manage a CDM and its sessions, and acts as a
+ * proxy to the Media Foundation interfaces
+ * (https://learn.microsoft.com/en-us/windows/win32/api/mfcontentdecryptionmodule/)
+ * by accepting calls from and calling back to MFCDMChild in the content
+ * process.
+ */
+class MFCDMParent final : public PMFCDMParent {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MFCDMParent);
+
+ MFCDMParent(const nsAString& aKeySystem, RemoteDecoderManagerParent* aManager,
+ nsISerialEventTarget* aManagerThread);
+
+ static MFCDMParent* GetCDMById(uint64_t aId) {
+ MOZ_ASSERT(sRegisteredCDMs.Contains(aId));
+ return sRegisteredCDMs.Get(aId);
+ }
+ uint64_t Id() const { return mId; }
+
+ mozilla::ipc::IPCResult RecvGetCapabilities(
+ GetCapabilitiesResolver&& aResolver);
+
+ mozilla::ipc::IPCResult RecvInit(const MFCDMInitParamsIPDL& aParams,
+ InitResolver&& aResolver);
+
+ mozilla::ipc::IPCResult RecvCreateSessionAndGenerateRequest(
+ const MFCDMCreateSessionParamsIPDL& aParams,
+ CreateSessionAndGenerateRequestResolver&& aResolver);
+
+ mozilla::ipc::IPCResult RecvLoadSession(
+ const KeySystemConfig::SessionType& aSessionType,
+ const nsString& aSessionId, LoadSessionResolver&& aResolver);
+
+ mozilla::ipc::IPCResult RecvUpdateSession(
+ const nsString& aSessionId, const CopyableTArray<uint8_t>& aResponse,
+ UpdateSessionResolver&& aResolver);
+
+ mozilla::ipc::IPCResult RecvCloseSession(const nsString& aSessionId,
+ UpdateSessionResolver&& aResolver);
+
+ mozilla::ipc::IPCResult RecvRemoveSession(const nsString& aSessionId,
+ UpdateSessionResolver&& aResolver);
+
+ nsISerialEventTarget* ManagerThread() { return mManagerThread; }
+ void AssertOnManagerThread() const {
+ MOZ_ASSERT(mManagerThread->IsOnCurrentThread());
+ }
+
+ already_AddRefed<MFCDMProxy> GetMFCDMProxy();
+
+ void Destroy();
+
+ private:
+ ~MFCDMParent() { Unregister(); }
+
+ HRESULT LoadFactory();
+
+ void Register();
+ void Unregister();
+
+ void ConnectSessionEvents(MFCDMSession* aSession);
+
+ MFCDMSession* GetSession(const nsString& aSessionId);
+
+ nsString mKeySystem;
+
+ const RefPtr<RemoteDecoderManagerParent> mManager;
+ const RefPtr<nsISerialEventTarget> mManagerThread;
+
+ static inline nsTHashMap<nsUint64HashKey, MFCDMParent*> sRegisteredCDMs;
+
+ static inline uint64_t sNextId = 1;
+ const uint64_t mId;
+
+ RefPtr<MFCDMParent> mIPDLSelfRef;
+ Microsoft::WRL::ComPtr<IMFContentDecryptionModuleFactory> mFactory;
+ Microsoft::WRL::ComPtr<IMFContentDecryptionModule> mCDM;
+ Microsoft::WRL::ComPtr<MFPMPHostWrapper> mPMPHostWrapper;
+
+ std::map<nsString, UniquePtr<MFCDMSession>> mSessions;
+
+ MediaEventForwarder<MFCDMKeyMessage> mKeyMessageEvents;
+ MediaEventForwarder<MFCDMKeyStatusChange> mKeyChangeEvents;
+ MediaEventForwarder<MFCDMKeyExpiration> mExpirationEvents;
+
+ MediaEventListener mKeyMessageListener;
+ MediaEventListener mKeyChangeListener;
+ MediaEventListener mExpirationListener;
+};
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_IPC_MFCDMPARENT_H_
diff --git a/dom/media/ipc/MFCDMSerializers.h b/dom/media/ipc/MFCDMSerializers.h
new file mode 100644
index 0000000000..6778d755a4
--- /dev/null
+++ b/dom/media/ipc/MFCDMSerializers.h
@@ -0,0 +1,52 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_IPC_MFCDMSERIALIZERS_H_
+#define DOM_MEDIA_IPC_MFCDMSERIALIZERS_H_
+
+#include "ipc/EnumSerializer.h"
+#include "MediaData.h"
+#include "mozilla/KeySystemConfig.h"
+#include "mozilla/dom/MediaKeyMessageEventBinding.h"
+#include "mozilla/dom/MediaKeyStatusMapBinding.h"
+
+namespace IPC {
+
+template <>
+struct ParamTraits<mozilla::KeySystemConfig::Requirement>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::KeySystemConfig::Requirement,
+ mozilla::KeySystemConfig::Requirement::Required,
+ mozilla::KeySystemConfig::Requirement::NotAllowed> {};
+
+template <>
+struct ParamTraits<mozilla::KeySystemConfig::SessionType>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::KeySystemConfig::SessionType,
+ mozilla::KeySystemConfig::SessionType::Temporary,
+ mozilla::KeySystemConfig::SessionType::PersistentLicense> {};
+
+template <>
+struct ParamTraits<mozilla::CryptoScheme>
+ : public ContiguousEnumSerializerInclusive<mozilla::CryptoScheme,
+ mozilla::CryptoScheme::None,
+ mozilla::CryptoScheme::Cbcs> {};
+
+template <>
+struct ParamTraits<mozilla::dom::MediaKeyMessageType>
+ : public ContiguousEnumSerializer<
+ mozilla::dom::MediaKeyMessageType,
+ mozilla::dom::MediaKeyMessageType::License_request,
+ mozilla::dom::MediaKeyMessageType::EndGuard_> {};
+
+template <>
+struct ParamTraits<mozilla::dom::MediaKeyStatus>
+ : public ContiguousEnumSerializer<mozilla::dom::MediaKeyStatus,
+ mozilla::dom::MediaKeyStatus::Usable,
+ mozilla::dom::MediaKeyStatus::EndGuard_> {
+};
+
+} // namespace IPC
+
+#endif // DOM_MEDIA_IPC_MFCDMSERIALIZERS_H_
diff --git a/dom/media/ipc/MFMediaEngineChild.cpp b/dom/media/ipc/MFMediaEngineChild.cpp
new file mode 100644
index 0000000000..8c5f8b286d
--- /dev/null
+++ b/dom/media/ipc/MFMediaEngineChild.cpp
@@ -0,0 +1,394 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MFMediaEngineChild.h"
+
+#include "MFMediaEngineUtils.h"
+#include "RemoteDecoderManagerChild.h"
+#include "mozilla/WindowsVersion.h"
+
+#ifdef MOZ_WMF_CDM
+# include "WMFCDMProxy.h"
+#endif
+
+namespace mozilla {
+
+#define CLOG(msg, ...) \
+ MOZ_LOG(gMFMediaEngineLog, LogLevel::Debug, \
+ ("MFMediaEngineChild=%p, Id=%" PRId64 ", " msg, this, this->Id(), \
+ ##__VA_ARGS__))
+
+#define WLOG(msg, ...) \
+ MOZ_LOG(gMFMediaEngineLog, LogLevel::Debug, \
+ ("MFMediaEngineWrapper=%p, Id=%" PRId64 ", " msg, this, this->Id(), \
+ ##__VA_ARGS__))
+
+#define WLOGV(msg, ...) \
+ MOZ_LOG(gMFMediaEngineLog, LogLevel::Verbose, \
+ ("MFMediaEngineWrapper=%p, Id=%" PRId64 ", " msg, this, this->Id(), \
+ ##__VA_ARGS__))
+
+using media::TimeUnit;
+
+MFMediaEngineChild::MFMediaEngineChild(MFMediaEngineWrapper* aOwner,
+ FrameStatistics* aFrameStats)
+ : mOwner(aOwner),
+ mManagerThread(RemoteDecoderManagerChild::GetManagerThread()),
+ mMediaEngineId(0 /* invalid id, will be initialized later */),
+ mFrameStats(WrapNotNull(aFrameStats)) {
+ if (mFrameStats->GetPresentedFrames() > 0) {
+ mAccumulatedPresentedFramesFromPrevEngine =
+ Some(mFrameStats->GetPresentedFrames());
+ }
+ if (mFrameStats->GetDroppedSinkFrames() > 0) {
+ mAccumulatedDroppedFramesFromPrevEngine =
+ Some(mFrameStats->GetDroppedSinkFrames());
+ }
+}
+
+RefPtr<GenericNonExclusivePromise> MFMediaEngineChild::Init(
+ bool aShouldPreload) {
+ if (!mManagerThread) {
+ return GenericNonExclusivePromise::CreateAndReject(NS_ERROR_FAILURE,
+ __func__);
+ }
+
+ if (!IsWin10OrLater()) {
+ CLOG("Only support MF media engine playback on Windows 10+");
+ return GenericNonExclusivePromise::CreateAndReject(NS_ERROR_FAILURE,
+ __func__);
+ }
+
+ CLOG("Init");
+ MOZ_ASSERT(mMediaEngineId == 0);
+ RefPtr<MFMediaEngineChild> self = this;
+ RemoteDecoderManagerChild::LaunchUtilityProcessIfNeeded(
+ RemoteDecodeIn::UtilityProcess_MFMediaEngineCDM)
+ ->Then(
+ mManagerThread, __func__,
+ [self, this, aShouldPreload](bool) {
+ RefPtr<RemoteDecoderManagerChild> manager =
+ RemoteDecoderManagerChild::GetSingleton(
+ RemoteDecodeIn::UtilityProcess_MFMediaEngineCDM);
+ if (!manager || !manager->CanSend()) {
+ CLOG("Manager not exists or can't send");
+ mInitPromiseHolder.RejectIfExists(NS_ERROR_FAILURE, __func__);
+ return;
+ }
+
+ mIPDLSelfRef = this;
+ Unused << manager->SendPMFMediaEngineConstructor(this);
+ MediaEngineInfoIPDL info(aShouldPreload);
+ SendInitMediaEngine(info)
+ ->Then(
+ mManagerThread, __func__,
+ [self, this](uint64_t aId) {
+ mInitEngineRequest.Complete();
+ // Id 0 is used to indicate error.
+ if (aId == 0) {
+ CLOG("Failed to initialize MFMediaEngineChild");
+ mInitPromiseHolder.RejectIfExists(NS_ERROR_FAILURE,
+ __func__);
+ return;
+ }
+ mMediaEngineId = aId;
+ CLOG("Initialized MFMediaEngineChild");
+ mInitPromiseHolder.ResolveIfExists(true, __func__);
+ },
+ [self,
+ this](const mozilla::ipc::ResponseRejectReason& aReason) {
+ mInitEngineRequest.Complete();
+ CLOG(
+ "Failed to initialize MFMediaEngineChild due to "
+ "IPC failure");
+ mInitPromiseHolder.RejectIfExists(NS_ERROR_FAILURE,
+ __func__);
+ })
+ ->Track(mInitEngineRequest);
+ },
+ [self, this](nsresult aResult) {
+ CLOG("SendInitMediaEngine Failed");
+ self->mInitPromiseHolder.Reject(NS_ERROR_FAILURE, __func__);
+ });
+ return mInitPromiseHolder.Ensure(__func__);
+}
+
+mozilla::ipc::IPCResult MFMediaEngineChild::RecvRequestSample(TrackType aType,
+ bool aIsEnough) {
+ AssertOnManagerThread();
+ if (!mOwner) {
+ return IPC_OK();
+ }
+ if (aType == TrackType::kVideoTrack) {
+ mOwner->NotifyEvent(aIsEnough ? ExternalEngineEvent::VideoEnough
+ : ExternalEngineEvent::RequestForVideo);
+ } else if (aType == TrackType::kAudioTrack) {
+ mOwner->NotifyEvent(aIsEnough ? ExternalEngineEvent::AudioEnough
+ : ExternalEngineEvent::RequestForAudio);
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult MFMediaEngineChild::RecvUpdateCurrentTime(
+ double aCurrentTimeInSecond) {
+ AssertOnManagerThread();
+ if (mOwner) {
+ mOwner->UpdateCurrentTime(aCurrentTimeInSecond);
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult MFMediaEngineChild::RecvNotifyEvent(
+ MFMediaEngineEvent aEvent) {
+ AssertOnManagerThread();
+ switch (aEvent) {
+ case MF_MEDIA_ENGINE_EVENT_FIRSTFRAMEREADY:
+ mOwner->NotifyEvent(ExternalEngineEvent::LoadedFirstFrame);
+ break;
+ case MF_MEDIA_ENGINE_EVENT_LOADEDDATA:
+ mOwner->NotifyEvent(ExternalEngineEvent::LoadedData);
+ break;
+ case MF_MEDIA_ENGINE_EVENT_WAITING:
+ mOwner->NotifyEvent(ExternalEngineEvent::Waiting);
+ break;
+ case MF_MEDIA_ENGINE_EVENT_SEEKED:
+ mOwner->NotifyEvent(ExternalEngineEvent::Seeked);
+ break;
+ case MF_MEDIA_ENGINE_EVENT_BUFFERINGSTARTED:
+ mOwner->NotifyEvent(ExternalEngineEvent::BufferingStarted);
+ break;
+ case MF_MEDIA_ENGINE_EVENT_BUFFERINGENDED:
+ mOwner->NotifyEvent(ExternalEngineEvent::BufferingEnded);
+ break;
+ case MF_MEDIA_ENGINE_EVENT_ENDED:
+ mOwner->NotifyEvent(ExternalEngineEvent::Ended);
+ break;
+ case MF_MEDIA_ENGINE_EVENT_PLAYING:
+ mOwner->NotifyEvent(ExternalEngineEvent::Playing);
+ break;
+ default:
+ NS_WARNING(
+ nsPrintfCString("Unhandled event=%s", MediaEngineEventToStr(aEvent))
+ .get());
+ break;
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult MFMediaEngineChild::RecvNotifyError(
+ const MediaResult& aError) {
+ AssertOnManagerThread();
+ mOwner->NotifyError(aError);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult MFMediaEngineChild::RecvUpdateStatisticData(
+ const StatisticData& aData) {
+ AssertOnManagerThread();
+ const uint64_t currentRenderedFrames = mFrameStats->GetPresentedFrames();
+ const uint64_t newRenderedFrames = GetUpdatedRenderedFrames(aData);
+ // Media engine won't tell us that which stage those dropped frames happened,
+ // so we treat all of them as the frames dropped in the a/v sync stage (sink).
+ const uint64_t currentDroppedSinkFrames = mFrameStats->GetDroppedSinkFrames();
+ const uint64_t newDroppedSinkFrames = GetUpdatedDroppedFrames(aData);
+ mFrameStats->Accumulate({0, 0, newRenderedFrames - currentRenderedFrames, 0,
+ newDroppedSinkFrames - currentDroppedSinkFrames, 0});
+ CLOG("Update statictis data (rendered %" PRIu64 " -> %" PRIu64
+ ", dropped %" PRIu64 " -> %" PRIu64 ")",
+ currentRenderedFrames, mFrameStats->GetPresentedFrames(),
+ currentDroppedSinkFrames, mFrameStats->GetDroppedSinkFrames());
+ MOZ_ASSERT(mFrameStats->GetPresentedFrames() >= currentRenderedFrames);
+ MOZ_ASSERT(mFrameStats->GetDroppedSinkFrames() >= currentDroppedSinkFrames);
+ return IPC_OK();
+}
+
+uint64_t MFMediaEngineChild::GetUpdatedRenderedFrames(
+ const StatisticData& aData) {
+ return mAccumulatedPresentedFramesFromPrevEngine
+ ? (aData.renderedFrames() +
+ *mAccumulatedPresentedFramesFromPrevEngine)
+ : aData.renderedFrames();
+}
+
+uint64_t MFMediaEngineChild::GetUpdatedDroppedFrames(
+ const StatisticData& aData) {
+ return mAccumulatedDroppedFramesFromPrevEngine
+ ? (aData.droppedFrames() +
+ *mAccumulatedDroppedFramesFromPrevEngine)
+ : aData.droppedFrames();
+}
+
+void MFMediaEngineChild::OwnerDestroyed() {
+ Unused << ManagerThread()->Dispatch(NS_NewRunnableFunction(
+ "MFMediaEngineChild::OwnerDestroy", [self = RefPtr{this}, this] {
+ self->mOwner = nullptr;
+ // Ask to destroy IPDL.
+ if (CanSend()) {
+ MFMediaEngineChild::Send__delete__(this);
+ }
+ }));
+}
+
+void MFMediaEngineChild::IPDLActorDestroyed() {
+ AssertOnManagerThread();
+ if (!mShutdown) {
+ CLOG("Destroyed actor without shutdown, remote process has crashed!");
+ mOwner->NotifyError(NS_ERROR_DOM_MEDIA_REMOTE_DECODER_CRASHED_MF_CDM_ERR);
+ }
+ mIPDLSelfRef = nullptr;
+}
+
+void MFMediaEngineChild::Shutdown() {
+ AssertOnManagerThread();
+ if (mShutdown) {
+ return;
+ }
+ SendShutdown();
+ mInitPromiseHolder.RejectIfExists(NS_ERROR_FAILURE, __func__);
+ mInitEngineRequest.DisconnectIfExists();
+ mShutdown = true;
+}
+
+MFMediaEngineWrapper::MFMediaEngineWrapper(ExternalEngineStateMachine* aOwner,
+ FrameStatistics* aFrameStats)
+ : ExternalPlaybackEngine(aOwner),
+ mEngine(new MFMediaEngineChild(this, aFrameStats)),
+ mCurrentTimeInSecond(0.0) {}
+
+RefPtr<GenericNonExclusivePromise> MFMediaEngineWrapper::Init(
+ bool aShouldPreload) {
+ WLOG("Init");
+ return mEngine->Init(aShouldPreload);
+}
+
+MFMediaEngineWrapper::~MFMediaEngineWrapper() { mEngine->OwnerDestroyed(); }
+
+void MFMediaEngineWrapper::Play() {
+ WLOG("Play");
+ MOZ_ASSERT(IsInited());
+ Unused << ManagerThread()->Dispatch(
+ NS_NewRunnableFunction("MFMediaEngineWrapper::Play",
+ [engine = mEngine] { engine->SendPlay(); }));
+}
+
+void MFMediaEngineWrapper::Pause() {
+ WLOG("Pause");
+ MOZ_ASSERT(IsInited());
+ Unused << ManagerThread()->Dispatch(
+ NS_NewRunnableFunction("MFMediaEngineWrapper::Pause",
+ [engine = mEngine] { engine->SendPause(); }));
+}
+
+void MFMediaEngineWrapper::Seek(const TimeUnit& aTargetTime) {
+ auto currentTimeInSecond = aTargetTime.ToSeconds();
+ mCurrentTimeInSecond = currentTimeInSecond;
+ WLOG("Seek to %f", currentTimeInSecond);
+ MOZ_ASSERT(IsInited());
+ Unused << ManagerThread()->Dispatch(NS_NewRunnableFunction(
+ "MFMediaEngineWrapper::Seek", [engine = mEngine, currentTimeInSecond] {
+ engine->SendSeek(currentTimeInSecond);
+ }));
+}
+
+void MFMediaEngineWrapper::Shutdown() {
+ WLOG("Shutdown");
+ Unused << ManagerThread()->Dispatch(
+ NS_NewRunnableFunction("MFMediaEngineWrapper::Shutdown",
+ [engine = mEngine] { engine->Shutdown(); }));
+}
+
+void MFMediaEngineWrapper::SetPlaybackRate(double aPlaybackRate) {
+ WLOG("Set playback rate %f", aPlaybackRate);
+ MOZ_ASSERT(IsInited());
+ Unused << ManagerThread()->Dispatch(
+ NS_NewRunnableFunction("MFMediaEngineWrapper::SetPlaybackRate",
+ [engine = mEngine, aPlaybackRate] {
+ engine->SendSetPlaybackRate(aPlaybackRate);
+ }));
+}
+
+void MFMediaEngineWrapper::SetVolume(double aVolume) {
+ WLOG("Set volume %f", aVolume);
+ MOZ_ASSERT(IsInited());
+ Unused << ManagerThread()->Dispatch(NS_NewRunnableFunction(
+ "MFMediaEngineWrapper::SetVolume",
+ [engine = mEngine, aVolume] { engine->SendSetVolume(aVolume); }));
+}
+
+void MFMediaEngineWrapper::SetLooping(bool aLooping) {
+ WLOG("Set looping %d", aLooping);
+ MOZ_ASSERT(IsInited());
+ Unused << ManagerThread()->Dispatch(NS_NewRunnableFunction(
+ "MFMediaEngineWrapper::SetLooping",
+ [engine = mEngine, aLooping] { engine->SendSetLooping(aLooping); }));
+}
+
+void MFMediaEngineWrapper::SetPreservesPitch(bool aPreservesPitch) {
+ // Media Engine doesn't support this.
+}
+
+void MFMediaEngineWrapper::NotifyEndOfStream(TrackInfo::TrackType aType) {
+ WLOG("NotifyEndOfStream, type=%s", TrackTypeToStr(aType));
+ MOZ_ASSERT(IsInited());
+ Unused << ManagerThread()->Dispatch(NS_NewRunnableFunction(
+ "MFMediaEngineWrapper::NotifyEndOfStream",
+ [engine = mEngine, aType] { engine->SendNotifyEndOfStream(aType); }));
+}
+
+void MFMediaEngineWrapper::SetMediaInfo(const MediaInfo& aInfo) {
+ WLOG("SetMediaInfo, hasAudio=%d, hasVideo=%d, encrypted=%d", aInfo.HasAudio(),
+ aInfo.HasVideo(), aInfo.IsEncrypted());
+ MOZ_ASSERT(IsInited());
+ Unused << ManagerThread()->Dispatch(NS_NewRunnableFunction(
+ "MFMediaEngineWrapper::SetMediaInfo", [engine = mEngine, aInfo] {
+ MediaInfoIPDL info(aInfo.HasAudio() ? Some(aInfo.mAudio) : Nothing(),
+ aInfo.HasVideo() ? Some(aInfo.mVideo) : Nothing());
+ engine->SendNotifyMediaInfo(info);
+ }));
+}
+
+bool MFMediaEngineWrapper::SetCDMProxy(CDMProxy* aProxy) {
+#ifdef MOZ_WMF_CDM
+ WMFCDMProxy* proxy = aProxy->AsWMFCDMProxy();
+ if (!proxy) {
+ WLOG("Only WFMCDM Proxy is supported for the media engine!");
+ return false;
+ }
+
+ const uint64_t proxyId = proxy->GetCDMProxyId();
+ WLOG("SetCDMProxy, CDM-Id=%" PRIu64, proxyId);
+ MOZ_ASSERT(IsInited());
+ Unused << ManagerThread()->Dispatch(NS_NewRunnableFunction(
+ "MFMediaEngineWrapper::SetCDMProxy",
+ [engine = mEngine, proxyId] { engine->SendSetCDMProxyId(proxyId); }));
+ return true;
+#else
+ return false;
+#endif
+}
+
+TimeUnit MFMediaEngineWrapper::GetCurrentPosition() {
+ return TimeUnit::FromSeconds(mCurrentTimeInSecond);
+}
+
+void MFMediaEngineWrapper::UpdateCurrentTime(double aCurrentTimeInSecond) {
+ AssertOnManagerThread();
+ WLOGV("Update current time %f", aCurrentTimeInSecond);
+ mCurrentTimeInSecond = aCurrentTimeInSecond;
+ NotifyEvent(ExternalEngineEvent::Timeupdate);
+}
+
+void MFMediaEngineWrapper::NotifyEvent(ExternalEngineEvent aEvent) {
+ AssertOnManagerThread();
+ WLOGV("Received event %s", ExternalEngineEventToStr(aEvent));
+ mOwner->NotifyEvent(aEvent);
+}
+
+void MFMediaEngineWrapper::NotifyError(const MediaResult& aError) {
+ AssertOnManagerThread();
+ WLOG("Received error: %s", aError.Description().get());
+ mOwner->NotifyError(aError);
+}
+
+} // namespace mozilla
diff --git a/dom/media/ipc/MFMediaEngineChild.h b/dom/media/ipc/MFMediaEngineChild.h
new file mode 100644
index 0000000000..45e7422ddf
--- /dev/null
+++ b/dom/media/ipc/MFMediaEngineChild.h
@@ -0,0 +1,136 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_IPC_MFMEDIAENGINECHILD_H_
+#define DOM_MEDIA_IPC_MFMEDIAENGINECHILD_H_
+
+#include "ExternalEngineStateMachine.h"
+#include "MFMediaEngineUtils.h"
+#include "TimeUnits.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/PMFMediaEngineChild.h"
+#include "mozilla/NotNull.h"
+
+namespace mozilla {
+
+class MFMediaEngineWrapper;
+
+/**
+ * MFMediaEngineChild is a wrapper class for a MediaEngine in the content
+ * process. It communicates with MFMediaEngineParent in the remote process by
+ * using IPDL interfaces to send commands to the MediaEngine.
+ * https://docs.microsoft.com/en-us/windows/win32/api/mfmediaengine/nn-mfmediaengine-imfmediaengine
+ */
+class MFMediaEngineChild final : public PMFMediaEngineChild {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MFMediaEngineChild);
+
+ MFMediaEngineChild(MFMediaEngineWrapper* aOwner,
+ FrameStatistics* aFrameStats);
+
+ void OwnerDestroyed();
+ void IPDLActorDestroyed();
+
+ RefPtr<GenericNonExclusivePromise> Init(bool aShouldPreload);
+ void Shutdown();
+
+ // Methods for PMFMediaEngineChild
+ mozilla::ipc::IPCResult RecvRequestSample(TrackInfo::TrackType aType,
+ bool aIsEnough);
+ mozilla::ipc::IPCResult RecvUpdateCurrentTime(double aCurrentTimeInSecond);
+ mozilla::ipc::IPCResult RecvNotifyEvent(MFMediaEngineEvent aEvent);
+ mozilla::ipc::IPCResult RecvNotifyError(const MediaResult& aError);
+ mozilla::ipc::IPCResult RecvUpdateStatisticData(const StatisticData& aData);
+
+ nsISerialEventTarget* ManagerThread() { return mManagerThread; }
+ void AssertOnManagerThread() const {
+ MOZ_ASSERT(mManagerThread->IsOnCurrentThread());
+ }
+
+ uint64_t Id() const { return mMediaEngineId; }
+
+ private:
+ ~MFMediaEngineChild() = default;
+
+ uint64_t GetUpdatedRenderedFrames(const StatisticData& aData);
+ uint64_t GetUpdatedDroppedFrames(const StatisticData& aData);
+
+ // Only modified on the manager thread.
+ MFMediaEngineWrapper* MOZ_NON_OWNING_REF mOwner;
+
+ const nsCOMPtr<nsISerialEventTarget> mManagerThread;
+
+ // This represents an unique Id to indentify the media engine in the remote
+ // process. Zero is used for the status before initializaing Id from the
+ // remote process.
+ // Modified on the manager thread, and read on other threads.
+ Atomic<uint64_t> mMediaEngineId;
+
+ RefPtr<MFMediaEngineChild> mIPDLSelfRef;
+
+ MozPromiseHolder<GenericNonExclusivePromise> mInitPromiseHolder;
+ MozPromiseRequestHolder<InitMediaEnginePromise> mInitEngineRequest;
+
+ // This is guaranteed always being alive in our lifetime.
+ NotNull<FrameStatistics*> const MOZ_NON_OWNING_REF mFrameStats;
+
+ bool mShutdown = false;
+
+ // Whenever the remote media engine process crashes, we will create a new
+ // engine child to rebuild the connection. These engine child shares the same
+ // frame stats data so we need to keep accumulate same data from previous
+ // engine.
+ Maybe<uint64_t> mAccumulatedPresentedFramesFromPrevEngine;
+ Maybe<uint64_t> mAccumulatedDroppedFramesFromPrevEngine;
+};
+
+/**
+ * MFMediaEngineWrapper acts as an external playback engine, which is
+ * implemented by using the Media Foundation Media Engine. It holds hold an
+ * actor used to communicate with the engine in the remote process. Its methods
+ * are all thread-safe.
+ */
+class MFMediaEngineWrapper final : public ExternalPlaybackEngine {
+ public:
+ MFMediaEngineWrapper(ExternalEngineStateMachine* aOwner,
+ FrameStatistics* aFrameStats);
+ ~MFMediaEngineWrapper();
+
+ // Methods for ExternalPlaybackEngine
+ RefPtr<GenericNonExclusivePromise> Init(bool aShouldPreload) override;
+ void Play() override;
+ void Pause() override;
+ void Seek(const media::TimeUnit& aTargetTime) override;
+ void Shutdown() override;
+ void SetPlaybackRate(double aPlaybackRate) override;
+ void SetVolume(double aVolume) override;
+ void SetLooping(bool aLooping) override;
+ void SetPreservesPitch(bool aPreservesPitch) override;
+ media::TimeUnit GetCurrentPosition() override;
+ void NotifyEndOfStream(TrackInfo::TrackType aType) override;
+ uint64_t Id() const override { return mEngine->Id(); }
+ void SetMediaInfo(const MediaInfo& aInfo) override;
+ bool SetCDMProxy(CDMProxy* aProxy) override;
+
+ nsISerialEventTarget* ManagerThread() { return mEngine->ManagerThread(); }
+ void AssertOnManagerThread() const { mEngine->AssertOnManagerThread(); }
+
+ private:
+ friend class MFMediaEngineChild;
+
+ bool IsInited() const { return mEngine->Id() != 0; }
+ void UpdateCurrentTime(double aCurrentTimeInSecond);
+ void NotifyEvent(ExternalEngineEvent aEvent);
+ void NotifyError(const MediaResult& aError);
+
+ const RefPtr<MFMediaEngineChild> mEngine;
+
+ // The current time which we receive from the MediaEngine or set by the state
+ // machine when seeking.
+ std::atomic<double> mCurrentTimeInSecond;
+};
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_IPC_MFMEDIAENGINECHILD_H_
diff --git a/dom/media/ipc/MFMediaEngineParent.cpp b/dom/media/ipc/MFMediaEngineParent.cpp
new file mode 100644
index 0000000000..b3d2fbc1c2
--- /dev/null
+++ b/dom/media/ipc/MFMediaEngineParent.cpp
@@ -0,0 +1,715 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MFMediaEngineParent.h"
+
+#include <audiosessiontypes.h>
+#include <intsafe.h>
+#include <mfapi.h>
+
+#ifdef MOZ_WMF_CDM
+# include "MFCDMParent.h"
+# include "MFContentProtectionManager.h"
+#endif
+
+#include "MFMediaEngineExtension.h"
+#include "MFMediaEngineVideoStream.h"
+#include "MFMediaEngineUtils.h"
+#include "MFMediaEngineStream.h"
+#include "MFMediaSource.h"
+#include "RemoteDecoderManagerParent.h"
+#include "WMF.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/RemoteDecodeUtils.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/WindowsVersion.h"
+#include "mozilla/gfx/DeviceManagerDx.h"
+
+namespace mozilla {
+
+#define LOG(msg, ...) \
+ MOZ_LOG(gMFMediaEngineLog, LogLevel::Debug, \
+ ("MFMediaEngineParent=%p, Id=%" PRId64 ", " msg, this, this->Id(), \
+ ##__VA_ARGS__))
+
+using MediaEngineMap = nsTHashMap<nsUint64HashKey, MFMediaEngineParent*>;
+static StaticAutoPtr<MediaEngineMap> sMediaEngines;
+
+using Microsoft::WRL::ComPtr;
+using Microsoft::WRL::MakeAndInitialize;
+
+StaticMutex sMediaEnginesLock;
+
+static void RegisterMediaEngine(MFMediaEngineParent* aMediaEngine) {
+ MOZ_ASSERT(aMediaEngine);
+ StaticMutexAutoLock lock(sMediaEnginesLock);
+ if (!sMediaEngines) {
+ sMediaEngines = new MediaEngineMap();
+ }
+ sMediaEngines->InsertOrUpdate(aMediaEngine->Id(), aMediaEngine);
+}
+
+static void UnregisterMedieEngine(MFMediaEngineParent* aMediaEngine) {
+ StaticMutexAutoLock lock(sMediaEnginesLock);
+ if (sMediaEngines) {
+ sMediaEngines->Remove(aMediaEngine->Id());
+ }
+}
+
+/* static */
+MFMediaEngineParent* MFMediaEngineParent::GetMediaEngineById(uint64_t aId) {
+ StaticMutexAutoLock lock(sMediaEnginesLock);
+ return sMediaEngines->Get(aId);
+}
+
+MFMediaEngineParent::MFMediaEngineParent(RemoteDecoderManagerParent* aManager,
+ nsISerialEventTarget* aManagerThread)
+ : mMediaEngineId(++sMediaEngineIdx),
+ mManager(aManager),
+ mManagerThread(aManagerThread) {
+ MOZ_ASSERT(aManager);
+ MOZ_ASSERT(aManagerThread);
+ MOZ_ASSERT(mMediaEngineId != 0);
+ MOZ_ASSERT(XRE_IsUtilityProcess());
+ MOZ_ASSERT(GetCurrentSandboxingKind() ==
+ ipc::SandboxingKind::MF_MEDIA_ENGINE_CDM);
+ LOG("Created MFMediaEngineParent");
+ RegisterMediaEngine(this);
+ mIPDLSelfRef = this;
+ CreateMediaEngine();
+}
+
+MFMediaEngineParent::~MFMediaEngineParent() {
+ LOG("Destoryed MFMediaEngineParent");
+ DestroyEngineIfExists();
+ UnregisterMedieEngine(this);
+}
+
+void MFMediaEngineParent::DestroyEngineIfExists(
+ const Maybe<MediaResult>& aError) {
+ LOG("DestroyEngineIfExists, hasError=%d", aError.isSome());
+ ENGINE_MARKER("MFMediaEngineParent::DestroyEngineIfExists");
+ mMediaEngineNotify = nullptr;
+ mMediaEngineExtension = nullptr;
+ if (mMediaSource) {
+ mMediaSource->ShutdownTaskQueue();
+ mMediaSource = nullptr;
+ }
+ if (mMediaEngine) {
+ mMediaEngine->Shutdown();
+ mMediaEngine = nullptr;
+ }
+ mMediaEngineEventListener.DisconnectIfExists();
+ mRequestSampleListener.DisconnectIfExists();
+ if (mDXGIDeviceManager) {
+ mDXGIDeviceManager = nullptr;
+ wmf::MFUnlockDXGIDeviceManager();
+ }
+ if (mVirtualVideoWindow) {
+ DestroyWindow(mVirtualVideoWindow);
+ }
+ if (aError) {
+ Unused << SendNotifyError(*aError);
+ }
+}
+
+void MFMediaEngineParent::CreateMediaEngine() {
+ LOG("CreateMediaEngine");
+ auto errorExit = MakeScopeExit([&] {
+ MediaResult error(NS_ERROR_DOM_MEDIA_FATAL_ERR, "Failed to create engine");
+ DestroyEngineIfExists(Some(error));
+ });
+
+ if (!wmf::MediaFoundationInitializer::HasInitialized()) {
+ NS_WARNING("Failed to initialize media foundation");
+ return;
+ }
+
+ InitializeDXGIDeviceManager();
+ InitializeVirtualVideoWindow();
+
+ // Create an attribute and set mandatory information that are required for
+ // a media engine creation.
+ ComPtr<IMFAttributes> creationAttributes;
+ RETURN_VOID_IF_FAILED(wmf::MFCreateAttributes(&creationAttributes, 6));
+ RETURN_VOID_IF_FAILED(
+ MakeAndInitialize<MFMediaEngineNotify>(&mMediaEngineNotify));
+ mMediaEngineEventListener = mMediaEngineNotify->MediaEngineEvent().Connect(
+ mManagerThread, this, &MFMediaEngineParent::HandleMediaEngineEvent);
+ RETURN_VOID_IF_FAILED(creationAttributes->SetUnknown(
+ MF_MEDIA_ENGINE_CALLBACK, mMediaEngineNotify.Get()));
+ RETURN_VOID_IF_FAILED(creationAttributes->SetUINT32(
+ MF_MEDIA_ENGINE_AUDIO_CATEGORY, AudioCategory_Media));
+ RETURN_VOID_IF_FAILED(
+ MakeAndInitialize<MFMediaEngineExtension>(&mMediaEngineExtension));
+ RETURN_VOID_IF_FAILED(creationAttributes->SetUnknown(
+ MF_MEDIA_ENGINE_EXTENSION, mMediaEngineExtension.Get()));
+ RETURN_VOID_IF_FAILED(
+ creationAttributes->SetUINT32(MF_MEDIA_ENGINE_CONTENT_PROTECTION_FLAGS,
+ MF_MEDIA_ENGINE_ENABLE_PROTECTED_CONTENT));
+ if (mDXGIDeviceManager) {
+ RETURN_VOID_IF_FAILED(creationAttributes->SetUnknown(
+ MF_MEDIA_ENGINE_DXGI_MANAGER, mDXGIDeviceManager.Get()));
+ }
+ if (mVirtualVideoWindow) {
+ RETURN_VOID_IF_FAILED(creationAttributes->SetUINT64(
+ MF_MEDIA_ENGINE_OPM_HWND,
+ reinterpret_cast<uint64_t>(mVirtualVideoWindow)));
+ }
+
+ ComPtr<IMFMediaEngineClassFactory> factory;
+ RETURN_VOID_IF_FAILED(CoCreateInstance(CLSID_MFMediaEngineClassFactory,
+ nullptr, CLSCTX_INPROC_SERVER,
+ IID_PPV_ARGS(&factory)));
+ const bool isLowLatency =
+ StaticPrefs::media_wmf_low_latency_enabled() &&
+ !StaticPrefs::media_wmf_low_latency_force_disabled();
+ static const DWORD MF_MEDIA_ENGINE_DEFAULT = 0;
+ RETURN_VOID_IF_FAILED(factory->CreateInstance(
+ isLowLatency ? MF_MEDIA_ENGINE_REAL_TIME_MODE : MF_MEDIA_ENGINE_DEFAULT,
+ creationAttributes.Get(), &mMediaEngine));
+
+ LOG("Created media engine successfully");
+ mIsCreatedMediaEngine = true;
+ ENGINE_MARKER("MFMediaEngineParent::CreatedMediaEngine");
+ errorExit.release();
+}
+
+void MFMediaEngineParent::InitializeDXGIDeviceManager() {
+ auto* deviceManager = gfx::DeviceManagerDx::Get();
+ if (!deviceManager) {
+ return;
+ }
+ RefPtr<ID3D11Device> d3d11Device = deviceManager->CreateMediaEngineDevice();
+ if (!d3d11Device) {
+ return;
+ }
+
+ auto errorExit = MakeScopeExit([&] {
+ mDXGIDeviceManager = nullptr;
+ wmf::MFUnlockDXGIDeviceManager();
+ });
+ UINT deviceResetToken;
+ RETURN_VOID_IF_FAILED(
+ wmf::MFLockDXGIDeviceManager(&deviceResetToken, &mDXGIDeviceManager));
+ RETURN_VOID_IF_FAILED(
+ mDXGIDeviceManager->ResetDevice(d3d11Device.get(), deviceResetToken));
+ LOG("Initialized DXGI manager");
+ errorExit.release();
+}
+
+void MFMediaEngineParent::InitializeVirtualVideoWindow() {
+ static ATOM sVideoWindowClass = 0;
+ if (!sVideoWindowClass) {
+ WNDCLASS wnd{};
+ wnd.lpszClassName = L"MFMediaEngine";
+ wnd.hInstance = nullptr;
+ wnd.lpfnWndProc = DefWindowProc;
+ sVideoWindowClass = RegisterClass(&wnd);
+ }
+ if (!sVideoWindowClass) {
+ HRESULT hr = HRESULT_FROM_WIN32(GetLastError());
+ LOG("Failed to register video window class: %lX", hr);
+ return;
+ }
+ mVirtualVideoWindow =
+ CreateWindowEx(WS_EX_NOPARENTNOTIFY | WS_EX_LAYERED | WS_EX_TRANSPARENT |
+ WS_EX_NOREDIRECTIONBITMAP,
+ reinterpret_cast<wchar_t*>(sVideoWindowClass), L"",
+ WS_POPUP | WS_DISABLED | WS_CLIPSIBLINGS, 0, 0, 1, 1,
+ nullptr, nullptr, nullptr, nullptr);
+ if (!mVirtualVideoWindow) {
+ HRESULT hr = HRESULT_FROM_WIN32(GetLastError());
+ LOG("Failed to create virtual window: %lX", hr);
+ return;
+ }
+ LOG("Initialized virtual window");
+}
+
+#ifndef ENSURE_EVENT_DISPATCH_DURING_PLAYING
+# define ENSURE_EVENT_DISPATCH_DURING_PLAYING(event) \
+ do { \
+ if (mMediaEngine->IsPaused()) { \
+ LOG("Ignore incorrect '%s' during pausing!", event); \
+ return; \
+ } \
+ } while (false)
+#endif
+
+void MFMediaEngineParent::HandleMediaEngineEvent(
+ MFMediaEngineEventWrapper aEvent) {
+ AssertOnManagerThread();
+ ENGINE_MARKER_TEXT(
+ "MFMediaEngineParent::HandleMediaEngineEvent",
+ nsPrintfCString("%s", MediaEngineEventToStr(aEvent.mEvent)));
+ switch (aEvent.mEvent) {
+ case MF_MEDIA_ENGINE_EVENT_ERROR: {
+ MOZ_ASSERT(aEvent.mParam1 && aEvent.mParam2);
+ auto error = static_cast<MF_MEDIA_ENGINE_ERR>(*aEvent.mParam1);
+ auto result = static_cast<HRESULT>(*aEvent.mParam2);
+ NotifyError(error, result);
+ break;
+ }
+ case MF_MEDIA_ENGINE_EVENT_FORMATCHANGE:
+ case MF_MEDIA_ENGINE_EVENT_FIRSTFRAMEREADY: {
+ if (mMediaEngine->HasVideo()) {
+ EnsureDcompSurfaceHandle();
+ }
+ [[fallthrough]];
+ }
+ case MF_MEDIA_ENGINE_EVENT_LOADEDDATA:
+ case MF_MEDIA_ENGINE_EVENT_WAITING:
+ case MF_MEDIA_ENGINE_EVENT_SEEKED:
+ case MF_MEDIA_ENGINE_EVENT_BUFFERINGSTARTED:
+ case MF_MEDIA_ENGINE_EVENT_BUFFERINGENDED:
+ Unused << SendNotifyEvent(aEvent.mEvent);
+ break;
+ case MF_MEDIA_ENGINE_EVENT_PLAYING:
+ ENSURE_EVENT_DISPATCH_DURING_PLAYING(
+ MediaEngineEventToStr(aEvent.mEvent));
+ Unused << SendNotifyEvent(aEvent.mEvent);
+ break;
+ case MF_MEDIA_ENGINE_EVENT_ENDED: {
+ ENSURE_EVENT_DISPATCH_DURING_PLAYING(
+ MediaEngineEventToStr(aEvent.mEvent));
+ Unused << SendNotifyEvent(aEvent.mEvent);
+ UpdateStatisticsData();
+ break;
+ }
+ case MF_MEDIA_ENGINE_EVENT_TIMEUPDATE: {
+ auto currentTimeInSeconds = mMediaEngine->GetCurrentTime();
+ Unused << SendUpdateCurrentTime(currentTimeInSeconds);
+ UpdateStatisticsData();
+ break;
+ }
+ default:
+ LOG("Unhandled event=%s", MediaEngineEventToStr(aEvent.mEvent));
+ break;
+ }
+}
+
+void MFMediaEngineParent::NotifyError(MF_MEDIA_ENGINE_ERR aError,
+ HRESULT aResult) {
+ // TODO : handle HRESULT 0x8004CD12, DRM_E_TEE_INVALID_HWDRM_STATE, which can
+ // happen during OS sleep/resume, or moving video to different graphics
+ // adapters.
+ if (aError == MF_MEDIA_ENGINE_ERR_NOERROR) {
+ return;
+ }
+ LOG("Notify error '%s', hr=%lx", MFMediaEngineErrorToStr(aError), aResult);
+ ENGINE_MARKER_TEXT(
+ "MFMediaEngineParent::NotifyError",
+ nsPrintfCString("%s, hr=%lx", MFMediaEngineErrorToStr(aError), aResult));
+ switch (aError) {
+ case MF_MEDIA_ENGINE_ERR_ABORTED:
+ case MF_MEDIA_ENGINE_ERR_NETWORK:
+ // We ignore these two because we fetch data by ourselves.
+ return;
+ case MF_MEDIA_ENGINE_ERR_DECODE: {
+ MediaResult error(NS_ERROR_DOM_MEDIA_DECODE_ERR, "Decoder error");
+ Unused << SendNotifyError(error);
+ return;
+ }
+ case MF_MEDIA_ENGINE_ERR_SRC_NOT_SUPPORTED: {
+ MediaResult error(NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR,
+ "Source not supported");
+ Unused << SendNotifyError(error);
+ return;
+ }
+ case MF_MEDIA_ENGINE_ERR_ENCRYPTED: {
+ MediaResult error(NS_ERROR_DOM_MEDIA_FATAL_ERR, "Encrypted error");
+ Unused << SendNotifyError(error);
+ return;
+ }
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unsupported error");
+ return;
+ }
+}
+
+MFMediaEngineStreamWrapper* MFMediaEngineParent::GetMediaEngineStream(
+ TrackType aType, const CreateDecoderParams& aParam) {
+ // Has been shutdowned.
+ if (!mMediaSource) {
+ return nullptr;
+ }
+ LOG("Create a media engine decoder for %s", TrackTypeToStr(aType));
+ if (aType == TrackType::kAudioTrack) {
+ auto* stream = mMediaSource->GetAudioStream();
+ return new MFMediaEngineStreamWrapper(stream, stream->GetTaskQueue(),
+ aParam);
+ }
+ MOZ_ASSERT(aType == TrackType::kVideoTrack);
+ auto* stream = mMediaSource->GetVideoStream();
+ stream->AsVideoStream()->SetKnowsCompositor(aParam.mKnowsCompositor);
+ stream->AsVideoStream()->SetConfig(aParam.mConfig);
+ return new MFMediaEngineStreamWrapper(stream, stream->GetTaskQueue(), aParam);
+}
+
+mozilla::ipc::IPCResult MFMediaEngineParent::RecvInitMediaEngine(
+ const MediaEngineInfoIPDL& aInfo, InitMediaEngineResolver&& aResolver) {
+ AssertOnManagerThread();
+ if (!mIsCreatedMediaEngine) {
+ aResolver(0);
+ return IPC_OK();
+ }
+ // Metadata preload is controlled by content process side before creating
+ // media engine.
+ if (aInfo.preload()) {
+ // TODO : really need this?
+ Unused << mMediaEngine->SetPreload(MF_MEDIA_ENGINE_PRELOAD_AUTOMATIC);
+ }
+ aResolver(mMediaEngineId);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult MFMediaEngineParent::RecvNotifyMediaInfo(
+ const MediaInfoIPDL& aInfo) {
+ AssertOnManagerThread();
+ MOZ_ASSERT(mIsCreatedMediaEngine, "Hasn't created media engine?");
+ MOZ_ASSERT(!mMediaSource);
+
+ LOG("RecvNotifyMediaInfo");
+
+ auto errorExit = MakeScopeExit([&] {
+ MediaResult error(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ "Failed to create media source");
+ DestroyEngineIfExists(Some(error));
+ });
+
+ // Create media source and set it to the media engine.
+ NS_ENSURE_TRUE(
+ SUCCEEDED(MakeAndInitialize<MFMediaSource>(
+ &mMediaSource, aInfo.audioInfo(), aInfo.videoInfo(), mManagerThread)),
+ IPC_OK());
+
+ const bool isEncryted = mMediaSource->IsEncrypted();
+ ENGINE_MARKER("MFMediaEngineParent,CreatedMediaSource");
+ nsPrintfCString message(
+ "Created the media source, audio=%s, video=%s, encrypted-audio=%s, "
+ "encrypted-video=%s, isEncrypted=%d",
+ aInfo.audioInfo() ? aInfo.audioInfo()->mMimeType.BeginReading() : "none",
+ aInfo.videoInfo() ? aInfo.videoInfo()->mMimeType.BeginReading() : "none",
+ aInfo.audioInfo() && aInfo.audioInfo()->mCrypto.IsEncrypted() ? "yes"
+ : "no",
+ aInfo.videoInfo() && aInfo.videoInfo()->mCrypto.IsEncrypted() ? "yes"
+ : "no",
+ isEncryted);
+ LOG("%s", message.get());
+
+ mRequestSampleListener = mMediaSource->RequestSampleEvent().Connect(
+ mManagerThread, this, &MFMediaEngineParent::HandleRequestSample);
+ errorExit.release();
+
+#ifdef MOZ_WMF_CDM
+ if (isEncryted && !mContentProtectionManager) {
+ // We will set the source later when the CDM proxy is ready.
+ return IPC_OK();
+ }
+
+ if (isEncryted && mContentProtectionManager) {
+ auto* proxy = mContentProtectionManager->GetCDMProxy();
+ MOZ_ASSERT(proxy);
+ mMediaSource->SetCDMProxy(proxy);
+ }
+#endif
+
+ SetMediaSourceOnEngine();
+ return IPC_OK();
+}
+
+void MFMediaEngineParent::SetMediaSourceOnEngine() {
+ AssertOnManagerThread();
+ MOZ_ASSERT(mMediaSource);
+
+ auto errorExit = MakeScopeExit([&] {
+ MediaResult error(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ "Failed to set media source");
+ DestroyEngineIfExists(Some(error));
+ });
+
+ mMediaEngineExtension->SetMediaSource(mMediaSource.Get());
+
+ // We use the source scheme in order to let the media engine to load our
+ // custom source.
+ RETURN_VOID_IF_FAILED(
+ mMediaEngine->SetSource(SysAllocString(L"MFRendererSrc")));
+
+ LOG("Finished setup our custom media source to the media engine");
+ ENGINE_MARKER("MFMediaEngineParent,FinishedSetupMediaSource");
+ errorExit.release();
+}
+
+mozilla::ipc::IPCResult MFMediaEngineParent::RecvPlay() {
+ AssertOnManagerThread();
+ if (!mMediaEngine) {
+ LOG("Engine has been shutdowned!");
+ return IPC_OK();
+ }
+ LOG("Play, expected playback rate %f, default playback rate=%f",
+ mPlaybackRate, mMediaEngine->GetDefaultPlaybackRate());
+ ENGINE_MARKER("MFMediaEngineParent,Play");
+ NS_ENSURE_TRUE(SUCCEEDED(mMediaEngine->Play()), IPC_OK());
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult MFMediaEngineParent::RecvPause() {
+ AssertOnManagerThread();
+ if (!mMediaEngine) {
+ LOG("Engine has been shutdowned!");
+ return IPC_OK();
+ }
+ LOG("Pause");
+ ENGINE_MARKER("MFMediaEngineParent,Pause");
+ NS_ENSURE_TRUE(SUCCEEDED(mMediaEngine->Pause()), IPC_OK());
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult MFMediaEngineParent::RecvSeek(
+ double aTargetTimeInSecond) {
+ AssertOnManagerThread();
+ if (!mMediaEngine) {
+ return IPC_OK();
+ }
+
+ // If the target time is already equal to the current time, the media engine
+ // doesn't perform seek internally so we won't be able to receive `seeked`
+ // event. Therefore, we simply return `seeked` here because we're already in
+ // the target time.
+ const auto currentTimeInSeconds = mMediaEngine->GetCurrentTime();
+ if (currentTimeInSeconds == aTargetTimeInSecond) {
+ Unused << SendNotifyEvent(MF_MEDIA_ENGINE_EVENT_SEEKED);
+ return IPC_OK();
+ }
+
+ LOG("Seek to %f", aTargetTimeInSecond);
+ ENGINE_MARKER_TEXT("MFMediaEngineParent,Seek",
+ nsPrintfCString("%f", aTargetTimeInSecond));
+ NS_ENSURE_TRUE(SUCCEEDED(mMediaEngine->SetCurrentTime(aTargetTimeInSecond)),
+ IPC_OK());
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult MFMediaEngineParent::RecvSetCDMProxyId(
+ uint64_t aProxyId) {
+ if (!mMediaEngine) {
+ return IPC_OK();
+ }
+#ifdef MOZ_WMF_CDM
+ LOG("SetCDMProxy, Id=%" PRIu64, aProxyId);
+ MFCDMParent* cdmParent = MFCDMParent::GetCDMById(aProxyId);
+ MOZ_DIAGNOSTIC_ASSERT(cdmParent);
+ RETURN_PARAM_IF_FAILED(
+ MakeAndInitialize<MFContentProtectionManager>(&mContentProtectionManager),
+ IPC_OK());
+
+ ComPtr<IMFMediaEngineProtectedContent> protectedMediaEngine;
+ RETURN_PARAM_IF_FAILED(mMediaEngine.As(&protectedMediaEngine), IPC_OK());
+ RETURN_PARAM_IF_FAILED(protectedMediaEngine->SetContentProtectionManager(
+ mContentProtectionManager.Get()),
+ IPC_OK());
+
+ RefPtr<MFCDMProxy> proxy = cdmParent->GetMFCDMProxy();
+ if (!proxy) {
+ LOG("Failed to get MFCDMProxy!");
+ return IPC_OK();
+ }
+
+ RETURN_PARAM_IF_FAILED(mContentProtectionManager->SetCDMProxy(proxy),
+ IPC_OK());
+ // TODO : is it possible to set CDM proxy before creating media source? If so,
+ // handle that as well.
+ if (mMediaSource) {
+ mMediaSource->SetCDMProxy(proxy);
+ SetMediaSourceOnEngine();
+ }
+ LOG("Set CDM Proxy successfully on the media engine!");
+#endif
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult MFMediaEngineParent::RecvSetVolume(double aVolume) {
+ AssertOnManagerThread();
+ if (mMediaEngine) {
+ LOG("SetVolume=%f", aVolume);
+ ENGINE_MARKER_TEXT("MFMediaEngineParent,SetVolume",
+ nsPrintfCString("%f", aVolume));
+ NS_ENSURE_TRUE(SUCCEEDED(mMediaEngine->SetVolume(aVolume)), IPC_OK());
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult MFMediaEngineParent::RecvSetPlaybackRate(
+ double aPlaybackRate) {
+ AssertOnManagerThread();
+ if (aPlaybackRate <= 0) {
+ LOG("Not support zero or negative playback rate");
+ return IPC_OK();
+ }
+ LOG("SetPlaybackRate=%f", aPlaybackRate);
+ ENGINE_MARKER_TEXT("MFMediaEngineParent,SetPlaybackRate",
+ nsPrintfCString("%f", aPlaybackRate));
+ mPlaybackRate = aPlaybackRate;
+ NS_ENSURE_TRUE(SUCCEEDED(mMediaEngine->SetPlaybackRate(mPlaybackRate)),
+ IPC_OK());
+ // The media Engine uses the default playback rate to determine the playback
+ // rate when calling `play()`. So if we don't change default playback rate
+ // together, the playback rate would fallback to 1 after pausing or
+ // seeking, which would be different from our expected playback rate.
+ NS_ENSURE_TRUE(SUCCEEDED(mMediaEngine->SetDefaultPlaybackRate(mPlaybackRate)),
+ IPC_OK());
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult MFMediaEngineParent::RecvSetLooping(bool aLooping) {
+ AssertOnManagerThread();
+ // We handle looping by seeking back to the head by ourselves, so we don't
+ // rely on the media engine for looping.
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult MFMediaEngineParent::RecvNotifyEndOfStream(
+ TrackInfo::TrackType aType) {
+ AssertOnManagerThread();
+ MOZ_ASSERT(mMediaSource);
+ LOG("NotifyEndOfStream, type=%s", TrackTypeToStr(aType));
+ mMediaSource->NotifyEndOfStream(aType);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult MFMediaEngineParent::RecvShutdown() {
+ AssertOnManagerThread();
+ LOG("Shutdown");
+ ENGINE_MARKER("MFMediaEngineParent,Shutdown");
+ DestroyEngineIfExists();
+ return IPC_OK();
+}
+
+void MFMediaEngineParent::Destroy() {
+ AssertOnManagerThread();
+ mIPDLSelfRef = nullptr;
+}
+
+void MFMediaEngineParent::HandleRequestSample(const SampleRequest& aRequest) {
+ AssertOnManagerThread();
+ MOZ_ASSERT(aRequest.mType == TrackInfo::TrackType::kAudioTrack ||
+ aRequest.mType == TrackInfo::TrackType::kVideoTrack);
+ Unused << SendRequestSample(aRequest.mType, aRequest.mIsEnough);
+}
+
+void MFMediaEngineParent::AssertOnManagerThread() const {
+ MOZ_ASSERT(mManagerThread->IsOnCurrentThread());
+}
+
+void MFMediaEngineParent::EnsureDcompSurfaceHandle() {
+ AssertOnManagerThread();
+ MOZ_ASSERT(mMediaEngine);
+ MOZ_ASSERT(mMediaEngine->HasVideo());
+
+ ComPtr<IMFMediaEngineEx> mediaEngineEx;
+ RETURN_VOID_IF_FAILED(mMediaEngine.As(&mediaEngineEx));
+ DWORD width, height;
+ RETURN_VOID_IF_FAILED(mMediaEngine->GetNativeVideoSize(&width, &height));
+ if (width != mDisplayWidth || height != mDisplayHeight) {
+ // Update stream size before asking for a handle. If we don't update the
+ // size, media engine will create the dcomp surface in a wrong size. If
+ // the size isn't changed, then we don't need to recreate the surface.
+ mDisplayWidth = width;
+ mDisplayHeight = height;
+ RECT rect = {0, 0, (LONG)mDisplayWidth, (LONG)mDisplayHeight};
+ RETURN_VOID_IF_FAILED(mediaEngineEx->UpdateVideoStream(
+ nullptr /* pSrc */, &rect, nullptr /* pBorderClr */));
+ LOG("Updated video size for engine=[%lux%lu]", mDisplayWidth,
+ mDisplayHeight);
+ ENGINE_MARKER_TEXT(
+ "MFMediaEngineParent,UpdateVideoSize",
+ nsPrintfCString("%lux%lu", mDisplayWidth, mDisplayHeight));
+ }
+
+ if (!mIsEnableDcompMode) {
+ RETURN_VOID_IF_FAILED(mediaEngineEx->EnableWindowlessSwapchainMode(true));
+ LOG("Enabled dcomp swap chain mode");
+ mIsEnableDcompMode = true;
+ ENGINE_MARKER("MFMediaEngineParent,EnabledSwapChain");
+ }
+
+ HANDLE surfaceHandle = INVALID_HANDLE_VALUE;
+ RETURN_VOID_IF_FAILED(mediaEngineEx->GetVideoSwapchainHandle(&surfaceHandle));
+ if (surfaceHandle && surfaceHandle != INVALID_HANDLE_VALUE) {
+ LOG("EnsureDcompSurfaceHandle, handle=%p, size=[%lux%lu]", surfaceHandle,
+ width, height);
+ mMediaSource->SetDCompSurfaceHandle(surfaceHandle,
+ gfx::IntSize{width, height});
+ } else {
+ NS_WARNING("SurfaceHandle is not ready yet");
+ }
+}
+
+void MFMediaEngineParent::UpdateStatisticsData() {
+ AssertOnManagerThread();
+
+ // Statistic data is only for video.
+ if (!mMediaEngine->HasVideo()) {
+ return;
+ }
+
+ ComPtr<IMFMediaEngineEx> mediaEngineEx;
+ RETURN_VOID_IF_FAILED(mMediaEngine.As(&mediaEngineEx));
+
+ struct scopePropVariant : public PROPVARIANT {
+ scopePropVariant() { PropVariantInit(this); }
+ ~scopePropVariant() { PropVariantClear(this); }
+ scopePropVariant(scopePropVariant const&) = delete;
+ scopePropVariant& operator=(scopePropVariant const&) = delete;
+ };
+
+ // https://docs.microsoft.com/en-us/windows/win32/api/mfmediaengine/ne-mfmediaengine-mf_media_engine_statistic
+ scopePropVariant renderedFramesProp, droppedFramesProp;
+ RETURN_VOID_IF_FAILED(mediaEngineEx->GetStatistics(
+ MF_MEDIA_ENGINE_STATISTIC_FRAMES_RENDERED, &renderedFramesProp));
+ RETURN_VOID_IF_FAILED(mediaEngineEx->GetStatistics(
+ MF_MEDIA_ENGINE_STATISTIC_FRAMES_DROPPED, &droppedFramesProp));
+
+ const unsigned long renderedFrames = renderedFramesProp.ulVal;
+ const unsigned long droppedFrames = droppedFramesProp.ulVal;
+
+ // As the amount of rendered frame MUST increase monotonically. If the new
+ // statistic data show the decreasing, which means the media engine has reset
+ // the statistic data and started a new one. (That will happens after calling
+ // flush internally)
+ if (renderedFrames < mCurrentPlaybackStatisticData.renderedFrames()) {
+ mPrevPlaybackStatisticData =
+ StatisticData{mPrevPlaybackStatisticData.renderedFrames() +
+ mCurrentPlaybackStatisticData.renderedFrames(),
+ mPrevPlaybackStatisticData.droppedFrames() +
+ mCurrentPlaybackStatisticData.droppedFrames()};
+ mCurrentPlaybackStatisticData = StatisticData{};
+ }
+
+ if (mCurrentPlaybackStatisticData.renderedFrames() != renderedFrames ||
+ mCurrentPlaybackStatisticData.droppedFrames() != droppedFrames) {
+ mCurrentPlaybackStatisticData =
+ StatisticData{renderedFrames, droppedFrames};
+ const uint64_t totalRenderedFrames =
+ mPrevPlaybackStatisticData.renderedFrames() +
+ mCurrentPlaybackStatisticData.renderedFrames();
+ const uint64_t totalDroppedFrames =
+ mPrevPlaybackStatisticData.droppedFrames() +
+ mCurrentPlaybackStatisticData.droppedFrames();
+ LOG("Update statistic data, rendered=%" PRIu64 ", dropped=%" PRIu64,
+ totalRenderedFrames, totalDroppedFrames);
+ Unused << SendUpdateStatisticData(
+ StatisticData{totalRenderedFrames, totalDroppedFrames});
+ }
+}
+
+#undef LOG
+#undef RETURN_IF_FAILED
+#undef ENSURE_EVENT_DISPATCH_DURING_PLAYING
+
+} // namespace mozilla
diff --git a/dom/media/ipc/MFMediaEngineParent.h b/dom/media/ipc/MFMediaEngineParent.h
new file mode 100644
index 0000000000..af921d49c5
--- /dev/null
+++ b/dom/media/ipc/MFMediaEngineParent.h
@@ -0,0 +1,144 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_IPC_MFMEDIAENGINEPARENT_H_
+#define DOM_MEDIA_IPC_MFMEDIAENGINEPARENT_H_
+
+#include <Mfidl.h>
+#include <winnt.h>
+#include <wrl.h>
+
+#include "MediaInfo.h"
+#include "MFMediaEngineExtra.h"
+#include "MFMediaEngineNotify.h"
+#include "MFMediaEngineUtils.h"
+#include "MFMediaSource.h"
+#include "PlatformDecoderModule.h"
+#include "mozilla/PMFMediaEngineParent.h"
+
+namespace mozilla {
+
+class MFCDMProxy;
+class MFContentProtectionManager;
+class MFMediaEngineExtension;
+class MFMediaEngineStreamWrapper;
+class MFMediaSource;
+class RemoteDecoderManagerParent;
+
+/**
+ * MFMediaEngineParent is a wrapper class for a MediaEngine in the MF-CDM
+ * process. It's responsible to create the media engine and its related classes,
+ * such as a custom media source, media engine extension, media engine
+ * notify...e.t.c It communicates with MFMediaEngineChild in the content process
+ * to receive commands and direct them to the media engine.
+ * https://docs.microsoft.com/en-us/windows/win32/api/mfmediaengine/nn-mfmediaengine-imfmediaengine
+ */
+class MFMediaEngineParent final : public PMFMediaEngineParent {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MFMediaEngineParent);
+ MFMediaEngineParent(RemoteDecoderManagerParent* aManager,
+ nsISerialEventTarget* aManagerThread);
+
+ using TrackType = TrackInfo::TrackType;
+
+ static MFMediaEngineParent* GetMediaEngineById(uint64_t aId);
+
+ MFMediaEngineStreamWrapper* GetMediaEngineStream(
+ TrackType aType, const CreateDecoderParams& aParam);
+
+ uint64_t Id() const { return mMediaEngineId; }
+
+ // Methods for PMFMediaEngineParent
+ mozilla::ipc::IPCResult RecvInitMediaEngine(
+ const MediaEngineInfoIPDL& aInfo, InitMediaEngineResolver&& aResolver);
+ mozilla::ipc::IPCResult RecvNotifyMediaInfo(const MediaInfoIPDL& aInfo);
+ mozilla::ipc::IPCResult RecvPlay();
+ mozilla::ipc::IPCResult RecvPause();
+ mozilla::ipc::IPCResult RecvSeek(double aTargetTimeInSecond);
+ mozilla::ipc::IPCResult RecvSetCDMProxyId(uint64_t aProxyId);
+ mozilla::ipc::IPCResult RecvSetVolume(double aVolume);
+ mozilla::ipc::IPCResult RecvSetPlaybackRate(double aPlaybackRate);
+ mozilla::ipc::IPCResult RecvSetLooping(bool aLooping);
+ mozilla::ipc::IPCResult RecvNotifyEndOfStream(TrackInfo::TrackType aType);
+ mozilla::ipc::IPCResult RecvShutdown();
+
+ void Destroy();
+
+ private:
+ ~MFMediaEngineParent();
+
+ void CreateMediaEngine();
+
+ void InitializeVirtualVideoWindow();
+ void InitializeDXGIDeviceManager();
+
+ void AssertOnManagerThread() const;
+
+ void HandleMediaEngineEvent(MFMediaEngineEventWrapper aEvent);
+ void HandleRequestSample(const SampleRequest& aRequest);
+
+ void NotifyError(MF_MEDIA_ENGINE_ERR aError, HRESULT aResult = 0);
+
+ void DestroyEngineIfExists(const Maybe<MediaResult>& aError = Nothing());
+
+ void EnsureDcompSurfaceHandle();
+
+ void UpdateStatisticsData();
+
+ void SetMediaSourceOnEngine();
+
+ // This generates unique id for each MFMediaEngineParent instance, and it
+ // would be increased monotonically.
+ static inline uint64_t sMediaEngineIdx = 0;
+
+ const uint64_t mMediaEngineId;
+
+ // The life cycle of this class is determined by the actor in the content
+ // process, we would hold a reference until the content actor asks us to
+ // destroy.
+ RefPtr<MFMediaEngineParent> mIPDLSelfRef;
+
+ const RefPtr<RemoteDecoderManagerParent> mManager;
+ const RefPtr<nsISerialEventTarget> mManagerThread;
+
+ // Required classes for working with the media engine.
+ Microsoft::WRL::ComPtr<IMFMediaEngine> mMediaEngine;
+ Microsoft::WRL::ComPtr<MFMediaEngineNotify> mMediaEngineNotify;
+ Microsoft::WRL::ComPtr<MFMediaEngineExtension> mMediaEngineExtension;
+ Microsoft::WRL::ComPtr<MFMediaSource> mMediaSource;
+#ifdef MOZ_WMF_CDM
+ Microsoft::WRL::ComPtr<MFContentProtectionManager> mContentProtectionManager;
+#endif
+
+ MediaEventListener mMediaEngineEventListener;
+ MediaEventListener mRequestSampleListener;
+ bool mIsCreatedMediaEngine = false;
+
+ // A fake window handle passed to MF-based rendering pipeline for OPM.
+ HWND mVirtualVideoWindow = nullptr;
+
+ Microsoft::WRL::ComPtr<IMFDXGIDeviceManager> mDXGIDeviceManager;
+
+ // These will be always zero for audio playback.
+ DWORD mDisplayWidth = 0;
+ DWORD mDisplayHeight = 0;
+
+ // When it's true, the media engine will output decoded video frames to a
+ // shareable dcomp surface.
+ bool mIsEnableDcompMode = false;
+
+ float mPlaybackRate = 1.0;
+
+ // When flush happens inside the media engine, it will reset the statistic
+ // data. Therefore, whenever the statistic data gets reset, we will use
+ // `mCurrentPlaybackStatisticData` to track new data and store previous data
+ // to `mPrevPlaybackStatisticData`. The sum of these two data is the total
+ // statistic data for playback.
+ StatisticData mCurrentPlaybackStatisticData;
+ StatisticData mPrevPlaybackStatisticData;
+};
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_IPC_MFMEDIAENGINEPARENT_H_
diff --git a/dom/media/ipc/MFMediaEngineUtils.cpp b/dom/media/ipc/MFMediaEngineUtils.cpp
new file mode 100644
index 0000000000..01c4d4d07c
--- /dev/null
+++ b/dom/media/ipc/MFMediaEngineUtils.cpp
@@ -0,0 +1,179 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MFMediaEngineUtils.h"
+
+#include "WMFUtils.h"
+
+namespace mozilla {
+
+#define ENUM_TO_STR(enumVal) \
+ case enumVal: \
+ return #enumVal
+
+#define ENUM_TO_STR2(guid, enumVal) \
+ if (guid == enumVal) { \
+ return #enumVal; \
+ }
+
+const char* MediaEventTypeToStr(MediaEventType aType) {
+ switch (aType) {
+ ENUM_TO_STR(MESourceUnknown);
+ ENUM_TO_STR(MESourceStarted);
+ ENUM_TO_STR(MEStreamStarted);
+ ENUM_TO_STR(MESourceSeeked);
+ ENUM_TO_STR(MEStreamSeeked);
+ ENUM_TO_STR(MENewStream);
+ ENUM_TO_STR(MEUpdatedStream);
+ ENUM_TO_STR(MESourceStopped);
+ ENUM_TO_STR(MEStreamStopped);
+ ENUM_TO_STR(MESourcePaused);
+ ENUM_TO_STR(MEStreamPaused);
+ ENUM_TO_STR(MEEndOfPresentation);
+ ENUM_TO_STR(MEEndOfStream);
+ ENUM_TO_STR(MEMediaSample);
+ ENUM_TO_STR(MEStreamTick);
+ ENUM_TO_STR(MEStreamThinMode);
+ ENUM_TO_STR(MEStreamFormatChanged);
+ ENUM_TO_STR(MESourceRateChanged);
+ ENUM_TO_STR(MEEndOfPresentationSegment);
+ ENUM_TO_STR(MESourceCharacteristicsChanged);
+ ENUM_TO_STR(MESourceRateChangeRequested);
+ ENUM_TO_STR(MESourceMetadataChanged);
+ ENUM_TO_STR(MESequencerSourceTopologyUpdated);
+ default:
+ return "Unknown MediaEventType";
+ }
+}
+
+const char* MediaEngineEventToStr(MF_MEDIA_ENGINE_EVENT aEvent) {
+ switch (aEvent) {
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_LOADSTART);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_PROGRESS);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_SUSPEND);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_ABORT);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_ERROR);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_EMPTIED);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_STALLED);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_PLAY);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_PAUSE);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_LOADEDMETADATA);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_LOADEDDATA);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_WAITING);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_PLAYING);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_CANPLAY);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_CANPLAYTHROUGH);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_SEEKING);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_SEEKED);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_TIMEUPDATE);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_ENDED);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_RATECHANGE);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_DURATIONCHANGE);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_VOLUMECHANGE);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_FORMATCHANGE);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_PURGEQUEUEDEVENTS);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_TIMELINE_MARKER);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_BALANCECHANGE);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_DOWNLOADCOMPLETE);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_BUFFERINGSTARTED);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_BUFFERINGENDED);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_FRAMESTEPCOMPLETED);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_NOTIFYSTABLESTATE);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_FIRSTFRAMEREADY);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_TRACKSCHANGE);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_OPMINFO);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_RESOURCELOST);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_DELAYLOADEVENT_CHANGED);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_STREAMRENDERINGERROR);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_SUPPORTEDRATES_CHANGED);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_EVENT_AUDIOENDPOINTCHANGE);
+ default:
+ return "Unknown MF_MEDIA_ENGINE_EVENT";
+ }
+}
+
+const char* MFMediaEngineErrorToStr(MFMediaEngineError aError) {
+ switch (aError) {
+ ENUM_TO_STR(MF_MEDIA_ENGINE_ERR_NOERROR);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_ERR_ABORTED);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_ERR_NETWORK);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_ERR_DECODE);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_ERR_SRC_NOT_SUPPORTED);
+ ENUM_TO_STR(MF_MEDIA_ENGINE_ERR_ENCRYPTED);
+ default:
+ return "Unknown MFMediaEngineError";
+ }
+}
+
+const char* GUIDToStr(GUID aGUID) {
+ ENUM_TO_STR2(aGUID, MFAudioFormat_MP3)
+ ENUM_TO_STR2(aGUID, MFAudioFormat_AAC)
+ ENUM_TO_STR2(aGUID, MFAudioFormat_Vorbis)
+ ENUM_TO_STR2(aGUID, MFAudioFormat_Opus)
+ ENUM_TO_STR2(aGUID, MFVideoFormat_H264)
+ ENUM_TO_STR2(aGUID, MFVideoFormat_VP80)
+ ENUM_TO_STR2(aGUID, MFVideoFormat_VP90)
+ ENUM_TO_STR2(aGUID, MFVideoFormat_AV1)
+ ENUM_TO_STR2(aGUID, MFMediaType_Audio)
+ return "Unknown GUID";
+}
+
+const char* MFVideoRotationFormatToStr(MFVideoRotationFormat aFormat) {
+ switch (aFormat) {
+ ENUM_TO_STR(MFVideoRotationFormat_0);
+ ENUM_TO_STR(MFVideoRotationFormat_90);
+ ENUM_TO_STR(MFVideoRotationFormat_180);
+ ENUM_TO_STR(MFVideoRotationFormat_270);
+ default:
+ return "Unknown MFVideoRotationFormat";
+ }
+}
+
+const char* MFVideoTransferFunctionToStr(MFVideoTransferFunction aFunc) {
+ switch (aFunc) {
+ ENUM_TO_STR(MFVideoTransFunc_Unknown);
+ ENUM_TO_STR(MFVideoTransFunc_709);
+ ENUM_TO_STR(MFVideoTransFunc_2020);
+ ENUM_TO_STR(MFVideoTransFunc_sRGB);
+ default:
+ return "Unsupported MFVideoTransferFunction";
+ }
+}
+
+const char* MFVideoPrimariesToStr(MFVideoPrimaries aPrimaries) {
+ switch (aPrimaries) {
+ ENUM_TO_STR(MFVideoPrimaries_Unknown);
+ ENUM_TO_STR(MFVideoPrimaries_BT709);
+ ENUM_TO_STR(MFVideoPrimaries_BT2020);
+ default:
+ return "Unsupported MFVideoPrimaries";
+ }
+}
+
+void ByteArrayFromGUID(REFGUID aGuidIn, nsTArray<uint8_t>& aByteArrayOut) {
+ aByteArrayOut.SetLength(sizeof(GUID));
+ // GUID is little endian. The byte array in network order is big endian.
+ GUID* reversedGuid = reinterpret_cast<GUID*>(aByteArrayOut.Elements());
+ *reversedGuid = aGuidIn;
+ reversedGuid->Data1 = _byteswap_ulong(aGuidIn.Data1);
+ reversedGuid->Data2 = _byteswap_ushort(aGuidIn.Data2);
+ reversedGuid->Data3 = _byteswap_ushort(aGuidIn.Data3);
+ // Data4 is already a byte array so no need to byte swap.
+}
+
+void GUIDFromByteArray(const nsTArray<uint8_t>& aByteArrayIn, GUID& aGuidOut) {
+ MOZ_ASSERT(aByteArrayIn.Length() == sizeof(GUID));
+ GUID* reversedGuid =
+ reinterpret_cast<GUID*>(const_cast<uint8_t*>(aByteArrayIn.Elements()));
+ aGuidOut = *reversedGuid;
+ aGuidOut.Data1 = _byteswap_ulong(reversedGuid->Data1);
+ aGuidOut.Data2 = _byteswap_ushort(reversedGuid->Data2);
+ aGuidOut.Data3 = _byteswap_ushort(reversedGuid->Data3);
+ // Data4 is already a byte array so no need to byte swap.
+}
+
+#undef ENUM_TO_STR
+#undef ENUM_TO_STR2
+
+} // namespace mozilla
diff --git a/dom/media/ipc/MFMediaEngineUtils.h b/dom/media/ipc/MFMediaEngineUtils.h
new file mode 100644
index 0000000000..a7635045c7
--- /dev/null
+++ b/dom/media/ipc/MFMediaEngineUtils.h
@@ -0,0 +1,163 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_IPC_MFMEDIAENGINEUTILS_H_
+#define DOM_MEDIA_IPC_MFMEDIAENGINEUTILS_H_
+
+#include "MFMediaEngineExtra.h"
+#include "ipc/EnumSerializer.h"
+#include "mozilla/Logging.h"
+#include "mozilla/ProfilerMarkerTypes.h"
+
+namespace mozilla {
+
+inline LazyLogModule gMFMediaEngineLog{"MFMediaEngine"};
+
+// https://docs.microsoft.com/en-us/windows/win32/api/mfmediaengine/ne-mfmediaengine-mf_media_engine_event
+using MFMediaEngineEvent = MF_MEDIA_ENGINE_EVENT;
+
+// https://docs.microsoft.com/en-us/windows/win32/api/mfmediaengine/ne-mfmediaengine-mf_media_engine_err
+using MFMediaEngineError = MF_MEDIA_ENGINE_ERR;
+
+#define LOG_AND_WARNING(msg, ...) \
+ do { \
+ NS_WARNING(nsPrintfCString(msg, rv).get()); \
+ MOZ_LOG(gMFMediaEngineLog, LogLevel::Debug, \
+ ("%s:%d, " msg, __FILE__, __LINE__, ##__VA_ARGS__)); \
+ } while (false)
+
+#ifndef RETURN_IF_FAILED
+# define RETURN_IF_FAILED(x) \
+ do { \
+ HRESULT rv = x; \
+ if (MOZ_UNLIKELY(FAILED(rv))) { \
+ LOG_AND_WARNING("(" #x ") failed, rv=%lx", rv); \
+ return rv; \
+ } \
+ } while (false)
+#endif
+
+#ifndef RETURN_VOID_IF_FAILED
+# define RETURN_VOID_IF_FAILED(x) \
+ do { \
+ HRESULT rv = x; \
+ if (MOZ_UNLIKELY(FAILED(rv))) { \
+ LOG_AND_WARNING("(" #x ") failed, rv=%lx", rv); \
+ return; \
+ } \
+ } while (false)
+#endif
+
+#ifndef RETURN_PARAM_IF_FAILED
+# define RETURN_PARAM_IF_FAILED(x, defaultOut) \
+ do { \
+ HRESULT rv = x; \
+ if (MOZ_UNLIKELY(FAILED(rv))) { \
+ LOG_AND_WARNING("(" #x ") failed, rv=%lx", rv); \
+ return defaultOut; \
+ } \
+ } while (false)
+#endif
+
+#define ENGINE_MARKER(markerName) \
+ PROFILER_MARKER(markerName, MEDIA_PLAYBACK, {}, MediaEngineMarker, Id())
+
+#define ENGINE_MARKER_TEXT(markerName, text) \
+ PROFILER_MARKER(markerName, MEDIA_PLAYBACK, {}, MediaEngineTextMarker, Id(), \
+ text)
+
+const char* MediaEventTypeToStr(MediaEventType aType);
+const char* MediaEngineEventToStr(MF_MEDIA_ENGINE_EVENT aEvent);
+const char* MFMediaEngineErrorToStr(MFMediaEngineError aError);
+const char* GUIDToStr(GUID aGUID);
+const char* MFVideoRotationFormatToStr(MFVideoRotationFormat aFormat);
+const char* MFVideoTransferFunctionToStr(MFVideoTransferFunction aFunc);
+const char* MFVideoPrimariesToStr(MFVideoPrimaries aPrimaries);
+void ByteArrayFromGUID(REFGUID aGuidIn, nsTArray<uint8_t>& aByteArrayOut);
+void GUIDFromByteArray(const nsTArray<uint8_t>& aByteArrayIn, GUID& aGuidOut);
+
+// See cdm::SubsampleEntry
+struct MediaFoundationSubsampleEntry {
+ uint32_t mClearBytes;
+ uint32_t mCipherBytes;
+};
+
+template <typename T>
+class ScopedCoMem {
+ public:
+ ScopedCoMem() : mPtr(nullptr) {}
+
+ ~ScopedCoMem() { Reset(nullptr); }
+
+ ScopedCoMem(const ScopedCoMem&) = delete;
+ ScopedCoMem& operator=(const ScopedCoMem&) = delete;
+
+ T** operator&() { // NOLINT
+ MOZ_ASSERT(mPtr == nullptr); // To catch memory leaks.
+ return &mPtr;
+ }
+
+ operator T*() { return mPtr; }
+
+ T* operator->() {
+ MOZ_ASSERT(mPtr != nullptr);
+ return mPtr;
+ }
+
+ const T* operator->() const {
+ MOZ_ASSERT(mPtr != nullptr);
+ return mPtr;
+ }
+
+ explicit operator bool() const { return mPtr; }
+
+ friend bool operator==(const ScopedCoMem& lhs, std::nullptr_t) {
+ return lhs.Get() == nullptr;
+ }
+
+ friend bool operator==(std::nullptr_t, const ScopedCoMem& rhs) {
+ return rhs.Get() == nullptr;
+ }
+
+ friend bool operator!=(const ScopedCoMem& lhs, std::nullptr_t) {
+ return lhs.Get() != nullptr;
+ }
+
+ friend bool operator!=(std::nullptr_t, const ScopedCoMem& rhs) {
+ return rhs.Get() != nullptr;
+ }
+
+ void Reset(T* ptr) {
+ if (mPtr) CoTaskMemFree(mPtr);
+ mPtr = ptr;
+ }
+
+ T* Get() const { return mPtr; }
+
+ private:
+ T* mPtr;
+};
+
+} // namespace mozilla
+
+namespace IPC {
+
+template <>
+struct ParamTraits<mozilla::MFMediaEngineError>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::MFMediaEngineError,
+ mozilla::MFMediaEngineError::MF_MEDIA_ENGINE_ERR_ABORTED,
+ mozilla::MFMediaEngineError::MF_MEDIA_ENGINE_ERR_ENCRYPTED> {};
+
+template <>
+struct ParamTraits<mozilla::MFMediaEngineEvent>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::MFMediaEngineEvent,
+ mozilla::MFMediaEngineEvent::MF_MEDIA_ENGINE_EVENT_LOADSTART,
+ mozilla::MFMediaEngineEvent::
+ MF_MEDIA_ENGINE_EVENT_AUDIOENDPOINTCHANGE> {};
+
+} // namespace IPC
+
+#endif // DOM_MEDIA_IPC_MFMEDIAENGINECHILD_H_
diff --git a/dom/media/ipc/MediaIPCUtils.h b/dom/media/ipc/MediaIPCUtils.h
new file mode 100644
index 0000000000..ddf37bd644
--- /dev/null
+++ b/dom/media/ipc/MediaIPCUtils.h
@@ -0,0 +1,376 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_media_MediaIPCUtils_h
+#define mozilla_dom_media_MediaIPCUtils_h
+
+#include <type_traits>
+
+#include "DecoderDoctorDiagnostics.h"
+#include "PerformanceRecorder.h"
+#include "PlatformDecoderModule.h"
+#include "ipc/EnumSerializer.h"
+#include "mozilla/EnumSet.h"
+#include "mozilla/GfxMessageUtils.h"
+#include "mozilla/gfx/Rect.h"
+#include "mozilla/dom/MFCDMSerializers.h"
+
+namespace IPC {
+template <>
+struct ParamTraits<mozilla::VideoInfo> {
+ typedef mozilla::VideoInfo paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ // TrackInfo
+ WriteParam(aWriter, aParam.mMimeType);
+
+ // VideoInfo
+ WriteParam(aWriter, aParam.mDisplay);
+ WriteParam(aWriter, aParam.mStereoMode);
+ WriteParam(aWriter, aParam.mImage);
+ WriteParam(aWriter, aParam.mImageRect);
+ WriteParam(aWriter, *aParam.mCodecSpecificConfig);
+ WriteParam(aWriter, *aParam.mExtraData);
+ WriteParam(aWriter, aParam.mRotation);
+ WriteParam(aWriter, aParam.mColorDepth);
+ WriteParam(aWriter, aParam.mColorSpace);
+ WriteParam(aWriter, aParam.mColorPrimaries);
+ WriteParam(aWriter, aParam.mTransferFunction);
+ WriteParam(aWriter, aParam.mColorRange);
+ WriteParam(aWriter, aParam.HasAlpha());
+ WriteParam(aWriter, aParam.mCrypto);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ mozilla::gfx::IntRect imageRect;
+ bool alphaPresent;
+ if (ReadParam(aReader, &aResult->mMimeType) &&
+ ReadParam(aReader, &aResult->mDisplay) &&
+ ReadParam(aReader, &aResult->mStereoMode) &&
+ ReadParam(aReader, &aResult->mImage) &&
+ ReadParam(aReader, &aResult->mImageRect) &&
+ ReadParam(aReader, aResult->mCodecSpecificConfig.get()) &&
+ ReadParam(aReader, aResult->mExtraData.get()) &&
+ ReadParam(aReader, &aResult->mRotation) &&
+ ReadParam(aReader, &aResult->mColorDepth) &&
+ ReadParam(aReader, &aResult->mColorSpace) &&
+ ReadParam(aReader, &aResult->mColorPrimaries) &&
+ ReadParam(aReader, &aResult->mTransferFunction) &&
+ ReadParam(aReader, &aResult->mColorRange) &&
+ ReadParam(aReader, &alphaPresent) &&
+ ReadParam(aReader, &aResult->mCrypto)) {
+ aResult->SetAlpha(alphaPresent);
+ return true;
+ }
+ return false;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::TrackInfo::TrackType>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::TrackInfo::TrackType,
+ mozilla::TrackInfo::TrackType::kUndefinedTrack,
+ mozilla::TrackInfo::TrackType::kTextTrack> {};
+
+template <>
+struct ParamTraits<mozilla::VideoInfo::Rotation>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::VideoInfo::Rotation, mozilla::VideoInfo::Rotation::kDegree_0,
+ mozilla::VideoInfo::Rotation::kDegree_270> {};
+
+template <>
+struct ParamTraits<mozilla::MediaByteBuffer>
+ : public ParamTraits<nsTArray<uint8_t>> {
+ typedef mozilla::MediaByteBuffer paramType;
+};
+
+// Traits for AudioCodecSpecificVariant types.
+
+template <>
+struct ParamTraits<mozilla::NoCodecSpecificData>
+ : public EmptyStructSerializer<mozilla::NoCodecSpecificData> {};
+
+template <>
+struct ParamTraits<mozilla::AudioCodecSpecificBinaryBlob> {
+ using paramType = mozilla::AudioCodecSpecificBinaryBlob;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, *aParam.mBinaryBlob);
+ }
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, aResult->mBinaryBlob.get());
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::AacCodecSpecificData> {
+ using paramType = mozilla::AacCodecSpecificData;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, *aParam.mEsDescriptorBinaryBlob);
+ WriteParam(aWriter, *aParam.mDecoderConfigDescriptorBinaryBlob);
+ WriteParam(aWriter, aParam.mEncoderDelayFrames);
+ WriteParam(aWriter, aParam.mMediaFrameCount);
+ }
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, aResult->mEsDescriptorBinaryBlob.get()) &&
+ ReadParam(aReader,
+ aResult->mDecoderConfigDescriptorBinaryBlob.get()) &&
+ ReadParam(aReader, &aResult->mEncoderDelayFrames) &&
+ ReadParam(aReader, &aResult->mMediaFrameCount);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::FlacCodecSpecificData> {
+ using paramType = mozilla::FlacCodecSpecificData;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, *aParam.mStreamInfoBinaryBlob);
+ }
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, aResult->mStreamInfoBinaryBlob.get());
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::Mp3CodecSpecificData>
+ : public PlainOldDataSerializer<mozilla::Mp3CodecSpecificData> {};
+
+template <>
+struct ParamTraits<mozilla::OpusCodecSpecificData> {
+ using paramType = mozilla::OpusCodecSpecificData;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mContainerCodecDelayMicroSeconds);
+ WriteParam(aWriter, *aParam.mHeadersBinaryBlob);
+ }
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mContainerCodecDelayMicroSeconds) &&
+ ReadParam(aReader, aResult->mHeadersBinaryBlob.get());
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::VorbisCodecSpecificData> {
+ using paramType = mozilla::VorbisCodecSpecificData;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, *aParam.mHeadersBinaryBlob);
+ }
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, aResult->mHeadersBinaryBlob.get());
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::WaveCodecSpecificData>
+ : public EmptyStructSerializer<mozilla::WaveCodecSpecificData> {};
+
+// End traits for AudioCodecSpecificVariant types.
+
+template <>
+struct ParamTraits<mozilla::AudioInfo> {
+ typedef mozilla::AudioInfo paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ // TrackInfo
+ WriteParam(aWriter, aParam.mMimeType);
+
+ // AudioInfo
+ WriteParam(aWriter, aParam.mRate);
+ WriteParam(aWriter, aParam.mChannels);
+ WriteParam(aWriter, aParam.mChannelMap);
+ WriteParam(aWriter, aParam.mBitDepth);
+ WriteParam(aWriter, aParam.mProfile);
+ WriteParam(aWriter, aParam.mExtendedProfile);
+ WriteParam(aWriter, aParam.mCodecSpecificConfig);
+ WriteParam(aWriter, aParam.mCrypto);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ if (ReadParam(aReader, &aResult->mMimeType) &&
+ ReadParam(aReader, &aResult->mRate) &&
+ ReadParam(aReader, &aResult->mChannels) &&
+ ReadParam(aReader, &aResult->mChannelMap) &&
+ ReadParam(aReader, &aResult->mBitDepth) &&
+ ReadParam(aReader, &aResult->mProfile) &&
+ ReadParam(aReader, &aResult->mExtendedProfile) &&
+ ReadParam(aReader, &aResult->mCodecSpecificConfig) &&
+ ReadParam(aReader, &aResult->mCrypto)) {
+ return true;
+ }
+ return false;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::MediaDataDecoder::ConversionRequired>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::MediaDataDecoder::ConversionRequired,
+ mozilla::MediaDataDecoder::ConversionRequired(0),
+ mozilla::MediaDataDecoder::ConversionRequired(
+ mozilla::MediaDataDecoder::ConversionRequired::kNeedAnnexB)> {};
+
+template <>
+struct ParamTraits<mozilla::media::TimeUnit> {
+ using paramType = mozilla::media::TimeUnit;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.IsValid());
+ WriteParam(aWriter, aParam.IsValid() ? aParam.mTicks.value() : 0);
+ WriteParam(aWriter,
+ aParam.IsValid() ? aParam.mBase : 1); // base can't be 0
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ bool valid;
+ int64_t ticks;
+ int64_t base;
+ if (ReadParam(aReader, &valid) && ReadParam(aReader, &ticks) &&
+ ReadParam(aReader, &base)) {
+ if (valid) {
+ *aResult = mozilla::media::TimeUnit(ticks, base);
+ } else {
+ *aResult = mozilla::media::TimeUnit::Invalid();
+ }
+ return true;
+ }
+ return false;
+ };
+};
+
+template <>
+struct ParamTraits<mozilla::media::TimeInterval> {
+ typedef mozilla::media::TimeInterval paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mStart);
+ WriteParam(aWriter, aParam.mEnd);
+ WriteParam(aWriter, aParam.mFuzz);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ if (ReadParam(aReader, &aResult->mStart) &&
+ ReadParam(aReader, &aResult->mEnd) &&
+ ReadParam(aReader, &aResult->mFuzz)) {
+ return true;
+ }
+ return false;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::MediaResult> {
+ typedef mozilla::MediaResult paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.Code());
+ WriteParam(aWriter, aParam.Message());
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ nsresult result;
+ nsCString message;
+ if (ReadParam(aReader, &result) && ReadParam(aReader, &message)) {
+ *aResult = paramType(result, std::move(message));
+ return true;
+ }
+ return false;
+ };
+};
+
+template <>
+struct ParamTraits<mozilla::DecoderDoctorDiagnostics> {
+ typedef mozilla::DecoderDoctorDiagnostics paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mDiagnosticsType);
+ WriteParam(aWriter, aParam.mFormat);
+ WriteParam(aWriter, aParam.mFlags);
+ WriteParam(aWriter, aParam.mEvent);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ if (ReadParam(aReader, &aResult->mDiagnosticsType) &&
+ ReadParam(aReader, &aResult->mFormat) &&
+ ReadParam(aReader, &aResult->mFlags) &&
+ ReadParam(aReader, &aResult->mEvent)) {
+ return true;
+ }
+ return false;
+ };
+};
+
+template <>
+struct ParamTraits<mozilla::DecoderDoctorDiagnostics::DiagnosticsType>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::DecoderDoctorDiagnostics::DiagnosticsType,
+ mozilla::DecoderDoctorDiagnostics::DiagnosticsType::eUnsaved,
+ mozilla::DecoderDoctorDiagnostics::DiagnosticsType::eDecodeWarning> {
+};
+
+template <>
+struct ParamTraits<mozilla::DecoderDoctorEvent> {
+ typedef mozilla::DecoderDoctorEvent paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ int domain = aParam.mDomain;
+ WriteParam(aWriter, domain);
+ WriteParam(aWriter, aParam.mResult);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ int domain = 0;
+ if (ReadParam(aReader, &domain) && ReadParam(aReader, &aResult->mResult)) {
+ aResult->mDomain = paramType::Domain(domain);
+ return true;
+ }
+ return false;
+ };
+};
+
+template <>
+struct ParamTraits<mozilla::TrackingId::Source>
+ : public ContiguousEnumSerializer<
+ mozilla::TrackingId::Source,
+ mozilla::TrackingId::Source::Unimplemented,
+ mozilla::TrackingId::Source::LAST> {};
+
+template <>
+struct ParamTraits<mozilla::TrackingId> {
+ typedef mozilla::TrackingId paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mSource);
+ WriteParam(aWriter, aParam.mProcId);
+ WriteParam(aWriter, aParam.mUniqueInProcId);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mSource) &&
+ ReadParam(aReader, &aResult->mProcId) &&
+ ReadParam(aReader, &aResult->mUniqueInProcId);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::CryptoTrack> {
+ typedef mozilla::CryptoTrack paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mCryptoScheme);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &aResult->mCryptoScheme);
+ }
+};
+
+} // namespace IPC
+
+#endif // mozilla_dom_media_MediaIPCUtils_h
diff --git a/dom/media/ipc/PMFCDM.ipdl b/dom/media/ipc/PMFCDM.ipdl
new file mode 100644
index 0000000000..4502fde97a
--- /dev/null
+++ b/dom/media/ipc/PMFCDM.ipdl
@@ -0,0 +1,116 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include "MFCDMSerializers.h";
+
+include protocol PRemoteDecoderManager;
+
+using mozilla::KeySystemConfig::Requirement from "mozilla/KeySystemConfig.h";
+using mozilla::KeySystemConfig::SessionType from "mozilla/KeySystemConfig.h";
+using mozilla::CryptoScheme from "MediaData.h";
+using mozilla::dom::MediaKeyMessageType from "mozilla/dom/MediaKeyMessageEventBinding.h";
+using mozilla::dom::MediaKeyStatus from "mozilla/dom/MediaKeyStatusMapBinding.h";
+
+namespace mozilla {
+
+// For EME spec 'message' event
+// https://w3c.github.io/encrypted-media/#queue-message
+struct MFCDMKeyMessage {
+ nsString sessionId;
+ MediaKeyMessageType type;
+ uint8_t[] message;
+};
+
+// For EME spec 'keystatuseschange' event
+// https://w3c.github.io/encrypted-media/#dom-evt-keystatuseschange
+struct MFCDMKeyInformation {
+ uint8_t[] keyId;
+ MediaKeyStatus status;
+};
+
+struct MFCDMKeyStatusChange {
+ nsString sessionId;
+ MFCDMKeyInformation[] keyInfo;
+};
+
+// For EME spec Update Expiration algorithm
+// https://w3c.github.io/encrypted-media/#update-expiration
+struct MFCDMKeyExpiration {
+ nsString sessionId;
+ double expiredTimeMilliSecondsSinceEpoch;
+};
+
+// For GetCapabilities()
+struct MFCDMMediaCapability {
+ nsString contentType;
+ nsString robustness;
+};
+
+struct MFCDMCapabilitiesIPDL {
+ nsString keySystem;
+ nsString[] initDataTypes;
+ MFCDMMediaCapability[] audioCapabilities;
+ MFCDMMediaCapability[] videoCapabilities;
+ SessionType[] sessionTypes;
+ CryptoScheme[] encryptionSchemes;
+ Requirement distinctiveID;
+ Requirement persistentState;
+};
+
+union MFCDMCapabilitiesResult {
+ nsresult;
+ MFCDMCapabilitiesIPDL;
+};
+
+// For Init()
+struct MFCDMInitParamsIPDL {
+ nsString origin;
+ nsString[] initDataTypes;
+ Requirement distinctiveID;
+ Requirement persistentState;
+ bool hwSecure;
+};
+
+struct MFCDMInitIPDL {
+ uint64_t id;
+};
+
+union MFCDMInitResult {
+ nsresult;
+ MFCDMInitIPDL;
+};
+
+struct MFCDMCreateSessionParamsIPDL {
+ SessionType sessionType;
+ nsString initDataType;
+ uint8_t[] initData;
+};
+
+union MFCDMSessionResult {
+ nsString;
+ nsresult;
+};
+
+[ManualDealloc]
+async protocol PMFCDM
+{
+ manager PRemoteDecoderManager;
+parent:
+ async GetCapabilities() returns (MFCDMCapabilitiesResult result);
+ async Init(MFCDMInitParamsIPDL params) returns (MFCDMInitResult result);
+ async CreateSessionAndGenerateRequest(MFCDMCreateSessionParamsIPDL type)
+ returns (MFCDMSessionResult result);
+ async LoadSession(SessionType sessionType, nsString sessionId) returns (nsresult result);
+ async UpdateSession(nsString sessionId, uint8_t[] response) returns (nsresult result);
+ async CloseSession(nsString sessionId) returns (nsresult result);
+ async RemoveSession(nsString sessionId) returns (nsresult result);
+ async __delete__();
+
+child:
+ async OnSessionKeyMessage(MFCDMKeyMessage message);
+ async OnSessionKeyStatusesChanged(MFCDMKeyStatusChange keystatuses);
+ async OnSessionKeyExpiration(MFCDMKeyExpiration expiration);
+};
+
+} // namespace mozilla
diff --git a/dom/media/ipc/PMFMediaEngine.ipdl b/dom/media/ipc/PMFMediaEngine.ipdl
new file mode 100644
index 0000000000..b6de550ffa
--- /dev/null
+++ b/dom/media/ipc/PMFMediaEngine.ipdl
@@ -0,0 +1,62 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include "mozilla/dom/MediaIPCUtils.h";
+
+include protocol PRemoteDecoderManager;
+
+using mozilla::AudioInfo from "MediaInfo.h";
+using mozilla::VideoInfo from "MediaInfo.h";
+using mozilla::MediaResult from "MediaResult.h";
+using mozilla::TrackInfo::TrackType from "MediaInfo.h";
+using mozilla::MFMediaEngineError from "MFMediaEngineUtils.h";
+using mozilla::MFMediaEngineEvent from "MFMediaEngineUtils.h";
+
+namespace mozilla {
+
+struct MediaEngineInfoIPDL
+{
+ bool preload;
+};
+
+struct MediaInfoIPDL
+{
+ AudioInfo? audioInfo;
+ VideoInfo? videoInfo;
+};
+
+struct StatisticData
+{
+ uint64_t renderedFrames;
+ uint64_t droppedFrames;
+};
+
+[ManualDealloc]
+async protocol PMFMediaEngine
+{
+ manager PRemoteDecoderManager;
+parent:
+ // Return 0 if media engine can't be created.
+ async InitMediaEngine(MediaEngineInfoIPDL info) returns (uint64_t id);
+ async NotifyMediaInfo(MediaInfoIPDL info);
+ async Play();
+ async Pause();
+ async Seek(double targetTimeInSecond);
+ async SetCDMProxyId(uint64_t type);
+ async SetVolume(double volume);
+ async SetPlaybackRate(double playbackRate);
+ async SetLooping(bool looping);
+ async NotifyEndOfStream(TrackType type);
+ async Shutdown();
+ async __delete__();
+
+child:
+ async NotifyEvent(MFMediaEngineEvent event);
+ async NotifyError(MediaResult error);
+ async UpdateCurrentTime(double currentTimeInSecond);
+ async RequestSample(TrackType type, bool isEnough);
+ async UpdateStatisticData(StatisticData data);
+};
+
+} // namespace mozilla
diff --git a/dom/media/ipc/PMediaDecoderParams.ipdlh b/dom/media/ipc/PMediaDecoderParams.ipdlh
new file mode 100644
index 0000000000..449744b5ac
--- /dev/null
+++ b/dom/media/ipc/PMediaDecoderParams.ipdlh
@@ -0,0 +1,30 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include "mozilla/dom/MediaIPCUtils.h";
+
+using mozilla::media::TimeUnit from "TimeUnits.h";
+using mozilla::CryptoScheme from "MediaData.h";
+
+namespace mozilla {
+
+// used for both SendInput/RecvInput and ProcessDecodedData/RecvOutput
+struct MediaDataIPDL
+{
+ int64_t offset;
+ TimeUnit time;
+ TimeUnit timecode;
+ TimeUnit duration;
+ bool keyframe;
+};
+
+struct CryptoInfo {
+ CryptoScheme mEncryptionScheme;
+ uint8_t[] mIV;
+ uint8_t[] mKeyId;
+ uint32_t[] mClearBytes;
+ uint32_t[] mCipherBytes;
+};
+
+} // namespace mozilla
diff --git a/dom/media/ipc/PRDD.ipdl b/dom/media/ipc/PRDD.ipdl
new file mode 100644
index 0000000000..e18f65edaf
--- /dev/null
+++ b/dom/media/ipc/PRDD.ipdl
@@ -0,0 +1,128 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include GraphicsMessages;
+include MemoryReportTypes;
+include PrefsTypes;
+
+include protocol PProfiler;
+include protocol PRemoteDecoderManager;
+include protocol PVideoBridge;
+
+#if defined(MOZ_SANDBOX) && defined(MOZ_DEBUG) && defined(ENABLE_TESTS)
+include protocol PSandboxTesting;
+#endif
+
+include "mozilla/ipc/ByteBufUtils.h";
+
+using mozilla::dom::ContentParentId from "mozilla/dom/ipc/IdType.h";
+using mozilla::dom::NativeThreadId from "mozilla/dom/NativeThreadId.h";
+using mozilla::media::MediaCodecsSupported from "MediaCodecsSupport.h";
+
+// Telemetry
+using mozilla::Telemetry::HistogramAccumulation from "mozilla/TelemetryComms.h";
+using mozilla::Telemetry::KeyedHistogramAccumulation from "mozilla/TelemetryComms.h";
+using mozilla::Telemetry::ScalarAction from "mozilla/TelemetryComms.h";
+using mozilla::Telemetry::KeyedScalarAction from "mozilla/TelemetryComms.h";
+using mozilla::Telemetry::ChildEventData from "mozilla/TelemetryComms.h";
+using mozilla::Telemetry::DiscardedData from "mozilla/TelemetryComms.h";
+
+#if defined(XP_WIN)
+[MoveOnly] using mozilla::UntrustedModulesData from "mozilla/UntrustedModulesData.h";
+[MoveOnly] using mozilla::ModulePaths from "mozilla/UntrustedModulesData.h";
+[MoveOnly] using mozilla::ModulesMapResult from "mozilla/UntrustedModulesData.h";
+#endif // defined(XP_WIN)
+
+namespace mozilla {
+
+// This protocol allows the UI process to talk to the RDD
+// (RemoteDataDecoder) process. There is one instance of this protocol,
+// with the RDDParent living on the main thread of the RDD process and
+// the RDDChild living on the main thread of the UI process.
+[NeedsOtherPid]
+protocol PRDD
+{
+parent:
+
+ async Init(GfxVarUpdate[] vars, FileDescriptor? sandboxBroker,
+ bool canRecordReleaseTelemetry,
+ bool aIsReadyForBackgroundProcessing);
+
+ async InitProfiler(Endpoint<PProfilerChild> endpoint);
+
+ async NewContentRemoteDecoderManager(
+ Endpoint<PRemoteDecoderManagerParent> endpoint, ContentParentId childId);
+
+ async RequestMemoryReport(uint32_t generation,
+ bool anonymize,
+ bool minimizeMemoryUsage,
+ FileDescriptor? DMDFile)
+ returns (uint32_t aGeneration);
+
+ async PreferenceUpdate(Pref pref);
+
+ async UpdateVar(GfxVarUpdate var);
+
+ async InitVideoBridge(Endpoint<PVideoBridgeChild> endpoint,
+ bool createHardwareDevice,
+ ContentDeviceData contentDeviceData);
+
+#if defined(XP_WIN)
+ async GetUntrustedModulesData() returns (UntrustedModulesData? data);
+
+ /**
+ * This method is used to notifty a child process to start
+ * processing module loading events in UntrustedModulesProcessor.
+ * This should be called when the parent process has gone idle.
+ */
+ async UnblockUntrustedModulesThread();
+#endif // defined(XP_WIN)
+
+#if defined(MOZ_SANDBOX) && defined(MOZ_DEBUG) && defined(ENABLE_TESTS)
+ async InitSandboxTesting(Endpoint<PSandboxTestingChild> aEndpoint);
+#endif
+
+ // Tells the RDD process to flush any pending telemetry.
+ // Used in tests and ping assembly. Buffer contains bincoded Rust structs.
+ // https://firefox-source-docs.mozilla.org/toolkit/components/glean/dev/ipc.html
+ async FlushFOGData() returns (ByteBuf buf);
+
+ // Test-only method.
+ // Asks the RDD process to trigger test-only instrumentation.
+ // The unused returned value is to have a promise we can await.
+ async TestTriggerMetrics() returns (bool unused);
+
+ async TestTelemetryProbes();
+
+child:
+
+ async InitCrashReporter(NativeThreadId threadId);
+
+ async AddMemoryReport(MemoryReport aReport);
+
+#if defined(XP_WIN)
+ async GetModulesTrust(ModulePaths aModPaths, bool aRunAtNormalPriority)
+ returns (ModulesMapResult? modMapResult);
+#endif // defined(XP_WIN)
+
+ // Update the cached list of codec supported following a check in the
+ // RDD parent.
+ async UpdateMediaCodecsSupported(MediaCodecsSupported aSupported);
+
+ // Messages for sending telemetry to parent process.
+ async AccumulateChildHistograms(HistogramAccumulation[] accumulations);
+ async AccumulateChildKeyedHistograms(KeyedHistogramAccumulation[] accumulations);
+ async UpdateChildScalars(ScalarAction[] actions);
+ async UpdateChildKeyedScalars(KeyedScalarAction[] actions);
+ async RecordChildEvents(ChildEventData[] events);
+ async RecordDiscardedData(DiscardedData data);
+
+ // Sent from time-to-time to limit the amount of telemetry vulnerable to loss
+ // Buffer contains bincoded Rust structs.
+ // https://firefox-source-docs.mozilla.org/toolkit/components/glean/dev/ipc.html
+ async FOGData(ByteBuf buf);
+};
+
+} // namespace mozilla
diff --git a/dom/media/ipc/PRemoteDecoder.ipdl b/dom/media/ipc/PRemoteDecoder.ipdl
new file mode 100644
index 0000000000..a0af29a9d6
--- /dev/null
+++ b/dom/media/ipc/PRemoteDecoder.ipdl
@@ -0,0 +1,80 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include "mozilla/dom/MediaIPCUtils.h";
+
+include protocol PRemoteDecoderManager;
+
+using mozilla::MediaDataDecoder::ConversionRequired from "PlatformDecoderModule.h";
+using mozilla::TrackInfo::TrackType from "MediaInfo.h";
+using mozilla::layers::LayersBackend from "mozilla/layers/LayersTypes.h";
+using mozilla::MediaResult from "MediaResult.h";
+[RefCounted] using class mozilla::ArrayOfRemoteMediaRawData from "mozilla/RemoteMediaData.h";
+[RefCounted] using class mozilla::ArrayOfRemoteAudioData from "mozilla/RemoteMediaData.h";
+[RefCounted] using class mozilla::ArrayOfRemoteVideoData from "mozilla/RemoteMediaData.h";
+include PMediaDecoderParams;
+include LayersSurfaces;
+
+namespace mozilla {
+
+union DecodedOutputIPDL
+{
+ nullable ArrayOfRemoteAudioData;
+ nullable ArrayOfRemoteVideoData;
+};
+
+struct InitCompletionIPDL
+{
+ TrackType type;
+ nsCString decoderDescription;
+ nsCString decoderProcessName;
+ nsCString decoderCodecName;
+ bool hardware;
+ nsCString hardwareReason;
+ ConversionRequired conversion;
+};
+
+union InitResultIPDL
+{
+ MediaResult;
+ InitCompletionIPDL;
+};
+
+union DecodeResultIPDL
+{
+ MediaResult;
+ DecodedOutputIPDL;
+};
+
+// This protocol provides a way to use MediaDataDecoder across processes.
+// The parent side currently is only implemented to work with
+// RemoteDecoderModule or WindowsMediaFoundation.
+// The child side runs in the content process, and the parent side runs
+// in the RDD process or the GPU process. We run a separate IPDL thread
+// for both sides.
+[ManualDealloc]
+async protocol PRemoteDecoder
+{
+ manager PRemoteDecoderManager;
+parent:
+ async Construct() returns (MediaResult result);
+
+ async Init() returns (InitResultIPDL result);
+
+ // Each output may include a SurfaceDescriptorGPUVideo that represents the decoded
+ // frame. This SurfaceDescriptor can be used on the Layers IPDL protocol, but
+ // must be released explicitly using DeallocateSurfaceDescriptorGPUVideo
+ // on the manager protocol.
+ async Decode(nullable ArrayOfRemoteMediaRawData data) returns (DecodeResultIPDL result);
+ async Flush() returns (MediaResult error);
+ async Drain() returns (DecodeResultIPDL result);
+ async Shutdown() returns (bool unused);
+ // To clear the threshold, call with INT64_MIN.
+ async SetSeekThreshold(TimeUnit time);
+
+ async __delete__();
+};
+
+} // namespace mozilla
diff --git a/dom/media/ipc/PRemoteDecoderManager.ipdl b/dom/media/ipc/PRemoteDecoderManager.ipdl
new file mode 100644
index 0000000000..e92e1088da
--- /dev/null
+++ b/dom/media/ipc/PRemoteDecoderManager.ipdl
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifdef MOZ_WMF_MEDIA_ENGINE
+ include protocol PMFMediaEngine;
+#endif
+#ifdef MOZ_WMF_CDM
+ include protocol PMFCDM;
+#endif
+
+include protocol PTexture;
+include protocol PRemoteDecoder;
+include LayersSurfaces;
+include PMediaDecoderParams;
+include "mozilla/dom/MediaIPCUtils.h";
+include "mozilla/layers/LayersMessageUtils.h";
+
+using mozilla::VideoInfo from "MediaInfo.h";
+using mozilla::AudioInfo from "MediaInfo.h";
+using struct mozilla::layers::TextureFactoryIdentifier from "mozilla/layers/CompositorTypes.h";
+using mozilla::CreateDecoderParams::OptionSet from "PlatformDecoderModule.h";
+using mozilla::DecoderDoctorDiagnostics from "DecoderDoctorDiagnostics.h";
+using mozilla::TrackingId from "PerformanceRecorder.h";
+
+namespace mozilla {
+
+struct VideoDecoderInfoIPDL
+{
+ VideoInfo videoInfo;
+ float framerate;
+};
+
+union RemoteDecoderInfoIPDL
+{
+ AudioInfo;
+ VideoDecoderInfoIPDL;
+};
+
+[NeedsOtherPid]
+sync protocol PRemoteDecoderManager
+{
+ manages PRemoteDecoder;
+#ifdef MOZ_WMF_MEDIA_ENGINE
+ manages PMFMediaEngine;
+#endif
+#ifdef MOZ_WMF_CDM
+ manages PMFCDM;
+#endif
+
+parent:
+#ifdef MOZ_WMF_MEDIA_ENGINE
+ async PMFMediaEngine();
+#endif
+#ifdef MOZ_WMF_CDM
+ async PMFCDM(nsString keySystem);
+#endif
+ async PRemoteDecoder(RemoteDecoderInfoIPDL info,
+ OptionSet options,
+ TextureFactoryIdentifier? identifier,
+ uint64_t? mediaEngineId,
+ TrackingId? trackingId);
+
+ sync Readback(SurfaceDescriptorGPUVideo sd) returns (SurfaceDescriptor aResult);
+
+ async DeallocateSurfaceDescriptorGPUVideo(SurfaceDescriptorGPUVideo sd);
+};
+
+} // namespace mozilla
diff --git a/dom/media/ipc/RDDChild.cpp b/dom/media/ipc/RDDChild.cpp
new file mode 100644
index 0000000000..fb2e14bb4f
--- /dev/null
+++ b/dom/media/ipc/RDDChild.cpp
@@ -0,0 +1,225 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "RDDChild.h"
+
+#include "mozilla/FOGIPC.h"
+#include "mozilla/RDDProcessManager.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/MemoryReportRequest.h"
+#include "mozilla/gfx/GPUProcessManager.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/ipc/CrashReporterHost.h"
+#include "mozilla/ipc/Endpoint.h"
+
+#if defined(XP_LINUX) && defined(MOZ_SANDBOX)
+# include "mozilla/SandboxBroker.h"
+# include "mozilla/SandboxBrokerPolicyFactory.h"
+#endif
+
+#include "mozilla/Telemetry.h"
+#include "mozilla/TelemetryIPC.h"
+
+#if defined(XP_WIN)
+# include "mozilla/WinDllServices.h"
+#endif
+
+#include "ProfilerParent.h"
+#include "RDDProcessHost.h"
+
+namespace mozilla {
+
+using namespace layers;
+using namespace gfx;
+
+RDDChild::RDDChild(RDDProcessHost* aHost) : mHost(aHost) {}
+
+RDDChild::~RDDChild() = default;
+
+bool RDDChild::Init() {
+ Maybe<FileDescriptor> brokerFd;
+
+#if defined(XP_LINUX) && defined(MOZ_SANDBOX)
+ auto policy = SandboxBrokerPolicyFactory::GetRDDPolicy(OtherPid());
+ if (policy != nullptr) {
+ brokerFd = Some(FileDescriptor());
+ mSandboxBroker =
+ SandboxBroker::Create(std::move(policy), OtherPid(), brokerFd.ref());
+ // This is unlikely to fail and probably indicates OS resource
+ // exhaustion, but we can at least try to recover.
+ if (NS_WARN_IF(mSandboxBroker == nullptr)) {
+ return false;
+ }
+ MOZ_ASSERT(brokerFd.ref().IsValid());
+ }
+#endif // XP_LINUX && MOZ_SANDBOX
+
+ nsTArray<GfxVarUpdate> updates = gfxVars::FetchNonDefaultVars();
+
+ bool isReadyForBackgroundProcessing = false;
+#if defined(XP_WIN)
+ RefPtr<DllServices> dllSvc(DllServices::Get());
+ isReadyForBackgroundProcessing = dllSvc->IsReadyForBackgroundProcessing();
+#endif
+
+ SendInit(updates, brokerFd, Telemetry::CanRecordReleaseData(),
+ isReadyForBackgroundProcessing);
+
+ Unused << SendInitProfiler(ProfilerParent::CreateForProcess(OtherPid()));
+
+ gfxVars::AddReceiver(this);
+ auto* gpm = gfx::GPUProcessManager::Get();
+ if (gpm) {
+ gpm->AddListener(this);
+ }
+
+ return true;
+}
+
+bool RDDChild::SendRequestMemoryReport(const uint32_t& aGeneration,
+ const bool& aAnonymize,
+ const bool& aMinimizeMemoryUsage,
+ const Maybe<FileDescriptor>& aDMDFile) {
+ mMemoryReportRequest = MakeUnique<MemoryReportRequestHost>(aGeneration);
+
+ PRDDChild::SendRequestMemoryReport(
+ aGeneration, aAnonymize, aMinimizeMemoryUsage, aDMDFile,
+ [&](const uint32_t& aGeneration2) {
+ if (RDDProcessManager* rddpm = RDDProcessManager::Get()) {
+ if (RDDChild* child = rddpm->GetRDDChild()) {
+ if (child->mMemoryReportRequest) {
+ child->mMemoryReportRequest->Finish(aGeneration2);
+ child->mMemoryReportRequest = nullptr;
+ }
+ }
+ }
+ },
+ [&](mozilla::ipc::ResponseRejectReason) {
+ if (RDDProcessManager* rddpm = RDDProcessManager::Get()) {
+ if (RDDChild* child = rddpm->GetRDDChild()) {
+ child->mMemoryReportRequest = nullptr;
+ }
+ }
+ });
+
+ return true;
+}
+
+void RDDChild::OnCompositorUnexpectedShutdown() {
+ auto* rddm = RDDProcessManager::Get();
+ if (rddm) {
+ rddm->CreateVideoBridge();
+ }
+}
+
+void RDDChild::OnVarChanged(const GfxVarUpdate& aVar) { SendUpdateVar(aVar); }
+
+mozilla::ipc::IPCResult RDDChild::RecvAddMemoryReport(
+ const MemoryReport& aReport) {
+ if (mMemoryReportRequest) {
+ mMemoryReportRequest->RecvReport(aReport);
+ }
+ return IPC_OK();
+}
+
+#if defined(XP_WIN)
+mozilla::ipc::IPCResult RDDChild::RecvGetModulesTrust(
+ ModulePaths&& aModPaths, bool aRunAtNormalPriority,
+ GetModulesTrustResolver&& aResolver) {
+ RefPtr<DllServices> dllSvc(DllServices::Get());
+ dllSvc->GetModulesTrust(std::move(aModPaths), aRunAtNormalPriority)
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [aResolver](ModulesMapResult&& aResult) {
+ aResolver(Some(ModulesMapResult(std::move(aResult))));
+ },
+ [aResolver](nsresult aRv) { aResolver(Nothing()); });
+ return IPC_OK();
+}
+#endif // defined(XP_WIN)
+
+mozilla::ipc::IPCResult RDDChild::RecvUpdateMediaCodecsSupported(
+ const media::MediaCodecsSupported& aSupported) {
+ dom::ContentParent::BroadcastMediaCodecsSupportedUpdate(
+ RemoteDecodeIn::RddProcess, aSupported);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult RDDChild::RecvAccumulateChildHistograms(
+ nsTArray<HistogramAccumulation>&& aAccumulations) {
+ TelemetryIPC::AccumulateChildHistograms(Telemetry::ProcessID::Rdd,
+ aAccumulations);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult RDDChild::RecvAccumulateChildKeyedHistograms(
+ nsTArray<KeyedHistogramAccumulation>&& aAccumulations) {
+ TelemetryIPC::AccumulateChildKeyedHistograms(Telemetry::ProcessID::Rdd,
+ aAccumulations);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult RDDChild::RecvUpdateChildScalars(
+ nsTArray<ScalarAction>&& aScalarActions) {
+ TelemetryIPC::UpdateChildScalars(Telemetry::ProcessID::Rdd, aScalarActions);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult RDDChild::RecvUpdateChildKeyedScalars(
+ nsTArray<KeyedScalarAction>&& aScalarActions) {
+ TelemetryIPC::UpdateChildKeyedScalars(Telemetry::ProcessID::Rdd,
+ aScalarActions);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult RDDChild::RecvRecordChildEvents(
+ nsTArray<mozilla::Telemetry::ChildEventData>&& aEvents) {
+ TelemetryIPC::RecordChildEvents(Telemetry::ProcessID::Rdd, aEvents);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult RDDChild::RecvRecordDiscardedData(
+ const mozilla::Telemetry::DiscardedData& aDiscardedData) {
+ TelemetryIPC::RecordDiscardedData(Telemetry::ProcessID::Rdd, aDiscardedData);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult RDDChild::RecvFOGData(ByteBuf&& aBuf) {
+ glean::FOGData(std::move(aBuf));
+ return IPC_OK();
+}
+
+void RDDChild::ActorDestroy(ActorDestroyReason aWhy) {
+ if (aWhy == AbnormalShutdown) {
+ GenerateCrashReport(OtherPid());
+ }
+
+ auto* gpm = gfx::GPUProcessManager::Get();
+ if (gpm) {
+ // Note: the manager could have shutdown already.
+ gpm->RemoveListener(this);
+ }
+
+ gfxVars::RemoveReceiver(this);
+ mHost->OnChannelClosed();
+}
+
+class DeferredDeleteRDDChild : public Runnable {
+ public:
+ explicit DeferredDeleteRDDChild(RefPtr<RDDChild>&& aChild)
+ : Runnable("gfx::DeferredDeleteRDDChild"), mChild(std::move(aChild)) {}
+
+ NS_IMETHODIMP Run() override { return NS_OK; }
+
+ private:
+ RefPtr<RDDChild> mChild;
+};
+
+/* static */
+void RDDChild::Destroy(RefPtr<RDDChild>&& aChild) {
+ NS_DispatchToMainThread(new DeferredDeleteRDDChild(std::move(aChild)));
+}
+
+} // namespace mozilla
diff --git a/dom/media/ipc/RDDChild.h b/dom/media/ipc/RDDChild.h
new file mode 100644
index 0000000000..1e0e1b439d
--- /dev/null
+++ b/dom/media/ipc/RDDChild.h
@@ -0,0 +1,86 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef _include_dom_media_ipc_RDDChild_h_
+#define _include_dom_media_ipc_RDDChild_h_
+#include "mozilla/PRDDChild.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/gfx/GPUProcessListener.h"
+#include "mozilla/gfx/gfxVarReceiver.h"
+#include "mozilla/ipc/CrashReporterHelper.h"
+
+namespace mozilla {
+
+#if defined(XP_LINUX) && defined(MOZ_SANDBOX)
+class SandboxBroker;
+#endif
+
+namespace dom {
+class MemoryReportRequestHost;
+} // namespace dom
+
+class RDDProcessHost;
+
+class RDDChild final : public PRDDChild,
+ public ipc::CrashReporterHelper<GeckoProcessType_RDD>,
+ public gfx::gfxVarReceiver,
+ public gfx::GPUProcessListener {
+ typedef mozilla::dom::MemoryReportRequestHost MemoryReportRequestHost;
+
+ public:
+ NS_INLINE_DECL_REFCOUNTING(RDDChild, final)
+
+ explicit RDDChild(RDDProcessHost* aHost);
+
+ bool Init();
+
+ void OnCompositorUnexpectedShutdown() override;
+ void OnVarChanged(const GfxVarUpdate& aVar) override;
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ mozilla::ipc::IPCResult RecvAddMemoryReport(const MemoryReport& aReport);
+#if defined(XP_WIN)
+ mozilla::ipc::IPCResult RecvGetModulesTrust(
+ ModulePaths&& aModPaths, bool aRunAtNormalPriority,
+ GetModulesTrustResolver&& aResolver);
+#endif // defined(XP_WIN)
+ mozilla::ipc::IPCResult RecvUpdateMediaCodecsSupported(
+ const media::MediaCodecsSupported& aSupported);
+ mozilla::ipc::IPCResult RecvFOGData(ByteBuf&& aBuf);
+
+ mozilla::ipc::IPCResult RecvAccumulateChildHistograms(
+ nsTArray<HistogramAccumulation>&& aAccumulations);
+ mozilla::ipc::IPCResult RecvAccumulateChildKeyedHistograms(
+ nsTArray<KeyedHistogramAccumulation>&& aAccumulations);
+ mozilla::ipc::IPCResult RecvUpdateChildScalars(
+ nsTArray<ScalarAction>&& aScalarActions);
+ mozilla::ipc::IPCResult RecvUpdateChildKeyedScalars(
+ nsTArray<KeyedScalarAction>&& aScalarActions);
+ mozilla::ipc::IPCResult RecvRecordChildEvents(
+ nsTArray<ChildEventData>&& events);
+ mozilla::ipc::IPCResult RecvRecordDiscardedData(
+ const DiscardedData& aDiscardedData);
+
+ bool SendRequestMemoryReport(const uint32_t& aGeneration,
+ const bool& aAnonymize,
+ const bool& aMinimizeMemoryUsage,
+ const Maybe<ipc::FileDescriptor>& aDMDFile);
+
+ static void Destroy(RefPtr<RDDChild>&& aChild);
+
+ private:
+ ~RDDChild();
+
+ RDDProcessHost* mHost;
+ UniquePtr<MemoryReportRequestHost> mMemoryReportRequest;
+#if defined(XP_LINUX) && defined(MOZ_SANDBOX)
+ UniquePtr<SandboxBroker> mSandboxBroker;
+#endif
+};
+
+} // namespace mozilla
+
+#endif // _include_dom_media_ipc_RDDChild_h_
diff --git a/dom/media/ipc/RDDParent.cpp b/dom/media/ipc/RDDParent.cpp
new file mode 100644
index 0000000000..0fae2703e8
--- /dev/null
+++ b/dom/media/ipc/RDDParent.cpp
@@ -0,0 +1,340 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "RDDParent.h"
+
+#if defined(XP_WIN)
+# include <dwrite.h>
+# include <process.h>
+
+# include "WMF.h"
+# include "WMFDecoderModule.h"
+# include "mozilla/WinDllServices.h"
+# include "mozilla/gfx/DeviceManagerDx.h"
+#else
+# include <unistd.h>
+#endif
+
+#include "PDMFactory.h"
+#include "gfxConfig.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/FOGIPC.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/RemoteDecoderManagerParent.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/dom/MemoryReportRequest.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/glean/GleanMetrics.h"
+#include "mozilla/ipc/CrashReporterClient.h"
+#include "mozilla/ipc/ProcessChild.h"
+
+#if defined(XP_LINUX) && defined(MOZ_SANDBOX)
+# include "mozilla/Sandbox.h"
+#endif
+
+#include "ChildProfilerController.h"
+
+#if defined(XP_MACOSX) && defined(MOZ_SANDBOX)
+# include "RDDProcessHost.h"
+# include "mozilla/Sandbox.h"
+# include "nsMacUtilsImpl.h"
+#endif
+
+#include "mozilla/ipc/ProcessUtils.h"
+#include "nsDebugImpl.h"
+#include "nsIXULRuntime.h"
+#include "nsThreadManager.h"
+
+#if defined(MOZ_SANDBOX) && defined(MOZ_DEBUG) && defined(ENABLE_TESTS)
+# include "mozilla/SandboxTestingChild.h"
+#endif
+
+namespace mozilla {
+
+using namespace ipc;
+using namespace gfx;
+
+static RDDParent* sRDDParent;
+
+RDDParent::RDDParent() : mLaunchTime(TimeStamp::Now()) { sRDDParent = this; }
+
+RDDParent::~RDDParent() { sRDDParent = nullptr; }
+
+/* static */
+RDDParent* RDDParent::GetSingleton() {
+ MOZ_DIAGNOSTIC_ASSERT(sRDDParent);
+ return sRDDParent;
+}
+
+bool RDDParent::Init(mozilla::ipc::UntypedEndpoint&& aEndpoint,
+ const char* aParentBuildID) {
+ // Initialize the thread manager before starting IPC. Otherwise, messages
+ // may be posted to the main thread and we won't be able to process them.
+ if (NS_WARN_IF(NS_FAILED(nsThreadManager::get().Init()))) {
+ return false;
+ }
+
+ // Now it's safe to start IPC.
+ if (NS_WARN_IF(!aEndpoint.Bind(this))) {
+ return false;
+ }
+
+ nsDebugImpl::SetMultiprocessMode("RDD");
+
+ // This must be checked before any IPDL message, which may hit sentinel
+ // errors due to parent and content processes having different
+ // versions.
+ MessageChannel* channel = GetIPCChannel();
+ if (channel && !channel->SendBuildIDsMatchMessage(aParentBuildID)) {
+ // We need to quit this process if the buildID doesn't match the parent's.
+ // This can occur when an update occurred in the background.
+ ProcessChild::QuickExit();
+ }
+
+ // Init crash reporter support.
+ CrashReporterClient::InitSingleton(this);
+
+ if (NS_FAILED(NS_InitMinimalXPCOM())) {
+ return false;
+ }
+
+ gfxConfig::Init();
+ gfxVars::Initialize();
+#ifdef XP_WIN
+ DeviceManagerDx::Init();
+ auto rv = wmf::MediaFoundationInitializer::HasInitialized();
+ if (!rv) {
+ NS_WARNING("Failed to init Media Foundation in the RDD process");
+ }
+#endif
+
+ mozilla::ipc::SetThisProcessName("RDD Process");
+
+ return true;
+}
+
+#if defined(XP_MACOSX) && defined(MOZ_SANDBOX)
+extern "C" {
+void CGSShutdownServerConnections();
+};
+#endif
+
+mozilla::ipc::IPCResult RDDParent::RecvInit(
+ nsTArray<GfxVarUpdate>&& vars, const Maybe<FileDescriptor>& aBrokerFd,
+ const bool& aCanRecordReleaseTelemetry,
+ const bool& aIsReadyForBackgroundProcessing) {
+ for (const auto& var : vars) {
+ gfxVars::ApplyUpdate(var);
+ }
+
+ auto supported = PDMFactory::Supported();
+ Unused << SendUpdateMediaCodecsSupported(supported);
+
+#if defined(MOZ_SANDBOX)
+# if defined(XP_MACOSX)
+ // Close all current connections to the WindowServer. This ensures that the
+ // Activity Monitor will not label the content process as "Not responding"
+ // because it's not running a native event loop. See bug 1384336.
+ CGSShutdownServerConnections();
+
+# elif defined(XP_LINUX)
+ int fd = -1;
+ if (aBrokerFd.isSome()) {
+ fd = aBrokerFd.value().ClonePlatformHandle().release();
+ }
+ SetRemoteDataDecoderSandbox(fd);
+# endif // XP_MACOSX/XP_LINUX
+#endif // MOZ_SANDBOX
+
+#if defined(XP_WIN)
+ if (aCanRecordReleaseTelemetry) {
+ RefPtr<DllServices> dllSvc(DllServices::Get());
+ dllSvc->StartUntrustedModulesProcessor(aIsReadyForBackgroundProcessing);
+ }
+#endif // defined(XP_WIN)
+ return IPC_OK();
+}
+
+IPCResult RDDParent::RecvUpdateVar(const GfxVarUpdate& aUpdate) {
+#if defined(XP_WIN)
+ auto scopeExit = MakeScopeExit(
+ [couldUseHWDecoder = gfx::gfxVars::CanUseHardwareVideoDecoding()] {
+ if (couldUseHWDecoder != gfx::gfxVars::CanUseHardwareVideoDecoding()) {
+ // The capabilities of the system may have changed, force a refresh by
+ // re-initializing the WMF PDM.
+ WMFDecoderModule::Init();
+ Unused << RDDParent::GetSingleton()->SendUpdateMediaCodecsSupported(
+ PDMFactory::Supported(true /* force refresh */));
+ }
+ });
+#endif
+ gfxVars::ApplyUpdate(aUpdate);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult RDDParent::RecvInitProfiler(
+ Endpoint<PProfilerChild>&& aEndpoint) {
+ mProfilerController = ChildProfilerController::Create(std::move(aEndpoint));
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult RDDParent::RecvNewContentRemoteDecoderManager(
+ Endpoint<PRemoteDecoderManagerParent>&& aEndpoint,
+ const ContentParentId& aParentId) {
+ if (!RemoteDecoderManagerParent::CreateForContent(std::move(aEndpoint),
+ aParentId)) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult RDDParent::RecvInitVideoBridge(
+ Endpoint<PVideoBridgeChild>&& aEndpoint, const bool& aCreateHardwareDevice,
+ const ContentDeviceData& aContentDeviceData) {
+ if (!RemoteDecoderManagerParent::CreateVideoBridgeToOtherProcess(
+ std::move(aEndpoint))) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+
+ gfxConfig::Inherit(
+ {
+ Feature::HW_COMPOSITING,
+ Feature::D3D11_COMPOSITING,
+ Feature::OPENGL_COMPOSITING,
+ Feature::DIRECT2D,
+ },
+ aContentDeviceData.prefs());
+#ifdef XP_WIN
+ if (gfxConfig::IsEnabled(Feature::D3D11_COMPOSITING)) {
+ auto* devmgr = DeviceManagerDx::Get();
+ if (devmgr) {
+ devmgr->ImportDeviceInfo(aContentDeviceData.d3d11());
+ if (aCreateHardwareDevice) {
+ devmgr->CreateContentDevices();
+ }
+ }
+ }
+#endif
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult RDDParent::RecvRequestMemoryReport(
+ const uint32_t& aGeneration, const bool& aAnonymize,
+ const bool& aMinimizeMemoryUsage, const Maybe<FileDescriptor>& aDMDFile,
+ const RequestMemoryReportResolver& aResolver) {
+ nsPrintfCString processName("RDD (pid %u)", (unsigned)getpid());
+
+ mozilla::dom::MemoryReportRequestClient::Start(
+ aGeneration, aAnonymize, aMinimizeMemoryUsage, aDMDFile, processName,
+ [&](const MemoryReport& aReport) {
+ Unused << GetSingleton()->SendAddMemoryReport(aReport);
+ },
+ aResolver);
+ return IPC_OK();
+}
+
+#if defined(XP_WIN)
+mozilla::ipc::IPCResult RDDParent::RecvGetUntrustedModulesData(
+ GetUntrustedModulesDataResolver&& aResolver) {
+ RefPtr<DllServices> dllSvc(DllServices::Get());
+ dllSvc->GetUntrustedModulesData()->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [aResolver](Maybe<UntrustedModulesData>&& aData) {
+ aResolver(std::move(aData));
+ },
+ [aResolver](nsresult aReason) { aResolver(Nothing()); });
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult RDDParent::RecvUnblockUntrustedModulesThread() {
+ if (nsCOMPtr<nsIObserverService> obs =
+ mozilla::services::GetObserverService()) {
+ obs->NotifyObservers(nullptr, "unblock-untrusted-modules-thread", nullptr);
+ }
+ return IPC_OK();
+}
+#endif // defined(XP_WIN)
+
+mozilla::ipc::IPCResult RDDParent::RecvPreferenceUpdate(const Pref& aPref) {
+ Preferences::SetPreference(aPref);
+ return IPC_OK();
+}
+
+#if defined(MOZ_SANDBOX) && defined(MOZ_DEBUG) && defined(ENABLE_TESTS)
+mozilla::ipc::IPCResult RDDParent::RecvInitSandboxTesting(
+ Endpoint<PSandboxTestingChild>&& aEndpoint) {
+ if (!SandboxTestingChild::Initialize(std::move(aEndpoint))) {
+ return IPC_FAIL(
+ this, "InitSandboxTesting failed to initialise the child process.");
+ }
+ return IPC_OK();
+}
+#endif
+
+mozilla::ipc::IPCResult RDDParent::RecvFlushFOGData(
+ FlushFOGDataResolver&& aResolver) {
+ glean::FlushFOGData(std::move(aResolver));
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult RDDParent::RecvTestTriggerMetrics(
+ TestTriggerMetricsResolver&& aResolve) {
+ mozilla::glean::test_only_ipc::a_counter.Add(nsIXULRuntime::PROCESS_TYPE_RDD);
+ aResolve(true);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult RDDParent::RecvTestTelemetryProbes() {
+ const uint32_t kExpectedUintValue = 42;
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_RDD_ONLY_UINT,
+ kExpectedUintValue);
+ return IPC_OK();
+}
+
+void RDDParent::ActorDestroy(ActorDestroyReason aWhy) {
+ if (AbnormalShutdown == aWhy) {
+ NS_WARNING("Shutting down RDD process early due to a crash!");
+ Telemetry::Accumulate(Telemetry::SUBPROCESS_ABNORMAL_ABORT, "rdd"_ns, 1);
+ ProcessChild::QuickExit();
+ }
+
+ // Send the last bits of Glean data over to the main process.
+ glean::FlushFOGData(
+ [](ByteBuf&& aBuf) { glean::SendFOGData(std::move(aBuf)); });
+
+#ifndef NS_FREE_PERMANENT_DATA
+ // No point in going through XPCOM shutdown because we don't keep persistent
+ // state.
+ ProcessChild::QuickExit();
+#endif
+
+ // Wait until all RemoteDecoderManagerParent have closed.
+ mShutdownBlockers.WaitUntilClear(10 * 1000 /* 10s timeout*/)
+ ->Then(GetCurrentSerialEventTarget(), __func__, [&]() {
+
+#if defined(XP_WIN)
+ RefPtr<DllServices> dllSvc(DllServices::Get());
+ dllSvc->DisableFull();
+#endif // defined(XP_WIN)
+
+ if (mProfilerController) {
+ mProfilerController->Shutdown();
+ mProfilerController = nullptr;
+ }
+
+ RemoteDecoderManagerParent::ShutdownVideoBridge();
+
+#ifdef XP_WIN
+ DeviceManagerDx::Shutdown();
+#endif
+ gfxVars::Shutdown();
+ gfxConfig::Shutdown();
+ CrashReporterClient::DestroySingleton();
+ XRE_ShutdownChildProcess();
+ });
+}
+
+} // namespace mozilla
diff --git a/dom/media/ipc/RDDParent.h b/dom/media/ipc/RDDParent.h
new file mode 100644
index 0000000000..9c682453c9
--- /dev/null
+++ b/dom/media/ipc/RDDParent.h
@@ -0,0 +1,81 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef _include_dom_media_ipc_RDDParent_h__
+#define _include_dom_media_ipc_RDDParent_h__
+#include "mozilla/PRDDParent.h"
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/ipc/AsyncBlockers.h"
+
+namespace mozilla {
+
+class TimeStamp;
+class ChildProfilerController;
+
+class RDDParent final : public PRDDParent {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(RDDParent, final)
+
+ RDDParent();
+
+ static RDDParent* GetSingleton();
+
+ ipc::AsyncBlockers& AsyncShutdownService() { return mShutdownBlockers; }
+
+ bool Init(mozilla::ipc::UntypedEndpoint&& aEndpoint,
+ const char* aParentBuildID);
+
+ mozilla::ipc::IPCResult RecvInit(nsTArray<GfxVarUpdate>&& vars,
+ const Maybe<ipc::FileDescriptor>& aBrokerFd,
+ const bool& aCanRecordReleaseTelemetry,
+ const bool& aIsReadyForBackgroundProcessing);
+ mozilla::ipc::IPCResult RecvInitProfiler(
+ Endpoint<PProfilerChild>&& aEndpoint);
+
+ mozilla::ipc::IPCResult RecvNewContentRemoteDecoderManager(
+ Endpoint<PRemoteDecoderManagerParent>&& aEndpoint,
+ const ContentParentId& aParentId);
+ mozilla::ipc::IPCResult RecvInitVideoBridge(
+ Endpoint<PVideoBridgeChild>&& aEndpoint,
+ const bool& aCreateHardwareDevice,
+ const ContentDeviceData& aContentDeviceData);
+ mozilla::ipc::IPCResult RecvRequestMemoryReport(
+ const uint32_t& generation, const bool& anonymize,
+ const bool& minimizeMemoryUsage,
+ const Maybe<ipc::FileDescriptor>& DMDFile,
+ const RequestMemoryReportResolver& aResolver);
+#if defined(XP_WIN)
+ mozilla::ipc::IPCResult RecvGetUntrustedModulesData(
+ GetUntrustedModulesDataResolver&& aResolver);
+ mozilla::ipc::IPCResult RecvUnblockUntrustedModulesThread();
+#endif // defined(XP_WIN)
+ mozilla::ipc::IPCResult RecvPreferenceUpdate(const Pref& pref);
+ mozilla::ipc::IPCResult RecvUpdateVar(const GfxVarUpdate& pref);
+
+#if defined(MOZ_SANDBOX) && defined(MOZ_DEBUG) && defined(ENABLE_TESTS)
+ mozilla::ipc::IPCResult RecvInitSandboxTesting(
+ Endpoint<PSandboxTestingChild>&& aEndpoint);
+#endif
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ mozilla::ipc::IPCResult RecvFlushFOGData(FlushFOGDataResolver&& aResolver);
+
+ mozilla::ipc::IPCResult RecvTestTriggerMetrics(
+ TestTriggerMetricsResolver&& aResolve);
+
+ mozilla::ipc::IPCResult RecvTestTelemetryProbes();
+
+ private:
+ ~RDDParent();
+
+ const TimeStamp mLaunchTime;
+ RefPtr<ChildProfilerController> mProfilerController;
+ ipc::AsyncBlockers mShutdownBlockers;
+};
+
+} // namespace mozilla
+
+#endif // _include_dom_media_ipc_RDDParent_h__
diff --git a/dom/media/ipc/RDDProcessHost.cpp b/dom/media/ipc/RDDProcessHost.cpp
new file mode 100644
index 0000000000..41fcea38b6
--- /dev/null
+++ b/dom/media/ipc/RDDProcessHost.cpp
@@ -0,0 +1,312 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "RDDProcessHost.h"
+
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/ipc/ProcessUtils.h"
+#include "RDDChild.h"
+#include "chrome/common/process_watcher.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_media.h"
+
+#if defined(XP_MACOSX) && defined(MOZ_SANDBOX)
+# include "mozilla/Sandbox.h"
+#endif
+
+namespace mozilla {
+
+using namespace ipc;
+
+#if defined(XP_MACOSX) && defined(MOZ_SANDBOX)
+bool RDDProcessHost::sLaunchWithMacSandbox = false;
+#endif
+
+RDDProcessHost::RDDProcessHost(Listener* aListener)
+ : GeckoChildProcessHost(GeckoProcessType_RDD),
+ mListener(aListener),
+ mLiveToken(new media::Refcountable<bool>(true)) {
+ MOZ_COUNT_CTOR(RDDProcessHost);
+
+#if defined(XP_MACOSX) && defined(MOZ_SANDBOX)
+ if (!sLaunchWithMacSandbox) {
+ sLaunchWithMacSandbox = (PR_GetEnv("MOZ_DISABLE_RDD_SANDBOX") == nullptr);
+ }
+ mDisableOSActivityMode = sLaunchWithMacSandbox;
+#endif
+}
+
+RDDProcessHost::~RDDProcessHost() { MOZ_COUNT_DTOR(RDDProcessHost); }
+
+bool RDDProcessHost::Launch(StringVector aExtraOpts) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ MOZ_ASSERT(mLaunchPhase == LaunchPhase::Unlaunched);
+ MOZ_ASSERT(!mRDDChild);
+
+ mPrefSerializer = MakeUnique<ipc::SharedPreferenceSerializer>();
+ if (!mPrefSerializer->SerializeToSharedMemory(GeckoProcessType_RDD,
+ /* remoteType */ ""_ns)) {
+ return false;
+ }
+ mPrefSerializer->AddSharedPrefCmdLineArgs(*this, aExtraOpts);
+
+#if defined(XP_WIN) && defined(MOZ_SANDBOX)
+ mSandboxLevel = Preferences::GetInt("security.sandbox.rdd.level");
+#endif
+
+ mLaunchPhase = LaunchPhase::Waiting;
+ mLaunchTime = TimeStamp::Now();
+
+ int32_t timeoutMs = StaticPrefs::media_rdd_process_startup_timeout_ms();
+
+ // If one of the following environment variables are set we can
+ // effectively ignore the timeout - as we can guarantee the RDD
+ // process will be terminated
+ if (PR_GetEnv("MOZ_DEBUG_CHILD_PROCESS") ||
+ PR_GetEnv("MOZ_DEBUG_CHILD_PAUSE")) {
+ timeoutMs = 0;
+ }
+ if (timeoutMs) {
+ // We queue a delayed task. If that task runs before the
+ // WhenProcessHandleReady promise gets resolved, we will abort the launch.
+ GetMainThreadSerialEventTarget()->DelayedDispatch(
+ NS_NewRunnableFunction(
+ "RDDProcessHost::Launchtimeout",
+ [this, liveToken = mLiveToken]() {
+ if (!*liveToken || mTimerChecked) {
+ // We have been deleted or the runnable has already started, we
+ // can abort.
+ return;
+ }
+ InitAfterConnect(false);
+ MOZ_ASSERT(mTimerChecked,
+ "InitAfterConnect must have acted on the promise");
+ }),
+ timeoutMs);
+ }
+
+ if (!GeckoChildProcessHost::AsyncLaunch(aExtraOpts)) {
+ mLaunchPhase = LaunchPhase::Complete;
+ mPrefSerializer = nullptr;
+ return false;
+ }
+ return true;
+}
+
+RefPtr<GenericNonExclusivePromise> RDDProcessHost::LaunchPromise() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mLaunchPromise) {
+ return mLaunchPromise;
+ }
+ mLaunchPromise = MakeRefPtr<GenericNonExclusivePromise::Private>(__func__);
+ WhenProcessHandleReady()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [this, liveToken = mLiveToken](
+ const ipc::ProcessHandlePromise::ResolveOrRejectValue& aResult) {
+ if (!*liveToken) {
+ // The RDDProcessHost got deleted. Abort. The promise would have
+ // already been rejected.
+ return;
+ }
+ if (mTimerChecked) {
+ // We hit the timeout earlier, abort.
+ return;
+ }
+ mTimerChecked = true;
+ if (aResult.IsReject()) {
+ RejectPromise();
+ }
+ // If aResult.IsResolve() then we have succeeded in launching the
+ // RDD process. The promise will be resolved once the channel has
+ // connected (or failed to) later.
+ });
+ return mLaunchPromise;
+}
+
+void RDDProcessHost::OnChannelConnected(base::ProcessId peer_pid) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ GeckoChildProcessHost::OnChannelConnected(peer_pid);
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "RDDProcessHost::OnChannelConnected", [this, liveToken = mLiveToken]() {
+ if (*liveToken && mLaunchPhase == LaunchPhase::Waiting) {
+ InitAfterConnect(true);
+ }
+ }));
+}
+
+void RDDProcessHost::OnChannelError() {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ GeckoChildProcessHost::OnChannelError();
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "RDDProcessHost::OnChannelError", [this, liveToken = mLiveToken]() {
+ if (*liveToken && mLaunchPhase == LaunchPhase::Waiting) {
+ InitAfterConnect(false);
+ }
+ }));
+}
+
+static uint64_t sRDDProcessTokenCounter = 0;
+
+void RDDProcessHost::InitAfterConnect(bool aSucceeded) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ MOZ_ASSERT(mLaunchPhase == LaunchPhase::Waiting);
+ MOZ_ASSERT(!mRDDChild);
+
+ mLaunchPhase = LaunchPhase::Complete;
+
+ if (!aSucceeded) {
+ RejectPromise();
+ return;
+ }
+ mProcessToken = ++sRDDProcessTokenCounter;
+ mRDDChild = MakeRefPtr<RDDChild>(this);
+ DebugOnly<bool> rv = TakeInitialEndpoint().Bind(mRDDChild.get());
+ MOZ_ASSERT(rv);
+
+ // Only clear mPrefSerializer in the success case to avoid a
+ // possible race in the case case of a timeout on Windows launch.
+ // See Bug 1555076 comment 7:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1555076#c7
+ mPrefSerializer = nullptr;
+
+ if (!mRDDChild->Init()) {
+ // Can't just kill here because it will create a timing race that
+ // will crash the tab. We don't really want to crash the tab just
+ // because RDD linux sandbox failed to initialize. In this case,
+ // we'll close the child channel which will cause the RDD process
+ // to shutdown nicely avoiding the tab crash (which manifests as
+ // Bug 1535335).
+ mRDDChild->Close();
+ RejectPromise();
+ } else {
+ ResolvePromise();
+ }
+}
+
+void RDDProcessHost::Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!mShutdownRequested);
+
+ RejectPromise();
+
+ if (mRDDChild) {
+ // OnChannelClosed uses this to check if the shutdown was expected or
+ // unexpected.
+ mShutdownRequested = true;
+
+ // The channel might already be closed if we got here unexpectedly.
+ if (!mChannelClosed) {
+ mRDDChild->Close();
+ }
+
+#ifndef NS_FREE_PERMANENT_DATA
+ // No need to communicate shutdown, the RDD process doesn't need to
+ // communicate anything back.
+ KillHard("NormalShutdown");
+#endif
+
+ // If we're shutting down unexpectedly, we're in the middle of handling an
+ // ActorDestroy for PRDDChild, which is still on the stack. We'll return
+ // back to OnChannelClosed.
+ //
+ // Otherwise, we'll wait for OnChannelClose to be called whenever PRDDChild
+ // acknowledges shutdown.
+ return;
+ }
+
+ DestroyProcess();
+}
+
+void RDDProcessHost::OnChannelClosed() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mChannelClosed = true;
+ RejectPromise();
+
+ if (!mShutdownRequested && mListener) {
+ // This is an unclean shutdown. Notify our listener that we're going away.
+ mListener->OnProcessUnexpectedShutdown(this);
+ } else {
+ DestroyProcess();
+ }
+
+ // Release the actor.
+ RDDChild::Destroy(std::move(mRDDChild));
+}
+
+void RDDProcessHost::KillHard(const char* aReason) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ ProcessHandle handle = GetChildProcessHandle();
+ if (!base::KillProcess(handle, base::PROCESS_END_KILLED_BY_USER)) {
+ NS_WARNING("failed to kill subprocess!");
+ }
+
+ SetAlreadyDead();
+}
+
+uint64_t RDDProcessHost::GetProcessToken() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mProcessToken;
+}
+
+void RDDProcessHost::DestroyProcess() {
+ MOZ_ASSERT(NS_IsMainThread());
+ RejectPromise();
+
+ // Any pending tasks will be cancelled from now on.
+ *mLiveToken = false;
+
+ NS_DispatchToMainThread(
+ NS_NewRunnableFunction("DestroyProcessRunnable", [this] { Destroy(); }));
+}
+
+void RDDProcessHost::ResolvePromise() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mLaunchPromiseSettled) {
+ mLaunchPromise->Resolve(true, __func__);
+ mLaunchPromiseSettled = true;
+ }
+ // We have already acted on the promise; the timeout runnable no longer needs
+ // to interrupt anything.
+ mTimerChecked = true;
+}
+
+void RDDProcessHost::RejectPromise() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mLaunchPromiseSettled) {
+ mLaunchPromise->Reject(NS_ERROR_FAILURE, __func__);
+ mLaunchPromiseSettled = true;
+ }
+ // We have already acted on the promise; the timeout runnable no longer needs
+ // to interrupt anything.
+ mTimerChecked = true;
+}
+
+#if defined(XP_MACOSX) && defined(MOZ_SANDBOX)
+bool RDDProcessHost::FillMacSandboxInfo(MacSandboxInfo& aInfo) {
+ GeckoChildProcessHost::FillMacSandboxInfo(aInfo);
+ if (!aInfo.shouldLog && PR_GetEnv("MOZ_SANDBOX_RDD_LOGGING")) {
+ aInfo.shouldLog = true;
+ }
+ return true;
+}
+
+/* static */
+MacSandboxType RDDProcessHost::GetMacSandboxType() {
+ return MacSandboxType_RDD;
+}
+#endif
+
+} // namespace mozilla
diff --git a/dom/media/ipc/RDDProcessHost.h b/dom/media/ipc/RDDProcessHost.h
new file mode 100644
index 0000000000..20c2c69c15
--- /dev/null
+++ b/dom/media/ipc/RDDProcessHost.h
@@ -0,0 +1,161 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _include_dom_media_ipc_RDDProcessHost_h_
+#define _include_dom_media_ipc_RDDProcessHost_h_
+#include "mozilla/UniquePtr.h"
+#include "mozilla/ipc/GeckoChildProcessHost.h"
+#include "mozilla/ipc/ProtocolUtils.h"
+#include "mozilla/media/MediaUtils.h"
+
+namespace mozilla::ipc {
+class SharedPreferenceSerializer;
+} // namespace mozilla::ipc
+class nsITimer;
+
+namespace mozilla {
+
+class RDDChild;
+
+// RDDProcessHost is the "parent process" container for a subprocess handle and
+// IPC connection. It owns the parent process IPDL actor, which in this case,
+// is a RDDChild.
+//
+// RDDProcessHosts are allocated and managed by RDDProcessManager. For all
+// intents and purposes it is a singleton, though more than one may be allocated
+// at a time due to its shutdown being asynchronous.
+class RDDProcessHost final : public mozilla::ipc::GeckoChildProcessHost {
+ friend class RDDChild;
+
+ public:
+ class Listener {
+ public:
+ // The RDDProcessHost has unexpectedly shutdown or had its connection
+ // severed. This is not called if an error occurs after calling
+ // Shutdown().
+ virtual void OnProcessUnexpectedShutdown(RDDProcessHost* aHost) {}
+ };
+
+ explicit RDDProcessHost(Listener* listener);
+
+ // Launch the subprocess asynchronously. On failure, false is returned.
+ // Otherwise, true is returned. If succeeded, a follow-up call should be made
+ // to LaunchPromise() which will return a promise that will be resolved once
+ // the RDD process has launched and a channel has been established.
+ //
+ // @param aExtraOpts (StringVector)
+ // Extra options to pass to the subprocess.
+ bool Launch(StringVector aExtraOpts);
+
+ // Return a promise that will be resolved once the process has completed its
+ // launch. The promise will be immediately resolved if the launch has already
+ // succeeded.
+ RefPtr<GenericNonExclusivePromise> LaunchPromise();
+
+ // Inform the process that it should clean up its resources and shut
+ // down. This initiates an asynchronous shutdown sequence. After this
+ // method returns, it is safe for the caller to forget its pointer to
+ // the RDDProcessHost.
+ //
+ // After this returns, the attached Listener is no longer used.
+ void Shutdown();
+
+ // Return the actor for the top-level actor of the process. If the process
+ // has not connected yet, this returns null.
+ RDDChild* GetActor() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mRDDChild.get();
+ }
+
+ // Return a unique id for this process, guaranteed not to be shared with any
+ // past or future instance of RDDProcessHost.
+ uint64_t GetProcessToken() const;
+
+ bool IsConnected() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return !!mRDDChild;
+ }
+
+ // Return the time stamp for when we tried to launch the RDD process.
+ // This is currently used for Telemetry so that we can determine how
+ // long RDD processes take to spin up. Note this doesn't denote a
+ // successful launch, just when we attempted launch.
+ TimeStamp GetLaunchTime() const { return mLaunchTime; }
+
+ // Called on the IO thread.
+ void OnChannelConnected(base::ProcessId peer_pid) override;
+ void OnChannelError() override;
+
+ void SetListener(Listener* aListener);
+
+#if defined(XP_MACOSX) && defined(MOZ_SANDBOX)
+ // Return the sandbox type to be used with this process type.
+ static MacSandboxType GetMacSandboxType();
+#endif
+
+ private:
+ ~RDDProcessHost();
+
+ // Called on the main thread with true after a connection has been established
+ // or false if it failed (including if it failed before the timeout kicked in)
+ void InitAfterConnect(bool aSucceeded);
+
+ // Called on the main thread when the mRDDChild actor is shutting down.
+ void OnChannelClosed();
+
+ // Kill the remote process, triggering IPC shutdown.
+ void KillHard(const char* aReason);
+
+ void DestroyProcess();
+
+#if defined(XP_MACOSX) && defined(MOZ_SANDBOX)
+ static bool sLaunchWithMacSandbox;
+
+ // Sandbox the RDD process at launch for all instances
+ bool IsMacSandboxLaunchEnabled() override { return sLaunchWithMacSandbox; }
+
+ // Override so we can turn on RDD process-specific sandbox logging
+ bool FillMacSandboxInfo(MacSandboxInfo& aInfo) override;
+#endif
+
+ DISALLOW_COPY_AND_ASSIGN(RDDProcessHost);
+
+ Listener* const mListener;
+
+ // All members below are only ever accessed on the main thread.
+ enum class LaunchPhase { Unlaunched, Waiting, Complete };
+ LaunchPhase mLaunchPhase = LaunchPhase::Unlaunched;
+
+ RefPtr<RDDChild> mRDDChild;
+ uint64_t mProcessToken = 0;
+
+ UniquePtr<ipc::SharedPreferenceSerializer> mPrefSerializer;
+
+ bool mShutdownRequested = false;
+ bool mChannelClosed = false;
+
+ TimeStamp mLaunchTime;
+ void RejectPromise();
+ void ResolvePromise();
+
+ // Set to true on construction and to false just prior deletion.
+ // The RDDProcessHost isn't refcounted; so we can capture this by value in
+ // lambdas along with a strong reference to mLiveToken and check if that value
+ // is true before accessing "this".
+ // While a reference to mLiveToken can be taken on any thread; its value can
+ // only be read on the main thread.
+ const RefPtr<media::Refcountable<bool>> mLiveToken;
+ RefPtr<GenericNonExclusivePromise::Private> mLaunchPromise;
+ bool mLaunchPromiseSettled = false;
+ // Will be set to true if we've exceeded the allowed startup time or if the
+ // RDD process as successfully started. This is used to determine if the
+ // timeout runnable needs to execute code or not.
+ bool mTimerChecked = false;
+};
+
+} // namespace mozilla
+
+#endif // _include_dom_media_ipc_RDDProcessHost_h_
diff --git a/dom/media/ipc/RDDProcessImpl.cpp b/dom/media/ipc/RDDProcessImpl.cpp
new file mode 100644
index 0000000000..6ddd4ffe95
--- /dev/null
+++ b/dom/media/ipc/RDDProcessImpl.cpp
@@ -0,0 +1,51 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "RDDProcessImpl.h"
+
+#include "mozilla/ipc/IOThreadChild.h"
+#include "mozilla/GeckoArgs.h"
+
+#if defined(OS_WIN) && defined(MOZ_SANDBOX)
+# include "mozilla/sandboxTarget.h"
+#elif defined(__OpenBSD__) && defined(MOZ_SANDBOX)
+# include "mozilla/SandboxSettings.h"
+# include "prlink.h"
+#endif
+
+namespace mozilla {
+
+using namespace ipc;
+
+RDDProcessImpl::~RDDProcessImpl() = default;
+
+bool RDDProcessImpl::Init(int aArgc, char* aArgv[]) {
+#if defined(MOZ_SANDBOX) && defined(OS_WIN)
+ // Preload AV dlls so we can enable Binary Signature Policy
+ // to restrict further dll loads.
+ LoadLibraryW(L"mozavcodec.dll");
+ LoadLibraryW(L"mozavutil.dll");
+ mozilla::SandboxTarget::Instance()->StartSandbox();
+#elif defined(__OpenBSD__) && defined(MOZ_SANDBOX)
+ PR_LoadLibrary("libmozavcodec.so");
+ PR_LoadLibrary("libmozavutil.so");
+ StartOpenBSDSandbox(GeckoProcessType_RDD);
+#endif
+ Maybe<const char*> parentBuildID =
+ geckoargs::sParentBuildID.Get(aArgc, aArgv);
+ if (parentBuildID.isNothing()) {
+ return false;
+ }
+
+ if (!ProcessChild::InitPrefs(aArgc, aArgv)) {
+ return false;
+ }
+
+ return mRDD->Init(TakeInitialEndpoint(), *parentBuildID);
+}
+
+void RDDProcessImpl::CleanUp() { NS_ShutdownXPCOM(nullptr); }
+
+} // namespace mozilla
diff --git a/dom/media/ipc/RDDProcessImpl.h b/dom/media/ipc/RDDProcessImpl.h
new file mode 100644
index 0000000000..a7d119a2c6
--- /dev/null
+++ b/dom/media/ipc/RDDProcessImpl.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef _include_dom_media_ipc_RDDProcessImpl_h__
+#define _include_dom_media_ipc_RDDProcessImpl_h__
+#include "mozilla/ipc/ProcessChild.h"
+
+#if defined(XP_WIN)
+# include "mozilla/mscom/ProcessRuntime.h"
+#endif
+
+#include "RDDParent.h"
+
+namespace mozilla {
+
+// This class owns the subprocess instance of a PRDD - which in this case,
+// is a RDDParent. It is instantiated as a singleton in XRE_InitChildProcess.
+class RDDProcessImpl final : public ipc::ProcessChild {
+ public:
+ using ipc::ProcessChild::ProcessChild;
+ ~RDDProcessImpl();
+
+ bool Init(int aArgc, char* aArgv[]) override;
+ void CleanUp() override;
+
+ private:
+ RefPtr<RDDParent> mRDD = new RDDParent;
+
+#if defined(XP_WIN)
+ // This object initializes and configures COM.
+ mozilla::mscom::ProcessRuntime mCOMRuntime;
+#endif
+};
+
+} // namespace mozilla
+
+#endif // _include_dom_media_ipc_RDDProcessImpl_h__
diff --git a/dom/media/ipc/RDDProcessManager.cpp b/dom/media/ipc/RDDProcessManager.cpp
new file mode 100644
index 0000000000..e7da3c3569
--- /dev/null
+++ b/dom/media/ipc/RDDProcessManager.cpp
@@ -0,0 +1,413 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "RDDProcessManager.h"
+
+#include "RDDChild.h"
+#include "RDDProcessHost.h"
+#include "mozilla/MemoryReportingProcess.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/RemoteDecoderManagerChild.h"
+#include "mozilla/RemoteDecoderManagerParent.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/SyncRunnable.h" // for LaunchRDDProcess
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/gfx/GPUProcessManager.h"
+#include "mozilla/ipc/Endpoint.h"
+#include "mozilla/ipc/ProcessChild.h"
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/layers/VideoBridgeParent.h"
+#include "nsAppRunner.h"
+#include "nsContentUtils.h"
+
+namespace mozilla {
+
+using namespace gfx;
+using namespace layers;
+
+static StaticAutoPtr<RDDProcessManager> sRDDSingleton;
+
+static bool sRDDProcessShutdown = false;
+
+bool RDDProcessManager::IsShutdown() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return sRDDProcessShutdown || !sRDDSingleton;
+}
+
+RDDProcessManager* RDDProcessManager::Get() { return sRDDSingleton; }
+
+void RDDProcessManager::Initialize() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ sRDDSingleton = new RDDProcessManager();
+}
+
+void RDDProcessManager::Shutdown() { sRDDSingleton = nullptr; }
+
+void RDDProcessManager::RDDProcessShutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ sRDDProcessShutdown = true;
+ if (sRDDSingleton) {
+ sRDDSingleton->DestroyProcess();
+ }
+}
+
+RDDProcessManager::RDDProcessManager()
+ : mObserver(new Observer(this)), mTaskFactory(this) {
+ MOZ_COUNT_CTOR(RDDProcessManager);
+ // Start listening for pref changes so we can
+ // forward them to the process once it is running.
+ nsContentUtils::RegisterShutdownObserver(mObserver);
+ Preferences::AddStrongObserver(mObserver, "");
+}
+
+RDDProcessManager::~RDDProcessManager() {
+ MOZ_COUNT_DTOR(RDDProcessManager);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // The RDD process should have already been shut down.
+ MOZ_ASSERT(!mProcess && !mRDDChild);
+}
+
+NS_IMPL_ISUPPORTS(RDDProcessManager::Observer, nsIObserver);
+
+RDDProcessManager::Observer::Observer(RDDProcessManager* aManager)
+ : mManager(aManager) {}
+
+NS_IMETHODIMP
+RDDProcessManager::Observer::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
+ mManager->OnXPCOMShutdown();
+ } else if (!strcmp(aTopic, "nsPref:changed")) {
+ mManager->OnPreferenceChange(aData);
+ }
+ return NS_OK;
+}
+
+void RDDProcessManager::OnXPCOMShutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsContentUtils::UnregisterShutdownObserver(mObserver);
+ Preferences::RemoveObserver(mObserver, "");
+}
+
+void RDDProcessManager::OnPreferenceChange(const char16_t* aData) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!mProcess) {
+ // Process hasn't been launched yet
+ return;
+ }
+
+ // We know prefs are ASCII here.
+ NS_LossyConvertUTF16toASCII strData(aData);
+
+ mozilla::dom::Pref pref(strData, /* isLocked */ false,
+ /* isSanitized */ false, Nothing(), Nothing());
+
+ Preferences::GetPreference(&pref, GeckoProcessType_RDD,
+ /* remoteType */ ""_ns);
+ if (!!mRDDChild) {
+ MOZ_ASSERT(mQueuedPrefs.IsEmpty());
+ mRDDChild->SendPreferenceUpdate(pref);
+ } else if (IsRDDProcessLaunching()) {
+ mQueuedPrefs.AppendElement(pref);
+ }
+}
+
+RefPtr<GenericNonExclusivePromise> RDDProcessManager::LaunchRDDProcess() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (IsShutdown()) {
+ return GenericNonExclusivePromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE,
+ __func__);
+ }
+
+ if (mNumProcessAttempts && !StaticPrefs::media_rdd_retryonfailure_enabled()) {
+ // We failed to start the RDD process earlier, abort now.
+ return GenericNonExclusivePromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE,
+ __func__);
+ }
+
+ if (mLaunchRDDPromise && mProcess) {
+ return mLaunchRDDPromise;
+ }
+
+ std::vector<std::string> extraArgs;
+ ipc::ProcessChild::AddPlatformBuildID(extraArgs);
+
+ // The subprocess is launched asynchronously, so we
+ // wait for the promise to be resolved to acquire the IPDL actor.
+ mProcess = new RDDProcessHost(this);
+ if (!mProcess->Launch(extraArgs)) {
+ mNumProcessAttempts++;
+ DestroyProcess();
+ return GenericNonExclusivePromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE,
+ __func__);
+ }
+
+ mLaunchRDDPromise = mProcess->LaunchPromise()->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [this](bool) {
+ if (IsShutdown()) {
+ return GenericNonExclusivePromise::CreateAndReject(
+ NS_ERROR_NOT_AVAILABLE, __func__);
+ }
+
+ if (IsRDDProcessDestroyed()) {
+ return GenericNonExclusivePromise::CreateAndReject(
+ NS_ERROR_NOT_AVAILABLE, __func__);
+ }
+
+ mRDDChild = mProcess->GetActor();
+ mProcessToken = mProcess->GetProcessToken();
+
+ // Flush any pref updates that happened during
+ // launch and weren't included in the blobs set
+ // up in LaunchRDDProcess.
+ for (const mozilla::dom::Pref& pref : mQueuedPrefs) {
+ Unused << NS_WARN_IF(!mRDDChild->SendPreferenceUpdate(pref));
+ }
+ mQueuedPrefs.Clear();
+
+ CrashReporter::AnnotateCrashReport(
+ CrashReporter::Annotation::RDDProcessStatus, "Running"_ns);
+
+ if (!CreateVideoBridge()) {
+ mNumProcessAttempts++;
+ DestroyProcess();
+ return GenericNonExclusivePromise::CreateAndReject(
+ NS_ERROR_NOT_AVAILABLE, __func__);
+ }
+ return GenericNonExclusivePromise::CreateAndResolve(true, __func__);
+ },
+ [this](nsresult aError) {
+ if (Get()) {
+ mNumProcessAttempts++;
+ DestroyProcess();
+ }
+ return GenericNonExclusivePromise::CreateAndReject(aError, __func__);
+ });
+ return mLaunchRDDPromise;
+}
+
+auto RDDProcessManager::EnsureRDDProcessAndCreateBridge(
+ base::ProcessId aOtherProcess, dom::ContentParentId aParentId)
+ -> RefPtr<EnsureRDDPromise> {
+ return InvokeAsync(
+ GetMainThreadSerialEventTarget(), __func__,
+ [aOtherProcess, aParentId, this]() -> RefPtr<EnsureRDDPromise> {
+ return LaunchRDDProcess()->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [aOtherProcess, aParentId, this]() {
+ if (IsShutdown()) {
+ return EnsureRDDPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE,
+ __func__);
+ }
+ ipc::Endpoint<PRemoteDecoderManagerChild> endpoint;
+ if (!CreateContentBridge(aOtherProcess, aParentId, &endpoint)) {
+ return EnsureRDDPromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE,
+ __func__);
+ }
+ mNumProcessAttempts = 0;
+ return EnsureRDDPromise::CreateAndResolve(std::move(endpoint),
+ __func__);
+ },
+ [](nsresult aError) {
+ return EnsureRDDPromise::CreateAndReject(aError, __func__);
+ });
+ });
+}
+
+bool RDDProcessManager::IsRDDProcessLaunching() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return !!mProcess && !mRDDChild;
+}
+
+bool RDDProcessManager::IsRDDProcessDestroyed() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return !mRDDChild && !mProcess;
+}
+
+void RDDProcessManager::OnProcessUnexpectedShutdown(RDDProcessHost* aHost) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mProcess && mProcess == aHost);
+
+ mNumUnexpectedCrashes++;
+
+ DestroyProcess();
+}
+
+void RDDProcessManager::NotifyRemoteActorDestroyed(
+ const uint64_t& aProcessToken) {
+ if (!NS_IsMainThread()) {
+ RefPtr<Runnable> task = mTaskFactory.NewRunnableMethod(
+ &RDDProcessManager::NotifyRemoteActorDestroyed, aProcessToken);
+ NS_DispatchToMainThread(task.forget());
+ return;
+ }
+
+ if (mProcessToken != aProcessToken) {
+ // This token is for an older process; we can safely ignore it.
+ return;
+ }
+
+ // One of the bridged top-level actors for the RDD process has been
+ // prematurely terminated, and we're receiving a notification. This
+ // can happen if the ActorDestroy for a bridged protocol fires
+ // before the ActorDestroy for PRDDChild.
+ OnProcessUnexpectedShutdown(mProcess);
+}
+
+void RDDProcessManager::DestroyProcess() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!mProcess) {
+ return;
+ }
+
+ mProcess->Shutdown();
+ mProcessToken = 0;
+ mProcess = nullptr;
+ mRDDChild = nullptr;
+ mQueuedPrefs.Clear();
+
+ CrashReporter::AnnotateCrashReport(
+ CrashReporter::Annotation::RDDProcessStatus, "Destroyed"_ns);
+}
+
+bool RDDProcessManager::CreateContentBridge(
+ base::ProcessId aOtherProcess, dom::ContentParentId aParentId,
+ ipc::Endpoint<PRemoteDecoderManagerChild>* aOutRemoteDecoderManager) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (IsRDDProcessDestroyed()) {
+ MOZ_LOG(sPDMLog, LogLevel::Debug,
+ ("RDD shutdown before creating content bridge"));
+ return false;
+ }
+
+ ipc::Endpoint<PRemoteDecoderManagerParent> parentPipe;
+ ipc::Endpoint<PRemoteDecoderManagerChild> childPipe;
+
+ nsresult rv = PRemoteDecoderManager::CreateEndpoints(
+ mRDDChild->OtherPid(), aOtherProcess, &parentPipe, &childPipe);
+ if (NS_FAILED(rv)) {
+ MOZ_LOG(sPDMLog, LogLevel::Debug,
+ ("Could not create content remote decoder: %d", int(rv)));
+ return false;
+ }
+
+ mRDDChild->SendNewContentRemoteDecoderManager(std::move(parentPipe),
+ aParentId);
+
+ *aOutRemoteDecoderManager = std::move(childPipe);
+ return true;
+}
+
+bool RDDProcessManager::CreateVideoBridge() {
+ MOZ_ASSERT(NS_IsMainThread());
+ ipc::Endpoint<PVideoBridgeParent> parentPipe;
+ ipc::Endpoint<PVideoBridgeChild> childPipe;
+
+ GPUProcessManager* gpuManager = GPUProcessManager::Get();
+ base::ProcessId gpuProcessPid =
+ gpuManager ? gpuManager->GPUProcessPid() : base::kInvalidProcessId;
+
+ // Build content device data first; this ensure that the GPU process is fully
+ // ready.
+ ContentDeviceData contentDeviceData;
+ gfxPlatform::GetPlatform()->BuildContentDeviceData(&contentDeviceData);
+
+ // The child end is the producer of video frames; the parent end is the
+ // consumer.
+ base::ProcessId childPid = RDDProcessPid();
+ base::ProcessId parentPid = gpuProcessPid != base::kInvalidProcessId
+ ? gpuProcessPid
+ : base::GetCurrentProcId();
+
+ nsresult rv = PVideoBridge::CreateEndpoints(parentPid, childPid, &parentPipe,
+ &childPipe);
+ if (NS_FAILED(rv)) {
+ MOZ_LOG(sPDMLog, LogLevel::Debug,
+ ("Could not create video bridge: %d", int(rv)));
+ return false;
+ }
+
+ mRDDChild->SendInitVideoBridge(std::move(childPipe),
+ mNumUnexpectedCrashes == 0, contentDeviceData);
+ if (gpuProcessPid != base::kInvalidProcessId) {
+ gpuManager->InitVideoBridge(std::move(parentPipe),
+ VideoBridgeSource::RddProcess);
+ } else {
+ VideoBridgeParent::Open(std::move(parentPipe),
+ VideoBridgeSource::RddProcess);
+ }
+
+ return true;
+}
+
+base::ProcessId RDDProcessManager::RDDProcessPid() {
+ MOZ_ASSERT(NS_IsMainThread());
+ base::ProcessId rddPid =
+ mRDDChild ? mRDDChild->OtherPid() : base::kInvalidProcessId;
+ return rddPid;
+}
+
+class RDDMemoryReporter : public MemoryReportingProcess {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RDDMemoryReporter, override)
+
+ bool IsAlive() const override { return !!GetChild(); }
+
+ bool SendRequestMemoryReport(
+ const uint32_t& aGeneration, const bool& aAnonymize,
+ const bool& aMinimizeMemoryUsage,
+ const Maybe<ipc::FileDescriptor>& aDMDFile) override {
+ RDDChild* child = GetChild();
+ if (!child) {
+ return false;
+ }
+
+ return child->SendRequestMemoryReport(aGeneration, aAnonymize,
+ aMinimizeMemoryUsage, aDMDFile);
+ }
+
+ int32_t Pid() const override {
+ if (RDDChild* child = GetChild()) {
+ return (int32_t)child->OtherPid();
+ }
+ return 0;
+ }
+
+ private:
+ RDDChild* GetChild() const {
+ if (RDDProcessManager* rddpm = RDDProcessManager::Get()) {
+ if (RDDChild* child = rddpm->GetRDDChild()) {
+ return child;
+ }
+ }
+ return nullptr;
+ }
+
+ protected:
+ ~RDDMemoryReporter() = default;
+};
+
+RefPtr<MemoryReportingProcess> RDDProcessManager::GetProcessMemoryReporter() {
+ if (!mProcess || !mProcess->IsConnected()) {
+ return nullptr;
+ }
+ return new RDDMemoryReporter();
+}
+
+RefPtr<PRDDChild::TestTriggerMetricsPromise>
+RDDProcessManager::TestTriggerMetrics() {
+ if (!NS_WARN_IF(!mRDDChild)) {
+ return mRDDChild->SendTestTriggerMetrics();
+ }
+
+ return PRDDChild::TestTriggerMetricsPromise::CreateAndReject(
+ ipc::ResponseRejectReason::SendError, __func__);
+}
+
+} // namespace mozilla
diff --git a/dom/media/ipc/RDDProcessManager.h b/dom/media/ipc/RDDProcessManager.h
new file mode 100644
index 0000000000..730ab4fa1f
--- /dev/null
+++ b/dom/media/ipc/RDDProcessManager.h
@@ -0,0 +1,128 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef _include_dom_media_ipc_RDDProcessManager_h_
+#define _include_dom_media_ipc_RDDProcessManager_h_
+#include "mozilla/MozPromise.h"
+#include "mozilla/PRemoteDecoderManagerChild.h"
+#include "mozilla/RDDProcessHost.h"
+#include "mozilla/dom/ipc/IdType.h"
+#include "mozilla/ipc/TaskFactory.h"
+#include "mozilla/PRDDChild.h"
+#include "nsIObserver.h"
+
+namespace mozilla {
+
+class MemoryReportingProcess;
+class RDDChild;
+
+// The RDDProcessManager is a singleton responsible for creating RDD-bound
+// objects that may live in another process. Currently, it provides access
+// to the RDD process via ContentParent.
+class RDDProcessManager final : public RDDProcessHost::Listener {
+ friend class RDDChild;
+
+ public:
+ static void Initialize();
+ static void RDDProcessShutdown();
+ static void Shutdown();
+ static RDDProcessManager* Get();
+
+ ~RDDProcessManager();
+
+ using EnsureRDDPromise =
+ MozPromise<ipc::Endpoint<PRemoteDecoderManagerChild>, nsresult, true>;
+ // Launch a new RDD process asynchronously
+ RefPtr<GenericNonExclusivePromise> LaunchRDDProcess();
+ // If not using a RDD process, launch a new RDD process asynchronously and
+ // create a RemoteDecoderManager bridge
+ RefPtr<EnsureRDDPromise> EnsureRDDProcessAndCreateBridge(
+ base::ProcessId aOtherProcess, dom::ContentParentId aParentId);
+
+ void OnProcessUnexpectedShutdown(RDDProcessHost* aHost) override;
+
+ // Notify the RDDProcessManager that a top-level PRDD protocol has been
+ // terminated. This may be called from any thread.
+ void NotifyRemoteActorDestroyed(const uint64_t& aProcessToken);
+
+ // Returns -1 if there is no RDD process, or the platform pid for it.
+ base::ProcessId RDDProcessPid();
+
+ // If a RDD process is present, create a MemoryReportingProcess object.
+ // Otherwise, return null.
+ RefPtr<MemoryReportingProcess> GetProcessMemoryReporter();
+
+ // Returns access to the PRDD protocol if a RDD process is present.
+ RDDChild* GetRDDChild() { return mRDDChild; }
+
+ // Returns whether or not a RDD process was ever launched.
+ bool AttemptedRDDProcess() const { return mNumProcessAttempts > 0; }
+
+ // Returns the RDD Process
+ RDDProcessHost* Process() { return mProcess; }
+
+ /*
+ * ** Test-only Method **
+ *
+ * Trigger RDD-process test metric instrumentation.
+ */
+ RefPtr<PRDDChild::TestTriggerMetricsPromise> TestTriggerMetrics();
+
+ private:
+ bool IsRDDProcessLaunching();
+ bool IsRDDProcessDestroyed() const;
+ bool CreateVideoBridge();
+
+ // Called from our xpcom-shutdown observer.
+ void OnXPCOMShutdown();
+ void OnPreferenceChange(const char16_t* aData);
+
+ RDDProcessManager();
+
+ // Shutdown the RDD process.
+ void DestroyProcess();
+
+ bool IsShutdown() const;
+
+ DISALLOW_COPY_AND_ASSIGN(RDDProcessManager);
+
+ class Observer final : public nsIObserver {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+ explicit Observer(RDDProcessManager* aManager);
+
+ protected:
+ ~Observer() = default;
+
+ RDDProcessManager* mManager;
+ };
+ friend class Observer;
+
+ bool CreateContentBridge(
+ base::ProcessId aOtherProcess, dom::ContentParentId aParentId,
+ ipc::Endpoint<PRemoteDecoderManagerChild>* aOutRemoteDecoderManager);
+
+ const RefPtr<Observer> mObserver;
+ ipc::TaskFactory<RDDProcessManager> mTaskFactory;
+ uint32_t mNumProcessAttempts = 0;
+ uint32_t mNumUnexpectedCrashes = 0;
+
+ // Fields that are associated with the current RDD process.
+ RDDProcessHost* mProcess = nullptr;
+ uint64_t mProcessToken = 0;
+ RDDChild* mRDDChild = nullptr;
+ // Collects any pref changes that occur during process launch (after
+ // the initial map is passed in command-line arguments) to be sent
+ // when the process can receive IPC messages.
+ nsTArray<dom::Pref> mQueuedPrefs;
+ // Promise will be resolved when the RDD process has been fully started and
+ // VideoBridge configured. Only accessed on the main thread.
+ RefPtr<GenericNonExclusivePromise> mLaunchRDDPromise;
+};
+
+} // namespace mozilla
+
+#endif // _include_dom_media_ipc_RDDProcessManager_h_
diff --git a/dom/media/ipc/RemoteAudioDecoder.cpp b/dom/media/ipc/RemoteAudioDecoder.cpp
new file mode 100644
index 0000000000..09b420261f
--- /dev/null
+++ b/dom/media/ipc/RemoteAudioDecoder.cpp
@@ -0,0 +1,121 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "RemoteAudioDecoder.h"
+
+#include "MediaDataDecoderProxy.h"
+#include "PDMFactory.h"
+#include "RemoteDecoderManagerChild.h"
+#include "RemoteDecoderManagerParent.h"
+#include "mozilla/PodOperations.h"
+#include "mozilla/StaticPrefs_media.h"
+
+namespace mozilla {
+
+RemoteAudioDecoderChild::RemoteAudioDecoderChild(RemoteDecodeIn aLocation)
+ : RemoteDecoderChild(aLocation) {}
+
+MediaResult RemoteAudioDecoderChild::ProcessOutput(
+ DecodedOutputIPDL&& aDecodedData) {
+ AssertOnManagerThread();
+
+ MOZ_ASSERT(aDecodedData.type() == DecodedOutputIPDL::TArrayOfRemoteAudioData);
+ RefPtr<ArrayOfRemoteAudioData> arrayData =
+ aDecodedData.get_ArrayOfRemoteAudioData();
+
+ for (size_t i = 0; i < arrayData->Count(); i++) {
+ RefPtr<AudioData> data = arrayData->ElementAt(i);
+ if (!data) {
+ // OOM
+ return MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__);
+ }
+ mDecodedData.AppendElement(data);
+ }
+ return NS_OK;
+}
+
+MediaResult RemoteAudioDecoderChild::InitIPDL(
+ const AudioInfo& aAudioInfo, const CreateDecoderParams::OptionSet& aOptions,
+ const Maybe<uint64_t>& aMediaEngineId) {
+ RefPtr<RemoteDecoderManagerChild> manager =
+ RemoteDecoderManagerChild::GetSingleton(mLocation);
+
+ // The manager isn't available because RemoteDecoderManagerChild has been
+ // initialized with null end points and we don't want to decode video on RDD
+ // process anymore. Return false here so that we can fallback to other PDMs.
+ if (!manager) {
+ return MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("RemoteDecoderManager is not available."));
+ }
+
+ if (!manager->CanSend()) {
+ return MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("RemoteDecoderManager unable to send."));
+ }
+
+ mIPDLSelfRef = this;
+ Unused << manager->SendPRemoteDecoderConstructor(
+ this, aAudioInfo, aOptions, Nothing(), aMediaEngineId, Nothing());
+ return NS_OK;
+}
+
+RemoteAudioDecoderParent::RemoteAudioDecoderParent(
+ RemoteDecoderManagerParent* aParent, const AudioInfo& aAudioInfo,
+ const CreateDecoderParams::OptionSet& aOptions,
+ nsISerialEventTarget* aManagerThread, TaskQueue* aDecodeTaskQueue,
+ Maybe<uint64_t> aMediaEngineId)
+ : RemoteDecoderParent(aParent, aOptions, aManagerThread, aDecodeTaskQueue,
+ aMediaEngineId, Nothing()),
+ mAudioInfo(aAudioInfo) {}
+
+IPCResult RemoteAudioDecoderParent::RecvConstruct(
+ ConstructResolver&& aResolver) {
+ auto params = CreateDecoderParams{mAudioInfo, mOptions,
+ CreateDecoderParams::NoWrapper(true),
+ mMediaEngineId, mTrackingId};
+
+ mParent->EnsurePDMFactory().CreateDecoder(params)->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [resolver = std::move(aResolver), self = RefPtr{this}](
+ PlatformDecoderModule::CreateDecoderPromise::ResolveOrRejectValue&&
+ aValue) {
+ if (aValue.IsReject()) {
+ resolver(aValue.RejectValue());
+ return;
+ }
+ MOZ_ASSERT(aValue.ResolveValue());
+ self->mDecoder =
+ new MediaDataDecoderProxy(aValue.ResolveValue().forget(),
+ do_AddRef(self->mDecodeTaskQueue.get()));
+ resolver(NS_OK);
+ });
+
+ return IPC_OK();
+}
+
+MediaResult RemoteAudioDecoderParent::ProcessDecodedData(
+ MediaDataDecoder::DecodedData&& aData, DecodedOutputIPDL& aDecodedData) {
+ MOZ_ASSERT(OnManagerThread());
+
+ // Converted array to array of RefPtr<AudioData>
+ nsTArray<RefPtr<AudioData>> data(aData.Length());
+ for (auto&& element : aData) {
+ MOZ_ASSERT(element->mType == MediaData::Type::AUDIO_DATA,
+ "Can only decode audio using RemoteAudioDecoderParent!");
+ AudioData* audio = static_cast<AudioData*>(element.get());
+ data.AppendElement(audio);
+ }
+ auto array = MakeRefPtr<ArrayOfRemoteAudioData>();
+ if (!array->Fill(std::move(data),
+ [&](size_t aSize) { return AllocateBuffer(aSize); })) {
+ return MediaResult(
+ NS_ERROR_OUT_OF_MEMORY,
+ "Failed in RemoteAudioDecoderParent::ProcessDecodedData");
+ }
+ aDecodedData = std::move(array);
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/dom/media/ipc/RemoteAudioDecoder.h b/dom/media/ipc/RemoteAudioDecoder.h
new file mode 100644
index 0000000000..55c4e001d1
--- /dev/null
+++ b/dom/media/ipc/RemoteAudioDecoder.h
@@ -0,0 +1,53 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef include_dom_media_ipc_RemoteAudioDecoderChild_h
+#define include_dom_media_ipc_RemoteAudioDecoderChild_h
+#include "RemoteDecoderChild.h"
+#include "RemoteDecoderParent.h"
+
+namespace mozilla {
+
+using mozilla::ipc::IPCResult;
+
+class RemoteAudioDecoderChild final : public RemoteDecoderChild {
+ public:
+ explicit RemoteAudioDecoderChild(RemoteDecodeIn aLocation);
+
+ MOZ_IS_CLASS_INIT
+ MediaResult InitIPDL(const AudioInfo& aAudioInfo,
+ const CreateDecoderParams::OptionSet& aOptions,
+ const Maybe<uint64_t>& aMediaEngineId);
+
+ MediaResult ProcessOutput(DecodedOutputIPDL&& aDecodedData) override;
+};
+
+class RemoteAudioDecoderParent final : public RemoteDecoderParent {
+ public:
+ RemoteAudioDecoderParent(RemoteDecoderManagerParent* aParent,
+ const AudioInfo& aAudioInfo,
+ const CreateDecoderParams::OptionSet& aOptions,
+ nsISerialEventTarget* aManagerThread,
+ TaskQueue* aDecodeTaskQueue,
+ Maybe<uint64_t> aMediaEngineId);
+
+ protected:
+ IPCResult RecvConstruct(ConstructResolver&& aResolver) override;
+ MediaResult ProcessDecodedData(MediaDataDecoder::DecodedData&& aData,
+ DecodedOutputIPDL& aDecodedData) override;
+
+ private:
+ // Can only be accessed from the manager thread
+ // Note: we can't keep a reference to the original AudioInfo here
+ // because unlike in typical MediaDataDecoder situations, we're being
+ // passed a deserialized AudioInfo from RecvPRemoteDecoderConstructor
+ // which is destroyed when RecvPRemoteDecoderConstructor returns.
+ const AudioInfo mAudioInfo;
+ const CreateDecoderParams::OptionSet mOptions;
+};
+
+} // namespace mozilla
+
+#endif // include_dom_media_ipc_RemoteAudioDecoderChild_h
diff --git a/dom/media/ipc/RemoteDecodeUtils.cpp b/dom/media/ipc/RemoteDecodeUtils.cpp
new file mode 100644
index 0000000000..15036a0370
--- /dev/null
+++ b/dom/media/ipc/RemoteDecodeUtils.cpp
@@ -0,0 +1,103 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "RemoteDecodeUtils.h"
+#include "mozilla/ipc/UtilityProcessChild.h"
+
+namespace mozilla {
+
+using SandboxingKind = ipc::SandboxingKind;
+
+SandboxingKind GetCurrentSandboxingKind() {
+ MOZ_ASSERT(XRE_IsUtilityProcess());
+ return ipc::UtilityProcessChild::GetSingleton()->mSandbox;
+}
+
+SandboxingKind GetSandboxingKindFromLocation(RemoteDecodeIn aLocation) {
+ switch (aLocation) {
+ case RemoteDecodeIn::UtilityProcess_Generic:
+ return SandboxingKind::GENERIC_UTILITY;
+#ifdef MOZ_APPLEMEDIA
+ case RemoteDecodeIn::UtilityProcess_AppleMedia:
+ return SandboxingKind::UTILITY_AUDIO_DECODING_APPLE_MEDIA;
+ break;
+#endif
+#ifdef XP_WIN
+ case RemoteDecodeIn::UtilityProcess_WMF:
+ return SandboxingKind::UTILITY_AUDIO_DECODING_WMF;
+#endif
+#ifdef MOZ_WMF_MEDIA_ENGINE
+ case RemoteDecodeIn::UtilityProcess_MFMediaEngineCDM:
+ return SandboxingKind::MF_MEDIA_ENGINE_CDM;
+#endif
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unsupported RemoteDecodeIn");
+ return SandboxingKind::COUNT;
+ }
+}
+
+RemoteDecodeIn GetRemoteDecodeInFromKind(SandboxingKind aKind) {
+ switch (aKind) {
+ case SandboxingKind::GENERIC_UTILITY:
+ return RemoteDecodeIn::UtilityProcess_Generic;
+#ifdef MOZ_APPLEMEDIA
+ case SandboxingKind::UTILITY_AUDIO_DECODING_APPLE_MEDIA:
+ return RemoteDecodeIn::UtilityProcess_AppleMedia;
+#endif
+#ifdef XP_WIN
+ case SandboxingKind::UTILITY_AUDIO_DECODING_WMF:
+ return RemoteDecodeIn::UtilityProcess_WMF;
+#endif
+#ifdef MOZ_WMF_MEDIA_ENGINE
+ case SandboxingKind::MF_MEDIA_ENGINE_CDM:
+ return RemoteDecodeIn::UtilityProcess_MFMediaEngineCDM;
+#endif
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unsupported SandboxingKind");
+ return RemoteDecodeIn::Unspecified;
+ }
+}
+
+RemoteDecodeIn GetRemoteDecodeInFromVideoBridgeSource(
+ layers::VideoBridgeSource aSource) {
+ switch (aSource) {
+ case layers::VideoBridgeSource::RddProcess:
+ return RemoteDecodeIn::RddProcess;
+ case layers::VideoBridgeSource::GpuProcess:
+ return RemoteDecodeIn::GpuProcess;
+ case layers::VideoBridgeSource::MFMediaEngineCDMProcess:
+ return RemoteDecodeIn::UtilityProcess_MFMediaEngineCDM;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unsupported VideoBridgeSource");
+ return RemoteDecodeIn::Unspecified;
+ }
+}
+
+const char* RemoteDecodeInToStr(RemoteDecodeIn aLocation) {
+ switch (aLocation) {
+ case RemoteDecodeIn::RddProcess:
+ return "RDD";
+ case RemoteDecodeIn::GpuProcess:
+ return "GPU";
+ case RemoteDecodeIn::UtilityProcess_Generic:
+ return "Utility Generic";
+#ifdef MOZ_APPLEMEDIA
+ case RemoteDecodeIn::UtilityProcess_AppleMedia:
+ return "Utility AppleMedia";
+#endif
+#ifdef XP_WIN
+ case RemoteDecodeIn::UtilityProcess_WMF:
+ return "Utility WMF";
+#endif
+#ifdef MOZ_WMF_MEDIA_ENGINE
+ case RemoteDecodeIn::UtilityProcess_MFMediaEngineCDM:
+ return "Utility MF Media Engine CDM";
+#endif
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unsupported RemoteDecodeIn");
+ return "Unknown";
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/ipc/RemoteDecodeUtils.h b/dom/media/ipc/RemoteDecodeUtils.h
new file mode 100644
index 0000000000..212250c089
--- /dev/null
+++ b/dom/media/ipc/RemoteDecodeUtils.h
@@ -0,0 +1,31 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_IPC_REMOTEDECODEUTILS_H_
+#define DOM_MEDIA_IPC_REMOTEDECODEUTILS_H_
+
+#include "mozilla/Logging.h"
+#include "mozilla/RemoteDecoderManagerChild.h"
+#include "mozilla/ipc/UtilityProcessSandboxing.h"
+
+namespace mozilla {
+
+inline LazyLogModule gRemoteDecodeLog{"RemoteDecode"};
+
+// Return the sandboxing kind of the current utility process. Should only be
+// called on the utility process.
+ipc::SandboxingKind GetCurrentSandboxingKind();
+
+ipc::SandboxingKind GetSandboxingKindFromLocation(RemoteDecodeIn aLocation);
+
+RemoteDecodeIn GetRemoteDecodeInFromKind(ipc::SandboxingKind aKind);
+
+RemoteDecodeIn GetRemoteDecodeInFromVideoBridgeSource(
+ layers::VideoBridgeSource aSource);
+
+const char* RemoteDecodeInToStr(RemoteDecodeIn aLocation);
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_IPC_REMOTEDECODEUTILS_H_
diff --git a/dom/media/ipc/RemoteDecoderChild.cpp b/dom/media/ipc/RemoteDecoderChild.cpp
new file mode 100644
index 0000000000..ff36f0ff1f
--- /dev/null
+++ b/dom/media/ipc/RemoteDecoderChild.cpp
@@ -0,0 +1,315 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "RemoteDecoderChild.h"
+
+#include "RemoteDecoderManagerChild.h"
+
+#include "mozilla/RemoteDecodeUtils.h"
+
+namespace mozilla {
+
+RemoteDecoderChild::RemoteDecoderChild(RemoteDecodeIn aLocation)
+ : ShmemRecycleAllocator(this),
+ mLocation(aLocation),
+ mThread(GetCurrentSerialEventTarget()) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ RemoteDecoderManagerChild::GetManagerThread() &&
+ RemoteDecoderManagerChild::GetManagerThread()->IsOnCurrentThread(),
+ "Must be created on the manager thread");
+}
+
+RemoteDecoderChild::~RemoteDecoderChild() = default;
+
+void RemoteDecoderChild::HandleRejectionError(
+ const ipc::ResponseRejectReason& aReason,
+ std::function<void(const MediaResult&)>&& aCallback) {
+ // If the channel goes down and CanSend() returns false, the IPDL promise will
+ // be rejected with SendError rather than ActorDestroyed. Both means the same
+ // thing and we can consider that the parent has crashed. The child can no
+ // longer be used.
+ //
+
+ // The GPU/RDD process crashed.
+ if (mLocation == RemoteDecodeIn::GpuProcess) {
+ // The GPU process will get automatically restarted by the parent process.
+ // Once it has been restarted the ContentChild will receive the message and
+ // will call GetManager()->InitForGPUProcess.
+ // We defer reporting an error until we've recreated the RemoteDecoder
+ // manager so that it'll be safe for MediaFormatReader to recreate decoders
+ RefPtr<RemoteDecoderChild> self = this;
+ GetManager()->RunWhenGPUProcessRecreated(NS_NewRunnableFunction(
+ "RemoteDecoderChild::HandleRejectionError",
+ [self, callback = std::move(aCallback)]() {
+ MediaResult error(
+ NS_ERROR_DOM_MEDIA_REMOTE_DECODER_CRASHED_RDD_OR_GPU_ERR,
+ __func__);
+ callback(error);
+ }));
+ return;
+ }
+
+ nsresult err = NS_ERROR_DOM_MEDIA_REMOTE_DECODER_CRASHED_UTILITY_ERR;
+ if (mLocation == RemoteDecodeIn::GpuProcess ||
+ mLocation == RemoteDecodeIn::RddProcess) {
+ err = NS_ERROR_DOM_MEDIA_REMOTE_DECODER_CRASHED_RDD_OR_GPU_ERR;
+ } else if (mLocation == RemoteDecodeIn::UtilityProcess_MFMediaEngineCDM) {
+ err = NS_ERROR_DOM_MEDIA_REMOTE_DECODER_CRASHED_MF_CDM_ERR;
+ }
+ // The RDD process is restarted on demand and asynchronously, we can
+ // immediately inform the caller that a new decoder is needed. The RDD will
+ // then be restarted during the new decoder creation by
+ aCallback(MediaResult(err, __func__));
+}
+
+// ActorDestroy is called if the channel goes down while waiting for a response.
+void RemoteDecoderChild::ActorDestroy(ActorDestroyReason aWhy) {
+ mRemoteDecoderCrashed = (aWhy == AbnormalShutdown);
+ mDecodedData.Clear();
+ CleanupShmemRecycleAllocator();
+ RecordShutdownTelemetry(mRemoteDecoderCrashed);
+}
+
+void RemoteDecoderChild::DestroyIPDL() {
+ AssertOnManagerThread();
+ MOZ_DIAGNOSTIC_ASSERT(mInitPromise.IsEmpty() && mDecodePromise.IsEmpty() &&
+ mDrainPromise.IsEmpty() &&
+ mFlushPromise.IsEmpty() &&
+ mShutdownPromise.IsEmpty(),
+ "All promises should have been rejected");
+ if (CanSend()) {
+ PRemoteDecoderChild::Send__delete__(this);
+ }
+}
+
+void RemoteDecoderChild::IPDLActorDestroyed() { mIPDLSelfRef = nullptr; }
+
+// MediaDataDecoder methods
+
+RefPtr<MediaDataDecoder::InitPromise> RemoteDecoderChild::Init() {
+ AssertOnManagerThread();
+
+ mRemoteDecoderCrashed = false;
+
+ RefPtr<RemoteDecoderChild> self = this;
+ SendInit()
+ ->Then(
+ mThread, __func__,
+ [self, this](InitResultIPDL&& aResponse) {
+ mInitPromiseRequest.Complete();
+ if (aResponse.type() == InitResultIPDL::TMediaResult) {
+ mInitPromise.Reject(aResponse.get_MediaResult(), __func__);
+ return;
+ }
+ const auto& initResponse = aResponse.get_InitCompletionIPDL();
+ mDescription = initResponse.decoderDescription();
+ mDescription.Append(" (");
+ mDescription.Append(RemoteDecodeInToStr(GetManager()->Location()));
+ mDescription.Append(" remote)");
+
+ mProcessName = initResponse.decoderProcessName();
+ mCodecName = initResponse.decoderCodecName();
+
+ mIsHardwareAccelerated = initResponse.hardware();
+ mHardwareAcceleratedReason = initResponse.hardwareReason();
+ mConversion = initResponse.conversion();
+ // Either the promise has not yet been resolved or the handler has
+ // been disconnected and we can't get here.
+ mInitPromise.Resolve(initResponse.type(), __func__);
+ },
+ [self](const mozilla::ipc::ResponseRejectReason& aReason) {
+ self->mInitPromiseRequest.Complete();
+ self->HandleRejectionError(
+ aReason, [self](const MediaResult& aError) {
+ self->mInitPromise.RejectIfExists(aError, __func__);
+ });
+ })
+ ->Track(mInitPromiseRequest);
+
+ return mInitPromise.Ensure(__func__);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> RemoteDecoderChild::Decode(
+ const nsTArray<RefPtr<MediaRawData>>& aSamples) {
+ AssertOnManagerThread();
+
+ if (mRemoteDecoderCrashed) {
+ nsresult err = NS_ERROR_DOM_MEDIA_REMOTE_DECODER_CRASHED_UTILITY_ERR;
+ if (mLocation == RemoteDecodeIn::GpuProcess ||
+ mLocation == RemoteDecodeIn::RddProcess) {
+ err = NS_ERROR_DOM_MEDIA_REMOTE_DECODER_CRASHED_RDD_OR_GPU_ERR;
+ } else if (mLocation == RemoteDecodeIn::UtilityProcess_MFMediaEngineCDM) {
+ err = NS_ERROR_DOM_MEDIA_REMOTE_DECODER_CRASHED_MF_CDM_ERR;
+ }
+ return MediaDataDecoder::DecodePromise::CreateAndReject(err, __func__);
+ }
+
+ auto samples = MakeRefPtr<ArrayOfRemoteMediaRawData>();
+ if (!samples->Fill(aSamples,
+ [&](size_t aSize) { return AllocateBuffer(aSize); })) {
+ return MediaDataDecoder::DecodePromise::CreateAndReject(
+ NS_ERROR_OUT_OF_MEMORY, __func__);
+ }
+ SendDecode(samples)->Then(
+ mThread, __func__,
+ [self = RefPtr{this}, this](
+ PRemoteDecoderChild::DecodePromise::ResolveOrRejectValue&& aValue) {
+ // We no longer need the samples as the data has been
+ // processed by the parent.
+ // If the parent died, the error being fatal will cause the
+ // decoder to be torn down and all shmem in the pool will be
+ // deallocated.
+ ReleaseAllBuffers();
+
+ if (aValue.IsReject()) {
+ HandleRejectionError(
+ aValue.RejectValue(), [self](const MediaResult& aError) {
+ self->mDecodePromise.RejectIfExists(aError, __func__);
+ });
+ return;
+ }
+ MOZ_DIAGNOSTIC_ASSERT(CanSend(),
+ "The parent unexpectedly died, promise should "
+ "have been rejected first");
+ if (mDecodePromise.IsEmpty()) {
+ // We got flushed.
+ return;
+ }
+ auto response = std::move(aValue.ResolveValue());
+ if (response.type() == DecodeResultIPDL::TMediaResult &&
+ NS_FAILED(response.get_MediaResult())) {
+ mDecodePromise.Reject(response.get_MediaResult(), __func__);
+ return;
+ }
+ if (response.type() == DecodeResultIPDL::TDecodedOutputIPDL) {
+ ProcessOutput(std::move(response.get_DecodedOutputIPDL()));
+ }
+ mDecodePromise.Resolve(std::move(mDecodedData), __func__);
+ mDecodedData = MediaDataDecoder::DecodedData();
+ });
+
+ return mDecodePromise.Ensure(__func__);
+}
+
+RefPtr<MediaDataDecoder::FlushPromise> RemoteDecoderChild::Flush() {
+ AssertOnManagerThread();
+ mDecodePromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ mDrainPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+
+ RefPtr<RemoteDecoderChild> self = this;
+ SendFlush()->Then(
+ mThread, __func__,
+ [self](const MediaResult& aResult) {
+ if (NS_SUCCEEDED(aResult)) {
+ self->mFlushPromise.ResolveIfExists(true, __func__);
+ } else {
+ self->mFlushPromise.RejectIfExists(aResult, __func__);
+ }
+ },
+ [self](const mozilla::ipc::ResponseRejectReason& aReason) {
+ self->HandleRejectionError(aReason, [self](const MediaResult& aError) {
+ self->mFlushPromise.RejectIfExists(aError, __func__);
+ });
+ });
+ return mFlushPromise.Ensure(__func__);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> RemoteDecoderChild::Drain() {
+ AssertOnManagerThread();
+
+ RefPtr<RemoteDecoderChild> self = this;
+ SendDrain()->Then(
+ mThread, __func__,
+ [self, this](DecodeResultIPDL&& aResponse) {
+ if (mDrainPromise.IsEmpty()) {
+ // We got flushed.
+ return;
+ }
+ if (aResponse.type() == DecodeResultIPDL::TMediaResult &&
+ NS_FAILED(aResponse.get_MediaResult())) {
+ mDrainPromise.Reject(aResponse.get_MediaResult(), __func__);
+ return;
+ }
+ MOZ_DIAGNOSTIC_ASSERT(CanSend(),
+ "The parent unexpectedly died, promise should "
+ "have been rejected first");
+ if (aResponse.type() == DecodeResultIPDL::TDecodedOutputIPDL) {
+ ProcessOutput(std::move(aResponse.get_DecodedOutputIPDL()));
+ }
+ mDrainPromise.Resolve(std::move(mDecodedData), __func__);
+ mDecodedData = MediaDataDecoder::DecodedData();
+ },
+ [self](const mozilla::ipc::ResponseRejectReason& aReason) {
+ self->HandleRejectionError(aReason, [self](const MediaResult& aError) {
+ self->mDrainPromise.RejectIfExists(aError, __func__);
+ });
+ });
+ return mDrainPromise.Ensure(__func__);
+}
+
+RefPtr<mozilla::ShutdownPromise> RemoteDecoderChild::Shutdown() {
+ AssertOnManagerThread();
+ // Shutdown() can be called while an InitPromise is pending.
+ mInitPromiseRequest.DisconnectIfExists();
+ mInitPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ mDecodePromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ mDrainPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ mFlushPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+
+ RefPtr<RemoteDecoderChild> self = this;
+ SendShutdown()->Then(
+ mThread, __func__,
+ [self](
+ PRemoteDecoderChild::ShutdownPromise::ResolveOrRejectValue&& aValue) {
+ self->mShutdownPromise.Resolve(aValue.IsResolve(), __func__);
+ });
+ return mShutdownPromise.Ensure(__func__);
+}
+
+bool RemoteDecoderChild::IsHardwareAccelerated(
+ nsACString& aFailureReason) const {
+ AssertOnManagerThread();
+ aFailureReason = mHardwareAcceleratedReason;
+ return mIsHardwareAccelerated;
+}
+
+nsCString RemoteDecoderChild::GetDescriptionName() const {
+ AssertOnManagerThread();
+ return mDescription;
+}
+
+nsCString RemoteDecoderChild::GetProcessName() const {
+ AssertOnManagerThread();
+ return mProcessName;
+}
+
+nsCString RemoteDecoderChild::GetCodecName() const {
+ AssertOnManagerThread();
+ return mCodecName;
+}
+
+void RemoteDecoderChild::SetSeekThreshold(const media::TimeUnit& aTime) {
+ AssertOnManagerThread();
+ Unused << SendSetSeekThreshold(aTime);
+}
+
+MediaDataDecoder::ConversionRequired RemoteDecoderChild::NeedsConversion()
+ const {
+ AssertOnManagerThread();
+ return mConversion;
+}
+
+void RemoteDecoderChild::AssertOnManagerThread() const {
+ MOZ_ASSERT(mThread->IsOnCurrentThread());
+}
+
+RemoteDecoderManagerChild* RemoteDecoderChild::GetManager() {
+ if (!CanSend()) {
+ return nullptr;
+ }
+ return static_cast<RemoteDecoderManagerChild*>(Manager());
+}
+
+} // namespace mozilla
diff --git a/dom/media/ipc/RemoteDecoderChild.h b/dom/media/ipc/RemoteDecoderChild.h
new file mode 100644
index 0000000000..b1c283cbe8
--- /dev/null
+++ b/dom/media/ipc/RemoteDecoderChild.h
@@ -0,0 +1,90 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef include_dom_media_ipc_RemoteDecoderChild_h
+#define include_dom_media_ipc_RemoteDecoderChild_h
+
+#include <functional>
+
+#include "mozilla/PRemoteDecoderChild.h"
+#include "mozilla/RemoteDecoderManagerChild.h"
+#include "mozilla/ShmemRecycleAllocator.h"
+
+namespace mozilla {
+
+class RemoteDecoderManagerChild;
+using mozilla::MediaDataDecoder;
+using mozilla::ipc::IPCResult;
+
+class RemoteDecoderChild : public ShmemRecycleAllocator<RemoteDecoderChild>,
+ public PRemoteDecoderChild {
+ friend class PRemoteDecoderChild;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RemoteDecoderChild);
+
+ explicit RemoteDecoderChild(RemoteDecodeIn aLocation);
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ // This interface closely mirrors the MediaDataDecoder plus a bit
+ // (DestroyIPDL) to allow proxying to a remote decoder in RemoteDecoderModule.
+ RefPtr<MediaDataDecoder::InitPromise> Init();
+ RefPtr<MediaDataDecoder::DecodePromise> Decode(
+ const nsTArray<RefPtr<MediaRawData>>& aSamples);
+ RefPtr<MediaDataDecoder::DecodePromise> Drain();
+ RefPtr<MediaDataDecoder::FlushPromise> Flush();
+ RefPtr<mozilla::ShutdownPromise> Shutdown();
+ bool IsHardwareAccelerated(nsACString& aFailureReason) const;
+ nsCString GetDescriptionName() const;
+ nsCString GetProcessName() const;
+ nsCString GetCodecName() const;
+ void SetSeekThreshold(const media::TimeUnit& aTime);
+ MediaDataDecoder::ConversionRequired NeedsConversion() const;
+ void DestroyIPDL();
+
+ // Called from IPDL when our actor has been destroyed
+ void IPDLActorDestroyed();
+
+ RemoteDecoderManagerChild* GetManager();
+
+ protected:
+ virtual ~RemoteDecoderChild();
+ void AssertOnManagerThread() const;
+
+ virtual MediaResult ProcessOutput(DecodedOutputIPDL&& aDecodedData) = 0;
+ virtual void RecordShutdownTelemetry(bool aForAbnormalShutdown) {}
+
+ RefPtr<RemoteDecoderChild> mIPDLSelfRef;
+ MediaDataDecoder::DecodedData mDecodedData;
+ const RemoteDecodeIn mLocation;
+
+ private:
+ const nsCOMPtr<nsISerialEventTarget> mThread;
+
+ MozPromiseHolder<MediaDataDecoder::InitPromise> mInitPromise;
+ MozPromiseRequestHolder<PRemoteDecoderChild::InitPromise> mInitPromiseRequest;
+ MozPromiseHolder<MediaDataDecoder::DecodePromise> mDecodePromise;
+ MozPromiseHolder<MediaDataDecoder::DecodePromise> mDrainPromise;
+ MozPromiseHolder<MediaDataDecoder::FlushPromise> mFlushPromise;
+ MozPromiseHolder<mozilla::ShutdownPromise> mShutdownPromise;
+
+ void HandleRejectionError(
+ const ipc::ResponseRejectReason& aReason,
+ std::function<void(const MediaResult&)>&& aCallback);
+
+ nsCString mHardwareAcceleratedReason;
+ nsCString mDescription;
+ nsCString mProcessName;
+ nsCString mCodecName;
+ bool mIsHardwareAccelerated = false;
+ bool mRemoteDecoderCrashed = false;
+ MediaDataDecoder::ConversionRequired mConversion =
+ MediaDataDecoder::ConversionRequired::kNeedNone;
+};
+
+} // namespace mozilla
+
+#endif // include_dom_media_ipc_RemoteDecoderChild_h
diff --git a/dom/media/ipc/RemoteDecoderManagerChild.cpp b/dom/media/ipc/RemoteDecoderManagerChild.cpp
new file mode 100644
index 0000000000..3c0e75ae16
--- /dev/null
+++ b/dom/media/ipc/RemoteDecoderManagerChild.cpp
@@ -0,0 +1,886 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "RemoteDecoderManagerChild.h"
+
+#include "PDMFactory.h"
+#include "RemoteAudioDecoder.h"
+#include "RemoteMediaDataDecoder.h"
+#include "RemoteVideoDecoder.h"
+#include "VideoUtils.h"
+#include "mozilla/DataMutex.h"
+#include "mozilla/RemoteDecodeUtils.h"
+#include "mozilla/SyncRunnable.h"
+#include "mozilla/dom/ContentChild.h" // for launching RDD w/ ContentChild
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/DataSurfaceHelpers.h"
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+#include "mozilla/ipc/Endpoint.h"
+#include "mozilla/layers/ISurfaceAllocator.h"
+#include "mozilla/ipc/UtilityAudioDecoderChild.h"
+#include "nsContentUtils.h"
+#include "nsIObserver.h"
+#include "mozilla/StaticPrefs_media.h"
+
+#ifdef MOZ_WMF_MEDIA_ENGINE
+# include "MFMediaEngineChild.h"
+#endif
+
+#ifdef MOZ_WMF_CDM
+# include "MFCDMChild.h"
+#endif
+
+namespace mozilla {
+
+#define LOG(msg, ...) \
+ MOZ_LOG(gRemoteDecodeLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
+
+using namespace layers;
+using namespace gfx;
+
+// Used so that we only ever attempt to check if the RDD/GPU/Utility processes
+// should be launched serially. Protects sLaunchPromise
+StaticMutex sLaunchMutex;
+static EnumeratedArray<RemoteDecodeIn, RemoteDecodeIn::SENTINEL,
+ StaticRefPtr<GenericNonExclusivePromise>>
+ sLaunchPromises MOZ_GUARDED_BY(sLaunchMutex);
+
+// Only modified on the main-thread, read on any thread. While it could be read
+// on the main thread directly, for clarity we force access via the DataMutex
+// wrapper.
+static StaticDataMutex<StaticRefPtr<nsIThread>>
+ sRemoteDecoderManagerChildThread("sRemoteDecoderManagerChildThread");
+
+// Only accessed from sRemoteDecoderManagerChildThread
+static EnumeratedArray<RemoteDecodeIn, RemoteDecodeIn::SENTINEL,
+ StaticRefPtr<RemoteDecoderManagerChild>>
+ sRemoteDecoderManagerChildForProcesses;
+
+static UniquePtr<nsTArray<RefPtr<Runnable>>> sRecreateTasks;
+
+// Used for protecting codec support information collected from different remote
+// processes.
+StaticMutex sProcessSupportedMutex;
+static EnumeratedArray<RemoteDecodeIn, RemoteDecodeIn::SENTINEL,
+ Maybe<media::MediaCodecsSupported>>
+ sProcessSupported MOZ_GUARDED_BY(sProcessSupportedMutex);
+
+class ShutdownObserver final : public nsIObserver {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+ protected:
+ ~ShutdownObserver() = default;
+};
+NS_IMPL_ISUPPORTS(ShutdownObserver, nsIObserver);
+
+NS_IMETHODIMP
+ShutdownObserver::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ MOZ_ASSERT(!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID));
+ RemoteDecoderManagerChild::Shutdown();
+ return NS_OK;
+}
+
+StaticRefPtr<ShutdownObserver> sObserver;
+
+/* static */
+void RemoteDecoderManagerChild::Init() {
+ MOZ_ASSERT(NS_IsMainThread());
+ LOG("RemoteDecoderManagerChild Init");
+
+ auto remoteDecoderManagerThread = sRemoteDecoderManagerChildThread.Lock();
+ if (!*remoteDecoderManagerThread) {
+ LOG("RemoteDecoderManagerChild's thread is created");
+ // We can't use a MediaThreadType::SUPERVISOR as the RemoteDecoderModule
+ // runs on it and dispatch synchronous tasks to the manager thread, should
+ // more than 4 concurrent videos being instantiated at the same time, we
+ // could end up in a deadlock.
+ RefPtr<nsIThread> childThread;
+ nsresult rv = NS_NewNamedThread(
+ "RemVidChild", getter_AddRefs(childThread),
+ NS_NewRunnableFunction(
+ "RemoteDecoderManagerChild::InitPBackground", []() {
+ ipc::PBackgroundChild* bgActor =
+ ipc::BackgroundChild::GetOrCreateForCurrentThread();
+ NS_WARNING_ASSERTION(bgActor,
+ "Failed to start Background channel");
+ Unused << bgActor;
+ }));
+
+ NS_ENSURE_SUCCESS_VOID(rv);
+ *remoteDecoderManagerThread = childThread;
+ sRecreateTasks = MakeUnique<nsTArray<RefPtr<Runnable>>>();
+ sObserver = new ShutdownObserver();
+ nsContentUtils::RegisterShutdownObserver(sObserver);
+ }
+}
+
+/* static */
+void RemoteDecoderManagerChild::InitForGPUProcess(
+ Endpoint<PRemoteDecoderManagerChild>&& aVideoManager) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ Init();
+
+ auto remoteDecoderManagerThread = sRemoteDecoderManagerChildThread.Lock();
+ MOZ_ALWAYS_SUCCEEDS(
+ (*remoteDecoderManagerThread)
+ ->Dispatch(NewRunnableFunction(
+ "InitForContentRunnable",
+ &OpenRemoteDecoderManagerChildForProcess,
+ std::move(aVideoManager), RemoteDecodeIn::GpuProcess)));
+}
+
+/* static */
+void RemoteDecoderManagerChild::Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ LOG("RemoteDecoderManagerChild Shutdown");
+
+ if (sObserver) {
+ nsContentUtils::UnregisterShutdownObserver(sObserver);
+ sObserver = nullptr;
+ }
+
+ nsCOMPtr<nsIThread> childThread;
+ {
+ auto remoteDecoderManagerThread = sRemoteDecoderManagerChildThread.Lock();
+ childThread = remoteDecoderManagerThread->forget();
+ LOG("RemoteDecoderManagerChild's thread is released");
+ }
+ if (childThread) {
+ MOZ_ALWAYS_SUCCEEDS(childThread->Dispatch(NS_NewRunnableFunction(
+ "dom::RemoteDecoderManagerChild::Shutdown", []() {
+ for (auto& p : sRemoteDecoderManagerChildForProcesses) {
+ if (p && p->CanSend()) {
+ p->Close();
+ }
+ p = nullptr;
+ }
+ {
+ StaticMutexAutoLock lock(sLaunchMutex);
+ for (auto& p : sLaunchPromises) {
+ p = nullptr;
+ }
+ }
+ ipc::BackgroundChild::CloseForCurrentThread();
+ })));
+ childThread->Shutdown();
+ sRecreateTasks = nullptr;
+ }
+}
+
+void RemoteDecoderManagerChild::RunWhenGPUProcessRecreated(
+ already_AddRefed<Runnable> aTask) {
+ nsCOMPtr<nsISerialEventTarget> managerThread = GetManagerThread();
+ if (!managerThread) {
+ // We've been shutdown, bail.
+ return;
+ }
+ MOZ_ASSERT(managerThread->IsOnCurrentThread());
+
+ // If we've already been recreated, then run the task immediately.
+ auto* manager = GetSingleton(RemoteDecodeIn::GpuProcess);
+ if (manager && manager != this && manager->CanSend()) {
+ RefPtr<Runnable> task = aTask;
+ task->Run();
+ } else {
+ sRecreateTasks->AppendElement(aTask);
+ }
+}
+
+/* static */
+RemoteDecoderManagerChild* RemoteDecoderManagerChild::GetSingleton(
+ RemoteDecodeIn aLocation) {
+ nsCOMPtr<nsISerialEventTarget> managerThread = GetManagerThread();
+ if (!managerThread) {
+ // We've been shutdown, bail.
+ return nullptr;
+ }
+ MOZ_ASSERT(managerThread->IsOnCurrentThread());
+ switch (aLocation) {
+ case RemoteDecodeIn::GpuProcess:
+ case RemoteDecodeIn::RddProcess:
+ case RemoteDecodeIn::UtilityProcess_Generic:
+ case RemoteDecodeIn::UtilityProcess_AppleMedia:
+ case RemoteDecodeIn::UtilityProcess_WMF:
+ case RemoteDecodeIn::UtilityProcess_MFMediaEngineCDM:
+ return sRemoteDecoderManagerChildForProcesses[aLocation];
+ default:
+ MOZ_CRASH("Unexpected RemoteDecode variant");
+ return nullptr;
+ }
+}
+
+/* static */
+nsISerialEventTarget* RemoteDecoderManagerChild::GetManagerThread() {
+ auto remoteDecoderManagerThread = sRemoteDecoderManagerChildThread.Lock();
+ return *remoteDecoderManagerThread;
+}
+
+/* static */
+bool RemoteDecoderManagerChild::Supports(
+ RemoteDecodeIn aLocation, const SupportDecoderParams& aParams,
+ DecoderDoctorDiagnostics* aDiagnostics) {
+ Maybe<media::MediaCodecsSupported> supported;
+ switch (aLocation) {
+ case RemoteDecodeIn::GpuProcess:
+ case RemoteDecodeIn::RddProcess:
+ case RemoteDecodeIn::UtilityProcess_AppleMedia:
+ case RemoteDecodeIn::UtilityProcess_Generic:
+ case RemoteDecodeIn::UtilityProcess_WMF:
+ case RemoteDecodeIn::UtilityProcess_MFMediaEngineCDM: {
+ StaticMutexAutoLock lock(sProcessSupportedMutex);
+ supported = sProcessSupported[aLocation];
+ break;
+ }
+ default:
+ return false;
+ }
+ if (!supported) {
+ // We haven't received the correct information yet from either the GPU or
+ // the RDD process nor the Utility process.
+ if (aLocation == RemoteDecodeIn::UtilityProcess_Generic ||
+ aLocation == RemoteDecodeIn::UtilityProcess_AppleMedia ||
+ aLocation == RemoteDecodeIn::UtilityProcess_WMF ||
+ aLocation == RemoteDecodeIn::UtilityProcess_MFMediaEngineCDM) {
+ LaunchUtilityProcessIfNeeded(aLocation);
+ }
+ if (aLocation == RemoteDecodeIn::RddProcess) {
+ // Ensure the RDD process got started.
+ // TODO: This can be removed once bug 1684991 is fixed.
+ LaunchRDDProcessIfNeeded();
+ }
+
+ // Assume the format is supported to prevent false negative, if the remote
+ // process supports that specific track type.
+ const bool isVideo = aParams.mConfig.IsVideo();
+ const bool isAudio = aParams.mConfig.IsAudio();
+ const auto trackSupport = GetTrackSupport(aLocation);
+ if (isVideo) {
+ return trackSupport.contains(TrackSupport::Video);
+ }
+ if (isAudio) {
+ return trackSupport.contains(TrackSupport::Audio);
+ }
+ MOZ_ASSERT_UNREACHABLE("Not audio and video?!");
+ return false;
+ }
+
+ // We can ignore the SupportDecoderParams argument for now as creation of the
+ // decoder will actually fail later and fallback PDMs will be tested on later.
+ return PDMFactory::SupportsMimeType(aParams.MimeType(), *supported,
+ aLocation) !=
+ media::DecodeSupport::Unsupported;
+}
+
+/* static */
+RefPtr<PlatformDecoderModule::CreateDecoderPromise>
+RemoteDecoderManagerChild::CreateAudioDecoder(
+ const CreateDecoderParams& aParams, RemoteDecodeIn aLocation) {
+ nsCOMPtr<nsISerialEventTarget> managerThread = GetManagerThread();
+ if (!managerThread) {
+ // We got shutdown.
+ return PlatformDecoderModule::CreateDecoderPromise::CreateAndReject(
+ NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ }
+
+ if (!GetTrackSupport(aLocation).contains(TrackSupport::Audio)) {
+ return PlatformDecoderModule::CreateDecoderPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_CANCELED,
+ nsPrintfCString("%s doesn't support audio decoding",
+ RemoteDecodeInToStr(aLocation))
+ .get()),
+ __func__);
+ }
+
+ RefPtr<GenericNonExclusivePromise> launchPromise;
+ if (StaticPrefs::media_utility_process_enabled() &&
+ (aLocation == RemoteDecodeIn::UtilityProcess_Generic ||
+ aLocation == RemoteDecodeIn::UtilityProcess_AppleMedia ||
+ aLocation == RemoteDecodeIn::UtilityProcess_WMF)) {
+ launchPromise = LaunchUtilityProcessIfNeeded(aLocation);
+ } else if (aLocation == RemoteDecodeIn::UtilityProcess_MFMediaEngineCDM) {
+ launchPromise = LaunchUtilityProcessIfNeeded(aLocation);
+ } else {
+ launchPromise = LaunchRDDProcessIfNeeded();
+ }
+ LOG("Create audio decoder in %s", RemoteDecodeInToStr(aLocation));
+
+ return launchPromise->Then(
+ managerThread, __func__,
+ [params = CreateDecoderParamsForAsync(aParams), aLocation](bool) {
+ auto child = MakeRefPtr<RemoteAudioDecoderChild>(aLocation);
+ MediaResult result = child->InitIPDL(
+ params.AudioConfig(), params.mOptions, params.mMediaEngineId);
+ if (NS_FAILED(result)) {
+ return PlatformDecoderModule::CreateDecoderPromise::CreateAndReject(
+ result, __func__);
+ }
+ return Construct(std::move(child), aLocation);
+ },
+ [aLocation](nsresult aResult) {
+ return PlatformDecoderModule::CreateDecoderPromise::CreateAndReject(
+ MediaResult(aResult,
+ aLocation == RemoteDecodeIn::GpuProcess
+ ? "Couldn't start GPU process"
+ : (aLocation == RemoteDecodeIn::RddProcess
+ ? "Couldn't start RDD process"
+ : "Couldn't start Utility process")),
+ __func__);
+ });
+}
+
+/* static */
+RefPtr<PlatformDecoderModule::CreateDecoderPromise>
+RemoteDecoderManagerChild::CreateVideoDecoder(
+ const CreateDecoderParams& aParams, RemoteDecodeIn aLocation) {
+ nsCOMPtr<nsISerialEventTarget> managerThread = GetManagerThread();
+ if (!managerThread) {
+ // We got shutdown.
+ return PlatformDecoderModule::CreateDecoderPromise::CreateAndReject(
+ NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ }
+
+ if (!aParams.mKnowsCompositor && aLocation == RemoteDecodeIn::GpuProcess) {
+ // We don't have an image bridge; don't attempt to decode in the GPU
+ // process.
+ return PlatformDecoderModule::CreateDecoderPromise::CreateAndReject(
+ NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR, __func__);
+ }
+
+ if (!GetTrackSupport(aLocation).contains(TrackSupport::Video)) {
+ return PlatformDecoderModule::CreateDecoderPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_CANCELED,
+ nsPrintfCString("%s doesn't support video decoding",
+ RemoteDecodeInToStr(aLocation))
+ .get()),
+ __func__);
+ }
+
+ MOZ_ASSERT(aLocation != RemoteDecodeIn::Unspecified);
+
+ RefPtr<GenericNonExclusivePromise> p;
+ if (aLocation == RemoteDecodeIn::GpuProcess) {
+ p = GenericNonExclusivePromise::CreateAndResolve(true, __func__);
+ } else if (aLocation == RemoteDecodeIn::UtilityProcess_MFMediaEngineCDM) {
+ p = LaunchUtilityProcessIfNeeded(aLocation);
+ } else {
+ p = LaunchRDDProcessIfNeeded();
+ }
+ LOG("Create video decoder in %s", RemoteDecodeInToStr(aLocation));
+
+ return p->Then(
+ managerThread, __func__,
+ [aLocation, params = CreateDecoderParamsForAsync(aParams)](bool) {
+ auto child = MakeRefPtr<RemoteVideoDecoderChild>(aLocation);
+ MediaResult result = child->InitIPDL(
+ params.VideoConfig(), params.mRate.mValue, params.mOptions,
+ params.mKnowsCompositor
+ ? Some(params.mKnowsCompositor->GetTextureFactoryIdentifier())
+ : Nothing(),
+ params.mMediaEngineId, params.mTrackingId);
+ if (NS_FAILED(result)) {
+ return PlatformDecoderModule::CreateDecoderPromise::CreateAndReject(
+ result, __func__);
+ }
+ return Construct(std::move(child), aLocation);
+ },
+ [](nsresult aResult) {
+ return PlatformDecoderModule::CreateDecoderPromise::CreateAndReject(
+ MediaResult(aResult, "Couldn't start RDD process"), __func__);
+ });
+}
+
+/* static */
+RefPtr<PlatformDecoderModule::CreateDecoderPromise>
+RemoteDecoderManagerChild::Construct(RefPtr<RemoteDecoderChild>&& aChild,
+ RemoteDecodeIn aLocation) {
+ nsCOMPtr<nsISerialEventTarget> managerThread = GetManagerThread();
+ if (!managerThread) {
+ // We got shutdown.
+ return PlatformDecoderModule::CreateDecoderPromise::CreateAndReject(
+ NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ }
+ MOZ_ASSERT(managerThread->IsOnCurrentThread());
+
+ RefPtr<PlatformDecoderModule::CreateDecoderPromise> p =
+ aChild->SendConstruct()->Then(
+ managerThread, __func__,
+ [child = std::move(aChild)](MediaResult aResult) {
+ if (NS_FAILED(aResult)) {
+ // We will never get to use this remote decoder, tear it down.
+ child->DestroyIPDL();
+ return PlatformDecoderModule::CreateDecoderPromise::
+ CreateAndReject(aResult, __func__);
+ }
+ return PlatformDecoderModule::CreateDecoderPromise::
+ CreateAndResolve(MakeRefPtr<RemoteMediaDataDecoder>(child),
+ __func__);
+ },
+ [aLocation](const mozilla::ipc::ResponseRejectReason& aReason) {
+ // The parent has died.
+ nsresult err =
+ NS_ERROR_DOM_MEDIA_REMOTE_DECODER_CRASHED_UTILITY_ERR;
+ if (aLocation == RemoteDecodeIn::GpuProcess ||
+ aLocation == RemoteDecodeIn::RddProcess) {
+ err = NS_ERROR_DOM_MEDIA_REMOTE_DECODER_CRASHED_RDD_OR_GPU_ERR;
+ } else if (aLocation ==
+ RemoteDecodeIn::UtilityProcess_MFMediaEngineCDM) {
+ err = NS_ERROR_DOM_MEDIA_REMOTE_DECODER_CRASHED_MF_CDM_ERR;
+ }
+ return PlatformDecoderModule::CreateDecoderPromise::CreateAndReject(
+ err, __func__);
+ });
+ return p;
+}
+
+/* static */
+RefPtr<GenericNonExclusivePromise>
+RemoteDecoderManagerChild::LaunchRDDProcessIfNeeded() {
+ MOZ_DIAGNOSTIC_ASSERT(XRE_IsContentProcess(),
+ "Only supported from a content process.");
+
+ nsCOMPtr<nsISerialEventTarget> managerThread = GetManagerThread();
+ if (!managerThread) {
+ // We got shutdown.
+ return GenericNonExclusivePromise::CreateAndReject(NS_ERROR_FAILURE,
+ __func__);
+ }
+
+ StaticMutexAutoLock lock(sLaunchMutex);
+ auto& rddLaunchPromise = sLaunchPromises[RemoteDecodeIn::RddProcess];
+ if (rddLaunchPromise) {
+ return rddLaunchPromise;
+ }
+
+ // We have a couple possible states here. We are in a content process
+ // and:
+ // 1) the RDD process has never been launched. RDD should be launched
+ // and the IPC connections setup.
+ // 2) the RDD process has been launched, but this particular content
+ // process has not setup (or has lost) its IPC connection.
+ // In the code below, we assume we need to launch the RDD process and
+ // setup the IPC connections. However, if the manager thread for
+ // RemoteDecoderManagerChild is available we do a quick check to see
+ // if we can send (meaning the IPC channel is open). If we can send,
+ // then no work is necessary. If we can't send, then we call
+ // LaunchRDDProcess which will launch RDD if necessary, and setup the
+ // IPC connections between *this* content process and the RDD process.
+
+ RefPtr<GenericNonExclusivePromise> p = InvokeAsync(
+ managerThread, __func__, []() -> RefPtr<GenericNonExclusivePromise> {
+ auto* rps = GetSingleton(RemoteDecodeIn::RddProcess);
+ if (rps && rps->CanSend()) {
+ return GenericNonExclusivePromise::CreateAndResolve(true, __func__);
+ }
+ nsCOMPtr<nsISerialEventTarget> managerThread = GetManagerThread();
+ ipc::PBackgroundChild* bgActor =
+ ipc::BackgroundChild::GetForCurrentThread();
+ if (!managerThread || NS_WARN_IF(!bgActor)) {
+ return GenericNonExclusivePromise::CreateAndReject(NS_ERROR_FAILURE,
+ __func__);
+ }
+
+ return bgActor->SendEnsureRDDProcessAndCreateBridge()->Then(
+ managerThread, __func__,
+ [](ipc::PBackgroundChild::EnsureRDDProcessAndCreateBridgePromise::
+ ResolveOrRejectValue&& aResult) {
+ nsCOMPtr<nsISerialEventTarget> managerThread = GetManagerThread();
+ if (!managerThread || aResult.IsReject()) {
+ // The parent process died or we got shutdown
+ return GenericNonExclusivePromise::CreateAndReject(
+ NS_ERROR_FAILURE, __func__);
+ }
+ nsresult rv = std::get<0>(aResult.ResolveValue());
+ if (NS_FAILED(rv)) {
+ return GenericNonExclusivePromise::CreateAndReject(rv,
+ __func__);
+ }
+ OpenRemoteDecoderManagerChildForProcess(
+ std::get<1>(std::move(aResult.ResolveValue())),
+ RemoteDecodeIn::RddProcess);
+ return GenericNonExclusivePromise::CreateAndResolve(true,
+ __func__);
+ });
+ });
+
+ // This should not be dispatched to a threadpool thread, so use managerThread
+ p = p->Then(
+ managerThread, __func__,
+ [](const GenericNonExclusivePromise::ResolveOrRejectValue& aResult) {
+ StaticMutexAutoLock lock(sLaunchMutex);
+ sLaunchPromises[RemoteDecodeIn::RddProcess] = nullptr;
+ return GenericNonExclusivePromise::CreateAndResolveOrReject(aResult,
+ __func__);
+ });
+
+ rddLaunchPromise = p;
+ return rddLaunchPromise;
+}
+
+/* static */
+RefPtr<GenericNonExclusivePromise>
+RemoteDecoderManagerChild::LaunchUtilityProcessIfNeeded(
+ RemoteDecodeIn aLocation) {
+ MOZ_DIAGNOSTIC_ASSERT(XRE_IsContentProcess(),
+ "Only supported from a content process.");
+
+ nsCOMPtr<nsISerialEventTarget> managerThread = GetManagerThread();
+ if (!managerThread) {
+ // We got shutdown.
+ return GenericNonExclusivePromise::CreateAndReject(NS_ERROR_FAILURE,
+ __func__);
+ }
+
+ MOZ_ASSERT(aLocation == RemoteDecodeIn::UtilityProcess_Generic ||
+ aLocation == RemoteDecodeIn::UtilityProcess_AppleMedia ||
+ aLocation == RemoteDecodeIn::UtilityProcess_WMF ||
+ aLocation == RemoteDecodeIn::UtilityProcess_MFMediaEngineCDM);
+ StaticMutexAutoLock lock(sLaunchMutex);
+ auto& utilityLaunchPromise = sLaunchPromises[aLocation];
+
+ if (utilityLaunchPromise) {
+ return utilityLaunchPromise;
+ }
+
+ // We have a couple possible states here. We are in a content process
+ // and:
+ // 1) the Utility process has never been launched. Utility should be launched
+ // and the IPC connections setup.
+ // 2) the Utility process has been launched, but this particular content
+ // process has not setup (or has lost) its IPC connection.
+ // In the code below, we assume we need to launch the Utility process and
+ // setup the IPC connections. However, if the manager thread for
+ // RemoteDecoderManagerChild is available we do a quick check to see
+ // if we can send (meaning the IPC channel is open). If we can send,
+ // then no work is necessary. If we can't send, then we call
+ // LaunchUtilityProcess which will launch Utility if necessary, and setup the
+ // IPC connections between *this* content process and the Utility process.
+
+ RefPtr<GenericNonExclusivePromise> p = InvokeAsync(
+ managerThread, __func__,
+ [aLocation]() -> RefPtr<GenericNonExclusivePromise> {
+ auto* rps = GetSingleton(aLocation);
+ if (rps && rps->CanSend()) {
+ return GenericNonExclusivePromise::CreateAndResolve(true, __func__);
+ }
+ nsCOMPtr<nsISerialEventTarget> managerThread = GetManagerThread();
+ ipc::PBackgroundChild* bgActor =
+ ipc::BackgroundChild::GetForCurrentThread();
+ if (!managerThread || NS_WARN_IF(!bgActor)) {
+ return GenericNonExclusivePromise::CreateAndReject(NS_ERROR_FAILURE,
+ __func__);
+ }
+
+ return bgActor->SendEnsureUtilityProcessAndCreateBridge(aLocation)
+ ->Then(managerThread, __func__,
+ [aLocation](ipc::PBackgroundChild::
+ EnsureUtilityProcessAndCreateBridgePromise::
+ ResolveOrRejectValue&& aResult)
+ -> RefPtr<GenericNonExclusivePromise> {
+ nsCOMPtr<nsISerialEventTarget> managerThread =
+ GetManagerThread();
+ if (!managerThread || aResult.IsReject()) {
+ // The parent process died or we got shutdown
+ return GenericNonExclusivePromise::CreateAndReject(
+ NS_ERROR_FAILURE, __func__);
+ }
+ nsresult rv = std::get<0>(aResult.ResolveValue());
+ if (NS_FAILED(rv)) {
+ return GenericNonExclusivePromise::CreateAndReject(
+ rv, __func__);
+ }
+ OpenRemoteDecoderManagerChildForProcess(
+ std::get<1>(std::move(aResult.ResolveValue())),
+ aLocation);
+ return GenericNonExclusivePromise::CreateAndResolve(
+ true, __func__);
+ });
+ });
+
+ // Let's make sure this promise is also run on the managerThread to avoid
+ // situations where it would be run on a threadpool thread.
+ // During bug 1794988 this was happening when enabling Utility for audio on
+ // Android when running the sequence of tests
+ // dom/media/test/test_access_control.html
+ // dom/media/test/test_arraybuffer.html
+ //
+ // We would have a launched utility process but the promises would not have
+ // been cleared, so any subsequent tentative to perform audio decoding would
+ // think the process is not yet ran and it would try to wait on the pending
+ // promises.
+ p = p->Then(
+ managerThread, __func__,
+ [aLocation](
+ const GenericNonExclusivePromise::ResolveOrRejectValue& aResult) {
+ StaticMutexAutoLock lock(sLaunchMutex);
+ sLaunchPromises[aLocation] = nullptr;
+ return GenericNonExclusivePromise::CreateAndResolveOrReject(aResult,
+ __func__);
+ });
+ utilityLaunchPromise = p;
+ return utilityLaunchPromise;
+}
+
+/* static */
+TrackSupportSet RemoteDecoderManagerChild::GetTrackSupport(
+ RemoteDecodeIn aLocation) {
+ switch (aLocation) {
+ case RemoteDecodeIn::GpuProcess: {
+ return TrackSupportSet{TrackSupport::Video};
+ }
+ case RemoteDecodeIn::RddProcess: {
+ TrackSupportSet s{TrackSupport::Video};
+ // Only use RDD for audio decoding if we don't have the utility process.
+ if (!StaticPrefs::media_utility_process_enabled()) {
+ s += TrackSupport::Audio;
+ }
+ return s;
+ }
+ case RemoteDecodeIn::UtilityProcess_Generic:
+ case RemoteDecodeIn::UtilityProcess_AppleMedia:
+ case RemoteDecodeIn::UtilityProcess_WMF:
+ return StaticPrefs::media_utility_process_enabled()
+ ? TrackSupportSet{TrackSupport::Audio}
+ : TrackSupportSet{TrackSupport::None};
+ case RemoteDecodeIn::UtilityProcess_MFMediaEngineCDM: {
+ TrackSupportSet s{TrackSupport::None};
+#ifdef MOZ_WMF_MEDIA_ENGINE
+ // When we enable the media engine, it would need both tracks to
+ // synchronize the a/v playback.
+ if (StaticPrefs::media_wmf_media_engine_enabled()) {
+ s += TrackSupportSet{TrackSupport::Audio, TrackSupport::Video};
+ }
+#endif
+ return s;
+ }
+ default:
+ MOZ_ASSERT_UNREACHABLE("Undefined location!");
+ }
+ return TrackSupportSet{TrackSupport::None};
+}
+
+PRemoteDecoderChild* RemoteDecoderManagerChild::AllocPRemoteDecoderChild(
+ const RemoteDecoderInfoIPDL& /* not used */,
+ const CreateDecoderParams::OptionSet& aOptions,
+ const Maybe<layers::TextureFactoryIdentifier>& aIdentifier,
+ const Maybe<uint64_t>& aMediaEngineId,
+ const Maybe<TrackingId>& aTrackingId) {
+ // RemoteDecoderModule is responsible for creating RemoteDecoderChild
+ // classes.
+ MOZ_ASSERT(false,
+ "RemoteDecoderManagerChild cannot create "
+ "RemoteDecoderChild classes");
+ return nullptr;
+}
+
+bool RemoteDecoderManagerChild::DeallocPRemoteDecoderChild(
+ PRemoteDecoderChild* actor) {
+ RemoteDecoderChild* child = static_cast<RemoteDecoderChild*>(actor);
+ child->IPDLActorDestroyed();
+ return true;
+}
+
+PMFMediaEngineChild* RemoteDecoderManagerChild::AllocPMFMediaEngineChild() {
+ MOZ_ASSERT_UNREACHABLE(
+ "RemoteDecoderManagerChild cannot create MFMediaEngineChild classes");
+ return nullptr;
+}
+
+bool RemoteDecoderManagerChild::DeallocPMFMediaEngineChild(
+ PMFMediaEngineChild* actor) {
+#ifdef MOZ_WMF_MEDIA_ENGINE
+ MFMediaEngineChild* child = static_cast<MFMediaEngineChild*>(actor);
+ child->IPDLActorDestroyed();
+#endif
+ return true;
+}
+
+PMFCDMChild* RemoteDecoderManagerChild::AllocPMFCDMChild(const nsAString&) {
+ MOZ_ASSERT_UNREACHABLE(
+ "RemoteDecoderManagerChild cannot create PMFContentDecryptionModuleChild "
+ "classes");
+ return nullptr;
+}
+
+bool RemoteDecoderManagerChild::DeallocPMFCDMChild(PMFCDMChild* actor) {
+#ifdef MOZ_WMF_CDM
+ static_cast<MFCDMChild*>(actor)->IPDLActorDestroyed();
+#endif
+ return true;
+}
+
+RemoteDecoderManagerChild::RemoteDecoderManagerChild(RemoteDecodeIn aLocation)
+ : mLocation(aLocation) {
+ MOZ_ASSERT(mLocation == RemoteDecodeIn::GpuProcess ||
+ mLocation == RemoteDecodeIn::RddProcess ||
+ mLocation == RemoteDecodeIn::UtilityProcess_Generic ||
+ mLocation == RemoteDecodeIn::UtilityProcess_AppleMedia ||
+ mLocation == RemoteDecodeIn::UtilityProcess_WMF ||
+ mLocation == RemoteDecodeIn::UtilityProcess_MFMediaEngineCDM);
+}
+
+/* static */
+void RemoteDecoderManagerChild::OpenRemoteDecoderManagerChildForProcess(
+ Endpoint<PRemoteDecoderManagerChild>&& aEndpoint,
+ RemoteDecodeIn aLocation) {
+ nsCOMPtr<nsISerialEventTarget> managerThread = GetManagerThread();
+ if (!managerThread) {
+ // We've been shutdown, bail.
+ return;
+ }
+ MOZ_ASSERT(managerThread->IsOnCurrentThread());
+
+ // For GPU process, make sure we always dispatch everything in sRecreateTasks,
+ // even if we fail since this is as close to being recreated as we will ever
+ // be.
+ auto runRecreateTasksIfNeeded = MakeScopeExit([aLocation]() {
+ if (aLocation == RemoteDecodeIn::GpuProcess) {
+ for (Runnable* task : *sRecreateTasks) {
+ task->Run();
+ }
+ sRecreateTasks->Clear();
+ }
+ });
+
+ // Only create RemoteDecoderManagerChild, bind new endpoint and init
+ // ipdl if:
+ // 1) haven't init'd sRemoteDecoderManagerChildForProcesses[aLocation]
+ // or
+ // 2) if ActorDestroy was called meaning the other end of the ipc channel was
+ // torn down
+ // But for GPU process, we always recreate a new manager child.
+ MOZ_ASSERT(aLocation != RemoteDecodeIn::SENTINEL);
+ auto& remoteDecoderManagerChild =
+ sRemoteDecoderManagerChildForProcesses[aLocation];
+ if (aLocation != RemoteDecodeIn::GpuProcess && remoteDecoderManagerChild &&
+ remoteDecoderManagerChild->CanSend()) {
+ return;
+ }
+ remoteDecoderManagerChild = nullptr;
+ if (aEndpoint.IsValid()) {
+ RefPtr<RemoteDecoderManagerChild> manager =
+ new RemoteDecoderManagerChild(aLocation);
+ if (aEndpoint.Bind(manager)) {
+ remoteDecoderManagerChild = manager;
+ }
+ }
+}
+
+bool RemoteDecoderManagerChild::DeallocShmem(mozilla::ipc::Shmem& aShmem) {
+ nsCOMPtr<nsISerialEventTarget> managerThread = GetManagerThread();
+ if (!managerThread) {
+ return false;
+ }
+ if (!managerThread->IsOnCurrentThread()) {
+ MOZ_ALWAYS_SUCCEEDS(managerThread->Dispatch(NS_NewRunnableFunction(
+ "RemoteDecoderManagerChild::DeallocShmem",
+ [self = RefPtr{this}, shmem = aShmem]() mutable {
+ if (self->CanSend()) {
+ self->PRemoteDecoderManagerChild::DeallocShmem(shmem);
+ }
+ })));
+ return true;
+ }
+ return PRemoteDecoderManagerChild::DeallocShmem(aShmem);
+}
+
+struct SurfaceDescriptorUserData {
+ SurfaceDescriptorUserData(RemoteDecoderManagerChild* aAllocator,
+ SurfaceDescriptor& aSD)
+ : mAllocator(aAllocator), mSD(aSD) {}
+ ~SurfaceDescriptorUserData() { DestroySurfaceDescriptor(mAllocator, &mSD); }
+
+ RefPtr<RemoteDecoderManagerChild> mAllocator;
+ SurfaceDescriptor mSD;
+};
+
+void DeleteSurfaceDescriptorUserData(void* aClosure) {
+ SurfaceDescriptorUserData* sd =
+ reinterpret_cast<SurfaceDescriptorUserData*>(aClosure);
+ delete sd;
+}
+
+already_AddRefed<SourceSurface> RemoteDecoderManagerChild::Readback(
+ const SurfaceDescriptorGPUVideo& aSD) {
+ // We can't use NS_DispatchAndSpinEventLoopUntilComplete here since that will
+ // spin the event loop while it waits. This function can be called from JS and
+ // we don't want that to happen.
+ nsCOMPtr<nsISerialEventTarget> managerThread = GetManagerThread();
+ if (!managerThread) {
+ return nullptr;
+ }
+
+ SurfaceDescriptor sd;
+ RefPtr<Runnable> task =
+ NS_NewRunnableFunction("RemoteDecoderManagerChild::Readback", [&]() {
+ if (CanSend()) {
+ SendReadback(aSD, &sd);
+ }
+ });
+ SyncRunnable::DispatchToThread(managerThread, task);
+
+ if (!IsSurfaceDescriptorValid(sd)) {
+ return nullptr;
+ }
+
+ RefPtr<DataSourceSurface> source = GetSurfaceForDescriptor(sd);
+ if (!source) {
+ DestroySurfaceDescriptor(this, &sd);
+ NS_WARNING("Failed to map SurfaceDescriptor in Readback");
+ return nullptr;
+ }
+
+ static UserDataKey sSurfaceDescriptor;
+ source->AddUserData(&sSurfaceDescriptor,
+ new SurfaceDescriptorUserData(this, sd),
+ DeleteSurfaceDescriptorUserData);
+
+ return source.forget();
+}
+
+void RemoteDecoderManagerChild::DeallocateSurfaceDescriptor(
+ const SurfaceDescriptorGPUVideo& aSD) {
+ nsCOMPtr<nsISerialEventTarget> managerThread = GetManagerThread();
+ if (!managerThread) {
+ return;
+ }
+ MOZ_ALWAYS_SUCCEEDS(managerThread->Dispatch(NS_NewRunnableFunction(
+ "RemoteDecoderManagerChild::DeallocateSurfaceDescriptor",
+ [ref = RefPtr{this}, sd = aSD]() {
+ if (ref->CanSend()) {
+ ref->SendDeallocateSurfaceDescriptorGPUVideo(sd);
+ }
+ })));
+}
+
+void RemoteDecoderManagerChild::HandleFatalError(const char* aMsg) {
+ dom::ContentChild::FatalErrorIfNotUsingGPUProcess(aMsg, OtherPid());
+}
+
+void RemoteDecoderManagerChild::SetSupported(
+ RemoteDecodeIn aLocation, const media::MediaCodecsSupported& aSupported) {
+ switch (aLocation) {
+ case RemoteDecodeIn::GpuProcess:
+ case RemoteDecodeIn::RddProcess:
+ case RemoteDecodeIn::UtilityProcess_AppleMedia:
+ case RemoteDecodeIn::UtilityProcess_Generic:
+ case RemoteDecodeIn::UtilityProcess_WMF:
+ case RemoteDecodeIn::UtilityProcess_MFMediaEngineCDM: {
+ StaticMutexAutoLock lock(sProcessSupportedMutex);
+ sProcessSupported[aLocation] = Some(aSupported);
+ break;
+ }
+ default:
+ MOZ_CRASH("Not to be used for any other process");
+ }
+}
+
+#undef LOG
+
+} // namespace mozilla
diff --git a/dom/media/ipc/RemoteDecoderManagerChild.h b/dom/media/ipc/RemoteDecoderManagerChild.h
new file mode 100644
index 0000000000..effeb92d93
--- /dev/null
+++ b/dom/media/ipc/RemoteDecoderManagerChild.h
@@ -0,0 +1,153 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef include_dom_media_ipc_RemoteDecoderManagerChild_h
+#define include_dom_media_ipc_RemoteDecoderManagerChild_h
+
+#include "GPUVideoImage.h"
+#include "PDMFactory.h"
+#include "ipc/EnumSerializer.h"
+#include "mozilla/EnumTypeTraits.h"
+#include "mozilla/PRemoteDecoderManagerChild.h"
+#include "mozilla/layers/VideoBridgeUtils.h"
+#include "mozilla/ipc/UtilityProcessSandboxing.h"
+
+namespace mozilla {
+
+class PMFCDMChild;
+class PMFMediaEngineChild;
+class RemoteDecoderChild;
+
+enum class RemoteDecodeIn {
+ Unspecified,
+ RddProcess,
+ GpuProcess,
+ UtilityProcess_Generic,
+ UtilityProcess_AppleMedia,
+ UtilityProcess_WMF,
+ UtilityProcess_MFMediaEngineCDM,
+ SENTINEL,
+};
+
+enum class TrackSupport {
+ None,
+ Audio,
+ Video,
+};
+using TrackSupportSet = EnumSet<TrackSupport, uint8_t>;
+
+class RemoteDecoderManagerChild final
+ : public PRemoteDecoderManagerChild,
+ public mozilla::ipc::IShmemAllocator,
+ public mozilla::layers::IGPUVideoSurfaceManager {
+ friend class PRemoteDecoderManagerChild;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RemoteDecoderManagerChild, override)
+
+ // Can only be called from the manager thread
+ static RemoteDecoderManagerChild* GetSingleton(RemoteDecodeIn aLocation);
+
+ static void Init();
+ static void SetSupported(RemoteDecodeIn aLocation,
+ const media::MediaCodecsSupported& aSupported);
+
+ // Can be called from any thread.
+ static bool Supports(RemoteDecodeIn aLocation,
+ const SupportDecoderParams& aParams,
+ DecoderDoctorDiagnostics* aDiagnostics);
+ static RefPtr<PlatformDecoderModule::CreateDecoderPromise> CreateAudioDecoder(
+ const CreateDecoderParams& aParams, RemoteDecodeIn aLocation);
+ static RefPtr<PlatformDecoderModule::CreateDecoderPromise> CreateVideoDecoder(
+ const CreateDecoderParams& aParams, RemoteDecodeIn aLocation);
+
+ // Can be called from any thread.
+ static nsISerialEventTarget* GetManagerThread();
+
+ // Return the track support information based on the location of the remote
+ // process. Thread-safe.
+ static TrackSupportSet GetTrackSupport(RemoteDecodeIn aLocation);
+
+ // Can be called from any thread, dispatches the request to the IPDL thread
+ // internally and will be ignored if the IPDL actor has been destroyed.
+ already_AddRefed<gfx::SourceSurface> Readback(
+ const SurfaceDescriptorGPUVideo& aSD) override;
+ void DeallocateSurfaceDescriptor(
+ const SurfaceDescriptorGPUVideo& aSD) override;
+
+ bool AllocShmem(size_t aSize, mozilla::ipc::Shmem* aShmem) override {
+ return PRemoteDecoderManagerChild::AllocShmem(aSize, aShmem);
+ }
+ bool AllocUnsafeShmem(size_t aSize, mozilla::ipc::Shmem* aShmem) override {
+ return PRemoteDecoderManagerChild::AllocUnsafeShmem(aSize, aShmem);
+ }
+
+ // Can be called from any thread, dispatches the request to the IPDL thread
+ // internally and will be ignored if the IPDL actor has been destroyed.
+ bool DeallocShmem(mozilla::ipc::Shmem& aShmem) override;
+
+ // Main thread only
+ static void InitForGPUProcess(
+ Endpoint<PRemoteDecoderManagerChild>&& aVideoManager);
+ static void Shutdown();
+
+ // Run aTask (on the manager thread) when we next attempt to create a new
+ // manager (even if creation fails). Intended to be called from ActorDestroy
+ // when we get notified that the old manager is being destroyed. Can only be
+ // called from the manager thread.
+ void RunWhenGPUProcessRecreated(already_AddRefed<Runnable> aTask);
+
+ RemoteDecodeIn Location() const { return mLocation; }
+
+ // A thread-safe method to launch the utility process if it hasn't launched
+ // yet.
+ static RefPtr<GenericNonExclusivePromise> LaunchUtilityProcessIfNeeded(
+ RemoteDecodeIn aLocation);
+
+ protected:
+ void HandleFatalError(const char* aMsg) override;
+
+ PRemoteDecoderChild* AllocPRemoteDecoderChild(
+ const RemoteDecoderInfoIPDL& aRemoteDecoderInfo,
+ const CreateDecoderParams::OptionSet& aOptions,
+ const Maybe<layers::TextureFactoryIdentifier>& aIdentifier,
+ const Maybe<uint64_t>& aMediaEngineId,
+ const Maybe<TrackingId>& aTrackingId);
+ bool DeallocPRemoteDecoderChild(PRemoteDecoderChild* actor);
+
+ PMFMediaEngineChild* AllocPMFMediaEngineChild();
+ bool DeallocPMFMediaEngineChild(PMFMediaEngineChild* actor);
+
+ PMFCDMChild* AllocPMFCDMChild(const nsAString& aKeySystem);
+ bool DeallocPMFCDMChild(PMFCDMChild* actor);
+
+ private:
+ explicit RemoteDecoderManagerChild(RemoteDecodeIn aLocation);
+ ~RemoteDecoderManagerChild() = default;
+ static RefPtr<PlatformDecoderModule::CreateDecoderPromise> Construct(
+ RefPtr<RemoteDecoderChild>&& aChild, RemoteDecodeIn aLocation);
+
+ static void OpenRemoteDecoderManagerChildForProcess(
+ Endpoint<PRemoteDecoderManagerChild>&& aEndpoint,
+ RemoteDecodeIn aLocation);
+
+ // A thread-safe method to launch the RDD process if it hasn't launched yet.
+ static RefPtr<GenericNonExclusivePromise> LaunchRDDProcessIfNeeded();
+
+ // The location for decoding, Rdd or Gpu process.
+ const RemoteDecodeIn mLocation;
+};
+
+} // namespace mozilla
+
+namespace IPC {
+template <>
+struct ParamTraits<mozilla::RemoteDecodeIn>
+ : public ContiguousEnumSerializer<mozilla::RemoteDecodeIn,
+ mozilla::RemoteDecodeIn::Unspecified,
+ mozilla::RemoteDecodeIn::SENTINEL> {};
+} // namespace IPC
+
+#endif // include_dom_media_ipc_RemoteDecoderManagerChild_h
diff --git a/dom/media/ipc/RemoteDecoderManagerParent.cpp b/dom/media/ipc/RemoteDecoderManagerParent.cpp
new file mode 100644
index 0000000000..bf6dd5c6aa
--- /dev/null
+++ b/dom/media/ipc/RemoteDecoderManagerParent.cpp
@@ -0,0 +1,351 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "RemoteDecoderManagerParent.h"
+
+#if XP_WIN
+# include <objbase.h>
+#endif
+
+#include "ImageContainer.h"
+#include "PDMFactory.h"
+#include "RemoteAudioDecoder.h"
+#include "RemoteVideoDecoder.h"
+#include "VideoUtils.h" // for MediaThreadType
+#include "mozilla/RDDParent.h"
+#include "mozilla/RemoteDecodeUtils.h"
+#include "mozilla/ipc/UtilityProcessChild.h"
+#include "mozilla/SyncRunnable.h"
+#include "mozilla/gfx/GPUParent.h"
+#include "mozilla/ipc/Endpoint.h"
+#include "mozilla/layers/ImageDataSerializer.h"
+#include "mozilla/layers/VideoBridgeChild.h"
+#include "mozilla/layers/VideoBridgeParent.h"
+
+#ifdef MOZ_WMF_MEDIA_ENGINE
+# include "MFMediaEngineParent.h"
+#endif
+
+#ifdef MOZ_WMF_CDM
+# include "MFCDMParent.h"
+#endif
+
+namespace mozilla {
+
+#define LOG(msg, ...) \
+ MOZ_LOG(gRemoteDecodeLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
+
+using namespace ipc;
+using namespace layers;
+using namespace gfx;
+
+StaticRefPtr<TaskQueue> sRemoteDecoderManagerParentThread;
+
+void RemoteDecoderManagerParent::StoreImage(
+ const SurfaceDescriptorGPUVideo& aSD, Image* aImage,
+ TextureClient* aTexture) {
+ MOZ_ASSERT(OnManagerThread());
+ mImageMap[static_cast<SurfaceDescriptorRemoteDecoder>(aSD).handle()] = aImage;
+ mTextureMap[static_cast<SurfaceDescriptorRemoteDecoder>(aSD).handle()] =
+ aTexture;
+}
+
+class RemoteDecoderManagerThreadShutdownObserver : public nsIObserver {
+ virtual ~RemoteDecoderManagerThreadShutdownObserver() = default;
+
+ public:
+ RemoteDecoderManagerThreadShutdownObserver() = default;
+
+ NS_DECL_ISUPPORTS
+
+ NS_IMETHOD Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) override {
+ MOZ_ASSERT(strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0);
+
+ RemoteDecoderManagerParent::ShutdownVideoBridge();
+ RemoteDecoderManagerParent::ShutdownThreads();
+ return NS_OK;
+ }
+};
+NS_IMPL_ISUPPORTS(RemoteDecoderManagerThreadShutdownObserver, nsIObserver);
+
+bool RemoteDecoderManagerParent::StartupThreads() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (sRemoteDecoderManagerParentThread) {
+ return true;
+ }
+
+ nsCOMPtr<nsIObserverService> observerService = services::GetObserverService();
+ if (!observerService) {
+ return false;
+ }
+
+ sRemoteDecoderManagerParentThread = TaskQueue::Create(
+ GetMediaThreadPool(MediaThreadType::SUPERVISOR), "RemVidParent");
+ if (XRE_IsGPUProcess()) {
+ MOZ_ALWAYS_SUCCEEDS(
+ sRemoteDecoderManagerParentThread->Dispatch(NS_NewRunnableFunction(
+ "RemoteDecoderManagerParent::StartupThreads",
+ []() { layers::VideoBridgeChild::StartupForGPUProcess(); })));
+ }
+
+ auto* obs = new RemoteDecoderManagerThreadShutdownObserver();
+ observerService->AddObserver(obs, NS_XPCOM_SHUTDOWN_OBSERVER_ID, false);
+ return true;
+}
+
+void RemoteDecoderManagerParent::ShutdownThreads() {
+ sRemoteDecoderManagerParentThread->BeginShutdown();
+ sRemoteDecoderManagerParentThread->AwaitShutdownAndIdle();
+ sRemoteDecoderManagerParentThread = nullptr;
+}
+
+/* static */
+void RemoteDecoderManagerParent::ShutdownVideoBridge() {
+ if (sRemoteDecoderManagerParentThread) {
+ RefPtr<Runnable> task = NS_NewRunnableFunction(
+ "RemoteDecoderManagerParent::ShutdownVideoBridge",
+ []() { VideoBridgeChild::Shutdown(); });
+ SyncRunnable::DispatchToThread(sRemoteDecoderManagerParentThread, task);
+ }
+}
+
+bool RemoteDecoderManagerParent::OnManagerThread() {
+ return sRemoteDecoderManagerParentThread->IsOnCurrentThread();
+}
+
+PDMFactory& RemoteDecoderManagerParent::EnsurePDMFactory() {
+ MOZ_ASSERT(OnManagerThread());
+ if (!mPDMFactory) {
+ mPDMFactory = MakeRefPtr<PDMFactory>();
+ }
+ return *mPDMFactory;
+}
+
+bool RemoteDecoderManagerParent::CreateForContent(
+ Endpoint<PRemoteDecoderManagerParent>&& aEndpoint,
+ dom::ContentParentId aChildId) {
+ MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_RDD ||
+ XRE_GetProcessType() == GeckoProcessType_Utility ||
+ XRE_GetProcessType() == GeckoProcessType_GPU);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!StartupThreads()) {
+ return false;
+ }
+
+ RefPtr<RemoteDecoderManagerParent> parent = new RemoteDecoderManagerParent(
+ sRemoteDecoderManagerParentThread, aChildId);
+
+ RefPtr<Runnable> task =
+ NewRunnableMethod<Endpoint<PRemoteDecoderManagerParent>&&>(
+ "dom::RemoteDecoderManagerParent::Open", parent,
+ &RemoteDecoderManagerParent::Open, std::move(aEndpoint));
+ MOZ_ALWAYS_SUCCEEDS(
+ sRemoteDecoderManagerParentThread->Dispatch(task.forget()));
+ return true;
+}
+
+bool RemoteDecoderManagerParent::CreateVideoBridgeToOtherProcess(
+ Endpoint<PVideoBridgeChild>&& aEndpoint) {
+ LOG("Create video bridge");
+ // We never want to decode in the GPU process, but output
+ // frames to the parent process.
+ MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_RDD ||
+ XRE_GetProcessType() == GeckoProcessType_Utility);
+#ifdef MOZ_WMF_MEDIA_ENGINE
+ MOZ_ASSERT_IF(
+ XRE_GetProcessType() == GeckoProcessType_Utility,
+ GetCurrentSandboxingKind() == SandboxingKind::MF_MEDIA_ENGINE_CDM);
+#endif
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!StartupThreads()) {
+ return false;
+ }
+
+ RefPtr<Runnable> task =
+ NewRunnableFunction("gfx::VideoBridgeChild::Open",
+ &VideoBridgeChild::Open, std::move(aEndpoint));
+ MOZ_ALWAYS_SUCCEEDS(
+ sRemoteDecoderManagerParentThread->Dispatch(task.forget()));
+ return true;
+}
+
+RemoteDecoderManagerParent::RemoteDecoderManagerParent(
+ nsISerialEventTarget* aThread, dom::ContentParentId aContentId)
+ : mThread(aThread), mContentId(aContentId) {
+ MOZ_COUNT_CTOR(RemoteDecoderManagerParent);
+ auto& registrar =
+ XRE_IsGPUProcess() ? GPUParent::GetSingleton()->AsyncShutdownService()
+ : XRE_IsUtilityProcess()
+ ? UtilityProcessChild::GetSingleton()->AsyncShutdownService()
+ : RDDParent::GetSingleton()->AsyncShutdownService();
+ registrar.Register(this);
+}
+
+RemoteDecoderManagerParent::~RemoteDecoderManagerParent() {
+ MOZ_COUNT_DTOR(RemoteDecoderManagerParent);
+ auto& registrar =
+ XRE_IsGPUProcess() ? GPUParent::GetSingleton()->AsyncShutdownService()
+ : XRE_IsUtilityProcess()
+ ? UtilityProcessChild::GetSingleton()->AsyncShutdownService()
+ : RDDParent::GetSingleton()->AsyncShutdownService();
+ registrar.Deregister(this);
+}
+
+void RemoteDecoderManagerParent::ActorDestroy(
+ mozilla::ipc::IProtocol::ActorDestroyReason) {
+ mThread = nullptr;
+}
+
+PRemoteDecoderParent* RemoteDecoderManagerParent::AllocPRemoteDecoderParent(
+ const RemoteDecoderInfoIPDL& aRemoteDecoderInfo,
+ const CreateDecoderParams::OptionSet& aOptions,
+ const Maybe<layers::TextureFactoryIdentifier>& aIdentifier,
+ const Maybe<uint64_t>& aMediaEngineId,
+ const Maybe<TrackingId>& aTrackingId) {
+ RefPtr<TaskQueue> decodeTaskQueue =
+ TaskQueue::Create(GetMediaThreadPool(MediaThreadType::PLATFORM_DECODER),
+ "RemoteVideoDecoderParent::mDecodeTaskQueue");
+
+ if (aRemoteDecoderInfo.type() ==
+ RemoteDecoderInfoIPDL::TVideoDecoderInfoIPDL) {
+ const VideoDecoderInfoIPDL& decoderInfo =
+ aRemoteDecoderInfo.get_VideoDecoderInfoIPDL();
+ return new RemoteVideoDecoderParent(
+ this, decoderInfo.videoInfo(), decoderInfo.framerate(), aOptions,
+ aIdentifier, sRemoteDecoderManagerParentThread, decodeTaskQueue,
+ aMediaEngineId, aTrackingId);
+ }
+
+ if (aRemoteDecoderInfo.type() == RemoteDecoderInfoIPDL::TAudioInfo) {
+ return new RemoteAudioDecoderParent(
+ this, aRemoteDecoderInfo.get_AudioInfo(), aOptions,
+ sRemoteDecoderManagerParentThread, decodeTaskQueue, aMediaEngineId);
+ }
+
+ MOZ_CRASH("unrecognized type of RemoteDecoderInfoIPDL union");
+ return nullptr;
+}
+
+bool RemoteDecoderManagerParent::DeallocPRemoteDecoderParent(
+ PRemoteDecoderParent* actor) {
+ RemoteDecoderParent* parent = static_cast<RemoteDecoderParent*>(actor);
+ parent->Destroy();
+ return true;
+}
+
+PMFMediaEngineParent* RemoteDecoderManagerParent::AllocPMFMediaEngineParent() {
+#ifdef MOZ_WMF_MEDIA_ENGINE
+ return new MFMediaEngineParent(this, sRemoteDecoderManagerParentThread);
+#else
+ return nullptr;
+#endif
+}
+
+bool RemoteDecoderManagerParent::DeallocPMFMediaEngineParent(
+ PMFMediaEngineParent* actor) {
+#ifdef MOZ_WMF_MEDIA_ENGINE
+ MFMediaEngineParent* parent = static_cast<MFMediaEngineParent*>(actor);
+ parent->Destroy();
+#endif
+ return true;
+}
+
+PMFCDMParent* RemoteDecoderManagerParent::AllocPMFCDMParent(
+ const nsAString& aKeySystem) {
+#ifdef MOZ_WMF_CDM
+ return new MFCDMParent(aKeySystem, this, sRemoteDecoderManagerParentThread);
+#else
+ return nullptr;
+#endif
+}
+
+bool RemoteDecoderManagerParent::DeallocPMFCDMParent(PMFCDMParent* actor) {
+#ifdef MOZ_WMF_CDM
+ static_cast<MFCDMParent*>(actor)->Destroy();
+#endif
+ return true;
+}
+
+void RemoteDecoderManagerParent::Open(
+ Endpoint<PRemoteDecoderManagerParent>&& aEndpoint) {
+ if (!aEndpoint.Bind(this)) {
+ // We can't recover from this.
+ MOZ_CRASH("Failed to bind RemoteDecoderManagerParent to endpoint");
+ }
+}
+
+mozilla::ipc::IPCResult RemoteDecoderManagerParent::RecvReadback(
+ const SurfaceDescriptorGPUVideo& aSD, SurfaceDescriptor* aResult) {
+ const SurfaceDescriptorRemoteDecoder& sd = aSD;
+ RefPtr<Image> image = mImageMap[sd.handle()];
+ if (!image) {
+ *aResult = null_t();
+ return IPC_OK();
+ }
+
+ RefPtr<SourceSurface> source = image->GetAsSourceSurface();
+ if (!source) {
+ *aResult = null_t();
+ return IPC_OK();
+ }
+
+ SurfaceFormat format = source->GetFormat();
+ IntSize size = source->GetSize();
+ size_t length = ImageDataSerializer::ComputeRGBBufferSize(size, format);
+
+ Shmem buffer;
+ if (!length || !AllocShmem(length, &buffer)) {
+ *aResult = null_t();
+ return IPC_OK();
+ }
+
+ RefPtr<DrawTarget> dt = Factory::CreateDrawTargetForData(
+ gfx::BackendType::CAIRO, buffer.get<uint8_t>(), size,
+ ImageDataSerializer::ComputeRGBStride(format, size.width), format);
+ if (!dt) {
+ DeallocShmem(buffer);
+ *aResult = null_t();
+ return IPC_OK();
+ }
+
+ dt->CopySurface(source, IntRect(0, 0, size.width, size.height), IntPoint());
+ dt->Flush();
+
+ *aResult = SurfaceDescriptorBuffer(RGBDescriptor(size, format),
+ MemoryOrShmem(std::move(buffer)));
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
+RemoteDecoderManagerParent::RecvDeallocateSurfaceDescriptorGPUVideo(
+ const SurfaceDescriptorGPUVideo& aSD) {
+ MOZ_ASSERT(OnManagerThread());
+ const SurfaceDescriptorRemoteDecoder& sd = aSD;
+ mImageMap.erase(sd.handle());
+ mTextureMap.erase(sd.handle());
+ return IPC_OK();
+}
+
+void RemoteDecoderManagerParent::DeallocateSurfaceDescriptor(
+ const SurfaceDescriptorGPUVideo& aSD) {
+ if (!OnManagerThread()) {
+ MOZ_ALWAYS_SUCCEEDS(
+ sRemoteDecoderManagerParentThread->Dispatch(NS_NewRunnableFunction(
+ "RemoteDecoderManagerParent::DeallocateSurfaceDescriptor",
+ [ref = RefPtr{this}, sd = aSD]() {
+ ref->RecvDeallocateSurfaceDescriptorGPUVideo(sd);
+ })));
+ } else {
+ RecvDeallocateSurfaceDescriptorGPUVideo(aSD);
+ }
+}
+
+#undef LOG
+
+} // namespace mozilla
diff --git a/dom/media/ipc/RemoteDecoderManagerParent.h b/dom/media/ipc/RemoteDecoderManagerParent.h
new file mode 100644
index 0000000000..772eee323e
--- /dev/null
+++ b/dom/media/ipc/RemoteDecoderManagerParent.h
@@ -0,0 +1,101 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef include_dom_media_ipc_RemoteDecoderManagerParent_h
+#define include_dom_media_ipc_RemoteDecoderManagerParent_h
+
+#include "GPUVideoImage.h"
+#include "mozilla/PRemoteDecoderManagerParent.h"
+#include "mozilla/dom/ipc/IdType.h"
+#include "mozilla/layers/VideoBridgeChild.h"
+
+namespace mozilla {
+
+class PDMFactory;
+class PMFCDMParent;
+class PMFMediaEngineParent;
+
+class RemoteDecoderManagerParent final
+ : public PRemoteDecoderManagerParent,
+ public layers::IGPUVideoSurfaceManager {
+ friend class PRemoteDecoderManagerParent;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RemoteDecoderManagerParent, override)
+
+ static bool CreateForContent(
+ Endpoint<PRemoteDecoderManagerParent>&& aEndpoint,
+ dom::ContentParentId aContentId);
+
+ static bool CreateVideoBridgeToOtherProcess(
+ Endpoint<layers::PVideoBridgeChild>&& aEndpoint);
+
+ // Must be called on manager thread.
+ // Store the image so that it can be used out of process. Will be released
+ // when DeallocateSurfaceDescriptor is called.
+ void StoreImage(const SurfaceDescriptorGPUVideo& aSD, layers::Image* aImage,
+ layers::TextureClient* aTexture);
+
+ // IGPUVideoSurfaceManager methods
+ already_AddRefed<gfx::SourceSurface> Readback(
+ const SurfaceDescriptorGPUVideo& aSD) override {
+ MOZ_ASSERT_UNREACHABLE("Not usable from the parent");
+ return nullptr;
+ }
+ void DeallocateSurfaceDescriptor(
+ const SurfaceDescriptorGPUVideo& aSD) override;
+
+ static bool StartupThreads();
+ static void ShutdownThreads();
+
+ static void ShutdownVideoBridge();
+
+ bool OnManagerThread();
+
+ // Can be called from manager thread only
+ PDMFactory& EnsurePDMFactory();
+
+ const dom::ContentParentId& GetContentId() const { return mContentId; }
+
+ protected:
+ PRemoteDecoderParent* AllocPRemoteDecoderParent(
+ const RemoteDecoderInfoIPDL& aRemoteDecoderInfo,
+ const CreateDecoderParams::OptionSet& aOptions,
+ const Maybe<layers::TextureFactoryIdentifier>& aIdentifier,
+ const Maybe<uint64_t>& aMediaEngineId,
+ const Maybe<TrackingId>& aTrackingId);
+ bool DeallocPRemoteDecoderParent(PRemoteDecoderParent* actor);
+
+ PMFMediaEngineParent* AllocPMFMediaEngineParent();
+ bool DeallocPMFMediaEngineParent(PMFMediaEngineParent* actor);
+
+ PMFCDMParent* AllocPMFCDMParent(const nsAString& aKeySystem);
+ bool DeallocPMFCDMParent(PMFCDMParent* actor);
+
+ mozilla::ipc::IPCResult RecvReadback(const SurfaceDescriptorGPUVideo& aSD,
+ SurfaceDescriptor* aResult);
+ mozilla::ipc::IPCResult RecvDeallocateSurfaceDescriptorGPUVideo(
+ const SurfaceDescriptorGPUVideo& aSD);
+
+ void ActorDestroy(mozilla::ipc::IProtocol::ActorDestroyReason) override;
+
+ private:
+ RemoteDecoderManagerParent(nsISerialEventTarget* aThread,
+ dom::ContentParentId aContentId);
+ ~RemoteDecoderManagerParent();
+
+ void Open(Endpoint<PRemoteDecoderManagerParent>&& aEndpoint);
+
+ std::map<uint64_t, RefPtr<layers::Image>> mImageMap;
+ std::map<uint64_t, RefPtr<layers::TextureClient>> mTextureMap;
+
+ nsCOMPtr<nsISerialEventTarget> mThread;
+ RefPtr<PDMFactory> mPDMFactory;
+ dom::ContentParentId mContentId;
+};
+
+} // namespace mozilla
+
+#endif // include_dom_media_ipc_RemoteDecoderManagerParent_h
diff --git a/dom/media/ipc/RemoteDecoderModule.cpp b/dom/media/ipc/RemoteDecoderModule.cpp
new file mode 100644
index 0000000000..2f0d6e1752
--- /dev/null
+++ b/dom/media/ipc/RemoteDecoderModule.cpp
@@ -0,0 +1,87 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "RemoteDecoderModule.h"
+
+#ifdef MOZ_AV1
+# include "AOMDecoder.h"
+#endif
+#include "OpusDecoder.h"
+#include "RemoteAudioDecoder.h"
+#include "RemoteDecoderManagerChild.h"
+#include "RemoteMediaDataDecoder.h"
+#include "RemoteVideoDecoder.h"
+#include "VideoUtils.h"
+#include "VorbisDecoder.h"
+#include "WAVDecoder.h"
+#include "gfxConfig.h"
+#include "mozilla/RemoteDecodeUtils.h"
+
+namespace mozilla {
+
+using namespace ipc;
+using namespace layers;
+
+already_AddRefed<PlatformDecoderModule> RemoteDecoderModule::Create(
+ RemoteDecodeIn aLocation) {
+ MOZ_ASSERT(!XRE_IsGPUProcess() && !XRE_IsRDDProcess(),
+ "Should not be created in GPU or RDD process.");
+ if (!XRE_IsContentProcess()) {
+ // For now, the RemoteDecoderModule is only available in the content
+ // process.
+ return nullptr;
+ }
+ return MakeAndAddRef<RemoteDecoderModule>(aLocation);
+}
+
+RemoteDecoderModule::RemoteDecoderModule(RemoteDecodeIn aLocation)
+ : mLocation(aLocation) {}
+
+media::DecodeSupportSet RemoteDecoderModule::SupportsMimeType(
+ const nsACString& aMimeType, DecoderDoctorDiagnostics* aDiagnostics) const {
+ MOZ_CRASH("Deprecated: Use RemoteDecoderModule::Supports");
+} // namespace mozilla
+
+media::DecodeSupportSet RemoteDecoderModule::Supports(
+ const SupportDecoderParams& aParams,
+ DecoderDoctorDiagnostics* aDiagnostics) const {
+ bool supports =
+ RemoteDecoderManagerChild::Supports(mLocation, aParams, aDiagnostics);
+ // This should only be supported by mf media engine cdm process.
+ if (aParams.mMediaEngineId &&
+ mLocation != RemoteDecodeIn::UtilityProcess_MFMediaEngineCDM) {
+ supports = false;
+ }
+ MOZ_LOG(sPDMLog, LogLevel::Debug,
+ ("Sandbox %s decoder %s requested type %s",
+ RemoteDecodeInToStr(mLocation), supports ? "supports" : "rejects",
+ aParams.MimeType().get()));
+ if (supports) {
+ // TODO: Note that we do not yet distinguish between SW/HW decode support.
+ // Will be done in bug 1754239.
+ return media::DecodeSupport::SoftwareDecode;
+ }
+ return media::DecodeSupport::Unsupported;
+}
+
+RefPtr<RemoteDecoderModule::CreateDecoderPromise>
+RemoteDecoderModule::AsyncCreateDecoder(const CreateDecoderParams& aParams) {
+ if (aParams.mConfig.IsAudio()) {
+ // OpusDataDecoder will check this option to provide the same info
+ // that IsDefaultPlaybackDeviceMono provides. We want to avoid calls
+ // to IsDefaultPlaybackDeviceMono on RDD because initializing audio
+ // backends on RDD will be blocked by the sandbox.
+ if (OpusDataDecoder::IsOpus(aParams.mConfig.mMimeType) &&
+ IsDefaultPlaybackDeviceMono()) {
+ CreateDecoderParams params = aParams;
+ params.mOptions += CreateDecoderParams::Option::DefaultPlaybackDeviceMono;
+ return RemoteDecoderManagerChild::CreateAudioDecoder(params, mLocation);
+ }
+ return RemoteDecoderManagerChild::CreateAudioDecoder(aParams, mLocation);
+ }
+ return RemoteDecoderManagerChild::CreateVideoDecoder(aParams, mLocation);
+}
+
+} // namespace mozilla
diff --git a/dom/media/ipc/RemoteDecoderModule.h b/dom/media/ipc/RemoteDecoderModule.h
new file mode 100644
index 0000000000..a11f73227e
--- /dev/null
+++ b/dom/media/ipc/RemoteDecoderModule.h
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef include_dom_media_ipc_RemoteDecoderModule_h
+#define include_dom_media_ipc_RemoteDecoderModule_h
+#include "PlatformDecoderModule.h"
+
+namespace mozilla {
+
+enum class RemoteDecodeIn;
+
+// A decoder module that proxies decoding to either GPU or RDD process.
+class RemoteDecoderModule : public PlatformDecoderModule {
+ template <typename T, typename... Args>
+ friend already_AddRefed<T> MakeAndAddRef(Args&&...);
+
+ public:
+ static already_AddRefed<PlatformDecoderModule> Create(
+ RemoteDecodeIn aLocation);
+
+ media::DecodeSupportSet SupportsMimeType(
+ const nsACString& aMimeType,
+ DecoderDoctorDiagnostics* aDiagnostics) const override;
+
+ media::DecodeSupportSet Supports(
+ const SupportDecoderParams& aParams,
+ DecoderDoctorDiagnostics* aDiagnostics) const override;
+
+ RefPtr<CreateDecoderPromise> AsyncCreateDecoder(
+ const CreateDecoderParams& aParams) override;
+
+ already_AddRefed<MediaDataDecoder> CreateVideoDecoder(
+ const CreateDecoderParams& aParams) override {
+ MOZ_CRASH("Not available");
+ }
+
+ already_AddRefed<MediaDataDecoder> CreateAudioDecoder(
+ const CreateDecoderParams& aParams) override {
+ MOZ_CRASH("Not available");
+ }
+
+ private:
+ explicit RemoteDecoderModule(RemoteDecodeIn aLocation);
+
+ const RemoteDecodeIn mLocation;
+};
+
+} // namespace mozilla
+
+#endif // include_dom_media_ipc_RemoteDecoderModule_h
diff --git a/dom/media/ipc/RemoteDecoderParent.cpp b/dom/media/ipc/RemoteDecoderParent.cpp
new file mode 100644
index 0000000000..2ca9eaa2d7
--- /dev/null
+++ b/dom/media/ipc/RemoteDecoderParent.cpp
@@ -0,0 +1,228 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "RemoteDecoderParent.h"
+
+#include "RemoteDecoderManagerParent.h"
+#include "mozilla/Unused.h"
+
+namespace mozilla {
+
+RemoteDecoderParent::RemoteDecoderParent(
+ RemoteDecoderManagerParent* aParent,
+ const CreateDecoderParams::OptionSet& aOptions,
+ nsISerialEventTarget* aManagerThread, TaskQueue* aDecodeTaskQueue,
+ const Maybe<uint64_t>& aMediaEngineId, Maybe<TrackingId> aTrackingId)
+ : ShmemRecycleAllocator(this),
+ mParent(aParent),
+ mOptions(aOptions),
+ mDecodeTaskQueue(aDecodeTaskQueue),
+ mTrackingId(aTrackingId),
+ mMediaEngineId(aMediaEngineId),
+ mManagerThread(aManagerThread) {
+ MOZ_COUNT_CTOR(RemoteDecoderParent);
+ MOZ_ASSERT(OnManagerThread());
+ // We hold a reference to ourselves to keep us alive until IPDL
+ // explictly destroys us. There may still be refs held by
+ // tasks, but no new ones should be added after we're
+ // destroyed.
+ mIPDLSelfRef = this;
+}
+
+RemoteDecoderParent::~RemoteDecoderParent() {
+ MOZ_COUNT_DTOR(RemoteDecoderParent);
+}
+
+void RemoteDecoderParent::Destroy() {
+ MOZ_ASSERT(OnManagerThread());
+ mIPDLSelfRef = nullptr;
+}
+
+mozilla::ipc::IPCResult RemoteDecoderParent::RecvInit(
+ InitResolver&& aResolver) {
+ MOZ_ASSERT(OnManagerThread());
+ RefPtr<RemoteDecoderParent> self = this;
+ mDecoder->Init()->Then(
+ mManagerThread, __func__,
+ [self, resolver = std::move(aResolver)](
+ MediaDataDecoder::InitPromise::ResolveOrRejectValue&& aValue) {
+ if (!self->CanRecv()) {
+ // The promise to the child would have already been rejected.
+ return;
+ }
+ if (aValue.IsReject()) {
+ resolver(aValue.RejectValue());
+ return;
+ }
+ auto track = aValue.ResolveValue();
+ MOZ_ASSERT(track == TrackInfo::kAudioTrack ||
+ track == TrackInfo::kVideoTrack);
+ if (self->mDecoder) {
+ nsCString hardwareReason;
+ bool hardwareAccelerated =
+ self->mDecoder->IsHardwareAccelerated(hardwareReason);
+ resolver(InitCompletionIPDL{
+ track, self->mDecoder->GetDescriptionName(),
+ self->mDecoder->GetProcessName(), self->mDecoder->GetCodecName(),
+ hardwareAccelerated, hardwareReason,
+ self->mDecoder->NeedsConversion()});
+ }
+ });
+ return IPC_OK();
+}
+
+void RemoteDecoderParent::DecodeNextSample(
+ const RefPtr<ArrayOfRemoteMediaRawData>& aData, size_t aIndex,
+ MediaDataDecoder::DecodedData&& aOutput, DecodeResolver&& aResolver) {
+ MOZ_ASSERT(OnManagerThread());
+
+ if (!CanRecv()) {
+ // Avoid unnecessarily creating shmem objects later.
+ return;
+ }
+
+ if (!mDecoder) {
+ // We got shutdown or the child got destroyed.
+ aResolver(MediaResult(NS_ERROR_ABORT, __func__));
+ return;
+ }
+
+ if (aData->Count() == aIndex) {
+ DecodedOutputIPDL result;
+ MediaResult rv = ProcessDecodedData(std::move(aOutput), result);
+ if (NS_FAILED(rv)) {
+ aResolver(std::move(rv)); // Out of Memory.
+ } else {
+ aResolver(std::move(result));
+ }
+ return;
+ }
+
+ RefPtr<MediaRawData> rawData = aData->ElementAt(aIndex);
+ if (!rawData) {
+ // OOM
+ aResolver(MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__));
+ return;
+ }
+
+ mDecoder->Decode(rawData)->Then(
+ mManagerThread, __func__,
+ [self = RefPtr{this}, this, aData, aIndex, output = std::move(aOutput),
+ resolver = std::move(aResolver)](
+ MediaDataDecoder::DecodePromise::ResolveOrRejectValue&&
+ aValue) mutable {
+ if (aValue.IsReject()) {
+ resolver(aValue.RejectValue());
+ return;
+ }
+
+ output.AppendElements(std::move(aValue.ResolveValue()));
+
+ // Call again in case we have more data to decode.
+ DecodeNextSample(aData, aIndex + 1, std::move(output),
+ std::move(resolver));
+ });
+}
+
+mozilla::ipc::IPCResult RemoteDecoderParent::RecvDecode(
+ ArrayOfRemoteMediaRawData* aData, DecodeResolver&& aResolver) {
+ MOZ_ASSERT(OnManagerThread());
+ // If we are here, we know all previously returned DecodedOutputIPDL got
+ // used by the child. We can mark all previously sent ShmemBuffer as
+ // available again.
+ ReleaseAllBuffers();
+ MediaDataDecoder::DecodedData output;
+ DecodeNextSample(aData, 0, std::move(output), std::move(aResolver));
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult RemoteDecoderParent::RecvFlush(
+ FlushResolver&& aResolver) {
+ MOZ_ASSERT(OnManagerThread());
+ RefPtr<RemoteDecoderParent> self = this;
+ mDecoder->Flush()->Then(
+ mManagerThread, __func__,
+ [self, resolver = std::move(aResolver)](
+ MediaDataDecoder::FlushPromise::ResolveOrRejectValue&& aValue) {
+ self->ReleaseAllBuffers();
+ if (aValue.IsReject()) {
+ resolver(aValue.RejectValue());
+ } else {
+ resolver(MediaResult(NS_OK));
+ }
+ });
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult RemoteDecoderParent::RecvDrain(
+ DrainResolver&& aResolver) {
+ MOZ_ASSERT(OnManagerThread());
+ RefPtr<RemoteDecoderParent> self = this;
+ mDecoder->Drain()->Then(
+ mManagerThread, __func__,
+ [self, this, resolver = std::move(aResolver)](
+ MediaDataDecoder::DecodePromise::ResolveOrRejectValue&& aValue) {
+ ReleaseAllBuffers();
+ if (!self->CanRecv()) {
+ // Avoid unnecessarily creating shmem objects later.
+ return;
+ }
+ if (aValue.IsReject()) {
+ resolver(aValue.RejectValue());
+ return;
+ }
+ DecodedOutputIPDL output;
+ MediaResult rv =
+ ProcessDecodedData(std::move(aValue.ResolveValue()), output);
+ if (NS_FAILED(rv)) {
+ resolver(rv);
+ } else {
+ resolver(std::move(output));
+ }
+ });
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult RemoteDecoderParent::RecvShutdown(
+ ShutdownResolver&& aResolver) {
+ MOZ_ASSERT(OnManagerThread());
+ if (mDecoder) {
+ RefPtr<RemoteDecoderParent> self = this;
+ mDecoder->Shutdown()->Then(
+ mManagerThread, __func__,
+ [self, resolver = std::move(aResolver)](
+ const ShutdownPromise::ResolveOrRejectValue& aValue) {
+ MOZ_ASSERT(aValue.IsResolve());
+ self->ReleaseAllBuffers();
+ resolver(true);
+ });
+ }
+ mDecoder = nullptr;
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult RemoteDecoderParent::RecvSetSeekThreshold(
+ const TimeUnit& aTime) {
+ MOZ_ASSERT(OnManagerThread());
+ mDecoder->SetSeekThreshold(aTime);
+ return IPC_OK();
+}
+
+void RemoteDecoderParent::ActorDestroy(ActorDestroyReason aWhy) {
+ MOZ_ASSERT(OnManagerThread());
+ if (mDecoder) {
+ mDecoder->Shutdown();
+ mDecoder = nullptr;
+ }
+ CleanupShmemRecycleAllocator();
+}
+
+bool RemoteDecoderParent::OnManagerThread() {
+ return mParent->OnManagerThread();
+}
+
+} // namespace mozilla
diff --git a/dom/media/ipc/RemoteDecoderParent.h b/dom/media/ipc/RemoteDecoderParent.h
new file mode 100644
index 0000000000..002a93fa09
--- /dev/null
+++ b/dom/media/ipc/RemoteDecoderParent.h
@@ -0,0 +1,74 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef include_dom_media_ipc_RemoteDecoderParent_h
+#define include_dom_media_ipc_RemoteDecoderParent_h
+
+#include "mozilla/PRemoteDecoderParent.h"
+#include "mozilla/ShmemRecycleAllocator.h"
+
+namespace mozilla {
+
+class RemoteDecoderManagerParent;
+using mozilla::ipc::IPCResult;
+
+class RemoteDecoderParent : public ShmemRecycleAllocator<RemoteDecoderParent>,
+ public PRemoteDecoderParent {
+ friend class PRemoteDecoderParent;
+
+ public:
+ // We refcount this class since the task queue can have runnables
+ // that reference us.
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RemoteDecoderParent)
+
+ RemoteDecoderParent(RemoteDecoderManagerParent* aParent,
+ const CreateDecoderParams::OptionSet& aOptions,
+ nsISerialEventTarget* aManagerThread,
+ TaskQueue* aDecodeTaskQueue,
+ const Maybe<uint64_t>& aMediaEngineId,
+ Maybe<TrackingId> aTrackingId);
+
+ void Destroy();
+
+ // PRemoteDecoderParent
+ virtual IPCResult RecvConstruct(ConstructResolver&& aResolver) = 0;
+ IPCResult RecvInit(InitResolver&& aResolver);
+ IPCResult RecvDecode(ArrayOfRemoteMediaRawData* aData,
+ DecodeResolver&& aResolver);
+ IPCResult RecvFlush(FlushResolver&& aResolver);
+ IPCResult RecvDrain(DrainResolver&& aResolver);
+ IPCResult RecvShutdown(ShutdownResolver&& aResolver);
+ IPCResult RecvSetSeekThreshold(const media::TimeUnit& aTime);
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ protected:
+ virtual ~RemoteDecoderParent();
+
+ bool OnManagerThread();
+
+ virtual MediaResult ProcessDecodedData(MediaDataDecoder::DecodedData&& aData,
+ DecodedOutputIPDL& aDecodedData) = 0;
+
+ const RefPtr<RemoteDecoderManagerParent> mParent;
+ const CreateDecoderParams::OptionSet mOptions;
+ const RefPtr<TaskQueue> mDecodeTaskQueue;
+ RefPtr<MediaDataDecoder> mDecoder;
+ const Maybe<TrackingId> mTrackingId;
+
+ // Only be used on Windows when the media engine playback is enabled.
+ const Maybe<uint64_t> mMediaEngineId;
+
+ private:
+ void DecodeNextSample(const RefPtr<ArrayOfRemoteMediaRawData>& aData,
+ size_t aIndex, MediaDataDecoder::DecodedData&& aOutput,
+ DecodeResolver&& aResolver);
+ RefPtr<RemoteDecoderParent> mIPDLSelfRef;
+ const RefPtr<nsISerialEventTarget> mManagerThread;
+};
+
+} // namespace mozilla
+
+#endif // include_dom_media_ipc_RemoteDecoderParent_h
diff --git a/dom/media/ipc/RemoteImageHolder.cpp b/dom/media/ipc/RemoteImageHolder.cpp
new file mode 100644
index 0000000000..553e41fef6
--- /dev/null
+++ b/dom/media/ipc/RemoteImageHolder.cpp
@@ -0,0 +1,172 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "RemoteImageHolder.h"
+
+#include "GPUVideoImage.h"
+#include "mozilla/PRemoteDecoderChild.h"
+#include "mozilla/RemoteDecodeUtils.h"
+#include "mozilla/RemoteDecoderManagerChild.h"
+#include "mozilla/layers/ImageDataSerializer.h"
+#include "mozilla/layers/VideoBridgeUtils.h"
+
+namespace mozilla {
+
+using namespace gfx;
+using namespace layers;
+
+RemoteImageHolder::RemoteImageHolder() = default;
+RemoteImageHolder::RemoteImageHolder(layers::IGPUVideoSurfaceManager* aManager,
+ layers::VideoBridgeSource aSource,
+ const gfx::IntSize& aSize,
+ const gfx::ColorDepth& aColorDepth,
+ const layers::SurfaceDescriptor& aSD)
+ : mSource(aSource),
+ mSize(aSize),
+ mColorDepth(aColorDepth),
+ mSD(Some(aSD)),
+ mManager(aManager) {}
+RemoteImageHolder::RemoteImageHolder(RemoteImageHolder&& aOther)
+ : mSource(aOther.mSource),
+ mSize(aOther.mSize),
+ mColorDepth(aOther.mColorDepth),
+ mSD(std::move(aOther.mSD)),
+ mManager(aOther.mManager) {
+ aOther.mSD = Nothing();
+}
+
+already_AddRefed<Image> RemoteImageHolder::DeserializeImage(
+ layers::BufferRecycleBin* aBufferRecycleBin) {
+ MOZ_ASSERT(mSD && mSD->type() == SurfaceDescriptor::TSurfaceDescriptorBuffer);
+ const SurfaceDescriptorBuffer& sdBuffer = mSD->get_SurfaceDescriptorBuffer();
+ MOZ_ASSERT(sdBuffer.desc().type() == BufferDescriptor::TYCbCrDescriptor);
+ if (sdBuffer.desc().type() != BufferDescriptor::TYCbCrDescriptor ||
+ !aBufferRecycleBin) {
+ return nullptr;
+ }
+ const YCbCrDescriptor& descriptor = sdBuffer.desc().get_YCbCrDescriptor();
+
+ uint8_t* buffer = nullptr;
+ const MemoryOrShmem& memOrShmem = sdBuffer.data();
+ switch (memOrShmem.type()) {
+ case MemoryOrShmem::Tuintptr_t:
+ buffer = reinterpret_cast<uint8_t*>(memOrShmem.get_uintptr_t());
+ break;
+ case MemoryOrShmem::TShmem:
+ buffer = memOrShmem.get_Shmem().get<uint8_t>();
+ break;
+ default:
+ MOZ_ASSERT(false, "Unknown MemoryOrShmem type");
+ }
+ if (!buffer) {
+ return nullptr;
+ }
+
+ PlanarYCbCrData pData;
+ pData.mYStride = descriptor.yStride();
+ pData.mCbCrStride = descriptor.cbCrStride();
+ // default mYSkip, mCbSkip, mCrSkip because not held in YCbCrDescriptor
+ pData.mYSkip = pData.mCbSkip = pData.mCrSkip = 0;
+ pData.mPictureRect = descriptor.display();
+ pData.mStereoMode = descriptor.stereoMode();
+ pData.mColorDepth = descriptor.colorDepth();
+ pData.mYUVColorSpace = descriptor.yUVColorSpace();
+ pData.mColorRange = descriptor.colorRange();
+ pData.mChromaSubsampling = descriptor.chromaSubsampling();
+ pData.mYChannel = ImageDataSerializer::GetYChannel(buffer, descriptor);
+ pData.mCbChannel = ImageDataSerializer::GetCbChannel(buffer, descriptor);
+ pData.mCrChannel = ImageDataSerializer::GetCrChannel(buffer, descriptor);
+
+ // images coming from AOMDecoder are RecyclingPlanarYCbCrImages.
+ RefPtr<RecyclingPlanarYCbCrImage> image =
+ new RecyclingPlanarYCbCrImage(aBufferRecycleBin);
+ bool setData = image->CopyData(pData);
+ MOZ_ASSERT(setData);
+
+ switch (memOrShmem.type()) {
+ case MemoryOrShmem::Tuintptr_t:
+ delete[] reinterpret_cast<uint8_t*>(memOrShmem.get_uintptr_t());
+ break;
+ case MemoryOrShmem::TShmem:
+ // Memory buffer will be recycled by the parent automatically.
+ break;
+ default:
+ MOZ_ASSERT(false, "Unknown MemoryOrShmem type");
+ }
+
+ if (!setData) {
+ return nullptr;
+ }
+
+ return image.forget();
+}
+
+already_AddRefed<layers::Image> RemoteImageHolder::TransferToImage(
+ layers::BufferRecycleBin* aBufferRecycleBin) {
+ if (IsEmpty()) {
+ return nullptr;
+ }
+ RefPtr<Image> image;
+ if (mSD->type() == SurfaceDescriptor::TSurfaceDescriptorBuffer) {
+ image = DeserializeImage(aBufferRecycleBin);
+ } else {
+ // The Image here creates a TextureData object that takes ownership
+ // of the SurfaceDescriptor, and is responsible for making sure that
+ // it gets deallocated.
+ SurfaceDescriptorRemoteDecoder remoteSD =
+ static_cast<const SurfaceDescriptorGPUVideo&>(*mSD);
+ remoteSD.source() = Some(mSource);
+ image = new GPUVideoImage(mManager, remoteSD, mSize, mColorDepth);
+ }
+ mSD = Nothing();
+ mManager = nullptr;
+
+ return image.forget();
+}
+
+RemoteImageHolder::~RemoteImageHolder() {
+ // GPU Images are held by the RemoteDecoderManagerParent, we didn't get to use
+ // this image holder (the decoder could have been flushed). We don't need to
+ // worry about Shmem based image as the Shmem will be automatically re-used
+ // once the decoder is used again.
+ if (!IsEmpty() && mManager &&
+ mSD->type() != SurfaceDescriptor::TSurfaceDescriptorBuffer) {
+ SurfaceDescriptorRemoteDecoder remoteSD =
+ static_cast<const SurfaceDescriptorGPUVideo&>(*mSD);
+ mManager->DeallocateSurfaceDescriptor(remoteSD);
+ }
+}
+
+/* static */ void ipc::IPDLParamTraits<RemoteImageHolder>::Write(
+ IPC::MessageWriter* aWriter, ipc::IProtocol* aActor,
+ RemoteImageHolder&& aParam) {
+ WriteIPDLParam(aWriter, aActor, aParam.mSource);
+ WriteIPDLParam(aWriter, aActor, aParam.mSize);
+ WriteIPDLParam(aWriter, aActor, aParam.mColorDepth);
+ WriteIPDLParam(aWriter, aActor, aParam.mSD);
+ // Empty this holder.
+ aParam.mSD = Nothing();
+ aParam.mManager = nullptr;
+}
+
+/* static */ bool ipc::IPDLParamTraits<RemoteImageHolder>::Read(
+ IPC::MessageReader* aReader, ipc::IProtocol* aActor,
+ RemoteImageHolder* aResult) {
+ if (!ReadIPDLParam(aReader, aActor, &aResult->mSource) ||
+ !ReadIPDLParam(aReader, aActor, &aResult->mSize) ||
+ !ReadIPDLParam(aReader, aActor, &aResult->mColorDepth) ||
+ !ReadIPDLParam(aReader, aActor, &aResult->mSD)) {
+ return false;
+ }
+
+ if (!aResult->IsEmpty()) {
+ aResult->mManager = RemoteDecoderManagerChild::GetSingleton(
+ GetRemoteDecodeInFromVideoBridgeSource(aResult->mSource));
+ }
+ return true;
+}
+
+} // namespace mozilla
diff --git a/dom/media/ipc/RemoteImageHolder.h b/dom/media/ipc/RemoteImageHolder.h
new file mode 100644
index 0000000000..b83293d0ed
--- /dev/null
+++ b/dom/media/ipc/RemoteImageHolder.h
@@ -0,0 +1,66 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_media_RemoteImageHolder_h
+#define mozilla_dom_media_RemoteImageHolder_h
+
+#include "MediaData.h"
+#include "ipc/IPCMessageUtils.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/layers/LayersSurfaces.h"
+#include "mozilla/layers/VideoBridgeUtils.h"
+
+namespace mozilla {
+namespace layers {
+class BufferRecycleBin;
+class IGPUVideoSurfaceManager;
+class SurfaceDescriptor;
+} // namespace layers
+class RemoteImageHolder final {
+ friend struct ipc::IPDLParamTraits<RemoteImageHolder>;
+
+ public:
+ RemoteImageHolder();
+ RemoteImageHolder(layers::IGPUVideoSurfaceManager* aManager,
+ layers::VideoBridgeSource aSource,
+ const gfx::IntSize& aSize,
+ const gfx::ColorDepth& aColorDepth,
+ const layers::SurfaceDescriptor& aSD);
+ RemoteImageHolder(RemoteImageHolder&& aOther);
+ // Ensure we never copy this object.
+ RemoteImageHolder(const RemoteImageHolder& aOther) = delete;
+ RemoteImageHolder& operator=(const RemoteImageHolder& aOther) = delete;
+ ~RemoteImageHolder();
+
+ bool IsEmpty() const { return mSD.isNothing(); }
+ // Move content of RemoteImageHolder into a usable Image. Ownership is
+ // transfered to that Image.
+ already_AddRefed<layers::Image> TransferToImage(
+ layers::BufferRecycleBin* aBufferRecycleBin = nullptr);
+
+ private:
+ already_AddRefed<layers::Image> DeserializeImage(
+ layers::BufferRecycleBin* aBufferRecycleBin);
+ // We need a default for the default constructor, never used in practice.
+ layers::VideoBridgeSource mSource = layers::VideoBridgeSource::GpuProcess;
+ gfx::IntSize mSize;
+ gfx::ColorDepth mColorDepth = gfx::ColorDepth::COLOR_8;
+ Maybe<layers::SurfaceDescriptor> mSD;
+ RefPtr<layers::IGPUVideoSurfaceManager> mManager;
+};
+
+template <>
+struct ipc::IPDLParamTraits<RemoteImageHolder> {
+ static void Write(IPC::MessageWriter* aWriter, IProtocol* aActor,
+ RemoteImageHolder&& aParam);
+ static bool Read(IPC::MessageReader* aReader, IProtocol* aActor,
+ RemoteImageHolder* aResult);
+};
+
+} // namespace mozilla
+
+#endif // mozilla_dom_media_RemoteImageHolder_h
diff --git a/dom/media/ipc/RemoteMediaData.cpp b/dom/media/ipc/RemoteMediaData.cpp
new file mode 100644
index 0000000000..bd323b7333
--- /dev/null
+++ b/dom/media/ipc/RemoteMediaData.cpp
@@ -0,0 +1,374 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "RemoteMediaData.h"
+
+#include "PerformanceRecorder.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/dom/MediaIPCUtils.h"
+#include "mozilla/ipc/Shmem.h"
+
+namespace mozilla {
+
+bool RemoteArrayOfByteBuffer::AllocateShmem(
+ size_t aSize, std::function<ShmemBuffer(size_t)>& aAllocator) {
+ ShmemBuffer buffer = aAllocator(aSize);
+ if (!buffer.Valid()) {
+ return false;
+ }
+ mBuffers.emplace(std::move(buffer.Get()));
+ return true;
+}
+
+uint8_t* RemoteArrayOfByteBuffer::BuffersStartAddress() const {
+ MOZ_ASSERT(mBuffers);
+ return mBuffers->get<uint8_t>();
+}
+
+bool RemoteArrayOfByteBuffer::Check(size_t aOffset, size_t aSizeInBytes) const {
+ return mBuffers && mBuffers->IsReadable() &&
+ detail::IsAddValid(aOffset, aSizeInBytes) &&
+ aOffset + aSizeInBytes <= mBuffers->Size<uint8_t>();
+}
+
+void RemoteArrayOfByteBuffer::Write(size_t aOffset, const void* aSourceAddr,
+ size_t aSizeInBytes) {
+ if (!aSizeInBytes) {
+ return;
+ }
+ MOZ_DIAGNOSTIC_ASSERT(Check(aOffset, aSizeInBytes),
+ "Allocated Shmem is too small");
+ memcpy(BuffersStartAddress() + aOffset, aSourceAddr, aSizeInBytes);
+}
+
+RemoteArrayOfByteBuffer::RemoteArrayOfByteBuffer() = default;
+
+RemoteArrayOfByteBuffer::RemoteArrayOfByteBuffer(
+ const nsTArray<RefPtr<MediaByteBuffer>>& aArray,
+ std::function<ShmemBuffer(size_t)>& aAllocator) {
+ // Determine the total size we will need for this object.
+ size_t totalSize = 0;
+ for (const auto& buffer : aArray) {
+ if (buffer) {
+ totalSize += buffer->Length();
+ }
+ }
+ if (totalSize) {
+ if (!AllocateShmem(totalSize, aAllocator)) {
+ return;
+ }
+ }
+ size_t offset = 0;
+ for (const auto& buffer : aArray) {
+ size_t sizeBuffer = buffer ? buffer->Length() : 0;
+ if (totalSize && sizeBuffer) {
+ Write(offset, buffer->Elements(), sizeBuffer);
+ }
+ mOffsets.AppendElement(OffsetEntry{offset, sizeBuffer});
+ offset += sizeBuffer;
+ }
+ mIsValid = true;
+}
+
+RemoteArrayOfByteBuffer& RemoteArrayOfByteBuffer::operator=(
+ RemoteArrayOfByteBuffer&& aOther) noexcept {
+ mIsValid = aOther.mIsValid;
+ mBuffers = std::move(aOther.mBuffers);
+ mOffsets = std::move(aOther.mOffsets);
+ aOther.mIsValid = false;
+ return *this;
+}
+
+RemoteArrayOfByteBuffer::~RemoteArrayOfByteBuffer() = default;
+
+already_AddRefed<MediaByteBuffer> RemoteArrayOfByteBuffer::MediaByteBufferAt(
+ size_t aIndex) const {
+ MOZ_ASSERT(aIndex < Count());
+ const OffsetEntry& entry = mOffsets[aIndex];
+ if (!mBuffers || !std::get<1>(entry)) {
+ // It's an empty one.
+ return nullptr;
+ }
+ size_t entrySize = std::get<1>(entry);
+ if (!Check(std::get<0>(entry), entrySize)) {
+ // This Shmem is corrupted and can't contain the data we are about to
+ // retrieve. We return an empty array instead of asserting to allow for
+ // recovery.
+ return nullptr;
+ }
+ RefPtr<MediaByteBuffer> buffer = new MediaByteBuffer(entrySize);
+ buffer->SetLength(entrySize);
+ memcpy(buffer->Elements(), mBuffers->get<uint8_t>() + std::get<0>(entry),
+ entrySize);
+ return buffer.forget();
+}
+
+/*static */ void ipc::IPDLParamTraits<RemoteArrayOfByteBuffer>::Write(
+ IPC::MessageWriter* aWriter, ipc::IProtocol* aActor,
+ const RemoteArrayOfByteBuffer& aVar) {
+ WriteIPDLParam(aWriter, aActor, aVar.mIsValid);
+ // We need the following gymnastic as the Shmem transfered over IPC will be
+ // revoked. We must create a temporary one instead so that it can be recycled
+ // later back into the original ShmemPool.
+ if (aVar.mBuffers) {
+ WriteIPDLParam(aWriter, aActor, Some(ipc::Shmem(*aVar.mBuffers)));
+ } else {
+ WriteIPDLParam(aWriter, aActor, Maybe<ipc::Shmem>());
+ }
+ WriteIPDLParam(aWriter, aActor, aVar.mOffsets);
+}
+
+/* static */ bool ipc::IPDLParamTraits<RemoteArrayOfByteBuffer>::Read(
+ IPC::MessageReader* aReader, mozilla::ipc::IProtocol* aActor,
+ RemoteArrayOfByteBuffer* aVar) {
+ return ReadIPDLParam(aReader, aActor, &aVar->mIsValid) &&
+ ReadIPDLParam(aReader, aActor, &aVar->mBuffers) &&
+ ReadIPDLParam(aReader, aActor, &aVar->mOffsets);
+}
+
+bool ArrayOfRemoteMediaRawData::Fill(
+ const nsTArray<RefPtr<MediaRawData>>& aData,
+ std::function<ShmemBuffer(size_t)>&& aAllocator) {
+ nsTArray<AlignedByteBuffer> dataBuffers(aData.Length());
+ nsTArray<AlignedByteBuffer> alphaBuffers(aData.Length());
+ nsTArray<RefPtr<MediaByteBuffer>> extraDataBuffers(aData.Length());
+ int32_t height = 0;
+ for (auto&& entry : aData) {
+ dataBuffers.AppendElement(std::move(entry->mBuffer));
+ alphaBuffers.AppendElement(std::move(entry->mAlphaBuffer));
+ extraDataBuffers.AppendElement(std::move(entry->mExtraData));
+ if (auto&& info = entry->mTrackInfo; info && info->GetAsVideoInfo()) {
+ height = info->GetAsVideoInfo()->mImage.height;
+ }
+ mSamples.AppendElement(RemoteMediaRawData{
+ MediaDataIPDL(entry->mOffset, entry->mTime, entry->mTimecode,
+ entry->mDuration, entry->mKeyframe),
+ entry->mEOS, height, entry->mDiscardPadding,
+ entry->mOriginalPresentationWindow,
+ entry->mCrypto.IsEncrypted() && entry->mShouldCopyCryptoToRemoteRawData
+ ? Some(CryptoInfo{
+ entry->mCrypto.mCryptoScheme,
+ entry->mCrypto.mIV,
+ entry->mCrypto.mKeyId,
+ entry->mCrypto.mPlainSizes,
+ entry->mCrypto.mEncryptedSizes,
+ })
+ : Nothing()});
+ }
+ PerformanceRecorder<PlaybackStage> perfRecorder(MediaStage::CopyDemuxedData,
+ height);
+ mBuffers = RemoteArrayOfByteBuffer(dataBuffers, aAllocator);
+ if (!mBuffers.IsValid()) {
+ return false;
+ }
+ mAlphaBuffers = RemoteArrayOfByteBuffer(alphaBuffers, aAllocator);
+ if (!mAlphaBuffers.IsValid()) {
+ return false;
+ }
+ mExtraDatas = RemoteArrayOfByteBuffer(extraDataBuffers, aAllocator);
+ if (!mExtraDatas.IsValid()) {
+ return false;
+ }
+ perfRecorder.Record();
+ return true;
+}
+
+already_AddRefed<MediaRawData> ArrayOfRemoteMediaRawData::ElementAt(
+ size_t aIndex) const {
+ if (!IsValid()) {
+ return nullptr;
+ }
+ MOZ_ASSERT(aIndex < Count());
+ MOZ_DIAGNOSTIC_ASSERT(mBuffers.Count() == Count() &&
+ mAlphaBuffers.Count() == Count() &&
+ mExtraDatas.Count() == Count(),
+ "Something ain't right here");
+ const auto& sample = mSamples[aIndex];
+ PerformanceRecorder<PlaybackStage> perfRecorder(MediaStage::CopyDemuxedData,
+ sample.mHeight);
+ AlignedByteBuffer data = mBuffers.AlignedBufferAt<uint8_t>(aIndex);
+ if (mBuffers.SizeAt(aIndex) && !data) {
+ // OOM
+ return nullptr;
+ }
+ AlignedByteBuffer alphaData = mAlphaBuffers.AlignedBufferAt<uint8_t>(aIndex);
+ if (mAlphaBuffers.SizeAt(aIndex) && !alphaData) {
+ // OOM
+ return nullptr;
+ }
+ RefPtr<MediaRawData> rawData;
+ if (mAlphaBuffers.SizeAt(aIndex)) {
+ rawData = new MediaRawData(std::move(data), std::move(alphaData));
+ } else {
+ rawData = new MediaRawData(std::move(data));
+ }
+ rawData->mOffset = sample.mBase.offset();
+ rawData->mTime = sample.mBase.time();
+ rawData->mTimecode = sample.mBase.timecode();
+ rawData->mDuration = sample.mBase.duration();
+ rawData->mKeyframe = sample.mBase.keyframe();
+ rawData->mEOS = sample.mEOS;
+ rawData->mDiscardPadding = sample.mDiscardPadding;
+ rawData->mExtraData = mExtraDatas.MediaByteBufferAt(aIndex);
+ if (sample.mCryptoConfig) {
+ CryptoSample& cypto = rawData->GetWritableCrypto();
+ cypto.mCryptoScheme = sample.mCryptoConfig->mEncryptionScheme();
+ cypto.mIV = std::move(sample.mCryptoConfig->mIV());
+ cypto.mIVSize = cypto.mIV.Length();
+ cypto.mKeyId = std::move(sample.mCryptoConfig->mKeyId());
+ cypto.mPlainSizes = std::move(sample.mCryptoConfig->mClearBytes());
+ cypto.mEncryptedSizes = std::move(sample.mCryptoConfig->mCipherBytes());
+ }
+ perfRecorder.Record();
+ return rawData.forget();
+}
+
+/*static */ void ipc::IPDLParamTraits<ArrayOfRemoteMediaRawData*>::Write(
+ IPC::MessageWriter* aWriter, ipc::IProtocol* aActor,
+ ArrayOfRemoteMediaRawData* aVar) {
+ WriteIPDLParam(aWriter, aActor, std::move(aVar->mSamples));
+ WriteIPDLParam(aWriter, aActor, std::move(aVar->mBuffers));
+ WriteIPDLParam(aWriter, aActor, std::move(aVar->mAlphaBuffers));
+ WriteIPDLParam(aWriter, aActor, std::move(aVar->mExtraDatas));
+}
+
+/* static */ bool ipc::IPDLParamTraits<ArrayOfRemoteMediaRawData*>::Read(
+ IPC::MessageReader* aReader, mozilla::ipc::IProtocol* aActor,
+ RefPtr<ArrayOfRemoteMediaRawData>* aVar) {
+ auto array = MakeRefPtr<ArrayOfRemoteMediaRawData>();
+ if (!ReadIPDLParam(aReader, aActor, &array->mSamples) ||
+ !ReadIPDLParam(aReader, aActor, &array->mBuffers) ||
+ !ReadIPDLParam(aReader, aActor, &array->mAlphaBuffers) ||
+ !ReadIPDLParam(aReader, aActor, &array->mExtraDatas)) {
+ return false;
+ }
+ *aVar = std::move(array);
+ return true;
+}
+
+/* static */ void
+ipc::IPDLParamTraits<ArrayOfRemoteMediaRawData::RemoteMediaRawData>::Write(
+ IPC::MessageWriter* aWriter, ipc::IProtocol* aActor,
+ const paramType& aVar) {
+ WriteIPDLParam(aWriter, aActor, aVar.mBase);
+ WriteIPDLParam(aWriter, aActor, aVar.mEOS);
+ WriteIPDLParam(aWriter, aActor, aVar.mHeight);
+ WriteIPDLParam(aWriter, aActor, aVar.mDiscardPadding);
+ WriteIPDLParam(aWriter, aActor, aVar.mOriginalPresentationWindow);
+ WriteIPDLParam(aWriter, aActor, aVar.mCryptoConfig);
+}
+
+/* static */ bool
+ipc::IPDLParamTraits<ArrayOfRemoteMediaRawData::RemoteMediaRawData>::Read(
+ IPC::MessageReader* aReader, ipc::IProtocol* aActor, paramType* aVar) {
+ MediaDataIPDL mBase;
+ return ReadIPDLParam(aReader, aActor, &aVar->mBase) &&
+ ReadIPDLParam(aReader, aActor, &aVar->mEOS) &&
+ ReadIPDLParam(aReader, aActor, &aVar->mHeight) &&
+ ReadIPDLParam(aReader, aActor, &aVar->mDiscardPadding) &&
+ ReadIPDLParam(aReader, aActor, &aVar->mOriginalPresentationWindow) &&
+ ReadIPDLParam(aReader, aActor, &aVar->mCryptoConfig);
+};
+
+bool ArrayOfRemoteAudioData::Fill(
+ const nsTArray<RefPtr<AudioData>>& aData,
+ std::function<ShmemBuffer(size_t)>&& aAllocator) {
+ nsTArray<AlignedAudioBuffer> dataBuffers(aData.Length());
+ for (auto&& entry : aData) {
+ dataBuffers.AppendElement(std::move(entry->mAudioData));
+ mSamples.AppendElement(RemoteAudioData{
+ MediaDataIPDL(entry->mOffset, entry->mTime, entry->mTimecode,
+ entry->mDuration, entry->mKeyframe),
+ entry->mChannels, entry->mRate, uint32_t(entry->mChannelMap),
+ entry->mOriginalTime, entry->mTrimWindow, entry->mFrames,
+ entry->mDataOffset});
+ }
+ mBuffers = RemoteArrayOfByteBuffer(dataBuffers, aAllocator);
+ if (!mBuffers.IsValid()) {
+ return false;
+ }
+ return true;
+}
+
+already_AddRefed<AudioData> ArrayOfRemoteAudioData::ElementAt(
+ size_t aIndex) const {
+ if (!IsValid()) {
+ return nullptr;
+ }
+ MOZ_ASSERT(aIndex < Count());
+ MOZ_DIAGNOSTIC_ASSERT(mBuffers.Count() == Count(),
+ "Something ain't right here");
+ const auto& sample = mSamples[aIndex];
+ AlignedAudioBuffer data = mBuffers.AlignedBufferAt<AudioDataValue>(aIndex);
+ if (mBuffers.SizeAt(aIndex) && !data) {
+ // OOM
+ return nullptr;
+ }
+ auto audioData = MakeRefPtr<AudioData>(
+ sample.mBase.offset(), sample.mBase.time(), std::move(data),
+ sample.mChannels, sample.mRate, sample.mChannelMap);
+ // An AudioData's duration is set at construction time based on the size of
+ // the provided buffer. However, if a trim window is set, this value will be
+ // incorrect. We have to re-set it to what it actually was.
+ audioData->mDuration = sample.mBase.duration();
+ audioData->mOriginalTime = sample.mOriginalTime;
+ audioData->mTrimWindow = sample.mTrimWindow;
+ audioData->mFrames = sample.mFrames;
+ audioData->mDataOffset = sample.mDataOffset;
+ return audioData.forget();
+}
+
+/*static */ void ipc::IPDLParamTraits<ArrayOfRemoteAudioData*>::Write(
+ IPC::MessageWriter* aWriter, ipc::IProtocol* aActor,
+ ArrayOfRemoteAudioData* aVar) {
+ WriteIPDLParam(aWriter, aActor, std::move(aVar->mSamples));
+ WriteIPDLParam(aWriter, aActor, std::move(aVar->mBuffers));
+}
+
+/* static */ bool ipc::IPDLParamTraits<ArrayOfRemoteAudioData*>::Read(
+ IPC::MessageReader* aReader, mozilla::ipc::IProtocol* aActor,
+ RefPtr<ArrayOfRemoteAudioData>* aVar) {
+ auto array = MakeRefPtr<ArrayOfRemoteAudioData>();
+ if (!ReadIPDLParam(aReader, aActor, &array->mSamples) ||
+ !ReadIPDLParam(aReader, aActor, &array->mBuffers)) {
+ return false;
+ }
+ *aVar = std::move(array);
+ return true;
+}
+
+/* static */ void
+ipc::IPDLParamTraits<ArrayOfRemoteAudioData::RemoteAudioData>::Write(
+ IPC::MessageWriter* aWriter, ipc::IProtocol* aActor,
+ const paramType& aVar) {
+ WriteIPDLParam(aWriter, aActor, aVar.mBase);
+ WriteIPDLParam(aWriter, aActor, aVar.mChannels);
+ WriteIPDLParam(aWriter, aActor, aVar.mRate);
+ WriteIPDLParam(aWriter, aActor, aVar.mChannelMap);
+ WriteIPDLParam(aWriter, aActor, aVar.mOriginalTime);
+ WriteIPDLParam(aWriter, aActor, aVar.mTrimWindow);
+ WriteIPDLParam(aWriter, aActor, aVar.mFrames);
+ WriteIPDLParam(aWriter, aActor, aVar.mDataOffset);
+}
+
+/* static */ bool
+ipc::IPDLParamTraits<ArrayOfRemoteAudioData::RemoteAudioData>::Read(
+ IPC::MessageReader* aReader, ipc::IProtocol* aActor, paramType* aVar) {
+ MediaDataIPDL mBase;
+ if (!ReadIPDLParam(aReader, aActor, &aVar->mBase) ||
+ !ReadIPDLParam(aReader, aActor, &aVar->mChannels) ||
+ !ReadIPDLParam(aReader, aActor, &aVar->mRate) ||
+ !ReadIPDLParam(aReader, aActor, &aVar->mChannelMap) ||
+ !ReadIPDLParam(aReader, aActor, &aVar->mOriginalTime) ||
+ !ReadIPDLParam(aReader, aActor, &aVar->mTrimWindow) ||
+ !ReadIPDLParam(aReader, aActor, &aVar->mFrames) ||
+ !ReadIPDLParam(aReader, aActor, &aVar->mDataOffset)) {
+ return false;
+ }
+ return true;
+};
+
+} // namespace mozilla
diff --git a/dom/media/ipc/RemoteMediaData.h b/dom/media/ipc/RemoteMediaData.h
new file mode 100644
index 0000000000..8055a397d9
--- /dev/null
+++ b/dom/media/ipc/RemoteMediaData.h
@@ -0,0 +1,395 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_media_ipc_RemoteMediaData_h
+#define mozilla_dom_media_ipc_RemoteMediaData_h
+
+#include <functional>
+
+#include "MediaData.h"
+#include "PlatformDecoderModule.h"
+#include "ipc/IPCMessageUtils.h"
+#include "mozilla/GfxMessageUtils.h"
+#include "mozilla/PMediaDecoderParams.h"
+#include "mozilla/RemoteImageHolder.h"
+#include "mozilla/ShmemPool.h"
+#include "mozilla/gfx/Rect.h"
+
+namespace mozilla {
+
+class ShmemPool;
+
+namespace ipc {
+class IProtocol;
+class Shmem;
+} // namespace ipc
+
+//-----------------------------------------------------------------------------
+// Declaration of the IPDL type |struct RemoteVideoData|
+//
+// We can't use the generated binding in order to use move semantics properly
+// (see bug 1664362)
+class RemoteVideoData final {
+ private:
+ typedef mozilla::gfx::IntSize IntSize;
+
+ public:
+ RemoteVideoData() = default;
+
+ RemoteVideoData(const MediaDataIPDL& aBase, const IntSize& aDisplay,
+ RemoteImageHolder&& aImage, int32_t aFrameID)
+ : mBase(aBase),
+ mDisplay(aDisplay),
+ mImage(std::move(aImage)),
+ mFrameID(aFrameID) {}
+
+ // This is equivalent to the old RemoteVideoDataIPDL object and is similar to
+ // the RemoteAudioDataIPDL object. To ensure style consistency we use the IPDL
+ // naming convention here.
+ MediaDataIPDL& base() { return mBase; }
+ const MediaDataIPDL& base() const { return mBase; }
+
+ IntSize& display() { return mDisplay; }
+ const IntSize& display() const { return mDisplay; }
+
+ RemoteImageHolder& image() { return mImage; }
+ const RemoteImageHolder& image() const { return mImage; }
+
+ int32_t& frameID() { return mFrameID; }
+ const int32_t& frameID() const { return mFrameID; }
+
+ private:
+ friend struct ipc::IPDLParamTraits<RemoteVideoData>;
+ MediaDataIPDL mBase;
+ IntSize mDisplay;
+ RemoteImageHolder mImage;
+ int32_t mFrameID;
+};
+
+// Until bug 1572054 is resolved, we can't move our objects when using IPDL's
+// union or array. They are always copied. So we make the class refcounted to
+// and always pass it by pointed to bypass the problem for now.
+class ArrayOfRemoteVideoData final {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ArrayOfRemoteVideoData)
+ public:
+ ArrayOfRemoteVideoData() = default;
+ ArrayOfRemoteVideoData(ArrayOfRemoteVideoData&& aOther)
+ : mArray(std::move(aOther.mArray)) {}
+ explicit ArrayOfRemoteVideoData(nsTArray<RemoteVideoData>&& aOther)
+ : mArray(std::move(aOther)) {}
+ ArrayOfRemoteVideoData(const ArrayOfRemoteVideoData& aOther) {
+ MOZ_CRASH("Should never be used but declared by generated IPDL binding");
+ }
+ ArrayOfRemoteVideoData& operator=(ArrayOfRemoteVideoData&& aOther) noexcept {
+ if (this != &aOther) {
+ mArray = std::move(aOther.mArray);
+ }
+ return *this;
+ }
+ ArrayOfRemoteVideoData& operator=(nsTArray<RemoteVideoData>&& aOther) {
+ mArray = std::move(aOther);
+ return *this;
+ }
+
+ void AppendElements(nsTArray<RemoteVideoData>&& aOther) {
+ mArray.AppendElements(std::move(aOther));
+ }
+ void Append(RemoteVideoData&& aVideo) {
+ mArray.AppendElement(std::move(aVideo));
+ }
+ const nsTArray<RemoteVideoData>& Array() const { return mArray; }
+ nsTArray<RemoteVideoData>& Array() { return mArray; }
+
+ private:
+ ~ArrayOfRemoteVideoData() = default;
+ friend struct ipc::IPDLParamTraits<mozilla::ArrayOfRemoteVideoData*>;
+ nsTArray<RemoteVideoData> mArray;
+};
+
+/* The class will pack either an array of AlignedBuffer or MediaByteBuffer
+ * into a single Shmem objects. */
+class RemoteArrayOfByteBuffer {
+ public:
+ RemoteArrayOfByteBuffer();
+ template <typename Type>
+ RemoteArrayOfByteBuffer(const nsTArray<AlignedBuffer<Type>>& aArray,
+ std::function<ShmemBuffer(size_t)>& aAllocator) {
+ // Determine the total size we will need for this object.
+ size_t totalSize = 0;
+ for (auto& buffer : aArray) {
+ totalSize += buffer.Size();
+ }
+ if (totalSize) {
+ if (!AllocateShmem(totalSize, aAllocator)) {
+ return;
+ }
+ }
+ size_t offset = 0;
+ for (auto& buffer : aArray) {
+ if (totalSize && buffer && buffer.Size()) {
+ Write(offset, buffer.Data(), buffer.Size());
+ }
+ mOffsets.AppendElement(OffsetEntry{offset, buffer.Size()});
+ offset += buffer.Size();
+ }
+ mIsValid = true;
+ }
+
+ RemoteArrayOfByteBuffer(const nsTArray<RefPtr<MediaByteBuffer>>& aArray,
+ std::function<ShmemBuffer(size_t)>& aAllocator);
+ RemoteArrayOfByteBuffer& operator=(RemoteArrayOfByteBuffer&& aOther) noexcept;
+
+ // Return the packed aIndexth buffer as an AlignedByteBuffer.
+ // The operation is fallible should an out of memory be encountered. The
+ // result should be tested accordingly.
+ template <typename Type>
+ AlignedBuffer<Type> AlignedBufferAt(size_t aIndex) const {
+ MOZ_ASSERT(aIndex < Count());
+ const OffsetEntry& entry = mOffsets[aIndex];
+ size_t entrySize = std::get<1>(entry);
+ if (!mBuffers || !entrySize) {
+ // It's an empty one.
+ return AlignedBuffer<Type>();
+ }
+ if (!Check(std::get<0>(entry), entrySize)) {
+ // This Shmem is corrupted and can't contain the data we are about to
+ // retrieve. We return an empty array instead of asserting to allow for
+ // recovery.
+ return AlignedBuffer<Type>();
+ }
+ if (0 != entrySize % sizeof(Type)) {
+ // There's an error, that entry can't represent this data.
+ return AlignedBuffer<Type>();
+ }
+ return AlignedBuffer<Type>(
+ reinterpret_cast<Type*>(BuffersStartAddress() + std::get<0>(entry)),
+ entrySize / sizeof(Type));
+ }
+
+ // Return the packed aIndexth buffer as aMediaByteBuffer.
+ // Will return nullptr if the packed buffer was originally empty.
+ already_AddRefed<MediaByteBuffer> MediaByteBufferAt(size_t aIndex) const;
+ // Return the size of the aIndexth buffer.
+ size_t SizeAt(size_t aIndex) const { return std::get<1>(mOffsets[aIndex]); }
+ // Return false if an out of memory error was encountered during construction.
+ bool IsValid() const { return mIsValid; };
+ // Return the number of buffers packed into this entity.
+ size_t Count() const { return mOffsets.Length(); }
+ virtual ~RemoteArrayOfByteBuffer();
+
+ private:
+ friend struct ipc::IPDLParamTraits<RemoteArrayOfByteBuffer>;
+ // Allocate shmem, false if an error occurred.
+ bool AllocateShmem(size_t aSize,
+ std::function<ShmemBuffer(size_t)>& aAllocator);
+ // The starting address of the Shmem
+ uint8_t* BuffersStartAddress() const;
+ // Check that the allocated Shmem can contain such range.
+ bool Check(size_t aOffset, size_t aSizeInBytes) const;
+ void Write(size_t aOffset, const void* aSourceAddr, size_t aSizeInBytes);
+ // Set to false is the buffer isn't initialized yet or a memory error occurred
+ // during construction.
+ bool mIsValid = false;
+ // The packed data. The Maybe will be empty if all buffers packed were
+ // orignally empty.
+ Maybe<ipc::Shmem> mBuffers;
+ // The offset to the start of the individual buffer and its size (all in
+ // bytes)
+ typedef std::tuple<size_t, size_t> OffsetEntry;
+ nsTArray<OffsetEntry> mOffsets;
+};
+
+/* The class will pack an array of MediaRawData using at most three Shmem
+ * objects. Under the most common scenaria, only two Shmems will be used as
+ * there are few videos with an alpha channel in the wild.
+ * We unfortunately can't populate the array at construction nor present an
+ * interface similar to an actual nsTArray or the ArrayOfRemoteVideoData above
+ * as currently IPC serialization is always non-fallible. So we must create the
+ * object first, fill it to determine if we ran out of memory and then send the
+ * object over IPC.
+ */
+class ArrayOfRemoteMediaRawData {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ArrayOfRemoteMediaRawData)
+ public:
+ // Fill the content, return false if an OOM occurred.
+ bool Fill(const nsTArray<RefPtr<MediaRawData>>& aData,
+ std::function<ShmemBuffer(size_t)>&& aAllocator);
+
+ // Return the aIndexth MediaRawData or nullptr if a memory error occurred.
+ already_AddRefed<MediaRawData> ElementAt(size_t aIndex) const;
+
+ // Return the number of MediaRawData stored in this container.
+ size_t Count() const { return mSamples.Length(); }
+ bool IsEmpty() const { return Count() == 0; }
+ bool IsValid() const {
+ return mBuffers.IsValid() && mAlphaBuffers.IsValid() &&
+ mExtraDatas.IsValid();
+ }
+
+ struct RemoteMediaRawData {
+ MediaDataIPDL mBase;
+ bool mEOS;
+ // This will be zero for audio.
+ int32_t mHeight;
+ uint32_t mDiscardPadding;
+ Maybe<media::TimeInterval> mOriginalPresentationWindow;
+ Maybe<CryptoInfo> mCryptoConfig;
+ };
+
+ private:
+ friend struct ipc::IPDLParamTraits<ArrayOfRemoteMediaRawData*>;
+ virtual ~ArrayOfRemoteMediaRawData() = default;
+
+ nsTArray<RemoteMediaRawData> mSamples;
+ RemoteArrayOfByteBuffer mBuffers;
+ RemoteArrayOfByteBuffer mAlphaBuffers;
+ RemoteArrayOfByteBuffer mExtraDatas;
+};
+
+/* The class will pack an array of MediaAudioData using at most a single Shmem
+ * objects.
+ * We unfortunately can't populate the array at construction nor present an
+ * interface similar to an actual nsTArray or the ArrayOfRemoteVideoData above
+ * as currently IPC serialization is always non-fallible. So we must create the
+ * object first, fill it to determine if we ran out of memory and then send the
+ * object over IPC.
+ */
+class ArrayOfRemoteAudioData final {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ArrayOfRemoteAudioData)
+ public:
+ // Fill the content, return false if an OOM occurred.
+ bool Fill(const nsTArray<RefPtr<AudioData>>& aData,
+ std::function<ShmemBuffer(size_t)>&& aAllocator);
+
+ // Return the aIndexth MediaRawData or nullptr if a memory error occurred.
+ already_AddRefed<AudioData> ElementAt(size_t aIndex) const;
+
+ // Return the number of MediaRawData stored in this container.
+ size_t Count() const { return mSamples.Length(); }
+ bool IsEmpty() const { return Count() == 0; }
+ bool IsValid() const { return mBuffers.IsValid(); }
+
+ struct RemoteAudioData {
+ friend struct ipc::IPDLParamTraits<RemoteVideoData>;
+ MediaDataIPDL mBase;
+ uint32_t mChannels;
+ uint32_t mRate;
+ uint32_t mChannelMap;
+ media::TimeUnit mOriginalTime;
+ Maybe<media::TimeInterval> mTrimWindow;
+ uint32_t mFrames;
+ size_t mDataOffset;
+ };
+
+ private:
+ friend struct ipc::IPDLParamTraits<ArrayOfRemoteAudioData*>;
+ ~ArrayOfRemoteAudioData() = default;
+
+ nsTArray<RemoteAudioData> mSamples;
+ RemoteArrayOfByteBuffer mBuffers;
+};
+
+namespace ipc {
+
+template <>
+struct IPDLParamTraits<RemoteVideoData> {
+ typedef RemoteVideoData paramType;
+ static void Write(IPC::MessageWriter* aWriter, ipc::IProtocol* aActor,
+ paramType&& aVar) {
+ WriteIPDLParam(aWriter, aActor, std::move(aVar.mBase));
+ WriteIPDLParam(aWriter, aActor, std::move(aVar.mDisplay));
+ WriteIPDLParam(aWriter, aActor, std::move(aVar.mImage));
+ aWriter->WriteBytes(&aVar.mFrameID, 4);
+ }
+
+ static bool Read(IPC::MessageReader* aReader, mozilla::ipc::IProtocol* aActor,
+ paramType* aVar) {
+ if (!ReadIPDLParam(aReader, aActor, &aVar->mBase) ||
+ !ReadIPDLParam(aReader, aActor, &aVar->mDisplay) ||
+ !ReadIPDLParam(aReader, aActor, &aVar->mImage) ||
+ !aReader->ReadBytesInto(&aVar->mFrameID, 4)) {
+ return false;
+ }
+ return true;
+ }
+};
+
+template <>
+struct IPDLParamTraits<ArrayOfRemoteVideoData*> {
+ typedef ArrayOfRemoteVideoData paramType;
+ static void Write(IPC::MessageWriter* aWriter,
+ mozilla::ipc::IProtocol* aActor, paramType* aVar) {
+ WriteIPDLParam(aWriter, aActor, std::move(aVar->mArray));
+ }
+
+ static bool Read(IPC::MessageReader* aReader, ipc::IProtocol* aActor,
+ RefPtr<paramType>* aVar) {
+ nsTArray<RemoteVideoData> array;
+ if (!ReadIPDLParam(aReader, aActor, &array)) {
+ return false;
+ }
+ auto results = MakeRefPtr<ArrayOfRemoteVideoData>(std::move(array));
+ *aVar = std::move(results);
+ return true;
+ }
+};
+
+template <>
+struct IPDLParamTraits<RemoteArrayOfByteBuffer> {
+ typedef RemoteArrayOfByteBuffer paramType;
+ // We do not want to move the RemoteArrayOfByteBuffer as we want to recycle
+ // the shmem it contains for another time.
+ static void Write(IPC::MessageWriter* aWriter, ipc::IProtocol* aActor,
+ const paramType& aVar);
+
+ static bool Read(IPC::MessageReader* aReader, ipc::IProtocol* aActor,
+ paramType* aVar);
+};
+
+template <>
+struct IPDLParamTraits<ArrayOfRemoteMediaRawData::RemoteMediaRawData> {
+ typedef ArrayOfRemoteMediaRawData::RemoteMediaRawData paramType;
+ static void Write(IPC::MessageWriter* aWriter, ipc::IProtocol* aActor,
+ const paramType& aVar);
+
+ static bool Read(IPC::MessageReader* aReader, ipc::IProtocol* aActor,
+ paramType* aVar);
+};
+
+template <>
+struct IPDLParamTraits<ArrayOfRemoteMediaRawData*> {
+ typedef ArrayOfRemoteMediaRawData paramType;
+ static void Write(IPC::MessageWriter* aWriter, ipc::IProtocol* aActor,
+ paramType* aVar);
+
+ static bool Read(IPC::MessageReader* aReader, ipc::IProtocol* aActor,
+ RefPtr<paramType>* aVar);
+};
+
+template <>
+struct IPDLParamTraits<ArrayOfRemoteAudioData::RemoteAudioData> {
+ typedef ArrayOfRemoteAudioData::RemoteAudioData paramType;
+ static void Write(IPC::MessageWriter* aWriter, ipc::IProtocol* aActor,
+ const paramType& aVar);
+
+ static bool Read(IPC::MessageReader* aReader, ipc::IProtocol* aActor,
+ paramType* aVar);
+};
+
+template <>
+struct IPDLParamTraits<ArrayOfRemoteAudioData*> {
+ typedef ArrayOfRemoteAudioData paramType;
+ static void Write(IPC::MessageWriter* aWriter, ipc::IProtocol* aActor,
+ paramType* aVar);
+
+ static bool Read(IPC::MessageReader* aReader, ipc::IProtocol* aActor,
+ RefPtr<paramType>* aVar);
+};
+} // namespace ipc
+
+} // namespace mozilla
+
+#endif // mozilla_dom_media_ipc_RemoteMediaData_h
diff --git a/dom/media/ipc/RemoteMediaDataDecoder.cpp b/dom/media/ipc/RemoteMediaDataDecoder.cpp
new file mode 100644
index 0000000000..6db3c0d940
--- /dev/null
+++ b/dom/media/ipc/RemoteMediaDataDecoder.cpp
@@ -0,0 +1,163 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "RemoteMediaDataDecoder.h"
+
+#include "RemoteDecoderChild.h"
+#include "RemoteDecoderManagerChild.h"
+
+namespace mozilla {
+
+#ifdef LOG
+# undef LOG
+#endif // LOG
+#define LOG(arg, ...) \
+ DDMOZ_LOG(sPDMLog, mozilla::LogLevel::Debug, "::%s: " arg, __func__, \
+ ##__VA_ARGS__)
+
+RemoteMediaDataDecoder::RemoteMediaDataDecoder(RemoteDecoderChild* aChild)
+ : mChild(aChild) {
+ LOG("%p is created", this);
+}
+
+RemoteMediaDataDecoder::~RemoteMediaDataDecoder() {
+ if (mChild) {
+ // Shutdown didn't get called. This can happen if the creation of the
+ // decoder got interrupted while pending.
+ nsCOMPtr<nsISerialEventTarget> thread =
+ RemoteDecoderManagerChild::GetManagerThread();
+ MOZ_ASSERT(thread);
+ thread->Dispatch(NS_NewRunnableFunction(
+ "RemoteMediaDataDecoderShutdown", [child = std::move(mChild), thread] {
+ child->Shutdown()->Then(
+ thread, __func__,
+ [child](const ShutdownPromise::ResolveOrRejectValue& aValue) {
+ child->DestroyIPDL();
+ });
+ }));
+ }
+ LOG("%p is released", this);
+}
+
+RefPtr<MediaDataDecoder::InitPromise> RemoteMediaDataDecoder::Init() {
+ RefPtr<RemoteMediaDataDecoder> self = this;
+ return InvokeAsync(RemoteDecoderManagerChild::GetManagerThread(), __func__,
+ [self]() { return self->mChild->Init(); })
+ ->Then(
+ RemoteDecoderManagerChild::GetManagerThread(), __func__,
+ [self, this](TrackType aTrack) {
+ // If shutdown has started in the meantime shutdown promise may
+ // be resloved before this task. In this case mChild will be null
+ // and the init promise has to be canceled.
+ if (!mChild) {
+ return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_CANCELED,
+ __func__);
+ }
+ mDescription = mChild->GetDescriptionName();
+ mProcessName = mChild->GetProcessName();
+ mCodecName = mChild->GetCodecName();
+ mIsHardwareAccelerated =
+ mChild->IsHardwareAccelerated(mHardwareAcceleratedReason);
+ mConversion = mChild->NeedsConversion();
+ LOG("%p RemoteDecoderChild has been initialized - description: %s, "
+ "process: %s, codec: %s",
+ this, mDescription.get(), mProcessName.get(), mCodecName.get());
+ return InitPromise::CreateAndResolve(aTrack, __func__);
+ },
+ [self](const MediaResult& aError) {
+ return InitPromise::CreateAndReject(aError, __func__);
+ });
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> RemoteMediaDataDecoder::Decode(
+ MediaRawData* aSample) {
+ RefPtr<RemoteMediaDataDecoder> self = this;
+ RefPtr<MediaRawData> sample = aSample;
+ return InvokeAsync(
+ RemoteDecoderManagerChild::GetManagerThread(), __func__,
+ [self, sample]() {
+ return self->mChild->Decode(nsTArray<RefPtr<MediaRawData>>{sample});
+ });
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> RemoteMediaDataDecoder::DecodeBatch(
+ nsTArray<RefPtr<MediaRawData>>&& aSamples) {
+ RefPtr<RemoteMediaDataDecoder> self = this;
+ return InvokeAsync(RemoteDecoderManagerChild::GetManagerThread(), __func__,
+ [self, samples = std::move(aSamples)]() {
+ return self->mChild->Decode(samples);
+ });
+}
+
+RefPtr<MediaDataDecoder::FlushPromise> RemoteMediaDataDecoder::Flush() {
+ RefPtr<RemoteMediaDataDecoder> self = this;
+ return InvokeAsync(RemoteDecoderManagerChild::GetManagerThread(), __func__,
+ [self]() { return self->mChild->Flush(); });
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> RemoteMediaDataDecoder::Drain() {
+ RefPtr<RemoteMediaDataDecoder> self = this;
+ return InvokeAsync(RemoteDecoderManagerChild::GetManagerThread(), __func__,
+ [self]() { return self->mChild->Drain(); });
+}
+
+RefPtr<ShutdownPromise> RemoteMediaDataDecoder::Shutdown() {
+ RefPtr<RemoteMediaDataDecoder> self = this;
+ return InvokeAsync(
+ RemoteDecoderManagerChild::GetManagerThread(), __func__, [self]() {
+ RefPtr<ShutdownPromise> p = self->mChild->Shutdown();
+
+ // We're about to be destroyed and drop our ref to
+ // *DecoderChild. Make sure we put a ref into the
+ // task queue for the *DecoderChild thread to keep
+ // it alive until we send the delete message.
+ p->Then(RemoteDecoderManagerChild::GetManagerThread(), __func__,
+ [child = std::move(self->mChild)](
+ const ShutdownPromise::ResolveOrRejectValue& aValue) {
+ MOZ_ASSERT(aValue.IsResolve());
+ child->DestroyIPDL();
+ return ShutdownPromise::CreateAndResolveOrReject(aValue,
+ __func__);
+ });
+ return p;
+ });
+}
+
+bool RemoteMediaDataDecoder::IsHardwareAccelerated(
+ nsACString& aFailureReason) const {
+ aFailureReason = mHardwareAcceleratedReason;
+ return mIsHardwareAccelerated;
+}
+
+void RemoteMediaDataDecoder::SetSeekThreshold(const media::TimeUnit& aTime) {
+ RefPtr<RemoteMediaDataDecoder> self = this;
+ media::TimeUnit time = aTime;
+ RemoteDecoderManagerChild::GetManagerThread()->Dispatch(
+ NS_NewRunnableFunction("dom::RemoteMediaDataDecoder::SetSeekThreshold",
+ [=]() {
+ MOZ_ASSERT(self->mChild);
+ self->mChild->SetSeekThreshold(time);
+ }),
+ NS_DISPATCH_NORMAL);
+}
+
+MediaDataDecoder::ConversionRequired RemoteMediaDataDecoder::NeedsConversion()
+ const {
+ return mConversion;
+}
+
+nsCString RemoteMediaDataDecoder::GetDescriptionName() const {
+ return mDescription;
+}
+
+nsCString RemoteMediaDataDecoder::GetProcessName() const {
+ return mProcessName;
+}
+
+nsCString RemoteMediaDataDecoder::GetCodecName() const { return mCodecName; }
+
+#undef LOG
+
+} // namespace mozilla
diff --git a/dom/media/ipc/RemoteMediaDataDecoder.h b/dom/media/ipc/RemoteMediaDataDecoder.h
new file mode 100644
index 0000000000..4acc5801f7
--- /dev/null
+++ b/dom/media/ipc/RemoteMediaDataDecoder.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef include_dom_media_ipc_RemoteMediaDataDecoder_h
+#define include_dom_media_ipc_RemoteMediaDataDecoder_h
+#include "PlatformDecoderModule.h"
+
+#include "MediaData.h"
+
+namespace mozilla {
+
+class RemoteDecoderChild;
+class RemoteDecoderManagerChild;
+class RemoteMediaDataDecoder;
+
+DDLoggedTypeCustomNameAndBase(RemoteMediaDataDecoder, RemoteMediaDataDecoder,
+ MediaDataDecoder);
+
+// A MediaDataDecoder implementation that proxies through IPDL
+// to a 'real' decoder in the GPU or RDD process.
+// All requests get forwarded to a *DecoderChild instance that
+// operates solely on the provided manager and abstract manager threads.
+class RemoteMediaDataDecoder final
+ : public MediaDataDecoder,
+ public DecoderDoctorLifeLogger<RemoteMediaDataDecoder> {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RemoteMediaDataDecoder, final);
+
+ explicit RemoteMediaDataDecoder(RemoteDecoderChild* aChild);
+
+ // MediaDataDecoder
+ RefPtr<InitPromise> Init() override;
+ RefPtr<DecodePromise> Decode(MediaRawData* aSample) override;
+ bool CanDecodeBatch() const override { return true; }
+ RefPtr<DecodePromise> DecodeBatch(
+ nsTArray<RefPtr<MediaRawData>>&& aSamples) override;
+ RefPtr<DecodePromise> Drain() override;
+ RefPtr<FlushPromise> Flush() override;
+ RefPtr<ShutdownPromise> Shutdown() override;
+ bool IsHardwareAccelerated(nsACString& aFailureReason) const override;
+ void SetSeekThreshold(const media::TimeUnit& aTime) override;
+ nsCString GetDescriptionName() const override;
+ nsCString GetProcessName() const override;
+ nsCString GetCodecName() const override;
+ ConversionRequired NeedsConversion() const override;
+
+ private:
+ ~RemoteMediaDataDecoder();
+
+ // Only ever written to from the reader task queue (during the constructor and
+ // destructor when we can guarantee no other threads are accessing it). Only
+ // read from the manager thread.
+ RefPtr<RemoteDecoderChild> mChild;
+ // Only ever written/modified during decoder initialisation.
+ // As such can be accessed from any threads after that.
+ nsCString mDescription = "RemoteMediaDataDecoder"_ns;
+ nsCString mProcessName = "unknown"_ns;
+ nsCString mCodecName = "unknown"_ns;
+ bool mIsHardwareAccelerated = false;
+ nsCString mHardwareAcceleratedReason;
+ ConversionRequired mConversion = ConversionRequired::kNeedNone;
+};
+
+} // namespace mozilla
+
+#endif // include_dom_media_ipc_RemoteMediaDataDecoder_h
diff --git a/dom/media/ipc/RemoteVideoDecoder.cpp b/dom/media/ipc/RemoteVideoDecoder.cpp
new file mode 100644
index 0000000000..d3587b4a0b
--- /dev/null
+++ b/dom/media/ipc/RemoteVideoDecoder.cpp
@@ -0,0 +1,296 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "RemoteVideoDecoder.h"
+
+#include "mozilla/layers/ImageDataSerializer.h"
+
+#ifdef MOZ_AV1
+# include "AOMDecoder.h"
+# include "DAV1DDecoder.h"
+#endif
+#ifdef XP_WIN
+# include "WMFDecoderModule.h"
+#endif
+#include "GPUVideoImage.h"
+#include "ImageContainer.h" // for PlanarYCbCrData and BufferRecycleBin
+#include "MediaDataDecoderProxy.h"
+#include "MediaInfo.h"
+#include "PDMFactory.h"
+#include "RemoteDecoderManagerParent.h"
+#include "RemoteImageHolder.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/layers/ImageClient.h"
+#include "mozilla/layers/TextureClient.h"
+#include "mozilla/layers/VideoBridgeChild.h"
+
+namespace mozilla {
+
+using namespace layers; // for PlanarYCbCrData and BufferRecycleBin
+using namespace ipc;
+using namespace gfx;
+
+layers::TextureForwarder* KnowsCompositorVideo::GetTextureForwarder() {
+ auto* vbc = VideoBridgeChild::GetSingleton();
+ return (vbc && vbc->CanSend()) ? vbc : nullptr;
+}
+layers::LayersIPCActor* KnowsCompositorVideo::GetLayersIPCActor() {
+ return GetTextureForwarder();
+}
+
+/* static */ already_AddRefed<KnowsCompositorVideo>
+KnowsCompositorVideo::TryCreateForIdentifier(
+ const layers::TextureFactoryIdentifier& aIdentifier) {
+ VideoBridgeChild* child = VideoBridgeChild::GetSingleton();
+ if (!child) {
+ return nullptr;
+ }
+
+ RefPtr<KnowsCompositorVideo> knowsCompositor = new KnowsCompositorVideo();
+ knowsCompositor->IdentifyTextureHost(aIdentifier);
+ return knowsCompositor.forget();
+}
+
+RemoteVideoDecoderChild::RemoteVideoDecoderChild(RemoteDecodeIn aLocation)
+ : RemoteDecoderChild(aLocation), mBufferRecycleBin(new BufferRecycleBin) {}
+
+MediaResult RemoteVideoDecoderChild::ProcessOutput(
+ DecodedOutputIPDL&& aDecodedData) {
+ AssertOnManagerThread();
+ MOZ_ASSERT(aDecodedData.type() == DecodedOutputIPDL::TArrayOfRemoteVideoData);
+
+ nsTArray<RemoteVideoData>& arrayData =
+ aDecodedData.get_ArrayOfRemoteVideoData()->Array();
+
+ for (auto&& data : arrayData) {
+ if (data.image().IsEmpty()) {
+ // This is a NullData object.
+ mDecodedData.AppendElement(MakeRefPtr<NullData>(
+ data.base().offset(), data.base().time(), data.base().duration()));
+ continue;
+ }
+ RefPtr<Image> image = data.image().TransferToImage(mBufferRecycleBin);
+
+ RefPtr<VideoData> video = VideoData::CreateFromImage(
+ data.display(), data.base().offset(), data.base().time(),
+ data.base().duration(), image, data.base().keyframe(),
+ data.base().timecode());
+
+ if (!video) {
+ // OOM
+ return MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__);
+ }
+ mDecodedData.AppendElement(std::move(video));
+ }
+ return NS_OK;
+}
+
+MediaResult RemoteVideoDecoderChild::InitIPDL(
+ const VideoInfo& aVideoInfo, float aFramerate,
+ const CreateDecoderParams::OptionSet& aOptions,
+ Maybe<layers::TextureFactoryIdentifier> aIdentifier,
+ const Maybe<uint64_t>& aMediaEngineId,
+ const Maybe<TrackingId>& aTrackingId) {
+ MOZ_ASSERT_IF(mLocation == RemoteDecodeIn::GpuProcess, aIdentifier);
+
+ RefPtr<RemoteDecoderManagerChild> manager =
+ RemoteDecoderManagerChild::GetSingleton(mLocation);
+
+ // The manager isn't available because RemoteDecoderManagerChild has been
+ // initialized with null end points and we don't want to decode video on RDD
+ // process anymore. Return false here so that we can fallback to other PDMs.
+ if (!manager) {
+ return MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("RemoteDecoderManager is not available."));
+ }
+
+ if (!manager->CanSend()) {
+ if (mLocation == RemoteDecodeIn::GpuProcess) {
+ // The manager doesn't support sending messages because we've just crashed
+ // and are working on reinitialization. Don't initialize mIPDLSelfRef and
+ // leave us in an error state. We'll then immediately reject the promise
+ // when Init() is called and the caller can try again. Hopefully by then
+ // the new manager is ready, or we've notified the caller of it being no
+ // longer available. If not, then the cycle repeats until we're ready.
+ return NS_OK;
+ }
+
+ return MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("RemoteDecoderManager unable to send."));
+ }
+
+ mIPDLSelfRef = this;
+ VideoDecoderInfoIPDL decoderInfo(aVideoInfo, aFramerate);
+ Unused << manager->SendPRemoteDecoderConstructor(
+ this, decoderInfo, aOptions, aIdentifier, aMediaEngineId, aTrackingId);
+
+ return NS_OK;
+}
+
+RemoteVideoDecoderParent::RemoteVideoDecoderParent(
+ RemoteDecoderManagerParent* aParent, const VideoInfo& aVideoInfo,
+ float aFramerate, const CreateDecoderParams::OptionSet& aOptions,
+ const Maybe<layers::TextureFactoryIdentifier>& aIdentifier,
+ nsISerialEventTarget* aManagerThread, TaskQueue* aDecodeTaskQueue,
+ const Maybe<uint64_t>& aMediaEngineId, Maybe<TrackingId> aTrackingId)
+ : RemoteDecoderParent(aParent, aOptions, aManagerThread, aDecodeTaskQueue,
+ aMediaEngineId, std::move(aTrackingId)),
+ mVideoInfo(aVideoInfo),
+ mFramerate(aFramerate) {
+ if (aIdentifier) {
+ // Check to see if we have a direct PVideoBridge connection to the
+ // destination process specified in aIdentifier, and create a
+ // KnowsCompositor representing that connection if so. If this fails, then
+ // we fall back to returning the decoded frames directly via Output().
+ mKnowsCompositor =
+ KnowsCompositorVideo::TryCreateForIdentifier(*aIdentifier);
+ }
+}
+
+IPCResult RemoteVideoDecoderParent::RecvConstruct(
+ ConstructResolver&& aResolver) {
+ auto imageContainer = MakeRefPtr<layers::ImageContainer>();
+ if (mKnowsCompositor && XRE_IsRDDProcess()) {
+ // Ensure to allocate recycle allocator
+ imageContainer->EnsureRecycleAllocatorForRDD(mKnowsCompositor);
+ }
+ auto params = CreateDecoderParams{
+ mVideoInfo, mKnowsCompositor,
+ imageContainer, CreateDecoderParams::VideoFrameRate(mFramerate),
+ mOptions, CreateDecoderParams::NoWrapper(true),
+ mMediaEngineId, mTrackingId,
+ };
+
+ mParent->EnsurePDMFactory().CreateDecoder(params)->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [resolver = std::move(aResolver), self = RefPtr{this}](
+ PlatformDecoderModule::CreateDecoderPromise::ResolveOrRejectValue&&
+ aValue) {
+ if (aValue.IsReject()) {
+ resolver(aValue.RejectValue());
+ return;
+ }
+ MOZ_ASSERT(aValue.ResolveValue());
+ self->mDecoder =
+ new MediaDataDecoderProxy(aValue.ResolveValue().forget(),
+ do_AddRef(self->mDecodeTaskQueue.get()));
+ resolver(NS_OK);
+ });
+ return IPC_OK();
+}
+
+MediaResult RemoteVideoDecoderParent::ProcessDecodedData(
+ MediaDataDecoder::DecodedData&& aData, DecodedOutputIPDL& aDecodedData) {
+ MOZ_ASSERT(OnManagerThread());
+
+ // If the video decoder bridge has shut down, stop.
+ if (mKnowsCompositor && !mKnowsCompositor->GetTextureForwarder()) {
+ aDecodedData = MakeRefPtr<ArrayOfRemoteVideoData>();
+ return NS_OK;
+ }
+
+ nsTArray<RemoteVideoData> array;
+
+ for (const auto& data : aData) {
+ MOZ_ASSERT(data->mType == MediaData::Type::VIDEO_DATA ||
+ data->mType == MediaData::Type::NULL_DATA,
+ "Can only decode videos using RemoteDecoderParent!");
+ if (data->mType == MediaData::Type::NULL_DATA) {
+ RemoteVideoData output(
+ MediaDataIPDL(data->mOffset, data->mTime, data->mTimecode,
+ data->mDuration, data->mKeyframe),
+ IntSize(), RemoteImageHolder(), -1);
+
+ array.AppendElement(std::move(output));
+ continue;
+ }
+ VideoData* video = static_cast<VideoData*>(data.get());
+
+ MOZ_ASSERT(video->mImage,
+ "Decoded video must output a layer::Image to "
+ "be used with RemoteDecoderParent");
+
+ RefPtr<TextureClient> texture;
+ SurfaceDescriptor sd;
+ IntSize size;
+ bool needStorage = false;
+
+ if (mKnowsCompositor) {
+ texture = video->mImage->GetTextureClient(mKnowsCompositor);
+
+ if (!texture) {
+ texture = ImageClient::CreateTextureClientForImage(video->mImage,
+ mKnowsCompositor);
+ }
+
+ if (texture) {
+ if (!texture->IsAddedToCompositableClient()) {
+ texture->InitIPDLActor(mKnowsCompositor, mParent->GetContentId());
+ texture->SetAddedToCompositableClient();
+ }
+ needStorage = true;
+ SurfaceDescriptorRemoteDecoder remoteSD;
+ texture->GetSurfaceDescriptorRemoteDecoder(&remoteSD);
+ sd = remoteSD;
+ size = texture->GetSize();
+ }
+ }
+
+ // If failed to create a GPU accelerated surface descriptor, fall back to
+ // copying frames via shmem.
+ if (!IsSurfaceDescriptorValid(sd)) {
+ needStorage = false;
+ PlanarYCbCrImage* image = video->mImage->AsPlanarYCbCrImage();
+ if (!image) {
+ return MediaResult(NS_ERROR_UNEXPECTED,
+ "Expected Planar YCbCr image in "
+ "RemoteVideoDecoderParent::ProcessDecodedData");
+ }
+
+ SurfaceDescriptorBuffer sdBuffer;
+ nsresult rv = image->BuildSurfaceDescriptorBuffer(
+ sdBuffer, [&](uint32_t aBufferSize) {
+ ShmemBuffer buffer = AllocateBuffer(aBufferSize);
+ if (buffer.Valid()) {
+ return MemoryOrShmem(std::move(buffer.Get()));
+ }
+ return MemoryOrShmem();
+ });
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ sd = sdBuffer;
+ size = image->GetSize();
+ }
+
+ if (needStorage) {
+ MOZ_ASSERT(sd.type() != SurfaceDescriptor::TSurfaceDescriptorBuffer);
+ mParent->StoreImage(static_cast<const SurfaceDescriptorGPUVideo&>(sd),
+ video->mImage, texture);
+ }
+
+ RemoteVideoData output(
+ MediaDataIPDL(data->mOffset, data->mTime, data->mTimecode,
+ data->mDuration, data->mKeyframe),
+ video->mDisplay,
+ RemoteImageHolder(
+ mParent,
+ XRE_IsGPUProcess()
+ ? VideoBridgeSource::GpuProcess
+ : (XRE_IsRDDProcess()
+ ? VideoBridgeSource::RddProcess
+ : VideoBridgeSource::MFMediaEngineCDMProcess),
+ size, video->mImage->GetColorDepth(), sd),
+ video->mFrameID);
+
+ array.AppendElement(std::move(output));
+ }
+
+ aDecodedData = MakeRefPtr<ArrayOfRemoteVideoData>(std::move(array));
+
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/dom/media/ipc/RemoteVideoDecoder.h b/dom/media/ipc/RemoteVideoDecoder.h
new file mode 100644
index 0000000000..160d5cc4ba
--- /dev/null
+++ b/dom/media/ipc/RemoteVideoDecoder.h
@@ -0,0 +1,80 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef include_dom_media_ipc_RemoteVideoDecoderChild_h
+#define include_dom_media_ipc_RemoteVideoDecoderChild_h
+#include "RemoteDecoderChild.h"
+#include "RemoteDecoderManagerChild.h"
+#include "RemoteDecoderParent.h"
+
+namespace mozilla::layers {
+class BufferRecycleBin;
+} // namespace mozilla::layers
+
+namespace mozilla {
+
+class KnowsCompositorVideo : public layers::KnowsCompositor {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(KnowsCompositorVideo, override)
+
+ layers::TextureForwarder* GetTextureForwarder() override;
+ layers::LayersIPCActor* GetLayersIPCActor() override;
+
+ static already_AddRefed<KnowsCompositorVideo> TryCreateForIdentifier(
+ const layers::TextureFactoryIdentifier& aIdentifier);
+
+ private:
+ KnowsCompositorVideo() = default;
+ virtual ~KnowsCompositorVideo() = default;
+};
+
+using mozilla::ipc::IPCResult;
+
+class RemoteVideoDecoderChild : public RemoteDecoderChild {
+ public:
+ explicit RemoteVideoDecoderChild(RemoteDecodeIn aLocation);
+
+ MOZ_IS_CLASS_INIT MediaResult
+ InitIPDL(const VideoInfo& aVideoInfo, float aFramerate,
+ const CreateDecoderParams::OptionSet& aOptions,
+ mozilla::Maybe<layers::TextureFactoryIdentifier> aIdentifier,
+ const Maybe<uint64_t>& aMediaEngineId,
+ const Maybe<TrackingId>& aTrackingId);
+
+ MediaResult ProcessOutput(DecodedOutputIPDL&& aDecodedData) override;
+
+ private:
+ RefPtr<mozilla::layers::BufferRecycleBin> mBufferRecycleBin;
+};
+
+class RemoteVideoDecoderParent final : public RemoteDecoderParent {
+ public:
+ RemoteVideoDecoderParent(
+ RemoteDecoderManagerParent* aParent, const VideoInfo& aVideoInfo,
+ float aFramerate, const CreateDecoderParams::OptionSet& aOptions,
+ const Maybe<layers::TextureFactoryIdentifier>& aIdentifier,
+ nsISerialEventTarget* aManagerThread, TaskQueue* aDecodeTaskQueue,
+ const Maybe<uint64_t>& aMediaEngineId, Maybe<TrackingId> aTrackingId);
+
+ protected:
+ IPCResult RecvConstruct(ConstructResolver&& aResolver) override;
+
+ MediaResult ProcessDecodedData(MediaDataDecoder::DecodedData&& aData,
+ DecodedOutputIPDL& aDecodedData) override;
+
+ private:
+ // Can only be accessed from the manager thread
+ // Note: we can't keep a reference to the original VideoInfo here
+ // because unlike in typical MediaDataDecoder situations, we're being
+ // passed a deserialized VideoInfo from RecvPRemoteDecoderConstructor
+ // which is destroyed when RecvPRemoteDecoderConstructor returns.
+ const VideoInfo mVideoInfo;
+ const float mFramerate;
+ RefPtr<KnowsCompositorVideo> mKnowsCompositor;
+};
+
+} // namespace mozilla
+
+#endif // include_dom_media_ipc_RemoteVideoDecoderChild_h
diff --git a/dom/media/ipc/ShmemRecycleAllocator.h b/dom/media/ipc/ShmemRecycleAllocator.h
new file mode 100644
index 0000000000..7207e27014
--- /dev/null
+++ b/dom/media/ipc/ShmemRecycleAllocator.h
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef include_dom_media_ipc_ShmemRecycleAllocator_h
+#define include_dom_media_ipc_ShmemRecycleAllocator_h
+
+#include "mozilla/ShmemPool.h"
+
+namespace mozilla {
+
+template <class T>
+class ShmemRecycleAllocator {
+ public:
+ explicit ShmemRecycleAllocator(T* aActor)
+ : mActor(aActor), mPool(1, ShmemPool::PoolType::DynamicPool) {}
+ ShmemBuffer AllocateBuffer(size_t aSize,
+ ShmemPool::AllocationPolicy aPolicy =
+ ShmemPool::AllocationPolicy::Unsafe) {
+ ShmemBuffer buffer = mPool.Get(mActor, aSize, aPolicy);
+ if (!buffer.Valid()) {
+ return buffer;
+ }
+ MOZ_DIAGNOSTIC_ASSERT(aSize <= buffer.Get().Size<uint8_t>());
+ mUsedShmems.AppendElement(buffer.Get());
+ mNeedCleanup = true;
+ return buffer;
+ }
+
+ void ReleaseBuffer(ShmemBuffer&& aBuffer) { mPool.Put(std::move(aBuffer)); }
+
+ void ReleaseAllBuffers() {
+ for (auto&& mem : mUsedShmems) {
+ ReleaseBuffer(ShmemBuffer(mem.Get()));
+ }
+ mUsedShmems.Clear();
+ }
+
+ void CleanupShmemRecycleAllocator() {
+ ReleaseAllBuffers();
+ mPool.Cleanup(mActor);
+ mNeedCleanup = false;
+ }
+
+ ~ShmemRecycleAllocator() {
+ MOZ_DIAGNOSTIC_ASSERT(mUsedShmems.IsEmpty() && !mNeedCleanup,
+ "Shmems not all deallocated");
+ }
+
+ private:
+ T* const mActor;
+ ShmemPool mPool;
+ AutoTArray<ShmemBuffer, 4> mUsedShmems;
+ bool mNeedCleanup = false;
+};
+
+} // namespace mozilla
+
+#endif // include_dom_media_ipc_ShmemRecycleAllocator_h
diff --git a/dom/media/ipc/moz.build b/dom/media/ipc/moz.build
new file mode 100644
index 0000000000..a9440780ce
--- /dev/null
+++ b/dom/media/ipc/moz.build
@@ -0,0 +1,107 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+IPDL_SOURCES += [
+ "PMediaDecoderParams.ipdlh",
+ "PRemoteDecoder.ipdl",
+]
+
+PREPROCESSED_IPDL_SOURCES += [
+ "PRDD.ipdl",
+ "PRemoteDecoderManager.ipdl",
+]
+
+EXPORTS.mozilla += [
+ "RDDChild.h",
+ "RDDParent.h",
+ "RDDProcessHost.h",
+ "RDDProcessImpl.h",
+ "RDDProcessManager.h",
+ "RemoteDecoderChild.h",
+ "RemoteDecoderManagerChild.h",
+ "RemoteDecoderManagerParent.h",
+ "RemoteDecoderModule.h",
+ "RemoteDecoderParent.h",
+ "RemoteDecodeUtils.h",
+ "RemoteImageHolder.h",
+ "RemoteMediaData.h",
+ "RemoteMediaDataDecoder.h",
+ "ShmemRecycleAllocator.h",
+]
+
+EXPORTS.mozilla.dom += [
+ "MediaIPCUtils.h",
+ "MFCDMSerializers.h",
+]
+
+SOURCES += [
+ "RDDChild.cpp",
+ "RDDParent.cpp",
+ "RDDProcessHost.cpp",
+ "RDDProcessImpl.cpp",
+ "RDDProcessManager.cpp",
+ "RemoteAudioDecoder.cpp",
+ "RemoteDecoderChild.cpp",
+ "RemoteDecoderManagerChild.cpp",
+ "RemoteDecoderManagerParent.cpp",
+ "RemoteDecoderModule.cpp",
+ "RemoteDecoderParent.cpp",
+ "RemoteDecodeUtils.cpp",
+ "RemoteImageHolder.cpp",
+ "RemoteMediaData.cpp",
+ "RemoteMediaDataDecoder.cpp",
+ "RemoteVideoDecoder.cpp",
+]
+
+if CONFIG["MOZ_WMF_MEDIA_ENGINE"]:
+ IPDL_SOURCES += [
+ "PMFMediaEngine.ipdl",
+ ]
+ SOURCES += [
+ "MFMediaEngineChild.cpp",
+ "MFMediaEngineParent.cpp",
+ "MFMediaEngineUtils.cpp",
+ ]
+ EXPORTS.mozilla += [
+ "MFMediaEngineChild.h",
+ "MFMediaEngineParent.h",
+ "MFMediaEngineUtils.h",
+ ]
+ LOCAL_INCLUDES += [
+ "../platforms/wmf",
+ ]
+
+if CONFIG["MOZ_WMF_CDM"]:
+ IPDL_SOURCES += [
+ "PMFCDM.ipdl",
+ ]
+ EXPORTS.mozilla += [
+ "MFCDMChild.h",
+ "MFCDMParent.h",
+ ]
+ UNIFIED_SOURCES += [
+ "MFCDMChild.cpp",
+ ]
+ SOURCES += [
+ "MFCDMParent.cpp",
+ ]
+ LOCAL_INCLUDES += [
+ "../eme/mediafoundation",
+ ]
+
+# so we can include nsMacUtilsImpl.h in RDDParent.cpp for sandboxing
+LOCAL_INCLUDES += [
+ "/xpcom/base",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+# Add libFuzzer configuration directives
+include("/tools/fuzzing/libfuzzer-config.mozbuild")
+
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/mediacapabilities/BenchmarkStorageChild.cpp b/dom/media/mediacapabilities/BenchmarkStorageChild.cpp
new file mode 100644
index 0000000000..9482f5bffe
--- /dev/null
+++ b/dom/media/mediacapabilities/BenchmarkStorageChild.cpp
@@ -0,0 +1,34 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "BenchmarkStorageChild.h"
+#include "mozilla/dom/ContentChild.h"
+
+namespace mozilla {
+
+static PBenchmarkStorageChild* sChild = nullptr;
+
+/* static */
+PBenchmarkStorageChild* BenchmarkStorageChild::Instance() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!sChild) {
+ sChild = new BenchmarkStorageChild();
+ PContentChild* contentChild = dom::ContentChild::GetSingleton();
+ MOZ_ASSERT(contentChild);
+ contentChild->SendPBenchmarkStorageConstructor();
+ }
+ MOZ_ASSERT(sChild);
+ return sChild;
+}
+
+BenchmarkStorageChild::~BenchmarkStorageChild() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (sChild == this) {
+ sChild = nullptr;
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/mediacapabilities/BenchmarkStorageChild.h b/dom/media/mediacapabilities/BenchmarkStorageChild.h
new file mode 100644
index 0000000000..c53571e13b
--- /dev/null
+++ b/dom/media/mediacapabilities/BenchmarkStorageChild.h
@@ -0,0 +1,28 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef include_dom_media_mediacapabilities_BenchmarkStorageChild_h
+#define include_dom_media_mediacapabilities_BenchmarkStorageChild_h
+
+#include "mozilla/PBenchmarkStorageChild.h"
+
+namespace mozilla {
+
+class BenchmarkStorageChild : public PBenchmarkStorageChild {
+ public:
+ /* Singleton class to avoid recreating the protocol every time we need access
+ * to the storage. */
+ static PBenchmarkStorageChild* Instance();
+
+ ~BenchmarkStorageChild();
+
+ private:
+ BenchmarkStorageChild() = default;
+};
+
+} // namespace mozilla
+
+#endif // include_dom_media_mediacapabilities_BenchmarkStorageChild_h
diff --git a/dom/media/mediacapabilities/BenchmarkStorageParent.cpp b/dom/media/mediacapabilities/BenchmarkStorageParent.cpp
new file mode 100644
index 0000000000..64ae6ddd44
--- /dev/null
+++ b/dom/media/mediacapabilities/BenchmarkStorageParent.cpp
@@ -0,0 +1,130 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "BenchmarkStorageParent.h"
+#include "KeyValueStorage.h"
+
+namespace mozilla {
+
+/* Moving average window size. */
+const int32_t AVG_WINDOW = 20;
+/* Calculate the moving average for the new value aValue for the current window
+ * aWindow and the already existing average aAverage. When the method returns
+ * aAverage will contain the new average and aWindow will contain the new
+ * window.*/
+void BenchmarkStorageParent::MovingAverage(int32_t& aAverage, int32_t& aWindow,
+ const int32_t aValue) {
+ if (aWindow < AVG_WINDOW) {
+ aAverage = (aAverage * aWindow + aValue) / (aWindow + 1);
+ aWindow++;
+ return;
+ }
+ MOZ_ASSERT(aWindow == AVG_WINDOW);
+ aAverage = (aAverage - aAverage / aWindow) + (aValue / aWindow);
+}
+
+/* In order to decrease the number of times the database is accessed when the
+ * moving average is stored or retrieved we use the same value to store _both_
+ * the window and the average. The range of the average is limited since it is
+ * a percentage (0-100), and the range of the window is limited
+ * (1-20). Thus the number that is stored in the database is in the form
+ * (digits): WWAAA. For example, the value stored when an average(A) of 88
+ * corresponds to a window(W) 7 is 7088. The average of 100 that corresponds to
+ * a window of 20 is 20100. The following methods are helpers to extract or
+ * construct the stored value according to the above. */
+
+/* Stored value will be in the form WWAAA(19098). We need to extract the window
+ * (19) and the average score (98). The aValue will
+ * be parsed, the aWindow will contain the window (of the moving average) and
+ * the return value will contain the average itself. */
+int32_t BenchmarkStorageParent::ParseStoredValue(int32_t aValue,
+ int32_t& aWindow) {
+ MOZ_ASSERT(aValue > 999);
+ MOZ_ASSERT(aValue < 100000);
+
+ int32_t score = aValue % 1000;
+ aWindow = (aValue / 1000) % 100;
+ return score;
+}
+
+int32_t BenchmarkStorageParent::PrepareStoredValue(int32_t aScore,
+ int32_t aWindow) {
+ MOZ_ASSERT(aScore >= 0);
+ MOZ_ASSERT(aScore <= 100);
+ MOZ_ASSERT(aWindow > 0);
+ MOZ_ASSERT(aWindow < 21);
+
+ return aWindow * 1000 + aScore;
+}
+
+BenchmarkStorageParent::BenchmarkStorageParent()
+ : mStorage(new KeyValueStorage) {}
+
+IPCResult BenchmarkStorageParent::RecvPut(const nsCString& aDbName,
+ const nsCString& aKey,
+ const int32_t& aValue) {
+ // In order to calculate and store the new moving average, we need to get the
+ // stored value and window first, to calculate the new score and window, and
+ // then to store the new aggregated value.
+ mStorage->Get(aDbName, aKey)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [storage = mStorage, aDbName, aKey, aValue](int32_t aResult) {
+ int32_t window = 0;
+ int32_t average = 0;
+ if (aResult >= 0) {
+ // The key found.
+ average = ParseStoredValue(aResult, window);
+ }
+ MovingAverage(average, window, aValue);
+ int32_t newValue = PrepareStoredValue(average, window);
+ // Avoid storing if the values are the same. This is an optimization
+ // to minimize the disk usage.
+ if (aResult != newValue) {
+ storage->Put(aDbName, aKey, newValue);
+ }
+ },
+ [](nsresult rv) { /*do nothing*/ });
+
+ return IPC_OK();
+}
+
+IPCResult BenchmarkStorageParent::RecvGet(const nsCString& aDbName,
+ const nsCString& aKey,
+ GetResolver&& aResolve) {
+ mStorage->Get(aDbName, aKey)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [aResolve](int32_t aResult) {
+ int32_t window = 0; // not used
+ aResolve(aResult < 0 ? -1 : ParseStoredValue(aResult, window));
+ },
+ [aResolve](nsresult rv) { aResolve(-1); });
+
+ return IPC_OK();
+}
+
+IPCResult BenchmarkStorageParent::RecvCheckVersion(const nsCString& aDbName,
+ int32_t aVersion) {
+ mStorage->Get(aDbName, "Version"_ns)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [storage = mStorage, aDbName, aVersion](int32_t aResult) {
+ if (aVersion != aResult) {
+ storage->Clear(aDbName)->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [storage, aDbName, aVersion](bool) {
+ storage->Put(aDbName, "Version"_ns, aVersion);
+ },
+ [](nsresult rv) { /*do nothing*/ });
+ }
+ },
+ [](nsresult rv) { /*do nothing*/ });
+
+ return IPC_OK();
+}
+
+}; // namespace mozilla
diff --git a/dom/media/mediacapabilities/BenchmarkStorageParent.h b/dom/media/mediacapabilities/BenchmarkStorageParent.h
new file mode 100644
index 0000000000..e9c8f9dd9b
--- /dev/null
+++ b/dom/media/mediacapabilities/BenchmarkStorageParent.h
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef include_dom_media_mediacapabilities_BenchmarkStorageParent_h
+#define include_dom_media_mediacapabilities_BenchmarkStorageParent_h
+
+#include "mozilla/PBenchmarkStorageParent.h"
+
+namespace mozilla {
+class KeyValueStorage;
+
+using mozilla::ipc::IPCResult;
+
+class BenchmarkStorageParent : public PBenchmarkStorageParent {
+ friend class PBenchmarkStorageParent;
+
+ public:
+ BenchmarkStorageParent();
+
+ IPCResult RecvPut(const nsCString& aDbName, const nsCString& aKey,
+ const int32_t& aValue);
+
+ IPCResult RecvGet(const nsCString& aDbName, const nsCString& aKey,
+ GetResolver&& aResolve);
+
+ IPCResult RecvCheckVersion(const nsCString& aDbName, int32_t aVersion);
+
+ /* Helper methods exposed here to be tested via gtest. */
+ static void MovingAverage(int32_t& aAverage, int32_t& aWindow,
+ const int32_t aValue);
+ static int32_t ParseStoredValue(int32_t aValue, int32_t& aWindow);
+ static int32_t PrepareStoredValue(int32_t aScore, int32_t aWindow);
+
+ private:
+ RefPtr<KeyValueStorage> mStorage;
+};
+
+} // namespace mozilla
+
+#endif // include_dom_media_mediacapabilities_BenchmarkStorageParent_h
diff --git a/dom/media/mediacapabilities/DecoderBenchmark.cpp b/dom/media/mediacapabilities/DecoderBenchmark.cpp
new file mode 100644
index 0000000000..b5c9a2a693
--- /dev/null
+++ b/dom/media/mediacapabilities/DecoderBenchmark.cpp
@@ -0,0 +1,243 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DecoderBenchmark.h"
+#include "mozilla/BenchmarkStorageChild.h"
+#include "mozilla/media/MediaUtils.h"
+#include "mozilla/StaticPrefs_media.h"
+
+namespace mozilla {
+
+void DecoderBenchmark::StoreScore(const nsACString& aDecoderName,
+ const nsACString& aKey,
+ RefPtr<FrameStatistics> aStats) {
+ FrameStatisticsData statsData = aStats->GetFrameStatisticsData();
+ uint64_t totalFrames = FrameStatistics::GetTotalFrames(statsData);
+ uint64_t droppedFrames = FrameStatistics::GetDroppedFrames(statsData);
+
+ MOZ_ASSERT(droppedFrames <= totalFrames);
+ MOZ_ASSERT(totalFrames >= mLastTotalFrames);
+ MOZ_ASSERT(droppedFrames >= mLastDroppedFrames);
+
+ uint64_t diffTotalFrames = totalFrames - mLastTotalFrames;
+ uint64_t diffDroppedFrames = droppedFrames - mLastDroppedFrames;
+
+ /* Update now in case the method returns at the if check bellow. */
+ mLastTotalFrames = totalFrames;
+ mLastDroppedFrames = droppedFrames;
+
+ /* A minimum number of 10 frames is required to store the score. */
+ if (diffTotalFrames < 10) {
+ return;
+ }
+
+ int32_t percentage =
+ 100 - 100 * float(diffDroppedFrames) / float(diffTotalFrames);
+
+ MOZ_ASSERT(percentage >= 0);
+
+ Put(aDecoderName, aKey, percentage);
+}
+
+void DecoderBenchmark::Put(const nsACString& aDecoderName,
+ const nsACString& aKey, int32_t aValue) {
+ MOZ_ASSERT(NS_IsMainThread());
+ const nsCString name(aDecoderName);
+ const nsCString key(aKey);
+ BenchmarkStorageChild::Instance()->SendPut(name, key, aValue);
+}
+
+RefPtr<BenchmarkScorePromise> DecoderBenchmark::GetScore(
+ const nsACString& aDecoderName, const nsACString& aKey) {
+ if (NS_IsMainThread()) {
+ return Get(aDecoderName, aKey);
+ }
+
+ RefPtr<DecoderBenchmark> self = this;
+ const nsCString decoderName(aDecoderName);
+ const nsCString key(aKey);
+ return InvokeAsync(
+ GetMainThreadSerialEventTarget(), __func__,
+ [self, decoderName, key] { return self->Get(decoderName, key); });
+}
+
+RefPtr<BenchmarkScorePromise> DecoderBenchmark::Get(
+ const nsACString& aDecoderName, const nsACString& aKey) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ const nsCString name(aDecoderName);
+ const nsCString key(aKey);
+ return BenchmarkStorageChild::Instance()->SendGet(name, key)->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [](int32_t aResult) {
+ return BenchmarkScorePromise::CreateAndResolve(aResult, __func__);
+ },
+ [](ipc::ResponseRejectReason&&) {
+ return BenchmarkScorePromise::CreateAndReject(NS_ERROR_FAILURE,
+ __func__);
+ });
+}
+
+/* The key string consists of the video properties resolution, framerate,
+ * and bitdepth. There are various levels for each of them. The key is
+ * formated by the closest level, for example, a video with width=1920,
+ * height=1080, frameRate=24, and bitdepth=8bit will have the key:
+ * "ResolutionLevel5-FrameRateLevel1-8bit". */
+
+#define NELEMS(x) (sizeof(x) / sizeof((x)[0]))
+
+/* Keep them sorted */
+const uint32_t PixelLevels[] = {
+ /* 256x144 =*/36864,
+ /* 426x240 =*/102240,
+ /* 640x360 =*/230400,
+ /* 854x480 =*/409920,
+ /* 1280x720 =*/921600,
+ /* 1920x1080 =*/2073600,
+ /* 2560x1440 =*/3686400,
+ /* 3840x2160 =*/8294400,
+};
+const size_t PixelLevelsSize = NELEMS(PixelLevels);
+
+const uint32_t FrameRateLevels[] = {
+ 15, 24, 30, 50, 60,
+};
+const size_t FrameRateLevelsSize = NELEMS(FrameRateLevels);
+
+/* static */
+nsCString KeyUtil::FindLevel(const uint32_t aLevels[], const size_t length,
+ uint32_t aValue) {
+ MOZ_ASSERT(aValue);
+ if (aValue <= aLevels[0]) {
+ return "Level0"_ns;
+ }
+ nsAutoCString level("Level");
+ size_t lastIndex = length - 1;
+ if (aValue >= aLevels[lastIndex]) {
+ level.AppendInt(static_cast<uint32_t>(lastIndex));
+ return std::move(level);
+ }
+ for (size_t i = 0; i < lastIndex; ++i) {
+ if (aValue >= aLevels[i + 1]) {
+ continue;
+ }
+ if (aValue - aLevels[i] < aLevels[i + 1] - aValue) {
+ level.AppendInt(static_cast<uint32_t>(i));
+ return std::move(level);
+ }
+ level.AppendInt(static_cast<uint32_t>(i + 1));
+ return std::move(level);
+ }
+ MOZ_CRASH("Array is not sorted");
+ return ""_ns;
+}
+
+/* static */
+nsCString KeyUtil::BitDepthToStr(uint8_t aBitDepth) {
+ switch (aBitDepth) {
+ case 8: // ColorDepth::COLOR_8
+ return "-8bit"_ns;
+ case 10: // ColorDepth::COLOR_10
+ case 12: // ColorDepth::COLOR_12
+ case 16: // ColorDepth::COLOR_16
+ return "-non8bit"_ns;
+ }
+ MOZ_ASSERT_UNREACHABLE("invalid color depth value");
+ return ""_ns;
+}
+
+/* static */
+nsCString KeyUtil::CreateKey(const DecoderBenchmarkInfo& aBenchInfo) {
+ nsAutoCString key("Resolution");
+ key.Append(FindLevel(PixelLevels, PixelLevelsSize,
+ aBenchInfo.mWidth * aBenchInfo.mHeight));
+
+ key.Append("-FrameRate");
+ key.Append(
+ FindLevel(FrameRateLevels, FrameRateLevelsSize, aBenchInfo.mFrameRate));
+
+ key.Append(BitDepthToStr(aBenchInfo.mBitDepth));
+
+ return std::move(key);
+}
+
+void DecoderBenchmark::Store(const DecoderBenchmarkInfo& aBenchInfo,
+ RefPtr<FrameStatistics> aStats) {
+ if (!XRE_IsContentProcess()) {
+ NS_WARNING(
+ "Storing a benchmark is only allowed only from the content process.");
+ return;
+ }
+ StoreScore(aBenchInfo.mContentType, KeyUtil::CreateKey(aBenchInfo), aStats);
+}
+
+/* static */
+RefPtr<BenchmarkScorePromise> DecoderBenchmark::Get(
+ const DecoderBenchmarkInfo& aBenchInfo) {
+ if (!XRE_IsContentProcess()) {
+ NS_WARNING(
+ "Getting a benchmark is only allowed only from the content process.");
+ return BenchmarkScorePromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+ // There is no need for any of the data members to query the database, thus
+ // it can be a static method.
+ auto bench = MakeRefPtr<DecoderBenchmark>();
+ return bench->GetScore(aBenchInfo.mContentType,
+ KeyUtil::CreateKey(aBenchInfo));
+}
+
+static nsTHashMap<nsCStringHashKey, int32_t> DecoderVersionTable() {
+ nsTHashMap<nsCStringHashKey, int32_t> decoderVersionTable;
+
+ /*
+ * For the decoders listed here, the benchmark version number will be checked.
+ * If the version number does not exist in the database or is different than
+ * the version number listed here, all the benchmark entries for this decoder
+ * will be erased. An example of assigning the version number `1` for AV1
+ * decoder is:
+ *
+ * decoderVersionTable.InsertOrUpdate("video/av1"_ns, 1);
+ *
+ * For the decoders not listed here the `CheckVersion` method exits early, to
+ * avoid sending unecessary IPC messages.
+ */
+
+ return decoderVersionTable;
+}
+
+/* static */
+void DecoderBenchmark::CheckVersion(const nsACString& aDecoderName) {
+ if (!XRE_IsContentProcess()) {
+ NS_WARNING(
+ "Checking version is only allowed only from the content process.");
+ return;
+ }
+
+ if (!StaticPrefs::media_mediacapabilities_from_database()) {
+ return;
+ }
+
+ nsCString name(aDecoderName);
+ int32_t version;
+ if (!DecoderVersionTable().Get(name, &version)) {
+ // A version is not set for that decoder ignore.
+ return;
+ }
+
+ if (NS_IsMainThread()) {
+ BenchmarkStorageChild::Instance()->SendCheckVersion(name, version);
+ return;
+ }
+
+ DebugOnly<nsresult> rv =
+ GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction(
+ "DecoderBenchmark::CheckVersion", [name, version]() {
+ BenchmarkStorageChild::Instance()->SendCheckVersion(name, version);
+ }));
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+}
+
+} // namespace mozilla
diff --git a/dom/media/mediacapabilities/DecoderBenchmark.h b/dom/media/mediacapabilities/DecoderBenchmark.h
new file mode 100644
index 0000000000..af730fcef3
--- /dev/null
+++ b/dom/media/mediacapabilities/DecoderBenchmark.h
@@ -0,0 +1,77 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_DECODER_BENCHMARK_H
+#define MOZILLA_DECODER_BENCHMARK_H
+
+#include "FrameStatistics.h"
+#include "mozilla/BenchmarkStorageChild.h"
+#include "mozilla/KeyValueStorage.h"
+
+namespace mozilla {
+
+typedef KeyValueStorage::GetPromise BenchmarkScorePromise;
+
+struct DecoderBenchmarkInfo final {
+ const nsCString mContentType;
+ const int32_t mWidth;
+ const int32_t mHeight;
+ const int32_t mFrameRate;
+ const uint32_t mBitDepth;
+};
+
+class DecoderBenchmark final {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DecoderBenchmark)
+
+ public:
+ void Store(const DecoderBenchmarkInfo& aBenchInfo,
+ RefPtr<FrameStatistics> aStats);
+
+ static RefPtr<BenchmarkScorePromise> Get(
+ const DecoderBenchmarkInfo& aBenchInfo);
+
+ /* For the specific decoder, specified by aDecoderName, it compares the
+ * version number, from a static list of versions, to the version number
+ * found in the database. If those numbers are different all benchmark
+ * entries for that decoder are deleted. */
+ static void CheckVersion(const nsACString& aDecoderName);
+
+ private:
+ void StoreScore(const nsACString& aDecoderName, const nsACString& aKey,
+ RefPtr<FrameStatistics> aStats);
+
+ RefPtr<BenchmarkScorePromise> GetScore(const nsACString& aDecoderName,
+ const nsACString& aKey);
+
+ void Put(const nsACString& aDecoderName, const nsACString& aKey,
+ int32_t aValue);
+
+ RefPtr<BenchmarkScorePromise> Get(const nsACString& aDecoderName,
+ const nsACString& aKey);
+ ~DecoderBenchmark() = default;
+
+ // Keep the last TotalFrames and DroppedFrames from FrameStatistics.
+ // FrameStatistics keep an ever-increasing counter across the entire video and
+ // even when there are resolution changes. This code is called whenever there
+ // is a resolution change and we need to calculate the benchmark since the
+ // last call.
+ uint64_t mLastTotalFrames = 0;
+ uint64_t mLastDroppedFrames = 0;
+};
+
+class KeyUtil {
+ public:
+ static nsCString CreateKey(const DecoderBenchmarkInfo& aBenchInfo);
+
+ private:
+ static nsCString BitDepthToStr(uint8_t aBitDepth);
+ static nsCString FindLevel(const uint32_t aLevels[], const size_t length,
+ uint32_t aValue);
+};
+
+} // namespace mozilla
+
+#endif // MOZILLA_DECODER_BENCHMARK_H
diff --git a/dom/media/mediacapabilities/KeyValueStorage.cpp b/dom/media/mediacapabilities/KeyValueStorage.cpp
new file mode 100644
index 0000000000..f0ac0aad7d
--- /dev/null
+++ b/dom/media/mediacapabilities/KeyValueStorage.cpp
@@ -0,0 +1,234 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "KeyValueStorage.h"
+
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsVariant.h"
+
+namespace mozilla {
+
+class DatabaseCallback final : public nsIKeyValueDatabaseCallback {
+ public:
+ NS_DECL_ISUPPORTS
+
+ explicit DatabaseCallback(RefPtr<nsIKeyValueDatabase>& aDatabase)
+ : mDatabase(aDatabase) {}
+ NS_IMETHOD Resolve(nsIKeyValueDatabase* aDatabase) override {
+ if (!aDatabase) {
+ mResultPromise.Reject(NS_ERROR_FAILURE, __func__);
+ }
+ mDatabase = aDatabase;
+ mResultPromise.Resolve(true, __func__);
+ return NS_OK;
+ }
+ NS_IMETHOD Reject(const nsACString&) override {
+ mResultPromise.Reject(NS_ERROR_FAILURE, __func__);
+ return NS_OK;
+ }
+ RefPtr<GenericPromise> Ensure() { return mResultPromise.Ensure(__func__); }
+
+ protected:
+ ~DatabaseCallback() = default;
+ RefPtr<nsIKeyValueDatabase>& mDatabase;
+ MozPromiseHolder<GenericPromise> mResultPromise;
+};
+NS_IMPL_ISUPPORTS(DatabaseCallback, nsIKeyValueDatabaseCallback);
+
+RefPtr<GenericPromise> KeyValueStorage::Init() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!mDatabaseName.IsEmpty());
+ nsresult rv;
+
+ nsCOMPtr<nsIFile> profileDir;
+ rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(profileDir));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return GenericPromise::CreateAndReject(rv, __func__);
+ }
+ MOZ_ASSERT(profileDir);
+
+ rv = profileDir->AppendNative("mediacapabilities"_ns);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return GenericPromise::CreateAndReject(rv, __func__);
+ }
+
+ rv = profileDir->Create(nsIFile::DIRECTORY_TYPE, 0700);
+ if (rv != NS_ERROR_FILE_ALREADY_EXISTS && NS_WARN_IF(NS_FAILED(rv))) {
+ return GenericPromise::CreateAndReject(rv, __func__);
+ }
+
+ nsCOMPtr<nsIKeyValueService> keyValueService =
+ do_GetService("@mozilla.org/key-value-service;1", &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return GenericPromise::CreateAndReject(rv, __func__);
+ }
+ MOZ_ASSERT(keyValueService);
+
+ auto callback = MakeRefPtr<DatabaseCallback>(mDatabase);
+
+ nsString path;
+ profileDir->GetPath(path);
+ keyValueService->GetOrCreate(callback, NS_ConvertUTF16toUTF8(path),
+ mDatabaseName);
+ return callback->Ensure();
+}
+
+class VoidCallback final : public nsIKeyValueVoidCallback {
+ public:
+ NS_DECL_ISUPPORTS
+
+ explicit VoidCallback(const RefPtr<KeyValueStorage>& aOwner)
+ : nsIKeyValueVoidCallback(), mOwner(aOwner) {}
+
+ NS_IMETHOD Resolve() override {
+ mResultPromise.Resolve(true, __func__);
+ return NS_OK;
+ }
+ NS_IMETHOD Reject(const nsACString&) override {
+ mResultPromise.Reject(NS_ERROR_FAILURE, __func__);
+ return NS_OK;
+ }
+ RefPtr<GenericPromise> Ensure(const char* aMethodName) {
+ return mResultPromise.Ensure(aMethodName);
+ }
+
+ protected:
+ ~VoidCallback() = default;
+ MozPromiseHolder<GenericPromise> mResultPromise;
+ RefPtr<KeyValueStorage> mOwner;
+};
+NS_IMPL_ISUPPORTS(VoidCallback, nsIKeyValueVoidCallback);
+
+RefPtr<GenericPromise> KeyValueStorage::Put(const nsACString& aKey,
+ int32_t aValue) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mDatabase);
+ MOZ_ASSERT(!mDatabaseName.IsEmpty());
+
+ auto value = MakeRefPtr<nsVariant>();
+ nsresult rv = value->SetAsInt32(aValue);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return GenericPromise::CreateAndReject(rv, __func__);
+ }
+
+ auto callback = MakeRefPtr<VoidCallback>(this);
+ rv = mDatabase->Put(callback, aKey, value);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return GenericPromise::CreateAndReject(rv, __func__);
+ }
+ return callback->Ensure(__func__);
+}
+
+RefPtr<GenericPromise> KeyValueStorage::Put(const nsACString& aName,
+ const nsACString& aKey,
+ int32_t aValue) {
+ if (!mDatabase || !mDatabaseName.Equals(aName)) {
+ mDatabaseName = aName;
+ RefPtr<KeyValueStorage> self = this;
+ const nsCString key(aKey);
+ return Init()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self, key, aValue](bool) { return self->Put(key, aValue); },
+ [](nsresult rv) {
+ return GenericPromise::CreateAndReject(rv, __func__);
+ });
+ }
+ return Put(aKey, aValue);
+}
+
+class GetValueCallback final : public nsIKeyValueVariantCallback {
+ public:
+ NS_DECL_ISUPPORTS
+
+ NS_IMETHOD Resolve(nsIVariant* aResult) override {
+ int32_t value = 0;
+ Unused << aResult->GetAsInt32(&value);
+ mResultPromise.Resolve(value, __func__);
+ return NS_OK;
+ }
+ NS_IMETHOD Reject(const nsACString&) override {
+ mResultPromise.Reject(NS_ERROR_FAILURE, __func__);
+ return NS_OK;
+ }
+ RefPtr<KeyValueStorage::GetPromise> Ensure() {
+ return mResultPromise.Ensure(__func__);
+ }
+
+ protected:
+ ~GetValueCallback() = default;
+
+ private:
+ MozPromiseHolder<KeyValueStorage::GetPromise> mResultPromise;
+};
+NS_IMPL_ISUPPORTS(GetValueCallback, nsIKeyValueVariantCallback);
+
+RefPtr<KeyValueStorage::GetPromise> KeyValueStorage::Get(
+ const nsACString& aKey) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mDatabase);
+ MOZ_ASSERT(!mDatabaseName.IsEmpty());
+
+ RefPtr<nsVariant> defaultValue = new nsVariant;
+ nsresult rv = defaultValue->SetAsInt32(-1);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return KeyValueStorage::GetPromise::CreateAndReject(rv, __func__);
+ }
+
+ auto callback = MakeRefPtr<GetValueCallback>();
+ rv = mDatabase->Get(callback, aKey, defaultValue);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return KeyValueStorage::GetPromise::CreateAndReject(rv, __func__);
+ }
+ return callback->Ensure();
+}
+
+RefPtr<KeyValueStorage::GetPromise> KeyValueStorage::Get(
+ const nsACString& aName, const nsACString& aKey) {
+ if (!mDatabase || !mDatabaseName.Equals(aName)) {
+ mDatabaseName = aName;
+ RefPtr<KeyValueStorage> self = this;
+ const nsCString key(aKey);
+ return Init()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self, key](bool) { return self->Get(key); },
+ [](nsresult rv) {
+ return KeyValueStorage::GetPromise::CreateAndReject(rv, __func__);
+ });
+ }
+ return Get(aKey);
+}
+
+RefPtr<GenericPromise> KeyValueStorage::Clear() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mDatabase);
+ MOZ_ASSERT(!mDatabaseName.IsEmpty());
+
+ auto callback = MakeRefPtr<VoidCallback>(this);
+ nsresult rv = mDatabase->Clear(callback);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return GenericPromise::CreateAndReject(rv, __func__);
+ }
+ return callback->Ensure(__func__);
+}
+
+RefPtr<GenericPromise> KeyValueStorage::Clear(const nsACString& aName) {
+ if (!mDatabase || !mDatabaseName.Equals(aName)) {
+ mDatabaseName = aName;
+ RefPtr<KeyValueStorage> self = this;
+ return Init()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self](bool) { return self->Clear(); },
+ [](nsresult rv) {
+ return GenericPromise::CreateAndReject(rv, __func__);
+ });
+ }
+ return Clear();
+}
+
+} // namespace mozilla
diff --git a/dom/media/mediacapabilities/KeyValueStorage.h b/dom/media/mediacapabilities/KeyValueStorage.h
new file mode 100644
index 0000000000..60d72a6c6d
--- /dev/null
+++ b/dom/media/mediacapabilities/KeyValueStorage.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_KEY_VALUE_STORAGE_H
+#define MOZILLA_KEY_VALUE_STORAGE_H
+
+#include "mozilla/MozPromise.h"
+#include "nsIKeyValue.h"
+
+namespace mozilla {
+
+/* A wrapper class around kv store service, which allows storing a pair of key
+ * value permanently. The class must be used from the parent process, where
+ * there is no sandbox because it requires access to the directory that the
+ * database is located. */
+class KeyValueStorage final {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(KeyValueStorage)
+
+ /* Store permanently the value in the Key. */
+ RefPtr<GenericPromise> Put(const nsACString& aName, const nsACString& aKey,
+ int32_t aValue);
+ /* Get the value stored in the aKey. If the aKey does not exist the promise is
+ * resolved with the value -1. */
+ typedef MozPromise<int32_t, nsresult, true> GetPromise;
+ RefPtr<GetPromise> Get(const nsACString& aName, const nsACString& aKey);
+
+ /* Clear all the key/value pairs from the aName database. */
+ RefPtr<GenericPromise> Clear(const nsACString& aName);
+
+ private:
+ /* Create, if doesn't exist, and initialize the database with a given name. */
+ RefPtr<GenericPromise> Init();
+ RefPtr<GenericPromise> Put(const nsACString& aKey, int32_t aValue);
+ RefPtr<GetPromise> Get(const nsACString& aKey);
+ RefPtr<GenericPromise> Clear();
+ ~KeyValueStorage() = default;
+
+ RefPtr<nsIKeyValueDatabase> mDatabase;
+ nsCString mDatabaseName;
+};
+
+} // namespace mozilla
+
+#endif // MOZILLA_KEY_VALUE_STORAGE_H
diff --git a/dom/media/mediacapabilities/MediaCapabilities.cpp b/dom/media/mediacapabilities/MediaCapabilities.cpp
new file mode 100644
index 0000000000..904e9f4f54
--- /dev/null
+++ b/dom/media/mediacapabilities/MediaCapabilities.cpp
@@ -0,0 +1,658 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaCapabilities.h"
+
+#include <inttypes.h>
+
+#include <utility>
+
+#include "AllocationPolicy.h"
+#include "Benchmark.h"
+#include "DecoderBenchmark.h"
+#include "DecoderTraits.h"
+#include "MediaInfo.h"
+#include "MediaRecorder.h"
+#include "PDMFactory.h"
+#include "VPXDecoder.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/TaskQueue.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/DOMMozPromiseRequestHolder.h"
+#include "mozilla/dom/MediaCapabilitiesBinding.h"
+#include "mozilla/dom/MediaSource.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/WorkerPrivate.h"
+#include "mozilla/dom/WorkerRef.h"
+#include "mozilla/layers/KnowsCompositor.h"
+#include "nsContentUtils.h"
+#include "WindowRenderer.h"
+
+static mozilla::LazyLogModule sMediaCapabilitiesLog("MediaCapabilities");
+
+#define LOG(msg, ...) \
+ DDMOZ_LOG(sMediaCapabilitiesLog, LogLevel::Debug, msg, ##__VA_ARGS__)
+
+namespace mozilla::dom {
+
+static nsCString VideoConfigurationToStr(const VideoConfiguration* aConfig) {
+ if (!aConfig) {
+ return nsCString();
+ }
+
+ nsCString hdrMetaType(
+ aConfig->mHdrMetadataType.WasPassed()
+ ? HdrMetadataTypeValues::GetString(aConfig->mHdrMetadataType.Value())
+ : "?");
+
+ nsCString colorGamut(
+ aConfig->mColorGamut.WasPassed()
+ ? ColorGamutValues::GetString(aConfig->mColorGamut.Value())
+ : "?");
+
+ nsCString transferFunction(aConfig->mTransferFunction.WasPassed()
+ ? TransferFunctionValues::GetString(
+ aConfig->mTransferFunction.Value())
+ : "?");
+
+ auto str = nsPrintfCString(
+ "[contentType:%s width:%d height:%d bitrate:%" PRIu64
+ " framerate:%lf hasAlphaChannel:%s hdrMetadataType:%s colorGamut:%s "
+ "transferFunction:%s scalabilityMode:%s]",
+ NS_ConvertUTF16toUTF8(aConfig->mContentType).get(), aConfig->mWidth,
+ aConfig->mHeight, aConfig->mBitrate, aConfig->mFramerate,
+ aConfig->mHasAlphaChannel.WasPassed()
+ ? aConfig->mHasAlphaChannel.Value() ? "true" : "false"
+ : "?",
+ hdrMetaType.get(), colorGamut.get(), transferFunction.get(),
+ aConfig->mScalabilityMode.WasPassed()
+ ? NS_ConvertUTF16toUTF8(aConfig->mScalabilityMode.Value()).get()
+ : "?");
+ return std::move(str);
+}
+
+static nsCString AudioConfigurationToStr(const AudioConfiguration* aConfig) {
+ if (!aConfig) {
+ return nsCString();
+ }
+ auto str = nsPrintfCString(
+ "[contentType:%s channels:%s bitrate:%" PRIu64 " samplerate:%d]",
+ NS_ConvertUTF16toUTF8(aConfig->mContentType).get(),
+ aConfig->mChannels.WasPassed()
+ ? NS_ConvertUTF16toUTF8(aConfig->mChannels.Value()).get()
+ : "?",
+ aConfig->mBitrate.WasPassed() ? aConfig->mBitrate.Value() : 0,
+ aConfig->mSamplerate.WasPassed() ? aConfig->mSamplerate.Value() : 0);
+ return std::move(str);
+}
+
+static nsCString MediaCapabilitiesInfoToStr(
+ const MediaCapabilitiesInfo* aInfo) {
+ if (!aInfo) {
+ return nsCString();
+ }
+ auto str = nsPrintfCString("[supported:%s smooth:%s powerEfficient:%s]",
+ aInfo->Supported() ? "true" : "false",
+ aInfo->Smooth() ? "true" : "false",
+ aInfo->PowerEfficient() ? "true" : "false");
+ return std::move(str);
+}
+
+static nsCString MediaDecodingConfigurationToStr(
+ const MediaDecodingConfiguration& aConfig) {
+ nsCString str;
+ str += "["_ns;
+ if (aConfig.mVideo.WasPassed()) {
+ str += "video:"_ns + VideoConfigurationToStr(&aConfig.mVideo.Value());
+ if (aConfig.mAudio.WasPassed()) {
+ str += " "_ns;
+ }
+ }
+ if (aConfig.mAudio.WasPassed()) {
+ str += "audio:"_ns + AudioConfigurationToStr(&aConfig.mAudio.Value());
+ }
+ str += "]"_ns;
+ return str;
+}
+
+MediaCapabilities::MediaCapabilities(nsIGlobalObject* aParent)
+ : mParent(aParent) {}
+
+already_AddRefed<Promise> MediaCapabilities::DecodingInfo(
+ const MediaDecodingConfiguration& aConfiguration, ErrorResult& aRv) {
+ RefPtr<Promise> promise = Promise::Create(mParent, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ // If configuration is not a valid MediaConfiguration, return a Promise
+ // rejected with a TypeError.
+ if (!aConfiguration.mVideo.WasPassed() &&
+ !aConfiguration.mAudio.WasPassed()) {
+ aRv.ThrowTypeError<MSG_MISSING_REQUIRED_DICTIONARY_MEMBER>(
+ "'audio' or 'video' member of argument of "
+ "MediaCapabilities.decodingInfo");
+ return nullptr;
+ }
+
+ LOG("Processing %s", MediaDecodingConfigurationToStr(aConfiguration).get());
+
+ bool supported = true;
+ Maybe<MediaContainerType> videoContainer;
+ Maybe<MediaContainerType> audioContainer;
+
+ // If configuration.video is present and is not a valid video configuration,
+ // return a Promise rejected with a TypeError.
+ if (aConfiguration.mVideo.WasPassed()) {
+ videoContainer = CheckVideoConfiguration(aConfiguration.mVideo.Value());
+ if (!videoContainer) {
+ aRv.ThrowTypeError<MSG_INVALID_MEDIA_VIDEO_CONFIGURATION>();
+ return nullptr;
+ }
+
+ // We have a video configuration and it is valid. Check if it is supported.
+ supported &=
+ aConfiguration.mType == MediaDecodingType::File
+ ? CheckTypeForFile(aConfiguration.mVideo.Value().mContentType)
+ : CheckTypeForMediaSource(
+ aConfiguration.mVideo.Value().mContentType);
+ }
+ if (aConfiguration.mAudio.WasPassed()) {
+ audioContainer = CheckAudioConfiguration(aConfiguration.mAudio.Value());
+ if (!audioContainer) {
+ aRv.ThrowTypeError<MSG_INVALID_MEDIA_AUDIO_CONFIGURATION>();
+ return nullptr;
+ }
+ // We have an audio configuration and it is valid. Check if it is supported.
+ supported &=
+ aConfiguration.mType == MediaDecodingType::File
+ ? CheckTypeForFile(aConfiguration.mAudio.Value().mContentType)
+ : CheckTypeForMediaSource(
+ aConfiguration.mAudio.Value().mContentType);
+ }
+
+ if (!supported) {
+ auto info = MakeUnique<MediaCapabilitiesInfo>(
+ false /* supported */, false /* smooth */, false /* power efficient */);
+ LOG("%s -> %s", MediaDecodingConfigurationToStr(aConfiguration).get(),
+ MediaCapabilitiesInfoToStr(info.get()).get());
+ promise->MaybeResolve(std::move(info));
+ return promise.forget();
+ }
+
+ nsTArray<UniquePtr<TrackInfo>> tracks;
+ if (aConfiguration.mVideo.WasPassed()) {
+ MOZ_ASSERT(videoContainer.isSome(), "configuration is valid and supported");
+ auto videoTracks = DecoderTraits::GetTracksInfo(*videoContainer);
+ // If the MIME type does not imply a codec, the string MUST
+ // also have one and only one parameter that is named codecs with a value
+ // describing a single media codec. Otherwise, it MUST contain no
+ // parameters.
+ if (videoTracks.Length() != 1) {
+ promise->MaybeRejectWithTypeError<MSG_NO_CODECS_PARAMETER>(
+ videoContainer->OriginalString());
+ return promise.forget();
+ }
+ MOZ_DIAGNOSTIC_ASSERT(videoTracks.ElementAt(0),
+ "must contain a valid trackinfo");
+ // If the type refers to an audio codec, reject now.
+ if (videoTracks[0]->GetType() != TrackInfo::kVideoTrack) {
+ promise
+ ->MaybeRejectWithTypeError<MSG_INVALID_MEDIA_VIDEO_CONFIGURATION>();
+ return promise.forget();
+ }
+ tracks.AppendElements(std::move(videoTracks));
+ }
+ if (aConfiguration.mAudio.WasPassed()) {
+ MOZ_ASSERT(audioContainer.isSome(), "configuration is valid and supported");
+ auto audioTracks = DecoderTraits::GetTracksInfo(*audioContainer);
+ // If the MIME type does not imply a codec, the string MUST
+ // also have one and only one parameter that is named codecs with a value
+ // describing a single media codec. Otherwise, it MUST contain no
+ // parameters.
+ if (audioTracks.Length() != 1) {
+ promise->MaybeRejectWithTypeError<MSG_NO_CODECS_PARAMETER>(
+ audioContainer->OriginalString());
+ return promise.forget();
+ }
+ MOZ_DIAGNOSTIC_ASSERT(audioTracks.ElementAt(0),
+ "must contain a valid trackinfo");
+ // If the type refers to a video codec, reject now.
+ if (audioTracks[0]->GetType() != TrackInfo::kAudioTrack) {
+ promise
+ ->MaybeRejectWithTypeError<MSG_INVALID_MEDIA_AUDIO_CONFIGURATION>();
+ return promise.forget();
+ }
+ tracks.AppendElements(std::move(audioTracks));
+ }
+
+ using CapabilitiesPromise = MozPromise<MediaCapabilitiesInfo, MediaResult,
+ /* IsExclusive = */ true>;
+ nsTArray<RefPtr<CapabilitiesPromise>> promises;
+
+ RefPtr<TaskQueue> taskQueue =
+ TaskQueue::Create(GetMediaThreadPool(MediaThreadType::PLATFORM_DECODER),
+ "MediaCapabilities::TaskQueue");
+ for (auto&& config : tracks) {
+ TrackInfo::TrackType type =
+ config->IsVideo() ? TrackInfo::kVideoTrack : TrackInfo::kAudioTrack;
+
+ MOZ_ASSERT(type == TrackInfo::kAudioTrack ||
+ videoContainer->ExtendedType().GetFramerate().isSome(),
+ "framerate is a required member of VideoConfiguration");
+
+ if (type == TrackInfo::kAudioTrack) {
+ // There's no need to create an audio decoder has we only want to know if
+ // such codec is supported. We do need to call the PDMFactory::Supports
+ // API outside the main thread to get accurate results.
+ promises.AppendElement(
+ InvokeAsync(taskQueue, __func__, [config = std::move(config)]() {
+ RefPtr<PDMFactory> pdm = new PDMFactory();
+ SupportDecoderParams params{*config};
+ if (pdm->Supports(params, nullptr /* decoder doctor */) ==
+ media::DecodeSupport::Unsupported) {
+ return CapabilitiesPromise::CreateAndReject(NS_ERROR_FAILURE,
+ __func__);
+ }
+ return CapabilitiesPromise::CreateAndResolve(
+ MediaCapabilitiesInfo(true /* supported */, true /* smooth */,
+ true /* power efficient */),
+ __func__);
+ }));
+ continue;
+ }
+
+ // On Windows, the MediaDataDecoder expects to be created on a thread
+ // supporting MTA, which the main thread doesn't. So we use our task queue
+ // to create such decoder and perform initialization.
+
+ RefPtr<layers::KnowsCompositor> compositor = GetCompositor();
+ float frameRate =
+ static_cast<float>(videoContainer->ExtendedType().GetFramerate().ref());
+ const bool shouldResistFingerprinting =
+ mParent->ShouldResistFingerprinting(RFPTarget::Unknown);
+
+ // clang-format off
+ promises.AppendElement(InvokeAsync(
+ taskQueue, __func__,
+ [taskQueue, frameRate, shouldResistFingerprinting, compositor,
+ config = std::move(config)]() mutable -> RefPtr<CapabilitiesPromise> {
+ // MediaDataDecoder keeps a reference to the config object, so we must
+ // keep it alive until the decoder has been shutdown.
+ static Atomic<uint32_t> sTrackingIdCounter(0);
+ TrackingId trackingId(TrackingId::Source::MediaCapabilities,
+ sTrackingIdCounter++,
+ TrackingId::TrackAcrossProcesses::Yes);
+ CreateDecoderParams params{
+ *config, compositor,
+ CreateDecoderParams::VideoFrameRate(frameRate),
+ TrackInfo::kVideoTrack, Some(std::move(trackingId))};
+ // We want to ensure that all decoder's queries are occurring only
+ // once at a time as it can quickly exhaust the system resources
+ // otherwise.
+ static RefPtr<AllocPolicy> sVideoAllocPolicy = [&taskQueue]() {
+ SchedulerGroup::Dispatch(
+ TaskCategory::Other,
+ NS_NewRunnableFunction(
+ "MediaCapabilities::AllocPolicy:Video", []() {
+ ClearOnShutdown(&sVideoAllocPolicy,
+ ShutdownPhase::XPCOMShutdownThreads);
+ }));
+ return new SingleAllocPolicy(TrackInfo::TrackType::kVideoTrack,
+ taskQueue);
+ }();
+ return AllocationWrapper::CreateDecoder(params, sVideoAllocPolicy)
+ ->Then(
+ taskQueue, __func__,
+ [taskQueue, frameRate, shouldResistFingerprinting,
+ config = std::move(config)](
+ AllocationWrapper::AllocateDecoderPromise::
+ ResolveOrRejectValue&& aValue) mutable {
+ if (aValue.IsReject()) {
+ return CapabilitiesPromise::CreateAndReject(
+ std::move(aValue.RejectValue()), __func__);
+ }
+ RefPtr<MediaDataDecoder> decoder =
+ std::move(aValue.ResolveValue());
+ // We now query the decoder to determine if it's power
+ // efficient.
+ RefPtr<CapabilitiesPromise> p = decoder->Init()->Then(
+ taskQueue, __func__,
+ [taskQueue, decoder, frameRate,
+ shouldResistFingerprinting,
+ config = std::move(config)](
+ MediaDataDecoder::InitPromise::
+ ResolveOrRejectValue&& aValue) mutable {
+ RefPtr<CapabilitiesPromise> p;
+ if (aValue.IsReject()) {
+ p = CapabilitiesPromise::CreateAndReject(
+ std::move(aValue.RejectValue()), __func__);
+ } else if (shouldResistFingerprinting) {
+ p = CapabilitiesPromise::CreateAndResolve(
+ MediaCapabilitiesInfo(true /* supported */,
+ true /* smooth */, false /* power efficient */),
+ __func__);
+ } else {
+ MOZ_ASSERT(config->IsVideo());
+ if (StaticPrefs::media_mediacapabilities_from_database()) {
+ nsAutoCString reason;
+ bool powerEfficient =
+ decoder->IsHardwareAccelerated(reason);
+
+ int32_t videoFrameRate = std::clamp<int32_t>(frameRate, 1, INT32_MAX);
+
+ DecoderBenchmarkInfo benchmarkInfo{
+ config->mMimeType,
+ config->GetAsVideoInfo()->mImage.width,
+ config->GetAsVideoInfo()->mImage.height,
+ videoFrameRate, 8};
+
+ p = DecoderBenchmark::Get(benchmarkInfo)->Then(
+ GetMainThreadSerialEventTarget(),
+ __func__,
+ [powerEfficient](int32_t score) {
+ // score < 0 means no entry found.
+ bool smooth = score < 0 || score >
+ StaticPrefs::
+ media_mediacapabilities_drop_threshold();
+ return CapabilitiesPromise::
+ CreateAndResolve(
+ MediaCapabilitiesInfo(
+ true, smooth,
+ powerEfficient),
+ __func__);
+ },
+ [](nsresult rv) {
+ return CapabilitiesPromise::
+ CreateAndReject(rv, __func__);
+ });
+ } else if (config->GetAsVideoInfo()->mImage.height < 480) {
+ // Assume that we can do stuff at 480p or less in
+ // a power efficient manner and smoothly. If
+ // greater than 480p we assume that if the video
+ // decoding is hardware accelerated it will be
+ // smooth and power efficient, otherwise we use
+ // the benchmark to estimate
+ p = CapabilitiesPromise::CreateAndResolve(
+ MediaCapabilitiesInfo(true, true, true),
+ __func__);
+ } else {
+ nsAutoCString reason;
+ bool smooth = true;
+ bool powerEfficient =
+ decoder->IsHardwareAccelerated(reason);
+ if (!powerEfficient &&
+ VPXDecoder::IsVP9(config->mMimeType)) {
+ smooth = VP9Benchmark::IsVP9DecodeFast(
+ true /* default */);
+ uint32_t fps =
+ VP9Benchmark::MediaBenchmarkVp9Fps();
+ if (!smooth && fps > 0) {
+ // The VP9 estimizer decode a 1280x720 video.
+ // Let's adjust the result for the resolution
+ // and frame rate of what we actually want. If
+ // the result is twice that we need we assume
+ // it will be smooth.
+ const auto& videoConfig =
+ *config->GetAsVideoInfo();
+ double needed = ((1280.0 * 720.0) /
+ (videoConfig.mImage.width *
+ videoConfig.mImage.height) *
+ fps) /
+ frameRate;
+ smooth = needed > 2;
+ }
+ }
+
+ p = CapabilitiesPromise::CreateAndResolve(
+ MediaCapabilitiesInfo(true /* supported */,
+ smooth, powerEfficient),
+ __func__);
+ }
+ }
+ MOZ_ASSERT(p.get(), "the promise has been created");
+ // Let's keep alive the decoder and the config object
+ // until the decoder has shutdown.
+ decoder->Shutdown()->Then(
+ taskQueue, __func__,
+ [taskQueue, decoder, config = std::move(config)](
+ const ShutdownPromise::ResolveOrRejectValue&
+ aValue) {});
+ return p;
+ });
+ return p;
+ });
+ }));
+ // clang-format on
+ }
+
+ auto holder = MakeRefPtr<
+ DOMMozPromiseRequestHolder<CapabilitiesPromise::AllPromiseType>>(mParent);
+ RefPtr<nsISerialEventTarget> targetThread;
+ RefPtr<StrongWorkerRef> workerRef;
+
+ if (NS_IsMainThread()) {
+ targetThread = mParent->AbstractMainThreadFor(TaskCategory::Other);
+ } else {
+ WorkerPrivate* wp = GetCurrentThreadWorkerPrivate();
+ MOZ_ASSERT(wp, "Must be called from a worker thread");
+ targetThread = wp->HybridEventTarget();
+ workerRef = StrongWorkerRef::Create(
+ wp, "MediaCapabilities", [holder, targetThread]() {
+ MOZ_ASSERT(targetThread->IsOnCurrentThread());
+ holder->DisconnectIfExists();
+ });
+ if (NS_WARN_IF(!workerRef)) {
+ // The worker is shutting down.
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+ }
+
+ MOZ_ASSERT(targetThread);
+
+ // this is only captured for use with the LOG macro.
+ RefPtr<MediaCapabilities> self = this;
+
+ CapabilitiesPromise::All(targetThread, promises)
+ ->Then(targetThread, __func__,
+ [promise, tracks = std::move(tracks), workerRef, holder,
+ aConfiguration, self,
+ this](CapabilitiesPromise::AllPromiseType::ResolveOrRejectValue&&
+ aValue) {
+ holder->Complete();
+ if (aValue.IsReject()) {
+ auto info = MakeUnique<MediaCapabilitiesInfo>(
+ false /* supported */, false /* smooth */,
+ false /* power efficient */);
+ LOG("%s -> %s",
+ MediaDecodingConfigurationToStr(aConfiguration).get(),
+ MediaCapabilitiesInfoToStr(info.get()).get());
+ promise->MaybeResolve(std::move(info));
+ return;
+ }
+ bool powerEfficient = true;
+ bool smooth = true;
+ for (auto&& capability : aValue.ResolveValue()) {
+ smooth &= capability.Smooth();
+ powerEfficient &= capability.PowerEfficient();
+ }
+ auto info = MakeUnique<MediaCapabilitiesInfo>(
+ true /* supported */, smooth, powerEfficient);
+ LOG("%s -> %s",
+ MediaDecodingConfigurationToStr(aConfiguration).get(),
+ MediaCapabilitiesInfoToStr(info.get()).get());
+ promise->MaybeResolve(std::move(info));
+ })
+ ->Track(*holder);
+
+ return promise.forget();
+}
+
+already_AddRefed<Promise> MediaCapabilities::EncodingInfo(
+ const MediaEncodingConfiguration& aConfiguration, ErrorResult& aRv) {
+ RefPtr<Promise> promise = Promise::Create(mParent, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ // If configuration is not a valid MediaConfiguration, return a Promise
+ // rejected with a TypeError.
+ if (!aConfiguration.mVideo.WasPassed() &&
+ !aConfiguration.mAudio.WasPassed()) {
+ aRv.ThrowTypeError<MSG_MISSING_REQUIRED_DICTIONARY_MEMBER>(
+ "'audio' or 'video' member of argument of "
+ "MediaCapabilities.encodingInfo");
+ return nullptr;
+ }
+
+ bool supported = true;
+
+ // If configuration.video is present and is not a valid video configuration,
+ // return a Promise rejected with a TypeError.
+ if (aConfiguration.mVideo.WasPassed()) {
+ if (!CheckVideoConfiguration(aConfiguration.mVideo.Value())) {
+ aRv.ThrowTypeError<MSG_INVALID_MEDIA_VIDEO_CONFIGURATION>();
+ return nullptr;
+ }
+ // We have a video configuration and it is valid. Check if it is supported.
+ supported &=
+ CheckTypeForEncoder(aConfiguration.mVideo.Value().mContentType);
+ }
+ if (aConfiguration.mAudio.WasPassed()) {
+ if (!CheckAudioConfiguration(aConfiguration.mAudio.Value())) {
+ aRv.ThrowTypeError<MSG_INVALID_MEDIA_AUDIO_CONFIGURATION>();
+ return nullptr;
+ }
+ // We have an audio configuration and it is valid. Check if it is supported.
+ supported &=
+ CheckTypeForEncoder(aConfiguration.mAudio.Value().mContentType);
+ }
+
+ auto info = MakeUnique<MediaCapabilitiesInfo>(supported, supported, false);
+ promise->MaybeResolve(std::move(info));
+
+ return promise.forget();
+}
+
+Maybe<MediaContainerType> MediaCapabilities::CheckVideoConfiguration(
+ const VideoConfiguration& aConfig) const {
+ Maybe<MediaExtendedMIMEType> container = MakeMediaExtendedMIMEType(aConfig);
+ if (!container) {
+ return Nothing();
+ }
+ // A valid video MIME type is a string that is a valid media MIME type and for
+ // which the type per [RFC7231] is either video or application.
+ if (!container->Type().HasVideoMajorType() &&
+ !container->Type().HasApplicationMajorType()) {
+ return Nothing();
+ }
+
+ // If the MIME type does not imply a codec, the string MUST also have one and
+ // only one parameter that is named codecs with a value describing a single
+ // media codec. Otherwise, it MUST contain no parameters.
+ // TODO (nsIMOMEHeaderParam doesn't provide backend to count number of
+ // parameters)
+
+ return Some(MediaContainerType(std::move(*container)));
+}
+
+Maybe<MediaContainerType> MediaCapabilities::CheckAudioConfiguration(
+ const AudioConfiguration& aConfig) const {
+ Maybe<MediaExtendedMIMEType> container = MakeMediaExtendedMIMEType(aConfig);
+ if (!container) {
+ return Nothing();
+ }
+ // A valid audio MIME type is a string that is valid media MIME type and for
+ // which the type per [RFC7231] is either audio or application.
+ if (!container->Type().HasAudioMajorType() &&
+ !container->Type().HasApplicationMajorType()) {
+ return Nothing();
+ }
+
+ // If the MIME type does not imply a codec, the string MUST also have one and
+ // only one parameter that is named codecs with a value describing a single
+ // media codec. Otherwise, it MUST contain no parameters.
+ // TODO (nsIMOMEHeaderParam doesn't provide backend to count number of
+ // parameters)
+
+ return Some(MediaContainerType(std::move(*container)));
+}
+
+bool MediaCapabilities::CheckTypeForMediaSource(const nsAString& aType) {
+ IgnoredErrorResult rv;
+ MediaSource::IsTypeSupported(aType, nullptr /* DecoderDoctorDiagnostics */,
+ rv);
+
+ return !rv.Failed();
+}
+
+bool MediaCapabilities::CheckTypeForFile(const nsAString& aType) {
+ Maybe<MediaContainerType> containerType = MakeMediaContainerType(aType);
+ if (!containerType) {
+ return false;
+ }
+
+ return DecoderTraits::CanHandleContainerType(
+ *containerType, nullptr /* DecoderDoctorDiagnostics */) !=
+ CANPLAY_NO;
+}
+
+bool MediaCapabilities::CheckTypeForEncoder(const nsAString& aType) {
+ return MediaRecorder::IsTypeSupported(aType);
+}
+
+already_AddRefed<layers::KnowsCompositor> MediaCapabilities::GetCompositor() {
+ nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(GetParentObject());
+ if (NS_WARN_IF(!window)) {
+ return nullptr;
+ }
+
+ nsCOMPtr<Document> doc = window->GetExtantDoc();
+ if (NS_WARN_IF(!doc)) {
+ return nullptr;
+ }
+ WindowRenderer* renderer = nsContentUtils::WindowRendererForDocument(doc);
+ if (NS_WARN_IF(!renderer)) {
+ return nullptr;
+ }
+ RefPtr<layers::KnowsCompositor> knows = renderer->AsKnowsCompositor();
+ if (NS_WARN_IF(!knows)) {
+ return nullptr;
+ }
+ return knows->GetForMedia().forget();
+}
+
+bool MediaCapabilities::Enabled(JSContext* aCx, JSObject* aGlobal) {
+ return StaticPrefs::media_media_capabilities_enabled();
+}
+
+JSObject* MediaCapabilities::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return MediaCapabilities_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaCapabilities)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaCapabilities)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaCapabilities)
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(MediaCapabilities, mParent)
+
+// MediaCapabilitiesInfo
+bool MediaCapabilitiesInfo::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto,
+ JS::MutableHandle<JSObject*> aReflector) {
+ return MediaCapabilitiesInfo_Binding::Wrap(aCx, this, aGivenProto,
+ aReflector);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/mediacapabilities/MediaCapabilities.h b/dom/media/mediacapabilities/MediaCapabilities.h
new file mode 100644
index 0000000000..b31e12e8ab
--- /dev/null
+++ b/dom/media/mediacapabilities/MediaCapabilities.h
@@ -0,0 +1,104 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_MediaCapabilities_h_
+#define mozilla_dom_MediaCapabilities_h_
+
+#include "DDLoggedTypeTraits.h"
+#include "js/TypeDecls.h"
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/dom/NonRefcountedDOMObject.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupports.h"
+#include "nsStringFwd.h"
+#include "nsWrapperCache.h"
+
+class nsIGlobalObject;
+
+namespace mozilla {
+class ErrorResult;
+class MediaContainerType;
+
+namespace layers {
+class KnowsCompositor;
+}
+namespace dom {
+class MediaCapabilities;
+} // namespace dom
+DDLoggedTypeName(dom::MediaCapabilities);
+
+namespace dom {
+
+struct MediaDecodingConfiguration;
+struct MediaEncodingConfiguration;
+struct AudioConfiguration;
+struct VideoConfiguration;
+class Promise;
+
+class MediaCapabilities final : public nsISupports, public nsWrapperCache {
+ public:
+ // Ref counting and cycle collection
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(MediaCapabilities)
+
+ // WebIDL Methods
+ already_AddRefed<Promise> DecodingInfo(
+ const MediaDecodingConfiguration& aConfiguration, ErrorResult& aRv);
+ already_AddRefed<Promise> EncodingInfo(
+ const MediaEncodingConfiguration& aConfiguration, ErrorResult& aRv);
+ // End WebIDL Methods
+
+ explicit MediaCapabilities(nsIGlobalObject* aParent);
+
+ nsIGlobalObject* GetParentObject() const { return mParent; }
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ static bool Enabled(JSContext* aCx, JSObject* aGlobal);
+
+ private:
+ virtual ~MediaCapabilities() = default;
+ Maybe<MediaContainerType> CheckVideoConfiguration(
+ const VideoConfiguration& aConfig) const;
+ Maybe<MediaContainerType> CheckAudioConfiguration(
+ const AudioConfiguration& aConfig) const;
+ bool CheckTypeForMediaSource(const nsAString& aType);
+ bool CheckTypeForFile(const nsAString& aType);
+ bool CheckTypeForEncoder(const nsAString& aType);
+ already_AddRefed<layers::KnowsCompositor> GetCompositor();
+ nsCOMPtr<nsIGlobalObject> mParent;
+};
+
+class MediaCapabilitiesInfo final : public NonRefcountedDOMObject {
+ public:
+ // WebIDL methods
+ bool Supported() const { return mSupported; }
+ bool Smooth() const { return mSmooth; }
+ bool PowerEfficient() const { return mPowerEfficient; }
+ // End WebIDL methods
+
+ MediaCapabilitiesInfo(bool aSupported, bool aSmooth, bool aPowerEfficient)
+ : mSupported(aSupported),
+ mSmooth(aSmooth),
+ mPowerEfficient(aPowerEfficient) {}
+
+ bool WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto,
+ JS::MutableHandle<JSObject*> aReflector);
+
+ private:
+ bool mSupported;
+ bool mSmooth;
+ bool mPowerEfficient;
+};
+
+} // namespace dom
+
+} // namespace mozilla
+
+#endif /* mozilla_dom_MediaCapabilities_h_ */
diff --git a/dom/media/mediacapabilities/PBenchmarkStorage.ipdl b/dom/media/mediacapabilities/PBenchmarkStorage.ipdl
new file mode 100644
index 0000000000..220d83f978
--- /dev/null
+++ b/dom/media/mediacapabilities/PBenchmarkStorage.ipdl
@@ -0,0 +1,23 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PContent;
+
+namespace mozilla {
+
+[ManualDealloc]
+async protocol PBenchmarkStorage
+{
+ manager PContent;
+
+parent:
+ async Put(nsCString aDbName, nsCString aKey, int32_t aValue);
+ async Get(nsCString aDbName, nsCString aKey) returns(int32_t aValue);
+ async CheckVersion(nsCString aDbName, int32_t aVersion);
+ async __delete__();
+
+};
+
+} // namespace mozilla
diff --git a/dom/media/mediacapabilities/moz.build b/dom/media/mediacapabilities/moz.build
new file mode 100644
index 0000000000..bafd3de59c
--- /dev/null
+++ b/dom/media/mediacapabilities/moz.build
@@ -0,0 +1,32 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS.mozilla.dom += [
+ "MediaCapabilities.h",
+]
+
+EXPORTS.mozilla += [
+ "BenchmarkStorageChild.h",
+ "BenchmarkStorageParent.h",
+ "KeyValueStorage.h",
+]
+
+EXPORTS += [
+ "DecoderBenchmark.h",
+]
+
+UNIFIED_SOURCES += [
+ "BenchmarkStorageChild.cpp",
+ "BenchmarkStorageParent.cpp",
+ "DecoderBenchmark.cpp",
+ "KeyValueStorage.cpp",
+ "MediaCapabilities.cpp",
+]
+
+IPDL_SOURCES += ["PBenchmarkStorage.ipdl"]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/mediacontrol/AudioFocusManager.cpp b/dom/media/mediacontrol/AudioFocusManager.cpp
new file mode 100644
index 0000000000..24ac7374d4
--- /dev/null
+++ b/dom/media/mediacontrol/AudioFocusManager.cpp
@@ -0,0 +1,134 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioFocusManager.h"
+
+#include "MediaController.h"
+#include "MediaControlUtils.h"
+#include "MediaControlService.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+#include "mozilla/Logging.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/Telemetry.h"
+#include "nsThreadUtils.h"
+
+#undef LOG
+#define LOG(msg, ...) \
+ MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
+ ("AudioFocusManager=%p, " msg, this, ##__VA_ARGS__))
+
+namespace mozilla::dom {
+
+void AudioFocusManager::RequestAudioFocus(IMediaController* aController) {
+ MOZ_ASSERT(aController);
+ if (mOwningFocusControllers.Contains(aController)) {
+ return;
+ }
+ const bool hasManagedAudioFocus = ClearFocusControllersIfNeeded();
+ LOG("Controller %" PRId64 " grants audio focus", aController->Id());
+ mOwningFocusControllers.AppendElement(aController);
+ if (hasManagedAudioFocus) {
+ AccumulateCategorical(
+ mozilla::Telemetry::LABELS_TABS_AUDIO_COMPETITION::ManagedFocusByGecko);
+ } else if (GetAudioFocusNums() == 1) {
+ // Only one audible tab is playing within gecko.
+ AccumulateCategorical(
+ mozilla::Telemetry::LABELS_TABS_AUDIO_COMPETITION::None);
+ } else {
+ // Multiple audible tabs are playing at the same time within gecko.
+ CreateTimerForUpdatingTelemetry();
+ }
+}
+
+void AudioFocusManager::RevokeAudioFocus(IMediaController* aController) {
+ MOZ_ASSERT(aController);
+ if (!mOwningFocusControllers.Contains(aController)) {
+ return;
+ }
+ LOG("Controller %" PRId64 " loses audio focus", aController->Id());
+ mOwningFocusControllers.RemoveElement(aController);
+}
+
+bool AudioFocusManager::ClearFocusControllersIfNeeded() {
+ // Enable audio focus management will start the audio competition which is
+ // only allowing one controller playing at a time.
+ if (!StaticPrefs::media_audioFocus_management()) {
+ return false;
+ }
+
+ bool hasStoppedAnyController = false;
+ for (auto& controller : mOwningFocusControllers) {
+ LOG("Controller %" PRId64 " loses audio focus in audio competitition",
+ controller->Id());
+ hasStoppedAnyController = true;
+ controller->Stop();
+ }
+ mOwningFocusControllers.Clear();
+ return hasStoppedAnyController;
+}
+
+uint32_t AudioFocusManager::GetAudioFocusNums() const {
+ return mOwningFocusControllers.Length();
+}
+
+void AudioFocusManager::CreateTimerForUpdatingTelemetry() {
+ MOZ_ASSERT(NS_IsMainThread());
+ // Already create the timer.
+ if (mTelemetryTimer) {
+ return;
+ }
+
+ const uint32_t focusNum = GetAudioFocusNums();
+ MOZ_ASSERT(focusNum > 1);
+
+ RefPtr<MediaControlService> service = MediaControlService::GetService();
+ MOZ_ASSERT(service);
+ const uint32_t activeControllerNum = service->GetActiveControllersNum();
+
+ // It takes time if users want to manually manage the audio competition by
+ // pausing one of playing tabs. So we will check the status after a short
+ // while to see if users handle the audio competition, or simply ignore it.
+ nsCOMPtr<nsIRunnable> task = NS_NewRunnableFunction(
+ "AudioFocusManager::RequestAudioFocus",
+ [focusNum, activeControllerNum]() {
+ if (RefPtr<MediaControlService> service =
+ MediaControlService::GetService()) {
+ service->GetAudioFocusManager().UpdateTelemetryDataFromTimer(
+ focusNum, activeControllerNum);
+ }
+ });
+ mTelemetryTimer =
+ SimpleTimer::Create(task, 4000, GetMainThreadSerialEventTarget());
+}
+
+void AudioFocusManager::UpdateTelemetryDataFromTimer(
+ uint32_t aPrevFocusNum, uint64_t aPrevActiveControllerNum) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mTelemetryTimer);
+ // Users pause or mute tabs which decreases the amount of audible playing
+ // tabs, which should not affect the total controller amount.
+ if (GetAudioFocusNums() < aPrevFocusNum) {
+ // If active controller amount is not equal, that means controllers got
+ // deactivated by other reasons, such as reaching to the end, which are not
+ // the situation we would like to accumulate for telemetry.
+ if (MediaControlService::GetService()->GetActiveControllersNum() ==
+ aPrevActiveControllerNum) {
+ AccumulateCategorical(mozilla::Telemetry::LABELS_TABS_AUDIO_COMPETITION::
+ ManagedFocusByUser);
+ }
+ } else {
+ AccumulateCategorical(
+ mozilla::Telemetry::LABELS_TABS_AUDIO_COMPETITION::Ignored);
+ }
+ mTelemetryTimer = nullptr;
+}
+
+AudioFocusManager::~AudioFocusManager() {
+ if (mTelemetryTimer) {
+ mTelemetryTimer->Cancel();
+ mTelemetryTimer = nullptr;
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/mediacontrol/AudioFocusManager.h b/dom/media/mediacontrol/AudioFocusManager.h
new file mode 100644
index 0000000000..b6047444bb
--- /dev/null
+++ b/dom/media/mediacontrol/AudioFocusManager.h
@@ -0,0 +1,54 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_MEDIACONTROL_AUDIOFOCUSMANAGER_H_
+#define DOM_MEDIA_MEDIACONTROL_AUDIOFOCUSMANAGER_H_
+
+#include "base/basictypes.h"
+#include "nsTArray.h"
+#include "VideoUtils.h"
+
+namespace mozilla::dom {
+
+class IMediaController;
+class MediaControlService;
+
+/**
+ * AudioFocusManager is used to assign the audio focus to different requester
+ * and decide which requester can own audio focus when audio competing happens.
+ * When the audio competing happens, the last request would be a winner who can
+ * still own the audio focus, and all the other requesters would lose the audio
+ * focus. Now MediaController is the onlt requester, it would request the audio
+ * focus when it becomes audible and revoke the audio focus when the controller
+ * is no longer active.
+ */
+class AudioFocusManager {
+ public:
+ void RequestAudioFocus(IMediaController* aController);
+ void RevokeAudioFocus(IMediaController* aController);
+
+ explicit AudioFocusManager() = default;
+ ~AudioFocusManager();
+
+ uint32_t GetAudioFocusNums() const;
+
+ private:
+ friend class MediaControlService;
+ // Return true if we manage audio focus by clearing other controllers owning
+ // audio focus before assigning audio focus to the new controller.
+ bool ClearFocusControllersIfNeeded();
+
+ void CreateTimerForUpdatingTelemetry();
+ // This would check if user has managed audio focus by themselves and update
+ // the result to telemetry. This method should only be called from timer.
+ void UpdateTelemetryDataFromTimer(uint32_t aPrevFocusNum,
+ uint64_t aPrevActiveControllerNum);
+
+ nsTArray<RefPtr<IMediaController>> mOwningFocusControllers;
+ RefPtr<SimpleTimer> mTelemetryTimer;
+};
+
+} // namespace mozilla::dom
+
+#endif // DOM_MEDIA_MEDIACONTROL_AUDIOFOCUSMANAGER_H_
diff --git a/dom/media/mediacontrol/ContentMediaController.cpp b/dom/media/mediacontrol/ContentMediaController.cpp
new file mode 100644
index 0000000000..a5d19a57c4
--- /dev/null
+++ b/dom/media/mediacontrol/ContentMediaController.cpp
@@ -0,0 +1,376 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ContentMediaController.h"
+
+#include "MediaControlUtils.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/Telemetry.h"
+#include "nsGlobalWindowOuter.h"
+
+namespace mozilla::dom {
+
+#undef LOG
+#define LOG(msg, ...) \
+ MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
+ ("ContentMediaController=%p, " msg, this, ##__VA_ARGS__))
+
+static Maybe<bool> sXPCOMShutdown;
+
+static void InitXPCOMShutdownMonitor() {
+ if (sXPCOMShutdown) {
+ return;
+ }
+ sXPCOMShutdown.emplace(false);
+ RunOnShutdown([&] { sXPCOMShutdown = Some(true); });
+}
+
+static ContentMediaController* GetContentMediaControllerFromBrowsingContext(
+ BrowsingContext* aBrowsingContext) {
+ MOZ_ASSERT(NS_IsMainThread());
+ InitXPCOMShutdownMonitor();
+ if (!aBrowsingContext || aBrowsingContext->IsDiscarded()) {
+ return nullptr;
+ }
+
+ nsPIDOMWindowOuter* outer = aBrowsingContext->GetDOMWindow();
+ if (!outer) {
+ return nullptr;
+ }
+
+ nsGlobalWindowInner* inner =
+ nsGlobalWindowInner::Cast(outer->GetCurrentInnerWindow());
+ return inner ? inner->GetContentMediaController() : nullptr;
+}
+
+static already_AddRefed<BrowsingContext> GetBrowsingContextForAgent(
+ uint64_t aBrowsingContextId) {
+ // If XPCOM has been shutdown, then we're not able to access browsing context.
+ if (sXPCOMShutdown && *sXPCOMShutdown) {
+ return nullptr;
+ }
+ return BrowsingContext::Get(aBrowsingContextId);
+}
+
+/* static */
+ContentMediaControlKeyReceiver* ContentMediaControlKeyReceiver::Get(
+ BrowsingContext* aBC) {
+ MOZ_ASSERT(NS_IsMainThread());
+ return GetContentMediaControllerFromBrowsingContext(aBC);
+}
+
+/* static */
+ContentMediaAgent* ContentMediaAgent::Get(BrowsingContext* aBC) {
+ MOZ_ASSERT(NS_IsMainThread());
+ return GetContentMediaControllerFromBrowsingContext(aBC);
+}
+
+void ContentMediaAgent::NotifyMediaPlaybackChanged(uint64_t aBrowsingContextId,
+ MediaPlaybackState aState) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RefPtr<BrowsingContext> bc = GetBrowsingContextForAgent(aBrowsingContextId);
+ if (!bc || bc->IsDiscarded()) {
+ return;
+ }
+
+ LOG("Notify media %s in BC %" PRId64, ToMediaPlaybackStateStr(aState),
+ bc->Id());
+ if (XRE_IsContentProcess()) {
+ ContentChild* contentChild = ContentChild::GetSingleton();
+ Unused << contentChild->SendNotifyMediaPlaybackChanged(bc, aState);
+ } else {
+ // Currently this only happen when we disable e10s, otherwise all controlled
+ // media would be run in the content process.
+ if (RefPtr<IMediaInfoUpdater> updater =
+ bc->Canonical()->GetMediaController()) {
+ updater->NotifyMediaPlaybackChanged(bc->Id(), aState);
+ }
+ }
+}
+
+void ContentMediaAgent::NotifyMediaAudibleChanged(uint64_t aBrowsingContextId,
+ MediaAudibleState aState) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RefPtr<BrowsingContext> bc = GetBrowsingContextForAgent(aBrowsingContextId);
+ if (!bc || bc->IsDiscarded()) {
+ return;
+ }
+
+ LOG("Notify media became %s in BC %" PRId64,
+ aState == MediaAudibleState::eAudible ? "audible" : "inaudible",
+ bc->Id());
+ if (XRE_IsContentProcess()) {
+ ContentChild* contentChild = ContentChild::GetSingleton();
+ Unused << contentChild->SendNotifyMediaAudibleChanged(bc, aState);
+ } else {
+ // Currently this only happen when we disable e10s, otherwise all controlled
+ // media would be run in the content process.
+ if (RefPtr<IMediaInfoUpdater> updater =
+ bc->Canonical()->GetMediaController()) {
+ updater->NotifyMediaAudibleChanged(bc->Id(), aState);
+ }
+ }
+}
+
+void ContentMediaAgent::SetIsInPictureInPictureMode(
+ uint64_t aBrowsingContextId, bool aIsInPictureInPictureMode) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RefPtr<BrowsingContext> bc = GetBrowsingContextForAgent(aBrowsingContextId);
+ if (!bc || bc->IsDiscarded()) {
+ return;
+ }
+
+ LOG("Notify media Picture-in-Picture mode '%s' in BC %" PRId64,
+ aIsInPictureInPictureMode ? "enabled" : "disabled", bc->Id());
+ if (XRE_IsContentProcess()) {
+ ContentChild* contentChild = ContentChild::GetSingleton();
+ Unused << contentChild->SendNotifyPictureInPictureModeChanged(
+ bc, aIsInPictureInPictureMode);
+ } else {
+ // Currently this only happen when we disable e10s, otherwise all controlled
+ // media would be run in the content process.
+ if (RefPtr<IMediaInfoUpdater> updater =
+ bc->Canonical()->GetMediaController()) {
+ updater->SetIsInPictureInPictureMode(bc->Id(), aIsInPictureInPictureMode);
+ }
+ }
+}
+
+void ContentMediaAgent::SetDeclaredPlaybackState(
+ uint64_t aBrowsingContextId, MediaSessionPlaybackState aState) {
+ RefPtr<BrowsingContext> bc = GetBrowsingContextForAgent(aBrowsingContextId);
+ if (!bc || bc->IsDiscarded()) {
+ return;
+ }
+
+ LOG("Notify declared playback state '%s' in BC %" PRId64,
+ ToMediaSessionPlaybackStateStr(aState), bc->Id());
+ if (XRE_IsContentProcess()) {
+ ContentChild* contentChild = ContentChild::GetSingleton();
+ Unused << contentChild->SendNotifyMediaSessionPlaybackStateChanged(bc,
+ aState);
+ return;
+ }
+ // This would only happen when we disable e10s.
+ if (RefPtr<IMediaInfoUpdater> updater =
+ bc->Canonical()->GetMediaController()) {
+ updater->SetDeclaredPlaybackState(bc->Id(), aState);
+ }
+}
+
+void ContentMediaAgent::NotifySessionCreated(uint64_t aBrowsingContextId) {
+ RefPtr<BrowsingContext> bc = GetBrowsingContextForAgent(aBrowsingContextId);
+ if (!bc || bc->IsDiscarded()) {
+ return;
+ }
+
+ LOG("Notify media session being created in BC %" PRId64, bc->Id());
+ if (XRE_IsContentProcess()) {
+ ContentChild* contentChild = ContentChild::GetSingleton();
+ Unused << contentChild->SendNotifyMediaSessionUpdated(bc, true);
+ return;
+ }
+ // This would only happen when we disable e10s.
+ if (RefPtr<IMediaInfoUpdater> updater =
+ bc->Canonical()->GetMediaController()) {
+ updater->NotifySessionCreated(bc->Id());
+ }
+}
+
+void ContentMediaAgent::NotifySessionDestroyed(uint64_t aBrowsingContextId) {
+ RefPtr<BrowsingContext> bc = GetBrowsingContextForAgent(aBrowsingContextId);
+ if (!bc || bc->IsDiscarded()) {
+ return;
+ }
+
+ LOG("Notify media session being destroyed in BC %" PRId64, bc->Id());
+ if (XRE_IsContentProcess()) {
+ ContentChild* contentChild = ContentChild::GetSingleton();
+ Unused << contentChild->SendNotifyMediaSessionUpdated(bc, false);
+ return;
+ }
+ // This would only happen when we disable e10s.
+ if (RefPtr<IMediaInfoUpdater> updater =
+ bc->Canonical()->GetMediaController()) {
+ updater->NotifySessionDestroyed(bc->Id());
+ }
+}
+
+void ContentMediaAgent::UpdateMetadata(
+ uint64_t aBrowsingContextId, const Maybe<MediaMetadataBase>& aMetadata) {
+ RefPtr<BrowsingContext> bc = GetBrowsingContextForAgent(aBrowsingContextId);
+ if (!bc || bc->IsDiscarded()) {
+ return;
+ }
+
+ LOG("Notify media session metadata change in BC %" PRId64, bc->Id());
+ if (XRE_IsContentProcess()) {
+ ContentChild* contentChild = ContentChild::GetSingleton();
+ Unused << contentChild->SendNotifyUpdateMediaMetadata(bc, aMetadata);
+ return;
+ }
+ // This would only happen when we disable e10s.
+ if (RefPtr<IMediaInfoUpdater> updater =
+ bc->Canonical()->GetMediaController()) {
+ updater->UpdateMetadata(bc->Id(), aMetadata);
+ }
+}
+
+void ContentMediaAgent::EnableAction(uint64_t aBrowsingContextId,
+ MediaSessionAction aAction) {
+ RefPtr<BrowsingContext> bc = GetBrowsingContextForAgent(aBrowsingContextId);
+ if (!bc || bc->IsDiscarded()) {
+ return;
+ }
+
+ LOG("Notify to enable action '%s' in BC %" PRId64,
+ ToMediaSessionActionStr(aAction), bc->Id());
+ if (XRE_IsContentProcess()) {
+ ContentChild* contentChild = ContentChild::GetSingleton();
+ Unused << contentChild->SendNotifyMediaSessionSupportedActionChanged(
+ bc, aAction, true);
+ return;
+ }
+ // This would only happen when we disable e10s.
+ if (RefPtr<IMediaInfoUpdater> updater =
+ bc->Canonical()->GetMediaController()) {
+ updater->EnableAction(bc->Id(), aAction);
+ }
+}
+
+void ContentMediaAgent::DisableAction(uint64_t aBrowsingContextId,
+ MediaSessionAction aAction) {
+ RefPtr<BrowsingContext> bc = GetBrowsingContextForAgent(aBrowsingContextId);
+ if (!bc || bc->IsDiscarded()) {
+ return;
+ }
+
+ LOG("Notify to disable action '%s' in BC %" PRId64,
+ ToMediaSessionActionStr(aAction), bc->Id());
+ if (XRE_IsContentProcess()) {
+ ContentChild* contentChild = ContentChild::GetSingleton();
+ Unused << contentChild->SendNotifyMediaSessionSupportedActionChanged(
+ bc, aAction, false);
+ return;
+ }
+ // This would only happen when we disable e10s.
+ if (RefPtr<IMediaInfoUpdater> updater =
+ bc->Canonical()->GetMediaController()) {
+ updater->DisableAction(bc->Id(), aAction);
+ }
+}
+
+void ContentMediaAgent::NotifyMediaFullScreenState(uint64_t aBrowsingContextId,
+ bool aIsInFullScreen) {
+ RefPtr<BrowsingContext> bc = GetBrowsingContextForAgent(aBrowsingContextId);
+ if (!bc || bc->IsDiscarded()) {
+ return;
+ }
+
+ LOG("Notify %s fullscreen in BC %" PRId64,
+ aIsInFullScreen ? "entered" : "left", bc->Id());
+ if (XRE_IsContentProcess()) {
+ ContentChild* contentChild = ContentChild::GetSingleton();
+ Unused << contentChild->SendNotifyMediaFullScreenState(bc, aIsInFullScreen);
+ return;
+ }
+ // This would only happen when we disable e10s.
+ if (RefPtr<IMediaInfoUpdater> updater =
+ bc->Canonical()->GetMediaController()) {
+ updater->NotifyMediaFullScreenState(bc->Id(), aIsInFullScreen);
+ }
+}
+
+void ContentMediaAgent::UpdatePositionState(uint64_t aBrowsingContextId,
+ const PositionState& aState) {
+ RefPtr<BrowsingContext> bc = GetBrowsingContextForAgent(aBrowsingContextId);
+ if (!bc || bc->IsDiscarded()) {
+ return;
+ }
+ if (XRE_IsContentProcess()) {
+ ContentChild* contentChild = ContentChild::GetSingleton();
+ Unused << contentChild->SendNotifyPositionStateChanged(bc, aState);
+ return;
+ }
+ // This would only happen when we disable e10s.
+ if (RefPtr<IMediaInfoUpdater> updater =
+ bc->Canonical()->GetMediaController()) {
+ updater->UpdatePositionState(bc->Id(), aState);
+ }
+}
+
+ContentMediaController::ContentMediaController(uint64_t aId) {
+ LOG("Create content media controller for BC %" PRId64, aId);
+}
+
+void ContentMediaController::AddReceiver(
+ ContentMediaControlKeyReceiver* aListener) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mReceivers.AppendElement(aListener);
+}
+
+void ContentMediaController::RemoveReceiver(
+ ContentMediaControlKeyReceiver* aListener) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mReceivers.RemoveElement(aListener);
+}
+
+void ContentMediaController::HandleMediaKey(MediaControlKey aKey) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mReceivers.IsEmpty()) {
+ return;
+ }
+ LOG("Handle '%s' event, receiver num=%zu", ToMediaControlKeyStr(aKey),
+ mReceivers.Length());
+ // We have default handlers for play, pause and stop.
+ // https://w3c.github.io/mediasession/#ref-for-dom-mediasessionaction-play%E2%91%A3
+ switch (aKey) {
+ case MediaControlKey::Pause:
+ PauseOrStopMedia();
+ return;
+ case MediaControlKey::Play:
+ [[fallthrough]];
+ case MediaControlKey::Stop:
+ // When receiving `Stop`, the amount of receiver would vary during the
+ // iteration, so we use the backward iteration to avoid accessing the
+ // index which is over the array length.
+ for (auto& receiver : Reversed(mReceivers)) {
+ receiver->HandleMediaKey(aKey);
+ }
+ return;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Not supported media key for default handler");
+ }
+}
+
+void ContentMediaController::PauseOrStopMedia() {
+ // When receiving `pause`, if a page contains playing media and paused media
+ // at that moment, that means a user intends to pause those playing
+ // media, not the already paused ones. Then, we're going to stop those already
+ // paused media and keep those latest paused media in `mReceivers`.
+ // The reason for doing that is, when resuming paused media, we only want to
+ // resume latest paused media, not all media, in order to get a better user
+ // experience, which matches Chrome's behavior.
+ bool isAnyMediaPlaying = false;
+ for (const auto& receiver : mReceivers) {
+ if (receiver->IsPlaying()) {
+ isAnyMediaPlaying = true;
+ break;
+ }
+ }
+
+ for (auto& receiver : Reversed(mReceivers)) {
+ if (isAnyMediaPlaying && !receiver->IsPlaying()) {
+ receiver->HandleMediaKey(MediaControlKey::Stop);
+ } else {
+ receiver->HandleMediaKey(MediaControlKey::Pause);
+ }
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/mediacontrol/ContentMediaController.h b/dom/media/mediacontrol/ContentMediaController.h
new file mode 100644
index 0000000000..9e162dbb27
--- /dev/null
+++ b/dom/media/mediacontrol/ContentMediaController.h
@@ -0,0 +1,109 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_MEDIACONTROL_CONTENTMEDIACONTROLLER_H_
+#define DOM_MEDIA_MEDIACONTROL_CONTENTMEDIACONTROLLER_H_
+
+#include "MediaControlKeySource.h"
+#include "MediaStatusManager.h"
+
+namespace mozilla::dom {
+
+class BrowsingContext;
+
+/**
+ * ContentMediaControlKeyReceiver is an interface which is used to receive media
+ * control key sent from the chrome process.
+ */
+class ContentMediaControlKeyReceiver {
+ public:
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+ // Return nullptr if the top level browsing context is no longer alive.
+ static ContentMediaControlKeyReceiver* Get(BrowsingContext* aBC);
+
+ // Use this method to handle the event from `ContentMediaAgent`.
+ virtual void HandleMediaKey(MediaControlKey aKey) = 0;
+
+ virtual bool IsPlaying() const = 0;
+};
+
+/**
+ * ContentMediaAgent is an interface which we use to (1) propoagate media
+ * related information from the content process to the chrome process (2) act an
+ * event source to dispatch media control key to its listeners.
+ *
+ * If the media would like to know the media control key, then media MUST
+ * inherit from ContentMediaControlKeyReceiver, and register themselves to
+ * ContentMediaAgent. Whenever media control key delivers, ContentMediaAgent
+ * would notify all its receivers. In addition, whenever controlled media
+ * changes its playback status or audible state, they should update their status
+ * update via ContentMediaAgent.
+ */
+class ContentMediaAgent : public IMediaInfoUpdater {
+ public:
+ // Return nullptr if the top level browsing context is no longer alive.
+ static ContentMediaAgent* Get(BrowsingContext* aBC);
+
+ // IMediaInfoUpdater Methods
+ void NotifyMediaPlaybackChanged(uint64_t aBrowsingContextId,
+ MediaPlaybackState aState) override;
+ void NotifyMediaAudibleChanged(uint64_t aBrowsingContextId,
+ MediaAudibleState aState) override;
+ void SetIsInPictureInPictureMode(uint64_t aBrowsingContextId,
+ bool aIsInPictureInPictureMode) override;
+ void SetDeclaredPlaybackState(uint64_t aBrowsingContextId,
+ MediaSessionPlaybackState aState) override;
+ void NotifySessionCreated(uint64_t aBrowsingContextId) override;
+ void NotifySessionDestroyed(uint64_t aBrowsingContextId) override;
+ void UpdateMetadata(uint64_t aBrowsingContextId,
+ const Maybe<MediaMetadataBase>& aMetadata) override;
+ void EnableAction(uint64_t aBrowsingContextId,
+ MediaSessionAction aAction) override;
+ void DisableAction(uint64_t aBrowsingContextId,
+ MediaSessionAction aAction) override;
+ void NotifyMediaFullScreenState(uint64_t aBrowsingContextId,
+ bool aIsInFullScreen) override;
+ void UpdatePositionState(uint64_t aBrowsingContextId,
+ const PositionState& aState) override;
+
+ // Use these methods to register/unregister `ContentMediaControlKeyReceiver`
+ // in order to listen to media control key events.
+ virtual void AddReceiver(ContentMediaControlKeyReceiver* aReceiver) = 0;
+ virtual void RemoveReceiver(ContentMediaControlKeyReceiver* aReceiver) = 0;
+};
+
+/**
+ * ContentMediaController exists in per inner window, which has a responsibility
+ * to update the content media state to MediaController (ContentMediaAgent) and
+ * delivers MediaControlKey to its receiver in order to control media in the
+ * content page (ContentMediaControlKeyReceiver).
+ */
+class ContentMediaController final : public ContentMediaAgent,
+ public ContentMediaControlKeyReceiver {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(ContentMediaController, override)
+
+ explicit ContentMediaController(uint64_t aId);
+ // ContentMediaAgent methods
+ void AddReceiver(ContentMediaControlKeyReceiver* aListener) override;
+ void RemoveReceiver(ContentMediaControlKeyReceiver* aListener) override;
+
+ // ContentMediaControlKeyReceiver method
+ void HandleMediaKey(MediaControlKey aKey) override;
+
+ private:
+ ~ContentMediaController() = default;
+
+ // We don't need this method, so make it as private and simply return false.
+ virtual bool IsPlaying() const override { return false; }
+
+ void PauseOrStopMedia();
+
+ nsTArray<RefPtr<ContentMediaControlKeyReceiver>> mReceivers;
+};
+
+} // namespace mozilla::dom
+
+#endif // DOM_MEDIA_MEDIACONTROL_CONTENTMEDIACONTROLLER_H_
diff --git a/dom/media/mediacontrol/ContentPlaybackController.cpp b/dom/media/mediacontrol/ContentPlaybackController.cpp
new file mode 100644
index 0000000000..ba48c0a5ce
--- /dev/null
+++ b/dom/media/mediacontrol/ContentPlaybackController.cpp
@@ -0,0 +1,210 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ContentPlaybackController.h"
+
+#include "MediaControlUtils.h"
+#include "mozilla/dom/ContentMediaController.h"
+#include "mozilla/dom/MediaSession.h"
+#include "mozilla/dom/Navigator.h"
+#include "mozilla/dom/WindowContext.h"
+#include "mozilla/Telemetry.h"
+#include "nsFocusManager.h"
+
+// avoid redefined macro in unified build
+#undef LOG
+#define LOG(msg, ...) \
+ MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
+ ("ContentPlaybackController=%p, " msg, this, ##__VA_ARGS__))
+
+namespace mozilla::dom {
+
+ContentPlaybackController::ContentPlaybackController(
+ BrowsingContext* aContext) {
+ MOZ_ASSERT(aContext);
+ mBC = aContext;
+}
+
+MediaSession* ContentPlaybackController::GetMediaSession() const {
+ RefPtr<nsPIDOMWindowOuter> window = mBC->GetDOMWindow();
+ if (!window) {
+ return nullptr;
+ }
+
+ RefPtr<Navigator> navigator = window->GetNavigator();
+ if (!navigator) {
+ return nullptr;
+ }
+
+ return navigator->HasCreatedMediaSession() ? navigator->MediaSession()
+ : nullptr;
+}
+
+void ContentPlaybackController::NotifyContentMediaControlKeyReceiver(
+ MediaControlKey aKey) {
+ if (RefPtr<ContentMediaControlKeyReceiver> receiver =
+ ContentMediaControlKeyReceiver::Get(mBC)) {
+ LOG("Handle '%s' in default behavior for BC %" PRIu64,
+ ToMediaControlKeyStr(aKey), mBC->Id());
+ receiver->HandleMediaKey(aKey);
+ }
+}
+
+void ContentPlaybackController::NotifyMediaSession(MediaSessionAction aAction) {
+ MediaSessionActionDetails details;
+ details.mAction = aAction;
+ NotifyMediaSession(details);
+}
+
+void ContentPlaybackController::NotifyMediaSession(
+ const MediaSessionActionDetails& aDetails) {
+ if (RefPtr<MediaSession> session = GetMediaSession()) {
+ LOG("Handle '%s' in media session behavior for BC %" PRIu64,
+ ToMediaSessionActionStr(aDetails.mAction), mBC->Id());
+ MOZ_ASSERT(session->IsActive(), "Notify inactive media session!");
+ session->NotifyHandler(aDetails);
+ }
+}
+
+void ContentPlaybackController::NotifyMediaSessionWhenActionIsSupported(
+ MediaSessionAction aAction) {
+ if (IsMediaSessionActionSupported(aAction)) {
+ NotifyMediaSession(aAction);
+ }
+}
+
+bool ContentPlaybackController::IsMediaSessionActionSupported(
+ MediaSessionAction aAction) const {
+ RefPtr<MediaSession> session = GetMediaSession();
+ return session ? session->IsActive() && session->IsSupportedAction(aAction)
+ : false;
+}
+
+Maybe<uint64_t> ContentPlaybackController::GetActiveMediaSessionId() const {
+ RefPtr<WindowContext> wc = mBC->GetTopWindowContext();
+ return wc ? wc->GetActiveMediaSessionContextId() : Nothing();
+}
+
+void ContentPlaybackController::Focus() {
+ // Focus is not part of the MediaSession standard, so always use the
+ // default behavior and focus the window currently playing media.
+ if (nsCOMPtr<nsPIDOMWindowOuter> win = mBC->GetDOMWindow()) {
+ nsFocusManager::FocusWindow(win, CallerType::System);
+ }
+}
+
+void ContentPlaybackController::Play() {
+ const MediaSessionAction action = MediaSessionAction::Play;
+ RefPtr<MediaSession> session = GetMediaSession();
+ if (IsMediaSessionActionSupported(action)) {
+ NotifyMediaSession(action);
+ }
+ // We don't want to arbitrarily call play default handler, because we want to
+ // resume the frame which a user really gets interest in, not all media in the
+ // same page. Therefore, we would only call default handler for `play` when
+ // (1) We don't have an active media session (If we have one, the play action
+ // handler should only be triggered on that session)
+ // (2) Active media session without setting action handler for `play`
+ else if (!GetActiveMediaSessionId() || (session && session->IsActive())) {
+ NotifyContentMediaControlKeyReceiver(MediaControlKey::Play);
+ }
+}
+
+void ContentPlaybackController::Pause() {
+ const MediaSessionAction action = MediaSessionAction::Pause;
+ if (IsMediaSessionActionSupported(action)) {
+ NotifyMediaSession(action);
+ } else {
+ NotifyContentMediaControlKeyReceiver(MediaControlKey::Pause);
+ }
+}
+
+void ContentPlaybackController::SeekBackward() {
+ NotifyMediaSessionWhenActionIsSupported(MediaSessionAction::Seekbackward);
+}
+
+void ContentPlaybackController::SeekForward() {
+ NotifyMediaSessionWhenActionIsSupported(MediaSessionAction::Seekforward);
+}
+
+void ContentPlaybackController::PreviousTrack() {
+ NotifyMediaSessionWhenActionIsSupported(MediaSessionAction::Previoustrack);
+}
+
+void ContentPlaybackController::NextTrack() {
+ NotifyMediaSessionWhenActionIsSupported(MediaSessionAction::Nexttrack);
+}
+
+void ContentPlaybackController::SkipAd() {
+ NotifyMediaSessionWhenActionIsSupported(MediaSessionAction::Skipad);
+}
+
+void ContentPlaybackController::Stop() {
+ const MediaSessionAction action = MediaSessionAction::Stop;
+ if (IsMediaSessionActionSupported(action)) {
+ NotifyMediaSession(action);
+ } else {
+ NotifyContentMediaControlKeyReceiver(MediaControlKey::Stop);
+ }
+}
+
+void ContentPlaybackController::SeekTo(double aSeekTime, bool aFastSeek) {
+ MediaSessionActionDetails details;
+ details.mAction = MediaSessionAction::Seekto;
+ details.mSeekTime.Construct(aSeekTime);
+ if (aFastSeek) {
+ details.mFastSeek.Construct(aFastSeek);
+ }
+ if (IsMediaSessionActionSupported(details.mAction)) {
+ NotifyMediaSession(details);
+ }
+}
+
+void ContentMediaControlKeyHandler::HandleMediaControlAction(
+ BrowsingContext* aContext, const MediaControlAction& aAction) {
+ MOZ_ASSERT(aContext);
+ // The web content doesn't exist in this browsing context.
+ if (!aContext->GetDocShell()) {
+ return;
+ }
+ ContentPlaybackController controller(aContext);
+ switch (aAction.mKey) {
+ case MediaControlKey::Focus:
+ controller.Focus();
+ return;
+ case MediaControlKey::Play:
+ controller.Play();
+ return;
+ case MediaControlKey::Pause:
+ controller.Pause();
+ return;
+ case MediaControlKey::Stop:
+ controller.Stop();
+ return;
+ case MediaControlKey::Previoustrack:
+ controller.PreviousTrack();
+ return;
+ case MediaControlKey::Nexttrack:
+ controller.NextTrack();
+ return;
+ case MediaControlKey::Seekbackward:
+ controller.SeekBackward();
+ return;
+ case MediaControlKey::Seekforward:
+ controller.SeekForward();
+ return;
+ case MediaControlKey::Skipad:
+ controller.SkipAd();
+ return;
+ case MediaControlKey::Seekto: {
+ const SeekDetails& details = *aAction.mDetails;
+ controller.SeekTo(details.mSeekTime, details.mFastSeek);
+ return;
+ }
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid media control key.");
+ };
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/mediacontrol/ContentPlaybackController.h b/dom/media/mediacontrol/ContentPlaybackController.h
new file mode 100644
index 0000000000..a692dedde0
--- /dev/null
+++ b/dom/media/mediacontrol/ContentPlaybackController.h
@@ -0,0 +1,73 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_MEDIACONTROL_CONTENTPLAYBACKCONTROLLER_H_
+#define DOM_MEDIA_MEDIACONTROL_CONTENTPLAYBACKCONTROLLER_H_
+
+#include "MediaControlKeySource.h"
+#include "nsPIDOMWindow.h"
+#include "mozilla/dom/BrowsingContext.h"
+
+namespace mozilla::dom {
+
+class MediaSession;
+
+/**
+ * This interface is used to handle different playback control actions in the
+ * content process. Most of the methods are designed based on the
+ * MediaSessionAction values, which are defined in the media session spec [1].
+ *
+ * The reason we need that is to explicitly separate the implementation from all
+ * different defined methods. If we want to add a new method in the future, we
+ * can do that without modifying other methods.
+ *
+ * If the active media session has a corresponding MediaSessionActionHandler,
+ * then we would invoke it, or we would do nothing. However, for certain
+ * actions, such as `play`, `pause` and `stop`, we have default action handling
+ * in order to control playback correctly even if the website doesn't use media
+ * session at all or the media session doesn't have correspending action handler
+ * [2].
+ *
+ * [1] https://w3c.github.io/mediasession/#enumdef-mediasessionaction
+ * [2]
+ * https://w3c.github.io/mediasession/#ref-for-active-media-session%E2%91%A1%E2%93%AA
+ */
+class MOZ_STACK_CLASS ContentPlaybackController {
+ public:
+ explicit ContentPlaybackController(BrowsingContext* aContext);
+ ~ContentPlaybackController() = default;
+
+ // TODO: Convert Focus() to MOZ_CAN_RUN_SCRIPT
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY void Focus();
+ void Play();
+ void Pause();
+ void SeekBackward();
+ void SeekForward();
+ void PreviousTrack();
+ void NextTrack();
+ void SkipAd();
+ void Stop();
+ void SeekTo(double aSeekTime, bool aFastSeek);
+
+ private:
+ void NotifyContentMediaControlKeyReceiver(MediaControlKey aKey);
+ void NotifyMediaSession(MediaSessionAction aAction);
+ void NotifyMediaSession(const MediaSessionActionDetails& aDetails);
+ void NotifyMediaSessionWhenActionIsSupported(MediaSessionAction aAction);
+ bool IsMediaSessionActionSupported(MediaSessionAction aAction) const;
+ Maybe<uint64_t> GetActiveMediaSessionId() const;
+ MediaSession* GetMediaSession() const;
+
+ RefPtr<BrowsingContext> mBC;
+};
+
+class ContentMediaControlKeyHandler {
+ public:
+ static void HandleMediaControlAction(BrowsingContext* aContext,
+ const MediaControlAction& aAction);
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/mediacontrol/FetchImageHelper.cpp b/dom/media/mediacontrol/FetchImageHelper.cpp
new file mode 100644
index 0000000000..8ba8d54485
--- /dev/null
+++ b/dom/media/mediacontrol/FetchImageHelper.cpp
@@ -0,0 +1,164 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FetchImageHelper.h"
+
+#include "mozilla/gfx/2D.h"
+#include "mozilla/Logging.h"
+#include "mozilla/NullPrincipal.h"
+#include "nsIChannel.h"
+#include "nsNetUtil.h"
+
+mozilla::LazyLogModule gFetchImageLog("FetchImageHelper");
+
+#undef LOG
+#define LOG(msg, ...) \
+ MOZ_LOG(gFetchImageLog, LogLevel::Debug, \
+ ("FetchImageHelper=%p, " msg, this, ##__VA_ARGS__))
+
+using namespace mozilla::gfx;
+
+namespace mozilla::dom {
+
+FetchImageHelper::FetchImageHelper(const MediaImage& aImage)
+ : mSrc(aImage.mSrc) {}
+
+FetchImageHelper::~FetchImageHelper() { AbortFetchingImage(); }
+
+RefPtr<ImagePromise> FetchImageHelper::FetchImage() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (IsFetchingImage()) {
+ return mPromise.Ensure(__func__);
+ }
+
+ LOG("Start fetching image from %s", NS_ConvertUTF16toUTF8(mSrc).get());
+ nsCOMPtr<nsIURI> uri;
+ if (NS_FAILED(NS_NewURI(getter_AddRefs(uri), mSrc))) {
+ LOG("Failed to create URI");
+ return ImagePromise::CreateAndReject(false, __func__);
+ }
+
+ MOZ_ASSERT(!mListener);
+ mListener = new ImageFetchListener();
+ if (NS_FAILED(mListener->FetchDecodedImageFromURI(uri, this))) {
+ LOG("Failed to decode image from async channel");
+ return ImagePromise::CreateAndReject(false, __func__);
+ }
+ return mPromise.Ensure(__func__);
+}
+
+void FetchImageHelper::AbortFetchingImage() {
+ MOZ_ASSERT(NS_IsMainThread());
+ LOG("AbortFetchingImage");
+ mPromise.RejectIfExists(false, __func__);
+ ClearListenerIfNeeded();
+}
+
+void FetchImageHelper::ClearListenerIfNeeded() {
+ if (mListener) {
+ mListener->Clear();
+ mListener = nullptr;
+ }
+}
+
+bool FetchImageHelper::IsFetchingImage() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return !mPromise.IsEmpty() && mListener;
+}
+
+void FetchImageHelper::HandleFetchSuccess(imgIContainer* aImage) {
+ MOZ_ASSERT(aImage);
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(IsFetchingImage());
+ LOG("Finished fetching image");
+ mPromise.Resolve(aImage, __func__);
+ ClearListenerIfNeeded();
+}
+
+void FetchImageHelper::HandleFetchFail() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(IsFetchingImage());
+ LOG("Reject the promise because of fetching failed");
+ mPromise.RejectIfExists(false, __func__);
+ ClearListenerIfNeeded();
+}
+
+/**
+ * Implementation for FetchImageHelper::ImageFetchListener
+ */
+NS_IMPL_ISUPPORTS(FetchImageHelper::ImageFetchListener, imgIContainerCallback)
+
+FetchImageHelper::ImageFetchListener::~ImageFetchListener() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!mHelper, "Cancel() should be called before desturction!");
+}
+
+nsresult FetchImageHelper::ImageFetchListener::FetchDecodedImageFromURI(
+ nsIURI* aURI, FetchImageHelper* aHelper) {
+ MOZ_ASSERT(!mHelper && !mChannel,
+ "Should call Clear() berfore running another fetching process!");
+ RefPtr<nsIPrincipal> nullPrincipal =
+ NullPrincipal::CreateWithoutOriginAttributes();
+ nsCOMPtr<nsIChannel> channel;
+ nsresult rv =
+ NS_NewChannel(getter_AddRefs(channel), aURI, nullPrincipal,
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_INTERNAL_IMAGE, nullptr, nullptr,
+ nullptr, nullptr, nsIRequest::LOAD_ANONYMOUS);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ nsCOMPtr<imgITools> imgTools = do_GetService("@mozilla.org/image/tools;1");
+ if (!imgTools) {
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = imgTools->DecodeImageFromChannelAsync(aURI, channel, this, nullptr);
+ if (NS_FAILED(rv)) {
+ return NS_ERROR_FAILURE;
+ }
+ MOZ_ASSERT(aHelper);
+ mHelper = aHelper;
+ mChannel = channel;
+ return NS_OK;
+}
+
+void FetchImageHelper::ImageFetchListener::Clear() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mChannel) {
+ mChannel->CancelWithReason(
+ NS_BINDING_ABORTED, "FetchImageHelper::ImageFetchListener::Clear"_ns);
+ mChannel = nullptr;
+ }
+ mHelper = nullptr;
+}
+
+bool FetchImageHelper::ImageFetchListener::IsFetchingImage() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mHelper ? mHelper->IsFetchingImage() : false;
+}
+
+NS_IMETHODIMP FetchImageHelper::ImageFetchListener::OnImageReady(
+ imgIContainer* aImage, nsresult aStatus) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!IsFetchingImage()) {
+ return NS_OK;
+ }
+ // We have received image, so we don't need the channel anymore.
+ mChannel = nullptr;
+
+ MOZ_ASSERT(mHelper);
+ if (NS_FAILED(aStatus) || !aImage) {
+ mHelper->HandleFetchFail();
+ Clear();
+ return aStatus;
+ }
+
+ mHelper->HandleFetchSuccess(aImage);
+
+ return NS_OK;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/mediacontrol/FetchImageHelper.h b/dom/media/mediacontrol/FetchImageHelper.h
new file mode 100644
index 0000000000..eb3e719610
--- /dev/null
+++ b/dom/media/mediacontrol/FetchImageHelper.h
@@ -0,0 +1,82 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_MEDIACONTROL_FETCHIMAGEHELPER_H_
+#define DOM_MEDIA_MEDIACONTROL_FETCHIMAGEHELPER_H_
+
+#include "imgIContainer.h"
+#include "imgITools.h"
+#include "mozilla/dom/MediaSessionBinding.h"
+#include "mozilla/MozPromise.h"
+
+namespace mozilla::dom {
+/**
+ * FetchImageHelper is used to fetch image data from MediaImage, and the fetched
+ * image data would be used to show on the virtual control inferface. The URL of
+ * MediaImage is defined by websites by using MediaSession API [1].
+ *
+ * By using `FetchImage()`, it would return a promise that would resolve with a
+ * `imgIContainer`, then we can get the image data from the container.
+ *
+ * [1] https://w3c.github.io/mediasession/#dictdef-mediaimage
+ */
+using ImagePromise = MozPromise<nsCOMPtr<imgIContainer>, bool,
+ /* IsExclusive = */ true>;
+class FetchImageHelper final {
+ public:
+ explicit FetchImageHelper(const MediaImage& aImage);
+ ~FetchImageHelper();
+
+ // Return a promise which would be resolved with the decoded image surface
+ // when we finish fetching and decoding image data, and it would be rejected
+ // when we fail to fecth the image.
+ RefPtr<ImagePromise> FetchImage();
+
+ // Stop fetching and decoding image and reject the image promise. If we have
+ // not started yet fetching image, then nothing would happen.
+ void AbortFetchingImage();
+
+ // Return true if we're fecthing image.
+ bool IsFetchingImage() const;
+
+ private:
+ /**
+ * ImageFetchListener is used to listen the notification of finishing fetching
+ * image data (via `OnImageReady()`) and finishing decoding image data (via
+ * `Notify()`).
+ */
+ class ImageFetchListener final : public imgIContainerCallback {
+ public:
+ NS_DECL_ISUPPORTS
+ ImageFetchListener() = default;
+
+ // Start an async channel to load the image, and return error if the channel
+ // opens failed. It would use `aHelper::HandleFetchSuccess/Fail()` to notify
+ // the result asynchronously.
+ nsresult FetchDecodedImageFromURI(nsIURI* aURI, FetchImageHelper* aHelper);
+ void Clear();
+ bool IsFetchingImage() const;
+
+ // Method of imgIContainerCallback
+ NS_IMETHOD OnImageReady(imgIContainer* aImage, nsresult aStatus) override;
+
+ private:
+ ~ImageFetchListener();
+
+ FetchImageHelper* MOZ_NON_OWNING_REF mHelper = nullptr;
+ nsCOMPtr<nsIChannel> mChannel;
+ };
+
+ void ClearListenerIfNeeded();
+ void HandleFetchSuccess(imgIContainer* aImage);
+ void HandleFetchFail();
+
+ nsString mSrc;
+ MozPromiseHolder<ImagePromise> mPromise;
+ RefPtr<ImageFetchListener> mListener;
+};
+
+} // namespace mozilla::dom
+
+#endif // DOM_MEDIA_MEDIACONTROL_FETCHIMAGEHELPER_H_
diff --git a/dom/media/mediacontrol/MediaControlIPC.h b/dom/media/mediacontrol/MediaControlIPC.h
new file mode 100644
index 0000000000..100e5832c2
--- /dev/null
+++ b/dom/media/mediacontrol/MediaControlIPC.h
@@ -0,0 +1,75 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef ipc_MediaControlIPC_h
+#define ipc_MediaControlIPC_h
+
+#include "ipc/EnumSerializer.h"
+
+#include "mozilla/dom/MediaControllerBinding.h"
+#include "mozilla/dom/MediaControlKeySource.h"
+#include "mozilla/dom/MediaPlaybackStatus.h"
+
+namespace IPC {
+template <>
+struct ParamTraits<mozilla::dom::MediaControlKey>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::dom::MediaControlKey, mozilla::dom::MediaControlKey::Focus,
+ mozilla::dom::MediaControlKey::Stop> {};
+
+template <>
+struct ParamTraits<mozilla::dom::MediaPlaybackState>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::dom::MediaPlaybackState,
+ mozilla::dom::MediaPlaybackState::eStarted,
+ mozilla::dom::MediaPlaybackState::eStopped> {};
+
+template <>
+struct ParamTraits<mozilla::dom::MediaAudibleState>
+ : public ContiguousEnumSerializerInclusive<
+ mozilla::dom::MediaAudibleState,
+ mozilla::dom::MediaAudibleState::eInaudible,
+ mozilla::dom::MediaAudibleState::eAudible> {};
+
+template <>
+struct ParamTraits<mozilla::dom::SeekDetails> {
+ typedef mozilla::dom::SeekDetails paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mSeekTime);
+ WriteParam(aWriter, aParam.mFastSeek);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ if (!ReadParam(aReader, &aResult->mSeekTime) ||
+ !ReadParam(aReader, &aResult->mFastSeek)) {
+ return false;
+ }
+ return true;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::dom::MediaControlAction> {
+ typedef mozilla::dom::MediaControlAction paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mKey);
+ WriteParam(aWriter, aParam.mDetails);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ if (!ReadParam(aReader, &aResult->mKey) ||
+ !ReadParam(aReader, &aResult->mDetails)) {
+ return false;
+ }
+ return true;
+ }
+};
+
+} // namespace IPC
+
+#endif // mozilla_MediaControlIPC_hh
diff --git a/dom/media/mediacontrol/MediaControlKeyManager.cpp b/dom/media/mediacontrol/MediaControlKeyManager.cpp
new file mode 100644
index 0000000000..4cb562aa84
--- /dev/null
+++ b/dom/media/mediacontrol/MediaControlKeyManager.cpp
@@ -0,0 +1,228 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaControlKeyManager.h"
+
+#include "MediaControlUtils.h"
+#include "MediaControlService.h"
+#include "mozilla/AbstractThread.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/widget/MediaKeysEventSourceFactory.h"
+#include "nsContentUtils.h"
+#include "nsIObserverService.h"
+
+#undef LOG
+#define LOG(msg, ...) \
+ MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
+ ("MediaControlKeyManager=%p, " msg, this, ##__VA_ARGS__))
+
+#undef LOG_INFO
+#define LOG_INFO(msg, ...) \
+ MOZ_LOG(gMediaControlLog, LogLevel::Info, \
+ ("MediaControlKeyManager=%p, " msg, this, ##__VA_ARGS__))
+
+#define MEDIA_CONTROL_PREF "media.hardwaremediakeys.enabled"
+
+namespace mozilla::dom {
+
+bool MediaControlKeyManager::IsOpened() const {
+ return mEventSource && mEventSource->IsOpened();
+}
+
+bool MediaControlKeyManager::Open() {
+ if (IsOpened()) {
+ return true;
+ }
+ const bool isEnabledMediaControl = StartMonitoringControlKeys();
+ if (isEnabledMediaControl) {
+ RefPtr<MediaControlService> service = MediaControlService::GetService();
+ MOZ_ASSERT(service);
+ service->NotifyMediaControlHasEverBeenEnabled();
+ }
+ return isEnabledMediaControl;
+}
+
+void MediaControlKeyManager::Close() {
+ // We don't call parent's `Close()` because we want to keep the listener
+ // (MediaControlKeyHandler) all the time. It would be manually removed by
+ // `MediaControlService` when shutdown.
+ StopMonitoringControlKeys();
+}
+
+MediaControlKeyManager::MediaControlKeyManager()
+ : mObserver(new Observer(this)) {
+ nsContentUtils::RegisterShutdownObserver(mObserver);
+ Preferences::AddStrongObserver(mObserver, MEDIA_CONTROL_PREF);
+}
+
+MediaControlKeyManager::~MediaControlKeyManager() { Shutdown(); }
+
+void MediaControlKeyManager::Shutdown() {
+ StopMonitoringControlKeys();
+ mEventSource = nullptr;
+ if (mObserver) {
+ nsContentUtils::UnregisterShutdownObserver(mObserver);
+ Preferences::RemoveObserver(mObserver, MEDIA_CONTROL_PREF);
+ mObserver = nullptr;
+ }
+}
+
+bool MediaControlKeyManager::StartMonitoringControlKeys() {
+ if (!StaticPrefs::media_hardwaremediakeys_enabled()) {
+ return false;
+ }
+
+ if (!mEventSource) {
+ mEventSource = widget::CreateMediaControlKeySource();
+ }
+ if (mEventSource && mEventSource->Open()) {
+ LOG_INFO("StartMonitoringControlKeys");
+ mEventSource->SetPlaybackState(mPlaybackState);
+ mEventSource->SetMediaMetadata(mMetadata);
+ mEventSource->SetSupportedMediaKeys(mSupportedKeys);
+ mEventSource->AddListener(this);
+ return true;
+ }
+ // Fail to open or create event source (eg. when cross-compiling with MinGW,
+ // we cannot use the related WinAPI)
+ return false;
+}
+
+void MediaControlKeyManager::StopMonitoringControlKeys() {
+ if (!mEventSource || !mEventSource->IsOpened()) {
+ return;
+ }
+
+ LOG_INFO("StopMonitoringControlKeys");
+ mEventSource->Close();
+ if (StaticPrefs::media_mediacontrol_testingevents_enabled()) {
+ // Close the source would reset the displayed playback state and metadata.
+ if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
+ obs->NotifyObservers(nullptr, "media-displayed-playback-changed",
+ nullptr);
+ obs->NotifyObservers(nullptr, "media-displayed-metadata-changed",
+ nullptr);
+ }
+ }
+}
+
+void MediaControlKeyManager::OnActionPerformed(
+ const MediaControlAction& aAction) {
+ for (auto listener : mListeners) {
+ listener->OnActionPerformed(aAction);
+ }
+}
+
+void MediaControlKeyManager::SetPlaybackState(
+ MediaSessionPlaybackState aState) {
+ if (mEventSource && mEventSource->IsOpened()) {
+ mEventSource->SetPlaybackState(aState);
+ }
+ mPlaybackState = aState;
+ LOG_INFO("playbackState=%s", ToMediaSessionPlaybackStateStr(mPlaybackState));
+ if (StaticPrefs::media_mediacontrol_testingevents_enabled()) {
+ if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
+ obs->NotifyObservers(nullptr, "media-displayed-playback-changed",
+ nullptr);
+ }
+ }
+}
+
+MediaSessionPlaybackState MediaControlKeyManager::GetPlaybackState() const {
+ return (mEventSource && mEventSource->IsOpened())
+ ? mEventSource->GetPlaybackState()
+ : mPlaybackState;
+}
+
+void MediaControlKeyManager::SetMediaMetadata(
+ const MediaMetadataBase& aMetadata) {
+ if (mEventSource && mEventSource->IsOpened()) {
+ mEventSource->SetMediaMetadata(aMetadata);
+ }
+ mMetadata = aMetadata;
+ LOG_INFO("title=%s, artist=%s album=%s",
+ NS_ConvertUTF16toUTF8(mMetadata.mTitle).get(),
+ NS_ConvertUTF16toUTF8(mMetadata.mArtist).get(),
+ NS_ConvertUTF16toUTF8(mMetadata.mAlbum).get());
+ if (StaticPrefs::media_mediacontrol_testingevents_enabled()) {
+ if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
+ obs->NotifyObservers(nullptr, "media-displayed-metadata-changed",
+ nullptr);
+ }
+ }
+}
+
+void MediaControlKeyManager::SetSupportedMediaKeys(
+ const MediaKeysArray& aSupportedKeys) {
+ mSupportedKeys.Clear();
+ for (const auto& key : aSupportedKeys) {
+ LOG_INFO("Supported keys=%s", ToMediaControlKeyStr(key));
+ mSupportedKeys.AppendElement(key);
+ }
+ if (mEventSource && mEventSource->IsOpened()) {
+ mEventSource->SetSupportedMediaKeys(mSupportedKeys);
+ }
+}
+
+void MediaControlKeyManager::SetEnableFullScreen(bool aIsEnabled) {
+ LOG_INFO("Set fullscreen %s", aIsEnabled ? "enabled" : "disabled");
+ if (mEventSource && mEventSource->IsOpened()) {
+ mEventSource->SetEnableFullScreen(aIsEnabled);
+ }
+}
+
+void MediaControlKeyManager::SetEnablePictureInPictureMode(bool aIsEnabled) {
+ LOG_INFO("Set Picture-In-Picture mode %s",
+ aIsEnabled ? "enabled" : "disabled");
+ if (mEventSource && mEventSource->IsOpened()) {
+ mEventSource->SetEnablePictureInPictureMode(aIsEnabled);
+ }
+}
+
+void MediaControlKeyManager::SetPositionState(const PositionState& aState) {
+ LOG_INFO("Set PositionState, duration=%f, playbackRate=%f, position=%f",
+ aState.mDuration, aState.mPlaybackRate,
+ aState.mLastReportedPlaybackPosition);
+ if (mEventSource && mEventSource->IsOpened()) {
+ mEventSource->SetPositionState(aState);
+ }
+}
+
+void MediaControlKeyManager::OnPreferenceChange() {
+ const bool isPrefEnabled = StaticPrefs::media_hardwaremediakeys_enabled();
+ // Only start monitoring control keys when the pref is on and having a main
+ // controller that means already having media which need to be controlled.
+ const bool shouldMonitorKeys =
+ isPrefEnabled && MediaControlService::GetService()->GetMainController();
+ LOG_INFO("Preference change : %s media control",
+ isPrefEnabled ? "enable" : "disable");
+ if (shouldMonitorKeys) {
+ Unused << StartMonitoringControlKeys();
+ } else {
+ StopMonitoringControlKeys();
+ }
+}
+
+NS_IMPL_ISUPPORTS(MediaControlKeyManager::Observer, nsIObserver);
+
+MediaControlKeyManager::Observer::Observer(MediaControlKeyManager* aManager)
+ : mManager(aManager) {}
+
+NS_IMETHODIMP
+MediaControlKeyManager::Observer::Observe(nsISupports* aSubject,
+ const char* aTopic,
+ const char16_t* aData) {
+ if (!strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID)) {
+ mManager->Shutdown();
+ } else if (!strcmp(aTopic, "nsPref:changed")) {
+ mManager->OnPreferenceChange();
+ }
+ return NS_OK;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/mediacontrol/MediaControlKeyManager.h b/dom/media/mediacontrol/MediaControlKeyManager.h
new file mode 100644
index 0000000000..feb857e335
--- /dev/null
+++ b/dom/media/mediacontrol/MediaControlKeyManager.h
@@ -0,0 +1,73 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_MEDIACONTROL_MEDIACONTROLKEYMANAGER_H_
+#define DOM_MEDIA_MEDIACONTROL_MEDIACONTROLKEYMANAGER_H_
+
+#include "MediaControlKeySource.h"
+#include "MediaEventSource.h"
+#include "nsIObserver.h"
+
+namespace mozilla::dom {
+
+/**
+ * MediaControlKeyManager is a wrapper of MediaControlKeySource, which
+ * is used to manage creating and destroying a real media keys event source.
+ *
+ * It monitors the amount of the media controller in MediaService, and would
+ * create the event source when there is any existing controller and destroy it
+ * when there is no controller.
+ */
+class MediaControlKeyManager final : public MediaControlKeySource,
+ public MediaControlKeyListener {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(MediaControlKeyManager, override)
+
+ MediaControlKeyManager();
+
+ // MediaControlKeySource methods
+ bool Open() override;
+ void Close() override;
+ bool IsOpened() const override;
+
+ void SetPlaybackState(MediaSessionPlaybackState aState) override;
+ MediaSessionPlaybackState GetPlaybackState() const override;
+
+ // MediaControlKeyListener methods
+ void OnActionPerformed(const MediaControlAction& aAction) override;
+
+ void SetMediaMetadata(const MediaMetadataBase& aMetadata) override;
+ void SetSupportedMediaKeys(const MediaKeysArray& aSupportedKeys) override;
+ void SetEnableFullScreen(bool aIsEnabled) override;
+ void SetEnablePictureInPictureMode(bool aIsEnabled) override;
+ void SetPositionState(const PositionState& aState) override;
+
+ private:
+ ~MediaControlKeyManager();
+ void Shutdown();
+
+ class Observer final : public nsIObserver {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+ explicit Observer(MediaControlKeyManager* aManager);
+
+ protected:
+ virtual ~Observer() = default;
+
+ MediaControlKeyManager* MOZ_OWNING_REF mManager;
+ };
+ RefPtr<Observer> mObserver;
+ void OnPreferenceChange();
+
+ bool StartMonitoringControlKeys();
+ void StopMonitoringControlKeys();
+ RefPtr<MediaControlKeySource> mEventSource;
+ MediaMetadataBase mMetadata;
+ nsTArray<MediaControlKey> mSupportedKeys;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/mediacontrol/MediaControlKeySource.cpp b/dom/media/mediacontrol/MediaControlKeySource.cpp
new file mode 100644
index 0000000000..22756c860a
--- /dev/null
+++ b/dom/media/mediacontrol/MediaControlKeySource.cpp
@@ -0,0 +1,122 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaControlKeySource.h"
+
+#include "MediaController.h"
+#include "MediaControlUtils.h"
+#include "MediaControlService.h"
+#include "mozilla/Logging.h"
+
+namespace mozilla::dom {
+
+// avoid redefined macro in unified build
+#undef LOG_SOURCE
+#define LOG_SOURCE(msg, ...) \
+ MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
+ ("MediaControlKeySource=%p, " msg, this, ##__VA_ARGS__))
+
+#undef LOG_KEY
+#define LOG_KEY(msg, key, ...) \
+ MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
+ ("MediaControlKeyHandler=%p, " msg, this, ToMediaControlKeyStr(key), \
+ ##__VA_ARGS__));
+
+void MediaControlKeyHandler::OnActionPerformed(
+ const MediaControlAction& aAction) {
+ LOG_KEY("OnActionPerformed '%s'", aAction.mKey);
+
+ RefPtr<MediaControlService> service = MediaControlService::GetService();
+ MOZ_ASSERT(service);
+ RefPtr<IMediaController> controller = service->GetMainController();
+ if (!controller) {
+ return;
+ }
+
+ switch (aAction.mKey) {
+ case MediaControlKey::Focus:
+ controller->Focus();
+ return;
+ case MediaControlKey::Play:
+ controller->Play();
+ return;
+ case MediaControlKey::Pause:
+ controller->Pause();
+ return;
+ case MediaControlKey::Playpause: {
+ if (controller->IsPlaying()) {
+ controller->Pause();
+ } else {
+ controller->Play();
+ }
+ return;
+ }
+ case MediaControlKey::Previoustrack:
+ controller->PrevTrack();
+ return;
+ case MediaControlKey::Nexttrack:
+ controller->NextTrack();
+ return;
+ case MediaControlKey::Seekbackward:
+ controller->SeekBackward();
+ return;
+ case MediaControlKey::Seekforward:
+ controller->SeekForward();
+ return;
+ case MediaControlKey::Skipad:
+ controller->SkipAd();
+ return;
+ case MediaControlKey::Seekto: {
+ const SeekDetails& details = *aAction.mDetails;
+ controller->SeekTo(details.mSeekTime, details.mFastSeek);
+ return;
+ }
+ case MediaControlKey::Stop:
+ controller->Stop();
+ return;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Error : undefined media key!");
+ return;
+ }
+}
+
+MediaControlKeySource::MediaControlKeySource()
+ : mPlaybackState(MediaSessionPlaybackState::None) {}
+
+void MediaControlKeySource::AddListener(MediaControlKeyListener* aListener) {
+ MOZ_ASSERT(aListener);
+ LOG_SOURCE("Add listener %p", aListener);
+ mListeners.AppendElement(aListener);
+}
+
+void MediaControlKeySource::RemoveListener(MediaControlKeyListener* aListener) {
+ MOZ_ASSERT(aListener);
+ LOG_SOURCE("Remove listener %p", aListener);
+ mListeners.RemoveElement(aListener);
+}
+
+size_t MediaControlKeySource::GetListenersNum() const {
+ return mListeners.Length();
+}
+
+void MediaControlKeySource::Close() {
+ LOG_SOURCE("Close source");
+ mListeners.Clear();
+}
+
+void MediaControlKeySource::SetPlaybackState(MediaSessionPlaybackState aState) {
+ if (mPlaybackState == aState) {
+ return;
+ }
+ LOG_SOURCE("SetPlaybackState '%s'", ToMediaSessionPlaybackStateStr(aState));
+ mPlaybackState = aState;
+}
+
+MediaSessionPlaybackState MediaControlKeySource::GetPlaybackState() const {
+ return mPlaybackState;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/mediacontrol/MediaControlKeySource.h b/dom/media/mediacontrol/MediaControlKeySource.h
new file mode 100644
index 0000000000..f5d62a429e
--- /dev/null
+++ b/dom/media/mediacontrol/MediaControlKeySource.h
@@ -0,0 +1,122 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_MEDIACONTROL_MEDIACONTROLKEYSOURCE_H_
+#define DOM_MEDIA_MEDIACONTROL_MEDIACONTROLKEYSOURCE_H_
+
+#include "mozilla/dom/MediaControllerBinding.h"
+#include "mozilla/dom/MediaMetadata.h"
+#include "mozilla/dom/MediaSession.h"
+#include "mozilla/dom/MediaSessionBinding.h"
+#include "nsISupportsImpl.h"
+#include "nsTArray.h"
+
+namespace mozilla::dom {
+
+// This is used to store seek related properties from MediaSessionActionDetails.
+// However, currently we have no plan to support `seekOffset`.
+// https://w3c.github.io/mediasession/#the-mediasessionactiondetails-dictionary
+struct SeekDetails {
+ SeekDetails() = default;
+ explicit SeekDetails(double aSeekTime) : mSeekTime(aSeekTime) {}
+ SeekDetails(double aSeekTime, bool aFastSeek)
+ : mSeekTime(aSeekTime), mFastSeek(aFastSeek) {}
+ double mSeekTime = 0.0;
+ bool mFastSeek = false;
+};
+
+struct MediaControlAction {
+ MediaControlAction() = default;
+ explicit MediaControlAction(MediaControlKey aKey) : mKey(aKey) {}
+ MediaControlAction(MediaControlKey aKey, const SeekDetails& aDetails)
+ : mKey(aKey), mDetails(Some(aDetails)) {}
+ MediaControlKey mKey = MediaControlKey::EndGuard_;
+ Maybe<SeekDetails> mDetails;
+};
+
+/**
+ * MediaControlKeyListener is a pure interface, which is used to monitor
+ * MediaControlKey, we can add it onto the MediaControlKeySource,
+ * and then everytime when the media key events occur, `OnActionPerformed` will
+ * be called so that we can do related handling.
+ */
+class MediaControlKeyListener {
+ public:
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+ MediaControlKeyListener() = default;
+
+ virtual void OnActionPerformed(const MediaControlAction& aAction) = 0;
+
+ protected:
+ virtual ~MediaControlKeyListener() = default;
+};
+
+/**
+ * MediaControlKeyHandler is used to operate media controller by corresponding
+ * received media control key events.
+ */
+class MediaControlKeyHandler final : public MediaControlKeyListener {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(MediaControlKeyHandler, override)
+ void OnActionPerformed(const MediaControlAction& aAction) override;
+
+ private:
+ virtual ~MediaControlKeyHandler() = default;
+};
+
+/**
+ * MediaControlKeySource is an abstract class which is used to implement
+ * transporting media control keys event to all its listeners when media keys
+ * event happens.
+ */
+class MediaControlKeySource {
+ public:
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+ MediaControlKeySource();
+
+ using MediaKeysArray = nsTArray<MediaControlKey>;
+
+ virtual void AddListener(MediaControlKeyListener* aListener);
+ virtual void RemoveListener(MediaControlKeyListener* aListener);
+ size_t GetListenersNum() const;
+
+ // Return true if the initialization of the source succeeds, and inherited
+ // sources should implement this method to handle the initialization fails.
+ virtual bool Open() = 0;
+ virtual void Close();
+ virtual bool IsOpened() const = 0;
+
+ /**
+ * All following `SetXXX()` functions are used to update the playback related
+ * properties change from a specific tab, which can represent the playback
+ * status for Firefox instance. Even if we have multiple tabs playing media at
+ * the same time, we would only update information from one of that tabs that
+ * would be done by `MediaControlService`.
+ */
+ virtual void SetPlaybackState(MediaSessionPlaybackState aState);
+ virtual MediaSessionPlaybackState GetPlaybackState() const;
+
+ // Override this method if the event source needs to handle the metadata.
+ virtual void SetMediaMetadata(const MediaMetadataBase& aMetadata) {}
+
+ // Set the supported media keys which the event source can use to determine
+ // what kinds of buttons should be shown on the UI.
+ virtual void SetSupportedMediaKeys(const MediaKeysArray& aSupportedKeys) = 0;
+
+ // Override these methods if the inherited key source want to know the change
+ // for following attributes. For example, GeckoView would use these methods
+ // to notify change to the embedded application.
+ virtual void SetEnableFullScreen(bool aIsEnabled){};
+ virtual void SetEnablePictureInPictureMode(bool aIsEnabled){};
+ virtual void SetPositionState(const PositionState& aState){};
+
+ protected:
+ virtual ~MediaControlKeySource() = default;
+ nsTArray<RefPtr<MediaControlKeyListener>> mListeners;
+ MediaSessionPlaybackState mPlaybackState;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/mediacontrol/MediaControlService.cpp b/dom/media/mediacontrol/MediaControlService.cpp
new file mode 100644
index 0000000000..c321e080d2
--- /dev/null
+++ b/dom/media/mediacontrol/MediaControlService.cpp
@@ -0,0 +1,540 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaControlService.h"
+
+#include "MediaController.h"
+#include "MediaControlUtils.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/intl/Localization.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/Telemetry.h"
+#include "nsIObserverService.h"
+#include "nsXULAppAPI.h"
+
+using mozilla::intl::Localization;
+
+#undef LOG
+#define LOG(msg, ...) \
+ MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
+ ("MediaControlService=%p, " msg, this, ##__VA_ARGS__))
+
+#undef LOG_MAINCONTROLLER
+#define LOG_MAINCONTROLLER(msg, ...) \
+ MOZ_LOG(gMediaControlLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
+
+#undef LOG_MAINCONTROLLER_INFO
+#define LOG_MAINCONTROLLER_INFO(msg, ...) \
+ MOZ_LOG(gMediaControlLog, LogLevel::Info, (msg, ##__VA_ARGS__))
+
+namespace mozilla::dom {
+
+StaticRefPtr<MediaControlService> gMediaControlService;
+static bool sIsXPCOMShutdown = false;
+
+/* static */
+RefPtr<MediaControlService> MediaControlService::GetService() {
+ MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess(),
+ "MediaControlService only runs on Chrome process!");
+ if (sIsXPCOMShutdown) {
+ return nullptr;
+ }
+ if (!gMediaControlService) {
+ gMediaControlService = new MediaControlService();
+ gMediaControlService->Init();
+ }
+ RefPtr<MediaControlService> service = gMediaControlService.get();
+ return service;
+}
+
+/* static */
+void MediaControlService::GenerateMediaControlKey(const GlobalObject& global,
+ MediaControlKey aKey) {
+ RefPtr<MediaControlService> service = MediaControlService::GetService();
+ if (service) {
+ service->GenerateTestMediaControlKey(aKey);
+ }
+}
+
+/* static */
+void MediaControlService::GetCurrentActiveMediaMetadata(
+ const GlobalObject& aGlobal, MediaMetadataInit& aMetadata) {
+ if (RefPtr<MediaControlService> service = MediaControlService::GetService()) {
+ MediaMetadataBase metadata = service->GetMainControllerMediaMetadata();
+ aMetadata.mTitle = metadata.mTitle;
+ aMetadata.mArtist = metadata.mArtist;
+ aMetadata.mAlbum = metadata.mAlbum;
+ for (const auto& artwork : metadata.mArtwork) {
+ // If OOM happens resulting in not able to append the element, then we
+ // would get incorrect result and fail on test, so we don't need to throw
+ // an error explicitly.
+ if (MediaImage* image = aMetadata.mArtwork.AppendElement(fallible)) {
+ image->mSrc = artwork.mSrc;
+ image->mSizes = artwork.mSizes;
+ image->mType = artwork.mType;
+ }
+ }
+ }
+}
+
+/* static */
+MediaSessionPlaybackState
+MediaControlService::GetCurrentMediaSessionPlaybackState(
+ GlobalObject& aGlobal) {
+ if (RefPtr<MediaControlService> service = MediaControlService::GetService()) {
+ return service->GetMainControllerPlaybackState();
+ }
+ return MediaSessionPlaybackState::None;
+}
+
+NS_INTERFACE_MAP_BEGIN(MediaControlService)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver)
+ NS_INTERFACE_MAP_ENTRY(nsIObserver)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_ADDREF(MediaControlService)
+NS_IMPL_RELEASE(MediaControlService)
+
+MediaControlService::MediaControlService() {
+ LOG("create media control service");
+ RefPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->AddObserver(this, "xpcom-shutdown", false);
+ }
+}
+
+void MediaControlService::Init() {
+ mMediaKeysHandler = new MediaControlKeyHandler();
+ mMediaControlKeyManager = new MediaControlKeyManager();
+ mMediaControlKeyManager->AddListener(mMediaKeysHandler.get());
+ mControllerManager = MakeUnique<ControllerManager>(this);
+
+ // Initialize the fallback title
+ nsTArray<nsCString> resIds{
+ "branding/brand.ftl"_ns,
+ "dom/media.ftl"_ns,
+ };
+ RefPtr<Localization> l10n = Localization::Create(resIds, true);
+ {
+ nsAutoCString translation;
+ IgnoredErrorResult rv;
+ l10n->FormatValueSync("mediastatus-fallback-title"_ns, {}, translation, rv);
+ if (!rv.Failed()) {
+ mFallbackTitle = NS_ConvertUTF8toUTF16(translation);
+ }
+ }
+}
+
+MediaControlService::~MediaControlService() {
+ LOG("destroy media control service");
+ Shutdown();
+}
+
+void MediaControlService::NotifyMediaControlHasEverBeenUsed() {
+ // We've already updated the telemetry for using meida control.
+ if (mHasEverUsedMediaControl) {
+ return;
+ }
+ mHasEverUsedMediaControl = true;
+ const uint32_t usedOnMediaControl = 1;
+#ifdef XP_WIN
+ Telemetry::ScalarSet(Telemetry::ScalarID::MEDIA_CONTROL_PLATFORM_USAGE,
+ u"Windows"_ns, usedOnMediaControl);
+#endif
+#ifdef XP_MACOSX
+ Telemetry::ScalarSet(Telemetry::ScalarID::MEDIA_CONTROL_PLATFORM_USAGE,
+ u"MacOS"_ns, usedOnMediaControl);
+#endif
+#ifdef MOZ_WIDGET_GTK
+ Telemetry::ScalarSet(Telemetry::ScalarID::MEDIA_CONTROL_PLATFORM_USAGE,
+ u"Linux"_ns, usedOnMediaControl);
+#endif
+#ifdef MOZ_WIDGET_ANDROID
+ Telemetry::ScalarSet(Telemetry::ScalarID::MEDIA_CONTROL_PLATFORM_USAGE,
+ u"Android"_ns, usedOnMediaControl);
+#endif
+}
+
+void MediaControlService::NotifyMediaControlHasEverBeenEnabled() {
+ // We've already enabled the service and update the telemetry.
+ if (mHasEverEnabledMediaControl) {
+ return;
+ }
+ mHasEverEnabledMediaControl = true;
+ const uint32_t enableOnMediaControl = 0;
+#ifdef XP_WIN
+ Telemetry::ScalarSet(Telemetry::ScalarID::MEDIA_CONTROL_PLATFORM_USAGE,
+ u"Windows"_ns, enableOnMediaControl);
+#endif
+#ifdef XP_MACOSX
+ Telemetry::ScalarSet(Telemetry::ScalarID::MEDIA_CONTROL_PLATFORM_USAGE,
+ u"MacOS"_ns, enableOnMediaControl);
+#endif
+#ifdef MOZ_WIDGET_GTK
+ Telemetry::ScalarSet(Telemetry::ScalarID::MEDIA_CONTROL_PLATFORM_USAGE,
+ u"Linux"_ns, enableOnMediaControl);
+#endif
+#ifdef MOZ_WIDGET_ANDROID
+ Telemetry::ScalarSet(Telemetry::ScalarID::MEDIA_CONTROL_PLATFORM_USAGE,
+ u"Android"_ns, enableOnMediaControl);
+#endif
+}
+
+NS_IMETHODIMP
+MediaControlService::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ if (!strcmp(aTopic, "xpcom-shutdown")) {
+ LOG("XPCOM shutdown");
+ MOZ_ASSERT(gMediaControlService);
+ RefPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->RemoveObserver(this, "xpcom-shutdown");
+ }
+ Shutdown();
+ sIsXPCOMShutdown = true;
+ gMediaControlService = nullptr;
+ }
+ return NS_OK;
+}
+
+void MediaControlService::Shutdown() {
+ mControllerManager->Shutdown();
+ mMediaControlKeyManager->RemoveListener(mMediaKeysHandler.get());
+}
+
+bool MediaControlService::RegisterActiveMediaController(
+ MediaController* aController) {
+ MOZ_DIAGNOSTIC_ASSERT(mControllerManager,
+ "Register controller before initializing service");
+ if (!mControllerManager->AddController(aController)) {
+ LOG("Fail to register controller %" PRId64, aController->Id());
+ return false;
+ }
+ LOG("Register media controller %" PRId64 ", currentNum=%" PRId64,
+ aController->Id(), GetActiveControllersNum());
+ if (StaticPrefs::media_mediacontrol_testingevents_enabled()) {
+ if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
+ obs->NotifyObservers(nullptr, "media-controller-amount-changed", nullptr);
+ }
+ }
+ return true;
+}
+
+bool MediaControlService::UnregisterActiveMediaController(
+ MediaController* aController) {
+ MOZ_DIAGNOSTIC_ASSERT(mControllerManager,
+ "Unregister controller before initializing service");
+ if (!mControllerManager->RemoveController(aController)) {
+ LOG("Fail to unregister controller %" PRId64, aController->Id());
+ return false;
+ }
+ LOG("Unregister media controller %" PRId64 ", currentNum=%" PRId64,
+ aController->Id(), GetActiveControllersNum());
+ if (StaticPrefs::media_mediacontrol_testingevents_enabled()) {
+ if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
+ obs->NotifyObservers(nullptr, "media-controller-amount-changed", nullptr);
+ }
+ }
+ return true;
+}
+
+void MediaControlService::NotifyControllerPlaybackStateChanged(
+ MediaController* aController) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ mControllerManager,
+ "controller state change happens before initializing service");
+ MOZ_DIAGNOSTIC_ASSERT(aController);
+ // The controller is not an active controller.
+ if (!mControllerManager->Contains(aController)) {
+ return;
+ }
+
+ // The controller is the main controller, propagate its playback state.
+ if (GetMainController() == aController) {
+ mControllerManager->MainControllerPlaybackStateChanged(
+ aController->PlaybackState());
+ return;
+ }
+
+ // The controller is not the main controller, but will become a new main
+ // controller. As the service can contains multiple controllers and only one
+ // controller can be controlled by media control keys. Therefore, when
+ // controller's state becomes `playing`, then we would like to let that
+ // controller being controlled, rather than other controller which might not
+ // be playing at the time.
+ if (GetMainController() != aController &&
+ aController->PlaybackState() == MediaSessionPlaybackState::Playing) {
+ mControllerManager->UpdateMainControllerIfNeeded(aController);
+ }
+}
+
+void MediaControlService::RequestUpdateMainController(
+ MediaController* aController) {
+ MOZ_DIAGNOSTIC_ASSERT(aController);
+ MOZ_DIAGNOSTIC_ASSERT(
+ mControllerManager,
+ "using controller in PIP mode before initializing service");
+ // The controller is not an active controller.
+ if (!mControllerManager->Contains(aController)) {
+ return;
+ }
+ mControllerManager->UpdateMainControllerIfNeeded(aController);
+}
+
+uint64_t MediaControlService::GetActiveControllersNum() const {
+ MOZ_DIAGNOSTIC_ASSERT(mControllerManager);
+ return mControllerManager->GetControllersNum();
+}
+
+MediaController* MediaControlService::GetMainController() const {
+ MOZ_DIAGNOSTIC_ASSERT(mControllerManager);
+ return mControllerManager->GetMainController();
+}
+
+void MediaControlService::GenerateTestMediaControlKey(MediaControlKey aKey) {
+ if (!StaticPrefs::media_mediacontrol_testingevents_enabled()) {
+ return;
+ }
+ // Generate a seek details for `seekto`
+ if (aKey == MediaControlKey::Seekto) {
+ mMediaKeysHandler->OnActionPerformed(
+ MediaControlAction(aKey, SeekDetails()));
+ } else {
+ mMediaKeysHandler->OnActionPerformed(MediaControlAction(aKey));
+ }
+}
+
+MediaMetadataBase MediaControlService::GetMainControllerMediaMetadata() const {
+ MediaMetadataBase metadata;
+ if (!StaticPrefs::media_mediacontrol_testingevents_enabled()) {
+ return metadata;
+ }
+ return GetMainController() ? GetMainController()->GetCurrentMediaMetadata()
+ : metadata;
+}
+
+MediaSessionPlaybackState MediaControlService::GetMainControllerPlaybackState()
+ const {
+ if (!StaticPrefs::media_mediacontrol_testingevents_enabled()) {
+ return MediaSessionPlaybackState::None;
+ }
+ return GetMainController() ? GetMainController()->PlaybackState()
+ : MediaSessionPlaybackState::None;
+}
+
+nsString MediaControlService::GetFallbackTitle() const {
+ return mFallbackTitle;
+}
+
+// Following functions belong to ControllerManager
+MediaControlService::ControllerManager::ControllerManager(
+ MediaControlService* aService)
+ : mSource(aService->GetMediaControlKeySource()) {
+ MOZ_ASSERT(mSource);
+}
+
+bool MediaControlService::ControllerManager::AddController(
+ MediaController* aController) {
+ MOZ_DIAGNOSTIC_ASSERT(aController);
+ if (mControllers.contains(aController)) {
+ return false;
+ }
+ mControllers.insertBack(aController);
+ UpdateMainControllerIfNeeded(aController);
+ return true;
+}
+
+bool MediaControlService::ControllerManager::RemoveController(
+ MediaController* aController) {
+ MOZ_DIAGNOSTIC_ASSERT(aController);
+ if (!mControllers.contains(aController)) {
+ return false;
+ }
+ // This is LinkedListElement's method which will remove controller from
+ // `mController`.
+ static_cast<LinkedListControllerPtr>(aController)->remove();
+ // If main controller is removed from the list, the last controller in the
+ // list would become the main controller. Or reset the main controller when
+ // the list is already empty.
+ if (GetMainController() == aController) {
+ UpdateMainControllerInternal(
+ mControllers.isEmpty() ? nullptr : mControllers.getLast());
+ }
+ return true;
+}
+
+void MediaControlService::ControllerManager::UpdateMainControllerIfNeeded(
+ MediaController* aController) {
+ MOZ_DIAGNOSTIC_ASSERT(aController);
+
+ if (GetMainController() == aController) {
+ LOG_MAINCONTROLLER("This controller is alreay the main controller");
+ return;
+ }
+
+ if (GetMainController() &&
+ GetMainController()->IsBeingUsedInPIPModeOrFullscreen() &&
+ !aController->IsBeingUsedInPIPModeOrFullscreen()) {
+ LOG_MAINCONTROLLER(
+ "Normal media controller can't replace the controller being used in "
+ "PIP mode or fullscreen");
+ return ReorderGivenController(aController,
+ InsertOptions::eInsertAsNormalController);
+ }
+ ReorderGivenController(aController, InsertOptions::eInsertAsMainController);
+ UpdateMainControllerInternal(aController);
+}
+
+void MediaControlService::ControllerManager::ReorderGivenController(
+ MediaController* aController, InsertOptions aOption) {
+ MOZ_DIAGNOSTIC_ASSERT(aController);
+ MOZ_DIAGNOSTIC_ASSERT(mControllers.contains(aController));
+ // Reset the controller's position and make it not in any list.
+ static_cast<LinkedListControllerPtr>(aController)->remove();
+
+ if (aOption == InsertOptions::eInsertAsMainController) {
+ // Make the main controller as the last element in the list to maintain the
+ // order of controllers because we always use the last controller in the
+ // list as the next main controller when removing current main controller
+ // from the list. Eg. If the list contains [A, B, C], and now the last
+ // element C is the main controller. When B becomes main controller later,
+ // the list would become [A, C, B]. And if A becomes main controller, list
+ // would become [C, B, A]. Then, if we remove A from the list, the next main
+ // controller would be B. But if we don't maintain the controller order when
+ // main controller changes, we would pick C as the main controller because
+ // the list is still [A, B, C].
+ return mControllers.insertBack(aController);
+ }
+
+ MOZ_ASSERT(aOption == InsertOptions::eInsertAsNormalController);
+ MOZ_ASSERT(GetMainController() != aController);
+ // We might have multiple controllers which have higher priority (being used
+ // in PIP or fullscreen) from the head, the normal controller should be
+ // inserted before them. Therefore, search a higher priority controller from
+ // the head and insert new controller before it.
+ // Eg. a list [A, B, C, D, E] and D and E have higher priority, if we want
+ // to insert F, then the final result would be [A, B, C, F, D, E]
+ auto* current = static_cast<LinkedListControllerPtr>(mControllers.getFirst());
+ while (!static_cast<MediaController*>(current)
+ ->IsBeingUsedInPIPModeOrFullscreen()) {
+ current = current->getNext();
+ }
+ MOZ_ASSERT(current, "Should have at least one higher priority controller!");
+ current->setPrevious(aController);
+}
+
+void MediaControlService::ControllerManager::Shutdown() {
+ mControllers.clear();
+ DisconnectMainControllerEvents();
+}
+
+void MediaControlService::ControllerManager::MainControllerPlaybackStateChanged(
+ MediaSessionPlaybackState aState) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mSource->SetPlaybackState(aState);
+}
+
+void MediaControlService::ControllerManager::MainControllerMetadataChanged(
+ const MediaMetadataBase& aMetadata) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mSource->SetMediaMetadata(aMetadata);
+}
+
+void MediaControlService::ControllerManager::UpdateMainControllerInternal(
+ MediaController* aController) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (aController) {
+ aController->Select();
+ }
+ if (mMainController) {
+ mMainController->Unselect();
+ }
+ mMainController = aController;
+
+ if (!mMainController) {
+ LOG_MAINCONTROLLER_INFO("Clear main controller");
+ mSource->Close();
+ DisconnectMainControllerEvents();
+ } else {
+ LOG_MAINCONTROLLER_INFO("Set controller %" PRId64 " as main controller",
+ mMainController->Id());
+ if (!mSource->Open()) {
+ LOG("Failed to open source for monitoring media keys");
+ }
+ // We would still update those status to the event source even if it failed
+ // to open, because it would save the result and set them to the real
+ // source when it opens. In addition, another benefit to do that is to
+ // prevent testing from affecting by platform specific issues, because our
+ // testing events rely on those status changes and they are all platform
+ // independent.
+ mSource->SetPlaybackState(mMainController->PlaybackState());
+ mSource->SetMediaMetadata(mMainController->GetCurrentMediaMetadata());
+ mSource->SetSupportedMediaKeys(mMainController->GetSupportedMediaKeys());
+ ConnectMainControllerEvents();
+ }
+
+ if (StaticPrefs::media_mediacontrol_testingevents_enabled()) {
+ if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
+ obs->NotifyObservers(nullptr, "main-media-controller-changed", nullptr);
+ }
+ }
+}
+
+void MediaControlService::ControllerManager::ConnectMainControllerEvents() {
+ // As main controller has been changed, we should disconnect listeners from
+ // the previous controller and reconnect them to the new controller.
+ DisconnectMainControllerEvents();
+ // Listen to main controller's event in order to propagate the content that
+ // might be displayed on the virtual control interface created by the source.
+ mMetadataChangedListener = mMainController->MetadataChangedEvent().Connect(
+ AbstractThread::MainThread(), this,
+ &ControllerManager::MainControllerMetadataChanged);
+ mSupportedKeysChangedListener =
+ mMainController->SupportedKeysChangedEvent().Connect(
+ AbstractThread::MainThread(),
+ [this](const MediaKeysArray& aSupportedKeys) {
+ mSource->SetSupportedMediaKeys(aSupportedKeys);
+ });
+ mFullScreenChangedListener =
+ mMainController->FullScreenChangedEvent().Connect(
+ AbstractThread::MainThread(), [this](bool aIsEnabled) {
+ mSource->SetEnableFullScreen(aIsEnabled);
+ });
+ mPictureInPictureModeChangedListener =
+ mMainController->PictureInPictureModeChangedEvent().Connect(
+ AbstractThread::MainThread(), [this](bool aIsEnabled) {
+ mSource->SetEnablePictureInPictureMode(aIsEnabled);
+ });
+ mPositionChangedListener = mMainController->PositionChangedEvent().Connect(
+ AbstractThread::MainThread(), [this](const PositionState& aState) {
+ mSource->SetPositionState(aState);
+ });
+}
+
+void MediaControlService::ControllerManager::DisconnectMainControllerEvents() {
+ mMetadataChangedListener.DisconnectIfExists();
+ mSupportedKeysChangedListener.DisconnectIfExists();
+ mFullScreenChangedListener.DisconnectIfExists();
+ mPictureInPictureModeChangedListener.DisconnectIfExists();
+ mPositionChangedListener.DisconnectIfExists();
+}
+
+MediaController* MediaControlService::ControllerManager::GetMainController()
+ const {
+ return mMainController.get();
+}
+
+uint64_t MediaControlService::ControllerManager::GetControllersNum() const {
+ return mControllers.length();
+}
+
+bool MediaControlService::ControllerManager::Contains(
+ MediaController* aController) const {
+ return mControllers.contains(aController);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/mediacontrol/MediaControlService.h b/dom/media/mediacontrol/MediaControlService.h
new file mode 100644
index 0000000000..d0112ffe3c
--- /dev/null
+++ b/dom/media/mediacontrol/MediaControlService.h
@@ -0,0 +1,181 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_MEDIACONTROL_MEDIACONTROLSERVICE_H_
+#define DOM_MEDIA_MEDIACONTROL_MEDIACONTROLSERVICE_H_
+
+#include "mozilla/AlreadyAddRefed.h"
+
+#include "AudioFocusManager.h"
+#include "MediaController.h"
+#include "MediaControlKeyManager.h"
+#include "mozilla/dom/MediaControllerBinding.h"
+#include "nsIObserver.h"
+#include "nsTArray.h"
+
+namespace mozilla::dom {
+
+/**
+ * MediaControlService is an interface to access controllers by providing
+ * controller Id. Everytime when controller becomes active, which means there is
+ * one or more media started in the corresponding browsing context, so now the
+ * controller is actually controlling something in the content process, so it
+ * would be added into the list of the MediaControlService. The controller would
+ * be removed from the list of the MediaControlService when it becomes inactive,
+ * which means no media is playing in the corresponding browsing context. Note
+ * that, a controller can't be added to or remove from the list twice. It should
+ * should have a responsibility to add and remove itself in the proper time.
+ */
+class MediaControlService final : public nsIObserver {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+ static RefPtr<MediaControlService> GetService();
+
+ // Currently these following static methods are only being used in testing.
+ static void GenerateMediaControlKey(const GlobalObject& global,
+ MediaControlKey aKey);
+ static void GetCurrentActiveMediaMetadata(const GlobalObject& aGlobal,
+ MediaMetadataInit& aMetadata);
+ static MediaSessionPlaybackState GetCurrentMediaSessionPlaybackState(
+ GlobalObject& aGlobal);
+
+ AudioFocusManager& GetAudioFocusManager() { return mAudioFocusManager; }
+ MediaControlKeySource* GetMediaControlKeySource() {
+ return mMediaControlKeyManager;
+ }
+
+ // Use these functions to register/unresgister controller to/from the active
+ // controller list in the service. Return true if the controller is registered
+ // or unregistered sucessfully.
+ bool RegisterActiveMediaController(MediaController* aController);
+ bool UnregisterActiveMediaController(MediaController* aController);
+ uint64_t GetActiveControllersNum() const;
+
+ // This method would be called when the controller changes its playback state.
+ void NotifyControllerPlaybackStateChanged(MediaController* aController);
+
+ // This method is used to help a media controller become a main controller, if
+ // it fits the requirement.
+ void RequestUpdateMainController(MediaController* aController);
+
+ // The main controller is the controller which can receive the media control
+ // key events and would show its metadata to virtual controller interface.
+ MediaController* GetMainController() const;
+
+ /**
+ * These following functions are used for testing only. We use them to
+ * generate fake media control key events, get the media metadata and playback
+ * state from the main controller.
+ */
+ void GenerateTestMediaControlKey(MediaControlKey aKey);
+ MediaMetadataBase GetMainControllerMediaMetadata() const;
+ MediaSessionPlaybackState GetMainControllerPlaybackState() const;
+
+ // Media title that should be used as a fallback. This commonly used
+ // when playing media in private browsing mode and we are trying to avoid
+ // exposing potentially sensitive titles.
+ nsString GetFallbackTitle() const;
+
+ // These functions are used to update the variable which would be used for
+ // telemetry probe.
+ void NotifyMediaControlHasEverBeenUsed();
+ void NotifyMediaControlHasEverBeenEnabled();
+
+ private:
+ MediaControlService();
+ ~MediaControlService();
+
+ /**
+ * When there are multiple media controllers existing, we would only choose
+ * one media controller as the main controller which can be controlled by
+ * media control keys event. The latest controller which is added into the
+ * service would become the main controller.
+ *
+ * However, as the main controller would be changed from time to time, so we
+ * create this wrapper to hold a real main controller if it exists. This class
+ * would also observe the playback state of controller in order to update the
+ * playback state of the event source.
+ *
+ * In addition, after finishing bug1592037, we would get the media metadata
+ * from the main controller, and update them to the event source in order to
+ * show those information on the virtual media controller interface on each
+ * platform.
+ */
+ class ControllerManager final {
+ public:
+ explicit ControllerManager(MediaControlService* aService);
+ ~ControllerManager() = default;
+
+ using MediaKeysArray = nsTArray<MediaControlKey>;
+ using LinkedListControllerPtr = LinkedListElement<RefPtr<MediaController>>*;
+ using ConstLinkedListControllerPtr =
+ const LinkedListElement<RefPtr<MediaController>>*;
+
+ bool AddController(MediaController* aController);
+ bool RemoveController(MediaController* aController);
+ void UpdateMainControllerIfNeeded(MediaController* aController);
+
+ void Shutdown();
+
+ MediaController* GetMainController() const;
+ bool Contains(MediaController* aController) const;
+ uint64_t GetControllersNum() const;
+
+ // These functions are used for monitoring main controller's status change.
+ void MainControllerPlaybackStateChanged(MediaSessionPlaybackState aState);
+ void MainControllerMetadataChanged(const MediaMetadataBase& aMetadata);
+
+ private:
+ // When applying `eInsertAsMainController`, we would always insert the
+ // element to the tail of the list. Eg. Insert C , [A, B] -> [A, B, C]
+ // When applying `eInsertAsNormalController`, we would insert the element
+ // prior to the element with a higher priority controller. Eg. Insert E and
+ // C and D have higher priority. [A, B, C, D] -> [A, B, E, C, D]
+ enum class InsertOptions {
+ eInsertAsMainController,
+ eInsertAsNormalController,
+ };
+
+ // Adjust the given controller's order by the insert option.
+ void ReorderGivenController(MediaController* aController,
+ InsertOptions aOption);
+
+ void UpdateMainControllerInternal(MediaController* aController);
+ void ConnectMainControllerEvents();
+ void DisconnectMainControllerEvents();
+
+ LinkedList<RefPtr<MediaController>> mControllers;
+ RefPtr<MediaController> mMainController;
+
+ // These member are use to listen main controller's play state changes and
+ // update the playback state to the event source.
+ RefPtr<MediaControlKeySource> mSource;
+ MediaEventListener mMetadataChangedListener;
+ MediaEventListener mSupportedKeysChangedListener;
+ MediaEventListener mFullScreenChangedListener;
+ MediaEventListener mPictureInPictureModeChangedListener;
+ MediaEventListener mPositionChangedListener;
+ };
+
+ void Init();
+ void Shutdown();
+
+ AudioFocusManager mAudioFocusManager;
+ RefPtr<MediaControlKeyManager> mMediaControlKeyManager;
+ RefPtr<MediaControlKeyListener> mMediaKeysHandler;
+ MediaEventProducer<uint64_t> mMediaControllerAmountChangedEvent;
+ UniquePtr<ControllerManager> mControllerManager;
+ nsString mFallbackTitle;
+
+ // Used for telemetry probe.
+ void UpdateTelemetryUsageProbe();
+ bool mHasEverUsedMediaControl = false;
+ bool mHasEverEnabledMediaControl = false;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/mediacontrol/MediaControlUtils.cpp b/dom/media/mediacontrol/MediaControlUtils.cpp
new file mode 100644
index 0000000000..090844e47c
--- /dev/null
+++ b/dom/media/mediacontrol/MediaControlUtils.cpp
@@ -0,0 +1,26 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaControlUtils.h"
+
+#include "mozilla/dom/BrowsingContext.h"
+
+mozilla::LazyLogModule gMediaControlLog("MediaControl");
+
+namespace mozilla::dom {
+
+BrowsingContext* GetAliveTopBrowsingContext(BrowsingContext* aBC) {
+ if (!aBC || aBC->IsDiscarded()) {
+ return nullptr;
+ }
+ aBC = aBC->Top();
+ if (!aBC || aBC->IsDiscarded()) {
+ return nullptr;
+ }
+ return aBC;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/mediacontrol/MediaControlUtils.h b/dom/media/mediacontrol/MediaControlUtils.h
new file mode 100644
index 0000000000..a327c2f3d8
--- /dev/null
+++ b/dom/media/mediacontrol/MediaControlUtils.h
@@ -0,0 +1,216 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_MEDIACONTROL_MEDIACONTROLUTILS_H_
+#define DOM_MEDIA_MEDIACONTROL_MEDIACONTROLUTILS_H_
+
+#include "imgIEncoder.h"
+#include "imgITools.h"
+#include "MediaController.h"
+#include "mozilla/dom/ChromeUtilsBinding.h"
+#include "mozilla/dom/MediaControllerBinding.h"
+#include "mozilla/Logging.h"
+#include "nsReadableUtils.h"
+#include "nsServiceManagerUtils.h"
+
+extern mozilla::LazyLogModule gMediaControlLog;
+
+namespace mozilla::dom {
+
+inline const char* ToMediaControlKeyStr(MediaControlKey aKey) {
+ switch (aKey) {
+ case MediaControlKey::Focus:
+ return "Focus";
+ case MediaControlKey::Pause:
+ return "Pause";
+ case MediaControlKey::Play:
+ return "Play";
+ case MediaControlKey::Playpause:
+ return "Play & pause";
+ case MediaControlKey::Previoustrack:
+ return "Previous track";
+ case MediaControlKey::Nexttrack:
+ return "Next track";
+ case MediaControlKey::Seekbackward:
+ return "Seek backward";
+ case MediaControlKey::Seekforward:
+ return "Seek forward";
+ case MediaControlKey::Skipad:
+ return "Skip Ad";
+ case MediaControlKey::Seekto:
+ return "Seek to";
+ case MediaControlKey::Stop:
+ return "Stop";
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid action.");
+ return "Unknown";
+ }
+}
+
+inline const char* ToMediaSessionActionStr(MediaSessionAction aAction) {
+ switch (aAction) {
+ case MediaSessionAction::Play:
+ return "play";
+ case MediaSessionAction::Pause:
+ return "pause";
+ case MediaSessionAction::Seekbackward:
+ return "seek backward";
+ case MediaSessionAction::Seekforward:
+ return "seek forward";
+ case MediaSessionAction::Previoustrack:
+ return "previous track";
+ case MediaSessionAction::Nexttrack:
+ return "next track";
+ case MediaSessionAction::Skipad:
+ return "skip ad";
+ case MediaSessionAction::Seekto:
+ return "Seek to";
+ default:
+ MOZ_ASSERT(aAction == MediaSessionAction::Stop);
+ return "stop";
+ }
+}
+
+inline MediaControlKey ConvertMediaSessionActionToControlKey(
+ MediaSessionAction aAction) {
+ switch (aAction) {
+ case MediaSessionAction::Play:
+ return MediaControlKey::Play;
+ case MediaSessionAction::Pause:
+ return MediaControlKey::Pause;
+ case MediaSessionAction::Seekbackward:
+ return MediaControlKey::Seekbackward;
+ case MediaSessionAction::Seekforward:
+ return MediaControlKey::Seekforward;
+ case MediaSessionAction::Previoustrack:
+ return MediaControlKey::Previoustrack;
+ case MediaSessionAction::Nexttrack:
+ return MediaControlKey::Nexttrack;
+ case MediaSessionAction::Skipad:
+ return MediaControlKey::Skipad;
+ case MediaSessionAction::Seekto:
+ return MediaControlKey::Seekto;
+ default:
+ MOZ_ASSERT(aAction == MediaSessionAction::Stop);
+ return MediaControlKey::Stop;
+ }
+}
+
+inline MediaSessionAction ConvertToMediaSessionAction(uint8_t aActionValue) {
+ MOZ_DIAGNOSTIC_ASSERT(aActionValue < uint8_t(MediaSessionAction::EndGuard_));
+ return static_cast<MediaSessionAction>(aActionValue);
+}
+
+inline const char* ToMediaPlaybackStateStr(MediaPlaybackState aState) {
+ switch (aState) {
+ case MediaPlaybackState::eStarted:
+ return "started";
+ case MediaPlaybackState::ePlayed:
+ return "played";
+ case MediaPlaybackState::ePaused:
+ return "paused";
+ case MediaPlaybackState::eStopped:
+ return "stopped";
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid media state.");
+ return "Unknown";
+ }
+}
+
+inline const char* ToMediaAudibleStateStr(MediaAudibleState aState) {
+ switch (aState) {
+ case MediaAudibleState::eInaudible:
+ return "inaudible";
+ case MediaAudibleState::eAudible:
+ return "audible";
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid audible state.");
+ return "Unknown";
+ }
+}
+
+inline const char* ToMediaSessionPlaybackStateStr(
+ const MediaSessionPlaybackState& aState) {
+ switch (aState) {
+ case MediaSessionPlaybackState::None:
+ return "none";
+ case MediaSessionPlaybackState::Paused:
+ return "paused";
+ case MediaSessionPlaybackState::Playing:
+ return "playing";
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid MediaSessionPlaybackState.");
+ return "Unknown";
+ }
+}
+
+BrowsingContext* GetAliveTopBrowsingContext(BrowsingContext* aBC);
+
+inline bool IsImageIn(const nsTArray<MediaImage>& aArtwork,
+ const nsAString& aImageUrl) {
+ for (const MediaImage& image : aArtwork) {
+ if (image.mSrc == aImageUrl) {
+ return true;
+ }
+ }
+ return false;
+}
+
+// The image buffer would be allocated in aStream whose size is aSize and the
+// buffer head is aBuffer
+inline nsresult GetEncodedImageBuffer(imgIContainer* aImage,
+ const nsACString& aMimeType,
+ nsIInputStream** aStream, uint32_t* aSize,
+ char** aBuffer) {
+ MOZ_ASSERT(aImage);
+
+ nsCOMPtr<imgITools> imgTools = do_GetService("@mozilla.org/image/tools;1");
+ if (!imgTools) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIInputStream> inputStream;
+ nsresult rv = imgTools->EncodeImage(aImage, aMimeType, u""_ns,
+ getter_AddRefs(inputStream));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (!inputStream) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<imgIEncoder> encoder = do_QueryInterface(inputStream);
+ if (!encoder) {
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = encoder->GetImageBufferUsed(aSize);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ rv = encoder->GetImageBuffer(aBuffer);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ encoder.forget(aStream);
+ return NS_OK;
+}
+
+inline bool IsValidImageUrl(const nsAString& aUrl) {
+ return StringBeginsWith(aUrl, u"http://"_ns) ||
+ StringBeginsWith(aUrl, u"https://"_ns);
+}
+
+inline uint32_t GetMediaKeyMask(mozilla::dom::MediaControlKey aKey) {
+ return 1 << static_cast<uint8_t>(aKey);
+}
+
+} // namespace mozilla::dom
+
+#endif // DOM_MEDIA_MEDIACONTROL_MEDIACONTROLUTILS_H_
diff --git a/dom/media/mediacontrol/MediaController.cpp b/dom/media/mediacontrol/MediaController.cpp
new file mode 100644
index 0000000000..b06635091d
--- /dev/null
+++ b/dom/media/mediacontrol/MediaController.cpp
@@ -0,0 +1,560 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaController.h"
+
+#include "MediaControlService.h"
+#include "MediaControlUtils.h"
+#include "MediaControlKeySource.h"
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+#include "mozilla/dom/MediaSession.h"
+#include "mozilla/dom/PositionStateEvent.h"
+
+// avoid redefined macro in unified build
+#undef LOG
+#define LOG(msg, ...) \
+ MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
+ ("MediaController=%p, Id=%" PRId64 ", " msg, this, this->Id(), \
+ ##__VA_ARGS__))
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(MediaController, DOMEventTargetHelper)
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(MediaController,
+ DOMEventTargetHelper,
+ nsITimerCallback, nsINamed)
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(MediaController,
+ DOMEventTargetHelper)
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+nsISupports* MediaController::GetParentObject() const {
+ RefPtr<BrowsingContext> bc = BrowsingContext::Get(Id());
+ return bc;
+}
+
+JSObject* MediaController::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return MediaController_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void MediaController::GetSupportedKeys(
+ nsTArray<MediaControlKey>& aRetVal) const {
+ aRetVal.Clear();
+ for (const auto& key : mSupportedKeys) {
+ aRetVal.AppendElement(key);
+ }
+}
+
+void MediaController::GetMetadata(MediaMetadataInit& aMetadata,
+ ErrorResult& aRv) {
+ if (!IsActive() || mShutdown) {
+ aRv.Throw(NS_ERROR_NOT_AVAILABLE);
+ return;
+ }
+
+ const MediaMetadataBase metadata = GetCurrentMediaMetadata();
+ aMetadata.mTitle = metadata.mTitle;
+ aMetadata.mArtist = metadata.mArtist;
+ aMetadata.mAlbum = metadata.mAlbum;
+ for (const auto& artwork : metadata.mArtwork) {
+ if (MediaImage* image = aMetadata.mArtwork.AppendElement(fallible)) {
+ image->mSrc = artwork.mSrc;
+ image->mSizes = artwork.mSizes;
+ image->mType = artwork.mType;
+ } else {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+ }
+}
+
+static const MediaControlKey sDefaultSupportedKeys[] = {
+ MediaControlKey::Focus, MediaControlKey::Play, MediaControlKey::Pause,
+ MediaControlKey::Playpause, MediaControlKey::Stop,
+};
+
+static void GetDefaultSupportedKeys(nsTArray<MediaControlKey>& aKeys) {
+ for (const auto& key : sDefaultSupportedKeys) {
+ aKeys.AppendElement(key);
+ }
+}
+
+MediaController::MediaController(uint64_t aBrowsingContextId)
+ : MediaStatusManager(aBrowsingContextId) {
+ MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess(),
+ "MediaController only runs on Chrome process!");
+ LOG("Create controller %" PRId64, Id());
+ GetDefaultSupportedKeys(mSupportedKeys);
+ mSupportedActionsChangedListener = SupportedActionsChangedEvent().Connect(
+ AbstractThread::MainThread(), this,
+ &MediaController::HandleSupportedMediaSessionActionsChanged);
+ mPlaybackChangedListener = PlaybackChangedEvent().Connect(
+ AbstractThread::MainThread(), this,
+ &MediaController::HandleActualPlaybackStateChanged);
+ mPositionStateChangedListener = PositionChangedEvent().Connect(
+ AbstractThread::MainThread(), this,
+ &MediaController::HandlePositionStateChanged);
+ mMetadataChangedListener =
+ MetadataChangedEvent().Connect(AbstractThread::MainThread(), this,
+ &MediaController::HandleMetadataChanged);
+}
+
+MediaController::~MediaController() {
+ LOG("Destroy controller %" PRId64, Id());
+ if (!mShutdown) {
+ Shutdown();
+ }
+};
+
+void MediaController::Focus() {
+ LOG("Focus");
+ UpdateMediaControlActionToContentMediaIfNeeded(
+ MediaControlAction(MediaControlKey::Focus));
+}
+
+void MediaController::Play() {
+ LOG("Play");
+ UpdateMediaControlActionToContentMediaIfNeeded(
+ MediaControlAction(MediaControlKey::Play));
+}
+
+void MediaController::Pause() {
+ LOG("Pause");
+ UpdateMediaControlActionToContentMediaIfNeeded(
+ MediaControlAction(MediaControlKey::Pause));
+}
+
+void MediaController::PrevTrack() {
+ LOG("Prev Track");
+ UpdateMediaControlActionToContentMediaIfNeeded(
+ MediaControlAction(MediaControlKey::Previoustrack));
+}
+
+void MediaController::NextTrack() {
+ LOG("Next Track");
+ UpdateMediaControlActionToContentMediaIfNeeded(
+ MediaControlAction(MediaControlKey::Nexttrack));
+}
+
+void MediaController::SeekBackward() {
+ LOG("Seek Backward");
+ UpdateMediaControlActionToContentMediaIfNeeded(
+ MediaControlAction(MediaControlKey::Seekbackward));
+}
+
+void MediaController::SeekForward() {
+ LOG("Seek Forward");
+ UpdateMediaControlActionToContentMediaIfNeeded(
+ MediaControlAction(MediaControlKey::Seekforward));
+}
+
+void MediaController::SkipAd() {
+ LOG("Skip Ad");
+ UpdateMediaControlActionToContentMediaIfNeeded(
+ MediaControlAction(MediaControlKey::Skipad));
+}
+
+void MediaController::SeekTo(double aSeekTime, bool aFastSeek) {
+ LOG("Seek To");
+ UpdateMediaControlActionToContentMediaIfNeeded(MediaControlAction(
+ MediaControlKey::Seekto, SeekDetails(aSeekTime, aFastSeek)));
+}
+
+void MediaController::Stop() {
+ LOG("Stop");
+ UpdateMediaControlActionToContentMediaIfNeeded(
+ MediaControlAction(MediaControlKey::Stop));
+ MediaStatusManager::ClearActiveMediaSessionContextIdIfNeeded();
+}
+
+uint64_t MediaController::Id() const { return mTopLevelBrowsingContextId; }
+
+bool MediaController::IsAudible() const { return IsMediaAudible(); }
+
+bool MediaController::IsPlaying() const { return IsMediaPlaying(); }
+
+bool MediaController::IsActive() const { return mIsActive; };
+
+bool MediaController::ShouldPropagateActionToAllContexts(
+ const MediaControlAction& aAction) const {
+ // These three actions have default action handler for each frame, so we
+ // need to propagate to all contexts. We would handle default handlers in
+ // `ContentMediaController::HandleMediaKey`.
+ return aAction.mKey == MediaControlKey::Play ||
+ aAction.mKey == MediaControlKey::Pause ||
+ aAction.mKey == MediaControlKey::Stop;
+}
+
+void MediaController::UpdateMediaControlActionToContentMediaIfNeeded(
+ const MediaControlAction& aAction) {
+ // If the controller isn't active or it has been shutdown, we don't need to
+ // update media action to the content process.
+ if (!mIsActive || mShutdown) {
+ return;
+ }
+
+ // For some actions which have default action handler, we want to propagate
+ // them on all contexts in order to trigger the default handler on each
+ // context separately. Otherwise, other action should only be propagated to
+ // the context where active media session exists.
+ const bool propateToAll = ShouldPropagateActionToAllContexts(aAction);
+ const uint64_t targetContextId = propateToAll || !mActiveMediaSessionContextId
+ ? Id()
+ : *mActiveMediaSessionContextId;
+ RefPtr<BrowsingContext> context = BrowsingContext::Get(targetContextId);
+ if (!context || context->IsDiscarded()) {
+ return;
+ }
+
+ if (propateToAll) {
+ context->PreOrderWalk([&](BrowsingContext* bc) {
+ bc->Canonical()->UpdateMediaControlAction(aAction);
+ });
+ } else {
+ context->Canonical()->UpdateMediaControlAction(aAction);
+ }
+ RefPtr<MediaControlService> service = MediaControlService::GetService();
+ MOZ_ASSERT(service);
+ service->NotifyMediaControlHasEverBeenUsed();
+}
+
+void MediaController::Shutdown() {
+ MOZ_ASSERT(!mShutdown, "Do not call shutdown twice!");
+ // The media controller would be removed from the service when we receive a
+ // notification from the content process about all controlled media has been
+ // stoppped. However, if controlled media is stopped after detaching
+ // browsing context, then sending the notification from the content process
+ // would fail so that we are not able to notify the chrome process to remove
+ // the corresponding controller. Therefore, we should manually remove the
+ // controller from the service.
+ Deactivate();
+ mShutdown = true;
+ mSupportedActionsChangedListener.DisconnectIfExists();
+ mPlaybackChangedListener.DisconnectIfExists();
+ mPositionStateChangedListener.DisconnectIfExists();
+ mMetadataChangedListener.DisconnectIfExists();
+}
+
+void MediaController::NotifyMediaPlaybackChanged(uint64_t aBrowsingContextId,
+ MediaPlaybackState aState) {
+ if (mShutdown) {
+ return;
+ }
+ MediaStatusManager::NotifyMediaPlaybackChanged(aBrowsingContextId, aState);
+ UpdateDeactivationTimerIfNeeded();
+ UpdateActivatedStateIfNeeded();
+}
+
+void MediaController::UpdateDeactivationTimerIfNeeded() {
+ if (!StaticPrefs::media_mediacontrol_stopcontrol_timer()) {
+ return;
+ }
+
+ bool shouldBeAlwaysActive = IsPlaying() || IsBeingUsedInPIPModeOrFullscreen();
+ if (shouldBeAlwaysActive && mDeactivationTimer) {
+ LOG("Cancel deactivation timer");
+ mDeactivationTimer->Cancel();
+ mDeactivationTimer = nullptr;
+ } else if (!shouldBeAlwaysActive && !mDeactivationTimer) {
+ nsresult rv = NS_NewTimerWithCallback(
+ getter_AddRefs(mDeactivationTimer), this,
+ StaticPrefs::media_mediacontrol_stopcontrol_timer_ms(),
+ nsITimer::TYPE_ONE_SHOT, AbstractThread::MainThread());
+ if (NS_SUCCEEDED(rv)) {
+ LOG("Create a deactivation timer");
+ } else {
+ LOG("Failed to create a deactivation timer");
+ }
+ }
+}
+
+bool MediaController::IsBeingUsedInPIPModeOrFullscreen() const {
+ return mIsInPictureInPictureMode || mIsInFullScreenMode;
+}
+
+NS_IMETHODIMP MediaController::Notify(nsITimer* aTimer) {
+ mDeactivationTimer = nullptr;
+ if (!StaticPrefs::media_mediacontrol_stopcontrol_timer()) {
+ return NS_OK;
+ }
+
+ if (mShutdown) {
+ LOG("Cancel deactivation timer because controller has been shutdown");
+ return NS_OK;
+ }
+
+ // As the media being used in the PIP mode or fullscreen would always display
+ // on the screen, users would have high chance to interact with it again, so
+ // we don't want to stop media control.
+ if (IsBeingUsedInPIPModeOrFullscreen()) {
+ LOG("Cancel deactivation timer because controller is in PIP mode");
+ return NS_OK;
+ }
+
+ if (IsPlaying()) {
+ LOG("Cancel deactivation timer because controller is still playing");
+ return NS_OK;
+ }
+
+ if (!mIsActive) {
+ LOG("Cancel deactivation timer because controller has been deactivated");
+ return NS_OK;
+ }
+ Deactivate();
+ return NS_OK;
+}
+
+NS_IMETHODIMP MediaController::GetName(nsACString& aName) {
+ aName.AssignLiteral("MediaController");
+ return NS_OK;
+}
+
+void MediaController::NotifyMediaAudibleChanged(uint64_t aBrowsingContextId,
+ MediaAudibleState aState) {
+ if (mShutdown) {
+ return;
+ }
+
+ bool oldAudible = IsAudible();
+ MediaStatusManager::NotifyMediaAudibleChanged(aBrowsingContextId, aState);
+ if (IsAudible() == oldAudible) {
+ return;
+ }
+ UpdateActivatedStateIfNeeded();
+
+ // Request the audio focus amongs different controllers that could cause
+ // pausing other audible controllers if we enable the audio focus management.
+ RefPtr<MediaControlService> service = MediaControlService::GetService();
+ MOZ_ASSERT(service);
+ if (IsAudible()) {
+ service->GetAudioFocusManager().RequestAudioFocus(this);
+ } else {
+ service->GetAudioFocusManager().RevokeAudioFocus(this);
+ }
+}
+
+bool MediaController::ShouldActivateController() const {
+ MOZ_ASSERT(!mShutdown);
+ // After media is successfully loaded and match our critiera, such as its
+ // duration is longer enough, which is used to exclude the notification-ish
+ // sound, then it would be able to be controlled once the controll gets
+ // activated.
+ //
+ // Activating a controller means that we would start to intercept the media
+ // keys on the platform and show the virtual control interface (if needed).
+ // The controller would be activated when (1) controllable media starts in the
+ // browsing context that controller belongs to (2) controllable media enters
+ // fullscreen or PIP mode.
+ return IsAnyMediaBeingControlled() &&
+ (IsPlaying() || IsBeingUsedInPIPModeOrFullscreen()) && !mIsActive;
+}
+
+bool MediaController::ShouldDeactivateController() const {
+ MOZ_ASSERT(!mShutdown);
+ // If we don't have an active media session and no controlled media exists,
+ // then we don't need to keep controller active, because there is nothing to
+ // control. However, if we still have an active media session, then we should
+ // keep controller active in order to receive media keys even if we don't have
+ // any controlled media existing, because a website might start other media
+ // when media session receives media keys.
+ return !IsAnyMediaBeingControlled() && mIsActive &&
+ !mActiveMediaSessionContextId;
+}
+
+void MediaController::Activate() {
+ MOZ_ASSERT(!mShutdown);
+ RefPtr<MediaControlService> service = MediaControlService::GetService();
+ if (service && !mIsActive) {
+ LOG("Activate");
+ mIsActive = service->RegisterActiveMediaController(this);
+ MOZ_ASSERT(mIsActive, "Fail to register controller!");
+ DispatchAsyncEvent(u"activated"_ns);
+ }
+}
+
+void MediaController::Deactivate() {
+ MOZ_ASSERT(!mShutdown);
+ RefPtr<MediaControlService> service = MediaControlService::GetService();
+ if (service) {
+ service->GetAudioFocusManager().RevokeAudioFocus(this);
+ if (mIsActive) {
+ LOG("Deactivate");
+ mIsActive = !service->UnregisterActiveMediaController(this);
+ MOZ_ASSERT(!mIsActive, "Fail to unregister controller!");
+ DispatchAsyncEvent(u"deactivated"_ns);
+ }
+ }
+}
+
+void MediaController::SetIsInPictureInPictureMode(
+ uint64_t aBrowsingContextId, bool aIsInPictureInPictureMode) {
+ if (mIsInPictureInPictureMode == aIsInPictureInPictureMode) {
+ return;
+ }
+ LOG("Set IsInPictureInPictureMode to %s",
+ aIsInPictureInPictureMode ? "true" : "false");
+ mIsInPictureInPictureMode = aIsInPictureInPictureMode;
+ ForceToBecomeMainControllerIfNeeded();
+ UpdateDeactivationTimerIfNeeded();
+ mPictureInPictureModeChangedEvent.Notify(mIsInPictureInPictureMode);
+}
+
+void MediaController::NotifyMediaFullScreenState(uint64_t aBrowsingContextId,
+ bool aIsInFullScreen) {
+ if (mIsInFullScreenMode == aIsInFullScreen) {
+ return;
+ }
+ LOG("%s fullscreen", aIsInFullScreen ? "Entered" : "Left");
+ mIsInFullScreenMode = aIsInFullScreen;
+ ForceToBecomeMainControllerIfNeeded();
+ mFullScreenChangedEvent.Notify(mIsInFullScreenMode);
+}
+
+bool MediaController::IsMainController() const {
+ RefPtr<MediaControlService> service = MediaControlService::GetService();
+ return service ? service->GetMainController() == this : false;
+}
+
+bool MediaController::ShouldRequestForMainController() const {
+ // This controller is already the main controller.
+ if (IsMainController()) {
+ return false;
+ }
+ // We would only force controller to become main controller if it's in the
+ // PIP mode or fullscreen, otherwise it should follow the general rule.
+ // In addition, do nothing if the controller has been shutdowned.
+ return IsBeingUsedInPIPModeOrFullscreen() && !mShutdown;
+}
+
+void MediaController::ForceToBecomeMainControllerIfNeeded() {
+ if (!ShouldRequestForMainController()) {
+ return;
+ }
+ RefPtr<MediaControlService> service = MediaControlService::GetService();
+ MOZ_ASSERT(service, "service was shutdown before shutting down controller?");
+ // If the controller hasn't been activated and it's ready to be activated,
+ // then activating it should also make it become a main controller. If it's
+ // already activated but isn't a main controller yet, then explicitly request
+ // it.
+ if (!IsActive() && ShouldActivateController()) {
+ Activate();
+ } else if (IsActive()) {
+ service->RequestUpdateMainController(this);
+ }
+}
+
+void MediaController::HandleActualPlaybackStateChanged() {
+ // Media control service would like to know all controllers' playback state
+ // in order to decide which controller should be the main controller that is
+ // usually the last tab which plays media.
+ if (RefPtr<MediaControlService> service = MediaControlService::GetService()) {
+ service->NotifyControllerPlaybackStateChanged(this);
+ }
+ DispatchAsyncEvent(u"playbackstatechange"_ns);
+}
+
+void MediaController::UpdateActivatedStateIfNeeded() {
+ if (ShouldActivateController()) {
+ Activate();
+ } else if (ShouldDeactivateController()) {
+ Deactivate();
+ }
+}
+
+void MediaController::HandleSupportedMediaSessionActionsChanged(
+ const nsTArray<MediaSessionAction>& aSupportedAction) {
+ // Convert actions to keys, some of them have been included in the supported
+ // keys, such as "play", "pause" and "stop".
+ nsTArray<MediaControlKey> newSupportedKeys;
+ GetDefaultSupportedKeys(newSupportedKeys);
+ for (const auto& action : aSupportedAction) {
+ MediaControlKey key = ConvertMediaSessionActionToControlKey(action);
+ if (!newSupportedKeys.Contains(key)) {
+ newSupportedKeys.AppendElement(key);
+ }
+ }
+ // As the supported key event should only be notified when supported keys
+ // change, so abort following steps if they don't change.
+ if (newSupportedKeys == mSupportedKeys) {
+ return;
+ }
+ LOG("Supported keys changes");
+ mSupportedKeys = newSupportedKeys;
+ mSupportedKeysChangedEvent.Notify(mSupportedKeys);
+ RefPtr<AsyncEventDispatcher> asyncDispatcher = new AsyncEventDispatcher(
+ this, u"supportedkeyschange"_ns, CanBubble::eYes);
+ asyncDispatcher->PostDOMEvent();
+ MediaController_Binding::ClearCachedSupportedKeysValue(this);
+}
+
+void MediaController::HandlePositionStateChanged(const PositionState& aState) {
+ PositionStateEventInit init;
+ init.mDuration = aState.mDuration;
+ init.mPlaybackRate = aState.mPlaybackRate;
+ init.mPosition = aState.mLastReportedPlaybackPosition;
+ RefPtr<PositionStateEvent> event =
+ PositionStateEvent::Constructor(this, u"positionstatechange"_ns, init);
+ DispatchAsyncEvent(event);
+}
+
+void MediaController::HandleMetadataChanged(
+ const MediaMetadataBase& aMetadata) {
+ // The reason we don't append metadata with `metadatachange` event is that
+ // allocating artwork might fail if the memory is not enough, but for the
+ // event we are not able to throw an error. Therefore, we want to the listener
+ // to use `getMetadata()` to get metadata, because it would throw an error if
+ // we fail to allocate artwork.
+ DispatchAsyncEvent(u"metadatachange"_ns);
+ // If metadata change is because of resetting active media session, then we
+ // should check if controller needs to be deactivated.
+ if (ShouldDeactivateController()) {
+ Deactivate();
+ }
+}
+
+void MediaController::DispatchAsyncEvent(const nsAString& aName) {
+ RefPtr<Event> event = NS_NewDOMEvent(this, nullptr, nullptr);
+ event->InitEvent(aName, false, false);
+ event->SetTrusted(true);
+ DispatchAsyncEvent(event);
+}
+
+void MediaController::DispatchAsyncEvent(Event* aEvent) {
+ MOZ_ASSERT(aEvent);
+ nsAutoString eventType;
+ aEvent->GetType(eventType);
+ if (!mIsActive && !eventType.EqualsLiteral("deactivated")) {
+ LOG("Only 'deactivated' can be dispatched on a deactivated controller, not "
+ "'%s'",
+ NS_ConvertUTF16toUTF8(eventType).get());
+ return;
+ }
+ LOG("Dispatch event %s", NS_ConvertUTF16toUTF8(eventType).get());
+ RefPtr<AsyncEventDispatcher> asyncDispatcher =
+ new AsyncEventDispatcher(this, aEvent);
+ asyncDispatcher->PostDOMEvent();
+}
+
+CopyableTArray<MediaControlKey> MediaController::GetSupportedMediaKeys() const {
+ return mSupportedKeys;
+}
+
+void MediaController::Select() const {
+ if (RefPtr<BrowsingContext> bc = BrowsingContext::Get(Id())) {
+ bc->Canonical()->AddPageAwakeRequest();
+ }
+}
+
+void MediaController::Unselect() const {
+ if (RefPtr<BrowsingContext> bc = BrowsingContext::Get(Id())) {
+ bc->Canonical()->RemovePageAwakeRequest();
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/mediacontrol/MediaController.h b/dom/media/mediacontrol/MediaController.h
new file mode 100644
index 0000000000..0f0e624f6a
--- /dev/null
+++ b/dom/media/mediacontrol/MediaController.h
@@ -0,0 +1,214 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_MEDIACONTROL_MEDIACONTROLLER_H_
+#define DOM_MEDIA_MEDIACONTROL_MEDIACONTROLLER_H_
+
+#include "MediaEventSource.h"
+#include "MediaPlaybackStatus.h"
+#include "MediaStatusManager.h"
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/dom/MediaControllerBinding.h"
+#include "mozilla/dom/MediaSession.h"
+#include "mozilla/LinkedList.h"
+#include "nsISupportsImpl.h"
+#include "nsITimer.h"
+
+namespace mozilla::dom {
+
+class BrowsingContext;
+
+/**
+ * IMediaController is an interface which includes control related methods and
+ * methods used to know its playback state.
+ */
+class IMediaController {
+ public:
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+ // Focus the window currently playing media.
+ virtual void Focus() = 0;
+ virtual void Play() = 0;
+ virtual void Pause() = 0;
+ virtual void Stop() = 0;
+ virtual void PrevTrack() = 0;
+ virtual void NextTrack() = 0;
+ virtual void SeekBackward() = 0;
+ virtual void SeekForward() = 0;
+ virtual void SkipAd() = 0;
+ virtual void SeekTo(double aSeekTime, bool aFastSeek) = 0;
+
+ // Return the ID of the top level browsing context within a tab.
+ virtual uint64_t Id() const = 0;
+ virtual bool IsAudible() const = 0;
+ virtual bool IsPlaying() const = 0;
+ virtual bool IsActive() const = 0;
+};
+
+/**
+ * MediaController is a class, which is used to control all media within a tab.
+ * It can only be used in Chrome process and the controlled media are usually
+ * in the content process (unless we disable e10s).
+ *
+ * Each tab would have only one media controller, they are 1-1 corresponding
+ * relationship, we use tab's top-level browsing context ID to initialize the
+ * controller and use that as its ID.
+ *
+ * The controller would be activated when its controlled media starts and
+ * becomes audible. After the controller is activated, then we can use its
+ * controlling methods, such as `Play()`, `Pause()` to control the media within
+ * the tab.
+ *
+ * If there is at least one controlled media playing in the tab, then we would
+ * say the controller is `playing`. If there is at least one controlled media is
+ * playing and audible, then we would say the controller is `audible`.
+ *
+ * Note that, if we don't enable audio competition, then we might have multiple
+ * tabs playing media at the same time, we can use the ID to query the specific
+ * controller from `MediaControlService`.
+ */
+class MediaController final : public DOMEventTargetHelper,
+ public IMediaController,
+ public LinkedListElement<RefPtr<MediaController>>,
+ public MediaStatusManager,
+ public nsITimerCallback,
+ public nsINamed {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_NSITIMERCALLBACK
+ NS_DECL_NSINAMED
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(MediaController,
+ DOMEventTargetHelper)
+ explicit MediaController(uint64_t aBrowsingContextId);
+
+ // WebIDL methods
+ nsISupports* GetParentObject() const;
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+ void GetSupportedKeys(nsTArray<MediaControlKey>& aRetVal) const;
+ void GetMetadata(MediaMetadataInit& aMetadata, ErrorResult& aRv);
+ IMPL_EVENT_HANDLER(activated);
+ IMPL_EVENT_HANDLER(deactivated);
+ IMPL_EVENT_HANDLER(metadatachange);
+ IMPL_EVENT_HANDLER(supportedkeyschange);
+ IMPL_EVENT_HANDLER(playbackstatechange);
+ IMPL_EVENT_HANDLER(positionstatechange);
+
+ // IMediaController's methods
+ void Focus() override;
+ void Play() override;
+ void Pause() override;
+ void Stop() override;
+ void PrevTrack() override;
+ void NextTrack() override;
+ void SeekBackward() override;
+ void SeekForward() override;
+ void SkipAd() override;
+ void SeekTo(double aSeekTime, bool aFastSeek) override;
+
+ uint64_t Id() const override;
+ bool IsAudible() const override;
+ bool IsPlaying() const override;
+ bool IsActive() const override;
+
+ // IMediaInfoUpdater's methods
+ void NotifyMediaPlaybackChanged(uint64_t aBrowsingContextId,
+ MediaPlaybackState aState) override;
+ void NotifyMediaAudibleChanged(uint64_t aBrowsingContextId,
+ MediaAudibleState aState) override;
+ void SetIsInPictureInPictureMode(uint64_t aBrowsingContextId,
+ bool aIsInPictureInPictureMode) override;
+ void NotifyMediaFullScreenState(uint64_t aBrowsingContextId,
+ bool aIsInFullScreen) override;
+
+ // Calling this method explicitly would mark this controller as deprecated,
+ // then calling any its method won't take any effect.
+ void Shutdown();
+
+ // This event would be notified media controller's supported media keys
+ // change.
+ MediaEventSource<nsTArray<MediaControlKey>>& SupportedKeysChangedEvent() {
+ return mSupportedKeysChangedEvent;
+ }
+
+ MediaEventSource<bool>& FullScreenChangedEvent() {
+ return mFullScreenChangedEvent;
+ }
+
+ MediaEventSource<bool>& PictureInPictureModeChangedEvent() {
+ return mPictureInPictureModeChangedEvent;
+ }
+
+ CopyableTArray<MediaControlKey> GetSupportedMediaKeys() const;
+
+ bool IsBeingUsedInPIPModeOrFullscreen() const;
+
+ // These methods are used to select/unselect the media controller as a main
+ // controller.
+ void Select() const;
+ void Unselect() const;
+
+ private:
+ ~MediaController();
+ void HandleActualPlaybackStateChanged();
+ void UpdateMediaControlActionToContentMediaIfNeeded(
+ const MediaControlAction& aAction);
+ void HandleSupportedMediaSessionActionsChanged(
+ const nsTArray<MediaSessionAction>& aSupportedAction);
+
+ void HandlePositionStateChanged(const PositionState& aState);
+ void HandleMetadataChanged(const MediaMetadataBase& aMetadata);
+
+ // This would register controller to the media control service that takes a
+ // responsibility to manage all active controllers.
+ void Activate();
+
+ // This would unregister controller from the media control service.
+ void Deactivate();
+
+ void UpdateActivatedStateIfNeeded();
+ bool ShouldActivateController() const;
+ bool ShouldDeactivateController() const;
+
+ void UpdateDeactivationTimerIfNeeded();
+
+ void DispatchAsyncEvent(const nsAString& aName);
+ void DispatchAsyncEvent(Event* aEvent);
+
+ bool IsMainController() const;
+ void ForceToBecomeMainControllerIfNeeded();
+ bool ShouldRequestForMainController() const;
+
+ bool ShouldPropagateActionToAllContexts(
+ const MediaControlAction& aAction) const;
+
+ bool mIsActive = false;
+ bool mShutdown = false;
+ bool mIsInPictureInPictureMode = false;
+ bool mIsInFullScreenMode = false;
+
+ // We would monitor the change of media session actions and convert them to
+ // the media keys, then determine the supported media keys.
+ MediaEventListener mSupportedActionsChangedListener;
+ MediaEventProducer<nsTArray<MediaControlKey>> mSupportedKeysChangedEvent;
+
+ MediaEventListener mPlaybackChangedListener;
+ MediaEventListener mPositionStateChangedListener;
+ MediaEventListener mMetadataChangedListener;
+
+ MediaEventProducer<bool> mFullScreenChangedEvent;
+ MediaEventProducer<bool> mPictureInPictureModeChangedEvent;
+ // Use copyable array so that we can use the result as a parameter for the
+ // media event.
+ CopyableTArray<MediaControlKey> mSupportedKeys;
+ // Timer to deactivate the controller if the time of being paused exceeds the
+ // threshold of time.
+ nsCOMPtr<nsITimer> mDeactivationTimer;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/mediacontrol/MediaPlaybackStatus.cpp b/dom/media/mediacontrol/MediaPlaybackStatus.cpp
new file mode 100644
index 0000000000..80dedf8599
--- /dev/null
+++ b/dom/media/mediacontrol/MediaPlaybackStatus.cpp
@@ -0,0 +1,142 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaPlaybackStatus.h"
+
+#include "MediaControlUtils.h"
+
+namespace mozilla::dom {
+
+#undef LOG
+#define LOG(msg, ...) \
+ MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
+ ("MediaPlaybackStatus=%p, " msg, this, ##__VA_ARGS__))
+
+void MediaPlaybackStatus::UpdateMediaPlaybackState(uint64_t aContextId,
+ MediaPlaybackState aState) {
+ LOG("Update playback state '%s' for context %" PRIu64,
+ ToMediaPlaybackStateStr(aState), aContextId);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ ContextMediaInfo& info = GetNotNullContextInfo(aContextId);
+ if (aState == MediaPlaybackState::eStarted) {
+ info.IncreaseControlledMediaNum();
+ } else if (aState == MediaPlaybackState::eStopped) {
+ info.DecreaseControlledMediaNum();
+ } else if (aState == MediaPlaybackState::ePlayed) {
+ info.IncreasePlayingMediaNum();
+ } else {
+ MOZ_ASSERT(aState == MediaPlaybackState::ePaused);
+ info.DecreasePlayingMediaNum();
+ }
+
+ // The context still has controlled media, we should keep its alive.
+ if (info.IsAnyMediaBeingControlled()) {
+ return;
+ }
+ MOZ_ASSERT(!info.IsPlaying());
+ MOZ_ASSERT(!info.IsAudible());
+ // DO NOT access `info` after this line.
+ DestroyContextInfo(aContextId);
+}
+
+void MediaPlaybackStatus::DestroyContextInfo(uint64_t aContextId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ LOG("Remove context %" PRIu64, aContextId);
+ mContextInfoMap.Remove(aContextId);
+ // If the removed context is owning the audio focus, we would find another
+ // context to take the audio focus if it's possible.
+ if (IsContextOwningAudioFocus(aContextId)) {
+ ChooseNewContextToOwnAudioFocus();
+ }
+}
+
+void MediaPlaybackStatus::UpdateMediaAudibleState(uint64_t aContextId,
+ MediaAudibleState aState) {
+ LOG("Update audible state '%s' for context %" PRIu64,
+ ToMediaAudibleStateStr(aState), aContextId);
+ MOZ_ASSERT(NS_IsMainThread());
+ ContextMediaInfo& info = GetNotNullContextInfo(aContextId);
+ if (aState == MediaAudibleState::eAudible) {
+ info.IncreaseAudibleMediaNum();
+ } else {
+ MOZ_ASSERT(aState == MediaAudibleState::eInaudible);
+ info.DecreaseAudibleMediaNum();
+ }
+ if (ShouldRequestAudioFocusForInfo(info)) {
+ SetOwningAudioFocusContextId(Some(aContextId));
+ } else if (ShouldAbandonAudioFocusForInfo(info)) {
+ ChooseNewContextToOwnAudioFocus();
+ }
+}
+
+bool MediaPlaybackStatus::IsPlaying() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return std::any_of(mContextInfoMap.Values().cbegin(),
+ mContextInfoMap.Values().cend(),
+ [](const auto& info) { return info->IsPlaying(); });
+}
+
+bool MediaPlaybackStatus::IsAudible() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return std::any_of(mContextInfoMap.Values().cbegin(),
+ mContextInfoMap.Values().cend(),
+ [](const auto& info) { return info->IsAudible(); });
+}
+
+bool MediaPlaybackStatus::IsAnyMediaBeingControlled() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return std::any_of(
+ mContextInfoMap.Values().cbegin(), mContextInfoMap.Values().cend(),
+ [](const auto& info) { return info->IsAnyMediaBeingControlled(); });
+}
+
+MediaPlaybackStatus::ContextMediaInfo&
+MediaPlaybackStatus::GetNotNullContextInfo(uint64_t aContextId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ return *mContextInfoMap.GetOrInsertNew(aContextId, aContextId);
+}
+
+Maybe<uint64_t> MediaPlaybackStatus::GetAudioFocusOwnerContextId() const {
+ return mOwningAudioFocusContextId;
+}
+
+void MediaPlaybackStatus::ChooseNewContextToOwnAudioFocus() {
+ for (const auto& info : mContextInfoMap.Values()) {
+ if (info->IsAudible()) {
+ SetOwningAudioFocusContextId(Some(info->Id()));
+ return;
+ }
+ }
+ // No context is audible, so no one should the own audio focus.
+ SetOwningAudioFocusContextId(Nothing());
+}
+
+void MediaPlaybackStatus::SetOwningAudioFocusContextId(
+ Maybe<uint64_t>&& aContextId) {
+ if (mOwningAudioFocusContextId == aContextId) {
+ return;
+ }
+ mOwningAudioFocusContextId = aContextId;
+}
+
+bool MediaPlaybackStatus::ShouldRequestAudioFocusForInfo(
+ const ContextMediaInfo& aInfo) const {
+ return aInfo.IsAudible() && !IsContextOwningAudioFocus(aInfo.Id());
+}
+
+bool MediaPlaybackStatus::ShouldAbandonAudioFocusForInfo(
+ const ContextMediaInfo& aInfo) const {
+ // The owner becomes inaudible and there is other context still playing, so we
+ // should switch the audio focus to the audible context.
+ return !aInfo.IsAudible() && IsContextOwningAudioFocus(aInfo.Id()) &&
+ IsAudible();
+}
+
+bool MediaPlaybackStatus::IsContextOwningAudioFocus(uint64_t aContextId) const {
+ return mOwningAudioFocusContextId ? *mOwningAudioFocusContextId == aContextId
+ : false;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/mediacontrol/MediaPlaybackStatus.h b/dom/media/mediacontrol/MediaPlaybackStatus.h
new file mode 100644
index 0000000000..bf0a5cc611
--- /dev/null
+++ b/dom/media/mediacontrol/MediaPlaybackStatus.h
@@ -0,0 +1,139 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_MEDIACONTROL_MEDIAPLAYBACKSTATUS_H_
+#define DOM_MEDIA_MEDIACONTROL_MEDIAPLAYBACKSTATUS_H_
+
+#include "mozilla/Maybe.h"
+#include "mozilla/RefPtr.h"
+#include "nsISupportsImpl.h"
+#include "nsTArray.h"
+#include "nsTHashMap.h"
+
+namespace mozilla::dom {
+
+/**
+ * This enum is used to update controlled media state to the media controller in
+ * the chrome process.
+ * `eStarted`: media has successfully registered to the content media controller
+ * `ePlayed` : media has started playing
+ * `ePaused` : media has paused playing, but still can be resumed by content
+ * media controller
+ * `eStopped`: media has unregistered from the content media controller, we can
+ * not control it anymore
+ */
+enum class MediaPlaybackState : uint32_t {
+ eStarted,
+ ePlayed,
+ ePaused,
+ eStopped,
+};
+
+/**
+ * This enum is used to update controlled media audible audible state to the
+ * media controller in the chrome process.
+ */
+enum class MediaAudibleState : bool {
+ eInaudible = false,
+ eAudible = true,
+};
+
+/**
+ * MediaPlaybackStatus is an internal module for the media controller, it
+ * represents a tab's media related status, such like "does the tab contain any
+ * controlled media? is the tab playing? is the tab audible?".
+ *
+ * The reason we need this class is that we would like to encapsulate the
+ * details of determining the tab's media status. A tab can contains multiple
+ * browsing contexts, and each browsing context can have different media status.
+ * The final media status would be decided by checking all those context status.
+ *
+ * Use `UpdateMediaXXXState()` to update controlled media status, and use
+ * `IsXXX()` methods to acquire the playback status of the tab.
+ *
+ * As we know each context's audible state, we can decide which context should
+ * owns the audio focus when multiple contexts are all playing audible media at
+ * the same time. In that cases, the latest context that plays media would own
+ * the audio focus. When the context owning the audio focus is destroyed, we
+ * would see if there is another other context still playing audible media, and
+ * switch the audio focus to another context.
+ */
+class MediaPlaybackStatus final {
+ public:
+ void UpdateMediaPlaybackState(uint64_t aContextId, MediaPlaybackState aState);
+ void UpdateMediaAudibleState(uint64_t aContextId, MediaAudibleState aState);
+
+ bool IsPlaying() const;
+ bool IsAudible() const;
+ bool IsAnyMediaBeingControlled() const;
+
+ Maybe<uint64_t> GetAudioFocusOwnerContextId() const;
+
+ private:
+ /**
+ * This internal class stores detailed media status of controlled media for
+ * a browsing context.
+ */
+ class ContextMediaInfo final {
+ public:
+ explicit ContextMediaInfo(uint64_t aContextId) : mContextId(aContextId) {}
+ ~ContextMediaInfo() = default;
+
+ void IncreaseControlledMediaNum() {
+ MOZ_DIAGNOSTIC_ASSERT(mControlledMediaNum < UINT_MAX);
+ mControlledMediaNum++;
+ }
+ void DecreaseControlledMediaNum() {
+ MOZ_DIAGNOSTIC_ASSERT(mControlledMediaNum > 0);
+ mControlledMediaNum--;
+ }
+ void IncreasePlayingMediaNum() {
+ MOZ_DIAGNOSTIC_ASSERT(mPlayingMediaNum < mControlledMediaNum);
+ mPlayingMediaNum++;
+ }
+ void DecreasePlayingMediaNum() {
+ MOZ_DIAGNOSTIC_ASSERT(mPlayingMediaNum > 0);
+ mPlayingMediaNum--;
+ }
+ void IncreaseAudibleMediaNum() {
+ MOZ_DIAGNOSTIC_ASSERT(mAudibleMediaNum < mPlayingMediaNum);
+ mAudibleMediaNum++;
+ }
+ void DecreaseAudibleMediaNum() {
+ MOZ_DIAGNOSTIC_ASSERT(mAudibleMediaNum > 0);
+ mAudibleMediaNum--;
+ }
+ bool IsPlaying() const { return mPlayingMediaNum > 0; }
+ bool IsAudible() const { return mAudibleMediaNum > 0; }
+ bool IsAnyMediaBeingControlled() const { return mControlledMediaNum > 0; }
+ uint64_t Id() const { return mContextId; }
+
+ private:
+ /**
+ * The possible value for those three numbers should follow this rule,
+ * mControlledMediaNum >= mPlayingMediaNum >= mAudibleMediaNum
+ */
+ uint32_t mControlledMediaNum = 0;
+ uint32_t mAudibleMediaNum = 0;
+ uint32_t mPlayingMediaNum = 0;
+ uint64_t mContextId = 0;
+ };
+
+ ContextMediaInfo& GetNotNullContextInfo(uint64_t aContextId);
+ void DestroyContextInfo(uint64_t aContextId);
+
+ void ChooseNewContextToOwnAudioFocus();
+ void SetOwningAudioFocusContextId(Maybe<uint64_t>&& aContextId);
+ bool IsContextOwningAudioFocus(uint64_t aContextId) const;
+ bool ShouldRequestAudioFocusForInfo(const ContextMediaInfo& aInfo) const;
+ bool ShouldAbandonAudioFocusForInfo(const ContextMediaInfo& aInfo) const;
+
+ // This contains all the media status of browsing contexts within a tab.
+ nsTHashMap<uint64_t, UniquePtr<ContextMediaInfo>> mContextInfoMap;
+ Maybe<uint64_t> mOwningAudioFocusContextId;
+};
+
+} // namespace mozilla::dom
+
+#endif // DOM_MEDIA_MEDIACONTROL_MEDIAPLAYBACKSTATUS_H_
diff --git a/dom/media/mediacontrol/MediaStatusManager.cpp b/dom/media/mediacontrol/MediaStatusManager.cpp
new file mode 100644
index 0000000000..4365e6b531
--- /dev/null
+++ b/dom/media/mediacontrol/MediaStatusManager.cpp
@@ -0,0 +1,482 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaStatusManager.h"
+
+#include "MediaControlService.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/MediaControlUtils.h"
+#include "mozilla/dom/WindowGlobalParent.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "nsContentUtils.h"
+#include "nsIChromeRegistry.h"
+#include "nsIObserverService.h"
+#include "nsIXULAppInfo.h"
+#include "nsNetUtil.h"
+
+#ifdef MOZ_PLACES
+# include "nsIFaviconService.h"
+#endif // MOZ_PLACES
+
+extern mozilla::LazyLogModule gMediaControlLog;
+
+// avoid redefined macro in unified build
+#undef LOG
+#define LOG(msg, ...) \
+ MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
+ ("MediaStatusManager=%p, " msg, this, ##__VA_ARGS__))
+
+namespace mozilla::dom {
+
+static bool IsMetadataEmpty(const Maybe<MediaMetadataBase>& aMetadata) {
+ // Media session's metadata is null.
+ if (!aMetadata) {
+ return true;
+ }
+
+ // All attirbutes in metadata are empty.
+ // https://w3c.github.io/mediasession/#empty-metadata
+ const MediaMetadataBase& metadata = *aMetadata;
+ return metadata.mTitle.IsEmpty() && metadata.mArtist.IsEmpty() &&
+ metadata.mAlbum.IsEmpty() && metadata.mArtwork.IsEmpty();
+}
+
+MediaStatusManager::MediaStatusManager(uint64_t aBrowsingContextId)
+ : mTopLevelBrowsingContextId(aBrowsingContextId) {
+ MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess(),
+ "MediaStatusManager only runs on Chrome process!");
+}
+
+void MediaStatusManager::NotifyMediaAudibleChanged(uint64_t aBrowsingContextId,
+ MediaAudibleState aState) {
+ Maybe<uint64_t> oldAudioFocusOwnerId =
+ mPlaybackStatusDelegate.GetAudioFocusOwnerContextId();
+ mPlaybackStatusDelegate.UpdateMediaAudibleState(aBrowsingContextId, aState);
+ Maybe<uint64_t> newAudioFocusOwnerId =
+ mPlaybackStatusDelegate.GetAudioFocusOwnerContextId();
+ if (oldAudioFocusOwnerId != newAudioFocusOwnerId) {
+ HandleAudioFocusOwnerChanged(newAudioFocusOwnerId);
+ }
+}
+
+void MediaStatusManager::NotifySessionCreated(uint64_t aBrowsingContextId) {
+ const bool created = mMediaSessionInfoMap.WithEntryHandle(
+ aBrowsingContextId, [&](auto&& entry) {
+ if (entry) return false;
+
+ LOG("Session %" PRIu64 " has been created", aBrowsingContextId);
+ entry.Insert(MediaSessionInfo::EmptyInfo());
+ return true;
+ });
+
+ if (created && IsSessionOwningAudioFocus(aBrowsingContextId)) {
+ // This can't be done from within the WithEntryHandle functor, since it
+ // accesses mMediaSessionInfoMap.
+ SetActiveMediaSessionContextId(aBrowsingContextId);
+ }
+}
+
+void MediaStatusManager::NotifySessionDestroyed(uint64_t aBrowsingContextId) {
+ if (mMediaSessionInfoMap.Remove(aBrowsingContextId)) {
+ LOG("Session %" PRIu64 " has been destroyed", aBrowsingContextId);
+
+ if (mActiveMediaSessionContextId &&
+ *mActiveMediaSessionContextId == aBrowsingContextId) {
+ ClearActiveMediaSessionContextIdIfNeeded();
+ }
+ }
+}
+
+void MediaStatusManager::UpdateMetadata(
+ uint64_t aBrowsingContextId, const Maybe<MediaMetadataBase>& aMetadata) {
+ auto info = mMediaSessionInfoMap.Lookup(aBrowsingContextId);
+ if (!info) {
+ return;
+ }
+ if (IsMetadataEmpty(aMetadata)) {
+ LOG("Reset metadata for session %" PRIu64, aBrowsingContextId);
+ info->mMetadata.reset();
+ } else {
+ LOG("Update metadata for session %" PRIu64 " title=%s artist=%s album=%s",
+ aBrowsingContextId, NS_ConvertUTF16toUTF8((*aMetadata).mTitle).get(),
+ NS_ConvertUTF16toUTF8(aMetadata->mArtist).get(),
+ NS_ConvertUTF16toUTF8(aMetadata->mAlbum).get());
+ info->mMetadata = aMetadata;
+ }
+ // Only notify the event if the changed metadata belongs to the active media
+ // session.
+ if (mActiveMediaSessionContextId &&
+ *mActiveMediaSessionContextId == aBrowsingContextId) {
+ LOG("Notify metadata change for active session %" PRIu64,
+ aBrowsingContextId);
+ mMetadataChangedEvent.Notify(GetCurrentMediaMetadata());
+ }
+ if (StaticPrefs::media_mediacontrol_testingevents_enabled()) {
+ if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
+ obs->NotifyObservers(nullptr, "media-session-controller-metadata-changed",
+ nullptr);
+ }
+ }
+}
+
+void MediaStatusManager::HandleAudioFocusOwnerChanged(
+ Maybe<uint64_t>& aBrowsingContextId) {
+ // No one is holding the audio focus.
+ if (!aBrowsingContextId) {
+ LOG("No one is owning audio focus");
+ return ClearActiveMediaSessionContextIdIfNeeded();
+ }
+
+ // This owner of audio focus doesn't have media session, so we should deactive
+ // the active session because the active session must own the audio focus.
+ if (!mMediaSessionInfoMap.Contains(*aBrowsingContextId)) {
+ LOG("The owner of audio focus doesn't have media session");
+ return ClearActiveMediaSessionContextIdIfNeeded();
+ }
+
+ // This owner has media session so it should become an active session context.
+ SetActiveMediaSessionContextId(*aBrowsingContextId);
+}
+
+void MediaStatusManager::SetActiveMediaSessionContextId(
+ uint64_t aBrowsingContextId) {
+ if (mActiveMediaSessionContextId &&
+ *mActiveMediaSessionContextId == aBrowsingContextId) {
+ LOG("Active session context %" PRIu64 " keeps unchanged",
+ *mActiveMediaSessionContextId);
+ return;
+ }
+ mActiveMediaSessionContextId = Some(aBrowsingContextId);
+ StoreMediaSessionContextIdOnWindowContext();
+ LOG("context %" PRIu64 " becomes active session context",
+ *mActiveMediaSessionContextId);
+ mMetadataChangedEvent.Notify(GetCurrentMediaMetadata());
+ mSupportedActionsChangedEvent.Notify(GetSupportedActions());
+ if (StaticPrefs::media_mediacontrol_testingevents_enabled()) {
+ if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
+ obs->NotifyObservers(nullptr, "active-media-session-changed", nullptr);
+ }
+ }
+}
+
+void MediaStatusManager::ClearActiveMediaSessionContextIdIfNeeded() {
+ if (!mActiveMediaSessionContextId) {
+ return;
+ }
+ LOG("Clear active session context");
+ mActiveMediaSessionContextId.reset();
+ StoreMediaSessionContextIdOnWindowContext();
+ mMetadataChangedEvent.Notify(GetCurrentMediaMetadata());
+ mSupportedActionsChangedEvent.Notify(GetSupportedActions());
+ if (StaticPrefs::media_mediacontrol_testingevents_enabled()) {
+ if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
+ obs->NotifyObservers(nullptr, "active-media-session-changed", nullptr);
+ }
+ }
+}
+
+void MediaStatusManager::StoreMediaSessionContextIdOnWindowContext() {
+ RefPtr<CanonicalBrowsingContext> bc =
+ CanonicalBrowsingContext::Get(mTopLevelBrowsingContextId);
+ if (bc && bc->GetTopWindowContext()) {
+ Unused << bc->GetTopWindowContext()->SetActiveMediaSessionContextId(
+ mActiveMediaSessionContextId);
+ }
+}
+
+bool MediaStatusManager::IsSessionOwningAudioFocus(
+ uint64_t aBrowsingContextId) const {
+ Maybe<uint64_t> audioFocusContextId =
+ mPlaybackStatusDelegate.GetAudioFocusOwnerContextId();
+ return audioFocusContextId ? *audioFocusContextId == aBrowsingContextId
+ : false;
+}
+
+MediaMetadataBase MediaStatusManager::CreateDefaultMetadata() const {
+ MediaMetadataBase metadata;
+ metadata.mTitle = GetDefaultTitle();
+ metadata.mArtwork.AppendElement()->mSrc = GetDefaultFaviconURL();
+
+ LOG("Default media metadata, title=%s, album src=%s",
+ NS_ConvertUTF16toUTF8(metadata.mTitle).get(),
+ NS_ConvertUTF16toUTF8(metadata.mArtwork[0].mSrc).get());
+ return metadata;
+}
+
+nsString MediaStatusManager::GetDefaultTitle() const {
+ RefPtr<MediaControlService> service = MediaControlService::GetService();
+ nsString defaultTitle = service->GetFallbackTitle();
+
+ RefPtr<CanonicalBrowsingContext> bc =
+ CanonicalBrowsingContext::Get(mTopLevelBrowsingContextId);
+ if (!bc) {
+ return defaultTitle;
+ }
+
+ RefPtr<WindowGlobalParent> globalParent = bc->GetCurrentWindowGlobal();
+ if (!globalParent) {
+ return defaultTitle;
+ }
+
+ // The media metadata would be shown on the virtual controller interface. For
+ // example, on Android, the interface would be shown on both notification bar
+ // and lockscreen. Therefore, what information we provide via metadata is
+ // quite important, because if we're in private browsing, we don't want to
+ // expose details about what website the user is browsing on the lockscreen.
+ // Therefore, using the default title when in the private browsing or the
+ // document title is empty. Otherwise, use the document title.
+ nsString documentTitle;
+ if (!IsInPrivateBrowsing()) {
+ globalParent->GetDocumentTitle(documentTitle);
+ }
+ return documentTitle.IsEmpty() ? defaultTitle : documentTitle;
+}
+
+nsString MediaStatusManager::GetDefaultFaviconURL() const {
+#ifdef MOZ_PLACES
+ nsCOMPtr<nsIURI> faviconURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(faviconURI),
+ nsLiteralCString(FAVICON_DEFAULT_URL));
+ NS_ENSURE_SUCCESS(rv, u""_ns);
+
+ // Convert URI from `chrome://XXX` to `file://XXX` because we would like to
+ // let OS related frameworks, such as SMTC and MPRIS, handle this URL in order
+ // to show the icon on virtual controller interface.
+ nsCOMPtr<nsIChromeRegistry> regService = services::GetChromeRegistry();
+ if (!regService) {
+ return u""_ns;
+ }
+ nsCOMPtr<nsIURI> processedURI;
+ regService->ConvertChromeURL(faviconURI, getter_AddRefs(processedURI));
+
+ nsAutoCString spec;
+ if (NS_FAILED(processedURI->GetSpec(spec))) {
+ return u""_ns;
+ }
+ return NS_ConvertUTF8toUTF16(spec);
+#else
+ return u""_ns;
+#endif
+}
+
+void MediaStatusManager::SetDeclaredPlaybackState(
+ uint64_t aBrowsingContextId, MediaSessionPlaybackState aState) {
+ auto info = mMediaSessionInfoMap.Lookup(aBrowsingContextId);
+ if (!info) {
+ return;
+ }
+ LOG("SetDeclaredPlaybackState from %s to %s",
+ ToMediaSessionPlaybackStateStr(info->mDeclaredPlaybackState),
+ ToMediaSessionPlaybackStateStr(aState));
+ info->mDeclaredPlaybackState = aState;
+ UpdateActualPlaybackState();
+}
+
+MediaSessionPlaybackState MediaStatusManager::GetCurrentDeclaredPlaybackState()
+ const {
+ if (!mActiveMediaSessionContextId) {
+ return MediaSessionPlaybackState::None;
+ }
+ return mMediaSessionInfoMap.Get(*mActiveMediaSessionContextId)
+ .mDeclaredPlaybackState;
+}
+
+void MediaStatusManager::NotifyMediaPlaybackChanged(uint64_t aBrowsingContextId,
+ MediaPlaybackState aState) {
+ LOG("UpdateMediaPlaybackState %s for context %" PRIu64,
+ ToMediaPlaybackStateStr(aState), aBrowsingContextId);
+ const bool oldPlaying = mPlaybackStatusDelegate.IsPlaying();
+ mPlaybackStatusDelegate.UpdateMediaPlaybackState(aBrowsingContextId, aState);
+
+ // Playback state doesn't change, we don't need to update the guessed playback
+ // state. This is used to prevent the state from changing from `none` to
+ // `paused` when receiving `MediaPlaybackState::eStarted`.
+ if (mPlaybackStatusDelegate.IsPlaying() == oldPlaying) {
+ return;
+ }
+ if (mPlaybackStatusDelegate.IsPlaying()) {
+ SetGuessedPlayState(MediaSessionPlaybackState::Playing);
+ } else {
+ SetGuessedPlayState(MediaSessionPlaybackState::Paused);
+ }
+}
+
+void MediaStatusManager::SetGuessedPlayState(MediaSessionPlaybackState aState) {
+ if (aState == mGuessedPlaybackState) {
+ return;
+ }
+ LOG("SetGuessedPlayState : '%s'", ToMediaSessionPlaybackStateStr(aState));
+ mGuessedPlaybackState = aState;
+ UpdateActualPlaybackState();
+}
+
+void MediaStatusManager::UpdateActualPlaybackState() {
+ // The way to compute the actual playback state is based on the spec.
+ // https://w3c.github.io/mediasession/#actual-playback-state
+ MediaSessionPlaybackState newState =
+ GetCurrentDeclaredPlaybackState() == MediaSessionPlaybackState::Playing
+ ? MediaSessionPlaybackState::Playing
+ : mGuessedPlaybackState;
+ if (mActualPlaybackState == newState) {
+ return;
+ }
+ mActualPlaybackState = newState;
+ LOG("UpdateActualPlaybackState : '%s'",
+ ToMediaSessionPlaybackStateStr(mActualPlaybackState));
+ mPlaybackStateChangedEvent.Notify(mActualPlaybackState);
+}
+
+void MediaStatusManager::EnableAction(uint64_t aBrowsingContextId,
+ MediaSessionAction aAction) {
+ auto info = mMediaSessionInfoMap.Lookup(aBrowsingContextId);
+ if (!info) {
+ return;
+ }
+ if (info->IsActionSupported(aAction)) {
+ LOG("Action '%s' has already been enabled for context %" PRIu64,
+ ToMediaSessionActionStr(aAction), aBrowsingContextId);
+ return;
+ }
+ LOG("Enable action %s for context %" PRIu64, ToMediaSessionActionStr(aAction),
+ aBrowsingContextId);
+ info->EnableAction(aAction);
+ NotifySupportedKeysChangedIfNeeded(aBrowsingContextId);
+}
+
+void MediaStatusManager::DisableAction(uint64_t aBrowsingContextId,
+ MediaSessionAction aAction) {
+ auto info = mMediaSessionInfoMap.Lookup(aBrowsingContextId);
+ if (!info) {
+ return;
+ }
+ if (!info->IsActionSupported(aAction)) {
+ LOG("Action '%s' hasn't been enabled yet for context %" PRIu64,
+ ToMediaSessionActionStr(aAction), aBrowsingContextId);
+ return;
+ }
+ LOG("Disable action %s for context %" PRIu64,
+ ToMediaSessionActionStr(aAction), aBrowsingContextId);
+ info->DisableAction(aAction);
+ NotifySupportedKeysChangedIfNeeded(aBrowsingContextId);
+}
+
+void MediaStatusManager::UpdatePositionState(uint64_t aBrowsingContextId,
+ const PositionState& aState) {
+ // The position state comes from non-active media session which we don't care.
+ if (!mActiveMediaSessionContextId ||
+ *mActiveMediaSessionContextId != aBrowsingContextId) {
+ return;
+ }
+ mPositionStateChangedEvent.Notify(aState);
+}
+
+void MediaStatusManager::NotifySupportedKeysChangedIfNeeded(
+ uint64_t aBrowsingContextId) {
+ // Only the active media session's supported actions would be shown in virtual
+ // control interface, so we only notify the event when supported actions
+ // change happens on the active media session.
+ if (!mActiveMediaSessionContextId ||
+ *mActiveMediaSessionContextId != aBrowsingContextId) {
+ return;
+ }
+ mSupportedActionsChangedEvent.Notify(GetSupportedActions());
+}
+
+CopyableTArray<MediaSessionAction> MediaStatusManager::GetSupportedActions()
+ const {
+ CopyableTArray<MediaSessionAction> supportedActions;
+ if (!mActiveMediaSessionContextId) {
+ return supportedActions;
+ }
+
+ MediaSessionInfo info =
+ mMediaSessionInfoMap.Get(*mActiveMediaSessionContextId);
+ const uint8_t actionNums = uint8_t(MediaSessionAction::EndGuard_);
+ for (uint8_t actionValue = 0; actionValue < actionNums; actionValue++) {
+ MediaSessionAction action = ConvertToMediaSessionAction(actionValue);
+ if (info.IsActionSupported(action)) {
+ supportedActions.AppendElement(action);
+ }
+ }
+ return supportedActions;
+}
+
+MediaMetadataBase MediaStatusManager::GetCurrentMediaMetadata() const {
+ // If we don't have active media session, active media session doesn't have
+ // media metadata, or we're in private browsing mode, then we should create a
+ // default metadata which is using website's title and favicon as title and
+ // artwork.
+ if (mActiveMediaSessionContextId && !IsInPrivateBrowsing()) {
+ MediaSessionInfo info =
+ mMediaSessionInfoMap.Get(*mActiveMediaSessionContextId);
+ if (!info.mMetadata) {
+ return CreateDefaultMetadata();
+ }
+ MediaMetadataBase& metadata = *(info.mMetadata);
+ FillMissingTitleAndArtworkIfNeeded(metadata);
+ return metadata;
+ }
+ return CreateDefaultMetadata();
+}
+
+void MediaStatusManager::FillMissingTitleAndArtworkIfNeeded(
+ MediaMetadataBase& aMetadata) const {
+ // If the metadata doesn't set its title and artwork properly, we would like
+ // to use default title and favicon instead in order to prevent showing
+ // nothing on the virtual control interface.
+ if (aMetadata.mTitle.IsEmpty()) {
+ aMetadata.mTitle = GetDefaultTitle();
+ }
+ if (aMetadata.mArtwork.IsEmpty()) {
+ aMetadata.mArtwork.AppendElement()->mSrc = GetDefaultFaviconURL();
+ }
+}
+
+bool MediaStatusManager::IsInPrivateBrowsing() const {
+ RefPtr<CanonicalBrowsingContext> bc =
+ CanonicalBrowsingContext::Get(mTopLevelBrowsingContextId);
+ if (!bc) {
+ return false;
+ }
+ RefPtr<Element> element = bc->GetEmbedderElement();
+ if (!element) {
+ return false;
+ }
+ return nsContentUtils::IsInPrivateBrowsing(element->OwnerDoc());
+}
+
+MediaSessionPlaybackState MediaStatusManager::PlaybackState() const {
+ return mActualPlaybackState;
+}
+
+bool MediaStatusManager::IsMediaAudible() const {
+ return mPlaybackStatusDelegate.IsAudible();
+}
+
+bool MediaStatusManager::IsMediaPlaying() const {
+ return mActualPlaybackState == MediaSessionPlaybackState::Playing;
+}
+
+bool MediaStatusManager::IsAnyMediaBeingControlled() const {
+ return mPlaybackStatusDelegate.IsAnyMediaBeingControlled();
+}
+
+void MediaStatusManager::NotifyPageTitleChanged() {
+ // If active media session has set non-empty metadata, then we would use that
+ // instead of using default metadata.
+ if (mActiveMediaSessionContextId &&
+ mMediaSessionInfoMap.Lookup(*mActiveMediaSessionContextId)->mMetadata) {
+ return;
+ }
+ // In private browsing mode, we won't show page title on default metadata so
+ // we don't need to update that.
+ if (IsInPrivateBrowsing()) {
+ return;
+ }
+ LOG("page title changed, update default metadata");
+ mMetadataChangedEvent.Notify(GetCurrentMediaMetadata());
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/mediacontrol/MediaStatusManager.h b/dom/media/mediacontrol/MediaStatusManager.h
new file mode 100644
index 0000000000..24247d119d
--- /dev/null
+++ b/dom/media/mediacontrol/MediaStatusManager.h
@@ -0,0 +1,276 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_MEDIACONTROL_MEDIASTATUSMANAGER_H_
+#define DOM_MEDIA_MEDIACONTROL_MEDIASTATUSMANAGER_H_
+
+#include "MediaControlKeySource.h"
+#include "MediaEventSource.h"
+#include "MediaPlaybackStatus.h"
+#include "mozilla/dom/MediaMetadata.h"
+#include "mozilla/dom/MediaSessionBinding.h"
+#include "mozilla/Maybe.h"
+#include "nsTHashMap.h"
+#include "nsISupportsImpl.h"
+
+namespace mozilla::dom {
+
+class MediaSessionInfo {
+ public:
+ MediaSessionInfo() = default;
+
+ explicit MediaSessionInfo(MediaMetadataBase& aMetadata) {
+ mMetadata.emplace(aMetadata);
+ }
+
+ MediaSessionInfo(MediaMetadataBase& aMetadata,
+ MediaSessionPlaybackState& aState) {
+ mMetadata.emplace(aMetadata);
+ mDeclaredPlaybackState = aState;
+ }
+
+ static MediaSessionInfo EmptyInfo() { return MediaSessionInfo(); }
+
+ static uint32_t GetActionBitMask(MediaSessionAction aAction) {
+ return 1 << static_cast<uint8_t>(aAction);
+ }
+
+ void EnableAction(MediaSessionAction aAction) {
+ mSupportedActions |= GetActionBitMask(aAction);
+ }
+
+ void DisableAction(MediaSessionAction aAction) {
+ mSupportedActions &= ~GetActionBitMask(aAction);
+ }
+
+ bool IsActionSupported(MediaSessionAction aAction) const {
+ return mSupportedActions & GetActionBitMask(aAction);
+ }
+
+ // These attributes are all propagated from the media session in the content
+ // process.
+ Maybe<MediaMetadataBase> mMetadata;
+ MediaSessionPlaybackState mDeclaredPlaybackState =
+ MediaSessionPlaybackState::None;
+ // Use bitwise to store the supported actions.
+ uint32_t mSupportedActions = 0;
+};
+
+/**
+ * IMediaInfoUpdater is an interface which provides methods to update the media
+ * related information that happens in the content process.
+ */
+class IMediaInfoUpdater {
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+ // Use this method to update controlled media's playback state and the
+ // browsing context where controlled media exists. When notifying the state
+ // change, we MUST follow the following rules.
+ // (1) `eStart` MUST be the first state and `eStop` MUST be the last state
+ // (2) Do not notify same state again
+ // (3) `ePaused` can only be notified after notifying `ePlayed`.
+ virtual void NotifyMediaPlaybackChanged(uint64_t aBrowsingContextId,
+ MediaPlaybackState aState) = 0;
+
+ // Use this method to update the audible state of controlled media, and MUST
+ // follow the following rules in which `audible` and `inaudible` should be a
+ // pair. `inaudible` should always be notified after `audible`. When audible
+ // media paused, `inaudible` should be notified
+ // Eg. (O) `audible` -> `inaudible` -> `audible` -> `inaudible`
+ // (X) `inaudible` -> `audible` [notify `inaudible` before `audible`]
+ // (X) `audible` -> `audible` [notify `audible` twice]
+ // (X) `audible` -> (media pauses) [forgot to notify `inaudible`]
+ virtual void NotifyMediaAudibleChanged(uint64_t aBrowsingContextId,
+ MediaAudibleState aState) = 0;
+
+ // Use this method to update media session's declared playback state for the
+ // specific media session.
+ virtual void SetDeclaredPlaybackState(uint64_t aBrowsingContextId,
+ MediaSessionPlaybackState aState) = 0;
+
+ // Use these methods to update controller's media session list. We'd use it
+ // when media session is created/destroyed in the content process.
+ virtual void NotifySessionCreated(uint64_t aBrowsingContextId) = 0;
+ virtual void NotifySessionDestroyed(uint64_t aBrowsingContextId) = 0;
+
+ // Use this method to update the metadata for the specific media session.
+ virtual void UpdateMetadata(uint64_t aBrowsingContextId,
+ const Maybe<MediaMetadataBase>& aMetadata) = 0;
+
+ // Use this method to update the picture in picture mode state of controlled
+ // media, and it's safe to notify same state again.
+ virtual void SetIsInPictureInPictureMode(uint64_t aBrowsingContextId,
+ bool aIsInPictureInPictureMode) = 0;
+
+ // Use these methods to update the supported media session action for the
+ // specific media session. For a media session from a given browsing context,
+ // do not re-enable the same action, or disable the action without enabling it
+ // before.
+ virtual void EnableAction(uint64_t aBrowsingContextId,
+ MediaSessionAction aAction) = 0;
+ virtual void DisableAction(uint64_t aBrowsingContextId,
+ MediaSessionAction aAction) = 0;
+
+ // Use this method when media enters or leaves the fullscreen.
+ virtual void NotifyMediaFullScreenState(uint64_t aBrowsingContextId,
+ bool aIsInFullScreen) = 0;
+
+ // Use this method when media session update its position state.
+ virtual void UpdatePositionState(uint64_t aBrowsingContextId,
+ const PositionState& aState) = 0;
+};
+
+/**
+ * MediaStatusManager would decide the media related status which can represents
+ * the whole tab. The status includes the playback status, tab's metadata and
+ * the active media session ID if it exists.
+ *
+ * We would use `IMediaInfoUpdater` methods to update the media playback related
+ * information and then use `MediaPlaybackStatus` to determine the final
+ * playback state.
+ *
+ * The metadata would be the one from the active media session, or the default
+ * one. This class would determine which media session is an active media
+ * session [1] whithin a tab. It tracks all alive media sessions within a tab
+ * and store their metadata which could be used to show on the virtual media
+ * control interface. In addition, we can use it to get the current media
+ * metadata even if there is no media session existing. However, the meaning of
+ * active media session here is not equal to the definition from the spec [1].
+ * We just choose the session which is the active one inside the tab, the global
+ * active media session among different tabs would be the one inside the main
+ * controller which is determined by MediaControlService.
+ *
+ * [1] https://w3c.github.io/mediasession/#active-media-session
+ */
+class MediaStatusManager : public IMediaInfoUpdater {
+ public:
+ explicit MediaStatusManager(uint64_t aBrowsingContextId);
+
+ // IMediaInfoUpdater's methods
+ void NotifyMediaPlaybackChanged(uint64_t aBrowsingContextId,
+ MediaPlaybackState aState) override;
+ void NotifyMediaAudibleChanged(uint64_t aBrowsingContextId,
+ MediaAudibleState aState) override;
+ void SetDeclaredPlaybackState(uint64_t aSessionContextId,
+ MediaSessionPlaybackState aState) override;
+ void NotifySessionCreated(uint64_t aSessionContextId) override;
+ void NotifySessionDestroyed(uint64_t aSessionContextId) override;
+ void UpdateMetadata(uint64_t aSessionContextId,
+ const Maybe<MediaMetadataBase>& aMetadata) override;
+ void EnableAction(uint64_t aBrowsingContextId,
+ MediaSessionAction aAction) override;
+ void DisableAction(uint64_t aBrowsingContextId,
+ MediaSessionAction aAction) override;
+ void UpdatePositionState(uint64_t aBrowsingContextId,
+ const PositionState& aState) override;
+
+ // Return active media session's metadata if active media session exists and
+ // it has already set its metadata. Otherwise, return default media metadata
+ // which is based on website's title and favicon.
+ MediaMetadataBase GetCurrentMediaMetadata() const;
+
+ bool IsMediaAudible() const;
+ bool IsMediaPlaying() const;
+ bool IsAnyMediaBeingControlled() const;
+
+ // These events would be notified when the active media session's certain
+ // property changes.
+ MediaEventSource<MediaMetadataBase>& MetadataChangedEvent() {
+ return mMetadataChangedEvent;
+ }
+
+ MediaEventSource<PositionState>& PositionChangedEvent() {
+ return mPositionStateChangedEvent;
+ }
+
+ MediaEventSource<MediaSessionPlaybackState>& PlaybackChangedEvent() {
+ return mPlaybackStateChangedEvent;
+ }
+
+ // Return the actual playback state.
+ MediaSessionPlaybackState PlaybackState() const;
+
+ // When page title changes, we might need to update it on the default
+ // metadata as well.
+ void NotifyPageTitleChanged();
+
+ protected:
+ ~MediaStatusManager() = default;
+
+ // This event would be notified when the active media session changes its
+ // supported actions.
+ MediaEventSource<nsTArray<MediaSessionAction>>&
+ SupportedActionsChangedEvent() {
+ return mSupportedActionsChangedEvent;
+ }
+
+ uint64_t mTopLevelBrowsingContextId;
+
+ // Within a tab, the Id of the browsing context which has already created a
+ // media session and owns the audio focus within a tab.
+ Maybe<uint64_t> mActiveMediaSessionContextId;
+
+ void ClearActiveMediaSessionContextIdIfNeeded();
+
+ private:
+ nsString GetDefaultFaviconURL() const;
+ nsString GetDefaultTitle() const;
+ MediaMetadataBase CreateDefaultMetadata() const;
+ bool IsInPrivateBrowsing() const;
+ void FillMissingTitleAndArtworkIfNeeded(MediaMetadataBase& aMetadata) const;
+
+ bool IsSessionOwningAudioFocus(uint64_t aBrowsingContextId) const;
+ void SetActiveMediaSessionContextId(uint64_t aBrowsingContextId);
+ void HandleAudioFocusOwnerChanged(Maybe<uint64_t>& aBrowsingContextId);
+
+ void NotifySupportedKeysChangedIfNeeded(uint64_t aBrowsingContextId);
+
+ // Return a copyable array filled with the supported media session actions.
+ // Use copyable array so that we can use the result as a parameter for the
+ // media event.
+ CopyableTArray<MediaSessionAction> GetSupportedActions() const;
+
+ void StoreMediaSessionContextIdOnWindowContext();
+
+ // When the amount of playing media changes, we would use this function to
+ // update the guessed playback state.
+ void SetGuessedPlayState(MediaSessionPlaybackState aState);
+
+ // Whenever the declared playback state or the guessed playback state changes,
+ // we should recompute actual playback state to know if we need to update the
+ // virtual control interface.
+ void UpdateActualPlaybackState();
+
+ // Return the active media session's declared playback state. If the active
+ // media session doesn't exist, return 'None' instead.
+ MediaSessionPlaybackState GetCurrentDeclaredPlaybackState() const;
+
+ // This state can match to the `guessed playback state` in the spec [1], it
+ // indicates if we have any media element playing within the tab which this
+ // controller belongs to. But currently we only take media elements into
+ // account, which is different from the way the spec recommends. In addition,
+ // We don't support web audio and plugin and not consider audible state of
+ // media.
+ // [1] https://w3c.github.io/mediasession/#guessed-playback-state
+ MediaSessionPlaybackState mGuessedPlaybackState =
+ MediaSessionPlaybackState::None;
+
+ // This playback state would be the final playback which can be used to know
+ // if the controller is playing or not.
+ // https://w3c.github.io/mediasession/#actual-playback-state
+ MediaSessionPlaybackState mActualPlaybackState =
+ MediaSessionPlaybackState::None;
+
+ nsTHashMap<nsUint64HashKey, MediaSessionInfo> mMediaSessionInfoMap;
+ MediaEventProducer<MediaMetadataBase> mMetadataChangedEvent;
+ MediaEventProducer<nsTArray<MediaSessionAction>>
+ mSupportedActionsChangedEvent;
+ MediaEventProducer<PositionState> mPositionStateChangedEvent;
+ MediaEventProducer<MediaSessionPlaybackState> mPlaybackStateChangedEvent;
+ MediaPlaybackStatus mPlaybackStatusDelegate;
+};
+
+} // namespace mozilla::dom
+
+#endif // DOM_MEDIA_MEDIACONTROL_MEDIASTATUSMANAGER_H_
diff --git a/dom/media/mediacontrol/PositionStateEvent.h b/dom/media/mediacontrol/PositionStateEvent.h
new file mode 100644
index 0000000000..5d6a5ebb78
--- /dev/null
+++ b/dom/media/mediacontrol/PositionStateEvent.h
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* THIS FILE IS AUTOGENERATED FROM PositionStateEvent.webidl BY Codegen.py - DO
+ * NOT EDIT */
+
+#ifndef mozilla_dom_PositionStateEvent_h
+#define mozilla_dom_PositionStateEvent_h
+
+#include "js/RootingAPI.h"
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/dom/Event.h"
+#include "nsISupports.h"
+#include "nsStringFwd.h"
+
+struct JSContext;
+namespace mozilla {
+namespace dom {
+struct PositionStateEventInit;
+
+class PositionStateEvent : public Event {
+ public:
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(PositionStateEvent, Event)
+
+ protected:
+ virtual ~PositionStateEvent();
+ explicit PositionStateEvent(mozilla::dom::EventTarget* aOwner);
+
+ double mDuration;
+ double mPlaybackRate;
+ double mPosition;
+
+ public:
+ PositionStateEvent* AsPositionStateEvent() override;
+
+ JSObject* WrapObjectInternal(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ static already_AddRefed<PositionStateEvent> Constructor(
+ mozilla::dom::EventTarget* aOwner, const nsAString& aType,
+ const PositionStateEventInit& aEventInitDict);
+
+ static already_AddRefed<PositionStateEvent> Constructor(
+ const GlobalObject& aGlobal, const nsAString& aType,
+ const PositionStateEventInit& aEventInitDict);
+
+ double Duration() const;
+
+ double PlaybackRate() const;
+
+ double Position() const;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_PositionStateEvent_h
diff --git a/dom/media/mediacontrol/moz.build b/dom/media/mediacontrol/moz.build
new file mode 100644
index 0000000000..54b3c40a36
--- /dev/null
+++ b/dom/media/mediacontrol/moz.build
@@ -0,0 +1,43 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS.mozilla.dom += [
+ "AudioFocusManager.h",
+ "ContentMediaController.h",
+ "ContentPlaybackController.h",
+ "FetchImageHelper.h",
+ "MediaControlKeyManager.h",
+ "MediaControlKeySource.h",
+ "MediaController.h",
+ "MediaControlService.h",
+ "MediaControlUtils.h",
+ "MediaPlaybackStatus.h",
+ "MediaStatusManager.h",
+]
+
+EXPORTS.ipc += [
+ "MediaControlIPC.h",
+]
+
+UNIFIED_SOURCES += [
+ "AudioFocusManager.cpp",
+ "ContentMediaController.cpp",
+ "ContentPlaybackController.cpp",
+ "FetchImageHelper.cpp",
+ "MediaControlKeyManager.cpp",
+ "MediaControlKeySource.cpp",
+ "MediaController.cpp",
+ "MediaControlService.cpp",
+ "MediaControlUtils.cpp",
+ "MediaPlaybackStatus.cpp",
+ "MediaStatusManager.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+if CONFIG["ENABLE_TESTS"]:
+ DIRS += ["tests/gtest"]
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/mediacontrol/tests/browser/browser.ini b/dom/media/mediacontrol/tests/browser/browser.ini
new file mode 100644
index 0000000000..d6338e598b
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser.ini
@@ -0,0 +1,53 @@
+[DEFAULT]
+subsuite = media-bc
+tags = mediacontrol
+skip-if = os == "linux" # Bug 1673527
+support-files =
+ file_autoplay.html
+ file_audio_and_inaudible_media.html
+ file_empty_title.html
+ file_error_media.html
+ file_iframe_media.html
+ file_main_frame_with_multiple_child_session_frames.html
+ file_multiple_audible_media.html
+ file_muted_autoplay.html
+ file_no_src_media.html
+ file_non_autoplay.html
+ file_non_eligible_media.html
+ file_non_looping_media.html
+ head.js
+ ../../../test/bogus.ogv
+ ../../../test/gizmo.mp4
+ ../../../test/gizmo-noaudio.webm
+ ../../../test/gizmo-short.mp4
+ !/toolkit/components/pictureinpicture/tests/head.js
+ ../../../../../toolkit/content/tests/browser/silentAudioTrack.webm
+
+[browser_audio_focus_management.js]
+skip-if =
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_control_page_with_audible_and_inaudible_media.js]
+[browser_default_action_handler.js]
+[browser_only_control_non_real_time_media.js]
+[browser_media_control_audio_focus_within_a_page.js]
+[browser_media_control_before_media_starts.js]
+[browser_media_control_captured_audio.js]
+[browser_media_control_metadata.js]
+[browser_media_control_keys_event.js]
+[browser_media_control_main_controller.js]
+[browser_media_control_non_eligible_media.js]
+skip-if =
+ verify && os == 'mac' # bug 1673509
+[browser_media_control_playback_state.js]
+[browser_media_control_position_state.js]
+[browser_media_control_seekto.js]
+[browser_media_control_supported_keys.js]
+[browser_media_control_stop_timer.js]
+[browser_nosrc_and_error_media.js]
+skip-if =
+ verify && os == 'mac' # bug 1673509
+[browser_seek_captured_audio.js]
+[browser_stop_control_after_media_reaches_to_end.js]
+[browser_suspend_inactive_tab.js]
+[browser_remove_controllable_media_for_active_controller.js]
+[browser_resume_latest_paused_media.js]
diff --git a/dom/media/mediacontrol/tests/browser/browser_audio_focus_management.js b/dom/media/mediacontrol/tests/browser/browser_audio_focus_management.js
new file mode 100644
index 0000000000..980281243d
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_audio_focus_management.js
@@ -0,0 +1,179 @@
+const PAGE_AUDIBLE =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_autoplay.html";
+const PAGE_INAUDIBLE =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_muted_autoplay.html";
+
+const testVideoId = "autoplay";
+
+/**
+ * These tests are used to ensure that the audio focus management works correctly
+ * amongs different tabs no matter the pref is on or off. If the pref is on,
+ * there is only one tab which is allowed to play audio at a time, the last tab
+ * starting audio will immediately stop other tabs which own audio focus. But
+ * notice that playing inaudible media won't gain audio focus. If the pref is
+ * off, all audible tabs can own audio focus at the same time without
+ * interfering each others.
+ */
+add_task(async function testDisableAudioFocusManagement() {
+ await switchAudioFocusManagerment(false);
+
+ info(`open audible autoplay media in tab1`);
+ const tab1 = await createLoadedTabWrapper(PAGE_AUDIBLE, { needCheck: false });
+ await checkOrWaitUntilMediaStartedPlaying(tab1, testVideoId);
+
+ info(`open same page on another tab, which shouldn't cause audio competing`);
+ const tab2 = await createLoadedTabWrapper(PAGE_AUDIBLE, { needCheck: false });
+ await checkOrWaitUntilMediaStartedPlaying(tab2, testVideoId);
+
+ info(`media in tab1 should be playing still`);
+ await checkOrWaitUntilMediaStartedPlaying(tab1, testVideoId);
+
+ info(`remove tabs`);
+ await clearTabsAndResetPref([tab1, tab2]);
+});
+
+add_task(async function testEnableAudioFocusManagement() {
+ await switchAudioFocusManagerment(true);
+
+ info(`open audible autoplay media in tab1`);
+ const tab1 = await createLoadedTabWrapper(PAGE_AUDIBLE, { needCheck: false });
+ await checkOrWaitUntilMediaStartedPlaying(tab1, testVideoId);
+
+ info(`open same page on another tab, which should cause audio competing`);
+ const tab2 = await createLoadedTabWrapper(PAGE_AUDIBLE, { needCheck: false });
+ await checkOrWaitUntilMediaStartedPlaying(tab2, testVideoId);
+
+ info(`media in tab1 should be stopped`);
+ await checkOrWaitUntilMediaStoppedPlaying(tab1, testVideoId);
+
+ info(`remove tabs`);
+ await clearTabsAndResetPref([tab1, tab2]);
+});
+
+add_task(async function testCheckAudioCompetingMultipleTimes() {
+ await switchAudioFocusManagerment(true);
+
+ info(`open audible autoplay media in tab1`);
+ const tab1 = await createLoadedTabWrapper(PAGE_AUDIBLE, { needCheck: false });
+ await checkOrWaitUntilMediaStartedPlaying(tab1, testVideoId);
+
+ info(`open same page on another tab, which should cause audio competing`);
+ const tab2 = await createLoadedTabWrapper(PAGE_AUDIBLE, { needCheck: false });
+ await checkOrWaitUntilMediaStartedPlaying(tab2, testVideoId);
+
+ info(`media in tab1 should be stopped`);
+ await checkOrWaitUntilMediaStoppedPlaying(tab1, testVideoId);
+
+ info(`play media in tab1 again`);
+ await playMedia(tab1);
+
+ info(`media in tab2 should be stopped`);
+ await checkOrWaitUntilMediaStoppedPlaying(tab2, testVideoId);
+
+ info(`play media in tab2 again`);
+ await playMedia(tab2);
+
+ info(`media in tab1 should be stopped`);
+ await checkOrWaitUntilMediaStoppedPlaying(tab1, testVideoId);
+
+ info(`remove tabs`);
+ await clearTabsAndResetPref([tab1, tab2]);
+});
+
+add_task(async function testMutedMediaWontInvolveAudioCompeting() {
+ await switchAudioFocusManagerment(true);
+
+ info(`open audible autoplay media in tab1`);
+ const tab1 = await createLoadedTabWrapper(PAGE_AUDIBLE, { needCheck: false });
+ await checkOrWaitUntilMediaStartedPlaying(tab1, testVideoId);
+
+ info(
+ `open inaudible media page on another tab, which shouldn't cause audio competing`
+ );
+ const tab2 = await createLoadedTabWrapper(PAGE_INAUDIBLE, {
+ needCheck: false,
+ });
+ await checkOrWaitUntilMediaStartedPlaying(tab2, testVideoId);
+
+ info(`media in tab1 should be playing still`);
+ await checkOrWaitUntilMediaStartedPlaying(tab1, testVideoId);
+
+ info(
+ `open audible media page on the third tab, which should cause audio competing`
+ );
+ const tab3 = await createLoadedTabWrapper(PAGE_AUDIBLE, { needCheck: false });
+ await checkOrWaitUntilMediaStartedPlaying(tab3, testVideoId);
+
+ info(`media in tab1 should be stopped`);
+ await checkOrWaitUntilMediaStoppedPlaying(tab1, testVideoId);
+
+ info(`media in tab2 should not be affected because it's inaudible.`);
+ await checkOrWaitUntilMediaStartedPlaying(tab2, testVideoId);
+
+ info(`remove tabs`);
+ await clearTabsAndResetPref([tab1, tab2, tab3]);
+});
+
+add_task(async function testStopMultipleTabsWhenSwitchingPrefDynamically() {
+ await switchAudioFocusManagerment(false);
+
+ info(`open audible autoplay media in tab1`);
+ const tab1 = await createLoadedTabWrapper(PAGE_AUDIBLE, { needCheck: false });
+ await checkOrWaitUntilMediaStartedPlaying(tab1, testVideoId);
+
+ info(`open same page on another tab, which shouldn't cause audio competing`);
+ const tab2 = await createLoadedTabWrapper(PAGE_AUDIBLE, { needCheck: false });
+ await checkOrWaitUntilMediaStartedPlaying(tab2, testVideoId);
+
+ await switchAudioFocusManagerment(true);
+
+ info(`open same page on the third tab, which should cause audio competing`);
+ const tab3 = await createLoadedTabWrapper(PAGE_AUDIBLE, { needCheck: false });
+ await checkOrWaitUntilMediaStartedPlaying(tab3, testVideoId);
+
+ info(`media in tab1 and tab2 should be stopped`);
+ await checkOrWaitUntilMediaStoppedPlaying(tab1, testVideoId);
+ await checkOrWaitUntilMediaStoppedPlaying(tab2, testVideoId);
+
+ info(`remove tabs`);
+ await clearTabsAndResetPref([tab1, tab2, tab3]);
+});
+
+/**
+ * The following are helper funcions.
+ */
+async function switchAudioFocusManagerment(enable) {
+ const state = enable ? "Enable" : "Disable";
+ info(`${state} audio focus management`);
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.audioFocus.management", enable]],
+ });
+}
+
+async function playMedia(tab) {
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ return new Promise(resolve => {
+ const video = content.document.getElementById("autoplay");
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+
+ ok(video.paused, `media has not started yet`);
+ info(`wait until media starts playing`);
+ video.play();
+ video.onplaying = () => {
+ video.onplaying = null;
+ ok(true, `media started playing`);
+ resolve();
+ };
+ });
+ });
+}
+
+async function clearTabsAndResetPref(tabs) {
+ info(`clear tabs and reset pref`);
+ for (let tab of tabs) {
+ await tab.close();
+ }
+ await switchAudioFocusManagerment(false);
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_control_page_with_audible_and_inaudible_media.js b/dom/media/mediacontrol/tests/browser/browser_control_page_with_audible_and_inaudible_media.js
new file mode 100644
index 0000000000..6d9bc38b04
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_control_page_with_audible_and_inaudible_media.js
@@ -0,0 +1,94 @@
+const PAGE_URL =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_audio_and_inaudible_media.html";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.mediacontrol.testingevents.enabled", true]],
+ });
+});
+
+/**
+ * When a page has audible media and inaudible media playing at the same time,
+ * only audible media should be controlled by media control keys. However, once
+ * inaudible media becomes audible, then it should be able to be controlled.
+ */
+add_task(async function testSetPositionState() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_URL);
+
+ info(`play video1 (audible) and video2 (inaudible)`);
+ await playBothAudibleAndInaudibleMedia(tab);
+
+ info(`pressing 'pause' should only affect video1 (audible)`);
+ await generateMediaControlKeyEvent("pause");
+ await checkMediaPausedState(tab, {
+ shouldVideo1BePaused: true,
+ shouldVideo2BePaused: false,
+ });
+
+ info(`make video2 become audible, then it would be able to be controlled`);
+ await unmuteInaudibleMedia(tab);
+
+ info(`pressing 'pause' should affect video2 (audible`);
+ await generateMediaControlKeyEvent("pause");
+ await checkMediaPausedState(tab, {
+ shouldVideo1BePaused: true,
+ shouldVideo2BePaused: true,
+ });
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+/**
+ * The following are helper functions.
+ */
+async function playBothAudibleAndInaudibleMedia(tab) {
+ const playbackStateChangedPromise = waitUntilDisplayedPlaybackChanged();
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ const videos = content.document.getElementsByTagName("video");
+ let promises = [];
+ for (let video of videos) {
+ info(`play ${video.id} video`);
+ promises.push(video.play());
+ }
+ return Promise.all(promises);
+ });
+ await playbackStateChangedPromise;
+}
+
+function checkMediaPausedState(
+ tab,
+ { shouldVideo1BePaused, shouldVideo2BePaused }
+) {
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [shouldVideo1BePaused, shouldVideo2BePaused],
+ (shouldVideo1BePaused, shouldVideo2BePaused) => {
+ const video1 = content.document.getElementById("video1");
+ const video2 = content.document.getElementById("video2");
+ is(
+ video1.paused,
+ shouldVideo1BePaused,
+ "Correct paused state for video1"
+ );
+ is(
+ video2.paused,
+ shouldVideo2BePaused,
+ "Correct paused state for video2"
+ );
+ }
+ );
+}
+
+function unmuteInaudibleMedia(tab) {
+ const unmutePromise = SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ const video2 = content.document.getElementById("video2");
+ video2.muted = false;
+ });
+ // Inaudible media was not controllable, so it won't affect the controller's
+ // playback state. However, when it becomes audible, which means being able to
+ // be controlled by media controller, it would make the playback state chanege
+ // to `playing` because now we have an audible playinng media in the page.
+ return Promise.all([unmutePromise, waitUntilDisplayedPlaybackChanged()]);
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_default_action_handler.js b/dom/media/mediacontrol/tests/browser/browser_default_action_handler.js
new file mode 100644
index 0000000000..c33c08b0c2
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_default_action_handler.js
@@ -0,0 +1,422 @@
+const PAGE_URL =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_autoplay.html";
+const PAGE2_URL =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_main_frame_with_multiple_child_session_frames.html";
+const IFRAME_URL =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_iframe_media.html";
+const CORS_IFRAME_URL =
+ "https://example.org/browser/dom/media/mediacontrol/tests/browser/file_iframe_media.html";
+const CORS_IFRAME2_URL =
+ "https://test1.example.org/browser/dom/media/mediacontrol/tests/browser/file_iframe_media.html";
+const videoId = "video";
+
+/**
+ * This test is used to check the scenario when we should use the customized
+ * action handler and the the default action handler (play/pause/stop).
+ * If a frame (DOM Window, it could be main frame or an iframe) has active media
+ * session, then it should use the customized action handler it it has one.
+ * Otherwise, the default action handler should be used.
+ */
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.media.mediasession.enabled", true],
+ ["media.mediacontrol.testingevents.enabled", true],
+ ],
+ });
+});
+
+add_task(async function triggerDefaultActionHandler() {
+ // Default handler should be triggered no matter if media session exists or not.
+ const kCreateMediaSession = [true, false];
+ for (const shouldCreateSession of kCreateMediaSession) {
+ info(`open page and start media`);
+ const tab = await createLoadedTabWrapper(PAGE_URL);
+ await playMedia(tab, videoId);
+
+ if (shouldCreateSession) {
+ info(
+ `media has started, so created session should become active session`
+ );
+ await Promise.all([
+ waitUntilActiveMediaSessionChanged(),
+ createMediaSession(tab),
+ ]);
+ }
+
+ info(`test 'pause' action`);
+ await simulateMediaAction(tab, "pause");
+
+ info(`default action handler should pause media`);
+ await checkOrWaitUntilMediaPauses(tab, { videoId });
+
+ info(`test 'play' action`);
+ await simulateMediaAction(tab, "play");
+
+ info(`default action handler should resume media`);
+ await checkOrWaitUntilMediaPlays(tab, { videoId });
+
+ info(`test 'stop' action`);
+ await simulateMediaAction(tab, "stop");
+
+ info(`default action handler should pause media`);
+ await checkOrWaitUntilMediaPauses(tab, { videoId });
+
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ ok(
+ !controller.isActive,
+ `controller should be deactivated after receiving stop`
+ );
+
+ info(`remove tab`);
+ await tab.close();
+ }
+});
+
+add_task(async function triggerNonDefaultHandlerWhenSetCustomizedHandler() {
+ info(`open page and start media`);
+ const tab = await createLoadedTabWrapper(PAGE_URL);
+ await Promise.all([
+ new Promise(r => (tab.controller.onactivated = r)),
+ startMedia(tab, { videoId }),
+ ]);
+
+ const kActions = ["play", "pause", "stop"];
+ for (const action of kActions) {
+ info(`set action handler for '${action}'`);
+ await setActionHandler(tab, action);
+
+ info(`press '${action}' should trigger action handler (not a default one)`);
+ await simulateMediaAction(tab, action);
+ await waitUntilActionHandlerIsTriggered(tab, action);
+
+ info(`action handler doesn't pause media, media should keep playing`);
+ await checkOrWaitUntilMediaPlays(tab, { videoId });
+ }
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(
+ async function triggerDefaultHandlerToPausePlaybackOnInactiveSession() {
+ const kIframeUrls = [IFRAME_URL, CORS_IFRAME_URL];
+ for (const url of kIframeUrls) {
+ const kActions = ["play", "pause", "stop"];
+ for (const action of kActions) {
+ info(`open page and load iframe`);
+ const tab = await createLoadedTabWrapper(PAGE_URL);
+ const frameId = "iframe";
+ await loadIframe(tab, frameId, url);
+
+ info(`start media from iframe would make it become active session`);
+ await Promise.all([
+ new Promise(r => (tab.controller.onactivated = r)),
+ startMedia(tab, { frameId }),
+ ]);
+
+ info(`press '${action}' should trigger iframe's action handler`);
+ await setActionHandler(tab, action, frameId);
+ await simulateMediaAction(tab, action);
+ await waitUntilActionHandlerIsTriggered(tab, action, frameId);
+
+ info(`start media from main frame so iframe would become inactive`);
+ // When action is `play`, controller is already playing, because above
+ // code won't pause media. So we need to wait for the active session
+ // changed to ensure the following tests can be executed on the right
+ // browsing context.
+ let waitForControllerStatusChanged =
+ action == "play"
+ ? waitUntilActiveMediaSessionChanged()
+ : ensureControllerIsPlaying(tab.controller);
+ await Promise.all([
+ waitForControllerStatusChanged,
+ startMedia(tab, { videoId }),
+ ]);
+
+ if (action == "play") {
+ info(`pause media first in order to test 'play'`);
+ await pauseAllMedia(tab);
+
+ info(
+ `press '${action}' would trigger default andler on main frame because it doesn't set action handler`
+ );
+ await simulateMediaAction(tab, action);
+ await checkOrWaitUntilMediaPlays(tab, { videoId });
+
+ info(
+ `default handler should also be triggered on inactive iframe, which would resume media`
+ );
+ await checkOrWaitUntilMediaPlays(tab, { frameId });
+ } else {
+ info(
+ `press '${action}' would trigger default andler on main frame because it doesn't set action handler`
+ );
+ await simulateMediaAction(tab, action);
+ await checkOrWaitUntilMediaPauses(tab, { videoId });
+
+ info(
+ `default handler should also be triggered on inactive iframe, which would pause media`
+ );
+ await checkOrWaitUntilMediaPauses(tab, { frameId });
+ }
+
+ info(`remove tab`);
+ await tab.close();
+ }
+ }
+ }
+);
+
+add_task(async function onlyResumeActiveMediaSession() {
+ info(`open page and load iframes`);
+ const tab = await createLoadedTabWrapper(PAGE2_URL);
+ const frame1Id = "frame1";
+ const frame2Id = "frame2";
+ await loadIframe(tab, frame1Id, CORS_IFRAME_URL);
+ await loadIframe(tab, frame2Id, CORS_IFRAME2_URL);
+
+ info(`start media from iframe1 would make it become active session`);
+ await createMediaSession(tab, frame1Id);
+ await Promise.all([
+ waitUntilActiveMediaSessionChanged(),
+ startMedia(tab, { frameId: frame1Id }),
+ ]);
+
+ info(`start media from iframe2 would make it become active session`);
+ await createMediaSession(tab, frame2Id);
+ await Promise.all([
+ waitUntilActiveMediaSessionChanged(),
+ startMedia(tab, { frameId: frame2Id }),
+ ]);
+
+ info(`press 'pause' should pause both iframes`);
+ await simulateMediaAction(tab, "pause");
+ await checkOrWaitUntilMediaPauses(tab, { frameId: frame1Id });
+ await checkOrWaitUntilMediaPauses(tab, { frameId: frame2Id });
+
+ info(
+ `press 'play' should only resume iframe2 which has active media session`
+ );
+ await simulateMediaAction(tab, "play");
+ await checkOrWaitUntilMediaPauses(tab, { frameId: frame1Id });
+ await checkOrWaitUntilMediaPlays(tab, { frameId: frame2Id });
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+/**
+ * The following are helper functions.
+ */
+function startMedia(tab, { videoId, frameId }) {
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [videoId, frameId],
+ (videoId, frameId) => {
+ if (frameId) {
+ return content.messageHelper(
+ content.document.getElementById(frameId),
+ "play",
+ "played"
+ );
+ }
+ return content.document.getElementById(videoId).play();
+ }
+ );
+}
+
+function pauseAllMedia(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ await content.messageHelper(
+ content.document.getElementById("iframe"),
+ "pause",
+ "paused"
+ );
+ const videos = content.document.getElementsByTagName("video");
+ for (let video of videos) {
+ video.pause();
+ }
+ });
+}
+
+function createMediaSession(tab, frameId = null) {
+ info(`create media session`);
+ return SpecialPowers.spawn(tab.linkedBrowser, [frameId], async frameId => {
+ if (frameId) {
+ await content.messageHelper(
+ content.document.getElementById(frameId),
+ "create-media-session",
+ "created-media-session"
+ );
+ return;
+ }
+ // simply calling a media session would create an instance.
+ content.navigator.mediaSession;
+ });
+}
+
+function checkOrWaitUntilMediaPauses(tab, { videoId, frameId }) {
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [videoId, frameId],
+ (videoId, frameId) => {
+ if (frameId) {
+ return content.messageHelper(
+ content.document.getElementById(frameId),
+ "check-pause",
+ "checked-pause"
+ );
+ }
+ return new Promise(r => {
+ const video = content.document.getElementById(videoId);
+ if (video.paused) {
+ ok(true, `media stopped playing`);
+ r();
+ } else {
+ info(`wait until media stops playing`);
+ video.onpause = () => {
+ video.onpause = null;
+ ok(true, `media stopped playing`);
+ r();
+ };
+ }
+ });
+ }
+ );
+}
+
+function checkOrWaitUntilMediaPlays(tab, { videoId, frameId }) {
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [videoId, frameId],
+ (videoId, frameId) => {
+ if (frameId) {
+ return content.messageHelper(
+ content.document.getElementById(frameId),
+ "check-playing",
+ "checked-playing"
+ );
+ }
+ return new Promise(r => {
+ const video = content.document.getElementById(videoId);
+ if (!video.paused) {
+ ok(true, `media is playing`);
+ r();
+ } else {
+ info(`wait until media starts playing`);
+ video.onplay = () => {
+ video.onplay = null;
+ ok(true, `media starts playing`);
+ r();
+ };
+ }
+ });
+ }
+ );
+}
+
+function setActionHandler(tab, action, frameId = null) {
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [action, frameId],
+ async (action, frameId) => {
+ if (frameId) {
+ await content.messageHelper(
+ content.document.getElementById(frameId),
+ {
+ cmd: "setActionHandler",
+ action,
+ },
+ "setActionHandler-done"
+ );
+ return;
+ }
+ // Create this on the first function call
+ if (content.actionHandlerPromises === undefined) {
+ content.actionHandlerPromises = {};
+ }
+ content.actionHandlerPromises[action] = new Promise(r => {
+ content.navigator.mediaSession.setActionHandler(action, () => {
+ info(`receive ${action}`);
+ r();
+ });
+ });
+ }
+ );
+}
+
+async function waitUntilActionHandlerIsTriggered(tab, action, frameId = null) {
+ info(`wait until '${action}' action handler is triggered`);
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [action, frameId],
+ (action, frameId) => {
+ if (frameId) {
+ return content.messageHelper(
+ content.document.getElementById(frameId),
+ {
+ cmd: "checkActionHandler",
+ action,
+ },
+ "checkActionHandler-done"
+ );
+ }
+ const actionTriggerPromise = content.actionHandlerPromises[action];
+ ok(actionTriggerPromise, `Has created promise for ${action}`);
+ return actionTriggerPromise;
+ }
+ );
+}
+
+async function simulateMediaAction(tab, action) {
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ if (!controller.isActive) {
+ await new Promise(r => (controller.onactivated = r));
+ }
+ MediaControlService.generateMediaControlKey(action);
+}
+
+function loadIframe(tab, iframeId, url) {
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [iframeId, url],
+ async (iframeId, url) => {
+ const iframe = content.document.getElementById(iframeId);
+ info(`load iframe with url '${url}'`);
+ iframe.src = url;
+ await new Promise(r => (iframe.onload = r));
+ // create a helper to simplify communication process with iframe
+ content.messageHelper = (target, sentMessage, expectedResponse) => {
+ target.contentWindow.postMessage(sentMessage, "*");
+ return new Promise(r => {
+ content.onmessage = event => {
+ if (event.data == expectedResponse) {
+ ok(true, `Received response ${expectedResponse}`);
+ content.onmessage = null;
+ r();
+ }
+ };
+ });
+ };
+ }
+ );
+}
+
+function waitUntilActiveMediaSessionChanged() {
+ return BrowserUtils.promiseObserved("active-media-session-changed");
+}
+
+function ensureControllerIsPlaying(controller) {
+ return new Promise(r => {
+ if (controller.isPlaying) {
+ r();
+ return;
+ }
+ controller.onplaybackstatechange = () => {
+ if (controller.isPlaying) {
+ r();
+ }
+ };
+ });
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_media_control_audio_focus_within_a_page.js b/dom/media/mediacontrol/tests/browser/browser_media_control_audio_focus_within_a_page.js
new file mode 100644
index 0000000000..5db37059dd
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_media_control_audio_focus_within_a_page.js
@@ -0,0 +1,358 @@
+const mainPageURL =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_main_frame_with_multiple_child_session_frames.html";
+const frameURL =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_iframe_media.html";
+
+const frame1 = "frame1";
+const frame2 = "frame2";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.mediacontrol.testingevents.enabled", true],
+ ["dom.media.mediasession.enabled", true],
+ ],
+ });
+});
+
+/**
+ * This test is used to check the behaviors when we play media from different
+ * frames. When a page contains multiple frames, if those frames are using the
+ * media session and set the metadata, then we have to know which frame owns the
+ * audio focus that would be the last tab playing media. When the frame owns
+ * audio focus, it means its metadata would be displayed on the virtual control
+ * interface if it has a media session.
+ */
+add_task(async function testAudioFocusChangesAmongMultipleFrames() {
+ /**
+ * Play the media from the main frame, so it would own the audio focus and
+ * its metadata should be shown on the virtual control interface. As the main
+ * frame doesn't use media session, the current metadata would be the default
+ * metadata.
+ */
+ const tab = await createLoadedTabWrapper(mainPageURL);
+ await playAndWaitUntilMetadataChanged(tab);
+ await isGivenTabUsingDefaultMetadata(tab);
+
+ /**
+ * Play media for frame1, so the audio focus switches to frame1 because it's
+ * the last tab playing media and frame1's metadata should be displayed.
+ */
+ await loadPageForFrame(tab, frame1, frameURL);
+ let metadata = await setMetadataAndGetReturnResult(tab, frame1);
+ await playAndWaitUntilMetadataChanged(tab, frame1);
+ isCurrentMetadataEqualTo(metadata);
+
+ /**
+ * Play media for frame2, so the audio focus switches to frame2 because it's
+ * the last tab playing media and frame2's metadata should be displayed.
+ */
+ await loadPageForFrame(tab, frame2, frameURL);
+ metadata = await setMetadataAndGetReturnResult(tab, frame2);
+ await playAndWaitUntilMetadataChanged(tab, frame2);
+ isCurrentMetadataEqualTo(metadata);
+
+ /**
+ * Remove tab and end test.
+ */
+ await tab.close();
+});
+
+add_task(async function testAudioFocusChangesAfterPausingAudioFocusOwner() {
+ /**
+ * Play the media from the main frame, so it would own the audio focus and
+ * its metadata should be shown on the virtual control interface. As the main
+ * frame doesn't use media session, the current metadata would be the default
+ * metadata.
+ */
+ const tab = await createLoadedTabWrapper(mainPageURL);
+ await playAndWaitUntilMetadataChanged(tab);
+ await isGivenTabUsingDefaultMetadata(tab);
+
+ /**
+ * Play media for frame1, so the audio focus switches to frame1 because it's
+ * the last tab playing media and frame1's metadata should be displayed.
+ */
+ await loadPageForFrame(tab, frame1, frameURL);
+ let metadata = await setMetadataAndGetReturnResult(tab, frame1);
+ await playAndWaitUntilMetadataChanged(tab, frame1);
+ isCurrentMetadataEqualTo(metadata);
+
+ /**
+ * Pause media for frame1, so the audio focus switches back to the main frame
+ * which is still playing media.
+ */
+ await pauseAndWaitUntilMetadataChangedFrom(tab, frame1);
+ await isGivenTabUsingDefaultMetadata(tab);
+
+ /**
+ * Remove tab and end test.
+ */
+ await tab.close();
+});
+
+add_task(async function testAudioFocusUnchangesAfterPausingAudioFocusOwner() {
+ /**
+ * Play the media from the main frame, so it would own the audio focus and
+ * its metadata should be shown on the virtual control interface. As the main
+ * frame doesn't use media session, the current metadata would be the default
+ * metadata.
+ */
+ const tab = await createLoadedTabWrapper(mainPageURL);
+ await playAndWaitUntilMetadataChanged(tab);
+ await isGivenTabUsingDefaultMetadata(tab);
+
+ /**
+ * Play media for frame1, so the audio focus switches to frame1 because it's
+ * the last tab playing media and frame1's metadata should be displayed.
+ */
+ await loadPageForFrame(tab, frame1, frameURL);
+ let metadata = await setMetadataAndGetReturnResult(tab, frame1);
+ await playAndWaitUntilMetadataChanged(tab, frame1);
+ isCurrentMetadataEqualTo(metadata);
+
+ /**
+ * Pause main frame's media first. When pausing frame1's media, there are not
+ * other frames playing media, so frame1 still owns the audio focus and its
+ * metadata should be displayed.
+ */
+ await pauseMediaFrom(tab);
+ isCurrentMetadataEqualTo(metadata);
+
+ /**
+ * Remove tab and end test.
+ */
+ await tab.close();
+});
+
+add_task(
+ async function testSwitchAudioFocusToMainFrameAfterRemovingAudioFocusOwner() {
+ /**
+ * Play the media from the main frame, so it would own the audio focus and
+ * its metadata should be displayed on the virtual control interface. As the
+ * main frame doesn't use media session, the current metadata would be the
+ * default metadata.
+ */
+ const tab = await createLoadedTabWrapper(mainPageURL);
+ await playAndWaitUntilMetadataChanged(tab);
+ await isGivenTabUsingDefaultMetadata(tab);
+
+ /**
+ * Play media for frame1, so the audio focus switches to frame1 because it's
+ * the last tab playing media and frame1's metadata should be displayed.
+ */
+ await loadPageForFrame(tab, frame1, frameURL);
+ let metadata = await setMetadataAndGetReturnResult(tab, frame1);
+ await playAndWaitUntilMetadataChanged(tab, frame1);
+ isCurrentMetadataEqualTo(metadata);
+
+ /**
+ * Remove frame1, the audio focus would switch to the main frame which
+ * metadata should be displayed.
+ */
+ await Promise.all([
+ waitUntilDisplayedMetadataChanged(),
+ removeFrame(tab, frame1),
+ ]);
+ await isGivenTabUsingDefaultMetadata(tab);
+
+ /**
+ * Remove tab and end test.
+ */
+ await tab.close();
+ }
+);
+
+add_task(
+ async function testSwitchAudioFocusToIframeAfterRemovingAudioFocusOwner() {
+ /**
+ * Play media for frame1, so frame1 owns the audio focus and frame1's metadata
+ * should be displayed.
+ */
+ const tab = await createLoadedTabWrapper(mainPageURL);
+ await loadPageForFrame(tab, frame1, frameURL);
+ let metadataFrame1 = await setMetadataAndGetReturnResult(tab, frame1);
+ await playAndWaitUntilMetadataChanged(tab, frame1);
+ isCurrentMetadataEqualTo(metadataFrame1);
+
+ /**
+ * Play media for frame2, so the audio focus switches to frame2 because it's
+ * the last tab playing media and frame2's metadata should be displayed.
+ */
+ await loadPageForFrame(tab, frame2, frameURL);
+ let metadataFrame2 = await setMetadataAndGetReturnResult(tab, frame2);
+ await playAndWaitUntilMetadataChanged(tab, frame2);
+ isCurrentMetadataEqualTo(metadataFrame2);
+
+ /**
+ * Remove frame2, the audio focus would switch to frame1 which metadata should
+ * be displayed.
+ */
+ await Promise.all([
+ waitUntilDisplayedMetadataChanged(),
+ removeFrame(tab, frame2),
+ ]);
+ isCurrentMetadataEqualTo(metadataFrame1);
+
+ /**
+ * Remove tab and end test.
+ */
+ await tab.close();
+ }
+);
+
+add_task(async function testNoAudioFocusAfterRemovingAudioFocusOwner() {
+ /**
+ * Play the media from the main frame, so it would own the audio focus and
+ * its metadata should be shown on the virtual control interface. As the main
+ * frame doesn't use media session, the current metadata would be the default
+ * metadata.
+ */
+ const tab = await createLoadedTabWrapper(mainPageURL);
+ await playAndWaitUntilMetadataChanged(tab);
+ await isGivenTabUsingDefaultMetadata(tab);
+
+ /**
+ * Play media for frame1, so the audio focus switches to frame1 because it's
+ * the last tab playing media and frame1's metadata should be displayed.
+ */
+ await loadPageForFrame(tab, frame1, frameURL);
+ let metadata = await setMetadataAndGetReturnResult(tab, frame1);
+ await playAndWaitUntilMetadataChanged(tab, frame1);
+ isCurrentMetadataEqualTo(metadata);
+
+ /**
+ * Pause media in main frame and then remove frame1. As the frame which owns
+ * the audio focus is removed and no frame is still playing media, the current
+ * metadata would be the default metadata.
+ */
+ await pauseMediaFrom(tab);
+ await Promise.all([
+ waitUntilDisplayedMetadataChanged(),
+ removeFrame(tab, frame1),
+ ]);
+ await isGivenTabUsingDefaultMetadata(tab);
+
+ /**
+ * Remove tab and end test.
+ */
+ await tab.close();
+});
+
+/**
+ * The following are helper functions.
+ */
+function loadPageForFrame(tab, frameId, pageUrl) {
+ info(`start to load page for ${frameId}`);
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [frameId, pageUrl],
+ async (id, url) => {
+ const iframe = content.document.getElementById(id);
+ if (!iframe) {
+ ok(false, `can not get iframe '${id}'`);
+ }
+ iframe.src = url;
+ await new Promise(r => (iframe.onload = r));
+ // Set the document title that would be used as the value for properties
+ // in frame's medadata.
+ iframe.contentDocument.title = id;
+ }
+ );
+}
+
+function playMediaFrom(tab, frameId = undefined) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [frameId], id => {
+ if (id == undefined) {
+ info(`start to play media from main frame`);
+ const video = content.document.getElementById("video");
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ return video.play();
+ }
+
+ info(`start to play media from ${id}`);
+ const iframe = content.document.getElementById(id);
+ if (!iframe) {
+ ok(false, `can not get ${id}`);
+ }
+ iframe.contentWindow.postMessage("play", "*");
+ return new Promise(r => {
+ content.onmessage = event => {
+ is(event.data, "played", `media started playing in ${id}`);
+ r();
+ };
+ });
+ });
+}
+
+function playAndWaitUntilMetadataChanged(tab, frameId = undefined) {
+ const metadataChanged = frameId
+ ? new Promise(r => (tab.controller.onmetadatachange = r))
+ : waitUntilDisplayedMetadataChanged();
+ return Promise.all([metadataChanged, playMediaFrom(tab, frameId)]);
+}
+
+function pauseMediaFrom(tab, frameId = undefined) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [frameId], id => {
+ if (id == undefined) {
+ info(`start to pause media from in frame`);
+ const video = content.document.getElementById("video");
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ return video.pause();
+ }
+
+ info(`start to pause media in ${id}`);
+ const iframe = content.document.getElementById(id);
+ if (!iframe) {
+ ok(false, `can not get ${id}`);
+ }
+ iframe.contentWindow.postMessage("pause", "*");
+ return new Promise(r => {
+ content.onmessage = event => {
+ is(event.data, "paused", `media paused in ${id}`);
+ r();
+ };
+ });
+ });
+}
+
+function pauseAndWaitUntilMetadataChangedFrom(tab, frameId = undefined) {
+ const metadataChanged = waitUntilDisplayedMetadataChanged();
+ return Promise.all([metadataChanged, pauseMediaFrom(tab, frameId)]);
+}
+
+function setMetadataAndGetReturnResult(tab, frameId) {
+ info(`start to set metadata for ${frameId}`);
+ return SpecialPowers.spawn(tab.linkedBrowser, [frameId], id => {
+ const iframe = content.document.getElementById(id);
+ if (!iframe) {
+ ok(false, `can not get ${id}`);
+ }
+ iframe.contentWindow.postMessage("setMetadata", "*");
+ info(`wait until we get metadata for ${id}`);
+ return new Promise(r => {
+ content.onmessage = event => {
+ ok(
+ event.data.title && event.data.artist && event.data.album,
+ "correct return format"
+ );
+ r(event.data);
+ };
+ });
+ });
+}
+
+function removeFrame(tab, frameId) {
+ info(`remove ${frameId}`);
+ return SpecialPowers.spawn(tab.linkedBrowser, [frameId], id => {
+ const iframe = content.document.getElementById(id);
+ if (!iframe) {
+ ok(false, `can not get ${id}`);
+ }
+ content.document.body.removeChild(iframe);
+ });
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_media_control_before_media_starts.js b/dom/media/mediacontrol/tests/browser/browser_media_control_before_media_starts.js
new file mode 100644
index 0000000000..292c2f521f
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_media_control_before_media_starts.js
@@ -0,0 +1,205 @@
+// Import this in order to use `triggerPictureInPicture()`.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/components/pictureinpicture/tests/head.js",
+ this
+);
+
+const PAGE_NON_AUTOPLAY =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_autoplay.html";
+const IFRAME_URL =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_iframe_media.html";
+const testVideoId = "video";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.mediacontrol.testingevents.enabled", true],
+ ["full-screen-api.allow-trusted-requests-only", false],
+ ],
+ });
+});
+
+/**
+ * Usually we would only start controlling media after media starts, but if
+ * media has entered Picture-in-Picture mode or fullscreen, then we would allow
+ * users to control them directly without prior to starting media.
+ */
+add_task(async function testMediaEntersPIPMode() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`trigger PIP mode`);
+ const winPIP = await triggerPictureInPicture(tab.linkedBrowser, testVideoId);
+
+ info(`press 'play' and wait until media starts`);
+ await generateMediaControlKeyEvent("play");
+ await checkOrWaitUntilMediaStartedPlaying(tab, testVideoId);
+
+ info(`remove tab`);
+ await BrowserTestUtils.closeWindow(winPIP);
+ await tab.close();
+});
+
+add_task(async function testMutedMediaEntersPIPMode() {
+ info(`open media page and mute video`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+ await muteMedia(tab, testVideoId);
+
+ info(`trigger PIP mode`);
+ const winPIP = await triggerPictureInPicture(tab.linkedBrowser, testVideoId);
+
+ info(`press 'play' and wait until media starts`);
+ await generateMediaControlKeyEvent("play");
+ await checkOrWaitUntilMediaStartedPlaying(tab, testVideoId);
+
+ info(`remove tab`);
+ await BrowserTestUtils.closeWindow(winPIP);
+ await tab.close();
+});
+
+add_task(async function testMediaEntersFullScreen() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`make video fullscreen`);
+ await enableFullScreen(tab, testVideoId);
+
+ info(`press 'play' and wait until media starts`);
+ await generateMediaControlKeyEvent("play");
+ await checkOrWaitUntilMediaStartedPlaying(tab, testVideoId);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testMutedMediaEntersFullScreen() {
+ info(`open media page and mute video`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+ await muteMedia(tab, testVideoId);
+
+ info(`make video fullscreen`);
+ await enableFullScreen(tab, testVideoId);
+
+ info(`press 'play' and wait until media starts`);
+ await generateMediaControlKeyEvent("play");
+ await checkOrWaitUntilMediaStartedPlaying(tab, testVideoId);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testNonMediaEntersFullScreen() {
+ info(`open media page which won't have an activated controller`);
+ // As we won't activate controller in this test case, not need to
+ // check controller's status.
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY, {
+ needCheck: false,
+ });
+
+ info(`make non-media element fullscreen`);
+ const nonMediaElementId = "image";
+ await enableFullScreen(tab, nonMediaElementId);
+
+ info(`press 'play' which should not start media`);
+ // Use `generateMediaControlKey()` directly because `play` won't affect the
+ // controller's playback state (don't need to wait for the change).
+ MediaControlService.generateMediaControlKey("play");
+ await checkOrWaitUntilMediaStoppedPlaying(tab, testVideoId);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testMediaInIframeEntersFullScreen() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`make video in iframe fullscreen`);
+ await enableMediaFullScreenInIframe(tab, testVideoId);
+
+ info(`press 'play' and wait until media starts`);
+ await generateMediaControlKeyEvent("play");
+
+ info(`full screen media in inframe should be started`);
+ await waitUntilIframeMediaStartedPlaying(tab);
+
+ info(`media not in fullscreen should keep paused`);
+ await checkOrWaitUntilMediaStoppedPlaying(tab, testVideoId);
+
+ info(`remove iframe that contains fullscreen video`);
+ await removeIframeFromDocument(tab);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+/**
+ * The following are helper functions.
+ */
+function muteMedia(tab, videoId) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [videoId], videoId => {
+ content.document.getElementById(videoId).muted = true;
+ });
+}
+
+function enableFullScreen(tab, elementId) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [elementId], elementId => {
+ return new Promise(r => {
+ const element = content.document.getElementById(elementId);
+ element.requestFullscreen();
+ element.onfullscreenchange = () => {
+ element.onfullscreenchange = null;
+ element.onfullscreenerror = null;
+ r();
+ };
+ element.onfullscreenerror = () => {
+ // Retry until the element successfully enters fullscreen.
+ element.requestFullscreen();
+ };
+ });
+ });
+}
+
+function enableMediaFullScreenInIframe(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [IFRAME_URL], async url => {
+ info(`create iframe and wait until it finishes loading`);
+ const iframe = content.document.getElementById("iframe");
+ iframe.src = url;
+ await new Promise(r => (iframe.onload = r));
+
+ info(`trigger media in iframe entering into fullscreen`);
+ iframe.contentWindow.postMessage("fullscreen", "*");
+ info(`wait until media in frame enters fullscreen`);
+ return new Promise(r => {
+ content.onmessage = event => {
+ is(
+ event.data,
+ "entered-fullscreen",
+ `media in iframe entered fullscreen`
+ );
+ r();
+ };
+ });
+ });
+}
+
+function waitUntilIframeMediaStartedPlaying(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [IFRAME_URL], async url => {
+ info(`check if media in iframe starts playing`);
+ const iframe = content.document.getElementById("iframe");
+ iframe.contentWindow.postMessage("check-playing", "*");
+ return new Promise(r => {
+ content.onmessage = event => {
+ is(event.data, "checked-playing", `media in iframe is playing`);
+ r();
+ };
+ });
+ });
+}
+
+function removeIframeFromDocument(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ info(`remove iframe from document`);
+ content.document.getElementById("iframe").remove();
+ });
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_media_control_captured_audio.js b/dom/media/mediacontrol/tests/browser/browser_media_control_captured_audio.js
new file mode 100644
index 0000000000..0da8acd62b
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_media_control_captured_audio.js
@@ -0,0 +1,45 @@
+const PAGE_NON_AUTOPLAY_MEDIA =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_autoplay.html";
+
+const testVideoId = "video";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.mediacontrol.testingevents.enabled", true]],
+ });
+});
+
+/**
+ * When we capture audio from an media element to the web audio, if the media
+ * is audible, it should be controlled by media keys as well.
+ */
+add_task(async function testAudibleCapturedMedia() {
+ info(`open new non autoplay media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY_MEDIA);
+
+ info(`capture audio and start playing`);
+ await captureAudio(tab, testVideoId);
+ await playMedia(tab, testVideoId);
+
+ info(`pressing 'pause' key, captured media should be paused`);
+ await generateMediaControlKeyEvent("pause");
+ await checkOrWaitUntilMediaStoppedPlaying(tab, testVideoId);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+/**
+ * The following are helper functions.
+ */
+function captureAudio(tab, elementId) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [elementId], Id => {
+ const video = content.document.getElementById(Id);
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ const context = new content.AudioContext();
+ // Capture audio from the media element to a MediaElementAudioSourceNode.
+ context.createMediaElementSource(video);
+ });
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_media_control_keys_event.js b/dom/media/mediacontrol/tests/browser/browser_media_control_keys_event.js
new file mode 100644
index 0000000000..a8525e61c5
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_media_control_keys_event.js
@@ -0,0 +1,62 @@
+const PAGE =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_autoplay.html";
+const testVideoId = "video";
+
+/**
+ * This test is used to generate platform-independent media control keys event
+ * and see if media can be controlled correctly and current we only support
+ * `play`, `pause`, `playPause` and `stop` events.
+ */
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.mediacontrol.testingevents.enabled", true]],
+ });
+});
+
+add_task(async function testPlayPauseAndStop() {
+ info(`open page and start media`);
+ const tab = await createLoadedTabWrapper(PAGE);
+ await playMedia(tab, testVideoId);
+
+ info(`pressing 'pause' key`);
+ MediaControlService.generateMediaControlKey("pause");
+ await checkOrWaitUntilMediaStoppedPlaying(tab, testVideoId);
+
+ info(`pressing 'play' key`);
+ MediaControlService.generateMediaControlKey("play");
+ await checkOrWaitUntilMediaStartedPlaying(tab, testVideoId);
+
+ info(`pressing 'stop' key`);
+ MediaControlService.generateMediaControlKey("stop");
+ await checkOrWaitUntilMediaStoppedPlaying(tab, testVideoId);
+
+ info(`Has stopped controlling media, pressing 'play' won't resume it`);
+ MediaControlService.generateMediaControlKey("play");
+ await checkOrWaitUntilMediaStoppedPlaying(tab, testVideoId);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testPlayPause() {
+ info(`open page and start media`);
+ const tab = await createLoadedTabWrapper(PAGE);
+ await playMedia(tab, testVideoId);
+
+ info(`pressing 'playPause' key, media should stop`);
+ MediaControlService.generateMediaControlKey("playpause");
+ await Promise.all([
+ new Promise(r => (tab.controller.onplaybackstatechange = r)),
+ checkOrWaitUntilMediaStoppedPlaying(tab, testVideoId),
+ ]);
+
+ info(`pressing 'playPause' key, media should start`);
+ MediaControlService.generateMediaControlKey("playpause");
+ await Promise.all([
+ new Promise(r => (tab.controller.onplaybackstatechange = r)),
+ checkOrWaitUntilMediaStartedPlaying(tab, testVideoId),
+ ]);
+
+ info(`remove tab`);
+ await tab.close();
+});
diff --git a/dom/media/mediacontrol/tests/browser/browser_media_control_main_controller.js b/dom/media/mediacontrol/tests/browser/browser_media_control_main_controller.js
new file mode 100644
index 0000000000..9ddd3d7a18
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_media_control_main_controller.js
@@ -0,0 +1,341 @@
+// Import this in order to use `triggerPictureInPicture()`.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/components/pictureinpicture/tests/head.js",
+ this
+);
+
+const PAGE_NON_AUTOPLAY =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_autoplay.html";
+
+const testVideoId = "video";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.mediacontrol.testingevents.enabled", true],
+ ["dom.media.mediasession.enabled", true],
+ ],
+ });
+});
+
+/**
+ * This test is used to check in different situaition if we can determine the
+ * main controller correctly that is the controller which can receive media
+ * control keys and show its metadata on the virtual control interface.
+ *
+ * We will assign different metadata for each tab and know which tab is the main
+ * controller by checking main controller's metadata.
+ *
+ * We will always choose the last tab which plays media as the main controller,
+ * and maintain a list by the order of playing media. If the top element in the
+ * list has been removed, then we will use the last element in the list as the
+ * main controller.
+ *
+ * Eg. tab1 plays first, then tab2 plays, then tab3 plays, the list would be
+ * like [tab1, tab2, tab3] and the main controller would be tab3. If tab3 has
+ * been closed, then the list would become [tab1, tab2] and the tab2 would be
+ * the main controller.
+ */
+add_task(async function testDeterminingMainController() {
+ info(`open three different tabs`);
+ const tab0 = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+ const tab1 = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+ const tab2 = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ /**
+ * part1 : [] -> [tab0] -> [tab0, tab1] -> [tab0, tab1, tab2]
+ */
+ info(`# [] -> [tab0] -> [tab0, tab1] -> [tab0, tab1, tab2] #`);
+ info(`set different metadata for each tab`);
+ await setMediaMetadataForTabs([tab0, tab1, tab2]);
+
+ info(`start media for tab0, main controller should become tab0`);
+ await makeTabBecomeMainControllerAndWaitForMetadataChange(tab0);
+
+ info(`currrent metadata should be equal to tab0's metadata`);
+ await isCurrentMetadataEqualTo(tab0.metadata);
+
+ info(`start media for tab1, main controller should become tab1`);
+ await makeTabBecomeMainControllerAndWaitForMetadataChange(tab1);
+
+ info(`currrent metadata should be equal to tab1's metadata`);
+ await isCurrentMetadataEqualTo(tab1.metadata);
+
+ info(`start media for tab2, main controller should become tab2`);
+ await makeTabBecomeMainControllerAndWaitForMetadataChange(tab2);
+
+ info(`currrent metadata should be equal to tab2's metadata`);
+ await isCurrentMetadataEqualTo(tab2.metadata);
+
+ /**
+ * part2 : [tab0, tab1, tab2] -> [tab0, tab2, tab1] -> [tab2, tab1, tab0]
+ */
+ info(`# [tab0, tab1, tab2] -> [tab0, tab2, tab1] -> [tab2, tab1, tab0] #`);
+ info(`start media for tab1, main controller should become tab1`);
+ await makeTabBecomeMainController(tab1);
+
+ info(`currrent metadata should be equal to tab1's metadata`);
+ await isCurrentMetadataEqualTo(tab1.metadata);
+
+ info(`start media for tab0, main controller should become tab0`);
+ await makeTabBecomeMainController(tab0);
+
+ info(`currrent metadata should be equal to tab0's metadata`);
+ await isCurrentMetadataEqualTo(tab0.metadata);
+
+ /**
+ * part3 : [tab2, tab1, tab0] -> [tab2, tab1] -> [tab2] -> []
+ */
+ info(`# [tab2, tab1, tab0] -> [tab2, tab1] -> [tab2] -> [] #`);
+ info(`remove tab0 and wait until main controller changes`);
+ await Promise.all([waitUntilMainMediaControllerChanged(), tab0.close()]);
+
+ info(`currrent metadata should be equal to tab1's metadata`);
+ await isCurrentMetadataEqualTo(tab1.metadata);
+
+ info(`remove tab1 and wait until main controller changes`);
+ await Promise.all([waitUntilMainMediaControllerChanged(), tab1.close()]);
+
+ info(`currrent metadata should be equal to tab2's metadata`);
+ await isCurrentMetadataEqualTo(tab2.metadata);
+
+ info(`remove tab2 and wait until main controller changes`);
+ await Promise.all([waitUntilMainMediaControllerChanged(), tab2.close()]);
+ isCurrentMetadataEmpty();
+});
+
+add_task(async function testPIPControllerWontBeReplacedByNormalController() {
+ info(`open two different tabs`);
+ const tab0 = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+ const tab1 = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`set different metadata for each tab`);
+ await setMediaMetadataForTabs([tab0, tab1]);
+
+ info(`start media for tab0, main controller should become tab0`);
+ await makeTabBecomeMainControllerAndWaitForMetadataChange(tab0);
+
+ info(`currrent metadata should be equal to tab0's metadata`);
+ await isCurrentMetadataEqualTo(tab0.metadata);
+
+ info(`trigger Picture-in-Picture mode for tab0`);
+ const winPIP = await triggerPictureInPicture(tab0.linkedBrowser, testVideoId);
+
+ info(`start media for tab1, main controller should still be tab0`);
+ await playMediaAndWaitUntilRegisteringController(tab1, testVideoId);
+
+ info(`currrent metadata should be equal to tab0's metadata`);
+ await isCurrentMetadataEqualTo(tab0.metadata);
+
+ info(`remove tab0 and wait until main controller changes`);
+ await BrowserTestUtils.closeWindow(winPIP);
+ await Promise.all([waitUntilMainMediaControllerChanged(), tab0.close()]);
+
+ info(`currrent metadata should be equal to tab1's metadata`);
+ await isCurrentMetadataEqualTo(tab1.metadata);
+
+ info(`remove tab1 and wait until main controller changes`);
+ await Promise.all([waitUntilMainMediaControllerChanged(), tab1.close()]);
+ isCurrentMetadataEmpty();
+});
+
+add_task(
+ async function testFullscreenControllerWontBeReplacedByNormalController() {
+ info(`open two different tabs`);
+ const tab0 = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+ const tab1 = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`set different metadata for each tab`);
+ await setMediaMetadataForTabs([tab0, tab1]);
+
+ info(`start media for tab0, main controller should become tab0`);
+ await makeTabBecomeMainControllerAndWaitForMetadataChange(tab0);
+
+ info(`current metadata should be equal to tab0's metadata`);
+ await isCurrentMetadataEqualTo(tab0.metadata);
+
+ info(`video in tab0 enters fullscreen`);
+ await switchTabToForegroundAndEnableFullScreen(tab0, testVideoId);
+
+ info(
+ `normal controller won't become the main controller, ` +
+ `which is still fullscreen controller`
+ );
+ await playMediaAndWaitUntilRegisteringController(tab1, testVideoId);
+
+ info(`currrent metadata should be equal to tab0's metadata`);
+ await isCurrentMetadataEqualTo(tab0.metadata);
+
+ info(`remove tabs`);
+ await Promise.all([tab0.close(), tab1.close()]);
+ }
+);
+
+add_task(async function testFullscreenAndPIPControllers() {
+ info(`open three different tabs`);
+ const tab0 = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+ const tab1 = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+ const tab2 = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`set different metadata for each tab`);
+ await setMediaMetadataForTabs([tab0, tab1, tab2]);
+
+ /**
+ * Current controller list : [tab0 (fullscreen)]
+ */
+ info(`start media for tab0, main controller should become tab0`);
+ await makeTabBecomeMainControllerAndWaitForMetadataChange(tab0);
+
+ info(`currrent metadata should be equal to tab0's metadata`);
+ await isCurrentMetadataEqualTo(tab0.metadata);
+
+ info(`video in tab0 enters fullscreen`);
+ await switchTabToForegroundAndEnableFullScreen(tab0, testVideoId);
+
+ /**
+ * Current controller list : [tab1, tab0 (fullscreen)]
+ */
+ info(`start media for tab1, main controller should still be tab0`);
+ await playMediaAndWaitUntilRegisteringController(tab1, testVideoId);
+
+ info(`currrent metadata should be equal to tab0's metadata`);
+ await isCurrentMetadataEqualTo(tab0.metadata);
+
+ /**
+ * Current controller list : [tab0 (fullscreen), tab1 (PIP)]
+ */
+ info(`tab1 enters PIP so tab1 should become new main controller`);
+ const mainControllerChange = waitUntilMainMediaControllerChanged();
+ const winPIP = await triggerPictureInPicture(tab1.linkedBrowser, testVideoId);
+ await mainControllerChange;
+
+ info(`currrent metadata should be equal to tab1's metadata`);
+ await isCurrentMetadataEqualTo(tab1.metadata);
+
+ /**
+ * Current controller list : [tab2, tab0 (fullscreen), tab1 (PIP)]
+ */
+ info(`play video from tab2 which shouldn't affect main controller`);
+ await playMediaAndWaitUntilRegisteringController(tab2, testVideoId);
+
+ /**
+ * Current controller list : [tab2, tab0 (fullscreen)]
+ */
+ info(`remove tab1 and wait until main controller changes`);
+ await BrowserTestUtils.closeWindow(winPIP);
+ await Promise.all([waitUntilMainMediaControllerChanged(), tab1.close()]);
+
+ info(`currrent metadata should be equal to tab0's metadata`);
+ await isCurrentMetadataEqualTo(tab0.metadata);
+
+ /**
+ * Current controller list : [tab2]
+ */
+ info(`remove tab0 and wait until main controller changes`);
+ await Promise.all([waitUntilMainMediaControllerChanged(), tab0.close()]);
+
+ info(`currrent metadata should be equal to tab0's metadata`);
+ await isCurrentMetadataEqualTo(tab2.metadata);
+
+ /**
+ * Current controller list : []
+ */
+ info(`remove tab2 and wait until main controller changes`);
+ await Promise.all([waitUntilMainMediaControllerChanged(), tab2.close()]);
+ isCurrentMetadataEmpty();
+});
+
+/**
+ * The following are helper functions
+ */
+async function setMediaMetadataForTabs(tabs) {
+ for (let idx = 0; idx < tabs.length; idx++) {
+ const tabName = "tab" + idx;
+ info(`create metadata for ${tabName}`);
+ tabs[idx].metadata = {
+ title: tabName,
+ artist: tabName,
+ album: tabName,
+ artwork: [{ src: tabName, sizes: "128x128", type: "image/jpeg" }],
+ };
+ const spawn = SpecialPowers.spawn(
+ tabs[idx].linkedBrowser,
+ [tabs[idx].metadata],
+ data => {
+ content.navigator.mediaSession.metadata = new content.MediaMetadata(
+ data
+ );
+ }
+ );
+ // As those controller hasn't been activated yet, we can't listen to
+ // `mediacontroll.onmetadatachange`, which would only be notified after a
+ // controller becomes active.
+ await Promise.all([spawn, waitUntilControllerMetadataChanged()]);
+ }
+}
+
+function makeTabBecomeMainController(tab) {
+ const playPromise = SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [testVideoId],
+ async Id => {
+ const video = content.document.getElementById(Id);
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ // If media has been started, we would stop media first and then start it
+ // again, which would make controller's playback state change to `playing`
+ // again and result in updating new main controller.
+ if (!video.paused) {
+ video.pause();
+ info(`wait until media stops`);
+ await new Promise(r => (video.onpause = r));
+ }
+ info(`start media`);
+ return video.play();
+ }
+ );
+ return Promise.all([playPromise, waitUntilMainMediaControllerChanged()]);
+}
+
+function makeTabBecomeMainControllerAndWaitForMetadataChange(tab) {
+ return Promise.all([
+ new Promise(r => (tab.controller.onmetadatachange = r)),
+ makeTabBecomeMainController(tab),
+ ]);
+}
+
+function playMediaAndWaitUntilRegisteringController(tab, elementId) {
+ const playPromise = SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [elementId],
+ Id => {
+ const video = content.document.getElementById(Id);
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ return video.play();
+ }
+ );
+ return Promise.all([waitUntilMediaControllerAmountChanged(), playPromise]);
+}
+
+async function switchTabToForegroundAndEnableFullScreen(tab, elementId) {
+ // Fullscreen can only be allowed to enter from a focus tab.
+ await BrowserTestUtils.switchTab(gBrowser, tab.tabElement);
+ await SpecialPowers.spawn(tab.linkedBrowser, [elementId], elementId => {
+ return new Promise(r => {
+ const element = content.document.getElementById(elementId);
+ element.requestFullscreen();
+ element.onfullscreenchange = () => {
+ element.onfullscreenchange = null;
+ element.onfullscreenerror = null;
+ r();
+ };
+ element.onfullscreenerror = () => {
+ // Retry until the element successfully enters fullscreen.
+ element.requestFullscreen();
+ };
+ });
+ });
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_media_control_metadata.js b/dom/media/mediacontrol/tests/browser/browser_media_control_metadata.js
new file mode 100644
index 0000000000..75a0d80ac9
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_media_control_metadata.js
@@ -0,0 +1,416 @@
+const PAGE_NON_AUTOPLAY =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_autoplay.html";
+const PAGE_EMPTY_TITLE_URL =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_empty_title.html";
+
+const testVideoId = "video";
+const defaultFaviconName = "defaultFavicon.svg";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.mediacontrol.testingevents.enabled", true],
+ ["dom.media.mediasession.enabled", true],
+ ],
+ });
+});
+
+/**
+ * This test includes many different test cases of checking the current media
+ * metadata from the tab which is being controlled by the media control. Each
+ * `add_task(..)` is a different testing scenario.
+ *
+ * Media metadata is the information that can tell user about what title, artist,
+ * album and even art work the tab is currently playing to. The metadta is
+ * usually set from MediaSession API, but if the site doesn't use that API, we
+ * would also generate a default metadata instead.
+ *
+ * The current metadata would only be available after the page starts playing
+ * media at least once, if the page hasn't started any media before, then the
+ * current metadata is always empty.
+ *
+ * For following situations, we would create a default metadata which title is
+ * website's title and artwork is from our default favicon icon.
+ * (1) the page doesn't use MediaSession API
+ * (2) media session doesn't has metadata
+ * (3) media session has an empty metadata
+ *
+ * Otherwise, the current metadata would be media session's metadata from the
+ * tab which is currently controlled by the media control.
+ */
+add_task(async function testDefaultMetadataForPageWithoutMediaSession() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`should use default metadata because of lacking of media session`);
+ await isGivenTabUsingDefaultMetadata(tab);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(
+ async function testDefaultMetadataForEmptyTitlePageWithoutMediaSession() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_EMPTY_TITLE_URL);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`should use default metadata because of lacking of media session`);
+ await isGivenTabUsingDefaultMetadata(tab);
+
+ info(`remove tab`);
+ await tab.close();
+ }
+);
+
+add_task(async function testDefaultMetadataForPageUsingEmptyMetadata() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`create empty media metadata`);
+ await setMediaMetadata(tab, {
+ title: "",
+ artist: "",
+ album: "",
+ artwork: [],
+ });
+
+ info(`should use default metadata because of empty media metadata`);
+ await isGivenTabUsingDefaultMetadata(tab);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testDefaultMetadataForPageUsingNullMetadata() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`create empty media metadata`);
+ await setNullMediaMetadata(tab);
+
+ info(`should use default metadata because of lacking of media metadata`);
+ await isGivenTabUsingDefaultMetadata(tab);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testMetadataWithEmptyTitleAndArtwork() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`create media metadata with empty title and artwork`);
+ await setMediaMetadata(tab, {
+ title: "",
+ artist: "foo",
+ album: "bar",
+ artwork: [],
+ });
+
+ info(`should use default metadata because of empty title and artwork`);
+ await isGivenTabUsingDefaultMetadata(tab);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testMetadataWithoutTitleAndArtwork() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`create media metadata with empty title and artwork`);
+ await setMediaMetadata(tab, {
+ artist: "foo",
+ album: "bar",
+ });
+
+ info(`should use default metadata because of lacking of title and artwork`);
+ await isGivenTabUsingDefaultMetadata(tab);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testMetadataInPrivateBrowsing() {
+ info(`create a private window`);
+ const inputWindow = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY, { inputWindow });
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`set metadata`);
+ let metadata = {
+ title: "foo",
+ artist: "bar",
+ album: "foo",
+ artwork: [{ src: "bar.jpg", sizes: "128x128", type: "image/jpeg" }],
+ };
+ await setMediaMetadata(tab, metadata);
+
+ info(`should use default metadata because of in private browsing mode`);
+ await isGivenTabUsingDefaultMetadata(tab, { isPrivateBrowsing: true });
+
+ info(`remove tab`);
+ await tab.close();
+
+ info(`close private window`);
+ await BrowserTestUtils.closeWindow(inputWindow);
+});
+
+add_task(async function testSetMetadataFromMediaSessionAPI() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`set metadata`);
+ let metadata = {
+ title: "foo",
+ artist: "bar",
+ album: "foo",
+ artwork: [{ src: "bar.jpg", sizes: "128x128", type: "image/jpeg" }],
+ };
+ await setMediaMetadata(tab, metadata);
+
+ info(`check if current active metadata is equal to what we've set before`);
+ await isCurrentMetadataEqualTo(metadata);
+
+ info(`set metadata again to see if current metadata would change`);
+ metadata = {
+ title: "foo2",
+ artist: "bar2",
+ album: "foo2",
+ artwork: [{ src: "bar2.jpg", sizes: "129x129", type: "image/jpeg" }],
+ };
+ await setMediaMetadata(tab, metadata);
+
+ info(`check if current active metadata is equal to what we've set before`);
+ await isCurrentMetadataEqualTo(metadata);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testSetMetadataBeforeMediaStarts() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY, {
+ needCheck: false,
+ });
+
+ info(`set metadata`);
+ let metadata = {
+ title: "foo",
+ artist: "bar",
+ album: "foo",
+ artwork: [{ src: "bar.jpg", sizes: "128x128", type: "image/jpeg" }],
+ };
+ await setMediaMetadata(tab, metadata, { notExpectChange: true });
+
+ info(`current media metadata should be empty before media starts`);
+ isCurrentMetadataEmpty();
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testSetMetadataAfterMediaPaused() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media in order to let this tab be controlled`);
+ await playMedia(tab, testVideoId);
+
+ info(`pause media`);
+ await pauseMedia(tab, testVideoId);
+
+ info(`set metadata after media is paused`);
+ let metadata = {
+ title: "foo",
+ artist: "bar",
+ album: "foo",
+ artwork: [{ src: "bar.jpg", sizes: "128x128", type: "image/jpeg" }],
+ };
+ await setMediaMetadata(tab, metadata);
+
+ info(`check if current active metadata is equal to what we've set before`);
+ await isCurrentMetadataEqualTo(metadata);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testSetMetadataAmongMultipleTabs() {
+ info(`open media page in tab1`);
+ const tab1 = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media in tab1`);
+ await playMedia(tab1, testVideoId);
+
+ info(`set metadata for tab1`);
+ let metadata = {
+ title: "foo",
+ artist: "bar",
+ album: "foo",
+ artwork: [{ src: "bar.jpg", sizes: "128x128", type: "image/jpeg" }],
+ };
+ await setMediaMetadata(tab1, metadata);
+
+ info(`check if current active metadata is equal to what we've set before`);
+ await isCurrentMetadataEqualTo(metadata);
+
+ info(`open another page in tab2`);
+ const tab2 = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media in tab2`);
+ await playMedia(tab2, testVideoId);
+
+ info(`set metadata for tab2`);
+ metadata = {
+ title: "foo2",
+ artist: "bar2",
+ album: "foo2",
+ artwork: [{ src: "bar2.jpg", sizes: "129x129", type: "image/jpeg" }],
+ };
+ await setMediaMetadata(tab2, metadata);
+
+ info(`current active metadata should become metadata from tab2`);
+ await isCurrentMetadataEqualTo(metadata);
+
+ info(
+ `update metadata for tab1, which should not affect current metadata ` +
+ `because media session in tab2 is the one we're controlling right now`
+ );
+ await setMediaMetadata(tab1, {
+ title: "foo3",
+ artist: "bar3",
+ album: "foo3",
+ artwork: [{ src: "bar3.jpg", sizes: "130x130", type: "image/jpeg" }],
+ });
+
+ info(`current active metadata should still be metadata from tab2`);
+ await isCurrentMetadataEqualTo(metadata);
+
+ info(`remove tabs`);
+ await Promise.all([tab1.close(), tab2.close()]);
+});
+
+add_task(async function testMetadataAfterTabNavigation() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`set metadata`);
+ let metadata = {
+ title: "foo",
+ artist: "bar",
+ album: "foo",
+ artwork: [{ src: "bar.jpg", sizes: "128x128", type: "image/jpeg" }],
+ };
+ await setMediaMetadata(tab, metadata);
+
+ info(`check if current active metadata is equal to what we've set before`);
+ await isCurrentMetadataEqualTo(metadata);
+
+ info(`navigate tab to blank page`);
+ await Promise.all([
+ new Promise(r => (tab.controller.ondeactivated = r)),
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, "about:blank"),
+ ]);
+
+ info(`current media metadata should be reset`);
+ isCurrentMetadataEmpty();
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testUpdateDefaultMetadataWhenPageTitleChanges() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`should use default metadata because of lacking of media session`);
+ await isGivenTabUsingDefaultMetadata(tab);
+
+ info(`default metadata should be updated after page title changes`);
+ await changePageTitle(tab, { shouldAffectMetadata: true });
+ await isGivenTabUsingDefaultMetadata(tab);
+
+ info(`after setting metadata, title change won't affect current metadata`);
+ const metadata = {
+ title: "foo",
+ artist: "bar",
+ album: "foo",
+ artwork: [{ src: "bar.jpg", sizes: "128x128", type: "image/jpeg" }],
+ };
+ await setMediaMetadata(tab, metadata);
+ await changePageTitle(tab, { shouldAffectMetadata: false });
+ await isCurrentMetadataEqualTo(metadata);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+/**
+ * The following are helper functions.
+ */
+function setMediaMetadata(tab, metadata, { notExpectChange } = {}) {
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ const metadatachangePromise = notExpectChange
+ ? Promise.resolve()
+ : new Promise(r => (controller.onmetadatachange = r));
+ return Promise.all([
+ metadatachangePromise,
+ SpecialPowers.spawn(tab.linkedBrowser, [metadata], data => {
+ content.navigator.mediaSession.metadata = new content.MediaMetadata(data);
+ }),
+ ]);
+}
+
+function setNullMediaMetadata(tab) {
+ const promise = SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ content.navigator.mediaSession.metadata = null;
+ });
+ return Promise.all([promise, waitUntilControllerMetadataChanged()]);
+}
+
+async function changePageTitle(tab, { shouldAffectMetadata } = {}) {
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ const shouldWaitMetadataChangePromise = shouldAffectMetadata
+ ? new Promise(r => (controller.onmetadatachange = r))
+ : Promise.resolve();
+ await Promise.all([
+ shouldWaitMetadataChangePromise,
+ SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ content.document.title = "new title";
+ }),
+ ]);
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_media_control_non_eligible_media.js b/dom/media/mediacontrol/tests/browser/browser_media_control_non_eligible_media.js
new file mode 100644
index 0000000000..ab18eab634
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_media_control_non_eligible_media.js
@@ -0,0 +1,204 @@
+const PAGE_NON_ELIGIBLE_MEDIA =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_eligible_media.html";
+
+// Import this in order to use `triggerPictureInPicture()`.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/components/pictureinpicture/tests/head.js",
+ this
+);
+
+// Bug 1673509 - This test requests a lot of fullscreen for media elements,
+// which sometime Gecko would take longer time to fulfill.
+requestLongerTimeout(2);
+
+// This array contains the elements' id in `file_non_eligible_media.html`.
+const gNonEligibleElementIds = [
+ "muted",
+ "volume-0",
+ "silent-audio-track",
+ "no-audio-track",
+ "short-duration",
+ "inaudible-captured-media",
+];
+
+/**
+ * This test is used to test couples of things about what kinds of media is
+ * eligible for being controlled by media control keys.
+ * (1) If media is inaudible all the time, then we would not control it.
+ * (2) If media starts inaudibly, we would not try to control it. But once it
+ * becomes audible later, we would keep controlling it until it's destroyed.
+ * (3) If media's duration is too short (<3s), then we would not control it.
+ */
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.mediacontrol.testingevents.enabled", true]],
+ });
+});
+
+add_task(
+ async function testNonAudibleMediaCantActivateControllerButAudibleMediaCan() {
+ for (const elementId of gNonEligibleElementIds) {
+ info(`open new tab with non eligible media elements`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_ELIGIBLE_MEDIA, {
+ needCheck: couldElementBecomeEligible(elementId),
+ });
+
+ info(`although media is playing but it won't activate controller`);
+ await Promise.all([
+ startNonEligibleMedia(tab, elementId),
+ checkIfMediaIsStillPlaying(tab, elementId),
+ ]);
+ ok(!tab.controller.isActive, "controller is still inactive");
+
+ if (couldElementBecomeEligible(elementId)) {
+ info(`make element ${elementId} audible would activate controller`);
+ await Promise.all([
+ makeElementEligible(tab, elementId),
+ checkOrWaitUntilControllerBecomeActive(tab),
+ ]);
+ }
+
+ info(`remove tab`);
+ await tab.close();
+ }
+ }
+);
+
+/**
+ * Normally those media are not able to being controlled, however, once they
+ * enter fullsceen or Picture-in-Picture mode, then they can be controlled.
+ */
+add_task(async function testNonEligibleMediaEnterFullscreen() {
+ info(`open new tab with non eligible media elements`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_ELIGIBLE_MEDIA);
+
+ for (const elementId of gNonEligibleElementIds) {
+ await startNonEligibleMedia(tab, elementId);
+
+ info(`entering fullscreen should activate the media controller`);
+ await enterFullScreen(tab, elementId);
+ await checkOrWaitUntilControllerBecomeActive(tab);
+ ok(true, `fullscreen ${elementId} media is able to being controlled`);
+
+ info(`leave fullscreen`);
+ await leaveFullScreen(tab);
+ }
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testNonEligibleMediaEnterPIPMode() {
+ info(`open new tab with non eligible media elements`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_ELIGIBLE_MEDIA);
+
+ for (const elementId of gNonEligibleElementIds) {
+ await startNonEligibleMedia(tab, elementId);
+
+ info(`media entering PIP mode should activate the media controller`);
+ const winPIP = await triggerPictureInPicture(tab.linkedBrowser, elementId);
+ await checkOrWaitUntilControllerBecomeActive(tab);
+ ok(true, `PIP ${elementId} media is able to being controlled`);
+
+ info(`stop PIP mode`);
+ await BrowserTestUtils.closeWindow(winPIP);
+ }
+ info(`remove tab`);
+ await tab.close();
+});
+
+/**
+ * The following are helper functions.
+ */
+function startNonEligibleMedia(tab, elementId) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [elementId], Id => {
+ const video = content.document.getElementById(Id);
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ if (Id == "volume-0") {
+ video.volume = 0.0;
+ }
+ if (Id == "inaudible-captured-media") {
+ const context = new content.AudioContext();
+ context.createMediaElementSource(video);
+ }
+ info(`start non eligible media ${Id}`);
+ return video.play();
+ });
+}
+
+function checkIfMediaIsStillPlaying(tab, elementId) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [elementId], Id => {
+ const video = content.document.getElementById(Id);
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ return new Promise(r => {
+ // In order to test "media isn't affected by media control", we would not
+ // only check `mPaused`, we would also oberve "timeupdate" event multiple
+ // times to ensure that video is still playing continually.
+ let timeUpdateCount = 0;
+ ok(!video.paused);
+ video.ontimeupdate = () => {
+ if (++timeUpdateCount == 3) {
+ video.ontimeupdate = null;
+ r();
+ }
+ };
+ });
+ });
+}
+
+function couldElementBecomeEligible(elementId) {
+ return elementId == "muted" || elementId == "volume-0";
+}
+
+function makeElementEligible(tab, elementId) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [elementId], Id => {
+ const video = content.document.getElementById(Id);
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ // to turn inaudible media become audible in order to be controlled.
+ video.volume = 1.0;
+ video.muted = false;
+ });
+}
+
+function waitUntilMediaPaused(tab, elementId) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [elementId], Id => {
+ const video = content.document.getElementById(Id);
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ if (video.paused) {
+ ok(true, "media has been paused");
+ return Promise.resolve();
+ }
+ return new Promise(r => (video.onpaused = r));
+ });
+}
+
+function enterFullScreen(tab, elementId) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [elementId], Id => {
+ return new Promise(r => {
+ const element = content.document.getElementById(Id);
+ element.requestFullscreen();
+ element.onfullscreenchange = () => {
+ element.onfullscreenchange = null;
+ element.onfullscreenerror = null;
+ r();
+ };
+ element.onfullscreenerror = () => {
+ // Retry until the element successfully enters fullscreen.
+ element.requestFullscreen();
+ };
+ });
+ });
+}
+
+function leaveFullScreen(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ return content.document.exitFullscreen();
+ });
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_media_control_playback_state.js b/dom/media/mediacontrol/tests/browser/browser_media_control_playback_state.js
new file mode 100644
index 0000000000..4d30566e0a
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_media_control_playback_state.js
@@ -0,0 +1,116 @@
+const PAGE_NON_AUTOPLAY =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_autoplay.html";
+
+const testVideoId = "video";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.mediacontrol.testingevents.enabled", true],
+ ["dom.media.mediasession.enabled", true],
+ ],
+ });
+});
+
+/**
+ * This test is used to check the actual playback state [1] of the main media
+ * controller. The declared playback state is the playback state from the active
+ * media session, and the guessed playback state is determined by the media's
+ * playback state. Both the declared playback and the guessed playback state
+ * would be used to decide the final result of the actual playback state.
+ *
+ * [1] https://w3c.github.io/mediasession/#actual-playback-state
+ */
+add_task(async function testDefaultPlaybackStateBeforeAnyMediaStart() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY, {
+ needCheck: false,
+ });
+
+ info(`before media starts, playback state should be 'none'`);
+ await isActualPlaybackStateEqualTo(tab, "none");
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testGuessedPlaybackState() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(
+ `Now declared='none', guessed='playing', so actual playback state should be 'playing'`
+ );
+ await setGuessedPlaybackState(tab, "playing");
+ await isActualPlaybackStateEqualTo(tab, "playing");
+
+ info(
+ `Now declared='none', guessed='paused', so actual playback state should be 'paused'`
+ );
+ await setGuessedPlaybackState(tab, "paused");
+ await isActualPlaybackStateEqualTo(tab, "paused");
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testBothGuessedAndDeclaredPlaybackState() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(
+ `Now declared='paused', guessed='playing', so actual playback state should be 'playing'`
+ );
+ await setDeclaredPlaybackState(tab, "paused");
+ await setGuessedPlaybackState(tab, "playing");
+ await isActualPlaybackStateEqualTo(tab, "playing");
+
+ info(
+ `Now declared='paused', guessed='paused', so actual playback state should be 'paused'`
+ );
+ await setGuessedPlaybackState(tab, "paused");
+ await isActualPlaybackStateEqualTo(tab, "paused");
+
+ info(
+ `Now declared='playing', guessed='paused', so actual playback state should be 'playing'`
+ );
+ await setDeclaredPlaybackState(tab, "playing");
+ await isActualPlaybackStateEqualTo(tab, "playing");
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+/**
+ * The following are helper functions.
+ */
+function setGuessedPlaybackState(tab, state) {
+ if (state == "playing") {
+ return playMedia(tab, testVideoId);
+ } else if (state == "paused") {
+ return pauseMedia(tab, testVideoId);
+ }
+ // We won't set the state `stopped`, which would only happen if no any media
+ // has ever been started in the page.
+ ok(false, `should only set 'playing' or 'paused' state`);
+ return Promise.resolve();
+}
+
+async function isActualPlaybackStateEqualTo(tab, expectedState) {
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ if (controller.playbackState != expectedState) {
+ await new Promise(r => (controller.onplaybackstatechange = r));
+ }
+ is(
+ controller.playbackState,
+ expectedState,
+ `current state '${controller.playbackState}' is equal to '${expectedState}'`
+ );
+}
+
+function setDeclaredPlaybackState(tab, state) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [state], playbackState => {
+ info(`set declared playback state to '${playbackState}'`);
+ content.navigator.mediaSession.playbackState = playbackState;
+ });
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_media_control_position_state.js b/dom/media/mediacontrol/tests/browser/browser_media_control_position_state.js
new file mode 100644
index 0000000000..f32ce26063
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_media_control_position_state.js
@@ -0,0 +1,150 @@
+const PAGE_URL =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_autoplay.html";
+const IFRAME_URL =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_iframe_media.html";
+
+const testVideoId = "video";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.mediacontrol.testingevents.enabled", true],
+ ["dom.media.mediasession.enabled", true],
+ ],
+ });
+});
+
+/**
+ * This test is used to check if we can receive correct position state change,
+ * when we set the position state to the media session.
+ */
+add_task(async function testSetPositionState() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_URL);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`set duration only`);
+ await setPositionState(tab, {
+ duration: 60,
+ });
+
+ info(`set duration and playback rate`);
+ await setPositionState(tab, {
+ duration: 50,
+ playbackRate: 2.0,
+ });
+
+ info(`set duration, playback rate and position`);
+ await setPositionState(tab, {
+ duration: 40,
+ playbackRate: 3.0,
+ position: 10,
+ });
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testSetPositionStateFromInactiveMediaSession() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_URL);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(
+ `add an event listener to measure how many times the position state changes`
+ );
+ let positionChangedNum = 0;
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ controller.onpositionstatechange = () => positionChangedNum++;
+
+ info(`set position state on the main page which has an active media session`);
+ await setPositionState(tab, {
+ duration: 60,
+ });
+
+ info(`set position state on the iframe which has an inactive media session`);
+ await setPositionStateOnInactiveMediaSession(tab);
+
+ info(`set position state on the main page again`);
+ await setPositionState(tab, {
+ duration: 60,
+ });
+ is(
+ positionChangedNum,
+ 2,
+ `We should only receive two times of position change, because ` +
+ `the second one which performs on inactive media session is effectless`
+ );
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+/**
+ * The following are helper functions.
+ */
+async function setPositionState(tab, positionState) {
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ const positionStateChanged = new Promise(r => {
+ controller.addEventListener(
+ "positionstatechange",
+ event => {
+ const { duration, playbackRate, position } = positionState;
+ // duration is mandatory.
+ is(
+ event.duration,
+ duration,
+ `expected duration ${event.duration} is equal to ${duration}`
+ );
+
+ // Playback rate is optional, if it's not present, default should be 1.0
+ if (playbackRate) {
+ is(
+ event.playbackRate,
+ playbackRate,
+ `expected playbackRate ${event.playbackRate} is equal to ${playbackRate}`
+ );
+ } else {
+ is(event.playbackRate, 1.0, `expected default playbackRate is 1.0`);
+ }
+
+ // Position state is optional, if it's not present, default should be 0.0
+ if (position) {
+ is(
+ event.position,
+ position,
+ `expected position ${event.position} is equal to ${position}`
+ );
+ } else {
+ is(event.position, 0.0, `expected default position is 0.0`);
+ }
+ r();
+ },
+ { once: true }
+ );
+ });
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [positionState],
+ positionState => {
+ content.navigator.mediaSession.setPositionState(positionState);
+ }
+ );
+ await positionStateChanged;
+}
+
+async function setPositionStateOnInactiveMediaSession(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [IFRAME_URL], async url => {
+ info(`create iframe and wait until it finishes loading`);
+ const iframe = content.document.getElementById("iframe");
+ iframe.src = url;
+ await new Promise(r => (iframe.onload = r));
+
+ info(`trigger media in iframe entering into fullscreen`);
+ iframe.contentWindow.postMessage("setPositionState", "*");
+ });
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_media_control_seekto.js b/dom/media/mediacontrol/tests/browser/browser_media_control_seekto.js
new file mode 100644
index 0000000000..70b75841b6
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_media_control_seekto.js
@@ -0,0 +1,89 @@
+const PAGE_URL =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_autoplay.html";
+
+const testVideoId = "video";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.mediacontrol.testingevents.enabled", true],
+ ["dom.media.mediasession.enabled", true],
+ ],
+ });
+});
+
+/**
+ * This test is used to check if the `seekto` action can be sent when calling
+ * media controller's `seekTo()`. In addition, the seeking related properties
+ * which would be sent to the action handler should also be correct as what we
+ * set in `seekTo()`.
+ */
+add_task(async function testSetPositionState() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_URL);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ const seektime = 0;
+ info(`seek to ${seektime} seconds.`);
+ await PerformSeekTo(tab, {
+ seekTime: seektime,
+ });
+
+ info(`seek to ${seektime} seconds and set fastseek to boolean`);
+ await PerformSeekTo(tab, {
+ seekTime: seektime,
+ fastSeek: true,
+ });
+
+ info(`seek to ${seektime} seconds and set fastseek to false`);
+ await PerformSeekTo(tab, {
+ seekTime: seektime,
+ fastSeek: false,
+ });
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+/**
+ * The following is helper function.
+ */
+async function PerformSeekTo(tab, seekDetails) {
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [seekDetails, testVideoId],
+ (seekDetails, Id) => {
+ const { seekTime, fastSeek } = seekDetails;
+ content.navigator.mediaSession.setActionHandler("seekto", details => {
+ ok(details.seekTime != undefined, "Seektime must be presented");
+ is(seekTime, details.seekTime, "Get correct seektime");
+ if (fastSeek) {
+ is(fastSeek, details.fastSeek, "Get correct fastSeek");
+ } else {
+ ok(
+ details.fastSeek === undefined,
+ "Details should not contain fastSeek"
+ );
+ }
+ // We use `onseek` as a hint to know if the `seekto` has been received
+ // or not. The reason we don't return a resolved promise instead is
+ // because if we do so, it can't guarantees that the `seekto` action
+ // handler has been set before calling `mediaController.seekTo`.
+ content.document.getElementById(Id).currentTime = seekTime;
+ });
+ }
+ );
+ const seekPromise = SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [testVideoId],
+ Id => {
+ const video = content.document.getElementById(Id);
+ return new Promise(r => (video.onseeking = r()));
+ }
+ );
+ const { seekTime, fastSeek } = seekDetails;
+ tab.linkedBrowser.browsingContext.mediaController.seekTo(seekTime, fastSeek);
+ await seekPromise;
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_media_control_stop_timer.js b/dom/media/mediacontrol/tests/browser/browser_media_control_stop_timer.js
new file mode 100644
index 0000000000..34fc10badd
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_media_control_stop_timer.js
@@ -0,0 +1,79 @@
+// Import this in order to use `triggerPictureInPicture()`.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/components/pictureinpicture/tests/head.js",
+ this
+);
+
+const PAGE_NON_AUTOPLAY =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_autoplay.html";
+
+const testVideoId = "video";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.mediacontrol.testingevents.enabled", true],
+ ["media.mediacontrol.stopcontrol.timer", true],
+ ["media.mediacontrol.stopcontrol.timer.ms", 0],
+ ],
+ });
+});
+
+/**
+ * This test is used to check the stop timer for media element, which would stop
+ * media control for the specific element when the element has been paused over
+ * certain length of time. (That is controlled by the value of the pref
+ * `media.mediacontrol.stopcontrol.timer.ms`) In this test, we set the pref to 0
+ * which means the stop timer would be triggered after the media is paused.
+ * However, if the media is being used in PIP mode, we won't start the stop
+ * timer for it.
+ */
+add_task(async function testStopMediaControlAfterPausingMedia() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`pause media and the stop timer would stop media control`);
+ await pauseMediaAndMediaControlShouldBeStopped(tab, testVideoId);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testNotToStopMediaControlForPIPVideo() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`trigger PIP mode`);
+ const winPIP = await triggerPictureInPicture(tab.linkedBrowser, testVideoId);
+
+ info(`pause media and the stop timer would not stop media control`);
+ await pauseMedia(tab, testVideoId);
+
+ info(`pressing 'play' key should start PIP video again`);
+ await generateMediaControlKeyEvent("play");
+ await checkOrWaitUntilMediaStartedPlaying(tab, testVideoId);
+
+ info(`remove tab`);
+ await BrowserTestUtils.closeWindow(winPIP);
+ await tab.close();
+});
+
+/**
+ * The following is helper function.
+ */
+function pauseMediaAndMediaControlShouldBeStopped(tab, elementId) {
+ // After pausing media, the stop timer would be triggered and stop the media
+ // control.
+ return Promise.all([
+ new Promise(r => (tab.controller.ondeactivated = r)),
+ SpecialPowers.spawn(tab.linkedBrowser, [elementId], Id => {
+ content.document.getElementById(Id).pause();
+ }),
+ ]);
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_media_control_supported_keys.js b/dom/media/mediacontrol/tests/browser/browser_media_control_supported_keys.js
new file mode 100644
index 0000000000..2312e90c88
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_media_control_supported_keys.js
@@ -0,0 +1,130 @@
+const PAGE_NON_AUTOPLAY =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_autoplay.html";
+
+const testVideoId = "video";
+const sDefaultSupportedKeys = ["focus", "play", "pause", "playpause", "stop"];
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.mediacontrol.testingevents.enabled", true],
+ ["dom.media.mediasession.enabled", true],
+ ],
+ });
+});
+
+/**
+ * Supported media keys are used for indicating what UI button should be shown
+ * on the virtual control interface. All supported media keys are listed in
+ * `MediaKey` in `MediaController.webidl`. Some media keys are defined as
+ * default media keys which are always supported. Otherwise, other media keys
+ * have to have corresponding action handler on the active media session in
+ * order to be added to the supported keys.
+ */
+add_task(async function testDefaultSupportedKeys() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`should use default supported keys`);
+ await supportedKeysShouldEqualTo(tab, sDefaultSupportedKeys);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testNoActionHandlerBeingSet() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`create media session but not set any action handler`);
+ await setMediaSessionSupportedAction(tab, []);
+
+ info(
+ `should use default supported keys even if ` +
+ `media session doesn't have any action handler`
+ );
+ await supportedKeysShouldEqualTo(tab, sDefaultSupportedKeys);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testSettingActionsWhichAreAlreadyDefaultKeys() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`create media session but not set any action handler`);
+ await setMediaSessionSupportedAction(tab, ["play", "pause", "stop"]);
+
+ info(
+ `those actions has already been included in default supported keys, so ` +
+ `the result should still be default supported keys`
+ );
+ await supportedKeysShouldEqualTo(tab, sDefaultSupportedKeys);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testSettingActionsWhichAreNotDefaultKeys() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY);
+
+ info(`start media`);
+ await playMedia(tab, testVideoId);
+
+ info(`create media session but not set any action handler`);
+ let nonDefaultActions = [
+ "seekbackward",
+ "seekforward",
+ "previoustrack",
+ "nexttrack",
+ ];
+ await setMediaSessionSupportedAction(tab, nonDefaultActions);
+
+ info(
+ `supported keys should include those actions which are not default supported keys`
+ );
+ let expectedKeys = sDefaultSupportedKeys.concat(nonDefaultActions);
+ await supportedKeysShouldEqualTo(tab, expectedKeys);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+/**
+ * The following are helper functions.
+ */
+async function supportedKeysShouldEqualTo(tab, expectedKeys) {
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ const supportedKeys = controller.supportedKeys;
+ while (JSON.stringify(expectedKeys) != JSON.stringify(supportedKeys)) {
+ await new Promise(r => (controller.onsupportedkeyschange = r));
+ }
+ for (let idx = 0; idx < expectedKeys.length; idx++) {
+ is(
+ supportedKeys[idx],
+ expectedKeys[idx],
+ `'${supportedKeys[idx]}' should equal to '${expectedKeys[idx]}'`
+ );
+ }
+}
+
+function setMediaSessionSupportedAction(tab, actions) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [actions], actionArr => {
+ for (let action of actionArr) {
+ content.navigator.mediaSession.setActionHandler(action, () => {
+ info(`set '${action}' action handler`);
+ });
+ }
+ });
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_nosrc_and_error_media.js b/dom/media/mediacontrol/tests/browser/browser_nosrc_and_error_media.js
new file mode 100644
index 0000000000..837b0ecda6
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_nosrc_and_error_media.js
@@ -0,0 +1,102 @@
+// Import this in order to use `triggerPictureInPicture()`.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/toolkit/components/pictureinpicture/tests/head.js",
+ this
+);
+
+const PAGE_NOSRC_MEDIA =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_no_src_media.html";
+const PAGE_ERROR_MEDIA =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_error_media.html";
+const PAGES = [PAGE_NOSRC_MEDIA, PAGE_ERROR_MEDIA];
+const testVideoId = "video";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.mediacontrol.testingevents.enabled", true]],
+ });
+});
+
+/**
+ * To ensure the no src media and media with error won't activate the media
+ * controller even if they enters PIP mode or fullscreen.
+ */
+add_task(async function testNoSrcOrErrorMediaEntersPIPMode() {
+ for (let page of PAGES) {
+ info(`open media page ${page}`);
+ const tab = await createLoadedTabWrapper(page, { needCheck: false });
+
+ info(`controller should always inactive`);
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ controller.onactivated = () => {
+ ok(false, "should not get activated!");
+ };
+
+ info(`enter PIP mode which would not affect controller`);
+ const winPIP = await triggerPictureInPicture(
+ tab.linkedBrowser,
+ testVideoId
+ );
+ info(`leave PIP mode`);
+ await ensureMessageAndClosePiP(
+ tab.linkedBrowser,
+ testVideoId,
+ winPIP,
+ false
+ );
+ ok(!controller.isActive, "controller is still inactive");
+
+ info(`remove tab`);
+ await tab.close();
+ }
+});
+
+add_task(async function testNoSrcOrErrorMediaEntersFullscreen() {
+ for (let page of PAGES) {
+ info(`open media page ${page}`);
+ const tab = await createLoadedTabWrapper(page, { needCheck: false });
+
+ info(`controller should always inactive`);
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ controller.onactivated = () => {
+ ok(false, "should not get activated!");
+ };
+
+ info(`enter and leave fullscreen which would not affect controller`);
+ await enterAndLeaveFullScreen(tab, testVideoId);
+ ok(!controller.isActive, "controller is still inactive");
+
+ info(`remove tab`);
+ await tab.close();
+ }
+});
+
+/**
+ * The following is helper function.
+ */
+async function enterAndLeaveFullScreen(tab, elementId) {
+ await new Promise(resolve =>
+ SimpleTest.waitForFocus(resolve, tab.linkedBrowser.ownerGlobal)
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [elementId], elementId => {
+ return new Promise(r => {
+ const element = content.document.getElementById(elementId);
+ ok(!content.document.fullscreenElement, "no fullscreen element");
+ element.requestFullscreen();
+ element.onfullscreenchange = () => {
+ if (content.document.fullscreenElement) {
+ element.onfullscreenerror = null;
+ content.document.exitFullscreen();
+ } else {
+ element.onfullscreenchange = null;
+ element.onfullscreenerror = null;
+ r();
+ }
+ };
+ element.onfullscreenerror = () => {
+ // Retry until the element successfully enters fullscreen.
+ element.requestFullscreen();
+ };
+ });
+ });
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_only_control_non_real_time_media.js b/dom/media/mediacontrol/tests/browser/browser_only_control_non_real_time_media.js
new file mode 100644
index 0000000000..76cf8b0ffd
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_only_control_non_real_time_media.js
@@ -0,0 +1,76 @@
+const PAGE_URL =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_empty_title.html";
+
+/**
+ * This test is used to ensure that real-time media won't be affected by the
+ * media control. Only non-real-time media would.
+ */
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.mediacontrol.testingevents.enabled", true]],
+ });
+});
+
+add_task(async function testOnlyControlNonRealTimeMedia() {
+ const tab = await createLoadedTabWrapper(PAGE_URL);
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ await StartRealTimeMedia(tab);
+ ok(
+ !controller.isActive,
+ "starting a real-time media won't acivate controller"
+ );
+
+ info(`playing a non-real-time media would activate controller`);
+ await Promise.all([
+ new Promise(r => (controller.onactivated = r)),
+ startNonRealTimeMedia(tab),
+ ]);
+
+ info(`'pause' action should only pause non-real-time media`);
+ MediaControlService.generateMediaControlKey("pause");
+ await new Promise(r => (controller.onplaybackstatechange = r));
+ await checkIfMediaAreAffectedByMediaControl(tab);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+async function startNonRealTimeMedia(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async _ => {
+ let video = content.document.getElementById("video");
+ if (!video) {
+ ok(false, `can not get the video element!`);
+ return;
+ }
+ await video.play();
+ });
+}
+
+async function StartRealTimeMedia(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async _ => {
+ let videoRealTime = content.document.createElement("video");
+ content.document.body.appendChild(videoRealTime);
+ videoRealTime.srcObject = await content.navigator.mediaDevices.getUserMedia(
+ { audio: true, fake: true }
+ );
+ // We want to ensure that the checking of should the media be controlled by
+ // media control would be performed after the element finishes loading the
+ // media stream. Using `autoplay` would trigger the play invocation only
+ // after the element get enough data.
+ videoRealTime.autoplay = true;
+ await new Promise(r => (videoRealTime.onplaying = r));
+ });
+}
+
+async function checkIfMediaAreAffectedByMediaControl(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async _ => {
+ const vids = content.document.getElementsByTagName("video");
+ for (let vid of vids) {
+ if (!vid.srcObject) {
+ ok(vid.paused, "non-real-time media should be paused");
+ } else {
+ ok(!vid.paused, "real-time media should not be affected");
+ }
+ }
+ });
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_remove_controllable_media_for_active_controller.js b/dom/media/mediacontrol/tests/browser/browser_remove_controllable_media_for_active_controller.js
new file mode 100644
index 0000000000..a0aa5af4e9
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_remove_controllable_media_for_active_controller.js
@@ -0,0 +1,108 @@
+const PAGE_URL =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_autoplay.html";
+
+const testVideoId = "video";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.mediacontrol.testingevents.enabled", true]],
+ });
+});
+
+/**
+ * If an active controller has an active media session, then it can still be
+ * controlled via media key even if there is no controllable media presents.
+ * As active media session could still create other controllable media in its
+ * action handler, it should still receive media key. However, if a controller
+ * doesn't have an active media session, then it won't be controlled via media
+ * key when no controllable media presents.
+ */
+add_task(
+ async function testControllerWithActiveMediaSessionShouldStillBeActiveWhenNoControllableMediaPresents() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_URL);
+
+ info(`play media would activate controller and media session`);
+ await setupMediaSession(tab);
+ await playMedia(tab, testVideoId);
+ await checkOrWaitControllerBecomesActive(tab);
+
+ info(`remove playing media so we don't have any controllable media now`);
+ await Promise.all([
+ new Promise(r => (tab.controller.onplaybackstatechange = r)),
+ removePlayingMedia(tab),
+ ]);
+
+ info(`despite that, controller should still be active`);
+ await checkOrWaitControllerBecomesActive(tab);
+
+ info(`active media session can still receive media key`);
+ await ensureActiveMediaSessionReceivedMediaKey(tab);
+
+ info(`remove tab`);
+ await tab.close();
+ }
+);
+
+add_task(
+ async function testControllerWithoutActiveMediaSessionShouldBecomeInactiveWhenNoControllableMediaPresents() {
+ info(`open media page`);
+ const tab = await createLoadedTabWrapper(PAGE_URL);
+
+ info(`play media would activate controller`);
+ await playMedia(tab, testVideoId);
+ await checkOrWaitControllerBecomesActive(tab);
+
+ info(
+ `remove playing media so we don't have any controllable media, which would deactivate controller`
+ );
+ await Promise.all([
+ new Promise(r => (tab.controller.ondeactivated = r)),
+ removePlayingMedia(tab),
+ ]);
+
+ info(`remove tab`);
+ await tab.close();
+ }
+);
+
+/**
+ * The following are helper functions.
+ */
+function setupMediaSession(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ // except `play/pause/stop`, set an action handler for arbitrary key in
+ // order to later verify if the session receives that media key by listening
+ // to session's `onpositionstatechange`.
+ content.navigator.mediaSession.setActionHandler("seekforward", _ => {
+ content.navigator.mediaSession.setPositionState({
+ duration: 60,
+ });
+ });
+ });
+}
+
+async function ensureActiveMediaSessionReceivedMediaKey(tab) {
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ const positionChangePromise = new Promise(
+ r => (controller.onpositionstatechange = r)
+ );
+ MediaControlService.generateMediaControlKey("seekforward");
+ await positionChangePromise;
+ ok(true, "active media session received media key");
+}
+
+function removePlayingMedia(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [testVideoId], Id => {
+ content.document.getElementById(Id).remove();
+ });
+}
+
+async function checkOrWaitControllerBecomesActive(tab) {
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ if (!controller.isActive) {
+ info(`wait until controller gets activated`);
+ await new Promise(r => (controller.onactivated = r));
+ }
+ ok(controller.isActive, `controller is active`);
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_resume_latest_paused_media.js b/dom/media/mediacontrol/tests/browser/browser_resume_latest_paused_media.js
new file mode 100644
index 0000000000..58cd3f5a0f
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_resume_latest_paused_media.js
@@ -0,0 +1,189 @@
+const PAGE_URL =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_multiple_audible_media.html";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.mediacontrol.testingevents.enabled", true]],
+ });
+});
+
+/**
+ * This test is used to check when resuming media, we would only resume latest
+ * paused media, not all media in the page.
+ */
+add_task(async function testResumingLatestPausedMedias() {
+ info(`open media page and play all media`);
+ const tab = await createLoadedTabWrapper(PAGE_URL);
+ await playAllMedia(tab);
+
+ /**
+ * Pressing `pause` key would pause video1, video2, video3
+ * So resuming from media control key would affect those three media
+ */
+ info(`pressing 'pause' should pause all media`);
+ await generateMediaControlKeyEvent("pause");
+ await checkMediaPausedState(tab, {
+ shouldVideo1BePaused: true,
+ shouldVideo2BePaused: true,
+ shouldVideo3BePaused: true,
+ });
+
+ info(`all media are latest paused, pressing 'play' should resume all`);
+ await generateMediaControlKeyEvent("play");
+ await checkMediaPausedState(tab, {
+ shouldVideo1BePaused: false,
+ shouldVideo2BePaused: false,
+ shouldVideo3BePaused: false,
+ });
+
+ info(`pause only one playing video by calling its webidl method`);
+ await pauseMedia(tab, "video3");
+ await checkMediaPausedState(tab, {
+ shouldVideo1BePaused: false,
+ shouldVideo2BePaused: false,
+ shouldVideo3BePaused: true,
+ });
+
+ /**
+ * Pressing `pause` key would pause video1, video2
+ * So resuming from media control key would affect those two media
+ */
+ info(`pressing 'pause' should pause two playing media`);
+ await generateMediaControlKeyEvent("pause");
+ await checkMediaPausedState(tab, {
+ shouldVideo1BePaused: true,
+ shouldVideo2BePaused: true,
+ shouldVideo3BePaused: true,
+ });
+
+ info(`two media are latest paused, pressing 'play' should only affect them`);
+ await generateMediaControlKeyEvent("play");
+ await checkMediaPausedState(tab, {
+ shouldVideo1BePaused: false,
+ shouldVideo2BePaused: false,
+ shouldVideo3BePaused: true,
+ });
+
+ info(`pause only one playing video by calling its webidl method`);
+ await pauseMedia(tab, "video2");
+ await checkMediaPausedState(tab, {
+ shouldVideo1BePaused: false,
+ shouldVideo2BePaused: true,
+ shouldVideo3BePaused: true,
+ });
+
+ /**
+ * Pressing `pause` key would pause video1
+ * So resuming from media control key would only affect one media
+ */
+ info(`pressing 'pause' should pause one playing media`);
+ await generateMediaControlKeyEvent("pause");
+ await checkMediaPausedState(tab, {
+ shouldVideo1BePaused: true,
+ shouldVideo2BePaused: true,
+ shouldVideo3BePaused: true,
+ });
+
+ info(`one media is latest paused, pressing 'play' should only affect it`);
+ await generateMediaControlKeyEvent("play");
+ await checkMediaPausedState(tab, {
+ shouldVideo1BePaused: false,
+ shouldVideo2BePaused: true,
+ shouldVideo3BePaused: true,
+ });
+
+ /**
+ * Only one media is playing, so pausing it should not stop controlling media.
+ * We should still be able to resume it later.
+ */
+ info(`pause only playing video by calling its webidl method`);
+ await pauseMedia(tab, "video1");
+ await checkMediaPausedState(tab, {
+ shouldVideo1BePaused: true,
+ shouldVideo2BePaused: true,
+ shouldVideo3BePaused: true,
+ });
+
+ info(`pressing 'pause' for already paused media, nothing would happen`);
+ // All media are already paused, so no need to wait for playback state change,
+ // call the method directly.
+ MediaControlService.generateMediaControlKey("pause");
+
+ info(`pressing 'play' would still affect on latest paused media`);
+ await generateMediaControlKeyEvent("play");
+ await checkMediaPausedState(tab, {
+ shouldVideo1BePaused: false,
+ shouldVideo2BePaused: true,
+ shouldVideo3BePaused: true,
+ });
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+/**
+ * The following are helper functions.
+ */
+async function playAllMedia(tab) {
+ const playbackStateChangedPromise = waitUntilDisplayedPlaybackChanged();
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ return new Promise(r => {
+ const videos = content.document.getElementsByTagName("video");
+ let mediaCount = 0;
+ docShell.chromeEventHandler.addEventListener(
+ "MozStartMediaControl",
+ () => {
+ if (++mediaCount == videos.length) {
+ info(`all media have started media control`);
+ r();
+ }
+ }
+ );
+ for (let video of videos) {
+ info(`play ${video.id} video`);
+ video.play();
+ }
+ });
+ });
+ await playbackStateChangedPromise;
+}
+
+async function pauseMedia(tab, videoId) {
+ await SpecialPowers.spawn(tab.linkedBrowser, [videoId], videoId => {
+ const video = content.document.getElementById(videoId);
+ if (!video) {
+ ok(false, `can not find ${videoId}!`);
+ }
+ video.pause();
+ });
+}
+
+function checkMediaPausedState(
+ tab,
+ { shouldVideo1BePaused, shouldVideo2BePaused, shouldVideo3BePaused }
+) {
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [shouldVideo1BePaused, shouldVideo2BePaused, shouldVideo3BePaused],
+ (shouldVideo1BePaused, shouldVideo2BePaused, shouldVideo3BePaused) => {
+ const video1 = content.document.getElementById("video1");
+ const video2 = content.document.getElementById("video2");
+ const video3 = content.document.getElementById("video3");
+ is(
+ video1.paused,
+ shouldVideo1BePaused,
+ "Correct paused state for video1"
+ );
+ is(
+ video2.paused,
+ shouldVideo2BePaused,
+ "Correct paused state for video2"
+ );
+ is(
+ video3.paused,
+ shouldVideo3BePaused,
+ "Correct paused state for video3"
+ );
+ }
+ );
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_seek_captured_audio.js b/dom/media/mediacontrol/tests/browser/browser_seek_captured_audio.js
new file mode 100644
index 0000000000..3dbbee065e
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_seek_captured_audio.js
@@ -0,0 +1,59 @@
+const PAGE_NON_AUTOPLAY_MEDIA =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_autoplay.html";
+
+const testVideoId = "video";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.mediacontrol.testingevents.enabled", true]],
+ });
+});
+
+/**
+ * Seeking a captured audio media before it starts, and it should still be able
+ * to be controlled via media key after it starts playing.
+ */
+add_task(async function testSeekAudibleCapturedMedia() {
+ info(`open new non autoplay media page`);
+ const tab = await createLoadedTabWrapper(PAGE_NON_AUTOPLAY_MEDIA);
+
+ info(`perform seek on the captured media before it starts`);
+ await captureAudio(tab, testVideoId);
+ await seekAudio(tab, testVideoId);
+
+ info(`start captured media`);
+ await playMedia(tab, testVideoId);
+
+ info(`pressing 'pause' key, captured media should be paused`);
+ await generateMediaControlKeyEvent("pause");
+ await checkOrWaitUntilMediaStoppedPlaying(tab, testVideoId);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+/**
+ * The following are helper functions.
+ */
+function captureAudio(tab, elementId) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [elementId], Id => {
+ const video = content.document.getElementById(Id);
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ const context = new content.AudioContext();
+ // Capture audio from the media element to a MediaElementAudioSourceNode.
+ context.createMediaElementSource(video);
+ });
+}
+
+function seekAudio(tab, elementId) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [elementId], async Id => {
+ const video = content.document.getElementById(Id);
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ video.currentTime = 0.0;
+ await new Promise(r => (video.onseeked = r));
+ });
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_stop_control_after_media_reaches_to_end.js b/dom/media/mediacontrol/tests/browser/browser_stop_control_after_media_reaches_to_end.js
new file mode 100644
index 0000000000..cc8ccf270a
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_stop_control_after_media_reaches_to_end.js
@@ -0,0 +1,108 @@
+const PAGE_URL =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_looping_media.html";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.mediacontrol.stopcontrol.aftermediaends", true]],
+ });
+});
+
+/**
+ * This test is used to ensure that we would stop controlling media after it
+ * reaches to the end when a controller doesn't have an active media session.
+ * If a controller has an active media session, it would keep active despite
+ * media reaches to the end.
+ */
+add_task(async function testControllerShouldStopAfterMediaReachesToTheEnd() {
+ info(`open media page and play media until the end`);
+ const tab = await createLoadedTabWrapper(PAGE_URL);
+ await Promise.all([
+ checkIfMediaControllerBecomeInactiveAfterMediaEnds(tab),
+ playMediaUntilItReachesToTheEnd(tab),
+ ]);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testControllerWontStopAfterMediaReachesToTheEnd() {
+ info(`open media page and create media session`);
+ const tab = await createLoadedTabWrapper(PAGE_URL);
+ await createMediaSession(tab);
+
+ info(`play media until the end`);
+ await playMediaUntilItReachesToTheEnd(tab);
+
+ info(`controller is still active because of having active media session`);
+ await checkControllerIsActive(tab);
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+/**
+ * The following are helper functions.
+ */
+function checkIfMediaControllerBecomeInactiveAfterMediaEnds(tab) {
+ return new Promise(r => {
+ let activeChangedNums = 0;
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ controller.onactivated = () => {
+ is(
+ ++activeChangedNums,
+ 1,
+ `Receive ${activeChangedNums} times 'onactivechange'`
+ );
+ // We activate controller when it becomes playing, which doesn't guarantee
+ // it's already audible, so we won't check audible state here.
+ ok(controller.isActive, "controller should be active");
+ ok(controller.isPlaying, "controller should be playing");
+ };
+ controller.ondeactivated = () => {
+ is(
+ ++activeChangedNums,
+ 2,
+ `Receive ${activeChangedNums} times 'onactivechange'`
+ );
+ ok(!controller.isActive, "controller should be inactive");
+ ok(!controller.isAudible, "controller should be inaudible");
+ ok(!controller.isPlaying, "controller should be paused");
+ r();
+ };
+ });
+}
+
+function playMediaUntilItReachesToTheEnd(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ const video = content.document.getElementById("video");
+ if (!video) {
+ ok(false, "can't get video");
+ }
+
+ if (video.readyState < video.HAVE_METADATA) {
+ info(`load media to get its duration`);
+ video.load();
+ await new Promise(r => (video.loadedmetadata = r));
+ }
+
+ info(`adjust the start position to faster reach to the end`);
+ ok(video.duration > 1.0, "video's duration is larger than 1.0s");
+ video.currentTime = video.duration - 1.0;
+
+ info(`play ${video.id} video`);
+ video.play();
+ await new Promise(r => (video.onended = r));
+ });
+}
+
+function createMediaSession(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ // simply create a media session, which would become the active media session later.
+ content.navigator.mediaSession;
+ });
+}
+
+function checkControllerIsActive(tab) {
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ ok(controller.isActive, `controller is active`);
+}
diff --git a/dom/media/mediacontrol/tests/browser/browser_suspend_inactive_tab.js b/dom/media/mediacontrol/tests/browser/browser_suspend_inactive_tab.js
new file mode 100644
index 0000000000..334717a2f2
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/browser_suspend_inactive_tab.js
@@ -0,0 +1,131 @@
+const PAGE_NON_AUTOPLAY =
+ "https://example.com/browser/dom/media/mediacontrol/tests/browser/file_non_autoplay.html";
+const VIDEO_ID = "video";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.mediacontrol.testingevents.enabled", true],
+ ["dom.suspend_inactive.enabled", true],
+ ["dom.audiocontext.testing", true],
+ ],
+ });
+});
+
+/**
+ * This test to used to test the feature that would suspend the inactive tab,
+ * which currently is only used on Android.
+ *
+ * Normally when tab becomes inactive, we would suspend it and stop its script
+ * from running. However, if a tab has a main controller, which indicates it
+ * might have playng media, or waiting media keys to control media, then it
+ * would not be suspended event if it's inactive.
+ *
+ * In addition, Note that, on Android, audio focus management is enabled by
+ * default, so there is only one tab being able to play at a time, which means
+ * the tab playing media always has main controller.
+ */
+add_task(async function testInactiveTabWouldBeSuspended() {
+ info(`open a tab`);
+ const tab = await createTab(PAGE_NON_AUTOPLAY);
+ await assertIfWindowGetSuspended(tab, { shouldBeSuspended: false });
+
+ info(`tab should be suspended when it becomes inactive`);
+ setTabActive(tab, false);
+ await assertIfWindowGetSuspended(tab, { shouldBeSuspended: true });
+
+ info(`remove tab`);
+ await tab.close();
+});
+
+add_task(async function testInactiveTabEverStartPlayingWontBeSuspended() {
+ info(`open tab1 and play media`);
+ const tab1 = await createTab(PAGE_NON_AUTOPLAY, { needCheck: true });
+ await assertIfWindowGetSuspended(tab1, { shouldBeSuspended: false });
+ await playMedia(tab1, VIDEO_ID);
+
+ info(`tab with playing media won't be suspended when it becomes inactive`);
+ setTabActive(tab1, false);
+ await assertIfWindowGetSuspended(tab1, { shouldBeSuspended: false });
+
+ info(
+ `even if media is paused, keep tab running so that it could listen to media keys to control media in the future`
+ );
+ await pauseMedia(tab1, VIDEO_ID);
+ await assertIfWindowGetSuspended(tab1, { shouldBeSuspended: false });
+
+ info(`open tab2 and play media`);
+ const tab2 = await createTab(PAGE_NON_AUTOPLAY, { needCheck: true });
+ await assertIfWindowGetSuspended(tab2, { shouldBeSuspended: false });
+ await playMedia(tab2, VIDEO_ID);
+
+ info(
+ `as inactive tab1 doesn't own main controller, it should be suspended again`
+ );
+ await assertIfWindowGetSuspended(tab1, { shouldBeSuspended: true });
+
+ info(`remove tabs`);
+ await Promise.all([tab1.close(), tab2.close()]);
+});
+
+add_task(
+ async function testInactiveTabWithRunningAudioContextWontBeSuspended() {
+ info(`open tab and start an audio context (AC)`);
+ const tab = await createTab("about:blank");
+ await startAudioContext(tab);
+ await assertIfWindowGetSuspended(tab, { shouldBeSuspended: false });
+
+ info(`tab with running AC won't be suspended when it becomes inactive`);
+ setTabActive(tab, false);
+ await assertIfWindowGetSuspended(tab, { shouldBeSuspended: false });
+
+ info(`if AC has been suspended, then inactive tab should be suspended`);
+ await suspendAudioContext(tab);
+ await assertIfWindowGetSuspended(tab, { shouldBeSuspended: true });
+
+ info(`remove tab`);
+ await tab.close();
+ }
+);
+
+/**
+ * The following are helper functions.
+ */
+async function createTab(url, needCheck = false) {
+ const tab = await createLoadedTabWrapper(url, { needCheck });
+ return tab;
+}
+
+function assertIfWindowGetSuspended(tab, { shouldBeSuspended }) {
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [shouldBeSuspended],
+ expectedSuspend => {
+ const isSuspended = content.windowUtils.suspendedByBrowsingContextGroup;
+ is(
+ expectedSuspend,
+ isSuspended,
+ `window suspended state (${isSuspended}) is equal to the expected`
+ );
+ }
+ );
+}
+
+function setTabActive(tab, isActive) {
+ tab.linkedBrowser.docShellIsActive = isActive;
+}
+
+function startAudioContext(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async _ => {
+ content.ac = new content.AudioContext();
+ await new Promise(r => (content.ac.onstatechange = r));
+ ok(content.ac.state == "running", `Audio context started running`);
+ });
+}
+
+function suspendAudioContext(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async _ => {
+ await content.ac.suspend();
+ ok(content.ac.state == "suspended", `Audio context is suspended`);
+ });
+}
diff --git a/dom/media/mediacontrol/tests/browser/file_audio_and_inaudible_media.html b/dom/media/mediacontrol/tests/browser/file_audio_and_inaudible_media.html
new file mode 100644
index 0000000000..e16d5dee26
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/file_audio_and_inaudible_media.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>page with audible and inaudible media</title>
+</head>
+<body>
+<video id="video1" src="gizmo.mp4" loop></video>
+<video id="video2" src="gizmo.mp4" loop muted></video>
+</body>
+</html>
diff --git a/dom/media/mediacontrol/tests/browser/file_autoplay.html b/dom/media/mediacontrol/tests/browser/file_autoplay.html
new file mode 100644
index 0000000000..97a58ec2a2
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/file_autoplay.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Autoplay page</title>
+</head>
+<body>
+<video id="autoplay" src="gizmo.mp4" autoplay loop></video>
+</body>
+</html>
diff --git a/dom/media/mediacontrol/tests/browser/file_empty_title.html b/dom/media/mediacontrol/tests/browser/file_empty_title.html
new file mode 100644
index 0000000000..516c16036f
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/file_empty_title.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title></title>
+</head>
+<body>
+<video id="video" src="gizmo.mp4" loop></video>
+</body>
+</html>
diff --git a/dom/media/mediacontrol/tests/browser/file_error_media.html b/dom/media/mediacontrol/tests/browser/file_error_media.html
new file mode 100644
index 0000000000..7f54340dd1
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/file_error_media.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Error media</title>
+</head>
+<body>
+<video id="video" src="bogus.ogv"></video>
+</body>
+</html>
diff --git a/dom/media/mediacontrol/tests/browser/file_iframe_media.html b/dom/media/mediacontrol/tests/browser/file_iframe_media.html
new file mode 100644
index 0000000000..2d2c4fd122
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/file_iframe_media.html
@@ -0,0 +1,94 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+<body>
+<video id="video" src="gizmo.mp4" loop></video>
+<script type="text/javascript">
+
+const video = document.getElementById("video");
+const w = window.opener || window.parent;
+
+window.onmessage = async event => {
+ if (event.data == "fullscreen") {
+ video.requestFullscreen();
+ video.onfullscreenchange = () => {
+ video.onfullscreenchange = null;
+ video.onfullscreenerror = null;
+ w.postMessage("entered-fullscreen", "*");
+ }
+ video.onfullscreenerror = () => {
+ // Retry until the element successfully enters fullscreen.
+ video.requestFullscreen();
+ }
+ } else if (event.data == "check-playing") {
+ if (!video.paused) {
+ w.postMessage("checked-playing", "*");
+ } else {
+ video.onplaying = () => {
+ video.onplaying = null;
+ w.postMessage("checked-playing", "*");
+ }
+ }
+ } else if (event.data == "check-pause") {
+ if (video.paused) {
+ w.postMessage("checked-pause", "*");
+ } else {
+ video.onpause = () => {
+ video.onpause = null;
+ w.postMessage("checked-pause", "*");
+ }
+ }
+ } else if (event.data == "play") {
+ await video.play();
+ w.postMessage("played", "*");
+ } else if (event.data == "pause") {
+ video.pause();
+ w.postMessage("paused", "*");
+ } else if (event.data == "setMetadata") {
+ const metadata = {
+ title: document.title,
+ artist: document.title,
+ album: document.title,
+ artwork: [{ src: document.title, sizes: "128x128", type: "image/jpeg" }],
+ };
+ navigator.mediaSession.metadata = new window.MediaMetadata(metadata);
+ w.postMessage(metadata, "*");
+ } else if (event.data == "setPositionState") {
+ navigator.mediaSession.setPositionState({
+ duration: 60, // The value doesn't matter
+ });
+ } else if (event.data.cmd == "setActionHandler") {
+ if (window.triggeredActionHandler === undefined) {
+ window.triggeredActionHandler = {};
+ }
+ const action = event.data.action;
+ window.triggeredActionHandler[action] = new Promise(r => {
+ navigator.mediaSession.setActionHandler(action, async () => {
+ if (action == "stop" || action == "pause") {
+ video.pause();
+ } else if (action == "play") {
+ await video.play();
+ }
+ r();
+ });
+ });
+ w.postMessage("setActionHandler-done", "*");
+ } else if (event.data.cmd == "checkActionHandler") {
+ const action = event.data.action;
+ if (!window.triggeredActionHandler[action]) {
+ w.postMessage("checkActionHandler-fail", "*");
+ } else {
+ await window.triggeredActionHandler[action];
+ w.postMessage("checkActionHandler-done", "*");
+ }
+ } else if (event.data == "create-media-session") {
+ // simply calling a media session would create an instance.
+ navigator.mediaSession;
+ w.postMessage("created-media-session", "*");
+ }
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/media/mediacontrol/tests/browser/file_main_frame_with_multiple_child_session_frames.html b/dom/media/mediacontrol/tests/browser/file_main_frame_with_multiple_child_session_frames.html
new file mode 100644
index 0000000000..f8e7aa9afe
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/file_main_frame_with_multiple_child_session_frames.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Media control page with multiple iframes which contain media session</title>
+</head>
+<body>
+<video id="video" src="gizmo.mp4" loop></video>
+<iframe id="frame1"></iframe>
+<iframe id="frame2"></iframe>
+</body>
+</html>
diff --git a/dom/media/mediacontrol/tests/browser/file_multiple_audible_media.html b/dom/media/mediacontrol/tests/browser/file_multiple_audible_media.html
new file mode 100644
index 0000000000..e78fabe7fa
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/file_multiple_audible_media.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>mutiple audible media</title>
+</head>
+<body>
+<video id="video1" src="gizmo.mp4" loop></video>
+<video id="video2" src="gizmo.mp4" loop></video>
+<video id="video3" src="gizmo.mp4" loop></video>
+</body>
+</html>
diff --git a/dom/media/mediacontrol/tests/browser/file_muted_autoplay.html b/dom/media/mediacontrol/tests/browser/file_muted_autoplay.html
new file mode 100644
index 0000000000..f64d537a46
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/file_muted_autoplay.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Muted autoplay page</title>
+</head>
+<body>
+<video id="autoplay" src="gizmo.mp4" autoplay loop muted></video>
+</body>
+</html>
diff --git a/dom/media/mediacontrol/tests/browser/file_no_src_media.html b/dom/media/mediacontrol/tests/browser/file_no_src_media.html
new file mode 100644
index 0000000000..e1318e863c
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/file_no_src_media.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>No src media</title>
+</head>
+<body>
+<video id="video"></video>
+</body>
+</html>
diff --git a/dom/media/mediacontrol/tests/browser/file_non_autoplay.html b/dom/media/mediacontrol/tests/browser/file_non_autoplay.html
new file mode 100644
index 0000000000..06daa7e2d8
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/file_non_autoplay.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Non-Autoplay page</title>
+</head>
+<body>
+<video id="video" src="gizmo.mp4" loop></video>
+<image id="image" src="data:image/svg+xml;base64,PCEtLSBUaGlzIFNvdXJjZSBDb2RlIEZvcm0gaXMgc3ViamVjdCB0byB0aGUgdGVybXMgb2YgdGhlIE1vemlsbGEgUHVibGljCiAgIC0gTGljZW5zZSwgdi4gMi4wLiBJZiBhIGNvcHkgb2YgdGhlIE1QTCB3YXMgbm90IGRpc3RyaWJ1dGVkIHdpdGggdGhpcwogICAtIGZpbGUsIFlvdSBjYW4gb2J0YWluIG9uZSBhdCBodHRwOi8vbW96aWxsYS5vcmcvTVBMLzIuMC8uIC0tPgo8c3ZnIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgd2lkdGg9IjE2IiBoZWlnaHQ9IjE2IiB2aWV3Qm94PSIwIDAgMTYgMTYiPgogIDxwYXRoIGZpbGw9ImNvbnRleHQtZmlsbCIgZmlsbC1vcGFjaXR5PSJjb250ZXh0LWZpbGwtb3BhY2l0eSIgZD0iTTggMGE4IDggMCAxIDAgOCA4IDguMDA5IDguMDA5IDAgMCAwLTgtOHptNS4xNjMgNC45NThoLTEuNTUyYTcuNyA3LjcgMCAwIDAtMS4wNTEtMi4zNzYgNi4wMyA2LjAzIDAgMCAxIDIuNjAzIDIuMzc2ek0xNCA4YTUuOTYzIDUuOTYzIDAgMCAxLS4zMzUgMS45NThoLTEuODIxQTEyLjMyNyAxMi4zMjcgMCAwIDAgMTIgOGExMi4zMjcgMTIuMzI3IDAgMCAwLS4xNTYtMS45NThoMS44MjFBNS45NjMgNS45NjMgMCAwIDEgMTQgOHptLTYgNmMtMS4wNzUgMC0yLjAzNy0xLjItMi41NjctMi45NThoNS4xMzVDMTAuMDM3IDEyLjggOS4wNzUgMTQgOCAxNHpNNS4xNzQgOS45NThhMTEuMDg0IDExLjA4NCAwIDAgMSAwLTMuOTE2aDUuNjUxQTExLjExNCAxMS4xMTQgMCAwIDEgMTEgOGExMS4xMTQgMTEuMTE0IDAgMCAxLS4xNzQgMS45NTh6TTIgOGE1Ljk2MyA1Ljk2MyAwIDAgMSAuMzM1LTEuOTU4aDEuODIxYTEyLjM2MSAxMi4zNjEgMCAwIDAgMCAzLjkxNkgyLjMzNUE1Ljk2MyA1Ljk2MyAwIDAgMSAyIDh6bTYtNmMxLjA3NSAwIDIuMDM3IDEuMiAyLjU2NyAyLjk1OEg1LjQzM0M1Ljk2MyAzLjIgNi45MjUgMiA4IDJ6bS0yLjU2LjU4MmE3LjcgNy43IDAgMCAwLTEuMDUxIDIuMzc2SDIuODM3QTYuMDMgNi4wMyAwIDAgMSA1LjQ0IDIuNTgyem0tMi42IDguNDZoMS41NDlhNy43IDcuNyAwIDAgMCAxLjA1MSAyLjM3NiA2LjAzIDYuMDMgMCAwIDEtMi42MDMtMi4zNzZ6bTcuNzIzIDIuMzc2YTcuNyA3LjcgMCAwIDAgMS4wNTEtMi4zNzZoMS41NTJhNi4wMyA2LjAzIDAgMCAxLTIuNjA2IDIuMzc2eiIvPgo8L3N2Zz4K">
+<iframe id="iframe" allow="fullscreen *" allowfullscreen></iframe>
+</body>
+</html>
diff --git a/dom/media/mediacontrol/tests/browser/file_non_eligible_media.html b/dom/media/mediacontrol/tests/browser/file_non_eligible_media.html
new file mode 100644
index 0000000000..bf27943fce
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/file_non_eligible_media.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Media are not eligible to be controlled</title>
+</head>
+<body>
+<video id="muted" src="gizmo.mp4" controls muted loop></video>
+<video id="volume-0" src="gizmo.mp4" controls loop></video>
+<video id="no-audio-track" src="gizmo-noaudio.webm" controls loop></video>
+<video id="silent-audio-track" src="silentAudioTrack.webm" controls loop></video>
+<video id="short-duration" src="gizmo-short.mp4" controls loop></video>
+<video id="inaudible-captured-media" src="gizmo.mp4" muted controls loop></video>
+</body>
+</html>
diff --git a/dom/media/mediacontrol/tests/browser/file_non_looping_media.html b/dom/media/mediacontrol/tests/browser/file_non_looping_media.html
new file mode 100644
index 0000000000..41e049645c
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/file_non_looping_media.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Non looping media page</title>
+</head>
+<body>
+<video id="video" src="gizmo.mp4"></video>
+</body>
+</html>
diff --git a/dom/media/mediacontrol/tests/browser/head.js b/dom/media/mediacontrol/tests/browser/head.js
new file mode 100644
index 0000000000..cac96c0bff
--- /dev/null
+++ b/dom/media/mediacontrol/tests/browser/head.js
@@ -0,0 +1,402 @@
+/**
+ * This function would create a new foreround tab and load the url for it. In
+ * addition, instead of returning a tab element, we return a tab wrapper that
+ * helps us to automatically detect if the media controller of that tab
+ * dispatches the first (activated) and the last event (deactivated) correctly.
+ * @ param url
+ * the page url which tab would load
+ * @ param input window (optional)
+ * if it exists, the tab would be created from the input window. If not,
+ * then the tab would be created in current window.
+ * @ param needCheck (optional)
+ * it decides if we would perform the check for the first and last event
+ * on the media controller. It's default true.
+ */
+async function createLoadedTabWrapper(
+ url,
+ { inputWindow = window, needCheck = true } = {}
+) {
+ class tabWrapper {
+ constructor(tab, needCheck) {
+ this._tab = tab;
+ this._controller = tab.linkedBrowser.browsingContext.mediaController;
+ this._firstEvent = "";
+ this._lastEvent = "";
+ this._events = [
+ "activated",
+ "deactivated",
+ "metadatachange",
+ "playbackstatechange",
+ "positionstatechange",
+ "supportedkeyschange",
+ ];
+ this._needCheck = needCheck;
+ if (this._needCheck) {
+ this._registerAllEvents();
+ }
+ }
+ _registerAllEvents() {
+ for (let event of this._events) {
+ this._controller.addEventListener(event, this._handleEvent.bind(this));
+ }
+ }
+ _unregisterAllEvents() {
+ for (let event of this._events) {
+ this._controller.removeEventListener(
+ event,
+ this._handleEvent.bind(this)
+ );
+ }
+ }
+ _handleEvent(event) {
+ info(`handle event=${event.type}`);
+ if (this._firstEvent === "") {
+ this._firstEvent = event.type;
+ }
+ this._lastEvent = event.type;
+ }
+ get linkedBrowser() {
+ return this._tab.linkedBrowser;
+ }
+ get controller() {
+ return this._controller;
+ }
+ get tabElement() {
+ return this._tab;
+ }
+ async close() {
+ info(`wait until finishing close tab wrapper`);
+ const deactivationPromise = this._controller.isActive
+ ? new Promise(r => (this._controller.ondeactivated = r))
+ : Promise.resolve();
+ BrowserTestUtils.removeTab(this._tab);
+ await deactivationPromise;
+ if (this._needCheck) {
+ is(this._firstEvent, "activated", "First event should be 'activated'");
+ is(
+ this._lastEvent,
+ "deactivated",
+ "Last event should be 'deactivated'"
+ );
+ this._unregisterAllEvents();
+ }
+ }
+ }
+ const browser = inputWindow ? inputWindow.gBrowser : window.gBrowser;
+ let tab = await BrowserTestUtils.openNewForegroundTab(browser, url);
+ return new tabWrapper(tab, needCheck);
+}
+
+/**
+ * Returns a promise that resolves when generated media control keys has
+ * triggered the main media controller's corresponding method and changes its
+ * playback state.
+ *
+ * @param {string} event
+ * The event name of the media control key
+ * @return {Promise}
+ * Resolve when the main controller receives the media control key event
+ * and change its playback state.
+ */
+function generateMediaControlKeyEvent(event) {
+ const playbackStateChanged = waitUntilDisplayedPlaybackChanged();
+ MediaControlService.generateMediaControlKey(event);
+ return playbackStateChanged;
+}
+
+/**
+ * Play the specific media and wait until it plays successfully and the main
+ * controller has been updated.
+ *
+ * @param {tab} tab
+ * The tab that contains the media which we would play
+ * @param {string} elementId
+ * The element Id of the media which we would play
+ * @return {Promise}
+ * Resolve when the media has been starting playing and the main
+ * controller has been updated.
+ */
+async function playMedia(tab, elementId) {
+ const playbackStatePromise = waitUntilDisplayedPlaybackChanged();
+ await SpecialPowers.spawn(tab.linkedBrowser, [elementId], async Id => {
+ const video = content.document.getElementById(Id);
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ ok(
+ await video.play().then(
+ _ => true,
+ _ => false
+ ),
+ "video started playing"
+ );
+ });
+ return playbackStatePromise;
+}
+
+/**
+ * Pause the specific media and wait until it pauses successfully and the main
+ * controller has been updated.
+ *
+ * @param {tab} tab
+ * The tab that contains the media which we would pause
+ * @param {string} elementId
+ * The element Id of the media which we would pause
+ * @return {Promise}
+ * Resolve when the media has been paused and the main controller has
+ * been updated.
+ */
+function pauseMedia(tab, elementId) {
+ const pausePromise = SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [elementId],
+ Id => {
+ const video = content.document.getElementById(Id);
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ ok(!video.paused, `video is playing before calling pause`);
+ video.pause();
+ }
+ );
+ return Promise.all([pausePromise, waitUntilDisplayedPlaybackChanged()]);
+}
+
+/**
+ * Returns a promise that resolves when the specific media starts playing.
+ *
+ * @param {tab} tab
+ * The tab that contains the media which we would check
+ * @param {string} elementId
+ * The element Id of the media which we would check
+ * @return {Promise}
+ * Resolve when the media has been starting playing.
+ */
+function checkOrWaitUntilMediaStartedPlaying(tab, elementId) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [elementId], Id => {
+ return new Promise(resolve => {
+ const video = content.document.getElementById(Id);
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ if (!video.paused) {
+ ok(true, `media started playing`);
+ resolve();
+ } else {
+ info(`wait until media starts playing`);
+ video.onplaying = () => {
+ video.onplaying = null;
+ ok(true, `media started playing`);
+ resolve();
+ };
+ }
+ });
+ });
+}
+
+/**
+ * Returns a promise that resolves when the specific media stops playing.
+ *
+ * @param {tab} tab
+ * The tab that contains the media which we would check
+ * @param {string} elementId
+ * The element Id of the media which we would check
+ * @return {Promise}
+ * Resolve when the media has been stopped playing.
+ */
+function checkOrWaitUntilMediaStoppedPlaying(tab, elementId) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [elementId], Id => {
+ return new Promise(resolve => {
+ const video = content.document.getElementById(Id);
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ if (video.paused) {
+ ok(true, `media stopped playing`);
+ resolve();
+ } else {
+ info(`wait until media stops playing`);
+ video.onpause = () => {
+ video.onpause = null;
+ ok(true, `media stopped playing`);
+ resolve();
+ };
+ }
+ });
+ });
+}
+
+/**
+ * Check if the active metadata is empty.
+ */
+function isCurrentMetadataEmpty() {
+ const current = MediaControlService.getCurrentActiveMediaMetadata();
+ is(current.title, "", `current title should be empty`);
+ is(current.artist, "", `current title should be empty`);
+ is(current.album, "", `current album should be empty`);
+ is(current.artwork.length, 0, `current artwork should be empty`);
+}
+
+/**
+ * Check if the active metadata is equal to the given metadata.artwork
+ *
+ * @param {object} metadata
+ * The metadata that would be compared with the active metadata
+ */
+function isCurrentMetadataEqualTo(metadata) {
+ const current = MediaControlService.getCurrentActiveMediaMetadata();
+ is(
+ current.title,
+ metadata.title,
+ `tile '${current.title}' is equal to ${metadata.title}`
+ );
+ is(
+ current.artist,
+ metadata.artist,
+ `artist '${current.artist}' is equal to ${metadata.artist}`
+ );
+ is(
+ current.album,
+ metadata.album,
+ `album '${current.album}' is equal to ${metadata.album}`
+ );
+ is(
+ current.artwork.length,
+ metadata.artwork.length,
+ `artwork length '${current.artwork.length}' is equal to ${metadata.artwork.length}`
+ );
+ for (let idx = 0; idx < metadata.artwork.length; idx++) {
+ // the current src we got would be a completed path of the image, so we do
+ // not check if they are equal, we check if the current src includes the
+ // metadata's file name. Eg. "http://foo/bar.jpg" v.s. "bar.jpg"
+ ok(
+ current.artwork[idx].src.includes(metadata.artwork[idx].src),
+ `artwork src '${current.artwork[idx].src}' includes ${metadata.artwork[idx].src}`
+ );
+ is(
+ current.artwork[idx].sizes,
+ metadata.artwork[idx].sizes,
+ `artwork sizes '${current.artwork[idx].sizes}' is equal to ${metadata.artwork[idx].sizes}`
+ );
+ is(
+ current.artwork[idx].type,
+ metadata.artwork[idx].type,
+ `artwork type '${current.artwork[idx].type}' is equal to ${metadata.artwork[idx].type}`
+ );
+ }
+}
+
+/**
+ * Check if the given tab is using the default metadata. If the tab is being
+ * used in the private browsing mode, `isPrivateBrowsing` should be definded in
+ * the `options`.
+ */
+async function isGivenTabUsingDefaultMetadata(tab, options = {}) {
+ const localization = new Localization([
+ "branding/brand.ftl",
+ "dom/media.ftl",
+ ]);
+ const fallbackTitle = await localization.formatValue(
+ "mediastatus-fallback-title"
+ );
+ ok(fallbackTitle.length, "l10n fallback title is not empty");
+
+ const metadata =
+ tab.linkedBrowser.browsingContext.mediaController.getMetadata();
+
+ await SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [metadata.title, fallbackTitle, options.isPrivateBrowsing],
+ (title, fallbackTitle, isPrivateBrowsing) => {
+ if (isPrivateBrowsing || !content.document.title.length) {
+ is(title, fallbackTitle, "Using a generic default fallback title");
+ } else {
+ is(
+ title,
+ content.document.title,
+ "Using website title as a default title"
+ );
+ }
+ }
+ );
+ is(metadata.artwork.length, 1, "Default metada contains one artwork");
+ ok(
+ metadata.artwork[0].src.includes("defaultFavicon.svg"),
+ "Using default favicon as a default art work"
+ );
+}
+
+/**
+ * Wait until the main media controller changes its playback state, we would
+ * observe that by listening for `media-displayed-playback-changed`
+ * notification.
+ *
+ * @return {Promise}
+ * Resolve when observing `media-displayed-playback-changed`
+ */
+function waitUntilDisplayedPlaybackChanged() {
+ return BrowserUtils.promiseObserved("media-displayed-playback-changed");
+}
+
+/**
+ * Wait until the metadata that would be displayed on the virtual control
+ * interface changes. we would observe that by listening for
+ * `media-displayed-metadata-changed` notification.
+ *
+ * @return {Promise}
+ * Resolve when observing `media-displayed-metadata-changed`
+ */
+function waitUntilDisplayedMetadataChanged() {
+ return BrowserUtils.promiseObserved("media-displayed-metadata-changed");
+}
+
+/**
+ * Wait until the main media controller has been changed, we would observe that
+ * by listening for the `main-media-controller-changed` notification.
+ *
+ * @return {Promise}
+ * Resolve when observing `main-media-controller-changed`
+ */
+function waitUntilMainMediaControllerChanged() {
+ return BrowserUtils.promiseObserved("main-media-controller-changed");
+}
+
+/**
+ * Wait until any media controller updates its metadata even if it's not the
+ * main controller. The difference between this function and
+ * `waitUntilDisplayedMetadataChanged()` is that the changed metadata might come
+ * from non-main controller so it won't be show on the virtual control
+ * interface. we would observe that by listening for
+ * `media-session-controller-metadata-changed` notification.
+ *
+ * @return {Promise}
+ * Resolve when observing `media-session-controller-metadata-changed`
+ */
+function waitUntilControllerMetadataChanged() {
+ return BrowserUtils.promiseObserved(
+ "media-session-controller-metadata-changed"
+ );
+}
+
+/**
+ * Wait until media controller amount changes, we would observe that by
+ * listening for `media-controller-amount-changed` notification.
+ *
+ * @return {Promise}
+ * Resolve when observing `media-controller-amount-changed`
+ */
+function waitUntilMediaControllerAmountChanged() {
+ return BrowserUtils.promiseObserved("media-controller-amount-changed");
+}
+
+/**
+ * check if the media controll from given tab is active. If not, return a
+ * promise and resolve it when controller become active.
+ */
+async function checkOrWaitUntilControllerBecomeActive(tab) {
+ const controller = tab.linkedBrowser.browsingContext.mediaController;
+ if (controller.isActive) {
+ return;
+ }
+ await new Promise(r => (controller.onactivated = r));
+}
diff --git a/dom/media/mediacontrol/tests/gtest/MediaKeyListenerTest.h b/dom/media/mediacontrol/tests/gtest/MediaKeyListenerTest.h
new file mode 100644
index 0000000000..5145eb7dbb
--- /dev/null
+++ b/dom/media/mediacontrol/tests/gtest/MediaKeyListenerTest.h
@@ -0,0 +1,39 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_MEDIAKEYLISTENERTEST_H_
+#define DOM_MEDIA_MEDIAKEYLISTENERTEST_H_
+
+#include "MediaControlKeySource.h"
+#include "mozilla/Maybe.h"
+
+namespace mozilla {
+namespace dom {
+
+class MediaKeyListenerTest : public MediaControlKeyListener {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(MediaKeyListenerTest, override)
+
+ void Clear() { mReceivedKey = mozilla::Nothing(); }
+
+ void OnActionPerformed(const MediaControlAction& aAction) override {
+ mReceivedKey = mozilla::Some(aAction.mKey);
+ }
+ bool IsResultEqualTo(MediaControlKey aResult) const {
+ if (mReceivedKey) {
+ return *mReceivedKey == aResult;
+ }
+ return false;
+ }
+ bool IsReceivedResult() const { return mReceivedKey.isSome(); }
+
+ private:
+ ~MediaKeyListenerTest() = default;
+ mozilla::Maybe<MediaControlKey> mReceivedKey;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // DOM_MEDIA_MEDIAKEYLISTENERTEST_H_
diff --git a/dom/media/mediacontrol/tests/gtest/TestAudioFocusManager.cpp b/dom/media/mediacontrol/tests/gtest/TestAudioFocusManager.cpp
new file mode 100644
index 0000000000..df1bb288a4
--- /dev/null
+++ b/dom/media/mediacontrol/tests/gtest/TestAudioFocusManager.cpp
@@ -0,0 +1,163 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "AudioFocusManager.h"
+#include "MediaControlService.h"
+#include "mozilla/Preferences.h"
+
+using namespace mozilla::dom;
+
+#define FIRST_CONTROLLER_ID 0
+#define SECOND_CONTROLLER_ID 1
+
+// This RAII class is used to set the audio focus management pref within a test
+// and automatically revert the change when a test ends, in order not to
+// interfere other tests unexpectedly.
+class AudioFocusManagmentPrefSetterRAII {
+ public:
+ explicit AudioFocusManagmentPrefSetterRAII(bool aPrefValue) {
+ mOriginalValue = mozilla::Preferences::GetBool(mPrefName, false);
+ mozilla::Preferences::SetBool(mPrefName, aPrefValue);
+ }
+ ~AudioFocusManagmentPrefSetterRAII() {
+ mozilla::Preferences::SetBool(mPrefName, mOriginalValue);
+ }
+
+ private:
+ const char* mPrefName = "media.audioFocus.management";
+ bool mOriginalValue;
+};
+
+TEST(AudioFocusManager, TestRequestAudioFocus)
+{
+ AudioFocusManager manager;
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 0);
+
+ RefPtr<MediaController> controller = new MediaController(FIRST_CONTROLLER_ID);
+
+ manager.RequestAudioFocus(controller);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 1);
+
+ manager.RevokeAudioFocus(controller);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 0);
+}
+
+TEST(AudioFocusManager, TestAudioFocusNumsWhenEnableAudioFocusManagement)
+{
+ // When enabling audio focus management, we only allow one controller owing
+ // audio focus at a time when the audio competing occurs. As the mechanism of
+ // handling the audio competing involves multiple components, we can't test it
+ // simply by using the APIs from AudioFocusManager.
+ AudioFocusManagmentPrefSetterRAII prefSetter(true);
+
+ AudioFocusManager manager;
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 0);
+
+ RefPtr<MediaController> controller1 =
+ new MediaController(FIRST_CONTROLLER_ID);
+
+ RefPtr<MediaController> controller2 =
+ new MediaController(SECOND_CONTROLLER_ID);
+
+ manager.RequestAudioFocus(controller1);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 1);
+
+ // When controller2 starts, it would win the audio focus from controller1. So
+ // only one audio focus would exist.
+ manager.RequestAudioFocus(controller2);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 1);
+
+ manager.RevokeAudioFocus(controller2);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 0);
+}
+
+TEST(AudioFocusManager, TestAudioFocusNumsWhenDisableAudioFocusManagement)
+{
+ // When disabling audio focus management, we won't handle the audio competing,
+ // so we allow multiple audio focus existing at the same time.
+ AudioFocusManagmentPrefSetterRAII prefSetter(false);
+
+ AudioFocusManager manager;
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 0);
+
+ RefPtr<MediaController> controller1 =
+ new MediaController(FIRST_CONTROLLER_ID);
+
+ RefPtr<MediaController> controller2 =
+ new MediaController(SECOND_CONTROLLER_ID);
+
+ manager.RequestAudioFocus(controller1);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 1);
+
+ manager.RequestAudioFocus(controller2);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 2);
+
+ manager.RevokeAudioFocus(controller1);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 1);
+
+ manager.RevokeAudioFocus(controller2);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 0);
+}
+
+TEST(AudioFocusManager, TestRequestAudioFocusRepeatedly)
+{
+ AudioFocusManager manager;
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 0);
+
+ RefPtr<MediaController> controller = new MediaController(FIRST_CONTROLLER_ID);
+
+ manager.RequestAudioFocus(controller);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 1);
+
+ manager.RequestAudioFocus(controller);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 1);
+}
+
+TEST(AudioFocusManager, TestRevokeAudioFocusRepeatedly)
+{
+ AudioFocusManager manager;
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 0);
+
+ RefPtr<MediaController> controller = new MediaController(FIRST_CONTROLLER_ID);
+
+ manager.RequestAudioFocus(controller);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 1);
+
+ manager.RevokeAudioFocus(controller);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 0);
+
+ manager.RevokeAudioFocus(controller);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 0);
+}
+
+TEST(AudioFocusManager, TestRevokeAudioFocusWithoutRequestAudioFocus)
+{
+ AudioFocusManager manager;
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 0);
+
+ RefPtr<MediaController> controller = new MediaController(FIRST_CONTROLLER_ID);
+
+ manager.RevokeAudioFocus(controller);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 0);
+}
+
+TEST(AudioFocusManager,
+ TestRevokeAudioFocusForControllerWithoutOwningAudioFocus)
+{
+ AudioFocusManager manager;
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 0);
+
+ RefPtr<MediaController> controller1 =
+ new MediaController(FIRST_CONTROLLER_ID);
+
+ RefPtr<MediaController> controller2 =
+ new MediaController(SECOND_CONTROLLER_ID);
+
+ manager.RequestAudioFocus(controller1);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 1);
+
+ manager.RevokeAudioFocus(controller2);
+ ASSERT_TRUE(manager.GetAudioFocusNums() == 1);
+}
diff --git a/dom/media/mediacontrol/tests/gtest/TestMediaControlService.cpp b/dom/media/mediacontrol/tests/gtest/TestMediaControlService.cpp
new file mode 100644
index 0000000000..ab5b0cb8c7
--- /dev/null
+++ b/dom/media/mediacontrol/tests/gtest/TestMediaControlService.cpp
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "MediaControlService.h"
+#include "MediaController.h"
+
+using namespace mozilla::dom;
+
+#define FIRST_CONTROLLER_ID 0
+#define SECOND_CONTROLLER_ID 1
+
+TEST(MediaControlService, TestAddOrRemoveControllers)
+{
+ RefPtr<MediaControlService> service = MediaControlService::GetService();
+ ASSERT_TRUE(service->GetActiveControllersNum() == 0);
+
+ RefPtr<MediaController> controller1 =
+ new MediaController(FIRST_CONTROLLER_ID);
+ RefPtr<MediaController> controller2 =
+ new MediaController(SECOND_CONTROLLER_ID);
+
+ service->RegisterActiveMediaController(controller1);
+ ASSERT_TRUE(service->GetActiveControllersNum() == 1);
+
+ service->RegisterActiveMediaController(controller2);
+ ASSERT_TRUE(service->GetActiveControllersNum() == 2);
+
+ service->UnregisterActiveMediaController(controller1);
+ ASSERT_TRUE(service->GetActiveControllersNum() == 1);
+
+ service->UnregisterActiveMediaController(controller2);
+ ASSERT_TRUE(service->GetActiveControllersNum() == 0);
+}
+
+TEST(MediaControlService, TestMainController)
+{
+ RefPtr<MediaControlService> service = MediaControlService::GetService();
+ ASSERT_TRUE(service->GetActiveControllersNum() == 0);
+
+ RefPtr<MediaController> controller1 =
+ new MediaController(FIRST_CONTROLLER_ID);
+ service->RegisterActiveMediaController(controller1);
+
+ RefPtr<MediaController> mainController = service->GetMainController();
+ ASSERT_TRUE(mainController->Id() == FIRST_CONTROLLER_ID);
+
+ RefPtr<MediaController> controller2 =
+ new MediaController(SECOND_CONTROLLER_ID);
+ service->RegisterActiveMediaController(controller2);
+
+ mainController = service->GetMainController();
+ ASSERT_TRUE(mainController->Id() == SECOND_CONTROLLER_ID);
+
+ service->UnregisterActiveMediaController(controller2);
+ mainController = service->GetMainController();
+ ASSERT_TRUE(mainController->Id() == FIRST_CONTROLLER_ID);
+
+ service->UnregisterActiveMediaController(controller1);
+ mainController = service->GetMainController();
+ ASSERT_TRUE(service->GetActiveControllersNum() == 0);
+ ASSERT_TRUE(!mainController);
+}
diff --git a/dom/media/mediacontrol/tests/gtest/TestMediaController.cpp b/dom/media/mediacontrol/tests/gtest/TestMediaController.cpp
new file mode 100644
index 0000000000..5ba8d2a7d5
--- /dev/null
+++ b/dom/media/mediacontrol/tests/gtest/TestMediaController.cpp
@@ -0,0 +1,204 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "MediaControlService.h"
+#include "MediaController.h"
+#include "mozilla/dom/MediaSessionBinding.h"
+
+using namespace mozilla::dom;
+
+#define CONTROLLER_ID 0
+#define FAKE_CONTEXT_ID 0
+
+#define FIRST_CONTROLLER_ID 0
+
+TEST(MediaController, DefaultValueCheck)
+{
+ RefPtr<MediaController> controller = new MediaController(CONTROLLER_ID);
+ ASSERT_TRUE(!controller->IsAnyMediaBeingControlled());
+ ASSERT_TRUE(controller->Id() == CONTROLLER_ID);
+ ASSERT_TRUE(controller->PlaybackState() == MediaSessionPlaybackState::None);
+ ASSERT_TRUE(!controller->IsAudible());
+}
+
+TEST(MediaController, IsAnyMediaBeingControlled)
+{
+ RefPtr<MediaController> controller = new MediaController(CONTROLLER_ID);
+ ASSERT_TRUE(!controller->IsAnyMediaBeingControlled());
+
+ controller->NotifyMediaPlaybackChanged(FAKE_CONTEXT_ID,
+ MediaPlaybackState::eStarted);
+ ASSERT_TRUE(controller->IsAnyMediaBeingControlled());
+
+ controller->NotifyMediaPlaybackChanged(FAKE_CONTEXT_ID,
+ MediaPlaybackState::eStarted);
+ ASSERT_TRUE(controller->IsAnyMediaBeingControlled());
+
+ controller->NotifyMediaPlaybackChanged(FAKE_CONTEXT_ID,
+ MediaPlaybackState::eStopped);
+ ASSERT_TRUE(controller->IsAnyMediaBeingControlled());
+
+ controller->NotifyMediaPlaybackChanged(FAKE_CONTEXT_ID,
+ MediaPlaybackState::eStopped);
+ ASSERT_TRUE(!controller->IsAnyMediaBeingControlled());
+}
+
+class FakeControlledMedia final {
+ public:
+ explicit FakeControlledMedia(MediaController* aController)
+ : mController(aController) {
+ mController->NotifyMediaPlaybackChanged(FAKE_CONTEXT_ID,
+ MediaPlaybackState::eStarted);
+ }
+
+ void SetPlaying(MediaPlaybackState aState) {
+ if (mPlaybackState == aState) {
+ return;
+ }
+ mController->NotifyMediaPlaybackChanged(FAKE_CONTEXT_ID, aState);
+ mPlaybackState = aState;
+ }
+
+ void SetAudible(MediaAudibleState aState) {
+ if (mAudibleState == aState) {
+ return;
+ }
+ mController->NotifyMediaAudibleChanged(FAKE_CONTEXT_ID, aState);
+ mAudibleState = aState;
+ }
+
+ ~FakeControlledMedia() {
+ if (mPlaybackState == MediaPlaybackState::ePlayed) {
+ mController->NotifyMediaPlaybackChanged(FAKE_CONTEXT_ID,
+ MediaPlaybackState::ePaused);
+ }
+ mController->NotifyMediaPlaybackChanged(FAKE_CONTEXT_ID,
+ MediaPlaybackState::eStopped);
+ }
+
+ private:
+ MediaPlaybackState mPlaybackState = MediaPlaybackState::eStopped;
+ MediaAudibleState mAudibleState = MediaAudibleState::eInaudible;
+ RefPtr<MediaController> mController;
+};
+
+TEST(MediaController, ActiveAndDeactiveController)
+{
+ RefPtr<MediaControlService> service = MediaControlService::GetService();
+ ASSERT_TRUE(service->GetActiveControllersNum() == 0);
+
+ RefPtr<MediaController> controller = new MediaController(FIRST_CONTROLLER_ID);
+
+ // In order to check active control number after FakeControlledMedia
+ // destroyed.
+ {
+ FakeControlledMedia fakeMedia(controller);
+ fakeMedia.SetPlaying(MediaPlaybackState::ePlayed);
+ ASSERT_TRUE(service->GetActiveControllersNum() == 1);
+
+ fakeMedia.SetAudible(MediaAudibleState::eAudible);
+ ASSERT_TRUE(service->GetActiveControllersNum() == 1);
+
+ fakeMedia.SetAudible(MediaAudibleState::eInaudible);
+ ASSERT_TRUE(service->GetActiveControllersNum() == 1);
+ }
+
+ ASSERT_TRUE(service->GetActiveControllersNum() == 0);
+}
+
+TEST(MediaController, AudibleChanged)
+{
+ RefPtr<MediaController> controller = new MediaController(CONTROLLER_ID);
+
+ FakeControlledMedia fakeMedia(controller);
+ fakeMedia.SetPlaying(MediaPlaybackState::ePlayed);
+ ASSERT_TRUE(!controller->IsAudible());
+
+ fakeMedia.SetAudible(MediaAudibleState::eAudible);
+ ASSERT_TRUE(controller->IsAudible());
+
+ fakeMedia.SetAudible(MediaAudibleState::eInaudible);
+ ASSERT_TRUE(!controller->IsAudible());
+}
+
+TEST(MediaController, PlayingStateChangeViaControlledMedia)
+{
+ RefPtr<MediaController> controller = new MediaController(CONTROLLER_ID);
+
+ // In order to check playing state after FakeControlledMedia destroyed.
+ {
+ FakeControlledMedia foo(controller);
+ ASSERT_TRUE(controller->PlaybackState() == MediaSessionPlaybackState::None);
+
+ foo.SetPlaying(MediaPlaybackState::ePlayed);
+ ASSERT_TRUE(controller->PlaybackState() ==
+ MediaSessionPlaybackState::Playing);
+
+ foo.SetPlaying(MediaPlaybackState::ePaused);
+ ASSERT_TRUE(controller->PlaybackState() ==
+ MediaSessionPlaybackState::Paused);
+
+ foo.SetPlaying(MediaPlaybackState::ePlayed);
+ ASSERT_TRUE(controller->PlaybackState() ==
+ MediaSessionPlaybackState::Playing);
+ }
+
+ // FakeControlledMedia has been destroyed, no playing media exists.
+ ASSERT_TRUE(controller->PlaybackState() == MediaSessionPlaybackState::Paused);
+}
+
+TEST(MediaController, ControllerShouldRemainPlayingIfAnyPlayingMediaExists)
+{
+ RefPtr<MediaController> controller = new MediaController(CONTROLLER_ID);
+
+ {
+ FakeControlledMedia foo(controller);
+ ASSERT_TRUE(controller->PlaybackState() == MediaSessionPlaybackState::None);
+
+ foo.SetPlaying(MediaPlaybackState::ePlayed);
+ ASSERT_TRUE(controller->PlaybackState() ==
+ MediaSessionPlaybackState::Playing);
+
+ // foo is playing, so controller is in `playing` state.
+ FakeControlledMedia bar(controller);
+ ASSERT_TRUE(controller->PlaybackState() ==
+ MediaSessionPlaybackState::Playing);
+
+ bar.SetPlaying(MediaPlaybackState::ePlayed);
+ ASSERT_TRUE(controller->PlaybackState() ==
+ MediaSessionPlaybackState::Playing);
+
+ // Although we paused bar, but foo is still playing, so the controller would
+ // still be in `playing`.
+ bar.SetPlaying(MediaPlaybackState::ePaused);
+ ASSERT_TRUE(controller->PlaybackState() ==
+ MediaSessionPlaybackState::Playing);
+
+ foo.SetPlaying(MediaPlaybackState::ePaused);
+ ASSERT_TRUE(controller->PlaybackState() ==
+ MediaSessionPlaybackState::Paused);
+ }
+
+ // both foo and bar have been destroyed, no playing media exists.
+ ASSERT_TRUE(controller->PlaybackState() == MediaSessionPlaybackState::Paused);
+}
+
+TEST(MediaController, PictureInPictureModeOrFullscreen)
+{
+ RefPtr<MediaController> controller = new MediaController(CONTROLLER_ID);
+ ASSERT_TRUE(!controller->IsBeingUsedInPIPModeOrFullscreen());
+
+ controller->SetIsInPictureInPictureMode(FAKE_CONTEXT_ID, true);
+ ASSERT_TRUE(controller->IsBeingUsedInPIPModeOrFullscreen());
+
+ controller->SetIsInPictureInPictureMode(FAKE_CONTEXT_ID, false);
+ ASSERT_TRUE(!controller->IsBeingUsedInPIPModeOrFullscreen());
+
+ controller->NotifyMediaFullScreenState(FAKE_CONTEXT_ID, true);
+ ASSERT_TRUE(controller->IsBeingUsedInPIPModeOrFullscreen());
+
+ controller->NotifyMediaFullScreenState(FAKE_CONTEXT_ID, false);
+ ASSERT_TRUE(!controller->IsBeingUsedInPIPModeOrFullscreen());
+}
diff --git a/dom/media/mediacontrol/tests/gtest/TestMediaKeysEvent.cpp b/dom/media/mediacontrol/tests/gtest/TestMediaKeysEvent.cpp
new file mode 100644
index 0000000000..a73ceb8592
--- /dev/null
+++ b/dom/media/mediacontrol/tests/gtest/TestMediaKeysEvent.cpp
@@ -0,0 +1,49 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "MediaController.h"
+#include "MediaControlKeySource.h"
+
+using namespace mozilla::dom;
+
+class MediaControlKeySourceTestImpl : public MediaControlKeySource {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(MediaControlKeySourceTestImpl, override)
+ bool Open() override { return true; }
+ bool IsOpened() const override { return true; }
+ void SetSupportedMediaKeys(const MediaKeysArray& aSupportedKeys) override {}
+
+ private:
+ ~MediaControlKeySourceTestImpl() = default;
+};
+
+TEST(MediaControlKey, TestAddOrRemoveListener)
+{
+ RefPtr<MediaControlKeySource> source = new MediaControlKeySourceTestImpl();
+ ASSERT_TRUE(source->GetListenersNum() == 0);
+
+ RefPtr<MediaControlKeyListener> listener = new MediaControlKeyHandler();
+
+ source->AddListener(listener);
+ ASSERT_TRUE(source->GetListenersNum() == 1);
+
+ source->RemoveListener(listener);
+ ASSERT_TRUE(source->GetListenersNum() == 0);
+}
+
+TEST(MediaControlKey, SetSourcePlaybackState)
+{
+ RefPtr<MediaControlKeySource> source = new MediaControlKeySourceTestImpl();
+ ASSERT_TRUE(source->GetPlaybackState() == MediaSessionPlaybackState::None);
+
+ source->SetPlaybackState(MediaSessionPlaybackState::Playing);
+ ASSERT_TRUE(source->GetPlaybackState() == MediaSessionPlaybackState::Playing);
+
+ source->SetPlaybackState(MediaSessionPlaybackState::Paused);
+ ASSERT_TRUE(source->GetPlaybackState() == MediaSessionPlaybackState::Paused);
+
+ source->SetPlaybackState(MediaSessionPlaybackState::None);
+ ASSERT_TRUE(source->GetPlaybackState() == MediaSessionPlaybackState::None);
+}
diff --git a/dom/media/mediacontrol/tests/gtest/TestMediaKeysEventMac.mm b/dom/media/mediacontrol/tests/gtest/TestMediaKeysEventMac.mm
new file mode 100644
index 0000000000..3c8ce8cd9e
--- /dev/null
+++ b/dom/media/mediacontrol/tests/gtest/TestMediaKeysEventMac.mm
@@ -0,0 +1,136 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import <AppKit/AppKit.h>
+#import <AppKit/NSEvent.h>
+#import <ApplicationServices/ApplicationServices.h>
+#import <CoreFoundation/CoreFoundation.h>
+#import <IOKit/hidsystem/ev_keymap.h>
+
+#include "gtest/gtest.h"
+#include "MediaHardwareKeysEventSourceMac.h"
+#include "MediaKeyListenerTest.h"
+#include "mozilla/Maybe.h"
+
+using namespace mozilla::dom;
+using namespace mozilla::widget;
+
+static const int kSystemDefinedEventMediaKeysSubtype = 8;
+
+static void SendFakeEvent(RefPtr<MediaHardwareKeysEventSourceMac>& aSource, int aKeyData) {
+ NSEvent* event = [NSEvent otherEventWithType:NSEventTypeSystemDefined
+ location:NSZeroPoint
+ modifierFlags:0
+ timestamp:0
+ windowNumber:0
+ context:nil
+ subtype:kSystemDefinedEventMediaKeysSubtype
+ data1:aKeyData
+ data2:0];
+ aSource->EventTapCallback(nullptr, static_cast<CGEventType>(0), [event CGEvent], aSource.get());
+}
+
+static void NotifyFakeNonMediaKey(RefPtr<MediaHardwareKeysEventSourceMac>& aSource,
+ bool aIsKeyPressed) {
+ int keyData = 0 | ((aIsKeyPressed ? 0xA : 0xB) << 8);
+ SendFakeEvent(aSource, keyData);
+}
+
+static void NotifyFakeMediaControlKey(RefPtr<MediaHardwareKeysEventSourceMac>& aSource,
+ MediaControlKey aEvent, bool aIsKeyPressed) {
+ int keyData = 0;
+ if (aEvent == MediaControlKey::Playpause) {
+ keyData = NX_KEYTYPE_PLAY << 16;
+ } else if (aEvent == MediaControlKey::Nexttrack) {
+ keyData = NX_KEYTYPE_NEXT << 16;
+ } else if (aEvent == MediaControlKey::Previoustrack) {
+ keyData = NX_KEYTYPE_PREVIOUS << 16;
+ }
+ keyData |= ((aIsKeyPressed ? 0xA : 0xB) << 8);
+ SendFakeEvent(aSource, keyData);
+}
+
+static void NotifyKeyPressedMediaKey(RefPtr<MediaHardwareKeysEventSourceMac>& aSource,
+ MediaControlKey aEvent) {
+ NotifyFakeMediaControlKey(aSource, aEvent, true /* key pressed */);
+}
+
+static void NotifyKeyReleasedMediaKeysEvent(RefPtr<MediaHardwareKeysEventSourceMac>& aSource,
+ MediaControlKey aEvent) {
+ NotifyFakeMediaControlKey(aSource, aEvent, false /* key released */);
+}
+
+static void NotifyKeyPressedNonMediaKeysEvents(RefPtr<MediaHardwareKeysEventSourceMac>& aSource) {
+ NotifyFakeNonMediaKey(aSource, true /* key pressed */);
+}
+
+static void NotifyKeyReleasedNonMediaKeysEvents(RefPtr<MediaHardwareKeysEventSourceMac>& aSource) {
+ NotifyFakeNonMediaKey(aSource, false /* key released */);
+}
+
+TEST(MediaHardwareKeysEventSourceMac, TestKeyPressedMediaKeysEvent)
+{
+ RefPtr<MediaHardwareKeysEventSourceMac> source = new MediaHardwareKeysEventSourceMac();
+ ASSERT_TRUE(source->GetListenersNum() == 0);
+
+ RefPtr<MediaKeyListenerTest> listener = new MediaKeyListenerTest();
+ source->AddListener(listener.get());
+ ASSERT_TRUE(source->GetListenersNum() == 1);
+ ASSERT_TRUE(!listener->IsReceivedResult());
+
+ NotifyKeyPressedMediaKey(source, MediaControlKey::Playpause);
+ ASSERT_TRUE(listener->IsResultEqualTo(MediaControlKey::Playpause));
+
+ NotifyKeyPressedMediaKey(source, MediaControlKey::Nexttrack);
+ ASSERT_TRUE(listener->IsResultEqualTo(MediaControlKey::Nexttrack));
+
+ NotifyKeyPressedMediaKey(source, MediaControlKey::Previoustrack);
+ ASSERT_TRUE(listener->IsResultEqualTo(MediaControlKey::Previoustrack));
+
+ source->RemoveListener(listener);
+ ASSERT_TRUE(source->GetListenersNum() == 0);
+}
+
+TEST(MediaHardwareKeysEventSourceMac, TestKeyReleasedMediaKeysEvent)
+{
+ RefPtr<MediaHardwareKeysEventSourceMac> source = new MediaHardwareKeysEventSourceMac();
+ ASSERT_TRUE(source->GetListenersNum() == 0);
+
+ RefPtr<MediaKeyListenerTest> listener = new MediaKeyListenerTest();
+ source->AddListener(listener.get());
+ ASSERT_TRUE(source->GetListenersNum() == 1);
+ ASSERT_TRUE(!listener->IsReceivedResult());
+
+ NotifyKeyReleasedMediaKeysEvent(source, MediaControlKey::Playpause);
+ ASSERT_TRUE(!listener->IsReceivedResult());
+
+ NotifyKeyReleasedMediaKeysEvent(source, MediaControlKey::Nexttrack);
+ ASSERT_TRUE(!listener->IsReceivedResult());
+
+ NotifyKeyReleasedMediaKeysEvent(source, MediaControlKey::Previoustrack);
+ ASSERT_TRUE(!listener->IsReceivedResult());
+
+ source->RemoveListener(listener);
+ ASSERT_TRUE(source->GetListenersNum() == 0);
+}
+
+TEST(MediaHardwareKeysEventSourceMac, TestNonMediaKeysEvent)
+{
+ RefPtr<MediaHardwareKeysEventSourceMac> source = new MediaHardwareKeysEventSourceMac();
+ ASSERT_TRUE(source->GetListenersNum() == 0);
+
+ RefPtr<MediaKeyListenerTest> listener = new MediaKeyListenerTest();
+ source->AddListener(listener.get());
+ ASSERT_TRUE(source->GetListenersNum() == 1);
+ ASSERT_TRUE(!listener->IsReceivedResult());
+
+ NotifyKeyPressedNonMediaKeysEvents(source);
+ ASSERT_TRUE(!listener->IsReceivedResult());
+
+ NotifyKeyReleasedNonMediaKeysEvents(source);
+ ASSERT_TRUE(!listener->IsReceivedResult());
+
+ source->RemoveListener(listener);
+ ASSERT_TRUE(source->GetListenersNum() == 0);
+}
diff --git a/dom/media/mediacontrol/tests/gtest/TestMediaKeysEventMediaCenter.mm b/dom/media/mediacontrol/tests/gtest/TestMediaKeysEventMediaCenter.mm
new file mode 100644
index 0000000000..1d32960d2f
--- /dev/null
+++ b/dom/media/mediacontrol/tests/gtest/TestMediaKeysEventMediaCenter.mm
@@ -0,0 +1,166 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#import <MediaPlayer/MediaPlayer.h>
+
+#include "gtest/gtest.h"
+#include "MediaHardwareKeysEventSourceMacMediaCenter.h"
+#include "MediaKeyListenerTest.h"
+#include "nsCocoaFeatures.h"
+#include "nsCocoaUtils.h"
+#include "prinrval.h"
+#include "prthread.h"
+
+using namespace mozilla::dom;
+using namespace mozilla::widget;
+
+NS_ASSUME_NONNULL_BEGIN
+
+TEST(MediaHardwareKeysEventSourceMacMediaCenter, TestMediaCenterPlayPauseEvent)
+{
+ RefPtr<MediaHardwareKeysEventSourceMacMediaCenter> source =
+ new MediaHardwareKeysEventSourceMacMediaCenter();
+
+ ASSERT_TRUE(source->GetListenersNum() == 0);
+
+ RefPtr<MediaKeyListenerTest> listener = new MediaKeyListenerTest();
+
+ MPNowPlayingInfoCenter* center = [MPNowPlayingInfoCenter defaultCenter];
+
+ source->AddListener(listener.get());
+
+ ASSERT_TRUE(source->Open());
+
+ ASSERT_TRUE(source->GetListenersNum() == 1);
+ ASSERT_TRUE(!listener->IsReceivedResult());
+ ASSERT_TRUE(center.playbackState == MPNowPlayingPlaybackStatePlaying);
+
+ MediaCenterEventHandler playPauseHandler = source->CreatePlayPauseHandler();
+ playPauseHandler(nil);
+
+ ASSERT_TRUE(center.playbackState == MPNowPlayingPlaybackStatePaused);
+ ASSERT_TRUE(listener->IsResultEqualTo(MediaControlKey::Playpause));
+
+ listener->Clear(); // Reset stored media key
+
+ playPauseHandler(nil);
+
+ ASSERT_TRUE(center.playbackState == MPNowPlayingPlaybackStatePlaying);
+ ASSERT_TRUE(listener->IsResultEqualTo(MediaControlKey::Playpause));
+}
+
+TEST(MediaHardwareKeysEventSourceMacMediaCenter, TestMediaCenterPlayEvent)
+{
+ RefPtr<MediaHardwareKeysEventSourceMacMediaCenter> source =
+ new MediaHardwareKeysEventSourceMacMediaCenter();
+
+ ASSERT_TRUE(source->GetListenersNum() == 0);
+
+ RefPtr<MediaKeyListenerTest> listener = new MediaKeyListenerTest();
+
+ MPNowPlayingInfoCenter* center = [MPNowPlayingInfoCenter defaultCenter];
+
+ source->AddListener(listener.get());
+
+ ASSERT_TRUE(source->Open());
+
+ ASSERT_TRUE(source->GetListenersNum() == 1);
+ ASSERT_TRUE(!listener->IsReceivedResult());
+ ASSERT_TRUE(center.playbackState == MPNowPlayingPlaybackStatePlaying);
+
+ MediaCenterEventHandler playHandler = source->CreatePlayHandler();
+
+ center.playbackState = MPNowPlayingPlaybackStatePaused;
+
+ playHandler(nil);
+
+ ASSERT_TRUE(center.playbackState == MPNowPlayingPlaybackStatePlaying);
+ ASSERT_TRUE(listener->IsResultEqualTo(MediaControlKey::Play));
+}
+
+TEST(MediaHardwareKeysEventSourceMacMediaCenter, TestMediaCenterPauseEvent)
+{
+ RefPtr<MediaHardwareKeysEventSourceMacMediaCenter> source =
+ new MediaHardwareKeysEventSourceMacMediaCenter();
+
+ ASSERT_TRUE(source->GetListenersNum() == 0);
+
+ RefPtr<MediaKeyListenerTest> listener = new MediaKeyListenerTest();
+
+ MPNowPlayingInfoCenter* center = [MPNowPlayingInfoCenter defaultCenter];
+
+ source->AddListener(listener.get());
+
+ ASSERT_TRUE(source->Open());
+
+ ASSERT_TRUE(source->GetListenersNum() == 1);
+ ASSERT_TRUE(!listener->IsReceivedResult());
+ ASSERT_TRUE(center.playbackState == MPNowPlayingPlaybackStatePlaying);
+
+ MediaCenterEventHandler pauseHandler = source->CreatePauseHandler();
+
+ pauseHandler(nil);
+
+ ASSERT_TRUE(center.playbackState == MPNowPlayingPlaybackStatePaused);
+ ASSERT_TRUE(listener->IsResultEqualTo(MediaControlKey::Pause));
+}
+
+TEST(MediaHardwareKeysEventSourceMacMediaCenter, TestMediaCenterPrevNextEvent)
+{
+ RefPtr<MediaHardwareKeysEventSourceMacMediaCenter> source =
+ new MediaHardwareKeysEventSourceMacMediaCenter();
+
+ ASSERT_TRUE(source->GetListenersNum() == 0);
+
+ RefPtr<MediaKeyListenerTest> listener = new MediaKeyListenerTest();
+
+ source->AddListener(listener.get());
+
+ ASSERT_TRUE(source->Open());
+
+ MediaCenterEventHandler nextHandler = source->CreateNextTrackHandler();
+
+ nextHandler(nil);
+
+ ASSERT_TRUE(listener->IsResultEqualTo(MediaControlKey::Nexttrack));
+
+ MediaCenterEventHandler previousHandler = source->CreatePreviousTrackHandler();
+
+ previousHandler(nil);
+
+ ASSERT_TRUE(listener->IsResultEqualTo(MediaControlKey::Previoustrack));
+}
+
+TEST(MediaHardwareKeysEventSourceMacMediaCenter, TestSetMetadata)
+{
+ RefPtr<MediaHardwareKeysEventSourceMacMediaCenter> source =
+ new MediaHardwareKeysEventSourceMacMediaCenter();
+
+ ASSERT_TRUE(source->GetListenersNum() == 0);
+
+ RefPtr<MediaKeyListenerTest> listener = new MediaKeyListenerTest();
+
+ source->AddListener(listener.get());
+
+ ASSERT_TRUE(source->Open());
+
+ MediaMetadataBase metadata;
+ metadata.mTitle = u"MediaPlayback";
+ metadata.mArtist = u"Firefox";
+ metadata.mAlbum = u"Mozilla";
+ source->SetMediaMetadata(metadata);
+
+ // The update procedure of nowPlayingInfo is async, so wait for a second
+ // before checking the result.
+ PR_Sleep(PR_SecondsToInterval(1));
+ MPNowPlayingInfoCenter* center = [MPNowPlayingInfoCenter defaultCenter];
+ ASSERT_TRUE([center.nowPlayingInfo[MPMediaItemPropertyTitle] isEqualToString:@"MediaPlayback"]);
+ ASSERT_TRUE([center.nowPlayingInfo[MPMediaItemPropertyArtist] isEqualToString:@"Firefox"]);
+ ASSERT_TRUE([center.nowPlayingInfo[MPMediaItemPropertyAlbumTitle] isEqualToString:@"Mozilla"]);
+
+ source->Close();
+ PR_Sleep(PR_SecondsToInterval(1));
+ ASSERT_TRUE(center.nowPlayingInfo == nil);
+}
+
+NS_ASSUME_NONNULL_END
diff --git a/dom/media/mediacontrol/tests/gtest/moz.build b/dom/media/mediacontrol/tests/gtest/moz.build
new file mode 100644
index 0000000000..7043bfcd5e
--- /dev/null
+++ b/dom/media/mediacontrol/tests/gtest/moz.build
@@ -0,0 +1,23 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES += [
+ "TestAudioFocusManager.cpp",
+ "TestMediaController.cpp",
+ "TestMediaControlService.cpp",
+ "TestMediaKeysEvent.cpp",
+]
+
+if CONFIG["MOZ_APPLEMEDIA"]:
+ UNIFIED_SOURCES += ["TestMediaKeysEventMac.mm", "TestMediaKeysEventMediaCenter.mm"]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+LOCAL_INCLUDES += [
+ "/dom/media/mediacontrol",
+]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/dom/media/mediasession/MediaMetadata.cpp b/dom/media/mediasession/MediaMetadata.cpp
new file mode 100644
index 0000000000..8ee2c217ac
--- /dev/null
+++ b/dom/media/mediasession/MediaMetadata.cpp
@@ -0,0 +1,154 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/MediaMetadata.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/MediaSessionBinding.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/dom/ToJSValue.h"
+#include "nsNetUtil.h"
+
+namespace mozilla::dom {
+
+// Only needed for refcounted objects.
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(MediaMetadata, mParent)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaMetadata)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaMetadata)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaMetadata)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+MediaMetadata::MediaMetadata(nsIGlobalObject* aParent, const nsString& aTitle,
+ const nsString& aArtist, const nsString& aAlbum)
+ : MediaMetadataBase(aTitle, aArtist, aAlbum), mParent(aParent) {
+ MOZ_ASSERT(mParent);
+}
+
+nsIGlobalObject* MediaMetadata::GetParentObject() const { return mParent; }
+
+JSObject* MediaMetadata::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return MediaMetadata_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+already_AddRefed<MediaMetadata> MediaMetadata::Constructor(
+ const GlobalObject& aGlobal, const MediaMetadataInit& aInit,
+ ErrorResult& aRv) {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+ if (!global) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ RefPtr<MediaMetadata> mediaMetadata =
+ new MediaMetadata(global, aInit.mTitle, aInit.mArtist, aInit.mAlbum);
+ mediaMetadata->SetArtworkInternal(aInit.mArtwork, aRv);
+ return aRv.Failed() ? nullptr : mediaMetadata.forget();
+}
+
+void MediaMetadata::GetTitle(nsString& aRetVal) const { aRetVal = mTitle; }
+
+void MediaMetadata::SetTitle(const nsAString& aTitle) { mTitle = aTitle; }
+
+void MediaMetadata::GetArtist(nsString& aRetVal) const { aRetVal = mArtist; }
+
+void MediaMetadata::SetArtist(const nsAString& aArtist) { mArtist = aArtist; }
+
+void MediaMetadata::GetAlbum(nsString& aRetVal) const { aRetVal = mAlbum; }
+
+void MediaMetadata::SetAlbum(const nsAString& aAlbum) { mAlbum = aAlbum; }
+
+void MediaMetadata::GetArtwork(JSContext* aCx, nsTArray<JSObject*>& aRetVal,
+ ErrorResult& aRv) const {
+ // Convert the MediaImages to JS Objects
+ if (!aRetVal.SetCapacity(mArtwork.Length(), fallible)) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ for (size_t i = 0; i < mArtwork.Length(); ++i) {
+ JS::Rooted<JS::Value> value(aCx);
+ if (!ToJSValue(aCx, mArtwork[i], &value)) {
+ aRv.NoteJSContextException(aCx);
+ return;
+ }
+
+ JS::Rooted<JSObject*> object(aCx, &value.toObject());
+ if (!JS_FreezeObject(aCx, object)) {
+ aRv.NoteJSContextException(aCx);
+ return;
+ }
+
+ aRetVal.AppendElement(object);
+ }
+}
+
+void MediaMetadata::SetArtwork(JSContext* aCx,
+ const Sequence<JSObject*>& aArtwork,
+ ErrorResult& aRv) {
+ // Convert the JS Objects to MediaImages
+ Sequence<MediaImage> artwork;
+ if (!artwork.SetCapacity(aArtwork.Length(), fallible)) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ for (JSObject* object : aArtwork) {
+ JS::Rooted<JS::Value> value(aCx, JS::ObjectValue(*object));
+
+ MediaImage* image = artwork.AppendElement(fallible);
+ MOZ_ASSERT(image, "The capacity is preallocated");
+ if (!image->Init(aCx, value)) {
+ aRv.NoteJSContextException(aCx);
+ return;
+ }
+ }
+
+ SetArtworkInternal(artwork, aRv);
+};
+
+static nsIURI* GetEntryBaseURL() {
+ nsCOMPtr<Document> doc = GetEntryDocument();
+ return doc ? doc->GetDocBaseURI() : nullptr;
+}
+
+// `aURL` is an inout parameter.
+static nsresult ResolveURL(nsString& aURL, nsIURI* aBaseURI) {
+ nsCOMPtr<nsIURI> uri;
+ nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL,
+ /* UTF-8 for charset */ nullptr, aBaseURI);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ nsAutoCString spec;
+ rv = uri->GetSpec(spec);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ CopyUTF8toUTF16(spec, aURL);
+ return NS_OK;
+}
+
+void MediaMetadata::SetArtworkInternal(const Sequence<MediaImage>& aArtwork,
+ ErrorResult& aRv) {
+ nsTArray<MediaImage> artwork;
+ artwork.Assign(aArtwork);
+
+ nsCOMPtr<nsIURI> baseURI = GetEntryBaseURL();
+ for (MediaImage& image : artwork) {
+ nsresult rv = ResolveURL(image.mSrc, baseURI);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aRv.ThrowTypeError<MSG_INVALID_URL>(NS_ConvertUTF16toUTF8(image.mSrc));
+ return;
+ }
+ }
+ mArtwork = std::move(artwork);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/mediasession/MediaMetadata.h b/dom/media/mediasession/MediaMetadata.h
new file mode 100644
index 0000000000..6c552e6465
--- /dev/null
+++ b/dom/media/mediasession/MediaMetadata.h
@@ -0,0 +1,97 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_MediaMetadata_h
+#define mozilla_dom_MediaMetadata_h
+
+#include "js/TypeDecls.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/MediaSessionBinding.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsWrapperCache.h"
+
+class nsIGlobalObject;
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+
+class MediaMetadataBase {
+ public:
+ MediaMetadataBase() = default;
+ MediaMetadataBase(const nsString& aTitle, const nsString& aArtist,
+ const nsString& aAlbum)
+ : mTitle(aTitle), mArtist(aArtist), mAlbum(aAlbum) {}
+
+ static MediaMetadataBase EmptyData() { return MediaMetadataBase(); }
+
+ nsString mTitle;
+ nsString mArtist;
+ nsString mAlbum;
+ CopyableTArray<MediaImage> mArtwork;
+};
+
+class MediaMetadata final : public nsISupports,
+ public nsWrapperCache,
+ private MediaMetadataBase {
+ public:
+ // Ref counting and cycle collection
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(MediaMetadata)
+
+ // WebIDL methods
+ nsIGlobalObject* GetParentObject() const;
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ static already_AddRefed<MediaMetadata> Constructor(
+ const GlobalObject& aGlobal, const MediaMetadataInit& aInit,
+ ErrorResult& aRv);
+
+ void GetTitle(nsString& aRetVal) const;
+
+ void SetTitle(const nsAString& aTitle);
+
+ void GetArtist(nsString& aRetVal) const;
+
+ void SetArtist(const nsAString& aArtist);
+
+ void GetAlbum(nsString& aRetVal) const;
+
+ void SetAlbum(const nsAString& aAlbum);
+
+ void GetArtwork(JSContext* aCx, nsTArray<JSObject*>& aRetVal,
+ ErrorResult& aRv) const;
+
+ void SetArtwork(JSContext* aCx, const Sequence<JSObject*>& aArtwork,
+ ErrorResult& aRv);
+
+ // This would expose MediaMetadataBase's members as public, so use this method
+ // carefully. Now we only use this when we want to update the metadata to the
+ // media session controller in the chrome process.
+ MediaMetadataBase* AsMetadataBase() { return this; }
+
+ private:
+ MediaMetadata(nsIGlobalObject* aParent, const nsString& aTitle,
+ const nsString& aArtist, const nsString& aAlbum);
+
+ ~MediaMetadata() = default;
+
+ // Perform `convert artwork algorithm`. Set `mArtwork` to a converted
+ // `aArtwork` if the conversion works, otherwise throw a type error in `aRv`.
+ void SetArtworkInternal(const Sequence<MediaImage>& aArtwork,
+ ErrorResult& aRv);
+
+ nsCOMPtr<nsIGlobalObject> mParent;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_MediaMetadata_h
diff --git a/dom/media/mediasession/MediaSession.cpp b/dom/media/mediasession/MediaSession.cpp
new file mode 100644
index 0000000000..e55fa28d96
--- /dev/null
+++ b/dom/media/mediasession/MediaSession.cpp
@@ -0,0 +1,335 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/ContentMediaController.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/MediaSession.h"
+#include "mozilla/dom/MediaControlUtils.h"
+#include "mozilla/dom/WindowContext.h"
+#include "mozilla/EnumeratedArrayCycleCollection.h"
+
+// avoid redefined macro in unified build
+#undef LOG
+#define LOG(msg, ...) \
+ MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
+ ("MediaSession=%p, " msg, this, ##__VA_ARGS__))
+
+namespace mozilla::dom {
+
+// We don't use NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE because we need to
+// unregister MediaSession from document's activity listeners.
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(MediaSession)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(MediaSession)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParent)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMediaMetadata)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mActionHandlers)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDoc)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(MediaSession)
+ tmp->Shutdown();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mMediaMetadata)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mActionHandlers)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mDoc)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaSession)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaSession)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaSession)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+ NS_INTERFACE_MAP_ENTRY(nsIDocumentActivity)
+NS_INTERFACE_MAP_END
+
+MediaSession::MediaSession(nsPIDOMWindowInner* aParent)
+ : mParent(aParent), mDoc(mParent->GetExtantDoc()) {
+ MOZ_ASSERT(mParent);
+ MOZ_ASSERT(mDoc);
+ mDoc->RegisterActivityObserver(this);
+ if (mDoc->IsCurrentActiveDocument()) {
+ SetMediaSessionDocStatus(SessionDocStatus::eActive);
+ }
+}
+
+void MediaSession::Shutdown() {
+ if (mDoc) {
+ mDoc->UnregisterActivityObserver(this);
+ }
+ if (mParent) {
+ SetMediaSessionDocStatus(SessionDocStatus::eInactive);
+ }
+}
+
+void MediaSession::NotifyOwnerDocumentActivityChanged() {
+ const bool isDocActive = mDoc->IsCurrentActiveDocument();
+ LOG("Document activity changed, isActive=%d", isDocActive);
+ if (isDocActive) {
+ SetMediaSessionDocStatus(SessionDocStatus::eActive);
+ } else {
+ SetMediaSessionDocStatus(SessionDocStatus::eInactive);
+ }
+}
+
+void MediaSession::SetMediaSessionDocStatus(SessionDocStatus aState) {
+ if (mSessionDocState == aState) {
+ return;
+ }
+ mSessionDocState = aState;
+ NotifyMediaSessionDocStatus(mSessionDocState);
+}
+
+nsPIDOMWindowInner* MediaSession::GetParentObject() const { return mParent; }
+
+JSObject* MediaSession::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return MediaSession_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+MediaMetadata* MediaSession::GetMetadata() const { return mMediaMetadata; }
+
+void MediaSession::SetMetadata(MediaMetadata* aMetadata) {
+ mMediaMetadata = aMetadata;
+ NotifyMetadataUpdated();
+}
+
+void MediaSession::SetPlaybackState(
+ const MediaSessionPlaybackState& aPlaybackState) {
+ if (mDeclaredPlaybackState == aPlaybackState) {
+ return;
+ }
+ mDeclaredPlaybackState = aPlaybackState;
+ NotifyPlaybackStateUpdated();
+}
+
+MediaSessionPlaybackState MediaSession::PlaybackState() const {
+ return mDeclaredPlaybackState;
+}
+
+void MediaSession::SetActionHandler(MediaSessionAction aAction,
+ MediaSessionActionHandler* aHandler) {
+ MOZ_ASSERT(size_t(aAction) < ArrayLength(mActionHandlers));
+ // If the media session changes its supported action, then we would propagate
+ // this information to the chrome process in order to run the media session
+ // actions update algorithm.
+ // https://w3c.github.io/mediasession/#supported-media-session-actions
+ RefPtr<MediaSessionActionHandler>& hanlder = mActionHandlers[aAction];
+ if (!hanlder && aHandler) {
+ NotifyEnableSupportedAction(aAction);
+ } else if (hanlder && !aHandler) {
+ NotifyDisableSupportedAction(aAction);
+ }
+ mActionHandlers[aAction] = aHandler;
+}
+
+MediaSessionActionHandler* MediaSession::GetActionHandler(
+ MediaSessionAction aAction) const {
+ MOZ_ASSERT(size_t(aAction) < ArrayLength(mActionHandlers));
+ return mActionHandlers[aAction];
+}
+
+void MediaSession::SetPositionState(const MediaPositionState& aState,
+ ErrorResult& aRv) {
+ // https://w3c.github.io/mediasession/#dom-mediasession-setpositionstate
+ // If the state is an empty dictionary then clear the position state.
+ if (!aState.IsAnyMemberPresent()) {
+ mPositionState.reset();
+ return;
+ }
+
+ // If the duration is not present, throw a TypeError.
+ if (!aState.mDuration.WasPassed()) {
+ return aRv.ThrowTypeError("Duration is not present");
+ }
+
+ // If the duration is negative, throw a TypeError.
+ if (aState.mDuration.WasPassed() && aState.mDuration.Value() < 0.0) {
+ return aRv.ThrowTypeError(nsPrintfCString(
+ "Invalid duration %f, it can't be negative", aState.mDuration.Value()));
+ }
+
+ // If the position is negative or greater than duration, throw a TypeError.
+ if (aState.mPosition.WasPassed() &&
+ (aState.mPosition.Value() < 0.0 ||
+ aState.mPosition.Value() > aState.mDuration.Value())) {
+ return aRv.ThrowTypeError(nsPrintfCString(
+ "Invalid position %f, it can't be negative or greater than duration",
+ aState.mPosition.Value()));
+ }
+
+ // If the playbackRate is zero, throw a TypeError.
+ if (aState.mPlaybackRate.WasPassed() && aState.mPlaybackRate.Value() == 0.0) {
+ return aRv.ThrowTypeError("The playbackRate is zero");
+ }
+
+ // If the position is not present, set it to zero.
+ double position = aState.mPosition.WasPassed() ? aState.mPosition.Value() : 0;
+
+ // If the playbackRate is not present, set it to 1.0.
+ double playbackRate =
+ aState.mPlaybackRate.WasPassed() ? aState.mPlaybackRate.Value() : 1.0;
+
+ // Update the position state and last position updated time.
+ MOZ_ASSERT(aState.mDuration.WasPassed());
+ mPositionState =
+ Some(PositionState(aState.mDuration.Value(), playbackRate, position));
+ NotifyPositionStateChanged();
+}
+
+void MediaSession::NotifyHandler(const MediaSessionActionDetails& aDetails) {
+ DispatchNotifyHandler(aDetails);
+}
+
+void MediaSession::DispatchNotifyHandler(
+ const MediaSessionActionDetails& aDetails) {
+ class Runnable final : public mozilla::Runnable {
+ public:
+ Runnable(const MediaSession* aSession,
+ const MediaSessionActionDetails& aDetails)
+ : mozilla::Runnable("MediaSession::DispatchNotifyHandler"),
+ mSession(aSession),
+ mDetails(aDetails) {}
+
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override {
+ if (RefPtr<MediaSessionActionHandler> handler =
+ mSession->GetActionHandler(mDetails.mAction)) {
+ handler->Call(mDetails);
+ }
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<const MediaSession> mSession;
+ MediaSessionActionDetails mDetails;
+ };
+
+ RefPtr<nsIRunnable> runnable = new Runnable(this, aDetails);
+ NS_DispatchToMainThread(runnable);
+}
+
+bool MediaSession::IsSupportedAction(MediaSessionAction aAction) const {
+ MOZ_ASSERT(size_t(aAction) < ArrayLength(mActionHandlers));
+ return mActionHandlers[aAction] != nullptr;
+}
+
+bool MediaSession::IsActive() const {
+ RefPtr<BrowsingContext> currentBC = GetParentObject()->GetBrowsingContext();
+ MOZ_ASSERT(currentBC);
+ RefPtr<WindowContext> wc = currentBC->GetTopWindowContext();
+ if (!wc) {
+ return false;
+ }
+ Maybe<uint64_t> activeSessionContextId = wc->GetActiveMediaSessionContextId();
+ if (!activeSessionContextId) {
+ return false;
+ }
+ LOG("session context Id=%" PRIu64 ", active session context Id=%" PRIu64,
+ currentBC->Id(), *activeSessionContextId);
+ return *activeSessionContextId == currentBC->Id();
+}
+
+void MediaSession::NotifyMediaSessionDocStatus(SessionDocStatus aState) {
+ RefPtr<BrowsingContext> currentBC = GetParentObject()->GetBrowsingContext();
+ MOZ_ASSERT(currentBC, "Update session status after context destroyed!");
+
+ RefPtr<IMediaInfoUpdater> updater = ContentMediaAgent::Get(currentBC);
+ if (!updater) {
+ return;
+ }
+ if (aState == SessionDocStatus::eActive) {
+ updater->NotifySessionCreated(currentBC->Id());
+ // If media session set its attributes before its document becomes active,
+ // then we would notify those attributes which hasn't been notified as well
+ // because attributes update would only happen if its document is already
+ // active.
+ NotifyMediaSessionAttributes();
+ } else {
+ updater->NotifySessionDestroyed(currentBC->Id());
+ }
+}
+
+void MediaSession::NotifyMediaSessionAttributes() {
+ MOZ_ASSERT(mSessionDocState == SessionDocStatus::eActive);
+ if (mDeclaredPlaybackState != MediaSessionPlaybackState::None) {
+ NotifyPlaybackStateUpdated();
+ }
+ if (mMediaMetadata) {
+ NotifyMetadataUpdated();
+ }
+ for (size_t idx = 0; idx < ArrayLength(mActionHandlers); idx++) {
+ MediaSessionAction action = static_cast<MediaSessionAction>(idx);
+ if (mActionHandlers[action]) {
+ NotifyEnableSupportedAction(action);
+ }
+ }
+ if (mPositionState) {
+ NotifyPositionStateChanged();
+ }
+}
+
+void MediaSession::NotifyPlaybackStateUpdated() {
+ if (mSessionDocState != SessionDocStatus::eActive) {
+ return;
+ }
+ RefPtr<BrowsingContext> currentBC = GetParentObject()->GetBrowsingContext();
+ MOZ_ASSERT(currentBC,
+ "Update session playback state after context destroyed!");
+ if (RefPtr<IMediaInfoUpdater> updater = ContentMediaAgent::Get(currentBC)) {
+ updater->SetDeclaredPlaybackState(currentBC->Id(), mDeclaredPlaybackState);
+ }
+}
+
+void MediaSession::NotifyMetadataUpdated() {
+ if (mSessionDocState != SessionDocStatus::eActive) {
+ return;
+ }
+ RefPtr<BrowsingContext> currentBC = GetParentObject()->GetBrowsingContext();
+ MOZ_ASSERT(currentBC, "Update session metadata after context destroyed!");
+
+ Maybe<MediaMetadataBase> metadata;
+ if (GetMetadata()) {
+ metadata.emplace(*(GetMetadata()->AsMetadataBase()));
+ }
+ if (RefPtr<IMediaInfoUpdater> updater = ContentMediaAgent::Get(currentBC)) {
+ updater->UpdateMetadata(currentBC->Id(), metadata);
+ }
+}
+
+void MediaSession::NotifyEnableSupportedAction(MediaSessionAction aAction) {
+ if (mSessionDocState != SessionDocStatus::eActive) {
+ return;
+ }
+ RefPtr<BrowsingContext> currentBC = GetParentObject()->GetBrowsingContext();
+ MOZ_ASSERT(currentBC, "Update action after context destroyed!");
+ if (RefPtr<IMediaInfoUpdater> updater = ContentMediaAgent::Get(currentBC)) {
+ updater->EnableAction(currentBC->Id(), aAction);
+ }
+}
+
+void MediaSession::NotifyDisableSupportedAction(MediaSessionAction aAction) {
+ if (mSessionDocState != SessionDocStatus::eActive) {
+ return;
+ }
+ RefPtr<BrowsingContext> currentBC = GetParentObject()->GetBrowsingContext();
+ MOZ_ASSERT(currentBC, "Update action after context destroyed!");
+ if (RefPtr<IMediaInfoUpdater> updater = ContentMediaAgent::Get(currentBC)) {
+ updater->DisableAction(currentBC->Id(), aAction);
+ }
+}
+
+void MediaSession::NotifyPositionStateChanged() {
+ if (mSessionDocState != SessionDocStatus::eActive) {
+ return;
+ }
+ RefPtr<BrowsingContext> currentBC = GetParentObject()->GetBrowsingContext();
+ MOZ_ASSERT(currentBC, "Update action after context destroyed!");
+ if (RefPtr<IMediaInfoUpdater> updater = ContentMediaAgent::Get(currentBC)) {
+ updater->UpdatePositionState(currentBC->Id(), *mPositionState);
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/mediasession/MediaSession.h b/dom/media/mediasession/MediaSession.h
new file mode 100644
index 0000000000..db6864c842
--- /dev/null
+++ b/dom/media/mediasession/MediaSession.h
@@ -0,0 +1,132 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_MediaSession_h
+#define mozilla_dom_MediaSession_h
+
+#include "js/TypeDecls.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/MediaSessionBinding.h"
+#include "mozilla/EnumeratedArray.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIDocumentActivity.h"
+#include "nsWrapperCache.h"
+
+class nsPIDOMWindowInner;
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+
+class Document;
+class MediaMetadata;
+
+// https://w3c.github.io/mediasession/#position-state
+struct PositionState {
+ PositionState() = default;
+ PositionState(double aDuration, double aPlaybackRate,
+ double aLastReportedTime)
+ : mDuration(aDuration),
+ mPlaybackRate(aPlaybackRate),
+ mLastReportedPlaybackPosition(aLastReportedTime) {}
+ double mDuration;
+ double mPlaybackRate;
+ double mLastReportedPlaybackPosition;
+};
+
+class MediaSession final : public nsIDocumentActivity, public nsWrapperCache {
+ public:
+ // Ref counting and cycle collection
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(MediaSession)
+ NS_DECL_NSIDOCUMENTACTIVITY
+
+ explicit MediaSession(nsPIDOMWindowInner* aParent);
+
+ // WebIDL methods
+ nsPIDOMWindowInner* GetParentObject() const;
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ MediaMetadata* GetMetadata() const;
+
+ void SetMetadata(MediaMetadata* aMetadata);
+
+ void SetPlaybackState(const MediaSessionPlaybackState& aPlaybackState);
+
+ MediaSessionPlaybackState PlaybackState() const;
+
+ void SetActionHandler(MediaSessionAction aAction,
+ MediaSessionActionHandler* aHandler);
+
+ void SetPositionState(const MediaPositionState& aState, ErrorResult& aRv);
+
+ bool IsSupportedAction(MediaSessionAction aAction) const;
+
+ // Use this method to trigger media session action handler asynchronously.
+ void NotifyHandler(const MediaSessionActionDetails& aDetails);
+
+ void Shutdown();
+
+ // `MediaStatusManager` would determine which media session is an active media
+ // session and update it from the chrome process. This active session is not
+ // 100% equal to the active media session in the spec, which is a globally
+ // active media session *among all tabs*. The active session here is *among
+ // different windows but in same tab*, so each tab can have at most one
+ // active media session.
+ bool IsActive() const;
+
+ private:
+ // When the document which media session belongs to is going to be destroyed,
+ // or is in the bfcache, then the session would be inactive. Otherwise, it's
+ // active all the time.
+ enum class SessionDocStatus : bool {
+ eInactive = false,
+ eActive = true,
+ };
+ void SetMediaSessionDocStatus(SessionDocStatus aState);
+
+ // These methods are used to propagate media session's status to the chrome
+ // process.
+ void NotifyMediaSessionDocStatus(SessionDocStatus aState);
+ void NotifyMediaSessionAttributes();
+ void NotifyPlaybackStateUpdated();
+ void NotifyMetadataUpdated();
+ void NotifyEnableSupportedAction(MediaSessionAction aAction);
+ void NotifyDisableSupportedAction(MediaSessionAction aAction);
+ void NotifyPositionStateChanged();
+
+ void DispatchNotifyHandler(const MediaSessionActionDetails& aDetails);
+
+ MediaSessionActionHandler* GetActionHandler(MediaSessionAction aAction) const;
+
+ ~MediaSession() = default;
+
+ nsCOMPtr<nsPIDOMWindowInner> mParent;
+
+ RefPtr<MediaMetadata> mMediaMetadata;
+
+ EnumeratedArray<MediaSessionAction, MediaSessionAction::EndGuard_,
+ RefPtr<MediaSessionActionHandler>>
+ mActionHandlers;
+
+ // This is used as is a hint for the user agent to determine whether the
+ // browsing context is playing or paused.
+ // https://w3c.github.io/mediasession/#declared-playback-state
+ MediaSessionPlaybackState mDeclaredPlaybackState =
+ MediaSessionPlaybackState::None;
+
+ Maybe<PositionState> mPositionState;
+ RefPtr<Document> mDoc;
+ SessionDocStatus mSessionDocState = SessionDocStatus::eInactive;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_MediaSession_h
diff --git a/dom/media/mediasession/MediaSessionIPCUtils.h b/dom/media/mediasession/MediaSessionIPCUtils.h
new file mode 100644
index 0000000000..c44f4b4553
--- /dev/null
+++ b/dom/media/mediasession/MediaSessionIPCUtils.h
@@ -0,0 +1,102 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_MEDIASESSION_MEDIASESSIONIPCUTILS_H_
+#define DOM_MEDIA_MEDIASESSION_MEDIASESSIONIPCUTILS_H_
+
+#include "ipc/EnumSerializer.h"
+#include "MediaMetadata.h"
+#include "mozilla/dom/MediaSession.h"
+#include "mozilla/dom/MediaSessionBinding.h"
+#include "mozilla/Maybe.h"
+
+namespace mozilla {
+namespace dom {
+
+typedef Maybe<MediaMetadataBase> MaybeMediaMetadataBase;
+
+} // namespace dom
+} // namespace mozilla
+
+namespace IPC {
+
+template <>
+struct ParamTraits<mozilla::dom::MediaImage> {
+ typedef mozilla::dom::MediaImage paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mSizes);
+ WriteParam(aWriter, aParam.mSrc);
+ WriteParam(aWriter, aParam.mType);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ if (!ReadParam(aReader, &(aResult->mSizes)) ||
+ !ReadParam(aReader, &(aResult->mSrc)) ||
+ !ReadParam(aReader, &(aResult->mType))) {
+ return false;
+ }
+ return true;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::dom::MediaMetadataBase> {
+ typedef mozilla::dom::MediaMetadataBase paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mTitle);
+ WriteParam(aWriter, aParam.mArtist);
+ WriteParam(aWriter, aParam.mAlbum);
+ WriteParam(aWriter, aParam.mArtwork);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ if (!ReadParam(aReader, &(aResult->mTitle)) ||
+ !ReadParam(aReader, &(aResult->mArtist)) ||
+ !ReadParam(aReader, &(aResult->mAlbum)) ||
+ !ReadParam(aReader, &(aResult->mArtwork))) {
+ return false;
+ }
+ return true;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::dom::PositionState> {
+ typedef mozilla::dom::PositionState paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mDuration);
+ WriteParam(aWriter, aParam.mPlaybackRate);
+ WriteParam(aWriter, aParam.mLastReportedPlaybackPosition);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ if (!ReadParam(aReader, &(aResult->mDuration)) ||
+ !ReadParam(aReader, &(aResult->mPlaybackRate)) ||
+ !ReadParam(aReader, &(aResult->mLastReportedPlaybackPosition))) {
+ return false;
+ }
+ return true;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::dom::MediaSessionPlaybackState>
+ : public ContiguousEnumSerializer<
+ mozilla::dom::MediaSessionPlaybackState,
+ mozilla::dom::MediaSessionPlaybackState::None,
+ mozilla::dom::MediaSessionPlaybackState::EndGuard_> {};
+
+template <>
+struct ParamTraits<mozilla::dom::MediaSessionAction>
+ : public ContiguousEnumSerializer<
+ mozilla::dom::MediaSessionAction,
+ mozilla::dom::MediaSessionAction::Play,
+ mozilla::dom::MediaSessionAction::EndGuard_> {};
+
+} // namespace IPC
+
+#endif // DOM_MEDIA_MEDIASESSION_MEDIASESSIONIPCUTILS_H_
diff --git a/dom/media/mediasession/moz.build b/dom/media/mediasession/moz.build
new file mode 100644
index 0000000000..afd208b0ca
--- /dev/null
+++ b/dom/media/mediasession/moz.build
@@ -0,0 +1,24 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+MOCHITEST_MANIFESTS += ["test/mochitest.ini"]
+
+EXPORTS.mozilla.dom += [
+ "MediaMetadata.h",
+ "MediaSession.h",
+ "MediaSessionIPCUtils.h",
+]
+
+UNIFIED_SOURCES += [
+ "MediaMetadata.cpp",
+ "MediaSession.cpp",
+]
+
+CRASHTEST_MANIFESTS += ["test/crashtests/crashtests.list"]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/mediasession/test/MediaSessionTestUtils.js b/dom/media/mediasession/test/MediaSessionTestUtils.js
new file mode 100644
index 0000000000..1ab0e1fe9b
--- /dev/null
+++ b/dom/media/mediasession/test/MediaSessionTestUtils.js
@@ -0,0 +1,30 @@
+const gMediaSessionActions = [
+ "play",
+ "pause",
+ "seekbackward",
+ "seekforward",
+ "previoustrack",
+ "nexttrack",
+ "skipad",
+ "seekto",
+ "stop",
+];
+
+// gCommands and gResults are used in `test_active_mediasession_within_page.html`
+const gCommands = {
+ createMainFrameSession: "create-main-frame-session",
+ createChildFrameSession: "create-child-frame-session",
+ destroyChildFrameSessions: "destroy-child-frame-sessions",
+ destroyActiveChildFrameSession: "destroy-active-child-frame-session",
+ destroyInactiveChildFrameSession: "destroy-inactive-child-frame-session",
+};
+
+const gResults = {
+ mainFrameSession: "main-frame-session",
+ childFrameSession: "child-session-unchanged",
+ childFrameSessionUpdated: "child-session-changed",
+};
+
+function nextWindowMessage() {
+ return new Promise(r => (window.onmessage = event => r(event)));
+}
diff --git a/dom/media/mediasession/test/browser.ini b/dom/media/mediasession/test/browser.ini
new file mode 100644
index 0000000000..b3cd8300cc
--- /dev/null
+++ b/dom/media/mediasession/test/browser.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+subsuite = media-bc
+tags = mediacontrol
+support-files =
+ file_media_session.html
+ ../../test/gizmo.mp4
+
+[browser_active_mediasession_among_tabs.js]
+
diff --git a/dom/media/mediasession/test/browser_active_mediasession_among_tabs.js b/dom/media/mediasession/test/browser_active_mediasession_among_tabs.js
new file mode 100644
index 0000000000..a6a1dbee68
--- /dev/null
+++ b/dom/media/mediasession/test/browser_active_mediasession_among_tabs.js
@@ -0,0 +1,201 @@
+/* eslint-disable no-undef */
+"use strict";
+
+const PAGE =
+ "https://example.com/browser/dom/media/mediasession/test/file_media_session.html";
+
+const ACTION = "previoustrack";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.media.mediasession.enabled", true],
+ ["media.mediacontrol.testingevents.enabled", true],
+ ],
+ });
+});
+
+/**
+ * When multiple tabs are all having media session, the latest created one would
+ * become an active session. When the active media session is destroyed via
+ * closing the tab, the previous active session would become current active
+ * session again.
+ */
+add_task(async function testActiveSessionWhenClosingTab() {
+ info(`open tab1 and load media session test page`);
+ const tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+ await startMediaPlaybackAndWaitMedisSessionBecomeActiveSession(tab1);
+
+ info(`pressing '${ACTION}' key`);
+ MediaControlService.generateMediaControlKey(ACTION);
+
+ info(`session in tab1 should become active session`);
+ await checkIfActionReceived(tab1, ACTION);
+
+ info(`open tab2 and load media session test page`);
+ const tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+ await startMediaPlaybackAndWaitMedisSessionBecomeActiveSession(tab2);
+
+ info(`pressing '${ACTION}' key`);
+ MediaControlService.generateMediaControlKey(ACTION);
+
+ info(`session in tab2 should become active session`);
+ await checkIfActionReceived(tab2, ACTION);
+ await checkIfActionNotReceived(tab1, ACTION);
+
+ info(`remove tab2`);
+ const controllerChanged = waitUntilMainMediaControllerChanged();
+ BrowserTestUtils.removeTab(tab2);
+ await controllerChanged;
+
+ info(`pressing '${ACTION}' key`);
+ MediaControlService.generateMediaControlKey(ACTION);
+
+ info(`session in tab1 should become active session again`);
+ await checkIfActionReceived(tab1, ACTION);
+
+ info(`remove tab1`);
+ BrowserTestUtils.removeTab(tab1);
+});
+
+/**
+ * This test is similar with `testActiveSessionWhenClosingTab`, the difference
+ * is that the way we use to destroy active session is via naviagation, not
+ * closing tab.
+ */
+add_task(async function testActiveSessionWhenNavigatingTab() {
+ info(`open tab1 and load media session test page`);
+ const tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+ await startMediaPlaybackAndWaitMedisSessionBecomeActiveSession(tab1);
+
+ info(`pressing '${ACTION}' key`);
+ MediaControlService.generateMediaControlKey(ACTION);
+
+ info(`session in tab1 should become active session`);
+ await checkIfActionReceived(tab1, ACTION);
+
+ info(`open tab2 and load media session test page`);
+ const tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+ await startMediaPlaybackAndWaitMedisSessionBecomeActiveSession(tab2);
+
+ info(`pressing '${ACTION}' key`);
+ MediaControlService.generateMediaControlKey(ACTION);
+
+ info(`session in tab2 should become active session`);
+ await checkIfActionReceived(tab2, ACTION);
+ await checkIfActionNotReceived(tab1, ACTION);
+
+ info(`navigate tab2 to blank page`);
+ const controllerChanged = waitUntilMainMediaControllerChanged();
+ BrowserTestUtils.loadURIString(tab2.linkedBrowser, "about:blank");
+ await controllerChanged;
+
+ info(`pressing '${ACTION}' key`);
+ MediaControlService.generateMediaControlKey(ACTION);
+
+ info(`session in tab1 should become active session`);
+ await checkIfActionReceived(tab1, ACTION);
+
+ info(`remove tabs`);
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
+
+/**
+ * If we create a media session in a tab where no any playing media exists, then
+ * that session would not involve in global active media session selection. The
+ * current active media session would remain unchanged.
+ */
+add_task(async function testCreatingSessionWithoutPlayingMedia() {
+ info(`open tab1 and load media session test page`);
+ const tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+ await startMediaPlaybackAndWaitMedisSessionBecomeActiveSession(tab1);
+
+ info(`pressing '${ACTION}' key`);
+ MediaControlService.generateMediaControlKey(ACTION);
+
+ info(`session in tab1 should become active session`);
+ await checkIfActionReceived(tab1, ACTION);
+
+ info(`open tab2 and load media session test page`);
+ const tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE);
+
+ info(`pressing '${ACTION}' key`);
+ MediaControlService.generateMediaControlKey(ACTION);
+
+ info(
+ `session in tab1 is still an active session because there is no media playing in tab2`
+ );
+ await checkIfActionReceived(tab1, ACTION);
+ await checkIfActionNotReceived(tab2, ACTION);
+
+ info(`remove tabs`);
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
+
+/**
+ * The following are helper functions
+ */
+async function startMediaPlaybackAndWaitMedisSessionBecomeActiveSession(tab) {
+ await Promise.all([
+ BrowserUtils.promiseObserved("active-media-session-changed"),
+ SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ const video = content.document.getElementById("testVideo");
+ if (!video) {
+ ok(false, `can't get the media element!`);
+ }
+ video.play();
+ }),
+ ]);
+}
+
+async function checkIfActionReceived(tab, action) {
+ await SpecialPowers.spawn(tab.linkedBrowser, [action], expectedAction => {
+ return new Promise(resolve => {
+ const result = content.document.getElementById("result");
+ if (!result) {
+ ok(false, `can't get the element for showing result!`);
+ }
+
+ function checkAction() {
+ is(
+ result.innerHTML,
+ expectedAction,
+ `received '${expectedAction}' correctly`
+ );
+ // Reset the result after finishing checking result, then we can dispatch
+ // same action again without worrying about previous result.
+ result.innerHTML = "";
+ resolve();
+ }
+
+ if (result.innerHTML == "") {
+ info(`wait until receiving action`);
+ result.addEventListener("actionChanged", () => checkAction(), {
+ once: true,
+ });
+ } else {
+ checkAction();
+ }
+ });
+ });
+}
+
+async function checkIfActionNotReceived(tab, action) {
+ await SpecialPowers.spawn(tab.linkedBrowser, [action], expectedAction => {
+ return new Promise(resolve => {
+ const result = content.document.getElementById("result");
+ if (!result) {
+ ok(false, `can't get the element for showing result!`);
+ }
+ is(result.innerHTML, "", `should not receive any action`);
+ ok(result.innerHTML != expectedAction, `not receive '${expectedAction}'`);
+ resolve();
+ });
+ });
+}
+
+function waitUntilMainMediaControllerChanged() {
+ return BrowserUtils.promiseObserved("main-media-controller-changed");
+}
diff --git a/dom/media/mediasession/test/crashtests/crashtests.list b/dom/media/mediasession/test/crashtests/crashtests.list
new file mode 100644
index 0000000000..9ca4956ab6
--- /dev/null
+++ b/dom/media/mediasession/test/crashtests/crashtests.list
@@ -0,0 +1 @@
+load inactive-mediasession.html
diff --git a/dom/media/mediasession/test/crashtests/inactive-mediasession.html b/dom/media/mediasession/test/crashtests/inactive-mediasession.html
new file mode 100644
index 0000000000..b24fb887ff
--- /dev/null
+++ b/dom/media/mediasession/test/crashtests/inactive-mediasession.html
@@ -0,0 +1,16 @@
+<html>
+<head></head>
+<script>
+const frame = document.createElementNS('http://www.w3.org/1999/xhtml', 'frame');
+document.documentElement.appendChild(frame);
+
+const windowPointer = frame.contentWindow;
+document.documentElement.replaceWith();
+
+// Setting attributes on inactive media session should not cause crash.
+windowPointer.navigator.mediaSession.setActionHandler('nexttrack', null);
+windowPointer.navigator.mediaSession.playbackState = "playing";
+windowPointer.navigator.mediaSession.setPositionState();
+windowPointer.navigator.mediaSession.metadata = null;
+</script>
+</html>
diff --git a/dom/media/mediasession/test/file_media_session.html b/dom/media/mediasession/test/file_media_session.html
new file mode 100644
index 0000000000..b6680fb46b
--- /dev/null
+++ b/dom/media/mediasession/test/file_media_session.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Media Session and non-autoplay media</title>
+</head>
+<body>
+<video id="testVideo" src="gizmo.mp4" loop></video>
+<h1 id="result"></h1>
+<script type="text/javascript">
+
+const MediaSessionActions = [
+ "play",
+ "pause",
+ "seekbackward",
+ "seekforward",
+ "previoustrack",
+ "nexttrack",
+ "stop",
+];
+
+for (const action of MediaSessionActions) {
+ navigator.mediaSession.setActionHandler(action, () => {
+ // eslint-disable-next-line no-unsanitized/property
+ document.getElementById("result").innerHTML = action;
+ document.getElementById("result").dispatchEvent(new CustomEvent("actionChanged"));
+ });
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/media/mediasession/test/file_trigger_actionhanlder_frame.html b/dom/media/mediasession/test/file_trigger_actionhanlder_frame.html
new file mode 100644
index 0000000000..4d55db2189
--- /dev/null
+++ b/dom/media/mediasession/test/file_trigger_actionhanlder_frame.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Test frame for triggering media session's action handler</title>
+ <script src="MediaSessionTestUtils.js"></script>
+ </head>
+<body>
+<video id="testVideo" src="gizmo.mp4" loop></video>
+<script>
+
+const video = document.getElementById("testVideo");
+const w = window.opener || window.parent;
+
+window.onmessage = async event => {
+ if (event.data == "play") {
+ await video.play();
+ // As we can't observe `media-displayed-playback-changed` notification,
+ // that can only be observed in the chrome process. Therefore, we use a
+ // workaround instead which is to wait for a while to ensure that the
+ // controller has already been created in the chrome process.
+ let timeupdatecount = 0;
+ await new Promise(r => video.ontimeupdate = () => {
+ if (++timeupdatecount == 3) {
+ video.ontimeupdate = null;
+ r();
+ }
+ });
+ w.postMessage("played", "*");
+ }
+}
+
+// Setup the action handlers which would post the result back to the main window.
+for (const action of gMediaSessionActions) {
+ navigator.mediaSession.setActionHandler(action, () => {
+ w.postMessage(action, "*");
+ });
+}
+</script>
+</body>
+</html>
diff --git a/dom/media/mediasession/test/file_trigger_actionhanlder_window.html b/dom/media/mediasession/test/file_trigger_actionhanlder_window.html
new file mode 100644
index 0000000000..f3316d6dad
--- /dev/null
+++ b/dom/media/mediasession/test/file_trigger_actionhanlder_window.html
@@ -0,0 +1,107 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Test window for triggering media session's action handler</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="MediaSessionTestUtils.js"></script>
+ </head>
+<body>
+<video id="testVideo" src="gizmo.mp4" loop></video>
+<iframe id="childFrame"></iframe>
+<script>
+
+var triggeredActionNums = 0;
+
+nextWindowMessage().then(
+ async (event) => {
+ const testInfo = event.data;
+ await createSession(testInfo);
+ // Media session would only become active if there is any media currently
+ // playing. Non-active media session won't receive any actions. Therefore,
+ // we start media playback before testing media session.
+ await startMediaPlayback(testInfo);
+ for (const action of gMediaSessionActions) {
+ await waitUntilActionHandlerTriggered(action, testInfo);
+ }
+ endTestAndReportResult();
+ });
+
+/**
+ * The following are helper functions
+ */
+async function startMediaPlayback({shouldCreateFrom}) {
+ info(`wait until media starts playing`);
+ if (shouldCreateFrom == "main-frame") {
+ const video = document.getElementById("testVideo");
+ await video.play();
+ // As we can't observe `media-displayed-playback-changed` notification,
+ // that can only be observed in the chrome process. Therefore, we use a
+ // workaround instead which is to wait for a while to ensure that the
+ // controller has already been created in the chrome process.
+ let timeupdatecount = 0;
+ await new Promise(r => video.ontimeupdate = () => {
+ if (++timeupdatecount == 3) {
+ video.ontimeupdate = null;
+ r();
+ }
+ });
+ } else {
+ const iframe = document.getElementById("childFrame");
+ iframe.contentWindow.postMessage("play", "*");
+ await new Promise(r => {
+ window.onmessage = event => {
+ is(event.data, "played", `media started playing in child-frame`);
+ r();
+ };
+ });
+ }
+}
+
+async function createSession({shouldCreateFrom, origin}) {
+ info(`create media session in ${shouldCreateFrom}`);
+ if (shouldCreateFrom == "main-frame") {
+ // Simply referencing media session will create media session.
+ navigator.mediaSession;
+ return;
+ };
+ const frame = document.getElementById("childFrame");
+ const originURL = origin == "same-origin"
+ ? "http://mochi.test:8888" : "http://example.org";
+ frame.src = originURL + "/tests/dom/media/mediasession/test/file_trigger_actionhanlder_frame.html";
+ await new Promise(r => frame.onload = r);
+}
+
+async function waitUntilActionHandlerTriggered(action, {shouldCreateFrom}) {
+ info(`wait until '${action}' handler of media session created in ` +
+ `${shouldCreateFrom} is triggered`);
+ if (shouldCreateFrom == "main-frame") {
+ let promise = new Promise(resolve => {
+ navigator.mediaSession.setActionHandler(action, () => {
+ ok(true, `Triggered ${action} handler`);
+ triggeredActionNums++;
+ resolve();
+ });
+ });
+ SpecialPowers.generateMediaControlKeyTestEvent(action);
+ await promise;
+ return;
+ }
+ SpecialPowers.generateMediaControlKeyTestEvent(action);
+ if ((await nextWindowMessage()).data == action) {
+ info(`Triggered ${action} handler in child-frame`);
+ triggeredActionNums++;
+ }
+}
+
+function endTestAndReportResult() {
+ const w = window.opener || window.parent;
+ if (triggeredActionNums == gMediaSessionActions.length) {
+ w.postMessage("success", "*");
+ } else {
+ w.postMessage("fail", "*");
+ }
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/media/mediasession/test/mochitest.ini b/dom/media/mediasession/test/mochitest.ini
new file mode 100644
index 0000000000..146f1afea8
--- /dev/null
+++ b/dom/media/mediasession/test/mochitest.ini
@@ -0,0 +1,12 @@
+[DEFAULT]
+subsuite = media
+tags = mediasession mediacontrol
+
+support-files =
+ ../../test/gizmo.mp4
+ file_trigger_actionhanlder_frame.html
+ file_trigger_actionhanlder_window.html
+ MediaSessionTestUtils.js
+
+[test_setactionhandler.html]
+[test_trigger_actionhanlder.html]
diff --git a/dom/media/mediasession/test/test_setactionhandler.html b/dom/media/mediasession/test/test_setactionhandler.html
new file mode 100644
index 0000000000..e0fba77d80
--- /dev/null
+++ b/dom/media/mediasession/test/test_setactionhandler.html
@@ -0,0 +1,92 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title></title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script>
+
+SimpleTest.waitForExplicitFinish();
+
+const ACTIONS = [
+ "play",
+ "pause",
+ "seekbackward",
+ "seekforward",
+ "previoustrack",
+ "nexttrack",
+ "skipad",
+ "seekto",
+ "stop",
+];
+
+(async function testSetActionHandler() {
+ await setupPreference();
+
+ for (const action of ACTIONS) {
+ info(`Test setActionHandler for '${action}'`);
+ generateAction(action);
+ ok(true, "it's ok to do " + action + " without any handler");
+
+ let expectedDetails = generateActionDetails(action);
+
+ let fired = false;
+ await setHandlerAndTakeAction(action, details => {
+ ok(hasSameValue(details, expectedDetails), "get expected details for " + action);
+ fired = !fired;
+ clearActionHandler(action);
+ });
+
+ generateAction(action);
+ ok(fired, "handler of " + action + " is cleared");
+ }
+
+ SimpleTest.finish();
+})();
+
+function setupPreference() {
+ return SpecialPowers.pushPrefEnv({"set": [
+ ["dom.media.mediasession.enabled", true],
+ ]});
+}
+
+function generateAction(action) {
+ let details = generateActionDetails(action);
+ SpecialPowers.wrap(navigator.mediaSession).notifyHandler(details);
+}
+
+function generateActionDetails(action) {
+ let details = { action };
+ if (action == "seekbackward" || action == "seekforward") {
+ details.seekOffset = 3.14159;
+ } else if (action == "seekto") {
+ details.seekTime = 1.618;
+ }
+ return details;
+}
+
+function setHandlerAndTakeAction(action, handler) {
+ let promise = new Promise(resolve => {
+ navigator.mediaSession.setActionHandler(action, details => {
+ handler(details);
+ resolve();
+ });
+ });
+ generateAction(action);
+ return promise;
+}
+
+function hasSameValue(a, b) {
+ // The order of the object matters when stringify the object.
+ return JSON.stringify(a) == JSON.stringify(b);
+}
+
+function clearActionHandler(action) {
+ navigator.mediaSession.setActionHandler(action, null);
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/media/mediasession/test/test_trigger_actionhanlder.html b/dom/media/mediasession/test/test_trigger_actionhanlder.html
new file mode 100644
index 0000000000..72e8eaf7b5
--- /dev/null
+++ b/dom/media/mediasession/test/test_trigger_actionhanlder.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Test of triggering media session's action handlers</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="MediaSessionTestUtils.js"></script>
+ </head>
+<body>
+<script>
+/**
+ * This test is used to test if pressing media control keys can trigger media
+ * session's corresponding action handler under different situations.
+ */
+const testCases = [
+ {
+ name: "Triggering action handlers for session created in [main-frame]",
+ shouldCreateFrom: "main-frame",
+ },
+ {
+ name: "Triggering action handlers for session created in [same-origin] [child-frame]",
+ shouldCreateFrom: "child-frame",
+ origin: "same-origin",
+ },
+ {
+ name: "Triggering action handlers for session created in [cross-origin] [child-frame]",
+ shouldCreateFrom: "child-frame",
+ origin: "cross-origin",
+ },
+];
+
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv({"set": [
+ ["dom.media.mediasession.enabled", true],
+ ["media.mediacontrol.testingevents.enabled", true],
+]}, startTest());
+
+async function startTest() {
+ for (const testCase of testCases) {
+ info(`- loading test '${testCase.name}' in a new window -`);
+ const testURL = "file_trigger_actionhanlder_window.html";
+ const testWindow = window.open(testURL, "", "width=500,height=500");
+ await new Promise(r => testWindow.onload = r);
+
+ info("- start running test -");
+ testWindow.postMessage(testCase, window.origin);
+ is((await nextWindowMessage()).data, "success",
+ `- finished test '${testCase.name}' -`);
+ testWindow.close();
+ }
+ SimpleTest.finish();
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/media/mediasink/AudioDecoderInputTrack.cpp b/dom/media/mediasink/AudioDecoderInputTrack.cpp
new file mode 100644
index 0000000000..7f970f0e4f
--- /dev/null
+++ b/dom/media/mediasink/AudioDecoderInputTrack.cpp
@@ -0,0 +1,681 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioDecoderInputTrack.h"
+
+#include "MediaData.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "Tracing.h"
+
+// Use abort() instead of exception in SoundTouch.
+#define ST_NO_EXCEPTION_HANDLING 1
+#include "soundtouch/SoundTouchFactory.h"
+
+namespace mozilla {
+
+extern LazyLogModule gMediaDecoderLog;
+
+#define LOG(msg, ...) \
+ MOZ_LOG(gMediaDecoderLog, LogLevel::Debug, \
+ ("AudioDecoderInputTrack=%p " msg, this, ##__VA_ARGS__))
+
+#define LOG_M(msg, this, ...) \
+ MOZ_LOG(gMediaDecoderLog, LogLevel::Debug, \
+ ("AudioDecoderInputTrack=%p " msg, this, ##__VA_ARGS__))
+
+/* static */
+AudioDecoderInputTrack* AudioDecoderInputTrack::Create(
+ MediaTrackGraph* aGraph, nsISerialEventTarget* aDecoderThread,
+ const AudioInfo& aInfo, float aPlaybackRate, float aVolume,
+ bool aPreservesPitch) {
+ MOZ_ASSERT(aGraph);
+ MOZ_ASSERT(aDecoderThread);
+ AudioDecoderInputTrack* track =
+ new AudioDecoderInputTrack(aDecoderThread, aGraph->GraphRate(), aInfo,
+ aPlaybackRate, aVolume, aPreservesPitch);
+ aGraph->AddTrack(track);
+ return track;
+}
+
+AudioDecoderInputTrack::AudioDecoderInputTrack(
+ nsISerialEventTarget* aDecoderThread, TrackRate aGraphRate,
+ const AudioInfo& aInfo, float aPlaybackRate, float aVolume,
+ bool aPreservesPitch)
+ : ProcessedMediaTrack(aGraphRate, MediaSegment::AUDIO, new AudioSegment()),
+ mDecoderThread(aDecoderThread),
+ mResamplerChannelCount(0),
+ mInitialInputChannels(aInfo.mChannels),
+ mInputSampleRate(aInfo.mRate),
+ mDelayedScheduler(mDecoderThread),
+ mPlaybackRate(aPlaybackRate),
+ mVolume(aVolume),
+ mPreservesPitch(aPreservesPitch) {}
+
+bool AudioDecoderInputTrack::ConvertAudioDataToSegment(
+ AudioData* aAudio, AudioSegment& aSegment,
+ const PrincipalHandle& aPrincipalHandle) {
+ AssertOnDecoderThread();
+ MOZ_ASSERT(aAudio);
+ MOZ_ASSERT(aSegment.IsEmpty());
+ if (!aAudio->Frames()) {
+ LOG("Ignore audio with zero frame");
+ return false;
+ }
+
+ aAudio->EnsureAudioBuffer();
+ RefPtr<SharedBuffer> buffer = aAudio->mAudioBuffer;
+ AudioDataValue* bufferData = static_cast<AudioDataValue*>(buffer->Data());
+ AutoTArray<const AudioDataValue*, 2> channels;
+ for (uint32_t i = 0; i < aAudio->mChannels; ++i) {
+ channels.AppendElement(bufferData + i * aAudio->Frames());
+ }
+ aSegment.AppendFrames(buffer.forget(), channels, aAudio->Frames(),
+ aPrincipalHandle);
+ const TrackRate newInputRate = static_cast<TrackRate>(aAudio->mRate);
+ if (newInputRate != mInputSampleRate) {
+ LOG("Input sample rate changed %u -> %u", mInputSampleRate, newInputRate);
+ mInputSampleRate = newInputRate;
+ mResampler.own(nullptr);
+ mResamplerChannelCount = 0;
+ }
+ if (mInputSampleRate != GraphImpl()->GraphRate()) {
+ aSegment.ResampleChunks(mResampler, &mResamplerChannelCount,
+ mInputSampleRate, GraphImpl()->GraphRate());
+ }
+ return aSegment.GetDuration() > 0;
+}
+
+void AudioDecoderInputTrack::AppendData(
+ AudioData* aAudio, const PrincipalHandle& aPrincipalHandle) {
+ AssertOnDecoderThread();
+ MOZ_ASSERT(aAudio);
+ nsTArray<RefPtr<AudioData>> audio;
+ audio.AppendElement(aAudio);
+ AppendData(audio, aPrincipalHandle);
+}
+
+void AudioDecoderInputTrack::AppendData(
+ nsTArray<RefPtr<AudioData>>& aAudioArray,
+ const PrincipalHandle& aPrincipalHandle) {
+ AssertOnDecoderThread();
+ MOZ_ASSERT(!mShutdownSPSCQueue);
+
+ // Batching all new data together in order to push them as a single unit that
+ // gives the SPSC queue more spaces.
+ for (const auto& audio : aAudioArray) {
+ BatchData(audio, aPrincipalHandle);
+ }
+
+ // If SPSC queue doesn't have much available capacity now, we would push
+ // batched later.
+ if (ShouldBatchData()) {
+ return;
+ }
+ PushBatchedDataIfNeeded();
+}
+
+bool AudioDecoderInputTrack::ShouldBatchData() const {
+ AssertOnDecoderThread();
+ // If the SPSC queue has less available capacity than the threshold, then all
+ // input audio data should be batched together, in order not to increase the
+ // pressure of SPSC queue.
+ static const int kThresholdNumerator = 3;
+ static const int kThresholdDenominator = 10;
+ return mSPSCQueue.AvailableWrite() <
+ mSPSCQueue.Capacity() * kThresholdNumerator / kThresholdDenominator;
+}
+
+bool AudioDecoderInputTrack::HasBatchedData() const {
+ AssertOnDecoderThread();
+ return !mBatchedData.mSegment.IsEmpty();
+}
+
+void AudioDecoderInputTrack::BatchData(
+ AudioData* aAudio, const PrincipalHandle& aPrincipalHandle) {
+ AssertOnDecoderThread();
+ AudioSegment segment;
+ if (!ConvertAudioDataToSegment(aAudio, segment, aPrincipalHandle)) {
+ return;
+ }
+ mBatchedData.mSegment.AppendFrom(&segment);
+ if (!mBatchedData.mStartTime.IsValid()) {
+ mBatchedData.mStartTime = aAudio->mTime;
+ }
+ mBatchedData.mEndTime = aAudio->GetEndTime();
+ LOG("batched data [%" PRId64 ":%" PRId64 "] sz=%" PRId64,
+ aAudio->mTime.ToMicroseconds(), aAudio->GetEndTime().ToMicroseconds(),
+ mBatchedData.mSegment.GetDuration());
+ DispatchPushBatchedDataIfNeeded();
+}
+
+void AudioDecoderInputTrack::DispatchPushBatchedDataIfNeeded() {
+ AssertOnDecoderThread();
+ MOZ_ASSERT(!mShutdownSPSCQueue);
+ // The graph thread runs iteration around per 2~10ms. Doing this to ensure
+ // that we can keep consuming data. If the producer stops pushing new data
+ // due to MDSM stops decoding, which is because MDSM thinks the data stored
+ // in the audio queue are enough. The way to remove those data from the
+ // audio queue is driven by us, so we have to keep consuming data.
+ // Otherwise, we would get stuck because those batched data would never be
+ // consumed.
+ static const uint8_t kTimeoutMS = 10;
+ TimeStamp target =
+ TimeStamp::Now() + TimeDuration::FromMilliseconds(kTimeoutMS);
+ mDelayedScheduler.Ensure(
+ target,
+ [self = RefPtr<AudioDecoderInputTrack>(this), this]() {
+ LOG("In the task of DispatchPushBatchedDataIfNeeded");
+ mDelayedScheduler.CompleteRequest();
+ MOZ_ASSERT(!mShutdownSPSCQueue);
+ MOZ_ASSERT(HasBatchedData());
+ // The capacity in SPSC is still not enough, so we can't push data now.
+ // Retrigger another task to push batched data.
+ if (ShouldBatchData()) {
+ DispatchPushBatchedDataIfNeeded();
+ return;
+ }
+ PushBatchedDataIfNeeded();
+ },
+ []() { MOZ_DIAGNOSTIC_ASSERT(false); });
+}
+
+void AudioDecoderInputTrack::PushBatchedDataIfNeeded() {
+ AssertOnDecoderThread();
+ if (!HasBatchedData()) {
+ return;
+ }
+ LOG("Append batched data [%" PRId64 ":%" PRId64 "], available SPSC sz=%u",
+ mBatchedData.mStartTime.ToMicroseconds(),
+ mBatchedData.mEndTime.ToMicroseconds(), mSPSCQueue.AvailableWrite());
+ SPSCData data({SPSCData::DecodedData(std::move(mBatchedData))});
+ PushDataToSPSCQueue(data);
+ MOZ_ASSERT(mBatchedData.mSegment.IsEmpty());
+ // No batched data remains, we can cancel the pending tasks.
+ mDelayedScheduler.Reset();
+}
+
+void AudioDecoderInputTrack::NotifyEndOfStream() {
+ AssertOnDecoderThread();
+ // Force to push all data before EOS. Otherwise, the track would be ended too
+ // early without sending all data.
+ PushBatchedDataIfNeeded();
+ SPSCData data({SPSCData::EOS()});
+ LOG("Set EOS, available SPSC sz=%u", mSPSCQueue.AvailableWrite());
+ PushDataToSPSCQueue(data);
+}
+
+void AudioDecoderInputTrack::ClearFutureData() {
+ AssertOnDecoderThread();
+ // Clear the data hasn't been pushed to SPSC queue yet.
+ mBatchedData.Clear();
+ mDelayedScheduler.Reset();
+ SPSCData data({SPSCData::ClearFutureData()});
+ LOG("Set clear future data, available SPSC sz=%u",
+ mSPSCQueue.AvailableWrite());
+ PushDataToSPSCQueue(data);
+}
+
+void AudioDecoderInputTrack::PushDataToSPSCQueue(SPSCData& data) {
+ AssertOnDecoderThread();
+ const bool rv = mSPSCQueue.Enqueue(data);
+ MOZ_DIAGNOSTIC_ASSERT(rv, "Failed to push data, SPSC queue is full!");
+ Unused << rv;
+}
+
+void AudioDecoderInputTrack::SetVolume(float aVolume) {
+ AssertOnDecoderThread();
+ LOG("Set volume=%f", aVolume);
+ GetMainThreadSerialEventTarget()->Dispatch(
+ NS_NewRunnableFunction("AudioDecoderInputTrack::SetVolume",
+ [self = RefPtr<AudioDecoderInputTrack>(this),
+ aVolume] { self->SetVolumeImpl(aVolume); }));
+}
+
+void AudioDecoderInputTrack::SetVolumeImpl(float aVolume) {
+ MOZ_ASSERT(NS_IsMainThread());
+ class Message : public ControlMessage {
+ public:
+ Message(AudioDecoderInputTrack* aTrack, float aVolume)
+ : ControlMessage(aTrack), mTrack(aTrack), mVolume(aVolume) {}
+ void Run() override {
+ TRACE_COMMENT("AudioDecoderInputTrack::SetVolume ControlMessage", "%f",
+ mVolume);
+ LOG_M("Apply volume=%f", mTrack.get(), mVolume);
+ mTrack->mVolume = mVolume;
+ }
+
+ protected:
+ const RefPtr<AudioDecoderInputTrack> mTrack;
+ const float mVolume;
+ };
+ GraphImpl()->AppendMessage(MakeUnique<Message>(this, aVolume));
+}
+
+void AudioDecoderInputTrack::SetPlaybackRate(float aPlaybackRate) {
+ AssertOnDecoderThread();
+ LOG("Set playback rate=%f", aPlaybackRate);
+ GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction(
+ "AudioDecoderInputTrack::SetPlaybackRate",
+ [self = RefPtr<AudioDecoderInputTrack>(this), aPlaybackRate] {
+ self->SetPlaybackRateImpl(aPlaybackRate);
+ }));
+}
+
+void AudioDecoderInputTrack::SetPlaybackRateImpl(float aPlaybackRate) {
+ MOZ_ASSERT(NS_IsMainThread());
+ class Message : public ControlMessage {
+ public:
+ Message(AudioDecoderInputTrack* aTrack, float aPlaybackRate)
+ : ControlMessage(aTrack),
+ mTrack(aTrack),
+ mPlaybackRate(aPlaybackRate) {}
+ void Run() override {
+ TRACE_COMMENT("AudioDecoderInputTrack::SetPlaybackRate ControlMessage",
+ "%f", mPlaybackRate);
+ LOG_M("Apply playback rate=%f", mTrack.get(), mPlaybackRate);
+ mTrack->mPlaybackRate = mPlaybackRate;
+ mTrack->SetTempoAndRateForTimeStretcher();
+ }
+
+ protected:
+ const RefPtr<AudioDecoderInputTrack> mTrack;
+ const float mPlaybackRate;
+ };
+ GraphImpl()->AppendMessage(MakeUnique<Message>(this, aPlaybackRate));
+}
+
+void AudioDecoderInputTrack::SetPreservesPitch(bool aPreservesPitch) {
+ AssertOnDecoderThread();
+ LOG("Set preserves pitch=%d", aPreservesPitch);
+ GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction(
+ "AudioDecoderInputTrack::SetPreservesPitch",
+ [self = RefPtr<AudioDecoderInputTrack>(this), aPreservesPitch] {
+ self->SetPreservesPitchImpl(aPreservesPitch);
+ }));
+}
+
+void AudioDecoderInputTrack::SetPreservesPitchImpl(bool aPreservesPitch) {
+ MOZ_ASSERT(NS_IsMainThread());
+ class Message : public ControlMessage {
+ public:
+ Message(AudioDecoderInputTrack* aTrack, bool aPreservesPitch)
+ : ControlMessage(aTrack),
+ mTrack(aTrack),
+ mPreservesPitch(aPreservesPitch) {}
+ void Run() override {
+ TRACE_COMMENT("AudioDecoderInputTrack::SetPreservesPitch", "%s",
+ mPreservesPitch ? "true" : "false")
+ LOG_M("Apply preserves pitch=%d", mTrack.get(), mPreservesPitch);
+ mTrack->mPreservesPitch = mPreservesPitch;
+ mTrack->SetTempoAndRateForTimeStretcher();
+ }
+
+ protected:
+ const RefPtr<AudioDecoderInputTrack> mTrack;
+ const bool mPreservesPitch;
+ };
+ GraphImpl()->AppendMessage(MakeUnique<Message>(this, aPreservesPitch));
+}
+
+void AudioDecoderInputTrack::Close() {
+ AssertOnDecoderThread();
+ LOG("Close");
+ mShutdownSPSCQueue = true;
+ mBatchedData.Clear();
+ mDelayedScheduler.Reset();
+}
+
+void AudioDecoderInputTrack::DestroyImpl() {
+ LOG("DestroyImpl");
+ AssertOnGraphThreadOrNotRunning();
+ mBufferedData.Clear();
+ if (mTimeStretcher) {
+ soundtouch::destroySoundTouchObj(mTimeStretcher);
+ }
+ ProcessedMediaTrack::DestroyImpl();
+}
+
+AudioDecoderInputTrack::~AudioDecoderInputTrack() {
+ MOZ_ASSERT(mBatchedData.mSegment.IsEmpty());
+ MOZ_ASSERT(mShutdownSPSCQueue);
+ mResampler.own(nullptr);
+}
+
+void AudioDecoderInputTrack::ProcessInput(GraphTime aFrom, GraphTime aTo,
+ uint32_t aFlags) {
+ AssertOnGraphThread();
+ if (Ended()) {
+ return;
+ }
+
+ TrackTime consumedDuration = 0;
+ auto notify = MakeScopeExit([this, &consumedDuration] {
+ NotifyInTheEndOfProcessInput(consumedDuration);
+ });
+
+ if (mSentAllData && (aFlags & ALLOW_END)) {
+ LOG("End track");
+ mEnded = true;
+ return;
+ }
+
+ const TrackTime expectedDuration = aTo - aFrom;
+ LOG("ProcessInput [%" PRId64 " to %" PRId64 "], duration=%" PRId64, aFrom,
+ aTo, expectedDuration);
+
+ // Drain all data from SPSC queue first, because we want that the SPSC queue
+ // always has capacity of accepting data from the producer. In addition, we
+ // also need to check if there is any control related data that should be
+ // applied to output segment, eg. `ClearFutureData`.
+ SPSCData data;
+ while (mSPSCQueue.Dequeue(&data, 1) > 0) {
+ HandleSPSCData(data);
+ }
+
+ consumedDuration += AppendBufferedDataToOutput(expectedDuration);
+ if (HasSentAllData()) {
+ LOG("Sent all data, should end track in next iteration");
+ mSentAllData = true;
+ }
+}
+
+void AudioDecoderInputTrack::HandleSPSCData(SPSCData& aData) {
+ AssertOnGraphThread();
+ if (aData.IsDecodedData()) {
+ MOZ_ASSERT(!mReceivedEOS);
+ AudioSegment& segment = aData.AsDecodedData()->mSegment;
+ LOG("popped out data [%" PRId64 ":%" PRId64 "] sz=%" PRId64,
+ aData.AsDecodedData()->mStartTime.ToMicroseconds(),
+ aData.AsDecodedData()->mEndTime.ToMicroseconds(),
+ segment.GetDuration());
+ mBufferedData.AppendFrom(&segment);
+ return;
+ }
+ if (aData.IsEOS()) {
+ MOZ_ASSERT(!Ended());
+ LOG("Received EOS");
+ mReceivedEOS = true;
+ return;
+ }
+ if (aData.IsClearFutureData()) {
+ LOG("Clear future data");
+ mBufferedData.Clear();
+ if (!Ended()) {
+ LOG("Clear EOS");
+ mReceivedEOS = false;
+ }
+ return;
+ }
+ MOZ_ASSERT_UNREACHABLE("unsupported SPSC data");
+}
+
+TrackTime AudioDecoderInputTrack::AppendBufferedDataToOutput(
+ TrackTime aExpectedDuration) {
+ AssertOnGraphThread();
+
+ // Remove the necessary part from `mBufferedData` to create a new
+ // segment in order to apply some operation without affecting all data.
+ AudioSegment outputSegment;
+ TrackTime consumedDuration = 0;
+ if (mPlaybackRate != 1.0) {
+ consumedDuration =
+ AppendTimeStretchedDataToSegment(aExpectedDuration, outputSegment);
+ } else {
+ consumedDuration =
+ AppendUnstretchedDataToSegment(aExpectedDuration, outputSegment);
+ }
+
+ // Apply any necessary change on the segement which would be outputed to the
+ // graph.
+ const TrackTime appendedDuration = outputSegment.GetDuration();
+ outputSegment.ApplyVolume(mVolume);
+ ApplyTrackDisabling(&outputSegment);
+ mSegment->AppendFrom(&outputSegment);
+
+ LOG("Appended %" PRId64 ", consumed %" PRId64
+ ", remaining raw buffered %" PRId64 ", remaining time-stretched %u",
+ appendedDuration, consumedDuration, mBufferedData.GetDuration(),
+ mTimeStretcher ? mTimeStretcher->numSamples() : 0);
+ if (auto gap = aExpectedDuration - appendedDuration; gap > 0) {
+ LOG("Audio underrun, fill silence %" PRId64, gap);
+ MOZ_ASSERT(mBufferedData.IsEmpty());
+ mSegment->AppendNullData(gap);
+ }
+ return consumedDuration;
+}
+
+TrackTime AudioDecoderInputTrack::AppendTimeStretchedDataToSegment(
+ TrackTime aExpectedDuration, AudioSegment& aOutput) {
+ AssertOnGraphThread();
+ EnsureTimeStretcher();
+
+ MOZ_ASSERT(mPlaybackRate != 1.0f);
+ MOZ_ASSERT(aExpectedDuration >= 0);
+ MOZ_ASSERT(mTimeStretcher);
+ MOZ_ASSERT(aOutput.IsEmpty());
+
+ // If we don't have enough data that have been time-stretched, fill raw data
+ // into the time stretcher until the amount of samples that time stretcher
+ // finishes processed reaches or exceeds the expected duration.
+ TrackTime consumedDuration = 0;
+ if (mTimeStretcher->numSamples() < aExpectedDuration) {
+ consumedDuration = FillDataToTimeStretcher(aExpectedDuration);
+ }
+ MOZ_ASSERT(consumedDuration >= 0);
+ Unused << GetDataFromTimeStretcher(aExpectedDuration, aOutput);
+ return consumedDuration;
+}
+
+TrackTime AudioDecoderInputTrack::FillDataToTimeStretcher(
+ TrackTime aExpectedDuration) {
+ AssertOnGraphThread();
+ MOZ_ASSERT(mPlaybackRate != 1.0f);
+ MOZ_ASSERT(aExpectedDuration >= 0);
+ MOZ_ASSERT(mTimeStretcher);
+
+ TrackTime consumedDuration = 0;
+ const uint32_t channels = GetChannelCountForTimeStretcher();
+ mBufferedData.IterateOnChunks([&](AudioChunk* aChunk) {
+ MOZ_ASSERT(aChunk);
+ if (aChunk->IsNull() && aChunk->GetDuration() == 0) {
+ // Skip this chunk and wait for next one.
+ return false;
+ }
+ const uint32_t bufferLength = channels * aChunk->GetDuration();
+ if (bufferLength > mInterleavedBuffer.Capacity()) {
+ mInterleavedBuffer.SetCapacity(bufferLength);
+ }
+ mInterleavedBuffer.SetLengthAndRetainStorage(bufferLength);
+ if (aChunk->IsNull()) {
+ MOZ_ASSERT(aChunk->GetDuration(), "chunk with only silence");
+ memset(mInterleavedBuffer.Elements(), 0, mInterleavedBuffer.Length());
+ } else {
+ // Do the up-mix/down-mix first if necessary that forces to change the
+ // data's channel count to the time stretcher's channel count. Then
+ // perform a transformation from planar to interleaved.
+ switch (aChunk->mBufferFormat) {
+ case AUDIO_FORMAT_S16:
+ WriteChunk<int16_t>(*aChunk, channels, 1.0f,
+ mInterleavedBuffer.Elements());
+ break;
+ case AUDIO_FORMAT_FLOAT32:
+ WriteChunk<float>(*aChunk, channels, 1.0f,
+ mInterleavedBuffer.Elements());
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Not expected format");
+ }
+ }
+ mTimeStretcher->putSamples(mInterleavedBuffer.Elements(),
+ aChunk->GetDuration());
+ consumedDuration += aChunk->GetDuration();
+ return mTimeStretcher->numSamples() >= aExpectedDuration;
+ });
+ mBufferedData.RemoveLeading(consumedDuration);
+ return consumedDuration;
+}
+
+TrackTime AudioDecoderInputTrack::AppendUnstretchedDataToSegment(
+ TrackTime aExpectedDuration, AudioSegment& aOutput) {
+ AssertOnGraphThread();
+ MOZ_ASSERT(mPlaybackRate == 1.0f);
+ MOZ_ASSERT(aExpectedDuration >= 0);
+ MOZ_ASSERT(aOutput.IsEmpty());
+
+ const TrackTime drained =
+ DrainStretchedDataIfNeeded(aExpectedDuration, aOutput);
+ const TrackTime available =
+ std::min(aExpectedDuration - drained, mBufferedData.GetDuration());
+ aOutput.AppendSlice(mBufferedData, 0, available);
+ MOZ_ASSERT(aOutput.GetDuration() <= aExpectedDuration);
+ mBufferedData.RemoveLeading(available);
+ return available;
+}
+
+TrackTime AudioDecoderInputTrack::DrainStretchedDataIfNeeded(
+ TrackTime aExpectedDuration, AudioSegment& aOutput) {
+ AssertOnGraphThread();
+ MOZ_ASSERT(mPlaybackRate == 1.0f);
+ MOZ_ASSERT(aExpectedDuration >= 0);
+
+ if (!mTimeStretcher) {
+ return 0;
+ }
+ if (mTimeStretcher->numSamples() == 0) {
+ return 0;
+ }
+ return GetDataFromTimeStretcher(aExpectedDuration, aOutput);
+}
+
+TrackTime AudioDecoderInputTrack::GetDataFromTimeStretcher(
+ TrackTime aExpectedDuration, AudioSegment& aOutput) {
+ AssertOnGraphThread();
+ MOZ_ASSERT(mTimeStretcher);
+ MOZ_ASSERT(aExpectedDuration >= 0);
+
+ if (HasSentAllData() && mTimeStretcher->numUnprocessedSamples()) {
+ mTimeStretcher->flush();
+ LOG("Flush %u frames from the time stretcher",
+ mTimeStretcher->numSamples());
+ }
+
+ const TrackTime available =
+ std::min((TrackTime)mTimeStretcher->numSamples(), aExpectedDuration);
+ if (available == 0) {
+ // Either running out of stretched data, or the raw data we filled into
+ // the time stretcher were not enough for producing stretched data.
+ return 0;
+ }
+
+ // Retrieve interleaved data from the time stretcher.
+ const uint32_t channelCount = GetChannelCountForTimeStretcher();
+ const uint32_t bufferLength = channelCount * available;
+ if (bufferLength > mInterleavedBuffer.Capacity()) {
+ mInterleavedBuffer.SetCapacity(bufferLength);
+ }
+ mInterleavedBuffer.SetLengthAndRetainStorage(bufferLength);
+ mTimeStretcher->receiveSamples(mInterleavedBuffer.Elements(), available);
+
+ // Perform a transformation from interleaved to planar.
+ CheckedInt<size_t> bufferSize(sizeof(AudioDataValue));
+ bufferSize *= bufferLength;
+ RefPtr<SharedBuffer> buffer = SharedBuffer::Create(bufferSize);
+ AudioDataValue* bufferData = static_cast<AudioDataValue*>(buffer->Data());
+ AutoTArray<AudioDataValue*, 2> planarBuffer;
+ planarBuffer.SetLength(channelCount);
+ for (size_t idx = 0; idx < channelCount; idx++) {
+ planarBuffer[idx] = bufferData + idx * available;
+ }
+ DeinterleaveAndConvertBuffer(mInterleavedBuffer.Elements(), available,
+ channelCount, planarBuffer.Elements());
+ AutoTArray<const AudioDataValue*, 2> outputChannels;
+ outputChannels.AppendElements(planarBuffer);
+ aOutput.AppendFrames(buffer.forget(), outputChannels,
+ static_cast<int32_t>(available),
+ mBufferedData.GetOldestPrinciple());
+ return available;
+}
+
+void AudioDecoderInputTrack::NotifyInTheEndOfProcessInput(
+ TrackTime aFillDuration) {
+ AssertOnGraphThread();
+ mWrittenFrames += aFillDuration;
+ LOG("Notify, fill=%" PRId64 ", total written=%" PRId64 ", ended=%d",
+ aFillDuration, mWrittenFrames, Ended());
+ if (aFillDuration > 0) {
+ mOnOutput.Notify(mWrittenFrames);
+ }
+ if (Ended()) {
+ mOnEnd.Notify();
+ }
+}
+
+bool AudioDecoderInputTrack::HasSentAllData() const {
+ AssertOnGraphThread();
+ return mReceivedEOS && mSPSCQueue.AvailableRead() == 0 &&
+ mBufferedData.IsEmpty();
+}
+
+uint32_t AudioDecoderInputTrack::NumberOfChannels() const {
+ AssertOnGraphThread();
+ const uint32_t maxChannelCount = GetData<AudioSegment>()->MaxChannelCount();
+ return maxChannelCount ? maxChannelCount : mInitialInputChannels;
+}
+
+void AudioDecoderInputTrack::EnsureTimeStretcher() {
+ AssertOnGraphThread();
+ if (!mTimeStretcher) {
+ mTimeStretcher = soundtouch::createSoundTouchObj();
+ mTimeStretcher->setSampleRate(GraphImpl()->GraphRate());
+ mTimeStretcher->setChannels(GetChannelCountForTimeStretcher());
+ mTimeStretcher->setPitch(1.0);
+
+ // SoundTouch v2.1.2 uses automatic time-stretch settings with the following
+ // values:
+ // Tempo 0.5: 90ms sequence, 20ms seekwindow, 8ms overlap
+ // Tempo 2.0: 40ms sequence, 15ms seekwindow, 8ms overlap
+ // We are going to use a smaller 10ms sequence size to improve speech
+ // clarity, giving more resolution at high tempo and less reverb at low
+ // tempo. Maintain 15ms seekwindow and 8ms overlap for smoothness.
+ mTimeStretcher->setSetting(
+ SETTING_SEQUENCE_MS,
+ StaticPrefs::media_audio_playbackrate_soundtouch_sequence_ms());
+ mTimeStretcher->setSetting(
+ SETTING_SEEKWINDOW_MS,
+ StaticPrefs::media_audio_playbackrate_soundtouch_seekwindow_ms());
+ mTimeStretcher->setSetting(
+ SETTING_OVERLAP_MS,
+ StaticPrefs::media_audio_playbackrate_soundtouch_overlap_ms());
+ SetTempoAndRateForTimeStretcher();
+ LOG("Create TimeStretcher (channel=%d, playbackRate=%f, preservePitch=%d)",
+ GetChannelCountForTimeStretcher(), mPlaybackRate, mPreservesPitch);
+ }
+}
+
+void AudioDecoderInputTrack::SetTempoAndRateForTimeStretcher() {
+ AssertOnGraphThread();
+ if (!mTimeStretcher) {
+ return;
+ }
+ if (mPreservesPitch) {
+ mTimeStretcher->setTempo(mPlaybackRate);
+ mTimeStretcher->setRate(1.0f);
+ } else {
+ mTimeStretcher->setTempo(1.0f);
+ mTimeStretcher->setRate(mPlaybackRate);
+ }
+}
+
+uint32_t AudioDecoderInputTrack::GetChannelCountForTimeStretcher() const {
+ // The time stretcher MUST be initialized with a fixed channel count, but the
+ // channel count in audio chunks might vary. Therefore, we always use the
+ // initial input channel count to initialize the time stretcher and perform a
+ // real-time down-mix/up-mix for audio chunks which have different channel
+ // count than the initial input channel count.
+ return mInitialInputChannels;
+}
+
+#undef LOG
+} // namespace mozilla
diff --git a/dom/media/mediasink/AudioDecoderInputTrack.h b/dom/media/mediasink/AudioDecoderInputTrack.h
new file mode 100644
index 0000000000..8c82d7bed6
--- /dev/null
+++ b/dom/media/mediasink/AudioDecoderInputTrack.h
@@ -0,0 +1,242 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef AudioDecoderInputTrack_h
+#define AudioDecoderInputTrack_h
+
+#include "AudioSegment.h"
+#include "MediaEventSource.h"
+#include "MediaTimer.h"
+#include "MediaTrackGraph.h"
+#include "MediaTrackGraphImpl.h"
+#include "MediaSegment.h"
+#include "mozilla/SPSCQueue.h"
+#include "mozilla/StateMirroring.h"
+#include "nsISerialEventTarget.h"
+
+namespace soundtouch {
+class MOZ_EXPORT SoundTouch;
+}
+
+namespace mozilla {
+
+class AudioData;
+
+/**
+ * AudioDecoderInputTrack is used as a source for the audio decoder data, which
+ * supports adjusting playback rate and preserve pitch.
+ * The owner of this track would be responsible to push audio data via
+ * `AppendData()` into a SPSC queue, which is a thread-safe queue between the
+ * decoder thread (producer) and the graph thread (consumer). MediaTrackGraph
+ * requires data via `ProcessInput()`, then AudioDecoderInputTrack would convert
+ * (based on sample rate and playback rate) and append the amount of needed
+ * audio frames onto the output segment that would be used by MediaTrackGraph.
+ */
+class AudioDecoderInputTrack final : public ProcessedMediaTrack {
+ public:
+ static AudioDecoderInputTrack* Create(MediaTrackGraph* aGraph,
+ nsISerialEventTarget* aDecoderThread,
+ const AudioInfo& aInfo,
+ float aPlaybackRate, float aVolume,
+ bool aPreservesPitch);
+
+ // SPSCData suppports filling different supported type variants, and is used
+ // to achieve a thread-safe information exchange between the decoder thread
+ // and the graph thread.
+ struct SPSCData final {
+ struct Empty {};
+ struct ClearFutureData {};
+ struct DecodedData {
+ DecodedData()
+ : mStartTime(media::TimeUnit::Invalid()),
+ mEndTime(media::TimeUnit::Invalid()) {}
+ DecodedData(DecodedData&& aDecodedData)
+ : mSegment(std::move(aDecodedData.mSegment)) {
+ mStartTime = aDecodedData.mStartTime;
+ mEndTime = aDecodedData.mEndTime;
+ aDecodedData.Clear();
+ }
+ DecodedData(media::TimeUnit aStartTime, media::TimeUnit aEndTime)
+ : mStartTime(aStartTime), mEndTime(aEndTime) {}
+ DecodedData(const DecodedData&) = delete;
+ DecodedData& operator=(const DecodedData&) = delete;
+ void Clear() {
+ mSegment.Clear();
+ mStartTime = media::TimeUnit::Invalid();
+ mEndTime = media::TimeUnit::Invalid();
+ }
+ AudioSegment mSegment;
+ media::TimeUnit mStartTime;
+ media::TimeUnit mEndTime;
+ };
+ struct EOS {};
+
+ SPSCData() : mData(Empty()){};
+ explicit SPSCData(ClearFutureData&& aArg) : mData(std::move(aArg)){};
+ explicit SPSCData(DecodedData&& aArg) : mData(std::move(aArg)){};
+ explicit SPSCData(EOS&& aArg) : mData(std::move(aArg)){};
+
+ bool HasData() const { return !mData.is<Empty>(); }
+ bool IsClearFutureData() const { return mData.is<ClearFutureData>(); }
+ bool IsDecodedData() const { return mData.is<DecodedData>(); }
+ bool IsEOS() const { return mData.is<EOS>(); }
+
+ DecodedData* AsDecodedData() {
+ return IsDecodedData() ? &mData.as<DecodedData>() : nullptr;
+ }
+
+ Variant<Empty, ClearFutureData, DecodedData, EOS> mData;
+ };
+
+ // Decoder thread API
+ void AppendData(AudioData* aAudio, const PrincipalHandle& aPrincipalHandle);
+ void AppendData(nsTArray<RefPtr<AudioData>>& aAudioArray,
+ const PrincipalHandle& aPrincipalHandle);
+ void NotifyEndOfStream();
+ void ClearFutureData();
+ void SetVolume(float aVolume);
+ void SetPlaybackRate(float aPlaybackRate);
+ void SetPreservesPitch(bool aPreservesPitch);
+ // After calling this, the track are not expected to receive any new data.
+ void Close();
+ bool HasBatchedData() const;
+
+ MediaEventSource<int64_t>& OnOutput() { return mOnOutput; }
+ MediaEventSource<void>& OnEnd() { return mOnEnd; }
+
+ // Graph Thread API
+ void DestroyImpl() override;
+ void ProcessInput(GraphTime aFrom, GraphTime aTo, uint32_t aFlags) override;
+ uint32_t NumberOfChannels() const override;
+
+ // The functions below are only used for testing.
+ TrackTime WrittenFrames() const {
+ AssertOnGraphThread();
+ return mWrittenFrames;
+ }
+ float Volume() const {
+ AssertOnGraphThread();
+ return mVolume;
+ }
+ float PlaybackRate() const {
+ AssertOnGraphThread();
+ return mPlaybackRate;
+ }
+
+ protected:
+ ~AudioDecoderInputTrack();
+
+ private:
+ AudioDecoderInputTrack(nsISerialEventTarget* aDecoderThread,
+ TrackRate aGraphRate, const AudioInfo& aInfo,
+ float aPlaybackRate, float aVolume,
+ bool aPreservesPitch);
+
+ // Return false if the converted segment contains zero duration.
+ bool ConvertAudioDataToSegment(AudioData* aAudio, AudioSegment& aSegment,
+ const PrincipalHandle& aPrincipalHandle);
+
+ void HandleSPSCData(SPSCData& aData);
+
+ // These methods would return the total frames that we consumed from
+ // `mBufferedData`.
+ TrackTime AppendBufferedDataToOutput(TrackTime aExpectedDuration);
+ TrackTime FillDataToTimeStretcher(TrackTime aExpectedDuration);
+ TrackTime AppendTimeStretchedDataToSegment(TrackTime aExpectedDuration,
+ AudioSegment& aOutput);
+ TrackTime AppendUnstretchedDataToSegment(TrackTime aExpectedDuration,
+ AudioSegment& aOutput);
+
+ // Return the total frames that we retrieve from the time stretcher.
+ TrackTime DrainStretchedDataIfNeeded(TrackTime aExpectedDuration,
+ AudioSegment& aOutput);
+ TrackTime GetDataFromTimeStretcher(TrackTime aExpectedDuration,
+ AudioSegment& aOutput);
+ void NotifyInTheEndOfProcessInput(TrackTime aFillDuration);
+
+ bool HasSentAllData() const;
+
+ bool ShouldBatchData() const;
+ void BatchData(AudioData* aAudio, const PrincipalHandle& aPrincipalHandle);
+ void DispatchPushBatchedDataIfNeeded();
+ void PushBatchedDataIfNeeded();
+ void PushDataToSPSCQueue(SPSCData& data);
+
+ void SetVolumeImpl(float aVolume);
+ void SetPlaybackRateImpl(float aPlaybackRate);
+ void SetPreservesPitchImpl(bool aPreservesPitch);
+
+ void EnsureTimeStretcher();
+ void SetTempoAndRateForTimeStretcher();
+ uint32_t GetChannelCountForTimeStretcher() const;
+
+ inline void AssertOnDecoderThread() const {
+ MOZ_ASSERT(mDecoderThread->IsOnCurrentThread());
+ }
+ inline void AssertOnGraphThread() const {
+ MOZ_ASSERT(GraphImpl()->OnGraphThread());
+ }
+ inline void AssertOnGraphThreadOrNotRunning() const {
+ MOZ_ASSERT(GraphImpl()->OnGraphThreadOrNotRunning());
+ }
+
+ const RefPtr<nsISerialEventTarget> mDecoderThread;
+
+ // Notify the amount of audio frames which have been sent to the track.
+ MediaEventProducer<int64_t> mOnOutput;
+ // Notify when the track is ended.
+ MediaEventProducer<void> mOnEnd;
+
+ // These variables are ONLY used in the decoder thread.
+ nsAutoRef<SpeexResamplerState> mResampler;
+ uint32_t mResamplerChannelCount;
+ const uint32_t mInitialInputChannels;
+ TrackRate mInputSampleRate;
+ DelayedScheduler mDelayedScheduler;
+ bool mShutdownSPSCQueue = false;
+
+ // These attributes are ONLY used in the graph thread.
+ bool mReceivedEOS = false;
+ TrackTime mWrittenFrames = 0;
+ float mPlaybackRate;
+ float mVolume;
+ bool mPreservesPitch;
+
+ // A thread-safe queue shared by the decoder thread and the graph thread.
+ // The decoder thread is the producer side, and the graph thread is the
+ // consumer side. This queue should NEVER get full. In order to achieve that,
+ // we would batch input samples when SPSC queue doesn't have many available
+ // capacity.
+ // In addition, as the media track isn't guaranteed to be destroyed on the
+ // graph thread (it could be destroyed on the main thread as well) so we might
+ // not clear all data in SPSC queue when the track's `DestroyImpl()` gets
+ // called. We leave to destroy the queue later when the track gets destroyed.
+ SPSCQueue<SPSCData> mSPSCQueue{40};
+
+ // When the graph requires the less amount of audio frames than the amount of
+ // frames an audio data has, then the remaining part of frames would be stored
+ // and used in next iteration.
+ // This is ONLY used in the graph thread.
+ AudioSegment mBufferedData;
+
+ // In order to prevent SPSC queue from being full, we want to batch multiple
+ // data into one to control the density of SPSC queue, the length of batched
+ // data would be dynamically adjusted by queue's available capacity.
+ // This is ONLY used in the decoder thread.
+ SPSCData::DecodedData mBatchedData;
+
+ // True if we've sent all data to the graph, then the track will be marked as
+ // ended in the next iteration.
+ bool mSentAllData = false;
+
+ // This is used to adjust the playback rate and pitch.
+ soundtouch::SoundTouch* mTimeStretcher = nullptr;
+
+ // Buffers that would be used for the time stretching.
+ AutoTArray<AudioDataValue, 2> mInterleavedBuffer;
+};
+
+} // namespace mozilla
+
+#endif // AudioDecoderInputTrack_h
diff --git a/dom/media/mediasink/AudioSink.cpp b/dom/media/mediasink/AudioSink.cpp
new file mode 100644
index 0000000000..536a2a4f8a
--- /dev/null
+++ b/dom/media/mediasink/AudioSink.cpp
@@ -0,0 +1,664 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioSink.h"
+#include "AudioConverter.h"
+#include "AudioDeviceInfo.h"
+#include "MediaQueue.h"
+#include "VideoUtils.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/IntegerPrintfMacros.h"
+#include "mozilla/ProfilerMarkerTypes.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "nsPrintfCString.h"
+#include "Tracing.h"
+
+namespace mozilla {
+
+mozilla::LazyLogModule gAudioSinkLog("AudioSink");
+#define SINK_LOG(msg, ...) \
+ MOZ_LOG(gAudioSinkLog, LogLevel::Debug, \
+ ("AudioSink=%p " msg, this, ##__VA_ARGS__))
+#define SINK_LOG_V(msg, ...) \
+ MOZ_LOG(gAudioSinkLog, LogLevel::Verbose, \
+ ("AudioSink=%p " msg, this, ##__VA_ARGS__))
+
+// The amount of audio frames that is used to fuzz rounding errors.
+static const int64_t AUDIO_FUZZ_FRAMES = 1;
+
+using media::TimeUnit;
+
+AudioSink::AudioSink(AbstractThread* aThread,
+ MediaQueue<AudioData>& aAudioQueue, const AudioInfo& aInfo,
+ bool aShouldResistFingerprinting)
+ : mPlaying(true),
+ mWritten(0),
+ mErrored(false),
+ mOwnerThread(aThread),
+ mFramesParsed(0),
+ mOutputRate(
+ DecideAudioPlaybackSampleRate(aInfo, aShouldResistFingerprinting)),
+ mOutputChannels(DecideAudioPlaybackChannels(aInfo)),
+ mAudibilityMonitor(
+ mOutputRate,
+ StaticPrefs::dom_media_silence_duration_for_audibility()),
+ mIsAudioDataAudible(false),
+ mProcessedQueueFinished(false),
+ mAudioQueue(aAudioQueue),
+ mProcessedQueueThresholdMS(
+ StaticPrefs::media_audio_audiosink_threshold_ms()) {
+ // Not much to initialize here if there's no audio.
+ if (!aInfo.IsValid()) {
+ mProcessedSPSCQueue = MakeUnique<SPSCQueue<AudioDataValue>>(0);
+ return;
+ }
+ // Twice the limit that trigger a refill.
+ double capacitySeconds = mProcessedQueueThresholdMS / 1000.f * 2;
+ // Clamp to correct boundaries, and align on the channel count
+ int elementCount = static_cast<int>(
+ std::clamp(capacitySeconds * mOutputChannels * mOutputRate, 0.,
+ std::numeric_limits<int>::max() - 1.));
+ elementCount -= elementCount % mOutputChannels;
+ mProcessedSPSCQueue = MakeUnique<SPSCQueue<AudioDataValue>>(elementCount);
+ SINK_LOG("Ringbuffer has space for %u elements (%lf seconds)",
+ mProcessedSPSCQueue->Capacity(),
+ static_cast<float>(elementCount) / mOutputChannels / mOutputRate);
+ // Determine if the data is likely to be audible when the stream will be
+ // ready, if possible.
+ RefPtr<AudioData> frontPacket = mAudioQueue.PeekFront();
+ if (frontPacket) {
+ mAudibilityMonitor.ProcessInterleaved(frontPacket->Data(),
+ frontPacket->mChannels);
+ mIsAudioDataAudible = mAudibilityMonitor.RecentlyAudible();
+ SINK_LOG("New AudioSink -- audio is likely to be %s",
+ mIsAudioDataAudible ? "audible" : "inaudible");
+ } else {
+ // If no packets are available, consider the audio audible.
+ mIsAudioDataAudible = true;
+ SINK_LOG(
+ "New AudioSink -- no audio packet avaialble, considering the stream "
+ "audible");
+ }
+}
+
+AudioSink::~AudioSink() {
+ // Generally instances of AudioSink should be properly Shutdown manually.
+ // The only way deleting an AudioSink without shutdown an happen is if the
+ // dispatch back to the MDSM thread after initializing it asynchronously
+ // fails. When that's the case, the stream has been initialized but not
+ // started. Manually shutdown the AudioStream in this case.
+ if (mAudioStream) {
+ mAudioStream->Shutdown();
+ }
+}
+
+nsresult AudioSink::InitializeAudioStream(
+ const PlaybackParams& aParams, const RefPtr<AudioDeviceInfo>& aAudioDevice,
+ AudioSink::InitializationType aInitializationType) {
+ if (aInitializationType == AudioSink::InitializationType::UNMUTING) {
+ // Consider the stream to be audible immediately, before initialization
+ // finishes when unmuting, in case initialization takes some time and it
+ // looked audible when the AudioSink was created.
+ mAudibleEvent.Notify(mIsAudioDataAudible);
+ SINK_LOG("InitializeAudioStream (Unmuting) notifying that audio is %s",
+ mIsAudioDataAudible ? "audible" : "inaudible");
+ } else {
+ // If not unmuting, the audibility event will be dispatched as usual,
+ // inspecting the audio content as it's being played and signaling the
+ // audibility event when a different in state is detected.
+ SINK_LOG("InitializeAudioStream (initial)");
+ mIsAudioDataAudible = false;
+ }
+
+ // When AudioQueue is empty, there is no way to know the channel layout of
+ // the coming audio data, so we use the predefined channel map instead.
+ AudioConfig::ChannelLayout::ChannelMap channelMap =
+ AudioConfig::ChannelLayout(mOutputChannels).Map();
+ // The layout map used here is already processed by mConverter with
+ // mOutputChannels into SMPTE format, so there is no need to worry if
+ // StaticPrefs::accessibility_monoaudio_enable() or
+ // StaticPrefs::media_forcestereo_enabled() is applied.
+ MOZ_ASSERT(!mAudioStream);
+ mAudioStream =
+ new AudioStream(*this, mOutputRate, mOutputChannels, channelMap);
+ nsresult rv = mAudioStream->Init(aAudioDevice);
+ if (NS_FAILED(rv)) {
+ mAudioStream->Shutdown();
+ mAudioStream = nullptr;
+ return rv;
+ }
+
+ // Set playback params before calling Start() so they can take effect
+ // as soon as the 1st DataCallback of the AudioStream fires.
+ mAudioStream->SetVolume(aParams.mVolume);
+ mAudioStream->SetPlaybackRate(aParams.mPlaybackRate);
+ mAudioStream->SetPreservesPitch(aParams.mPreservesPitch);
+
+ return NS_OK;
+}
+
+nsresult AudioSink::Start(
+ const media::TimeUnit& aStartTime,
+ MozPromiseHolder<MediaSink::EndedPromise>& aEndedPromise) {
+ MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
+
+ mAudioQueueListener = mAudioQueue.PushEvent().Connect(
+ mOwnerThread, this, &AudioSink::OnAudioPushed);
+ mAudioQueueFinishListener = mAudioQueue.FinishEvent().Connect(
+ mOwnerThread, this, &AudioSink::NotifyAudioNeeded);
+ mProcessedQueueListener =
+ mAudioPopped.Connect(mOwnerThread, this, &AudioSink::OnAudioPopped);
+
+ mStartTime = aStartTime;
+
+ // To ensure at least one audio packet will be popped from AudioQueue and
+ // ready to be played.
+ NotifyAudioNeeded();
+
+ return mAudioStream->Start(aEndedPromise);
+}
+
+TimeUnit AudioSink::GetPosition() {
+ int64_t tmp;
+ if (mAudioStream && (tmp = mAudioStream->GetPosition()) >= 0) {
+ TimeUnit pos = TimeUnit::FromMicroseconds(tmp);
+ NS_ASSERTION(pos >= mLastGoodPosition,
+ "AudioStream position shouldn't go backward");
+ TimeUnit tmp = mStartTime + pos;
+ if (!tmp.IsValid()) {
+ mErrored = true;
+ return mStartTime + mLastGoodPosition;
+ }
+ // Update the last good position when we got a good one.
+ if (pos >= mLastGoodPosition) {
+ mLastGoodPosition = pos;
+ }
+ }
+
+ return mStartTime + mLastGoodPosition;
+}
+
+bool AudioSink::HasUnplayedFrames() {
+ // Experimentation suggests that GetPositionInFrames() is zero-indexed,
+ // so we need to add 1 here before comparing it to mWritten.
+ return mProcessedSPSCQueue->AvailableRead() ||
+ (mAudioStream && mAudioStream->GetPositionInFrames() + 1 < mWritten);
+}
+
+TimeUnit AudioSink::UnplayedDuration() const {
+ return TimeUnit::FromMicroseconds(AudioQueuedInRingBufferMS());
+}
+
+void AudioSink::ReenqueueUnplayedAudioDataIfNeeded() {
+ // This is OK: the AudioStream has been shut down. Shutdown guarantees that
+ // the audio callback thread won't call back again.
+ mProcessedSPSCQueue->ResetThreadIds();
+
+ // construct an AudioData
+ int sampleInRingbuffer = mProcessedSPSCQueue->AvailableRead();
+
+ if (!sampleInRingbuffer) {
+ return;
+ }
+
+ uint32_t channelCount;
+ uint32_t rate;
+ if (mConverter) {
+ channelCount = mConverter->OutputConfig().Channels();
+ rate = mConverter->OutputConfig().Rate();
+ } else {
+ channelCount = mOutputChannels;
+ rate = mOutputRate;
+ }
+
+ uint32_t framesRemaining = sampleInRingbuffer / channelCount;
+
+ nsTArray<AlignedAudioBuffer> packetsToReenqueue;
+ RefPtr<AudioData> frontPacket = mAudioQueue.PeekFront();
+ uint32_t offset;
+ TimeUnit time;
+ uint32_t typicalPacketFrameCount;
+ // Extrapolate mOffset, mTime from the front of the queue
+ // We can't really find a good value for `mOffset`, so we take what we have
+ // at the front of the queue.
+ // For `mTime`, assume there hasn't been a discontinuity recently.
+ if (!frontPacket) {
+ // We do our best here, but it's not going to be perfect.
+ typicalPacketFrameCount = 1024; // typical for e.g. AAC
+ offset = 0;
+ time = GetPosition();
+ } else {
+ typicalPacketFrameCount = frontPacket->Frames();
+ offset = frontPacket->mOffset;
+ time = frontPacket->mTime;
+ }
+
+ // Extract all audio data from the ring buffer, we can only read the data from
+ // the most recent, so we reenqueue the data, packetized, in a temporary
+ // array.
+ while (framesRemaining) {
+ uint32_t packetFrameCount =
+ std::min(framesRemaining, typicalPacketFrameCount);
+ framesRemaining -= packetFrameCount;
+
+ int packetSampleCount = packetFrameCount * channelCount;
+ AlignedAudioBuffer packetData(packetSampleCount);
+ DebugOnly<int> samplesRead =
+ mProcessedSPSCQueue->Dequeue(packetData.Data(), packetSampleCount);
+ MOZ_ASSERT(samplesRead == packetSampleCount);
+
+ packetsToReenqueue.AppendElement(packetData);
+ }
+ // Reenqueue in the audio queue in correct order in the audio queue, starting
+ // with the end of the temporary array.
+ while (!packetsToReenqueue.IsEmpty()) {
+ auto packetData = packetsToReenqueue.PopLastElement();
+ uint32_t packetFrameCount = packetData.Length() / channelCount;
+ auto duration = TimeUnit(packetFrameCount, rate);
+ if (!duration.IsValid()) {
+ NS_WARNING("Int overflow in AudioSink");
+ mErrored = true;
+ return;
+ }
+ time -= duration;
+ RefPtr<AudioData> packet =
+ new AudioData(offset, time, std::move(packetData), channelCount, rate);
+ MOZ_DIAGNOSTIC_ASSERT(duration == packet->mDuration, "must be equal");
+
+ SINK_LOG(
+ "Muting: Pushing back %u frames (%lfms) from the ring buffer back into "
+ "the audio queue at pts %lf",
+ packetFrameCount, 1000 * static_cast<float>(packetFrameCount) / rate,
+ time.ToSeconds());
+ // The audio data's timestamp would be adjusted already if we're in looping,
+ // so we don't want to adjust them again.
+ mAudioQueue.PushFront(packet,
+ MediaQueue<AudioData>::TimestampAdjustment::Disable);
+ }
+}
+
+Maybe<MozPromiseHolder<MediaSink::EndedPromise>> AudioSink::Shutdown(
+ ShutdownCause aShutdownCause) {
+ MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
+
+ mAudioQueueListener.DisconnectIfExists();
+ mAudioQueueFinishListener.DisconnectIfExists();
+ mProcessedQueueListener.DisconnectIfExists();
+
+ Maybe<MozPromiseHolder<MediaSink::EndedPromise>> rv;
+
+ if (mAudioStream) {
+ rv = mAudioStream->Shutdown(aShutdownCause);
+ mAudioStream = nullptr;
+ if (aShutdownCause == ShutdownCause::Muting) {
+ ReenqueueUnplayedAudioDataIfNeeded();
+ }
+ }
+ mProcessedQueueFinished = true;
+
+ return rv;
+}
+
+void AudioSink::SetVolume(double aVolume) {
+ if (mAudioStream) {
+ mAudioStream->SetVolume(aVolume);
+ }
+}
+
+void AudioSink::SetStreamName(const nsAString& aStreamName) {
+ if (mAudioStream) {
+ mAudioStream->SetStreamName(aStreamName);
+ }
+}
+
+void AudioSink::SetPlaybackRate(double aPlaybackRate) {
+ MOZ_ASSERT(aPlaybackRate != 0,
+ "Don't set the playbackRate to 0 on AudioStream");
+ if (mAudioStream) {
+ mAudioStream->SetPlaybackRate(aPlaybackRate);
+ }
+}
+
+void AudioSink::SetPreservesPitch(bool aPreservesPitch) {
+ if (mAudioStream) {
+ mAudioStream->SetPreservesPitch(aPreservesPitch);
+ }
+}
+
+void AudioSink::SetPlaying(bool aPlaying) {
+ if (!mAudioStream || mAudioStream->IsPlaybackCompleted() ||
+ mPlaying == aPlaying) {
+ return;
+ }
+ // pause/resume AudioStream as necessary.
+ if (!aPlaying) {
+ mAudioStream->Pause();
+ } else if (aPlaying) {
+ mAudioStream->Resume();
+ }
+ mPlaying = aPlaying;
+}
+
+TimeUnit AudioSink::GetEndTime() const {
+ uint64_t written = mWritten;
+ TimeUnit played = media::TimeUnit(written, mOutputRate) + mStartTime;
+ if (!played.IsValid()) {
+ NS_WARNING("Int overflow calculating audio end time");
+ return TimeUnit::Zero();
+ }
+ // As we may be resampling, rounding errors may occur. Ensure we never get
+ // past the original end time.
+ return std::min(mLastEndTime, played);
+}
+
+uint32_t AudioSink::PopFrames(AudioDataValue* aBuffer, uint32_t aFrames,
+ bool aAudioThreadChanged) {
+ // This is safe, because we have the guarantee, by the OS, that audio
+ // callbacks are never called concurrently. Audio thread changes can only
+ // happen when not using cubeb remoting, and often when changing audio device
+ // at the system level.
+ if (aAudioThreadChanged) {
+ mProcessedSPSCQueue->ResetThreadIds();
+ }
+
+ TRACE_COMMENT("AudioSink::PopFrames", "%u frames (ringbuffer: %u/%u)",
+ aFrames, SampleToFrame(mProcessedSPSCQueue->AvailableRead()),
+ SampleToFrame(mProcessedSPSCQueue->Capacity()));
+
+ const int samplesToPop = static_cast<int>(aFrames * mOutputChannels);
+ const int samplesRead = mProcessedSPSCQueue->Dequeue(aBuffer, samplesToPop);
+ auto sampleOut = samplesRead;
+ MOZ_ASSERT(samplesRead % mOutputChannels == 0);
+ mWritten += SampleToFrame(samplesRead);
+ if (samplesRead != samplesToPop) {
+ if (Ended()) {
+ SINK_LOG("Last PopFrames -- Source ended.");
+ } else if (mTreatUnderrunAsSilence) {
+ SINK_LOG("Treat underrun frames (%u) as silence frames",
+ SampleToFrame(samplesToPop - samplesRead));
+ sampleOut = samplesToPop;
+ } else {
+ NS_WARNING("Underrun when popping samples from audiosink ring buffer.");
+ TRACE_COMMENT("AudioSink::PopFrames", "Underrun %u frames missing",
+ SampleToFrame(samplesToPop - samplesRead));
+ }
+ // silence the rest
+ PodZero(aBuffer + samplesRead, samplesToPop - samplesRead);
+ }
+
+ mAudioPopped.Notify();
+
+ SINK_LOG_V("Popping %u frames. Remaining in ringbuffer %u / %u\n", aFrames,
+ SampleToFrame(mProcessedSPSCQueue->AvailableRead()),
+ SampleToFrame(mProcessedSPSCQueue->Capacity()));
+ CheckIsAudible(Span(aBuffer, sampleOut), mOutputChannels);
+
+ return SampleToFrame(sampleOut);
+}
+
+bool AudioSink::Ended() const {
+ // Return true when error encountered so AudioStream can start draining.
+ // Both atomic so we don't need locking
+ return mProcessedQueueFinished || mErrored;
+}
+
+void AudioSink::CheckIsAudible(const Span<AudioDataValue>& aInterleaved,
+ size_t aChannel) {
+ mAudibilityMonitor.ProcessInterleaved(aInterleaved, aChannel);
+ bool isAudible = mAudibilityMonitor.RecentlyAudible();
+
+ if (isAudible != mIsAudioDataAudible) {
+ mIsAudioDataAudible = isAudible;
+ SINK_LOG("Notifying that audio is now %s",
+ mIsAudioDataAudible ? "audible" : "inaudible");
+ mAudibleEvent.Notify(mIsAudioDataAudible);
+ }
+}
+
+void AudioSink::OnAudioPopped() {
+ SINK_LOG_V("AudioStream has used an audio packet.");
+ NotifyAudioNeeded();
+}
+
+void AudioSink::OnAudioPushed(const RefPtr<AudioData>& aSample) {
+ SINK_LOG_V("One new audio packet available.");
+ NotifyAudioNeeded();
+}
+
+uint32_t AudioSink::AudioQueuedInRingBufferMS() const {
+ return static_cast<uint32_t>(
+ 1000 * SampleToFrame(mProcessedSPSCQueue->AvailableRead()) / mOutputRate);
+}
+
+uint32_t AudioSink::SampleToFrame(uint32_t aSamples) const {
+ return aSamples / mOutputChannels;
+}
+
+void AudioSink::NotifyAudioNeeded() {
+ MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn(),
+ "Not called from the owner's thread");
+
+ while (mAudioQueue.GetSize() &&
+ AudioQueuedInRingBufferMS() <
+ static_cast<uint32_t>(mProcessedQueueThresholdMS)) {
+ // Check if there's room in our ring buffer.
+ if (mAudioQueue.PeekFront()->Frames() >
+ SampleToFrame(mProcessedSPSCQueue->AvailableWrite())) {
+ SINK_LOG_V("Can't push %u frames. In ringbuffer %u / %u\n",
+ mAudioQueue.PeekFront()->Frames(),
+ SampleToFrame(mProcessedSPSCQueue->AvailableRead()),
+ SampleToFrame(mProcessedSPSCQueue->Capacity()));
+ return;
+ }
+ SINK_LOG_V("Pushing %u frames. In ringbuffer %u / %u\n",
+ mAudioQueue.PeekFront()->Frames(),
+ SampleToFrame(mProcessedSPSCQueue->AvailableRead()),
+ SampleToFrame(mProcessedSPSCQueue->Capacity()));
+ RefPtr<AudioData> data = mAudioQueue.PopFront();
+
+ // Ignore the element with 0 frames and try next.
+ if (!data->Frames()) {
+ continue;
+ }
+
+ if (!mConverter ||
+ (data->mRate != mConverter->InputConfig().Rate() ||
+ data->mChannels != mConverter->InputConfig().Channels())) {
+ SINK_LOG_V("Audio format changed from %u@%uHz to %u@%uHz",
+ mConverter ? mConverter->InputConfig().Channels() : 0,
+ mConverter ? mConverter->InputConfig().Rate() : 0,
+ data->mChannels, data->mRate);
+
+ DrainConverter(SampleToFrame(mProcessedSPSCQueue->AvailableWrite()));
+
+ // mFramesParsed indicates the current playtime in frames at the current
+ // input sampling rate. Recalculate it per the new sampling rate.
+ if (mFramesParsed) {
+ // We minimize overflow.
+ uint32_t oldRate = mConverter->InputConfig().Rate();
+ uint32_t newRate = data->mRate;
+ CheckedInt64 result = SaferMultDiv(mFramesParsed, newRate, oldRate);
+ if (!result.isValid()) {
+ NS_WARNING("Int overflow in AudioSink");
+ mErrored = true;
+ return;
+ }
+ mFramesParsed = result.value();
+ }
+
+ const AudioConfig::ChannelLayout inputLayout =
+ data->mChannelMap
+ ? AudioConfig::ChannelLayout::SMPTEDefault(data->mChannelMap)
+ : AudioConfig::ChannelLayout(data->mChannels);
+ const AudioConfig::ChannelLayout outputLayout =
+ mOutputChannels == data->mChannels
+ ? inputLayout
+ : AudioConfig::ChannelLayout(mOutputChannels);
+ AudioConfig inConfig =
+ AudioConfig(inputLayout, data->mChannels, data->mRate);
+ AudioConfig outConfig =
+ AudioConfig(outputLayout, mOutputChannels, mOutputRate);
+ if (!AudioConverter::CanConvert(inConfig, outConfig)) {
+ mErrored = true;
+ return;
+ }
+ mConverter = MakeUnique<AudioConverter>(inConfig, outConfig);
+ }
+
+ // See if there's a gap in the audio. If there is, push silence into the
+ // audio hardware, so we can play across the gap.
+ // Calculate the timestamp of the next chunk of audio in numbers of
+ // samples.
+ CheckedInt64 sampleTime =
+ TimeUnitToFrames(data->mTime - mStartTime, data->mRate);
+ // Calculate the number of frames that have been pushed onto the audio
+ // hardware.
+ CheckedInt64 missingFrames = sampleTime - mFramesParsed;
+
+ if (!missingFrames.isValid() || !sampleTime.isValid()) {
+ NS_WARNING("Int overflow in AudioSink");
+ mErrored = true;
+ return;
+ }
+
+ if (missingFrames.value() > AUDIO_FUZZ_FRAMES) {
+ // The next audio packet begins some time after the end of the last packet
+ // we pushed to the audio hardware. We must push silence into the audio
+ // hardware so that the next audio packet begins playback at the correct
+ // time. But don't push more than the ring buffer can receive.
+ missingFrames = std::min<int64_t>(
+ std::min<int64_t>(INT32_MAX, missingFrames.value()),
+ SampleToFrame(mProcessedSPSCQueue->AvailableWrite()));
+ mFramesParsed += missingFrames.value();
+
+ SINK_LOG("Gap in the audio input, push %" PRId64 " frames of silence",
+ missingFrames.value());
+
+ RefPtr<AudioData> silenceData;
+ AlignedAudioBuffer silenceBuffer(missingFrames.value() * data->mChannels);
+ if (!silenceBuffer) {
+ NS_WARNING("OOM in AudioSink");
+ mErrored = true;
+ return;
+ }
+ if (mConverter->InputConfig() != mConverter->OutputConfig()) {
+ AlignedAudioBuffer convertedData =
+ mConverter->Process(AudioSampleBuffer(std::move(silenceBuffer)))
+ .Forget();
+ silenceData = CreateAudioFromBuffer(std::move(convertedData), data);
+ } else {
+ silenceData = CreateAudioFromBuffer(std::move(silenceBuffer), data);
+ }
+ TRACE("Pushing silence");
+ PushProcessedAudio(silenceData);
+ }
+
+ mLastEndTime = data->GetEndTime();
+ mFramesParsed += data->Frames();
+
+ if (mConverter->InputConfig() != mConverter->OutputConfig()) {
+ AlignedAudioBuffer buffer(data->MoveableData());
+ AlignedAudioBuffer convertedData =
+ mConverter->Process(AudioSampleBuffer(std::move(buffer))).Forget();
+ data = CreateAudioFromBuffer(std::move(convertedData), data);
+ }
+ if (PushProcessedAudio(data)) {
+ mLastProcessedPacket = Some(data);
+ }
+ }
+
+ if (mAudioQueue.IsFinished() && mAudioQueue.GetSize() == 0) {
+ // We have reached the end of the data, drain the resampler.
+ DrainConverter(SampleToFrame(mProcessedSPSCQueue->AvailableWrite()));
+ mProcessedQueueFinished = true;
+ }
+}
+
+uint32_t AudioSink::PushProcessedAudio(AudioData* aData) {
+ if (!aData || !aData->Frames()) {
+ return 0;
+ }
+ int framesToEnqueue = static_cast<int>(aData->Frames() * aData->mChannels);
+ TRACE_COMMENT("AudioSink::PushProcessedAudio", "%u frames (%u/%u)",
+ framesToEnqueue,
+ SampleToFrame(mProcessedSPSCQueue->AvailableWrite()),
+ SampleToFrame(mProcessedSPSCQueue->Capacity()));
+ DebugOnly<int> rv =
+ mProcessedSPSCQueue->Enqueue(aData->Data().Elements(), framesToEnqueue);
+ NS_WARNING_ASSERTION(
+ rv == static_cast<int>(aData->Frames() * aData->mChannels),
+ "AudioSink ring buffer over-run, can't push new data");
+ return aData->Frames();
+}
+
+already_AddRefed<AudioData> AudioSink::CreateAudioFromBuffer(
+ AlignedAudioBuffer&& aBuffer, AudioData* aReference) {
+ uint32_t frames = SampleToFrame(aBuffer.Length());
+ if (!frames) {
+ return nullptr;
+ }
+ auto duration = media::TimeUnit(frames, mOutputRate);
+ if (!duration.IsValid()) {
+ NS_WARNING("Int overflow in AudioSink");
+ mErrored = true;
+ return nullptr;
+ }
+ RefPtr<AudioData> data =
+ new AudioData(aReference->mOffset, aReference->mTime, std::move(aBuffer),
+ mOutputChannels, mOutputRate);
+ MOZ_DIAGNOSTIC_ASSERT(duration == data->mDuration, "must be equal");
+ return data.forget();
+}
+
+uint32_t AudioSink::DrainConverter(uint32_t aMaxFrames) {
+ MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
+
+ if (!mConverter || !mLastProcessedPacket || !aMaxFrames) {
+ // nothing to drain.
+ return 0;
+ }
+
+ RefPtr<AudioData> lastPacket = mLastProcessedPacket.ref();
+ mLastProcessedPacket.reset();
+
+ // To drain we simply provide an empty packet to the audio converter.
+ AlignedAudioBuffer convertedData =
+ mConverter->Process(AudioSampleBuffer(AlignedAudioBuffer())).Forget();
+
+ uint32_t frames = SampleToFrame(convertedData.Length());
+ if (!convertedData.SetLength(std::min(frames, aMaxFrames) *
+ mOutputChannels)) {
+ // This can never happen as we were reducing the length of convertData.
+ mErrored = true;
+ return 0;
+ }
+
+ RefPtr<AudioData> data =
+ CreateAudioFromBuffer(std::move(convertedData), lastPacket);
+ return PushProcessedAudio(data);
+}
+
+void AudioSink::GetDebugInfo(dom::MediaSinkDebugInfo& aInfo) {
+ MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
+ aInfo.mAudioSinkWrapper.mAudioSink.mStartTime = mStartTime.ToMicroseconds();
+ aInfo.mAudioSinkWrapper.mAudioSink.mLastGoodPosition =
+ mLastGoodPosition.ToMicroseconds();
+ aInfo.mAudioSinkWrapper.mAudioSink.mIsPlaying = mPlaying;
+ aInfo.mAudioSinkWrapper.mAudioSink.mOutputRate = mOutputRate;
+ aInfo.mAudioSinkWrapper.mAudioSink.mWritten = mWritten;
+ aInfo.mAudioSinkWrapper.mAudioSink.mHasErrored = bool(mErrored);
+ aInfo.mAudioSinkWrapper.mAudioSink.mPlaybackComplete =
+ mAudioStream ? mAudioStream->IsPlaybackCompleted() : false;
+}
+
+void AudioSink::EnableTreatAudioUnderrunAsSilence(bool aEnabled) {
+ SINK_LOG("set mTreatUnderrunAsSilence=%d", aEnabled);
+ mTreatUnderrunAsSilence = aEnabled;
+}
+
+} // namespace mozilla
diff --git a/dom/media/mediasink/AudioSink.h b/dom/media/mediasink/AudioSink.h
new file mode 100644
index 0000000000..856227ee4c
--- /dev/null
+++ b/dom/media/mediasink/AudioSink.h
@@ -0,0 +1,188 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef AudioSink_h__
+#define AudioSink_h__
+
+#include "AudioStream.h"
+#include "AudibilityMonitor.h"
+#include "MediaEventSource.h"
+#include "MediaInfo.h"
+#include "MediaQueue.h"
+#include "MediaSink.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Result.h"
+#include "nsISupportsImpl.h"
+
+namespace mozilla {
+
+class AudioConverter;
+
+class AudioSink : private AudioStream::DataSource {
+ public:
+ enum class InitializationType {
+ // This AudioSink is being initialized for the first time
+ INITIAL,
+ UNMUTING
+ };
+ struct PlaybackParams {
+ PlaybackParams(double aVolume, double aPlaybackRate, bool aPreservesPitch)
+ : mVolume(aVolume),
+ mPlaybackRate(aPlaybackRate),
+ mPreservesPitch(aPreservesPitch) {}
+ double mVolume;
+ double mPlaybackRate;
+ bool mPreservesPitch;
+ };
+
+ AudioSink(AbstractThread* aThread, MediaQueue<AudioData>& aAudioQueue,
+ const AudioInfo& aInfo, bool aShouldResistFingerprinting);
+
+ ~AudioSink();
+
+ // Allocate and initialize mAudioStream. Returns NS_OK on success.
+ nsresult InitializeAudioStream(const PlaybackParams& aParams,
+ const RefPtr<AudioDeviceInfo>& aAudioDevice,
+ InitializationType aInitializationType);
+
+ // Start audio playback.
+ nsresult Start(const media::TimeUnit& aStartTime,
+ MozPromiseHolder<MediaSink::EndedPromise>& aEndedPromise);
+
+ /*
+ * All public functions are not thread-safe.
+ * Called on the task queue of MDSM only.
+ */
+ media::TimeUnit GetPosition();
+ media::TimeUnit GetEndTime() const;
+
+ // Check whether we've pushed more frames to the audio stream than it
+ // has played.
+ bool HasUnplayedFrames();
+
+ // The duration of the buffered frames.
+ media::TimeUnit UnplayedDuration() const;
+
+ // Shut down the AudioSink's resources. If an AudioStream existed, return the
+ // ended promise it had, if it's shutting down-mid stream becaues it's muting.
+ Maybe<MozPromiseHolder<MediaSink::EndedPromise>> Shutdown(
+ ShutdownCause aShutdownCause = ShutdownCause::Regular);
+
+ void SetVolume(double aVolume);
+ void SetStreamName(const nsAString& aStreamName);
+ void SetPlaybackRate(double aPlaybackRate);
+ void SetPreservesPitch(bool aPreservesPitch);
+ void SetPlaying(bool aPlaying);
+
+ MediaEventSource<bool>& AudibleEvent() { return mAudibleEvent; }
+
+ void GetDebugInfo(dom::MediaSinkDebugInfo& aInfo);
+
+ // This returns true if the audio callbacks are being called, and so the
+ // audio stream-based clock is moving forward.
+ bool AudioStreamCallbackStarted() {
+ return mAudioStream && mAudioStream->CallbackStarted();
+ }
+
+ void UpdateStartTime(const media::TimeUnit& aStartTime) {
+ mStartTime = aStartTime;
+ }
+
+ void EnableTreatAudioUnderrunAsSilence(bool aEnabled);
+
+ private:
+ // Interface of AudioStream::DataSource.
+ // Called on the callback thread of cubeb. Returns the number of frames that
+ // were available.
+ uint32_t PopFrames(AudioDataValue* aBuffer, uint32_t aFrames,
+ bool aAudioThreadChanged) override;
+ bool Ended() const override;
+
+ // When shutting down, it's important to not lose any audio data, it might be
+ // still of use, in two scenarios:
+ // - If the audio is now captured to a MediaStream, whatever is enqueued in
+ // the ring buffer needs to be played out now ;
+ // - If the AudioSink is shutting down because the audio is muted, it's
+ // important to keep the audio around in case it's quickly unmuted,
+ // and in general to keep A/V sync correct when unmuted.
+ void ReenqueueUnplayedAudioDataIfNeeded();
+
+ void CheckIsAudible(const Span<AudioDataValue>& aInterleaved,
+ size_t aChannel);
+
+ // The audio stream resource. Used on the task queue of MDSM only.
+ RefPtr<AudioStream> mAudioStream;
+
+ // The presentation time of the first audio frame that was played.
+ // We can add this to the audio stream position to determine
+ // the current audio time.
+ media::TimeUnit mStartTime;
+
+ // Keep the last good position returned from the audio stream. Used to ensure
+ // position returned by GetPosition() is mono-increasing in spite of audio
+ // stream error. Used on the task queue of MDSM only.
+ media::TimeUnit mLastGoodPosition;
+
+ // Used on the task queue of MDSM only.
+ bool mPlaying;
+
+ // PCM frames written to the stream so far. Written on the callback thread,
+ // read on the MDSM thread.
+ Atomic<int64_t> mWritten;
+
+ // True if there is any error in processing audio data like overflow.
+ Atomic<bool> mErrored;
+
+ const RefPtr<AbstractThread> mOwnerThread;
+
+ // Audio Processing objects and methods
+ void OnAudioPopped();
+ void OnAudioPushed(const RefPtr<AudioData>& aSample);
+ void NotifyAudioNeeded();
+ // Drain the converter and add the output to the processed audio queue.
+ // A maximum of aMaxFrames will be added.
+ uint32_t DrainConverter(uint32_t aMaxFrames = UINT32_MAX);
+ already_AddRefed<AudioData> CreateAudioFromBuffer(
+ AlignedAudioBuffer&& aBuffer, AudioData* aReference);
+ // Add data to the processsed queue return the number of frames added.
+ uint32_t PushProcessedAudio(AudioData* aData);
+ uint32_t AudioQueuedInRingBufferMS() const;
+ uint32_t SampleToFrame(uint32_t aSamples) const;
+ UniquePtr<AudioConverter> mConverter;
+ UniquePtr<SPSCQueue<AudioDataValue>> mProcessedSPSCQueue;
+ MediaEventListener mAudioQueueListener;
+ MediaEventListener mAudioQueueFinishListener;
+ MediaEventListener mProcessedQueueListener;
+ // Number of frames processed from mAudioQueue. Used to determine gaps in
+ // the input stream. It indicates the time in frames since playback started
+ // at the current input framerate.
+ int64_t mFramesParsed;
+ Maybe<RefPtr<AudioData>> mLastProcessedPacket;
+ media::TimeUnit mLastEndTime;
+ // Never modifed after construction.
+ uint32_t mOutputRate;
+ uint32_t mOutputChannels;
+ AudibilityMonitor mAudibilityMonitor;
+ bool mIsAudioDataAudible;
+ MediaEventProducer<bool> mAudibleEvent;
+ // Only signed on the real-time audio thread.
+ MediaEventProducer<void> mAudioPopped;
+
+ Atomic<bool> mProcessedQueueFinished;
+ MediaQueue<AudioData>& mAudioQueue;
+ const float mProcessedQueueThresholdMS;
+
+ // True if we'd like to treat underrun as silent frames. But that can only be
+ // applied in the special situation for seamless looping.
+ bool mTreatUnderrunAsSilence = false;
+};
+
+} // namespace mozilla
+
+#endif // AudioSink_h__
diff --git a/dom/media/mediasink/AudioSinkWrapper.cpp b/dom/media/mediasink/AudioSinkWrapper.cpp
new file mode 100644
index 0000000000..5a006479e1
--- /dev/null
+++ b/dom/media/mediasink/AudioSinkWrapper.cpp
@@ -0,0 +1,496 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioSinkWrapper.h"
+#include "AudioDeviceInfo.h"
+#include "AudioSink.h"
+#include "VideoUtils.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Result.h"
+#include "nsPrintfCString.h"
+
+mozilla::LazyLogModule gAudioSinkWrapperLog("AudioSinkWrapper");
+#define LOG(...) \
+ MOZ_LOG(gAudioSinkWrapperLog, mozilla::LogLevel::Debug, (__VA_ARGS__));
+#define LOGV(...) \
+ MOZ_LOG(gAudioSinkWrapperLog, mozilla::LogLevel::Verbose, (__VA_ARGS__));
+
+namespace mozilla {
+
+using media::TimeUnit;
+
+AudioSinkWrapper::~AudioSinkWrapper() = default;
+
+void AudioSinkWrapper::Shutdown() {
+ AssertOwnerThread();
+ MOZ_ASSERT(!mIsStarted, "Must be called after playback stopped.");
+ mCreator = nullptr;
+ mEndedPromiseHolder.ResolveIfExists(true, __func__);
+}
+
+RefPtr<MediaSink::EndedPromise> AudioSinkWrapper::OnEnded(TrackType aType) {
+ AssertOwnerThread();
+ MOZ_ASSERT(mIsStarted, "Must be called after playback starts.");
+ if (aType == TrackInfo::kAudioTrack) {
+ return mEndedPromise;
+ }
+ return nullptr;
+}
+
+TimeUnit AudioSinkWrapper::GetEndTime(TrackType aType) const {
+ AssertOwnerThread();
+ MOZ_ASSERT(mIsStarted, "Must be called after playback starts.");
+ if (aType == TrackInfo::kAudioTrack && mAudioSink &&
+ mAudioSink->AudioStreamCallbackStarted()) {
+ return mAudioSink->GetEndTime();
+ }
+
+ if (aType == TrackInfo::kAudioTrack && !mAudioSink && IsMuted()) {
+ if (IsPlaying()) {
+ return GetSystemClockPosition(TimeStamp::Now());
+ }
+
+ return mPlayDuration;
+ }
+ return TimeUnit::Zero();
+}
+
+TimeUnit AudioSinkWrapper::GetSystemClockPosition(TimeStamp aNow) const {
+ AssertOwnerThread();
+ MOZ_ASSERT(!mPlayStartTime.IsNull());
+ // Time elapsed since we started playing.
+ double delta = (aNow - mPlayStartTime).ToSeconds();
+ // Take playback rate into account.
+ return mPlayDuration + TimeUnit::FromSeconds(delta * mParams.mPlaybackRate);
+}
+
+bool AudioSinkWrapper::IsMuted() const {
+ AssertOwnerThread();
+ return mParams.mVolume == 0.0;
+}
+
+TimeUnit AudioSinkWrapper::GetPosition(TimeStamp* aTimeStamp) {
+ AssertOwnerThread();
+ MOZ_ASSERT(mIsStarted, "Must be called after playback starts.");
+
+ TimeUnit pos;
+ TimeStamp t = TimeStamp::Now();
+
+ if (!mAudioEnded && !IsMuted() && mAudioSink) {
+ if (mLastClockSource == ClockSource::SystemClock) {
+ TimeUnit switchTime = GetSystemClockPosition(t);
+ // Update the _actual_ start time of the audio stream now that it has
+ // started, preventing any clock discontinuity.
+ mAudioSink->UpdateStartTime(switchTime);
+ LOGV("%p: switching to audio clock at media time %lf", this,
+ switchTime.ToSeconds());
+ }
+ // Rely on the audio sink to report playback position when it is not ended.
+ pos = mAudioSink->GetPosition();
+ LOGV("%p: Getting position from the Audio Sink %lf", this, pos.ToSeconds());
+ mLastClockSource = ClockSource::AudioStream;
+ } else if (!mPlayStartTime.IsNull()) {
+ // Calculate playback position using system clock if we are still playing,
+ // but not rendering the audio, because this audio sink is muted.
+ pos = GetSystemClockPosition(t);
+ LOGV("%p: Getting position from the system clock %lf", this,
+ pos.ToSeconds());
+ if (IsMuted()) {
+ if (mAudioQueue.GetSize() > 0) {
+ // audio track, but it's muted and won't be dequeued, discard packets
+ // that are behind the current media time, to keep the queue size under
+ // control.
+ DropAudioPacketsIfNeeded(pos);
+ }
+ // If muted, it's necessary to manually check if the audio has "ended",
+ // meaning that all the audio packets have been consumed, to resolve the
+ // ended promise.
+ if (CheckIfEnded()) {
+ MOZ_ASSERT(!mAudioSink);
+ mEndedPromiseHolder.ResolveIfExists(true, __func__);
+ }
+ }
+ mLastClockSource = ClockSource::SystemClock;
+ } else {
+ // Return how long we've played if we are not playing.
+ pos = mPlayDuration;
+ LOGV("%p: Getting static position, not playing %lf", this, pos.ToSeconds());
+ mLastClockSource = ClockSource::Paused;
+ }
+
+ if (aTimeStamp) {
+ *aTimeStamp = t;
+ }
+
+ return pos;
+}
+
+bool AudioSinkWrapper::CheckIfEnded() const {
+ return mAudioQueue.IsFinished() && mAudioQueue.GetSize() == 0u;
+}
+
+bool AudioSinkWrapper::HasUnplayedFrames(TrackType aType) const {
+ AssertOwnerThread();
+ return mAudioSink ? mAudioSink->HasUnplayedFrames() : false;
+}
+
+media::TimeUnit AudioSinkWrapper::UnplayedDuration(TrackType aType) const {
+ AssertOwnerThread();
+ return mAudioSink ? mAudioSink->UnplayedDuration() : media::TimeUnit::Zero();
+}
+
+void AudioSinkWrapper::DropAudioPacketsIfNeeded(
+ const TimeUnit& aMediaPosition) {
+ RefPtr<AudioData> audio = mAudioQueue.PeekFront();
+ uint32_t dropped = 0;
+ while (audio && audio->mTime + audio->mDuration < aMediaPosition) {
+ // drop this packet, try the next one
+ audio = mAudioQueue.PopFront();
+ dropped++;
+ if (audio) {
+ LOGV(
+ "Dropping audio packets: media position: %lf, "
+ "packet dropped: [%lf, %lf] (%u so far).\n",
+ aMediaPosition.ToSeconds(), audio->mTime.ToSeconds(),
+ (audio->GetEndTime()).ToSeconds(), dropped);
+ }
+ audio = mAudioQueue.PeekFront();
+ }
+}
+
+void AudioSinkWrapper::OnMuted(bool aMuted) {
+ AssertOwnerThread();
+ LOG("%p: AudioSinkWrapper::OnMuted(%s)", this, aMuted ? "true" : "false");
+ // Nothing to do
+ if (mAudioEnded) {
+ LOG("%p: AudioSinkWrapper::OnMuted, but no audio track", this);
+ return;
+ }
+ if (aMuted) {
+ if (mAudioSink) {
+ LOG("AudioSinkWrapper muted, shutting down AudioStream.");
+ mAudioSinkEndedPromise.DisconnectIfExists();
+ if (IsPlaying()) {
+ mPlayDuration = mAudioSink->GetPosition();
+ mPlayStartTime = TimeStamp::Now();
+ }
+ Maybe<MozPromiseHolder<MediaSink::EndedPromise>> rv =
+ mAudioSink->Shutdown(ShutdownCause::Muting);
+ // There will generally be a promise here, except if the stream has
+ // errored out, or if it has just finished. In both cases, the promise has
+ // been handled appropriately, there is nothing to do.
+ if (rv.isSome()) {
+ mEndedPromiseHolder = std::move(rv.ref());
+ }
+ mAudioSink = nullptr;
+ }
+ } else {
+ if (!IsPlaying()) {
+ LOG("%p: AudioSinkWrapper::OnMuted: not playing, not re-creating an "
+ "AudioSink",
+ this);
+ return;
+ }
+ LOG("%p: AudioSinkWrapper unmuted, re-creating an AudioStream.", this);
+ TimeUnit mediaPosition = GetSystemClockPosition(TimeStamp::Now());
+ nsresult rv = StartAudioSink(mediaPosition, AudioSinkStartPolicy::ASYNC);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "Could not start AudioSink from AudioSinkWrapper when unmuting");
+ }
+ }
+}
+
+void AudioSinkWrapper::SetVolume(double aVolume) {
+ AssertOwnerThread();
+
+ bool wasMuted = mParams.mVolume == 0;
+ bool nowMuted = aVolume == 0.;
+ mParams.mVolume = aVolume;
+
+ if (!wasMuted && nowMuted) {
+ OnMuted(true);
+ } else if (wasMuted && !nowMuted) {
+ OnMuted(false);
+ }
+
+ if (mAudioSink) {
+ mAudioSink->SetVolume(aVolume);
+ }
+}
+
+void AudioSinkWrapper::SetStreamName(const nsAString& aStreamName) {
+ AssertOwnerThread();
+ if (mAudioSink) {
+ mAudioSink->SetStreamName(aStreamName);
+ }
+}
+
+void AudioSinkWrapper::SetPlaybackRate(double aPlaybackRate) {
+ AssertOwnerThread();
+ if (!mAudioEnded && mAudioSink) {
+ // Pass the playback rate to the audio sink. The underlying AudioStream
+ // will handle playback rate changes and report correct audio position.
+ mAudioSink->SetPlaybackRate(aPlaybackRate);
+ } else if (!mPlayStartTime.IsNull()) {
+ // Adjust playback duration and start time when we are still playing.
+ TimeStamp now = TimeStamp::Now();
+ mPlayDuration = GetSystemClockPosition(now);
+ mPlayStartTime = now;
+ }
+ // mParams.mPlaybackRate affects GetSystemClockPosition(). It should be
+ // updated after the calls to GetSystemClockPosition();
+ mParams.mPlaybackRate = aPlaybackRate;
+
+ // Do nothing when not playing. Changes in playback rate will be taken into
+ // account by GetSystemClockPosition().
+}
+
+void AudioSinkWrapper::SetPreservesPitch(bool aPreservesPitch) {
+ AssertOwnerThread();
+ mParams.mPreservesPitch = aPreservesPitch;
+ if (mAudioSink) {
+ mAudioSink->SetPreservesPitch(aPreservesPitch);
+ }
+}
+
+void AudioSinkWrapper::SetPlaying(bool aPlaying) {
+ AssertOwnerThread();
+ LOG("%p: AudioSinkWrapper::SetPlaying %s", this, aPlaying ? "true" : "false");
+
+ // Resume/pause matters only when playback started.
+ if (!mIsStarted) {
+ return;
+ }
+
+ if (mAudioSink) {
+ mAudioSink->SetPlaying(aPlaying);
+ } else {
+ if (aPlaying) {
+ LOG("%p: AudioSinkWrapper::SetPlaying : starting an AudioSink", this);
+ TimeUnit switchTime = GetPosition();
+ DropAudioPacketsIfNeeded(switchTime);
+ StartAudioSink(switchTime, AudioSinkStartPolicy::SYNC);
+ }
+ }
+
+ if (aPlaying) {
+ MOZ_ASSERT(mPlayStartTime.IsNull());
+ mPlayStartTime = TimeStamp::Now();
+ } else {
+ // Remember how long we've played.
+ mPlayDuration = GetPosition();
+ // mPlayStartTime must be updated later since GetPosition()
+ // depends on the value of mPlayStartTime.
+ mPlayStartTime = TimeStamp();
+ }
+}
+
+double AudioSinkWrapper::PlaybackRate() const {
+ AssertOwnerThread();
+ return mParams.mPlaybackRate;
+}
+
+nsresult AudioSinkWrapper::Start(const TimeUnit& aStartTime,
+ const MediaInfo& aInfo) {
+ LOG("%p AudioSinkWrapper::Start", this);
+ AssertOwnerThread();
+ MOZ_ASSERT(!mIsStarted, "playback already started.");
+
+ mIsStarted = true;
+ mPlayDuration = aStartTime;
+ mPlayStartTime = TimeStamp::Now();
+ mAudioEnded = IsAudioSourceEnded(aInfo);
+
+ if (mAudioEnded) {
+ // Resolve promise if we start playback at the end position of the audio.
+ mEndedPromise =
+ aInfo.HasAudio()
+ ? MediaSink::EndedPromise::CreateAndResolve(true, __func__)
+ : nullptr;
+ return NS_OK;
+ }
+
+ return StartAudioSink(aStartTime, AudioSinkStartPolicy::SYNC);
+}
+
+nsresult AudioSinkWrapper::StartAudioSink(const TimeUnit& aStartTime,
+ AudioSinkStartPolicy aPolicy) {
+ MOZ_RELEASE_ASSERT(!mAudioSink);
+
+ nsresult rv = NS_OK;
+
+ mAudioSinkEndedPromise.DisconnectIfExists();
+ mEndedPromise = mEndedPromiseHolder.Ensure(__func__);
+ mEndedPromise
+ ->Then(mOwnerThread.get(), __func__, this,
+ &AudioSinkWrapper::OnAudioEnded, &AudioSinkWrapper::OnAudioEnded)
+ ->Track(mAudioSinkEndedPromise);
+
+ LOG("%p: AudioSinkWrapper::StartAudioSink (%s)", this,
+ aPolicy == AudioSinkStartPolicy::ASYNC ? "Async" : "Sync");
+
+ if (IsMuted()) {
+ LOG("%p: Muted: not starting an audio sink", this);
+ return NS_OK;
+ }
+ LOG("%p: Not muted: starting a new audio sink", this);
+ if (aPolicy == AudioSinkStartPolicy::ASYNC) {
+ UniquePtr<AudioSink> audioSink;
+ audioSink.reset(mCreator->Create());
+ NS_DispatchBackgroundTask(NS_NewRunnableFunction(
+ "StartAudioSink (Async part: initialization)",
+ [self = RefPtr<AudioSinkWrapper>(this), audioSink{std::move(audioSink)},
+ this]() mutable {
+ LOG("AudioSink initialization on background thread");
+ // This can take about 200ms, e.g. on Windows, we don't want to do
+ // it on the MDSM thread, because it would make the clock not update
+ // for that amount of time, and the video would therefore not
+ // update. The Start() call is very cheap on the other hand, we can
+ // do it from the MDSM thread.
+ nsresult rv = audioSink->InitializeAudioStream(
+ mParams, mAudioDevice, AudioSink::InitializationType::UNMUTING);
+ mOwnerThread->Dispatch(NS_NewRunnableFunction(
+ "StartAudioSink (Async part: start from MDSM thread)",
+ [self = RefPtr<AudioSinkWrapper>(this),
+ audioSink{std::move(audioSink)}, this, rv]() mutable {
+ LOG("AudioSink async init done, back on MDSM thread");
+ if (NS_FAILED(rv)) {
+ LOG("Async AudioSink initialization failed");
+ mEndedPromiseHolder.RejectIfExists(rv, __func__);
+ return;
+ }
+
+ // It's possible that the newly created isn't needed at this
+ // point, in some cases:
+ // 1. An AudioSink was created synchronously while this
+ // AudioSink was initialized asynchronously, bail out here. This
+ // happens when seeking (which does a synchronous
+ // initialization) right after unmuting.
+ // 2. The media element was muted while the async initialization
+ // was happening.
+ // 3. The AudioSinkWrapper was stopped during asynchronous
+ // creation.
+ // 4. The AudioSinkWrapper was paused during asynchronous
+ // creation.
+ if (mAudioSink || IsMuted() || !mIsStarted ||
+ mPlayStartTime.IsNull()) {
+ LOG("AudioSink initialized async isn't needed, shutting "
+ "it down.");
+ DebugOnly<Maybe<MozPromiseHolder<EndedPromise>>> rv =
+ audioSink->Shutdown();
+ MOZ_ASSERT(rv.inspect().isNothing());
+ return;
+ }
+
+ MOZ_ASSERT(!mAudioSink);
+ TimeUnit switchTime = GetPosition();
+ DropAudioPacketsIfNeeded(switchTime);
+ mAudioSink.swap(audioSink);
+ if (mTreatUnderrunAsSilence) {
+ mAudioSink->EnableTreatAudioUnderrunAsSilence(
+ mTreatUnderrunAsSilence);
+ }
+ LOG("AudioSink async, start");
+ nsresult rv2 =
+ mAudioSink->Start(switchTime, mEndedPromiseHolder);
+ if (NS_FAILED(rv2)) {
+ LOG("Async AudioSinkWrapper start failed");
+ mEndedPromiseHolder.RejectIfExists(rv2, __func__);
+ }
+ }));
+ }));
+ } else {
+ mAudioSink.reset(mCreator->Create());
+ nsresult rv = mAudioSink->InitializeAudioStream(
+ mParams, mAudioDevice, AudioSink::InitializationType::INITIAL);
+ if (NS_FAILED(rv)) {
+ mEndedPromiseHolder.RejectIfExists(rv, __func__);
+ LOG("Sync AudioSinkWrapper initialization failed");
+ return rv;
+ }
+ if (mTreatUnderrunAsSilence) {
+ mAudioSink->EnableTreatAudioUnderrunAsSilence(mTreatUnderrunAsSilence);
+ }
+ rv = mAudioSink->Start(aStartTime, mEndedPromiseHolder);
+ if (NS_FAILED(rv)) {
+ LOG("Sync AudioSinkWrapper start failed");
+ mEndedPromiseHolder.RejectIfExists(rv, __func__);
+ }
+ }
+
+ return rv;
+}
+
+bool AudioSinkWrapper::IsAudioSourceEnded(const MediaInfo& aInfo) const {
+ // no audio or empty audio queue which won't get data anymore is equivalent to
+ // audio ended
+ return !aInfo.HasAudio() ||
+ (mAudioQueue.IsFinished() && mAudioQueue.GetSize() == 0u);
+}
+
+void AudioSinkWrapper::Stop() {
+ AssertOwnerThread();
+ MOZ_ASSERT(mIsStarted, "playback not started.");
+
+ LOG("%p: AudioSinkWrapper::Stop", this);
+
+ mIsStarted = false;
+ mAudioEnded = true;
+
+ mAudioSinkEndedPromise.DisconnectIfExists();
+
+ if (mAudioSink) {
+ DebugOnly<Maybe<MozPromiseHolder<EndedPromise>>> rv =
+ mAudioSink->Shutdown();
+ MOZ_ASSERT(rv.inspect().isNothing());
+ mAudioSink = nullptr;
+ mEndedPromise = nullptr;
+ }
+}
+
+bool AudioSinkWrapper::IsStarted() const {
+ AssertOwnerThread();
+ return mIsStarted;
+}
+
+bool AudioSinkWrapper::IsPlaying() const {
+ AssertOwnerThread();
+ return IsStarted() && !mPlayStartTime.IsNull();
+}
+
+void AudioSinkWrapper::OnAudioEnded() {
+ AssertOwnerThread();
+ LOG("%p: AudioSinkWrapper::OnAudioEnded", this);
+ mAudioSinkEndedPromise.Complete();
+ mPlayDuration = GetPosition();
+ if (!mPlayStartTime.IsNull()) {
+ mPlayStartTime = TimeStamp::Now();
+ }
+ mAudioEnded = true;
+}
+
+void AudioSinkWrapper::GetDebugInfo(dom::MediaSinkDebugInfo& aInfo) {
+ AssertOwnerThread();
+ aInfo.mAudioSinkWrapper.mIsPlaying = IsPlaying();
+ aInfo.mAudioSinkWrapper.mIsStarted = IsStarted();
+ aInfo.mAudioSinkWrapper.mAudioEnded = mAudioEnded;
+ if (mAudioSink) {
+ mAudioSink->GetDebugInfo(aInfo);
+ }
+}
+
+void AudioSinkWrapper::EnableTreatAudioUnderrunAsSilence(bool aEnabled) {
+ mTreatUnderrunAsSilence = aEnabled;
+ if (mAudioSink) {
+ mAudioSink->EnableTreatAudioUnderrunAsSilence(aEnabled);
+ }
+}
+
+} // namespace mozilla
+
+#undef LOG
+#undef LOGV
diff --git a/dom/media/mediasink/AudioSinkWrapper.h b/dom/media/mediasink/AudioSinkWrapper.h
new file mode 100644
index 0000000000..411983c526
--- /dev/null
+++ b/dom/media/mediasink/AudioSinkWrapper.h
@@ -0,0 +1,161 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef AudioSinkWrapper_h_
+#define AudioSinkWrapper_h_
+
+#include "mozilla/AbstractThread.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/UniquePtr.h"
+
+#include "AudioSink.h"
+#include "MediaSink.h"
+
+namespace mozilla {
+class MediaData;
+template <class T>
+class MediaQueue;
+
+/**
+ * A wrapper around AudioSink to provide the interface of MediaSink.
+ */
+class AudioSinkWrapper : public MediaSink {
+ using PlaybackParams = AudioSink::PlaybackParams;
+
+ // An AudioSink factory.
+ class Creator {
+ public:
+ virtual ~Creator() = default;
+ virtual AudioSink* Create() = 0;
+ };
+
+ // Wrap around a function object which creates AudioSinks.
+ template <typename Function>
+ class CreatorImpl : public Creator {
+ public:
+ explicit CreatorImpl(const Function& aFunc) : mFunction(aFunc) {}
+ AudioSink* Create() override { return mFunction(); }
+
+ private:
+ Function mFunction;
+ };
+
+ public:
+ template <typename Function>
+ AudioSinkWrapper(AbstractThread* aOwnerThread,
+ MediaQueue<AudioData>& aAudioQueue, const Function& aFunc,
+ double aVolume, double aPlaybackRate, bool aPreservesPitch,
+ RefPtr<AudioDeviceInfo> aAudioDevice)
+ : mOwnerThread(aOwnerThread),
+ mCreator(new CreatorImpl<Function>(aFunc)),
+ mAudioDevice(std::move(aAudioDevice)),
+ mIsStarted(false),
+ mParams(aVolume, aPlaybackRate, aPreservesPitch),
+ // Give an invalid value to facilitate debug if used before playback
+ // starts.
+ mPlayDuration(media::TimeUnit::Invalid()),
+ mAudioEnded(true),
+ mAudioQueue(aAudioQueue) {}
+
+ RefPtr<EndedPromise> OnEnded(TrackType aType) override;
+ media::TimeUnit GetEndTime(TrackType aType) const override;
+ media::TimeUnit GetPosition(TimeStamp* aTimeStamp = nullptr) override;
+ bool HasUnplayedFrames(TrackType aType) const override;
+ media::TimeUnit UnplayedDuration(TrackType aType) const override;
+ void DropAudioPacketsIfNeeded(const media::TimeUnit& aMediaPosition);
+
+ void SetVolume(double aVolume) override;
+ void SetStreamName(const nsAString& aStreamName) override;
+ void SetPlaybackRate(double aPlaybackRate) override;
+ void SetPreservesPitch(bool aPreservesPitch) override;
+ void SetPlaying(bool aPlaying) override;
+
+ double PlaybackRate() const override;
+
+ nsresult Start(const media::TimeUnit& aStartTime,
+ const MediaInfo& aInfo) override;
+ void Stop() override;
+ bool IsStarted() const override;
+ bool IsPlaying() const override;
+
+ const AudioDeviceInfo* AudioDevice() const override { return mAudioDevice; }
+
+ void Shutdown() override;
+
+ void GetDebugInfo(dom::MediaSinkDebugInfo& aInfo) override;
+
+ void EnableTreatAudioUnderrunAsSilence(bool aEnabled) override;
+
+ private:
+ // The clock that was in use for the previous position query, allowing to
+ // detect clock switches.
+ enum class ClockSource {
+ // The clock comes from an underlying system-level audio stream.
+ AudioStream,
+ // The clock comes from the system clock.
+ SystemClock,
+ // The stream is paused, a constant time is reported.
+ Paused
+ } mLastClockSource = ClockSource::Paused;
+ bool IsMuted() const;
+ void OnMuted(bool aMuted);
+ virtual ~AudioSinkWrapper();
+
+ void AssertOwnerThread() const {
+ MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
+ }
+
+ // An AudioSink can be started synchronously from the MDSM thread, or
+ // asynchronously.
+ // In synchronous mode, the clock doesn't advance until the sink has been
+ // created, initialized and started. This is useful for the initial startup,
+ // and when seeking.
+ // In asynchronous mode, the clock will keep going forward (using the system
+ // clock) until the AudioSink is started, at which point the clock will use
+ // the AudioSink clock. This is used when unmuting a media element.
+ enum class AudioSinkStartPolicy { SYNC, ASYNC };
+ nsresult StartAudioSink(const media::TimeUnit& aStartTime,
+ AudioSinkStartPolicy aPolicy);
+
+ // Get the current media position using the system clock. This is used when
+ // the audio is muted, or when the media has no audio track. Otherwise, the
+ // media's position is based on the clock of the AudioStream.
+ media::TimeUnit GetSystemClockPosition(TimeStamp aNow) const;
+ bool CheckIfEnded() const;
+
+ void OnAudioEnded();
+
+ bool IsAudioSourceEnded(const MediaInfo& aInfo) const;
+
+ const RefPtr<AbstractThread> mOwnerThread;
+ UniquePtr<Creator> mCreator;
+ UniquePtr<AudioSink> mAudioSink;
+ // The output device this AudioSink is playing data to. The system's default
+ // device is used if this is null.
+ const RefPtr<AudioDeviceInfo> mAudioDevice;
+ // Will only exist when media has an audio track.
+ RefPtr<EndedPromise> mEndedPromise;
+ MozPromiseHolder<EndedPromise> mEndedPromiseHolder;
+
+ bool mIsStarted;
+ PlaybackParams mParams;
+
+ TimeStamp mPlayStartTime;
+ media::TimeUnit mPlayDuration;
+
+ bool mAudioEnded;
+ MozPromiseRequestHolder<EndedPromise> mAudioSinkEndedPromise;
+ MediaQueue<AudioData>& mAudioQueue;
+
+ // True if we'd like to treat underrun as silent frames. But that can only be
+ // applied in the special situation for seamless looping.
+ bool mTreatUnderrunAsSilence = false;
+};
+
+} // namespace mozilla
+
+#endif // AudioSinkWrapper_h_
diff --git a/dom/media/mediasink/DecodedStream.cpp b/dom/media/mediasink/DecodedStream.cpp
new file mode 100644
index 0000000000..0a488dcfdf
--- /dev/null
+++ b/dom/media/mediasink/DecodedStream.cpp
@@ -0,0 +1,1171 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DecodedStream.h"
+
+#include "AudioDecoderInputTrack.h"
+#include "AudioSegment.h"
+#include "MediaData.h"
+#include "MediaDecoderStateMachine.h"
+#include "MediaQueue.h"
+#include "MediaTrackGraph.h"
+#include "MediaTrackListener.h"
+#include "SharedBuffer.h"
+#include "Tracing.h"
+#include "VideoSegment.h"
+#include "VideoUtils.h"
+#include "mozilla/AbstractThread.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/ProfilerMarkerTypes.h"
+#include "mozilla/SyncRunnable.h"
+#include "mozilla/gfx/Point.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "nsProxyRelease.h"
+
+namespace mozilla {
+
+using media::NullableTimeUnit;
+using media::TimeUnit;
+
+extern LazyLogModule gMediaDecoderLog;
+
+#define LOG_DS(type, fmt, ...) \
+ MOZ_LOG(gMediaDecoderLog, type, \
+ ("DecodedStream=%p " fmt, this, ##__VA_ARGS__))
+
+#define PLAYBACK_PROFILER_MARKER(markerString) \
+ PROFILER_MARKER_TEXT(FUNCTION_SIGNATURE, MEDIA_PLAYBACK, {}, markerString)
+
+/*
+ * A container class to make it easier to pass the playback info all the
+ * way to DecodedStreamGraphListener from DecodedStream.
+ */
+struct PlaybackInfoInit {
+ TimeUnit mStartTime;
+ MediaInfo mInfo;
+};
+
+class DecodedStreamGraphListener;
+
+class SourceVideoTrackListener : public MediaTrackListener {
+ public:
+ SourceVideoTrackListener(DecodedStreamGraphListener* aGraphListener,
+ SourceMediaTrack* aVideoTrack,
+ MediaTrack* aAudioTrack,
+ nsISerialEventTarget* aDecoderThread);
+
+ void NotifyOutput(MediaTrackGraph* aGraph,
+ TrackTime aCurrentTrackTime) override;
+ void NotifyEnded(MediaTrackGraph* aGraph) override;
+
+ private:
+ const RefPtr<DecodedStreamGraphListener> mGraphListener;
+ const RefPtr<SourceMediaTrack> mVideoTrack;
+ const RefPtr<const MediaTrack> mAudioTrack;
+ const RefPtr<nsISerialEventTarget> mDecoderThread;
+ TrackTime mLastVideoOutputTime = 0;
+};
+
+class DecodedStreamGraphListener {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DecodedStreamGraphListener)
+ public:
+ DecodedStreamGraphListener(
+ nsISerialEventTarget* aDecoderThread, AudioDecoderInputTrack* aAudioTrack,
+ MozPromiseHolder<DecodedStream::EndedPromise>&& aAudioEndedHolder,
+ SourceMediaTrack* aVideoTrack,
+ MozPromiseHolder<DecodedStream::EndedPromise>&& aVideoEndedHolder)
+ : mDecoderThread(aDecoderThread),
+ mVideoTrackListener(
+ aVideoTrack ? MakeRefPtr<SourceVideoTrackListener>(
+ this, aVideoTrack, aAudioTrack, aDecoderThread)
+ : nullptr),
+ mAudioEndedHolder(std::move(aAudioEndedHolder)),
+ mVideoEndedHolder(std::move(aVideoEndedHolder)),
+ mAudioTrack(aAudioTrack),
+ mVideoTrack(aVideoTrack) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mDecoderThread);
+
+ if (mAudioTrack) {
+ mOnAudioOutput = mAudioTrack->OnOutput().Connect(
+ mDecoderThread,
+ [self = RefPtr<DecodedStreamGraphListener>(this)](TrackTime aTime) {
+ self->NotifyOutput(MediaSegment::AUDIO, aTime);
+ });
+ mOnAudioEnd = mAudioTrack->OnEnd().Connect(
+ mDecoderThread, [self = RefPtr<DecodedStreamGraphListener>(this)]() {
+ self->NotifyEnded(MediaSegment::AUDIO);
+ });
+ } else {
+ mAudioEnded = true;
+ mAudioEndedHolder.ResolveIfExists(true, __func__);
+ }
+
+ if (mVideoTrackListener) {
+ mVideoTrack->AddListener(mVideoTrackListener);
+ } else {
+ mVideoEnded = true;
+ mVideoEndedHolder.ResolveIfExists(true, __func__);
+ }
+ }
+
+ void Close() {
+ AssertOnDecoderThread();
+ if (mAudioTrack) {
+ mAudioTrack->Close();
+ }
+ if (mVideoTrack) {
+ mVideoTrack->End();
+ }
+ mAudioEndedHolder.ResolveIfExists(false, __func__);
+ mVideoEndedHolder.ResolveIfExists(false, __func__);
+ mOnAudioOutput.DisconnectIfExists();
+ mOnAudioEnd.DisconnectIfExists();
+ }
+
+ void NotifyOutput(MediaSegment::Type aType, TrackTime aCurrentTrackTime) {
+ AssertOnDecoderThread();
+ if (aType == MediaSegment::AUDIO) {
+ mAudioOutputFrames = aCurrentTrackTime;
+ } else if (aType == MediaSegment::VIDEO) {
+ if (aCurrentTrackTime >= mVideoEndTime) {
+ mVideoTrack->End();
+ }
+ } else {
+ MOZ_CRASH("Unexpected track type");
+ }
+
+ MOZ_ASSERT_IF(aType == MediaSegment::AUDIO, !mAudioEnded);
+ MOZ_ASSERT_IF(aType == MediaSegment::VIDEO, !mVideoEnded);
+ // This situation would happen when playing audio in >1x playback rate,
+ // because the audio output clock isn't align the graph time and would go
+ // forward faster. Eg. playback rate=2, when the graph time passes 10s, the
+ // audio clock time actually already goes forward 20s. After audio track
+ // ended, video track would tirgger the clock, but the video time still
+ // follows the graph time, which is smaller than the preivous audio clock
+ // time and should be ignored.
+ if (aCurrentTrackTime <= mLastOutputTime) {
+ MOZ_ASSERT(aType == MediaSegment::VIDEO);
+ return;
+ }
+ MOZ_ASSERT(aCurrentTrackTime > mLastOutputTime);
+ mLastOutputTime = aCurrentTrackTime;
+
+ // Only when audio track doesn't exists or has reached the end, video
+ // track should drive the clock.
+ MOZ_ASSERT_IF(aType == MediaSegment::VIDEO, mAudioEnded);
+ const MediaTrack* track = aType == MediaSegment::VIDEO
+ ? static_cast<MediaTrack*>(mVideoTrack)
+ : static_cast<MediaTrack*>(mAudioTrack);
+ mOnOutput.Notify(track->TrackTimeToMicroseconds(aCurrentTrackTime));
+ }
+
+ void NotifyEnded(MediaSegment::Type aType) {
+ AssertOnDecoderThread();
+ if (aType == MediaSegment::AUDIO) {
+ MOZ_ASSERT(!mAudioEnded);
+ mAudioEnded = true;
+ mAudioEndedHolder.ResolveIfExists(true, __func__);
+ } else if (aType == MediaSegment::VIDEO) {
+ MOZ_ASSERT(!mVideoEnded);
+ mVideoEnded = true;
+ mVideoEndedHolder.ResolveIfExists(true, __func__);
+ } else {
+ MOZ_CRASH("Unexpected track type");
+ }
+ }
+
+ /**
+ * Tell the graph listener to end the track sourced by the given track after
+ * it has seen at least aEnd worth of output reported as processed by the
+ * graph.
+ *
+ * A TrackTime of TRACK_TIME_MAX indicates that the track has no end and is
+ * the default.
+ *
+ * This method of ending tracks is needed because the MediaTrackGraph
+ * processes ended tracks (through SourceMediaTrack::EndTrack) at the
+ * beginning of an iteration, but waits until the end of the iteration to
+ * process any ControlMessages. When such a ControlMessage is a listener that
+ * is to be added to a track that has ended in its very first iteration, the
+ * track ends before the listener tracking this ending is added. This can lead
+ * to a MediaStreamTrack ending on main thread (it uses another listener)
+ * before the listeners to render the track get added, potentially meaning a
+ * media element doesn't progress before reaching the end although data was
+ * available.
+ */
+ void EndVideoTrackAt(MediaTrack* aTrack, TrackTime aEnd) {
+ AssertOnDecoderThread();
+ MOZ_DIAGNOSTIC_ASSERT(aTrack == mVideoTrack);
+ mVideoEndTime = aEnd;
+ }
+
+ void Forget() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mVideoTrackListener && !mVideoTrack->IsDestroyed()) {
+ mVideoTrack->RemoveListener(mVideoTrackListener);
+ }
+ mVideoTrackListener = nullptr;
+ }
+
+ TrackTime GetAudioFramesPlayed() {
+ AssertOnDecoderThread();
+ return mAudioOutputFrames;
+ }
+
+ MediaEventSource<int64_t>& OnOutput() { return mOnOutput; }
+
+ private:
+ ~DecodedStreamGraphListener() {
+ MOZ_ASSERT(mAudioEndedHolder.IsEmpty());
+ MOZ_ASSERT(mVideoEndedHolder.IsEmpty());
+ }
+
+ inline void AssertOnDecoderThread() const {
+ MOZ_ASSERT(mDecoderThread->IsOnCurrentThread());
+ }
+
+ const RefPtr<nsISerialEventTarget> mDecoderThread;
+
+ // Accessible on any thread, but only notify on the decoder thread.
+ MediaEventProducer<int64_t> mOnOutput;
+
+ RefPtr<SourceVideoTrackListener> mVideoTrackListener;
+
+ // These can be resolved on the main thread on creation if there is no
+ // corresponding track, otherwise they are resolved on the decoder thread.
+ MozPromiseHolder<DecodedStream::EndedPromise> mAudioEndedHolder;
+ MozPromiseHolder<DecodedStream::EndedPromise> mVideoEndedHolder;
+
+ // Decoder thread only.
+ TrackTime mAudioOutputFrames = 0;
+ TrackTime mLastOutputTime = 0;
+ bool mAudioEnded = false;
+ bool mVideoEnded = false;
+
+ // Any thread.
+ const RefPtr<AudioDecoderInputTrack> mAudioTrack;
+ const RefPtr<SourceMediaTrack> mVideoTrack;
+ MediaEventListener mOnAudioOutput;
+ MediaEventListener mOnAudioEnd;
+ Atomic<TrackTime> mVideoEndTime{TRACK_TIME_MAX};
+};
+
+SourceVideoTrackListener::SourceVideoTrackListener(
+ DecodedStreamGraphListener* aGraphListener, SourceMediaTrack* aVideoTrack,
+ MediaTrack* aAudioTrack, nsISerialEventTarget* aDecoderThread)
+ : mGraphListener(aGraphListener),
+ mVideoTrack(aVideoTrack),
+ mAudioTrack(aAudioTrack),
+ mDecoderThread(aDecoderThread) {}
+
+void SourceVideoTrackListener::NotifyOutput(MediaTrackGraph* aGraph,
+ TrackTime aCurrentTrackTime) {
+ aGraph->AssertOnGraphThreadOrNotRunning();
+ if (mAudioTrack && !mAudioTrack->Ended()) {
+ // Only audio playout drives the clock forward, if present and live.
+ return;
+ }
+ // The graph can iterate without time advancing, but the invariant is that
+ // time can never go backwards.
+ if (aCurrentTrackTime <= mLastVideoOutputTime) {
+ MOZ_ASSERT(aCurrentTrackTime == mLastVideoOutputTime);
+ return;
+ }
+ mLastVideoOutputTime = aCurrentTrackTime;
+ mDecoderThread->Dispatch(NS_NewRunnableFunction(
+ "SourceVideoTrackListener::NotifyOutput",
+ [self = RefPtr<SourceVideoTrackListener>(this), aCurrentTrackTime]() {
+ self->mGraphListener->NotifyOutput(MediaSegment::VIDEO,
+ aCurrentTrackTime);
+ }));
+}
+
+void SourceVideoTrackListener::NotifyEnded(MediaTrackGraph* aGraph) {
+ aGraph->AssertOnGraphThreadOrNotRunning();
+ mDecoderThread->Dispatch(NS_NewRunnableFunction(
+ "SourceVideoTrackListener::NotifyEnded",
+ [self = RefPtr<SourceVideoTrackListener>(this)]() {
+ self->mGraphListener->NotifyEnded(MediaSegment::VIDEO);
+ }));
+}
+
+/**
+ * All MediaStream-related data is protected by the decoder's monitor. We have
+ * at most one DecodedStreamData per MediaDecoder. XXX Its tracks are used as
+ * inputs for all output tracks created by OutputStreamManager after calls to
+ * captureStream/UntilEnded. Seeking creates new source tracks, as does
+ * replaying after the input as ended. In the latter case, the new sources are
+ * not connected to tracks created by captureStreamUntilEnded.
+ */
+class DecodedStreamData final {
+ public:
+ DecodedStreamData(
+ PlaybackInfoInit&& aInit, MediaTrackGraph* aGraph,
+ RefPtr<ProcessedMediaTrack> aAudioOutputTrack,
+ RefPtr<ProcessedMediaTrack> aVideoOutputTrack,
+ MozPromiseHolder<DecodedStream::EndedPromise>&& aAudioEndedPromise,
+ MozPromiseHolder<DecodedStream::EndedPromise>&& aVideoEndedPromise,
+ float aPlaybackRate, float aVolume, bool aPreservesPitch,
+ nsISerialEventTarget* aDecoderThread);
+ ~DecodedStreamData();
+ MediaEventSource<int64_t>& OnOutput();
+ // This is used to mark track as closed and should be called before Forget().
+ // Decoder thread only.
+ void Close();
+ // After calling this function, the DecodedStreamData would be destroyed.
+ // Main thread only.
+ void Forget();
+ void GetDebugInfo(dom::DecodedStreamDataDebugInfo& aInfo);
+
+ void WriteVideoToSegment(layers::Image* aImage, const TimeUnit& aStart,
+ const TimeUnit& aEnd,
+ const gfx::IntSize& aIntrinsicSize,
+ const TimeStamp& aTimeStamp, VideoSegment* aOutput,
+ const PrincipalHandle& aPrincipalHandle,
+ double aPlaybackRate);
+
+ /* The following group of fields are protected by the decoder's monitor
+ * and can be read or written on any thread.
+ */
+ // Count of audio frames written to the track
+ int64_t mAudioFramesWritten;
+ // Count of video frames written to the track in the track's rate
+ TrackTime mVideoTrackWritten;
+ // mNextAudioTime is the end timestamp for the last packet sent to the track.
+ // Therefore audio packets starting at or after this time need to be copied
+ // to the output track.
+ TimeUnit mNextAudioTime;
+ // mLastVideoStartTime is the start timestamp for the last packet sent to the
+ // track. Therefore video packets starting after this time need to be copied
+ // to the output track.
+ NullableTimeUnit mLastVideoStartTime;
+ // mLastVideoEndTime is the end timestamp for the last packet sent to the
+ // track. It is used to adjust durations of chunks sent to the output track
+ // when there are overlaps in VideoData.
+ NullableTimeUnit mLastVideoEndTime;
+ // The timestamp of the last frame, so we can ensure time never goes
+ // backwards.
+ TimeStamp mLastVideoTimeStamp;
+ // The last video image sent to the track. Useful if we need to replicate
+ // the image.
+ RefPtr<layers::Image> mLastVideoImage;
+ gfx::IntSize mLastVideoImageDisplaySize;
+ bool mHaveSentFinishAudio;
+ bool mHaveSentFinishVideo;
+
+ const RefPtr<AudioDecoderInputTrack> mAudioTrack;
+ const RefPtr<SourceMediaTrack> mVideoTrack;
+ const RefPtr<ProcessedMediaTrack> mAudioOutputTrack;
+ const RefPtr<ProcessedMediaTrack> mVideoOutputTrack;
+ const RefPtr<MediaInputPort> mAudioPort;
+ const RefPtr<MediaInputPort> mVideoPort;
+ const RefPtr<DecodedStream::EndedPromise> mAudioEndedPromise;
+ const RefPtr<DecodedStream::EndedPromise> mVideoEndedPromise;
+ const RefPtr<DecodedStreamGraphListener> mListener;
+};
+
+DecodedStreamData::DecodedStreamData(
+ PlaybackInfoInit&& aInit, MediaTrackGraph* aGraph,
+ RefPtr<ProcessedMediaTrack> aAudioOutputTrack,
+ RefPtr<ProcessedMediaTrack> aVideoOutputTrack,
+ MozPromiseHolder<DecodedStream::EndedPromise>&& aAudioEndedPromise,
+ MozPromiseHolder<DecodedStream::EndedPromise>&& aVideoEndedPromise,
+ float aPlaybackRate, float aVolume, bool aPreservesPitch,
+ nsISerialEventTarget* aDecoderThread)
+ : mAudioFramesWritten(0),
+ mVideoTrackWritten(0),
+ mNextAudioTime(aInit.mStartTime),
+ mHaveSentFinishAudio(false),
+ mHaveSentFinishVideo(false),
+ mAudioTrack(aInit.mInfo.HasAudio()
+ ? AudioDecoderInputTrack::Create(
+ aGraph, aDecoderThread, aInit.mInfo.mAudio,
+ aPlaybackRate, aVolume, aPreservesPitch)
+ : nullptr),
+ mVideoTrack(aInit.mInfo.HasVideo()
+ ? aGraph->CreateSourceTrack(MediaSegment::VIDEO)
+ : nullptr),
+ mAudioOutputTrack(std::move(aAudioOutputTrack)),
+ mVideoOutputTrack(std::move(aVideoOutputTrack)),
+ mAudioPort((mAudioOutputTrack && mAudioTrack)
+ ? mAudioOutputTrack->AllocateInputPort(mAudioTrack)
+ : nullptr),
+ mVideoPort((mVideoOutputTrack && mVideoTrack)
+ ? mVideoOutputTrack->AllocateInputPort(mVideoTrack)
+ : nullptr),
+ mAudioEndedPromise(aAudioEndedPromise.Ensure(__func__)),
+ mVideoEndedPromise(aVideoEndedPromise.Ensure(__func__)),
+ // DecodedStreamGraphListener will resolve these promises.
+ mListener(MakeRefPtr<DecodedStreamGraphListener>(
+ aDecoderThread, mAudioTrack, std::move(aAudioEndedPromise),
+ mVideoTrack, std::move(aVideoEndedPromise))) {
+ MOZ_ASSERT(NS_IsMainThread());
+}
+
+DecodedStreamData::~DecodedStreamData() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mAudioTrack) {
+ mAudioTrack->Destroy();
+ }
+ if (mVideoTrack) {
+ mVideoTrack->Destroy();
+ }
+ if (mAudioPort) {
+ mAudioPort->Destroy();
+ }
+ if (mVideoPort) {
+ mVideoPort->Destroy();
+ }
+}
+
+MediaEventSource<int64_t>& DecodedStreamData::OnOutput() {
+ return mListener->OnOutput();
+}
+
+void DecodedStreamData::Close() { mListener->Close(); }
+
+void DecodedStreamData::Forget() { mListener->Forget(); }
+
+void DecodedStreamData::GetDebugInfo(dom::DecodedStreamDataDebugInfo& aInfo) {
+ CopyUTF8toUTF16(nsPrintfCString("%p", this), aInfo.mInstance);
+ aInfo.mAudioFramesWritten = mAudioFramesWritten;
+ aInfo.mStreamAudioWritten = mListener->GetAudioFramesPlayed();
+ aInfo.mNextAudioTime = mNextAudioTime.ToMicroseconds();
+ aInfo.mLastVideoStartTime =
+ mLastVideoStartTime.valueOr(TimeUnit::FromMicroseconds(-1))
+ .ToMicroseconds();
+ aInfo.mLastVideoEndTime =
+ mLastVideoEndTime.valueOr(TimeUnit::FromMicroseconds(-1))
+ .ToMicroseconds();
+ aInfo.mHaveSentFinishAudio = mHaveSentFinishAudio;
+ aInfo.mHaveSentFinishVideo = mHaveSentFinishVideo;
+}
+
+DecodedStream::DecodedStream(
+ MediaDecoderStateMachine* aStateMachine,
+ nsMainThreadPtrHandle<SharedDummyTrack> aDummyTrack,
+ CopyableTArray<RefPtr<ProcessedMediaTrack>> aOutputTracks, double aVolume,
+ double aPlaybackRate, bool aPreservesPitch,
+ MediaQueue<AudioData>& aAudioQueue, MediaQueue<VideoData>& aVideoQueue,
+ RefPtr<AudioDeviceInfo> aAudioDevice)
+ : mOwnerThread(aStateMachine->OwnerThread()),
+ mDummyTrack(std::move(aDummyTrack)),
+ mWatchManager(this, mOwnerThread),
+ mPlaying(false, "DecodedStream::mPlaying"),
+ mPrincipalHandle(aStateMachine->OwnerThread(), PRINCIPAL_HANDLE_NONE,
+ "DecodedStream::mPrincipalHandle (Mirror)"),
+ mCanonicalOutputPrincipal(aStateMachine->CanonicalOutputPrincipal()),
+ mOutputTracks(std::move(aOutputTracks)),
+ mVolume(aVolume),
+ mPlaybackRate(aPlaybackRate),
+ mPreservesPitch(aPreservesPitch),
+ mAudioQueue(aAudioQueue),
+ mVideoQueue(aVideoQueue),
+ mAudioDevice(std::move(aAudioDevice)) {}
+
+DecodedStream::~DecodedStream() {
+ MOZ_ASSERT(mStartTime.isNothing(), "playback should've ended.");
+}
+
+RefPtr<DecodedStream::EndedPromise> DecodedStream::OnEnded(TrackType aType) {
+ AssertOwnerThread();
+ MOZ_ASSERT(mStartTime.isSome());
+
+ if (aType == TrackInfo::kAudioTrack && mInfo.HasAudio()) {
+ return mAudioEndedPromise;
+ }
+ if (aType == TrackInfo::kVideoTrack && mInfo.HasVideo()) {
+ return mVideoEndedPromise;
+ }
+ return nullptr;
+}
+
+nsresult DecodedStream::Start(const TimeUnit& aStartTime,
+ const MediaInfo& aInfo) {
+ AssertOwnerThread();
+ MOZ_ASSERT(mStartTime.isNothing(), "playback already started.");
+
+ AUTO_PROFILER_LABEL(FUNCTION_SIGNATURE, MEDIA_PLAYBACK);
+ if (profiler_thread_is_being_profiled_for_markers()) {
+ nsPrintfCString markerString("StartTime=%" PRId64,
+ aStartTime.ToMicroseconds());
+ PLAYBACK_PROFILER_MARKER(markerString);
+ }
+ LOG_DS(LogLevel::Debug, "Start() mStartTime=%" PRId64,
+ aStartTime.ToMicroseconds());
+
+ mStartTime.emplace(aStartTime);
+ mLastOutputTime = TimeUnit::Zero();
+ mInfo = aInfo;
+ mPlaying = true;
+ mPrincipalHandle.Connect(mCanonicalOutputPrincipal);
+ mWatchManager.Watch(mPlaying, &DecodedStream::PlayingChanged);
+ mAudibilityMonitor.emplace(
+ mInfo.mAudio.mRate,
+ StaticPrefs::dom_media_silence_duration_for_audibility());
+ ConnectListener();
+
+ class R : public Runnable {
+ public:
+ R(PlaybackInfoInit&& aInit,
+ nsMainThreadPtrHandle<SharedDummyTrack> aDummyTrack,
+ nsTArray<RefPtr<ProcessedMediaTrack>> aOutputTracks,
+ MozPromiseHolder<MediaSink::EndedPromise>&& aAudioEndedPromise,
+ MozPromiseHolder<MediaSink::EndedPromise>&& aVideoEndedPromise,
+ float aPlaybackRate, float aVolume, bool aPreservesPitch,
+ nsISerialEventTarget* aDecoderThread)
+ : Runnable("CreateDecodedStreamData"),
+ mInit(std::move(aInit)),
+ mDummyTrack(std::move(aDummyTrack)),
+ mOutputTracks(std::move(aOutputTracks)),
+ mAudioEndedPromise(std::move(aAudioEndedPromise)),
+ mVideoEndedPromise(std::move(aVideoEndedPromise)),
+ mPlaybackRate(aPlaybackRate),
+ mVolume(aVolume),
+ mPreservesPitch(aPreservesPitch),
+ mDecoderThread(aDecoderThread) {}
+ NS_IMETHOD Run() override {
+ MOZ_ASSERT(NS_IsMainThread());
+ RefPtr<ProcessedMediaTrack> audioOutputTrack;
+ RefPtr<ProcessedMediaTrack> videoOutputTrack;
+ for (const auto& track : mOutputTracks) {
+ if (track->mType == MediaSegment::AUDIO) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ !audioOutputTrack,
+ "We only support capturing to one output track per kind");
+ audioOutputTrack = track;
+ } else if (track->mType == MediaSegment::VIDEO) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ !videoOutputTrack,
+ "We only support capturing to one output track per kind");
+ videoOutputTrack = track;
+ } else {
+ MOZ_CRASH("Unknown media type");
+ }
+ }
+ if (!mDummyTrack) {
+ // No dummy track - no graph. This could be intentional as the owning
+ // media element needs access to the tracks on main thread to set up
+ // forwarding of them before playback starts. MDSM will re-create
+ // DecodedStream once a dummy track is available. This effectively halts
+ // playback for this DecodedStream.
+ return NS_OK;
+ }
+ if ((audioOutputTrack && audioOutputTrack->IsDestroyed()) ||
+ (videoOutputTrack && videoOutputTrack->IsDestroyed())) {
+ // A track has been destroyed and we'll soon get re-created with a
+ // proper one. This effectively halts playback for this DecodedStream.
+ return NS_OK;
+ }
+ mData = MakeUnique<DecodedStreamData>(
+ std::move(mInit), mDummyTrack->mTrack->Graph(),
+ std::move(audioOutputTrack), std::move(videoOutputTrack),
+ std::move(mAudioEndedPromise), std::move(mVideoEndedPromise),
+ mPlaybackRate, mVolume, mPreservesPitch, mDecoderThread);
+ return NS_OK;
+ }
+ UniquePtr<DecodedStreamData> ReleaseData() { return std::move(mData); }
+
+ private:
+ PlaybackInfoInit mInit;
+ nsMainThreadPtrHandle<SharedDummyTrack> mDummyTrack;
+ const nsTArray<RefPtr<ProcessedMediaTrack>> mOutputTracks;
+ MozPromiseHolder<MediaSink::EndedPromise> mAudioEndedPromise;
+ MozPromiseHolder<MediaSink::EndedPromise> mVideoEndedPromise;
+ UniquePtr<DecodedStreamData> mData;
+ const float mPlaybackRate;
+ const float mVolume;
+ const bool mPreservesPitch;
+ const RefPtr<nsISerialEventTarget> mDecoderThread;
+ };
+
+ MozPromiseHolder<DecodedStream::EndedPromise> audioEndedHolder;
+ MozPromiseHolder<DecodedStream::EndedPromise> videoEndedHolder;
+ PlaybackInfoInit init{aStartTime, aInfo};
+ nsCOMPtr<nsIRunnable> r =
+ new R(std::move(init), mDummyTrack, mOutputTracks.Clone(),
+ std::move(audioEndedHolder), std::move(videoEndedHolder),
+ static_cast<float>(mPlaybackRate), static_cast<float>(mVolume),
+ mPreservesPitch, mOwnerThread);
+ SyncRunnable::DispatchToThread(GetMainThreadSerialEventTarget(), r);
+ mData = static_cast<R*>(r.get())->ReleaseData();
+
+ if (mData) {
+ mAudioEndedPromise = mData->mAudioEndedPromise;
+ mVideoEndedPromise = mData->mVideoEndedPromise;
+ mOutputListener = mData->OnOutput().Connect(mOwnerThread, this,
+ &DecodedStream::NotifyOutput);
+ SendData();
+ }
+ return NS_OK;
+}
+
+void DecodedStream::Stop() {
+ AssertOwnerThread();
+ MOZ_ASSERT(mStartTime.isSome(), "playback not started.");
+
+ TRACE("DecodedStream::Stop");
+ LOG_DS(LogLevel::Debug, "Stop()");
+
+ DisconnectListener();
+ ResetVideo(mPrincipalHandle);
+ ResetAudio();
+ mStartTime.reset();
+ mAudioEndedPromise = nullptr;
+ mVideoEndedPromise = nullptr;
+
+ // Clear mData immediately when this playback session ends so we won't
+ // send data to the wrong track in SendData() in next playback session.
+ DestroyData(std::move(mData));
+
+ mPrincipalHandle.DisconnectIfConnected();
+ mWatchManager.Unwatch(mPlaying, &DecodedStream::PlayingChanged);
+ mAudibilityMonitor.reset();
+}
+
+bool DecodedStream::IsStarted() const {
+ AssertOwnerThread();
+ return mStartTime.isSome();
+}
+
+bool DecodedStream::IsPlaying() const {
+ AssertOwnerThread();
+ return IsStarted() && mPlaying;
+}
+
+void DecodedStream::Shutdown() {
+ AssertOwnerThread();
+ mPrincipalHandle.DisconnectIfConnected();
+ mWatchManager.Shutdown();
+}
+
+void DecodedStream::DestroyData(UniquePtr<DecodedStreamData>&& aData) {
+ AssertOwnerThread();
+
+ if (!aData) {
+ return;
+ }
+
+ TRACE("DecodedStream::DestroyData");
+ mOutputListener.Disconnect();
+
+ aData->Close();
+ NS_DispatchToMainThread(
+ NS_NewRunnableFunction("DecodedStream::DestroyData",
+ [data = std::move(aData)]() { data->Forget(); }));
+}
+
+void DecodedStream::SetPlaying(bool aPlaying) {
+ AssertOwnerThread();
+
+ // Resume/pause matters only when playback started.
+ if (mStartTime.isNothing()) {
+ return;
+ }
+
+ if (profiler_thread_is_being_profiled_for_markers()) {
+ nsPrintfCString markerString("Playing=%s", aPlaying ? "true" : "false");
+ PLAYBACK_PROFILER_MARKER(markerString);
+ }
+ LOG_DS(LogLevel::Debug, "playing (%d) -> (%d)", mPlaying.Ref(), aPlaying);
+ mPlaying = aPlaying;
+}
+
+void DecodedStream::SetVolume(double aVolume) {
+ AssertOwnerThread();
+ if (profiler_thread_is_being_profiled_for_markers()) {
+ nsPrintfCString markerString("Volume=%f", aVolume);
+ PLAYBACK_PROFILER_MARKER(markerString);
+ }
+ if (mVolume == aVolume) {
+ return;
+ }
+ mVolume = aVolume;
+ if (mData && mData->mAudioTrack) {
+ mData->mAudioTrack->SetVolume(static_cast<float>(aVolume));
+ }
+}
+
+void DecodedStream::SetPlaybackRate(double aPlaybackRate) {
+ AssertOwnerThread();
+ if (profiler_thread_is_being_profiled_for_markers()) {
+ nsPrintfCString markerString("PlaybackRate=%f", aPlaybackRate);
+ PLAYBACK_PROFILER_MARKER(markerString);
+ }
+ if (mPlaybackRate == aPlaybackRate) {
+ return;
+ }
+ mPlaybackRate = aPlaybackRate;
+ if (mData && mData->mAudioTrack) {
+ mData->mAudioTrack->SetPlaybackRate(static_cast<float>(aPlaybackRate));
+ }
+}
+
+void DecodedStream::SetPreservesPitch(bool aPreservesPitch) {
+ AssertOwnerThread();
+ if (profiler_thread_is_being_profiled_for_markers()) {
+ nsPrintfCString markerString("PreservesPitch=%s",
+ aPreservesPitch ? "true" : "false");
+ PLAYBACK_PROFILER_MARKER(markerString);
+ }
+ if (mPreservesPitch == aPreservesPitch) {
+ return;
+ }
+ mPreservesPitch = aPreservesPitch;
+ if (mData && mData->mAudioTrack) {
+ mData->mAudioTrack->SetPreservesPitch(aPreservesPitch);
+ }
+}
+
+double DecodedStream::PlaybackRate() const {
+ AssertOwnerThread();
+ return mPlaybackRate;
+}
+
+void DecodedStream::SendAudio(const PrincipalHandle& aPrincipalHandle) {
+ AssertOwnerThread();
+
+ if (!mInfo.HasAudio()) {
+ return;
+ }
+
+ if (mData->mHaveSentFinishAudio) {
+ return;
+ }
+
+ TRACE("DecodedStream::SendAudio");
+ // It's OK to hold references to the AudioData because AudioData
+ // is ref-counted.
+ AutoTArray<RefPtr<AudioData>, 10> audio;
+ mAudioQueue.GetElementsAfter(mData->mNextAudioTime, &audio);
+
+ // This will happen everytime when the media sink switches from `AudioSink` to
+ // `DecodedStream`. If we don't insert the silence then the A/V will be out of
+ // sync.
+ RefPtr<AudioData> nextAudio = audio.IsEmpty() ? nullptr : audio[0];
+ if (RefPtr<AudioData> silence = CreateSilenceDataIfGapExists(nextAudio)) {
+ LOG_DS(LogLevel::Verbose, "Detect a gap in audio, insert silence=%u",
+ silence->Frames());
+ audio.InsertElementAt(0, silence);
+ }
+
+ // Append data which hasn't been sent to audio track before.
+ mData->mAudioTrack->AppendData(audio, aPrincipalHandle);
+ for (uint32_t i = 0; i < audio.Length(); ++i) {
+ CheckIsDataAudible(audio[i]);
+ mData->mNextAudioTime = audio[i]->GetEndTime();
+ mData->mAudioFramesWritten += audio[i]->Frames();
+ }
+
+ if (mAudioQueue.IsFinished() && !mData->mHaveSentFinishAudio) {
+ mData->mAudioTrack->NotifyEndOfStream();
+ mData->mHaveSentFinishAudio = true;
+ }
+}
+
+already_AddRefed<AudioData> DecodedStream::CreateSilenceDataIfGapExists(
+ RefPtr<AudioData>& aNextAudio) {
+ AssertOwnerThread();
+ if (!aNextAudio) {
+ return nullptr;
+ }
+ CheckedInt64 audioWrittenOffset =
+ mData->mAudioFramesWritten +
+ TimeUnitToFrames(*mStartTime, aNextAudio->mRate);
+ CheckedInt64 frameOffset =
+ TimeUnitToFrames(aNextAudio->mTime, aNextAudio->mRate);
+ if (audioWrittenOffset.value() >= frameOffset.value()) {
+ return nullptr;
+ }
+ // We've written less audio than our frame offset, return a silence data so we
+ // have enough audio to be at the correct offset for our current frames.
+ CheckedInt64 missingFrames = frameOffset - audioWrittenOffset;
+ AlignedAudioBuffer silenceBuffer(missingFrames.value() *
+ aNextAudio->mChannels);
+ if (!silenceBuffer) {
+ NS_WARNING("OOM in DecodedStream::CreateSilenceDataIfGapExists");
+ return nullptr;
+ }
+ auto duration = media::TimeUnit(missingFrames.value(), aNextAudio->mRate);
+ if (!duration.IsValid()) {
+ NS_WARNING("Int overflow in DecodedStream::CreateSilenceDataIfGapExists");
+ return nullptr;
+ }
+ RefPtr<AudioData> silenceData = new AudioData(
+ aNextAudio->mOffset, aNextAudio->mTime, std::move(silenceBuffer),
+ aNextAudio->mChannels, aNextAudio->mRate);
+ MOZ_DIAGNOSTIC_ASSERT(duration == silenceData->mDuration, "must be equal");
+ return silenceData.forget();
+}
+
+void DecodedStream::CheckIsDataAudible(const AudioData* aData) {
+ MOZ_ASSERT(aData);
+
+ mAudibilityMonitor->Process(aData);
+ bool isAudible = mAudibilityMonitor->RecentlyAudible();
+
+ if (isAudible != mIsAudioDataAudible) {
+ mIsAudioDataAudible = isAudible;
+ mAudibleEvent.Notify(mIsAudioDataAudible);
+ }
+}
+
+void DecodedStreamData::WriteVideoToSegment(
+ layers::Image* aImage, const TimeUnit& aStart, const TimeUnit& aEnd,
+ const gfx::IntSize& aIntrinsicSize, const TimeStamp& aTimeStamp,
+ VideoSegment* aOutput, const PrincipalHandle& aPrincipalHandle,
+ double aPlaybackRate) {
+ RefPtr<layers::Image> image = aImage;
+ aOutput->AppendFrame(image.forget(), aIntrinsicSize, aPrincipalHandle, false,
+ aTimeStamp);
+ // Extend this so we get accurate durations for all frames.
+ // Because this track is pushed, we need durations so the graph can track
+ // when playout of the track has finished.
+ MOZ_ASSERT(aPlaybackRate > 0);
+ TrackTime start = aStart.ToTicksAtRate(mVideoTrack->mSampleRate);
+ TrackTime end = aEnd.ToTicksAtRate(mVideoTrack->mSampleRate);
+ aOutput->ExtendLastFrameBy(
+ static_cast<TrackTime>((float)(end - start) / aPlaybackRate));
+
+ mLastVideoStartTime = Some(aStart);
+ mLastVideoEndTime = Some(aEnd);
+ mLastVideoTimeStamp = aTimeStamp;
+}
+
+static bool ZeroDurationAtLastChunk(VideoSegment& aInput) {
+ // Get the last video frame's start time in VideoSegment aInput.
+ // If the start time is equal to the duration of aInput, means the last video
+ // frame's duration is zero.
+ TrackTime lastVideoStratTime;
+ aInput.GetLastFrame(&lastVideoStratTime);
+ return lastVideoStratTime == aInput.GetDuration();
+}
+
+void DecodedStream::ResetAudio() {
+ AssertOwnerThread();
+
+ if (!mData) {
+ return;
+ }
+
+ if (!mInfo.HasAudio()) {
+ return;
+ }
+
+ TRACE("DecodedStream::ResetAudio");
+ mData->mAudioTrack->ClearFutureData();
+ if (const RefPtr<AudioData>& v = mAudioQueue.PeekFront()) {
+ mData->mNextAudioTime = v->mTime;
+ mData->mHaveSentFinishAudio = false;
+ }
+}
+
+void DecodedStream::ResetVideo(const PrincipalHandle& aPrincipalHandle) {
+ AssertOwnerThread();
+
+ if (!mData) {
+ return;
+ }
+
+ if (!mInfo.HasVideo()) {
+ return;
+ }
+
+ TRACE("DecodedStream::ResetVideo");
+ TrackTime cleared = mData->mVideoTrack->ClearFutureData();
+ mData->mVideoTrackWritten -= cleared;
+ if (mData->mHaveSentFinishVideo && cleared > 0) {
+ mData->mHaveSentFinishVideo = false;
+ mData->mListener->EndVideoTrackAt(mData->mVideoTrack, TRACK_TIME_MAX);
+ }
+
+ VideoSegment resetter;
+ TimeStamp currentTime;
+ TimeUnit currentPosition = GetPosition(&currentTime);
+
+ // Giving direct consumers a frame (really *any* frame, so in this case:
+ // nullptr) at an earlier time than the previous, will signal to that consumer
+ // to discard any frames ahead in time of the new frame. To be honest, this is
+ // an ugly hack because the direct listeners of the MediaTrackGraph do not
+ // have an API that supports clearing the future frames. ImageContainer and
+ // VideoFrameContainer do though, and we will need to move to a similar API
+ // for video tracks as part of bug 1493618.
+ resetter.AppendFrame(nullptr, mData->mLastVideoImageDisplaySize,
+ aPrincipalHandle, false, currentTime);
+ mData->mVideoTrack->AppendData(&resetter);
+
+ // Consumer buffers have been reset. We now set the next time to the start
+ // time of the current frame, so that it can be displayed again on resuming.
+ if (RefPtr<VideoData> v = mVideoQueue.PeekFront()) {
+ mData->mLastVideoStartTime = Some(v->mTime - TimeUnit::FromMicroseconds(1));
+ mData->mLastVideoEndTime = Some(v->mTime);
+ } else {
+ // There was no current frame in the queue. We set the next time to the
+ // current time, so we at least don't resume starting in the future.
+ mData->mLastVideoStartTime =
+ Some(currentPosition - TimeUnit::FromMicroseconds(1));
+ mData->mLastVideoEndTime = Some(currentPosition);
+ }
+
+ mData->mLastVideoTimeStamp = currentTime;
+}
+
+void DecodedStream::SendVideo(const PrincipalHandle& aPrincipalHandle) {
+ AssertOwnerThread();
+
+ if (!mInfo.HasVideo()) {
+ return;
+ }
+
+ if (mData->mHaveSentFinishVideo) {
+ return;
+ }
+
+ TRACE("DecodedStream::SendVideo");
+ VideoSegment output;
+ AutoTArray<RefPtr<VideoData>, 10> video;
+
+ // It's OK to hold references to the VideoData because VideoData
+ // is ref-counted.
+ mVideoQueue.GetElementsAfter(
+ mData->mLastVideoStartTime.valueOr(mStartTime.ref()), &video);
+
+ TimeStamp currentTime;
+ TimeUnit currentPosition = GetPosition(&currentTime);
+
+ if (mData->mLastVideoTimeStamp.IsNull()) {
+ mData->mLastVideoTimeStamp = currentTime;
+ }
+
+ for (uint32_t i = 0; i < video.Length(); ++i) {
+ VideoData* v = video[i];
+ TimeUnit lastStart = mData->mLastVideoStartTime.valueOr(
+ mStartTime.ref() - TimeUnit::FromMicroseconds(1));
+ TimeUnit lastEnd = mData->mLastVideoEndTime.valueOr(mStartTime.ref());
+
+ if (lastEnd < v->mTime) {
+ // Write last video frame to catch up. mLastVideoImage can be null here
+ // which is fine, it just means there's no video.
+
+ // TODO: |mLastVideoImage| should come from the last image rendered
+ // by the state machine. This will avoid the black frame when capture
+ // happens in the middle of playback (especially in th middle of a
+ // video frame). E.g. if we have a video frame that is 30 sec long
+ // and capture happens at 15 sec, we'll have to append a black frame
+ // that is 15 sec long.
+ TimeStamp t =
+ std::max(mData->mLastVideoTimeStamp,
+ currentTime + (lastEnd - currentPosition).ToTimeDuration());
+ mData->WriteVideoToSegment(mData->mLastVideoImage, lastEnd, v->mTime,
+ mData->mLastVideoImageDisplaySize, t, &output,
+ aPrincipalHandle, mPlaybackRate);
+ lastEnd = v->mTime;
+ }
+
+ if (lastStart < v->mTime) {
+ // This frame starts after the last frame's start. Note that this could be
+ // before the last frame's end time for some videos. This only matters for
+ // the track's lifetime in the MTG, as rendering is based on timestamps,
+ // aka frame start times.
+ TimeStamp t =
+ std::max(mData->mLastVideoTimeStamp,
+ currentTime + (lastEnd - currentPosition).ToTimeDuration());
+ TimeUnit end = std::max(
+ v->GetEndTime(),
+ lastEnd + TimeUnit::FromMicroseconds(
+ mData->mVideoTrack->TrackTimeToMicroseconds(1) + 1));
+ mData->mLastVideoImage = v->mImage;
+ mData->mLastVideoImageDisplaySize = v->mDisplay;
+ mData->WriteVideoToSegment(v->mImage, lastEnd, end, v->mDisplay, t,
+ &output, aPrincipalHandle, mPlaybackRate);
+ }
+ }
+
+ // Check the output is not empty.
+ bool compensateEOS = false;
+ bool forceBlack = false;
+ if (output.GetLastFrame()) {
+ compensateEOS = ZeroDurationAtLastChunk(output);
+ }
+
+ if (output.GetDuration() > 0) {
+ mData->mVideoTrackWritten += mData->mVideoTrack->AppendData(&output);
+ }
+
+ if (mVideoQueue.IsFinished() && !mData->mHaveSentFinishVideo) {
+ if (!mData->mLastVideoImage) {
+ // We have video, but the video queue finished before we received any
+ // frame. We insert a black frame to progress any consuming
+ // HTMLMediaElement. This mirrors the behavior of VideoSink.
+
+ // Force a frame - can be null
+ compensateEOS = true;
+ // Force frame to be black
+ forceBlack = true;
+ // Override the frame's size (will be 0x0 otherwise)
+ mData->mLastVideoImageDisplaySize = mInfo.mVideo.mDisplay;
+ LOG_DS(LogLevel::Debug, "No mLastVideoImage");
+ }
+ if (compensateEOS) {
+ VideoSegment endSegment;
+ auto start = mData->mLastVideoEndTime.valueOr(mStartTime.ref());
+ mData->WriteVideoToSegment(
+ mData->mLastVideoImage, start, start,
+ mData->mLastVideoImageDisplaySize,
+ currentTime + (start - currentPosition).ToTimeDuration(), &endSegment,
+ aPrincipalHandle, mPlaybackRate);
+ // ForwardedInputTrack drops zero duration frames, even at the end of
+ // the track. Give the frame a minimum duration so that it is not
+ // dropped.
+ endSegment.ExtendLastFrameBy(1);
+ LOG_DS(LogLevel::Debug,
+ "compensateEOS: start %s, duration %" PRId64
+ ", mPlaybackRate %lf, sample rate %" PRId32,
+ start.ToString().get(), endSegment.GetDuration(), mPlaybackRate,
+ mData->mVideoTrack->mSampleRate);
+ MOZ_ASSERT(endSegment.GetDuration() > 0);
+ if (forceBlack) {
+ endSegment.ReplaceWithDisabled();
+ }
+ mData->mVideoTrackWritten += mData->mVideoTrack->AppendData(&endSegment);
+ }
+ mData->mListener->EndVideoTrackAt(mData->mVideoTrack,
+ mData->mVideoTrackWritten);
+ mData->mHaveSentFinishVideo = true;
+ }
+}
+
+void DecodedStream::SendData() {
+ AssertOwnerThread();
+
+ // Not yet created on the main thread. MDSM will try again later.
+ if (!mData) {
+ return;
+ }
+
+ if (!mPlaying) {
+ return;
+ }
+
+ LOG_DS(LogLevel::Verbose, "SendData()");
+ SendAudio(mPrincipalHandle);
+ SendVideo(mPrincipalHandle);
+}
+
+TimeUnit DecodedStream::GetEndTime(TrackType aType) const {
+ AssertOwnerThread();
+ TRACE("DecodedStream::GetEndTime");
+ if (aType == TrackInfo::kAudioTrack && mInfo.HasAudio() && mData) {
+ auto t = mStartTime.ref() +
+ media::TimeUnit(mData->mAudioFramesWritten, mInfo.mAudio.mRate);
+ if (t.IsValid()) {
+ return t;
+ }
+ } else if (aType == TrackInfo::kVideoTrack && mData) {
+ return mData->mLastVideoEndTime.valueOr(mStartTime.ref());
+ }
+ return TimeUnit::Zero();
+}
+
+TimeUnit DecodedStream::GetPosition(TimeStamp* aTimeStamp) {
+ AssertOwnerThread();
+ TRACE("DecodedStream::GetPosition");
+ // This is only called after MDSM starts playback. So mStartTime is
+ // guaranteed to be something.
+ MOZ_ASSERT(mStartTime.isSome());
+ if (aTimeStamp) {
+ *aTimeStamp = TimeStamp::Now();
+ }
+ return mStartTime.ref() + mLastOutputTime;
+}
+
+void DecodedStream::NotifyOutput(int64_t aTime) {
+ AssertOwnerThread();
+ TimeUnit time = TimeUnit::FromMicroseconds(aTime);
+ if (time == mLastOutputTime) {
+ return;
+ }
+ MOZ_ASSERT(mLastOutputTime < time);
+ mLastOutputTime = time;
+ auto currentTime = GetPosition();
+
+ if (profiler_thread_is_being_profiled_for_markers()) {
+ nsPrintfCString markerString("OutputTime=%" PRId64,
+ currentTime.ToMicroseconds());
+ PLAYBACK_PROFILER_MARKER(markerString);
+ }
+ LOG_DS(LogLevel::Verbose, "time is now %" PRId64,
+ currentTime.ToMicroseconds());
+
+ // Remove audio samples that have been played by MTG from the queue.
+ RefPtr<AudioData> a = mAudioQueue.PeekFront();
+ for (; a && a->GetEndTime() <= currentTime;) {
+ LOG_DS(LogLevel::Debug, "Dropping audio [%" PRId64 ",%" PRId64 "]",
+ a->mTime.ToMicroseconds(), a->GetEndTime().ToMicroseconds());
+ RefPtr<AudioData> releaseMe = mAudioQueue.PopFront();
+ a = mAudioQueue.PeekFront();
+ }
+}
+
+void DecodedStream::PlayingChanged() {
+ AssertOwnerThread();
+ TRACE("DecodedStream::PlayingChanged");
+
+ if (!mPlaying) {
+ // On seek or pause we discard future frames.
+ ResetVideo(mPrincipalHandle);
+ ResetAudio();
+ }
+}
+
+void DecodedStream::ConnectListener() {
+ AssertOwnerThread();
+
+ mAudioPushListener = mAudioQueue.PushEvent().Connect(
+ mOwnerThread, this, &DecodedStream::SendData);
+ mAudioFinishListener = mAudioQueue.FinishEvent().Connect(
+ mOwnerThread, this, &DecodedStream::SendData);
+ mVideoPushListener = mVideoQueue.PushEvent().Connect(
+ mOwnerThread, this, &DecodedStream::SendData);
+ mVideoFinishListener = mVideoQueue.FinishEvent().Connect(
+ mOwnerThread, this, &DecodedStream::SendData);
+ mWatchManager.Watch(mPlaying, &DecodedStream::SendData);
+}
+
+void DecodedStream::DisconnectListener() {
+ AssertOwnerThread();
+
+ mAudioPushListener.Disconnect();
+ mVideoPushListener.Disconnect();
+ mAudioFinishListener.Disconnect();
+ mVideoFinishListener.Disconnect();
+ mWatchManager.Unwatch(mPlaying, &DecodedStream::SendData);
+}
+
+void DecodedStream::GetDebugInfo(dom::MediaSinkDebugInfo& aInfo) {
+ AssertOwnerThread();
+ int64_t startTime = mStartTime.isSome() ? mStartTime->ToMicroseconds() : -1;
+ aInfo.mDecodedStream.mInstance =
+ NS_ConvertUTF8toUTF16(nsPrintfCString("%p", this));
+ aInfo.mDecodedStream.mStartTime = startTime;
+ aInfo.mDecodedStream.mLastOutputTime = mLastOutputTime.ToMicroseconds();
+ aInfo.mDecodedStream.mPlaying = mPlaying.Ref();
+ auto lastAudio = mAudioQueue.PeekBack();
+ aInfo.mDecodedStream.mLastAudio =
+ lastAudio ? lastAudio->GetEndTime().ToMicroseconds() : -1;
+ aInfo.mDecodedStream.mAudioQueueFinished = mAudioQueue.IsFinished();
+ aInfo.mDecodedStream.mAudioQueueSize =
+ AssertedCast<int>(mAudioQueue.GetSize());
+ if (mData) {
+ mData->GetDebugInfo(aInfo.mDecodedStream.mData);
+ }
+}
+
+#undef LOG_DS
+
+} // namespace mozilla
diff --git a/dom/media/mediasink/DecodedStream.h b/dom/media/mediasink/DecodedStream.h
new file mode 100644
index 0000000000..4709ffeda6
--- /dev/null
+++ b/dom/media/mediasink/DecodedStream.h
@@ -0,0 +1,154 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DecodedStream_h_
+#define DecodedStream_h_
+
+#include "AudibilityMonitor.h"
+#include "MediaEventSource.h"
+#include "MediaInfo.h"
+#include "MediaSegment.h"
+#include "MediaSink.h"
+
+#include "mozilla/AbstractThread.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/StateMirroring.h"
+#include "mozilla/UniquePtr.h"
+
+namespace mozilla {
+
+class DecodedStreamData;
+class MediaDecoderStateMachine;
+class AudioData;
+class VideoData;
+struct PlaybackInfoInit;
+class ProcessedMediaTrack;
+class TimeStamp;
+
+template <class T>
+class MediaQueue;
+
+class DecodedStream : public MediaSink {
+ public:
+ DecodedStream(MediaDecoderStateMachine* aStateMachine,
+ nsMainThreadPtrHandle<SharedDummyTrack> aDummyTrack,
+ CopyableTArray<RefPtr<ProcessedMediaTrack>> aOutputTracks,
+ double aVolume, double aPlaybackRate, bool aPreservesPitch,
+ MediaQueue<AudioData>& aAudioQueue,
+ MediaQueue<VideoData>& aVideoQueue,
+ RefPtr<AudioDeviceInfo> aAudioDevice);
+
+ RefPtr<EndedPromise> OnEnded(TrackType aType) override;
+ media::TimeUnit GetEndTime(TrackType aType) const override;
+ media::TimeUnit GetPosition(TimeStamp* aTimeStamp = nullptr) override;
+ bool HasUnplayedFrames(TrackType aType) const override {
+ // TODO: bug 1755026
+ return false;
+ }
+
+ media::TimeUnit UnplayedDuration(TrackType aType) const override {
+ // TODO: bug 1755026
+ return media::TimeUnit::Zero();
+ }
+
+ void SetVolume(double aVolume) override;
+ void SetPlaybackRate(double aPlaybackRate) override;
+ void SetPreservesPitch(bool aPreservesPitch) override;
+ void SetPlaying(bool aPlaying) override;
+
+ double PlaybackRate() const override;
+
+ nsresult Start(const media::TimeUnit& aStartTime,
+ const MediaInfo& aInfo) override;
+ void Stop() override;
+ bool IsStarted() const override;
+ bool IsPlaying() const override;
+ void Shutdown() override;
+ void GetDebugInfo(dom::MediaSinkDebugInfo& aInfo) override;
+ const AudioDeviceInfo* AudioDevice() const override { return mAudioDevice; }
+
+ MediaEventSource<bool>& AudibleEvent() { return mAudibleEvent; }
+
+ protected:
+ virtual ~DecodedStream();
+
+ private:
+ void DestroyData(UniquePtr<DecodedStreamData>&& aData);
+ void SendAudio(const PrincipalHandle& aPrincipalHandle);
+ void SendVideo(const PrincipalHandle& aPrincipalHandle);
+ void ResetAudio();
+ void ResetVideo(const PrincipalHandle& aPrincipalHandle);
+ void SendData();
+ void NotifyOutput(int64_t aTime);
+ void CheckIsDataAudible(const AudioData* aData);
+
+ void AssertOwnerThread() const {
+ MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
+ }
+
+ void PlayingChanged();
+
+ void ConnectListener();
+ void DisconnectListener();
+
+ // Give the audio that is going to be appended next as an input, if there is
+ // a gap between audio's time and the frames that we've written, then return
+ // a silence data that has same amount of frames and can be used to fill the
+ // gap. If no gap exists, return nullptr.
+ already_AddRefed<AudioData> CreateSilenceDataIfGapExists(
+ RefPtr<AudioData>& aNextAudio);
+
+ const RefPtr<AbstractThread> mOwnerThread;
+
+ // Used to access the graph.
+ const nsMainThreadPtrHandle<SharedDummyTrack> mDummyTrack;
+
+ /*
+ * Worker thread only members.
+ */
+ WatchManager<DecodedStream> mWatchManager;
+ UniquePtr<DecodedStreamData> mData;
+ RefPtr<EndedPromise> mAudioEndedPromise;
+ RefPtr<EndedPromise> mVideoEndedPromise;
+
+ Watchable<bool> mPlaying;
+ Mirror<PrincipalHandle> mPrincipalHandle;
+ AbstractCanonical<PrincipalHandle>* mCanonicalOutputPrincipal;
+ const nsTArray<RefPtr<ProcessedMediaTrack>> mOutputTracks;
+
+ double mVolume;
+ double mPlaybackRate;
+ bool mPreservesPitch;
+
+ media::NullableTimeUnit mStartTime;
+ media::TimeUnit mLastOutputTime;
+ MediaInfo mInfo;
+ // True when stream is producing audible sound, false when stream is silent.
+ bool mIsAudioDataAudible = false;
+ Maybe<AudibilityMonitor> mAudibilityMonitor;
+ MediaEventProducer<bool> mAudibleEvent;
+
+ MediaQueue<AudioData>& mAudioQueue;
+ MediaQueue<VideoData>& mVideoQueue;
+
+ // This is the audio device we were told to play out to.
+ // All audio is captured, so nothing is actually played out -- but we report
+ // this upwards as it could save us from being recreated when the sink
+ // changes.
+ const RefPtr<AudioDeviceInfo> mAudioDevice;
+
+ MediaEventListener mAudioPushListener;
+ MediaEventListener mVideoPushListener;
+ MediaEventListener mAudioFinishListener;
+ MediaEventListener mVideoFinishListener;
+ MediaEventListener mOutputListener;
+};
+
+} // namespace mozilla
+
+#endif // DecodedStream_h_
diff --git a/dom/media/mediasink/MediaSink.h b/dom/media/mediasink/MediaSink.h
new file mode 100644
index 0000000000..de6f26dcc9
--- /dev/null
+++ b/dom/media/mediasink/MediaSink.h
@@ -0,0 +1,142 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MediaSink_h_
+#define MediaSink_h_
+
+#include "MediaInfo.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/dom/MediaDebugInfoBinding.h"
+#include "nsISupportsImpl.h"
+
+class AudioDeviceInfo;
+
+namespace mozilla {
+
+class TimeStamp;
+class VideoFrameContainer;
+
+/**
+ * A consumer of audio/video data which plays audio and video tracks and
+ * manages A/V sync between them.
+ *
+ * A typical sink sends audio/video outputs to the speaker and screen.
+ * However, there are also sinks which capture the output of an media element
+ * and send the output to a MediaStream.
+ *
+ * This class is used to move A/V sync management and audio/video rendering
+ * out of MDSM so it is possible for subclasses to do external rendering using
+ * specific hardware which is required by TV projects and CDM.
+ *
+ * Note this class is not thread-safe and should be called from the state
+ * machine thread only.
+ */
+class MediaSink {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaSink);
+ typedef mozilla::TrackInfo::TrackType TrackType;
+
+ // EndedPromise needs to be a non-exclusive promise as it is shared between
+ // both the AudioSink and VideoSink.
+ typedef MozPromise<bool, nsresult, /* IsExclusive = */ false> EndedPromise;
+
+ // Return a promise which is resolved when the track finishes
+ // or null if no such track.
+ // Must be called after playback starts.
+ virtual RefPtr<EndedPromise> OnEnded(TrackType aType) = 0;
+
+ // Return the end time of the audio/video data that has been consumed
+ // or 0 if no such track.
+ // Must be called after playback starts.
+ virtual media::TimeUnit GetEndTime(TrackType aType) const = 0;
+
+ // Return playback position of the media.
+ // Since A/V sync is always maintained by this sink, there is no need to
+ // specify whether we want to get audio or video position.
+ // aTimeStamp returns the timeStamp corresponding to the returned position
+ // which is used by the compositor to derive the render time of video frames.
+ // Must be called after playback starts.
+ virtual media::TimeUnit GetPosition(TimeStamp* aTimeStamp = nullptr) = 0;
+
+ // Return true if there are data consumed but not played yet.
+ // Can be called in any state.
+ virtual bool HasUnplayedFrames(TrackType aType) const = 0;
+
+ // Return the duration of data consumed but not played yet.
+ // Can be called in any state.
+ virtual media::TimeUnit UnplayedDuration(TrackType aType) const = 0;
+
+ // Set volume of the audio track.
+ // Do nothing if this sink has no audio track.
+ // Can be called in any state.
+ virtual void SetVolume(double aVolume) {}
+
+ // Set the audio stream name.
+ // Does nothing if this sink has no audio stream.
+ // Can be called in any state.
+ virtual void SetStreamName(const nsAString& aStreamName) {}
+
+ // Set the playback rate.
+ // Can be called in any state.
+ virtual void SetPlaybackRate(double aPlaybackRate) {}
+
+ // Whether to preserve pitch of the audio track.
+ // Do nothing if this sink has no audio track.
+ // Can be called in any state.
+ virtual void SetPreservesPitch(bool aPreservesPitch) {}
+
+ // Pause/resume the playback. Only work after playback starts.
+ virtual void SetPlaying(bool aPlaying) = 0;
+
+ // Get the playback rate.
+ // Can be called in any state.
+ virtual double PlaybackRate() const = 0;
+
+ // Single frame rendering operation may need to be done before playback
+ // started (1st frame) or right after seek completed or playback stopped.
+ // Do nothing if this sink has no video track. Can be called in any state.
+ virtual void Redraw(const VideoInfo& aInfo){};
+
+ // Begin a playback session with the provided start time and media info.
+ // Must be called when playback is stopped.
+ virtual nsresult Start(const media::TimeUnit& aStartTime,
+ const MediaInfo& aInfo) = 0;
+
+ // Finish a playback session.
+ // Must be called after playback starts.
+ virtual void Stop() = 0;
+
+ // Return true if playback has started.
+ // Can be called in any state.
+ virtual bool IsStarted() const = 0;
+
+ // Return true if playback is started and not paused otherwise false.
+ // Can be called in any state.
+ virtual bool IsPlaying() const = 0;
+
+ // The audio output device this MediaSink is playing audio data to. The
+ // default device is used if this returns null.
+ virtual const AudioDeviceInfo* AudioDevice() const = 0;
+
+ // Called on the state machine thread to shut down the sink. All resources
+ // allocated by this sink should be released.
+ // Must be called after playback stopped.
+ virtual void Shutdown() {}
+
+ virtual void SetSecondaryVideoContainer(VideoFrameContainer* aSecondary) {}
+
+ virtual void GetDebugInfo(dom::MediaSinkDebugInfo& aInfo) {}
+
+ virtual void EnableTreatAudioUnderrunAsSilence(bool aEnabled) {}
+
+ protected:
+ virtual ~MediaSink() = default;
+};
+
+} // namespace mozilla
+
+#endif // MediaSink_h_
diff --git a/dom/media/mediasink/VideoSink.cpp b/dom/media/mediasink/VideoSink.cpp
new file mode 100644
index 0000000000..906efdf0db
--- /dev/null
+++ b/dom/media/mediasink/VideoSink.cpp
@@ -0,0 +1,706 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifdef XP_WIN
+// Include Windows headers required for enabling high precision timers.
+# include <windows.h>
+# include <mmsystem.h>
+#endif
+
+#include "VideoSink.h"
+
+#include "MediaQueue.h"
+#include "VideoUtils.h"
+
+#include "mozilla/IntegerPrintfMacros.h"
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/ProfilerMarkerTypes.h"
+#include "mozilla/StaticPrefs_browser.h"
+#include "mozilla/StaticPrefs_media.h"
+
+namespace mozilla {
+extern LazyLogModule gMediaDecoderLog;
+}
+
+#undef FMT
+
+#define FMT(x, ...) "VideoSink=%p " x, this, ##__VA_ARGS__
+#define VSINK_LOG(x, ...) \
+ MOZ_LOG(gMediaDecoderLog, LogLevel::Debug, (FMT(x, ##__VA_ARGS__)))
+#define VSINK_LOG_V(x, ...) \
+ MOZ_LOG(gMediaDecoderLog, LogLevel::Verbose, (FMT(x, ##__VA_ARGS__)))
+
+namespace mozilla {
+
+using namespace mozilla::layers;
+
+// Minimum update frequency is 1/120th of a second, i.e. half the
+// duration of a 60-fps frame.
+static const int64_t MIN_UPDATE_INTERVAL_US = 1000000 / (60 * 2);
+
+static void SetImageToGreenPixel(PlanarYCbCrImage* aImage) {
+ static uint8_t greenPixel[] = {0x00, 0x00, 0x00};
+ PlanarYCbCrData data;
+ data.mYChannel = greenPixel;
+ data.mCbChannel = greenPixel + 1;
+ data.mCrChannel = greenPixel + 2;
+ data.mYStride = data.mCbCrStride = 1;
+ data.mPictureRect = gfx::IntRect(0, 0, 1, 1);
+ data.mYUVColorSpace = gfx::YUVColorSpace::BT601;
+ aImage->CopyData(data);
+}
+
+VideoSink::VideoSink(AbstractThread* aThread, MediaSink* aAudioSink,
+ MediaQueue<VideoData>& aVideoQueue,
+ VideoFrameContainer* aContainer,
+ FrameStatistics& aFrameStats,
+ uint32_t aVQueueSentToCompositerSize)
+ : mOwnerThread(aThread),
+ mAudioSink(aAudioSink),
+ mVideoQueue(aVideoQueue),
+ mContainer(aContainer),
+ mProducerID(ImageContainer::AllocateProducerID()),
+ mFrameStats(aFrameStats),
+ mOldCompositorDroppedCount(mContainer ? mContainer->GetDroppedImageCount()
+ : 0),
+ mPendingDroppedCount(0),
+ mHasVideo(false),
+ mUpdateScheduler(aThread),
+ mVideoQueueSendToCompositorSize(aVQueueSentToCompositerSize),
+ mMinVideoQueueSize(StaticPrefs::media_ruin_av_sync_enabled() ? 1 : 0)
+#ifdef XP_WIN
+ ,
+ mHiResTimersRequested(false)
+#endif
+{
+ MOZ_ASSERT(mAudioSink, "AudioSink should exist.");
+
+ if (StaticPrefs::browser_measurement_render_anims_and_video_solid() &&
+ mContainer) {
+ InitializeBlankImage();
+ MOZ_ASSERT(mBlankImage, "Blank image should exist.");
+ }
+}
+
+VideoSink::~VideoSink() {
+#ifdef XP_WIN
+ MOZ_ASSERT(!mHiResTimersRequested);
+#endif
+}
+
+RefPtr<VideoSink::EndedPromise> VideoSink::OnEnded(TrackType aType) {
+ AssertOwnerThread();
+ MOZ_ASSERT(mAudioSink->IsStarted(), "Must be called after playback starts.");
+
+ if (aType == TrackInfo::kAudioTrack) {
+ return mAudioSink->OnEnded(aType);
+ } else if (aType == TrackInfo::kVideoTrack) {
+ return mEndPromise;
+ }
+ return nullptr;
+}
+
+media::TimeUnit VideoSink::GetEndTime(TrackType aType) const {
+ AssertOwnerThread();
+ MOZ_ASSERT(mAudioSink->IsStarted(), "Must be called after playback starts.");
+
+ if (aType == TrackInfo::kVideoTrack) {
+ return mVideoFrameEndTime;
+ } else if (aType == TrackInfo::kAudioTrack) {
+ return mAudioSink->GetEndTime(aType);
+ }
+ return media::TimeUnit::Zero();
+}
+
+media::TimeUnit VideoSink::GetPosition(TimeStamp* aTimeStamp) {
+ AssertOwnerThread();
+ return mAudioSink->GetPosition(aTimeStamp);
+}
+
+bool VideoSink::HasUnplayedFrames(TrackType aType) const {
+ AssertOwnerThread();
+ MOZ_ASSERT(aType == TrackInfo::kAudioTrack,
+ "Not implemented for non audio tracks.");
+
+ return mAudioSink->HasUnplayedFrames(aType);
+}
+
+media::TimeUnit VideoSink::UnplayedDuration(TrackType aType) const {
+ AssertOwnerThread();
+ MOZ_ASSERT(aType == TrackInfo::kAudioTrack,
+ "Not implemented for non audio tracks.");
+
+ return mAudioSink->UnplayedDuration(aType);
+}
+
+void VideoSink::SetPlaybackRate(double aPlaybackRate) {
+ AssertOwnerThread();
+
+ mAudioSink->SetPlaybackRate(aPlaybackRate);
+}
+
+void VideoSink::SetVolume(double aVolume) {
+ AssertOwnerThread();
+
+ mAudioSink->SetVolume(aVolume);
+}
+
+void VideoSink::SetStreamName(const nsAString& aStreamName) {
+ AssertOwnerThread();
+
+ mAudioSink->SetStreamName(aStreamName);
+}
+
+void VideoSink::SetPreservesPitch(bool aPreservesPitch) {
+ AssertOwnerThread();
+
+ mAudioSink->SetPreservesPitch(aPreservesPitch);
+}
+
+double VideoSink::PlaybackRate() const {
+ AssertOwnerThread();
+
+ return mAudioSink->PlaybackRate();
+}
+
+void VideoSink::EnsureHighResTimersOnOnlyIfPlaying() {
+#ifdef XP_WIN
+ const bool needed = IsPlaying();
+ if (needed == mHiResTimersRequested) {
+ return;
+ }
+ if (needed) {
+ // Ensure high precision timers are enabled on Windows, otherwise the
+ // VideoSink isn't woken up at reliable intervals to set the next frame, and
+ // we drop frames while painting. Note that each call must be matched by a
+ // corresponding timeEndPeriod() call. Enabling high precision timers causes
+ // the CPU to wake up more frequently on Windows 7 and earlier, which causes
+ // more CPU load and battery use. So we only enable high precision timers
+ // when we're actually playing.
+ timeBeginPeriod(1);
+ } else {
+ timeEndPeriod(1);
+ }
+ mHiResTimersRequested = needed;
+#endif
+}
+
+void VideoSink::SetPlaying(bool aPlaying) {
+ AssertOwnerThread();
+ VSINK_LOG_V(" playing (%d) -> (%d)", mAudioSink->IsPlaying(), aPlaying);
+
+ if (!aPlaying) {
+ // Reset any update timer if paused.
+ mUpdateScheduler.Reset();
+ // Since playback is paused, tell compositor to render only current frame.
+ TimeStamp nowTime;
+ const auto clockTime = mAudioSink->GetPosition(&nowTime);
+ RenderVideoFrames(1, clockTime.ToMicroseconds(), nowTime);
+ if (mContainer) {
+ mContainer->ClearCachedResources();
+ }
+ if (mSecondaryContainer) {
+ mSecondaryContainer->ClearCachedResources();
+ }
+ }
+
+ mAudioSink->SetPlaying(aPlaying);
+
+ if (mHasVideo && aPlaying) {
+ // There's no thread in VideoSink for pulling video frames, need to trigger
+ // rendering while becoming playing status. because the VideoQueue may be
+ // full already.
+ TryUpdateRenderedVideoFrames();
+ }
+
+ EnsureHighResTimersOnOnlyIfPlaying();
+}
+
+nsresult VideoSink::Start(const media::TimeUnit& aStartTime,
+ const MediaInfo& aInfo) {
+ AssertOwnerThread();
+ VSINK_LOG("[%s]", __func__);
+
+ nsresult rv = mAudioSink->Start(aStartTime, aInfo);
+
+ mHasVideo = aInfo.HasVideo();
+
+ if (mHasVideo) {
+ mEndPromise = mEndPromiseHolder.Ensure(__func__);
+
+ // If the underlying MediaSink has an end promise for the video track (which
+ // happens when mAudioSink refers to a DecodedStream), we must wait for it
+ // to complete before resolving our own end promise. Otherwise, MDSM might
+ // stop playback before DecodedStream plays to the end and cause
+ // test_streams_element_capture.html to time out.
+ RefPtr<EndedPromise> p = mAudioSink->OnEnded(TrackInfo::kVideoTrack);
+ if (p) {
+ RefPtr<VideoSink> self = this;
+ p->Then(
+ mOwnerThread, __func__,
+ [self]() {
+ self->mVideoSinkEndRequest.Complete();
+ self->TryUpdateRenderedVideoFrames();
+ // It is possible the video queue size is 0 and we have no
+ // frames to render. However, we need to call
+ // MaybeResolveEndPromise() to ensure mEndPromiseHolder is
+ // resolved.
+ self->MaybeResolveEndPromise();
+ },
+ [self]() {
+ self->mVideoSinkEndRequest.Complete();
+ self->TryUpdateRenderedVideoFrames();
+ self->MaybeResolveEndPromise();
+ })
+ ->Track(mVideoSinkEndRequest);
+ }
+
+ ConnectListener();
+ // Run the render loop at least once so we can resolve the end promise
+ // when video duration is 0.
+ UpdateRenderedVideoFrames();
+ }
+ return rv;
+}
+
+void VideoSink::Stop() {
+ AssertOwnerThread();
+ MOZ_ASSERT(mAudioSink->IsStarted(), "playback not started.");
+ VSINK_LOG("[%s]", __func__);
+
+ mAudioSink->Stop();
+
+ mUpdateScheduler.Reset();
+ if (mHasVideo) {
+ DisconnectListener();
+ mVideoSinkEndRequest.DisconnectIfExists();
+ mEndPromiseHolder.ResolveIfExists(true, __func__);
+ mEndPromise = nullptr;
+ }
+ mVideoFrameEndTime = media::TimeUnit::Zero();
+
+ EnsureHighResTimersOnOnlyIfPlaying();
+}
+
+bool VideoSink::IsStarted() const {
+ AssertOwnerThread();
+
+ return mAudioSink->IsStarted();
+}
+
+bool VideoSink::IsPlaying() const {
+ AssertOwnerThread();
+
+ return mAudioSink->IsPlaying();
+}
+
+const AudioDeviceInfo* VideoSink::AudioDevice() const {
+ return mAudioSink->AudioDevice();
+}
+
+void VideoSink::Shutdown() {
+ AssertOwnerThread();
+ MOZ_ASSERT(!mAudioSink->IsStarted(), "must be called after playback stops.");
+ VSINK_LOG("[%s]", __func__);
+
+ mAudioSink->Shutdown();
+}
+
+void VideoSink::OnVideoQueuePushed(RefPtr<VideoData>&& aSample) {
+ AssertOwnerThread();
+ // Listen to push event, VideoSink should try rendering ASAP if first frame
+ // arrives but update scheduler is not triggered yet.
+ if (!aSample->IsSentToCompositor()) {
+ // Since we push rendered frames back to the queue, we will receive
+ // push events for them. We only need to trigger render loop
+ // when this frame is not rendered yet.
+ TryUpdateRenderedVideoFrames();
+ }
+}
+
+void VideoSink::OnVideoQueueFinished() {
+ AssertOwnerThread();
+ // Run render loop if the end promise is not resolved yet.
+ if (!mUpdateScheduler.IsScheduled() && mAudioSink->IsPlaying() &&
+ !mEndPromiseHolder.IsEmpty()) {
+ UpdateRenderedVideoFrames();
+ }
+}
+
+void VideoSink::Redraw(const VideoInfo& aInfo) {
+ AUTO_PROFILER_LABEL("VideoSink::Redraw", MEDIA_PLAYBACK);
+ AssertOwnerThread();
+
+ // No video track, nothing to draw.
+ if (!aInfo.IsValid() || !mContainer) {
+ return;
+ }
+
+ auto now = TimeStamp::Now();
+
+ RefPtr<VideoData> video = VideoQueue().PeekFront();
+ if (video) {
+ if (mBlankImage) {
+ video->mImage = mBlankImage;
+ }
+ video->MarkSentToCompositor();
+ mContainer->SetCurrentFrame(video->mDisplay, video->mImage, now);
+ if (mSecondaryContainer) {
+ mSecondaryContainer->SetCurrentFrame(video->mDisplay, video->mImage, now);
+ }
+ return;
+ }
+
+ // When we reach here, it means there are no frames in this video track.
+ // Draw a blank frame to ensure there is something in the image container
+ // to fire 'loadeddata'.
+
+ RefPtr<Image> blank =
+ mContainer->GetImageContainer()->CreatePlanarYCbCrImage();
+ mContainer->SetCurrentFrame(aInfo.mDisplay, blank, now);
+
+ if (mSecondaryContainer) {
+ mSecondaryContainer->SetCurrentFrame(aInfo.mDisplay, blank, now);
+ }
+}
+
+void VideoSink::TryUpdateRenderedVideoFrames() {
+ AUTO_PROFILER_LABEL("VideoSink::TryUpdateRenderedVideoFrames",
+ MEDIA_PLAYBACK);
+ AssertOwnerThread();
+ if (mUpdateScheduler.IsScheduled() || !mAudioSink->IsPlaying()) {
+ return;
+ }
+ RefPtr<VideoData> v = VideoQueue().PeekFront();
+ if (!v) {
+ // No frames to render.
+ return;
+ }
+
+ TimeStamp nowTime;
+ const media::TimeUnit clockTime = mAudioSink->GetPosition(&nowTime);
+ if (clockTime >= v->mTime) {
+ // Time to render this frame.
+ UpdateRenderedVideoFrames();
+ return;
+ }
+
+ // If we send this future frame to the compositor now, it will be rendered
+ // immediately and break A/V sync. Instead, we schedule a timer to send it
+ // later.
+ int64_t delta =
+ (v->mTime - clockTime).ToMicroseconds() / mAudioSink->PlaybackRate();
+ TimeStamp target = nowTime + TimeDuration::FromMicroseconds(delta);
+ RefPtr<VideoSink> self = this;
+ mUpdateScheduler.Ensure(
+ target, [self]() { self->UpdateRenderedVideoFramesByTimer(); },
+ [self]() { self->UpdateRenderedVideoFramesByTimer(); });
+}
+
+void VideoSink::UpdateRenderedVideoFramesByTimer() {
+ AssertOwnerThread();
+ mUpdateScheduler.CompleteRequest();
+ UpdateRenderedVideoFrames();
+}
+
+void VideoSink::ConnectListener() {
+ AssertOwnerThread();
+ mPushListener = VideoQueue().PushEvent().Connect(
+ mOwnerThread, this, &VideoSink::OnVideoQueuePushed);
+ mFinishListener = VideoQueue().FinishEvent().Connect(
+ mOwnerThread, this, &VideoSink::OnVideoQueueFinished);
+}
+
+void VideoSink::DisconnectListener() {
+ AssertOwnerThread();
+ mPushListener.Disconnect();
+ mFinishListener.Disconnect();
+}
+
+void VideoSink::RenderVideoFrames(int32_t aMaxFrames, int64_t aClockTime,
+ const TimeStamp& aClockTimeStamp) {
+ AUTO_PROFILER_LABEL("VideoSink::RenderVideoFrames", MEDIA_PLAYBACK);
+ AssertOwnerThread();
+
+ AutoTArray<RefPtr<VideoData>, 16> frames;
+ VideoQueue().GetFirstElements(aMaxFrames, &frames);
+ if (frames.IsEmpty() || !mContainer) {
+ return;
+ }
+
+ AutoTArray<ImageContainer::NonOwningImage, 16> images;
+ TimeStamp lastFrameTime;
+ double playbackRate = mAudioSink->PlaybackRate();
+ for (uint32_t i = 0; i < frames.Length(); ++i) {
+ VideoData* frame = frames[i];
+ bool wasSent = frame->IsSentToCompositor();
+ frame->MarkSentToCompositor();
+
+ if (!frame->mImage || !frame->mImage->IsValid() ||
+ !frame->mImage->GetSize().width || !frame->mImage->GetSize().height) {
+ continue;
+ }
+
+ if (frame->mTime.IsNegative()) {
+ // Frame times before the start time are invalid; drop such frames
+ continue;
+ }
+
+ MOZ_ASSERT(!aClockTimeStamp.IsNull());
+ int64_t delta = frame->mTime.ToMicroseconds() - aClockTime;
+ TimeStamp t =
+ aClockTimeStamp + TimeDuration::FromMicroseconds(delta / playbackRate);
+ if (!lastFrameTime.IsNull() && t <= lastFrameTime) {
+ // Timestamps out of order; drop the new frame. In theory we should
+ // probably replace the previous frame with the new frame if the
+ // timestamps are equal, but this is a corrupt video file already so
+ // never mind.
+ continue;
+ }
+ MOZ_ASSERT(!t.IsNull());
+ lastFrameTime = t;
+
+ ImageContainer::NonOwningImage* img = images.AppendElement();
+ img->mTimeStamp = t;
+ img->mImage = frame->mImage;
+ if (mBlankImage) {
+ img->mImage = mBlankImage;
+ }
+ img->mFrameID = frame->mFrameID;
+ img->mProducerID = mProducerID;
+
+ VSINK_LOG_V("playing video frame %" PRId64
+ " (id=%x, vq-queued=%zu, clock=%" PRId64 ")",
+ frame->mTime.ToMicroseconds(), frame->mFrameID,
+ VideoQueue().GetSize(), aClockTime);
+ if (!wasSent) {
+ PROFILER_MARKER("PlayVideo", MEDIA_PLAYBACK, {}, MediaSampleMarker,
+ frame->mTime.ToMicroseconds(),
+ frame->GetEndTime().ToMicroseconds(),
+ VideoQueue().GetSize());
+ }
+ }
+
+ if (images.Length() > 0) {
+ mContainer->SetCurrentFrames(frames[0]->mDisplay, images);
+
+ if (mSecondaryContainer) {
+ mSecondaryContainer->SetCurrentFrames(frames[0]->mDisplay, images);
+ }
+ }
+}
+
+void VideoSink::UpdateRenderedVideoFrames() {
+ AUTO_PROFILER_LABEL("VideoSink::UpdateRenderedVideoFrames", MEDIA_PLAYBACK);
+ AssertOwnerThread();
+ MOZ_ASSERT(mAudioSink->IsPlaying(), "should be called while playing.");
+
+ // Get the current playback position.
+ TimeStamp nowTime;
+ const auto clockTime = mAudioSink->GetPosition(&nowTime);
+ MOZ_ASSERT(!clockTime.IsNegative(), "Should have positive clock time.");
+
+ uint32_t sentToCompositorCount = 0;
+ uint32_t droppedInSink = 0;
+
+ // Skip frames up to the playback position.
+ media::TimeUnit lastFrameEndTime;
+ while (VideoQueue().GetSize() > mMinVideoQueueSize &&
+ clockTime >= VideoQueue().PeekFront()->GetEndTime()) {
+ RefPtr<VideoData> frame = VideoQueue().PopFront();
+ lastFrameEndTime = frame->GetEndTime();
+ if (frame->IsSentToCompositor()) {
+ sentToCompositorCount++;
+ } else {
+ droppedInSink++;
+ VSINK_LOG_V("discarding video frame mTime=%" PRId64
+ " clock_time=%" PRId64,
+ frame->mTime.ToMicroseconds(), clockTime.ToMicroseconds());
+
+ struct VideoSinkDroppedFrameMarker {
+ static constexpr Span<const char> MarkerTypeName() {
+ return MakeStringSpan("VideoSinkDroppedFrame");
+ }
+ static void StreamJSONMarkerData(
+ baseprofiler::SpliceableJSONWriter& aWriter,
+ int64_t aSampleStartTimeUs, int64_t aSampleEndTimeUs,
+ int64_t aClockTimeUs) {
+ aWriter.IntProperty("sampleStartTimeUs", aSampleStartTimeUs);
+ aWriter.IntProperty("sampleEndTimeUs", aSampleEndTimeUs);
+ aWriter.IntProperty("clockTimeUs", aClockTimeUs);
+ }
+ static MarkerSchema MarkerTypeDisplay() {
+ using MS = MarkerSchema;
+ MS schema{MS::Location::MarkerChart, MS::Location::MarkerTable};
+ schema.AddKeyLabelFormat("sampleStartTimeUs", "Sample start time",
+ MS::Format::Microseconds);
+ schema.AddKeyLabelFormat("sampleEndTimeUs", "Sample end time",
+ MS::Format::Microseconds);
+ schema.AddKeyLabelFormat("clockTimeUs", "Audio clock time",
+ MS::Format::Microseconds);
+ return schema;
+ }
+ };
+ profiler_add_marker(
+ "VideoSinkDroppedFrame", geckoprofiler::category::MEDIA_PLAYBACK, {},
+ VideoSinkDroppedFrameMarker{}, frame->mTime.ToMicroseconds(),
+ frame->GetEndTime().ToMicroseconds(), clockTime.ToMicroseconds());
+ }
+ }
+
+ if (droppedInSink || sentToCompositorCount) {
+ uint32_t totalCompositorDroppedCount = mContainer->GetDroppedImageCount();
+ uint32_t droppedInCompositor =
+ totalCompositorDroppedCount - mOldCompositorDroppedCount;
+ if (droppedInCompositor > 0) {
+ mOldCompositorDroppedCount = totalCompositorDroppedCount;
+ VSINK_LOG_V("%u video frame previously discarded by compositor",
+ droppedInCompositor);
+ }
+ mPendingDroppedCount += droppedInCompositor;
+ uint32_t droppedReported = mPendingDroppedCount > sentToCompositorCount
+ ? sentToCompositorCount
+ : mPendingDroppedCount;
+ mPendingDroppedCount -= droppedReported;
+
+ mFrameStats.Accumulate({0, 0, sentToCompositorCount - droppedReported, 0,
+ droppedInSink, droppedInCompositor});
+ }
+
+ // The presentation end time of the last video frame displayed is either
+ // the end time of the current frame, or if we dropped all frames in the
+ // queue, the end time of the last frame we removed from the queue.
+ RefPtr<VideoData> currentFrame = VideoQueue().PeekFront();
+ mVideoFrameEndTime =
+ std::max(mVideoFrameEndTime,
+ currentFrame ? currentFrame->GetEndTime() : lastFrameEndTime);
+
+ RenderVideoFrames(mVideoQueueSendToCompositorSize, clockTime.ToMicroseconds(),
+ nowTime);
+
+ MaybeResolveEndPromise();
+
+ // Get the timestamp of the next frame. Schedule the next update at
+ // the start time of the next frame. If we don't have a next frame,
+ // we will run render loops again upon incoming frames.
+ nsTArray<RefPtr<VideoData>> frames;
+ VideoQueue().GetFirstElements(2, &frames);
+ if (frames.Length() < 2) {
+ return;
+ }
+
+ int64_t nextFrameTime = frames[1]->mTime.ToMicroseconds();
+ int64_t delta = std::max(nextFrameTime - clockTime.ToMicroseconds(),
+ MIN_UPDATE_INTERVAL_US);
+ TimeStamp target = nowTime + TimeDuration::FromMicroseconds(
+ delta / mAudioSink->PlaybackRate());
+
+ RefPtr<VideoSink> self = this;
+ mUpdateScheduler.Ensure(
+ target, [self]() { self->UpdateRenderedVideoFramesByTimer(); },
+ [self]() { self->UpdateRenderedVideoFramesByTimer(); });
+}
+
+void VideoSink::MaybeResolveEndPromise() {
+ AssertOwnerThread();
+ // All frames are rendered, Let's resolve the promise.
+ if (VideoQueue().IsFinished() && VideoQueue().GetSize() <= 1 &&
+ !mVideoSinkEndRequest.Exists()) {
+ if (VideoQueue().GetSize() == 1) {
+ // Remove the last frame since we have sent it to compositor.
+ RefPtr<VideoData> frame = VideoQueue().PopFront();
+ if (mPendingDroppedCount > 0) {
+ mFrameStats.Accumulate({0, 0, 0, 0, 0, 1});
+ mPendingDroppedCount--;
+ } else {
+ mFrameStats.NotifyPresentedFrame();
+ }
+ }
+
+ TimeStamp nowTime;
+ const auto clockTime = mAudioSink->GetPosition(&nowTime);
+
+ // Clear future frames from the compositor, in case the playback position
+ // unexpectedly jumped to the end, and all frames between the previous
+ // playback position and the end were discarded. Old frames based on the
+ // previous playback position might still be queued in the compositor. See
+ // bug 1598143 for when this can happen.
+ mContainer->ClearFutureFrames(nowTime);
+ if (mSecondaryContainer) {
+ mSecondaryContainer->ClearFutureFrames(nowTime);
+ }
+
+ if (clockTime < mVideoFrameEndTime) {
+ VSINK_LOG_V(
+ "Not reach video end time yet, reschedule timer to resolve "
+ "end promise. clockTime=%" PRId64 ", endTime=%" PRId64,
+ clockTime.ToMicroseconds(), mVideoFrameEndTime.ToMicroseconds());
+ int64_t delta = (mVideoFrameEndTime - clockTime).ToMicroseconds() /
+ mAudioSink->PlaybackRate();
+ TimeStamp target = nowTime + TimeDuration::FromMicroseconds(delta);
+ auto resolveEndPromise = [self = RefPtr<VideoSink>(this)]() {
+ self->mEndPromiseHolder.ResolveIfExists(true, __func__);
+ self->mUpdateScheduler.CompleteRequest();
+ };
+ mUpdateScheduler.Ensure(target, std::move(resolveEndPromise),
+ std::move(resolveEndPromise));
+ } else {
+ mEndPromiseHolder.ResolveIfExists(true, __func__);
+ }
+ }
+}
+
+void VideoSink::SetSecondaryVideoContainer(VideoFrameContainer* aSecondary) {
+ AssertOwnerThread();
+ mSecondaryContainer = aSecondary;
+ if (!IsPlaying() && mSecondaryContainer) {
+ ImageContainer* mainImageContainer = mContainer->GetImageContainer();
+ ImageContainer* secondaryImageContainer =
+ mSecondaryContainer->GetImageContainer();
+ MOZ_DIAGNOSTIC_ASSERT(mainImageContainer);
+ MOZ_DIAGNOSTIC_ASSERT(secondaryImageContainer);
+
+ // If the video isn't currently playing, get the current frame and display
+ // that in the secondary container as well.
+ AutoLockImage lockImage(mainImageContainer);
+ TimeStamp now = TimeStamp::Now();
+ if (RefPtr<Image> image = lockImage.GetImage(now)) {
+ AutoTArray<ImageContainer::NonOwningImage, 1> currentFrame;
+ currentFrame.AppendElement(ImageContainer::NonOwningImage(
+ image, now, /* frameID */ 1,
+ /* producerId */ ImageContainer::AllocateProducerID()));
+ secondaryImageContainer->SetCurrentImages(currentFrame);
+ }
+ }
+}
+
+void VideoSink::GetDebugInfo(dom::MediaSinkDebugInfo& aInfo) {
+ AssertOwnerThread();
+ aInfo.mVideoSink.mIsStarted = IsStarted();
+ aInfo.mVideoSink.mIsPlaying = IsPlaying();
+ aInfo.mVideoSink.mFinished = VideoQueue().IsFinished();
+ aInfo.mVideoSink.mSize = VideoQueue().GetSize();
+ aInfo.mVideoSink.mVideoFrameEndTime = mVideoFrameEndTime.ToMicroseconds();
+ aInfo.mVideoSink.mHasVideo = mHasVideo;
+ aInfo.mVideoSink.mVideoSinkEndRequestExists = mVideoSinkEndRequest.Exists();
+ aInfo.mVideoSink.mEndPromiseHolderIsEmpty = mEndPromiseHolder.IsEmpty();
+ mAudioSink->GetDebugInfo(aInfo);
+}
+
+bool VideoSink::InitializeBlankImage() {
+ mBlankImage = mContainer->GetImageContainer()->CreatePlanarYCbCrImage();
+ if (mBlankImage == nullptr) {
+ return false;
+ }
+ SetImageToGreenPixel(mBlankImage->AsPlanarYCbCrImage());
+ return true;
+}
+
+void VideoSink::EnableTreatAudioUnderrunAsSilence(bool aEnabled) {
+ mAudioSink->EnableTreatAudioUnderrunAsSilence(aEnabled);
+}
+
+} // namespace mozilla
diff --git a/dom/media/mediasink/VideoSink.h b/dom/media/mediasink/VideoSink.h
new file mode 100644
index 0000000000..7f2528d870
--- /dev/null
+++ b/dom/media/mediasink/VideoSink.h
@@ -0,0 +1,177 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef VideoSink_h_
+#define VideoSink_h_
+
+#include "FrameStatistics.h"
+#include "ImageContainer.h"
+#include "MediaEventSource.h"
+#include "MediaSink.h"
+#include "MediaTimer.h"
+#include "VideoFrameContainer.h"
+#include "mozilla/AbstractThread.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/TimeStamp.h"
+
+namespace mozilla {
+
+class VideoFrameContainer;
+template <class T>
+class MediaQueue;
+
+class VideoSink : public MediaSink {
+ typedef mozilla::layers::ImageContainer::ProducerID ProducerID;
+
+ public:
+ VideoSink(AbstractThread* aThread, MediaSink* aAudioSink,
+ MediaQueue<VideoData>& aVideoQueue, VideoFrameContainer* aContainer,
+ FrameStatistics& aFrameStats, uint32_t aVQueueSentToCompositerSize);
+
+ RefPtr<EndedPromise> OnEnded(TrackType aType) override;
+
+ media::TimeUnit GetEndTime(TrackType aType) const override;
+
+ media::TimeUnit GetPosition(TimeStamp* aTimeStamp = nullptr) override;
+
+ bool HasUnplayedFrames(TrackType aType) const override;
+ media::TimeUnit UnplayedDuration(TrackType aType) const override;
+
+ void SetPlaybackRate(double aPlaybackRate) override;
+
+ void SetVolume(double aVolume) override;
+
+ void SetStreamName(const nsAString& aStreamName) override;
+
+ void SetPreservesPitch(bool aPreservesPitch) override;
+
+ void SetPlaying(bool aPlaying) override;
+
+ double PlaybackRate() const override;
+
+ void Redraw(const VideoInfo& aInfo) override;
+
+ nsresult Start(const media::TimeUnit& aStartTime,
+ const MediaInfo& aInfo) override;
+
+ void Stop() override;
+
+ bool IsStarted() const override;
+
+ bool IsPlaying() const override;
+
+ const AudioDeviceInfo* AudioDevice() const override;
+
+ void Shutdown() override;
+
+ void SetSecondaryVideoContainer(VideoFrameContainer* aSecondary) override;
+
+ void GetDebugInfo(dom::MediaSinkDebugInfo& aInfo) override;
+
+ void EnableTreatAudioUnderrunAsSilence(bool aEnabled) override;
+
+ private:
+ virtual ~VideoSink();
+
+ // VideoQueue listener related.
+ void OnVideoQueuePushed(RefPtr<VideoData>&& aSample);
+ void OnVideoQueueFinished();
+ void ConnectListener();
+ void DisconnectListener();
+
+ void EnsureHighResTimersOnOnlyIfPlaying();
+
+ // Sets VideoQueue images into the VideoFrameContainer. Called on the shared
+ // state machine thread. The first aMaxFrames (at most) are set.
+ // aClockTime and aClockTimeStamp are used as the baseline for deriving
+ // timestamps for the frames; when omitted, aMaxFrames must be 1 and
+ // a null timestamp is passed to the VideoFrameContainer.
+ // If the VideoQueue is empty, this does nothing.
+ void RenderVideoFrames(int32_t aMaxFrames, int64_t aClockTime = 0,
+ const TimeStamp& aClickTimeStamp = TimeStamp());
+
+ // Triggered while videosink is started, videosink becomes "playing" status,
+ // or VideoQueue event arrived.
+ void TryUpdateRenderedVideoFrames();
+
+ // If we have video, display a video frame if it's time for display has
+ // arrived, otherwise sleep until it's time for the next frame. Update the
+ // current frame time as appropriate, and trigger ready state update.
+ // Called on the shared state machine thread.
+ void UpdateRenderedVideoFrames();
+ void UpdateRenderedVideoFramesByTimer();
+
+ void MaybeResolveEndPromise();
+
+ void AssertOwnerThread() const {
+ MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
+ }
+
+ MediaQueue<VideoData>& VideoQueue() const { return mVideoQueue; }
+
+ const RefPtr<AbstractThread> mOwnerThread;
+ const RefPtr<MediaSink> mAudioSink;
+ MediaQueue<VideoData>& mVideoQueue;
+ VideoFrameContainer* mContainer;
+ RefPtr<VideoFrameContainer> mSecondaryContainer;
+
+ // Producer ID to help ImageContainer distinguish different streams of
+ // FrameIDs. A unique and immutable value per VideoSink.
+ const ProducerID mProducerID;
+
+ // Used to notify MediaDecoder's frame statistics
+ FrameStatistics& mFrameStats;
+
+ RefPtr<EndedPromise> mEndPromise;
+ MozPromiseHolder<EndedPromise> mEndPromiseHolder;
+ MozPromiseRequestHolder<EndedPromise> mVideoSinkEndRequest;
+
+ // The presentation end time of the last video frame which has been displayed.
+ media::TimeUnit mVideoFrameEndTime;
+
+ uint32_t mOldCompositorDroppedCount;
+ uint32_t mPendingDroppedCount;
+
+ // Event listeners for VideoQueue
+ MediaEventListener mPushListener;
+ MediaEventListener mFinishListener;
+
+ // True if this sink is going to handle video track.
+ bool mHasVideo;
+
+ // Used to trigger another update of rendered frames in next round.
+ DelayedScheduler mUpdateScheduler;
+
+ // Max frame number sent to compositor at a time.
+ // Based on the pref value obtained in MDSM.
+ const uint32_t mVideoQueueSendToCompositorSize;
+
+ // Talos tests for the compositor require at least one frame in the
+ // video queue so that the compositor has something to composit during
+ // the talos test when the decode is stressed. We have a minimum size
+ // on the video queue in order to facilitate this talos test.
+ // Note: Normal playback should not have a queue size of more than 0,
+ // otherwise A/V sync will be ruined! *Only* make this non-zero for
+ // testing purposes.
+ const uint32_t mMinVideoQueueSize;
+
+#ifdef XP_WIN
+ // Whether we've called timeBeginPeriod(1) to request high resolution
+ // timers. We request high resolution timers when playback starts, and
+ // turn them off when playback is paused. Enabling high resolution
+ // timers can cause higher CPU usage and battery drain on Windows 7,
+ // but reduces our frame drop rate.
+ bool mHiResTimersRequested;
+#endif
+
+ RefPtr<layers::Image> mBlankImage;
+ bool InitializeBlankImage();
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/mediasink/moz.build b/dom/media/mediasink/moz.build
new file mode 100644
index 0000000000..6db074538f
--- /dev/null
+++ b/dom/media/mediasink/moz.build
@@ -0,0 +1,25 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES += [
+ "AudioDecoderInputTrack.cpp",
+ "AudioSink.cpp",
+ "AudioSinkWrapper.cpp",
+ "DecodedStream.cpp",
+ "VideoSink.cpp",
+]
+
+EXPORTS += [
+ "MediaSink.h",
+]
+
+LOCAL_INCLUDES += [
+ "/dom/media",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/mediasource/AsyncEventRunner.h b/dom/media/mediasource/AsyncEventRunner.h
new file mode 100644
index 0000000000..37a7a1b6b3
--- /dev/null
+++ b/dom/media/mediasource/AsyncEventRunner.h
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_ASYNCEVENTRUNNER_H_
+#define MOZILLA_ASYNCEVENTRUNNER_H_
+
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+
+template <typename T>
+class AsyncEventRunner : public Runnable {
+ public:
+ AsyncEventRunner(T* aTarget, const char* aName)
+ : Runnable("AsyncEventRunner"), mTarget(aTarget), mName(aName) {}
+
+ NS_IMETHOD Run() override {
+ mTarget->DispatchSimpleEvent(mName);
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<T> mTarget;
+ const char* mName;
+};
+
+} // namespace mozilla
+
+#endif /* MOZILLA_ASYNCEVENTRUNNER_H_ */
diff --git a/dom/media/mediasource/ContainerParser.cpp b/dom/media/mediasource/ContainerParser.cpp
new file mode 100644
index 0000000000..b31ee42d67
--- /dev/null
+++ b/dom/media/mediasource/ContainerParser.cpp
@@ -0,0 +1,767 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ContainerParser.h"
+
+#include "WebMBufferedParser.h"
+#include "mozilla/EndianUtils.h"
+#include "mozilla/IntegerPrintfMacros.h"
+#include "mozilla/ErrorResult.h"
+#include "MoofParser.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Result.h"
+#include "MediaData.h"
+#include "nsMimeTypes.h"
+#ifdef MOZ_FMP4
+# include "AtomType.h"
+# include "BufferReader.h"
+# include "ByteStream.h"
+# include "MP4Interval.h"
+# include "SampleIterator.h"
+#endif
+#include "SourceBufferResource.h"
+#include <algorithm>
+
+extern mozilla::LogModule* GetMediaSourceSamplesLog();
+
+#define MSE_DEBUG(arg, ...) \
+ DDMOZ_LOG(GetMediaSourceSamplesLog(), mozilla::LogLevel::Debug, \
+ "(%s)::%s: " arg, mType.OriginalString().Data(), __func__, \
+ ##__VA_ARGS__)
+#define MSE_DEBUGV(arg, ...) \
+ DDMOZ_LOG(GetMediaSourceSamplesLog(), mozilla::LogLevel::Verbose, \
+ "(%s)::%s: " arg, mType.OriginalString().Data(), __func__, \
+ ##__VA_ARGS__)
+#define MSE_DEBUGVEX(_this, arg, ...) \
+ DDMOZ_LOGEX(_this, GetMediaSourceSamplesLog(), mozilla::LogLevel::Verbose, \
+ "(%s)::%s: " arg, mType.OriginalString().Data(), __func__, \
+ ##__VA_ARGS__)
+
+namespace mozilla {
+
+ContainerParser::ContainerParser(const MediaContainerType& aType)
+ : mHasInitData(false), mTotalParsed(0), mGlobalOffset(0), mType(aType) {}
+
+ContainerParser::~ContainerParser() = default;
+
+MediaResult ContainerParser::IsInitSegmentPresent(const MediaSpan& aData) {
+ MSE_DEBUG(
+ "aLength=%zu [%x%x%x%x]", aData.Length(),
+ aData.Length() > 0 ? aData[0] : 0, aData.Length() > 1 ? aData[1] : 0,
+ aData.Length() > 2 ? aData[2] : 0, aData.Length() > 3 ? aData[3] : 0);
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+MediaResult ContainerParser::IsMediaSegmentPresent(const MediaSpan& aData) {
+ MSE_DEBUG(
+ "aLength=%zu [%x%x%x%x]", aData.Length(),
+ aData.Length() > 0 ? aData[0] : 0, aData.Length() > 1 ? aData[1] : 0,
+ aData.Length() > 2 ? aData[2] : 0, aData.Length() > 3 ? aData[3] : 0);
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+MediaResult ContainerParser::ParseStartAndEndTimestamps(const MediaSpan& aData,
+ media::TimeUnit& aStart,
+ media::TimeUnit& aEnd) {
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+bool ContainerParser::TimestampsFuzzyEqual(int64_t aLhs, int64_t aRhs) {
+ return llabs(aLhs - aRhs) <= GetRoundingError();
+}
+
+int64_t ContainerParser::GetRoundingError() {
+ NS_WARNING("Using default ContainerParser::GetRoundingError implementation");
+ return 0;
+}
+
+bool ContainerParser::HasCompleteInitData() {
+ return mHasInitData && !!mInitData->Length();
+}
+
+MediaByteBuffer* ContainerParser::InitData() { return mInitData; }
+
+MediaByteRange ContainerParser::InitSegmentRange() {
+ return mCompleteInitSegmentRange;
+}
+
+MediaByteRange ContainerParser::MediaHeaderRange() {
+ return mCompleteMediaHeaderRange;
+}
+
+MediaByteRange ContainerParser::MediaSegmentRange() {
+ return mCompleteMediaSegmentRange;
+}
+
+DDLoggedTypeDeclNameAndBase(WebMContainerParser, ContainerParser);
+
+class WebMContainerParser
+ : public ContainerParser,
+ public DecoderDoctorLifeLogger<WebMContainerParser> {
+ public:
+ explicit WebMContainerParser(const MediaContainerType& aType)
+ : ContainerParser(aType), mParser(0), mOffset(0) {}
+
+ static const unsigned NS_PER_USEC = 1000;
+
+ MediaResult IsInitSegmentPresent(const MediaSpan& aData) override {
+ ContainerParser::IsInitSegmentPresent(aData);
+ if (aData.Length() < 4) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ WebMBufferedParser parser(0);
+ nsTArray<WebMTimeDataOffset> mapping;
+ if (auto result = parser.Append(aData.Elements(), aData.Length(), mapping);
+ NS_FAILED(result)) {
+ return result;
+ }
+ return parser.mInitEndOffset > 0 ? NS_OK : NS_ERROR_NOT_AVAILABLE;
+ }
+
+ MediaResult IsMediaSegmentPresent(const MediaSpan& aData) override {
+ ContainerParser::IsMediaSegmentPresent(aData);
+ if (aData.Length() < 4) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ WebMBufferedParser parser(0);
+ nsTArray<WebMTimeDataOffset> mapping;
+ parser.AppendMediaSegmentOnly();
+ if (auto result = parser.Append(aData.Elements(), aData.Length(), mapping);
+ NS_FAILED(result)) {
+ return result;
+ }
+ return parser.GetClusterOffset() >= 0 ? NS_OK : NS_ERROR_NOT_AVAILABLE;
+ }
+
+ MediaResult ParseStartAndEndTimestamps(const MediaSpan& aData,
+ media::TimeUnit& aStart,
+ media::TimeUnit& aEnd) override {
+ bool initSegment = NS_SUCCEEDED(IsInitSegmentPresent(aData));
+
+ if (mLastMapping &&
+ (initSegment || NS_SUCCEEDED(IsMediaSegmentPresent(aData)))) {
+ // The last data contained a complete cluster but we can only detect it
+ // now that a new one is starting.
+ // We use mOffset as end position to ensure that any blocks not reported
+ // by WebMBufferParser are properly skipped.
+ mCompleteMediaSegmentRange =
+ MediaByteRange(mLastMapping.ref().mSyncOffset, mOffset) +
+ mGlobalOffset;
+ mLastMapping.reset();
+ MSE_DEBUG("New cluster found at start, ending previous one");
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ if (initSegment) {
+ mOffset = 0;
+ mParser = WebMBufferedParser(0);
+ mOverlappedMapping.Clear();
+ mInitData = new MediaByteBuffer();
+ mResource = new SourceBufferResource();
+ DDLINKCHILD("resource", mResource.get());
+ mCompleteInitSegmentRange = MediaByteRange();
+ mCompleteMediaHeaderRange = MediaByteRange();
+ mCompleteMediaSegmentRange = MediaByteRange();
+ mGlobalOffset = mTotalParsed;
+ }
+
+ // XXX if it only adds new mappings, overlapped but not available
+ // (e.g. overlap < 0) frames are "lost" from the reported mappings here.
+ nsTArray<WebMTimeDataOffset> mapping;
+ mapping.AppendElements(mOverlappedMapping);
+ mOverlappedMapping.Clear();
+ if (auto result = mParser.Append(aData.Elements(), aData.Length(), mapping);
+ NS_FAILED(result)) {
+ return result;
+ }
+ if (mResource) {
+ mResource->AppendData(aData);
+ }
+
+ // XXX This is a bit of a hack. Assume if there are no timecodes
+ // present and it's an init segment that it's _just_ an init segment.
+ // We should be more precise.
+ if (initSegment || !HasCompleteInitData()) {
+ if (mParser.mInitEndOffset > 0) {
+ MOZ_DIAGNOSTIC_ASSERT(mInitData && mResource &&
+ mParser.mInitEndOffset <= mResource->GetLength());
+ if (!mInitData->SetLength(mParser.mInitEndOffset, fallible)) {
+ // Super unlikely OOM
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ mCompleteInitSegmentRange =
+ MediaByteRange(0, mParser.mInitEndOffset) + mGlobalOffset;
+ char* buffer = reinterpret_cast<char*>(mInitData->Elements());
+ mResource->ReadFromCache(buffer, 0, mParser.mInitEndOffset);
+ MSE_DEBUG("Stashed init of %" PRId64 " bytes.", mParser.mInitEndOffset);
+ mResource = nullptr;
+ } else {
+ MSE_DEBUG("Incomplete init found.");
+ }
+ mHasInitData = true;
+ }
+ mOffset += aData.Length();
+ mTotalParsed += aData.Length();
+
+ if (mapping.IsEmpty()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // Calculate media range for first media segment.
+
+ // Check if we have a cluster finishing in the current data.
+ uint32_t endIdx = mapping.Length() - 1;
+ bool foundNewCluster = false;
+ while (mapping[0].mSyncOffset != mapping[endIdx].mSyncOffset) {
+ endIdx -= 1;
+ foundNewCluster = true;
+ }
+
+ int32_t completeIdx = endIdx;
+ while (completeIdx >= 0 && mOffset < mapping[completeIdx].mEndOffset) {
+ MSE_DEBUG("block is incomplete, missing: %" PRId64,
+ mapping[completeIdx].mEndOffset - mOffset);
+ completeIdx -= 1;
+ }
+
+ // Save parsed blocks for which we do not have all data yet.
+ mOverlappedMapping.AppendElements(mapping.Elements() + completeIdx + 1,
+ mapping.Length() - completeIdx - 1);
+
+ if (completeIdx < 0) {
+ mLastMapping.reset();
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ if (mCompleteMediaHeaderRange.IsEmpty()) {
+ mCompleteMediaHeaderRange =
+ MediaByteRange(mapping[0].mSyncOffset, mapping[0].mEndOffset) +
+ mGlobalOffset;
+ }
+
+ if (foundNewCluster && mOffset >= mapping[endIdx].mEndOffset) {
+ // We now have all information required to delimit a complete cluster.
+ int64_t endOffset = mapping[endIdx + 1].mSyncOffset;
+ if (mapping[endIdx + 1].mInitOffset > mapping[endIdx].mInitOffset) {
+ // We have a new init segment before this cluster.
+ endOffset = mapping[endIdx + 1].mInitOffset;
+ }
+ mCompleteMediaSegmentRange =
+ MediaByteRange(mapping[endIdx].mSyncOffset, endOffset) +
+ mGlobalOffset;
+ } else if (mapping[endIdx].mClusterEndOffset >= 0 &&
+ mOffset >= mapping[endIdx].mClusterEndOffset) {
+ mCompleteMediaSegmentRange =
+ MediaByteRange(
+ mapping[endIdx].mSyncOffset,
+ mParser.EndSegmentOffset(mapping[endIdx].mClusterEndOffset)) +
+ mGlobalOffset;
+ }
+
+ Maybe<WebMTimeDataOffset> previousMapping;
+ if (completeIdx) {
+ previousMapping = Some(mapping[completeIdx - 1]);
+ } else {
+ previousMapping = mLastMapping;
+ }
+
+ mLastMapping = Some(mapping[completeIdx]);
+
+ if (!previousMapping && completeIdx + 1u >= mapping.Length()) {
+ // We have no previous nor next block available,
+ // so we can't estimate this block's duration.
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ uint64_t frameDuration =
+ (completeIdx + 1u < mapping.Length())
+ ? mapping[completeIdx + 1].mTimecode -
+ mapping[completeIdx].mTimecode
+ : mapping[completeIdx].mTimecode - previousMapping.ref().mTimecode;
+ aStart = media::TimeUnit::FromNanoseconds(
+ AssertedCast<int64_t>(mapping[0].mTimecode));
+ aEnd = media::TimeUnit::FromNanoseconds(
+ AssertedCast<int64_t>(mapping[completeIdx].mTimecode + frameDuration));
+
+ MSE_DEBUG("[%" PRId64 ", %" PRId64 "] [fso=%" PRId64 ", leo=%" PRId64
+ ", l=%zu processedIdx=%u fs=%" PRId64 "]",
+ aStart.ToMicroseconds(), aEnd.ToMicroseconds(),
+ mapping[0].mSyncOffset, mapping[completeIdx].mEndOffset,
+ mapping.Length(), completeIdx, mCompleteMediaSegmentRange.mEnd);
+
+ return NS_OK;
+ }
+
+ int64_t GetRoundingError() override {
+ int64_t error = mParser.GetTimecodeScale() / NS_PER_USEC;
+ return error * 2;
+ }
+
+ private:
+ WebMBufferedParser mParser;
+ nsTArray<WebMTimeDataOffset> mOverlappedMapping;
+ int64_t mOffset;
+ Maybe<WebMTimeDataOffset> mLastMapping;
+};
+
+#ifdef MOZ_FMP4
+
+DDLoggedTypeDeclNameAndBase(MP4Stream, ByteStream);
+
+class MP4Stream : public ByteStream, public DecoderDoctorLifeLogger<MP4Stream> {
+ public:
+ explicit MP4Stream(SourceBufferResource* aResource);
+ virtual ~MP4Stream();
+ bool ReadAt(int64_t aOffset, void* aBuffer, size_t aCount,
+ size_t* aBytesRead) override;
+ bool CachedReadAt(int64_t aOffset, void* aBuffer, size_t aCount,
+ size_t* aBytesRead) override;
+ bool Length(int64_t* aSize) override;
+ const uint8_t* GetContiguousAccess(int64_t aOffset, size_t aSize) override;
+
+ private:
+ RefPtr<SourceBufferResource> mResource;
+};
+
+MP4Stream::MP4Stream(SourceBufferResource* aResource) : mResource(aResource) {
+ MOZ_COUNT_CTOR(MP4Stream);
+ MOZ_ASSERT(aResource);
+ DDLINKCHILD("resource", aResource);
+}
+
+MP4Stream::~MP4Stream() { MOZ_COUNT_DTOR(MP4Stream); }
+
+bool MP4Stream::ReadAt(int64_t aOffset, void* aBuffer, size_t aCount,
+ size_t* aBytesRead) {
+ return CachedReadAt(aOffset, aBuffer, aCount, aBytesRead);
+}
+
+bool MP4Stream::CachedReadAt(int64_t aOffset, void* aBuffer, size_t aCount,
+ size_t* aBytesRead) {
+ nsresult rv = mResource->ReadFromCache(reinterpret_cast<char*>(aBuffer),
+ aOffset, aCount);
+ if (NS_FAILED(rv)) {
+ *aBytesRead = 0;
+ return false;
+ }
+ *aBytesRead = aCount;
+ return true;
+}
+
+const uint8_t* MP4Stream::GetContiguousAccess(int64_t aOffset, size_t aSize) {
+ return mResource->GetContiguousAccess(aOffset, aSize);
+}
+
+bool MP4Stream::Length(int64_t* aSize) {
+ if (mResource->GetLength() < 0) return false;
+ *aSize = mResource->GetLength();
+ return true;
+}
+
+DDLoggedTypeDeclNameAndBase(MP4ContainerParser, ContainerParser);
+
+class MP4ContainerParser : public ContainerParser,
+ public DecoderDoctorLifeLogger<MP4ContainerParser> {
+ public:
+ explicit MP4ContainerParser(const MediaContainerType& aType)
+ : ContainerParser(aType) {}
+
+ MediaResult IsInitSegmentPresent(const MediaSpan& aData) override {
+ ContainerParser::IsInitSegmentPresent(aData);
+ // Each MP4 atom has a chunk size and chunk type. The root chunk in an MP4
+ // file is the 'ftyp' atom followed by a file type. We just check for a
+ // vaguely valid 'ftyp' atom.
+ if (aData.Length() < 8) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ AtomParser parser(*this, aData, AtomParser::StopAt::eInitSegment);
+ if (!parser.IsValid()) {
+ return MediaResult(
+ NS_ERROR_FAILURE,
+ RESULT_DETAIL("Invalid Top-Level Box:%s", parser.LastInvalidBox()));
+ }
+ return parser.StartWithInitSegment() ? NS_OK : NS_ERROR_NOT_AVAILABLE;
+ }
+
+ MediaResult IsMediaSegmentPresent(const MediaSpan& aData) override {
+ if (aData.Length() < 8) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ AtomParser parser(*this, aData, AtomParser::StopAt::eMediaSegment);
+ if (!parser.IsValid()) {
+ return MediaResult(
+ NS_ERROR_FAILURE,
+ RESULT_DETAIL("Invalid Box:%s", parser.LastInvalidBox()));
+ }
+ return parser.StartWithMediaSegment() ? NS_OK : NS_ERROR_NOT_AVAILABLE;
+ }
+
+ private:
+ class AtomParser {
+ public:
+ enum class StopAt { eInitSegment, eMediaSegment, eEnd };
+
+ AtomParser(const MP4ContainerParser& aParser, const MediaSpan& aData,
+ StopAt aStop = StopAt::eEnd) {
+ mValid = Init(aParser, aData, aStop).isOk();
+ }
+
+ Result<Ok, nsresult> Init(const MP4ContainerParser& aParser,
+ const MediaSpan& aData, StopAt aStop) {
+ const MediaContainerType mType(
+ aParser.ContainerType()); // for logging macro.
+ BufferReader reader(aData);
+ AtomType initAtom("moov");
+ AtomType mediaAtom("moof");
+ AtomType dataAtom("mdat");
+
+ // Valid top-level boxes defined in ISO/IEC 14496-12 (Table 1)
+ static const AtomType validBoxes[] = {
+ "ftyp", "moov", // init segment
+ "pdin", "free", "sidx", // optional prior moov box
+ "styp", "moof", "mdat", // media segment
+ "mfra", "skip", "meta", "meco", "ssix", "prft", // others.
+ "pssh", // optional with encrypted EME, though ignored.
+ "emsg", // ISO23009-1:2014 Section 5.10.3.3
+ "bloc", "uuid" // boxes accepted by chrome.
+ };
+
+ while (reader.Remaining() >= 8) {
+ uint32_t tmp;
+ MOZ_TRY_VAR(tmp, reader.ReadU32());
+ uint64_t size = tmp;
+ const uint8_t* typec = reader.Peek(4);
+ MOZ_TRY_VAR(tmp, reader.ReadU32());
+ AtomType type(tmp);
+ MSE_DEBUGVEX(&aParser, "Checking atom:'%c%c%c%c' @ %u", typec[0],
+ typec[1], typec[2], typec[3],
+ (uint32_t)reader.Offset() - 8);
+ if (std::find(std::begin(validBoxes), std::end(validBoxes), type) ==
+ std::end(validBoxes)) {
+ // No valid box found, no point continuing.
+ mLastInvalidBox[0] = typec[0];
+ mLastInvalidBox[1] = typec[1];
+ mLastInvalidBox[2] = typec[2];
+ mLastInvalidBox[3] = typec[3];
+ mLastInvalidBox[4] = '\0';
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (mInitOffset.isNothing() && AtomType(type) == initAtom) {
+ mInitOffset = Some(reader.Offset());
+ }
+ if (mMediaOffset.isNothing() && AtomType(type) == mediaAtom) {
+ mMediaOffset = Some(reader.Offset());
+ }
+ if (mDataOffset.isNothing() && AtomType(type) == dataAtom) {
+ mDataOffset = Some(reader.Offset());
+ }
+ if (size == 1) {
+ // 64 bits size.
+ MOZ_TRY_VAR(size, reader.ReadU64());
+ } else if (size == 0) {
+ // Atom extends to the end of the buffer, it can't have what we're
+ // looking for.
+ break;
+ }
+ if (reader.Remaining() < size - 8) {
+ // Incomplete atom.
+ break;
+ }
+ reader.Read(size - 8);
+
+ if (aStop == StopAt::eInitSegment && (mInitOffset || mMediaOffset)) {
+ // When we're looking for an init segment, if we encountered a media
+ // segment, it we will need to be processed first. So we can stop
+ // right away if we have found a media segment.
+ break;
+ }
+ if (aStop == StopAt::eMediaSegment &&
+ (mInitOffset || (mMediaOffset && mDataOffset))) {
+ // When we're looking for a media segment, if we encountered an init
+ // segment, it we will need to be processed first. So we can stop
+ // right away if we have found an init segment.
+ break;
+ }
+ }
+
+ return Ok();
+ }
+
+ bool StartWithInitSegment() const {
+ return mInitOffset.isSome() && (mMediaOffset.isNothing() ||
+ mInitOffset.ref() < mMediaOffset.ref());
+ }
+ bool StartWithMediaSegment() const {
+ return mMediaOffset.isSome() && (mInitOffset.isNothing() ||
+ mMediaOffset.ref() < mInitOffset.ref());
+ }
+ bool IsValid() const { return mValid; }
+ const char* LastInvalidBox() const { return mLastInvalidBox; }
+
+ private:
+ Maybe<size_t> mInitOffset;
+ Maybe<size_t> mMediaOffset;
+ Maybe<size_t> mDataOffset;
+ bool mValid;
+ char mLastInvalidBox[5];
+ };
+
+ public:
+ MediaResult ParseStartAndEndTimestamps(const MediaSpan& aData,
+ media::TimeUnit& aStart,
+ media::TimeUnit& aEnd) override {
+ bool initSegment = NS_SUCCEEDED(IsInitSegmentPresent(aData));
+ if (initSegment) {
+ mResource = new SourceBufferResource();
+ DDLINKCHILD("resource", mResource.get());
+ mStream = new MP4Stream(mResource);
+ // We use a timestampOffset of 0 for ContainerParser, and require
+ // consumers of ParseStartAndEndTimestamps to add their timestamp offset
+ // manually. This allows the ContainerParser to be shared across different
+ // timestampOffsets.
+ mParser = MakeUnique<MoofParser>(mStream, AsVariant(ParseAllTracks{}),
+ /* aIsAudio = */ false);
+ DDLINKCHILD("parser", mParser.get());
+ mInitData = new MediaByteBuffer();
+ mCompleteInitSegmentRange = MediaByteRange();
+ mCompleteMediaHeaderRange = MediaByteRange();
+ mCompleteMediaSegmentRange = MediaByteRange();
+ mGlobalOffset = mTotalParsed;
+ } else if (!mStream || !mParser) {
+ mTotalParsed += aData.Length();
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(mResource && mParser && mInitData,
+ "Should have received an init segment first");
+
+ mResource->AppendData(aData);
+ MediaByteRangeSet byteRanges;
+ byteRanges +=
+ MediaByteRange(int64_t(mParser->mOffset), mResource->GetLength());
+ mParser->RebuildFragmentedIndex(byteRanges);
+
+ if (initSegment || !HasCompleteInitData()) {
+ MediaByteRange& range = mParser->mInitRange;
+ if (range.Length()) {
+ mCompleteInitSegmentRange = range + mGlobalOffset;
+ if (!mInitData->SetLength(range.Length(), fallible)) {
+ // Super unlikely OOM
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ char* buffer = reinterpret_cast<char*>(mInitData->Elements());
+ mResource->ReadFromCache(buffer, range.mStart, range.Length());
+ MSE_DEBUG("Stashed init of %" PRIu64 " bytes.", range.Length());
+ } else {
+ MSE_DEBUG("Incomplete init found.");
+ }
+ mHasInitData = true;
+ }
+ mTotalParsed += aData.Length();
+
+ MP4Interval<media::TimeUnit> compositionRange =
+ mParser->GetCompositionRange(byteRanges);
+
+ mCompleteMediaHeaderRange =
+ mParser->FirstCompleteMediaHeader() + mGlobalOffset;
+ mCompleteMediaSegmentRange =
+ mParser->FirstCompleteMediaSegment() + mGlobalOffset;
+
+ if (HasCompleteInitData()) {
+ mResource->EvictData(mParser->mOffset, mParser->mOffset);
+ }
+
+ if (compositionRange.IsNull()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ aStart = compositionRange.start;
+ aEnd = compositionRange.end;
+ MSE_DEBUG("[%" PRId64 ", %" PRId64 "]", aStart.ToMicroseconds(),
+ aEnd.ToMicroseconds());
+ return NS_OK;
+ }
+
+ // Gaps of up to 35ms (marginally longer than a single frame at 30fps) are
+ // considered to be sequential frames.
+ int64_t GetRoundingError() override { return 35000; }
+
+ private:
+ RefPtr<MP4Stream> mStream;
+ UniquePtr<MoofParser> mParser;
+};
+#endif // MOZ_FMP4
+
+#ifdef MOZ_FMP4
+DDLoggedTypeDeclNameAndBase(ADTSContainerParser, ContainerParser);
+
+class ADTSContainerParser
+ : public ContainerParser,
+ public DecoderDoctorLifeLogger<ADTSContainerParser> {
+ public:
+ explicit ADTSContainerParser(const MediaContainerType& aType)
+ : ContainerParser(aType) {}
+
+ typedef struct {
+ size_t header_length; // Length of just the initialization data.
+ size_t frame_length; // Includes header_length;
+ uint8_t aac_frames; // Number of AAC frames in the ADTS frame.
+ bool have_crc;
+ } Header;
+
+ /// Helper to parse the ADTS header, returning data we care about.
+ /// Returns true if the header is parsed successfully.
+ /// Returns false if the header is invalid or incomplete,
+ /// without modifying the passed-in Header object.
+ bool Parse(const MediaSpan& aData, Header& header) {
+ // ADTS initialization segments are just the packet header.
+ if (aData.Length() < 7) {
+ MSE_DEBUG("buffer too short for header.");
+ return false;
+ }
+ // Check 0xfffx sync word plus layer 0.
+ if ((aData[0] != 0xff) || ((aData[1] & 0xf6) != 0xf0)) {
+ MSE_DEBUG("no syncword.");
+ return false;
+ }
+ bool have_crc = !(aData[1] & 0x01);
+ if (have_crc && aData.Length() < 9) {
+ MSE_DEBUG("buffer too short for header with crc.");
+ return false;
+ }
+ uint8_t frequency_index = (aData[2] & 0x3c) >> 2;
+ MOZ_ASSERT(frequency_index < 16);
+ if (frequency_index == 15) {
+ MSE_DEBUG("explicit frequency disallowed.");
+ return false;
+ }
+ size_t header_length = have_crc ? 9 : 7;
+ size_t data_length = ((aData[3] & 0x03) << 11) | ((aData[4] & 0xff) << 3) |
+ ((aData[5] & 0xe0) >> 5);
+ uint8_t frames = (aData[6] & 0x03) + 1;
+ MOZ_ASSERT(frames > 0);
+ MOZ_ASSERT(frames < 4);
+
+ // Return successfully parsed data.
+ header.header_length = header_length;
+ header.frame_length = header_length + data_length;
+ header.aac_frames = frames;
+ header.have_crc = have_crc;
+ return true;
+ }
+
+ MediaResult IsInitSegmentPresent(const MediaSpan& aData) override {
+ // Call superclass for logging.
+ ContainerParser::IsInitSegmentPresent(aData);
+
+ Header header;
+ if (!Parse(aData, header)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ MSE_DEBUGV("%llu byte frame %d aac frames%s",
+ (unsigned long long)header.frame_length, (int)header.aac_frames,
+ header.have_crc ? " crc" : "");
+
+ return NS_OK;
+ }
+
+ MediaResult IsMediaSegmentPresent(const MediaSpan& aData) override {
+ // Call superclass for logging.
+ ContainerParser::IsMediaSegmentPresent(aData);
+
+ // Make sure we have a header so we know how long the frame is.
+ // NB this assumes the media segment buffer starts with an
+ // initialization segment. Since every frame has an ADTS header
+ // this is a normal place to divide packets, but we can re-parse
+ // mInitData if we need to handle separate media segments.
+ Header header;
+ if (!Parse(aData, header)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ // We're supposed to return true as long as aData contains the
+ // start of a media segment, whether or not it's complete. So
+ // return true if we have any data beyond the header.
+ if (aData.Length() <= header.header_length) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // We should have at least a partial frame.
+ return NS_OK;
+ }
+
+ MediaResult ParseStartAndEndTimestamps(const MediaSpan& aData,
+ media::TimeUnit& aStart,
+ media::TimeUnit& aEnd) override {
+ // ADTS header.
+ Header header;
+ if (!Parse(aData, header)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ mHasInitData = true;
+ mCompleteInitSegmentRange =
+ MediaByteRange(0, int64_t(header.header_length));
+
+ // Cache raw header in case the caller wants a copy.
+ mInitData = new MediaByteBuffer(header.header_length);
+ mInitData->AppendElements(aData.Elements(), header.header_length);
+
+ // Check that we have enough data for the frame body.
+ if (aData.Length() < header.frame_length) {
+ MSE_DEBUGV(
+ "Not enough data for %llu byte frame"
+ " in %llu byte buffer.",
+ (unsigned long long)header.frame_length,
+ (unsigned long long)(aData.Length()));
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ mCompleteMediaSegmentRange =
+ MediaByteRange(header.header_length, header.frame_length);
+ // The ADTS MediaSource Byte Stream Format document doesn't
+ // define media header. Just treat it the same as the whole
+ // media segment.
+ mCompleteMediaHeaderRange = mCompleteMediaSegmentRange;
+
+ MSE_DEBUG("[%" PRId64 ", %" PRId64 "]", aStart.ToMicroseconds(),
+ aEnd.ToMicroseconds());
+ // We don't update timestamps, regardless.
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // Audio shouldn't have gaps.
+ // Especially when we generate the timestamps ourselves.
+ int64_t GetRoundingError() override { return 0; }
+};
+#endif // MOZ_FMP4
+
+/*static*/
+UniquePtr<ContainerParser> ContainerParser::CreateForMIMEType(
+ const MediaContainerType& aType) {
+ if (aType.Type() == MEDIAMIMETYPE(VIDEO_WEBM) ||
+ aType.Type() == MEDIAMIMETYPE(AUDIO_WEBM)) {
+ return MakeUnique<WebMContainerParser>(aType);
+ }
+
+#ifdef MOZ_FMP4
+ if (aType.Type() == MEDIAMIMETYPE(VIDEO_MP4) ||
+ aType.Type() == MEDIAMIMETYPE(AUDIO_MP4)) {
+ return MakeUnique<MP4ContainerParser>(aType);
+ }
+ if (aType.Type() == MEDIAMIMETYPE("audio/aac")) {
+ return MakeUnique<ADTSContainerParser>(aType);
+ }
+#endif
+
+ return MakeUnique<ContainerParser>(aType);
+}
+
+#undef MSE_DEBUG
+#undef MSE_DEBUGV
+#undef MSE_DEBUGVEX
+
+} // namespace mozilla
diff --git a/dom/media/mediasource/ContainerParser.h b/dom/media/mediasource/ContainerParser.h
new file mode 100644
index 0000000000..baac33f545
--- /dev/null
+++ b/dom/media/mediasource/ContainerParser.h
@@ -0,0 +1,97 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_CONTAINERPARSER_H_
+#define MOZILLA_CONTAINERPARSER_H_
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "MediaSpan.h"
+#include "MediaContainerType.h"
+#include "MediaResource.h"
+#include "MediaResult.h"
+
+namespace mozilla {
+
+class MediaByteBuffer;
+class SourceBufferResource;
+
+DDLoggedTypeDeclName(ContainerParser);
+
+class ContainerParser : public DecoderDoctorLifeLogger<ContainerParser> {
+ public:
+ explicit ContainerParser(const MediaContainerType& aType);
+ virtual ~ContainerParser();
+
+ // Return true if aData starts with an initialization segment.
+ // The base implementation exists only for debug logging and is expected
+ // to be called first from the overriding implementation.
+ // Return NS_OK if segment is present, NS_ERROR_NOT_AVAILABLE if no sufficient
+ // data is currently available to make a determination. Any other value
+ // indicates an error.
+ virtual MediaResult IsInitSegmentPresent(const MediaSpan& aData);
+
+ // Return true if aData starts with a media segment.
+ // The base implementation exists only for debug logging and is expected
+ // to be called first from the overriding implementation.
+ // Return NS_OK if segment is present, NS_ERROR_NOT_AVAILABLE if no sufficient
+ // data is currently available to make a determination. Any other value
+ // indicates an error.
+ virtual MediaResult IsMediaSegmentPresent(const MediaSpan& aData);
+
+ // Parse aData to extract the start and end frame times from the media
+ // segment. aData may not start on a parser sync boundary. Return NS_OK
+ // if aStart and aEnd have been updated and NS_ERROR_NOT_AVAILABLE otherwise
+ // when no error were encountered.
+ virtual MediaResult ParseStartAndEndTimestamps(const MediaSpan& aData,
+ media::TimeUnit& aStart,
+ media::TimeUnit& aEnd);
+
+ // Compare aLhs and rHs, considering any error that may exist in the
+ // timestamps from the format's base representation. Return true if aLhs
+ // == aRhs within the error epsilon.
+ bool TimestampsFuzzyEqual(int64_t aLhs, int64_t aRhs);
+
+ virtual int64_t GetRoundingError();
+
+ MediaByteBuffer* InitData();
+
+ bool HasInitData() { return mHasInitData; }
+
+ // Return true if a complete initialization segment has been passed
+ // to ParseStartAndEndTimestamps(). The calls below to retrieve
+ // MediaByteRanges will be valid from when this call first succeeds.
+ bool HasCompleteInitData();
+ // Returns the byte range of the first complete init segment, or an empty
+ // range if not complete.
+ MediaByteRange InitSegmentRange();
+ // Returns the byte range of the first complete media segment header,
+ // or an empty range if not complete.
+ MediaByteRange MediaHeaderRange();
+ // Returns the byte range of the first complete media segment or an empty
+ // range if not complete.
+ MediaByteRange MediaSegmentRange();
+
+ static UniquePtr<ContainerParser> CreateForMIMEType(
+ const MediaContainerType& aType);
+
+ const MediaContainerType& ContainerType() const { return mType; }
+
+ protected:
+ RefPtr<MediaByteBuffer> mInitData;
+ RefPtr<SourceBufferResource> mResource;
+ bool mHasInitData;
+ uint64_t mTotalParsed;
+ uint64_t mGlobalOffset;
+ MediaByteRange mCompleteInitSegmentRange;
+ MediaByteRange mCompleteMediaHeaderRange;
+ MediaByteRange mCompleteMediaSegmentRange;
+ const MediaContainerType mType;
+};
+
+} // namespace mozilla
+
+#endif /* MOZILLA_CONTAINERPARSER_H_ */
diff --git a/dom/media/mediasource/MediaSource.cpp b/dom/media/mediasource/MediaSource.cpp
new file mode 100644
index 0000000000..e38f0fdcb5
--- /dev/null
+++ b/dom/media/mediasource/MediaSource.cpp
@@ -0,0 +1,698 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaSource.h"
+
+#include "AsyncEventRunner.h"
+#include "Benchmark.h"
+#include "DecoderDoctorDiagnostics.h"
+#include "DecoderTraits.h"
+#include "MediaContainerType.h"
+#include "MediaResult.h"
+#include "MediaSourceDemuxer.h"
+#include "MediaSourceUtils.h"
+#include "SourceBuffer.h"
+#include "SourceBufferList.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/FloatingPoint.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Sprintf.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/HTMLMediaElement.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/mozalloc.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsIRunnable.h"
+#include "nsIScriptObjectPrincipal.h"
+#include "nsMimeTypes.h"
+#include "nsPIDOMWindow.h"
+#include "nsServiceManagerUtils.h"
+#include "nsString.h"
+#include "nsThreadUtils.h"
+
+#ifdef MOZ_WIDGET_ANDROID
+# include "AndroidBridge.h"
+# include "mozilla/java/HardwareCodecCapabilityUtilsWrappers.h"
+#endif
+
+struct JSContext;
+class JSObject;
+
+mozilla::LogModule* GetMediaSourceLog() {
+ static mozilla::LazyLogModule sLogModule("MediaSource");
+ return sLogModule;
+}
+
+mozilla::LogModule* GetMediaSourceAPILog() {
+ static mozilla::LazyLogModule sLogModule("MediaSource");
+ return sLogModule;
+}
+
+#define MSE_DEBUG(arg, ...) \
+ DDMOZ_LOG(GetMediaSourceLog(), mozilla::LogLevel::Debug, "::%s: " arg, \
+ __func__, ##__VA_ARGS__)
+#define MSE_API(arg, ...) \
+ DDMOZ_LOG(GetMediaSourceAPILog(), mozilla::LogLevel::Debug, "::%s: " arg, \
+ __func__, ##__VA_ARGS__)
+
+// Arbitrary limit.
+static const unsigned int MAX_SOURCE_BUFFERS = 16;
+
+namespace mozilla {
+
+// Returns true if we should enable MSE webm regardless of preferences.
+// 1. If MP4/H264 isn't supported:
+// * Windows XP
+// * Windows Vista and Server 2008 without the optional "Platform Update
+// Supplement"
+// * N/KN editions (Europe and Korea) of Windows 7/8/8.1/10 without the
+// optional "Windows Media Feature Pack"
+// 2. If H264 hardware acceleration is not available.
+// 3. The CPU is considered to be fast enough
+static bool IsVP9Forced(DecoderDoctorDiagnostics* aDiagnostics) {
+ bool mp4supported = DecoderTraits::IsMP4SupportedType(
+ MediaContainerType(MEDIAMIMETYPE(VIDEO_MP4)), aDiagnostics);
+ bool hwsupported = gfx::gfxVars::CanUseHardwareVideoDecoding();
+#ifdef MOZ_WIDGET_ANDROID
+ return !mp4supported || !hwsupported || VP9Benchmark::IsVP9DecodeFast() ||
+ java::HardwareCodecCapabilityUtils::HasHWVP9(false /* aIsEncoder */);
+#else
+ return !mp4supported || !hwsupported || VP9Benchmark::IsVP9DecodeFast();
+#endif
+}
+
+namespace dom {
+
+static void RecordTypeForTelemetry(const nsAString& aType,
+ nsPIDOMWindowInner* aWindow) {
+ Maybe<MediaContainerType> containerType = MakeMediaContainerType(aType);
+ if (!containerType) {
+ return;
+ }
+
+ const MediaMIMEType& mimeType = containerType->Type();
+ if (mimeType == MEDIAMIMETYPE(VIDEO_WEBM)) {
+ AccumulateCategorical(
+ mozilla::Telemetry::LABELS_MSE_SOURCE_BUFFER_TYPE::VideoWebm);
+ } else if (mimeType == MEDIAMIMETYPE(AUDIO_WEBM)) {
+ AccumulateCategorical(
+ mozilla::Telemetry::LABELS_MSE_SOURCE_BUFFER_TYPE::AudioWebm);
+ } else if (mimeType == MEDIAMIMETYPE(VIDEO_MP4)) {
+ AccumulateCategorical(
+ mozilla::Telemetry::LABELS_MSE_SOURCE_BUFFER_TYPE::VideoMp4);
+ } else if (mimeType == MEDIAMIMETYPE(AUDIO_MP4)) {
+ AccumulateCategorical(
+ mozilla::Telemetry::LABELS_MSE_SOURCE_BUFFER_TYPE::AudioMp4);
+ } else if (mimeType == MEDIAMIMETYPE(VIDEO_MPEG_TS)) {
+ AccumulateCategorical(
+ mozilla::Telemetry::LABELS_MSE_SOURCE_BUFFER_TYPE::VideoMp2t);
+ } else if (mimeType == MEDIAMIMETYPE(AUDIO_MPEG_TS)) {
+ AccumulateCategorical(
+ mozilla::Telemetry::LABELS_MSE_SOURCE_BUFFER_TYPE::AudioMp2t);
+ } else if (mimeType == MEDIAMIMETYPE(AUDIO_MP3)) {
+ AccumulateCategorical(
+ mozilla::Telemetry::LABELS_MSE_SOURCE_BUFFER_TYPE::AudioMpeg);
+ } else if (mimeType == MEDIAMIMETYPE(AUDIO_AAC)) {
+ AccumulateCategorical(
+ mozilla::Telemetry::LABELS_MSE_SOURCE_BUFFER_TYPE::AudioAac);
+ }
+}
+
+/* static */
+void MediaSource::IsTypeSupported(const nsAString& aType,
+ DecoderDoctorDiagnostics* aDiagnostics,
+ ErrorResult& aRv) {
+ if (aType.IsEmpty()) {
+ return aRv.ThrowTypeError("Empty type");
+ }
+
+ Maybe<MediaContainerType> containerType = MakeMediaContainerType(aType);
+ if (!containerType) {
+ return aRv.ThrowNotSupportedError("Unknown type");
+ }
+
+ if (DecoderTraits::CanHandleContainerType(*containerType, aDiagnostics) ==
+ CANPLAY_NO) {
+ return aRv.ThrowNotSupportedError("Can't play type");
+ }
+
+ bool hasVP9 = false;
+ const MediaCodecs& codecs = containerType->ExtendedType().Codecs();
+ for (const auto& codec : codecs.Range()) {
+ if (IsVP9CodecString(codec)) {
+ hasVP9 = true;
+ break;
+ }
+ }
+
+ // Now we know that this media type could be played.
+ // MediaSource imposes extra restrictions, and some prefs.
+ const MediaMIMEType& mimeType = containerType->Type();
+ if (mimeType == MEDIAMIMETYPE("video/mp4") ||
+ mimeType == MEDIAMIMETYPE("audio/mp4")) {
+ if (!StaticPrefs::media_mediasource_mp4_enabled()) {
+ // Don't leak information about the fact that it's pref-disabled; just act
+ // like we can't play it. Or should this throw "Unknown type"?
+ return aRv.ThrowNotSupportedError("Can't play type");
+ }
+ if (!StaticPrefs::media_mediasource_vp9_enabled() && hasVP9 &&
+ !IsVP9Forced(aDiagnostics)) {
+ // Don't leak information about the fact that it's pref-disabled; just act
+ // like we can't play it. Or should this throw "Unknown type"?
+ return aRv.ThrowNotSupportedError("Can't play type");
+ }
+
+ return;
+ }
+ if (mimeType == MEDIAMIMETYPE("video/webm")) {
+ if (!StaticPrefs::media_mediasource_webm_enabled()) {
+ // Don't leak information about the fact that it's pref-disabled; just act
+ // like we can't play it. Or should this throw "Unknown type"?
+ return aRv.ThrowNotSupportedError("Can't play type");
+ }
+ if (!StaticPrefs::media_mediasource_vp9_enabled() && hasVP9 &&
+ !IsVP9Forced(aDiagnostics)) {
+ // Don't leak information about the fact that it's pref-disabled; just act
+ // like we can't play it. Or should this throw "Unknown type"?
+ return aRv.ThrowNotSupportedError("Can't play type");
+ }
+ return;
+ }
+ if (mimeType == MEDIAMIMETYPE("audio/webm")) {
+ if (!(StaticPrefs::media_mediasource_webm_enabled() ||
+ StaticPrefs::media_mediasource_webm_audio_enabled())) {
+ // Don't leak information about the fact that it's pref-disabled; just act
+ // like we can't play it. Or should this throw "Unknown type"?
+ return aRv.ThrowNotSupportedError("Can't play type");
+ }
+ return;
+ }
+
+ return aRv.ThrowNotSupportedError("Type not supported in MediaSource");
+}
+
+/* static */
+already_AddRefed<MediaSource> MediaSource::Constructor(
+ const GlobalObject& aGlobal, ErrorResult& aRv) {
+ nsCOMPtr<nsPIDOMWindowInner> window =
+ do_QueryInterface(aGlobal.GetAsSupports());
+ if (!window) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ RefPtr<MediaSource> mediaSource = new MediaSource(window);
+ return mediaSource.forget();
+}
+
+MediaSource::~MediaSource() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_API("");
+ if (mDecoder) {
+ mDecoder->DetachMediaSource();
+ }
+}
+
+SourceBufferList* MediaSource::SourceBuffers() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT_IF(mReadyState == MediaSourceReadyState::Closed,
+ mSourceBuffers->IsEmpty());
+ return mSourceBuffers;
+}
+
+SourceBufferList* MediaSource::ActiveSourceBuffers() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT_IF(mReadyState == MediaSourceReadyState::Closed,
+ mActiveSourceBuffers->IsEmpty());
+ return mActiveSourceBuffers;
+}
+
+MediaSourceReadyState MediaSource::ReadyState() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mReadyState;
+}
+
+double MediaSource::Duration() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mReadyState == MediaSourceReadyState::Closed) {
+ return UnspecifiedNaN<double>();
+ }
+ MOZ_ASSERT(mDecoder);
+ return mDecoder->GetDuration();
+}
+
+void MediaSource::SetDuration(double aDuration, ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (aDuration < 0 || std::isnan(aDuration)) {
+ nsPrintfCString error("Invalid duration value %f", aDuration);
+ MSE_API("SetDuration(aDuration=%f, invalid value)", aDuration);
+ aRv.ThrowTypeError(error);
+ return;
+ }
+ if (mReadyState != MediaSourceReadyState::Open ||
+ mSourceBuffers->AnyUpdating()) {
+ MSE_API("SetDuration(aDuration=%f, invalid state)", aDuration);
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ DurationChange(aDuration, aRv);
+ MSE_API("SetDuration(aDuration=%f, errorCode=%d)", aDuration,
+ aRv.ErrorCodeAsInt());
+}
+
+void MediaSource::SetDuration(const media::TimeUnit& aDuration) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_API("SetDuration(aDuration=%f)", aDuration.ToSeconds());
+ mDecoder->SetMediaSourceDuration(aDuration);
+}
+
+already_AddRefed<SourceBuffer> MediaSource::AddSourceBuffer(
+ const nsAString& aType, ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+ DecoderDoctorDiagnostics diagnostics;
+ IsTypeSupported(aType, &diagnostics, aRv);
+ RecordTypeForTelemetry(aType, GetOwner());
+ bool supported = !aRv.Failed();
+ diagnostics.StoreFormatDiagnostics(
+ GetOwner() ? GetOwner()->GetExtantDoc() : nullptr, aType, supported,
+ __func__);
+ MSE_API("AddSourceBuffer(aType=%s)%s", NS_ConvertUTF16toUTF8(aType).get(),
+ supported ? "" : " [not supported]");
+ if (!supported) {
+ return nullptr;
+ }
+ if (mSourceBuffers->Length() >= MAX_SOURCE_BUFFERS) {
+ aRv.Throw(NS_ERROR_DOM_MEDIA_SOURCE_MAX_BUFFER_QUOTA_EXCEEDED_ERR);
+ return nullptr;
+ }
+ if (mReadyState != MediaSourceReadyState::Open) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+ Maybe<MediaContainerType> containerType = MakeMediaContainerType(aType);
+ if (!containerType) {
+ aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+ return nullptr;
+ }
+ RefPtr<SourceBuffer> sourceBuffer = new SourceBuffer(this, *containerType);
+ mSourceBuffers->Append(sourceBuffer);
+ DDLINKCHILD("sourcebuffer[]", sourceBuffer.get());
+ MSE_DEBUG("sourceBuffer=%p", sourceBuffer.get());
+ return sourceBuffer.forget();
+}
+
+RefPtr<MediaSource::ActiveCompletionPromise> MediaSource::SourceBufferIsActive(
+ SourceBuffer* aSourceBuffer) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mActiveSourceBuffers->ClearSimple();
+ bool initMissing = false;
+ bool found = false;
+ for (uint32_t i = 0; i < mSourceBuffers->Length(); i++) {
+ SourceBuffer* sourceBuffer = mSourceBuffers->IndexedGetter(i, found);
+ MOZ_ALWAYS_TRUE(found);
+ if (sourceBuffer == aSourceBuffer) {
+ mActiveSourceBuffers->Append(aSourceBuffer);
+ } else if (sourceBuffer->IsActive()) {
+ mActiveSourceBuffers->AppendSimple(sourceBuffer);
+ } else {
+ // Some source buffers haven't yet received an init segment.
+ // There's nothing more we can do at this stage.
+ initMissing = true;
+ }
+ }
+ if (initMissing || !mDecoder) {
+ return ActiveCompletionPromise::CreateAndResolve(true, __func__);
+ }
+
+ mDecoder->NotifyInitDataArrived();
+
+ // Add our promise to the queue.
+ // It will be resolved once the HTMLMediaElement modifies its readyState.
+ MozPromiseHolder<ActiveCompletionPromise> holder;
+ RefPtr<ActiveCompletionPromise> promise = holder.Ensure(__func__);
+ mCompletionPromises.AppendElement(std::move(holder));
+ return promise;
+}
+
+void MediaSource::CompletePendingTransactions() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_DEBUG("Resolving %u promises", unsigned(mCompletionPromises.Length()));
+ for (auto& promise : mCompletionPromises) {
+ promise.Resolve(true, __func__);
+ }
+ mCompletionPromises.Clear();
+}
+
+void MediaSource::RemoveSourceBuffer(SourceBuffer& aSourceBuffer,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+ SourceBuffer* sourceBuffer = &aSourceBuffer;
+ MSE_API("RemoveSourceBuffer(aSourceBuffer=%p)", sourceBuffer);
+ if (!mSourceBuffers->Contains(sourceBuffer)) {
+ aRv.Throw(NS_ERROR_DOM_NOT_FOUND_ERR);
+ return;
+ }
+
+ sourceBuffer->AbortBufferAppend();
+ // TODO:
+ // abort stream append loop (if running)
+
+ // TODO:
+ // For all sourceBuffer audioTracks, videoTracks, textTracks:
+ // set sourceBuffer to null
+ // remove sourceBuffer video, audio, text Tracks from MediaElement tracks
+ // remove sourceBuffer video, audio, text Tracks and fire "removetrack" at
+ // affected lists fire "removetrack" at modified MediaElement track lists
+ // If removed enabled/selected, fire "change" at affected MediaElement list.
+ if (mActiveSourceBuffers->Contains(sourceBuffer)) {
+ mActiveSourceBuffers->Remove(sourceBuffer);
+ }
+ mSourceBuffers->Remove(sourceBuffer);
+ DDUNLINKCHILD(sourceBuffer);
+ // TODO: Free all resources associated with sourceBuffer
+}
+
+void MediaSource::EndOfStream(
+ const Optional<MediaSourceEndOfStreamError>& aError, ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_API("EndOfStream(aError=%d)",
+ aError.WasPassed() ? uint32_t(aError.Value()) : 0);
+ if (mReadyState != MediaSourceReadyState::Open ||
+ mSourceBuffers->AnyUpdating()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ SetReadyState(MediaSourceReadyState::Ended);
+ mSourceBuffers->Ended();
+ if (!aError.WasPassed()) {
+ DurationChange(mSourceBuffers->GetHighestBufferedEndTime().ToBase(1000000),
+ aRv);
+ // Notify reader that all data is now available.
+ mDecoder->Ended(true);
+ return;
+ }
+ switch (aError.Value()) {
+ case MediaSourceEndOfStreamError::Network:
+ mDecoder->NetworkError(MediaResult(NS_ERROR_FAILURE, "MSE network"));
+ break;
+ case MediaSourceEndOfStreamError::Decode:
+ mDecoder->DecodeError(NS_ERROR_DOM_MEDIA_FATAL_ERR);
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE(
+ "Someone added a MediaSourceReadyState value and didn't handle it "
+ "here");
+ break;
+ }
+}
+
+void MediaSource::EndOfStream(const MediaResult& aError) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_API("EndOfStream(aError=%s)", aError.ErrorName().get());
+
+ SetReadyState(MediaSourceReadyState::Ended);
+ mSourceBuffers->Ended();
+ mDecoder->DecodeError(aError);
+}
+
+/* static */
+bool MediaSource::IsTypeSupported(const GlobalObject& aOwner,
+ const nsAString& aType) {
+ MOZ_ASSERT(NS_IsMainThread());
+ DecoderDoctorDiagnostics diagnostics;
+ IgnoredErrorResult rv;
+ IsTypeSupported(aType, &diagnostics, rv);
+ bool supported = !rv.Failed();
+ nsCOMPtr<nsPIDOMWindowInner> window =
+ do_QueryInterface(aOwner.GetAsSupports());
+ RecordTypeForTelemetry(aType, window);
+ diagnostics.StoreFormatDiagnostics(window ? window->GetExtantDoc() : nullptr,
+ aType, supported, __func__);
+ MOZ_LOG(GetMediaSourceAPILog(), mozilla::LogLevel::Debug,
+ ("MediaSource::%s: IsTypeSupported(aType=%s) %s", __func__,
+ NS_ConvertUTF16toUTF8(aType).get(),
+ supported ? "OK" : "[not supported]"));
+ return supported;
+}
+
+void MediaSource::SetLiveSeekableRange(double aStart, double aEnd,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // 1. If the readyState attribute is not "open" then throw an
+ // InvalidStateError exception and abort these steps.
+ if (mReadyState != MediaSourceReadyState::Open) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ // 2. If start is negative or greater than end, then throw a TypeError
+ // exception and abort these steps.
+ if (aStart < 0 || aStart > aEnd) {
+ aRv.ThrowTypeError("Invalid start value");
+ return;
+ }
+
+ // 3. Set live seekable range to be a new normalized TimeRanges object
+ // containing a single range whose start position is start and end position is
+ // end.
+ mLiveSeekableRange = Some(media::TimeRanges(media::TimeRange(aStart, aEnd)));
+}
+
+void MediaSource::ClearLiveSeekableRange(ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // 1. If the readyState attribute is not "open" then throw an
+ // InvalidStateError exception and abort these steps.
+ if (mReadyState != MediaSourceReadyState::Open) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ // 2. If live seekable range contains a range, then set live seekable range to
+ // be a new empty TimeRanges object.
+ mLiveSeekableRange.reset();
+}
+
+bool MediaSource::Attach(MediaSourceDecoder* aDecoder) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_DEBUG("Attach(aDecoder=%p) owner=%p", aDecoder, aDecoder->GetOwner());
+ MOZ_ASSERT(aDecoder);
+ MOZ_ASSERT(aDecoder->GetOwner());
+ if (mReadyState != MediaSourceReadyState::Closed) {
+ return false;
+ }
+ MOZ_ASSERT(!mMediaElement);
+ mMediaElement = aDecoder->GetOwner()->GetMediaElement();
+ MOZ_ASSERT(!mDecoder);
+ mDecoder = aDecoder;
+ mDecoder->AttachMediaSource(this);
+ SetReadyState(MediaSourceReadyState::Open);
+ return true;
+}
+
+void MediaSource::Detach() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_RELEASE_ASSERT(mCompletionPromises.IsEmpty());
+ MSE_DEBUG("mDecoder=%p owner=%p", mDecoder.get(),
+ mDecoder ? mDecoder->GetOwner() : nullptr);
+ if (!mDecoder) {
+ MOZ_ASSERT(mReadyState == MediaSourceReadyState::Closed);
+ MOZ_ASSERT(mActiveSourceBuffers->IsEmpty() && mSourceBuffers->IsEmpty());
+ return;
+ }
+ mMediaElement = nullptr;
+ SetReadyState(MediaSourceReadyState::Closed);
+ if (mActiveSourceBuffers) {
+ mActiveSourceBuffers->Clear();
+ }
+ if (mSourceBuffers) {
+ mSourceBuffers->Clear();
+ }
+ mDecoder->DetachMediaSource();
+ mDecoder = nullptr;
+}
+
+MediaSource::MediaSource(nsPIDOMWindowInner* aWindow)
+ : DOMEventTargetHelper(aWindow),
+ mDecoder(nullptr),
+ mPrincipal(nullptr),
+ mAbstractMainThread(
+ GetOwnerGlobal()->AbstractMainThreadFor(TaskCategory::Other)),
+ mReadyState(MediaSourceReadyState::Closed) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mSourceBuffers = new SourceBufferList(this);
+ mActiveSourceBuffers = new SourceBufferList(this);
+
+ nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(aWindow);
+ if (sop) {
+ mPrincipal = sop->GetPrincipal();
+ }
+
+ MSE_API("MediaSource(aWindow=%p) mSourceBuffers=%p mActiveSourceBuffers=%p",
+ aWindow, mSourceBuffers.get(), mActiveSourceBuffers.get());
+}
+
+void MediaSource::SetReadyState(MediaSourceReadyState aState) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aState != mReadyState);
+ MSE_DEBUG("SetReadyState(aState=%" PRIu32 ") mReadyState=%" PRIu32,
+ static_cast<uint32_t>(aState), static_cast<uint32_t>(mReadyState));
+
+ MediaSourceReadyState oldState = mReadyState;
+ mReadyState = aState;
+
+ if (mReadyState == MediaSourceReadyState::Open &&
+ (oldState == MediaSourceReadyState::Closed ||
+ oldState == MediaSourceReadyState::Ended)) {
+ QueueAsyncSimpleEvent("sourceopen");
+ if (oldState == MediaSourceReadyState::Ended) {
+ // Notify reader that more data may come.
+ mDecoder->Ended(false);
+ }
+ return;
+ }
+
+ if (mReadyState == MediaSourceReadyState::Ended &&
+ oldState == MediaSourceReadyState::Open) {
+ QueueAsyncSimpleEvent("sourceended");
+ return;
+ }
+
+ if (mReadyState == MediaSourceReadyState::Closed &&
+ (oldState == MediaSourceReadyState::Open ||
+ oldState == MediaSourceReadyState::Ended)) {
+ QueueAsyncSimpleEvent("sourceclose");
+ return;
+ }
+
+ NS_WARNING("Invalid MediaSource readyState transition");
+}
+
+void MediaSource::DispatchSimpleEvent(const char* aName) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_API("Dispatch event '%s'", aName);
+ DispatchTrustedEvent(NS_ConvertUTF8toUTF16(aName));
+}
+
+void MediaSource::QueueAsyncSimpleEvent(const char* aName) {
+ MSE_DEBUG("Queuing event '%s'", aName);
+ nsCOMPtr<nsIRunnable> event = new AsyncEventRunner<MediaSource>(this, aName);
+ mAbstractMainThread->Dispatch(event.forget());
+}
+
+void MediaSource::DurationChange(const media::TimeUnit& aNewDuration,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_DEBUG("DurationChange(aNewDuration=%s)", aNewDuration.ToString().get());
+
+ // 1. If the current value of duration is equal to new duration, then return.
+ if (mDecoder->GetDuration() == aNewDuration.ToSeconds()) {
+ return;
+ }
+
+ // 2. If new duration is less than the highest starting presentation timestamp
+ // of any buffered coded frames for all SourceBuffer objects in sourceBuffers,
+ // then throw an InvalidStateError exception and abort these steps.
+ if (aNewDuration < mSourceBuffers->HighestStartTime()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ // 3. Let highest end time be the largest track buffer ranges end time across
+ // all the track buffers across all SourceBuffer objects in sourceBuffers.
+ media::TimeUnit highestEndTime = mSourceBuffers->HighestEndTime();
+ // 4. If new duration is less than highest end time, then
+ // 4.1 Update new duration to equal highest end time.
+ media::TimeUnit newDuration = std::max(aNewDuration, highestEndTime);
+
+ // 5. Update the media duration to new duration and run the HTMLMediaElement
+ // duration change algorithm.
+ mDecoder->SetMediaSourceDuration(newDuration);
+}
+
+void MediaSource::DurationChange(double aNewDuration, ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_DEBUG("DurationChange(aNewDuration=%f)", aNewDuration);
+
+ // 1. If the current value of duration is equal to new duration, then return.
+ if (mDecoder->GetDuration() == aNewDuration) {
+ return;
+ }
+
+ // 2. If new duration is less than the highest starting presentation timestamp
+ // of any buffered coded frames for all SourceBuffer objects in sourceBuffers,
+ // then throw an InvalidStateError exception and abort these steps.
+ if (aNewDuration < mSourceBuffers->HighestStartTime().ToSeconds()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ // 3. Let highest end time be the largest track buffer ranges end time across
+ // all the track buffers across all SourceBuffer objects in sourceBuffers.
+ double highestEndTime = mSourceBuffers->HighestEndTime().ToSeconds();
+ // 4. If new duration is less than highest end time, then
+ // 4.1 Update new duration to equal highest end time.
+ double newDuration = std::max(aNewDuration, highestEndTime);
+
+ // 5. Update the media duration to new duration and run the HTMLMediaElement
+ // duration change algorithm.
+ mDecoder->SetMediaSourceDuration(newDuration);
+}
+
+already_AddRefed<Promise> MediaSource::MozDebugReaderData(ErrorResult& aRv) {
+ // Creating a JS promise
+ nsPIDOMWindowInner* win = GetOwner();
+ if (!win) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+ RefPtr<Promise> domPromise = Promise::Create(win->AsGlobal(), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+ MOZ_ASSERT(domPromise);
+ UniquePtr<MediaSourceDecoderDebugInfo> info =
+ MakeUnique<MediaSourceDecoderDebugInfo>();
+ mDecoder->RequestDebugInfo(*info)->Then(
+ mAbstractMainThread, __func__,
+ [domPromise, infoPtr = std::move(info)] {
+ domPromise->MaybeResolve(infoPtr.get());
+ },
+ [] {
+ MOZ_ASSERT_UNREACHABLE("Unexpected rejection while getting debug data");
+ });
+
+ return domPromise.forget();
+}
+
+nsPIDOMWindowInner* MediaSource::GetParentObject() const { return GetOwner(); }
+
+JSObject* MediaSource::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return MediaSource_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(MediaSource, DOMEventTargetHelper,
+ mMediaElement, mSourceBuffers,
+ mActiveSourceBuffers)
+
+NS_IMPL_ADDREF_INHERITED(MediaSource, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(MediaSource, DOMEventTargetHelper)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaSource)
+ NS_INTERFACE_MAP_ENTRY_CONCRETE(mozilla::dom::MediaSource)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+#undef MSE_DEBUG
+#undef MSE_API
+
+} // namespace dom
+
+} // namespace mozilla
diff --git a/dom/media/mediasource/MediaSource.h b/dom/media/mediasource/MediaSource.h
new file mode 100644
index 0000000000..e0f90e8416
--- /dev/null
+++ b/dom/media/mediasource/MediaSource.h
@@ -0,0 +1,182 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_MediaSource_h_
+#define mozilla_dom_MediaSource_h_
+
+#include "MediaSourceDecoder.h"
+#include "js/RootingAPI.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/dom/MediaSourceBinding.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionNoteChild.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsID.h"
+#include "nsISupports.h"
+#include "nscore.h"
+#include "TimeUnits.h"
+
+struct JSContext;
+class JSObject;
+class nsPIDOMWindowInner;
+
+namespace mozilla {
+
+class AbstractThread;
+class ErrorResult;
+template <typename T>
+class AsyncEventRunner;
+class MediaResult;
+
+namespace dom {
+class MediaSource;
+} // namespace dom
+DDLoggedTypeName(dom::MediaSource);
+
+namespace dom {
+
+class GlobalObject;
+class SourceBuffer;
+class SourceBufferList;
+template <typename T>
+class Optional;
+
+#define MOZILLA_DOM_MEDIASOURCE_IMPLEMENTATION_IID \
+ { \
+ 0x3839d699, 0x22c5, 0x439f, { \
+ 0x94, 0xca, 0x0e, 0x0b, 0x26, 0xf9, 0xca, 0xbf \
+ } \
+ }
+
+class MediaSource final : public DOMEventTargetHelper,
+ public DecoderDoctorLifeLogger<MediaSource> {
+ public:
+ /** WebIDL Methods. */
+ static already_AddRefed<MediaSource> Constructor(const GlobalObject& aGlobal,
+ ErrorResult& aRv);
+
+ SourceBufferList* SourceBuffers();
+ SourceBufferList* ActiveSourceBuffers();
+ MediaSourceReadyState ReadyState();
+
+ double Duration();
+ void SetDuration(double aDuration, ErrorResult& aRv);
+
+ already_AddRefed<SourceBuffer> AddSourceBuffer(const nsAString& aType,
+ ErrorResult& aRv);
+ void RemoveSourceBuffer(SourceBuffer& aSourceBuffer, ErrorResult& aRv);
+
+ void EndOfStream(const Optional<MediaSourceEndOfStreamError>& aError,
+ ErrorResult& aRv);
+ void EndOfStream(const MediaResult& aError);
+
+ void SetLiveSeekableRange(double aStart, double aEnd, ErrorResult& aRv);
+ void ClearLiveSeekableRange(ErrorResult& aRv);
+
+ static bool IsTypeSupported(const GlobalObject&, const nsAString& aType);
+ // Throws on aRv if not supported.
+ static void IsTypeSupported(const nsAString& aType,
+ DecoderDoctorDiagnostics* aDiagnostics,
+ ErrorResult& aRv);
+
+ IMPL_EVENT_HANDLER(sourceopen);
+ IMPL_EVENT_HANDLER(sourceended);
+ IMPL_EVENT_HANDLER(sourceclose);
+
+ /** End WebIDL Methods. */
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MediaSource, DOMEventTargetHelper)
+ NS_DECLARE_STATIC_IID_ACCESSOR(MOZILLA_DOM_MEDIASOURCE_IMPLEMENTATION_IID)
+
+ nsPIDOMWindowInner* GetParentObject() const;
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // Attach this MediaSource to Decoder aDecoder. Returns false if already
+ // attached.
+ bool Attach(MediaSourceDecoder* aDecoder);
+ void Detach();
+
+ // Set mReadyState to aState and fire the required events at the MediaSource.
+ void SetReadyState(MediaSourceReadyState aState);
+
+ // Used by SourceBuffer to call CreateSubDecoder.
+ MediaSourceDecoder* GetDecoder() { return mDecoder; }
+
+ nsIPrincipal* GetPrincipal() { return mPrincipal; }
+
+ // Returns a structure describing the state of the MediaSource internal
+ // buffered data. Used for debugging purposes.
+ already_AddRefed<Promise> MozDebugReaderData(ErrorResult& aRv);
+
+ bool HasLiveSeekableRange() const { return mLiveSeekableRange.isSome(); }
+ media::TimeRanges LiveSeekableRange() const {
+ return mLiveSeekableRange.value();
+ }
+
+ AbstractThread* AbstractMainThread() const { return mAbstractMainThread; }
+
+ // Resolve all CompletionPromise pending.
+ void CompletePendingTransactions();
+
+ private:
+ // SourceBuffer uses SetDuration and SourceBufferIsActive
+ friend class mozilla::dom::SourceBuffer;
+
+ ~MediaSource();
+
+ explicit MediaSource(nsPIDOMWindowInner* aWindow);
+
+ friend class AsyncEventRunner<MediaSource>;
+ void DispatchSimpleEvent(const char* aName);
+ void QueueAsyncSimpleEvent(const char* aName);
+
+ void DurationChange(const media::TimeUnit& aNewDuration, ErrorResult& aRv);
+ void DurationChange(double aNewDuration, ErrorResult& aRv);
+
+ // SetDuration with no checks.
+ void SetDuration(const media::TimeUnit& aDuration);
+
+ typedef MozPromise<bool, MediaResult, /* IsExclusive = */ true>
+ ActiveCompletionPromise;
+ // Mark SourceBuffer as active and rebuild ActiveSourceBuffers.
+ // Return a MozPromise that will be resolved once all related operations are
+ // completed, or can't progress any further.
+ // Such as, transition of readyState from HAVE_NOTHING to HAVE_METADATA.
+ RefPtr<ActiveCompletionPromise> SourceBufferIsActive(
+ SourceBuffer* aSourceBuffer);
+
+ RefPtr<SourceBufferList> mSourceBuffers;
+ RefPtr<SourceBufferList> mActiveSourceBuffers;
+
+ RefPtr<MediaSourceDecoder> mDecoder;
+ // Ensures the media element remains alive to dispatch progress and
+ // durationchanged events.
+ RefPtr<HTMLMediaElement> mMediaElement;
+
+ RefPtr<nsIPrincipal> mPrincipal;
+
+ const RefPtr<AbstractThread> mAbstractMainThread;
+
+ MediaSourceReadyState mReadyState;
+
+ Maybe<media::TimeRanges> mLiveSeekableRange;
+ nsTArray<MozPromiseHolder<ActiveCompletionPromise>> mCompletionPromises;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(MediaSource,
+ MOZILLA_DOM_MEDIASOURCE_IMPLEMENTATION_IID)
+
+} // namespace dom
+
+} // namespace mozilla
+
+#endif /* mozilla_dom_MediaSource_h_ */
diff --git a/dom/media/mediasource/MediaSourceDecoder.cpp b/dom/media/mediasource/MediaSourceDecoder.cpp
new file mode 100644
index 0000000000..24a74e261b
--- /dev/null
+++ b/dom/media/mediasource/MediaSourceDecoder.cpp
@@ -0,0 +1,372 @@
+/* -*- mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "MediaSourceDecoder.h"
+
+#include "base/process_util.h"
+#include "mozilla/Logging.h"
+#include "ExternalEngineStateMachine.h"
+#include "MediaDecoder.h"
+#include "MediaDecoderStateMachine.h"
+#include "MediaShutdownManager.h"
+#include "MediaSource.h"
+#include "MediaSourceDemuxer.h"
+#include "MediaSourceUtils.h"
+#include "SourceBuffer.h"
+#include "SourceBufferList.h"
+#include "VideoUtils.h"
+#include <algorithm>
+
+extern mozilla::LogModule* GetMediaSourceLog();
+
+#define MSE_DEBUG(arg, ...) \
+ DDMOZ_LOG(GetMediaSourceLog(), mozilla::LogLevel::Debug, "::%s: " arg, \
+ __func__, ##__VA_ARGS__)
+#define MSE_DEBUGV(arg, ...) \
+ DDMOZ_LOG(GetMediaSourceLog(), mozilla::LogLevel::Verbose, "::%s: " arg, \
+ __func__, ##__VA_ARGS__)
+
+using namespace mozilla::media;
+
+namespace mozilla {
+
+MediaSourceDecoder::MediaSourceDecoder(MediaDecoderInit& aInit)
+ : MediaDecoder(aInit), mMediaSource(nullptr), mEnded(false) {
+ mExplicitDuration.emplace(UnspecifiedNaN<double>());
+}
+
+MediaDecoderStateMachineBase* MediaSourceDecoder::CreateStateMachine(
+ bool aDisableExternalEngine) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // if `mDemuxer` already exists, that means we're in the process of recreating
+ // the state machine. The track buffers are tied to the demuxer so we would
+ // need to reuse it.
+ if (!mDemuxer) {
+ mDemuxer = new MediaSourceDemuxer(AbstractMainThread());
+ }
+ MediaFormatReaderInit init;
+ init.mVideoFrameContainer = GetVideoFrameContainer();
+ init.mKnowsCompositor = GetCompositor();
+ init.mCrashHelper = GetOwner()->CreateGMPCrashHelper();
+ init.mFrameStats = mFrameStats;
+ init.mMediaDecoderOwnerID = mOwner;
+ static Atomic<uint32_t> sTrackingIdCounter(0);
+ init.mTrackingId.emplace(TrackingId::Source::MSEDecoder, sTrackingIdCounter++,
+ TrackingId::TrackAcrossProcesses::Yes);
+ mReader = new MediaFormatReader(init, mDemuxer);
+#ifdef MOZ_WMF_MEDIA_ENGINE
+ // TODO : Only for testing development for now. In the future this should be
+ // used for encrypted content only.
+ if (StaticPrefs::media_wmf_media_engine_enabled() &&
+ !aDisableExternalEngine) {
+ return new ExternalEngineStateMachine(this, mReader);
+ }
+#endif
+ return new MediaDecoderStateMachine(this, mReader);
+}
+
+nsresult MediaSourceDecoder::Load(nsIPrincipal* aPrincipal) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!GetStateMachine());
+
+ mPrincipal = aPrincipal;
+
+ nsresult rv = MediaShutdownManager::Instance().Register(this);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ return CreateAndInitStateMachine(!mEnded);
+}
+
+template <typename IntervalType>
+IntervalType MediaSourceDecoder::GetSeekableImpl() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!mMediaSource) {
+ NS_WARNING("MediaSource element isn't attached");
+ return IntervalType();
+ }
+
+ TimeIntervals seekable;
+ double duration = mMediaSource->Duration();
+ if (std::isnan(duration)) {
+ // Return empty range.
+ } else if (duration > 0 && std::isinf(duration)) {
+ media::TimeIntervals buffered = GetBuffered();
+
+ // 1. If live seekable range is not empty:
+ if (mMediaSource->HasLiveSeekableRange()) {
+ // 1. Let union ranges be the union of live seekable range and the
+ // HTMLMediaElement.buffered attribute.
+ TimeRanges unionRanges =
+ media::TimeRanges(buffered) + mMediaSource->LiveSeekableRange();
+ // 2. Return a single range with a start time equal to the earliest start
+ // time in union ranges and an end time equal to the highest end time in
+ // union ranges and abort these steps.
+ if constexpr (std::is_same<IntervalType, TimeRanges>::value) {
+ TimeRanges seekableRange = media::TimeRanges(
+ TimeRange(unionRanges.GetStart(), unionRanges.GetEnd()));
+ return seekableRange;
+ } else {
+ MOZ_RELEASE_ASSERT(false);
+ }
+ }
+
+ if (!buffered.IsEmpty()) {
+ seekable += media::TimeInterval(TimeUnit::Zero(), buffered.GetEnd());
+ }
+ } else {
+ if constexpr (std::is_same<IntervalType, TimeRanges>::value) {
+ // Common case: seekable in entire range of the media.
+ return TimeRanges(TimeRange(0, duration));
+ } else if constexpr (std::is_same<IntervalType, TimeIntervals>::value) {
+ seekable += media::TimeInterval(TimeUnit::Zero(),
+ mDuration.match(DurationToTimeUnit()));
+ } else {
+ MOZ_RELEASE_ASSERT(false);
+ }
+ }
+ MSE_DEBUG("ranges=%s", DumpTimeRanges(seekable).get());
+ return IntervalType(seekable);
+}
+
+media::TimeIntervals MediaSourceDecoder::GetSeekable() {
+ return GetSeekableImpl<media::TimeIntervals>();
+}
+
+media::TimeRanges MediaSourceDecoder::GetSeekableTimeRanges() {
+ return GetSeekableImpl<media::TimeRanges>();
+}
+
+media::TimeIntervals MediaSourceDecoder::GetBuffered() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mMediaSource) {
+ NS_WARNING("MediaSource element isn't attached");
+ return media::TimeIntervals::Invalid();
+ }
+ dom::SourceBufferList* sourceBuffers = mMediaSource->ActiveSourceBuffers();
+ if (!sourceBuffers) {
+ // Media source object is shutting down.
+ return TimeIntervals();
+ }
+ TimeUnit highestEndTime;
+ nsTArray<media::TimeIntervals> activeRanges;
+ media::TimeIntervals buffered;
+
+ for (uint32_t i = 0; i < sourceBuffers->Length(); i++) {
+ bool found;
+ dom::SourceBuffer* sb = sourceBuffers->IndexedGetter(i, found);
+ MOZ_ASSERT(found);
+
+ activeRanges.AppendElement(sb->GetTimeIntervals());
+ highestEndTime =
+ std::max(highestEndTime, activeRanges.LastElement().GetEnd());
+ }
+
+ buffered += media::TimeInterval(TimeUnit::Zero(), highestEndTime);
+
+ for (auto& range : activeRanges) {
+ if (mEnded && !range.IsEmpty()) {
+ // Set the end time on the last range to highestEndTime by adding a
+ // new range spanning the current end time to highestEndTime, which
+ // Normalize() will then merge with the old last range.
+ range += media::TimeInterval(range.GetEnd(), highestEndTime);
+ }
+ buffered.Intersection(range);
+ }
+
+ MSE_DEBUG("ranges=%s", DumpTimeRanges(buffered).get());
+ return buffered;
+}
+
+void MediaSourceDecoder::Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_DEBUG("Shutdown");
+ // Detach first so that TrackBuffers are unused on the main thread when
+ // shut down on the decode task queue.
+ if (mMediaSource) {
+ mMediaSource->Detach();
+ }
+ mDemuxer = nullptr;
+
+ MediaDecoder::Shutdown();
+}
+
+void MediaSourceDecoder::AttachMediaSource(dom::MediaSource* aMediaSource) {
+ MOZ_ASSERT(!mMediaSource && !GetStateMachine() && NS_IsMainThread());
+ mMediaSource = aMediaSource;
+ DDLINKCHILD("mediasource", aMediaSource);
+}
+
+void MediaSourceDecoder::DetachMediaSource() {
+ MOZ_ASSERT(mMediaSource && NS_IsMainThread());
+ DDUNLINKCHILD(mMediaSource);
+ mMediaSource = nullptr;
+}
+
+void MediaSourceDecoder::Ended(bool aEnded) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (aEnded) {
+ // We want the MediaSourceReader to refresh its buffered range as it may
+ // have been modified (end lined up).
+ NotifyDataArrived();
+ }
+ mEnded = aEnded;
+ GetStateMachine()->DispatchIsLiveStream(!mEnded);
+}
+
+void MediaSourceDecoder::AddSizeOfResources(ResourceSizes* aSizes) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (GetDemuxer()) {
+ GetDemuxer()->AddSizeOfResources(aSizes);
+ }
+}
+
+void MediaSourceDecoder::SetInitialDuration(const TimeUnit& aDuration) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // Only use the decoded duration if one wasn't already
+ // set.
+ if (!mMediaSource || !std::isnan(ExplicitDuration())) {
+ return;
+ }
+ SetMediaSourceDuration(aDuration);
+}
+
+void MediaSourceDecoder::SetMediaSourceDuration(const TimeUnit& aDuration) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!IsShutdown());
+ if (aDuration.IsPositiveOrZero()) {
+ SetExplicitDuration(ToMicrosecondResolution(aDuration.ToSeconds()));
+ } else {
+ SetExplicitDuration(PositiveInfinity<double>());
+ }
+}
+
+void MediaSourceDecoder::SetMediaSourceDuration(double aDuration) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!IsShutdown());
+ if (aDuration >= 0) {
+ SetExplicitDuration(aDuration);
+ } else {
+ SetExplicitDuration(PositiveInfinity<double>());
+ }
+}
+
+RefPtr<GenericPromise> MediaSourceDecoder::RequestDebugInfo(
+ dom::MediaSourceDecoderDebugInfo& aInfo) {
+ // This should be safe to call off main thead, but there's no such usage at
+ // time of writing. Can be carefully relaxed if needed.
+ MOZ_ASSERT(NS_IsMainThread(), "Expects to be called on main thread.");
+ nsTArray<RefPtr<GenericPromise>> promises;
+ if (mReader) {
+ promises.AppendElement(mReader->RequestDebugInfo(aInfo.mReader));
+ }
+ if (mDemuxer) {
+ promises.AppendElement(mDemuxer->GetDebugInfo(aInfo.mDemuxer));
+ }
+ return GenericPromise::All(GetCurrentSerialEventTarget(), promises)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ []() { return GenericPromise::CreateAndResolve(true, __func__); },
+ [] {
+ return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ });
+}
+
+double MediaSourceDecoder::GetDuration() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return ExplicitDuration();
+}
+
+MediaDecoderOwner::NextFrameStatus
+MediaSourceDecoder::NextFrameBufferedStatus() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mMediaSource ||
+ mMediaSource->ReadyState() == dom::MediaSourceReadyState::Closed) {
+ return MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE;
+ }
+
+ // Next frame hasn't been decoded yet.
+ // Use the buffered range to consider if we have the next frame available.
+ auto currentPosition = CurrentPosition();
+ TimeIntervals buffered = GetBuffered();
+ buffered.SetFuzz(MediaSourceDemuxer::EOS_FUZZ / 2);
+ TimeInterval interval(
+ currentPosition, currentPosition + DEFAULT_NEXT_FRAME_AVAILABLE_BUFFERED);
+ return buffered.ContainsWithStrictEnd(ClampIntervalToEnd(interval))
+ ? MediaDecoderOwner::NEXT_FRAME_AVAILABLE
+ : MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE;
+}
+
+bool MediaSourceDecoder::CanPlayThroughImpl() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NextFrameBufferedStatus() == MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE) {
+ return false;
+ }
+
+ if (std::isnan(mMediaSource->Duration())) {
+ // Don't have any data yet.
+ return false;
+ }
+ TimeUnit duration = TimeUnit::FromSeconds(mMediaSource->Duration());
+ auto currentPosition = CurrentPosition();
+ if (duration <= currentPosition) {
+ return true;
+ }
+ // If we have data up to the mediasource's duration or 3s ahead, we can
+ // assume that we can play without interruption.
+ dom::SourceBufferList* sourceBuffers = mMediaSource->ActiveSourceBuffers();
+ TimeUnit bufferedEnd = sourceBuffers->GetHighestBufferedEndTime();
+ TimeUnit timeAhead =
+ std::min(duration, currentPosition + TimeUnit::FromSeconds(3));
+ TimeInterval interval(currentPosition, timeAhead);
+ return bufferedEnd >= timeAhead;
+}
+
+TimeInterval MediaSourceDecoder::ClampIntervalToEnd(
+ const TimeInterval& aInterval) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mEnded) {
+ return aInterval;
+ }
+ TimeUnit duration = mDuration.match(DurationToTimeUnit());
+ if (duration < aInterval.mStart) {
+ return aInterval;
+ }
+ return TimeInterval(aInterval.mStart, std::min(aInterval.mEnd, duration),
+ aInterval.mFuzz);
+}
+
+void MediaSourceDecoder::NotifyInitDataArrived() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mDemuxer) {
+ mDemuxer->NotifyInitDataArrived();
+ }
+}
+
+void MediaSourceDecoder::NotifyDataArrived() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
+ NotifyReaderDataArrived();
+ GetOwner()->DownloadProgressed();
+}
+
+already_AddRefed<nsIPrincipal> MediaSourceDecoder::GetCurrentPrincipal() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return do_AddRef(mPrincipal);
+}
+
+bool MediaSourceDecoder::HadCrossOriginRedirects() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return false;
+}
+
+#undef MSE_DEBUG
+#undef MSE_DEBUGV
+
+} // namespace mozilla
diff --git a/dom/media/mediasource/MediaSourceDecoder.h b/dom/media/mediasource/MediaSourceDecoder.h
new file mode 100644
index 0000000000..ff312cb6cf
--- /dev/null
+++ b/dom/media/mediasource/MediaSourceDecoder.h
@@ -0,0 +1,101 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_MEDIASOURCEDECODER_H_
+#define MOZILLA_MEDIASOURCEDECODER_H_
+
+#include "MediaDecoder.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/dom/MediaDebugInfoBinding.h"
+
+namespace mozilla {
+
+class MediaDecoderStateMachineBase;
+class MediaSourceDemuxer;
+
+namespace dom {
+
+class MediaSource;
+
+} // namespace dom
+
+DDLoggedTypeDeclNameAndBase(MediaSourceDecoder, MediaDecoder);
+
+class MediaSourceDecoder : public MediaDecoder,
+ public DecoderDoctorLifeLogger<MediaSourceDecoder> {
+ public:
+ explicit MediaSourceDecoder(MediaDecoderInit& aInit);
+
+ nsresult Load(nsIPrincipal* aPrincipal);
+ media::TimeIntervals GetSeekable() override;
+ media::TimeRanges GetSeekableTimeRanges() override;
+ media::TimeIntervals GetBuffered() override;
+
+ void Shutdown() override;
+
+ void AttachMediaSource(dom::MediaSource* aMediaSource);
+ void DetachMediaSource();
+
+ void Ended(bool aEnded);
+
+ // Return the duration of the video in seconds.
+ double GetDuration() override;
+
+ void SetInitialDuration(const media::TimeUnit& aDuration);
+ void SetMediaSourceDuration(const media::TimeUnit& aDuration);
+ void SetMediaSourceDuration(double aDuration);
+
+ MediaSourceDemuxer* GetDemuxer() { return mDemuxer; }
+
+ already_AddRefed<nsIPrincipal> GetCurrentPrincipal() override;
+
+ bool HadCrossOriginRedirects() override;
+
+ bool IsTransportSeekable() override { return true; }
+
+ // Requests that the MediaSourceDecoder populates aInfo with debug
+ // information. This may be done asynchronously, and aInfo should *not* be
+ // accessed by the caller until the returned promise is resolved or rejected.
+ RefPtr<GenericPromise> RequestDebugInfo(
+ dom::MediaSourceDecoderDebugInfo& aInfo);
+
+ void AddSizeOfResources(ResourceSizes* aSizes) override;
+
+ MediaDecoderOwner::NextFrameStatus NextFrameBufferedStatus() override;
+
+ bool IsMSE() const override { return true; }
+
+ void NotifyInitDataArrived();
+
+ // Called as data appended to the source buffer or EOS is called on the media
+ // source. Main thread only.
+ void NotifyDataArrived();
+
+ private:
+ MediaDecoderStateMachineBase* CreateStateMachine(
+ bool aDisableExternalEngine) override;
+
+ template <typename IntervalType>
+ IntervalType GetSeekableImpl();
+
+ void DoSetMediaSourceDuration(double aDuration);
+ media::TimeInterval ClampIntervalToEnd(const media::TimeInterval& aInterval);
+ bool CanPlayThroughImpl() override;
+
+ RefPtr<nsIPrincipal> mPrincipal;
+
+ // The owning MediaSource holds a strong reference to this decoder, and
+ // calls Attach/DetachMediaSource on this decoder to set and clear
+ // mMediaSource.
+ dom::MediaSource* mMediaSource;
+ RefPtr<MediaSourceDemuxer> mDemuxer;
+
+ bool mEnded;
+};
+
+} // namespace mozilla
+
+#endif /* MOZILLA_MEDIASOURCEDECODER_H_ */
diff --git a/dom/media/mediasource/MediaSourceDemuxer.cpp b/dom/media/mediasource/MediaSourceDemuxer.cpp
new file mode 100644
index 0000000000..9d08eda4fa
--- /dev/null
+++ b/dom/media/mediasource/MediaSourceDemuxer.cpp
@@ -0,0 +1,530 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaSourceDemuxer.h"
+
+#include "MediaSourceUtils.h"
+#include "OpusDecoder.h"
+#include "SourceBufferList.h"
+#include "VorbisDecoder.h"
+#include "VideoUtils.h"
+#include "nsPrintfCString.h"
+
+#include <algorithm>
+#include <limits>
+#include <stdint.h>
+
+namespace mozilla {
+
+typedef TrackInfo::TrackType TrackType;
+using media::TimeIntervals;
+using media::TimeUnit;
+
+MediaSourceDemuxer::MediaSourceDemuxer(AbstractThread* aAbstractMainThread)
+ : mTaskQueue(
+ TaskQueue::Create(GetMediaThreadPool(MediaThreadType::SUPERVISOR),
+ "MediaSourceDemuxer::mTaskQueue")),
+ mMonitor("MediaSourceDemuxer") {
+ MOZ_ASSERT(NS_IsMainThread());
+}
+
+constexpr TimeUnit MediaSourceDemuxer::EOS_FUZZ;
+constexpr TimeUnit MediaSourceDemuxer::EOS_FUZZ_START;
+
+RefPtr<MediaSourceDemuxer::InitPromise> MediaSourceDemuxer::Init() {
+ RefPtr<MediaSourceDemuxer> self = this;
+ return InvokeAsync(GetTaskQueue(), __func__, [self]() {
+ if (self->ScanSourceBuffersForContent()) {
+ return InitPromise::CreateAndResolve(NS_OK, __func__);
+ }
+
+ RefPtr<InitPromise> p = self->mInitPromise.Ensure(__func__);
+
+ return p;
+ });
+}
+
+void MediaSourceDemuxer::AddSizeOfResources(
+ MediaSourceDecoder::ResourceSizes* aSizes) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // NB: The track buffers must only be accessed on the TaskQueue.
+ RefPtr<MediaSourceDemuxer> self = this;
+ RefPtr<MediaSourceDecoder::ResourceSizes> sizes = aSizes;
+ nsCOMPtr<nsIRunnable> task = NS_NewRunnableFunction(
+ "MediaSourceDemuxer::AddSizeOfResources", [self, sizes]() {
+ for (const RefPtr<TrackBuffersManager>& manager :
+ self->mSourceBuffers) {
+ manager->AddSizeOfResources(sizes);
+ }
+ });
+
+ nsresult rv = GetTaskQueue()->Dispatch(task.forget());
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+}
+
+void MediaSourceDemuxer::NotifyInitDataArrived() {
+ RefPtr<MediaSourceDemuxer> self = this;
+ nsCOMPtr<nsIRunnable> task = NS_NewRunnableFunction(
+ "MediaSourceDemuxer::NotifyInitDataArrived", [self]() {
+ if (self->mInitPromise.IsEmpty()) {
+ return;
+ }
+ if (self->ScanSourceBuffersForContent()) {
+ self->mInitPromise.ResolveIfExists(NS_OK, __func__);
+ }
+ });
+ nsresult rv = GetTaskQueue()->Dispatch(task.forget());
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+}
+
+bool MediaSourceDemuxer::ScanSourceBuffersForContent() {
+ MOZ_ASSERT(OnTaskQueue());
+
+ if (mSourceBuffers.IsEmpty()) {
+ return false;
+ }
+
+ MonitorAutoLock mon(mMonitor);
+
+ bool haveEmptySourceBuffer = false;
+ for (const auto& sourceBuffer : mSourceBuffers) {
+ MediaInfo info = sourceBuffer->GetMetadata();
+ if (!info.HasAudio() && !info.HasVideo()) {
+ haveEmptySourceBuffer = true;
+ }
+ if (info.HasAudio() && !mAudioTrack) {
+ mInfo.mAudio = info.mAudio;
+ mAudioTrack = sourceBuffer;
+ }
+ if (info.HasVideo() && !mVideoTrack) {
+ mInfo.mVideo = info.mVideo;
+ mVideoTrack = sourceBuffer;
+ }
+ if (info.IsEncrypted() && !mInfo.IsEncrypted()) {
+ mInfo.mCrypto = info.mCrypto;
+ }
+ }
+ if (mInfo.HasAudio() && mInfo.HasVideo()) {
+ // We have both audio and video. We can ignore non-ready source buffer.
+ return true;
+ }
+ return !haveEmptySourceBuffer;
+}
+
+uint32_t MediaSourceDemuxer::GetNumberTracks(TrackType aType) const {
+ MonitorAutoLock mon(mMonitor);
+
+ switch (aType) {
+ case TrackType::kAudioTrack:
+ return mInfo.HasAudio() ? 1u : 0;
+ case TrackType::kVideoTrack:
+ return mInfo.HasVideo() ? 1u : 0;
+ default:
+ return 0;
+ }
+}
+
+already_AddRefed<MediaTrackDemuxer> MediaSourceDemuxer::GetTrackDemuxer(
+ TrackType aType, uint32_t aTrackNumber) {
+ MonitorAutoLock mon(mMonitor);
+ RefPtr<TrackBuffersManager> manager = GetManager(aType);
+ if (!manager) {
+ return nullptr;
+ }
+ RefPtr<MediaSourceTrackDemuxer> e =
+ new MediaSourceTrackDemuxer(this, aType, manager);
+ DDLINKCHILD("track demuxer", e.get());
+ mDemuxers.AppendElement(e);
+ return e.forget();
+}
+
+bool MediaSourceDemuxer::IsSeekable() const { return true; }
+
+UniquePtr<EncryptionInfo> MediaSourceDemuxer::GetCrypto() {
+ MonitorAutoLock mon(mMonitor);
+ auto crypto = MakeUnique<EncryptionInfo>();
+ *crypto = mInfo.mCrypto;
+ return crypto;
+}
+
+void MediaSourceDemuxer::AttachSourceBuffer(
+ RefPtr<TrackBuffersManager>& aSourceBuffer) {
+ nsCOMPtr<nsIRunnable> task = NewRunnableMethod<RefPtr<TrackBuffersManager>&&>(
+ "MediaSourceDemuxer::DoAttachSourceBuffer", this,
+ &MediaSourceDemuxer::DoAttachSourceBuffer, aSourceBuffer);
+ nsresult rv = GetTaskQueue()->Dispatch(task.forget());
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+}
+
+void MediaSourceDemuxer::DoAttachSourceBuffer(
+ RefPtr<mozilla::TrackBuffersManager>&& aSourceBuffer) {
+ MOZ_ASSERT(OnTaskQueue());
+ mSourceBuffers.AppendElement(std::move(aSourceBuffer));
+ ScanSourceBuffersForContent();
+}
+
+void MediaSourceDemuxer::DetachSourceBuffer(
+ RefPtr<TrackBuffersManager>& aSourceBuffer) {
+ nsCOMPtr<nsIRunnable> task =
+ NS_NewRunnableFunction("MediaSourceDemuxer::DoDetachSourceBuffer",
+ [self = RefPtr{this}, aSourceBuffer]() {
+ self->DoDetachSourceBuffer(aSourceBuffer);
+ });
+ nsresult rv = GetTaskQueue()->Dispatch(task.forget());
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+}
+
+void MediaSourceDemuxer::DoDetachSourceBuffer(
+ const RefPtr<TrackBuffersManager>& aSourceBuffer) {
+ MOZ_ASSERT(OnTaskQueue());
+ mSourceBuffers.RemoveElementsBy(
+ [&aSourceBuffer](const RefPtr<TrackBuffersManager> aLinkedSourceBuffer) {
+ return aLinkedSourceBuffer == aSourceBuffer;
+ });
+
+ AutoTArray<RefPtr<MediaSourceTrackDemuxer>, 2> matchingDemuxers;
+ {
+ MonitorAutoLock mon(mMonitor);
+ if (aSourceBuffer == mAudioTrack) {
+ mAudioTrack = nullptr;
+ }
+ if (aSourceBuffer == mVideoTrack) {
+ mVideoTrack = nullptr;
+ }
+
+ mDemuxers.RemoveElementsBy(
+ [&](RefPtr<MediaSourceTrackDemuxer>& elementRef) {
+ if (!elementRef->HasManager(aSourceBuffer)) {
+ return false;
+ }
+ matchingDemuxers.AppendElement(std::move(elementRef));
+ return true;
+ });
+ }
+
+ for (MediaSourceTrackDemuxer* demuxer : matchingDemuxers) {
+ demuxer->DetachManager();
+ }
+ ScanSourceBuffersForContent();
+}
+
+TrackInfo* MediaSourceDemuxer::GetTrackInfo(TrackType aTrack) {
+ switch (aTrack) {
+ case TrackType::kAudioTrack:
+ return &mInfo.mAudio;
+ case TrackType::kVideoTrack:
+ return &mInfo.mVideo;
+ default:
+ return nullptr;
+ }
+}
+
+RefPtr<TrackBuffersManager> MediaSourceDemuxer::GetManager(TrackType aTrack) {
+ switch (aTrack) {
+ case TrackType::kAudioTrack:
+ return mAudioTrack;
+ case TrackType::kVideoTrack:
+ return mVideoTrack;
+ default:
+ return nullptr;
+ }
+}
+
+MediaSourceDemuxer::~MediaSourceDemuxer() {
+ mInitPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+}
+
+RefPtr<GenericPromise> MediaSourceDemuxer::GetDebugInfo(
+ dom::MediaSourceDemuxerDebugInfo& aInfo) const {
+ MonitorAutoLock mon(mMonitor);
+ nsTArray<RefPtr<GenericPromise>> promises;
+ if (mAudioTrack) {
+ promises.AppendElement(mAudioTrack->RequestDebugInfo(aInfo.mAudioTrack));
+ }
+ if (mVideoTrack) {
+ promises.AppendElement(mVideoTrack->RequestDebugInfo(aInfo.mVideoTrack));
+ }
+ return GenericPromise::All(GetCurrentSerialEventTarget(), promises)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ []() { return GenericPromise::CreateAndResolve(true, __func__); },
+ [] {
+ return GenericPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ });
+}
+
+MediaSourceTrackDemuxer::MediaSourceTrackDemuxer(MediaSourceDemuxer* aParent,
+ TrackInfo::TrackType aType,
+ TrackBuffersManager* aManager)
+ : mParent(aParent),
+ mTaskQueue(mParent->GetTaskQueue()),
+ mType(aType),
+ mMonitor("MediaSourceTrackDemuxer"),
+ mManager(aManager),
+ mReset(true),
+ mPreRoll(TimeUnit::FromMicroseconds(
+ OpusDataDecoder::IsOpus(mParent->GetTrackInfo(mType)->mMimeType) ||
+ VorbisDataDecoder::IsVorbis(
+ mParent->GetTrackInfo(mType)->mMimeType)
+ ? 80000
+ : mParent->GetTrackInfo(mType)->mMimeType.EqualsLiteral(
+ "audio/mp4a-latm")
+ // AAC encoder delay is by default 2112 audio frames.
+ // See
+ // https://developer.apple.com/library/content/documentation/QuickTime/QTFF/QTFFAppenG/QTFFAppenG.html
+ // So we always seek 2112 frames
+ ? (2112 * 1000000ULL /
+ mParent->GetTrackInfo(mType)->GetAsAudioInfo()->mRate)
+ : 0)) {
+ MOZ_ASSERT(mParent);
+ MOZ_ASSERT(mTaskQueue);
+}
+
+UniquePtr<TrackInfo> MediaSourceTrackDemuxer::GetInfo() const {
+ MonitorAutoLock mon(mParent->mMonitor);
+ return mParent->GetTrackInfo(mType)->Clone();
+}
+
+RefPtr<MediaSourceTrackDemuxer::SeekPromise> MediaSourceTrackDemuxer::Seek(
+ const TimeUnit& aTime) {
+ MOZ_ASSERT(mParent, "Called after BreackCycle()");
+ return InvokeAsync(mParent->GetTaskQueue(), this, __func__,
+ &MediaSourceTrackDemuxer::DoSeek, aTime);
+}
+
+RefPtr<MediaSourceTrackDemuxer::SamplesPromise>
+MediaSourceTrackDemuxer::GetSamples(int32_t aNumSamples) {
+ MOZ_ASSERT(mParent, "Called after BreackCycle()");
+ return InvokeAsync(mParent->GetTaskQueue(), this, __func__,
+ &MediaSourceTrackDemuxer::DoGetSamples, aNumSamples);
+}
+
+void MediaSourceTrackDemuxer::Reset() {
+ MOZ_ASSERT(mParent, "Called after BreackCycle()");
+ RefPtr<MediaSourceTrackDemuxer> self = this;
+ nsCOMPtr<nsIRunnable> task =
+ NS_NewRunnableFunction("MediaSourceTrackDemuxer::Reset", [self]() {
+ self->mNextSample.reset();
+ self->mReset = true;
+ if (!self->mManager) {
+ return;
+ }
+ MOZ_ASSERT(self->OnTaskQueue());
+ self->mManager->Seek(self->mType, TimeUnit::Zero(), TimeUnit::Zero());
+ {
+ MonitorAutoLock mon(self->mMonitor);
+ self->mNextRandomAccessPoint =
+ self->mManager->GetNextRandomAccessPoint(
+ self->mType, MediaSourceDemuxer::EOS_FUZZ);
+ }
+ });
+ nsresult rv = mParent->GetTaskQueue()->Dispatch(task.forget());
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+}
+
+nsresult MediaSourceTrackDemuxer::GetNextRandomAccessPoint(TimeUnit* aTime) {
+ MonitorAutoLock mon(mMonitor);
+ *aTime = mNextRandomAccessPoint;
+ return NS_OK;
+}
+
+RefPtr<MediaSourceTrackDemuxer::SkipAccessPointPromise>
+MediaSourceTrackDemuxer::SkipToNextRandomAccessPoint(
+ const TimeUnit& aTimeThreshold) {
+ return InvokeAsync(mParent->GetTaskQueue(), this, __func__,
+ &MediaSourceTrackDemuxer::DoSkipToNextRandomAccessPoint,
+ aTimeThreshold);
+}
+
+media::TimeIntervals MediaSourceTrackDemuxer::GetBuffered() {
+ MonitorAutoLock mon(mMonitor);
+ if (!mManager) {
+ return media::TimeIntervals();
+ }
+ return mManager->Buffered();
+}
+
+void MediaSourceTrackDemuxer::BreakCycles() {
+ RefPtr<MediaSourceTrackDemuxer> self = this;
+ nsCOMPtr<nsIRunnable> task =
+ NS_NewRunnableFunction("MediaSourceTrackDemuxer::BreakCycles", [self]() {
+ self->DetachManager();
+ self->mParent = nullptr;
+ });
+ nsresult rv = mParent->GetTaskQueue()->Dispatch(task.forget());
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+}
+
+RefPtr<MediaSourceTrackDemuxer::SeekPromise> MediaSourceTrackDemuxer::DoSeek(
+ const TimeUnit& aTime) {
+ if (!mManager) {
+ return SeekPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_CANCELED,
+ RESULT_DETAIL("manager is detached.")),
+ __func__);
+ }
+
+ MOZ_ASSERT(OnTaskQueue());
+ TimeIntervals buffered = mManager->Buffered(mType);
+ // Fuzz factor represents a +/- threshold. So when seeking it allows the gap
+ // to be twice as big as the fuzz value. We only want to allow EOS_FUZZ gap.
+ buffered.SetFuzz(MediaSourceDemuxer::EOS_FUZZ / 2);
+ TimeUnit seekTime = std::max(aTime - mPreRoll, TimeUnit::Zero());
+
+ if (mManager->IsEnded() && seekTime >= buffered.GetEnd()) {
+ // We're attempting to seek past the end time. Cap seekTime so that we seek
+ // to the last sample instead.
+ seekTime = std::max(mManager->HighestStartTime(mType) - mPreRoll,
+ TimeUnit::Zero());
+ }
+ if (!buffered.ContainsWithStrictEnd(seekTime)) {
+ if (!buffered.ContainsWithStrictEnd(aTime)) {
+ // We don't have the data to seek to.
+ return SeekPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA,
+ __func__);
+ }
+ // Theoretically we should reject the promise with WAITING_FOR_DATA,
+ // however, to avoid unwanted regressions we assume that if at this time
+ // we don't have the wanted data it won't come later.
+ // Instead of using the pre-rolled time, use the earliest time available in
+ // the interval.
+ TimeIntervals::IndexType index = buffered.Find(aTime);
+ MOZ_ASSERT(index != TimeIntervals::NoIndex);
+ seekTime = buffered[index].mStart;
+ }
+ seekTime = mManager->Seek(mType, seekTime, MediaSourceDemuxer::EOS_FUZZ);
+ MediaResult result = NS_OK;
+ RefPtr<MediaRawData> sample =
+ mManager->GetSample(mType, TimeUnit::Zero(), result);
+ MOZ_ASSERT(NS_SUCCEEDED(result) && sample);
+ mNextSample = Some(sample);
+ mReset = false;
+ {
+ MonitorAutoLock mon(mMonitor);
+ mNextRandomAccessPoint =
+ mManager->GetNextRandomAccessPoint(mType, MediaSourceDemuxer::EOS_FUZZ);
+ }
+ return SeekPromise::CreateAndResolve(seekTime, __func__);
+}
+
+RefPtr<MediaSourceTrackDemuxer::SamplesPromise>
+MediaSourceTrackDemuxer::DoGetSamples(int32_t aNumSamples) {
+ if (!mManager) {
+ return SamplesPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_CANCELED,
+ RESULT_DETAIL("manager is detached.")),
+ __func__);
+ }
+
+ MOZ_ASSERT(OnTaskQueue());
+ if (mReset) {
+ // If a reset was recently performed, we ensure that the data
+ // we are about to retrieve is still available.
+ TimeIntervals buffered = mManager->Buffered(mType);
+ if (buffered.IsEmpty() && mManager->IsEnded()) {
+ return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_END_OF_STREAM,
+ __func__);
+ }
+
+ // We use a larger fuzz to determine the presentation start
+ // time than the fuzz we use to determine acceptable gaps between
+ // frames. This is needed to fix embedded video issues as seen in the wild
+ // from different muxed stream start times.
+ // See: https://www.w3.org/TR/media-source-2/#presentation-start-time
+ buffered.SetFuzz(MediaSourceDemuxer::EOS_FUZZ_START);
+ if (!buffered.ContainsWithStrictEnd(TimeUnit::Zero())) {
+ return SamplesPromise::CreateAndReject(
+ NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA, __func__);
+ }
+ mReset = false;
+ }
+ RefPtr<MediaRawData> sample;
+ MediaResult result = NS_OK;
+ if (mNextSample) {
+ sample = mNextSample.ref();
+ mNextSample.reset();
+ } else {
+ sample = mManager->GetSample(mType, MediaSourceDemuxer::EOS_FUZZ, result);
+ }
+ if (!sample) {
+ if (result == NS_ERROR_DOM_MEDIA_END_OF_STREAM ||
+ result == NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA) {
+ return SamplesPromise::CreateAndReject(
+ (result == NS_ERROR_DOM_MEDIA_END_OF_STREAM && mManager->IsEnded())
+ ? NS_ERROR_DOM_MEDIA_END_OF_STREAM
+ : NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA,
+ __func__);
+ }
+ return SamplesPromise::CreateAndReject(result, __func__);
+ }
+ RefPtr<SamplesHolder> samples = new SamplesHolder;
+ samples->AppendSample(sample);
+ {
+ MonitorAutoLock mon(mMonitor); // spurious warning will be given
+ // Diagnostic asserts for bug 1810396
+ MOZ_DIAGNOSTIC_ASSERT(sample, "Invalid sample pointer found!");
+ MOZ_DIAGNOSTIC_ASSERT(sample->HasValidTime(), "Invalid sample time found!");
+ if (!sample) {
+ return SamplesPromise::CreateAndReject(NS_ERROR_NULL_POINTER, __func__);
+ }
+ if (mNextRandomAccessPoint <= sample->mTime) {
+ mNextRandomAccessPoint = mManager->GetNextRandomAccessPoint(
+ mType, MediaSourceDemuxer::EOS_FUZZ);
+ }
+ }
+ return SamplesPromise::CreateAndResolve(samples, __func__);
+}
+
+RefPtr<MediaSourceTrackDemuxer::SkipAccessPointPromise>
+MediaSourceTrackDemuxer::DoSkipToNextRandomAccessPoint(
+ const TimeUnit& aTimeThreadshold) {
+ if (!mManager) {
+ return SkipAccessPointPromise::CreateAndReject(
+ SkipFailureHolder(MediaResult(NS_ERROR_DOM_MEDIA_CANCELED,
+ RESULT_DETAIL("manager is detached.")),
+ 0),
+ __func__);
+ }
+
+ MOZ_ASSERT(OnTaskQueue());
+ uint32_t parsed = 0;
+ // Ensure that the data we are about to skip to is still available.
+ TimeIntervals buffered = mManager->Buffered(mType);
+ buffered.SetFuzz(MediaSourceDemuxer::EOS_FUZZ / 2);
+ if (buffered.ContainsWithStrictEnd(aTimeThreadshold)) {
+ bool found;
+ parsed = mManager->SkipToNextRandomAccessPoint(
+ mType, aTimeThreadshold, MediaSourceDemuxer::EOS_FUZZ, found);
+ if (found) {
+ return SkipAccessPointPromise::CreateAndResolve(parsed, __func__);
+ }
+ }
+ SkipFailureHolder holder(mManager->IsEnded()
+ ? NS_ERROR_DOM_MEDIA_END_OF_STREAM
+ : NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA,
+ parsed);
+ return SkipAccessPointPromise::CreateAndReject(holder, __func__);
+}
+
+bool MediaSourceTrackDemuxer::HasManager(TrackBuffersManager* aManager) const {
+ MOZ_ASSERT(OnTaskQueue());
+ return mManager == aManager;
+}
+
+void MediaSourceTrackDemuxer::DetachManager() {
+ MOZ_ASSERT(OnTaskQueue());
+ MonitorAutoLock mon(mMonitor);
+ mManager = nullptr;
+}
+
+} // namespace mozilla
diff --git a/dom/media/mediasource/MediaSourceDemuxer.h b/dom/media/mediasource/MediaSourceDemuxer.h
new file mode 100644
index 0000000000..215b0210e2
--- /dev/null
+++ b/dom/media/mediasource/MediaSourceDemuxer.h
@@ -0,0 +1,172 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(MediaSourceDemuxer_h_)
+# define MediaSourceDemuxer_h_
+
+# include "MediaDataDemuxer.h"
+# include "MediaResource.h"
+# include "MediaSource.h"
+# include "TrackBuffersManager.h"
+# include "mozilla/Atomics.h"
+# include "mozilla/Maybe.h"
+# include "mozilla/Monitor.h"
+# include "mozilla/TaskQueue.h"
+# include "mozilla/dom/MediaDebugInfoBinding.h"
+
+namespace mozilla {
+
+class AbstractThread;
+class MediaResult;
+class MediaSourceTrackDemuxer;
+
+DDLoggedTypeDeclNameAndBase(MediaSourceDemuxer, MediaDataDemuxer);
+DDLoggedTypeNameAndBase(MediaSourceTrackDemuxer, MediaTrackDemuxer);
+
+class MediaSourceDemuxer : public MediaDataDemuxer,
+ public DecoderDoctorLifeLogger<MediaSourceDemuxer> {
+ public:
+ explicit MediaSourceDemuxer(AbstractThread* aAbstractMainThread);
+
+ RefPtr<InitPromise> Init() override;
+
+ uint32_t GetNumberTracks(TrackInfo::TrackType aType) const override;
+
+ already_AddRefed<MediaTrackDemuxer> GetTrackDemuxer(
+ TrackInfo::TrackType aType, uint32_t aTrackNumber) override;
+
+ bool IsSeekable() const override;
+
+ UniquePtr<EncryptionInfo> GetCrypto() override;
+
+ bool ShouldComputeStartTime() const override { return false; }
+
+ /* interface for TrackBuffersManager */
+ void AttachSourceBuffer(RefPtr<TrackBuffersManager>& aSourceBuffer);
+ void DetachSourceBuffer(RefPtr<TrackBuffersManager>& aSourceBuffer);
+ TaskQueue* GetTaskQueue() { return mTaskQueue; }
+ void NotifyInitDataArrived();
+
+ // Populates aInfo with info describing the state of the MediaSource internal
+ // buffered data. Used for debugging purposes.
+ // aInfo should *not* be accessed until the returned promise has been resolved
+ // or rejected.
+ RefPtr<GenericPromise> GetDebugInfo(
+ dom::MediaSourceDemuxerDebugInfo& aInfo) const;
+
+ void AddSizeOfResources(MediaSourceDecoder::ResourceSizes* aSizes);
+
+ // Gap allowed between frames.
+ // Due to inaccuracies in determining buffer end
+ // frames (Bug 1065207). This value is based on videos seen in the wild.
+ static constexpr media::TimeUnit EOS_FUZZ =
+ media::TimeUnit::FromMicroseconds(500000);
+
+ // Largest gap allowed between muxed streams with different
+ // start times. The specs suggest up to a "reasonably short" gap of
+ // one second. We conservatively choose to allow a gap up to a bit over
+ // a half-second here, which is still twice our previous effective value
+ // and should resolve embedded playback issues on Twitter, DokiDoki, etc.
+ // See: https://www.w3.org/TR/media-source-2/#presentation-start-time
+ static constexpr media::TimeUnit EOS_FUZZ_START =
+ media::TimeUnit::FromMicroseconds(550000);
+
+ private:
+ ~MediaSourceDemuxer();
+ friend class MediaSourceTrackDemuxer;
+ // Scan source buffers and update information.
+ bool ScanSourceBuffersForContent();
+ RefPtr<TrackBuffersManager> GetManager(TrackInfo::TrackType aType)
+ MOZ_REQUIRES(mMonitor);
+ TrackInfo* GetTrackInfo(TrackInfo::TrackType) MOZ_REQUIRES(mMonitor);
+ void DoAttachSourceBuffer(RefPtr<TrackBuffersManager>&& aSourceBuffer);
+ void DoDetachSourceBuffer(const RefPtr<TrackBuffersManager>& aSourceBuffer);
+ bool OnTaskQueue() {
+ return !GetTaskQueue() || GetTaskQueue()->IsCurrentThreadIn();
+ }
+
+ RefPtr<TaskQueue> mTaskQueue;
+ // Accessed on mTaskQueue or from destructor
+ nsTArray<RefPtr<TrackBuffersManager>> mSourceBuffers;
+ MozPromiseHolder<InitPromise> mInitPromise;
+
+ // Monitor to protect members below across multiple threads.
+ mutable Monitor mMonitor;
+ nsTArray<RefPtr<MediaSourceTrackDemuxer>> mDemuxers MOZ_GUARDED_BY(mMonitor);
+ RefPtr<TrackBuffersManager> mAudioTrack MOZ_GUARDED_BY(mMonitor);
+ RefPtr<TrackBuffersManager> mVideoTrack MOZ_GUARDED_BY(mMonitor);
+ MediaInfo mInfo MOZ_GUARDED_BY(mMonitor);
+};
+
+class MediaSourceTrackDemuxer
+ : public MediaTrackDemuxer,
+ public DecoderDoctorLifeLogger<MediaSourceTrackDemuxer> {
+ public:
+ MediaSourceTrackDemuxer(MediaSourceDemuxer* aParent,
+ TrackInfo::TrackType aType,
+ TrackBuffersManager* aManager)
+ MOZ_REQUIRES(aParent->mMonitor);
+
+ UniquePtr<TrackInfo> GetInfo() const override;
+
+ RefPtr<SeekPromise> Seek(const media::TimeUnit& aTime) override;
+
+ RefPtr<SamplesPromise> GetSamples(int32_t aNumSamples = 1) override;
+
+ void Reset() override;
+
+ nsresult GetNextRandomAccessPoint(media::TimeUnit* aTime) override;
+
+ RefPtr<SkipAccessPointPromise> SkipToNextRandomAccessPoint(
+ const media::TimeUnit& aTimeThreshold) override;
+
+ media::TimeIntervals GetBuffered() override;
+
+ void BreakCycles() override;
+
+ bool GetSamplesMayBlock() const override { return false; }
+
+ bool HasManager(TrackBuffersManager* aManager) const;
+ void DetachManager();
+
+ private:
+ bool OnTaskQueue() const { return mTaskQueue->IsCurrentThreadIn(); }
+
+ RefPtr<SeekPromise> DoSeek(const media::TimeUnit& aTime);
+ RefPtr<SamplesPromise> DoGetSamples(int32_t aNumSamples);
+ RefPtr<SkipAccessPointPromise> DoSkipToNextRandomAccessPoint(
+ const media::TimeUnit& aTimeThreadshold);
+ already_AddRefed<MediaRawData> GetSample(MediaResult& aError);
+ // Return the timestamp of the next keyframe after mLastSampleIndex.
+ media::TimeUnit GetNextRandomAccessPoint();
+
+ RefPtr<MediaSourceDemuxer> mParent;
+ const RefPtr<TaskQueue> mTaskQueue;
+
+ TrackInfo::TrackType mType;
+ // Monitor protecting members below accessed from multiple threads.
+ Monitor mMonitor MOZ_UNANNOTATED;
+ media::TimeUnit mNextRandomAccessPoint;
+ // Would be accessed in MFR's demuxer proxy task queue and TaskQueue, and
+ // only be set on the TaskQueue. It can be accessed while on TaskQueue without
+ // the need for the lock.
+ RefPtr<TrackBuffersManager> mManager;
+
+ // Only accessed on TaskQueue
+ Maybe<RefPtr<MediaRawData>> mNextSample;
+ // Set to true following a reset. Ensure that the next sample demuxed
+ // is available at position 0.
+ // Only accessed on TaskQueue
+ bool mReset;
+
+ // Amount of pre-roll time when seeking.
+ // Set to 80ms if track is Opus.
+ const media::TimeUnit mPreRoll;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/mediasource/MediaSourceUtils.cpp b/dom/media/mediasource/MediaSourceUtils.cpp
new file mode 100644
index 0000000000..37e94d7dc7
--- /dev/null
+++ b/dom/media/mediasource/MediaSourceUtils.cpp
@@ -0,0 +1,49 @@
+/* -*- mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "MediaSourceUtils.h"
+
+#include "mozilla/Logging.h"
+#include "nsPrintfCString.h"
+
+namespace mozilla {
+
+nsCString DumpTimeRanges(const media::TimeIntervals& aRanges) {
+ nsCString dump;
+
+ dump = "[";
+
+ for (uint32_t i = 0; i < aRanges.Length(); ++i) {
+ if (i > 0) {
+ dump += ", ";
+ }
+ dump += nsPrintfCString("(%f, %f)", aRanges.Start(i).ToSeconds(),
+ aRanges.End(i).ToSeconds());
+ }
+
+ dump += "]";
+
+ return dump;
+}
+
+nsCString DumpTimeRangesRaw(const media::TimeIntervals& aRanges) {
+ nsCString dump;
+
+ dump = "[";
+
+ for (uint32_t i = 0; i < aRanges.Length(); ++i) {
+ if (i > 0) {
+ dump += ", ";
+ }
+ dump += nsPrintfCString("(%s, %s)", aRanges.Start(i).ToString().get(),
+ aRanges.End(i).ToString().get());
+ }
+
+ dump += "]";
+
+ return dump;
+}
+
+} // namespace mozilla
diff --git a/dom/media/mediasource/MediaSourceUtils.h b/dom/media/mediasource/MediaSourceUtils.h
new file mode 100644
index 0000000000..18c2d387df
--- /dev/null
+++ b/dom/media/mediasource/MediaSourceUtils.h
@@ -0,0 +1,20 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_MEDIASOURCEUTILS_H_
+#define MOZILLA_MEDIASOURCEUTILS_H_
+
+#include "nsString.h"
+#include "TimeUnits.h"
+
+namespace mozilla {
+
+nsCString DumpTimeRanges(const media::TimeIntervals& aRanges);
+nsCString DumpTimeRangesRaw(const media::TimeIntervals& aRanges);
+
+} // namespace mozilla
+
+#endif /* MOZILLA_MEDIASOURCEUTILS_H_ */
diff --git a/dom/media/mediasource/ResourceQueue.cpp b/dom/media/mediasource/ResourceQueue.cpp
new file mode 100644
index 0000000000..717638b5c8
--- /dev/null
+++ b/dom/media/mediasource/ResourceQueue.cpp
@@ -0,0 +1,204 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ResourceQueue.h"
+#include "MediaData.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/IntegerPrintfMacros.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Sprintf.h"
+#include "mozilla/Unused.h"
+
+extern mozilla::LogModule* GetSourceBufferResourceLog();
+
+#define SBR_DEBUG(arg, ...) \
+ MOZ_LOG(GetSourceBufferResourceLog(), mozilla::LogLevel::Debug, \
+ ("ResourceQueue(%p)::%s: " arg, this, __func__, ##__VA_ARGS__))
+#define SBR_DEBUGV(arg, ...) \
+ MOZ_LOG(GetSourceBufferResourceLog(), mozilla::LogLevel::Verbose, \
+ ("ResourceQueue(%p)::%s: " arg, this, __func__, ##__VA_ARGS__))
+
+namespace mozilla {
+
+ResourceItem::ResourceItem(const MediaSpan& aData, uint64_t aOffset)
+ : mData(aData), mOffset(aOffset) {}
+
+size_t ResourceItem::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this);
+}
+
+class ResourceQueueDeallocator : public nsDequeFunctor<ResourceItem> {
+ void operator()(ResourceItem* aObject) override { delete aObject; }
+};
+
+ResourceQueue::ResourceQueue()
+ : nsDeque<ResourceItem>(new ResourceQueueDeallocator()),
+ mLogicalLength(0),
+ mOffset(0) {}
+
+uint64_t ResourceQueue::GetOffset() { return mOffset; }
+
+uint64_t ResourceQueue::GetLength() { return mLogicalLength; }
+
+const uint8_t* ResourceQueue::GetContiguousAccess(int64_t aOffset,
+ size_t aSize) {
+ uint32_t offset = 0;
+ uint32_t start = GetAtOffset(aOffset, &offset);
+ if (start >= GetSize()) {
+ return nullptr;
+ }
+ ResourceItem* item = ResourceAt(start);
+ if (offset + aSize > item->mData.Length()) {
+ return nullptr;
+ }
+ return item->mData.Elements() + offset;
+}
+
+void ResourceQueue::CopyData(uint64_t aOffset, uint32_t aCount, char* aDest) {
+ uint32_t offset = 0;
+ uint32_t start = GetAtOffset(aOffset, &offset);
+ size_t i = start;
+ while (i < uint32_t(GetSize()) && aCount > 0) {
+ ResourceItem* item = ResourceAt(i++);
+ uint32_t bytes = std::min(aCount, uint32_t(item->mData.Length() - offset));
+ if (bytes != 0) {
+ memcpy(aDest, item->mData.Elements() + offset, bytes);
+ offset = 0;
+ aCount -= bytes;
+ aDest += bytes;
+ }
+ }
+}
+
+void ResourceQueue::AppendItem(const MediaSpan& aData) {
+ uint64_t offset = mLogicalLength;
+ mLogicalLength += aData.Length();
+ Push(new ResourceItem(aData, offset));
+}
+
+uint32_t ResourceQueue::Evict(uint64_t aOffset, uint32_t aSizeToEvict) {
+ SBR_DEBUG("Evict(aOffset=%" PRIu64 ", aSizeToEvict=%u)", aOffset,
+ aSizeToEvict);
+ return EvictBefore(std::min(aOffset, mOffset + (uint64_t)aSizeToEvict));
+}
+
+uint32_t ResourceQueue::EvictBefore(uint64_t aOffset) {
+ SBR_DEBUG("EvictBefore(%" PRIu64 ")", aOffset);
+ uint32_t evicted = 0;
+ while (GetSize()) {
+ ResourceItem* item = ResourceAt(0);
+ SBR_DEBUG("item=%p length=%zu offset=%" PRIu64, item, item->mData.Length(),
+ mOffset);
+ if (item->mData.Length() + mOffset >= aOffset) {
+ if (aOffset <= mOffset) {
+ break;
+ }
+ uint32_t offset = aOffset - mOffset;
+ mOffset += offset;
+ evicted += offset;
+ item->mData.RemoveFront(offset);
+ item->mOffset += offset;
+ break;
+ }
+ mOffset += item->mData.Length();
+ evicted += item->mData.Length();
+ delete PopFront();
+ }
+ return evicted;
+}
+
+uint32_t ResourceQueue::EvictAll() {
+ SBR_DEBUG("EvictAll()");
+ uint32_t evicted = 0;
+ while (GetSize()) {
+ ResourceItem* item = ResourceAt(0);
+ SBR_DEBUG("item=%p length=%zu offset=%" PRIu64, item, item->mData.Length(),
+ mOffset);
+ mOffset += item->mData.Length();
+ evicted += item->mData.Length();
+ delete PopFront();
+ }
+ return evicted;
+}
+
+size_t ResourceQueue::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ // Calculate the size of the internal deque.
+ size_t size = nsDeque<ResourceItem>::SizeOfExcludingThis(aMallocSizeOf);
+
+ // Sum the ResourceItems. The ResourceItems's MediaSpans may share the
+ // same underlying MediaByteBuffers, so we need to de-dupe the buffers
+ // in order to report an accurate size.
+ nsTArray<MediaByteBuffer*> buffers;
+ for (uint32_t i = 0; i < uint32_t(GetSize()); ++i) {
+ const ResourceItem* item = ResourceAt(i);
+ size += item->SizeOfIncludingThis(aMallocSizeOf);
+ if (!buffers.Contains(item->mData.Buffer())) {
+ buffers.AppendElement(item->mData.Buffer());
+ }
+ }
+
+ for (MediaByteBuffer* buffer : buffers) {
+ size += buffer->ShallowSizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ return size;
+}
+
+#if defined(DEBUG)
+void ResourceQueue::Dump(const char* aPath) {
+ for (uint32_t i = 0; i < uint32_t(GetSize()); ++i) {
+ ResourceItem* item = ResourceAt(i);
+
+ char buf[255];
+ SprintfLiteral(buf, "%s/%08u.bin", aPath, i);
+ FILE* fp = fopen(buf, "wb");
+ if (!fp) {
+ return;
+ }
+ Unused << fwrite(item->mData.Elements(), item->mData.Length(), 1, fp);
+ fclose(fp);
+ }
+}
+#endif
+
+ResourceItem* ResourceQueue::ResourceAt(uint32_t aIndex) const {
+ return static_cast<ResourceItem*>(ObjectAt(aIndex));
+}
+
+uint32_t ResourceQueue::GetAtOffset(uint64_t aOffset,
+ uint32_t* aResourceOffset) const {
+ MOZ_RELEASE_ASSERT(aOffset >= mOffset);
+
+ size_t hi = GetSize();
+ size_t lo = 0;
+ while (lo < hi) {
+ size_t mid = lo + (hi - lo) / 2;
+ const ResourceItem* resource = ResourceAt(mid);
+ if (resource->mOffset <= aOffset &&
+ aOffset < resource->mOffset + resource->mData.Length()) {
+ if (aResourceOffset) {
+ *aResourceOffset = aOffset - resource->mOffset;
+ }
+ return uint32_t(mid);
+ }
+ if (resource->mOffset + resource->mData.Length() <= aOffset) {
+ lo = mid + 1;
+ } else {
+ hi = mid;
+ }
+ }
+
+ return uint32_t(GetSize());
+}
+
+ResourceItem* ResourceQueue::PopFront() {
+ return nsDeque<ResourceItem>::PopFront();
+}
+
+#undef SBR_DEBUG
+#undef SBR_DEBUGV
+
+} // namespace mozilla
diff --git a/dom/media/mediasource/ResourceQueue.h b/dom/media/mediasource/ResourceQueue.h
new file mode 100644
index 0000000000..efd1e0c20a
--- /dev/null
+++ b/dom/media/mediasource/ResourceQueue.h
@@ -0,0 +1,88 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_RESOURCEQUEUE_H_
+#define MOZILLA_RESOURCEQUEUE_H_
+
+#include "nsDeque.h"
+#include "MediaSpan.h"
+
+namespace mozilla {
+
+class ErrorResult;
+
+// A SourceBufferResource has a queue containing the data that is appended
+// to it. The queue holds instances of ResourceItem which is an array of the
+// bytes. Appending data to the SourceBufferResource pushes this onto the
+// queue.
+
+// Data is evicted once it reaches a size threshold. This pops the items off
+// the front of the queue and deletes it. If an eviction happens then the
+// MediaSource is notified (done in SourceBuffer::AppendData) which then
+// requests all SourceBuffers to evict data up to approximately the same
+// timepoint.
+
+struct ResourceItem {
+ ResourceItem(const MediaSpan& aData, uint64_t aOffset);
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const;
+ MediaSpan mData;
+ uint64_t mOffset;
+};
+
+class ResourceQueue : private nsDeque<ResourceItem> {
+ public:
+ ResourceQueue();
+
+ // Returns the logical byte offset of the start of the data.
+ uint64_t GetOffset();
+
+ // Returns the length of all items in the queue plus the offset.
+ // This is the logical length of the resource.
+ uint64_t GetLength();
+
+ // Copies aCount bytes from aOffset in the queue into aDest.
+ void CopyData(uint64_t aOffset, uint32_t aCount, char* aDest);
+
+ void AppendItem(const MediaSpan& aData);
+
+ // Tries to evict at least aSizeToEvict from the queue up until
+ // aOffset. Returns amount evicted.
+ uint32_t Evict(uint64_t aOffset, uint32_t aSizeToEvict);
+
+ uint32_t EvictBefore(uint64_t aOffset);
+
+ uint32_t EvictAll();
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const;
+
+#if defined(DEBUG)
+ void Dump(const char* aPath);
+#endif
+
+ const uint8_t* GetContiguousAccess(int64_t aOffset, size_t aSize);
+
+ private:
+ ResourceItem* ResourceAt(uint32_t aIndex) const;
+
+ // Returns the index of the resource that contains the given
+ // logical offset. aResourceOffset will contain the offset into
+ // the resource at the given index returned if it is not null. If
+ // no such resource exists, returns GetSize() and aOffset is
+ // untouched.
+ uint32_t GetAtOffset(uint64_t aOffset, uint32_t* aResourceOffset) const;
+
+ ResourceItem* PopFront();
+
+ // Logical length of the resource.
+ uint64_t mLogicalLength;
+
+ // Logical offset into the resource of the first element in the queue.
+ uint64_t mOffset;
+};
+
+} // namespace mozilla
+
+#endif /* MOZILLA_RESOURCEQUEUE_H_ */
diff --git a/dom/media/mediasource/SourceBuffer.cpp b/dom/media/mediasource/SourceBuffer.cpp
new file mode 100644
index 0000000000..391e5253c9
--- /dev/null
+++ b/dom/media/mediasource/SourceBuffer.cpp
@@ -0,0 +1,765 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SourceBuffer.h"
+
+#include "AsyncEventRunner.h"
+#include "MediaData.h"
+#include "MediaSourceDemuxer.h"
+#include "MediaSourceUtils.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/FloatingPoint.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/dom/MediaSourceBinding.h"
+#include "mozilla/dom/TimeRanges.h"
+#include "nsError.h"
+#include "nsIRunnable.h"
+#include "nsThreadUtils.h"
+#include "mozilla/Logging.h"
+#include <time.h>
+#include "TimeUnits.h"
+
+struct JSContext;
+class JSObject;
+
+extern mozilla::LogModule* GetMediaSourceLog();
+extern mozilla::LogModule* GetMediaSourceAPILog();
+
+#define MSE_DEBUG(arg, ...) \
+ DDMOZ_LOG(GetMediaSourceLog(), mozilla::LogLevel::Debug, "(%s)::%s: " arg, \
+ mType.OriginalString().Data(), __func__, ##__VA_ARGS__)
+#define MSE_DEBUGV(arg, ...) \
+ DDMOZ_LOG(GetMediaSourceLog(), mozilla::LogLevel::Verbose, "(%s)::%s: " arg, \
+ mType.OriginalString().Data(), __func__, ##__VA_ARGS__)
+#define MSE_API(arg, ...) \
+ DDMOZ_LOG(GetMediaSourceAPILog(), mozilla::LogLevel::Debug, \
+ "(%s)::%s: " arg, mType.OriginalString().Data(), __func__, \
+ ##__VA_ARGS__)
+
+namespace mozilla {
+
+using media::TimeUnit;
+typedef SourceBufferAttributes::AppendState AppendState;
+
+namespace dom {
+
+void SourceBuffer::SetMode(SourceBufferAppendMode aMode, ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_API("SetMode(aMode=%" PRIu32 ")", static_cast<uint32_t>(aMode));
+ if (!IsAttached() || mUpdating) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ if (mCurrentAttributes.mGenerateTimestamps &&
+ aMode == SourceBufferAppendMode::Segments) {
+ aRv.ThrowTypeError(
+ "Can't set mode to \"segments\" when the byte stream generates "
+ "timestamps");
+ return;
+ }
+ MOZ_ASSERT(mMediaSource->ReadyState() != MediaSourceReadyState::Closed);
+ if (mMediaSource->ReadyState() == MediaSourceReadyState::Ended) {
+ mMediaSource->SetReadyState(MediaSourceReadyState::Open);
+ }
+ if (mCurrentAttributes.GetAppendState() ==
+ AppendState::PARSING_MEDIA_SEGMENT) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ if (aMode == SourceBufferAppendMode::Sequence) {
+ // Will set GroupStartTimestamp to GroupEndTimestamp.
+ mCurrentAttributes.RestartGroupStartTimestamp();
+ }
+
+ mCurrentAttributes.SetAppendMode(aMode);
+}
+
+void SourceBuffer::SetTimestampOffset(double aTimestampOffset,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_API("SetTimestampOffset(aTimestampOffset=%f)", aTimestampOffset);
+ if (!IsAttached() || mUpdating) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ MOZ_ASSERT(mMediaSource->ReadyState() != MediaSourceReadyState::Closed);
+ if (mMediaSource->ReadyState() == MediaSourceReadyState::Ended) {
+ mMediaSource->SetReadyState(MediaSourceReadyState::Open);
+ }
+ if (mCurrentAttributes.GetAppendState() ==
+ AppendState::PARSING_MEDIA_SEGMENT) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ mCurrentAttributes.SetApparentTimestampOffset(aTimestampOffset);
+ if (mCurrentAttributes.GetAppendMode() == SourceBufferAppendMode::Sequence) {
+ mCurrentAttributes.SetGroupStartTimestamp(
+ mCurrentAttributes.GetTimestampOffset());
+ }
+}
+
+media::TimeIntervals SourceBuffer::GetBufferedIntervals() {
+ MOZ_ASSERT(mTrackBuffersManager);
+ return mTrackBuffersManager->Buffered();
+}
+
+TimeRanges* SourceBuffer::GetBuffered(ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // http://w3c.github.io/media-source/index.html#widl-SourceBuffer-buffered
+ // 1. If this object has been removed from the sourceBuffers attribute of the
+ // parent media source then throw an InvalidStateError exception and abort
+ // these steps.
+ if (!IsAttached()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+ bool rangeChanged = true;
+ media::TimeIntervals intersection = mTrackBuffersManager->Buffered();
+ MSE_DEBUGV("intersection=%s", DumpTimeRanges(intersection).get());
+ if (mBuffered) {
+ media::TimeIntervals currentValue(mBuffered->ToTimeIntervals());
+ rangeChanged = (intersection != currentValue);
+ MSE_DEBUGV("currentValue=%s", DumpTimeRanges(currentValue).get());
+ }
+ // 5. If intersection ranges does not contain the exact same range information
+ // as the current value of this attribute, then update the current value of
+ // this attribute to intersection ranges.
+ if (rangeChanged) {
+ mBuffered = new TimeRanges(ToSupports(this),
+ intersection.ToMicrosecondResolution());
+ }
+ // 6. Return the current value of this attribute.
+ return mBuffered;
+}
+
+media::TimeIntervals SourceBuffer::GetTimeIntervals() {
+ MOZ_ASSERT(mTrackBuffersManager);
+ return mTrackBuffersManager->Buffered();
+}
+
+void SourceBuffer::SetAppendWindowStart(double aAppendWindowStart,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_API("SetAppendWindowStart(aAppendWindowStart=%f)", aAppendWindowStart);
+ DDLOG(DDLogCategory::API, "SetAppendWindowStart", aAppendWindowStart);
+ if (!IsAttached() || mUpdating) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ if (aAppendWindowStart < 0 ||
+ aAppendWindowStart >= mCurrentAttributes.GetAppendWindowEnd()) {
+ aRv.ThrowTypeError("Invalid appendWindowStart value");
+ return;
+ }
+ mCurrentAttributes.SetAppendWindowStart(aAppendWindowStart);
+}
+
+void SourceBuffer::SetAppendWindowEnd(double aAppendWindowEnd,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_API("SetAppendWindowEnd(aAppendWindowEnd=%f)", aAppendWindowEnd);
+ DDLOG(DDLogCategory::API, "SetAppendWindowEnd", aAppendWindowEnd);
+ if (!IsAttached() || mUpdating) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ if (std::isnan(aAppendWindowEnd) ||
+ aAppendWindowEnd <= mCurrentAttributes.GetAppendWindowStart()) {
+ aRv.ThrowTypeError("Invalid appendWindowEnd value");
+ return;
+ }
+ mCurrentAttributes.SetAppendWindowEnd(aAppendWindowEnd);
+}
+
+void SourceBuffer::AppendBuffer(const ArrayBuffer& aData, ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_API("AppendBuffer(ArrayBuffer)");
+ aData.ComputeState();
+ DDLOG(DDLogCategory::API, "AppendBuffer", aData.Length());
+ AppendData(aData.Data(), aData.Length(), aRv);
+}
+
+void SourceBuffer::AppendBuffer(const ArrayBufferView& aData,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_API("AppendBuffer(ArrayBufferView)");
+ aData.ComputeState();
+ DDLOG(DDLogCategory::API, "AppendBuffer", aData.Length());
+ AppendData(aData.Data(), aData.Length(), aRv);
+}
+
+already_AddRefed<Promise> SourceBuffer::AppendBufferAsync(
+ const ArrayBuffer& aData, ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ MSE_API("AppendBufferAsync(ArrayBuffer)");
+ aData.ComputeState();
+ DDLOG(DDLogCategory::API, "AppendBufferAsync", aData.Length());
+
+ return AppendDataAsync(aData.Data(), aData.Length(), aRv);
+}
+
+already_AddRefed<Promise> SourceBuffer::AppendBufferAsync(
+ const ArrayBufferView& aData, ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ MSE_API("AppendBufferAsync(ArrayBufferView)");
+ aData.ComputeState();
+ DDLOG(DDLogCategory::API, "AppendBufferAsync", aData.Length());
+
+ return AppendDataAsync(aData.Data(), aData.Length(), aRv);
+}
+
+void SourceBuffer::Abort(ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_API("Abort()");
+ if (!IsAttached()) {
+ DDLOG(DDLogCategory::API, "Abort", NS_ERROR_DOM_INVALID_STATE_ERR);
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ if (mMediaSource->ReadyState() != MediaSourceReadyState::Open) {
+ DDLOG(DDLogCategory::API, "Abort", NS_ERROR_DOM_INVALID_STATE_ERR);
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ if (mPendingRemoval.Exists()) {
+ DDLOG(DDLogCategory::API, "Abort", NS_ERROR_DOM_INVALID_STATE_ERR);
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ DDLOG(DDLogCategory::API, "Abort", NS_OK);
+ AbortBufferAppend();
+ ResetParserState();
+ mCurrentAttributes.SetAppendWindowStart(0);
+ mCurrentAttributes.SetAppendWindowEnd(PositiveInfinity<double>());
+}
+
+void SourceBuffer::AbortBufferAppend() {
+ if (mUpdating) {
+ mCompletionPromise.DisconnectIfExists();
+ if (mPendingAppend.Exists()) {
+ mPendingAppend.Disconnect();
+ mTrackBuffersManager->AbortAppendData();
+ }
+ AbortUpdating();
+ }
+}
+
+void SourceBuffer::ResetParserState() {
+ mTrackBuffersManager->ResetParserState(mCurrentAttributes);
+}
+
+void SourceBuffer::Remove(double aStart, double aEnd, ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_API("Remove(aStart=%f, aEnd=%f)", aStart, aEnd);
+ DDLOG(DDLogCategory::API, "Remove-from", aStart);
+ DDLOG(DDLogCategory::API, "Remove-until", aEnd);
+
+ PrepareRemove(aStart, aEnd, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+ RangeRemoval(aStart, aEnd);
+}
+
+already_AddRefed<Promise> SourceBuffer::RemoveAsync(double aStart, double aEnd,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_API("RemoveAsync(aStart=%f, aEnd=%f)", aStart, aEnd);
+ DDLOG(DDLogCategory::API, "Remove-from", aStart);
+ DDLOG(DDLogCategory::API, "Remove-until", aEnd);
+
+ if (!IsAttached()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIGlobalObject> parentObject =
+ do_QueryInterface(mMediaSource->GetParentObject());
+ if (!parentObject) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ RefPtr<Promise> promise = Promise::Create(parentObject, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ PrepareRemove(aStart, aEnd, aRv);
+
+ if (aRv.Failed()) {
+ // The bindings will automatically return a rejected promise.
+ return nullptr;
+ }
+ MOZ_ASSERT(!mDOMPromise, "Can't have a pending operation going");
+ mDOMPromise = promise;
+ RangeRemoval(aStart, aEnd);
+
+ return promise.forget();
+}
+
+void SourceBuffer::PrepareRemove(double aStart, double aEnd, ErrorResult& aRv) {
+ if (!IsAttached()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ if (mUpdating) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ if (std::isnan(mMediaSource->Duration())) {
+ aRv.ThrowTypeError("Duration is NaN");
+ return;
+ }
+ if (aStart < 0 || aStart > mMediaSource->Duration()) {
+ aRv.ThrowTypeError("Invalid start value");
+ return;
+ }
+ if (aEnd <= aStart || std::isnan(aEnd)) {
+ aRv.ThrowTypeError("Invalid end value");
+ return;
+ }
+ if (mMediaSource->ReadyState() == MediaSourceReadyState::Ended) {
+ mMediaSource->SetReadyState(MediaSourceReadyState::Open);
+ }
+}
+
+void SourceBuffer::RangeRemoval(double aStart, double aEnd) {
+ StartUpdating();
+
+ RefPtr<SourceBuffer> self = this;
+ mTrackBuffersManager
+ ->RangeRemoval(TimeUnit::FromSeconds(aStart), TimeUnit::FromSeconds(aEnd))
+ ->Then(
+ mAbstractMainThread, __func__,
+ [self](bool) {
+ self->mPendingRemoval.Complete();
+ self->StopUpdating();
+ },
+ []() { MOZ_ASSERT(false); })
+ ->Track(mPendingRemoval);
+}
+
+void SourceBuffer::ChangeType(const nsAString& aType, ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // 1. If type is an empty string then throw a TypeError exception and abort
+ // these steps.
+ if (aType.IsEmpty()) {
+ aRv.ThrowTypeError("Type must not be empty");
+ return;
+ }
+
+ // 2. If this object has been removed from the sourceBuffers attribute of the
+ // parent media source , then throw an InvalidStateError exception and
+ // abort these steps.
+ // 3. If the updating attribute equals true, then throw an InvalidStateError
+ // exception and abort these steps.
+ if (!IsAttached() || mUpdating) {
+ DDLOG(DDLogCategory::API, "ChangeType", NS_ERROR_DOM_INVALID_STATE_ERR);
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ // 4. If type contains a MIME type that is not supported or contains a MIME
+ // type that is not supported with the types specified (currently or
+ // previously) of SourceBuffer objects in the sourceBuffers attribute of
+ // the parent media source , then throw a NotSupportedError exception and
+ // abort these steps.
+ DecoderDoctorDiagnostics diagnostics;
+ MediaSource::IsTypeSupported(aType, &diagnostics, aRv);
+ bool supported = !aRv.Failed();
+ diagnostics.StoreFormatDiagnostics(
+ mMediaSource->GetOwner() ? mMediaSource->GetOwner()->GetExtantDoc()
+ : nullptr,
+ aType, supported, __func__);
+ MSE_API("ChangeType(aType=%s)%s", NS_ConvertUTF16toUTF8(aType).get(),
+ supported ? "" : " [not supported]");
+ if (!supported) {
+ DDLOG(DDLogCategory::API, "ChangeType",
+ static_cast<nsresult>(aRv.ErrorCodeAsInt()));
+ return;
+ }
+
+ // 5. If the readyState attribute of the parent media source is in the "ended"
+ // state then run the following steps:
+ // 1. Set the readyState attribute of the parent media source to "open"
+ // 2. Queue a task to fire a simple event named sourceopen at the parent
+ // media source .
+ MOZ_ASSERT(mMediaSource->ReadyState() != MediaSourceReadyState::Closed);
+ if (mMediaSource->ReadyState() == MediaSourceReadyState::Ended) {
+ mMediaSource->SetReadyState(MediaSourceReadyState::Open);
+ }
+ Maybe<MediaContainerType> containerType = MakeMediaContainerType(aType);
+ MOZ_ASSERT(containerType);
+ mType = *containerType;
+ // 6. Run the reset parser state algorithm .
+ ResetParserState();
+
+ // 7. Update the generate timestamps flag on this SourceBuffer object to the
+ // value in the "Generate Timestamps Flag" column of the byte stream format
+ // registry [ MSE-REGISTRY ] entry that is associated with type .
+ if (mType.Type() == MEDIAMIMETYPE("audio/mpeg") ||
+ mType.Type() == MEDIAMIMETYPE("audio/aac")) {
+ mCurrentAttributes.mGenerateTimestamps = true;
+ // 8. If the generate timestamps flag equals true:
+ // Set the mode attribute on this SourceBuffer object to "sequence" ,
+ // including running the associated steps for that attribute being set.
+ ErrorResult dummy;
+ SetMode(SourceBufferAppendMode::Sequence, dummy);
+ } else {
+ mCurrentAttributes.mGenerateTimestamps = false;
+ // Otherwise: Keep the previous value of the mode attribute on this
+ // SourceBuffer object, without running any associated steps for that
+ // attribute being set.
+ }
+
+ // 9. Set pending initialization segment for changeType flag to true.
+ mTrackBuffersManager->ChangeType(mType);
+}
+
+void SourceBuffer::Detach() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_DEBUG("Detach");
+ if (!mMediaSource) {
+ MSE_DEBUG("Already detached");
+ return;
+ }
+ AbortBufferAppend();
+ if (mTrackBuffersManager) {
+ mMediaSource->GetDecoder()->GetDemuxer()->DetachSourceBuffer(
+ mTrackBuffersManager);
+ mTrackBuffersManager->Detach();
+ }
+ mTrackBuffersManager = nullptr;
+ mMediaSource = nullptr;
+}
+
+void SourceBuffer::Ended() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(IsAttached());
+ MSE_DEBUG("Ended");
+ mTrackBuffersManager->Ended();
+}
+
+SourceBuffer::SourceBuffer(MediaSource* aMediaSource,
+ const MediaContainerType& aType)
+ : DOMEventTargetHelper(aMediaSource->GetParentObject()),
+ mMediaSource(aMediaSource),
+ mAbstractMainThread(aMediaSource->AbstractMainThread()),
+ mCurrentAttributes(aType.Type() == MEDIAMIMETYPE("audio/mpeg") ||
+ aType.Type() == MEDIAMIMETYPE("audio/aac")),
+ mUpdating(false),
+ mActive(false),
+ mType(aType) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aMediaSource);
+
+ mTrackBuffersManager =
+ new TrackBuffersManager(aMediaSource->GetDecoder(), aType);
+ DDLINKCHILD("track buffers manager", mTrackBuffersManager.get());
+
+ MSE_DEBUG("Create mTrackBuffersManager=%p", mTrackBuffersManager.get());
+
+ ErrorResult dummy;
+ if (mCurrentAttributes.mGenerateTimestamps) {
+ SetMode(SourceBufferAppendMode::Sequence, dummy);
+ } else {
+ SetMode(SourceBufferAppendMode::Segments, dummy);
+ }
+ mMediaSource->GetDecoder()->GetDemuxer()->AttachSourceBuffer(
+ mTrackBuffersManager);
+}
+
+SourceBuffer::~SourceBuffer() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!mMediaSource);
+ MSE_DEBUG("");
+}
+
+MediaSource* SourceBuffer::GetParentObject() const { return mMediaSource; }
+
+JSObject* SourceBuffer::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return SourceBuffer_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void SourceBuffer::DispatchSimpleEvent(const char* aName) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_API("Dispatch event '%s'", aName);
+ DispatchTrustedEvent(NS_ConvertUTF8toUTF16(aName));
+}
+
+void SourceBuffer::QueueAsyncSimpleEvent(const char* aName) {
+ MSE_DEBUG("Queuing event '%s'", aName);
+ nsCOMPtr<nsIRunnable> event = new AsyncEventRunner<SourceBuffer>(this, aName);
+ mAbstractMainThread->Dispatch(event.forget());
+}
+
+void SourceBuffer::StartUpdating() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!mUpdating);
+ mUpdating = true;
+ QueueAsyncSimpleEvent("updatestart");
+}
+
+void SourceBuffer::StopUpdating() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!mUpdating) {
+ // The buffer append or range removal algorithm has been interrupted by
+ // abort().
+ return;
+ }
+ mUpdating = false;
+ QueueAsyncSimpleEvent("update");
+ QueueAsyncSimpleEvent("updateend");
+ if (mDOMPromise) {
+ mDOMPromise->MaybeResolveWithUndefined();
+ mDOMPromise = nullptr;
+ }
+}
+
+void SourceBuffer::AbortUpdating() {
+ MOZ_ASSERT(NS_IsMainThread());
+ mUpdating = false;
+ QueueAsyncSimpleEvent("abort");
+ QueueAsyncSimpleEvent("updateend");
+ if (mDOMPromise) {
+ mDOMPromise->MaybeReject(NS_ERROR_DOM_MEDIA_ABORT_ERR);
+ mDOMPromise = nullptr;
+ }
+}
+
+void SourceBuffer::CheckEndTime() {
+ MOZ_ASSERT(NS_IsMainThread());
+ // Check if we need to update mMediaSource duration
+ TimeUnit endTime = mCurrentAttributes.GetGroupEndTimestamp();
+ double duration = mMediaSource->Duration();
+ if (!std::isnan(duration) && endTime.ToSeconds() > duration) {
+ mMediaSource->SetDuration(endTime);
+ }
+}
+
+void SourceBuffer::AppendData(const uint8_t* aData, uint32_t aLength,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_DEBUG("AppendData(aLength=%u)", aLength);
+
+ RefPtr<MediaByteBuffer> data = PrepareAppend(aData, aLength, aRv);
+ if (!data) {
+ return;
+ }
+ StartUpdating();
+
+ mTrackBuffersManager->AppendData(data.forget(), mCurrentAttributes)
+ ->Then(mAbstractMainThread, __func__, this,
+ &SourceBuffer::AppendDataCompletedWithSuccess,
+ &SourceBuffer::AppendDataErrored)
+ ->Track(mPendingAppend);
+}
+
+already_AddRefed<Promise> SourceBuffer::AppendDataAsync(const uint8_t* aData,
+ uint32_t aLength,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!IsAttached()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIGlobalObject> parentObject =
+ do_QueryInterface(mMediaSource->GetParentObject());
+ if (!parentObject) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ RefPtr<Promise> promise = Promise::Create(parentObject, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ AppendData(aData, aLength, aRv);
+
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ MOZ_ASSERT(!mDOMPromise, "Can't have a pending operation going");
+ mDOMPromise = promise;
+
+ return promise.forget();
+}
+
+void SourceBuffer::AppendDataCompletedWithSuccess(
+ const SourceBufferTask::AppendBufferResult& aResult) {
+ MOZ_ASSERT(mUpdating);
+ mPendingAppend.Complete();
+ DDLOG(DDLogCategory::API, "AppendBuffer-completed", NS_OK);
+
+ if (aResult.first) {
+ if (!mActive) {
+ mActive = true;
+ MSE_DEBUG("Init segment received");
+ RefPtr<SourceBuffer> self = this;
+ mMediaSource->SourceBufferIsActive(this)
+ ->Then(mAbstractMainThread, __func__,
+ [self, this]() {
+ MSE_DEBUG("Complete AppendBuffer operation");
+ mCompletionPromise.Complete();
+ StopUpdating();
+ })
+ ->Track(mCompletionPromise);
+ }
+ }
+ if (mActive) {
+ // Tell our parent decoder that we have received new data
+ // and send progress event.
+ mMediaSource->GetDecoder()->NotifyDataArrived();
+ }
+
+ mCurrentAttributes = aResult.second;
+
+ CheckEndTime();
+
+ if (!mCompletionPromise.Exists()) {
+ StopUpdating();
+ }
+}
+
+void SourceBuffer::AppendDataErrored(const MediaResult& aError) {
+ MOZ_ASSERT(mUpdating);
+ mPendingAppend.Complete();
+ DDLOG(DDLogCategory::API, "AppendBuffer-error", aError);
+
+ switch (aError.Code()) {
+ case NS_ERROR_DOM_MEDIA_CANCELED:
+ // Nothing further to do as the trackbuffer has been shutdown.
+ // or append was aborted and abort() has handled all the events.
+ break;
+ default:
+ AppendError(aError);
+ break;
+ }
+}
+
+void SourceBuffer::AppendError(const MediaResult& aDecodeError) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ ResetParserState();
+
+ mUpdating = false;
+
+ QueueAsyncSimpleEvent("error");
+ QueueAsyncSimpleEvent("updateend");
+
+ MOZ_ASSERT(NS_FAILED(aDecodeError));
+
+ mMediaSource->EndOfStream(aDecodeError);
+
+ if (mDOMPromise) {
+ mDOMPromise->MaybeReject(aDecodeError);
+ mDOMPromise = nullptr;
+ }
+}
+
+already_AddRefed<MediaByteBuffer> SourceBuffer::PrepareAppend(
+ const uint8_t* aData, uint32_t aLength, ErrorResult& aRv) {
+ typedef TrackBuffersManager::EvictDataResult Result;
+
+ if (!IsAttached() || mUpdating) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ // If the HTMLMediaElement.error attribute is not null, then throw an
+ // InvalidStateError exception and abort these steps.
+ if (!mMediaSource->GetDecoder() ||
+ mMediaSource->GetDecoder()->OwnerHasError()) {
+ MSE_DEBUG("HTMLMediaElement.error is not null");
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ if (mMediaSource->ReadyState() == MediaSourceReadyState::Ended) {
+ mMediaSource->SetReadyState(MediaSourceReadyState::Open);
+ }
+
+ // Eviction uses a byte threshold. If the buffer is greater than the
+ // number of bytes then data is evicted.
+ // TODO: Drive evictions off memory pressure notifications.
+ // TODO: Consider a global eviction threshold rather than per TrackBuffer.
+ // Give a chance to the TrackBuffersManager to evict some data if needed.
+ Result evicted = mTrackBuffersManager->EvictData(
+ TimeUnit::FromSeconds(mMediaSource->GetDecoder()->GetCurrentTime()),
+ aLength);
+
+ // See if we have enough free space to append our new data.
+ if (evicted == Result::BUFFER_FULL) {
+ aRv.Throw(NS_ERROR_DOM_MEDIA_SOURCE_FULL_BUFFER_QUOTA_EXCEEDED_ERR);
+ return nullptr;
+ }
+
+ RefPtr<MediaByteBuffer> data = new MediaByteBuffer();
+ if (!data->AppendElements(aData, aLength, fallible)) {
+ aRv.Throw(NS_ERROR_DOM_MEDIA_SOURCE_FULL_BUFFER_QUOTA_EXCEEDED_ERR);
+ return nullptr;
+ }
+ return data.forget();
+}
+
+TimeUnit SourceBuffer::GetBufferedEnd() {
+ MOZ_ASSERT(NS_IsMainThread());
+ ErrorResult dummy;
+ media::TimeIntervals intervals = GetBufferedIntervals();
+ return intervals.GetEnd();
+}
+
+TimeUnit SourceBuffer::HighestStartTime() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mTrackBuffersManager);
+ return mTrackBuffersManager->HighestStartTime();
+}
+
+TimeUnit SourceBuffer::HighestEndTime() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mTrackBuffersManager);
+ return mTrackBuffersManager->HighestEndTime();
+}
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(SourceBuffer)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(SourceBuffer)
+ tmp->Detach();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mMediaSource)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mBuffered)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mDOMPromise)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END_INHERITED(DOMEventTargetHelper)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(SourceBuffer,
+ DOMEventTargetHelper)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMediaSource)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBuffered)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDOMPromise)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_ADDREF_INHERITED(SourceBuffer, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(SourceBuffer, DOMEventTargetHelper)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SourceBuffer)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+#undef MSE_DEBUG
+#undef MSE_DEBUGV
+#undef MSE_API
+
+} // namespace dom
+
+} // namespace mozilla
diff --git a/dom/media/mediasource/SourceBuffer.h b/dom/media/mediasource/SourceBuffer.h
new file mode 100644
index 0000000000..6155952acf
--- /dev/null
+++ b/dom/media/mediasource/SourceBuffer.h
@@ -0,0 +1,207 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_SourceBuffer_h_
+#define mozilla_dom_SourceBuffer_h_
+
+#include "mozilla/MozPromise.h"
+#include "MediaContainerType.h"
+#include "MediaSource.h"
+#include "js/RootingAPI.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/SourceBufferBinding.h"
+#include "mozilla/dom/TypedArray.h"
+#include "mozilla/mozalloc.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionNoteChild.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupports.h"
+#include "nscore.h"
+#include "TrackBuffersManager.h"
+#include "SourceBufferTask.h"
+
+class JSObject;
+struct JSContext;
+
+namespace mozilla {
+
+class AbstractThread;
+class ErrorResult;
+class MediaByteBuffer;
+template <typename T>
+class AsyncEventRunner;
+
+DDLoggedTypeName(dom::SourceBuffer);
+
+namespace dom {
+
+class TimeRanges;
+
+class SourceBuffer final : public DOMEventTargetHelper,
+ public DecoderDoctorLifeLogger<SourceBuffer> {
+ public:
+ /** WebIDL Methods. */
+ SourceBufferAppendMode Mode() const {
+ return mCurrentAttributes.GetAppendMode();
+ }
+
+ void SetMode(SourceBufferAppendMode aMode, ErrorResult& aRv);
+
+ bool Updating() const { return mUpdating; }
+
+ TimeRanges* GetBuffered(ErrorResult& aRv);
+ media::TimeIntervals GetTimeIntervals();
+
+ double TimestampOffset() const {
+ return mCurrentAttributes.GetApparentTimestampOffset();
+ }
+
+ void SetTimestampOffset(double aTimestampOffset, ErrorResult& aRv);
+
+ double AppendWindowStart() const {
+ return mCurrentAttributes.GetAppendWindowStart();
+ }
+
+ void SetAppendWindowStart(double aAppendWindowStart, ErrorResult& aRv);
+
+ double AppendWindowEnd() const {
+ return mCurrentAttributes.GetAppendWindowEnd();
+ }
+
+ void SetAppendWindowEnd(double aAppendWindowEnd, ErrorResult& aRv);
+
+ void AppendBuffer(const ArrayBuffer& aData, ErrorResult& aRv);
+ void AppendBuffer(const ArrayBufferView& aData, ErrorResult& aRv);
+
+ already_AddRefed<Promise> AppendBufferAsync(const ArrayBuffer& aData,
+ ErrorResult& aRv);
+ already_AddRefed<Promise> AppendBufferAsync(const ArrayBufferView& aData,
+ ErrorResult& aRv);
+
+ void Abort(ErrorResult& aRv);
+ void AbortBufferAppend();
+
+ void Remove(double aStart, double aEnd, ErrorResult& aRv);
+
+ already_AddRefed<Promise> RemoveAsync(double aStart, double aEnd,
+ ErrorResult& aRv);
+
+ void ChangeType(const nsAString& aType, ErrorResult& aRv);
+
+ IMPL_EVENT_HANDLER(updatestart);
+ IMPL_EVENT_HANDLER(update);
+ IMPL_EVENT_HANDLER(updateend);
+ IMPL_EVENT_HANDLER(error);
+ IMPL_EVENT_HANDLER(abort);
+
+ /** End WebIDL Methods. */
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(SourceBuffer, DOMEventTargetHelper)
+
+ SourceBuffer(MediaSource* aMediaSource, const MediaContainerType& aType);
+
+ MediaSource* GetParentObject() const;
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // Notify the SourceBuffer that it has been detached from the
+ // MediaSource's sourceBuffer list.
+ void Detach();
+ bool IsAttached() const { return mMediaSource != nullptr; }
+
+ void Ended();
+
+ media::TimeIntervals GetBufferedIntervals();
+ media::TimeUnit GetBufferedEnd();
+ media::TimeUnit HighestStartTime();
+ media::TimeUnit HighestEndTime();
+
+ // Runs the range removal algorithm as defined by the MSE spec.
+ void RangeRemoval(double aStart, double aEnd);
+
+ bool IsActive() const { return mActive; }
+
+ private:
+ ~SourceBuffer();
+
+ friend class AsyncEventRunner<SourceBuffer>;
+ friend class BufferAppendRunnable;
+ friend class mozilla::TrackBuffersManager;
+ void DispatchSimpleEvent(const char* aName);
+ void QueueAsyncSimpleEvent(const char* aName);
+
+ // Update mUpdating and fire the appropriate events.
+ void StartUpdating();
+ void StopUpdating();
+ void AbortUpdating();
+ void ResetParserState();
+
+ // If the media segment contains data beyond the current duration,
+ // then run the duration change algorithm with new duration set to the
+ // maximum of the current duration and the group end timestamp.
+ void CheckEndTime();
+
+ // Shared implementation of AppendBuffer overloads.
+ void AppendData(const uint8_t* aData, uint32_t aLength, ErrorResult& aRv);
+ // Shared implementation of AppendBufferAsync overloads.
+ already_AddRefed<Promise> AppendDataAsync(const uint8_t* aData,
+ uint32_t aLength, ErrorResult& aRv);
+
+ void PrepareRemove(double aStart, double aEnd, ErrorResult& aRv);
+
+ // Implement the "Append Error Algorithm".
+ // Will call endOfStream() with "decode" error if aDecodeError is true.
+ // 3.5.3 Append Error Algorithm
+ // http://w3c.github.io/media-source/#sourcebuffer-append-error
+ void AppendError(const MediaResult& aDecodeError);
+
+ // Implements the "Prepare Append Algorithm". Returns MediaByteBuffer object
+ // on success or nullptr (with aRv set) on error.
+ already_AddRefed<MediaByteBuffer> PrepareAppend(const uint8_t* aData,
+ uint32_t aLength,
+ ErrorResult& aRv);
+
+ void AppendDataCompletedWithSuccess(
+ const SourceBufferTask::AppendBufferResult& aResult);
+ void AppendDataErrored(const MediaResult& aError);
+
+ RefPtr<MediaSource> mMediaSource;
+ const RefPtr<AbstractThread> mAbstractMainThread;
+
+ RefPtr<TrackBuffersManager> mTrackBuffersManager;
+ SourceBufferAttributes mCurrentAttributes;
+
+ bool mUpdating;
+
+ mozilla::Atomic<bool> mActive;
+
+ MozPromiseRequestHolder<SourceBufferTask::AppendPromise> mPendingAppend;
+ MozPromiseRequestHolder<SourceBufferTask::RangeRemovalPromise>
+ mPendingRemoval;
+ MediaContainerType mType;
+
+ RefPtr<TimeRanges> mBuffered;
+
+ MozPromiseRequestHolder<MediaSource::ActiveCompletionPromise>
+ mCompletionPromise;
+
+ // Only used if MSE v2 experimental mode is active.
+ // Contains the current Promise to be resolved following use of
+ // appendBufferAsync and removeAsync. Not set of no operation is pending.
+ RefPtr<Promise> mDOMPromise;
+};
+
+} // namespace dom
+
+} // namespace mozilla
+
+#endif /* mozilla_dom_SourceBuffer_h_ */
diff --git a/dom/media/mediasource/SourceBufferAttributes.h b/dom/media/mediasource/SourceBufferAttributes.h
new file mode 100644
index 0000000000..f15845b8a9
--- /dev/null
+++ b/dom/media/mediasource/SourceBufferAttributes.h
@@ -0,0 +1,116 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_SourceBufferAttributes_h_
+#define mozilla_SourceBufferAttributes_h_
+
+#include "TimeUnits.h"
+#include "mozilla/dom/SourceBufferBinding.h"
+#include "mozilla/Maybe.h"
+
+namespace mozilla {
+
+class SourceBufferAttributes {
+ public:
+ // Current state as per Segment Parser Loop Algorithm
+ // http://w3c.github.io/media-source/index.html#sourcebuffer-segment-parser-loop
+ enum class AppendState {
+ WAITING_FOR_SEGMENT,
+ PARSING_INIT_SEGMENT,
+ PARSING_MEDIA_SEGMENT,
+ };
+
+ explicit SourceBufferAttributes(bool aGenerateTimestamp)
+ : mGenerateTimestamps(aGenerateTimestamp),
+ mAppendWindowStart(0),
+ mAppendWindowEnd(PositiveInfinity<double>()),
+ mAppendMode(dom::SourceBufferAppendMode::Segments),
+ mApparentTimestampOffset(0),
+ mAppendState(AppendState::WAITING_FOR_SEGMENT) {}
+
+ SourceBufferAttributes(const SourceBufferAttributes& aOther) = default;
+
+ double GetAppendWindowStart() const { return mAppendWindowStart; }
+
+ double GetAppendWindowEnd() const { return mAppendWindowEnd; }
+
+ void SetAppendWindowStart(double aWindowStart) {
+ mAppendWindowStart = aWindowStart;
+ }
+
+ void SetAppendWindowEnd(double aWindowEnd) { mAppendWindowEnd = aWindowEnd; }
+
+ double GetApparentTimestampOffset() const { return mApparentTimestampOffset; }
+
+ void SetApparentTimestampOffset(double aTimestampOffset) {
+ mApparentTimestampOffset = aTimestampOffset;
+ mTimestampOffset = media::TimeUnit::FromSeconds(aTimestampOffset);
+ }
+
+ media::TimeUnit GetTimestampOffset() const { return mTimestampOffset; }
+
+ void SetTimestampOffset(const media::TimeUnit& aTimestampOffset) {
+ mTimestampOffset = aTimestampOffset;
+ mApparentTimestampOffset = aTimestampOffset.ToSeconds();
+ }
+
+ dom::SourceBufferAppendMode GetAppendMode() const { return mAppendMode; }
+
+ void SetAppendMode(dom::SourceBufferAppendMode aAppendMode) {
+ mAppendMode = aAppendMode;
+ }
+
+ void SetGroupStartTimestamp(const media::TimeUnit& aGroupStartTimestamp) {
+ mGroupStartTimestamp = Some(aGroupStartTimestamp);
+ }
+
+ media::TimeUnit GetGroupStartTimestamp() const {
+ return mGroupStartTimestamp.ref();
+ }
+
+ bool HaveGroupStartTimestamp() const { return mGroupStartTimestamp.isSome(); }
+
+ void ResetGroupStartTimestamp() { mGroupStartTimestamp.reset(); }
+
+ void RestartGroupStartTimestamp() {
+ mGroupStartTimestamp = Some(mGroupEndTimestamp);
+ }
+
+ media::TimeUnit GetGroupEndTimestamp() const { return mGroupEndTimestamp; }
+
+ void SetGroupEndTimestamp(const media::TimeUnit& aGroupEndTimestamp) {
+ mGroupEndTimestamp = aGroupEndTimestamp;
+ }
+
+ AppendState GetAppendState() const { return mAppendState; }
+
+ void SetAppendState(AppendState aState) { mAppendState = aState; }
+
+ // mGenerateTimestamp isn't mutable once the source buffer has been
+ // constructed
+ bool mGenerateTimestamps;
+
+ SourceBufferAttributes& operator=(const SourceBufferAttributes& aOther) =
+ default;
+
+ private:
+ SourceBufferAttributes() = delete;
+
+ double mAppendWindowStart;
+ double mAppendWindowEnd;
+ dom::SourceBufferAppendMode mAppendMode;
+ double mApparentTimestampOffset;
+ media::TimeUnit mTimestampOffset;
+ Maybe<media::TimeUnit> mGroupStartTimestamp;
+ media::TimeUnit mGroupEndTimestamp;
+ // The current append state as per
+ // https://w3c.github.io/media-source/#sourcebuffer-append-state
+ AppendState mAppendState;
+};
+
+} // end namespace mozilla
+
+#endif /* mozilla_SourceBufferAttributes_h_ */
diff --git a/dom/media/mediasource/SourceBufferList.cpp b/dom/media/mediasource/SourceBufferList.cpp
new file mode 100644
index 0000000000..9a98f83a1c
--- /dev/null
+++ b/dom/media/mediasource/SourceBufferList.cpp
@@ -0,0 +1,187 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SourceBufferList.h"
+
+#include "AsyncEventRunner.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/dom/SourceBufferListBinding.h"
+#include "mozilla/mozalloc.h"
+#include "nsCOMPtr.h"
+#include "nsIRunnable.h"
+#include "nsString.h"
+#include "nsThreadUtils.h"
+#include "mozilla/Logging.h"
+
+extern mozilla::LogModule* GetMediaSourceLog();
+extern mozilla::LogModule* GetMediaSourceAPILog();
+
+#define MSE_API(arg, ...) \
+ MOZ_LOG(GetMediaSourceAPILog(), mozilla::LogLevel::Debug, \
+ ("SourceBufferList(%p)::%s: " arg, this, __func__, ##__VA_ARGS__))
+#define MSE_DEBUG(arg, ...) \
+ MOZ_LOG(GetMediaSourceLog(), mozilla::LogLevel::Debug, \
+ ("SourceBufferList(%p)::%s: " arg, this, __func__, ##__VA_ARGS__))
+
+struct JSContext;
+class JSObject;
+
+using TimeUnit = mozilla::media::TimeUnit;
+
+namespace mozilla::dom {
+
+SourceBufferList::~SourceBufferList() = default;
+
+SourceBuffer* SourceBufferList::IndexedGetter(uint32_t aIndex, bool& aFound) {
+ MOZ_ASSERT(NS_IsMainThread());
+ aFound = aIndex < mSourceBuffers.Length();
+
+ if (!aFound) {
+ return nullptr;
+ }
+ return mSourceBuffers[aIndex];
+}
+
+uint32_t SourceBufferList::Length() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mSourceBuffers.Length();
+}
+
+void SourceBufferList::Append(SourceBuffer* aSourceBuffer) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mSourceBuffers.AppendElement(aSourceBuffer);
+ QueueAsyncSimpleEvent("addsourcebuffer");
+}
+
+void SourceBufferList::AppendSimple(SourceBuffer* aSourceBuffer) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mSourceBuffers.AppendElement(aSourceBuffer);
+}
+
+void SourceBufferList::Remove(SourceBuffer* aSourceBuffer) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ALWAYS_TRUE(mSourceBuffers.RemoveElement(aSourceBuffer));
+ aSourceBuffer->Detach();
+ QueueAsyncSimpleEvent("removesourcebuffer");
+}
+
+bool SourceBufferList::Contains(SourceBuffer* aSourceBuffer) {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mSourceBuffers.Contains(aSourceBuffer);
+}
+
+void SourceBufferList::Clear() {
+ MOZ_ASSERT(NS_IsMainThread());
+ for (uint32_t i = 0; i < mSourceBuffers.Length(); ++i) {
+ mSourceBuffers[i]->Detach();
+ }
+ mSourceBuffers.Clear();
+ QueueAsyncSimpleEvent("removesourcebuffer");
+}
+
+void SourceBufferList::ClearSimple() {
+ MOZ_ASSERT(NS_IsMainThread());
+ mSourceBuffers.Clear();
+}
+
+bool SourceBufferList::IsEmpty() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mSourceBuffers.IsEmpty();
+}
+
+bool SourceBufferList::AnyUpdating() {
+ MOZ_ASSERT(NS_IsMainThread());
+ for (uint32_t i = 0; i < mSourceBuffers.Length(); ++i) {
+ if (mSourceBuffers[i]->Updating()) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void SourceBufferList::RangeRemoval(double aStart, double aEnd) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_DEBUG("RangeRemoval(aStart=%f, aEnd=%f)", aStart, aEnd);
+ for (uint32_t i = 0; i < mSourceBuffers.Length(); ++i) {
+ mSourceBuffers[i]->RangeRemoval(aStart, aEnd);
+ }
+}
+
+void SourceBufferList::Ended() {
+ MOZ_ASSERT(NS_IsMainThread());
+ for (uint32_t i = 0; i < mSourceBuffers.Length(); ++i) {
+ mSourceBuffers[i]->Ended();
+ }
+}
+
+TimeUnit SourceBufferList::GetHighestBufferedEndTime() {
+ MOZ_ASSERT(NS_IsMainThread());
+ TimeUnit highestEndTime = TimeUnit::Zero();
+ for (uint32_t i = 0; i < mSourceBuffers.Length(); ++i) {
+ highestEndTime =
+ std::max(highestEndTime, mSourceBuffers[i]->GetBufferedEnd());
+ }
+ return highestEndTime;
+}
+
+void SourceBufferList::DispatchSimpleEvent(const char* aName) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_API("Dispatch event '%s'", aName);
+ DispatchTrustedEvent(NS_ConvertUTF8toUTF16(aName));
+}
+
+void SourceBufferList::QueueAsyncSimpleEvent(const char* aName) {
+ MSE_DEBUG("Queue event '%s'", aName);
+ nsCOMPtr<nsIRunnable> event =
+ new AsyncEventRunner<SourceBufferList>(this, aName);
+ mAbstractMainThread->Dispatch(event.forget());
+}
+
+SourceBufferList::SourceBufferList(MediaSource* aMediaSource)
+ : DOMEventTargetHelper(aMediaSource->GetParentObject()),
+ mMediaSource(aMediaSource),
+ mAbstractMainThread(mMediaSource->AbstractMainThread()) {
+ MOZ_ASSERT(aMediaSource);
+}
+
+MediaSource* SourceBufferList::GetParentObject() const { return mMediaSource; }
+
+TimeUnit SourceBufferList::HighestStartTime() {
+ MOZ_ASSERT(NS_IsMainThread());
+ TimeUnit highestStartTime = TimeUnit::Zero();
+ for (auto& sourceBuffer : mSourceBuffers) {
+ highestStartTime =
+ std::max(sourceBuffer->HighestStartTime(), highestStartTime);
+ }
+ return highestStartTime;
+}
+
+TimeUnit SourceBufferList::HighestEndTime() {
+ MOZ_ASSERT(NS_IsMainThread());
+ TimeUnit highestEndTime = TimeUnit::Zero();
+ for (auto& sourceBuffer : mSourceBuffers) {
+ highestEndTime = std::max(sourceBuffer->HighestEndTime(), highestEndTime);
+ }
+ return highestEndTime;
+}
+
+JSObject* SourceBufferList::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return SourceBufferList_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(SourceBufferList, DOMEventTargetHelper,
+ mMediaSource, mSourceBuffers)
+
+NS_IMPL_ADDREF_INHERITED(SourceBufferList, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(SourceBufferList, DOMEventTargetHelper)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SourceBufferList)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+#undef MSE_API
+#undef MSE_DEBUG
+} // namespace mozilla::dom
diff --git a/dom/media/mediasource/SourceBufferList.h b/dom/media/mediasource/SourceBufferList.h
new file mode 100644
index 0000000000..3779bf353a
--- /dev/null
+++ b/dom/media/mediasource/SourceBufferList.h
@@ -0,0 +1,110 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_SourceBufferList_h_
+#define mozilla_dom_SourceBufferList_h_
+
+#include "SourceBuffer.h"
+#include "js/RootingAPI.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/DOMEventTargetHelper.h"
+#include "nsCycleCollectionNoteChild.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupports.h"
+#include "nsTArray.h"
+
+struct JSContext;
+class JSObject;
+
+namespace mozilla {
+
+template <typename T>
+class AsyncEventRunner;
+
+namespace dom {
+
+class MediaSource;
+
+class SourceBufferList final : public DOMEventTargetHelper {
+ public:
+ /** WebIDL Methods. */
+ SourceBuffer* IndexedGetter(uint32_t aIndex, bool& aFound);
+
+ uint32_t Length();
+
+ IMPL_EVENT_HANDLER(addsourcebuffer);
+ IMPL_EVENT_HANDLER(removesourcebuffer);
+
+ /** End WebIDL methods. */
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(SourceBufferList,
+ DOMEventTargetHelper)
+
+ explicit SourceBufferList(MediaSource* aMediaSource);
+
+ MediaSource* GetParentObject() const;
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // Append a SourceBuffer and fire "addsourcebuffer" at the list.
+ void Append(SourceBuffer* aSourceBuffer);
+
+ // Remove a SourceBuffer and fire "removesourcebuffer" at the list.
+ void Remove(SourceBuffer* aSourceBuffer);
+
+ // Returns true if aSourceBuffer is present in the list.
+ bool Contains(SourceBuffer* aSourceBuffer);
+
+ // Remove all SourceBuffers and fire a single "removesourcebuffer" at the
+ // list.
+ void Clear();
+
+ // True if list has zero entries.
+ bool IsEmpty();
+
+ // Returns true if updating is true on any SourceBuffers in the list.
+ bool AnyUpdating();
+
+ // Runs the range removal steps from the MSE specification on each
+ // SourceBuffer.
+ void RangeRemoval(double aStart, double aEnd);
+
+ // Mark all SourceBuffers input buffers as ended.
+ void Ended();
+
+ // Returns the highest end time of any of the Sourcebuffers.
+ media::TimeUnit GetHighestBufferedEndTime();
+
+ // Append a SourceBuffer to the list. No event is fired.
+ void AppendSimple(SourceBuffer* aSourceBuffer);
+
+ // Remove all SourceBuffers from mSourceBuffers.
+ // No event is fired and no action is performed on the sourcebuffers.
+ void ClearSimple();
+
+ media::TimeUnit HighestStartTime();
+ media::TimeUnit HighestEndTime();
+
+ private:
+ ~SourceBufferList();
+
+ friend class AsyncEventRunner<SourceBufferList>;
+ void DispatchSimpleEvent(const char* aName);
+ void QueueAsyncSimpleEvent(const char* aName);
+
+ RefPtr<MediaSource> mMediaSource;
+ nsTArray<RefPtr<SourceBuffer> > mSourceBuffers;
+ const RefPtr<AbstractThread> mAbstractMainThread;
+};
+
+} // namespace dom
+
+} // namespace mozilla
+
+#endif /* mozilla_dom_SourceBufferList_h_ */
diff --git a/dom/media/mediasource/SourceBufferResource.cpp b/dom/media/mediasource/SourceBufferResource.cpp
new file mode 100644
index 0000000000..49447be015
--- /dev/null
+++ b/dom/media/mediasource/SourceBufferResource.cpp
@@ -0,0 +1,144 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SourceBufferResource.h"
+
+#include "mozilla/Logging.h"
+#include "mozilla/TaskQueue.h"
+#include "MediaData.h"
+
+mozilla::LogModule* GetSourceBufferResourceLog() {
+ static mozilla::LazyLogModule sLogModule("SourceBufferResource");
+ return sLogModule;
+}
+
+#define SBR_DEBUG(arg, ...) \
+ DDMOZ_LOG(GetSourceBufferResourceLog(), mozilla::LogLevel::Debug, \
+ "::%s: " arg, __func__, ##__VA_ARGS__)
+#define SBR_DEBUGV(arg, ...) \
+ DDMOZ_LOG(GetSourceBufferResourceLog(), mozilla::LogLevel::Verbose, \
+ "::%s: " arg, __func__, ##__VA_ARGS__)
+
+namespace mozilla {
+
+RefPtr<GenericPromise> SourceBufferResource::Close() {
+ MOZ_ASSERT(OnThread());
+ SBR_DEBUG("Close");
+ mClosed = true;
+ return GenericPromise::CreateAndResolve(true, __func__);
+}
+
+nsresult SourceBufferResource::ReadAt(int64_t aOffset, char* aBuffer,
+ uint32_t aCount, uint32_t* aBytes) {
+ SBR_DEBUG("ReadAt(aOffset=%" PRId64 ", aBuffer=%p, aCount=%u, aBytes=%p)",
+ aOffset, aBytes, aCount, aBytes);
+ return ReadAtInternal(aOffset, aBuffer, aCount, aBytes);
+}
+
+nsresult SourceBufferResource::ReadAtInternal(int64_t aOffset, char* aBuffer,
+ uint32_t aCount,
+ uint32_t* aBytes) {
+ MOZ_ASSERT(OnThread());
+
+ if (mClosed || aOffset < 0 || uint64_t(aOffset) < mInputBuffer.GetOffset() ||
+ aOffset > GetLength()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ uint32_t available = GetLength() - aOffset;
+ uint32_t count = std::min(aCount, available);
+
+ SBR_DEBUGV("offset=%" PRId64 " GetLength()=%" PRId64
+ " available=%u count=%u mEnded=%d",
+ aOffset, GetLength(), available, count, mEnded);
+ if (available == 0) {
+ SBR_DEBUGV("reached EOF");
+ *aBytes = 0;
+ return NS_OK;
+ }
+
+ mInputBuffer.CopyData(aOffset, count, aBuffer);
+ *aBytes = count;
+
+ return NS_OK;
+}
+
+nsresult SourceBufferResource::ReadFromCache(char* aBuffer, int64_t aOffset,
+ uint32_t aCount) {
+ SBR_DEBUG("ReadFromCache(aBuffer=%p, aOffset=%" PRId64 ", aCount=%u)",
+ aBuffer, aOffset, aCount);
+ uint32_t bytesRead;
+ nsresult rv = ReadAtInternal(aOffset, aBuffer, aCount, &bytesRead);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // ReadFromCache return failure if not all the data is cached.
+ return bytesRead == aCount ? NS_OK : NS_ERROR_FAILURE;
+}
+
+uint32_t SourceBufferResource::EvictData(uint64_t aPlaybackOffset,
+ int64_t aThreshold) {
+ MOZ_ASSERT(OnThread());
+ SBR_DEBUG("EvictData(aPlaybackOffset=%" PRIu64
+ ","
+ "aThreshold=%" PRId64 ")",
+ aPlaybackOffset, aThreshold);
+ uint32_t result = mInputBuffer.Evict(aPlaybackOffset, aThreshold);
+ return result;
+}
+
+void SourceBufferResource::EvictBefore(uint64_t aOffset) {
+ MOZ_ASSERT(OnThread());
+ SBR_DEBUG("EvictBefore(aOffset=%" PRIu64 ")", aOffset);
+
+ mInputBuffer.EvictBefore(aOffset);
+}
+
+uint32_t SourceBufferResource::EvictAll() {
+ MOZ_ASSERT(OnThread());
+ SBR_DEBUG("EvictAll()");
+ return mInputBuffer.EvictAll();
+}
+
+void SourceBufferResource::AppendData(MediaByteBuffer* aData) {
+ AppendData(MediaSpan(aData));
+}
+
+void SourceBufferResource::AppendData(const MediaSpan& aData) {
+ MOZ_ASSERT(OnThread());
+ SBR_DEBUG("AppendData(aData=%p, aLength=%zu)", aData.Elements(),
+ aData.Length());
+ mInputBuffer.AppendItem(aData);
+ mEnded = false;
+}
+
+void SourceBufferResource::Ended() {
+ MOZ_ASSERT(OnThread());
+ SBR_DEBUG("");
+ mEnded = true;
+}
+
+SourceBufferResource::~SourceBufferResource() { SBR_DEBUG(""); }
+
+SourceBufferResource::SourceBufferResource()
+#if defined(DEBUG)
+ : mThread(AbstractThread::GetCurrent())
+#endif
+{
+ SBR_DEBUG("");
+}
+
+#if defined(DEBUG)
+const AbstractThread* SourceBufferResource::GetThread() const {
+ return mThread;
+}
+bool SourceBufferResource::OnThread() const {
+ return !GetThread() || GetThread()->IsCurrentThreadIn();
+}
+#endif
+
+#undef SBR_DEBUG
+#undef SBR_DEBUGV
+} // namespace mozilla
diff --git a/dom/media/mediasource/SourceBufferResource.h b/dom/media/mediasource/SourceBufferResource.h
new file mode 100644
index 0000000000..b117edb558
--- /dev/null
+++ b/dom/media/mediasource/SourceBufferResource.h
@@ -0,0 +1,143 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_SOURCEBUFFERRESOURCE_H_
+#define MOZILLA_SOURCEBUFFERRESOURCE_H_
+
+#include "mozilla/AbstractThread.h"
+#include "mozilla/Logging.h"
+#include "MediaResource.h"
+#include "ResourceQueue.h"
+
+#define UNIMPLEMENTED() \
+ { /* Logging this is too spammy to do by default */ \
+ }
+
+namespace mozilla {
+
+class MediaByteBuffer;
+class AbstractThread;
+
+namespace dom {
+
+class SourceBuffer;
+
+} // namespace dom
+
+DDLoggedTypeDeclNameAndBase(SourceBufferResource, MediaResource);
+
+// SourceBufferResource is not thread safe.
+class SourceBufferResource final
+ : public MediaResource,
+ public DecoderDoctorLifeLogger<SourceBufferResource> {
+ public:
+ SourceBufferResource();
+ RefPtr<GenericPromise> Close() override;
+ nsresult ReadAt(int64_t aOffset, char* aBuffer, uint32_t aCount,
+ uint32_t* aBytes) override;
+ // Memory-based and no locks, caching discouraged.
+ bool ShouldCacheReads() override { return false; }
+ void Pin() override { UNIMPLEMENTED(); }
+ void Unpin() override { UNIMPLEMENTED(); }
+ int64_t GetLength() override { return mInputBuffer.GetLength(); }
+ int64_t GetNextCachedData(int64_t aOffset) override {
+ MOZ_ASSERT(OnThread());
+ MOZ_ASSERT(aOffset >= 0);
+ if (uint64_t(aOffset) < mInputBuffer.GetOffset()) {
+ return mInputBuffer.GetOffset();
+ } else if (aOffset == GetLength()) {
+ return -1;
+ }
+ return aOffset;
+ }
+ int64_t GetCachedDataEnd(int64_t aOffset) override {
+ MOZ_ASSERT(OnThread());
+ MOZ_ASSERT(aOffset >= 0);
+ if (uint64_t(aOffset) < mInputBuffer.GetOffset() ||
+ aOffset >= GetLength()) {
+ // aOffset is outside of the buffered range.
+ return aOffset;
+ }
+ return GetLength();
+ }
+ bool IsDataCachedToEndOfResource(int64_t aOffset) override { return false; }
+ nsresult ReadFromCache(char* aBuffer, int64_t aOffset,
+ uint32_t aCount) override;
+
+ nsresult GetCachedRanges(MediaByteRangeSet& aRanges) override {
+ MOZ_ASSERT(OnThread());
+ if (mInputBuffer.GetLength()) {
+ aRanges +=
+ MediaByteRange(mInputBuffer.GetOffset(), mInputBuffer.GetLength());
+ }
+ return NS_OK;
+ }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ MOZ_ASSERT(OnThread());
+ return mInputBuffer.SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ // Used by SourceBuffer.
+ void AppendData(MediaByteBuffer* aData);
+ void AppendData(const MediaSpan& aData);
+ void Ended();
+ bool IsEnded() {
+ MOZ_ASSERT(OnThread());
+ return mEnded;
+ }
+ // Remove data from resource if it holds more than the threshold reduced by
+ // the given number of bytes. Returns amount evicted.
+ uint32_t EvictData(uint64_t aPlaybackOffset, int64_t aThresholdReduct);
+
+ // Remove data from resource before the given offset.
+ void EvictBefore(uint64_t aOffset);
+
+ // Remove all data from the resource
+ uint32_t EvictAll();
+
+ // Returns the amount of data currently retained by this resource.
+ int64_t GetSize() {
+ MOZ_ASSERT(OnThread());
+ return mInputBuffer.GetLength() - mInputBuffer.GetOffset();
+ }
+
+ const uint8_t* GetContiguousAccess(int64_t aOffset, size_t aSize) {
+ return mInputBuffer.GetContiguousAccess(aOffset, aSize);
+ }
+
+#if defined(DEBUG)
+ void Dump(const char* aPath) { mInputBuffer.Dump(aPath); }
+#endif
+
+ private:
+ virtual ~SourceBufferResource();
+ nsresult ReadAtInternal(int64_t aOffset, char* aBuffer, uint32_t aCount,
+ uint32_t* aBytes);
+
+#if defined(DEBUG)
+ const RefPtr<AbstractThread> mThread;
+ // TaskQueue methods and objects.
+ const AbstractThread* GetThread() const;
+ bool OnThread() const;
+#endif
+
+ // The buffer holding resource data.
+ ResourceQueue mInputBuffer;
+
+ bool mClosed = false;
+ bool mEnded = false;
+};
+
+} // namespace mozilla
+
+#undef UNIMPLEMENTED
+
+#endif /* MOZILLA_SOURCEBUFFERRESOURCE_H_ */
diff --git a/dom/media/mediasource/SourceBufferTask.h b/dom/media/mediasource/SourceBufferTask.h
new file mode 100644
index 0000000000..34ccba1426
--- /dev/null
+++ b/dom/media/mediasource/SourceBufferTask.h
@@ -0,0 +1,126 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_SOURCEBUFFERTASK_H_
+#define MOZILLA_SOURCEBUFFERTASK_H_
+
+#include "mozilla/MozPromise.h"
+#include "SourceBufferAttributes.h"
+#include "TimeUnits.h"
+#include "MediaResult.h"
+
+#include <utility>
+
+namespace mozilla {
+
+class SourceBufferTask {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SourceBufferTask);
+ enum class Type {
+ AppendBuffer,
+ Abort,
+ Reset,
+ RangeRemoval,
+ EvictData,
+ Detach,
+ ChangeType
+ };
+
+ typedef std::pair<bool, SourceBufferAttributes> AppendBufferResult;
+ typedef MozPromise<AppendBufferResult, MediaResult, /* IsExclusive = */ true>
+ AppendPromise;
+ typedef MozPromise<bool, nsresult, /* IsExclusive = */ true>
+ RangeRemovalPromise;
+
+ virtual Type GetType() const = 0;
+ virtual const char* GetTypeName() const = 0;
+
+ template <typename ReturnType>
+ ReturnType* As() {
+ MOZ_ASSERT(this->GetType() == ReturnType::sType);
+ return static_cast<ReturnType*>(this);
+ }
+
+ protected:
+ virtual ~SourceBufferTask() = default;
+};
+
+class AppendBufferTask : public SourceBufferTask {
+ public:
+ AppendBufferTask(already_AddRefed<MediaByteBuffer> aData,
+ const SourceBufferAttributes& aAttributes)
+ : mBuffer(aData), mAttributes(aAttributes) {}
+
+ static const Type sType = Type::AppendBuffer;
+ Type GetType() const override { return Type::AppendBuffer; }
+ const char* GetTypeName() const override { return "AppendBuffer"; }
+
+ RefPtr<MediaByteBuffer> mBuffer;
+ SourceBufferAttributes mAttributes;
+ MozPromiseHolder<AppendPromise> mPromise;
+};
+
+class AbortTask : public SourceBufferTask {
+ public:
+ static const Type sType = Type::Abort;
+ Type GetType() const override { return Type::Abort; }
+ const char* GetTypeName() const override { return "Abort"; }
+};
+
+class ResetTask : public SourceBufferTask {
+ public:
+ static const Type sType = Type::Reset;
+ Type GetType() const override { return Type::Reset; }
+ const char* GetTypeName() const override { return "Reset"; }
+};
+
+class RangeRemovalTask : public SourceBufferTask {
+ public:
+ explicit RangeRemovalTask(const media::TimeInterval& aRange)
+ : mRange(aRange) {}
+
+ static const Type sType = Type::RangeRemoval;
+ Type GetType() const override { return Type::RangeRemoval; }
+ const char* GetTypeName() const override { return "RangeRemoval"; }
+
+ media::TimeInterval mRange;
+ MozPromiseHolder<RangeRemovalPromise> mPromise;
+};
+
+class EvictDataTask : public SourceBufferTask {
+ public:
+ EvictDataTask(const media::TimeUnit& aPlaybackTime, int64_t aSizetoEvict)
+ : mPlaybackTime(aPlaybackTime), mSizeToEvict(aSizetoEvict) {}
+
+ static const Type sType = Type::EvictData;
+ Type GetType() const override { return Type::EvictData; }
+ const char* GetTypeName() const override { return "EvictData"; }
+
+ media::TimeUnit mPlaybackTime;
+ int64_t mSizeToEvict;
+};
+
+class DetachTask : public SourceBufferTask {
+ public:
+ static const Type sType = Type::Detach;
+ Type GetType() const override { return Type::Detach; }
+ const char* GetTypeName() const override { return "Detach"; }
+};
+
+class ChangeTypeTask : public SourceBufferTask {
+ public:
+ explicit ChangeTypeTask(const MediaContainerType& aType) : mType(aType) {}
+
+ static const Type sType = Type::ChangeType;
+ Type GetType() const override { return Type::ChangeType; }
+ const char* GetTypeName() const override { return "ChangeType"; }
+
+ const MediaContainerType mType;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/mediasource/TrackBuffersManager.cpp b/dom/media/mediasource/TrackBuffersManager.cpp
new file mode 100644
index 0000000000..779e1bd9d1
--- /dev/null
+++ b/dom/media/mediasource/TrackBuffersManager.cpp
@@ -0,0 +1,3092 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TrackBuffersManager.h"
+#include "ContainerParser.h"
+#include "MediaSourceDemuxer.h"
+#include "MediaSourceUtils.h"
+#include "SourceBuffer.h"
+#include "SourceBufferResource.h"
+#include "SourceBufferTask.h"
+#include "WebMDemuxer.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/ProfilerMarkers.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "nsMimeTypes.h"
+
+#ifdef MOZ_FMP4
+# include "MP4Demuxer.h"
+#endif
+
+#include <limits>
+
+extern mozilla::LogModule* GetMediaSourceLog();
+
+#define MSE_DEBUG(arg, ...) \
+ DDMOZ_LOG(GetMediaSourceLog(), mozilla::LogLevel::Debug, "::%s: " arg, \
+ __func__, ##__VA_ARGS__)
+#define MSE_DEBUGV(arg, ...) \
+ DDMOZ_LOG(GetMediaSourceLog(), mozilla::LogLevel::Verbose, "::%s: " arg, \
+ __func__, ##__VA_ARGS__)
+
+mozilla::LogModule* GetMediaSourceSamplesLog() {
+ static mozilla::LazyLogModule sLogModule("MediaSourceSamples");
+ return sLogModule;
+}
+#define SAMPLE_DEBUG(arg, ...) \
+ DDMOZ_LOG(GetMediaSourceSamplesLog(), mozilla::LogLevel::Debug, \
+ "::%s: " arg, __func__, ##__VA_ARGS__)
+
+namespace mozilla {
+
+using dom::SourceBufferAppendMode;
+using media::TimeInterval;
+using media::TimeIntervals;
+using media::TimeUnit;
+typedef SourceBufferTask::AppendBufferResult AppendBufferResult;
+typedef SourceBufferAttributes::AppendState AppendState;
+
+static const char* AppendStateToStr(AppendState aState) {
+ switch (aState) {
+ case AppendState::WAITING_FOR_SEGMENT:
+ return "WAITING_FOR_SEGMENT";
+ case AppendState::PARSING_INIT_SEGMENT:
+ return "PARSING_INIT_SEGMENT";
+ case AppendState::PARSING_MEDIA_SEGMENT:
+ return "PARSING_MEDIA_SEGMENT";
+ default:
+ return "IMPOSSIBLE";
+ }
+}
+
+static Atomic<uint32_t> sStreamSourceID(0u);
+
+class DispatchKeyNeededEvent : public Runnable {
+ public:
+ DispatchKeyNeededEvent(MediaSourceDecoder* aDecoder,
+ const nsTArray<uint8_t>& aInitData,
+ const nsString& aInitDataType)
+ : Runnable("DispatchKeyNeededEvent"),
+ mDecoder(aDecoder),
+ mInitData(aInitData.Clone()),
+ mInitDataType(aInitDataType) {}
+ NS_IMETHOD Run() override {
+ // Note: Null check the owner, as the decoder could have been shutdown
+ // since this event was dispatched.
+ MediaDecoderOwner* owner = mDecoder->GetOwner();
+ if (owner) {
+ owner->DispatchEncrypted(mInitData, mInitDataType);
+ }
+ mDecoder = nullptr;
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<MediaSourceDecoder> mDecoder;
+ nsTArray<uint8_t> mInitData;
+ nsString mInitDataType;
+};
+
+TrackBuffersManager::TrackBuffersManager(MediaSourceDecoder* aParentDecoder,
+ const MediaContainerType& aType)
+ : mBufferFull(false),
+ mFirstInitializationSegmentReceived(false),
+ mChangeTypeReceived(false),
+ mNewMediaSegmentStarted(false),
+ mActiveTrack(false),
+ mType(aType),
+ mParser(ContainerParser::CreateForMIMEType(aType)),
+ mProcessedInput(0),
+ mParentDecoder(new nsMainThreadPtrHolder<MediaSourceDecoder>(
+ "TrackBuffersManager::mParentDecoder", aParentDecoder,
+ false /* strict */)),
+ mAbstractMainThread(aParentDecoder->AbstractMainThread()),
+ mEnded(false),
+ mVideoEvictionThreshold(Preferences::GetUint(
+ "media.mediasource.eviction_threshold.video", 100 * 1024 * 1024)),
+ mAudioEvictionThreshold(Preferences::GetUint(
+ "media.mediasource.eviction_threshold.audio", 20 * 1024 * 1024)),
+ mEvictionState(EvictionState::NO_EVICTION_NEEDED),
+ mMutex("TrackBuffersManager"),
+ mTaskQueue(aParentDecoder->GetDemuxer()->GetTaskQueue()),
+ mTaskQueueCapability(Some(EventTargetCapability{mTaskQueue.get()})) {
+ MOZ_ASSERT(NS_IsMainThread(), "Must be instanciated on the main thread");
+ DDLINKCHILD("parser", mParser.get());
+}
+
+TrackBuffersManager::~TrackBuffersManager() { ShutdownDemuxers(); }
+
+RefPtr<TrackBuffersManager::AppendPromise> TrackBuffersManager::AppendData(
+ already_AddRefed<MediaByteBuffer> aData,
+ const SourceBufferAttributes& aAttributes) {
+ MOZ_ASSERT(NS_IsMainThread());
+ RefPtr<MediaByteBuffer> data(aData);
+ MSE_DEBUG("Appending %zu bytes", data->Length());
+
+ mEnded = false;
+
+ return InvokeAsync(static_cast<AbstractThread*>(GetTaskQueueSafe().get()),
+ this, __func__, &TrackBuffersManager::DoAppendData,
+ data.forget(), aAttributes);
+}
+
+RefPtr<TrackBuffersManager::AppendPromise> TrackBuffersManager::DoAppendData(
+ already_AddRefed<MediaByteBuffer> aData,
+ const SourceBufferAttributes& aAttributes) {
+ RefPtr<AppendBufferTask> task =
+ new AppendBufferTask(std::move(aData), aAttributes);
+ RefPtr<AppendPromise> p = task->mPromise.Ensure(__func__);
+ QueueTask(task);
+
+ return p;
+}
+
+void TrackBuffersManager::QueueTask(SourceBufferTask* aTask) {
+ // The source buffer is a wrapped native, it would be unlinked twice and so
+ // the TrackBuffersManager::Detach() would also be called twice. Since the
+ // detach task has been done before, we could ignore this task.
+ RefPtr<TaskQueue> taskQueue = GetTaskQueueSafe();
+ if (!taskQueue) {
+ MOZ_ASSERT(aTask->GetType() == SourceBufferTask::Type::Detach,
+ "only detach task could happen here!");
+ MSE_DEBUG("Could not queue the task '%s' without task queue",
+ aTask->GetTypeName());
+ return;
+ }
+
+ if (!taskQueue->IsCurrentThreadIn()) {
+ nsresult rv =
+ taskQueue->Dispatch(NewRunnableMethod<RefPtr<SourceBufferTask>>(
+ "TrackBuffersManager::QueueTask", this,
+ &TrackBuffersManager::QueueTask, aTask));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ return;
+ }
+ mQueue.Push(aTask);
+ ProcessTasks();
+}
+
+void TrackBuffersManager::ProcessTasks() {
+ // ProcessTask is always called OnTaskQueue, however it is possible that it is
+ // called once again after a first Detach task has run, in which case
+ // mTaskQueue would be null.
+ // This can happen under two conditions:
+ // 1- Two Detach tasks were queued in a row due to a double cycle collection.
+ // 2- An call to ProcessTasks() had queued another run of ProcessTasks while
+ // a Detach task is pending.
+ // We handle these two cases by aborting early.
+ // A second Detach task was queued, prior the first one running, ignore it.
+ if (!mTaskQueue) {
+ RefPtr<SourceBufferTask> task = mQueue.Pop();
+ if (!task) {
+ return;
+ }
+ MOZ_RELEASE_ASSERT(task->GetType() == SourceBufferTask::Type::Detach,
+ "only detach task could happen here!");
+ MSE_DEBUG("Could not process the task '%s' after detached",
+ task->GetTypeName());
+ return;
+ }
+
+ mTaskQueueCapability->AssertOnCurrentThread();
+ typedef SourceBufferTask::Type Type;
+
+ if (mCurrentTask) {
+ // Already have a task pending. ProcessTask will be scheduled once the
+ // current task complete.
+ return;
+ }
+ RefPtr<SourceBufferTask> task = mQueue.Pop();
+ if (!task) {
+ // nothing to do.
+ return;
+ }
+
+ MSE_DEBUG("Process task '%s'", task->GetTypeName());
+ switch (task->GetType()) {
+ case Type::AppendBuffer:
+ mCurrentTask = task;
+ if (!mInputBuffer || mInputBuffer->IsEmpty()) {
+ // Note: we reset mInputBuffer here to ensure it doesn't grow unbounded.
+ mInputBuffer.reset();
+ mInputBuffer = Some(MediaSpan(task->As<AppendBufferTask>()->mBuffer));
+ } else {
+ // mInputBuffer wasn't empty, so we can't just reset it, but we move
+ // the data into a new buffer to clear out data no longer in the span.
+ MSE_DEBUG(
+ "mInputBuffer not empty during append -- data will be copied to "
+ "new buffer. mInputBuffer->Length()=%zu "
+ "mInputBuffer->Buffer()->Length()=%zu",
+ mInputBuffer->Length(), mInputBuffer->Buffer()->Length());
+ const RefPtr<MediaByteBuffer> newBuffer{new MediaByteBuffer()};
+ // Set capacity outside of ctor to let us explicitly handle OOM.
+ const size_t newCapacity =
+ mInputBuffer->Length() +
+ task->As<AppendBufferTask>()->mBuffer->Length();
+ if (!newBuffer->SetCapacity(newCapacity, fallible)) {
+ RejectAppend(NS_ERROR_OUT_OF_MEMORY, __func__);
+ return;
+ }
+ // Use infallible appends as we've already set capacity above.
+ newBuffer->AppendElements(mInputBuffer->Elements(),
+ mInputBuffer->Length());
+ newBuffer->AppendElements(*task->As<AppendBufferTask>()->mBuffer);
+ mInputBuffer = Some(MediaSpan(newBuffer));
+ }
+ mSourceBufferAttributes = MakeUnique<SourceBufferAttributes>(
+ task->As<AppendBufferTask>()->mAttributes);
+ mAppendWindow = TimeInterval(
+ TimeUnit::FromSeconds(
+ mSourceBufferAttributes->GetAppendWindowStart()),
+ TimeUnit::FromSeconds(mSourceBufferAttributes->GetAppendWindowEnd()));
+ ScheduleSegmentParserLoop();
+ break;
+ case Type::RangeRemoval: {
+ bool rv = CodedFrameRemoval(task->As<RangeRemovalTask>()->mRange);
+ task->As<RangeRemovalTask>()->mPromise.Resolve(rv, __func__);
+ break;
+ }
+ case Type::EvictData:
+ DoEvictData(task->As<EvictDataTask>()->mPlaybackTime,
+ task->As<EvictDataTask>()->mSizeToEvict);
+ break;
+ case Type::Abort:
+ // not handled yet, and probably never.
+ break;
+ case Type::Reset:
+ CompleteResetParserState();
+ break;
+ case Type::Detach:
+ mCurrentInputBuffer = nullptr;
+ MOZ_DIAGNOSTIC_ASSERT(mQueue.Length() == 0,
+ "Detach task must be the last");
+ mVideoTracks.Reset();
+ mAudioTracks.Reset();
+ ShutdownDemuxers();
+ ResetTaskQueue();
+ return;
+ case Type::ChangeType:
+ MOZ_RELEASE_ASSERT(!mCurrentTask);
+ MSE_DEBUG("Processing type change from %s -> %s",
+ mType.OriginalString().get(),
+ task->As<ChangeTypeTask>()->mType.OriginalString().get());
+ mType = task->As<ChangeTypeTask>()->mType;
+ mChangeTypeReceived = true;
+ mInitData = nullptr;
+ // A new input buffer will be created once we receive a new init segment.
+ // The first segment received after a changeType call must be an init
+ // segment.
+ mCurrentInputBuffer = nullptr;
+ CompleteResetParserState();
+ break;
+ default:
+ NS_WARNING("Invalid Task");
+ }
+ TaskQueueFromTaskQueue()->Dispatch(
+ NewRunnableMethod("TrackBuffersManager::ProcessTasks", this,
+ &TrackBuffersManager::ProcessTasks));
+}
+
+// The MSE spec requires that we abort the current SegmentParserLoop
+// which is then followed by a call to ResetParserState.
+// However due to our asynchronous design this causes inherent difficulties.
+// As the spec behaviour is non deterministic anyway, we instead process all
+// pending frames found in the input buffer.
+void TrackBuffersManager::AbortAppendData() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_DEBUG("");
+
+ QueueTask(new AbortTask());
+}
+
+void TrackBuffersManager::ResetParserState(
+ SourceBufferAttributes& aAttributes) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_DEBUG("");
+
+ // Spec states:
+ // 1. If the append state equals PARSING_MEDIA_SEGMENT and the input buffer
+ // contains some complete coded frames, then run the coded frame processing
+ // algorithm until all of these complete coded frames have been processed.
+ // However, we will wait until all coded frames have been processed regardless
+ // of the value of append state.
+ QueueTask(new ResetTask());
+
+ // ResetParserState has some synchronous steps that much be performed now.
+ // The remaining steps will be performed once the ResetTask gets executed.
+
+ // 6. If the mode attribute equals "sequence", then set the group start
+ // timestamp to the group end timestamp
+ if (aAttributes.GetAppendMode() == SourceBufferAppendMode::Sequence) {
+ aAttributes.SetGroupStartTimestamp(aAttributes.GetGroupEndTimestamp());
+ }
+ // 8. Set append state to WAITING_FOR_SEGMENT.
+ aAttributes.SetAppendState(AppendState::WAITING_FOR_SEGMENT);
+}
+
+RefPtr<TrackBuffersManager::RangeRemovalPromise>
+TrackBuffersManager::RangeRemoval(TimeUnit aStart, TimeUnit aEnd) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_DEBUG("From %.2f to %.2f", aStart.ToSeconds(), aEnd.ToSeconds());
+
+ mEnded = false;
+
+ return InvokeAsync(static_cast<AbstractThread*>(GetTaskQueueSafe().get()),
+ this, __func__,
+ &TrackBuffersManager::CodedFrameRemovalWithPromise,
+ TimeInterval(aStart, aEnd));
+}
+
+TrackBuffersManager::EvictDataResult TrackBuffersManager::EvictData(
+ const TimeUnit& aPlaybackTime, int64_t aSize) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (aSize > EvictionThreshold()) {
+ // We're adding more data than we can hold.
+ return EvictDataResult::BUFFER_FULL;
+ }
+ const int64_t toEvict = GetSize() + aSize - EvictionThreshold();
+
+ const uint32_t canEvict =
+ Evictable(HasVideo() ? TrackInfo::kVideoTrack : TrackInfo::kAudioTrack);
+
+ MSE_DEBUG("currentTime=%" PRId64 " buffered=%" PRId64
+ "kB, eviction threshold=%" PRId64
+ "kB, "
+ "evict=%" PRId64 "kB canevict=%" PRIu32 "kB",
+ aPlaybackTime.ToMicroseconds(), GetSize() / 1024,
+ EvictionThreshold() / 1024, toEvict / 1024, canEvict / 1024);
+
+ if (toEvict <= 0) {
+ mEvictionState = EvictionState::NO_EVICTION_NEEDED;
+ return EvictDataResult::NO_DATA_EVICTED;
+ }
+
+ EvictDataResult result;
+
+ if (mBufferFull && mEvictionState == EvictionState::EVICTION_COMPLETED &&
+ canEvict < uint32_t(toEvict)) {
+ // Our buffer is currently full. We will make another eviction attempt.
+ // However, the current appendBuffer will fail as we can't know ahead of
+ // time if the eviction will later succeed.
+ result = EvictDataResult::BUFFER_FULL;
+ } else {
+ mEvictionState = EvictionState::EVICTION_NEEDED;
+ result = EvictDataResult::NO_DATA_EVICTED;
+ }
+ MSE_DEBUG("Reached our size limit, schedule eviction of %" PRId64
+ " bytes (%s)",
+ toEvict,
+ result == EvictDataResult::BUFFER_FULL ? "buffer full"
+ : "no data evicted");
+ QueueTask(new EvictDataTask(aPlaybackTime, toEvict));
+
+ return result;
+}
+
+void TrackBuffersManager::ChangeType(const MediaContainerType& aType) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ QueueTask(new ChangeTypeTask(aType));
+}
+
+TimeIntervals TrackBuffersManager::Buffered() const {
+ MSE_DEBUG("");
+
+ // http://w3c.github.io/media-source/index.html#widl-SourceBuffer-buffered
+
+ MutexAutoLock mut(mMutex);
+ nsTArray<const TimeIntervals*> tracks;
+ if (HasVideo()) {
+ tracks.AppendElement(&mVideoBufferedRanges);
+ }
+ if (HasAudio()) {
+ tracks.AppendElement(&mAudioBufferedRanges);
+ }
+
+ // 2. Let highest end time be the largest track buffer ranges end time across
+ // all the track buffers managed by this SourceBuffer object.
+ TimeUnit highestEndTime = HighestEndTime(tracks);
+
+ // 3. Let intersection ranges equal a TimeRange object containing a single
+ // range from 0 to highest end time.
+ TimeIntervals intersection{
+ TimeInterval(TimeUnit::FromSeconds(0), highestEndTime)};
+
+ // 4. For each track buffer managed by this SourceBuffer, run the following
+ // steps:
+ // 1. Let track ranges equal the track buffer ranges for the current track
+ // buffer.
+ for (const TimeIntervals* trackRanges : tracks) {
+ // 2. If readyState is "ended", then set the end time on the last range in
+ // track ranges to highest end time.
+ // 3. Let new intersection ranges equal the intersection between the
+ // intersection ranges and the track ranges.
+ if (mEnded) {
+ TimeIntervals tR = *trackRanges;
+ tR.Add(TimeInterval(tR.GetEnd(), highestEndTime));
+ intersection.Intersection(tR);
+ } else {
+ intersection.Intersection(*trackRanges);
+ }
+ }
+ return intersection;
+}
+
+int64_t TrackBuffersManager::GetSize() const { return mSizeSourceBuffer; }
+
+void TrackBuffersManager::Ended() { mEnded = true; }
+
+void TrackBuffersManager::Detach() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_DEBUG("");
+ QueueTask(new DetachTask());
+}
+
+void TrackBuffersManager::CompleteResetParserState() {
+ mTaskQueueCapability->AssertOnCurrentThread();
+ AUTO_PROFILER_LABEL("TrackBuffersManager::CompleteResetParserState",
+ MEDIA_PLAYBACK);
+ MSE_DEBUG("");
+
+ // We shouldn't change mInputDemuxer while a demuxer init/reset request is
+ // being processed. See bug 1239983.
+ MOZ_DIAGNOSTIC_ASSERT(!mDemuxerInitRequest.Exists(),
+ "Previous AppendBuffer didn't complete");
+
+ for (auto& track : GetTracksList()) {
+ // 2. Unset the last decode timestamp on all track buffers.
+ // 3. Unset the last frame duration on all track buffers.
+ // 4. Unset the highest end timestamp on all track buffers.
+ // 5. Set the need random access point flag on all track buffers to true.
+ track->ResetAppendState();
+
+ // if we have been aborted, we may have pending frames that we are going
+ // to discard now.
+ track->mQueuedSamples.Clear();
+ }
+
+ // 7. Remove all bytes from the input buffer.
+ mPendingInputBuffer.reset();
+ mInputBuffer.reset();
+ if (mCurrentInputBuffer) {
+ mCurrentInputBuffer->EvictAll();
+ // The demuxer will be recreated during the next run of SegmentParserLoop.
+ // As such we don't need to notify it that data has been removed.
+ mCurrentInputBuffer = new SourceBufferResource();
+ }
+
+ // We could be left with a demuxer in an unusable state. It needs to be
+ // recreated. Unless we have a pending changeType operation, we store in the
+ // InputBuffer an init segment which will be parsed during the next Segment
+ // Parser Loop and a new demuxer will be created and initialized.
+ // If we are in the middle of a changeType operation, then we do not have an
+ // init segment yet. The next appendBuffer operation will need to provide such
+ // init segment.
+ if (mFirstInitializationSegmentReceived && !mChangeTypeReceived) {
+ MOZ_ASSERT(mInitData && mInitData->Length(),
+ "we must have an init segment");
+ // The aim here is really to destroy our current demuxer.
+ CreateDemuxerforMIMEType();
+ // Recreate our input buffer. We can't directly assign the initData buffer
+ // to mInputBuffer as it will get modified in the Segment Parser Loop.
+ mInputBuffer = Some(MediaSpan::WithCopyOf(mInitData));
+ RecreateParser(true);
+ } else {
+ RecreateParser(false);
+ }
+}
+
+int64_t TrackBuffersManager::EvictionThreshold() const {
+ if (HasVideo()) {
+ return mVideoEvictionThreshold;
+ }
+ return mAudioEvictionThreshold;
+}
+
+void TrackBuffersManager::DoEvictData(const TimeUnit& aPlaybackTime,
+ int64_t aSizeToEvict) {
+ mTaskQueueCapability->AssertOnCurrentThread();
+ AUTO_PROFILER_LABEL("TrackBuffersManager::DoEvictData", MEDIA_PLAYBACK);
+
+ mEvictionState = EvictionState::EVICTION_COMPLETED;
+
+ // Video is what takes the most space, only evict there if we have video.
+ auto& track = HasVideo() ? mVideoTracks : mAudioTracks;
+ const auto& buffer = track.GetTrackBuffer();
+ if (buffer.IsEmpty()) {
+ // Buffer has been emptied while the eviction was queued, nothing to do.
+ return;
+ }
+ if (track.mBufferedRanges.IsEmpty()) {
+ MSE_DEBUG(
+ "DoEvictData running with no buffered ranges. 0 duration data likely "
+ "present in our buffer(s). Evicting all data!");
+ // We have no buffered ranges, but may still have data. This happens if the
+ // buffer is full of 0 duration data. Normal removal procedures don't clear
+ // 0 duration data, so blow away all our data.
+ RemoveAllCodedFrames();
+ return;
+ }
+ // Remove any data we've already played, or before the next sample to be
+ // demuxed whichever is lowest.
+ TimeUnit lowerLimit = std::min(track.mNextSampleTime, aPlaybackTime);
+ uint32_t lastKeyFrameIndex = 0;
+ int64_t toEvict = aSizeToEvict;
+ int64_t partialEvict = 0;
+ for (uint32_t i = 0; i < buffer.Length(); i++) {
+ const auto& frame = buffer[i];
+ if (frame->mKeyframe) {
+ lastKeyFrameIndex = i;
+ toEvict -= partialEvict;
+ if (toEvict < 0) {
+ break;
+ }
+ partialEvict = 0;
+ }
+ if (frame->GetEndTime() >= lowerLimit) {
+ break;
+ }
+ partialEvict += frame->ComputedSizeOfIncludingThis();
+ }
+
+ const int64_t finalSize = mSizeSourceBuffer - aSizeToEvict;
+
+ if (lastKeyFrameIndex > 0) {
+ MSE_DEBUG("Step1. Evicting %" PRId64 " bytes prior currentTime",
+ aSizeToEvict - toEvict);
+ TimeUnit start = track.mBufferedRanges[0].mStart;
+ TimeUnit end =
+ buffer[lastKeyFrameIndex]->mTime - TimeUnit::FromMicroseconds(1);
+ if (end > start) {
+ CodedFrameRemoval(TimeInterval(start, end));
+ }
+ }
+
+ if (mSizeSourceBuffer <= finalSize) {
+ return;
+ }
+
+ toEvict = mSizeSourceBuffer - finalSize;
+
+ // See if we can evict data into the future.
+ // We do not evict data from the currently used buffered interval.
+
+ TimeUnit currentPosition = std::max(aPlaybackTime, track.mNextSampleTime);
+ TimeIntervals futureBuffered(
+ TimeInterval(currentPosition, TimeUnit::FromInfinity()));
+ futureBuffered.Intersection(track.mBufferedRanges);
+ futureBuffered.SetFuzz(MediaSourceDemuxer::EOS_FUZZ / 2);
+ if (futureBuffered.Length() <= 1) {
+ // We have one continuous segment ahead of us:
+ // nothing further can be evicted.
+ return;
+ }
+
+ // Don't evict before the end of the current segment
+ TimeUnit upperLimit = futureBuffered[0].mEnd;
+ uint32_t evictedFramesStartIndex = buffer.Length();
+ for (int32_t i = buffer.Length() - 1; i >= 0; i--) {
+ const auto& frame = buffer[i];
+ if (frame->mTime <= upperLimit || toEvict < 0) {
+ // We've reached a frame that shouldn't be evicted -> Evict after it ->
+ // i+1. Or the previous loop reached the eviction threshold -> Evict from
+ // it -> i+1.
+ evictedFramesStartIndex = i + 1;
+ break;
+ }
+ toEvict -= frame->ComputedSizeOfIncludingThis();
+ }
+ if (evictedFramesStartIndex < buffer.Length()) {
+ MSE_DEBUG("Step2. Evicting %" PRId64 " bytes from trailing data",
+ mSizeSourceBuffer - finalSize - toEvict);
+ CodedFrameRemoval(TimeInterval(buffer[evictedFramesStartIndex]->mTime,
+ TimeUnit::FromInfinity()));
+ }
+}
+
+RefPtr<TrackBuffersManager::RangeRemovalPromise>
+TrackBuffersManager::CodedFrameRemovalWithPromise(TimeInterval aInterval) {
+ mTaskQueueCapability->AssertOnCurrentThread();
+
+ RefPtr<RangeRemovalTask> task = new RangeRemovalTask(aInterval);
+ RefPtr<RangeRemovalPromise> p = task->mPromise.Ensure(__func__);
+ QueueTask(task);
+
+ return p;
+}
+
+bool TrackBuffersManager::CodedFrameRemoval(TimeInterval aInterval) {
+ MOZ_ASSERT(OnTaskQueue());
+ AUTO_PROFILER_LABEL("TrackBuffersManager::CodedFrameRemoval", MEDIA_PLAYBACK);
+ MSE_DEBUG("From %.2fs to %.2f", aInterval.mStart.ToSeconds(),
+ aInterval.mEnd.ToSeconds());
+
+#if DEBUG
+ if (HasVideo()) {
+ MSE_DEBUG("before video ranges=%s",
+ DumpTimeRangesRaw(mVideoTracks.mBufferedRanges).get());
+ }
+ if (HasAudio()) {
+ MSE_DEBUG("before audio ranges=%s",
+ DumpTimeRangesRaw(mAudioTracks.mBufferedRanges).get());
+ }
+#endif
+
+ // 1. Let start be the starting presentation timestamp for the removal range.
+ TimeUnit start = aInterval.mStart;
+ // 2. Let end be the end presentation timestamp for the removal range.
+ TimeUnit end = aInterval.mEnd;
+
+ bool dataRemoved = false;
+
+ // 3. For each track buffer in this source buffer, run the following steps:
+ for (auto track : GetTracksList()) {
+ MSE_DEBUGV("Processing %s track", track->mInfo->mMimeType.get());
+ // 1. Let remove end timestamp be the current value of duration
+ // See bug: https://www.w3.org/Bugs/Public/show_bug.cgi?id=28727
+ // At worse we will remove all frames until the end, unless a key frame is
+ // found between the current interval's end and the trackbuffer's end.
+ TimeUnit removeEndTimestamp = track->mBufferedRanges.GetEnd();
+
+ if (start > removeEndTimestamp) {
+ // Nothing to remove.
+ continue;
+ }
+
+ // 2. If this track buffer has a random access point timestamp that is
+ // greater than or equal to end, then update remove end timestamp to that
+ // random access point timestamp.
+ if (end < track->mBufferedRanges.GetEnd()) {
+ for (auto& frame : track->GetTrackBuffer()) {
+ if (frame->mKeyframe && frame->mTime >= end) {
+ removeEndTimestamp = frame->mTime;
+ break;
+ }
+ }
+ }
+
+ // 3. Remove all media data, from this track buffer, that contain starting
+ // timestamps greater than or equal to start and less than the remove end
+ // timestamp.
+ // 4. Remove decoding dependencies of the coded frames removed in the
+ // previous step: Remove all coded frames between the coded frames removed
+ // in the previous step and the next random access point after those removed
+ // frames.
+ TimeIntervals removedInterval{TimeInterval(start, removeEndTimestamp)};
+ RemoveFrames(removedInterval, *track, 0, RemovalMode::kRemoveFrame);
+
+ // 5. If this object is in activeSourceBuffers, the current playback
+ // position is greater than or equal to start and less than the remove end
+ // timestamp, and HTMLMediaElement.readyState is greater than HAVE_METADATA,
+ // then set the HTMLMediaElement.readyState attribute to HAVE_METADATA and
+ // stall playback. This will be done by the MDSM during playback.
+ // TODO properly, so it works even if paused.
+ }
+
+ UpdateBufferedRanges();
+
+ // Update our reported total size.
+ mSizeSourceBuffer = mVideoTracks.mSizeBuffer + mAudioTracks.mSizeBuffer;
+
+ // 4. If buffer full flag equals true and this object is ready to accept more
+ // bytes, then set the buffer full flag to false.
+ if (mBufferFull && mSizeSourceBuffer < EvictionThreshold()) {
+ mBufferFull = false;
+ }
+
+ return dataRemoved;
+}
+
+void TrackBuffersManager::RemoveAllCodedFrames() {
+ // This is similar to RemoveCodedFrames, but will attempt to remove ALL
+ // the frames. This is not to spec, as explained below at step 3.1. Steps
+ // below coincide with Remove Coded Frames algorithm from the spec.
+ MSE_DEBUG("RemoveAllCodedFrames called.");
+ MOZ_ASSERT(OnTaskQueue());
+ AUTO_PROFILER_LABEL("TrackBuffersManager::RemoveAllCodedFrames",
+ MEDIA_PLAYBACK);
+
+ // 1. Let start be the starting presentation timestamp for the removal range.
+ TimeUnit start{};
+ // 2. Let end be the end presentation timestamp for the removal range.
+ TimeUnit end = TimeUnit::FromMicroseconds(1);
+ // Find an end time such that our range will include every frame in every
+ // track. We do this by setting the end of our interval to the largest end
+ // time seen + 1 microsecond.
+ for (TrackData* track : GetTracksList()) {
+ for (auto& frame : track->GetTrackBuffer()) {
+ MOZ_ASSERT(frame->mTime >= start,
+ "Shouldn't have frame at negative time!");
+ TimeUnit frameEnd = frame->mTime + frame->mDuration;
+ if (frameEnd > end) {
+ end = frameEnd + TimeUnit::FromMicroseconds(1);
+ }
+ }
+ }
+
+ // 3. For each track buffer in this source buffer, run the following steps:
+ TimeIntervals removedInterval{TimeInterval(start, end)};
+ for (TrackData* track : GetTracksList()) {
+ // 1. Let remove end timestamp be the current value of duration
+ // ^ It's off spec, but we ignore this in order to clear 0 duration frames.
+ // If we don't ignore this rule and our buffer is full of 0 duration frames
+ // at timestamp n, we get an eviction range of [0, n). When we get to step
+ // 3.3 below, the 0 duration frames will not be evicted because their
+ // timestamp is not less than remove end timestamp -- it will in fact be
+ // equal to remove end timestamp.
+ //
+ // 2. If this track buffer has a random access point timestamp that is
+ // greater than or equal to end, then update remove end timestamp to that
+ // random access point timestamp.
+ // ^ We've made sure end > any sample's timestamp, so can skip this.
+ //
+ // 3. Remove all media data, from this track buffer, that contain starting
+ // timestamps greater than or equal to start and less than the remove end
+ // timestamp.
+ // 4. Remove decoding dependencies of the coded frames removed in the
+ // previous step: Remove all coded frames between the coded frames removed
+ // in the previous step and the next random access point after those removed
+ // frames.
+
+ // This should remove every frame in the track because removedInterval was
+ // constructed such that every frame in any track falls into that interval.
+ RemoveFrames(removedInterval, *track, 0, RemovalMode::kRemoveFrame);
+
+ // 5. If this object is in activeSourceBuffers, the current playback
+ // position is greater than or equal to start and less than the remove end
+ // timestamp, and HTMLMediaElement.readyState is greater than HAVE_METADATA,
+ // then set the HTMLMediaElement.readyState attribute to HAVE_METADATA and
+ // stall playback. This will be done by the MDSM during playback.
+ // TODO properly, so it works even if paused.
+ }
+
+ UpdateBufferedRanges();
+#ifdef DEBUG
+ {
+ MutexAutoLock lock(mMutex);
+ MOZ_ASSERT(
+ mAudioBufferedRanges.IsEmpty(),
+ "Should have no buffered video ranges after evicting everything.");
+ MOZ_ASSERT(
+ mVideoBufferedRanges.IsEmpty(),
+ "Should have no buffered video ranges after evicting everything.");
+ }
+#endif
+ mSizeSourceBuffer = mVideoTracks.mSizeBuffer + mAudioTracks.mSizeBuffer;
+ MOZ_ASSERT(mSizeSourceBuffer == 0,
+ "Buffer should be empty after evicting everything!");
+ if (mBufferFull && mSizeSourceBuffer < EvictionThreshold()) {
+ mBufferFull = false;
+ }
+}
+
+void TrackBuffersManager::UpdateBufferedRanges() {
+ MutexAutoLock mut(mMutex);
+
+ mVideoBufferedRanges = mVideoTracks.mSanitizedBufferedRanges;
+ mAudioBufferedRanges = mAudioTracks.mSanitizedBufferedRanges;
+
+#if DEBUG
+ if (HasVideo()) {
+ MSE_DEBUG("after video ranges=%s",
+ DumpTimeRangesRaw(mVideoTracks.mBufferedRanges).get());
+ }
+ if (HasAudio()) {
+ MSE_DEBUG("after audio ranges=%s",
+ DumpTimeRangesRaw(mAudioTracks.mBufferedRanges).get());
+ }
+#endif
+}
+
+void TrackBuffersManager::SegmentParserLoop() {
+ MOZ_ASSERT(OnTaskQueue());
+ AUTO_PROFILER_LABEL("TrackBuffersManager::SegmentParserLoop", MEDIA_PLAYBACK);
+
+ while (true) {
+ // 1. If the input buffer is empty, then jump to the need more data step
+ // below.
+ if (!mInputBuffer || mInputBuffer->IsEmpty()) {
+ NeedMoreData();
+ return;
+ }
+ // 2. If the input buffer contains bytes that violate the SourceBuffer
+ // byte stream format specification, then run the append error algorithm
+ // with the decode error parameter set to true and abort this algorithm.
+ // TODO
+
+ // 3. Remove any bytes that the byte stream format specifications say must
+ // be ignored from the start of the input buffer. We do not remove bytes
+ // from our input buffer. Instead we enforce that our ContainerParser is
+ // able to skip over all data that is supposed to be ignored.
+
+ // 4. If the append state equals WAITING_FOR_SEGMENT, then run the following
+ // steps:
+ if (mSourceBufferAttributes->GetAppendState() ==
+ AppendState::WAITING_FOR_SEGMENT) {
+ MediaResult haveInitSegment =
+ mParser->IsInitSegmentPresent(*mInputBuffer);
+ if (NS_SUCCEEDED(haveInitSegment)) {
+ SetAppendState(AppendState::PARSING_INIT_SEGMENT);
+ if (mFirstInitializationSegmentReceived && !mChangeTypeReceived) {
+ // This is a new initialization segment. Obsolete the old one.
+ RecreateParser(false);
+ }
+ continue;
+ }
+ MediaResult haveMediaSegment =
+ mParser->IsMediaSegmentPresent(*mInputBuffer);
+ if (NS_SUCCEEDED(haveMediaSegment)) {
+ SetAppendState(AppendState::PARSING_MEDIA_SEGMENT);
+ mNewMediaSegmentStarted = true;
+ continue;
+ }
+ // We have neither an init segment nor a media segment.
+ // Check if it was invalid data.
+ if (haveInitSegment != NS_ERROR_NOT_AVAILABLE) {
+ MSE_DEBUG("Found invalid data.");
+ RejectAppend(haveInitSegment, __func__);
+ return;
+ }
+ if (haveMediaSegment != NS_ERROR_NOT_AVAILABLE) {
+ MSE_DEBUG("Found invalid data.");
+ RejectAppend(haveMediaSegment, __func__);
+ return;
+ }
+ MSE_DEBUG("Found incomplete data.");
+ NeedMoreData();
+ return;
+ }
+
+ MOZ_ASSERT(mSourceBufferAttributes->GetAppendState() ==
+ AppendState::PARSING_INIT_SEGMENT ||
+ mSourceBufferAttributes->GetAppendState() ==
+ AppendState::PARSING_MEDIA_SEGMENT);
+
+ TimeUnit start, end;
+ MediaResult newData = NS_ERROR_NOT_AVAILABLE;
+
+ if (mSourceBufferAttributes->GetAppendState() ==
+ AppendState::PARSING_INIT_SEGMENT ||
+ (mSourceBufferAttributes->GetAppendState() ==
+ AppendState::PARSING_MEDIA_SEGMENT &&
+ mFirstInitializationSegmentReceived && !mChangeTypeReceived)) {
+ newData = mParser->ParseStartAndEndTimestamps(*mInputBuffer, start, end);
+ if (NS_FAILED(newData) && newData.Code() != NS_ERROR_NOT_AVAILABLE) {
+ RejectAppend(newData, __func__);
+ return;
+ }
+ mProcessedInput += mInputBuffer->Length();
+ }
+
+ // 5. If the append state equals PARSING_INIT_SEGMENT, then run the
+ // following steps:
+ if (mSourceBufferAttributes->GetAppendState() ==
+ AppendState::PARSING_INIT_SEGMENT) {
+ if (mParser->InitSegmentRange().IsEmpty()) {
+ mInputBuffer.reset();
+ NeedMoreData();
+ return;
+ }
+ InitializationSegmentReceived();
+ return;
+ }
+ if (mSourceBufferAttributes->GetAppendState() ==
+ AppendState::PARSING_MEDIA_SEGMENT) {
+ // 1. If the first initialization segment received flag is false, then run
+ // the append error algorithm with the decode error parameter set to
+ // true and abort this algorithm.
+ // Or we are in the process of changeType, in which case we must first
+ // get an init segment before getting a media segment.
+ if (!mFirstInitializationSegmentReceived || mChangeTypeReceived) {
+ RejectAppend(NS_ERROR_FAILURE, __func__);
+ return;
+ }
+
+ // We can't feed some demuxers (WebMDemuxer) with data that do not have
+ // monotonizally increasing timestamps. So we check if we have a
+ // discontinuity from the previous segment parsed.
+ // If so, recreate a new demuxer to ensure that the demuxer is only fed
+ // monotonically increasing data.
+ if (mNewMediaSegmentStarted) {
+ if (NS_SUCCEEDED(newData) && mLastParsedEndTime.isSome() &&
+ start < mLastParsedEndTime.ref()) {
+ MSE_DEBUG("Re-creating demuxer");
+ ResetDemuxingState();
+ return;
+ }
+ if (NS_SUCCEEDED(newData) || !mParser->MediaSegmentRange().IsEmpty()) {
+ if (mPendingInputBuffer) {
+ // We now have a complete media segment header. We can resume
+ // parsing the data.
+ AppendDataToCurrentInputBuffer(*mPendingInputBuffer);
+ mPendingInputBuffer.reset();
+ }
+ mNewMediaSegmentStarted = false;
+ } else {
+ // We don't have any data to demux yet, stash aside the data.
+ // This also handles the case:
+ // 2. If the input buffer does not contain a complete media segment
+ // header yet, then jump to the need more data step below.
+ if (!mPendingInputBuffer) {
+ mPendingInputBuffer = Some(MediaSpan(*mInputBuffer));
+ } else {
+ // Note we reset mInputBuffer below, so this won't end up appending
+ // the contents of mInputBuffer to itself.
+ mPendingInputBuffer->Append(*mInputBuffer);
+ }
+
+ mInputBuffer.reset();
+ NeedMoreData();
+ return;
+ }
+ }
+
+ // 3. If the input buffer contains one or more complete coded frames, then
+ // run the coded frame processing algorithm.
+ RefPtr<TrackBuffersManager> self = this;
+ CodedFrameProcessing()
+ ->Then(
+ TaskQueueFromTaskQueue(), __func__,
+ [self](bool aNeedMoreData) {
+ self->mTaskQueueCapability->AssertOnCurrentThread();
+ self->mProcessingRequest.Complete();
+ if (aNeedMoreData) {
+ self->NeedMoreData();
+ } else {
+ self->ScheduleSegmentParserLoop();
+ }
+ },
+ [self](const MediaResult& aRejectValue) {
+ self->mTaskQueueCapability->AssertOnCurrentThread();
+ self->mProcessingRequest.Complete();
+ self->RejectAppend(aRejectValue, __func__);
+ })
+ ->Track(mProcessingRequest);
+ return;
+ }
+ }
+}
+
+void TrackBuffersManager::NeedMoreData() {
+ MSE_DEBUG("");
+ MOZ_DIAGNOSTIC_ASSERT(mCurrentTask &&
+ mCurrentTask->GetType() ==
+ SourceBufferTask::Type::AppendBuffer);
+ MOZ_DIAGNOSTIC_ASSERT(mSourceBufferAttributes);
+
+ mCurrentTask->As<AppendBufferTask>()->mPromise.Resolve(
+ SourceBufferTask::AppendBufferResult(mActiveTrack,
+ *mSourceBufferAttributes),
+ __func__);
+ mSourceBufferAttributes = nullptr;
+ mCurrentTask = nullptr;
+ ProcessTasks();
+}
+
+void TrackBuffersManager::RejectAppend(const MediaResult& aRejectValue,
+ const char* aName) {
+ MSE_DEBUG("rv=%" PRIu32, static_cast<uint32_t>(aRejectValue.Code()));
+ MOZ_DIAGNOSTIC_ASSERT(mCurrentTask &&
+ mCurrentTask->GetType() ==
+ SourceBufferTask::Type::AppendBuffer);
+
+ mCurrentTask->As<AppendBufferTask>()->mPromise.Reject(aRejectValue, __func__);
+ mSourceBufferAttributes = nullptr;
+ mCurrentTask = nullptr;
+ ProcessTasks();
+}
+
+void TrackBuffersManager::ScheduleSegmentParserLoop() {
+ MOZ_ASSERT(OnTaskQueue());
+ TaskQueueFromTaskQueue()->Dispatch(
+ NewRunnableMethod("TrackBuffersManager::SegmentParserLoop", this,
+ &TrackBuffersManager::SegmentParserLoop));
+}
+
+void TrackBuffersManager::ShutdownDemuxers() {
+ if (mVideoTracks.mDemuxer) {
+ mVideoTracks.mDemuxer->BreakCycles();
+ mVideoTracks.mDemuxer = nullptr;
+ }
+ if (mAudioTracks.mDemuxer) {
+ mAudioTracks.mDemuxer->BreakCycles();
+ mAudioTracks.mDemuxer = nullptr;
+ }
+ // We shouldn't change mInputDemuxer while a demuxer init/reset request is
+ // being processed. See bug 1239983.
+ MOZ_DIAGNOSTIC_ASSERT(!mDemuxerInitRequest.Exists());
+ mInputDemuxer = nullptr;
+ mLastParsedEndTime.reset();
+}
+
+void TrackBuffersManager::CreateDemuxerforMIMEType() {
+ mTaskQueueCapability->AssertOnCurrentThread();
+ MSE_DEBUG("mType.OriginalString=%s", mType.OriginalString().get());
+ ShutdownDemuxers();
+
+ if (mType.Type() == MEDIAMIMETYPE(VIDEO_WEBM) ||
+ mType.Type() == MEDIAMIMETYPE(AUDIO_WEBM)) {
+ mInputDemuxer =
+ new WebMDemuxer(mCurrentInputBuffer, true /* IsMediaSource*/);
+ DDLINKCHILD("demuxer", mInputDemuxer.get());
+ return;
+ }
+
+#ifdef MOZ_FMP4
+ if (mType.Type() == MEDIAMIMETYPE(VIDEO_MP4) ||
+ mType.Type() == MEDIAMIMETYPE(AUDIO_MP4)) {
+ mInputDemuxer = new MP4Demuxer(mCurrentInputBuffer);
+ DDLINKCHILD("demuxer", mInputDemuxer.get());
+ return;
+ }
+#endif
+ NS_WARNING("Not supported (yet)");
+}
+
+// We reset the demuxer by creating a new one and initializing it.
+void TrackBuffersManager::ResetDemuxingState() {
+ MOZ_ASSERT(OnTaskQueue());
+ MOZ_ASSERT(mParser && mParser->HasInitData());
+ AUTO_PROFILER_LABEL("TrackBuffersManager::ResetDemuxingState",
+ MEDIA_PLAYBACK);
+ RecreateParser(true);
+ mCurrentInputBuffer = new SourceBufferResource();
+ // The demuxer isn't initialized yet ; we don't want to notify it
+ // that data has been appended yet ; so we simply append the init segment
+ // to the resource.
+ mCurrentInputBuffer->AppendData(mParser->InitData());
+ CreateDemuxerforMIMEType();
+ if (!mInputDemuxer) {
+ RejectAppend(NS_ERROR_FAILURE, __func__);
+ return;
+ }
+ mInputDemuxer->Init()
+ ->Then(TaskQueueFromTaskQueue(), __func__, this,
+ &TrackBuffersManager::OnDemuxerResetDone,
+ &TrackBuffersManager::OnDemuxerInitFailed)
+ ->Track(mDemuxerInitRequest);
+}
+
+void TrackBuffersManager::OnDemuxerResetDone(const MediaResult& aResult) {
+ MOZ_ASSERT(OnTaskQueue());
+ mDemuxerInitRequest.Complete();
+
+ if (NS_FAILED(aResult) && StaticPrefs::media_playback_warnings_as_errors()) {
+ RejectAppend(aResult, __func__);
+ return;
+ }
+
+ // mInputDemuxer shouldn't have been destroyed while a demuxer init/reset
+ // request was being processed. See bug 1239983.
+ MOZ_DIAGNOSTIC_ASSERT(mInputDemuxer);
+
+ if (aResult != NS_OK && mParentDecoder) {
+ RefPtr<TrackBuffersManager> self = this;
+ mAbstractMainThread->Dispatch(NS_NewRunnableFunction(
+ "TrackBuffersManager::OnDemuxerResetDone", [self, aResult]() {
+ if (self->mParentDecoder && self->mParentDecoder->GetOwner()) {
+ self->mParentDecoder->GetOwner()->DecodeWarning(aResult);
+ }
+ }));
+ }
+
+ // Recreate track demuxers.
+ uint32_t numVideos = mInputDemuxer->GetNumberTracks(TrackInfo::kVideoTrack);
+ if (numVideos) {
+ // We currently only handle the first video track.
+ mVideoTracks.mDemuxer =
+ mInputDemuxer->GetTrackDemuxer(TrackInfo::kVideoTrack, 0);
+ MOZ_ASSERT(mVideoTracks.mDemuxer);
+ DDLINKCHILD("video demuxer", mVideoTracks.mDemuxer.get());
+ }
+
+ uint32_t numAudios = mInputDemuxer->GetNumberTracks(TrackInfo::kAudioTrack);
+ if (numAudios) {
+ // We currently only handle the first audio track.
+ mAudioTracks.mDemuxer =
+ mInputDemuxer->GetTrackDemuxer(TrackInfo::kAudioTrack, 0);
+ MOZ_ASSERT(mAudioTracks.mDemuxer);
+ DDLINKCHILD("audio demuxer", mAudioTracks.mDemuxer.get());
+ }
+
+ if (mPendingInputBuffer) {
+ // We had a partial media segment header stashed aside.
+ // Reparse its content so we can continue parsing the current input buffer.
+ TimeUnit start, end;
+ mParser->ParseStartAndEndTimestamps(*mPendingInputBuffer, start, end);
+ mProcessedInput += mPendingInputBuffer->Length();
+ }
+
+ SegmentParserLoop();
+}
+
+void TrackBuffersManager::AppendDataToCurrentInputBuffer(
+ const MediaSpan& aData) {
+ MOZ_ASSERT(mCurrentInputBuffer);
+ mCurrentInputBuffer->AppendData(aData);
+ mInputDemuxer->NotifyDataArrived();
+}
+
+void TrackBuffersManager::InitializationSegmentReceived() {
+ MOZ_ASSERT(OnTaskQueue());
+ MOZ_ASSERT(mParser->HasCompleteInitData());
+ AUTO_PROFILER_LABEL("TrackBuffersManager::InitializationSegmentReceived",
+ MEDIA_PLAYBACK);
+
+ int64_t endInit = mParser->InitSegmentRange().mEnd;
+ if (mInputBuffer->Length() > mProcessedInput ||
+ int64_t(mProcessedInput - mInputBuffer->Length()) > endInit) {
+ // Something is not quite right with the data appended. Refuse it.
+ RejectAppend(MediaResult(NS_ERROR_FAILURE,
+ "Invalid state following initialization segment"),
+ __func__);
+ return;
+ }
+
+ mCurrentInputBuffer = new SourceBufferResource();
+ // The demuxer isn't initialized yet ; we don't want to notify it
+ // that data has been appended yet ; so we simply append the init segment
+ // to the resource.
+ mCurrentInputBuffer->AppendData(mParser->InitData());
+ uint32_t length = endInit - (mProcessedInput - mInputBuffer->Length());
+ MOZ_RELEASE_ASSERT(length <= mInputBuffer->Length());
+ mInputBuffer->RemoveFront(length);
+ CreateDemuxerforMIMEType();
+ if (!mInputDemuxer) {
+ NS_WARNING("TODO type not supported");
+ RejectAppend(NS_ERROR_DOM_NOT_SUPPORTED_ERR, __func__);
+ return;
+ }
+ mInputDemuxer->Init()
+ ->Then(TaskQueueFromTaskQueue(), __func__, this,
+ &TrackBuffersManager::OnDemuxerInitDone,
+ &TrackBuffersManager::OnDemuxerInitFailed)
+ ->Track(mDemuxerInitRequest);
+}
+
+bool TrackBuffersManager::IsRepeatInitData(
+ const MediaInfo& aNewMediaInfo) const {
+ MOZ_ASSERT(OnTaskQueue());
+ if (!mInitData) {
+ // There is no previous init data, so this cannot be a repeat.
+ return false;
+ }
+
+ if (mChangeTypeReceived) {
+ // If we're received change type we want to reprocess init data.
+ return false;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(mInitData, "Init data should be non-null");
+ if (*mInitData == *mParser->InitData()) {
+ // We have previous init data, and it's the same binary data as we've just
+ // parsed.
+ return true;
+ }
+
+ // At this point the binary data doesn't match, but it's possible to have the
+ // different binary representations for the same logical init data. These
+ // checks can be revised as we encounter such cases in the wild.
+
+ bool audioInfoIsRepeat = false;
+ if (aNewMediaInfo.HasAudio()) {
+ if (!mAudioTracks.mLastInfo) {
+ // There is no old audio info, so this can't be a repeat.
+ return false;
+ }
+ audioInfoIsRepeat =
+ *mAudioTracks.mLastInfo->GetAsAudioInfo() == aNewMediaInfo.mAudio;
+ if (!aNewMediaInfo.HasVideo()) {
+ // Only have audio.
+ return audioInfoIsRepeat;
+ }
+ }
+
+ bool videoInfoIsRepeat = false;
+ if (aNewMediaInfo.HasVideo()) {
+ if (!mVideoTracks.mLastInfo) {
+ // There is no old video info, so this can't be a repeat.
+ return false;
+ }
+ videoInfoIsRepeat =
+ *mVideoTracks.mLastInfo->GetAsVideoInfo() == aNewMediaInfo.mVideo;
+ if (!aNewMediaInfo.HasAudio()) {
+ // Only have video.
+ return videoInfoIsRepeat;
+ }
+ }
+
+ if (audioInfoIsRepeat && videoInfoIsRepeat) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ aNewMediaInfo.HasVideo() && aNewMediaInfo.HasAudio(),
+ "This should only be reachable if audio and video are present");
+ // Video + audio are present and both have the same init data.
+ return true;
+ }
+
+ return false;
+}
+
+void TrackBuffersManager::OnDemuxerInitDone(const MediaResult& aResult) {
+ mTaskQueueCapability->AssertOnCurrentThread();
+ MOZ_DIAGNOSTIC_ASSERT(mInputDemuxer, "mInputDemuxer has been destroyed");
+ AUTO_PROFILER_LABEL("TrackBuffersManager::OnDemuxerInitDone", MEDIA_PLAYBACK);
+
+ mDemuxerInitRequest.Complete();
+
+ if (NS_FAILED(aResult) && StaticPrefs::media_playback_warnings_as_errors()) {
+ RejectAppend(aResult, __func__);
+ return;
+ }
+
+ MediaInfo info;
+
+ uint32_t numVideos = mInputDemuxer->GetNumberTracks(TrackInfo::kVideoTrack);
+ if (numVideos) {
+ // We currently only handle the first video track.
+ mVideoTracks.mDemuxer =
+ mInputDemuxer->GetTrackDemuxer(TrackInfo::kVideoTrack, 0);
+ MOZ_ASSERT(mVideoTracks.mDemuxer);
+ DDLINKCHILD("video demuxer", mVideoTracks.mDemuxer.get());
+ info.mVideo = *mVideoTracks.mDemuxer->GetInfo()->GetAsVideoInfo();
+ info.mVideo.mTrackId = 2;
+ }
+
+ uint32_t numAudios = mInputDemuxer->GetNumberTracks(TrackInfo::kAudioTrack);
+ if (numAudios) {
+ // We currently only handle the first audio track.
+ mAudioTracks.mDemuxer =
+ mInputDemuxer->GetTrackDemuxer(TrackInfo::kAudioTrack, 0);
+ MOZ_ASSERT(mAudioTracks.mDemuxer);
+ DDLINKCHILD("audio demuxer", mAudioTracks.mDemuxer.get());
+ info.mAudio = *mAudioTracks.mDemuxer->GetInfo()->GetAsAudioInfo();
+ info.mAudio.mTrackId = 1;
+ }
+
+ TimeUnit videoDuration = numVideos ? info.mVideo.mDuration : TimeUnit::Zero();
+ TimeUnit audioDuration = numAudios ? info.mAudio.mDuration : TimeUnit::Zero();
+
+ TimeUnit duration = std::max(videoDuration, audioDuration);
+ // 1. Update the duration attribute if it currently equals NaN.
+ // Those steps are performed by the MediaSourceDecoder::SetInitialDuration
+ mAbstractMainThread->Dispatch(NewRunnableMethod<TimeUnit>(
+ "MediaSourceDecoder::SetInitialDuration", mParentDecoder.get(),
+ &MediaSourceDecoder::SetInitialDuration,
+ !duration.IsZero() ? duration : TimeUnit::FromInfinity()));
+
+ // 2. If the initialization segment has no audio, video, or text tracks, then
+ // run the append error algorithm with the decode error parameter set to true
+ // and abort these steps.
+ if (!numVideos && !numAudios) {
+ RejectAppend(NS_ERROR_FAILURE, __func__);
+ return;
+ }
+
+ // 3. If the first initialization segment received flag is true, then run the
+ // following steps:
+ if (mFirstInitializationSegmentReceived) {
+ if (numVideos != mVideoTracks.mNumTracks ||
+ numAudios != mAudioTracks.mNumTracks) {
+ RejectAppend(NS_ERROR_FAILURE, __func__);
+ return;
+ }
+ // 1. If more than one track for a single type are present (ie 2 audio
+ // tracks), then the Track IDs match the ones in the first initialization
+ // segment.
+ // TODO
+ // 2. Add the appropriate track descriptions from this initialization
+ // segment to each of the track buffers.
+ // TODO
+ // 3. Set the need random access point flag on all track buffers to true.
+ mVideoTracks.mNeedRandomAccessPoint = true;
+ mAudioTracks.mNeedRandomAccessPoint = true;
+ }
+
+ // Check if we've received the same init data again. Some streams will
+ // resend the same data. In these cases we don't need to change the stream
+ // id as it's the same stream. Doing so would recreate decoders, possibly
+ // leading to gaps in audio and/or video (see bug 1450952).
+ bool isRepeatInitData = IsRepeatInitData(info);
+
+ MOZ_ASSERT(mFirstInitializationSegmentReceived || !isRepeatInitData,
+ "Should never detect repeat init data for first segment!");
+
+ // If we have new init data we configure and set track info as needed. If we
+ // have repeat init data we carry forward our existing track info.
+ if (!isRepeatInitData) {
+ // Increase our stream id.
+ uint32_t streamID = sStreamSourceID++;
+
+ // 4. Let active track flag equal false.
+ bool activeTrack = false;
+
+ // 5. If the first initialization segment received flag is false, then run
+ // the following steps:
+ if (!mFirstInitializationSegmentReceived) {
+ MSE_DEBUG("Get first init data");
+ mAudioTracks.mNumTracks = numAudios;
+ // TODO:
+ // 1. If the initialization segment contains tracks with codecs the user
+ // agent does not support, then run the append error algorithm with the
+ // decode error parameter set to true and abort these steps.
+
+ // 2. For each audio track in the initialization segment, run following
+ // steps: for (uint32_t i = 0; i < numAudios; i++) {
+ if (numAudios) {
+ // 1. Let audio byte stream track ID be the Track ID for the current
+ // track being processed.
+ // 2. Let audio language be a BCP 47 language tag for the language
+ // specified in the initialization segment for this track or an empty
+ // string if no language info is present.
+ // 3. If audio language equals an empty string or the 'und' BCP 47
+ // value, then run the default track language algorithm with
+ // byteStreamTrackID set to audio byte stream track ID and type set to
+ // "audio" and assign the value returned by the algorithm to audio
+ // language.
+ // 4. Let audio label be a label specified in the initialization segment
+ // for this track or an empty string if no label info is present.
+ // 5. If audio label equals an empty string, then run the default track
+ // label algorithm with byteStreamTrackID set to audio byte stream track
+ // ID and type set to "audio" and assign the value returned by the
+ // algorithm to audio label.
+ // 6. Let audio kinds be an array of kind strings specified in the
+ // initialization segment for this track or an empty array if no kind
+ // information is provided.
+ // 7. If audio kinds equals an empty array, then run the default track
+ // kinds algorithm with byteStreamTrackID set to audio byte stream track
+ // ID and type set to "audio" and assign the value returned by the
+ // algorithm to audio kinds.
+ // 8. For each value in audio kinds, run the following steps:
+ // 1. Let current audio kind equal the value from audio kinds for this
+ // iteration of the loop.
+ // 2. Let new audio track be a new AudioTrack object.
+ // 3. Generate a unique ID and assign it to the id property on new
+ // audio track.
+ // 4. Assign audio language to the language property on new audio
+ // track.
+ // 5. Assign audio label to the label property on new audio track.
+ // 6. Assign current audio kind to the kind property on new audio
+ // track.
+ // 7. If audioTracks.length equals 0, then run the following steps:
+ // 1. Set the enabled property on new audio track to true.
+ // 2. Set active track flag to true.
+ activeTrack = true;
+ // 8. Add new audio track to the audioTracks attribute on this
+ // SourceBuffer object.
+ // 9. Queue a task to fire a trusted event named addtrack, that does
+ // not bubble and is not cancelable, and that uses the TrackEvent
+ // interface, at the AudioTrackList object referenced by the
+ // audioTracks attribute on this SourceBuffer object.
+ // 10. Add new audio track to the audioTracks attribute on the
+ // HTMLMediaElement.
+ // 11. Queue a task to fire a trusted event named addtrack, that does
+ // not bubble and is not cancelable, and that uses the TrackEvent
+ // interface, at the AudioTrackList object referenced by the
+ // audioTracks attribute on the HTMLMediaElement.
+ mAudioTracks.mBuffers.AppendElement(TrackBuffer());
+ // 10. Add the track description for this track to the track buffer.
+ mAudioTracks.mInfo = new TrackInfoSharedPtr(info.mAudio, streamID);
+ mAudioTracks.mLastInfo = mAudioTracks.mInfo;
+ }
+
+ mVideoTracks.mNumTracks = numVideos;
+ // 3. For each video track in the initialization segment, run following
+ // steps: for (uint32_t i = 0; i < numVideos; i++) {
+ if (numVideos) {
+ // 1. Let video byte stream track ID be the Track ID for the current
+ // track being processed.
+ // 2. Let video language be a BCP 47 language tag for the language
+ // specified in the initialization segment for this track or an empty
+ // string if no language info is present.
+ // 3. If video language equals an empty string or the 'und' BCP 47
+ // value, then run the default track language algorithm with
+ // byteStreamTrackID set to video byte stream track ID and type set to
+ // "video" and assign the value returned by the algorithm to video
+ // language.
+ // 4. Let video label be a label specified in the initialization segment
+ // for this track or an empty string if no label info is present.
+ // 5. If video label equals an empty string, then run the default track
+ // label algorithm with byteStreamTrackID set to video byte stream track
+ // ID and type set to "video" and assign the value returned by the
+ // algorithm to video label.
+ // 6. Let video kinds be an array of kind strings specified in the
+ // initialization segment for this track or an empty array if no kind
+ // information is provided.
+ // 7. If video kinds equals an empty array, then run the default track
+ // kinds algorithm with byteStreamTrackID set to video byte stream track
+ // ID and type set to "video" and assign the value returned by the
+ // algorithm to video kinds.
+ // 8. For each value in video kinds, run the following steps:
+ // 1. Let current video kind equal the value from video kinds for this
+ // iteration of the loop.
+ // 2. Let new video track be a new VideoTrack object.
+ // 3. Generate a unique ID and assign it to the id property on new
+ // video track.
+ // 4. Assign video language to the language property on new video
+ // track.
+ // 5. Assign video label to the label property on new video track.
+ // 6. Assign current video kind to the kind property on new video
+ // track.
+ // 7. If videoTracks.length equals 0, then run the following steps:
+ // 1. Set the selected property on new video track to true.
+ // 2. Set active track flag to true.
+ activeTrack = true;
+ // 8. Add new video track to the videoTracks attribute on this
+ // SourceBuffer object.
+ // 9. Queue a task to fire a trusted event named addtrack, that does
+ // not bubble and is not cancelable, and that uses the TrackEvent
+ // interface, at the VideoTrackList object referenced by the
+ // videoTracks attribute on this SourceBuffer object.
+ // 10. Add new video track to the videoTracks attribute on the
+ // HTMLMediaElement.
+ // 11. Queue a task to fire a trusted event named addtrack, that does
+ // not bubble and is not cancelable, and that uses the TrackEvent
+ // interface, at the VideoTrackList object referenced by the
+ // videoTracks attribute on the HTMLMediaElement.
+ mVideoTracks.mBuffers.AppendElement(TrackBuffer());
+ // 10. Add the track description for this track to the track buffer.
+ mVideoTracks.mInfo = new TrackInfoSharedPtr(info.mVideo, streamID);
+ mVideoTracks.mLastInfo = mVideoTracks.mInfo;
+ }
+ // 4. For each text track in the initialization segment, run following
+ // steps:
+ // 5. If active track flag equals true, then run the following steps:
+ // This is handled by SourceBuffer once the promise is resolved.
+ if (activeTrack) {
+ mActiveTrack = true;
+ }
+
+ // 6. Set first initialization segment received flag to true.
+ mFirstInitializationSegmentReceived = true;
+ } else {
+ MSE_DEBUG("Get new init data");
+ mAudioTracks.mLastInfo = new TrackInfoSharedPtr(info.mAudio, streamID);
+ mVideoTracks.mLastInfo = new TrackInfoSharedPtr(info.mVideo, streamID);
+ }
+
+ UniquePtr<EncryptionInfo> crypto = mInputDemuxer->GetCrypto();
+ if (crypto && crypto->IsEncrypted()) {
+ // Try and dispatch 'encrypted'. Won't go if ready state still
+ // HAVE_NOTHING.
+ for (uint32_t i = 0; i < crypto->mInitDatas.Length(); i++) {
+ nsCOMPtr<nsIRunnable> r = new DispatchKeyNeededEvent(
+ mParentDecoder, crypto->mInitDatas[i].mInitData,
+ crypto->mInitDatas[i].mType);
+ mAbstractMainThread->Dispatch(r.forget());
+ }
+ info.mCrypto = *crypto;
+ // We clear our crypto init data array, so the MediaFormatReader will
+ // not emit an encrypted event for the same init data again.
+ info.mCrypto.mInitDatas.Clear();
+ }
+
+ {
+ MutexAutoLock mut(mMutex);
+ mInfo = info;
+ }
+ }
+ // We now have a valid init data ; we can store it for later use.
+ mInitData = mParser->InitData();
+
+ // We have now completed the changeType operation.
+ mChangeTypeReceived = false;
+
+ // 3. Remove the initialization segment bytes from the beginning of the input
+ // buffer. This step has already been done in InitializationSegmentReceived
+ // when we transferred the content into mCurrentInputBuffer.
+ mCurrentInputBuffer->EvictAll();
+ mInputDemuxer->NotifyDataRemoved();
+ RecreateParser(true);
+
+ // 4. Set append state to WAITING_FOR_SEGMENT.
+ SetAppendState(AppendState::WAITING_FOR_SEGMENT);
+ // 5. Jump to the loop top step above.
+ ScheduleSegmentParserLoop();
+
+ if (aResult != NS_OK && mParentDecoder) {
+ RefPtr<TrackBuffersManager> self = this;
+ mAbstractMainThread->Dispatch(NS_NewRunnableFunction(
+ "TrackBuffersManager::OnDemuxerInitDone", [self, aResult]() {
+ if (self->mParentDecoder && self->mParentDecoder->GetOwner()) {
+ self->mParentDecoder->GetOwner()->DecodeWarning(aResult);
+ }
+ }));
+ }
+}
+
+void TrackBuffersManager::OnDemuxerInitFailed(const MediaResult& aError) {
+ mTaskQueueCapability->AssertOnCurrentThread();
+ MSE_DEBUG("");
+ MOZ_ASSERT(aError != NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA);
+ mDemuxerInitRequest.Complete();
+
+ RejectAppend(aError, __func__);
+}
+
+RefPtr<TrackBuffersManager::CodedFrameProcessingPromise>
+TrackBuffersManager::CodedFrameProcessing() {
+ MOZ_ASSERT(OnTaskQueue());
+ MOZ_ASSERT(mProcessingPromise.IsEmpty());
+ AUTO_PROFILER_LABEL("TrackBuffersManager::CodedFrameProcessing",
+ MEDIA_PLAYBACK);
+
+ MediaByteRange mediaRange = mParser->MediaSegmentRange();
+ if (mediaRange.IsEmpty()) {
+ AppendDataToCurrentInputBuffer(*mInputBuffer);
+ mInputBuffer.reset();
+ } else {
+ MOZ_ASSERT(mProcessedInput >= mInputBuffer->Length());
+ if (int64_t(mProcessedInput - mInputBuffer->Length()) > mediaRange.mEnd) {
+ // Something is not quite right with the data appended. Refuse it.
+ // This would typically happen if the previous media segment was partial
+ // yet a new complete media segment was added.
+ return CodedFrameProcessingPromise::CreateAndReject(NS_ERROR_FAILURE,
+ __func__);
+ }
+ // The mediaRange is offset by the init segment position previously added.
+ uint32_t length =
+ mediaRange.mEnd - (mProcessedInput - mInputBuffer->Length());
+ if (!length) {
+ // We've completed our earlier media segment and no new data is to be
+ // processed. This happens with some containers that can't detect that a
+ // media segment is ending until a new one starts.
+ RefPtr<CodedFrameProcessingPromise> p =
+ mProcessingPromise.Ensure(__func__);
+ CompleteCodedFrameProcessing();
+ return p;
+ }
+ AppendDataToCurrentInputBuffer(mInputBuffer->To(length));
+ mInputBuffer->RemoveFront(length);
+ }
+
+ RefPtr<CodedFrameProcessingPromise> p = mProcessingPromise.Ensure(__func__);
+
+ DoDemuxVideo();
+
+ return p;
+}
+
+void TrackBuffersManager::OnDemuxFailed(TrackType aTrack,
+ const MediaResult& aError) {
+ MOZ_ASSERT(OnTaskQueue());
+ MSE_DEBUG("Failed to demux %s, failure:%s",
+ aTrack == TrackType::kVideoTrack ? "video" : "audio",
+ aError.ErrorName().get());
+ switch (aError.Code()) {
+ case NS_ERROR_DOM_MEDIA_END_OF_STREAM:
+ case NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA:
+ if (aTrack == TrackType::kVideoTrack) {
+ DoDemuxAudio();
+ } else {
+ CompleteCodedFrameProcessing();
+ }
+ break;
+ default:
+ RejectProcessing(aError, __func__);
+ break;
+ }
+}
+
+void TrackBuffersManager::DoDemuxVideo() {
+ MOZ_ASSERT(OnTaskQueue());
+ if (!HasVideo()) {
+ DoDemuxAudio();
+ return;
+ }
+ mVideoTracks.mDemuxer->GetSamples(-1)
+ ->Then(TaskQueueFromTaskQueue(), __func__, this,
+ &TrackBuffersManager::OnVideoDemuxCompleted,
+ &TrackBuffersManager::OnVideoDemuxFailed)
+ ->Track(mVideoTracks.mDemuxRequest);
+}
+
+void TrackBuffersManager::MaybeDispatchEncryptedEvent(
+ const nsTArray<RefPtr<MediaRawData>>& aSamples) {
+ // Try and dispatch 'encrypted'. Won't go if ready state still HAVE_NOTHING.
+ for (const RefPtr<MediaRawData>& sample : aSamples) {
+ for (const nsTArray<uint8_t>& initData : sample->mCrypto.mInitDatas) {
+ nsCOMPtr<nsIRunnable> r = new DispatchKeyNeededEvent(
+ mParentDecoder, initData, sample->mCrypto.mInitDataType);
+ mAbstractMainThread->Dispatch(r.forget());
+ }
+ }
+}
+
+void TrackBuffersManager::OnVideoDemuxCompleted(
+ RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples) {
+ mTaskQueueCapability->AssertOnCurrentThread();
+ MSE_DEBUG("%zu video samples demuxed", aSamples->GetSamples().Length());
+ mVideoTracks.mDemuxRequest.Complete();
+ mVideoTracks.mQueuedSamples.AppendElements(aSamples->GetSamples());
+
+ MaybeDispatchEncryptedEvent(aSamples->GetSamples());
+ DoDemuxAudio();
+}
+
+void TrackBuffersManager::DoDemuxAudio() {
+ MOZ_ASSERT(OnTaskQueue());
+ if (!HasAudio()) {
+ CompleteCodedFrameProcessing();
+ return;
+ }
+ mAudioTracks.mDemuxer->GetSamples(-1)
+ ->Then(TaskQueueFromTaskQueue(), __func__, this,
+ &TrackBuffersManager::OnAudioDemuxCompleted,
+ &TrackBuffersManager::OnAudioDemuxFailed)
+ ->Track(mAudioTracks.mDemuxRequest);
+}
+
+void TrackBuffersManager::OnAudioDemuxCompleted(
+ RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples) {
+ mTaskQueueCapability->AssertOnCurrentThread();
+ MSE_DEBUG("%zu audio samples demuxed", aSamples->GetSamples().Length());
+ // When using MSE, it's possible for each fragments to have their own
+ // duration, with a duration that is incorrectly rounded. Ignore the trimming
+ // information set by the demuxer to ensure a continous playback.
+ for (const auto& sample : aSamples->GetSamples()) {
+ sample->mOriginalPresentationWindow = Nothing();
+ }
+ mAudioTracks.mDemuxRequest.Complete();
+ mAudioTracks.mQueuedSamples.AppendElements(aSamples->GetSamples());
+ CompleteCodedFrameProcessing();
+
+ MaybeDispatchEncryptedEvent(aSamples->GetSamples());
+}
+
+void TrackBuffersManager::CompleteCodedFrameProcessing() {
+ MOZ_ASSERT(OnTaskQueue());
+ AUTO_PROFILER_LABEL("TrackBuffersManager::CompleteCodedFrameProcessing",
+ MEDIA_PLAYBACK);
+
+ // 1. For each coded frame in the media segment run the following steps:
+ // Coded Frame Processing steps 1.1 to 1.21.
+
+ if (mSourceBufferAttributes->GetAppendMode() ==
+ SourceBufferAppendMode::Sequence &&
+ mVideoTracks.mQueuedSamples.Length() &&
+ mAudioTracks.mQueuedSamples.Length()) {
+ // When we are in sequence mode, the order in which we process the frames is
+ // important as it determines the future value of timestampOffset.
+ // So we process the earliest sample first. See bug 1293576.
+ TimeInterval videoInterval =
+ PresentationInterval(mVideoTracks.mQueuedSamples);
+ TimeInterval audioInterval =
+ PresentationInterval(mAudioTracks.mQueuedSamples);
+ if (audioInterval.mStart < videoInterval.mStart) {
+ ProcessFrames(mAudioTracks.mQueuedSamples, mAudioTracks);
+ ProcessFrames(mVideoTracks.mQueuedSamples, mVideoTracks);
+ } else {
+ ProcessFrames(mVideoTracks.mQueuedSamples, mVideoTracks);
+ ProcessFrames(mAudioTracks.mQueuedSamples, mAudioTracks);
+ }
+ } else {
+ ProcessFrames(mVideoTracks.mQueuedSamples, mVideoTracks);
+ ProcessFrames(mAudioTracks.mQueuedSamples, mAudioTracks);
+ }
+
+#if defined(DEBUG)
+ if (HasVideo()) {
+ const auto& track = mVideoTracks.GetTrackBuffer();
+ MOZ_ASSERT(track.IsEmpty() || track[0]->mKeyframe);
+ for (uint32_t i = 1; i < track.Length(); i++) {
+ MOZ_ASSERT(
+ (track[i - 1]->mTrackInfo->GetID() == track[i]->mTrackInfo->GetID() &&
+ track[i - 1]->mTimecode <= track[i]->mTimecode) ||
+ track[i]->mKeyframe);
+ }
+ }
+ if (HasAudio()) {
+ const auto& track = mAudioTracks.GetTrackBuffer();
+ MOZ_ASSERT(track.IsEmpty() || track[0]->mKeyframe);
+ for (uint32_t i = 1; i < track.Length(); i++) {
+ MOZ_ASSERT(
+ (track[i - 1]->mTrackInfo->GetID() == track[i]->mTrackInfo->GetID() &&
+ track[i - 1]->mTimecode <= track[i]->mTimecode) ||
+ track[i]->mKeyframe);
+ }
+ }
+#endif
+
+ mVideoTracks.mQueuedSamples.Clear();
+ mAudioTracks.mQueuedSamples.Clear();
+
+ UpdateBufferedRanges();
+
+ // Update our reported total size.
+ mSizeSourceBuffer = mVideoTracks.mSizeBuffer + mAudioTracks.mSizeBuffer;
+
+ // Return to step 6.4 of Segment Parser Loop algorithm
+ // 4. If this SourceBuffer is full and cannot accept more media data, then set
+ // the buffer full flag to true.
+ if (mSizeSourceBuffer >= EvictionThreshold()) {
+ mBufferFull = true;
+ }
+
+ // 5. If the input buffer does not contain a complete media segment, then jump
+ // to the need more data step below.
+ if (mParser->MediaSegmentRange().IsEmpty()) {
+ ResolveProcessing(true, __func__);
+ return;
+ }
+
+ mLastParsedEndTime = Some(std::max(mAudioTracks.mLastParsedEndTime,
+ mVideoTracks.mLastParsedEndTime));
+
+ // 6. Remove the media segment bytes from the beginning of the input buffer.
+ // Clear our demuxer from any already processed data.
+ int64_t safeToEvict =
+ std::min(HasVideo() ? mVideoTracks.mDemuxer->GetEvictionOffset(
+ mVideoTracks.mLastParsedEndTime)
+ : INT64_MAX,
+ HasAudio() ? mAudioTracks.mDemuxer->GetEvictionOffset(
+ mAudioTracks.mLastParsedEndTime)
+ : INT64_MAX);
+ mCurrentInputBuffer->EvictBefore(safeToEvict);
+
+ mInputDemuxer->NotifyDataRemoved();
+ RecreateParser(true);
+
+ // 7. Set append state to WAITING_FOR_SEGMENT.
+ SetAppendState(AppendState::WAITING_FOR_SEGMENT);
+
+ // 8. Jump to the loop top step above.
+ ResolveProcessing(false, __func__);
+}
+
+void TrackBuffersManager::RejectProcessing(const MediaResult& aRejectValue,
+ const char* aName) {
+ mProcessingPromise.RejectIfExists(aRejectValue, __func__);
+}
+
+void TrackBuffersManager::ResolveProcessing(bool aResolveValue,
+ const char* aName) {
+ mProcessingPromise.ResolveIfExists(aResolveValue, __func__);
+}
+
+void TrackBuffersManager::CheckSequenceDiscontinuity(
+ const TimeUnit& aPresentationTime) {
+ if (mSourceBufferAttributes->GetAppendMode() ==
+ SourceBufferAppendMode::Sequence &&
+ mSourceBufferAttributes->HaveGroupStartTimestamp()) {
+ mSourceBufferAttributes->SetTimestampOffset(
+ mSourceBufferAttributes->GetGroupStartTimestamp() - aPresentationTime);
+ mSourceBufferAttributes->SetGroupEndTimestamp(
+ mSourceBufferAttributes->GetGroupStartTimestamp());
+ mVideoTracks.mNeedRandomAccessPoint = true;
+ mAudioTracks.mNeedRandomAccessPoint = true;
+ mSourceBufferAttributes->ResetGroupStartTimestamp();
+ }
+}
+
+TimeInterval TrackBuffersManager::PresentationInterval(
+ const TrackBuffer& aSamples) const {
+ TimeInterval presentationInterval =
+ TimeInterval(aSamples[0]->mTime, aSamples[0]->GetEndTime());
+
+ for (uint32_t i = 1; i < aSamples.Length(); i++) {
+ auto& sample = aSamples[i];
+ presentationInterval = presentationInterval.Span(
+ TimeInterval(sample->mTime, sample->GetEndTime()));
+ }
+ return presentationInterval;
+}
+
+void TrackBuffersManager::ProcessFrames(TrackBuffer& aSamples,
+ TrackData& aTrackData) {
+ AUTO_PROFILER_LABEL("TrackBuffersManager::ProcessFrames", MEDIA_PLAYBACK);
+ if (!aSamples.Length()) {
+ return;
+ }
+
+ // 1. If generate timestamps flag equals true
+ // Let presentation timestamp equal 0.
+ // Otherwise
+ // Let presentation timestamp be a double precision floating point
+ // representation of the coded frame's presentation timestamp in seconds.
+ TimeUnit presentationTimestamp = mSourceBufferAttributes->mGenerateTimestamps
+ ? TimeUnit::Zero()
+ : aSamples[0]->mTime;
+
+ // 3. If mode equals "sequence" and group start timestamp is set, then run the
+ // following steps:
+ CheckSequenceDiscontinuity(presentationTimestamp);
+
+ // 5. Let track buffer equal the track buffer that the coded frame will be
+ // added to.
+ auto& trackBuffer = aTrackData;
+
+ TimeIntervals samplesRange;
+ uint32_t sizeNewSamples = 0;
+ TrackBuffer samples; // array that will contain the frames to be added
+ // to our track buffer.
+
+ // We assume that no frames are contiguous within a media segment and as such
+ // don't need to check for discontinuity except for the first frame and should
+ // a frame be ignored due to the target window.
+ bool needDiscontinuityCheck = true;
+
+ // Highest presentation time seen in samples block.
+ TimeUnit highestSampleTime;
+
+ if (aSamples.Length()) {
+ aTrackData.mLastParsedEndTime = TimeUnit();
+ }
+
+ auto addToSamples = [&](MediaRawData* aSample,
+ const TimeInterval& aInterval) {
+ aSample->mTime = aInterval.mStart;
+ aSample->mDuration = aInterval.Length();
+ aSample->mTrackInfo = trackBuffer.mLastInfo;
+ MOZ_DIAGNOSTIC_ASSERT(aSample->HasValidTime());
+ samplesRange += aInterval;
+ sizeNewSamples += aSample->ComputedSizeOfIncludingThis();
+ samples.AppendElement(aSample);
+ };
+
+ // Will be set to the last frame dropped due to being outside mAppendWindow.
+ // It will be added prior the first following frame which can be added to the
+ // track buffer.
+ // This sample will be set with a duration of only 1us which will cause it to
+ // be dropped once returned by the decoder.
+ // This sample is required to "prime" the decoder so that the following frame
+ // can be fully decoded.
+ RefPtr<MediaRawData> previouslyDroppedSample;
+ for (auto& sample : aSamples) {
+ const TimeUnit sampleEndTime = sample->GetEndTime();
+ if (sampleEndTime > aTrackData.mLastParsedEndTime) {
+ aTrackData.mLastParsedEndTime = sampleEndTime;
+ }
+
+ // We perform step 10 right away as we can't do anything should a keyframe
+ // be needed until we have one.
+
+ // 10. If the need random access point flag on track buffer equals true,
+ // then run the following steps:
+ if (trackBuffer.mNeedRandomAccessPoint) {
+ // 1. If the coded frame is not a random access point, then drop the coded
+ // frame and jump to the top of the loop to start processing the next
+ // coded frame.
+ if (!sample->mKeyframe) {
+ previouslyDroppedSample = nullptr;
+ continue;
+ }
+ // 2. Set the need random access point flag on track buffer to false.
+ trackBuffer.mNeedRandomAccessPoint = false;
+ }
+
+ // We perform step 1,2 and 4 at once:
+ // 1. If generate timestamps flag equals true:
+ // Let presentation timestamp equal 0.
+ // Let decode timestamp equal 0.
+ // Otherwise:
+ // Let presentation timestamp be a double precision floating point
+ // representation of the coded frame's presentation timestamp in seconds.
+ // Let decode timestamp be a double precision floating point
+ // representation of the coded frame's decode timestamp in seconds.
+
+ // 2. Let frame duration be a double precision floating point representation
+ // of the coded frame's duration in seconds. Step 3 is performed earlier or
+ // when a discontinuity has been detected.
+ // 4. If timestampOffset is not 0, then run the following steps:
+
+ TimeUnit sampleTime = sample->mTime;
+ TimeUnit sampleTimecode = sample->mTimecode;
+ TimeUnit sampleDuration = sample->mDuration;
+ // Keep the timestamp, set by js, in the time base of the container.
+ TimeUnit timestampOffset =
+ mSourceBufferAttributes->GetTimestampOffset().ToBase(sample->mTime);
+
+ TimeInterval sampleInterval =
+ mSourceBufferAttributes->mGenerateTimestamps
+ ? TimeInterval(timestampOffset, timestampOffset + sampleDuration)
+ : TimeInterval(timestampOffset + sampleTime,
+ timestampOffset + sampleTime + sampleDuration);
+ TimeUnit decodeTimestamp = mSourceBufferAttributes->mGenerateTimestamps
+ ? timestampOffset
+ : timestampOffset + sampleTimecode;
+
+ SAMPLE_DEBUG(
+ "Processing %s frame [%" PRId64 ",%" PRId64 "] (adjusted:[%" PRId64
+ ",%" PRId64 "]), dts:%" PRId64 ", duration:%" PRId64 ", kf:%d)",
+ aTrackData.mInfo->mMimeType.get(), sample->mTime.ToMicroseconds(),
+ sample->GetEndTime().ToMicroseconds(),
+ sampleInterval.mStart.ToMicroseconds(),
+ sampleInterval.mEnd.ToMicroseconds(),
+ sample->mTimecode.ToMicroseconds(), sample->mDuration.ToMicroseconds(),
+ sample->mKeyframe);
+
+ // 6. If last decode timestamp for track buffer is set and decode timestamp
+ // is less than last decode timestamp: OR If last decode timestamp for track
+ // buffer is set and the difference between decode timestamp and last decode
+ // timestamp is greater than 2 times last frame duration:
+ if (needDiscontinuityCheck && trackBuffer.mLastDecodeTimestamp.isSome() &&
+ (decodeTimestamp < trackBuffer.mLastDecodeTimestamp.ref() ||
+ (decodeTimestamp - trackBuffer.mLastDecodeTimestamp.ref() >
+ trackBuffer.mLongestFrameDuration * 2))) {
+ MSE_DEBUG("Discontinuity detected.");
+ SourceBufferAppendMode appendMode =
+ mSourceBufferAttributes->GetAppendMode();
+
+ // 1a. If mode equals "segments":
+ if (appendMode == SourceBufferAppendMode::Segments) {
+ // Set group end timestamp to presentation timestamp.
+ mSourceBufferAttributes->SetGroupEndTimestamp(sampleInterval.mStart);
+ }
+ // 1b. If mode equals "sequence":
+ if (appendMode == SourceBufferAppendMode::Sequence) {
+ // Set group start timestamp equal to the group end timestamp.
+ mSourceBufferAttributes->SetGroupStartTimestamp(
+ mSourceBufferAttributes->GetGroupEndTimestamp());
+ }
+ for (auto& track : GetTracksList()) {
+ // 2. Unset the last decode timestamp on all track buffers.
+ // 3. Unset the last frame duration on all track buffers.
+ // 4. Unset the highest end timestamp on all track buffers.
+ // 5. Set the need random access point flag on all track buffers to
+ // true.
+ track->ResetAppendState();
+ }
+ // 6. Jump to the Loop Top step above to restart processing of the current
+ // coded frame. Rather that restarting the process for the frame, we run
+ // the first steps again instead.
+ // 3. If mode equals "sequence" and group start timestamp is set, then run
+ // the following steps:
+ TimeUnit presentationTimestamp =
+ mSourceBufferAttributes->mGenerateTimestamps ? TimeUnit()
+ : sampleTime;
+ CheckSequenceDiscontinuity(presentationTimestamp);
+
+ if (!sample->mKeyframe) {
+ previouslyDroppedSample = nullptr;
+ continue;
+ }
+ if (appendMode == SourceBufferAppendMode::Sequence) {
+ // mSourceBufferAttributes->GetTimestampOffset() was modified during
+ // CheckSequenceDiscontinuity. We need to update our variables.
+ timestampOffset = mSourceBufferAttributes->GetTimestampOffset();
+ sampleInterval =
+ mSourceBufferAttributes->mGenerateTimestamps
+ ? TimeInterval(timestampOffset,
+ timestampOffset + sampleDuration)
+ : TimeInterval(timestampOffset + sampleTime,
+ timestampOffset + sampleTime + sampleDuration);
+ decodeTimestamp = mSourceBufferAttributes->mGenerateTimestamps
+ ? timestampOffset
+ : timestampOffset + sampleTimecode;
+ }
+ trackBuffer.mNeedRandomAccessPoint = false;
+ needDiscontinuityCheck = false;
+ }
+
+ // 7. Let frame end timestamp equal the sum of presentation timestamp and
+ // frame duration. This is sampleInterval.mEnd
+
+ // 8. If presentation timestamp is less than appendWindowStart, then set the
+ // need random access point flag to true, drop the coded frame, and jump to
+ // the top of the loop to start processing the next coded frame.
+ // 9. If frame end timestamp is greater than appendWindowEnd, then set the
+ // need random access point flag to true, drop the coded frame, and jump to
+ // the top of the loop to start processing the next coded frame.
+ if (!mAppendWindow.ContainsStrict(sampleInterval)) {
+ if (mAppendWindow.IntersectsStrict(sampleInterval)) {
+ // 8. Note: Some implementations MAY choose to collect some of these
+ // coded frames with presentation timestamp less than
+ // appendWindowStart and use them to generate a splice at the first
+ // coded frame that has a presentation timestamp greater than or
+ // equal to appendWindowStart even if that frame is not a random
+ // access point. Supporting this requires multiple decoders or faster
+ // than real-time decoding so for now this behavior will not be a
+ // normative requirement.
+ // 9. Note: Some implementations MAY choose to collect coded frames with
+ // presentation timestamp less than appendWindowEnd and frame end
+ // timestamp greater than appendWindowEnd and use them to generate a
+ // splice across the portion of the collected coded frames within the
+ // append window at time of collection, and the beginning portion of
+ // later processed frames which only partially overlap the end of the
+ // collected coded frames. Supporting this requires multiple decoders
+ // or faster than real-time decoding so for now this behavior will
+ // not be a normative requirement. In conjunction with collecting
+ // coded frames that span appendWindowStart, implementations MAY thus
+ // support gapless audio splicing.
+ TimeInterval intersection = mAppendWindow.Intersection(sampleInterval);
+ sample->mOriginalPresentationWindow = Some(sampleInterval);
+ MSE_DEBUGV("will truncate frame from [%" PRId64 ",%" PRId64
+ "] to [%" PRId64 ",%" PRId64 "]",
+ sampleInterval.mStart.ToMicroseconds(),
+ sampleInterval.mEnd.ToMicroseconds(),
+ intersection.mStart.ToMicroseconds(),
+ intersection.mEnd.ToMicroseconds());
+ sampleInterval = intersection;
+ } else {
+ sample->mOriginalPresentationWindow = Some(sampleInterval);
+ sample->mTimecode = decodeTimestamp;
+ previouslyDroppedSample = sample;
+ MSE_DEBUGV("frame [%" PRId64 ",%" PRId64
+ "] outside appendWindow [%" PRId64 ",%" PRId64 "] dropping",
+ sampleInterval.mStart.ToMicroseconds(),
+ sampleInterval.mEnd.ToMicroseconds(),
+ mAppendWindow.mStart.ToMicroseconds(),
+ mAppendWindow.mEnd.ToMicroseconds());
+ if (samples.Length()) {
+ // We are creating a discontinuity in the samples.
+ // Insert the samples processed so far.
+ InsertFrames(samples, samplesRange, trackBuffer);
+ samples.Clear();
+ samplesRange = TimeIntervals();
+ trackBuffer.mSizeBuffer += sizeNewSamples;
+ sizeNewSamples = 0;
+ UpdateHighestTimestamp(trackBuffer, highestSampleTime);
+ }
+ trackBuffer.mNeedRandomAccessPoint = true;
+ needDiscontinuityCheck = true;
+ continue;
+ }
+ }
+ if (previouslyDroppedSample) {
+ MSE_DEBUGV("Adding silent frame");
+ // This "silent" sample will be added so that it starts exactly before the
+ // first usable one. The duration of the actual sample will be adjusted so
+ // that the total duration staty the same.
+ // Setting a dummy presentation window of 1us will cause this sample to be
+ // dropped after decoding by the AudioTrimmer (if audio).
+ TimeInterval previouslyDroppedSampleInterval =
+ TimeInterval(sampleInterval.mStart,
+ sampleInterval.mStart + TimeUnit::FromMicroseconds(1));
+ addToSamples(previouslyDroppedSample, previouslyDroppedSampleInterval);
+ previouslyDroppedSample = nullptr;
+ sampleInterval.mStart += previouslyDroppedSampleInterval.Length();
+ }
+
+ sample->mTimecode = decodeTimestamp;
+ addToSamples(sample, sampleInterval);
+
+ // Steps 11,12,13,14, 15 and 16 will be done in one block in InsertFrames.
+
+ trackBuffer.mLongestFrameDuration =
+ trackBuffer.mLastFrameDuration.isSome()
+ ? sample->mKeyframe
+ ? sampleDuration
+ : std::max(sampleDuration, trackBuffer.mLongestFrameDuration)
+ : sampleDuration;
+
+ // 17. Set last decode timestamp for track buffer to decode timestamp.
+ trackBuffer.mLastDecodeTimestamp = Some(decodeTimestamp);
+ // 18. Set last frame duration for track buffer to frame duration.
+ trackBuffer.mLastFrameDuration = Some(sampleDuration);
+
+ // 19. If highest end timestamp for track buffer is unset or frame end
+ // timestamp is greater than highest end timestamp, then set highest end
+ // timestamp for track buffer to frame end timestamp.
+ if (trackBuffer.mHighestEndTimestamp.isNothing() ||
+ sampleInterval.mEnd > trackBuffer.mHighestEndTimestamp.ref()) {
+ trackBuffer.mHighestEndTimestamp = Some(sampleInterval.mEnd);
+ }
+ if (sampleInterval.mStart > highestSampleTime) {
+ highestSampleTime = sampleInterval.mStart;
+ }
+ // 20. If frame end timestamp is greater than group end timestamp, then set
+ // group end timestamp equal to frame end timestamp.
+ if (sampleInterval.mEnd > mSourceBufferAttributes->GetGroupEndTimestamp()) {
+ mSourceBufferAttributes->SetGroupEndTimestamp(sampleInterval.mEnd);
+ }
+ // 21. If generate timestamps flag equals true, then set timestampOffset
+ // equal to frame end timestamp.
+ if (mSourceBufferAttributes->mGenerateTimestamps) {
+ mSourceBufferAttributes->SetTimestampOffset(sampleInterval.mEnd);
+ }
+ }
+
+ if (samples.Length()) {
+ InsertFrames(samples, samplesRange, trackBuffer);
+ trackBuffer.mSizeBuffer += sizeNewSamples;
+ UpdateHighestTimestamp(trackBuffer, highestSampleTime);
+ }
+}
+
+bool TrackBuffersManager::CheckNextInsertionIndex(TrackData& aTrackData,
+ const TimeUnit& aSampleTime) {
+ if (aTrackData.mNextInsertionIndex.isSome()) {
+ return true;
+ }
+
+ const TrackBuffer& data = aTrackData.GetTrackBuffer();
+
+ if (data.IsEmpty() || aSampleTime < aTrackData.mBufferedRanges.GetStart()) {
+ aTrackData.mNextInsertionIndex = Some(0u);
+ return true;
+ }
+
+ // Find which discontinuity we should insert the frame before.
+ TimeInterval target;
+ for (const auto& interval : aTrackData.mBufferedRanges) {
+ if (aSampleTime < interval.mStart) {
+ target = interval;
+ break;
+ }
+ }
+ if (target.IsEmpty()) {
+ // No target found, it will be added at the end of the track buffer.
+ aTrackData.mNextInsertionIndex = Some(uint32_t(data.Length()));
+ return true;
+ }
+ // We now need to find the first frame of the searched interval.
+ // We will insert our new frames right before.
+ for (uint32_t i = 0; i < data.Length(); i++) {
+ const RefPtr<MediaRawData>& sample = data[i];
+ if (sample->mTime >= target.mStart ||
+ sample->GetEndTime() > target.mStart) {
+ aTrackData.mNextInsertionIndex = Some(i);
+ return true;
+ }
+ }
+ NS_ASSERTION(false, "Insertion Index Not Found");
+ return false;
+}
+
+void TrackBuffersManager::InsertFrames(TrackBuffer& aSamples,
+ const TimeIntervals& aIntervals,
+ TrackData& aTrackData) {
+ AUTO_PROFILER_LABEL("TrackBuffersManager::InsertFrames", MEDIA_PLAYBACK);
+ // 5. Let track buffer equal the track buffer that the coded frame will be
+ // added to.
+ auto& trackBuffer = aTrackData;
+
+ MSE_DEBUGV("Processing %zu %s frames(start:%" PRId64 " end:%" PRId64 ")",
+ aSamples.Length(), aTrackData.mInfo->mMimeType.get(),
+ aIntervals.GetStart().ToMicroseconds(),
+ aIntervals.GetEnd().ToMicroseconds());
+ if (profiler_thread_is_being_profiled_for_markers()) {
+ nsPrintfCString markerString(
+ "Processing %zu %s frames(start:%" PRId64 " end:%" PRId64 ")",
+ aSamples.Length(), aTrackData.mInfo->mMimeType.get(),
+ aIntervals.GetStart().ToMicroseconds(),
+ aIntervals.GetEnd().ToMicroseconds());
+ PROFILER_MARKER_TEXT("InsertFrames", MEDIA_PLAYBACK, {}, markerString);
+ }
+
+ // 11. Let spliced audio frame be an unset variable for holding audio splice
+ // information
+ // 12. Let spliced timed text frame be an unset variable for holding timed
+ // text splice information
+
+ // 13. If last decode timestamp for track buffer is unset and presentation
+ // timestamp falls within the presentation interval of a coded frame in track
+ // buffer,then run the following steps: For now we only handle replacing
+ // existing frames with the new ones. So we skip this step.
+
+ // 14. Remove existing coded frames in track buffer:
+ // a) If highest end timestamp for track buffer is not set:
+ // Remove all coded frames from track buffer that have a presentation
+ // timestamp greater than or equal to presentation timestamp and less
+ // than frame end timestamp.
+ // b) If highest end timestamp for track buffer is set and less than or
+ // equal to presentation timestamp:
+ // Remove all coded frames from track buffer that have a presentation
+ // timestamp greater than or equal to highest end timestamp and less than
+ // frame end timestamp
+
+ // There is an ambiguity on how to remove frames, which was lodged with:
+ // https://www.w3.org/Bugs/Public/show_bug.cgi?id=28710, implementing as per
+ // bug description.
+
+ // 15. Remove decoding dependencies of the coded frames removed in the
+ // previous step: Remove all coded frames between the coded frames removed in
+ // the previous step and the next random access point after those removed
+ // frames.
+
+ if (trackBuffer.mBufferedRanges.IntersectsStrict(aIntervals)) {
+ if (aSamples[0]->mKeyframe &&
+ (mType.Type() == MEDIAMIMETYPE("video/webm") ||
+ mType.Type() == MEDIAMIMETYPE("audio/webm"))) {
+ // We are starting a new GOP, we do not have to worry about breaking an
+ // existing current coded frame group. Reset the next insertion index
+ // so the search for when to start our frames removal can be exhaustive.
+ // This is a workaround for bug 1276184 and only until either bug 1277733
+ // or bug 1209386 is fixed.
+ // With the webm container, we can't always properly determine the
+ // duration of the last frame, which may cause the last frame of a cluster
+ // to overlap the following frame.
+ trackBuffer.mNextInsertionIndex.reset();
+ }
+ uint32_t index = RemoveFrames(aIntervals, trackBuffer,
+ trackBuffer.mNextInsertionIndex.refOr(0),
+ RemovalMode::kTruncateFrame);
+ if (index) {
+ trackBuffer.mNextInsertionIndex = Some(index);
+ }
+ }
+
+ // 16. Add the coded frame with the presentation timestamp, decode timestamp,
+ // and frame duration to the track buffer.
+ if (!CheckNextInsertionIndex(aTrackData, aSamples[0]->mTime)) {
+ RejectProcessing(NS_ERROR_FAILURE, __func__);
+ return;
+ }
+
+ // Adjust our demuxing index if necessary.
+ if (trackBuffer.mNextGetSampleIndex.isSome()) {
+ if (trackBuffer.mNextInsertionIndex.ref() ==
+ trackBuffer.mNextGetSampleIndex.ref() &&
+ aIntervals.GetEnd() >= trackBuffer.mNextSampleTime) {
+ MSE_DEBUG("Next sample to be played got overwritten");
+ trackBuffer.mNextGetSampleIndex.reset();
+ ResetEvictionIndex(trackBuffer);
+ } else if (trackBuffer.mNextInsertionIndex.ref() <=
+ trackBuffer.mNextGetSampleIndex.ref()) {
+ trackBuffer.mNextGetSampleIndex.ref() += aSamples.Length();
+ // We could adjust the eviction index so that the new data gets added to
+ // the evictable amount (as it is prior currentTime). However, considering
+ // new data is being added prior the current playback, it's likely that
+ // this data will be played next, and as such we probably don't want to
+ // have it evicted too early. So instead reset the eviction index instead.
+ ResetEvictionIndex(trackBuffer);
+ }
+ }
+
+ TrackBuffer& data = trackBuffer.GetTrackBuffer();
+ data.InsertElementsAt(trackBuffer.mNextInsertionIndex.ref(), aSamples);
+ trackBuffer.mNextInsertionIndex.ref() += aSamples.Length();
+
+ // Update our buffered range with new sample interval.
+ trackBuffer.mBufferedRanges += aIntervals;
+ // We allow a fuzz factor in our interval of half a frame length,
+ // as fuzz is +/- value, giving an effective leeway of a full frame
+ // length.
+ if (!aIntervals.IsEmpty()) {
+ TimeIntervals range(aIntervals);
+ range.SetFuzz(trackBuffer.mLongestFrameDuration / 2);
+ trackBuffer.mSanitizedBufferedRanges += range;
+ }
+}
+
+void TrackBuffersManager::UpdateHighestTimestamp(
+ TrackData& aTrackData, const media::TimeUnit& aHighestTime) {
+ if (aHighestTime > aTrackData.mHighestStartTimestamp) {
+ MutexAutoLock mut(mMutex);
+ aTrackData.mHighestStartTimestamp = aHighestTime;
+ }
+}
+
+uint32_t TrackBuffersManager::RemoveFrames(const TimeIntervals& aIntervals,
+ TrackData& aTrackData,
+ uint32_t aStartIndex,
+ RemovalMode aMode) {
+ AUTO_PROFILER_LABEL("TrackBuffersManager::RemoveFrames", MEDIA_PLAYBACK);
+ TrackBuffer& data = aTrackData.GetTrackBuffer();
+ Maybe<uint32_t> firstRemovedIndex;
+ uint32_t lastRemovedIndex = 0;
+
+ // We loop from aStartIndex to avoid removing frames that we inserted earlier
+ // and part of the current coded frame group. This is allows to handle step
+ // 14 of the coded frame processing algorithm without having to check the
+ // value of highest end timestamp: "Remove existing coded frames in track
+ // buffer:
+ // If highest end timestamp for track buffer is not set:
+ // Remove all coded frames from track buffer that have a presentation
+ // timestamp greater than or equal to presentation timestamp and less than
+ // frame end timestamp.
+ // If highest end timestamp for track buffer is set and less than or equal to
+ // presentation timestamp:
+ // Remove all coded frames from track buffer that have a presentation
+ // timestamp greater than or equal to highest end timestamp and less than
+ // frame end timestamp"
+ TimeUnit intervalsEnd = aIntervals.GetEnd();
+ for (uint32_t i = aStartIndex; i < data.Length(); i++) {
+ RefPtr<MediaRawData>& sample = data[i];
+ if (aIntervals.ContainsStrict(sample->mTime)) {
+ // The start of this existing frame will be overwritten, we drop that
+ // entire frame.
+ MSE_DEBUGV("overridding start of frame [%" PRId64 ",%" PRId64
+ "] with [%" PRId64 ",%" PRId64 "] dropping",
+ sample->mTime.ToMicroseconds(),
+ sample->GetEndTime().ToMicroseconds(),
+ aIntervals.GetStart().ToMicroseconds(),
+ aIntervals.GetEnd().ToMicroseconds());
+ if (firstRemovedIndex.isNothing()) {
+ firstRemovedIndex = Some(i);
+ }
+ lastRemovedIndex = i;
+ continue;
+ }
+ TimeInterval sampleInterval(sample->mTime, sample->GetEndTime());
+ if (aMode == RemovalMode::kTruncateFrame &&
+ aIntervals.IntersectsStrict(sampleInterval)) {
+ // The sample to be overwritten is only partially covered.
+ TimeIntervals intersection =
+ Intersection(aIntervals, TimeIntervals(sampleInterval));
+ bool found = false;
+ TimeUnit startTime = intersection.GetStart(&found);
+ MOZ_DIAGNOSTIC_ASSERT(found, "Must intersect with added coded frames");
+ Unused << found;
+ // Signal that this frame should be truncated when decoded.
+ if (!sample->mOriginalPresentationWindow) {
+ sample->mOriginalPresentationWindow = Some(sampleInterval);
+ }
+ MOZ_ASSERT(startTime > sample->mTime);
+ sample->mDuration = startTime - sample->mTime;
+ MOZ_DIAGNOSTIC_ASSERT(sample->mDuration.IsValid());
+ MSE_DEBUGV("partial overwrite of frame [%" PRId64 ",%" PRId64
+ "] with [%" PRId64 ",%" PRId64
+ "] trim to "
+ "[%" PRId64 ",%" PRId64 "]",
+ sampleInterval.mStart.ToMicroseconds(),
+ sampleInterval.mEnd.ToMicroseconds(),
+ aIntervals.GetStart().ToMicroseconds(),
+ aIntervals.GetEnd().ToMicroseconds(),
+ sample->mTime.ToMicroseconds(),
+ sample->GetEndTime().ToMicroseconds());
+ continue;
+ }
+
+ if (sample->mTime >= intervalsEnd) {
+ // We can break the loop now. All frames up to the next keyframe will be
+ // removed during the next step.
+ break;
+ }
+ }
+
+ if (firstRemovedIndex.isNothing()) {
+ return 0;
+ }
+
+ // Remove decoding dependencies of the coded frames removed in the previous
+ // step: Remove all coded frames between the coded frames removed in the
+ // previous step and the next random access point after those removed frames.
+ for (uint32_t i = lastRemovedIndex + 1; i < data.Length(); i++) {
+ const RefPtr<MediaRawData>& sample = data[i];
+ if (sample->mKeyframe) {
+ break;
+ }
+ lastRemovedIndex = i;
+ }
+
+ TimeUnit maxSampleDuration;
+ uint32_t sizeRemoved = 0;
+ TimeIntervals removedIntervals;
+ for (uint32_t i = firstRemovedIndex.ref(); i <= lastRemovedIndex; i++) {
+ const RefPtr<MediaRawData> sample = data[i];
+ TimeInterval sampleInterval =
+ TimeInterval(sample->mTime, sample->GetEndTime());
+ removedIntervals += sampleInterval;
+ if (sample->mDuration > maxSampleDuration) {
+ maxSampleDuration = sample->mDuration;
+ }
+ sizeRemoved += sample->ComputedSizeOfIncludingThis();
+ }
+ aTrackData.mSizeBuffer -= sizeRemoved;
+
+ MSE_DEBUG("Removing frames from:%u (frames:%u) ([%f, %f))",
+ firstRemovedIndex.ref(),
+ lastRemovedIndex - firstRemovedIndex.ref() + 1,
+ removedIntervals.GetStart().ToSeconds(),
+ removedIntervals.GetEnd().ToSeconds());
+ if (profiler_thread_is_being_profiled_for_markers()) {
+ nsPrintfCString markerString(
+ "Removing frames from:%u (frames:%u) ([%f, %f))",
+ firstRemovedIndex.ref(), lastRemovedIndex - firstRemovedIndex.ref() + 1,
+ removedIntervals.GetStart().ToSeconds(),
+ removedIntervals.GetEnd().ToSeconds());
+ PROFILER_MARKER_TEXT("RemoveFrames", MEDIA_PLAYBACK, {}, markerString);
+ }
+
+ if (aTrackData.mNextGetSampleIndex.isSome()) {
+ if (aTrackData.mNextGetSampleIndex.ref() >= firstRemovedIndex.ref() &&
+ aTrackData.mNextGetSampleIndex.ref() <= lastRemovedIndex) {
+ MSE_DEBUG("Next sample to be played got evicted");
+ aTrackData.mNextGetSampleIndex.reset();
+ ResetEvictionIndex(aTrackData);
+ } else if (aTrackData.mNextGetSampleIndex.ref() > lastRemovedIndex) {
+ uint32_t samplesRemoved = lastRemovedIndex - firstRemovedIndex.ref() + 1;
+ aTrackData.mNextGetSampleIndex.ref() -= samplesRemoved;
+ if (aTrackData.mEvictionIndex.mLastIndex > lastRemovedIndex) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ aTrackData.mEvictionIndex.mLastIndex >= samplesRemoved &&
+ aTrackData.mEvictionIndex.mEvictable >= sizeRemoved,
+ "Invalid eviction index");
+ MutexAutoLock mut(mMutex);
+ aTrackData.mEvictionIndex.mLastIndex -= samplesRemoved;
+ aTrackData.mEvictionIndex.mEvictable -= sizeRemoved;
+ } else {
+ ResetEvictionIndex(aTrackData);
+ }
+ }
+ }
+
+ if (aTrackData.mNextInsertionIndex.isSome()) {
+ if (aTrackData.mNextInsertionIndex.ref() > firstRemovedIndex.ref() &&
+ aTrackData.mNextInsertionIndex.ref() <= lastRemovedIndex + 1) {
+ aTrackData.ResetAppendState();
+ MSE_DEBUG("NextInsertionIndex got reset.");
+ } else if (aTrackData.mNextInsertionIndex.ref() > lastRemovedIndex + 1) {
+ aTrackData.mNextInsertionIndex.ref() -=
+ lastRemovedIndex - firstRemovedIndex.ref() + 1;
+ }
+ }
+
+ // Update our buffered range to exclude the range just removed.
+ aTrackData.mBufferedRanges -= removedIntervals;
+
+ // Recalculate sanitized buffered ranges.
+ aTrackData.mSanitizedBufferedRanges = aTrackData.mBufferedRanges;
+ aTrackData.mSanitizedBufferedRanges.SetFuzz(maxSampleDuration / 2);
+
+ data.RemoveElementsAt(firstRemovedIndex.ref(),
+ lastRemovedIndex - firstRemovedIndex.ref() + 1);
+
+ if (removedIntervals.GetEnd() >= aTrackData.mHighestStartTimestamp &&
+ removedIntervals.GetStart() <= aTrackData.mHighestStartTimestamp) {
+ // The sample with the highest presentation time got removed.
+ // Rescan the trackbuffer to determine the new one.
+ TimeUnit highestStartTime;
+ for (const auto& sample : data) {
+ if (sample->mTime > highestStartTime) {
+ highestStartTime = sample->mTime;
+ }
+ }
+ MutexAutoLock mut(mMutex);
+ aTrackData.mHighestStartTimestamp = highestStartTime;
+ }
+
+ return firstRemovedIndex.ref();
+}
+
+void TrackBuffersManager::RecreateParser(bool aReuseInitData) {
+ MOZ_ASSERT(OnTaskQueue());
+ // Recreate our parser for only the data remaining. This is required
+ // as it has parsed the entire InputBuffer provided.
+ // Once the old TrackBuffer/MediaSource implementation is removed
+ // we can optimize this part. TODO
+ if (mParser) {
+ DDUNLINKCHILD(mParser.get());
+ }
+ mParser = ContainerParser::CreateForMIMEType(mType);
+ DDLINKCHILD("parser", mParser.get());
+ if (aReuseInitData && mInitData) {
+ TimeUnit start, end;
+ mParser->ParseStartAndEndTimestamps(MediaSpan(mInitData), start, end);
+ mProcessedInput = mInitData->Length();
+ } else {
+ mProcessedInput = 0;
+ }
+}
+
+nsTArray<TrackBuffersManager::TrackData*> TrackBuffersManager::GetTracksList() {
+ nsTArray<TrackData*> tracks;
+ if (HasVideo()) {
+ tracks.AppendElement(&mVideoTracks);
+ }
+ if (HasAudio()) {
+ tracks.AppendElement(&mAudioTracks);
+ }
+ return tracks;
+}
+
+nsTArray<const TrackBuffersManager::TrackData*>
+TrackBuffersManager::GetTracksList() const {
+ nsTArray<const TrackData*> tracks;
+ if (HasVideo()) {
+ tracks.AppendElement(&mVideoTracks);
+ }
+ if (HasAudio()) {
+ tracks.AppendElement(&mAudioTracks);
+ }
+ return tracks;
+}
+
+void TrackBuffersManager::SetAppendState(AppendState aAppendState) {
+ MSE_DEBUG("AppendState changed from %s to %s",
+ AppendStateToStr(mSourceBufferAttributes->GetAppendState()),
+ AppendStateToStr(aAppendState));
+ mSourceBufferAttributes->SetAppendState(aAppendState);
+}
+
+MediaInfo TrackBuffersManager::GetMetadata() const {
+ MutexAutoLock mut(mMutex);
+ return mInfo;
+}
+
+const TimeIntervals& TrackBuffersManager::Buffered(
+ TrackInfo::TrackType aTrack) const {
+ MOZ_ASSERT(OnTaskQueue());
+ return GetTracksData(aTrack).mBufferedRanges;
+}
+
+const media::TimeUnit& TrackBuffersManager::HighestStartTime(
+ TrackInfo::TrackType aTrack) const {
+ MOZ_ASSERT(OnTaskQueue());
+ return GetTracksData(aTrack).mHighestStartTimestamp;
+}
+
+TimeIntervals TrackBuffersManager::SafeBuffered(
+ TrackInfo::TrackType aTrack) const {
+ MutexAutoLock mut(mMutex);
+ return aTrack == TrackInfo::kVideoTrack ? mVideoBufferedRanges
+ : mAudioBufferedRanges;
+}
+
+TimeUnit TrackBuffersManager::HighestStartTime() const {
+ MutexAutoLock mut(mMutex);
+ TimeUnit highestStartTime;
+ for (auto& track : GetTracksList()) {
+ highestStartTime =
+ std::max(track->mHighestStartTimestamp, highestStartTime);
+ }
+ return highestStartTime;
+}
+
+TimeUnit TrackBuffersManager::HighestEndTime() const {
+ MutexAutoLock mut(mMutex);
+
+ nsTArray<const TimeIntervals*> tracks;
+ if (HasVideo()) {
+ tracks.AppendElement(&mVideoBufferedRanges);
+ }
+ if (HasAudio()) {
+ tracks.AppendElement(&mAudioBufferedRanges);
+ }
+ return HighestEndTime(tracks);
+}
+
+TimeUnit TrackBuffersManager::HighestEndTime(
+ nsTArray<const TimeIntervals*>& aTracks) const {
+ mMutex.AssertCurrentThreadOwns();
+
+ TimeUnit highestEndTime;
+
+ for (const auto& trackRanges : aTracks) {
+ highestEndTime = std::max(trackRanges->GetEnd(), highestEndTime);
+ }
+ return highestEndTime;
+}
+
+void TrackBuffersManager::ResetEvictionIndex(TrackData& aTrackData) {
+ MutexAutoLock mut(mMutex);
+ aTrackData.mEvictionIndex.Reset();
+}
+
+void TrackBuffersManager::UpdateEvictionIndex(TrackData& aTrackData,
+ uint32_t currentIndex) {
+ uint32_t evictable = 0;
+ TrackBuffer& data = aTrackData.GetTrackBuffer();
+ MOZ_DIAGNOSTIC_ASSERT(currentIndex >= aTrackData.mEvictionIndex.mLastIndex,
+ "Invalid call");
+ MOZ_DIAGNOSTIC_ASSERT(
+ currentIndex == data.Length() || data[currentIndex]->mKeyframe,
+ "Must stop at keyframe");
+
+ for (uint32_t i = aTrackData.mEvictionIndex.mLastIndex; i < currentIndex;
+ i++) {
+ evictable += data[i]->ComputedSizeOfIncludingThis();
+ }
+ aTrackData.mEvictionIndex.mLastIndex = currentIndex;
+ MutexAutoLock mut(mMutex);
+ aTrackData.mEvictionIndex.mEvictable += evictable;
+}
+
+const TrackBuffersManager::TrackBuffer& TrackBuffersManager::GetTrackBuffer(
+ TrackInfo::TrackType aTrack) const {
+ MOZ_ASSERT(OnTaskQueue());
+ return GetTracksData(aTrack).GetTrackBuffer();
+}
+
+uint32_t TrackBuffersManager::FindSampleIndex(const TrackBuffer& aTrackBuffer,
+ const TimeInterval& aInterval) {
+ TimeUnit target = aInterval.mStart - aInterval.mFuzz;
+
+ for (uint32_t i = 0; i < aTrackBuffer.Length(); i++) {
+ const RefPtr<MediaRawData>& sample = aTrackBuffer[i];
+ if (sample->mTime >= target || sample->GetEndTime() > target) {
+ return i;
+ }
+ }
+ NS_ASSERTION(false, "FindSampleIndex called with invalid arguments");
+
+ return 0;
+}
+
+TimeUnit TrackBuffersManager::Seek(TrackInfo::TrackType aTrack,
+ const TimeUnit& aTime,
+ const TimeUnit& aFuzz) {
+ MOZ_ASSERT(OnTaskQueue());
+ AUTO_PROFILER_LABEL("TrackBuffersManager::Seek", MEDIA_PLAYBACK);
+ auto& trackBuffer = GetTracksData(aTrack);
+ const TrackBuffersManager::TrackBuffer& track = GetTrackBuffer(aTrack);
+
+ if (!track.Length()) {
+ // This a reset. It will be followed by another valid seek.
+ trackBuffer.mNextGetSampleIndex = Some(uint32_t(0));
+ trackBuffer.mNextSampleTimecode = TimeUnit();
+ trackBuffer.mNextSampleTime = TimeUnit();
+ ResetEvictionIndex(trackBuffer);
+ return TimeUnit();
+ }
+
+ uint32_t i = 0;
+
+ if (aTime != TimeUnit()) {
+ // Determine the interval of samples we're attempting to seek to.
+ TimeIntervals buffered = trackBuffer.mBufferedRanges;
+ // Fuzz factor is +/- aFuzz; as we want to only eliminate gaps
+ // that are less than aFuzz wide, we set a fuzz factor aFuzz/2.
+ buffered.SetFuzz(aFuzz / 2);
+ TimeIntervals::IndexType index = buffered.Find(aTime);
+ MOZ_ASSERT(index != TimeIntervals::NoIndex,
+ "We shouldn't be called if aTime isn't buffered");
+ TimeInterval target = buffered[index];
+ target.mFuzz = aFuzz;
+ i = FindSampleIndex(track, target);
+ }
+
+ Maybe<TimeUnit> lastKeyFrameTime;
+ TimeUnit lastKeyFrameTimecode;
+ uint32_t lastKeyFrameIndex = 0;
+ for (; i < track.Length(); i++) {
+ const RefPtr<MediaRawData>& sample = track[i];
+ TimeUnit sampleTime = sample->mTime;
+ if (sampleTime > aTime && lastKeyFrameTime.isSome()) {
+ break;
+ }
+ if (sample->mKeyframe) {
+ lastKeyFrameTimecode = sample->mTimecode;
+ lastKeyFrameTime = Some(sampleTime);
+ lastKeyFrameIndex = i;
+ }
+ if (sampleTime == aTime ||
+ (sampleTime > aTime && lastKeyFrameTime.isSome())) {
+ break;
+ }
+ }
+ MSE_DEBUG("Keyframe %s found at %" PRId64 " @ %u",
+ lastKeyFrameTime.isSome() ? "" : "not",
+ lastKeyFrameTime.refOr(TimeUnit()).ToMicroseconds(),
+ lastKeyFrameIndex);
+
+ trackBuffer.mNextGetSampleIndex = Some(lastKeyFrameIndex);
+ trackBuffer.mNextSampleTimecode = lastKeyFrameTimecode;
+ trackBuffer.mNextSampleTime = lastKeyFrameTime.refOr(TimeUnit());
+ ResetEvictionIndex(trackBuffer);
+ UpdateEvictionIndex(trackBuffer, lastKeyFrameIndex);
+
+ return lastKeyFrameTime.refOr(TimeUnit());
+}
+
+uint32_t TrackBuffersManager::SkipToNextRandomAccessPoint(
+ TrackInfo::TrackType aTrack, const TimeUnit& aTimeThreadshold,
+ const media::TimeUnit& aFuzz, bool& aFound) {
+ mTaskQueueCapability->AssertOnCurrentThread();
+ AUTO_PROFILER_LABEL("TrackBuffersManager::SkipToNextRandomAccessPoint",
+ MEDIA_PLAYBACK);
+ uint32_t parsed = 0;
+ auto& trackData = GetTracksData(aTrack);
+ const TrackBuffer& track = GetTrackBuffer(aTrack);
+ aFound = false;
+
+ // SkipToNextRandomAccessPoint can only be called if aTimeThreadshold is known
+ // to be buffered.
+
+ if (NS_FAILED(SetNextGetSampleIndexIfNeeded(aTrack, aFuzz))) {
+ return 0;
+ }
+
+ TimeUnit nextSampleTimecode = trackData.mNextSampleTimecode;
+ TimeUnit nextSampleTime = trackData.mNextSampleTime;
+ uint32_t i = trackData.mNextGetSampleIndex.ref();
+ int32_t originalPos = i;
+
+ for (; i < track.Length(); i++) {
+ const MediaRawData* sample =
+ GetSample(aTrack, i, nextSampleTimecode, nextSampleTime, aFuzz);
+ if (!sample) {
+ break;
+ }
+ if (sample->mKeyframe && sample->mTime >= aTimeThreadshold) {
+ aFound = true;
+ break;
+ }
+ nextSampleTimecode = sample->GetEndTimecode();
+ nextSampleTime = sample->GetEndTime();
+ parsed++;
+ }
+
+ // Adjust the next demux time and index so that the next call to
+ // SkipToNextRandomAccessPoint will not count again the parsed sample as
+ // skipped.
+ if (aFound) {
+ trackData.mNextSampleTimecode = track[i]->mTimecode;
+ trackData.mNextSampleTime = track[i]->mTime;
+ trackData.mNextGetSampleIndex = Some(i);
+ } else if (i > 0) {
+ // Go back to the previous keyframe or the original position so the next
+ // demux can succeed and be decoded.
+ for (int j = i - 1; j >= originalPos; j--) {
+ const RefPtr<MediaRawData>& sample = track[j];
+ if (sample->mKeyframe) {
+ trackData.mNextSampleTimecode = sample->mTimecode;
+ trackData.mNextSampleTime = sample->mTime;
+ trackData.mNextGetSampleIndex = Some(uint32_t(j));
+ // We are unable to skip to a keyframe past aTimeThreshold, however
+ // we are speeding up decoding by dropping the unplayable frames.
+ // So we can mark aFound as true.
+ aFound = true;
+ break;
+ }
+ parsed--;
+ }
+ }
+
+ if (aFound) {
+ UpdateEvictionIndex(trackData, trackData.mNextGetSampleIndex.ref());
+ }
+
+ return parsed;
+}
+
+const MediaRawData* TrackBuffersManager::GetSample(TrackInfo::TrackType aTrack,
+ uint32_t aIndex,
+ const TimeUnit& aExpectedDts,
+ const TimeUnit& aExpectedPts,
+ const TimeUnit& aFuzz) {
+ MOZ_ASSERT(OnTaskQueue());
+ const TrackBuffer& track = GetTrackBuffer(aTrack);
+
+ if (aIndex >= track.Length()) {
+ // reached the end.
+ return nullptr;
+ }
+
+ if (!(aExpectedDts + aFuzz).IsValid() || !(aExpectedPts + aFuzz).IsValid()) {
+ // Time overflow, it seems like we also reached the end.
+ return nullptr;
+ }
+
+ const RefPtr<MediaRawData>& sample = track[aIndex];
+ if (!aIndex || sample->mTimecode <= aExpectedDts + aFuzz ||
+ sample->mTime <= aExpectedPts + aFuzz) {
+ MOZ_DIAGNOSTIC_ASSERT(sample->HasValidTime());
+ return sample;
+ }
+
+ // Gap is too big. End of Stream or Waiting for Data.
+ // TODO, check that we have continuous data based on the sanitized buffered
+ // range instead.
+ return nullptr;
+}
+
+already_AddRefed<MediaRawData> TrackBuffersManager::GetSample(
+ TrackInfo::TrackType aTrack, const TimeUnit& aFuzz, MediaResult& aResult) {
+ mTaskQueueCapability->AssertOnCurrentThread();
+ AUTO_PROFILER_LABEL("TrackBuffersManager::GetSample", MEDIA_PLAYBACK);
+ auto& trackData = GetTracksData(aTrack);
+ const TrackBuffer& track = GetTrackBuffer(aTrack);
+
+ aResult = NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA;
+
+ if (trackData.mNextGetSampleIndex.isSome()) {
+ if (trackData.mNextGetSampleIndex.ref() >= track.Length()) {
+ aResult = NS_ERROR_DOM_MEDIA_END_OF_STREAM;
+ return nullptr;
+ }
+ const MediaRawData* sample = GetSample(
+ aTrack, trackData.mNextGetSampleIndex.ref(),
+ trackData.mNextSampleTimecode, trackData.mNextSampleTime, aFuzz);
+ if (!sample) {
+ return nullptr;
+ }
+
+ RefPtr<MediaRawData> p = sample->Clone();
+ if (!p) {
+ aResult = MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__);
+ return nullptr;
+ }
+ if (p->mKeyframe) {
+ UpdateEvictionIndex(trackData, trackData.mNextGetSampleIndex.ref());
+ }
+ trackData.mNextGetSampleIndex.ref()++;
+ // Estimate decode timestamp and timestamp of the next sample.
+ TimeUnit nextSampleTimecode = sample->GetEndTimecode();
+ TimeUnit nextSampleTime = sample->GetEndTime();
+ const MediaRawData* nextSample =
+ GetSample(aTrack, trackData.mNextGetSampleIndex.ref(),
+ nextSampleTimecode, nextSampleTime, aFuzz);
+ if (nextSample) {
+ // We have a valid next sample, can use exact values.
+ trackData.mNextSampleTimecode = nextSample->mTimecode;
+ trackData.mNextSampleTime = nextSample->mTime;
+ } else {
+ // Next sample isn't available yet. Use estimates.
+ trackData.mNextSampleTimecode = nextSampleTimecode;
+ trackData.mNextSampleTime = nextSampleTime;
+ }
+ aResult = NS_OK;
+ return p.forget();
+ }
+
+ aResult = SetNextGetSampleIndexIfNeeded(aTrack, aFuzz);
+
+ if (NS_FAILED(aResult)) {
+ return nullptr;
+ }
+
+ MOZ_RELEASE_ASSERT(trackData.mNextGetSampleIndex.isSome() &&
+ trackData.mNextGetSampleIndex.ref() < track.Length());
+ const RefPtr<MediaRawData>& sample =
+ track[trackData.mNextGetSampleIndex.ref()];
+ RefPtr<MediaRawData> p = sample->Clone();
+ if (!p) {
+ // OOM
+ aResult = MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__);
+ return nullptr;
+ }
+ MOZ_DIAGNOSTIC_ASSERT(p->HasValidTime());
+
+ // Find the previous keyframe to calculate the evictable amount.
+ uint32_t i = trackData.mNextGetSampleIndex.ref();
+ for (; !track[i]->mKeyframe; i--) {
+ }
+ UpdateEvictionIndex(trackData, i);
+
+ trackData.mNextGetSampleIndex.ref()++;
+ trackData.mNextSampleTimecode = sample->GetEndTimecode();
+ trackData.mNextSampleTime = sample->GetEndTime();
+ return p.forget();
+}
+
+int32_t TrackBuffersManager::FindCurrentPosition(TrackInfo::TrackType aTrack,
+ const TimeUnit& aFuzz) const {
+ MOZ_ASSERT(OnTaskQueue());
+ auto& trackData = GetTracksData(aTrack);
+ const TrackBuffer& track = GetTrackBuffer(aTrack);
+
+ // Perform an exact search first.
+ for (uint32_t i = 0; i < track.Length(); i++) {
+ const RefPtr<MediaRawData>& sample = track[i];
+ TimeInterval sampleInterval{sample->mTimecode, sample->GetEndTimecode()};
+
+ if (sampleInterval.ContainsStrict(trackData.mNextSampleTimecode)) {
+ return i;
+ }
+ if (sampleInterval.mStart > trackData.mNextSampleTimecode) {
+ // Samples are ordered by timecode. There's no need to search
+ // any further.
+ break;
+ }
+ }
+
+ for (uint32_t i = 0; i < track.Length(); i++) {
+ const RefPtr<MediaRawData>& sample = track[i];
+ TimeInterval sampleInterval{sample->mTimecode, sample->GetEndTimecode(),
+ aFuzz};
+
+ if (sampleInterval.ContainsWithStrictEnd(trackData.mNextSampleTimecode)) {
+ return i;
+ }
+ if (sampleInterval.mStart - aFuzz > trackData.mNextSampleTimecode) {
+ // Samples are ordered by timecode. There's no need to search
+ // any further.
+ break;
+ }
+ }
+
+ // We couldn't find our sample by decode timestamp. Attempt to find it using
+ // presentation timestamp. There will likely be small jerkiness.
+ for (uint32_t i = 0; i < track.Length(); i++) {
+ const RefPtr<MediaRawData>& sample = track[i];
+ TimeInterval sampleInterval{sample->mTime, sample->GetEndTime(), aFuzz};
+
+ if (sampleInterval.ContainsWithStrictEnd(trackData.mNextSampleTimecode)) {
+ return i;
+ }
+ }
+
+ // Still not found.
+ return -1;
+}
+
+uint32_t TrackBuffersManager::Evictable(TrackInfo::TrackType aTrack) const {
+ MutexAutoLock mut(mMutex);
+ return GetTracksData(aTrack).mEvictionIndex.mEvictable;
+}
+
+TimeUnit TrackBuffersManager::GetNextRandomAccessPoint(
+ TrackInfo::TrackType aTrack, const TimeUnit& aFuzz) {
+ mTaskQueueCapability->AssertOnCurrentThread();
+
+ // So first determine the current position in the track buffer if necessary.
+ if (NS_FAILED(SetNextGetSampleIndexIfNeeded(aTrack, aFuzz))) {
+ return TimeUnit::FromInfinity();
+ }
+
+ auto& trackData = GetTracksData(aTrack);
+ const TrackBuffersManager::TrackBuffer& track = GetTrackBuffer(aTrack);
+
+ uint32_t i = trackData.mNextGetSampleIndex.ref();
+ TimeUnit nextSampleTimecode = trackData.mNextSampleTimecode;
+ TimeUnit nextSampleTime = trackData.mNextSampleTime;
+
+ for (; i < track.Length(); i++) {
+ const MediaRawData* sample =
+ GetSample(aTrack, i, nextSampleTimecode, nextSampleTime, aFuzz);
+ if (!sample) {
+ break;
+ }
+ if (sample->mKeyframe) {
+ return sample->mTime;
+ }
+ nextSampleTimecode = sample->GetEndTimecode();
+ nextSampleTime = sample->GetEndTime();
+ }
+ return TimeUnit::FromInfinity();
+}
+
+nsresult TrackBuffersManager::SetNextGetSampleIndexIfNeeded(
+ TrackInfo::TrackType aTrack, const TimeUnit& aFuzz) {
+ MOZ_ASSERT(OnTaskQueue());
+ auto& trackData = GetTracksData(aTrack);
+ const TrackBuffer& track = GetTrackBuffer(aTrack);
+
+ if (trackData.mNextGetSampleIndex.isSome()) {
+ // We already know the next GetSample index.
+ return NS_OK;
+ }
+
+ if (!track.Length()) {
+ // There's nothing to find yet.
+ return NS_ERROR_DOM_MEDIA_END_OF_STREAM;
+ }
+
+ if (trackData.mNextSampleTimecode == TimeUnit()) {
+ // First demux, get first sample.
+ trackData.mNextGetSampleIndex = Some(0u);
+ return NS_OK;
+ }
+
+ if (trackData.mNextSampleTimecode > track.LastElement()->GetEndTimecode()) {
+ // The next element is past our last sample. We're done.
+ trackData.mNextGetSampleIndex = Some(uint32_t(track.Length()));
+ return NS_ERROR_DOM_MEDIA_END_OF_STREAM;
+ }
+
+ int32_t pos = FindCurrentPosition(aTrack, aFuzz);
+ if (pos < 0) {
+ // Not found, must wait for more data.
+ MSE_DEBUG("Couldn't find sample (pts:%" PRId64 " dts:%" PRId64 ")",
+ trackData.mNextSampleTime.ToMicroseconds(),
+ trackData.mNextSampleTimecode.ToMicroseconds());
+ return NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA;
+ }
+ trackData.mNextGetSampleIndex = Some(uint32_t(pos));
+ return NS_OK;
+}
+
+void TrackBuffersManager::TrackData::AddSizeOfResources(
+ MediaSourceDecoder::ResourceSizes* aSizes) const {
+ for (const TrackBuffer& buffer : mBuffers) {
+ for (const MediaRawData* data : buffer) {
+ aSizes->mByteSize += data->SizeOfIncludingThis(aSizes->mMallocSizeOf);
+ }
+ }
+}
+
+RefPtr<GenericPromise> TrackBuffersManager::RequestDebugInfo(
+ dom::TrackBuffersManagerDebugInfo& aInfo) const {
+ const RefPtr<TaskQueue> taskQueue = GetTaskQueueSafe();
+ if (!taskQueue) {
+ return GenericPromise::CreateAndResolve(true, __func__);
+ }
+ if (!taskQueue->IsCurrentThreadIn()) {
+ // Run the request on the task queue if it's not already.
+ return InvokeAsync(taskQueue.get(), __func__,
+ [this, self = RefPtr{this}, &aInfo] {
+ return RequestDebugInfo(aInfo);
+ });
+ }
+ mTaskQueueCapability->AssertOnCurrentThread();
+ GetDebugInfo(aInfo);
+ return GenericPromise::CreateAndResolve(true, __func__);
+}
+
+void TrackBuffersManager::GetDebugInfo(
+ dom::TrackBuffersManagerDebugInfo& aInfo) const {
+ MOZ_ASSERT(OnTaskQueue(),
+ "This shouldn't be called off the task queue because we're about "
+ "to touch a lot of data that is used on the task queue");
+ CopyUTF8toUTF16(mType.Type().AsString(), aInfo.mType);
+
+ if (HasAudio()) {
+ aInfo.mNextSampleTime = mAudioTracks.mNextSampleTime.ToSeconds();
+ aInfo.mNumSamples = mAudioTracks.mBuffers[0].Length();
+ aInfo.mBufferSize = mAudioTracks.mSizeBuffer;
+ aInfo.mEvictable = Evictable(TrackInfo::kAudioTrack);
+ aInfo.mNextGetSampleIndex = mAudioTracks.mNextGetSampleIndex.valueOr(-1);
+ aInfo.mNextInsertionIndex = mAudioTracks.mNextInsertionIndex.valueOr(-1);
+ media::TimeIntervals ranges = SafeBuffered(TrackInfo::kAudioTrack);
+ dom::Sequence<dom::BufferRange> items;
+ for (uint32_t i = 0; i < ranges.Length(); ++i) {
+ // dom::Sequence is a FallibleTArray
+ dom::BufferRange* range = items.AppendElement(fallible);
+ if (!range) {
+ break;
+ }
+ range->mStart = ranges.Start(i).ToSeconds();
+ range->mEnd = ranges.End(i).ToSeconds();
+ }
+ aInfo.mRanges = std::move(items);
+ } else if (HasVideo()) {
+ aInfo.mNextSampleTime = mVideoTracks.mNextSampleTime.ToSeconds();
+ aInfo.mNumSamples = mVideoTracks.mBuffers[0].Length();
+ aInfo.mBufferSize = mVideoTracks.mSizeBuffer;
+ aInfo.mEvictable = Evictable(TrackInfo::kVideoTrack);
+ aInfo.mNextGetSampleIndex = mVideoTracks.mNextGetSampleIndex.valueOr(-1);
+ aInfo.mNextInsertionIndex = mVideoTracks.mNextInsertionIndex.valueOr(-1);
+ media::TimeIntervals ranges = SafeBuffered(TrackInfo::kVideoTrack);
+ dom::Sequence<dom::BufferRange> items;
+ for (uint32_t i = 0; i < ranges.Length(); ++i) {
+ // dom::Sequence is a FallibleTArray
+ dom::BufferRange* range = items.AppendElement(fallible);
+ if (!range) {
+ break;
+ }
+ range->mStart = ranges.Start(i).ToSeconds();
+ range->mEnd = ranges.End(i).ToSeconds();
+ }
+ aInfo.mRanges = std::move(items);
+ }
+}
+
+void TrackBuffersManager::AddSizeOfResources(
+ MediaSourceDecoder::ResourceSizes* aSizes) const {
+ mTaskQueueCapability->AssertOnCurrentThread();
+
+ if (mInputBuffer.isSome() && mInputBuffer->Buffer()) {
+ // mInputBuffer should be the sole owner of the underlying buffer, so this
+ // won't double count.
+ aSizes->mByteSize += mInputBuffer->Buffer()->ShallowSizeOfIncludingThis(
+ aSizes->mMallocSizeOf);
+ }
+ if (mInitData) {
+ aSizes->mByteSize +=
+ mInitData->ShallowSizeOfIncludingThis(aSizes->mMallocSizeOf);
+ }
+ if (mPendingInputBuffer.isSome() && mPendingInputBuffer->Buffer()) {
+ // mPendingInputBuffer should be the sole owner of the underlying buffer, so
+ // this won't double count.
+ aSizes->mByteSize +=
+ mPendingInputBuffer->Buffer()->ShallowSizeOfIncludingThis(
+ aSizes->mMallocSizeOf);
+ }
+
+ mVideoTracks.AddSizeOfResources(aSizes);
+ mAudioTracks.AddSizeOfResources(aSizes);
+}
+
+} // namespace mozilla
+#undef MSE_DEBUG
+#undef MSE_DEBUGV
+#undef SAMPLE_DEBUG
diff --git a/dom/media/mediasource/TrackBuffersManager.h b/dom/media/mediasource/TrackBuffersManager.h
new file mode 100644
index 0000000000..0b3b64b8fe
--- /dev/null
+++ b/dom/media/mediasource/TrackBuffersManager.h
@@ -0,0 +1,568 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_TRACKBUFFERSMANAGER_H_
+#define MOZILLA_TRACKBUFFERSMANAGER_H_
+
+#include "mozilla/Atomics.h"
+#include "mozilla/EventTargetCapability.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/NotNull.h"
+#include "mozilla/TaskQueue.h"
+#include "mozilla/dom/MediaDebugInfoBinding.h"
+
+#include "MediaContainerType.h"
+#include "MediaData.h"
+#include "MediaDataDemuxer.h"
+#include "MediaResult.h"
+#include "MediaSourceDecoder.h"
+#include "MediaSpan.h"
+#include "SourceBufferTask.h"
+#include "TimeUnits.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+
+class AbstractThread;
+class ContainerParser;
+class MediaByteBuffer;
+class MediaRawData;
+class MediaSourceDemuxer;
+class SourceBufferResource;
+
+class SourceBufferTaskQueue {
+ public:
+ SourceBufferTaskQueue() = default;
+
+ ~SourceBufferTaskQueue() {
+ MOZ_ASSERT(mQueue.IsEmpty(), "All tasks must have been processed");
+ }
+
+ void Push(SourceBufferTask* aTask) { mQueue.AppendElement(aTask); }
+
+ already_AddRefed<SourceBufferTask> Pop() {
+ if (!mQueue.Length()) {
+ return nullptr;
+ }
+ RefPtr<SourceBufferTask> task = std::move(mQueue[0]);
+ mQueue.RemoveElementAt(0);
+ return task.forget();
+ }
+
+ nsTArray<RefPtr<SourceBufferTask>>::size_type Length() const {
+ return mQueue.Length();
+ }
+
+ private:
+ nsTArray<RefPtr<SourceBufferTask>> mQueue;
+};
+
+DDLoggedTypeDeclName(TrackBuffersManager);
+
+class TrackBuffersManager final
+ : public DecoderDoctorLifeLogger<TrackBuffersManager> {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(TrackBuffersManager);
+
+ enum class EvictDataResult : int8_t {
+ NO_DATA_EVICTED,
+ CANT_EVICT,
+ BUFFER_FULL,
+ };
+
+ typedef TrackInfo::TrackType TrackType;
+ typedef MediaData::Type MediaType;
+ typedef nsTArray<RefPtr<MediaRawData>> TrackBuffer;
+ typedef SourceBufferTask::AppendPromise AppendPromise;
+ typedef SourceBufferTask::RangeRemovalPromise RangeRemovalPromise;
+
+ // Interface for SourceBuffer
+ TrackBuffersManager(MediaSourceDecoder* aParentDecoder,
+ const MediaContainerType& aType);
+
+ // Queue a task to add data to the end of the input buffer and run the MSE
+ // Buffer Append Algorithm
+ // 3.5.5 Buffer Append Algorithm.
+ // http://w3c.github.io/media-source/index.html#sourcebuffer-buffer-append
+ RefPtr<AppendPromise> AppendData(already_AddRefed<MediaByteBuffer> aData,
+ const SourceBufferAttributes& aAttributes);
+
+ // Queue a task to abort any pending AppendData.
+ // Does nothing at this stage.
+ void AbortAppendData();
+
+ // Queue a task to run MSE Reset Parser State Algorithm.
+ // 3.5.2 Reset Parser State
+ void ResetParserState(SourceBufferAttributes& aAttributes);
+
+ // Queue a task to run the MSE range removal algorithm.
+ // http://w3c.github.io/media-source/#sourcebuffer-coded-frame-removal
+ RefPtr<RangeRemovalPromise> RangeRemoval(media::TimeUnit aStart,
+ media::TimeUnit aEnd);
+
+ // Schedule data eviction if necessary as the next call to AppendData will
+ // add aSize bytes.
+ // Eviction is done in two steps, first remove data up to aPlaybackTime
+ // and if still more space is needed remove from the end.
+ EvictDataResult EvictData(const media::TimeUnit& aPlaybackTime,
+ int64_t aSize);
+
+ // Queue a task to run ChangeType
+ void ChangeType(const MediaContainerType& aType);
+
+ // Returns the buffered range currently managed.
+ // This may be called on any thread.
+ // Buffered must conform to
+ // http://w3c.github.io/media-source/index.html#widl-SourceBuffer-buffered
+ media::TimeIntervals Buffered() const;
+ media::TimeUnit HighestStartTime() const;
+ media::TimeUnit HighestEndTime() const;
+
+ // Return the size of the data managed by this SourceBufferContentManager.
+ int64_t GetSize() const;
+
+ // Indicate that the MediaSource parent object got into "ended" state.
+ void Ended();
+
+ // The parent SourceBuffer is about to be destroyed.
+ void Detach();
+
+ int64_t EvictionThreshold() const;
+
+ // Interface for MediaSourceDemuxer
+ MediaInfo GetMetadata() const;
+ const TrackBuffer& GetTrackBuffer(TrackInfo::TrackType aTrack) const;
+ const media::TimeIntervals& Buffered(TrackInfo::TrackType) const;
+ const media::TimeUnit& HighestStartTime(TrackInfo::TrackType) const;
+ media::TimeIntervals SafeBuffered(TrackInfo::TrackType) const;
+ bool IsEnded() const { return mEnded; }
+ uint32_t Evictable(TrackInfo::TrackType aTrack) const;
+ media::TimeUnit Seek(TrackInfo::TrackType aTrack,
+ const media::TimeUnit& aTime,
+ const media::TimeUnit& aFuzz);
+ uint32_t SkipToNextRandomAccessPoint(TrackInfo::TrackType aTrack,
+ const media::TimeUnit& aTimeThreadshold,
+ const media::TimeUnit& aFuzz,
+ bool& aFound);
+
+ already_AddRefed<MediaRawData> GetSample(TrackInfo::TrackType aTrack,
+ const media::TimeUnit& aFuzz,
+ MediaResult& aResult);
+ int32_t FindCurrentPosition(TrackInfo::TrackType aTrack,
+ const media::TimeUnit& aFuzz) const
+ MOZ_REQUIRES(mTaskQueueCapability);
+
+ // Will set the next GetSample index if needed. This information is determined
+ // through the value of mNextSampleTimecode. Return false if the index
+ // couldn't be determined or if there's nothing more that could be demuxed.
+ // This occurs if either the track buffer doesn't contain the required
+ // timecode or is empty.
+ nsresult SetNextGetSampleIndexIfNeeded(TrackInfo::TrackType aTrack,
+ const media::TimeUnit& aFuzz)
+ MOZ_REQUIRES(mTaskQueueCapability);
+
+ media::TimeUnit GetNextRandomAccessPoint(TrackInfo::TrackType aTrack,
+ const media::TimeUnit& aFuzz);
+
+ // Requests that the TrackBuffersManager populates aInfo with debug
+ // information. This may be done asynchronously, and aInfo should *not* be
+ // accessed by the caller until the returned promise is resolved or rejected.
+ RefPtr<GenericPromise> RequestDebugInfo(
+ dom::TrackBuffersManagerDebugInfo& aInfo) const;
+ void AddSizeOfResources(MediaSourceDecoder::ResourceSizes* aSizes) const;
+
+ private:
+ typedef MozPromise<bool, MediaResult, /* IsExclusive = */ true>
+ CodedFrameProcessingPromise;
+
+ ~TrackBuffersManager();
+ // All following functions run on the taskqueue.
+ RefPtr<AppendPromise> DoAppendData(already_AddRefed<MediaByteBuffer> aData,
+ const SourceBufferAttributes& aAttributes);
+ void ScheduleSegmentParserLoop() MOZ_REQUIRES(mTaskQueueCapability);
+ void SegmentParserLoop() MOZ_REQUIRES(mTaskQueueCapability);
+ void InitializationSegmentReceived() MOZ_REQUIRES(mTaskQueueCapability);
+ void ShutdownDemuxers() MOZ_REQUIRES(mTaskQueueCapability);
+ void CreateDemuxerforMIMEType() MOZ_REQUIRES(mTaskQueueCapability);
+ void ResetDemuxingState() MOZ_REQUIRES(mTaskQueueCapability);
+ void NeedMoreData() MOZ_REQUIRES(mTaskQueueCapability);
+ void RejectAppend(const MediaResult& aRejectValue, const char* aName)
+ MOZ_REQUIRES(mTaskQueueCapability);
+ // Will return a promise that will be resolved once all frames of the current
+ // media segment have been processed.
+ RefPtr<CodedFrameProcessingPromise> CodedFrameProcessing()
+ MOZ_REQUIRES(mTaskQueueCapability);
+ void CompleteCodedFrameProcessing() MOZ_REQUIRES(mTaskQueueCapability);
+ // Called by ResetParserState.
+ void CompleteResetParserState() MOZ_REQUIRES(mTaskQueueCapability);
+ RefPtr<RangeRemovalPromise> CodedFrameRemovalWithPromise(
+ media::TimeInterval aInterval) MOZ_REQUIRES(mTaskQueueCapability);
+ bool CodedFrameRemoval(media::TimeInterval aInterval)
+ MOZ_REQUIRES(mTaskQueueCapability);
+ // Removes all coded frames -- this is not to spec and should be used as a
+ // last resort to clear buffers only if other methods cannot.
+ void RemoveAllCodedFrames() MOZ_REQUIRES(mTaskQueueCapability);
+ void SetAppendState(SourceBufferAttributes::AppendState aAppendState)
+ MOZ_REQUIRES(mTaskQueueCapability);
+
+ bool HasVideo() const { return mVideoTracks.mNumTracks > 0; }
+ bool HasAudio() const { return mAudioTracks.mNumTracks > 0; }
+
+ // The input buffer as per
+ // http://w3c.github.io/media-source/index.html#sourcebuffer-input-buffer
+ Maybe<MediaSpan> mInputBuffer MOZ_GUARDED_BY(mTaskQueueCapability);
+ // Buffer full flag as per
+ // https://w3c.github.io/media-source/#sourcebuffer-buffer-full-flag. Accessed
+ // on both the main thread and the task queue.
+ Atomic<bool> mBufferFull;
+ bool mFirstInitializationSegmentReceived MOZ_GUARDED_BY(mTaskQueueCapability);
+ bool mChangeTypeReceived MOZ_GUARDED_BY(mTaskQueueCapability);
+ // Set to true once a new segment is started.
+ bool mNewMediaSegmentStarted MOZ_GUARDED_BY(mTaskQueueCapability);
+ bool mActiveTrack MOZ_GUARDED_BY(mTaskQueueCapability);
+ MediaContainerType mType MOZ_GUARDED_BY(mTaskQueueCapability);
+
+ // ContainerParser objects and methods.
+ // Those are used to parse the incoming input buffer.
+
+ // Recreate the ContainerParser and if aReuseInitData is true then
+ // feed it with the previous init segment found.
+ void RecreateParser(bool aReuseInitData) MOZ_REQUIRES(mTaskQueueCapability);
+ UniquePtr<ContainerParser> mParser;
+
+ // Demuxer objects and methods.
+ void AppendDataToCurrentInputBuffer(const MediaSpan& aData)
+ MOZ_REQUIRES(mTaskQueueCapability);
+
+ RefPtr<MediaByteBuffer> mInitData MOZ_GUARDED_BY(mTaskQueueCapability);
+
+ // Checks if a new set of init data is a repeat of the last set of init data
+ // received. Because streams may retransmit the same init data (or
+ // functionally equivalent init data) we do not want to perform costly
+ // operations each time we receive init data, only when it's actually
+ // different data.
+ bool IsRepeatInitData(const MediaInfo& aNewMediaInfo) const
+ MOZ_REQUIRES(mTaskQueueCapability);
+
+ // Temporary input buffer to handle partial media segment header.
+ // We store the current input buffer content into it should we need to
+ // reinitialize the demuxer once we have some samples and a discontinuity is
+ // detected.
+ Maybe<MediaSpan> mPendingInputBuffer MOZ_GUARDED_BY(mTaskQueueCapability);
+ RefPtr<SourceBufferResource> mCurrentInputBuffer
+ MOZ_GUARDED_BY(mTaskQueueCapability);
+ RefPtr<MediaDataDemuxer> mInputDemuxer MOZ_GUARDED_BY(mTaskQueueCapability);
+ // Length already processed in current media segment.
+ uint64_t mProcessedInput MOZ_GUARDED_BY(mTaskQueueCapability);
+ Maybe<media::TimeUnit> mLastParsedEndTime
+ MOZ_GUARDED_BY(mTaskQueueCapability);
+
+ void OnDemuxerInitDone(const MediaResult& aResult);
+ void OnDemuxerInitFailed(const MediaResult& aFailure);
+ void OnDemuxerResetDone(const MediaResult& aResult)
+ MOZ_REQUIRES(mTaskQueueCapability);
+ MozPromiseRequestHolder<MediaDataDemuxer::InitPromise> mDemuxerInitRequest;
+
+ void OnDemuxFailed(TrackType aTrack, const MediaResult& aError)
+ MOZ_REQUIRES(mTaskQueueCapability);
+ void DoDemuxVideo() MOZ_REQUIRES(mTaskQueueCapability);
+ void OnVideoDemuxCompleted(RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples);
+ void OnVideoDemuxFailed(const MediaResult& aError) {
+ mVideoTracks.mDemuxRequest.Complete();
+ mTaskQueueCapability->AssertOnCurrentThread();
+ OnDemuxFailed(TrackType::kVideoTrack, aError);
+ }
+ void DoDemuxAudio() MOZ_REQUIRES(mTaskQueueCapability);
+ void OnAudioDemuxCompleted(RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples);
+ void OnAudioDemuxFailed(const MediaResult& aError) {
+ mAudioTracks.mDemuxRequest.Complete();
+ mTaskQueueCapability->AssertOnCurrentThread();
+ OnDemuxFailed(TrackType::kAudioTrack, aError);
+ }
+
+ // Dispatches an "encrypted" event is any sample in array has initData
+ // present.
+ void MaybeDispatchEncryptedEvent(
+ const nsTArray<RefPtr<MediaRawData>>& aSamples);
+
+ void DoEvictData(const media::TimeUnit& aPlaybackTime, int64_t aSizeToEvict)
+ MOZ_REQUIRES(mTaskQueueCapability);
+
+ void GetDebugInfo(dom::TrackBuffersManagerDebugInfo& aInfo) const
+ MOZ_REQUIRES(mTaskQueueCapability);
+
+ struct TrackData {
+ TrackData() : mNumTracks(0), mNeedRandomAccessPoint(true), mSizeBuffer(0) {}
+ Atomic<uint32_t> mNumTracks;
+ // Definition of variables:
+ // https://w3c.github.io/media-source/#track-buffers
+ // Last decode timestamp variable that stores the decode timestamp of the
+ // last coded frame appended in the current coded frame group.
+ // The variable is initially unset to indicate that no coded frames have
+ // been appended yet.
+ Maybe<media::TimeUnit> mLastDecodeTimestamp;
+ // Last frame duration variable that stores the coded frame duration of the
+ // last coded frame appended in the current coded frame group.
+ // The variable is initially unset to indicate that no coded frames have
+ // been appended yet.
+ Maybe<media::TimeUnit> mLastFrameDuration;
+ // Highest end timestamp variable that stores the highest coded frame end
+ // timestamp across all coded frames in the current coded frame group that
+ // were appended to this track buffer.
+ // The variable is initially unset to indicate that no coded frames have
+ // been appended yet.
+ Maybe<media::TimeUnit> mHighestEndTimestamp;
+ // Highest presentation timestamp in track buffer.
+ // Protected by global monitor, except when reading on the task queue as it
+ // is only written there.
+ media::TimeUnit mHighestStartTimestamp;
+ // Longest frame duration seen since last random access point.
+ // Only ever accessed when mLastDecodeTimestamp and mLastFrameDuration are
+ // set.
+ media::TimeUnit mLongestFrameDuration;
+ // Need random access point flag variable that keeps track of whether the
+ // track buffer is waiting for a random access point coded frame.
+ // The variable is initially set to true to indicate that random access
+ // point coded frame is needed before anything can be added to the track
+ // buffer.
+ bool mNeedRandomAccessPoint;
+ RefPtr<MediaTrackDemuxer> mDemuxer;
+ MozPromiseRequestHolder<MediaTrackDemuxer::SamplesPromise> mDemuxRequest;
+ // Highest end timestamp of the last media segment demuxed.
+ media::TimeUnit mLastParsedEndTime;
+
+ // If set, position where the next contiguous frame will be inserted.
+ // If a discontinuity is detected, it will be unset and recalculated upon
+ // the next insertion.
+ Maybe<uint32_t> mNextInsertionIndex;
+ // Samples just demuxed, but not yet parsed.
+ TrackBuffer mQueuedSamples;
+ const TrackBuffer& GetTrackBuffer() const {
+ MOZ_RELEASE_ASSERT(mBuffers.Length(),
+ "TrackBuffer must have been created");
+ return mBuffers.LastElement();
+ }
+ TrackBuffer& GetTrackBuffer() {
+ MOZ_RELEASE_ASSERT(mBuffers.Length(),
+ "TrackBuffer must have been created");
+ return mBuffers.LastElement();
+ }
+ // We only manage a single track of each type at this time.
+ nsTArray<TrackBuffer> mBuffers;
+ // Track buffer ranges variable that represents the presentation time ranges
+ // occupied by the coded frames currently stored in the track buffer.
+ media::TimeIntervals mBufferedRanges;
+ // Sanitized mBufferedRanges with a fuzz of half a sample's duration applied
+ // This buffered ranges is the basis of what is exposed to the JS.
+ media::TimeIntervals mSanitizedBufferedRanges;
+ // Byte size of all samples contained in this track buffer.
+ uint32_t mSizeBuffer;
+ // TrackInfo of the first metadata received.
+ RefPtr<TrackInfoSharedPtr> mInfo;
+ // TrackInfo of the last metadata parsed (updated with each init segment.
+ RefPtr<TrackInfoSharedPtr> mLastInfo;
+
+ // If set, position of the next sample to be retrieved by GetSample().
+ // If the position is equal to the TrackBuffer's length, it indicates that
+ // we've reached EOS.
+ Maybe<uint32_t> mNextGetSampleIndex;
+ // Approximation of the next sample's decode timestamp.
+ media::TimeUnit mNextSampleTimecode;
+ // Approximation of the next sample's presentation timestamp.
+ media::TimeUnit mNextSampleTime;
+
+ struct EvictionIndex {
+ EvictionIndex() { Reset(); }
+ void Reset() {
+ mEvictable = 0;
+ mLastIndex = 0;
+ }
+ uint32_t mEvictable;
+ uint32_t mLastIndex;
+ };
+ // Size of data that can be safely evicted during the next eviction
+ // cycle.
+ // We consider as evictable all frames up to the last keyframe prior to
+ // mNextGetSampleIndex. If mNextGetSampleIndex isn't set, then we assume
+ // that we can't yet evict data.
+ // Protected by global monitor, except when reading on the task queue as it
+ // is only written there.
+ EvictionIndex mEvictionIndex;
+
+ void ResetAppendState() {
+ mLastDecodeTimestamp.reset();
+ mLastFrameDuration.reset();
+ mHighestEndTimestamp.reset();
+ mNeedRandomAccessPoint = true;
+ mNextInsertionIndex.reset();
+ }
+
+ void Reset() {
+ ResetAppendState();
+ mEvictionIndex.Reset();
+ for (auto& buffer : mBuffers) {
+ buffer.Clear();
+ }
+ mSizeBuffer = 0;
+ mNextGetSampleIndex.reset();
+ mBufferedRanges.Clear();
+ mSanitizedBufferedRanges.Clear();
+ }
+
+ void AddSizeOfResources(MediaSourceDecoder::ResourceSizes* aSizes) const;
+ };
+
+ void CheckSequenceDiscontinuity(const media::TimeUnit& aPresentationTime)
+ MOZ_REQUIRES(mTaskQueueCapability);
+ void ProcessFrames(TrackBuffer& aSamples, TrackData& aTrackData)
+ MOZ_REQUIRES(mTaskQueueCapability);
+ media::TimeInterval PresentationInterval(const TrackBuffer& aSamples) const
+ MOZ_REQUIRES(mTaskQueueCapability);
+ bool CheckNextInsertionIndex(TrackData& aTrackData,
+ const media::TimeUnit& aSampleTime)
+ MOZ_REQUIRES(mTaskQueueCapability);
+ void InsertFrames(TrackBuffer& aSamples,
+ const media::TimeIntervals& aIntervals,
+ TrackData& aTrackData) MOZ_REQUIRES(mTaskQueueCapability);
+ void UpdateHighestTimestamp(TrackData& aTrackData,
+ const media::TimeUnit& aHighestTime)
+ MOZ_REQUIRES(mTaskQueueCapability);
+ // Remove all frames and their dependencies contained in aIntervals.
+ // Return the index at which frames were first removed or 0 if no frames
+ // removed.
+ enum class RemovalMode {
+ kRemoveFrame,
+ kTruncateFrame,
+ };
+ uint32_t RemoveFrames(const media::TimeIntervals& aIntervals,
+ TrackData& aTrackData, uint32_t aStartIndex,
+ RemovalMode aMode);
+ // Recalculate track's evictable amount.
+ void ResetEvictionIndex(TrackData& aTrackData);
+ void UpdateEvictionIndex(TrackData& aTrackData, uint32_t aCurrentIndex);
+ // Find index of sample. Return a negative value if not found.
+ uint32_t FindSampleIndex(const TrackBuffer& aTrackBuffer,
+ const media::TimeInterval& aInterval);
+ const MediaRawData* GetSample(TrackInfo::TrackType aTrack, uint32_t aIndex,
+ const media::TimeUnit& aExpectedDts,
+ const media::TimeUnit& aExpectedPts,
+ const media::TimeUnit& aFuzz);
+ void UpdateBufferedRanges();
+ void RejectProcessing(const MediaResult& aRejectValue, const char* aName);
+ void ResolveProcessing(bool aResolveValue, const char* aName);
+ MozPromiseRequestHolder<CodedFrameProcessingPromise> mProcessingRequest;
+ MozPromiseHolder<CodedFrameProcessingPromise> mProcessingPromise;
+
+ // Trackbuffers definition.
+ nsTArray<const TrackData*> GetTracksList() const;
+ nsTArray<TrackData*> GetTracksList();
+ TrackData& GetTracksData(TrackType aTrack) {
+ switch (aTrack) {
+ case TrackType::kVideoTrack:
+ return mVideoTracks;
+ case TrackType::kAudioTrack:
+ default:
+ return mAudioTracks;
+ }
+ }
+ const TrackData& GetTracksData(TrackType aTrack) const {
+ switch (aTrack) {
+ case TrackType::kVideoTrack:
+ return mVideoTracks;
+ case TrackType::kAudioTrack:
+ default:
+ return mAudioTracks;
+ }
+ }
+ TrackData mVideoTracks;
+ TrackData mAudioTracks;
+
+ // TaskQueue methods and objects.
+ RefPtr<TaskQueue> GetTaskQueueSafe() const {
+ MutexAutoLock mut(mMutex);
+ return mTaskQueue;
+ }
+ NotNull<AbstractThread*> TaskQueueFromTaskQueue() const {
+#ifdef DEBUG
+ RefPtr<TaskQueue> taskQueue = GetTaskQueueSafe();
+ MOZ_ASSERT(taskQueue && taskQueue->IsCurrentThreadIn());
+#endif
+ return WrapNotNull(mTaskQueue.get());
+ }
+ bool OnTaskQueue() const {
+ auto taskQueue = TaskQueueFromTaskQueue();
+ return taskQueue->IsCurrentThreadIn();
+ }
+ void ResetTaskQueue() {
+ MutexAutoLock mut(mMutex);
+ mTaskQueue = nullptr;
+ }
+
+ // SourceBuffer Queues and running context.
+ SourceBufferTaskQueue mQueue;
+ void QueueTask(SourceBufferTask* aTask);
+ void ProcessTasks();
+ // Set if the TrackBuffersManager is currently processing a task.
+ // At this stage, this task is always a AppendBufferTask.
+ RefPtr<SourceBufferTask> mCurrentTask MOZ_GUARDED_BY(mTaskQueueCapability);
+ // Current SourceBuffer state for ongoing task.
+ // Its content is returned to the SourceBuffer once the AppendBufferTask has
+ // completed.
+ UniquePtr<SourceBufferAttributes> mSourceBufferAttributes
+ MOZ_GUARDED_BY(mTaskQueueCapability);
+ // The current sourcebuffer append window. It's content is equivalent to
+ // mSourceBufferAttributes.mAppendWindowStart/End
+ media::TimeInterval mAppendWindow MOZ_GUARDED_BY(mTaskQueueCapability);
+
+ // Strong references to external objects.
+ nsMainThreadPtrHandle<MediaSourceDecoder> mParentDecoder;
+
+ const RefPtr<AbstractThread> mAbstractMainThread;
+
+ // Return public highest end time across all aTracks.
+ // Monitor must be held.
+ media::TimeUnit HighestEndTime(
+ nsTArray<const media::TimeIntervals*>& aTracks) const;
+
+ // Set to true if mediasource state changed to ended.
+ Atomic<bool> mEnded;
+
+ // Global size of this source buffer content.
+ Atomic<int64_t> mSizeSourceBuffer;
+ const int64_t mVideoEvictionThreshold;
+ const int64_t mAudioEvictionThreshold;
+ enum class EvictionState {
+ NO_EVICTION_NEEDED,
+ EVICTION_NEEDED,
+ EVICTION_COMPLETED,
+ };
+ Atomic<EvictionState> mEvictionState;
+
+ // Monitor to protect following objects accessed across multiple threads.
+ mutable Mutex mMutex MOZ_UNANNOTATED;
+ // mTaskQueue is only ever written after construction on the task queue.
+ // As such, it can be accessed while on task queue without the need for the
+ // mutex.
+ RefPtr<TaskQueue> mTaskQueue;
+ // Stable audio and video track time ranges.
+ media::TimeIntervals mVideoBufferedRanges;
+ media::TimeIntervals mAudioBufferedRanges;
+ // MediaInfo of the first init segment read.
+ MediaInfo mInfo;
+ // End mutex protected members.
+
+ // EventTargetCapability used to ensure we're running on the task queue
+ // as expected for various accesses.
+ // TODO: we could store only this and dispatch to it, rather than also having
+ // mTaskQueue. However, there's special locking around mTaskQueue, so we keep
+ // both for now.
+ Maybe<EventTargetCapability<TaskQueue>> mTaskQueueCapability;
+};
+
+} // namespace mozilla
+
+#endif /* MOZILLA_TRACKBUFFERSMANAGER_H_ */
diff --git a/dom/media/mediasource/gtest/TestContainerParser.cpp b/dom/media/mediasource/gtest/TestContainerParser.cpp
new file mode 100644
index 0000000000..dd5a87b64b
--- /dev/null
+++ b/dom/media/mediasource/gtest/TestContainerParser.cpp
@@ -0,0 +1,148 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <gtest/gtest.h>
+#include <stdint.h>
+
+#include "ContainerParser.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/gtest/MozAssertions.h"
+
+using namespace mozilla;
+using TimeUnit = mozilla::media::TimeUnit;
+
+TEST(ContainerParser, MIMETypes)
+{
+ const char* containerTypes[] = {"video/webm", "audio/webm", "video/mp4",
+ "audio/mp4", "audio/aac"};
+ UniquePtr<ContainerParser> parser;
+ for (size_t i = 0; i < ArrayLength(containerTypes); ++i) {
+ Maybe<MediaContainerType> containerType =
+ MakeMediaContainerType(containerTypes[i]);
+ ASSERT_TRUE(containerType.isSome());
+ parser = ContainerParser::CreateForMIMEType(*containerType);
+ ASSERT_NE(parser, nullptr);
+ }
+}
+
+already_AddRefed<MediaByteBuffer> make_adts_header() {
+ const uint8_t test[] = {0xff, 0xf1, 0x50, 0x80, 0x03, 0x1f, 0xfc};
+ RefPtr<MediaByteBuffer> buffer(new MediaByteBuffer);
+ buffer->AppendElements(test, ArrayLength(test));
+ return buffer.forget();
+}
+
+TEST(ContainerParser, ADTSHeader)
+{
+ UniquePtr<ContainerParser> parser;
+ parser = ContainerParser::CreateForMIMEType(
+ MediaContainerType(MEDIAMIMETYPE("audio/aac")));
+ ASSERT_NE(parser, nullptr);
+
+ // Audio data should have no gaps.
+ EXPECT_EQ(parser->GetRoundingError(), 0);
+
+ // Test a valid header.
+ RefPtr<MediaByteBuffer> header = make_adts_header();
+ EXPECT_NS_SUCCEEDED(parser->IsInitSegmentPresent(MediaSpan(header)));
+
+ // Test variations.
+ uint8_t save = header->ElementAt(1);
+ for (uint8_t i = 1; i < 3; ++i) {
+ // Set non-zero layer.
+ header->ReplaceElementAt(1, (header->ElementAt(1) & 0xf9) | (i << 1));
+ EXPECT_FALSE(NS_SUCCEEDED(parser->IsInitSegmentPresent(MediaSpan(header))))
+ << "Accepted non-zero layer in header.";
+ }
+ header->ReplaceElementAt(1, save);
+ save = header->ElementAt(2);
+ header->ReplaceElementAt(2, (header->ElementAt(2) & 0x3b) | (15 << 2));
+ EXPECT_FALSE(NS_SUCCEEDED(parser->IsInitSegmentPresent(MediaSpan(header))))
+ << "Accepted explicit frequency in header.";
+ header->ReplaceElementAt(2, save);
+
+ // Test a short header.
+ header->SetLength(6);
+ EXPECT_FALSE(NS_SUCCEEDED(parser->IsInitSegmentPresent(MediaSpan(header))))
+ << "Accepted too-short header.";
+ EXPECT_FALSE(NS_SUCCEEDED(parser->IsMediaSegmentPresent(MediaSpan(header))))
+ << "Found media segment when there was just a partial header.";
+
+ // Test a header with short data.
+ header = make_adts_header();
+ header->AppendElements(1);
+ EXPECT_TRUE(NS_SUCCEEDED(parser->IsInitSegmentPresent(MediaSpan(header))))
+ << "Rejected a valid header.";
+ EXPECT_TRUE(NS_SUCCEEDED(parser->IsMediaSegmentPresent(MediaSpan(header))))
+ << "Rejected a one-byte media segment.";
+
+ // Test parse results.
+ header = make_adts_header();
+ EXPECT_FALSE(NS_SUCCEEDED(parser->IsMediaSegmentPresent(MediaSpan(header))))
+ << "Found media segment when there was just a header.";
+ TimeUnit start;
+ TimeUnit end;
+ EXPECT_TRUE(NS_FAILED(
+ parser->ParseStartAndEndTimestamps(MediaSpan(header), start, end)));
+
+ EXPECT_TRUE(parser->HasInitData());
+ EXPECT_TRUE(parser->HasCompleteInitData());
+ MediaByteBuffer* init = parser->InitData();
+ ASSERT_NE(init, nullptr);
+ EXPECT_EQ(init->Length(), header->Length());
+
+ EXPECT_EQ(parser->InitSegmentRange(),
+ MediaByteRange(0, int64_t(header->Length())));
+ // Media segment range should be empty here.
+ EXPECT_EQ(parser->MediaHeaderRange(), MediaByteRange());
+ EXPECT_EQ(parser->MediaSegmentRange(), MediaByteRange());
+}
+
+TEST(ContainerParser, ADTSBlankMedia)
+{
+ UniquePtr<ContainerParser> parser;
+ parser = ContainerParser::CreateForMIMEType(
+ MediaContainerType(MEDIAMIMETYPE("audio/aac")));
+ ASSERT_NE(parser, nullptr);
+
+ // Audio data should have no gaps.
+ EXPECT_EQ(parser->GetRoundingError(), 0);
+
+ // Test the header only.
+ RefPtr<MediaByteBuffer> header = make_adts_header();
+ EXPECT_NS_SUCCEEDED(parser->IsInitSegmentPresent(MediaSpan(header)));
+
+ // Test with the correct length of (invalid) frame data.
+ size_t header_length = header->Length();
+ size_t data_length = 24;
+ size_t frame_length = header_length + data_length;
+ header->AppendElements(data_length);
+ EXPECT_TRUE(NS_SUCCEEDED(parser->IsInitSegmentPresent(MediaSpan(header))))
+ << "Rejected a valid header.";
+ EXPECT_TRUE(NS_SUCCEEDED(parser->IsMediaSegmentPresent(MediaSpan(header))))
+ << "Rejected a full (but zeroed) media segment.";
+ TimeUnit start;
+ TimeUnit end;
+ // We don't report timestamps from ADTS.
+ EXPECT_TRUE(NS_FAILED(
+ parser->ParseStartAndEndTimestamps(MediaSpan(header), start, end)));
+ EXPECT_TRUE(start.IsZero());
+ EXPECT_TRUE(end.IsZero());
+
+ // Verify the parser calculated header and packet data boundaries.
+ EXPECT_TRUE(parser->HasInitData());
+ EXPECT_TRUE(parser->HasCompleteInitData());
+ MediaByteBuffer* init = parser->InitData();
+ ASSERT_NE(init, nullptr);
+ EXPECT_EQ(init->Length(), header_length)
+ << "Found incorrect init segment length.";
+ EXPECT_EQ(parser->InitSegmentRange(),
+ MediaByteRange(0, int64_t(header_length)));
+ // In ADTS the Media Header is the same as the Media Segment.
+ MediaByteRange expected_media =
+ MediaByteRange(int64_t(header_length), int64_t(frame_length));
+ EXPECT_EQ(parser->MediaHeaderRange(), expected_media);
+ EXPECT_EQ(parser->MediaSegmentRange(), expected_media);
+}
diff --git a/dom/media/mediasource/gtest/TestExtractAV1CodecDetails.cpp b/dom/media/mediasource/gtest/TestExtractAV1CodecDetails.cpp
new file mode 100644
index 0000000000..8683023204
--- /dev/null
+++ b/dom/media/mediasource/gtest/TestExtractAV1CodecDetails.cpp
@@ -0,0 +1,290 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <gtest/gtest.h>
+#include <stdint.h>
+
+#include "AOMDecoder.h"
+#include "VideoUtils.h"
+
+namespace mozilla {
+void PrintTo(const AOMDecoder::AV1SequenceInfo& aInfo, std::ostream* aStream) {
+ nsAutoCString formatted = nsAutoCString();
+ formatted.AppendPrintf(
+ "av01.%01u.%02u%c.%02u.%01u.%01u%01u%01u.%02u.%02u.%02u.%01u (res: "
+ "%ux%u) operating points: [",
+ aInfo.mProfile, aInfo.mOperatingPoints[0].mLevel,
+ aInfo.mOperatingPoints[0].mTier == 1 ? 'H' : 'M', aInfo.mBitDepth,
+ aInfo.mMonochrome, aInfo.mSubsamplingX, aInfo.mSubsamplingY,
+ static_cast<uint8_t>(aInfo.mChromaSamplePosition),
+ static_cast<uint8_t>(aInfo.mColorSpace.mPrimaries),
+ static_cast<uint8_t>(aInfo.mColorSpace.mTransfer),
+ static_cast<uint8_t>(aInfo.mColorSpace.mMatrix),
+ static_cast<uint8_t>(aInfo.mColorSpace.mRange), aInfo.mImage.Width(),
+ aInfo.mImage.Height());
+ size_t opCount = aInfo.mOperatingPoints.Length();
+ for (size_t i = 0; i < opCount; i++) {
+ const auto& op = aInfo.mOperatingPoints[i];
+ formatted.AppendPrintf("{ layers: %x, level: %u, tier: %u }", op.mLayers,
+ op.mLevel, op.mTier);
+ if (i != opCount - 1) {
+ formatted.Append(", ");
+ }
+ }
+ formatted.Append("]");
+ *aStream << formatted;
+}
+} // namespace mozilla
+
+using namespace mozilla;
+
+struct AV1TestData {
+ const char* mCodecParameterString;
+ const bool mExpectedValue;
+ const char* mComment;
+};
+
+TEST(ExtractAV1CodecDetails, TestInputData)
+{
+ AV1TestData tests[] = {
+ // Format is:
+ // av01.N.NN[MH].NN.B.BBN.NN.NN.NN.B
+ // where
+ // N = decimal digit
+ // [] = single character
+ // B = binary digit
+ // Field order:
+ // <sample entry 4CC>.<profile>.<level><tier>.<bitDepth>
+ // [.<monochrome>.<chromaSubsampling>
+ // .<colorPrimaries>.<transferCharacteristics>.<matrixCoefficients>
+ // .<videoFullRangeFlag>]
+
+ // Format checks
+ {"av01.0.10M.08", true, "Minimum length"},
+ {"av1.0.10M.08", false, "Invalid 4CC"},
+ {"av01..10M.08", false, "Blank field"},
+ {"av01.-1.10M.08", false, "Negative field"},
+ {"av01.0.10M.8", false, "Missing leading zeros"},
+
+ // Field counts
+ {"av01", false, "0 of 4 required fields"},
+ {"av01.0", false, "1 of 4 required fields"},
+ {"av01.0.10", false, "2 of 4 required fields"},
+ {"av01.0.10M", false, "3 of 4 required fields"},
+ {"av01.0.10M.08.0", false, "5 fields, AV1 requires 4 or 10"},
+ {"av01.0.10M.08.0.110.01.01.01", false, "9 fields, AV1 requires 4 or 10"},
+ {"av01.0.10M.08.0.110.01.01.01.0", true, "Maximum fields"},
+ {"av01.0.10M.08.0.110.01.01.01.0.0", false, "Too many fields"},
+
+ // "Comments" are allowed (unknown characters at the end of fields)
+ {"av01.0.10M.08this is ignored", true, "Minimum length with comment"},
+ {"av01.0.10Mbad comment", false, "Comment before required field"},
+ {"av01.0.10M.08.0.110.01.01.01.0also ignored", true,
+ "Maximum length with comment"},
+
+ // Begin field checks
+
+ // -- Profile --
+ // Main Profile (0) tested above
+
+ // High Profile requires 4:4:4 chroma subsampling without monochrome
+ {"av01.1.10M.08", false, "High Profile (1) without parameters"},
+ {"av01.1.10M.08.0.000.01.01.01.0", true, "High Profile (1)"},
+
+ // Professional requires either of:
+ // - 8bit or 10bit at 4:2:2
+ // - 12bit at any subsampling
+ {"av01.2.10M.10.0.100.01.01.01.0", true,
+ "Professional Profile (2) 10-bit 4:2:2"},
+ {"av01.2.10M.12.0.110.01.01.01.0", true,
+ "Professional Profile (2) 12-bit 4:2:0"},
+
+ {"av01.3.10M.12.0.000.01.01.01.0", false, "Invalid Profile 3"},
+
+ // -- Level --
+ {"av01.0.00M.08", true, "Level 0 (2.1)"},
+ // Level 4.2 (10) tested above
+ {"av01.0.14M.08", true, "Level 14 (5.2)"},
+ {"av01.0.23M.08", true, "Level 23 (7.3)"},
+ {"av01.0.24M.08", false, "Level 24 (Reserved)"},
+
+ // -- Tier --
+ // Main tier tested above
+ {"av01.0.10H.08", true, "High tier"},
+
+ // -- Bit depth --
+ // 8-bit tested above with Main and High Profiles
+ {"av01.0.10M.10", true, "Main 10-bit"},
+ {"av01.1.10M.10.0.000.01.01.01.0", true, "High 10-bit"},
+ {"av01.1.10M.12.0.000.01.01.01.0", false, "High 12-bit (Invalid)"},
+ // Valid 12-bit tested for Professional Profile
+
+ // -- Monochrome --
+ // Monochrome off tested above
+ {"av01.0.10M.08.1.110.01.01.01.0", true, "Main 8-bit monochrome"},
+ {"av01.1.10M.10.1.000.01.01.01.0", false,
+ "4:4:4 is incompatible with monochrome"},
+ {"av01.2.10M.10.1.100.01.01.01.0", false,
+ "4:2:0 is incompatible with monochrome"},
+ {"av01.2.10M.12.1.110.01.01.01.0", true,
+ "Professional 12-bit monochrome"},
+
+ // -- Chroma subsampling --
+ // Field is parsed by digits <x><y><position>
+ // where positions are [unknown, vertical, colocated]
+ {"av01.0.10M.08.0.112.01.01.01.0", true, "Chroma colocated"},
+ // Main Profile, 4:2:0 tested above
+ {"av01.0.10M.08.0.100.01.01.01.0", false,
+ "4:2:2 not allowed on Main Profile"},
+ // High Profile, 4:4:4 tested above
+ {"av01.1.10M.08.0.110.01.01.01.0", false,
+ "4:4:4 required on High Profile"},
+ {"av01.2.10M.08.0.110.01.01.01.0", false,
+ "4:2:0 not allowed on 8-bit Professional"},
+ // Professional Profile, 8-bit 4:2:2 tested above
+ // Professional Profile, 12-bit 4:2:0 tested above
+ {"av01.2.10M.12.0.100.01.01.01.0", true, "12-bit 4:2:2"},
+ {"av01.2.10M.12.0.000.01.01.01.0", true, "12-bit 4:4:4"},
+
+ {"av01.2.10M.08.0.101.01.01.01.0", false, "Chroma position with 4:2:2"},
+ {"av01.1.10M.08.0.001.01.01.01.0", false, "Chroma position with 4:4:4"},
+ {"av01.0.10M.08.0.113.01.01.01.0", false, "Chroma position 3 (Reserved)"},
+
+ // -- Color primaries --
+ // 0, 3, [13-21], >23 are reserved
+ // 1 (BT709) is tested above
+ {"av01.0.10M.10.0.110.09.16.09.0", true,
+ "Color space: BT2020/SMPTE2084/BT2020NCL"},
+ {"av01.0.10M.10.0.110.00.16.09.0", false, "Primaries 0: Reserved"},
+ {"av01.0.10M.10.0.110.03.16.09.0", false, "Primaries 3: Reserved"},
+ {"av01.0.10M.10.0.110.13.16.09.0", false, "Primaries 13: Reserved"},
+ {"av01.0.10M.10.0.110.21.16.09.0", false, "Primaries 21: Reserved"},
+ {"av01.0.10M.10.0.110.22.16.09.0", true, "Primaries 22: EBU3213"},
+ {"av01.0.10M.10.0.110.23.16.09.0", false, "Primaries 23: Reserved"},
+
+ // -- Transfer characteristics --
+ // 0, 3, >19 are all reserved
+ // 1 (BT709) is tested above
+ // 16 (SMPTE2084) is tested above
+ {"av01.0.10M.10.0.110.09.14.09.0", true,
+ "Color space: BT2020/BT2020 10-bit/BT2020NCL"},
+ {"av01.0.10M.10.0.110.09.00.09.0", false, "Transfer 0: Reserved"},
+ {"av01.0.10M.10.0.110.09.03.09.0", false, "Transfer 3: Reserved"},
+ {"av01.0.10M.10.0.110.09.20.09.0", false, "Transfer 20: Reserved"},
+
+ // -- Matrix coefficients --
+ // 3, >15 are all reserved
+ // 1 (BT709) is tested above
+ // 9 (BT2020NCL) is tested above
+ {"av01.1.10M.10.0.000.01.13.00.1", true, "4:4:4 10-bit sRGB"},
+ {"av01.1.10M.10.0.000.01.13.00.0", false, "sRGB requires full range"},
+ {"av01.2.10M.10.0.100.01.13.00.1", false,
+ "Subsampling incompatible with sRGB"},
+ {"av01.2.10M.12.0.000.01.13.00.1", true, "4:4:4 12-bit sRGB"},
+ {"av01.2.10M.12.0.000.01.01.15.1", false, "Matrix 15: Reserved"},
+
+ // -- Color range --
+ // Full range and limited range tested above
+ {"av01.0.10M.12.0.002.01.13.00.2", false, "Color range 2 invalid"},
+ };
+
+ for (const auto& data : tests) {
+ auto info = AOMDecoder::CreateSequenceInfoFromCodecs(
+ NS_ConvertUTF8toUTF16(data.mCodecParameterString));
+ nsAutoCString desc = nsAutoCString(data.mCodecParameterString,
+ strlen(data.mCodecParameterString));
+ desc.AppendLiteral(" (");
+ desc.Append(data.mComment, strlen(data.mComment));
+ desc.AppendLiteral(")");
+ EXPECT_EQ(info.isSome(), data.mExpectedValue) << desc;
+
+ if (info.isSome()) {
+ AOMDecoder::AV1SequenceInfo inputInfo = info.value();
+ inputInfo.mImage = gfx::IntSize(1920, 1080);
+ RefPtr<MediaByteBuffer> buffer = new MediaByteBuffer();
+ bool wroteSequenceHeader;
+ AOMDecoder::WriteAV1CBox(inputInfo, buffer, wroteSequenceHeader);
+ EXPECT_EQ(wroteSequenceHeader, data.mExpectedValue) << desc;
+ // Read equality test will fail also, don't clutter.
+ if (!wroteSequenceHeader) {
+ continue;
+ }
+ AOMDecoder::AV1SequenceInfo parsedInfo;
+ bool readSequenceHeader;
+ AOMDecoder::ReadAV1CBox(buffer, parsedInfo, readSequenceHeader);
+ EXPECT_EQ(wroteSequenceHeader, readSequenceHeader) << desc;
+ EXPECT_EQ(inputInfo, parsedInfo) << desc;
+ }
+ }
+}
+
+TEST(ExtractAV1CodecDetails, TestParsingOutput)
+{
+ auto info = AOMDecoder::CreateSequenceInfoFromCodecs(
+ nsString(u"av01.0.14M.08.0.112.01.01.01.0"));
+ EXPECT_TRUE(info.isSome());
+
+ if (info.isSome()) {
+ EXPECT_EQ(info->mProfile, 0u);
+ EXPECT_EQ(info->mOperatingPoints.Length(), 1u);
+ EXPECT_EQ(info->mOperatingPoints[0].mLayers, 0u);
+ EXPECT_EQ(info->mOperatingPoints[0].mLevel, 14u);
+ EXPECT_EQ(info->mOperatingPoints[0].mTier, 0u);
+ EXPECT_EQ(info->mBitDepth, 8u);
+ EXPECT_EQ(info->mMonochrome, false);
+ EXPECT_EQ(info->mSubsamplingX, true);
+ EXPECT_EQ(info->mSubsamplingY, true);
+ EXPECT_EQ(info->mChromaSamplePosition,
+ AOMDecoder::ChromaSamplePosition::Colocated);
+ EXPECT_EQ(info->mColorSpace.mPrimaries, gfx::CICP::CP_BT709);
+ EXPECT_EQ(info->mColorSpace.mTransfer, gfx::CICP::TC_BT709);
+ EXPECT_EQ(info->mColorSpace.mMatrix, gfx::CICP::MC_BT709);
+ EXPECT_EQ(info->mColorSpace.mRange, gfx::ColorRange::LIMITED);
+ }
+
+ info = AOMDecoder::CreateSequenceInfoFromCodecs(
+ nsString(u"av01.1.11H.10.0.000.07.07.07.1"));
+ EXPECT_TRUE(info.isSome());
+
+ if (info.isSome()) {
+ EXPECT_EQ(info->mProfile, 1u);
+ EXPECT_EQ(info->mOperatingPoints.Length(), 1u);
+ EXPECT_EQ(info->mOperatingPoints[0].mLayers, 0u);
+ EXPECT_EQ(info->mOperatingPoints[0].mLevel, 11u);
+ EXPECT_EQ(info->mOperatingPoints[0].mTier, 1u);
+ EXPECT_EQ(info->mBitDepth, 10u);
+ EXPECT_EQ(info->mMonochrome, false);
+ EXPECT_EQ(info->mSubsamplingX, false);
+ EXPECT_EQ(info->mSubsamplingY, false);
+ EXPECT_EQ(info->mChromaSamplePosition,
+ AOMDecoder::ChromaSamplePosition::Unknown);
+ EXPECT_EQ(info->mColorSpace.mPrimaries, gfx::CICP::CP_SMPTE240);
+ EXPECT_EQ(info->mColorSpace.mTransfer, gfx::CICP::TC_SMPTE240);
+ EXPECT_EQ(info->mColorSpace.mMatrix, gfx::CICP::MC_SMPTE240);
+ EXPECT_EQ(info->mColorSpace.mRange, gfx::ColorRange::FULL);
+ }
+
+ info = AOMDecoder::CreateSequenceInfoFromCodecs(
+ nsString(u"av01.2.22H.12.1.110.10.08.04.1"));
+ EXPECT_TRUE(info.isSome());
+
+ if (info.isSome()) {
+ EXPECT_EQ(info->mProfile, 2u);
+ EXPECT_EQ(info->mOperatingPoints.Length(), 1u);
+ EXPECT_EQ(info->mOperatingPoints[0].mLayers, 0u);
+ EXPECT_EQ(info->mOperatingPoints[0].mLevel, 22u);
+ EXPECT_EQ(info->mOperatingPoints[0].mTier, 1u);
+ EXPECT_EQ(info->mBitDepth, 12u);
+ EXPECT_EQ(info->mMonochrome, true);
+ EXPECT_EQ(info->mSubsamplingX, true);
+ EXPECT_EQ(info->mSubsamplingY, true);
+ EXPECT_EQ(info->mChromaSamplePosition,
+ AOMDecoder::ChromaSamplePosition::Unknown);
+ EXPECT_EQ(info->mColorSpace.mPrimaries, gfx::CICP::CP_XYZ);
+ EXPECT_EQ(info->mColorSpace.mTransfer, gfx::CICP::TC_LINEAR);
+ EXPECT_EQ(info->mColorSpace.mMatrix, gfx::CICP::MC_FCC);
+ EXPECT_EQ(info->mColorSpace.mRange, gfx::ColorRange::FULL);
+ }
+}
diff --git a/dom/media/mediasource/gtest/TestExtractVPXCodecDetails.cpp b/dom/media/mediasource/gtest/TestExtractVPXCodecDetails.cpp
new file mode 100644
index 0000000000..7e255e2dc7
--- /dev/null
+++ b/dom/media/mediasource/gtest/TestExtractVPXCodecDetails.cpp
@@ -0,0 +1,141 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <gtest/gtest.h>
+#include <stdint.h>
+
+#include "VideoUtils.h"
+
+using namespace mozilla;
+
+struct TestData {
+ const char16_t* const mCodecParameterString;
+ const bool mExpectedValue;
+ const char* const mComment;
+};
+
+TEST(ExtractVPXCodecDetails, TestInputData)
+{
+ TestData tests[] = {
+ // <sample entry 4CC>.<profile>.<level>.<bitDepth>.<chromaSubsampling>.
+ // <colourPrimaries>.<transferCharacteristics>.<matrixCoefficients>.
+ // <videoFullRangeFlag>
+
+ // Format checks
+ {u"vp09.0.10.8", true, "Valid minimum length"},
+ {u"vp9.00.10.08", false, "Invalid 4CC"},
+ {u"vp09.00..08", false, "Blank field"},
+ {u"vp09", false, "0 of 3 required fields"},
+ {u"vp09.00", false, "1 of 3 required fields"},
+ {u"vp09.00.10", false, "2 of 3 required fields"},
+
+ // Profiles
+ {u"vp09.00.10.08", true, "Profile 0"},
+ {u"vp09.01.10.08", true, "Profile 1"},
+ {u"vp09.02.10.10", true, "Profile 2"},
+ {u"vp09.03.10.10", true, "Profile 3"},
+ {u"vp09.-1.10.08", false, "Invalid profile < 0"},
+ {u"vp09.04.10.08", false, "Invalid profile > 3"},
+
+ // Levels
+ {u"vp09.00.11.08", true, "Level 1.1"},
+ {u"vp09.00.12.08", false, "Invalid level 1.2"},
+ {u"vp09.00.52.08", true, "Level 5.2"},
+ {u"vp09.00.64.08", false, "Level greater than max"},
+
+ // Bit depths
+ // - 8-bit tested in Profiles section
+ // - 10-bit tested in Profiles section
+ {u"vp09.02.10.12", true, "12-bit"},
+ {u"vp09.00.10.07", false, "Invalid, 7-bit"},
+ {u"vp09.02.10.11", false, "Invalid, 11-bit"},
+ {u"vp09.02.10.13", false, "Invalid, 13-bit"},
+
+ // Chroma subsampling
+ {u"vp09.00.10.08.00", true, "4:2:0 vertical"},
+ {u"vp09.00.10.08.01", true, "4:2:0 colocated"},
+ {u"vp09.00.10.08.02", true, "4:2:2"},
+ {u"vp09.00.10.08.03", true, "4:4:4"},
+ {u"vp09.00.10.08.04", false, "Invalid chroma"},
+
+ // Color primaries
+ {u"vp09.00.10.08.01.00", false, "CP 0: Reserved"},
+ {u"vp09.00.10.08.01.01", true, "CP 1: BT.709"},
+ {u"vp09.00.10.08.01.03", false, "CP 3: Reserved"},
+ {u"vp09.00.10.08.01.09", true, "CP 9: BT.2020"},
+ {u"vp09.00.10.08.01.21", false, "CP 21: Reserved"},
+ {u"vp09.00.10.08.01.22", true, "CP 22: EBU Tech 3213"},
+ {u"vp09.00.10.08.01.23", false, "CP 23: Out of range"},
+
+ // Transfer characteristics
+ {u"vp09.00.10.08.01.01.00", false, "TC 0: Reserved"},
+ {u"vp09.00.10.08.01.01.01", true, "TC 1: BT.709"},
+ {u"vp09.00.10.08.01.01.03", false, "TC 3: Reserved"},
+ {u"vp09.00.10.08.01.09.16", true, "TC 16: ST 2084"},
+ {u"vp09.00.10.08.01.09.19", false, "TC 19: Out of range"},
+
+ // Matrix coefficients
+ {u"vp09.00.10.08.03.09.16.00", true, "MC 0: Identity"},
+ {u"vp09.00.10.08.01.09.16.00", false, "MC 0: Identity without 4:4:4"},
+ {u"vp09.00.10.08.01.09.16.01", true, "MC 1: BT.709"},
+ {u"vp09.00.10.08.01.09.16.03", false, "MC 3: Reserved"},
+ {u"vp09.00.10.08.01.09.16.09", true, "MC 9: BT.2020"},
+ {u"vp09.00.10.08.01.09.16.15", false, "MC 15: Out of range"},
+
+ // Color range
+ {u"vp09.00.10.08.01.09.16.09.00", true, "Limited range"},
+ {u"vp09.00.10.08.01.09.16.09.01", true, "Full range"},
+ {u"vp09.00.10.08.01.09.16.09.02", false, "Invalid range value"},
+
+ {u"vp09.00.10.08.01.09.16.09.00.", false, "Extra ."},
+ {u"vp09.00.10.08.01.09.16.09.00.00", false, "More than 9 fields"},
+ };
+
+ for (const auto& data : tests) {
+ uint8_t profile = 0;
+ uint8_t level = 0;
+ uint8_t bitDepth = 0;
+ bool result = ExtractVPXCodecDetails(nsString(data.mCodecParameterString),
+ profile, level, bitDepth);
+ EXPECT_EQ(result, data.mExpectedValue)
+ << NS_ConvertUTF16toUTF8(data.mCodecParameterString).get() << " ("
+ << data.mComment << ")";
+ }
+}
+
+TEST(ExtractVPXCodecDetails, TestParsingOutput)
+{
+ uint8_t profile = 0;
+ uint8_t level = 0;
+ uint8_t bitDepth = 0;
+ uint8_t chromaSubsampling = 0;
+ VideoColorSpace colorSpace;
+ auto data = u"vp09.01.11.08";
+ bool result = ExtractVPXCodecDetails(nsString(data), profile, level, bitDepth,
+ chromaSubsampling, colorSpace);
+ EXPECT_EQ(result, true);
+ EXPECT_EQ(profile, 1);
+ EXPECT_EQ(level, 11);
+ EXPECT_EQ(bitDepth, 8);
+ // Should keep spec defined default value.
+ EXPECT_EQ(chromaSubsampling, 1);
+ EXPECT_EQ(colorSpace.mPrimaries, gfx::CICP::CP_BT709);
+ EXPECT_EQ(colorSpace.mTransfer, gfx::CICP::TC_BT709);
+ EXPECT_EQ(colorSpace.mMatrix, gfx::CICP::MC_BT709);
+ EXPECT_EQ(colorSpace.mRange, gfx::ColorRange::LIMITED);
+
+ data = u"vp09.02.10.10.01.09.16.09.01";
+ result = ExtractVPXCodecDetails(nsString(data), profile, level, bitDepth,
+ chromaSubsampling, colorSpace);
+ EXPECT_EQ(result, true);
+ EXPECT_EQ(profile, 2);
+ EXPECT_EQ(level, 10);
+ EXPECT_EQ(bitDepth, 10);
+ EXPECT_EQ(chromaSubsampling, 1);
+ EXPECT_EQ(colorSpace.mPrimaries, gfx::CICP::CP_BT2020);
+ EXPECT_EQ(colorSpace.mTransfer, gfx::CICP::TC_SMPTE2084);
+ EXPECT_EQ(colorSpace.mMatrix, gfx::CICP::MC_BT2020_NCL);
+ EXPECT_EQ(colorSpace.mRange, gfx::ColorRange::FULL);
+}
diff --git a/dom/media/mediasource/gtest/moz.build b/dom/media/mediasource/gtest/moz.build
new file mode 100644
index 0000000000..42ef6beb9b
--- /dev/null
+++ b/dom/media/mediasource/gtest/moz.build
@@ -0,0 +1,22 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES += [
+ "TestContainerParser.cpp",
+ "TestExtractVPXCodecDetails.cpp",
+]
+
+if CONFIG["MOZ_AV1"]:
+ UNIFIED_SOURCES += [
+ "TestExtractAV1CodecDetails.cpp",
+ ]
+
+LOCAL_INCLUDES += [
+ "/dom/media",
+ "/dom/media/mediasource",
+]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/dom/media/mediasource/moz.build b/dom/media/mediasource/moz.build
new file mode 100644
index 0000000000..3fa98e42b7
--- /dev/null
+++ b/dom/media/mediasource/moz.build
@@ -0,0 +1,42 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+MOCHITEST_MANIFESTS += ["test/mochitest.ini"]
+
+EXPORTS += [
+ "AsyncEventRunner.h",
+ "MediaSourceDecoder.h",
+ "MediaSourceDemuxer.h",
+ "SourceBufferAttributes.h",
+ "SourceBufferTask.h",
+ "TrackBuffersManager.h",
+]
+
+EXPORTS.mozilla.dom += [
+ "MediaSource.h",
+ "SourceBuffer.h",
+ "SourceBufferList.h",
+]
+
+UNIFIED_SOURCES += [
+ "ContainerParser.cpp",
+ "MediaSource.cpp",
+ "MediaSourceDecoder.cpp",
+ "MediaSourceDemuxer.cpp",
+ "MediaSourceUtils.cpp",
+ "ResourceQueue.cpp",
+ "SourceBuffer.cpp",
+ "SourceBufferList.cpp",
+ "SourceBufferResource.cpp",
+ "TrackBuffersManager.cpp",
+]
+
+TEST_DIRS += [
+ "gtest",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/mediasource/test/.eslintrc.js b/dom/media/mediasource/test/.eslintrc.js
new file mode 100644
index 0000000000..e283e384ba
--- /dev/null
+++ b/dom/media/mediasource/test/.eslintrc.js
@@ -0,0 +1,28 @@
+"use strict";
+
+module.exports = {
+ // Globals from mediasource.js. We use false to indicate they should not
+ // be overwritten in scripts.
+ globals: {
+ addMSEPrefs: false,
+ fetchAndLoad: false,
+ fetchAndLoadAsync: false,
+ fetchWithXHR: false,
+ logEvents: false,
+ loadSegment: false,
+ must_not_reject: false,
+ must_not_throw: false,
+ must_reject: false,
+ must_throw: false,
+ once: false,
+ range: false,
+ runWithMSE: false,
+ wait: false,
+ waitUntilTime: false,
+ },
+ // Use const/let instead of var for tighter scoping, avoiding redeclaration
+ rules: {
+ "no-var": "error",
+ "prefer-const": "error",
+ },
+};
diff --git a/dom/media/mediasource/test/1516754.webm b/dom/media/mediasource/test/1516754.webm
new file mode 100644
index 0000000000..05a008d906
--- /dev/null
+++ b/dom/media/mediasource/test/1516754.webm
Binary files differ
diff --git a/dom/media/mediasource/test/1516754.webm^headers^ b/dom/media/mediasource/test/1516754.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/1516754.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/aac20-48000-64000-1.m4s b/dom/media/mediasource/test/aac20-48000-64000-1.m4s
new file mode 100644
index 0000000000..56506e1f2d
--- /dev/null
+++ b/dom/media/mediasource/test/aac20-48000-64000-1.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/aac20-48000-64000-1.m4s^headers^ b/dom/media/mediasource/test/aac20-48000-64000-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/aac20-48000-64000-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/aac20-48000-64000-2.m4s b/dom/media/mediasource/test/aac20-48000-64000-2.m4s
new file mode 100644
index 0000000000..3faff17ebf
--- /dev/null
+++ b/dom/media/mediasource/test/aac20-48000-64000-2.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/aac20-48000-64000-2.m4s^headers^ b/dom/media/mediasource/test/aac20-48000-64000-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/aac20-48000-64000-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/aac20-48000-64000-init.mp4 b/dom/media/mediasource/test/aac20-48000-64000-init.mp4
new file mode 100644
index 0000000000..b70e016512
--- /dev/null
+++ b/dom/media/mediasource/test/aac20-48000-64000-init.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/aac20-48000-64000-init.mp4^headers^ b/dom/media/mediasource/test/aac20-48000-64000-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/aac20-48000-64000-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/aac51-48000-128000-1.m4s b/dom/media/mediasource/test/aac51-48000-128000-1.m4s
new file mode 100644
index 0000000000..3424acfecc
--- /dev/null
+++ b/dom/media/mediasource/test/aac51-48000-128000-1.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/aac51-48000-128000-1.m4s^headers^ b/dom/media/mediasource/test/aac51-48000-128000-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/aac51-48000-128000-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/aac51-48000-128000-2.m4s b/dom/media/mediasource/test/aac51-48000-128000-2.m4s
new file mode 100644
index 0000000000..b02bfd043d
--- /dev/null
+++ b/dom/media/mediasource/test/aac51-48000-128000-2.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/aac51-48000-128000-2.m4s^headers^ b/dom/media/mediasource/test/aac51-48000-128000-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/aac51-48000-128000-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/aac51-48000-128000-init.mp4 b/dom/media/mediasource/test/aac51-48000-128000-init.mp4
new file mode 100644
index 0000000000..7d62401f28
--- /dev/null
+++ b/dom/media/mediasource/test/aac51-48000-128000-init.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/aac51-48000-128000-init.mp4^headers^ b/dom/media/mediasource/test/aac51-48000-128000-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/aac51-48000-128000-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/avc3/init.mp4 b/dom/media/mediasource/test/avc3/init.mp4
new file mode 100644
index 0000000000..12fc38bd20
--- /dev/null
+++ b/dom/media/mediasource/test/avc3/init.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/avc3/init.mp4^headers^ b/dom/media/mediasource/test/avc3/init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/avc3/init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/avc3/segment1.m4s b/dom/media/mediasource/test/avc3/segment1.m4s
new file mode 100644
index 0000000000..d95a6adf02
--- /dev/null
+++ b/dom/media/mediasource/test/avc3/segment1.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/avc3/segment1.m4s^headers^ b/dom/media/mediasource/test/avc3/segment1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/avc3/segment1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop1.m4s b/dom/media/mediasource/test/bipbop/bipbop1.m4s
new file mode 100644
index 0000000000..a237f2e91e
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop1.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop1.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop10.m4s b/dom/media/mediasource/test/bipbop/bipbop10.m4s
new file mode 100644
index 0000000000..d1f5e6a0b0
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop10.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop10.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop10.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop10.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop11.m4s b/dom/media/mediasource/test/bipbop/bipbop11.m4s
new file mode 100644
index 0000000000..57232fb359
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop11.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop11.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop11.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop11.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop12.m4s b/dom/media/mediasource/test/bipbop/bipbop12.m4s
new file mode 100644
index 0000000000..f9b18713ee
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop12.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop12.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop12.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop12.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop13.m4s b/dom/media/mediasource/test/bipbop/bipbop13.m4s
new file mode 100644
index 0000000000..f2a876946c
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop13.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop13.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop13.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop13.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop2.m4s b/dom/media/mediasource/test/bipbop/bipbop2.m4s
new file mode 100644
index 0000000000..baa0d8578c
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop2.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop2.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop2s.mp4 b/dom/media/mediasource/test/bipbop/bipbop2s.mp4
new file mode 100644
index 0000000000..4fd8b9cb6e
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop2s.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop2s.mp4^headers^ b/dom/media/mediasource/test/bipbop/bipbop2s.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop2s.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop3.m4s b/dom/media/mediasource/test/bipbop/bipbop3.m4s
new file mode 100644
index 0000000000..ed313e668c
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop3.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop3.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop3.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop3.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop4.m4s b/dom/media/mediasource/test/bipbop/bipbop4.m4s
new file mode 100644
index 0000000000..7709ac08c5
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop4.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop4.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop4.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop4.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop5.m4s b/dom/media/mediasource/test/bipbop/bipbop5.m4s
new file mode 100644
index 0000000000..6d36788e44
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop5.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop5.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop5.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop5.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop6.m4s b/dom/media/mediasource/test/bipbop/bipbop6.m4s
new file mode 100644
index 0000000000..64f475c700
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop6.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop6.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop6.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop6.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop7.m4s b/dom/media/mediasource/test/bipbop/bipbop7.m4s
new file mode 100644
index 0000000000..c148918d6d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop7.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop7.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop7.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop7.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop8.m4s b/dom/media/mediasource/test/bipbop/bipbop8.m4s
new file mode 100644
index 0000000000..707dd48485
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop8.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop8.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop8.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop8.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop9.m4s b/dom/media/mediasource/test/bipbop/bipbop9.m4s
new file mode 100644
index 0000000000..538cf72a4d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop9.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop9.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop9.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop9.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_300-3s.webm b/dom/media/mediasource/test/bipbop/bipbop_300-3s.webm
new file mode 100644
index 0000000000..db578dc96c
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_300-3s.webm
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_300-3s.webm^headers^ b/dom/media/mediasource/test/bipbop/bipbop_300-3s.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_300-3s.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-video1.m4s b/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-video1.m4s
new file mode 100644
index 0000000000..3dad336e8e
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-video1.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-video1.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-video1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-video1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-video2.m4s b/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-video2.m4s
new file mode 100644
index 0000000000..dd7491241f
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-video2.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-video2.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-video2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-video2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-videoinit.mp4 b/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-videoinit.mp4
new file mode 100644
index 0000000000..b1a2d44058
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-videoinit.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-videoinit.mp4^headers^ b/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-videoinit.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-videoinit.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio1.m4s b/dom/media/mediasource/test/bipbop/bipbop_audio1.m4s
new file mode 100644
index 0000000000..33da98b5a9
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio1.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio1.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_audio1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio10.m4s b/dom/media/mediasource/test/bipbop/bipbop_audio10.m4s
new file mode 100644
index 0000000000..36a98afd29
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio10.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio10.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_audio10.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio10.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio11.m4s b/dom/media/mediasource/test/bipbop/bipbop_audio11.m4s
new file mode 100644
index 0000000000..23d4aa8d86
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio11.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio11.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_audio11.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio11.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio2.m4s b/dom/media/mediasource/test/bipbop/bipbop_audio2.m4s
new file mode 100644
index 0000000000..96f4bcc344
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio2.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio2.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_audio2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio3.m4s b/dom/media/mediasource/test/bipbop/bipbop_audio3.m4s
new file mode 100644
index 0000000000..7de4bd0ca1
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio3.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio3.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_audio3.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio3.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio4.m4s b/dom/media/mediasource/test/bipbop/bipbop_audio4.m4s
new file mode 100644
index 0000000000..494c71eb92
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio4.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio4.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_audio4.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio4.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio5.m4s b/dom/media/mediasource/test/bipbop/bipbop_audio5.m4s
new file mode 100644
index 0000000000..b50496b6ce
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio5.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio5.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_audio5.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio5.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio6.m4s b/dom/media/mediasource/test/bipbop/bipbop_audio6.m4s
new file mode 100644
index 0000000000..02cf4d363c
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio6.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio6.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_audio6.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio6.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio7.m4s b/dom/media/mediasource/test/bipbop/bipbop_audio7.m4s
new file mode 100644
index 0000000000..bb2252889f
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio7.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio7.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_audio7.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio7.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio8.m4s b/dom/media/mediasource/test/bipbop/bipbop_audio8.m4s
new file mode 100644
index 0000000000..04a6a7af91
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio8.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio8.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_audio8.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio8.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio9.m4s b/dom/media/mediasource/test/bipbop/bipbop_audio9.m4s
new file mode 100644
index 0000000000..cb94b529a7
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio9.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio9.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_audio9.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio9.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audioinit.mp4 b/dom/media/mediasource/test/bipbop/bipbop_audioinit.mp4
new file mode 100644
index 0000000000..bbf272197d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audioinit.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audioinit.mp4^headers^ b/dom/media/mediasource/test/bipbop/bipbop_audioinit.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audioinit.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_dash.mpd b/dom/media/mediasource/test/bipbop/bipbop_dash.mpd
new file mode 100644
index 0000000000..532cdc65d5
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_dash.mpd
@@ -0,0 +1,48 @@
+<?xml version="1.0"?>
+<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" minBufferTime="PT1.500000S" type="static" mediaPresentationDuration="PT0H0M9.98S" profiles="urn:mpeg:dash:profile:full:2011">
+ <ProgramInformation moreInformationURL="http://gpac.sourceforge.net">
+ <Title>bipbop_dash.mpd handcrafted by JYA</Title>
+ </ProgramInformation>
+
+ <Period duration="PT0H0M9.98S">
+ <AdaptationSet segmentAlignment="true" maxWidth="400" maxHeight="300" maxFrameRate="90000" par="4:3" lang="und">
+ <Representation id="1" mimeType="video/mp4" codecs="avc1.4d4015" width="400" height="300" frameRate="90000" sar="1:1" startWithSAP="1" bandwidth="226425">
+ <SegmentList timescale="90000" duration="69043">
+ <Initialization sourceURL="bipbop_videoinit.mp4"/>
+ <SegmentURL media="bipbop_video1.m4s"/>
+ <SegmentURL media="bipbop_video2.m4s"/>
+ <SegmentURL media="bipbop_video3.m4s"/>
+ <SegmentURL media="bipbop_video4.m4s"/>
+ <SegmentURL media="bipbop_video5.m4s"/>
+ <SegmentURL media="bipbop_video6.m4s"/>
+ <SegmentURL media="bipbop_video7.m4s"/>
+ <SegmentURL media="bipbop_video8.m4s"/>
+ <SegmentURL media="bipbop_video9.m4s"/>
+ <SegmentURL media="bipbop_video10.m4s"/>
+ <SegmentURL media="bipbop_video11.m4s"/>
+ <SegmentURL media="bipbop_video12.m4s"/>
+ <SegmentURL media="bipbop_video13.m4s"/>
+ </SegmentList>
+ </Representation>
+ </AdaptationSet>
+ <AdaptationSet segmentAlignment="true" lang="und">
+ <Representation id="1" mimeType="audio/mp4" codecs="mp4a.40.2" audioSamplingRate="22050" startWithSAP="1" bandwidth="7206">
+ <AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
+ <SegmentList timescale="22050" duration="20101">
+ <Initialization sourceURL="bipbop_audioinit.mp4"/>
+ <SegmentURL media="bipbop_audio1.m4s"/>
+ <SegmentURL media="bipbop_audio2.m4s"/>
+ <SegmentURL media="bipbop_audio3.m4s"/>
+ <SegmentURL media="bipbop_audio4.m4s"/>
+ <SegmentURL media="bipbop_audio5.m4s"/>
+ <SegmentURL media="bipbop_audio6.m4s"/>
+ <SegmentURL media="bipbop_audio7.m4s"/>
+ <SegmentURL media="bipbop_audio8.m4s"/>
+ <SegmentURL media="bipbop_audio9.m4s"/>
+ <SegmentURL media="bipbop_audio10.m4s"/>
+ <SegmentURL media="bipbop_audio11.m4s"/>
+ </SegmentList>
+ </Representation>
+ </AdaptationSet>
+ </Period>
+</MPD>
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.0-1.m4s b/dom/media/mediasource/test/bipbop/bipbop_offset_0.0-1.m4s
new file mode 100644
index 0000000000..2b95d49de9
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.0-1.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.0-1.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.0-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.0-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.0-2.m4s b/dom/media/mediasource/test/bipbop/bipbop_offset_0.0-2.m4s
new file mode 100644
index 0000000000..3d95e7e2bf
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.0-2.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.0-2.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.0-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.0-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.0-init.mp4 b/dom/media/mediasource/test/bipbop/bipbop_offset_0.0-init.mp4
new file mode 100644
index 0000000000..cc7a48b5ce
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.0-init.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.0-init.mp4^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.0-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.0-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.1-1.m4s b/dom/media/mediasource/test/bipbop/bipbop_offset_0.1-1.m4s
new file mode 100644
index 0000000000..d67c4ef4cc
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.1-1.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.1-1.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.1-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.1-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.1-2.m4s b/dom/media/mediasource/test/bipbop/bipbop_offset_0.1-2.m4s
new file mode 100644
index 0000000000..be155dbb9c
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.1-2.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.1-2.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.1-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.1-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.1-init.mp4 b/dom/media/mediasource/test/bipbop/bipbop_offset_0.1-init.mp4
new file mode 100644
index 0000000000..b67beb9548
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.1-init.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.1-init.mp4^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.1-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.1-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.2-1.m4s b/dom/media/mediasource/test/bipbop/bipbop_offset_0.2-1.m4s
new file mode 100644
index 0000000000..2056aaec7f
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.2-1.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.2-1.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.2-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.2-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.2-2.m4s b/dom/media/mediasource/test/bipbop/bipbop_offset_0.2-2.m4s
new file mode 100644
index 0000000000..ccdad15c39
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.2-2.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.2-2.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.2-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.2-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.2-init.mp4 b/dom/media/mediasource/test/bipbop/bipbop_offset_0.2-init.mp4
new file mode 100644
index 0000000000..5b618c64d8
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.2-init.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.2-init.mp4^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.2-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.2-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.3-1.m4s b/dom/media/mediasource/test/bipbop/bipbop_offset_0.3-1.m4s
new file mode 100644
index 0000000000..c834ea6ae8
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.3-1.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.3-1.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.3-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.3-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.3-2.m4s b/dom/media/mediasource/test/bipbop/bipbop_offset_0.3-2.m4s
new file mode 100644
index 0000000000..aad6b355ae
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.3-2.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.3-2.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.3-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.3-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.3-init.mp4 b/dom/media/mediasource/test/bipbop/bipbop_offset_0.3-init.mp4
new file mode 100644
index 0000000000..1f878bc84b
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.3-init.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.3-init.mp4^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.3-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.3-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.4-1.m4s b/dom/media/mediasource/test/bipbop/bipbop_offset_0.4-1.m4s
new file mode 100644
index 0000000000..88f05ee8bb
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.4-1.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.4-1.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.4-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.4-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.4-2.m4s b/dom/media/mediasource/test/bipbop/bipbop_offset_0.4-2.m4s
new file mode 100644
index 0000000000..23ecab42e2
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.4-2.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.4-2.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.4-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.4-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.4-init.mp4 b/dom/media/mediasource/test/bipbop/bipbop_offset_0.4-init.mp4
new file mode 100644
index 0000000000..3e5ad8ad3b
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.4-init.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.4-init.mp4^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.4-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.4-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.5-1.m4s b/dom/media/mediasource/test/bipbop/bipbop_offset_0.5-1.m4s
new file mode 100644
index 0000000000..df05700d87
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.5-1.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.5-1.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.5-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.5-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.5-2.m4s b/dom/media/mediasource/test/bipbop/bipbop_offset_0.5-2.m4s
new file mode 100644
index 0000000000..14daa425c7
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.5-2.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.5-2.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.5-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.5-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.5-init.mp4 b/dom/media/mediasource/test/bipbop/bipbop_offset_0.5-init.mp4
new file mode 100644
index 0000000000..2101dd876c
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.5-init.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.5-init.mp4^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.5-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.5-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.6-1.m4s b/dom/media/mediasource/test/bipbop/bipbop_offset_0.6-1.m4s
new file mode 100644
index 0000000000..ef0a4614fc
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.6-1.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.6-1.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.6-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.6-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.6-2.m4s b/dom/media/mediasource/test/bipbop/bipbop_offset_0.6-2.m4s
new file mode 100644
index 0000000000..8f7c819867
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.6-2.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.6-2.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.6-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.6-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.6-init.mp4 b/dom/media/mediasource/test/bipbop/bipbop_offset_0.6-init.mp4
new file mode 100644
index 0000000000..91f48ab6a1
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.6-init.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.6-init.mp4^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.6-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.6-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.7-1.m4s b/dom/media/mediasource/test/bipbop/bipbop_offset_0.7-1.m4s
new file mode 100644
index 0000000000..dded8a37af
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.7-1.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.7-1.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.7-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.7-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.7-2.m4s b/dom/media/mediasource/test/bipbop/bipbop_offset_0.7-2.m4s
new file mode 100644
index 0000000000..2a3c10859c
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.7-2.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.7-2.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.7-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.7-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.7-init.mp4 b/dom/media/mediasource/test/bipbop/bipbop_offset_0.7-init.mp4
new file mode 100644
index 0000000000..cf45610f7b
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.7-init.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.7-init.mp4^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.7-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.7-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.8-1.m4s b/dom/media/mediasource/test/bipbop/bipbop_offset_0.8-1.m4s
new file mode 100644
index 0000000000..74f0addd4f
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.8-1.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.8-1.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.8-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.8-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.8-2.m4s b/dom/media/mediasource/test/bipbop/bipbop_offset_0.8-2.m4s
new file mode 100644
index 0000000000..f062c85333
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.8-2.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.8-2.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.8-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.8-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.8-init.mp4 b/dom/media/mediasource/test/bipbop/bipbop_offset_0.8-init.mp4
new file mode 100644
index 0000000000..30a0ab0fed
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.8-init.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.8-init.mp4^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.8-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.8-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.9-1.m4s b/dom/media/mediasource/test/bipbop/bipbop_offset_0.9-1.m4s
new file mode 100644
index 0000000000..b74ebf1f64
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.9-1.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.9-1.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.9-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.9-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.9-2.m4s b/dom/media/mediasource/test/bipbop/bipbop_offset_0.9-2.m4s
new file mode 100644
index 0000000000..eabd8a3411
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.9-2.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.9-2.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.9-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.9-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.9-init.mp4 b/dom/media/mediasource/test/bipbop/bipbop_offset_0.9-init.mp4
new file mode 100644
index 0000000000..449722b0fd
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.9-init.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_0.9-init.mp4^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.9-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_0.9-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_1.0-1.m4s b/dom/media/mediasource/test/bipbop/bipbop_offset_1.0-1.m4s
new file mode 100644
index 0000000000..e032afcc4f
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_1.0-1.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_1.0-1.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_1.0-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_1.0-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_1.0-2.m4s b/dom/media/mediasource/test/bipbop/bipbop_offset_1.0-2.m4s
new file mode 100644
index 0000000000..6542c8d3d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_1.0-2.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_1.0-2.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_1.0-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_1.0-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_1.0-init.mp4 b/dom/media/mediasource/test/bipbop/bipbop_offset_1.0-init.mp4
new file mode 100644
index 0000000000..0a9da048f0
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_1.0-init.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_1.0-init.mp4^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_1.0-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_1.0-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_1.1-1.m4s b/dom/media/mediasource/test/bipbop/bipbop_offset_1.1-1.m4s
new file mode 100644
index 0000000000..1b8b22be4a
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_1.1-1.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_1.1-1.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_1.1-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_1.1-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_1.1-2.m4s b/dom/media/mediasource/test/bipbop/bipbop_offset_1.1-2.m4s
new file mode 100644
index 0000000000..3de855982f
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_1.1-2.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_1.1-2.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_1.1-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_1.1-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_1.1-init.mp4 b/dom/media/mediasource/test/bipbop/bipbop_offset_1.1-init.mp4
new file mode 100644
index 0000000000..80b3814f7c
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_1.1-init.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_offset_1.1-init.mp4^headers^ b/dom/media/mediasource/test/bipbop/bipbop_offset_1.1-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_offset_1.1-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_trailing_skip_box_video1.m4s b/dom/media/mediasource/test/bipbop/bipbop_trailing_skip_box_video1.m4s
new file mode 100644
index 0000000000..fa5d454277
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_trailing_skip_box_video1.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_trailing_skip_box_video1.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_trailing_skip_box_video1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_trailing_skip_box_video1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video1.m4s b/dom/media/mediasource/test/bipbop/bipbop_video1.m4s
new file mode 100644
index 0000000000..9291182516
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video1.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video1.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_video1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video10.m4s b/dom/media/mediasource/test/bipbop/bipbop_video10.m4s
new file mode 100644
index 0000000000..72c7afaca7
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video10.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video10.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_video10.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video10.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video11.m4s b/dom/media/mediasource/test/bipbop/bipbop_video11.m4s
new file mode 100644
index 0000000000..e6109f5e71
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video11.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video11.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_video11.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video11.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video12.m4s b/dom/media/mediasource/test/bipbop/bipbop_video12.m4s
new file mode 100644
index 0000000000..5c54a510f7
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video12.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video12.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_video12.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video12.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video13.m4s b/dom/media/mediasource/test/bipbop/bipbop_video13.m4s
new file mode 100644
index 0000000000..c64f38a337
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video13.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video13.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_video13.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video13.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video2.m4s b/dom/media/mediasource/test/bipbop/bipbop_video2.m4s
new file mode 100644
index 0000000000..cd34fae561
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video2.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video2.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_video2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video3.m4s b/dom/media/mediasource/test/bipbop/bipbop_video3.m4s
new file mode 100644
index 0000000000..5a13340043
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video3.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video3.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_video3.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video3.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video4.m4s b/dom/media/mediasource/test/bipbop/bipbop_video4.m4s
new file mode 100644
index 0000000000..e8d96b6ed1
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video4.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video4.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_video4.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video4.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video5.m4s b/dom/media/mediasource/test/bipbop/bipbop_video5.m4s
new file mode 100644
index 0000000000..ca6a820468
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video5.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video5.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_video5.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video5.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video6.m4s b/dom/media/mediasource/test/bipbop/bipbop_video6.m4s
new file mode 100644
index 0000000000..fe9824355b
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video6.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video6.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_video6.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video6.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video7.m4s b/dom/media/mediasource/test/bipbop/bipbop_video7.m4s
new file mode 100644
index 0000000000..3351fa6859
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video7.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video7.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_video7.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video7.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video8.m4s b/dom/media/mediasource/test/bipbop/bipbop_video8.m4s
new file mode 100644
index 0000000000..af26ae5f9e
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video8.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video8.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_video8.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video8.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video9.m4s b/dom/media/mediasource/test/bipbop/bipbop_video9.m4s
new file mode 100644
index 0000000000..25be672c15
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video9.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video9.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_video9.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video9.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_videoinit.mp4 b/dom/media/mediasource/test/bipbop/bipbop_videoinit.mp4
new file mode 100644
index 0000000000..7c9c533c36
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_videoinit.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_videoinit.mp4^headers^ b/dom/media/mediasource/test/bipbop/bipbop_videoinit.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_videoinit.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbopinit.mp4 b/dom/media/mediasource/test/bipbop/bipbopinit.mp4
new file mode 100644
index 0000000000..39f0575a71
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbopinit.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbopinit.mp4^headers^ b/dom/media/mediasource/test/bipbop/bipbopinit.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbopinit.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bug1718709_high_res.mp4 b/dom/media/mediasource/test/bug1718709_high_res.mp4
new file mode 100644
index 0000000000..3f211d2370
--- /dev/null
+++ b/dom/media/mediasource/test/bug1718709_high_res.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/bug1718709_low_res.mp4 b/dom/media/mediasource/test/bug1718709_low_res.mp4
new file mode 100644
index 0000000000..dc5cd6b7f1
--- /dev/null
+++ b/dom/media/mediasource/test/bug1718709_low_res.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/crashtests/1005366.html b/dom/media/mediasource/test/crashtests/1005366.html
new file mode 100644
index 0000000000..aa8b7f652e
--- /dev/null
+++ b/dom/media/mediasource/test/crashtests/1005366.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<script>
+
+/*
+user_pref("media.mediasource.enabled", true);
+*/
+
+function boom()
+{
+ var source = new window.MediaSource();
+ var videoElement = document.createElementNS('http://www.w3.org/1999/xhtml', 'video');
+ videoElement.src = URL.createObjectURL(source);
+
+ setTimeout(function() {
+ var buf = source.addSourceBuffer("video/webm");
+ buf.abort();
+ buf.appendBuffer(new Float32Array(203));
+ }, 0);
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/dom/media/mediasource/test/crashtests/1059035.html b/dom/media/mediasource/test/crashtests/1059035.html
new file mode 100644
index 0000000000..9dfda34b82
--- /dev/null
+++ b/dom/media/mediasource/test/crashtests/1059035.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+/*
+user_pref("media.mediasource.enabled", true);
+*/
+
+function boom()
+{
+ var mediaSource = new MediaSource();
+ var htmlAudio = document.createElement("audio");
+ htmlAudio.src = URL.createObjectURL(mediaSource);
+
+ setTimeout(function() {
+ var sourceBuffer = mediaSource.addSourceBuffer("video/webm");
+ mediaSource.removeSourceBuffer(sourceBuffer);
+ sourceBuffer.remove(0, 0);
+ }, 0);
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/dom/media/mediasource/test/crashtests/926665.html b/dom/media/mediasource/test/crashtests/926665.html
new file mode 100644
index 0000000000..ccac5a7644
--- /dev/null
+++ b/dom/media/mediasource/test/crashtests/926665.html
@@ -0,0 +1,26 @@
+<html>
+<head>
+<meta charset="UTF-8">
+<script style="display: none;" id="fuzz1" type="text/javascript">
+
+function boom()
+{
+ var mediaSource = new window.MediaSource();
+ var mediaSourceURL = URL.createObjectURL(mediaSource);
+ var v1 = document.createElement('video');
+ v1.src = mediaSourceURL;
+ mediaSource.addEventListener("sourceopen", function (e) {
+ var v2 = document.createElement('video');
+ v2.src = mediaSourceURL;
+ setTimeout(function () {
+ v2.src = "data:text/plain,1";
+ v1.src = "data:text/plain,2";
+ }, 0);
+ });
+}
+
+</script>
+</head>
+
+<body onload="boom();"></body>
+</html>
diff --git a/dom/media/mediasource/test/crashtests/931388.html b/dom/media/mediasource/test/crashtests/931388.html
new file mode 100644
index 0000000000..cdb5bd9add
--- /dev/null
+++ b/dom/media/mediasource/test/crashtests/931388.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<script>
+
+function boom()
+{
+ var v = document.createElement('video');
+ v.src = URL.createObjectURL(new MediaSource());
+ v.play();
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/dom/media/mediasource/test/crashtests/crashtests.list b/dom/media/mediasource/test/crashtests/crashtests.list
new file mode 100644
index 0000000000..e16ec261d2
--- /dev/null
+++ b/dom/media/mediasource/test/crashtests/crashtests.list
@@ -0,0 +1,4 @@
+test-pref(media.mediasource.enabled,true) load 926665.html
+test-pref(media.mediasource.enabled,true) load 931388.html
+test-pref(media.mediasource.enabled,true) load 1005366.html
+test-pref(media.mediasource.enabled,true) load 1059035.html
diff --git a/dom/media/mediasource/test/flac/00001.m4s b/dom/media/mediasource/test/flac/00001.m4s
new file mode 100644
index 0000000000..02745ba2f8
--- /dev/null
+++ b/dom/media/mediasource/test/flac/00001.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/flac/00001.m4s^headers^ b/dom/media/mediasource/test/flac/00001.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/flac/00001.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/flac/00002.m4s b/dom/media/mediasource/test/flac/00002.m4s
new file mode 100644
index 0000000000..cd6b1f5949
--- /dev/null
+++ b/dom/media/mediasource/test/flac/00002.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/flac/00002.m4s^headers^ b/dom/media/mediasource/test/flac/00002.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/flac/00002.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/flac/00003.m4s b/dom/media/mediasource/test/flac/00003.m4s
new file mode 100644
index 0000000000..c5b78e1ce0
--- /dev/null
+++ b/dom/media/mediasource/test/flac/00003.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/flac/00003.m4s^headers^ b/dom/media/mediasource/test/flac/00003.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/flac/00003.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/flac/IS.mp4 b/dom/media/mediasource/test/flac/IS.mp4
new file mode 100644
index 0000000000..7f108602fd
--- /dev/null
+++ b/dom/media/mediasource/test/flac/IS.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/flac/IS.mp4^headers^ b/dom/media/mediasource/test/flac/IS.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/flac/IS.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/init-trackid2.mp4 b/dom/media/mediasource/test/init-trackid2.mp4
new file mode 100644
index 0000000000..c96da9d4df
--- /dev/null
+++ b/dom/media/mediasource/test/init-trackid2.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/init-trackid2.mp4^headers^ b/dom/media/mediasource/test/init-trackid2.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/init-trackid2.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/init-trackid3.mp4 b/dom/media/mediasource/test/init-trackid3.mp4
new file mode 100644
index 0000000000..e37d8ea098
--- /dev/null
+++ b/dom/media/mediasource/test/init-trackid3.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/init-trackid3.mp4^headers^ b/dom/media/mediasource/test/init-trackid3.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/init-trackid3.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/mediasource.js b/dom/media/mediasource/test/mediasource.js
new file mode 100644
index 0000000000..71d8d4ef9f
--- /dev/null
+++ b/dom/media/mediasource/test/mediasource.js
@@ -0,0 +1,235 @@
+// Helpers for Media Source Extensions tests
+
+let gMSETestPrefs = [
+ ["media.mediasource.enabled", true],
+ ["media.audio-max-decode-error", 0],
+ ["media.video-max-decode-error", 0],
+];
+
+// Called before runWithMSE() to set the prefs before running MSE tests.
+function addMSEPrefs(...prefs) {
+ gMSETestPrefs = gMSETestPrefs.concat(prefs);
+}
+
+async function runWithMSE(testFunction) {
+ await once(window, "load");
+ await SpecialPowers.pushPrefEnv({ set: gMSETestPrefs });
+
+ const ms = new MediaSource();
+
+ const el = document.createElement("video");
+ el.src = URL.createObjectURL(ms);
+ el.preload = "auto";
+
+ document.body.appendChild(el);
+ SimpleTest.registerCleanupFunction(() => {
+ el.remove();
+ el.removeAttribute("src");
+ el.load();
+ });
+ try {
+ await testFunction(ms, el);
+ } catch (e) {
+ ok(false, `${testFunction.name} failed with error ${e.name}`);
+ throw e;
+ }
+}
+
+async function fetchWithXHR(uri) {
+ return new Promise(resolve => {
+ const xhr = new XMLHttpRequest();
+ xhr.open("GET", uri, true);
+ xhr.responseType = "arraybuffer";
+ xhr.addEventListener("load", function () {
+ is(
+ xhr.status,
+ 200,
+ "fetchWithXHR load uri='" + uri + "' status=" + xhr.status
+ );
+ resolve(xhr.response);
+ });
+ xhr.send();
+ });
+}
+
+function range(start, end) {
+ const rv = [];
+ for (let i = start; i < end; ++i) {
+ rv.push(i);
+ }
+ return rv;
+}
+
+function must_throw(f, msg, error = true) {
+ try {
+ f();
+ ok(!error, msg);
+ } catch (e) {
+ ok(error, msg);
+ if (error === true) {
+ ok(
+ false,
+ `Please provide name of expected error! Got ${e.name}: ${e.message}.`
+ );
+ } else if (e.name != error) {
+ throw e;
+ }
+ }
+}
+
+async function must_reject(f, msg, error = true) {
+ try {
+ await f();
+ ok(!error, msg);
+ } catch (e) {
+ ok(error, msg);
+ if (error === true) {
+ ok(
+ false,
+ `Please provide name of expected error! Got ${e.name}: ${e.message}.`
+ );
+ } else if (e.name != error) {
+ throw e;
+ }
+ }
+}
+
+const wait = ms => new Promise(resolve => setTimeout(resolve, ms));
+
+const must_not_throw = (f, msg) => must_throw(f, msg, false);
+const must_not_reject = (f, msg) => must_reject(f, msg, false);
+
+async function once(target, name) {
+ return new Promise(r => target.addEventListener(name, r, { once: true }));
+}
+
+function timeRangeToString(r) {
+ let str = "TimeRanges: ";
+ for (let i = 0; i < r.length; i++) {
+ str += "[" + r.start(i) + ", " + r.end(i) + ")";
+ }
+ return str;
+}
+
+async function loadSegment(sb, typedArrayOrArrayBuffer) {
+ const typedArray =
+ typedArrayOrArrayBuffer instanceof ArrayBuffer
+ ? new Uint8Array(typedArrayOrArrayBuffer)
+ : typedArrayOrArrayBuffer;
+ info(
+ `Loading buffer: [${typedArray.byteOffset}, ${
+ typedArray.byteOffset + typedArray.byteLength
+ })`
+ );
+ const beforeBuffered = timeRangeToString(sb.buffered);
+ const p = once(sb, "update");
+ sb.appendBuffer(typedArray);
+ await p;
+ const afterBuffered = timeRangeToString(sb.buffered);
+ info(
+ `SourceBuffer buffered ranges grew from ${beforeBuffered} to ${afterBuffered}`
+ );
+}
+
+async function fetchAndLoad(sb, prefix, chunks, suffix) {
+ // Fetch the buffers in parallel.
+ const buffers = await Promise.all(
+ chunks.map(c => fetchWithXHR(prefix + c + suffix))
+ );
+
+ // Load them in series, as required per spec.
+ for (const buffer of buffers) {
+ await loadSegment(sb, buffer);
+ }
+}
+
+function loadSegmentAsync(sb, typedArrayOrArrayBuffer) {
+ const typedArray =
+ typedArrayOrArrayBuffer instanceof ArrayBuffer
+ ? new Uint8Array(typedArrayOrArrayBuffer)
+ : typedArrayOrArrayBuffer;
+ info(
+ `Loading buffer2: [${typedArray.byteOffset}, ${
+ typedArray.byteOffset + typedArray.byteLength
+ })`
+ );
+ const beforeBuffered = timeRangeToString(sb.buffered);
+ return sb.appendBufferAsync(typedArray).then(() => {
+ const afterBuffered = timeRangeToString(sb.buffered);
+ info(
+ `SourceBuffer buffered ranges grew from ${beforeBuffered} to ${afterBuffered}`
+ );
+ });
+}
+
+function fetchAndLoadAsync(sb, prefix, chunks, suffix) {
+ // Fetch the buffers in parallel.
+ const buffers = {};
+ const fetches = [];
+ for (const chunk of chunks) {
+ fetches.push(
+ fetchWithXHR(prefix + chunk + suffix).then(
+ ((c, x) => (buffers[c] = x)).bind(null, chunk)
+ )
+ );
+ }
+
+ // Load them in series, as required per spec.
+ return Promise.all(fetches).then(function () {
+ let rv = Promise.resolve();
+ for (const chunk of chunks) {
+ rv = rv.then(loadSegmentAsync.bind(null, sb, buffers[chunk]));
+ }
+ return rv;
+ });
+}
+
+// Register timeout function to dump debugging logs.
+SimpleTest.registerTimeoutFunction(async function () {
+ for (const v of document.getElementsByTagName("video")) {
+ console.log(await SpecialPowers.wrap(v).mozRequestDebugInfo());
+ }
+ for (const a of document.getElementsByTagName("audio")) {
+ console.log(await SpecialPowers.wrap(a).mozRequestDebugInfo());
+ }
+});
+
+async function waitUntilTime(target, targetTime) {
+ await new Promise(resolve => {
+ target.addEventListener("waiting", function onwaiting() {
+ info("Got a waiting event at " + target.currentTime);
+ if (target.currentTime >= targetTime) {
+ target.removeEventListener("waiting", onwaiting);
+ resolve();
+ }
+ });
+ });
+ ok(true, "Reached target time of: " + targetTime);
+}
+
+// Log events for debugging.
+
+function logEvents(el) {
+ [
+ "suspend",
+ "play",
+ "canplay",
+ "canplaythrough",
+ "loadstart",
+ "loadedmetadata",
+ "loadeddata",
+ "playing",
+ "ended",
+ "error",
+ "stalled",
+ "emptied",
+ "abort",
+ "waiting",
+ "pause",
+ "durationchange",
+ "seeking",
+ "seeked",
+ ].forEach(type =>
+ el.addEventListener(type, e => info(`got ${e.type} event`))
+ );
+}
diff --git a/dom/media/mediasource/test/mochitest.ini b/dom/media/mediasource/test/mochitest.ini
new file mode 100644
index 0000000000..f231255c1e
--- /dev/null
+++ b/dom/media/mediasource/test/mochitest.ini
@@ -0,0 +1,213 @@
+[DEFAULT]
+subsuite = media
+support-files =
+ mediasource.js
+ seek.webm seek.webm^headers^
+ seek_lowres.webm seek_lowres.webm^headers^
+ bipbop/bipbop_300-3s.webm bipbop/bipbop_300-3s.webm^headers^
+ bipbop/bipbop2s.mp4 bipbop/bipbop2s.mp4^headers^
+ bipbop/bipbop_trailing_skip_box_video1.m4s
+ bipbop/bipbop_trailing_skip_box_video1.m4s^headers^
+ bipbop/bipbopinit.mp4 bipbop/bipbop_audioinit.mp4 bipbop/bipbop_videoinit.mp4
+ bipbop/bipbop1.m4s bipbop/bipbop_audio1.m4s bipbop/bipbop_video1.m4s
+ bipbop/bipbop2.m4s bipbop/bipbop_audio2.m4s bipbop/bipbop_video2.m4s
+ bipbop/bipbop3.m4s bipbop/bipbop_audio3.m4s bipbop/bipbop_video3.m4s
+ bipbop/bipbop4.m4s bipbop/bipbop_audio4.m4s bipbop/bipbop_video4.m4s
+ bipbop/bipbop5.m4s bipbop/bipbop_audio5.m4s bipbop/bipbop_video5.m4s
+ bipbop/bipbop6.m4s bipbop/bipbop_audio6.m4s bipbop/bipbop_video6.m4s
+ bipbop/bipbop7.m4s bipbop/bipbop_audio7.m4s bipbop/bipbop_video7.m4s
+ bipbop/bipbop8.m4s bipbop/bipbop_audio8.m4s bipbop/bipbop_video8.m4s
+ bipbop/bipbop9.m4s bipbop/bipbop_audio9.m4s bipbop/bipbop_video9.m4s
+ bipbop/bipbop10.m4s bipbop/bipbop_audio10.m4s bipbop/bipbop_video10.m4s
+ bipbop/bipbop11.m4s bipbop/bipbop_audio11.m4s bipbop/bipbop_video11.m4s
+ bipbop/bipbop12.m4s bipbop/bipbop_video12.m4s
+ bipbop/bipbop13.m4s bipbop/bipbop_video13.m4s
+ bipbop/bipbopinit.mp4^headers^ bipbop/bipbop_audioinit.mp4^headers^ bipbop/bipbop_videoinit.mp4^headers^
+ bipbop/bipbop1.m4s^headers^ bipbop/bipbop_audio1.m4s^headers^ bipbop/bipbop_video1.m4s^headers^
+ bipbop/bipbop2.m4s^headers^ bipbop/bipbop_audio2.m4s^headers^ bipbop/bipbop_video2.m4s^headers^
+ bipbop/bipbop3.m4s^headers^ bipbop/bipbop_audio3.m4s^headers^ bipbop/bipbop_video3.m4s^headers^
+ bipbop/bipbop4.m4s^headers^ bipbop/bipbop_audio4.m4s^headers^ bipbop/bipbop_video4.m4s^headers^
+ bipbop/bipbop5.m4s^headers^ bipbop/bipbop_audio5.m4s^headers^ bipbop/bipbop_video5.m4s^headers^
+ bipbop/bipbop6.m4s^headers^ bipbop/bipbop_audio6.m4s^headers^ bipbop/bipbop_video6.m4s^headers^
+ bipbop/bipbop7.m4s^headers^ bipbop/bipbop_audio7.m4s^headers^ bipbop/bipbop_video7.m4s^headers^
+ bipbop/bipbop8.m4s^headers^ bipbop/bipbop_audio8.m4s^headers^ bipbop/bipbop_video8.m4s^headers^
+ bipbop/bipbop9.m4s^headers^ bipbop/bipbop_audio9.m4s^headers^ bipbop/bipbop_video9.m4s^headers^
+ bipbop/bipbop10.m4s^headers^ bipbop/bipbop_audio10.m4s^headers^ bipbop/bipbop_video10.m4s^headers^
+ bipbop/bipbop11.m4s^headers^ bipbop/bipbop_audio11.m4s^headers^ bipbop/bipbop_video11.m4s^headers^
+ bipbop/bipbop12.m4s^headers^ bipbop/bipbop_video12.m4s^headers^
+ bipbop/bipbop13.m4s^headers^ bipbop/bipbop_video13.m4s^headers^
+ bipbop/bipbop_offset_0.0-1.m4s
+ bipbop/bipbop_offset_0.0-1.m4s^headers^
+ bipbop/bipbop_offset_0.0-2.m4s
+ bipbop/bipbop_offset_0.0-2.m4s^headers^
+ bipbop/bipbop_offset_0.0-init.mp4
+ bipbop/bipbop_offset_0.0-init.mp4^headers^
+ bipbop/bipbop_offset_0.1-1.m4s
+ bipbop/bipbop_offset_0.1-1.m4s^headers^
+ bipbop/bipbop_offset_0.1-2.m4s
+ bipbop/bipbop_offset_0.1-2.m4s^headers^
+ bipbop/bipbop_offset_0.1-init.mp4
+ bipbop/bipbop_offset_0.1-init.mp4^headers^
+ bipbop/bipbop_offset_0.2-1.m4s
+ bipbop/bipbop_offset_0.2-1.m4s^headers^
+ bipbop/bipbop_offset_0.2-2.m4s
+ bipbop/bipbop_offset_0.2-2.m4s^headers^
+ bipbop/bipbop_offset_0.2-init.mp4
+ bipbop/bipbop_offset_0.2-init.mp4^headers^
+ bipbop/bipbop_offset_0.3-1.m4s
+ bipbop/bipbop_offset_0.3-1.m4s^headers^
+ bipbop/bipbop_offset_0.3-2.m4s
+ bipbop/bipbop_offset_0.3-2.m4s^headers^
+ bipbop/bipbop_offset_0.3-init.mp4
+ bipbop/bipbop_offset_0.3-init.mp4^headers^
+ bipbop/bipbop_offset_0.4-1.m4s
+ bipbop/bipbop_offset_0.4-1.m4s^headers^
+ bipbop/bipbop_offset_0.4-2.m4s
+ bipbop/bipbop_offset_0.4-2.m4s^headers^
+ bipbop/bipbop_offset_0.4-init.mp4
+ bipbop/bipbop_offset_0.4-init.mp4^headers^
+ bipbop/bipbop_offset_0.5-1.m4s
+ bipbop/bipbop_offset_0.5-1.m4s^headers^
+ bipbop/bipbop_offset_0.5-2.m4s
+ bipbop/bipbop_offset_0.5-2.m4s^headers^
+ bipbop/bipbop_offset_0.5-init.mp4
+ bipbop/bipbop_offset_0.5-init.mp4^headers^
+ bipbop/bipbop_offset_0.6-1.m4s
+ bipbop/bipbop_offset_0.6-1.m4s^headers^
+ bipbop/bipbop_offset_0.6-2.m4s
+ bipbop/bipbop_offset_0.6-2.m4s^headers^
+ bipbop/bipbop_offset_0.6-init.mp4
+ bipbop/bipbop_offset_0.6-init.mp4^headers^
+ bipbop/bipbop_offset_0.7-1.m4s
+ bipbop/bipbop_offset_0.7-1.m4s^headers^
+ bipbop/bipbop_offset_0.7-2.m4s
+ bipbop/bipbop_offset_0.7-2.m4s^headers^
+ bipbop/bipbop_offset_0.7-init.mp4
+ bipbop/bipbop_offset_0.7-init.mp4^headers^
+ bipbop/bipbop_offset_0.8-1.m4s
+ bipbop/bipbop_offset_0.8-1.m4s^headers^
+ bipbop/bipbop_offset_0.8-2.m4s
+ bipbop/bipbop_offset_0.8-2.m4s^headers^
+ bipbop/bipbop_offset_0.8-init.mp4
+ bipbop/bipbop_offset_0.8-init.mp4^headers^
+ bipbop/bipbop_offset_0.9-1.m4s
+ bipbop/bipbop_offset_0.9-1.m4s^headers^
+ bipbop/bipbop_offset_0.9-2.m4s
+ bipbop/bipbop_offset_0.9-2.m4s^headers^
+ bipbop/bipbop_offset_0.9-init.mp4
+ bipbop/bipbop_offset_0.9-init.mp4^headers^
+ bipbop/bipbop_offset_1.0-1.m4s
+ bipbop/bipbop_offset_1.0-1.m4s^headers^
+ bipbop/bipbop_offset_1.0-2.m4s
+ bipbop/bipbop_offset_1.0-2.m4s^headers^
+ bipbop/bipbop_offset_1.0-init.mp4
+ bipbop/bipbop_offset_1.0-init.mp4^headers^
+ bipbop/bipbop_offset_1.1-1.m4s
+ bipbop/bipbop_offset_1.1-1.m4s^headers^
+ bipbop/bipbop_offset_1.1-2.m4s
+ bipbop/bipbop_offset_1.1-2.m4s^headers^
+ bipbop/bipbop_offset_1.1-init.mp4
+ bipbop/bipbop_offset_1.1-init.mp4^headers^
+ aac20-48000-64000-init.mp4 aac20-48000-64000-init.mp4^headers^
+ aac20-48000-64000-1.m4s aac20-48000-64000-1.m4s^headers^
+ aac20-48000-64000-2.m4s aac20-48000-64000-2.m4s^headers^
+ aac51-48000-128000-init.mp4 aac51-48000-128000-init.mp4^headers^
+ aac51-48000-128000-1.m4s aac51-48000-128000-1.m4s^headers^
+ aac51-48000-128000-2.m4s aac51-48000-128000-2.m4s^headers^
+ bipbop/bipbop_480_624kbps-videoinit.mp4 bipbop/bipbop_480_624kbps-videoinit.mp4^headers^
+ bipbop/bipbop_480_624kbps-video1.m4s bipbop/bipbop_480_624kbps-video1.m4s^headers^
+ bipbop/bipbop_480_624kbps-video2.m4s bipbop/bipbop_480_624kbps-video2.m4s^headers^
+ flac/IS.mp4 flac/IS.mp4^headers^ flac/00001.m4s flac/00001.m4s^headers^
+ flac/00002.m4s flac/00002.m4s^headers^ flac/00003.m4s flac/00003.m4s^headers^
+ avc3/init.mp4 avc3/init.mp4^headers^ avc3/segment1.m4s avc3/segment1.m4s^headers^
+ tags_before_cluster.webm
+ tags_before_cluster.webm^header^
+ 1516754.webm 1516754.webm^headers^
+ init-trackid2.mp4 init-trackid3.mp4 segment-2.0001.m4s segment-2.0002.m4s segment-3.0001.m4s segment-3.0002.m4s
+ init-trackid2.mp4^headers^ init-trackid3.mp4^headers^ segment-2.0001.m4s^headers^ segment-2.0002.m4s^headers^
+ segment-3.0001.m4s^headers^ segment-3.0002.m4s^headers^
+ wmf_mismatchedaudiotime.mp4
+ bug1718709_low_res.mp4
+ bug1718709_high_res.mp4
+ whitenoise-he-aac-5s.mp4
+
+[test_AbortAfterPartialMediaSegment.html]
+[test_AppendPartialInitSegment.html]
+[test_AVC3_mp4.html]
+[test_AudioChange_mp4.html]
+[test_AudioChange_mp4_WebAudio.html]
+[test_AutoRevocation.html]
+tags = firstpartyisolation
+[test_BufferedSeek.html]
+[test_BufferedSeek_mp4.html]
+[test_BufferingWait.html]
+[test_BufferingWait_mp4.html]
+[test_ChangeType.html]
+[test_ChangeWhileWaitingOnMissingData_mp4.html]
+[test_DifferentStreamStartTimes.html]
+[test_DrainOnMissingData_mp4.html]
+[test_DurationChange.html]
+[test_DurationUpdated.html]
+[test_DurationUpdated_mp4.html]
+[test_EndedEvent.html]
+[test_EndOfStream.html]
+[test_EndOfStream_mp4.html]
+[test_Eviction_mp4.html]
+[test_ExperimentalAsync.html]
+[test_FrameSelection.html]
+skip-if = toolkit == 'android' # bug 1341519, bug 1401090
+[test_FrameSelection_mp4.html]
+skip-if = os == 'win' # bug 1487973,
+ (os == 'mac') # mac due to bug 1487973
+[test_HaveMetadataUnbufferedSeek.html]
+[test_HaveMetadataUnbufferedSeek_mp4.html]
+[test_HEAAC_extradata.html]
+[test_InputBufferIsCleared.html]
+[test_LiveSeekable.html]
+[test_LoadedDataFired_mp4.html]
+[test_LoadedMetadataFired.html]
+[test_LoadedMetadataFired_mp4.html]
+[test_MediaSource.html]
+[test_MediaSource_capture_gc.html]
+[test_MediaSource_memory_reporting.html]
+[test_MediaSource_mp4.html]
+[test_MediaSource_flac_mp4.html]
+[test_MediaSource_disabled.html]
+[test_MultipleInitSegments.html]
+[test_MultipleInitSegments_mp4.html]
+[test_NoAudioLoopBackData.html]
+[test_NoAudioLoopBackData_Muted.html]
+[test_NoVideoLoopBackData.html]
+[test_OnEvents.html]
+[test_PlayEvents.html]
+[test_PlayEventsAutoPlaying.html]
+[test_PlayEventsAutoPlaying2.html]
+[test_RemoveSourceBuffer.html]
+[test_Resolution_change_should_not_cause_video_freeze.html]
+[test_ResumeAfterClearing_mp4.html]
+[test_SeekableBeforeAndAfterEndOfStream.html]
+[test_SeekableBeforeAndAfterEndOfStream_mp4.html]
+[test_SeekableBeforeAndAfterEndOfStreamSplit.html]
+[test_SeekableBeforeAndAfterEndOfStreamSplit_mp4.html]
+[test_SeekNoData_mp4.html]
+[test_SeekedEvent_mp4.html]
+[test_SeekToEnd_mp4.html]
+[test_SeekToLastFrame_mp4.html]
+[test_SeekTwice_mp4.html]
+[test_Sequence_mp4.html]
+[test_SetModeThrows.html]
+[test_SplitAppendDelay.html]
+[test_SplitAppendDelay_mp4.html]
+[test_SplitAppend.html]
+[test_SplitAppend_mp4.html]
+[test_Threshold_mp4.html]
+[test_TimestampOffset_mp4.html]
+[test_trackidchange_mp4.html]
+[test_TruncatedDuration.html]
+[test_TruncatedDuration_mp4.html]
+[test_WaitingOnMissingData.html]
+[test_WaitingOnMissingData_mp4.html]
+[test_WaitingOnMissingDataEnded_mp4.html]
+[test_WaitingToEndedTransition_mp4.html]
+[test_WebMTagsBeforeCluster.html]
+[test_WMFUnmatchedAudioDataTime.html]
diff --git a/dom/media/mediasource/test/seek.webm b/dom/media/mediasource/test/seek.webm
new file mode 100644
index 0000000000..72b0297233
--- /dev/null
+++ b/dom/media/mediasource/test/seek.webm
Binary files differ
diff --git a/dom/media/mediasource/test/seek.webm^headers^ b/dom/media/mediasource/test/seek.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/seek.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/seek_lowres.webm b/dom/media/mediasource/test/seek_lowres.webm
new file mode 100644
index 0000000000..8a76e06470
--- /dev/null
+++ b/dom/media/mediasource/test/seek_lowres.webm
Binary files differ
diff --git a/dom/media/mediasource/test/seek_lowres.webm^headers^ b/dom/media/mediasource/test/seek_lowres.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/seek_lowres.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/segment-2.0001.m4s b/dom/media/mediasource/test/segment-2.0001.m4s
new file mode 100644
index 0000000000..b63fd6aaa6
--- /dev/null
+++ b/dom/media/mediasource/test/segment-2.0001.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/segment-2.0001.m4s^headers^ b/dom/media/mediasource/test/segment-2.0001.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/segment-2.0001.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/segment-2.0002.m4s b/dom/media/mediasource/test/segment-2.0002.m4s
new file mode 100644
index 0000000000..3a0051f10e
--- /dev/null
+++ b/dom/media/mediasource/test/segment-2.0002.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/segment-2.0002.m4s^headers^ b/dom/media/mediasource/test/segment-2.0002.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/segment-2.0002.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/segment-3.0001.m4s b/dom/media/mediasource/test/segment-3.0001.m4s
new file mode 100644
index 0000000000..71e33f0e8f
--- /dev/null
+++ b/dom/media/mediasource/test/segment-3.0001.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/segment-3.0001.m4s^headers^ b/dom/media/mediasource/test/segment-3.0001.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/segment-3.0001.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/segment-3.0002.m4s b/dom/media/mediasource/test/segment-3.0002.m4s
new file mode 100644
index 0000000000..10a3ce695d
--- /dev/null
+++ b/dom/media/mediasource/test/segment-3.0002.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/segment-3.0002.m4s^headers^ b/dom/media/mediasource/test/segment-3.0002.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/segment-3.0002.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/tags_before_cluster.webm b/dom/media/mediasource/test/tags_before_cluster.webm
new file mode 100644
index 0000000000..cf7d596b0e
--- /dev/null
+++ b/dom/media/mediasource/test/tags_before_cluster.webm
Binary files differ
diff --git a/dom/media/mediasource/test/tags_before_cluster.webm^header^ b/dom/media/mediasource/test/tags_before_cluster.webm^header^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/mediasource/test/tags_before_cluster.webm^header^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/test_AVC3_mp4.html b/dom/media/mediasource/test/test_AVC3_mp4.html
new file mode 100644
index 0000000000..dd20feed06
--- /dev/null
+++ b/dom/media/mediasource/test/test_AVC3_mp4.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: AVC3 content playback.</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, el) => {
+ await once(ms, "sourceopen");
+ ok(true, "Receive a sourceopen event");
+ const videosb = ms.addSourceBuffer("video/mp4");
+
+ await fetchAndLoad(videosb, "avc3/init", [""], ".mp4");
+ const p = once(el, "loadeddata");
+ await fetchAndLoad(videosb, "avc3/segment", range(1, 2), ".m4s");
+ await p;
+ is(videosb.buffered.length, 1, "continuous buffered range");
+ ok(true, "got loadeddata");
+ ms.endOfStream();
+ await once(ms, "sourceended");
+ ok(true, "endOfStream completed");
+ // Now ensure that we can play to the end.
+ el.play();
+ await once(el, "ended");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_AbortAfterPartialMediaSegment.html b/dom/media/mediasource/test/test_AbortAfterPartialMediaSegment.html
new file mode 100644
index 0000000000..4c695f48c7
--- /dev/null
+++ b/dom/media/mediasource/test/test_AbortAfterPartialMediaSegment.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: can properly resume after a partial media segment header followed by abort </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+const VIDEO_CODEC_STRING = 'video/webm; codecs="vp09.00.51.08.01.01.01.01"';
+
+const logError = (error) => {
+ console.error(error, error.message);
+ ok(false, "should not reach here");
+};
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+
+ const supported = MediaSource.isTypeSupported(VIDEO_CODEC_STRING);
+ if (!supported) {
+ ok(true, "vp9 isn't supported on this platform, abort");
+ SimpleTest.finish();
+ return;
+ }
+ const sb = ms.addSourceBuffer(VIDEO_CODEC_STRING);
+
+ const arrayBuffer = await fetchWithXHR("1516754.webm");
+ info("- append init segment, a media segment and a partial media segment header -");
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 0, 87355 + 3000));
+
+ info("- wait for updateend -");
+ await once(sb, "updateend");
+
+ // start seeking.
+ v.currentTime = 11;
+ v.addEventListener("seeked", () => {
+ info("- seek completed -");
+ SimpleTest.finish();
+ });
+
+ sb.abort();
+
+ info("- append init segment -");
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 0, 3150));
+ info("- wait for updateend -");
+ await once(sb, "updateend");
+ info("- append media segment 10-15s -");
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 159968, 72931));
+
+ // We now wait for seek to complete
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_AppendPartialInitSegment.html b/dom/media/mediasource/test/test_AppendPartialInitSegment.html
new file mode 100644
index 0000000000..408c073bd6
--- /dev/null
+++ b/dom/media/mediasource/test/test_AppendPartialInitSegment.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: split init segment and append them separately </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/webm");
+
+ const arrayBuffer = await fetchWithXHR("seek.webm");
+ // init segment is total 236 bytes.
+ info("- append partial init segment -");
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 0, 100));
+
+ info("- wait for updateend -");
+ await once(sb, "updateend");
+
+ info("- append remaining init segment -");
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 100, 136));
+
+ info("- wait for metadata -");
+ await once(v, "loadedmetadata");
+ is(v.videoWidth, 320, "videoWidth has correct initial value");
+ is(v.videoHeight, 240, "videoHeight has correct initial value");
+
+ info("- wait for updateend -");
+ await once(sb, "updateend");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_AudioChange_mp4.html b/dom/media/mediasource/test/test_AudioChange_mp4.html
new file mode 100644
index 0000000000..9051af05a1
--- /dev/null
+++ b/dom/media/mediasource/test/test_AudioChange_mp4.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: basic functionality</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// This test checks loading a stereo segment, followed by a 5.1 segment plays without error.
+
+runWithMSE(async (ms, el) => {
+ el.controls = true;
+ await once(ms, "sourceopen");
+ ok(true, "Receive a sourceopen event");
+ logEvents(el);
+
+ const audiosb = ms.addSourceBuffer("audio/mp4");
+ el.addEventListener("error", e => {
+ ok(false, `should not fire ${e.type} event`);
+ SimpleTest.finish();
+ });
+ is(el.readyState, el.HAVE_NOTHING, "readyState is HAVE_NOTHING");
+ let p = once(el, "loadedmetadata");
+ await fetchAndLoad(audiosb, "aac20-48000-64000-", ["init"], ".mp4");
+ await p;
+ ok(true, "got loadedmetadata event");
+ p = Promise.all([once(el, "loadeddata"), once(el, "canplay")]);
+ await fetchAndLoad(audiosb, "aac20-48000-64000-", ["1"], ".m4s");
+ await p;
+ ok(true, "got canplay event");
+ el.play();
+ await fetchAndLoad(audiosb, "aac51-48000-128000-", ["init"], ".mp4");
+ await fetchAndLoad(audiosb, "aac51-48000-128000-", ["2"], ".m4s");
+ ms.endOfStream();
+ await once(el, "ended");
+ ok(el.currentTime >= 6, "played to the end");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_AudioChange_mp4_WebAudio.html b/dom/media/mediasource/test/test_AudioChange_mp4_WebAudio.html
new file mode 100644
index 0000000000..c76342f793
--- /dev/null
+++ b/dom/media/mediasource/test/test_AudioChange_mp4_WebAudio.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: basic functionality</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// This test checks loading a stereo segment, followed by a 5.1 segment plays
+// without error, when the audio is being routed to an AudioContext.
+
+const ac = new AudioContext();
+
+runWithMSE(async (ms, el) => {
+ el.controls = true;
+ const source = ac.createMediaElementSource(el);
+ source.connect(ac.destination);
+
+ await once(ms, "sourceopen");
+ ok(true, "Receive a sourceopen event");
+ logEvents(el);
+
+ const audiosb = ms.addSourceBuffer("audio/mp4");
+ el.addEventListener("error", e => {
+ ok(false, `should not fire ${e.type} event`);
+ SimpleTest.finish();
+ });
+ is(el.readyState, el.HAVE_NOTHING, "readyState is HAVE_NOTHING");
+ let p = once(el, "loadedmetadata");
+ await fetchAndLoad(audiosb, "aac20-48000-64000-", ["init"], ".mp4");
+ await p;
+ ok(true, "got loadedmetadata event");
+ p = Promise.all([once(el, "loadeddata"), once(el, "canplay")]);
+ await fetchAndLoad(audiosb, "aac20-48000-64000-", ["1"], ".m4s");
+ await p;
+ ok(true, "got canplay event");
+ el.play();
+ await fetchAndLoad(audiosb, "aac51-48000-128000-", ["init"], ".mp4");
+ await fetchAndLoad(audiosb, "aac51-48000-128000-", ["2"], ".m4s");
+ ms.endOfStream();
+ await once(el, "ended");
+ ok(el.currentTime >= 6, "played to the end");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_AutoRevocation.html b/dom/media/mediasource/test/test_AutoRevocation.html
new file mode 100644
index 0000000000..42e9b0e6a5
--- /dev/null
+++ b/dom/media/mediasource/test/test_AutoRevocation.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: auto-revocation</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(function() {
+ const ms = new MediaSource();
+ const o = URL.createObjectURL(ms);
+ const v = document.createElement("video");
+
+ v.addEventListener("error", () => {
+ ok(true, "ObjectURL should be auto-revoked");
+ SimpleTest.finish();
+ });
+
+ v.addEventListener("stalled", () => {
+ ok(false, "If auto-revocation is gone, please turn on TODOs in browser_mediaSourceURL.js");
+ SimpleTest.finish();
+ });
+
+ setTimeout(function() {
+ v.src = o;
+ v.preload = "auto";
+ document.body.appendChild(v);
+ }, 0);
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_BufferedSeek.html b/dom/media/mediasource/test/test_BufferedSeek.html
new file mode 100644
index 0000000000..039f56bc16
--- /dev/null
+++ b/dom/media/mediasource/test/test_BufferedSeek.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: seeking in buffered range</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/webm");
+
+ sb.appendBuffer(new Uint8Array(await fetchWithXHR("seek.webm")));
+
+ const target = 2;
+
+ v.addEventListener("loadedmetadata", () => {
+ ok(true, "received loadedmetadata");
+ v.currentTime = target;
+ });
+
+ let wasSeeking = false;
+
+ v.addEventListener("seeking", () => {
+ wasSeeking = true;
+ is(v.currentTime, target, "Video currentTime at target");
+ });
+
+ await once(v, "seeked");
+ ok(wasSeeking, "Received expected seeking and seeked events");
+ is(v.currentTime, target, "Video currentTime at target");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_BufferedSeek_mp4.html b/dom/media/mediasource/test/test_BufferedSeek_mp4.html
new file mode 100644
index 0000000000..e89e972c91
--- /dev/null
+++ b/dom/media/mediasource/test/test_BufferedSeek_mp4.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: seeking in buffered range</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/mp4");
+
+ sb.appendBuffer(new Uint8Array(await fetchWithXHR("bipbop/bipbop2s.mp4")));
+
+ const target = 1.3;
+
+ await once(v, "loadedmetadata");
+ ok(true, "received loadedmetadata");
+ v.currentTime = target;
+
+ let wasSeeking = false;
+
+ v.addEventListener("seeking", () => {
+ wasSeeking = true;
+ is(v.currentTime, target, "Video currentTime at target");
+ });
+
+ await once(v, "seeked");
+ ok(wasSeeking, "Received expected seeking and seeked events");
+ is(v.currentTime, target, "Video currentTime at target");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_BufferingWait.html b/dom/media/mediasource/test/test_BufferingWait.html
new file mode 100644
index 0000000000..289ddfe4d2
--- /dev/null
+++ b/dom/media/mediasource/test/test_BufferingWait.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <title>MSE: Don't get stuck buffering for too long when we have frames to show</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test"><script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ ok(true, "Receive a sourceopen event");
+ ms.addEventListener("sourceopen", () => ok(false, "No more sourceopen"));
+ const sb = ms.addSourceBuffer("video/webm");
+ ok(sb, "Create a SourceBuffer");
+
+ const arrayBuffer = await fetchWithXHR("seek.webm");
+ sb.addEventListener("error", e => {
+ ok(false, "Got Error: " + e);
+ SimpleTest.finish();
+ });
+ await loadSegment(sb, new Uint8Array(arrayBuffer, 0, 318));
+ await loadSegment(sb, new Uint8Array(arrayBuffer, 318, 25523 - 318));
+ await loadSegment(sb, new Uint8Array(arrayBuffer, 25523, 46712 - 25523));
+ /* Note - Missing |46712, 67833 - 46712| segment here corresponding to (0.8, 1.2] */
+ /* Note - Missing |67833, 88966 - 67833| segment here corresponding to (1.2, 1.6] */
+ await loadSegment(sb, new Uint8Array(arrayBuffer, 88966));
+ // 0.767 is the time of the last video sample +- 40ms.
+ info("Playing video. It should play for a bit, then fire 'waiting'");
+ v.play();
+ await waitUntilTime(v, .767 - 0.04);
+ const firstStop = Date.now();
+ await loadSegment(sb, new Uint8Array(arrayBuffer, 46712, 67833 - 46712));
+ await waitUntilTime(v, 1.167 - 0.04);
+ const waitDuration = (Date.now() - firstStop) / 1000;
+ ok(waitDuration < 15, `Should not spend inordinate amount of time buffering: ${waitDuration}`);
+ SimpleTest.finish();
+ /* If we allow the rest of the stream to be played, we get stuck at
+ around 2s. See bug 1093133.
+ await once(v, "ended");
+ SimpleTest.finish();
+ await loadSegment(sb, new Uint8Array(arrayBuffer, 67833, 88966 - 67833));
+ */
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_BufferingWait_mp4.html b/dom/media/mediasource/test/test_BufferingWait_mp4.html
new file mode 100644
index 0000000000..04e094a852
--- /dev/null
+++ b/dom/media/mediasource/test/test_BufferingWait_mp4.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <title>MSE: Don't get stuck buffering for too long when we have frames to show</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test"><script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ ok(true, "Receive a sourceopen event");
+ ms.addEventListener("sourceopen", () => ok(false, "No more sourceopen"));
+ const sb = ms.addSourceBuffer("video/mp4");
+ ok(sb, "Create a SourceBuffer");
+
+ sb.addEventListener("error", e => {
+ ok(false, "Got Error: " + e);
+ SimpleTest.finish();
+ });
+ await fetchAndLoad(sb, "bipbop/bipbop", ["init"], ".mp4");
+ await fetchAndLoad(sb, "bipbop/bipbop", ["1"], ".m4s");
+ await fetchAndLoad(sb, "bipbop/bipbop", ["2"], ".m4s");
+ /* Note - Missing |bipbop3| segment here corresponding to (1.62, 2.41] */
+ /* Note - Missing |bipbop4| segment here corresponding to (2.41, 3.20] */
+ await fetchAndLoad(sb, "bipbop/bipbop", ["5"], ".m4s");
+ // last audio sample has a start time of 1.578956s
+ info("Playing video. It should play for a bit, then fire 'waiting'");
+ v.play();
+ await waitUntilTime(v, 1.57895);
+ const firstStop = Date.now();
+ await fetchAndLoad(sb, "bipbop/bipbop", ["3"], ".m4s");
+ // last audio sample has a start time of 2.368435
+ await waitUntilTime(v, 2.36843);
+ const waitDuration = (Date.now() - firstStop) / 1000;
+ ok(waitDuration < 15, `Should not spend inordinate amount of time buffering: ${waitDuration}`);
+ await fetchAndLoad(sb, "bipbop/bipbop", ["4"], ".m4s");
+ ms.endOfStream();
+ await once(v, "ended");
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_ChangeType.html b/dom/media/mediasource/test/test_ChangeType.html
new file mode 100644
index 0000000000..690b9f61c4
--- /dev/null
+++ b/dom/media/mediasource/test/test_ChangeType.html
@@ -0,0 +1,84 @@
+<!DOCTYPE html>
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <title>MSE: changeType allow to change container and codec type</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test"><script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(function(ms, el) {
+ el.controls = true;
+ once(ms, "sourceopen").then(function() {
+ // Log events for debugging.
+ const events = ["suspend", "play", "canplay", "canplaythrough", "loadstart", "loadedmetadata",
+ "loadeddata", "playing", "ended", "error", "stalled", "emptied", "abort",
+ "waiting", "pause", "durationchange", "seeking", "seeked"];
+ function logEvent(e) {
+ info("got " + e.type + " event");
+ }
+ events.forEach(function(e) {
+ el.addEventListener(e, logEvent);
+ });
+
+ ok(true, "Receive a sourceopen event");
+
+ const videosb = ms.addSourceBuffer("video/mp4");
+ if (typeof videosb.changeType === "undefined") {
+ info("changeType API is not available");
+ }
+
+ el.addEventListener("error", e => {
+ ok(false, "should not fire '" + e.type + "' event");
+ SimpleTest.finish();
+ });
+ is(el.readyState, el.HAVE_NOTHING, "readyState is HAVE_NOTHING");
+ const loadedmetadataPromises = [];
+ loadedmetadataPromises.push(fetchAndLoad(videosb, "bipbop/bipbop", ["init"], ".mp4"));
+ loadedmetadataPromises.push(once(el, "loadedmetadata"));
+ Promise.all(loadedmetadataPromises)
+ .then(function() {
+ ok(true, "got loadedmetadata event");
+ const canplayPromises = [];
+ canplayPromises.push(once(el, "loadeddata"));
+ canplayPromises.push(once(el, "canplay"));
+ canplayPromises.push(fetchAndLoad(videosb, "bipbop/bipbop", range(1, 3), ".m4s"));
+ return Promise.all(canplayPromises);
+ })
+ .then(function() {
+ ok(true, "got canplay event");
+ el.play();
+ videosb.timestampOffset = el.buffered.end(0);
+ return fetchAndLoad(videosb, "bipbop/bipbop_480_624kbps-video", ["init"], ".mp4");
+ })
+ .then(fetchAndLoad.bind(null, videosb, "bipbop/bipbop_480_624kbps-video", range(1, 3), ".m4s"))
+ .then(function() {
+ videosb.timestampOffset = el.buffered.end(0);
+ try {
+ videosb.changeType("video/webm");
+ } catch (e) {
+ ok(false, "shouldn't throw an exception");
+ SimpleTest.finish();
+ throw e;
+ }
+ return fetchAndLoad(videosb, "bipbop/bipbop_300-3s", [""], ".webm");
+ })
+ .then(function() {
+ ms.endOfStream();
+ return once(el, "ended");
+ })
+ .then(function() {
+ ok(el.currentTime >= el.buffered.end(0), "played to the end");
+ SimpleTest.finish();
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_ChangeWhileWaitingOnMissingData_mp4.html b/dom/media/mediasource/test/test_ChangeWhileWaitingOnMissingData_mp4.html
new file mode 100644
index 0000000000..b5889da560
--- /dev/null
+++ b/dom/media/mediasource/test/test_ChangeWhileWaitingOnMissingData_mp4.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <title>MSE: resume from waiting even after format change occurred</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test"><script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, el) => {
+ el.controls = true;
+ await once(ms, "sourceopen");
+ ok(true, "Receive a sourceopen event");
+ const sb = ms.addSourceBuffer("video/mp4");
+ await fetchAndLoad(sb, "bipbop/bipbop_480_624kbps-video", ["init"], ".mp4");
+ await fetchAndLoad(sb, "bipbop/bipbop_480_624kbps-video", range(1, 3), ".m4s");
+ el.play();
+ // let seek to the last audio frame.
+ // The seek will complete and then playback will stall.
+ el.currentTime = 1.532517;
+ await Promise.all([once(el, "seeked"), once(el, "waiting")]);
+ info("seek completed");
+ await fetchAndLoad(sb, "bipbop/bipbop", ["init"], ".mp4");
+ await fetchAndLoad(sb, "bipbop/bipbop", range(1, 4), ".m4s");
+ ms.endOfStream();
+ await once(el, "ended");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_DifferentStreamStartTimes.html b/dom/media/mediasource/test/test_DifferentStreamStartTimes.html
new file mode 100644
index 0000000000..197e809e4f
--- /dev/null
+++ b/dom/media/mediasource/test/test_DifferentStreamStartTimes.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: Mismatched stream start time playback test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+runWithMSE(async (ms, el) => {
+ el.controls = true;
+ el.autoplay = true;
+ for (let i = 0; i <= 0.5; i += 0.1) {
+ const offset = i.toFixed(1);
+
+ info("----------------------");
+ info("Running test for mismatched stream start times with offset of: " + offset);
+ info("----------------------");
+
+ ms = new MediaSource();
+ el.removeAttribute("src");
+ el.src = URL.createObjectURL(ms);
+
+ await once(ms, "sourceopen");
+ logEvents(el);
+ const videosb = ms.addSourceBuffer("video/mp4");
+ ok(true, "Receive a sourceopen event");
+
+ el.addEventListener("error", e => {
+ ok(false, `should not fire ${e.type} event`);
+ SimpleTest.finish();
+ });
+
+ let p = once(el, "loadedmetadata");
+ await fetchAndLoad(videosb, "bipbop/bipbop_offset_" + offset + "-", ["init"], ".mp4");
+ await p;
+ ok(true, "got loadedmetadata event");
+
+ p = Promise.all(["loadeddata", "canplay", "play", "playing"].map(e => once(el, e)));
+ await fetchAndLoad(videosb, "bipbop/bipbop_offset_" + offset + "-", range(1, 2), ".m4s");
+ el.play();
+ await p;
+ }
+ ok(true, "got all required event");
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_DrainOnMissingData_mp4.html b/dom/media/mediasource/test/test_DrainOnMissingData_mp4.html
new file mode 100644
index 0000000000..ddc503aebf
--- /dev/null
+++ b/dom/media/mediasource/test/test_DrainOnMissingData_mp4.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <title>MSE: |waiting| event when source data is missing</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test"><script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, el) => {
+ el.controls = true;
+ await once(ms, "sourceopen");
+ ok(true, "Receive a sourceopen event");
+ const videosb = ms.addSourceBuffer("video/mp4");
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", ["init"], ".mp4");
+ // Set appendWindowEnd to ensure we only have about 6 frames worth.
+ // We must feed at least 6 frames to pass the MDSM pre-roll.
+ videosb.appendWindowEnd = .4;
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", ["1"], ".m4s");
+ info("Invoking play()");
+ const p = once(el, "playing");
+ await el.play();
+ await p;
+ info("got playing");
+ await once(el, "waiting");
+ info("got waiting");
+ info("Loading more data");
+ // Waiting will be fired on the last frame +- 40ms.
+ isfuzzy(el.currentTime, videosb.buffered.end(0) - 1 / 30,
+ 0.04, `Got a waiting event at ${el.currentTime}`);
+ videosb.appendWindowEnd = 1;
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", [1], ".m4s");
+ ms.endOfStream();
+ await once(el, "ended");
+ // These fuzz factors are bigger than they should be. We should investigate
+ // and fix them in bug 1137574.
+ is(el.duration, 0.801666, "Video has correct duration: " + el.duration);
+ is(el.currentTime, el.duration, "Video has correct currentTime.");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_DurationChange.html b/dom/media/mediasource/test/test_DurationChange.html
new file mode 100644
index 0000000000..3c83e83fa4
--- /dev/null
+++ b/dom/media/mediasource/test/test_DurationChange.html
@@ -0,0 +1,71 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: check that duration change behaves properly</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/webm");
+
+ const arrayBuffer = await fetchWithXHR("seek.webm");
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 0, 318));
+ await Promise.all([once(v, "loadedmetadata"), once(sb, "updateend")]);
+ is(v.duration, ms.duration, "video duration is mediasource one");
+ must_not_throw(() => ms.duration = 0, "duration = 0 is valid initially");
+ is(v.duration, 0, "reducing duration with no data buffered is valid");
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 318));
+ // Adding more data will fire durationchange.
+ await once(sb, "updateend");
+ ok(true, "got updateend");
+ // XXX: Duration should be exactly 4.0, see bug 1065207.
+ ok(Math.abs(v.duration - 4) <= 0.002, "Video has correct duration");
+ must_throw(() => ms.duration = 0,
+ "Must use remove for range removal",
+ "InvalidStateError");
+ ok(Math.abs(v.duration - 4) <= 0.002, "Video has correct duration");
+ must_not_throw(() => ms.duration = 10, "setting duration past data is valid");
+ is(v.duration, 10, "extending duration is always valid");
+ // The last sample has a start time of 3.967000s and a end time of 4.001 (see bug 1065207).
+ must_not_throw(() => ms.duration = 3.967000,
+ "setting duration with >= highest frame presentation time is valid");
+ is(v.duration, sb.buffered.end(0),
+ "duration is the highest end time reported by the buffered attribute ");
+ must_not_throw(() => ms.duration = 3.97,
+ "setting duration with >= highest frame presentation time is valid");
+ is(v.duration, sb.buffered.end(0),
+ "duration is the highest end time reported by the buffered attribute ");
+ must_throw(() => ms.duration = 3.96,
+ "setting duration with < highest frame presentation time is not valid",
+ "InvalidStateError");
+ is(v.duration, sb.buffered.end(0),
+ "duration is the highest end time reported by the buffered attribute ");
+ must_throw(() => ms.duration = -1, "can't set a negative duration", "TypeError");
+ sb.remove(sb.buffered.end(0), Infinity);
+ is(sb.updating, true, "updating is true");
+ must_throw(() => ms.duration = Infinity,
+ "setting the duration while updating is not allowed",
+ "InvalidStateError");
+ must_throw(() => sb.abort(),
+ "Can't use abort while range removal is in progress",
+ "InvalidStateError");
+ is(v.duration, sb.buffered.end(0),
+ "duration is the highest end time reported by the buffered attribute ");
+ await once(sb, "updateend");
+ ms.endOfStream();
+ await once(ms, "sourceended");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_DurationUpdated.html b/dom/media/mediasource/test/test_DurationUpdated.html
new file mode 100644
index 0000000000..eb54e76c90
--- /dev/null
+++ b/dom/media/mediasource/test/test_DurationUpdated.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: append data and check that mediasource duration got updated</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/webm");
+
+ let durationChangeCount = 0;
+ v.addEventListener("durationchange", () => durationChangeCount++);
+
+ const arrayBuffer = await fetchWithXHR("seek.webm");
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 0, 318));
+
+ // Adding the first init segment will fire a durationchange.
+ await Promise.all([once(sb, "updateend"), once(v, "loadedmetadata")]);
+ ok(true, "got loadedmetadata");
+ // Set mediasource duration to 0, so future appendBuffer
+ // will update the mediasource duration.
+ // Changing the duration will fire a durationchange.
+ ms.duration = 0;
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 318));
+ // Adding more data will fire durationchange.
+ await once(sb, "updateend");
+ ok(true, "got updateend");
+ // this will not fire durationchange as new duration == old duration
+ ms.endOfStream();
+ await once(ms, "sourceended");
+ is(durationChangeCount, 3, "durationchange not fired as many times as expected");
+ // XXX: Duration should be exactly 4.0, see bug 1065207.
+ ok(Math.abs(v.duration - 4) <= 0.002, "Video has correct duration");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_DurationUpdated_mp4.html b/dom/media/mediasource/test/test_DurationUpdated_mp4.html
new file mode 100644
index 0000000000..f263264b09
--- /dev/null
+++ b/dom/media/mediasource/test/test_DurationUpdated_mp4.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: append data and check that mediasource duration got updated</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/mp4");
+
+ let durationChangeCount = 0;
+ v.addEventListener("durationchange", () => durationChangeCount++);
+
+ const arrayBuffer = await fetchWithXHR("bipbop/bipbop2s.mp4");
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 0, 1395));
+
+ // Adding the first init segment will fire a durationchange.
+ await Promise.all([once(sb, "updateend"), once(v, "loadedmetadata")]);
+ ok(true, "got loadedmetadata");
+ // Set mediasource duration to 0, so future appendBuffer
+ // will update the mediasource duration.
+ // Changing the duration will fire a durationchange.
+ ms.duration = 0;
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 1395));
+ // Adding more data will fire durationchange.
+ await once(sb, "updateend");
+ ok(true, "got updateend");
+ // this will not fire durationchange as new duration == old duration
+ ms.endOfStream();
+ await once(ms, "sourceended");
+ is(durationChangeCount, 3, "durationchange not fired as many times as expected");
+ is(v.duration, 1.696666, "Video has correct duration");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_EndOfStream.html b/dom/media/mediasource/test/test_EndOfStream.html
new file mode 100644
index 0000000000..b926869f1f
--- /dev/null
+++ b/dom/media/mediasource/test/test_EndOfStream.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: endOfStream call after an appendBuffer</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/webm");
+
+ sb.appendBuffer(new Uint8Array(await fetchWithXHR("seek.webm"), 0, 88966));
+ await once(sb, "updateend");
+ await wait(0);
+ must_not_throw(() => ms.endOfStream(), "MediaSource.endOfStream succeeded");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_EndOfStream_mp4.html b/dom/media/mediasource/test/test_EndOfStream_mp4.html
new file mode 100644
index 0000000000..9319b80390
--- /dev/null
+++ b/dom/media/mediasource/test/test_EndOfStream_mp4.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: endOfStream call after an appendBuffer</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/mp4");
+
+ sb.appendBuffer(new Uint8Array(await fetchWithXHR("bipbop/bipbop2s.mp4")));
+ await once(sb, "updateend");
+ await wait(0);
+ must_not_throw(() => ms.endOfStream(), "MediaSource.endOfStream succeeded");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_EndedEvent.html b/dom/media/mediasource/test/test_EndedEvent.html
new file mode 100644
index 0000000000..ee43fa8cf5
--- /dev/null
+++ b/dom/media/mediasource/test/test_EndedEvent.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: basic functionality</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, el) => {
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/webm");
+ sb.appendBuffer(new Uint8Array(await fetchWithXHR("seek.webm")));
+ sb.addEventListener("updateend", () => ms.endOfStream());
+
+ // Test "ended" is fired when seeking to the end of the media
+ // once the duration is known.
+ ms.onsourceended = () => el.currentTime = el.duration;
+ await once(el, "ended");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_Eviction_mp4.html b/dom/media/mediasource/test/test_Eviction_mp4.html
new file mode 100644
index 0000000000..e336fae4c7
--- /dev/null
+++ b/dom/media/mediasource/test/test_Eviction_mp4.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <title>MSE: QuotaExceededError when source buffer is full</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test"><script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+// We fill up the source buffer with audio data until the buffer is full.
+// We ensure that QuotaExceededError is thrown once the buffer is full.
+// We then seek to half the content. By that time, another appendBuffer must succeed
+// as the auto-eviction would succeed (removing all data prior currentTime)
+
+addMSEPrefs(
+ ["media.mediasource.eviction_threshold.audio", 524288],
+ ["media.dormant-on-pause-timeout-ms", -1] // FIXME: bug 1319292
+);
+
+runWithMSE(async (ms, el) => {
+ el.controls = true;
+ await once(ms, "sourceopen");
+ ok(true, "Receive a sourceopen event");
+ const audiosb = ms.addSourceBuffer("audio/mp4");
+ audiosb.mode = "sequence";
+ await fetchAndLoad(audiosb, "bipbop/bipbop_audio", ["init"], ".mp4");
+ const audioBuffer = await fetchWithXHR("bipbop/bipbop_audio1.m4s");
+
+ await must_reject(async () => {
+ // We are appending data repeatedly in sequence mode, there should be no gaps.
+ while (true) {
+ ok(audiosb.buffered.length <= 1, "there should be no gap in buffered ranges.");
+ audiosb.appendBuffer(audioBuffer);
+ await once(audiosb, "updateend");
+ }
+ },
+ "Fill up SourceBuffer by appending data until an exception is thrown.",
+ "QuotaExceededError");
+
+ is(audiosb.buffered.end(0), el.duration, "Duration is end of buffered range");
+ const seekTime = audiosb.buffered.end(0) / 2;
+ el.currentTime = seekTime;
+ await once(el, "seeked");
+ dump("dump: seeked to " + seekTime);
+ is(el.currentTime, seekTime, "correctly seeked to " + seekTime);
+ try {
+ audiosb.appendBuffer(audioBuffer);
+ await once(audiosb, "update");
+ ok(true, "appendBuffer succeeded");
+ } catch (ex) {
+ ok(false, "Shouldn't throw another time when data can be evicted");
+ dump(JSON.stringify(await SpecialPowers.wrap(el).mozRequestDebugInfo()));
+ }
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_ExperimentalAsync.html b/dom/media/mediasource/test/test_ExperimentalAsync.html
new file mode 100644
index 0000000000..6617716f26
--- /dev/null
+++ b/dom/media/mediasource/test/test_ExperimentalAsync.html
@@ -0,0 +1,102 @@
+<!DOCTYPE html>
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <title>MSE: testing removeAsync and appendBufferAsync</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test"><script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+addMSEPrefs(
+ ["media.mediasource.eviction_threshold.audio", 524288],
+ ["media.dormant-on-pause-timeout-ms", -1], // FIXME: bug 1319292
+ ["media.mediasource.experimental.enabled", true]
+);
+
+// We fill up the source buffer with audio data until the buffer is full.
+// We ensure that QuotaExceededError is thrown once the buffer is full.
+// We then seek to half the content. By that time, another appendBuffer must succeed
+// as the auto-eviction would succeed (removing all data prior currentTime)
+// The test then fills the audio buffer and plays until the end.
+
+// Fill up the SourceBuffer by appending data repeatedly via doAppendDataFunc until
+// an exception is thrown.
+async function fillUpSourceBuffer(sourceBuffer, doAppendDataFunc, onCaughtExceptionCallback) {
+ try {
+ // We are appending data repeatedly in sequence mode, there should be no gaps.
+ while (true) {
+ ok(sourceBuffer.buffered.length <= 1, "there should be no gap in buffered ranges.");
+ await doAppendDataFunc();
+ }
+ } catch (ex) {
+ ok(true, "appendBuffer promise got rejected");
+ onCaughtExceptionCallback(ex);
+ }
+}
+
+runWithMSE(async function(ms, el) {
+ el.controls = true;
+ await once(ms, "sourceopen");
+ ok(true, "Receive a sourceopen event");
+ const audiosb = ms.addSourceBuffer("audio/mp4");
+
+ // Test removeAsync
+ audiosb.mode = "sequence";
+ const audioInitBuffer = await fetchWithXHR("bipbop/bipbop_audioinit.mp4");
+ await audiosb.appendBufferAsync(audioInitBuffer);
+ const audioBuffer = await fetchWithXHR("bipbop/bipbop_audio1.m4s");
+ fillUpSourceBuffer(audiosb,
+ function() { // doAppendDataFunc
+ return audiosb.appendBufferAsync(audioBuffer);
+ },
+ async function(ex1) { // onCaughtExceptionCallback
+ is(ex1.name, "QuotaExceededError", "QuotaExceededError thrown");
+ is(audiosb.buffered.end(0), el.duration, "Duration is end of buffered range");
+ const seekTime = audiosb.buffered.end(0) / 2;
+ el.currentTime = seekTime;
+ await once(el, "seeked");
+ dump("dump: seeked to " + seekTime);
+ is(el.currentTime, seekTime, "correctly seeked to " + seekTime);
+ await audiosb.appendBufferAsync(audioBuffer).catch(async function(ex2) {
+ ok(false, "Shouldn't throw another time when data can be evicted");
+ dump(JSON.stringify(await SpecialPowers.wrap(el).mozRequestDebugInfo()));
+ SimpleTest.finish();
+ });
+ // Test that an error in remove return a rejected promise
+ await audiosb.removeAsync(5, 0).catch(async function(ex3) {
+ ok(true, "remove promise got rejected with end <= start");
+ is(ex3.name, "TypeError");
+ await audiosb.removeAsync(ms.duration + 1, Infinity).catch(async function(ex4) {
+ ok(true, "remove promise got rejected with start > duration");
+ is(ex4.name, "TypeError");
+ await audiosb.removeAsync(0, Infinity).catch(function(ex5) {
+ ok(false, "shouldn't throw");
+ });
+ ok(true, "remove succeeded");
+ is(audiosb.buffered.length, 0, "buffered should be empty");
+ audiosb.mode = "segment";
+ audiosb.timestampOffset = 0;
+ el.currentTime = 0;
+ await fetchAndLoadAsync(audiosb, "bipbop/bipbop_audio", range(1, 4), ".m4s");
+ ms.endOfStream();
+ el.play();
+ await once(el, "ended");
+ is(el.currentTime, el.duration, "played to the end");
+ SimpleTest.finish();
+ throw ex4; // ensure we don't fallback on lines below.
+ });
+ ok(false, "should have returned an error");
+ });
+ ok(false, "should have returned an error");
+ }
+ );
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_FrameSelection.html b/dom/media/mediasource/test/test_FrameSelection.html
new file mode 100644
index 0000000000..3e696841c2
--- /dev/null
+++ b/dom/media/mediasource/test/test_FrameSelection.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: verify correct frames selected for given position</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/webm");
+
+ let arrayBuffer = await fetchWithXHR("seek.webm");
+ let p = once(v, "loadedmetadata");
+ // Append entire file covering range [0, 4].
+ sb.appendBuffer(new Uint8Array(arrayBuffer));
+ await p;
+ is(v.currentTime, 0, "currentTime has correct initial value");
+ is(v.videoWidth, 320, "videoWidth has correct initial value");
+ is(v.videoHeight, 240, "videoHeight has correct initial value");
+
+ arrayBuffer = await fetchWithXHR("seek_lowres.webm");
+ // Append initialization segment.
+ info("Appending low-res init segment");
+ p = once(sb, "updateend");
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 0, 438));
+ await p;
+
+ info("Appending low-res range [2,4]");
+ // Append media segment covering range [2, 4].
+ p = once(sb, "updateend");
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 51003));
+ await p;
+
+ ms.endOfStream();
+
+ info("Seeking to t=3");
+ p = Promise.all([once(v, "seeked"), once(v, "resize")]);
+ v.currentTime = 3;
+ await p;
+ is(v.currentTime, 3, "Video currentTime at target");
+ is(v.videoWidth, 160, "videoWidth has correct low-res value");
+ is(v.videoHeight, 120, "videoHeight has correct low-res value");
+
+ info("Seeking to t=1");
+ p = Promise.all([once(v, "seeked"), once(v, "resize")]);
+ v.currentTime = 1;
+ await p;
+ is(v.currentTime, 1, "Video currentTime at target");
+ is(v.videoWidth, 320, "videoWidth has correct high-res value");
+ is(v.videoHeight, 240, "videoHeight has correct high-res value");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_FrameSelection_mp4.html b/dom/media/mediasource/test/test_FrameSelection_mp4.html
new file mode 100644
index 0000000000..628b4bf0e9
--- /dev/null
+++ b/dom/media/mediasource/test/test_FrameSelection_mp4.html
@@ -0,0 +1,49 @@
+<!DOCTYPE html>
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <title>MSE: Don't get stuck buffering for too long when we have frames to show</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test"><script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// This test loads partial video, plays and waits until playback stalls.
+// It then loads only 3 frames of a video at higher resolution.
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ ok(true, "Receive a sourceopen event");
+ ms.addEventListener("sourceopen", () => ok(false, "No more sourceopen"));
+ const sb = ms.addSourceBuffer("video/mp4");
+ ok(sb, "Create a SourceBuffer");
+ logEvents(v);
+ sb.addEventListener("error", e => {
+ ok(false, `should not fire ${e.type} event`);
+ SimpleTest.finish();
+ });
+ await fetchAndLoad(sb, "bipbop/bipbop", ["init"], ".mp4");
+ const p = once(v, "loadeddata");
+ await fetchAndLoad(sb, "bipbop/bipbop", range(1, 3), ".m4s");
+ await p;
+ is(sb.buffered.length, 1, "continuous range");
+ v.play();
+ // We have nothing to play, waiting will be fired.
+ await waitUntilTime(v, 1.5);
+ await fetchAndLoad(sb, "bipbop/bipbop_480_624kbps-video", ["init"], ".mp4");
+ sb.timestampOffset = 1.601666; // End of the video track buffered - time of first video sample (0.095).
+ sb.appendWindowEnd = 1.796677; // Only allow room for three extra video frames (we need 3 as this video has b-frames).
+ await fetchAndLoad(sb, "bipbop/bipbop_480_624kbps-video", ["1"], ".m4s");
+ ms.endOfStream();
+ await Promise.all([once(ms, "sourceended"), once(v, "playing"), once(v, "ended")]);
+ is(v.videoWidth, 640, "has proper width");
+ is(v.videoHeight, 480, "has proper height");
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_HEAAC_extradata.html b/dom/media/mediasource/test/test_HEAAC_extradata.html
new file mode 100644
index 0000000000..9fbbec8d72
--- /dev/null
+++ b/dom/media/mediasource/test/test_HEAAC_extradata.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>HE-AAC decoding test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+
+SimpleTest.waitForExplicitFinish();
+
+const SOURCE_FILE = "whitenoise-he-aac-5s.mp4";
+
+// This test checks when decoding HE-AAC using MediaSource or HTTP playback, the
+// audio is decoded correctly (in particular with the SBR part). This means
+// that the extradata describing the encoded AAC stream have been communicated
+// correctly to the audio decoder. For this, we check that there is energy
+// above 10kHz using the Web Audio API when playing white noise, which has
+// maximum energy accross the audible spectrum.
+
+// Return the index corresponding for a particular frequency in an array
+// containing frequency data from a FFT.
+function binIndexForFrequency(frequency, fftSize, sampleRate) {
+ return (1 + Math.round((frequency * fftSize) / sampleRate));
+}
+
+async function checkHighFrequencyContent(element) {
+ const ac = new AudioContext();
+ await ac.resume();
+ const mediaElementSource = ac.createMediaElementSource(element);
+ const analyser = new AnalyserNode(ac);
+
+ // Undo the volume scaling applied globally during test. This is fine because
+ // the audio isn't routed to an actual audio output device in this test, it's
+ // just analyzed with the Web Audio API.
+ const gain = new GainNode(ac);
+ const testVolumeScaling =
+ parseFloat(SpecialPowers.getCharPref("media.volume_scale"));
+ gain.gain.value = 1 / parseFloat(testVolumeScaling);
+ mediaElementSource.connect(gain).connect(analyser)
+
+ const spectrum = new Float32Array(analyser.frequencyBinCount);
+ const indexFor15kHz =
+ binIndexForFrequency(15000, analyser.fftSize, ac.sampleRate);
+ // Wait a few hundreds of milliseconds
+ while (!element.ended) {
+ await once(element, "timeupdate");
+ analyser.getFloatFrequencyData(spectrum);
+ if (spectrum[indexFor15kHz] > -50) {
+ ok(spectrum[indexFor15kHz] > -50,
+ `Energy present at 15kHz (bin index: ${indexFor15kHz}) when playing white noise encoded in HE-AAC ${spectrum[indexFor15kHz]}`);
+ return;
+ }
+ }
+ ok(false,
+ `No energy present at 15kHz (bin index: ${indexFor15kHz}) when playing white noise encoded in HE-AAC (last value ${spectrum[indexFor15kHz]})`);
+}
+
+runWithMSE(async (ms, el) => {
+ // First check with MSE playback
+ el.controls = true;
+ await once(ms, "sourceopen");
+
+ const audiosb = ms.addSourceBuffer('audio/mp4; codecs="mp4a.40.5"');
+ await fetchAndLoad(audiosb, SOURCE_FILE, [""], "");
+ ms.endOfStream();
+ el.play();
+ once(el, "playing");
+
+ await checkHighFrequencyContent(el);
+
+ // Redo the same test, with HTTP playback
+ el.src = SOURCE_FILE;
+ el.play();
+ once(el, "playing");
+
+ await checkHighFrequencyContent(el);
+
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_HaveMetadataUnbufferedSeek.html b/dom/media/mediasource/test/test_HaveMetadataUnbufferedSeek.html
new file mode 100644
index 0000000000..dd1b252f01
--- /dev/null
+++ b/dom/media/mediasource/test/test_HaveMetadataUnbufferedSeek.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: seekable attribute before end of stream</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/webm");
+
+ const arrayBuffer = await fetchWithXHR("seek.webm");
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 0, 67833));
+
+ const target = 2;
+
+ await once(v, "loadeddata");
+ ok(v.readyState >= v.HAVE_CURRENT_DATA, "readyState is >= CURRENT_DATA");
+ v.currentTime = target;
+
+ await once(v, "seeking");
+ is(v.readyState, v.HAVE_METADATA, "readyState is HAVE_METADATA");
+ sb.appendBuffer(new Uint8Array(await fetchWithXHR("seek.webm"), 67833));
+ await once(v, "seeked");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_HaveMetadataUnbufferedSeek_mp4.html b/dom/media/mediasource/test/test_HaveMetadataUnbufferedSeek_mp4.html
new file mode 100644
index 0000000000..9b8e885cda
--- /dev/null
+++ b/dom/media/mediasource/test/test_HaveMetadataUnbufferedSeek_mp4.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: seekable attribute before end of stream</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/mp4");
+
+ const arrayBuffer = await fetchWithXHR("bipbop/bipbop2s.mp4");
+ // 25819 is the offset of the first media segment's end
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 0, 25819));
+
+ const target = 1.3;
+
+ await once(v, "loadeddata");
+ ok(v.readyState >= v.HAVE_CURRENT_DATA, "readyState is >= CURRENT_DATA");
+ v.currentTime = target;
+
+ await once(v, "seeking");
+ is(v.readyState, v.HAVE_METADATA);
+ // 25819 is the offset of the first media segment's end
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 25819));
+ await once(sb, "updateend");
+ ms.endOfStream();
+ await once(v, "seeked");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_InputBufferIsCleared.html b/dom/media/mediasource/test/test_InputBufferIsCleared.html
new file mode 100644
index 0000000000..bad9a0c558
--- /dev/null
+++ b/dom/media/mediasource/test/test_InputBufferIsCleared.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: input buffer is cleared as expected (bug 1697476)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// Test bug 1697476 is fixed. We do this by appending a number of segments with
+// trailing `skip` boxes. If the bug is fixed, then the data from these appends
+// will eventually be cleared from memory. If not fixed, we leak that memory.
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/mp4");
+ await fetchAndLoad(sb, "bipbop/bipbop_video", ["init"], ".mp4");
+ // Load ~1mb of media.
+ await fetchAndLoad(sb, "bipbop/bipbop_trailing_skip_box_video", ["1"], ".m4s");
+ // Load ~1mb more media several more times.
+ const numberOfAppends = 5;
+ for (let i = 1; i < numberOfAppends; ++i) {
+ sb.timestampOffset = v.buffered.end(0);
+ await fetchAndLoad(sb, "bipbop/bipbop_trailing_skip_box_video", ["1"], ".m4s");
+ }
+
+ // Grab a memory report. We'll use this to make sure we're not accumulating
+ // too much data in our buffers.
+ const mgr = SpecialPowers.Cc["@mozilla.org/memory-reporter-manager;1"]
+ .getService(SpecialPowers.Ci.nsIMemoryReporterManager);
+
+ let amount = 0;
+ const handleReport = (aProcess, aPath, aKind, aUnits, aAmount) => {
+ if (aPath == "explicit/media/resources") {
+ amount += aAmount;
+ }
+ };
+
+ await new Promise(r => mgr.getReports(handleReport, null, r, null, /* anonymized = */ false));
+ ok(true, "Yay didn't crash!");
+ ok(amount !== undefined, "Got media resources amount");
+ const sgementSize = 1023860;
+ // Set the limit to be equal to the total data we appended. If we're not
+ // clearing buffers, we'll have all the data from the appends + some other
+ // data, so will fail.
+ const limit = sgementSize * numberOfAppends - 1;
+ ok(amount < limit, `Should have less than ${limit} bytes of media usage. Got ${amount} bytes.`);
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_LiveSeekable.html b/dom/media/mediasource/test/test_LiveSeekable.html
new file mode 100644
index 0000000000..f48852f6af
--- /dev/null
+++ b/dom/media/mediasource/test/test_LiveSeekable.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: live seekable range</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/webm");
+
+ // Load data with a +2 offset so that we can distinguish buffered range start
+ // and seekable range start.
+ sb.timestampOffset = 2;
+ const p = once(v, "loadedmetadata");
+ await fetchAndLoad(sb, "seek", [""], ".webm");
+ await p;
+ ms.duration = Infinity;
+ sb.abort();
+ is(sb.buffered.length, 1, "continuous buffered range");
+ is(sb.buffered.start(0), 2, "buffered range start at timestamp offset");
+ is(sb.buffered.end(0), 6.001, "buffered range end at original duration + timestamp offset");
+ is(v.seekable.length, 1, "continuous seekable range");
+ is(v.seekable.start(0), 0, "seekable range start at 0");
+ is(v.seekable.end(0), sb.buffered.end(0), "seekable range end at buffered end");
+
+ // LiveSeekableRange.start < buffered.start
+ ms.setLiveSeekableRange(1, 5);
+ is(v.seekable.length, 1, "continuous seekable range");
+ is(v.seekable.start(0), 1, "seekable range start at live range start");
+ is(v.seekable.end(0), sb.buffered.end(0), "seekable range end at buffered end");
+
+ ms.clearLiveSeekableRange();
+ is(v.seekable.length, 1, "continuous seekable range");
+ is(v.seekable.start(0), 0, "seekable range start at 0");
+ is(v.seekable.end(0), sb.buffered.end(0), "seekable range end at buffered end");
+
+ // LiveSeekableRange.end > buffered.end
+ ms.setLiveSeekableRange(1, 8);
+ is(v.seekable.start(0), 1, "seekable range start at live range start");
+ is(v.seekable.end(0), 8, "seekable range end at live range end");
+
+ // LiveSeekableRange.start > buffered.start
+ // LiveSeekableRange.end < buffered.end
+ ms.setLiveSeekableRange(3, 5);
+ is(v.seekable.start(0), sb.buffered.start(0), "seekable range start at buffered start");
+ is(v.seekable.end(0), sb.buffered.end(0), "seekable range end at live range end");
+
+ // LiveSeekableRange.start > buffered.end
+ ms.setLiveSeekableRange(8, 10);
+ is(v.seekable.start(0), sb.buffered.start(0), "seekable range start at buffered start");
+ is(v.seekable.end(0), 10, "seekable range end at live range end");
+
+ // LiveSeekableRange.end < buffered.start
+ ms.setLiveSeekableRange(0, 2);
+ is(v.seekable.start(0), 0, "seekable range start at live range start");
+ is(v.seekable.end(0), sb.buffered.end(0), "seekable range end at buffered end");
+
+ must_throw(() => ms.setLiveSeekableRange(2, 0),
+ "must thow if start > end",
+ "TypeError");
+
+ must_throw(() => ms.setLiveSeekableRange(2, 0),
+ "must thow if start > end",
+ "TypeError");
+
+ ms.setLiveSeekableRange(0, 1e300);
+ is(v.seekable.start(0), 0, "seekable range start at live range start");
+ is(v.seekable.end(0), 1e300, "seekable range end at specified time");
+
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_LoadedDataFired_mp4.html b/dom/media/mediasource/test/test_LoadedDataFired_mp4.html
new file mode 100644
index 0000000000..476303d4fd
--- /dev/null
+++ b/dom/media/mediasource/test/test_LoadedDataFired_mp4.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: Check that playback only starts once we have data at time = 0</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, el) => {
+ el.controls = true;
+ el.addEventListener("loadeddata", () => {
+ ok(el.buffered.length, "data is buffered");
+ is(el.buffered.start(0), 0, "must fire loadeddata when data has been loaded");
+ is(el.currentTime, 0, "must fire loadeddata at start");
+ });
+ el.addEventListener("playing", () => {
+ ok(el.buffered.length, "data is buffered");
+ is(el.buffered.start(0), 0, "must fire playing when data has been loaded");
+ ok(el.currentTime >= 0, "must have started playback");
+ });
+ await once(ms, "sourceopen");
+ ok(true, "Receive a sourceopen event");
+ const videosb = ms.addSourceBuffer("video/mp4");
+ is(el.readyState, el.HAVE_NOTHING, "readyState is HAVE_NOTHING");
+ let p = once(el, "loadedmetadata");
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", ["init"], ".mp4");
+ await p;
+ videosb.appendWindowStart = 2;
+ videosb.appendWindowEnd = 4;
+ is(el.readyState, el.HAVE_METADATA, "readyState is HAVE_METADATA");
+ // Load [2.4, 3.968344). 2.4 as it's the first keyframe after 2s and
+ // 3.968344 as the last frame ends after 4s.
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", range(1, 8), ".m4s");
+ is(el.readyState, el.HAVE_METADATA, "readyState is HAVE_METADATA");
+ // test that appendWindowEnd did its job.
+ ok(el.buffered.start(0) >= 2, "no data can be found prior appendWindowStart");
+ ok(el.buffered.end(el.buffered.length - 1) <= 4, "no data can be found beyond appendWindowEnd");
+ el.play();
+ await once(el, "play");
+ videosb.appendWindowStart = 0;
+ p = once(el, "playing");
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", range(1, 8), ".m4s");
+ await p;
+ ok(true, "playing");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_LoadedMetadataFired.html b/dom/media/mediasource/test/test_LoadedMetadataFired.html
new file mode 100644
index 0000000000..68030dbe2f
--- /dev/null
+++ b/dom/media/mediasource/test/test_LoadedMetadataFired.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: append initialization only</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/webm");
+
+ sb.appendBuffer(new Uint8Array(await fetchWithXHR("seek.webm"), 0, 318));
+ v.play();
+ await once(v, "loadedmetadata");
+ ok(true, "Got loadedmetadata event");
+ is(v.videoWidth, 320, "videoWidth has correct initial value");
+ is(v.videoHeight, 240, "videoHeight has correct initial value");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_LoadedMetadataFired_mp4.html b/dom/media/mediasource/test/test_LoadedMetadataFired_mp4.html
new file mode 100644
index 0000000000..0934907578
--- /dev/null
+++ b/dom/media/mediasource/test/test_LoadedMetadataFired_mp4.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: append initialization only</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/mp4");
+
+ sb.appendBuffer(new Uint8Array(await fetchWithXHR("bipbop/bipbop2s.mp4"), 0, 1395));
+ v.play();
+ await once(v, "loadedmetadata");
+ ok(true, "Got loadedmetadata event");
+ is(v.videoWidth, 400, "videoWidth has correct initial value");
+ is(v.videoHeight, 300, "videoHeight has correct initial value");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_MediaSource.html b/dom/media/mediasource/test/test_MediaSource.html
new file mode 100644
index 0000000000..9bdaa0d30b
--- /dev/null
+++ b/dom/media/mediasource/test/test_MediaSource.html
@@ -0,0 +1,92 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: basic functionality</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, v) => {
+ SimpleTest.doesThrow(() => new SourceBuffer, "new SourceBuffer should fail");
+ SimpleTest.doesThrow(() => new SourceBufferList, "new SourceBufferList direct should fail");
+
+ ok(ms instanceof EventTarget, "MediaSource must be an EventTarget");
+ is(ms.readyState, "closed", "New MediaSource must be in closed state");
+
+ // Wrapper creation, tests for leaks.
+ SpecialPowers.wrap(ms);
+
+ // Set an expando to force wrapper creation, tests for leaks.
+ ms.foo = null;
+
+ ok(URL.createObjectURL(ms), "Create an objectURL from the MediaSource");
+
+ let loadedmetadataCount = 0;
+ let updatestartCount = 0;
+ let updateendCount = 0;
+ let updateCount = 0;
+
+ ok(MediaSource.isTypeSupported("video/webm; codecs=vp8"), "VP8 MSE is always supported");
+ ok(MediaSource.isTypeSupported("audio/webm"), "Audio MSE is always supported");
+
+ await once(ms, "sourceopen");
+ ok(true, "Receive a sourceopen event");
+ is(ms.readyState, "open", "MediaSource must be in open state after sourceopen");
+ const sb = ms.addSourceBuffer("video/webm");
+ ok(sb, "Create a SourceBuffer");
+ is(ms.sourceBuffers.length, 1, "MediaSource.sourceBuffers is expected length");
+ is(ms.sourceBuffers[0], sb, "SourceBuffer in list matches our SourceBuffer");
+ is(ms.activeSourceBuffers.length, 0, "MediaSource.activeSourceBuffers is expected length");
+
+ sb.appendBuffer(new Uint8Array(await fetchWithXHR("seek.webm")));
+ is(sb.updating, true, "SourceBuffer.updating is expected value after appendBuffer");
+
+ sb.addEventListener("update", () => {
+ is(sb.updating, false, "SourceBuffer.updating is expected value in update event");
+ updateCount++;
+ /* Ensure that we endOfStream on the first update event only as endOfStream can
+ raise more if the duration of the last buffered range and the intial duration
+ differ. See bug 1065207 */
+ if (updateCount == 1) {
+ ms.endOfStream();
+ }
+ });
+
+ sb.addEventListener("updatestart", () => updatestartCount++);
+
+ sb.addEventListener("updateend", () => {
+ is(ms.activeSourceBuffers[0], sb, "SourceBuffer in active list matches our SourceBuffer");
+ is(sb.updating, false, "SourceBuffer.updating is expected value in updateend event");
+ updateendCount++;
+ v.play();
+ });
+
+ ms.addEventListener("sourceended", () => {
+ ok(true, "Receive a sourceended event");
+ is(ms.readyState, "ended", "MediaSource must be in ended state after sourceended");
+ });
+
+ v.addEventListener("loadedmetadata", () => loadedmetadataCount++);
+
+ await once(v, "ended");
+ // XXX: Duration should be exactly 4.0, see bug 1065207.
+ ok(Math.abs(v.duration - 4) <= 0.002, "Video has correct duration");
+ ok(Math.abs(v.currentTime - 4) <= 0.002, "Video has played to end");
+ // XXX: 2 update events can be received dueto duration differences, see bug 1065207.
+ ok(updateCount == 1 || updateCount == 2, "update event received");
+ ok(updateendCount == 1 || updateendCount == 2, "updateend event received");
+ ok(updatestartCount == 1 || updatestartCount == 2, "updatestart event received");
+ is(loadedmetadataCount, 1, "loadedmetadata event received");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_MediaSource_capture_gc.html b/dom/media/mediasource/test/test_MediaSource_capture_gc.html
new file mode 100644
index 0000000000..d986a6f9ac
--- /dev/null
+++ b/dom/media/mediasource/test/test_MediaSource_capture_gc.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test garbage collection of captured stream, when playing a MediaSource</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="mediasource.js"></script>
+</head>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+function forceGC() {
+ SpecialPowers.gc();
+ SpecialPowers.forceGC();
+ SpecialPowers.forceCC();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+window.onload = async function() {
+// Create an infinite source using a MediaSource
+let el = document.createElement("audio");
+const ms = new MediaSource();
+el.src = URL.createObjectURL(ms);
+await once(ms, "sourceopen");
+const sb = ms.addSourceBuffer("video/mp4");
+await fetchAndLoad(sb, "bipbop/bipbop_audio", ["init"], ".mp4");
+await fetchAndLoad(sb, "bipbop/bipbop_audio", range(1, 11), ".m4s");
+setInterval(async function() {
+ sb.timestampOffset = sb.buffered.end(sb.buffered.length - 1);
+ await fetchAndLoad(sb, "bipbop/bipbop_audio", range(1, 11), ".m4s");
+}, 8000);
+el.play();
+
+// Analyze the media element output.
+const ac = new AudioContext;
+const analyzer = ac.createAnalyser();
+
+// bug 1703603
+const stream = el.mozCaptureStreamUntilEnded();
+const mss = ac.createMediaStreamSource(stream);
+const gain = ac.createGain();
+// compensate mochitest volume scaling, but don't connect to the AudioContext's
+// destination to avoid noise during the test
+gain.gain.value = 90;
+mss.connect(gain).connect(analyzer);
+
+
+// Drop the media element reference: it is supposed to be kept alive by the
+// AudioContext via the `MediaStream`.
+el = null;
+
+// check whether the media element is still playing using the analyzer, spam the
+// GC to ensure all refs are kept.
+const buf = new Float32Array(analyzer.frequencyBinCount);
+const startTime = Date.now();
+function checkNonSilent() {
+ analyzer.getFloatFrequencyData(buf);
+ forceGC();
+ // Wait a good 20 seconds.
+ if (Date.now() - startTime < 2000) {
+ requestAnimationFrame(checkNonSilent);
+ } else {
+ ok(true, "All objects were kept alive.");
+ SimpleTest.finish();
+ }
+}
+checkNonSilent();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_MediaSource_disabled.html b/dom/media/mediasource/test/test_MediaSource_disabled.html
new file mode 100644
index 0000000000..e14f493e0f
--- /dev/null
+++ b/dom/media/mediasource/test/test_MediaSource_disabled.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: disabling via pref</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+function test() {
+ ok(!window.MediaSource && !window.SourceBuffer && !window.SourceBufferList,
+ "MediaSource should be hidden behind a pref");
+ SimpleTest.doesThrow(() => new MediaSource,
+ "MediaSource should be hidden behind a pref");
+ SimpleTest.finish();
+}
+
+SpecialPowers.pushPrefEnv({"set":
+ [
+ ["media.mediasource.enabled", false],
+ ],
+}, test);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_MediaSource_flac_mp4.html b/dom/media/mediasource/test/test_MediaSource_flac_mp4.html
new file mode 100644
index 0000000000..9cc159e467
--- /dev/null
+++ b/dom/media/mediasource/test/test_MediaSource_flac_mp4.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <title>MSE: Can seek to last frame</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test"><script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, el) => {
+ el.controls = true;
+ await once(ms, "sourceopen");
+ ok(true, "Receive a sourceopen event");
+ is(ms.readyState, "open", "MediaSource must be in open state after sourceopen");
+ const sb = ms.addSourceBuffer("audio/mp4; codecs=\"flac\"");
+ ok(sb, "Create a SourceBuffer");
+
+ await fetchAndLoad(sb, "flac/IS", [""], ".mp4");
+ await fetchAndLoad(sb, "flac/0000", range(1, 3), ".m4s");
+ el.play();
+ ms.endOfStream();
+ await once(el, "ended");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_MediaSource_memory_reporting.html b/dom/media/mediasource/test/test_MediaSource_memory_reporting.html
new file mode 100644
index 0000000000..70c720effd
--- /dev/null
+++ b/dom/media/mediasource/test/test_MediaSource_memory_reporting.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: memory reporting</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, v) => {
+ // Load a webm video and play it.
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/webm");
+ await fetchAndLoad(sb, "seek", [""], ".webm");
+ const p = once(v, "ended");
+ ms.endOfStream();
+ v.play();
+ await p;
+
+ // Test that memory reporting works once we've played a video.
+ // Grab a memory report.
+ const mgr = SpecialPowers.Cc["@mozilla.org/memory-reporter-manager;1"]
+ .getService(SpecialPowers.Ci.nsIMemoryReporterManager);
+
+ let amount;
+ const handleReport = (aProcess, aPath, aKind, aUnits, aAmount) => {
+ if (aPath == "explicit/media/resources") {
+ amount = (amount || 0) + aAmount;
+ }
+ };
+
+ await new Promise(r => mgr.getReports(handleReport, null, r, null, /* anonymized = */ false));
+ ok(true, "Yay didn't crash!");
+ ok(amount !== undefined, "Got media resources amount");
+ ok(amount > 0, "Non-zero amount reported for media resources");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_MediaSource_mp4.html b/dom/media/mediasource/test/test_MediaSource_mp4.html
new file mode 100644
index 0000000000..2ab79f37f3
--- /dev/null
+++ b/dom/media/mediasource/test/test_MediaSource_mp4.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: basic functionality</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, v) => {
+ SimpleTest.doesThrow(() => new SourceBuffer, "new SourceBuffer should fail");
+ SimpleTest.doesThrow(() => new SourceBufferList, "new SourceBufferList direct should fail");
+
+ ok(ms instanceof EventTarget, "MediaSource must be an EventTarget");
+ is(ms.readyState, "closed", "New MediaSource must be in closed state");
+
+ // Wrapper creation, tests for leaks.
+ SpecialPowers.wrap(ms);
+
+ // Set an expando to force wrapper creation, tests for leaks.
+ ms.foo = null;
+
+ ok(URL.createObjectURL(ms), "Create an objectURL from the MediaSource");
+
+ let loadedmetadataCount = 0;
+ let updatestartCount = 0;
+ let updateendCount = 0;
+ let updateCount = 0;
+
+ await once(ms, "sourceopen");
+ ok(true, "Receive a sourceopen event");
+ is(ms.readyState, "open", "MediaSource must be in open state after sourceopen");
+ const sb = ms.addSourceBuffer("video/mp4");
+ ok(sb, "Create a SourceBuffer");
+ is(ms.sourceBuffers.length, 1, "MediaSource.sourceBuffers is expected length");
+ is(ms.sourceBuffers[0], sb, "SourceBuffer in list matches our SourceBuffer");
+ is(ms.activeSourceBuffers.length, 0, "MediaSource.activeSourceBuffers is expected length");
+
+ sb.appendBuffer(new Uint8Array(await fetchWithXHR("bipbop/bipbop2s.mp4")));
+ is(sb.updating, true, "SourceBuffer.updating is expected value after appendBuffer");
+
+ sb.addEventListener("update", () => {
+ is(sb.updating, false, "SourceBuffer.updating is expected value in update event");
+ updateCount++;
+ /* Ensure that we endOfStream on the first update event only as endOfStream can
+ raise more if the duration of the last buffered range and the intial duration
+ differ. See bug 1065207 */
+ if (updateCount == 1) {
+ ms.endOfStream();
+ }
+ });
+
+ sb.addEventListener("updatestart", () => updatestartCount++);
+
+ sb.addEventListener("updateend", () => {
+ is(ms.activeSourceBuffers[0], sb, "SourceBuffer in active list matches our SourceBuffer");
+ is(sb.updating, false, "SourceBuffer.updating is expected value in updateend event");
+ updateendCount++;
+ v.play();
+ });
+
+ ms.addEventListener("sourceended", () => {
+ ok(true, "Receive a sourceended event");
+ is(ms.readyState, "ended", "MediaSource must be in ended state after sourceended");
+ });
+
+ v.addEventListener("loadedmetadata", () => loadedmetadataCount++);
+
+ await once(v, "ended");
+ // The bipbop video doesn't start at 0. The old MSE code adjust the
+ // timestamps and ignore the audio track. The new one doesn't.
+ isfuzzy(v.duration, 1.696, 0.166, "Video has correct duration");
+ isfuzzy(v.currentTime, 1.696, 0.166, "Video has correct duration");
+ // XXX: 2 update events can be received dueto duration differences, see bug 1065207.
+ ok(updateCount == 1 || updateCount == 2, "update event received");
+ ok(updateendCount == 1 || updateendCount == 2, "updateend event received");
+ ok(updatestartCount == 1 || updatestartCount == 2, "updatestart event received");
+ is(loadedmetadataCount, 1, "loadedmetadata event received");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_MultipleInitSegments.html b/dom/media/mediasource/test/test_MultipleInitSegments.html
new file mode 100644
index 0000000000..f4c91c08c5
--- /dev/null
+++ b/dom/media/mediasource/test/test_MultipleInitSegments.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <title>MSE: Append buffer with multiple init segments</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/webm");
+ const seek_lowres = await fetchWithXHR("seek_lowres.webm");
+ const seek = await fetchWithXHR("seek.webm");
+ const data = [
+ [seek_lowres, 0, 438], // lowres init segment
+ [seek_lowres, 438, 25950], // lowres media segment 0-1
+ [seek, 0, 318], // init segment
+ [seek, 46712, 67833], // media segment 0.8-1.201
+ ];
+ const length = data.map(d => d[2] - d[1]).reduce((a, b) => a + b, 0);
+ const arrayBuffer = new Uint8Array(length);
+ let pos = 0;
+ for (const d of data) {
+ const buffer = new Uint8Array(d[0], d[1], d[2] - d[1]);
+ arrayBuffer.set(buffer, pos);
+ pos += buffer.byteLength;
+ }
+ await loadSegment(sb, arrayBuffer);
+ // Since we are passing multiple segments in one buffer,
+ // the first durationchange event from parsing the init
+ // segment will be fired before updateend.
+ const p = once(v, "durationchange");
+ ms.endOfStream();
+ await p;
+ ok(v.duration, 1.201);
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_MultipleInitSegments_mp4.html b/dom/media/mediasource/test/test_MultipleInitSegments_mp4.html
new file mode 100644
index 0000000000..47c115677d
--- /dev/null
+++ b/dom/media/mediasource/test/test_MultipleInitSegments_mp4.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <title>MSE: Append buffer with multiple init segments</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/mp4");
+ const init = new Uint8Array(await fetchWithXHR("bipbop/bipbop_videoinit.mp4"));
+ const segment1 = new Uint8Array(await fetchWithXHR("bipbop/bipbop_video1.m4s"));
+ const segment2 = new Uint8Array(await fetchWithXHR("bipbop/bipbop_video2.m4s"));
+ const data = [init, segment1, init, segment2];
+ const length = data.map(d => d.byteLength).reduce((a, b) => a + b, 0);
+ const arrayBuffer = new Uint8Array(length);
+ let pos = 0;
+ for (const buffer of data) {
+ arrayBuffer.set(buffer, pos);
+ pos += buffer.byteLength;
+ }
+ await loadSegment(sb, arrayBuffer);
+ // Since we are passing multiple segments in one buffer,
+ // the first durationchange event from parsing the init
+ // segment will be fired before updateend.
+ const p = once(v, "durationchange");
+ ms.endOfStream();
+ await p;
+ ok(v.duration, 1.601666);
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_NoAudioLoopBackData.html b/dom/media/mediasource/test/test_NoAudioLoopBackData.html
new file mode 100644
index 0000000000..7de7209b74
--- /dev/null
+++ b/dom/media/mediasource/test/test_NoAudioLoopBackData.html
@@ -0,0 +1,78 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: loop-back data not available yet (shorter audio)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+
+/**
+ * This test is used to check whether a looping video can loop back successfully
+ * when it has a shorter audio track than its video track. When reaching EOS for
+ * the shorter track, there is no loop-back data at the start position (they are
+ * not appended yet) Even that, we should still be able to loop back but the
+ * looping would become non-seamless in this situation.
+ */
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, el) => {
+ await once(ms, "sourceopen");
+ ok(true, "Receive a sourceopen event");
+ const videosb = ms.addSourceBuffer("video/mp4");
+ const audiosb = ms.addSourceBuffer("audio/mp4");
+
+ // Here we create a shorter audio than video.
+ info(`create different length source buffers`);
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", ["init"], ".mp4");
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", range(5, 8), ".m4s");
+ audiosb.appendWindowEnd = videosb.buffered.end(0) - 0.2;
+ await fetchAndLoad(audiosb, "bipbop/bipbop_audio", ["init"], ".mp4");
+ await fetchAndLoad(audiosb, "bipbop/bipbop_audio", range(5, 8), ".m4s");
+ ms.endOfStream();
+ await Promise.all([once(el, "durationchange"), once(ms, "sourceended")]);
+ info(`audio=[${audiosb.buffered.start(0)}-${audiosb.buffered.end(0)}], video=[${videosb.buffered.start(0)}-${videosb.buffered.end(0)}]`);
+ ok(true, `endOfStream completed, buffer=[${el.buffered.start(0)}, ${el.buffered.end(0)}]`);
+ ok(videosb.buffered.end(0) > audiosb.buffered.end(0), `video should be longer than audio`);
+
+ info(`seek to the position where buffered data exists`);
+ el.loop = true;
+ el.controls = true;
+ el.currentTime = el.buffered.start(0);
+ await el.play();
+
+ info(`video should trigger seeking when reaching to the end`);
+ let seekingCount = 0, seekedCount = 0;
+ el.addEventListener("seeking", () => {
+ is(++seekingCount, 1, "should only receive seeking once!");
+ });
+ el.addEventListener("seeked", () => {
+ is(++seekedCount, 1, "should only receive seeked once!");
+ });
+ await once(el, "seeking");
+
+ info(`trim old data before append new data`);
+ let p = Promise.all([once(videosb, "updateend"), once(audiosb, "updateend")]);
+ videosb.remove(videosb.buffered.start(0), videosb.buffered.end(0));
+ audiosb.remove(audiosb.buffered.start(0), audiosb.buffered.end(0));
+ await p;
+
+ info(`append new data`);
+ const seekedPromise = once(el, "seeked");
+ p = Promise.all([once(videosb, "updateend"), once(audiosb, "updateend")]);
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", range(1, 2), ".m4s");
+ await fetchAndLoad(audiosb, "bipbop/bipbop_audio", range(1, 2), ".m4s");
+ await p;
+ info(`audio=[${audiosb.buffered.start(0)}-${audiosb.buffered.end(0)}], video=[${videosb.buffered.start(0)}-${videosb.buffered.end(0)}]`);
+
+ info(`now we should be able to finish seeking to the start position`);
+ await seekedPromise;
+
+ SimpleTest.finish(SimpleTest);
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_NoAudioLoopBackData_Muted.html b/dom/media/mediasource/test/test_NoAudioLoopBackData_Muted.html
new file mode 100644
index 0000000000..14cdf34bc6
--- /dev/null
+++ b/dom/media/mediasource/test/test_NoAudioLoopBackData_Muted.html
@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: loop-back data not available yet (shorter MUTED audio)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+
+/**
+ * This test is used to check whether a looping video can loop back successfully
+ * when it has a shorter MUTED audio track than its video track. When reaching
+ * EOS for the shorter track, there is no loop-back data at the start position
+ * (they are not appended yet) Even that, we should still be able to loop back
+ * but the looping would become non-seamless in this situation.
+ */
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, el) => {
+ await once(ms, "sourceopen");
+ ok(true, "Receive a sourceopen event");
+ const videosb = ms.addSourceBuffer("video/mp4");
+ const audiosb = ms.addSourceBuffer("audio/mp4");
+
+ // Here we create a shorter audio than video.
+ info(`create different length source buffers`);
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", ["init"], ".mp4");
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", range(5, 8), ".m4s");
+ audiosb.appendWindowEnd = videosb.buffered.end(0) - 0.2;
+ await fetchAndLoad(audiosb, "bipbop/bipbop_audio", ["init"], ".mp4");
+ await fetchAndLoad(audiosb, "bipbop/bipbop_audio", range(5, 8), ".m4s");
+ ms.endOfStream();
+ await Promise.all([once(el, "durationchange"), once(ms, "sourceended")]);
+ info(`audio=[${audiosb.buffered.start(0)}-${audiosb.buffered.end(0)}], video=[${videosb.buffered.start(0)}-${videosb.buffered.end(0)}]`);
+ ok(true, `endOfStream completed, buffer=[${el.buffered.start(0)}, ${el.buffered.end(0)}]`);
+ ok(videosb.buffered.end(0) > audiosb.buffered.end(0), `video should be longer than audio`);
+
+ info(`seek to the position where buffered data exists`);
+ el.muted = true;
+ el.loop = true;
+ el.controls = true;
+ el.currentTime = el.buffered.start(0);
+ await el.play();
+
+ info(`video should trigger seeking when reaching to the end`);
+ let seekingCount = 0, seekedCount = 0;
+ el.addEventListener("seeking", () => {
+ is(++seekingCount, 1, "should only receive seeking once!");
+ });
+ el.addEventListener("seeked", () => {
+ is(++seekedCount, 1, "should only receive seeked once!");
+ });
+ await once(el, "seeking");
+
+ info(`trim old data before append new data`);
+ let p = Promise.all([once(videosb, "updateend"), once(audiosb, "updateend")]);
+ videosb.remove(videosb.buffered.start(0), videosb.buffered.end(0));
+ audiosb.remove(audiosb.buffered.start(0), audiosb.buffered.end(0));
+ await p;
+
+ info(`append new data`);
+ const seekedPromise = once(el, "seeked");
+ p = Promise.all([once(videosb, "updateend"), once(audiosb, "updateend")]);
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", range(1, 2), ".m4s");
+ await fetchAndLoad(audiosb, "bipbop/bipbop_audio", range(1, 2), ".m4s");
+ await p;
+ info(`audio=[${audiosb.buffered.start(0)}-${audiosb.buffered.end(0)}], video=[${videosb.buffered.start(0)}-${videosb.buffered.end(0)}]`);
+
+ info(`now we should be able to finish seeking to the start position`);
+ await seekedPromise;
+
+ SimpleTest.finish(SimpleTest);
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_NoVideoLoopBackData.html b/dom/media/mediasource/test/test_NoVideoLoopBackData.html
new file mode 100644
index 0000000000..407b2ecb42
--- /dev/null
+++ b/dom/media/mediasource/test/test_NoVideoLoopBackData.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: loop-back data not available yet (shorter video)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+
+/**
+ * This test is used to check whether a looping video can loop back successfully
+ * when it has a shorter video track than its audio track. When reaching EOS for
+ * the shorter track, there is no loop-back data at the start position (they are
+ * not appended yet) Even that, we should still be able to loop back but the
+ * looping would become non-seamless in this situation.
+ */
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, el) => {
+ await once(ms, "sourceopen");
+ ok(true, "Receive a sourceopen event");
+ const videosb = ms.addSourceBuffer("video/mp4");
+ const audiosb = ms.addSourceBuffer("audio/mp4");
+
+ // Here we create a way shorter video than audio because audio decoding is
+ // very fast. If two track only have small diffence in length, audio track
+ // would still reach to the end first. But in this test, we want to test
+ // reaching video EOS first.
+ info(`create different length source buffers`);
+ await fetchAndLoad(audiosb, "bipbop/bipbop_audio", ["init"], ".mp4");
+ await fetchAndLoad(audiosb, "bipbop/bipbop_audio", range(5, 8), ".m4s");
+ videosb.appendWindowEnd = audiosb.buffered.end(0) - 2.5;
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", ["init"], ".mp4");
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", range(5, 8), ".m4s");
+ ms.endOfStream();
+ await Promise.all([once(el, "durationchange"), once(ms, "sourceended")]);
+ info(`audio=[${audiosb.buffered.start(0)}-${audiosb.buffered.end(0)}], video=[${videosb.buffered.start(0)}-${videosb.buffered.end(0)}]`);
+ ok(true, `endOfStream completed, buffer=[${el.buffered.start(0)}, ${el.buffered.end(0)}]`);
+ ok(audiosb.buffered.end(0) > videosb.buffered.end(0), `audio should be longer than video`);
+
+ info(`seek to the position where buffered data exists`);
+ el.loop = true;
+ el.controls = true;
+ el.currentTime = el.buffered.start(0);
+ await el.play();
+
+ info(`video should trigger seeking when reaching to the end`);
+ let seekingCount = 0, seekedCount = 0;
+ el.addEventListener("seeking", () => {
+ is(++seekingCount, 1, "should only receive seeking once!");
+ });
+ el.addEventListener("seeked", () => {
+ is(++seekedCount, 1, "should only receive seeked once!");
+ });
+ await once(el, "seeking");
+
+ info(`trim old data before append new data`);
+ let p = Promise.all([once(videosb, "updateend"), once(audiosb, "updateend")]);
+ videosb.remove(videosb.buffered.start(0), videosb.buffered.end(0));
+ audiosb.remove(audiosb.buffered.start(0), audiosb.buffered.end(0));
+ await p;
+
+ info(`append new data`);
+ const seekedPromise = once(el, "seeked");
+ p = Promise.all([once(videosb, "updateend"), once(audiosb, "updateend")]);
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", range(1, 2), ".m4s");
+ await fetchAndLoad(audiosb, "bipbop/bipbop_audio", range(1, 2), ".m4s");
+ await p;
+ info(`audio=[${audiosb.buffered.start(0)}-${audiosb.buffered.end(0)}], video=[${videosb.buffered.start(0)}-${videosb.buffered.end(0)}]`);
+
+ info(`now we should be able to finish seeking to the start position`);
+ await seekedPromise;
+
+ SimpleTest.finish(SimpleTest);
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_OnEvents.html b/dom/media/mediasource/test/test_OnEvents.html
new file mode 100644
index 0000000000..ae0f348ebe
--- /dev/null
+++ b/dom/media/mediasource/test/test_OnEvents.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: live seekable range</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, v) => {
+ const receiveEvent = e => v["got" + e] = true;
+
+ const msevents = ["onsourceopen", "onsourceended"];
+ msevents.forEach(e => ms[e] = () => receiveEvent(e));
+
+ const sblistevents = ["onaddsourcebuffer", "onremovesourcebuffer"];
+ sblistevents.forEach(e => ms.sourceBuffers[e] = () => receiveEvent(e));
+
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/webm");
+
+ const sbevents = ["onupdatestart", "onupdate", "onupdateend", "onabort"];
+ sbevents.forEach(e => sb[e] = () => receiveEvent(e));
+
+ await fetchAndLoad(sb, "seek", [""], ".webm");
+ sb.appendBuffer(await fetchWithXHR("seek.webm"));
+ ms.removeSourceBuffer(sb); // will fire abort and removesourcebuffer
+ ms.endOfStream(); // will fire sourceended
+ await once(ms, "sourceended");
+ [...msevents, ...sbevents, ...sblistevents].forEach(e => ok(v["got" + e], "got " + e));
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_PlayEvents.html b/dom/media/mediasource/test/test_PlayEvents.html
new file mode 100644
index 0000000000..82ccaa42b5
--- /dev/null
+++ b/dom/media/mediasource/test/test_PlayEvents.html
@@ -0,0 +1,115 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: basic functionality</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// This test checks that readyState is properly set and the appropriate events are being fired accordingly:
+// 1. Load 1.6s of data and ensure that canplay event is fired.
+// 2. Load data to have a complete buffered range from 0 to duration and ensure that canplaythrough is fired.
+// 3. Seek to an area with no buffered data, and ensure that readyState goes back to HAVE_METADATA
+// 4. Load 1.6s of data at the seek position and ensure that canplay is fired and that readyState is now HAVE_FUTURE_DATA
+// 5. Start playing video and check that once it reaches a position with no data, readyState goes back to HAVE_CURRENT_DATA and waiting event is fired.
+// 6. Add 1.6s of data once video element fired waiting, that canplay is fired once readyState is HAVE_FUTURE_DATA.
+// 7. Finally load data to the end and ensure that canplaythrough is fired and that readyState is now HAVE_ENOUGH_DATA
+
+runWithMSE(async (ms, el) => {
+ el.controls = true;
+ await once(ms, "sourceopen");
+ logEvents(el);
+ ok(true, "Receive a sourceopen event");
+ const videosb = ms.addSourceBuffer("video/mp4");
+ el.addEventListener("error", e => {
+ ok(false, `should not fire ${e.type} event`);
+ SimpleTest.finish();
+ });
+ is(el.readyState, el.HAVE_NOTHING, "readyState is HAVE_NOTHING");
+ let p = once(el, "loadedmetadata");
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", ["init"], ".mp4");
+ await p;
+ ok(true, "got loadedmetadata event");
+ p = Promise.all([once(el, "loadeddata"), once(el, "canplay")]);
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", range(1, 3), ".m4s");
+ await p;
+ ok(true, "got canplay event");
+ // set element duration to 3.203333s. We do so in order to guarantee that
+ // the end of the buffered range will be equal to duration, causing
+ // canplaythrough to be fired later.
+ ms.duration = 3.203333;
+ await once(el, "durationchange");
+ ok(true, "got durationchange event");
+ // Load [0.801666, 3.203333]
+ p = once(el, "canplaythrough");
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", range(3, 5), ".m4s");
+ await p;
+ ok(true, "got canplaythrough event");
+ // set element duration to 9.203333s, this value is set to coincide with
+ // data added later (we now have an empty range from 6s to 9.203333s).
+ ms.duration = 9.203333;
+ await once(el, "durationchange");
+ ok(true, "got durationchange event");
+ // An arbitrary value, so we are guaranteed to be in a range with no data.
+ el.currentTime = 6;
+ videosb.timestampOffset = 6;
+ ok(el.seeking, "seeking started");
+ await once(el, "seeking");
+ ok(true, "got seeking event");
+ is(el.readyState, el.HAVE_METADATA, "readyState is HAVE_METADATA");
+ // Load [6+0, 6+1.601666)
+ p = Promise.all([once(el, "seeked"), once(el, "canplay")]);
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", range(1, 3), ".m4s");
+ await p;
+ ok(true, "got seeked and canplay event");
+ is(el.currentTime, 6, "seeked to 6s");
+ is(el.readyState, el.HAVE_FUTURE_DATA, "readyState is HAVE_FUTURE_DATA");
+ // Load [6+1.60166, 6+3.203333]
+ p = once(el, "canplaythrough");
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", range(3, 5), ".m4s");
+ await p;
+ ok(true, "got canplaythrough event");
+ // set element duration to 19.805s, this value is set to coincide with
+ // data added later (we now have an empty range from 15 to 19.805).
+ ms.duration = 19.805;
+ await once(el, "durationchange");
+ ok(true, "got durationchange event");
+ el.currentTime = 15;
+ videosb.timestampOffset = 15;
+ ok(el.seeking, "seeking started");
+ await once(el, "seeking");
+ ok(true, "got seeking event");
+ // Load [15+0, 15+1.601666)
+ p = once(el, "seeked");
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", range(1, 3), ".m4s");
+ await p;
+ ok(true, "got seeked event");
+ // Load [15+1.60166, 15+3.203333]
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", range(3, 5), ".m4s");
+ ok(true, "data loaded");
+ // Playback we play for a little while then stall.
+ p = Promise.all([once(el, "playing"), once(el, "waiting")]);
+ el.play();
+ await p;
+ ok(true, "got playing and waiting event");
+ // Playback has stalled, readyState is back to HAVE_CURRENT_DATA.
+ is(el.readyState, el.HAVE_CURRENT_DATA, "readyState is HAVE_CURRENT_DATA");
+ // Load [15+3.203333, 15+4.805)
+ // Our final buffered range will now be [0, 3.203333)[6, 9.203333)[15, 19.805)
+ p = Promise.all([once(el, "playing"), once(el, "canplay"), once(el, "canplaythrough")]);
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", range(5, 7), ".m4s");
+ await p;
+ ok(true, "got playing, canplay and canplaythrough event");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_PlayEventsAutoPlaying.html b/dom/media/mediasource/test/test_PlayEventsAutoPlaying.html
new file mode 100644
index 0000000000..3e395c799d
--- /dev/null
+++ b/dom/media/mediasource/test/test_PlayEventsAutoPlaying.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: basic functionality</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// This test checks that readyState is properly set and the appropriate events are being fired accordingly:
+// 1. Ensure that play/playing aren't fired before any media data been added.
+// 2. Load 1.6s of data and ensure that canplay, play and playing events are fired.
+
+runWithMSE(async (ms, el) => {
+ el.controls = true;
+ el.autoplay = true;
+ const eventCounts = { play: 0, playing: 0 };
+ await once(ms, "sourceopen");
+ logEvents(el);
+ ok(true, "Receive a sourceopen event");
+
+ const forbiddenEvents = e => {
+ ok(el.readyState >= el.HAVE_FUTURE_DATA, "Must not have received event too early");
+ is(eventCounts[e.type], 0, "event should have only be fired once");
+ eventCounts[e.type]++;
+ };
+ el.addEventListener("play", forbiddenEvents);
+ el.addEventListener("playing", forbiddenEvents);
+
+ const videosb = ms.addSourceBuffer("video/mp4");
+ el.addEventListener("error", e => {
+ ok(false, `should not fire ${e.type} event`);
+ SimpleTest.finish();
+ });
+ is(el.readyState, el.HAVE_NOTHING, "readyState is HAVE_NOTHING");
+ let p = once(el, "loadedmetadata");
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", ["init"], ".mp4");
+ await p;
+ ok(true, "got loadedmetadata event");
+ // We're only adding 1.6s worth of data, not enough for readyState to change to HAVE_ENOUGH_DATA
+ // So we end the media source so that all the playable data is available.
+ p = Promise.all(["loadeddata", "canplay", "play", "playing", "ended"].map(e => once(el, e)));
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", range(1, 3), ".m4s");
+ ms.endOfStream();
+ await p;
+ ok(true, "got all required event");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_PlayEventsAutoPlaying2.html b/dom/media/mediasource/test/test_PlayEventsAutoPlaying2.html
new file mode 100644
index 0000000000..8845a26ac4
--- /dev/null
+++ b/dom/media/mediasource/test/test_PlayEventsAutoPlaying2.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: basic functionality</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// This test checks that readyState is properly set and the appropriate events are being fired accordingly:
+// 1. Ensure that play/playing aren't fired before any media data been added.
+// 2. Load more than 10s of data and ensure that canplay, play and playing events are fired.
+
+runWithMSE(async (ms, el) => {
+ el.controls = true;
+ el.autoplay = true;
+ const eventCounts = { play: 0, playing: 0 };
+ await once(ms, "sourceopen");
+ logEvents(el);
+ ok(true, "Receive a sourceopen event");
+
+ const forbiddenEvents = e => {
+ ok(el.readyState >= el.HAVE_FUTURE_DATA, "Must not have received event too early");
+ is(eventCounts[e.type], 0, "event should have only be fired once");
+ eventCounts[e.type]++;
+ };
+ el.addEventListener("play", forbiddenEvents);
+ el.addEventListener("playing", forbiddenEvents);
+
+ const videosb = ms.addSourceBuffer("video/mp4");
+ el.addEventListener("error", e => {
+ ok(false, `should not fire ${e.type} event`);
+ SimpleTest.finish();
+ });
+ is(el.readyState, el.HAVE_NOTHING, "readyState is HAVE_NOTHING");
+ let p = once(el, "loadedmetadata");
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", ["init"], ".mp4");
+ await p;
+ ok(true, "got loadedmetadata event");
+ // We shift the timestamps slightly to create a small gaps at the start.
+ // one that should normally be ignored.
+ videosb.timestampOffset = 0.1;
+ p = Promise.all(["loadeddata", "canplay", "play", "playing"].map(e => once(el, e)));
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", range(1, 14), ".m4s");
+ await p;
+ ok(true, "got all required event");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_RemoveSourceBuffer.html b/dom/media/mediasource/test/test_RemoveSourceBuffer.html
new file mode 100644
index 0000000000..11c6a51deb
--- /dev/null
+++ b/dom/media/mediasource/test/test_RemoveSourceBuffer.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <title>MSE: check buffered status after removed all source buffer</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test"><script class="testbody" type="text/javascript">
+
+const videoURL = "seek.webm";
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async function(ms, el) {
+ info("- wait for sourceopen -");
+ await once(ms, "sourceopen");
+
+ info("- wait for fetching data -");
+ const arrayBuffer = await fetchWithXHR(videoURL);
+
+ info("- create source buffer and append data -");
+ const sourceBuffer = ms.addSourceBuffer("video/webm");
+ sourceBuffer.appendBuffer(arrayBuffer);
+ await once(sourceBuffer, "updateend");
+ is(ms.sourceBuffers.length, 1,
+ "the length of source buffers list is 1.");
+ is(ms.activeSourceBuffers.length, 1,
+ "the length of active source buffers list is 1.");
+ ok(ms.duration != 0, "duration is not 0.");
+ is(el.buffered.length, 1, "buffered range is 1.");
+
+ info("- remove source buffer from media source -");
+ ms.removeSourceBuffer(sourceBuffer);
+ await once(ms.sourceBuffers, "removesourcebuffer");
+ is(ms.sourceBuffers.length, 0, "source buffers list is empty.");
+ is(ms.activeSourceBuffers.length, 0, "active source buffers list is empty.");
+ ok(ms.duration != 0, "duration is not 0.");
+ is(el.buffered.length, 0,
+ "buffered range is empty since we don't have any source buffer.");
+
+ info("- call endOfStream -");
+ ms.endOfStream();
+ is(ms.duration, 0, "duraton is 0 since we don't have any source buffer.");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_Resolution_change_should_not_cause_video_freeze.html b/dom/media/mediasource/test/test_Resolution_change_should_not_cause_video_freeze.html
new file mode 100644
index 0000000000..640b53441e
--- /dev/null
+++ b/dom/media/mediasource/test/test_Resolution_change_should_not_cause_video_freeze.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: video resolution changes during playback should not cause video freeze (Bug 1718709)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer('video/mp4');
+ sb.appendBuffer(new Uint8Array(await fetchWithXHR("bug1718709_low_res.mp4")));
+ ok(true, "appended low resolution video");
+ sb.appendBuffer(new Uint8Array(await fetchWithXHR("bug1718709_high_res.mp4")));
+ ok(true, "appended high resolution video");
+
+ info(`start from the position which is near to the place where resolution changes`);
+ v.currentTime = 13;
+ ok(await v.play().then(_=>true,_=>false), "video started playing");
+
+ // When video resolution changes, it should not cause video freeze so we check
+ // its painted frame amount regularly to see if we stop updating video frames.
+ let lastPaintedFramesAmount = v.mozPaintedFrames;
+ const intervalHandle = setInterval(_=>{
+ ok(lastPaintedFramesAmount < v.mozPaintedFrames,
+ `painted frames keeps growing from ${lastPaintedFramesAmount} to ${v.mozPaintedFrames}`);
+ lastPaintedFramesAmount = v.mozPaintedFrames;
+ }, 1000);
+
+ // As we didn't append full video, so we will receive `waiting` event later
+ // which indicates that we can stop testing because we've finished playing
+ // the high resolution part.
+ await new Promise(r => {
+ v.onwaiting = _ => {
+ clearInterval(intervalHandle);
+ r();
+ }
+ });
+ SimpleTest.finish();
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_ResumeAfterClearing_mp4.html b/dom/media/mediasource/test/test_ResumeAfterClearing_mp4.html
new file mode 100644
index 0000000000..40e512ba12
--- /dev/null
+++ b/dom/media/mediasource/test/test_ResumeAfterClearing_mp4.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <title>MSE: Don't get stuck buffering for too long when we have frames to show</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test"><script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ ok(true, "Receive a sourceopen event");
+ ms.addEventListener("sourceopen", () => ok(false, "No more sourceopen"));
+ const sb = ms.addSourceBuffer("video/mp4");
+ ok(sb, "Create a SourceBuffer");
+ sb.addEventListener("error", e => {
+ ok(false, "Got Error: " + e);
+ SimpleTest.finish();
+ });
+ await fetchAndLoad(sb, "bipbop/bipbop", ["init"], ".mp4");
+ let p = once(v, "loadeddata");
+ await fetchAndLoad(sb, "bipbop/bipbop", range(1, 3), ".m4s");
+ await p;
+ // clear the entire sourcebuffer.
+ sb.remove(0, 5);
+ await once(sb, "updateend");
+ v.play();
+ // We have nothing to play, waiting will be fired.
+ await once(v, "waiting");
+ p = once(v, "playing");
+ await fetchAndLoad(sb, "bipbop/bipbop", range(1, 4), ".m4s");
+ await p;
+ ms.endOfStream();
+ await Promise.all([once(ms, "sourceended"), once(v, "ended")]);
+ SimpleTest.finish(SimpleTest);
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_SeekNoData_mp4.html b/dom/media/mediasource/test/test_SeekNoData_mp4.html
new file mode 100644
index 0000000000..1ea64f3fa4
--- /dev/null
+++ b/dom/media/mediasource/test/test_SeekNoData_mp4.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: basic functionality</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, el) => {
+ el.controls = true;
+ await once(ms, "sourceopen");
+ ok(true, "Receive a sourceopen event");
+ const audiosb = ms.addSourceBuffer("audio/mp4");
+ const videosb = ms.addSourceBuffer("video/mp4");
+ el.addEventListener("error", e => {
+ ok(false, `should not fire ${e.type} event`);
+ SimpleTest.finish();
+ });
+ is(el.readyState, el.HAVE_NOTHING, "readyState is HAVE_NOTHING");
+ must_not_throw(() => el.currentTime = 3, "setting currentTime is valid");
+ is(el.currentTime, 3, "currentTime is default playback start position");
+ is(el.seeking, false, "seek not started with HAVE_NOTHING");
+ await Promise.all([
+ fetchAndLoad(audiosb, "bipbop/bipbop_audio", ["init"], ".mp4"),
+ fetchAndLoad(videosb, "bipbop/bipbop_video", ["init"], ".mp4"),
+ once(el, "loadedmetadata"),
+ ]);
+ const p = once(el, "seeking");
+ el.play();
+ el.currentTime = 5;
+ is(el.readyState, el.HAVE_METADATA, "readyState is HAVE_METADATA");
+ is(el.seeking, true, "seek not started with HAVE_METADATA");
+ is(el.currentTime, 5, "currentTime is seek position");
+ await p;
+ ok(true, "Got seeking event");
+ await Promise.all([
+ once(el, "seeked"),
+ fetchAndLoad(audiosb, "bipbop/bipbop_audio", range(5, 9), ".m4s"),
+ fetchAndLoad(videosb, "bipbop/bipbop_video", range(6, 10), ".m4s"),
+ ]);
+ ok(true, "Got seeked event");
+ ok(el.currentTime >= 5, "Time >= 5");
+ ms.endOfStream();
+ await once(el, "ended");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_SeekToEnd_mp4.html b/dom/media/mediasource/test/test_SeekToEnd_mp4.html
new file mode 100644
index 0000000000..0405cb875f
--- /dev/null
+++ b/dom/media/mediasource/test/test_SeekToEnd_mp4.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: seeking to end of data with data gap.</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, el) => {
+ await once(ms, "sourceopen");
+ ok(true, "Receive a sourceopen event");
+ const videosb = ms.addSourceBuffer("video/mp4");
+ const audiosb = ms.addSourceBuffer("audio/mp4");
+
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", ["init"], ".mp4");
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", range(1, 6), ".m4s");
+ await fetchAndLoad(audiosb, "bipbop/bipbop_audio", ["init"], ".mp4");
+ is(videosb.buffered.length, 1, "continuous buffered range");
+ // Ensure we have at least 2s less audio than video.
+ audiosb.appendWindowEnd = videosb.buffered.end(0) - 2;
+ await fetchAndLoad(audiosb, "bipbop/bipbop_audio", range(1, 6), ".m4s");
+ ms.endOfStream();
+ await Promise.all([once(el, "durationchange"), once(ms, "sourceended")]);
+ ok(true, "endOfStream completed");
+ // Seek to the middle of the gap where audio is missing. As we are in readyState = ended
+ // seeking must complete.
+ el.currentTime = videosb.buffered.end(0) / 2 + audiosb.buffered.end(0) / 2;
+ ok(el.currentTime - audiosb.buffered.end(0) >= 1, "gap is big enough");
+ is(el.buffered.length, 1, "continuous buffered range");
+ is(el.buffered.end(0), videosb.buffered.end(0),
+ "buffered range end is aligned with longest track");
+ ok(el.seeking, "element is now seeking");
+ ok(el.currentTime >= el.buffered.start(0) && el.currentTime <= el.buffered.end(0),
+ "seeking time is in buffered range");
+ ok(el.currentTime > audiosb.buffered.end(0),
+ "seeking point is not buffered in audio track");
+ await once(el, "seeked");
+ ok(true, "we have successfully seeked");
+ // Now ensure that we can play to the end, even though we are missing data in one track.
+ el.play();
+ await once(el, "ended");
+ SimpleTest.finish(SimpleTest);
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_SeekToLastFrame_mp4.html b/dom/media/mediasource/test/test_SeekToLastFrame_mp4.html
new file mode 100644
index 0000000000..edbfdff0a0
--- /dev/null
+++ b/dom/media/mediasource/test/test_SeekToLastFrame_mp4.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <title>MSE: Can seek to last frame</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test"><script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, el) => {
+ el.controls = true;
+ await once(ms, "sourceopen");
+ ok(true, "Receive a sourceopen event");
+ const sb = ms.addSourceBuffer("video/mp4");
+ await fetchAndLoad(sb, "bipbop/bipbop_480_624kbps-video", ["init"], ".mp4");
+ await fetchAndLoad(sb, "bipbop/bipbop_480_624kbps-video", range(1, 3), ".m4s");
+ el.play();
+ // let seek to the last audio frame.
+ el.currentTime = 1.532517;
+ await once(el, "seeked");
+ ok(true, "seek completed");
+ ms.endOfStream();
+ await once(el, "ended");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_SeekTwice_mp4.html b/dom/media/mediasource/test/test_SeekTwice_mp4.html
new file mode 100644
index 0000000000..50ff32b1cd
--- /dev/null
+++ b/dom/media/mediasource/test/test_SeekTwice_mp4.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: basic functionality</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, el) => {
+ el.controls = true;
+ await once(ms, "sourceopen");
+ ok(true, "Receive a sourceopen event");
+ const audiosb = ms.addSourceBuffer("audio/mp4");
+ const videosb = ms.addSourceBuffer("video/mp4");
+ await fetchAndLoad(audiosb, "bipbop/bipbop_audio", ["init"], ".mp4");
+ await fetchAndLoad(audiosb, "bipbop/bipbop_audio", range(1, 5), ".m4s");
+ await fetchAndLoad(audiosb, "bipbop/bipbop_audio", range(6, 12), ".m4s");
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", ["init"], ".mp4");
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", range(1, 6), ".m4s");
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", range(7, 14), ".m4s");
+ let p = once(el, "seeking");
+ el.play();
+ el.currentTime = 4.5; // Seek to a gap in the video
+ await p;
+ ok(true, "Got seeking event");
+ p = once(el, "seeked");
+ el.currentTime = 6; // Seek past the gap.
+ await p;
+ ok(true, "Got seeked event");
+ ok(el.currentTime >= 6, "Time >= 6");
+ ms.endOfStream();
+ await once(el, "ended");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_SeekableBeforeAndAfterEndOfStream.html b/dom/media/mediasource/test/test_SeekableBeforeAndAfterEndOfStream.html
new file mode 100644
index 0000000000..c65a4aff7e
--- /dev/null
+++ b/dom/media/mediasource/test/test_SeekableBeforeAndAfterEndOfStream.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: seekable attribute after end of stream</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/webm");
+
+ const arrayBuffer = await fetchWithXHR("seek.webm");
+ info("- append first buffer -");
+ sb.appendBuffer(new Uint8Array(arrayBuffer));
+
+ info("- wait for metadata -");
+ await once(v, "loadedmetadata");
+
+ info("- wait for updateend -");
+ await once(sb, "updateend");
+
+ info("- check seekable -");
+ const target = 2;
+ ok(v.seekable.length, "Resource is seekable");
+ is(v.seekable.start(0), 0, "Seekable's start point is correct");
+ is(v.seekable.end(0), ms.duration, "Seekable's end point is correct");
+ ok(v.seekable.length &&
+ target >= v.seekable.start(0) &&
+ target < v.seekable.end(0), "Target is within seekable range");
+
+ info("- call end of stream -");
+ ms.endOfStream();
+ await once(ms, "sourceended");
+
+ info("- check seekable -");
+ ok(v.seekable.length, "Resource is seekable");
+ is(v.seekable.start(0), 0, "Seekable's start point is correct");
+ is(v.seekable.end(0), ms.duration, "Seekable's end point is correct");
+ ok(v.seekable.length &&
+ target >= v.seekable.start(0) &&
+ target < v.seekable.end(0), "Target is within seekable range");
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_SeekableBeforeAndAfterEndOfStreamSplit.html b/dom/media/mediasource/test/test_SeekableBeforeAndAfterEndOfStreamSplit.html
new file mode 100644
index 0000000000..bed2af8d48
--- /dev/null
+++ b/dom/media/mediasource/test/test_SeekableBeforeAndAfterEndOfStreamSplit.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: seekable attribute after end of stream with split appendBuffer</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/webm");
+
+ const arrayBuffer = await fetchWithXHR("seek.webm");
+ info("- append first buffer -");
+ // 25523 is the offset of the first media segment's end
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 0, 25523));
+
+ info("- wait for metadata -");
+ await once(v, "loadedmetadata");
+
+ info("- wait for updateend -");
+ await once(sb, "updateend");
+
+ info("- append second buffer -");
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 25523));
+ await once(sb, "updateend");
+
+ info("- check seekable -");
+ const target = 2;
+ ok(v.seekable.length, "Resource is seekable");
+ is(v.seekable.start(0), 0, "Seekable's start point is correct");
+ is(v.seekable.end(0), ms.duration, "Seekable's end point is correct");
+ ok(v.seekable.length &&
+ target >= v.seekable.start(0) &&
+ target < v.seekable.end(0), "Target is within seekable range");
+
+ info("- call end of stream -");
+ ms.endOfStream();
+ await once(ms, "sourceended");
+
+ info("- check seekable -");
+ ok(v.seekable.length, "Resource is seekable");
+ is(v.seekable.start(0), 0, "Seekable's start point is correct");
+ is(v.seekable.end(0), ms.duration, "Seekable's end point is correct");
+ ok(v.seekable.length &&
+ target >= v.seekable.start(0) &&
+ target < v.seekable.end(0), "Target is within seekable range");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_SeekableBeforeAndAfterEndOfStreamSplit_mp4.html b/dom/media/mediasource/test/test_SeekableBeforeAndAfterEndOfStreamSplit_mp4.html
new file mode 100644
index 0000000000..00b5f9a832
--- /dev/null
+++ b/dom/media/mediasource/test/test_SeekableBeforeAndAfterEndOfStreamSplit_mp4.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: seekable attribute after end of stream with split appendBuffer</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/mp4");
+
+ const arrayBuffer = await fetchWithXHR("bipbop/bipbop2s.mp4");
+ info("- append first buffer -");
+ // 25819 is the offset of the first media segment's end
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 0, 25819));
+
+ info("- wait for metadata -");
+ await once(v, "loadedmetadata");
+
+ info("- wait for updateend -");
+ await once(sb, "updateend");
+
+ info("- append second buffer -");
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 25819));
+ await once(sb, "updateend");
+
+ info("- check seekable -");
+ const target = 1.3;
+ ok(v.seekable.length, "Resource is seekable");
+ is(v.seekable.start(0), 0, "Seekable's start point is correct");
+ is(v.seekable.end(0), ms.duration, "Seekable's end point is correct");
+ ok(v.seekable.length &&
+ target >= v.seekable.start(0) &&
+ target < v.seekable.end(0), "Target is within seekable range");
+
+ info("- call end of stream -");
+ ms.endOfStream();
+ await once(ms, "sourceended");
+
+ info("- check seekable -");
+ ok(v.seekable.length, "Resource is seekable");
+ is(v.seekable.start(0), 0, "Seekable's start point is correct");
+ is(v.seekable.end(0), ms.duration, "Seekable's end point is correct");
+ ok(v.seekable.length &&
+ target >= v.seekable.start(0) &&
+ target < v.seekable.end(0), "Target is within seekable range");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_SeekableBeforeAndAfterEndOfStream_mp4.html b/dom/media/mediasource/test/test_SeekableBeforeAndAfterEndOfStream_mp4.html
new file mode 100644
index 0000000000..c8e53833fb
--- /dev/null
+++ b/dom/media/mediasource/test/test_SeekableBeforeAndAfterEndOfStream_mp4.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: seekable attribute after end of stream</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/mp4");
+
+ const arrayBuffer = await fetchWithXHR("bipbop/bipbop2s.mp4");
+ info("- append buffer -");
+ sb.appendBuffer(new Uint8Array(arrayBuffer));
+
+ info("- wait for metadata -");
+ await once(v, "loadedmetadata");
+
+ info("- wait for updateend -");
+ await once(sb, "updateend");
+
+ info("- check seekable -");
+ const target = 1.3;
+ ok(v.seekable.length, "Resource is seekable");
+ is(v.seekable.start(0), 0, "Seekable's start point is correct");
+ is(v.seekable.end(0), ms.duration, "Seekable's end point is correct");
+ ok(v.seekable.length &&
+ target >= v.seekable.start(0) &&
+ target < v.seekable.end(0), "Target is within seekable range");
+
+ info("- call end of stream -");
+ ms.endOfStream();
+ await once(ms, "sourceended");
+
+ info("- check seekable -");
+ ok(v.seekable.length, "Resource is seekable");
+ is(v.seekable.start(0), 0, "Seekable's start point is correct");
+ is(v.seekable.end(0), ms.duration, "Seekable's end point is correct");
+ ok(v.seekable.length &&
+ target >= v.seekable.start(0) &&
+ target < v.seekable.end(0), "Target is within seekable range");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_SeekedEvent_mp4.html b/dom/media/mediasource/test/test_SeekedEvent_mp4.html
new file mode 100644
index 0000000000..70401f1eb1
--- /dev/null
+++ b/dom/media/mediasource/test/test_SeekedEvent_mp4.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: Check that seeked event is fired prior loadeddata</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, el) => {
+ el.controls = true;
+ const events = ["seeked", "loadeddata", "playing"];
+ let eventCount = 0;
+ events.forEach(type => el.addEventListener(type,
+ () => is(events[eventCount++], type, "events must come in order")));
+ await once(ms, "sourceopen");
+ ok(true, "Receive a sourceopen event");
+ const videosb = ms.addSourceBuffer("video/mp4");
+ is(el.readyState, el.HAVE_NOTHING, "readyState is HAVE_NOTHING");
+ let p = once(el, "loadedmetadata");
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", ["init"], ".mp4");
+ await p;
+ el.play();
+ videosb.timestampOffset = 2;
+ is(el.readyState, el.HAVE_METADATA, "readyState is HAVE_METADATA");
+ // Load [2, 3.606).
+ p = once(el, "play");
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", ["1"], ".m4s");
+ await p;
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", ["2"], ".m4s");
+ // TODO: readyState should be at least HAVE_CURRENTDATA, see bug 1367993.
+ ok(el.readyState >= el.HAVE_METADATA, "readyState is HAVE_METADATA");
+ el.currentTime = 2;
+ await Promise.all([once(el, "seeked"), once(el, "playing")]);
+ ok(true, "completed seek");
+ is(eventCount, events.length, "Received expected number of events");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_Sequence_mp4.html b/dom/media/mediasource/test/test_Sequence_mp4.html
new file mode 100644
index 0000000000..5af7fe5a0b
--- /dev/null
+++ b/dom/media/mediasource/test/test_Sequence_mp4.html
@@ -0,0 +1,37 @@
+<!DOCTYPE html>
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <title>MSE: Don't get stuck buffering for too long when we have frames to show</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test"><script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ ok(true, "Receive a sourceopen event");
+ ms.addEventListener("sourceopen", () => ok(false, "No more sourceopen"));
+ const sb = ms.addSourceBuffer("video/mp4");
+ ok(sb, "Create a SourceBuffer");
+ sb.addEventListener("error", e => {
+ ok(false, "Got Error: " + e);
+ SimpleTest.finish();
+ });
+ sb.mode = "sequence";
+
+ await fetchAndLoad(sb, "bipbop/bipbop_video", ["init"], ".mp4");
+ await fetchAndLoad(sb, "bipbop/bipbop_video", ["5"], ".m4s");
+ await fetchAndLoad(sb, "bipbop/bipbop_video", ["2"], ".m4s");
+ is(v.buffered.length, 1, "Continuous buffered range");
+ is(v.buffered.start(0), 0, "Buffered range starts at 0");
+ ok(sb.timestampOffset >= 0, "SourceBuffer.timestampOffset set to allow continuous range");
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_SetModeThrows.html b/dom/media/mediasource/test/test_SetModeThrows.html
new file mode 100644
index 0000000000..c715854b41
--- /dev/null
+++ b/dom/media/mediasource/test/test_SetModeThrows.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: append initialization</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// MSE supports setting mode now. make sure it does not throw.
+runWithMSE(function(ms, v) {
+ ms.addEventListener("sourceopen", () => {
+ const sb = ms.addSourceBuffer("video/webm");
+
+ sb.mode = "segments";
+ ok("true", "Setting to segments does not throw");
+ try {
+ sb.mode = "sequence";
+ ok("true", "Setting to sequence does not throw");
+ } catch (e) { ok(false, "Should not throw setting mode to sequence: " + e); }
+
+ SimpleTest.finish();
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_SplitAppend.html b/dom/media/mediasource/test/test_SplitAppend.html
new file mode 100644
index 0000000000..a4be5de282
--- /dev/null
+++ b/dom/media/mediasource/test/test_SplitAppend.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: append initialization and media segment separately</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/webm");
+
+ const arrayBuffer = await fetchWithXHR("seek.webm");
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 0, 318));
+ v.play();
+ await once(sb, "updateend");
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 318));
+ await once(sb, "updateend");
+ ms.endOfStream();
+ await once(v, "ended");
+ // XXX: Duration should be exactly 4.0, see bug 1065207.
+ ok(Math.abs(v.duration - 4) <= 0.002, "Video has correct duration");
+ ok(Math.abs(v.currentTime - 4) <= 0.002, "Video has played to end");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_SplitAppendDelay.html b/dom/media/mediasource/test/test_SplitAppendDelay.html
new file mode 100644
index 0000000000..40183c3db0
--- /dev/null
+++ b/dom/media/mediasource/test/test_SplitAppendDelay.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: append segments with delay</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/webm");
+
+ const arrayBuffer = await fetchWithXHR("seek.webm");
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 0, 318));
+ v.play();
+ await once(sb, "updateend");
+ await wait(1000);
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 318));
+ await once(sb, "updateend");
+ ms.endOfStream();
+ await once(v, "ended");
+ // XXX: Duration should be exactly 4.0, see bug 1065207.
+ ok(Math.abs(v.duration - 4) <= 0.002, "Video has correct duration");
+ ok(Math.abs(v.currentTime - 4) <= 0.002, "Video has played to end");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_SplitAppendDelay_mp4.html b/dom/media/mediasource/test/test_SplitAppendDelay_mp4.html
new file mode 100644
index 0000000000..c072a526cf
--- /dev/null
+++ b/dom/media/mediasource/test/test_SplitAppendDelay_mp4.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: append segments with delay</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/mp4");
+
+ const arrayBuffer = await fetchWithXHR("bipbop/bipbop2s.mp4");
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 0, 1395));
+ v.play();
+ await once(sb, "updateend");
+ await wait(1000);
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 1395));
+ await once(sb, "updateend");
+ ms.endOfStream();
+ await once(v, "ended");
+ // The bipbop video doesn't start at 0. The old MSE code adjust the
+ // timestamps and ignore the audio track. The new one doesn't.
+ isfuzzy(v.duration, 1.696, 0.166, "Video has correct duration");
+ isfuzzy(v.currentTime, 1.696, 0.166, "Video has played to end");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_SplitAppend_mp4.html b/dom/media/mediasource/test/test_SplitAppend_mp4.html
new file mode 100644
index 0000000000..308fa9837d
--- /dev/null
+++ b/dom/media/mediasource/test/test_SplitAppend_mp4.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: append initialization and media segment separately</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/mp4");
+
+ const arrayBuffer = await fetchWithXHR("bipbop/bipbop2s.mp4");
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 0, 1395));
+ v.play();
+ await once(sb, "updateend");
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 1395));
+ await once(sb, "updateend");
+ ms.endOfStream();
+
+ await once(v, "ended");
+ // The bipbop video doesn't start at 0. The old MSE code adjust the
+ // timestamps and ignore the audio track. The new one doesn't.
+ isfuzzy(v.duration, 1.696, 0.166, "Video has correct duration");
+ isfuzzy(v.currentTime, 1.696, 0.166, "Video has played to end");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_Threshold_mp4.html b/dom/media/mediasource/test/test_Threshold_mp4.html
new file mode 100644
index 0000000000..c46883c93d
--- /dev/null
+++ b/dom/media/mediasource/test/test_Threshold_mp4.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: data gap detection</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, el) => {
+ const threshold = 0.5; // gap threshold in seconds.
+ const fuzz = 0.000001; // fuzz when comparing double.
+
+ await once(ms, "sourceopen");
+ ok(true, "Receive a sourceopen event");
+ const videosb = ms.addSourceBuffer("video/mp4");
+ const vchunks = [{start: 0, end: 3.203333}, { start: 3.203333, end: 6.406666}];
+
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", ["init"], ".mp4");
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", range(1, 5), ".m4s");
+ // We will insert a gap of threshold
+ videosb.timestampOffset = threshold;
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", range(5, 9), ".m4s");
+ // HTMLMediaElement fires 'waiting' if somebody invokes |play()| before the MDSM
+ // has notified it of available data. Make sure that we get 'playing' before
+ // we starting waiting for 'waiting'.
+ info("Invoking play()");
+ let p = once(el, "playing");
+ el.play();
+ await p;
+ await once(el, "waiting");
+ // We're waiting for data after the start of the last frame.
+ // 0.033333 is the duration of the last frame.
+ ok((el.currentTime >= vchunks[1].end - 0.033333 + threshold - fuzz &&
+ el.currentTime <= vchunks[1].end + threshold + fuzz),
+ `skipped the gap properly: ${el.currentTime} ${vchunks[1].end + threshold}`);
+ is(el.buffered.length, 2, "buffered range has right length");
+ // Now we test that seeking will succeed despite the gap.
+ el.currentTime = el.buffered.end(0) + (threshold / 2);
+ await once(el, "seeked");
+ // Now we test that we don't pass the gap.
+ // Clean up our sourcebuffer by removing all data.
+ videosb.timestampOffset = 0;
+ videosb.remove(0, Infinity);
+ el.currentTime = 0;
+ el.pause();
+ await once(videosb, "updateend");
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", range(1, 5), ".m4s");
+ // We will insert a gap of threshold + 1ms
+ videosb.timestampOffset = threshold + 1 / 1000;
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", range(5, 9), ".m4s");
+ info("Invoking play()");
+ p = once(el, "playing");
+ el.play();
+ await p;
+ await once(el, "waiting");
+ // We're waiting for data after the start of the last frame.
+ // 0.033333 is the duration of the last frame.
+ ok((el.currentTime >= vchunks[0].end - 0.033333 - fuzz &&
+ el.currentTime <= vchunks[0].end + fuzz),
+ `stopped at the gap properly: ${el.currentTime} ${vchunks[0].end}`);
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_TimestampOffset_mp4.html b/dom/media/mediasource/test/test_TimestampOffset_mp4.html
new file mode 100644
index 0000000000..bd08e0f36e
--- /dev/null
+++ b/dom/media/mediasource/test/test_TimestampOffset_mp4.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: basic functionality</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, el) => {
+ const eps = 0.01;
+
+ await once(ms, "sourceopen");
+ ok(true, "Receive a sourceopen event");
+ const audiosb = ms.addSourceBuffer("audio/mp4");
+ const videosb = ms.addSourceBuffer("video/mp4");
+ // We divide the video into 3 chunks:
+ // chunk 0: segments 1-4
+ // chunk 1: segments 5-8
+ // chunk 2: segments 9-13
+ // We then fill the timeline so that it seamlessly plays the chunks in order 0, 2, 1.
+ const vchunks = [{start: 0, end: 3.2033},
+ { start: 3.2033, end: 6.4066},
+ { start: 6.4066, end: 10.01}];
+ const firstvoffset = vchunks[2].end - vchunks[2].start; // Duration of chunk 2
+ const secondvoffset = -(vchunks[1].end - vchunks[1].start); // -(Duration of chunk 1)
+
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", ["init"], ".mp4");
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", range(1, 5), ".m4s");
+ is(videosb.buffered.length, 1, "No discontinuity");
+ isfuzzy(videosb.buffered.start(0), vchunks[0].start, eps, "Chunk start");
+ isfuzzy(videosb.buffered.end(0), vchunks[0].end, eps, "Chunk end");
+ videosb.timestampOffset = firstvoffset;
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", range(5, 9), ".m4s");
+ is(videosb.buffered.length, 2, "One discontinuity");
+ isfuzzy(videosb.buffered.start(0), vchunks[0].start, eps, "First Chunk start");
+ isfuzzy(videosb.buffered.end(0), vchunks[0].end, eps, "First chunk end");
+ isfuzzy(videosb.buffered.start(1), vchunks[1].start + firstvoffset, eps, "Second chunk start");
+ isfuzzy(videosb.buffered.end(1), vchunks[1].end + firstvoffset, eps, "Second chunk end");
+ videosb.timestampOffset = secondvoffset;
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", range(9, 14), ".m4s");
+ is(videosb.buffered.length, 1, "No discontinuity (end)");
+ isfuzzy(videosb.buffered.start(0), vchunks[0].start, eps, "Chunk start");
+ isfuzzy(videosb.buffered.end(0), vchunks[2].end, eps, "Chunk end");
+ audiosb.timestampOffset = 3;
+ await fetchAndLoad(audiosb, "bipbop/bipbop_audio", ["init"], ".mp4");
+ await fetchAndLoad(audiosb, "bipbop/bipbop_audio", range(1, 12), ".m4s");
+ is(audiosb.buffered.length, 1, "No audio discontinuity");
+ isfuzzy(audiosb.buffered.start(0), 3, eps, "Audio starts at 3");
+
+ // Trim the rest of the audio.
+ audiosb.remove(videosb.buffered.end(0), Infinity);
+ videosb.remove(videosb.buffered.end(0), Infinity);
+ if (audiosb.updating) {
+ await once(audiosb, "updateend");
+ }
+ if (videosb.updating) {
+ await once(videosb, "updateend");
+ }
+ info("waiting for play to complete");
+ el.play();
+ el.currentTime = el.buffered.start(0);
+ ms.endOfStream();
+ await Promise.all([once(el, "ended"), once(el, "seeked")]);
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_TruncatedDuration.html b/dom/media/mediasource/test/test_TruncatedDuration.html
new file mode 100644
index 0000000000..c80e40ac98
--- /dev/null
+++ b/dom/media/mediasource/test/test_TruncatedDuration.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: truncating the media seeks to end of media and update buffered range</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// This test append data to a mediasource and then seek to half the duration
+// of the video.
+// We then shorten the video to 1/3rd of its original size by modifying the
+// mediasource.duration attribute.
+// We ensure that the buffered range immediately reflect the truncation
+// and that we've seeked to the new end of the media as per W3C spec and
+// video.currentTime got updated.
+
+SimpleTest.waitForExplicitFinish();
+
+const round = n => Math.round(n * 1000) / 1000;
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/webm");
+
+ sb.appendBuffer(new Uint8Array(await fetchWithXHR("seek.webm")));
+ await once(sb, "updateend");
+ v.currentTime = v.duration / 2;
+ is(v.currentTime, v.duration / 2, "current time was updated");
+ ok(v.seeking, "seeking is true");
+ await once(v, "seeked");
+ const duration = round(v.duration / 3);
+ is(sb.updating, false, "sourcebuffer isn't updating");
+ sb.remove(duration, Infinity);
+ await once(sb, "updateend");
+ ms.duration = duration;
+ // frames aren't truncated, so duration may be slightly more.
+ isfuzzy(v.duration, duration, 1 / 30, "element duration was updated");
+ sb.abort(); // this shouldn't abort updating the duration (bug 1130826).
+ ok(v.seeking, "seeking is true");
+ // test playback position was updated (bug 1130839).
+ is(v.currentTime, v.duration, "current time was updated");
+ is(sb.buffered.length, 1, "One buffered range");
+ // Truncated mediasource duration will cause the video element to seek.
+ await once(v, "seeking");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_TruncatedDuration_mp4.html b/dom/media/mediasource/test/test_TruncatedDuration_mp4.html
new file mode 100644
index 0000000000..2f37150fd3
--- /dev/null
+++ b/dom/media/mediasource/test/test_TruncatedDuration_mp4.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: truncating the media seeks to end of media and update buffered range</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// This test append data to a mediasource and then seek to half the duration
+// of the video.
+// We then shorten the video to 1/3rd of its original size.
+// We ensure that the buffered range immediately reflect the truncation
+// and that we've seeked to the new end of the media as per W3C spec and
+// video.currentTime got updated.
+
+SimpleTest.waitForExplicitFinish();
+
+const round = n => Math.round(n * 1000) / 1000;
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/mp4");
+
+ sb.appendBuffer(new Uint8Array(await fetchWithXHR("bipbop/bipbop2s.mp4")));
+ await once(sb, "updateend");
+ // mp4 metadata states 10s when we only have 1.6s worth of video.
+ sb.remove(sb.buffered.end(0), Infinity);
+ await once(sb, "updateend");
+ ms.duration = sb.buffered.end(0);
+ is(v.duration, ms.duration, "current time updated with mediasource duration");
+ v.currentTime = v.duration / 2;
+ is(v.currentTime, v.duration / 2, "current time was updated");
+ ok(v.seeking, "seeking is true");
+ await once(v, "seeked");
+ const duration = round(v.duration / 3);
+ is(sb.updating, false, "sourcebuffer isn't updating");
+ sb.remove(duration, Infinity);
+ await once(sb, "updateend");
+ ms.duration = duration;
+ // frames aren't truncated, so duration may be slightly more.
+ isfuzzy(v.duration, duration, 1 / 30, "element duration was updated");
+ sb.abort(); // this shouldn't abort updating the duration (bug 1130826).
+ ok(v.seeking, "seeking is true");
+ // test playback position was updated (bug 1130839).
+ is(v.currentTime, v.duration, "current time was updated");
+ is(sb.buffered.length, 1, "One buffered range");
+ // Truncated mediasource duration will cause the video element to seek.
+ await once(v, "seeking");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_WMFUnmatchedAudioDataTime.html b/dom/media/mediasource/test/test_WMFUnmatchedAudioDataTime.html
new file mode 100644
index 0000000000..7c03214c7b
--- /dev/null
+++ b/dom/media/mediasource/test/test_WMFUnmatchedAudioDataTime.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: audio output time doesn't match the input time on WMF</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer('audio/mp4;codecs=" mp4a.40.2"');
+ sb.appendBuffer(new Uint8Array(await fetchWithXHR("wmf_mismatchedaudiotime.mp4")));
+ ok(true, "appended data");
+
+ info(`if error doesn't occur, we should be able to receive 'seeked', otherwise 'error' would be dispatched`);
+ v.currentTime = 22.05;
+ ok(await Promise.race([
+ once(v, "seeked").then(_ => true),
+ once(v, "error").then(_ => false),
+ ]), "finished seeking without any error");
+ ok(!v.error, "should not get any error");
+ SimpleTest.finish();
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_WaitingOnMissingData.html b/dom/media/mediasource/test/test_WaitingOnMissingData.html
new file mode 100644
index 0000000000..b1ad41bb37
--- /dev/null
+++ b/dom/media/mediasource/test/test_WaitingOnMissingData.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <title>MSE: |waiting| event when source data is missing</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test"><script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, el) => {
+ await once(ms, "sourceopen");
+ ok(true, "Receive a sourceopen event");
+ const sb = ms.addSourceBuffer("video/webm");
+ sb.addEventListener("error", e => {
+ ok(false, "Got Error: " + e);
+ SimpleTest.finish();
+ });
+ const arrayBuffer = await fetchWithXHR("seek.webm");
+ await loadSegment(sb, new Uint8Array(arrayBuffer, 0, 318));
+ await loadSegment(sb, new Uint8Array(arrayBuffer, 318, 25223 - 318));
+ await loadSegment(sb, new Uint8Array(arrayBuffer, 25223, 46712 - 25223));
+ /* Note - Missing |46712, 67833 - 46712| segment here */
+ await loadSegment(sb, new Uint8Array(arrayBuffer, 67833, 88966 - 67833));
+ await loadSegment(sb, new Uint8Array(arrayBuffer, 88966));
+ // HTMLMediaElement fires "waiting" if somebody invokes |play()| before the MDSM
+ // has notified it of available data. Make sure that we get "playing" before
+ // we starting waiting for "waiting".
+ info("Invoking play()");
+ let p = once(el, "playing");
+ el.play();
+ await p;
+ ok(true, "Video playing. It should play for a bit, then fire 'waiting'");
+ p = once(el, "waiting");
+ el.play();
+ await p;
+ // currentTime is based on the current video frame, so if the audio ends just before
+ // the next video frame, currentTime can be up to 1 frame's worth earlier than
+ // min(audioEnd, videoEnd).
+ // 0.0465 is the length of the last audio frame.
+ ok(el.currentTime >= (sb.buffered.end(0) - 0.0465),
+ `Got a waiting event at ${el.currentTime}`);
+ info("Loading more data");
+ p = once(el, "ended");
+ await loadSegment(sb, new Uint8Array(arrayBuffer, 46712, 67833 - 46712));
+ ms.endOfStream();
+ await p;
+ // These fuzz factors are bigger than they should be. We should investigate
+ // and fix them in bug 1137574.
+ isfuzzy(el.duration, 4.001, 0.1, "Video has correct duration: " + el.duration);
+ isfuzzy(el.currentTime, el.duration, 0.1, "Video has correct currentTime.");
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_WaitingOnMissingDataEnded_mp4.html b/dom/media/mediasource/test/test_WaitingOnMissingDataEnded_mp4.html
new file mode 100644
index 0000000000..8ca61eae7e
--- /dev/null
+++ b/dom/media/mediasource/test/test_WaitingOnMissingDataEnded_mp4.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <title>MSE: |waiting| event when source data is missing</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test"><script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, el) => {
+ el.controls = true;
+ await once(ms, "sourceopen");
+ ok(true, "Receive a sourceopen event");
+ el.addEventListener("ended", () => {
+ ok(false, "ended should never fire");
+ SimpleTest.finish();
+ });
+ const videosb = ms.addSourceBuffer("video/mp4");
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", ["init"], ".mp4");
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", range(1, 5), ".m4s");
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", range(6, 8), ".m4s");
+ is(el.buffered.length, 2, "discontinuous buffered range");
+ ms.endOfStream();
+ await Promise.all([once(el, "durationchange"), once(ms, "sourceended")]);
+ // HTMLMediaElement fires "waiting" if somebody invokes |play()| before the MDSM
+ // has notified it of available data. Make sure that we get "playing" before
+ // we starting waiting for "waiting".
+ info("Invoking play()");
+ el.play();
+ await once(el, "playing");
+ ok(true, "Video playing. It should play for a bit, then fire 'waiting'");
+ await once(el, "waiting");
+ // waiting is fired when we start to play the last frame.
+ // 0.033334 is the duration of the last frame, + 0.000001 of fuzz.
+ // the next video frame, currentTime can be up to 1 frame's worth earlier than end of video.
+ isfuzzy(el.currentTime, videosb.buffered.end(0), 0.033334, "waiting was fired on gap");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_WaitingOnMissingData_mp4.html b/dom/media/mediasource/test/test_WaitingOnMissingData_mp4.html
new file mode 100644
index 0000000000..f6768754e7
--- /dev/null
+++ b/dom/media/mediasource/test/test_WaitingOnMissingData_mp4.html
@@ -0,0 +1,61 @@
+<!DOCTYPE html>
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <title>MSE: |waiting| event when source data is missing</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test"><script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, el) => {
+ el.controls = true;
+ await once(ms, "sourceopen");
+ ok(true, "Receive a sourceopen event");
+ const audiosb = ms.addSourceBuffer("audio/mp4");
+ const videosb = ms.addSourceBuffer("video/mp4");
+ await fetchAndLoad(audiosb, "bipbop/bipbop_audio", ["init"], ".mp4");
+ await fetchAndLoad(audiosb, "bipbop/bipbop_audio", range(1, 5), ".m4s");
+ await fetchAndLoad(audiosb, "bipbop/bipbop_audio", range(6, 12), ".m4s");
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", ["init"], ".mp4");
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", range(1, 6), ".m4s");
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", range(7, 14), ".m4s");
+ // HTMLMediaElement fires "waiting" if somebody invokes |play()| before the MDSM
+ // has notified it of available data. Make sure that we get "playing" before
+ // we starting waiting for "waiting".
+ info("Invoking play()");
+ let p = once(el, "playing");
+ el.play();
+ await p;
+ ok(true, "Video playing. It should play for a bit, then fire 'waiting'");
+ p = once(el, "waiting");
+ el.play();
+ await p;
+ // currentTime is based on the current video frame, so if the audio ends just before
+ // the next video frame, currentTime can be up to 1 frame's worth earlier than
+ // min(audioEnd, videoEnd).
+ // 0.0465 is the length of the last audio frame.
+ ok(el.currentTime >= (Math.min(audiosb.buffered.end(0), videosb.buffered.end(0)) - 0.0465),
+ `Got a waiting event at ${el.currentTime}`);
+ info("Loading more data");
+ p = once(el, "ended");
+ await Promise.all([
+ fetchAndLoad(audiosb, "bipbop/bipbop_audio", [5], ".m4s"),
+ fetchAndLoad(videosb, "bipbop/bipbop_video", [6], ".m4s"),
+ ]);
+ ms.endOfStream();
+ await p;
+ // These fuzz factors are bigger than they should be. We should investigate
+ // and fix them in bug 1137574.
+ isfuzzy(el.duration, 10.1, 0.1, "Video has correct duration: " + el.duration);
+ isfuzzy(el.currentTime, el.duration, 0.1, "Video has correct currentTime.");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_WaitingToEndedTransition_mp4.html b/dom/media/mediasource/test/test_WaitingToEndedTransition_mp4.html
new file mode 100644
index 0000000000..9c3fc73161
--- /dev/null
+++ b/dom/media/mediasource/test/test_WaitingToEndedTransition_mp4.html
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <title>MSE: |waiting| event when source data is missing</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test"><script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, el) => {
+ el.controls = true;
+ await once(ms, "sourceopen");
+ ok(true, "Receive a sourceopen event");
+ const audiosb = ms.addSourceBuffer("audio/mp4");
+ const videosb = ms.addSourceBuffer("video/mp4");
+ // ensure tracks end at approximately the same time to ensure ended event is
+ // always fired (bug 1233639).
+ audiosb.appendWindowEnd = 3.9;
+ videosb.appendWindowEnd = 3.9;
+ await fetchAndLoad(audiosb, "bipbop/bipbop_audio", ["init"], ".mp4");
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", ["init"], ".mp4");
+ await fetchAndLoad(audiosb, "bipbop/bipbop_audio", range(1, 5), ".m4s");
+ await fetchAndLoad(videosb, "bipbop/bipbop_video", range(1, 6), ".m4s");
+ // HTMLMediaElement fires "waiting" if somebody invokes |play()| before the MDSM
+ // has notified it of available data. Make sure that we get "playing" before
+ // we starting waiting for "waiting".
+ info("Invoking play()");
+ let p = once(el, "playing");
+ el.play();
+ await p;
+ ok(true, "Video playing. It should play for a bit, then fire 'waiting'");
+ await once(el, "waiting");
+ p = once(el, "ended");
+ ms.endOfStream();
+ await p;
+ // Following bug 1524890, we now implement fully step 8 of the coded frame
+ // processing algorithm
+ // http://w3c.github.io/media-source/index.html#sourcebuffer-coded-frame-processing
+ // As such, duration is exactly the value of videosb.appendWindowEnd
+ is(el.duration, videosb.appendWindowEnd, "Video has correct duration: " + el.duration);
+ is(el.currentTime, el.duration, "Video has correct currentTime.");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_WebMTagsBeforeCluster.html b/dom/media/mediasource/test/test_WebMTagsBeforeCluster.html
new file mode 100644
index 0000000000..d1d45173cd
--- /dev/null
+++ b/dom/media/mediasource/test/test_WebMTagsBeforeCluster.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: WebM tags element before cluster element</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+addMSEPrefs(["media.mediasource.webm.enabled", true]);
+
+runWithMSE(async (ms, v) => {
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/webm");
+
+ const arrayBuffer = await fetchWithXHR("tags_before_cluster.webm");
+ info("- append buffer -");
+ sb.appendBuffer(new Uint8Array(arrayBuffer));
+
+ info("- wait for metadata -");
+ await once(v, "loadedmetadata");
+
+ info("- wait for updateend -");
+ await once(sb, "updateend");
+
+ info("- call end of stream -");
+ ms.endOfStream();
+ await once(ms, "sourceended");
+
+ info("- check buffered range -");
+ is(sb.buffered.length, 1, "buffered range is not empty.");
+
+ info("- video is playing -");
+ v.play();
+ await once(v, "timeupdate");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_trackidchange_mp4.html b/dom/media/mediasource/test/test_trackidchange_mp4.html
new file mode 100644
index 0000000000..fdbeece3cd
--- /dev/null
+++ b/dom/media/mediasource/test/test_trackidchange_mp4.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: test append of audio with similar init segments that have different track ids</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(async (ms, el) => {
+ el.controls = true;
+ await once(ms, "sourceopen");
+ ok(true, "Receive a sourceopen event");
+ const audiosb = ms.addSourceBuffer("audio/mp4");
+ await fetchAndLoad(audiosb, "init-trackid2", [''], ".mp4");
+ await fetchAndLoad(audiosb, "segment-2.0001", [''], ".m4s");
+ await fetchAndLoad(audiosb, "init-trackid3", [''], ".mp4");
+ await fetchAndLoad(audiosb, "segment-3.0002", [''], ".m4s");
+ is(el.buffered.length, 1, "data is buffered");
+ is(el.buffered.end(0), 8, "all data got appended");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/whitenoise-he-aac-5s.mp4 b/dom/media/mediasource/test/whitenoise-he-aac-5s.mp4
new file mode 100644
index 0000000000..db648b8229
--- /dev/null
+++ b/dom/media/mediasource/test/whitenoise-he-aac-5s.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/wmf_mismatchedaudiotime.mp4 b/dom/media/mediasource/test/wmf_mismatchedaudiotime.mp4
new file mode 100644
index 0000000000..9e179bd326
--- /dev/null
+++ b/dom/media/mediasource/test/wmf_mismatchedaudiotime.mp4
Binary files differ
diff --git a/dom/media/metrics.yaml b/dom/media/metrics.yaml
new file mode 100644
index 0000000000..bccfde4aa6
--- /dev/null
+++ b/dom/media/metrics.yaml
@@ -0,0 +1,51 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Adding a new metric? We have docs for that!
+# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html
+
+---
+$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
+$tags:
+ - 'Core :: Audio/Video'
+
+gmp:
+ update_xml_fetch_result:
+ type: labeled_counter
+ labels:
+ - cert_pin_success
+ - cert_pin_net_request_error
+ - cert_pin_net_timeout
+ - cert_pin_abort
+ - cert_pin_missing_data
+ - cert_pin_failed
+ - cert_pin_invalid
+ - cert_pin_xml_parse_error
+ - cert_pin_unknown_error
+ - content_sig_success
+ - content_sig_net_request_error
+ - content_sig_net_timeout
+ - content_sig_abort
+ - content_sig_missing_data
+ - content_sig_failed
+ - content_sig_invalid
+ - content_sig_xml_parse_error
+ - content_sig_unknown_error
+ description: >
+ The result of Gecko fetching an update.xml from Balrog.
+ This captures 3 different data points: success or failure of the request,
+ if cert pinning or content signatures were used to verify the result, and
+ the reason for failure, if the request failed.
+ metadata:
+ tags:
+ - 'Core :: Audio/Video: GMP'
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1739664
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1739664
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - media-alerts@mozilla.com
+ expires: never
diff --git a/dom/media/moz.build b/dom/media/moz.build
new file mode 100644
index 0000000000..6e9f9eb205
--- /dev/null
+++ b/dom/media/moz.build
@@ -0,0 +1,416 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+include("/dom/media/webrtc/third_party_build/webrtc.mozbuild")
+
+with Files("*"):
+ BUG_COMPONENT = ("Core", "Audio/Video")
+
+with Files("test/**"):
+ BUG_COMPONENT = ("Core", "Audio/Video: Playback")
+
+with Files("gtest/TestGMP*"):
+ BUG_COMPONENT = ("Core", "Audio/Video: GMP")
+
+with Files("tests/**"):
+ BUG_COMPONENT = ("Core", "WebRTC")
+
+component_signaling = ("Core", "WebRTC: Signaling")
+with Files("IdpSandbox.sys.mjs"):
+ BUG_COMPONENT = component_signaling
+with Files("PeerConnection*"):
+ BUG_COMPONENT = component_signaling
+
+component_av = ("Core", "WebRTC: Audio/Video")
+with Files("GetUserMedia*"):
+ BUG_COMPONENT = component_av
+
+DIRS += [
+ "autoplay",
+ "doctor",
+ "eme",
+ "encoder",
+ "fake-cdm",
+ "flac",
+ "gmp",
+ "gmp-plugin-openh264",
+ "imagecapture",
+ "ipc",
+ "mediacapabilities",
+ "mediacontrol",
+ "mediasink",
+ "mediasource",
+ "mediasession",
+ "mp3",
+ "ogg",
+ "platforms",
+ "systemservices",
+ "utils",
+ "wave",
+ "webaudio",
+ "webcodecs",
+ "webm",
+ "webrtc",
+ "webspeech",
+ "webvtt",
+]
+
+if CONFIG["MOZ_ANDROID_HLS_SUPPORT"]:
+ DIRS += ["hls"]
+
+if CONFIG["MOZ_FMP4"]:
+ DIRS += ["mp4"]
+
+if CONFIG["MOZ_WEBRTC"]:
+ DIRS += ["bridge"]
+
+TEST_DIRS += [
+ "gtest",
+]
+
+# Add libFuzzer configuration directives
+include("/tools/fuzzing/libfuzzer-config.mozbuild")
+
+if CONFIG["FUZZING_INTERFACES"]:
+ TEST_DIRS += ["fuzz"]
+
+if CONFIG["MOZ_WEBRTC_SIGNALING"]:
+ if CONFIG["FUZZING_INTERFACES"]:
+ TEST_DIRS += ["webrtc/tests/fuzztests"]
+
+MOCHITEST_MANIFESTS += [
+ "test/mochitest.ini",
+ "test/mochitest_background_video.ini",
+ "test/mochitest_bugs.ini",
+ "test/mochitest_compat.ini",
+ "test/mochitest_eme.ini",
+ "test/mochitest_media_recorder.ini",
+ "test/mochitest_seek.ini",
+ "test/mochitest_stream.ini",
+ "webrtc/tests/mochitests/identity/mochitest.ini",
+]
+
+MOCHITEST_CHROME_MANIFESTS += ["test/chrome/chrome.ini"]
+
+BROWSER_CHROME_MANIFESTS += [
+ "mediacontrol/tests/browser/browser.ini",
+ "mediasession/test/browser.ini",
+ "test/browser/browser.ini",
+ "test/browser/wmfme/browser.ini",
+]
+
+if CONFIG["MOZ_WEBRTC"]:
+ MOCHITEST_MANIFESTS += [
+ "webrtc/tests/mochitests/mochitest.ini",
+ "webrtc/tests/mochitests/mochitest_datachannel.ini",
+ "webrtc/tests/mochitests/mochitest_getusermedia.ini",
+ "webrtc/tests/mochitests/mochitest_peerconnection.ini",
+ ]
+
+ CRASHTEST_MANIFESTS += ["webrtc/tests/crashtests/crashtests.list"]
+
+XPIDL_SOURCES += [
+ "nsIAudioDeviceInfo.idl",
+ "nsIMediaDevice.idl",
+ "nsIMediaManager.idl",
+]
+
+XPIDL_MODULE = "dom_media"
+
+EXPORTS += [
+ "../../third_party/rust/audio_thread_priority/audio_thread_priority.h",
+ "ADTSDecoder.h",
+ "ADTSDemuxer.h",
+ "AsyncLogger.h",
+ "AudibilityMonitor.h",
+ "AudioBufferUtils.h",
+ "AudioChannelFormat.h",
+ "AudioCompactor.h",
+ "AudioConfig.h",
+ "AudioConverter.h",
+ "AudioDeviceInfo.h",
+ "AudioDriftCorrection.h",
+ "AudioMixer.h",
+ "AudioPacketizer.h",
+ "AudioRingBuffer.h",
+ "AudioSampleFormat.h",
+ "AudioSegment.h",
+ "AudioStream.h",
+ "BackgroundVideoDecodingPermissionObserver.h",
+ "Benchmark.h",
+ "BitReader.h",
+ "BitWriter.h",
+ "BufferMediaResource.h",
+ "BufferReader.h",
+ "ByteWriter.h",
+ "CallbackThreadRegistry.h",
+ "ChannelMediaDecoder.h",
+ "CrossGraphPort.h",
+ "CubebUtils.h",
+ "DecoderTraits.h",
+ "DOMMediaStream.h",
+ "DriftCompensation.h",
+ "DynamicResampler.h",
+ "ExternalEngineStateMachine.h",
+ "FileBlockCache.h",
+ "ForwardedInputTrack.h",
+ "FrameStatistics.h",
+ "ImageToI420.h",
+ "Intervals.h",
+ "MediaCache.h",
+ "MediaContainerType.h",
+ "MediaData.h",
+ "MediaDataDemuxer.h",
+ "MediaDecoder.h",
+ "MediaDecoderOwner.h",
+ "MediaDecoderStateMachine.h",
+ "MediaDecoderStateMachineBase.h",
+ "MediaEventSource.h",
+ "MediaFormatReader.h",
+ "MediaInfo.h",
+ "MediaMetadataManager.h",
+ "MediaMIMETypes.h",
+ "MediaPlaybackDelayPolicy.h",
+ "MediaPromiseDefs.h",
+ "MediaQueue.h",
+ "MediaRecorder.h",
+ "MediaResource.h",
+ "MediaResourceCallback.h",
+ "MediaResult.h",
+ "MediaSegment.h",
+ "MediaShutdownManager.h",
+ "MediaSpan.h",
+ "MediaStatistics.h",
+ "MediaStreamWindowCapturer.h",
+ "MediaTimer.h",
+ "MediaTrack.h",
+ "MediaTrackGraph.h",
+ "MediaTrackList.h",
+ "MediaTrackListener.h",
+ "MemoryBlockCache.h",
+ "MPSCQueue.h",
+ "nsIDocumentActivity.h",
+ "PrincipalChangeObserver.h",
+ "PrincipalHandle.h",
+ "QueueObject.h",
+ "ReaderProxy.h",
+ "SeekJob.h",
+ "SeekTarget.h",
+ "SelfRef.h",
+ "SharedBuffer.h",
+ "TimeUnits.h",
+ "Tracing.h",
+ "VideoFrameContainer.h",
+ "VideoLimits.h",
+ "VideoSegment.h",
+ "VideoUtils.h",
+ "VorbisUtils.h",
+ "WavDumper.h",
+ "XiphExtradata.h",
+]
+
+EXPORTS.mozilla += [
+ "MediaManager.h",
+ "UnderrunHandler.h",
+]
+
+EXPORTS.mozilla.media.webrtc += [
+ "webrtc/WebrtcGlobal.h",
+ "webrtc/WebrtcIPCTraits.h",
+]
+
+if not CONFIG["MOZ_WEBRTC"]:
+ EXPORTS.transport += [
+ "webrtc/transport/runnable_utils.h",
+ ]
+
+EXPORTS.mozilla.dom += [
+ "AudioDeviceInfo.h",
+ "AudioStreamTrack.h",
+ "AudioTrack.h",
+ "AudioTrackList.h",
+ "CanvasCaptureMediaStream.h",
+ "GetUserMediaRequest.h",
+ "MediaDeviceInfo.h",
+ "MediaDevices.h",
+ "MediaStreamError.h",
+ "MediaStreamTrack.h",
+ "VideoPlaybackQuality.h",
+ "VideoStreamTrack.h",
+ "VideoTrack.h",
+ "VideoTrackList.h",
+ "webrtc/MediaTransportChild.h",
+ "webrtc/MediaTransportParent.h",
+]
+
+UNIFIED_SOURCES += [
+ "ADTSDecoder.cpp",
+ "ADTSDemuxer.cpp",
+ "AudioCaptureTrack.cpp",
+ "AudioChannelFormat.cpp",
+ "AudioCompactor.cpp",
+ "AudioConfig.cpp",
+ "AudioConverter.cpp",
+ "AudioDeviceInfo.cpp",
+ "AudioInputSource.cpp",
+ "AudioRingBuffer.cpp",
+ "AudioSegment.cpp",
+ "AudioStream.cpp",
+ "AudioStreamTrack.cpp",
+ "AudioTrack.cpp",
+ "AudioTrackList.cpp",
+ "BackgroundVideoDecodingPermissionObserver.cpp",
+ "BaseMediaResource.cpp",
+ "Benchmark.cpp",
+ "BitReader.cpp",
+ "BitWriter.cpp",
+ "CallbackThreadRegistry.cpp",
+ "CanvasCaptureMediaStream.cpp",
+ "ChannelMediaDecoder.cpp",
+ "ChannelMediaResource.cpp",
+ "CloneableWithRangeMediaResource.cpp",
+ "CrossGraphPort.cpp",
+ "CubebInputStream.cpp",
+ "DeviceInputTrack.cpp",
+ "DOMMediaStream.cpp",
+ "DynamicResampler.cpp",
+ "ExternalEngineStateMachine.cpp",
+ "FileBlockCache.cpp",
+ "FileMediaResource.cpp",
+ "ForwardedInputTrack.cpp",
+ "GetUserMediaRequest.cpp",
+ "GraphDriver.cpp",
+ "GraphRunner.cpp",
+ "ImageToI420.cpp",
+ "MediaCache.cpp",
+ "MediaContainerType.cpp",
+ "MediaDecoder.cpp",
+ "MediaDecoderStateMachine.cpp",
+ "MediaDecoderStateMachineBase.cpp",
+ "MediaDeviceInfo.cpp",
+ "MediaDevices.cpp",
+ "MediaFormatReader.cpp",
+ "MediaInfo.cpp",
+ "MediaManager.cpp",
+ "MediaMIMETypes.cpp",
+ "MediaPlaybackDelayPolicy.cpp",
+ "MediaRecorder.cpp",
+ "MediaResource.cpp",
+ "MediaShutdownManager.cpp",
+ "MediaStreamError.cpp",
+ "MediaStreamTrack.cpp",
+ "MediaStreamWindowCapturer.cpp",
+ "MediaTimer.cpp",
+ "MediaTrack.cpp",
+ "MediaTrackGraph.cpp",
+ "MediaTrackList.cpp",
+ "MediaTrackListener.cpp",
+ "MemoryBlockCache.cpp",
+ "QueueObject.cpp",
+ "ReaderProxy.cpp",
+ "SeekJob.cpp",
+ "TimeUnits.cpp",
+ "Tracing.cpp",
+ "VideoFrameContainer.cpp",
+ "VideoPlaybackQuality.cpp",
+ "VideoSegment.cpp",
+ "VideoStreamTrack.cpp",
+ "VideoTrack.cpp",
+ "VideoTrackList.cpp",
+ "VideoUtils.cpp",
+ "XiphExtradata.cpp",
+]
+
+if CONFIG["OS_TARGET"] == "Linux":
+ UNIFIED_SOURCES += ["UnderrunHandlerLinux.cpp"]
+else:
+ UNIFIED_SOURCES += ["UnderrunHandlerNoop.cpp"]
+
+# CubebUtils.cpp needs to be built separately due to what appears to be some kind
+# of compiler bug on Android 4.2 x86 opt. See bug 1408459.
+# DecoderTraits.cpp needs to be built separately because of Mac OS X headers.
+# MediaData.cpp : ambiguous compilation error caused by Mac OS X headers.
+SOURCES += [
+ "CubebUtils.cpp",
+ "DecoderTraits.cpp",
+ "MediaData.cpp",
+]
+
+# Some codec-related code uses multi-character constants, which GCC and clang
+# warn about. Suppress turning this warning into an error.
+SOURCES["DecoderTraits.cpp"].flags += ["-Wno-error=multichar"]
+
+if CONFIG["MOZ_WEBRTC"]:
+ XPCOM_MANIFESTS += [
+ "components.conf",
+ ]
+
+ EXTRA_JS_MODULES.media += [
+ "PeerConnection.sys.mjs",
+ ]
+
+EXTRA_JS_MODULES.media += [
+ "IdpSandbox.sys.mjs",
+ "PeerConnectionIdp.sys.mjs",
+]
+
+LOCAL_INCLUDES += [
+ "/caps",
+ "/docshell/base",
+ "/dom/base",
+ "/dom/media/webrtc",
+ "/layout/generic",
+ "/layout/xul",
+ "/media/libyuv/libyuv/include",
+ "/netwerk/base",
+ "/toolkit/content/tests/browser/",
+]
+
+if CONFIG["MOZ_WEBRTC"]:
+ LOCAL_INCLUDES += [
+ "/dom/media/webrtc/common",
+ "/third_party/libwebrtc",
+ "/third_party/libwebrtc/third_party/abseil-cpp",
+ ]
+
+DEFINES["MOZILLA_INTERNAL_API"] = True
+
+if CONFIG["MOZ_ANDROID_HLS_SUPPORT"]:
+ DEFINES["MOZ_ANDROID_HLS_SUPPORT"] = True
+
+if CONFIG["COMPILE_ENVIRONMENT"]:
+ EXPORTS += [
+ "!audioipc2_client_ffi_generated.h",
+ "!audioipc2_server_ffi_generated.h",
+ ]
+
+ CbindgenHeader(
+ "audioipc2_client_ffi_generated.h",
+ inputs=["/third_party/rust/audioipc2-client"],
+ )
+
+ CbindgenHeader(
+ "audioipc2_server_ffi_generated.h",
+ inputs=["/third_party/rust/audioipc2-server"],
+ )
+
+if CONFIG["ENABLE_TESTS"]:
+ DIRS += [
+ "test/rdd_process_xpcom",
+ ]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+# Suppress some GCC warnings being treated as errors:
+# - about attributes on forward declarations for types that are already
+# defined, which complains about an important MOZ_EXPORT for android::AString
+if CONFIG["CC_TYPE"] in ("clang", "gcc"):
+ CXXFLAGS += [
+ "-Wno-error=attributes",
+ ]
+
+CXXFLAGS += ["-Werror=switch"]
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/mp3/MP3Decoder.cpp b/dom/media/mp3/MP3Decoder.cpp
new file mode 100644
index 0000000000..efbe96cf4b
--- /dev/null
+++ b/dom/media/mp3/MP3Decoder.cpp
@@ -0,0 +1,45 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MP3Decoder.h"
+#include "MediaContainerType.h"
+#include "PDMFactory.h"
+
+namespace mozilla {
+
+/* static */
+bool MP3Decoder::IsEnabled() {
+ RefPtr<PDMFactory> platform = new PDMFactory();
+ return platform->SupportsMimeType("audio/mpeg"_ns) !=
+ media::DecodeSupport::Unsupported;
+}
+
+/* static */
+bool MP3Decoder::IsSupportedType(const MediaContainerType& aContainerType) {
+ if (aContainerType.Type() == MEDIAMIMETYPE("audio/mp3") ||
+ aContainerType.Type() == MEDIAMIMETYPE("audio/mpeg")) {
+ return IsEnabled() && (aContainerType.ExtendedType().Codecs().IsEmpty() ||
+ aContainerType.ExtendedType().Codecs() == "mp3");
+ }
+ return false;
+}
+
+/* static */
+nsTArray<UniquePtr<TrackInfo>> MP3Decoder::GetTracksInfo(
+ const MediaContainerType& aType) {
+ nsTArray<UniquePtr<TrackInfo>> tracks;
+ if (!IsSupportedType(aType)) {
+ return tracks;
+ }
+
+ tracks.AppendElement(
+ CreateTrackInfoWithMIMETypeAndContainerTypeExtraParameters(
+ "audio/mpeg"_ns, aType));
+
+ return tracks;
+}
+
+} // namespace mozilla
diff --git a/dom/media/mp3/MP3Decoder.h b/dom/media/mp3/MP3Decoder.h
new file mode 100644
index 0000000000..eab5387b51
--- /dev/null
+++ b/dom/media/mp3/MP3Decoder.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef MP3Decoder_h_
+#define MP3Decoder_h_
+
+#include "mozilla/UniquePtr.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+
+class MediaContainerType;
+class TrackInfo;
+
+class MP3Decoder {
+ public:
+ // Returns true if the MP3 backend is preffed on, and we're running on a
+ // platform that is likely to have decoders for the format.
+ static bool IsEnabled();
+ static bool IsSupportedType(const MediaContainerType& aContainerType);
+ static nsTArray<UniquePtr<TrackInfo>> GetTracksInfo(
+ const MediaContainerType& aType);
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/mp3/MP3Demuxer.cpp b/dom/media/mp3/MP3Demuxer.cpp
new file mode 100644
index 0000000000..25d878b3be
--- /dev/null
+++ b/dom/media/mp3/MP3Demuxer.cpp
@@ -0,0 +1,890 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MP3Demuxer.h"
+
+#include <algorithm>
+#include <inttypes.h>
+#include <limits>
+
+#include "ByteWriter.h"
+#include "TimeUnits.h"
+#include "VideoUtils.h"
+#include "mozilla/Assertions.h"
+
+extern mozilla::LazyLogModule gMediaDemuxerLog;
+#define MP3LOG(msg, ...) \
+ DDMOZ_LOG(gMediaDemuxerLog, LogLevel::Debug, msg, ##__VA_ARGS__)
+#define MP3LOGV(msg, ...) \
+ DDMOZ_LOG(gMediaDemuxerLog, LogLevel::Verbose, msg, ##__VA_ARGS__)
+
+using mozilla::media::TimeInterval;
+using mozilla::media::TimeIntervals;
+using mozilla::media::TimeUnit;
+
+namespace mozilla {
+
+// MP3Demuxer
+
+MP3Demuxer::MP3Demuxer(MediaResource* aSource) : mSource(aSource) {
+ DDLINKCHILD("source", aSource);
+}
+
+bool MP3Demuxer::InitInternal() {
+ if (!mTrackDemuxer) {
+ mTrackDemuxer = new MP3TrackDemuxer(mSource);
+ DDLINKCHILD("track demuxer", mTrackDemuxer.get());
+ }
+ return mTrackDemuxer->Init();
+}
+
+RefPtr<MP3Demuxer::InitPromise> MP3Demuxer::Init() {
+ if (!InitInternal()) {
+ MP3LOG("MP3Demuxer::Init() failure: waiting for data");
+
+ return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_METADATA_ERR,
+ __func__);
+ }
+
+ MP3LOG("MP3Demuxer::Init() successful");
+ return InitPromise::CreateAndResolve(NS_OK, __func__);
+}
+
+uint32_t MP3Demuxer::GetNumberTracks(TrackInfo::TrackType aType) const {
+ return aType == TrackInfo::kAudioTrack ? 1u : 0u;
+}
+
+already_AddRefed<MediaTrackDemuxer> MP3Demuxer::GetTrackDemuxer(
+ TrackInfo::TrackType aType, uint32_t aTrackNumber) {
+ if (!mTrackDemuxer) {
+ return nullptr;
+ }
+ return RefPtr<MP3TrackDemuxer>(mTrackDemuxer).forget();
+}
+
+bool MP3Demuxer::IsSeekable() const { return true; }
+
+void MP3Demuxer::NotifyDataArrived() {
+ // TODO: bug 1169485.
+ NS_WARNING("Unimplemented function NotifyDataArrived");
+ MP3LOGV("NotifyDataArrived()");
+}
+
+void MP3Demuxer::NotifyDataRemoved() {
+ // TODO: bug 1169485.
+ NS_WARNING("Unimplemented function NotifyDataRemoved");
+ MP3LOGV("NotifyDataRemoved()");
+}
+
+// MP3TrackDemuxer
+
+MP3TrackDemuxer::MP3TrackDemuxer(MediaResource* aSource)
+ : mSource(aSource),
+ mFrameLock(false),
+ mOffset(0),
+ mFirstFrameOffset(0),
+ mNumParsedFrames(0),
+ mFrameIndex(0),
+ mTotalFrameLen(0),
+ mSamplesPerFrame(0),
+ mSamplesPerSecond(0),
+ mChannels(0) {
+ DDLINKCHILD("source", aSource);
+ Reset();
+}
+
+bool MP3TrackDemuxer::Init() {
+ Reset();
+ FastSeek(TimeUnit());
+ // Read the first frame to fetch sample rate and other meta data.
+ RefPtr<MediaRawData> frame(GetNextFrame(FindFirstFrame()));
+
+ MP3LOG("Init StreamLength()=%" PRId64 " first-frame-found=%d", StreamLength(),
+ !!frame);
+
+ if (!frame) {
+ return false;
+ }
+
+ // Rewind back to the stream begin to avoid dropping the first frame.
+ FastSeek(TimeUnit());
+
+ if (!mInfo) {
+ mInfo = MakeUnique<AudioInfo>();
+ }
+
+ mInfo->mRate = mSamplesPerSecond;
+ mInfo->mChannels = mChannels;
+ mInfo->mBitDepth = 16;
+ mInfo->mMimeType = "audio/mpeg";
+ mInfo->mDuration = Duration().valueOr(TimeUnit::FromInfinity());
+ Mp3CodecSpecificData mp3CodecData{};
+ if (mEncoderDelay) {
+ mp3CodecData.mEncoderDelayFrames = mEncoderDelay;
+ mp3CodecData.mEncoderPaddingFrames = mEncoderPadding;
+ }
+ mInfo->mCodecSpecificConfig =
+ AudioCodecSpecificVariant{std::move(mp3CodecData)};
+
+ MP3LOG("Init mInfo={mRate=%d mChannels=%d mBitDepth=%d mDuration=%s (%lfs)}",
+ mInfo->mRate, mInfo->mChannels, mInfo->mBitDepth,
+ mInfo->mDuration.ToString().get(), mInfo->mDuration.ToSeconds());
+
+ return mSamplesPerSecond && mChannels;
+}
+
+media::TimeUnit MP3TrackDemuxer::SeekPosition() const {
+ TimeUnit pos = Duration(mFrameIndex);
+ auto duration = Duration();
+ if (duration) {
+ pos = std::min(*duration, pos);
+ }
+ return pos;
+}
+
+const FrameParser::Frame& MP3TrackDemuxer::LastFrame() const {
+ return mParser.PrevFrame();
+}
+
+RefPtr<MediaRawData> MP3TrackDemuxer::DemuxSample() {
+ return GetNextFrame(FindNextFrame());
+}
+
+const ID3Parser::ID3Header& MP3TrackDemuxer::ID3Header() const {
+ return mParser.ID3Header();
+}
+
+const FrameParser::VBRHeader& MP3TrackDemuxer::VBRInfo() const {
+ return mParser.VBRInfo();
+}
+
+UniquePtr<TrackInfo> MP3TrackDemuxer::GetInfo() const { return mInfo->Clone(); }
+
+RefPtr<MP3TrackDemuxer::SeekPromise> MP3TrackDemuxer::Seek(
+ const TimeUnit& aTime) {
+ mRemainingEncoderPadding = AssertedCast<int32_t>(mEncoderPadding);
+ // Efficiently seek to the position.
+ FastSeek(aTime);
+ // Correct seek position by scanning the next frames.
+ const TimeUnit seekTime = ScanUntil(aTime);
+
+ return SeekPromise::CreateAndResolve(seekTime, __func__);
+}
+
+TimeUnit MP3TrackDemuxer::FastSeek(const TimeUnit& aTime) {
+ MP3LOG("FastSeek(%" PRId64 ") avgFrameLen=%f mNumParsedFrames=%" PRIu64
+ " mFrameIndex=%" PRId64 " mOffset=%" PRIu64,
+ aTime.ToMicroseconds(), AverageFrameLength(), mNumParsedFrames,
+ mFrameIndex, mOffset);
+
+ const auto& vbr = mParser.VBRInfo();
+ if (aTime.IsZero()) {
+ // Quick seek to the beginning of the stream.
+ mFrameIndex = 0;
+ } else if (vbr.IsTOCPresent() && Duration() &&
+ *Duration() != TimeUnit::Zero()) {
+ // Use TOC for more precise seeking.
+ mFrameIndex = FrameIndexFromOffset(vbr.Offset(aTime, Duration().value()));
+ } else if (AverageFrameLength() > 0) {
+ mFrameIndex = FrameIndexFromTime(aTime);
+ }
+
+ mOffset = OffsetFromFrameIndex(mFrameIndex);
+
+ if (mOffset > mFirstFrameOffset && StreamLength() > 0) {
+ mOffset = std::min(StreamLength() - 1, mOffset);
+ }
+
+ mParser.EndFrameSession();
+
+ MP3LOG("FastSeek End TOC=%d avgFrameLen=%f mNumParsedFrames=%" PRIu64
+ " mFrameIndex=%" PRId64 " mFirstFrameOffset=%" PRId64
+ " mOffset=%" PRIu64 " SL=%" PRId64 " NumBytes=%u",
+ vbr.IsTOCPresent(), AverageFrameLength(), mNumParsedFrames,
+ mFrameIndex, mFirstFrameOffset, mOffset, StreamLength(),
+ vbr.NumBytes().valueOr(0));
+
+ return Duration(mFrameIndex);
+}
+
+TimeUnit MP3TrackDemuxer::ScanUntil(const TimeUnit& aTime) {
+ MP3LOG("ScanUntil(%" PRId64 ") avgFrameLen=%f mNumParsedFrames=%" PRIu64
+ " mFrameIndex=%" PRId64 " mOffset=%" PRIu64,
+ aTime.ToMicroseconds(), AverageFrameLength(), mNumParsedFrames,
+ mFrameIndex, mOffset);
+
+ if (aTime.IsZero()) {
+ return FastSeek(aTime);
+ }
+
+ if (Duration(mFrameIndex) > aTime) {
+ // We've seeked past the target time, rewind back a little to correct it.
+ const int64_t rewind = aTime.ToMicroseconds() / 100;
+ FastSeek(aTime - TimeUnit::FromMicroseconds(rewind));
+ }
+
+ if (Duration(mFrameIndex + 1) > aTime) {
+ return SeekPosition();
+ }
+
+ MediaByteRange nextRange = FindNextFrame();
+ while (SkipNextFrame(nextRange) && Duration(mFrameIndex + 1) < aTime) {
+ nextRange = FindNextFrame();
+ MP3LOGV("ScanUntil* avgFrameLen=%f mNumParsedFrames=%" PRIu64
+ " mFrameIndex=%" PRId64 " mOffset=%" PRIu64 " Duration=%" PRId64,
+ AverageFrameLength(), mNumParsedFrames, mFrameIndex, mOffset,
+ Duration(mFrameIndex + 1).ToMicroseconds());
+ }
+
+ MP3LOG("ScanUntil End avgFrameLen=%f mNumParsedFrames=%" PRIu64
+ " mFrameIndex=%" PRId64 " mOffset=%" PRIu64,
+ AverageFrameLength(), mNumParsedFrames, mFrameIndex, mOffset);
+
+ return SeekPosition();
+}
+
+RefPtr<MP3TrackDemuxer::SamplesPromise> MP3TrackDemuxer::GetSamples(
+ int32_t aNumSamples) {
+ MP3LOGV("GetSamples(%d) Begin mOffset=%" PRIu64 " mNumParsedFrames=%" PRIu64
+ " mFrameIndex=%" PRId64 " mTotalFrameLen=%" PRIu64
+ " mSamplesPerFrame=%d mSamplesPerSecond=%d mChannels=%d",
+ aNumSamples, mOffset, mNumParsedFrames, mFrameIndex, mTotalFrameLen,
+ mSamplesPerFrame, mSamplesPerSecond, mChannels);
+
+ if (!aNumSamples) {
+ return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_DEMUXER_ERR,
+ __func__);
+ }
+
+ RefPtr<SamplesHolder> frames = new SamplesHolder();
+
+ while (aNumSamples--) {
+ RefPtr<MediaRawData> frame(GetNextFrame(FindNextFrame()));
+ if (!frame) {
+ break;
+ }
+ if (!frame->HasValidTime()) {
+ return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_DEMUXER_ERR,
+ __func__);
+ }
+ frames->AppendSample(frame);
+ }
+
+ MP3LOGV("GetSamples() End mSamples.Size()=%zu aNumSamples=%d mOffset=%" PRIu64
+ " mNumParsedFrames=%" PRIu64 " mFrameIndex=%" PRId64
+ " mTotalFrameLen=%" PRIu64
+ " mSamplesPerFrame=%d mSamplesPerSecond=%d "
+ "mChannels=%d",
+ frames->GetSamples().Length(), aNumSamples, mOffset, mNumParsedFrames,
+ mFrameIndex, mTotalFrameLen, mSamplesPerFrame, mSamplesPerSecond,
+ mChannels);
+
+ if (frames->GetSamples().IsEmpty()) {
+ return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_END_OF_STREAM,
+ __func__);
+ }
+ return SamplesPromise::CreateAndResolve(frames, __func__);
+}
+
+void MP3TrackDemuxer::Reset() {
+ MP3LOG("Reset()");
+
+ FastSeek(TimeUnit());
+ mParser.Reset();
+}
+
+RefPtr<MP3TrackDemuxer::SkipAccessPointPromise>
+MP3TrackDemuxer::SkipToNextRandomAccessPoint(const TimeUnit& aTimeThreshold) {
+ // Will not be called for audio-only resources.
+ return SkipAccessPointPromise::CreateAndReject(
+ SkipFailureHolder(NS_ERROR_DOM_MEDIA_DEMUXER_ERR, 0), __func__);
+}
+
+int64_t MP3TrackDemuxer::GetResourceOffset() const { return mOffset; }
+
+TimeIntervals MP3TrackDemuxer::GetBuffered() {
+ AutoPinned<MediaResource> stream(mSource.GetResource());
+ TimeIntervals buffered;
+
+ if (Duration() && stream->IsDataCachedToEndOfResource(0)) {
+ // Special case completely cached files. This also handles local files.
+ buffered += TimeInterval(TimeUnit(), *Duration());
+ MP3LOGV("buffered = [[%" PRId64 ", %" PRId64 "]]",
+ TimeUnit().ToMicroseconds(), Duration()->ToMicroseconds());
+ return buffered;
+ }
+
+ MediaByteRangeSet ranges;
+ nsresult rv = stream->GetCachedRanges(ranges);
+ NS_ENSURE_SUCCESS(rv, buffered);
+
+ for (const auto& range : ranges) {
+ if (range.IsEmpty()) {
+ continue;
+ }
+ TimeUnit start = Duration(FrameIndexFromOffset(range.mStart));
+ TimeUnit end = Duration(FrameIndexFromOffset(range.mEnd));
+ MP3LOGV("buffered += [%" PRId64 ", %" PRId64 "]", start.ToMicroseconds(),
+ end.ToMicroseconds());
+ buffered += TimeInterval(start, end);
+ }
+
+ // If the number of frames reported by the header is valid,
+ // the duration calculated from it is the maximal duration.
+ if (ValidNumAudioFrames() && Duration()) {
+ TimeInterval duration = TimeInterval(TimeUnit(), *Duration());
+ return buffered.Intersection(duration);
+ }
+
+ return buffered;
+}
+
+int64_t MP3TrackDemuxer::StreamLength() const { return mSource.GetLength(); }
+
+media::NullableTimeUnit NothingIfNegative(TimeUnit aDuration) {
+ if (aDuration.IsNegative()) {
+ return Nothing();
+ }
+ return Some(aDuration);
+}
+
+media::NullableTimeUnit MP3TrackDemuxer::Duration() const {
+ if (!mNumParsedFrames) {
+ return Nothing();
+ }
+
+ int64_t numFrames = 0;
+ const auto numAudioFrames = ValidNumAudioFrames();
+ if (numAudioFrames) {
+ // VBR headers don't include the VBR header frame.
+ numFrames = numAudioFrames.value() + 1;
+ return NothingIfNegative(Duration(numFrames) -
+ (EncoderDelay() + Padding()));
+ }
+
+ const int64_t streamLen = StreamLength();
+ if (streamLen < 0) { // Live streams.
+ // Unknown length, we can't estimate duration.
+ return Nothing();
+ }
+ // We can't early return when streamLen < 0 before checking numAudioFrames
+ // since some live radio will give an opening remark before playing music
+ // and the duration of the opening talk can be calculated by numAudioFrames.
+
+ int64_t size = streamLen - mFirstFrameOffset;
+ MOZ_ASSERT(size);
+
+ if (mParser.ID3v1MetadataFound() && size > 128) {
+ size -= 128;
+ }
+
+ // If it's CBR, calculate the duration by bitrate.
+ if (!mParser.VBRInfo().IsValid()) {
+ const uint32_t bitrate = mParser.CurrentFrame().Header().Bitrate();
+ return NothingIfNegative(
+ media::TimeUnit::FromSeconds(static_cast<double>(size) * 8 / bitrate));
+ }
+
+ if (AverageFrameLength() > 0) {
+ numFrames = std::lround(AssertedCast<double>(size) / AverageFrameLength());
+ }
+
+ return NothingIfNegative(Duration(numFrames) - (EncoderDelay() + Padding()));
+}
+
+TimeUnit MP3TrackDemuxer::Duration(int64_t aNumFrames) const {
+ if (!mSamplesPerSecond) {
+ return TimeUnit::Invalid();
+ }
+
+ const int64_t frameCount = aNumFrames * mSamplesPerFrame;
+ return TimeUnit(frameCount, mSamplesPerSecond);
+}
+
+MediaByteRange MP3TrackDemuxer::FindFirstFrame() {
+ // We attempt to find multiple successive frames to avoid locking onto a false
+ // positive if we're fed a stream that has been cut mid-frame.
+ // For compatibility reasons we have to use the same frame count as Chrome,
+ // since some web sites actually use a file that short to test our playback
+ // capabilities.
+ static const int MIN_SUCCESSIVE_FRAMES = 3;
+ mFrameLock = false;
+
+ MediaByteRange candidateFrame = FindNextFrame();
+ int numSuccFrames = candidateFrame.Length() > 0;
+ MediaByteRange currentFrame = candidateFrame;
+ MP3LOGV("FindFirst() first candidate frame: mOffset=%" PRIu64
+ " Length()=%" PRIu64,
+ candidateFrame.mStart, candidateFrame.Length());
+
+ while (candidateFrame.Length()) {
+ mParser.EndFrameSession();
+ mOffset = currentFrame.mEnd;
+ const MediaByteRange prevFrame = currentFrame;
+
+ // FindNextFrame() here will only return frames consistent with our
+ // candidate frame.
+ currentFrame = FindNextFrame();
+ numSuccFrames += currentFrame.Length() > 0;
+ // Multiple successive false positives, which wouldn't be caught by the
+ // consistency checks alone, can be detected by wrong alignment (non-zero
+ // gap between frames).
+ const int64_t frameSeparation = currentFrame.mStart - prevFrame.mEnd;
+
+ if (!currentFrame.Length() || frameSeparation != 0) {
+ MP3LOGV(
+ "FindFirst() not enough successive frames detected, "
+ "rejecting candidate frame: successiveFrames=%d, last "
+ "Length()=%" PRIu64 ", last frameSeparation=%" PRId64,
+ numSuccFrames, currentFrame.Length(), frameSeparation);
+
+ mParser.ResetFrameData();
+ mOffset = candidateFrame.mStart + 1;
+ candidateFrame = FindNextFrame();
+ numSuccFrames = candidateFrame.Length() > 0;
+ currentFrame = candidateFrame;
+ MP3LOGV("FindFirst() new candidate frame: mOffset=%" PRIu64
+ " Length()=%" PRIu64,
+ candidateFrame.mStart, candidateFrame.Length());
+ } else if (numSuccFrames >= MIN_SUCCESSIVE_FRAMES) {
+ MP3LOG(
+ "FindFirst() accepting candidate frame: "
+ "successiveFrames=%d",
+ numSuccFrames);
+ mFrameLock = true;
+ return candidateFrame;
+ } else if (prevFrame.mStart == mParser.TotalID3HeaderSize() &&
+ currentFrame.mEnd == StreamLength()) {
+ // We accept streams with only two frames if both frames are valid. This
+ // is to handle very short files and provide parity with Chrome. See
+ // bug 1432195 for more information. This will not handle short files
+ // with a trailing tag, but as of writing we lack infrastructure to
+ // handle such tags.
+ MP3LOG(
+ "FindFirst() accepting candidate frame for short stream: "
+ "successiveFrames=%d",
+ numSuccFrames);
+ mFrameLock = true;
+ return candidateFrame;
+ }
+ }
+
+ MP3LOG("FindFirst() no suitable first frame found");
+ return candidateFrame;
+}
+
+static bool VerifyFrameConsistency(const FrameParser::Frame& aFrame1,
+ const FrameParser::Frame& aFrame2) {
+ const auto& h1 = aFrame1.Header();
+ const auto& h2 = aFrame2.Header();
+
+ return h1.IsValid() && h2.IsValid() && h1.Layer() == h2.Layer() &&
+ h1.SlotSize() == h2.SlotSize() &&
+ h1.SamplesPerFrame() == h2.SamplesPerFrame() &&
+ h1.Channels() == h2.Channels() && h1.SampleRate() == h2.SampleRate() &&
+ h1.RawVersion() == h2.RawVersion() &&
+ h1.RawProtection() == h2.RawProtection();
+}
+
+MediaByteRange MP3TrackDemuxer::FindNextFrame() {
+ static const int BUFFER_SIZE = 64;
+ static const uint32_t MAX_SKIPPABLE_BYTES = 1024 * BUFFER_SIZE;
+
+ MP3LOGV("FindNext() Begin mOffset=%" PRIu64 " mNumParsedFrames=%" PRIu64
+ " mFrameIndex=%" PRId64 " mTotalFrameLen=%" PRIu64
+ " mSamplesPerFrame=%d mSamplesPerSecond=%d mChannels=%d",
+ mOffset, mNumParsedFrames, mFrameIndex, mTotalFrameLen,
+ mSamplesPerFrame, mSamplesPerSecond, mChannels);
+
+ uint8_t buffer[BUFFER_SIZE];
+ uint32_t read = 0;
+
+ bool foundFrame = false;
+ int64_t frameHeaderOffset = 0;
+ int64_t startOffset = mOffset;
+ const bool searchingForID3 = !mParser.ID3Header().HasSizeBeenSet();
+
+ // Check whether we've found a valid MPEG frame.
+ while (!foundFrame) {
+ // How many bytes we can go without finding a valid MPEG frame
+ // (effectively rounded up to the next full buffer size multiple, as we
+ // only check this before reading the next set of data into the buffer).
+
+ // This default value of 0 will be used during testing whether we're being
+ // fed a valid stream, which shouldn't have any gaps between frames.
+ uint32_t maxSkippableBytes = 0;
+
+ if (!mParser.FirstFrame().Length()) {
+ // We're looking for the first valid frame. A well-formed file should
+ // have its first frame header right at the start (skipping an ID3 tag
+ // if necessary), but in order to support files that might have been
+ // improperly cut, we search the first few kB for a frame header.
+ maxSkippableBytes = MAX_SKIPPABLE_BYTES;
+ // Since we're counting the skipped bytes from the offset we started
+ // this parsing session with, we need to discount the ID3 tag size only
+ // if we were looking for one during the current frame parsing session.
+ if (searchingForID3) {
+ maxSkippableBytes += mParser.TotalID3HeaderSize();
+ }
+ } else if (mFrameLock) {
+ // We've found a valid MPEG stream, so don't impose any limits
+ // to allow skipping corrupted data until we hit EOS.
+ maxSkippableBytes = std::numeric_limits<uint32_t>::max();
+ }
+
+ if ((mOffset - startOffset > maxSkippableBytes) ||
+ (read = Read(buffer, mOffset, BUFFER_SIZE)) == 0) {
+ MP3LOG(
+ "FindNext() EOS or exceeded maxSkippeableBytes without a frame "
+ "(read: %d)",
+ read);
+ // This is not a valid MPEG audio stream or we've reached EOS, give up.
+ break;
+ }
+
+ BufferReader reader(buffer, read);
+ uint32_t bytesToSkip = 0;
+ auto res = mParser.Parse(&reader, &bytesToSkip);
+ foundFrame = res.unwrapOr(false);
+ int64_t readerOffset = static_cast<int64_t>(reader.Offset());
+ frameHeaderOffset = mOffset + readerOffset - FrameParser::FrameHeader::SIZE;
+
+ // If we've found neither an MPEG frame header nor an ID3v2 tag,
+ // the reader shouldn't have any bytes remaining.
+ MOZ_ASSERT(foundFrame || bytesToSkip || !reader.Remaining());
+
+ if (foundFrame && mParser.FirstFrame().Length() &&
+ !VerifyFrameConsistency(mParser.FirstFrame(), mParser.CurrentFrame())) {
+ MP3LOG("Skipping frame");
+ // We've likely hit a false-positive, ignore it and proceed with the
+ // search for the next valid frame.
+ foundFrame = false;
+ mOffset = frameHeaderOffset + 1;
+ mParser.EndFrameSession();
+ } else {
+ // Advance mOffset by the amount of bytes read and if necessary,
+ // skip an ID3v2 tag which stretches beyond the current buffer.
+ NS_ENSURE_TRUE(mOffset + read + bytesToSkip > mOffset,
+ MediaByteRange(0, 0));
+ mOffset += static_cast<int64_t>(read + bytesToSkip);
+ }
+ }
+
+ if (StreamLength() != -1) {
+ mEOS = frameHeaderOffset + mParser.CurrentFrame().Length() + BUFFER_SIZE >
+ StreamLength();
+ }
+
+ if (!foundFrame || !mParser.CurrentFrame().Length()) {
+ MP3LOG("FindNext() Exit foundFrame=%d mParser.CurrentFrame().Length()=%d ",
+ foundFrame, mParser.CurrentFrame().Length());
+ return {0, 0};
+ }
+
+ MP3LOGV("FindNext() End mOffset=%" PRIu64 " mNumParsedFrames=%" PRIu64
+ " mFrameIndex=%" PRId64 " frameHeaderOffset=%" PRId64
+ " mTotalFrameLen=%" PRIu64
+ " mSamplesPerFrame=%d mSamplesPerSecond=%d"
+ " mChannels=%d, mEOS=%s",
+ mOffset, mNumParsedFrames, mFrameIndex, frameHeaderOffset,
+ mTotalFrameLen, mSamplesPerFrame, mSamplesPerSecond, mChannels,
+ mEOS ? "true" : "false");
+
+ return {frameHeaderOffset,
+ frameHeaderOffset + mParser.CurrentFrame().Length()};
+}
+
+bool MP3TrackDemuxer::SkipNextFrame(const MediaByteRange& aRange) {
+ if (!mNumParsedFrames || !aRange.Length()) {
+ // We can't skip the first frame, since it could contain VBR headers.
+ RefPtr<MediaRawData> frame(GetNextFrame(aRange));
+ return frame;
+ }
+
+ UpdateState(aRange);
+
+ MP3LOGV("SkipNext() End mOffset=%" PRIu64 " mNumParsedFrames=%" PRIu64
+ " mFrameIndex=%" PRId64 " mTotalFrameLen=%" PRIu64
+ " mSamplesPerFrame=%d mSamplesPerSecond=%d mChannels=%d",
+ mOffset, mNumParsedFrames, mFrameIndex, mTotalFrameLen,
+ mSamplesPerFrame, mSamplesPerSecond, mChannels);
+
+ return true;
+}
+
+media::TimeUnit MP3TrackDemuxer::EncoderDelay() const {
+ return media::TimeUnit(mEncoderDelay, mSamplesPerSecond);
+}
+
+uint32_t MP3TrackDemuxer::EncoderDelayFrames() const { return mEncoderDelay; }
+
+media::TimeUnit MP3TrackDemuxer::Padding() const {
+ return media::TimeUnit(mEncoderPadding, mSamplesPerSecond);
+}
+
+uint32_t MP3TrackDemuxer::PaddingFrames() const { return mEncoderPadding; }
+
+already_AddRefed<MediaRawData> MP3TrackDemuxer::GetNextFrame(
+ const MediaByteRange& aRange) {
+ MP3LOG("GetNext() Begin({mStart=%" PRId64 " Length()=%" PRId64 "})",
+ aRange.mStart, aRange.Length());
+ if (!aRange.Length()) {
+ return nullptr;
+ }
+
+ RefPtr<MediaRawData> frame = new MediaRawData();
+ frame->mOffset = aRange.mStart;
+
+ UniquePtr<MediaRawDataWriter> frameWriter(frame->CreateWriter());
+ if (!frameWriter->SetSize(static_cast<size_t>(aRange.Length()))) {
+ MP3LOG("GetNext() Exit failed to allocated media buffer");
+ return nullptr;
+ }
+
+ const uint32_t read =
+ Read(frameWriter->Data(), frame->mOffset, frame->Size());
+
+ if (read != aRange.Length()) {
+ MP3LOG("GetNext() Exit read=%u frame->Size()=%zu", read, frame->Size());
+ return nullptr;
+ }
+
+ UpdateState(aRange);
+
+ if (mNumParsedFrames == 1) {
+ // First frame parsed, let's read VBR info if available.
+ BufferReader reader(frame->Data(), frame->Size());
+ mFirstFrameOffset = frame->mOffset;
+
+ if (mParser.ParseVBRHeader(&reader)) {
+ // Parsing was successful
+ if (mParser.VBRInfo().Type() == FrameParser::VBRHeader::XING) {
+ MP3LOG("XING header present, skipping encoder delay (%u frames)",
+ mParser.VBRInfo().EncoderDelay());
+ mEncoderDelay = mParser.VBRInfo().EncoderDelay();
+ mEncoderPadding = mParser.VBRInfo().EncoderPadding();
+ // Padding is encoded as a 12-bit unsigned number so this is fine.
+ mRemainingEncoderPadding = AssertedCast<int32_t>(mEncoderPadding);
+ if (mEncoderDelay == 0) {
+ // Skip the VBR frame + the decoder delay, that is always 529 frames
+ // in practice for the decoder we're using.
+ mEncoderDelay = mSamplesPerFrame + 529;
+ MP3LOG(
+ "No explicit delay present in vbr header, delay is assumed to be "
+ "%u frames\n",
+ mEncoderDelay);
+ }
+ } else if (mParser.VBRInfo().Type() == FrameParser::VBRHeader::VBRI) {
+ MP3LOG("VBRI header present, skipping encoder delay (%u frames)",
+ mParser.VBRInfo().EncoderDelay());
+ mEncoderDelay = mParser.VBRInfo().EncoderDelay();
+ }
+ }
+ }
+
+ TimeUnit rawPts = Duration(mFrameIndex - 1) - EncoderDelay();
+ TimeUnit rawDuration = Duration(1);
+ TimeUnit rawEnd = rawPts + rawDuration;
+
+ frame->mTime = std::max(TimeUnit::Zero(mSamplesPerSecond), rawPts);
+
+ frame->mDuration = Duration(1);
+ frame->mTimecode = frame->mTime;
+ frame->mKeyframe = true;
+ frame->mEOS = mEOS;
+
+ // Handle decoder delay. A packet must be trimmed if its pts, adjusted for
+ // decoder delay, is negative. A packet can be trimmed entirely.
+ if (rawPts.IsNegative()) {
+ frame->mDuration =
+ std::max(TimeUnit::Zero(mSamplesPerSecond), rawEnd - frame->mTime);
+ }
+
+ // It's possible to create an mp3 file that has a padding value that somehow
+ // spans multiple packets. In that case the duration is probably known,
+ // because it's probably a VBR file with a XING header (that has a duration
+ // field). Use the duration to be able to set the correct duration on
+ // packets that aren't the last one.
+ // For most files, the padding is less than a packet, it's simply substracted.
+ if (mParser.VBRInfo().Type() == FrameParser::VBRHeader::XING &&
+ mRemainingEncoderPadding > 0 &&
+ frame->GetEndTime() > Duration().valueOr(TimeUnit::FromInfinity())) {
+ TimeUnit duration = Duration().value();
+ TimeUnit inPaddingZone = frame->GetEndTime() - duration;
+ TimeUnit originalEnd = frame->GetEndTime();
+ TimeUnit originalPts = frame->mTime;
+ frame->mDuration -= inPaddingZone;
+ // Packet is entirely padding and will be completely discarded by the
+ // decoder.
+ if (frame->mDuration.IsNegative()) {
+ frame->mDuration = TimeUnit::Zero(mSamplesPerSecond);
+ }
+ int32_t paddingFrames =
+ AssertedCast<int32_t>(inPaddingZone.ToTicksAtRate(mSamplesPerSecond));
+ if (mRemainingEncoderPadding >= paddingFrames) {
+ mRemainingEncoderPadding -= paddingFrames;
+ } else {
+ mRemainingEncoderPadding = 0;
+ }
+ MP3LOG("Trimming [%s, %s] to [%s,%s] (padding) (stream duration: %s)",
+ originalPts.ToString().get(), originalEnd.ToString().get(),
+ frame->mTime.ToString().get(), frame->GetEndTime().ToString().get(),
+ duration.ToString().get());
+ } else if (frame->mEOS &&
+ mRemainingEncoderPadding <=
+ frame->mDuration.ToTicksAtRate(mSamplesPerSecond)) {
+ frame->mDuration -= TimeUnit(mRemainingEncoderPadding, mSamplesPerSecond);
+ MOZ_ASSERT(frame->mDuration.IsPositiveOrZero());
+ MP3LOG("Trimming last packet %s to [%s,%s]", Padding().ToString().get(),
+ frame->mTime.ToString().get(), frame->GetEndTime().ToString().get());
+ }
+
+ MP3LOGV("GetNext() End mOffset=%" PRIu64 " mNumParsedFrames=%" PRIu64
+ " mFrameIndex=%" PRId64 " mTotalFrameLen=%" PRIu64
+ " mSamplesPerFrame=%d mSamplesPerSecond=%d mChannels=%d, mEOS=%s",
+ mOffset, mNumParsedFrames, mFrameIndex, mTotalFrameLen,
+ mSamplesPerFrame, mSamplesPerSecond, mChannels,
+ mEOS ? "true" : "false");
+
+ // It's possible for the duration of a frame to be zero if the frame is to be
+ // trimmed entirely because it's fully comprised of decoder delay samples.
+ // This is common at the beginning of an stream.
+ MOZ_ASSERT(frame->mDuration.IsPositiveOrZero());
+
+ MP3LOG("Packet demuxed: pts [%s, %s] (duration: %s)",
+ frame->mTime.ToString().get(), frame->GetEndTime().ToString().get(),
+ frame->mDuration.ToString().get());
+
+ // Indicate original packet information to trim after decoding.
+ if (frame->mDuration != rawDuration) {
+ frame->mOriginalPresentationWindow = Some(TimeInterval{rawPts, rawEnd});
+ MP3LOG("Total packet time excluding trimming: [%s, %s]",
+ rawPts.ToString().get(), rawEnd.ToString().get());
+ }
+
+ return frame.forget();
+}
+
+int64_t MP3TrackDemuxer::OffsetFromFrameIndex(int64_t aFrameIndex) const {
+ int64_t offset = 0;
+ const auto& vbr = mParser.VBRInfo();
+
+ if (vbr.IsComplete()) {
+ offset = mFirstFrameOffset + aFrameIndex * vbr.NumBytes().value() /
+ vbr.NumAudioFrames().value();
+ } else if (AverageFrameLength() > 0) {
+ offset = mFirstFrameOffset +
+ AssertedCast<int64_t>(static_cast<float>(aFrameIndex) *
+ AverageFrameLength());
+ }
+
+ MP3LOGV("OffsetFromFrameIndex(%" PRId64 ") -> %" PRId64, aFrameIndex, offset);
+ return std::max<int64_t>(mFirstFrameOffset, offset);
+}
+
+int64_t MP3TrackDemuxer::FrameIndexFromOffset(int64_t aOffset) const {
+ int64_t frameIndex = 0;
+ const auto& vbr = mParser.VBRInfo();
+
+ if (vbr.IsComplete()) {
+ frameIndex =
+ AssertedCast<int64_t>(static_cast<float>(aOffset - mFirstFrameOffset) /
+ static_cast<float>(vbr.NumBytes().value()) *
+ static_cast<float>(vbr.NumAudioFrames().value()));
+ frameIndex = std::min<int64_t>(vbr.NumAudioFrames().value(), frameIndex);
+ } else if (AverageFrameLength() > 0) {
+ frameIndex = AssertedCast<int64_t>(
+ static_cast<float>(aOffset - mFirstFrameOffset) / AverageFrameLength());
+ }
+
+ MP3LOGV("FrameIndexFromOffset(%" PRId64 ") -> %" PRId64, aOffset, frameIndex);
+ return std::max<int64_t>(0, frameIndex);
+}
+
+int64_t MP3TrackDemuxer::FrameIndexFromTime(
+ const media::TimeUnit& aTime) const {
+ int64_t frameIndex = 0;
+ if (mSamplesPerSecond > 0 && mSamplesPerFrame > 0) {
+ frameIndex = AssertedCast<int64_t>(
+ aTime.ToSeconds() * mSamplesPerSecond / mSamplesPerFrame - 1);
+ }
+
+ MP3LOGV("FrameIndexFromOffset(%fs) -> %" PRId64, aTime.ToSeconds(),
+ frameIndex);
+ return std::max<int64_t>(0, frameIndex);
+}
+
+void MP3TrackDemuxer::UpdateState(const MediaByteRange& aRange) {
+ // Prevent overflow.
+ if (mTotalFrameLen + aRange.Length() < mTotalFrameLen) {
+ // These variables have a linear dependency and are only used to derive the
+ // average frame length.
+ mTotalFrameLen /= 2;
+ mNumParsedFrames /= 2;
+ }
+
+ // Full frame parsed, move offset to its end.
+ mOffset = aRange.mEnd;
+
+ mTotalFrameLen += aRange.Length();
+
+ if (!mSamplesPerFrame) {
+ mSamplesPerFrame = mParser.CurrentFrame().Header().SamplesPerFrame();
+ mSamplesPerSecond = mParser.CurrentFrame().Header().SampleRate();
+ mChannels = mParser.CurrentFrame().Header().Channels();
+ }
+
+ ++mNumParsedFrames;
+ ++mFrameIndex;
+ MOZ_ASSERT(mFrameIndex > 0);
+
+ // Prepare the parser for the next frame parsing session.
+ mParser.EndFrameSession();
+}
+
+uint32_t MP3TrackDemuxer::Read(uint8_t* aBuffer, int64_t aOffset,
+ uint32_t aSize) {
+ MP3LOGV("MP3TrackDemuxer::Read(%p %" PRId64 " %d)", aBuffer, aOffset, aSize);
+
+ const int64_t streamLen = StreamLength();
+ if (mInfo && streamLen > 0) {
+ // Prevent blocking reads after successful initialization.
+ int64_t max = streamLen > aOffset ? streamLen - aOffset : 0;
+ aSize = std::min<int64_t>(aSize, max);
+ }
+
+ uint32_t read = 0;
+ MP3LOGV("MP3TrackDemuxer::Read -> ReadAt(%u)", aSize);
+ const nsresult rv = mSource.ReadAt(aOffset, reinterpret_cast<char*>(aBuffer),
+ static_cast<uint32_t>(aSize), &read);
+ NS_ENSURE_SUCCESS(rv, 0);
+ return read;
+}
+
+double MP3TrackDemuxer::AverageFrameLength() const {
+ if (mNumParsedFrames) {
+ return static_cast<double>(mTotalFrameLen) /
+ static_cast<double>(mNumParsedFrames);
+ }
+ const auto& vbr = mParser.VBRInfo();
+ if (vbr.IsComplete() && vbr.NumAudioFrames().value() + 1) {
+ return static_cast<double>(vbr.NumBytes().value()) /
+ (vbr.NumAudioFrames().value() + 1);
+ }
+ return 0.0;
+}
+
+Maybe<uint32_t> MP3TrackDemuxer::ValidNumAudioFrames() const {
+ return mParser.VBRInfo().IsValid() &&
+ mParser.VBRInfo().NumAudioFrames().valueOr(0) + 1 > 1
+ ? mParser.VBRInfo().NumAudioFrames()
+ : Nothing();
+}
+
+} // namespace mozilla
+
+#undef MP3LOG
+#undef MP3LOGV
diff --git a/dom/media/mp3/MP3Demuxer.h b/dom/media/mp3/MP3Demuxer.h
new file mode 100644
index 0000000000..5189e82acf
--- /dev/null
+++ b/dom/media/mp3/MP3Demuxer.h
@@ -0,0 +1,187 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MP3_DEMUXER_H_
+#define MP3_DEMUXER_H_
+
+#include "MediaDataDemuxer.h"
+#include "MediaResource.h"
+#include "MP3FrameParser.h"
+
+namespace mozilla {
+
+class MP3TrackDemuxer;
+
+DDLoggedTypeDeclNameAndBase(MP3Demuxer, MediaDataDemuxer);
+DDLoggedTypeNameAndBase(MP3TrackDemuxer, MediaTrackDemuxer);
+
+class MP3Demuxer : public MediaDataDemuxer,
+ public DecoderDoctorLifeLogger<MP3Demuxer> {
+ public:
+ // MediaDataDemuxer interface.
+ explicit MP3Demuxer(MediaResource* aSource);
+ RefPtr<InitPromise> Init() override;
+ uint32_t GetNumberTracks(TrackInfo::TrackType aType) const override;
+ already_AddRefed<MediaTrackDemuxer> GetTrackDemuxer(
+ TrackInfo::TrackType aType, uint32_t aTrackNumber) override;
+ bool IsSeekable() const override;
+ void NotifyDataArrived() override;
+ void NotifyDataRemoved() override;
+
+ private:
+ // Synchronous initialization.
+ bool InitInternal();
+
+ RefPtr<MediaResource> mSource;
+ RefPtr<MP3TrackDemuxer> mTrackDemuxer;
+};
+
+// The MP3 demuxer used to extract MPEG frames and side information out of
+// MPEG streams.
+class MP3TrackDemuxer : public MediaTrackDemuxer,
+ public DecoderDoctorLifeLogger<MP3TrackDemuxer> {
+ public:
+ // Constructor, expecting a valid media resource.
+ explicit MP3TrackDemuxer(MediaResource* aSource);
+
+ // Initializes the track demuxer by reading the first frame for meta data.
+ // Returns initialization success state.
+ bool Init();
+
+ // Returns the total stream length if known, -1 otherwise.
+ int64_t StreamLength() const;
+
+ // Returns the estimated stream duration, or a 0-duration if unknown.
+ media::NullableTimeUnit Duration() const;
+
+ // Returns the estimated duration up to the given frame number,
+ // or a 0-duration if unknown.
+ media::TimeUnit Duration(int64_t aNumFrames) const;
+
+ // Returns the estimated current seek position time.
+ media::TimeUnit SeekPosition() const;
+
+ const FrameParser::Frame& LastFrame() const;
+ RefPtr<MediaRawData> DemuxSample();
+
+ const ID3Parser::ID3Header& ID3Header() const;
+ const FrameParser::VBRHeader& VBRInfo() const;
+
+ // MediaTrackDemuxer interface.
+ UniquePtr<TrackInfo> GetInfo() const override;
+ RefPtr<SeekPromise> Seek(const media::TimeUnit& aTime) override;
+ RefPtr<SamplesPromise> GetSamples(int32_t aNumSamples = 1) override;
+ void Reset() override;
+ RefPtr<SkipAccessPointPromise> SkipToNextRandomAccessPoint(
+ const media::TimeUnit& aTimeThreshold) override;
+ int64_t GetResourceOffset() const override;
+ media::TimeIntervals GetBuffered() override;
+ // Return the duration in frames of the encoder delay.
+ uint32_t EncoderDelayFrames() const;
+ // Return the duration in frames of the padding.
+ uint32_t PaddingFrames() const;
+
+ private:
+ // Destructor.
+ ~MP3TrackDemuxer() = default;
+
+ // Fast approximate seeking to given time.
+ media::TimeUnit FastSeek(const media::TimeUnit& aTime);
+
+ // Seeks by scanning the stream up to the given time for more accurate
+ // results.
+ media::TimeUnit ScanUntil(const media::TimeUnit& aTime);
+
+ // Finds the first valid frame and returns its byte range if found
+ // or a null-byte range otherwise.
+ MediaByteRange FindFirstFrame();
+
+ // Finds the next valid frame and returns its byte range if found
+ // or a null-byte range otherwise.
+ MediaByteRange FindNextFrame();
+
+ // Skips the next frame given the provided byte range.
+ bool SkipNextFrame(const MediaByteRange& aRange);
+
+ // Returns the next MPEG frame, if available.
+ already_AddRefed<MediaRawData> GetNextFrame(const MediaByteRange& aRange);
+
+ // Updates post-read meta data.
+ void UpdateState(const MediaByteRange& aRange);
+
+ // Returns the estimated offset for the given frame index.
+ int64_t OffsetFromFrameIndex(int64_t aFrameIndex) const;
+
+ // Returns the estimated frame index for the given offset.
+ int64_t FrameIndexFromOffset(int64_t aOffset) const;
+
+ // Returns the estimated frame index for the given time.
+ int64_t FrameIndexFromTime(const media::TimeUnit& aTime) const;
+
+ // Reads aSize bytes into aBuffer from the source starting at aOffset.
+ // Returns the actual size read.
+ uint32_t Read(uint8_t* aBuffer, int64_t aOffset, uint32_t aSize);
+
+ // Returns the average frame length derived from the previously parsed frames.
+ double AverageFrameLength() const;
+
+ // Returns the number of frames reported by the header if it's valid. Nothing
+ // otherwise.
+ Maybe<uint32_t> ValidNumAudioFrames() const;
+
+ // Return the duration of the encoder delay.
+ media::TimeUnit EncoderDelay() const;
+
+ // Return the duration of the padding.
+ media::TimeUnit Padding() const;
+
+ // The (hopefully) MPEG resource.
+ MediaResourceIndex mSource;
+
+ // MPEG frame parser used to detect frames and extract side info.
+ FrameParser mParser;
+
+ // Whether we've locked onto a valid sequence of frames or not.
+ bool mFrameLock;
+
+ // Current byte offset in the source stream.
+ int64_t mOffset;
+
+ // Byte offset of the begin of the first frame, or 0 if none parsed yet.
+ int64_t mFirstFrameOffset;
+
+ // Total parsed frames.
+ uint64_t mNumParsedFrames;
+
+ // Current frame index.
+ int64_t mFrameIndex;
+
+ // Sum of parsed frames' lengths in bytes.
+ int64_t mTotalFrameLen;
+
+ // Samples per frame metric derived from frame headers or 0 if none available.
+ uint32_t mSamplesPerFrame;
+
+ // Samples per second metric derived from frame headers or 0 if none
+ // available.
+ uint32_t mSamplesPerSecond;
+
+ // Channel count derived from frame headers or 0 if none available.
+ uint32_t mChannels;
+
+ // Audio track config info.
+ UniquePtr<AudioInfo> mInfo;
+
+ // Number of frames to skip at the beginning
+ uint32_t mEncoderDelay = 0;
+ // Number of frames to skip at the end
+ uint32_t mEncoderPadding = 0;
+ int32_t mRemainingEncoderPadding = 0;
+ // End of stream has been found
+ bool mEOS = false;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/mp3/MP3FrameParser.cpp b/dom/media/mp3/MP3FrameParser.cpp
new file mode 100644
index 0000000000..9701aa2f8f
--- /dev/null
+++ b/dom/media/mp3/MP3FrameParser.cpp
@@ -0,0 +1,817 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MP3FrameParser.h"
+
+#include <algorithm>
+#include <inttypes.h>
+
+#include "TimeUnits.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/EndianUtils.h"
+#include "mozilla/ResultExtensions.h"
+#include "mozilla/ScopeExit.h"
+#include "VideoUtils.h"
+
+extern mozilla::LazyLogModule gMediaDemuxerLog;
+#define MP3LOG(msg, ...) \
+ MOZ_LOG(gMediaDemuxerLog, LogLevel::Debug, ("MP3Demuxer " msg, ##__VA_ARGS__))
+#define MP3LOGV(msg, ...) \
+ MOZ_LOG(gMediaDemuxerLog, LogLevel::Verbose, \
+ ("MP3Demuxer " msg, ##__VA_ARGS__))
+
+namespace mozilla {
+
+// FrameParser
+
+namespace frame_header {
+// FrameHeader mRaw byte offsets.
+static const int SYNC1 = 0;
+static const int SYNC2_VERSION_LAYER_PROTECTION = 1;
+static const int BITRATE_SAMPLERATE_PADDING_PRIVATE = 2;
+static const int CHANNELMODE_MODEEXT_COPY_ORIG_EMPH = 3;
+} // namespace frame_header
+
+FrameParser::FrameParser() = default;
+
+void FrameParser::Reset() {
+ mID3Parser.Reset();
+ mFrame.Reset();
+}
+
+void FrameParser::ResetFrameData() {
+ mFrame.Reset();
+ mFirstFrame.Reset();
+ mPrevFrame.Reset();
+}
+
+void FrameParser::EndFrameSession() {
+ if (!mID3Parser.Header().IsValid()) {
+ // Reset ID3 tags only if we have not parsed a valid ID3 header yet.
+ mID3Parser.Reset();
+ }
+ mPrevFrame = mFrame;
+ mFrame.Reset();
+}
+
+const FrameParser::Frame& FrameParser::CurrentFrame() const { return mFrame; }
+
+const FrameParser::Frame& FrameParser::PrevFrame() const { return mPrevFrame; }
+
+const FrameParser::Frame& FrameParser::FirstFrame() const {
+ return mFirstFrame;
+}
+
+const ID3Parser::ID3Header& FrameParser::ID3Header() const {
+ return mID3Parser.Header();
+}
+
+uint32_t FrameParser::TotalID3HeaderSize() const {
+ uint32_t ID3v1Size = 0;
+ if (mID3v1MetadataFound) {
+ ID3v1Size = 128;
+ }
+ return ID3v1Size + mID3Parser.TotalHeadersSize();
+}
+
+const FrameParser::VBRHeader& FrameParser::VBRInfo() const {
+ return mVBRHeader;
+}
+
+Result<bool, nsresult> FrameParser::Parse(BufferReader* aReader,
+ uint32_t* aBytesToSkip) {
+ MOZ_ASSERT(aReader && aBytesToSkip);
+ *aBytesToSkip = 0;
+
+ if (ID3Parser::IsBufferStartingWithID3v1Tag(aReader)) {
+ // This is usually at the end of the file, and is always 128 bytes, that
+ // can simply be skipped.
+ aReader->Read(128);
+ *aBytesToSkip = 128;
+ mID3v1MetadataFound = true;
+ MP3LOGV("ID3v1 tag detected, skipping 128 bytes past the current buffer");
+ return false;
+ }
+
+ if (ID3Parser::IsBufferStartingWithID3Tag(aReader) && !mFirstFrame.Length()) {
+ // No MP3 frames have been parsed yet, look for ID3v2 headers at file begin.
+ // ID3v1 tags may only be at file end.
+ const size_t prevReaderOffset = aReader->Offset();
+ const uint32_t tagSize = mID3Parser.Parse(aReader);
+ if (!!tagSize) {
+ // ID3 tag found, skip past it.
+ const uint32_t skipSize = tagSize - ID3Parser::ID3Header::SIZE;
+
+ if (skipSize > aReader->Remaining()) {
+ // Skipping across the ID3v2 tag would take us past the end of the
+ // buffer, therefore we return immediately and let the calling function
+ // handle skipping the rest of the tag.
+ MP3LOGV(
+ "ID3v2 tag detected, size=%d,"
+ " needing to skip %zu bytes past the current buffer",
+ tagSize, skipSize - aReader->Remaining());
+ *aBytesToSkip = skipSize - aReader->Remaining();
+ return false;
+ }
+ MP3LOGV("ID3v2 tag detected, size=%d", tagSize);
+ aReader->Read(skipSize);
+ } else {
+ // No ID3v2 tag found, rewinding reader in order to search for a MPEG
+ // frame header.
+ aReader->Seek(prevReaderOffset);
+ }
+ }
+
+ for (auto res = aReader->ReadU8();
+ res.isOk() && !mFrame.ParseNext(res.unwrap()); res = aReader->ReadU8()) {
+ }
+
+ if (mFrame.Length()) {
+ // MP3 frame found.
+ if (!mFirstFrame.Length()) {
+ mFirstFrame = mFrame;
+ }
+ // Indicate success.
+ return true;
+ }
+ return false;
+}
+
+// FrameParser::Header
+
+FrameParser::FrameHeader::FrameHeader() { Reset(); }
+
+uint8_t FrameParser::FrameHeader::Sync1() const {
+ return mRaw[frame_header::SYNC1];
+}
+
+uint8_t FrameParser::FrameHeader::Sync2() const {
+ return 0x7 & mRaw[frame_header::SYNC2_VERSION_LAYER_PROTECTION] >> 5;
+}
+
+uint8_t FrameParser::FrameHeader::RawVersion() const {
+ return 0x3 & mRaw[frame_header::SYNC2_VERSION_LAYER_PROTECTION] >> 3;
+}
+
+uint8_t FrameParser::FrameHeader::RawLayer() const {
+ return 0x3 & mRaw[frame_header::SYNC2_VERSION_LAYER_PROTECTION] >> 1;
+}
+
+uint8_t FrameParser::FrameHeader::RawProtection() const {
+ return 0x1 & mRaw[frame_header::SYNC2_VERSION_LAYER_PROTECTION] >> 6;
+}
+
+uint8_t FrameParser::FrameHeader::RawBitrate() const {
+ return 0xF & mRaw[frame_header::BITRATE_SAMPLERATE_PADDING_PRIVATE] >> 4;
+}
+
+uint8_t FrameParser::FrameHeader::RawSampleRate() const {
+ return 0x3 & mRaw[frame_header::BITRATE_SAMPLERATE_PADDING_PRIVATE] >> 2;
+}
+
+uint8_t FrameParser::FrameHeader::Padding() const {
+ return 0x1 & mRaw[frame_header::BITRATE_SAMPLERATE_PADDING_PRIVATE] >> 1;
+}
+
+uint8_t FrameParser::FrameHeader::Private() const {
+ return 0x1 & mRaw[frame_header::BITRATE_SAMPLERATE_PADDING_PRIVATE];
+}
+
+uint8_t FrameParser::FrameHeader::RawChannelMode() const {
+ return 0x3 & mRaw[frame_header::CHANNELMODE_MODEEXT_COPY_ORIG_EMPH] >> 6;
+}
+
+uint32_t FrameParser::FrameHeader::Layer() const {
+ static const uint8_t LAYERS[4] = {0, 3, 2, 1};
+
+ return LAYERS[RawLayer()];
+}
+
+uint32_t FrameParser::FrameHeader::SampleRate() const {
+ // Sample rates - use [version][srate]
+ static const uint16_t SAMPLE_RATE[4][4] = {
+ // clang-format off
+ { 11025, 12000, 8000, 0 }, // MPEG 2.5
+ { 0, 0, 0, 0 }, // Reserved
+ { 22050, 24000, 16000, 0 }, // MPEG 2
+ { 44100, 48000, 32000, 0 } // MPEG 1
+ // clang-format on
+ };
+
+ return SAMPLE_RATE[RawVersion()][RawSampleRate()];
+}
+
+uint32_t FrameParser::FrameHeader::Channels() const {
+ // 3 is single channel (mono), any other value is some variant of dual
+ // channel.
+ return RawChannelMode() == 3 ? 1 : 2;
+}
+
+uint32_t FrameParser::FrameHeader::SamplesPerFrame() const {
+ // Samples per frame - use [version][layer]
+ static const uint16_t FRAME_SAMPLE[4][4] = {
+ // clang-format off
+ // Layer 3 2 1 Version
+ { 0, 576, 1152, 384 }, // 2.5
+ { 0, 0, 0, 0 }, // Reserved
+ { 0, 576, 1152, 384 }, // 2
+ { 0, 1152, 1152, 384 } // 1
+ // clang-format on
+ };
+
+ return FRAME_SAMPLE[RawVersion()][RawLayer()];
+}
+
+uint32_t FrameParser::FrameHeader::Bitrate() const {
+ // Bitrates - use [version][layer][bitrate]
+ static const uint16_t BITRATE[4][4][16] = {
+ // clang-format off
+ { // Version 2.5
+ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // Reserved
+ { 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0 }, // Layer 3
+ { 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0 }, // Layer 2
+ { 0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0 } // Layer 1
+ },
+ { // Reserved
+ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // Invalid
+ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // Invalid
+ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // Invalid
+ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 } // Invalid
+ },
+ { // Version 2
+ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // Reserved
+ { 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0 }, // Layer 3
+ { 0, 8, 16, 24, 32, 40, 48, 56, 64, 80, 96, 112, 128, 144, 160, 0 }, // Layer 2
+ { 0, 32, 48, 56, 64, 80, 96, 112, 128, 144, 160, 176, 192, 224, 256, 0 } // Layer 1
+ },
+ { // Version 1
+ { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, // Reserved
+ { 0, 32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 0 }, // Layer 3
+ { 0, 32, 48, 56, 64, 80, 96, 112, 128, 160, 192, 224, 256, 320, 384, 0 }, // Layer 2
+ { 0, 32, 64, 96, 128, 160, 192, 224, 256, 288, 320, 352, 384, 416, 448, 0 }, // Layer 1
+ }
+ // clang-format on
+ };
+
+ return 1000 * BITRATE[RawVersion()][RawLayer()][RawBitrate()];
+}
+
+uint32_t FrameParser::FrameHeader::SlotSize() const {
+ // Slot size (MPEG unit of measurement) - use [layer]
+ static const uint8_t SLOT_SIZE[4] = {0, 1, 1, 4}; // Rsvd, 3, 2, 1
+
+ return SLOT_SIZE[RawLayer()];
+}
+
+bool FrameParser::FrameHeader::ParseNext(uint8_t c) {
+ if (!Update(c)) {
+ Reset();
+ if (!Update(c)) {
+ Reset();
+ }
+ }
+ return IsValid();
+}
+
+bool FrameParser::ID3v1MetadataFound() const { return mID3v1MetadataFound; }
+
+bool FrameParser::FrameHeader::IsValid(int aPos) const {
+ if (aPos >= SIZE) {
+ return true;
+ }
+ if (aPos == frame_header::SYNC1) {
+ return Sync1() == 0xFF;
+ }
+ if (aPos == frame_header::SYNC2_VERSION_LAYER_PROTECTION) {
+ return Sync2() == 7 && RawVersion() != 1 && Layer() == 3;
+ }
+ if (aPos == frame_header::BITRATE_SAMPLERATE_PADDING_PRIVATE) {
+ return RawBitrate() != 0xF && RawBitrate() != 0 && RawSampleRate() != 3;
+ }
+ return true;
+}
+
+bool FrameParser::FrameHeader::IsValid() const { return mPos >= SIZE; }
+
+void FrameParser::FrameHeader::Reset() { mPos = 0; }
+
+bool FrameParser::FrameHeader::Update(uint8_t c) {
+ if (mPos < SIZE) {
+ mRaw[mPos] = c;
+ }
+ return IsValid(mPos++);
+}
+
+// FrameParser::VBRHeader
+
+namespace vbr_header {
+static const char* TYPE_STR[3] = {"NONE", "XING", "VBRI"};
+static const uint32_t TOC_SIZE = 100;
+} // namespace vbr_header
+
+FrameParser::VBRHeader::VBRHeader() : mType(NONE) {}
+
+FrameParser::VBRHeader::VBRHeaderType FrameParser::VBRHeader::Type() const {
+ return mType;
+}
+
+const Maybe<uint32_t>& FrameParser::VBRHeader::NumAudioFrames() const {
+ return mNumAudioFrames;
+}
+
+const Maybe<uint32_t>& FrameParser::VBRHeader::NumBytes() const {
+ return mNumBytes;
+}
+
+const Maybe<uint32_t>& FrameParser::VBRHeader::Scale() const { return mScale; }
+
+bool FrameParser::VBRHeader::IsTOCPresent() const {
+ // This doesn't use VBRI TOC
+ return !mTOC.empty() && mType != VBRI;
+}
+
+bool FrameParser::VBRHeader::IsValid() const { return mType != NONE; }
+
+bool FrameParser::VBRHeader::IsComplete() const {
+ return IsValid() && mNumAudioFrames.valueOr(0) > 0 && mNumBytes.valueOr(0) > 0
+ // We don't care about the scale for any computations here.
+ // && mScale < 101
+ ;
+}
+
+int64_t FrameParser::VBRHeader::Offset(media::TimeUnit aTime,
+ media::TimeUnit aDuration) const {
+ if (!IsTOCPresent()) {
+ return -1;
+ }
+
+ int64_t offset = -1;
+ if (mType == XING) {
+ // Constrain the duration percentage to [0, 99].
+ double percent = 100. * aTime.ToSeconds() / aDuration.ToSeconds();
+ const double durationPer = std::clamp(percent, 0., 99.);
+ double integer;
+ const double fractional = modf(durationPer, &integer);
+ size_t integerPer = AssertedCast<size_t>(integer);
+
+ MOZ_ASSERT(integerPer < mTOC.size());
+ offset = mTOC.at(integerPer);
+ if (fractional > 0.0 && integerPer + 1 < mTOC.size()) {
+ offset += AssertedCast<int64_t>(fractional) *
+ (mTOC.at(integerPer + 1) - offset);
+ }
+ }
+ // TODO: VBRI TOC seeking
+ MP3LOG("VBRHeader::Offset (%s): %f is at byte %" PRId64 "",
+ mType == XING ? "XING" : "VBRI", aTime.ToSeconds(), offset);
+
+ return offset;
+}
+
+Result<bool, nsresult> FrameParser::VBRHeader::ParseXing(BufferReader* aReader,
+ size_t aFrameSize) {
+ static const uint32_t XING_TAG = BigEndian::readUint32("Xing");
+ static const uint32_t INFO_TAG = BigEndian::readUint32("Info");
+ static const uint32_t LAME_TAG = BigEndian::readUint32("LAME");
+ static const uint32_t LAVC_TAG = BigEndian::readUint32("Lavc");
+
+ enum Flags {
+ NUM_FRAMES = 0x01,
+ NUM_BYTES = 0x02,
+ TOC = 0x04,
+ VBR_SCALE = 0x08
+ };
+
+ MOZ_ASSERT(aReader);
+
+ // Seek backward to the original position before leaving this scope.
+ const size_t prevReaderOffset = aReader->Offset();
+ auto scopeExit = MakeScopeExit([&] { aReader->Seek(prevReaderOffset); });
+
+ // We have to search for the Xing header as its position can change.
+ for (auto res = aReader->PeekU32();
+ res.isOk() && res.unwrap() != XING_TAG && res.unwrap() != INFO_TAG;) {
+ aReader->Read(1);
+ res = aReader->PeekU32();
+ }
+
+ // Skip across the VBR header ID tag.
+ MOZ_TRY(aReader->ReadU32());
+ mType = XING;
+
+ uint32_t flags;
+ MOZ_TRY_VAR(flags, aReader->ReadU32());
+
+ if (flags & NUM_FRAMES) {
+ uint32_t frames;
+ MOZ_TRY_VAR(frames, aReader->ReadU32());
+ mNumAudioFrames = Some(frames);
+ }
+ if (flags & NUM_BYTES) {
+ uint32_t bytes;
+ MOZ_TRY_VAR(bytes, aReader->ReadU32());
+ mNumBytes = Some(bytes);
+ }
+ if (flags & TOC && aReader->Remaining() >= vbr_header::TOC_SIZE) {
+ if (!mNumBytes) {
+ // We don't have the stream size to calculate offsets, skip the TOC.
+ aReader->Read(vbr_header::TOC_SIZE);
+ } else {
+ mTOC.clear();
+ mTOC.reserve(vbr_header::TOC_SIZE);
+ uint8_t data;
+ for (size_t i = 0; i < vbr_header::TOC_SIZE; ++i) {
+ MOZ_TRY_VAR(data, aReader->ReadU8());
+ mTOC.push_back(
+ AssertedCast<uint32_t>(1.0f / 256.0f * data * mNumBytes.value()));
+ }
+ }
+ }
+ if (flags & VBR_SCALE) {
+ uint32_t scale;
+ MOZ_TRY_VAR(scale, aReader->ReadU32());
+ mScale = Some(scale);
+ }
+
+ uint32_t lameOrLavcTag;
+ MOZ_TRY_VAR(lameOrLavcTag, aReader->ReadU32());
+
+ if (lameOrLavcTag == LAME_TAG || lameOrLavcTag == LAVC_TAG) {
+ // Skip 17 bytes after the LAME tag:
+ // - http://gabriel.mp3-tech.org/mp3infotag.html
+ // - 5 bytes for the encoder short version string
+ // - 1 byte for the info tag revision + VBR method
+ // - 1 byte for the lowpass filter value
+ // - 8 bytes for the ReplayGain information
+ // - 1 byte for the encoding flags + ATH Type
+ // - 1 byte for the specified bitrate if ABR, else the minimal bitrate
+ if (!aReader->Read(17)) {
+ return mozilla::Err(NS_ERROR_FAILURE);
+ }
+
+ // The encoder delay is three bytes, for two 12-bits integers are the
+ // encoder delay and the padding.
+ const uint8_t* delayPadding = aReader->Read(3);
+ if (!delayPadding) {
+ return mozilla::Err(NS_ERROR_FAILURE);
+ }
+ mEncoderDelay =
+ uint32_t(delayPadding[0]) << 4 | (delayPadding[1] & 0xf0) >> 4;
+ mEncoderPadding = uint32_t(delayPadding[1] & 0x0f) << 8 | delayPadding[2];
+
+ constexpr uint16_t DEFAULT_DECODER_DELAY = 529;
+ mEncoderDelay += DEFAULT_DECODER_DELAY + aFrameSize; // ignore first frame.
+ mEncoderPadding -= std::min(mEncoderPadding, DEFAULT_DECODER_DELAY);
+
+ MP3LOG("VBRHeader::ParseXing: LAME encoder delay section: delay: %" PRIu16
+ " frames, padding: %" PRIu16 " frames",
+ mEncoderDelay, mEncoderPadding);
+ }
+
+ return mType == XING;
+}
+
+template <typename T>
+int readAndConvertToInt(BufferReader* aReader) {
+ int value = AssertedCast<int>(aReader->ReadType<T>());
+ return value;
+}
+
+Result<bool, nsresult> FrameParser::VBRHeader::ParseVBRI(
+ BufferReader* aReader) {
+ static const uint32_t TAG = BigEndian::readUint32("VBRI");
+ static const uint32_t OFFSET = 32 + FrameParser::FrameHeader::SIZE;
+ static const uint32_t MIN_FRAME_SIZE = OFFSET + 26;
+
+ MOZ_ASSERT(aReader);
+ // ParseVBRI assumes that the ByteReader offset points to the beginning of a
+ // frame, therefore as a simple check, we look for the presence of a frame
+ // sync at that position.
+ auto sync = aReader->PeekU16();
+ if (sync.isOk()) { // To avoid compiler complains 'set but unused'.
+ MOZ_ASSERT((sync.unwrap() & 0xFFE0) == 0xFFE0);
+ }
+
+ // Seek backward to the original position before leaving this scope.
+ const size_t prevReaderOffset = aReader->Offset();
+ auto scopeExit = MakeScopeExit([&] { aReader->Seek(prevReaderOffset); });
+
+ // VBRI have a fixed relative position, so let's check for it there.
+ if (aReader->Remaining() > MIN_FRAME_SIZE) {
+ aReader->Seek(prevReaderOffset + OFFSET);
+ uint32_t tag;
+ MOZ_TRY_VAR(tag, aReader->ReadU32());
+ if (tag == TAG) {
+ uint16_t vbriEncoderVersion, vbriEncoderDelay, vbriQuality;
+ uint32_t vbriBytes, vbriFrames;
+ uint16_t vbriSeekOffsetsTableSize, vbriSeekOffsetsScaleFactor,
+ vbriSeekOffsetsBytesPerEntry, vbriSeekOffsetsFramesPerEntry;
+ MOZ_TRY_VAR(vbriEncoderVersion, aReader->ReadU16());
+ MOZ_TRY_VAR(vbriEncoderDelay, aReader->ReadU16());
+ MOZ_TRY_VAR(vbriQuality, aReader->ReadU16());
+ MOZ_TRY_VAR(vbriBytes, aReader->ReadU32());
+ MOZ_TRY_VAR(vbriFrames, aReader->ReadU32());
+ MOZ_TRY_VAR(vbriSeekOffsetsTableSize, aReader->ReadU16());
+ MOZ_TRY_VAR(vbriSeekOffsetsScaleFactor, aReader->ReadU32());
+ MOZ_TRY_VAR(vbriSeekOffsetsBytesPerEntry, aReader->ReadU16());
+ MOZ_TRY_VAR(vbriSeekOffsetsFramesPerEntry, aReader->ReadU16());
+
+ mTOC.reserve(vbriSeekOffsetsTableSize + 1);
+
+ int (*readFunc)(BufferReader*) = nullptr;
+ switch (vbriSeekOffsetsBytesPerEntry) {
+ case 1:
+ readFunc = &readAndConvertToInt<uint8_t>;
+ break;
+ case 2:
+ readFunc = &readAndConvertToInt<int16_t>;
+ break;
+ case 4:
+ readFunc = &readAndConvertToInt<int32_t>;
+ break;
+ case 8:
+ readFunc = &readAndConvertToInt<int64_t>;
+ break;
+ default:
+ MP3LOG("Unhandled vbriSeekOffsetsBytesPerEntry size of %hd",
+ vbriSeekOffsetsBytesPerEntry);
+ break;
+ }
+ for (uint32_t i = 0; readFunc && i < vbriSeekOffsetsTableSize; i++) {
+ int entry = readFunc(aReader);
+ mTOC.push_back(entry * vbriSeekOffsetsScaleFactor);
+ }
+ MP3LOG(
+ "Header::Parse found valid header: EncoderVersion=%hu "
+ "EncoderDelay=%hu "
+ "Quality=%hu "
+ "Bytes=%u "
+ "Frames=%u "
+ "SeekOffsetsTableSize=%u "
+ "SeekOffsetsScaleFactor=%hu "
+ "SeekOffsetsBytesPerEntry=%hu "
+ "SeekOffsetsFramesPerEntry=%hu",
+ vbriEncoderVersion, vbriEncoderDelay, vbriQuality, vbriBytes,
+ vbriFrames, vbriSeekOffsetsTableSize, vbriSeekOffsetsScaleFactor,
+ vbriSeekOffsetsBytesPerEntry, vbriSeekOffsetsFramesPerEntry);
+ // Adjust the number of frames so it's counted the same way as in the XING
+ // header
+ if (vbriFrames < 1) {
+ return false;
+ }
+ mNumAudioFrames = Some(vbriFrames - 1);
+ mNumBytes = Some(vbriBytes);
+ mEncoderDelay = vbriEncoderDelay;
+ mVBRISeekOffsetsFramesPerEntry = vbriSeekOffsetsFramesPerEntry;
+ MP3LOG("TOC:");
+ for (auto entry : mTOC) {
+ MP3LOG("%" PRId64, entry);
+ }
+
+ mType = VBRI;
+ return true;
+ }
+ }
+ return false;
+}
+
+bool FrameParser::VBRHeader::Parse(BufferReader* aReader, size_t aFrameSize) {
+ auto res = std::make_pair(ParseVBRI(aReader), ParseXing(aReader, aFrameSize));
+ const bool rv = (res.first.isOk() && res.first.unwrap()) ||
+ (res.second.isOk() && res.second.unwrap());
+ if (rv) {
+ MP3LOG(
+ "VBRHeader::Parse found valid VBR/CBR header: type=%s"
+ " NumAudioFrames=%u NumBytes=%u Scale=%u TOC-size=%zu Delay=%u",
+ vbr_header::TYPE_STR[Type()], NumAudioFrames().valueOr(0),
+ NumBytes().valueOr(0), Scale().valueOr(0), mTOC.size(), mEncoderDelay);
+ }
+ return rv;
+}
+
+// FrameParser::Frame
+
+void FrameParser::Frame::Reset() { mHeader.Reset(); }
+
+uint32_t FrameParser::Frame::Length() const {
+ if (!mHeader.IsValid() || !mHeader.SampleRate()) {
+ return 0;
+ }
+
+ const uint32_t bitsPerSample = mHeader.SamplesPerFrame() / 8;
+ const uint32_t frameLen =
+ bitsPerSample * mHeader.Bitrate() / mHeader.SampleRate() +
+ mHeader.Padding() * mHeader.SlotSize();
+ return frameLen;
+}
+
+bool FrameParser::Frame::ParseNext(uint8_t c) { return mHeader.ParseNext(c); }
+
+const FrameParser::FrameHeader& FrameParser::Frame::Header() const {
+ return mHeader;
+}
+
+bool FrameParser::ParseVBRHeader(BufferReader* aReader) {
+ return mVBRHeader.Parse(aReader, CurrentFrame().Header().SamplesPerFrame());
+}
+
+// ID3Parser
+
+// Constants
+namespace id3_header {
+static const int ID_LEN = 3;
+static const int VERSION_LEN = 2;
+static const int FLAGS_LEN = 1;
+static const int SIZE_LEN = 4;
+
+static const int ID_END = ID_LEN;
+static const int VERSION_END = ID_END + VERSION_LEN;
+static const int FLAGS_END = VERSION_END + FLAGS_LEN;
+static const int SIZE_END = FLAGS_END + SIZE_LEN;
+
+static const uint8_t ID[ID_LEN] = {'I', 'D', '3'};
+static const uint8_t IDv1[ID_LEN] = {'T', 'A', 'G'};
+
+static const uint8_t MIN_MAJOR_VER = 2;
+static const uint8_t MAX_MAJOR_VER = 4;
+} // namespace id3_header
+
+bool ID3Parser::IsBufferStartingWithID3v1Tag(BufferReader* aReader) {
+ mozilla::Result<uint32_t, nsresult> res = aReader->PeekU24();
+ if (res.isErr()) {
+ return false;
+ }
+ // If buffer starts with ID3v1 tag, `rv` would be reverse and its content
+ // should be '3' 'D' 'I' from the lowest bit.
+ uint32_t rv = res.unwrap();
+ for (int idx = id3_header::ID_LEN - 1; idx >= 0; idx--) {
+ if ((rv & 0xff) != id3_header::IDv1[idx]) {
+ return false;
+ }
+ rv = rv >> 8;
+ }
+ return true;
+}
+
+/* static */
+bool ID3Parser::IsBufferStartingWithID3Tag(BufferReader* aReader) {
+ mozilla::Result<uint32_t, nsresult> res = aReader->PeekU24();
+ if (res.isErr()) {
+ return false;
+ }
+ // If buffer starts with ID3v2 tag, `rv` would be reverse and its content
+ // should be '3' 'D' 'I' from the lowest bit.
+ uint32_t rv = res.unwrap();
+ for (int idx = id3_header::ID_LEN - 1; idx >= 0; idx--) {
+ if ((rv & 0xff) != id3_header::ID[idx]) {
+ return false;
+ }
+ rv = rv >> 8;
+ }
+ return true;
+}
+
+uint32_t ID3Parser::Parse(BufferReader* aReader) {
+ MOZ_ASSERT(aReader);
+ MOZ_ASSERT(ID3Parser::IsBufferStartingWithID3Tag(aReader));
+
+ if (!mHeader.HasSizeBeenSet()) {
+ return ParseInternal(aReader);
+ }
+
+ // Encounter another possible ID3 header, if that is valid then we would use
+ // it and save the size of previous one in order to report the size of all ID3
+ // headers together in `TotalHeadersSize()`.
+ ID3Header prevHeader = mHeader;
+ mHeader.Reset();
+ uint32_t size = ParseInternal(aReader);
+ if (!size) {
+ // next ID3 is invalid, so revert the header.
+ mHeader = prevHeader;
+ return size;
+ }
+
+ mFormerID3Size += prevHeader.TotalTagSize();
+ return size;
+}
+
+uint32_t ID3Parser::ParseInternal(BufferReader* aReader) {
+ for (auto res = aReader->ReadU8();
+ res.isOk() && !mHeader.ParseNext(res.unwrap());
+ res = aReader->ReadU8()) {
+ }
+ return mHeader.TotalTagSize();
+}
+
+void ID3Parser::Reset() {
+ mHeader.Reset();
+ mFormerID3Size = 0;
+}
+
+uint32_t ID3Parser::TotalHeadersSize() const {
+ return mHeader.TotalTagSize() + mFormerID3Size;
+}
+
+const ID3Parser::ID3Header& ID3Parser::Header() const { return mHeader; }
+
+// ID3Parser::Header
+
+ID3Parser::ID3Header::ID3Header() { Reset(); }
+
+void ID3Parser::ID3Header::Reset() {
+ mSize.reset();
+ mPos = 0;
+}
+
+uint8_t ID3Parser::ID3Header::MajorVersion() const {
+ return mRaw[id3_header::ID_END];
+}
+
+uint8_t ID3Parser::ID3Header::MinorVersion() const {
+ return mRaw[id3_header::ID_END + 1];
+}
+
+uint8_t ID3Parser::ID3Header::Flags() const {
+ return mRaw[id3_header::FLAGS_END - id3_header::FLAGS_LEN];
+}
+
+uint32_t ID3Parser::ID3Header::Size() const {
+ if (!IsValid() || !mSize) {
+ return 0;
+ }
+ return *mSize;
+}
+
+bool ID3Parser::ID3Header::HasSizeBeenSet() const { return !!mSize; }
+
+uint8_t ID3Parser::ID3Header::FooterSize() const {
+ if (Flags() & (1 << 4)) {
+ return SIZE;
+ }
+ return 0;
+}
+
+uint32_t ID3Parser::ID3Header::TotalTagSize() const {
+ if (IsValid()) {
+ // Header found, return total tag size.
+ return ID3Header::SIZE + Size() + FooterSize();
+ }
+ return 0;
+}
+
+bool ID3Parser::ID3Header::ParseNext(uint8_t c) {
+ if (!Update(c)) {
+ Reset();
+ if (!Update(c)) {
+ Reset();
+ }
+ }
+ return IsValid();
+}
+
+bool ID3Parser::ID3Header::IsValid(int aPos) const {
+ if (aPos >= SIZE) {
+ return true;
+ }
+ const uint8_t c = mRaw[aPos];
+ switch (aPos) {
+ case 0:
+ case 1:
+ case 2:
+ // Expecting "ID3".
+ return id3_header::ID[aPos] == c;
+ case 3:
+ return MajorVersion() >= id3_header::MIN_MAJOR_VER &&
+ MajorVersion() <= id3_header::MAX_MAJOR_VER;
+ case 4:
+ return MinorVersion() < 0xFF;
+ case 5:
+ // Validate flags for supported versions, see bug 949036.
+ return ((0xFF >> MajorVersion()) & c) == 0;
+ case 6:
+ case 7:
+ case 8:
+ case 9:
+ return c < 0x80;
+ }
+ return true;
+}
+
+bool ID3Parser::ID3Header::IsValid() const { return mPos >= SIZE; }
+
+bool ID3Parser::ID3Header::Update(uint8_t c) {
+ if (mPos >= id3_header::SIZE_END - id3_header::SIZE_LEN &&
+ mPos < id3_header::SIZE_END) {
+ uint32_t tmp = mSize.valueOr(0) << 7;
+ mSize = Some(tmp | c);
+ }
+ if (mPos < SIZE) {
+ mRaw[mPos] = c;
+ }
+ return IsValid(mPos++);
+}
+
+} // namespace mozilla
diff --git a/dom/media/mp3/MP3FrameParser.h b/dom/media/mp3/MP3FrameParser.h
new file mode 100644
index 0000000000..d0d7a372d2
--- /dev/null
+++ b/dom/media/mp3/MP3FrameParser.h
@@ -0,0 +1,374 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MP3_FRAME_PARSER_H_
+#define MP3_FRAME_PARSER_H_
+
+#include <vector>
+
+#include "mozilla/Maybe.h"
+#include "mozilla/Result.h"
+#include "BufferReader.h"
+
+namespace mozilla {
+
+// ID3 header parser state machine used by FrameParser.
+// The header contains the following format (one byte per term):
+// 'I' 'D' '3' MajorVersion MinorVersion Flags Size1 Size2 Size3 Size4
+// For more details see https://id3.org/id3v2.4.0-structure
+class ID3Parser {
+ public:
+ // Holds the ID3 header and its parsing state.
+ class ID3Header {
+ public:
+ // The header size is static, see class comment.
+ static const int SIZE = 10;
+ static const int ID3v1_SIZE = 128;
+
+ // Constructor.
+ ID3Header();
+
+ // Resets the state to allow for a new parsing session.
+ void Reset();
+
+ // The ID3 tags are versioned like this: ID3vMajorVersion.MinorVersion.
+ uint8_t MajorVersion() const;
+ uint8_t MinorVersion() const;
+
+ // The ID3 flags field.
+ uint8_t Flags() const;
+
+ // The derived size based on the provided size fields.
+ uint32_t Size() const;
+
+ // To see whether we have parsed the value of the size from header.
+ bool HasSizeBeenSet() const;
+
+ // Returns the size of an ID3v2.4 footer if present and zero otherwise.
+ uint8_t FooterSize() const;
+
+ // The total size of the ID3 tag including header/footer, or zero if
+ // none has been found.
+ uint32_t TotalTagSize() const;
+
+ // Returns whether the parsed data is a valid ID3 header up to the given
+ // byte position.
+ bool IsValid(int aPos) const;
+
+ // Returns whether the parsed data is a complete and valid ID3 header.
+ bool IsValid() const;
+
+ // Parses the next provided byte.
+ // Returns whether the byte creates a valid sequence up to this point.
+ bool ParseNext(uint8_t c);
+
+ private:
+ // Updates the parser state machine with the provided next byte.
+ // Returns whether the provided byte is a valid next byte in the sequence.
+ bool Update(uint8_t c);
+
+ // The currently parsed byte sequence.
+ uint8_t mRaw[SIZE] = {};
+
+ // The derived size as provided by the size fields.
+ // The header size fields holds a 4 byte sequence with each MSB set to 0,
+ // this bits need to be ignored when deriving the actual size.
+ Maybe<uint32_t> mSize;
+
+ // The current byte position in the parsed sequence. Reset via Reset and
+ // incremented via Update.
+ int mPos = 0;
+ };
+
+ // Check if the buffer is starting with ID3v2 tag.
+ static bool IsBufferStartingWithID3Tag(BufferReader* aReader);
+ // Similarly, if the buffer is starting with ID3v1 tag.
+ static bool IsBufferStartingWithID3v1Tag(BufferReader* aReader);
+
+ // Returns the parsed ID3 header. Note: check for validity.
+ const ID3Header& Header() const;
+
+ // Returns the size of all parsed ID3 headers.
+ uint32_t TotalHeadersSize() const;
+
+ // Parses contents of given BufferReader for a valid ID3v2 header.
+ // Returns the parsed ID3v2 tag size if successful and zero otherwise.
+ uint32_t Parse(BufferReader* aReader);
+
+ // Resets the state to allow for a new parsing session.
+ void Reset();
+
+ private:
+ uint32_t ParseInternal(BufferReader* aReader);
+
+ // The currently parsed ID3 header. Reset via Reset, updated via Parse.
+ ID3Header mHeader;
+ // If a file contains multiple ID3 headers, then we would only select the
+ // latest one, but keep the size of former abandoned in order to return the
+ // correct size offset.
+ uint32_t mFormerID3Size = 0;
+};
+
+// MPEG audio frame parser.
+// The MPEG frame header has the following format (one bit per character):
+// 11111111 111VVLLC BBBBSSPR MMEETOHH
+// { sync } - 11 sync bits
+// VV - MPEG audio version ID (0->2.5, 1->reserved, 2->2, 3->1)
+// LL - Layer description (0->reserved, 1->III, 2->II, 3->I)
+// C - CRC protection bit (0->protected, 1->not protected)
+// BBBB - Bitrate index (see table in implementation)
+// SS - Sampling rate index (see table in implementation)
+// P - Padding bit (0->not padded, 1->padded by 1 slot size)
+// R - Private bit (ignored)
+// MM - Channel mode (0->stereo, 1->joint stereo, 2->dual channel,
+// 3->single channel)
+// EE - Mode extension for joint stereo (ignored)
+// T - Copyright (0->disabled, 1->enabled)
+// O - Original (0->copy, 1->original)
+// HH - Emphasis (0->none, 1->50/15 ms, 2->reserved, 3->CCIT J.17)
+class FrameParser {
+ public:
+ // Holds the frame header and its parsing state.
+ class FrameHeader {
+ public:
+ // The header size is static, see class comments.
+ static const int SIZE = 4;
+
+ // Constructor.
+ FrameHeader();
+
+ // Raw field access, see class comments for details.
+ uint8_t Sync1() const;
+ uint8_t Sync2() const;
+ uint8_t RawVersion() const;
+ uint8_t RawLayer() const;
+ uint8_t RawProtection() const;
+ uint8_t RawBitrate() const;
+ uint8_t RawSampleRate() const;
+ uint8_t Padding() const;
+ uint8_t Private() const;
+ uint8_t RawChannelMode() const;
+
+ // Sampling rate frequency in Hz.
+ uint32_t SampleRate() const;
+
+ // Number of audio channels.
+ uint32_t Channels() const;
+
+ // Samples per frames, static depending on MPEG version and layer.
+ uint32_t SamplesPerFrame() const;
+
+ // Slot size used for padding, static depending on MPEG layer.
+ uint32_t SlotSize() const;
+
+ // Bitrate in kbps, can vary between frames.
+ uint32_t Bitrate() const;
+
+ // MPEG layer (0->invalid, 1->I, 2->II, 3->III).
+ uint32_t Layer() const;
+
+ // Returns whether the parsed data is a valid frame header up to the given
+ // byte position.
+ bool IsValid(const int aPos) const;
+
+ // Returns whether the parsed data is a complete and valid frame header.
+ bool IsValid() const;
+
+ // Resets the state to allow for a new parsing session.
+ void Reset();
+
+ // Parses the next provided byte.
+ // Returns whether the byte creates a valid sequence up to this point.
+ bool ParseNext(const uint8_t c);
+
+ private:
+ // Updates the parser state machine with the provided next byte.
+ // Returns whether the provided byte is a valid next byte in the sequence.
+ bool Update(const uint8_t c);
+
+ // The currently parsed byte sequence.
+ uint8_t mRaw[SIZE] = {};
+
+ // The current byte position in the parsed sequence. Reset via Reset and
+ // incremented via Update.
+ int mPos = 0;
+ };
+
+ // VBR frames may contain Xing or VBRI headers for additional info, we use
+ // this class to parse them and access this info.
+ class VBRHeader {
+ public:
+ // Synchronize with vbr_header TYPE_STR on change.
+ enum VBRHeaderType { NONE = 0, XING, VBRI };
+
+ // Constructor.
+ VBRHeader();
+
+ // Returns the parsed VBR header type, or NONE if no valid header found.
+ VBRHeaderType Type() const;
+
+ // Returns the total number of audio frames (excluding the VBR header frame)
+ // expected in the stream/file.
+ const Maybe<uint32_t>& NumAudioFrames() const;
+
+ // Returns the expected size of the stream.
+ const Maybe<uint32_t>& NumBytes() const;
+
+ // Returns the VBR scale factor (0: best quality, 100: lowest quality).
+ const Maybe<uint32_t>& Scale() const;
+
+ // Returns true iff Xing/Info TOC (table of contents) is present.
+ bool IsTOCPresent() const;
+
+ // Returns whether the header is valid (type XING or VBRI).
+ bool IsValid() const;
+
+ // Returns whether the header is valid and contains reasonable non-zero
+ // field values.
+ bool IsComplete() const;
+
+ // Returns the byte offset for the given duration percentage as a factor
+ // (0: begin, 1.0: end).
+ int64_t Offset(media::TimeUnit aTime, media::TimeUnit aDuration) const;
+
+ // Parses contents of given ByteReader for a valid VBR header.
+ // The offset of the passed ByteReader needs to point to an MPEG frame
+ // begin, as a VBRI-style header is searched at a fixed offset relative to
+ // frame begin. Returns whether a valid VBR header was found in the range.
+ bool Parse(BufferReader* aReader, size_t aFrameSize);
+
+ uint32_t EncoderDelay() const { return mEncoderDelay; }
+ uint32_t EncoderPadding() const { return mEncoderPadding; }
+
+ private:
+ // Parses contents of given ByteReader for a valid Xing header.
+ // The initial ByteReader offset will be preserved.
+ // Returns whether a valid Xing header was found in the range.
+ Result<bool, nsresult> ParseXing(BufferReader* aReader, size_t aFrameSize);
+
+ // Parses contents of given ByteReader for a valid VBRI header.
+ // The initial ByteReader offset will be preserved. It also needs to point
+ // to the beginning of a valid MPEG frame, as VBRI headers are searched
+ // at a fixed offset relative to frame begin.
+ // Returns whether a valid VBRI header was found in the range.
+ Result<bool, nsresult> ParseVBRI(BufferReader* aReader);
+
+ // The total number of frames expected as parsed from a VBR header.
+ Maybe<uint32_t> mNumAudioFrames;
+
+ // The total number of bytes expected in the stream.
+ Maybe<uint32_t> mNumBytes;
+
+ // The VBR scale factor.
+ Maybe<uint32_t> mScale;
+
+ // The TOC table mapping duration percentage to byte offset.
+ std::vector<int64_t> mTOC;
+
+ // The detected VBR header type.
+ VBRHeaderType mType;
+
+ uint16_t mVBRISeekOffsetsFramesPerEntry = 0;
+
+ // Delay and padding values found in the LAME header. The encoder delay is a
+ // number of frames that has to be skipped at the beginning of the stream,
+ // encoder padding is a number of frames that needs to be ignored in the
+ // last packet.
+ uint16_t mEncoderDelay = 0;
+ uint16_t mEncoderPadding = 0;
+ };
+
+ // Frame meta container used to parse and hold a frame header and side info.
+ class Frame {
+ public:
+ // Returns the length of the frame excluding the header in bytes.
+ uint32_t Length() const;
+
+ // Returns the parsed frame header.
+ const FrameHeader& Header() const;
+
+ // Resets the frame header and data.
+ void Reset();
+
+ // Parses the next provided byte.
+ // Returns whether the byte creates a valid sequence up to this point.
+ bool ParseNext(uint8_t c);
+
+ private:
+ // The currently parsed frame header.
+ FrameHeader mHeader;
+ };
+
+ // Constructor.
+ FrameParser();
+
+ // Returns the currently parsed frame. Reset via Reset or EndFrameSession.
+ const Frame& CurrentFrame() const;
+
+ // Returns the previously parsed frame. Reset via Reset.
+ const Frame& PrevFrame() const;
+
+ // Returns the first parsed frame. Reset via Reset.
+ const Frame& FirstFrame() const;
+
+ // Returns the parsed ID3 header. Note: check for validity.
+ const ID3Parser::ID3Header& ID3Header() const;
+
+ // Returns whether ID3 metadata have been found, at the end of the file.
+ bool ID3v1MetadataFound() const;
+
+ // Returns the size of all parsed ID3 headers.
+ uint32_t TotalID3HeaderSize() const;
+
+ // Returns the parsed VBR header info. Note: check for validity by type.
+ const VBRHeader& VBRInfo() const;
+
+ // Resets the parser.
+ void Reset();
+
+ // Resets all frame data, but not the ID3Header.
+ // Don't use between frames as first frame data is reset.
+ void ResetFrameData();
+
+ // Clear the last parsed frame to allow for next frame parsing, i.e.:
+ // - sets PrevFrame to CurrentFrame
+ // - resets the CurrentFrame
+ // - resets ID3Header if no valid header was parsed yet
+ void EndFrameSession();
+
+ // Parses contents of given BufferReader for a valid frame header and returns
+ // true if one was found. After returning, the variable passed to
+ // 'aBytesToSkip' holds the amount of bytes to be skipped (if any) in order to
+ // jump across a large ID3v2 tag spanning multiple buffers.
+ Result<bool, nsresult> Parse(BufferReader* aReader, uint32_t* aBytesToSkip);
+
+ // Parses contents of given BufferReader for a valid VBR header.
+ // The offset of the passed BufferReader needs to point to an MPEG frame
+ // begin, as a VBRI-style header is searched at a fixed offset relative to
+ // frame begin. Returns whether a valid VBR header was found.
+ bool ParseVBRHeader(BufferReader* aReader);
+
+ private:
+ // ID3 header parser.
+ ID3Parser mID3Parser;
+
+ // VBR header parser.
+ VBRHeader mVBRHeader;
+
+ // We keep the first parsed frame around for static info access, the
+ // previously parsed frame for debugging and the currently parsed frame.
+ Frame mFirstFrame;
+ Frame mFrame;
+ Frame mPrevFrame;
+ // If this is true, ID3v1 metadata have been found at the end of the file, and
+ // must be sustracted from the stream size in order to compute the stream
+ // duration, when computing the duration of a CBR file based on its length in
+ // bytes. This means that the duration can change at the moment we reach the
+ // end of the file.
+ bool mID3v1MetadataFound = false;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/mp3/moz.build b/dom/media/mp3/moz.build
new file mode 100644
index 0000000000..70031cc2b4
--- /dev/null
+++ b/dom/media/mp3/moz.build
@@ -0,0 +1,22 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS += [
+ "MP3Decoder.h",
+ "MP3Demuxer.h",
+ "MP3FrameParser.h",
+]
+
+UNIFIED_SOURCES += [
+ "MP3Decoder.cpp",
+ "MP3Demuxer.cpp",
+ "MP3FrameParser.cpp",
+]
+
+FINAL_LIBRARY = "xul"
+
+# Add libFuzzer configuration directives
+include("/tools/fuzzing/libfuzzer-config.mozbuild")
diff --git a/dom/media/mp4/Atom.h b/dom/media/mp4/Atom.h
new file mode 100644
index 0000000000..f008dfe148
--- /dev/null
+++ b/dom/media/mp4/Atom.h
@@ -0,0 +1,21 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef ATOM_H_
+#define ATOM_H_
+
+namespace mozilla {
+
+class Atom {
+ public:
+ Atom() : mValid(false) {}
+ virtual bool IsValid() { return mValid; }
+
+ protected:
+ bool mValid;
+};
+
+} // namespace mozilla
+
+#endif // ATOM_H_
diff --git a/dom/media/mp4/AtomType.h b/dom/media/mp4/AtomType.h
new file mode 100644
index 0000000000..dcecde845d
--- /dev/null
+++ b/dom/media/mp4/AtomType.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef ATOM_TYPE_H_
+#define ATOM_TYPE_H_
+
+#include <stdint.h>
+#include "mozilla/EndianUtils.h"
+
+namespace mozilla {
+
+class AtomType {
+ public:
+ AtomType() : mType(0) {}
+ MOZ_IMPLICIT AtomType(uint32_t aType) : mType(aType) {}
+ MOZ_IMPLICIT AtomType(const char* aType)
+ : mType(BigEndian::readUint32(aType)) {}
+ bool operator==(const AtomType& aType) const { return mType == aType.mType; }
+ bool operator!() const { return !mType; }
+
+ private:
+ uint32_t mType;
+};
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/mp4/Box.cpp b/dom/media/mp4/Box.cpp
new file mode 100644
index 0000000000..334ba3e3f8
--- /dev/null
+++ b/dom/media/mp4/Box.cpp
@@ -0,0 +1,230 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "Box.h"
+#include "ByteStream.h"
+#include "mozilla/EndianUtils.h"
+#include "mozilla/Unused.h"
+#include <algorithm>
+
+namespace mozilla {
+
+// Limit reads to 32MiB max.
+// static
+const uint64_t Box::kMAX_BOX_READ = 32 * 1024 * 1024;
+
+// Returns the offset from the start of the body of a box of type |aType|
+// to the start of its first child.
+static uint32_t BoxOffset(AtomType aType) {
+ const uint32_t FULLBOX_OFFSET = 4;
+
+ if (aType == AtomType("mp4a") || aType == AtomType("enca")) {
+ // AudioSampleEntry; ISO 14496-12, section 8.16
+ return 28;
+ } else if (aType == AtomType("mp4v") || aType == AtomType("encv")) {
+ // VideoSampleEntry; ISO 14496-12, section 8.16
+ return 78;
+ } else if (aType == AtomType("stsd")) {
+ // SampleDescriptionBox; ISO 14496-12, section 8.16
+ // This is a FullBox, and contains a |count| member before its child
+ // boxes.
+ return FULLBOX_OFFSET + 4;
+ }
+
+ return 0;
+}
+
+Box::Box(BoxContext* aContext, uint64_t aOffset, const Box* aParent)
+ : mContext(aContext), mParent(aParent) {
+ uint8_t header[8];
+
+ if (aOffset > INT64_MAX - sizeof(header)) {
+ return;
+ }
+
+ MediaByteRange headerRange(aOffset, aOffset + sizeof(header));
+ if (mParent && !mParent->mRange.Contains(headerRange)) {
+ return;
+ }
+
+ const MediaByteRange* byteRange;
+ for (int i = 0;; i++) {
+ if (i == mContext->mByteRanges.Length()) {
+ return;
+ }
+
+ byteRange = static_cast<const MediaByteRange*>(&mContext->mByteRanges[i]);
+ if (byteRange->Contains(headerRange)) {
+ break;
+ }
+ }
+
+ size_t bytes;
+ if (!mContext->mSource->CachedReadAt(aOffset, header, sizeof(header),
+ &bytes) ||
+ bytes != sizeof(header)) {
+ return;
+ }
+
+ uint64_t size = BigEndian::readUint32(header);
+ if (size == 1) {
+ uint8_t bigLength[8];
+ if (aOffset > INT64_MAX - sizeof(header) - sizeof(bigLength)) {
+ return;
+ }
+ MediaByteRange bigLengthRange(headerRange.mEnd,
+ headerRange.mEnd + sizeof(bigLength));
+ if ((mParent && !mParent->mRange.Contains(bigLengthRange)) ||
+ !byteRange->Contains(bigLengthRange) ||
+ !mContext->mSource->CachedReadAt(aOffset + sizeof(header), bigLength,
+ sizeof(bigLength), &bytes) ||
+ bytes != sizeof(bigLength)) {
+ return;
+ }
+ size = BigEndian::readUint64(bigLength);
+ mBodyOffset = bigLengthRange.mEnd;
+ } else if (size == 0) {
+ // box extends to end of file.
+ size = mContext->mByteRanges.LastInterval().mEnd - aOffset;
+ mBodyOffset = headerRange.mEnd;
+ } else {
+ mBodyOffset = headerRange.mEnd;
+ }
+
+ if (size > INT64_MAX) {
+ return;
+ }
+ int64_t end = static_cast<int64_t>(aOffset) + static_cast<int64_t>(size);
+ if (end < static_cast<int64_t>(aOffset)) {
+ // Overflowed.
+ return;
+ }
+
+ mType = BigEndian::readUint32(&header[4]);
+ mChildOffset = mBodyOffset + BoxOffset(mType);
+
+ MediaByteRange boxRange(aOffset, end);
+ if (mChildOffset > boxRange.mEnd ||
+ (mParent && !mParent->mRange.Contains(boxRange)) ||
+ !byteRange->Contains(boxRange)) {
+ return;
+ }
+
+ mRange = boxRange;
+}
+
+Box::Box()
+ : mContext(nullptr), mBodyOffset(0), mChildOffset(0), mParent(nullptr) {}
+
+Box Box::Next() const {
+ MOZ_ASSERT(IsAvailable());
+ return Box(mContext, mRange.mEnd, mParent);
+}
+
+Box Box::FirstChild() const {
+ MOZ_ASSERT(IsAvailable());
+ if (mChildOffset == mRange.mEnd) {
+ return Box();
+ }
+ return Box(mContext, mChildOffset, this);
+}
+
+nsTArray<uint8_t> Box::ReadCompleteBox() const {
+ const size_t length = mRange.mEnd - mRange.mStart;
+ nsTArray<uint8_t> out(length);
+ out.SetLength(length);
+ size_t bytesRead = 0;
+ if (!mContext->mSource->CachedReadAt(mRange.mStart, out.Elements(), length,
+ &bytesRead) ||
+ bytesRead != length) {
+ // Byte ranges are being reported incorrectly
+ NS_WARNING("Read failed in mozilla::Box::ReadCompleteBox()");
+ return nsTArray<uint8_t>(0);
+ }
+ return out;
+}
+
+nsTArray<uint8_t> Box::Read() const {
+ nsTArray<uint8_t> out;
+ Unused << Read(&out, mRange);
+ return out;
+}
+
+bool Box::Read(nsTArray<uint8_t>* aDest, const MediaByteRange& aRange) const {
+ int64_t length;
+ if (!mContext->mSource->Length(&length)) {
+ // The HTTP server didn't give us a length to work with.
+ // Limit the read to kMAX_BOX_READ max.
+ length = std::min(aRange.mEnd - mChildOffset, kMAX_BOX_READ);
+ } else {
+ length = aRange.mEnd - mChildOffset;
+ }
+ aDest->SetLength(length);
+ size_t bytes;
+ if (!mContext->mSource->CachedReadAt(mChildOffset, aDest->Elements(),
+ aDest->Length(), &bytes) ||
+ bytes != aDest->Length()) {
+ // Byte ranges are being reported incorrectly
+ NS_WARNING("Read failed in mozilla::Box::Read()");
+ aDest->Clear();
+ return false;
+ }
+ return true;
+}
+
+ByteSlice Box::ReadAsSlice() {
+ if (!mContext || mRange.IsEmpty()) {
+ return ByteSlice{nullptr, 0};
+ }
+
+ int64_t length;
+ if (!mContext->mSource->Length(&length)) {
+ // The HTTP server didn't give us a length to work with.
+ // Limit the read to kMAX_BOX_READ max.
+ length = std::min(mRange.mEnd - mChildOffset, kMAX_BOX_READ);
+ } else {
+ length = mRange.mEnd - mChildOffset;
+ }
+
+ const uint8_t* data =
+ mContext->mSource->GetContiguousAccess(mChildOffset, length);
+ if (data) {
+ // We can direct access the underlying storage of the ByteStream.
+ return ByteSlice{data, size_t(length)};
+ }
+
+ uint8_t* p = mContext->mAllocator.Allocate(size_t(length));
+ size_t bytes;
+ if (!mContext->mSource->CachedReadAt(mChildOffset, p, length, &bytes) ||
+ bytes != length) {
+ // Byte ranges are being reported incorrectly
+ NS_WARNING("Read failed in mozilla::Box::ReadAsSlice()");
+ return ByteSlice{nullptr, 0};
+ }
+ return ByteSlice{p, size_t(length)};
+}
+
+const size_t BLOCK_CAPACITY = 16 * 1024;
+
+uint8_t* BumpAllocator::Allocate(size_t aNumBytes) {
+ if (aNumBytes > BLOCK_CAPACITY) {
+ mBuffers.AppendElement(nsTArray<uint8_t>(aNumBytes));
+ mBuffers.LastElement().SetLength(aNumBytes);
+ return mBuffers.LastElement().Elements();
+ }
+ for (nsTArray<uint8_t>& buffer : mBuffers) {
+ if (buffer.Length() + aNumBytes < BLOCK_CAPACITY) {
+ size_t offset = buffer.Length();
+ buffer.SetLength(buffer.Length() + aNumBytes);
+ return buffer.Elements() + offset;
+ }
+ }
+ mBuffers.AppendElement(nsTArray<uint8_t>(BLOCK_CAPACITY));
+ mBuffers.LastElement().SetLength(aNumBytes);
+ return mBuffers.LastElement().Elements();
+}
+
+} // namespace mozilla
diff --git a/dom/media/mp4/Box.h b/dom/media/mp4/Box.h
new file mode 100644
index 0000000000..e63bfbcc90
--- /dev/null
+++ b/dom/media/mp4/Box.h
@@ -0,0 +1,100 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef BOX_H_
+#define BOX_H_
+
+#include <stdint.h>
+#include "nsTArray.h"
+#include "MediaResource.h"
+#include "mozilla/EndianUtils.h"
+#include "AtomType.h"
+#include "BufferReader.h"
+
+namespace mozilla {
+class ByteStream;
+
+class BumpAllocator {
+ public:
+ uint8_t* Allocate(size_t aNumBytes);
+
+ private:
+ nsTArray<nsTArray<uint8_t>> mBuffers;
+};
+
+class BoxContext {
+ public:
+ BoxContext(ByteStream* aSource, const MediaByteRangeSet& aByteRanges)
+ : mSource(aSource), mByteRanges(aByteRanges) {}
+
+ RefPtr<ByteStream> mSource;
+ const MediaByteRangeSet& mByteRanges;
+ BumpAllocator mAllocator;
+};
+
+struct ByteSlice {
+ const uint8_t* mBytes;
+ size_t mSize;
+};
+
+class Box {
+ public:
+ Box(BoxContext* aContext, uint64_t aOffset, const Box* aParent = nullptr);
+ Box();
+
+ bool IsAvailable() const { return !mRange.IsEmpty(); }
+ uint64_t Offset() const { return mRange.mStart; }
+ uint64_t Length() const { return mRange.mEnd - mRange.mStart; }
+ uint64_t NextOffset() const { return mRange.mEnd; }
+ const MediaByteRange& Range() const { return mRange; }
+ const Box* Parent() const { return mParent; }
+ bool IsType(const char* aType) const { return mType == AtomType(aType); }
+
+ Box Next() const;
+ Box FirstChild() const;
+ // Reads the box contents, excluding the header.
+ nsTArray<uint8_t> Read() const;
+
+ // Reads the complete box; its header and body.
+ nsTArray<uint8_t> ReadCompleteBox() const;
+
+ // Reads from the content of the box, excluding header.
+ bool Read(nsTArray<uint8_t>* aDest, const MediaByteRange& aRange) const;
+
+ static const uint64_t kMAX_BOX_READ;
+
+ // Returns a slice, pointing to the data of this box. The lifetime of
+ // the memory this slice points to matches the box's context's lifetime.
+ ByteSlice ReadAsSlice();
+
+ private:
+ bool Contains(MediaByteRange aRange) const;
+ BoxContext* mContext;
+ mozilla::MediaByteRange mRange;
+ uint64_t mBodyOffset;
+ uint64_t mChildOffset;
+ AtomType mType;
+ const Box* mParent;
+};
+
+// BoxReader serves box data through an AutoByteReader. The box data is
+// stored either in the box's context's bump allocator, or in the ByteStream
+// itself if the ByteStream implements the Access() method.
+// NOTE: The data the BoxReader reads may be stored in the Box's BoxContext.
+// Ensure that the BoxReader doesn't outlive the BoxContext!
+class MOZ_RAII BoxReader {
+ public:
+ explicit BoxReader(Box& aBox)
+ : mData(aBox.ReadAsSlice()), mReader(mData.mBytes, mData.mSize) {}
+ BufferReader* operator->() { return &mReader; }
+
+ private:
+ ByteSlice mData;
+ BufferReader mReader;
+};
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/mp4/BufferStream.cpp b/dom/media/mp4/BufferStream.cpp
new file mode 100644
index 0000000000..c2fa40cb8a
--- /dev/null
+++ b/dom/media/mp4/BufferStream.cpp
@@ -0,0 +1,59 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "BufferStream.h"
+#include "MediaData.h"
+#include "MediaResource.h"
+#include <algorithm>
+
+namespace mozilla {
+
+BufferStream::BufferStream()
+ : mStartOffset(0), mData(new mozilla::MediaByteBuffer) {}
+
+BufferStream::BufferStream(mozilla::MediaByteBuffer* aBuffer)
+ : mStartOffset(0), mData(aBuffer) {}
+
+BufferStream::~BufferStream() = default;
+
+/*virtual*/
+bool BufferStream::ReadAt(int64_t aOffset, void* aData, size_t aLength,
+ size_t* aBytesRead) {
+ if (aOffset < mStartOffset || aOffset > mStartOffset + mData->Length()) {
+ return false;
+ }
+ *aBytesRead =
+ std::min(aLength, size_t(mStartOffset + mData->Length() - aOffset));
+ memcpy(aData, mData->Elements() + aOffset - mStartOffset, *aBytesRead);
+ return true;
+}
+
+/*virtual*/
+bool BufferStream::CachedReadAt(int64_t aOffset, void* aData, size_t aLength,
+ size_t* aBytesRead) {
+ return ReadAt(aOffset, aData, aLength, aBytesRead);
+}
+
+/*virtual*/
+bool BufferStream::Length(int64_t* aLength) {
+ *aLength = mStartOffset + mData->Length();
+ return true;
+}
+
+/* virtual */
+void BufferStream::DiscardBefore(int64_t aOffset) {
+ if (aOffset > mStartOffset) {
+ mData->RemoveElementsAt(0, aOffset - mStartOffset);
+ mStartOffset = aOffset;
+ }
+}
+
+bool BufferStream::AppendBytes(const uint8_t* aData, size_t aLength) {
+ return mData->AppendElements(aData, aLength, fallible);
+}
+
+MediaByteRange BufferStream::GetByteRange() {
+ return MediaByteRange(mStartOffset, mStartOffset + mData->Length());
+}
+} // namespace mozilla
diff --git a/dom/media/mp4/BufferStream.h b/dom/media/mp4/BufferStream.h
new file mode 100644
index 0000000000..fb817b5916
--- /dev/null
+++ b/dom/media/mp4/BufferStream.h
@@ -0,0 +1,45 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef BUFFER_STREAM_H_
+#define BUFFER_STREAM_H_
+
+#include "ByteStream.h"
+#include "nsTArray.h"
+#include "MediaResource.h"
+
+namespace mozilla {
+class MediaByteBuffer;
+
+DDLoggedTypeDeclNameAndBase(BufferStream, ByteStream);
+
+class BufferStream : public ByteStream,
+ public mozilla::DecoderDoctorLifeLogger<BufferStream> {
+ public:
+ /* BufferStream does not take ownership of aData nor does it make a copy.
+ * Therefore BufferStream shouldn't get used after aData is destroyed.
+ */
+ BufferStream();
+ explicit BufferStream(mozilla::MediaByteBuffer* aBuffer);
+
+ virtual bool ReadAt(int64_t aOffset, void* aData, size_t aLength,
+ size_t* aBytesRead) override;
+ virtual bool CachedReadAt(int64_t aOffset, void* aData, size_t aLength,
+ size_t* aBytesRead) override;
+ virtual bool Length(int64_t* aLength) override;
+
+ virtual void DiscardBefore(int64_t aOffset) override;
+
+ bool AppendBytes(const uint8_t* aData, size_t aLength);
+
+ mozilla::MediaByteRange GetByteRange();
+
+ private:
+ ~BufferStream();
+ int64_t mStartOffset;
+ RefPtr<mozilla::MediaByteBuffer> mData;
+};
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/mp4/ByteStream.h b/dom/media/mp4/ByteStream.h
new file mode 100644
index 0000000000..0f733dfb97
--- /dev/null
+++ b/dom/media/mp4/ByteStream.h
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef STREAM_H_
+#define STREAM_H_
+
+#include "DecoderDoctorLogger.h"
+#include "nsISupportsImpl.h"
+
+namespace mozilla {
+
+DDLoggedTypeDeclName(ByteStream);
+
+class ByteStream : public DecoderDoctorLifeLogger<ByteStream> {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ByteStream);
+
+ virtual bool ReadAt(int64_t offset, void* data, size_t size,
+ size_t* bytes_read) = 0;
+ virtual bool CachedReadAt(int64_t offset, void* data, size_t size,
+ size_t* bytes_read) = 0;
+ virtual bool Length(int64_t* size) = 0;
+
+ virtual void DiscardBefore(int64_t offset) {}
+
+ // If this ByteStream's underlying storage of media is in-memory, this
+ // function returns a pointer to the in-memory storage of data at offset.
+ // Note that even if a ByteStream stores data in memory, it may not be
+ // stored contiguously, in which case this returns nullptr.
+ virtual const uint8_t* GetContiguousAccess(int64_t aOffset, size_t aSize) {
+ return nullptr;
+ }
+
+ protected:
+ virtual ~ByteStream() = default;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/mp4/DecoderData.cpp b/dom/media/mp4/DecoderData.cpp
new file mode 100644
index 0000000000..b7c9c86954
--- /dev/null
+++ b/dom/media/mp4/DecoderData.cpp
@@ -0,0 +1,357 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "Adts.h"
+#include "AnnexB.h"
+#include "BufferReader.h"
+#include "DecoderData.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/EndianUtils.h"
+#include "mozilla/Telemetry.h"
+#include "VideoUtils.h"
+#include "MP4Metadata.h"
+#include "mozilla/Logging.h"
+
+// OpusDecoder header is really needed only by MP4 in rust
+#include "OpusDecoder.h"
+#include "mp4parse.h"
+
+#define LOG(...) \
+ MOZ_LOG(gMP4MetadataLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
+
+using mozilla::media::TimeUnit;
+
+namespace mozilla {
+
+mozilla::Result<mozilla::Ok, nsresult> CryptoFile::DoUpdate(
+ const uint8_t* aData, size_t aLength) {
+ BufferReader reader(aData, aLength);
+ while (reader.Remaining()) {
+ PsshInfo psshInfo;
+ if (!reader.ReadArray(psshInfo.uuid, 16)) {
+ return mozilla::Err(NS_ERROR_FAILURE);
+ }
+
+ if (!reader.CanReadType<uint32_t>()) {
+ return mozilla::Err(NS_ERROR_FAILURE);
+ }
+ auto length = reader.ReadType<uint32_t>();
+
+ if (!reader.ReadArray(psshInfo.data, length)) {
+ return mozilla::Err(NS_ERROR_FAILURE);
+ }
+ pssh.AppendElement(std::move(psshInfo));
+ }
+ return mozilla::Ok();
+}
+
+static MediaResult UpdateTrackProtectedInfo(mozilla::TrackInfo& aConfig,
+ const Mp4parseSinfInfo& aSinf) {
+ if (aSinf.is_encrypted != 0) {
+ if (aSinf.scheme_type == MP4_PARSE_ENCRYPTION_SCHEME_TYPE_CENC) {
+ aConfig.mCrypto.mCryptoScheme = CryptoScheme::Cenc;
+ } else if (aSinf.scheme_type == MP4_PARSE_ENCRYPTION_SCHEME_TYPE_CBCS) {
+ aConfig.mCrypto.mCryptoScheme = CryptoScheme::Cbcs;
+ } else {
+ // Unsupported encryption type;
+ return MediaResult(
+ NS_ERROR_DOM_MEDIA_METADATA_ERR,
+ RESULT_DETAIL(
+ "Unsupported encryption scheme encountered aSinf.scheme_type=%d",
+ static_cast<int>(aSinf.scheme_type)));
+ }
+ aConfig.mCrypto.mIVSize = aSinf.iv_size;
+ aConfig.mCrypto.mKeyId.AppendElements(aSinf.kid.data, aSinf.kid.length);
+ aConfig.mCrypto.mCryptByteBlock = aSinf.crypt_byte_block;
+ aConfig.mCrypto.mSkipByteBlock = aSinf.skip_byte_block;
+ aConfig.mCrypto.mConstantIV.AppendElements(aSinf.constant_iv.data,
+ aSinf.constant_iv.length);
+ }
+ return NS_OK;
+}
+
+// Verify various information shared by Mp4ParseTrackAudioInfo and
+// Mp4ParseTrackVideoInfo and record telemetry on that info. Returns an
+// appropriate MediaResult indicating if the info is valid or not.
+// This verifies:
+// - That we have a sample_info_count > 0 (valid tracks should have at least one
+// sample description entry)
+// - That only a single codec is used across all sample infos, as we don't
+// handle multiple.
+// - If more than one sample information structures contain crypto info. This
+// case is not fatal (we don't return an error), but does record telemetry
+// to help judge if we need more handling in gecko for multiple crypto.
+//
+// Telemetry is also recorded on the above. As of writing, the
+// telemetry is recorded to give us early warning if MP4s exist that we're not
+// handling. Note, if adding new checks and telemetry to this function,
+// telemetry should be recorded before returning to ensure it is gathered.
+template <typename Mp4ParseTrackAudioOrVideoInfo>
+static MediaResult VerifyAudioOrVideoInfoAndRecordTelemetry(
+ Mp4ParseTrackAudioOrVideoInfo* audioOrVideoInfo) {
+ Telemetry::Accumulate(
+ Telemetry::MEDIA_MP4_PARSE_NUM_SAMPLE_DESCRIPTION_ENTRIES,
+ audioOrVideoInfo->sample_info_count);
+
+ bool hasMultipleCodecs = false;
+ uint32_t cryptoCount = 0;
+ Mp4parseCodec codecType = audioOrVideoInfo->sample_info[0].codec_type;
+ for (uint32_t i = 0; i < audioOrVideoInfo->sample_info_count; i++) {
+ if (audioOrVideoInfo->sample_info[0].codec_type != codecType) {
+ hasMultipleCodecs = true;
+ }
+
+ // Update our encryption info if any is present on the sample info.
+ if (audioOrVideoInfo->sample_info[i].protected_data.is_encrypted) {
+ cryptoCount += 1;
+ }
+ }
+
+ Telemetry::Accumulate(
+ Telemetry::
+ MEDIA_MP4_PARSE_SAMPLE_DESCRIPTION_ENTRIES_HAVE_MULTIPLE_CODECS,
+ hasMultipleCodecs);
+
+ // Accumulate if we have multiple (2 or more) crypto entries.
+ // TODO(1715283): rework this to count number of crypto entries + gather
+ // richer data.
+ Telemetry::Accumulate(
+ Telemetry::
+ MEDIA_MP4_PARSE_SAMPLE_DESCRIPTION_ENTRIES_HAVE_MULTIPLE_CRYPTO,
+ cryptoCount >= 2);
+
+ if (audioOrVideoInfo->sample_info_count == 0) {
+ return MediaResult(
+ NS_ERROR_DOM_MEDIA_METADATA_ERR,
+ RESULT_DETAIL("Got 0 sample info while verifying track."));
+ }
+
+ if (hasMultipleCodecs) {
+ // Different codecs in a single track. We don't handle this.
+ return MediaResult(
+ NS_ERROR_DOM_MEDIA_METADATA_ERR,
+ RESULT_DETAIL("Multiple codecs encountered while verifying track."));
+ }
+
+ return NS_OK;
+}
+
+MediaResult MP4AudioInfo::Update(const Mp4parseTrackInfo* aTrack,
+ const Mp4parseTrackAudioInfo* aAudio,
+ const IndiceWrapper* aIndices) {
+ auto rv = VerifyAudioOrVideoInfoAndRecordTelemetry(aAudio);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ Mp4parseCodec codecType = aAudio->sample_info[0].codec_type;
+ for (uint32_t i = 0; i < aAudio->sample_info_count; i++) {
+ if (aAudio->sample_info[i].protected_data.is_encrypted) {
+ auto rv = UpdateTrackProtectedInfo(*this,
+ aAudio->sample_info[i].protected_data);
+ NS_ENSURE_SUCCESS(rv, rv);
+ break;
+ }
+ }
+
+ // We assume that the members of the first sample info are representative of
+ // the entire track. This code will need to be updated should this assumption
+ // ever not hold. E.g. if we need to handle different codecs in a single
+ // track, or if we have different numbers or channels in a single track.
+ Mp4parseByteData mp4ParseSampleCodecSpecific =
+ aAudio->sample_info[0].codec_specific_config;
+ Mp4parseByteData extraData = aAudio->sample_info[0].extra_data;
+ MOZ_ASSERT(mCodecSpecificConfig.is<NoCodecSpecificData>(),
+ "Should have no codec specific data yet");
+ if (codecType == MP4PARSE_CODEC_OPUS) {
+ mMimeType = "audio/opus"_ns;
+ OpusCodecSpecificData opusCodecSpecificData{};
+ // The Opus decoder expects the container's codec delay or
+ // pre-skip value, in microseconds, as a 64-bit int at the
+ // start of the codec-specific config blob.
+ if (mp4ParseSampleCodecSpecific.data &&
+ mp4ParseSampleCodecSpecific.length >= 12) {
+ uint16_t preskip = mozilla::LittleEndian::readUint16(
+ mp4ParseSampleCodecSpecific.data + 10);
+ opusCodecSpecificData.mContainerCodecDelayMicroSeconds =
+ mozilla::FramesToUsecs(preskip, 48000).value();
+ LOG("Opus stream in MP4 container, %" PRId64
+ " microseconds of encoder delay (%" PRIu16 ").",
+ opusCodecSpecificData.mContainerCodecDelayMicroSeconds, preskip);
+ } else {
+ // This file will error later as it will be rejected by the opus decoder.
+ opusCodecSpecificData.mContainerCodecDelayMicroSeconds = 0;
+ }
+ opusCodecSpecificData.mHeadersBinaryBlob->AppendElements(
+ mp4ParseSampleCodecSpecific.data, mp4ParseSampleCodecSpecific.length);
+ mCodecSpecificConfig =
+ AudioCodecSpecificVariant{std::move(opusCodecSpecificData)};
+ } else if (codecType == MP4PARSE_CODEC_AAC) {
+ mMimeType = "audio/mp4a-latm"_ns;
+ int64_t codecDelayUS = aTrack->media_time;
+ double USECS_PER_S = 1e6;
+ // We can't use mozilla::UsecsToFrames here because we need to round, and it
+ // floors.
+ uint32_t encoderDelayFrameCount = 0;
+ if (codecDelayUS > 0) {
+ encoderDelayFrameCount = static_cast<uint32_t>(
+ std::lround(static_cast<double>(codecDelayUS) *
+ aAudio->sample_info->sample_rate / USECS_PER_S));
+ LOG("AAC stream in MP4 container, %" PRIu32 " frames of encoder delay.",
+ encoderDelayFrameCount);
+ }
+
+ uint64_t mediaFrameCount = 0;
+ // Pass the padding number, in frames, to the AAC decoder as well.
+ if (aIndices) {
+ MP4SampleIndex::Indice firstIndice = {0};
+ MP4SampleIndex::Indice lastIndice = {0};
+ bool rv = aIndices->GetIndice(0, firstIndice);
+ rv |= aIndices->GetIndice(aIndices->Length() - 1, lastIndice);
+ if (rv) {
+ if (firstIndice.start_composition > lastIndice.end_composition) {
+ return MediaResult(
+ NS_ERROR_DOM_MEDIA_METADATA_ERR,
+ RESULT_DETAIL("Inconsistent start and end time in index"));
+ }
+ // The `end_composition` member of the very last index member is the
+ // duration of the media in microseconds, excluding decoder delay and
+ // padding. Convert to frames and give to the decoder so that trimming
+ // can be done properly.
+ mediaFrameCount =
+ lastIndice.end_composition - firstIndice.start_composition;
+ LOG("AAC stream in MP4 container, total media duration is %" PRIu64
+ " frames",
+ mediaFrameCount);
+ } else {
+ LOG("AAC stream in MP4 container, couldn't determine total media time");
+ }
+ }
+
+ AacCodecSpecificData aacCodecSpecificData{};
+
+ aacCodecSpecificData.mEncoderDelayFrames = encoderDelayFrameCount;
+ aacCodecSpecificData.mMediaFrameCount = mediaFrameCount;
+
+ // codec specific data is used to store the DecoderConfigDescriptor.
+ aacCodecSpecificData.mDecoderConfigDescriptorBinaryBlob->AppendElements(
+ mp4ParseSampleCodecSpecific.data, mp4ParseSampleCodecSpecific.length);
+ // extra data stores the ES_Descriptor.
+ aacCodecSpecificData.mEsDescriptorBinaryBlob->AppendElements(
+ extraData.data, extraData.length);
+ mCodecSpecificConfig =
+ AudioCodecSpecificVariant{std::move(aacCodecSpecificData)};
+ } else if (codecType == MP4PARSE_CODEC_FLAC) {
+ MOZ_ASSERT(extraData.length == 0,
+ "FLAC doesn't expect extra data so doesn't handle it!");
+ mMimeType = "audio/flac"_ns;
+ FlacCodecSpecificData flacCodecSpecificData{};
+ flacCodecSpecificData.mStreamInfoBinaryBlob->AppendElements(
+ mp4ParseSampleCodecSpecific.data, mp4ParseSampleCodecSpecific.length);
+ mCodecSpecificConfig =
+ AudioCodecSpecificVariant{std::move(flacCodecSpecificData)};
+ } else if (codecType == MP4PARSE_CODEC_MP3) {
+ // mp3 in mp4 can contain ES_Descriptor info (it also has a flash in mp4
+ // specific box, which the rust parser recognizes). However, we don't
+ // handle any such data here.
+ mMimeType = "audio/mpeg"_ns;
+ // TODO(bug 1705812): parse the encoder delay values from the mp4.
+ mCodecSpecificConfig = AudioCodecSpecificVariant{Mp3CodecSpecificData{}};
+ }
+
+ mRate = aAudio->sample_info[0].sample_rate;
+ mChannels = aAudio->sample_info[0].channels;
+ mBitDepth = aAudio->sample_info[0].bit_depth;
+ mExtendedProfile =
+ AssertedCast<int8_t>(aAudio->sample_info[0].extended_profile);
+ if (aTrack->duration > TimeUnit::MaxTicks()) {
+ mDuration = TimeUnit::FromInfinity();
+ } else {
+ mDuration =
+ TimeUnit(AssertedCast<int64_t>(aTrack->duration), aTrack->time_scale);
+ }
+ mMediaTime = TimeUnit(aTrack->media_time, aTrack->time_scale);
+ mTrackId = aTrack->track_id;
+
+ // In stagefright, mProfile is kKeyAACProfile, mExtendedProfile is kKeyAACAOT.
+ if (aAudio->sample_info[0].profile <= 4) {
+ mProfile = AssertedCast<int8_t>(aAudio->sample_info[0].profile);
+ }
+
+ if (mCodecSpecificConfig.is<NoCodecSpecificData>()) {
+ // Handle codecs that are not explicitly handled above.
+ MOZ_ASSERT(
+ extraData.length == 0,
+ "Codecs that use extra data should be explicitly handled already");
+ AudioCodecSpecificBinaryBlob codecSpecificBinaryBlob;
+ // No codec specific metadata set, use the generic form.
+ codecSpecificBinaryBlob.mBinaryBlob->AppendElements(
+ mp4ParseSampleCodecSpecific.data, mp4ParseSampleCodecSpecific.length);
+ mCodecSpecificConfig =
+ AudioCodecSpecificVariant{std::move(codecSpecificBinaryBlob)};
+ }
+
+ return NS_OK;
+}
+
+bool MP4AudioInfo::IsValid() const {
+ return mChannels > 0 && mRate > 0 &&
+ // Accept any mime type here, but if it's aac, validate the profile.
+ (!mMimeType.EqualsLiteral("audio/mp4a-latm") || mProfile > 0 ||
+ mExtendedProfile > 0);
+}
+
+MediaResult MP4VideoInfo::Update(const Mp4parseTrackInfo* track,
+ const Mp4parseTrackVideoInfo* video) {
+ auto rv = VerifyAudioOrVideoInfoAndRecordTelemetry(video);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ Mp4parseCodec codecType = video->sample_info[0].codec_type;
+ for (uint32_t i = 0; i < video->sample_info_count; i++) {
+ if (video->sample_info[i].protected_data.is_encrypted) {
+ auto rv =
+ UpdateTrackProtectedInfo(*this, video->sample_info[i].protected_data);
+ NS_ENSURE_SUCCESS(rv, rv);
+ break;
+ }
+ }
+
+ // We assume that the members of the first sample info are representative of
+ // the entire track. This code will need to be updated should this assumption
+ // ever not hold. E.g. if we need to handle different codecs in a single
+ // track, or if we have different numbers or channels in a single track.
+ if (codecType == MP4PARSE_CODEC_AVC) {
+ mMimeType = "video/avc"_ns;
+ } else if (codecType == MP4PARSE_CODEC_VP9) {
+ mMimeType = "video/vp9"_ns;
+ } else if (codecType == MP4PARSE_CODEC_AV1) {
+ mMimeType = "video/av1"_ns;
+ } else if (codecType == MP4PARSE_CODEC_MP4V) {
+ mMimeType = "video/mp4v-es"_ns;
+ }
+ mTrackId = track->track_id;
+ if (track->duration > TimeUnit::MaxTicks()) {
+ mDuration = TimeUnit::FromInfinity();
+ } else {
+ mDuration =
+ TimeUnit(AssertedCast<int64_t>(track->duration), track->time_scale);
+ }
+ mMediaTime = TimeUnit(track->media_time, track->time_scale);
+ mDisplay.width = AssertedCast<int32_t>(video->display_width);
+ mDisplay.height = AssertedCast<int32_t>(video->display_height);
+ mImage.width = video->sample_info[0].image_width;
+ mImage.height = video->sample_info[0].image_height;
+ mRotation = ToSupportedRotation(video->rotation);
+ Mp4parseByteData extraData = video->sample_info[0].extra_data;
+ // If length is 0 we append nothing
+ mExtraData->AppendElements(extraData.data, extraData.length);
+ return NS_OK;
+}
+
+bool MP4VideoInfo::IsValid() const {
+ return (mDisplay.width > 0 && mDisplay.height > 0) ||
+ (mImage.width > 0 && mImage.height > 0);
+}
+
+} // namespace mozilla
+
+#undef LOG
diff --git a/dom/media/mp4/DecoderData.h b/dom/media/mp4/DecoderData.h
new file mode 100644
index 0000000000..a8d38d0abc
--- /dev/null
+++ b/dom/media/mp4/DecoderData.h
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DECODER_DATA_H_
+#define DECODER_DATA_H_
+
+#include "MediaInfo.h"
+#include "MediaResult.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Result.h"
+#include "mozilla/Types.h"
+#include "mozilla/Vector.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "mp4parse.h"
+
+namespace mozilla {
+
+class IndiceWrapper;
+class MP4Demuxer;
+
+struct PsshInfo {
+ PsshInfo() = default;
+ PsshInfo(const PsshInfo& aOther) = delete;
+ PsshInfo(PsshInfo&& aOther) = default;
+
+ nsTArray<uint8_t> uuid;
+ nsTArray<uint8_t> data;
+
+ bool operator==(const PsshInfo& aOther) const {
+ return uuid == aOther.uuid && data == aOther.data;
+ }
+};
+
+class CryptoFile {
+ public:
+ CryptoFile() : valid(false) {}
+ CryptoFile(const CryptoFile& aCryptoFile) = delete;
+
+ void Update(const uint8_t* aData, size_t aLength) {
+ valid = DoUpdate(aData, aLength).isOk();
+ }
+
+ bool valid;
+ nsTArray<PsshInfo> pssh;
+
+ private:
+ mozilla::Result<mozilla::Ok, nsresult> DoUpdate(const uint8_t* aData,
+ size_t aLength);
+};
+
+class MP4AudioInfo : public mozilla::AudioInfo {
+ public:
+ MP4AudioInfo() = default;
+
+ MediaResult Update(const Mp4parseTrackInfo* aTrack,
+ const Mp4parseTrackAudioInfo* aAudio,
+ const IndiceWrapper* aIndices);
+
+ virtual bool IsValid() const override;
+};
+
+class MP4VideoInfo : public mozilla::VideoInfo {
+ public:
+ MP4VideoInfo() = default;
+
+ MediaResult Update(const Mp4parseTrackInfo* track,
+ const Mp4parseTrackVideoInfo* video);
+
+ virtual bool IsValid() const override;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/mp4/MP4Decoder.cpp b/dom/media/mp4/MP4Decoder.cpp
new file mode 100644
index 0000000000..7e2fdf63d9
--- /dev/null
+++ b/dom/media/mp4/MP4Decoder.cpp
@@ -0,0 +1,222 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MP4Decoder.h"
+#include "H264.h"
+#include "VPXDecoder.h"
+#ifdef MOZ_AV1
+# include "AOMDecoder.h"
+#endif
+#include "MP4Demuxer.h"
+#include "MediaContainerType.h"
+#include "PDMFactory.h"
+#include "PlatformDecoderModule.h"
+#include "VideoUtils.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/gfx/Tools.h"
+#include "nsMimeTypes.h"
+#include "nsReadableUtils.h"
+
+namespace mozilla {
+
+static bool IsWhitelistedH264Codec(const nsAString& aCodec) {
+ uint8_t profile = 0, constraint = 0, level = 0;
+
+ if (!ExtractH264CodecDetails(aCodec, profile, constraint, level)) {
+ return false;
+ }
+
+ // Just assume what we can play on all platforms the codecs/formats that
+ // WMF can play, since we don't have documentation about what other
+ // platforms can play... According to the WMF documentation:
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/dd797815%28v=vs.85%29.aspx
+ // "The Media Foundation H.264 video decoder is a Media Foundation Transform
+ // that supports decoding of Baseline, Main, and High profiles, up to level
+ // 5.1.". We extend the limit to level 5.2, relying on the decoder to handle
+ // any potential errors, the level limit being rather arbitrary.
+ // We also report that we can play Extended profile, as there are
+ // bitstreams that are Extended compliant that are also Baseline compliant.
+ return level >= H264_LEVEL_1 && level <= H264_LEVEL_5_2 &&
+ (profile == H264_PROFILE_BASE || profile == H264_PROFILE_MAIN ||
+ profile == H264_PROFILE_EXTENDED || profile == H264_PROFILE_HIGH);
+}
+
+static bool IsTypeValid(const MediaContainerType& aType) {
+ // Whitelist MP4 types, so they explicitly match what we encounter on
+ // the web, as opposed to what we use internally (i.e. what our demuxers
+ // etc output).
+ return aType.Type() == MEDIAMIMETYPE("audio/mp4") ||
+ aType.Type() == MEDIAMIMETYPE("audio/x-m4a") ||
+ aType.Type() == MEDIAMIMETYPE("video/mp4") ||
+ aType.Type() == MEDIAMIMETYPE("video/quicktime") ||
+ aType.Type() == MEDIAMIMETYPE("video/x-m4v");
+}
+
+/* statis */
+nsTArray<UniquePtr<TrackInfo>> MP4Decoder::GetTracksInfo(
+ const MediaContainerType& aType, MediaResult& aError) {
+ nsTArray<UniquePtr<TrackInfo>> tracks;
+
+ if (!IsTypeValid(aType)) {
+ aError = MediaResult(
+ NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Invalid type:%s", aType.Type().AsString().get()));
+ return tracks;
+ }
+
+ aError = NS_OK;
+
+ const MediaCodecs& codecs = aType.ExtendedType().Codecs();
+ if (codecs.IsEmpty()) {
+ return tracks;
+ }
+
+ const bool isVideo = aType.Type() == MEDIAMIMETYPE("video/mp4") ||
+ aType.Type() == MEDIAMIMETYPE("video/quicktime") ||
+ aType.Type() == MEDIAMIMETYPE("video/x-m4v");
+
+ for (const auto& codec : codecs.Range()) {
+ if (IsAACCodecString(codec)) {
+ tracks.AppendElement(
+ CreateTrackInfoWithMIMETypeAndContainerTypeExtraParameters(
+ "audio/mp4a-latm"_ns, aType));
+ continue;
+ }
+ if (codec.EqualsLiteral("mp3")) {
+ tracks.AppendElement(
+ CreateTrackInfoWithMIMETypeAndContainerTypeExtraParameters(
+ "audio/mpeg"_ns, aType));
+ continue;
+ }
+ // The valid codecs parameter value with mp4 MIME types should be "Opus" and
+ // "fLaC", but "opus" and "flac" are acceptable due to historical reasons.
+ if (codec.EqualsLiteral("opus") || codec.EqualsLiteral("Opus") ||
+ codec.EqualsLiteral("flac") || codec.EqualsLiteral("fLaC")) {
+ NS_ConvertUTF16toUTF8 c(codec);
+ ToLowerCase(c);
+ tracks.AppendElement(
+ CreateTrackInfoWithMIMETypeAndContainerTypeExtraParameters(
+ "audio/"_ns + c, aType));
+ continue;
+ }
+ if (IsVP9CodecString(codec)) {
+ auto trackInfo =
+ CreateTrackInfoWithMIMETypeAndContainerTypeExtraParameters(
+ "video/vp9"_ns, aType);
+ VPXDecoder::SetVideoInfo(trackInfo->GetAsVideoInfo(), codec);
+ tracks.AppendElement(std::move(trackInfo));
+ continue;
+ }
+#ifdef MOZ_AV1
+ if (StaticPrefs::media_av1_enabled() && IsAV1CodecString(codec)) {
+ auto trackInfo =
+ CreateTrackInfoWithMIMETypeAndContainerTypeExtraParameters(
+ "video/av1"_ns, aType);
+ AOMDecoder::SetVideoInfo(trackInfo->GetAsVideoInfo(), codec);
+ tracks.AppendElement(std::move(trackInfo));
+ continue;
+ }
+#endif
+ if (isVideo && IsWhitelistedH264Codec(codec)) {
+ auto trackInfo =
+ CreateTrackInfoWithMIMETypeAndContainerTypeExtraParameters(
+ "video/avc"_ns, aType);
+ uint8_t profile = 0, constraint = 0, level = 0;
+ MOZ_ALWAYS_TRUE(
+ ExtractH264CodecDetails(codec, profile, constraint, level));
+ uint32_t width = aType.ExtendedType().GetWidth().refOr(1280);
+ uint32_t height = aType.ExtendedType().GetHeight().refOr(720);
+ trackInfo->GetAsVideoInfo()->mExtraData =
+ H264::CreateExtraData(profile, constraint, level, {width, height});
+ tracks.AppendElement(std::move(trackInfo));
+ continue;
+ }
+ // Unknown codec
+ aError = MediaResult(
+ NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Unknown codec:%s", NS_ConvertUTF16toUTF8(codec).get()));
+ }
+ return tracks;
+}
+
+/* static */
+bool MP4Decoder::IsSupportedType(const MediaContainerType& aType,
+ DecoderDoctorDiagnostics* aDiagnostics) {
+ if (!IsEnabled()) {
+ return false;
+ }
+
+ MediaResult rv = NS_OK;
+ auto tracks = GetTracksInfo(aType, rv);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ if (!tracks.IsEmpty()) {
+ // Look for exact match as we know used codecs.
+ RefPtr<PDMFactory> platform = new PDMFactory();
+ for (const auto& track : tracks) {
+ if (!track ||
+ platform->Supports(SupportDecoderParams(*track), aDiagnostics) ==
+ media::DecodeSupport::Unsupported) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // We have only container info so try to guess the content type.
+ // Assume H.264/AV1 or AAC
+ if (aType.Type() == MEDIAMIMETYPE("audio/mp4") ||
+ aType.Type() == MEDIAMIMETYPE("audio/x-m4a")) {
+ tracks.AppendElement(
+ CreateTrackInfoWithMIMETypeAndContainerTypeExtraParameters(
+ "audio/mp4a-latm"_ns, aType));
+ } else {
+ tracks.AppendElement(
+ CreateTrackInfoWithMIMETypeAndContainerTypeExtraParameters(
+ "video/avc"_ns, aType));
+ if (StaticPrefs::media_av1_enabled()) {
+ tracks.AppendElement(
+ CreateTrackInfoWithMIMETypeAndContainerTypeExtraParameters(
+ "video/av1"_ns, aType));
+ }
+ }
+
+ // Check that something is supported at least.
+ RefPtr<PDMFactory> platform = new PDMFactory();
+ for (const auto& track : tracks) {
+ if (track &&
+ platform->Supports(SupportDecoderParams(*track), aDiagnostics) !=
+ media::DecodeSupport::Unsupported) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/* static */
+bool MP4Decoder::IsH264(const nsACString& aMimeType) {
+ return aMimeType.EqualsLiteral("video/mp4") ||
+ aMimeType.EqualsLiteral("video/avc");
+}
+
+/* static */
+bool MP4Decoder::IsAAC(const nsACString& aMimeType) {
+ return aMimeType.EqualsLiteral("audio/mp4a-latm");
+}
+
+/* static */
+bool MP4Decoder::IsEnabled() { return StaticPrefs::media_mp4_enabled(); }
+
+/* static */
+nsTArray<UniquePtr<TrackInfo>> MP4Decoder::GetTracksInfo(
+ const MediaContainerType& aType) {
+ MediaResult rv = NS_OK;
+ return GetTracksInfo(aType, rv);
+}
+
+} // namespace mozilla
diff --git a/dom/media/mp4/MP4Decoder.h b/dom/media/mp4/MP4Decoder.h
new file mode 100644
index 0000000000..07b085929b
--- /dev/null
+++ b/dom/media/mp4/MP4Decoder.h
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#if !defined(MP4Decoder_h_)
+# define MP4Decoder_h_
+
+# include "mozilla/UniquePtr.h"
+# include "nsStringFwd.h"
+# include "nsTArray.h"
+
+namespace mozilla {
+
+class MediaContainerType;
+class MediaResult;
+class DecoderDoctorDiagnostics;
+class TrackInfo;
+
+// Decoder that uses a bundled MP4 demuxer and platform decoders to play MP4.
+class MP4Decoder {
+ public:
+ // Returns true if aContainerType is an MP4 type that we think we can render
+ // with the a platform decoder backend.
+ // If provided, codecs are checked for support.
+ static bool IsSupportedType(const MediaContainerType& aContainerType,
+ DecoderDoctorDiagnostics* aDiagnostics);
+
+ // Return true if aMimeType is a one of the strings used by our demuxers to
+ // identify H264. Does not parse general content type strings, i.e. white
+ // space matters.
+ static bool IsH264(const nsACString& aMimeType);
+
+ // Return true if aMimeType is a one of the strings used by our demuxers to
+ // identify AAC. Does not parse general content type strings, i.e. white
+ // space matters.
+ static bool IsAAC(const nsACString& aMimeType);
+
+ // Returns true if the MP4 backend is preffed on.
+ static bool IsEnabled();
+
+ static nsTArray<UniquePtr<TrackInfo>> GetTracksInfo(
+ const MediaContainerType& aType);
+
+ private:
+ static nsTArray<UniquePtr<TrackInfo>> GetTracksInfo(
+ const MediaContainerType& aType, MediaResult& aError);
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/mp4/MP4Demuxer.cpp b/dom/media/mp4/MP4Demuxer.cpp
new file mode 100644
index 0000000000..f8b9e12810
--- /dev/null
+++ b/dom/media/mp4/MP4Demuxer.cpp
@@ -0,0 +1,620 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <algorithm>
+#include <limits>
+#include <stdint.h>
+
+#include "MP4Demuxer.h"
+
+#include "AnnexB.h"
+#include "BufferStream.h"
+#include "H264.h"
+#include "MP4Decoder.h"
+#include "MP4Metadata.h"
+#include "MoofParser.h"
+#include "ResourceStream.h"
+#include "TimeUnits.h"
+#include "VPXDecoder.h"
+#include "mozilla/Span.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/Telemetry.h"
+#include "nsPrintfCString.h"
+#include "SampleIterator.h"
+
+extern mozilla::LazyLogModule gMediaDemuxerLog;
+mozilla::LogModule* GetDemuxerLog() { return gMediaDemuxerLog; }
+
+#define LOG(arg, ...) \
+ DDMOZ_LOG(gMediaDemuxerLog, mozilla::LogLevel::Debug, "::%s: " arg, \
+ __func__, ##__VA_ARGS__)
+
+namespace mozilla {
+
+using TimeUnit = media::TimeUnit;
+using TimeInterval = media::TimeInterval;
+using TimeIntervals = media::TimeIntervals;
+
+DDLoggedTypeDeclNameAndBase(MP4TrackDemuxer, MediaTrackDemuxer);
+
+class MP4TrackDemuxer : public MediaTrackDemuxer,
+ public DecoderDoctorLifeLogger<MP4TrackDemuxer> {
+ public:
+ MP4TrackDemuxer(MediaResource* aResource, UniquePtr<TrackInfo>&& aInfo,
+ const IndiceWrapper& aIndices, uint32_t aTimeScale);
+
+ UniquePtr<TrackInfo> GetInfo() const override;
+
+ RefPtr<SeekPromise> Seek(const TimeUnit& aTime) override;
+
+ RefPtr<SamplesPromise> GetSamples(int32_t aNumSamples = 1) override;
+
+ void Reset() override;
+
+ nsresult GetNextRandomAccessPoint(TimeUnit* aTime) override;
+
+ RefPtr<SkipAccessPointPromise> SkipToNextRandomAccessPoint(
+ const TimeUnit& aTimeThreshold) override;
+
+ TimeIntervals GetBuffered() override;
+
+ void NotifyDataRemoved();
+ void NotifyDataArrived();
+
+ private:
+ already_AddRefed<MediaRawData> GetNextSample();
+ void EnsureUpToDateIndex();
+ void SetNextKeyFrameTime();
+ RefPtr<MediaResource> mResource;
+ RefPtr<ResourceStream> mStream;
+ UniquePtr<TrackInfo> mInfo;
+ RefPtr<MP4SampleIndex> mIndex;
+ UniquePtr<SampleIterator> mIterator;
+ Maybe<TimeUnit> mNextKeyframeTime;
+ // Queued samples extracted by the demuxer, but not yet returned.
+ RefPtr<MediaRawData> mQueuedSample;
+ bool mNeedReIndex;
+ enum CodecType { kH264, kVP9, kAAC, kOther } mType = kOther;
+};
+
+MP4Demuxer::MP4Demuxer(MediaResource* aResource)
+ : mResource(aResource),
+ mStream(new ResourceStream(aResource)),
+ mIsSeekable(false) {
+ DDLINKCHILD("resource", aResource);
+ DDLINKCHILD("stream", mStream.get());
+}
+
+RefPtr<MP4Demuxer::InitPromise> MP4Demuxer::Init() {
+ AutoPinned<ResourceStream> stream(mStream);
+
+ // 'result' will capture the first warning, if any.
+ MediaResult result{NS_OK};
+
+ MP4Metadata::ResultAndByteBuffer initData = MP4Metadata::Metadata(stream);
+ if (!initData.Ref()) {
+ return InitPromise::CreateAndReject(
+ NS_FAILED(initData.Result())
+ ? std::move(initData.Result())
+ : MediaResult(NS_ERROR_DOM_MEDIA_DEMUXER_ERR,
+ RESULT_DETAIL("Invalid MP4 metadata or OOM")),
+ __func__);
+ } else if (NS_FAILED(initData.Result()) && result == NS_OK) {
+ result = std::move(initData.Result());
+ }
+
+ RefPtr<BufferStream> bufferstream = new BufferStream(initData.Ref());
+
+ MP4Metadata metadata{bufferstream};
+ DDLINKCHILD("metadata", &metadata);
+ nsresult rv = metadata.Parse();
+ if (NS_FAILED(rv)) {
+ return InitPromise::CreateAndReject(
+ MediaResult(rv, RESULT_DETAIL("Parse MP4 metadata failed")), __func__);
+ }
+
+ auto audioTrackCount = metadata.GetNumberTracks(TrackInfo::kAudioTrack);
+ if (audioTrackCount.Ref() == MP4Metadata::NumberTracksError()) {
+ if (StaticPrefs::media_playback_warnings_as_errors()) {
+ return InitPromise::CreateAndReject(
+ MediaResult(
+ NS_ERROR_DOM_MEDIA_DEMUXER_ERR,
+ RESULT_DETAIL("Invalid audio track (%s)",
+ audioTrackCount.Result().Description().get())),
+ __func__);
+ }
+ audioTrackCount.Ref() = 0;
+ }
+
+ auto videoTrackCount = metadata.GetNumberTracks(TrackInfo::kVideoTrack);
+ if (videoTrackCount.Ref() == MP4Metadata::NumberTracksError()) {
+ if (StaticPrefs::media_playback_warnings_as_errors()) {
+ return InitPromise::CreateAndReject(
+ MediaResult(
+ NS_ERROR_DOM_MEDIA_DEMUXER_ERR,
+ RESULT_DETAIL("Invalid video track (%s)",
+ videoTrackCount.Result().Description().get())),
+ __func__);
+ }
+ videoTrackCount.Ref() = 0;
+ }
+
+ if (audioTrackCount.Ref() == 0 && videoTrackCount.Ref() == 0) {
+ return InitPromise::CreateAndReject(
+ MediaResult(
+ NS_ERROR_DOM_MEDIA_DEMUXER_ERR,
+ RESULT_DETAIL("No MP4 audio (%s) or video (%s) tracks",
+ audioTrackCount.Result().Description().get(),
+ videoTrackCount.Result().Description().get())),
+ __func__);
+ }
+
+ if (NS_FAILED(audioTrackCount.Result()) && result == NS_OK) {
+ result = std::move(audioTrackCount.Result());
+ }
+ if (NS_FAILED(videoTrackCount.Result()) && result == NS_OK) {
+ result = std::move(videoTrackCount.Result());
+ }
+
+ if (audioTrackCount.Ref() != 0) {
+ for (size_t i = 0; i < audioTrackCount.Ref(); i++) {
+ MP4Metadata::ResultAndTrackInfo info =
+ metadata.GetTrackInfo(TrackInfo::kAudioTrack, i);
+ if (!info.Ref()) {
+ if (StaticPrefs::media_playback_warnings_as_errors()) {
+ return InitPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_DEMUXER_ERR,
+ RESULT_DETAIL("Invalid MP4 audio track (%s)",
+ info.Result().Description().get())),
+ __func__);
+ }
+ if (result == NS_OK) {
+ result =
+ MediaResult(NS_ERROR_DOM_MEDIA_DEMUXER_ERR,
+ RESULT_DETAIL("Invalid MP4 audio track (%s)",
+ info.Result().Description().get()));
+ }
+ continue;
+ } else if (NS_FAILED(info.Result()) && result == NS_OK) {
+ result = std::move(info.Result());
+ }
+ MP4Metadata::ResultAndIndice indices =
+ metadata.GetTrackIndice(info.Ref()->mTrackId);
+ if (!indices.Ref()) {
+ if (NS_FAILED(info.Result()) && result == NS_OK) {
+ result = std::move(indices.Result());
+ }
+ continue;
+ }
+ RefPtr<MP4TrackDemuxer> demuxer =
+ new MP4TrackDemuxer(mResource, std::move(info.Ref()),
+ *indices.Ref().get(), info.Ref()->mTimeScale);
+ DDLINKCHILD("audio demuxer", demuxer.get());
+ mAudioDemuxers.AppendElement(std::move(demuxer));
+ }
+ }
+
+ if (videoTrackCount.Ref() != 0) {
+ for (size_t i = 0; i < videoTrackCount.Ref(); i++) {
+ MP4Metadata::ResultAndTrackInfo info =
+ metadata.GetTrackInfo(TrackInfo::kVideoTrack, i);
+ if (!info.Ref()) {
+ if (StaticPrefs::media_playback_warnings_as_errors()) {
+ return InitPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_DEMUXER_ERR,
+ RESULT_DETAIL("Invalid MP4 video track (%s)",
+ info.Result().Description().get())),
+ __func__);
+ }
+ if (result == NS_OK) {
+ result =
+ MediaResult(NS_ERROR_DOM_MEDIA_DEMUXER_ERR,
+ RESULT_DETAIL("Invalid MP4 video track (%s)",
+ info.Result().Description().get()));
+ }
+ continue;
+ } else if (NS_FAILED(info.Result()) && result == NS_OK) {
+ result = std::move(info.Result());
+ }
+ MP4Metadata::ResultAndIndice indices =
+ metadata.GetTrackIndice(info.Ref()->mTrackId);
+ if (!indices.Ref()) {
+ if (NS_FAILED(info.Result()) && result == NS_OK) {
+ result = std::move(indices.Result());
+ }
+ continue;
+ }
+ RefPtr<MP4TrackDemuxer> demuxer =
+ new MP4TrackDemuxer(mResource, std::move(info.Ref()),
+ *indices.Ref().get(), info.Ref()->mTimeScale);
+ DDLINKCHILD("video demuxer", demuxer.get());
+ mVideoDemuxers.AppendElement(std::move(demuxer));
+ }
+ }
+
+ MP4Metadata::ResultAndCryptoFile cryptoFile = metadata.Crypto();
+ if (NS_FAILED(cryptoFile.Result()) && result == NS_OK) {
+ result = std::move(cryptoFile.Result());
+ }
+ MOZ_ASSERT(cryptoFile.Ref());
+ if (cryptoFile.Ref()->valid) {
+ const nsTArray<PsshInfo>& psshs = cryptoFile.Ref()->pssh;
+ for (uint32_t i = 0; i < psshs.Length(); i++) {
+ mCryptoInitData.AppendElements(psshs[i].data);
+ }
+ }
+
+ mIsSeekable = metadata.CanSeek();
+
+ return InitPromise::CreateAndResolve(result, __func__);
+}
+
+uint32_t MP4Demuxer::GetNumberTracks(TrackInfo::TrackType aType) const {
+ switch (aType) {
+ case TrackInfo::kAudioTrack:
+ return uint32_t(mAudioDemuxers.Length());
+ case TrackInfo::kVideoTrack:
+ return uint32_t(mVideoDemuxers.Length());
+ default:
+ return 0;
+ }
+}
+
+already_AddRefed<MediaTrackDemuxer> MP4Demuxer::GetTrackDemuxer(
+ TrackInfo::TrackType aType, uint32_t aTrackNumber) {
+ switch (aType) {
+ case TrackInfo::kAudioTrack:
+ if (aTrackNumber >= uint32_t(mAudioDemuxers.Length())) {
+ return nullptr;
+ }
+ return RefPtr<MediaTrackDemuxer>(mAudioDemuxers[aTrackNumber]).forget();
+ case TrackInfo::kVideoTrack:
+ if (aTrackNumber >= uint32_t(mVideoDemuxers.Length())) {
+ return nullptr;
+ }
+ return RefPtr<MediaTrackDemuxer>(mVideoDemuxers[aTrackNumber]).forget();
+ default:
+ return nullptr;
+ }
+}
+
+bool MP4Demuxer::IsSeekable() const { return mIsSeekable; }
+
+void MP4Demuxer::NotifyDataArrived() {
+ for (auto& dmx : mAudioDemuxers) {
+ dmx->NotifyDataArrived();
+ }
+ for (auto& dmx : mVideoDemuxers) {
+ dmx->NotifyDataArrived();
+ }
+}
+
+void MP4Demuxer::NotifyDataRemoved() {
+ for (auto& dmx : mAudioDemuxers) {
+ dmx->NotifyDataRemoved();
+ }
+ for (auto& dmx : mVideoDemuxers) {
+ dmx->NotifyDataRemoved();
+ }
+}
+
+UniquePtr<EncryptionInfo> MP4Demuxer::GetCrypto() {
+ UniquePtr<EncryptionInfo> crypto;
+ if (!mCryptoInitData.IsEmpty()) {
+ crypto.reset(new EncryptionInfo{});
+ crypto->AddInitData(u"cenc"_ns, mCryptoInitData);
+ }
+ return crypto;
+}
+
+MP4TrackDemuxer::MP4TrackDemuxer(MediaResource* aResource,
+ UniquePtr<TrackInfo>&& aInfo,
+ const IndiceWrapper& aIndices,
+ uint32_t aTimeScale)
+ : mResource(aResource),
+ mStream(new ResourceStream(aResource)),
+ mInfo(std::move(aInfo)),
+ mIndex(new MP4SampleIndex(aIndices, mStream, mInfo->mTrackId,
+ mInfo->IsAudio(), aTimeScale)),
+ mIterator(MakeUnique<SampleIterator>(mIndex)),
+ mNeedReIndex(true) {
+ EnsureUpToDateIndex(); // Force update of index
+
+ VideoInfo* videoInfo = mInfo->GetAsVideoInfo();
+ AudioInfo* audioInfo = mInfo->GetAsAudioInfo();
+ if (videoInfo && MP4Decoder::IsH264(mInfo->mMimeType)) {
+ mType = kH264;
+ RefPtr<MediaByteBuffer> extraData = videoInfo->mExtraData;
+ SPSData spsdata;
+ if (H264::DecodeSPSFromExtraData(extraData, spsdata) &&
+ spsdata.pic_width > 0 && spsdata.pic_height > 0 &&
+ H264::EnsureSPSIsSane(spsdata)) {
+ videoInfo->mImage.width = spsdata.pic_width;
+ videoInfo->mImage.height = spsdata.pic_height;
+ videoInfo->mDisplay.width = spsdata.display_width;
+ videoInfo->mDisplay.height = spsdata.display_height;
+ }
+ } else if (videoInfo && VPXDecoder::IsVP9(mInfo->mMimeType)) {
+ mType = kVP9;
+ } else if (audioInfo && MP4Decoder::IsAAC(mInfo->mMimeType)) {
+ mType = kAAC;
+ }
+}
+
+UniquePtr<TrackInfo> MP4TrackDemuxer::GetInfo() const { return mInfo->Clone(); }
+
+void MP4TrackDemuxer::EnsureUpToDateIndex() {
+ if (!mNeedReIndex) {
+ return;
+ }
+ AutoPinned<MediaResource> resource(mResource);
+ MediaByteRangeSet byteRanges;
+ nsresult rv = resource->GetCachedRanges(byteRanges);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+ mIndex->UpdateMoofIndex(byteRanges);
+ mNeedReIndex = false;
+}
+
+RefPtr<MP4TrackDemuxer::SeekPromise> MP4TrackDemuxer::Seek(
+ const TimeUnit& aTime) {
+ auto seekTime = aTime;
+ mQueuedSample = nullptr;
+
+ mIterator->Seek(seekTime);
+
+ // Check what time we actually seeked to.
+ do {
+ RefPtr<MediaRawData> sample = GetNextSample();
+ if (!sample) {
+ return SeekPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_END_OF_STREAM,
+ __func__);
+ }
+ if (!sample->Size()) {
+ // This sample can't be decoded, continue searching.
+ continue;
+ }
+ if (sample->mKeyframe) {
+ MOZ_DIAGNOSTIC_ASSERT(sample->HasValidTime());
+ mQueuedSample = sample;
+ seekTime = mQueuedSample->mTime;
+ }
+ } while (!mQueuedSample);
+
+ SetNextKeyFrameTime();
+
+ return SeekPromise::CreateAndResolve(seekTime, __func__);
+}
+
+already_AddRefed<MediaRawData> MP4TrackDemuxer::GetNextSample() {
+ RefPtr<MediaRawData> sample = mIterator->GetNext();
+ if (!sample) {
+ return nullptr;
+ }
+ if (mInfo->GetAsVideoInfo()) {
+ sample->mExtraData = mInfo->GetAsVideoInfo()->mExtraData;
+ if (mType == kH264 && !sample->mCrypto.IsEncrypted()) {
+ H264::FrameType type = H264::GetFrameType(sample);
+ switch (type) {
+ case H264::FrameType::I_FRAME:
+ [[fallthrough]];
+ case H264::FrameType::OTHER: {
+ bool keyframe = type == H264::FrameType::I_FRAME;
+ if (sample->mKeyframe != keyframe) {
+ NS_WARNING(nsPrintfCString("Frame incorrectly marked as %skeyframe "
+ "@ pts:%" PRId64 " dur:%" PRId64
+ " dts:%" PRId64,
+ keyframe ? "" : "non-",
+ sample->mTime.ToMicroseconds(),
+ sample->mDuration.ToMicroseconds(),
+ sample->mTimecode.ToMicroseconds())
+ .get());
+ sample->mKeyframe = keyframe;
+ }
+ break;
+ }
+ case H264::FrameType::INVALID:
+ NS_WARNING(nsPrintfCString("Invalid H264 frame @ pts:%" PRId64
+ " dur:%" PRId64 " dts:%" PRId64,
+ sample->mTime.ToMicroseconds(),
+ sample->mDuration.ToMicroseconds(),
+ sample->mTimecode.ToMicroseconds())
+ .get());
+ // We could reject the sample now, however demuxer errors are fatal.
+ // So we keep the invalid frame, relying on the H264 decoder to
+ // handle the error later.
+ // TODO: make demuxer errors non-fatal.
+ break;
+ }
+ } else if (mType == kVP9 && !sample->mCrypto.IsEncrypted()) {
+ bool keyframe = VPXDecoder::IsKeyframe(
+ Span<const uint8_t>(sample->Data(), sample->Size()),
+ VPXDecoder::Codec::VP9);
+ if (sample->mKeyframe != keyframe) {
+ NS_WARNING(nsPrintfCString(
+ "Frame incorrectly marked as %skeyframe "
+ "@ pts:%" PRId64 " dur:%" PRId64 " dts:%" PRId64,
+ keyframe ? "" : "non-", sample->mTime.ToMicroseconds(),
+ sample->mDuration.ToMicroseconds(),
+ sample->mTimecode.ToMicroseconds())
+ .get());
+ sample->mKeyframe = keyframe;
+ }
+ }
+ }
+
+ // Adjust trimming information if needed.
+ if (mInfo->GetAsAudioInfo()) {
+ AudioInfo* info = mInfo->GetAsAudioInfo();
+ TimeUnit originalPts = sample->mTime;
+ TimeUnit originalEnd = sample->GetEndTime();
+ if (sample->mTime.IsNegative()) {
+ sample->mTime = TimeUnit::Zero(originalPts);
+ sample->mDuration = std::max(TimeUnit::Zero(sample->mTime),
+ originalPts + sample->mDuration);
+ sample->mOriginalPresentationWindow =
+ Some(TimeInterval{originalPts, originalEnd});
+ }
+ // The demuxer only knows the presentation time of the packet, not the
+ // actual number of samples that will be decoded from this packet.
+ // However we need to trim the last packet to the correct duration.
+ // Find the actual size of the decoded packet to know how many samples to
+ // trim. This only works because the packet size are constant.
+ TimeUnit totalMediaDurationIncludingTrimming =
+ info->mDuration - info->mMediaTime;
+ if (mType == kAAC &&
+ sample->GetEndTime() >= totalMediaDurationIncludingTrimming &&
+ totalMediaDurationIncludingTrimming.IsPositive()) {
+ // Seek backward a bit.
+ mIterator->Seek(sample->mTime - sample->mDuration);
+ RefPtr<MediaRawData> previousSample = mIterator->GetNext();
+ if (previousSample) {
+ TimeInterval fullPacketDuration{previousSample->mTime,
+ previousSample->GetEndTime()};
+ sample->mOriginalPresentationWindow = Some(TimeInterval{
+ originalPts, originalPts + fullPacketDuration.Length()});
+ }
+ // Seek back so we're back at the original location -- there's no packet
+ // left anyway.
+ mIterator->Seek(sample->mTime);
+ RefPtr<MediaRawData> dummy = mIterator->GetNext();
+ }
+ }
+
+ if (MOZ_LOG_TEST(GetDemuxerLog(), LogLevel::Verbose)) {
+ bool isAudio = mInfo->GetAsAudioInfo();
+ TimeUnit originalStart = TimeUnit::Invalid();
+ TimeUnit originalEnd = TimeUnit::Invalid();
+ if (sample->mOriginalPresentationWindow) {
+ originalStart = sample->mOriginalPresentationWindow->mStart;
+ originalEnd = sample->mOriginalPresentationWindow->mEnd;
+ }
+ LOG("%s packet demuxed (track id: %d): [%s,%s], duration: %s (original "
+ "time: [%s,%s])",
+ isAudio ? "Audio" : "Video", mInfo->mTrackId,
+ sample->mTime.ToString().get(), sample->GetEndTime().ToString().get(),
+ sample->mDuration.ToString().get(), originalStart.ToString().get(),
+ originalEnd.ToString().get());
+ }
+
+ return sample.forget();
+}
+
+RefPtr<MP4TrackDemuxer::SamplesPromise> MP4TrackDemuxer::GetSamples(
+ int32_t aNumSamples) {
+ EnsureUpToDateIndex();
+ RefPtr<SamplesHolder> samples = new SamplesHolder;
+ if (!aNumSamples) {
+ return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_DEMUXER_ERR,
+ __func__);
+ }
+
+ if (mQueuedSample) {
+ NS_ASSERTION(mQueuedSample->mKeyframe, "mQueuedSample must be a keyframe");
+ samples->AppendSample(mQueuedSample);
+ mQueuedSample = nullptr;
+ aNumSamples--;
+ }
+ RefPtr<MediaRawData> sample;
+ while (aNumSamples && (sample = GetNextSample())) {
+ if (!sample->Size()) {
+ continue;
+ }
+ MOZ_DIAGNOSTIC_ASSERT(sample->HasValidTime());
+ samples->AppendSample(sample);
+ aNumSamples--;
+ }
+
+ if (samples->GetSamples().IsEmpty()) {
+ return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_END_OF_STREAM,
+ __func__);
+ }
+
+ if (mNextKeyframeTime.isNothing() ||
+ samples->GetSamples().LastElement()->mTime >= mNextKeyframeTime.value()) {
+ SetNextKeyFrameTime();
+ }
+ return SamplesPromise::CreateAndResolve(samples, __func__);
+}
+
+void MP4TrackDemuxer::SetNextKeyFrameTime() {
+ mNextKeyframeTime.reset();
+ TimeUnit frameTime = mIterator->GetNextKeyframeTime();
+ if (frameTime.IsValid()) {
+ mNextKeyframeTime.emplace(frameTime);
+ }
+}
+
+void MP4TrackDemuxer::Reset() {
+ mQueuedSample = nullptr;
+ // TODO: verify this
+ mIterator->Seek(TimeUnit::FromNegativeInfinity());
+ SetNextKeyFrameTime();
+}
+
+nsresult MP4TrackDemuxer::GetNextRandomAccessPoint(TimeUnit* aTime) {
+ if (mNextKeyframeTime.isNothing()) {
+ // There's no next key frame.
+ *aTime = TimeUnit::FromInfinity();
+ } else {
+ *aTime = mNextKeyframeTime.value();
+ }
+ return NS_OK;
+}
+
+RefPtr<MP4TrackDemuxer::SkipAccessPointPromise>
+MP4TrackDemuxer::SkipToNextRandomAccessPoint(const TimeUnit& aTimeThreshold) {
+ mQueuedSample = nullptr;
+ // Loop until we reach the next keyframe after the threshold.
+ uint32_t parsed = 0;
+ bool found = false;
+ RefPtr<MediaRawData> sample;
+ while (!found && (sample = GetNextSample())) {
+ parsed++;
+ MOZ_DIAGNOSTIC_ASSERT(sample->HasValidTime());
+ if (sample->mKeyframe && sample->mTime >= aTimeThreshold) {
+ found = true;
+ mQueuedSample = sample;
+ }
+ }
+ SetNextKeyFrameTime();
+ if (found) {
+ return SkipAccessPointPromise::CreateAndResolve(parsed, __func__);
+ }
+ SkipFailureHolder failure(NS_ERROR_DOM_MEDIA_END_OF_STREAM, parsed);
+ return SkipAccessPointPromise::CreateAndReject(std::move(failure), __func__);
+}
+
+TimeIntervals MP4TrackDemuxer::GetBuffered() {
+ EnsureUpToDateIndex();
+ AutoPinned<MediaResource> resource(mResource);
+ MediaByteRangeSet byteRanges;
+ nsresult rv = resource->GetCachedRanges(byteRanges);
+
+ if (NS_FAILED(rv)) {
+ return TimeIntervals();
+ }
+
+ return mIndex->ConvertByteRangesToTimeRanges(byteRanges);
+}
+
+void MP4TrackDemuxer::NotifyDataArrived() { mNeedReIndex = true; }
+
+void MP4TrackDemuxer::NotifyDataRemoved() {
+ AutoPinned<MediaResource> resource(mResource);
+ MediaByteRangeSet byteRanges;
+ nsresult rv = resource->GetCachedRanges(byteRanges);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+ mIndex->UpdateMoofIndex(byteRanges, true /* can evict */);
+ mNeedReIndex = false;
+}
+
+} // namespace mozilla
+
+#undef LOG
diff --git a/dom/media/mp4/MP4Demuxer.h b/dom/media/mp4/MP4Demuxer.h
new file mode 100644
index 0000000000..22fa5b137f
--- /dev/null
+++ b/dom/media/mp4/MP4Demuxer.h
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(MP4Demuxer_h_)
+# define MP4Demuxer_h_
+
+# include "mozilla/Maybe.h"
+# include "mozilla/Monitor.h"
+# include "MediaDataDemuxer.h"
+# include "MediaResource.h"
+
+namespace mozilla {
+class MP4TrackDemuxer;
+class ResourceStream;
+
+DDLoggedTypeDeclNameAndBase(MP4Demuxer, MediaDataDemuxer);
+
+class MP4Demuxer : public MediaDataDemuxer,
+ public DecoderDoctorLifeLogger<MP4Demuxer> {
+ public:
+ explicit MP4Demuxer(MediaResource* aResource);
+
+ RefPtr<InitPromise> Init() override;
+
+ uint32_t GetNumberTracks(TrackInfo::TrackType aType) const override;
+
+ already_AddRefed<MediaTrackDemuxer> GetTrackDemuxer(
+ TrackInfo::TrackType aType, uint32_t aTrackNumber) override;
+
+ bool IsSeekable() const override;
+
+ UniquePtr<EncryptionInfo> GetCrypto() override;
+
+ void NotifyDataArrived() override;
+
+ void NotifyDataRemoved() override;
+
+ private:
+ RefPtr<MediaResource> mResource;
+ RefPtr<ResourceStream> mStream;
+ AutoTArray<RefPtr<MP4TrackDemuxer>, 1> mAudioDemuxers;
+ AutoTArray<RefPtr<MP4TrackDemuxer>, 1> mVideoDemuxers;
+ nsTArray<uint8_t> mCryptoInitData;
+ bool mIsSeekable;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/mp4/MP4Interval.h b/dom/media/mp4/MP4Interval.h
new file mode 100644
index 0000000000..70e6daeadd
--- /dev/null
+++ b/dom/media/mp4/MP4Interval.h
@@ -0,0 +1,137 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef INTERVAL_H_
+#define INTERVAL_H_
+
+#include "nsTArray.h"
+#include <algorithm>
+#include <nsString.h>
+
+namespace mozilla {
+
+template <typename T>
+struct MP4Interval {
+ MP4Interval() : start{}, end{} {}
+ MP4Interval(T aStart, T aEnd) : start(aStart), end(aEnd) {
+ MOZ_ASSERT(aStart <= aEnd);
+ }
+ T Length() { return end - start; }
+ MP4Interval Intersection(const MP4Interval& aOther) const {
+ T s = start > aOther.start ? start : aOther.start;
+ T e = end < aOther.end ? end : aOther.end;
+ if (s > e) {
+ return MP4Interval();
+ }
+ return MP4Interval(s, e);
+ }
+ bool Contains(const MP4Interval& aOther) const {
+ return aOther.start >= start && aOther.end <= end;
+ }
+ bool operator==(const MP4Interval& aOther) const {
+ return start == aOther.start && end == aOther.end;
+ }
+ bool operator!=(const MP4Interval& aOther) const {
+ return !(*this == aOther);
+ }
+ bool IsNull() const { return end == start; }
+ MP4Interval Extents(const MP4Interval& aOther) const {
+ if (IsNull()) {
+ return aOther;
+ }
+ return MP4Interval(std::min(start, aOther.start),
+ std::max(end, aOther.end));
+ }
+
+ T start;
+ T end;
+
+ nsCString ToString() {
+ return nsPrintfCString("[%s, %s]", start.ToString().get(),
+ end.ToString().get());
+ }
+
+ static void SemiNormalAppend(nsTArray<MP4Interval<T>>& aIntervals,
+ MP4Interval<T> aMP4Interval) {
+ if (!aIntervals.IsEmpty() &&
+ aIntervals.LastElement().end == aMP4Interval.start) {
+ aIntervals.LastElement().end = aMP4Interval.end;
+ } else {
+ aIntervals.AppendElement(aMP4Interval);
+ }
+ }
+
+ static void Normalize(const nsTArray<MP4Interval<T>>& aIntervals,
+ nsTArray<MP4Interval<T>>* aNormalized) {
+ if (!aNormalized || !aIntervals.Length()) {
+ MOZ_ASSERT(aNormalized);
+ return;
+ }
+ MOZ_ASSERT(aNormalized->IsEmpty());
+
+ nsTArray<MP4Interval<T>> sorted = aIntervals.Clone();
+ sorted.Sort(Compare());
+
+ MP4Interval<T> current = sorted[0];
+ for (size_t i = 1; i < sorted.Length(); i++) {
+ MOZ_ASSERT(sorted[i].start <= sorted[i].end);
+ if (current.Contains(sorted[i])) {
+ continue;
+ }
+ if (current.end >= sorted[i].start) {
+ current.end = sorted[i].end;
+ } else {
+ aNormalized->AppendElement(current);
+ current = sorted[i];
+ }
+ }
+ aNormalized->AppendElement(current);
+ }
+
+ static void Intersection(const nsTArray<MP4Interval<T>>& a0,
+ const nsTArray<MP4Interval<T>>& a1,
+ nsTArray<MP4Interval<T>>* aIntersection) {
+ MOZ_ASSERT(IsNormalized(a0));
+ MOZ_ASSERT(IsNormalized(a1));
+ size_t i0 = 0;
+ size_t i1 = 0;
+ while (i0 < a0.Length() && i1 < a1.Length()) {
+ MP4Interval i = a0[i0].Intersection(a1[i1]);
+ if (i.Length()) {
+ aIntersection->AppendElement(i);
+ }
+ if (a0[i0].end < a1[i1].end) {
+ i0++;
+ // Assert that the array is sorted
+ MOZ_ASSERT(i0 == a0.Length() || a0[i0 - 1].start < a0[i0].start);
+ } else {
+ i1++;
+ // Assert that the array is sorted
+ MOZ_ASSERT(i1 == a1.Length() || a1[i1 - 1].start < a1[i1].start);
+ }
+ }
+ }
+
+ static bool IsNormalized(const nsTArray<MP4Interval<T>>& aIntervals) {
+ for (size_t i = 1; i < aIntervals.Length(); i++) {
+ if (aIntervals[i - 1].end >= aIntervals[i].start) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ struct Compare {
+ bool Equals(const MP4Interval<T>& a0, const MP4Interval<T>& a1) const {
+ return a0.start == a1.start && a0.end == a1.end;
+ }
+
+ bool LessThan(const MP4Interval<T>& a0, const MP4Interval<T>& a1) const {
+ return a0.start < a1.start;
+ }
+ };
+};
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/mp4/MP4Metadata.cpp b/dom/media/mp4/MP4Metadata.cpp
new file mode 100644
index 0000000000..88b4fb5c39
--- /dev/null
+++ b/dom/media/mp4/MP4Metadata.cpp
@@ -0,0 +1,507 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/Assertions.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/EndianUtils.h"
+#include "mozilla/Logging.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/UniquePtr.h"
+#include "VideoUtils.h"
+#include "MoofParser.h"
+#include "MP4Metadata.h"
+#include "ByteStream.h"
+#include "mp4parse.h"
+
+#include <limits>
+#include <stdint.h>
+#include <vector>
+
+using mozilla::media::TimeUnit;
+
+namespace mozilla {
+LazyLogModule gMP4MetadataLog("MP4Metadata");
+
+IndiceWrapper::IndiceWrapper(Mp4parseByteData& aRustIndice) {
+ mIndice.data = nullptr;
+ mIndice.length = aRustIndice.length;
+ mIndice.indices = aRustIndice.indices;
+}
+
+size_t IndiceWrapper::Length() const { return mIndice.length; }
+
+bool IndiceWrapper::GetIndice(size_t aIndex,
+ MP4SampleIndex::Indice& aIndice) const {
+ if (aIndex >= mIndice.length) {
+ MOZ_LOG(gMP4MetadataLog, LogLevel::Error, ("Index overflow in indice"));
+ return false;
+ }
+
+ const Mp4parseIndice* indice = &mIndice.indices[aIndex];
+ aIndice.start_offset = indice->start_offset;
+ aIndice.end_offset = indice->end_offset;
+ aIndice.start_composition = indice->start_composition;
+ aIndice.end_composition = indice->end_composition;
+ aIndice.start_decode = indice->start_decode;
+ aIndice.sync = indice->sync;
+ return true;
+}
+
+static const char* TrackTypeToString(mozilla::TrackInfo::TrackType aType) {
+ switch (aType) {
+ case mozilla::TrackInfo::kAudioTrack:
+ return "audio";
+ case mozilla::TrackInfo::kVideoTrack:
+ return "video";
+ default:
+ return "unknown";
+ }
+}
+
+bool StreamAdaptor::Read(uint8_t* buffer, uintptr_t size, size_t* bytes_read) {
+ if (!mOffset.isValid()) {
+ MOZ_LOG(gMP4MetadataLog, LogLevel::Error,
+ ("Overflow in source stream offset"));
+ return false;
+ }
+ bool rv = mSource->ReadAt(mOffset.value(), buffer, size, bytes_read);
+ if (rv) {
+ mOffset += *bytes_read;
+ }
+ return rv;
+}
+
+// Wrapper to allow rust to call our read adaptor.
+static intptr_t read_source(uint8_t* buffer, uintptr_t size, void* userdata) {
+ MOZ_ASSERT(buffer);
+ MOZ_ASSERT(userdata);
+
+ auto source = reinterpret_cast<StreamAdaptor*>(userdata);
+ size_t bytes_read = 0;
+ bool rv = source->Read(buffer, size, &bytes_read);
+ if (!rv) {
+ MOZ_LOG(gMP4MetadataLog, LogLevel::Warning, ("Error reading source data"));
+ return -1;
+ }
+ return bytes_read;
+}
+
+MP4Metadata::MP4Metadata(ByteStream* aSource)
+ : mSource(aSource), mSourceAdaptor(aSource) {
+ DDLINKCHILD("source", aSource);
+}
+
+MP4Metadata::~MP4Metadata() = default;
+
+nsresult MP4Metadata::Parse() {
+ Mp4parseIo io = {read_source, &mSourceAdaptor};
+ Mp4parseParser* parser = nullptr;
+ Mp4parseStatus status = mp4parse_new(&io, &parser);
+ if (status == MP4PARSE_STATUS_OK && parser) {
+ mParser.reset(parser);
+ MOZ_ASSERT(mParser);
+ } else {
+ MOZ_ASSERT(!mParser);
+ MOZ_LOG(gMP4MetadataLog, LogLevel::Debug,
+ ("Parse failed, return code %d\n", status));
+ return status == MP4PARSE_STATUS_OOM ? NS_ERROR_OUT_OF_MEMORY
+ : NS_ERROR_DOM_MEDIA_METADATA_ERR;
+ }
+
+ UpdateCrypto();
+
+ return NS_OK;
+}
+
+void MP4Metadata::UpdateCrypto() {
+ Mp4parsePsshInfo info = {};
+ if (mp4parse_get_pssh_info(mParser.get(), &info) != MP4PARSE_STATUS_OK) {
+ return;
+ }
+
+ if (info.data.length == 0) {
+ return;
+ }
+
+ mCrypto.Update(info.data.data, info.data.length);
+}
+
+bool TrackTypeEqual(TrackInfo::TrackType aLHS, Mp4parseTrackType aRHS) {
+ switch (aLHS) {
+ case TrackInfo::kAudioTrack:
+ return aRHS == MP4PARSE_TRACK_TYPE_AUDIO;
+ case TrackInfo::kVideoTrack:
+ return aRHS == MP4PARSE_TRACK_TYPE_VIDEO;
+ default:
+ return false;
+ }
+}
+
+MP4Metadata::ResultAndTrackCount MP4Metadata::GetNumberTracks(
+ mozilla::TrackInfo::TrackType aType) const {
+ uint32_t tracks;
+ auto rv = mp4parse_get_track_count(mParser.get(), &tracks);
+ if (rv != MP4PARSE_STATUS_OK) {
+ MOZ_LOG(gMP4MetadataLog, LogLevel::Warning,
+ ("rust parser error %d counting tracks", rv));
+ return {MediaResult(NS_ERROR_DOM_MEDIA_METADATA_ERR,
+ RESULT_DETAIL("Rust parser error %d", rv)),
+ MP4Metadata::NumberTracksError()};
+ }
+
+ uint32_t total = 0;
+ for (uint32_t i = 0; i < tracks; ++i) {
+ Mp4parseTrackInfo track_info;
+ rv = mp4parse_get_track_info(mParser.get(), i, &track_info);
+ if (rv != MP4PARSE_STATUS_OK) {
+ continue;
+ }
+
+ if (track_info.track_type == MP4PARSE_TRACK_TYPE_AUDIO) {
+ Mp4parseTrackAudioInfo audio;
+ auto rv = mp4parse_get_track_audio_info(mParser.get(), i, &audio);
+ if (rv != MP4PARSE_STATUS_OK) {
+ MOZ_LOG(gMP4MetadataLog, LogLevel::Warning,
+ ("mp4parse_get_track_audio_info returned error %d", rv));
+ continue;
+ }
+ MOZ_DIAGNOSTIC_ASSERT(audio.sample_info_count > 0,
+ "Must have at least one audio sample info");
+ if (audio.sample_info_count == 0) {
+ return {
+ MediaResult(
+ NS_ERROR_DOM_MEDIA_METADATA_ERR,
+ RESULT_DETAIL(
+ "Got 0 audio sample info while checking number tracks")),
+ MP4Metadata::NumberTracksError()};
+ }
+ // We assume the codec of the first sample info is representative of the
+ // whole track and skip it if we don't recognize the codec.
+ if (audio.sample_info[0].codec_type == MP4PARSE_CODEC_UNKNOWN) {
+ continue;
+ }
+ } else if (track_info.track_type == MP4PARSE_TRACK_TYPE_VIDEO) {
+ Mp4parseTrackVideoInfo video;
+ auto rv = mp4parse_get_track_video_info(mParser.get(), i, &video);
+ if (rv != MP4PARSE_STATUS_OK) {
+ MOZ_LOG(gMP4MetadataLog, LogLevel::Warning,
+ ("mp4parse_get_track_video_info returned error %d", rv));
+ continue;
+ }
+ MOZ_DIAGNOSTIC_ASSERT(video.sample_info_count > 0,
+ "Must have at least one video sample info");
+ if (video.sample_info_count == 0) {
+ return {
+ MediaResult(
+ NS_ERROR_DOM_MEDIA_METADATA_ERR,
+ RESULT_DETAIL(
+ "Got 0 video sample info while checking number tracks")),
+ MP4Metadata::NumberTracksError()};
+ }
+ // We assume the codec of the first sample info is representative of the
+ // whole track and skip it if we don't recognize the codec.
+ if (video.sample_info[0].codec_type == MP4PARSE_CODEC_UNKNOWN) {
+ continue;
+ }
+ } else {
+ // Only audio and video are supported
+ continue;
+ }
+ if (TrackTypeEqual(aType, track_info.track_type)) {
+ total += 1;
+ }
+ }
+
+ MOZ_LOG(gMP4MetadataLog, LogLevel::Info,
+ ("%s tracks found: %u", TrackTypeToString(aType), total));
+
+ return {NS_OK, total};
+}
+
+Maybe<uint32_t> MP4Metadata::TrackTypeToGlobalTrackIndex(
+ mozilla::TrackInfo::TrackType aType, size_t aTrackNumber) const {
+ uint32_t tracks;
+ auto rv = mp4parse_get_track_count(mParser.get(), &tracks);
+ if (rv != MP4PARSE_STATUS_OK) {
+ return Nothing();
+ }
+
+ /* The MP4Metadata API uses a per-TrackType index of tracks, but mp4parse
+ (and libstagefright) use a global track index. Convert the index by
+ counting the tracks of the requested type and returning the global
+ track index when a match is found. */
+ uint32_t perType = 0;
+ for (uint32_t i = 0; i < tracks; ++i) {
+ Mp4parseTrackInfo track_info;
+ rv = mp4parse_get_track_info(mParser.get(), i, &track_info);
+ if (rv != MP4PARSE_STATUS_OK) {
+ continue;
+ }
+ if (TrackTypeEqual(aType, track_info.track_type)) {
+ if (perType == aTrackNumber) {
+ return Some(i);
+ }
+ perType += 1;
+ }
+ }
+
+ return Nothing();
+}
+
+MP4Metadata::ResultAndTrackInfo MP4Metadata::GetTrackInfo(
+ mozilla::TrackInfo::TrackType aType, size_t aTrackNumber) const {
+ Maybe<uint32_t> trackIndex = TrackTypeToGlobalTrackIndex(aType, aTrackNumber);
+ if (trackIndex.isNothing()) {
+ return {MediaResult(NS_ERROR_DOM_MEDIA_METADATA_ERR,
+ RESULT_DETAIL("No %s tracks", TrackTypeToStr(aType))),
+ nullptr};
+ }
+
+ Mp4parseTrackInfo info;
+ auto rv = mp4parse_get_track_info(mParser.get(), trackIndex.value(), &info);
+ if (rv != MP4PARSE_STATUS_OK) {
+ MOZ_LOG(gMP4MetadataLog, LogLevel::Warning,
+ ("mp4parse_get_track_info returned %d", rv));
+ return {MediaResult(NS_ERROR_DOM_MEDIA_METADATA_ERR,
+ RESULT_DETAIL("Cannot find %s track #%zu",
+ TrackTypeToStr(aType), aTrackNumber)),
+ nullptr};
+ }
+#ifdef DEBUG
+ bool haveSampleInfo = false;
+ const char* codecString = "unrecognized";
+ Mp4parseCodec codecType = MP4PARSE_CODEC_UNKNOWN;
+ if (info.track_type == MP4PARSE_TRACK_TYPE_AUDIO) {
+ Mp4parseTrackAudioInfo audio;
+ auto rv = mp4parse_get_track_audio_info(mParser.get(), trackIndex.value(),
+ &audio);
+ if (rv == MP4PARSE_STATUS_OK && audio.sample_info_count > 0) {
+ codecType = audio.sample_info[0].codec_type;
+ haveSampleInfo = true;
+ }
+ } else if (info.track_type == MP4PARSE_TRACK_TYPE_VIDEO) {
+ Mp4parseTrackVideoInfo video;
+ auto rv = mp4parse_get_track_video_info(mParser.get(), trackIndex.value(),
+ &video);
+ if (rv == MP4PARSE_STATUS_OK && video.sample_info_count > 0) {
+ codecType = video.sample_info[0].codec_type;
+ haveSampleInfo = true;
+ }
+ }
+ if (haveSampleInfo) {
+ switch (codecType) {
+ case MP4PARSE_CODEC_UNKNOWN:
+ codecString = "unknown";
+ break;
+ case MP4PARSE_CODEC_AAC:
+ codecString = "aac";
+ break;
+ case MP4PARSE_CODEC_OPUS:
+ codecString = "opus";
+ break;
+ case MP4PARSE_CODEC_FLAC:
+ codecString = "flac";
+ break;
+ case MP4PARSE_CODEC_ALAC:
+ codecString = "alac";
+ break;
+ case MP4PARSE_CODEC_H263:
+ codecString = "h.263";
+ break;
+ case MP4PARSE_CODEC_AVC:
+ codecString = "h.264";
+ break;
+ case MP4PARSE_CODEC_VP9:
+ codecString = "vp9";
+ break;
+ case MP4PARSE_CODEC_AV1:
+ codecString = "av1";
+ break;
+ case MP4PARSE_CODEC_MP3:
+ codecString = "mp3";
+ break;
+ case MP4PARSE_CODEC_MP4V:
+ codecString = "mp4v";
+ break;
+ case MP4PARSE_CODEC_JPEG:
+ codecString = "jpeg";
+ break;
+ case MP4PARSE_CODEC_AC3:
+ codecString = "ac-3";
+ break;
+ case MP4PARSE_CODEC_EC3:
+ codecString = "ec-3";
+ break;
+ }
+ }
+ MOZ_LOG(gMP4MetadataLog, LogLevel::Debug,
+ ("track codec %s (%u)\n", codecString, codecType));
+#endif
+
+ Mp4parseTrackInfo track_info;
+ rv = mp4parse_get_track_info(mParser.get(), trackIndex.value(), &track_info);
+ if (rv != MP4PARSE_STATUS_OK) {
+ MOZ_LOG(gMP4MetadataLog, LogLevel::Warning,
+ ("mp4parse_get_track_info returned error %d", rv));
+ return {MediaResult(NS_ERROR_DOM_MEDIA_METADATA_ERR,
+ RESULT_DETAIL("Cannot parse %s track #%zu",
+ TrackTypeToStr(aType), aTrackNumber)),
+ nullptr};
+ }
+
+ uint32_t timeScale = info.time_scale;
+
+ // This specialization interface is wild.
+ UniquePtr<mozilla::TrackInfo> e;
+ switch (aType) {
+ case TrackInfo::TrackType::kAudioTrack: {
+ Mp4parseTrackAudioInfo audio;
+ auto rv = mp4parse_get_track_audio_info(mParser.get(), trackIndex.value(),
+ &audio);
+ if (rv != MP4PARSE_STATUS_OK) {
+ MOZ_LOG(gMP4MetadataLog, LogLevel::Warning,
+ ("mp4parse_get_track_audio_info returned error %d", rv));
+ return {MediaResult(NS_ERROR_DOM_MEDIA_METADATA_ERR,
+ RESULT_DETAIL("Cannot parse %s track #%zu",
+ TrackTypeToStr(aType), aTrackNumber)),
+ nullptr};
+ }
+
+ auto indices = GetTrackIndice(info.track_id);
+ if (!indices.Ref()) {
+ // non fatal
+ MOZ_LOG(gMP4MetadataLog, LogLevel::Warning,
+ ("Can't get index table for audio track, duration might be "
+ "slightly incorrect"));
+ }
+ auto track = mozilla::MakeUnique<MP4AudioInfo>();
+ MediaResult updateStatus =
+ track->Update(&info, &audio, indices.Ref().get());
+ if (NS_FAILED(updateStatus)) {
+ MOZ_LOG(gMP4MetadataLog, LogLevel::Warning,
+ ("Updating audio track failed with %s",
+ updateStatus.Message().get()));
+ return {MediaResult(NS_ERROR_DOM_MEDIA_METADATA_ERR,
+ RESULT_DETAIL(
+ "Failed to update %s track #%zu with error: %s",
+ TrackTypeToStr(aType), aTrackNumber,
+ updateStatus.Message().get())),
+ nullptr};
+ }
+ e = std::move(track);
+ } break;
+ case TrackInfo::TrackType::kVideoTrack: {
+ Mp4parseTrackVideoInfo video;
+ auto rv = mp4parse_get_track_video_info(mParser.get(), trackIndex.value(),
+ &video);
+ if (rv != MP4PARSE_STATUS_OK) {
+ MOZ_LOG(gMP4MetadataLog, LogLevel::Warning,
+ ("mp4parse_get_track_video_info returned error %d", rv));
+ return {MediaResult(NS_ERROR_DOM_MEDIA_METADATA_ERR,
+ RESULT_DETAIL("Cannot parse %s track #%zu",
+ TrackTypeToStr(aType), aTrackNumber)),
+ nullptr};
+ }
+ auto track = mozilla::MakeUnique<MP4VideoInfo>();
+ MediaResult updateStatus = track->Update(&info, &video);
+ if (NS_FAILED(updateStatus)) {
+ MOZ_LOG(gMP4MetadataLog, LogLevel::Warning,
+ ("Updating video track failed with %s",
+ updateStatus.Message().get()));
+ return {MediaResult(NS_ERROR_DOM_MEDIA_METADATA_ERR,
+ RESULT_DETAIL(
+ "Failed to update %s track #%zu with error: %s",
+ TrackTypeToStr(aType), aTrackNumber,
+ updateStatus.Message().get())),
+ nullptr};
+ }
+ e = std::move(track);
+ } break;
+ default:
+ MOZ_LOG(gMP4MetadataLog, LogLevel::Warning,
+ ("unhandled track type %d", aType));
+ return {MediaResult(NS_ERROR_DOM_MEDIA_METADATA_ERR,
+ RESULT_DETAIL("Cannot handle %s track #%zu",
+ TrackTypeToStr(aType), aTrackNumber)),
+ nullptr};
+ }
+
+ e->mTimeScale = timeScale;
+
+ // No duration in track, use fragment_duration.
+ if (e && !e->mDuration.IsPositive()) {
+ Mp4parseFragmentInfo fragmentInfo;
+ auto rv = mp4parse_get_fragment_info(mParser.get(), &fragmentInfo);
+ if (rv == MP4PARSE_STATUS_OK) {
+ // This doesn't use the time scale of the track, but the time scale
+ // indicated in the mvhd box
+ e->mDuration = TimeUnit(fragmentInfo.fragment_duration,
+ AssertedCast<int64_t>(fragmentInfo.time_scale));
+ }
+ }
+
+ if (e && e->IsValid()) {
+ return {NS_OK, std::move(e)};
+ }
+ MOZ_LOG(gMP4MetadataLog, LogLevel::Debug, ("TrackInfo didn't validate"));
+
+ return {MediaResult(NS_ERROR_DOM_MEDIA_METADATA_ERR,
+ RESULT_DETAIL("Invalid %s track #%zu",
+ TrackTypeToStr(aType), aTrackNumber)),
+ nullptr};
+}
+
+bool MP4Metadata::CanSeek() const { return true; }
+
+MP4Metadata::ResultAndCryptoFile MP4Metadata::Crypto() const {
+ return {NS_OK, &mCrypto};
+}
+
+MP4Metadata::ResultAndIndice MP4Metadata::GetTrackIndice(
+ uint32_t aTrackId) const {
+ Mp4parseByteData indiceRawData = {};
+
+ uint8_t fragmented = false;
+ auto rv = mp4parse_is_fragmented(mParser.get(), aTrackId, &fragmented);
+ if (rv != MP4PARSE_STATUS_OK) {
+ return {MediaResult(NS_ERROR_DOM_MEDIA_METADATA_ERR,
+ RESULT_DETAIL("Cannot parse whether track id %u is "
+ "fragmented, mp4parse_error=%d",
+ aTrackId, int(rv))),
+ nullptr};
+ }
+
+ if (!fragmented) {
+ rv = mp4parse_get_indice_table(mParser.get(), aTrackId, &indiceRawData);
+ if (rv != MP4PARSE_STATUS_OK) {
+ return {
+ MediaResult(NS_ERROR_DOM_MEDIA_METADATA_ERR,
+ RESULT_DETAIL("Cannot parse index table in track id %u, "
+ "mp4parse_error=%d",
+ aTrackId, int(rv))),
+ nullptr};
+ }
+ }
+
+ UniquePtr<IndiceWrapper> indice;
+ indice = mozilla::MakeUnique<IndiceWrapper>(indiceRawData);
+
+ return {NS_OK, std::move(indice)};
+}
+
+/*static*/ MP4Metadata::ResultAndByteBuffer MP4Metadata::Metadata(
+ ByteStream* aSource) {
+ auto parser = mozilla::MakeUnique<MoofParser>(
+ aSource, AsVariant(ParseAllTracks{}), false);
+ RefPtr<mozilla::MediaByteBuffer> buffer = parser->Metadata();
+ if (!buffer) {
+ return {MediaResult(NS_ERROR_DOM_MEDIA_METADATA_ERR,
+ RESULT_DETAIL("Cannot parse metadata")),
+ nullptr};
+ }
+ return {NS_OK, std::move(buffer)};
+}
+
+} // namespace mozilla
diff --git a/dom/media/mp4/MP4Metadata.h b/dom/media/mp4/MP4Metadata.h
new file mode 100644
index 0000000000..e900fbedc3
--- /dev/null
+++ b/dom/media/mp4/MP4Metadata.h
@@ -0,0 +1,116 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MP4METADATA_H_
+#define MP4METADATA_H_
+
+#include <type_traits>
+
+#include "mozilla/UniquePtr.h"
+#include "DecoderData.h"
+#include "MediaData.h"
+#include "MediaInfo.h"
+#include "MediaResult.h"
+#include "ByteStream.h"
+#include "mp4parse.h"
+#include "SampleIterator.h"
+
+namespace mozilla {
+
+DDLoggedTypeDeclName(MP4Metadata);
+
+// The memory owner in mIndice.indices is rust mp4 parser, so lifetime of this
+// class SHOULD NOT longer than rust parser.
+class IndiceWrapper {
+ public:
+ size_t Length() const;
+
+ bool GetIndice(size_t aIndex, MP4SampleIndex::Indice& aIndice) const;
+
+ explicit IndiceWrapper(Mp4parseByteData& aRustIndice);
+
+ protected:
+ Mp4parseByteData mIndice;
+};
+
+struct FreeMP4Parser {
+ void operator()(Mp4parseParser* aPtr) { mp4parse_free(aPtr); }
+};
+
+// Wrap an Stream to remember the read offset.
+class StreamAdaptor {
+ public:
+ explicit StreamAdaptor(ByteStream* aSource) : mSource(aSource), mOffset(0) {}
+
+ ~StreamAdaptor() = default;
+
+ bool Read(uint8_t* buffer, uintptr_t size, size_t* bytes_read);
+
+ private:
+ ByteStream* mSource;
+ CheckedInt<size_t> mOffset;
+};
+
+class MP4Metadata : public DecoderDoctorLifeLogger<MP4Metadata> {
+ public:
+ explicit MP4Metadata(ByteStream* aSource);
+ ~MP4Metadata();
+
+ // Simple template class containing a MediaResult and another type.
+ template <typename T>
+ class ResultAndType {
+ public:
+ template <typename M2, typename T2>
+ ResultAndType(M2&& aM, T2&& aT)
+ : mResult(std::forward<M2>(aM)), mT(std::forward<T2>(aT)) {}
+ ResultAndType(const ResultAndType&) = default;
+ ResultAndType& operator=(const ResultAndType&) = default;
+ ResultAndType(ResultAndType&&) = default;
+ ResultAndType& operator=(ResultAndType&&) = default;
+
+ mozilla::MediaResult& Result() { return mResult; }
+ T& Ref() { return mT; }
+
+ private:
+ mozilla::MediaResult mResult;
+ std::decay_t<T> mT;
+ };
+
+ using ResultAndByteBuffer = ResultAndType<RefPtr<mozilla::MediaByteBuffer>>;
+ static ResultAndByteBuffer Metadata(ByteStream* aSource);
+
+ static constexpr uint32_t NumberTracksError() { return UINT32_MAX; }
+ using ResultAndTrackCount = ResultAndType<uint32_t>;
+ ResultAndTrackCount GetNumberTracks(
+ mozilla::TrackInfo::TrackType aType) const;
+
+ using ResultAndTrackInfo =
+ ResultAndType<mozilla::UniquePtr<mozilla::TrackInfo>>;
+ ResultAndTrackInfo GetTrackInfo(mozilla::TrackInfo::TrackType aType,
+ size_t aTrackNumber) const;
+
+ bool CanSeek() const;
+
+ using ResultAndCryptoFile = ResultAndType<const CryptoFile*>;
+ ResultAndCryptoFile Crypto() const;
+
+ using ResultAndIndice = ResultAndType<mozilla::UniquePtr<IndiceWrapper>>;
+ ResultAndIndice GetTrackIndice(uint32_t aTrackId) const;
+
+ nsresult Parse();
+
+ private:
+ void UpdateCrypto();
+ Maybe<uint32_t> TrackTypeToGlobalTrackIndex(
+ mozilla::TrackInfo::TrackType aType, size_t aTrackNumber) const;
+
+ CryptoFile mCrypto;
+ RefPtr<ByteStream> mSource;
+ StreamAdaptor mSourceAdaptor;
+ mozilla::UniquePtr<Mp4parseParser, FreeMP4Parser> mParser;
+};
+
+} // namespace mozilla
+
+#endif // MP4METADATA_H_
diff --git a/dom/media/mp4/MoofParser.cpp b/dom/media/mp4/MoofParser.cpp
new file mode 100644
index 0000000000..71d3939502
--- /dev/null
+++ b/dom/media/mp4/MoofParser.cpp
@@ -0,0 +1,1286 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MoofParser.h"
+#include "Box.h"
+#include "SinfParser.h"
+#include <limits>
+#include "MP4Interval.h"
+
+#include "mozilla/CheckedInt.h"
+#include "mozilla/HelperMacros.h"
+#include "mozilla/Logging.h"
+
+#if defined(MOZ_FMP4)
+extern mozilla::LogModule* GetDemuxerLog();
+
+# define LOG_ERROR(name, arg, ...) \
+ MOZ_LOG( \
+ GetDemuxerLog(), mozilla::LogLevel::Error, \
+ (MOZ_STRINGIFY(name) "(%p)::%s: " arg, this, __func__, ##__VA_ARGS__))
+# define LOG_WARN(name, arg, ...) \
+ MOZ_LOG( \
+ GetDemuxerLog(), mozilla::LogLevel::Warning, \
+ (MOZ_STRINGIFY(name) "(%p)::%s: " arg, this, __func__, ##__VA_ARGS__))
+# define LOG_DEBUG(name, arg, ...) \
+ MOZ_LOG( \
+ GetDemuxerLog(), mozilla::LogLevel::Debug, \
+ (MOZ_STRINGIFY(name) "(%p)::%s: " arg, this, __func__, ##__VA_ARGS__))
+
+#else
+# define LOG_ERROR(...)
+# define LOG_WARN(...)
+# define LOG_DEBUG(...)
+#endif
+
+namespace mozilla {
+
+using TimeUnit = media::TimeUnit;
+
+const uint32_t kKeyIdSize = 16;
+
+bool MoofParser::RebuildFragmentedIndex(const MediaByteRangeSet& aByteRanges) {
+ BoxContext context(mSource, aByteRanges);
+ return RebuildFragmentedIndex(context);
+}
+
+bool MoofParser::RebuildFragmentedIndex(const MediaByteRangeSet& aByteRanges,
+ bool* aCanEvict) {
+ MOZ_ASSERT(aCanEvict);
+ if (*aCanEvict && mMoofs.Length() > 1) {
+ MOZ_ASSERT(mMoofs.Length() == mMediaRanges.Length());
+ mMoofs.RemoveElementsAt(0, mMoofs.Length() - 1);
+ mMediaRanges.RemoveElementsAt(0, mMediaRanges.Length() - 1);
+ *aCanEvict = true;
+ } else {
+ *aCanEvict = false;
+ }
+ return RebuildFragmentedIndex(aByteRanges);
+}
+
+bool MoofParser::RebuildFragmentedIndex(BoxContext& aContext) {
+ LOG_DEBUG(
+ Moof,
+ "Starting, mTrackParseMode=%s, track#=%" PRIu32
+ " (ignore if multitrack).",
+ mTrackParseMode.is<ParseAllTracks>() ? "multitrack" : "single track",
+ mTrackParseMode.is<ParseAllTracks>() ? 0
+ : mTrackParseMode.as<uint32_t>());
+ bool foundValidMoof = false;
+
+ for (Box box(&aContext, mOffset); box.IsAvailable(); box = box.Next()) {
+ if (box.IsType("moov") && mInitRange.IsEmpty()) {
+ mInitRange = MediaByteRange(0, box.Range().mEnd);
+ ParseMoov(box);
+ } else if (box.IsType("moof")) {
+ Moof moof(box, mTrackParseMode, mTrex, mMvhd, mMdhd, mEdts, mSinf,
+ &mLastDecodeTime, mIsAudio, mTracksEndCts);
+
+ if (!moof.IsValid() && !box.Next().IsAvailable()) {
+ // Moof isn't valid abort search for now.
+ LOG_WARN(Moof,
+ "Could not find valid moof, moof may not be complete yet.");
+ break;
+ }
+
+ if (!mMoofs.IsEmpty()) {
+ // Stitch time ranges together in the case of a (hopefully small) time
+ // range gap between moofs.
+ mMoofs.LastElement().FixRounding(moof);
+ }
+
+ mMediaRanges.AppendElement(moof.mRange);
+ mMoofs.AppendElement(std::move(moof));
+ foundValidMoof = true;
+ } else if (box.IsType("mdat") && !Moofs().IsEmpty()) {
+ // Check if we have all our data from last moof.
+ Moof& moof = Moofs().LastElement();
+ media::Interval<int64_t> datarange(moof.mMdatRange.mStart,
+ moof.mMdatRange.mEnd, 0);
+ media::Interval<int64_t> mdat(box.Range().mStart, box.Range().mEnd, 0);
+ if (datarange.Intersects(mdat)) {
+ mMediaRanges.LastElement() =
+ mMediaRanges.LastElement().Span(box.Range());
+ }
+ }
+ mOffset = box.NextOffset();
+ }
+ MOZ_ASSERT(mTrackParseMode.is<ParseAllTracks>() ||
+ mTrex.mTrackId == mTrackParseMode.as<uint32_t>(),
+ "If not parsing all tracks, mTrex should have the same track id "
+ "as the track being parsed.");
+ LOG_DEBUG(Moof, "Done, foundValidMoof=%s.",
+ foundValidMoof ? "true" : "false");
+ return foundValidMoof;
+}
+
+MediaByteRange MoofParser::FirstCompleteMediaHeader() {
+ if (Moofs().IsEmpty()) {
+ return MediaByteRange();
+ }
+ return Moofs()[0].mRange;
+}
+
+MediaByteRange MoofParser::FirstCompleteMediaSegment() {
+ for (uint32_t i = 0; i < mMediaRanges.Length(); i++) {
+ if (mMediaRanges[i].Contains(Moofs()[i].mMdatRange)) {
+ return mMediaRanges[i];
+ }
+ }
+ return MediaByteRange();
+}
+
+DDLoggedTypeDeclNameAndBase(BlockingStream, ByteStream);
+
+class BlockingStream : public ByteStream,
+ public DecoderDoctorLifeLogger<BlockingStream> {
+ public:
+ explicit BlockingStream(ByteStream* aStream) : mStream(aStream) {
+ DDLINKCHILD("stream", aStream);
+ }
+
+ bool ReadAt(int64_t offset, void* data, size_t size,
+ size_t* bytes_read) override {
+ return mStream->ReadAt(offset, data, size, bytes_read);
+ }
+
+ bool CachedReadAt(int64_t offset, void* data, size_t size,
+ size_t* bytes_read) override {
+ return mStream->ReadAt(offset, data, size, bytes_read);
+ }
+
+ virtual bool Length(int64_t* size) override { return mStream->Length(size); }
+
+ private:
+ RefPtr<ByteStream> mStream;
+};
+
+bool MoofParser::BlockingReadNextMoof() {
+ LOG_DEBUG(Moof, "Starting.");
+ int64_t length = std::numeric_limits<int64_t>::max();
+ mSource->Length(&length);
+ RefPtr<BlockingStream> stream = new BlockingStream(mSource);
+ MediaByteRangeSet byteRanges(MediaByteRange(0, length));
+
+ BoxContext context(stream, byteRanges);
+ for (Box box(&context, mOffset); box.IsAvailable(); box = box.Next()) {
+ if (box.IsType("moof")) {
+ MediaByteRangeSet parseByteRanges(
+ MediaByteRange(mOffset, box.Range().mEnd));
+ BoxContext parseContext(stream, parseByteRanges);
+ if (RebuildFragmentedIndex(parseContext)) {
+ LOG_DEBUG(Moof, "Succeeded on RebuildFragmentedIndex, returning true.");
+ return true;
+ }
+ }
+ }
+ LOG_DEBUG(Moof, "Couldn't read next moof, returning false.");
+ return false;
+}
+
+void MoofParser::ScanForMetadata(mozilla::MediaByteRange& aMoov) {
+ LOG_DEBUG(Moof, "Starting.");
+ int64_t length = std::numeric_limits<int64_t>::max();
+ mSource->Length(&length);
+ MediaByteRangeSet byteRanges;
+ byteRanges += MediaByteRange(0, length);
+ RefPtr<BlockingStream> stream = new BlockingStream(mSource);
+
+ BoxContext context(stream, byteRanges);
+ for (Box box(&context, mOffset); box.IsAvailable(); box = box.Next()) {
+ if (box.IsType("moov")) {
+ aMoov = box.Range();
+ break;
+ }
+ }
+ mInitRange = aMoov;
+ LOG_DEBUG(Moof,
+ "Done, mInitRange.mStart=%" PRIi64 ", mInitRange.mEnd=%" PRIi64,
+ mInitRange.mStart, mInitRange.mEnd);
+}
+
+already_AddRefed<mozilla::MediaByteBuffer> MoofParser::Metadata() {
+ LOG_DEBUG(Moof, "Starting.");
+ MediaByteRange moov;
+ ScanForMetadata(moov);
+ CheckedInt<MediaByteBuffer::size_type> moovLength = moov.Length();
+ if (!moovLength.isValid() || !moovLength.value()) {
+ // No moov, or cannot be used as array size.
+ LOG_WARN(Moof,
+ "Did not get usable moov length while trying to parse Metadata.");
+ return nullptr;
+ }
+
+ RefPtr<MediaByteBuffer> metadata = new MediaByteBuffer();
+ if (!metadata->SetLength(moovLength.value(), fallible)) {
+ LOG_ERROR(Moof, "OOM");
+ return nullptr;
+ }
+
+ RefPtr<BlockingStream> stream = new BlockingStream(mSource);
+ size_t read;
+ bool rv = stream->ReadAt(moov.mStart, metadata->Elements(),
+ moovLength.value(), &read);
+ if (!rv || read != moovLength.value()) {
+ LOG_WARN(Moof, "Failed to read moov while trying to parse Metadata.");
+ return nullptr;
+ }
+ LOG_DEBUG(Moof, "Done, found metadata.");
+ return metadata.forget();
+}
+
+MP4Interval<TimeUnit> MoofParser::GetCompositionRange(
+ const MediaByteRangeSet& aByteRanges) {
+ LOG_DEBUG(Moof, "Starting.");
+ MP4Interval<TimeUnit> compositionRange;
+ BoxContext context(mSource, aByteRanges);
+ for (size_t i = 0; i < mMoofs.Length(); i++) {
+ Moof& moof = mMoofs[i];
+ Box box(&context, moof.mRange.mStart);
+ if (box.IsAvailable()) {
+ compositionRange = compositionRange.Extents(moof.mTimeRange);
+ }
+ }
+ LOG_DEBUG(Moof,
+ "Done, compositionRange.start=%" PRIi64
+ ", compositionRange.end=%" PRIi64 ".",
+ compositionRange.start.ToMicroseconds(),
+ compositionRange.end.ToMicroseconds());
+ return compositionRange;
+}
+
+bool MoofParser::ReachedEnd() {
+ int64_t length;
+ return mSource->Length(&length) && mOffset == length;
+}
+
+void MoofParser::ParseMoov(Box& aBox) {
+ LOG_DEBUG(Moof, "Starting.");
+ for (Box box = aBox.FirstChild(); box.IsAvailable(); box = box.Next()) {
+ if (box.IsType("mvhd")) {
+ mMvhd = Mvhd(box);
+ } else if (box.IsType("trak")) {
+ ParseTrak(box);
+ } else if (box.IsType("mvex")) {
+ ParseMvex(box);
+ }
+ }
+ LOG_DEBUG(Moof, "Done.");
+}
+
+void MoofParser::ParseTrak(Box& aBox) {
+ LOG_DEBUG(Trak, "Starting.");
+ Tkhd tkhd;
+ for (Box box = aBox.FirstChild(); box.IsAvailable(); box = box.Next()) {
+ if (box.IsType("tkhd")) {
+ tkhd = Tkhd(box);
+ } else if (box.IsType("mdia")) {
+ if (mTrackParseMode.is<ParseAllTracks>() ||
+ tkhd.mTrackId == mTrackParseMode.as<uint32_t>()) {
+ ParseMdia(box);
+ }
+ } else if (box.IsType("edts") &&
+ (mTrackParseMode.is<ParseAllTracks>() ||
+ tkhd.mTrackId == mTrackParseMode.as<uint32_t>())) {
+ mEdts = Edts(box);
+ }
+ }
+ LOG_DEBUG(Trak, "Done.");
+}
+
+void MoofParser::ParseMdia(Box& aBox) {
+ LOG_DEBUG(Mdia, "Starting.");
+ for (Box box = aBox.FirstChild(); box.IsAvailable(); box = box.Next()) {
+ if (box.IsType("mdhd")) {
+ mMdhd = Mdhd(box);
+ } else if (box.IsType("minf")) {
+ ParseMinf(box);
+ }
+ }
+ LOG_DEBUG(Mdia, "Done.");
+}
+
+void MoofParser::ParseMvex(Box& aBox) {
+ LOG_DEBUG(Mvex, "Starting.");
+ for (Box box = aBox.FirstChild(); box.IsAvailable(); box = box.Next()) {
+ if (box.IsType("trex")) {
+ Trex trex = Trex(box);
+ if (mTrackParseMode.is<ParseAllTracks>() ||
+ trex.mTrackId == mTrackParseMode.as<uint32_t>()) {
+ mTrex = trex;
+ }
+ }
+ }
+ LOG_DEBUG(Mvex, "Done.");
+}
+
+void MoofParser::ParseMinf(Box& aBox) {
+ LOG_DEBUG(Minf, "Starting.");
+ for (Box box = aBox.FirstChild(); box.IsAvailable(); box = box.Next()) {
+ if (box.IsType("stbl")) {
+ ParseStbl(box);
+ }
+ }
+ LOG_DEBUG(Minf, "Done.");
+}
+
+void MoofParser::ParseStbl(Box& aBox) {
+ LOG_DEBUG(Stbl, "Starting.");
+ for (Box box = aBox.FirstChild(); box.IsAvailable(); box = box.Next()) {
+ if (box.IsType("stsd")) {
+ ParseStsd(box);
+ } else if (box.IsType("sgpd")) {
+ Sgpd sgpd(box);
+ if (sgpd.IsValid() && sgpd.mGroupingType == "seig") {
+ mTrackSampleEncryptionInfoEntries.Clear();
+ if (!mTrackSampleEncryptionInfoEntries.AppendElements(
+ sgpd.mEntries, mozilla::fallible)) {
+ LOG_ERROR(Stbl, "OOM");
+ return;
+ }
+ }
+ } else if (box.IsType("sbgp")) {
+ Sbgp sbgp(box);
+ if (sbgp.IsValid() && sbgp.mGroupingType == "seig") {
+ mTrackSampleToGroupEntries.Clear();
+ if (!mTrackSampleToGroupEntries.AppendElements(sbgp.mEntries,
+ mozilla::fallible)) {
+ LOG_ERROR(Stbl, "OOM");
+ return;
+ }
+ }
+ }
+ }
+ LOG_DEBUG(Stbl, "Done.");
+}
+
+void MoofParser::ParseStsd(Box& aBox) {
+ LOG_DEBUG(Stsd, "Starting.");
+ if (mTrackParseMode.is<ParseAllTracks>()) {
+ // It is not a sane operation to try and map sample description boxes from
+ // multiple tracks onto the parser, which is modeled around storing metadata
+ // for a single track.
+ LOG_DEBUG(Stsd, "Early return due to multitrack parser.");
+ return;
+ }
+ MOZ_ASSERT(
+ mSampleDescriptions.IsEmpty(),
+ "Shouldn't have any sample descriptions yet when starting to parse stsd");
+ uint32_t numberEncryptedEntries = 0;
+ for (Box box = aBox.FirstChild(); box.IsAvailable(); box = box.Next()) {
+ SampleDescriptionEntry sampleDescriptionEntry{false};
+ if (box.IsType("encv") || box.IsType("enca")) {
+ ParseEncrypted(box);
+ sampleDescriptionEntry.mIsEncryptedEntry = true;
+ numberEncryptedEntries++;
+ }
+ if (!mSampleDescriptions.AppendElement(sampleDescriptionEntry,
+ mozilla::fallible)) {
+ LOG_ERROR(Stsd, "OOM");
+ return;
+ }
+ }
+ if (mSampleDescriptions.IsEmpty()) {
+ LOG_WARN(Stsd,
+ "No sample description entries found while parsing Stsd! This "
+ "shouldn't happen, as the spec requires one for each track!");
+ }
+ if (numberEncryptedEntries > 1) {
+ LOG_WARN(Stsd,
+ "More than one encrypted sample description entry found while "
+ "parsing track! We don't expect this, and it will likely break "
+ "during fragment look up!");
+ }
+ LOG_DEBUG(Stsd,
+ "Done, numberEncryptedEntries=%" PRIu32
+ ", mSampleDescriptions.Length=%zu",
+ numberEncryptedEntries, mSampleDescriptions.Length());
+}
+
+void MoofParser::ParseEncrypted(Box& aBox) {
+ LOG_DEBUG(Moof, "Starting.");
+ for (Box box = aBox.FirstChild(); box.IsAvailable(); box = box.Next()) {
+ // Some MP4 files have been found to have multiple sinf boxes in the same
+ // enc* box. This does not match spec anyway, so just choose the first
+ // one that parses properly.
+ if (box.IsType("sinf")) {
+ mSinf = Sinf(box);
+
+ if (mSinf.IsValid()) {
+ break;
+ }
+ }
+ }
+ LOG_DEBUG(Moof, "Done.");
+}
+
+class CtsComparator {
+ public:
+ bool Equals(Sample* const aA, Sample* const aB) const {
+ return aA->mCompositionRange.start == aB->mCompositionRange.start;
+ }
+ bool LessThan(Sample* const aA, Sample* const aB) const {
+ return aA->mCompositionRange.start < aB->mCompositionRange.start;
+ }
+};
+
+Moof::Moof(Box& aBox, const TrackParseMode& aTrackParseMode, Trex& aTrex,
+ Mvhd& aMvhd, Mdhd& aMdhd, Edts& aEdts, Sinf& aSinf,
+ uint64_t* aDecodeTime, bool aIsAudio,
+ nsTArray<TrackEndCts>& aTracksEndCts)
+ : mRange(aBox.Range()),
+ mTfhd(aTrex),
+ // Do not reporting discontuities less than 35ms
+ mMaxRoundingError(TimeUnit::FromSeconds(0.035)) {
+ LOG_DEBUG(
+ Moof,
+ "Starting, aTrackParseMode=%s, track#=%" PRIu32
+ " (ignore if multitrack).",
+ aTrackParseMode.is<ParseAllTracks>() ? "multitrack" : "single track",
+ aTrackParseMode.is<ParseAllTracks>() ? 0
+ : aTrackParseMode.as<uint32_t>());
+ MOZ_ASSERT(aTrackParseMode.is<ParseAllTracks>() ||
+ aTrex.mTrackId == aTrackParseMode.as<uint32_t>(),
+ "If not parsing all tracks, aTrex should have the same track id "
+ "as the track being parsed.");
+ nsTArray<Box> psshBoxes;
+ for (Box box = aBox.FirstChild(); box.IsAvailable(); box = box.Next()) {
+ if (box.IsType("traf")) {
+ ParseTraf(box, aTrackParseMode, aTrex, aMvhd, aMdhd, aEdts, aSinf,
+ aDecodeTime, aIsAudio);
+ }
+ if (box.IsType("pssh")) {
+ psshBoxes.AppendElement(box);
+ }
+ }
+
+ // The EME spec requires that PSSH boxes which are contiguous in the
+ // file are dispatched to the media element in a single "encrypted" event.
+ // So append contiguous boxes here.
+ for (size_t i = 0; i < psshBoxes.Length(); ++i) {
+ Box box = psshBoxes[i];
+ if (i == 0 || box.Offset() != psshBoxes[i - 1].NextOffset()) {
+ mPsshes.AppendElement();
+ }
+ nsTArray<uint8_t>& pssh = mPsshes.LastElement();
+ pssh.AppendElements(std::move(box.ReadCompleteBox()));
+ }
+
+ if (IsValid()) {
+ if (mIndex.Length()) {
+ // Ensure the samples are contiguous with no gaps.
+ nsTArray<Sample*> ctsOrder;
+ for (auto& sample : mIndex) {
+ ctsOrder.AppendElement(&sample);
+ }
+ ctsOrder.Sort(CtsComparator());
+
+ for (size_t i = 1; i < ctsOrder.Length(); i++) {
+ ctsOrder[i - 1]->mCompositionRange.end =
+ ctsOrder[i]->mCompositionRange.start;
+ }
+
+ // Ensure that there are no gaps between the first sample in this
+ // Moof and the preceeding Moof.
+ if (!ctsOrder.IsEmpty()) {
+ bool found = false;
+ // Track ID of the track we're parsing.
+ const uint32_t trackId = aTrex.mTrackId;
+ // Find the previous CTS end time of Moof preceeding the Moofs we just
+ // parsed, for the track we're parsing.
+ for (auto& prevCts : aTracksEndCts) {
+ if (prevCts.mTrackId == trackId) {
+ // We ensure there are no gaps in samples' CTS between the last
+ // sample in a Moof, and the first sample in the next Moof, if
+ // they're within these many Microseconds of each other.
+ const TimeUnit CROSS_MOOF_CTS_MERGE_THRESHOLD =
+ TimeUnit::FromMicroseconds(1);
+ // We have previously parsed a Moof for this track. Smooth the gap
+ // between samples for this track across the Moof bounary.
+ if (ctsOrder[0]->mCompositionRange.start > prevCts.mCtsEndTime &&
+ ctsOrder[0]->mCompositionRange.start - prevCts.mCtsEndTime <=
+ CROSS_MOOF_CTS_MERGE_THRESHOLD) {
+ ctsOrder[0]->mCompositionRange.start = prevCts.mCtsEndTime;
+ }
+ prevCts.mCtsEndTime = ctsOrder.LastElement()->mCompositionRange.end;
+ found = true;
+ break;
+ }
+ }
+ if (!found) {
+ // We've not parsed a Moof for this track yet. Save its CTS end
+ // time for the next Moof we parse.
+ aTracksEndCts.AppendElement(TrackEndCts(
+ trackId, ctsOrder.LastElement()->mCompositionRange.end));
+ }
+ }
+
+ // In MP4, the duration of a sample is defined as the delta between two
+ // decode timestamps. The operation above has updated the duration of each
+ // sample as a Sample's duration is mCompositionRange.end -
+ // mCompositionRange.start MSE's TrackBuffersManager expects dts that
+ // increased by the sample's duration, so we rewrite the dts accordingly.
+ TimeUnit presentationDuration =
+ ctsOrder.LastElement()->mCompositionRange.end -
+ ctsOrder[0]->mCompositionRange.start;
+ auto decodeOffset =
+ aMdhd.ToTimeUnit((int64_t)*aDecodeTime - aEdts.mMediaStart);
+ auto offsetOffset = aMvhd.ToTimeUnit(aEdts.mEmptyOffset);
+ TimeUnit endDecodeTime =
+ (decodeOffset.isOk() && offsetOffset.isOk())
+ ? decodeOffset.unwrap() + offsetOffset.unwrap()
+ : TimeUnit::Zero(aMvhd.mTimescale);
+ TimeUnit decodeDuration = endDecodeTime - mIndex[0].mDecodeTime;
+ double adjust = 0.;
+ if (!presentationDuration.IsZero()) {
+ double num = decodeDuration.ToSeconds();
+ double denom = presentationDuration.ToSeconds();
+ if (denom != 0.) {
+ adjust = num / denom;
+ }
+ }
+
+ TimeUnit dtsOffset = mIndex[0].mDecodeTime;
+ TimeUnit compositionDuration(0, aMvhd.mTimescale);
+ // Adjust the dts, ensuring that the new adjusted dts will never be
+ // greater than decodeTime (the next moof's decode start time).
+ for (auto& sample : mIndex) {
+ sample.mDecodeTime = dtsOffset + compositionDuration.MultDouble(adjust);
+ compositionDuration += sample.mCompositionRange.Length();
+ }
+ mTimeRange =
+ MP4Interval<TimeUnit>(ctsOrder[0]->mCompositionRange.start,
+ ctsOrder.LastElement()->mCompositionRange.end);
+ }
+ ProcessCencAuxInfo(aSinf.mDefaultEncryptionType);
+ }
+ LOG_DEBUG(Moof, "Done.");
+}
+
+bool Moof::GetAuxInfo(AtomType aType,
+ FallibleTArray<MediaByteRange>* aByteRanges) {
+ LOG_DEBUG(Moof, "Starting.");
+ aByteRanges->Clear();
+
+ Saiz* saiz = nullptr;
+ for (int i = 0;; i++) {
+ if (i == mSaizs.Length()) {
+ LOG_DEBUG(Moof, "Could not find saiz matching aType. Returning false.");
+ return false;
+ }
+ if (mSaizs[i].mAuxInfoType == aType) {
+ saiz = &mSaizs[i];
+ break;
+ }
+ }
+ Saio* saio = nullptr;
+ for (int i = 0;; i++) {
+ if (i == mSaios.Length()) {
+ LOG_DEBUG(Moof, "Could not find saio matching aType. Returning false.");
+ return false;
+ }
+ if (mSaios[i].mAuxInfoType == aType) {
+ saio = &mSaios[i];
+ break;
+ }
+ }
+
+ if (saio->mOffsets.Length() == 1) {
+ if (!aByteRanges->SetCapacity(saiz->mSampleInfoSize.Length(),
+ mozilla::fallible)) {
+ LOG_ERROR(Moof, "OOM");
+ return false;
+ }
+ uint64_t offset = mRange.mStart + saio->mOffsets[0];
+ for (size_t i = 0; i < saiz->mSampleInfoSize.Length(); i++) {
+ if (!aByteRanges->AppendElement(
+ MediaByteRange(offset, offset + saiz->mSampleInfoSize[i]),
+ mozilla::fallible)) {
+ LOG_ERROR(Moof, "OOM");
+ return false;
+ }
+ offset += saiz->mSampleInfoSize[i];
+ }
+ LOG_DEBUG(
+ Moof,
+ "Saio has 1 entry. aByteRanges populated accordingly. Returning true.");
+ return true;
+ }
+
+ if (saio->mOffsets.Length() == saiz->mSampleInfoSize.Length()) {
+ if (!aByteRanges->SetCapacity(saiz->mSampleInfoSize.Length(),
+ mozilla::fallible)) {
+ LOG_ERROR(Moof, "OOM");
+ return false;
+ }
+ for (size_t i = 0; i < saio->mOffsets.Length(); i++) {
+ uint64_t offset = mRange.mStart + saio->mOffsets[i];
+ if (!aByteRanges->AppendElement(
+ MediaByteRange(offset, offset + saiz->mSampleInfoSize[i]),
+ mozilla::fallible)) {
+ LOG_ERROR(Moof, "OOM");
+ return false;
+ }
+ }
+ LOG_DEBUG(
+ Moof,
+ "Saio and saiz have same number of entries. aByteRanges populated "
+ "accordingly. Returning true.");
+ return true;
+ }
+
+ LOG_DEBUG(Moof,
+ "Moof::GetAuxInfo could not find any Aux info, returning false.");
+ return false;
+}
+
+bool Moof::ProcessCencAuxInfo(AtomType aScheme) {
+ LOG_DEBUG(Moof, "Starting.");
+ FallibleTArray<MediaByteRange> cencRanges;
+ if (!GetAuxInfo(aScheme, &cencRanges) ||
+ cencRanges.Length() != mIndex.Length()) {
+ LOG_DEBUG(Moof, "Couldn't find cenc aux info.");
+ return false;
+ }
+ for (int i = 0; i < cencRanges.Length(); i++) {
+ mIndex[i].mCencRange = cencRanges[i];
+ }
+ LOG_DEBUG(Moof, "Found cenc aux info and stored on index.");
+ return true;
+}
+
+void Moof::ParseTraf(Box& aBox, const TrackParseMode& aTrackParseMode,
+ Trex& aTrex, Mvhd& aMvhd, Mdhd& aMdhd, Edts& aEdts,
+ Sinf& aSinf, uint64_t* aDecodeTime, bool aIsAudio) {
+ LOG_DEBUG(
+ Traf,
+ "Starting, aTrackParseMode=%s, track#=%" PRIu32
+ " (ignore if multitrack).",
+ aTrackParseMode.is<ParseAllTracks>() ? "multitrack" : "single track",
+ aTrackParseMode.is<ParseAllTracks>() ? 0
+ : aTrackParseMode.as<uint32_t>());
+ MOZ_ASSERT(aDecodeTime);
+ MOZ_ASSERT(aTrackParseMode.is<ParseAllTracks>() ||
+ aTrex.mTrackId == aTrackParseMode.as<uint32_t>(),
+ "If not parsing all tracks, aTrex should have the same track id "
+ "as the track being parsed.");
+ Tfdt tfdt;
+
+ for (Box box = aBox.FirstChild(); box.IsAvailable(); box = box.Next()) {
+ if (box.IsType("tfhd")) {
+ mTfhd = Tfhd(box, aTrex);
+ } else if (aTrackParseMode.is<ParseAllTracks>() ||
+ mTfhd.mTrackId == aTrackParseMode.as<uint32_t>()) {
+ if (box.IsType("tfdt")) {
+ tfdt = Tfdt(box);
+ } else if (box.IsType("sgpd")) {
+ Sgpd sgpd(box);
+ if (sgpd.IsValid() && sgpd.mGroupingType == "seig") {
+ mFragmentSampleEncryptionInfoEntries.Clear();
+ if (!mFragmentSampleEncryptionInfoEntries.AppendElements(
+ sgpd.mEntries, mozilla::fallible)) {
+ LOG_ERROR(Moof, "OOM");
+ return;
+ }
+ }
+ } else if (box.IsType("sbgp")) {
+ Sbgp sbgp(box);
+ if (sbgp.IsValid() && sbgp.mGroupingType == "seig") {
+ mFragmentSampleToGroupEntries.Clear();
+ if (!mFragmentSampleToGroupEntries.AppendElements(
+ sbgp.mEntries, mozilla::fallible)) {
+ LOG_ERROR(Moof, "OOM");
+ return;
+ }
+ }
+ } else if (box.IsType("saiz")) {
+ if (!mSaizs.AppendElement(Saiz(box, aSinf.mDefaultEncryptionType),
+ mozilla::fallible)) {
+ LOG_ERROR(Moof, "OOM");
+ return;
+ }
+ } else if (box.IsType("saio")) {
+ if (!mSaios.AppendElement(Saio(box, aSinf.mDefaultEncryptionType),
+ mozilla::fallible)) {
+ LOG_ERROR(Moof, "OOM");
+ return;
+ }
+ }
+ }
+ }
+ if (aTrackParseMode.is<uint32_t>() &&
+ mTfhd.mTrackId != aTrackParseMode.as<uint32_t>()) {
+ LOG_DEBUG(Traf,
+ "Early return as not multitrack parser and track id didn't match "
+ "mTfhd.mTrackId=%" PRIu32,
+ mTfhd.mTrackId);
+ return;
+ }
+ // Now search for TRUN boxes.
+ uint64_t decodeTime =
+ tfdt.IsValid() ? tfdt.mBaseMediaDecodeTime : *aDecodeTime;
+ for (Box box = aBox.FirstChild(); box.IsAvailable(); box = box.Next()) {
+ if (box.IsType("trun")) {
+ if (ParseTrun(box, aMvhd, aMdhd, aEdts, &decodeTime, aIsAudio).isOk()) {
+ mValid = true;
+ } else {
+ LOG_WARN(Moof, "ParseTrun failed");
+ mValid = false;
+ break;
+ }
+ }
+ }
+ *aDecodeTime = decodeTime;
+ LOG_DEBUG(Traf, "Done, setting aDecodeTime=%." PRIu64 ".", decodeTime);
+}
+
+void Moof::FixRounding(const Moof& aMoof) {
+ TimeUnit gap = aMoof.mTimeRange.start - mTimeRange.end;
+ if (gap.IsPositive() && gap <= mMaxRoundingError) {
+ mTimeRange.end = aMoof.mTimeRange.start;
+ }
+}
+
+Result<Ok, nsresult> Moof::ParseTrun(Box& aBox, Mvhd& aMvhd, Mdhd& aMdhd,
+ Edts& aEdts, uint64_t* aDecodeTime,
+ bool aIsAudio) {
+ LOG_DEBUG(Trun, "Starting.");
+ if (!mTfhd.IsValid() || !aMvhd.IsValid() || !aMdhd.IsValid() ||
+ !aEdts.IsValid()) {
+ LOG_WARN(
+ Moof, "Invalid dependencies: mTfhd(%d) aMvhd(%d) aMdhd(%d) aEdts(%d)",
+ mTfhd.IsValid(), aMvhd.IsValid(), aMdhd.IsValid(), !aEdts.IsValid());
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ BoxReader reader(aBox);
+ if (!reader->CanReadType<uint32_t>()) {
+ LOG_WARN(Moof, "Incomplete Box (missing flags)");
+ return Err(NS_ERROR_FAILURE);
+ }
+ uint32_t flags;
+ MOZ_TRY_VAR(flags, reader->ReadU32());
+
+ if (!reader->CanReadType<uint32_t>()) {
+ LOG_WARN(Moof, "Incomplete Box (missing sampleCount)");
+ return Err(NS_ERROR_FAILURE);
+ }
+ uint32_t sampleCount;
+ MOZ_TRY_VAR(sampleCount, reader->ReadU32());
+ if (sampleCount == 0) {
+ LOG_DEBUG(Trun, "Trun with no samples, returning.");
+ return Ok();
+ }
+
+ uint64_t offset = mTfhd.mBaseDataOffset;
+ if (flags & 0x01) {
+ uint32_t tmp;
+ MOZ_TRY_VAR(tmp, reader->ReadU32());
+ offset += tmp;
+ }
+ uint32_t firstSampleFlags = mTfhd.mDefaultSampleFlags;
+ if (flags & 0x04) {
+ MOZ_TRY_VAR(firstSampleFlags, reader->ReadU32());
+ }
+ nsTArray<MP4Interval<TimeUnit>> timeRanges;
+ uint64_t decodeTime = *aDecodeTime;
+
+ if (!mIndex.SetCapacity(sampleCount, fallible)) {
+ LOG_ERROR(Moof, "Out of Memory");
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ for (size_t i = 0; i < sampleCount; i++) {
+ uint32_t sampleDuration = mTfhd.mDefaultSampleDuration;
+ if (flags & 0x100) {
+ MOZ_TRY_VAR(sampleDuration, reader->ReadU32());
+ }
+ uint32_t sampleSize = mTfhd.mDefaultSampleSize;
+ if (flags & 0x200) {
+ MOZ_TRY_VAR(sampleSize, reader->ReadU32());
+ }
+ uint32_t sampleFlags = i ? mTfhd.mDefaultSampleFlags : firstSampleFlags;
+ if (flags & 0x400) {
+ MOZ_TRY_VAR(sampleFlags, reader->ReadU32());
+ }
+ int32_t ctsOffset = 0;
+ if (flags & 0x800) {
+ MOZ_TRY_VAR(ctsOffset, reader->Read32());
+ }
+
+ if (sampleSize) {
+ Sample sample;
+ sample.mByteRange = MediaByteRange(offset, offset + sampleSize);
+ offset += sampleSize;
+
+ TimeUnit decodeOffset, emptyOffset, startCts, endCts;
+ MOZ_TRY_VAR(decodeOffset,
+ aMdhd.ToTimeUnit((int64_t)decodeTime - aEdts.mMediaStart));
+ MOZ_TRY_VAR(emptyOffset, aMvhd.ToTimeUnit(aEdts.mEmptyOffset));
+ sample.mDecodeTime = decodeOffset + emptyOffset;
+ MOZ_TRY_VAR(startCts, aMdhd.ToTimeUnit((int64_t)decodeTime + ctsOffset -
+ aEdts.mMediaStart));
+ MOZ_TRY_VAR(endCts, aMdhd.ToTimeUnit((int64_t)decodeTime + ctsOffset +
+ sampleDuration - aEdts.mMediaStart));
+ sample.mCompositionRange =
+ MP4Interval<TimeUnit>(startCts + emptyOffset, endCts + emptyOffset);
+ // Sometimes audio streams don't properly mark their samples as keyframes,
+ // because every audio sample is a keyframe.
+ sample.mSync = !(sampleFlags & 0x1010000) || aIsAudio;
+
+ // FIXME: Make this infallible after bug 968520 is done.
+ MOZ_ALWAYS_TRUE(mIndex.AppendElement(sample, fallible));
+
+ mMdatRange = mMdatRange.Span(sample.mByteRange);
+ }
+ decodeTime += sampleDuration;
+ }
+ TimeUnit roundTime;
+ MOZ_TRY_VAR(roundTime, aMdhd.ToTimeUnit(sampleCount));
+ mMaxRoundingError = roundTime + mMaxRoundingError;
+
+ *aDecodeTime = decodeTime;
+
+ LOG_DEBUG(Trun, "Done.");
+ return Ok();
+}
+
+Tkhd::Tkhd(Box& aBox) : mTrackId(0) {
+ mValid = Parse(aBox).isOk();
+ if (!mValid) {
+ LOG_WARN(Tkhd, "Parse failed");
+ }
+}
+
+Result<Ok, nsresult> Tkhd::Parse(Box& aBox) {
+ BoxReader reader(aBox);
+ uint32_t flags;
+ MOZ_TRY_VAR(flags, reader->ReadU32());
+ uint8_t version = flags >> 24;
+ if (version == 0) {
+ uint32_t creationTime, modificationTime, reserved, duration;
+ MOZ_TRY_VAR(creationTime, reader->ReadU32());
+ MOZ_TRY_VAR(modificationTime, reader->ReadU32());
+ MOZ_TRY_VAR(mTrackId, reader->ReadU32());
+ MOZ_TRY_VAR(reserved, reader->ReadU32());
+ MOZ_TRY_VAR(duration, reader->ReadU32());
+
+ (void)reserved;
+ NS_ASSERTION(!reserved, "reserved should be 0");
+
+ mCreationTime = creationTime;
+ mModificationTime = modificationTime;
+ mDuration = duration;
+ } else if (version == 1) {
+ uint32_t reserved;
+ MOZ_TRY_VAR(mCreationTime, reader->ReadU64());
+ MOZ_TRY_VAR(mModificationTime, reader->ReadU64());
+ MOZ_TRY_VAR(mTrackId, reader->ReadU32());
+ MOZ_TRY_VAR(reserved, reader->ReadU32());
+ (void)reserved;
+ NS_ASSERTION(!reserved, "reserved should be 0");
+ MOZ_TRY_VAR(mDuration, reader->ReadU64());
+ }
+ return Ok();
+}
+
+Mvhd::Mvhd(Box& aBox)
+ : mCreationTime(0), mModificationTime(0), mTimescale(0), mDuration(0) {
+ mValid = Parse(aBox).isOk();
+ if (!mValid) {
+ LOG_WARN(Mvhd, "Parse failed");
+ }
+}
+
+Result<Ok, nsresult> Mvhd::Parse(Box& aBox) {
+ BoxReader reader(aBox);
+
+ uint32_t flags;
+ MOZ_TRY_VAR(flags, reader->ReadU32());
+ uint8_t version = flags >> 24;
+
+ if (version == 0) {
+ uint32_t creationTime, modificationTime, duration;
+ MOZ_TRY_VAR(creationTime, reader->ReadU32());
+ MOZ_TRY_VAR(modificationTime, reader->ReadU32());
+ MOZ_TRY_VAR(mTimescale, reader->ReadU32());
+ MOZ_TRY_VAR(duration, reader->ReadU32());
+ mCreationTime = creationTime;
+ mModificationTime = modificationTime;
+ mDuration = duration;
+ } else if (version == 1) {
+ MOZ_TRY_VAR(mCreationTime, reader->ReadU64());
+ MOZ_TRY_VAR(mModificationTime, reader->ReadU64());
+ MOZ_TRY_VAR(mTimescale, reader->ReadU32());
+ MOZ_TRY_VAR(mDuration, reader->ReadU64());
+ } else {
+ return Err(NS_ERROR_FAILURE);
+ }
+ return Ok();
+}
+
+Mdhd::Mdhd(Box& aBox) : Mvhd(aBox) {}
+
+Trex::Trex(Box& aBox)
+ : mFlags(0),
+ mTrackId(0),
+ mDefaultSampleDescriptionIndex(0),
+ mDefaultSampleDuration(0),
+ mDefaultSampleSize(0),
+ mDefaultSampleFlags(0) {
+ mValid = Parse(aBox).isOk();
+ if (!mValid) {
+ LOG_WARN(Trex, "Parse failed");
+ }
+}
+
+Result<Ok, nsresult> Trex::Parse(Box& aBox) {
+ BoxReader reader(aBox);
+
+ MOZ_TRY_VAR(mFlags, reader->ReadU32());
+ MOZ_TRY_VAR(mTrackId, reader->ReadU32());
+ MOZ_TRY_VAR(mDefaultSampleDescriptionIndex, reader->ReadU32());
+ MOZ_TRY_VAR(mDefaultSampleDuration, reader->ReadU32());
+ MOZ_TRY_VAR(mDefaultSampleSize, reader->ReadU32());
+ MOZ_TRY_VAR(mDefaultSampleFlags, reader->ReadU32());
+
+ return Ok();
+}
+
+Tfhd::Tfhd(Box& aBox, Trex& aTrex) : Trex(aTrex), mBaseDataOffset(0) {
+ mValid = Parse(aBox).isOk();
+ if (!mValid) {
+ LOG_WARN(Tfhd, "Parse failed");
+ }
+}
+
+Result<Ok, nsresult> Tfhd::Parse(Box& aBox) {
+ MOZ_ASSERT(aBox.IsType("tfhd"));
+ MOZ_ASSERT(aBox.Parent()->IsType("traf"));
+ MOZ_ASSERT(aBox.Parent()->Parent()->IsType("moof"));
+
+ BoxReader reader(aBox);
+
+ MOZ_TRY_VAR(mFlags, reader->ReadU32());
+ MOZ_TRY_VAR(mTrackId, reader->ReadU32());
+ mBaseDataOffset = aBox.Parent()->Parent()->Offset();
+ if (mFlags & 0x01) {
+ MOZ_TRY_VAR(mBaseDataOffset, reader->ReadU64());
+ }
+ if (mFlags & 0x02) {
+ MOZ_TRY_VAR(mDefaultSampleDescriptionIndex, reader->ReadU32());
+ }
+ if (mFlags & 0x08) {
+ MOZ_TRY_VAR(mDefaultSampleDuration, reader->ReadU32());
+ }
+ if (mFlags & 0x10) {
+ MOZ_TRY_VAR(mDefaultSampleSize, reader->ReadU32());
+ }
+ if (mFlags & 0x20) {
+ MOZ_TRY_VAR(mDefaultSampleFlags, reader->ReadU32());
+ }
+
+ return Ok();
+}
+
+Tfdt::Tfdt(Box& aBox) : mBaseMediaDecodeTime(0) {
+ mValid = Parse(aBox).isOk();
+ if (!mValid) {
+ LOG_WARN(Tfdt, "Parse failed");
+ }
+}
+
+Result<Ok, nsresult> Tfdt::Parse(Box& aBox) {
+ BoxReader reader(aBox);
+
+ uint32_t flags;
+ MOZ_TRY_VAR(flags, reader->ReadU32());
+ uint8_t version = flags >> 24;
+ if (version == 0) {
+ uint32_t tmp;
+ MOZ_TRY_VAR(tmp, reader->ReadU32());
+ mBaseMediaDecodeTime = tmp;
+ } else if (version == 1) {
+ MOZ_TRY_VAR(mBaseMediaDecodeTime, reader->ReadU64());
+ }
+ return Ok();
+}
+
+Edts::Edts(Box& aBox) : mMediaStart(0), mEmptyOffset(0) {
+ mValid = Parse(aBox).isOk();
+ if (!mValid) {
+ LOG_WARN(Edts, "Parse failed");
+ }
+}
+
+Result<Ok, nsresult> Edts::Parse(Box& aBox) {
+ Box child = aBox.FirstChild();
+ if (!child.IsType("elst")) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ BoxReader reader(child);
+ uint32_t flags;
+ MOZ_TRY_VAR(flags, reader->ReadU32());
+ uint8_t version = flags >> 24;
+ bool emptyEntry = false;
+ uint32_t entryCount;
+ MOZ_TRY_VAR(entryCount, reader->ReadU32());
+ for (uint32_t i = 0; i < entryCount; i++) {
+ uint64_t segment_duration;
+ int64_t media_time;
+ if (version == 1) {
+ MOZ_TRY_VAR(segment_duration, reader->ReadU64());
+ MOZ_TRY_VAR(media_time, reader->Read64());
+ } else {
+ uint32_t tmp;
+ MOZ_TRY_VAR(tmp, reader->ReadU32());
+ segment_duration = tmp;
+ int32_t tmp2;
+ MOZ_TRY_VAR(tmp2, reader->Read32());
+ media_time = tmp2;
+ }
+ if (media_time == -1 && i) {
+ LOG_WARN(Edts, "Multiple empty edit, not handled");
+ } else if (media_time == -1) {
+ mEmptyOffset = segment_duration;
+ emptyEntry = true;
+ } else if (i > 1 || (i > 0 && !emptyEntry)) {
+ LOG_WARN(Edts,
+ "More than one edit entry, not handled. A/V sync will be wrong");
+ break;
+ } else {
+ mMediaStart = media_time;
+ }
+ MOZ_TRY(reader->ReadU32()); // media_rate_integer and media_rate_fraction
+ }
+
+ return Ok();
+}
+
+Saiz::Saiz(Box& aBox, AtomType aDefaultType)
+ : mAuxInfoType(aDefaultType), mAuxInfoTypeParameter(0) {
+ mValid = Parse(aBox).isOk();
+ if (!mValid) {
+ LOG_WARN(Saiz, "Parse failed");
+ }
+}
+
+Result<Ok, nsresult> Saiz::Parse(Box& aBox) {
+ BoxReader reader(aBox);
+
+ uint32_t flags;
+ MOZ_TRY_VAR(flags, reader->ReadU32());
+ if (flags & 1) {
+ MOZ_TRY_VAR(mAuxInfoType, reader->ReadU32());
+ MOZ_TRY_VAR(mAuxInfoTypeParameter, reader->ReadU32());
+ }
+ uint8_t defaultSampleInfoSize;
+ MOZ_TRY_VAR(defaultSampleInfoSize, reader->ReadU8());
+ uint32_t count;
+ MOZ_TRY_VAR(count, reader->ReadU32());
+ if (defaultSampleInfoSize) {
+ if (!mSampleInfoSize.SetLength(count, fallible)) {
+ LOG_ERROR(Saiz, "OOM");
+ return Err(NS_ERROR_FAILURE);
+ }
+ memset(mSampleInfoSize.Elements(), defaultSampleInfoSize,
+ mSampleInfoSize.Length());
+ } else {
+ if (!reader->ReadArray(mSampleInfoSize, count)) {
+ LOG_WARN(Saiz, "Incomplete Box (OOM or missing count:%u)", count);
+ return Err(NS_ERROR_FAILURE);
+ }
+ }
+ return Ok();
+}
+
+Saio::Saio(Box& aBox, AtomType aDefaultType)
+ : mAuxInfoType(aDefaultType), mAuxInfoTypeParameter(0) {
+ mValid = Parse(aBox).isOk();
+ if (!mValid) {
+ LOG_WARN(Saio, "Parse failed");
+ }
+}
+
+Result<Ok, nsresult> Saio::Parse(Box& aBox) {
+ BoxReader reader(aBox);
+
+ uint32_t flags;
+ MOZ_TRY_VAR(flags, reader->ReadU32());
+ uint8_t version = flags >> 24;
+ if (flags & 1) {
+ MOZ_TRY_VAR(mAuxInfoType, reader->ReadU32());
+ MOZ_TRY_VAR(mAuxInfoTypeParameter, reader->ReadU32());
+ }
+
+ size_t count;
+ MOZ_TRY_VAR(count, reader->ReadU32());
+ if (!mOffsets.SetCapacity(count, fallible)) {
+ LOG_ERROR(Saiz, "OOM");
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (version == 0) {
+ uint32_t offset;
+ for (size_t i = 0; i < count; i++) {
+ MOZ_TRY_VAR(offset, reader->ReadU32());
+ MOZ_ALWAYS_TRUE(mOffsets.AppendElement(offset, fallible));
+ }
+ } else {
+ uint64_t offset;
+ for (size_t i = 0; i < count; i++) {
+ MOZ_TRY_VAR(offset, reader->ReadU64());
+ MOZ_ALWAYS_TRUE(mOffsets.AppendElement(offset, fallible));
+ }
+ }
+ return Ok();
+}
+
+Sbgp::Sbgp(Box& aBox) : mGroupingTypeParam(0) {
+ mValid = Parse(aBox).isOk();
+ if (!mValid) {
+ LOG_WARN(Sbgp, "Parse failed");
+ }
+}
+
+Result<Ok, nsresult> Sbgp::Parse(Box& aBox) {
+ BoxReader reader(aBox);
+
+ uint32_t flags;
+ MOZ_TRY_VAR(flags, reader->ReadU32());
+ const uint8_t version = flags >> 24;
+
+ uint32_t type;
+ MOZ_TRY_VAR(type, reader->ReadU32());
+ mGroupingType = type;
+
+ if (version == 1) {
+ MOZ_TRY_VAR(mGroupingTypeParam, reader->ReadU32());
+ }
+
+ uint32_t count;
+ MOZ_TRY_VAR(count, reader->ReadU32());
+
+ for (uint32_t i = 0; i < count; i++) {
+ uint32_t sampleCount;
+ MOZ_TRY_VAR(sampleCount, reader->ReadU32());
+ uint32_t groupDescriptionIndex;
+ MOZ_TRY_VAR(groupDescriptionIndex, reader->ReadU32());
+
+ SampleToGroupEntry entry(sampleCount, groupDescriptionIndex);
+ if (!mEntries.AppendElement(entry, mozilla::fallible)) {
+ LOG_ERROR(Sbgp, "OOM");
+ return Err(NS_ERROR_FAILURE);
+ }
+ }
+ return Ok();
+}
+
+Sgpd::Sgpd(Box& aBox) {
+ mValid = Parse(aBox).isOk();
+ if (!mValid) {
+ LOG_WARN(Sgpd, "Parse failed");
+ }
+}
+
+Result<Ok, nsresult> Sgpd::Parse(Box& aBox) {
+ BoxReader reader(aBox);
+
+ uint32_t flags;
+ MOZ_TRY_VAR(flags, reader->ReadU32());
+ const uint8_t version = flags >> 24;
+
+ uint32_t type;
+ MOZ_TRY_VAR(type, reader->ReadU32());
+ mGroupingType = type;
+
+ const uint32_t entrySize = sizeof(uint32_t) + kKeyIdSize;
+ uint32_t defaultLength = 0;
+
+ if (version == 1) {
+ MOZ_TRY_VAR(defaultLength, reader->ReadU32());
+ if (defaultLength < entrySize && defaultLength != 0) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ }
+
+ uint32_t count;
+ MOZ_TRY_VAR(count, reader->ReadU32());
+
+ for (uint32_t i = 0; i < count; ++i) {
+ if (version == 1 && defaultLength == 0) {
+ uint32_t descriptionLength;
+ MOZ_TRY_VAR(descriptionLength, reader->ReadU32());
+ if (descriptionLength < entrySize) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ }
+
+ CencSampleEncryptionInfoEntry entry;
+ bool valid = entry.Init(reader).isOk();
+ if (!valid) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (!mEntries.AppendElement(entry, mozilla::fallible)) {
+ LOG_ERROR(Sgpd, "OOM");
+ return Err(NS_ERROR_FAILURE);
+ }
+ }
+ return Ok();
+}
+
+Result<Ok, nsresult> CencSampleEncryptionInfoEntry::Init(BoxReader& aReader) {
+ // Skip a reserved byte.
+ MOZ_TRY(aReader->ReadU8());
+
+ uint8_t pattern;
+ MOZ_TRY_VAR(pattern, aReader->ReadU8());
+ mCryptByteBlock = pattern >> 4;
+ mSkipByteBlock = pattern & 0x0f;
+
+ uint8_t isEncrypted;
+ MOZ_TRY_VAR(isEncrypted, aReader->ReadU8());
+ mIsEncrypted = isEncrypted != 0;
+
+ MOZ_TRY_VAR(mIVSize, aReader->ReadU8());
+
+ // Read the key id.
+ if (!mKeyId.SetLength(kKeyIdSize, fallible)) {
+ LOG_ERROR(CencSampleEncryptionInfoEntry, "OOM");
+ return Err(NS_ERROR_FAILURE);
+ }
+ for (uint32_t i = 0; i < kKeyIdSize; ++i) {
+ MOZ_TRY_VAR(mKeyId.ElementAt(i), aReader->ReadU8());
+ }
+
+ if (mIsEncrypted) {
+ if (mIVSize != 8 && mIVSize != 16) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ } else if (mIVSize != 0) {
+ // Protected content with 0 sized IV indicates a constant IV is present.
+ // This is used for the cbcs scheme.
+ uint8_t constantIVSize;
+ MOZ_TRY_VAR(constantIVSize, aReader->ReadU8());
+ if (constantIVSize != 8 && constantIVSize != 16) {
+ LOG_WARN(CencSampleEncryptionInfoEntry,
+ "Unexpected constantIVSize: %" PRIu8, constantIVSize);
+ return Err(NS_ERROR_FAILURE);
+ }
+ if (!mConsantIV.SetLength(constantIVSize, mozilla::fallible)) {
+ LOG_ERROR(CencSampleEncryptionInfoEntry, "OOM");
+ return Err(NS_ERROR_FAILURE);
+ }
+ for (uint32_t i = 0; i < constantIVSize; ++i) {
+ MOZ_TRY_VAR(mConsantIV.ElementAt(i), aReader->ReadU8());
+ }
+ }
+
+ return Ok();
+}
+} // namespace mozilla
+
+#undef LOG_DEBUG
+#undef LOG_WARN
+#undef LOG_ERROR
diff --git a/dom/media/mp4/MoofParser.h b/dom/media/mp4/MoofParser.h
new file mode 100644
index 0000000000..f644157308
--- /dev/null
+++ b/dom/media/mp4/MoofParser.h
@@ -0,0 +1,361 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOOF_PARSER_H_
+#define MOOF_PARSER_H_
+
+#include "mozilla/ResultExtensions.h"
+#include "TimeUnits.h"
+#include "mozilla/Variant.h"
+#include "Atom.h"
+#include "AtomType.h"
+#include "SinfParser.h"
+#include "ByteStream.h"
+#include "MP4Interval.h"
+#include "MediaResource.h"
+
+namespace mozilla {
+
+class Box;
+class BoxContext;
+class BoxReader;
+class Moof;
+
+// Used to track the CTS end time of the last sample of a track
+// in the preceeding Moof, so that we can smooth tracks' timestamps
+// across Moofs.
+struct TrackEndCts {
+ TrackEndCts(uint32_t aTrackId, const media::TimeUnit& aCtsEndTime)
+ : mTrackId(aTrackId), mCtsEndTime(aCtsEndTime) {}
+ uint32_t mTrackId;
+ media::TimeUnit mCtsEndTime;
+};
+
+class Mvhd : public Atom {
+ public:
+ Mvhd()
+ : mCreationTime(0), mModificationTime(0), mTimescale(0), mDuration(0) {}
+ explicit Mvhd(Box& aBox);
+
+ Result<media::TimeUnit, nsresult> ToTimeUnit(int64_t aTimescaleUnits) {
+ if (!mTimescale) {
+ NS_WARNING("invalid mTimescale");
+ return Err(NS_ERROR_FAILURE);
+ }
+ return media::TimeUnit(aTimescaleUnits, mTimescale);
+ }
+
+ uint64_t mCreationTime;
+ uint64_t mModificationTime;
+ uint32_t mTimescale;
+ uint64_t mDuration;
+
+ protected:
+ Result<Ok, nsresult> Parse(Box& aBox);
+};
+
+class Tkhd : public Mvhd {
+ public:
+ Tkhd() : mTrackId(0) {}
+ explicit Tkhd(Box& aBox);
+
+ uint32_t mTrackId;
+
+ protected:
+ Result<Ok, nsresult> Parse(Box& aBox);
+};
+
+class Mdhd : public Mvhd {
+ public:
+ Mdhd() = default;
+ explicit Mdhd(Box& aBox);
+};
+
+class Trex : public Atom {
+ public:
+ explicit Trex(uint32_t aTrackId)
+ : mFlags(0),
+ mTrackId(aTrackId),
+ mDefaultSampleDescriptionIndex(0),
+ mDefaultSampleDuration(0),
+ mDefaultSampleSize(0),
+ mDefaultSampleFlags(0) {}
+
+ explicit Trex(Box& aBox);
+
+ uint32_t mFlags;
+ uint32_t mTrackId;
+ uint32_t mDefaultSampleDescriptionIndex;
+ uint32_t mDefaultSampleDuration;
+ uint32_t mDefaultSampleSize;
+ uint32_t mDefaultSampleFlags;
+
+ protected:
+ Result<Ok, nsresult> Parse(Box& aBox);
+};
+
+class Tfhd : public Trex {
+ public:
+ explicit Tfhd(Trex& aTrex) : Trex(aTrex), mBaseDataOffset(0) {
+ mValid = aTrex.IsValid();
+ }
+ Tfhd(Box& aBox, Trex& aTrex);
+
+ uint64_t mBaseDataOffset;
+
+ protected:
+ Result<Ok, nsresult> Parse(Box& aBox);
+};
+
+class Tfdt : public Atom {
+ public:
+ Tfdt() : mBaseMediaDecodeTime(0) {}
+ explicit Tfdt(Box& aBox);
+
+ uint64_t mBaseMediaDecodeTime;
+
+ protected:
+ Result<Ok, nsresult> Parse(Box& aBox);
+};
+
+class Edts : public Atom {
+ public:
+ Edts() : mMediaStart(0), mEmptyOffset(0) {}
+ explicit Edts(Box& aBox);
+ virtual bool IsValid() override {
+ // edts is optional
+ return true;
+ }
+
+ int64_t mMediaStart;
+ int64_t mEmptyOffset;
+
+ protected:
+ Result<Ok, nsresult> Parse(Box& aBox);
+};
+
+struct Sample {
+ mozilla::MediaByteRange mByteRange;
+ mozilla::MediaByteRange mCencRange;
+ media::TimeUnit mDecodeTime;
+ MP4Interval<media::TimeUnit> mCompositionRange;
+ bool mSync;
+};
+
+class Saiz final : public Atom {
+ public:
+ Saiz(Box& aBox, AtomType aDefaultType);
+
+ AtomType mAuxInfoType;
+ uint32_t mAuxInfoTypeParameter;
+ FallibleTArray<uint8_t> mSampleInfoSize;
+
+ protected:
+ Result<Ok, nsresult> Parse(Box& aBox);
+};
+
+class Saio final : public Atom {
+ public:
+ Saio(Box& aBox, AtomType aDefaultType);
+
+ AtomType mAuxInfoType;
+ uint32_t mAuxInfoTypeParameter;
+ FallibleTArray<uint64_t> mOffsets;
+
+ protected:
+ Result<Ok, nsresult> Parse(Box& aBox);
+};
+
+struct SampleToGroupEntry {
+ public:
+ static const uint32_t kTrackGroupDescriptionIndexBase = 0;
+ static const uint32_t kFragmentGroupDescriptionIndexBase = 0x10000;
+
+ SampleToGroupEntry(uint32_t aSampleCount, uint32_t aGroupDescriptionIndex)
+ : mSampleCount(aSampleCount),
+ mGroupDescriptionIndex(aGroupDescriptionIndex) {}
+
+ uint32_t mSampleCount;
+ uint32_t mGroupDescriptionIndex;
+};
+
+class Sbgp final : public Atom // SampleToGroup box.
+{
+ public:
+ explicit Sbgp(Box& aBox);
+
+ AtomType mGroupingType;
+ uint32_t mGroupingTypeParam;
+ FallibleTArray<SampleToGroupEntry> mEntries;
+
+ protected:
+ Result<Ok, nsresult> Parse(Box& aBox);
+};
+
+// Stores information form CencSampleEncryptionInformationGroupEntry (seig).
+// Cenc here refers to the common encryption standard, rather than the specific
+// cenc scheme from that standard. This structure is used for all encryption
+// schemes. I.e. it is used for both cenc and cbcs, not just cenc.
+struct CencSampleEncryptionInfoEntry final {
+ public:
+ CencSampleEncryptionInfoEntry() = default;
+
+ Result<Ok, nsresult> Init(BoxReader& aReader);
+
+ bool mIsEncrypted = false;
+ uint8_t mIVSize = 0;
+ CopyableTArray<uint8_t> mKeyId;
+ uint8_t mCryptByteBlock = 0;
+ uint8_t mSkipByteBlock = 0;
+ CopyableTArray<uint8_t> mConsantIV;
+};
+
+class Sgpd final : public Atom // SampleGroupDescription box.
+{
+ public:
+ explicit Sgpd(Box& aBox);
+
+ AtomType mGroupingType;
+ FallibleTArray<CencSampleEncryptionInfoEntry> mEntries;
+
+ protected:
+ Result<Ok, nsresult> Parse(Box& aBox);
+};
+
+// Audio/video entries from the sample description box (stsd). We only need to
+// store if these are encrypted, so do not need a specialized class for
+// different audio and video data. Currently most of the parsing of these
+// entries is by the mp4parse-rust, but moof pasrser needs to know which of
+// these are encrypted when parsing the track fragment header (tfhd).
+struct SampleDescriptionEntry {
+ bool mIsEncryptedEntry = false;
+};
+
+// Used to indicate in variants if all tracks should be parsed.
+struct ParseAllTracks {};
+
+typedef Variant<ParseAllTracks, uint32_t> TrackParseMode;
+
+class Moof final : public Atom {
+ public:
+ Moof(Box& aBox, const TrackParseMode& aTrackParseMode, Trex& aTrex,
+ Mvhd& aMvhd, Mdhd& aMdhd, Edts& aEdts, Sinf& aSinf,
+ uint64_t* aDecodeTime, bool aIsAudio,
+ nsTArray<TrackEndCts>& aTracksEndCts);
+ bool GetAuxInfo(AtomType aType, FallibleTArray<MediaByteRange>* aByteRanges);
+ void FixRounding(const Moof& aMoof);
+
+ mozilla::MediaByteRange mRange;
+ mozilla::MediaByteRange mMdatRange;
+ MP4Interval<media::TimeUnit> mTimeRange;
+ FallibleTArray<Sample> mIndex;
+
+ FallibleTArray<CencSampleEncryptionInfoEntry>
+ mFragmentSampleEncryptionInfoEntries;
+ FallibleTArray<SampleToGroupEntry> mFragmentSampleToGroupEntries;
+
+ Tfhd mTfhd;
+ FallibleTArray<Saiz> mSaizs;
+ FallibleTArray<Saio> mSaios;
+ nsTArray<nsTArray<uint8_t>> mPsshes;
+
+ private:
+ // aDecodeTime is updated to the end of the parsed TRAF on return.
+ void ParseTraf(Box& aBox, const TrackParseMode& aTrackParseMode, Trex& aTrex,
+ Mvhd& aMvhd, Mdhd& aMdhd, Edts& aEdts, Sinf& aSinf,
+ uint64_t* aDecodeTime, bool aIsAudio);
+ // aDecodeTime is updated to the end of the parsed TRUN on return.
+ Result<Ok, nsresult> ParseTrun(Box& aBox, Mvhd& aMvhd, Mdhd& aMdhd,
+ Edts& aEdts, uint64_t* aDecodeTime,
+ bool aIsAudio);
+ // Process the sample auxiliary information used by common encryption.
+ // aScheme is used to select the appropriate auxiliary information and should
+ // be set based on the encryption scheme used by the track being processed.
+ // Note, the term cenc here refers to the standard, not the specific scheme
+ // from that standard. I.e. this function is used to handle up auxiliary
+ // information from the cenc and cbcs schemes.
+ bool ProcessCencAuxInfo(AtomType aScheme);
+ media::TimeUnit mMaxRoundingError;
+};
+
+DDLoggedTypeDeclName(MoofParser);
+
+class MoofParser : public DecoderDoctorLifeLogger<MoofParser> {
+ public:
+ MoofParser(ByteStream* aSource, const TrackParseMode& aTrackParseMode,
+ bool aIsAudio)
+ : mSource(aSource),
+ mOffset(0),
+ mTrex(aTrackParseMode.is<uint32_t>() ? aTrackParseMode.as<uint32_t>()
+ : 0),
+ mIsAudio(aIsAudio),
+ mLastDecodeTime(0),
+ mTrackParseMode(aTrackParseMode) {
+ // Setting mIsMultitrackParser is a nasty work around for calculating
+ // the composition range for MSE that causes the parser to parse multiple
+ // tracks. Ideally we'd store an array of tracks with different metadata
+ // for each.
+ DDLINKCHILD("source", aSource);
+ }
+ bool RebuildFragmentedIndex(const mozilla::MediaByteRangeSet& aByteRanges);
+ // If *aCanEvict is set to true. then will remove all moofs already parsed
+ // from index then rebuild the index. *aCanEvict is set to true upon return if
+ // some moofs were removed.
+ bool RebuildFragmentedIndex(const mozilla::MediaByteRangeSet& aByteRanges,
+ bool* aCanEvict);
+ bool RebuildFragmentedIndex(BoxContext& aContext);
+ MP4Interval<media::TimeUnit> GetCompositionRange(
+ const mozilla::MediaByteRangeSet& aByteRanges);
+ bool ReachedEnd();
+ void ParseMoov(Box& aBox);
+ void ParseTrak(Box& aBox);
+ void ParseMdia(Box& aBox);
+ void ParseMvex(Box& aBox);
+
+ void ParseMinf(Box& aBox);
+ void ParseStbl(Box& aBox);
+ void ParseStsd(Box& aBox);
+ void ParseEncrypted(Box& aBox);
+
+ bool BlockingReadNextMoof();
+
+ already_AddRefed<mozilla::MediaByteBuffer> Metadata();
+ MediaByteRange FirstCompleteMediaSegment();
+ MediaByteRange FirstCompleteMediaHeader();
+
+ mozilla::MediaByteRange mInitRange;
+ RefPtr<ByteStream> mSource;
+ uint64_t mOffset;
+ Mvhd mMvhd;
+ Mdhd mMdhd;
+ Trex mTrex;
+ Tfdt mTfdt;
+ Edts mEdts;
+ Sinf mSinf;
+
+ FallibleTArray<CencSampleEncryptionInfoEntry>
+ mTrackSampleEncryptionInfoEntries;
+ FallibleTArray<SampleToGroupEntry> mTrackSampleToGroupEntries;
+ FallibleTArray<SampleDescriptionEntry> mSampleDescriptions;
+
+ nsTArray<Moof>& Moofs() { return mMoofs; }
+
+ private:
+ void ScanForMetadata(mozilla::MediaByteRange& aMoov);
+ nsTArray<Moof> mMoofs;
+ nsTArray<MediaByteRange> mMediaRanges;
+ nsTArray<TrackEndCts> mTracksEndCts;
+ bool mIsAudio;
+ uint64_t mLastDecodeTime;
+ // Either a ParseAllTracks if in multitrack mode, or an integer representing
+ // the track_id for the track being parsed. If parsing a specific track, mTrex
+ // should have an id matching mTrackParseMode.as<uint32_t>(). In this case 0
+ // is a valid track id -- this is not allowed in the spec, but such mp4s
+ // appear in the wild. In the ParseAllTracks case, mTrex can have an arbitrary
+ // id based on the tracks being parsed.
+ const TrackParseMode mTrackParseMode;
+};
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/mp4/ResourceStream.cpp b/dom/media/mp4/ResourceStream.cpp
new file mode 100644
index 0000000000..ce2fb6f2f6
--- /dev/null
+++ b/dom/media/mp4/ResourceStream.cpp
@@ -0,0 +1,56 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ResourceStream.h"
+
+namespace mozilla {
+
+ResourceStream::ResourceStream(mozilla::MediaResource* aResource)
+ : mResource(aResource), mPinCount(0) {
+ MOZ_ASSERT(aResource);
+ DDLINKCHILD("resource", &mResource);
+}
+
+ResourceStream::~ResourceStream() { MOZ_ASSERT(mPinCount == 0); }
+
+bool ResourceStream::ReadAt(int64_t aOffset, void* aBuffer, size_t aCount,
+ size_t* aBytesRead) {
+ uint32_t sum = 0;
+ uint32_t bytesRead = 0;
+ do {
+ uint64_t offset = aOffset + sum;
+ char* buffer = reinterpret_cast<char*>(aBuffer) + sum;
+ uint32_t toRead = aCount - sum;
+ nsresult rv = mResource.ReadAt(offset, buffer, toRead, &bytesRead);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+ sum += bytesRead;
+ } while (sum < aCount && bytesRead > 0);
+
+ *aBytesRead = sum;
+ return true;
+}
+
+bool ResourceStream::CachedReadAt(int64_t aOffset, void* aBuffer, size_t aCount,
+ size_t* aBytesRead) {
+ nsresult rv = mResource.GetResource()->ReadFromCache(
+ reinterpret_cast<char*>(aBuffer), aOffset, aCount);
+ if (NS_FAILED(rv)) {
+ *aBytesRead = 0;
+ return false;
+ }
+ *aBytesRead = aCount;
+ return true;
+}
+
+bool ResourceStream::Length(int64_t* aSize) {
+ if (mResource.GetLength() < 0) return false;
+ *aSize = mResource.GetLength();
+ return true;
+}
+
+} // namespace mozilla
diff --git a/dom/media/mp4/ResourceStream.h b/dom/media/mp4/ResourceStream.h
new file mode 100644
index 0000000000..1aa59fdaed
--- /dev/null
+++ b/dom/media/mp4/ResourceStream.h
@@ -0,0 +1,48 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef RESOURCESTREAM_H_
+#define RESOURCESTREAM_H_
+
+#include "MediaResource.h"
+#include "ByteStream.h"
+#include "mozilla/RefPtr.h"
+
+namespace mozilla {
+
+DDLoggedTypeDeclNameAndBase(ResourceStream, ByteStream);
+
+class ResourceStream : public ByteStream,
+ public DecoderDoctorLifeLogger<ResourceStream> {
+ public:
+ explicit ResourceStream(mozilla::MediaResource* aResource);
+
+ virtual bool ReadAt(int64_t offset, void* aBuffer, size_t aCount,
+ size_t* aBytesRead) override;
+ virtual bool CachedReadAt(int64_t aOffset, void* aBuffer, size_t aCount,
+ size_t* aBytesRead) override;
+ virtual bool Length(int64_t* size) override;
+
+ void Pin() {
+ mResource.GetResource()->Pin();
+ ++mPinCount;
+ }
+
+ void Unpin() {
+ mResource.GetResource()->Unpin();
+ MOZ_ASSERT(mPinCount);
+ --mPinCount;
+ }
+
+ protected:
+ virtual ~ResourceStream();
+
+ private:
+ mozilla::MediaResourceIndex mResource;
+ uint32_t mPinCount;
+};
+
+} // namespace mozilla
+
+#endif // RESOURCESTREAM_H_
diff --git a/dom/media/mp4/SampleIterator.cpp b/dom/media/mp4/SampleIterator.cpp
new file mode 100644
index 0000000000..95fc8af457
--- /dev/null
+++ b/dom/media/mp4/SampleIterator.cpp
@@ -0,0 +1,712 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SampleIterator.h"
+
+#include <algorithm>
+#include <limits>
+
+#include "BufferReader.h"
+#include "mozilla/RefPtr.h"
+#include "MP4Interval.h"
+#include "MP4Metadata.h"
+#include "SinfParser.h"
+
+using namespace mozilla::media;
+
+namespace mozilla {
+
+class MOZ_STACK_CLASS RangeFinder {
+ public:
+ // Given that we're processing this in order we don't use a binary search
+ // to find the apropriate time range. Instead we search linearly from the
+ // last used point.
+ explicit RangeFinder(const MediaByteRangeSet& ranges)
+ : mRanges(ranges), mIndex(0) {
+ // Ranges must be normalised for this to work
+ }
+
+ bool Contains(const MediaByteRange& aByteRange);
+
+ private:
+ const MediaByteRangeSet& mRanges;
+ size_t mIndex;
+};
+
+bool RangeFinder::Contains(const MediaByteRange& aByteRange) {
+ if (mRanges.IsEmpty()) {
+ return false;
+ }
+
+ if (mRanges[mIndex].ContainsStrict(aByteRange)) {
+ return true;
+ }
+
+ if (aByteRange.mStart < mRanges[mIndex].mStart) {
+ // Search backwards
+ do {
+ if (!mIndex) {
+ return false;
+ }
+ --mIndex;
+ if (mRanges[mIndex].ContainsStrict(aByteRange)) {
+ return true;
+ }
+ } while (aByteRange.mStart < mRanges[mIndex].mStart);
+
+ return false;
+ }
+
+ while (aByteRange.mEnd > mRanges[mIndex].mEnd) {
+ if (mIndex == mRanges.Length() - 1) {
+ return false;
+ }
+ ++mIndex;
+ if (mRanges[mIndex].ContainsStrict(aByteRange)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+SampleIterator::SampleIterator(MP4SampleIndex* aIndex)
+ : mIndex(aIndex), mCurrentMoof(0), mCurrentSample(0) {
+ mIndex->RegisterIterator(this);
+}
+
+SampleIterator::~SampleIterator() { mIndex->UnregisterIterator(this); }
+
+bool SampleIterator::HasNext() { return !!Get(); }
+
+already_AddRefed<MediaRawData> SampleIterator::GetNext() {
+ Sample* s(Get());
+ if (!s) {
+ return nullptr;
+ }
+
+ int64_t length = std::numeric_limits<int64_t>::max();
+ mIndex->mSource->Length(&length);
+ if (s->mByteRange.mEnd > length) {
+ // We don't have this complete sample.
+ return nullptr;
+ }
+
+ RefPtr<MediaRawData> sample = new MediaRawData();
+ sample->mTimecode = s->mDecodeTime;
+ sample->mTime = s->mCompositionRange.start;
+ sample->mDuration = s->mCompositionRange.Length();
+ sample->mOffset = s->mByteRange.mStart;
+ sample->mKeyframe = s->mSync;
+
+ UniquePtr<MediaRawDataWriter> writer(sample->CreateWriter());
+ // Do the blocking read
+ if (!writer->SetSize(s->mByteRange.Length())) {
+ return nullptr;
+ }
+
+ size_t bytesRead;
+ if (!mIndex->mSource->ReadAt(sample->mOffset, writer->Data(), sample->Size(),
+ &bytesRead) ||
+ bytesRead != sample->Size()) {
+ return nullptr;
+ }
+
+ MoofParser* moofParser = mIndex->mMoofParser.get();
+ if (!moofParser) {
+ // File is not fragmented, we can't have crypto, just early return.
+ Next();
+ return sample.forget();
+ }
+
+ // We need to check if this moof has init data the CDM expects us to surface.
+ // This should happen when handling the first sample, even if that sample
+ // isn't encrypted (samples later in the moof may be).
+ if (mCurrentSample == 0) {
+ const nsTArray<Moof>& moofs = moofParser->Moofs();
+ const Moof* currentMoof = &moofs[mCurrentMoof];
+ if (!currentMoof->mPsshes.IsEmpty()) {
+ // This Moof contained crypto init data. Report that. We only report
+ // the init data on the Moof's first sample, to avoid reporting it more
+ // than once per Moof.
+ writer->mCrypto.mInitDatas.AppendElements(currentMoof->mPsshes);
+ writer->mCrypto.mInitDataType = u"cenc"_ns;
+ }
+ }
+
+ auto cryptoSchemeResult = GetEncryptionScheme();
+ if (cryptoSchemeResult.isErr()) {
+ // Log the error here in future.
+ return nullptr;
+ }
+ CryptoScheme cryptoScheme = cryptoSchemeResult.unwrap();
+ if (cryptoScheme == CryptoScheme::None) {
+ // No crypto to handle, early return.
+ Next();
+ return sample.forget();
+ }
+
+ writer->mCrypto.mCryptoScheme = cryptoScheme;
+ MOZ_ASSERT(writer->mCrypto.mCryptoScheme != CryptoScheme::None,
+ "Should have early returned if we don't have a crypto scheme!");
+ MOZ_ASSERT(writer->mCrypto.mKeyId.IsEmpty(),
+ "Sample should not already have a key ID");
+ MOZ_ASSERT(writer->mCrypto.mConstantIV.IsEmpty(),
+ "Sample should not already have a constant IV");
+ CencSampleEncryptionInfoEntry* sampleInfo = GetSampleEncryptionEntry();
+ if (sampleInfo) {
+ // Use sample group information if present, this supersedes track level
+ // information.
+ writer->mCrypto.mKeyId.AppendElements(sampleInfo->mKeyId);
+ writer->mCrypto.mIVSize = sampleInfo->mIVSize;
+ writer->mCrypto.mCryptByteBlock = sampleInfo->mCryptByteBlock;
+ writer->mCrypto.mSkipByteBlock = sampleInfo->mSkipByteBlock;
+ writer->mCrypto.mConstantIV.AppendElements(sampleInfo->mConsantIV);
+ } else {
+ // Use the crypto info from track metadata
+ writer->mCrypto.mKeyId.AppendElements(moofParser->mSinf.mDefaultKeyID, 16);
+ writer->mCrypto.mIVSize = moofParser->mSinf.mDefaultIVSize;
+ writer->mCrypto.mCryptByteBlock = moofParser->mSinf.mDefaultCryptByteBlock;
+ writer->mCrypto.mSkipByteBlock = moofParser->mSinf.mDefaultSkipByteBlock;
+ writer->mCrypto.mConstantIV.AppendElements(
+ moofParser->mSinf.mDefaultConstantIV);
+ }
+
+ if ((writer->mCrypto.mIVSize == 0 && writer->mCrypto.mConstantIV.IsEmpty()) ||
+ (writer->mCrypto.mIVSize != 0 && s->mCencRange.IsEmpty())) {
+ // If mIVSize == 0, this indicates that a constant IV is in use, thus we
+ // should have a non empty constant IV. Alternatively if IV size is non
+ // zero, we should have an IV for this sample, which we need to look up
+ // in mCencRange (which must then be non empty). If neither of these are
+ // true we have bad crypto data, so bail.
+ return nullptr;
+ }
+ // Parse auxiliary information if present
+ if (!s->mCencRange.IsEmpty()) {
+ // The size comes from an 8 bit field
+ AutoTArray<uint8_t, 256> cencAuxInfo;
+ cencAuxInfo.SetLength(s->mCencRange.Length());
+ if (!mIndex->mSource->ReadAt(s->mCencRange.mStart, cencAuxInfo.Elements(),
+ cencAuxInfo.Length(), &bytesRead) ||
+ bytesRead != cencAuxInfo.Length()) {
+ return nullptr;
+ }
+ BufferReader reader(cencAuxInfo);
+ if (!reader.ReadArray(writer->mCrypto.mIV, writer->mCrypto.mIVSize)) {
+ return nullptr;
+ }
+
+ // Parse the auxiliary information for subsample information
+ auto res = reader.ReadU16();
+ if (res.isOk() && res.unwrap() > 0) {
+ uint16_t count = res.unwrap();
+
+ if (reader.Remaining() < count * 6) {
+ return nullptr;
+ }
+
+ for (size_t i = 0; i < count; i++) {
+ auto res_16 = reader.ReadU16();
+ auto res_32 = reader.ReadU32();
+ if (res_16.isErr() || res_32.isErr()) {
+ return nullptr;
+ }
+ writer->mCrypto.mPlainSizes.AppendElement(res_16.unwrap());
+ writer->mCrypto.mEncryptedSizes.AppendElement(res_32.unwrap());
+ }
+ } else {
+ // No subsample information means the entire sample is encrypted.
+ writer->mCrypto.mPlainSizes.AppendElement(0);
+ writer->mCrypto.mEncryptedSizes.AppendElement(sample->Size());
+ }
+ }
+
+ Next();
+
+ return sample.forget();
+}
+
+SampleDescriptionEntry* SampleIterator::GetSampleDescriptionEntry() {
+ nsTArray<Moof>& moofs = mIndex->mMoofParser->Moofs();
+ Moof& currentMoof = moofs[mCurrentMoof];
+ uint32_t sampleDescriptionIndex =
+ currentMoof.mTfhd.mDefaultSampleDescriptionIndex;
+ // Mp4 indices start at 1, shift down 1 so we index our array correctly.
+ sampleDescriptionIndex--;
+ FallibleTArray<SampleDescriptionEntry>& sampleDescriptions =
+ mIndex->mMoofParser->mSampleDescriptions;
+ if (sampleDescriptionIndex >= sampleDescriptions.Length()) {
+ // The sample description index is invalid, the mp4 is malformed. Bail out.
+ return nullptr;
+ }
+ return &sampleDescriptions[sampleDescriptionIndex];
+}
+
+CencSampleEncryptionInfoEntry* SampleIterator::GetSampleEncryptionEntry() {
+ nsTArray<Moof>& moofs = mIndex->mMoofParser->Moofs();
+ Moof* currentMoof = &moofs[mCurrentMoof];
+ SampleToGroupEntry* sampleToGroupEntry = nullptr;
+
+ // Default to using the sample to group entries for the fragment, otherwise
+ // fall back to the sample to group entries for the track.
+ FallibleTArray<SampleToGroupEntry>* sampleToGroupEntries =
+ currentMoof->mFragmentSampleToGroupEntries.Length() != 0
+ ? &currentMoof->mFragmentSampleToGroupEntries
+ : &mIndex->mMoofParser->mTrackSampleToGroupEntries;
+
+ uint32_t seen = 0;
+
+ for (SampleToGroupEntry& entry : *sampleToGroupEntries) {
+ if (seen + entry.mSampleCount > mCurrentSample) {
+ sampleToGroupEntry = &entry;
+ break;
+ }
+ seen += entry.mSampleCount;
+ }
+
+ // ISO-14496-12 Section 8.9.2.3 and 8.9.4 : group description index
+ // (1) ranges from 1 to the number of sample group entries in the track
+ // level SampleGroupDescription Box, or (2) takes the value 0 to
+ // indicate that this sample is a member of no group, in this case, the
+ // sample is associated with the default values specified in
+ // TrackEncryption Box, or (3) starts at 0x10001, i.e. the index value
+ // 1, with the value 1 in the top 16 bits, to reference fragment-local
+ // SampleGroupDescription Box.
+
+ // According to the spec, ISO-14496-12, the sum of the sample counts in this
+ // box should be equal to the total number of samples, and, if less, the
+ // reader should behave as if an extra SampleToGroupEntry existed, with
+ // groupDescriptionIndex 0.
+
+ if (!sampleToGroupEntry || sampleToGroupEntry->mGroupDescriptionIndex == 0) {
+ return nullptr;
+ }
+
+ FallibleTArray<CencSampleEncryptionInfoEntry>* entries =
+ &mIndex->mMoofParser->mTrackSampleEncryptionInfoEntries;
+
+ uint32_t groupIndex = sampleToGroupEntry->mGroupDescriptionIndex;
+
+ // If the first bit is set to a one, then we should use the sample group
+ // descriptions from the fragment.
+ if (groupIndex > SampleToGroupEntry::kFragmentGroupDescriptionIndexBase) {
+ groupIndex -= SampleToGroupEntry::kFragmentGroupDescriptionIndexBase;
+ entries = &currentMoof->mFragmentSampleEncryptionInfoEntries;
+ }
+
+ // The group_index is one based.
+ return groupIndex > entries->Length() ? nullptr
+ : &entries->ElementAt(groupIndex - 1);
+}
+
+Result<CryptoScheme, nsCString> SampleIterator::GetEncryptionScheme() {
+ // See ISO/IEC 23001-7 for information on the metadata being checked.
+ MoofParser* moofParser = mIndex->mMoofParser.get();
+ if (!moofParser) {
+ // This mp4 isn't fragmented so it can't be encrypted.
+ return CryptoScheme::None;
+ }
+
+ SampleDescriptionEntry* sampleDescriptionEntry = GetSampleDescriptionEntry();
+ if (!sampleDescriptionEntry) {
+ // For the file to be valid the tfhd must reference a sample description
+ // entry.
+ // If we encounter this error often, we may consider using the first
+ // sample description entry if the index is out of bounds.
+ return mozilla::Err(nsLiteralCString(
+ "Could not determine encryption scheme due to bad index for sample "
+ "description entry."));
+ }
+
+ if (!sampleDescriptionEntry->mIsEncryptedEntry) {
+ return CryptoScheme::None;
+ }
+
+ if (!moofParser->mSinf.IsValid()) {
+ // The sample description entry says this sample is encrypted, but we
+ // don't have a valid sinf box. This shouldn't happen as the sinf box is
+ // part of the sample description entry. Suggests a malformed file, bail.
+ return mozilla::Err(nsLiteralCString(
+ "Could not determine encryption scheme. Sample description entry "
+ "indicates encryption, but could not find associated sinf box."));
+ }
+
+ CencSampleEncryptionInfoEntry* sampleInfo = GetSampleEncryptionEntry();
+ if (sampleInfo && !sampleInfo->mIsEncrypted) {
+ // May not have sample encryption info, but if we do, it should match other
+ // metadata.
+ return mozilla::Err(nsLiteralCString(
+ "Could not determine encryption scheme. Sample description entry "
+ "indicates encryption, but sample encryption entry indicates sample is "
+ "not encrypted. These should be consistent."));
+ }
+
+ if (moofParser->mSinf.mDefaultEncryptionType == AtomType("cenc")) {
+ return CryptoScheme::Cenc;
+ } else if (moofParser->mSinf.mDefaultEncryptionType == AtomType("cbcs")) {
+ return CryptoScheme::Cbcs;
+ }
+ return mozilla::Err(nsLiteralCString(
+ "Could not determine encryption scheme. Sample description entry "
+ "reports sample is encrypted, but no scheme, or an unsupported scheme "
+ "is in use."));
+}
+
+Sample* SampleIterator::Get() {
+ if (!mIndex->mMoofParser) {
+ MOZ_ASSERT(!mCurrentMoof);
+ return mCurrentSample < mIndex->mIndex.Length()
+ ? &mIndex->mIndex[mCurrentSample]
+ : nullptr;
+ }
+
+ nsTArray<Moof>& moofs = mIndex->mMoofParser->Moofs();
+ while (true) {
+ if (mCurrentMoof == moofs.Length()) {
+ if (!mIndex->mMoofParser->BlockingReadNextMoof()) {
+ return nullptr;
+ }
+ MOZ_ASSERT(mCurrentMoof < moofs.Length());
+ }
+ if (mCurrentSample < moofs[mCurrentMoof].mIndex.Length()) {
+ break;
+ }
+ mCurrentSample = 0;
+ ++mCurrentMoof;
+ }
+ return &moofs[mCurrentMoof].mIndex[mCurrentSample];
+}
+
+void SampleIterator::Next() { ++mCurrentSample; }
+
+void SampleIterator::Seek(const TimeUnit& aTime) {
+ size_t syncMoof = 0;
+ size_t syncSample = 0;
+ mCurrentMoof = 0;
+ mCurrentSample = 0;
+ Sample* sample;
+ while (!!(sample = Get())) {
+ if (sample->mCompositionRange.start > aTime) {
+ break;
+ }
+ if (sample->mSync) {
+ syncMoof = mCurrentMoof;
+ syncSample = mCurrentSample;
+ }
+ if (sample->mCompositionRange.start == aTime) {
+ break;
+ }
+ Next();
+ }
+ mCurrentMoof = syncMoof;
+ mCurrentSample = syncSample;
+}
+
+TimeUnit SampleIterator::GetNextKeyframeTime() {
+ SampleIterator itr(*this);
+ Sample* sample;
+ while (!!(sample = itr.Get())) {
+ if (sample->mSync) {
+ return sample->mCompositionRange.start;
+ }
+ itr.Next();
+ }
+ return TimeUnit::Invalid();
+}
+
+MP4SampleIndex::MP4SampleIndex(const IndiceWrapper& aIndices,
+ ByteStream* aSource, uint32_t aTrackId,
+ bool aIsAudio, uint32_t aTimeScale)
+ : mSource(aSource), mIsAudio(aIsAudio) {
+ if (!aIndices.Length()) {
+ mMoofParser =
+ MakeUnique<MoofParser>(aSource, AsVariant(aTrackId), aIsAudio);
+ } else {
+ if (!mIndex.SetCapacity(aIndices.Length(), fallible)) {
+ // OOM.
+ return;
+ }
+ media::IntervalSet<TimeUnit> intervalTime;
+ MediaByteRange intervalRange;
+ bool haveSync = false;
+ bool progressive = true;
+ int64_t lastOffset = 0;
+ for (size_t i = 0; i < aIndices.Length(); i++) {
+ Indice indice{};
+ int64_t timescale =
+ mMoofParser ? AssertedCast<int64_t>(mMoofParser->mMvhd.mTimescale)
+ : aTimeScale;
+ if (!aIndices.GetIndice(i, indice)) {
+ // Out of index?
+ return;
+ }
+ if (indice.sync || mIsAudio) {
+ haveSync = true;
+ }
+ if (!haveSync) {
+ continue;
+ }
+ Sample sample;
+ sample.mByteRange =
+ MediaByteRange(indice.start_offset, indice.end_offset);
+ sample.mCompositionRange = MP4Interval<media::TimeUnit>(
+ TimeUnit(indice.start_composition, timescale),
+ TimeUnit(indice.end_composition, timescale));
+ sample.mDecodeTime = TimeUnit(indice.start_decode, timescale);
+ sample.mSync = indice.sync || mIsAudio;
+ // FIXME: Make this infallible after bug 968520 is done.
+ MOZ_ALWAYS_TRUE(mIndex.AppendElement(sample, fallible));
+ if (indice.start_offset < lastOffset) {
+ NS_WARNING("Chunks in MP4 out of order, expect slow down");
+ progressive = false;
+ }
+ lastOffset = indice.end_offset;
+
+ // Pack audio samples in group of 128.
+ if (sample.mSync && progressive && (!mIsAudio || !(i % 128))) {
+ if (mDataOffset.Length()) {
+ auto& last = mDataOffset.LastElement();
+ last.mEndOffset = intervalRange.mEnd;
+ NS_ASSERTION(intervalTime.Length() == 1,
+ "Discontinuous samples between keyframes");
+ last.mTime.start = intervalTime.GetStart();
+ last.mTime.end = intervalTime.GetEnd();
+ }
+ if (!mDataOffset.AppendElement(
+ MP4DataOffset(mIndex.Length() - 1, indice.start_offset),
+ fallible)) {
+ // OOM.
+ return;
+ }
+ intervalTime = media::IntervalSet<TimeUnit>();
+ intervalRange = MediaByteRange();
+ }
+ intervalTime += media::Interval<TimeUnit>(sample.mCompositionRange.start,
+ sample.mCompositionRange.end);
+ intervalRange = intervalRange.Span(sample.mByteRange);
+ }
+
+ if (mDataOffset.Length() && progressive) {
+ Indice indice;
+ if (!aIndices.GetIndice(aIndices.Length() - 1, indice)) {
+ return;
+ }
+ auto& last = mDataOffset.LastElement();
+ last.mEndOffset = indice.end_offset;
+ last.mTime =
+ MP4Interval<TimeUnit>(intervalTime.GetStart(), intervalTime.GetEnd());
+ } else {
+ mDataOffset.Clear();
+ }
+ }
+}
+
+MP4SampleIndex::~MP4SampleIndex() = default;
+
+void MP4SampleIndex::UpdateMoofIndex(const MediaByteRangeSet& aByteRanges) {
+ UpdateMoofIndex(aByteRanges, false);
+}
+
+void MP4SampleIndex::UpdateMoofIndex(const MediaByteRangeSet& aByteRanges,
+ bool aCanEvict) {
+ if (!mMoofParser) {
+ return;
+ }
+ size_t moofs = mMoofParser->Moofs().Length();
+ bool canEvict = aCanEvict && moofs > 1;
+ if (canEvict) {
+ // Check that we can trim the mMoofParser. We can only do so if all
+ // iterators have demuxed all possible samples.
+ for (const SampleIterator* iterator : mIterators) {
+ if ((iterator->mCurrentSample == 0 && iterator->mCurrentMoof == moofs) ||
+ iterator->mCurrentMoof == moofs - 1) {
+ continue;
+ }
+ canEvict = false;
+ break;
+ }
+ }
+ mMoofParser->RebuildFragmentedIndex(aByteRanges, &canEvict);
+ if (canEvict) {
+ // The moofparser got trimmed. Adjust all registered iterators.
+ for (SampleIterator* iterator : mIterators) {
+ iterator->mCurrentMoof -= moofs - 1;
+ }
+ }
+}
+
+TimeUnit MP4SampleIndex::GetEndCompositionIfBuffered(
+ const MediaByteRangeSet& aByteRanges) {
+ FallibleTArray<Sample>* index;
+ if (mMoofParser) {
+ int64_t base = mMoofParser->mMdhd.mTimescale;
+ if (!mMoofParser->ReachedEnd() || mMoofParser->Moofs().IsEmpty()) {
+ return TimeUnit::Zero(base);
+ }
+ index = &mMoofParser->Moofs().LastElement().mIndex;
+ } else {
+ index = &mIndex;
+ }
+
+ int64_t base = mMoofParser->mMdhd.mTimescale;
+ media::TimeUnit lastComposition = TimeUnit::Zero(base);
+ RangeFinder rangeFinder(aByteRanges);
+ for (size_t i = index->Length(); i--;) {
+ const Sample& sample = (*index)[i];
+ if (!rangeFinder.Contains(sample.mByteRange)) {
+ return TimeUnit::Zero(base);
+ }
+ lastComposition = std::max(lastComposition, sample.mCompositionRange.end);
+ if (sample.mSync) {
+ return lastComposition;
+ }
+ }
+ return TimeUnit::Zero(base);
+}
+
+TimeIntervals MP4SampleIndex::ConvertByteRangesToTimeRanges(
+ const MediaByteRangeSet& aByteRanges) {
+ if (aByteRanges == mLastCachedRanges) {
+ return mLastBufferedRanges;
+ }
+ mLastCachedRanges = aByteRanges;
+
+ if (mDataOffset.Length()) {
+ TimeIntervals timeRanges;
+ for (const auto& range : aByteRanges) {
+ uint32_t start = mDataOffset.IndexOfFirstElementGt(range.mStart - 1);
+ if (!mIsAudio && start == mDataOffset.Length()) {
+ continue;
+ }
+ uint32_t end = mDataOffset.IndexOfFirstElementGt(
+ range.mEnd, MP4DataOffset::EndOffsetComparator());
+ if (!mIsAudio && end < start) {
+ continue;
+ }
+ if (mIsAudio && start &&
+ range.Intersects(MediaByteRange(mDataOffset[start - 1].mStartOffset,
+ mDataOffset[start - 1].mEndOffset))) {
+ // Check if previous audio data block contains some available samples.
+ for (size_t i = mDataOffset[start - 1].mIndex; i < mIndex.Length();
+ i++) {
+ if (range.ContainsStrict(mIndex[i].mByteRange)) {
+ timeRanges += TimeInterval(mIndex[i].mCompositionRange.start,
+ mIndex[i].mCompositionRange.end);
+ }
+ }
+ }
+ if (end > start) {
+ for (uint32_t i = start; i < end; i++) {
+ timeRanges += TimeInterval(mDataOffset[i].mTime.start,
+ mDataOffset[i].mTime.end);
+ }
+ }
+ if (end < mDataOffset.Length()) {
+ // Find samples in partial block contained in the byte range.
+ for (size_t i = mDataOffset[end].mIndex;
+ i < mIndex.Length() && range.ContainsStrict(mIndex[i].mByteRange);
+ i++) {
+ timeRanges += TimeInterval(mIndex[i].mCompositionRange.start,
+ mIndex[i].mCompositionRange.end);
+ }
+ }
+ }
+ mLastBufferedRanges = timeRanges;
+ return timeRanges;
+ }
+
+ RangeFinder rangeFinder(aByteRanges);
+ nsTArray<MP4Interval<media::TimeUnit>> timeRanges;
+ nsTArray<FallibleTArray<Sample>*> indexes;
+ if (mMoofParser) {
+ // We take the index out of the moof parser and move it into a local
+ // variable so we don't get concurrency issues. It gets freed when we
+ // exit this function.
+ for (int i = 0; i < mMoofParser->Moofs().Length(); i++) {
+ Moof& moof = mMoofParser->Moofs()[i];
+
+ // We need the entire moof in order to play anything
+ if (rangeFinder.Contains(moof.mRange)) {
+ if (rangeFinder.Contains(moof.mMdatRange)) {
+ MP4Interval<media::TimeUnit>::SemiNormalAppend(timeRanges,
+ moof.mTimeRange);
+ } else {
+ indexes.AppendElement(&moof.mIndex);
+ }
+ }
+ }
+ } else {
+ indexes.AppendElement(&mIndex);
+ }
+
+ bool hasSync = false;
+ for (size_t i = 0; i < indexes.Length(); i++) {
+ FallibleTArray<Sample>* index = indexes[i];
+ for (size_t j = 0; j < index->Length(); j++) {
+ const Sample& sample = (*index)[j];
+ if (!rangeFinder.Contains(sample.mByteRange)) {
+ // We process the index in decode order so we clear hasSync when we hit
+ // a range that isn't buffered.
+ hasSync = false;
+ continue;
+ }
+
+ hasSync |= sample.mSync;
+ if (!hasSync) {
+ continue;
+ }
+
+ MP4Interval<media::TimeUnit>::SemiNormalAppend(timeRanges,
+ sample.mCompositionRange);
+ }
+ }
+
+ // This fixes up when the compositon order differs from the byte range order
+ nsTArray<MP4Interval<TimeUnit>> timeRangesNormalized;
+ MP4Interval<media::TimeUnit>::Normalize(timeRanges, &timeRangesNormalized);
+ // convert timeRanges.
+ media::TimeIntervals ranges;
+ for (size_t i = 0; i < timeRangesNormalized.Length(); i++) {
+ ranges += media::TimeInterval(timeRangesNormalized[i].start,
+ timeRangesNormalized[i].end);
+ }
+ mLastBufferedRanges = ranges;
+ return ranges;
+}
+
+uint64_t MP4SampleIndex::GetEvictionOffset(const TimeUnit& aTime) {
+ uint64_t offset = std::numeric_limits<uint64_t>::max();
+ if (mMoofParser) {
+ // We need to keep the whole moof if we're keeping any of it because the
+ // parser doesn't keep parsed moofs.
+ for (int i = 0; i < mMoofParser->Moofs().Length(); i++) {
+ Moof& moof = mMoofParser->Moofs()[i];
+
+ if (!moof.mTimeRange.Length().IsZero() && moof.mTimeRange.end > aTime) {
+ offset = std::min(offset, uint64_t(std::min(moof.mRange.mStart,
+ moof.mMdatRange.mStart)));
+ }
+ }
+ } else {
+ // We've already parsed and stored the moov so we don't need to keep it.
+ // All we need to keep is the sample data itself.
+ for (size_t i = 0; i < mIndex.Length(); i++) {
+ const Sample& sample = mIndex[i];
+ if (aTime >= sample.mCompositionRange.end) {
+ offset = std::min(offset, uint64_t(sample.mByteRange.mEnd));
+ }
+ }
+ }
+ return offset;
+}
+
+void MP4SampleIndex::RegisterIterator(SampleIterator* aIterator) {
+ mIterators.AppendElement(aIterator);
+}
+
+void MP4SampleIndex::UnregisterIterator(SampleIterator* aIterator) {
+ mIterators.RemoveElement(aIterator);
+}
+
+} // namespace mozilla
diff --git a/dom/media/mp4/SampleIterator.h b/dom/media/mp4/SampleIterator.h
new file mode 100644
index 0000000000..61b60df6af
--- /dev/null
+++ b/dom/media/mp4/SampleIterator.h
@@ -0,0 +1,134 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_MP4_SAMPLE_ITERATOR_H_
+#define DOM_MEDIA_MP4_SAMPLE_ITERATOR_H_
+
+#include "ByteStream.h"
+#include "MediaData.h"
+#include "MediaResource.h"
+#include "MoofParser.h"
+#include "mozilla/ResultVariant.h"
+#include "MP4Interval.h"
+#include "nsISupportsImpl.h"
+#include "TimeUnits.h"
+
+namespace mozilla {
+
+struct CencSampleEncryptionInfoEntry;
+class IndiceWrapper;
+class MP4SampleIndex;
+struct Sample;
+
+class SampleIterator {
+ public:
+ explicit SampleIterator(MP4SampleIndex* aIndex);
+ ~SampleIterator();
+ bool HasNext();
+ already_AddRefed<mozilla::MediaRawData> GetNext();
+ void Seek(const media::TimeUnit& aTime);
+ media::TimeUnit GetNextKeyframeTime();
+
+ private:
+ Sample* Get();
+
+ // Gets the sample description entry for the current moof, or nullptr if
+ // called without a valid current moof.
+ SampleDescriptionEntry* GetSampleDescriptionEntry();
+ CencSampleEncryptionInfoEntry* GetSampleEncryptionEntry();
+
+ // Determines the encryption scheme in use for the current sample. If the
+ // the scheme cannot be unambiguously determined, will return an error with
+ // the reason.
+ //
+ // Returns: Ok(CryptoScheme) if a crypto scheme, including None, can be
+ // determined, or Err(nsCString) if there is an issue determining the scheme.
+ Result<CryptoScheme, nsCString> GetEncryptionScheme();
+
+ void Next();
+ RefPtr<MP4SampleIndex> mIndex;
+ friend class MP4SampleIndex;
+ size_t mCurrentMoof;
+ size_t mCurrentSample;
+};
+
+class MP4SampleIndex {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MP4SampleIndex)
+
+ struct Indice {
+ uint64_t start_offset;
+ uint64_t end_offset;
+ int64_t start_composition;
+ int64_t end_composition;
+ int64_t start_decode;
+ bool sync;
+ };
+
+ struct MP4DataOffset {
+ MP4DataOffset(uint32_t aIndex, int64_t aStartOffset)
+ : mIndex(aIndex), mStartOffset(aStartOffset), mEndOffset(0) {}
+
+ bool operator==(int64_t aStartOffset) const {
+ return mStartOffset == aStartOffset;
+ }
+
+ bool operator!=(int64_t aStartOffset) const {
+ return mStartOffset != aStartOffset;
+ }
+
+ bool operator<(int64_t aStartOffset) const {
+ return mStartOffset < aStartOffset;
+ }
+
+ struct EndOffsetComparator {
+ bool Equals(const MP4DataOffset& a, const int64_t& b) const {
+ return a.mEndOffset == b;
+ }
+
+ bool LessThan(const MP4DataOffset& a, const int64_t& b) const {
+ return a.mEndOffset < b;
+ }
+ };
+
+ uint32_t mIndex;
+ int64_t mStartOffset;
+ int64_t mEndOffset;
+ MP4Interval<media::TimeUnit> mTime;
+ };
+
+ MP4SampleIndex(const mozilla::IndiceWrapper& aIndices, ByteStream* aSource,
+ uint32_t aTrackId, bool aIsAudio, uint32_t aTimeScale);
+
+ void UpdateMoofIndex(const mozilla::MediaByteRangeSet& aByteRanges,
+ bool aCanEvict);
+ void UpdateMoofIndex(const mozilla::MediaByteRangeSet& aByteRanges);
+ media::TimeUnit GetEndCompositionIfBuffered(
+ const mozilla::MediaByteRangeSet& aByteRanges);
+ mozilla::media::TimeIntervals ConvertByteRangesToTimeRanges(
+ const mozilla::MediaByteRangeSet& aByteRanges);
+ uint64_t GetEvictionOffset(const media::TimeUnit& aTime);
+ bool IsFragmented() { return !!mMoofParser; }
+
+ friend class SampleIterator;
+
+ private:
+ ~MP4SampleIndex();
+ void RegisterIterator(SampleIterator* aIterator);
+ void UnregisterIterator(SampleIterator* aIterator);
+
+ ByteStream* mSource;
+ FallibleTArray<Sample> mIndex;
+ FallibleTArray<MP4DataOffset> mDataOffset;
+ UniquePtr<MoofParser> mMoofParser;
+ nsTArray<SampleIterator*> mIterators;
+
+ // ConvertByteRangesToTimeRanges cache
+ mozilla::MediaByteRangeSet mLastCachedRanges;
+ mozilla::media::TimeIntervals mLastBufferedRanges;
+ bool mIsAudio;
+};
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/mp4/SinfParser.cpp b/dom/media/mp4/SinfParser.cpp
new file mode 100644
index 0000000000..4ea14adaaa
--- /dev/null
+++ b/dom/media/mp4/SinfParser.cpp
@@ -0,0 +1,95 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/Unused.h"
+#include "SinfParser.h"
+#include "AtomType.h"
+#include "Box.h"
+#include "ByteStream.h"
+
+namespace mozilla {
+
+Sinf::Sinf(Box& aBox) : mDefaultIVSize(0), mDefaultEncryptionType() {
+ SinfParser parser(aBox);
+ if (parser.GetSinf().IsValid()) {
+ *this = parser.GetSinf();
+ }
+}
+
+SinfParser::SinfParser(Box& aBox) {
+ for (Box box = aBox.FirstChild(); box.IsAvailable(); box = box.Next()) {
+ if (box.IsType("schm")) {
+ mozilla::Unused << ParseSchm(box);
+ } else if (box.IsType("schi")) {
+ mozilla::Unused << ParseSchi(box);
+ }
+ }
+}
+
+Result<Ok, nsresult> SinfParser::ParseSchm(Box& aBox) {
+ BoxReader reader(aBox);
+
+ if (reader->Remaining() < 8) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ MOZ_TRY(reader->ReadU32()); // flags -- ignore
+ MOZ_TRY_VAR(mSinf.mDefaultEncryptionType, reader->ReadU32());
+ return Ok();
+}
+
+Result<Ok, nsresult> SinfParser::ParseSchi(Box& aBox) {
+ for (Box box = aBox.FirstChild(); box.IsAvailable(); box = box.Next()) {
+ if (box.IsType("tenc") && ParseTenc(box).isErr()) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ }
+ return Ok();
+}
+
+Result<Ok, nsresult> SinfParser::ParseTenc(Box& aBox) {
+ BoxReader reader(aBox);
+
+ if (reader->Remaining() < 24) {
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ uint32_t flags;
+ MOZ_TRY_VAR(flags, reader->ReadU32());
+ uint8_t version = flags >> 24;
+
+ // Skip reserved byte
+ MOZ_TRY(reader->ReadU8());
+ if (version >= 1) {
+ uint8_t pattern;
+ MOZ_TRY_VAR(pattern, reader->ReadU8());
+ mSinf.mDefaultCryptByteBlock = pattern >> 4;
+ mSinf.mDefaultSkipByteBlock = pattern & 0x0f;
+ } else {
+ // Reserved if version is less than 1
+ MOZ_TRY(reader->ReadU8());
+ mSinf.mDefaultCryptByteBlock = 0;
+ mSinf.mDefaultSkipByteBlock = 0;
+ }
+
+ uint8_t isEncrypted;
+ MOZ_TRY_VAR(isEncrypted, reader->ReadU8());
+ MOZ_TRY_VAR(mSinf.mDefaultIVSize, reader->ReadU8());
+ memcpy(mSinf.mDefaultKeyID, reader->Read(16), 16);
+
+ if (isEncrypted && mSinf.mDefaultIVSize == 0) {
+ uint8_t defaultConstantIVSize;
+ MOZ_TRY_VAR(defaultConstantIVSize, reader->ReadU8());
+ if (!mSinf.mDefaultConstantIV.SetLength(defaultConstantIVSize,
+ mozilla::fallible)) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ for (uint8_t i = 0; i < defaultConstantIVSize; i++) {
+ MOZ_TRY_VAR(mSinf.mDefaultConstantIV.ElementAt(i), reader->ReadU8());
+ }
+ }
+ return Ok();
+}
+
+} // namespace mozilla
diff --git a/dom/media/mp4/SinfParser.h b/dom/media/mp4/SinfParser.h
new file mode 100644
index 0000000000..084892854c
--- /dev/null
+++ b/dom/media/mp4/SinfParser.h
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef SINF_PARSER_H_
+#define SINF_PARSER_H_
+
+#include "mozilla/ResultExtensions.h"
+#include "Atom.h"
+#include "AtomType.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+
+class Box;
+
+class Sinf : public Atom {
+ public:
+ Sinf()
+ : mDefaultIVSize(0),
+ mDefaultEncryptionType(),
+ mDefaultCryptByteBlock(0),
+ mDefaultSkipByteBlock(0) {}
+ explicit Sinf(Box& aBox);
+
+ bool IsValid() override {
+ return !!mDefaultEncryptionType && // Should have an encryption scheme
+ (mDefaultIVSize > 0 || // and either a default IV size
+ mDefaultConstantIV.Length() > 0); // or a constant IV.
+ }
+
+ uint8_t mDefaultIVSize;
+ AtomType mDefaultEncryptionType;
+ uint8_t mDefaultKeyID[16];
+ uint8_t mDefaultCryptByteBlock;
+ uint8_t mDefaultSkipByteBlock;
+ CopyableTArray<uint8_t> mDefaultConstantIV;
+};
+
+class SinfParser {
+ public:
+ explicit SinfParser(Box& aBox);
+
+ Sinf& GetSinf() { return mSinf; }
+
+ private:
+ Result<Ok, nsresult> ParseSchm(Box& aBox);
+ Result<Ok, nsresult> ParseSchi(Box& aBox);
+ Result<Ok, nsresult> ParseTenc(Box& aBox);
+
+ Sinf mSinf;
+};
+
+} // namespace mozilla
+
+#endif // SINF_PARSER_H_
diff --git a/dom/media/mp4/moz.build b/dom/media/mp4/moz.build
new file mode 100644
index 0000000000..48fce2a040
--- /dev/null
+++ b/dom/media/mp4/moz.build
@@ -0,0 +1,45 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS += [
+ "Atom.h",
+ "AtomType.h",
+ "Box.h",
+ "BufferStream.h",
+ "ByteStream.h",
+ "DecoderData.h",
+ "MoofParser.h",
+ "MP4Decoder.h",
+ "MP4Demuxer.h",
+ "MP4Interval.h",
+ "MP4Metadata.h",
+ "ResourceStream.h",
+ "SampleIterator.h",
+ "SinfParser.h",
+]
+
+UNIFIED_SOURCES += [
+ "Box.cpp",
+ "BufferStream.cpp",
+ "DecoderData.cpp",
+ "MoofParser.cpp",
+ "MP4Decoder.cpp",
+ "MP4Demuxer.cpp",
+ "MP4Metadata.cpp",
+ "ResourceStream.cpp",
+ "SampleIterator.cpp",
+ "SinfParser.cpp",
+]
+
+FINAL_LIBRARY = "xul"
+
+# Suppress warnings for now.
+CXXFLAGS += [
+ "-Wno-sign-compare",
+]
+
+# Add libFuzzer configuration directives
+include("/tools/fuzzing/libfuzzer-config.mozbuild")
diff --git a/dom/media/nsIAudioDeviceInfo.idl b/dom/media/nsIAudioDeviceInfo.idl
new file mode 100644
index 0000000000..f0a1853875
--- /dev/null
+++ b/dom/media/nsIAudioDeviceInfo.idl
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(feb979a8-f8cc-4522-9dff-6c055ca50762)]
+interface nsIAudioDeviceInfo : nsISupports
+{
+ readonly attribute AString name;
+
+ readonly attribute AString groupId;
+
+ readonly attribute AString vendor;
+
+ // type: Unknown/Input/Output
+ const unsigned short TYPE_UNKNOWN = 0;
+ const unsigned short TYPE_INPUT = 1;
+ const unsigned short TYPE_OUTPUT = 2;
+ readonly attribute unsigned short type;
+
+ // state: Disabled/Unplugged/Enabled
+ const unsigned short STATE_DISABLED = 0;
+ const unsigned short STATE_UNPLUGGED = 1;
+ const unsigned short STATE_ENABLED = 2;
+ readonly attribute unsigned short state;
+
+ // preferred: None/Multimedia/Voice/Notification/All
+ const unsigned short PREF_NONE = 0x00;
+ const unsigned short PREF_MULTIMEDIA = 0x01;
+ const unsigned short PREF_VOICE = 0x02;
+ const unsigned short PREF_NOTIFICATION = 0x04;
+ const unsigned short PREF_ALL = 0x0F;
+ readonly attribute unsigned short preferred;
+
+ // supported format, default format: S16LE/S16BE/F32LE/F32BE
+ const unsigned short FMT_S16LE = 0x0010;
+ const unsigned short FMT_S16BE = 0x0020;
+ const unsigned short FMT_F32LE = 0x1000;
+ const unsigned short FMT_F32BE = 0x2000;
+ readonly attribute unsigned short supportedFormat;
+ readonly attribute unsigned short defaultFormat;
+
+ // Max number of channels: [1, 255]
+ readonly attribute unsigned long maxChannels;
+
+ readonly attribute unsigned long defaultRate;
+ readonly attribute unsigned long maxRate;
+ readonly attribute unsigned long minRate;
+
+ readonly attribute unsigned long maxLatency;
+ readonly attribute unsigned long minLatency;
+};
diff --git a/dom/media/nsIDocumentActivity.h b/dom/media/nsIDocumentActivity.h
new file mode 100644
index 0000000000..2b28247598
--- /dev/null
+++ b/dom/media/nsIDocumentActivity.h
@@ -0,0 +1,31 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsIDocumentActivity_h__
+#define nsIDocumentActivity_h__
+
+#include "nsISupports.h"
+
+#define NS_IDOCUMENTACTIVITY_IID \
+ { \
+ 0x9b9f584e, 0xefa8, 0x11e3, { \
+ 0xbb, 0x74, 0x5e, 0xdd, 0x1d, 0x5d, 0x46, 0xb0 \
+ } \
+ }
+
+class nsIDocumentActivity : public nsISupports {
+ public:
+ NS_DECLARE_STATIC_IID_ACCESSOR(NS_IDOCUMENTACTIVITY_IID)
+
+ virtual void NotifyOwnerDocumentActivityChanged() = 0;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(nsIDocumentActivity, NS_IDOCUMENTACTIVITY_IID)
+
+/* Use this macro when declaring classes that implement this interface. */
+#define NS_DECL_NSIDOCUMENTACTIVITY \
+ virtual void NotifyOwnerDocumentActivityChanged() override;
+
+#endif /* nsIDocumentActivity_h__ */
diff --git a/dom/media/nsIMediaDevice.idl b/dom/media/nsIMediaDevice.idl
new file mode 100644
index 0000000000..0f4afc7cfc
--- /dev/null
+++ b/dom/media/nsIMediaDevice.idl
@@ -0,0 +1,18 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "nsIVariant.idl"
+
+[scriptable, builtinclass, uuid(d16af630-2fca-11ec-932f-0800200c9a66)]
+interface nsIMediaDevice : nsISupports
+{
+ readonly attribute AString type;
+ readonly attribute AString mediaSource;
+ readonly attribute AString rawId;
+ readonly attribute AString id; // anonymized and localized to the origin
+ readonly attribute boolean scary;
+ readonly attribute boolean canRequestOsLevelPrompt;
+ readonly attribute AString rawName; // unfiltered device name, from 1616661
+};
diff --git a/dom/media/nsIMediaManager.idl b/dom/media/nsIMediaManager.idl
new file mode 100644
index 0000000000..9cc39d04f4
--- /dev/null
+++ b/dom/media/nsIMediaManager.idl
@@ -0,0 +1,42 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIArray;
+interface nsIDOMWindow;
+interface nsIMediaDevice;
+
+%{C++
+#define NS_MEDIAMANAGERSERVICE_CID {0xabc622ea, 0x9655, 0x4123, {0x80, 0xd9, 0x22, 0x62, 0x1b, 0xdd, 0x54, 0x65}}
+#define MEDIAMANAGERSERVICE_CONTRACTID "@mozilla.org/mediaManagerService;1"
+%}
+
+[scriptable, builtinclass, uuid(24b23e01-33fd-401f-ba25-6e52658750b0)]
+interface nsIMediaManagerService : nsISupports
+{
+ /* return a array of inner windows that have active captures */
+ readonly attribute nsIArray activeMediaCaptureWindows;
+
+ /* possible states for camera and microphone capture */
+ const unsigned short STATE_NOCAPTURE = 0;
+ const unsigned short STATE_CAPTURE_ENABLED = 1;
+ const unsigned short STATE_CAPTURE_DISABLED = 2;
+
+ /* Get the capture state for the given window. This will not check
+ * descendants, such as iframes. Callers who need to check descendants should
+ * iterate descendants manually and call this on each.
+ */
+ void mediaCaptureWindowState(in nsIDOMWindow aWindow,
+ out unsigned short aCamera,
+ out unsigned short aMicrophone,
+ out unsigned short aScreenShare,
+ out unsigned short aWindowShare,
+ out unsigned short aBrowserShare,
+ out Array<nsIMediaDevice> devices);
+
+ /* Clear per-orgin list of persistent DeviceIds stored for enumerateDevices
+ sinceTime is milliseconds since 1 January 1970 00:00:00 UTC. 0 = clear all */
+ void sanitizeDeviceIds(in long long sinceWhen);
+};
diff --git a/dom/media/ogg/OggCodecState.cpp b/dom/media/ogg/OggCodecState.cpp
new file mode 100644
index 0000000000..c20a6a17bc
--- /dev/null
+++ b/dom/media/ogg/OggCodecState.cpp
@@ -0,0 +1,1800 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <string.h>
+
+#include "mozilla/EndianUtils.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/TextUtils.h"
+#include "mozilla/Utf8.h"
+#include <stdint.h>
+#include <algorithm>
+#include <opus/opus.h>
+
+#include "OggCodecState.h"
+#include "OggRLBox.h"
+#include "OpusDecoder.h"
+#include "OpusParser.h"
+#include "VideoUtils.h"
+#include "XiphExtradata.h"
+#include "nsDebug.h"
+#include "opus/opus_multistream.h"
+
+namespace mozilla {
+
+extern LazyLogModule gMediaDecoderLog;
+#define LOG(type, msg) MOZ_LOG(gMediaDecoderLog, type, msg)
+
+using media::TimeUnit;
+
+/** Decoder base class for Ogg-encapsulated streams. */
+UniquePtr<OggCodecState> OggCodecState::Create(
+ rlbox_sandbox_ogg* aSandbox, tainted_opaque_ogg<ogg_page*> aPage,
+ uint32_t aSerial) {
+ NS_ASSERTION(sandbox_invoke(*aSandbox, ogg_page_bos, aPage)
+ .unverified_safe_because(RLBOX_SAFE_DEBUG_ASSERTION),
+ "Only call on BOS page!");
+ UniquePtr<OggCodecState> codecState;
+ tainted_ogg<ogg_page*> aPage_t = rlbox::from_opaque(aPage);
+ const char codec_reason[] =
+ "These conditions set the type of codec. Since we are relying on "
+ "ogg_page to determine the codec type, the library could lie about "
+ "this. We allow this as it does not directly allow renderer "
+ "vulnerabilities if this is incorrect.";
+ long body_len = aPage_t->body_len.unverified_safe_because(codec_reason);
+
+ if (body_len > 6 && rlbox::memcmp(*aSandbox, aPage_t->body + 1, "theora", 6u)
+ .unverified_safe_because(codec_reason) == 0) {
+ codecState = MakeUnique<TheoraState>(aSandbox, aPage, aSerial);
+ } else if (body_len > 6 &&
+ rlbox::memcmp(*aSandbox, aPage_t->body + 1, "vorbis", 6u)
+ .unverified_safe_because(codec_reason) == 0) {
+ codecState = MakeUnique<VorbisState>(aSandbox, aPage, aSerial);
+ } else if (body_len > 8 &&
+ rlbox::memcmp(*aSandbox, aPage_t->body, "OpusHead", 8u)
+ .unverified_safe_because(codec_reason) == 0) {
+ codecState = MakeUnique<OpusState>(aSandbox, aPage, aSerial);
+ } else if (body_len > 8 &&
+ rlbox::memcmp(*aSandbox, aPage_t->body, "fishead\0", 8u)
+ .unverified_safe_because(codec_reason) == 0) {
+ codecState = MakeUnique<SkeletonState>(aSandbox, aPage, aSerial);
+ } else if (body_len > 5 &&
+ rlbox::memcmp(*aSandbox, aPage_t->body, "\177FLAC", 5u)
+ .unverified_safe_because(codec_reason) == 0) {
+ codecState = MakeUnique<FlacState>(aSandbox, aPage, aSerial);
+ } else {
+ // Can't use MakeUnique here, OggCodecState is protected.
+ codecState.reset(new OggCodecState(aSandbox, aPage, aSerial, false));
+ }
+
+ if (!codecState->OggCodecState::InternalInit()) {
+ codecState.reset();
+ }
+
+ return codecState;
+}
+
+OggCodecState::OggCodecState(rlbox_sandbox_ogg* aSandbox,
+ tainted_opaque_ogg<ogg_page*> aBosPage,
+ uint32_t aSerial, bool aActive)
+ : mPacketCount(0),
+ mSerial(aSerial),
+ mActive(aActive),
+ mDoneReadingHeaders(!aActive),
+ mSandbox(aSandbox) {
+ MOZ_COUNT_CTOR(OggCodecState);
+ tainted_ogg<ogg_stream_state*> state =
+ mSandbox->malloc_in_sandbox<ogg_stream_state>();
+ MOZ_RELEASE_ASSERT(state != nullptr);
+ rlbox::memset(*mSandbox, state, 0, sizeof(ogg_stream_state));
+ mState = state.to_opaque();
+}
+
+OggCodecState::~OggCodecState() {
+ MOZ_COUNT_DTOR(OggCodecState);
+ Reset();
+#ifdef DEBUG
+ int ret =
+#endif
+ sandbox_invoke(*mSandbox, ogg_stream_clear, mState)
+ .unverified_safe_because(RLBOX_SAFE_DEBUG_ASSERTION);
+ NS_ASSERTION(ret == 0, "ogg_stream_clear failed");
+ mSandbox->free_in_sandbox(rlbox::from_opaque(mState));
+ tainted_ogg<ogg_stream_state*> nullval = nullptr;
+ mState = nullval.to_opaque();
+}
+
+nsresult OggCodecState::Reset() {
+ if (sandbox_invoke(*mSandbox, ogg_stream_reset, mState)
+ .unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON) != 0) {
+ return NS_ERROR_FAILURE;
+ }
+ mPackets.Erase();
+ ClearUnstamped();
+ return NS_OK;
+}
+
+void OggCodecState::ClearUnstamped() { mUnstamped.Clear(); }
+
+bool OggCodecState::InternalInit() {
+ int ret = sandbox_invoke(*mSandbox, ogg_stream_init, mState, mSerial)
+ .unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON);
+ return ret == 0;
+}
+
+bool OggCodecState::IsValidVorbisTagName(nsCString& aName) {
+ // Tag names must consist of ASCII 0x20 through 0x7D,
+ // excluding 0x3D '=' which is the separator.
+ uint32_t length = aName.Length();
+ const char* data = aName.Data();
+ for (uint32_t i = 0; i < length; i++) {
+ if (data[i] < 0x20 || data[i] > 0x7D || data[i] == '=') {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool OggCodecState::AddVorbisComment(UniquePtr<MetadataTags>& aTags,
+ const char* aComment, uint32_t aLength) {
+ const char* div = (const char*)memchr(aComment, '=', aLength);
+ if (!div) {
+ LOG(LogLevel::Debug, ("Skipping comment: no separator"));
+ return false;
+ }
+ nsCString key = nsCString(aComment, div - aComment);
+ if (!IsValidVorbisTagName(key)) {
+ LOG(LogLevel::Debug, ("Skipping comment: invalid tag name"));
+ return false;
+ }
+ uint32_t valueLength = aLength - (div - aComment);
+ nsCString value = nsCString(div + 1, valueLength);
+ if (!IsUtf8(value)) {
+ LOG(LogLevel::Debug, ("Skipping comment: invalid UTF-8 in value"));
+ return false;
+ }
+ aTags->InsertOrUpdate(key, value);
+ return true;
+}
+
+bool OggCodecState::SetCodecSpecificConfig(MediaByteBuffer* aBuffer,
+ OggPacketQueue& aHeaders) {
+ nsTArray<const unsigned char*> headers;
+ nsTArray<size_t> headerLens;
+ for (size_t i = 0; i < aHeaders.Length(); i++) {
+ headers.AppendElement(aHeaders[i]->packet);
+ headerLens.AppendElement(aHeaders[i]->bytes);
+ }
+ // Save header packets for the decoder
+ if (!XiphHeadersToExtradata(aBuffer, headers, headerLens)) {
+ return false;
+ }
+ aHeaders.Erase();
+ return true;
+}
+
+void VorbisState::RecordVorbisPacketSamples(ogg_packet* aPacket,
+ long aSamples) {
+#ifdef VALIDATE_VORBIS_SAMPLE_CALCULATION
+ mVorbisPacketSamples[aPacket] = aSamples;
+#endif
+}
+
+void VorbisState::ValidateVorbisPacketSamples(ogg_packet* aPacket,
+ long aSamples) {
+#ifdef VALIDATE_VORBIS_SAMPLE_CALCULATION
+ NS_ASSERTION(mVorbisPacketSamples[aPacket] == aSamples,
+ "Decoded samples for Vorbis packet don't match expected!");
+ mVorbisPacketSamples.erase(aPacket);
+#endif
+}
+
+void VorbisState::AssertHasRecordedPacketSamples(ogg_packet* aPacket) {
+#ifdef VALIDATE_VORBIS_SAMPLE_CALCULATION
+ NS_ASSERTION(mVorbisPacketSamples.count(aPacket) == 1,
+ "Must have recorded packet samples");
+#endif
+}
+
+// Clone the given packet from memory accessible to the sandboxed libOgg to
+// memory accessible only to the Firefox renderer
+static OggPacketPtr CloneOutOfSandbox(tainted_ogg<ogg_packet*> aPacket) {
+ ogg_packet* clone =
+ aPacket.copy_and_verify([](std::unique_ptr<tainted_ogg<ogg_packet>> val) {
+ const char packet_reason[] =
+ "Packets have no guarantees on what data they hold. The renderer's "
+ "safety is not compromised even if packets return garbage data.";
+
+ ogg_packet* p = new ogg_packet();
+ p->bytes = val->bytes.unverified_safe_because(packet_reason);
+ p->b_o_s = val->b_o_s.unverified_safe_because(packet_reason);
+ p->e_o_s = val->e_o_s.unverified_safe_because(packet_reason);
+ p->granulepos = val->granulepos.unverified_safe_because(packet_reason);
+ p->packetno = val->packetno.unverified_safe_because(packet_reason);
+ if (p->bytes == 0) {
+ p->packet = nullptr;
+ } else {
+ p->packet = val->packet.copy_and_verify_range(
+ [](std::unique_ptr<unsigned char[]> packet) {
+ return packet.release();
+ },
+ p->bytes);
+ }
+ return p;
+ });
+ return OggPacketPtr(clone);
+}
+
+void OggPacketQueue::Append(OggPacketPtr aPacket) {
+ nsDeque::Push(aPacket.release());
+}
+
+bool OggCodecState::IsPacketReady() { return !mPackets.IsEmpty(); }
+
+OggPacketPtr OggCodecState::PacketOut() {
+ if (mPackets.IsEmpty()) {
+ return nullptr;
+ }
+ return mPackets.PopFront();
+}
+
+ogg_packet* OggCodecState::PacketPeek() {
+ if (mPackets.IsEmpty()) {
+ return nullptr;
+ }
+ return mPackets.PeekFront();
+}
+
+void OggCodecState::PushFront(OggPacketQueue&& aOther) {
+ while (!aOther.IsEmpty()) {
+ mPackets.PushFront(aOther.Pop());
+ }
+}
+
+already_AddRefed<MediaRawData> OggCodecState::PacketOutAsMediaRawData() {
+ OggPacketPtr packet = PacketOut();
+ if (!packet) {
+ return nullptr;
+ }
+
+ NS_ASSERTION(
+ !IsHeader(packet.get()),
+ "PacketOutAsMediaRawData can only be called on non-header packets");
+ RefPtr<MediaRawData> sample = new MediaRawData(packet->packet, packet->bytes);
+ if (packet->bytes && !sample->Data()) {
+ // OOM.
+ return nullptr;
+ }
+
+ int64_t end_tstamp = Time(packet->granulepos);
+ NS_ASSERTION(end_tstamp >= 0, "timestamp invalid");
+
+ int64_t duration = PacketDuration(packet.get());
+ NS_ASSERTION(duration >= 0, "duration invalid");
+
+ sample->mTimecode = TimeUnit::FromMicroseconds(packet->granulepos);
+ sample->mTime = TimeUnit::FromMicroseconds(end_tstamp - duration);
+ sample->mDuration = TimeUnit::FromMicroseconds(duration);
+ sample->mKeyframe = IsKeyframe(packet.get());
+ sample->mEOS = packet->e_o_s;
+
+ return sample.forget();
+}
+
+nsresult OggCodecState::PageIn(tainted_opaque_ogg<ogg_page*> aPage) {
+ if (!mActive) {
+ return NS_OK;
+ }
+ NS_ASSERTION((rlbox::sandbox_static_cast<uint32_t>(sandbox_invoke(
+ *mSandbox, ogg_page_serialno, aPage)) == mSerial)
+ .unverified_safe_because(RLBOX_OGG_PAGE_SERIAL_REASON),
+ "Page must be for this stream!");
+ if (sandbox_invoke(*mSandbox, ogg_stream_pagein, mState, aPage)
+ .unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON) == -1) {
+ return NS_ERROR_FAILURE;
+ }
+ int r;
+ tainted_ogg<ogg_packet*> packet = mSandbox->malloc_in_sandbox<ogg_packet>();
+ if (!packet) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ auto clean_packet = MakeScopeExit([&] { mSandbox->free_in_sandbox(packet); });
+
+ do {
+ r = sandbox_invoke(*mSandbox, ogg_stream_packetout, mState, packet)
+ .unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON);
+ if (r == 1) {
+ mPackets.Append(CloneOutOfSandbox(packet));
+ }
+ } while (r != 0);
+ if (sandbox_invoke(*mSandbox, ogg_stream_check, mState)
+ .unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON)) {
+ NS_WARNING("Unrecoverable error in ogg_stream_packetout");
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+nsresult OggCodecState::PacketOutUntilGranulepos(bool& aFoundGranulepos) {
+ tainted_ogg<int> r;
+ aFoundGranulepos = false;
+ // Extract packets from the sync state until either no more packets
+ // come out, or we get a data packet with non -1 granulepos.
+ tainted_ogg<ogg_packet*> packet = mSandbox->malloc_in_sandbox<ogg_packet>();
+ if (!packet) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ auto clean_packet = MakeScopeExit([&] { mSandbox->free_in_sandbox(packet); });
+
+ do {
+ r = sandbox_invoke(*mSandbox, ogg_stream_packetout, mState, packet);
+ if (r.unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON) == 1) {
+ OggPacketPtr clone = CloneOutOfSandbox(packet);
+ if (IsHeader(clone.get())) {
+ // Header packets go straight into the packet queue.
+ mPackets.Append(std::move(clone));
+ } else {
+ // We buffer data packets until we encounter a granulepos. We'll
+ // then use the granulepos to figure out the granulepos of the
+ // preceeding packets.
+ aFoundGranulepos = clone.get()->granulepos > 0;
+ mUnstamped.AppendElement(std::move(clone));
+ }
+ }
+ } while (r.unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON) != 0 &&
+ !aFoundGranulepos);
+ if (sandbox_invoke(*mSandbox, ogg_stream_check, mState)
+ .unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON)) {
+ NS_WARNING("Unrecoverable error in ogg_stream_packetout");
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+TheoraState::TheoraState(rlbox_sandbox_ogg* aSandbox,
+ tainted_opaque_ogg<ogg_page*> aBosPage,
+ uint32_t aSerial)
+ : OggCodecState(aSandbox, aBosPage, aSerial, true),
+ mSetup(nullptr),
+ mCtx(nullptr) {
+ MOZ_COUNT_CTOR(TheoraState);
+ th_info_init(&mTheoraInfo);
+ th_comment_init(&mComment);
+}
+
+TheoraState::~TheoraState() {
+ MOZ_COUNT_DTOR(TheoraState);
+ th_setup_free(mSetup);
+ th_decode_free(mCtx);
+ th_comment_clear(&mComment);
+ th_info_clear(&mTheoraInfo);
+ Reset();
+}
+
+bool TheoraState::Init() {
+ if (!mActive) {
+ return false;
+ }
+
+ int64_t n = mTheoraInfo.aspect_numerator;
+ int64_t d = mTheoraInfo.aspect_denominator;
+
+ float aspectRatio =
+ (n == 0 || d == 0) ? 1.0f : static_cast<float>(n) / static_cast<float>(d);
+
+ // Ensure the frame and picture regions aren't larger than our prescribed
+ // maximum, or zero sized.
+ gfx::IntSize frame(mTheoraInfo.frame_width, mTheoraInfo.frame_height);
+ gfx::IntRect picture(mTheoraInfo.pic_x, mTheoraInfo.pic_y,
+ mTheoraInfo.pic_width, mTheoraInfo.pic_height);
+ gfx::IntSize display(mTheoraInfo.pic_width, mTheoraInfo.pic_height);
+ ScaleDisplayByAspectRatio(display, aspectRatio);
+ if (!IsValidVideoRegion(frame, picture, display)) {
+ return mActive = false;
+ }
+
+ mCtx = th_decode_alloc(&mTheoraInfo, mSetup);
+ if (!mCtx) {
+ return mActive = false;
+ }
+
+ // Video track's frame sizes will not overflow. Activate the video track.
+ mInfo.mMimeType = "video/theora"_ns;
+ mInfo.mDisplay = display;
+ mInfo.mImage = frame;
+ mInfo.SetImageRect(picture);
+
+ return mActive = SetCodecSpecificConfig(mInfo.mCodecSpecificConfig, mHeaders);
+}
+
+nsresult TheoraState::Reset() {
+ mHeaders.Erase();
+ return OggCodecState::Reset();
+}
+
+bool TheoraState::DecodeHeader(OggPacketPtr aPacket) {
+ ogg_packet* packet = aPacket.get(); // Will be owned by mHeaders.
+ mHeaders.Append(std::move(aPacket));
+ mPacketCount++;
+ int ret = th_decode_headerin(&mTheoraInfo, &mComment, &mSetup, packet);
+
+ // We must determine when we've read the last header packet.
+ // th_decode_headerin() does not tell us when it's read the last header, so
+ // we must keep track of the headers externally.
+ //
+ // There are 3 header packets, the Identification, Comment, and Setup
+ // headers, which must be in that order. If they're out of order, the file
+ // is invalid. If we've successfully read a header, and it's the setup
+ // header, then we're done reading headers. The first byte of each packet
+ // determines it's type as follows:
+ // 0x80 -> Identification header
+ // 0x81 -> Comment header
+ // 0x82 -> Setup header
+ // See http://www.theora.org/doc/Theora.pdf Chapter 6, "Bitstream Headers",
+ // for more details of the Ogg/Theora containment scheme.
+ bool isSetupHeader = packet->bytes > 0 && packet->packet[0] == 0x82;
+ if (ret < 0 || mPacketCount > 3) {
+ // We've received an error, or the first three packets weren't valid
+ // header packets. Assume bad input.
+ // Our caller will deactivate the bitstream.
+ return false;
+ } else if (ret > 0 && isSetupHeader && mPacketCount == 3) {
+ // Successfully read the three header packets.
+ mDoneReadingHeaders = true;
+ }
+ return true;
+}
+
+int64_t TheoraState::Time(int64_t granulepos) {
+ if (!mActive) {
+ return -1;
+ }
+ return TheoraState::Time(&mTheoraInfo, granulepos);
+}
+
+bool TheoraState::IsHeader(ogg_packet* aPacket) {
+ return th_packet_isheader(aPacket);
+}
+
+#define TH_VERSION_CHECK(_info, _maj, _min, _sub) \
+ (((_info)->version_major > (_maj) || (_info)->version_major == (_maj)) && \
+ (((_info)->version_minor > (_min) || (_info)->version_minor == (_min)) && \
+ (_info)->version_subminor >= (_sub)))
+
+int64_t TheoraState::Time(th_info* aInfo, int64_t aGranulepos) {
+ if (aGranulepos < 0 || aInfo->fps_numerator == 0) {
+ return -1;
+ }
+ // Implementation of th_granule_frame inlined here to operate
+ // on the th_info structure instead of the theora_state.
+ int shift = aInfo->keyframe_granule_shift;
+ ogg_int64_t iframe = aGranulepos >> shift;
+ ogg_int64_t pframe = aGranulepos - (iframe << shift);
+ int64_t frameno = iframe + pframe - TH_VERSION_CHECK(aInfo, 3, 2, 1);
+ CheckedInt64 t =
+ ((CheckedInt64(frameno) + 1) * USECS_PER_S) * aInfo->fps_denominator;
+ if (!t.isValid()) {
+ return -1;
+ }
+ t /= aInfo->fps_numerator;
+ return t.isValid() ? t.value() : -1;
+}
+
+int64_t TheoraState::StartTime(int64_t granulepos) {
+ if (granulepos < 0 || !mActive || mTheoraInfo.fps_numerator == 0) {
+ return -1;
+ }
+ CheckedInt64 t =
+ (CheckedInt64(th_granule_frame(mCtx, granulepos)) * USECS_PER_S) *
+ mTheoraInfo.fps_denominator;
+ if (!t.isValid()) {
+ return -1;
+ }
+ return t.value() / mTheoraInfo.fps_numerator;
+}
+
+int64_t TheoraState::PacketDuration(ogg_packet* aPacket) {
+ if (!mActive || mTheoraInfo.fps_numerator == 0) {
+ return -1;
+ }
+ CheckedInt64 t = SaferMultDiv(mTheoraInfo.fps_denominator, USECS_PER_S,
+ mTheoraInfo.fps_numerator);
+ return t.isValid() ? t.value() : -1;
+}
+
+int64_t TheoraState::MaxKeyframeOffset() {
+ // Determine the maximum time in microseconds by which a key frame could
+ // offset for the theora bitstream. Theora granulepos encode time as:
+ // ((key_frame_number << granule_shift) + frame_offset).
+ // Therefore the maximum possible time by which any frame could be offset
+ // from a keyframe is the duration of (1 << granule_shift) - 1) frames.
+ int64_t frameDuration;
+
+ // Max number of frames keyframe could possibly be offset.
+ int64_t keyframeDiff = (1 << mTheoraInfo.keyframe_granule_shift) - 1;
+
+ // Length of frame in usecs.
+ frameDuration =
+ (mTheoraInfo.fps_denominator * USECS_PER_S) / mTheoraInfo.fps_numerator;
+
+ // Total time in usecs keyframe can be offset from any given frame.
+ return frameDuration * keyframeDiff;
+}
+
+bool TheoraState::IsKeyframe(ogg_packet* pkt) {
+ // first bit of packet is 1 for header, 0 for data
+ // second bit of packet is 1 for inter frame, 0 for intra frame
+ return (pkt->bytes >= 1 && (pkt->packet[0] & 0x40) == 0x00);
+}
+
+nsresult TheoraState::PageIn(tainted_opaque_ogg<ogg_page*> aPage) {
+ if (!mActive) return NS_OK;
+ NS_ASSERTION((rlbox::sandbox_static_cast<uint32_t>(sandbox_invoke(
+ *mSandbox, ogg_page_serialno, aPage)) == mSerial)
+ .unverified_safe_because(RLBOX_OGG_PAGE_SERIAL_REASON),
+ "Page must be for this stream!");
+ if (sandbox_invoke(*mSandbox, ogg_stream_pagein, mState, aPage)
+ .unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON) == -1) {
+ return NS_ERROR_FAILURE;
+ }
+ bool foundGp;
+ nsresult res = PacketOutUntilGranulepos(foundGp);
+ if (NS_FAILED(res)) return res;
+ if (foundGp && mDoneReadingHeaders) {
+ // We've found a packet with a granulepos, and we've loaded our metadata
+ // and initialized our decoder. Determine granulepos of buffered packets.
+ ReconstructTheoraGranulepos();
+ for (uint32_t i = 0; i < mUnstamped.Length(); ++i) {
+ OggPacketPtr packet = std::move(mUnstamped[i]);
+#ifdef DEBUG
+ NS_ASSERTION(!IsHeader(packet.get()),
+ "Don't try to recover header packet gp");
+ NS_ASSERTION(packet->granulepos != -1, "Packet must have gp by now");
+#endif
+ mPackets.Append(std::move(packet));
+ }
+ mUnstamped.Clear();
+ }
+ return NS_OK;
+}
+
+// Returns 1 if the Theora info struct is decoding a media of Theora
+// version (maj,min,sub) or later, otherwise returns 0.
+int TheoraVersion(th_info* info, unsigned char maj, unsigned char min,
+ unsigned char sub) {
+ ogg_uint32_t ver = (maj << 16) + (min << 8) + sub;
+ ogg_uint32_t th_ver = (info->version_major << 16) +
+ (info->version_minor << 8) + info->version_subminor;
+ return (th_ver >= ver) ? 1 : 0;
+}
+
+void TheoraState::ReconstructTheoraGranulepos() {
+ if (mUnstamped.Length() == 0) {
+ return;
+ }
+ ogg_int64_t lastGranulepos = mUnstamped[mUnstamped.Length() - 1]->granulepos;
+ NS_ASSERTION(lastGranulepos != -1, "Must know last granulepos");
+
+ // Reconstruct the granulepos (and thus timestamps) of the decoded
+ // frames. Granulepos are stored as ((keyframe<<shift)+offset). We
+ // know the granulepos of the last frame in the list, so we can infer
+ // the granulepos of the intermediate frames using their frame numbers.
+ ogg_int64_t shift = mTheoraInfo.keyframe_granule_shift;
+ ogg_int64_t version_3_2_1 = TheoraVersion(&mTheoraInfo, 3, 2, 1);
+ ogg_int64_t lastFrame =
+ th_granule_frame(mCtx, lastGranulepos) + version_3_2_1;
+ ogg_int64_t firstFrame = lastFrame - mUnstamped.Length() + 1;
+
+ // Until we encounter a keyframe, we'll assume that the "keyframe"
+ // segment of the granulepos is the first frame, or if that causes
+ // the "offset" segment to overflow, we assume the required
+ // keyframe is maximumally offset. Until we encounter a keyframe
+ // the granulepos will probably be wrong, but we can't decode the
+ // frame anyway (since we don't have its keyframe) so it doesn't really
+ // matter.
+ ogg_int64_t keyframe = lastGranulepos >> shift;
+
+ // The lastFrame, firstFrame, keyframe variables, as well as the frame
+ // variable in the loop below, store the frame number for Theora
+ // version >= 3.2.1 streams, and store the frame index for Theora
+ // version < 3.2.1 streams.
+ for (uint32_t i = 0; i < mUnstamped.Length() - 1; ++i) {
+ ogg_int64_t frame = firstFrame + i;
+ ogg_int64_t granulepos;
+ auto& packet = mUnstamped[i];
+ bool isKeyframe = th_packet_iskeyframe(packet.get()) == 1;
+
+ if (isKeyframe) {
+ granulepos = frame << shift;
+ keyframe = frame;
+ } else if (frame >= keyframe &&
+ frame - keyframe < ((ogg_int64_t)1 << shift)) {
+ // (frame - keyframe) won't overflow the "offset" segment of the
+ // granulepos, so it's safe to calculate the granulepos.
+ granulepos = (keyframe << shift) + (frame - keyframe);
+ } else {
+ // (frame - keyframeno) will overflow the "offset" segment of the
+ // granulepos, so we take "keyframe" to be the max possible offset
+ // frame instead.
+ ogg_int64_t k =
+ std::max(frame - (((ogg_int64_t)1 << shift) - 1), version_3_2_1);
+ granulepos = (k << shift) + (frame - k);
+ }
+ // Theora 3.2.1+ granulepos store frame number [1..N], so granulepos
+ // should be > 0.
+ // Theora 3.2.0 granulepos store the frame index [0..(N-1)], so
+ // granulepos should be >= 0.
+ NS_ASSERTION(granulepos >= version_3_2_1,
+ "Invalid granulepos for Theora version");
+
+ // Check that the frame's granule number is one more than the
+ // previous frame's.
+ NS_ASSERTION(
+ i == 0 || th_granule_frame(mCtx, granulepos) ==
+ th_granule_frame(mCtx, mUnstamped[i - 1]->granulepos) + 1,
+ "Granulepos calculation is incorrect!");
+
+ packet->granulepos = granulepos;
+ }
+
+ // Check that the second to last frame's granule number is one less than
+ // the last frame's (the known granule number). If not our granulepos
+ // recovery missed a beat.
+ NS_ASSERTION(mUnstamped.Length() < 2 ||
+ (th_granule_frame(
+ mCtx, mUnstamped[mUnstamped.Length() - 2]->granulepos) +
+ 1) == th_granule_frame(mCtx, lastGranulepos),
+ "Granulepos recovery should catch up with packet->granulepos!");
+}
+
+nsresult VorbisState::Reset() {
+ nsresult res = NS_OK;
+ if (mActive && vorbis_synthesis_restart(&mDsp) != 0) {
+ res = NS_ERROR_FAILURE;
+ }
+ mHeaders.Erase();
+ if (NS_FAILED(OggCodecState::Reset())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mGranulepos = 0;
+ mPrevVorbisBlockSize = 0;
+
+ return res;
+}
+
+VorbisState::VorbisState(rlbox_sandbox_ogg* aSandbox,
+ tainted_opaque_ogg<ogg_page*> aBosPage,
+ uint32_t aSerial)
+ : OggCodecState(aSandbox, aBosPage, aSerial, true),
+ mPrevVorbisBlockSize(0),
+ mGranulepos(0) {
+ MOZ_COUNT_CTOR(VorbisState);
+ vorbis_info_init(&mVorbisInfo);
+ vorbis_comment_init(&mComment);
+ memset(&mDsp, 0, sizeof(vorbis_dsp_state));
+ memset(&mBlock, 0, sizeof(vorbis_block));
+}
+
+VorbisState::~VorbisState() {
+ MOZ_COUNT_DTOR(VorbisState);
+ Reset();
+ vorbis_block_clear(&mBlock);
+ vorbis_dsp_clear(&mDsp);
+ vorbis_info_clear(&mVorbisInfo);
+ vorbis_comment_clear(&mComment);
+}
+
+bool VorbisState::DecodeHeader(OggPacketPtr aPacket) {
+ ogg_packet* packet = aPacket.get(); // Will be owned by mHeaders.
+ mHeaders.Append(std::move(aPacket));
+ mPacketCount++;
+ int ret = vorbis_synthesis_headerin(&mVorbisInfo, &mComment, packet);
+ // We must determine when we've read the last header packet.
+ // vorbis_synthesis_headerin() does not tell us when it's read the last
+ // header, so we must keep track of the headers externally.
+ //
+ // There are 3 header packets, the Identification, Comment, and Setup
+ // headers, which must be in that order. If they're out of order, the file
+ // is invalid. If we've successfully read a header, and it's the setup
+ // header, then we're done reading headers. The first byte of each packet
+ // determines it's type as follows:
+ // 0x1 -> Identification header
+ // 0x3 -> Comment header
+ // 0x5 -> Setup header
+ // For more details of the Vorbis/Ogg containment scheme, see the Vorbis I
+ // Specification, Chapter 4, Codec Setup and Packet Decode:
+ // http://www.xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-580004
+
+ bool isSetupHeader = packet->bytes > 0 && packet->packet[0] == 0x5;
+
+ if (ret < 0 || mPacketCount > 3) {
+ // We've received an error, or the first three packets weren't valid
+ // header packets. Assume bad input. Our caller will deactivate the
+ // bitstream.
+ return false;
+ } else if (!ret && isSetupHeader && mPacketCount == 3) {
+ // Successfully read the three header packets.
+ // The bitstream remains active.
+ mDoneReadingHeaders = true;
+ }
+
+ return true;
+}
+
+bool VorbisState::Init() {
+ if (!mActive) {
+ return false;
+ }
+
+ int ret = vorbis_synthesis_init(&mDsp, &mVorbisInfo);
+ if (ret != 0) {
+ NS_WARNING("vorbis_synthesis_init() failed initializing vorbis bitstream");
+ return mActive = false;
+ }
+ ret = vorbis_block_init(&mDsp, &mBlock);
+ if (ret != 0) {
+ NS_WARNING("vorbis_block_init() failed initializing vorbis bitstream");
+ if (mActive) {
+ vorbis_dsp_clear(&mDsp);
+ }
+ return mActive = false;
+ }
+
+ nsTArray<const unsigned char*> headers;
+ nsTArray<size_t> headerLens;
+ for (size_t i = 0; i < mHeaders.Length(); i++) {
+ headers.AppendElement(mHeaders[i]->packet);
+ headerLens.AppendElement(mHeaders[i]->bytes);
+ }
+ // Save header packets for the decoder
+ VorbisCodecSpecificData vorbisCodecSpecificData{};
+ if (!XiphHeadersToExtradata(vorbisCodecSpecificData.mHeadersBinaryBlob,
+ headers, headerLens)) {
+ return mActive = false;
+ }
+ mHeaders.Erase();
+ mInfo.mMimeType = "audio/vorbis"_ns;
+ mInfo.mRate = mVorbisInfo.rate;
+ mInfo.mChannels = mVorbisInfo.channels;
+ mInfo.mBitDepth = 16;
+ mInfo.mCodecSpecificConfig =
+ AudioCodecSpecificVariant{std::move(vorbisCodecSpecificData)};
+
+ return true;
+}
+
+int64_t VorbisState::Time(int64_t granulepos) {
+ if (!mActive) {
+ return -1;
+ }
+
+ return VorbisState::Time(&mVorbisInfo, granulepos);
+}
+
+int64_t VorbisState::Time(vorbis_info* aInfo, int64_t aGranulepos) {
+ if (aGranulepos == -1 || aInfo->rate == 0) {
+ return -1;
+ }
+ CheckedInt64 t = SaferMultDiv(aGranulepos, USECS_PER_S, aInfo->rate);
+ return t.isValid() ? t.value() : 0;
+}
+
+int64_t VorbisState::PacketDuration(ogg_packet* aPacket) {
+ if (!mActive) {
+ return -1;
+ }
+ if (aPacket->granulepos == -1) {
+ return -1;
+ }
+ // @FIXME store these in a more stable place
+ if (mVorbisPacketSamples.count(aPacket) == 0) {
+ // We haven't seen this packet, don't know its size?
+ return -1;
+ }
+
+ long samples = mVorbisPacketSamples[aPacket];
+ return Time(samples);
+}
+
+bool VorbisState::IsHeader(ogg_packet* aPacket) {
+ // The first byte in each Vorbis header packet is either 0x01, 0x03, or 0x05,
+ // i.e. the first bit is odd. Audio data packets have their first bit as 0x0.
+ // Any packet with its first bit set cannot be a data packet, it's a
+ // (possibly invalid) header packet.
+ // See: http://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-610004.2.1
+ return aPacket->bytes > 0 ? (aPacket->packet[0] & 0x1) : false;
+}
+
+UniquePtr<MetadataTags> VorbisState::GetTags() {
+ NS_ASSERTION(mComment.user_comments, "no vorbis comment strings!");
+ NS_ASSERTION(mComment.comment_lengths, "no vorbis comment lengths!");
+ auto tags = MakeUnique<MetadataTags>();
+ for (int i = 0; i < mComment.comments; i++) {
+ AddVorbisComment(tags, mComment.user_comments[i],
+ mComment.comment_lengths[i]);
+ }
+ return tags;
+}
+
+nsresult VorbisState::PageIn(tainted_opaque_ogg<ogg_page*> aPage) {
+ if (!mActive) {
+ return NS_OK;
+ }
+ NS_ASSERTION((rlbox::sandbox_static_cast<uint32_t>(sandbox_invoke(
+ *mSandbox, ogg_page_serialno, aPage)) == mSerial)
+ .unverified_safe_because(RLBOX_OGG_PAGE_SERIAL_REASON),
+ "Page must be for this stream!");
+ if (sandbox_invoke(*mSandbox, ogg_stream_pagein, mState, aPage)
+ .unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON) == -1) {
+ return NS_ERROR_FAILURE;
+ }
+ bool foundGp;
+ nsresult res = PacketOutUntilGranulepos(foundGp);
+ if (NS_FAILED(res)) {
+ return res;
+ }
+ if (foundGp && mDoneReadingHeaders) {
+ // We've found a packet with a granulepos, and we've loaded our metadata
+ // and initialized our decoder. Determine granulepos of buffered packets.
+ ReconstructVorbisGranulepos();
+ for (uint32_t i = 0; i < mUnstamped.Length(); ++i) {
+ OggPacketPtr packet = std::move(mUnstamped[i]);
+ AssertHasRecordedPacketSamples(packet.get());
+ NS_ASSERTION(!IsHeader(packet.get()),
+ "Don't try to recover header packet gp");
+ NS_ASSERTION(packet->granulepos != -1, "Packet must have gp by now");
+ mPackets.Append(std::move(packet));
+ }
+ mUnstamped.Clear();
+ }
+ return NS_OK;
+}
+
+void VorbisState::ReconstructVorbisGranulepos() {
+ // The number of samples in a Vorbis packet is:
+ // window_blocksize(previous_packet)/4+window_blocksize(current_packet)/4
+ // See: http://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-230001.3.2
+ // So we maintain mPrevVorbisBlockSize, the block size of the last packet
+ // encountered. We also maintain mGranulepos, which is the granulepos of
+ // the last encountered packet. This enables us to give granulepos to
+ // packets when the last packet in mUnstamped doesn't have a granulepos
+ // (for example if the stream was truncated).
+ //
+ // We validate our prediction of the number of samples decoded when
+ // VALIDATE_VORBIS_SAMPLE_CALCULATION is defined by recording the predicted
+ // number of samples, and verifing we extract that many when decoding
+ // each packet.
+
+ NS_ASSERTION(mUnstamped.Length() > 0, "Length must be > 0");
+ auto& last = mUnstamped.LastElement();
+ NS_ASSERTION(last->e_o_s || last->granulepos >= 0,
+ "Must know last granulepos!");
+ if (mUnstamped.Length() == 1) {
+ auto& packet = mUnstamped[0];
+ long blockSize = vorbis_packet_blocksize(&mVorbisInfo, packet.get());
+ if (blockSize < 0) {
+ // On failure vorbis_packet_blocksize returns < 0. If we've got
+ // a bad packet, we just assume that decode will have to skip this
+ // packet, i.e. assume 0 samples are decodable from this packet.
+ blockSize = 0;
+ mPrevVorbisBlockSize = 0;
+ }
+ long samples = mPrevVorbisBlockSize / 4 + blockSize / 4;
+ mPrevVorbisBlockSize = blockSize;
+ if (packet->granulepos == -1) {
+ packet->granulepos = mGranulepos + samples;
+ }
+
+ // Account for a partial last frame
+ if (packet->e_o_s && packet->granulepos >= mGranulepos) {
+ samples = packet->granulepos - mGranulepos;
+ }
+
+ mGranulepos = packet->granulepos;
+ RecordVorbisPacketSamples(packet.get(), samples);
+ return;
+ }
+
+ bool unknownGranulepos = last->granulepos == -1;
+ int totalSamples = 0;
+ for (int32_t i = mUnstamped.Length() - 1; i > 0; i--) {
+ auto& packet = mUnstamped[i];
+ auto& prev = mUnstamped[i - 1];
+ ogg_int64_t granulepos = packet->granulepos;
+ NS_ASSERTION(granulepos != -1, "Must know granulepos!");
+ long prevBlockSize = vorbis_packet_blocksize(&mVorbisInfo, prev.get());
+ long blockSize = vorbis_packet_blocksize(&mVorbisInfo, packet.get());
+
+ if (blockSize < 0 || prevBlockSize < 0) {
+ // On failure vorbis_packet_blocksize returns < 0. If we've got
+ // a bad packet, we just assume that decode will have to skip this
+ // packet, i.e. assume 0 samples are decodable from this packet.
+ blockSize = 0;
+ prevBlockSize = 0;
+ }
+
+ long samples = prevBlockSize / 4 + blockSize / 4;
+ totalSamples += samples;
+ prev->granulepos = granulepos - samples;
+ RecordVorbisPacketSamples(packet.get(), samples);
+ }
+
+ if (unknownGranulepos) {
+ for (uint32_t i = 0; i < mUnstamped.Length(); i++) {
+ mUnstamped[i]->granulepos += mGranulepos + totalSamples + 1;
+ }
+ }
+
+ auto& first = mUnstamped[0];
+ long blockSize = vorbis_packet_blocksize(&mVorbisInfo, first.get());
+ if (blockSize < 0) {
+ mPrevVorbisBlockSize = 0;
+ blockSize = 0;
+ }
+
+ long samples = (mPrevVorbisBlockSize == 0)
+ ? 0
+ : mPrevVorbisBlockSize / 4 + blockSize / 4;
+ int64_t start = first->granulepos - samples;
+ RecordVorbisPacketSamples(first.get(), samples);
+
+ if (last->e_o_s && start < mGranulepos) {
+ // We've calculated that there are more samples in this page than its
+ // granulepos claims, and it's the last page in the stream. This is legal,
+ // and we will need to prune the trailing samples when we come to decode it.
+ // We must correct the timestamps so that they follow the last Vorbis page's
+ // samples.
+ int64_t pruned = mGranulepos - start;
+ for (uint32_t i = 0; i < mUnstamped.Length() - 1; i++) {
+ mUnstamped[i]->granulepos += pruned;
+ }
+#ifdef VALIDATE_VORBIS_SAMPLE_CALCULATION
+ mVorbisPacketSamples[last.get()] -= pruned;
+#endif
+ }
+
+ mPrevVorbisBlockSize = vorbis_packet_blocksize(&mVorbisInfo, last.get());
+ mPrevVorbisBlockSize = std::max(static_cast<long>(0), mPrevVorbisBlockSize);
+ mGranulepos = last->granulepos;
+}
+
+OpusState::OpusState(rlbox_sandbox_ogg* aSandbox,
+ tainted_opaque_ogg<ogg_page*> aBosPage, uint32_t aSerial)
+ : OggCodecState(aSandbox, aBosPage, aSerial, true),
+ mParser(nullptr),
+ mDecoder(nullptr),
+ mPrevPacketGranulepos(0),
+ mPrevPageGranulepos(0) {
+ MOZ_COUNT_CTOR(OpusState);
+}
+
+OpusState::~OpusState() {
+ MOZ_COUNT_DTOR(OpusState);
+ Reset();
+
+ if (mDecoder) {
+ opus_multistream_decoder_destroy(mDecoder);
+ mDecoder = nullptr;
+ }
+}
+
+nsresult OpusState::Reset() { return Reset(false); }
+
+nsresult OpusState::Reset(bool aStart) {
+ nsresult res = NS_OK;
+
+ if (mActive && mDecoder) {
+ // Reset the decoder.
+ opus_multistream_decoder_ctl(mDecoder, OPUS_RESET_STATE);
+ // This lets us distinguish the first page being the last page vs. just
+ // not having processed the previous page when we encounter the last page.
+ mPrevPageGranulepos = aStart ? 0 : -1;
+ mPrevPacketGranulepos = aStart ? 0 : -1;
+ }
+
+ // Clear queued data.
+ if (NS_FAILED(OggCodecState::Reset())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ LOG(LogLevel::Debug, ("Opus decoder reset"));
+
+ return res;
+}
+
+bool OpusState::Init(void) {
+ if (!mActive) {
+ return false;
+ }
+
+ int error;
+
+ NS_ASSERTION(mDecoder == nullptr, "leaking OpusDecoder");
+
+ mDecoder = opus_multistream_decoder_create(
+ mParser->mRate, mParser->mChannels, mParser->mStreams,
+ mParser->mCoupledStreams, mParser->mMappingTable, &error);
+
+ mInfo.mMimeType = "audio/opus"_ns;
+ mInfo.mRate = mParser->mRate;
+ mInfo.mChannels = mParser->mChannels;
+ mInfo.mBitDepth = 16;
+ // Save preskip & the first header packet for the Opus decoder
+ OpusCodecSpecificData opusData;
+ opusData.mContainerCodecDelayMicroSeconds = Time(0, mParser->mPreSkip);
+
+ if (!mHeaders.PeekFront()) {
+ return false;
+ }
+ opusData.mHeadersBinaryBlob->AppendElements(mHeaders.PeekFront()->packet,
+ mHeaders.PeekFront()->bytes);
+ mInfo.mCodecSpecificConfig = AudioCodecSpecificVariant{std::move(opusData)};
+
+ mHeaders.Erase();
+ LOG(LogLevel::Debug, ("Opus decoder init"));
+
+ return error == OPUS_OK;
+}
+
+bool OpusState::DecodeHeader(OggPacketPtr aPacket) {
+ switch (mPacketCount++) {
+ // Parse the id header.
+ case 0:
+ mParser = MakeUnique<OpusParser>();
+ if (!mParser->DecodeHeader(aPacket->packet, aPacket->bytes)) {
+ return false;
+ }
+ mHeaders.Append(std::move(aPacket));
+ break;
+
+ // Parse the metadata header.
+ case 1:
+ if (!mParser->DecodeTags(aPacket->packet, aPacket->bytes)) {
+ return false;
+ }
+ break;
+
+ // We made it to the first data packet (which includes reconstructing
+ // timestamps for it in PageIn). Success!
+ default:
+ mDoneReadingHeaders = true;
+ // Put it back on the queue so we can decode it.
+ mPackets.PushFront(std::move(aPacket));
+ break;
+ }
+ return true;
+}
+
+/* Construct and return a tags hashmap from our internal array */
+UniquePtr<MetadataTags> OpusState::GetTags() {
+ auto tags = MakeUnique<MetadataTags>();
+ for (uint32_t i = 0; i < mParser->mTags.Length(); i++) {
+ AddVorbisComment(tags, mParser->mTags[i].Data(),
+ mParser->mTags[i].Length());
+ }
+
+ return tags;
+}
+
+/* Return the timestamp (in microseconds) equivalent to a granulepos. */
+int64_t OpusState::Time(int64_t aGranulepos) {
+ if (!mActive) {
+ return -1;
+ }
+
+ return Time(mParser->mPreSkip, aGranulepos);
+}
+
+int64_t OpusState::Time(int aPreSkip, int64_t aGranulepos) {
+ if (aGranulepos < 0) {
+ return -1;
+ }
+
+ // Ogg Opus always runs at a granule rate of 48 kHz.
+ CheckedInt64 t = SaferMultDiv(aGranulepos - aPreSkip, USECS_PER_S, 48000);
+ return t.isValid() ? t.value() : -1;
+}
+
+bool OpusState::IsHeader(ogg_packet* aPacket) {
+ return aPacket->bytes >= 16 && (!memcmp(aPacket->packet, "OpusHead", 8) ||
+ !memcmp(aPacket->packet, "OpusTags", 8));
+}
+
+nsresult OpusState::PageIn(tainted_opaque_ogg<ogg_page*> aPage) {
+ if (!mActive) {
+ return NS_OK;
+ }
+ NS_ASSERTION((rlbox::sandbox_static_cast<uint32_t>(sandbox_invoke(
+ *mSandbox, ogg_page_serialno, aPage)) == mSerial)
+ .unverified_safe_because(RLBOX_OGG_PAGE_SERIAL_REASON),
+ "Page must be for this stream!");
+ if (sandbox_invoke(*mSandbox, ogg_stream_pagein, mState, aPage)
+ .unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON) == -1) {
+ return NS_ERROR_FAILURE;
+ }
+
+ bool haveGranulepos;
+ nsresult rv = PacketOutUntilGranulepos(haveGranulepos);
+ if (NS_FAILED(rv) || !haveGranulepos || mPacketCount < 2) {
+ return rv;
+ }
+ if (!ReconstructOpusGranulepos()) {
+ return NS_ERROR_FAILURE;
+ }
+ for (uint32_t i = 0; i < mUnstamped.Length(); i++) {
+ OggPacketPtr packet = std::move(mUnstamped[i]);
+ NS_ASSERTION(!IsHeader(packet.get()), "Don't try to play a header packet");
+ NS_ASSERTION(packet->granulepos != -1, "Packet should have a granulepos");
+ mPackets.Append(std::move(packet));
+ }
+ mUnstamped.Clear();
+ return NS_OK;
+}
+
+// Helper method to return the change in granule position due to an Opus packet
+// (as distinct from the number of samples in the packet, which depends on the
+// decoder rate). It should work with a multistream Opus file, and continue to
+// work should we ever allow the decoder to decode at a rate other than 48 kHz.
+// It even works before we've created the actual Opus decoder.
+static int GetOpusDeltaGP(ogg_packet* packet) {
+ int nframes;
+ nframes = opus_packet_get_nb_frames(packet->packet, packet->bytes);
+ if (nframes > 0) {
+ return nframes * opus_packet_get_samples_per_frame(packet->packet, 48000);
+ }
+ NS_WARNING("Invalid Opus packet.");
+ return nframes;
+}
+
+int64_t OpusState::PacketDuration(ogg_packet* aPacket) {
+ CheckedInt64 t = SaferMultDiv(GetOpusDeltaGP(aPacket), USECS_PER_S, 48000);
+ return t.isValid() ? t.value() : -1;
+}
+
+bool OpusState::ReconstructOpusGranulepos(void) {
+ NS_ASSERTION(mUnstamped.Length() > 0, "Must have unstamped packets");
+ NS_ASSERTION(mUnstamped.LastElement()->e_o_s ||
+ mUnstamped.LastElement()->granulepos > 0,
+ "Must know last granulepos!");
+ int64_t gp;
+ // If this is the last page, and we've seen at least one previous page (or
+ // this is the first page)...
+ if (mUnstamped.LastElement()->e_o_s) {
+ auto& last = mUnstamped.LastElement();
+ if (mPrevPageGranulepos != -1) {
+ // If this file only has one page and the final granule position is
+ // smaller than the pre-skip amount, we MUST reject the stream.
+ if (!mDoneReadingHeaders && last->granulepos < mParser->mPreSkip)
+ return false;
+ int64_t last_gp = last->granulepos;
+ gp = mPrevPageGranulepos;
+ // Loop through the packets forwards, adding the current packet's
+ // duration to the previous granulepos to get the value for the
+ // current packet.
+ for (uint32_t i = 0; i < mUnstamped.Length() - 1; ++i) {
+ auto& packet = mUnstamped[i];
+ int offset = GetOpusDeltaGP(packet.get());
+ // Check for error (negative offset) and overflow.
+ if (offset >= 0 && gp <= INT64_MAX - offset) {
+ gp += offset;
+ if (gp >= last_gp) {
+ NS_WARNING("Opus end trimming removed more than a full packet.");
+ // We were asked to remove a full packet's worth of data or more.
+ // Encoders SHOULD NOT produce streams like this, but we'll handle
+ // it for them anyway.
+ gp = last_gp;
+ mUnstamped.RemoveLastElements(mUnstamped.Length() - (i + 1));
+ packet->e_o_s = 1;
+ }
+ }
+ packet->granulepos = gp;
+ }
+ mPrevPageGranulepos = last_gp;
+ return true;
+ } else {
+ NS_WARNING("No previous granule position to use for Opus end trimming.");
+ // If we don't have a previous granule position, fall through.
+ // We simply won't trim any samples from the end.
+ // TODO: Are we guaranteed to have seen a previous page if there is one?
+ }
+ }
+
+ auto& last = mUnstamped.LastElement();
+ gp = last->granulepos;
+ // Loop through the packets backwards, subtracting the next
+ // packet's duration from its granulepos to get the value
+ // for the current packet.
+ for (uint32_t i = mUnstamped.Length() - 1; i > 0; i--) {
+ int offset = GetOpusDeltaGP(mUnstamped[i].get());
+ // Check for error (negative offset) and overflow.
+ if (offset >= 0) {
+ if (offset <= gp) {
+ gp -= offset;
+ } else {
+ // If the granule position of the first data page is smaller than the
+ // number of decodable audio samples on that page, then we MUST reject
+ // the stream.
+ if (!mDoneReadingHeaders) return false;
+ // It's too late to reject the stream.
+ // If we get here, this almost certainly means the file has screwed-up
+ // timestamps somewhere after the first page.
+ NS_WARNING("Clamping negative Opus granulepos to zero.");
+ gp = 0;
+ }
+ }
+ mUnstamped[i - 1]->granulepos = gp;
+ }
+
+ // Check to make sure the first granule position is at least as large as the
+ // total number of samples decodable from the first page with completed
+ // packets. This requires looking at the duration of the first packet, too.
+ // We MUST reject such streams.
+ if (!mDoneReadingHeaders && GetOpusDeltaGP(mUnstamped[0].get()) > gp) {
+ return false;
+ }
+ mPrevPageGranulepos = last->granulepos;
+ return true;
+}
+
+already_AddRefed<MediaRawData> OpusState::PacketOutAsMediaRawData() {
+ ogg_packet* packet = PacketPeek();
+ if (!packet) {
+ return nullptr;
+ }
+
+ uint32_t frames = 0;
+ const int64_t endFrame = packet->granulepos;
+
+ if (packet->e_o_s) {
+ frames = GetOpusDeltaGP(packet);
+ }
+
+ RefPtr<MediaRawData> data = OggCodecState::PacketOutAsMediaRawData();
+ if (!data) {
+ return nullptr;
+ }
+
+ if (data->mEOS && mPrevPacketGranulepos != -1) {
+ // If this is the last packet, perform end trimming.
+ int64_t startFrame = mPrevPacketGranulepos;
+ frames -= std::max<int64_t>(
+ 0, std::min(endFrame - startFrame, static_cast<int64_t>(frames)));
+ data->mDiscardPadding = frames;
+ }
+
+ // Save this packet's granule position in case we need to perform end
+ // trimming on the next packet.
+ mPrevPacketGranulepos = endFrame;
+
+ return data.forget();
+}
+
+FlacState::FlacState(rlbox_sandbox_ogg* aSandbox,
+ tainted_opaque_ogg<ogg_page*> aBosPage, uint32_t aSerial)
+ : OggCodecState(aSandbox, aBosPage, aSerial, true) {}
+
+bool FlacState::DecodeHeader(OggPacketPtr aPacket) {
+ if (mParser.DecodeHeaderBlock(aPacket->packet, aPacket->bytes).isErr()) {
+ return false;
+ }
+ if (mParser.HasFullMetadata()) {
+ mDoneReadingHeaders = true;
+ }
+ return true;
+}
+
+int64_t FlacState::Time(int64_t granulepos) {
+ if (!mParser.mInfo.IsValid()) {
+ return -1;
+ }
+ CheckedInt64 t = SaferMultDiv(granulepos, USECS_PER_S, mParser.mInfo.mRate);
+ if (!t.isValid()) {
+ return -1;
+ }
+ return t.value();
+}
+
+int64_t FlacState::PacketDuration(ogg_packet* aPacket) {
+ return mParser.BlockDuration(aPacket->packet, aPacket->bytes);
+}
+
+bool FlacState::IsHeader(ogg_packet* aPacket) {
+ auto res = mParser.IsHeaderBlock(aPacket->packet, aPacket->bytes);
+ return res.isOk() ? res.unwrap() : false;
+}
+
+nsresult FlacState::PageIn(tainted_opaque_ogg<ogg_page*> aPage) {
+ if (!mActive) {
+ return NS_OK;
+ }
+ NS_ASSERTION((rlbox::sandbox_static_cast<uint32_t>(sandbox_invoke(
+ *mSandbox, ogg_page_serialno, aPage)) == mSerial)
+ .unverified_safe_because(RLBOX_OGG_PAGE_SERIAL_REASON),
+ "Page must be for this stream!");
+ if (sandbox_invoke(*mSandbox, ogg_stream_pagein, mState, aPage)
+ .unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON) == -1) {
+ return NS_ERROR_FAILURE;
+ }
+ bool foundGp;
+ nsresult res = PacketOutUntilGranulepos(foundGp);
+ if (NS_FAILED(res)) {
+ return res;
+ }
+ if (foundGp && mDoneReadingHeaders) {
+ // We've found a packet with a granulepos, and we've loaded our metadata
+ // and initialized our decoder. Determine granulepos of buffered packets.
+ ReconstructFlacGranulepos();
+ for (uint32_t i = 0; i < mUnstamped.Length(); ++i) {
+ OggPacketPtr packet = std::move(mUnstamped[i]);
+ NS_ASSERTION(!IsHeader(packet.get()),
+ "Don't try to recover header packet gp");
+ NS_ASSERTION(packet->granulepos != -1, "Packet must have gp by now");
+ mPackets.Append(std::move(packet));
+ }
+ mUnstamped.Clear();
+ }
+ return NS_OK;
+}
+
+// Return a hash table with tag metadata.
+UniquePtr<MetadataTags> FlacState::GetTags() { return mParser.GetTags(); }
+
+const TrackInfo* FlacState::GetInfo() const { return &mParser.mInfo; }
+
+bool FlacState::ReconstructFlacGranulepos(void) {
+ NS_ASSERTION(mUnstamped.Length() > 0, "Must have unstamped packets");
+ auto& last = mUnstamped.LastElement();
+ NS_ASSERTION(last->e_o_s || last->granulepos > 0,
+ "Must know last granulepos!");
+ int64_t gp;
+
+ gp = last->granulepos;
+ // Loop through the packets backwards, subtracting the next
+ // packet's duration from its granulepos to get the value
+ // for the current packet.
+ for (uint32_t i = mUnstamped.Length() - 1; i > 0; i--) {
+ int offset =
+ mParser.BlockDuration(mUnstamped[i]->packet, mUnstamped[i]->bytes);
+ // Check for error (negative offset) and overflow.
+ if (offset >= 0) {
+ if (offset <= gp) {
+ gp -= offset;
+ } else {
+ // If the granule position of the first data page is smaller than the
+ // number of decodable audio samples on that page, then we MUST reject
+ // the stream.
+ if (!mDoneReadingHeaders) {
+ return false;
+ }
+ // It's too late to reject the stream.
+ // If we get here, this almost certainly means the file has screwed-up
+ // timestamps somewhere after the first page.
+ NS_WARNING("Clamping negative granulepos to zero.");
+ gp = 0;
+ }
+ }
+ mUnstamped[i - 1]->granulepos = gp;
+ }
+
+ return true;
+}
+
+SkeletonState::SkeletonState(rlbox_sandbox_ogg* aSandbox,
+ tainted_opaque_ogg<ogg_page*> aBosPage,
+ uint32_t aSerial)
+ : OggCodecState(aSandbox, aBosPage, aSerial, true),
+ mVersion(0),
+ mPresentationTime(0),
+ mLength(0) {
+ MOZ_COUNT_CTOR(SkeletonState);
+}
+
+SkeletonState::~SkeletonState() { MOZ_COUNT_DTOR(SkeletonState); }
+
+// Support for Ogg Skeleton 4.0, as per specification at:
+// http://wiki.xiph.org/Ogg_Skeleton_4
+
+// Minimum length in bytes of a Skeleton header packet.
+static const long SKELETON_MIN_HEADER_LEN = 28;
+static const long SKELETON_4_0_MIN_HEADER_LEN = 80;
+
+// Minimum length in bytes of a Skeleton 4.0 index packet.
+static const long SKELETON_4_0_MIN_INDEX_LEN = 42;
+
+// Minimum length in bytes of a Skeleton 3.0/4.0 Fisbone packet.
+static const long SKELETON_MIN_FISBONE_LEN = 52;
+
+// Minimum possible size of a compressed index keypoint.
+static const size_t MIN_KEY_POINT_SIZE = 2;
+
+// Byte offset of the major and minor version numbers in the
+// Ogg Skeleton 4.0 header packet.
+static const size_t SKELETON_VERSION_MAJOR_OFFSET = 8;
+static const size_t SKELETON_VERSION_MINOR_OFFSET = 10;
+
+// Byte-offsets of the presentation time numerator and denominator
+static const size_t SKELETON_PRESENTATION_TIME_NUMERATOR_OFFSET = 12;
+static const size_t SKELETON_PRESENTATION_TIME_DENOMINATOR_OFFSET = 20;
+
+// Byte-offsets of the length of file field in the Skeleton 4.0 header packet.
+static const size_t SKELETON_FILE_LENGTH_OFFSET = 64;
+
+// Byte-offsets of the fields in the Skeleton index packet.
+static const size_t INDEX_SERIALNO_OFFSET = 6;
+static const size_t INDEX_NUM_KEYPOINTS_OFFSET = 10;
+static const size_t INDEX_TIME_DENOM_OFFSET = 18;
+static const size_t INDEX_FIRST_NUMER_OFFSET = 26;
+static const size_t INDEX_LAST_NUMER_OFFSET = 34;
+static const size_t INDEX_KEYPOINT_OFFSET = 42;
+
+// Byte-offsets of the fields in the Skeleton Fisbone packet.
+static const size_t FISBONE_MSG_FIELDS_OFFSET = 8;
+static const size_t FISBONE_SERIALNO_OFFSET = 12;
+
+static bool IsSkeletonBOS(ogg_packet* aPacket) {
+ static_assert(SKELETON_MIN_HEADER_LEN >= 8,
+ "Minimum length of skeleton BOS header incorrect");
+ return aPacket->bytes >= SKELETON_MIN_HEADER_LEN &&
+ memcmp(reinterpret_cast<char*>(aPacket->packet), "fishead", 8) == 0;
+}
+
+static bool IsSkeletonIndex(ogg_packet* aPacket) {
+ static_assert(SKELETON_4_0_MIN_INDEX_LEN >= 5,
+ "Minimum length of skeleton index header incorrect");
+ return aPacket->bytes >= SKELETON_4_0_MIN_INDEX_LEN &&
+ memcmp(reinterpret_cast<char*>(aPacket->packet), "index", 5) == 0;
+}
+
+static bool IsSkeletonFisbone(ogg_packet* aPacket) {
+ static_assert(SKELETON_MIN_FISBONE_LEN >= 8,
+ "Minimum length of skeleton fisbone header incorrect");
+ return aPacket->bytes >= SKELETON_MIN_FISBONE_LEN &&
+ memcmp(reinterpret_cast<char*>(aPacket->packet), "fisbone", 8) == 0;
+}
+
+// Reads a variable length encoded integer at p. Will not read
+// past aLimit. Returns pointer to character after end of integer.
+static const unsigned char* ReadVariableLengthInt(const unsigned char* p,
+ const unsigned char* aLimit,
+ int64_t& n) {
+ int shift = 0;
+ int64_t byte = 0;
+ n = 0;
+ while (p < aLimit && (byte & 0x80) != 0x80 && shift < 57) {
+ byte = static_cast<int64_t>(*p);
+ n |= ((byte & 0x7f) << shift);
+ shift += 7;
+ p++;
+ }
+ return p;
+}
+
+bool SkeletonState::DecodeIndex(ogg_packet* aPacket) {
+ NS_ASSERTION(aPacket->bytes >= SKELETON_4_0_MIN_INDEX_LEN,
+ "Index must be at least minimum size");
+ if (!mActive) {
+ return false;
+ }
+
+ uint32_t serialno =
+ LittleEndian::readUint32(aPacket->packet + INDEX_SERIALNO_OFFSET);
+ int64_t numKeyPoints =
+ LittleEndian::readInt64(aPacket->packet + INDEX_NUM_KEYPOINTS_OFFSET);
+
+ int64_t endTime = 0, startTime = 0;
+ const unsigned char* p = aPacket->packet;
+
+ int64_t timeDenom =
+ LittleEndian::readInt64(aPacket->packet + INDEX_TIME_DENOM_OFFSET);
+ if (timeDenom == 0) {
+ LOG(LogLevel::Debug, ("Ogg Skeleton Index packet for stream %u has 0 "
+ "timestamp denominator.",
+ serialno));
+ return (mActive = false);
+ }
+
+ // Extract the start time.
+ int64_t timeRawInt = LittleEndian::readInt64(p + INDEX_FIRST_NUMER_OFFSET);
+ CheckedInt64 t = SaferMultDiv(timeRawInt, USECS_PER_S, timeDenom);
+ if (!t.isValid()) {
+ return (mActive = false);
+ } else {
+ startTime = t.value();
+ }
+
+ // Extract the end time.
+ timeRawInt = LittleEndian::readInt64(p + INDEX_LAST_NUMER_OFFSET);
+ t = SaferMultDiv(timeRawInt, USECS_PER_S, timeDenom);
+ if (!t.isValid()) {
+ return (mActive = false);
+ } else {
+ endTime = t.value();
+ }
+
+ // Check the numKeyPoints value read, ensure we're not going to run out of
+ // memory while trying to decode the index packet.
+ CheckedInt64 minPacketSize =
+ (CheckedInt64(numKeyPoints) * MIN_KEY_POINT_SIZE) + INDEX_KEYPOINT_OFFSET;
+ if (!minPacketSize.isValid()) {
+ return (mActive = false);
+ }
+
+ int64_t sizeofIndex = aPacket->bytes - INDEX_KEYPOINT_OFFSET;
+ int64_t maxNumKeyPoints = sizeofIndex / MIN_KEY_POINT_SIZE;
+ if (aPacket->bytes < minPacketSize.value() ||
+ numKeyPoints > maxNumKeyPoints || numKeyPoints < 0) {
+ // Packet size is less than the theoretical minimum size, or the packet is
+ // claiming to store more keypoints than it's capable of storing. This means
+ // that the numKeyPoints field is too large or small for the packet to
+ // possibly contain as many packets as it claims to, so the numKeyPoints
+ // field is possibly malicious. Don't try decoding this index, we may run
+ // out of memory.
+ LOG(LogLevel::Debug, ("Possibly malicious number of key points reported "
+ "(%" PRId64 ") in index packet for stream %u.",
+ numKeyPoints, serialno));
+ return (mActive = false);
+ }
+
+ UniquePtr<nsKeyFrameIndex> keyPoints(new nsKeyFrameIndex(startTime, endTime));
+
+ p = aPacket->packet + INDEX_KEYPOINT_OFFSET;
+ const unsigned char* limit = aPacket->packet + aPacket->bytes;
+ int64_t numKeyPointsRead = 0;
+ CheckedInt64 offset = 0;
+ CheckedInt64 time = 0;
+ while (p < limit && numKeyPointsRead < numKeyPoints) {
+ int64_t delta = 0;
+ p = ReadVariableLengthInt(p, limit, delta);
+ offset += delta;
+ if (p == limit || !offset.isValid() || offset.value() > mLength ||
+ offset.value() < 0) {
+ return (mActive = false);
+ }
+ p = ReadVariableLengthInt(p, limit, delta);
+ time += delta;
+ if (!time.isValid() || time.value() > endTime || time.value() < startTime) {
+ return (mActive = false);
+ }
+ CheckedInt64 timeUsecs = SaferMultDiv(time.value(), USECS_PER_S, timeDenom);
+ if (!timeUsecs.isValid()) {
+ return (mActive = false);
+ }
+ keyPoints->Add(offset.value(), timeUsecs.value());
+ numKeyPointsRead++;
+ }
+
+ int32_t keyPointsRead = keyPoints->Length();
+ if (keyPointsRead > 0) {
+ mIndex.InsertOrUpdate(serialno, std::move(keyPoints));
+ }
+
+ LOG(LogLevel::Debug, ("Loaded %d keypoints for Skeleton on stream %u",
+ keyPointsRead, serialno));
+ return true;
+}
+
+nsresult SkeletonState::IndexedSeekTargetForTrack(uint32_t aSerialno,
+ int64_t aTarget,
+ nsKeyPoint& aResult) {
+ nsKeyFrameIndex* index = nullptr;
+ mIndex.Get(aSerialno, &index);
+
+ if (!index || index->Length() == 0 || aTarget < index->mStartTime ||
+ aTarget > index->mEndTime) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Binary search to find the last key point with time less than target.
+ int start = 0;
+ int end = index->Length() - 1;
+ while (end > start) {
+ int mid = start + ((end - start + 1) >> 1);
+ if (index->Get(mid).mTime == aTarget) {
+ start = mid;
+ break;
+ } else if (index->Get(mid).mTime < aTarget) {
+ start = mid;
+ } else {
+ end = mid - 1;
+ }
+ }
+
+ aResult = index->Get(start);
+ NS_ASSERTION(aResult.mTime <= aTarget, "Result should have time <= target");
+ return NS_OK;
+}
+
+nsresult SkeletonState::IndexedSeekTarget(int64_t aTarget,
+ nsTArray<uint32_t>& aTracks,
+ nsSeekTarget& aResult) {
+ if (!mActive || mVersion < SKELETON_VERSION(4, 0)) {
+ return NS_ERROR_FAILURE;
+ }
+ // Loop over all requested tracks' indexes, and get the keypoint for that
+ // seek target. Record the keypoint with the lowest offset, this will be
+ // our seek result. User must seek to the one with lowest offset to ensure we
+ // pass "keyframes" on all tracks when we decode forwards to the seek target.
+ nsSeekTarget r;
+ for (uint32_t i = 0; i < aTracks.Length(); i++) {
+ nsKeyPoint k;
+ if (NS_SUCCEEDED(IndexedSeekTargetForTrack(aTracks[i], aTarget, k)) &&
+ k.mOffset < r.mKeyPoint.mOffset) {
+ r.mKeyPoint = k;
+ r.mSerial = aTracks[i];
+ }
+ }
+ if (r.IsNull()) {
+ return NS_ERROR_FAILURE;
+ }
+ LOG(LogLevel::Debug,
+ ("Indexed seek target for time %" PRId64 " is offset %" PRId64, aTarget,
+ r.mKeyPoint.mOffset));
+ aResult = r;
+ return NS_OK;
+}
+
+nsresult SkeletonState::GetDuration(const nsTArray<uint32_t>& aTracks,
+ int64_t& aDuration) {
+ if (!mActive || mVersion < SKELETON_VERSION(4, 0) || !HasIndex() ||
+ aTracks.Length() == 0) {
+ return NS_ERROR_FAILURE;
+ }
+ int64_t endTime = INT64_MIN;
+ int64_t startTime = INT64_MAX;
+ for (uint32_t i = 0; i < aTracks.Length(); i++) {
+ nsKeyFrameIndex* index = nullptr;
+ mIndex.Get(aTracks[i], &index);
+ if (!index) {
+ // Can't get the timestamps for one of the required tracks, fail.
+ return NS_ERROR_FAILURE;
+ }
+ if (index->mEndTime > endTime) {
+ endTime = index->mEndTime;
+ }
+ if (index->mStartTime < startTime) {
+ startTime = index->mStartTime;
+ }
+ }
+ NS_ASSERTION(endTime > startTime, "Duration must be positive");
+ CheckedInt64 duration = CheckedInt64(endTime) - startTime;
+ aDuration = duration.isValid() ? duration.value() : 0;
+ return duration.isValid() ? NS_OK : NS_ERROR_FAILURE;
+}
+
+bool SkeletonState::DecodeFisbone(ogg_packet* aPacket) {
+ if (aPacket->bytes < static_cast<long>(FISBONE_MSG_FIELDS_OFFSET + 4)) {
+ return false;
+ }
+ uint32_t offsetMsgField =
+ LittleEndian::readUint32(aPacket->packet + FISBONE_MSG_FIELDS_OFFSET);
+
+ if (aPacket->bytes < static_cast<long>(FISBONE_SERIALNO_OFFSET + 4)) {
+ return false;
+ }
+ uint32_t serialno =
+ LittleEndian::readUint32(aPacket->packet + FISBONE_SERIALNO_OFFSET);
+
+ CheckedUint32 checked_fields_pos =
+ CheckedUint32(FISBONE_MSG_FIELDS_OFFSET) + offsetMsgField;
+ if (!checked_fields_pos.isValid() ||
+ aPacket->bytes < static_cast<int64_t>(checked_fields_pos.value())) {
+ return false;
+ }
+ int64_t msgLength = aPacket->bytes - checked_fields_pos.value();
+ char* msgProbe = (char*)aPacket->packet + checked_fields_pos.value();
+ char* msgHead = msgProbe;
+ UniquePtr<MessageField> field(new MessageField());
+
+ const static FieldPatternType kFieldTypeMaps[] = {
+ {"Content-Type:", eContentType},
+ {"Role:", eRole},
+ {"Name:", eName},
+ {"Language:", eLanguage},
+ {"Title:", eTitle},
+ {"Display-hint:", eDisplayHint},
+ {"Altitude:", eAltitude},
+ {"TrackOrder:", eTrackOrder},
+ {"Track dependencies:", eTrackDependencies}};
+
+ bool isContentTypeParsed = false;
+ while (msgLength > 1) {
+ if (*msgProbe == '\r' && *(msgProbe + 1) == '\n') {
+ nsAutoCString strMsg(msgHead, msgProbe - msgHead);
+ for (size_t i = 0; i < ArrayLength(kFieldTypeMaps); i++) {
+ if (strMsg.Find(kFieldTypeMaps[i].mPatternToRecognize) != -1) {
+ // The content of message header fields follows [RFC2822], and the
+ // mandatory message field must be encoded in US-ASCII, others
+ // must be be encoded in UTF-8. "Content-Type" must come first
+ // for all of message header fields.
+ // See
+ // http://svn.annodex.net/standards/draft-pfeiffer-oggskeleton-current.txt.
+ if (i != 0 && !isContentTypeParsed) {
+ return false;
+ }
+
+ if ((i == 0 && IsAscii(strMsg)) || (i != 0 && IsUtf8(strMsg))) {
+ EMsgHeaderType eHeaderType = kFieldTypeMaps[i].mMsgHeaderType;
+ Unused << field->mValuesStore.LookupOrInsertWith(
+ eHeaderType, [i, msgHead, msgProbe]() {
+ uint32_t nameLen =
+ strlen(kFieldTypeMaps[i].mPatternToRecognize);
+ return MakeUnique<nsCString>(msgHead + nameLen,
+ msgProbe - msgHead - nameLen);
+ });
+ isContentTypeParsed = i == 0 ? true : isContentTypeParsed;
+ }
+ break;
+ }
+ }
+ msgProbe += 2;
+ msgLength -= 2;
+ msgHead = msgProbe;
+ continue;
+ }
+ msgLength--;
+ msgProbe++;
+ }
+
+ return mMsgFieldStore.WithEntryHandle(serialno, [&](auto&& entry) {
+ if (entry) {
+ // mMsgFieldStore has an entry for serialno already.
+ return false;
+ }
+ entry.Insert(std::move(field));
+ return true;
+ });
+}
+
+bool SkeletonState::DecodeHeader(OggPacketPtr aPacket) {
+ if (IsSkeletonBOS(aPacket.get())) {
+ uint16_t verMajor = LittleEndian::readUint16(aPacket->packet +
+ SKELETON_VERSION_MAJOR_OFFSET);
+ uint16_t verMinor = LittleEndian::readUint16(aPacket->packet +
+ SKELETON_VERSION_MINOR_OFFSET);
+
+ // Read the presentation time. We read this before the version check as the
+ // presentation time exists in all versions.
+ int64_t n = LittleEndian::readInt64(
+ aPacket->packet + SKELETON_PRESENTATION_TIME_NUMERATOR_OFFSET);
+ int64_t d = LittleEndian::readInt64(
+ aPacket->packet + SKELETON_PRESENTATION_TIME_DENOMINATOR_OFFSET);
+ mPresentationTime =
+ d == 0 ? 0
+ : (static_cast<float>(n) / static_cast<float>(d)) * USECS_PER_S;
+
+ mVersion = SKELETON_VERSION(verMajor, verMinor);
+ // We can only care to parse Skeleton version 4.0+.
+ if (mVersion < SKELETON_VERSION(4, 0) ||
+ mVersion >= SKELETON_VERSION(5, 0) ||
+ aPacket->bytes < SKELETON_4_0_MIN_HEADER_LEN) {
+ return false;
+ }
+
+ // Extract the segment length.
+ mLength =
+ LittleEndian::readInt64(aPacket->packet + SKELETON_FILE_LENGTH_OFFSET);
+
+ LOG(LogLevel::Debug, ("Skeleton segment length: %" PRId64, mLength));
+
+ // Initialize the serialno-to-index map.
+ return true;
+ }
+ if (IsSkeletonIndex(aPacket.get()) && mVersion >= SKELETON_VERSION(4, 0)) {
+ return DecodeIndex(aPacket.get());
+ }
+ if (IsSkeletonFisbone(aPacket.get())) {
+ return DecodeFisbone(aPacket.get());
+ }
+ if (aPacket->e_o_s) {
+ mDoneReadingHeaders = true;
+ }
+ return true;
+}
+
+#undef LOG
+
+} // namespace mozilla
diff --git a/dom/media/ogg/OggCodecState.h b/dom/media/ogg/OggCodecState.h
new file mode 100644
index 0000000000..b8a3857875
--- /dev/null
+++ b/dom/media/ogg/OggCodecState.h
@@ -0,0 +1,628 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#if !defined(OggCodecState_h_)
+# define OggCodecState_h_
+
+# include <ogg/ogg.h>
+// For MOZ_SAMPLE_TYPE_*
+# include "FlacFrameParser.h"
+# include "OggRLBoxTypes.h"
+# include "VideoUtils.h"
+# include <nsDeque.h>
+# include <nsTArray.h>
+# include <nsClassHashtable.h>
+
+# include <theora/theoradec.h>
+# ifdef MOZ_TREMOR
+# include <tremor/ivorbiscodec.h>
+# else
+# include <vorbis/codec.h>
+# endif
+
+// Uncomment the following to validate that we're predicting the number
+// of Vorbis samples in each packet correctly.
+# define VALIDATE_VORBIS_SAMPLE_CALCULATION
+# ifdef VALIDATE_VORBIS_SAMPLE_CALCULATION
+# include <map>
+# endif
+
+struct OpusMSDecoder;
+
+namespace mozilla {
+
+inline constexpr char RLBOX_SAFE_DEBUG_ASSERTION[] =
+ "Tainted data is being inspected only for debugging purposes. This is not "
+ "a condition that is critical for safety of the renderer.";
+
+inline constexpr char RLBOX_OGG_STATE_ASSERT_REASON[] =
+ "Tainted data is being inspected only to check the internal state of "
+ "libogg structures. This is not a condition that is critical for safety of "
+ "the renderer.";
+
+inline constexpr char RLBOX_OGG_PAGE_SERIAL_REASON[] =
+ "We are checking the serial of the page. If libogg is operating correctly, "
+ "we check serial numbers to make sure the Firefox renderer is correctly "
+ "passing streams to the correct source. If libogg has been corrupted, it "
+ "could return an incorrect serial, however this would mean that an OGG "
+ "file has intentionally corrupted data across multiple logical streams. "
+ "This however cannot compromise memory safety of the renderer.";
+
+class OpusParser;
+
+struct OggPacketDeletePolicy {
+ void operator()(ogg_packet* aPacket) const {
+ delete[] aPacket->packet;
+ delete aPacket;
+ }
+};
+
+using OggPacketPtr = UniquePtr<ogg_packet, OggPacketDeletePolicy>;
+
+// Deallocates a packet, used in OggPacketQueue below.
+class OggPacketDeallocator : public nsDequeFunctor<ogg_packet> {
+ virtual void operator()(ogg_packet* aPacket) override {
+ OggPacketDeletePolicy()(aPacket);
+ }
+};
+
+// A queue of ogg_packets. When we read a page, we extract the page's packets
+// and buffer them in the owning stream's OggCodecState. This is because
+// if we're skipping up to the next keyframe in very large frame sized videos,
+// there may be several megabytes of data between keyframes, and the
+// ogg_stream_state would end up resizing its buffer every time we added a
+// new 4KB page to the bitstream, which kills performance on Windows. This
+// also gives us the option to timestamp packets rather than decoded
+// frames/samples, reducing the amount of frames/samples we must decode to
+// determine start-time at a particular offset, and gives us finer control
+// over memory usage.
+class OggPacketQueue : private nsDeque<ogg_packet> {
+ public:
+ OggPacketQueue() : nsDeque(new OggPacketDeallocator()) {}
+ ~OggPacketQueue() { Erase(); }
+ bool IsEmpty() { return nsDeque<ogg_packet>::GetSize() == 0; }
+ void Append(OggPacketPtr aPacket);
+ OggPacketPtr PopFront() {
+ return OggPacketPtr(nsDeque<ogg_packet>::PopFront());
+ }
+ ogg_packet* PeekFront() { return nsDeque<ogg_packet>::PeekFront(); }
+ OggPacketPtr Pop() { return OggPacketPtr(nsDeque<ogg_packet>::Pop()); }
+ ogg_packet* operator[](size_t aIndex) const {
+ return nsDeque<ogg_packet>::ObjectAt(aIndex);
+ }
+ size_t Length() const { return nsDeque<ogg_packet>::GetSize(); }
+ void PushFront(OggPacketPtr aPacket) {
+ nsDeque<ogg_packet>::PushFront(aPacket.release());
+ }
+ void Erase() { nsDeque<ogg_packet>::Erase(); }
+};
+
+// Encapsulates the data required for decoding an ogg bitstream and for
+// converting granulepos to timestamps.
+class OggCodecState {
+ public:
+ typedef mozilla::MetadataTags MetadataTags;
+ // Ogg types we know about
+ enum CodecType {
+ TYPE_VORBIS = 0,
+ TYPE_THEORA,
+ TYPE_OPUS,
+ TYPE_SKELETON,
+ TYPE_FLAC,
+ TYPE_UNKNOWN
+ };
+
+ virtual ~OggCodecState();
+
+ // Factory for creating nsCodecStates. Use instead of constructor.
+ // aPage should be a beginning-of-stream page.
+ static UniquePtr<OggCodecState> Create(rlbox_sandbox_ogg* aSandbox,
+ tainted_opaque_ogg<ogg_page*> aPage,
+ uint32_t aSerial);
+
+ virtual CodecType GetType() { return TYPE_UNKNOWN; }
+
+ // Reads a header packet. Returns false if an error was encountered
+ // while reading header packets. Callers should check DoneReadingHeaders()
+ // to determine if the last header has been read.
+ // This function takes ownership of the packet and is responsible for
+ // releasing it or queuing it for later processing.
+ virtual bool DecodeHeader(OggPacketPtr aPacket) {
+ return (mDoneReadingHeaders = true);
+ }
+
+ // Build a hash table with tag metadata parsed from the stream.
+ virtual UniquePtr<MetadataTags> GetTags() { return nullptr; }
+
+ // Returns the end time that a granulepos represents.
+ virtual int64_t Time(int64_t granulepos) { return -1; }
+
+ // Returns the start time that a granulepos represents.
+ virtual int64_t StartTime(int64_t granulepos) { return -1; }
+
+ // Returns the duration of the given packet, if it can be determined.
+ virtual int64_t PacketDuration(ogg_packet* aPacket) { return -1; }
+
+ // Returns the start time of the given packet, if it can be determined.
+ virtual int64_t PacketStartTime(ogg_packet* aPacket) {
+ if (aPacket->granulepos < 0) {
+ return -1;
+ }
+ int64_t endTime = Time(aPacket->granulepos);
+ int64_t duration = PacketDuration(aPacket);
+ if (duration > endTime) {
+ // Audio preskip may eat a whole packet or more.
+ return 0;
+ } else {
+ return endTime - duration;
+ }
+ }
+
+ // Initializes the codec state.
+ virtual bool Init() { return true; }
+
+ // Returns true when this bitstream has finished reading all its
+ // header packets.
+ bool DoneReadingHeaders() { return mDoneReadingHeaders; }
+
+ // Deactivates the bitstream. Only the primary video and audio bitstreams
+ // should be active.
+ void Deactivate() {
+ mActive = false;
+ mDoneReadingHeaders = true;
+ Reset();
+ }
+
+ // Resets decoding state.
+ virtual nsresult Reset();
+
+ // Returns true if the OggCodecState thinks this packet is a header
+ // packet. Note this does not verify the validity of the header packet,
+ // it just guarantees that the packet is marked as a header packet (i.e.
+ // it is definintely not a data packet). Do not use this to identify
+ // streams, use it to filter header packets from data packets while
+ // decoding.
+ virtual bool IsHeader(ogg_packet* aPacket) { return false; }
+
+ // Returns true if the OggCodecState thinks this packet represents a
+ // keyframe, from which decoding can restart safely.
+ virtual bool IsKeyframe(ogg_packet* aPacket) { return true; }
+
+ // Returns true if there is a packet available for dequeueing in the stream.
+ bool IsPacketReady();
+
+ // Returns the next raw packet in the stream, or nullptr if there are no more
+ // packets buffered in the packet queue. More packets can be buffered by
+ // inserting one or more pages into the stream by calling PageIn().
+ // The packet will have a valid granulepos.
+ OggPacketPtr PacketOut();
+
+ // Returns the next raw packet in the stream, or nullptr if there are no more
+ // packets buffered in the packet queue, without consuming it.
+ // The packet will have a valid granulepos.
+ ogg_packet* PacketPeek();
+
+ // Moves all raw packets from aOther to the front of the current packet queue.
+ void PushFront(OggPacketQueue&& aOther);
+
+ // Returns the next packet in the stream as a MediaRawData, or nullptr
+ // if there are no more packets buffered in the packet queue. More packets
+ // can be buffered by inserting one or more pages into the stream by calling
+ // PageIn(). The packet will have a valid granulepos.
+ virtual already_AddRefed<MediaRawData> PacketOutAsMediaRawData();
+
+ // Extracts all packets from the page, and inserts them into the packet
+ // queue. They can be extracted by calling PacketOut(). Packets from an
+ // inactive stream are not buffered, i.e. this call has no effect for
+ // inactive streams. Multiple pages may need to be inserted before
+ // PacketOut() starts to return packets, as granulepos may need to be
+ // captured.
+ virtual nsresult PageIn(tainted_opaque_ogg<ogg_page*> aPage);
+
+ // Returns the maximum number of microseconds which a keyframe can be offset
+ // from any given interframe.b
+ virtual int64_t MaxKeyframeOffset() { return 0; }
+ // Public access for mTheoraInfo.keyframe_granule_shift
+ virtual int32_t KeyFrameGranuleJobs() { return 0; }
+
+ // Number of packets read.
+ uint64_t mPacketCount;
+
+ // Serial number of the bitstream.
+ uint32_t mSerial;
+
+ // Ogg specific state.
+ tainted_opaque_ogg<ogg_stream_state*> mState;
+
+ // Queue of as yet undecoded packets. Packets are guaranteed to have
+ // a valid granulepos.
+ OggPacketQueue mPackets;
+
+ // Is the bitstream active; whether we're decoding and playing this bitstream.
+ bool mActive;
+
+ // True when all headers packets have been read.
+ bool mDoneReadingHeaders;
+
+ // All invocations of libogg functionality from the demuxer is sandboxed using
+ // wasm library sandboxes on supported platforms. This is the sandbox
+ // instance.
+ rlbox_sandbox_ogg* mSandbox;
+
+ virtual const TrackInfo* GetInfo() const {
+ MOZ_RELEASE_ASSERT(false, "Can't be called directly");
+ return nullptr;
+ }
+
+ // Validation utility for vorbis-style tag names.
+ static bool IsValidVorbisTagName(nsCString& aName);
+
+ // Utility method to parse and add a vorbis-style comment
+ // to a metadata hash table. Most Ogg-encapsulated codecs
+ // use the vorbis comment format for metadata.
+ static bool AddVorbisComment(UniquePtr<MetadataTags>& aTags,
+ const char* aComment, uint32_t aLength);
+
+ protected:
+ // Constructs a new OggCodecState. aActive denotes whether the stream is
+ // active. For streams of unsupported or unknown types, aActive should be
+ // false.
+ OggCodecState(rlbox_sandbox_ogg* aSandbox,
+ tainted_opaque_ogg<ogg_page*> aBosPage, uint32_t aSerial,
+ bool aActive);
+
+ // Deallocates all packets stored in mUnstamped, and clears the array.
+ void ClearUnstamped();
+
+ // Extracts packets out of mState until a data packet with a non -1
+ // granulepos is encountered, or no more packets are readable. Header
+ // packets are pushed into the packet queue immediately, and data packets
+ // are buffered in mUnstamped. Once a non -1 granulepos packet is read
+ // the granulepos of the packets in mUnstamped can be inferred, and they
+ // can be pushed over to mPackets. Used by PageIn() implementations in
+ // subclasses.
+ nsresult PacketOutUntilGranulepos(bool& aFoundGranulepos);
+
+ // Temporary buffer in which to store packets while we're reading packets
+ // in order to capture granulepos.
+ nsTArray<OggPacketPtr> mUnstamped;
+
+ bool SetCodecSpecificConfig(MediaByteBuffer* aBuffer,
+ OggPacketQueue& aHeaders);
+
+ private:
+ bool InternalInit();
+};
+
+class VorbisState : public OggCodecState {
+ public:
+ explicit VorbisState(rlbox_sandbox_ogg* aSandbox,
+ tainted_opaque_ogg<ogg_page*> aBosPage,
+ uint32_t aSerial);
+ virtual ~VorbisState();
+
+ CodecType GetType() override { return TYPE_VORBIS; }
+ bool DecodeHeader(OggPacketPtr aPacket) override;
+ int64_t Time(int64_t granulepos) override;
+ int64_t PacketDuration(ogg_packet* aPacket) override;
+ bool Init() override;
+ nsresult Reset() override;
+ bool IsHeader(ogg_packet* aPacket) override;
+ nsresult PageIn(tainted_opaque_ogg<ogg_page*> aPage) override;
+ const TrackInfo* GetInfo() const override { return &mInfo; }
+
+ // Return a hash table with tag metadata.
+ UniquePtr<MetadataTags> GetTags() override;
+
+ private:
+ AudioInfo mInfo;
+ vorbis_info mVorbisInfo;
+ vorbis_comment mComment;
+ vorbis_dsp_state mDsp;
+ vorbis_block mBlock;
+ OggPacketQueue mHeaders;
+
+ // Returns the end time that a granulepos represents.
+ static int64_t Time(vorbis_info* aInfo, int64_t aGranulePos);
+
+ // Reconstructs the granulepos of Vorbis packets stored in the mUnstamped
+ // array.
+ void ReconstructVorbisGranulepos();
+
+ // The "block size" of the previously decoded Vorbis packet, or 0 if we've
+ // not yet decoded anything. This is used to calculate the number of samples
+ // in a Vorbis packet, since each Vorbis packet depends on the previous
+ // packet while being decoded.
+ long mPrevVorbisBlockSize;
+
+ // Granulepos (end sample) of the last decoded Vorbis packet. This is used
+ // to calculate the Vorbis granulepos when we don't find a granulepos to
+ // back-propagate from.
+ int64_t mGranulepos;
+
+# ifdef VALIDATE_VORBIS_SAMPLE_CALCULATION
+ // When validating that we've correctly predicted Vorbis packets' number
+ // of samples, we store each packet's predicted number of samples in this
+ // map, and verify we decode the predicted number of samples.
+ std::map<ogg_packet*, long> mVorbisPacketSamples;
+# endif
+
+ // Records that aPacket is predicted to have aSamples samples.
+ // This function has no effect if VALIDATE_VORBIS_SAMPLE_CALCULATION
+ // is not defined.
+ void RecordVorbisPacketSamples(ogg_packet* aPacket, long aSamples);
+
+ // Verifies that aPacket has had its number of samples predicted.
+ // This function has no effect if VALIDATE_VORBIS_SAMPLE_CALCULATION
+ // is not defined.
+ void AssertHasRecordedPacketSamples(ogg_packet* aPacket);
+
+ public:
+ // Asserts that the number of samples predicted for aPacket is aSamples.
+ // This function has no effect if VALIDATE_VORBIS_SAMPLE_CALCULATION
+ // is not defined.
+ void ValidateVorbisPacketSamples(ogg_packet* aPacket, long aSamples);
+};
+
+// Returns 1 if the Theora info struct is decoding a media of Theora
+// version (maj,min,sub) or later, otherwise returns 0.
+int TheoraVersion(th_info* info, unsigned char maj, unsigned char min,
+ unsigned char sub);
+
+class TheoraState : public OggCodecState {
+ public:
+ explicit TheoraState(rlbox_sandbox_ogg* aSandbox,
+ tainted_opaque_ogg<ogg_page*> aBosPage,
+ uint32_t aSerial);
+ virtual ~TheoraState();
+
+ CodecType GetType() override { return TYPE_THEORA; }
+ bool DecodeHeader(OggPacketPtr aPacket) override;
+ int64_t Time(int64_t granulepos) override;
+ int64_t StartTime(int64_t granulepos) override;
+ int64_t PacketDuration(ogg_packet* aPacket) override;
+ bool Init() override;
+ nsresult Reset() override;
+ bool IsHeader(ogg_packet* aPacket) override;
+ bool IsKeyframe(ogg_packet* aPacket) override;
+ nsresult PageIn(tainted_opaque_ogg<ogg_page*> aPage) override;
+ const TrackInfo* GetInfo() const override { return &mInfo; }
+ int64_t MaxKeyframeOffset() override;
+ int32_t KeyFrameGranuleJobs() override {
+ return mTheoraInfo.keyframe_granule_shift;
+ }
+
+ private:
+ // Returns the end time that a granulepos represents.
+ static int64_t Time(th_info* aInfo, int64_t aGranulePos);
+
+ th_info mTheoraInfo;
+ th_comment mComment;
+ th_setup_info* mSetup;
+ th_dec_ctx* mCtx;
+
+ VideoInfo mInfo;
+ OggPacketQueue mHeaders;
+
+ // Reconstructs the granulepos of Theora packets stored in the
+ // mUnstamped array. mUnstamped must be filled with consecutive packets from
+ // the stream, with the last packet having a known granulepos. Using this
+ // known granulepos, and the known frame numbers, we recover the granulepos
+ // of all frames in the array. This enables us to determine their timestamps.
+ void ReconstructTheoraGranulepos();
+};
+
+class OpusState : public OggCodecState {
+ public:
+ explicit OpusState(rlbox_sandbox_ogg* aSandbox,
+ tainted_opaque_ogg<ogg_page*> aBosPage, uint32_t aSerial);
+ virtual ~OpusState();
+
+ CodecType GetType() override { return TYPE_OPUS; }
+ bool DecodeHeader(OggPacketPtr aPacket) override;
+ int64_t Time(int64_t aGranulepos) override;
+ int64_t PacketDuration(ogg_packet* aPacket) override;
+ bool Init() override;
+ nsresult Reset() override;
+ nsresult Reset(bool aStart);
+ bool IsHeader(ogg_packet* aPacket) override;
+ nsresult PageIn(tainted_opaque_ogg<ogg_page*> aPage) override;
+ already_AddRefed<MediaRawData> PacketOutAsMediaRawData() override;
+ const TrackInfo* GetInfo() const override { return &mInfo; }
+
+ // Returns the end time that a granulepos represents.
+ static int64_t Time(int aPreSkip, int64_t aGranulepos);
+
+ // Construct and return a table of tags from the metadata header.
+ UniquePtr<MetadataTags> GetTags() override;
+
+ private:
+ UniquePtr<OpusParser> mParser;
+ OpusMSDecoder* mDecoder;
+
+ // Granule position (end sample) of the last decoded Opus packet. This is
+ // used to calculate the amount we should trim from the last packet.
+ int64_t mPrevPacketGranulepos;
+
+ // Reconstructs the granulepos of Opus packets stored in the
+ // mUnstamped array. mUnstamped must be filled with consecutive packets from
+ // the stream, with the last packet having a known granulepos. Using this
+ // known granulepos, and the known frame numbers, we recover the granulepos
+ // of all frames in the array. This enables us to determine their timestamps.
+ bool ReconstructOpusGranulepos();
+
+ // Granule position (end sample) of the last decoded Opus page. This is
+ // used to calculate the Opus per-packet granule positions on the last page,
+ // where we may need to trim some samples from the end.
+ int64_t mPrevPageGranulepos;
+ AudioInfo mInfo;
+ OggPacketQueue mHeaders;
+};
+
+// Constructs a 32bit version number out of two 16 bit major,minor
+// version numbers.
+# define SKELETON_VERSION(major, minor) (((major) << 16) | (minor))
+
+enum EMsgHeaderType {
+ eContentType,
+ eRole,
+ eName,
+ eLanguage,
+ eTitle,
+ eDisplayHint,
+ eAltitude,
+ eTrackOrder,
+ eTrackDependencies
+};
+
+struct FieldPatternType {
+ const char* mPatternToRecognize;
+ EMsgHeaderType mMsgHeaderType;
+};
+
+// Stores the message information for different logical bitstream.
+struct MessageField {
+ nsClassHashtable<nsUint32HashKey, nsCString> mValuesStore;
+};
+
+class SkeletonState : public OggCodecState {
+ public:
+ explicit SkeletonState(rlbox_sandbox_ogg* aSandbox,
+ tainted_opaque_ogg<ogg_page*> aBosPage,
+ uint32_t aSerial);
+ ~SkeletonState();
+
+ nsClassHashtable<nsUint32HashKey, MessageField> mMsgFieldStore;
+
+ CodecType GetType() override { return TYPE_SKELETON; }
+ bool DecodeHeader(OggPacketPtr aPacket) override;
+ int64_t Time(int64_t granulepos) override { return -1; }
+ bool IsHeader(ogg_packet* aPacket) override { return true; }
+
+ // Return true if the given time (in milliseconds) is within
+ // the presentation time defined in the skeleton track.
+ bool IsPresentable(int64_t aTime) { return aTime >= mPresentationTime; }
+
+ // Stores the offset of the page on which a keyframe starts,
+ // and its presentation time.
+ class nsKeyPoint {
+ public:
+ nsKeyPoint() : mOffset(INT64_MAX), mTime(INT64_MAX) {}
+
+ nsKeyPoint(int64_t aOffset, int64_t aTime)
+ : mOffset(aOffset), mTime(aTime) {}
+
+ // Offset from start of segment/link-in-the-chain in bytes.
+ int64_t mOffset;
+
+ // Presentation time in usecs.
+ int64_t mTime;
+
+ bool IsNull() { return mOffset == INT64_MAX && mTime == INT64_MAX; }
+ };
+
+ // Stores a keyframe's byte-offset, presentation time and the serialno
+ // of the stream it belongs to.
+ class nsSeekTarget {
+ public:
+ nsSeekTarget() : mSerial(0) {}
+ nsKeyPoint mKeyPoint;
+ uint32_t mSerial;
+ bool IsNull() { return mKeyPoint.IsNull() && mSerial == 0; }
+ };
+
+ // Determines from the seek index the keyframe which you must seek back to
+ // in order to get all keyframes required to render all streams with
+ // serialnos in aTracks, at time aTarget.
+ nsresult IndexedSeekTarget(int64_t aTarget, nsTArray<uint32_t>& aTracks,
+ nsSeekTarget& aResult);
+
+ bool HasIndex() const { return mIndex.Count() > 0; }
+
+ // Returns the duration of the active tracks in the media, if we have
+ // an index. aTracks must be filled with the serialnos of the active tracks.
+ // The duration is calculated as the greatest end time of all active tracks,
+ // minus the smalled start time of all the active tracks.
+ nsresult GetDuration(const nsTArray<uint32_t>& aTracks, int64_t& aDuration);
+
+ private:
+ // Decodes an index packet. Returns false on failure.
+ bool DecodeIndex(ogg_packet* aPacket);
+ // Decodes an fisbone packet. Returns false on failure.
+ bool DecodeFisbone(ogg_packet* aPacket);
+
+ // Gets the keypoint you must seek to in order to get the keyframe required
+ // to render the stream at time aTarget on stream with serial aSerialno.
+ nsresult IndexedSeekTargetForTrack(uint32_t aSerialno, int64_t aTarget,
+ nsKeyPoint& aResult);
+
+ // Version of the decoded skeleton track, as per the SKELETON_VERSION macro.
+ uint32_t mVersion;
+
+ // Presentation time of the resource in milliseconds
+ int64_t mPresentationTime;
+
+ // Length of the resource in bytes.
+ int64_t mLength;
+
+ // Stores the keyframe index and duration information for a particular
+ // stream.
+ class nsKeyFrameIndex {
+ public:
+ nsKeyFrameIndex(int64_t aStartTime, int64_t aEndTime)
+ : mStartTime(aStartTime), mEndTime(aEndTime) {
+ MOZ_COUNT_CTOR(nsKeyFrameIndex);
+ }
+
+ MOZ_COUNTED_DTOR(nsKeyFrameIndex)
+
+ void Add(int64_t aOffset, int64_t aTimeMs) {
+ mKeyPoints.AppendElement(nsKeyPoint(aOffset, aTimeMs));
+ }
+
+ const nsKeyPoint& Get(uint32_t aIndex) const { return mKeyPoints[aIndex]; }
+
+ uint32_t Length() const { return mKeyPoints.Length(); }
+
+ // Presentation time of the first sample in this stream in usecs.
+ const int64_t mStartTime;
+
+ // End time of the last sample in this stream in usecs.
+ const int64_t mEndTime;
+
+ private:
+ nsTArray<nsKeyPoint> mKeyPoints;
+ };
+
+ // Maps Ogg serialnos to the index-keypoint list.
+ nsClassHashtable<nsUint32HashKey, nsKeyFrameIndex> mIndex;
+};
+
+class FlacState : public OggCodecState {
+ public:
+ explicit FlacState(rlbox_sandbox_ogg* aSandbox,
+ tainted_opaque_ogg<ogg_page*> aBosPage, uint32_t aSerial);
+
+ CodecType GetType() override { return TYPE_FLAC; }
+ bool DecodeHeader(OggPacketPtr aPacket) override;
+ int64_t Time(int64_t granulepos) override;
+ int64_t PacketDuration(ogg_packet* aPacket) override;
+ bool IsHeader(ogg_packet* aPacket) override;
+ nsresult PageIn(tainted_opaque_ogg<ogg_page*> aPage) override;
+
+ // Return a hash table with tag metadata.
+ UniquePtr<MetadataTags> GetTags() override;
+
+ const TrackInfo* GetInfo() const override;
+
+ private:
+ bool ReconstructFlacGranulepos(void);
+
+ FlacFrameParser mParser;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/ogg/OggCodecStore.cpp b/dom/media/ogg/OggCodecStore.cpp
new file mode 100644
index 0000000000..ef3498adec
--- /dev/null
+++ b/dom/media/ogg/OggCodecStore.cpp
@@ -0,0 +1,31 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/DebugOnly.h"
+
+#include "OggCodecStore.h"
+
+namespace mozilla {
+
+OggCodecStore::OggCodecStore() : mMonitor("CodecStore") {}
+
+OggCodecState* OggCodecStore::Add(uint32_t serial,
+ UniquePtr<OggCodecState> codecState) {
+ MonitorAutoLock mon(mMonitor);
+ return mCodecStates.InsertOrUpdate(serial, std::move(codecState)).get();
+}
+
+bool OggCodecStore::Contains(uint32_t serial) {
+ MonitorAutoLock mon(mMonitor);
+ return mCodecStates.Get(serial, nullptr);
+}
+
+OggCodecState* OggCodecStore::Get(uint32_t serial) {
+ MonitorAutoLock mon(mMonitor);
+ return mCodecStates.Get(serial);
+}
+
+} // namespace mozilla
diff --git a/dom/media/ogg/OggCodecStore.h b/dom/media/ogg/OggCodecStore.h
new file mode 100644
index 0000000000..bcde8bed00
--- /dev/null
+++ b/dom/media/ogg/OggCodecStore.h
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#if !defined(OggCodecStore_h_)
+# define OggCodecStore_h_
+
+# include <ogg/ogg.h>
+
+# include "OggCodecState.h"
+# include "VideoUtils.h"
+# include "mozilla/Monitor.h"
+
+namespace mozilla {
+
+// Thread safe container to store the codec information and the serial for each
+// streams.
+class OggCodecStore {
+ public:
+ OggCodecStore();
+ OggCodecState* Add(uint32_t serial, UniquePtr<OggCodecState> codecState);
+ bool Contains(uint32_t serial);
+ OggCodecState* Get(uint32_t serial);
+ bool IsKnownStream(uint32_t aSerial);
+
+ private:
+ // Maps Ogg serialnos to OggStreams.
+ nsClassHashtable<nsUint32HashKey, OggCodecState> mCodecStates;
+
+ // Protects the |mCodecStates| and the |mKnownStreams| members.
+ Monitor mMonitor MOZ_UNANNOTATED;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/ogg/OggDecoder.cpp b/dom/media/ogg/OggDecoder.cpp
new file mode 100644
index 0000000000..5f6d61f694
--- /dev/null
+++ b/dom/media/ogg/OggDecoder.cpp
@@ -0,0 +1,82 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "OggDecoder.h"
+#include "MediaContainerType.h"
+#include "MediaDecoder.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "nsMimeTypes.h"
+
+namespace mozilla {
+
+/* static */
+bool OggDecoder::IsSupportedType(const MediaContainerType& aContainerType) {
+ if (!StaticPrefs::media_ogg_enabled()) {
+ return false;
+ }
+
+ if (aContainerType.Type() != MEDIAMIMETYPE(AUDIO_OGG) &&
+ aContainerType.Type() != MEDIAMIMETYPE(VIDEO_OGG) &&
+ aContainerType.Type() != MEDIAMIMETYPE("application/ogg")) {
+ return false;
+ }
+
+ const bool isOggVideo = (aContainerType.Type() != MEDIAMIMETYPE(AUDIO_OGG));
+
+ const MediaCodecs& codecs = aContainerType.ExtendedType().Codecs();
+ if (codecs.IsEmpty()) {
+ // Ogg guarantees that the only codecs it contained are supported.
+ return true;
+ }
+ // Verify that all the codecs specified are ones that we expect that
+ // we can play.
+ for (const auto& codec : codecs.Range()) {
+ if ((MediaDecoder::IsOpusEnabled() && codec.EqualsLiteral("opus")) ||
+ codec.EqualsLiteral("vorbis") || codec.EqualsLiteral("flac")) {
+ continue;
+ }
+ // Note: Only accept Theora in a video container type, not in an audio
+ // container type.
+ if (isOggVideo && codec.EqualsLiteral("theora")) {
+ continue;
+ }
+ // Some unsupported codec.
+ return false;
+ }
+ return true;
+}
+
+/* static */
+nsTArray<UniquePtr<TrackInfo>> OggDecoder::GetTracksInfo(
+ const MediaContainerType& aType) {
+ nsTArray<UniquePtr<TrackInfo>> tracks;
+ if (!IsSupportedType(aType)) {
+ return tracks;
+ }
+
+ const MediaCodecs& codecs = aType.ExtendedType().Codecs();
+ if (codecs.IsEmpty()) {
+ // Codecs must be specified for ogg as it can't be implied.
+ return tracks;
+ }
+
+ for (const auto& codec : codecs.Range()) {
+ if (codec.EqualsLiteral("opus") || codec.EqualsLiteral("vorbis") ||
+ codec.EqualsLiteral("flac")) {
+ tracks.AppendElement(
+ CreateTrackInfoWithMIMETypeAndContainerTypeExtraParameters(
+ "audio/"_ns + NS_ConvertUTF16toUTF8(codec), aType));
+ } else {
+ MOZ_ASSERT(codec.EqualsLiteral("theora"));
+ tracks.AppendElement(
+ CreateTrackInfoWithMIMETypeAndContainerTypeExtraParameters(
+ "video/"_ns + NS_ConvertUTF16toUTF8(codec), aType));
+ }
+ }
+ return tracks;
+}
+
+} // namespace mozilla
diff --git a/dom/media/ogg/OggDecoder.h b/dom/media/ogg/OggDecoder.h
new file mode 100644
index 0000000000..95e8663746
--- /dev/null
+++ b/dom/media/ogg/OggDecoder.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#if !defined(OggDecoder_h_)
+# define OggDecoder_h_
+
+# include "mozilla/UniquePtr.h"
+# include "nsTArray.h"
+
+namespace mozilla {
+
+class MediaContainerType;
+class TrackInfo;
+
+class OggDecoder {
+ public:
+ // Returns true if aContainerType is an Ogg type that we think we can render
+ // with an enabled platform decoder backend.
+ // If provided, codecs are checked for support.
+ static bool IsSupportedType(const MediaContainerType& aContainerType);
+ static nsTArray<UniquePtr<TrackInfo>> GetTracksInfo(
+ const MediaContainerType& aType);
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/ogg/OggDemuxer.cpp b/dom/media/ogg/OggDemuxer.cpp
new file mode 100644
index 0000000000..2d1fdd3097
--- /dev/null
+++ b/dom/media/ogg/OggDemuxer.cpp
@@ -0,0 +1,2172 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "OggDemuxer.h"
+#include "OggRLBox.h"
+#include "MediaDataDemuxer.h"
+#include "OggCodecState.h"
+#include "XiphExtradata.h"
+#include "mozilla/AbstractThread.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/PodOperations.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/SharedThreadPool.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/TimeStamp.h"
+#ifdef MOZ_WASM_SANDBOXING_OGG
+# include "mozilla/ipc/LibrarySandboxPreload.h"
+#endif
+#include "nsAutoRef.h"
+#include "nsError.h"
+
+#include <algorithm>
+
+extern mozilla::LazyLogModule gMediaDemuxerLog;
+#define OGG_DEBUG(arg, ...) \
+ DDMOZ_LOG(gMediaDemuxerLog, mozilla::LogLevel::Debug, "::%s: " arg, \
+ __func__, ##__VA_ARGS__)
+
+// Un-comment to enable logging of seek bisections.
+// #define SEEK_LOGGING
+#ifdef SEEK_LOGGING
+# define SEEK_LOG(type, msg) MOZ_LOG(gMediaDemuxerLog, type, msg)
+#else
+# define SEEK_LOG(type, msg)
+#endif
+
+#define CopyAndVerifyOrFail(t, cond, failed) \
+ (t).copy_and_verify([&](auto val) { \
+ if (!(cond)) { \
+ *(failed) = true; \
+ } \
+ return val; \
+ })
+
+namespace mozilla {
+
+using media::TimeInterval;
+using media::TimeIntervals;
+using media::TimeUnit;
+
+// The number of microseconds of "fuzz" we use in a bisection search over
+// HTTP. When we're seeking with fuzz, we'll stop the search if a bisection
+// lands between the seek target and OGG_SEEK_FUZZ_USECS microseconds before the
+// seek target. This is becaue it's usually quicker to just keep downloading
+// from an exisiting connection than to do another bisection inside that
+// small range, which would open a new HTTP connetion.
+static const uint32_t OGG_SEEK_FUZZ_USECS = 500000;
+
+// The number of microseconds of "pre-roll" we use for Opus streams.
+// The specification recommends 80 ms.
+static const TimeUnit OGG_SEEK_OPUS_PREROLL = TimeUnit::FromMicroseconds(80000);
+
+static Atomic<uint32_t> sStreamSourceID(0u);
+
+OggDemuxer::nsAutoOggSyncState::nsAutoOggSyncState(rlbox_sandbox_ogg* aSandbox)
+ : mSandbox(aSandbox) {
+ if (mSandbox) {
+ tainted_ogg<ogg_sync_state*> state =
+ mSandbox->malloc_in_sandbox<ogg_sync_state>();
+ MOZ_RELEASE_ASSERT(state != nullptr);
+ mState = state.to_opaque();
+ sandbox_invoke(*mSandbox, ogg_sync_init, mState);
+ }
+}
+OggDemuxer::nsAutoOggSyncState::~nsAutoOggSyncState() {
+ if (mSandbox) {
+ sandbox_invoke(*mSandbox, ogg_sync_clear, mState);
+ mSandbox->free_in_sandbox(rlbox::from_opaque(mState));
+ tainted_ogg<ogg_sync_state*> null = nullptr;
+ mState = null.to_opaque();
+ }
+}
+
+/* static */
+rlbox_sandbox_ogg* OggDemuxer::CreateSandbox() {
+ rlbox_sandbox_ogg* sandbox = new rlbox_sandbox_ogg();
+#ifdef MOZ_WASM_SANDBOXING_OGG
+ bool success = sandbox->create_sandbox(false /* infallible */);
+#else
+ bool success = sandbox->create_sandbox();
+#endif
+ if (!success) {
+ delete sandbox;
+ sandbox = nullptr;
+ }
+ return sandbox;
+}
+
+void OggDemuxer::SandboxDestroy::operator()(rlbox_sandbox_ogg* sandbox) {
+ if (sandbox) {
+ sandbox->destroy_sandbox();
+ delete sandbox;
+ }
+}
+
+// Return the corresponding category in aKind based on the following specs.
+// (https://www.whatwg.org/specs/web-apps/current-
+// work/multipage/embedded-content.html#dom-audiotrack-kind) &
+// (http://wiki.xiph.org/SkeletonHeaders)
+const nsString OggDemuxer::GetKind(const nsCString& aRole) {
+ if (aRole.Find("audio/main") != -1 || aRole.Find("video/main") != -1) {
+ return u"main"_ns;
+ } else if (aRole.Find("audio/alternate") != -1 ||
+ aRole.Find("video/alternate") != -1) {
+ return u"alternative"_ns;
+ } else if (aRole.Find("audio/audiodesc") != -1) {
+ return u"descriptions"_ns;
+ } else if (aRole.Find("audio/described") != -1) {
+ return u"main-desc"_ns;
+ } else if (aRole.Find("audio/dub") != -1) {
+ return u"translation"_ns;
+ } else if (aRole.Find("audio/commentary") != -1) {
+ return u"commentary"_ns;
+ } else if (aRole.Find("video/sign") != -1) {
+ return u"sign"_ns;
+ } else if (aRole.Find("video/captioned") != -1) {
+ return u"captions"_ns;
+ } else if (aRole.Find("video/subtitled") != -1) {
+ return u"subtitles"_ns;
+ }
+ return u""_ns;
+}
+
+void OggDemuxer::InitTrack(MessageField* aMsgInfo, TrackInfo* aInfo,
+ bool aEnable) {
+ MOZ_ASSERT(aMsgInfo);
+ MOZ_ASSERT(aInfo);
+
+ nsCString* sName = aMsgInfo->mValuesStore.Get(eName);
+ nsCString* sRole = aMsgInfo->mValuesStore.Get(eRole);
+ nsCString* sTitle = aMsgInfo->mValuesStore.Get(eTitle);
+ nsCString* sLanguage = aMsgInfo->mValuesStore.Get(eLanguage);
+ aInfo->Init(sName ? NS_ConvertUTF8toUTF16(*sName) : EmptyString(),
+ sRole ? GetKind(*sRole) : u""_ns,
+ sTitle ? NS_ConvertUTF8toUTF16(*sTitle) : EmptyString(),
+ sLanguage ? NS_ConvertUTF8toUTF16(*sLanguage) : EmptyString(),
+ aEnable);
+}
+
+OggDemuxer::OggDemuxer(MediaResource* aResource)
+ : mSandbox(CreateSandbox()),
+ mTheoraState(nullptr),
+ mVorbisState(nullptr),
+ mOpusState(nullptr),
+ mFlacState(nullptr),
+ mOpusEnabled(MediaDecoder::IsOpusEnabled()),
+ mSkeletonState(nullptr),
+ mAudioOggState(aResource, mSandbox.get()),
+ mVideoOggState(aResource, mSandbox.get()),
+ mIsChained(false),
+ mTimedMetadataEvent(nullptr),
+ mOnSeekableEvent(nullptr) {
+ MOZ_COUNT_CTOR(OggDemuxer);
+ // aResource is referenced through inner m{Audio,Video}OffState members.
+ DDLINKCHILD("resource", aResource);
+}
+
+OggDemuxer::~OggDemuxer() {
+ MOZ_COUNT_DTOR(OggDemuxer);
+ Reset(TrackInfo::kAudioTrack);
+ Reset(TrackInfo::kVideoTrack);
+}
+
+void OggDemuxer::SetChainingEvents(TimedMetadataEventProducer* aMetadataEvent,
+ MediaEventProducer<void>* aOnSeekableEvent) {
+ mTimedMetadataEvent = aMetadataEvent;
+ mOnSeekableEvent = aOnSeekableEvent;
+}
+
+bool OggDemuxer::HasAudio() const {
+ return mVorbisState || mOpusState || mFlacState;
+}
+
+bool OggDemuxer::HasVideo() const { return mTheoraState; }
+
+bool OggDemuxer::HaveStartTime() const { return mStartTime.isSome(); }
+
+int64_t OggDemuxer::StartTime() const { return mStartTime.refOr(0); }
+
+bool OggDemuxer::HaveStartTime(TrackInfo::TrackType aType) {
+ return OggState(aType).mStartTime.isSome();
+}
+
+int64_t OggDemuxer::StartTime(TrackInfo::TrackType aType) {
+ return OggState(aType).mStartTime.refOr(TimeUnit::Zero()).ToMicroseconds();
+}
+
+RefPtr<OggDemuxer::InitPromise> OggDemuxer::Init() {
+ if (!mSandbox) {
+ return InitPromise::CreateAndReject(NS_ERROR_OUT_OF_MEMORY, __func__);
+ }
+ const char RLBOX_OGG_RETURN_CODE_SAFE[] =
+ "Return codes only control whether to early exit. Incorrect return codes "
+ "will not lead to memory safety issues in the renderer.";
+
+ int ret = sandbox_invoke(*mSandbox, ogg_sync_init,
+ OggSyncState(TrackInfo::kAudioTrack))
+ .unverified_safe_because(RLBOX_OGG_RETURN_CODE_SAFE);
+ if (ret != 0) {
+ return InitPromise::CreateAndReject(NS_ERROR_OUT_OF_MEMORY, __func__);
+ }
+ ret = sandbox_invoke(*mSandbox, ogg_sync_init,
+ OggSyncState(TrackInfo::kVideoTrack))
+ .unverified_safe_because(RLBOX_OGG_RETURN_CODE_SAFE);
+ if (ret != 0) {
+ return InitPromise::CreateAndReject(NS_ERROR_OUT_OF_MEMORY, __func__);
+ }
+ if (ReadMetadata() != NS_OK) {
+ return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_METADATA_ERR,
+ __func__);
+ }
+
+ if (!GetNumberTracks(TrackInfo::kAudioTrack) &&
+ !GetNumberTracks(TrackInfo::kVideoTrack)) {
+ return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_METADATA_ERR,
+ __func__);
+ }
+
+ return InitPromise::CreateAndResolve(NS_OK, __func__);
+}
+
+OggCodecState* OggDemuxer::GetTrackCodecState(
+ TrackInfo::TrackType aType) const {
+ switch (aType) {
+ case TrackInfo::kAudioTrack:
+ if (mVorbisState) {
+ return mVorbisState;
+ } else if (mOpusState) {
+ return mOpusState;
+ } else {
+ return mFlacState;
+ }
+ case TrackInfo::kVideoTrack:
+ return mTheoraState;
+ default:
+ return 0;
+ }
+}
+
+TrackInfo::TrackType OggDemuxer::GetCodecStateType(
+ OggCodecState* aState) const {
+ switch (aState->GetType()) {
+ case OggCodecState::TYPE_THEORA:
+ return TrackInfo::kVideoTrack;
+ case OggCodecState::TYPE_OPUS:
+ case OggCodecState::TYPE_VORBIS:
+ case OggCodecState::TYPE_FLAC:
+ return TrackInfo::kAudioTrack;
+ default:
+ return TrackInfo::kUndefinedTrack;
+ }
+}
+
+uint32_t OggDemuxer::GetNumberTracks(TrackInfo::TrackType aType) const {
+ switch (aType) {
+ case TrackInfo::kAudioTrack:
+ return HasAudio() ? 1 : 0;
+ case TrackInfo::kVideoTrack:
+ return HasVideo() ? 1 : 0;
+ default:
+ return 0;
+ }
+}
+
+UniquePtr<TrackInfo> OggDemuxer::GetTrackInfo(TrackInfo::TrackType aType,
+ size_t aTrackNumber) const {
+ switch (aType) {
+ case TrackInfo::kAudioTrack:
+ return mInfo.mAudio.Clone();
+ case TrackInfo::kVideoTrack:
+ return mInfo.mVideo.Clone();
+ default:
+ return nullptr;
+ }
+}
+
+already_AddRefed<MediaTrackDemuxer> OggDemuxer::GetTrackDemuxer(
+ TrackInfo::TrackType aType, uint32_t aTrackNumber) {
+ if (GetNumberTracks(aType) <= aTrackNumber) {
+ return nullptr;
+ }
+ RefPtr<OggTrackDemuxer> e = new OggTrackDemuxer(this, aType, aTrackNumber);
+ DDLINKCHILD("track demuxer", e.get());
+ mDemuxers.AppendElement(e);
+
+ return e.forget();
+}
+
+nsresult OggDemuxer::Reset(TrackInfo::TrackType aType) {
+ // Discard any previously buffered packets/pages.
+ if (mSandbox) {
+ sandbox_invoke(*mSandbox, ogg_sync_reset, OggSyncState(aType));
+ }
+ OggCodecState* trackState = GetTrackCodecState(aType);
+ if (trackState) {
+ return trackState->Reset();
+ }
+ OggState(aType).mNeedKeyframe = true;
+ return NS_OK;
+}
+
+bool OggDemuxer::ReadHeaders(TrackInfo::TrackType aType,
+ OggCodecState* aState) {
+ while (!aState->DoneReadingHeaders()) {
+ DemuxUntilPacketAvailable(aType, aState);
+ OggPacketPtr packet = aState->PacketOut();
+ if (!packet) {
+ OGG_DEBUG("Ran out of header packets early; deactivating stream %" PRIu32,
+ aState->mSerial);
+ aState->Deactivate();
+ return false;
+ }
+
+ // Local OggCodecState needs to decode headers in order to process
+ // packet granulepos -> time mappings, etc.
+ if (!aState->DecodeHeader(std::move(packet))) {
+ OGG_DEBUG(
+ "Failed to decode ogg header packet; deactivating stream %" PRIu32,
+ aState->mSerial);
+ aState->Deactivate();
+ return false;
+ }
+ }
+
+ return aState->Init();
+}
+
+void OggDemuxer::BuildSerialList(nsTArray<uint32_t>& aTracks) {
+ // Obtaining seek index information for currently active bitstreams.
+ if (HasVideo()) {
+ aTracks.AppendElement(mTheoraState->mSerial);
+ }
+ if (HasAudio()) {
+ if (mVorbisState) {
+ aTracks.AppendElement(mVorbisState->mSerial);
+ } else if (mOpusState) {
+ aTracks.AppendElement(mOpusState->mSerial);
+ }
+ }
+}
+
+void OggDemuxer::SetupTarget(OggCodecState** aSavedState,
+ OggCodecState* aNewState) {
+ if (*aSavedState) {
+ (*aSavedState)->Reset();
+ }
+
+ if (aNewState->GetInfo()->GetAsAudioInfo()) {
+ mInfo.mAudio = *aNewState->GetInfo()->GetAsAudioInfo();
+ } else {
+ mInfo.mVideo = *aNewState->GetInfo()->GetAsVideoInfo();
+ }
+ *aSavedState = aNewState;
+}
+
+void OggDemuxer::SetupTargetSkeleton() {
+ // Setup skeleton related information after mVorbisState & mTheroState
+ // being set (if they exist).
+ if (mSkeletonState) {
+ if (!HasAudio() && !HasVideo()) {
+ // We have a skeleton track, but no audio or video, may as well disable
+ // the skeleton, we can't do anything useful with this media.
+ OGG_DEBUG("Deactivating skeleton stream %" PRIu32,
+ mSkeletonState->mSerial);
+ mSkeletonState->Deactivate();
+ } else if (ReadHeaders(TrackInfo::kAudioTrack, mSkeletonState) &&
+ mSkeletonState->HasIndex()) {
+ // We don't particularly care about which track we are currently using
+ // as both MediaResource points to the same content.
+ // Extract the duration info out of the index, so we don't need to seek to
+ // the end of resource to get it.
+ nsTArray<uint32_t> tracks;
+ BuildSerialList(tracks);
+ int64_t duration = 0;
+ if (NS_SUCCEEDED(mSkeletonState->GetDuration(tracks, duration))) {
+ OGG_DEBUG("Got duration from Skeleton index %" PRId64, duration);
+ mInfo.mMetadataDuration.emplace(TimeUnit::FromMicroseconds(duration));
+ }
+ }
+ }
+}
+
+void OggDemuxer::SetupMediaTracksInfo(const nsTArray<uint32_t>& aSerials) {
+ // For each serial number
+ // 1. Retrieve a codecState from mCodecStore by this serial number.
+ // 2. Retrieve a message field from mMsgFieldStore by this serial number.
+ // 3. For now, skip if the serial number refers to a non-primary bitstream.
+ // 4. Setup track and other audio/video related information per different
+ // types.
+ for (size_t i = 0; i < aSerials.Length(); i++) {
+ uint32_t serial = aSerials[i];
+ OggCodecState* codecState = mCodecStore.Get(serial);
+
+ MessageField* msgInfo = nullptr;
+ if (mSkeletonState) {
+ mSkeletonState->mMsgFieldStore.Get(serial, &msgInfo);
+ }
+
+ OggCodecState* primeState = nullptr;
+ switch (codecState->GetType()) {
+ case OggCodecState::TYPE_THEORA:
+ primeState = mTheoraState;
+ break;
+ case OggCodecState::TYPE_VORBIS:
+ primeState = mVorbisState;
+ break;
+ case OggCodecState::TYPE_OPUS:
+ primeState = mOpusState;
+ break;
+ case OggCodecState::TYPE_FLAC:
+ primeState = mFlacState;
+ break;
+ default:
+ break;
+ }
+ if (primeState && primeState == codecState) {
+ bool isAudio = primeState->GetInfo()->GetAsAudioInfo();
+ if (msgInfo) {
+ InitTrack(
+ msgInfo,
+ isAudio ? static_cast<TrackInfo*>(&mInfo.mAudio) : &mInfo.mVideo,
+ true);
+ }
+ FillTags(isAudio ? static_cast<TrackInfo*>(&mInfo.mAudio) : &mInfo.mVideo,
+ primeState->GetTags());
+ }
+ }
+}
+
+void OggDemuxer::FillTags(TrackInfo* aInfo, UniquePtr<MetadataTags>&& aTags) {
+ if (!aTags) {
+ return;
+ }
+ UniquePtr<MetadataTags> tags(std::move(aTags));
+ for (const auto& entry : *tags) {
+ aInfo->mTags.AppendElement(MetadataTag(entry.GetKey(), entry.GetData()));
+ }
+}
+
+nsresult OggDemuxer::ReadMetadata() {
+ OGG_DEBUG("OggDemuxer::ReadMetadata called!");
+
+ // We read packets until all bitstreams have read all their header packets.
+ // We record the offset of the first non-header page so that we know
+ // what page to seek to when seeking to the media start.
+
+ // @FIXME we have to read all the header packets on all the streams
+ // and THEN we can run SetupTarget*
+ // @fixme fixme
+
+ TrackInfo::TrackType tracks[2] = {TrackInfo::kAudioTrack,
+ TrackInfo::kVideoTrack};
+
+ nsTArray<OggCodecState*> bitstreams;
+ nsTArray<uint32_t> serials;
+
+ for (uint32_t i = 0; i < ArrayLength(tracks); i++) {
+ tainted_ogg<ogg_page*> page = mSandbox->malloc_in_sandbox<ogg_page>();
+ if (!page) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ auto clean_page = MakeScopeExit([&] { mSandbox->free_in_sandbox(page); });
+
+ bool readAllBOS = false;
+ while (!readAllBOS) {
+ if (!ReadOggPage(tracks[i], page.to_opaque())) {
+ // Some kind of error...
+ OGG_DEBUG("OggDemuxer::ReadOggPage failed? leaving ReadMetadata...");
+ return NS_ERROR_FAILURE;
+ }
+
+ uint32_t serial = static_cast<uint32_t>(
+ sandbox_invoke(*mSandbox, ogg_page_serialno, page)
+ .unverified_safe_because(RLBOX_OGG_PAGE_SERIAL_REASON));
+
+ if (!sandbox_invoke(*mSandbox, ogg_page_bos, page)
+ .unverified_safe_because(
+ "If this value is incorrect, it would mean not all "
+ "bitstreams are read. This does not affect the memory "
+ "safety of the renderer.")) {
+ // We've encountered a non Beginning Of Stream page. No more BOS pages
+ // can follow in this Ogg segment, so there will be no other bitstreams
+ // in the Ogg (unless it's invalid).
+ readAllBOS = true;
+ } else if (!mCodecStore.Contains(serial)) {
+ // We've not encountered a stream with this serial number before. Create
+ // an OggCodecState to demux it, and map that to the OggCodecState
+ // in mCodecStates.
+ OggCodecState* const codecState = mCodecStore.Add(
+ serial,
+ OggCodecState::Create(mSandbox.get(), page.to_opaque(), serial));
+ bitstreams.AppendElement(codecState);
+ serials.AppendElement(serial);
+ }
+ if (NS_FAILED(DemuxOggPage(tracks[i], page.to_opaque()))) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ }
+
+ // We've read all BOS pages, so we know the streams contained in the media.
+ // 1. Find the first encountered Theora/Vorbis/Opus bitstream, and configure
+ // it as the target A/V bitstream.
+ // 2. Deactivate the rest of bitstreams for now, until we have MediaInfo
+ // support multiple track infos.
+ for (uint32_t i = 0; i < bitstreams.Length(); ++i) {
+ OggCodecState* s = bitstreams[i];
+ if (s) {
+ if (s->GetType() == OggCodecState::TYPE_THEORA &&
+ ReadHeaders(TrackInfo::kVideoTrack, s)) {
+ if (!mTheoraState) {
+ SetupTarget(&mTheoraState, s);
+ } else {
+ s->Deactivate();
+ }
+ } else if (s->GetType() == OggCodecState::TYPE_VORBIS &&
+ ReadHeaders(TrackInfo::kAudioTrack, s)) {
+ if (!mVorbisState) {
+ SetupTarget(&mVorbisState, s);
+ } else {
+ s->Deactivate();
+ }
+ } else if (s->GetType() == OggCodecState::TYPE_OPUS &&
+ ReadHeaders(TrackInfo::kAudioTrack, s)) {
+ if (mOpusEnabled) {
+ if (!mOpusState) {
+ SetupTarget(&mOpusState, s);
+ } else {
+ s->Deactivate();
+ }
+ } else {
+ NS_WARNING(
+ "Opus decoding disabled."
+ " See media.opus.enabled in about:config");
+ }
+ } else if (s->GetType() == OggCodecState::TYPE_FLAC &&
+ ReadHeaders(TrackInfo::kAudioTrack, s)) {
+ if (!mFlacState) {
+ SetupTarget(&mFlacState, s);
+ } else {
+ s->Deactivate();
+ }
+ } else if (s->GetType() == OggCodecState::TYPE_SKELETON &&
+ !mSkeletonState) {
+ mSkeletonState = static_cast<SkeletonState*>(s);
+ } else {
+ // Deactivate any non-primary bitstreams.
+ s->Deactivate();
+ }
+ }
+ }
+
+ SetupTargetSkeleton();
+ SetupMediaTracksInfo(serials);
+
+ if (HasAudio() || HasVideo()) {
+ int64_t startTime = -1;
+ FindStartTime(startTime);
+ if (startTime >= 0) {
+ OGG_DEBUG("Detected stream start time %" PRId64, startTime);
+ mStartTime.emplace(startTime);
+ }
+
+ if (mInfo.mMetadataDuration.isNothing() &&
+ Resource(TrackInfo::kAudioTrack)->GetLength() >= 0) {
+ // We didn't get a duration from the index or a Content-Duration header.
+ // Seek to the end of file to find the end time.
+ int64_t length = Resource(TrackInfo::kAudioTrack)->GetLength();
+
+ MOZ_ASSERT(length > 0, "Must have a content length to get end time");
+
+ int64_t endTime = RangeEndTime(TrackInfo::kAudioTrack, length);
+
+ if (endTime != -1) {
+ mInfo.mUnadjustedMetadataEndTime.emplace(
+ TimeUnit::FromMicroseconds(endTime));
+ mInfo.mMetadataDuration.emplace(
+ TimeUnit::FromMicroseconds(endTime - mStartTime.refOr(0)));
+ OGG_DEBUG("Got Ogg duration from seeking to end %" PRId64, endTime);
+ }
+ }
+ if (mInfo.mMetadataDuration.isNothing()) {
+ mInfo.mMetadataDuration.emplace(TimeUnit::FromInfinity());
+ }
+ if (HasAudio()) {
+ mInfo.mAudio.mDuration = mInfo.mMetadataDuration.ref();
+ }
+ if (HasVideo()) {
+ mInfo.mVideo.mDuration = mInfo.mMetadataDuration.ref();
+ }
+ } else {
+ OGG_DEBUG("no audio or video tracks");
+ return NS_ERROR_FAILURE;
+ }
+
+ OGG_DEBUG("success?!");
+ return NS_OK;
+}
+
+void OggDemuxer::SetChained() {
+ {
+ if (mIsChained) {
+ return;
+ }
+ mIsChained = true;
+ }
+ if (mOnSeekableEvent) {
+ mOnSeekableEvent->Notify();
+ }
+}
+
+bool OggDemuxer::ReadOggChain(const media::TimeUnit& aLastEndTime) {
+ bool chained = false;
+ OpusState* newOpusState = nullptr;
+ VorbisState* newVorbisState = nullptr;
+ FlacState* newFlacState = nullptr;
+ UniquePtr<MetadataTags> tags;
+
+ if (HasVideo() || HasSkeleton() || !HasAudio()) {
+ return false;
+ }
+
+ tainted_ogg<ogg_page*> page = mSandbox->malloc_in_sandbox<ogg_page>();
+ if (!page) {
+ return false;
+ }
+ auto clean_page = MakeScopeExit([&] { mSandbox->free_in_sandbox(page); });
+ if (!ReadOggPage(TrackInfo::kAudioTrack, page.to_opaque()) ||
+ !sandbox_invoke(*mSandbox, ogg_page_bos, page)
+ .unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON)) {
+ // Chaining is only supported for audio only ogg files.
+ return false;
+ }
+
+ uint32_t serial = static_cast<uint32_t>(
+ sandbox_invoke(*mSandbox, ogg_page_serialno, page)
+ .unverified_safe_because(
+ "We are reading a new page with a serial number for the first "
+ "time and will check if we have seen it before prior to use."));
+ if (mCodecStore.Contains(serial)) {
+ return false;
+ }
+
+ UniquePtr<OggCodecState> codecState(
+ OggCodecState::Create(mSandbox.get(), page.to_opaque(), serial));
+ if (!codecState) {
+ return false;
+ }
+
+ if (mVorbisState && (codecState->GetType() == OggCodecState::TYPE_VORBIS)) {
+ newVorbisState = static_cast<VorbisState*>(codecState.get());
+ } else if (mOpusState &&
+ (codecState->GetType() == OggCodecState::TYPE_OPUS)) {
+ newOpusState = static_cast<OpusState*>(codecState.get());
+ } else if (mFlacState &&
+ (codecState->GetType() == OggCodecState::TYPE_FLAC)) {
+ newFlacState = static_cast<FlacState*>(codecState.get());
+ } else {
+ return false;
+ }
+
+ OggCodecState* state;
+
+ mCodecStore.Add(serial, std::move(codecState));
+ state = mCodecStore.Get(serial);
+
+ NS_ENSURE_TRUE(state != nullptr, false);
+
+ if (NS_FAILED(state->PageIn(page.to_opaque()))) {
+ return false;
+ }
+
+ MessageField* msgInfo = nullptr;
+ if (mSkeletonState) {
+ mSkeletonState->mMsgFieldStore.Get(serial, &msgInfo);
+ }
+
+ if ((newVorbisState && ReadHeaders(TrackInfo::kAudioTrack, newVorbisState)) &&
+ (mVorbisState->GetInfo()->GetAsAudioInfo()->mRate ==
+ newVorbisState->GetInfo()->GetAsAudioInfo()->mRate) &&
+ (mVorbisState->GetInfo()->GetAsAudioInfo()->mChannels ==
+ newVorbisState->GetInfo()->GetAsAudioInfo()->mChannels)) {
+ SetupTarget(&mVorbisState, newVorbisState);
+ OGG_DEBUG("New vorbis ogg link, serial=%d\n", mVorbisState->mSerial);
+
+ if (msgInfo) {
+ InitTrack(msgInfo, &mInfo.mAudio, true);
+ }
+
+ chained = true;
+ tags = newVorbisState->GetTags();
+ }
+
+ if ((newOpusState && ReadHeaders(TrackInfo::kAudioTrack, newOpusState)) &&
+ (mOpusState->GetInfo()->GetAsAudioInfo()->mRate ==
+ newOpusState->GetInfo()->GetAsAudioInfo()->mRate) &&
+ (mOpusState->GetInfo()->GetAsAudioInfo()->mChannels ==
+ newOpusState->GetInfo()->GetAsAudioInfo()->mChannels)) {
+ SetupTarget(&mOpusState, newOpusState);
+
+ if (msgInfo) {
+ InitTrack(msgInfo, &mInfo.mAudio, true);
+ }
+
+ chained = true;
+ tags = newOpusState->GetTags();
+ }
+
+ if ((newFlacState && ReadHeaders(TrackInfo::kAudioTrack, newFlacState)) &&
+ (mFlacState->GetInfo()->GetAsAudioInfo()->mRate ==
+ newFlacState->GetInfo()->GetAsAudioInfo()->mRate) &&
+ (mFlacState->GetInfo()->GetAsAudioInfo()->mChannels ==
+ newFlacState->GetInfo()->GetAsAudioInfo()->mChannels)) {
+ SetupTarget(&mFlacState, newFlacState);
+ OGG_DEBUG("New flac ogg link, serial=%d\n", mFlacState->mSerial);
+
+ if (msgInfo) {
+ InitTrack(msgInfo, &mInfo.mAudio, true);
+ }
+
+ chained = true;
+ tags = newFlacState->GetTags();
+ }
+
+ if (chained) {
+ SetChained();
+ mInfo.mMediaSeekable = false;
+ mDecodedAudioDuration += aLastEndTime;
+ if (mTimedMetadataEvent) {
+ mTimedMetadataEvent->Notify(
+ TimedMetadata(mDecodedAudioDuration, std::move(tags),
+ UniquePtr<MediaInfo>(new MediaInfo(mInfo))));
+ }
+ // Setup a new TrackInfo so that the MediaFormatReader will flush the
+ // current decoder.
+ mSharedAudioTrackInfo =
+ new TrackInfoSharedPtr(mInfo.mAudio, ++sStreamSourceID);
+ return true;
+ }
+
+ return false;
+}
+
+OggDemuxer::OggStateContext& OggDemuxer::OggState(TrackInfo::TrackType aType) {
+ if (aType == TrackInfo::kVideoTrack) {
+ return mVideoOggState;
+ }
+ return mAudioOggState;
+}
+
+tainted_opaque_ogg<ogg_sync_state*> OggDemuxer::OggSyncState(
+ TrackInfo::TrackType aType) {
+ return OggState(aType).mOggState.mState;
+}
+
+MediaResourceIndex* OggDemuxer::Resource(TrackInfo::TrackType aType) {
+ return &OggState(aType).mResource;
+}
+
+MediaResourceIndex* OggDemuxer::CommonResource() {
+ return &mAudioOggState.mResource;
+}
+
+bool OggDemuxer::ReadOggPage(TrackInfo::TrackType aType,
+ tainted_opaque_ogg<ogg_page*> aPage) {
+ int ret = 0;
+ while ((ret = sandbox_invoke(*mSandbox, ogg_sync_pageseek,
+ OggSyncState(aType), aPage)
+ .unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON)) <=
+ 0) {
+ if (ret < 0) {
+ // Lost page sync, have to skip up to next page.
+ continue;
+ }
+ // Returns a buffer that can be written too
+ // with the given size. This buffer is stored
+ // in the ogg synchronisation structure.
+ const uint32_t MIN_BUFFER_SIZE = 4096;
+ tainted_ogg<char*> buffer_tainted = sandbox_invoke(
+ *mSandbox, ogg_sync_buffer, OggSyncState(aType), MIN_BUFFER_SIZE);
+ MOZ_ASSERT(buffer_tainted != nullptr, "ogg_sync_buffer failed");
+
+ // Read from the resource into the buffer
+ uint32_t bytesRead = 0;
+
+ char* buffer = buffer_tainted.copy_and_verify_buffer_address(
+ [](uintptr_t val) { return reinterpret_cast<char*>(val); },
+ MIN_BUFFER_SIZE);
+
+ nsresult rv = Resource(aType)->Read(buffer, MIN_BUFFER_SIZE, &bytesRead);
+ if (NS_FAILED(rv) || !bytesRead) {
+ // End of file or error.
+ return false;
+ }
+
+ // Update the synchronisation layer with the number
+ // of bytes written to the buffer
+ ret = sandbox_invoke(*mSandbox, ogg_sync_wrote, OggSyncState(aType),
+ bytesRead)
+ .unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON);
+ NS_ENSURE_TRUE(ret == 0, false);
+ }
+
+ return true;
+}
+
+nsresult OggDemuxer::DemuxOggPage(TrackInfo::TrackType aType,
+ tainted_opaque_ogg<ogg_page*> aPage) {
+ tainted_ogg<int> serial = sandbox_invoke(*mSandbox, ogg_page_serialno, aPage);
+ OggCodecState* codecState = mCodecStore.Get(static_cast<uint32_t>(
+ serial.unverified_safe_because(RLBOX_OGG_PAGE_SERIAL_REASON)));
+ if (codecState == nullptr) {
+ OGG_DEBUG("encountered packet for unrecognized codecState");
+ return NS_ERROR_FAILURE;
+ }
+ if (GetCodecStateType(codecState) != aType &&
+ codecState->GetType() != OggCodecState::TYPE_SKELETON) {
+ // Not a page we're interested in.
+ return NS_OK;
+ }
+ if (NS_FAILED(codecState->PageIn(aPage))) {
+ OGG_DEBUG("codecState->PageIn failed");
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+bool OggDemuxer::IsSeekable() const {
+ if (mIsChained) {
+ return false;
+ }
+ return true;
+}
+
+UniquePtr<EncryptionInfo> OggDemuxer::GetCrypto() { return nullptr; }
+
+ogg_packet* OggDemuxer::GetNextPacket(TrackInfo::TrackType aType) {
+ OggCodecState* state = GetTrackCodecState(aType);
+ ogg_packet* packet = nullptr;
+ OggStateContext& context = OggState(aType);
+
+ while (true) {
+ if (packet) {
+ Unused << state->PacketOut();
+ }
+ DemuxUntilPacketAvailable(aType, state);
+
+ packet = state->PacketPeek();
+ if (!packet) {
+ break;
+ }
+ if (state->IsHeader(packet)) {
+ continue;
+ }
+ if (context.mNeedKeyframe && !state->IsKeyframe(packet)) {
+ continue;
+ }
+ context.mNeedKeyframe = false;
+ break;
+ }
+
+ return packet;
+}
+
+void OggDemuxer::DemuxUntilPacketAvailable(TrackInfo::TrackType aType,
+ OggCodecState* aState) {
+ while (!aState->IsPacketReady()) {
+ OGG_DEBUG("no packet yet, reading some more");
+ tainted_ogg<ogg_page*> page = mSandbox->malloc_in_sandbox<ogg_page>();
+ MOZ_RELEASE_ASSERT(page != nullptr);
+ auto clean_page = MakeScopeExit([&] { mSandbox->free_in_sandbox(page); });
+ if (!ReadOggPage(aType, page.to_opaque())) {
+ OGG_DEBUG("no more pages to read in resource?");
+ return;
+ }
+ DemuxOggPage(aType, page.to_opaque());
+ }
+}
+
+TimeIntervals OggDemuxer::GetBuffered(TrackInfo::TrackType aType) {
+ if (!HaveStartTime(aType)) {
+ return TimeIntervals();
+ }
+ if (mIsChained) {
+ return TimeIntervals::Invalid();
+ }
+ TimeIntervals buffered;
+ // HasAudio and HasVideo are not used here as they take a lock and cause
+ // a deadlock. Accessing mInfo doesn't require a lock - it doesn't change
+ // after metadata is read.
+ if (!mInfo.HasValidMedia()) {
+ // No need to search through the file if there are no audio or video tracks
+ return buffered;
+ }
+
+ AutoPinned<MediaResource> resource(Resource(aType)->GetResource());
+ MediaByteRangeSet ranges;
+ nsresult res = resource->GetCachedRanges(ranges);
+ NS_ENSURE_SUCCESS(res, TimeIntervals::Invalid());
+
+ const char time_interval_reason[] =
+ "Even if this computation is incorrect due to the reliance on tainted "
+ "values, only the search for the time interval or the time interval "
+ "returned will be affected. However this will not result in a memory "
+ "safety vulnerabilty in the Firefox renderer.";
+
+ // Traverse across the buffered byte ranges, determining the time ranges
+ // they contain. MediaResource::GetNextCachedData(offset) returns -1 when
+ // offset is after the end of the media resource, or there's no more cached
+ // data after the offset. This loop will run until we've checked every
+ // buffered range in the media, in increasing order of offset.
+ nsAutoOggSyncState sync(mSandbox.get());
+ for (uint32_t index = 0; index < ranges.Length(); index++) {
+ // Ensure the offsets are after the header pages.
+ int64_t startOffset = ranges[index].mStart;
+ int64_t endOffset = ranges[index].mEnd;
+
+ // Because the granulepos time is actually the end time of the page,
+ // we special-case (startOffset == 0) so that the first
+ // buffered range always appears to be buffered from the media start
+ // time, rather than from the end-time of the first page.
+ int64_t startTime = (startOffset == 0) ? StartTime() : -1;
+
+ // Find the start time of the range. Read pages until we find one with a
+ // granulepos which we can convert into a timestamp to use as the time of
+ // the start of the buffered range.
+ sandbox_invoke(*mSandbox, ogg_sync_reset, sync.mState);
+ tainted_ogg<ogg_page*> page = mSandbox->malloc_in_sandbox<ogg_page>();
+ if (!page) {
+ return TimeIntervals::Invalid();
+ }
+ auto clean_page = MakeScopeExit([&] { mSandbox->free_in_sandbox(page); });
+
+ while (startTime == -1) {
+ int32_t discard;
+ PageSyncResult pageSyncResult =
+ PageSync(mSandbox.get(), Resource(aType), sync.mState, true,
+ startOffset, endOffset, page, discard);
+ if (pageSyncResult == PAGE_SYNC_ERROR) {
+ return TimeIntervals::Invalid();
+ } else if (pageSyncResult == PAGE_SYNC_END_OF_RANGE) {
+ // Hit the end of range without reading a page, give up trying to
+ // find a start time for this buffered range, skip onto the next one.
+ break;
+ }
+
+ int64_t granulepos = sandbox_invoke(*mSandbox, ogg_page_granulepos, page)
+ .unverified_safe_because(time_interval_reason);
+ if (granulepos == -1) {
+ // Page doesn't have an end time, advance to the next page
+ // until we find one.
+
+ bool failedPageLenVerify = false;
+ // Page length should be under 64Kb according to
+ // https://xiph.org/ogg/doc/libogg/ogg_page.html
+ long pageLength =
+ CopyAndVerifyOrFail(page->header_len + page->body_len,
+ val <= 64 * 1024, &failedPageLenVerify);
+ if (failedPageLenVerify) {
+ return TimeIntervals::Invalid();
+ }
+
+ startOffset += pageLength;
+ continue;
+ }
+
+ tainted_ogg<uint32_t> serial = rlbox::sandbox_static_cast<uint32_t>(
+ sandbox_invoke(*mSandbox, ogg_page_serialno, page));
+ if (aType == TrackInfo::kAudioTrack && mVorbisState &&
+ (serial == mVorbisState->mSerial)
+ .unverified_safe_because(time_interval_reason)) {
+ startTime = mVorbisState->Time(granulepos);
+ MOZ_ASSERT(startTime > 0, "Must have positive start time");
+ } else if (aType == TrackInfo::kAudioTrack && mOpusState &&
+ (serial == mOpusState->mSerial)
+ .unverified_safe_because(time_interval_reason)) {
+ startTime = mOpusState->Time(granulepos);
+ MOZ_ASSERT(startTime > 0, "Must have positive start time");
+ } else if (aType == TrackInfo::kAudioTrack && mFlacState &&
+ (serial == mFlacState->mSerial)
+ .unverified_safe_because(time_interval_reason)) {
+ startTime = mFlacState->Time(granulepos);
+ MOZ_ASSERT(startTime > 0, "Must have positive start time");
+ } else if (aType == TrackInfo::kVideoTrack && mTheoraState &&
+ (serial == mTheoraState->mSerial)
+ .unverified_safe_because(time_interval_reason)) {
+ startTime = mTheoraState->Time(granulepos);
+ MOZ_ASSERT(startTime > 0, "Must have positive start time");
+ } else if (mCodecStore.Contains(
+ serial.unverified_safe_because(time_interval_reason))) {
+ // Stream is not the theora or vorbis stream we're playing,
+ // but is one that we have header data for.
+
+ bool failedPageLenVerify = false;
+ // Page length should be under 64Kb according to
+ // https://xiph.org/ogg/doc/libogg/ogg_page.html
+ long pageLength =
+ CopyAndVerifyOrFail(page->header_len + page->body_len,
+ val <= 64 * 1024, &failedPageLenVerify);
+ if (failedPageLenVerify) {
+ return TimeIntervals::Invalid();
+ }
+
+ startOffset += pageLength;
+ continue;
+ } else {
+ // Page is for a stream we don't know about (possibly a chained
+ // ogg), return OK to abort the finding any further ranges. This
+ // prevents us searching through the rest of the media when we
+ // may not be able to extract timestamps from it.
+ SetChained();
+ return buffered;
+ }
+ }
+
+ if (startTime != -1) {
+ // We were able to find a start time for that range, see if we can
+ // find an end time.
+ int64_t endTime = RangeEndTime(aType, startOffset, endOffset, true);
+ if (endTime > startTime) {
+ buffered +=
+ TimeInterval(TimeUnit::FromMicroseconds(startTime - StartTime()),
+ TimeUnit::FromMicroseconds(endTime - StartTime()));
+ }
+ }
+ }
+
+ return buffered;
+}
+
+void OggDemuxer::FindStartTime(int64_t& aOutStartTime) {
+ // Extract the start times of the bitstreams in order to calculate
+ // the duration.
+ int64_t videoStartTime = INT64_MAX;
+ int64_t audioStartTime = INT64_MAX;
+
+ if (HasVideo()) {
+ FindStartTime(TrackInfo::kVideoTrack, videoStartTime);
+ if (videoStartTime != INT64_MAX) {
+ OGG_DEBUG("OggDemuxer::FindStartTime() video=%" PRId64, videoStartTime);
+ mVideoOggState.mStartTime =
+ Some(TimeUnit::FromMicroseconds(videoStartTime));
+ }
+ }
+ if (HasAudio()) {
+ FindStartTime(TrackInfo::kAudioTrack, audioStartTime);
+ if (audioStartTime != INT64_MAX) {
+ OGG_DEBUG("OggDemuxer::FindStartTime() audio=%" PRId64, audioStartTime);
+ mAudioOggState.mStartTime =
+ Some(TimeUnit::FromMicroseconds(audioStartTime));
+ }
+ }
+
+ int64_t startTime = std::min(videoStartTime, audioStartTime);
+ if (startTime != INT64_MAX) {
+ aOutStartTime = startTime;
+ }
+}
+
+void OggDemuxer::FindStartTime(TrackInfo::TrackType aType,
+ int64_t& aOutStartTime) {
+ int64_t startTime = INT64_MAX;
+
+ OggCodecState* state = GetTrackCodecState(aType);
+ ogg_packet* pkt = GetNextPacket(aType);
+ if (pkt) {
+ startTime = state->PacketStartTime(pkt);
+ }
+
+ if (startTime != INT64_MAX) {
+ aOutStartTime = startTime;
+ }
+}
+
+nsresult OggDemuxer::SeekInternal(TrackInfo::TrackType aType,
+ const TimeUnit& aTarget) {
+ int64_t target = aTarget.ToMicroseconds();
+ OGG_DEBUG("About to seek to %" PRId64, target);
+ nsresult res;
+ int64_t adjustedTarget = target;
+ int64_t startTime = StartTime(aType);
+ int64_t endTime = mInfo.mMetadataDuration->ToMicroseconds() + startTime;
+ if (aType == TrackInfo::kAudioTrack && mOpusState) {
+ adjustedTarget =
+ std::max(startTime, target - OGG_SEEK_OPUS_PREROLL.ToMicroseconds());
+ }
+
+ if (!HaveStartTime(aType) || adjustedTarget == startTime) {
+ // We've seeked to the media start or we can't seek.
+ // Just seek to the offset of the first content page.
+ res = Resource(aType)->Seek(nsISeekableStream::NS_SEEK_SET, 0);
+ NS_ENSURE_SUCCESS(res, res);
+
+ res = Reset(aType);
+ NS_ENSURE_SUCCESS(res, res);
+ } else {
+ // TODO: This may seek back unnecessarily far in the video, but we don't
+ // have a way of asking Skeleton to seek to a different target for each
+ // stream yet. Using adjustedTarget here is at least correct, if slow.
+ IndexedSeekResult sres = SeekToKeyframeUsingIndex(aType, adjustedTarget);
+ NS_ENSURE_TRUE(sres != SEEK_FATAL_ERROR, NS_ERROR_FAILURE);
+ if (sres == SEEK_INDEX_FAIL) {
+ // No index or other non-fatal index-related failure. Try to seek
+ // using a bisection search. Determine the already downloaded data
+ // in the media cache, so we can try to seek in the cached data first.
+ AutoTArray<SeekRange, 16> ranges;
+ res = GetSeekRanges(aType, ranges);
+ NS_ENSURE_SUCCESS(res, res);
+
+ // Figure out if the seek target lies in a buffered range.
+ SeekRange r =
+ SelectSeekRange(aType, ranges, target, startTime, endTime, true);
+
+ if (!r.IsNull()) {
+ // We know the buffered range in which the seek target lies, do a
+ // bisection search in that buffered range.
+ res = SeekInBufferedRange(aType, target, adjustedTarget, startTime,
+ endTime, ranges, r);
+ NS_ENSURE_SUCCESS(res, res);
+ } else {
+ // The target doesn't lie in a buffered range. Perform a bisection
+ // search over the whole media, using the known buffered ranges to
+ // reduce the search space.
+ res = SeekInUnbuffered(aType, target, startTime, endTime, ranges);
+ NS_ENSURE_SUCCESS(res, res);
+ }
+ }
+ }
+
+ // Demux forwards until we find the first keyframe prior the target.
+ // there may be non-keyframes in the page before the keyframe.
+ // Additionally, we may have seeked to the first page referenced by the
+ // page index which may be quite far off the target.
+ // When doing fastSeek we display the first frame after the seek, so
+ // we need to advance the decode to the keyframe otherwise we'll get
+ // visual artifacts in the first frame output after the seek.
+ OggCodecState* state = GetTrackCodecState(aType);
+ OggPacketQueue tempPackets;
+ bool foundKeyframe = false;
+ while (true) {
+ DemuxUntilPacketAvailable(aType, state);
+ ogg_packet* packet = state->PacketPeek();
+ if (packet == nullptr) {
+ OGG_DEBUG("End of stream reached before keyframe found in indexed seek");
+ break;
+ }
+ int64_t startTstamp = state->PacketStartTime(packet);
+ if (foundKeyframe && startTstamp > adjustedTarget) {
+ break;
+ }
+ if (state->IsKeyframe(packet)) {
+ OGG_DEBUG("keyframe found after seeking at %" PRId64, startTstamp);
+ tempPackets.Erase();
+ foundKeyframe = true;
+ }
+ if (foundKeyframe && startTstamp == adjustedTarget) {
+ break;
+ }
+ if (foundKeyframe) {
+ tempPackets.Append(state->PacketOut());
+ } else {
+ // Discard video packets before the first keyframe.
+ Unused << state->PacketOut();
+ }
+ }
+ // Re-add all packet into the codec state in order.
+ state->PushFront(std::move(tempPackets));
+
+ return NS_OK;
+}
+
+OggDemuxer::IndexedSeekResult OggDemuxer::RollbackIndexedSeek(
+ TrackInfo::TrackType aType, int64_t aOffset) {
+ if (mSkeletonState) {
+ mSkeletonState->Deactivate();
+ }
+ nsresult res = Resource(aType)->Seek(nsISeekableStream::NS_SEEK_SET, aOffset);
+ NS_ENSURE_SUCCESS(res, SEEK_FATAL_ERROR);
+ return SEEK_INDEX_FAIL;
+}
+
+OggDemuxer::IndexedSeekResult OggDemuxer::SeekToKeyframeUsingIndex(
+ TrackInfo::TrackType aType, int64_t aTarget) {
+ if (!HasSkeleton() || !mSkeletonState->HasIndex()) {
+ return SEEK_INDEX_FAIL;
+ }
+ // We have an index from the Skeleton track, try to use it to seek.
+ AutoTArray<uint32_t, 2> tracks;
+ BuildSerialList(tracks);
+ SkeletonState::nsSeekTarget keyframe;
+ if (NS_FAILED(mSkeletonState->IndexedSeekTarget(aTarget, tracks, keyframe))) {
+ // Could not locate a keypoint for the target in the index.
+ return SEEK_INDEX_FAIL;
+ }
+
+ // Remember original resource read cursor position so we can rollback on
+ // failure.
+ int64_t tell = Resource(aType)->Tell();
+
+ // Seek to the keypoint returned by the index.
+ if (keyframe.mKeyPoint.mOffset > Resource(aType)->GetLength() ||
+ keyframe.mKeyPoint.mOffset < 0) {
+ // Index must be invalid.
+ return RollbackIndexedSeek(aType, tell);
+ }
+ OGG_DEBUG("Seeking using index to keyframe at offset %" PRId64 "\n",
+ keyframe.mKeyPoint.mOffset);
+ nsresult res = Resource(aType)->Seek(nsISeekableStream::NS_SEEK_SET,
+ keyframe.mKeyPoint.mOffset);
+ NS_ENSURE_SUCCESS(res, SEEK_FATAL_ERROR);
+
+ // We've moved the read set, so reset decode.
+ res = Reset(aType);
+ NS_ENSURE_SUCCESS(res, SEEK_FATAL_ERROR);
+
+ // Check that the page the index thinks is exactly here is actually exactly
+ // here. If not, the index is invalid.
+ tainted_ogg<ogg_page*> page = mSandbox->malloc_in_sandbox<ogg_page>();
+ if (!page) {
+ return SEEK_INDEX_FAIL;
+ }
+ auto clean_page = MakeScopeExit([&] { mSandbox->free_in_sandbox(page); });
+ int skippedBytes = 0;
+ PageSyncResult syncres =
+ PageSync(mSandbox.get(), Resource(aType), OggSyncState(aType), false,
+ keyframe.mKeyPoint.mOffset, Resource(aType)->GetLength(), page,
+ skippedBytes);
+ NS_ENSURE_TRUE(syncres != PAGE_SYNC_ERROR, SEEK_FATAL_ERROR);
+ if (syncres != PAGE_SYNC_OK || skippedBytes != 0) {
+ OGG_DEBUG(
+ "Indexed-seek failure: Ogg Skeleton Index is invalid "
+ "or sync error after seek");
+ return RollbackIndexedSeek(aType, tell);
+ }
+ uint32_t serial = static_cast<uint32_t>(
+ sandbox_invoke(*mSandbox, ogg_page_serialno, page)
+ .unverified_safe_because(
+ "Serial is only used to locate the correct page. If the serial "
+ "is incorrect the the renderer would just fail to seek with an "
+ "error code. This would not lead to any memory safety bugs."));
+ if (serial != keyframe.mSerial) {
+ // Serialno of page at offset isn't what the index told us to expect.
+ // Assume the index is invalid.
+ return RollbackIndexedSeek(aType, tell);
+ }
+ OggCodecState* codecState = mCodecStore.Get(serial);
+ if (codecState && codecState->mActive &&
+ sandbox_invoke(*mSandbox, ogg_stream_pagein, codecState->mState, page)
+ .unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON) != 0) {
+ // Couldn't insert page into the ogg resource, or somehow the resource
+ // is no longer active.
+ return RollbackIndexedSeek(aType, tell);
+ }
+ return SEEK_OK;
+}
+
+// Reads a page from the media resource.
+OggDemuxer::PageSyncResult OggDemuxer::PageSync(
+ rlbox_sandbox_ogg* aSandbox, MediaResourceIndex* aResource,
+ tainted_opaque_ogg<ogg_sync_state*> aState, bool aCachedDataOnly,
+ int64_t aOffset, int64_t aEndOffset, tainted_ogg<ogg_page*> aPage,
+ int& aSkippedBytes) {
+ aSkippedBytes = 0;
+ // Sync to the next page.
+ tainted_ogg<int> ret = 0;
+ uint32_t bytesRead = 0;
+ int64_t readHead = aOffset;
+ while (ret.unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON) <= 0) {
+ tainted_ogg<long> seek_ret =
+ sandbox_invoke(*aSandbox, ogg_sync_pageseek, aState, aPage);
+
+ // We aren't really verifying the value of seek_ret below.
+ // We are merely ensuring that it won't overflow an integer.
+ // However we are assigning the value to ret which is marked tainted, so
+ // this is fine.
+ bool failedVerify = false;
+ CheckedInt<int> checker;
+ ret = CopyAndVerifyOrFail(
+ seek_ret, (static_cast<void>(checker = val), checker.isValid()),
+ &failedVerify);
+ if (failedVerify) {
+ return PAGE_SYNC_ERROR;
+ }
+
+ if (ret.unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON) == 0) {
+ const int page_step_val = PAGE_STEP;
+ tainted_ogg<char*> buffer_tainted =
+ sandbox_invoke(*aSandbox, ogg_sync_buffer, aState, page_step_val);
+ MOZ_ASSERT(buffer_tainted != nullptr, "Must have a buffer");
+
+ // Read from the file into the buffer
+ int64_t bytesToRead =
+ std::min(static_cast<int64_t>(PAGE_STEP), aEndOffset - readHead);
+ MOZ_ASSERT(bytesToRead <= UINT32_MAX, "bytesToRead range check");
+ if (bytesToRead <= 0) {
+ return PAGE_SYNC_END_OF_RANGE;
+ }
+ char* buffer = buffer_tainted.copy_and_verify_buffer_address(
+ [](uintptr_t val) { return reinterpret_cast<char*>(val); },
+ static_cast<size_t>(bytesToRead));
+
+ nsresult rv = NS_OK;
+ if (aCachedDataOnly) {
+ rv = aResource->GetResource()->ReadFromCache(
+ buffer, readHead, static_cast<uint32_t>(bytesToRead));
+ NS_ENSURE_SUCCESS(rv, PAGE_SYNC_ERROR);
+ bytesRead = static_cast<uint32_t>(bytesToRead);
+ } else {
+ rv = aResource->Seek(nsISeekableStream::NS_SEEK_SET, readHead);
+ NS_ENSURE_SUCCESS(rv, PAGE_SYNC_ERROR);
+ rv = aResource->Read(buffer, static_cast<uint32_t>(bytesToRead),
+ &bytesRead);
+ NS_ENSURE_SUCCESS(rv, PAGE_SYNC_ERROR);
+ }
+ if (bytesRead == 0 && NS_SUCCEEDED(rv)) {
+ // End of file.
+ return PAGE_SYNC_END_OF_RANGE;
+ }
+ readHead += bytesRead;
+
+ // Update the synchronisation layer with the number
+ // of bytes written to the buffer
+ ret = sandbox_invoke(*aSandbox, ogg_sync_wrote, aState, bytesRead);
+ NS_ENSURE_TRUE(
+ ret.unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON) == 0,
+ PAGE_SYNC_ERROR);
+ continue;
+ }
+
+ if (ret.unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON) < 0) {
+ MOZ_ASSERT(aSkippedBytes >= 0, "Offset >= 0");
+ bool failedSkippedBytesVerify = false;
+ ret.copy_and_verify([&](int val) {
+ int64_t result = static_cast<int64_t>(aSkippedBytes) - val;
+ if (result > std::numeric_limits<int>::max() ||
+ result > (aEndOffset - aOffset) || result < 0) {
+ failedSkippedBytesVerify = true;
+ } else {
+ aSkippedBytes = result;
+ }
+ });
+ if (failedSkippedBytesVerify) {
+ return PAGE_SYNC_ERROR;
+ }
+ continue;
+ }
+ }
+
+ return PAGE_SYNC_OK;
+}
+
+// OggTrackDemuxer
+OggTrackDemuxer::OggTrackDemuxer(OggDemuxer* aParent,
+ TrackInfo::TrackType aType,
+ uint32_t aTrackNumber)
+ : mParent(aParent), mType(aType) {
+ mInfo = mParent->GetTrackInfo(aType, aTrackNumber);
+ MOZ_ASSERT(mInfo);
+}
+
+OggTrackDemuxer::~OggTrackDemuxer() = default;
+
+UniquePtr<TrackInfo> OggTrackDemuxer::GetInfo() const { return mInfo->Clone(); }
+
+RefPtr<OggTrackDemuxer::SeekPromise> OggTrackDemuxer::Seek(
+ const TimeUnit& aTime) {
+ // Seeks to aTime. Upon success, SeekPromise will be resolved with the
+ // actual time seeked to. Typically the random access point time
+ mQueuedSample = nullptr;
+ TimeUnit seekTime = aTime;
+ if (mParent->SeekInternal(mType, aTime) == NS_OK) {
+ RefPtr<MediaRawData> sample(NextSample());
+
+ // Check what time we actually seeked to.
+ if (sample != nullptr) {
+ seekTime = sample->mTime;
+ OGG_DEBUG("%p seeked to time %" PRId64, this, seekTime.ToMicroseconds());
+ }
+ mQueuedSample = sample;
+
+ return SeekPromise::CreateAndResolve(seekTime, __func__);
+ } else {
+ return SeekPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_DEMUXER_ERR,
+ __func__);
+ }
+}
+
+RefPtr<MediaRawData> OggTrackDemuxer::NextSample() {
+ if (mQueuedSample) {
+ RefPtr<MediaRawData> nextSample = mQueuedSample;
+ mQueuedSample = nullptr;
+ if (mType == TrackInfo::kAudioTrack) {
+ nextSample->mTrackInfo = mParent->mSharedAudioTrackInfo;
+ }
+ return nextSample;
+ }
+ ogg_packet* packet = mParent->GetNextPacket(mType);
+ if (!packet) {
+ return nullptr;
+ }
+ // Check the eos state in case we need to look for chained streams.
+ bool eos = packet->e_o_s;
+ OggCodecState* state = mParent->GetTrackCodecState(mType);
+ RefPtr<MediaRawData> data = state->PacketOutAsMediaRawData();
+ // ogg allows 'nil' packets, that are EOS and of size 0.
+ if (!data || (data->mEOS && data->Size() == 0)) {
+ return nullptr;
+ }
+ if (mType == TrackInfo::kAudioTrack) {
+ data->mTrackInfo = mParent->mSharedAudioTrackInfo;
+ }
+ // mDecodedAudioDuration gets adjusted during ReadOggChain().
+ TimeUnit totalDuration = mParent->mDecodedAudioDuration;
+ if (eos) {
+ // We've encountered an end of bitstream packet; check for a chained
+ // bitstream following this one.
+ // This will also update mSharedAudioTrackInfo.
+ mParent->ReadOggChain(data->GetEndTime());
+ }
+ data->mOffset = mParent->Resource(mType)->Tell();
+ // We adjust the start time of the sample to account for the potential ogg
+ // chaining.
+ data->mTime += totalDuration;
+ if (!data->mTime.IsValid()) {
+ return nullptr;
+ }
+
+ return data;
+}
+
+RefPtr<OggTrackDemuxer::SamplesPromise> OggTrackDemuxer::GetSamples(
+ int32_t aNumSamples) {
+ RefPtr<SamplesHolder> samples = new SamplesHolder;
+ if (!aNumSamples) {
+ return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_DEMUXER_ERR,
+ __func__);
+ }
+
+ while (aNumSamples) {
+ RefPtr<MediaRawData> sample(NextSample());
+ if (!sample) {
+ break;
+ }
+ if (!sample->HasValidTime()) {
+ return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_DEMUXER_ERR,
+ __func__);
+ }
+ samples->AppendSample(sample);
+ aNumSamples--;
+ }
+
+ if (samples->GetSamples().IsEmpty()) {
+ return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_END_OF_STREAM,
+ __func__);
+ } else {
+ return SamplesPromise::CreateAndResolve(samples, __func__);
+ }
+}
+
+void OggTrackDemuxer::Reset() {
+ mParent->Reset(mType);
+ mQueuedSample = nullptr;
+}
+
+RefPtr<OggTrackDemuxer::SkipAccessPointPromise>
+OggTrackDemuxer::SkipToNextRandomAccessPoint(const TimeUnit& aTimeThreshold) {
+ uint32_t parsed = 0;
+ bool found = false;
+ RefPtr<MediaRawData> sample;
+
+ OGG_DEBUG("TimeThreshold: %f", aTimeThreshold.ToSeconds());
+ while (!found && (sample = NextSample())) {
+ parsed++;
+ if (sample->mKeyframe && sample->mTime >= aTimeThreshold) {
+ found = true;
+ mQueuedSample = sample;
+ }
+ }
+ if (found) {
+ OGG_DEBUG("next sample: %f (parsed: %d)", sample->mTime.ToSeconds(),
+ parsed);
+ return SkipAccessPointPromise::CreateAndResolve(parsed, __func__);
+ } else {
+ SkipFailureHolder failure(NS_ERROR_DOM_MEDIA_END_OF_STREAM, parsed);
+ return SkipAccessPointPromise::CreateAndReject(std::move(failure),
+ __func__);
+ }
+}
+
+TimeIntervals OggTrackDemuxer::GetBuffered() {
+ return mParent->GetBuffered(mType);
+}
+
+void OggTrackDemuxer::BreakCycles() { mParent = nullptr; }
+
+// Returns an ogg page's checksum.
+tainted_opaque_ogg<ogg_uint32_t> OggDemuxer::GetPageChecksum(
+ tainted_opaque_ogg<ogg_page*> aPage) {
+ tainted_ogg<ogg_page*> page = rlbox::from_opaque(aPage);
+
+ const char hint_reason[] =
+ "Early bail out of checksum. Even if this is wrong, the renderer's "
+ "security is not compromised.";
+ if (page == nullptr ||
+ (page->header == nullptr).unverified_safe_because(hint_reason) ||
+ (page->header_len < 25).unverified_safe_because(hint_reason)) {
+ tainted_ogg<ogg_uint32_t> ret = 0;
+ return ret.to_opaque();
+ }
+
+ const int CHECKSUM_BYTES_LENGTH = 4;
+ const unsigned char* p =
+ (page->header + 22u)
+ .copy_and_verify_buffer_address(
+ [](uintptr_t val) {
+ return reinterpret_cast<const unsigned char*>(val);
+ },
+ CHECKSUM_BYTES_LENGTH);
+ uint32_t c =
+ static_cast<uint32_t>(p[0] + (p[1] << 8) + (p[2] << 16) + (p[3] << 24));
+ tainted_ogg<uint32_t> ret = c;
+ return ret.to_opaque();
+}
+
+int64_t OggDemuxer::RangeStartTime(TrackInfo::TrackType aType,
+ int64_t aOffset) {
+ int64_t position = Resource(aType)->Tell();
+ nsresult res = Resource(aType)->Seek(nsISeekableStream::NS_SEEK_SET, aOffset);
+ NS_ENSURE_SUCCESS(res, 0);
+ int64_t startTime = 0;
+ FindStartTime(aType, startTime);
+ res = Resource(aType)->Seek(nsISeekableStream::NS_SEEK_SET, position);
+ NS_ENSURE_SUCCESS(res, -1);
+ return startTime;
+}
+
+struct nsDemuxerAutoOggSyncState {
+ explicit nsDemuxerAutoOggSyncState(rlbox_sandbox_ogg& aSandbox)
+ : mSandbox(aSandbox) {
+ mState = mSandbox.malloc_in_sandbox<ogg_sync_state>();
+ MOZ_RELEASE_ASSERT(mState != nullptr);
+ sandbox_invoke(mSandbox, ogg_sync_init, mState);
+ }
+ ~nsDemuxerAutoOggSyncState() {
+ sandbox_invoke(mSandbox, ogg_sync_clear, mState);
+ mSandbox.free_in_sandbox(mState);
+ }
+ rlbox_sandbox_ogg& mSandbox;
+ tainted_ogg<ogg_sync_state*> mState;
+};
+
+int64_t OggDemuxer::RangeEndTime(TrackInfo::TrackType aType,
+ int64_t aEndOffset) {
+ int64_t position = Resource(aType)->Tell();
+ int64_t endTime = RangeEndTime(aType, 0, aEndOffset, false);
+ nsresult res =
+ Resource(aType)->Seek(nsISeekableStream::NS_SEEK_SET, position);
+ NS_ENSURE_SUCCESS(res, -1);
+ return endTime;
+}
+
+int64_t OggDemuxer::RangeEndTime(TrackInfo::TrackType aType,
+ int64_t aStartOffset, int64_t aEndOffset,
+ bool aCachedDataOnly) {
+ nsDemuxerAutoOggSyncState sync(*mSandbox);
+
+ // We need to find the last page which ends before aEndOffset that
+ // has a granulepos that we can convert to a timestamp. We do this by
+ // backing off from aEndOffset until we encounter a page on which we can
+ // interpret the granulepos. If while backing off we encounter a page which
+ // we've previously encountered before, we'll either backoff again if we
+ // haven't found an end time yet, or return the last end time found.
+ const int step = 5000;
+ const int maxOggPageSize = 65306;
+ int64_t readStartOffset = aEndOffset;
+ int64_t readLimitOffset = aEndOffset;
+ int64_t readHead = aEndOffset;
+ int64_t endTime = -1;
+ uint32_t checksumAfterSeek = 0;
+ uint32_t prevChecksumAfterSeek = 0;
+ bool mustBackOff = false;
+ tainted_ogg<ogg_page*> page = mSandbox->malloc_in_sandbox<ogg_page>();
+ if (!page) {
+ return -1;
+ }
+ auto clean_page = MakeScopeExit([&] { mSandbox->free_in_sandbox(page); });
+ while (true) {
+ tainted_ogg<long> seek_ret =
+ sandbox_invoke(*mSandbox, ogg_sync_pageseek, sync.mState, page);
+
+ // We aren't really verifying the value of seek_ret below.
+ // We are merely ensuring that it won't overflow an integer.
+ // However we are assigning the value to ret which is marked tainted, so
+ // this is fine.
+ bool failedVerify = false;
+ CheckedInt<int> checker;
+ tainted_ogg<int> ret = CopyAndVerifyOrFail(
+ seek_ret, (static_cast<void>(checker = val), checker.isValid()),
+ &failedVerify);
+ if (failedVerify) {
+ return -1;
+ }
+
+ if (ret.unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON) == 0) {
+ // We need more data if we've not encountered a page we've seen before,
+ // or we've read to the end of file.
+ if (mustBackOff || readHead == aEndOffset || readHead == aStartOffset) {
+ if (endTime != -1 || readStartOffset == 0) {
+ // We have encountered a page before, or we're at the end of file.
+ break;
+ }
+ mustBackOff = false;
+ prevChecksumAfterSeek = checksumAfterSeek;
+ checksumAfterSeek = 0;
+ sandbox_invoke(*mSandbox, ogg_sync_reset, sync.mState);
+ readStartOffset =
+ std::max(static_cast<int64_t>(0), readStartOffset - step);
+ // There's no point reading more than the maximum size of
+ // an Ogg page into data we've previously scanned. Any data
+ // between readLimitOffset and aEndOffset must be garbage
+ // and we can ignore it thereafter.
+ readLimitOffset =
+ std::min(readLimitOffset, readStartOffset + maxOggPageSize);
+ readHead = std::max(aStartOffset, readStartOffset);
+ }
+
+ int64_t limit =
+ std::min(static_cast<int64_t>(UINT32_MAX), aEndOffset - readHead);
+ limit = std::max(static_cast<int64_t>(0), limit);
+ limit = std::min(limit, static_cast<int64_t>(step));
+ uint32_t bytesToRead = static_cast<uint32_t>(limit);
+ uint32_t bytesRead = 0;
+ tainted_ogg<char*> buffer_tainted =
+ sandbox_invoke(*mSandbox, ogg_sync_buffer, sync.mState, bytesToRead);
+ char* buffer = buffer_tainted.copy_and_verify_buffer_address(
+ [](uintptr_t val) { return reinterpret_cast<char*>(val); },
+ bytesToRead);
+ MOZ_ASSERT(buffer, "Must have buffer");
+ nsresult res;
+ if (aCachedDataOnly) {
+ res = Resource(aType)->GetResource()->ReadFromCache(buffer, readHead,
+ bytesToRead);
+ NS_ENSURE_SUCCESS(res, -1);
+ bytesRead = bytesToRead;
+ } else {
+ MOZ_ASSERT(readHead < aEndOffset,
+ "resource pos must be before range end");
+ res = Resource(aType)->Seek(nsISeekableStream::NS_SEEK_SET, readHead);
+ NS_ENSURE_SUCCESS(res, -1);
+ res = Resource(aType)->Read(buffer, bytesToRead, &bytesRead);
+ NS_ENSURE_SUCCESS(res, -1);
+ }
+ readHead += bytesRead;
+ if (readHead > readLimitOffset) {
+ mustBackOff = true;
+ }
+
+ // Update the synchronisation layer with the number
+ // of bytes written to the buffer
+ ret = sandbox_invoke(*mSandbox, ogg_sync_wrote, sync.mState, bytesRead);
+ bool failedWroteVerify = false;
+ int wrote_success =
+ CopyAndVerifyOrFail(ret, val == 0 || val == -1, &failedWroteVerify);
+ if (failedWroteVerify) {
+ return -1;
+ }
+
+ if (wrote_success != 0) {
+ endTime = -1;
+ break;
+ }
+ continue;
+ }
+
+ if (ret.unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON) < 0 ||
+ sandbox_invoke(*mSandbox, ogg_page_granulepos, page)
+ .unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON) < 0) {
+ continue;
+ }
+
+ tainted_ogg<uint32_t> checksum_tainted =
+ rlbox::from_opaque(GetPageChecksum(page.to_opaque()));
+ uint32_t checksum = checksum_tainted.unverified_safe_because(
+ "checksum is only being used as a hint as part of search for end time. "
+ "Incorrect values will not affect the memory safety of the renderer.");
+ if (checksumAfterSeek == 0) {
+ // This is the first page we've decoded after a backoff/seek. Remember
+ // the page checksum. If we backoff further and encounter this page
+ // again, we'll know that we won't find a page with an end time after
+ // this one, so we'll know to back off again.
+ checksumAfterSeek = checksum;
+ }
+ if (checksum == prevChecksumAfterSeek) {
+ // This page has the same checksum as the first page we encountered
+ // after the last backoff/seek. Since we've already scanned after this
+ // page and failed to find an end time, we may as well backoff again and
+ // try to find an end time from an earlier page.
+ mustBackOff = true;
+ continue;
+ }
+
+ int64_t granulepos =
+ sandbox_invoke(*mSandbox, ogg_page_granulepos, page)
+ .unverified_safe_because(
+ "If this is incorrect it may lead to incorrect seeking "
+ "behavior in the stream, however will not affect the memory "
+ "safety of the Firefox renderer.");
+ uint32_t serial = static_cast<uint32_t>(
+ sandbox_invoke(*mSandbox, ogg_page_serialno, page)
+ .unverified_safe_because(RLBOX_OGG_PAGE_SERIAL_REASON));
+
+ OggCodecState* codecState = nullptr;
+ codecState = mCodecStore.Get(serial);
+ if (!codecState) {
+ // This page is from a bitstream which we haven't encountered yet.
+ // It's probably from a new "link" in a "chained" ogg. Don't
+ // bother even trying to find a duration...
+ SetChained();
+ endTime = -1;
+ break;
+ }
+
+ int64_t t = codecState->Time(granulepos);
+ if (t != -1) {
+ endTime = t;
+ }
+ }
+
+ return endTime;
+}
+
+nsresult OggDemuxer::GetSeekRanges(TrackInfo::TrackType aType,
+ nsTArray<SeekRange>& aRanges) {
+ AutoPinned<MediaResource> resource(Resource(aType)->GetResource());
+ MediaByteRangeSet cached;
+ nsresult res = resource->GetCachedRanges(cached);
+ NS_ENSURE_SUCCESS(res, res);
+
+ for (uint32_t index = 0; index < cached.Length(); index++) {
+ auto& range = cached[index];
+ int64_t startTime = -1;
+ int64_t endTime = -1;
+ if (NS_FAILED(Reset(aType))) {
+ return NS_ERROR_FAILURE;
+ }
+ int64_t startOffset = range.mStart;
+ int64_t endOffset = range.mEnd;
+ startTime = RangeStartTime(aType, startOffset);
+ if (startTime != -1 && ((endTime = RangeEndTime(aType, endOffset)) != -1)) {
+ NS_WARNING_ASSERTION(startTime < endTime,
+ "Start time must be before end time");
+ aRanges.AppendElement(
+ SeekRange(startOffset, endOffset, startTime, endTime));
+ }
+ }
+ if (NS_FAILED(Reset(aType))) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+OggDemuxer::SeekRange OggDemuxer::SelectSeekRange(
+ TrackInfo::TrackType aType, const nsTArray<SeekRange>& ranges,
+ int64_t aTarget, int64_t aStartTime, int64_t aEndTime, bool aExact) {
+ int64_t so = 0;
+ int64_t eo = Resource(aType)->GetLength();
+ int64_t st = aStartTime;
+ int64_t et = aEndTime;
+ for (uint32_t i = 0; i < ranges.Length(); i++) {
+ const SeekRange& r = ranges[i];
+ if (r.mTimeStart < aTarget) {
+ so = r.mOffsetStart;
+ st = r.mTimeStart;
+ }
+ if (r.mTimeEnd >= aTarget && r.mTimeEnd < et) {
+ eo = r.mOffsetEnd;
+ et = r.mTimeEnd;
+ }
+
+ if (r.mTimeStart < aTarget && aTarget <= r.mTimeEnd) {
+ // Target lies exactly in this range.
+ return ranges[i];
+ }
+ }
+ if (aExact || eo == -1) {
+ return SeekRange();
+ }
+ return SeekRange(so, eo, st, et);
+}
+
+nsresult OggDemuxer::SeekInBufferedRange(TrackInfo::TrackType aType,
+ int64_t aTarget,
+ int64_t aAdjustedTarget,
+ int64_t aStartTime, int64_t aEndTime,
+ const nsTArray<SeekRange>& aRanges,
+ const SeekRange& aRange) {
+ OGG_DEBUG("Seeking in buffered data to %" PRId64 " using bisection search",
+ aTarget);
+ if (aType == TrackInfo::kVideoTrack || aAdjustedTarget >= aTarget) {
+ // We know the exact byte range in which the target must lie. It must
+ // be buffered in the media cache. Seek there.
+ nsresult res = SeekBisection(aType, aTarget, aRange, 0);
+ if (NS_FAILED(res) || aType != TrackInfo::kVideoTrack) {
+ return res;
+ }
+
+ // We have an active Theora bitstream. Peek the next Theora frame, and
+ // extract its keyframe's time.
+ DemuxUntilPacketAvailable(aType, mTheoraState);
+ ogg_packet* packet = mTheoraState->PacketPeek();
+ if (packet && !mTheoraState->IsKeyframe(packet)) {
+ // First post-seek frame isn't a keyframe, seek back to previous keyframe,
+ // otherwise we'll get visual artifacts.
+ MOZ_ASSERT(packet->granulepos != -1, "Must have a granulepos");
+ int shift = mTheoraState->KeyFrameGranuleJobs();
+ int64_t keyframeGranulepos = (packet->granulepos >> shift) << shift;
+ int64_t keyframeTime = mTheoraState->StartTime(keyframeGranulepos);
+ SEEK_LOG(LogLevel::Debug,
+ ("Keyframe for %lld is at %lld, seeking back to it", frameTime,
+ keyframeTime));
+ aAdjustedTarget = std::min(aAdjustedTarget, keyframeTime);
+ }
+ }
+
+ nsresult res = NS_OK;
+ if (aAdjustedTarget < aTarget) {
+ SeekRange k = SelectSeekRange(aType, aRanges, aAdjustedTarget, aStartTime,
+ aEndTime, false);
+ res = SeekBisection(aType, aAdjustedTarget, k, OGG_SEEK_FUZZ_USECS);
+ }
+ return res;
+}
+
+nsresult OggDemuxer::SeekInUnbuffered(TrackInfo::TrackType aType,
+ int64_t aTarget, int64_t aStartTime,
+ int64_t aEndTime,
+ const nsTArray<SeekRange>& aRanges) {
+ OGG_DEBUG("Seeking in unbuffered data to %" PRId64 " using bisection search",
+ aTarget);
+
+ // If we've got an active Theora bitstream, determine the maximum possible
+ // time in usecs which a keyframe could be before a given interframe. We
+ // subtract this from our seek target, seek to the new target, and then
+ // will decode forward to the original seek target. We should encounter a
+ // keyframe in that interval. This prevents us from needing to run two
+ // bisections; one for the seek target frame, and another to find its
+ // keyframe. It's usually faster to just download this extra data, rather
+ // tham perform two bisections to find the seek target's keyframe. We
+ // don't do this offsetting when seeking in a buffered range,
+ // as the extra decoding causes a noticeable speed hit when all the data
+ // is buffered (compared to just doing a bisection to exactly find the
+ // keyframe).
+ int64_t keyframeOffsetMs = 0;
+ if (aType == TrackInfo::kVideoTrack && mTheoraState) {
+ keyframeOffsetMs = mTheoraState->MaxKeyframeOffset();
+ }
+ // Add in the Opus pre-roll if necessary, as well.
+ if (aType == TrackInfo::kAudioTrack && mOpusState) {
+ keyframeOffsetMs =
+ std::max(keyframeOffsetMs, OGG_SEEK_OPUS_PREROLL.ToMilliseconds());
+ }
+ int64_t seekTarget = std::max(aStartTime, aTarget - keyframeOffsetMs);
+ // Minimize the bisection search space using the known timestamps from the
+ // buffered ranges.
+ SeekRange k =
+ SelectSeekRange(aType, aRanges, seekTarget, aStartTime, aEndTime, false);
+ return SeekBisection(aType, seekTarget, k, OGG_SEEK_FUZZ_USECS);
+}
+
+nsresult OggDemuxer::SeekBisection(TrackInfo::TrackType aType, int64_t aTarget,
+ const SeekRange& aRange, uint32_t aFuzz) {
+ nsresult res;
+
+ if (aTarget <= aRange.mTimeStart) {
+ if (NS_FAILED(Reset(aType))) {
+ return NS_ERROR_FAILURE;
+ }
+ res = Resource(aType)->Seek(nsISeekableStream::NS_SEEK_SET, 0);
+ NS_ENSURE_SUCCESS(res, res);
+ return NS_OK;
+ }
+
+ // Bisection search, find start offset of last page with end time less than
+ // the seek target.
+ ogg_int64_t startOffset = aRange.mOffsetStart;
+ ogg_int64_t startTime = aRange.mTimeStart;
+ ogg_int64_t startLength = 0; // Length of the page at startOffset.
+ ogg_int64_t endOffset = aRange.mOffsetEnd;
+ ogg_int64_t endTime = aRange.mTimeEnd;
+
+ ogg_int64_t seekTarget = aTarget;
+ int64_t seekLowerBound = std::max(static_cast<int64_t>(0), aTarget - aFuzz);
+ int hops = 0;
+ DebugOnly<ogg_int64_t> previousGuess = -1;
+ int backsteps = 0;
+ const int maxBackStep = 10;
+ MOZ_ASSERT(
+ static_cast<uint64_t>(PAGE_STEP) * pow(2.0, maxBackStep) < INT32_MAX,
+ "Backstep calculation must not overflow");
+
+ // Seek via bisection search. Loop until we find the offset where the page
+ // before the offset is before the seek target, and the page after the offset
+ // is after the seek target.
+ tainted_ogg<ogg_page*> page = mSandbox->malloc_in_sandbox<ogg_page>();
+ if (!page) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ auto clean_page = MakeScopeExit([&] { mSandbox->free_in_sandbox(page); });
+ while (true) {
+ ogg_int64_t duration = 0;
+ double target = 0;
+ ogg_int64_t interval = 0;
+ ogg_int64_t guess = 0;
+ int skippedBytes = 0;
+ ogg_int64_t pageOffset = 0;
+ ogg_int64_t pageLength = 0;
+ ogg_int64_t granuleTime = -1;
+ bool mustBackoff = false;
+
+ // Guess where we should bisect to, based on the bit rate and the time
+ // remaining in the interval. Loop until we can determine the time at
+ // the guess offset.
+ while (true) {
+ // Discard any previously buffered packets/pages.
+ if (NS_FAILED(Reset(aType))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ interval = endOffset - startOffset - startLength;
+ if (interval == 0) {
+ // Our interval is empty, we've found the optimal seek point, as the
+ // page at the start offset is before the seek target, and the page
+ // at the end offset is after the seek target.
+ SEEK_LOG(LogLevel::Debug,
+ ("Interval narrowed, terminating bisection."));
+ break;
+ }
+
+ // Guess bisection point.
+ duration = endTime - startTime;
+ target = (double)(seekTarget - startTime) / (double)duration;
+ guess = startOffset + startLength +
+ static_cast<ogg_int64_t>((double)interval * target);
+ guess = std::min(guess, endOffset - PAGE_STEP);
+ if (mustBackoff) {
+ // We previously failed to determine the time at the guess offset,
+ // probably because we ran out of data to decode. This usually happens
+ // when we guess very close to the end offset. So reduce the guess
+ // offset using an exponential backoff until we determine the time.
+ SEEK_LOG(
+ LogLevel::Debug,
+ ("Backing off %d bytes, backsteps=%d",
+ static_cast<int32_t>(PAGE_STEP * pow(2.0, backsteps)), backsteps));
+ guess -= PAGE_STEP * static_cast<ogg_int64_t>(pow(2.0, backsteps));
+
+ if (guess <= startOffset) {
+ // We've tried to backoff to before the start offset of our seek
+ // range. This means we couldn't find a seek termination position
+ // near the end of the seek range, so just set the seek termination
+ // condition, and break out of the bisection loop. We'll begin
+ // decoding from the start of the seek range.
+ interval = 0;
+ break;
+ }
+
+ backsteps = std::min(backsteps + 1, maxBackStep);
+ // We reset mustBackoff. If we still need to backoff further, it will
+ // be set to true again.
+ mustBackoff = false;
+ } else {
+ backsteps = 0;
+ }
+ guess = std::max(guess, startOffset + startLength);
+
+ SEEK_LOG(LogLevel::Debug,
+ ("Seek loop start[o=%lld..%lld t=%lld] "
+ "end[o=%lld t=%lld] "
+ "interval=%lld target=%lf guess=%lld",
+ startOffset, (startOffset + startLength), startTime, endOffset,
+ endTime, interval, target, guess));
+
+ MOZ_ASSERT(guess >= startOffset + startLength,
+ "Guess must be after range start");
+ MOZ_ASSERT(guess < endOffset, "Guess must be before range end");
+ MOZ_ASSERT(guess != previousGuess,
+ "Guess should be different to previous");
+ previousGuess = guess;
+
+ hops++;
+
+ // Locate the next page after our seek guess, and then figure out the
+ // granule time of the audio and video bitstreams there. We can then
+ // make a bisection decision based on our location in the media.
+ PageSyncResult pageSyncResult =
+ PageSync(mSandbox.get(), Resource(aType), OggSyncState(aType), false,
+ guess, endOffset, page, skippedBytes);
+ NS_ENSURE_TRUE(pageSyncResult != PAGE_SYNC_ERROR, NS_ERROR_FAILURE);
+
+ if (pageSyncResult == PAGE_SYNC_END_OF_RANGE) {
+ // Our guess was too close to the end, we've ended up reading the end
+ // page. Backoff exponentially from the end point, in case the last
+ // page/frame/sample is huge.
+ mustBackoff = true;
+ SEEK_LOG(LogLevel::Debug, ("Hit the end of range, backing off"));
+ continue;
+ }
+
+ // We've located a page of length |ret| at |guess + skippedBytes|.
+ // Remember where the page is located.
+ pageOffset = guess + skippedBytes;
+
+ bool failedPageLenVerify = false;
+ // Page length should be under 64Kb according to
+ // https://xiph.org/ogg/doc/libogg/ogg_page.html
+ pageLength = CopyAndVerifyOrFail(page->header_len + page->body_len,
+ val <= 64 * 1024, &failedPageLenVerify);
+ if (failedPageLenVerify) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Read pages until we can determine the granule time of the audio and
+ // video bitstream.
+ ogg_int64_t audioTime = -1;
+ ogg_int64_t videoTime = -1;
+ do {
+ // Add the page to its codec state, determine its granule time.
+ uint32_t serial = static_cast<uint32_t>(
+ sandbox_invoke(*mSandbox, ogg_page_serialno, page)
+ .unverified_safe_because(RLBOX_OGG_PAGE_SERIAL_REASON));
+ OggCodecState* codecState = mCodecStore.Get(serial);
+ if (codecState && GetCodecStateType(codecState) == aType) {
+ if (codecState->mActive) {
+ int ret =
+ sandbox_invoke(*mSandbox, ogg_stream_pagein, codecState->mState,
+ page)
+ .unverified_safe_because(RLBOX_OGG_STATE_ASSERT_REASON);
+ NS_ENSURE_TRUE(ret == 0, NS_ERROR_FAILURE);
+ }
+
+ ogg_int64_t granulepos =
+ sandbox_invoke(*mSandbox, ogg_page_granulepos, page)
+ .unverified_safe_because(
+ "If this is incorrect it may lead to incorrect seeking "
+ "behavior in the stream, however will not affect the "
+ "memory safety of the Firefox renderer.");
+
+ if (aType == TrackInfo::kAudioTrack && granulepos > 0 &&
+ audioTime == -1) {
+ if (mVorbisState && serial == mVorbisState->mSerial) {
+ audioTime = mVorbisState->Time(granulepos);
+ } else if (mOpusState && serial == mOpusState->mSerial) {
+ audioTime = mOpusState->Time(granulepos);
+ } else if (mFlacState && serial == mFlacState->mSerial) {
+ audioTime = mFlacState->Time(granulepos);
+ }
+ }
+
+ if (aType == TrackInfo::kVideoTrack && granulepos > 0 &&
+ serial == mTheoraState->mSerial && videoTime == -1) {
+ videoTime = mTheoraState->Time(granulepos);
+ }
+
+ if (pageOffset + pageLength >= endOffset) {
+ // Hit end of readable data.
+ break;
+ }
+ }
+ if (!ReadOggPage(aType, page.to_opaque())) {
+ break;
+ }
+
+ } while ((aType == TrackInfo::kAudioTrack && audioTime == -1) ||
+ (aType == TrackInfo::kVideoTrack && videoTime == -1));
+
+ if ((aType == TrackInfo::kAudioTrack && audioTime == -1) ||
+ (aType == TrackInfo::kVideoTrack && videoTime == -1)) {
+ // We don't have timestamps for all active tracks...
+ if (pageOffset == startOffset + startLength &&
+ pageOffset + pageLength >= endOffset) {
+ // We read the entire interval without finding timestamps for all
+ // active tracks. We know the interval start offset is before the seek
+ // target, and the interval end is after the seek target, and we can't
+ // terminate inside the interval, so we terminate the seek at the
+ // start of the interval.
+ interval = 0;
+ break;
+ }
+
+ // We should backoff; cause the guess to back off from the end, so
+ // that we've got more room to capture.
+ mustBackoff = true;
+ continue;
+ }
+
+ // We've found appropriate time stamps here. Proceed to bisect
+ // the search space.
+ granuleTime = aType == TrackInfo::kAudioTrack ? audioTime : videoTime;
+ MOZ_ASSERT(granuleTime > 0, "Must get a granuletime");
+ break;
+ } // End of "until we determine time at guess offset" loop.
+
+ if (interval == 0) {
+ // Seek termination condition; we've found the page boundary of the
+ // last page before the target, and the first page after the target.
+ SEEK_LOG(LogLevel::Debug,
+ ("Terminating seek at offset=%lld", startOffset));
+ MOZ_ASSERT(startTime < aTarget,
+ "Start time must always be less than target");
+ res = Resource(aType)->Seek(nsISeekableStream::NS_SEEK_SET, startOffset);
+ NS_ENSURE_SUCCESS(res, res);
+ if (NS_FAILED(Reset(aType))) {
+ return NS_ERROR_FAILURE;
+ }
+ break;
+ }
+
+ SEEK_LOG(LogLevel::Debug,
+ ("Time at offset %lld is %lld", guess, granuleTime));
+ if (granuleTime < seekTarget && granuleTime > seekLowerBound) {
+ // We're within the fuzzy region in which we want to terminate the search.
+ res = Resource(aType)->Seek(nsISeekableStream::NS_SEEK_SET, pageOffset);
+ NS_ENSURE_SUCCESS(res, res);
+ if (NS_FAILED(Reset(aType))) {
+ return NS_ERROR_FAILURE;
+ }
+ SEEK_LOG(LogLevel::Debug,
+ ("Terminating seek at offset=%lld", pageOffset));
+ break;
+ }
+
+ if (granuleTime >= seekTarget) {
+ // We've landed after the seek target.
+ MOZ_ASSERT(pageOffset < endOffset, "offset_end must decrease");
+ endOffset = pageOffset;
+ endTime = granuleTime;
+ } else if (granuleTime < seekTarget) {
+ // Landed before seek target.
+ MOZ_ASSERT(pageOffset >= startOffset + startLength,
+ "Bisection point should be at or after end of first page in "
+ "interval");
+ startOffset = pageOffset;
+ startLength = pageLength;
+ startTime = granuleTime;
+ }
+ MOZ_ASSERT(startTime <= seekTarget, "Must be before seek target");
+ MOZ_ASSERT(endTime >= seekTarget, "End must be after seek target");
+ }
+
+ (void)hops;
+ SEEK_LOG(LogLevel::Debug, ("Seek complete in %d bisections.", hops));
+
+ return NS_OK;
+}
+
+#undef OGG_DEBUG
+#undef SEEK_LOG
+#undef CopyAndVerifyOrFail
+} // namespace mozilla
diff --git a/dom/media/ogg/OggDemuxer.h b/dom/media/ogg/OggDemuxer.h
new file mode 100644
index 0000000000..8a65398cf9
--- /dev/null
+++ b/dom/media/ogg/OggDemuxer.h
@@ -0,0 +1,363 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#if !defined(OggDemuxer_h_)
+# define OggDemuxer_h_
+
+# include "nsTArray.h"
+# include "MediaDataDemuxer.h"
+# include "OggCodecState.h"
+# include "OggCodecStore.h"
+# include "OggRLBoxTypes.h"
+# include "MediaMetadataManager.h"
+
+# include <memory>
+
+namespace mozilla {
+
+class OggTrackDemuxer;
+
+DDLoggedTypeDeclNameAndBase(OggDemuxer, MediaDataDemuxer);
+DDLoggedTypeNameAndBase(OggTrackDemuxer, MediaTrackDemuxer);
+
+class OggDemuxer : public MediaDataDemuxer,
+ public DecoderDoctorLifeLogger<OggDemuxer> {
+ public:
+ explicit OggDemuxer(MediaResource* aResource);
+
+ RefPtr<InitPromise> Init() override;
+
+ uint32_t GetNumberTracks(TrackInfo::TrackType aType) const override;
+
+ already_AddRefed<MediaTrackDemuxer> GetTrackDemuxer(
+ TrackInfo::TrackType aType, uint32_t aTrackNumber) override;
+
+ bool IsSeekable() const override;
+
+ UniquePtr<EncryptionInfo> GetCrypto() override;
+
+ // Set the events to notify when chaining is encountered.
+ void SetChainingEvents(TimedMetadataEventProducer* aMetadataEvent,
+ MediaEventProducer<void>* aOnSeekableEvent);
+
+ private:
+ // helpers for friend OggTrackDemuxer
+ UniquePtr<TrackInfo> GetTrackInfo(TrackInfo::TrackType aType,
+ size_t aTrackNumber) const;
+
+ struct nsAutoOggSyncState {
+ explicit nsAutoOggSyncState(rlbox_sandbox_ogg* aSandbox);
+ ~nsAutoOggSyncState();
+ rlbox_sandbox_ogg* mSandbox;
+ tainted_opaque_ogg<ogg_sync_state*> mState;
+ };
+ media::TimeIntervals GetBuffered(TrackInfo::TrackType aType);
+ void FindStartTime(int64_t& aOutStartTime);
+ void FindStartTime(TrackInfo::TrackType, int64_t& aOutStartTime);
+
+ nsresult SeekInternal(TrackInfo::TrackType aType,
+ const media::TimeUnit& aTarget);
+
+ // Seeks to the keyframe preceding the target time using available
+ // keyframe indexes.
+ enum IndexedSeekResult {
+ SEEK_OK, // Success.
+ SEEK_INDEX_FAIL, // Failure due to no index, or invalid index.
+ SEEK_FATAL_ERROR // Error returned by a stream operation.
+ };
+ IndexedSeekResult SeekToKeyframeUsingIndex(TrackInfo::TrackType aType,
+ int64_t aTarget);
+
+ // Rolls back a seek-using-index attempt, returning a failure error code.
+ IndexedSeekResult RollbackIndexedSeek(TrackInfo::TrackType aType,
+ int64_t aOffset);
+
+ // Represents a section of contiguous media, with a start and end offset,
+ // and the timestamps of the start and end of that range, that is cached.
+ // Used to denote the extremities of a range in which we can seek quickly
+ // (because it's cached).
+ class SeekRange {
+ public:
+ SeekRange() : mOffsetStart(0), mOffsetEnd(0), mTimeStart(0), mTimeEnd(0) {}
+
+ SeekRange(int64_t aOffsetStart, int64_t aOffsetEnd, int64_t aTimeStart,
+ int64_t aTimeEnd)
+ : mOffsetStart(aOffsetStart),
+ mOffsetEnd(aOffsetEnd),
+ mTimeStart(aTimeStart),
+ mTimeEnd(aTimeEnd) {}
+
+ bool IsNull() const {
+ return mOffsetStart == 0 && mOffsetEnd == 0 && mTimeStart == 0 &&
+ mTimeEnd == 0;
+ }
+
+ int64_t mOffsetStart, mOffsetEnd; // in bytes.
+ int64_t mTimeStart, mTimeEnd; // in usecs.
+ };
+
+ nsresult GetSeekRanges(TrackInfo::TrackType aType,
+ nsTArray<SeekRange>& aRanges);
+ SeekRange SelectSeekRange(TrackInfo::TrackType aType,
+ const nsTArray<SeekRange>& ranges, int64_t aTarget,
+ int64_t aStartTime, int64_t aEndTime, bool aExact);
+
+ // Seeks to aTarget usecs in the buffered range aRange using bisection search,
+ // or to the keyframe prior to aTarget if we have video. aAdjustedTarget is
+ // an adjusted version of the target used to account for Opus pre-roll, if
+ // necessary. aStartTime must be the presentation time at the start of media,
+ // and aEndTime the time at end of media. aRanges must be the time/byte ranges
+ // buffered in the media cache as per GetSeekRanges().
+ nsresult SeekInBufferedRange(TrackInfo::TrackType aType, int64_t aTarget,
+ int64_t aAdjustedTarget, int64_t aStartTime,
+ int64_t aEndTime,
+ const nsTArray<SeekRange>& aRanges,
+ const SeekRange& aRange);
+
+ // Seeks to before aTarget usecs in media using bisection search. If the media
+ // has video, this will seek to before the keyframe required to render the
+ // media at aTarget. Will use aRanges in order to narrow the bisection
+ // search space. aStartTime must be the presentation time at the start of
+ // media, and aEndTime the time at end of media. aRanges must be the time/byte
+ // ranges buffered in the media cache as per GetSeekRanges().
+ nsresult SeekInUnbuffered(TrackInfo::TrackType aType, int64_t aTarget,
+ int64_t aStartTime, int64_t aEndTime,
+ const nsTArray<SeekRange>& aRanges);
+
+ // Performs a seek bisection to move the media stream's read cursor to the
+ // last ogg page boundary which has end time before aTarget usecs on both the
+ // Theora and Vorbis bitstreams. Limits its search to data inside aRange;
+ // i.e. it will only read inside of the aRange's start and end offsets.
+ // aFuzz is the number of usecs of leniency we'll allow; we'll terminate the
+ // seek when we land in the range (aTime - aFuzz, aTime) usecs.
+ nsresult SeekBisection(TrackInfo::TrackType aType, int64_t aTarget,
+ const SeekRange& aRange, uint32_t aFuzz);
+
+ // Chunk size to read when reading Ogg files. Average Ogg page length
+ // is about 4300 bytes, so we read the file in chunks larger than that.
+ static const int PAGE_STEP = 8192;
+
+ enum PageSyncResult {
+ PAGE_SYNC_ERROR = 1,
+ PAGE_SYNC_END_OF_RANGE = 2,
+ PAGE_SYNC_OK = 3
+ };
+ static PageSyncResult PageSync(rlbox_sandbox_ogg* aSandbox,
+ MediaResourceIndex* aResource,
+ tainted_opaque_ogg<ogg_sync_state*> aState,
+ bool aCachedDataOnly, int64_t aOffset,
+ int64_t aEndOffset,
+ tainted_ogg<ogg_page*> aPage,
+ int& aSkippedBytes);
+
+ // Demux next Ogg packet
+ ogg_packet* GetNextPacket(TrackInfo::TrackType aType);
+
+ nsresult Reset(TrackInfo::TrackType aType);
+
+ static const nsString GetKind(const nsCString& aRole);
+ static void InitTrack(MessageField* aMsgInfo, TrackInfo* aInfo, bool aEnable);
+
+ // Really private!
+ ~OggDemuxer();
+
+ // Read enough of the file to identify track information and header
+ // packets necessary for decoding to begin.
+ nsresult ReadMetadata();
+
+ // Read a page of data from the Ogg file. Returns true if a page has been
+ // read, false if the page read failed or end of file reached.
+ bool ReadOggPage(TrackInfo::TrackType aType,
+ tainted_opaque_ogg<ogg_page*> aPage);
+
+ // Send a page off to the individual streams it belongs to.
+ // Reconstructed packets, if any are ready, will be available
+ // on the individual OggCodecStates.
+ nsresult DemuxOggPage(TrackInfo::TrackType aType,
+ tainted_opaque_ogg<ogg_page*> aPage);
+
+ // Read data and demux until a packet is available on the given stream state
+ void DemuxUntilPacketAvailable(TrackInfo::TrackType aType,
+ OggCodecState* aState);
+
+ // Reads and decodes header packets for aState, until either header decode
+ // fails, or is complete. Initializes the codec state before returning.
+ // Returns true if reading headers and initializtion of the stream
+ // succeeds.
+ bool ReadHeaders(TrackInfo::TrackType aType, OggCodecState* aState);
+
+ // Reads the next link in the chain.
+ bool ReadOggChain(const media::TimeUnit& aLastEndTime);
+
+ // Set this media as being a chain and notifies the state machine that the
+ // media is no longer seekable.
+ void SetChained();
+
+ // Fills aTracks with the serial numbers of each active stream, for use by
+ // various SkeletonState functions.
+ void BuildSerialList(nsTArray<uint32_t>& aTracks);
+
+ // Setup target bitstreams for decoding.
+ void SetupTarget(OggCodecState** aSavedState, OggCodecState* aNewState);
+ void SetupTargetSkeleton();
+ void SetupMediaTracksInfo(const nsTArray<uint32_t>& aSerials);
+ void FillTags(TrackInfo* aInfo, UniquePtr<MetadataTags>&& aTags);
+
+ // Compute an ogg page's checksum
+ tainted_opaque_ogg<ogg_uint32_t> GetPageChecksum(
+ tainted_opaque_ogg<ogg_page*> aPage);
+
+ // Get the end time of aEndOffset. This is the playback position we'd reach
+ // after playback finished at aEndOffset.
+ int64_t RangeEndTime(TrackInfo::TrackType aType, int64_t aEndOffset);
+
+ // Get the end time of aEndOffset, without reading before aStartOffset.
+ // This is the playback position we'd reach after playback finished at
+ // aEndOffset. If bool aCachedDataOnly is true, then we'll only read
+ // from data which is cached in the media cached, otherwise we'll do
+ // regular blocking reads from the media stream. If bool aCachedDataOnly
+ // is true, this can safely be called on the main thread, otherwise it
+ // must be called on the state machine thread.
+ int64_t RangeEndTime(TrackInfo::TrackType aType, int64_t aStartOffset,
+ int64_t aEndOffset, bool aCachedDataOnly);
+
+ // Get the start time of the range beginning at aOffset. This is the start
+ // time of the first aType sample we'd be able to play if we
+ // started playback at aOffset.
+ int64_t RangeStartTime(TrackInfo::TrackType aType, int64_t aOffset);
+
+ // All invocations of libogg functionality from the demuxer is sandboxed using
+ // wasm library sandboxes on supported platforms. These functions that create
+ // and destroy the sandbox instance.
+ static rlbox_sandbox_ogg* CreateSandbox();
+ struct SandboxDestroy {
+ void operator()(rlbox_sandbox_ogg* sandbox);
+ };
+
+ // The sandbox instance used to sandbox libogg functionality in the demuxer.
+ // This must be declared before other members so that constructors/destructors
+ // run in the right order.
+ std::unique_ptr<rlbox_sandbox_ogg, SandboxDestroy> mSandbox;
+
+ MediaInfo mInfo;
+ nsTArray<RefPtr<OggTrackDemuxer>> mDemuxers;
+
+ // Map of codec-specific bitstream states.
+ OggCodecStore mCodecStore;
+
+ // Decode state of the Theora bitstream we're decoding, if we have video.
+ OggCodecState* mTheoraState;
+
+ // Decode state of the Vorbis bitstream we're decoding, if we have audio.
+ OggCodecState* mVorbisState;
+
+ // Decode state of the Opus bitstream we're decoding, if we have one.
+ OggCodecState* mOpusState;
+
+ // Get the bitstream decode state for the given track type
+ // Decode state of the Flac bitstream we're decoding, if we have one.
+ OggCodecState* mFlacState;
+
+ OggCodecState* GetTrackCodecState(TrackInfo::TrackType aType) const;
+ TrackInfo::TrackType GetCodecStateType(OggCodecState* aState) const;
+
+ // Represents the user pref media.opus.enabled at the time our
+ // contructor was called. We can't check it dynamically because
+ // we're not on the main thread;
+ bool mOpusEnabled;
+
+ // Decode state of the Skeleton bitstream.
+ SkeletonState* mSkeletonState;
+
+ // Ogg decoding state.
+ struct OggStateContext {
+ explicit OggStateContext(MediaResource* aResource,
+ rlbox_sandbox_ogg* aSandbox)
+ : mOggState(aSandbox), mResource(aResource), mNeedKeyframe(true) {}
+ nsAutoOggSyncState mOggState;
+ MediaResourceIndex mResource;
+ Maybe<media::TimeUnit> mStartTime;
+ bool mNeedKeyframe;
+ };
+
+ OggStateContext& OggState(TrackInfo::TrackType aType);
+ tainted_opaque_ogg<ogg_sync_state*> OggSyncState(TrackInfo::TrackType aType);
+ MediaResourceIndex* Resource(TrackInfo::TrackType aType);
+ MediaResourceIndex* CommonResource();
+ OggStateContext mAudioOggState;
+ OggStateContext mVideoOggState;
+
+ Maybe<int64_t> mStartTime;
+
+ // Booleans to indicate if we have audio and/or video data
+ bool HasVideo() const;
+ bool HasAudio() const;
+ bool HasSkeleton() const {
+ return mSkeletonState != 0 && mSkeletonState->mActive;
+ }
+ bool HaveStartTime() const;
+ bool HaveStartTime(TrackInfo::TrackType aType);
+ int64_t StartTime() const;
+ int64_t StartTime(TrackInfo::TrackType aType);
+
+ // The picture region inside Theora frame to be displayed, if we have
+ // a Theora video track.
+ gfx::IntRect mPicture;
+
+ // True if we are decoding a chained ogg.
+ bool mIsChained;
+
+ // Total audio duration played so far.
+ media::TimeUnit mDecodedAudioDuration;
+
+ // Events manager
+ TimedMetadataEventProducer* mTimedMetadataEvent;
+ MediaEventProducer<void>* mOnSeekableEvent;
+
+ // This will be populated only if a content change occurs, otherwise it
+ // will be left as null so the original metadata is used.
+ // It is updated once a chained ogg is encountered.
+ // As Ogg chaining is only supported for audio, we only need an audio track
+ // info.
+ RefPtr<TrackInfoSharedPtr> mSharedAudioTrackInfo;
+
+ friend class OggTrackDemuxer;
+};
+
+class OggTrackDemuxer : public MediaTrackDemuxer,
+ public DecoderDoctorLifeLogger<OggTrackDemuxer> {
+ public:
+ OggTrackDemuxer(OggDemuxer* aParent, TrackInfo::TrackType aType,
+ uint32_t aTrackNumber);
+
+ UniquePtr<TrackInfo> GetInfo() const override;
+
+ RefPtr<SeekPromise> Seek(const media::TimeUnit& aTime) override;
+
+ RefPtr<SamplesPromise> GetSamples(int32_t aNumSamples = 1) override;
+
+ void Reset() override;
+
+ RefPtr<SkipAccessPointPromise> SkipToNextRandomAccessPoint(
+ const media::TimeUnit& aTimeThreshold) override;
+
+ media::TimeIntervals GetBuffered() override;
+
+ void BreakCycles() override;
+
+ private:
+ ~OggTrackDemuxer();
+ void SetNextKeyFrameTime();
+ RefPtr<MediaRawData> NextSample();
+ RefPtr<OggDemuxer> mParent;
+ TrackInfo::TrackType mType;
+ UniquePtr<TrackInfo> mInfo;
+
+ // Queued sample extracted by the demuxer, but not yet returned.
+ RefPtr<MediaRawData> mQueuedSample;
+};
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/ogg/OggRLBox.h b/dom/media/ogg/OggRLBox.h
new file mode 100644
index 0000000000..0a451fa077
--- /dev/null
+++ b/dom/media/ogg/OggRLBox.h
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef OGG_RLBOX
+#define OGG_RLBOX
+
+#include "OggRLBoxTypes.h"
+
+// Load general firefox configuration of RLBox
+#include "mozilla/rlbox/rlbox_config.h"
+
+#ifdef MOZ_WASM_SANDBOXING_OGG
+// Include the generated header file so that we are able to resolve the symbols
+// in the wasm binary
+# include "rlbox.wasm.h"
+# define RLBOX_USE_STATIC_CALLS() rlbox_wasm2c_sandbox_lookup_symbol
+# include "mozilla/rlbox/rlbox_wasm2c_sandbox.hpp"
+#else
+# define RLBOX_USE_STATIC_CALLS() rlbox_noop_sandbox_lookup_symbol
+# include "mozilla/rlbox/rlbox_noop_sandbox.hpp"
+#endif
+
+#include "mozilla/rlbox/rlbox.hpp"
+
+#include "ogg/OggStructsForRLBox.h"
+rlbox_load_structs_from_library(ogg);
+
+#endif
diff --git a/dom/media/ogg/OggRLBoxTypes.h b/dom/media/ogg/OggRLBoxTypes.h
new file mode 100644
index 0000000000..d2dfdd1dff
--- /dev/null
+++ b/dom/media/ogg/OggRLBoxTypes.h
@@ -0,0 +1,17 @@
+/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef OGG_RLBOX_TYPES
+#define OGG_RLBOX_TYPES
+
+#include "mozilla/rlbox/rlbox_types.hpp"
+
+#ifdef MOZ_WASM_SANDBOXING_OGG
+RLBOX_DEFINE_BASE_TYPES_FOR(ogg, wasm2c)
+#else
+RLBOX_DEFINE_BASE_TYPES_FOR(ogg, noop)
+#endif
+
+#endif
diff --git a/dom/media/ogg/OggWriter.cpp b/dom/media/ogg/OggWriter.cpp
new file mode 100644
index 0000000000..6f29d44124
--- /dev/null
+++ b/dom/media/ogg/OggWriter.cpp
@@ -0,0 +1,197 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "OggWriter.h"
+#include "prtime.h"
+#include "mozilla/ProfilerLabels.h"
+
+#define LOG(args, ...)
+
+namespace mozilla {
+
+OggWriter::OggWriter()
+ : ContainerWriter(), mOggStreamState(), mOggPage(), mPacket() {
+ if (NS_FAILED(Init())) {
+ LOG("ERROR! Fail to initialize the OggWriter.");
+ }
+}
+
+OggWriter::~OggWriter() {
+ if (mInitialized) {
+ ogg_stream_clear(&mOggStreamState);
+ }
+ // mPacket's data was always owned by us, no need to ogg_packet_clear.
+}
+
+nsresult OggWriter::Init() {
+ MOZ_ASSERT(!mInitialized);
+
+ // The serial number (serialno) should be a random number, for the current
+ // implementation where the output file contains only a single stream, this
+ // serialno is used to differentiate between files.
+ srand(static_cast<unsigned>(PR_Now()));
+ int rc = ogg_stream_init(&mOggStreamState, rand());
+
+ mPacket.b_o_s = 1;
+ mPacket.e_o_s = 0;
+ mPacket.granulepos = 0;
+ mPacket.packet = nullptr;
+ mPacket.packetno = 0;
+ mPacket.bytes = 0;
+
+ mInitialized = (rc == 0);
+
+ return (rc == 0) ? NS_OK : NS_ERROR_NOT_INITIALIZED;
+}
+
+nsresult OggWriter::WriteEncodedTrack(
+ const nsTArray<RefPtr<EncodedFrame>>& aData, uint32_t aFlags) {
+ AUTO_PROFILER_LABEL("OggWriter::WriteEncodedTrack", OTHER);
+
+ uint32_t len = aData.Length();
+ for (uint32_t i = 0; i < len; i++) {
+ if (aData[i]->mFrameType != EncodedFrame::OPUS_AUDIO_FRAME) {
+ LOG("[OggWriter] wrong encoded data type!");
+ return NS_ERROR_FAILURE;
+ }
+
+ // only pass END_OF_STREAM on the last frame!
+ nsresult rv = WriteEncodedData(
+ *aData[i]->mFrameData, aData[i]->mDuration,
+ i < len - 1 ? (aFlags & ~ContainerWriter::END_OF_STREAM) : aFlags);
+ if (NS_FAILED(rv)) {
+ LOG("%p Failed to WriteEncodedTrack!", this);
+ return rv;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult OggWriter::WriteEncodedData(const nsTArray<uint8_t>& aBuffer,
+ int aDuration, uint32_t aFlags) {
+ if (!mInitialized) {
+ LOG("[OggWriter] OggWriter has not initialized!");
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_ASSERT(!ogg_stream_eos(&mOggStreamState),
+ "No data can be written after eos has marked.");
+
+ // Set eos flag to true, and once the eos is written to a packet, there must
+ // not be anymore pages after a page has marked as eos.
+ if (aFlags & ContainerWriter::END_OF_STREAM) {
+ LOG("[OggWriter] Set e_o_s flag to true.");
+ mPacket.e_o_s = 1;
+ }
+
+ mPacket.packet = const_cast<uint8_t*>(aBuffer.Elements());
+ mPacket.bytes = aBuffer.Length();
+ mPacket.granulepos += aDuration;
+
+ // 0 returned on success. -1 returned in the event of internal error.
+ // The data in the packet is copied into the internal storage managed by the
+ // mOggStreamState, so we are free to alter the contents of mPacket after
+ // this call has returned.
+ int rc = ogg_stream_packetin(&mOggStreamState, &mPacket);
+ if (rc < 0) {
+ LOG("[OggWriter] Failed in ogg_stream_packetin! (%d).", rc);
+ return NS_ERROR_FAILURE;
+ }
+
+ if (mPacket.b_o_s) {
+ mPacket.b_o_s = 0;
+ }
+ mPacket.packetno++;
+ mPacket.packet = nullptr;
+
+ return NS_OK;
+}
+
+void OggWriter::ProduceOggPage(nsTArray<nsTArray<uint8_t>>* aOutputBufs) {
+ aOutputBufs->AppendElement();
+ aOutputBufs->LastElement().SetLength(mOggPage.header_len + mOggPage.body_len);
+ memcpy(aOutputBufs->LastElement().Elements(), mOggPage.header,
+ mOggPage.header_len);
+ memcpy(aOutputBufs->LastElement().Elements() + mOggPage.header_len,
+ mOggPage.body, mOggPage.body_len);
+}
+
+nsresult OggWriter::GetContainerData(nsTArray<nsTArray<uint8_t>>* aOutputBufs,
+ uint32_t aFlags) {
+ int rc = -1;
+ AUTO_PROFILER_LABEL("OggWriter::GetContainerData", OTHER);
+ // Generate the oggOpus Header
+ if (aFlags & ContainerWriter::GET_HEADER) {
+ OpusMetadata* meta = static_cast<OpusMetadata*>(mMetadata.get());
+ NS_ASSERTION(meta, "should have meta data");
+ NS_ASSERTION(meta->GetKind() == TrackMetadataBase::METADATA_OPUS,
+ "should have Opus meta data");
+
+ nsresult rv = WriteEncodedData(meta->mIdHeader, 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rc = ogg_stream_flush(&mOggStreamState, &mOggPage);
+ NS_ENSURE_TRUE(rc > 0, NS_ERROR_FAILURE);
+ ProduceOggPage(aOutputBufs);
+
+ rv = WriteEncodedData(meta->mCommentHeader, 0);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rc = ogg_stream_flush(&mOggStreamState, &mOggPage);
+ NS_ENSURE_TRUE(rc > 0, NS_ERROR_FAILURE);
+
+ // Force generate a page even if the amount of packet data is not enough.
+ // Usually do so after a header packet.
+
+ ProduceOggPage(aOutputBufs);
+ }
+
+ // return value 0 means insufficient data has accumulated to fill a page, or
+ // an internal error has occurred.
+ while (ogg_stream_pageout(&mOggStreamState, &mOggPage) != 0) {
+ ProduceOggPage(aOutputBufs);
+ }
+
+ if (aFlags & ContainerWriter::FLUSH_NEEDED) {
+ // return value 0 means no packet to put into a page, or an internal error.
+ if (ogg_stream_flush(&mOggStreamState, &mOggPage) != 0) {
+ ProduceOggPage(aOutputBufs);
+ }
+ mIsWritingComplete = true;
+ }
+
+ // We always return NS_OK here since it's OK to call this without having
+ // enough data to fill a page. It's the more common case compared to internal
+ // errors, and we cannot distinguish the two.
+ return NS_OK;
+}
+
+nsresult OggWriter::SetMetadata(
+ const nsTArray<RefPtr<TrackMetadataBase>>& aMetadata) {
+ MOZ_ASSERT(aMetadata.Length() == 1);
+ MOZ_ASSERT(aMetadata[0]);
+
+ AUTO_PROFILER_LABEL("OggWriter::SetMetadata", OTHER);
+
+ if (aMetadata[0]->GetKind() != TrackMetadataBase::METADATA_OPUS) {
+ LOG("wrong meta data type!");
+ return NS_ERROR_FAILURE;
+ }
+ // Validate each field of METADATA
+ mMetadata = static_cast<OpusMetadata*>(aMetadata[0].get());
+ if (mMetadata->mIdHeader.Length() == 0) {
+ LOG("miss mIdHeader!");
+ return NS_ERROR_FAILURE;
+ }
+ if (mMetadata->mCommentHeader.Length() == 0) {
+ LOG("miss mCommentHeader!");
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla
+
+#undef LOG
diff --git a/dom/media/ogg/OggWriter.h b/dom/media/ogg/OggWriter.h
new file mode 100644
index 0000000000..73d5bd87e9
--- /dev/null
+++ b/dom/media/ogg/OggWriter.h
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef OggWriter_h_
+#define OggWriter_h_
+
+#include "ContainerWriter.h"
+#include "OpusTrackEncoder.h"
+#include <ogg/ogg.h>
+
+namespace mozilla {
+/**
+ * WriteEncodedTrack inserts raw packets into Ogg stream (ogg_stream_state), and
+ * GetContainerData outputs an ogg_page when enough packets have been written
+ * to the Ogg stream.
+ * For more details, please reference:
+ * http://www.xiph.org/ogg/doc/libogg/encoding.html
+ */
+class OggWriter : public ContainerWriter {
+ public:
+ OggWriter();
+ ~OggWriter();
+
+ // Write frames into the ogg container. aFlags should be set to END_OF_STREAM
+ // for the final set of frames.
+ nsresult WriteEncodedTrack(const nsTArray<RefPtr<EncodedFrame>>& aData,
+ uint32_t aFlags = 0) override;
+
+ nsresult GetContainerData(nsTArray<nsTArray<uint8_t>>* aOutputBufs,
+ uint32_t aFlags = 0) override;
+
+ // Check metadata type integrity and reject unacceptable track encoder.
+ nsresult SetMetadata(
+ const nsTArray<RefPtr<TrackMetadataBase>>& aMetadata) override;
+
+ private:
+ nsresult Init();
+
+ nsresult WriteEncodedData(const nsTArray<uint8_t>& aBuffer, int aDuration,
+ uint32_t aFlags = 0);
+
+ void ProduceOggPage(nsTArray<nsTArray<uint8_t>>* aOutputBufs);
+ // Store the Medatata from track encoder
+ RefPtr<OpusMetadata> mMetadata;
+
+ ogg_stream_state mOggStreamState;
+ ogg_page mOggPage;
+ ogg_packet mPacket;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/ogg/OpusParser.cpp b/dom/media/ogg/OpusParser.cpp
new file mode 100644
index 0000000000..918888ea8c
--- /dev/null
+++ b/dom/media/ogg/OpusParser.cpp
@@ -0,0 +1,217 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <algorithm>
+#include "mozilla/EndianUtils.h"
+
+#include "OpusParser.h"
+#include "VideoUtils.h"
+
+#include "opus/opus.h"
+extern "C" {
+#include "opus/opus_multistream.h"
+}
+
+#include <cmath>
+
+namespace mozilla {
+
+extern LazyLogModule gMediaDecoderLog;
+#define OPUS_LOG(type, msg) MOZ_LOG(gMediaDecoderLog, type, msg)
+
+OpusParser::OpusParser()
+ : mRate(0),
+ mNominalRate(0),
+ mChannels(0),
+ mPreSkip(0),
+#ifdef MOZ_SAMPLE_TYPE_FLOAT32
+ mGain(1.0f),
+#else
+ mGain_Q16(65536),
+#endif
+ mChannelMapping(0),
+ mStreams(0),
+ mCoupledStreams(0),
+ mPrevPacketGranulepos(0) {
+}
+
+bool OpusParser::DecodeHeader(unsigned char* aData, size_t aLength) {
+ if (aLength < 19 || memcmp(aData, "OpusHead", 8)) {
+ OPUS_LOG(LogLevel::Debug, ("Invalid Opus file: unrecognized header"));
+ return false;
+ }
+
+ mRate = 48000; // The Opus decoder runs at 48 kHz regardless.
+
+ int version = aData[8];
+ // Accept file format versions 0.x.
+ if ((version & 0xf0) != 0) {
+ OPUS_LOG(LogLevel::Debug,
+ ("Rejecting unknown Opus file version %d", version));
+ return false;
+ }
+
+ mChannels = aData[9];
+ if (mChannels < 1) {
+ OPUS_LOG(LogLevel::Debug,
+ ("Invalid Opus file: Number of channels %d", mChannels));
+ return false;
+ }
+
+ mPreSkip = LittleEndian::readUint16(aData + 10);
+ mNominalRate = LittleEndian::readUint32(aData + 12);
+ double gain_dB = LittleEndian::readInt16(aData + 16) / 256.0;
+#ifdef MOZ_SAMPLE_TYPE_FLOAT32
+ mGain = static_cast<float>(pow(10, 0.05 * gain_dB));
+#else
+ mGain_Q16 = static_cast<int32_t>(std::min(
+ 65536 * pow(10, 0.05 * gain_dB) + 0.5, static_cast<double>(INT32_MAX)));
+#endif
+ mChannelMapping = aData[18];
+
+ if (mChannelMapping == 0) {
+ // Mapping family 0 only allows two channels
+ if (mChannels > 2) {
+ OPUS_LOG(LogLevel::Debug, ("Invalid Opus file: too many channels (%d) for"
+ " mapping family 0.",
+ mChannels));
+ return false;
+ }
+ mStreams = 1;
+ mCoupledStreams = mChannels - 1;
+ mMappingTable[0] = 0;
+ mMappingTable[1] = 1;
+ } else if (mChannelMapping == 1 || mChannelMapping == 2 ||
+ mChannelMapping == 255) {
+ // Currently only up to 8 channels are defined for mapping family 1
+ if (mChannelMapping == 1 && mChannels > 8) {
+ OPUS_LOG(LogLevel::Debug, ("Invalid Opus file: too many channels (%d) for"
+ " mapping family 1.",
+ mChannels));
+ return false;
+ }
+ if (mChannelMapping == 2) {
+ if (!IsValidMapping2ChannelsCount(mChannels)) {
+ return false;
+ }
+ }
+ if (aLength > static_cast<unsigned>(20 + mChannels)) {
+ mStreams = aData[19];
+ mCoupledStreams = aData[20];
+ int i;
+ for (i = 0; i < mChannels; i++) {
+ mMappingTable[i] = aData[21 + i];
+ }
+ } else {
+ OPUS_LOG(LogLevel::Debug, ("Invalid Opus file: channel mapping %d,"
+ " but no channel mapping table",
+ mChannelMapping));
+ return false;
+ }
+ } else {
+ OPUS_LOG(LogLevel::Debug, ("Invalid Opus file: unsupported channel mapping "
+ "family %d",
+ mChannelMapping));
+ return false;
+ }
+ if (mStreams < 1) {
+ OPUS_LOG(LogLevel::Debug, ("Invalid Opus file: no streams"));
+ return false;
+ }
+ if (mCoupledStreams > mStreams) {
+ OPUS_LOG(LogLevel::Debug,
+ ("Invalid Opus file: more coupled streams (%d) than "
+ "total streams (%d)",
+ mCoupledStreams, mStreams));
+ return false;
+ }
+
+#ifdef DEBUG
+ OPUS_LOG(LogLevel::Debug, ("Opus stream header:"));
+ OPUS_LOG(LogLevel::Debug, (" channels: %d", mChannels));
+ OPUS_LOG(LogLevel::Debug, (" preskip: %d", mPreSkip));
+ OPUS_LOG(LogLevel::Debug, (" original: %d Hz", mNominalRate));
+ OPUS_LOG(LogLevel::Debug, (" gain: %.2f dB", gain_dB));
+ OPUS_LOG(LogLevel::Debug, ("Channel Mapping:"));
+ OPUS_LOG(LogLevel::Debug, (" family: %d", mChannelMapping));
+ OPUS_LOG(LogLevel::Debug, (" streams: %d", mStreams));
+#endif
+ return true;
+}
+
+bool OpusParser::DecodeTags(unsigned char* aData, size_t aLength) {
+ if (aLength < 16 || memcmp(aData, "OpusTags", 8)) return false;
+
+ // Copy out the raw comment lines, but only do basic validation
+ // checks against the string packing: too little data, too many
+ // comments, or comments that are too long. Rejecting these cases
+ // helps reduce the propagation of broken files.
+ // We do not ensure they are valid UTF-8 here, nor do we validate
+ // the required ASCII_TAG=value format of the user comments.
+ const unsigned char* buf = aData + 8;
+ uint32_t bytes = aLength - 8;
+ uint32_t len;
+ // Read the vendor string.
+ len = LittleEndian::readUint32(buf);
+ buf += 4;
+ bytes -= 4;
+ if (len > bytes) return false;
+ mVendorString = nsCString(reinterpret_cast<const char*>(buf), len);
+ buf += len;
+ bytes -= len;
+ // Read the user comments.
+ if (bytes < 4) return false;
+ uint32_t ncomments = LittleEndian::readUint32(buf);
+ buf += 4;
+ bytes -= 4;
+ // If there are so many comments even their length fields
+ // won't fit in the packet, stop reading now.
+ if (ncomments > (bytes >> 2)) return false;
+ for (uint32_t i = 0; i < ncomments; i++) {
+ if (bytes < 4) return false;
+ len = LittleEndian::readUint32(buf);
+ buf += 4;
+ bytes -= 4;
+ if (len > bytes) return false;
+ mTags.AppendElement(nsCString(reinterpret_cast<const char*>(buf), len));
+ buf += len;
+ bytes -= len;
+ }
+
+#ifdef DEBUG
+ OPUS_LOG(LogLevel::Debug, ("Opus metadata header:"));
+ OPUS_LOG(LogLevel::Debug, (" vendor: %s", mVendorString.get()));
+ for (uint32_t i = 0; i < mTags.Length(); i++) {
+ OPUS_LOG(LogLevel::Debug, (" %s", mTags[i].get()));
+ }
+#endif
+ return true;
+}
+
+/* static */
+bool OpusParser::IsValidMapping2ChannelsCount(uint8_t aChannels) {
+ // https://tools.ietf.org/html/draft-ietf-codec-ambisonics-08#page-4
+ // For both channel mapping family 2 and family 3, the allowed numbers
+ // of channels: (1 + n)^2 + 2j for n = 0, 1, ..., 14 and j = 0 or 1,
+ // where n denotes the (highest) ambisonic order and j denotes whether
+ // or not there is a separate non-diegetic stereo stream Explicitly the
+ // allowed number of channels are 1, 3, 4, 6, 9, 11, 16, 18, 25, 27, 36,
+ // 38, 49, 51, 64, 66, 81, 83, 100, 102, 121, 123, 144, 146, 169, 171,
+ // 196, 198, 225, and 227.
+
+ // We use the property that int(sqrt(n)) == int(sqrt(n+2)) for n != 3
+ // which is handled by the test n^2 + 2 != channel
+ if (aChannels < 1 || aChannels > 227) {
+ return false;
+ }
+ double val = sqrt(aChannels);
+ int32_t valInt = int32_t(val);
+ return val == valInt || valInt * valInt + 2 == aChannels;
+}
+
+#undef OPUS_LOG
+
+} // namespace mozilla
diff --git a/dom/media/ogg/OpusParser.h b/dom/media/ogg/OpusParser.h
new file mode 100644
index 0000000000..fc2fc5094d
--- /dev/null
+++ b/dom/media/ogg/OpusParser.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#if !defined(OpusParser_h_)
+# define OpusParser_h_
+
+# include "nsTArray.h"
+# include "nsString.h"
+
+namespace mozilla {
+
+class OpusParser {
+ public:
+ OpusParser();
+
+ bool DecodeHeader(unsigned char* aData, size_t aLength);
+ bool DecodeTags(unsigned char* aData, size_t aLength);
+ static bool IsValidMapping2ChannelsCount(uint8_t aChannels);
+
+ // Various fields from the Ogg Opus header.
+ int mRate; // Sample rate the decoder uses (always 48 kHz).
+ uint32_t mNominalRate; // Original sample rate of the data (informational).
+ int mChannels; // Number of channels the stream encodes.
+ uint16_t mPreSkip; // Number of samples to strip after decoder reset.
+# ifdef MOZ_SAMPLE_TYPE_FLOAT32
+ float mGain; // Gain to apply to decoder output.
+# else
+ int32_t mGain_Q16; // Gain to apply to the decoder output.
+# endif
+ int mChannelMapping; // Channel mapping family.
+ int mStreams; // Number of packed streams in each packet.
+ int mCoupledStreams; // Number of packed coupled streams in each packet.
+ unsigned char mMappingTable[255]; // Channel mapping table.
+
+ // Granule position (end sample) of the last decoded Opus packet. This is
+ // used to calculate the amount we should trim from the last packet.
+ int64_t mPrevPacketGranulepos;
+
+ nsTArray<nsCString> mTags; // Unparsed comment strings from the header.
+
+ nsCString mVendorString; // Encoder vendor string from the header.
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/ogg/moz.build b/dom/media/ogg/moz.build
new file mode 100644
index 0000000000..863686686a
--- /dev/null
+++ b/dom/media/ogg/moz.build
@@ -0,0 +1,32 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS += [
+ "OggCodecState.h",
+ "OggCodecStore.h",
+ "OggDecoder.h",
+ "OggDemuxer.h",
+ "OggRLBox.h",
+ "OggRLBoxTypes.h",
+ "OggWriter.h",
+ "OpusParser.h",
+]
+
+UNIFIED_SOURCES += [
+ "OggCodecState.cpp",
+ "OggCodecStore.cpp",
+ "OggDecoder.cpp",
+ "OggDemuxer.cpp",
+ "OggWriter.cpp",
+ "OpusParser.cpp",
+]
+
+LOCAL_INCLUDES += ["!/security/rlbox"]
+
+FINAL_LIBRARY = "xul"
+
+# Add libFuzzer configuration directives
+include("/tools/fuzzing/libfuzzer-config.mozbuild")
diff --git a/dom/media/platforms/AllocationPolicy.cpp b/dom/media/platforms/AllocationPolicy.cpp
new file mode 100644
index 0000000000..a56bf1d3a1
--- /dev/null
+++ b/dom/media/platforms/AllocationPolicy.cpp
@@ -0,0 +1,241 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AllocationPolicy.h"
+
+#include "ImageContainer.h"
+#include "MediaInfo.h"
+#include "PDMFactory.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/SchedulerGroup.h"
+#ifdef MOZ_WIDGET_ANDROID
+# include "mozilla/jni/Utils.h"
+#endif
+
+namespace mozilla {
+
+using TrackType = TrackInfo::TrackType;
+
+class AllocPolicyImpl::AutoDeallocToken : public Token {
+ public:
+ explicit AutoDeallocToken(const RefPtr<AllocPolicyImpl>& aPolicy)
+ : mPolicy(aPolicy) {}
+
+ private:
+ ~AutoDeallocToken() { mPolicy->Dealloc(); }
+
+ RefPtr<AllocPolicyImpl> mPolicy;
+};
+
+AllocPolicyImpl::AllocPolicyImpl(int aDecoderLimit)
+ : mMaxDecoderLimit(aDecoderLimit),
+ mMonitor("AllocPolicyImpl"),
+ mDecoderLimit(aDecoderLimit) {}
+AllocPolicyImpl::~AllocPolicyImpl() { RejectAll(); }
+
+auto AllocPolicyImpl::Alloc() -> RefPtr<Promise> {
+ ReentrantMonitorAutoEnter mon(mMonitor);
+ // No decoder limit set.
+ if (mDecoderLimit < 0) {
+ return Promise::CreateAndResolve(new Token(), __func__);
+ }
+
+ RefPtr<PromisePrivate> p = new PromisePrivate(__func__);
+ mPromises.push(p);
+ ResolvePromise(mon);
+ return p;
+}
+
+void AllocPolicyImpl::Dealloc() {
+ ReentrantMonitorAutoEnter mon(mMonitor);
+ ++mDecoderLimit;
+ ResolvePromise(mon);
+}
+
+void AllocPolicyImpl::ResolvePromise(ReentrantMonitorAutoEnter& aProofOfLock) {
+ MOZ_ASSERT(mDecoderLimit >= 0);
+
+ if (mDecoderLimit > 0 && !mPromises.empty()) {
+ --mDecoderLimit;
+ RefPtr<PromisePrivate> p = std::move(mPromises.front());
+ mPromises.pop();
+ p->Resolve(new AutoDeallocToken(this), __func__);
+ }
+}
+
+void AllocPolicyImpl::RejectAll() {
+ ReentrantMonitorAutoEnter mon(mMonitor);
+ while (!mPromises.empty()) {
+ RefPtr<PromisePrivate> p = std::move(mPromises.front());
+ mPromises.pop();
+ p->Reject(true, __func__);
+ }
+}
+
+static int32_t MediaDecoderLimitDefault() {
+#ifdef MOZ_WIDGET_ANDROID
+ if (jni::GetAPIVersion() < 18) {
+ // Older Android versions have broken support for multiple simultaneous
+ // decoders, see bug 1278574.
+ return 1;
+ }
+#endif
+ // Otherwise, set no decoder limit.
+ return -1;
+}
+
+StaticMutex GlobalAllocPolicy::sMutex;
+
+NotNull<AllocPolicy*> GlobalAllocPolicy::Instance(TrackType aTrack) {
+ StaticMutexAutoLock lock(sMutex);
+ if (aTrack == TrackType::kAudioTrack) {
+ static RefPtr<AllocPolicyImpl> sAudioPolicy = []() {
+ SchedulerGroup::Dispatch(
+ TaskCategory::Other,
+ NS_NewRunnableFunction(
+ "GlobalAllocPolicy::GlobalAllocPolicy:Audio", []() {
+ ClearOnShutdown(&sAudioPolicy,
+ ShutdownPhase::XPCOMShutdownThreads);
+ }));
+ return new AllocPolicyImpl(MediaDecoderLimitDefault());
+ }();
+ return WrapNotNull(sAudioPolicy.get());
+ }
+ static RefPtr<AllocPolicyImpl> sVideoPolicy = []() {
+ SchedulerGroup::Dispatch(
+ TaskCategory::Other,
+ NS_NewRunnableFunction(
+ "GlobalAllocPolicy::GlobalAllocPolicy:Audio", []() {
+ ClearOnShutdown(&sVideoPolicy,
+ ShutdownPhase::XPCOMShutdownThreads);
+ }));
+ return new AllocPolicyImpl(MediaDecoderLimitDefault());
+ }();
+ return WrapNotNull(sVideoPolicy.get());
+}
+
+class SingleAllocPolicy::AutoDeallocCombinedToken : public Token {
+ public:
+ AutoDeallocCombinedToken(already_AddRefed<Token> aSingleAllocPolicyToken,
+ already_AddRefed<Token> aGlobalAllocPolicyToken)
+ : mSingleToken(aSingleAllocPolicyToken),
+ mGlobalToken(aGlobalAllocPolicyToken) {}
+
+ private:
+ // Release tokens allocated from GlobalAllocPolicy and LocalAllocPolicy
+ // and process next token request if any.
+ ~AutoDeallocCombinedToken() = default;
+ const RefPtr<Token> mSingleToken;
+ const RefPtr<Token> mGlobalToken;
+};
+
+auto SingleAllocPolicy::Alloc() -> RefPtr<Promise> {
+ MOZ_DIAGNOSTIC_ASSERT(MaxDecoderLimit() == 1,
+ "We can only handle at most one token out at a time.");
+ RefPtr<SingleAllocPolicy> self = this;
+ return AllocPolicyImpl::Alloc()->Then(
+ mOwnerThread, __func__,
+ [self](RefPtr<Token> aToken) {
+ RefPtr<Token> localToken = std::move(aToken);
+ RefPtr<Promise> p = self->mPendingPromise.Ensure(__func__);
+ GlobalAllocPolicy::Instance(self->mTrack)
+ ->Alloc()
+ ->Then(
+ self->mOwnerThread, __func__,
+ [self, localToken = std::move(localToken)](
+ RefPtr<Token> aToken) mutable {
+ self->mTokenRequest.Complete();
+ RefPtr<Token> combinedToken = new AutoDeallocCombinedToken(
+ localToken.forget(), aToken.forget());
+ self->mPendingPromise.Resolve(combinedToken, __func__);
+ },
+ [self]() {
+ self->mTokenRequest.Complete();
+ self->mPendingPromise.Reject(true, __func__);
+ })
+ ->Track(self->mTokenRequest);
+ return p;
+ },
+ []() { return Promise::CreateAndReject(true, __func__); });
+}
+
+SingleAllocPolicy::~SingleAllocPolicy() {
+ mPendingPromise.RejectIfExists(true, __func__);
+ mTokenRequest.DisconnectIfExists();
+}
+
+void SingleAllocPolicy::Cancel() {
+ MOZ_ASSERT(mOwnerThread->IsCurrentThreadIn());
+ mPendingPromise.RejectIfExists(true, __func__);
+ mTokenRequest.DisconnectIfExists();
+ RejectAll();
+}
+
+AllocationWrapper::AllocationWrapper(
+ already_AddRefed<MediaDataDecoder> aDecoder, already_AddRefed<Token> aToken)
+ : mDecoder(aDecoder), mToken(aToken) {
+ DecoderDoctorLogger::LogConstructionAndBase(
+ "AllocationWrapper", this, static_cast<const MediaDataDecoder*>(this));
+ DecoderDoctorLogger::LinkParentAndChild("AllocationWrapper", this, "decoder",
+ mDecoder.get());
+}
+
+AllocationWrapper::~AllocationWrapper() {
+ DecoderDoctorLogger::LogDestruction("AllocationWrapper", this);
+}
+
+RefPtr<ShutdownPromise> AllocationWrapper::Shutdown() {
+ RefPtr<MediaDataDecoder> decoder = std::move(mDecoder);
+ RefPtr<Token> token = std::move(mToken);
+ return decoder->Shutdown()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [token]() { return ShutdownPromise::CreateAndResolve(true, __func__); });
+}
+/* static */ RefPtr<AllocationWrapper::AllocateDecoderPromise>
+AllocationWrapper::CreateDecoder(const CreateDecoderParams& aParams,
+ AllocPolicy* aPolicy) {
+ RefPtr<AllocateDecoderPromise> p =
+ (aPolicy ? aPolicy : GlobalAllocPolicy::Instance(aParams.mType))
+ ->Alloc()
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [params =
+ CreateDecoderParamsForAsync(aParams)](RefPtr<Token> aToken) {
+ // result may not always be updated by
+ // PDMFactory::CreateDecoder either when the creation
+ // succeeded or failed, as such it must be initialized to a
+ // fatal error by default.
+ MediaResult result =
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ nsPrintfCString("error creating %s decoder",
+ TrackTypeToStr(params.mType)));
+ RefPtr<PDMFactory> pdm = new PDMFactory();
+ RefPtr<PlatformDecoderModule::CreateDecoderPromise> p =
+ pdm->CreateDecoder(params)->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [aToken](RefPtr<MediaDataDecoder>&& aDecoder) mutable {
+ RefPtr<AllocationWrapper> wrapper =
+ new AllocationWrapper(aDecoder.forget(),
+ aToken.forget());
+ return AllocateDecoderPromise::CreateAndResolve(
+ wrapper, __func__);
+ },
+ [](const MediaResult& aError) {
+ return AllocateDecoderPromise::CreateAndReject(
+ aError, __func__);
+ });
+ return p;
+ },
+ []() {
+ return AllocateDecoderPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ "Allocation policy expired"),
+ __func__);
+ });
+ return p;
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/AllocationPolicy.h b/dom/media/platforms/AllocationPolicy.h
new file mode 100644
index 0000000000..a53f923483
--- /dev/null
+++ b/dom/media/platforms/AllocationPolicy.h
@@ -0,0 +1,183 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef AllocationPolicy_h_
+#define AllocationPolicy_h_
+
+#include <queue>
+
+#include "MediaInfo.h"
+#include "PlatformDecoderModule.h"
+#include "TimeUnits.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/NotNull.h"
+#include "mozilla/ReentrantMonitor.h"
+#include "mozilla/StaticMutex.h"
+
+namespace mozilla {
+
+/**
+ * Before calling PDMFactory::CreateDecoder(), Alloc() must be called on the
+ * policy to get a token object as a permission to create a decoder. The
+ * token should stay alive until Shutdown() is called on the decoder. The
+ * destructor of the token will restore the decoder count so it is available
+ * for next calls of Alloc().
+ */
+class AllocPolicy {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AllocPolicy)
+
+ public:
+ class Token {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(Token)
+ protected:
+ virtual ~Token() = default;
+ };
+ using Promise = MozPromise<RefPtr<Token>, bool, true>;
+
+ // Acquire a token for decoder creation. Thread-safe.
+ virtual RefPtr<Promise> Alloc() = 0;
+
+ protected:
+ virtual ~AllocPolicy() = default;
+};
+
+/**
+ * This is a singleton which controls the number of decoders that can be created
+ * concurrently.
+ * Instance() will return the TrackType global AllocPolicy.
+ * Instance() will always return a non-null value.
+ */
+class GlobalAllocPolicy {
+ public:
+ // Get the singleton for the given track type. Thread-safe.
+ static NotNull<AllocPolicy*> Instance(TrackInfo::TrackType aTrack);
+
+ private:
+ // Protect access to Instance().
+ static StaticMutex sMutex MOZ_UNANNOTATED;
+};
+
+/** This the actual base implementation underneath all AllocPolicy objects and
+ * control how many decoders can be created concurrently.
+ * Alloc() must be called to get a token object as a permission to perform an
+ * action. The token should stay alive until Shutdown() is called on the
+ * decoder. The destructor of the token will restore the decoder count so it is
+ * available for next calls of Alloc().
+ **/
+class AllocPolicyImpl : public AllocPolicy {
+ public:
+ explicit AllocPolicyImpl(int aDecoderLimit);
+ RefPtr<Promise> Alloc() override;
+
+ protected:
+ virtual ~AllocPolicyImpl();
+ void RejectAll();
+ int MaxDecoderLimit() const { return mMaxDecoderLimit; }
+
+ private:
+ class AutoDeallocToken;
+ using PromisePrivate = Promise::Private;
+ // Called by the destructor of TokenImpl to restore the decoder limit.
+ void Dealloc();
+ // Decrement the decoder limit and resolve a promise if available.
+ void ResolvePromise(ReentrantMonitorAutoEnter& aProofOfLock);
+
+ const int mMaxDecoderLimit;
+ ReentrantMonitor mMonitor MOZ_UNANNOTATED;
+ // The number of decoders available for creation.
+ int mDecoderLimit;
+ // Requests to acquire tokens.
+ std::queue<RefPtr<PromisePrivate>> mPromises;
+};
+
+/**
+ * This class allows to track and serialise a single decoder allocation at a
+ * time
+ */
+class SingleAllocPolicy : public AllocPolicyImpl {
+ using TrackType = TrackInfo::TrackType;
+
+ public:
+ SingleAllocPolicy(TrackType aTrack, TaskQueue* aOwnerThread)
+ : AllocPolicyImpl(1), mTrack(aTrack), mOwnerThread(aOwnerThread) {}
+
+ RefPtr<Promise> Alloc() override;
+
+ // Cancel the request to GlobalAllocPolicy and reject the current token
+ // request. Note this must happen before mOwnerThread->BeginShutdown().
+ void Cancel();
+
+ private:
+ class AutoDeallocCombinedToken;
+ virtual ~SingleAllocPolicy();
+
+ const TrackType mTrack;
+ RefPtr<TaskQueue> mOwnerThread;
+ MozPromiseHolder<Promise> mPendingPromise;
+ MozPromiseRequestHolder<Promise> mTokenRequest;
+};
+
+class AllocationWrapper final : public MediaDataDecoder {
+ using Token = AllocPolicy::Token;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AllocationWrapper, final);
+
+ AllocationWrapper(already_AddRefed<MediaDataDecoder> aDecoder,
+ already_AddRefed<Token> aToken);
+
+ RefPtr<InitPromise> Init() override { return mDecoder->Init(); }
+ RefPtr<DecodePromise> Decode(MediaRawData* aSample) override {
+ return mDecoder->Decode(aSample);
+ }
+ bool CanDecodeBatch() const override { return mDecoder->CanDecodeBatch(); }
+ RefPtr<DecodePromise> DecodeBatch(
+ nsTArray<RefPtr<MediaRawData>>&& aSamples) override {
+ return mDecoder->DecodeBatch(std::move(aSamples));
+ }
+ RefPtr<DecodePromise> Drain() override { return mDecoder->Drain(); }
+ RefPtr<FlushPromise> Flush() override { return mDecoder->Flush(); }
+ bool IsHardwareAccelerated(nsACString& aFailureReason) const override {
+ return mDecoder->IsHardwareAccelerated(aFailureReason);
+ }
+ nsCString GetDescriptionName() const override {
+ return mDecoder->GetDescriptionName();
+ }
+ nsCString GetProcessName() const override {
+ return mDecoder->GetProcessName();
+ }
+ nsCString GetCodecName() const override { return mDecoder->GetCodecName(); }
+ void SetSeekThreshold(const media::TimeUnit& aTime) override {
+ mDecoder->SetSeekThreshold(aTime);
+ }
+ bool SupportDecoderRecycling() const override {
+ return mDecoder->SupportDecoderRecycling();
+ }
+ RefPtr<ShutdownPromise> Shutdown() override;
+ ConversionRequired NeedsConversion() const override {
+ return mDecoder->NeedsConversion();
+ }
+
+ typedef MozPromise<RefPtr<MediaDataDecoder>, MediaResult,
+ /* IsExclusive = */ true>
+ AllocateDecoderPromise;
+ // Will create a decoder has soon as one can be created according to the
+ // AllocPolicy (or GlobalAllocPolicy if aPolicy is null)
+ // Warning: all aParams members must be valid until the promise has been
+ // resolved, as some contains raw pointers to objects.
+ static RefPtr<AllocateDecoderPromise> CreateDecoder(
+ const CreateDecoderParams& aParams, AllocPolicy* aPolicy = nullptr);
+
+ private:
+ ~AllocationWrapper();
+
+ RefPtr<MediaDataDecoder> mDecoder;
+ RefPtr<Token> mToken;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/platforms/MediaCodecsSupport.cpp b/dom/media/platforms/MediaCodecsSupport.cpp
new file mode 100644
index 0000000000..8377765c1c
--- /dev/null
+++ b/dom/media/platforms/MediaCodecsSupport.cpp
@@ -0,0 +1,206 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include <array>
+
+#include "MediaCodecsSupport.h"
+#include "PlatformDecoderModule.h"
+#include "mozilla/AppShutdown.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "nsTHashMap.h"
+
+using MediaCodecsSupport = mozilla::media::MediaCodecsSupport;
+
+namespace mozilla::media {
+
+static StaticAutoPtr<MCSInfo> sInstance;
+static StaticMutex sInitMutex;
+static StaticMutex sUpdateMutex;
+
+#define CODEC_SUPPORT_LOG(msg, ...) \
+ MOZ_LOG(sPDMLog, LogLevel::Debug, ("MediaCodecsSupport, " msg, ##__VA_ARGS__))
+
+void MCSInfo::AddSupport(const MediaCodecsSupported& aSupport) {
+ StaticMutexAutoLock lock(sUpdateMutex);
+ MCSInfo* instance = GetInstance();
+ if (!instance) {
+ CODEC_SUPPORT_LOG("Can't add codec support without a MCSInfo instance!");
+ return;
+ }
+ instance->mSupport += aSupport;
+}
+
+MediaCodecsSupported MCSInfo::GetSupport() {
+ StaticMutexAutoLock lock(sUpdateMutex);
+ MCSInfo* instance = GetInstance();
+ if (!instance) {
+ CODEC_SUPPORT_LOG("Can't get codec support without a MCSInfo instance!");
+ return MediaCodecsSupported{};
+ }
+ return instance->mSupport;
+}
+
+void MCSInfo::ResetSupport() {
+ StaticMutexAutoLock lock(sUpdateMutex);
+ MCSInfo* instance = GetInstance();
+ if (!instance) {
+ CODEC_SUPPORT_LOG("Can't reset codec support without a MCSInfo instance!");
+ return;
+ }
+ instance->mSupport.clear();
+}
+
+DecodeSupportSet MCSInfo::GetDecodeSupportSet(
+ const MediaCodec& aCodec, const MediaCodecsSupported& aSupported) {
+ DecodeSupportSet support;
+ const auto supportInfo = GetCodecDefinition(aCodec);
+ if (aSupported.contains(supportInfo.swDecodeSupport)) {
+ support += DecodeSupport::SoftwareDecode;
+ }
+ if (aSupported.contains(supportInfo.hwDecodeSupport)) {
+ support += DecodeSupport::HardwareDecode;
+ }
+ return support;
+}
+
+MediaCodecsSupported MCSInfo::GetDecodeMediaCodecsSupported(
+ const MediaCodec& aCodec, const DecodeSupportSet& aSupportSet) {
+ MediaCodecsSupported support;
+ const auto supportInfo = GetCodecDefinition(aCodec);
+ if (aSupportSet.contains(DecodeSupport::SoftwareDecode)) {
+ support += supportInfo.swDecodeSupport;
+ }
+ if (aSupportSet.contains(DecodeSupport::HardwareDecode)) {
+ support += supportInfo.hwDecodeSupport;
+ }
+ return support;
+}
+
+void MCSInfo::GetMediaCodecsSupportedString(
+ nsCString& aSupportString, const MediaCodecsSupported& aSupportedCodecs) {
+ CodecDefinition supportInfo;
+ aSupportString = ""_ns;
+ MCSInfo* instance = GetInstance();
+ if (!instance) {
+ CODEC_SUPPORT_LOG("Can't get codec support string w/o a MCSInfo instance!");
+ return;
+ }
+ for (const auto& it : aSupportedCodecs) {
+ if (!instance->mHashTableMCS->Get(it, &supportInfo)) {
+ CODEC_SUPPORT_LOG("Can't find string for MediaCodecsSupported enum: %d",
+ static_cast<int>(it));
+ aSupportString.Append("Unknown codec entry found!\n"_ns);
+ continue;
+ }
+ // Get codec name string and append SW/HW support info
+ aSupportString.Append(supportInfo.commonName);
+ if (it == supportInfo.swDecodeSupport) {
+ aSupportString.Append(" SW"_ns);
+ }
+ if (it == supportInfo.hwDecodeSupport) {
+ aSupportString.Append(" HW"_ns);
+ }
+ aSupportString.Append("\n"_ns);
+ }
+ // Remove any trailing newline characters
+ if (!aSupportString.IsEmpty()) {
+ aSupportString.Truncate(aSupportString.Length() - 1);
+ }
+}
+
+MCSInfo* MCSInfo::GetInstance() {
+ StaticMutexAutoLock lock(sInitMutex);
+ if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdownConfirmed)) {
+ CODEC_SUPPORT_LOG("In XPCOM shutdown - not returning MCSInfo instance!");
+ return nullptr;
+ }
+ if (!sInstance) {
+ sInstance = new MCSInfo();
+ }
+ return sInstance.get();
+}
+
+MCSInfo::MCSInfo() {
+ // Initialize hash tables
+ mHashTableMCS.reset(new nsTHashMap<MediaCodecsSupport, CodecDefinition>());
+ mHashTableCodec.reset(new nsTHashMap<MediaCodec, CodecDefinition>());
+
+ for (const auto& it : GetAllCodecDefinitions()) {
+ // Insert MediaCodecsSupport values as keys
+ mHashTableMCS->InsertOrUpdate(it.swDecodeSupport, it);
+ mHashTableMCS->InsertOrUpdate(it.hwDecodeSupport, it);
+ // Insert codec enum values as keys
+ mHashTableCodec->InsertOrUpdate(it.codec, it);
+ }
+
+ GetMainThreadSerialEventTarget()->Dispatch(
+ NS_NewRunnableFunction("MCSInfo::MCSInfo", [&] {
+ // Ensure hash tables freed on shutdown
+ RunOnShutdown(
+ [&] {
+ mHashTableMCS.reset();
+ mHashTableString.reset();
+ mHashTableCodec.reset();
+ sInstance = nullptr;
+ },
+ ShutdownPhase::XPCOMShutdown);
+ }));
+}
+
+CodecDefinition MCSInfo::GetCodecDefinition(const MediaCodec& aCodec) {
+ CodecDefinition info;
+ MCSInfo* instance = GetInstance();
+ if (!instance) {
+ CODEC_SUPPORT_LOG("Can't get codec definition without a MCSInfo instance!");
+ } else if (!instance->mHashTableCodec->Get(aCodec, &info)) {
+ CODEC_SUPPORT_LOG("Could not find codec definition for codec enum: %d!",
+ static_cast<int>(aCodec));
+ }
+ return info;
+}
+
+std::array<CodecDefinition, 12> MCSInfo::GetAllCodecDefinitions() {
+ static constexpr std::array<CodecDefinition, 12> codecDefinitions = {
+ {{MediaCodec::H264, "H264", "video/avc",
+ MediaCodecsSupport::H264SoftwareDecode,
+ MediaCodecsSupport::H264HardwareDecode},
+ {MediaCodec::VP9, "VP9", "video/vp9",
+ MediaCodecsSupport::VP9SoftwareDecode,
+ MediaCodecsSupport::VP9HardwareDecode},
+ {MediaCodec::VP8, "VP8", "video/vp8",
+ MediaCodecsSupport::VP8SoftwareDecode,
+ MediaCodecsSupport::VP8HardwareDecode},
+ {MediaCodec::AV1, "AV1", "video/av1",
+ MediaCodecsSupport::AV1SoftwareDecode,
+ MediaCodecsSupport::AV1HardwareDecode},
+ {MediaCodec::Theora, "Theora", "video/theora",
+ MediaCodecsSupport::TheoraSoftwareDecode,
+ MediaCodecsSupport::TheoraHardwareDecode},
+ {MediaCodec::AAC, "AAC", "audio/mp4a-latm",
+ MediaCodecsSupport::AACSoftwareDecode,
+ MediaCodecsSupport::AACHardwareDecode},
+ {MediaCodec::MP3, "MP3", "audio/mpeg",
+ MediaCodecsSupport::MP3SoftwareDecode,
+ MediaCodecsSupport::MP3HardwareDecode},
+ {MediaCodec::Opus, "Opus", "audio/opus",
+ MediaCodecsSupport::OpusSoftwareDecode,
+ MediaCodecsSupport::OpusHardwareDecode},
+ {MediaCodec::Vorbis, "Vorbis", "audio/vorbis",
+ MediaCodecsSupport::VorbisSoftwareDecode,
+ MediaCodecsSupport::VorbisHardwareDecode},
+ {MediaCodec::FLAC, "FLAC", "audio/flac",
+ MediaCodecsSupport::FLACSoftwareDecode,
+ MediaCodecsSupport::FLACHardwareDecode},
+ {MediaCodec::Wave, "Wave", "audio/x-wav",
+ MediaCodecsSupport::WaveSoftwareDecode,
+ MediaCodecsSupport::WaveHardwareDecode},
+ {MediaCodec::SENTINEL, "Undefined codec name",
+ "Undefined MIME type string", MediaCodecsSupport::SENTINEL,
+ MediaCodecsSupport::SENTINEL}}};
+ return codecDefinitions;
+}
+} // namespace mozilla::media
+
+#undef CODEC_SUPPORT_LOG
diff --git a/dom/media/platforms/MediaCodecsSupport.h b/dom/media/platforms/MediaCodecsSupport.h
new file mode 100644
index 0000000000..ae2fdd63a8
--- /dev/null
+++ b/dom/media/platforms/MediaCodecsSupport.h
@@ -0,0 +1,194 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef DOM_MEDIA_PLATFORMS_MEDIACODECSSUPPORT_H_
+#define DOM_MEDIA_PLATFORMS_MEDIACODECSSUPPORT_H_
+#include <array>
+
+#include "mozilla/Atomics.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/EnumSet.h"
+#include "mozilla/StaticMutex.h"
+#include "nsString.h"
+#include "nsTHashMap.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla::media {
+// List of codecs we support, used in the below macros
+// to generate MediaCodec and MediaCodecSupports enums.
+#define CODEC_LIST \
+ X(H264) \
+ X(VP8) \
+ X(VP9) \
+ X(AV1) \
+ X(Theora) \
+ X(AAC) \
+ X(FLAC) \
+ X(MP3) \
+ X(Opus) \
+ X(Vorbis) \
+ X(Wave)
+
+// Generate MediaCodec enum with the names of each codec we support.
+// Example: MediaCodec::H264
+enum class MediaCodec : int {
+#define X(name) name,
+ CODEC_LIST
+#undef X
+ SENTINEL
+};
+
+// Helper macros used to create codec-specific SW/HW decode enums below.
+#define SW_DECODE(codec) codec##SoftwareDecode
+#define HW_DECODE(codec) codec##HardwareDecode
+
+// Generate the MediaCodecsSupport enum, containing
+// codec-specific SW/HW decode/encode information.
+// Entries for HW audio decode/encode should never be set as we
+// don't support HW audio decode/encode, but they are included
+// for debug purposes / check for erroneous PDM return values.
+// Example: MediaCodecsSupport::AACSoftwareDecode
+enum class MediaCodecsSupport : int {
+#define X(name) SW_DECODE(name), HW_DECODE(name),
+ CODEC_LIST
+#undef X
+ SENTINEL
+};
+#undef SW_DECODE
+#undef HW_DECODE
+#undef CODEC_LIST // end of macros!
+
+// Enumset containing per-codec SW/HW support
+using MediaCodecsSupported = EnumSet<MediaCodecsSupport, uint64_t>;
+
+// Codec-agnostic SW/HW decode support information.
+enum class DecodeSupport : int {
+ Unsupported = 0,
+ SoftwareDecode,
+ HardwareDecode,
+};
+using DecodeSupportSet = EnumSet<DecodeSupport, uint64_t>;
+
+// CodecDefinition stores information needed to convert / index
+// codec support information between types. See: GetAllCodecDefinitions()
+struct CodecDefinition {
+ MediaCodec codec = MediaCodec::SENTINEL;
+ const char* commonName = "Undefined codec name";
+ const char* mimeTypeString = "Undefined MIME type string";
+ MediaCodecsSupport swDecodeSupport = MediaCodecsSupport::SENTINEL;
+ MediaCodecsSupport hwDecodeSupport = MediaCodecsSupport::SENTINEL;
+};
+
+// Singleton class used to collect, manage, and report codec support data.
+class MCSInfo final {
+ public:
+ // Add codec support information to our aggregated list of supported codecs.
+ // Incoming support info is merged with the current support info.
+ // This is because different PDMs may report different codec support
+ // information, so merging their results allows us to maintain a
+ // cumulative support list without overwriting any existing data.
+ static void AddSupport(const MediaCodecsSupported& aSupport);
+
+ // Return a cumulative list of codec support information.
+ // Each call to AddSupport adds to or updates this list.
+ // This support information can be used to create user-readable strings
+ // to report codec support information in about:support.
+ static MediaCodecsSupported GetSupport();
+
+ // Reset codec support information saved from calls to AddSupport().
+ static void ResetSupport();
+
+ // Query a MediaCodecsSupported EnumSet for codec-specific SW/HW support enums
+ // and return general support information as stored in a DecodeSupportSet.
+ //
+ // Example input:
+ //
+ // aCodec: MediaCodec::H264
+ // aDecode: MediaCodecsSupport {
+ // MediaCodecsSupport::AACSoftwareDecode
+ // MediaCodecsSupport::H264HardwareDecode,
+ // MediaCodecsSupport::H264SoftwareDecode,
+ // MediaCodecsSupport::VP8SoftwareDecode,
+ // }
+ //
+ // Example output:
+ //
+ // DecodeSupportSet {
+ // DecodeSupport::SoftwareDecode,
+ // DecodeSupport::HardwareDecode
+ // }
+ //
+ static DecodeSupportSet GetDecodeSupportSet(
+ const MediaCodec& aCodec, const MediaCodecsSupported& aSupported);
+
+ // Return codec-specific SW/HW support enums for a given codec.
+ // The DecodeSupportSet argument is used which codec-specific SW/HW
+ // support values are returned, if any.
+ //
+ // Example input:
+ // aCodec: MediaCodec::VP8
+ // aSupportSet: DecodeSupportSet {DecodeSupport::SoftwareDecode}
+ //
+ // Example output:
+ // MediaCodecsSupported {MediaCodecsSupport::VP8SoftwareDecode}
+ //
+ static MediaCodecsSupported GetDecodeMediaCodecsSupported(
+ const MediaCodec& aCodec, const DecodeSupportSet& aSupportSet);
+
+ // Generate a plaintext description for the SW/HW support information
+ // contained in a MediaCodecsSupported EnumSet.
+ //
+ // Example input:
+ // MediaCodecsSupported {
+ // MediaCodecsSupport::H264SoftwareDecode,
+ // MediaCodecsSupport::H264HardwareDecode,
+ // MediaCodecsSupport::VP8SoftwareDecode
+ // }
+ //
+ // Example output (returned via argument aCodecString)
+ //
+ // "SW H264 decode\n
+ // HW H264 decode\n
+ // SW VP8 decode"_ns
+ //
+ static void GetMediaCodecsSupportedString(
+ nsCString& aSupportString, const MediaCodecsSupported& aSupportedCodecs);
+
+ // Returns array of hardcoded codec definitions used to generate hashtables
+ // that convert between types
+ static std::array<CodecDefinition, 12> GetAllCodecDefinitions();
+
+ MCSInfo(MCSInfo const&) = delete;
+ void operator=(MCSInfo const&) = delete;
+ ~MCSInfo() = default;
+
+ private:
+ MCSInfo();
+ static MCSInfo* GetInstance();
+
+ // Returns a codec definition by MIME type name ("media/vp9")
+ // or "common" name ("VP9")
+ static CodecDefinition GetCodecDefinition(const MediaCodec& aCodec);
+
+ UniquePtr<nsTHashMap<MediaCodecsSupport, CodecDefinition>> mHashTableMCS;
+ UniquePtr<nsTHashMap<const char*, CodecDefinition>> mHashTableString;
+ UniquePtr<nsTHashMap<MediaCodec, CodecDefinition>> mHashTableCodec;
+ MediaCodecsSupported mSupport;
+};
+} // namespace mozilla::media
+
+namespace mozilla {
+// Used for IPDL serialization.
+// The 'value' has to be the biggest enum from MediaCodecsSupport.
+template <typename T>
+struct MaxEnumValue;
+template <>
+struct MaxEnumValue<media::MediaCodecsSupport> {
+ static constexpr unsigned int value =
+ static_cast<unsigned int>(media::MediaCodecsSupport::SENTINEL);
+};
+} // namespace mozilla
+
+#endif /* MediaCodecsSupport_h_ */
diff --git a/dom/media/platforms/MediaTelemetryConstants.h b/dom/media/platforms/MediaTelemetryConstants.h
new file mode 100644
index 0000000000..d22f310e12
--- /dev/null
+++ b/dom/media/platforms/MediaTelemetryConstants.h
@@ -0,0 +1,21 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef dom_media_platforms_MediaTelemetryConstants_h___
+#define dom_media_platforms_MediaTelemetryConstants_h___
+
+namespace mozilla {
+namespace media {
+
+enum class MediaDecoderBackend : uint32_t {
+ WMFSoftware = 0,
+ WMFDXVA2D3D9 = 1,
+ WMFDXVA2D3D11 = 2
+};
+
+} // namespace media
+} // namespace mozilla
+
+#endif // dom_media_platforms_MediaTelemetryConstants_h___
diff --git a/dom/media/platforms/PDMFactory.cpp b/dom/media/platforms/PDMFactory.cpp
new file mode 100644
index 0000000000..49d9e835f7
--- /dev/null
+++ b/dom/media/platforms/PDMFactory.cpp
@@ -0,0 +1,904 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "PDMFactory.h"
+
+#ifdef MOZ_AV1
+# include "AOMDecoder.h"
+#endif
+#include "AgnosticDecoderModule.h"
+#include "AudioTrimmer.h"
+#include "BlankDecoderModule.h"
+#include "DecoderDoctorDiagnostics.h"
+#include "EMEDecoderModule.h"
+#include "GMPDecoderModule.h"
+#include "H264.h"
+#include "MP4Decoder.h"
+#include "MediaChangeMonitor.h"
+#include "MediaInfo.h"
+#include "OpusDecoder.h"
+#include "TheoraDecoder.h"
+#include "VPXDecoder.h"
+#include "VideoUtils.h"
+#include "VorbisDecoder.h"
+#include "WAVDecoder.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/RemoteDecodeUtils.h"
+#include "mozilla/RemoteDecoderManagerChild.h"
+#include "mozilla/RemoteDecoderModule.h"
+#include "mozilla/SharedThreadPool.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/SyncRunnable.h"
+#include "mozilla/TaskQueue.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "nsIXULRuntime.h" // for BrowserTabsRemoteAutostart
+#include "nsPrintfCString.h"
+
+#include "mozilla/ipc/UtilityAudioDecoderParent.h"
+
+#ifdef XP_WIN
+# include "WMFDecoderModule.h"
+# include "mozilla/WindowsVersion.h"
+# ifdef MOZ_WMF_MEDIA_ENGINE
+# include "MFMediaEngineDecoderModule.h"
+# endif
+# ifdef MOZ_WMF_CDM
+# include "mozilla/CDMProxy.h"
+# endif
+#endif
+#ifdef MOZ_FFVPX
+# include "FFVPXRuntimeLinker.h"
+#endif
+#ifdef MOZ_FFMPEG
+# include "FFmpegRuntimeLinker.h"
+#endif
+#ifdef MOZ_APPLEMEDIA
+# include "AppleDecoderModule.h"
+#endif
+#ifdef MOZ_WIDGET_ANDROID
+# include "AndroidDecoderModule.h"
+#endif
+#ifdef MOZ_OMX
+# include "OmxDecoderModule.h"
+#endif
+
+#include <functional>
+
+using DecodeSupport = mozilla::media::DecodeSupport;
+using DecodeSupportSet = mozilla::media::DecodeSupportSet;
+using MediaCodec = mozilla::media::MediaCodec;
+using MediaCodecsSupport = mozilla::media::MediaCodecsSupport;
+using MediaCodecsSupported = mozilla::media::MediaCodecsSupported;
+using MCSInfo = mozilla::media::MCSInfo;
+
+namespace mozilla {
+
+#define PDM_INIT_LOG(msg, ...) \
+ MOZ_LOG(sPDMLog, LogLevel::Debug, ("PDMInitializer, " msg, ##__VA_ARGS__))
+
+extern already_AddRefed<PlatformDecoderModule> CreateNullDecoderModule();
+
+class PDMInitializer final {
+ public:
+ // This function should only be executed ONCE per process.
+ static void InitPDMs();
+
+ // Return true if we've finished PDMs initialization.
+ static bool HasInitializedPDMs();
+
+ private:
+ static void InitGpuPDMs() {
+#ifdef XP_WIN
+ if (!IsWin7AndPre2000Compatible()) {
+ WMFDecoderModule::Init();
+ }
+#endif
+ }
+
+ static void InitRddPDMs() {
+#ifdef XP_WIN
+ if (!IsWin7AndPre2000Compatible()) {
+ WMFDecoderModule::Init();
+ }
+#endif
+#ifdef MOZ_APPLEMEDIA
+ AppleDecoderModule::Init();
+#endif
+#ifdef MOZ_FFVPX
+ FFVPXRuntimeLinker::Init();
+#endif
+#ifdef MOZ_FFMPEG
+ if (StaticPrefs::media_rdd_ffmpeg_enabled()) {
+ FFmpegRuntimeLinker::Init();
+ }
+#endif
+ }
+
+ static void InitUtilityPDMs() {
+ const ipc::SandboxingKind kind = GetCurrentSandboxingKind();
+#ifdef XP_WIN
+ if (!IsWin7AndPre2000Compatible() &&
+ kind == ipc::SandboxingKind::UTILITY_AUDIO_DECODING_WMF) {
+ WMFDecoderModule::Init();
+ }
+# ifdef MOZ_WMF_MEDIA_ENGINE
+ if (IsWin10OrLater() && StaticPrefs::media_wmf_media_engine_enabled() &&
+ kind == ipc::SandboxingKind::MF_MEDIA_ENGINE_CDM) {
+ MFMediaEngineDecoderModule::Init();
+ }
+# endif
+#endif
+#ifdef MOZ_APPLEMEDIA
+ if (kind == ipc::SandboxingKind::UTILITY_AUDIO_DECODING_APPLE_MEDIA) {
+ AppleDecoderModule::Init();
+ }
+#endif
+#ifdef MOZ_FFVPX
+ if (kind == ipc::SandboxingKind::GENERIC_UTILITY) {
+ FFVPXRuntimeLinker::Init();
+ }
+#endif
+#ifdef MOZ_FFMPEG
+ if (StaticPrefs::media_utility_ffmpeg_enabled() &&
+ kind == ipc::SandboxingKind::GENERIC_UTILITY) {
+ FFmpegRuntimeLinker::Init();
+ }
+#endif
+ }
+
+ static void InitContentPDMs() {
+#ifdef XP_WIN
+ if (!IsWin7AndPre2000Compatible()) {
+# ifdef MOZ_WMF
+ if (!StaticPrefs::media_rdd_process_enabled() ||
+ !StaticPrefs::media_rdd_wmf_enabled() ||
+ !StaticPrefs::media_utility_process_enabled() ||
+ !StaticPrefs::media_utility_wmf_enabled()) {
+ WMFDecoderModule::Init();
+ }
+# endif
+ }
+#endif
+#ifdef MOZ_APPLEMEDIA
+ AppleDecoderModule::Init();
+#endif
+#ifdef MOZ_OMX
+ OmxDecoderModule::Init();
+#endif
+#ifdef MOZ_FFVPX
+ FFVPXRuntimeLinker::Init();
+#endif
+#ifdef MOZ_FFMPEG
+ FFmpegRuntimeLinker::Init();
+#endif
+ RemoteDecoderManagerChild::Init();
+ }
+
+ static void InitDefaultPDMs() {
+#ifdef XP_WIN
+ if (!IsWin7AndPre2000Compatible()) {
+ WMFDecoderModule::Init();
+ }
+#endif
+#ifdef MOZ_APPLEMEDIA
+ AppleDecoderModule::Init();
+#endif
+#ifdef MOZ_OMX
+ OmxDecoderModule::Init();
+#endif
+#ifdef MOZ_FFVPX
+ FFVPXRuntimeLinker::Init();
+#endif
+#ifdef MOZ_FFMPEG
+ FFmpegRuntimeLinker::Init();
+#endif
+ }
+
+ static bool sHasInitializedPDMs;
+ static StaticMutex sMonitor MOZ_UNANNOTATED;
+};
+
+bool PDMInitializer::sHasInitializedPDMs = false;
+StaticMutex PDMInitializer::sMonitor;
+
+/* static */
+void PDMInitializer::InitPDMs() {
+ StaticMutexAutoLock mon(sMonitor);
+ MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!sHasInitializedPDMs);
+ if (XRE_IsGPUProcess()) {
+ PDM_INIT_LOG("Init PDMs in GPU process");
+ InitGpuPDMs();
+ } else if (XRE_IsRDDProcess()) {
+ PDM_INIT_LOG("Init PDMs in RDD process");
+ InitRddPDMs();
+ } else if (XRE_IsUtilityProcess()) {
+ PDM_INIT_LOG("Init PDMs in Utility process");
+ InitUtilityPDMs();
+ } else if (XRE_IsContentProcess()) {
+ PDM_INIT_LOG("Init PDMs in Content process");
+ InitContentPDMs();
+ } else {
+ MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess(),
+ "PDMFactory is only usable in the "
+ "Parent/GPU/RDD/Utility/Content process");
+ PDM_INIT_LOG("Init PDMs in Chrome process");
+ InitDefaultPDMs();
+ }
+ sHasInitializedPDMs = true;
+}
+
+/* static */
+bool PDMInitializer::HasInitializedPDMs() {
+ StaticMutexAutoLock mon(sMonitor);
+ return sHasInitializedPDMs;
+}
+
+class SupportChecker {
+ public:
+ enum class Reason : uint8_t {
+ kSupported,
+ kVideoFormatNotSupported,
+ kAudioFormatNotSupported,
+ kUnknown,
+ };
+
+ struct CheckResult {
+ explicit CheckResult(Reason aReason,
+ MediaResult aResult = MediaResult(NS_OK))
+ : mReason(aReason), mMediaResult(std::move(aResult)) {}
+ CheckResult(const CheckResult& aOther) = default;
+ CheckResult(CheckResult&& aOther) = default;
+ CheckResult& operator=(const CheckResult& aOther) = default;
+ CheckResult& operator=(CheckResult&& aOther) = default;
+
+ Reason mReason;
+ MediaResult mMediaResult;
+ };
+
+ template <class Func>
+ void AddToCheckList(Func&& aChecker) {
+ mCheckerList.AppendElement(std::forward<Func>(aChecker));
+ }
+
+ void AddMediaFormatChecker(const TrackInfo& aTrackConfig) {
+ if (aTrackConfig.IsVideo()) {
+ auto mimeType = aTrackConfig.GetAsVideoInfo()->mMimeType;
+ RefPtr<MediaByteBuffer> extraData =
+ aTrackConfig.GetAsVideoInfo()->mExtraData;
+ AddToCheckList([mimeType, extraData]() {
+ if (MP4Decoder::IsH264(mimeType)) {
+ SPSData spsdata;
+ // WMF H.264 Video Decoder and Apple ATDecoder
+ // do not support YUV444 format.
+ // For consistency, all decoders should be checked.
+ if (H264::DecodeSPSFromExtraData(extraData, spsdata) &&
+ (spsdata.profile_idc == 244 /* Hi444PP */ ||
+ spsdata.chroma_format_idc == PDMFactory::kYUV444)) {
+ return CheckResult(
+ SupportChecker::Reason::kVideoFormatNotSupported,
+ MediaResult(
+ NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Decoder may not have the capability "
+ "to handle the requested video format "
+ "with YUV444 chroma subsampling.")));
+ }
+ }
+ return CheckResult(SupportChecker::Reason::kSupported);
+ });
+ }
+ }
+
+ SupportChecker::CheckResult Check() {
+ for (auto& checker : mCheckerList) {
+ auto result = checker();
+ if (result.mReason != SupportChecker::Reason::kSupported) {
+ return result;
+ }
+ }
+ return CheckResult(SupportChecker::Reason::kSupported);
+ }
+
+ void Clear() { mCheckerList.Clear(); }
+
+ private:
+ nsTArray<std::function<CheckResult()>> mCheckerList;
+}; // SupportChecker
+
+PDMFactory::PDMFactory() {
+ EnsureInit();
+ CreatePDMs();
+ CreateNullPDM();
+}
+
+PDMFactory::~PDMFactory() = default;
+
+/* static */
+void PDMFactory::EnsureInit() {
+ if (PDMInitializer::HasInitializedPDMs()) {
+ return;
+ }
+ auto initalization = []() {
+ MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
+ if (!PDMInitializer::HasInitializedPDMs()) {
+ // Ensure that all system variables are initialized.
+ gfx::gfxVars::Initialize();
+ // Prime the preferences system from the main thread.
+ Unused << BrowserTabsRemoteAutostart();
+ PDMInitializer::InitPDMs();
+ }
+ };
+ // If on the main thread, then initialize PDMs. Otherwise, do a sync-dispatch
+ // to main thread.
+ if (NS_IsMainThread()) {
+ initalization();
+ return;
+ }
+ nsCOMPtr<nsIEventTarget> mainTarget = GetMainThreadSerialEventTarget();
+ nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction(
+ "PDMFactory::EnsureInit", std::move(initalization));
+ SyncRunnable::DispatchToThread(mainTarget, runnable);
+}
+
+RefPtr<PlatformDecoderModule::CreateDecoderPromise> PDMFactory::CreateDecoder(
+ const CreateDecoderParams& aParams) {
+ if (aParams.mUseNullDecoder.mUse) {
+ MOZ_ASSERT(mNullPDM);
+ return CreateDecoderWithPDM(mNullPDM, aParams);
+ }
+ bool isEncrypted = mEMEPDM && aParams.mConfig.mCrypto.IsEncrypted();
+
+ if (isEncrypted) {
+ return CreateDecoderWithPDM(mEMEPDM, aParams);
+ }
+
+ return CheckAndMaybeCreateDecoder(CreateDecoderParamsForAsync(aParams), 0);
+}
+
+RefPtr<PlatformDecoderModule::CreateDecoderPromise>
+PDMFactory::CheckAndMaybeCreateDecoder(CreateDecoderParamsForAsync&& aParams,
+ uint32_t aIndex,
+ Maybe<MediaResult> aEarlierError) {
+ uint32_t i = aIndex;
+ auto params = SupportDecoderParams(aParams);
+ for (; i < mCurrentPDMs.Length(); i++) {
+ if (mCurrentPDMs[i]->Supports(params, nullptr /* diagnostic */) ==
+ media::DecodeSupport::Unsupported) {
+ continue;
+ }
+ RefPtr<PlatformDecoderModule::CreateDecoderPromise> p =
+ CreateDecoderWithPDM(mCurrentPDMs[i], aParams)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [](RefPtr<MediaDataDecoder>&& aDecoder) {
+ return PlatformDecoderModule::CreateDecoderPromise::
+ CreateAndResolve(std::move(aDecoder), __func__);
+ },
+ [self = RefPtr{this}, i, params = std::move(aParams)](
+ const MediaResult& aError) mutable {
+ // Try the next PDM.
+ return self->CheckAndMaybeCreateDecoder(std::move(params),
+ i + 1, Some(aError));
+ });
+ return p;
+ }
+ if (aEarlierError) {
+ return PlatformDecoderModule::CreateDecoderPromise::CreateAndReject(
+ std::move(*aEarlierError), __func__);
+ }
+ return PlatformDecoderModule::CreateDecoderPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ nsPrintfCString("Error no decoder found for %s",
+ aParams.mConfig->mMimeType.get())
+ .get()),
+ __func__);
+}
+
+RefPtr<PlatformDecoderModule::CreateDecoderPromise>
+PDMFactory::CreateDecoderWithPDM(PlatformDecoderModule* aPDM,
+ const CreateDecoderParams& aParams) {
+ MOZ_ASSERT(aPDM);
+ MediaResult result = NS_OK;
+
+ SupportChecker supportChecker;
+ const TrackInfo& config = aParams.mConfig;
+ supportChecker.AddMediaFormatChecker(config);
+
+ auto checkResult = supportChecker.Check();
+ if (checkResult.mReason != SupportChecker::Reason::kSupported) {
+ if (checkResult.mReason ==
+ SupportChecker::Reason::kVideoFormatNotSupported) {
+ result = checkResult.mMediaResult;
+ } else if (checkResult.mReason ==
+ SupportChecker::Reason::kAudioFormatNotSupported) {
+ result = checkResult.mMediaResult;
+ }
+ return PlatformDecoderModule::CreateDecoderPromise::CreateAndReject(
+ result, __func__);
+ }
+
+ if (config.IsAudio()) {
+ RefPtr<PlatformDecoderModule::CreateDecoderPromise> p;
+ p = aPDM->AsyncCreateDecoder(aParams)->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [params = CreateDecoderParamsForAsync(aParams)](
+ RefPtr<MediaDataDecoder>&& aDecoder) {
+ RefPtr<MediaDataDecoder> decoder = std::move(aDecoder);
+ if (!params.mNoWrapper.mDontUseWrapper) {
+ decoder =
+ new AudioTrimmer(decoder.forget(), CreateDecoderParams(params));
+ }
+ return PlatformDecoderModule::CreateDecoderPromise::CreateAndResolve(
+ decoder, __func__);
+ },
+ [](const MediaResult& aError) {
+ return PlatformDecoderModule::CreateDecoderPromise::CreateAndReject(
+ aError, __func__);
+ });
+ return p;
+ }
+
+ if (!config.IsVideo()) {
+ return PlatformDecoderModule::CreateDecoderPromise::CreateAndReject(
+ MediaResult(
+ NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL(
+ "Decoder configuration error, expected audio or video.")),
+ __func__);
+ }
+
+ if ((MP4Decoder::IsH264(config.mMimeType) ||
+#ifdef MOZ_AV1
+ AOMDecoder::IsAV1(config.mMimeType) ||
+#endif
+ VPXDecoder::IsVPX(config.mMimeType)) &&
+ !aParams.mUseNullDecoder.mUse && !aParams.mNoWrapper.mDontUseWrapper) {
+ return MediaChangeMonitor::Create(this, aParams);
+ }
+ return aPDM->AsyncCreateDecoder(aParams);
+}
+
+DecodeSupportSet PDMFactory::SupportsMimeType(
+ const nsACString& aMimeType) const {
+ UniquePtr<TrackInfo> trackInfo = CreateTrackInfoWithMIMEType(aMimeType);
+ if (!trackInfo) {
+ return DecodeSupport::Unsupported;
+ }
+ return Supports(SupportDecoderParams(*trackInfo), nullptr);
+}
+
+DecodeSupportSet PDMFactory::Supports(
+ const SupportDecoderParams& aParams,
+ DecoderDoctorDiagnostics* aDiagnostics) const {
+ if (mEMEPDM) {
+ return mEMEPDM->Supports(aParams, aDiagnostics);
+ }
+
+ RefPtr<PlatformDecoderModule> current =
+ GetDecoderModule(aParams, aDiagnostics);
+
+ if (!current) {
+ return DecodeSupport::Unsupported;
+ }
+
+ // We have a PDM - check for + return SW/HW support info
+ return current->Supports(aParams, aDiagnostics);
+}
+
+void PDMFactory::CreatePDMs() {
+ if (StaticPrefs::media_use_blank_decoder()) {
+ CreateAndStartupPDM<BlankDecoderModule>();
+ // The Blank PDM SupportsMimeType reports true for all codecs; the creation
+ // of its decoder is infallible. As such it will be used for all media, we
+ // can stop creating more PDM from this point.
+ return;
+ }
+
+ if (XRE_IsGPUProcess()) {
+ CreateGpuPDMs();
+ } else if (XRE_IsRDDProcess()) {
+ CreateRddPDMs();
+ } else if (XRE_IsUtilityProcess()) {
+ CreateUtilityPDMs();
+ } else if (XRE_IsContentProcess()) {
+ CreateContentPDMs();
+ } else {
+ MOZ_DIAGNOSTIC_ASSERT(
+ XRE_IsParentProcess(),
+ "PDMFactory is only usable in the Parent/GPU/RDD/Content process");
+ CreateDefaultPDMs();
+ }
+}
+
+void PDMFactory::CreateGpuPDMs() {
+#ifdef XP_WIN
+ if (StaticPrefs::media_wmf_enabled() && !IsWin7AndPre2000Compatible()) {
+ CreateAndStartupPDM<WMFDecoderModule>();
+ }
+#endif
+}
+
+#if defined(MOZ_FFMPEG)
+static DecoderDoctorDiagnostics::Flags GetFailureFlagBasedOnFFmpegStatus(
+ const FFmpegRuntimeLinker::LinkStatus& aStatus) {
+ switch (aStatus) {
+ case FFmpegRuntimeLinker::LinkStatus_INVALID_FFMPEG_CANDIDATE:
+ case FFmpegRuntimeLinker::LinkStatus_UNUSABLE_LIBAV57:
+ case FFmpegRuntimeLinker::LinkStatus_INVALID_LIBAV_CANDIDATE:
+ case FFmpegRuntimeLinker::LinkStatus_OBSOLETE_FFMPEG:
+ case FFmpegRuntimeLinker::LinkStatus_OBSOLETE_LIBAV:
+ case FFmpegRuntimeLinker::LinkStatus_INVALID_CANDIDATE:
+ return DecoderDoctorDiagnostics::Flags::LibAVCodecUnsupported;
+ default:
+ MOZ_DIAGNOSTIC_ASSERT(
+ aStatus == FFmpegRuntimeLinker::LinkStatus_NOT_FOUND,
+ "Only call this method when linker fails.");
+ return DecoderDoctorDiagnostics::Flags::FFmpegNotFound;
+ }
+}
+#endif
+
+void PDMFactory::CreateRddPDMs() {
+#ifdef XP_WIN
+ if (StaticPrefs::media_wmf_enabled() &&
+ StaticPrefs::media_rdd_wmf_enabled()) {
+ CreateAndStartupPDM<WMFDecoderModule>();
+ }
+#endif
+#ifdef MOZ_APPLEMEDIA
+ if (StaticPrefs::media_rdd_applemedia_enabled()) {
+ CreateAndStartupPDM<AppleDecoderModule>();
+ }
+#endif
+#ifdef MOZ_FFVPX
+ if (StaticPrefs::media_ffvpx_enabled() &&
+ StaticPrefs::media_rdd_ffvpx_enabled()) {
+ CreateAndStartupPDM<FFVPXRuntimeLinker>();
+ }
+#endif
+#ifdef MOZ_FFMPEG
+ if (StaticPrefs::media_ffmpeg_enabled() &&
+ StaticPrefs::media_rdd_ffmpeg_enabled() &&
+ !CreateAndStartupPDM<FFmpegRuntimeLinker>()) {
+ mFailureFlags += GetFailureFlagBasedOnFFmpegStatus(
+ FFmpegRuntimeLinker::LinkStatusCode());
+ }
+#endif
+ CreateAndStartupPDM<AgnosticDecoderModule>();
+}
+
+void PDMFactory::CreateUtilityPDMs() {
+ const ipc::SandboxingKind aKind = GetCurrentSandboxingKind();
+#ifdef XP_WIN
+ if (StaticPrefs::media_wmf_enabled() &&
+ StaticPrefs::media_utility_wmf_enabled() &&
+ aKind == ipc::SandboxingKind::UTILITY_AUDIO_DECODING_WMF) {
+ CreateAndStartupPDM<WMFDecoderModule>();
+ }
+#endif
+#ifdef MOZ_APPLEMEDIA
+ if (StaticPrefs::media_utility_applemedia_enabled() &&
+ aKind == ipc::SandboxingKind::UTILITY_AUDIO_DECODING_APPLE_MEDIA) {
+ CreateAndStartupPDM<AppleDecoderModule>();
+ }
+#endif
+ if (aKind == ipc::SandboxingKind::GENERIC_UTILITY) {
+#ifdef MOZ_FFVPX
+ if (StaticPrefs::media_ffvpx_enabled() &&
+ StaticPrefs::media_utility_ffvpx_enabled()) {
+ CreateAndStartupPDM<FFVPXRuntimeLinker>();
+ }
+#endif
+#ifdef MOZ_FFMPEG
+ if (StaticPrefs::media_ffmpeg_enabled() &&
+ StaticPrefs::media_utility_ffmpeg_enabled() &&
+ !CreateAndStartupPDM<FFmpegRuntimeLinker>()) {
+ mFailureFlags += GetFailureFlagBasedOnFFmpegStatus(
+ FFmpegRuntimeLinker::LinkStatusCode());
+ }
+#endif
+#ifdef MOZ_WIDGET_ANDROID
+ if (StaticPrefs::media_utility_android_media_codec_enabled()) {
+ StartupPDM(AndroidDecoderModule::Create(),
+ StaticPrefs::media_android_media_codec_preferred());
+ }
+#endif
+ CreateAndStartupPDM<AgnosticDecoderModule>();
+ }
+#ifdef MOZ_WMF_MEDIA_ENGINE
+ if (aKind == ipc::SandboxingKind::MF_MEDIA_ENGINE_CDM) {
+ if (IsWin10OrLater() && StaticPrefs::media_wmf_media_engine_enabled()) {
+ CreateAndStartupPDM<MFMediaEngineDecoderModule>();
+ }
+ }
+#endif
+}
+
+void PDMFactory::CreateContentPDMs() {
+ if (StaticPrefs::media_gpu_process_decoder()) {
+ CreateAndStartupPDM<RemoteDecoderModule>(RemoteDecodeIn::GpuProcess);
+ }
+
+ if (StaticPrefs::media_rdd_process_enabled()) {
+ CreateAndStartupPDM<RemoteDecoderModule>(RemoteDecodeIn::RddProcess);
+ }
+
+ if (StaticPrefs::media_utility_process_enabled()) {
+#ifdef MOZ_APPLEMEDIA
+ CreateAndStartupPDM<RemoteDecoderModule>(
+ RemoteDecodeIn::UtilityProcess_AppleMedia);
+#endif
+#ifdef XP_WIN
+ CreateAndStartupPDM<RemoteDecoderModule>(
+ RemoteDecodeIn::UtilityProcess_WMF);
+#endif
+ // WMF and AppleMedia should be created before Generic because the order
+ // affects what decoder module would be chose first.
+ CreateAndStartupPDM<RemoteDecoderModule>(
+ RemoteDecodeIn::UtilityProcess_Generic);
+ }
+#ifdef MOZ_WMF_MEDIA_ENGINE
+ if (StaticPrefs::media_wmf_media_engine_enabled()) {
+ CreateAndStartupPDM<RemoteDecoderModule>(
+ RemoteDecodeIn::UtilityProcess_MFMediaEngineCDM);
+ }
+#endif
+
+#ifdef XP_WIN
+ if (StaticPrefs::media_wmf_enabled() && !IsWin7AndPre2000Compatible()) {
+# ifdef MOZ_WMF
+ if (!StaticPrefs::media_rdd_process_enabled() ||
+ !StaticPrefs::media_rdd_wmf_enabled()) {
+ if (!CreateAndStartupPDM<WMFDecoderModule>()) {
+ mFailureFlags += DecoderDoctorDiagnostics::Flags::WMFFailedToLoad;
+ }
+ }
+# endif
+ } else if (StaticPrefs::media_decoder_doctor_wmf_disabled_is_failure()) {
+ mFailureFlags += DecoderDoctorDiagnostics::Flags::WMFFailedToLoad;
+ }
+#endif
+
+#ifdef MOZ_APPLEMEDIA
+ CreateAndStartupPDM<AppleDecoderModule>();
+#endif
+#ifdef MOZ_OMX
+ if (StaticPrefs::media_omx_enabled()) {
+ CreateAndStartupPDM<OmxDecoderModule>();
+ }
+#endif
+#ifdef MOZ_FFVPX
+ if (StaticPrefs::media_ffvpx_enabled()) {
+ CreateAndStartupPDM<FFVPXRuntimeLinker>();
+ }
+#endif
+#ifdef MOZ_FFMPEG
+ if (StaticPrefs::media_ffmpeg_enabled() &&
+ !CreateAndStartupPDM<FFmpegRuntimeLinker>()) {
+ mFailureFlags += GetFailureFlagBasedOnFFmpegStatus(
+ FFmpegRuntimeLinker::LinkStatusCode());
+ }
+#endif
+#ifdef MOZ_WIDGET_ANDROID
+ if (StaticPrefs::media_android_media_codec_enabled()) {
+ StartupPDM(AndroidDecoderModule::Create(),
+ StaticPrefs::media_android_media_codec_preferred());
+ }
+#endif
+
+ CreateAndStartupPDM<AgnosticDecoderModule>();
+
+ if (StaticPrefs::media_gmp_decoder_enabled() &&
+ !StartupPDM(GMPDecoderModule::Create(),
+ StaticPrefs::media_gmp_decoder_preferred())) {
+ mFailureFlags += DecoderDoctorDiagnostics::Flags::GMPPDMFailedToStartup;
+ }
+}
+
+void PDMFactory::CreateDefaultPDMs() {
+#ifdef XP_WIN
+ if (StaticPrefs::media_wmf_enabled() && !IsWin7AndPre2000Compatible()) {
+ if (!CreateAndStartupPDM<WMFDecoderModule>()) {
+ mFailureFlags += DecoderDoctorDiagnostics::Flags::WMFFailedToLoad;
+ }
+ } else if (StaticPrefs::media_decoder_doctor_wmf_disabled_is_failure()) {
+ mFailureFlags += DecoderDoctorDiagnostics::Flags::WMFFailedToLoad;
+ }
+#endif
+
+#ifdef MOZ_APPLEMEDIA
+ CreateAndStartupPDM<AppleDecoderModule>();
+#endif
+#ifdef MOZ_OMX
+ if (StaticPrefs::media_omx_enabled()) {
+ CreateAndStartupPDM<OmxDecoderModule>();
+ }
+#endif
+#ifdef MOZ_FFVPX
+ if (StaticPrefs::media_ffvpx_enabled()) {
+ CreateAndStartupPDM<FFVPXRuntimeLinker>();
+ }
+#endif
+#ifdef MOZ_FFMPEG
+ if (StaticPrefs::media_ffmpeg_enabled() &&
+ !CreateAndStartupPDM<FFmpegRuntimeLinker>()) {
+ mFailureFlags += GetFailureFlagBasedOnFFmpegStatus(
+ FFmpegRuntimeLinker::LinkStatusCode());
+ }
+#endif
+#ifdef MOZ_WIDGET_ANDROID
+ if (StaticPrefs::media_android_media_codec_enabled()) {
+ StartupPDM(AndroidDecoderModule::Create(),
+ StaticPrefs::media_android_media_codec_preferred());
+ }
+#endif
+
+ CreateAndStartupPDM<AgnosticDecoderModule>();
+
+ if (StaticPrefs::media_gmp_decoder_enabled() &&
+ !StartupPDM(GMPDecoderModule::Create(),
+ StaticPrefs::media_gmp_decoder_preferred())) {
+ mFailureFlags += DecoderDoctorDiagnostics::Flags::GMPPDMFailedToStartup;
+ }
+}
+
+void PDMFactory::CreateNullPDM() {
+ mNullPDM = CreateNullDecoderModule();
+ MOZ_ASSERT(mNullPDM && NS_SUCCEEDED(mNullPDM->Startup()));
+}
+
+bool PDMFactory::StartupPDM(already_AddRefed<PlatformDecoderModule> aPDM,
+ bool aInsertAtBeginning) {
+ RefPtr<PlatformDecoderModule> pdm = aPDM;
+ if (pdm && NS_SUCCEEDED(pdm->Startup())) {
+ if (aInsertAtBeginning) {
+ mCurrentPDMs.InsertElementAt(0, pdm);
+ } else {
+ mCurrentPDMs.AppendElement(pdm);
+ }
+ return true;
+ }
+ return false;
+}
+
+already_AddRefed<PlatformDecoderModule> PDMFactory::GetDecoderModule(
+ const SupportDecoderParams& aParams,
+ DecoderDoctorDiagnostics* aDiagnostics) const {
+ if (aDiagnostics) {
+ // If libraries failed to load, the following loop over mCurrentPDMs
+ // will not even try to use them. So we record failures now.
+ aDiagnostics->SetFailureFlags(mFailureFlags);
+ }
+
+ RefPtr<PlatformDecoderModule> pdm;
+ for (const auto& current : mCurrentPDMs) {
+ if (current->Supports(aParams, aDiagnostics) !=
+ media::DecodeSupport::Unsupported) {
+ pdm = current;
+ break;
+ }
+ }
+ return pdm.forget();
+}
+
+void PDMFactory::SetCDMProxy(CDMProxy* aProxy) {
+ MOZ_ASSERT(aProxy);
+
+#ifdef MOZ_WIDGET_ANDROID
+ if (IsWidevineKeySystem(aProxy->KeySystem())) {
+ mEMEPDM = AndroidDecoderModule::Create(aProxy);
+ return;
+ }
+#endif
+#ifdef MOZ_WMF_CDM
+ if (IsPlayReadyKeySystemAndSupported(aProxy->KeySystem())) {
+ mEMEPDM = RemoteDecoderModule::Create(
+ RemoteDecodeIn::UtilityProcess_MFMediaEngineCDM);
+ return;
+ }
+#endif
+ auto m = MakeRefPtr<PDMFactory>();
+ mEMEPDM = MakeRefPtr<EMEDecoderModule>(aProxy, m);
+}
+
+/* static */
+media::MediaCodecsSupported PDMFactory::Supported(bool aForceRefresh) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ static auto calculate = []() {
+ auto pdm = MakeRefPtr<PDMFactory>();
+ MediaCodecsSupported supported;
+ // H264 and AAC depends on external framework that must be dynamically
+ // loaded.
+ // We currently only ship a single PDM per platform able to decode AAC or
+ // H264. As such we can assert that being able to create a H264 or AAC
+ // decoder indicates that with WMF on Windows or FFmpeg on Unixes is
+ // available.
+ // This logic will have to be revisited if a PDM supporting either codec
+ // will be added in addition to the WMF and FFmpeg PDM (such as OpenH264)
+ for (const auto& cd : MCSInfo::GetAllCodecDefinitions()) {
+ supported += MCSInfo::GetDecodeMediaCodecsSupported(
+ cd.codec, pdm->SupportsMimeType(nsCString(cd.mimeTypeString)));
+ }
+ return supported;
+ };
+
+ static MediaCodecsSupported supported = calculate();
+ if (aForceRefresh) {
+ supported = calculate();
+ }
+
+ return supported;
+}
+
+/* static */
+DecodeSupportSet PDMFactory::SupportsMimeType(
+ const nsACString& aMimeType, const MediaCodecsSupported& aSupported,
+ RemoteDecodeIn aLocation) {
+ const TrackSupportSet supports =
+ RemoteDecoderManagerChild::GetTrackSupport(aLocation);
+
+ if (supports.contains(TrackSupport::Video)) {
+ if (MP4Decoder::IsH264(aMimeType)) {
+ return MCSInfo::GetDecodeSupportSet(MediaCodec::H264, aSupported);
+ }
+ if (VPXDecoder::IsVP9(aMimeType)) {
+ return MCSInfo::GetDecodeSupportSet(MediaCodec::VP9, aSupported);
+ }
+ if (VPXDecoder::IsVP8(aMimeType)) {
+ return MCSInfo::GetDecodeSupportSet(MediaCodec::VP8, aSupported);
+ }
+#ifdef MOZ_AV1
+ if (AOMDecoder::IsAV1(aMimeType)) {
+ return MCSInfo::GetDecodeSupportSet(MediaCodec::AV1, aSupported);
+ }
+#endif
+ if (TheoraDecoder::IsTheora(aMimeType)) {
+ return MCSInfo::GetDecodeSupportSet(MediaCodec::Theora, aSupported);
+ }
+ }
+
+ if (supports.contains(TrackSupport::Audio)) {
+ if (MP4Decoder::IsAAC(aMimeType)) {
+ return MCSInfo::GetDecodeSupportSet(MediaCodec::AAC, aSupported);
+ }
+ if (aMimeType.EqualsLiteral("audio/mpeg")) {
+ return MCSInfo::GetDecodeSupportSet(MediaCodec::MP3, aSupported);
+ }
+ if (OpusDataDecoder::IsOpus(aMimeType)) {
+ return MCSInfo::GetDecodeSupportSet(MediaCodec::Opus, aSupported);
+ }
+ if (VorbisDataDecoder::IsVorbis(aMimeType)) {
+ return MCSInfo::GetDecodeSupportSet(MediaCodec::Vorbis, aSupported);
+ }
+ if (aMimeType.EqualsLiteral("audio/flac")) {
+ return MCSInfo::GetDecodeSupportSet(MediaCodec::FLAC, aSupported);
+ }
+ if (WaveDataDecoder::IsWave(aMimeType)) {
+ return MCSInfo::GetDecodeSupportSet(MediaCodec::Wave, aSupported);
+ }
+ }
+ return DecodeSupport::Unsupported;
+}
+
+/* static */
+bool PDMFactory::AllDecodersAreRemote() {
+ return StaticPrefs::media_rdd_process_enabled() &&
+#if defined(MOZ_FFVPX)
+ StaticPrefs::media_rdd_ffvpx_enabled() &&
+#endif
+ StaticPrefs::media_rdd_opus_enabled() &&
+ StaticPrefs::media_rdd_theora_enabled() &&
+ StaticPrefs::media_rdd_vorbis_enabled() &&
+ StaticPrefs::media_rdd_vpx_enabled() &&
+#if defined(MOZ_WMF)
+ StaticPrefs::media_rdd_wmf_enabled() &&
+#endif
+ StaticPrefs::media_rdd_wav_enabled();
+}
+
+#undef PDM_INIT_LOG
+} // namespace mozilla
diff --git a/dom/media/platforms/PDMFactory.h b/dom/media/platforms/PDMFactory.h
new file mode 100644
index 0000000000..c56c11c506
--- /dev/null
+++ b/dom/media/platforms/PDMFactory.h
@@ -0,0 +1,113 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(PDMFactory_h_)
+# define PDMFactory_h_
+
+# include "DecoderDoctorDiagnostics.h"
+# include "MediaCodecsSupport.h"
+# include "PlatformDecoderModule.h"
+# include "mozilla/AlreadyAddRefed.h"
+# include "mozilla/EnumSet.h"
+# include "mozilla/MozPromise.h"
+# include "mozilla/RefPtr.h"
+# include "mozilla/ipc/UtilityProcessSandboxing.h"
+# include "nsISupports.h"
+# include "nsStringFwd.h"
+# include "nsTArray.h"
+# include <utility>
+
+namespace mozilla {
+
+class CDMProxy;
+class MediaDataDecoder;
+class MediaResult;
+class StaticMutex;
+struct CreateDecoderParams;
+struct CreateDecoderParamsForAsync;
+struct SupportDecoderParams;
+enum class RemoteDecodeIn;
+
+using PDMCreateDecoderPromise = PlatformDecoderModule::CreateDecoderPromise;
+
+class PDMFactory final {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(PDMFactory)
+
+ PDMFactory();
+
+ // Factory method that creates the appropriate PlatformDecoderModule for
+ // the platform we're running on.
+ RefPtr<PDMCreateDecoderPromise> CreateDecoder(
+ const CreateDecoderParams& aParams);
+
+ media::DecodeSupportSet SupportsMimeType(const nsACString& aMimeType) const;
+ media::DecodeSupportSet Supports(
+ const SupportDecoderParams& aParams,
+ DecoderDoctorDiagnostics* aDiagnostics) const;
+
+ // Creates a PlatformDecoderModule that uses a CDMProxy to decrypt or
+ // decrypt-and-decode EME encrypted content. If the CDM only decrypts and
+ // does not decode, we create a PDM and use that to create MediaDataDecoders
+ // that we use on on aTaskQueue to decode the decrypted stream.
+ // This is called on the decode task queue.
+ void SetCDMProxy(CDMProxy* aProxy);
+
+ static constexpr int kYUV400 = 0;
+ static constexpr int kYUV420 = 1;
+ static constexpr int kYUV422 = 2;
+ static constexpr int kYUV444 = 3;
+
+ static media::MediaCodecsSupported Supported(bool aForceRefresh = false);
+ static media::DecodeSupportSet SupportsMimeType(
+ const nsACString& aMimeType,
+ const media::MediaCodecsSupported& aSupported, RemoteDecodeIn aLocation);
+
+ static bool AllDecodersAreRemote();
+
+ private:
+ virtual ~PDMFactory();
+
+ void CreatePDMs();
+ void CreateNullPDM();
+ void CreateGpuPDMs();
+ void CreateRddPDMs();
+ void CreateUtilityPDMs();
+ void CreateContentPDMs();
+ void CreateDefaultPDMs();
+
+ template <typename DECODER_MODULE, typename... ARGS>
+ bool CreateAndStartupPDM(ARGS&&... aArgs) {
+ return StartupPDM(DECODER_MODULE::Create(std::forward<ARGS>(aArgs)...));
+ }
+
+ // Startup the provided PDM and add it to our list if successful.
+ bool StartupPDM(already_AddRefed<PlatformDecoderModule> aPDM,
+ bool aInsertAtBeginning = false);
+ // Returns the first PDM in our list supporting the mimetype.
+ already_AddRefed<PlatformDecoderModule> GetDecoderModule(
+ const SupportDecoderParams& aParams,
+ DecoderDoctorDiagnostics* aDiagnostics) const;
+
+ RefPtr<PDMCreateDecoderPromise> CreateDecoderWithPDM(
+ PlatformDecoderModule* aPDM, const CreateDecoderParams& aParams);
+ RefPtr<PDMCreateDecoderPromise> CheckAndMaybeCreateDecoder(
+ CreateDecoderParamsForAsync&& aParams, uint32_t aIndex,
+ Maybe<MediaResult> aEarlierError = Nothing());
+
+ nsTArray<RefPtr<PlatformDecoderModule>> mCurrentPDMs;
+ RefPtr<PlatformDecoderModule> mEMEPDM;
+ RefPtr<PlatformDecoderModule> mNullPDM;
+
+ DecoderDoctorDiagnostics::FlagsSet mFailureFlags;
+
+ friend class RemoteVideoDecoderParent;
+ static void EnsureInit();
+};
+
+} // namespace mozilla
+
+#endif /* PDMFactory_h_ */
diff --git a/dom/media/platforms/PEMFactory.cpp b/dom/media/platforms/PEMFactory.cpp
new file mode 100644
index 0000000000..40adf268fe
--- /dev/null
+++ b/dom/media/platforms/PEMFactory.cpp
@@ -0,0 +1,74 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "PEMFactory.h"
+
+#ifdef MOZ_APPLEMEDIA
+# include "AppleEncoderModule.h"
+#endif
+
+#ifdef MOZ_WIDGET_ANDROID
+# include "AndroidEncoderModule.h"
+#endif
+
+#ifdef XP_WIN
+# include "WMFEncoderModule.h"
+#endif
+
+namespace mozilla {
+
+LazyLogModule sPEMLog("PlatformEncoderModule");
+
+PEMFactory::PEMFactory() {
+#ifdef MOZ_APPLEMEDIA
+ RefPtr<PlatformEncoderModule> m(new AppleEncoderModule());
+ mModules.AppendElement(m);
+#endif
+
+#ifdef MOZ_WIDGET_ANDROID
+ mModules.AppendElement(new AndroidEncoderModule());
+#endif
+
+#ifdef XP_WIN
+ mModules.AppendElement(new WMFEncoderModule());
+#endif
+}
+
+bool PEMFactory::SupportsMimeType(const nsACString& aMimeType) const {
+ for (auto m : mModules) {
+ if (m->SupportsMimeType(aMimeType)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+already_AddRefed<MediaDataEncoder> PEMFactory::CreateEncoder(
+ const CreateEncoderParams& aParams, const bool aHardwareNotAllowed) {
+ const TrackInfo& info = aParams.mConfig;
+ RefPtr<PlatformEncoderModule> m = FindPEM(info);
+ if (!m) {
+ return nullptr;
+ }
+
+ return info.IsVideo() ? m->CreateVideoEncoder(aParams, aHardwareNotAllowed)
+ : nullptr;
+}
+
+already_AddRefed<PlatformEncoderModule> PEMFactory::FindPEM(
+ const TrackInfo& aTrackInfo) const {
+ RefPtr<PlatformEncoderModule> found;
+ for (auto m : mModules) {
+ if (m->SupportsMimeType(aTrackInfo.mMimeType)) {
+ found = m;
+ break;
+ }
+ }
+
+ return found.forget();
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/PEMFactory.h b/dom/media/platforms/PEMFactory.h
new file mode 100644
index 0000000000..fdc922ba4b
--- /dev/null
+++ b/dom/media/platforms/PEMFactory.h
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(PEMFactory_h_)
+# define PEMFactory_h_
+
+# include "PlatformEncoderModule.h"
+
+namespace mozilla {
+
+class PEMFactory final {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(PEMFactory)
+
+ PEMFactory();
+
+ // Factory method that creates the appropriate PlatformEncoderModule for
+ // the platform we're running on. Caller is responsible for deleting this
+ // instance. It's expected that there will be multiple
+ // PlatformEncoderModules alive at the same time.
+ already_AddRefed<MediaDataEncoder> CreateEncoder(
+ const CreateEncoderParams& aParams, const bool aHardwareNotAllowed);
+
+ bool SupportsMimeType(const nsACString& aMimeType) const;
+
+ private:
+ virtual ~PEMFactory() = default;
+ // Returns the first PEM in our list supporting the mimetype.
+ already_AddRefed<PlatformEncoderModule> FindPEM(
+ const TrackInfo& aTrackInfo) const;
+
+ nsTArray<RefPtr<PlatformEncoderModule>> mModules;
+};
+
+} // namespace mozilla
+
+#endif /* PEMFactory_h_ */
diff --git a/dom/media/platforms/PlatformDecoderModule.cpp b/dom/media/platforms/PlatformDecoderModule.cpp
new file mode 100644
index 0000000000..ceff8ca2ed
--- /dev/null
+++ b/dom/media/platforms/PlatformDecoderModule.cpp
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "PlatformDecoderModule.h"
+
+#include "ImageContainer.h"
+#include "nsPrintfCString.h"
+
+namespace mozilla {
+
+CreateDecoderParamsForAsync::CreateDecoderParamsForAsync(
+ const CreateDecoderParams& aParams)
+ : mConfig(aParams.mConfig.Clone()),
+ mImageContainer(aParams.mImageContainer),
+ mKnowsCompositor(aParams.mKnowsCompositor),
+ mCrashHelper(aParams.mCrashHelper),
+ mUseNullDecoder(aParams.mUseNullDecoder),
+ mNoWrapper(aParams.mNoWrapper),
+ mType(aParams.mType),
+ mOnWaitingForKeyEvent(aParams.mOnWaitingForKeyEvent),
+ mOptions(aParams.mOptions),
+ mRate(aParams.mRate),
+ mMediaEngineId(aParams.mMediaEngineId),
+ mTrackingId(aParams.mTrackingId) {}
+
+CreateDecoderParamsForAsync::CreateDecoderParamsForAsync(
+ CreateDecoderParamsForAsync&& aParams) = default;
+
+RefPtr<PlatformDecoderModule::CreateDecoderPromise>
+PlatformDecoderModule::AsyncCreateDecoder(const CreateDecoderParams& aParams) {
+ RefPtr<MediaDataDecoder> decoder;
+ MediaResult result = NS_OK;
+ if (aParams.mConfig.IsAudio()) {
+ decoder = CreateAudioDecoder(CreateDecoderParams{aParams, &result});
+ } else if (aParams.mConfig.IsVideo()) {
+ decoder = CreateVideoDecoder(CreateDecoderParams{aParams, &result});
+ }
+ if (!decoder) {
+ if (NS_FAILED(result)) {
+ return CreateDecoderPromise::CreateAndReject(result, __func__);
+ }
+ return CreateDecoderPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ nsPrintfCString("Error creating decoder for %s",
+ aParams.mConfig.mMimeType.get())
+ .get()),
+ __func__);
+ }
+ return CreateDecoderPromise::CreateAndResolve(decoder, __func__);
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/PlatformDecoderModule.h b/dom/media/platforms/PlatformDecoderModule.h
new file mode 100644
index 0000000000..a230428748
--- /dev/null
+++ b/dom/media/platforms/PlatformDecoderModule.h
@@ -0,0 +1,567 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(PlatformDecoderModule_h_)
+# define PlatformDecoderModule_h_
+
+# include <queue>
+
+# include "DecoderDoctorLogger.h"
+# include "GMPCrashHelper.h"
+# include "MediaCodecsSupport.h"
+# include "MediaEventSource.h"
+# include "MediaInfo.h"
+# include "MediaResult.h"
+# include "mozilla/EnumSet.h"
+# include "mozilla/EnumTypeTraits.h"
+# include "mozilla/MozPromise.h"
+# include "mozilla/RefPtr.h"
+# include "mozilla/TaskQueue.h"
+# include "mozilla/layers/KnowsCompositor.h"
+# include "mozilla/layers/LayersTypes.h"
+# include "mozilla/ipc/UtilityAudioDecoder.h"
+# include "nsTArray.h"
+# include "PerformanceRecorder.h"
+
+namespace mozilla {
+class TrackInfo;
+class AudioInfo;
+class VideoInfo;
+class MediaRawData;
+class DecoderDoctorDiagnostics;
+
+namespace layers {
+class ImageContainer;
+} // namespace layers
+
+class MediaDataDecoder;
+class RemoteDecoderModule;
+class CDMProxy;
+
+static LazyLogModule sPDMLog("PlatformDecoderModule");
+
+namespace media {
+
+enum class Option {
+ Default,
+ LowLatency,
+ HardwareDecoderNotAllowed,
+ FullH264Parsing,
+ ErrorIfNoInitializationData, // By default frames delivered before
+ // initialization data are dropped. Pass this
+ // option to raise an error if frames are
+ // delivered before initialization data.
+ DefaultPlaybackDeviceMono, // Currently only used by Opus on RDD to avoid
+ // initialization of audio backends on RDD
+
+ SENTINEL // one past the last valid value
+};
+using OptionSet = EnumSet<Option>;
+
+struct UseNullDecoder {
+ UseNullDecoder() = default;
+ explicit UseNullDecoder(bool aUseNullDecoder) : mUse(aUseNullDecoder) {}
+ bool mUse = false;
+};
+
+// Do not wrap H264 decoder in a H264Converter.
+struct NoWrapper {
+ NoWrapper() = default;
+ explicit NoWrapper(bool aDontUseWrapper) : mDontUseWrapper(aDontUseWrapper) {}
+ bool mDontUseWrapper = false;
+};
+
+struct VideoFrameRate {
+ VideoFrameRate() = default;
+ explicit VideoFrameRate(float aFramerate) : mValue(aFramerate) {}
+ float mValue = 0.0f;
+};
+
+} // namespace media
+
+struct CreateDecoderParams;
+struct CreateDecoderParamsForAsync {
+ using Option = media::Option;
+ using OptionSet = media::OptionSet;
+ explicit CreateDecoderParamsForAsync(const CreateDecoderParams& aParams);
+ CreateDecoderParamsForAsync(CreateDecoderParamsForAsync&& aParams);
+
+ const VideoInfo& VideoConfig() const {
+ MOZ_ASSERT(mConfig->IsVideo());
+ return *mConfig->GetAsVideoInfo();
+ }
+
+ const AudioInfo& AudioConfig() const {
+ MOZ_ASSERT(mConfig->IsAudio());
+ return *mConfig->GetAsAudioInfo();
+ }
+
+ UniquePtr<TrackInfo> mConfig;
+ const RefPtr<layers::ImageContainer> mImageContainer;
+ const RefPtr<layers::KnowsCompositor> mKnowsCompositor;
+ const RefPtr<GMPCrashHelper> mCrashHelper;
+ const media::UseNullDecoder mUseNullDecoder;
+ const media::NoWrapper mNoWrapper;
+ const TrackInfo::TrackType mType = TrackInfo::kUndefinedTrack;
+ std::function<MediaEventProducer<TrackInfo::TrackType>*()>
+ mOnWaitingForKeyEvent;
+ const OptionSet mOptions = OptionSet(Option::Default);
+ const media::VideoFrameRate mRate;
+ const Maybe<uint64_t> mMediaEngineId;
+ const Maybe<TrackingId> mTrackingId;
+};
+
+struct MOZ_STACK_CLASS CreateDecoderParams final {
+ using Option = media::Option;
+ using OptionSet = media::OptionSet;
+ using UseNullDecoder = media::UseNullDecoder;
+ using NoWrapper = media::NoWrapper;
+ using VideoFrameRate = media::VideoFrameRate;
+
+ explicit CreateDecoderParams(const TrackInfo& aConfig) : mConfig(aConfig) {}
+ CreateDecoderParams(const CreateDecoderParams& aParams) = default;
+
+ MOZ_IMPLICIT CreateDecoderParams(const CreateDecoderParamsForAsync& aParams)
+ : mConfig(*aParams.mConfig.get()),
+ mImageContainer(aParams.mImageContainer),
+ mKnowsCompositor(aParams.mKnowsCompositor),
+ mCrashHelper(aParams.mCrashHelper),
+ mUseNullDecoder(aParams.mUseNullDecoder),
+ mNoWrapper(aParams.mNoWrapper),
+ mType(aParams.mType),
+ mOnWaitingForKeyEvent(aParams.mOnWaitingForKeyEvent),
+ mOptions(aParams.mOptions),
+ mRate(aParams.mRate),
+ mMediaEngineId(aParams.mMediaEngineId),
+ mTrackingId(aParams.mTrackingId) {}
+
+ template <typename T1, typename... Ts>
+ CreateDecoderParams(const TrackInfo& aConfig, T1&& a1, Ts&&... args)
+ : mConfig(aConfig) {
+ Set(std::forward<T1>(a1), std::forward<Ts>(args)...);
+ }
+
+ template <typename T1, typename... Ts>
+ CreateDecoderParams(const CreateDecoderParams& aParams, T1&& a1, Ts&&... args)
+ : CreateDecoderParams(aParams) {
+ Set(std::forward<T1>(a1), std::forward<Ts>(args)...);
+ }
+
+ const VideoInfo& VideoConfig() const {
+ MOZ_ASSERT(mConfig.IsVideo());
+ return *mConfig.GetAsVideoInfo();
+ }
+
+ const AudioInfo& AudioConfig() const {
+ MOZ_ASSERT(mConfig.IsAudio());
+ return *mConfig.GetAsAudioInfo();
+ }
+
+ layers::LayersBackend GetLayersBackend() const {
+ if (mKnowsCompositor) {
+ return mKnowsCompositor->GetCompositorBackendType();
+ }
+ return layers::LayersBackend::LAYERS_NONE;
+ }
+
+ // CreateDecoderParams is a MOZ_STACK_CLASS, it is only used to
+ // simplify the passing of arguments to Create*Decoder.
+ // It is safe to use references and raw pointers.
+ const TrackInfo& mConfig;
+ layers::ImageContainer* mImageContainer = nullptr;
+ MediaResult* mError = nullptr;
+ layers::KnowsCompositor* mKnowsCompositor = nullptr;
+ GMPCrashHelper* mCrashHelper = nullptr;
+ media::UseNullDecoder mUseNullDecoder;
+ media::NoWrapper mNoWrapper;
+ TrackInfo::TrackType mType = TrackInfo::kUndefinedTrack;
+ std::function<MediaEventProducer<TrackInfo::TrackType>*()>
+ mOnWaitingForKeyEvent;
+ OptionSet mOptions = OptionSet(Option::Default);
+ media::VideoFrameRate mRate;
+ // Used on Windows when the MF media engine playback is enabled.
+ Maybe<uint64_t> mMediaEngineId;
+ Maybe<TrackingId> mTrackingId;
+
+ private:
+ void Set(layers::ImageContainer* aImageContainer) {
+ mImageContainer = aImageContainer;
+ }
+ void Set(MediaResult* aError) { mError = aError; }
+ void Set(GMPCrashHelper* aCrashHelper) { mCrashHelper = aCrashHelper; }
+ void Set(UseNullDecoder aUseNullDecoder) {
+ mUseNullDecoder = aUseNullDecoder;
+ }
+ void Set(NoWrapper aNoWrapper) { mNoWrapper = aNoWrapper; }
+ void Set(OptionSet aOptions) { mOptions = aOptions; }
+ void Set(VideoFrameRate aRate) { mRate = aRate; }
+ void Set(layers::KnowsCompositor* aKnowsCompositor) {
+ if (aKnowsCompositor) {
+ mKnowsCompositor = aKnowsCompositor;
+ MOZ_ASSERT(aKnowsCompositor->IsThreadSafe());
+ }
+ }
+ void Set(TrackInfo::TrackType aType) { mType = aType; }
+ void Set(std::function<MediaEventProducer<TrackInfo::TrackType>*()>&&
+ aOnWaitingForKey) {
+ mOnWaitingForKeyEvent = std::move(aOnWaitingForKey);
+ }
+ void Set(const std::function<MediaEventProducer<TrackInfo::TrackType>*()>&
+ aOnWaitingForKey) {
+ mOnWaitingForKeyEvent = aOnWaitingForKey;
+ }
+ void Set(const Maybe<uint64_t>& aMediaEngineId) {
+ mMediaEngineId = aMediaEngineId;
+ }
+ void Set(const Maybe<TrackingId>& aTrackingId) { mTrackingId = aTrackingId; }
+ void Set(const CreateDecoderParams& aParams) {
+ // Set all but mTrackInfo;
+ mImageContainer = aParams.mImageContainer;
+ mError = aParams.mError;
+ mKnowsCompositor = aParams.mKnowsCompositor;
+ mCrashHelper = aParams.mCrashHelper;
+ mUseNullDecoder = aParams.mUseNullDecoder;
+ mNoWrapper = aParams.mNoWrapper;
+ mType = aParams.mType;
+ mOnWaitingForKeyEvent = aParams.mOnWaitingForKeyEvent;
+ mOptions = aParams.mOptions;
+ mRate = aParams.mRate;
+ mMediaEngineId = aParams.mMediaEngineId;
+ mTrackingId = aParams.mTrackingId;
+ }
+ template <typename T1, typename T2, typename... Ts>
+ void Set(T1&& a1, T2&& a2, Ts&&... args) {
+ Set(std::forward<T1>(a1));
+ Set(std::forward<T2>(a2), std::forward<Ts>(args)...);
+ }
+};
+
+struct MOZ_STACK_CLASS SupportDecoderParams final {
+ using Option = media::Option;
+ using OptionSet = media::OptionSet;
+ using UseNullDecoder = media::UseNullDecoder;
+ using NoWrapper = media::NoWrapper;
+ using VideoFrameRate = media::VideoFrameRate;
+
+ explicit SupportDecoderParams(const TrackInfo& aConfig) : mConfig(aConfig) {}
+
+ explicit SupportDecoderParams(const CreateDecoderParams& aParams)
+ : mConfig(aParams.mConfig),
+ mError(aParams.mError),
+ mKnowsCompositor(aParams.mKnowsCompositor),
+ mUseNullDecoder(aParams.mUseNullDecoder),
+ mNoWrapper(aParams.mNoWrapper),
+ mOptions(aParams.mOptions),
+ mRate(aParams.mRate),
+ mMediaEngineId(aParams.mMediaEngineId) {}
+
+ template <typename T1, typename... Ts>
+ SupportDecoderParams(const TrackInfo& aConfig, T1&& a1, Ts&&... args)
+ : mConfig(aConfig) {
+ Set(std::forward<T1>(a1), std::forward<Ts>(args)...);
+ }
+
+ const nsCString& MimeType() const { return mConfig.mMimeType; }
+
+ const TrackInfo& mConfig;
+ DecoderDoctorDiagnostics* mDiagnostics = nullptr;
+ MediaResult* mError = nullptr;
+ RefPtr<layers::KnowsCompositor> mKnowsCompositor;
+ UseNullDecoder mUseNullDecoder;
+ NoWrapper mNoWrapper;
+ OptionSet mOptions = OptionSet(Option::Default);
+ VideoFrameRate mRate;
+ Maybe<uint64_t> mMediaEngineId;
+
+ private:
+ void Set(DecoderDoctorDiagnostics* aDiagnostics) {
+ mDiagnostics = aDiagnostics;
+ }
+ void Set(MediaResult* aError) { mError = aError; }
+ void Set(media::UseNullDecoder aUseNullDecoder) {
+ mUseNullDecoder = aUseNullDecoder;
+ }
+ void Set(media::NoWrapper aNoWrapper) { mNoWrapper = aNoWrapper; }
+ void Set(media::OptionSet aOptions) { mOptions = aOptions; }
+ void Set(media::VideoFrameRate aRate) { mRate = aRate; }
+ void Set(layers::KnowsCompositor* aKnowsCompositor) {
+ if (aKnowsCompositor) {
+ mKnowsCompositor = aKnowsCompositor;
+ MOZ_ASSERT(aKnowsCompositor->IsThreadSafe());
+ }
+ }
+ void Set(const Maybe<uint64_t>& aMediaEngineId) {
+ mMediaEngineId = aMediaEngineId;
+ }
+
+ template <typename T1, typename T2, typename... Ts>
+ void Set(T1&& a1, T2&& a2, Ts&&... args) {
+ Set(std::forward<T1>(a1));
+ Set(std::forward<T2>(a2), std::forward<Ts>(args)...);
+ }
+};
+
+// Used for IPDL serialization.
+// The 'value' have to be the biggest enum from CreateDecoderParams::Option.
+template <>
+struct MaxEnumValue<::mozilla::CreateDecoderParams::Option> {
+ static constexpr unsigned int value =
+ static_cast<unsigned int>(CreateDecoderParams::Option::SENTINEL);
+};
+
+// The PlatformDecoderModule interface is used by the MediaFormatReader to
+// abstract access to decoders provided by various
+// platforms.
+// Each platform (Windows, MacOSX, Linux, B2G etc) must implement a
+// PlatformDecoderModule to provide access to its decoders in order to get
+// decompressed H.264/AAC from the MediaFormatReader.
+//
+// Decoding is asynchronous, and should be performed on the task queue
+// provided if the underlying platform isn't already exposing an async API.
+//
+// A cross-platform decoder module that discards input and produces "blank"
+// output samples exists for testing, and is created when the pref
+// "media.use-blank-decoder" is true.
+
+class PlatformDecoderModule {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(PlatformDecoderModule)
+
+ // Perform any per-instance initialization.
+ // This is called on the decode task queue.
+ virtual nsresult Startup() { return NS_OK; }
+
+ // Indicates if the PlatformDecoderModule supports decoding of aMimeType,
+ // and whether or not hardware-accelerated decoding is supported.
+ // The answer to both SupportsMimeType and Supports doesn't guarantee that
+ // creation of a decoder will actually succeed.
+ virtual media::DecodeSupportSet SupportsMimeType(
+ const nsACString& aMimeType,
+ DecoderDoctorDiagnostics* aDiagnostics) const = 0;
+
+ virtual media::DecodeSupportSet Supports(
+ const SupportDecoderParams& aParams,
+ DecoderDoctorDiagnostics* aDiagnostics) const {
+ const TrackInfo& trackInfo = aParams.mConfig;
+ const media::DecodeSupportSet support =
+ SupportsMimeType(trackInfo.mMimeType, aDiagnostics);
+
+ // Bail early if we don't support this format at all
+ if (support == media::DecodeSupport::Unsupported) {
+ return support;
+ }
+
+ const auto* videoInfo = trackInfo.GetAsVideoInfo();
+
+ if (!videoInfo) {
+ // No video stream = software decode only
+ return media::DecodeSupport::SoftwareDecode;
+ }
+
+ // Check whether we support the desired color depth
+ if (!SupportsColorDepth(videoInfo->mColorDepth, aDiagnostics)) {
+ return media::DecodeSupport::Unsupported;
+ }
+ return support;
+ }
+
+ using CreateDecoderPromise = MozPromise<RefPtr<MediaDataDecoder>, MediaResult,
+ /* IsExclusive = */ true>;
+
+ protected:
+ PlatformDecoderModule() = default;
+ virtual ~PlatformDecoderModule() = default;
+
+ friend class MediaChangeMonitor;
+ friend class PDMFactory;
+ friend class EMEDecoderModule;
+ friend class RemoteDecoderModule;
+
+ // Indicates if the PlatformDecoderModule supports decoding of aColorDepth.
+ // Should override this method when the platform can support color depth != 8.
+ virtual bool SupportsColorDepth(
+ gfx::ColorDepth aColorDepth,
+ DecoderDoctorDiagnostics* aDiagnostics) const {
+ return aColorDepth == gfx::ColorDepth::COLOR_8;
+ }
+
+ // Creates a Video decoder. The layers backend is passed in so that
+ // decoders can determine whether hardware accelerated decoding can be used.
+ // On Windows the task queue's threads in have MSCOM initialized with
+ // COINIT_MULTITHREADED.
+ // Returns nullptr if the decoder can't be created.
+ // It is not safe to store a reference to aParams or aParams.mConfig as the
+ // object isn't guaranteed to live after the call.
+ // CreateVideoDecoder may need to make additional checks if the
+ // CreateDecoderParams argument is actually supported and return nullptr if
+ // not to allow for fallback PDMs to be tried.
+ virtual already_AddRefed<MediaDataDecoder> CreateVideoDecoder(
+ const CreateDecoderParams& aParams) = 0;
+
+ // Creates an Audio decoder with the specified properties.
+ // Returns nullptr if the decoder can't be created.
+ // On Windows the task queue's threads in have MSCOM initialized with
+ // COINIT_MULTITHREADED.
+ // It is not safe to store a reference to aParams or aParams.mConfig as the
+ // object isn't guaranteed to live after the call.
+ // CreateAudioDecoder may need to make additional checks if the
+ // CreateDecoderParams argument is actually supported and return nullptr if
+ // not to allow for fallback PDMs to be tried.
+ virtual already_AddRefed<MediaDataDecoder> CreateAudioDecoder(
+ const CreateDecoderParams& aParams) = 0;
+
+ // Asychronously create a decoder.
+ virtual RefPtr<CreateDecoderPromise> AsyncCreateDecoder(
+ const CreateDecoderParams& aParams);
+};
+
+DDLoggedTypeDeclName(MediaDataDecoder);
+
+// MediaDataDecoder is the interface exposed by decoders created by the
+// PlatformDecoderModule's Create*Decoder() functions. The type of
+// media data that the decoder accepts as valid input and produces as
+// output is determined when the MediaDataDecoder is created.
+//
+// Unless otherwise noted, all functions are only called on the decode task
+// queue. An exception is the MediaDataDecoder in
+// MediaFormatReader::IsVideoAccelerated() for which all calls (Init(),
+// IsHardwareAccelerated(), and Shutdown()) are from the main thread.
+//
+// Don't block inside these functions, unless it's explicitly noted that you
+// should (like in Flush()).
+//
+// Decoding is done asynchronously.
+class MediaDataDecoder : public DecoderDoctorLifeLogger<MediaDataDecoder> {
+ protected:
+ virtual ~MediaDataDecoder() = default;
+
+ public:
+ typedef TrackInfo::TrackType TrackType;
+ typedef nsTArray<RefPtr<MediaData>> DecodedData;
+ typedef MozPromise<TrackType, MediaResult, /* IsExclusive = */ true>
+ InitPromise;
+ typedef MozPromise<DecodedData, MediaResult, /* IsExclusive = */ true>
+ DecodePromise;
+ typedef MozPromise<bool, MediaResult, /* IsExclusive = */ true> FlushPromise;
+
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+ // Initialize the decoder. The decoder should be ready to decode once
+ // the promise resolves. The decoder should do any initialization here, rather
+ // than in its constructor or PlatformDecoderModule::Create*Decoder(),
+ // so that if the MediaFormatReader needs to shutdown during initialization,
+ // it can call Shutdown() to cancel this operation. Any initialization
+ // that requires blocking the calling thread in this function *must*
+ // be done here so that it can be canceled by calling Shutdown()!
+ // Methods Decode, DecodeBatch, Drain, Flush, Shutdown are guaranteed to be
+ // called on the thread where Init() first ran.
+ virtual RefPtr<InitPromise> Init() = 0;
+
+ // Inserts a sample into the decoder's decode pipeline. The DecodePromise will
+ // be resolved with the decoded MediaData. In case the decoder needs more
+ // input, the DecodePromise may be resolved with an empty array of samples to
+ // indicate that Decode should be called again before a MediaData is returned.
+ virtual RefPtr<DecodePromise> Decode(MediaRawData* aSample) = 0;
+
+ // This could probably be implemented as a wrapper that takes a
+ // generic MediaDataDecoder and manages batching as needed. For now
+ // only AudioTrimmer with RemoteMediaDataDecoder supports batch
+ // decoding.
+ // Inserts an array of samples into the decoder's decode pipeline. The
+ // DecodePromise will be resolved with the decoded MediaData. In case
+ // the decoder needs more input, the DecodePromise may be resolved
+ // with an empty array of samples to indicate that Decode should be
+ // called again before a MediaData is returned.
+ virtual bool CanDecodeBatch() const { return false; }
+ virtual RefPtr<DecodePromise> DecodeBatch(
+ nsTArray<RefPtr<MediaRawData>>&& aSamples) {
+ MOZ_CRASH("DecodeBatch not implemented yet");
+ return MediaDataDecoder::DecodePromise::CreateAndReject(
+ NS_ERROR_DOM_MEDIA_DECODE_ERR, __func__);
+ }
+
+ // Causes all complete samples in the pipeline that can be decoded to be
+ // output. If the decoder can't produce samples from the current output,
+ // it drops the input samples. The decoder may be holding onto samples
+ // that are required to decode samples that it expects to get in future.
+ // This is called when the demuxer reaches end of stream.
+ // This function is asynchronous.
+ // The MediaDataDecoder shall resolve the pending DecodePromise with drained
+ // samples. Drain will be called multiple times until the resolved
+ // DecodePromise is empty which indicates that there are no more samples to
+ // drain.
+ virtual RefPtr<DecodePromise> Drain() = 0;
+
+ // Causes all samples in the decoding pipeline to be discarded. When this
+ // promise resolves, the decoder must be ready to accept new data for
+ // decoding. This function is called when the demuxer seeks, before decoding
+ // resumes after the seek. The current DecodePromise if any shall be rejected
+ // with NS_ERROR_DOM_MEDIA_CANCELED
+ virtual RefPtr<FlushPromise> Flush() = 0;
+
+ // Cancels all init/decode/drain operations, and shuts down the decoder. The
+ // platform decoder should clean up any resources it's using and release
+ // memory etc. The shutdown promise will be resolved once the decoder has
+ // completed shutdown. The reader calls Flush() before calling Shutdown(). The
+ // reader will delete the decoder once the promise is resolved.
+ // The ShutdownPromise must only ever be resolved.
+ // Shutdown() may not be called if init hasn't been called first. It is
+ // possible under some circumstances for the decoder to be deleted without
+ // Init having been called first.
+ virtual RefPtr<ShutdownPromise> Shutdown() = 0;
+
+ // Called from the state machine task queue or main thread. Decoder needs to
+ // decide whether or not hardware acceleration is supported after creating.
+ // It doesn't need to call Init() before calling this function.
+ virtual bool IsHardwareAccelerated(nsACString& aFailureReason) const {
+ return false;
+ }
+
+ // Return the name of the MediaDataDecoder, only used for decoding.
+ // May be accessed in a non thread-safe fashion.
+ virtual nsCString GetDescriptionName() const = 0;
+
+ virtual nsCString GetProcessName() const {
+ nsCString rv = nsCString(XRE_GetProcessTypeString());
+ if (XRE_IsUtilityProcess()) {
+ rv += "+"_ns + mozilla::ipc::GetChildAudioActorName();
+ }
+ return rv;
+ };
+ virtual nsCString GetCodecName() const = 0;
+
+ // Set a hint of seek target time to decoder. Decoder will drop any decoded
+ // data which pts is smaller than this value. This threshold needs to be clear
+ // after reset decoder. To clear it explicitly, call this method with
+ // TimeUnit::Invalid().
+ // Decoder may not honor this value. However, it'd be better that
+ // video decoder implements this API to improve seek performance.
+ // Note: it should be called before Input() or after Flush().
+ virtual void SetSeekThreshold(const media::TimeUnit& aTime) {}
+
+ // When playing adaptive playback, recreating an Android video decoder will
+ // cause the transition not smooth during resolution change.
+ // Reuse the decoder if the decoder support recycling.
+ // Currently, only Android video decoder will return true.
+ virtual bool SupportDecoderRecycling() const { return false; }
+
+ enum class ConversionRequired {
+ kNeedNone = 0,
+ kNeedAVCC = 1,
+ kNeedAnnexB = 2,
+ };
+
+ // Indicates that the decoder requires a specific format.
+ // The demuxed data will be converted accordingly before feeding it to
+ // Decode().
+ virtual ConversionRequired NeedsConversion() const {
+ return ConversionRequired::kNeedNone;
+ }
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/platforms/PlatformEncoderModule.h b/dom/media/platforms/PlatformEncoderModule.h
new file mode 100644
index 0000000000..1dd21330db
--- /dev/null
+++ b/dom/media/platforms/PlatformEncoderModule.h
@@ -0,0 +1,408 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(PlatformEncoderModule_h_)
+# define PlatformEncoderModule_h_
+
+# include "MP4Decoder.h"
+# include "MediaData.h"
+# include "MediaInfo.h"
+# include "MediaResult.h"
+# include "mozilla/Attributes.h"
+# include "mozilla/Maybe.h"
+# include "mozilla/MozPromise.h"
+# include "mozilla/RefPtr.h"
+# include "mozilla/TaskQueue.h"
+# include "mozilla/dom/ImageBitmapBinding.h"
+# include "nsISupportsImpl.h"
+# include "VPXDecoder.h"
+
+namespace mozilla {
+
+class MediaDataEncoder;
+struct CreateEncoderParams;
+
+class PlatformEncoderModule {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(PlatformEncoderModule)
+
+ virtual already_AddRefed<MediaDataEncoder> CreateVideoEncoder(
+ const CreateEncoderParams& aParams,
+ const bool aHardwareNotAllowed) const {
+ return nullptr;
+ };
+
+ virtual already_AddRefed<MediaDataEncoder> CreateAudioEncoder(
+ const CreateEncoderParams& aParams) const {
+ return nullptr;
+ };
+
+ // Indicates if the PlatformDecoderModule supports encoding of aMimeType.
+ virtual bool SupportsMimeType(const nsACString& aMimeType) const = 0;
+
+ protected:
+ PlatformEncoderModule() = default;
+ virtual ~PlatformEncoderModule() = default;
+};
+
+class MediaDataEncoder {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaDataEncoder)
+
+ enum class Usage {
+ Realtime, // For WebRTC
+ Record // For MediaRecoder
+ };
+
+ enum class CodecType {
+ _BeginVideo_,
+ H264,
+ VP8,
+ VP9,
+ _EndVideo_,
+ _BeginAudio_ = _EndVideo_,
+ Opus,
+ G722,
+ _EndAudio_,
+ Unknown,
+ };
+
+ struct H264Specific final {
+ enum class ProfileLevel { BaselineAutoLevel, MainAutoLevel };
+
+ const ProfileLevel mProfileLevel;
+
+ explicit H264Specific(const ProfileLevel aProfileLevel)
+ : mProfileLevel(aProfileLevel) {}
+ };
+
+ struct OpusSpecific final {
+ enum class Application { Voip, Audio, RestricedLowDelay };
+
+ const Application mApplication;
+ const uint8_t mComplexity; // from 0-10
+
+ OpusSpecific(const Application aApplication, const uint8_t aComplexity)
+ : mApplication(aApplication), mComplexity(aComplexity) {
+ MOZ_ASSERT(mComplexity <= 10);
+ }
+ };
+
+// From webrtc::VideoCodecVP8. mResilience is a boolean value because while
+// VP8ResilienceMode has 3 values, kResilientFrames is not supported.
+# define VPX_COMMON_SETTINGS \
+ const Complexity mComplexity; \
+ const bool mResilience; \
+ const uint8_t mNumTemporalLayers; \
+ const bool mDenoising; \
+ const bool mAutoResize; \
+ const bool mFrameDropping;
+
+// See webrtc::VideoEncoder::GetDefaultVp(8|9)Settings().
+# define VPX_COMMON_DEFAULTS(resize) \
+ mComplexity(Complexity::Normal), mResilience(true), mNumTemporalLayers(1), \
+ mDenoising(true), mAutoResize(resize), mFrameDropping(0)
+
+ struct VPXSpecific final {
+ enum class Complexity { Normal, High, Higher, Max };
+ struct VP8 final {
+ VPX_COMMON_SETTINGS
+ // Ignore webrtc::VideoCodecVP8::errorConcealmentOn,
+ // for it's always false in the codebase (except libwebrtc test cases).
+
+ VP8() : VPX_COMMON_DEFAULTS(false /* auto resize */) {}
+ VP8(const Complexity aComplexity, const bool aResilience,
+ const uint8_t aNumTemporalLayers, const bool aDenoising,
+ const bool aAutoResize, const bool aFrameDropping)
+ : mComplexity(aComplexity),
+ mResilience(aResilience),
+ mNumTemporalLayers(aNumTemporalLayers),
+ mDenoising(aDenoising),
+ mAutoResize(aAutoResize),
+ mFrameDropping(aFrameDropping) {}
+ };
+
+ struct VP9 final {
+ VPX_COMMON_SETTINGS
+ // From webrtc::VideoCodecVP9.
+ bool mAdaptiveQp;
+ uint8_t mNumSpatialLayers;
+ bool mFlexible;
+
+ VP9()
+ : VPX_COMMON_DEFAULTS(true /* auto resize */),
+ mAdaptiveQp(true),
+ mNumSpatialLayers(1),
+ mFlexible(false) {}
+ VP9(const Complexity aComplexity, const bool aResilience,
+ const uint8_t aNumTemporalLayers, const bool aDenoising,
+ const bool aAutoResize, const bool aFrameDropping,
+ const bool aAdaptiveQp, const uint8_t aNumSpatialLayers,
+ const bool aFlexible)
+ : mComplexity(aComplexity),
+ mResilience(aResilience),
+ mNumTemporalLayers(aNumTemporalLayers),
+ mDenoising(aDenoising),
+ mAutoResize(aAutoResize),
+ mFrameDropping(aFrameDropping),
+ mAdaptiveQp(aAdaptiveQp),
+ mNumSpatialLayers(aNumSpatialLayers),
+ mFlexible(aFlexible) {}
+ };
+
+ VPXSpecific() = delete;
+ };
+
+ static bool IsVideo(const CodecType aCodec) {
+ return aCodec > CodecType::_BeginVideo_ && aCodec < CodecType::_EndVideo_;
+ }
+ static bool IsAudio(const CodecType aCodec) {
+ return aCodec > CodecType::_BeginAudio_ && aCodec < CodecType::_EndAudio_;
+ }
+
+ using PixelFormat = dom::ImageBitmapFormat;
+ // Sample rate for audio, framerate for video, and bitrate for both.
+ using Rate = uint32_t;
+
+ using InitPromise =
+ MozPromise<TrackInfo::TrackType, MediaResult, /* IsExclusive = */ true>;
+ using EncodedData = nsTArray<RefPtr<MediaRawData>>;
+ using EncodePromise =
+ MozPromise<EncodedData, MediaResult, /* IsExclusive = */ true>;
+
+ // Initialize the encoder. It should be ready to encode once the returned
+ // promise resolves. The encoder should do any initialization here, rather
+ // than in its constructor or PlatformEncoderModule::Create*Encoder(),
+ // so that if the client needs to shutdown during initialization,
+ // it can call Shutdown() to cancel this operation. Any initialization
+ // that requires blocking the calling thread in this function *must*
+ // be done here so that it can be canceled by calling Shutdown()!
+ virtual RefPtr<InitPromise> Init() = 0;
+
+ // Inserts a sample into the encoder's encode pipeline. The EncodePromise it
+ // returns will be resolved with already encoded MediaRawData at the moment,
+ // or empty when there is none available yet.
+ virtual RefPtr<EncodePromise> Encode(const MediaData* aSample) = 0;
+
+ // Causes all complete samples in the pipeline that can be encoded to be
+ // output. It indicates that there is no more input sample to insert.
+ // This function is asynchronous.
+ // The MediaDataEncoder shall resolve the pending EncodePromise with drained
+ // samples. Drain will be called multiple times until the resolved
+ // EncodePromise is empty which indicates that there are no more samples to
+ // drain.
+ virtual RefPtr<EncodePromise> Drain() = 0;
+
+ // Cancels all init/encode/drain operations, and shuts down the encoder. The
+ // platform encoder should clean up any resources it's using and release
+ // memory etc. The shutdown promise will be resolved once the encoder has
+ // completed shutdown. The client will delete the decoder once the promise is
+ // resolved.
+ // The ShutdownPromise must only ever be resolved.
+ virtual RefPtr<ShutdownPromise> Shutdown() = 0;
+
+ virtual RefPtr<GenericPromise> SetBitrate(Rate aBitsPerSec) {
+ return GenericPromise::CreateAndResolve(true, __func__);
+ }
+
+ // Decoder needs to decide whether or not hardware acceleration is supported
+ // after creating. It doesn't need to call Init() before calling this
+ // function.
+ virtual bool IsHardwareAccelerated(nsACString& aFailureReason) const {
+ return false;
+ }
+
+ // Return the name of the MediaDataEncoder, only used for encoding.
+ // May be accessed in a non thread-safe fashion.
+ virtual nsCString GetDescriptionName() const = 0;
+
+ friend class PlatformEncoderModule;
+
+ protected:
+ template <typename T>
+ struct BaseConfig {
+ const CodecType mCodecType;
+ const Usage mUsage;
+ const Rate mBitsPerSec;
+ Maybe<T> mCodecSpecific;
+
+ void SetCodecSpecific(const T& aCodecSpecific) {
+ mCodecSpecific.emplace(aCodecSpecific);
+ }
+
+ protected:
+ BaseConfig(const CodecType aCodecType, const Usage aUsage,
+ const Rate aBitsPerSec)
+ : mCodecType(aCodecType), mUsage(aUsage), mBitsPerSec(aBitsPerSec) {}
+
+ virtual ~BaseConfig() = default;
+ };
+
+ template <typename T>
+ struct VideoConfig final : public BaseConfig<T> {
+ const gfx::IntSize mSize;
+ const PixelFormat mSourcePixelFormat;
+ const uint8_t mFramerate;
+ const size_t mKeyframeInterval;
+
+ VideoConfig(const CodecType aCodecType, const Usage aUsage,
+ const gfx::IntSize& aSize, const PixelFormat aSourcePixelFormat,
+ const uint8_t aFramerate, const size_t aKeyframeInterval,
+ const Rate aBitrate)
+ : BaseConfig<T>(aCodecType, aUsage, aBitrate),
+ mSize(aSize),
+ mSourcePixelFormat(aSourcePixelFormat),
+ mFramerate(aFramerate),
+ mKeyframeInterval(aKeyframeInterval) {}
+ };
+
+ template <typename T>
+ struct AudioConfig final : public BaseConfig<T> {
+ const uint8_t mNumChannels;
+ const Rate mSampleRate;
+
+ AudioConfig(const CodecType aCodecType, const Usage aUsage,
+ const Rate aBitrate, const Rate aSampleRate,
+ const uint8_t aNumChannels)
+ : BaseConfig<T>(aCodecType, aUsage, aBitrate),
+ mNumChannels(aNumChannels),
+ mSampleRate(aSampleRate) {}
+ };
+
+ virtual ~MediaDataEncoder() = default;
+
+ public:
+ using H264Config = VideoConfig<H264Specific>;
+ using VP8Config = VideoConfig<VPXSpecific::VP8>;
+ using VP9Config = VideoConfig<VPXSpecific::VP9>;
+};
+
+struct MOZ_STACK_CLASS CreateEncoderParams final {
+ union CodecSpecific {
+ MediaDataEncoder::H264Specific mH264;
+ MediaDataEncoder::OpusSpecific mOpus;
+ MediaDataEncoder::VPXSpecific::VP8 mVP8;
+ MediaDataEncoder::VPXSpecific::VP9 mVP9;
+
+ explicit CodecSpecific(const MediaDataEncoder::H264Specific&& aH264)
+ : mH264(aH264) {}
+ explicit CodecSpecific(const MediaDataEncoder::OpusSpecific&& aOpus)
+ : mOpus(aOpus) {}
+ explicit CodecSpecific(const MediaDataEncoder::VPXSpecific::VP8&& aVP8)
+ : mVP8(aVP8) {}
+ explicit CodecSpecific(const MediaDataEncoder::VPXSpecific::VP9&& aVP9)
+ : mVP9(aVP9) {}
+ };
+
+ CreateEncoderParams(const TrackInfo& aConfig,
+ const MediaDataEncoder::Usage aUsage,
+ const RefPtr<TaskQueue> aTaskQueue,
+ const MediaDataEncoder::PixelFormat aPixelFormat,
+ const uint8_t aFramerate, const size_t aKeyframeInterval,
+ const MediaDataEncoder::Rate aBitrate)
+ : mConfig(aConfig),
+ mUsage(aUsage),
+ mTaskQueue(aTaskQueue),
+ mPixelFormat(aPixelFormat),
+ mFramerate(aFramerate),
+ mKeyframeInterval(aKeyframeInterval),
+ mBitrate(aBitrate) {
+ MOZ_ASSERT(mTaskQueue);
+ }
+
+ template <typename... Ts>
+ CreateEncoderParams(const TrackInfo& aConfig,
+ const MediaDataEncoder::Usage aUsage,
+ const RefPtr<TaskQueue> aTaskQueue,
+ const MediaDataEncoder::PixelFormat aPixelFormat,
+ const uint8_t aFramerate, const size_t aKeyframeInterval,
+ const MediaDataEncoder::Rate aBitrate,
+ const Ts&&... aCodecSpecific)
+ : mConfig(aConfig),
+ mUsage(aUsage),
+ mTaskQueue(aTaskQueue),
+ mPixelFormat(aPixelFormat),
+ mFramerate(aFramerate),
+ mKeyframeInterval(aKeyframeInterval),
+ mBitrate(aBitrate) {
+ MOZ_ASSERT(mTaskQueue);
+ SetCodecSpecific(std::forward<const Ts>(aCodecSpecific)...);
+ }
+
+ template <typename T>
+ void SetCodecSpecific(const T&& aCodecSpecific) {
+ mCodecSpecific.emplace(std::forward<const T>(aCodecSpecific));
+ }
+
+ const MediaDataEncoder::H264Config ToH264Config() const {
+ const VideoInfo* info = mConfig.GetAsVideoInfo();
+ MOZ_ASSERT(info);
+
+ auto config = MediaDataEncoder::H264Config(
+ MediaDataEncoder::CodecType::H264, mUsage, info->mImage, mPixelFormat,
+ mFramerate, mKeyframeInterval, mBitrate);
+ if (mCodecSpecific) {
+ config.SetCodecSpecific(mCodecSpecific.ref().mH264);
+ }
+
+ return config;
+ }
+
+ const MediaDataEncoder::VP8Config ToVP8Config() const {
+ const VideoInfo* info = mConfig.GetAsVideoInfo();
+ MOZ_ASSERT(info);
+
+ auto config = MediaDataEncoder::VP8Config(
+ CodecTypeForMime(info->mMimeType), mUsage, info->mImage, mPixelFormat,
+ mFramerate, mKeyframeInterval, mBitrate);
+ if (mCodecSpecific) {
+ config.SetCodecSpecific(mCodecSpecific.ref().mVP8);
+ }
+ return config;
+ }
+
+ const MediaDataEncoder::VP9Config ToVP9Config() const {
+ const VideoInfo* info = mConfig.GetAsVideoInfo();
+ MOZ_ASSERT(info);
+
+ auto config = MediaDataEncoder::VP9Config(
+ CodecTypeForMime(info->mMimeType), mUsage, info->mImage, mPixelFormat,
+ mFramerate, mKeyframeInterval, mBitrate);
+ if (mCodecSpecific) {
+ config.SetCodecSpecific(mCodecSpecific.ref().mVP9);
+ }
+ return config;
+ }
+
+ static MediaDataEncoder::CodecType CodecTypeForMime(
+ const nsACString& aMimeType) {
+ if (MP4Decoder::IsH264(aMimeType)) {
+ return MediaDataEncoder::CodecType::H264;
+ } else if (VPXDecoder::IsVPX(aMimeType, VPXDecoder::VP8)) {
+ return MediaDataEncoder::CodecType::VP8;
+ } else if (VPXDecoder::IsVPX(aMimeType, VPXDecoder::VP9)) {
+ return MediaDataEncoder::CodecType::VP9;
+ } else {
+ MOZ_ASSERT_UNREACHABLE("Unsupported Mimetype");
+ return MediaDataEncoder::CodecType::Unknown;
+ }
+ }
+
+ const TrackInfo& mConfig;
+ const MediaDataEncoder::Usage mUsage;
+ const RefPtr<TaskQueue> mTaskQueue;
+ const MediaDataEncoder::PixelFormat mPixelFormat;
+ const uint8_t mFramerate;
+ const size_t mKeyframeInterval;
+ const MediaDataEncoder::Rate mBitrate;
+ Maybe<CodecSpecific> mCodecSpecific;
+
+ private:
+};
+
+} // namespace mozilla
+
+#endif /* PlatformEncoderModule_h_ */
diff --git a/dom/media/platforms/ReorderQueue.h b/dom/media/platforms/ReorderQueue.h
new file mode 100644
index 0000000000..ada6597f7b
--- /dev/null
+++ b/dom/media/platforms/ReorderQueue.h
@@ -0,0 +1,28 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Queue for ordering decoded video frames by presentation time.
+// Decoders often return frames out of order, which we need to
+// buffer so we can forward them in correct presentation order.
+
+#ifndef mozilla_ReorderQueue_h
+#define mozilla_ReorderQueue_h
+
+#include <MediaData.h>
+#include <nsTPriorityQueue.h>
+
+namespace mozilla {
+
+struct ReorderQueueComparator {
+ bool LessThan(MediaData* const& a, MediaData* const& b) const {
+ return a->mTime < b->mTime;
+ }
+};
+
+typedef nsTPriorityQueue<RefPtr<MediaData>, ReorderQueueComparator>
+ ReorderQueue;
+
+} // namespace mozilla
+
+#endif // mozilla_ReorderQueue_h
diff --git a/dom/media/platforms/SimpleMap.h b/dom/media/platforms/SimpleMap.h
new file mode 100644
index 0000000000..c26bff1e9a
--- /dev/null
+++ b/dom/media/platforms/SimpleMap.h
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_SimpleMap_h
+#define mozilla_SimpleMap_h
+
+#include "mozilla/Mutex.h"
+#include "nsTArray.h"
+
+#include <utility>
+
+namespace mozilla {
+
+template <typename T>
+class SimpleMap {
+ public:
+ typedef std::pair<int64_t, T> Element;
+
+ SimpleMap() : mMutex("SimpleMap") {}
+
+ // Insert Key and Value pair at the end of our map.
+ void Insert(int64_t aKey, const T& aValue) {
+ MutexAutoLock lock(mMutex);
+ mMap.AppendElement(std::make_pair(aKey, aValue));
+ }
+ // Sets aValue matching aKey and remove it from the map if found.
+ // The element returned is the first one found.
+ // Returns true if found, false otherwise.
+ bool Find(int64_t aKey, T& aValue) {
+ MutexAutoLock lock(mMutex);
+ for (uint32_t i = 0; i < mMap.Length(); i++) {
+ Element& element = mMap[i];
+ if (element.first == aKey) {
+ aValue = element.second;
+ mMap.RemoveElementAt(i);
+ return true;
+ }
+ }
+ return false;
+ }
+ // Remove all elements of the map.
+ void Clear() {
+ MutexAutoLock lock(mMutex);
+ mMap.Clear();
+ }
+
+ private:
+ Mutex mMutex MOZ_UNANNOTATED; // To protect mMap.
+ AutoTArray<Element, 16> mMap;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_SimpleMap_h
diff --git a/dom/media/platforms/agnostic/AOMDecoder.cpp b/dom/media/platforms/agnostic/AOMDecoder.cpp
new file mode 100644
index 0000000000..d6b0576cd2
--- /dev/null
+++ b/dom/media/platforms/agnostic/AOMDecoder.cpp
@@ -0,0 +1,1066 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AOMDecoder.h"
+
+#include <algorithm>
+
+#include "BitWriter.h"
+#include "BitReader.h"
+#include "ImageContainer.h"
+#include "MediaResult.h"
+#include "TimeUnits.h"
+#include "aom/aom_image.h"
+#include "aom/aomdx.h"
+#include "gfx2DGlue.h"
+#include "gfxUtils.h"
+#include "mozilla/PodOperations.h"
+#include "mozilla/SyncRunnable.h"
+#include "mozilla/TaskQueue.h"
+#include "nsError.h"
+#include "nsThreadUtils.h"
+#include "prsystem.h"
+#include "VideoUtils.h"
+
+#undef LOG
+#define LOG(arg, ...) \
+ DDMOZ_LOG(sPDMLog, mozilla::LogLevel::Debug, "::%s: " arg, __func__, \
+ ##__VA_ARGS__)
+#define LOG_RESULT(code, message, ...) \
+ DDMOZ_LOG(sPDMLog, mozilla::LogLevel::Debug, "::%s: %s (code %d) " message, \
+ __func__, aom_codec_err_to_string(code), (int)code, ##__VA_ARGS__)
+#define LOGEX_RESULT(_this, code, message, ...) \
+ DDMOZ_LOGEX(_this, sPDMLog, mozilla::LogLevel::Debug, \
+ "::%s: %s (code %d) " message, __func__, \
+ aom_codec_err_to_string(code), (int)code, ##__VA_ARGS__)
+#define LOG_STATIC_RESULT(code, message, ...) \
+ MOZ_LOG(sPDMLog, mozilla::LogLevel::Debug, \
+ ("AOMDecoder::%s: %s (code %d) " message, __func__, \
+ aom_codec_err_to_string(code), (int)code, ##__VA_ARGS__))
+
+#define ASSERT_BYTE_ALIGNED(bitIO) MOZ_ASSERT((bitIO).BitCount() % 8 == 0)
+
+namespace mozilla {
+
+using namespace gfx;
+using namespace layers;
+using gfx::CICP::ColourPrimaries;
+using gfx::CICP::MatrixCoefficients;
+using gfx::CICP::TransferCharacteristics;
+
+static MediaResult InitContext(AOMDecoder& aAOMDecoder, aom_codec_ctx_t* aCtx,
+ const VideoInfo& aInfo) {
+ aom_codec_iface_t* dx = aom_codec_av1_dx();
+ if (!dx) {
+ return MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Couldn't get AV1 decoder interface."));
+ }
+
+ size_t decode_threads = 2;
+ if (aInfo.mDisplay.width >= 2048) {
+ decode_threads = 8;
+ } else if (aInfo.mDisplay.width >= 1024) {
+ decode_threads = 4;
+ }
+ decode_threads = std::min(decode_threads, GetNumberOfProcessors());
+
+ aom_codec_dec_cfg_t config;
+ PodZero(&config);
+ config.threads = static_cast<unsigned int>(decode_threads);
+ config.w = config.h = 0; // set after decode
+ config.allow_lowbitdepth = true;
+
+ aom_codec_flags_t flags = 0;
+
+ auto res = aom_codec_dec_init(aCtx, dx, &config, flags);
+ if (res != AOM_CODEC_OK) {
+ LOGEX_RESULT(&aAOMDecoder, res, "Codec initialization failed, res=%d",
+ int(res));
+ return MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("AOM error initializing AV1 decoder: %s",
+ aom_codec_err_to_string(res)));
+ }
+ return NS_OK;
+}
+
+AOMDecoder::AOMDecoder(const CreateDecoderParams& aParams)
+ : mImageContainer(aParams.mImageContainer),
+ mTaskQueue(TaskQueue::Create(
+ GetMediaThreadPool(MediaThreadType::PLATFORM_DECODER), "AOMDecoder")),
+ mInfo(aParams.VideoConfig()),
+ mTrackingId(aParams.mTrackingId) {
+ PodZero(&mCodec);
+}
+
+AOMDecoder::~AOMDecoder() = default;
+
+RefPtr<ShutdownPromise> AOMDecoder::Shutdown() {
+ RefPtr<AOMDecoder> self = this;
+ return InvokeAsync(mTaskQueue, __func__, [self]() {
+ auto res = aom_codec_destroy(&self->mCodec);
+ if (res != AOM_CODEC_OK) {
+ LOGEX_RESULT(self.get(), res, "aom_codec_destroy");
+ }
+ return self->mTaskQueue->BeginShutdown();
+ });
+}
+
+RefPtr<MediaDataDecoder::InitPromise> AOMDecoder::Init() {
+ MediaResult rv = InitContext(*this, &mCodec, mInfo);
+ if (NS_FAILED(rv)) {
+ return AOMDecoder::InitPromise::CreateAndReject(rv, __func__);
+ }
+ return AOMDecoder::InitPromise::CreateAndResolve(TrackInfo::kVideoTrack,
+ __func__);
+}
+
+RefPtr<MediaDataDecoder::FlushPromise> AOMDecoder::Flush() {
+ return InvokeAsync(mTaskQueue, __func__, [this, self = RefPtr(this)]() {
+ mPerformanceRecorder.Record(std::numeric_limits<int64_t>::max());
+ return FlushPromise::CreateAndResolve(true, __func__);
+ });
+}
+
+// UniquePtr dtor wrapper for aom_image_t.
+struct AomImageFree {
+ void operator()(aom_image_t* img) { aom_img_free(img); }
+};
+
+RefPtr<MediaDataDecoder::DecodePromise> AOMDecoder::ProcessDecode(
+ MediaRawData* aSample) {
+ MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
+
+#if defined(DEBUG)
+ NS_ASSERTION(
+ IsKeyframe(*aSample) == aSample->mKeyframe,
+ "AOM Decode Keyframe error sample->mKeyframe and si.si_kf out of sync");
+#endif
+
+ MediaInfoFlag flag = MediaInfoFlag::None;
+ flag |= (aSample->mKeyframe ? MediaInfoFlag::KeyFrame
+ : MediaInfoFlag::NonKeyFrame);
+ flag |= MediaInfoFlag::SoftwareDecoding;
+ flag |= MediaInfoFlag::VIDEO_AV1;
+
+ mTrackingId.apply([&](const auto& aId) {
+ mPerformanceRecorder.Start(aSample->mTimecode.ToMicroseconds(),
+ "AOMDecoder"_ns, aId, flag);
+ });
+
+ if (aom_codec_err_t r = aom_codec_decode(&mCodec, aSample->Data(),
+ aSample->Size(), nullptr)) {
+ LOG_RESULT(r, "Decode error!");
+ return DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ RESULT_DETAIL("AOM error decoding AV1 sample: %s",
+ aom_codec_err_to_string(r))),
+ __func__);
+ }
+
+ aom_codec_iter_t iter = nullptr;
+ aom_image_t* img;
+ UniquePtr<aom_image_t, AomImageFree> img8;
+ DecodedData results;
+
+ while ((img = aom_codec_get_frame(&mCodec, &iter))) {
+ NS_ASSERTION(
+ img->fmt == AOM_IMG_FMT_I420 || img->fmt == AOM_IMG_FMT_I42016 ||
+ img->fmt == AOM_IMG_FMT_I444 || img->fmt == AOM_IMG_FMT_I44416,
+ "AV1 image format not I420 or I444");
+
+ // Chroma shifts are rounded down as per the decoding examples in the SDK
+ VideoData::YCbCrBuffer b;
+ b.mPlanes[0].mData = img->planes[0];
+ b.mPlanes[0].mStride = img->stride[0];
+ b.mPlanes[0].mHeight = img->d_h;
+ b.mPlanes[0].mWidth = img->d_w;
+ b.mPlanes[0].mSkip = 0;
+
+ b.mPlanes[1].mData = img->planes[1];
+ b.mPlanes[1].mStride = img->stride[1];
+ b.mPlanes[1].mSkip = 0;
+
+ b.mPlanes[2].mData = img->planes[2];
+ b.mPlanes[2].mStride = img->stride[2];
+ b.mPlanes[2].mSkip = 0;
+
+ if (img->fmt == AOM_IMG_FMT_I420 || img->fmt == AOM_IMG_FMT_I42016) {
+ b.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT;
+
+ b.mPlanes[1].mHeight = (img->d_h + 1) >> img->y_chroma_shift;
+ b.mPlanes[1].mWidth = (img->d_w + 1) >> img->x_chroma_shift;
+
+ b.mPlanes[2].mHeight = (img->d_h + 1) >> img->y_chroma_shift;
+ b.mPlanes[2].mWidth = (img->d_w + 1) >> img->x_chroma_shift;
+ } else if (img->fmt == AOM_IMG_FMT_I444 || img->fmt == AOM_IMG_FMT_I44416) {
+ b.mPlanes[1].mHeight = img->d_h;
+ b.mPlanes[1].mWidth = img->d_w;
+
+ b.mPlanes[2].mHeight = img->d_h;
+ b.mPlanes[2].mWidth = img->d_w;
+ } else {
+ LOG("AOM Unknown image format");
+ return DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ RESULT_DETAIL("AOM Unknown image format")),
+ __func__);
+ }
+
+ if (img->bit_depth == 10) {
+ b.mColorDepth = ColorDepth::COLOR_10;
+ } else if (img->bit_depth == 12) {
+ b.mColorDepth = ColorDepth::COLOR_12;
+ }
+
+ switch (img->mc) {
+ case AOM_CICP_MC_BT_601:
+ b.mYUVColorSpace = YUVColorSpace::BT601;
+ break;
+ case AOM_CICP_MC_BT_2020_NCL:
+ case AOM_CICP_MC_BT_2020_CL:
+ b.mYUVColorSpace = YUVColorSpace::BT2020;
+ break;
+ case AOM_CICP_MC_BT_709:
+ b.mYUVColorSpace = YUVColorSpace::BT709;
+ break;
+ default:
+ b.mYUVColorSpace = DefaultColorSpace({img->d_w, img->d_h});
+ break;
+ }
+ b.mColorRange = img->range == AOM_CR_FULL_RANGE ? ColorRange::FULL
+ : ColorRange::LIMITED;
+
+ switch (img->cp) {
+ case AOM_CICP_CP_BT_709:
+ b.mColorPrimaries = ColorSpace2::BT709;
+ break;
+ case AOM_CICP_CP_BT_2020:
+ b.mColorPrimaries = ColorSpace2::BT2020;
+ break;
+ default:
+ b.mColorPrimaries = ColorSpace2::BT709;
+ break;
+ }
+
+ RefPtr<VideoData> v;
+ v = VideoData::CreateAndCopyData(
+ mInfo, mImageContainer, aSample->mOffset, aSample->mTime,
+ aSample->mDuration, b, aSample->mKeyframe, aSample->mTimecode,
+ mInfo.ScaledImageRect(img->d_w, img->d_h), nullptr);
+
+ if (!v) {
+ LOG("Image allocation error source %ux%u display %ux%u picture %ux%u",
+ img->d_w, img->d_h, mInfo.mDisplay.width, mInfo.mDisplay.height,
+ mInfo.mImage.width, mInfo.mImage.height);
+ return DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__), __func__);
+ }
+ mPerformanceRecorder.Record(
+ aSample->mTimecode.ToMicroseconds(), [&](DecodeStage& aStage) {
+ aStage.SetResolution(mInfo.mImage.width, mInfo.mImage.height);
+ auto format = [&]() -> Maybe<DecodeStage::ImageFormat> {
+ switch (img->fmt) {
+ case AOM_IMG_FMT_I420:
+ case AOM_IMG_FMT_I42016:
+ return Some(DecodeStage::YUV420P);
+ case AOM_IMG_FMT_I444:
+ case AOM_IMG_FMT_I44416:
+ return Some(DecodeStage::YUV444P);
+ default:
+ return Nothing();
+ }
+ }();
+ format.apply([&](auto& aFmt) { aStage.SetImageFormat(aFmt); });
+ aStage.SetYUVColorSpace(b.mYUVColorSpace);
+ aStage.SetColorRange(b.mColorRange);
+ aStage.SetColorDepth(b.mColorDepth);
+ });
+ results.AppendElement(std::move(v));
+ }
+ return DecodePromise::CreateAndResolve(std::move(results), __func__);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> AOMDecoder::Decode(
+ MediaRawData* aSample) {
+ return InvokeAsync<MediaRawData*>(mTaskQueue, this, __func__,
+ &AOMDecoder::ProcessDecode, aSample);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> AOMDecoder::Drain() {
+ return InvokeAsync(mTaskQueue, __func__, [] {
+ return DecodePromise::CreateAndResolve(DecodedData(), __func__);
+ });
+}
+
+/* static */
+bool AOMDecoder::IsAV1(const nsACString& aMimeType) {
+ return aMimeType.EqualsLiteral("video/av1");
+}
+
+/* static */
+bool AOMDecoder::IsKeyframe(Span<const uint8_t> aBuffer) {
+ aom_codec_stream_info_t info;
+ PodZero(&info);
+
+ auto res = aom_codec_peek_stream_info(aom_codec_av1_dx(), aBuffer.Elements(),
+ aBuffer.Length(), &info);
+ if (res != AOM_CODEC_OK) {
+ LOG_STATIC_RESULT(
+ res, "couldn't get keyframe flag with aom_codec_peek_stream_info");
+ return false;
+ }
+
+ return bool(info.is_kf);
+}
+
+/* static */
+gfx::IntSize AOMDecoder::GetFrameSize(Span<const uint8_t> aBuffer) {
+ aom_codec_stream_info_t info;
+ PodZero(&info);
+
+ auto res = aom_codec_peek_stream_info(aom_codec_av1_dx(), aBuffer.Elements(),
+ aBuffer.Length(), &info);
+ if (res != AOM_CODEC_OK) {
+ LOG_STATIC_RESULT(
+ res, "couldn't get frame size with aom_codec_peek_stream_info");
+ }
+
+ return gfx::IntSize(info.w, info.h);
+}
+
+/* static */
+AOMDecoder::OBUIterator AOMDecoder::ReadOBUs(const Span<const uint8_t>& aData) {
+ return OBUIterator(aData);
+}
+
+void AOMDecoder::OBUIterator::UpdateNext() {
+ // If mGoNext is not set, we don't need to load a new OBU.
+ if (!mGoNext) {
+ return;
+ }
+ // Check if we've reached the end of the data. Allow mGoNext to stay true so
+ // that HasNext() will return false.
+ if (mPosition >= mData.Length()) {
+ return;
+ }
+ mGoNext = false;
+
+ // If retrieving the next OBU fails, reset the current OBU and set the
+ // position past the end of the data so that HasNext() returns false.
+ auto resetExit = MakeScopeExit([&]() {
+ mCurrent = OBUInfo();
+ mPosition = mData.Length();
+ });
+
+ auto subspan = mData.Subspan(mPosition, mData.Length() - mPosition);
+ BitReader br(subspan.Elements(), subspan.Length() * 8);
+ OBUInfo temp;
+
+ // AV1 spec available at:
+ // https://aomediacodec.github.io/av1-spec/
+ // or https://aomediacodec.github.io/av1-spec/av1-spec.pdf
+
+ // begin open_bitstream_unit( )
+ // https://aomediacodec.github.io/av1-spec/#general-obu-syntax
+
+ // begin obu_header( )
+ // https://aomediacodec.github.io/av1-spec/#obu-header-syntax
+ br.ReadBit(); // obu_forbidden_bit
+ temp.mType = static_cast<OBUType>(br.ReadBits(4));
+ if (!temp.IsValid()) {
+ // Non-fatal error, unknown OBUs can be skipped as long as the size field
+ // is properly specified.
+ NS_WARNING(nsPrintfCString("Encountered unknown OBU type (%" PRIu8
+ ", OBU may be invalid",
+ static_cast<uint8_t>(temp.mType))
+ .get());
+ }
+ temp.mExtensionFlag = br.ReadBit();
+ bool hasSizeField = br.ReadBit();
+ br.ReadBit(); // obu_reserved_1bit
+
+ // begin obu_extension_header( ) (5.3.3)
+ if (temp.mExtensionFlag) {
+ if (br.BitsLeft() < 8) {
+ mResult = MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ "Not enough bits left for an OBU extension header");
+ return;
+ }
+ br.ReadBits(3); // temporal_id
+ br.ReadBits(2); // spatial_id
+ br.ReadBits(3); // extension_header_reserved_3bits
+ }
+ // end obu_extension_header( )
+ // end obu_header( )
+
+ // Get the size of the remaining OBU data attached to the header in
+ // bytes.
+ size_t size;
+ if (hasSizeField) {
+ if (br.BitsLeft() < 8) {
+ mResult = MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ "Not enough bits left for an OBU size field");
+ return;
+ }
+ CheckedUint32 checkedSize = br.ReadULEB128().toChecked<uint32_t>();
+ // Spec requires that the value ULEB128 reads is (1 << 32) - 1 or below.
+ // See leb128(): https://aomediacodec.github.io/av1-spec/#leb128
+ if (!checkedSize.isValid()) {
+ mResult =
+ MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR, "OBU size was too large");
+ return;
+ }
+ size = checkedSize.value();
+ } else {
+ // This case should rarely happen in practice. To support the Annex B
+ // format in the specification, we would have to parse every header type
+ // to skip over them, but this allows us to at least iterate once to
+ // retrieve the first OBU in the data.
+ size = mData.Length() - 1 - temp.mExtensionFlag;
+ }
+
+ if (br.BitsLeft() / 8 < size) {
+ mResult = MediaResult(
+ NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ nsPrintfCString("Size specified by the OBU header (%zu) is more "
+ "than the actual remaining OBU data (%zu)",
+ size, br.BitsLeft() / 8)
+ .get());
+ return;
+ }
+
+ ASSERT_BYTE_ALIGNED(br);
+
+ size_t bytes = br.BitCount() / 8;
+ temp.mContents = mData.Subspan(mPosition + bytes, size);
+ mCurrent = temp;
+ // end open_bitstream_unit( )
+
+ mPosition += bytes + size;
+ resetExit.release();
+ mResult = NS_OK;
+}
+
+/* static */
+already_AddRefed<MediaByteBuffer> AOMDecoder::CreateOBU(
+ const OBUType aType, const Span<const uint8_t>& aContents) {
+ RefPtr<MediaByteBuffer> buffer = new MediaByteBuffer();
+
+ BitWriter bw(buffer);
+ bw.WriteBits(0, 1); // obu_forbidden_bit
+ bw.WriteBits(static_cast<uint8_t>(aType), 4);
+ bw.WriteBit(false); // obu_extension_flag
+ bw.WriteBit(true); // obu_has_size_field
+ bw.WriteBits(0, 1); // obu_reserved_1bit
+ ASSERT_BYTE_ALIGNED(bw);
+ bw.WriteULEB128(aContents.Length());
+ ASSERT_BYTE_ALIGNED(bw);
+
+ buffer->AppendElements(aContents.Elements(), aContents.Length());
+ return buffer.forget();
+}
+
+/* static */
+MediaResult AOMDecoder::ReadSequenceHeaderInfo(
+ const Span<const uint8_t>& aSample, AV1SequenceInfo& aDestInfo) {
+ // We need to get the last sequence header OBU, the specification does not
+ // limit a temporal unit to one sequence header.
+ OBUIterator iter = ReadOBUs(aSample);
+ OBUInfo seqOBU;
+
+ while (true) {
+ if (!iter.HasNext()) {
+ // Pass along the error from parsing the OBU.
+ MediaResult result = iter.GetResult();
+ if (result.Code() != NS_OK) {
+ return result;
+ }
+ break;
+ }
+ OBUInfo obu = iter.Next();
+ if (obu.mType == OBUType::SequenceHeader) {
+ seqOBU = obu;
+ }
+ }
+
+ if (seqOBU.mType != OBUType::SequenceHeader) {
+ return NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA;
+ }
+
+ // Sequence header syntax is specified here:
+ // https://aomediacodec.github.io/av1-spec/#sequence-header-obu-syntax
+ // Section 5.5: Sequence header OBU syntax
+
+ // See also Section 6.4: Sequence header OBU semantics
+ // https://aomediacodec.github.io/av1-spec/#sequence-header-obu-semantics
+ // This section defines all the fields used in the sequence header.
+ BitReader br(seqOBU.mContents.Elements(), seqOBU.mContents.Length() * 8);
+ AV1SequenceInfo tempInfo;
+
+ // begin sequence_header_obu( )
+ // https://aomediacodec.github.io/av1-spec/#general-sequence-header-obu-syntax
+ tempInfo.mProfile = br.ReadBits(3);
+ const bool stillPicture = br.ReadBit();
+ const bool reducedStillPicture = br.ReadBit();
+ if (!stillPicture && reducedStillPicture) {
+ return MediaResult(
+ NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ "reduced_still_picture is true while still_picture is false");
+ }
+
+ if (reducedStillPicture) {
+ OperatingPoint op;
+ op.mLayers = 0;
+ op.mLevel = br.ReadBits(5); // seq_level_idx[0]
+ op.mTier = 0;
+ tempInfo.mOperatingPoints.SetCapacity(1);
+ tempInfo.mOperatingPoints.AppendElement(op);
+ } else {
+ bool decoderModelInfoPresent;
+ uint8_t operatingPointCountMinusOne;
+
+ if (br.ReadBit()) { // timing_info_present_flag
+ // begin timing_info( )
+ // https://aomediacodec.github.io/av1-spec/#timing-info-syntax
+ br.ReadBits(32); // num_units_in_display_tick
+ br.ReadBits(32); // time_scale
+ if (br.ReadBit()) { // equal_picture_interval
+ br.ReadUE(); // num_ticks_per_picture_minus_1
+ }
+ // end timing_info( )
+
+ decoderModelInfoPresent = br.ReadBit();
+ if (decoderModelInfoPresent) {
+ // begin decoder_model_info( )
+ // https://aomediacodec.github.io/av1-spec/#decoder-model-info-syntax
+ br.ReadBits(5); // buffer_delay_length_minus_1
+ br.ReadBits(32); // num_units_in_decoding_tick
+ br.ReadBits(5); // buffer_removal_time_length_minus_1
+ br.ReadBits(5); // frame_presentation_time_length_minus_1
+ // end decoder_model_info( )
+ }
+ } else {
+ decoderModelInfoPresent = false;
+ }
+
+ bool initialDisplayDelayPresent = br.ReadBit();
+ operatingPointCountMinusOne = br.ReadBits(5);
+ tempInfo.mOperatingPoints.SetCapacity(operatingPointCountMinusOne + 1);
+ for (uint8_t i = 0; i <= operatingPointCountMinusOne; i++) {
+ OperatingPoint op;
+ op.mLayers = br.ReadBits(12); // operating_point_idc[ i ]
+ op.mLevel = br.ReadBits(5); // seq_level_idx[ i ]
+ op.mTier = op.mLevel > 7 ? br.ReadBits(1) : 0;
+ if (decoderModelInfoPresent) {
+ br.ReadBit(); // decoder_model_present_for_this_op[ i ]
+ }
+ if (initialDisplayDelayPresent) {
+ if (br.ReadBit()) { // initial_display_delay_present_for_this_op[ i ]
+ br.ReadBits(4);
+ }
+ }
+ tempInfo.mOperatingPoints.AppendElement(op);
+ }
+ }
+
+ uint8_t frameWidthBits = br.ReadBits(4) + 1;
+ uint8_t frameHeightBits = br.ReadBits(4) + 1;
+ uint32_t maxWidth = br.ReadBits(frameWidthBits) + 1;
+ uint32_t maxHeight = br.ReadBits(frameHeightBits) + 1;
+ tempInfo.mImage = gfx::IntSize(maxWidth, maxHeight);
+
+ if (!reducedStillPicture) {
+ if (br.ReadBit()) { // frame_id_numbers_present_flag
+ br.ReadBits(4); // delta_frame_id_length_minus_2
+ br.ReadBits(3); // additional_frame_id_legnth_minus_1
+ }
+ }
+
+ br.ReadBit(); // use_128x128_superblock
+ br.ReadBit(); // enable_filter_intra
+ br.ReadBit(); // enable_intra_edge_filter
+
+ if (reducedStillPicture) {
+ // enable_interintra_compound = 0
+ // enable_masked_compound = 0
+ // enable_warped_motion = 0
+ // enable_dual_filter = 0
+ // enable_order_hint = 0
+ // enable_jnt_comp = 0
+ // enable_ref_frame_mvs = 0
+ // seq_force_screen_content_tools = SELECT_SCREEN_CONTENT_TOOLS
+ // seq_force_integer_mv = SELECT_INTEGER_MV
+ // OrderHintBits = 0
+ } else {
+ br.ReadBit(); // enable_interintra_compound
+ br.ReadBit(); // enable_masked_compound
+ br.ReadBit(); // enable_warped_motion
+ br.ReadBit(); // enable_dual_filter
+
+ const bool enableOrderHint = br.ReadBit();
+
+ if (enableOrderHint) {
+ br.ReadBit(); // enable_jnt_comp
+ br.ReadBit(); // enable_ref_frame_mvs
+ }
+
+ uint8_t forceScreenContentTools;
+
+ if (br.ReadBit()) { // seq_choose_screen_content_tools
+ forceScreenContentTools = 2; // SELECT_SCREEN_CONTENT_TOOLS
+ } else {
+ forceScreenContentTools = br.ReadBits(1);
+ }
+
+ if (forceScreenContentTools > 0) {
+ if (!br.ReadBit()) { // seq_choose_integer_mv
+ br.ReadBit(); // seq_force_integer_mv
+ }
+ }
+
+ if (enableOrderHint) {
+ br.ReadBits(3); // order_hint_bits_minus_1
+ }
+ }
+
+ br.ReadBit(); // enable_superres
+ br.ReadBit(); // enable_cdef
+ br.ReadBit(); // enable_restoration
+
+ // begin color_config( )
+ // https://aomediacodec.github.io/av1-spec/#color-config-syntax
+ const bool highBitDepth = br.ReadBit();
+ if (tempInfo.mProfile == 2 && highBitDepth) {
+ const bool twelveBit = br.ReadBit();
+ tempInfo.mBitDepth = twelveBit ? 12 : 10;
+ } else {
+ tempInfo.mBitDepth = highBitDepth ? 10 : 8;
+ }
+
+ tempInfo.mMonochrome = tempInfo.mProfile == 1 ? false : br.ReadBit();
+
+ VideoColorSpace* colors = &tempInfo.mColorSpace;
+
+ if (br.ReadBit()) { // color_description_present_flag
+ colors->mPrimaries = static_cast<ColourPrimaries>(br.ReadBits(8));
+ colors->mTransfer = static_cast<TransferCharacteristics>(br.ReadBits(8));
+ colors->mMatrix = static_cast<MatrixCoefficients>(br.ReadBits(8));
+ } else {
+ colors->mPrimaries = ColourPrimaries::CP_UNSPECIFIED;
+ colors->mTransfer = TransferCharacteristics::TC_UNSPECIFIED;
+ colors->mMatrix = MatrixCoefficients::MC_UNSPECIFIED;
+ }
+
+ if (tempInfo.mMonochrome) {
+ colors->mRange = br.ReadBit() ? ColorRange::FULL : ColorRange::LIMITED;
+ tempInfo.mSubsamplingX = true;
+ tempInfo.mSubsamplingY = true;
+ tempInfo.mChromaSamplePosition = ChromaSamplePosition::Unknown;
+ } else if (colors->mPrimaries == ColourPrimaries::CP_BT709 &&
+ colors->mTransfer == TransferCharacteristics::TC_SRGB &&
+ colors->mMatrix == MatrixCoefficients::MC_IDENTITY) {
+ colors->mRange = ColorRange::FULL;
+ tempInfo.mSubsamplingX = false;
+ tempInfo.mSubsamplingY = false;
+ } else {
+ colors->mRange = br.ReadBit() ? ColorRange::FULL : ColorRange::LIMITED;
+ switch (tempInfo.mProfile) {
+ case 0:
+ tempInfo.mSubsamplingX = true;
+ tempInfo.mSubsamplingY = true;
+ break;
+ case 1:
+ tempInfo.mSubsamplingX = false;
+ tempInfo.mSubsamplingY = false;
+ break;
+ case 2:
+ if (tempInfo.mBitDepth == 12) {
+ tempInfo.mSubsamplingX = br.ReadBit();
+ tempInfo.mSubsamplingY =
+ tempInfo.mSubsamplingX ? br.ReadBit() : false;
+ } else {
+ tempInfo.mSubsamplingX = true;
+ tempInfo.mSubsamplingY = false;
+ }
+ break;
+ }
+ tempInfo.mChromaSamplePosition =
+ tempInfo.mSubsamplingX && tempInfo.mSubsamplingY
+ ? static_cast<ChromaSamplePosition>(br.ReadBits(2))
+ : ChromaSamplePosition::Unknown;
+ }
+
+ br.ReadBit(); // separate_uv_delta_q
+ // end color_config( )
+
+ br.ReadBit(); // film_grain_params_present
+ // end sequence_header_obu( )
+
+ // begin trailing_bits( )
+ // https://aomediacodec.github.io/av1-spec/#trailing-bits-syntax
+ if (br.BitsLeft() > 8) {
+ NS_WARNING(
+ "AV1 sequence header finished reading with more than "
+ "a byte of aligning bits, may indicate an error");
+ }
+ // Ensure that data is read correctly by checking trailing bits.
+ bool correct = br.ReadBit();
+ correct &= br.ReadBits(br.BitsLeft() % 8) == 0;
+ while (br.BitsLeft() > 0) {
+ correct &= br.ReadBits(8) == 0;
+ }
+ if (!correct) {
+ return MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ "AV1 sequence header was corrupted");
+ }
+ // end trailing_bits( )
+
+ aDestInfo = tempInfo;
+ return NS_OK;
+}
+
+/* static */
+already_AddRefed<MediaByteBuffer> AOMDecoder::CreateSequenceHeader(
+ const AV1SequenceInfo& aInfo, nsresult& aResult) {
+ aResult = NS_ERROR_FAILURE;
+
+ RefPtr<MediaByteBuffer> seqHdrBuffer = new MediaByteBuffer();
+ BitWriter bw(seqHdrBuffer);
+
+ // See 5.5.1: General sequence header OBU syntax
+ // https://aomediacodec.github.io/av1-spec/#general-sequence-header-obu-syntax
+ bw.WriteBits(aInfo.mProfile, 3);
+ bw.WriteBit(false); // still_picture
+ bw.WriteBit(false); // reduced_still_picture_header
+
+ bw.WriteBit(false); // timing_info_present_flag
+ // if ( timing_info_present_flag ) {...}
+ bw.WriteBit(false); // initial_display_delay_present_flag
+
+ size_t opCount = aInfo.mOperatingPoints.Length();
+ bw.WriteBits(opCount - 1, 5); // operating_points_cnt_minus_1
+ for (size_t i = 0; i < opCount; i++) {
+ OperatingPoint op = aInfo.mOperatingPoints[i];
+ bw.WriteBits(op.mLayers, 12); // operating_point_idc[ i ]
+ bw.WriteBits(op.mLevel, 5);
+ if (op.mLevel > 7) {
+ bw.WriteBits(op.mTier, 1);
+ } else {
+ // seq_tier[ i ] = 0
+ if (op.mTier != 0) {
+ NS_WARNING("Operating points cannot specify tier for levels under 8.");
+ return nullptr;
+ }
+ }
+ // if ( decoder_model_info_present_flag ) {...}
+ // else
+ // decoder_model_info_present_for_this_op[ i ] = 0
+ // if ( initial_display_delay_present_flag ) {...}
+ }
+
+ if (!aInfo.mImage.IsEmpty() <= 0) {
+ NS_WARNING("Sequence header requires a valid image size");
+ return nullptr;
+ }
+ auto getBits = [](int32_t value) {
+ uint8_t bit = 0;
+ do {
+ value >>= 1;
+ bit++;
+ } while (value > 0);
+ return bit;
+ };
+ uint8_t bitsW = getBits(aInfo.mImage.Width());
+ uint8_t bitsH = getBits(aInfo.mImage.Height());
+ bw.WriteBits(bitsW - 1, 4);
+ bw.WriteBits(bitsH - 1, 4);
+ bw.WriteBits(aInfo.mImage.Width() - 1, bitsW);
+ bw.WriteBits(aInfo.mImage.Height() - 1, bitsH);
+
+ // if ( !reduced_still_picture_header )
+ bw.WriteBit(false); // frame_id_numbers_present_flag
+ // if ( frame_id_numbers_present_flag ) {...}
+ // end if ( !reduced_still_picture_header )
+
+ // Values below are derived from a 1080p YouTube AV1 stream.
+ // The values are unused currently for determining the usable
+ // decoder, and are only included to allow successful validation
+ // of the generated sequence header.
+
+ bw.WriteBit(true); // use_128x128_superblock
+ bw.WriteBit(true); // enable_filter_intra
+ bw.WriteBit(true); // enable_intra_edge_filter
+
+ // if ( !reduced_still_picture_header)
+ bw.WriteBit(false); // enable_interintra_compound
+ bw.WriteBit(true); // enable_masked_compound
+ bw.WriteBit(true); // enable_warped_motion
+ bw.WriteBit(false); // enable_dual_filter
+
+ bw.WriteBit(true); // enable_order_hint
+ // if ( enable_order_hint )
+ bw.WriteBit(false); // enable_jnt_comp
+ bw.WriteBit(true); // enable_ref_frame_mvs
+ // end if ( enable_order_hint )
+
+ bw.WriteBit(true); // seq_choose_screen_content_tools
+ // if ( seq_choose_screen_content_tools )
+ // seq_force_screen_content_tools = SELECT_SCREEN_CONTENT_TOOLS (2)
+ // else
+ // seq_force_screen_content_tools = f(1)
+
+ // if ( seq_force_screen_content_tools > 0 )
+ bw.WriteBit(true); // seq_choose_integer_mv
+ // if ( !seq_choose_integer_mv ) {...}
+ // end if ( seq_force_screen_content_tools > 0 )
+
+ // if ( enable_order_hint )
+ bw.WriteBits(6, 3); // order_hint_bits_minus_1
+ // end if ( enable_order_hint )
+ // end if ( !reduced_still_picture_header )
+
+ bw.WriteBit(false); // enable_superres
+ bw.WriteBit(false); // enable_cdef
+ bw.WriteBit(true); // enable_restoration
+
+ // Begin color_config( )
+ // https://aomediacodec.github.io/av1-spec/#color-config-syntax
+ bool highBitDepth = aInfo.mBitDepth >= 10;
+ bw.WriteBit(highBitDepth);
+
+ if (aInfo.mBitDepth == 12 && aInfo.mProfile != 2) {
+ NS_WARNING("Profile must be 2 for 12-bit");
+ return nullptr;
+ }
+ if (aInfo.mProfile == 2 && highBitDepth) {
+ bw.WriteBit(aInfo.mBitDepth == 12); // twelve_bit
+ }
+
+ if (aInfo.mMonochrome && aInfo.mProfile == 1) {
+ NS_WARNING("Profile 1 does not support monochrome");
+ return nullptr;
+ }
+ if (aInfo.mProfile != 1) {
+ bw.WriteBit(aInfo.mMonochrome);
+ }
+
+ const VideoColorSpace colors = aInfo.mColorSpace;
+ bool colorsPresent =
+ colors.mPrimaries != ColourPrimaries::CP_UNSPECIFIED ||
+ colors.mTransfer != TransferCharacteristics::TC_UNSPECIFIED ||
+ colors.mMatrix != MatrixCoefficients::MC_UNSPECIFIED;
+ bw.WriteBit(colorsPresent);
+
+ if (colorsPresent) {
+ bw.WriteBits(static_cast<uint8_t>(colors.mPrimaries), 8);
+ bw.WriteBits(static_cast<uint8_t>(colors.mTransfer), 8);
+ bw.WriteBits(static_cast<uint8_t>(colors.mMatrix), 8);
+ }
+
+ if (aInfo.mMonochrome) {
+ if (!aInfo.mSubsamplingX || !aInfo.mSubsamplingY) {
+ NS_WARNING("Monochrome requires 4:0:0 subsampling");
+ return nullptr;
+ }
+ if (aInfo.mChromaSamplePosition != ChromaSamplePosition::Unknown) {
+ NS_WARNING(
+ "Cannot specify chroma sample position on monochrome sequence");
+ return nullptr;
+ }
+ bw.WriteBit(colors.mRange == ColorRange::FULL);
+ } else if (colors.mPrimaries == ColourPrimaries::CP_BT709 &&
+ colors.mTransfer == TransferCharacteristics::TC_SRGB &&
+ colors.mMatrix == MatrixCoefficients::MC_IDENTITY) {
+ if (aInfo.mSubsamplingX || aInfo.mSubsamplingY ||
+ colors.mRange != ColorRange::FULL ||
+ aInfo.mChromaSamplePosition != ChromaSamplePosition::Unknown) {
+ NS_WARNING("sRGB requires 4:4:4 subsampling with full color range");
+ return nullptr;
+ }
+ } else {
+ bw.WriteBit(colors.mRange == ColorRange::FULL);
+ switch (aInfo.mProfile) {
+ case 0:
+ if (!aInfo.mSubsamplingX || !aInfo.mSubsamplingY) {
+ NS_WARNING("Main Profile requires 4:2:0 subsampling");
+ return nullptr;
+ }
+ break;
+ case 1:
+ if (aInfo.mSubsamplingX || aInfo.mSubsamplingY) {
+ NS_WARNING("High Profile requires 4:4:4 subsampling");
+ return nullptr;
+ }
+ break;
+ case 2:
+ if (aInfo.mBitDepth == 12) {
+ bw.WriteBit(aInfo.mSubsamplingX);
+ if (aInfo.mSubsamplingX) {
+ bw.WriteBit(aInfo.mSubsamplingY);
+ }
+ } else {
+ if (!aInfo.mSubsamplingX || aInfo.mSubsamplingY) {
+ NS_WARNING(
+ "Professional Profile < 12-bit requires 4:2:2 subsampling");
+ return nullptr;
+ }
+ }
+ break;
+ }
+
+ if (aInfo.mSubsamplingX && aInfo.mSubsamplingY) {
+ bw.WriteBits(static_cast<uint8_t>(aInfo.mChromaSamplePosition), 2);
+ } else {
+ if (aInfo.mChromaSamplePosition != ChromaSamplePosition::Unknown) {
+ NS_WARNING("Only 4:2:0 subsampling can specify chroma position");
+ return nullptr;
+ }
+ }
+ }
+
+ bw.WriteBit(false); // separate_uv_delta_q
+ // end color_config( )
+
+ bw.WriteBit(true); // film_grain_params_present
+
+ // trailing_bits( )
+ // https://aomediacodec.github.io/av1-spec/#trailing-bits-syntax
+ size_t numTrailingBits = 8 - (bw.BitCount() % 8);
+ bw.WriteBit(true);
+ bw.WriteBits(0, numTrailingBits - 1);
+ ASSERT_BYTE_ALIGNED(bw);
+
+ Span<const uint8_t> seqHdr(seqHdrBuffer->Elements(), seqHdrBuffer->Length());
+ aResult = NS_OK;
+ return CreateOBU(OBUType::SequenceHeader, seqHdr);
+}
+
+/* static */
+void AOMDecoder::TryReadAV1CBox(const MediaByteBuffer* aBox,
+ AV1SequenceInfo& aDestInfo,
+ MediaResult& aSeqHdrResult) {
+ // See av1C specification:
+ // https://aomediacodec.github.io/av1-isobmff/#av1codecconfigurationbox-section
+ BitReader br(aBox);
+
+ br.ReadBits(8); // marker, version
+
+ aDestInfo.mProfile = br.ReadBits(3);
+
+ OperatingPoint op;
+ op.mLevel = br.ReadBits(5);
+ op.mTier = br.ReadBits(1);
+ aDestInfo.mOperatingPoints.AppendElement(op);
+
+ bool highBitDepth = br.ReadBit();
+ bool twelveBit = br.ReadBit();
+ aDestInfo.mBitDepth = highBitDepth ? twelveBit ? 12 : 10 : 8;
+
+ aDestInfo.mMonochrome = br.ReadBit();
+ aDestInfo.mSubsamplingX = br.ReadBit();
+ aDestInfo.mSubsamplingY = br.ReadBit();
+ aDestInfo.mChromaSamplePosition =
+ static_cast<ChromaSamplePosition>(br.ReadBits(2));
+
+ br.ReadBits(3); // reserved
+ br.ReadBit(); // initial_presentation_delay_present
+ br.ReadBits(4); // initial_presentation_delay_minus_one or reserved
+
+ ASSERT_BYTE_ALIGNED(br);
+
+ size_t skipBytes = br.BitCount() / 8;
+ Span<const uint8_t> obus(aBox->Elements() + skipBytes,
+ aBox->Length() - skipBytes);
+
+ // Minimum possible OBU header size
+ if (obus.Length() < 1) {
+ aSeqHdrResult = NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA;
+ return;
+ }
+
+ // If present, the sequence header will be redundant to some values, but any
+ // values stored in it should be treated as more accurate than av1C.
+ aSeqHdrResult = ReadSequenceHeaderInfo(obus, aDestInfo);
+}
+
+/* static */
+void AOMDecoder::WriteAV1CBox(const AV1SequenceInfo& aInfo,
+ MediaByteBuffer* aDestBox, bool& aHasSeqHdr) {
+ aHasSeqHdr = false;
+
+ BitWriter bw(aDestBox);
+
+ bw.WriteBit(true); // marker
+ bw.WriteBits(1, 7); // version
+
+ bw.WriteBits(aInfo.mProfile, 3);
+
+ MOZ_DIAGNOSTIC_ASSERT(aInfo.mOperatingPoints.Length() > 0);
+ bw.WriteBits(aInfo.mOperatingPoints[0].mLevel, 5);
+ bw.WriteBits(aInfo.mOperatingPoints[0].mTier, 1);
+
+ bw.WriteBit(aInfo.mBitDepth >= 10); // high_bitdepth
+ bw.WriteBit(aInfo.mBitDepth == 12); // twelve_bit
+
+ bw.WriteBit(aInfo.mMonochrome);
+ bw.WriteBit(aInfo.mSubsamplingX);
+ bw.WriteBit(aInfo.mSubsamplingY);
+ bw.WriteBits(static_cast<uint8_t>(aInfo.mChromaSamplePosition), 2);
+
+ bw.WriteBits(0, 3); // reserved
+ bw.WriteBit(false); // initial_presentation_delay_present
+ bw.WriteBits(0, 4); // initial_presentation_delay_minus_one or reserved
+
+ ASSERT_BYTE_ALIGNED(bw);
+
+ nsresult rv;
+ RefPtr<MediaByteBuffer> seqHdrBuffer = CreateSequenceHeader(aInfo, rv);
+
+ if (NS_SUCCEEDED(rv)) {
+ aDestBox->AppendElements(seqHdrBuffer->Elements(), seqHdrBuffer->Length());
+ aHasSeqHdr = true;
+ }
+}
+
+/* static */
+Maybe<AOMDecoder::AV1SequenceInfo> AOMDecoder::CreateSequenceInfoFromCodecs(
+ const nsAString& aCodec) {
+ AV1SequenceInfo info;
+ OperatingPoint op;
+ uint8_t chromaSamplePosition;
+ if (!ExtractAV1CodecDetails(aCodec, info.mProfile, op.mLevel, op.mTier,
+ info.mBitDepth, info.mMonochrome,
+ info.mSubsamplingX, info.mSubsamplingY,
+ chromaSamplePosition, info.mColorSpace)) {
+ return Nothing();
+ }
+ info.mOperatingPoints.AppendElement(op);
+ info.mChromaSamplePosition =
+ static_cast<ChromaSamplePosition>(chromaSamplePosition);
+ return Some(info);
+}
+
+/* static */
+bool AOMDecoder::SetVideoInfo(VideoInfo* aDestInfo, const nsAString& aCodec) {
+ Maybe<AV1SequenceInfo> info = CreateSequenceInfoFromCodecs(aCodec);
+ if (info.isNothing()) {
+ return false;
+ }
+
+ if (!aDestInfo->mImage.IsEmpty()) {
+ info->mImage = aDestInfo->mImage;
+ }
+
+ RefPtr<MediaByteBuffer> extraData = new MediaByteBuffer();
+ bool hasSeqHdr;
+ WriteAV1CBox(info.value(), extraData, hasSeqHdr);
+ aDestInfo->mExtraData = extraData;
+ return true;
+}
+
+} // namespace mozilla
+#undef LOG
+#undef ASSERT_BYTE_ALIGNED
diff --git a/dom/media/platforms/agnostic/AOMDecoder.h b/dom/media/platforms/agnostic/AOMDecoder.h
new file mode 100644
index 0000000000..2d3d7761fa
--- /dev/null
+++ b/dom/media/platforms/agnostic/AOMDecoder.h
@@ -0,0 +1,287 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#if !defined(AOMDecoder_h_)
+# define AOMDecoder_h_
+
+# include <stdint.h>
+
+# include "PerformanceRecorder.h"
+# include "PlatformDecoderModule.h"
+# include "aom/aom_decoder.h"
+# include "mozilla/Span.h"
+# include "VideoUtils.h"
+
+namespace mozilla {
+
+DDLoggedTypeDeclNameAndBase(AOMDecoder, MediaDataDecoder);
+
+class AOMDecoder final : public MediaDataDecoder,
+ public DecoderDoctorLifeLogger<AOMDecoder> {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AOMDecoder, final);
+
+ explicit AOMDecoder(const CreateDecoderParams& aParams);
+
+ RefPtr<InitPromise> Init() override;
+ RefPtr<DecodePromise> Decode(MediaRawData* aSample) override;
+ RefPtr<DecodePromise> Drain() override;
+ RefPtr<FlushPromise> Flush() override;
+ RefPtr<ShutdownPromise> Shutdown() override;
+ nsCString GetDescriptionName() const override {
+ return "av1 libaom video decoder"_ns;
+ }
+ nsCString GetCodecName() const override { return "av1"_ns; }
+
+ // Return true if aMimeType is a one of the strings used
+ // by our demuxers to identify AV1 streams.
+ static bool IsAV1(const nsACString& aMimeType);
+
+ // Return true if a sample is a keyframe.
+ static bool IsKeyframe(Span<const uint8_t> aBuffer);
+
+ // Return the frame dimensions for a sample.
+ static gfx::IntSize GetFrameSize(Span<const uint8_t> aBuffer);
+
+ // obu_type defined at:
+ // https://aomediacodec.github.io/av1-spec/av1-spec.pdf#page=123
+ enum class OBUType : uint8_t {
+ Reserved = 0,
+ SequenceHeader = 1,
+ TemporalDelimiter = 2,
+ FrameHeader = 3,
+ TileGroup = 4,
+ Metadata = 5,
+ Frame = 6,
+ RedundantFrameHeader = 7,
+ TileList = 8,
+ Padding = 15
+ };
+
+ struct OBUInfo {
+ OBUType mType = OBUType::Reserved;
+ bool mExtensionFlag = false;
+ Span<const uint8_t> mContents;
+
+ bool IsValid() const {
+ switch (mType) {
+ case OBUType::SequenceHeader:
+ case OBUType::TemporalDelimiter:
+ case OBUType::FrameHeader:
+ case OBUType::TileGroup:
+ case OBUType::Metadata:
+ case OBUType::Frame:
+ case OBUType::RedundantFrameHeader:
+ case OBUType::TileList:
+ case OBUType::Padding:
+ return true;
+ default:
+ return false;
+ }
+ }
+ };
+
+ struct OBUIterator {
+ public:
+ explicit OBUIterator(const Span<const uint8_t>& aData)
+ : mData(aData), mPosition(0), mGoNext(true), mResult(NS_OK) {}
+ bool HasNext() {
+ UpdateNext();
+ return !mGoNext;
+ }
+ OBUInfo Next() {
+ UpdateNext();
+ mGoNext = true;
+ return mCurrent;
+ }
+ MediaResult GetResult() const { return mResult; }
+
+ private:
+ const Span<const uint8_t>& mData;
+ size_t mPosition;
+ OBUInfo mCurrent;
+ bool mGoNext;
+ MediaResult mResult;
+
+ // Used to fill mCurrent with the next OBU in the iterator.
+ // mGoNext must be set to false if the next OBU is retrieved,
+ // otherwise it will be true so that HasNext() returns false.
+ // When an invalid OBU is read, the iterator will finish and
+ // mCurrent will be reset to default OBUInfo().
+ void UpdateNext();
+ };
+
+ // Create an iterator to parse Open Bitstream Units from a buffer.
+ static OBUIterator ReadOBUs(const Span<const uint8_t>& aData);
+ // Writes an Open Bitstream Unit header type and the contained subheader.
+ // Extension flag is set to 0 and size field is always present.
+ static already_AddRefed<MediaByteBuffer> CreateOBU(
+ const OBUType aType, const Span<const uint8_t>& aContents);
+
+ // chroma_sample_position defined at:
+ // https://aomediacodec.github.io/av1-spec/av1-spec.pdf#page=131
+ enum class ChromaSamplePosition : uint8_t {
+ Unknown = 0,
+ Vertical = 1,
+ Colocated = 2,
+ Reserved = 3
+ };
+
+ struct OperatingPoint {
+ // https://aomediacodec.github.io/av1-spec/av1-spec.pdf#page=125
+ // operating_point_idc[ i ]: A set of bitwise flags determining
+ // the temporal and spatial layers to decode.
+ // A value of 0 indicates that scalability is not being used.
+ uint16_t mLayers = 0;
+ // https://aomediacodec.github.io/av1-spec/av1-spec.pdf#page=650
+ // See A.3: Levels for a definition of the available levels.
+ uint8_t mLevel = 0;
+ // https://aomediacodec.github.io/av1-spec/av1-spec.pdf#page=126
+ // seq_tier[ i ]: The tier for the selected operating point.
+ uint8_t mTier = 0;
+
+ bool operator==(const OperatingPoint& aOther) const {
+ return mLayers == aOther.mLayers && mLevel == aOther.mLevel &&
+ mTier == aOther.mTier;
+ }
+ bool operator!=(const OperatingPoint& aOther) const {
+ return !(*this == aOther);
+ }
+ };
+
+ struct AV1SequenceInfo {
+ AV1SequenceInfo() = default;
+
+ AV1SequenceInfo(const AV1SequenceInfo& aOther) { *this = aOther; }
+
+ // Profiles, levels and tiers defined at:
+ // https://aomediacodec.github.io/av1-spec/av1-spec.pdf#page=650
+ uint8_t mProfile = 0;
+
+ // choose_operating_point( ) defines that the operating points are
+ // specified in order of preference by the encoder. Higher operating
+ // points indices in the header will allow a tradeoff of quality for
+ // performance, dropping some data from the decoding process.
+ // Normally we are only interested in the first operating point.
+ // See: https://aomediacodec.github.io/av1-spec/av1-spec.pdf#page=126
+ nsTArray<OperatingPoint> mOperatingPoints = nsTArray<OperatingPoint>(1);
+
+ gfx::IntSize mImage = {0, 0};
+
+ // Color configs explained at:
+ // https://aomediacodec.github.io/av1-spec/av1-spec.pdf#page=129
+ uint8_t mBitDepth = 8;
+ bool mMonochrome = false;
+ bool mSubsamplingX = true;
+ bool mSubsamplingY = true;
+ ChromaSamplePosition mChromaSamplePosition = ChromaSamplePosition::Unknown;
+
+ VideoColorSpace mColorSpace;
+
+ gfx::ColorDepth ColorDepth() const {
+ return gfx::ColorDepthForBitDepth(mBitDepth);
+ }
+
+ bool operator==(const AV1SequenceInfo& aOther) const {
+ if (mProfile != aOther.mProfile || mImage != aOther.mImage ||
+ mBitDepth != aOther.mBitDepth || mMonochrome != aOther.mMonochrome ||
+ mSubsamplingX != aOther.mSubsamplingX ||
+ mSubsamplingY != aOther.mSubsamplingY ||
+ mChromaSamplePosition != aOther.mChromaSamplePosition ||
+ mColorSpace != aOther.mColorSpace) {
+ return false;
+ }
+
+ size_t opCount = mOperatingPoints.Length();
+ if (opCount != aOther.mOperatingPoints.Length()) {
+ return false;
+ }
+ for (size_t i = 0; i < opCount; i++) {
+ if (mOperatingPoints[i] != aOther.mOperatingPoints[i]) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+ bool operator!=(const AV1SequenceInfo& aOther) const {
+ return !(*this == aOther);
+ }
+ AV1SequenceInfo& operator=(const AV1SequenceInfo& aOther) {
+ mProfile = aOther.mProfile;
+
+ size_t opCount = aOther.mOperatingPoints.Length();
+ mOperatingPoints.ClearAndRetainStorage();
+ mOperatingPoints.SetCapacity(opCount);
+ for (size_t i = 0; i < opCount; i++) {
+ mOperatingPoints.AppendElement(aOther.mOperatingPoints[i]);
+ }
+
+ mImage = aOther.mImage;
+ mBitDepth = aOther.mBitDepth;
+ mMonochrome = aOther.mMonochrome;
+ mSubsamplingX = aOther.mSubsamplingX;
+ mSubsamplingY = aOther.mSubsamplingY;
+ mChromaSamplePosition = aOther.mChromaSamplePosition;
+ mColorSpace = aOther.mColorSpace;
+ return *this;
+ }
+ };
+
+ // Get a sequence header's info from a sample.
+ // Returns a MediaResult with codes:
+ // NS_OK: Sequence header was successfully found and read.
+ // NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA: Sequence header was not present.
+ // Other errors will indicate that the data was corrupt.
+ static MediaResult ReadSequenceHeaderInfo(const Span<const uint8_t>& aSample,
+ AV1SequenceInfo& aDestInfo);
+ // Writes a sequence header OBU to the buffer.
+ static already_AddRefed<MediaByteBuffer> CreateSequenceHeader(
+ const AV1SequenceInfo& aInfo, nsresult& aResult);
+
+ // Reads the raw data of an ISOBMFF-compatible av1 configuration box (av1C),
+ // including any included sequence header.
+ static void TryReadAV1CBox(const MediaByteBuffer* aBox,
+ AV1SequenceInfo& aDestInfo,
+ MediaResult& aSeqHdrResult);
+ // Reads the raw data of an ISOBMFF-compatible av1 configuration box (av1C),
+ // including any included sequence header.
+ // This function should only be called for av1C boxes made by WriteAV1CBox, as
+ // it will assert that the box and its contained OBUs are not corrupted.
+ static void ReadAV1CBox(const MediaByteBuffer* aBox,
+ AV1SequenceInfo& aDestInfo, bool& aHadSeqHdr) {
+ MediaResult seqHdrResult;
+ TryReadAV1CBox(aBox, aDestInfo, seqHdrResult);
+ nsresult code = seqHdrResult.Code();
+ MOZ_ASSERT(code == NS_OK || code == NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA);
+ aHadSeqHdr = code == NS_OK;
+ }
+ // Writes an ISOBMFF-compatible av1 configuration box (av1C) to the buffer.
+ static void WriteAV1CBox(const AV1SequenceInfo& aInfo,
+ MediaByteBuffer* aDestBox, bool& aHasSeqHdr);
+
+ // Create sequence info from a MIME codecs string.
+ static Maybe<AV1SequenceInfo> CreateSequenceInfoFromCodecs(
+ const nsAString& aCodec);
+ static bool SetVideoInfo(VideoInfo* aDestInfo, const nsAString& aCodec);
+
+ private:
+ ~AOMDecoder();
+ RefPtr<DecodePromise> ProcessDecode(MediaRawData* aSample);
+
+ const RefPtr<layers::ImageContainer> mImageContainer;
+ const RefPtr<TaskQueue> mTaskQueue;
+
+ // AOM decoder state
+ aom_codec_ctx_t mCodec;
+
+ const VideoInfo mInfo;
+ const Maybe<TrackingId> mTrackingId;
+ PerformanceRecorderMulti<DecodeStage> mPerformanceRecorder;
+};
+
+} // namespace mozilla
+
+#endif // AOMDecoder_h_
diff --git a/dom/media/platforms/agnostic/AgnosticDecoderModule.cpp b/dom/media/platforms/agnostic/AgnosticDecoderModule.cpp
new file mode 100644
index 0000000000..22723a6a04
--- /dev/null
+++ b/dom/media/platforms/agnostic/AgnosticDecoderModule.cpp
@@ -0,0 +1,218 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AgnosticDecoderModule.h"
+
+#include "OpusDecoder.h"
+#include "TheoraDecoder.h"
+#include "VPXDecoder.h"
+#include "VorbisDecoder.h"
+#include "WAVDecoder.h"
+#include "mozilla/Logging.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "VideoUtils.h"
+
+#ifdef MOZ_AV1
+# include "AOMDecoder.h"
+# include "DAV1DDecoder.h"
+#endif
+
+namespace mozilla {
+
+enum class DecoderType {
+#ifdef MOZ_AV1
+ AV1,
+#endif
+ Opus,
+ Theora,
+ Vorbis,
+ VPX,
+ Wave,
+};
+
+static bool IsAvailableInDefault(DecoderType type) {
+ switch (type) {
+#ifdef MOZ_AV1
+ case DecoderType::AV1:
+ return StaticPrefs::media_av1_enabled();
+#endif
+ case DecoderType::Opus:
+ case DecoderType::Theora:
+ case DecoderType::Vorbis:
+ case DecoderType::VPX:
+ case DecoderType::Wave:
+ return true;
+ default:
+ return false;
+ }
+}
+
+static bool IsAvailableInRdd(DecoderType type) {
+ switch (type) {
+#ifdef MOZ_AV1
+ case DecoderType::AV1:
+ return StaticPrefs::media_av1_enabled();
+#endif
+ case DecoderType::Opus:
+ return StaticPrefs::media_rdd_opus_enabled();
+ case DecoderType::Theora:
+ return StaticPrefs::media_rdd_theora_enabled();
+ case DecoderType::Vorbis:
+#if defined(__MINGW32__)
+ // If this is a MinGW build we need to force AgnosticDecoderModule to
+ // handle the decision to support Vorbis decoding (instead of
+ // RDD/RemoteDecoderModule) because of Bug 1597408 (Vorbis decoding on
+ // RDD causing sandboxing failure on MinGW-clang). Typically this
+ // would be dealt with using defines in StaticPrefList.yaml, but we
+ // must handle it here because of Bug 1598426 (the __MINGW32__ define
+ // isn't supported in StaticPrefList.yaml).
+ return false;
+#else
+ return StaticPrefs::media_rdd_vorbis_enabled();
+#endif
+ case DecoderType::VPX:
+ return StaticPrefs::media_rdd_vpx_enabled();
+ case DecoderType::Wave:
+ return StaticPrefs::media_rdd_wav_enabled();
+ default:
+ return false;
+ }
+}
+
+static bool IsAvailableInUtility(DecoderType type) {
+ switch (type) {
+ case DecoderType::Opus:
+ return StaticPrefs::media_utility_opus_enabled();
+ case DecoderType::Vorbis:
+#if defined(__MINGW32__)
+ // If this is a MinGW build we need to force AgnosticDecoderModule to
+ // handle the decision to support Vorbis decoding (instead of
+ // Utility/RemoteDecoderModule) because of Bug 1597408 (Vorbis decoding on
+ // Utility causing sandboxing failure on MinGW-clang). Typically this
+ // would be dealt with using defines in StaticPrefList.yaml, but we
+ // must handle it here because of Bug 1598426 (the __MINGW32__ define
+ // isn't supported in StaticPrefList.yaml).
+ return false;
+#else
+ return StaticPrefs::media_utility_vorbis_enabled();
+#endif
+ case DecoderType::Wave:
+ return StaticPrefs::media_utility_wav_enabled();
+ case DecoderType::Theora: // Video codecs, dont take care of them
+ case DecoderType::VPX:
+ default:
+ return false;
+ }
+}
+
+// Checks if decoder is available in the current process
+static bool IsAvailable(DecoderType type) {
+ return XRE_IsRDDProcess() ? IsAvailableInRdd(type)
+ : XRE_IsUtilityProcess() ? IsAvailableInUtility(type)
+ : IsAvailableInDefault(type);
+}
+
+media::DecodeSupportSet AgnosticDecoderModule::SupportsMimeType(
+ const nsACString& aMimeType, DecoderDoctorDiagnostics* aDiagnostics) const {
+ UniquePtr<TrackInfo> trackInfo = CreateTrackInfoWithMIMEType(aMimeType);
+ if (!trackInfo) {
+ return media::DecodeSupport::Unsupported;
+ }
+ return Supports(SupportDecoderParams(*trackInfo), aDiagnostics);
+}
+
+media::DecodeSupportSet AgnosticDecoderModule::Supports(
+ const SupportDecoderParams& aParams,
+ DecoderDoctorDiagnostics* aDiagnostics) const {
+ // This should only be supported by MFMediaEngineDecoderModule.
+ if (aParams.mMediaEngineId) {
+ return media::DecodeSupport::Unsupported;
+ }
+
+ const auto& trackInfo = aParams.mConfig;
+ const nsACString& mimeType = trackInfo.mMimeType;
+
+ bool supports =
+#ifdef MOZ_AV1
+ // We remove support for decoding AV1 here if RDD is enabled so that
+ // decoding on the content process doesn't accidentally happen in case
+ // something goes wrong with launching the RDD process.
+ (AOMDecoder::IsAV1(mimeType) && IsAvailable(DecoderType::AV1)) ||
+#endif
+ (VPXDecoder::IsVPX(mimeType) && IsAvailable(DecoderType::VPX)) ||
+ (TheoraDecoder::IsTheora(mimeType) && IsAvailable(DecoderType::Theora)) ||
+ (VorbisDataDecoder::IsVorbis(mimeType) &&
+ IsAvailable(DecoderType::Vorbis)) ||
+ (WaveDataDecoder::IsWave(mimeType) && IsAvailable(DecoderType::Wave)) ||
+ (OpusDataDecoder::IsOpus(mimeType) && IsAvailable(DecoderType::Opus));
+ MOZ_LOG(sPDMLog, LogLevel::Debug,
+ ("Agnostic decoder %s requested type '%s'",
+ supports ? "supports" : "rejects", mimeType.BeginReading()));
+ if (supports) {
+ return media::DecodeSupport::SoftwareDecode;
+ }
+ return media::DecodeSupport::Unsupported;
+}
+
+already_AddRefed<MediaDataDecoder> AgnosticDecoderModule::CreateVideoDecoder(
+ const CreateDecoderParams& aParams) {
+ if (Supports(SupportDecoderParams(aParams), nullptr /* diagnostic */) ==
+ media::DecodeSupport::Unsupported) {
+ return nullptr;
+ }
+ RefPtr<MediaDataDecoder> m;
+
+ if (VPXDecoder::IsVPX(aParams.mConfig.mMimeType)) {
+ m = new VPXDecoder(aParams);
+ }
+#ifdef MOZ_AV1
+ // We remove support for decoding AV1 here if RDD is enabled so that
+ // decoding on the content process doesn't accidentally happen in case
+ // something goes wrong with launching the RDD process.
+ if (StaticPrefs::media_av1_enabled() &&
+ (!StaticPrefs::media_rdd_process_enabled() || XRE_IsRDDProcess()) &&
+ AOMDecoder::IsAV1(aParams.mConfig.mMimeType)) {
+ if (StaticPrefs::media_av1_use_dav1d()) {
+ m = new DAV1DDecoder(aParams);
+ } else {
+ m = new AOMDecoder(aParams);
+ }
+ }
+#endif
+ else if (TheoraDecoder::IsTheora(aParams.mConfig.mMimeType)) {
+ m = new TheoraDecoder(aParams);
+ }
+
+ return m.forget();
+}
+
+already_AddRefed<MediaDataDecoder> AgnosticDecoderModule::CreateAudioDecoder(
+ const CreateDecoderParams& aParams) {
+ if (Supports(SupportDecoderParams(aParams), nullptr /* diagnostic */) ==
+ media::DecodeSupport::Unsupported) {
+ return nullptr;
+ }
+ RefPtr<MediaDataDecoder> m;
+
+ const TrackInfo& config = aParams.mConfig;
+ if (VorbisDataDecoder::IsVorbis(config.mMimeType)) {
+ m = new VorbisDataDecoder(aParams);
+ } else if (OpusDataDecoder::IsOpus(config.mMimeType)) {
+ m = new OpusDataDecoder(aParams);
+ } else if (WaveDataDecoder::IsWave(config.mMimeType)) {
+ m = new WaveDataDecoder(aParams);
+ }
+
+ return m.forget();
+}
+
+/* static */
+already_AddRefed<PlatformDecoderModule> AgnosticDecoderModule::Create() {
+ RefPtr<PlatformDecoderModule> pdm = new AgnosticDecoderModule();
+ return pdm.forget();
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/agnostic/AgnosticDecoderModule.h b/dom/media/platforms/agnostic/AgnosticDecoderModule.h
new file mode 100644
index 0000000000..bac5f4bf42
--- /dev/null
+++ b/dom/media/platforms/agnostic/AgnosticDecoderModule.h
@@ -0,0 +1,39 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(AgnosticDecoderModule_h_)
+# define AgnosticDecoderModule_h_
+
+# include "PlatformDecoderModule.h"
+
+namespace mozilla {
+
+class AgnosticDecoderModule : public PlatformDecoderModule {
+ public:
+ static already_AddRefed<PlatformDecoderModule> Create();
+
+ media::DecodeSupportSet SupportsMimeType(
+ const nsACString& aMimeType,
+ DecoderDoctorDiagnostics* aDiagnostics) const override;
+ media::DecodeSupportSet Supports(
+ const SupportDecoderParams& aParams,
+ DecoderDoctorDiagnostics* aDiagnostics) const override;
+
+ protected:
+ AgnosticDecoderModule() = default;
+ virtual ~AgnosticDecoderModule() = default;
+ // Decode thread.
+ already_AddRefed<MediaDataDecoder> CreateVideoDecoder(
+ const CreateDecoderParams& aParams) override;
+
+ // Decode thread.
+ already_AddRefed<MediaDataDecoder> CreateAudioDecoder(
+ const CreateDecoderParams& aParams) override;
+};
+
+} // namespace mozilla
+
+#endif /* AgnosticDecoderModule_h_ */
diff --git a/dom/media/platforms/agnostic/BlankDecoderModule.cpp b/dom/media/platforms/agnostic/BlankDecoderModule.cpp
new file mode 100644
index 0000000000..fba07338dc
--- /dev/null
+++ b/dom/media/platforms/agnostic/BlankDecoderModule.cpp
@@ -0,0 +1,144 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "BlankDecoderModule.h"
+
+#include "mozilla/CheckedInt.h"
+#include "mozilla/UniquePtrExtensions.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/gfx/Rect.h"
+#include "mozilla/gfx/Point.h"
+#include "ImageContainer.h"
+#include "MediaData.h"
+#include "MediaInfo.h"
+#include "VideoUtils.h"
+
+namespace mozilla {
+
+BlankVideoDataCreator::BlankVideoDataCreator(
+ uint32_t aFrameWidth, uint32_t aFrameHeight,
+ layers::ImageContainer* aImageContainer)
+ : mFrameWidth(aFrameWidth),
+ mFrameHeight(aFrameHeight),
+ mImageContainer(aImageContainer) {
+ mInfo.mDisplay = gfx::IntSize(mFrameWidth, mFrameHeight);
+ mPicture = gfx::IntRect(0, 0, mFrameWidth, mFrameHeight);
+}
+
+already_AddRefed<MediaData> BlankVideoDataCreator::Create(
+ MediaRawData* aSample) {
+ // Create a fake YUV buffer in a 420 format. That is, an 8bpp Y plane,
+ // with a U and V plane that are half the size of the Y plane, i.e 8 bit,
+ // 2x2 subsampled. Have the data pointer of each frame point to the
+ // first plane, they'll always be zero'd memory anyway.
+ const CheckedUint32 size = CheckedUint32(mFrameWidth) * mFrameHeight;
+ if (!size.isValid()) {
+ // Overflow happened.
+ return nullptr;
+ }
+ auto frame = MakeUniqueFallible<uint8_t[]>(size.value());
+ if (!frame) {
+ return nullptr;
+ }
+ memset(frame.get(), 0, mFrameWidth * mFrameHeight);
+ VideoData::YCbCrBuffer buffer;
+
+ // Y plane.
+ buffer.mPlanes[0].mData = frame.get();
+ buffer.mPlanes[0].mStride = mFrameWidth;
+ buffer.mPlanes[0].mHeight = mFrameHeight;
+ buffer.mPlanes[0].mWidth = mFrameWidth;
+ buffer.mPlanes[0].mSkip = 0;
+
+ // Cb plane.
+ buffer.mPlanes[1].mData = frame.get();
+ buffer.mPlanes[1].mStride = (mFrameWidth + 1) / 2;
+ buffer.mPlanes[1].mHeight = (mFrameHeight + 1) / 2;
+ buffer.mPlanes[1].mWidth = (mFrameWidth + 1) / 2;
+ buffer.mPlanes[1].mSkip = 0;
+
+ // Cr plane.
+ buffer.mPlanes[2].mData = frame.get();
+ buffer.mPlanes[2].mStride = (mFrameWidth + 1) / 2;
+ buffer.mPlanes[2].mHeight = (mFrameHeight + 1) / 2;
+ buffer.mPlanes[2].mWidth = (mFrameWidth + 1) / 2;
+ buffer.mPlanes[2].mSkip = 0;
+
+ buffer.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT;
+ buffer.mYUVColorSpace = gfx::YUVColorSpace::BT601;
+ buffer.mColorPrimaries = gfx::ColorSpace2::BT709;
+
+ return VideoData::CreateAndCopyData(mInfo, mImageContainer, aSample->mOffset,
+ aSample->mTime, aSample->mDuration,
+ buffer, aSample->mKeyframe,
+ aSample->mTime, mPicture, nullptr);
+}
+
+BlankAudioDataCreator::BlankAudioDataCreator(uint32_t aChannelCount,
+ uint32_t aSampleRate)
+ : mFrameSum(0), mChannelCount(aChannelCount), mSampleRate(aSampleRate) {}
+
+already_AddRefed<MediaData> BlankAudioDataCreator::Create(
+ MediaRawData* aSample) {
+ // Convert duration to frames. We add 1 to duration to account for
+ // rounding errors, so we get a consistent tone.
+ CheckedInt64 frames =
+ UsecsToFrames(aSample->mDuration.ToMicroseconds() + 1, mSampleRate);
+ if (!frames.isValid() || !mChannelCount || !mSampleRate ||
+ frames.value() > (UINT32_MAX / mChannelCount)) {
+ return nullptr;
+ }
+ AlignedAudioBuffer samples(frames.value() * mChannelCount);
+ if (!samples) {
+ return nullptr;
+ }
+ // Fill the sound buffer with an A4 tone.
+ static const float pi = 3.14159265f;
+ static const float noteHz = 440.0f;
+ for (int i = 0; i < frames.value(); i++) {
+ float f = sin(2 * pi * noteHz * mFrameSum / mSampleRate);
+ for (unsigned c = 0; c < mChannelCount; c++) {
+ samples[i * mChannelCount + c] = AudioDataValue(f);
+ }
+ mFrameSum++;
+ }
+ RefPtr<AudioData> data(new AudioData(aSample->mOffset, aSample->mTime,
+ std::move(samples), mChannelCount,
+ mSampleRate));
+ return data.forget();
+}
+
+already_AddRefed<MediaDataDecoder> BlankDecoderModule::CreateVideoDecoder(
+ const CreateDecoderParams& aParams) {
+ const VideoInfo& config = aParams.VideoConfig();
+ UniquePtr<DummyDataCreator> creator = MakeUnique<BlankVideoDataCreator>(
+ config.mDisplay.width, config.mDisplay.height, aParams.mImageContainer);
+ RefPtr<MediaDataDecoder> decoder = new DummyMediaDataDecoder(
+ std::move(creator), "blank media data decoder"_ns, aParams);
+ return decoder.forget();
+}
+
+already_AddRefed<MediaDataDecoder> BlankDecoderModule::CreateAudioDecoder(
+ const CreateDecoderParams& aParams) {
+ const AudioInfo& config = aParams.AudioConfig();
+ UniquePtr<DummyDataCreator> creator =
+ MakeUnique<BlankAudioDataCreator>(config.mChannels, config.mRate);
+ RefPtr<MediaDataDecoder> decoder = new DummyMediaDataDecoder(
+ std::move(creator), "blank media data decoder"_ns, aParams);
+ return decoder.forget();
+}
+
+media::DecodeSupportSet BlankDecoderModule::SupportsMimeType(
+ const nsACString& aMimeType, DecoderDoctorDiagnostics* aDiagnostics) const {
+ return media::DecodeSupport::SoftwareDecode;
+}
+
+/* static */
+already_AddRefed<PlatformDecoderModule> BlankDecoderModule::Create() {
+ return MakeAndAddRef<BlankDecoderModule>();
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/agnostic/BlankDecoderModule.h b/dom/media/platforms/agnostic/BlankDecoderModule.h
new file mode 100644
index 0000000000..65e5d4479e
--- /dev/null
+++ b/dom/media/platforms/agnostic/BlankDecoderModule.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#if !defined(BlankDecoderModule_h_)
+# define BlankDecoderModule_h_
+
+# include "DummyMediaDataDecoder.h"
+# include "PlatformDecoderModule.h"
+
+namespace mozilla {
+
+namespace layers {
+class ImageContainer;
+}
+
+class MediaData;
+class MediaRawData;
+
+class BlankVideoDataCreator : public DummyDataCreator {
+ public:
+ BlankVideoDataCreator(uint32_t aFrameWidth, uint32_t aFrameHeight,
+ layers::ImageContainer* aImageContainer);
+
+ already_AddRefed<MediaData> Create(MediaRawData* aSample) override;
+
+ private:
+ VideoInfo mInfo;
+ gfx::IntRect mPicture;
+ uint32_t mFrameWidth;
+ uint32_t mFrameHeight;
+ RefPtr<layers::ImageContainer> mImageContainer;
+};
+
+class BlankAudioDataCreator : public DummyDataCreator {
+ public:
+ BlankAudioDataCreator(uint32_t aChannelCount, uint32_t aSampleRate);
+
+ already_AddRefed<MediaData> Create(MediaRawData* aSample) override;
+
+ private:
+ int64_t mFrameSum;
+ uint32_t mChannelCount;
+ uint32_t mSampleRate;
+};
+
+class BlankDecoderModule : public PlatformDecoderModule {
+ template <typename T, typename... Args>
+ friend already_AddRefed<T> MakeAndAddRef(Args&&...);
+
+ public:
+ static already_AddRefed<PlatformDecoderModule> Create();
+
+ already_AddRefed<MediaDataDecoder> CreateVideoDecoder(
+ const CreateDecoderParams& aParams) override;
+
+ already_AddRefed<MediaDataDecoder> CreateAudioDecoder(
+ const CreateDecoderParams& aParams) override;
+
+ media::DecodeSupportSet SupportsMimeType(
+ const nsACString& aMimeType,
+ DecoderDoctorDiagnostics* aDiagnostics) const override;
+};
+
+} // namespace mozilla
+
+#endif /* BlankDecoderModule_h_ */
diff --git a/dom/media/platforms/agnostic/DAV1DDecoder.cpp b/dom/media/platforms/agnostic/DAV1DDecoder.cpp
new file mode 100644
index 0000000000..7e4af2bd88
--- /dev/null
+++ b/dom/media/platforms/agnostic/DAV1DDecoder.cpp
@@ -0,0 +1,382 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DAV1DDecoder.h"
+
+#include "gfxUtils.h"
+#include "ImageContainer.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/TaskQueue.h"
+#include "nsThreadUtils.h"
+#include "PerformanceRecorder.h"
+#include "VideoUtils.h"
+
+#undef LOG
+#define LOG(arg, ...) \
+ DDMOZ_LOG(sPDMLog, mozilla::LogLevel::Debug, "::%s: " arg, __func__, \
+ ##__VA_ARGS__)
+
+namespace mozilla {
+
+static int GetDecodingThreadCount(uint32_t aCodedHeight) {
+ /**
+ * Based on the result we print out from the dav1decoder [1], the
+ * following information shows the number of tiles for AV1 videos served on
+ * Youtube. Each Tile can be decoded in parallel, so we would like to make
+ * sure we at least use enough threads to match the number of tiles.
+ *
+ * ----------------------------
+ * | resolution row col total |
+ * | 480p 2 1 2 |
+ * | 720p 2 2 4 |
+ * | 1080p 4 2 8 |
+ * | 1440p 4 2 8 |
+ * | 2160p 8 4 32 |
+ * ----------------------------
+ *
+ * Besides the tile thread count, the frame thread count also needs to be
+ * considered. As we didn't find anything about what the best number is for
+ * the count of frame thread, just simply use 2 for parallel jobs, which
+ * is similar with Chromium's implementation. They uses 3 frame threads for
+ * 720p+ but less tile threads, so we will still use more total threads. In
+ * addition, their data is measured on 2019, our data should be closer to the
+ * current real world situation.
+ * [1]
+ * https://searchfox.org/mozilla-central/rev/2f5ed7b7244172d46f538051250b14fb4d8f1a5f/third_party/dav1d/src/decode.c#2940
+ */
+ int tileThreads = 2, frameThreads = 2;
+ if (aCodedHeight >= 2160) {
+ tileThreads = 32;
+ } else if (aCodedHeight >= 1080) {
+ tileThreads = 8;
+ } else if (aCodedHeight >= 720) {
+ tileThreads = 4;
+ }
+ return tileThreads * frameThreads;
+}
+
+DAV1DDecoder::DAV1DDecoder(const CreateDecoderParams& aParams)
+ : mInfo(aParams.VideoConfig()),
+ mTaskQueue(TaskQueue::Create(
+ GetMediaThreadPool(MediaThreadType::PLATFORM_DECODER),
+ "Dav1dDecoder")),
+ mImageContainer(aParams.mImageContainer),
+ mImageAllocator(aParams.mKnowsCompositor),
+ mTrackingId(aParams.mTrackingId) {}
+
+DAV1DDecoder::~DAV1DDecoder() = default;
+
+RefPtr<MediaDataDecoder::InitPromise> DAV1DDecoder::Init() {
+ Dav1dSettings settings;
+ dav1d_default_settings(&settings);
+ size_t decoder_threads = 2;
+ if (mInfo.mDisplay.width >= 2048) {
+ decoder_threads = 8;
+ } else if (mInfo.mDisplay.width >= 1024) {
+ decoder_threads = 4;
+ }
+ if (StaticPrefs::media_av1_new_thread_count_strategy()) {
+ decoder_threads = GetDecodingThreadCount(mInfo.mImage.Height());
+ }
+ // Still need to consider the amount of physical cores in order to achieve
+ // best performance.
+ settings.n_threads =
+ static_cast<int>(std::min(decoder_threads, GetNumberOfProcessors()));
+ if (int32_t count = StaticPrefs::media_av1_force_thread_count(); count > 0) {
+ settings.n_threads = count;
+ }
+
+ int res = dav1d_open(&mContext, &settings);
+ if (res < 0) {
+ return DAV1DDecoder::InitPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Couldn't get dAV1d decoder interface.")),
+ __func__);
+ }
+ return DAV1DDecoder::InitPromise::CreateAndResolve(TrackInfo::kVideoTrack,
+ __func__);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> DAV1DDecoder::Decode(
+ MediaRawData* aSample) {
+ return InvokeAsync<MediaRawData*>(mTaskQueue, this, __func__,
+ &DAV1DDecoder::InvokeDecode, aSample);
+}
+
+void ReleaseDataBuffer_s(const uint8_t* buf, void* user_data) {
+ MOZ_ASSERT(user_data);
+ MOZ_ASSERT(buf);
+ DAV1DDecoder* d = static_cast<DAV1DDecoder*>(user_data);
+ d->ReleaseDataBuffer(buf);
+}
+
+void DAV1DDecoder::ReleaseDataBuffer(const uint8_t* buf) {
+ // The release callback may be called on a different thread defined by the
+ // third party dav1d execution. In that case post a task into TaskQueue to
+ // ensure that mDecodingBuffers is only ever accessed on the TaskQueue.
+ RefPtr<DAV1DDecoder> self = this;
+ auto releaseBuffer = [self, buf] {
+ MOZ_ASSERT(self->mTaskQueue->IsCurrentThreadIn());
+ DebugOnly<bool> found = self->mDecodingBuffers.Remove(buf);
+ MOZ_ASSERT(found);
+ };
+
+ if (mTaskQueue->IsCurrentThreadIn()) {
+ releaseBuffer();
+ } else {
+ nsresult rv = mTaskQueue->Dispatch(NS_NewRunnableFunction(
+ "DAV1DDecoder::ReleaseDataBuffer", std::move(releaseBuffer)));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ }
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> DAV1DDecoder::InvokeDecode(
+ MediaRawData* aSample) {
+ MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
+ MOZ_ASSERT(aSample);
+
+ MediaInfoFlag flag = MediaInfoFlag::None;
+ flag |= (aSample->mKeyframe ? MediaInfoFlag::KeyFrame
+ : MediaInfoFlag::NonKeyFrame);
+ flag |= MediaInfoFlag::SoftwareDecoding;
+ flag |= MediaInfoFlag::VIDEO_AV1;
+ mTrackingId.apply([&](const auto& aId) {
+ mPerformanceRecorder.Start(aSample->mTimecode.ToMicroseconds(),
+ "DAV1DDecoder"_ns, aId, flag);
+ });
+
+ // Add the buffer to the hashtable in order to increase
+ // the ref counter and keep it alive. When dav1d does not
+ // need it any more will call it's release callback. Remove
+ // the buffer, in there, to reduce the ref counter and eventually
+ // free it. We need a hashtable and not an array because the
+ // release callback are not coming in the same order that the
+ // buffers have been added in the decoder (threading ordering
+ // inside decoder)
+ mDecodingBuffers.InsertOrUpdate(aSample->Data(), RefPtr{aSample});
+ Dav1dData data;
+ int res = dav1d_data_wrap(&data, aSample->Data(), aSample->Size(),
+ ReleaseDataBuffer_s, this);
+ data.m.timestamp = aSample->mTimecode.ToMicroseconds();
+ data.m.duration = aSample->mDuration.ToMicroseconds();
+ data.m.offset = aSample->mOffset;
+
+ if (res < 0) {
+ LOG("Create decoder data error.");
+ return DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__), __func__);
+ }
+ DecodedData results;
+ do {
+ res = dav1d_send_data(mContext, &data);
+ if (res < 0 && res != -EAGAIN) {
+ LOG("Decode error: %d", res);
+ return DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR, __func__), __func__);
+ }
+ // Alway consume the whole buffer on success.
+ // At this point only -EAGAIN error is expected.
+ MOZ_ASSERT((res == 0 && !data.sz) ||
+ (res == -EAGAIN && data.sz == aSample->Size()));
+
+ MediaResult rs(NS_OK);
+ res = GetPicture(results, rs);
+ if (res < 0) {
+ if (res == -EAGAIN) {
+ // No frames ready to return. This is not an
+ // error, in some circumstances, we need to
+ // feed it with a certain amount of frames
+ // before we get a picture.
+ continue;
+ }
+ return DecodePromise::CreateAndReject(rs, __func__);
+ }
+ } while (data.sz > 0);
+
+ return DecodePromise::CreateAndResolve(std::move(results), __func__);
+}
+
+int DAV1DDecoder::GetPicture(DecodedData& aData, MediaResult& aResult) {
+ class Dav1dPictureWrapper {
+ public:
+ Dav1dPicture* operator&() { return &p; }
+ const Dav1dPicture& operator*() const { return p; }
+ ~Dav1dPictureWrapper() { dav1d_picture_unref(&p); }
+
+ private:
+ Dav1dPicture p = Dav1dPicture();
+ };
+ Dav1dPictureWrapper picture;
+
+ int res = dav1d_get_picture(mContext, &picture);
+ if (res < 0) {
+ LOG("Decode error: %d", res);
+ aResult = MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR, __func__);
+ return res;
+ }
+
+ if ((*picture).p.layout == DAV1D_PIXEL_LAYOUT_I400) {
+ return 0;
+ }
+
+ RefPtr<VideoData> v = ConstructImage(*picture);
+ if (!v) {
+ LOG("Image allocation error: %ux%u"
+ " display %ux%u picture %ux%u",
+ (*picture).p.w, (*picture).p.h, mInfo.mDisplay.width,
+ mInfo.mDisplay.height, mInfo.mImage.width, mInfo.mImage.height);
+ aResult = MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__);
+ return -1;
+ }
+ aData.AppendElement(std::move(v));
+ return 0;
+}
+
+/* static */
+Maybe<gfx::YUVColorSpace> DAV1DDecoder::GetColorSpace(
+ const Dav1dPicture& aPicture, LazyLogModule& aLogger) {
+ // When returning Nothing(), the caller chooses the appropriate default.
+ if (!aPicture.seq_hdr || !aPicture.seq_hdr->color_description_present) {
+ return Nothing();
+ }
+
+ return gfxUtils::CicpToColorSpace(
+ static_cast<gfx::CICP::MatrixCoefficients>(aPicture.seq_hdr->mtrx),
+ static_cast<gfx::CICP::ColourPrimaries>(aPicture.seq_hdr->pri), aLogger);
+}
+
+/* static */
+Maybe<gfx::ColorSpace2> DAV1DDecoder::GetColorPrimaries(
+ const Dav1dPicture& aPicture, LazyLogModule& aLogger) {
+ // When returning Nothing(), the caller chooses the appropriate default.
+ if (!aPicture.seq_hdr || !aPicture.seq_hdr->color_description_present) {
+ return Nothing();
+ }
+
+ return gfxUtils::CicpToColorPrimaries(
+ static_cast<gfx::CICP::ColourPrimaries>(aPicture.seq_hdr->pri), aLogger);
+}
+
+already_AddRefed<VideoData> DAV1DDecoder::ConstructImage(
+ const Dav1dPicture& aPicture) {
+ VideoData::YCbCrBuffer b;
+ if (aPicture.p.bpc == 10) {
+ b.mColorDepth = gfx::ColorDepth::COLOR_10;
+ } else if (aPicture.p.bpc == 12) {
+ b.mColorDepth = gfx::ColorDepth::COLOR_12;
+ } else {
+ b.mColorDepth = gfx::ColorDepth::COLOR_8;
+ }
+
+ b.mYUVColorSpace =
+ DAV1DDecoder::GetColorSpace(aPicture, sPDMLog)
+ .valueOr(DefaultColorSpace({aPicture.p.w, aPicture.p.h}));
+ b.mColorPrimaries = DAV1DDecoder::GetColorPrimaries(aPicture, sPDMLog)
+ .valueOr(gfx::ColorSpace2::BT709);
+ b.mColorRange = aPicture.seq_hdr->color_range ? gfx::ColorRange::FULL
+ : gfx::ColorRange::LIMITED;
+
+ b.mPlanes[0].mData = static_cast<uint8_t*>(aPicture.data[0]);
+ b.mPlanes[0].mStride = aPicture.stride[0];
+ b.mPlanes[0].mHeight = aPicture.p.h;
+ b.mPlanes[0].mWidth = aPicture.p.w;
+ b.mPlanes[0].mSkip = 0;
+
+ b.mPlanes[1].mData = static_cast<uint8_t*>(aPicture.data[1]);
+ b.mPlanes[1].mStride = aPicture.stride[1];
+ b.mPlanes[1].mSkip = 0;
+
+ b.mPlanes[2].mData = static_cast<uint8_t*>(aPicture.data[2]);
+ b.mPlanes[2].mStride = aPicture.stride[1];
+ b.mPlanes[2].mSkip = 0;
+
+ // https://code.videolan.org/videolan/dav1d/blob/master/tools/output/yuv.c#L67
+ const int ss_ver = aPicture.p.layout == DAV1D_PIXEL_LAYOUT_I420;
+ const int ss_hor = aPicture.p.layout != DAV1D_PIXEL_LAYOUT_I444;
+
+ b.mPlanes[1].mHeight = (aPicture.p.h + ss_ver) >> ss_ver;
+ b.mPlanes[1].mWidth = (aPicture.p.w + ss_hor) >> ss_hor;
+
+ b.mPlanes[2].mHeight = (aPicture.p.h + ss_ver) >> ss_ver;
+ b.mPlanes[2].mWidth = (aPicture.p.w + ss_hor) >> ss_hor;
+
+ if (ss_ver) {
+ b.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT;
+ } else if (ss_hor) {
+ b.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH;
+ }
+
+ // Timestamp, duration and offset used here are wrong.
+ // We need to take those values from the decoder. Latest
+ // dav1d version allows for that.
+ media::TimeUnit timecode =
+ media::TimeUnit::FromMicroseconds(aPicture.m.timestamp);
+ media::TimeUnit duration =
+ media::TimeUnit::FromMicroseconds(aPicture.m.duration);
+ int64_t offset = aPicture.m.offset;
+ bool keyframe = aPicture.frame_hdr->frame_type == DAV1D_FRAME_TYPE_KEY;
+
+ mPerformanceRecorder.Record(aPicture.m.timestamp, [&](DecodeStage& aStage) {
+ aStage.SetResolution(aPicture.p.w, aPicture.p.h);
+ auto format = [&]() -> Maybe<DecodeStage::ImageFormat> {
+ switch (aPicture.p.layout) {
+ case DAV1D_PIXEL_LAYOUT_I420:
+ return Some(DecodeStage::YUV420P);
+ case DAV1D_PIXEL_LAYOUT_I422:
+ return Some(DecodeStage::YUV422P);
+ case DAV1D_PIXEL_LAYOUT_I444:
+ return Some(DecodeStage::YUV444P);
+ default:
+ return Nothing();
+ }
+ }();
+ format.apply([&](auto& aFmt) { aStage.SetImageFormat(aFmt); });
+ aStage.SetYUVColorSpace(b.mYUVColorSpace);
+ aStage.SetColorRange(b.mColorRange);
+ aStage.SetColorDepth(b.mColorDepth);
+ });
+
+ return VideoData::CreateAndCopyData(
+ mInfo, mImageContainer, offset, timecode, duration, b, keyframe, timecode,
+ mInfo.ScaledImageRect(aPicture.p.w, aPicture.p.h), mImageAllocator);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> DAV1DDecoder::Drain() {
+ RefPtr<DAV1DDecoder> self = this;
+ return InvokeAsync(mTaskQueue, __func__, [self, this] {
+ int res = 0;
+ DecodedData results;
+ do {
+ MediaResult rs(NS_OK);
+ res = GetPicture(results, rs);
+ if (res < 0 && res != -EAGAIN) {
+ return DecodePromise::CreateAndReject(rs, __func__);
+ }
+ } while (res != -EAGAIN);
+ return DecodePromise::CreateAndResolve(std::move(results), __func__);
+ });
+}
+
+RefPtr<MediaDataDecoder::FlushPromise> DAV1DDecoder::Flush() {
+ RefPtr<DAV1DDecoder> self = this;
+ return InvokeAsync(mTaskQueue, __func__, [this, self]() {
+ dav1d_flush(self->mContext);
+ mPerformanceRecorder.Record(std::numeric_limits<int64_t>::max());
+ return FlushPromise::CreateAndResolve(true, __func__);
+ });
+}
+
+RefPtr<ShutdownPromise> DAV1DDecoder::Shutdown() {
+ RefPtr<DAV1DDecoder> self = this;
+ return InvokeAsync(mTaskQueue, __func__, [self]() {
+ dav1d_close(&self->mContext);
+ return self->mTaskQueue->BeginShutdown();
+ });
+}
+
+} // namespace mozilla
+#undef LOG
diff --git a/dom/media/platforms/agnostic/DAV1DDecoder.h b/dom/media/platforms/agnostic/DAV1DDecoder.h
new file mode 100644
index 0000000000..3a7f4d4ee4
--- /dev/null
+++ b/dom/media/platforms/agnostic/DAV1DDecoder.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#if !defined(DAV1DDecoder_h_)
+# define DAV1DDecoder_h_
+
+# include "PerformanceRecorder.h"
+# include "PlatformDecoderModule.h"
+# include "dav1d/dav1d.h"
+# include "nsRefPtrHashtable.h"
+
+namespace mozilla {
+
+DDLoggedTypeDeclNameAndBase(DAV1DDecoder, MediaDataDecoder);
+
+typedef nsRefPtrHashtable<nsPtrHashKey<const uint8_t>, MediaRawData>
+ MediaRawDataHashtable;
+
+class DAV1DDecoder final : public MediaDataDecoder,
+ public DecoderDoctorLifeLogger<DAV1DDecoder> {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DAV1DDecoder, final);
+
+ explicit DAV1DDecoder(const CreateDecoderParams& aParams);
+
+ RefPtr<InitPromise> Init() override;
+ RefPtr<DecodePromise> Decode(MediaRawData* aSample) override;
+ RefPtr<DecodePromise> Drain() override;
+ RefPtr<FlushPromise> Flush() override;
+ RefPtr<ShutdownPromise> Shutdown() override;
+ nsCString GetDescriptionName() const override {
+ return "av1 libdav1d video decoder"_ns;
+ }
+ nsCString GetCodecName() const override { return "av1"_ns; }
+
+ void ReleaseDataBuffer(const uint8_t* buf);
+
+ static Maybe<gfx::YUVColorSpace> GetColorSpace(const Dav1dPicture& aPicture,
+ LazyLogModule& aLogger);
+
+ static Maybe<gfx::ColorSpace2> GetColorPrimaries(const Dav1dPicture& aPicture,
+ LazyLogModule& aLogger);
+
+ private:
+ virtual ~DAV1DDecoder();
+ RefPtr<DecodePromise> InvokeDecode(MediaRawData* aSample);
+ int GetPicture(DecodedData& aData, MediaResult& aResult);
+ already_AddRefed<VideoData> ConstructImage(const Dav1dPicture& aPicture);
+
+ Dav1dContext* mContext = nullptr;
+
+ const VideoInfo mInfo;
+ const RefPtr<TaskQueue> mTaskQueue;
+ const RefPtr<layers::ImageContainer> mImageContainer;
+ const RefPtr<layers::KnowsCompositor> mImageAllocator;
+ const Maybe<TrackingId> mTrackingId;
+ PerformanceRecorderMulti<DecodeStage> mPerformanceRecorder;
+
+ // Keep the buffers alive until dav1d
+ // does not need them any more.
+ MediaRawDataHashtable mDecodingBuffers;
+};
+
+} // namespace mozilla
+
+#endif // DAV1DDecoder_h_
diff --git a/dom/media/platforms/agnostic/DummyMediaDataDecoder.cpp b/dom/media/platforms/agnostic/DummyMediaDataDecoder.cpp
new file mode 100644
index 0000000000..172c7f3dd9
--- /dev/null
+++ b/dom/media/platforms/agnostic/DummyMediaDataDecoder.cpp
@@ -0,0 +1,80 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DummyMediaDataDecoder.h"
+#include "AnnexB.h"
+#include "H264.h"
+#include "MP4Decoder.h"
+
+namespace mozilla {
+
+DummyDataCreator::~DummyDataCreator() = default;
+
+DummyMediaDataDecoder::DummyMediaDataDecoder(
+ UniquePtr<DummyDataCreator>&& aCreator, const nsACString& aDescription,
+ const CreateDecoderParams& aParams)
+ : mCreator(std::move(aCreator)),
+ mIsH264(MP4Decoder::IsH264(aParams.mConfig.mMimeType)),
+ mMaxRefFrames(mIsH264 ? H264::HasSPS(aParams.VideoConfig().mExtraData)
+ ? H264::ComputeMaxRefFrames(
+ aParams.VideoConfig().mExtraData)
+ : 16
+ : 0),
+ mType(aParams.mConfig.GetType()),
+ mDescription(aDescription) {}
+
+RefPtr<MediaDataDecoder::InitPromise> DummyMediaDataDecoder::Init() {
+ return InitPromise::CreateAndResolve(mType, __func__);
+}
+
+RefPtr<ShutdownPromise> DummyMediaDataDecoder::Shutdown() {
+ return ShutdownPromise::CreateAndResolve(true, __func__);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> DummyMediaDataDecoder::Decode(
+ MediaRawData* aSample) {
+ RefPtr<MediaData> data = mCreator->Create(aSample);
+
+ if (!data) {
+ return DecodePromise::CreateAndReject(NS_ERROR_OUT_OF_MEMORY, __func__);
+ }
+
+ // Frames come out in DTS order but we need to output them in PTS order.
+ mReorderQueue.Push(std::move(data));
+
+ if (mReorderQueue.Length() > mMaxRefFrames) {
+ return DecodePromise::CreateAndResolve(DecodedData{mReorderQueue.Pop()},
+ __func__);
+ }
+ return DecodePromise::CreateAndResolve(DecodedData(), __func__);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> DummyMediaDataDecoder::Drain() {
+ DecodedData samples;
+ while (!mReorderQueue.IsEmpty()) {
+ samples.AppendElement(mReorderQueue.Pop());
+ }
+ return DecodePromise::CreateAndResolve(std::move(samples), __func__);
+}
+
+RefPtr<MediaDataDecoder::FlushPromise> DummyMediaDataDecoder::Flush() {
+ mReorderQueue.Clear();
+ return FlushPromise::CreateAndResolve(true, __func__);
+}
+
+nsCString DummyMediaDataDecoder::GetDescriptionName() const {
+ return "blank media data decoder"_ns;
+}
+
+nsCString DummyMediaDataDecoder::GetCodecName() const { return "unknown"_ns; }
+
+MediaDataDecoder::ConversionRequired DummyMediaDataDecoder::NeedsConversion()
+ const {
+ return mIsH264 ? ConversionRequired::kNeedAVCC
+ : ConversionRequired::kNeedNone;
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/agnostic/DummyMediaDataDecoder.h b/dom/media/platforms/agnostic/DummyMediaDataDecoder.h
new file mode 100644
index 0000000000..562d289bd9
--- /dev/null
+++ b/dom/media/platforms/agnostic/DummyMediaDataDecoder.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(DummyMediaDataDecoder_h_)
+# define DummyMediaDataDecoder_h_
+
+# include "MediaInfo.h"
+# include "mozilla/UniquePtr.h"
+# include "PlatformDecoderModule.h"
+# include "ReorderQueue.h"
+
+namespace mozilla {
+
+class MediaRawData;
+
+class DummyDataCreator {
+ public:
+ virtual ~DummyDataCreator();
+ virtual already_AddRefed<MediaData> Create(MediaRawData* aSample) = 0;
+};
+
+DDLoggedTypeDeclNameAndBase(DummyMediaDataDecoder, MediaDataDecoder);
+
+// Decoder that uses a passed in object's Create function to create Null
+// MediaData objects.
+class DummyMediaDataDecoder final
+ : public MediaDataDecoder,
+ public DecoderDoctorLifeLogger<DummyMediaDataDecoder> {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DummyMediaDataDecoder, final);
+
+ DummyMediaDataDecoder(UniquePtr<DummyDataCreator>&& aCreator,
+ const nsACString& aDescription,
+ const CreateDecoderParams& aParams);
+
+ RefPtr<InitPromise> Init() override;
+
+ RefPtr<ShutdownPromise> Shutdown() override;
+
+ RefPtr<DecodePromise> Decode(MediaRawData* aSample) override;
+
+ RefPtr<DecodePromise> Drain() override;
+
+ RefPtr<FlushPromise> Flush() override;
+
+ nsCString GetDescriptionName() const override;
+
+ nsCString GetCodecName() const override;
+
+ ConversionRequired NeedsConversion() const override;
+
+ private:
+ ~DummyMediaDataDecoder() = default;
+
+ UniquePtr<DummyDataCreator> mCreator;
+ const bool mIsH264;
+ const uint32_t mMaxRefFrames;
+ ReorderQueue mReorderQueue;
+ TrackInfo::TrackType mType;
+ nsCString mDescription;
+};
+
+} // namespace mozilla
+
+#endif // !defined(DummyMediaDataDecoder_h_)
diff --git a/dom/media/platforms/agnostic/NullDecoderModule.cpp b/dom/media/platforms/agnostic/NullDecoderModule.cpp
new file mode 100644
index 0000000000..2893a4af67
--- /dev/null
+++ b/dom/media/platforms/agnostic/NullDecoderModule.cpp
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DummyMediaDataDecoder.h"
+#include "ImageContainer.h"
+
+namespace mozilla {
+
+class NullVideoDataCreator : public DummyDataCreator {
+ public:
+ NullVideoDataCreator() = default;
+
+ already_AddRefed<MediaData> Create(MediaRawData* aSample) override {
+ // Create a dummy VideoData with an empty image. This gives us something to
+ // send to media streams if necessary.
+ RefPtr<layers::PlanarYCbCrImage> image =
+ new layers::RecyclingPlanarYCbCrImage(new layers::BufferRecycleBin());
+ return VideoData::CreateFromImage(gfx::IntSize(), aSample->mOffset,
+ aSample->mTime, aSample->mDuration, image,
+ aSample->mKeyframe, aSample->mTimecode);
+ }
+};
+
+class NullDecoderModule : public PlatformDecoderModule {
+ public:
+ // Decode thread.
+ already_AddRefed<MediaDataDecoder> CreateVideoDecoder(
+ const CreateDecoderParams& aParams) override {
+ UniquePtr<DummyDataCreator> creator = MakeUnique<NullVideoDataCreator>();
+ RefPtr<MediaDataDecoder> decoder = new DummyMediaDataDecoder(
+ std::move(creator), "null media data decoder"_ns, aParams);
+ return decoder.forget();
+ }
+
+ // Decode thread.
+ already_AddRefed<MediaDataDecoder> CreateAudioDecoder(
+ const CreateDecoderParams& aParams) override {
+ MOZ_ASSERT(false, "Audio decoders are unsupported.");
+ return nullptr;
+ }
+
+ media::DecodeSupportSet SupportsMimeType(
+ const nsACString& aMimeType,
+ DecoderDoctorDiagnostics* aDiagnostics) const override {
+ return media::DecodeSupport::SoftwareDecode;
+ }
+};
+
+already_AddRefed<PlatformDecoderModule> CreateNullDecoderModule() {
+ RefPtr<PlatformDecoderModule> pdm = new NullDecoderModule();
+ return pdm.forget();
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/agnostic/OpusDecoder.cpp b/dom/media/platforms/agnostic/OpusDecoder.cpp
new file mode 100644
index 0000000000..715fa848dc
--- /dev/null
+++ b/dom/media/platforms/agnostic/OpusDecoder.cpp
@@ -0,0 +1,380 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "OpusDecoder.h"
+
+#include <inttypes.h> // For PRId64
+
+#include "OpusParser.h"
+#include "TimeUnits.h"
+#include "VideoUtils.h"
+#include "VorbisDecoder.h" // For VorbisLayout
+#include "VorbisUtils.h"
+#include "mozilla/EndianUtils.h"
+#include "mozilla/PodOperations.h"
+#include "mozilla/SyncRunnable.h"
+#include "opus/opus.h"
+extern "C" {
+#include "opus/opus_multistream.h"
+}
+
+#define OPUS_DEBUG(arg, ...) \
+ DDMOZ_LOG(sPDMLog, mozilla::LogLevel::Debug, "::%s: " arg, __func__, \
+ ##__VA_ARGS__)
+
+namespace mozilla {
+
+OpusDataDecoder::OpusDataDecoder(const CreateDecoderParams& aParams)
+ : mInfo(aParams.AudioConfig()),
+ mOpusDecoder(nullptr),
+ mSkip(0),
+ mDecodedHeader(false),
+ mPaddingDiscarded(false),
+ mFrames(0),
+ mChannelMap(AudioConfig::ChannelLayout::UNKNOWN_MAP),
+ mDefaultPlaybackDeviceMono(aParams.mOptions.contains(
+ CreateDecoderParams::Option::DefaultPlaybackDeviceMono)) {}
+
+OpusDataDecoder::~OpusDataDecoder() {
+ if (mOpusDecoder) {
+ opus_multistream_decoder_destroy(mOpusDecoder);
+ mOpusDecoder = nullptr;
+ }
+}
+
+RefPtr<ShutdownPromise> OpusDataDecoder::Shutdown() {
+ // mThread may not be set if Init hasn't been called first.
+ MOZ_ASSERT(!mThread || mThread->IsOnCurrentThread());
+ return ShutdownPromise::CreateAndResolve(true, __func__);
+}
+
+RefPtr<MediaDataDecoder::InitPromise> OpusDataDecoder::Init() {
+ mThread = GetCurrentSerialEventTarget();
+ if (!mInfo.mCodecSpecificConfig.is<OpusCodecSpecificData>()) {
+ MOZ_ASSERT_UNREACHABLE();
+ OPUS_DEBUG("Opus decoder got non-opus codec specific data");
+ return InitPromise::CreateAndReject(
+ MediaResult(
+ NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Opus decoder got non-opus codec specific data!")),
+ __func__);
+ }
+ const OpusCodecSpecificData opusCodecSpecificData =
+ mInfo.mCodecSpecificConfig.as<OpusCodecSpecificData>();
+ RefPtr<MediaByteBuffer> opusHeaderBlob =
+ opusCodecSpecificData.mHeadersBinaryBlob;
+ size_t length = opusHeaderBlob->Length();
+ uint8_t* p = opusHeaderBlob->Elements();
+ if (NS_FAILED(DecodeHeader(p, length))) {
+ OPUS_DEBUG("Error decoding header!");
+ return InitPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Error decoding header!")),
+ __func__);
+ }
+
+ MOZ_ASSERT(mMappingTable.Length() >= uint32_t(mOpusParser->mChannels));
+ int r;
+ mOpusDecoder = opus_multistream_decoder_create(
+ mOpusParser->mRate, mOpusParser->mChannels, mOpusParser->mStreams,
+ mOpusParser->mCoupledStreams, mMappingTable.Elements(), &r);
+
+ if (!mOpusDecoder) {
+ OPUS_DEBUG("Error creating decoder!");
+ return InitPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Error creating decoder!")),
+ __func__);
+ }
+
+ // Opus has a special feature for stereo coding where it represent wide
+ // stereo channels by 180-degree out of phase. This improves quality, but
+ // needs to be disabled when the output is downmixed to mono. Playback number
+ // of channels are set in AudioSink, using the same method
+ // `DecideAudioPlaybackChannels()`, and triggers downmix if needed.
+ if (mDefaultPlaybackDeviceMono || DecideAudioPlaybackChannels(mInfo) == 1) {
+ opus_multistream_decoder_ctl(mOpusDecoder,
+ OPUS_SET_PHASE_INVERSION_DISABLED(1));
+ }
+
+ mSkip = mOpusParser->mPreSkip;
+ mPaddingDiscarded = false;
+
+ if (opusCodecSpecificData.mContainerCodecDelayMicroSeconds !=
+ FramesToUsecs(mOpusParser->mPreSkip, mOpusParser->mRate).value()) {
+ NS_WARNING(
+ "Invalid Opus header: container CodecDelay and Opus pre-skip do not "
+ "match!");
+ }
+ OPUS_DEBUG("Opus preskip in extradata: %" PRId64 "us",
+ opusCodecSpecificData.mContainerCodecDelayMicroSeconds);
+
+ if (mInfo.mRate != (uint32_t)mOpusParser->mRate) {
+ NS_WARNING("Invalid Opus header: container and codec rate do not match!");
+ }
+ if (mInfo.mChannels != (uint32_t)mOpusParser->mChannels) {
+ NS_WARNING(
+ "Invalid Opus header: container and codec channels do not match!");
+ }
+
+ return r == OPUS_OK
+ ? InitPromise::CreateAndResolve(TrackInfo::kAudioTrack, __func__)
+ : InitPromise::CreateAndReject(
+ MediaResult(
+ NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL(
+ "could not create opus multistream decoder!")),
+ __func__);
+}
+
+nsresult OpusDataDecoder::DecodeHeader(const unsigned char* aData,
+ size_t aLength) {
+ MOZ_ASSERT(!mOpusParser);
+ MOZ_ASSERT(!mOpusDecoder);
+ MOZ_ASSERT(!mDecodedHeader);
+ mDecodedHeader = true;
+
+ mOpusParser = MakeUnique<OpusParser>();
+ if (!mOpusParser->DecodeHeader(const_cast<unsigned char*>(aData), aLength)) {
+ return NS_ERROR_FAILURE;
+ }
+ int channels = mOpusParser->mChannels;
+
+ mMappingTable.SetLength(channels);
+ AudioConfig::ChannelLayout vorbisLayout(
+ channels, VorbisDataDecoder::VorbisLayout(channels));
+ if (vorbisLayout.IsValid()) {
+ mChannelMap = vorbisLayout.Map();
+
+ AudioConfig::ChannelLayout smpteLayout(
+ AudioConfig::ChannelLayout::SMPTEDefault(vorbisLayout));
+
+ AutoTArray<uint8_t, 8> map;
+ map.SetLength(channels);
+ if (mOpusParser->mChannelMapping == 1 &&
+ vorbisLayout.MappingTable(smpteLayout, &map)) {
+ for (int i = 0; i < channels; i++) {
+ mMappingTable[i] = mOpusParser->mMappingTable[map[i]];
+ }
+ } else {
+ // Use Opus set channel mapping and return channels as-is.
+ PodCopy(mMappingTable.Elements(), mOpusParser->mMappingTable, channels);
+ }
+ } else {
+ // Create a dummy mapping table so that channel ordering stay the same
+ // during decoding.
+ for (int i = 0; i < channels; i++) {
+ mMappingTable[i] = i;
+ }
+ }
+
+ return NS_OK;
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> OpusDataDecoder::Decode(
+ MediaRawData* aSample) {
+ MOZ_ASSERT(mThread->IsOnCurrentThread());
+ PROCESS_DECODE_LOG(aSample);
+ uint32_t channels = mOpusParser->mChannels;
+
+ if (mPaddingDiscarded) {
+ // Discard padding should be used only on the final packet, so
+ // decoding after a padding discard is invalid.
+ OPUS_DEBUG("Opus error, discard padding on interstitial packet");
+ return DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Discard padding on interstitial packet")),
+ __func__);
+ }
+
+ if (!mLastFrameTime ||
+ mLastFrameTime.ref() != aSample->mTime.ToMicroseconds()) {
+ // We are starting a new block.
+ mFrames = 0;
+ mLastFrameTime = Some(aSample->mTime.ToMicroseconds());
+ }
+
+ // Maximum value is 63*2880, so there's no chance of overflow.
+ int frames_number =
+ opus_packet_get_nb_frames(aSample->Data(), aSample->Size());
+ if (frames_number <= 0) {
+ OPUS_DEBUG("Invalid packet header: r=%d length=%zu", frames_number,
+ aSample->Size());
+ return DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ RESULT_DETAIL("Invalid packet header: r=%d length=%u",
+ frames_number, uint32_t(aSample->Size()))),
+ __func__);
+ }
+
+ int samples = opus_packet_get_samples_per_frame(
+ aSample->Data(), opus_int32(mOpusParser->mRate));
+
+ // A valid Opus packet must be between 2.5 and 120 ms long (48kHz).
+ CheckedInt32 totalFrames =
+ CheckedInt32(frames_number) * CheckedInt32(samples);
+ if (!totalFrames.isValid()) {
+ return DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ RESULT_DETAIL("Frames count overflow")),
+ __func__);
+ }
+
+ int frames = totalFrames.value();
+ if (frames < 120 || frames > 5760) {
+ OPUS_DEBUG("Invalid packet frames: %d", frames);
+ return DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ RESULT_DETAIL("Invalid packet frames:%d", frames)),
+ __func__);
+ }
+
+ AlignedAudioBuffer buffer(frames * channels);
+ if (!buffer) {
+ return DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__), __func__);
+ }
+
+ // Decode to the appropriate sample type.
+#ifdef MOZ_SAMPLE_TYPE_FLOAT32
+ int ret = opus_multistream_decode_float(mOpusDecoder, aSample->Data(),
+ aSample->Size(), buffer.get(), frames,
+ false);
+#else
+ int ret =
+ opus_multistream_decode(mOpusDecoder, aSample->Data(), aSample->Size(),
+ buffer.get(), frames, false);
+#endif
+ if (ret < 0) {
+ return DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ RESULT_DETAIL("Opus decoding error:%d", ret)),
+ __func__);
+ }
+ NS_ASSERTION(ret == frames, "Opus decoded too few audio samples");
+ auto startTime = aSample->mTime;
+
+ OPUS_DEBUG("Decoding frames: [%lf, %lf]", aSample->mTime.ToSeconds(),
+ aSample->GetEndTime().ToSeconds());
+
+ // Trim the initial frames while the decoder is settling.
+ if (mSkip > 0) {
+ int32_t skipFrames = std::min<int32_t>(mSkip, frames);
+ int32_t keepFrames = frames - skipFrames;
+ OPUS_DEBUG("Opus decoder trimming %d of %d frames", skipFrames, frames);
+ PodMove(buffer.get(), buffer.get() + skipFrames * channels,
+ keepFrames * channels);
+ startTime = startTime + media::TimeUnit(skipFrames, mOpusParser->mRate);
+ frames = keepFrames;
+ mSkip -= skipFrames;
+ aSample->mTime += media::TimeUnit(skipFrames, 48000);
+ aSample->mDuration -= media::TimeUnit(skipFrames, 48000);
+ OPUS_DEBUG("Adjusted frame after trimming pre-roll: [%lf, %lf]",
+ aSample->mTime.ToSeconds(), aSample->GetEndTime().ToSeconds());
+ }
+
+ if (aSample->mDiscardPadding > 0) {
+ OPUS_DEBUG("Opus decoder discarding %u of %d frames",
+ aSample->mDiscardPadding, frames);
+ // Padding discard is only supposed to happen on the final packet.
+ // Record the discard so we can return an error if another packet is
+ // decoded.
+ if (aSample->mDiscardPadding > uint32_t(frames)) {
+ // Discarding more than the entire packet is invalid.
+ OPUS_DEBUG("Opus error, discard padding larger than packet");
+ return DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Discard padding larger than packet")),
+ __func__);
+ }
+
+ mPaddingDiscarded = true;
+ frames = frames - aSample->mDiscardPadding;
+ }
+
+ // Apply the header gain if one was specified.
+#ifdef MOZ_SAMPLE_TYPE_FLOAT32
+ if (mOpusParser->mGain != 1.0f) {
+ float gain = mOpusParser->mGain;
+ uint32_t samples = frames * channels;
+ for (uint32_t i = 0; i < samples; i++) {
+ buffer[i] *= gain;
+ }
+ }
+#else
+ if (mOpusParser->mGain_Q16 != 65536) {
+ int64_t gain_Q16 = mOpusParser->mGain_Q16;
+ uint32_t samples = frames * channels;
+ for (uint32_t i = 0; i < samples; i++) {
+ int32_t val = static_cast<int32_t>((gain_Q16 * buffer[i] + 32768) >> 16);
+ buffer[i] = static_cast<AudioDataValue>(MOZ_CLIP_TO_15(val));
+ }
+ }
+#endif
+
+ auto duration = media::TimeUnit(frames, mOpusParser->mRate);
+ if (!duration.IsValid()) {
+ return DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_OVERFLOW_ERR,
+ RESULT_DETAIL("Overflow converting WebM audio duration")),
+ __func__);
+ }
+ auto time = startTime -
+ media::TimeUnit(mOpusParser->mPreSkip, mOpusParser->mRate) +
+ media::TimeUnit(mFrames, mOpusParser->mRate);
+ if (!time.IsValid()) {
+ return DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_OVERFLOW_ERR,
+ RESULT_DETAIL("Overflow shifting tstamp by codec delay")),
+ __func__);
+ };
+
+ mFrames += frames;
+ mTotalFrames += frames;
+
+ OPUS_DEBUG("Total frames so far: %" PRId64, mTotalFrames);
+
+ if (!frames) {
+ return DecodePromise::CreateAndResolve(DecodedData(), __func__);
+ }
+
+ // Trim extra allocated frames.
+ buffer.SetLength(frames * channels);
+
+ return DecodePromise::CreateAndResolve(
+ DecodedData{new AudioData(aSample->mOffset, time, std::move(buffer),
+ mOpusParser->mChannels, mOpusParser->mRate,
+ mChannelMap)},
+ __func__);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> OpusDataDecoder::Drain() {
+ MOZ_ASSERT(mThread->IsOnCurrentThread());
+ return DecodePromise::CreateAndResolve(DecodedData(), __func__);
+}
+
+RefPtr<MediaDataDecoder::FlushPromise> OpusDataDecoder::Flush() {
+ MOZ_ASSERT(mThread->IsOnCurrentThread());
+ if (!mOpusDecoder) {
+ return FlushPromise::CreateAndResolve(true, __func__);
+ }
+
+ MOZ_ASSERT(mOpusDecoder);
+ // Reset the decoder.
+ opus_multistream_decoder_ctl(mOpusDecoder, OPUS_RESET_STATE);
+ mSkip = mOpusParser->mPreSkip;
+ mPaddingDiscarded = false;
+ mLastFrameTime.reset();
+ return FlushPromise::CreateAndResolve(true, __func__);
+}
+
+/* static */
+bool OpusDataDecoder::IsOpus(const nsACString& aMimeType) {
+ return aMimeType.EqualsLiteral("audio/opus");
+}
+
+} // namespace mozilla
+#undef OPUS_DEBUG
diff --git a/dom/media/platforms/agnostic/OpusDecoder.h b/dom/media/platforms/agnostic/OpusDecoder.h
new file mode 100644
index 0000000000..3df3471292
--- /dev/null
+++ b/dom/media/platforms/agnostic/OpusDecoder.h
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#if !defined(OpusDecoder_h_)
+# define OpusDecoder_h_
+
+# include "PlatformDecoderModule.h"
+
+# include "mozilla/Maybe.h"
+# include "nsTArray.h"
+
+struct OpusMSDecoder;
+
+namespace mozilla {
+
+class OpusParser;
+
+DDLoggedTypeDeclNameAndBase(OpusDataDecoder, MediaDataDecoder);
+
+class OpusDataDecoder final : public MediaDataDecoder,
+ public DecoderDoctorLifeLogger<OpusDataDecoder> {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(OpusDataDecoder, final);
+
+ explicit OpusDataDecoder(const CreateDecoderParams& aParams);
+
+ RefPtr<InitPromise> Init() override;
+ RefPtr<DecodePromise> Decode(MediaRawData* aSample) override;
+ RefPtr<DecodePromise> Drain() override;
+ RefPtr<FlushPromise> Flush() override;
+ RefPtr<ShutdownPromise> Shutdown() override;
+ nsCString GetDescriptionName() const override {
+ return "opus audio decoder"_ns;
+ }
+ nsCString GetCodecName() const override { return "opus"_ns; }
+
+ // Return true if mimetype is Opus
+ static bool IsOpus(const nsACString& aMimeType);
+
+ private:
+ ~OpusDataDecoder();
+
+ nsresult DecodeHeader(const unsigned char* aData, size_t aLength);
+
+ const AudioInfo mInfo;
+ nsCOMPtr<nsISerialEventTarget> mThread;
+
+ // Opus decoder state
+ UniquePtr<OpusParser> mOpusParser;
+ OpusMSDecoder* mOpusDecoder;
+
+ uint16_t mSkip; // Samples left to trim before playback.
+ bool mDecodedHeader;
+
+ // Opus padding should only be discarded on the final packet. Once this
+ // is set to true, if the reader attempts to decode any further packets it
+ // will raise an error so we can indicate that the file is invalid.
+ bool mPaddingDiscarded;
+ int64_t mFrames;
+ int64_t mTotalFrames = 0;
+ Maybe<int64_t> mLastFrameTime;
+ AutoTArray<uint8_t, 8> mMappingTable;
+ AudioConfig::ChannelLayout::ChannelMap mChannelMap;
+ bool mDefaultPlaybackDeviceMono;
+};
+
+} // namespace mozilla
+#endif
diff --git a/dom/media/platforms/agnostic/TheoraDecoder.cpp b/dom/media/platforms/agnostic/TheoraDecoder.cpp
new file mode 100644
index 0000000000..468eda9014
--- /dev/null
+++ b/dom/media/platforms/agnostic/TheoraDecoder.cpp
@@ -0,0 +1,267 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TheoraDecoder.h"
+
+#include <algorithm>
+
+#include "ImageContainer.h"
+#include "TimeUnits.h"
+#include "XiphExtradata.h"
+#include "gfx2DGlue.h"
+#include "mozilla/PodOperations.h"
+#include "mozilla/TaskQueue.h"
+#include "nsError.h"
+#include "PerformanceRecorder.h"
+#include "VideoUtils.h"
+
+#undef LOG
+#define LOG(arg, ...) \
+ DDMOZ_LOG(gMediaDecoderLog, mozilla::LogLevel::Debug, "::%s: " arg, \
+ __func__, ##__VA_ARGS__)
+
+namespace mozilla {
+
+using namespace gfx;
+using namespace layers;
+
+extern LazyLogModule gMediaDecoderLog;
+
+ogg_packet InitTheoraPacket(const unsigned char* aData, size_t aLength,
+ bool aBOS, bool aEOS, int64_t aGranulepos,
+ int64_t aPacketNo) {
+ ogg_packet packet;
+ packet.packet = const_cast<unsigned char*>(aData);
+ packet.bytes = aLength;
+ packet.b_o_s = aBOS;
+ packet.e_o_s = aEOS;
+ packet.granulepos = aGranulepos;
+ packet.packetno = aPacketNo;
+ return packet;
+}
+
+TheoraDecoder::TheoraDecoder(const CreateDecoderParams& aParams)
+ : mImageAllocator(aParams.mKnowsCompositor),
+ mImageContainer(aParams.mImageContainer),
+ mTaskQueue(TaskQueue::Create(
+ GetMediaThreadPool(MediaThreadType::PLATFORM_DECODER),
+ "TheoraDecoder")),
+ mTheoraInfo{},
+ mTheoraComment{},
+ mTheoraSetupInfo(nullptr),
+ mTheoraDecoderContext(nullptr),
+ mPacketCount(0),
+ mInfo(aParams.VideoConfig()),
+ mTrackingId(aParams.mTrackingId) {
+ MOZ_COUNT_CTOR(TheoraDecoder);
+}
+
+TheoraDecoder::~TheoraDecoder() {
+ MOZ_COUNT_DTOR(TheoraDecoder);
+ th_setup_free(mTheoraSetupInfo);
+ th_comment_clear(&mTheoraComment);
+ th_info_clear(&mTheoraInfo);
+}
+
+RefPtr<ShutdownPromise> TheoraDecoder::Shutdown() {
+ RefPtr<TheoraDecoder> self = this;
+ return InvokeAsync(mTaskQueue, __func__, [self, this]() {
+ if (mTheoraDecoderContext) {
+ th_decode_free(mTheoraDecoderContext);
+ mTheoraDecoderContext = nullptr;
+ }
+ return mTaskQueue->BeginShutdown();
+ });
+}
+
+RefPtr<MediaDataDecoder::InitPromise> TheoraDecoder::Init() {
+ th_comment_init(&mTheoraComment);
+ th_info_init(&mTheoraInfo);
+
+ nsTArray<unsigned char*> headers;
+ nsTArray<size_t> headerLens;
+ if (!XiphExtradataToHeaders(headers, headerLens,
+ mInfo.mCodecSpecificConfig->Elements(),
+ mInfo.mCodecSpecificConfig->Length())) {
+ return InitPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Could not get theora header.")),
+ __func__);
+ }
+ for (size_t i = 0; i < headers.Length(); i++) {
+ if (NS_FAILED(DoDecodeHeader(headers[i], headerLens[i]))) {
+ return InitPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Could not decode theora header.")),
+ __func__);
+ }
+ }
+ if (mPacketCount != 3) {
+ return InitPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Packet count is wrong.")),
+ __func__);
+ }
+
+ mTheoraDecoderContext = th_decode_alloc(&mTheoraInfo, mTheoraSetupInfo);
+ if (mTheoraDecoderContext) {
+ return InitPromise::CreateAndResolve(TrackInfo::kVideoTrack, __func__);
+ } else {
+ return InitPromise::CreateAndReject(
+ MediaResult(NS_ERROR_OUT_OF_MEMORY,
+ RESULT_DETAIL("Could not allocate theora decoder.")),
+ __func__);
+ }
+}
+
+RefPtr<MediaDataDecoder::FlushPromise> TheoraDecoder::Flush() {
+ return InvokeAsync(mTaskQueue, __func__, []() {
+ return FlushPromise::CreateAndResolve(true, __func__);
+ });
+}
+
+nsresult TheoraDecoder::DoDecodeHeader(const unsigned char* aData,
+ size_t aLength) {
+ bool bos = mPacketCount == 0;
+ ogg_packet pkt =
+ InitTheoraPacket(aData, aLength, bos, false, 0, mPacketCount++);
+
+ int r = th_decode_headerin(&mTheoraInfo, &mTheoraComment, &mTheoraSetupInfo,
+ &pkt);
+ return r > 0 ? NS_OK : NS_ERROR_FAILURE;
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> TheoraDecoder::ProcessDecode(
+ MediaRawData* aSample) {
+ MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
+
+ MediaInfoFlag flag = MediaInfoFlag::None;
+ flag |= (aSample->mKeyframe ? MediaInfoFlag::KeyFrame
+ : MediaInfoFlag::NonKeyFrame);
+ flag |= MediaInfoFlag::SoftwareDecoding;
+ flag |= MediaInfoFlag::VIDEO_THEORA;
+ Maybe<PerformanceRecorder<DecodeStage>> rec =
+ mTrackingId.map([&](const auto& aId) {
+ return PerformanceRecorder<DecodeStage>("TheoraDecoder"_ns, aId, flag);
+ });
+
+ const unsigned char* aData = aSample->Data();
+ size_t aLength = aSample->Size();
+
+ bool bos = mPacketCount == 0;
+ ogg_packet pkt =
+ InitTheoraPacket(aData, aLength, bos, false,
+ aSample->mTimecode.ToMicroseconds(), mPacketCount++);
+
+ int ret = th_decode_packetin(mTheoraDecoderContext, &pkt, nullptr);
+ if (ret == 0 || ret == TH_DUPFRAME) {
+ th_ycbcr_buffer ycbcr;
+ th_decode_ycbcr_out(mTheoraDecoderContext, ycbcr);
+
+ int hdec = !(mTheoraInfo.pixel_fmt & 1);
+ int vdec = !(mTheoraInfo.pixel_fmt & 2);
+
+ VideoData::YCbCrBuffer b;
+ b.mPlanes[0].mData = ycbcr[0].data;
+ b.mPlanes[0].mStride = ycbcr[0].stride;
+ b.mPlanes[0].mHeight = mTheoraInfo.frame_height;
+ b.mPlanes[0].mWidth = mTheoraInfo.frame_width;
+ b.mPlanes[0].mSkip = 0;
+
+ b.mPlanes[1].mData = ycbcr[1].data;
+ b.mPlanes[1].mStride = ycbcr[1].stride;
+ b.mPlanes[1].mHeight = mTheoraInfo.frame_height >> vdec;
+ b.mPlanes[1].mWidth = mTheoraInfo.frame_width >> hdec;
+ b.mPlanes[1].mSkip = 0;
+
+ b.mPlanes[2].mData = ycbcr[2].data;
+ b.mPlanes[2].mStride = ycbcr[2].stride;
+ b.mPlanes[2].mHeight = mTheoraInfo.frame_height >> vdec;
+ b.mPlanes[2].mWidth = mTheoraInfo.frame_width >> hdec;
+ b.mPlanes[2].mSkip = 0;
+
+ if (vdec) {
+ b.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT;
+ } else if (hdec) {
+ b.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH;
+ }
+
+ b.mYUVColorSpace =
+ DefaultColorSpace({mTheoraInfo.frame_width, mTheoraInfo.frame_height});
+
+ IntRect pictureArea(mTheoraInfo.pic_x, mTheoraInfo.pic_y,
+ mTheoraInfo.pic_width, mTheoraInfo.pic_height);
+
+ VideoInfo info;
+ info.mDisplay = mInfo.mDisplay;
+ RefPtr<VideoData> v = VideoData::CreateAndCopyData(
+ info, mImageContainer, aSample->mOffset, aSample->mTime,
+ aSample->mDuration, b, aSample->mKeyframe, aSample->mTimecode,
+ mInfo.ScaledImageRect(mTheoraInfo.frame_width,
+ mTheoraInfo.frame_height),
+ mImageAllocator);
+ if (!v) {
+ LOG("Image allocation error source %ux%u display %ux%u picture %ux%u",
+ mTheoraInfo.frame_width, mTheoraInfo.frame_height,
+ mInfo.mDisplay.width, mInfo.mDisplay.height, mInfo.mImage.width,
+ mInfo.mImage.height);
+ return DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_OUT_OF_MEMORY,
+ RESULT_DETAIL("Insufficient memory")),
+ __func__);
+ }
+
+ rec.apply([&](auto& aRec) {
+ aRec.Record([&](DecodeStage& aStage) {
+ aStage.SetResolution(static_cast<int>(mTheoraInfo.frame_width),
+ static_cast<int>(mTheoraInfo.frame_height));
+ auto format = [&]() -> Maybe<DecodeStage::ImageFormat> {
+ switch (mTheoraInfo.pixel_fmt) {
+ case TH_PF_420:
+ return Some(DecodeStage::YUV420P);
+ case TH_PF_422:
+ return Some(DecodeStage::YUV422P);
+ case TH_PF_444:
+ return Some(DecodeStage::YUV444P);
+ default:
+ return Nothing();
+ }
+ }();
+ format.apply([&](auto& aFmt) { aStage.SetImageFormat(aFmt); });
+ aStage.SetYUVColorSpace(b.mYUVColorSpace);
+ aStage.SetColorRange(b.mColorRange);
+ aStage.SetColorDepth(b.mColorDepth);
+ });
+ });
+
+ return DecodePromise::CreateAndResolve(DecodedData{v}, __func__);
+ }
+ LOG("Theora Decode error: %d", ret);
+ return DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ RESULT_DETAIL("Theora decode error:%d", ret)),
+ __func__);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> TheoraDecoder::Decode(
+ MediaRawData* aSample) {
+ return InvokeAsync<MediaRawData*>(mTaskQueue, this, __func__,
+ &TheoraDecoder::ProcessDecode, aSample);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> TheoraDecoder::Drain() {
+ return InvokeAsync(mTaskQueue, __func__, [] {
+ return DecodePromise::CreateAndResolve(DecodedData(), __func__);
+ });
+}
+
+/* static */
+bool TheoraDecoder::IsTheora(const nsACString& aMimeType) {
+ return aMimeType.EqualsLiteral("video/theora");
+}
+
+} // namespace mozilla
+#undef LOG
diff --git a/dom/media/platforms/agnostic/TheoraDecoder.h b/dom/media/platforms/agnostic/TheoraDecoder.h
new file mode 100644
index 0000000000..23e994b667
--- /dev/null
+++ b/dom/media/platforms/agnostic/TheoraDecoder.h
@@ -0,0 +1,64 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#if !defined(TheoraDecoder_h_)
+# define TheoraDecoder_h_
+
+# include <stdint.h>
+
+# include "PlatformDecoderModule.h"
+# include "ogg/ogg.h"
+# include "theora/theoradec.h"
+
+namespace mozilla {
+
+DDLoggedTypeDeclNameAndBase(TheoraDecoder, MediaDataDecoder);
+
+class TheoraDecoder final : public MediaDataDecoder,
+ public DecoderDoctorLifeLogger<TheoraDecoder> {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(TheoraDecoder, final);
+
+ explicit TheoraDecoder(const CreateDecoderParams& aParams);
+
+ RefPtr<InitPromise> Init() override;
+ RefPtr<DecodePromise> Decode(MediaRawData* aSample) override;
+ RefPtr<DecodePromise> Drain() override;
+ RefPtr<FlushPromise> Flush() override;
+ RefPtr<ShutdownPromise> Shutdown() override;
+
+ // Return true if mimetype is a Theora codec
+ static bool IsTheora(const nsACString& aMimeType);
+
+ nsCString GetDescriptionName() const override {
+ return "theora video decoder"_ns;
+ }
+
+ nsCString GetCodecName() const override { return "theora"_ns; }
+
+ private:
+ ~TheoraDecoder();
+ nsresult DoDecodeHeader(const unsigned char* aData, size_t aLength);
+
+ RefPtr<DecodePromise> ProcessDecode(MediaRawData* aSample);
+
+ const RefPtr<layers::KnowsCompositor> mImageAllocator;
+ const RefPtr<layers::ImageContainer> mImageContainer;
+ const RefPtr<TaskQueue> mTaskQueue;
+
+ // Theora header & decoder state
+ th_info mTheoraInfo;
+ th_comment mTheoraComment;
+ th_setup_info* mTheoraSetupInfo;
+ th_dec_ctx* mTheoraDecoderContext;
+ int mPacketCount;
+
+ const VideoInfo mInfo;
+ const Maybe<TrackingId> mTrackingId;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/platforms/agnostic/VPXDecoder.cpp b/dom/media/platforms/agnostic/VPXDecoder.cpp
new file mode 100644
index 0000000000..0c4d8234f5
--- /dev/null
+++ b/dom/media/platforms/agnostic/VPXDecoder.cpp
@@ -0,0 +1,676 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "VPXDecoder.h"
+
+#include <algorithm>
+
+#include "BitReader.h"
+#include "BitWriter.h"
+#include "ImageContainer.h"
+#include "TimeUnits.h"
+#include "gfx2DGlue.h"
+#include "gfxUtils.h"
+#include "mozilla/PodOperations.h"
+#include "mozilla/SyncRunnable.h"
+#include "mozilla/TaskQueue.h"
+#include "mozilla/Unused.h"
+#include "nsError.h"
+#include "PerformanceRecorder.h"
+#include "prsystem.h"
+#include "VideoUtils.h"
+#include "vpx/vpx_image.h"
+
+#undef LOG
+#define LOG(arg, ...) \
+ DDMOZ_LOG(sPDMLog, mozilla::LogLevel::Debug, "::%s: " arg, __func__, \
+ ##__VA_ARGS__)
+
+namespace mozilla {
+
+using namespace gfx;
+using namespace layers;
+
+static VPXDecoder::Codec MimeTypeToCodec(const nsACString& aMimeType) {
+ if (aMimeType.EqualsLiteral("video/vp8")) {
+ return VPXDecoder::Codec::VP8;
+ } else if (aMimeType.EqualsLiteral("video/vp9")) {
+ return VPXDecoder::Codec::VP9;
+ }
+ return VPXDecoder::Codec::Unknown;
+}
+
+static nsresult InitContext(vpx_codec_ctx_t* aCtx, const VideoInfo& aInfo,
+ const VPXDecoder::Codec aCodec, bool aLowLatency) {
+ int decode_threads = 2;
+
+ vpx_codec_iface_t* dx = nullptr;
+ if (aCodec == VPXDecoder::Codec::VP8) {
+ dx = vpx_codec_vp8_dx();
+ } else if (aCodec == VPXDecoder::Codec::VP9) {
+ dx = vpx_codec_vp9_dx();
+ if (aInfo.mDisplay.width >= 2048) {
+ decode_threads = 8;
+ } else if (aInfo.mDisplay.width >= 1024) {
+ decode_threads = 4;
+ }
+ }
+ decode_threads = std::min(decode_threads, PR_GetNumberOfProcessors());
+
+ vpx_codec_dec_cfg_t config;
+ config.threads = aLowLatency ? 1 : decode_threads;
+ config.w = config.h = 0; // set after decode
+
+ if (!dx || vpx_codec_dec_init(aCtx, dx, &config, 0)) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+VPXDecoder::VPXDecoder(const CreateDecoderParams& aParams)
+ : mImageContainer(aParams.mImageContainer),
+ mImageAllocator(aParams.mKnowsCompositor),
+ mTaskQueue(TaskQueue::Create(
+ GetMediaThreadPool(MediaThreadType::PLATFORM_DECODER), "VPXDecoder")),
+ mInfo(aParams.VideoConfig()),
+ mCodec(MimeTypeToCodec(aParams.VideoConfig().mMimeType)),
+ mLowLatency(
+ aParams.mOptions.contains(CreateDecoderParams::Option::LowLatency)),
+ mTrackingId(aParams.mTrackingId) {
+ MOZ_COUNT_CTOR(VPXDecoder);
+ PodZero(&mVPX);
+ PodZero(&mVPXAlpha);
+}
+
+VPXDecoder::~VPXDecoder() { MOZ_COUNT_DTOR(VPXDecoder); }
+
+RefPtr<ShutdownPromise> VPXDecoder::Shutdown() {
+ RefPtr<VPXDecoder> self = this;
+ return InvokeAsync(mTaskQueue, __func__, [self]() {
+ vpx_codec_destroy(&self->mVPX);
+ vpx_codec_destroy(&self->mVPXAlpha);
+ return self->mTaskQueue->BeginShutdown();
+ });
+}
+
+RefPtr<MediaDataDecoder::InitPromise> VPXDecoder::Init() {
+ if (NS_FAILED(InitContext(&mVPX, mInfo, mCodec, mLowLatency))) {
+ return VPXDecoder::InitPromise::CreateAndReject(
+ NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
+ }
+ if (mInfo.HasAlpha()) {
+ if (NS_FAILED(InitContext(&mVPXAlpha, mInfo, mCodec, mLowLatency))) {
+ return VPXDecoder::InitPromise::CreateAndReject(
+ NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
+ }
+ }
+ return VPXDecoder::InitPromise::CreateAndResolve(TrackInfo::kVideoTrack,
+ __func__);
+}
+
+RefPtr<MediaDataDecoder::FlushPromise> VPXDecoder::Flush() {
+ return InvokeAsync(mTaskQueue, __func__, []() {
+ return FlushPromise::CreateAndResolve(true, __func__);
+ });
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> VPXDecoder::ProcessDecode(
+ MediaRawData* aSample) {
+ MOZ_ASSERT(mTaskQueue->IsOnCurrentThread());
+
+ MediaInfoFlag flag = MediaInfoFlag::None;
+ flag |= (aSample->mKeyframe ? MediaInfoFlag::KeyFrame
+ : MediaInfoFlag::NonKeyFrame);
+ flag |= MediaInfoFlag::SoftwareDecoding;
+ switch (mCodec) {
+ case Codec::VP8:
+ flag |= MediaInfoFlag::VIDEO_VP8;
+ break;
+ case Codec::VP9:
+ flag |= MediaInfoFlag::VIDEO_VP9;
+ break;
+ default:
+ break;
+ }
+ flag |= MediaInfoFlag::VIDEO_THEORA;
+ auto rec = mTrackingId.map([&](const auto& aId) {
+ return PerformanceRecorder<DecodeStage>("VPXDecoder"_ns, aId, flag);
+ });
+
+ if (vpx_codec_err_t r = vpx_codec_decode(&mVPX, aSample->Data(),
+ aSample->Size(), nullptr, 0)) {
+ LOG("VPX Decode error: %s", vpx_codec_err_to_string(r));
+ return DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ RESULT_DETAIL("VPX error: %s", vpx_codec_err_to_string(r))),
+ __func__);
+ }
+
+ vpx_codec_iter_t iter = nullptr;
+ vpx_image_t* img;
+ vpx_image_t* img_alpha = nullptr;
+ bool alpha_decoded = false;
+ DecodedData results;
+
+ while ((img = vpx_codec_get_frame(&mVPX, &iter))) {
+ NS_ASSERTION(img->fmt == VPX_IMG_FMT_I420 || img->fmt == VPX_IMG_FMT_I444,
+ "WebM image format not I420 or I444");
+ NS_ASSERTION(!alpha_decoded,
+ "Multiple frames per packet that contains alpha");
+
+ if (aSample->AlphaSize() > 0) {
+ if (!alpha_decoded) {
+ MediaResult rv = DecodeAlpha(&img_alpha, aSample);
+ if (NS_FAILED(rv)) {
+ return DecodePromise::CreateAndReject(rv, __func__);
+ }
+ alpha_decoded = true;
+ }
+ }
+ // Chroma shifts are rounded down as per the decoding examples in the SDK
+ VideoData::YCbCrBuffer b;
+ b.mPlanes[0].mData = img->planes[0];
+ b.mPlanes[0].mStride = img->stride[0];
+ b.mPlanes[0].mHeight = img->d_h;
+ b.mPlanes[0].mWidth = img->d_w;
+ b.mPlanes[0].mSkip = 0;
+
+ b.mPlanes[1].mData = img->planes[1];
+ b.mPlanes[1].mStride = img->stride[1];
+ b.mPlanes[1].mSkip = 0;
+
+ b.mPlanes[2].mData = img->planes[2];
+ b.mPlanes[2].mStride = img->stride[2];
+ b.mPlanes[2].mSkip = 0;
+
+ if (img->fmt == VPX_IMG_FMT_I420) {
+ b.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT;
+
+ b.mPlanes[1].mHeight = (img->d_h + 1) >> img->y_chroma_shift;
+ b.mPlanes[1].mWidth = (img->d_w + 1) >> img->x_chroma_shift;
+
+ b.mPlanes[2].mHeight = (img->d_h + 1) >> img->y_chroma_shift;
+ b.mPlanes[2].mWidth = (img->d_w + 1) >> img->x_chroma_shift;
+ } else if (img->fmt == VPX_IMG_FMT_I444) {
+ b.mPlanes[1].mHeight = img->d_h;
+ b.mPlanes[1].mWidth = img->d_w;
+
+ b.mPlanes[2].mHeight = img->d_h;
+ b.mPlanes[2].mWidth = img->d_w;
+ } else {
+ LOG("VPX Unknown image format");
+ return DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ RESULT_DETAIL("VPX Unknown image format")),
+ __func__);
+ }
+ b.mYUVColorSpace = [&]() {
+ switch (img->cs) {
+ case VPX_CS_BT_601:
+ case VPX_CS_SMPTE_170:
+ case VPX_CS_SMPTE_240:
+ return gfx::YUVColorSpace::BT601;
+ case VPX_CS_BT_709:
+ return gfx::YUVColorSpace::BT709;
+ case VPX_CS_BT_2020:
+ return gfx::YUVColorSpace::BT2020;
+ default:
+ return DefaultColorSpace({img->d_w, img->d_h});
+ }
+ }();
+ b.mColorRange = img->range == VPX_CR_FULL_RANGE ? gfx::ColorRange::FULL
+ : gfx::ColorRange::LIMITED;
+
+ RefPtr<VideoData> v;
+ if (!img_alpha) {
+ v = VideoData::CreateAndCopyData(
+ mInfo, mImageContainer, aSample->mOffset, aSample->mTime,
+ aSample->mDuration, b, aSample->mKeyframe, aSample->mTimecode,
+ mInfo.ScaledImageRect(img->d_w, img->d_h), mImageAllocator);
+ } else {
+ VideoData::YCbCrBuffer::Plane alpha_plane;
+ alpha_plane.mData = img_alpha->planes[0];
+ alpha_plane.mStride = img_alpha->stride[0];
+ alpha_plane.mHeight = img_alpha->d_h;
+ alpha_plane.mWidth = img_alpha->d_w;
+ alpha_plane.mSkip = 0;
+ v = VideoData::CreateAndCopyData(
+ mInfo, mImageContainer, aSample->mOffset, aSample->mTime,
+ aSample->mDuration, b, alpha_plane, aSample->mKeyframe,
+ aSample->mTimecode, mInfo.ScaledImageRect(img->d_w, img->d_h));
+ }
+
+ if (!v) {
+ LOG("Image allocation error source %ux%u display %ux%u picture %ux%u",
+ img->d_w, img->d_h, mInfo.mDisplay.width, mInfo.mDisplay.height,
+ mInfo.mImage.width, mInfo.mImage.height);
+ return DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__), __func__);
+ }
+
+ rec.apply([&](auto& aRec) {
+ return aRec.Record([&](DecodeStage& aStage) {
+ aStage.SetResolution(static_cast<int>(img->d_w),
+ static_cast<int>(img->d_h));
+ auto format = [&]() -> Maybe<DecodeStage::ImageFormat> {
+ switch (img->fmt) {
+ case VPX_IMG_FMT_I420:
+ return Some(DecodeStage::YUV420P);
+ case VPX_IMG_FMT_I444:
+ return Some(DecodeStage::YUV444P);
+ default:
+ return Nothing();
+ }
+ }();
+ format.apply([&](auto& aFmt) { aStage.SetImageFormat(aFmt); });
+ aStage.SetYUVColorSpace(b.mYUVColorSpace);
+ aStage.SetColorRange(b.mColorRange);
+ aStage.SetColorDepth(b.mColorDepth);
+ });
+ });
+
+ results.AppendElement(std::move(v));
+ }
+ return DecodePromise::CreateAndResolve(std::move(results), __func__);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> VPXDecoder::Decode(
+ MediaRawData* aSample) {
+ return InvokeAsync<MediaRawData*>(mTaskQueue, this, __func__,
+ &VPXDecoder::ProcessDecode, aSample);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> VPXDecoder::Drain() {
+ return InvokeAsync(mTaskQueue, __func__, [] {
+ return DecodePromise::CreateAndResolve(DecodedData(), __func__);
+ });
+}
+
+MediaResult VPXDecoder::DecodeAlpha(vpx_image_t** aImgAlpha,
+ const MediaRawData* aSample) {
+ vpx_codec_err_t r = vpx_codec_decode(&mVPXAlpha, aSample->AlphaData(),
+ aSample->AlphaSize(), nullptr, 0);
+ if (r) {
+ LOG("VPX decode alpha error: %s", vpx_codec_err_to_string(r));
+ return MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ RESULT_DETAIL("VPX decode alpha error: %s",
+ vpx_codec_err_to_string(r)));
+ }
+
+ vpx_codec_iter_t iter = nullptr;
+
+ *aImgAlpha = vpx_codec_get_frame(&mVPXAlpha, &iter);
+ NS_ASSERTION((*aImgAlpha)->fmt == VPX_IMG_FMT_I420 ||
+ (*aImgAlpha)->fmt == VPX_IMG_FMT_I444,
+ "WebM image format not I420 or I444");
+
+ return NS_OK;
+}
+
+nsCString VPXDecoder::GetCodecName() const {
+ switch (mCodec) {
+ case Codec::VP8:
+ return "vp8"_ns;
+ case Codec::VP9:
+ return "vp9"_ns;
+ default:
+ return "unknown"_ns;
+ }
+}
+
+/* static */
+bool VPXDecoder::IsVPX(const nsACString& aMimeType, uint8_t aCodecMask) {
+ return ((aCodecMask & VPXDecoder::VP8) &&
+ aMimeType.EqualsLiteral("video/vp8")) ||
+ ((aCodecMask & VPXDecoder::VP9) &&
+ aMimeType.EqualsLiteral("video/vp9"));
+}
+
+/* static */
+bool VPXDecoder::IsVP8(const nsACString& aMimeType) {
+ return IsVPX(aMimeType, VPXDecoder::VP8);
+}
+
+/* static */
+bool VPXDecoder::IsVP9(const nsACString& aMimeType) {
+ return IsVPX(aMimeType, VPXDecoder::VP9);
+}
+
+/* static */
+bool VPXDecoder::IsKeyframe(Span<const uint8_t> aBuffer, Codec aCodec) {
+ VPXStreamInfo info;
+ return GetStreamInfo(aBuffer, info, aCodec) && info.mKeyFrame;
+}
+
+/* static */
+gfx::IntSize VPXDecoder::GetFrameSize(Span<const uint8_t> aBuffer,
+ Codec aCodec) {
+ VPXStreamInfo info;
+ if (!GetStreamInfo(aBuffer, info, aCodec)) {
+ return gfx::IntSize();
+ }
+ return info.mImage;
+}
+
+/* static */
+gfx::IntSize VPXDecoder::GetDisplaySize(Span<const uint8_t> aBuffer,
+ Codec aCodec) {
+ VPXStreamInfo info;
+ if (!GetStreamInfo(aBuffer, info, aCodec)) {
+ return gfx::IntSize();
+ }
+ return info.mDisplay;
+}
+
+/* static */
+int VPXDecoder::GetVP9Profile(Span<const uint8_t> aBuffer) {
+ VPXStreamInfo info;
+ if (!GetStreamInfo(aBuffer, info, Codec::VP9)) {
+ return -1;
+ }
+ return info.mProfile;
+}
+
+/* static */
+bool VPXDecoder::GetStreamInfo(Span<const uint8_t> aBuffer,
+ VPXDecoder::VPXStreamInfo& aInfo, Codec aCodec) {
+ if (aBuffer.IsEmpty()) {
+ // Can't be good.
+ return false;
+ }
+
+ aInfo = VPXStreamInfo();
+
+ if (aCodec == Codec::VP8) {
+ aInfo.mKeyFrame = (aBuffer[0] & 1) ==
+ 0; // frame type (0 for key frames, 1 for interframes)
+ if (!aInfo.mKeyFrame) {
+ // We can't retrieve the required information from interframes.
+ return true;
+ }
+ if (aBuffer.Length() < 10) {
+ return false;
+ }
+ uint8_t version = (aBuffer[0] >> 1) & 0x7;
+ if (version > 3) {
+ return false;
+ }
+ uint8_t start_code_byte_0 = aBuffer[3];
+ uint8_t start_code_byte_1 = aBuffer[4];
+ uint8_t start_code_byte_2 = aBuffer[5];
+ if (start_code_byte_0 != 0x9d || start_code_byte_1 != 0x01 ||
+ start_code_byte_2 != 0x2a) {
+ return false;
+ }
+ uint16_t width = (aBuffer[6] | aBuffer[7] << 8) & 0x3fff;
+ uint16_t height = (aBuffer[8] | aBuffer[9] << 8) & 0x3fff;
+
+ // aspect ratio isn't found in the VP8 frame header.
+ aInfo.mImage = gfx::IntSize(width, height);
+ aInfo.mDisplayAndImageDifferent = false;
+ aInfo.mDisplay = aInfo.mImage;
+ return true;
+ }
+
+ BitReader br(aBuffer.Elements(), aBuffer.Length() * 8);
+ uint32_t frameMarker = br.ReadBits(2); // frame_marker
+ if (frameMarker != 2) {
+ // That's not a valid vp9 header.
+ return false;
+ }
+ uint32_t profile = br.ReadBits(1); // profile_low_bit
+ profile |= br.ReadBits(1) << 1; // profile_high_bit
+ if (profile == 3) {
+ profile += br.ReadBits(1); // reserved_zero
+ if (profile > 3) {
+ // reserved_zero wasn't zero.
+ return false;
+ }
+ }
+
+ aInfo.mProfile = profile;
+
+ bool show_existing_frame = br.ReadBits(1);
+ if (show_existing_frame) {
+ if (profile == 3 && aBuffer.Length() < 2) {
+ return false;
+ }
+ Unused << br.ReadBits(3); // frame_to_show_map_idx
+ return true;
+ }
+
+ if (aBuffer.Length() < 10) {
+ // Header too small;
+ return false;
+ }
+
+ aInfo.mKeyFrame = !br.ReadBits(1);
+ bool show_frame = br.ReadBits(1);
+ bool error_resilient_mode = br.ReadBits(1);
+
+ auto frame_sync_code = [&]() -> bool {
+ uint8_t frame_sync_byte_1 = br.ReadBits(8);
+ uint8_t frame_sync_byte_2 = br.ReadBits(8);
+ uint8_t frame_sync_byte_3 = br.ReadBits(8);
+ return frame_sync_byte_1 == 0x49 && frame_sync_byte_2 == 0x83 &&
+ frame_sync_byte_3 == 0x42;
+ };
+
+ auto color_config = [&]() -> bool {
+ aInfo.mBitDepth = 8;
+ if (profile >= 2) {
+ bool ten_or_twelve_bit = br.ReadBits(1);
+ aInfo.mBitDepth = ten_or_twelve_bit ? 12 : 10;
+ }
+ aInfo.mColorSpace = br.ReadBits(3);
+ if (aInfo.mColorSpace != 7 /* CS_RGB */) {
+ aInfo.mFullRange = br.ReadBits(1);
+ if (profile == 1 || profile == 3) {
+ aInfo.mSubSampling_x = br.ReadBits(1);
+ aInfo.mSubSampling_y = br.ReadBits(1);
+ if (br.ReadBits(1)) { // reserved_zero
+ return false;
+ };
+ } else {
+ aInfo.mSubSampling_x = true;
+ aInfo.mSubSampling_y = true;
+ }
+ } else {
+ aInfo.mFullRange = true;
+ if (profile == 1 || profile == 3) {
+ aInfo.mSubSampling_x = false;
+ aInfo.mSubSampling_y = false;
+ if (br.ReadBits(1)) { // reserved_zero
+ return false;
+ };
+ } else {
+ // sRGB color space is only available with VP9 profile 1.
+ return false;
+ }
+ }
+ return true;
+ };
+
+ auto frame_size = [&]() {
+ int32_t width = static_cast<int32_t>(br.ReadBits(16)) + 1;
+ int32_t height = static_cast<int32_t>(br.ReadBits(16)) + 1;
+ aInfo.mImage = gfx::IntSize(width, height);
+ };
+
+ auto render_size = [&]() {
+ // render_and_frame_size_different
+ aInfo.mDisplayAndImageDifferent = br.ReadBits(1);
+ if (aInfo.mDisplayAndImageDifferent) {
+ int32_t width = static_cast<int32_t>(br.ReadBits(16)) + 1;
+ int32_t height = static_cast<int32_t>(br.ReadBits(16)) + 1;
+ aInfo.mDisplay = gfx::IntSize(width, height);
+ } else {
+ aInfo.mDisplay = aInfo.mImage;
+ }
+ };
+
+ if (aInfo.mKeyFrame) {
+ if (!frame_sync_code()) {
+ return false;
+ }
+ if (!color_config()) {
+ return false;
+ }
+ frame_size();
+ render_size();
+ } else {
+ bool intra_only = show_frame ? false : br.ReadBit();
+ if (!error_resilient_mode) {
+ Unused << br.ReadBits(2); // reset_frame_context
+ }
+ if (intra_only) {
+ if (!frame_sync_code()) {
+ return false;
+ }
+ if (profile > 0) {
+ if (!color_config()) {
+ return false;
+ }
+ } else {
+ aInfo.mColorSpace = 1; // CS_BT_601
+ aInfo.mSubSampling_x = true;
+ aInfo.mSubSampling_y = true;
+ aInfo.mBitDepth = 8;
+ }
+ Unused << br.ReadBits(8); // refresh_frame_flags
+ frame_size();
+ render_size();
+ }
+ }
+ return true;
+}
+
+// Ref: "VP Codec ISO Media File Format Binding, v1.0, 2017-03-31"
+// <https://www.webmproject.org/vp9/mp4/>
+//
+// class VPCodecConfigurationBox extends FullBox('vpcC', version = 1, 0)
+// {
+// VPCodecConfigurationRecord() vpcConfig;
+// }
+//
+// aligned (8) class VPCodecConfigurationRecord {
+// unsigned int (8) profile;
+// unsigned int (8) level;
+// unsigned int (4) bitDepth;
+// unsigned int (3) chromaSubsampling;
+// unsigned int (1) videoFullRangeFlag;
+// unsigned int (8) colourPrimaries;
+// unsigned int (8) transferCharacteristics;
+// unsigned int (8) matrixCoefficients;
+// unsigned int (16) codecIntializationDataSize;
+// unsigned int (8)[] codecIntializationData;
+// }
+
+/* static */
+void VPXDecoder::GetVPCCBox(MediaByteBuffer* aDestBox,
+ const VPXStreamInfo& aInfo) {
+ BitWriter writer(aDestBox);
+
+ int chroma = [&]() {
+ if (aInfo.mSubSampling_x && aInfo.mSubSampling_y) {
+ return 1; // 420 Colocated;
+ }
+ if (aInfo.mSubSampling_x && !aInfo.mSubSampling_y) {
+ return 2; // 422
+ }
+ if (!aInfo.mSubSampling_x && !aInfo.mSubSampling_y) {
+ return 3; // 444
+ }
+ // This indicates 4:4:0 subsampling, which is not expressable in the
+ // 'vpcC' box. Default to 4:2:0.
+ return 1;
+ }();
+
+ writer.WriteU8(1); // version
+ writer.WriteBits(0, 24); // flags
+
+ writer.WriteU8(aInfo.mProfile); // profile
+ writer.WriteU8(10); // level set it to 1.0
+
+ writer.WriteBits(aInfo.mBitDepth, 4); // bitdepth
+ writer.WriteBits(chroma, 3); // chroma
+ writer.WriteBit(aInfo.mFullRange); // full/restricted range
+
+ // See VPXDecoder::VPXStreamInfo enums
+ writer.WriteU8(aInfo.mColorPrimaries); // color primaries
+ writer.WriteU8(aInfo.mTransferFunction); // transfer characteristics
+ writer.WriteU8(2); // matrix coefficients: unspecified
+
+ writer.WriteBits(0,
+ 16); // codecIntializationDataSize (must be 0 for VP8/VP9)
+}
+
+/* static */
+bool VPXDecoder::SetVideoInfo(VideoInfo* aDestInfo, const nsAString& aCodec) {
+ VPXDecoder::VPXStreamInfo info;
+ uint8_t level = 0;
+ uint8_t chroma = 1;
+ VideoColorSpace colorSpace;
+ if (!ExtractVPXCodecDetails(aCodec, info.mProfile, level, info.mBitDepth,
+ chroma, colorSpace)) {
+ return false;
+ }
+
+ aDestInfo->mColorPrimaries =
+ gfxUtils::CicpToColorPrimaries(colorSpace.mPrimaries, sPDMLog);
+ aDestInfo->mTransferFunction =
+ gfxUtils::CicpToTransferFunction(colorSpace.mTransfer);
+ aDestInfo->mColorDepth = gfx::ColorDepthForBitDepth(info.mBitDepth);
+ VPXDecoder::SetChroma(info, chroma);
+ info.mFullRange = colorSpace.mRange == ColorRange::FULL;
+ RefPtr<MediaByteBuffer> extraData = new MediaByteBuffer();
+ VPXDecoder::GetVPCCBox(extraData, info);
+ aDestInfo->mExtraData = extraData;
+ return true;
+}
+
+/* static */
+void VPXDecoder::SetChroma(VPXStreamInfo& aDestInfo, uint8_t chroma) {
+ switch (chroma) {
+ case 0:
+ case 1:
+ aDestInfo.mSubSampling_x = true;
+ aDestInfo.mSubSampling_y = true;
+ break;
+ case 2:
+ aDestInfo.mSubSampling_x = true;
+ aDestInfo.mSubSampling_y = false;
+ break;
+ case 3:
+ aDestInfo.mSubSampling_x = false;
+ aDestInfo.mSubSampling_y = false;
+ break;
+ }
+}
+
+/* static */
+void VPXDecoder::ReadVPCCBox(VPXStreamInfo& aDestInfo, MediaByteBuffer* aBox) {
+ BitReader reader(aBox);
+
+ reader.ReadBits(8); // version
+ reader.ReadBits(24); // flags
+ aDestInfo.mProfile = reader.ReadBits(8);
+ reader.ReadBits(8); // level
+
+ aDestInfo.mBitDepth = reader.ReadBits(4);
+ SetChroma(aDestInfo, reader.ReadBits(3));
+ aDestInfo.mFullRange = reader.ReadBit();
+
+ aDestInfo.mColorPrimaries = reader.ReadBits(8); // color primaries
+ aDestInfo.mTransferFunction = reader.ReadBits(8); // transfer characteristics
+ reader.ReadBits(8); // matrix coefficients
+
+ MOZ_ASSERT(reader.ReadBits(16) ==
+ 0); // codecInitializationDataSize (must be 0 for VP8/VP9)
+}
+
+} // namespace mozilla
+#undef LOG
diff --git a/dom/media/platforms/agnostic/VPXDecoder.h b/dom/media/platforms/agnostic/VPXDecoder.h
new file mode 100644
index 0000000000..e5fe80128f
--- /dev/null
+++ b/dom/media/platforms/agnostic/VPXDecoder.h
@@ -0,0 +1,208 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#if !defined(VPXDecoder_h_)
+# define VPXDecoder_h_
+
+# include <stdint.h>
+
+# include "PlatformDecoderModule.h"
+# include "mozilla/Span.h"
+# include "mozilla/gfx/Types.h"
+# include "vpx/vp8dx.h"
+# include "vpx/vpx_codec.h"
+# include "vpx/vpx_decoder.h"
+
+namespace mozilla {
+
+DDLoggedTypeDeclNameAndBase(VPXDecoder, MediaDataDecoder);
+
+class VPXDecoder final : public MediaDataDecoder,
+ public DecoderDoctorLifeLogger<VPXDecoder> {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(VPXDecoder, final);
+
+ explicit VPXDecoder(const CreateDecoderParams& aParams);
+
+ RefPtr<InitPromise> Init() override;
+ RefPtr<DecodePromise> Decode(MediaRawData* aSample) override;
+ RefPtr<DecodePromise> Drain() override;
+ RefPtr<FlushPromise> Flush() override;
+ RefPtr<ShutdownPromise> Shutdown() override;
+ nsCString GetDescriptionName() const override {
+ return "libvpx video decoder"_ns;
+ }
+ nsCString GetCodecName() const override;
+
+ enum Codec : uint8_t {
+ VP8 = 1 << 0,
+ VP9 = 1 << 1,
+ Unknown = 1 << 7,
+ };
+
+ // Return true if aMimeType is a one of the strings used by our demuxers to
+ // identify VPX of the specified type. Does not parse general content type
+ // strings, i.e. white space matters.
+ static bool IsVPX(const nsACString& aMimeType,
+ uint8_t aCodecMask = VP8 | VP9);
+ static bool IsVP8(const nsACString& aMimeType);
+ static bool IsVP9(const nsACString& aMimeType);
+
+ // Return true if a sample is a keyframe for the specified codec.
+ static bool IsKeyframe(Span<const uint8_t> aBuffer, Codec aCodec);
+
+ // Return the frame dimensions for a sample for the specified codec.
+ static gfx::IntSize GetFrameSize(Span<const uint8_t> aBuffer, Codec aCodec);
+ // Return the display dimensions for a sample for the specified codec.
+ static gfx::IntSize GetDisplaySize(Span<const uint8_t> aBuffer, Codec aCodec);
+
+ // Return the VP9 profile as per https://www.webmproject.org/vp9/profiles/
+ // Return negative value if error.
+ static int GetVP9Profile(Span<const uint8_t> aBuffer);
+
+ struct VPXStreamInfo {
+ gfx::IntSize mImage;
+ bool mDisplayAndImageDifferent = false;
+ gfx::IntSize mDisplay;
+ bool mKeyFrame = false;
+
+ uint8_t mProfile = 0;
+ uint8_t mBitDepth = 8;
+ /*
+ 0 CS_UNKNOWN Unknown (in this case the color space must be signaled outside
+ the VP9 bitstream).
+ 1 CS_BT_601 Rec. ITU-R BT.601-7
+ 2 CS_BT_709 Rec. ITU-R BT.709-6
+ 3 CS_SMPTE_170 SMPTE-170
+ 4 CS_SMPTE_240 SMPTE-240
+ 5 CS_BT_2020 Rec. ITU-R BT.2020-2
+ 6 CS_RESERVED Reserved
+ 7 CS_RGB sRGB (IEC 61966-2-1)
+ */
+ int mColorSpace = 1; // CS_BT_601
+
+ gfx::YUVColorSpace ColorSpace() const {
+ switch (mColorSpace) {
+ case 1:
+ case 3:
+ case 4:
+ return gfx::YUVColorSpace::BT601;
+ case 2:
+ return gfx::YUVColorSpace::BT709;
+ case 5:
+ return gfx::YUVColorSpace::BT2020;
+ default:
+ return gfx::YUVColorSpace::Default;
+ }
+ }
+
+ uint8_t mColorPrimaries = gfx::CICP::ColourPrimaries::CP_UNSPECIFIED;
+ gfx::ColorSpace2 ColorPrimaries() const {
+ switch (mColorPrimaries) {
+ case gfx::CICP::ColourPrimaries::CP_BT709:
+ return gfx::ColorSpace2::BT709;
+ case gfx::CICP::ColourPrimaries::CP_UNSPECIFIED:
+ return gfx::ColorSpace2::BT709;
+ case gfx::CICP::ColourPrimaries::CP_BT2020:
+ return gfx::ColorSpace2::BT2020;
+ default:
+ return gfx::ColorSpace2::BT709;
+ }
+ }
+
+ uint8_t mTransferFunction =
+ gfx::CICP::TransferCharacteristics::TC_UNSPECIFIED;
+ gfx::TransferFunction TransferFunction() const {
+ switch (mTransferFunction) {
+ case gfx::CICP::TransferCharacteristics::TC_BT709:
+ return gfx::TransferFunction::BT709;
+ case gfx::CICP::TransferCharacteristics::TC_SRGB:
+ return gfx::TransferFunction::SRGB;
+ case gfx::CICP::TransferCharacteristics::TC_SMPTE2084:
+ return gfx::TransferFunction::PQ;
+ case gfx::CICP::TransferCharacteristics::TC_HLG:
+ return gfx::TransferFunction::HLG;
+ default:
+ return gfx::TransferFunction::BT709;
+ }
+ }
+
+ /*
+ mFullRange == false then:
+ For BitDepth equals 8:
+ Y is between 16 and 235 inclusive.
+ U and V are between 16 and 240 inclusive.
+ For BitDepth equals 10:
+ Y is between 64 and 940 inclusive.
+ U and V are between 64 and 960 inclusive.
+ For BitDepth equals 12:
+ Y is between 256 and 3760.
+ U and V are between 256 and 3840 inclusive.
+ mFullRange == true then:
+ No restriction on Y, U, V values.
+ */
+ bool mFullRange = false;
+
+ gfx::ColorRange ColorRange() const {
+ return mFullRange ? gfx::ColorRange::FULL : gfx::ColorRange::LIMITED;
+ }
+
+ /*
+ Sub-sampling, used only for non sRGB colorspace.
+ subsampling_x subsampling_y Description
+ 0 0 YUV 4:4:4
+ 0 1 YUV 4:4:0
+ 1 0 YUV 4:2:2
+ 1 1 YUV 4:2:0
+ */
+ bool mSubSampling_x = true;
+ bool mSubSampling_y = true;
+
+ bool IsCompatible(const VPXStreamInfo& aOther) const {
+ return mImage == aOther.mImage && mProfile == aOther.mProfile &&
+ mBitDepth == aOther.mBitDepth &&
+ mSubSampling_x == aOther.mSubSampling_x &&
+ mSubSampling_y == aOther.mSubSampling_y &&
+ mColorSpace == aOther.mColorSpace &&
+ mFullRange == aOther.mFullRange;
+ }
+ };
+
+ static bool GetStreamInfo(Span<const uint8_t> aBuffer, VPXStreamInfo& aInfo,
+ Codec aCodec);
+
+ static void GetVPCCBox(MediaByteBuffer* aDestBox, const VPXStreamInfo& aInfo);
+ // Set extradata for a VP8/VP9 track, returning false if the codec was
+ // invalid.
+ static bool SetVideoInfo(VideoInfo* aDestInfo, const nsAString& aCodec);
+
+ static void SetChroma(VPXStreamInfo& aDestInfo, uint8_t chroma);
+ static void ReadVPCCBox(VPXStreamInfo& aDestInfo, MediaByteBuffer* aBox);
+
+ private:
+ ~VPXDecoder();
+ RefPtr<DecodePromise> ProcessDecode(MediaRawData* aSample);
+ MediaResult DecodeAlpha(vpx_image_t** aImgAlpha, const MediaRawData* aSample);
+
+ const RefPtr<layers::ImageContainer> mImageContainer;
+ RefPtr<layers::KnowsCompositor> mImageAllocator;
+ const RefPtr<TaskQueue> mTaskQueue;
+
+ // VPx decoder state
+ vpx_codec_ctx_t mVPX;
+
+ // VPx alpha decoder state
+ vpx_codec_ctx_t mVPXAlpha;
+
+ const VideoInfo mInfo;
+
+ const Codec mCodec;
+ const bool mLowLatency;
+ const Maybe<TrackingId> mTrackingId;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/platforms/agnostic/VorbisDecoder.cpp b/dom/media/platforms/agnostic/VorbisDecoder.cpp
new file mode 100644
index 0000000000..01c0e8dbe5
--- /dev/null
+++ b/dom/media/platforms/agnostic/VorbisDecoder.cpp
@@ -0,0 +1,364 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "VorbisDecoder.h"
+
+#include "VideoUtils.h"
+#include "VorbisUtils.h"
+#include "XiphExtradata.h"
+#include "mozilla/Logging.h"
+#include "mozilla/PodOperations.h"
+#include "mozilla/SyncRunnable.h"
+
+#undef LOG
+#define LOG(type, msg) MOZ_LOG(sPDMLog, type, msg)
+
+namespace mozilla {
+
+ogg_packet InitVorbisPacket(const unsigned char* aData, size_t aLength,
+ bool aBOS, bool aEOS, int64_t aGranulepos,
+ int64_t aPacketNo) {
+ ogg_packet packet;
+ packet.packet = const_cast<unsigned char*>(aData);
+ packet.bytes = aLength;
+ packet.b_o_s = aBOS;
+ packet.e_o_s = aEOS;
+ packet.granulepos = aGranulepos;
+ packet.packetno = aPacketNo;
+ return packet;
+}
+
+VorbisDataDecoder::VorbisDataDecoder(const CreateDecoderParams& aParams)
+ : mInfo(aParams.AudioConfig()), mPacketCount(0), mFrames(0) {
+ // Zero these member vars to avoid crashes in Vorbis clear functions when
+ // destructor is called before |Init|.
+ PodZero(&mVorbisBlock);
+ PodZero(&mVorbisDsp);
+ PodZero(&mVorbisInfo);
+ PodZero(&mVorbisComment);
+}
+
+VorbisDataDecoder::~VorbisDataDecoder() {
+ vorbis_block_clear(&mVorbisBlock);
+ vorbis_dsp_clear(&mVorbisDsp);
+ vorbis_info_clear(&mVorbisInfo);
+ vorbis_comment_clear(&mVorbisComment);
+}
+
+RefPtr<ShutdownPromise> VorbisDataDecoder::Shutdown() {
+ // mThread may not be set if Init hasn't been called first.
+ MOZ_ASSERT(!mThread || mThread->IsOnCurrentThread());
+ return ShutdownPromise::CreateAndResolve(true, __func__);
+}
+
+RefPtr<MediaDataDecoder::InitPromise> VorbisDataDecoder::Init() {
+ mThread = GetCurrentSerialEventTarget();
+ vorbis_info_init(&mVorbisInfo);
+ vorbis_comment_init(&mVorbisComment);
+ PodZero(&mVorbisDsp);
+ PodZero(&mVorbisBlock);
+
+ AutoTArray<unsigned char*, 4> headers;
+ AutoTArray<size_t, 4> headerLens;
+ MOZ_ASSERT(mInfo.mCodecSpecificConfig.is<VorbisCodecSpecificData>(),
+ "Vorbis decoder should get vorbis codec specific data");
+ RefPtr<MediaByteBuffer> vorbisHeaderBlob =
+ GetAudioCodecSpecificBlob(mInfo.mCodecSpecificConfig);
+ if (!XiphExtradataToHeaders(headers, headerLens, vorbisHeaderBlob->Elements(),
+ vorbisHeaderBlob->Length())) {
+ LOG(LogLevel::Warning, ("VorbisDecoder: could not get vorbis header"));
+ return InitPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Could not get vorbis header.")),
+ __func__);
+ }
+ for (size_t i = 0; i < headers.Length(); i++) {
+ if (NS_FAILED(DecodeHeader(headers[i], headerLens[i]))) {
+ LOG(LogLevel::Warning,
+ ("VorbisDecoder: could not get decode vorbis header"));
+ return InitPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Could not decode vorbis header.")),
+ __func__);
+ }
+ }
+
+ MOZ_ASSERT(mPacketCount == 3);
+
+ int r = vorbis_synthesis_init(&mVorbisDsp, &mVorbisInfo);
+ if (r) {
+ LOG(LogLevel::Warning, ("VorbisDecoder: could not init vorbis decoder"));
+ return InitPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Systhesis init fail.")),
+ __func__);
+ }
+
+ r = vorbis_block_init(&mVorbisDsp, &mVorbisBlock);
+ if (r) {
+ LOG(LogLevel::Warning, ("VorbisDecoder: could not init vorbis block"));
+ return InitPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Block init fail.")),
+ __func__);
+ }
+
+ if (mInfo.mRate != (uint32_t)mVorbisDsp.vi->rate) {
+ LOG(LogLevel::Warning, ("VorbisDecoder: Invalid Vorbis header: container "
+ "and codec rate do not match!"));
+ }
+ if (mInfo.mChannels != (uint32_t)mVorbisDsp.vi->channels) {
+ LOG(LogLevel::Warning, ("VorbisDecoder: Invalid Vorbis header: container "
+ "and codec channels do not match!"));
+ }
+
+ AudioConfig::ChannelLayout layout(mVorbisDsp.vi->channels);
+ if (!layout.IsValid()) {
+ LOG(LogLevel::Warning,
+ ("VorbisDecoder: Invalid Vorbis header: invalid channel layout!"));
+ return InitPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Invalid audio layout.")),
+ __func__);
+ }
+
+ return InitPromise::CreateAndResolve(TrackInfo::kAudioTrack, __func__);
+}
+
+nsresult VorbisDataDecoder::DecodeHeader(const unsigned char* aData,
+ size_t aLength) {
+ bool bos = mPacketCount == 0;
+ ogg_packet pkt =
+ InitVorbisPacket(aData, aLength, bos, false, 0, mPacketCount++);
+ MOZ_ASSERT(mPacketCount <= 3);
+
+ int r = vorbis_synthesis_headerin(&mVorbisInfo, &mVorbisComment, &pkt);
+ return r == 0 ? NS_OK : NS_ERROR_FAILURE;
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> VorbisDataDecoder::Decode(
+ MediaRawData* aSample) {
+ MOZ_ASSERT(mThread->IsOnCurrentThread());
+ PROCESS_DECODE_LOG(aSample);
+
+ const unsigned char* aData = aSample->Data();
+ size_t aLength = aSample->Size();
+ int64_t aOffset = aSample->mOffset;
+
+ MOZ_ASSERT(mPacketCount >= 3);
+
+ if (!mLastFrameTime ||
+ mLastFrameTime.ref() != aSample->mTime.ToMicroseconds()) {
+ // We are starting a new block.
+ mFrames = 0;
+ mLastFrameTime = Some(aSample->mTime.ToMicroseconds());
+ }
+
+ ogg_packet pkt =
+ InitVorbisPacket(aData, aLength, false, aSample->mEOS,
+ aSample->mTimecode.ToMicroseconds(), mPacketCount++);
+
+ int err = vorbis_synthesis(&mVorbisBlock, &pkt);
+ if (err) {
+ return DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ RESULT_DETAIL("vorbis_synthesis:%d", err)),
+ __func__);
+ LOG(LogLevel::Warning, ("vorbis_synthesis returned an error"));
+ }
+
+ err = vorbis_synthesis_blockin(&mVorbisDsp, &mVorbisBlock);
+ if (err) {
+ return DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ RESULT_DETAIL("vorbis_synthesis_blockin:%d", err)),
+ __func__);
+ LOG(LogLevel::Warning, ("vorbis_synthesis_blockin returned an error"));
+ }
+
+ VorbisPCMValue** pcm = 0;
+ int32_t frames = vorbis_synthesis_pcmout(&mVorbisDsp, &pcm);
+ if (frames == 0) {
+ return DecodePromise::CreateAndResolve(DecodedData(), __func__);
+ }
+
+ DecodedData results;
+ while (frames > 0) {
+ uint32_t channels = mVorbisDsp.vi->channels;
+ uint32_t rate = mVorbisDsp.vi->rate;
+ AlignedAudioBuffer buffer(frames * channels);
+ if (!buffer) {
+ LOG(LogLevel::Warning, ("VorbisDecoder: cannot allocate buffer"));
+ return DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__), __func__);
+ }
+ for (uint32_t j = 0; j < channels; ++j) {
+ VorbisPCMValue* channel = pcm[j];
+ for (uint32_t i = 0; i < uint32_t(frames); ++i) {
+ buffer[i * channels + j] = MOZ_CONVERT_VORBIS_SAMPLE(channel[i]);
+ }
+ }
+
+ auto duration = media::TimeUnit(frames, rate);
+ if (!duration.IsValid()) {
+ LOG(LogLevel::Warning, ("VorbisDecoder: invalid packet duration"));
+ return DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_OVERFLOW_ERR,
+ RESULT_DETAIL("Overflow converting audio duration")),
+ __func__);
+ }
+ auto total_duration = media::TimeUnit(mFrames, rate);
+ if (!total_duration.IsValid()) {
+ LOG(LogLevel::Warning, ("VorbisDecoder: invalid total duration"));
+ return DecodePromise::CreateAndReject(
+ MediaResult(
+ NS_ERROR_DOM_MEDIA_OVERFLOW_ERR,
+ RESULT_DETAIL("Overflow converting audio total_duration")),
+ __func__);
+ }
+
+ auto time = total_duration + aSample->mTime;
+ if (!time.IsValid()) {
+ LOG(LogLevel::Warning, ("VorbisDecoder: invalid sample time"));
+ return DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_OVERFLOW_ERR,
+ RESULT_DETAIL(
+ "Overflow adding total_duration and aSample->mTime")),
+ __func__);
+ };
+
+ if (!mAudioConverter) {
+ const AudioConfig::ChannelLayout layout =
+ AudioConfig::ChannelLayout(channels, VorbisLayout(channels));
+ AudioConfig in(layout, channels, rate);
+ AudioConfig out(AudioConfig::ChannelLayout::SMPTEDefault(layout),
+ channels, rate);
+ mAudioConverter = MakeUnique<AudioConverter>(in, out);
+ }
+ MOZ_ASSERT(mAudioConverter->CanWorkInPlace());
+ AudioSampleBuffer data(std::move(buffer));
+ data = mAudioConverter->Process(std::move(data));
+
+ RefPtr<AudioData> audio =
+ new AudioData(aOffset, time, data.Forget(), channels, rate,
+ mAudioConverter->OutputConfig().Layout().Map());
+ MOZ_DIAGNOSTIC_ASSERT(duration == audio->mDuration, "must be equal");
+ results.AppendElement(std::move(audio));
+ mFrames += frames;
+ err = vorbis_synthesis_read(&mVorbisDsp, frames);
+ if (err) {
+ LOG(LogLevel::Warning, ("VorbisDecoder: vorbis_synthesis_read"));
+ return DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ RESULT_DETAIL("vorbis_synthesis_read:%d", err)),
+ __func__);
+ }
+
+ frames = vorbis_synthesis_pcmout(&mVorbisDsp, &pcm);
+ }
+ return DecodePromise::CreateAndResolve(std::move(results), __func__);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> VorbisDataDecoder::Drain() {
+ MOZ_ASSERT(mThread->IsOnCurrentThread());
+ return DecodePromise::CreateAndResolve(DecodedData(), __func__);
+}
+
+RefPtr<MediaDataDecoder::FlushPromise> VorbisDataDecoder::Flush() {
+ MOZ_ASSERT(mThread->IsOnCurrentThread());
+ // Ignore failed results from vorbis_synthesis_restart. They
+ // aren't fatal and it fails when ResetDecode is called at a
+ // time when no vorbis data has been read.
+ vorbis_synthesis_restart(&mVorbisDsp);
+ mLastFrameTime.reset();
+ return FlushPromise::CreateAndResolve(true, __func__);
+}
+
+/* static */
+bool VorbisDataDecoder::IsVorbis(const nsACString& aMimeType) {
+ return aMimeType.EqualsLiteral("audio/vorbis");
+}
+
+/* static */
+const AudioConfig::Channel* VorbisDataDecoder::VorbisLayout(
+ uint32_t aChannels) {
+ // From https://www.xiph.org/vorbis/doc/Vorbis_I_spec.html
+ // Section 4.3.9.
+ typedef AudioConfig::Channel Channel;
+
+ switch (aChannels) {
+ case 1: // the stream is monophonic
+ {
+ static const Channel config[] = {AudioConfig::CHANNEL_FRONT_CENTER};
+ return config;
+ }
+ case 2: // the stream is stereo. channel order: left, right
+ {
+ static const Channel config[] = {AudioConfig::CHANNEL_FRONT_LEFT,
+ AudioConfig::CHANNEL_FRONT_RIGHT};
+ return config;
+ }
+ case 3: // the stream is a 1d-surround encoding. channel order: left,
+ // center, right
+ {
+ static const Channel config[] = {AudioConfig::CHANNEL_FRONT_LEFT,
+ AudioConfig::CHANNEL_FRONT_CENTER,
+ AudioConfig::CHANNEL_FRONT_RIGHT};
+ return config;
+ }
+ case 4: // the stream is quadraphonic surround. channel order: front left,
+ // front right, rear left, rear right
+ {
+ static const Channel config[] = {
+ AudioConfig::CHANNEL_FRONT_LEFT, AudioConfig::CHANNEL_FRONT_RIGHT,
+ AudioConfig::CHANNEL_BACK_LEFT, AudioConfig::CHANNEL_BACK_RIGHT};
+ return config;
+ }
+ case 5: // the stream is five-channel surround. channel order: front left,
+ // center, front right, rear left, rear right
+ {
+ static const Channel config[] = {
+ AudioConfig::CHANNEL_FRONT_LEFT, AudioConfig::CHANNEL_FRONT_CENTER,
+ AudioConfig::CHANNEL_FRONT_RIGHT, AudioConfig::CHANNEL_BACK_LEFT,
+ AudioConfig::CHANNEL_BACK_RIGHT};
+ return config;
+ }
+ case 6: // the stream is 5.1 surround. channel order: front left, center,
+ // front right, rear left, rear right, LFE
+ {
+ static const Channel config[] = {
+ AudioConfig::CHANNEL_FRONT_LEFT, AudioConfig::CHANNEL_FRONT_CENTER,
+ AudioConfig::CHANNEL_FRONT_RIGHT, AudioConfig::CHANNEL_BACK_LEFT,
+ AudioConfig::CHANNEL_BACK_RIGHT, AudioConfig::CHANNEL_LFE};
+ return config;
+ }
+ case 7: // surround. channel order: front left, center, front right, side
+ // left, side right, rear center, LFE
+ {
+ static const Channel config[] = {
+ AudioConfig::CHANNEL_FRONT_LEFT, AudioConfig::CHANNEL_FRONT_CENTER,
+ AudioConfig::CHANNEL_FRONT_RIGHT, AudioConfig::CHANNEL_SIDE_LEFT,
+ AudioConfig::CHANNEL_SIDE_RIGHT, AudioConfig::CHANNEL_BACK_CENTER,
+ AudioConfig::CHANNEL_LFE};
+ return config;
+ }
+ case 8: // the stream is 7.1 surround. channel order: front left, center,
+ // front right, side left, side right, rear left, rear right, LFE
+ {
+ static const Channel config[] = {
+ AudioConfig::CHANNEL_FRONT_LEFT, AudioConfig::CHANNEL_FRONT_CENTER,
+ AudioConfig::CHANNEL_FRONT_RIGHT, AudioConfig::CHANNEL_SIDE_LEFT,
+ AudioConfig::CHANNEL_SIDE_RIGHT, AudioConfig::CHANNEL_BACK_LEFT,
+ AudioConfig::CHANNEL_BACK_RIGHT, AudioConfig::CHANNEL_LFE};
+ return config;
+ }
+ default:
+ return nullptr;
+ }
+}
+
+} // namespace mozilla
+#undef LOG
diff --git a/dom/media/platforms/agnostic/VorbisDecoder.h b/dom/media/platforms/agnostic/VorbisDecoder.h
new file mode 100644
index 0000000000..ce61ccdcad
--- /dev/null
+++ b/dom/media/platforms/agnostic/VorbisDecoder.h
@@ -0,0 +1,66 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#if !defined(VorbisDecoder_h_)
+# define VorbisDecoder_h_
+
+# include "AudioConverter.h"
+# include "PlatformDecoderModule.h"
+# include "mozilla/Maybe.h"
+
+# ifdef MOZ_TREMOR
+# include "tremor/ivorbiscodec.h"
+# else
+# include "vorbis/codec.h"
+# endif
+
+namespace mozilla {
+
+DDLoggedTypeDeclNameAndBase(VorbisDataDecoder, MediaDataDecoder);
+
+class VorbisDataDecoder final
+ : public MediaDataDecoder,
+ public DecoderDoctorLifeLogger<VorbisDataDecoder> {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(VorbisDataDecoder, final);
+
+ explicit VorbisDataDecoder(const CreateDecoderParams& aParams);
+
+ RefPtr<InitPromise> Init() override;
+ RefPtr<DecodePromise> Decode(MediaRawData* aSample) override;
+ RefPtr<DecodePromise> Drain() override;
+ RefPtr<FlushPromise> Flush() override;
+ RefPtr<ShutdownPromise> Shutdown() override;
+ nsCString GetDescriptionName() const override {
+ return "vorbis audio decoder"_ns;
+ }
+ nsCString GetCodecName() const override { return "vorbis"_ns; }
+
+ // Return true if mimetype is Vorbis
+ static bool IsVorbis(const nsACString& aMimeType);
+ static const AudioConfig::Channel* VorbisLayout(uint32_t aChannels);
+
+ private:
+ ~VorbisDataDecoder();
+
+ nsresult DecodeHeader(const unsigned char* aData, size_t aLength);
+
+ const AudioInfo mInfo;
+ nsCOMPtr<nsISerialEventTarget> mThread;
+
+ // Vorbis decoder state
+ vorbis_info mVorbisInfo;
+ vorbis_comment mVorbisComment;
+ vorbis_dsp_state mVorbisDsp;
+ vorbis_block mVorbisBlock;
+
+ int64_t mPacketCount;
+ int64_t mFrames;
+ Maybe<int64_t> mLastFrameTime;
+ UniquePtr<AudioConverter> mAudioConverter;
+};
+
+} // namespace mozilla
+#endif
diff --git a/dom/media/platforms/agnostic/WAVDecoder.cpp b/dom/media/platforms/agnostic/WAVDecoder.cpp
new file mode 100644
index 0000000000..e8dc0dc38d
--- /dev/null
+++ b/dom/media/platforms/agnostic/WAVDecoder.cpp
@@ -0,0 +1,162 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WAVDecoder.h"
+
+#include "AudioSampleFormat.h"
+#include "BufferReader.h"
+#include "VideoUtils.h"
+#include "mozilla/Casting.h"
+#include "mozilla/SyncRunnable.h"
+
+namespace mozilla {
+
+int16_t DecodeALawSample(uint8_t aValue) {
+ aValue = aValue ^ 0x55;
+ int8_t sign = (aValue & 0x80) ? -1 : 1;
+ uint8_t exponent = (aValue & 0x70) >> 4;
+ uint8_t mantissa = aValue & 0x0F;
+ int16_t sample = mantissa << 4;
+ switch (exponent) {
+ case 0:
+ sample += 8;
+ break;
+ case 1:
+ sample += 0x108;
+ break;
+ default:
+ sample += 0x108;
+ sample <<= exponent - 1;
+ }
+ return sign * sample;
+}
+
+int16_t DecodeULawSample(uint8_t aValue) {
+ aValue = aValue ^ 0xFF;
+ int8_t sign = (aValue & 0x80) ? -1 : 1;
+ uint8_t exponent = (aValue & 0x70) >> 4;
+ uint8_t mantissa = aValue & 0x0F;
+ int16_t sample = (33 + 2 * mantissa) * (2 << (exponent + 1)) - 33;
+ return sign * sample;
+}
+
+WaveDataDecoder::WaveDataDecoder(const CreateDecoderParams& aParams)
+ : mInfo(aParams.AudioConfig()) {}
+
+RefPtr<ShutdownPromise> WaveDataDecoder::Shutdown() {
+ // mThread may not be set if Init hasn't been called first.
+ MOZ_ASSERT(!mThread || mThread->IsOnCurrentThread());
+ return ShutdownPromise::CreateAndResolve(true, __func__);
+}
+
+RefPtr<MediaDataDecoder::InitPromise> WaveDataDecoder::Init() {
+ mThread = GetCurrentSerialEventTarget();
+ return InitPromise::CreateAndResolve(TrackInfo::kAudioTrack, __func__);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> WaveDataDecoder::Decode(
+ MediaRawData* aSample) {
+ MOZ_ASSERT(mThread->IsOnCurrentThread());
+ size_t aLength = aSample->Size();
+ BufferReader aReader(aSample->Data(), aLength);
+ int64_t aOffset = aSample->mOffset;
+
+ int32_t frames = aLength * 8 / mInfo.mBitDepth / mInfo.mChannels;
+
+ AlignedAudioBuffer buffer(frames * mInfo.mChannels);
+ if (!buffer) {
+ return DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__), __func__);
+ }
+ for (int i = 0; i < frames; ++i) {
+ for (unsigned int j = 0; j < mInfo.mChannels; ++j) {
+ if (mInfo.mProfile == 3) { // IEEE Float Data
+ auto res = aReader.ReadLEU32();
+ if (res.isErr()) {
+ return DecodePromise::CreateAndReject(
+ MediaResult(res.unwrapErr(), __func__), __func__);
+ }
+ float sample = BitwiseCast<float>(res.unwrap());
+ buffer[i * mInfo.mChannels + j] =
+ FloatToAudioSample<AudioDataValue>(sample);
+ } else if (mInfo.mProfile == 6) { // ALAW Data
+ auto res = aReader.ReadU8();
+ if (res.isErr()) {
+ return DecodePromise::CreateAndReject(
+ MediaResult(res.unwrapErr(), __func__), __func__);
+ }
+ int16_t decoded = DecodeALawSample(res.unwrap());
+ buffer[i * mInfo.mChannels + j] =
+ IntegerToAudioSample<AudioDataValue>(decoded);
+ } else if (mInfo.mProfile == 7) { // ULAW Data
+ auto res = aReader.ReadU8();
+ if (res.isErr()) {
+ return DecodePromise::CreateAndReject(
+ MediaResult(res.unwrapErr(), __func__), __func__);
+ }
+ int16_t decoded = DecodeULawSample(res.unwrap());
+ buffer[i * mInfo.mChannels + j] =
+ IntegerToAudioSample<AudioDataValue>(decoded);
+ } else { // PCM Data
+ if (mInfo.mBitDepth == 8) {
+ auto res = aReader.ReadU8();
+ if (res.isErr()) {
+ return DecodePromise::CreateAndReject(
+ MediaResult(res.unwrapErr(), __func__), __func__);
+ }
+ buffer[i * mInfo.mChannels + j] =
+ UInt8bitToAudioSample<AudioDataValue>(res.unwrap());
+ } else if (mInfo.mBitDepth == 16) {
+ auto res = aReader.ReadLE16();
+ if (res.isErr()) {
+ return DecodePromise::CreateAndReject(
+ MediaResult(res.unwrapErr(), __func__), __func__);
+ }
+ buffer[i * mInfo.mChannels + j] =
+ IntegerToAudioSample<AudioDataValue>(res.unwrap());
+ } else if (mInfo.mBitDepth == 24) {
+ auto res = aReader.ReadLE24();
+ if (res.isErr()) {
+ return DecodePromise::CreateAndReject(
+ MediaResult(res.unwrapErr(), __func__), __func__);
+ }
+ buffer[i * mInfo.mChannels + j] =
+ Int24bitToAudioSample<AudioDataValue>(res.unwrap());
+ }
+ }
+ }
+ }
+
+ return DecodePromise::CreateAndResolve(
+ DecodedData{new AudioData(aOffset, aSample->mTime, std::move(buffer),
+ mInfo.mChannels, mInfo.mRate)},
+ __func__);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> WaveDataDecoder::Drain() {
+ MOZ_ASSERT(mThread->IsOnCurrentThread());
+ return DecodePromise::CreateAndResolve(DecodedData(), __func__);
+}
+
+RefPtr<MediaDataDecoder::FlushPromise> WaveDataDecoder::Flush() {
+ MOZ_ASSERT(mThread->IsOnCurrentThread());
+ return FlushPromise::CreateAndResolve(true, __func__);
+}
+
+/* static */
+bool WaveDataDecoder::IsWave(const nsACString& aMimeType) {
+ // Some WebAudio uses "audio/x-wav",
+ // WAVdemuxer uses "audio/wave; codecs=aNum".
+ return aMimeType.EqualsLiteral("audio/x-wav") ||
+ aMimeType.EqualsLiteral("audio/wave; codecs=1") ||
+ aMimeType.EqualsLiteral("audio/wave; codecs=3") ||
+ aMimeType.EqualsLiteral("audio/wave; codecs=6") ||
+ aMimeType.EqualsLiteral("audio/wave; codecs=7") ||
+ aMimeType.EqualsLiteral("audio/wave; codecs=65534");
+}
+
+} // namespace mozilla
+#undef LOG
diff --git a/dom/media/platforms/agnostic/WAVDecoder.h b/dom/media/platforms/agnostic/WAVDecoder.h
new file mode 100644
index 0000000000..8e3b614bd9
--- /dev/null
+++ b/dom/media/platforms/agnostic/WAVDecoder.h
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(WaveDecoder_h_)
+# define WaveDecoder_h_
+
+# include "PlatformDecoderModule.h"
+
+namespace mozilla {
+
+DDLoggedTypeDeclNameAndBase(WaveDataDecoder, MediaDataDecoder);
+
+class WaveDataDecoder final : public MediaDataDecoder,
+ public DecoderDoctorLifeLogger<WaveDataDecoder> {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WaveDataDecoder, final);
+
+ explicit WaveDataDecoder(const CreateDecoderParams& aParams);
+
+ // Return true if mimetype is Wave
+ static bool IsWave(const nsACString& aMimeType);
+
+ RefPtr<InitPromise> Init() override;
+ RefPtr<DecodePromise> Decode(MediaRawData* aSample) override;
+ RefPtr<DecodePromise> Drain() override;
+ RefPtr<FlushPromise> Flush() override;
+ RefPtr<ShutdownPromise> Shutdown() override;
+ nsCString GetDescriptionName() const override {
+ return "wave audio decoder"_ns;
+ }
+ nsCString GetCodecName() const override { return "wave"_ns; }
+
+ private:
+ ~WaveDataDecoder() = default;
+
+ const AudioInfo mInfo;
+ nsCOMPtr<nsISerialEventTarget> mThread;
+};
+
+} // namespace mozilla
+#endif
diff --git a/dom/media/platforms/agnostic/bytestreams/Adts.cpp b/dom/media/platforms/agnostic/bytestreams/Adts.cpp
new file mode 100644
index 0000000000..5f31904d9c
--- /dev/null
+++ b/dom/media/platforms/agnostic/bytestreams/Adts.cpp
@@ -0,0 +1,94 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "Adts.h"
+#include "MediaData.h"
+#include "mozilla/Array.h"
+#include "mozilla/ArrayUtils.h"
+
+namespace mozilla {
+
+static const int kADTSHeaderSize = 7;
+
+int8_t Adts::GetFrequencyIndex(uint32_t aSamplesPerSecond) {
+ static const uint32_t freq_lookup[] = {96000, 88200, 64000, 48000, 44100,
+ 32000, 24000, 22050, 16000, 12000,
+ 11025, 8000, 7350, 0};
+
+ int8_t i = 0;
+ while (freq_lookup[i] && aSamplesPerSecond < freq_lookup[i]) {
+ i++;
+ }
+
+ if (!freq_lookup[i]) {
+ return -1;
+ }
+
+ return i;
+}
+
+bool Adts::ConvertSample(uint16_t aChannelCount, int8_t aFrequencyIndex,
+ int8_t aProfile, MediaRawData* aSample) {
+ size_t newSize = aSample->Size() + kADTSHeaderSize;
+
+ // ADTS header uses 13 bits for packet size.
+ if (newSize >= (1 << 13) || aChannelCount > 15 || aFrequencyIndex < 0 ||
+ aProfile < 1 || aProfile > 4) {
+ return false;
+ }
+
+ Array<uint8_t, kADTSHeaderSize> header;
+ header[0] = 0xff;
+ header[1] = 0xf1;
+ header[2] =
+ ((aProfile - 1) << 6) + (aFrequencyIndex << 2) + (aChannelCount >> 2);
+ header[3] = ((aChannelCount & 0x3) << 6) + (newSize >> 11);
+ header[4] = (newSize & 0x7ff) >> 3;
+ header[5] = ((newSize & 7) << 5) + 0x1f;
+ header[6] = 0xfc;
+
+ UniquePtr<MediaRawDataWriter> writer(aSample->CreateWriter());
+ if (!writer->Prepend(&header[0], ArrayLength(header))) {
+ return false;
+ }
+
+ if (aSample->mCrypto.IsEncrypted()) {
+ if (aSample->mCrypto.mPlainSizes.Length() == 0) {
+ writer->mCrypto.mPlainSizes.AppendElement(kADTSHeaderSize);
+ writer->mCrypto.mEncryptedSizes.AppendElement(aSample->Size() -
+ kADTSHeaderSize);
+ } else {
+ writer->mCrypto.mPlainSizes[0] += kADTSHeaderSize;
+ }
+ }
+
+ return true;
+}
+
+bool Adts::RevertSample(MediaRawData* aSample) {
+ if (aSample->Size() < kADTSHeaderSize) {
+ return false;
+ }
+
+ {
+ const uint8_t* header = aSample->Data();
+ if (header[0] != 0xff || header[1] != 0xf1 || header[6] != 0xfc) {
+ // Not ADTS.
+ return false;
+ }
+ }
+
+ UniquePtr<MediaRawDataWriter> writer(aSample->CreateWriter());
+ writer->PopFront(kADTSHeaderSize);
+
+ if (aSample->mCrypto.IsEncrypted()) {
+ if (aSample->mCrypto.mPlainSizes.Length() > 0 &&
+ writer->mCrypto.mPlainSizes[0] >= kADTSHeaderSize) {
+ writer->mCrypto.mPlainSizes[0] -= kADTSHeaderSize;
+ }
+ }
+
+ return true;
+}
+} // namespace mozilla
diff --git a/dom/media/platforms/agnostic/bytestreams/Adts.h b/dom/media/platforms/agnostic/bytestreams/Adts.h
new file mode 100644
index 0000000000..c2b6b558b6
--- /dev/null
+++ b/dom/media/platforms/agnostic/bytestreams/Adts.h
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef ADTS_H_
+#define ADTS_H_
+
+#include <stdint.h>
+
+namespace mozilla {
+class MediaRawData;
+
+class Adts {
+ public:
+ static int8_t GetFrequencyIndex(uint32_t aSamplesPerSecond);
+ static bool ConvertSample(uint16_t aChannelCount, int8_t aFrequencyIndex,
+ int8_t aProfile, mozilla::MediaRawData* aSample);
+ static bool RevertSample(MediaRawData* aSample);
+};
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/platforms/agnostic/bytestreams/AnnexB.cpp b/dom/media/platforms/agnostic/bytestreams/AnnexB.cpp
new file mode 100644
index 0000000000..07e9c2dde8
--- /dev/null
+++ b/dom/media/platforms/agnostic/bytestreams/AnnexB.cpp
@@ -0,0 +1,364 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/EndianUtils.h"
+#include "mozilla/ResultExtensions.h"
+#include "mozilla/Unused.h"
+#include "AnnexB.h"
+#include "BufferReader.h"
+#include "ByteWriter.h"
+#include "MediaData.h"
+
+namespace mozilla {
+
+static const uint8_t kAnnexBDelimiter[] = {0, 0, 0, 1};
+
+Result<Ok, nsresult> AnnexB::ConvertSampleToAnnexB(
+ mozilla::MediaRawData* aSample, bool aAddSPS) {
+ MOZ_ASSERT(aSample);
+
+ if (!IsAVCC(aSample)) {
+ return Ok();
+ }
+ MOZ_ASSERT(aSample->Data());
+
+ MOZ_TRY(ConvertSampleTo4BytesAVCC(aSample));
+
+ if (aSample->Size() < 4) {
+ // Nothing to do, it's corrupted anyway.
+ return Ok();
+ }
+
+ BufferReader reader(aSample->Data(), aSample->Size());
+
+ nsTArray<uint8_t> tmp;
+ ByteWriter<BigEndian> writer(tmp);
+
+ while (reader.Remaining() >= 4) {
+ uint32_t nalLen;
+ MOZ_TRY_VAR(nalLen, reader.ReadU32());
+ const uint8_t* p = reader.Read(nalLen);
+
+ if (!writer.Write(kAnnexBDelimiter, ArrayLength(kAnnexBDelimiter))) {
+ return Err(NS_ERROR_OUT_OF_MEMORY);
+ }
+ if (!p) {
+ break;
+ }
+ if (!writer.Write(p, nalLen)) {
+ return Err(NS_ERROR_OUT_OF_MEMORY);
+ }
+ }
+
+ UniquePtr<MediaRawDataWriter> samplewriter(aSample->CreateWriter());
+
+ if (!samplewriter->Replace(tmp.Elements(), tmp.Length())) {
+ return Err(NS_ERROR_OUT_OF_MEMORY);
+ }
+
+ // Prepend the Annex B NAL with SPS and PPS tables to keyframes.
+ if (aAddSPS && aSample->mKeyframe) {
+ RefPtr<MediaByteBuffer> annexB =
+ ConvertExtraDataToAnnexB(aSample->mExtraData);
+ if (!samplewriter->Prepend(annexB->Elements(), annexB->Length())) {
+ return Err(NS_ERROR_OUT_OF_MEMORY);
+ }
+
+ // Prepending the NAL with SPS/PPS will mess up the encryption subsample
+ // offsets. So we need to account for the extra bytes by increasing
+ // the length of the first clear data subsample. Otherwise decryption
+ // will fail.
+ if (aSample->mCrypto.IsEncrypted()) {
+ if (aSample->mCrypto.mPlainSizes.Length() == 0) {
+ CheckedUint32 plainSize{annexB->Length()};
+ CheckedUint32 encryptedSize{samplewriter->Size()};
+ encryptedSize -= annexB->Length();
+ samplewriter->mCrypto.mPlainSizes.AppendElement(plainSize.value());
+ samplewriter->mCrypto.mEncryptedSizes.AppendElement(
+ encryptedSize.value());
+ } else {
+ CheckedUint32 newSize{samplewriter->mCrypto.mPlainSizes[0]};
+ newSize += annexB->Length();
+ samplewriter->mCrypto.mPlainSizes[0] = newSize.value();
+ }
+ }
+ }
+
+ return Ok();
+}
+
+already_AddRefed<mozilla::MediaByteBuffer> AnnexB::ConvertExtraDataToAnnexB(
+ const mozilla::MediaByteBuffer* aExtraData) {
+ // AVCC 6 byte header looks like:
+ // +------+------+------+------+------+------+------+------+
+ // [0] | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
+ // +------+------+------+------+------+------+------+------+
+ // [1] | profile |
+ // +------+------+------+------+------+------+------+------+
+ // [2] | compatiblity |
+ // +------+------+------+------+------+------+------+------+
+ // [3] | level |
+ // +------+------+------+------+------+------+------+------+
+ // [4] | unused | nalLenSiz-1 |
+ // +------+------+------+------+------+------+------+------+
+ // [5] | unused | numSps |
+ // +------+------+------+------+------+------+------+------+
+
+ RefPtr<mozilla::MediaByteBuffer> annexB = new mozilla::MediaByteBuffer;
+
+ BufferReader reader(*aExtraData);
+ const uint8_t* ptr = reader.Read(5);
+ if (ptr && ptr[0] == 1) {
+ // Append SPS then PPS
+ Unused << reader.ReadU8().map(
+ [&](uint8_t x) { return ConvertSPSOrPPS(reader, x & 31, annexB); });
+ Unused << reader.ReadU8().map(
+ [&](uint8_t x) { return ConvertSPSOrPPS(reader, x, annexB); });
+ // MP4Box adds extra bytes that we ignore. I don't know what they do.
+ }
+
+ return annexB.forget();
+}
+
+Result<mozilla::Ok, nsresult> AnnexB::ConvertSPSOrPPS(
+ BufferReader& aReader, uint8_t aCount, mozilla::MediaByteBuffer* aAnnexB) {
+ for (int i = 0; i < aCount; i++) {
+ uint16_t length;
+ MOZ_TRY_VAR(length, aReader.ReadU16());
+
+ const uint8_t* ptr = aReader.Read(length);
+ if (!ptr) {
+ return Err(NS_ERROR_FAILURE);
+ }
+ aAnnexB->AppendElements(kAnnexBDelimiter, ArrayLength(kAnnexBDelimiter));
+ aAnnexB->AppendElements(ptr, length);
+ }
+ return Ok();
+}
+
+static Result<Ok, nsresult> FindStartCodeInternal(BufferReader& aBr) {
+ size_t offset = aBr.Offset();
+
+ for (uint32_t i = 0; i < aBr.Align() && aBr.Remaining() >= 3; i++) {
+ auto res = aBr.PeekU24();
+ if (res.isOk() && (res.unwrap() == 0x000001)) {
+ return Ok();
+ }
+ mozilla::Unused << aBr.Read(1);
+ }
+
+ while (aBr.Remaining() >= 6) {
+ uint32_t x32;
+ MOZ_TRY_VAR(x32, aBr.PeekU32());
+ if ((x32 - 0x01010101) & (~x32) & 0x80808080) {
+ if ((x32 >> 8) == 0x000001) {
+ return Ok();
+ }
+ if (x32 == 0x000001) {
+ mozilla::Unused << aBr.Read(1);
+ return Ok();
+ }
+ if ((x32 & 0xff) == 0) {
+ const uint8_t* p = aBr.Peek(1);
+ if ((x32 & 0xff00) == 0 && p[4] == 1) {
+ mozilla::Unused << aBr.Read(2);
+ return Ok();
+ }
+ if (p[4] == 0 && p[5] == 1) {
+ mozilla::Unused << aBr.Read(3);
+ return Ok();
+ }
+ }
+ }
+ mozilla::Unused << aBr.Read(4);
+ }
+
+ while (aBr.Remaining() >= 3) {
+ uint32_t data;
+ MOZ_TRY_VAR(data, aBr.PeekU24());
+ if (data == 0x000001) {
+ return Ok();
+ }
+ mozilla::Unused << aBr.Read(1);
+ }
+
+ // No start code were found; Go back to the beginning.
+ mozilla::Unused << aBr.Seek(offset);
+ return Err(NS_ERROR_FAILURE);
+}
+
+static Result<Ok, nsresult> FindStartCode(BufferReader& aBr,
+ size_t& aStartSize) {
+ if (FindStartCodeInternal(aBr).isErr()) {
+ aStartSize = 0;
+ return Err(NS_ERROR_FAILURE);
+ }
+
+ aStartSize = 3;
+ if (aBr.Offset()) {
+ // Check if it's 4-bytes start code
+ aBr.Rewind(1);
+ uint8_t data;
+ MOZ_TRY_VAR(data, aBr.ReadU8());
+ if (data == 0) {
+ aStartSize = 4;
+ }
+ }
+ mozilla::Unused << aBr.Read(3);
+ return Ok();
+}
+
+/* static */
+void AnnexB::ParseNALEntries(const Span<const uint8_t>& aSpan,
+ nsTArray<AnnexB::NALEntry>& aEntries) {
+ BufferReader reader(aSpan.data(), aSpan.Length());
+ size_t startSize;
+ auto rv = FindStartCode(reader, startSize);
+ size_t startOffset = reader.Offset();
+ if (rv.isOk()) {
+ while (FindStartCode(reader, startSize).isOk()) {
+ int64_t offset = reader.Offset();
+ int64_t sizeNAL = offset - startOffset - startSize;
+ aEntries.AppendElement(AnnexB::NALEntry(startOffset, sizeNAL));
+ reader.Seek(startOffset);
+ reader.Read(sizeNAL + startSize);
+ startOffset = offset;
+ }
+ }
+ int64_t sizeNAL = reader.Remaining();
+ if (sizeNAL) {
+ aEntries.AppendElement(AnnexB::NALEntry(startOffset, sizeNAL));
+ }
+}
+
+static Result<mozilla::Ok, nsresult> ParseNALUnits(ByteWriter<BigEndian>& aBw,
+ BufferReader& aBr) {
+ size_t startSize;
+
+ auto rv = FindStartCode(aBr, startSize);
+ if (rv.isOk()) {
+ size_t startOffset = aBr.Offset();
+ while (FindStartCode(aBr, startSize).isOk()) {
+ size_t offset = aBr.Offset();
+ size_t sizeNAL = offset - startOffset - startSize;
+ aBr.Seek(startOffset);
+ if (!aBw.WriteU32(sizeNAL) || !aBw.Write(aBr.Read(sizeNAL), sizeNAL)) {
+ return Err(NS_ERROR_OUT_OF_MEMORY);
+ }
+ aBr.Read(startSize);
+ startOffset = offset;
+ }
+ }
+ size_t sizeNAL = aBr.Remaining();
+ if (sizeNAL) {
+ if (!aBw.WriteU32(sizeNAL) || !aBw.Write(aBr.Read(sizeNAL), sizeNAL)) {
+ return Err(NS_ERROR_OUT_OF_MEMORY);
+ }
+ }
+ return Ok();
+}
+
+bool AnnexB::ConvertSampleToAVCC(mozilla::MediaRawData* aSample,
+ const RefPtr<MediaByteBuffer>& aAVCCHeader) {
+ if (IsAVCC(aSample)) {
+ return ConvertSampleTo4BytesAVCC(aSample).isOk();
+ }
+ if (!IsAnnexB(aSample)) {
+ // Not AnnexB, nothing to convert.
+ return true;
+ }
+
+ nsTArray<uint8_t> nalu;
+ ByteWriter<BigEndian> writer(nalu);
+ BufferReader reader(aSample->Data(), aSample->Size());
+
+ if (ParseNALUnits(writer, reader).isErr()) {
+ return false;
+ }
+ UniquePtr<MediaRawDataWriter> samplewriter(aSample->CreateWriter());
+ if (!samplewriter->Replace(nalu.Elements(), nalu.Length())) {
+ return false;
+ }
+
+ if (aAVCCHeader) {
+ aSample->mExtraData = aAVCCHeader;
+ return true;
+ }
+
+ // Create the AVCC header.
+ auto extradata = MakeRefPtr<mozilla::MediaByteBuffer>();
+ static const uint8_t kFakeExtraData[] = {
+ 1 /* version */,
+ 0x64 /* profile (High) */,
+ 0 /* profile compat (0) */,
+ 40 /* level (40) */,
+ 0xfc | 3 /* nal size - 1 */,
+ 0xe0 /* num SPS (0) */,
+ 0 /* num PPS (0) */
+ };
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier.
+ extradata->AppendElements(kFakeExtraData, ArrayLength(kFakeExtraData));
+ aSample->mExtraData = std::move(extradata);
+ return true;
+}
+
+Result<mozilla::Ok, nsresult> AnnexB::ConvertSampleTo4BytesAVCC(
+ mozilla::MediaRawData* aSample) {
+ MOZ_ASSERT(IsAVCC(aSample));
+
+ int nalLenSize = ((*aSample->mExtraData)[4] & 3) + 1;
+
+ if (nalLenSize == 4) {
+ return Ok();
+ }
+ nsTArray<uint8_t> dest;
+ ByteWriter<BigEndian> writer(dest);
+ BufferReader reader(aSample->Data(), aSample->Size());
+ while (reader.Remaining() > nalLenSize) {
+ uint32_t nalLen;
+ switch (nalLenSize) {
+ case 1:
+ MOZ_TRY_VAR(nalLen, reader.ReadU8());
+ break;
+ case 2:
+ MOZ_TRY_VAR(nalLen, reader.ReadU16());
+ break;
+ case 3:
+ MOZ_TRY_VAR(nalLen, reader.ReadU24());
+ break;
+ }
+
+ MOZ_ASSERT(nalLenSize != 4);
+
+ const uint8_t* p = reader.Read(nalLen);
+ if (!p) {
+ return Ok();
+ }
+ if (!writer.WriteU32(nalLen) || !writer.Write(p, nalLen)) {
+ return Err(NS_ERROR_OUT_OF_MEMORY);
+ }
+ }
+ UniquePtr<MediaRawDataWriter> samplewriter(aSample->CreateWriter());
+ if (!samplewriter->Replace(dest.Elements(), dest.Length())) {
+ return Err(NS_ERROR_OUT_OF_MEMORY);
+ }
+ return Ok();
+}
+
+bool AnnexB::IsAVCC(const mozilla::MediaRawData* aSample) {
+ return aSample->Size() >= 3 && aSample->mExtraData &&
+ aSample->mExtraData->Length() >= 7 && (*aSample->mExtraData)[0] == 1;
+}
+
+bool AnnexB::IsAnnexB(const mozilla::MediaRawData* aSample) {
+ if (aSample->Size() < 4) {
+ return false;
+ }
+ uint32_t header = mozilla::BigEndian::readUint32(aSample->Data());
+ return header == 0x00000001 || (header >> 8) == 0x000001;
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/agnostic/bytestreams/AnnexB.h b/dom/media/platforms/agnostic/bytestreams/AnnexB.h
new file mode 100644
index 0000000000..dbb8a7c3e1
--- /dev/null
+++ b/dom/media/platforms/agnostic/bytestreams/AnnexB.h
@@ -0,0 +1,66 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_PLATFORMS_AGNOSTIC_BYTESTREAMS_ANNEX_B_H_
+#define DOM_MEDIA_PLATFORMS_AGNOSTIC_BYTESTREAMS_ANNEX_B_H_
+
+#include "ErrorList.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Result.h"
+
+template <class>
+class nsTArray;
+
+namespace mozilla {
+class BufferReader;
+class MediaRawData;
+class MediaByteBuffer;
+
+class AnnexB {
+ public:
+ struct NALEntry {
+ NALEntry(int64_t aOffset, int64_t aSize) : mOffset(aOffset), mSize(aSize) {
+ MOZ_ASSERT(mOffset >= 0);
+ MOZ_ASSERT(mSize >= 0);
+ }
+ // They should be non-negative, so we use int64_t to assert their value when
+ // assigning value to them.
+ int64_t mOffset;
+ int64_t mSize;
+ };
+ // All conversions assume size of NAL length field is 4 bytes.
+ // Convert a sample from AVCC format to Annex B.
+ static mozilla::Result<mozilla::Ok, nsresult> ConvertSampleToAnnexB(
+ mozilla::MediaRawData* aSample, bool aAddSPS = true);
+ // Convert a sample from Annex B to AVCC.
+ // an AVCC extradata must not be set.
+ static bool ConvertSampleToAVCC(
+ mozilla::MediaRawData* aSample,
+ const RefPtr<mozilla::MediaByteBuffer>& aAVCCHeader = nullptr);
+ static mozilla::Result<mozilla::Ok, nsresult> ConvertSampleTo4BytesAVCC(
+ mozilla::MediaRawData* aSample);
+
+ // Parse an AVCC extradata and construct the Annex B sample header.
+ static already_AddRefed<mozilla::MediaByteBuffer> ConvertExtraDataToAnnexB(
+ const mozilla::MediaByteBuffer* aExtraData);
+ // Returns true if format is AVCC and sample has valid extradata.
+ static bool IsAVCC(const mozilla::MediaRawData* aSample);
+ // Returns true if format is AnnexB.
+ static bool IsAnnexB(const mozilla::MediaRawData* aSample);
+
+ // Parse NAL entries from the bytes stream to know the offset and the size of
+ // each NAL in the bytes stream.
+ static void ParseNALEntries(const Span<const uint8_t>& aSpan,
+ nsTArray<AnnexB::NALEntry>& aEntries);
+
+ private:
+ // AVCC box parser helper.
+ static mozilla::Result<mozilla::Ok, nsresult> ConvertSPSOrPPS(
+ mozilla::BufferReader& aReader, uint8_t aCount,
+ mozilla::MediaByteBuffer* aAnnexB);
+};
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_PLATFORMS_AGNOSTIC_BYTESTREAMS_ANNEX_B_H_
diff --git a/dom/media/platforms/agnostic/bytestreams/H264.cpp b/dom/media/platforms/agnostic/bytestreams/H264.cpp
new file mode 100644
index 0000000000..4dc33e1763
--- /dev/null
+++ b/dom/media/platforms/agnostic/bytestreams/H264.cpp
@@ -0,0 +1,1356 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "H264.h"
+#include <cmath>
+#include <limits>
+#include "AnnexB.h"
+#include "BitReader.h"
+#include "BitWriter.h"
+#include "BufferReader.h"
+#include "ByteWriter.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/PodOperations.h"
+#include "mozilla/ResultExtensions.h"
+
+#define READSE(var, min, max) \
+ { \
+ int32_t val = br.ReadSE(); \
+ if (val < min || val > max) { \
+ return false; \
+ } \
+ aDest.var = val; \
+ }
+
+#define READUE(var, max) \
+ { \
+ uint32_t uval = br.ReadUE(); \
+ if (uval > max) { \
+ return false; \
+ } \
+ aDest.var = uval; \
+ }
+
+namespace mozilla {
+
+// Default scaling lists (per spec).
+// ITU H264:
+// Table 7-2 – Assignment of mnemonic names to scaling list indices and
+// specification of fall-back rule
+static const uint8_t Default_4x4_Intra[16] = {6, 13, 13, 20, 20, 20, 28, 28,
+ 28, 28, 32, 32, 32, 37, 37, 42};
+
+static const uint8_t Default_4x4_Inter[16] = {10, 14, 14, 20, 20, 20, 24, 24,
+ 24, 24, 27, 27, 27, 30, 30, 34};
+
+static const uint8_t Default_8x8_Intra[64] = {
+ 6, 10, 10, 13, 11, 13, 16, 16, 16, 16, 18, 18, 18, 18, 18, 23,
+ 23, 23, 23, 23, 23, 25, 25, 25, 25, 25, 25, 25, 27, 27, 27, 27,
+ 27, 27, 27, 27, 29, 29, 29, 29, 29, 29, 29, 31, 31, 31, 31, 31,
+ 31, 33, 33, 33, 33, 33, 36, 36, 36, 36, 38, 38, 38, 40, 40, 42};
+
+static const uint8_t Default_8x8_Inter[64] = {
+ 9, 13, 13, 15, 13, 15, 17, 17, 17, 17, 19, 19, 19, 19, 19, 21,
+ 21, 21, 21, 21, 21, 22, 22, 22, 22, 22, 22, 22, 24, 24, 24, 24,
+ 24, 24, 24, 24, 25, 25, 25, 25, 25, 25, 25, 27, 27, 27, 27, 27,
+ 27, 28, 28, 28, 28, 28, 30, 30, 30, 30, 32, 32, 32, 33, 33, 35};
+
+namespace detail {
+static void scaling_list(BitReader& aBr, uint8_t* aScalingList,
+ int aSizeOfScalingList, const uint8_t* aDefaultList,
+ const uint8_t* aFallbackList) {
+ int32_t lastScale = 8;
+ int32_t nextScale = 8;
+ int32_t deltaScale;
+
+ // (pic|seq)_scaling_list_present_flag[i]
+ if (!aBr.ReadBit()) {
+ if (aFallbackList) {
+ memcpy(aScalingList, aFallbackList, aSizeOfScalingList);
+ }
+ return;
+ }
+
+ for (int i = 0; i < aSizeOfScalingList; i++) {
+ if (nextScale != 0) {
+ deltaScale = aBr.ReadSE();
+ nextScale = (lastScale + deltaScale + 256) % 256;
+ if (!i && !nextScale) {
+ memcpy(aScalingList, aDefaultList, aSizeOfScalingList);
+ return;
+ }
+ }
+ aScalingList[i] = (nextScale == 0) ? lastScale : nextScale;
+ lastScale = aScalingList[i];
+ }
+}
+} // namespace detail.
+
+template <size_t N>
+static void scaling_list(BitReader& aBr, uint8_t (&aScalingList)[N],
+ const uint8_t (&aDefaultList)[N],
+ const uint8_t (&aFallbackList)[N]) {
+ detail::scaling_list(aBr, aScalingList, N, aDefaultList, aFallbackList);
+}
+
+template <size_t N>
+static void scaling_list(BitReader& aBr, uint8_t (&aScalingList)[N],
+ const uint8_t (&aDefaultList)[N]) {
+ detail::scaling_list(aBr, aScalingList, N, aDefaultList, nullptr);
+}
+
+SPSData::SPSData() {
+ PodZero(this);
+ // Default values when they aren't defined as per ITU-T H.264 (2014/02).
+ chroma_format_idc = 1;
+ video_format = 5;
+ colour_primaries = 2;
+ transfer_characteristics = 2;
+ sample_ratio = 1.0;
+ memset(scaling_matrix4x4, 16, sizeof(scaling_matrix4x4));
+ memset(scaling_matrix8x8, 16, sizeof(scaling_matrix8x8));
+}
+
+bool SPSData::operator==(const SPSData& aOther) const {
+ return this->valid && aOther.valid && !memcmp(this, &aOther, sizeof(SPSData));
+}
+
+bool SPSData::operator!=(const SPSData& aOther) const {
+ return !(operator==(aOther));
+}
+
+// Described in ISO 23001-8:2016
+// Table 2
+enum class PrimaryID : uint8_t {
+ INVALID = 0,
+ BT709 = 1,
+ UNSPECIFIED = 2,
+ BT470M = 4,
+ BT470BG = 5,
+ SMPTE170M = 6,
+ SMPTE240M = 7,
+ FILM = 8,
+ BT2020 = 9,
+ SMPTEST428_1 = 10,
+ SMPTEST431_2 = 11,
+ SMPTEST432_1 = 12,
+ EBU_3213_E = 22
+};
+
+// Table 3
+enum class TransferID : uint8_t {
+ INVALID = 0,
+ BT709 = 1,
+ UNSPECIFIED = 2,
+ GAMMA22 = 4,
+ GAMMA28 = 5,
+ SMPTE170M = 6,
+ SMPTE240M = 7,
+ LINEAR = 8,
+ LOG = 9,
+ LOG_SQRT = 10,
+ IEC61966_2_4 = 11,
+ BT1361_ECG = 12,
+ IEC61966_2_1 = 13,
+ BT2020_10 = 14,
+ BT2020_12 = 15,
+ SMPTEST2084 = 16,
+ SMPTEST428_1 = 17,
+
+ // Not yet standardized
+ ARIB_STD_B67 = 18, // AKA hybrid-log gamma, HLG.
+};
+
+// Table 4
+enum class MatrixID : uint8_t {
+ RGB = 0,
+ BT709 = 1,
+ UNSPECIFIED = 2,
+ FCC = 4,
+ BT470BG = 5,
+ SMPTE170M = 6,
+ SMPTE240M = 7,
+ YCOCG = 8,
+ BT2020_NCL = 9,
+ BT2020_CL = 10,
+ YDZDX = 11,
+ INVALID = 255,
+};
+
+static PrimaryID GetPrimaryID(int aPrimary) {
+ if (aPrimary < 1 || aPrimary > 22 || aPrimary == 3) {
+ return PrimaryID::INVALID;
+ }
+ if (aPrimary > 12 && aPrimary < 22) {
+ return PrimaryID::INVALID;
+ }
+ return static_cast<PrimaryID>(aPrimary);
+}
+
+static TransferID GetTransferID(int aTransfer) {
+ if (aTransfer < 1 || aTransfer > 18 || aTransfer == 3) {
+ return TransferID::INVALID;
+ }
+ return static_cast<TransferID>(aTransfer);
+}
+
+static MatrixID GetMatrixID(int aMatrix) {
+ if (aMatrix < 0 || aMatrix > 11 || aMatrix == 3) {
+ return MatrixID::INVALID;
+ }
+ return static_cast<MatrixID>(aMatrix);
+}
+
+gfx::YUVColorSpace SPSData::ColorSpace() const {
+ // Bitfield, note that guesses with higher values take precedence over
+ // guesses with lower values.
+ enum Guess {
+ GUESS_BT601 = 1 << 0,
+ GUESS_BT709 = 1 << 1,
+ GUESS_BT2020 = 1 << 2,
+ };
+
+ uint32_t guess = 0;
+
+ switch (GetPrimaryID(colour_primaries)) {
+ case PrimaryID::BT709:
+ guess |= GUESS_BT709;
+ break;
+ case PrimaryID::BT470M:
+ case PrimaryID::BT470BG:
+ case PrimaryID::SMPTE170M:
+ case PrimaryID::SMPTE240M:
+ guess |= GUESS_BT601;
+ break;
+ case PrimaryID::BT2020:
+ guess |= GUESS_BT2020;
+ break;
+ case PrimaryID::FILM:
+ case PrimaryID::SMPTEST428_1:
+ case PrimaryID::SMPTEST431_2:
+ case PrimaryID::SMPTEST432_1:
+ case PrimaryID::EBU_3213_E:
+ case PrimaryID::INVALID:
+ case PrimaryID::UNSPECIFIED:
+ break;
+ }
+
+ switch (GetTransferID(transfer_characteristics)) {
+ case TransferID::BT709:
+ guess |= GUESS_BT709;
+ break;
+ case TransferID::GAMMA22:
+ case TransferID::GAMMA28:
+ case TransferID::SMPTE170M:
+ case TransferID::SMPTE240M:
+ guess |= GUESS_BT601;
+ break;
+ case TransferID::BT2020_10:
+ case TransferID::BT2020_12:
+ guess |= GUESS_BT2020;
+ break;
+ case TransferID::LINEAR:
+ case TransferID::LOG:
+ case TransferID::LOG_SQRT:
+ case TransferID::IEC61966_2_4:
+ case TransferID::BT1361_ECG:
+ case TransferID::IEC61966_2_1:
+ case TransferID::SMPTEST2084:
+ case TransferID::SMPTEST428_1:
+ case TransferID::ARIB_STD_B67:
+ case TransferID::INVALID:
+ case TransferID::UNSPECIFIED:
+ break;
+ }
+
+ switch (GetMatrixID(matrix_coefficients)) {
+ case MatrixID::BT709:
+ guess |= GUESS_BT709;
+ break;
+ case MatrixID::BT470BG:
+ case MatrixID::SMPTE170M:
+ case MatrixID::SMPTE240M:
+ guess |= GUESS_BT601;
+ break;
+ case MatrixID::BT2020_NCL:
+ case MatrixID::BT2020_CL:
+ guess |= GUESS_BT2020;
+ break;
+ case MatrixID::RGB:
+ case MatrixID::FCC:
+ case MatrixID::YCOCG:
+ case MatrixID::YDZDX:
+ case MatrixID::INVALID:
+ case MatrixID::UNSPECIFIED:
+ break;
+ }
+
+ // Removes lowest bit until only a single bit remains.
+ while (guess & (guess - 1)) {
+ guess &= guess - 1;
+ }
+ if (!guess) {
+ // A better default to BT601 which should die a slow death.
+ guess = GUESS_BT709;
+ }
+
+ switch (guess) {
+ case GUESS_BT601:
+ return gfx::YUVColorSpace::BT601;
+ case GUESS_BT709:
+ return gfx::YUVColorSpace::BT709;
+ case GUESS_BT2020:
+ return gfx::YUVColorSpace::BT2020;
+ default:
+ MOZ_CRASH("not possible to get here but makes compiler happy");
+ }
+}
+
+gfx::ColorDepth SPSData::ColorDepth() const {
+ if (bit_depth_luma_minus8 != 0 && bit_depth_luma_minus8 != 2 &&
+ bit_depth_luma_minus8 != 4) {
+ // We don't know what that is, just assume 8 bits to prevent decoding
+ // regressions if we ever encounter those.
+ return gfx::ColorDepth::COLOR_8;
+ }
+ return gfx::ColorDepthForBitDepth(bit_depth_luma_minus8 + 8);
+}
+
+// SPSNAL and SPSNALIterator do not own their data.
+class SPSNAL {
+ public:
+ SPSNAL(const uint8_t* aPtr, size_t aLength) {
+ MOZ_ASSERT(aPtr);
+
+ if (aLength == 0 || (*aPtr & 0x1f) != H264_NAL_SPS) {
+ return;
+ }
+ mDecodedNAL = H264::DecodeNALUnit(aPtr, aLength);
+ if (mDecodedNAL) {
+ mLength = BitReader::GetBitLength(mDecodedNAL);
+ }
+ }
+
+ SPSNAL() = default;
+
+ bool IsValid() const { return mDecodedNAL; }
+
+ bool operator==(const SPSNAL& aOther) const {
+ if (!mDecodedNAL || !aOther.mDecodedNAL) {
+ return false;
+ }
+
+ SPSData decodedSPS1;
+ SPSData decodedSPS2;
+ if (!GetSPSData(decodedSPS1) || !aOther.GetSPSData(decodedSPS2)) {
+ // Couldn't decode one SPS, perform a binary comparison
+ if (mLength != aOther.mLength) {
+ return false;
+ }
+ MOZ_ASSERT(mLength / 8 <= mDecodedNAL->Length());
+
+ if (memcmp(mDecodedNAL->Elements(), aOther.mDecodedNAL->Elements(),
+ mLength / 8)) {
+ return false;
+ }
+
+ uint32_t remaining = mLength - (mLength & ~7);
+
+ BitReader b1(mDecodedNAL->Elements() + mLength / 8, remaining);
+ BitReader b2(aOther.mDecodedNAL->Elements() + mLength / 8, remaining);
+ for (uint32_t i = 0; i < remaining; i++) {
+ if (b1.ReadBit() != b2.ReadBit()) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ return decodedSPS1 == decodedSPS2;
+ }
+
+ bool operator!=(const SPSNAL& aOther) const { return !(operator==(aOther)); }
+
+ bool GetSPSData(SPSData& aDest) const {
+ return H264::DecodeSPS(mDecodedNAL, aDest);
+ }
+
+ private:
+ RefPtr<mozilla::MediaByteBuffer> mDecodedNAL;
+ uint32_t mLength = 0;
+};
+
+class SPSNALIterator {
+ public:
+ explicit SPSNALIterator(const mozilla::MediaByteBuffer* aExtraData)
+ : mExtraDataPtr(aExtraData->Elements()), mReader(aExtraData) {
+ if (!mReader.Read(5)) {
+ return;
+ }
+
+ auto res = mReader.ReadU8();
+ mNumSPS = res.isOk() ? res.unwrap() & 0x1f : 0;
+ if (mNumSPS == 0) {
+ return;
+ }
+ mValid = true;
+ }
+
+ SPSNALIterator& operator++() {
+ if (mEOS || !mValid) {
+ return *this;
+ }
+ if (--mNumSPS == 0) {
+ mEOS = true;
+ }
+ auto res = mReader.ReadU16();
+ uint16_t length = res.isOk() ? res.unwrap() : 0;
+ if (length == 0 || !mReader.Read(length)) {
+ mEOS = true;
+ }
+ return *this;
+ }
+
+ explicit operator bool() const { return mValid && !mEOS; }
+
+ SPSNAL operator*() const {
+ MOZ_ASSERT(bool(*this));
+ BufferReader reader(mExtraDataPtr + mReader.Offset(), mReader.Remaining());
+
+ auto res = reader.ReadU16();
+ uint16_t length = res.isOk() ? res.unwrap() : 0;
+ const uint8_t* ptr = reader.Read(length);
+ if (!ptr || !length) {
+ return SPSNAL();
+ }
+ return SPSNAL(ptr, length);
+ }
+
+ private:
+ const uint8_t* mExtraDataPtr;
+ BufferReader mReader;
+ bool mValid = false;
+ bool mEOS = false;
+ uint8_t mNumSPS = 0;
+};
+
+/* static */ already_AddRefed<mozilla::MediaByteBuffer> H264::DecodeNALUnit(
+ const uint8_t* aNAL, size_t aLength) {
+ MOZ_ASSERT(aNAL);
+
+ if (aLength < 4) {
+ return nullptr;
+ }
+
+ RefPtr<mozilla::MediaByteBuffer> rbsp = new mozilla::MediaByteBuffer;
+ BufferReader reader(aNAL, aLength);
+ auto res = reader.ReadU8();
+ if (res.isErr()) {
+ return nullptr;
+ }
+ uint8_t nal_unit_type = res.unwrap() & 0x1f;
+ uint32_t nalUnitHeaderBytes = 1;
+ if (nal_unit_type == H264_NAL_PREFIX || nal_unit_type == H264_NAL_SLICE_EXT ||
+ nal_unit_type == H264_NAL_SLICE_EXT_DVC) {
+ bool svc_extension_flag = false;
+ bool avc_3d_extension_flag = false;
+ if (nal_unit_type != H264_NAL_SLICE_EXT_DVC) {
+ res = reader.PeekU8();
+ if (res.isErr()) {
+ return nullptr;
+ }
+ svc_extension_flag = res.unwrap() & 0x80;
+ } else {
+ res = reader.PeekU8();
+ if (res.isErr()) {
+ return nullptr;
+ }
+ avc_3d_extension_flag = res.unwrap() & 0x80;
+ }
+ if (svc_extension_flag) {
+ nalUnitHeaderBytes += 3;
+ } else if (avc_3d_extension_flag) {
+ nalUnitHeaderBytes += 2;
+ } else {
+ nalUnitHeaderBytes += 3;
+ }
+ }
+ if (!reader.Read(nalUnitHeaderBytes - 1)) {
+ return nullptr;
+ }
+ uint32_t lastbytes = 0xffff;
+ while (reader.Remaining()) {
+ auto res = reader.ReadU8();
+ if (res.isErr()) {
+ return nullptr;
+ }
+ uint8_t byte = res.unwrap();
+ if ((lastbytes & 0xffff) == 0 && byte == 0x03) {
+ // reset last two bytes, to detect the 0x000003 sequence again.
+ lastbytes = 0xffff;
+ } else {
+ rbsp->AppendElement(byte);
+ }
+ lastbytes = (lastbytes << 8) | byte;
+ }
+ return rbsp.forget();
+}
+
+// The reverse of DecodeNALUnit. To allow the distinction between Annex B (that
+// uses 0x000001 as marker) and AVCC, the pattern 0x00 0x00 0x0n (where n is
+// between 0 and 3) can't be found in the bytestream. A 0x03 byte is inserted
+// after the second 0. Eg. 0x00 0x00 0x00 becomes 0x00 0x00 0x03 0x00
+/* static */ already_AddRefed<mozilla::MediaByteBuffer> H264::EncodeNALUnit(
+ const uint8_t* aNAL, size_t aLength) {
+ MOZ_ASSERT(aNAL);
+ RefPtr<MediaByteBuffer> rbsp = new MediaByteBuffer();
+ BufferReader reader(aNAL, aLength);
+
+ auto res = reader.ReadU8();
+ if (res.isErr()) {
+ return rbsp.forget();
+ }
+ rbsp->AppendElement(res.unwrap());
+
+ res = reader.ReadU8();
+ if (res.isErr()) {
+ return rbsp.forget();
+ }
+ rbsp->AppendElement(res.unwrap());
+
+ while ((res = reader.ReadU8()).isOk()) {
+ uint8_t val = res.unwrap();
+ if (val <= 0x03 && rbsp->ElementAt(rbsp->Length() - 2) == 0 &&
+ rbsp->ElementAt(rbsp->Length() - 1) == 0) {
+ rbsp->AppendElement(0x03);
+ }
+ rbsp->AppendElement(val);
+ }
+ return rbsp.forget();
+}
+
+static int32_t ConditionDimension(float aValue) {
+ // This will exclude NaNs and too-big values.
+ if (aValue > 1.0 && aValue <= float(INT32_MAX) / 2) {
+ return int32_t(aValue);
+ }
+ return 0;
+}
+
+/* static */
+bool H264::DecodeSPS(const mozilla::MediaByteBuffer* aSPS, SPSData& aDest) {
+ if (!aSPS) {
+ return false;
+ }
+ BitReader br(aSPS, BitReader::GetBitLength(aSPS));
+
+ aDest.profile_idc = br.ReadBits(8);
+ aDest.constraint_set0_flag = br.ReadBit();
+ aDest.constraint_set1_flag = br.ReadBit();
+ aDest.constraint_set2_flag = br.ReadBit();
+ aDest.constraint_set3_flag = br.ReadBit();
+ aDest.constraint_set4_flag = br.ReadBit();
+ aDest.constraint_set5_flag = br.ReadBit();
+ br.ReadBits(2); // reserved_zero_2bits
+ aDest.level_idc = br.ReadBits(8);
+ READUE(seq_parameter_set_id, MAX_SPS_COUNT - 1);
+
+ if (aDest.profile_idc == 100 || aDest.profile_idc == 110 ||
+ aDest.profile_idc == 122 || aDest.profile_idc == 244 ||
+ aDest.profile_idc == 44 || aDest.profile_idc == 83 ||
+ aDest.profile_idc == 86 || aDest.profile_idc == 118 ||
+ aDest.profile_idc == 128 || aDest.profile_idc == 138 ||
+ aDest.profile_idc == 139 || aDest.profile_idc == 134) {
+ READUE(chroma_format_idc, 3);
+ if (aDest.chroma_format_idc == 3) {
+ aDest.separate_colour_plane_flag = br.ReadBit();
+ }
+ READUE(bit_depth_luma_minus8, 6);
+ READUE(bit_depth_chroma_minus8, 6);
+ br.ReadBit(); // qpprime_y_zero_transform_bypass_flag
+ aDest.seq_scaling_matrix_present_flag = br.ReadBit();
+ if (aDest.seq_scaling_matrix_present_flag) {
+ scaling_list(br, aDest.scaling_matrix4x4[0], Default_4x4_Intra,
+ Default_4x4_Intra);
+ scaling_list(br, aDest.scaling_matrix4x4[1], Default_4x4_Intra,
+ aDest.scaling_matrix4x4[0]);
+ scaling_list(br, aDest.scaling_matrix4x4[2], Default_4x4_Intra,
+ aDest.scaling_matrix4x4[1]);
+ scaling_list(br, aDest.scaling_matrix4x4[3], Default_4x4_Inter,
+ Default_4x4_Inter);
+ scaling_list(br, aDest.scaling_matrix4x4[4], Default_4x4_Inter,
+ aDest.scaling_matrix4x4[3]);
+ scaling_list(br, aDest.scaling_matrix4x4[5], Default_4x4_Inter,
+ aDest.scaling_matrix4x4[4]);
+
+ scaling_list(br, aDest.scaling_matrix8x8[0], Default_8x8_Intra,
+ Default_8x8_Intra);
+ scaling_list(br, aDest.scaling_matrix8x8[1], Default_8x8_Inter,
+ Default_8x8_Inter);
+ if (aDest.chroma_format_idc == 3) {
+ scaling_list(br, aDest.scaling_matrix8x8[2], Default_8x8_Intra,
+ aDest.scaling_matrix8x8[0]);
+ scaling_list(br, aDest.scaling_matrix8x8[3], Default_8x8_Inter,
+ aDest.scaling_matrix8x8[1]);
+ scaling_list(br, aDest.scaling_matrix8x8[4], Default_8x8_Intra,
+ aDest.scaling_matrix8x8[2]);
+ scaling_list(br, aDest.scaling_matrix8x8[5], Default_8x8_Inter,
+ aDest.scaling_matrix8x8[3]);
+ }
+ }
+ } else if (aDest.profile_idc == 183) {
+ aDest.chroma_format_idc = 0;
+ } else {
+ // default value if chroma_format_idc isn't set.
+ aDest.chroma_format_idc = 1;
+ }
+ READUE(log2_max_frame_num, 12);
+ aDest.log2_max_frame_num += 4;
+ READUE(pic_order_cnt_type, 2);
+ if (aDest.pic_order_cnt_type == 0) {
+ READUE(log2_max_pic_order_cnt_lsb, 12);
+ aDest.log2_max_pic_order_cnt_lsb += 4;
+ } else if (aDest.pic_order_cnt_type == 1) {
+ aDest.delta_pic_order_always_zero_flag = br.ReadBit();
+ READSE(offset_for_non_ref_pic, -231, 230);
+ READSE(offset_for_top_to_bottom_field, -231, 230);
+ uint32_t num_ref_frames_in_pic_order_cnt_cycle = br.ReadUE();
+ for (uint32_t i = 0; i < num_ref_frames_in_pic_order_cnt_cycle; i++) {
+ br.ReadSE(); // offset_for_ref_frame[i]
+ }
+ }
+ aDest.max_num_ref_frames = br.ReadUE();
+ aDest.gaps_in_frame_num_allowed_flag = br.ReadBit();
+ aDest.pic_width_in_mbs = br.ReadUE() + 1;
+ aDest.pic_height_in_map_units = br.ReadUE() + 1;
+ aDest.frame_mbs_only_flag = br.ReadBit();
+ if (!aDest.frame_mbs_only_flag) {
+ aDest.pic_height_in_map_units *= 2;
+ aDest.mb_adaptive_frame_field_flag = br.ReadBit();
+ }
+ aDest.direct_8x8_inference_flag = br.ReadBit();
+ aDest.frame_cropping_flag = br.ReadBit();
+ if (aDest.frame_cropping_flag) {
+ aDest.frame_crop_left_offset = br.ReadUE();
+ aDest.frame_crop_right_offset = br.ReadUE();
+ aDest.frame_crop_top_offset = br.ReadUE();
+ aDest.frame_crop_bottom_offset = br.ReadUE();
+ }
+
+ aDest.sample_ratio = 1.0f;
+ aDest.vui_parameters_present_flag = br.ReadBit();
+ if (aDest.vui_parameters_present_flag) {
+ if (!vui_parameters(br, aDest)) {
+ return false;
+ }
+ }
+
+ // Calculate common values.
+
+ uint8_t ChromaArrayType =
+ aDest.separate_colour_plane_flag ? 0 : aDest.chroma_format_idc;
+ // Calculate width.
+ uint32_t CropUnitX = 1;
+ uint32_t SubWidthC = aDest.chroma_format_idc == 3 ? 1 : 2;
+ if (ChromaArrayType != 0) {
+ CropUnitX = SubWidthC;
+ }
+
+ // Calculate Height
+ uint32_t CropUnitY = 2 - aDest.frame_mbs_only_flag;
+ uint32_t SubHeightC = aDest.chroma_format_idc <= 1 ? 2 : 1;
+ if (ChromaArrayType != 0) {
+ CropUnitY *= SubHeightC;
+ }
+
+ uint32_t width = aDest.pic_width_in_mbs * 16;
+ uint32_t height = aDest.pic_height_in_map_units * 16;
+ if (aDest.frame_crop_left_offset <=
+ std::numeric_limits<int32_t>::max() / 4 / CropUnitX &&
+ aDest.frame_crop_right_offset <=
+ std::numeric_limits<int32_t>::max() / 4 / CropUnitX &&
+ aDest.frame_crop_top_offset <=
+ std::numeric_limits<int32_t>::max() / 4 / CropUnitY &&
+ aDest.frame_crop_bottom_offset <=
+ std::numeric_limits<int32_t>::max() / 4 / CropUnitY &&
+ (aDest.frame_crop_left_offset + aDest.frame_crop_right_offset) *
+ CropUnitX <
+ width &&
+ (aDest.frame_crop_top_offset + aDest.frame_crop_bottom_offset) *
+ CropUnitY <
+ height) {
+ aDest.crop_left = aDest.frame_crop_left_offset * CropUnitX;
+ aDest.crop_right = aDest.frame_crop_right_offset * CropUnitX;
+ aDest.crop_top = aDest.frame_crop_top_offset * CropUnitY;
+ aDest.crop_bottom = aDest.frame_crop_bottom_offset * CropUnitY;
+ } else {
+ // Nonsensical value, ignore them.
+ aDest.crop_left = aDest.crop_right = aDest.crop_top = aDest.crop_bottom = 0;
+ }
+
+ aDest.pic_width = width - aDest.crop_left - aDest.crop_right;
+ aDest.pic_height = height - aDest.crop_top - aDest.crop_bottom;
+
+ aDest.interlaced = !aDest.frame_mbs_only_flag;
+
+ // Determine display size.
+ if (aDest.sample_ratio > 1.0) {
+ // Increase the intrinsic width
+ aDest.display_width =
+ ConditionDimension(aDest.pic_width * aDest.sample_ratio);
+ aDest.display_height = aDest.pic_height;
+ } else {
+ // Increase the intrinsic height
+ aDest.display_width = aDest.pic_width;
+ aDest.display_height =
+ ConditionDimension(aDest.pic_height / aDest.sample_ratio);
+ }
+
+ aDest.valid = true;
+
+ return true;
+}
+
+/* static */
+bool H264::vui_parameters(BitReader& aBr, SPSData& aDest) {
+ aDest.aspect_ratio_info_present_flag = aBr.ReadBit();
+ if (aDest.aspect_ratio_info_present_flag) {
+ aDest.aspect_ratio_idc = aBr.ReadBits(8);
+ aDest.sar_width = aDest.sar_height = 0;
+
+ // From E.2.1 VUI parameters semantics (ITU-T H.264 02/2014)
+ switch (aDest.aspect_ratio_idc) {
+ case 0:
+ // Unspecified
+ break;
+ case 1:
+ /*
+ 1:1
+ 7680x4320 16:9 frame without horizontal overscan
+ 3840x2160 16:9 frame without horizontal overscan
+ 1280x720 16:9 frame without horizontal overscan
+ 1920x1080 16:9 frame without horizontal overscan (cropped from
+ 1920x1088) 640x480 4:3 frame without horizontal overscan
+ */
+ aDest.sample_ratio = 1.0f;
+ break;
+ case 2:
+ /*
+ 12:11
+ 720x576 4:3 frame with horizontal overscan
+ 352x288 4:3 frame without horizontal overscan
+ */
+ aDest.sample_ratio = 12.0 / 11.0;
+ break;
+ case 3:
+ /*
+ 10:11
+ 720x480 4:3 frame with horizontal overscan
+ 352x240 4:3 frame without horizontal overscan
+ */
+ aDest.sample_ratio = 10.0 / 11.0;
+ break;
+ case 4:
+ /*
+ 16:11
+ 720x576 16:9 frame with horizontal overscan
+ 528x576 4:3 frame without horizontal overscan
+ */
+ aDest.sample_ratio = 16.0 / 11.0;
+ break;
+ case 5:
+ /*
+ 40:33
+ 720x480 16:9 frame with horizontal overscan
+ 528x480 4:3 frame without horizontal overscan
+ */
+ aDest.sample_ratio = 40.0 / 33.0;
+ break;
+ case 6:
+ /*
+ 24:11
+ 352x576 4:3 frame without horizontal overscan
+ 480x576 16:9 frame with horizontal overscan
+ */
+ aDest.sample_ratio = 24.0 / 11.0;
+ break;
+ case 7:
+ /*
+ 20:11
+ 352x480 4:3 frame without horizontal overscan
+ 480x480 16:9 frame with horizontal overscan
+ */
+ aDest.sample_ratio = 20.0 / 11.0;
+ break;
+ case 8:
+ /*
+ 32:11
+ 352x576 16:9 frame without horizontal overscan
+ */
+ aDest.sample_ratio = 32.0 / 11.0;
+ break;
+ case 9:
+ /*
+ 80:33
+ 352x480 16:9 frame without horizontal overscan
+ */
+ aDest.sample_ratio = 80.0 / 33.0;
+ break;
+ case 10:
+ /*
+ 18:11
+ 480x576 4:3 frame with horizontal overscan
+ */
+ aDest.sample_ratio = 18.0 / 11.0;
+ break;
+ case 11:
+ /*
+ 15:11
+ 480x480 4:3 frame with horizontal overscan
+ */
+ aDest.sample_ratio = 15.0 / 11.0;
+ break;
+ case 12:
+ /*
+ 64:33
+ 528x576 16:9 frame with horizontal overscan
+ */
+ aDest.sample_ratio = 64.0 / 33.0;
+ break;
+ case 13:
+ /*
+ 160:99
+ 528x480 16:9 frame without horizontal overscan
+ */
+ aDest.sample_ratio = 160.0 / 99.0;
+ break;
+ case 14:
+ /*
+ 4:3
+ 1440x1080 16:9 frame without horizontal overscan
+ */
+ aDest.sample_ratio = 4.0 / 3.0;
+ break;
+ case 15:
+ /*
+ 3:2
+ 1280x1080 16:9 frame without horizontal overscan
+ */
+ aDest.sample_ratio = 3.2 / 2.0;
+ break;
+ case 16:
+ /*
+ 2:1
+ 960x1080 16:9 frame without horizontal overscan
+ */
+ aDest.sample_ratio = 2.0 / 1.0;
+ break;
+ case 255:
+ /* Extended_SAR */
+ aDest.sar_width = aBr.ReadBits(16);
+ aDest.sar_height = aBr.ReadBits(16);
+ if (aDest.sar_width && aDest.sar_height) {
+ aDest.sample_ratio = float(aDest.sar_width) / float(aDest.sar_height);
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (aBr.ReadBit()) { // overscan_info_present_flag
+ aDest.overscan_appropriate_flag = aBr.ReadBit();
+ }
+
+ if (aBr.ReadBit()) { // video_signal_type_present_flag
+ aDest.video_format = aBr.ReadBits(3);
+ aDest.video_full_range_flag = aBr.ReadBit();
+ aDest.colour_description_present_flag = aBr.ReadBit();
+ if (aDest.colour_description_present_flag) {
+ aDest.colour_primaries = aBr.ReadBits(8);
+ aDest.transfer_characteristics = aBr.ReadBits(8);
+ aDest.matrix_coefficients = aBr.ReadBits(8);
+ }
+ }
+
+ aDest.chroma_loc_info_present_flag = aBr.ReadBit();
+ if (aDest.chroma_loc_info_present_flag) {
+ BitReader& br = aBr; // so that macro READUE works
+ READUE(chroma_sample_loc_type_top_field, 5);
+ READUE(chroma_sample_loc_type_bottom_field, 5);
+ }
+
+ bool timing_info_present_flag = aBr.ReadBit();
+ if (timing_info_present_flag) {
+ aBr.ReadBits(32); // num_units_in_tick
+ aBr.ReadBits(32); // time_scale
+ aBr.ReadBit(); // fixed_frame_rate_flag
+ }
+ return true;
+}
+
+/* static */
+bool H264::DecodeSPSFromExtraData(const mozilla::MediaByteBuffer* aExtraData,
+ SPSData& aDest) {
+ SPSNALIterator it(aExtraData);
+ if (!it) {
+ return false;
+ }
+ return (*it).GetSPSData(aDest);
+}
+
+/* static */
+bool H264::EnsureSPSIsSane(SPSData& aSPS) {
+ bool valid = true;
+ static const float default_aspect = 4.0f / 3.0f;
+ if (aSPS.sample_ratio <= 0.0f || aSPS.sample_ratio > 6.0f) {
+ if (aSPS.pic_width && aSPS.pic_height) {
+ aSPS.sample_ratio = (float)aSPS.pic_width / (float)aSPS.pic_height;
+ } else {
+ aSPS.sample_ratio = default_aspect;
+ }
+ aSPS.display_width = aSPS.pic_width;
+ aSPS.display_height = aSPS.pic_height;
+ valid = false;
+ }
+ if (aSPS.max_num_ref_frames > 16) {
+ aSPS.max_num_ref_frames = 16;
+ valid = false;
+ }
+ return valid;
+}
+
+/* static */
+uint32_t H264::ComputeMaxRefFrames(const mozilla::MediaByteBuffer* aExtraData) {
+ uint32_t maxRefFrames = 4;
+ // Retrieve video dimensions from H264 SPS NAL.
+ SPSData spsdata;
+ if (DecodeSPSFromExtraData(aExtraData, spsdata)) {
+ // max_num_ref_frames determines the size of the sliding window
+ // we need to queue that many frames in order to guarantee proper
+ // pts frames ordering. Use a minimum of 4 to ensure proper playback of
+ // non compliant videos.
+ maxRefFrames =
+ std::min(std::max(maxRefFrames, spsdata.max_num_ref_frames + 1), 16u);
+ }
+ return maxRefFrames;
+}
+
+/* static */ H264::FrameType H264::GetFrameType(
+ const mozilla::MediaRawData* aSample) {
+ if (!AnnexB::IsAVCC(aSample)) {
+ // We must have a valid AVCC frame with extradata.
+ return FrameType::INVALID;
+ }
+ MOZ_ASSERT(aSample->Data());
+
+ int nalLenSize = ((*aSample->mExtraData)[4] & 3) + 1;
+
+ BufferReader reader(aSample->Data(), aSample->Size());
+
+ while (reader.Remaining() >= nalLenSize) {
+ uint32_t nalLen = 0;
+ switch (nalLenSize) {
+ case 1:
+ nalLen = reader.ReadU8().unwrapOr(0);
+ break;
+ case 2:
+ nalLen = reader.ReadU16().unwrapOr(0);
+ break;
+ case 3:
+ nalLen = reader.ReadU24().unwrapOr(0);
+ break;
+ case 4:
+ nalLen = reader.ReadU32().unwrapOr(0);
+ break;
+ }
+ if (!nalLen) {
+ continue;
+ }
+ const uint8_t* p = reader.Read(nalLen);
+ if (!p) {
+ return FrameType::INVALID;
+ }
+ int8_t nalType = *p & 0x1f;
+ if (nalType == H264_NAL_IDR_SLICE) {
+ // IDR NAL.
+ return FrameType::I_FRAME;
+ } else if (nalType == H264_NAL_SEI) {
+ RefPtr<mozilla::MediaByteBuffer> decodedNAL = DecodeNALUnit(p, nalLen);
+ SEIRecoveryData data;
+ if (DecodeRecoverySEI(decodedNAL, data)) {
+ return FrameType::I_FRAME;
+ }
+ } else if (nalType == H264_NAL_SLICE) {
+ RefPtr<mozilla::MediaByteBuffer> decodedNAL = DecodeNALUnit(p, nalLen);
+ if (DecodeISlice(decodedNAL)) {
+ return FrameType::I_FRAME;
+ }
+ }
+ }
+
+ return FrameType::OTHER;
+}
+
+/* static */ already_AddRefed<mozilla::MediaByteBuffer> H264::ExtractExtraData(
+ const mozilla::MediaRawData* aSample) {
+ MOZ_ASSERT(AnnexB::IsAVCC(aSample));
+
+ RefPtr<mozilla::MediaByteBuffer> extradata = new mozilla::MediaByteBuffer;
+
+ // SPS content
+ nsTArray<uint8_t> sps;
+ ByteWriter<BigEndian> spsw(sps);
+ int numSps = 0;
+ // PPS content
+ nsTArray<uint8_t> pps;
+ ByteWriter<BigEndian> ppsw(pps);
+ int numPps = 0;
+
+ int nalLenSize = ((*aSample->mExtraData)[4] & 3) + 1;
+
+ size_t sampleSize = aSample->Size();
+ if (aSample->mCrypto.IsEncrypted()) {
+ // The content is encrypted, we can only parse the non-encrypted data.
+ MOZ_ASSERT(aSample->mCrypto.mPlainSizes.Length() > 0);
+ if (aSample->mCrypto.mPlainSizes.Length() == 0 ||
+ aSample->mCrypto.mPlainSizes[0] > sampleSize) {
+ // This is invalid content.
+ return nullptr;
+ }
+ sampleSize = aSample->mCrypto.mPlainSizes[0];
+ }
+
+ BufferReader reader(aSample->Data(), sampleSize);
+
+ nsTArray<SPSData> SPSTable;
+ // If we encounter SPS with the same id but different content, we will stop
+ // attempting to detect duplicates.
+ bool checkDuplicate = true;
+
+ // Find SPS and PPS NALUs in AVCC data
+ while (reader.Remaining() > nalLenSize) {
+ uint32_t nalLen = 0;
+ switch (nalLenSize) {
+ case 1:
+ Unused << reader.ReadU8().map(
+ [&](uint8_t x) mutable { return nalLen = x; });
+ break;
+ case 2:
+ Unused << reader.ReadU16().map(
+ [&](uint16_t x) mutable { return nalLen = x; });
+ break;
+ case 3:
+ Unused << reader.ReadU24().map(
+ [&](uint32_t x) mutable { return nalLen = x; });
+ break;
+ case 4:
+ Unused << reader.ReadU32().map(
+ [&](uint32_t x) mutable { return nalLen = x; });
+ break;
+ }
+ const uint8_t* p = reader.Read(nalLen);
+ if (!p) {
+ // The read failed, but we may already have some SPS + PPS data so
+ // break out of reading and process what we have, if any.
+ break;
+ }
+ uint8_t nalType = *p & 0x1f;
+
+ if (nalType == H264_NAL_SPS) {
+ RefPtr<mozilla::MediaByteBuffer> sps = DecodeNALUnit(p, nalLen);
+ SPSData data;
+ if (!DecodeSPS(sps, data)) {
+ // Invalid SPS, ignore.
+ continue;
+ }
+ uint8_t spsId = data.seq_parameter_set_id;
+ if (spsId >= SPSTable.Length()) {
+ if (!SPSTable.SetLength(spsId + 1, fallible)) {
+ // OOM.
+ return nullptr;
+ }
+ }
+ if (checkDuplicate && SPSTable[spsId].valid && SPSTable[spsId] == data) {
+ // Duplicate ignore.
+ continue;
+ }
+ if (SPSTable[spsId].valid) {
+ // We already have detected a SPS with this Id. Just to be safe we
+ // disable SPS duplicate detection.
+ checkDuplicate = false;
+ } else {
+ SPSTable[spsId] = data;
+ }
+ numSps++;
+ if (!spsw.WriteU16(nalLen) || !spsw.Write(p, nalLen)) {
+ return extradata.forget();
+ }
+ } else if (nalType == H264_NAL_PPS) {
+ numPps++;
+ if (!ppsw.WriteU16(nalLen) || !ppsw.Write(p, nalLen)) {
+ return extradata.forget();
+ }
+ }
+ }
+
+ // We ignore PPS data if we didn't find a SPS as we would be unable to
+ // decode it anyway.
+ numPps = numSps ? numPps : 0;
+
+ if (numSps && sps.Length() > 5) {
+ extradata->AppendElement(1); // version
+ extradata->AppendElement(sps[3]); // profile
+ extradata->AppendElement(sps[4]); // profile compat
+ extradata->AppendElement(sps[5]); // level
+ extradata->AppendElement(0xfc | 3); // nal size - 1
+ extradata->AppendElement(0xe0 | numSps);
+ extradata->AppendElements(sps.Elements(), sps.Length());
+ extradata->AppendElement(numPps);
+ if (numPps) {
+ extradata->AppendElements(pps.Elements(), pps.Length());
+ }
+ }
+
+ return extradata.forget();
+}
+
+/* static */
+bool H264::HasSPS(const mozilla::MediaByteBuffer* aExtraData) {
+ return NumSPS(aExtraData) > 0;
+}
+
+/* static */
+uint8_t H264::NumSPS(const mozilla::MediaByteBuffer* aExtraData) {
+ if (!aExtraData || aExtraData->IsEmpty()) {
+ return 0;
+ }
+
+ BufferReader reader(aExtraData);
+ if (!reader.Read(5)) {
+ return 0;
+ }
+ auto res = reader.ReadU8();
+ if (res.isErr()) {
+ return 0;
+ }
+ return res.unwrap() & 0x1f;
+}
+
+/* static */
+bool H264::CompareExtraData(const mozilla::MediaByteBuffer* aExtraData1,
+ const mozilla::MediaByteBuffer* aExtraData2) {
+ if (aExtraData1 == aExtraData2) {
+ return true;
+ }
+ uint8_t numSPS = NumSPS(aExtraData1);
+ if (numSPS == 0 || numSPS != NumSPS(aExtraData2)) {
+ return false;
+ }
+
+ // We only compare if the SPS are the same as the various H264 decoders can
+ // deal with in-band change of PPS.
+
+ SPSNALIterator it1(aExtraData1);
+ SPSNALIterator it2(aExtraData2);
+
+ while (it1 && it2) {
+ if (*it1 != *it2) {
+ return false;
+ }
+ ++it1;
+ ++it2;
+ }
+ return true;
+}
+
+static inline Result<Ok, nsresult> ReadSEIInt(BufferReader& aBr,
+ uint32_t& aOutput) {
+ uint8_t tmpByte;
+
+ aOutput = 0;
+ MOZ_TRY_VAR(tmpByte, aBr.ReadU8());
+ while (tmpByte == 0xFF) {
+ aOutput += 255;
+ MOZ_TRY_VAR(tmpByte, aBr.ReadU8());
+ }
+ aOutput += tmpByte; // this is the last byte
+ return Ok();
+}
+
+/* static */
+bool H264::DecodeISlice(const mozilla::MediaByteBuffer* aSlice) {
+ if (!aSlice) {
+ return false;
+ }
+
+ // According to ITU-T Rec H.264 Table 7.3.3, read the slice type from
+ // slice_header, and the slice type 2 and 7 are representing I slice.
+ BitReader br(aSlice);
+ // Skip `first_mb_in_slice`
+ br.ReadUE();
+ // The value of slice type can go from 0 to 9, but the value between 5 to
+ // 9 are actually equal to 0 to 4.
+ const uint32_t sliceType = br.ReadUE() % 5;
+ return sliceType == SLICE_TYPES::I_SLICE || sliceType == SI_SLICE;
+}
+
+/* static */
+bool H264::DecodeRecoverySEI(const mozilla::MediaByteBuffer* aSEI,
+ SEIRecoveryData& aDest) {
+ if (!aSEI) {
+ return false;
+ }
+ // sei_rbsp() as per 7.3.2.3 Supplemental enhancement information RBSP syntax
+ BufferReader br(aSEI);
+
+ do {
+ // sei_message() as per
+ // 7.3.2.3.1 Supplemental enhancement information message syntax
+ uint32_t payloadType = 0;
+ if (ReadSEIInt(br, payloadType).isErr()) {
+ return false;
+ }
+
+ uint32_t payloadSize = 0;
+ if (ReadSEIInt(br, payloadSize).isErr()) {
+ return false;
+ }
+
+ // sei_payload(payloadType, payloadSize) as per
+ // D.1 SEI payload syntax.
+ const uint8_t* p = br.Read(payloadSize);
+ if (!p) {
+ return false;
+ }
+ if (payloadType == 6) { // SEI_RECOVERY_POINT
+ if (payloadSize == 0) {
+ // Invalid content, ignore.
+ continue;
+ }
+ // D.1.7 Recovery point SEI message syntax
+ BitReader br(p, payloadSize * 8);
+ aDest.recovery_frame_cnt = br.ReadUE();
+ aDest.exact_match_flag = br.ReadBit();
+ aDest.broken_link_flag = br.ReadBit();
+ aDest.changing_slice_group_idc = br.ReadBits(2);
+ return true;
+ }
+ } while (br.PeekU8().isOk() &&
+ br.PeekU8().unwrap() !=
+ 0x80); // more_rbsp_data() msg[offset] != 0x80
+ // ignore the trailing bits rbsp_trailing_bits();
+ return false;
+}
+
+/*static */ already_AddRefed<mozilla::MediaByteBuffer> H264::CreateExtraData(
+ uint8_t aProfile, uint8_t aConstraints, uint8_t aLevel,
+ const gfx::IntSize& aSize) {
+ // SPS of a 144p video.
+ const uint8_t originSPS[] = {0x4d, 0x40, 0x0c, 0xe8, 0x80, 0x80, 0x9d,
+ 0x80, 0xb5, 0x01, 0x01, 0x01, 0x40, 0x00,
+ 0x00, 0x00, 0x40, 0x00, 0x00, 0x0f, 0x03,
+ 0xc5, 0x0a, 0x44, 0x80};
+
+ RefPtr<MediaByteBuffer> extraData = new MediaByteBuffer();
+ extraData->AppendElements(originSPS, sizeof(originSPS));
+ BitReader br(extraData, BitReader::GetBitLength(extraData));
+
+ RefPtr<MediaByteBuffer> sps = new MediaByteBuffer();
+ BitWriter bw(sps);
+
+ br.ReadBits(8); // Skip original profile_idc
+ bw.WriteU8(aProfile);
+ br.ReadBits(8); // Skip original constraint flags && reserved_zero_2bits
+ aConstraints =
+ aConstraints & ~0x3; // Ensure reserved_zero_2bits are set to 0
+ bw.WriteBits(aConstraints, 8);
+ br.ReadBits(8); // Skip original level_idc
+ bw.WriteU8(aLevel);
+ bw.WriteUE(br.ReadUE()); // seq_parameter_set_id (0 stored on 1 bit)
+
+ if (aProfile == 100 || aProfile == 110 || aProfile == 122 ||
+ aProfile == 244 || aProfile == 44 || aProfile == 83 || aProfile == 86 ||
+ aProfile == 118 || aProfile == 128 || aProfile == 138 ||
+ aProfile == 139 || aProfile == 134) {
+ bw.WriteUE(1); // chroma_format_idc -> always set to 4:2:0 chroma format
+ bw.WriteUE(0); // bit_depth_luma_minus8 -> always 8 bits here
+ bw.WriteUE(0); // bit_depth_chroma_minus8 -> always 8 bits here
+ bw.WriteBit(false); // qpprime_y_zero_transform_bypass_flag
+ bw.WriteBit(false); // seq_scaling_matrix_present_flag
+ }
+
+ bw.WriteBits(br.ReadBits(11),
+ 11); // log2_max_frame_num to gaps_in_frame_num_allowed_flag
+
+ // skip over original exp-golomb encoded width/height
+ br.ReadUE(); // skip width
+ br.ReadUE(); // skip height
+ uint32_t width = aSize.width;
+ uint32_t widthNeeded = width % 16 != 0 ? (width / 16 + 1) * 16 : width;
+ uint32_t height = aSize.height;
+ uint32_t heightNeeded = height % 16 != 0 ? (height / 16 + 1) * 16 : height;
+ bw.WriteUE(widthNeeded / 16 - 1);
+ bw.WriteUE(heightNeeded / 16 - 1);
+ bw.WriteBit(br.ReadBit()); // write frame_mbs_only_flag
+ bw.WriteBit(br.ReadBit()); // write direct_8x8_inference_flag;
+ if (widthNeeded != width || heightNeeded != height) {
+ // Write cropping value
+ bw.WriteBit(true); // skip frame_cropping_flag
+ bw.WriteUE(0); // frame_crop_left_offset
+ bw.WriteUE((widthNeeded - width) / 2); // frame_crop_right_offset
+ bw.WriteUE(0); // frame_crop_top_offset
+ bw.WriteUE((heightNeeded - height) / 2); // frame_crop_bottom_offset
+ } else {
+ bw.WriteBit(false); // skip frame_cropping_flag
+ }
+ br.ReadBit(); // skip frame_cropping_flag;
+ // Write the remainings of the original sps (vui_parameters which sets an
+ // aspect ration of 1.0)
+ while (br.BitsLeft()) {
+ bw.WriteBit(br.ReadBit());
+ }
+ bw.CloseWithRbspTrailing();
+
+ RefPtr<MediaByteBuffer> encodedSPS =
+ EncodeNALUnit(sps->Elements(), sps->Length());
+ extraData->Clear();
+
+ const uint8_t PPS[] = {0xeb, 0xef, 0x20};
+
+ WriteExtraData(
+ extraData, aProfile, aConstraints, aLevel,
+ Span<const uint8_t>(encodedSPS->Elements(), encodedSPS->Length()),
+ Span<const uint8_t>(PPS, sizeof(PPS)));
+
+ return extraData.forget();
+}
+
+void H264::WriteExtraData(MediaByteBuffer* aDestExtraData,
+ const uint8_t aProfile, const uint8_t aConstraints,
+ const uint8_t aLevel, const Span<const uint8_t> aSPS,
+ const Span<const uint8_t> aPPS) {
+ aDestExtraData->AppendElement(1);
+ aDestExtraData->AppendElement(aProfile);
+ aDestExtraData->AppendElement(aConstraints);
+ aDestExtraData->AppendElement(aLevel);
+ aDestExtraData->AppendElement(3); // nalLENSize-1
+ aDestExtraData->AppendElement(1); // numPPS
+ uint8_t c[2];
+ mozilla::BigEndian::writeUint16(&c[0], aSPS.Length() + 1);
+ aDestExtraData->AppendElements(c, 2);
+ aDestExtraData->AppendElement((0x00 << 7) | (0x3 << 5) | H264_NAL_SPS);
+ aDestExtraData->AppendElements(aSPS.Elements(), aSPS.Length());
+
+ aDestExtraData->AppendElement(1); // numPPS
+ mozilla::BigEndian::writeUint16(&c[0], aPPS.Length() + 1);
+ aDestExtraData->AppendElements(c, 2);
+ aDestExtraData->AppendElement((0x00 << 7) | (0x3 << 5) | H264_NAL_PPS);
+ aDestExtraData->AppendElements(aPPS.Elements(), aPPS.Length());
+}
+
+#undef READUE
+#undef READSE
+
+} // namespace mozilla
diff --git a/dom/media/platforms/agnostic/bytestreams/H264.h b/dom/media/platforms/agnostic/bytestreams/H264.h
new file mode 100644
index 0000000000..37c0e5bc14
--- /dev/null
+++ b/dom/media/platforms/agnostic/bytestreams/H264.h
@@ -0,0 +1,525 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MP4_DEMUXER_H264_H_
+#define MP4_DEMUXER_H264_H_
+
+#include "DecoderData.h"
+#include "mozilla/gfx/Types.h"
+
+namespace mozilla {
+class BitReader;
+
+// Spec 7.4.2.1
+#define MAX_SPS_COUNT 32
+#define MAX_PPS_COUNT 256
+
+// NAL unit types
+enum NAL_TYPES {
+ H264_NAL_SLICE = 1,
+ H264_NAL_DPA = 2,
+ H264_NAL_DPB = 3,
+ H264_NAL_DPC = 4,
+ H264_NAL_IDR_SLICE = 5,
+ H264_NAL_SEI = 6,
+ H264_NAL_SPS = 7,
+ H264_NAL_PPS = 8,
+ H264_NAL_AUD = 9,
+ H264_NAL_END_SEQUENCE = 10,
+ H264_NAL_END_STREAM = 11,
+ H264_NAL_FILLER_DATA = 12,
+ H264_NAL_SPS_EXT = 13,
+ H264_NAL_PREFIX = 14,
+ H264_NAL_AUXILIARY_SLICE = 19,
+ H264_NAL_SLICE_EXT = 20,
+ H264_NAL_SLICE_EXT_DVC = 21,
+};
+
+// According to ITU-T Rec H.264 (2017/04) Table 7.6.
+enum SLICE_TYPES {
+ P_SLICE = 0,
+ B_SLICE = 1,
+ I_SLICE = 2,
+ SP_SLICE = 3,
+ SI_SLICE = 4,
+};
+
+struct SPSData {
+ bool operator==(const SPSData& aOther) const;
+ bool operator!=(const SPSData& aOther) const;
+
+ gfx::YUVColorSpace ColorSpace() const;
+ gfx::ColorDepth ColorDepth() const;
+
+ bool valid;
+
+ /* Decoded Members */
+ /*
+ pic_width is the decoded width according to:
+ pic_width = ((pic_width_in_mbs_minus1 + 1) * 16)
+ - (frame_crop_left_offset + frame_crop_right_offset) * 2
+ */
+ uint32_t pic_width;
+ /*
+ pic_height is the decoded height according to:
+ pic_height = (2 - frame_mbs_only_flag) * ((pic_height_in_map_units_minus1 +
+ 1) * 16)
+ - (frame_crop_top_offset + frame_crop_bottom_offset) * 2
+ */
+ uint32_t pic_height;
+
+ bool interlaced;
+
+ /*
+ Displayed size.
+ display_width and display_height are adjusted according to the display
+ sample aspect ratio.
+ */
+ uint32_t display_width;
+ uint32_t display_height;
+
+ float sample_ratio;
+
+ uint32_t crop_left;
+ uint32_t crop_right;
+ uint32_t crop_top;
+ uint32_t crop_bottom;
+
+ /*
+ H264 decoding parameters according to ITU-T H.264 (T-REC-H.264-201402-I/en)
+ http://www.itu.int/rec/T-REC-H.264-201402-I/en
+ */
+
+ bool constraint_set0_flag;
+ bool constraint_set1_flag;
+ bool constraint_set2_flag;
+ bool constraint_set3_flag;
+ bool constraint_set4_flag;
+ bool constraint_set5_flag;
+
+ /*
+ profile_idc and level_idc indicate the profile and level to which the coded
+ video sequence conforms when the SVC sequence parameter set is the active
+ SVC sequence parameter set.
+ */
+ uint8_t profile_idc;
+ uint8_t level_idc;
+
+ /*
+ seq_parameter_set_id identifies the sequence parameter set that is referred
+ to by the picture parameter set. The value of seq_parameter_set_id shall be
+ in the range of 0 to 31, inclusive.
+ */
+ uint8_t seq_parameter_set_id;
+
+ /*
+ chroma_format_idc specifies the chroma sampling relative to the luma
+ sampling as specified in clause 6.2. The value of chroma_format_idc shall be
+ in the range of 0 to 3, inclusive. When chroma_format_idc is not present,
+ it shall be inferred to be equal to 1 (4:2:0 chroma format).
+ When profile_idc is equal to 183, chroma_format_idc shall be equal to 0
+ (4:0:0 chroma format).
+ */
+ uint8_t chroma_format_idc;
+
+ /*
+ bit_depth_luma_minus8 specifies the bit depth of the samples of the luma
+ array and the value of the luma quantisation parameter range offset
+ QpBdOffset Y , as specified by
+ BitDepth Y = 8 + bit_depth_luma_minus8 (7-3)
+ QpBdOffset Y = 6 * bit_depth_luma_minus8 (7-4)
+ When bit_depth_luma_minus8 is not present, it shall be inferred to be equal
+ to 0. bit_depth_luma_minus8 shall be in the range of 0 to 6, inclusive.
+ */
+ uint8_t bit_depth_luma_minus8;
+
+ /*
+ bit_depth_chroma_minus8 specifies the bit depth of the samples of the chroma
+ arrays and the value of the chroma quantisation parameter range offset
+ QpBdOffset C , as specified by
+ BitDepth C = 8 + bit_depth_chroma_minus8 (7-5)
+ QpBdOffset C = 6 * bit_depth_chroma_minus8 (7-6)
+ When bit_depth_chroma_minus8 is not present, it shall be inferred to be
+ equal to 0. bit_depth_chroma_minus8 shall be in the range of 0 to 6,
+ inclusive.
+ */
+ uint8_t bit_depth_chroma_minus8;
+
+ /*
+ separate_colour_plane_flag equal to 1 specifies that the three colour
+ components of the 4:4:4 chroma format are coded separately.
+ separate_colour_plane_flag equal to 0 specifies that the colour components
+ are not coded separately. When separate_colour_plane_flag is not present,
+ it shall be inferred to be equal to 0. When separate_colour_plane_flag is
+ equal to 1, the primary coded picture consists of three separate components,
+ each of which consists of coded samples of one colour plane (Y, Cb or Cr)
+ that each use the monochrome coding syntax. In this case, each colour plane
+ is associated with a specific colour_plane_id value.
+ */
+ bool separate_colour_plane_flag;
+
+ /*
+ seq_scaling_matrix_present_flag equal to 1 specifies that the flags
+ seq_scaling_list_present_flag[ i ] for i = 0..7 or
+ i = 0..11 are present. seq_scaling_matrix_present_flag equal to 0 specifies
+ that these flags are not present and the sequence-level scaling list
+ specified by Flat_4x4_16 shall be inferred for i = 0..5 and the
+ sequence-level scaling list specified by Flat_8x8_16 shall be inferred for
+ i = 6..11. When seq_scaling_matrix_present_flag is not present, it shall be
+ inferred to be equal to 0.
+ */
+ bool seq_scaling_matrix_present_flag;
+
+ /*
+ log2_max_frame_num_minus4 specifies the value of the variable
+ MaxFrameNum that is used in frame_num related derivations as
+ follows:
+
+ MaxFrameNum = 2( log2_max_frame_num_minus4 + 4 ). The value of
+ log2_max_frame_num_minus4 shall be in the range of 0 to 12, inclusive.
+ */
+ uint8_t log2_max_frame_num;
+
+ /*
+ pic_order_cnt_type specifies the method to decode picture order
+ count (as specified in subclause 8.2.1). The value of
+ pic_order_cnt_type shall be in the range of 0 to 2, inclusive.
+ */
+ uint8_t pic_order_cnt_type;
+
+ /*
+ log2_max_pic_order_cnt_lsb_minus4 specifies the value of the
+ variable MaxPicOrderCntLsb that is used in the decoding
+ process for picture order count as specified in subclause
+ 8.2.1 as follows:
+
+ MaxPicOrderCntLsb = 2( log2_max_pic_order_cnt_lsb_minus4 + 4 )
+
+ The value of log2_max_pic_order_cnt_lsb_minus4 shall be in
+ the range of 0 to 12, inclusive.
+ */
+ uint8_t log2_max_pic_order_cnt_lsb;
+
+ /*
+ delta_pic_order_always_zero_flag equal to 1 specifies that
+ delta_pic_order_cnt[ 0 ] and delta_pic_order_cnt[ 1 ] are
+ not present in the slice headers of the sequence and shall
+ be inferred to be equal to 0.
+ */
+ bool delta_pic_order_always_zero_flag;
+
+ /*
+ offset_for_non_ref_pic is used to calculate the picture
+ order count of a non-reference picture as specified in
+ 8.2.1. The value of offset_for_non_ref_pic shall be in the
+ range of -231 to 231 - 1, inclusive.
+ */
+ int8_t offset_for_non_ref_pic;
+
+ /*
+ offset_for_top_to_bottom_field is used to calculate the
+ picture order count of a bottom field as specified in
+ subclause 8.2.1. The value of offset_for_top_to_bottom_field
+ shall be in the range of -231 to 231 - 1, inclusive.
+ */
+ int8_t offset_for_top_to_bottom_field;
+
+ /*
+ max_num_ref_frames specifies the maximum number of short-term and
+ long-term reference frames, complementary reference field pairs,
+ and non-paired reference fields that may be used by the decoding
+ process for inter prediction of any picture in the
+ sequence. max_num_ref_frames also determines the size of the sliding
+ window operation as specified in subclause 8.2.5.3. The value of
+ max_num_ref_frames shall be in the range of 0 to MaxDpbFrames (as
+ specified in subclause A.3.1 or A.3.2), inclusive.
+ */
+ uint32_t max_num_ref_frames;
+
+ /*
+ gaps_in_frame_num_value_allowed_flag specifies the allowed
+ values of frame_num as specified in subclause 7.4.3 and the
+ decoding process in case of an inferred gap between values of
+ frame_num as specified in subclause 8.2.5.2.
+ */
+ bool gaps_in_frame_num_allowed_flag;
+
+ /*
+ pic_width_in_mbs_minus1 plus 1 specifies the width of each
+ decoded picture in units of macroblocks. 16 macroblocks in a row
+ */
+ uint32_t pic_width_in_mbs;
+
+ /*
+ pic_height_in_map_units_minus1 plus 1 specifies the height in
+ slice group map units of a decoded frame or field. 16
+ macroblocks in each column.
+ */
+ uint32_t pic_height_in_map_units;
+
+ /*
+ frame_mbs_only_flag equal to 0 specifies that coded pictures of
+ the coded video sequence may either be coded fields or coded
+ frames. frame_mbs_only_flag equal to 1 specifies that every
+ coded picture of the coded video sequence is a coded frame
+ containing only frame macroblocks.
+ */
+ bool frame_mbs_only_flag;
+
+ /*
+ mb_adaptive_frame_field_flag equal to 0 specifies no
+ switching between frame and field macroblocks within a
+ picture. mb_adaptive_frame_field_flag equal to 1 specifies
+ the possible use of switching between frame and field
+ macroblocks within frames. When mb_adaptive_frame_field_flag
+ is not present, it shall be inferred to be equal to 0.
+ */
+ bool mb_adaptive_frame_field_flag;
+
+ /*
+ direct_8x8_inference_flag specifies the method used in the derivation
+ process for luma motion vectors for B_Skip, B_Direct_16x16 and B_Direct_8x8
+ as specified in clause 8.4.1.2. When frame_mbs_only_flag is equal to 0,
+ direct_8x8_inference_flag shall be equal to 1.
+ */
+ bool direct_8x8_inference_flag;
+
+ /*
+ frame_cropping_flag equal to 1 specifies that the frame cropping
+ offset parameters follow next in the sequence parameter
+ set. frame_cropping_flag equal to 0 specifies that the frame
+ cropping offset parameters are not present.
+ */
+ bool frame_cropping_flag;
+ uint32_t frame_crop_left_offset;
+ uint32_t frame_crop_right_offset;
+ uint32_t frame_crop_top_offset;
+ uint32_t frame_crop_bottom_offset;
+
+ // VUI Parameters
+
+ /*
+ vui_parameters_present_flag equal to 1 specifies that the
+ vui_parameters( ) syntax structure as specified in Annex E is
+ present. vui_parameters_present_flag equal to 0 specifies that
+ the vui_parameters( ) syntax structure as specified in Annex E
+ is not present.
+ */
+ bool vui_parameters_present_flag;
+
+ /*
+ aspect_ratio_info_present_flag equal to 1 specifies that
+ aspect_ratio_idc is present. aspect_ratio_info_present_flag
+ equal to 0 specifies that aspect_ratio_idc is not present.
+ */
+ bool aspect_ratio_info_present_flag;
+
+ /*
+ aspect_ratio_idc specifies the value of the sample aspect
+ ratio of the luma samples. Table E-1 shows the meaning of
+ the code. When aspect_ratio_idc indicates Extended_SAR, the
+ sample aspect ratio is represented by sar_width and
+ sar_height. When the aspect_ratio_idc syntax element is not
+ present, aspect_ratio_idc value shall be inferred to be
+ equal to 0.
+ */
+ uint8_t aspect_ratio_idc;
+ uint32_t sar_width;
+ uint32_t sar_height;
+
+ /*
+ video_signal_type_present_flag equal to 1 specifies that video_format,
+ video_full_range_flag and colour_description_present_flag are present.
+ video_signal_type_present_flag equal to 0, specify that video_format,
+ video_full_range_flag and colour_description_present_flag are not present.
+ */
+ bool video_signal_type_present_flag;
+
+ /*
+ overscan_info_present_flag equal to1 specifies that the
+ overscan_appropriate_flag is present. When overscan_info_present_flag is
+ equal to 0 or is not present, the preferred display method for the video
+ signal is unspecified (Unspecified).
+ */
+ bool overscan_info_present_flag;
+ /*
+ overscan_appropriate_flag equal to 1 indicates that the cropped decoded
+ pictures output are suitable for display using overscan.
+ overscan_appropriate_flag equal to 0 indicates that the cropped decoded
+ pictures output contain visually important information in the entire region
+ out to the edges of the cropping rectangle of the picture
+ */
+ bool overscan_appropriate_flag;
+
+ /*
+ video_format indicates the representation of the pictures as specified in
+ Table E-2, before being coded in accordance with this
+ Recommendation | International Standard. When the video_format syntax
+ element is not present, video_format value shall be inferred to be equal
+ to 5. (Unspecified video format)
+ */
+ uint8_t video_format;
+
+ /*
+ video_full_range_flag indicates the black level and range of the luma and
+ chroma signals as derived from E′Y, E′PB, and E′PR or E′R, E′G, and E′B
+ real-valued component signals.
+ When the video_full_range_flag syntax element is not present, the value of
+ video_full_range_flag shall be inferred to be equal to 0.
+ */
+ bool video_full_range_flag;
+
+ /*
+ colour_description_present_flag equal to1 specifies that colour_primaries,
+ transfer_characteristics and matrix_coefficients are present.
+ colour_description_present_flag equal to 0 specifies that colour_primaries,
+ transfer_characteristics and matrix_coefficients are not present.
+ */
+ bool colour_description_present_flag;
+
+ /*
+ colour_primaries indicates the chromaticity coordinates of the source
+ primaries as specified in Table E-3 in terms of the CIE 1931 definition of
+ x and y as specified by ISO 11664-1.
+ When the colour_primaries syntax element is not present, the value of
+ colour_primaries shall be inferred to be equal to 2 (the chromaticity is
+ unspecified or is determined by the application).
+ */
+ uint8_t colour_primaries;
+
+ /*
+ transfer_characteristics indicates the opto-electronic transfer
+ characteristic of the source picture as specified in Table E-4 as a function
+ of a linear optical intensity input Lc with a nominal real-valued range of 0
+ to 1.
+ When the transfer_characteristics syntax element is not present, the value
+ of transfer_characteristics shall be inferred to be equal to 2
+ (the transfer characteristics are unspecified or are determined by the
+ application).
+ */
+ uint8_t transfer_characteristics;
+
+ uint8_t matrix_coefficients;
+ bool chroma_loc_info_present_flag;
+ /*
+ The value of chroma_sample_loc_type_top_field and
+ chroma_sample_loc_type_bottom_field shall be in the range of 0 to 5,
+ inclusive
+ */
+ uint8_t chroma_sample_loc_type_top_field;
+ uint8_t chroma_sample_loc_type_bottom_field;
+
+ bool scaling_matrix_present;
+ uint8_t scaling_matrix4x4[6][16];
+ uint8_t scaling_matrix8x8[6][64];
+
+ SPSData();
+};
+
+struct SEIRecoveryData {
+ /*
+ recovery_frame_cnt specifies the recovery point of output pictures in output
+ order. All decoded pictures in output order are indicated to be correct or
+ approximately correct in content starting at the output order position of
+ the reference picture having the frame_num equal to the frame_num of the VCL
+ NAL units for the current access unit incremented by recovery_frame_cnt in
+ modulo MaxFrameNum arithmetic. recovery_frame_cnt shall be in the range of 0
+ to MaxFrameNum − 1, inclusive.
+ */
+ uint32_t recovery_frame_cnt = 0;
+ /*
+ exact_match_flag indicates whether decoded pictures at and subsequent to the
+ specified recovery point in output order derived by starting the decoding
+ process at the access unit associated with the recovery point SEI message
+ shall be an exact match to the pictures that would be produced by starting
+ the decoding process at the location of a previous IDR access unit in the
+ NAL unit stream. The value 0 indicates that the match need not be exact and
+ the value 1 indicates that the match shall be exact.
+ */
+ bool exact_match_flag = false;
+ /*
+ broken_link_flag indicates the presence or absence of a broken link in the
+ NAL unit stream at the location of the recovery point SEI message */
+ bool broken_link_flag = false;
+ /*
+ changing_slice_group_idc equal to 0 indicates that decoded pictures are
+ correct or approximately correct in content at and subsequent to the
+ recovery point in output order when all macroblocks of the primary coded
+ pictures are decoded within the changing slice group period
+ */
+ uint8_t changing_slice_group_idc = 0;
+};
+
+class H264 {
+ public:
+ /* Check if out of band extradata contains a SPS NAL */
+ static bool HasSPS(const mozilla::MediaByteBuffer* aExtraData);
+ // Extract SPS and PPS NALs from aSample by looking into each NALs.
+ // aSample must be in AVCC format.
+ static already_AddRefed<mozilla::MediaByteBuffer> ExtractExtraData(
+ const mozilla::MediaRawData* aSample);
+ // Return true if both extradata are equal.
+ static bool CompareExtraData(const mozilla::MediaByteBuffer* aExtraData1,
+ const mozilla::MediaByteBuffer* aExtraData2);
+
+ // Ensure that SPS data makes sense, Return true if SPS data was, and false
+ // otherwise. If false, then content will be adjusted accordingly.
+ static bool EnsureSPSIsSane(SPSData& aSPS);
+
+ static bool DecodeSPSFromExtraData(const mozilla::MediaByteBuffer* aExtraData,
+ SPSData& aDest);
+ /* Decode SPS NAL RBSP and fill SPSData structure */
+ static bool DecodeSPS(const mozilla::MediaByteBuffer* aSPS, SPSData& aDest);
+
+ // If the given aExtraData is valid, return the aExtraData.max_num_ref_frames
+ // clamped to be in the range of [4, 16]; otherwise return 4.
+ static uint32_t ComputeMaxRefFrames(
+ const mozilla::MediaByteBuffer* aExtraData);
+
+ enum class FrameType {
+ I_FRAME,
+ OTHER,
+ INVALID,
+ };
+
+ // Returns the frame type. Returns I_FRAME if the sample is an IDR
+ // (Instantaneous Decoding Refresh) Picture.
+ static FrameType GetFrameType(const mozilla::MediaRawData* aSample);
+ // Create a dummy extradata, useful to create a decoder and test the
+ // capabilities of the decoder.
+ static already_AddRefed<mozilla::MediaByteBuffer> CreateExtraData(
+ uint8_t aProfile, uint8_t aConstraints, uint8_t aLevel,
+ const gfx::IntSize& aSize);
+ static void WriteExtraData(mozilla::MediaByteBuffer* aDestExtraData,
+ const uint8_t aProfile, const uint8_t aConstraints,
+ const uint8_t aLevel,
+ const Span<const uint8_t> aSPS,
+ const Span<const uint8_t> aPPS);
+
+ private:
+ friend class SPSNAL;
+ /* Extract RAW BYTE SEQUENCE PAYLOAD from NAL content.
+ Returns nullptr if invalid content.
+ This is compliant to ITU H.264 7.3.1 Syntax in tabular form NAL unit syntax
+ */
+ static already_AddRefed<mozilla::MediaByteBuffer> DecodeNALUnit(
+ const uint8_t* aNAL, size_t aLength);
+ static already_AddRefed<mozilla::MediaByteBuffer> EncodeNALUnit(
+ const uint8_t* aNAL, size_t aLength);
+ static bool vui_parameters(mozilla::BitReader& aBr, SPSData& aDest);
+ // Read HRD parameters, all data is ignored.
+ static void hrd_parameters(mozilla::BitReader& aBr);
+ static uint8_t NumSPS(const mozilla::MediaByteBuffer* aExtraData);
+ // Decode SEI payload and return true if the SEI NAL indicates a recovery
+ // point.
+ static bool DecodeRecoverySEI(const mozilla::MediaByteBuffer* aSEI,
+ SEIRecoveryData& aDest);
+ // Decode NAL Slice payload and return true if its slice type is I slice or SI
+ // slice.
+ static bool DecodeISlice(const mozilla::MediaByteBuffer* aSlice);
+};
+
+} // namespace mozilla
+
+#endif // MP4_DEMUXER_H264_H_
diff --git a/dom/media/platforms/agnostic/bytestreams/gtest/TestAnnexB.cpp b/dom/media/platforms/agnostic/bytestreams/gtest/TestAnnexB.cpp
new file mode 100644
index 0000000000..abc4b2ae8d
--- /dev/null
+++ b/dom/media/platforms/agnostic/bytestreams/gtest/TestAnnexB.cpp
@@ -0,0 +1,144 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+
+#include "AnnexB.h"
+#include "ByteWriter.h"
+#include "H264.h"
+
+namespace mozilla {
+
+// Create AVCC style extra data (the contents on an AVCC box). Note
+// NALLengthSize will be 4 so AVCC samples need to set their data up
+// accordingly.
+static already_AddRefed<MediaByteBuffer> GetExtraData() {
+ // Extra data with
+ // - baseline profile(0x42 == 66).
+ // - constraint flags 0 and 1 set(0xc0) -- normal for baseline profile.
+ // - level 4.0 (0x28 == 40).
+ // - 1280 * 720 resolution.
+ return H264::CreateExtraData(0x42, 0xc0, 0x28, {1280, 720});
+}
+
+// Create an AVCC style sample with requested size in bytes. This sample is
+// setup to contain a single NAL (in practice samples can contain many). The
+// sample sets its NAL size to aSampleSize - 4 and stores that size in the first
+// 4 bytes. Aside from the NAL size at the start, the data is uninitialized
+// (beware)! aSampleSize is a uint32_t as samples larger than can be expressed
+// by a uint32_t are not to spec.
+static already_AddRefed<MediaRawData> GetAvccSample(uint32_t aSampleSize) {
+ if (aSampleSize < 4) {
+ // Stop tests asking for insane samples.
+ EXPECT_FALSE(true) << "Samples should be requested with sane sizes";
+ }
+ nsTArray<uint8_t> sampleData;
+
+ // Write the NAL size.
+ ByteWriter<BigEndian> writer(sampleData);
+ EXPECT_TRUE(writer.WriteU32(aSampleSize - 4));
+
+ // Write the 'NAL'. Beware, this data is uninitialized.
+ sampleData.AppendElements(static_cast<size_t>(aSampleSize) - 4);
+ RefPtr<MediaRawData> rawData =
+ new MediaRawData{sampleData.Elements(), sampleData.Length()};
+ EXPECT_NE(rawData->Data(), nullptr);
+
+ // Set extra data.
+ rawData->mExtraData = GetExtraData();
+ return rawData.forget();
+}
+
+// Test that conversion from AVCC to AnnexB works as expected.
+TEST(AnnexB, AnnexBConversion)
+{
+ RefPtr<MediaRawData> rawData{GetAvccSample(128)};
+
+ {
+ // Test conversion of data when not adding SPS works as expected.
+ RefPtr<MediaRawData> rawDataClone = rawData->Clone();
+ Result<Ok, nsresult> result =
+ AnnexB::ConvertSampleToAnnexB(rawDataClone, /* aAddSps */ false);
+ EXPECT_TRUE(result.isOk()) << "Conversion should succeed";
+ EXPECT_EQ(rawDataClone->Size(), rawData->Size())
+ << "AnnexB sample should be the same size as the AVCC sample -- the 4 "
+ "byte NAL length data (AVCC) is replaced with 4 bytes of NAL "
+ "separator (AnnexB)";
+ EXPECT_TRUE(AnnexB::IsAnnexB(rawDataClone))
+ << "The sample should be AnnexB following conversion";
+ }
+
+ {
+ // Test that the SPS data is not added if the frame is not a keyframe.
+ RefPtr<MediaRawData> rawDataClone = rawData->Clone();
+ rawDataClone->mKeyframe =
+ false; // false is the default, but let's be sure.
+ Result<Ok, nsresult> result =
+ AnnexB::ConvertSampleToAnnexB(rawDataClone, /* aAddSps */ true);
+ EXPECT_TRUE(result.isOk()) << "Conversion should succeed";
+ EXPECT_EQ(rawDataClone->Size(), rawData->Size())
+ << "AnnexB sample should be the same size as the AVCC sample -- the 4 "
+ "byte NAL length data (AVCC) is replaced with 4 bytes of NAL "
+ "separator (AnnexB) and SPS data is not added as the frame is not a "
+ "keyframe";
+ EXPECT_TRUE(AnnexB::IsAnnexB(rawDataClone))
+ << "The sample should be AnnexB following conversion";
+ }
+
+ {
+ // Test that the SPS data is added to keyframes.
+ RefPtr<MediaRawData> rawDataClone = rawData->Clone();
+ rawDataClone->mKeyframe = true;
+ Result<Ok, nsresult> result =
+ AnnexB::ConvertSampleToAnnexB(rawDataClone, /* aAddSps */ true);
+ EXPECT_TRUE(result.isOk()) << "Conversion should succeed";
+ EXPECT_GT(rawDataClone->Size(), rawData->Size())
+ << "AnnexB sample should be larger than the AVCC sample because we've "
+ "added SPS data";
+ EXPECT_TRUE(AnnexB::IsAnnexB(rawDataClone))
+ << "The sample should be AnnexB following conversion";
+ // We could verify the SPS and PPS data we add, but we don't have great
+ // tooling to do so. Consider doing so in future.
+ }
+
+ {
+ // Test conversion involving subsample encryption doesn't overflow vlaues.
+ const uint32_t sampleSize = UINT16_MAX * 2;
+ RefPtr<MediaRawData> rawCryptoData{GetAvccSample(sampleSize)};
+ // Need to be a keyframe to test prepending SPS + PPS to sample.
+ rawCryptoData->mKeyframe = true;
+ UniquePtr<MediaRawDataWriter> rawDataWriter = rawCryptoData->CreateWriter();
+
+ rawDataWriter->mCrypto.mCryptoScheme = CryptoScheme::Cenc;
+
+ // We want to check that the clear size doesn't overflow during conversion.
+ // This size originates in a uint16_t, but since it can grow during AnnexB
+ // we cover it here.
+ const uint16_t clearSize = UINT16_MAX - 10;
+ // Set a clear size very close to uint16_t max value.
+ rawDataWriter->mCrypto.mPlainSizes.AppendElement(clearSize);
+ rawDataWriter->mCrypto.mEncryptedSizes.AppendElement(sampleSize -
+ clearSize);
+
+ RefPtr<MediaRawData> rawCryptoDataClone = rawCryptoData->Clone();
+ Result<Ok, nsresult> result =
+ AnnexB::ConvertSampleToAnnexB(rawCryptoDataClone, /* aAddSps */ true);
+ EXPECT_TRUE(result.isOk()) << "Conversion should succeed";
+ EXPECT_GT(rawCryptoDataClone->Size(), rawCryptoData->Size())
+ << "AnnexB sample should be larger than the AVCC sample because we've "
+ "added SPS data";
+ EXPECT_GT(rawCryptoDataClone->mCrypto.mPlainSizes[0],
+ rawCryptoData->mCrypto.mPlainSizes[0])
+ << "Conversion should have increased clear data sizes without overflow";
+ EXPECT_EQ(rawCryptoDataClone->mCrypto.mEncryptedSizes[0],
+ rawCryptoData->mCrypto.mEncryptedSizes[0])
+ << "Conversion should not affect encrypted sizes";
+ EXPECT_TRUE(AnnexB::IsAnnexB(rawCryptoDataClone))
+ << "The sample should be AnnexB following conversion";
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/agnostic/bytestreams/gtest/moz.build b/dom/media/platforms/agnostic/bytestreams/gtest/moz.build
new file mode 100644
index 0000000000..be0bb9b3ae
--- /dev/null
+++ b/dom/media/platforms/agnostic/bytestreams/gtest/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES += [
+ "TestAnnexB.cpp",
+]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/dom/media/platforms/agnostic/bytestreams/moz.build b/dom/media/platforms/agnostic/bytestreams/moz.build
new file mode 100644
index 0000000000..225cb427f8
--- /dev/null
+++ b/dom/media/platforms/agnostic/bytestreams/moz.build
@@ -0,0 +1,35 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "Audio/Video: Playback")
+
+TEST_DIRS += [
+ "gtest",
+]
+
+EXPORTS += [
+ "Adts.h",
+ "AnnexB.h",
+ "H264.h",
+]
+
+UNIFIED_SOURCES += [
+ "Adts.cpp",
+ "AnnexB.cpp",
+ "H264.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "../../../mp4/",
+]
+
+FINAL_LIBRARY = "xul"
+
+# Suppress warnings for now.
+CXXFLAGS += [
+ "-Wno-sign-compare",
+]
diff --git a/dom/media/platforms/agnostic/eme/ChromiumCDMVideoDecoder.cpp b/dom/media/platforms/agnostic/eme/ChromiumCDMVideoDecoder.cpp
new file mode 100644
index 0000000000..e71632e6d3
--- /dev/null
+++ b/dom/media/platforms/agnostic/eme/ChromiumCDMVideoDecoder.cpp
@@ -0,0 +1,156 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ChromiumCDMVideoDecoder.h"
+#include "ChromiumCDMProxy.h"
+#include "content_decryption_module.h"
+#include "GMPService.h"
+#include "GMPVideoDecoder.h"
+#include "MP4Decoder.h"
+#include "VPXDecoder.h"
+
+namespace mozilla {
+
+ChromiumCDMVideoDecoder::ChromiumCDMVideoDecoder(
+ const GMPVideoDecoderParams& aParams, CDMProxy* aCDMProxy)
+ : mCDMParent(aCDMProxy->AsChromiumCDMProxy()->GetCDMParent()),
+ mConfig(aParams.mConfig),
+ mCrashHelper(aParams.mCrashHelper),
+ mGMPThread(GetGMPThread()),
+ mImageContainer(aParams.mImageContainer),
+ mKnowsCompositor(aParams.mKnowsCompositor) {}
+
+ChromiumCDMVideoDecoder::~ChromiumCDMVideoDecoder() = default;
+
+static uint32_t ToCDMH264Profile(uint8_t aProfile) {
+ switch (aProfile) {
+ case 66:
+ return cdm::VideoCodecProfile::kH264ProfileBaseline;
+ case 77:
+ return cdm::VideoCodecProfile::kH264ProfileMain;
+ case 88:
+ return cdm::VideoCodecProfile::kH264ProfileExtended;
+ case 100:
+ return cdm::VideoCodecProfile::kH264ProfileHigh;
+ case 110:
+ return cdm::VideoCodecProfile::kH264ProfileHigh10;
+ case 122:
+ return cdm::VideoCodecProfile::kH264ProfileHigh422;
+ case 144:
+ return cdm::VideoCodecProfile::kH264ProfileHigh444Predictive;
+ }
+ return cdm::VideoCodecProfile::kUnknownVideoCodecProfile;
+}
+
+RefPtr<MediaDataDecoder::InitPromise> ChromiumCDMVideoDecoder::Init() {
+ if (!mCDMParent) {
+ // Must have failed to get the CDMParent from the ChromiumCDMProxy
+ // in our constructor; the MediaKeys must have shut down the CDM
+ // before we had a chance to start up the decoder.
+ return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
+ }
+
+ gmp::CDMVideoDecoderConfig config;
+ if (MP4Decoder::IsH264(mConfig.mMimeType)) {
+ config.mCodec() = cdm::VideoCodec::kCodecH264;
+ config.mProfile() =
+ ToCDMH264Profile(mConfig.mExtraData->SafeElementAt(1, 0));
+ config.mExtraData() = mConfig.mExtraData->Clone();
+ mConvertToAnnexB = true;
+ } else if (VPXDecoder::IsVP8(mConfig.mMimeType)) {
+ config.mCodec() = cdm::VideoCodec::kCodecVp8;
+ config.mProfile() = cdm::VideoCodecProfile::kProfileNotNeeded;
+ } else if (VPXDecoder::IsVP9(mConfig.mMimeType)) {
+ config.mCodec() = cdm::VideoCodec::kCodecVp9;
+ config.mProfile() = cdm::VideoCodecProfile::kProfileNotNeeded;
+ } else {
+ return MediaDataDecoder::InitPromise::CreateAndReject(
+ NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
+ }
+ config.mImageWidth() = mConfig.mImage.width;
+ config.mImageHeight() = mConfig.mImage.height;
+ config.mEncryptionScheme() = cdm::EncryptionScheme::kUnencrypted;
+ switch (mConfig.mCrypto.mCryptoScheme) {
+ case CryptoScheme::None:
+ break;
+ case CryptoScheme::Cenc:
+ config.mEncryptionScheme() = cdm::EncryptionScheme::kCenc;
+ break;
+ case CryptoScheme::Cbcs:
+ config.mEncryptionScheme() = cdm::EncryptionScheme::kCbcs;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Should not have unrecognized encryption type");
+ break;
+ }
+
+ RefPtr<gmp::ChromiumCDMParent> cdm = mCDMParent;
+ VideoInfo info = mConfig;
+ RefPtr<layers::ImageContainer> imageContainer = mImageContainer;
+ RefPtr<layers::KnowsCompositor> knowsCompositor = mKnowsCompositor;
+ return InvokeAsync(mGMPThread, __func__,
+ [cdm, config, info, imageContainer, knowsCompositor]() {
+ return cdm->InitializeVideoDecoder(
+ config, info, imageContainer, knowsCompositor);
+ });
+}
+
+nsCString ChromiumCDMVideoDecoder::GetDescriptionName() const {
+ return "chromium cdm video decoder"_ns;
+}
+
+nsCString ChromiumCDMVideoDecoder::GetCodecName() const {
+ if (MP4Decoder::IsH264(mConfig.mMimeType)) {
+ return "h264"_ns;
+ } else if (VPXDecoder::IsVP8(mConfig.mMimeType)) {
+ return "vp8"_ns;
+ } else if (VPXDecoder::IsVP9(mConfig.mMimeType)) {
+ return "vp9"_ns;
+ }
+ return "unknown"_ns;
+}
+
+MediaDataDecoder::ConversionRequired ChromiumCDMVideoDecoder::NeedsConversion()
+ const {
+ return mConvertToAnnexB ? ConversionRequired::kNeedAnnexB
+ : ConversionRequired::kNeedNone;
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> ChromiumCDMVideoDecoder::Decode(
+ MediaRawData* aSample) {
+ RefPtr<gmp::ChromiumCDMParent> cdm = mCDMParent;
+ RefPtr<MediaRawData> sample = aSample;
+ return InvokeAsync(mGMPThread, __func__, [cdm, sample]() {
+ return cdm->DecryptAndDecodeFrame(sample);
+ });
+}
+
+RefPtr<MediaDataDecoder::FlushPromise> ChromiumCDMVideoDecoder::Flush() {
+ MOZ_ASSERT(mCDMParent);
+ RefPtr<gmp::ChromiumCDMParent> cdm = mCDMParent;
+ return InvokeAsync(mGMPThread, __func__,
+ [cdm]() { return cdm->FlushVideoDecoder(); });
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> ChromiumCDMVideoDecoder::Drain() {
+ MOZ_ASSERT(mCDMParent);
+ RefPtr<gmp::ChromiumCDMParent> cdm = mCDMParent;
+ return InvokeAsync(mGMPThread, __func__, [cdm]() { return cdm->Drain(); });
+}
+
+RefPtr<ShutdownPromise> ChromiumCDMVideoDecoder::Shutdown() {
+ if (!mCDMParent) {
+ // Must have failed to get the CDMParent from the ChromiumCDMProxy
+ // in our constructor; the MediaKeys must have shut down the CDM
+ // before we had a chance to start up the decoder.
+ return ShutdownPromise::CreateAndResolve(true, __func__);
+ }
+ RefPtr<gmp::ChromiumCDMParent> cdm = mCDMParent;
+ return InvokeAsync(mGMPThread, __func__,
+ [cdm]() { return cdm->ShutdownVideoDecoder(); });
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/agnostic/eme/ChromiumCDMVideoDecoder.h b/dom/media/platforms/agnostic/eme/ChromiumCDMVideoDecoder.h
new file mode 100644
index 0000000000..c177bf2e48
--- /dev/null
+++ b/dom/media/platforms/agnostic/eme/ChromiumCDMVideoDecoder.h
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef ChromiumCDMVideoDecoder_h_
+#define ChromiumCDMVideoDecoder_h_
+
+#include "ChromiumCDMParent.h"
+#include "PlatformDecoderModule.h"
+#include "mozilla/layers/KnowsCompositor.h"
+
+namespace mozilla {
+
+class CDMProxy;
+struct GMPVideoDecoderParams;
+
+DDLoggedTypeDeclNameAndBase(ChromiumCDMVideoDecoder, MediaDataDecoder);
+
+class ChromiumCDMVideoDecoder final
+ : public MediaDataDecoder,
+ public DecoderDoctorLifeLogger<ChromiumCDMVideoDecoder> {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ChromiumCDMVideoDecoder, final);
+
+ ChromiumCDMVideoDecoder(const GMPVideoDecoderParams& aParams,
+ CDMProxy* aCDMProxy);
+
+ RefPtr<InitPromise> Init() override;
+ RefPtr<DecodePromise> Decode(MediaRawData* aSample) override;
+ RefPtr<FlushPromise> Flush() override;
+ RefPtr<DecodePromise> Drain() override;
+ RefPtr<ShutdownPromise> Shutdown() override;
+ nsCString GetDescriptionName() const override;
+ nsCString GetCodecName() const override;
+ ConversionRequired NeedsConversion() const override;
+
+ private:
+ ~ChromiumCDMVideoDecoder();
+
+ RefPtr<gmp::ChromiumCDMParent> mCDMParent;
+ const VideoInfo mConfig;
+ RefPtr<GMPCrashHelper> mCrashHelper;
+ nsCOMPtr<nsISerialEventTarget> mGMPThread;
+ RefPtr<layers::ImageContainer> mImageContainer;
+ RefPtr<layers::KnowsCompositor> mKnowsCompositor;
+ MozPromiseHolder<InitPromise> mInitPromise;
+ bool mConvertToAnnexB = false;
+};
+
+} // namespace mozilla
+
+#endif // ChromiumCDMVideoDecoder_h_
diff --git a/dom/media/platforms/agnostic/eme/DecryptThroughputLimit.h b/dom/media/platforms/agnostic/eme/DecryptThroughputLimit.h
new file mode 100644
index 0000000000..bafb387f83
--- /dev/null
+++ b/dom/media/platforms/agnostic/eme/DecryptThroughputLimit.h
@@ -0,0 +1,103 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DecryptThroughputLimit_h
+#define DecryptThroughputLimit_h
+
+#include <deque>
+
+#include "MediaTimer.h"
+#include "PlatformDecoderModule.h"
+
+namespace mozilla {
+
+// We throttle our decrypt so that we don't decrypt more than a certain
+// duration of samples per second. This is to work around bugs in the
+// Widevine CDM. See bugs 1338924, 1342822, 1718223.
+class DecryptThroughputLimit {
+ public:
+ explicit DecryptThroughputLimit(nsISerialEventTarget* aTargetThread,
+ uint32_t aMaxThroughputMs)
+ : mThrottleScheduler(aTargetThread),
+ mMaxThroughput(aMaxThroughputMs / 1000.0) {}
+
+ typedef MozPromise<RefPtr<MediaRawData>, MediaResult, true> ThrottlePromise;
+
+ // Resolves promise after a delay if necessary in order to reduce the
+ // throughput of samples sent through the CDM for decryption.
+ RefPtr<ThrottlePromise> Throttle(MediaRawData* aSample) {
+ // We should only have one decrypt request being processed at once.
+ MOZ_RELEASE_ASSERT(!mThrottleScheduler.IsScheduled());
+
+ const TimeDuration WindowSize = TimeDuration::FromSeconds(0.1);
+ const TimeDuration MaxThroughput =
+ TimeDuration::FromSeconds(mMaxThroughput);
+
+ // Forget decrypts that happened before the start of our window.
+ const TimeStamp now = TimeStamp::Now();
+ while (!mDecrypts.empty() &&
+ mDecrypts.front().mTimestamp < now - WindowSize) {
+ mDecrypts.pop_front();
+ }
+
+ // How much time duration of the media would we have decrypted inside the
+ // time window if we did decrypt this block?
+ TimeDuration sampleDuration = aSample->mDuration.ToTimeDuration();
+ TimeDuration durationDecrypted = sampleDuration;
+ for (const DecryptedJob& job : mDecrypts) {
+ durationDecrypted += job.mSampleDuration;
+ }
+
+ if (durationDecrypted < MaxThroughput) {
+ // If we decrypted a sample of this duration, we would *not* have
+ // decrypted more than our threshold for max throughput, over the
+ // preceding wall time window. So we're safe to proceed with this
+ // decrypt.
+ mDecrypts.push_back(DecryptedJob({now, sampleDuration}));
+ return ThrottlePromise::CreateAndResolve(aSample, __func__);
+ }
+
+ // Otherwise, we need to delay until decrypting won't exceed our
+ // throughput threshold.
+
+ RefPtr<ThrottlePromise> p = mPromiseHolder.Ensure(__func__);
+
+ TimeDuration delay = durationDecrypted - MaxThroughput;
+ TimeStamp target = now + delay;
+ RefPtr<MediaRawData> sample(aSample);
+ mThrottleScheduler.Ensure(
+ target,
+ [this, sample, sampleDuration]() {
+ mThrottleScheduler.CompleteRequest();
+ mDecrypts.push_back(DecryptedJob({TimeStamp::Now(), sampleDuration}));
+ mPromiseHolder.Resolve(sample, __func__);
+ },
+ []() { MOZ_DIAGNOSTIC_ASSERT(false); });
+
+ return p;
+ }
+
+ void Flush() {
+ mThrottleScheduler.Reset();
+ mPromiseHolder.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ }
+
+ private:
+ DelayedScheduler mThrottleScheduler;
+ MozPromiseHolder<ThrottlePromise> mPromiseHolder;
+
+ double mMaxThroughput;
+
+ struct DecryptedJob {
+ TimeStamp mTimestamp;
+ TimeDuration mSampleDuration;
+ };
+ std::deque<DecryptedJob> mDecrypts;
+};
+
+} // namespace mozilla
+
+#endif // DecryptThroughputLimit_h
diff --git a/dom/media/platforms/agnostic/eme/EMEDecoderModule.cpp b/dom/media/platforms/agnostic/eme/EMEDecoderModule.cpp
new file mode 100644
index 0000000000..d4477bd6cd
--- /dev/null
+++ b/dom/media/platforms/agnostic/eme/EMEDecoderModule.cpp
@@ -0,0 +1,479 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "EMEDecoderModule.h"
+
+#include <inttypes.h>
+
+#include "Adts.h"
+#include "BlankDecoderModule.h"
+#include "ChromiumCDMVideoDecoder.h"
+#include "DecryptThroughputLimit.h"
+#include "GMPDecoderModule.h"
+#include "GMPService.h"
+#include "GMPVideoDecoder.h"
+#include "MP4Decoder.h"
+#include "MediaInfo.h"
+#include "PDMFactory.h"
+#include "mozilla/CDMProxy.h"
+#include "mozilla/EMEUtils.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Unused.h"
+#include "nsClassHashtable.h"
+#include "nsServiceManagerUtils.h"
+
+namespace mozilla {
+
+typedef MozPromiseRequestHolder<DecryptPromise> DecryptPromiseRequestHolder;
+
+DDLoggedTypeDeclNameAndBase(EMEDecryptor, MediaDataDecoder);
+
+class ADTSSampleConverter {
+ public:
+ explicit ADTSSampleConverter(const AudioInfo& aInfo)
+ : mNumChannels(aInfo.mChannels)
+ // Note: we set profile to 2 if we encounter an extended profile (which
+ // set mProfile to 0 and then set mExtendedProfile) such as HE-AACv2
+ // (profile 5). These can then pass through conversion to ADTS and back.
+ // This is done as ADTS only has 2 bits for profile, and the transform
+ // subtracts one from the value. We check if the profile supplied is > 4
+ // for safety. 2 is used as a fallback value, though it seems the CDM
+ // doesn't care what is set.
+ ,
+ mProfile(aInfo.mProfile < 1 || aInfo.mProfile > 4 ? 2 : aInfo.mProfile),
+ mFrequencyIndex(Adts::GetFrequencyIndex(aInfo.mRate)) {
+ EME_LOG("ADTSSampleConvertor(): aInfo.mProfile=%" PRIi8
+ " aInfo.mExtendedProfile=%" PRIi8,
+ aInfo.mProfile, aInfo.mExtendedProfile);
+ if (aInfo.mProfile < 1 || aInfo.mProfile > 4) {
+ EME_LOG(
+ "ADTSSampleConvertor(): Profile not in [1, 4]! Samples will "
+ "their profile set to 2!");
+ }
+ }
+ bool Convert(MediaRawData* aSample) const {
+ return Adts::ConvertSample(mNumChannels, mFrequencyIndex, mProfile,
+ aSample);
+ }
+ bool Revert(MediaRawData* aSample) const {
+ return Adts::RevertSample(aSample);
+ }
+
+ private:
+ const uint32_t mNumChannels;
+ const uint8_t mProfile;
+ const uint8_t mFrequencyIndex;
+};
+
+class EMEDecryptor final : public MediaDataDecoder,
+ public DecoderDoctorLifeLogger<EMEDecryptor> {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(EMEDecryptor, final);
+
+ EMEDecryptor(MediaDataDecoder* aDecoder, CDMProxy* aProxy,
+ TrackInfo::TrackType aType,
+ const std::function<MediaEventProducer<TrackInfo::TrackType>*()>&
+ aOnWaitingForKey,
+ UniquePtr<ADTSSampleConverter> aConverter = nullptr)
+ : mDecoder(aDecoder),
+ mProxy(aProxy),
+ mSamplesWaitingForKey(
+ new SamplesWaitingForKey(mProxy, aType, aOnWaitingForKey)),
+ mADTSSampleConverter(std::move(aConverter)),
+ mIsShutdown(false) {
+ DDLINKCHILD("decoder", mDecoder.get());
+ }
+
+ RefPtr<InitPromise> Init() override {
+ MOZ_ASSERT(!mIsShutdown);
+ mThread = GetCurrentSerialEventTarget();
+ uint32_t maxThroughputMs = StaticPrefs::media_eme_max_throughput_ms();
+ EME_LOG("EME max-throughput-ms=%" PRIu32, maxThroughputMs);
+ mThroughputLimiter.emplace(mThread, maxThroughputMs);
+
+ return mDecoder->Init();
+ }
+
+ RefPtr<DecodePromise> Decode(MediaRawData* aSample) override {
+ MOZ_ASSERT(mThread->IsOnCurrentThread());
+ MOZ_RELEASE_ASSERT(mDecrypts.Count() == 0,
+ "Can only process one sample at a time");
+ RefPtr<DecodePromise> p = mDecodePromise.Ensure(__func__);
+
+ RefPtr<EMEDecryptor> self = this;
+ mSamplesWaitingForKey->WaitIfKeyNotUsable(aSample)
+ ->Then(
+ mThread, __func__,
+ [self](const RefPtr<MediaRawData>& aSample) {
+ self->mKeyRequest.Complete();
+ self->ThrottleDecode(aSample);
+ },
+ [self]() { self->mKeyRequest.Complete(); })
+ ->Track(mKeyRequest);
+ return p;
+ }
+
+ void ThrottleDecode(MediaRawData* aSample) {
+ MOZ_ASSERT(mThread->IsOnCurrentThread());
+
+ RefPtr<EMEDecryptor> self = this;
+ mThroughputLimiter->Throttle(aSample)
+ ->Then(
+ mThread, __func__,
+ [self](RefPtr<MediaRawData> aSample) {
+ self->mThrottleRequest.Complete();
+ self->AttemptDecode(aSample);
+ },
+ [self]() { self->mThrottleRequest.Complete(); })
+ ->Track(mThrottleRequest);
+ }
+
+ void AttemptDecode(MediaRawData* aSample) {
+ MOZ_ASSERT(mThread->IsOnCurrentThread());
+ if (mIsShutdown) {
+ NS_WARNING("EME encrypted sample arrived after shutdown");
+ mDecodePromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ return;
+ }
+
+ if (mADTSSampleConverter && !mADTSSampleConverter->Convert(aSample)) {
+ mDecodePromise.RejectIfExists(
+ MediaResult(
+ NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Failed to convert encrypted AAC sample to ADTS")),
+ __func__);
+ return;
+ }
+
+ const auto& decrypt = mDecrypts.InsertOrUpdate(
+ aSample, MakeUnique<DecryptPromiseRequestHolder>());
+ mProxy->Decrypt(aSample)
+ ->Then(mThread, __func__, this, &EMEDecryptor::Decrypted,
+ &EMEDecryptor::Decrypted)
+ ->Track(*decrypt);
+ }
+
+ void Decrypted(const DecryptResult& aDecrypted) {
+ MOZ_ASSERT(mThread->IsOnCurrentThread());
+ MOZ_ASSERT(aDecrypted.mSample);
+
+ UniquePtr<DecryptPromiseRequestHolder> holder;
+ mDecrypts.Remove(aDecrypted.mSample, &holder);
+ if (holder) {
+ holder->Complete();
+ } else {
+ // Decryption is not in the list of decrypt operations waiting
+ // for a result. It must have been flushed or drained. Ignore result.
+ return;
+ }
+
+ if (mADTSSampleConverter &&
+ !mADTSSampleConverter->Revert(aDecrypted.mSample)) {
+ mDecodePromise.RejectIfExists(
+ MediaResult(
+ NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Failed to revert decrypted ADTS sample to AAC")),
+ __func__);
+ return;
+ }
+
+ if (mIsShutdown) {
+ NS_WARNING("EME decrypted sample arrived after shutdown");
+ return;
+ }
+
+ if (aDecrypted.mStatus == eme::NoKeyErr) {
+ // Key became unusable after we sent the sample to CDM to decrypt.
+ // Call Decode() again, so that the sample is enqueued for decryption
+ // if the key becomes usable again.
+ AttemptDecode(aDecrypted.mSample);
+ } else if (aDecrypted.mStatus != eme::Ok) {
+ mDecodePromise.RejectIfExists(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("decrypted.mStatus=%u",
+ uint32_t(aDecrypted.mStatus))),
+ __func__);
+ } else {
+ MOZ_ASSERT(!mIsShutdown);
+ // The sample is no longer encrypted, so clear its crypto metadata.
+ UniquePtr<MediaRawDataWriter> writer(aDecrypted.mSample->CreateWriter());
+ writer->mCrypto = CryptoSample();
+ RefPtr<EMEDecryptor> self = this;
+ mDecoder->Decode(aDecrypted.mSample)
+ ->Then(mThread, __func__,
+ [self](DecodePromise::ResolveOrRejectValue&& aValue) {
+ self->mDecodeRequest.Complete();
+ self->mDecodePromise.ResolveOrReject(std::move(aValue),
+ __func__);
+ })
+ ->Track(mDecodeRequest);
+ }
+ }
+
+ RefPtr<FlushPromise> Flush() override {
+ MOZ_ASSERT(mThread->IsOnCurrentThread());
+ MOZ_ASSERT(!mIsShutdown);
+ mKeyRequest.DisconnectIfExists();
+ mThrottleRequest.DisconnectIfExists();
+ mDecodeRequest.DisconnectIfExists();
+ mDecodePromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ mThroughputLimiter->Flush();
+ for (auto iter = mDecrypts.Iter(); !iter.Done(); iter.Next()) {
+ auto holder = iter.UserData();
+ holder->DisconnectIfExists();
+ iter.Remove();
+ }
+ RefPtr<SamplesWaitingForKey> k = mSamplesWaitingForKey;
+ return mDecoder->Flush()->Then(mThread, __func__, [k]() {
+ k->Flush();
+ return FlushPromise::CreateAndResolve(true, __func__);
+ });
+ }
+
+ RefPtr<DecodePromise> Drain() override {
+ MOZ_ASSERT(mThread->IsOnCurrentThread());
+ MOZ_ASSERT(!mIsShutdown);
+ MOZ_ASSERT(mDecodePromise.IsEmpty() && !mDecodeRequest.Exists(),
+ "Must wait for decoding to complete");
+ for (auto iter = mDecrypts.Iter(); !iter.Done(); iter.Next()) {
+ auto holder = iter.UserData();
+ holder->DisconnectIfExists();
+ iter.Remove();
+ }
+ return mDecoder->Drain();
+ }
+
+ RefPtr<ShutdownPromise> Shutdown() override {
+ // mThread may not be set if Init hasn't been called first.
+ MOZ_ASSERT(!mThread || mThread->IsOnCurrentThread());
+ MOZ_ASSERT(!mIsShutdown);
+ mIsShutdown = true;
+ mSamplesWaitingForKey->BreakCycles();
+ mSamplesWaitingForKey = nullptr;
+ RefPtr<MediaDataDecoder> decoder = std::move(mDecoder);
+ mProxy = nullptr;
+ return decoder->Shutdown();
+ }
+
+ nsCString GetDescriptionName() const override {
+ return mDecoder->GetDescriptionName();
+ }
+
+ nsCString GetCodecName() const override { return mDecoder->GetCodecName(); }
+
+ ConversionRequired NeedsConversion() const override {
+ return mDecoder->NeedsConversion();
+ }
+
+ private:
+ ~EMEDecryptor() = default;
+
+ RefPtr<MediaDataDecoder> mDecoder;
+ nsCOMPtr<nsISerialEventTarget> mThread;
+ RefPtr<CDMProxy> mProxy;
+ nsClassHashtable<nsRefPtrHashKey<MediaRawData>, DecryptPromiseRequestHolder>
+ mDecrypts;
+ RefPtr<SamplesWaitingForKey> mSamplesWaitingForKey;
+ MozPromiseRequestHolder<SamplesWaitingForKey::WaitForKeyPromise> mKeyRequest;
+ Maybe<DecryptThroughputLimit> mThroughputLimiter;
+ MozPromiseRequestHolder<DecryptThroughputLimit::ThrottlePromise>
+ mThrottleRequest;
+ MozPromiseHolder<DecodePromise> mDecodePromise;
+ MozPromiseHolder<DecodePromise> mDrainPromise;
+ MozPromiseHolder<FlushPromise> mFlushPromise;
+ MozPromiseRequestHolder<DecodePromise> mDecodeRequest;
+ UniquePtr<ADTSSampleConverter> mADTSSampleConverter;
+ bool mIsShutdown;
+};
+
+EMEMediaDataDecoderProxy::EMEMediaDataDecoderProxy(
+ const CreateDecoderParams& aParams,
+ already_AddRefed<MediaDataDecoder> aProxyDecoder,
+ already_AddRefed<nsISerialEventTarget> aProxyThread, CDMProxy* aProxy)
+ : MediaDataDecoderProxy(std::move(aProxyDecoder), std::move(aProxyThread)),
+ mThread(GetCurrentSerialEventTarget()),
+ mSamplesWaitingForKey(new SamplesWaitingForKey(
+ aProxy, aParams.mType, aParams.mOnWaitingForKeyEvent)),
+ mProxy(aProxy) {}
+
+EMEMediaDataDecoderProxy::EMEMediaDataDecoderProxy(
+ const CreateDecoderParams& aParams,
+ already_AddRefed<MediaDataDecoder> aProxyDecoder, CDMProxy* aProxy)
+ : MediaDataDecoderProxy(std::move(aProxyDecoder),
+ do_AddRef(GetCurrentSerialEventTarget())),
+ mThread(GetCurrentSerialEventTarget()),
+ mSamplesWaitingForKey(new SamplesWaitingForKey(
+ aProxy, aParams.mType, aParams.mOnWaitingForKeyEvent)),
+ mProxy(aProxy) {}
+
+RefPtr<MediaDataDecoder::DecodePromise> EMEMediaDataDecoderProxy::Decode(
+ MediaRawData* aSample) {
+ RefPtr<EMEMediaDataDecoderProxy> self = this;
+ RefPtr<MediaRawData> sample = aSample;
+ return InvokeAsync(mThread, __func__, [self, this, sample]() {
+ RefPtr<DecodePromise> p = mDecodePromise.Ensure(__func__);
+ mSamplesWaitingForKey->WaitIfKeyNotUsable(sample)
+ ->Then(
+ mThread, __func__,
+ [self, this](RefPtr<MediaRawData> aSample) {
+ mKeyRequest.Complete();
+
+ MediaDataDecoderProxy::Decode(aSample)
+ ->Then(mThread, __func__,
+ [self,
+ this](DecodePromise::ResolveOrRejectValue&& aValue) {
+ mDecodeRequest.Complete();
+ mDecodePromise.ResolveOrReject(std::move(aValue),
+ __func__);
+ })
+ ->Track(mDecodeRequest);
+ },
+ [self]() {
+ self->mKeyRequest.Complete();
+ MOZ_CRASH("Should never get here");
+ })
+ ->Track(mKeyRequest);
+
+ return p;
+ });
+}
+
+RefPtr<MediaDataDecoder::FlushPromise> EMEMediaDataDecoderProxy::Flush() {
+ RefPtr<EMEMediaDataDecoderProxy> self = this;
+ return InvokeAsync(mThread, __func__, [self, this]() {
+ mKeyRequest.DisconnectIfExists();
+ mDecodeRequest.DisconnectIfExists();
+ mDecodePromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ return MediaDataDecoderProxy::Flush();
+ });
+}
+
+RefPtr<ShutdownPromise> EMEMediaDataDecoderProxy::Shutdown() {
+ RefPtr<EMEMediaDataDecoderProxy> self = this;
+ return InvokeAsync(mThread, __func__, [self, this]() {
+ mSamplesWaitingForKey->BreakCycles();
+ mSamplesWaitingForKey = nullptr;
+ mProxy = nullptr;
+ return MediaDataDecoderProxy::Shutdown();
+ });
+}
+
+EMEDecoderModule::EMEDecoderModule(CDMProxy* aProxy, PDMFactory* aPDM)
+ : mProxy(aProxy), mPDM(aPDM) {}
+
+EMEDecoderModule::~EMEDecoderModule() = default;
+
+static already_AddRefed<MediaDataDecoderProxy> CreateDecoderWrapper(
+ CDMProxy* aProxy, const CreateDecoderParams& aParams) {
+ RefPtr<gmp::GeckoMediaPluginService> s(
+ gmp::GeckoMediaPluginService::GetGeckoMediaPluginService());
+ if (!s) {
+ return nullptr;
+ }
+ nsCOMPtr<nsISerialEventTarget> thread(s->GetGMPThread());
+ if (!thread) {
+ return nullptr;
+ }
+ RefPtr<MediaDataDecoderProxy> decoder(
+ new EMEMediaDataDecoderProxy(aParams,
+ do_AddRef(new ChromiumCDMVideoDecoder(
+ GMPVideoDecoderParams(aParams), aProxy)),
+ thread.forget(), aProxy));
+ return decoder.forget();
+}
+
+RefPtr<EMEDecoderModule::CreateDecoderPromise>
+EMEDecoderModule::AsyncCreateDecoder(const CreateDecoderParams& aParams) {
+ MOZ_ASSERT(aParams.mConfig.mCrypto.IsEncrypted());
+ MOZ_ASSERT(mPDM);
+
+ if (aParams.mConfig.IsVideo()) {
+ if (StaticPrefs::media_eme_video_blank()) {
+ EME_LOG(
+ "EMEDecoderModule::CreateVideoDecoder() creating a blank decoder.");
+ RefPtr<PlatformDecoderModule> m(BlankDecoderModule::Create());
+ RefPtr<MediaDataDecoder> decoder = m->CreateVideoDecoder(aParams);
+ return EMEDecoderModule::CreateDecoderPromise::CreateAndResolve(decoder,
+ __func__);
+ }
+
+ if (SupportsMimeType(aParams.mConfig.mMimeType, nullptr) !=
+ media::DecodeSupport::Unsupported) {
+ // GMP decodes. Assume that means it can decrypt too.
+ return EMEDecoderModule::CreateDecoderPromise::CreateAndResolve(
+ CreateDecoderWrapper(mProxy, aParams), __func__);
+ }
+
+ RefPtr<EMEDecoderModule::CreateDecoderPromise> p =
+ mPDM->CreateDecoder(aParams)->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr{this},
+ params = CreateDecoderParamsForAsync(aParams)](
+ RefPtr<MediaDataDecoder>&& aDecoder) {
+ RefPtr<MediaDataDecoder> emeDecoder(
+ new EMEDecryptor(aDecoder, self->mProxy, params.mType,
+ params.mOnWaitingForKeyEvent));
+ return EMEDecoderModule::CreateDecoderPromise::CreateAndResolve(
+ emeDecoder, __func__);
+ },
+ [](const MediaResult& aError) {
+ return EMEDecoderModule::CreateDecoderPromise::CreateAndReject(
+ aError, __func__);
+ });
+ return p;
+ }
+
+ MOZ_ASSERT(aParams.mConfig.IsAudio());
+
+ // We don't support using the GMP to decode audio.
+ MOZ_ASSERT(SupportsMimeType(aParams.mConfig.mMimeType, nullptr) ==
+ media::DecodeSupport::Unsupported);
+ MOZ_ASSERT(mPDM);
+
+ if (StaticPrefs::media_eme_audio_blank()) {
+ EME_LOG("EMEDecoderModule::CreateAudioDecoder() creating a blank decoder.");
+ RefPtr<PlatformDecoderModule> m(BlankDecoderModule::Create());
+ RefPtr<MediaDataDecoder> decoder = m->CreateAudioDecoder(aParams);
+ return EMEDecoderModule::CreateDecoderPromise::CreateAndResolve(decoder,
+ __func__);
+ }
+
+ UniquePtr<ADTSSampleConverter> converter = nullptr;
+ if (MP4Decoder::IsAAC(aParams.mConfig.mMimeType)) {
+ // The CDM expects encrypted AAC to be in ADTS format.
+ // See bug 1433344.
+ converter = MakeUnique<ADTSSampleConverter>(aParams.AudioConfig());
+ }
+
+ RefPtr<EMEDecoderModule::CreateDecoderPromise> p =
+ mPDM->CreateDecoder(aParams)->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr{this}, params = CreateDecoderParamsForAsync(aParams),
+ converter = std::move(converter)](
+ RefPtr<MediaDataDecoder>&& aDecoder) mutable {
+ RefPtr<MediaDataDecoder> emeDecoder(new EMEDecryptor(
+ aDecoder, self->mProxy, params.mType,
+ params.mOnWaitingForKeyEvent, std::move(converter)));
+ return EMEDecoderModule::CreateDecoderPromise::CreateAndResolve(
+ emeDecoder, __func__);
+ },
+ [](const MediaResult& aError) {
+ return EMEDecoderModule::CreateDecoderPromise::CreateAndReject(
+ aError, __func__);
+ });
+ return p;
+}
+
+media::DecodeSupportSet EMEDecoderModule::SupportsMimeType(
+ const nsACString& aMimeType, DecoderDoctorDiagnostics* aDiagnostics) const {
+ Maybe<nsCString> keySystem;
+ keySystem.emplace(NS_ConvertUTF16toUTF8(mProxy->KeySystem()));
+ return GMPDecoderModule::SupportsMimeType(
+ aMimeType, nsLiteralCString(CHROMIUM_CDM_API), keySystem);
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/agnostic/eme/EMEDecoderModule.h b/dom/media/platforms/agnostic/eme/EMEDecoderModule.h
new file mode 100644
index 0000000000..06fea6e650
--- /dev/null
+++ b/dom/media/platforms/agnostic/eme/EMEDecoderModule.h
@@ -0,0 +1,79 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(EMEDecoderModule_h_)
+# define EMEDecoderModule_h_
+
+# include "MediaDataDecoderProxy.h"
+# include "PlatformDecoderModule.h"
+# include "SamplesWaitingForKey.h"
+
+namespace mozilla {
+
+class CDMProxy;
+class PDMFactory;
+
+class EMEDecoderModule : public PlatformDecoderModule {
+ public:
+ EMEDecoderModule(CDMProxy* aProxy, PDMFactory* aPDM);
+
+ protected:
+ RefPtr<CreateDecoderPromise> AsyncCreateDecoder(
+ const CreateDecoderParams& aParams) override;
+
+ // Decode thread.
+ already_AddRefed<MediaDataDecoder> CreateVideoDecoder(
+ const CreateDecoderParams& aParams) override {
+ MOZ_CRASH("Not used");
+ }
+
+ // Decode thread.
+ already_AddRefed<MediaDataDecoder> CreateAudioDecoder(
+ const CreateDecoderParams& aParams) override {
+ MOZ_CRASH("Not used");
+ }
+
+ media::DecodeSupportSet SupportsMimeType(
+ const nsACString& aMimeType,
+ DecoderDoctorDiagnostics* aDiagnostics) const override;
+
+ private:
+ virtual ~EMEDecoderModule();
+ RefPtr<CDMProxy> mProxy;
+ // Will be null if CDM has decoding capability.
+ RefPtr<PDMFactory> mPDM;
+};
+
+DDLoggedTypeDeclNameAndBase(EMEMediaDataDecoderProxy, MediaDataDecoderProxy);
+
+class EMEMediaDataDecoderProxy
+ : public MediaDataDecoderProxy,
+ public DecoderDoctorLifeLogger<EMEMediaDataDecoderProxy> {
+ public:
+ EMEMediaDataDecoderProxy(const CreateDecoderParams& aParams,
+ already_AddRefed<MediaDataDecoder> aProxyDecoder,
+ already_AddRefed<nsISerialEventTarget> aProxyThread,
+ CDMProxy* aProxy);
+ EMEMediaDataDecoderProxy(const CreateDecoderParams& aParams,
+ already_AddRefed<MediaDataDecoder> aProxyDecoder,
+ CDMProxy* aProxy);
+
+ RefPtr<DecodePromise> Decode(MediaRawData* aSample) override;
+ RefPtr<FlushPromise> Flush() override;
+ RefPtr<ShutdownPromise> Shutdown() override;
+
+ private:
+ nsCOMPtr<nsISerialEventTarget> mThread;
+ RefPtr<SamplesWaitingForKey> mSamplesWaitingForKey;
+ MozPromiseRequestHolder<SamplesWaitingForKey::WaitForKeyPromise> mKeyRequest;
+ MozPromiseHolder<DecodePromise> mDecodePromise;
+ MozPromiseRequestHolder<DecodePromise> mDecodeRequest;
+ RefPtr<CDMProxy> mProxy;
+};
+
+} // namespace mozilla
+
+#endif // EMEDecoderModule_h_
diff --git a/dom/media/platforms/agnostic/eme/SamplesWaitingForKey.cpp b/dom/media/platforms/agnostic/eme/SamplesWaitingForKey.cpp
new file mode 100644
index 0000000000..23d79ce56a
--- /dev/null
+++ b/dom/media/platforms/agnostic/eme/SamplesWaitingForKey.cpp
@@ -0,0 +1,79 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SamplesWaitingForKey.h"
+
+#include "MediaData.h"
+#include "MediaEventSource.h"
+#include "mozilla/CDMCaps.h"
+#include "mozilla/CDMProxy.h"
+#include "mozilla/TaskQueue.h"
+
+namespace mozilla {
+
+SamplesWaitingForKey::SamplesWaitingForKey(
+ CDMProxy* aProxy, TrackInfo::TrackType aType,
+ const std::function<MediaEventProducer<TrackInfo::TrackType>*()>&
+ aOnWaitingForKeyEvent)
+ : mMutex("SamplesWaitingForKey"),
+ mProxy(aProxy),
+ mType(aType),
+ mOnWaitingForKeyEvent(aOnWaitingForKeyEvent) {}
+
+SamplesWaitingForKey::~SamplesWaitingForKey() { Flush(); }
+
+RefPtr<SamplesWaitingForKey::WaitForKeyPromise>
+SamplesWaitingForKey::WaitIfKeyNotUsable(MediaRawData* aSample) {
+ if (!aSample || !aSample->mCrypto.IsEncrypted() || !mProxy) {
+ return WaitForKeyPromise::CreateAndResolve(aSample, __func__);
+ }
+ auto caps = mProxy->Capabilites().Lock();
+ const auto& keyid = aSample->mCrypto.mKeyId;
+ if (caps->IsKeyUsable(keyid)) {
+ return WaitForKeyPromise::CreateAndResolve(aSample, __func__);
+ }
+ SampleEntry entry;
+ entry.mSample = aSample;
+ RefPtr<WaitForKeyPromise> p = entry.mPromise.Ensure(__func__);
+ {
+ MutexAutoLock lock(mMutex);
+ mSamples.AppendElement(std::move(entry));
+ }
+ if (mOnWaitingForKeyEvent && mOnWaitingForKeyEvent()) {
+ mOnWaitingForKeyEvent()->Notify(mType);
+ }
+ caps->NotifyWhenKeyIdUsable(aSample->mCrypto.mKeyId, this);
+ return p;
+}
+
+void SamplesWaitingForKey::NotifyUsable(const CencKeyId& aKeyId) {
+ MutexAutoLock lock(mMutex);
+ size_t i = 0;
+ while (i < mSamples.Length()) {
+ auto& entry = mSamples[i];
+ if (aKeyId == entry.mSample->mCrypto.mKeyId) {
+ entry.mPromise.Resolve(entry.mSample, __func__);
+ mSamples.RemoveElementAt(i);
+ } else {
+ i++;
+ }
+ }
+}
+
+void SamplesWaitingForKey::Flush() {
+ MutexAutoLock lock(mMutex);
+ for (auto& sample : mSamples) {
+ sample.mPromise.Reject(true, __func__);
+ }
+ mSamples.Clear();
+}
+
+void SamplesWaitingForKey::BreakCycles() {
+ MutexAutoLock lock(mMutex);
+ mProxy = nullptr;
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/agnostic/eme/SamplesWaitingForKey.h b/dom/media/platforms/agnostic/eme/SamplesWaitingForKey.h
new file mode 100644
index 0000000000..06d72e3aae
--- /dev/null
+++ b/dom/media/platforms/agnostic/eme/SamplesWaitingForKey.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef SamplesWaitingForKey_h_
+#define SamplesWaitingForKey_h_
+
+#include <functional>
+
+#include "MediaInfo.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/RefPtr.h"
+
+namespace mozilla {
+
+typedef nsTArray<uint8_t> CencKeyId;
+
+class CDMProxy;
+template <typename... Es>
+class MediaEventProducer;
+class MediaRawData;
+
+// Encapsulates the task of waiting for the CDMProxy to have the necessary
+// keys to decrypt a given sample.
+class SamplesWaitingForKey {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SamplesWaitingForKey)
+
+ typedef MozPromise<RefPtr<MediaRawData>, bool, /* IsExclusive = */ true>
+ WaitForKeyPromise;
+
+ SamplesWaitingForKey(
+ CDMProxy* aProxy, TrackInfo::TrackType aType,
+ const std::function<MediaEventProducer<TrackInfo::TrackType>*()>&
+ aOnWaitingForKeyEvent);
+
+ // Returns a promise that will be resolved if or when a key for decoding the
+ // sample becomes usable.
+ RefPtr<WaitForKeyPromise> WaitIfKeyNotUsable(MediaRawData* aSample);
+
+ void NotifyUsable(const CencKeyId& aKeyId);
+
+ void Flush();
+
+ void BreakCycles();
+
+ protected:
+ ~SamplesWaitingForKey();
+
+ private:
+ Mutex mMutex MOZ_UNANNOTATED;
+ RefPtr<CDMProxy> mProxy;
+ struct SampleEntry {
+ RefPtr<MediaRawData> mSample;
+ MozPromiseHolder<WaitForKeyPromise> mPromise;
+ };
+ nsTArray<SampleEntry> mSamples;
+ const TrackInfo::TrackType mType;
+ const std::function<MediaEventProducer<TrackInfo::TrackType>*()>
+ mOnWaitingForKeyEvent;
+};
+
+} // namespace mozilla
+
+#endif // SamplesWaitingForKey_h_
diff --git a/dom/media/platforms/agnostic/eme/moz.build b/dom/media/platforms/agnostic/eme/moz.build
new file mode 100644
index 0000000000..34f0007b3b
--- /dev/null
+++ b/dom/media/platforms/agnostic/eme/moz.build
@@ -0,0 +1,22 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS += [
+ "ChromiumCDMVideoDecoder.h",
+ "DecryptThroughputLimit.h",
+ "EMEDecoderModule.h",
+ "SamplesWaitingForKey.h",
+]
+
+UNIFIED_SOURCES += [
+ "ChromiumCDMVideoDecoder.cpp",
+ "EMEDecoderModule.cpp",
+ "SamplesWaitingForKey.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/platforms/agnostic/gmp/GMPDecoderModule.cpp b/dom/media/platforms/agnostic/gmp/GMPDecoderModule.cpp
new file mode 100644
index 0000000000..a389c9ad0b
--- /dev/null
+++ b/dom/media/platforms/agnostic/gmp/GMPDecoderModule.cpp
@@ -0,0 +1,94 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GMPDecoderModule.h"
+
+#include "DecoderDoctorDiagnostics.h"
+#include "GMPService.h"
+#include "GMPUtils.h"
+#include "GMPVideoDecoder.h"
+#include "MP4Decoder.h"
+#include "MediaDataDecoderProxy.h"
+#include "VPXDecoder.h"
+#include "VideoUtils.h"
+#include "gmp-video-decode.h"
+#include "mozilla/StaticMutex.h"
+#include "nsServiceManagerUtils.h"
+#ifdef XP_WIN
+# include "WMFDecoderModule.h"
+#endif
+
+namespace mozilla {
+
+static already_AddRefed<MediaDataDecoderProxy> CreateDecoderWrapper(
+ GMPVideoDecoderParams&& aParams) {
+ RefPtr<gmp::GeckoMediaPluginService> s(
+ gmp::GeckoMediaPluginService::GetGeckoMediaPluginService());
+ if (!s) {
+ return nullptr;
+ }
+ nsCOMPtr<nsISerialEventTarget> thread(s->GetGMPThread());
+ if (!thread) {
+ return nullptr;
+ }
+
+ RefPtr<MediaDataDecoderProxy> decoder(new MediaDataDecoderProxy(
+ do_AddRef(new GMPVideoDecoder(std::move(aParams))), thread.forget()));
+ return decoder.forget();
+}
+
+already_AddRefed<MediaDataDecoder> GMPDecoderModule::CreateVideoDecoder(
+ const CreateDecoderParams& aParams) {
+ if (!MP4Decoder::IsH264(aParams.mConfig.mMimeType) &&
+ !VPXDecoder::IsVP8(aParams.mConfig.mMimeType) &&
+ !VPXDecoder::IsVP9(aParams.mConfig.mMimeType)) {
+ return nullptr;
+ }
+
+ return CreateDecoderWrapper(GMPVideoDecoderParams(aParams));
+}
+
+already_AddRefed<MediaDataDecoder> GMPDecoderModule::CreateAudioDecoder(
+ const CreateDecoderParams& aParams) {
+ return nullptr;
+}
+
+/* static */
+media::DecodeSupportSet GMPDecoderModule::SupportsMimeType(
+ const nsACString& aMimeType, const nsACString& aApi,
+ const Maybe<nsCString>& aKeySystem) {
+ AutoTArray<nsCString, 2> tags;
+ if (MP4Decoder::IsH264(aMimeType)) {
+ tags.AppendElement("h264"_ns);
+ } else if (VPXDecoder::IsVP9(aMimeType)) {
+ tags.AppendElement("vp9"_ns);
+ } else if (VPXDecoder::IsVP8(aMimeType)) {
+ tags.AppendElement("vp8"_ns);
+ } else {
+ return media::DecodeSupport::Unsupported;
+ }
+
+ // Optional tag for EME GMP plugins.
+ if (aKeySystem) {
+ tags.AppendElement(*aKeySystem);
+ }
+
+ // GMP plugins are always software based.
+ return HaveGMPFor(aApi, tags) ? media::DecodeSupport::SoftwareDecode
+ : media::DecodeSupport::Unsupported;
+}
+
+media::DecodeSupportSet GMPDecoderModule::SupportsMimeType(
+ const nsACString& aMimeType, DecoderDoctorDiagnostics* aDiagnostics) const {
+ return SupportsMimeType(aMimeType, "decode-video"_ns, Nothing());
+}
+
+/* static */
+already_AddRefed<PlatformDecoderModule> GMPDecoderModule::Create() {
+ return MakeAndAddRef<GMPDecoderModule>();
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/agnostic/gmp/GMPDecoderModule.h b/dom/media/platforms/agnostic/gmp/GMPDecoderModule.h
new file mode 100644
index 0000000000..1a131dc154
--- /dev/null
+++ b/dom/media/platforms/agnostic/gmp/GMPDecoderModule.h
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(GMPDecoderModule_h_)
+# define GMPDecoderModule_h_
+
+# include "PlatformDecoderModule.h"
+# include "mozilla/Maybe.h"
+
+// The special NodeId we use when doing unencrypted decoding using the GMP's
+// decoder. This ensures that each GMP MediaDataDecoder we create doesn't
+// require spinning up a new process, but instead we run all instances of
+// GMP decoders in the one process, to reduce overhead.
+//
+// Note: GMP storage is isolated by NodeId, and non persistent for this
+// special NodeId, and the only way a GMP can communicate with the outside
+// world is through the EME GMP APIs, and we never run EME with this NodeID
+// (because NodeIds are random strings which can't contain the '-' character),
+// so there's no way a malicious GMP can harvest, store, and then report any
+// privacy sensitive data about what users are watching.
+# define SHARED_GMP_DECODING_NODE_ID "gmp-shared-decoding"_ns
+
+namespace mozilla {
+
+class GMPDecoderModule : public PlatformDecoderModule {
+ template <typename T, typename... Args>
+ friend already_AddRefed<T> MakeAndAddRef(Args&&...);
+
+ public:
+ static already_AddRefed<PlatformDecoderModule> Create();
+
+ // Decode thread.
+ already_AddRefed<MediaDataDecoder> CreateVideoDecoder(
+ const CreateDecoderParams& aParams) override;
+
+ // Decode thread.
+ already_AddRefed<MediaDataDecoder> CreateAudioDecoder(
+ const CreateDecoderParams& aParams) override;
+
+ media::DecodeSupportSet SupportsMimeType(
+ const nsACString& aMimeType,
+ DecoderDoctorDiagnostics* aDiagnostics) const override;
+
+ static media::DecodeSupportSet SupportsMimeType(
+ const nsACString& aMimeType, const nsACString& aApi,
+ const Maybe<nsCString>& aKeySystem);
+
+ private:
+ GMPDecoderModule() = default;
+ virtual ~GMPDecoderModule() = default;
+};
+
+} // namespace mozilla
+
+#endif // GMPDecoderModule_h_
diff --git a/dom/media/platforms/agnostic/gmp/GMPVideoDecoder.cpp b/dom/media/platforms/agnostic/gmp/GMPVideoDecoder.cpp
new file mode 100644
index 0000000000..47798fafb0
--- /dev/null
+++ b/dom/media/platforms/agnostic/gmp/GMPVideoDecoder.cpp
@@ -0,0 +1,489 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GMPVideoDecoder.h"
+#include "GMPDecoderModule.h"
+#include "GMPVideoHost.h"
+#include "GMPLog.h"
+#include "MediaData.h"
+#include "mozilla/EndianUtils.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "nsServiceManagerUtils.h"
+#include "AnnexB.h"
+#include "H264.h"
+#include "MP4Decoder.h"
+#include "prsystem.h"
+#include "VPXDecoder.h"
+#include "VideoUtils.h"
+
+namespace mozilla {
+
+#if defined(DEBUG)
+static bool IsOnGMPThread() {
+ nsCOMPtr<mozIGeckoMediaPluginService> mps =
+ do_GetService("@mozilla.org/gecko-media-plugin-service;1");
+ MOZ_ASSERT(mps);
+
+ nsCOMPtr<nsIThread> gmpThread;
+ nsresult rv = mps->GetThread(getter_AddRefs(gmpThread));
+ MOZ_ASSERT(NS_SUCCEEDED(rv) && gmpThread);
+ return gmpThread->IsOnCurrentThread();
+}
+#endif
+
+GMPVideoDecoderParams::GMPVideoDecoderParams(const CreateDecoderParams& aParams)
+ : mConfig(aParams.VideoConfig()),
+ mImageContainer(aParams.mImageContainer),
+ mCrashHelper(aParams.mCrashHelper),
+ mKnowsCompositor(aParams.mKnowsCompositor),
+ mTrackingId(aParams.mTrackingId) {}
+
+nsCString GMPVideoDecoder::GetCodecName() const {
+ if (MP4Decoder::IsH264(mConfig.mMimeType)) {
+ return "h264"_ns;
+ } else if (VPXDecoder::IsVP8(mConfig.mMimeType)) {
+ return "vp8"_ns;
+ } else if (VPXDecoder::IsVP9(mConfig.mMimeType)) {
+ return "vp9"_ns;
+ }
+ return "unknown"_ns;
+}
+
+void GMPVideoDecoder::Decoded(GMPVideoi420Frame* aDecodedFrame) {
+ GMP_LOG_DEBUG("GMPVideoDecoder::Decoded");
+
+ GMPUniquePtr<GMPVideoi420Frame> decodedFrame(aDecodedFrame);
+ MOZ_ASSERT(IsOnGMPThread());
+
+ VideoData::YCbCrBuffer b;
+ for (int i = 0; i < kGMPNumOfPlanes; ++i) {
+ b.mPlanes[i].mData = decodedFrame->Buffer(GMPPlaneType(i));
+ b.mPlanes[i].mStride = decodedFrame->Stride(GMPPlaneType(i));
+ if (i == kGMPYPlane) {
+ b.mPlanes[i].mWidth = decodedFrame->Width();
+ b.mPlanes[i].mHeight = decodedFrame->Height();
+ } else {
+ b.mPlanes[i].mWidth = (decodedFrame->Width() + 1) / 2;
+ b.mPlanes[i].mHeight = (decodedFrame->Height() + 1) / 2;
+ }
+ b.mPlanes[i].mSkip = 0;
+ }
+
+ b.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT;
+ b.mYUVColorSpace =
+ DefaultColorSpace({decodedFrame->Width(), decodedFrame->Height()});
+
+ UniquePtr<SampleMetadata> sampleData;
+ if (auto entryHandle = mSamples.Lookup(decodedFrame->Timestamp())) {
+ sampleData = std::move(entryHandle.Data());
+ entryHandle.Remove();
+ } else {
+ GMP_LOG_DEBUG(
+ "GMPVideoDecoder::Decoded(this=%p) missing sample metadata for "
+ "time %" PRIu64,
+ this, decodedFrame->Timestamp());
+ if (mSamples.IsEmpty()) {
+ // If we have no remaining samples in the table, then we have processed
+ // all outstanding decode requests.
+ ProcessReorderQueue(mDecodePromise, __func__);
+ }
+ return;
+ }
+
+ MOZ_ASSERT(sampleData);
+
+ gfx::IntRect pictureRegion(0, 0, decodedFrame->Width(),
+ decodedFrame->Height());
+ RefPtr<VideoData> v = VideoData::CreateAndCopyData(
+ mConfig, mImageContainer, sampleData->mOffset,
+ media::TimeUnit::FromMicroseconds(decodedFrame->UpdatedTimestamp()),
+ media::TimeUnit::FromMicroseconds(decodedFrame->Duration()), b,
+ sampleData->mKeyframe, media::TimeUnit::FromMicroseconds(-1),
+ pictureRegion, mKnowsCompositor);
+ RefPtr<GMPVideoDecoder> self = this;
+ if (v) {
+ mPerformanceRecorder.Record(static_cast<int64_t>(decodedFrame->Timestamp()),
+ [&](DecodeStage& aStage) {
+ aStage.SetImageFormat(DecodeStage::YUV420P);
+ aStage.SetResolution(decodedFrame->Width(),
+ decodedFrame->Height());
+ aStage.SetYUVColorSpace(b.mYUVColorSpace);
+ aStage.SetColorDepth(b.mColorDepth);
+ aStage.SetColorRange(b.mColorRange);
+ });
+
+ if (mReorderFrames) {
+ mReorderQueue.Push(std::move(v));
+ } else {
+ mUnorderedData.AppendElement(std::move(v));
+ }
+
+ if (mSamples.IsEmpty()) {
+ // If we have no remaining samples in the table, then we have processed
+ // all outstanding decode requests.
+ ProcessReorderQueue(mDecodePromise, __func__);
+ }
+ } else {
+ mReorderQueue.Clear();
+ mUnorderedData.Clear();
+ mSamples.Clear();
+ mDecodePromise.RejectIfExists(
+ MediaResult(NS_ERROR_OUT_OF_MEMORY,
+ RESULT_DETAIL("CallBack::CreateAndCopyData")),
+ __func__);
+ }
+}
+
+void GMPVideoDecoder::ReceivedDecodedReferenceFrame(const uint64_t aPictureId) {
+ GMP_LOG_DEBUG("GMPVideoDecoder::ReceivedDecodedReferenceFrame");
+ MOZ_ASSERT(IsOnGMPThread());
+}
+
+void GMPVideoDecoder::ReceivedDecodedFrame(const uint64_t aPictureId) {
+ GMP_LOG_DEBUG("GMPVideoDecoder::ReceivedDecodedFrame");
+ MOZ_ASSERT(IsOnGMPThread());
+}
+
+void GMPVideoDecoder::InputDataExhausted() {
+ GMP_LOG_DEBUG("GMPVideoDecoder::InputDataExhausted");
+ MOZ_ASSERT(IsOnGMPThread());
+ mSamples.Clear();
+ ProcessReorderQueue(mDecodePromise, __func__);
+}
+
+void GMPVideoDecoder::DrainComplete() {
+ GMP_LOG_DEBUG("GMPVideoDecoder::DrainComplete");
+ MOZ_ASSERT(IsOnGMPThread());
+ mSamples.Clear();
+
+ if (mDrainPromise.IsEmpty()) {
+ return;
+ }
+
+ DecodedData results;
+ if (mReorderFrames) {
+ results.SetCapacity(mReorderQueue.Length());
+ while (!mReorderQueue.IsEmpty()) {
+ results.AppendElement(mReorderQueue.Pop());
+ }
+ } else {
+ results = std::move(mUnorderedData);
+ }
+
+ mDrainPromise.Resolve(std::move(results), __func__);
+}
+
+void GMPVideoDecoder::ResetComplete() {
+ GMP_LOG_DEBUG("GMPVideoDecoder::ResetComplete");
+ MOZ_ASSERT(IsOnGMPThread());
+ mPerformanceRecorder.Record(std::numeric_limits<int64_t>::max());
+ mFlushPromise.ResolveIfExists(true, __func__);
+}
+
+void GMPVideoDecoder::Error(GMPErr aErr) {
+ GMP_LOG_DEBUG("GMPVideoDecoder::Error");
+ MOZ_ASSERT(IsOnGMPThread());
+ auto error = MediaResult(aErr == GMPDecodeErr ? NS_ERROR_DOM_MEDIA_DECODE_ERR
+ : NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("GMPErr:%x", aErr));
+ mDecodePromise.RejectIfExists(error, __func__);
+ mDrainPromise.RejectIfExists(error, __func__);
+ mFlushPromise.RejectIfExists(error, __func__);
+}
+
+void GMPVideoDecoder::Terminated() {
+ GMP_LOG_DEBUG("GMPVideoDecoder::Terminated");
+ MOZ_ASSERT(IsOnGMPThread());
+ Error(GMPErr::GMPAbortedErr);
+}
+
+void GMPVideoDecoder::ProcessReorderQueue(
+ MozPromiseHolder<DecodePromise>& aPromise, const char* aMethodName) {
+ if (aPromise.IsEmpty()) {
+ return;
+ }
+
+ if (!mReorderFrames) {
+ aPromise.Resolve(std::move(mUnorderedData), aMethodName);
+ return;
+ }
+
+ DecodedData results;
+ size_t availableFrames = mReorderQueue.Length();
+ if (availableFrames > mMaxRefFrames) {
+ size_t resolvedFrames = availableFrames - mMaxRefFrames;
+ results.SetCapacity(resolvedFrames);
+ do {
+ results.AppendElement(mReorderQueue.Pop());
+ } while (--resolvedFrames > 0);
+ }
+
+ aPromise.Resolve(std::move(results), aMethodName);
+}
+
+GMPVideoDecoder::GMPVideoDecoder(const GMPVideoDecoderParams& aParams)
+ : mConfig(aParams.mConfig),
+ mGMP(nullptr),
+ mHost(nullptr),
+ mConvertNALUnitLengths(false),
+ mCrashHelper(aParams.mCrashHelper),
+ mImageContainer(aParams.mImageContainer),
+ mKnowsCompositor(aParams.mKnowsCompositor),
+ mTrackingId(aParams.mTrackingId),
+ mCanDecodeBatch(StaticPrefs::media_gmp_decoder_decode_batch()),
+ mReorderFrames(StaticPrefs::media_gmp_decoder_reorder_frames()) {}
+
+void GMPVideoDecoder::InitTags(nsTArray<nsCString>& aTags) {
+ if (MP4Decoder::IsH264(mConfig.mMimeType)) {
+ aTags.AppendElement("h264"_ns);
+ } else if (VPXDecoder::IsVP8(mConfig.mMimeType)) {
+ aTags.AppendElement("vp8"_ns);
+ } else if (VPXDecoder::IsVP9(mConfig.mMimeType)) {
+ aTags.AppendElement("vp9"_ns);
+ }
+}
+
+nsCString GMPVideoDecoder::GetNodeId() { return SHARED_GMP_DECODING_NODE_ID; }
+
+GMPUniquePtr<GMPVideoEncodedFrame> GMPVideoDecoder::CreateFrame(
+ MediaRawData* aSample) {
+ GMPVideoFrame* ftmp = nullptr;
+ GMPErr err = mHost->CreateFrame(kGMPEncodedVideoFrame, &ftmp);
+ if (GMP_FAILED(err)) {
+ return nullptr;
+ }
+
+ GMPUniquePtr<GMPVideoEncodedFrame> frame(
+ static_cast<GMPVideoEncodedFrame*>(ftmp));
+ err = frame->CreateEmptyFrame(aSample->Size());
+ if (GMP_FAILED(err)) {
+ return nullptr;
+ }
+
+ memcpy(frame->Buffer(), aSample->Data(), frame->Size());
+
+ // Convert 4-byte NAL unit lengths to host-endian 4-byte buffer lengths to
+ // suit the GMP API.
+ if (mConvertNALUnitLengths) {
+ const int kNALLengthSize = 4;
+ uint8_t* buf = frame->Buffer();
+ while (buf < frame->Buffer() + frame->Size() - kNALLengthSize) {
+ uint32_t length = BigEndian::readUint32(buf) + kNALLengthSize;
+ *reinterpret_cast<uint32_t*>(buf) = length;
+ buf += length;
+ }
+ }
+
+ frame->SetBufferType(GMP_BufferLength32);
+
+ frame->SetEncodedWidth(mConfig.mDisplay.width);
+ frame->SetEncodedHeight(mConfig.mDisplay.height);
+ frame->SetTimeStamp(aSample->mTime.ToMicroseconds());
+ frame->SetCompleteFrame(true);
+ frame->SetDuration(aSample->mDuration.ToMicroseconds());
+ frame->SetFrameType(aSample->mKeyframe ? kGMPKeyFrame : kGMPDeltaFrame);
+
+ return frame;
+}
+
+const VideoInfo& GMPVideoDecoder::GetConfig() const { return mConfig; }
+
+void GMPVideoDecoder::GMPInitDone(GMPVideoDecoderProxy* aGMP,
+ GMPVideoHost* aHost) {
+ MOZ_ASSERT(IsOnGMPThread());
+
+ if (!aGMP) {
+ mInitPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
+ return;
+ }
+ MOZ_ASSERT(aHost);
+
+ if (mInitPromise.IsEmpty()) {
+ // GMP must have been shutdown while we were waiting for Init operation
+ // to complete.
+ aGMP->Close();
+ return;
+ }
+
+ bool isOpenH264 = aGMP->GetPluginType() == GMPPluginType::OpenH264;
+
+ GMPVideoCodec codec;
+ memset(&codec, 0, sizeof(codec));
+
+ codec.mGMPApiVersion = kGMPVersion34;
+ nsTArray<uint8_t> codecSpecific;
+ if (MP4Decoder::IsH264(mConfig.mMimeType)) {
+ codec.mCodecType = kGMPVideoCodecH264;
+ codecSpecific.AppendElement(0); // mPacketizationMode.
+ codecSpecific.AppendElements(mConfig.mExtraData->Elements(),
+ mConfig.mExtraData->Length());
+ // OpenH264 expects pseudo-AVCC, but others must be passed
+ // AnnexB for H264.
+ mConvertToAnnexB = !isOpenH264;
+ mMaxRefFrames = H264::ComputeMaxRefFrames(mConfig.mExtraData);
+ } else if (VPXDecoder::IsVP8(mConfig.mMimeType)) {
+ codec.mCodecType = kGMPVideoCodecVP8;
+ } else if (VPXDecoder::IsVP9(mConfig.mMimeType)) {
+ codec.mCodecType = kGMPVideoCodecVP9;
+ } else {
+ // Unrecognized mime type
+ aGMP->Close();
+ mInitPromise.Reject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
+ return;
+ }
+ codec.mWidth = mConfig.mImage.width;
+ codec.mHeight = mConfig.mImage.height;
+ codec.mUseThreadedDecode = StaticPrefs::media_gmp_decoder_multithreaded();
+ codec.mLogLevel = GetGMPLibraryLogLevel();
+
+ nsresult rv =
+ aGMP->InitDecode(codec, codecSpecific, this, PR_GetNumberOfProcessors());
+ if (NS_FAILED(rv)) {
+ aGMP->Close();
+ mInitPromise.Reject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
+ return;
+ }
+
+ mGMP = aGMP;
+ mHost = aHost;
+
+ // GMP implementations have interpreted the meaning of GMP_BufferLength32
+ // differently. The OpenH264 GMP expects GMP_BufferLength32 to behave as
+ // specified in the GMP API, where each buffer is prefixed by a 32-bit
+ // host-endian buffer length that includes the size of the buffer length
+ // field. Other existing GMPs currently expect GMP_BufferLength32 (when
+ // combined with kGMPVideoCodecH264) to mean "like AVCC but restricted to
+ // 4-byte NAL lengths" (i.e. buffer lengths are specified in big-endian
+ // and do not include the length of the buffer length field.
+ mConvertNALUnitLengths = isOpenH264;
+
+ mInitPromise.Resolve(TrackInfo::kVideoTrack, __func__);
+}
+
+RefPtr<MediaDataDecoder::InitPromise> GMPVideoDecoder::Init() {
+ MOZ_ASSERT(IsOnGMPThread());
+
+ mMPS = do_GetService("@mozilla.org/gecko-media-plugin-service;1");
+ MOZ_ASSERT(mMPS);
+
+ RefPtr<InitPromise> promise(mInitPromise.Ensure(__func__));
+
+ nsTArray<nsCString> tags;
+ InitTags(tags);
+ UniquePtr<GetGMPVideoDecoderCallback> callback(new GMPInitDoneCallback(this));
+ if (NS_FAILED(mMPS->GetGMPVideoDecoder(mCrashHelper, &tags, GetNodeId(),
+ std::move(callback)))) {
+ mInitPromise.Reject(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
+ }
+
+ return promise;
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> GMPVideoDecoder::Decode(
+ MediaRawData* aSample) {
+ MOZ_ASSERT(IsOnGMPThread());
+
+ RefPtr<MediaRawData> sample(aSample);
+ if (!mGMP) {
+ return DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("mGMP not initialized")),
+ __func__);
+ }
+
+ if (mTrackingId) {
+ MediaInfoFlag flag = MediaInfoFlag::None;
+ flag |= (aSample->mKeyframe ? MediaInfoFlag::KeyFrame
+ : MediaInfoFlag::NonKeyFrame);
+ if (mGMP->GetPluginType() == GMPPluginType::OpenH264) {
+ flag |= MediaInfoFlag::SoftwareDecoding;
+ }
+ if (MP4Decoder::IsH264(mConfig.mMimeType)) {
+ flag |= MediaInfoFlag::VIDEO_H264;
+ } else if (VPXDecoder::IsVP8(mConfig.mMimeType)) {
+ flag |= MediaInfoFlag::VIDEO_VP8;
+ } else if (VPXDecoder::IsVP9(mConfig.mMimeType)) {
+ flag |= MediaInfoFlag::VIDEO_VP9;
+ }
+ mPerformanceRecorder.Start(aSample->mTime.ToMicroseconds(),
+ "GMPVideoDecoder"_ns, *mTrackingId, flag);
+ }
+
+ GMPUniquePtr<GMPVideoEncodedFrame> frame = CreateFrame(sample);
+ if (!frame) {
+ return DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_OUT_OF_MEMORY,
+ RESULT_DETAIL("CreateFrame returned null")),
+ __func__);
+ }
+
+ uint64_t frameTimestamp = frame->TimeStamp();
+ RefPtr<DecodePromise> p = mDecodePromise.Ensure(__func__);
+ nsTArray<uint8_t> info; // No codec specific per-frame info to pass.
+ nsresult rv = mGMP->Decode(std::move(frame), false, info, 0);
+ if (NS_FAILED(rv)) {
+ mDecodePromise.Reject(MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ RESULT_DETAIL("mGMP->Decode:%" PRIx32,
+ static_cast<uint32_t>(rv))),
+ __func__);
+ }
+
+ // If we have multiple outstanding frames, we need to track which offset
+ // belongs to which frame. During seek, it is possible to get the same frame
+ // requested twice, if the old frame is still outstanding. We will simply drop
+ // the extra decoded frame and request more input if the last outstanding.
+ mSamples.WithEntryHandle(frameTimestamp, [&](auto entryHandle) {
+ auto sampleData = MakeUnique<SampleMetadata>(sample);
+ entryHandle.InsertOrUpdate(std::move(sampleData));
+ });
+
+ return p;
+}
+
+RefPtr<MediaDataDecoder::FlushPromise> GMPVideoDecoder::Flush() {
+ MOZ_ASSERT(IsOnGMPThread());
+
+ mDecodePromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ mDrainPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+
+ RefPtr<FlushPromise> p = mFlushPromise.Ensure(__func__);
+ if (!mGMP || NS_FAILED(mGMP->Reset())) {
+ // Abort the flush.
+ mPerformanceRecorder.Record(std::numeric_limits<int64_t>::max());
+ mFlushPromise.Resolve(true, __func__);
+ }
+ return p;
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> GMPVideoDecoder::Drain() {
+ MOZ_ASSERT(IsOnGMPThread());
+
+ MOZ_ASSERT(mDecodePromise.IsEmpty(), "Must wait for decoding to complete");
+
+ RefPtr<DecodePromise> p = mDrainPromise.Ensure(__func__);
+ if (!mGMP || NS_FAILED(mGMP->Drain())) {
+ mDrainPromise.Resolve(DecodedData(), __func__);
+ }
+
+ return p;
+}
+
+RefPtr<ShutdownPromise> GMPVideoDecoder::Shutdown() {
+ mInitPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ mFlushPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+
+ // Note that this *may* be called from the proxy thread also.
+ // TODO: If that's the case, then this code is racy.
+ if (!mGMP) {
+ return ShutdownPromise::CreateAndResolve(true, __func__);
+ }
+ // Note this unblocks flush and drain operations waiting for callbacks.
+ mGMP->Close();
+ mGMP = nullptr;
+ return ShutdownPromise::CreateAndResolve(true, __func__);
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/agnostic/gmp/GMPVideoDecoder.h b/dom/media/platforms/agnostic/gmp/GMPVideoDecoder.h
new file mode 100644
index 0000000000..1f0f59c685
--- /dev/null
+++ b/dom/media/platforms/agnostic/gmp/GMPVideoDecoder.h
@@ -0,0 +1,129 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/layers/KnowsCompositor.h"
+#if !defined(GMPVideoDecoder_h_)
+# define GMPVideoDecoder_h_
+
+# include "GMPVideoDecoderProxy.h"
+# include "ImageContainer.h"
+# include "MediaDataDecoderProxy.h"
+# include "MediaInfo.h"
+# include "PerformanceRecorder.h"
+# include "PlatformDecoderModule.h"
+# include "ReorderQueue.h"
+# include "mozIGeckoMediaPluginService.h"
+# include "nsClassHashtable.h"
+
+namespace mozilla {
+
+struct MOZ_STACK_CLASS GMPVideoDecoderParams {
+ explicit GMPVideoDecoderParams(const CreateDecoderParams& aParams);
+
+ const VideoInfo& mConfig;
+ layers::ImageContainer* mImageContainer;
+ GMPCrashHelper* mCrashHelper;
+ layers::KnowsCompositor* mKnowsCompositor;
+ const Maybe<TrackingId> mTrackingId;
+};
+
+DDLoggedTypeDeclNameAndBase(GMPVideoDecoder, MediaDataDecoder);
+
+class GMPVideoDecoder final : public MediaDataDecoder,
+ public GMPVideoDecoderCallbackProxy,
+ public DecoderDoctorLifeLogger<GMPVideoDecoder> {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(GMPVideoDecoder, final);
+
+ explicit GMPVideoDecoder(const GMPVideoDecoderParams& aParams);
+
+ RefPtr<InitPromise> Init() override;
+ RefPtr<DecodePromise> Decode(MediaRawData* aSample) override;
+ RefPtr<DecodePromise> Drain() override;
+ RefPtr<FlushPromise> Flush() override;
+ RefPtr<ShutdownPromise> Shutdown() override;
+ nsCString GetDescriptionName() const override {
+ return "gmp video decoder"_ns;
+ }
+ nsCString GetCodecName() const override;
+ ConversionRequired NeedsConversion() const override {
+ return mConvertToAnnexB ? ConversionRequired::kNeedAnnexB
+ : ConversionRequired::kNeedAVCC;
+ }
+ bool CanDecodeBatch() const override { return mCanDecodeBatch; }
+
+ // GMPVideoDecoderCallbackProxy
+ // All those methods are called on the GMP thread.
+ void Decoded(GMPVideoi420Frame* aDecodedFrame) override;
+ void ReceivedDecodedReferenceFrame(const uint64_t aPictureId) override;
+ void ReceivedDecodedFrame(const uint64_t aPictureId) override;
+ void InputDataExhausted() override;
+ void DrainComplete() override;
+ void ResetComplete() override;
+ void Error(GMPErr aErr) override;
+ void Terminated() override;
+
+ protected:
+ virtual void InitTags(nsTArray<nsCString>& aTags);
+ virtual nsCString GetNodeId();
+ virtual GMPUniquePtr<GMPVideoEncodedFrame> CreateFrame(MediaRawData* aSample);
+ virtual const VideoInfo& GetConfig() const;
+ void ProcessReorderQueue(MozPromiseHolder<DecodePromise>& aPromise,
+ const char* aMethodName);
+
+ private:
+ ~GMPVideoDecoder() = default;
+
+ class GMPInitDoneCallback : public GetGMPVideoDecoderCallback {
+ public:
+ explicit GMPInitDoneCallback(GMPVideoDecoder* aDecoder)
+ : mDecoder(aDecoder) {}
+
+ void Done(GMPVideoDecoderProxy* aGMP, GMPVideoHost* aHost) override {
+ mDecoder->GMPInitDone(aGMP, aHost);
+ }
+
+ private:
+ RefPtr<GMPVideoDecoder> mDecoder;
+ };
+ void GMPInitDone(GMPVideoDecoderProxy* aGMP, GMPVideoHost* aHost);
+
+ const VideoInfo mConfig;
+ nsCOMPtr<mozIGeckoMediaPluginService> mMPS;
+ GMPVideoDecoderProxy* mGMP;
+ GMPVideoHost* mHost;
+ bool mConvertNALUnitLengths;
+ MozPromiseHolder<InitPromise> mInitPromise;
+ RefPtr<GMPCrashHelper> mCrashHelper;
+
+ struct SampleMetadata {
+ explicit SampleMetadata(MediaRawData* aSample)
+ : mOffset(aSample->mOffset), mKeyframe(aSample->mKeyframe) {}
+ int64_t mOffset;
+ bool mKeyframe;
+ };
+
+ nsClassHashtable<nsUint64HashKey, SampleMetadata> mSamples;
+ RefPtr<layers::ImageContainer> mImageContainer;
+ RefPtr<layers::KnowsCompositor> mKnowsCompositor;
+ PerformanceRecorderMulti<DecodeStage> mPerformanceRecorder;
+ const Maybe<TrackingId> mTrackingId;
+
+ uint32_t mMaxRefFrames = 0;
+ ReorderQueue mReorderQueue;
+ DecodedData mUnorderedData;
+
+ MozPromiseHolder<DecodePromise> mDecodePromise;
+ MozPromiseHolder<DecodePromise> mDrainPromise;
+ MozPromiseHolder<FlushPromise> mFlushPromise;
+ bool mConvertToAnnexB = false;
+ bool mCanDecodeBatch = false;
+ bool mReorderFrames = true;
+};
+
+} // namespace mozilla
+
+#endif // GMPVideoDecoder_h_
diff --git a/dom/media/platforms/agnostic/gmp/moz.build b/dom/media/platforms/agnostic/gmp/moz.build
new file mode 100644
index 0000000000..8e49f4a0e8
--- /dev/null
+++ b/dom/media/platforms/agnostic/gmp/moz.build
@@ -0,0 +1,24 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+LOCAL_INCLUDES += [
+ "/dom/media/gmp", # for GMPLog.h,
+]
+
+EXPORTS += [
+ "GMPDecoderModule.h",
+ "GMPVideoDecoder.h",
+]
+
+UNIFIED_SOURCES += [
+ "GMPDecoderModule.cpp",
+ "GMPVideoDecoder.cpp",
+]
+
+# GMPVideoEncodedFrameImpl.h needs IPC
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/platforms/android/AndroidDataEncoder.cpp b/dom/media/platforms/android/AndroidDataEncoder.cpp
new file mode 100644
index 0000000000..f2768af09e
--- /dev/null
+++ b/dom/media/platforms/android/AndroidDataEncoder.cpp
@@ -0,0 +1,534 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AndroidDataEncoder.h"
+
+#include "AnnexB.h"
+#include "H264.h"
+#include "MediaData.h"
+#include "MediaInfo.h"
+#include "SimpleMap.h"
+
+#include "ImageContainer.h"
+#include "mozilla/Logging.h"
+#include "mozilla/ResultVariant.h"
+
+#include "nsMimeTypes.h"
+
+#include "libyuv.h"
+
+namespace mozilla {
+using media::TimeUnit;
+
+extern LazyLogModule sPEMLog;
+#define AND_ENC_LOG(arg, ...) \
+ MOZ_LOG(sPEMLog, mozilla::LogLevel::Debug, \
+ ("AndroidDataEncoder(%p)::%s: " arg, this, __func__, ##__VA_ARGS__))
+#define AND_ENC_LOGE(arg, ...) \
+ MOZ_LOG(sPEMLog, mozilla::LogLevel::Error, \
+ ("AndroidDataEncoder(%p)::%s: " arg, this, __func__, ##__VA_ARGS__))
+
+#define REJECT_IF_ERROR() \
+ do { \
+ if (mError) { \
+ auto error = mError.value(); \
+ mError.reset(); \
+ return EncodePromise::CreateAndReject(std::move(error), __func__); \
+ } \
+ } while (0)
+
+template <typename ConfigType>
+RefPtr<MediaDataEncoder::InitPromise> AndroidDataEncoder<ConfigType>::Init() {
+ // Sanity-check the input size for Android software encoder fails to do it.
+ if (mConfig.mSize.width == 0 || mConfig.mSize.height == 0) {
+ return InitPromise::CreateAndReject(NS_ERROR_ILLEGAL_VALUE, __func__);
+ }
+
+ return InvokeAsync(mTaskQueue, this, __func__,
+ &AndroidDataEncoder<ConfigType>::ProcessInit);
+}
+
+static const char* MimeTypeOf(MediaDataEncoder::CodecType aCodec) {
+ switch (aCodec) {
+ case MediaDataEncoder::CodecType::H264:
+ return "video/avc";
+ case MediaDataEncoder::CodecType::VP8:
+ return "video/x-vnd.on2.vp8";
+ case MediaDataEncoder::CodecType::VP9:
+ return "video/x-vnd.on2.vp9";
+ default:
+ return "";
+ }
+}
+
+using FormatResult = Result<java::sdk::MediaFormat::LocalRef, MediaResult>;
+
+template <typename ConfigType>
+FormatResult ToMediaFormat(const ConfigType& aConfig) {
+ nsresult rv = NS_OK;
+ java::sdk::MediaFormat::LocalRef format;
+ rv = java::sdk::MediaFormat::CreateVideoFormat(MimeTypeOf(aConfig.mCodecType),
+ aConfig.mSize.width,
+ aConfig.mSize.height, &format);
+ NS_ENSURE_SUCCESS(
+ rv, FormatResult(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ "fail to create Java MediaFormat object")));
+
+ rv =
+ format->SetInteger(java::sdk::MediaFormat::KEY_BITRATE_MODE, 2 /* CBR */);
+ NS_ENSURE_SUCCESS(rv, FormatResult(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ "fail to set bitrate mode")));
+
+ rv = format->SetInteger(java::sdk::MediaFormat::KEY_BIT_RATE,
+ aConfig.mBitsPerSec);
+ NS_ENSURE_SUCCESS(rv, FormatResult(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ "fail to set bitrate")));
+
+ // COLOR_FormatYUV420SemiPlanar(NV12) is the most widely supported
+ // format.
+ rv = format->SetInteger(java::sdk::MediaFormat::KEY_COLOR_FORMAT, 0x15);
+ NS_ENSURE_SUCCESS(rv, FormatResult(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ "fail to set color format")));
+
+ rv = format->SetInteger(java::sdk::MediaFormat::KEY_FRAME_RATE,
+ aConfig.mFramerate);
+ NS_ENSURE_SUCCESS(rv, FormatResult(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ "fail to set frame rate")));
+
+ // Ensure interval >= 1. A negative value means no key frames are
+ // requested after the first frame. A zero value means a stream
+ // containing all key frames is requested.
+ int32_t intervalInSec =
+ std::max<size_t>(1, aConfig.mKeyframeInterval / aConfig.mFramerate);
+ rv = format->SetInteger(java::sdk::MediaFormat::KEY_I_FRAME_INTERVAL,
+ intervalInSec);
+ NS_ENSURE_SUCCESS(rv,
+ FormatResult(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ "fail to set I-frame interval")));
+
+ return format;
+}
+
+template <typename ConfigType>
+RefPtr<MediaDataEncoder::InitPromise>
+AndroidDataEncoder<ConfigType>::ProcessInit() {
+ AssertOnTaskQueue();
+ MOZ_ASSERT(!mJavaEncoder);
+
+ java::sdk::MediaCodec::BufferInfo::LocalRef bufferInfo;
+ if (NS_FAILED(java::sdk::MediaCodec::BufferInfo::New(&bufferInfo)) ||
+ !bufferInfo) {
+ return InitPromise::CreateAndReject(NS_ERROR_OUT_OF_MEMORY, __func__);
+ }
+ mInputBufferInfo = bufferInfo;
+
+ FormatResult result = ToMediaFormat<ConfigType>(mConfig);
+ if (result.isErr()) {
+ return InitPromise::CreateAndReject(result.unwrapErr(), __func__);
+ }
+ mFormat = result.unwrap();
+
+ // Register native methods.
+ JavaCallbacksSupport::Init();
+
+ mJavaCallbacks = java::CodecProxy::NativeCallbacks::New();
+ if (!mJavaCallbacks) {
+ return InitPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ "cannot create Java callback object"),
+ __func__);
+ }
+ JavaCallbacksSupport::AttachNative(
+ mJavaCallbacks, mozilla::MakeUnique<CallbacksSupport>(this));
+
+ mJavaEncoder = java::CodecProxy::Create(true /* encoder */, mFormat, nullptr,
+ mJavaCallbacks, u""_ns);
+ if (!mJavaEncoder) {
+ return InitPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ "cannot create Java encoder object"),
+ __func__);
+ }
+
+ mIsHardwareAccelerated = mJavaEncoder->IsHardwareAccelerated();
+ mDrainState = DrainState::DRAINABLE;
+
+ return InitPromise::CreateAndResolve(TrackInfo::kVideoTrack, __func__);
+}
+
+template <typename ConfigType>
+RefPtr<MediaDataEncoder::EncodePromise> AndroidDataEncoder<ConfigType>::Encode(
+ const MediaData* aSample) {
+ RefPtr<AndroidDataEncoder> self = this;
+ MOZ_ASSERT(aSample != nullptr);
+
+ RefPtr<const MediaData> sample(aSample);
+ return InvokeAsync(mTaskQueue, __func__, [self, sample]() {
+ return self->ProcessEncode(std::move(sample));
+ });
+}
+
+static jni::ByteBuffer::LocalRef ConvertI420ToNV12Buffer(
+ RefPtr<const VideoData> aSample, RefPtr<MediaByteBuffer>& aYUVBuffer,
+ int aStride, int aYPlaneHeight) {
+ const layers::PlanarYCbCrImage* image = aSample->mImage->AsPlanarYCbCrImage();
+ MOZ_ASSERT(image);
+ const layers::PlanarYCbCrData* yuv = image->GetData();
+ auto ySize = yuv->YDataSize();
+ auto cbcrSize = yuv->CbCrDataSize();
+ // If we have a stride or height passed in from the Codec we need to use
+ // those.
+ auto yStride = aStride != 0 ? aStride : yuv->mYStride;
+ auto height = aYPlaneHeight != 0 ? aYPlaneHeight : ySize.height;
+ size_t yLength = yStride * height;
+ size_t length =
+ yLength + yStride * (cbcrSize.height - 1) + cbcrSize.width * 2;
+
+ if (!aYUVBuffer || aYUVBuffer->Capacity() < length) {
+ aYUVBuffer = MakeRefPtr<MediaByteBuffer>(length);
+ aYUVBuffer->SetLength(length);
+ } else {
+ MOZ_ASSERT(aYUVBuffer->Length() >= length);
+ }
+
+ if (libyuv::I420ToNV12(yuv->mYChannel, yuv->mYStride, yuv->mCbChannel,
+ yuv->mCbCrStride, yuv->mCrChannel, yuv->mCbCrStride,
+ aYUVBuffer->Elements(), yStride,
+ aYUVBuffer->Elements() + yLength, yStride, ySize.width,
+ ySize.height) != 0) {
+ return nullptr;
+ }
+
+ return jni::ByteBuffer::New(aYUVBuffer->Elements(), aYUVBuffer->Length());
+}
+
+template <typename ConfigType>
+RefPtr<MediaDataEncoder::EncodePromise>
+AndroidDataEncoder<ConfigType>::ProcessEncode(RefPtr<const MediaData> aSample) {
+ AssertOnTaskQueue();
+
+ REJECT_IF_ERROR();
+
+ RefPtr<const VideoData> sample(aSample->As<const VideoData>());
+ MOZ_ASSERT(sample);
+
+ // Bug 1789846: Check with the Encoder if MediaCodec has a stride or height
+ // value to use.
+ jni::ByteBuffer::LocalRef buffer = ConvertI420ToNV12Buffer(
+ sample, mYUVBuffer, mJavaEncoder->GetInputFormatStride(),
+ mJavaEncoder->GetInputFormatYPlaneHeight());
+ if (!buffer) {
+ return EncodePromise::CreateAndReject(NS_ERROR_ILLEGAL_INPUT, __func__);
+ }
+
+ if (aSample->mKeyframe) {
+ mInputBufferInfo->Set(0, mYUVBuffer->Length(),
+ aSample->mTime.ToMicroseconds(),
+ java::sdk::MediaCodec::BUFFER_FLAG_SYNC_FRAME);
+ } else {
+ mInputBufferInfo->Set(0, mYUVBuffer->Length(),
+ aSample->mTime.ToMicroseconds(), 0);
+ }
+
+ mJavaEncoder->Input(buffer, mInputBufferInfo, nullptr);
+
+ if (mEncodedData.Length() > 0) {
+ EncodedData pending = std::move(mEncodedData);
+ return EncodePromise::CreateAndResolve(std::move(pending), __func__);
+ } else {
+ return EncodePromise::CreateAndResolve(EncodedData(), __func__);
+ }
+}
+
+class AutoRelease final {
+ public:
+ AutoRelease(java::CodecProxy::Param aEncoder, java::Sample::Param aSample)
+ : mEncoder(aEncoder), mSample(aSample) {}
+
+ ~AutoRelease() { mEncoder->ReleaseOutput(mSample, false); }
+
+ private:
+ java::CodecProxy::GlobalRef mEncoder;
+ java::Sample::GlobalRef mSample;
+};
+
+static RefPtr<MediaByteBuffer> ExtractCodecConfig(
+ java::SampleBuffer::Param aBuffer, const int32_t aOffset,
+ const int32_t aSize, const bool aAsAnnexB) {
+ auto annexB = MakeRefPtr<MediaByteBuffer>(aSize);
+ annexB->SetLength(aSize);
+ jni::ByteBuffer::LocalRef dest =
+ jni::ByteBuffer::New(annexB->Elements(), aSize);
+ aBuffer->WriteToByteBuffer(dest, aOffset, aSize);
+ if (aAsAnnexB) {
+ return annexB;
+ }
+ // Convert to avcC.
+ nsTArray<AnnexB::NALEntry> paramSets;
+ AnnexB::ParseNALEntries(
+ Span<const uint8_t>(annexB->Elements(), annexB->Length()), paramSets);
+
+ auto avcc = MakeRefPtr<MediaByteBuffer>();
+ AnnexB::NALEntry& sps = paramSets.ElementAt(0);
+ AnnexB::NALEntry& pps = paramSets.ElementAt(1);
+ const uint8_t* spsPtr = annexB->Elements() + sps.mOffset;
+ H264::WriteExtraData(
+ avcc, spsPtr[1], spsPtr[2], spsPtr[3],
+ Span<const uint8_t>(spsPtr, sps.mSize),
+ Span<const uint8_t>(annexB->Elements() + pps.mOffset, pps.mSize));
+ return avcc;
+}
+
+template <typename ConfigType>
+void AndroidDataEncoder<ConfigType>::ProcessOutput(
+ java::Sample::GlobalRef&& aSample,
+ java::SampleBuffer::GlobalRef&& aBuffer) {
+ if (!mTaskQueue->IsCurrentThreadIn()) {
+ nsresult rv =
+ mTaskQueue->Dispatch(NewRunnableMethod<java::Sample::GlobalRef&&,
+ java::SampleBuffer::GlobalRef&&>(
+ "AndroidDataEncoder::ProcessOutput", this,
+ &AndroidDataEncoder::ProcessOutput, std::move(aSample),
+ std::move(aBuffer)));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ return;
+ }
+ AssertOnTaskQueue();
+
+ if (!mJavaEncoder) {
+ return;
+ }
+
+ AutoRelease releaseSample(mJavaEncoder, aSample);
+
+ java::sdk::MediaCodec::BufferInfo::LocalRef info = aSample->Info();
+ MOZ_ASSERT(info);
+
+ int32_t flags;
+ bool ok = NS_SUCCEEDED(info->Flags(&flags));
+ bool isEOS = !!(flags & java::sdk::MediaCodec::BUFFER_FLAG_END_OF_STREAM);
+
+ int32_t offset;
+ ok &= NS_SUCCEEDED(info->Offset(&offset));
+
+ int32_t size;
+ ok &= NS_SUCCEEDED(info->Size(&size));
+
+ int64_t presentationTimeUs;
+ ok &= NS_SUCCEEDED(info->PresentationTimeUs(&presentationTimeUs));
+
+ if (!ok) {
+ return;
+ }
+
+ if (size > 0) {
+ if ((flags & java::sdk::MediaCodec::BUFFER_FLAG_CODEC_CONFIG) != 0) {
+ mConfigData = ExtractCodecConfig(aBuffer, offset, size,
+ mConfig.mUsage == Usage::Realtime);
+ return;
+ }
+ RefPtr<MediaRawData> output =
+ GetOutputData(aBuffer, offset, size,
+ !!(flags & java::sdk::MediaCodec::BUFFER_FLAG_KEY_FRAME));
+ output->mEOS = isEOS;
+ output->mTime = media::TimeUnit::FromMicroseconds(presentationTimeUs);
+ mEncodedData.AppendElement(std::move(output));
+ }
+
+ if (isEOS) {
+ mDrainState = DrainState::DRAINED;
+ }
+ if (!mDrainPromise.IsEmpty()) {
+ EncodedData pending = std::move(mEncodedData);
+ mDrainPromise.Resolve(std::move(pending), __func__);
+ }
+}
+
+template <typename ConfigType>
+RefPtr<MediaRawData> AndroidDataEncoder<ConfigType>::GetOutputData(
+ java::SampleBuffer::Param aBuffer, const int32_t aOffset,
+ const int32_t aSize, const bool aIsKeyFrame) {
+ // Copy frame data from Java buffer.
+ auto output = MakeRefPtr<MediaRawData>();
+ UniquePtr<MediaRawDataWriter> writer(output->CreateWriter());
+ if (!writer->SetSize(aSize)) {
+ AND_ENC_LOGE("fail to allocate output buffer");
+ return nullptr;
+ }
+
+ jni::ByteBuffer::LocalRef buf = jni::ByteBuffer::New(writer->Data(), aSize);
+ aBuffer->WriteToByteBuffer(buf, aOffset, aSize);
+ output->mKeyframe = aIsKeyFrame;
+
+ return output;
+}
+
+// AVC/H.264 frame can be in avcC or Annex B and needs extra convertion steps.
+template <>
+RefPtr<MediaRawData>
+AndroidDataEncoder<MediaDataEncoder::H264Config>::GetOutputData(
+ java::SampleBuffer::Param aBuffer, const int32_t aOffset,
+ const int32_t aSize, const bool aIsKeyFrame) {
+ auto output = MakeRefPtr<MediaRawData>();
+
+ size_t prependSize = 0;
+ RefPtr<MediaByteBuffer> avccHeader;
+ if (aIsKeyFrame && mConfigData) {
+ if (mConfig.mUsage == Usage::Realtime) {
+ prependSize = mConfigData->Length();
+ } else {
+ avccHeader = mConfigData;
+ }
+ }
+
+ UniquePtr<MediaRawDataWriter> writer(output->CreateWriter());
+ if (!writer->SetSize(prependSize + aSize)) {
+ AND_ENC_LOGE("fail to allocate output buffer");
+ return nullptr;
+ }
+
+ if (prependSize > 0) {
+ PodCopy(writer->Data(), mConfigData->Elements(), prependSize);
+ }
+
+ jni::ByteBuffer::LocalRef buf =
+ jni::ByteBuffer::New(writer->Data() + prependSize, aSize);
+ aBuffer->WriteToByteBuffer(buf, aOffset, aSize);
+
+ if (mConfig.mUsage != Usage::Realtime &&
+ !AnnexB::ConvertSampleToAVCC(output, avccHeader)) {
+ AND_ENC_LOGE("fail to convert annex-b sample to AVCC");
+ return nullptr;
+ }
+
+ output->mKeyframe = aIsKeyFrame;
+
+ return output;
+}
+
+template <typename ConfigType>
+RefPtr<MediaDataEncoder::EncodePromise>
+AndroidDataEncoder<ConfigType>::Drain() {
+ return InvokeAsync(mTaskQueue, this, __func__,
+ &AndroidDataEncoder<ConfigType>::ProcessDrain);
+}
+
+template <typename ConfigType>
+RefPtr<MediaDataEncoder::EncodePromise>
+AndroidDataEncoder<ConfigType>::ProcessDrain() {
+ AssertOnTaskQueue();
+ MOZ_ASSERT(mJavaEncoder);
+ MOZ_ASSERT(mDrainPromise.IsEmpty());
+
+ REJECT_IF_ERROR();
+
+ switch (mDrainState) {
+ case DrainState::DRAINABLE:
+ mInputBufferInfo->Set(0, 0, -1,
+ java::sdk::MediaCodec::BUFFER_FLAG_END_OF_STREAM);
+ mJavaEncoder->Input(nullptr, mInputBufferInfo, nullptr);
+ mDrainState = DrainState::DRAINING;
+ [[fallthrough]];
+ case DrainState::DRAINING:
+ if (mEncodedData.IsEmpty()) {
+ return mDrainPromise.Ensure(__func__); // Pending promise.
+ }
+ [[fallthrough]];
+ case DrainState::DRAINED:
+ if (mEncodedData.Length() > 0) {
+ EncodedData pending = std::move(mEncodedData);
+ return EncodePromise::CreateAndResolve(std::move(pending), __func__);
+ } else {
+ return EncodePromise::CreateAndResolve(EncodedData(), __func__);
+ }
+ }
+}
+
+template <typename ConfigType>
+RefPtr<ShutdownPromise> AndroidDataEncoder<ConfigType>::Shutdown() {
+ return InvokeAsync(mTaskQueue, this, __func__,
+ &AndroidDataEncoder<ConfigType>::ProcessShutdown);
+}
+
+template <typename ConfigType>
+RefPtr<ShutdownPromise> AndroidDataEncoder<ConfigType>::ProcessShutdown() {
+ AssertOnTaskQueue();
+ if (mJavaEncoder) {
+ mJavaEncoder->Release();
+ mJavaEncoder = nullptr;
+ }
+
+ if (mJavaCallbacks) {
+ JavaCallbacksSupport::GetNative(mJavaCallbacks)->Cancel();
+ JavaCallbacksSupport::DisposeNative(mJavaCallbacks);
+ mJavaCallbacks = nullptr;
+ }
+
+ mFormat = nullptr;
+
+ return ShutdownPromise::CreateAndResolve(true, __func__);
+}
+
+template <typename ConfigType>
+RefPtr<GenericPromise> AndroidDataEncoder<ConfigType>::SetBitrate(
+ const MediaDataEncoder::Rate aBitsPerSec) {
+ RefPtr<AndroidDataEncoder> self(this);
+ return InvokeAsync(mTaskQueue, __func__, [self, aBitsPerSec]() {
+ self->mJavaEncoder->SetBitrate(aBitsPerSec);
+ return GenericPromise::CreateAndResolve(true, __func__);
+ });
+
+ return nullptr;
+}
+
+template <typename ConfigType>
+void AndroidDataEncoder<ConfigType>::Error(const MediaResult& aError) {
+ if (!mTaskQueue->IsCurrentThreadIn()) {
+ nsresult rv = mTaskQueue->Dispatch(NewRunnableMethod<MediaResult>(
+ "AndroidDataEncoder::Error", this,
+ &AndroidDataEncoder<ConfigType>::Error, aError));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ return;
+ }
+ AssertOnTaskQueue();
+
+ mError = Some(aError);
+}
+
+template <typename ConfigType>
+void AndroidDataEncoder<ConfigType>::CallbacksSupport::HandleInput(
+ int64_t aTimestamp, bool aProcessed) {}
+
+template <typename ConfigType>
+void AndroidDataEncoder<ConfigType>::CallbacksSupport::HandleOutput(
+ java::Sample::Param aSample, java::SampleBuffer::Param aBuffer) {
+ MutexAutoLock lock(mMutex);
+ if (mEncoder) {
+ mEncoder->ProcessOutput(std::move(aSample), std::move(aBuffer));
+ }
+}
+
+template <typename ConfigType>
+void AndroidDataEncoder<ConfigType>::CallbacksSupport::
+ HandleOutputFormatChanged(java::sdk::MediaFormat::Param aFormat) {}
+
+template <typename ConfigType>
+void AndroidDataEncoder<ConfigType>::CallbacksSupport::HandleError(
+ const MediaResult& aError) {
+ MutexAutoLock lock(mMutex);
+ if (mEncoder) {
+ mEncoder->Error(aError);
+ }
+}
+
+// Force compiler to generate code.
+template class AndroidDataEncoder<MediaDataEncoder::H264Config>;
+template class AndroidDataEncoder<MediaDataEncoder::VP8Config>;
+template class AndroidDataEncoder<MediaDataEncoder::VP9Config>;
+} // namespace mozilla
+
+#undef AND_ENC_LOG
+#undef AND_ENC_LOGE
diff --git a/dom/media/platforms/android/AndroidDataEncoder.h b/dom/media/platforms/android/AndroidDataEncoder.h
new file mode 100644
index 0000000000..b7752e2a43
--- /dev/null
+++ b/dom/media/platforms/android/AndroidDataEncoder.h
@@ -0,0 +1,110 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_PLATFORMS_ANDROID_ANDROIDDATAENCODER_H_
+#define DOM_MEDIA_PLATFORMS_ANDROID_ANDROIDDATAENCODER_H_
+
+#include "MediaData.h"
+#include "PlatformEncoderModule.h"
+#include "TimeUnits.h"
+
+#include "JavaCallbacksSupport.h"
+
+#include "mozilla/Maybe.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/Mutex.h"
+
+namespace mozilla {
+
+template <typename ConfigType>
+class AndroidDataEncoder final : public MediaDataEncoder {
+ public:
+ AndroidDataEncoder(const ConfigType& aConfig, RefPtr<TaskQueue> aTaskQueue)
+ : mConfig(aConfig), mTaskQueue(aTaskQueue) {
+ MOZ_ASSERT(mConfig.mSize.width > 0 && mConfig.mSize.height > 0);
+ MOZ_ASSERT(mTaskQueue);
+ }
+ RefPtr<InitPromise> Init() override;
+ RefPtr<EncodePromise> Encode(const MediaData* aSample) override;
+ RefPtr<EncodePromise> Drain() override;
+ RefPtr<ShutdownPromise> Shutdown() override;
+ RefPtr<GenericPromise> SetBitrate(const Rate aBitsPerSec) override;
+
+ nsCString GetDescriptionName() const override { return "Android Encoder"_ns; }
+
+ private:
+ class CallbacksSupport final : public JavaCallbacksSupport {
+ public:
+ explicit CallbacksSupport(AndroidDataEncoder* aEncoder)
+ : mMutex("AndroidDataEncoder::CallbacksSupport") {
+ MutexAutoLock lock(mMutex);
+ mEncoder = aEncoder;
+ }
+
+ ~CallbacksSupport() {
+ MutexAutoLock lock(mMutex);
+ mEncoder = nullptr;
+ }
+
+ void HandleInput(int64_t aTimestamp, bool aProcessed) override;
+ void HandleOutput(java::Sample::Param aSample,
+ java::SampleBuffer::Param aBuffer) override;
+ void HandleOutputFormatChanged(
+ java::sdk::MediaFormat::Param aFormat) override;
+ void HandleError(const MediaResult& aError) override;
+
+ private:
+ Mutex mMutex;
+ AndroidDataEncoder* mEncoder MOZ_GUARDED_BY(mMutex);
+ };
+ friend class CallbacksSupport;
+
+ // Methods only called on mTaskQueue.
+ RefPtr<InitPromise> ProcessInit();
+ RefPtr<EncodePromise> ProcessEncode(RefPtr<const MediaData> aSample);
+ RefPtr<EncodePromise> ProcessDrain();
+ RefPtr<ShutdownPromise> ProcessShutdown();
+ void ProcessInput();
+ void ProcessOutput(java::Sample::GlobalRef&& aSample,
+ java::SampleBuffer::GlobalRef&& aBuffer);
+ RefPtr<MediaRawData> GetOutputData(java::SampleBuffer::Param aBuffer,
+ const int32_t aOffset, const int32_t aSize,
+ const bool aIsKeyFrame);
+ void Error(const MediaResult& aError);
+
+ void AssertOnTaskQueue() const {
+ MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
+ }
+
+ const ConfigType mConfig;
+
+ RefPtr<TaskQueue> mTaskQueue;
+
+ // Can be accessed on any thread, but only written on during init.
+ bool mIsHardwareAccelerated = false;
+
+ java::CodecProxy::GlobalRef mJavaEncoder;
+ java::CodecProxy::NativeCallbacks::GlobalRef mJavaCallbacks;
+ java::sdk::MediaFormat::GlobalRef mFormat;
+ // Preallocated Java object used as a reusable storage for input buffer
+ // information. Contents must be changed only on mTaskQueue.
+ java::sdk::MediaCodec::BufferInfo::GlobalRef mInputBufferInfo;
+
+ MozPromiseHolder<EncodePromise> mDrainPromise;
+
+ // Accessed on mTaskqueue only.
+ RefPtr<MediaByteBuffer> mYUVBuffer;
+ EncodedData mEncodedData;
+ // SPS/PPS NALUs for realtime usage, avcC otherwise.
+ RefPtr<MediaByteBuffer> mConfigData;
+
+ enum class DrainState { DRAINED, DRAINABLE, DRAINING };
+ DrainState mDrainState;
+
+ Maybe<MediaResult> mError;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/platforms/android/AndroidDecoderModule.cpp b/dom/media/platforms/android/AndroidDecoderModule.cpp
new file mode 100644
index 0000000000..af7691168b
--- /dev/null
+++ b/dom/media/platforms/android/AndroidDecoderModule.cpp
@@ -0,0 +1,243 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <jni.h>
+
+#ifdef MOZ_AV1
+# include "AOMDecoder.h"
+#endif
+#include "MediaInfo.h"
+#include "OpusDecoder.h"
+#include "RemoteDataDecoder.h"
+#include "TheoraDecoder.h"
+#include "VPXDecoder.h"
+#include "VorbisDecoder.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Components.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/java/HardwareCodecCapabilityUtilsWrappers.h"
+#include "nsIGfxInfo.h"
+#include "nsPromiseFlatString.h"
+#include "prlog.h"
+
+#undef LOG
+#define LOG(arg, ...) \
+ MOZ_LOG( \
+ sAndroidDecoderModuleLog, mozilla::LogLevel::Debug, \
+ ("AndroidDecoderModule(%p)::%s: " arg, this, __func__, ##__VA_ARGS__))
+#define SLOG(arg, ...) \
+ MOZ_LOG(sAndroidDecoderModuleLog, mozilla::LogLevel::Debug, \
+ ("%s: " arg, __func__, ##__VA_ARGS__))
+
+using namespace mozilla;
+
+namespace mozilla {
+
+mozilla::LazyLogModule sAndroidDecoderModuleLog("AndroidDecoderModule");
+
+nsCString TranslateMimeType(const nsACString& aMimeType) {
+ if (VPXDecoder::IsVPX(aMimeType, VPXDecoder::VP8)) {
+ static constexpr auto vp8 = "video/x-vnd.on2.vp8"_ns;
+ return vp8;
+ }
+ if (VPXDecoder::IsVPX(aMimeType, VPXDecoder::VP9)) {
+ static constexpr auto vp9 = "video/x-vnd.on2.vp9"_ns;
+ return vp9;
+ }
+ if (aMimeType.EqualsLiteral("video/av1")) {
+ static constexpr auto av1 = "video/av01"_ns;
+ return av1;
+ }
+ return nsCString(aMimeType);
+}
+
+AndroidDecoderModule::AndroidDecoderModule(CDMProxy* aProxy) {
+ mProxy = static_cast<MediaDrmCDMProxy*>(aProxy);
+}
+
+StaticAutoPtr<nsTArray<nsCString>> AndroidDecoderModule::sSupportedMimeTypes;
+
+media::DecodeSupportSet AndroidDecoderModule::SupportsMimeType(
+ const nsACString& aMimeType) {
+ if (jni::GetAPIVersion() < 16) {
+ return media::DecodeSupport::Unsupported;
+ }
+
+ if (aMimeType.EqualsLiteral("video/mp4") ||
+ aMimeType.EqualsLiteral("video/avc")) {
+ // TODO: Note that we do not yet distinguish between SW/HW decode support.
+ // Will be done in bug 1754239.
+ return media::DecodeSupport::SoftwareDecode;
+ }
+
+ // When checking "audio/x-wav", CreateDecoder can cause a JNI ERROR by
+ // Accessing a stale local reference leading to a SIGSEGV crash.
+ // To avoid this we check for wav types here.
+ if (aMimeType.EqualsLiteral("audio/x-wav") ||
+ aMimeType.EqualsLiteral("audio/wave; codecs=1") ||
+ aMimeType.EqualsLiteral("audio/wave; codecs=3") ||
+ aMimeType.EqualsLiteral("audio/wave; codecs=6") ||
+ aMimeType.EqualsLiteral("audio/wave; codecs=7") ||
+ aMimeType.EqualsLiteral("audio/wave; codecs=65534")) {
+ return media::DecodeSupport::Unsupported;
+ }
+
+ if ((VPXDecoder::IsVPX(aMimeType, VPXDecoder::VP8) &&
+ !gfx::gfxVars::UseVP8HwDecode()) ||
+ (VPXDecoder::IsVPX(aMimeType, VPXDecoder::VP9) &&
+ !gfx::gfxVars::UseVP9HwDecode())) {
+ return media::DecodeSupport::Unsupported;
+ }
+
+ // Prefer the gecko decoder for opus and vorbis; stagefright crashes
+ // on content demuxed from mp4.
+ // Not all android devices support FLAC even when they say they do.
+ if (OpusDataDecoder::IsOpus(aMimeType) ||
+ VorbisDataDecoder::IsVorbis(aMimeType) ||
+ aMimeType.EqualsLiteral("audio/flac")) {
+ SLOG("Rejecting audio of type %s", aMimeType.Data());
+ return media::DecodeSupport::Unsupported;
+ }
+
+ // Prefer the gecko decoder for Theora.
+ // Not all android devices support Theora even when they say they do.
+ if (TheoraDecoder::IsTheora(aMimeType)) {
+ SLOG("Rejecting video of type %s", aMimeType.Data());
+ return media::DecodeSupport::Unsupported;
+ }
+
+ if (aMimeType.EqualsLiteral("audio/mpeg") &&
+ StaticPrefs::media_ffvpx_mp3_enabled()) {
+ // Prefer the ffvpx mp3 software decoder if available.
+ return media::DecodeSupport::Unsupported;
+ }
+
+ if (sSupportedMimeTypes) {
+ if (sSupportedMimeTypes->Contains(TranslateMimeType(aMimeType))) {
+ // TODO: Note that we do not yet distinguish between SW/HW decode support.
+ // Will be done in bug 1754239.
+ return media::DecodeSupport::SoftwareDecode;
+ }
+ }
+
+ return media::DecodeSupport::Unsupported;
+}
+
+nsTArray<nsCString> AndroidDecoderModule::GetSupportedMimeTypes() {
+ mozilla::jni::ObjectArray::LocalRef supportedTypes = mozilla::java::
+ HardwareCodecCapabilityUtils::GetDecoderSupportedMimeTypes();
+
+ nsTArray<nsCString> st = nsTArray<nsCString>();
+ for (size_t i = 0; i < supportedTypes->Length(); i++) {
+ st.AppendElement(
+ jni::String::LocalRef(supportedTypes->GetElement(i))->ToCString());
+ }
+
+ return st;
+}
+
+void AndroidDecoderModule::SetSupportedMimeTypes(
+ nsTArray<nsCString>&& aSupportedTypes) {
+ if (!sSupportedMimeTypes) {
+ sSupportedMimeTypes = new nsTArray<nsCString>(std::move(aSupportedTypes));
+ ClearOnShutdown(&sSupportedMimeTypes);
+ }
+}
+
+media::DecodeSupportSet AndroidDecoderModule::SupportsMimeType(
+ const nsACString& aMimeType, DecoderDoctorDiagnostics* aDiagnostics) const {
+ return AndroidDecoderModule::SupportsMimeType(aMimeType);
+}
+
+bool AndroidDecoderModule::SupportsColorDepth(
+ gfx::ColorDepth aColorDepth, DecoderDoctorDiagnostics* aDiagnostics) const {
+ // 10-bit support is codec dependent so this is not entirely accurate.
+ // Supports() will correct it.
+ return aColorDepth == gfx::ColorDepth::COLOR_8 ||
+ aColorDepth == gfx::ColorDepth::COLOR_10;
+}
+
+// Further check is needed because the base class uses the inaccurate
+// SupportsColorDepth().
+media::DecodeSupportSet AndroidDecoderModule::Supports(
+ const SupportDecoderParams& aParams,
+ DecoderDoctorDiagnostics* aDiagnostics) const {
+ media::DecodeSupportSet support =
+ PlatformDecoderModule::Supports(aParams, aDiagnostics);
+
+ // Short-circuit.
+ if (support == media::DecodeSupport::Unsupported) {
+ return support;
+ }
+
+#ifdef MOZ_AV1
+ // For AV1, only allow HW decoder.
+ if (AOMDecoder::IsAV1(aParams.MimeType()) &&
+ (!StaticPrefs::media_av1_enabled() ||
+ !support.contains(media::DecodeSupport::HardwareDecode))) {
+ return media::DecodeSupport::Unsupported;
+ }
+#endif
+
+ // Check 10-bit video.
+ const TrackInfo& trackInfo = aParams.mConfig;
+ const VideoInfo* videoInfo = trackInfo.GetAsVideoInfo();
+ if (!videoInfo || videoInfo->mColorDepth != gfx::ColorDepth::COLOR_10) {
+ return support;
+ }
+
+ return java::HardwareCodecCapabilityUtils::Decodes10Bit(
+ TranslateMimeType(aParams.MimeType()))
+ ? support
+ : media::DecodeSupport::Unsupported;
+}
+
+already_AddRefed<MediaDataDecoder> AndroidDecoderModule::CreateVideoDecoder(
+ const CreateDecoderParams& aParams) {
+ // Temporary - forces use of VPXDecoder when alpha is present.
+ // Bug 1263836 will handle alpha scenario once implemented. It will shift
+ // the check for alpha to PDMFactory but not itself remove the need for a
+ // check.
+ if (aParams.VideoConfig().HasAlpha()) {
+ return nullptr;
+ }
+
+ nsString drmStubId;
+ if (mProxy) {
+ drmStubId = mProxy->GetMediaDrmStubId();
+ }
+
+ RefPtr<MediaDataDecoder> decoder =
+ RemoteDataDecoder::CreateVideoDecoder(aParams, drmStubId, mProxy);
+ return decoder.forget();
+}
+
+already_AddRefed<MediaDataDecoder> AndroidDecoderModule::CreateAudioDecoder(
+ const CreateDecoderParams& aParams) {
+ const AudioInfo& config = aParams.AudioConfig();
+ if (config.mBitDepth != 16) {
+ // We only handle 16-bit audio.
+ return nullptr;
+ }
+
+ LOG("CreateAudioFormat with mimeType=%s, mRate=%d, channels=%d",
+ config.mMimeType.Data(), config.mRate, config.mChannels);
+
+ nsString drmStubId;
+ if (mProxy) {
+ drmStubId = mProxy->GetMediaDrmStubId();
+ }
+ RefPtr<MediaDataDecoder> decoder =
+ RemoteDataDecoder::CreateAudioDecoder(aParams, drmStubId, mProxy);
+ return decoder.forget();
+}
+
+/* static */
+already_AddRefed<PlatformDecoderModule> AndroidDecoderModule::Create(
+ CDMProxy* aProxy) {
+ return MakeAndAddRef<AndroidDecoderModule>(aProxy);
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/android/AndroidDecoderModule.h b/dom/media/platforms/android/AndroidDecoderModule.h
new file mode 100644
index 0000000000..406fe09f31
--- /dev/null
+++ b/dom/media/platforms/android/AndroidDecoderModule.h
@@ -0,0 +1,60 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef AndroidDecoderModule_h_
+#define AndroidDecoderModule_h_
+
+#include "PlatformDecoderModule.h"
+#include "mozilla/MediaDrmCDMProxy.h"
+#include "mozilla/StaticPtr.h" // for StaticAutoPtr
+
+namespace mozilla {
+
+class AndroidDecoderModule : public PlatformDecoderModule {
+ template <typename T, typename... Args>
+ friend already_AddRefed<T> MakeAndAddRef(Args&&...);
+
+ public:
+ static already_AddRefed<PlatformDecoderModule> Create(
+ CDMProxy* aProxy = nullptr);
+
+ already_AddRefed<MediaDataDecoder> CreateVideoDecoder(
+ const CreateDecoderParams& aParams) override;
+
+ already_AddRefed<MediaDataDecoder> CreateAudioDecoder(
+ const CreateDecoderParams& aParams) override;
+
+ media::DecodeSupportSet SupportsMimeType(
+ const nsACString& aMimeType,
+ DecoderDoctorDiagnostics* aDiagnostics) const override;
+
+ static media::DecodeSupportSet SupportsMimeType(const nsACString& aMimeType);
+
+ static nsTArray<nsCString> GetSupportedMimeTypes();
+
+ static void SetSupportedMimeTypes(nsTArray<nsCString>&& aSupportedTypes);
+
+ media::DecodeSupportSet Supports(
+ const SupportDecoderParams& aParams,
+ DecoderDoctorDiagnostics* aDiagnostics) const override;
+
+ protected:
+ bool SupportsColorDepth(
+ gfx::ColorDepth aColorDepth,
+ DecoderDoctorDiagnostics* aDiagnostics) const override;
+
+ private:
+ explicit AndroidDecoderModule(CDMProxy* aProxy = nullptr);
+ virtual ~AndroidDecoderModule() = default;
+ RefPtr<MediaDrmCDMProxy> mProxy;
+ static StaticAutoPtr<nsTArray<nsCString>> sSupportedMimeTypes;
+};
+
+extern LazyLogModule sAndroidDecoderModuleLog;
+
+nsCString TranslateMimeType(const nsACString& aMimeType);
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/platforms/android/AndroidEncoderModule.cpp b/dom/media/platforms/android/AndroidEncoderModule.cpp
new file mode 100644
index 0000000000..4fd85e3182
--- /dev/null
+++ b/dom/media/platforms/android/AndroidEncoderModule.cpp
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AndroidEncoderModule.h"
+
+#include "AndroidDataEncoder.h"
+#include "MP4Decoder.h"
+#include "VPXDecoder.h"
+
+#include "mozilla/Logging.h"
+
+namespace mozilla {
+extern LazyLogModule sPEMLog;
+#define AND_PEM_LOG(arg, ...) \
+ MOZ_LOG( \
+ sPEMLog, mozilla::LogLevel::Debug, \
+ ("AndroidEncoderModule(%p)::%s: " arg, this, __func__, ##__VA_ARGS__))
+
+bool AndroidEncoderModule::SupportsMimeType(const nsACString& aMimeType) const {
+ return (MP4Decoder::IsH264(aMimeType) &&
+ java::HardwareCodecCapabilityUtils::HasHWH264(true /* encoder */)) ||
+ (VPXDecoder::IsVP8(aMimeType) &&
+ java::HardwareCodecCapabilityUtils::HasHWVP8(true /* encoder */)) ||
+ (VPXDecoder::IsVP9(aMimeType) &&
+ java::HardwareCodecCapabilityUtils::HasHWVP9(true /* encoder */));
+}
+
+already_AddRefed<MediaDataEncoder> AndroidEncoderModule::CreateVideoEncoder(
+ const CreateEncoderParams& aParams, const bool aHardwareNotAllowed) const {
+ // TODO: extend AndroidDataEncoder and Java codec to accept this option.
+ MOZ_ASSERT(!aHardwareNotAllowed);
+
+ RefPtr<MediaDataEncoder> encoder;
+ switch (CreateEncoderParams::CodecTypeForMime(aParams.mConfig.mMimeType)) {
+ case MediaDataEncoder::CodecType::H264:
+ return MakeRefPtr<AndroidDataEncoder<MediaDataEncoder::H264Config>>(
+ aParams.ToH264Config(), aParams.mTaskQueue)
+ .forget();
+ case MediaDataEncoder::CodecType::VP8:
+ return MakeRefPtr<AndroidDataEncoder<MediaDataEncoder::VP8Config>>(
+ aParams.ToVP8Config(), aParams.mTaskQueue)
+ .forget();
+ case MediaDataEncoder::CodecType::VP9:
+ return MakeRefPtr<AndroidDataEncoder<MediaDataEncoder::VP9Config>>(
+ aParams.ToVP9Config(), aParams.mTaskQueue)
+ .forget();
+ default:
+ AND_PEM_LOG("Unsupported MIME type:%s", aParams.mConfig.mMimeType.get());
+ return nullptr;
+ }
+}
+
+} // namespace mozilla
+
+#undef AND_PEM_LOG
diff --git a/dom/media/platforms/android/AndroidEncoderModule.h b/dom/media/platforms/android/AndroidEncoderModule.h
new file mode 100644
index 0000000000..e19b472a0e
--- /dev/null
+++ b/dom/media/platforms/android/AndroidEncoderModule.h
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_PLATFORMS_ANDROID_ANDROIDENCODERMODULE_H_
+#define DOM_MEDIA_PLATFORMS_ANDROID_ANDROIDENCODERMODULE_H_
+
+#include "PlatformEncoderModule.h"
+
+namespace mozilla {
+
+class AndroidEncoderModule final : public PlatformEncoderModule {
+ public:
+ bool SupportsMimeType(const nsACString& aMimeType) const override;
+
+ already_AddRefed<MediaDataEncoder> CreateVideoEncoder(
+ const CreateEncoderParams& aParams,
+ const bool aHardwareNotAllowed) const override;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/platforms/android/JavaCallbacksSupport.h b/dom/media/platforms/android/JavaCallbacksSupport.h
new file mode 100644
index 0000000000..e79d796209
--- /dev/null
+++ b/dom/media/platforms/android/JavaCallbacksSupport.h
@@ -0,0 +1,73 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef JavaCallbacksSupport_h_
+#define JavaCallbacksSupport_h_
+
+#include "MediaResult.h"
+#include "MediaCodec.h"
+#include "mozilla/java/CodecProxyNatives.h"
+#include "mozilla/java/SampleBufferWrappers.h"
+#include "mozilla/java/SampleWrappers.h"
+
+namespace mozilla {
+
+class JavaCallbacksSupport
+ : public java::CodecProxy::NativeCallbacks::Natives<JavaCallbacksSupport> {
+ public:
+ typedef java::CodecProxy::NativeCallbacks::Natives<JavaCallbacksSupport> Base;
+ using Base::AttachNative;
+ using Base::DisposeNative;
+ using Base::GetNative;
+
+ JavaCallbacksSupport() : mCanceled(false) {}
+
+ virtual ~JavaCallbacksSupport() {}
+
+ virtual void HandleInput(int64_t aTimestamp, bool aProcessed) = 0;
+
+ void OnInputStatus(jlong aTimestamp, bool aProcessed) {
+ if (!mCanceled) {
+ HandleInput(aTimestamp, aProcessed);
+ }
+ }
+
+ virtual void HandleOutput(java::Sample::Param aSample,
+ java::SampleBuffer::Param aBuffer) = 0;
+
+ void OnOutput(jni::Object::Param aSample, jni::Object::Param aBuffer) {
+ if (!mCanceled) {
+ HandleOutput(java::Sample::Ref::From(aSample),
+ java::SampleBuffer::Ref::From(aBuffer));
+ }
+ }
+
+ virtual void HandleOutputFormatChanged(
+ java::sdk::MediaFormat::Param aFormat){};
+
+ void OnOutputFormatChanged(jni::Object::Param aFormat) {
+ if (!mCanceled) {
+ HandleOutputFormatChanged(java::sdk::MediaFormat::Ref::From(aFormat));
+ }
+ }
+
+ virtual void HandleError(const MediaResult& aError) = 0;
+
+ void OnError(bool aIsFatal) {
+ if (!mCanceled) {
+ HandleError(aIsFatal
+ ? MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__)
+ : MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR, __func__));
+ }
+ }
+
+ void Cancel() { mCanceled = true; }
+
+ private:
+ Atomic<bool> mCanceled;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/platforms/android/RemoteDataDecoder.cpp b/dom/media/platforms/android/RemoteDataDecoder.cpp
new file mode 100644
index 0000000000..5233f9ac90
--- /dev/null
+++ b/dom/media/platforms/android/RemoteDataDecoder.cpp
@@ -0,0 +1,1136 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "RemoteDataDecoder.h"
+
+#include <jni.h>
+
+#include "AndroidBridge.h"
+#include "AndroidBuild.h"
+#include "AndroidDecoderModule.h"
+#include "EMEDecoderModule.h"
+#include "GLImages.h"
+#include "JavaCallbacksSupport.h"
+#include "MediaCodec.h"
+#include "MediaData.h"
+#include "MediaInfo.h"
+#include "PerformanceRecorder.h"
+#include "SimpleMap.h"
+#include "VPXDecoder.h"
+#include "VideoUtils.h"
+#include "mozilla/gfx/Matrix.h"
+#include "mozilla/gfx/Types.h"
+#include "mozilla/java/CodecProxyWrappers.h"
+#include "mozilla/java/GeckoSurfaceWrappers.h"
+#include "mozilla/java/SampleBufferWrappers.h"
+#include "mozilla/java/SampleWrappers.h"
+#include "mozilla/java/SurfaceAllocatorWrappers.h"
+#include "mozilla/Maybe.h"
+#include "nsPromiseFlatString.h"
+#include "nsThreadUtils.h"
+#include "prlog.h"
+
+#undef LOG
+#define LOG(arg, ...) \
+ MOZ_LOG(sAndroidDecoderModuleLog, mozilla::LogLevel::Debug, \
+ ("RemoteDataDecoder(%p)::%s: " arg, this, __func__, ##__VA_ARGS__))
+
+using namespace mozilla;
+using namespace mozilla::gl;
+using media::TimeUnit;
+
+namespace mozilla {
+
+// Hold a reference to the output buffer until we're ready to release it back to
+// the Java codec (for rendering or not).
+class RenderOrReleaseOutput {
+ public:
+ RenderOrReleaseOutput(java::CodecProxy::Param aCodec,
+ java::Sample::Param aSample)
+ : mCodec(aCodec), mSample(aSample) {}
+
+ virtual ~RenderOrReleaseOutput() { ReleaseOutput(false); }
+
+ protected:
+ void ReleaseOutput(bool aToRender) {
+ if (mCodec && mSample) {
+ mCodec->ReleaseOutput(mSample, aToRender);
+ mCodec = nullptr;
+ mSample = nullptr;
+ }
+ }
+
+ private:
+ java::CodecProxy::GlobalRef mCodec;
+ java::Sample::GlobalRef mSample;
+};
+
+class RemoteVideoDecoder final : public RemoteDataDecoder {
+ public:
+ // Render the output to the surface when the frame is sent
+ // to compositor, or release it if not presented.
+ class CompositeListener
+ : private RenderOrReleaseOutput,
+ public layers::SurfaceTextureImage::SetCurrentCallback {
+ public:
+ CompositeListener(java::CodecProxy::Param aCodec,
+ java::Sample::Param aSample)
+ : RenderOrReleaseOutput(aCodec, aSample) {}
+
+ void operator()(void) override { ReleaseOutput(true); }
+ };
+
+ class InputInfo {
+ public:
+ InputInfo() = default;
+
+ InputInfo(const int64_t aDurationUs, const gfx::IntSize& aImageSize,
+ const gfx::IntSize& aDisplaySize)
+ : mDurationUs(aDurationUs),
+ mImageSize(aImageSize),
+ mDisplaySize(aDisplaySize) {}
+
+ int64_t mDurationUs = {};
+ gfx::IntSize mImageSize = {};
+ gfx::IntSize mDisplaySize = {};
+ };
+
+ class CallbacksSupport final : public JavaCallbacksSupport {
+ public:
+ explicit CallbacksSupport(RemoteVideoDecoder* aDecoder)
+ : mDecoder(aDecoder) {}
+
+ void HandleInput(int64_t aTimestamp, bool aProcessed) override {
+ mDecoder->UpdateInputStatus(aTimestamp, aProcessed);
+ }
+
+ void HandleOutput(java::Sample::Param aSample,
+ java::SampleBuffer::Param aBuffer) override {
+ MOZ_ASSERT(!aBuffer, "Video sample should be bufferless");
+ // aSample will be implicitly converted into a GlobalRef.
+ mDecoder->ProcessOutput(aSample);
+ }
+
+ void HandleOutputFormatChanged(
+ java::sdk::MediaFormat::Param aFormat) override {
+ int32_t colorFormat = 0;
+ aFormat->GetInteger(java::sdk::MediaFormat::KEY_COLOR_FORMAT,
+ &colorFormat);
+ if (colorFormat == 0) {
+ mDecoder->Error(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Invalid color format:%d", colorFormat)));
+ return;
+ }
+
+ Maybe<int32_t> colorRange;
+ {
+ int32_t range = 0;
+ if (NS_SUCCEEDED(aFormat->GetInteger(
+ java::sdk::MediaFormat::KEY_COLOR_RANGE, &range))) {
+ colorRange.emplace(range);
+ }
+ }
+
+ Maybe<int32_t> colorSpace;
+ {
+ int32_t space = 0;
+ if (NS_SUCCEEDED(aFormat->GetInteger(
+ java::sdk::MediaFormat::KEY_COLOR_STANDARD, &space))) {
+ colorSpace.emplace(space);
+ }
+ }
+
+ mDecoder->ProcessOutputFormatChange(colorFormat, colorRange, colorSpace);
+ }
+
+ void HandleError(const MediaResult& aError) override {
+ mDecoder->Error(aError);
+ }
+
+ friend class RemoteDataDecoder;
+
+ private:
+ RemoteVideoDecoder* mDecoder;
+ };
+
+ RemoteVideoDecoder(const VideoInfo& aConfig,
+ java::sdk::MediaFormat::Param aFormat,
+ const nsString& aDrmStubId, Maybe<TrackingId> aTrackingId)
+ : RemoteDataDecoder(MediaData::Type::VIDEO_DATA, aConfig.mMimeType,
+ aFormat, aDrmStubId),
+ mConfig(aConfig),
+ mTrackingId(std::move(aTrackingId)) {}
+
+ ~RemoteVideoDecoder() {
+ if (mSurface) {
+ java::SurfaceAllocator::DisposeSurface(mSurface);
+ }
+ }
+
+ RefPtr<InitPromise> Init() override {
+ mThread = GetCurrentSerialEventTarget();
+ java::sdk::MediaCodec::BufferInfo::LocalRef bufferInfo;
+ if (NS_FAILED(java::sdk::MediaCodec::BufferInfo::New(&bufferInfo)) ||
+ !bufferInfo) {
+ return InitPromise::CreateAndReject(NS_ERROR_OUT_OF_MEMORY, __func__);
+ }
+ mInputBufferInfo = bufferInfo;
+
+ mSurface =
+ java::GeckoSurface::LocalRef(java::SurfaceAllocator::AcquireSurface(
+ mConfig.mImage.width, mConfig.mImage.height, false));
+ if (!mSurface) {
+ return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ __func__);
+ }
+
+ mSurfaceHandle = mSurface->GetHandle();
+
+ // Register native methods.
+ JavaCallbacksSupport::Init();
+
+ mJavaCallbacks = java::CodecProxy::NativeCallbacks::New();
+ if (!mJavaCallbacks) {
+ return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ __func__);
+ }
+ JavaCallbacksSupport::AttachNative(
+ mJavaCallbacks, mozilla::MakeUnique<CallbacksSupport>(this));
+
+ mJavaDecoder = java::CodecProxy::Create(
+ false, // false indicates to create a decoder and true denotes encoder
+ mFormat, mSurface, mJavaCallbacks, mDrmStubId);
+ if (mJavaDecoder == nullptr) {
+ return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ __func__);
+ }
+ mIsCodecSupportAdaptivePlayback =
+ mJavaDecoder->IsAdaptivePlaybackSupported();
+ mIsHardwareAccelerated = mJavaDecoder->IsHardwareAccelerated();
+
+ // On some devices we have observed that the transform obtained from
+ // SurfaceTexture.getTransformMatrix() is incorrect for surfaces produced by
+ // a MediaCodec. We therefore override the transform to be a simple y-flip
+ // to ensure it is rendered correctly.
+ const auto hardware = java::sdk::Build::HARDWARE()->ToString();
+ if (hardware.EqualsASCII("mt6735") || hardware.EqualsASCII("kirin980")) {
+ mTransformOverride = Some(
+ gfx::Matrix4x4::Scaling(1.0, -1.0, 1.0).PostTranslate(0.0, 1.0, 0.0));
+ }
+
+ mMediaInfoFlag = MediaInfoFlag::None;
+ mMediaInfoFlag |= mIsHardwareAccelerated ? MediaInfoFlag::HardwareDecoding
+ : MediaInfoFlag::SoftwareDecoding;
+ if (mMimeType.EqualsLiteral("video/mp4") ||
+ mMimeType.EqualsLiteral("video/avc")) {
+ mMediaInfoFlag |= MediaInfoFlag::VIDEO_H264;
+ } else if (mMimeType.EqualsLiteral("video/vp8")) {
+ mMediaInfoFlag |= MediaInfoFlag::VIDEO_VP8;
+ } else if (mMimeType.EqualsLiteral("video/vp9")) {
+ mMediaInfoFlag |= MediaInfoFlag::VIDEO_VP9;
+ } else if (mMimeType.EqualsLiteral("video/av1")) {
+ mMediaInfoFlag |= MediaInfoFlag::VIDEO_AV1;
+ }
+ return InitPromise::CreateAndResolve(TrackInfo::kVideoTrack, __func__);
+ }
+
+ RefPtr<MediaDataDecoder::FlushPromise> Flush() override {
+ AssertOnThread();
+ mInputInfos.Clear();
+ mSeekTarget.reset();
+ mLatestOutputTime.reset();
+ mPerformanceRecorder.Record(std::numeric_limits<int64_t>::max());
+ return RemoteDataDecoder::Flush();
+ }
+
+ nsCString GetCodecName() const override {
+ if (mMediaInfoFlag & MediaInfoFlag::VIDEO_H264) {
+ return "h264"_ns;
+ } else if (mMediaInfoFlag & MediaInfoFlag::VIDEO_VP8) {
+ return "vp8"_ns;
+ } else if (mMediaInfoFlag & MediaInfoFlag::VIDEO_VP9) {
+ return "vp9"_ns;
+ } else if (mMediaInfoFlag & MediaInfoFlag::VIDEO_AV1) {
+ return "av1"_ns;
+ } else {
+ return "unknown"_ns;
+ }
+ }
+
+ RefPtr<MediaDataDecoder::DecodePromise> Decode(
+ MediaRawData* aSample) override {
+ AssertOnThread();
+
+ if (NeedsNewDecoder()) {
+ return DecodePromise::CreateAndReject(NS_ERROR_DOM_MEDIA_NEED_NEW_DECODER,
+ __func__);
+ }
+
+ const VideoInfo* config =
+ aSample->mTrackInfo ? aSample->mTrackInfo->GetAsVideoInfo() : &mConfig;
+ MOZ_ASSERT(config);
+
+ mTrackingId.apply([&](const auto& aId) {
+ MediaInfoFlag flag = mMediaInfoFlag;
+ flag |= (aSample->mKeyframe ? MediaInfoFlag::KeyFrame
+ : MediaInfoFlag::NonKeyFrame);
+ mPerformanceRecorder.Start(aSample->mTime.ToMicroseconds(),
+ "AndroidDecoder"_ns, aId, flag);
+ });
+
+ InputInfo info(aSample->mDuration.ToMicroseconds(), config->mImage,
+ config->mDisplay);
+ mInputInfos.Insert(aSample->mTime.ToMicroseconds(), info);
+ return RemoteDataDecoder::Decode(aSample);
+ }
+
+ bool SupportDecoderRecycling() const override {
+ return mIsCodecSupportAdaptivePlayback;
+ }
+
+ void SetSeekThreshold(const TimeUnit& aTime) override {
+ auto setter = [self = RefPtr{this}, aTime] {
+ if (aTime.IsValid()) {
+ self->mSeekTarget = Some(aTime);
+ } else {
+ self->mSeekTarget.reset();
+ }
+ };
+ if (mThread->IsOnCurrentThread()) {
+ setter();
+ } else {
+ nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction(
+ "RemoteVideoDecoder::SetSeekThreshold", std::move(setter));
+ nsresult rv = mThread->Dispatch(runnable.forget());
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ }
+ }
+
+ bool IsUsefulData(const RefPtr<MediaData>& aSample) override {
+ AssertOnThread();
+
+ if (mLatestOutputTime && aSample->mTime < mLatestOutputTime.value()) {
+ return false;
+ }
+
+ const TimeUnit endTime = aSample->GetEndTime();
+ if (mSeekTarget && endTime <= mSeekTarget.value()) {
+ return false;
+ }
+
+ mSeekTarget.reset();
+ mLatestOutputTime = Some(endTime);
+ return true;
+ }
+
+ bool IsHardwareAccelerated(nsACString& aFailureReason) const override {
+ return mIsHardwareAccelerated;
+ }
+
+ ConversionRequired NeedsConversion() const override {
+ return ConversionRequired::kNeedAnnexB;
+ }
+
+ private:
+ // Param and LocalRef are only valid for the duration of a JNI method call.
+ // Use GlobalRef as the parameter type to keep the Java object referenced
+ // until running.
+ void ProcessOutput(java::Sample::GlobalRef&& aSample) {
+ if (!mThread->IsOnCurrentThread()) {
+ nsresult rv =
+ mThread->Dispatch(NewRunnableMethod<java::Sample::GlobalRef&&>(
+ "RemoteVideoDecoder::ProcessOutput", this,
+ &RemoteVideoDecoder::ProcessOutput, std::move(aSample)));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ return;
+ }
+
+ AssertOnThread();
+ if (GetState() == State::SHUTDOWN) {
+ aSample->Dispose();
+ return;
+ }
+
+ UniquePtr<layers::SurfaceTextureImage::SetCurrentCallback> releaseSample(
+ new CompositeListener(mJavaDecoder, aSample));
+
+ // If our output surface has been released (due to the GPU process crashing)
+ // then request a new decoder, which will in turn allocate a new
+ // Surface. This is usually be handled by the Error() callback, but on some
+ // devices (or at least on the emulator) the java decoder does not raise an
+ // error when the Surface is released. So we raise this error here as well.
+ if (NeedsNewDecoder()) {
+ Error(MediaResult(NS_ERROR_DOM_MEDIA_NEED_NEW_DECODER,
+ RESULT_DETAIL("VideoCallBack::HandleOutput")));
+ return;
+ }
+
+ java::sdk::MediaCodec::BufferInfo::LocalRef info = aSample->Info();
+ MOZ_ASSERT(info);
+
+ int32_t flags;
+ bool ok = NS_SUCCEEDED(info->Flags(&flags));
+
+ int32_t offset;
+ ok &= NS_SUCCEEDED(info->Offset(&offset));
+
+ int32_t size;
+ ok &= NS_SUCCEEDED(info->Size(&size));
+
+ int64_t presentationTimeUs;
+ ok &= NS_SUCCEEDED(info->PresentationTimeUs(&presentationTimeUs));
+
+ if (!ok) {
+ Error(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("VideoCallBack::HandleOutput")));
+ return;
+ }
+
+ InputInfo inputInfo;
+ ok = mInputInfos.Find(presentationTimeUs, inputInfo);
+ bool isEOS = !!(flags & java::sdk::MediaCodec::BUFFER_FLAG_END_OF_STREAM);
+ if (!ok && !isEOS) {
+ // Ignore output with no corresponding input.
+ return;
+ }
+
+ if (ok && (size > 0 || presentationTimeUs >= 0)) {
+ RefPtr<layers::Image> img = new layers::SurfaceTextureImage(
+ mSurfaceHandle, inputInfo.mImageSize, false /* NOT continuous */,
+ gl::OriginPos::BottomLeft, mConfig.HasAlpha(), mTransformOverride);
+ img->AsSurfaceTextureImage()->RegisterSetCurrentCallback(
+ std::move(releaseSample));
+
+ RefPtr<VideoData> v = VideoData::CreateFromImage(
+ inputInfo.mDisplaySize, offset,
+ TimeUnit::FromMicroseconds(presentationTimeUs),
+ TimeUnit::FromMicroseconds(inputInfo.mDurationUs), img.forget(),
+ !!(flags & java::sdk::MediaCodec::BUFFER_FLAG_SYNC_FRAME),
+ TimeUnit::FromMicroseconds(presentationTimeUs));
+
+ mPerformanceRecorder.Record(presentationTimeUs, [&](DecodeStage& aStage) {
+ using Cap = java::sdk::MediaCodecInfo::CodecCapabilities;
+ using Fmt = java::sdk::MediaFormat;
+ mColorFormat.apply([&](int32_t aFormat) {
+ switch (aFormat) {
+ case Cap::COLOR_Format32bitABGR8888:
+ case Cap::COLOR_Format32bitARGB8888:
+ case Cap::COLOR_Format32bitBGRA8888:
+ case Cap::COLOR_FormatRGBAFlexible:
+ aStage.SetImageFormat(DecodeStage::RGBA32);
+ break;
+ case Cap::COLOR_Format24bitBGR888:
+ case Cap::COLOR_Format24bitRGB888:
+ case Cap::COLOR_FormatRGBFlexible:
+ aStage.SetImageFormat(DecodeStage::RGB24);
+ break;
+ case Cap::COLOR_FormatYUV411Planar:
+ case Cap::COLOR_FormatYUV411PackedPlanar:
+ case Cap::COLOR_FormatYUV420Planar:
+ case Cap::COLOR_FormatYUV420PackedPlanar:
+ case Cap::COLOR_FormatYUV420Flexible:
+ aStage.SetImageFormat(DecodeStage::YUV420P);
+ break;
+ case Cap::COLOR_FormatYUV420SemiPlanar:
+ case Cap::COLOR_FormatYUV420PackedSemiPlanar:
+ case Cap::COLOR_QCOM_FormatYUV420SemiPlanar:
+ case Cap::COLOR_TI_FormatYUV420PackedSemiPlanar:
+ aStage.SetImageFormat(DecodeStage::NV12);
+ break;
+ case Cap::COLOR_FormatYCbYCr:
+ case Cap::COLOR_FormatYCrYCb:
+ case Cap::COLOR_FormatCbYCrY:
+ case Cap::COLOR_FormatCrYCbY:
+ case Cap::COLOR_FormatYUV422Planar:
+ case Cap::COLOR_FormatYUV422PackedPlanar:
+ case Cap::COLOR_FormatYUV422Flexible:
+ aStage.SetImageFormat(DecodeStage::YUV422P);
+ break;
+ case Cap::COLOR_FormatYUV444Interleaved:
+ case Cap::COLOR_FormatYUV444Flexible:
+ aStage.SetImageFormat(DecodeStage::YUV444P);
+ break;
+ case Cap::COLOR_FormatSurface:
+ aStage.SetImageFormat(DecodeStage::ANDROID_SURFACE);
+ break;
+ /* Added in API level 33
+ case Cap::COLOR_FormatYUVP010:
+ aStage.SetImageFormat(DecodeStage::P010);
+ break;
+ */
+ default:
+ NS_WARNING(nsPrintfCString("Unhandled color format %d (0x%08x)",
+ aFormat, aFormat)
+ .get());
+ }
+ });
+ mColorRange.apply([&](int32_t aRange) {
+ switch (aRange) {
+ case Fmt::COLOR_RANGE_FULL:
+ aStage.SetColorRange(gfx::ColorRange::FULL);
+ break;
+ case Fmt::COLOR_RANGE_LIMITED:
+ aStage.SetColorRange(gfx::ColorRange::LIMITED);
+ break;
+ default:
+ NS_WARNING(nsPrintfCString("Unhandled color range %d (0x%08x)",
+ aRange, aRange)
+ .get());
+ }
+ });
+ mColorSpace.apply([&](int32_t aSpace) {
+ switch (aSpace) {
+ case Fmt::COLOR_STANDARD_BT2020:
+ aStage.SetYUVColorSpace(gfx::YUVColorSpace::BT2020);
+ break;
+ case Fmt::COLOR_STANDARD_BT601_NTSC:
+ case Fmt::COLOR_STANDARD_BT601_PAL:
+ aStage.SetYUVColorSpace(gfx::YUVColorSpace::BT601);
+ break;
+ case Fmt::COLOR_STANDARD_BT709:
+ aStage.SetYUVColorSpace(gfx::YUVColorSpace::BT709);
+ break;
+ default:
+ NS_WARNING(nsPrintfCString("Unhandled color space %d (0x%08x)",
+ aSpace, aSpace)
+ .get());
+ }
+ });
+ aStage.SetResolution(v->mImage->GetSize().Width(),
+ v->mImage->GetSize().Height());
+ });
+
+ RemoteDataDecoder::UpdateOutputStatus(std::move(v));
+ }
+
+ if (isEOS) {
+ DrainComplete();
+ }
+ }
+
+ void ProcessOutputFormatChange(int32_t aColorFormat,
+ Maybe<int32_t> aColorRange,
+ Maybe<int32_t> aColorSpace) {
+ if (!mThread->IsOnCurrentThread()) {
+ nsresult rv = mThread->Dispatch(
+ NewRunnableMethod<int32_t, Maybe<int32_t>, Maybe<int32_t>>(
+ "RemoteVideoDecoder::ProcessOutputFormatChange", this,
+ &RemoteVideoDecoder::ProcessOutputFormatChange, aColorFormat,
+ aColorRange, aColorSpace));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ return;
+ }
+
+ AssertOnThread();
+
+ mColorFormat = Some(aColorFormat);
+ mColorRange = aColorRange;
+ mColorSpace = aColorSpace;
+ }
+
+ bool NeedsNewDecoder() const override {
+ return !mSurface || mSurface->IsReleased();
+ }
+
+ const VideoInfo mConfig;
+ java::GeckoSurface::GlobalRef mSurface;
+ AndroidSurfaceTextureHandle mSurfaceHandle{};
+ // Used to override the SurfaceTexture transform on some devices where the
+ // decoder provides a buggy value.
+ Maybe<gfx::Matrix4x4> mTransformOverride;
+ // Only accessed on reader's task queue.
+ bool mIsCodecSupportAdaptivePlayback = false;
+ // Can be accessed on any thread, but only written on during init.
+ bool mIsHardwareAccelerated = false;
+ // Accessed on mThread and reader's thread. SimpleMap however is
+ // thread-safe, so it's okay to do so.
+ SimpleMap<InputInfo> mInputInfos;
+ // Only accessed on mThread.
+ Maybe<TimeUnit> mSeekTarget;
+ Maybe<TimeUnit> mLatestOutputTime;
+ Maybe<int32_t> mColorFormat;
+ Maybe<int32_t> mColorRange;
+ Maybe<int32_t> mColorSpace;
+ // Only accessed on mThread.
+ // Tracking id for the performance recorder.
+ const Maybe<TrackingId> mTrackingId;
+ // Can be accessed on any thread, but only written during init.
+ // Pre-filled decode info used by the performance recorder.
+ MediaInfoFlag mMediaInfoFlag = {};
+ // Only accessed on mThread.
+ // Records decode performance to the profiler.
+ PerformanceRecorderMulti<DecodeStage> mPerformanceRecorder;
+};
+
+class RemoteAudioDecoder final : public RemoteDataDecoder {
+ public:
+ RemoteAudioDecoder(const AudioInfo& aConfig,
+ java::sdk::MediaFormat::Param aFormat,
+ const nsString& aDrmStubId)
+ : RemoteDataDecoder(MediaData::Type::AUDIO_DATA, aConfig.mMimeType,
+ aFormat, aDrmStubId),
+ mOutputChannels(AssertedCast<int32_t>(aConfig.mChannels)),
+ mOutputSampleRate(AssertedCast<int32_t>(aConfig.mRate)) {
+ JNIEnv* const env = jni::GetEnvForThread();
+
+ bool formatHasCSD = false;
+ NS_ENSURE_SUCCESS_VOID(aFormat->ContainsKey(u"csd-0"_ns, &formatHasCSD));
+
+ // It would be nice to instead use more specific information here, but
+ // we force a byte buffer for now since this handles arbitrary codecs.
+ // TODO(bug 1768564): implement further type checking for codec data.
+ RefPtr<MediaByteBuffer> audioCodecSpecificBinaryBlob =
+ ForceGetAudioCodecSpecificBlob(aConfig.mCodecSpecificConfig);
+ if (!formatHasCSD && audioCodecSpecificBinaryBlob->Length() >= 2) {
+ jni::ByteBuffer::LocalRef buffer(env);
+ buffer = jni::ByteBuffer::New(audioCodecSpecificBinaryBlob->Elements(),
+ audioCodecSpecificBinaryBlob->Length());
+ NS_ENSURE_SUCCESS_VOID(aFormat->SetByteBuffer(u"csd-0"_ns, buffer));
+ }
+ }
+
+ RefPtr<InitPromise> Init() override {
+ mThread = GetCurrentSerialEventTarget();
+ java::sdk::MediaCodec::BufferInfo::LocalRef bufferInfo;
+ if (NS_FAILED(java::sdk::MediaCodec::BufferInfo::New(&bufferInfo)) ||
+ !bufferInfo) {
+ return InitPromise::CreateAndReject(NS_ERROR_OUT_OF_MEMORY, __func__);
+ }
+ mInputBufferInfo = bufferInfo;
+
+ // Register native methods.
+ JavaCallbacksSupport::Init();
+
+ mJavaCallbacks = java::CodecProxy::NativeCallbacks::New();
+ if (!mJavaCallbacks) {
+ return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ __func__);
+ }
+ JavaCallbacksSupport::AttachNative(
+ mJavaCallbacks, mozilla::MakeUnique<CallbacksSupport>(this));
+
+ mJavaDecoder = java::CodecProxy::Create(false, mFormat, nullptr,
+ mJavaCallbacks, mDrmStubId);
+ if (mJavaDecoder == nullptr) {
+ return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ __func__);
+ }
+
+ return InitPromise::CreateAndResolve(TrackInfo::kAudioTrack, __func__);
+ }
+
+ nsCString GetCodecName() const override {
+ if (mMimeType.EqualsLiteral("audio/mp4a-latm")) {
+ return "aac"_ns;
+ }
+ return "unknown"_ns;
+ }
+
+ RefPtr<FlushPromise> Flush() override {
+ AssertOnThread();
+ mFirstDemuxedSampleTime.reset();
+ return RemoteDataDecoder::Flush();
+ }
+
+ RefPtr<DecodePromise> Decode(MediaRawData* aSample) override {
+ AssertOnThread();
+ if (!mFirstDemuxedSampleTime) {
+ MOZ_ASSERT(aSample->mTime.IsValid());
+ mFirstDemuxedSampleTime.emplace(aSample->mTime);
+ }
+ return RemoteDataDecoder::Decode(aSample);
+ }
+
+ private:
+ class CallbacksSupport final : public JavaCallbacksSupport {
+ public:
+ explicit CallbacksSupport(RemoteAudioDecoder* aDecoder)
+ : mDecoder(aDecoder) {}
+
+ void HandleInput(int64_t aTimestamp, bool aProcessed) override {
+ mDecoder->UpdateInputStatus(aTimestamp, aProcessed);
+ }
+
+ void HandleOutput(java::Sample::Param aSample,
+ java::SampleBuffer::Param aBuffer) override {
+ MOZ_ASSERT(aBuffer, "Audio sample should have buffer");
+ // aSample will be implicitly converted into a GlobalRef.
+ mDecoder->ProcessOutput(aSample, aBuffer);
+ }
+
+ void HandleOutputFormatChanged(
+ java::sdk::MediaFormat::Param aFormat) override {
+ int32_t outputChannels = 0;
+ aFormat->GetInteger(u"channel-count"_ns, &outputChannels);
+ AudioConfig::ChannelLayout layout(outputChannels);
+ if (!layout.IsValid()) {
+ mDecoder->Error(MediaResult(
+ NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Invalid channel layout:%d", outputChannels)));
+ return;
+ }
+
+ int32_t sampleRate = 0;
+ aFormat->GetInteger(u"sample-rate"_ns, &sampleRate);
+ LOG("Audio output format changed: channels:%d sample rate:%d",
+ outputChannels, sampleRate);
+
+ mDecoder->ProcessOutputFormatChange(outputChannels, sampleRate);
+ }
+
+ void HandleError(const MediaResult& aError) override {
+ mDecoder->Error(aError);
+ }
+
+ private:
+ RemoteAudioDecoder* mDecoder;
+ };
+
+ bool IsSampleTimeSmallerThanFirstDemuxedSampleTime(int64_t aTime) const {
+ return mFirstDemuxedSampleTime->ToMicroseconds() > aTime;
+ }
+
+ bool ShouldDiscardSample(int64_t aSession) const {
+ AssertOnThread();
+ // HandleOutput() runs on Android binder thread pool and could be preempted
+ // by RemoteDateDecoder task queue. That means ProcessOutput() could be
+ // scheduled after Shutdown() or Flush(). We won't need the
+ // sample which is returned after calling Shutdown() and Flush(). We can
+ // check mFirstDemuxedSampleTime to know whether the Flush() has been
+ // called, becasue it would be reset in Flush().
+ return GetState() == State::SHUTDOWN || !mFirstDemuxedSampleTime ||
+ mSession != aSession;
+ }
+
+ // Param and LocalRef are only valid for the duration of a JNI method call.
+ // Use GlobalRef as the parameter type to keep the Java object referenced
+ // until running.
+ void ProcessOutput(java::Sample::GlobalRef&& aSample,
+ java::SampleBuffer::GlobalRef&& aBuffer) {
+ if (!mThread->IsOnCurrentThread()) {
+ nsresult rv =
+ mThread->Dispatch(NewRunnableMethod<java::Sample::GlobalRef&&,
+ java::SampleBuffer::GlobalRef&&>(
+ "RemoteAudioDecoder::ProcessOutput", this,
+ &RemoteAudioDecoder::ProcessOutput, std::move(aSample),
+ std::move(aBuffer)));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ return;
+ }
+
+ AssertOnThread();
+
+ if (ShouldDiscardSample(aSample->Session()) || !aBuffer->IsValid()) {
+ aSample->Dispose();
+ return;
+ }
+
+ RenderOrReleaseOutput autoRelease(mJavaDecoder, aSample);
+
+ java::sdk::MediaCodec::BufferInfo::LocalRef info = aSample->Info();
+ MOZ_ASSERT(info);
+
+ int32_t flags = 0;
+ bool ok = NS_SUCCEEDED(info->Flags(&flags));
+ bool isEOS = !!(flags & java::sdk::MediaCodec::BUFFER_FLAG_END_OF_STREAM);
+
+ int32_t offset;
+ ok &= NS_SUCCEEDED(info->Offset(&offset));
+
+ int64_t presentationTimeUs;
+ ok &= NS_SUCCEEDED(info->PresentationTimeUs(&presentationTimeUs));
+
+ int32_t size;
+ ok &= NS_SUCCEEDED(info->Size(&size));
+
+ if (!ok ||
+ (IsSampleTimeSmallerThanFirstDemuxedSampleTime(presentationTimeUs) &&
+ !isEOS)) {
+ Error(MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__));
+ return;
+ }
+
+ if (size > 0) {
+#ifdef MOZ_SAMPLE_TYPE_S16
+ const int32_t numSamples = size / 2;
+#else
+# error We only support 16-bit integer PCM
+#endif
+
+ AlignedAudioBuffer audio(numSamples);
+ if (!audio) {
+ Error(MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__));
+ return;
+ }
+
+ jni::ByteBuffer::LocalRef dest = jni::ByteBuffer::New(audio.get(), size);
+ aBuffer->WriteToByteBuffer(dest, offset, size);
+
+ RefPtr<AudioData> data =
+ new AudioData(0, TimeUnit::FromMicroseconds(presentationTimeUs),
+ std::move(audio), mOutputChannels, mOutputSampleRate);
+
+ UpdateOutputStatus(std::move(data));
+ }
+
+ if (isEOS) {
+ DrainComplete();
+ }
+ }
+
+ void ProcessOutputFormatChange(int32_t aChannels, int32_t aSampleRate) {
+ if (!mThread->IsOnCurrentThread()) {
+ nsresult rv = mThread->Dispatch(NewRunnableMethod<int32_t, int32_t>(
+ "RemoteAudioDecoder::ProcessOutputFormatChange", this,
+ &RemoteAudioDecoder::ProcessOutputFormatChange, aChannels,
+ aSampleRate));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ return;
+ }
+
+ AssertOnThread();
+
+ mOutputChannels = aChannels;
+ mOutputSampleRate = aSampleRate;
+ }
+
+ int32_t mOutputChannels{};
+ int32_t mOutputSampleRate{};
+ Maybe<TimeUnit> mFirstDemuxedSampleTime;
+};
+
+already_AddRefed<MediaDataDecoder> RemoteDataDecoder::CreateAudioDecoder(
+ const CreateDecoderParams& aParams, const nsString& aDrmStubId,
+ CDMProxy* aProxy) {
+ const AudioInfo& config = aParams.AudioConfig();
+ java::sdk::MediaFormat::LocalRef format;
+ NS_ENSURE_SUCCESS(
+ java::sdk::MediaFormat::CreateAudioFormat(config.mMimeType, config.mRate,
+ config.mChannels, &format),
+ nullptr);
+
+ RefPtr<MediaDataDecoder> decoder =
+ new RemoteAudioDecoder(config, format, aDrmStubId);
+ if (aProxy) {
+ decoder = new EMEMediaDataDecoderProxy(aParams, decoder.forget(), aProxy);
+ }
+ return decoder.forget();
+}
+
+already_AddRefed<MediaDataDecoder> RemoteDataDecoder::CreateVideoDecoder(
+ const CreateDecoderParams& aParams, const nsString& aDrmStubId,
+ CDMProxy* aProxy) {
+ const VideoInfo& config = aParams.VideoConfig();
+ java::sdk::MediaFormat::LocalRef format;
+ NS_ENSURE_SUCCESS(java::sdk::MediaFormat::CreateVideoFormat(
+ TranslateMimeType(config.mMimeType),
+ config.mImage.width, config.mImage.height, &format),
+ nullptr);
+
+ RefPtr<MediaDataDecoder> decoder =
+ new RemoteVideoDecoder(config, format, aDrmStubId, aParams.mTrackingId);
+ if (aProxy) {
+ decoder = new EMEMediaDataDecoderProxy(aParams, decoder.forget(), aProxy);
+ }
+ return decoder.forget();
+}
+
+RemoteDataDecoder::RemoteDataDecoder(MediaData::Type aType,
+ const nsACString& aMimeType,
+ java::sdk::MediaFormat::Param aFormat,
+ const nsString& aDrmStubId)
+ : mType(aType),
+ mMimeType(aMimeType),
+ mFormat(aFormat),
+ mDrmStubId(aDrmStubId),
+ mSession(0),
+ mNumPendingInputs(0) {}
+
+RefPtr<MediaDataDecoder::FlushPromise> RemoteDataDecoder::Flush() {
+ AssertOnThread();
+ MOZ_ASSERT(GetState() != State::SHUTDOWN);
+
+ mDecodedData = DecodedData();
+ UpdatePendingInputStatus(PendingOp::CLEAR);
+ mDecodePromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ mDrainPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ SetState(State::DRAINED);
+ mJavaDecoder->Flush();
+ return FlushPromise::CreateAndResolve(true, __func__);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> RemoteDataDecoder::Drain() {
+ AssertOnThread();
+ if (GetState() == State::SHUTDOWN) {
+ return DecodePromise::CreateAndReject(NS_ERROR_DOM_MEDIA_CANCELED,
+ __func__);
+ }
+ RefPtr<DecodePromise> p = mDrainPromise.Ensure(__func__);
+ if (GetState() == State::DRAINED) {
+ // There's no operation to perform other than returning any already
+ // decoded data.
+ ReturnDecodedData();
+ return p;
+ }
+
+ if (GetState() == State::DRAINING) {
+ // Draining operation already pending, let it complete its course.
+ return p;
+ }
+
+ SetState(State::DRAINING);
+ mInputBufferInfo->Set(0, 0, -1,
+ java::sdk::MediaCodec::BUFFER_FLAG_END_OF_STREAM);
+ mSession = mJavaDecoder->Input(nullptr, mInputBufferInfo, nullptr);
+ return p;
+}
+
+RefPtr<ShutdownPromise> RemoteDataDecoder::Shutdown() {
+ LOG("");
+ AssertOnThread();
+ SetState(State::SHUTDOWN);
+ if (mJavaDecoder) {
+ mJavaDecoder->Release();
+ mJavaDecoder = nullptr;
+ }
+
+ if (mJavaCallbacks) {
+ JavaCallbacksSupport::GetNative(mJavaCallbacks)->Cancel();
+ JavaCallbacksSupport::DisposeNative(mJavaCallbacks);
+ mJavaCallbacks = nullptr;
+ }
+
+ mFormat = nullptr;
+
+ return ShutdownPromise::CreateAndResolve(true, __func__);
+}
+
+using CryptoInfoResult =
+ Result<java::sdk::MediaCodec::CryptoInfo::LocalRef, nsresult>;
+
+static CryptoInfoResult GetCryptoInfoFromSample(const MediaRawData* aSample) {
+ const auto& cryptoObj = aSample->mCrypto;
+ java::sdk::MediaCodec::CryptoInfo::LocalRef cryptoInfo;
+
+ if (!cryptoObj.IsEncrypted()) {
+ return CryptoInfoResult(cryptoInfo);
+ }
+
+ static bool supportsCBCS = java::CodecProxy::SupportsCBCS();
+ if (cryptoObj.mCryptoScheme == CryptoScheme::Cbcs && !supportsCBCS) {
+ return CryptoInfoResult(NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR);
+ }
+
+ nsresult rv = java::sdk::MediaCodec::CryptoInfo::New(&cryptoInfo);
+ NS_ENSURE_SUCCESS(rv, CryptoInfoResult(rv));
+
+ uint32_t numSubSamples = std::min<uint32_t>(
+ cryptoObj.mPlainSizes.Length(), cryptoObj.mEncryptedSizes.Length());
+
+ uint32_t totalSubSamplesSize = 0;
+ for (const auto& size : cryptoObj.mPlainSizes) {
+ totalSubSamplesSize += size;
+ }
+ for (const auto& size : cryptoObj.mEncryptedSizes) {
+ totalSubSamplesSize += size;
+ }
+
+ // Deep copy the plain sizes so we can modify them.
+ nsTArray<uint32_t> plainSizes = cryptoObj.mPlainSizes.Clone();
+ uint32_t codecSpecificDataSize = aSample->Size() - totalSubSamplesSize;
+ // Size of codec specific data("CSD") for Android java::sdk::MediaCodec usage
+ // should be included in the 1st plain size if it exists.
+ if (codecSpecificDataSize > 0 && !plainSizes.IsEmpty()) {
+ // This shouldn't overflow as the the plain size should be UINT16_MAX at
+ // most, and the CSD should never be that large. Checked int acts like a
+ // diagnostic assert here to help catch if we ever have insane inputs.
+ CheckedUint32 newLeadingPlainSize{plainSizes[0]};
+ newLeadingPlainSize += codecSpecificDataSize;
+ plainSizes[0] = newLeadingPlainSize.value();
+ }
+
+ static const int kExpectedIVLength = 16;
+ nsTArray<uint8_t> tempIV(kExpectedIVLength);
+ jint mode;
+ switch (cryptoObj.mCryptoScheme) {
+ case CryptoScheme::None:
+ mode = java::sdk::MediaCodec::CRYPTO_MODE_UNENCRYPTED;
+ MOZ_ASSERT(cryptoObj.mIV.Length() <= kExpectedIVLength);
+ tempIV.AppendElements(cryptoObj.mIV);
+ break;
+ case CryptoScheme::Cenc:
+ mode = java::sdk::MediaCodec::CRYPTO_MODE_AES_CTR;
+ MOZ_ASSERT(cryptoObj.mIV.Length() <= kExpectedIVLength);
+ tempIV.AppendElements(cryptoObj.mIV);
+ break;
+ case CryptoScheme::Cbcs:
+ mode = java::sdk::MediaCodec::CRYPTO_MODE_AES_CBC;
+ MOZ_ASSERT(cryptoObj.mConstantIV.Length() <= kExpectedIVLength);
+ tempIV.AppendElements(cryptoObj.mConstantIV);
+ break;
+ }
+ auto tempIVLength = tempIV.Length();
+ for (size_t i = tempIVLength; i < kExpectedIVLength; i++) {
+ // Padding with 0
+ tempIV.AppendElement(0);
+ }
+
+ MOZ_ASSERT(numSubSamples <= INT32_MAX);
+ cryptoInfo->Set(static_cast<int32_t>(numSubSamples),
+ mozilla::jni::IntArray::From(plainSizes),
+ mozilla::jni::IntArray::From(cryptoObj.mEncryptedSizes),
+ mozilla::jni::ByteArray::From(cryptoObj.mKeyId),
+ mozilla::jni::ByteArray::From(tempIV), mode);
+ if (mode == java::sdk::MediaCodec::CRYPTO_MODE_AES_CBC) {
+ java::CodecProxy::SetCryptoPatternIfNeeded(
+ cryptoInfo, cryptoObj.mCryptByteBlock, cryptoObj.mSkipByteBlock);
+ }
+
+ return CryptoInfoResult(cryptoInfo);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> RemoteDataDecoder::Decode(
+ MediaRawData* aSample) {
+ AssertOnThread();
+ MOZ_ASSERT(GetState() != State::SHUTDOWN);
+ MOZ_ASSERT(aSample != nullptr);
+ jni::ByteBuffer::LocalRef bytes = jni::ByteBuffer::New(
+ const_cast<uint8_t*>(aSample->Data()), aSample->Size());
+
+ SetState(State::DRAINABLE);
+ MOZ_ASSERT(aSample->Size() <= INT32_MAX);
+ mInputBufferInfo->Set(0, static_cast<int32_t>(aSample->Size()),
+ aSample->mTime.ToMicroseconds(), 0);
+ CryptoInfoResult crypto = GetCryptoInfoFromSample(aSample);
+ if (crypto.isErr()) {
+ return DecodePromise::CreateAndReject(
+ MediaResult(crypto.unwrapErr(), __func__), __func__);
+ }
+ int64_t session =
+ mJavaDecoder->Input(bytes, mInputBufferInfo, crypto.unwrap());
+ if (session == java::CodecProxy::INVALID_SESSION) {
+ return DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__), __func__);
+ }
+ mSession = session;
+ return mDecodePromise.Ensure(__func__);
+}
+
+void RemoteDataDecoder::UpdatePendingInputStatus(PendingOp aOp) {
+ AssertOnThread();
+ switch (aOp) {
+ case PendingOp::INCREASE:
+ mNumPendingInputs++;
+ break;
+ case PendingOp::DECREASE:
+ mNumPendingInputs--;
+ break;
+ case PendingOp::CLEAR:
+ mNumPendingInputs = 0;
+ break;
+ }
+}
+
+void RemoteDataDecoder::UpdateInputStatus(int64_t aTimestamp, bool aProcessed) {
+ if (!mThread->IsOnCurrentThread()) {
+ nsresult rv = mThread->Dispatch(NewRunnableMethod<int64_t, bool>(
+ "RemoteDataDecoder::UpdateInputStatus", this,
+ &RemoteDataDecoder::UpdateInputStatus, aTimestamp, aProcessed));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ return;
+ }
+ AssertOnThread();
+ if (GetState() == State::SHUTDOWN) {
+ return;
+ }
+
+ if (!aProcessed) {
+ UpdatePendingInputStatus(PendingOp::INCREASE);
+ } else if (HasPendingInputs()) {
+ UpdatePendingInputStatus(PendingOp::DECREASE);
+ }
+
+ if (!HasPendingInputs() || // Input has been processed, request the next one.
+ !mDecodedData.IsEmpty()) { // Previous output arrived before Decode().
+ ReturnDecodedData();
+ }
+}
+
+void RemoteDataDecoder::UpdateOutputStatus(RefPtr<MediaData>&& aSample) {
+ AssertOnThread();
+ if (GetState() == State::SHUTDOWN) {
+ return;
+ }
+ if (IsUsefulData(aSample)) {
+ mDecodedData.AppendElement(std::move(aSample));
+ }
+ ReturnDecodedData();
+}
+
+void RemoteDataDecoder::ReturnDecodedData() {
+ AssertOnThread();
+ MOZ_ASSERT(GetState() != State::SHUTDOWN);
+
+ // We only want to clear mDecodedData when we have resolved the promises.
+ if (!mDecodePromise.IsEmpty()) {
+ mDecodePromise.Resolve(std::move(mDecodedData), __func__);
+ mDecodedData = DecodedData();
+ } else if (!mDrainPromise.IsEmpty() &&
+ (!mDecodedData.IsEmpty() || GetState() == State::DRAINED)) {
+ mDrainPromise.Resolve(std::move(mDecodedData), __func__);
+ mDecodedData = DecodedData();
+ }
+}
+
+void RemoteDataDecoder::DrainComplete() {
+ if (!mThread->IsOnCurrentThread()) {
+ nsresult rv = mThread->Dispatch(
+ NewRunnableMethod("RemoteDataDecoder::DrainComplete", this,
+ &RemoteDataDecoder::DrainComplete));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ return;
+ }
+ AssertOnThread();
+ if (GetState() == State::SHUTDOWN) {
+ return;
+ }
+ SetState(State::DRAINED);
+ ReturnDecodedData();
+}
+
+void RemoteDataDecoder::Error(const MediaResult& aError) {
+ if (!mThread->IsOnCurrentThread()) {
+ nsresult rv = mThread->Dispatch(NewRunnableMethod<MediaResult>(
+ "RemoteDataDecoder::Error", this, &RemoteDataDecoder::Error, aError));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ return;
+ }
+ AssertOnThread();
+ if (GetState() == State::SHUTDOWN) {
+ return;
+ }
+
+ // If we know we need a new decoder (eg because RemoteVideoDecoder's mSurface
+ // has been released due to a GPU process crash) then override the error to
+ // request a new decoder.
+ const MediaResult& error =
+ NeedsNewDecoder()
+ ? MediaResult(NS_ERROR_DOM_MEDIA_NEED_NEW_DECODER, __func__)
+ : aError;
+
+ mDecodePromise.RejectIfExists(error, __func__);
+ mDrainPromise.RejectIfExists(error, __func__);
+}
+
+} // namespace mozilla
+#undef LOG
diff --git a/dom/media/platforms/android/RemoteDataDecoder.h b/dom/media/platforms/android/RemoteDataDecoder.h
new file mode 100644
index 0000000000..5dfda4a7f5
--- /dev/null
+++ b/dom/media/platforms/android/RemoteDataDecoder.h
@@ -0,0 +1,112 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef RemoteDataDecoder_h_
+#define RemoteDataDecoder_h_
+
+#include "AndroidDecoderModule.h"
+#include "SurfaceTexture.h"
+#include "TimeUnits.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/java/CodecProxyWrappers.h"
+
+namespace mozilla {
+
+DDLoggedTypeDeclNameAndBase(RemoteDataDecoder, MediaDataDecoder);
+
+class RemoteDataDecoder : public MediaDataDecoder,
+ public DecoderDoctorLifeLogger<RemoteDataDecoder> {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RemoteDataDecoder, final);
+
+ static already_AddRefed<MediaDataDecoder> CreateAudioDecoder(
+ const CreateDecoderParams& aParams, const nsString& aDrmStubId,
+ CDMProxy* aProxy);
+
+ static already_AddRefed<MediaDataDecoder> CreateVideoDecoder(
+ const CreateDecoderParams& aParams, const nsString& aDrmStubId,
+ CDMProxy* aProxy);
+
+ RefPtr<DecodePromise> Decode(MediaRawData* aSample) override;
+ RefPtr<DecodePromise> Drain() override;
+ RefPtr<FlushPromise> Flush() override;
+ RefPtr<ShutdownPromise> Shutdown() override;
+ nsCString GetDescriptionName() const override {
+ return "android decoder (remote)"_ns;
+ }
+
+ protected:
+ virtual ~RemoteDataDecoder() = default;
+ RemoteDataDecoder(MediaData::Type aType, const nsACString& aMimeType,
+ java::sdk::MediaFormat::Param aFormat,
+ const nsString& aDrmStubId);
+
+ // Methods only called on mThread.
+ void UpdateInputStatus(int64_t aTimestamp, bool aProcessed);
+ void UpdateOutputStatus(RefPtr<MediaData>&& aSample);
+ void ReturnDecodedData();
+ void DrainComplete();
+ void Error(const MediaResult& aError);
+ void AssertOnThread() const {
+ // mThread may not be set if Init hasn't been called first.
+ MOZ_ASSERT(!mThread || mThread->IsOnCurrentThread());
+ }
+
+ enum class State { DRAINED, DRAINABLE, DRAINING, SHUTDOWN };
+ void SetState(State aState) {
+ AssertOnThread();
+ mState = aState;
+ }
+ State GetState() const {
+ AssertOnThread();
+ return mState;
+ }
+
+ // Whether the sample will be used.
+ virtual bool IsUsefulData(const RefPtr<MediaData>& aSample) { return true; }
+
+ MediaData::Type mType;
+
+ nsAutoCString mMimeType;
+ java::sdk::MediaFormat::GlobalRef mFormat;
+
+ java::CodecProxy::GlobalRef mJavaDecoder;
+ java::CodecProxy::NativeCallbacks::GlobalRef mJavaCallbacks;
+ nsString mDrmStubId;
+
+ nsCOMPtr<nsISerialEventTarget> mThread;
+
+ // Preallocated Java object used as a reusable storage for input buffer
+ // information. Contents must be changed only on mThread.
+ java::sdk::MediaCodec::BufferInfo::GlobalRef mInputBufferInfo;
+
+ // Session ID attached to samples. It is returned by CodecProxy::Input().
+ // Accessed on mThread only.
+ int64_t mSession;
+
+ private:
+ enum class PendingOp { INCREASE, DECREASE, CLEAR };
+ void UpdatePendingInputStatus(PendingOp aOp);
+ size_t HasPendingInputs() {
+ AssertOnThread();
+ return mNumPendingInputs > 0;
+ }
+
+ // Returns true if we are in a state which requires a new decoder to be
+ // created. In this case all errors will be reported as
+ // NS_ERROR_DOM_MEDIA_NEED_NEW_DECODER to avoid reporting errors as fatal when
+ // they can be fixed with a new decoder.
+ virtual bool NeedsNewDecoder() const { return false; }
+
+ // The following members must only be accessed on mThread.
+ MozPromiseHolder<DecodePromise> mDecodePromise;
+ MozPromiseHolder<DecodePromise> mDrainPromise;
+ DecodedData mDecodedData;
+ State mState = State::DRAINED;
+ size_t mNumPendingInputs;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/platforms/apple/AppleATDecoder.cpp b/dom/media/platforms/apple/AppleATDecoder.cpp
new file mode 100644
index 0000000000..13a6be3e31
--- /dev/null
+++ b/dom/media/platforms/apple/AppleATDecoder.cpp
@@ -0,0 +1,672 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AppleATDecoder.h"
+#include "Adts.h"
+#include "AppleUtils.h"
+#include "MP4Decoder.h"
+#include "MediaInfo.h"
+#include "VideoUtils.h"
+#include "mozilla/Logging.h"
+#include "mozilla/SyncRunnable.h"
+#include "mozilla/UniquePtr.h"
+#include "nsTArray.h"
+
+#define LOG(...) DDMOZ_LOG(sPDMLog, mozilla::LogLevel::Debug, __VA_ARGS__)
+#define LOGEX(_this, ...) \
+ DDMOZ_LOGEX(_this, sPDMLog, mozilla::LogLevel::Debug, __VA_ARGS__)
+#define FourCC2Str(n) \
+ ((char[5]){(char)(n >> 24), (char)(n >> 16), (char)(n >> 8), (char)(n), 0})
+
+namespace mozilla {
+
+AppleATDecoder::AppleATDecoder(const AudioInfo& aConfig)
+ : mConfig(aConfig),
+ mFileStreamError(false),
+ mConverter(nullptr),
+ mOutputFormat(),
+ mStream(nullptr),
+ mParsedFramesForAACMagicCookie(0),
+ mErrored(false) {
+ MOZ_COUNT_CTOR(AppleATDecoder);
+ LOG("Creating Apple AudioToolbox decoder");
+ LOG("Audio Decoder configuration: %s %d Hz %d channels %d bits per channel",
+ mConfig.mMimeType.get(), mConfig.mRate, mConfig.mChannels,
+ mConfig.mBitDepth);
+
+ if (mConfig.mMimeType.EqualsLiteral("audio/mpeg")) {
+ mFormatID = kAudioFormatMPEGLayer3;
+ } else if (mConfig.mMimeType.EqualsLiteral("audio/mp4a-latm")) {
+ mFormatID = kAudioFormatMPEG4AAC;
+ if (aConfig.mCodecSpecificConfig.is<AacCodecSpecificData>()) {
+ const AacCodecSpecificData& aacCodecSpecificData =
+ aConfig.mCodecSpecificConfig.as<AacCodecSpecificData>();
+ mEncoderDelay = aacCodecSpecificData.mEncoderDelayFrames;
+ mTotalMediaFrames = aacCodecSpecificData.mMediaFrameCount;
+ LOG("AppleATDecoder (aac), found encoder delay (%" PRIu32
+ ") and total frame count (%" PRIu64 ") in codec-specific side data",
+ mEncoderDelay, mTotalMediaFrames);
+ }
+ } else {
+ mFormatID = 0;
+ }
+}
+
+AppleATDecoder::~AppleATDecoder() {
+ MOZ_COUNT_DTOR(AppleATDecoder);
+ MOZ_ASSERT(!mConverter);
+}
+
+RefPtr<MediaDataDecoder::InitPromise> AppleATDecoder::Init() {
+ if (!mFormatID) {
+ return InitPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Non recognised format")),
+ __func__);
+ }
+ mThread = GetCurrentSerialEventTarget();
+
+ return InitPromise::CreateAndResolve(TrackType::kAudioTrack, __func__);
+}
+
+RefPtr<MediaDataDecoder::FlushPromise> AppleATDecoder::Flush() {
+ MOZ_ASSERT(mThread->IsOnCurrentThread());
+ LOG("Flushing AudioToolbox AAC decoder");
+ mQueuedSamples.Clear();
+ mDecodedSamples.Clear();
+
+ if (mConverter) {
+ OSStatus rv = AudioConverterReset(mConverter);
+ if (rv) {
+ LOG("Error %d resetting AudioConverter", static_cast<int>(rv));
+ }
+ }
+ if (mErrored) {
+ mParsedFramesForAACMagicCookie = 0;
+ mMagicCookie.Clear();
+ ProcessShutdown();
+ mErrored = false;
+ }
+ return FlushPromise::CreateAndResolve(true, __func__);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> AppleATDecoder::Drain() {
+ MOZ_ASSERT(mThread->IsOnCurrentThread());
+ LOG("Draining AudioToolbox AAC decoder");
+ return DecodePromise::CreateAndResolve(DecodedData(), __func__);
+}
+
+RefPtr<ShutdownPromise> AppleATDecoder::Shutdown() {
+ // mThread may not be set if Init hasn't been called first.
+ MOZ_ASSERT(!mThread || mThread->IsOnCurrentThread());
+ ProcessShutdown();
+ return ShutdownPromise::CreateAndResolve(true, __func__);
+}
+
+void AppleATDecoder::ProcessShutdown() {
+ // mThread may not be set if Init hasn't been called first.
+ MOZ_ASSERT(!mThread || mThread->IsOnCurrentThread());
+
+ if (mStream) {
+ OSStatus rv = AudioFileStreamClose(mStream);
+ if (rv) {
+ LOG("error %d disposing of AudioFileStream", static_cast<int>(rv));
+ return;
+ }
+ mStream = nullptr;
+ }
+
+ if (mConverter) {
+ LOG("Shutdown: Apple AudioToolbox AAC decoder");
+ OSStatus rv = AudioConverterDispose(mConverter);
+ if (rv) {
+ LOG("error %d disposing of AudioConverter", static_cast<int>(rv));
+ }
+ mConverter = nullptr;
+ }
+}
+
+nsCString AppleATDecoder::GetCodecName() const {
+ switch (mFormatID) {
+ case kAudioFormatMPEGLayer3:
+ return "mp3"_ns;
+ case kAudioFormatMPEG4AAC:
+ return "aac"_ns;
+ default:
+ return "unknown"_ns;
+ }
+}
+
+struct PassthroughUserData {
+ UInt32 mChannels;
+ UInt32 mDataSize;
+ const void* mData;
+ AudioStreamPacketDescription mPacket;
+};
+
+// Error value we pass through the decoder to signal that nothing
+// has gone wrong during decoding and we're done processing the packet.
+const uint32_t kNoMoreDataErr = 'MOAR';
+
+static OSStatus _PassthroughInputDataCallback(
+ AudioConverterRef aAudioConverter, UInt32* aNumDataPackets /* in/out */,
+ AudioBufferList* aData /* in/out */,
+ AudioStreamPacketDescription** aPacketDesc, void* aUserData) {
+ PassthroughUserData* userData = (PassthroughUserData*)aUserData;
+ if (!userData->mDataSize) {
+ *aNumDataPackets = 0;
+ return kNoMoreDataErr;
+ }
+
+ if (aPacketDesc) {
+ userData->mPacket.mStartOffset = 0;
+ userData->mPacket.mVariableFramesInPacket = 0;
+ userData->mPacket.mDataByteSize = userData->mDataSize;
+ *aPacketDesc = &userData->mPacket;
+ }
+
+ aData->mBuffers[0].mNumberChannels = userData->mChannels;
+ aData->mBuffers[0].mDataByteSize = userData->mDataSize;
+ aData->mBuffers[0].mData = const_cast<void*>(userData->mData);
+
+ // No more data to provide following this run.
+ userData->mDataSize = 0;
+
+ return noErr;
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> AppleATDecoder::Decode(
+ MediaRawData* aSample) {
+ MOZ_ASSERT(mThread->IsOnCurrentThread());
+ LOG("mp4 input sample pts=%s duration=%s %s %llu bytes audio",
+ aSample->mTime.ToString().get(), aSample->GetEndTime().ToString().get(),
+ aSample->mKeyframe ? " keyframe" : "",
+ (unsigned long long)aSample->Size());
+
+ MediaResult rv = NS_OK;
+ if (!mConverter) {
+ rv = SetupDecoder(aSample);
+ if (rv != NS_OK && rv != NS_ERROR_NOT_INITIALIZED) {
+ return DecodePromise::CreateAndReject(rv, __func__);
+ }
+ }
+
+ mQueuedSamples.AppendElement(aSample);
+
+ if (rv == NS_OK) {
+ for (size_t i = 0; i < mQueuedSamples.Length(); i++) {
+ rv = DecodeSample(mQueuedSamples[i]);
+ if (NS_FAILED(rv)) {
+ mErrored = true;
+ return DecodePromise::CreateAndReject(rv, __func__);
+ }
+ }
+ mQueuedSamples.Clear();
+ }
+
+ DecodedData results = std::move(mDecodedSamples);
+ mDecodedSamples = DecodedData();
+ return DecodePromise::CreateAndResolve(std::move(results), __func__);
+}
+
+MediaResult AppleATDecoder::DecodeSample(MediaRawData* aSample) {
+ MOZ_ASSERT(mThread->IsOnCurrentThread());
+
+ // Array containing the queued decoded audio frames, about to be output.
+ nsTArray<AudioDataValue> outputData;
+ UInt32 channels = mOutputFormat.mChannelsPerFrame;
+ // Pick a multiple of the frame size close to a power of two
+ // for efficient allocation. We're mainly using this decoder to decode AAC,
+ // that has packets of 1024 audio frames.
+ const uint32_t MAX_AUDIO_FRAMES = 1024;
+ const uint32_t maxDecodedSamples = MAX_AUDIO_FRAMES * channels;
+
+ // Descriptions for _decompressed_ audio packets. ignored.
+ auto packets = MakeUnique<AudioStreamPacketDescription[]>(MAX_AUDIO_FRAMES);
+
+ // This API insists on having packets spoon-fed to it from a callback.
+ // This structure exists only to pass our state.
+ PassthroughUserData userData = {channels, (UInt32)aSample->Size(),
+ aSample->Data()};
+
+ // Decompressed audio buffer
+ AlignedAudioBuffer decoded(maxDecodedSamples);
+ if (!decoded) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ do {
+ AudioBufferList decBuffer;
+ decBuffer.mNumberBuffers = 1;
+ decBuffer.mBuffers[0].mNumberChannels = channels;
+ decBuffer.mBuffers[0].mDataByteSize =
+ maxDecodedSamples * sizeof(AudioDataValue);
+ decBuffer.mBuffers[0].mData = decoded.get();
+
+ // in: the max number of packets we can handle from the decoder.
+ // out: the number of packets the decoder is actually returning.
+ UInt32 numFrames = MAX_AUDIO_FRAMES;
+
+ OSStatus rv = AudioConverterFillComplexBuffer(
+ mConverter, _PassthroughInputDataCallback, &userData,
+ &numFrames /* in/out */, &decBuffer, packets.get());
+
+ if (rv && rv != kNoMoreDataErr) {
+ LOG("Error decoding audio sample: %d\n", static_cast<int>(rv));
+ return MediaResult(
+ NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ RESULT_DETAIL("Error decoding audio sample: %d @ %s",
+ static_cast<int>(rv), aSample->mTime.ToString().get()));
+ }
+
+ if (numFrames) {
+ AudioDataValue* outputFrames = decoded.get();
+ outputData.AppendElements(outputFrames, numFrames * channels);
+ }
+
+ if (rv == kNoMoreDataErr) {
+ break;
+ }
+ } while (true);
+
+ if (outputData.IsEmpty()) {
+ return NS_OK;
+ }
+
+ size_t numFrames = outputData.Length() / channels;
+ int rate = mOutputFormat.mSampleRate;
+ media::TimeUnit duration(numFrames, rate);
+ if (!duration.IsValid()) {
+ NS_WARNING("Invalid count of accumulated audio samples");
+ return MediaResult(
+ NS_ERROR_DOM_MEDIA_OVERFLOW_ERR,
+ RESULT_DETAIL(
+ "Invalid count of accumulated audio samples: num:%llu rate:%d",
+ uint64_t(numFrames), rate));
+ }
+
+ LOG("Decoded audio packet [%s, %s] (duration: %s)\n",
+ aSample->mTime.ToString().get(), aSample->GetEndTime().ToString().get(),
+ duration.ToString().get());
+
+ AudioSampleBuffer data(outputData.Elements(), outputData.Length());
+ if (!data.Data()) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ if (mChannelLayout && !mAudioConverter) {
+ AudioConfig in(*mChannelLayout, channels, rate);
+ AudioConfig out(AudioConfig::ChannelLayout::SMPTEDefault(*mChannelLayout),
+ channels, rate);
+ mAudioConverter = MakeUnique<AudioConverter>(in, out);
+ }
+ if (mAudioConverter && mChannelLayout && mChannelLayout->IsValid()) {
+ MOZ_ASSERT(mAudioConverter->CanWorkInPlace());
+ data = mAudioConverter->Process(std::move(data));
+ }
+
+ RefPtr<AudioData> audio = new AudioData(
+ aSample->mOffset, aSample->mTime, data.Forget(), channels, rate,
+ mChannelLayout && mChannelLayout->IsValid()
+ ? mChannelLayout->Map()
+ : AudioConfig::ChannelLayout::UNKNOWN_MAP);
+ MOZ_DIAGNOSTIC_ASSERT(duration == audio->mDuration, "must be equal");
+ mDecodedSamples.AppendElement(std::move(audio));
+ return NS_OK;
+}
+
+MediaResult AppleATDecoder::GetInputAudioDescription(
+ AudioStreamBasicDescription& aDesc, const nsTArray<uint8_t>& aExtraData) {
+ MOZ_ASSERT(mThread->IsOnCurrentThread());
+
+ // Request the properties from CoreAudio using the codec magic cookie
+ AudioFormatInfo formatInfo;
+ PodZero(&formatInfo.mASBD);
+ formatInfo.mASBD.mFormatID = mFormatID;
+ if (mFormatID == kAudioFormatMPEG4AAC) {
+ formatInfo.mASBD.mFormatFlags = mConfig.mExtendedProfile;
+ }
+ formatInfo.mMagicCookieSize = aExtraData.Length();
+ formatInfo.mMagicCookie = aExtraData.Elements();
+
+ UInt32 formatListSize;
+ // Attempt to retrieve the default format using
+ // kAudioFormatProperty_FormatInfo method.
+ // This method only retrieves the FramesPerPacket information required
+ // by the decoder, which depends on the codec type and profile.
+ aDesc.mFormatID = mFormatID;
+ aDesc.mChannelsPerFrame = mConfig.mChannels;
+ aDesc.mSampleRate = mConfig.mRate;
+ UInt32 inputFormatSize = sizeof(aDesc);
+ OSStatus rv = AudioFormatGetProperty(kAudioFormatProperty_FormatInfo, 0, NULL,
+ &inputFormatSize, &aDesc);
+ if (NS_WARN_IF(rv)) {
+ return MediaResult(
+ NS_ERROR_FAILURE,
+ RESULT_DETAIL("Unable to get format info:%d", int32_t(rv)));
+ }
+
+ // If any of the methods below fail, we will return the default format as
+ // created using kAudioFormatProperty_FormatInfo above.
+ rv = AudioFormatGetPropertyInfo(kAudioFormatProperty_FormatList,
+ sizeof(formatInfo), &formatInfo,
+ &formatListSize);
+ if (rv || (formatListSize % sizeof(AudioFormatListItem))) {
+ return NS_OK;
+ }
+ size_t listCount = formatListSize / sizeof(AudioFormatListItem);
+ auto formatList = MakeUnique<AudioFormatListItem[]>(listCount);
+
+ rv = AudioFormatGetProperty(kAudioFormatProperty_FormatList,
+ sizeof(formatInfo), &formatInfo, &formatListSize,
+ formatList.get());
+ if (rv) {
+ return NS_OK;
+ }
+ LOG("found %zu available audio stream(s)",
+ formatListSize / sizeof(AudioFormatListItem));
+ // Get the index number of the first playable format.
+ // This index number will be for the highest quality layer the platform
+ // is capable of playing.
+ UInt32 itemIndex;
+ UInt32 indexSize = sizeof(itemIndex);
+ rv = AudioFormatGetProperty(kAudioFormatProperty_FirstPlayableFormatFromList,
+ formatListSize, formatList.get(), &indexSize,
+ &itemIndex);
+ if (rv) {
+ return NS_OK;
+ }
+
+ aDesc = formatList[itemIndex].mASBD;
+
+ return NS_OK;
+}
+
+AudioConfig::Channel ConvertChannelLabel(AudioChannelLabel id) {
+ switch (id) {
+ case kAudioChannelLabel_Left:
+ return AudioConfig::CHANNEL_FRONT_LEFT;
+ case kAudioChannelLabel_Right:
+ return AudioConfig::CHANNEL_FRONT_RIGHT;
+ case kAudioChannelLabel_Mono:
+ case kAudioChannelLabel_Center:
+ return AudioConfig::CHANNEL_FRONT_CENTER;
+ case kAudioChannelLabel_LFEScreen:
+ return AudioConfig::CHANNEL_LFE;
+ case kAudioChannelLabel_LeftSurround:
+ return AudioConfig::CHANNEL_SIDE_LEFT;
+ case kAudioChannelLabel_RightSurround:
+ return AudioConfig::CHANNEL_SIDE_RIGHT;
+ case kAudioChannelLabel_CenterSurround:
+ return AudioConfig::CHANNEL_BACK_CENTER;
+ case kAudioChannelLabel_RearSurroundLeft:
+ return AudioConfig::CHANNEL_BACK_LEFT;
+ case kAudioChannelLabel_RearSurroundRight:
+ return AudioConfig::CHANNEL_BACK_RIGHT;
+ default:
+ return AudioConfig::CHANNEL_INVALID;
+ }
+}
+
+// Will set mChannelLayout if a channel layout could properly be identified
+// and is supported.
+nsresult AppleATDecoder::SetupChannelLayout() {
+ MOZ_ASSERT(mThread->IsOnCurrentThread());
+
+ // Determine the channel layout.
+ UInt32 propertySize;
+ UInt32 size;
+ OSStatus status = AudioConverterGetPropertyInfo(
+ mConverter, kAudioConverterOutputChannelLayout, &propertySize, NULL);
+ if (status || !propertySize) {
+ LOG("Couldn't get channel layout property (%s)", FourCC2Str(status));
+ return NS_ERROR_FAILURE;
+ }
+
+ auto data = MakeUnique<uint8_t[]>(propertySize);
+ size = propertySize;
+ status = AudioConverterGetProperty(
+ mConverter, kAudioConverterInputChannelLayout, &size, data.get());
+ if (status || size != propertySize) {
+ LOG("Couldn't get channel layout property (%s)", FourCC2Str(status));
+ return NS_ERROR_FAILURE;
+ }
+
+ AudioChannelLayout* layout =
+ reinterpret_cast<AudioChannelLayout*>(data.get());
+ AudioChannelLayoutTag tag = layout->mChannelLayoutTag;
+
+ // if tag is kAudioChannelLayoutTag_UseChannelDescriptions then the structure
+ // directly contains the the channel layout mapping.
+ // If tag is kAudioChannelLayoutTag_UseChannelBitmap then the layout will
+ // be defined via the bitmap and can be retrieved using
+ // kAudioFormatProperty_ChannelLayoutForBitmap property.
+ // Otherwise the tag itself describes the layout.
+ if (tag != kAudioChannelLayoutTag_UseChannelDescriptions) {
+ AudioFormatPropertyID property =
+ tag == kAudioChannelLayoutTag_UseChannelBitmap
+ ? kAudioFormatProperty_ChannelLayoutForBitmap
+ : kAudioFormatProperty_ChannelLayoutForTag;
+
+ if (property == kAudioFormatProperty_ChannelLayoutForBitmap) {
+ status = AudioFormatGetPropertyInfo(
+ property, sizeof(UInt32), &layout->mChannelBitmap, &propertySize);
+ } else {
+ status = AudioFormatGetPropertyInfo(
+ property, sizeof(AudioChannelLayoutTag), &tag, &propertySize);
+ }
+ if (status || !propertySize) {
+ LOG("Couldn't get channel layout property info (%s:%s)",
+ FourCC2Str(property), FourCC2Str(status));
+ return NS_ERROR_FAILURE;
+ }
+ data = MakeUnique<uint8_t[]>(propertySize);
+ layout = reinterpret_cast<AudioChannelLayout*>(data.get());
+ size = propertySize;
+
+ if (property == kAudioFormatProperty_ChannelLayoutForBitmap) {
+ status = AudioFormatGetProperty(property, sizeof(UInt32),
+ &layout->mChannelBitmap, &size, layout);
+ } else {
+ status = AudioFormatGetProperty(property, sizeof(AudioChannelLayoutTag),
+ &tag, &size, layout);
+ }
+ if (status || size != propertySize) {
+ LOG("Couldn't get channel layout property (%s:%s)", FourCC2Str(property),
+ FourCC2Str(status));
+ return NS_ERROR_FAILURE;
+ }
+ // We have retrieved the channel layout from the tag or bitmap.
+ // We can now directly use the channel descriptions.
+ layout->mChannelLayoutTag = kAudioChannelLayoutTag_UseChannelDescriptions;
+ }
+
+ if (layout->mNumberChannelDescriptions != mOutputFormat.mChannelsPerFrame) {
+ LOG("Not matching the original channel number");
+ return NS_ERROR_FAILURE;
+ }
+
+ AutoTArray<AudioConfig::Channel, 8> channels;
+ channels.SetLength(layout->mNumberChannelDescriptions);
+ for (uint32_t i = 0; i < layout->mNumberChannelDescriptions; i++) {
+ AudioChannelLabel id = layout->mChannelDescriptions[i].mChannelLabel;
+ AudioConfig::Channel channel = ConvertChannelLabel(id);
+ channels[i] = channel;
+ }
+ mChannelLayout = MakeUnique<AudioConfig::ChannelLayout>(
+ mOutputFormat.mChannelsPerFrame, channels.Elements());
+ return NS_OK;
+}
+
+MediaResult AppleATDecoder::SetupDecoder(MediaRawData* aSample) {
+ MOZ_ASSERT(mThread->IsOnCurrentThread());
+ static const uint32_t MAX_FRAMES = 2;
+
+ if (mFormatID == kAudioFormatMPEG4AAC && mConfig.mExtendedProfile == 2 &&
+ mParsedFramesForAACMagicCookie < MAX_FRAMES) {
+ // Check for implicit SBR signalling if stream is AAC-LC
+ // This will provide us with an updated magic cookie for use with
+ // GetInputAudioDescription.
+ if (NS_SUCCEEDED(GetImplicitAACMagicCookie(aSample)) &&
+ !mMagicCookie.Length()) {
+ // nothing found yet, will try again later
+ mParsedFramesForAACMagicCookie++;
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+ // An error occurred, fallback to using default stream description
+ }
+
+ LOG("Initializing Apple AudioToolbox decoder");
+
+ // Should we try and use magic cookie data from the AAC data? We do this if
+ // - We have an AAC config &
+ // - We do not aleady have magic cookie data.
+ // Otherwise we just use the existing cookie (which may be empty).
+ bool shouldUseAacMagicCookie =
+ mConfig.mCodecSpecificConfig.is<AacCodecSpecificData>() &&
+ mMagicCookie.IsEmpty();
+
+ nsTArray<uint8_t>& magicCookie =
+ shouldUseAacMagicCookie
+ ? *mConfig.mCodecSpecificConfig.as<AacCodecSpecificData>()
+ .mEsDescriptorBinaryBlob
+ : mMagicCookie;
+ AudioStreamBasicDescription inputFormat;
+ PodZero(&inputFormat);
+
+ MediaResult rv = GetInputAudioDescription(inputFormat, magicCookie);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ // Fill in the output format manually.
+ PodZero(&mOutputFormat);
+ mOutputFormat.mFormatID = kAudioFormatLinearPCM;
+ mOutputFormat.mSampleRate = inputFormat.mSampleRate;
+ mOutputFormat.mChannelsPerFrame = inputFormat.mChannelsPerFrame;
+#if defined(MOZ_SAMPLE_TYPE_FLOAT32)
+ mOutputFormat.mBitsPerChannel = 32;
+ mOutputFormat.mFormatFlags = kLinearPCMFormatFlagIsFloat | 0;
+#elif defined(MOZ_SAMPLE_TYPE_S16)
+ mOutputFormat.mBitsPerChannel = 16;
+ mOutputFormat.mFormatFlags = kLinearPCMFormatFlagIsSignedInteger | 0;
+#else
+# error Unknown audio sample type
+#endif
+ // Set up the decoder so it gives us one sample per frame
+ mOutputFormat.mFramesPerPacket = 1;
+ mOutputFormat.mBytesPerPacket = mOutputFormat.mBytesPerFrame =
+ mOutputFormat.mChannelsPerFrame * mOutputFormat.mBitsPerChannel / 8;
+
+ OSStatus status =
+ AudioConverterNew(&inputFormat, &mOutputFormat, &mConverter);
+ if (status) {
+ LOG("Error %d constructing AudioConverter", int(status));
+ mConverter = nullptr;
+ return MediaResult(
+ NS_ERROR_FAILURE,
+ RESULT_DETAIL("Error constructing AudioConverter:%d", int32_t(status)));
+ }
+
+ if (magicCookie.Length() && mFormatID == kAudioFormatMPEG4AAC) {
+ status = AudioConverterSetProperty(
+ mConverter, kAudioConverterDecompressionMagicCookie,
+ magicCookie.Length(), magicCookie.Elements());
+ if (status) {
+ LOG("Error setting AudioConverter AAC cookie:%d", int32_t(status));
+ ProcessShutdown();
+ return MediaResult(
+ NS_ERROR_FAILURE,
+ RESULT_DETAIL("Error setting AudioConverter AAC cookie:%d",
+ int32_t(status)));
+ }
+ }
+
+ if (NS_FAILED(SetupChannelLayout())) {
+ NS_WARNING("Couldn't retrieve channel layout, will use default layout");
+ }
+
+ return NS_OK;
+}
+
+static void _MetadataCallback(void* aAppleATDecoder, AudioFileStreamID aStream,
+ AudioFileStreamPropertyID aProperty,
+ UInt32* aFlags) {
+ AppleATDecoder* decoder = static_cast<AppleATDecoder*>(aAppleATDecoder);
+ MOZ_RELEASE_ASSERT(decoder->mThread->IsOnCurrentThread());
+
+ LOGEX(decoder, "MetadataCallback receiving: '%s'", FourCC2Str(aProperty));
+ if (aProperty == kAudioFileStreamProperty_MagicCookieData) {
+ UInt32 size;
+ Boolean writeable;
+ OSStatus rv =
+ AudioFileStreamGetPropertyInfo(aStream, aProperty, &size, &writeable);
+ if (rv) {
+ LOGEX(decoder, "Couldn't get property info for '%s' (%s)",
+ FourCC2Str(aProperty), FourCC2Str(rv));
+ decoder->mFileStreamError = true;
+ return;
+ }
+ auto data = MakeUnique<uint8_t[]>(size);
+ rv = AudioFileStreamGetProperty(aStream, aProperty, &size, data.get());
+ if (rv) {
+ LOGEX(decoder, "Couldn't get property '%s' (%s)", FourCC2Str(aProperty),
+ FourCC2Str(rv));
+ decoder->mFileStreamError = true;
+ return;
+ }
+ decoder->mMagicCookie.AppendElements(data.get(), size);
+ }
+}
+
+static void _SampleCallback(void* aSBR, UInt32 aNumBytes, UInt32 aNumPackets,
+ const void* aData,
+ AudioStreamPacketDescription* aPackets) {}
+
+nsresult AppleATDecoder::GetImplicitAACMagicCookie(
+ const MediaRawData* aSample) {
+ MOZ_ASSERT(mThread->IsOnCurrentThread());
+
+ // Prepend ADTS header to AAC audio.
+ RefPtr<MediaRawData> adtssample(aSample->Clone());
+ if (!adtssample) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ int8_t frequency_index = Adts::GetFrequencyIndex(mConfig.mRate);
+
+ bool rv = Adts::ConvertSample(mConfig.mChannels, frequency_index,
+ mConfig.mProfile, adtssample);
+ if (!rv) {
+ NS_WARNING("Failed to apply ADTS header");
+ return NS_ERROR_FAILURE;
+ }
+ if (!mStream) {
+ OSStatus rv = AudioFileStreamOpen(this, _MetadataCallback, _SampleCallback,
+ kAudioFileAAC_ADTSType, &mStream);
+ if (rv) {
+ NS_WARNING("Couldn't open AudioFileStream");
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ OSStatus status = AudioFileStreamParseBytes(
+ mStream, adtssample->Size(), adtssample->Data(), 0 /* discontinuity */);
+ if (status) {
+ NS_WARNING("Couldn't parse sample");
+ }
+
+ if (status || mFileStreamError || mMagicCookie.Length()) {
+ // We have decoded a magic cookie or an error occurred as such
+ // we won't need the stream any longer.
+ AudioFileStreamClose(mStream);
+ mStream = nullptr;
+ }
+
+ return (mFileStreamError || status) ? NS_ERROR_FAILURE : NS_OK;
+}
+
+} // namespace mozilla
+
+#undef LOG
+#undef LOGEX
diff --git a/dom/media/platforms/apple/AppleATDecoder.h b/dom/media/platforms/apple/AppleATDecoder.h
new file mode 100644
index 0000000000..d7aba2aacb
--- /dev/null
+++ b/dom/media/platforms/apple/AppleATDecoder.h
@@ -0,0 +1,80 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_AppleATDecoder_h
+#define mozilla_AppleATDecoder_h
+
+#include <AudioToolbox/AudioToolbox.h>
+#include "PlatformDecoderModule.h"
+#include "mozilla/Vector.h"
+#include "AudioConverter.h"
+
+namespace mozilla {
+
+class TaskQueue;
+
+DDLoggedTypeDeclNameAndBase(AppleATDecoder, MediaDataDecoder);
+
+class AppleATDecoder final : public MediaDataDecoder,
+ public DecoderDoctorLifeLogger<AppleATDecoder> {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AppleATDecoder, final);
+
+ explicit AppleATDecoder(const AudioInfo& aConfig);
+
+ RefPtr<InitPromise> Init() override;
+ RefPtr<DecodePromise> Decode(MediaRawData* aSample) override;
+ RefPtr<DecodePromise> Drain() override;
+ RefPtr<FlushPromise> Flush() override;
+ RefPtr<ShutdownPromise> Shutdown() override;
+
+ nsCString GetDescriptionName() const override {
+ return "apple coremedia decoder"_ns;
+ }
+
+ nsCString GetCodecName() const override;
+
+ // Callbacks also need access to the config.
+ const AudioInfo mConfig;
+
+ // Use to extract magic cookie for HE-AAC detection.
+ nsTArray<uint8_t> mMagicCookie;
+ // Will be set to true should an error occurred while attempting to retrieve
+ // the magic cookie property.
+ bool mFileStreamError;
+
+ nsCOMPtr<nsISerialEventTarget> mThread;
+
+ private:
+ ~AppleATDecoder();
+
+ AudioConverterRef mConverter;
+ AudioStreamBasicDescription mOutputFormat;
+ UInt32 mFormatID;
+ AudioFileStreamID mStream;
+ nsTArray<RefPtr<MediaRawData>> mQueuedSamples;
+ UniquePtr<AudioConfig::ChannelLayout> mChannelLayout;
+ UniquePtr<AudioConverter> mAudioConverter;
+ DecodedData mDecodedSamples;
+
+ void ProcessShutdown();
+ MediaResult DecodeSample(MediaRawData* aSample);
+ MediaResult GetInputAudioDescription(AudioStreamBasicDescription& aDesc,
+ const nsTArray<uint8_t>& aExtraData);
+ // Setup AudioConverter once all information required has been gathered.
+ // Will return NS_ERROR_NOT_INITIALIZED if more data is required.
+ MediaResult SetupDecoder(MediaRawData* aSample);
+ nsresult GetImplicitAACMagicCookie(const MediaRawData* aSample);
+ nsresult SetupChannelLayout();
+ uint32_t mParsedFramesForAACMagicCookie;
+ uint32_t mEncoderDelay = 0;
+ uint64_t mTotalMediaFrames = 0;
+ bool mErrored;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_AppleATDecoder_h
diff --git a/dom/media/platforms/apple/AppleDecoderModule.cpp b/dom/media/platforms/apple/AppleDecoderModule.cpp
new file mode 100644
index 0000000000..2e17d93313
--- /dev/null
+++ b/dom/media/platforms/apple/AppleDecoderModule.cpp
@@ -0,0 +1,230 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AppleDecoderModule.h"
+
+#include <dlfcn.h>
+
+#include "AppleATDecoder.h"
+#include "AppleVTDecoder.h"
+#include "MP4Decoder.h"
+#include "VideoUtils.h"
+#include "VPXDecoder.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/Logging.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/gfx/gfxVars.h"
+
+extern "C" {
+// Only exists from MacOS 11
+extern void VTRegisterSupplementalVideoDecoderIfAvailable(
+ CMVideoCodecType codecType) __attribute__((weak_import));
+extern Boolean VTIsHardwareDecodeSupported(CMVideoCodecType codecType)
+ __attribute__((weak_import));
+}
+
+namespace mozilla {
+
+bool AppleDecoderModule::sInitialized = false;
+bool AppleDecoderModule::sCanUseVP9Decoder = false;
+
+/* static */
+void AppleDecoderModule::Init() {
+ if (sInitialized) {
+ return;
+ }
+
+ sInitialized = true;
+ if (RegisterSupplementalVP9Decoder()) {
+ sCanUseVP9Decoder = CanCreateHWDecoder(media::MediaCodec::VP9);
+ }
+}
+
+nsresult AppleDecoderModule::Startup() {
+ if (!sInitialized) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+already_AddRefed<MediaDataDecoder> AppleDecoderModule::CreateVideoDecoder(
+ const CreateDecoderParams& aParams) {
+ if (Supports(SupportDecoderParams(aParams), nullptr /* diagnostics */) ==
+ media::DecodeSupport::Unsupported) {
+ return nullptr;
+ }
+ RefPtr<MediaDataDecoder> decoder;
+ if (IsVideoSupported(aParams.VideoConfig(), aParams.mOptions)) {
+ decoder = new AppleVTDecoder(aParams.VideoConfig(), aParams.mImageContainer,
+ aParams.mOptions, aParams.mKnowsCompositor,
+ aParams.mTrackingId);
+ }
+ return decoder.forget();
+}
+
+already_AddRefed<MediaDataDecoder> AppleDecoderModule::CreateAudioDecoder(
+ const CreateDecoderParams& aParams) {
+ if (Supports(SupportDecoderParams(aParams), nullptr /* diagnostics */) ==
+ media::DecodeSupport::Unsupported) {
+ return nullptr;
+ }
+ RefPtr<MediaDataDecoder> decoder = new AppleATDecoder(aParams.AudioConfig());
+ return decoder.forget();
+}
+
+media::DecodeSupportSet AppleDecoderModule::SupportsMimeType(
+ const nsACString& aMimeType, DecoderDoctorDiagnostics* aDiagnostics) const {
+ bool checkSupport = (aMimeType.EqualsLiteral("audio/mpeg") &&
+ !StaticPrefs::media_ffvpx_mp3_enabled()) ||
+ aMimeType.EqualsLiteral("audio/mp4a-latm") ||
+ MP4Decoder::IsH264(aMimeType) ||
+ VPXDecoder::IsVP9(aMimeType);
+ media::DecodeSupportSet supportType{media::DecodeSupport::Unsupported};
+
+ if (checkSupport) {
+ UniquePtr<TrackInfo> trackInfo = CreateTrackInfoWithMIMEType(aMimeType);
+ if (!trackInfo) {
+ supportType = media::DecodeSupport::Unsupported;
+ } else if (trackInfo->IsAudio()) {
+ supportType = media::DecodeSupport::SoftwareDecode;
+ } else {
+ supportType = Supports(SupportDecoderParams(*trackInfo), aDiagnostics);
+ }
+ }
+
+ MOZ_LOG(sPDMLog, LogLevel::Debug,
+ ("Apple decoder %s requested type '%s'",
+ supportType == media::DecodeSupport::Unsupported ? "rejects"
+ : "supports",
+ aMimeType.BeginReading()));
+ return supportType;
+}
+
+media::DecodeSupportSet AppleDecoderModule::Supports(
+ const SupportDecoderParams& aParams,
+ DecoderDoctorDiagnostics* aDiagnostics) const {
+ const auto& trackInfo = aParams.mConfig;
+ if (trackInfo.IsAudio()) {
+ return SupportsMimeType(trackInfo.mMimeType, aDiagnostics);
+ }
+ bool checkSupport = trackInfo.GetAsVideoInfo() &&
+ IsVideoSupported(*trackInfo.GetAsVideoInfo());
+ if (checkSupport) {
+ if (trackInfo.mMimeType == "video/vp9" &&
+ CanCreateHWDecoder(media::MediaCodec::VP9)) {
+ return media::DecodeSupport::HardwareDecode;
+ }
+ return media::DecodeSupport::SoftwareDecode;
+ }
+ return media::DecodeSupport::Unsupported;
+}
+
+bool AppleDecoderModule::IsVideoSupported(
+ const VideoInfo& aConfig,
+ const CreateDecoderParams::OptionSet& aOptions) const {
+ if (MP4Decoder::IsH264(aConfig.mMimeType)) {
+ return true;
+ }
+ if (!VPXDecoder::IsVP9(aConfig.mMimeType) || !sCanUseVP9Decoder ||
+ aOptions.contains(
+ CreateDecoderParams::Option::HardwareDecoderNotAllowed)) {
+ return false;
+ }
+ if (aConfig.HasAlpha()) {
+ return false;
+ }
+
+ // HW VP9 decoder only supports 8 or 10 bit color.
+ if (aConfig.mColorDepth != gfx::ColorDepth::COLOR_8 &&
+ aConfig.mColorDepth != gfx::ColorDepth::COLOR_10) {
+ return false;
+ }
+
+ // See if we have a vpcC box, and check further constraints.
+ // HW VP9 Decoder supports Profile 0 & 2 (YUV420)
+ if (aConfig.mExtraData && aConfig.mExtraData->Length() < 5) {
+ return true; // Assume it's okay.
+ }
+ int profile = aConfig.mExtraData->ElementAt(4);
+
+ if (profile != 0 && profile != 2) {
+ return false;
+ }
+
+ return true;
+}
+
+/* static */
+bool AppleDecoderModule::CanCreateHWDecoder(media::MediaCodec aCodec) {
+ // Check whether HW decode should even be enabled
+ if (!gfx::gfxVars::CanUseHardwareVideoDecoding()) {
+ return false;
+ }
+
+ VideoInfo info(1920, 1080);
+ bool checkSupport = false;
+
+ // We must wrap the code within __builtin_available to avoid compilation
+ // warning as VTIsHardwareDecodeSupported is only available from macOS 10.13.
+ if (__builtin_available(macOS 10.13, *)) {
+ if (!VTIsHardwareDecodeSupported) {
+ return false;
+ }
+ switch (aCodec) {
+ case media::MediaCodec::VP9:
+ info.mMimeType = "video/vp9";
+ VPXDecoder::GetVPCCBox(info.mExtraData, VPXDecoder::VPXStreamInfo());
+ checkSupport = VTIsHardwareDecodeSupported(kCMVideoCodecType_VP9);
+ break;
+ default:
+ // Only support VP9 HW decode for time being
+ checkSupport = false;
+ break;
+ }
+ }
+ // Attempt to create decoder
+ if (checkSupport) {
+ RefPtr<AppleVTDecoder> decoder =
+ new AppleVTDecoder(info, nullptr, {}, nullptr, Nothing());
+ MediaResult rv = decoder->InitializeSession();
+ if (!NS_SUCCEEDED(rv)) {
+ return false;
+ }
+ nsAutoCString failureReason;
+ bool hwSupport = decoder->IsHardwareAccelerated(failureReason);
+ decoder->Shutdown();
+ if (!hwSupport) {
+ MOZ_LOG(sPDMLog, LogLevel::Debug,
+ ("Apple HW decode failure: '%s'", failureReason.BeginReading()));
+ }
+ return hwSupport;
+ }
+ return false;
+}
+
+/* static */
+bool AppleDecoderModule::RegisterSupplementalVP9Decoder() {
+ static bool sRegisterIfAvailable = []() {
+#if !defined(MAC_OS_VERSION_11_0) || \
+ MAC_OS_X_VERSION_MAX_ALLOWED < MAC_OS_VERSION_11_0
+ if (nsCocoaFeatures::OnBigSurOrLater()) {
+#else
+ if (__builtin_available(macos 11.0, *)) {
+#endif
+ VTRegisterSupplementalVideoDecoderIfAvailable(kCMVideoCodecType_VP9);
+ return true;
+ }
+ return false;
+ }();
+ return sRegisterIfAvailable;
+}
+
+/* static */
+already_AddRefed<PlatformDecoderModule> AppleDecoderModule::Create() {
+ return MakeAndAddRef<AppleDecoderModule>();
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/apple/AppleDecoderModule.h b/dom/media/platforms/apple/AppleDecoderModule.h
new file mode 100644
index 0000000000..f869243a5c
--- /dev/null
+++ b/dom/media/platforms/apple/AppleDecoderModule.h
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_AppleDecoderModule_h
+#define mozilla_AppleDecoderModule_h
+
+#include "PlatformDecoderModule.h"
+
+namespace mozilla {
+
+class AppleDecoderModule : public PlatformDecoderModule {
+ template <typename T, typename... Args>
+ friend already_AddRefed<T> MakeAndAddRef(Args&&...);
+
+ public:
+ static already_AddRefed<PlatformDecoderModule> Create();
+
+ nsresult Startup() override;
+
+ // Decode thread.
+ already_AddRefed<MediaDataDecoder> CreateVideoDecoder(
+ const CreateDecoderParams& aParams) override;
+
+ // Decode thread.
+ already_AddRefed<MediaDataDecoder> CreateAudioDecoder(
+ const CreateDecoderParams& aParams) override;
+
+ media::DecodeSupportSet SupportsMimeType(
+ const nsACString& aMimeType,
+ DecoderDoctorDiagnostics* aDiagnostics) const override;
+
+ media::DecodeSupportSet Supports(
+ const SupportDecoderParams& aParams,
+ DecoderDoctorDiagnostics* aDiagnostics) const override;
+
+ static void Init();
+
+ static bool sCanUseVP9Decoder;
+
+ static constexpr int kCMVideoCodecType_H264{'avc1'};
+ static constexpr int kCMVideoCodecType_VP9{'vp09'};
+
+ private:
+ AppleDecoderModule() = default;
+ virtual ~AppleDecoderModule() = default;
+
+ static bool sInitialized;
+ bool IsVideoSupported(const VideoInfo& aConfig,
+ const CreateDecoderParams::OptionSet& aOptions =
+ CreateDecoderParams::OptionSet()) const;
+ // Enable VP9 HW decoder.
+ static bool RegisterSupplementalVP9Decoder();
+ // Return true if a dummy hardware decoder could be created.
+ static bool CanCreateHWDecoder(media::MediaCodec aCodec);
+};
+
+} // namespace mozilla
+
+#endif // mozilla_AppleDecoderModule_h
diff --git a/dom/media/platforms/apple/AppleEncoderModule.cpp b/dom/media/platforms/apple/AppleEncoderModule.cpp
new file mode 100644
index 0000000000..f0321297a4
--- /dev/null
+++ b/dom/media/platforms/apple/AppleEncoderModule.cpp
@@ -0,0 +1,25 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AppleEncoderModule.h"
+
+#include "AppleVTEncoder.h"
+#include "MP4Decoder.h"
+
+namespace mozilla {
+
+bool AppleEncoderModule::SupportsMimeType(const nsACString& aMimeType) const {
+ return MP4Decoder::IsH264(aMimeType);
+}
+
+already_AddRefed<MediaDataEncoder> AppleEncoderModule::CreateVideoEncoder(
+ const CreateEncoderParams& aParams, const bool aHardwareNotAllowed) const {
+ RefPtr<MediaDataEncoder> encoder(new AppleVTEncoder(
+ aParams.ToH264Config(), aParams.mTaskQueue, aHardwareNotAllowed));
+ return encoder.forget();
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/apple/AppleEncoderModule.h b/dom/media/platforms/apple/AppleEncoderModule.h
new file mode 100644
index 0000000000..ec2868f104
--- /dev/null
+++ b/dom/media/platforms/apple/AppleEncoderModule.h
@@ -0,0 +1,27 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef AppleEncoderModule_h_
+#define AppleEncoderModule_h_
+
+#include "PlatformEncoderModule.h"
+
+namespace mozilla {
+class AppleEncoderModule final : public PlatformEncoderModule {
+ public:
+ AppleEncoderModule() {}
+ virtual ~AppleEncoderModule() {}
+
+ bool SupportsMimeType(const nsACString& aMimeType) const override;
+
+ already_AddRefed<MediaDataEncoder> CreateVideoEncoder(
+ const CreateEncoderParams& aParams,
+ const bool aHardwareNotAllowed) const override;
+};
+
+} // namespace mozilla
+
+#endif /* AppleEncoderModule_h_ */
diff --git a/dom/media/platforms/apple/AppleUtils.h b/dom/media/platforms/apple/AppleUtils.h
new file mode 100644
index 0000000000..96bf079b0c
--- /dev/null
+++ b/dom/media/platforms/apple/AppleUtils.h
@@ -0,0 +1,88 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Utility functions to help with Apple API calls.
+
+#ifndef mozilla_AppleUtils_h
+#define mozilla_AppleUtils_h
+
+#include "mozilla/Attributes.h"
+#include <CoreFoundation/CFBase.h> // For CFRelease()
+#include <CoreVideo/CVBuffer.h> // For CVBufferRelease()
+
+namespace mozilla {
+
+// Wrapper class to call CFRelease/CVBufferRelease on reference types
+// when they go out of scope.
+template <class T, class F, F relFunc>
+class AutoObjRefRelease {
+ public:
+ MOZ_IMPLICIT AutoObjRefRelease(T aRef) : mRef(aRef) {}
+ ~AutoObjRefRelease() {
+ if (mRef) {
+ relFunc(mRef);
+ }
+ }
+ // Return the wrapped ref so it can be used as an in parameter.
+ operator T() { return mRef; }
+ // Return a pointer to the wrapped ref for use as an out parameter.
+ T* receive() { return &mRef; }
+
+ private:
+ // Copy operator isn't supported and is not implemented.
+ AutoObjRefRelease<T, F, relFunc>& operator=(
+ const AutoObjRefRelease<T, F, relFunc>&);
+ T mRef;
+};
+
+template <typename T>
+using AutoCFRelease = AutoObjRefRelease<T, decltype(&CFRelease), &CFRelease>;
+template <typename T>
+using AutoCVBufferRelease =
+ AutoObjRefRelease<T, decltype(&CVBufferRelease), &CVBufferRelease>;
+
+// CFRefPtr: A CoreFoundation smart pointer.
+template <class T>
+class CFRefPtr {
+ public:
+ explicit CFRefPtr(T aRef) : mRef(aRef) {
+ if (mRef) {
+ CFRetain(mRef);
+ }
+ }
+ // Copy constructor.
+ CFRefPtr(const CFRefPtr<T>& aCFRefPtr) : mRef(aCFRefPtr.mRef) {
+ if (mRef) {
+ CFRetain(mRef);
+ }
+ }
+ // Copy operator
+ CFRefPtr<T>& operator=(const CFRefPtr<T>& aCFRefPtr) {
+ if (mRef == aCFRefPtr.mRef) {
+ return;
+ }
+ if (mRef) {
+ CFRelease(mRef);
+ }
+ mRef = aCFRefPtr.mRef;
+ if (mRef) {
+ CFRetain(mRef);
+ }
+ return *this;
+ }
+ ~CFRefPtr() {
+ if (mRef) {
+ CFRelease(mRef);
+ }
+ }
+ // Return the wrapped ref so it can be used as an in parameter.
+ operator T() { return mRef; }
+
+ private:
+ T mRef;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_AppleUtils_h
diff --git a/dom/media/platforms/apple/AppleVTDecoder.cpp b/dom/media/platforms/apple/AppleVTDecoder.cpp
new file mode 100644
index 0000000000..7abc46274b
--- /dev/null
+++ b/dom/media/platforms/apple/AppleVTDecoder.cpp
@@ -0,0 +1,761 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AppleVTDecoder.h"
+
+#include <CoreVideo/CVPixelBufferIOSurface.h>
+#include <IOSurface/IOSurface.h>
+#include <limits>
+
+#include "AppleDecoderModule.h"
+#include "AppleUtils.h"
+#include "CallbackThreadRegistry.h"
+#include "H264.h"
+#include "MP4Decoder.h"
+#include "MacIOSurfaceImage.h"
+#include "MediaData.h"
+#include "VPXDecoder.h"
+#include "VideoUtils.h"
+#include "gfxMacUtils.h"
+#include "gfxPlatform.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Logging.h"
+#include "mozilla/TaskQueue.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "nsThreadUtils.h"
+
+#define LOG(...) DDMOZ_LOG(sPDMLog, mozilla::LogLevel::Debug, __VA_ARGS__)
+#define LOGEX(_this, ...) \
+ DDMOZ_LOGEX(_this, sPDMLog, mozilla::LogLevel::Debug, __VA_ARGS__)
+
+namespace mozilla {
+
+using namespace layers;
+
+AppleVTDecoder::AppleVTDecoder(const VideoInfo& aConfig,
+ layers::ImageContainer* aImageContainer,
+ CreateDecoderParams::OptionSet aOptions,
+ layers::KnowsCompositor* aKnowsCompositor,
+ Maybe<TrackingId> aTrackingId)
+ : mExtraData(aConfig.mExtraData),
+ mPictureWidth(aConfig.mImage.width),
+ mPictureHeight(aConfig.mImage.height),
+ mDisplayWidth(aConfig.mDisplay.width),
+ mDisplayHeight(aConfig.mDisplay.height),
+ mColorSpace(aConfig.mColorSpace
+ ? *aConfig.mColorSpace
+ : DefaultColorSpace({mPictureWidth, mPictureHeight})),
+ mColorPrimaries(aConfig.mColorPrimaries ? *aConfig.mColorPrimaries
+ : gfx::ColorSpace2::BT709),
+ mTransferFunction(aConfig.mTransferFunction
+ ? *aConfig.mTransferFunction
+ : gfx::TransferFunction::BT709),
+ mColorRange(aConfig.mColorRange),
+ mColorDepth(aConfig.mColorDepth),
+ mStreamType(MP4Decoder::IsH264(aConfig.mMimeType) ? StreamType::H264
+ : VPXDecoder::IsVP9(aConfig.mMimeType) ? StreamType::VP9
+ : StreamType::Unknown),
+ mTaskQueue(TaskQueue::Create(
+ GetMediaThreadPool(MediaThreadType::PLATFORM_DECODER),
+ "AppleVTDecoder")),
+ mMaxRefFrames(
+ mStreamType != StreamType::H264 ||
+ aOptions.contains(CreateDecoderParams::Option::LowLatency)
+ ? 0
+ : H264::ComputeMaxRefFrames(aConfig.mExtraData)),
+ mImageContainer(aImageContainer),
+ mKnowsCompositor(aKnowsCompositor)
+#ifdef MOZ_WIDGET_UIKIT
+ ,
+ mUseSoftwareImages(true)
+#else
+ ,
+ mUseSoftwareImages(aKnowsCompositor &&
+ aKnowsCompositor->GetWebRenderCompositorType() ==
+ layers::WebRenderCompositor::SOFTWARE)
+#endif
+ ,
+ mTrackingId(aTrackingId),
+ mIsFlushing(false),
+ mCallbackThreadId(),
+ mMonitor("AppleVTDecoder"),
+ mPromise(&mMonitor), // To ensure our PromiseHolder is only ever accessed
+ // with the monitor held.
+ mFormat(nullptr),
+ mSession(nullptr),
+ mIsHardwareAccelerated(false) {
+ MOZ_COUNT_CTOR(AppleVTDecoder);
+ MOZ_ASSERT(mStreamType != StreamType::Unknown);
+ // TODO: Verify aConfig.mime_type.
+ LOG("Creating AppleVTDecoder for %dx%d %s video", mDisplayWidth,
+ mDisplayHeight, mStreamType == StreamType::H264 ? "H.264" : "VP9");
+}
+
+AppleVTDecoder::~AppleVTDecoder() { MOZ_COUNT_DTOR(AppleVTDecoder); }
+
+RefPtr<MediaDataDecoder::InitPromise> AppleVTDecoder::Init() {
+ MediaResult rv = InitializeSession();
+
+ if (NS_SUCCEEDED(rv)) {
+ return InitPromise::CreateAndResolve(TrackType::kVideoTrack, __func__);
+ }
+
+ return InitPromise::CreateAndReject(rv, __func__);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> AppleVTDecoder::Decode(
+ MediaRawData* aSample) {
+ LOG("mp4 input sample %p pts %lld duration %lld us%s %zu bytes", aSample,
+ aSample->mTime.ToMicroseconds(), aSample->mDuration.ToMicroseconds(),
+ aSample->mKeyframe ? " keyframe" : "", aSample->Size());
+
+ RefPtr<AppleVTDecoder> self = this;
+ RefPtr<MediaRawData> sample = aSample;
+ return InvokeAsync(mTaskQueue, __func__, [self, this, sample] {
+ RefPtr<DecodePromise> p;
+ {
+ MonitorAutoLock mon(mMonitor);
+ p = mPromise.Ensure(__func__);
+ }
+ ProcessDecode(sample);
+ return p;
+ });
+}
+
+RefPtr<MediaDataDecoder::FlushPromise> AppleVTDecoder::Flush() {
+ mIsFlushing = true;
+ return InvokeAsync(mTaskQueue, this, __func__, &AppleVTDecoder::ProcessFlush);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> AppleVTDecoder::Drain() {
+ return InvokeAsync(mTaskQueue, this, __func__, &AppleVTDecoder::ProcessDrain);
+}
+
+RefPtr<ShutdownPromise> AppleVTDecoder::Shutdown() {
+ RefPtr<AppleVTDecoder> self = this;
+ return InvokeAsync(mTaskQueue, __func__, [self]() {
+ self->ProcessShutdown();
+ return self->mTaskQueue->BeginShutdown();
+ });
+}
+
+// Helper to fill in a timestamp structure.
+static CMSampleTimingInfo TimingInfoFromSample(MediaRawData* aSample) {
+ CMSampleTimingInfo timestamp;
+
+ timestamp.duration =
+ CMTimeMake(aSample->mDuration.ToMicroseconds(), USECS_PER_S);
+ timestamp.presentationTimeStamp =
+ CMTimeMake(aSample->mTime.ToMicroseconds(), USECS_PER_S);
+ timestamp.decodeTimeStamp =
+ CMTimeMake(aSample->mTimecode.ToMicroseconds(), USECS_PER_S);
+
+ return timestamp;
+}
+
+void AppleVTDecoder::ProcessDecode(MediaRawData* aSample) {
+ AssertOnTaskQueue();
+ PROCESS_DECODE_LOG(aSample);
+
+ if (mIsFlushing) {
+ MonitorAutoLock mon(mMonitor);
+ mPromise.Reject(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ return;
+ }
+
+ mTrackingId.apply([&](const auto& aId) {
+ MediaInfoFlag flag = MediaInfoFlag::None;
+ flag |= (aSample->mKeyframe ? MediaInfoFlag::KeyFrame
+ : MediaInfoFlag::NonKeyFrame);
+ flag |= (mIsHardwareAccelerated ? MediaInfoFlag::HardwareDecoding
+ : MediaInfoFlag::SoftwareDecoding);
+ switch (mStreamType) {
+ case StreamType::H264:
+ flag |= MediaInfoFlag::VIDEO_H264;
+ break;
+ case StreamType::VP9:
+ flag |= MediaInfoFlag::VIDEO_VP9;
+ break;
+ default:
+ break;
+ }
+ mPerformanceRecorder.Start(aSample->mTimecode.ToMicroseconds(),
+ "AppleVTDecoder"_ns, aId, flag);
+ });
+
+ AutoCFRelease<CMBlockBufferRef> block = nullptr;
+ AutoCFRelease<CMSampleBufferRef> sample = nullptr;
+ VTDecodeInfoFlags infoFlags;
+ OSStatus rv;
+
+ // FIXME: This copies the sample data. I think we can provide
+ // a custom block source which reuses the aSample buffer.
+ // But note that there may be a problem keeping the samples
+ // alive over multiple frames.
+ rv = CMBlockBufferCreateWithMemoryBlock(
+ kCFAllocatorDefault, // Struct allocator.
+ const_cast<uint8_t*>(aSample->Data()), aSample->Size(),
+ kCFAllocatorNull, // Block allocator.
+ NULL, // Block source.
+ 0, // Data offset.
+ aSample->Size(), false, block.receive());
+ if (rv != noErr) {
+ NS_ERROR("Couldn't create CMBlockBuffer");
+ MonitorAutoLock mon(mMonitor);
+ mPromise.Reject(
+ MediaResult(NS_ERROR_OUT_OF_MEMORY,
+ RESULT_DETAIL("CMBlockBufferCreateWithMemoryBlock:%x", rv)),
+ __func__);
+ return;
+ }
+
+ CMSampleTimingInfo timestamp = TimingInfoFromSample(aSample);
+ rv = CMSampleBufferCreate(kCFAllocatorDefault, block, true, 0, 0, mFormat, 1,
+ 1, &timestamp, 0, NULL, sample.receive());
+ if (rv != noErr) {
+ NS_ERROR("Couldn't create CMSampleBuffer");
+ MonitorAutoLock mon(mMonitor);
+ mPromise.Reject(MediaResult(NS_ERROR_OUT_OF_MEMORY,
+ RESULT_DETAIL("CMSampleBufferCreate:%x", rv)),
+ __func__);
+ return;
+ }
+
+ VTDecodeFrameFlags decodeFlags =
+ kVTDecodeFrame_EnableAsynchronousDecompression;
+ rv = VTDecompressionSessionDecodeFrame(
+ mSession, sample, decodeFlags, CreateAppleFrameRef(aSample), &infoFlags);
+ if (infoFlags & kVTDecodeInfo_FrameDropped) {
+ MonitorAutoLock mon(mMonitor);
+ // Smile and nod
+ NS_WARNING("Decoder synchronously dropped frame");
+ MaybeResolveBufferedFrames();
+ return;
+ }
+
+ if (rv != noErr) {
+ LOG("AppleVTDecoder: Error %d VTDecompressionSessionDecodeFrame", rv);
+ NS_WARNING("Couldn't pass frame to decoder");
+ // It appears that even when VTDecompressionSessionDecodeFrame returned a
+ // failure. Decoding sometimes actually get processed.
+ MonitorAutoLock mon(mMonitor);
+ mPromise.RejectIfExists(
+ MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ RESULT_DETAIL("VTDecompressionSessionDecodeFrame:%x", rv)),
+ __func__);
+ return;
+ }
+}
+
+void AppleVTDecoder::ProcessShutdown() {
+ if (mSession) {
+ LOG("%s: cleaning up session %p", __func__, mSession);
+ VTDecompressionSessionInvalidate(mSession);
+ CFRelease(mSession);
+ mSession = nullptr;
+ }
+ if (mFormat) {
+ LOG("%s: releasing format %p", __func__, mFormat);
+ CFRelease(mFormat);
+ mFormat = nullptr;
+ }
+}
+
+RefPtr<MediaDataDecoder::FlushPromise> AppleVTDecoder::ProcessFlush() {
+ AssertOnTaskQueue();
+ nsresult rv = WaitForAsynchronousFrames();
+ if (NS_FAILED(rv)) {
+ LOG("AppleVTDecoder::Flush failed waiting for platform decoder");
+ }
+ MonitorAutoLock mon(mMonitor);
+ mPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+
+ while (!mReorderQueue.IsEmpty()) {
+ mReorderQueue.Pop();
+ }
+ mPerformanceRecorder.Record(std::numeric_limits<int64_t>::max());
+ mSeekTargetThreshold.reset();
+ mIsFlushing = false;
+ return FlushPromise::CreateAndResolve(true, __func__);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> AppleVTDecoder::ProcessDrain() {
+ AssertOnTaskQueue();
+ nsresult rv = WaitForAsynchronousFrames();
+ if (NS_FAILED(rv)) {
+ LOG("AppleVTDecoder::Drain failed waiting for platform decoder");
+ }
+ MonitorAutoLock mon(mMonitor);
+ DecodedData samples;
+ while (!mReorderQueue.IsEmpty()) {
+ samples.AppendElement(mReorderQueue.Pop());
+ }
+ return DecodePromise::CreateAndResolve(std::move(samples), __func__);
+}
+
+AppleVTDecoder::AppleFrameRef* AppleVTDecoder::CreateAppleFrameRef(
+ const MediaRawData* aSample) {
+ MOZ_ASSERT(aSample);
+ return new AppleFrameRef(*aSample);
+}
+
+void AppleVTDecoder::SetSeekThreshold(const media::TimeUnit& aTime) {
+ if (aTime.IsValid()) {
+ mSeekTargetThreshold = Some(aTime);
+ } else {
+ mSeekTargetThreshold.reset();
+ }
+}
+
+//
+// Implementation details.
+//
+
+// Callback passed to the VideoToolbox decoder for returning data.
+// This needs to be static because the API takes a C-style pair of
+// function and userdata pointers. This validates parameters and
+// forwards the decoded image back to an object method.
+static void PlatformCallback(void* decompressionOutputRefCon,
+ void* sourceFrameRefCon, OSStatus status,
+ VTDecodeInfoFlags flags, CVImageBufferRef image,
+ CMTime presentationTimeStamp,
+ CMTime presentationDuration) {
+ AppleVTDecoder* decoder =
+ static_cast<AppleVTDecoder*>(decompressionOutputRefCon);
+ LOGEX(decoder, "AppleVideoDecoder %s status %d flags %d", __func__,
+ static_cast<int>(status), flags);
+
+ UniquePtr<AppleVTDecoder::AppleFrameRef> frameRef(
+ static_cast<AppleVTDecoder::AppleFrameRef*>(sourceFrameRefCon));
+
+ // Validate our arguments.
+ if (status != noErr) {
+ NS_WARNING("VideoToolbox decoder returned an error");
+ decoder->OnDecodeError(status);
+ return;
+ } else if (!image) {
+ NS_WARNING("VideoToolbox decoder returned no data");
+ } else if (flags & kVTDecodeInfo_FrameDropped) {
+ NS_WARNING(" ...frame tagged as dropped...");
+ } else {
+ MOZ_ASSERT(CFGetTypeID(image) == CVPixelBufferGetTypeID(),
+ "VideoToolbox returned an unexpected image type");
+ }
+
+ decoder->OutputFrame(image, *frameRef);
+}
+
+void AppleVTDecoder::MaybeResolveBufferedFrames() {
+ mMonitor.AssertCurrentThreadOwns();
+
+ if (mPromise.IsEmpty()) {
+ return;
+ }
+
+ DecodedData results;
+ while (mReorderQueue.Length() > mMaxRefFrames) {
+ results.AppendElement(mReorderQueue.Pop());
+ }
+ mPromise.Resolve(std::move(results), __func__);
+}
+
+void AppleVTDecoder::MaybeRegisterCallbackThread() {
+ ProfilerThreadId id = profiler_current_thread_id();
+ if (MOZ_LIKELY(id == mCallbackThreadId)) {
+ return;
+ }
+ mCallbackThreadId = id;
+ CallbackThreadRegistry::Get()->Register(mCallbackThreadId,
+ "AppleVTDecoderCallback");
+}
+
+nsCString AppleVTDecoder::GetCodecName() const {
+ switch (mStreamType) {
+ case StreamType::H264:
+ return "h264"_ns;
+ case StreamType::VP9:
+ return "vp9"_ns;
+ default:
+ return "unknown"_ns;
+ }
+}
+
+// Copy and return a decoded frame.
+void AppleVTDecoder::OutputFrame(CVPixelBufferRef aImage,
+ AppleVTDecoder::AppleFrameRef aFrameRef) {
+ MaybeRegisterCallbackThread();
+
+ if (mIsFlushing) {
+ // We are in the process of flushing or shutting down; ignore frame.
+ return;
+ }
+
+ LOG("mp4 output frame %lld dts %lld pts %lld duration %lld us%s",
+ aFrameRef.byte_offset, aFrameRef.decode_timestamp.ToMicroseconds(),
+ aFrameRef.composition_timestamp.ToMicroseconds(),
+ aFrameRef.duration.ToMicroseconds(),
+ aFrameRef.is_sync_point ? " keyframe" : "");
+
+ if (!aImage) {
+ // Image was dropped by decoder or none return yet.
+ // We need more input to continue.
+ MonitorAutoLock mon(mMonitor);
+ MaybeResolveBufferedFrames();
+ return;
+ }
+
+ bool useNullSample = false;
+ if (mSeekTargetThreshold.isSome()) {
+ if ((aFrameRef.composition_timestamp + aFrameRef.duration) <
+ mSeekTargetThreshold.ref()) {
+ useNullSample = true;
+ } else {
+ mSeekTargetThreshold.reset();
+ }
+ }
+
+ // Where our resulting image will end up.
+ RefPtr<MediaData> data;
+ // Bounds.
+ VideoInfo info;
+ info.mDisplay = gfx::IntSize(mDisplayWidth, mDisplayHeight);
+
+ if (useNullSample) {
+ data = new NullData(aFrameRef.byte_offset, aFrameRef.composition_timestamp,
+ aFrameRef.duration);
+ } else if (mUseSoftwareImages) {
+ size_t width = CVPixelBufferGetWidth(aImage);
+ size_t height = CVPixelBufferGetHeight(aImage);
+ DebugOnly<size_t> planes = CVPixelBufferGetPlaneCount(aImage);
+ MOZ_ASSERT(planes == 3, "Likely not YUV420 format and it must be.");
+
+ VideoData::YCbCrBuffer buffer;
+
+ // Lock the returned image data.
+ CVReturn rv =
+ CVPixelBufferLockBaseAddress(aImage, kCVPixelBufferLock_ReadOnly);
+ if (rv != kCVReturnSuccess) {
+ NS_ERROR("error locking pixel data");
+ MonitorAutoLock mon(mMonitor);
+ mPromise.Reject(
+ MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ RESULT_DETAIL("CVPixelBufferLockBaseAddress:%x", rv)),
+ __func__);
+ return;
+ }
+ // Y plane.
+ buffer.mPlanes[0].mData =
+ static_cast<uint8_t*>(CVPixelBufferGetBaseAddressOfPlane(aImage, 0));
+ buffer.mPlanes[0].mStride = CVPixelBufferGetBytesPerRowOfPlane(aImage, 0);
+ buffer.mPlanes[0].mWidth = width;
+ buffer.mPlanes[0].mHeight = height;
+ buffer.mPlanes[0].mSkip = 0;
+ // Cb plane.
+ buffer.mPlanes[1].mData =
+ static_cast<uint8_t*>(CVPixelBufferGetBaseAddressOfPlane(aImage, 1));
+ buffer.mPlanes[1].mStride = CVPixelBufferGetBytesPerRowOfPlane(aImage, 1);
+ buffer.mPlanes[1].mWidth = (width + 1) / 2;
+ buffer.mPlanes[1].mHeight = (height + 1) / 2;
+ buffer.mPlanes[1].mSkip = 0;
+ // Cr plane.
+ buffer.mPlanes[2].mData =
+ static_cast<uint8_t*>(CVPixelBufferGetBaseAddressOfPlane(aImage, 2));
+ buffer.mPlanes[2].mStride = CVPixelBufferGetBytesPerRowOfPlane(aImage, 2);
+ buffer.mPlanes[2].mWidth = (width + 1) / 2;
+ buffer.mPlanes[2].mHeight = (height + 1) / 2;
+ buffer.mPlanes[2].mSkip = 0;
+
+ buffer.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT;
+ buffer.mYUVColorSpace = mColorSpace;
+ buffer.mColorPrimaries = mColorPrimaries;
+ buffer.mColorRange = mColorRange;
+
+ gfx::IntRect visible = gfx::IntRect(0, 0, mPictureWidth, mPictureHeight);
+
+ // Copy the image data into our own format.
+ data = VideoData::CreateAndCopyData(
+ info, mImageContainer, aFrameRef.byte_offset,
+ aFrameRef.composition_timestamp, aFrameRef.duration, buffer,
+ aFrameRef.is_sync_point, aFrameRef.decode_timestamp, visible,
+ mKnowsCompositor);
+ // Unlock the returned image data.
+ CVPixelBufferUnlockBaseAddress(aImage, kCVPixelBufferLock_ReadOnly);
+ } else {
+#ifndef MOZ_WIDGET_UIKIT
+ // Set pixel buffer properties on aImage before we extract its surface.
+ // This ensures that we can use defined enums to set values instead
+ // of later setting magic CFSTR values on the surface itself.
+ if (mColorSpace == gfx::YUVColorSpace::BT601) {
+ CVBufferSetAttachment(aImage, kCVImageBufferYCbCrMatrixKey,
+ kCVImageBufferYCbCrMatrix_ITU_R_601_4,
+ kCVAttachmentMode_ShouldPropagate);
+ } else if (mColorSpace == gfx::YUVColorSpace::BT709) {
+ CVBufferSetAttachment(aImage, kCVImageBufferYCbCrMatrixKey,
+ kCVImageBufferYCbCrMatrix_ITU_R_709_2,
+ kCVAttachmentMode_ShouldPropagate);
+ } else if (mColorSpace == gfx::YUVColorSpace::BT2020) {
+ CVBufferSetAttachment(aImage, kCVImageBufferYCbCrMatrixKey,
+ kCVImageBufferYCbCrMatrix_ITU_R_2020,
+ kCVAttachmentMode_ShouldPropagate);
+ }
+
+ if (mColorPrimaries == gfx::ColorSpace2::BT709) {
+ CVBufferSetAttachment(aImage, kCVImageBufferColorPrimariesKey,
+ kCVImageBufferColorPrimaries_ITU_R_709_2,
+ kCVAttachmentMode_ShouldPropagate);
+ } else if (mColorPrimaries == gfx::ColorSpace2::BT2020) {
+ CVBufferSetAttachment(aImage, kCVImageBufferColorPrimariesKey,
+ kCVImageBufferColorPrimaries_ITU_R_2020,
+ kCVAttachmentMode_ShouldPropagate);
+ }
+
+ // Transfer function is applied independently from the colorSpace.
+ CVBufferSetAttachment(
+ aImage, kCVImageBufferTransferFunctionKey,
+ gfxMacUtils::CFStringForTransferFunction(mTransferFunction),
+ kCVAttachmentMode_ShouldPropagate);
+
+ CFTypeRefPtr<IOSurfaceRef> surface =
+ CFTypeRefPtr<IOSurfaceRef>::WrapUnderGetRule(
+ CVPixelBufferGetIOSurface(aImage));
+ MOZ_ASSERT(surface, "Decoder didn't return an IOSurface backed buffer");
+
+ RefPtr<MacIOSurface> macSurface = new MacIOSurface(std::move(surface));
+ macSurface->SetYUVColorSpace(mColorSpace);
+ macSurface->mColorPrimaries = mColorPrimaries;
+
+ RefPtr<layers::Image> image = new layers::MacIOSurfaceImage(macSurface);
+
+ data = VideoData::CreateFromImage(
+ info.mDisplay, aFrameRef.byte_offset, aFrameRef.composition_timestamp,
+ aFrameRef.duration, image.forget(), aFrameRef.is_sync_point,
+ aFrameRef.decode_timestamp);
+#else
+ MOZ_ASSERT_UNREACHABLE("No MacIOSurface on iOS");
+#endif
+ }
+
+ if (!data) {
+ NS_ERROR("Couldn't create VideoData for frame");
+ MonitorAutoLock mon(mMonitor);
+ mPromise.Reject(MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__), __func__);
+ return;
+ }
+
+ mPerformanceRecorder.Record(
+ aFrameRef.decode_timestamp.ToMicroseconds(), [&](DecodeStage& aStage) {
+ aStage.SetResolution(static_cast<int>(CVPixelBufferGetWidth(aImage)),
+ static_cast<int>(CVPixelBufferGetHeight(aImage)));
+ auto format = [&]() -> Maybe<DecodeStage::ImageFormat> {
+ switch (CVPixelBufferGetPixelFormatType(aImage)) {
+ case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
+ case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
+ return Some(DecodeStage::NV12);
+ case kCVPixelFormatType_422YpCbCr8_yuvs:
+ case kCVPixelFormatType_422YpCbCr8FullRange:
+ return Some(DecodeStage::YUV422P);
+ case kCVPixelFormatType_32BGRA:
+ return Some(DecodeStage::RGBA32);
+ default:
+ return Nothing();
+ }
+ }();
+ format.apply([&](auto aFormat) { aStage.SetImageFormat(aFormat); });
+ aStage.SetColorDepth(mColorDepth);
+ aStage.SetYUVColorSpace(mColorSpace);
+ aStage.SetColorRange(mColorRange);
+ });
+
+ // Frames come out in DTS order but we need to output them
+ // in composition order.
+ MonitorAutoLock mon(mMonitor);
+ mReorderQueue.Push(std::move(data));
+ MaybeResolveBufferedFrames();
+
+ LOG("%llu decoded frames queued",
+ static_cast<unsigned long long>(mReorderQueue.Length()));
+}
+
+void AppleVTDecoder::OnDecodeError(OSStatus aError) {
+ MonitorAutoLock mon(mMonitor);
+ mPromise.RejectIfExists(
+ MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ RESULT_DETAIL("OnDecodeError:%x", aError)),
+ __func__);
+}
+
+nsresult AppleVTDecoder::WaitForAsynchronousFrames() {
+ OSStatus rv = VTDecompressionSessionWaitForAsynchronousFrames(mSession);
+ if (rv != noErr) {
+ NS_ERROR("AppleVTDecoder: Error waiting for asynchronous frames");
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+MediaResult AppleVTDecoder::InitializeSession() {
+ OSStatus rv;
+
+ AutoCFRelease<CFDictionaryRef> extensions = CreateDecoderExtensions();
+
+ rv = CMVideoFormatDescriptionCreate(
+ kCFAllocatorDefault,
+ mStreamType == StreamType::H264
+ ? kCMVideoCodecType_H264
+ : CMVideoCodecType(AppleDecoderModule::kCMVideoCodecType_VP9),
+ mPictureWidth, mPictureHeight, extensions, &mFormat);
+ if (rv != noErr) {
+ return MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Couldn't create format description!"));
+ }
+
+ // Contruct video decoder selection spec.
+ AutoCFRelease<CFDictionaryRef> spec = CreateDecoderSpecification();
+
+ // Contruct output configuration.
+ AutoCFRelease<CFDictionaryRef> outputConfiguration =
+ CreateOutputConfiguration();
+
+ VTDecompressionOutputCallbackRecord cb = {PlatformCallback, this};
+ rv =
+ VTDecompressionSessionCreate(kCFAllocatorDefault, mFormat,
+ spec, // Video decoder selection.
+ outputConfiguration, // Output video format.
+ &cb, &mSession);
+
+ if (rv != noErr) {
+ return MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Couldn't create decompression session!"));
+ }
+
+ CFBooleanRef isUsingHW = nullptr;
+ rv = VTSessionCopyProperty(
+ mSession,
+ kVTDecompressionPropertyKey_UsingHardwareAcceleratedVideoDecoder,
+ kCFAllocatorDefault, &isUsingHW);
+ if (rv == noErr) {
+ mIsHardwareAccelerated = isUsingHW == kCFBooleanTrue;
+ LOG("AppleVTDecoder: %s hardware accelerated decoding",
+ mIsHardwareAccelerated ? "using" : "not using");
+ } else {
+ LOG("AppleVTDecoder: maybe hardware accelerated decoding "
+ "(VTSessionCopyProperty query failed)");
+ }
+ if (isUsingHW) {
+ CFRelease(isUsingHW);
+ }
+
+ return NS_OK;
+}
+
+CFDictionaryRef AppleVTDecoder::CreateDecoderExtensions() {
+ AutoCFRelease<CFDataRef> data = CFDataCreate(
+ kCFAllocatorDefault, mExtraData->Elements(), mExtraData->Length());
+
+ const void* atomsKey[1];
+ atomsKey[0] = mStreamType == StreamType::H264 ? CFSTR("avcC") : CFSTR("vpcC");
+ const void* atomsValue[] = {data};
+ static_assert(ArrayLength(atomsKey) == ArrayLength(atomsValue),
+ "Non matching keys/values array size");
+
+ AutoCFRelease<CFDictionaryRef> atoms = CFDictionaryCreate(
+ kCFAllocatorDefault, atomsKey, atomsValue, ArrayLength(atomsKey),
+ &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
+
+ const void* extensionKeys[] = {
+ kCVImageBufferChromaLocationBottomFieldKey,
+ kCVImageBufferChromaLocationTopFieldKey,
+ kCMFormatDescriptionExtension_SampleDescriptionExtensionAtoms};
+
+ const void* extensionValues[] = {kCVImageBufferChromaLocation_Left,
+ kCVImageBufferChromaLocation_Left, atoms};
+ static_assert(ArrayLength(extensionKeys) == ArrayLength(extensionValues),
+ "Non matching keys/values array size");
+
+ return CFDictionaryCreate(kCFAllocatorDefault, extensionKeys, extensionValues,
+ ArrayLength(extensionKeys),
+ &kCFTypeDictionaryKeyCallBacks,
+ &kCFTypeDictionaryValueCallBacks);
+}
+
+CFDictionaryRef AppleVTDecoder::CreateDecoderSpecification() {
+ const void* specKeys[] = {
+ kVTVideoDecoderSpecification_EnableHardwareAcceleratedVideoDecoder};
+ const void* specValues[1];
+ if (gfx::gfxVars::CanUseHardwareVideoDecoding()) {
+ specValues[0] = kCFBooleanTrue;
+ } else {
+ // This GPU is blacklisted for hardware decoding.
+ specValues[0] = kCFBooleanFalse;
+ }
+ static_assert(ArrayLength(specKeys) == ArrayLength(specValues),
+ "Non matching keys/values array size");
+
+ return CFDictionaryCreate(
+ kCFAllocatorDefault, specKeys, specValues, ArrayLength(specKeys),
+ &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
+}
+
+CFDictionaryRef AppleVTDecoder::CreateOutputConfiguration() {
+ if (mUseSoftwareImages) {
+ // Output format type:
+ SInt32 PixelFormatTypeValue = kCVPixelFormatType_420YpCbCr8Planar;
+ AutoCFRelease<CFNumberRef> PixelFormatTypeNumber = CFNumberCreate(
+ kCFAllocatorDefault, kCFNumberSInt32Type, &PixelFormatTypeValue);
+ const void* outputKeys[] = {kCVPixelBufferPixelFormatTypeKey};
+ const void* outputValues[] = {PixelFormatTypeNumber};
+ static_assert(ArrayLength(outputKeys) == ArrayLength(outputValues),
+ "Non matching keys/values array size");
+
+ return CFDictionaryCreate(
+ kCFAllocatorDefault, outputKeys, outputValues, ArrayLength(outputKeys),
+ &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
+ }
+
+#ifndef MOZ_WIDGET_UIKIT
+ // Output format type:
+
+ bool is10Bit = (gfx::BitDepthForColorDepth(mColorDepth) == 10);
+ SInt32 PixelFormatTypeValue =
+ mColorRange == gfx::ColorRange::FULL
+ ? (is10Bit ? kCVPixelFormatType_420YpCbCr10BiPlanarFullRange
+ : kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)
+ : (is10Bit ? kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange
+ : kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange);
+ AutoCFRelease<CFNumberRef> PixelFormatTypeNumber = CFNumberCreate(
+ kCFAllocatorDefault, kCFNumberSInt32Type, &PixelFormatTypeValue);
+ // Construct IOSurface Properties
+ const void* IOSurfaceKeys[] = {kIOSurfaceIsGlobal};
+ const void* IOSurfaceValues[] = {kCFBooleanTrue};
+ static_assert(ArrayLength(IOSurfaceKeys) == ArrayLength(IOSurfaceValues),
+ "Non matching keys/values array size");
+
+ // Contruct output configuration.
+ AutoCFRelease<CFDictionaryRef> IOSurfaceProperties = CFDictionaryCreate(
+ kCFAllocatorDefault, IOSurfaceKeys, IOSurfaceValues,
+ ArrayLength(IOSurfaceKeys), &kCFTypeDictionaryKeyCallBacks,
+ &kCFTypeDictionaryValueCallBacks);
+
+ const void* outputKeys[] = {kCVPixelBufferIOSurfacePropertiesKey,
+ kCVPixelBufferPixelFormatTypeKey,
+ kCVPixelBufferOpenGLCompatibilityKey};
+ const void* outputValues[] = {IOSurfaceProperties, PixelFormatTypeNumber,
+ kCFBooleanTrue};
+ static_assert(ArrayLength(outputKeys) == ArrayLength(outputValues),
+ "Non matching keys/values array size");
+
+ return CFDictionaryCreate(
+ kCFAllocatorDefault, outputKeys, outputValues, ArrayLength(outputKeys),
+ &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
+#else
+ MOZ_ASSERT_UNREACHABLE("No MacIOSurface on iOS");
+#endif
+}
+
+} // namespace mozilla
+
+#undef LOG
+#undef LOGEX
diff --git a/dom/media/platforms/apple/AppleVTDecoder.h b/dom/media/platforms/apple/AppleVTDecoder.h
new file mode 100644
index 0000000000..140a335628
--- /dev/null
+++ b/dom/media/platforms/apple/AppleVTDecoder.h
@@ -0,0 +1,145 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_AppleVTDecoder_h
+#define mozilla_AppleVTDecoder_h
+
+#include <CoreFoundation/CFDictionary.h> // For CFDictionaryRef
+#include <CoreMedia/CoreMedia.h> // For CMVideoFormatDescriptionRef
+#include <VideoToolbox/VideoToolbox.h> // For VTDecompressionSessionRef
+
+#include "AppleDecoderModule.h"
+#include "PerformanceRecorder.h"
+#include "PlatformDecoderModule.h"
+#include "ReorderQueue.h"
+#include "TimeUnits.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/gfx/Types.h"
+#include "mozilla/ProfilerUtils.h"
+
+namespace mozilla {
+
+DDLoggedTypeDeclNameAndBase(AppleVTDecoder, MediaDataDecoder);
+
+class AppleVTDecoder final : public MediaDataDecoder,
+ public DecoderDoctorLifeLogger<AppleVTDecoder> {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AppleVTDecoder, final);
+
+ AppleVTDecoder(const VideoInfo& aConfig,
+ layers::ImageContainer* aImageContainer,
+ CreateDecoderParams::OptionSet aOptions,
+ layers::KnowsCompositor* aKnowsCompositor,
+ Maybe<TrackingId> aTrackingId);
+
+ class AppleFrameRef {
+ public:
+ media::TimeUnit decode_timestamp;
+ media::TimeUnit composition_timestamp;
+ media::TimeUnit duration;
+ int64_t byte_offset;
+ bool is_sync_point;
+
+ explicit AppleFrameRef(const MediaRawData& aSample)
+ : decode_timestamp(aSample.mTimecode),
+ composition_timestamp(aSample.mTime),
+ duration(aSample.mDuration),
+ byte_offset(aSample.mOffset),
+ is_sync_point(aSample.mKeyframe) {}
+ };
+
+ RefPtr<InitPromise> Init() override;
+ RefPtr<DecodePromise> Decode(MediaRawData* aSample) override;
+ RefPtr<DecodePromise> Drain() override;
+ RefPtr<FlushPromise> Flush() override;
+ RefPtr<ShutdownPromise> Shutdown() override;
+ void SetSeekThreshold(const media::TimeUnit& aTime) override;
+
+ bool IsHardwareAccelerated(nsACString& aFailureReason) const override {
+ return mIsHardwareAccelerated;
+ }
+
+ nsCString GetDescriptionName() const override {
+ return mIsHardwareAccelerated ? "apple hardware VT decoder"_ns
+ : "apple software VT decoder"_ns;
+ }
+
+ nsCString GetCodecName() const override;
+
+ ConversionRequired NeedsConversion() const override {
+ return ConversionRequired::kNeedAVCC;
+ }
+
+ // Access from the taskqueue and the decoder's thread.
+ // OutputFrame is thread-safe.
+ void OutputFrame(CVPixelBufferRef aImage, AppleFrameRef aFrameRef);
+ void OnDecodeError(OSStatus aError);
+
+ private:
+ friend class AppleDecoderModule; // To access InitializeSession.
+ virtual ~AppleVTDecoder();
+ RefPtr<FlushPromise> ProcessFlush();
+ RefPtr<DecodePromise> ProcessDrain();
+ void ProcessShutdown();
+ void ProcessDecode(MediaRawData* aSample);
+ void MaybeResolveBufferedFrames();
+
+ void MaybeRegisterCallbackThread();
+
+ void AssertOnTaskQueue() { MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn()); }
+
+ AppleFrameRef* CreateAppleFrameRef(const MediaRawData* aSample);
+ CFDictionaryRef CreateOutputConfiguration();
+
+ const RefPtr<MediaByteBuffer> mExtraData;
+ const uint32_t mPictureWidth;
+ const uint32_t mPictureHeight;
+ const uint32_t mDisplayWidth;
+ const uint32_t mDisplayHeight;
+ const gfx::YUVColorSpace mColorSpace;
+ const gfx::ColorSpace2 mColorPrimaries;
+ const gfx::TransferFunction mTransferFunction;
+ const gfx::ColorRange mColorRange;
+ const gfx::ColorDepth mColorDepth;
+
+ // Method to set up the decompression session.
+ MediaResult InitializeSession();
+ nsresult WaitForAsynchronousFrames();
+ CFDictionaryRef CreateDecoderSpecification();
+ CFDictionaryRef CreateDecoderExtensions();
+
+ enum class StreamType { Unknown, H264, VP9 };
+ const StreamType mStreamType;
+ const RefPtr<TaskQueue> mTaskQueue;
+ const uint32_t mMaxRefFrames;
+ const RefPtr<layers::ImageContainer> mImageContainer;
+ const RefPtr<layers::KnowsCompositor> mKnowsCompositor;
+ const bool mUseSoftwareImages;
+ const Maybe<TrackingId> mTrackingId;
+
+ // Set on reader/decode thread calling Flush() to indicate that output is
+ // not required and so input samples on mTaskQueue need not be processed.
+ Atomic<bool> mIsFlushing;
+ std::atomic<ProfilerThreadId> mCallbackThreadId;
+ // Protects mReorderQueue and mPromise.
+ Monitor mMonitor MOZ_UNANNOTATED;
+ ReorderQueue mReorderQueue;
+ MozMonitoredPromiseHolder<DecodePromise> mPromise;
+
+ // Decoded frame will be dropped if its pts is smaller than this
+ // value. It shold be initialized before Input() or after Flush(). So it is
+ // safe to access it in OutputFrame without protecting.
+ Maybe<media::TimeUnit> mSeekTargetThreshold;
+
+ CMVideoFormatDescriptionRef mFormat;
+ VTDecompressionSessionRef mSession;
+ Atomic<bool> mIsHardwareAccelerated;
+ PerformanceRecorderMulti<DecodeStage> mPerformanceRecorder;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_AppleVTDecoder_h
diff --git a/dom/media/platforms/apple/AppleVTEncoder.cpp b/dom/media/platforms/apple/AppleVTEncoder.cpp
new file mode 100644
index 0000000000..af91d99bcb
--- /dev/null
+++ b/dom/media/platforms/apple/AppleVTEncoder.cpp
@@ -0,0 +1,628 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AppleVTEncoder.h"
+
+#include <CoreFoundation/CFArray.h>
+#include <CoreFoundation/CFByteOrder.h>
+#include <CoreFoundation/CFDictionary.h>
+
+#include "ImageContainer.h"
+#include "AnnexB.h"
+#include "H264.h"
+
+#include "libyuv.h"
+
+#include "AppleUtils.h"
+
+namespace mozilla {
+extern LazyLogModule sPEMLog;
+#define VTENC_LOGE(fmt, ...) \
+ MOZ_LOG(sPEMLog, mozilla::LogLevel::Error, \
+ ("[AppleVTEncoder] %s: " fmt, __func__, ##__VA_ARGS__))
+#define VTENC_LOGD(fmt, ...) \
+ MOZ_LOG(sPEMLog, mozilla::LogLevel::Debug, \
+ ("[AppleVTEncoder] %s: " fmt, __func__, ##__VA_ARGS__))
+
+static CFDictionaryRef BuildEncoderSpec(const bool aHardwareNotAllowed) {
+ const void* keys[] = {
+ kVTVideoEncoderSpecification_EnableHardwareAcceleratedVideoEncoder};
+ const void* values[] = {aHardwareNotAllowed ? kCFBooleanFalse
+ : kCFBooleanTrue};
+
+ static_assert(ArrayLength(keys) == ArrayLength(values),
+ "Non matching keys/values array size");
+ return CFDictionaryCreate(kCFAllocatorDefault, keys, values,
+ ArrayLength(keys), &kCFTypeDictionaryKeyCallBacks,
+ &kCFTypeDictionaryValueCallBacks);
+}
+
+static void FrameCallback(void* aEncoder, void* aFrameRefCon, OSStatus aStatus,
+ VTEncodeInfoFlags aInfoFlags,
+ CMSampleBufferRef aSampleBuffer) {
+ if (aStatus != noErr || !aSampleBuffer) {
+ VTENC_LOGE("VideoToolbox encoder returned no data status=%d sample=%p",
+ aStatus, aSampleBuffer);
+ aSampleBuffer = nullptr;
+ } else if (aInfoFlags & kVTEncodeInfo_FrameDropped) {
+ VTENC_LOGE("frame tagged as dropped");
+ return;
+ }
+ (static_cast<AppleVTEncoder*>(aEncoder))->OutputFrame(aSampleBuffer);
+}
+
+static bool SetAverageBitrate(VTCompressionSessionRef& aSession,
+ MediaDataEncoder::Rate aBitsPerSec) {
+ int64_t bps(aBitsPerSec);
+ AutoCFRelease<CFNumberRef> bitrate(
+ CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt64Type, &bps));
+ return VTSessionSetProperty(aSession,
+ kVTCompressionPropertyKey_AverageBitRate,
+ bitrate) == noErr;
+}
+
+static bool SetRealtimeProperties(VTCompressionSessionRef& aSession) {
+ return VTSessionSetProperty(aSession, kVTCompressionPropertyKey_RealTime,
+ kCFBooleanTrue) == noErr &&
+ VTSessionSetProperty(aSession,
+ kVTCompressionPropertyKey_AllowFrameReordering,
+ kCFBooleanFalse) == noErr;
+}
+
+static bool SetProfileLevel(VTCompressionSessionRef& aSession,
+ AppleVTEncoder::H264Specific::ProfileLevel aValue) {
+ CFStringRef profileLevel = nullptr;
+ switch (aValue) {
+ case AppleVTEncoder::H264Specific::ProfileLevel::BaselineAutoLevel:
+ profileLevel = kVTProfileLevel_H264_Baseline_AutoLevel;
+ break;
+ case AppleVTEncoder::H264Specific::ProfileLevel::MainAutoLevel:
+ profileLevel = kVTProfileLevel_H264_Main_AutoLevel;
+ break;
+ }
+
+ return profileLevel ? VTSessionSetProperty(
+ aSession, kVTCompressionPropertyKey_ProfileLevel,
+ profileLevel) == noErr
+ : false;
+}
+
+RefPtr<MediaDataEncoder::InitPromise> AppleVTEncoder::Init() {
+ MOZ_ASSERT(!mInited, "Cannot initialize encoder again without shutting down");
+
+ if (mConfig.mSize.width == 0 || mConfig.mSize.height == 0) {
+ return InitPromise::CreateAndReject(NS_ERROR_ILLEGAL_VALUE, __func__);
+ }
+
+ AutoCFRelease<CFDictionaryRef> spec(BuildEncoderSpec(mHardwareNotAllowed));
+ AutoCFRelease<CFDictionaryRef> srcBufferAttr(
+ BuildSourceImageBufferAttributes());
+ if (!srcBufferAttr) {
+ return InitPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR,
+ "fail to create source buffer attributes"),
+ __func__);
+ }
+
+ OSStatus status = VTCompressionSessionCreate(
+ kCFAllocatorDefault, mConfig.mSize.width, mConfig.mSize.height,
+ kCMVideoCodecType_H264, spec, srcBufferAttr, kCFAllocatorDefault,
+ &FrameCallback, this /* outputCallbackRefCon */, &mSession);
+
+ if (status != noErr) {
+ return InitPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ "fail to create encoder session"),
+ __func__);
+ }
+
+ if (!SetAverageBitrate(mSession, mConfig.mBitsPerSec)) {
+ return InitPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ "fail to configurate average bitrate"),
+ __func__);
+ }
+
+ if (mConfig.mUsage == Usage::Realtime && !SetRealtimeProperties(mSession)) {
+ VTENC_LOGE("fail to configurate realtime properties");
+ return InitPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ "fail to configurate average bitrate"),
+ __func__);
+ }
+
+ int64_t interval =
+ mConfig.mKeyframeInterval > std::numeric_limits<int64_t>::max()
+ ? std::numeric_limits<int64_t>::max()
+ : mConfig.mKeyframeInterval;
+ AutoCFRelease<CFNumberRef> cf(
+ CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt64Type, &interval));
+ if (VTSessionSetProperty(mSession,
+ kVTCompressionPropertyKey_MaxKeyFrameInterval,
+ cf) != noErr) {
+ return InitPromise::CreateAndReject(
+ MediaResult(
+ NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ nsPrintfCString("fail to configurate keyframe interval:%" PRId64,
+ interval)),
+ __func__);
+ }
+
+ if (mConfig.mCodecSpecific) {
+ const H264Specific& specific = mConfig.mCodecSpecific.ref();
+ if (!SetProfileLevel(mSession, specific.mProfileLevel)) {
+ return InitPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ nsPrintfCString("fail to configurate profile level:%d",
+ specific.mProfileLevel)),
+ __func__);
+ }
+ }
+
+ CFBooleanRef isUsingHW = nullptr;
+ status = VTSessionCopyProperty(
+ mSession, kVTCompressionPropertyKey_UsingHardwareAcceleratedVideoEncoder,
+ kCFAllocatorDefault, &isUsingHW);
+ mIsHardwareAccelerated = status == noErr && isUsingHW == kCFBooleanTrue;
+ if (isUsingHW) {
+ CFRelease(isUsingHW);
+ }
+
+ mError = NS_OK;
+ return InitPromise::CreateAndResolve(TrackInfo::TrackType::kVideoTrack,
+ __func__);
+}
+
+static Maybe<OSType> MapPixelFormat(MediaDataEncoder::PixelFormat aFormat) {
+ switch (aFormat) {
+ case MediaDataEncoder::PixelFormat::RGBA32:
+ case MediaDataEncoder::PixelFormat::BGRA32:
+ return Some(kCVPixelFormatType_32BGRA);
+ case MediaDataEncoder::PixelFormat::RGB24:
+ return Some(kCVPixelFormatType_24RGB);
+ case MediaDataEncoder::PixelFormat::BGR24:
+ return Some(kCVPixelFormatType_24BGR);
+ case MediaDataEncoder::PixelFormat::GRAY8:
+ return Some(kCVPixelFormatType_OneComponent8);
+ case MediaDataEncoder::PixelFormat::YUV444P:
+ return Some(kCVPixelFormatType_444YpCbCr8);
+ case MediaDataEncoder::PixelFormat::YUV420P:
+ return Some(kCVPixelFormatType_420YpCbCr8PlanarFullRange);
+ case MediaDataEncoder::PixelFormat::YUV420SP_NV12:
+ return Some(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange);
+ default:
+ return Nothing();
+ }
+}
+
+CFDictionaryRef AppleVTEncoder::BuildSourceImageBufferAttributes() {
+ Maybe<OSType> fmt = MapPixelFormat(mConfig.mSourcePixelFormat);
+ if (fmt.isNothing()) {
+ VTENC_LOGE("unsupported source pixel format");
+ return nullptr;
+ }
+
+ // Source image buffer attributes
+ const void* keys[] = {kCVPixelBufferOpenGLCompatibilityKey, // TODO
+ kCVPixelBufferIOSurfacePropertiesKey, // TODO
+ kCVPixelBufferPixelFormatTypeKey};
+
+ AutoCFRelease<CFDictionaryRef> ioSurfaceProps(CFDictionaryCreate(
+ kCFAllocatorDefault, nullptr, nullptr, 0, &kCFTypeDictionaryKeyCallBacks,
+ &kCFTypeDictionaryValueCallBacks));
+ AutoCFRelease<CFNumberRef> pixelFormat(
+ CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &fmt));
+ const void* values[] = {kCFBooleanTrue, ioSurfaceProps, pixelFormat};
+
+ MOZ_ASSERT(ArrayLength(keys) == ArrayLength(values),
+ "Non matching keys/values array size");
+
+ return CFDictionaryCreate(kCFAllocatorDefault, keys, values,
+ ArrayLength(keys), &kCFTypeDictionaryKeyCallBacks,
+ &kCFTypeDictionaryValueCallBacks);
+}
+
+static bool IsKeyframe(CMSampleBufferRef aSample) {
+ CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(aSample, 0);
+ if (attachments == nullptr || CFArrayGetCount(attachments) == 0) {
+ return false;
+ }
+
+ return !CFDictionaryContainsKey(
+ static_cast<CFDictionaryRef>(CFArrayGetValueAtIndex(attachments, 0)),
+ kCMSampleAttachmentKey_NotSync);
+}
+
+static size_t GetNumParamSets(CMFormatDescriptionRef aDescription) {
+ size_t numParamSets = 0;
+ OSStatus status = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
+ aDescription, 0, nullptr, nullptr, &numParamSets, nullptr);
+ if (status != noErr) {
+ VTENC_LOGE("Cannot get number of parameter sets from format description");
+ }
+
+ return numParamSets;
+}
+
+static const uint8_t kNALUStart[4] = {0, 0, 0, 1};
+
+static size_t GetParamSet(CMFormatDescriptionRef aDescription, size_t aIndex,
+ const uint8_t** aDataPtr) {
+ size_t length = 0;
+ int headerSize = 0;
+ if (CMVideoFormatDescriptionGetH264ParameterSetAtIndex(
+ aDescription, aIndex, aDataPtr, &length, nullptr, &headerSize) !=
+ noErr) {
+ VTENC_LOGE("fail to get parameter set from format description");
+ return 0;
+ }
+ MOZ_ASSERT(headerSize == sizeof(kNALUStart), "Only support 4 byte header");
+
+ return length;
+}
+
+static bool WriteSPSPPS(MediaRawData* aDst,
+ CMFormatDescriptionRef aDescription) {
+ // Get SPS/PPS
+ const size_t numParamSets = GetNumParamSets(aDescription);
+ UniquePtr<MediaRawDataWriter> writer(aDst->CreateWriter());
+ for (size_t i = 0; i < numParamSets; i++) {
+ const uint8_t* data = nullptr;
+ size_t length = GetParamSet(aDescription, i, &data);
+ if (length == 0) {
+ return false;
+ }
+ if (!writer->Append(kNALUStart, sizeof(kNALUStart))) {
+ VTENC_LOGE("Cannot write NAL unit start code");
+ return false;
+ }
+ if (!writer->Append(data, length)) {
+ VTENC_LOGE("Cannot write parameter set");
+ return false;
+ }
+ }
+ return true;
+}
+
+static RefPtr<MediaByteBuffer> extractAvcc(
+ CMFormatDescriptionRef aDescription) {
+ CFPropertyListRef list = CMFormatDescriptionGetExtension(
+ aDescription,
+ kCMFormatDescriptionExtension_SampleDescriptionExtensionAtoms);
+ if (!list) {
+ VTENC_LOGE("fail to get atoms");
+ return nullptr;
+ }
+ CFDataRef avcC = static_cast<CFDataRef>(
+ CFDictionaryGetValue(static_cast<CFDictionaryRef>(list), CFSTR("avcC")));
+ if (!avcC) {
+ VTENC_LOGE("fail to extract avcC");
+ return nullptr;
+ }
+ CFIndex length = CFDataGetLength(avcC);
+ const UInt8* bytes = CFDataGetBytePtr(avcC);
+ if (length <= 0 || !bytes) {
+ VTENC_LOGE("empty avcC");
+ return nullptr;
+ }
+
+ RefPtr<MediaByteBuffer> config = new MediaByteBuffer(length);
+ config->AppendElements(bytes, length);
+ return config;
+}
+
+bool AppleVTEncoder::WriteExtraData(MediaRawData* aDst, CMSampleBufferRef aSrc,
+ const bool aAsAnnexB) {
+ if (!IsKeyframe(aSrc)) {
+ return true;
+ }
+
+ aDst->mKeyframe = true;
+ CMFormatDescriptionRef desc = CMSampleBufferGetFormatDescription(aSrc);
+ if (!desc) {
+ VTENC_LOGE("fail to get format description from sample");
+ return false;
+ }
+
+ if (aAsAnnexB) {
+ return WriteSPSPPS(aDst, desc);
+ }
+
+ RefPtr<MediaByteBuffer> avcc = extractAvcc(desc);
+ if (!avcc) {
+ return false;
+ }
+
+ if (!mAvcc || !H264::CompareExtraData(avcc, mAvcc)) {
+ mAvcc = avcc;
+ aDst->mExtraData = mAvcc;
+ }
+
+ return avcc != nullptr;
+}
+
+static bool WriteNALUs(MediaRawData* aDst, CMSampleBufferRef aSrc,
+ bool aAsAnnexB = false) {
+ size_t srcRemaining = CMSampleBufferGetTotalSampleSize(aSrc);
+ CMBlockBufferRef block = CMSampleBufferGetDataBuffer(aSrc);
+ if (!block) {
+ VTENC_LOGE("Cannot get block buffer frome sample");
+ return false;
+ }
+ UniquePtr<MediaRawDataWriter> writer(aDst->CreateWriter());
+ size_t writtenLength = aDst->Size();
+ // Ensure capacity.
+ if (!writer->SetSize(writtenLength + srcRemaining)) {
+ VTENC_LOGE("Cannot allocate buffer");
+ return false;
+ }
+ size_t readLength = 0;
+ while (srcRemaining > 0) {
+ // Extract the size of next NAL unit
+ uint8_t unitSizeBytes[4];
+ MOZ_ASSERT(srcRemaining > sizeof(unitSizeBytes));
+ if (CMBlockBufferCopyDataBytes(block, readLength, sizeof(unitSizeBytes),
+ reinterpret_cast<uint32_t*>(
+ unitSizeBytes)) != kCMBlockBufferNoErr) {
+ VTENC_LOGE("Cannot copy unit size bytes");
+ return false;
+ }
+ size_t unitSize =
+ CFSwapInt32BigToHost(*reinterpret_cast<uint32_t*>(unitSizeBytes));
+
+ if (aAsAnnexB) {
+ // Replace unit size bytes with NALU start code.
+ PodCopy(writer->Data() + writtenLength, kNALUStart, sizeof(kNALUStart));
+ readLength += sizeof(unitSizeBytes);
+ srcRemaining -= sizeof(unitSizeBytes);
+ writtenLength += sizeof(kNALUStart);
+ } else {
+ // Copy unit size bytes + data.
+ unitSize += sizeof(unitSizeBytes);
+ }
+ MOZ_ASSERT(writtenLength + unitSize <= aDst->Size());
+ // Copy NAL unit data
+ if (CMBlockBufferCopyDataBytes(block, readLength, unitSize,
+ writer->Data() + writtenLength) !=
+ kCMBlockBufferNoErr) {
+ VTENC_LOGE("Cannot copy unit data");
+ return false;
+ }
+ readLength += unitSize;
+ srcRemaining -= unitSize;
+ writtenLength += unitSize;
+ }
+ MOZ_ASSERT(writtenLength == aDst->Size());
+ return true;
+}
+
+void AppleVTEncoder::OutputFrame(CMSampleBufferRef aBuffer) {
+ RefPtr<MediaRawData> output(new MediaRawData());
+
+ bool asAnnexB = mConfig.mUsage == Usage::Realtime;
+ bool succeeded = WriteExtraData(output, aBuffer, asAnnexB) &&
+ WriteNALUs(output, aBuffer, asAnnexB);
+
+ output->mTime = media::TimeUnit::FromSeconds(
+ CMTimeGetSeconds(CMSampleBufferGetPresentationTimeStamp(aBuffer)));
+ output->mDuration = media::TimeUnit::FromSeconds(
+ CMTimeGetSeconds(CMSampleBufferGetOutputDuration(aBuffer)));
+ ProcessOutput(succeeded ? std::move(output) : nullptr);
+}
+
+void AppleVTEncoder::ProcessOutput(RefPtr<MediaRawData>&& aOutput) {
+ if (!mTaskQueue->IsCurrentThreadIn()) {
+ nsresult rv = mTaskQueue->Dispatch(NewRunnableMethod<RefPtr<MediaRawData>>(
+ "AppleVTEncoder::ProcessOutput", this, &AppleVTEncoder::ProcessOutput,
+ std::move(aOutput)));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ return;
+ }
+ AssertOnTaskQueue();
+
+ if (aOutput) {
+ mEncodedData.AppendElement(std::move(aOutput));
+ } else {
+ mError = NS_ERROR_DOM_MEDIA_FATAL_ERR;
+ }
+}
+
+RefPtr<MediaDataEncoder::EncodePromise> AppleVTEncoder::Encode(
+ const MediaData* aSample) {
+ MOZ_ASSERT(aSample != nullptr);
+ RefPtr<const VideoData> sample(aSample->As<const VideoData>());
+
+ return InvokeAsync<RefPtr<const VideoData>>(mTaskQueue, this, __func__,
+ &AppleVTEncoder::ProcessEncode,
+ std::move(sample));
+}
+
+RefPtr<MediaDataEncoder::EncodePromise> AppleVTEncoder::ProcessEncode(
+ RefPtr<const VideoData> aSample) {
+ AssertOnTaskQueue();
+ MOZ_ASSERT(mSession);
+
+ if (NS_FAILED(mError)) {
+ return EncodePromise::CreateAndReject(mError, __func__);
+ }
+
+ AutoCVBufferRelease<CVImageBufferRef> buffer(
+ CreateCVPixelBuffer(aSample->mImage));
+ if (!buffer) {
+ return EncodePromise::CreateAndReject(NS_ERROR_OUT_OF_MEMORY, __func__);
+ }
+
+ CFDictionaryRef frameProps = nullptr;
+ if (aSample->mKeyframe) {
+ CFTypeRef keys[] = {kVTEncodeFrameOptionKey_ForceKeyFrame};
+ CFTypeRef values[] = {kCFBooleanTrue};
+ MOZ_ASSERT(ArrayLength(keys) == ArrayLength(values));
+ frameProps = CFDictionaryCreate(
+ kCFAllocatorDefault, keys, values, ArrayLength(keys),
+ &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
+ };
+
+ VTEncodeInfoFlags info;
+ OSStatus status = VTCompressionSessionEncodeFrame(
+ mSession, buffer,
+ CMTimeMake(aSample->mTime.ToMicroseconds(), USECS_PER_S),
+ CMTimeMake(aSample->mDuration.ToMicroseconds(), USECS_PER_S), frameProps,
+ nullptr /* sourceFrameRefcon */, &info);
+ if (status != noErr) {
+ return EncodePromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ __func__);
+ }
+
+ return EncodePromise::CreateAndResolve(std::move(mEncodedData), __func__);
+}
+
+static size_t NumberOfPlanes(MediaDataEncoder::PixelFormat aPixelFormat) {
+ switch (aPixelFormat) {
+ case MediaDataEncoder::PixelFormat::RGBA32:
+ case MediaDataEncoder::PixelFormat::BGRA32:
+ case MediaDataEncoder::PixelFormat::RGB24:
+ case MediaDataEncoder::PixelFormat::BGR24:
+ case MediaDataEncoder::PixelFormat::GRAY8:
+ return 1;
+ case MediaDataEncoder::PixelFormat::YUV444P:
+ case MediaDataEncoder::PixelFormat::YUV420P:
+ return 3;
+ case MediaDataEncoder::PixelFormat::YUV420SP_NV12:
+ return 2;
+ default:
+ VTENC_LOGE("Unsupported input pixel format");
+ return 0;
+ }
+}
+
+using namespace layers;
+
+static void ReleaseImage(void* aImageGrip, const void* aDataPtr,
+ size_t aDataSize, size_t aNumOfPlanes,
+ const void** aPlanes) {
+ (static_cast<PlanarYCbCrImage*>(aImageGrip))->Release();
+}
+
+CVPixelBufferRef AppleVTEncoder::CreateCVPixelBuffer(const Image* aSource) {
+ AssertOnTaskQueue();
+
+ // TODO: support types other than YUV
+ PlanarYCbCrImage* image = const_cast<Image*>(aSource)->AsPlanarYCbCrImage();
+ if (!image || !image->GetData()) {
+ return nullptr;
+ }
+
+ OSType format = MapPixelFormat(mConfig.mSourcePixelFormat).ref();
+ size_t numPlanes = NumberOfPlanes(mConfig.mSourcePixelFormat);
+ const PlanarYCbCrImage::Data* yuv = image->GetData();
+ if (!yuv) {
+ return nullptr;
+ }
+ auto ySize = yuv->YDataSize();
+ auto cbcrSize = yuv->CbCrDataSize();
+ void* addresses[3] = {};
+ size_t widths[3] = {};
+ size_t heights[3] = {};
+ size_t strides[3] = {};
+ switch (numPlanes) {
+ case 3:
+ addresses[2] = yuv->mCrChannel;
+ widths[2] = cbcrSize.width;
+ heights[2] = cbcrSize.height;
+ strides[2] = yuv->mCbCrStride;
+ [[fallthrough]];
+ case 2:
+ addresses[1] = yuv->mCbChannel;
+ widths[1] = cbcrSize.width;
+ heights[1] = cbcrSize.height;
+ strides[1] = yuv->mCbCrStride;
+ [[fallthrough]];
+ case 1:
+ addresses[0] = yuv->mYChannel;
+ widths[0] = ySize.width;
+ heights[0] = ySize.height;
+ strides[0] = yuv->mYStride;
+ break;
+ default:
+ return nullptr;
+ }
+
+ CVPixelBufferRef buffer = nullptr;
+ image->AddRef(); // Grip input buffers.
+ CVReturn rv = CVPixelBufferCreateWithPlanarBytes(
+ kCFAllocatorDefault, yuv->mPictureRect.width, yuv->mPictureRect.height,
+ format, nullptr /* dataPtr */, 0 /* dataSize */, numPlanes, addresses,
+ widths, heights, strides, ReleaseImage /* releaseCallback */,
+ image /* releaseRefCon */, nullptr /* pixelBufferAttributes */, &buffer);
+ if (rv == kCVReturnSuccess) {
+ return buffer;
+ // |image| will be released in |ReleaseImage()|.
+ } else {
+ image->Release();
+ return nullptr;
+ }
+}
+
+RefPtr<MediaDataEncoder::EncodePromise> AppleVTEncoder::Drain() {
+ return InvokeAsync(mTaskQueue, this, __func__, &AppleVTEncoder::ProcessDrain);
+}
+
+RefPtr<MediaDataEncoder::EncodePromise> AppleVTEncoder::ProcessDrain() {
+ AssertOnTaskQueue();
+ MOZ_ASSERT(mSession);
+
+ if (mFramesCompleted) {
+ MOZ_DIAGNOSTIC_ASSERT(mEncodedData.IsEmpty());
+ return EncodePromise::CreateAndResolve(EncodedData(), __func__);
+ }
+
+ OSStatus status =
+ VTCompressionSessionCompleteFrames(mSession, kCMTimeIndefinite);
+ if (status != noErr) {
+ return EncodePromise::CreateAndReject(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ __func__);
+ }
+ mFramesCompleted = true;
+ // VTCompressionSessionCompleteFrames() could have queued multiple tasks with
+ // the new drained frames. Dispatch a task after them to resolve the promise
+ // with those frames.
+ RefPtr<AppleVTEncoder> self = this;
+ return InvokeAsync(mTaskQueue, __func__, [self]() {
+ EncodedData pendingFrames(std::move(self->mEncodedData));
+ self->mEncodedData = EncodedData();
+ return EncodePromise::CreateAndResolve(std::move(pendingFrames), __func__);
+ });
+}
+
+RefPtr<ShutdownPromise> AppleVTEncoder::Shutdown() {
+ return InvokeAsync(mTaskQueue, this, __func__,
+ &AppleVTEncoder::ProcessShutdown);
+}
+
+RefPtr<ShutdownPromise> AppleVTEncoder::ProcessShutdown() {
+ if (mSession) {
+ VTCompressionSessionInvalidate(mSession);
+ CFRelease(mSession);
+ mSession = nullptr;
+ mInited = false;
+ }
+ return ShutdownPromise::CreateAndResolve(true, __func__);
+}
+
+RefPtr<GenericPromise> AppleVTEncoder::SetBitrate(
+ MediaDataEncoder::Rate aBitsPerSec) {
+ RefPtr<AppleVTEncoder> self = this;
+ return InvokeAsync(mTaskQueue, __func__, [self, aBitsPerSec]() {
+ MOZ_ASSERT(self->mSession);
+ return SetAverageBitrate(self->mSession, aBitsPerSec)
+ ? GenericPromise::CreateAndResolve(true, __func__)
+ : GenericPromise::CreateAndReject(
+ NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR, __func__);
+ });
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/apple/AppleVTEncoder.h b/dom/media/platforms/apple/AppleVTEncoder.h
new file mode 100644
index 0000000000..7f12f7ebb5
--- /dev/null
+++ b/dom/media/platforms/apple/AppleVTEncoder.h
@@ -0,0 +1,85 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_AppleVTEncoder_h_
+#define mozilla_AppleVTEncoder_h_
+
+#include <CoreMedia/CoreMedia.h>
+#include <VideoToolbox/VideoToolbox.h>
+
+#include "PlatformEncoderModule.h"
+#include "TimeUnits.h"
+
+namespace mozilla {
+
+namespace layers {
+class Image;
+}
+
+class AppleVTEncoder final : public MediaDataEncoder {
+ public:
+ using Config = H264Config;
+
+ AppleVTEncoder(const Config& aConfig, RefPtr<TaskQueue> aTaskQueue,
+ const bool aHwardwareNotAllowed)
+ : mConfig(aConfig),
+ mTaskQueue(aTaskQueue),
+ mHardwareNotAllowed(aHwardwareNotAllowed),
+ mFramesCompleted(false),
+ mError(NS_OK),
+ mSession(nullptr) {
+ MOZ_ASSERT(mConfig.mSize.width > 0 && mConfig.mSize.height > 0);
+ MOZ_ASSERT(mTaskQueue);
+ }
+
+ RefPtr<InitPromise> Init() override;
+ RefPtr<EncodePromise> Encode(const MediaData* aSample) override;
+ RefPtr<EncodePromise> Drain() override;
+ RefPtr<ShutdownPromise> Shutdown() override;
+ RefPtr<GenericPromise> SetBitrate(Rate aBitsPerSec) override;
+
+ nsCString GetDescriptionName() const override {
+ MOZ_ASSERT(mSession);
+ return mIsHardwareAccelerated ? "apple hardware VT encoder"_ns
+ : "apple software VT encoder"_ns;
+ }
+
+ void OutputFrame(CMSampleBufferRef aBuffer);
+
+ private:
+ virtual ~AppleVTEncoder() { MOZ_ASSERT(!mSession); }
+ RefPtr<EncodePromise> ProcessEncode(RefPtr<const VideoData> aSample);
+ void ProcessOutput(RefPtr<MediaRawData>&& aOutput);
+ void ResolvePromise();
+ RefPtr<EncodePromise> ProcessDrain();
+ RefPtr<ShutdownPromise> ProcessShutdown();
+
+ CFDictionaryRef BuildSourceImageBufferAttributes();
+ CVPixelBufferRef CreateCVPixelBuffer(const layers::Image* aSource);
+ bool WriteExtraData(MediaRawData* aDst, CMSampleBufferRef aSrc,
+ const bool aAsAnnexB);
+ void AssertOnTaskQueue() { MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn()); }
+
+ const Config mConfig;
+ const RefPtr<TaskQueue> mTaskQueue;
+ const bool mHardwareNotAllowed;
+ // Access only in mTaskQueue.
+ EncodedData mEncodedData;
+ bool mFramesCompleted;
+ RefPtr<MediaByteBuffer> mAvcc; // Stores latest avcC data.
+ MediaResult mError;
+
+ // Written by Init() but used only in task queue.
+ VTCompressionSessionRef mSession;
+ // Can be accessed on any thread, but only written on during init.
+ Atomic<bool> mIsHardwareAccelerated;
+ // Written during init and shutdown.
+ Atomic<bool> mInited;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_AppleVTEncoder_h_
diff --git a/dom/media/platforms/ffmpeg/FFmpegAudioDecoder.cpp b/dom/media/platforms/ffmpeg/FFmpegAudioDecoder.cpp
new file mode 100644
index 0000000000..b6b3d7687e
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/FFmpegAudioDecoder.cpp
@@ -0,0 +1,421 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FFmpegAudioDecoder.h"
+#include "FFmpegLog.h"
+#include "TimeUnits.h"
+#include "VideoUtils.h"
+#include "BufferReader.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/Telemetry.h"
+
+namespace mozilla {
+
+using TimeUnit = media::TimeUnit;
+
+FFmpegAudioDecoder<LIBAV_VER>::FFmpegAudioDecoder(FFmpegLibWrapper* aLib,
+ const AudioInfo& aConfig)
+ : FFmpegDataDecoder(aLib, GetCodecId(aConfig.mMimeType)) {
+ MOZ_COUNT_CTOR(FFmpegAudioDecoder);
+
+ if (mCodecID == AV_CODEC_ID_AAC &&
+ aConfig.mCodecSpecificConfig.is<AacCodecSpecificData>()) {
+ const AacCodecSpecificData& aacCodecSpecificData =
+ aConfig.mCodecSpecificConfig.as<AacCodecSpecificData>();
+ mExtraData = new MediaByteBuffer;
+ // Ffmpeg expects the DecoderConfigDescriptor blob.
+ mExtraData->AppendElements(
+ *aacCodecSpecificData.mDecoderConfigDescriptorBinaryBlob);
+ mEncoderDelay = aacCodecSpecificData.mEncoderDelayFrames;
+ mEncoderPaddingOrTotalFrames = aacCodecSpecificData.mMediaFrameCount;
+ FFMPEG_LOG("FFmpegAudioDecoder (aac), found encoder delay (%" PRIu32
+ ") and total frame count (%" PRIu64
+ ") in codec-specific side data",
+ mEncoderDelay, TotalFrames());
+ return;
+ }
+
+ if (mCodecID == AV_CODEC_ID_MP3) {
+ // Downgraded from diagnostic assert due to BMO 1776524 on Android.
+ MOZ_ASSERT(aConfig.mCodecSpecificConfig.is<Mp3CodecSpecificData>());
+ // Gracefully handle bad data. If don't hit the preceding assert once this
+ // has been shipped for awhile, we can remove it and make the following code
+ // non-conditional.
+ if (aConfig.mCodecSpecificConfig.is<Mp3CodecSpecificData>()) {
+ const Mp3CodecSpecificData& mp3CodecSpecificData =
+ aConfig.mCodecSpecificConfig.as<Mp3CodecSpecificData>();
+ mEncoderDelay = mp3CodecSpecificData.mEncoderDelayFrames;
+ mEncoderPaddingOrTotalFrames = mp3CodecSpecificData.mEncoderPaddingFrames;
+ FFMPEG_LOG("FFmpegAudioDecoder (mp3), found encoder delay (%" PRIu32
+ ")"
+ "and padding values (%" PRIu64 ") in codec-specific side-data",
+ mEncoderDelay, Padding());
+ return;
+ }
+ }
+
+ if (mCodecID == AV_CODEC_ID_FLAC) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ aConfig.mCodecSpecificConfig.is<FlacCodecSpecificData>());
+ // Gracefully handle bad data. If don't hit the preceding assert once this
+ // has been shipped for awhile, we can remove it and make the following code
+ // non-conditional.
+ if (aConfig.mCodecSpecificConfig.is<FlacCodecSpecificData>()) {
+ const FlacCodecSpecificData& flacCodecSpecificData =
+ aConfig.mCodecSpecificConfig.as<FlacCodecSpecificData>();
+ if (flacCodecSpecificData.mStreamInfoBinaryBlob->IsEmpty()) {
+ // Flac files without headers will be missing stream info. In this case
+ // we don't want to feed ffmpeg empty extra data as it will fail, just
+ // early return.
+ return;
+ }
+ // Use a new MediaByteBuffer as the object will be modified during
+ // initialization.
+ mExtraData = new MediaByteBuffer;
+ mExtraData->AppendElements(*flacCodecSpecificData.mStreamInfoBinaryBlob);
+ return;
+ }
+ }
+
+ // Gracefully handle failure to cover all codec specific cases above. Once
+ // we're confident there is no fall through from these cases above, we should
+ // remove this code.
+ RefPtr<MediaByteBuffer> audioCodecSpecificBinaryBlob =
+ GetAudioCodecSpecificBlob(aConfig.mCodecSpecificConfig);
+ if (audioCodecSpecificBinaryBlob && audioCodecSpecificBinaryBlob->Length()) {
+ // Use a new MediaByteBuffer as the object will be modified during
+ // initialization.
+ mExtraData = new MediaByteBuffer;
+ mExtraData->AppendElements(*audioCodecSpecificBinaryBlob);
+ }
+}
+
+RefPtr<MediaDataDecoder::InitPromise> FFmpegAudioDecoder<LIBAV_VER>::Init() {
+ MediaResult rv = InitDecoder();
+
+ return NS_SUCCEEDED(rv)
+ ? InitPromise::CreateAndResolve(TrackInfo::kAudioTrack, __func__)
+ : InitPromise::CreateAndReject(rv, __func__);
+}
+
+void FFmpegAudioDecoder<LIBAV_VER>::InitCodecContext() {
+ MOZ_ASSERT(mCodecContext);
+ // We do not want to set this value to 0 as FFmpeg by default will
+ // use the number of cores, which with our mozlibavutil get_cpu_count
+ // isn't implemented.
+ mCodecContext->thread_count = 1;
+ // FFmpeg takes this as a suggestion for what format to use for audio samples.
+ // LibAV 0.8 produces rubbish float interleaved samples, request 16 bits
+ // audio.
+#ifdef MOZ_SAMPLE_TYPE_S16
+ mCodecContext->request_sample_fmt = AV_SAMPLE_FMT_S16;
+#else
+ mCodecContext->request_sample_fmt =
+ (mLib->mVersion == 53) ? AV_SAMPLE_FMT_S16 : AV_SAMPLE_FMT_FLT;
+#endif
+}
+
+static AlignedAudioBuffer CopyAndPackAudio(AVFrame* aFrame,
+ uint32_t aNumChannels,
+ uint32_t aNumAFrames) {
+ AlignedAudioBuffer audio(aNumChannels * aNumAFrames);
+ if (!audio) {
+ return audio;
+ }
+
+#ifdef MOZ_SAMPLE_TYPE_S16
+ if (aFrame->format == AV_SAMPLE_FMT_FLT) {
+ // Audio data already packed. Need to convert from 32 bits Float to S16
+ AudioDataValue* tmp = audio.get();
+ float* data = reinterpret_cast<float**>(aFrame->data)[0];
+ for (uint32_t frame = 0; frame < aNumAFrames; frame++) {
+ for (uint32_t channel = 0; channel < aNumChannels; channel++) {
+ *tmp++ = FloatToAudioSample<int16_t>(*data++);
+ }
+ }
+ } else if (aFrame->format == AV_SAMPLE_FMT_FLTP) {
+ // Planar audio data. Convert it from 32 bits float to S16
+ // and pack it into something we can understand.
+ AudioDataValue* tmp = audio.get();
+ float** data = reinterpret_cast<float**>(aFrame->data);
+ for (uint32_t frame = 0; frame < aNumAFrames; frame++) {
+ for (uint32_t channel = 0; channel < aNumChannels; channel++) {
+ *tmp++ = FloatToAudioSample<int16_t>(data[channel][frame]);
+ }
+ }
+ } else if (aFrame->format == AV_SAMPLE_FMT_S16) {
+ // Audio data already packed. No need to do anything other than copy it
+ // into a buffer we own.
+ memcpy(audio.get(), aFrame->data[0],
+ aNumChannels * aNumAFrames * sizeof(AudioDataValue));
+ } else if (aFrame->format == AV_SAMPLE_FMT_S16P) {
+ // Planar audio data. Pack it into something we can understand.
+ AudioDataValue* tmp = audio.get();
+ AudioDataValue** data = reinterpret_cast<AudioDataValue**>(aFrame->data);
+ for (uint32_t frame = 0; frame < aNumAFrames; frame++) {
+ for (uint32_t channel = 0; channel < aNumChannels; channel++) {
+ *tmp++ = data[channel][frame];
+ }
+ }
+ } else if (aFrame->format == AV_SAMPLE_FMT_S32) {
+ // Audio data already packed. Need to convert from S32 to S16
+ AudioDataValue* tmp = audio.get();
+ int32_t* data = reinterpret_cast<int32_t**>(aFrame->data)[0];
+ for (uint32_t frame = 0; frame < aNumAFrames; frame++) {
+ for (uint32_t channel = 0; channel < aNumChannels; channel++) {
+ *tmp++ = *data++ / (1U << 16);
+ }
+ }
+ } else if (aFrame->format == AV_SAMPLE_FMT_S32P) {
+ // Planar audio data. Convert it from S32 to S16
+ // and pack it into something we can understand.
+ AudioDataValue* tmp = audio.get();
+ int32_t** data = reinterpret_cast<int32_t**>(aFrame->data);
+ for (uint32_t frame = 0; frame < aNumAFrames; frame++) {
+ for (uint32_t channel = 0; channel < aNumChannels; channel++) {
+ *tmp++ = data[channel][frame] / (1U << 16);
+ }
+ }
+ }
+#else
+ if (aFrame->format == AV_SAMPLE_FMT_FLT) {
+ // Audio data already packed. No need to do anything other than copy it
+ // into a buffer we own.
+ memcpy(audio.get(), aFrame->data[0],
+ aNumChannels * aNumAFrames * sizeof(AudioDataValue));
+ } else if (aFrame->format == AV_SAMPLE_FMT_FLTP) {
+ // Planar audio data. Pack it into something we can understand.
+ AudioDataValue* tmp = audio.get();
+ AudioDataValue** data = reinterpret_cast<AudioDataValue**>(aFrame->data);
+ for (uint32_t frame = 0; frame < aNumAFrames; frame++) {
+ for (uint32_t channel = 0; channel < aNumChannels; channel++) {
+ *tmp++ = data[channel][frame];
+ }
+ }
+ } else if (aFrame->format == AV_SAMPLE_FMT_S16) {
+ // Audio data already packed. Need to convert from S16 to 32 bits Float
+ AudioDataValue* tmp = audio.get();
+ int16_t* data = reinterpret_cast<int16_t**>(aFrame->data)[0];
+ for (uint32_t frame = 0; frame < aNumAFrames; frame++) {
+ for (uint32_t channel = 0; channel < aNumChannels; channel++) {
+ *tmp++ = AudioSampleToFloat(*data++);
+ }
+ }
+ } else if (aFrame->format == AV_SAMPLE_FMT_S16P) {
+ // Planar audio data. Convert it from S16 to 32 bits float
+ // and pack it into something we can understand.
+ AudioDataValue* tmp = audio.get();
+ int16_t** data = reinterpret_cast<int16_t**>(aFrame->data);
+ for (uint32_t frame = 0; frame < aNumAFrames; frame++) {
+ for (uint32_t channel = 0; channel < aNumChannels; channel++) {
+ *tmp++ = AudioSampleToFloat(data[channel][frame]);
+ }
+ }
+ } else if (aFrame->format == AV_SAMPLE_FMT_S32) {
+ // Audio data already packed. Need to convert from S16 to 32 bits Float
+ AudioDataValue* tmp = audio.get();
+ int32_t* data = reinterpret_cast<int32_t**>(aFrame->data)[0];
+ for (uint32_t frame = 0; frame < aNumAFrames; frame++) {
+ for (uint32_t channel = 0; channel < aNumChannels; channel++) {
+ *tmp++ = AudioSampleToFloat(*data++);
+ }
+ }
+ } else if (aFrame->format == AV_SAMPLE_FMT_S32P) {
+ // Planar audio data. Convert it from S32 to 32 bits float
+ // and pack it into something we can understand.
+ AudioDataValue* tmp = audio.get();
+ int32_t** data = reinterpret_cast<int32_t**>(aFrame->data);
+ for (uint32_t frame = 0; frame < aNumAFrames; frame++) {
+ for (uint32_t channel = 0; channel < aNumChannels; channel++) {
+ *tmp++ = AudioSampleToFloat(data[channel][frame]);
+ }
+ }
+ }
+#endif
+
+ return audio;
+}
+
+using ChannelLayout = AudioConfig::ChannelLayout;
+
+uint64_t FFmpegAudioDecoder<LIBAV_VER>::Padding() const {
+ MOZ_ASSERT(mCodecID == AV_CODEC_ID_MP3);
+ return mEncoderPaddingOrTotalFrames;
+}
+uint64_t FFmpegAudioDecoder<LIBAV_VER>::TotalFrames() const {
+ MOZ_ASSERT(mCodecID == AV_CODEC_ID_AAC);
+ return mEncoderPaddingOrTotalFrames;
+}
+
+MediaResult FFmpegAudioDecoder<LIBAV_VER>::DoDecode(MediaRawData* aSample,
+ uint8_t* aData, int aSize,
+ bool* aGotFrame,
+ DecodedData& aResults) {
+ MOZ_ASSERT(mTaskQueue->IsOnCurrentThread());
+ PROCESS_DECODE_LOG(aSample);
+ AVPacket packet;
+ mLib->av_init_packet(&packet);
+
+ packet.data = const_cast<uint8_t*>(aData);
+ packet.size = aSize;
+
+ if (aGotFrame) {
+ *aGotFrame = false;
+ }
+
+ if (!PrepareFrame()) {
+ FFMPEG_LOG("FFmpegAudioDecoder: OOM in PrepareFrame");
+ return MediaResult(
+ NS_ERROR_OUT_OF_MEMORY,
+ RESULT_DETAIL("FFmpeg audio decoder failed to allocate frame"));
+ }
+
+ int64_t samplePosition = aSample->mOffset;
+
+ while (packet.size > 0) {
+ int decoded = false;
+ int bytesConsumed = -1;
+#if LIBAVCODEC_VERSION_MAJOR < 59
+ bytesConsumed =
+ mLib->avcodec_decode_audio4(mCodecContext, mFrame, &decoded, &packet);
+ if (bytesConsumed < 0) {
+ NS_WARNING("FFmpeg audio decoder error.");
+ return MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ RESULT_DETAIL("FFmpeg audio error:%d", bytesConsumed));
+ }
+#else
+# define AVRESULT_OK 0
+ int ret = mLib->avcodec_send_packet(mCodecContext, &packet);
+ switch (ret) {
+ case AVRESULT_OK:
+ bytesConsumed = packet.size;
+ break;
+ case AVERROR(EAGAIN):
+ break;
+ case AVERROR_EOF:
+ FFMPEG_LOG(" End of stream.");
+ return MediaResult(NS_ERROR_DOM_MEDIA_END_OF_STREAM,
+ RESULT_DETAIL("End of stream"));
+ default:
+ NS_WARNING("FFmpeg audio decoder error.");
+ return MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ RESULT_DETAIL("FFmpeg audio error"));
+ }
+
+ ret = mLib->avcodec_receive_frame(mCodecContext, mFrame);
+ switch (ret) {
+ case AVRESULT_OK:
+ decoded = true;
+ break;
+ case AVERROR(EAGAIN):
+ break;
+ case AVERROR_EOF: {
+ FFMPEG_LOG(" End of stream.");
+ return MediaResult(NS_ERROR_DOM_MEDIA_END_OF_STREAM,
+ RESULT_DETAIL("End of stream"));
+ }
+ }
+#endif
+
+ if (decoded) {
+ if (mFrame->format != AV_SAMPLE_FMT_FLT &&
+ mFrame->format != AV_SAMPLE_FMT_FLTP &&
+ mFrame->format != AV_SAMPLE_FMT_S16 &&
+ mFrame->format != AV_SAMPLE_FMT_S16P &&
+ mFrame->format != AV_SAMPLE_FMT_S32 &&
+ mFrame->format != AV_SAMPLE_FMT_S32P) {
+ return MediaResult(
+ NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ RESULT_DETAIL(
+ "FFmpeg audio decoder outputs unsupported audio format"));
+ }
+ uint32_t numChannels = mCodecContext->channels;
+ uint32_t samplingRate = mCodecContext->sample_rate;
+
+ AlignedAudioBuffer audio =
+ CopyAndPackAudio(mFrame, numChannels, mFrame->nb_samples);
+ if (!audio) {
+ FFMPEG_LOG("FFmpegAudioDecoder: OOM");
+ return MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__);
+ }
+
+ FFMPEG_LOG("Packet decoded: [%s, %s] (%" PRId64 "us, %d frames)",
+ aSample->mTime.ToString().get(),
+ aSample->GetEndTime().ToString().get(),
+ aSample->mDuration.ToMicroseconds(), mFrame->nb_samples);
+
+ media::TimeUnit duration = TimeUnit(mFrame->nb_samples, samplingRate);
+ if (!duration.IsValid()) {
+ FFMPEG_LOG("FFmpegAudioDecoder: invalid duration");
+ return MediaResult(NS_ERROR_DOM_MEDIA_OVERFLOW_ERR,
+ RESULT_DETAIL("Invalid sample duration"));
+ }
+
+ media::TimeUnit pts = aSample->mTime;
+ media::TimeUnit newpts = pts + duration;
+ if (!newpts.IsValid()) {
+ FFMPEG_LOG("FFmpegAudioDecoder: invalid PTS.");
+ return MediaResult(
+ NS_ERROR_DOM_MEDIA_OVERFLOW_ERR,
+ RESULT_DETAIL("Invalid count of accumulated audio samples"));
+ }
+
+ RefPtr<AudioData> data =
+ new AudioData(samplePosition, pts, std::move(audio), numChannels,
+ samplingRate, mCodecContext->channel_layout);
+ MOZ_ASSERT(duration == data->mDuration, "must be equal");
+ aResults.AppendElement(std::move(data));
+
+ pts = newpts;
+
+ if (aGotFrame) {
+ *aGotFrame = true;
+ }
+ }
+ // The packet wasn't sent to ffmpeg, another attempt will happen next
+ // iteration.
+ if (bytesConsumed != -1) {
+ packet.data += bytesConsumed;
+ packet.size -= bytesConsumed;
+ samplePosition += bytesConsumed;
+ }
+ }
+ return NS_OK;
+}
+
+AVCodecID FFmpegAudioDecoder<LIBAV_VER>::GetCodecId(
+ const nsACString& aMimeType) {
+ if (aMimeType.EqualsLiteral("audio/mpeg")) {
+#ifdef FFVPX_VERSION
+ if (!StaticPrefs::media_ffvpx_mp3_enabled()) {
+ return AV_CODEC_ID_NONE;
+ }
+#endif
+ return AV_CODEC_ID_MP3;
+ }
+ if (aMimeType.EqualsLiteral("audio/flac")) {
+ return AV_CODEC_ID_FLAC;
+ }
+ if (aMimeType.EqualsLiteral("audio/mp4a-latm")) {
+ return AV_CODEC_ID_AAC;
+ }
+
+ return AV_CODEC_ID_NONE;
+}
+
+nsCString FFmpegAudioDecoder<LIBAV_VER>::GetCodecName() const {
+#if LIBAVCODEC_VERSION_MAJOR > 53
+ return nsCString(mLib->avcodec_descriptor_get(mCodecID)->name);
+#else
+ return "unknown"_ns;
+#endif
+}
+
+FFmpegAudioDecoder<LIBAV_VER>::~FFmpegAudioDecoder() {
+ MOZ_COUNT_DTOR(FFmpegAudioDecoder);
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/ffmpeg/FFmpegAudioDecoder.h b/dom/media/platforms/ffmpeg/FFmpegAudioDecoder.h
new file mode 100644
index 0000000000..63f7f463b2
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/FFmpegAudioDecoder.h
@@ -0,0 +1,65 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef __FFmpegAACDecoder_h__
+#define __FFmpegAACDecoder_h__
+
+#include "FFmpegDataDecoder.h"
+#include "FFmpegLibWrapper.h"
+
+namespace mozilla {
+
+template <int V>
+class FFmpegAudioDecoder {};
+
+template <>
+class FFmpegAudioDecoder<LIBAV_VER>;
+DDLoggedTypeNameAndBase(FFmpegAudioDecoder<LIBAV_VER>,
+ FFmpegDataDecoder<LIBAV_VER>);
+
+template <>
+class FFmpegAudioDecoder<LIBAV_VER>
+ : public FFmpegDataDecoder<LIBAV_VER>,
+ public DecoderDoctorLifeLogger<FFmpegAudioDecoder<LIBAV_VER>> {
+ public:
+ FFmpegAudioDecoder(FFmpegLibWrapper* aLib, const AudioInfo& aConfig);
+ virtual ~FFmpegAudioDecoder();
+
+ RefPtr<InitPromise> Init() override;
+ void InitCodecContext() MOZ_REQUIRES(sMutex) override;
+ static AVCodecID GetCodecId(const nsACString& aMimeType);
+ nsCString GetDescriptionName() const override {
+#ifdef USING_MOZFFVPX
+ return "ffvpx audio decoder"_ns;
+#else
+ return "ffmpeg audio decoder"_ns;
+#endif
+ }
+ nsCString GetCodecName() const override;
+
+ private:
+ MediaResult DoDecode(MediaRawData* aSample, uint8_t* aData, int aSize,
+ bool* aGotFrame, DecodedData& aResults) override;
+ // This method is to be called only when decoding mp3, in order to correctly
+ // discard padding frames.
+ uint64_t Padding() const;
+ // This method is to be called only when decoding AAC, in order to correctly
+ // discard padding frames, based on the number of frames decoded and the total
+ // frame count of the media.
+ uint64_t TotalFrames() const;
+ // The number of frames of encoder delay, that need to be discarded at the
+ // beginning of the stream.
+ uint32_t mEncoderDelay = 0;
+ // This holds either the encoder padding (when this decoder decodes mp3), or
+ // the total frame count of the media (when this decoder decodes AAC).
+ // It is best accessed via the `Padding` and `TotalFrames` methods, for
+ // clarity.
+ uint64_t mEncoderPaddingOrTotalFrames = 0;
+};
+
+} // namespace mozilla
+
+#endif // __FFmpegAACDecoder_h__
diff --git a/dom/media/platforms/ffmpeg/FFmpegDataDecoder.cpp b/dom/media/platforms/ffmpeg/FFmpegDataDecoder.cpp
new file mode 100644
index 0000000000..77b8328c93
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/FFmpegDataDecoder.cpp
@@ -0,0 +1,308 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <string.h>
+#ifdef __GNUC__
+# include <unistd.h>
+#endif
+
+#include "FFmpegDataDecoder.h"
+#include "FFmpegLog.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/TaskQueue.h"
+#include "prsystem.h"
+#include "VideoUtils.h"
+
+namespace mozilla {
+
+StaticMutex FFmpegDataDecoder<LIBAV_VER>::sMutex;
+
+FFmpegDataDecoder<LIBAV_VER>::FFmpegDataDecoder(FFmpegLibWrapper* aLib,
+ AVCodecID aCodecID)
+ : mLib(aLib),
+ mCodecContext(nullptr),
+ mCodecParser(nullptr),
+ mFrame(nullptr),
+ mExtraData(nullptr),
+ mCodecID(aCodecID),
+ mTaskQueue(TaskQueue::Create(
+ GetMediaThreadPool(MediaThreadType::PLATFORM_DECODER),
+ "FFmpegDataDecoder")),
+ mLastInputDts(media::TimeUnit::FromNegativeInfinity()) {
+ MOZ_ASSERT(aLib);
+ MOZ_COUNT_CTOR(FFmpegDataDecoder);
+}
+
+FFmpegDataDecoder<LIBAV_VER>::~FFmpegDataDecoder() {
+ MOZ_COUNT_DTOR(FFmpegDataDecoder);
+ if (mCodecParser) {
+ mLib->av_parser_close(mCodecParser);
+ mCodecParser = nullptr;
+ }
+}
+
+MediaResult FFmpegDataDecoder<LIBAV_VER>::AllocateExtraData() {
+ if (mExtraData) {
+ mCodecContext->extradata_size = mExtraData->Length();
+ // FFmpeg may use SIMD instructions to access the data which reads the
+ // data in 32 bytes block. Must ensure we have enough data to read.
+ uint32_t padding_size =
+#if LIBAVCODEC_VERSION_MAJOR >= 58
+ AV_INPUT_BUFFER_PADDING_SIZE;
+#else
+ FF_INPUT_BUFFER_PADDING_SIZE;
+#endif
+ mCodecContext->extradata = static_cast<uint8_t*>(
+ mLib->av_malloc(mExtraData->Length() + padding_size));
+ if (!mCodecContext->extradata) {
+ return MediaResult(NS_ERROR_OUT_OF_MEMORY,
+ RESULT_DETAIL("Couldn't init ffmpeg extradata"));
+ }
+ memcpy(mCodecContext->extradata, mExtraData->Elements(),
+ mExtraData->Length());
+ } else {
+ mCodecContext->extradata_size = 0;
+ }
+
+ return NS_OK;
+}
+
+// Note: This doesn't run on the ffmpeg TaskQueue, it runs on some other media
+// taskqueue
+MediaResult FFmpegDataDecoder<LIBAV_VER>::InitDecoder() {
+ FFMPEG_LOG("Initialising FFmpeg decoder");
+
+ AVCodec* codec = FindAVCodec(mLib, mCodecID);
+ if (!codec) {
+ FFMPEG_LOG(" couldn't find ffmpeg decoder for codec id %d", mCodecID);
+ return MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("unable to find codec"));
+ }
+ // openh264 has broken decoding of some h264 videos so
+ // don't use it unless explicitly allowed for now.
+ if (!strcmp(codec->name, "libopenh264") &&
+ !StaticPrefs::media_ffmpeg_allow_openh264()) {
+ FFMPEG_LOG(" unable to find codec (openh264 disabled by pref)");
+ return MediaResult(
+ NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("unable to find codec (openh264 disabled by pref)"));
+ }
+ FFMPEG_LOG(" codec %s : %s", codec->name, codec->long_name);
+
+ StaticMutexAutoLock mon(sMutex);
+
+ if (!(mCodecContext = mLib->avcodec_alloc_context3(codec))) {
+ FFMPEG_LOG(" couldn't allocate ffmpeg context for codec %s", codec->name);
+ return MediaResult(NS_ERROR_OUT_OF_MEMORY,
+ RESULT_DETAIL("Couldn't init ffmpeg context"));
+ }
+
+ if (NeedParser()) {
+ MOZ_ASSERT(mCodecParser == nullptr);
+ mCodecParser = mLib->av_parser_init(mCodecID);
+ if (mCodecParser) {
+ mCodecParser->flags |= ParserFlags();
+ }
+ }
+ mCodecContext->opaque = this;
+
+ InitCodecContext();
+ MediaResult ret = AllocateExtraData();
+ if (NS_FAILED(ret)) {
+ FFMPEG_LOG(" couldn't allocate ffmpeg extra data for codec %s",
+ codec->name);
+ mLib->av_freep(&mCodecContext);
+ return ret;
+ }
+
+#if LIBAVCODEC_VERSION_MAJOR < 57
+ if (codec->capabilities & CODEC_CAP_DR1) {
+ mCodecContext->flags |= CODEC_FLAG_EMU_EDGE;
+ }
+#endif
+
+ if (mLib->avcodec_open2(mCodecContext, codec, nullptr) < 0) {
+ mLib->av_freep(&mCodecContext);
+ FFMPEG_LOG(" Couldn't open avcodec");
+ return MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Couldn't open avcodec"));
+ }
+
+ FFMPEG_LOG(" FFmpeg decoder init successful.");
+ return NS_OK;
+}
+
+RefPtr<ShutdownPromise> FFmpegDataDecoder<LIBAV_VER>::Shutdown() {
+ RefPtr<FFmpegDataDecoder<LIBAV_VER>> self = this;
+ return InvokeAsync(mTaskQueue, __func__, [self]() {
+ self->ProcessShutdown();
+ return self->mTaskQueue->BeginShutdown();
+ });
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> FFmpegDataDecoder<LIBAV_VER>::Decode(
+ MediaRawData* aSample) {
+ return InvokeAsync<MediaRawData*>(mTaskQueue, this, __func__,
+ &FFmpegDataDecoder::ProcessDecode, aSample);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise>
+FFmpegDataDecoder<LIBAV_VER>::ProcessDecode(MediaRawData* aSample) {
+ MOZ_ASSERT(mTaskQueue->IsOnCurrentThread());
+ PROCESS_DECODE_LOG(aSample);
+ bool gotFrame = false;
+ DecodedData results;
+ MediaResult rv = DoDecode(aSample, &gotFrame, results);
+ if (NS_FAILED(rv)) {
+ return DecodePromise::CreateAndReject(rv, __func__);
+ }
+ return DecodePromise::CreateAndResolve(std::move(results), __func__);
+}
+
+MediaResult FFmpegDataDecoder<LIBAV_VER>::DoDecode(
+ MediaRawData* aSample, bool* aGotFrame,
+ MediaDataDecoder::DecodedData& aResults) {
+ MOZ_ASSERT(mTaskQueue->IsOnCurrentThread());
+
+ uint8_t* inputData = const_cast<uint8_t*>(aSample->Data());
+ size_t inputSize = aSample->Size();
+
+ mLastInputDts = aSample->mTimecode;
+
+ if (inputData && mCodecParser) { // inputData is null when draining.
+ if (aGotFrame) {
+ *aGotFrame = false;
+ }
+ while (inputSize) {
+ uint8_t* data = inputData;
+ int size = inputSize;
+ int len = mLib->av_parser_parse2(
+ mCodecParser, mCodecContext, &data, &size, inputData, inputSize,
+ aSample->mTime.ToMicroseconds(), aSample->mTimecode.ToMicroseconds(),
+ aSample->mOffset);
+ if (size_t(len) > inputSize) {
+ return NS_ERROR_DOM_MEDIA_DECODE_ERR;
+ }
+ if (size) {
+ bool gotFrame = false;
+ MediaResult rv = DoDecode(aSample, data, size, &gotFrame, aResults);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (gotFrame && aGotFrame) {
+ *aGotFrame = true;
+ }
+ }
+ inputData += len;
+ inputSize -= len;
+ }
+ return NS_OK;
+ }
+ return DoDecode(aSample, inputData, inputSize, aGotFrame, aResults);
+}
+
+RefPtr<MediaDataDecoder::FlushPromise> FFmpegDataDecoder<LIBAV_VER>::Flush() {
+ return InvokeAsync(mTaskQueue, this, __func__,
+ &FFmpegDataDecoder<LIBAV_VER>::ProcessFlush);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> FFmpegDataDecoder<LIBAV_VER>::Drain() {
+ return InvokeAsync(mTaskQueue, this, __func__,
+ &FFmpegDataDecoder<LIBAV_VER>::ProcessDrain);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise>
+FFmpegDataDecoder<LIBAV_VER>::ProcessDrain() {
+ MOZ_ASSERT(mTaskQueue->IsOnCurrentThread());
+ RefPtr<MediaRawData> empty(new MediaRawData());
+ empty->mTimecode = mLastInputDts;
+ bool gotFrame = false;
+ DecodedData results;
+ // When draining the FFmpeg decoder will return either a single frame at a
+ // time until gotFrame is set to false; or return a block of frames with
+ // NS_ERROR_DOM_MEDIA_END_OF_STREAM
+ while (NS_SUCCEEDED(DoDecode(empty, &gotFrame, results)) && gotFrame) {
+ }
+ return DecodePromise::CreateAndResolve(std::move(results), __func__);
+}
+
+RefPtr<MediaDataDecoder::FlushPromise>
+FFmpegDataDecoder<LIBAV_VER>::ProcessFlush() {
+ MOZ_ASSERT(mTaskQueue->IsOnCurrentThread());
+ if (mCodecContext) {
+ FFMPEG_LOG("FFmpegDataDecoder: flushing buffers");
+ mLib->avcodec_flush_buffers(mCodecContext);
+ }
+ if (mCodecParser) {
+ FFMPEG_LOG("FFmpegDataDecoder: reinitializing parser");
+ mLib->av_parser_close(mCodecParser);
+ mCodecParser = mLib->av_parser_init(mCodecID);
+ }
+ return FlushPromise::CreateAndResolve(true, __func__);
+}
+
+void FFmpegDataDecoder<LIBAV_VER>::ProcessShutdown() {
+ MOZ_ASSERT(mTaskQueue->IsOnCurrentThread());
+ StaticMutexAutoLock mon(sMutex);
+
+ if (mCodecContext) {
+ FFMPEG_LOG("FFmpegDataDecoder: shutdown");
+ if (mCodecContext->extradata) {
+ mLib->av_freep(&mCodecContext->extradata);
+ }
+ mLib->avcodec_close(mCodecContext);
+ mLib->av_freep(&mCodecContext);
+#if LIBAVCODEC_VERSION_MAJOR >= 55
+ mLib->av_frame_free(&mFrame);
+#elif LIBAVCODEC_VERSION_MAJOR == 54
+ mLib->avcodec_free_frame(&mFrame);
+#else
+ mLib->av_freep(&mFrame);
+#endif
+ }
+}
+
+AVFrame* FFmpegDataDecoder<LIBAV_VER>::PrepareFrame() {
+ MOZ_ASSERT(mTaskQueue->IsOnCurrentThread());
+#if LIBAVCODEC_VERSION_MAJOR >= 55
+ if (mFrame) {
+ mLib->av_frame_unref(mFrame);
+ } else {
+ mFrame = mLib->av_frame_alloc();
+ }
+#elif LIBAVCODEC_VERSION_MAJOR == 54
+ if (mFrame) {
+ mLib->avcodec_get_frame_defaults(mFrame);
+ } else {
+ mFrame = mLib->avcodec_alloc_frame();
+ }
+#else
+ mLib->av_freep(&mFrame);
+ mFrame = mLib->avcodec_alloc_frame();
+#endif
+ return mFrame;
+}
+
+/* static */ AVCodec* FFmpegDataDecoder<LIBAV_VER>::FindAVCodec(
+ FFmpegLibWrapper* aLib, AVCodecID aCodec) {
+ return aLib->avcodec_find_decoder(aCodec);
+}
+
+#ifdef MOZ_WAYLAND
+/* static */ AVCodec* FFmpegDataDecoder<LIBAV_VER>::FindHardwareAVCodec(
+ FFmpegLibWrapper* aLib, AVCodecID aCodec) {
+ void* opaque = nullptr;
+ while (AVCodec* codec = aLib->av_codec_iterate(&opaque)) {
+ if (codec->id == aCodec && aLib->av_codec_is_decoder(codec) &&
+ aLib->avcodec_get_hw_config(codec, 0)) {
+ return codec;
+ }
+ }
+ return nullptr;
+}
+#endif
+
+} // namespace mozilla
diff --git a/dom/media/platforms/ffmpeg/FFmpegDataDecoder.h b/dom/media/platforms/ffmpeg/FFmpegDataDecoder.h
new file mode 100644
index 0000000000..54f13bf159
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/FFmpegDataDecoder.h
@@ -0,0 +1,90 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef __FFmpegDataDecoder_h__
+#define __FFmpegDataDecoder_h__
+
+#include "FFmpegLibWrapper.h"
+#include "PlatformDecoderModule.h"
+#include "mozilla/StaticMutex.h"
+
+// This must be the last header included
+#include "FFmpegLibs.h"
+
+namespace mozilla {
+
+template <int V>
+class FFmpegDataDecoder : public MediaDataDecoder {};
+
+template <>
+class FFmpegDataDecoder<LIBAV_VER>;
+DDLoggedTypeNameAndBase(FFmpegDataDecoder<LIBAV_VER>, MediaDataDecoder);
+
+template <>
+class FFmpegDataDecoder<LIBAV_VER>
+ : public MediaDataDecoder,
+ public DecoderDoctorLifeLogger<FFmpegDataDecoder<LIBAV_VER>> {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(FFmpegDataDecoder, final);
+
+ FFmpegDataDecoder(FFmpegLibWrapper* aLib, AVCodecID aCodecID);
+
+ static bool Link();
+
+ RefPtr<InitPromise> Init() override = 0;
+ RefPtr<DecodePromise> Decode(MediaRawData* aSample) override;
+ RefPtr<DecodePromise> Drain() override;
+ RefPtr<FlushPromise> Flush() override;
+ RefPtr<ShutdownPromise> Shutdown() override;
+
+ static AVCodec* FindAVCodec(FFmpegLibWrapper* aLib, AVCodecID aCodec);
+#ifdef MOZ_WAYLAND
+ static AVCodec* FindHardwareAVCodec(FFmpegLibWrapper* aLib, AVCodecID aCodec);
+#endif
+
+ protected:
+ // Flush and Drain operation, always run
+ virtual RefPtr<FlushPromise> ProcessFlush();
+ virtual void ProcessShutdown();
+ virtual void InitCodecContext() MOZ_REQUIRES(sMutex) {}
+ AVFrame* PrepareFrame();
+ MediaResult InitDecoder();
+ MediaResult AllocateExtraData();
+ MediaResult DoDecode(MediaRawData* aSample, bool* aGotFrame,
+ DecodedData& aResults);
+
+ FFmpegLibWrapper* mLib; // set in constructor
+
+ // mCodecContext is accessed on taskqueue only, no locking needed
+ AVCodecContext* mCodecContext;
+ AVCodecParserContext* mCodecParser;
+ AVFrame* mFrame;
+ RefPtr<MediaByteBuffer> mExtraData;
+ AVCodecID mCodecID; // set in constructor
+
+ protected:
+ virtual ~FFmpegDataDecoder();
+
+ static StaticMutex sMutex; // used to provide critical-section locking
+ // for calls into ffmpeg
+ const RefPtr<TaskQueue> mTaskQueue; // set in constructor
+
+ private:
+ RefPtr<DecodePromise> ProcessDecode(MediaRawData* aSample);
+ RefPtr<DecodePromise> ProcessDrain();
+ virtual MediaResult DoDecode(MediaRawData* aSample, uint8_t* aData, int aSize,
+ bool* aGotFrame,
+ MediaDataDecoder::DecodedData& aOutResults) = 0;
+ virtual bool NeedParser() const { return false; }
+ virtual int ParserFlags() const { return PARSER_FLAG_COMPLETE_FRAMES; }
+
+ MozPromiseHolder<DecodePromise> mPromise;
+ media::TimeUnit mLastInputDts; // used on Taskqueue
+};
+
+} // namespace mozilla
+
+#endif // __FFmpegDataDecoder_h__
diff --git a/dom/media/platforms/ffmpeg/FFmpegDecoderModule.cpp b/dom/media/platforms/ffmpeg/FFmpegDecoderModule.cpp
new file mode 100644
index 0000000000..81cf13daae
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/FFmpegDecoderModule.cpp
@@ -0,0 +1,13 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FFmpegDecoderModule.h"
+
+namespace mozilla {
+
+template class FFmpegDecoderModule<LIBAV_VER>;
+
+} // namespace mozilla
diff --git a/dom/media/platforms/ffmpeg/FFmpegDecoderModule.h b/dom/media/platforms/ffmpeg/FFmpegDecoderModule.h
new file mode 100644
index 0000000000..3b1a5b673a
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/FFmpegDecoderModule.h
@@ -0,0 +1,129 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef __FFmpegDecoderModule_h__
+#define __FFmpegDecoderModule_h__
+
+#include "FFmpegAudioDecoder.h"
+#include "FFmpegLibWrapper.h"
+#include "FFmpegVideoDecoder.h"
+#include "PlatformDecoderModule.h"
+#include "VideoUtils.h"
+#include "VPXDecoder.h"
+#include "mozilla/StaticPrefs_media.h"
+
+namespace mozilla {
+
+template <int V>
+class FFmpegDecoderModule : public PlatformDecoderModule {
+ public:
+ static already_AddRefed<PlatformDecoderModule> Create(
+ FFmpegLibWrapper* aLib) {
+ RefPtr<PlatformDecoderModule> pdm = new FFmpegDecoderModule(aLib);
+
+ return pdm.forget();
+ }
+
+ explicit FFmpegDecoderModule(FFmpegLibWrapper* aLib) : mLib(aLib) {}
+ virtual ~FFmpegDecoderModule() = default;
+
+ already_AddRefed<MediaDataDecoder> CreateVideoDecoder(
+ const CreateDecoderParams& aParams) override {
+ if (Supports(SupportDecoderParams(aParams), nullptr) ==
+ media::DecodeSupport::Unsupported) {
+ return nullptr;
+ }
+ RefPtr<MediaDataDecoder> decoder = new FFmpegVideoDecoder<V>(
+ mLib, aParams.VideoConfig(), aParams.mKnowsCompositor,
+ aParams.mImageContainer,
+ aParams.mOptions.contains(CreateDecoderParams::Option::LowLatency),
+ aParams.mOptions.contains(
+ CreateDecoderParams::Option::HardwareDecoderNotAllowed),
+ aParams.mTrackingId);
+ return decoder.forget();
+ }
+
+ already_AddRefed<MediaDataDecoder> CreateAudioDecoder(
+ const CreateDecoderParams& aParams) override {
+ if (Supports(SupportDecoderParams(aParams), nullptr) ==
+ media::DecodeSupport::Unsupported) {
+ return nullptr;
+ }
+ RefPtr<MediaDataDecoder> decoder =
+ new FFmpegAudioDecoder<V>(mLib, aParams.AudioConfig());
+ return decoder.forget();
+ }
+
+ media::DecodeSupportSet SupportsMimeType(
+ const nsACString& aMimeType,
+ DecoderDoctorDiagnostics* aDiagnostics) const override {
+ UniquePtr<TrackInfo> trackInfo = CreateTrackInfoWithMIMEType(aMimeType);
+ if (!trackInfo) {
+ return media::DecodeSupport::Unsupported;
+ }
+ return Supports(SupportDecoderParams(*trackInfo), aDiagnostics);
+ }
+
+ media::DecodeSupportSet Supports(
+ const SupportDecoderParams& aParams,
+ DecoderDoctorDiagnostics* aDiagnostics) const override {
+ // This should only be supported by MFMediaEngineDecoderModule.
+ if (aParams.mMediaEngineId) {
+ return media::DecodeSupport::Unsupported;
+ }
+
+ const auto& trackInfo = aParams.mConfig;
+ const nsACString& mimeType = trackInfo.mMimeType;
+
+ // Temporary - forces use of VPXDecoder when alpha is present.
+ // Bug 1263836 will handle alpha scenario once implemented. It will shift
+ // the check for alpha to PDMFactory but not itself remove the need for a
+ // check.
+ if (VPXDecoder::IsVPX(mimeType) && trackInfo.GetAsVideoInfo()->HasAlpha()) {
+ MOZ_LOG(sPDMLog, LogLevel::Debug,
+ ("FFmpeg decoder rejects requested type '%s'",
+ mimeType.BeginReading()));
+ return media::DecodeSupport::Unsupported;
+ }
+
+ AVCodecID videoCodec = FFmpegVideoDecoder<V>::GetCodecId(mimeType);
+ AVCodecID audioCodec = FFmpegAudioDecoder<V>::GetCodecId(mimeType);
+ if (audioCodec == AV_CODEC_ID_NONE && videoCodec == AV_CODEC_ID_NONE) {
+ MOZ_LOG(sPDMLog, LogLevel::Debug,
+ ("FFmpeg decoder rejects requested type '%s'",
+ mimeType.BeginReading()));
+ return media::DecodeSupport::Unsupported;
+ }
+ AVCodecID codec = audioCodec != AV_CODEC_ID_NONE ? audioCodec : videoCodec;
+ bool supports = !!FFmpegDataDecoder<V>::FindAVCodec(mLib, codec);
+ MOZ_LOG(sPDMLog, LogLevel::Debug,
+ ("FFmpeg decoder %s requested type '%s'",
+ supports ? "supports" : "rejects", mimeType.BeginReading()));
+ if (supports) {
+ // TODO: Note that we do not yet distinguish between SW/HW decode support.
+ // Will be done in bug 1754239.
+ return media::DecodeSupport::SoftwareDecode;
+ }
+ return media::DecodeSupport::Unsupported;
+ }
+
+ protected:
+ bool SupportsColorDepth(
+ gfx::ColorDepth aColorDepth,
+ DecoderDoctorDiagnostics* aDiagnostics) const override {
+#if defined(MOZ_WIDGET_ANDROID)
+ return aColorDepth == gfx::ColorDepth::COLOR_8;
+#endif
+ return true;
+ }
+
+ private:
+ FFmpegLibWrapper* mLib;
+};
+
+} // namespace mozilla
+
+#endif // __FFmpegDecoderModule_h__
diff --git a/dom/media/platforms/ffmpeg/FFmpegLibWrapper.cpp b/dom/media/platforms/ffmpeg/FFmpegLibWrapper.cpp
new file mode 100644
index 0000000000..71c042372e
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/FFmpegLibWrapper.cpp
@@ -0,0 +1,358 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FFmpegLibWrapper.h"
+#include "FFmpegLog.h"
+#include "mozilla/PodOperations.h"
+#ifdef MOZ_FFMPEG
+# include "mozilla/StaticPrefs_media.h"
+#endif
+#include "mozilla/Types.h"
+#include "PlatformDecoderModule.h"
+#include "prlink.h"
+#ifdef MOZ_WAYLAND
+# include "mozilla/gfx/gfxVars.h"
+# include "mozilla/widget/DMABufLibWrapper.h"
+#endif
+
+#define AV_LOG_INFO 32
+#define AV_LOG_WARNING 24
+
+namespace mozilla {
+
+FFmpegLibWrapper::LinkResult FFmpegLibWrapper::Link() {
+ if (!mAVCodecLib || !mAVUtilLib) {
+ Unlink();
+ return LinkResult::NoProvidedLib;
+ }
+
+ avcodec_version =
+ (decltype(avcodec_version))PR_FindSymbol(mAVCodecLib, "avcodec_version");
+ if (!avcodec_version) {
+ Unlink();
+ return LinkResult::NoAVCodecVersion;
+ }
+ uint32_t version = avcodec_version();
+ uint32_t macro = (version >> 16) & 0xFFu;
+ mVersion = static_cast<int>(macro);
+ uint32_t micro = version & 0xFFu;
+ // A micro version >= 100 indicates that it's FFmpeg (as opposed to LibAV).
+ bool isFFMpeg = micro >= 100;
+ if (!isFFMpeg) {
+ if (macro == 57) {
+ // Due to current AVCodecContext binary incompatibility we can only
+ // support FFmpeg 57 at this stage.
+ Unlink();
+ return LinkResult::CannotUseLibAV57;
+ }
+#ifdef MOZ_FFMPEG
+ if (version < (54u << 16 | 35u << 8 | 1u) &&
+ !StaticPrefs::media_libavcodec_allow_obsolete()) {
+ // Refuse any libavcodec version prior to 54.35.1.
+ // (Unless media.libavcodec.allow-obsolete==true)
+ Unlink();
+ return LinkResult::BlockedOldLibAVVersion;
+ }
+#endif
+ }
+
+ enum {
+ AV_FUNC_AVUTIL_MASK = 1 << 8,
+ AV_FUNC_53 = 1 << 0,
+ AV_FUNC_54 = 1 << 1,
+ AV_FUNC_55 = 1 << 2,
+ AV_FUNC_56 = 1 << 3,
+ AV_FUNC_57 = 1 << 4,
+ AV_FUNC_58 = 1 << 5,
+ AV_FUNC_59 = 1 << 6,
+ AV_FUNC_60 = 1 << 7,
+ AV_FUNC_AVUTIL_53 = AV_FUNC_53 | AV_FUNC_AVUTIL_MASK,
+ AV_FUNC_AVUTIL_54 = AV_FUNC_54 | AV_FUNC_AVUTIL_MASK,
+ AV_FUNC_AVUTIL_55 = AV_FUNC_55 | AV_FUNC_AVUTIL_MASK,
+ AV_FUNC_AVUTIL_56 = AV_FUNC_56 | AV_FUNC_AVUTIL_MASK,
+ AV_FUNC_AVUTIL_57 = AV_FUNC_57 | AV_FUNC_AVUTIL_MASK,
+ AV_FUNC_AVUTIL_58 = AV_FUNC_58 | AV_FUNC_AVUTIL_MASK,
+ AV_FUNC_AVUTIL_59 = AV_FUNC_59 | AV_FUNC_AVUTIL_MASK,
+ AV_FUNC_AVUTIL_60 = AV_FUNC_60 | AV_FUNC_AVUTIL_MASK,
+ AV_FUNC_AVCODEC_ALL = AV_FUNC_53 | AV_FUNC_54 | AV_FUNC_55 | AV_FUNC_56 |
+ AV_FUNC_57 | AV_FUNC_58 | AV_FUNC_59 | AV_FUNC_60,
+ AV_FUNC_AVUTIL_ALL = AV_FUNC_AVCODEC_ALL | AV_FUNC_AVUTIL_MASK
+ };
+
+ switch (macro) {
+ case 53:
+ version = AV_FUNC_53;
+ break;
+ case 54:
+ version = AV_FUNC_54;
+ break;
+ case 55:
+ version = AV_FUNC_55;
+ break;
+ case 56:
+ version = AV_FUNC_56;
+ break;
+ case 57:
+ version = AV_FUNC_57;
+ break;
+ case 58:
+ version = AV_FUNC_58;
+ break;
+ case 59:
+ version = AV_FUNC_59;
+ break;
+ case 60:
+ version = AV_FUNC_60;
+ break;
+ default:
+ FFMPEG_LOG("Unknown avcodec version: %d", macro);
+ Unlink();
+ return isFFMpeg ? ((macro > 57) ? LinkResult::UnknownFutureFFMpegVersion
+ : LinkResult::UnknownOlderFFMpegVersion)
+ // All LibAV versions<54.35.1 are blocked, therefore we
+ // must be dealing with a later one.
+ : LinkResult::UnknownFutureLibAVVersion;
+ }
+
+#define AV_FUNC_OPTION_SILENT(func, ver) \
+ if ((ver)&version) { \
+ if (!(func = (decltype(func))PR_FindSymbol( \
+ ((ver)&AV_FUNC_AVUTIL_MASK) ? mAVUtilLib : mAVCodecLib, \
+ #func))) { \
+ } \
+ } else { \
+ func = (decltype(func))nullptr; \
+ }
+
+#define AV_FUNC_OPTION(func, ver) \
+ AV_FUNC_OPTION_SILENT(func, ver) \
+ if ((ver)&version && (func) == (decltype(func))nullptr) { \
+ FFMPEG_LOG("Couldn't load function " #func); \
+ }
+
+#define AV_FUNC(func, ver) \
+ AV_FUNC_OPTION(func, ver) \
+ if ((ver)&version && !func) { \
+ Unlink(); \
+ return isFFMpeg ? LinkResult::MissingFFMpegFunction \
+ : LinkResult::MissingLibAVFunction; \
+ }
+
+ AV_FUNC(av_lockmgr_register, AV_FUNC_53 | AV_FUNC_54 | AV_FUNC_55 |
+ AV_FUNC_56 | AV_FUNC_57 | AV_FUNC_58)
+ AV_FUNC(avcodec_alloc_context3, AV_FUNC_AVCODEC_ALL)
+ AV_FUNC(avcodec_close, AV_FUNC_AVCODEC_ALL)
+ AV_FUNC(avcodec_decode_audio4, AV_FUNC_53 | AV_FUNC_54 | AV_FUNC_55 |
+ AV_FUNC_56 | AV_FUNC_57 | AV_FUNC_58)
+ AV_FUNC(avcodec_decode_video2, AV_FUNC_53 | AV_FUNC_54 | AV_FUNC_55 |
+ AV_FUNC_56 | AV_FUNC_57 | AV_FUNC_58)
+ AV_FUNC(avcodec_find_decoder, AV_FUNC_AVCODEC_ALL)
+ AV_FUNC(avcodec_flush_buffers, AV_FUNC_AVCODEC_ALL)
+ AV_FUNC(avcodec_open2, AV_FUNC_AVCODEC_ALL)
+ AV_FUNC(avcodec_register_all, AV_FUNC_53 | AV_FUNC_54 | AV_FUNC_55 |
+ AV_FUNC_56 | AV_FUNC_57 | AV_FUNC_58)
+ AV_FUNC(av_init_packet, AV_FUNC_AVCODEC_ALL)
+ AV_FUNC(av_parser_init, AV_FUNC_AVCODEC_ALL)
+ AV_FUNC(av_parser_close, AV_FUNC_AVCODEC_ALL)
+ AV_FUNC(av_parser_parse2, AV_FUNC_AVCODEC_ALL)
+ AV_FUNC(avcodec_align_dimensions, AV_FUNC_AVCODEC_ALL)
+ AV_FUNC(avcodec_alloc_frame, (AV_FUNC_53 | AV_FUNC_54))
+ AV_FUNC(avcodec_get_frame_defaults, (AV_FUNC_53 | AV_FUNC_54))
+ AV_FUNC(avcodec_free_frame, AV_FUNC_54)
+ AV_FUNC(avcodec_send_packet, AV_FUNC_58 | AV_FUNC_59 | AV_FUNC_60)
+ AV_FUNC(avcodec_receive_frame, AV_FUNC_58 | AV_FUNC_59 | AV_FUNC_60)
+ AV_FUNC(avcodec_default_get_buffer2, (AV_FUNC_55 | AV_FUNC_56 | AV_FUNC_57 |
+ AV_FUNC_58 | AV_FUNC_59 | AV_FUNC_60))
+ AV_FUNC_OPTION(av_rdft_init, AV_FUNC_AVCODEC_ALL)
+ AV_FUNC_OPTION(av_rdft_calc, AV_FUNC_AVCODEC_ALL)
+ AV_FUNC_OPTION(av_rdft_end, AV_FUNC_AVCODEC_ALL)
+ AV_FUNC(av_log_set_level, AV_FUNC_AVUTIL_ALL)
+ AV_FUNC(av_malloc, AV_FUNC_AVUTIL_ALL)
+ AV_FUNC(av_freep, AV_FUNC_AVUTIL_ALL)
+ AV_FUNC(av_frame_alloc,
+ (AV_FUNC_AVUTIL_55 | AV_FUNC_AVUTIL_56 | AV_FUNC_AVUTIL_57 |
+ AV_FUNC_AVUTIL_58 | AV_FUNC_AVUTIL_59 | AV_FUNC_AVUTIL_60))
+ AV_FUNC(av_frame_free,
+ (AV_FUNC_AVUTIL_55 | AV_FUNC_AVUTIL_56 | AV_FUNC_AVUTIL_57 |
+ AV_FUNC_AVUTIL_58 | AV_FUNC_AVUTIL_59 | AV_FUNC_AVUTIL_60))
+ AV_FUNC(av_frame_unref,
+ (AV_FUNC_AVUTIL_55 | AV_FUNC_AVUTIL_56 | AV_FUNC_AVUTIL_57 |
+ AV_FUNC_AVUTIL_58 | AV_FUNC_AVUTIL_59 | AV_FUNC_AVUTIL_60))
+ AV_FUNC(av_image_check_size, AV_FUNC_AVUTIL_ALL)
+ AV_FUNC(av_image_get_buffer_size, AV_FUNC_AVUTIL_ALL)
+ AV_FUNC_OPTION(av_buffer_get_opaque,
+ (AV_FUNC_AVUTIL_56 | AV_FUNC_AVUTIL_57 | AV_FUNC_AVUTIL_58 |
+ AV_FUNC_AVUTIL_59 | AV_FUNC_AVUTIL_60))
+ AV_FUNC(av_buffer_create,
+ (AV_FUNC_AVUTIL_55 | AV_FUNC_AVUTIL_56 | AV_FUNC_AVUTIL_57 |
+ AV_FUNC_AVUTIL_58 | AV_FUNC_AVUTIL_59 | AV_FUNC_AVUTIL_60))
+ AV_FUNC_OPTION(av_frame_get_colorspace,
+ AV_FUNC_AVUTIL_55 | AV_FUNC_AVUTIL_56 | AV_FUNC_AVUTIL_57 |
+ AV_FUNC_AVUTIL_58)
+ AV_FUNC_OPTION(av_frame_get_color_range,
+ AV_FUNC_AVUTIL_55 | AV_FUNC_AVUTIL_56 | AV_FUNC_AVUTIL_57 |
+ AV_FUNC_AVUTIL_58)
+ AV_FUNC(av_strerror,
+ AV_FUNC_AVUTIL_58 | AV_FUNC_AVUTIL_59 | AV_FUNC_AVUTIL_60)
+ AV_FUNC(avcodec_descriptor_get, AV_FUNC_53 | AV_FUNC_55 | AV_FUNC_56 |
+ AV_FUNC_57 | AV_FUNC_58 | AV_FUNC_59 |
+ AV_FUNC_60)
+
+#ifdef MOZ_WAYLAND
+ AV_FUNC_OPTION_SILENT(avcodec_get_hw_config,
+ AV_FUNC_58 | AV_FUNC_59 | AV_FUNC_60)
+ AV_FUNC_OPTION_SILENT(av_codec_iterate, AV_FUNC_58 | AV_FUNC_59 | AV_FUNC_60)
+ AV_FUNC_OPTION_SILENT(av_codec_is_decoder,
+ AV_FUNC_58 | AV_FUNC_59 | AV_FUNC_60)
+ AV_FUNC_OPTION_SILENT(av_hwdevice_ctx_init,
+ AV_FUNC_58 | AV_FUNC_59 | AV_FUNC_60)
+ AV_FUNC_OPTION_SILENT(av_hwdevice_ctx_alloc,
+ AV_FUNC_58 | AV_FUNC_59 | AV_FUNC_60)
+ AV_FUNC_OPTION_SILENT(av_hwdevice_hwconfig_alloc,
+ AV_FUNC_58 | AV_FUNC_59 | AV_FUNC_60)
+ AV_FUNC_OPTION_SILENT(av_hwdevice_get_hwframe_constraints,
+ AV_FUNC_58 | AV_FUNC_59 | AV_FUNC_60)
+ AV_FUNC_OPTION_SILENT(av_hwframe_constraints_free,
+ AV_FUNC_58 | AV_FUNC_59 | AV_FUNC_60)
+ AV_FUNC_OPTION_SILENT(av_buffer_ref,
+ AV_FUNC_AVUTIL_58 | AV_FUNC_59 | AV_FUNC_60)
+ AV_FUNC_OPTION_SILENT(av_buffer_unref,
+ AV_FUNC_AVUTIL_58 | AV_FUNC_59 | AV_FUNC_60)
+ AV_FUNC_OPTION_SILENT(av_hwframe_transfer_get_formats,
+ AV_FUNC_58 | AV_FUNC_59 | AV_FUNC_60)
+ AV_FUNC_OPTION_SILENT(av_hwdevice_ctx_create_derived,
+ AV_FUNC_58 | AV_FUNC_59 | AV_FUNC_60)
+ AV_FUNC_OPTION_SILENT(av_hwframe_ctx_alloc,
+ AV_FUNC_58 | AV_FUNC_59 | AV_FUNC_60)
+ AV_FUNC_OPTION_SILENT(av_dict_set, AV_FUNC_58 | AV_FUNC_59 | AV_FUNC_60)
+ AV_FUNC_OPTION_SILENT(av_dict_free, AV_FUNC_58 | AV_FUNC_59 | AV_FUNC_60)
+ AV_FUNC_OPTION_SILENT(avcodec_get_name, AV_FUNC_58 | AV_FUNC_59 | AV_FUNC_60)
+ AV_FUNC_OPTION_SILENT(av_get_pix_fmt_string, AV_FUNC_AVUTIL_58 |
+ AV_FUNC_AVUTIL_59 |
+ AV_FUNC_AVUTIL_60)
+#endif
+#undef AV_FUNC
+#undef AV_FUNC_OPTION
+
+#ifdef MOZ_WAYLAND
+# define VA_FUNC_OPTION_SILENT(func) \
+ if (!(func = (decltype(func))PR_FindSymbol(mVALib, #func))) { \
+ func = (decltype(func))nullptr; \
+ }
+
+ // mVALib is optional and may not be present.
+ if (mVALib) {
+ VA_FUNC_OPTION_SILENT(vaExportSurfaceHandle)
+ VA_FUNC_OPTION_SILENT(vaSyncSurface)
+ VA_FUNC_OPTION_SILENT(vaInitialize)
+ VA_FUNC_OPTION_SILENT(vaTerminate)
+ }
+# undef VA_FUNC_OPTION_SILENT
+
+# define VAD_FUNC_OPTION_SILENT(func) \
+ if (!(func = (decltype(func))PR_FindSymbol(mVALibDrm, #func))) { \
+ FFMPEG_LOG("Couldn't load function " #func); \
+ }
+
+ // mVALibDrm is optional and may not be present.
+ if (mVALibDrm) {
+ VAD_FUNC_OPTION_SILENT(vaGetDisplayDRM)
+ }
+# undef VAD_FUNC_OPTION_SILENT
+#endif
+
+ if (avcodec_register_all) {
+ avcodec_register_all();
+ }
+ if (MOZ_LOG_TEST(sPDMLog, LogLevel::Debug)) {
+ av_log_set_level(AV_LOG_WARNING);
+ } else if (MOZ_LOG_TEST(sPDMLog, LogLevel::Info)) {
+ av_log_set_level(AV_LOG_INFO);
+ } else {
+ av_log_set_level(0);
+ }
+ return LinkResult::Success;
+}
+
+void FFmpegLibWrapper::Unlink() {
+ if (av_lockmgr_register) {
+ // Registering a null lockmgr cause the destruction of libav* global mutexes
+ // as the default lockmgr that allocated them will be deregistered.
+ // This prevents ASAN and valgrind to report sizeof(pthread_mutex_t) leaks.
+ av_lockmgr_register(nullptr);
+ }
+#ifndef MOZ_TSAN
+ // With TSan, we cannot unload libav once we have loaded it because
+ // TSan does not support unloading libraries that are matched from its
+ // suppression list. Hence we just keep the library loaded in TSan builds.
+ if (mAVUtilLib && mAVUtilLib != mAVCodecLib) {
+ PR_UnloadLibrary(mAVUtilLib);
+ }
+ if (mAVCodecLib) {
+ PR_UnloadLibrary(mAVCodecLib);
+ }
+#endif
+#ifdef MOZ_WAYLAND
+ if (mVALib) {
+ PR_UnloadLibrary(mVALib);
+ }
+ if (mVALibDrm) {
+ PR_UnloadLibrary(mVALibDrm);
+ }
+#endif
+ PodZero(this);
+}
+
+#ifdef MOZ_WAYLAND
+void FFmpegLibWrapper::LinkVAAPILibs() {
+ if (!gfx::gfxVars::CanUseHardwareVideoDecoding() || !XRE_IsRDDProcess()) {
+ return;
+ }
+
+ PRLibSpec lspec;
+ lspec.type = PR_LibSpec_Pathname;
+ const char* libDrm = "libva-drm.so.2";
+ lspec.value.pathname = libDrm;
+ mVALibDrm = PR_LoadLibraryWithFlags(lspec, PR_LD_NOW | PR_LD_LOCAL);
+ if (!mVALibDrm) {
+ FFMPEG_LOG("VA-API support: Missing or old %s library.\n", libDrm);
+ return;
+ }
+
+ const char* lib = "libva.so.2";
+ lspec.value.pathname = lib;
+ mVALib = PR_LoadLibraryWithFlags(lspec, PR_LD_NOW | PR_LD_LOCAL);
+ // Don't use libva when it's missing vaExportSurfaceHandle.
+ if (mVALib && !PR_FindSymbol(mVALib, "vaExportSurfaceHandle")) {
+ PR_UnloadLibrary(mVALib);
+ mVALib = nullptr;
+ }
+ if (!mVALib) {
+ FFMPEG_LOG("VA-API support: Missing or old %s library.\n", lib);
+ }
+}
+#endif
+
+#ifdef MOZ_WAYLAND
+bool FFmpegLibWrapper::IsVAAPIAvailable() {
+# define VA_FUNC_LOADED(func) (func != nullptr)
+ return VA_FUNC_LOADED(avcodec_get_hw_config) &&
+ VA_FUNC_LOADED(av_hwdevice_ctx_alloc) &&
+ VA_FUNC_LOADED(av_hwdevice_ctx_init) &&
+ VA_FUNC_LOADED(av_hwdevice_hwconfig_alloc) &&
+ VA_FUNC_LOADED(av_hwdevice_get_hwframe_constraints) &&
+ VA_FUNC_LOADED(av_hwframe_constraints_free) &&
+ VA_FUNC_LOADED(av_buffer_ref) && VA_FUNC_LOADED(av_buffer_unref) &&
+ VA_FUNC_LOADED(av_hwframe_transfer_get_formats) &&
+ VA_FUNC_LOADED(av_hwdevice_ctx_create_derived) &&
+ VA_FUNC_LOADED(av_hwframe_ctx_alloc) && VA_FUNC_LOADED(av_dict_set) &&
+ VA_FUNC_LOADED(av_dict_free) && VA_FUNC_LOADED(avcodec_get_name) &&
+ VA_FUNC_LOADED(av_get_pix_fmt_string) &&
+ VA_FUNC_LOADED(vaExportSurfaceHandle) &&
+ VA_FUNC_LOADED(vaSyncSurface) && VA_FUNC_LOADED(vaInitialize) &&
+ VA_FUNC_LOADED(vaTerminate) && VA_FUNC_LOADED(vaGetDisplayDRM);
+}
+#endif
+
+} // namespace mozilla
diff --git a/dom/media/platforms/ffmpeg/FFmpegLibWrapper.h b/dom/media/platforms/ffmpeg/FFmpegLibWrapper.h
new file mode 100644
index 0000000000..7ca1d9131e
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/FFmpegLibWrapper.h
@@ -0,0 +1,180 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef __FFmpegLibWrapper_h__
+#define __FFmpegLibWrapper_h__
+
+#include "FFmpegRDFTTypes.h" // for AvRdftInitFn, etc.
+#include "mozilla/Attributes.h"
+#include "mozilla/Types.h"
+
+struct AVCodec;
+struct AVCodecContext;
+struct AVCodecDescriptor;
+struct AVFrame;
+struct AVPacket;
+struct AVDictionary;
+struct AVCodecParserContext;
+struct PRLibrary;
+#ifdef MOZ_WAYLAND
+struct AVCodecHWConfig;
+struct AVVAAPIHWConfig;
+struct AVHWFramesConstraints;
+#endif
+struct AVBufferRef;
+
+namespace mozilla {
+
+struct MOZ_ONLY_USED_TO_AVOID_STATIC_CONSTRUCTORS FFmpegLibWrapper {
+ // The class is used only in static storage and so is zero initialized.
+ FFmpegLibWrapper() = default;
+ // The libraries are not unloaded in the destructor, because doing so would
+ // require a static constructor to register the static destructor. As the
+ // class is in static storage, the destructor would only run on shutdown
+ // anyway.
+ ~FFmpegLibWrapper() = default;
+
+ enum class LinkResult {
+ Success,
+ NoProvidedLib,
+ NoAVCodecVersion,
+ CannotUseLibAV57,
+ BlockedOldLibAVVersion,
+ UnknownFutureLibAVVersion,
+ UnknownFutureFFMpegVersion,
+ UnknownOlderFFMpegVersion,
+ MissingFFMpegFunction,
+ MissingLibAVFunction,
+ };
+ // Examine mAVCodecLib, mAVUtilLib and mVALib, and attempt to resolve
+ // all symbols.
+ // Upon failure, the entire object will be reset and any attached libraries
+ // will be unlinked.
+ LinkResult Link();
+
+ // Reset the wrapper and unlink all attached libraries.
+ void Unlink();
+
+#ifdef MOZ_WAYLAND
+ // Check if mVALib are available and we can use HW decode.
+ bool IsVAAPIAvailable();
+ void LinkVAAPILibs();
+#endif
+
+ // indicate the version of libavcodec linked to.
+ // 0 indicates that the function wasn't initialized with Link().
+ int mVersion;
+
+ // libavcodec
+ unsigned (*avcodec_version)();
+ int (*av_lockmgr_register)(int (*cb)(void** mutex, int op));
+ AVCodecContext* (*avcodec_alloc_context3)(const AVCodec* codec);
+ int (*avcodec_close)(AVCodecContext* avctx);
+ int (*avcodec_decode_audio4)(AVCodecContext* avctx, AVFrame* frame,
+ int* got_frame_ptr, const AVPacket* avpkt);
+ int (*avcodec_decode_video2)(AVCodecContext* avctx, AVFrame* picture,
+ int* got_picture_ptr, const AVPacket* avpkt);
+ AVCodec* (*avcodec_find_decoder)(int id);
+ void (*avcodec_flush_buffers)(AVCodecContext* avctx);
+ int (*avcodec_open2)(AVCodecContext* avctx, const AVCodec* codec,
+ AVDictionary** options);
+ void (*avcodec_register_all)();
+ void (*av_init_packet)(AVPacket* pkt);
+ AVCodecParserContext* (*av_parser_init)(int codec_id);
+ void (*av_parser_close)(AVCodecParserContext* s);
+ int (*av_parser_parse2)(AVCodecParserContext* s, AVCodecContext* avctx,
+ uint8_t** poutbuf, int* poutbuf_size,
+ const uint8_t* buf, int buf_size, int64_t pts,
+ int64_t dts, int64_t pos);
+ AVCodec* (*av_codec_iterate)(void** opaque);
+ int (*av_codec_is_decoder)(const AVCodec* codec);
+ void (*avcodec_align_dimensions)(AVCodecContext* s, int* width, int* height);
+ int (*av_strerror)(int errnum, char* errbuf, size_t errbuf_size);
+ AVCodecDescriptor* (*avcodec_descriptor_get)(int id);
+
+ // only used in libavcodec <= 54
+ AVFrame* (*avcodec_alloc_frame)();
+ void (*avcodec_get_frame_defaults)(AVFrame* pic);
+
+ // libavcodec v54 only
+ void (*avcodec_free_frame)(AVFrame** frame);
+
+ // libavcodec >= v55
+ int (*avcodec_default_get_buffer2)(AVCodecContext* s, AVFrame* frame,
+ int flags);
+
+ // libavcodec v58 and later only
+ int (*avcodec_send_packet)(AVCodecContext* avctx, const AVPacket* avpkt);
+ int (*avcodec_receive_frame)(AVCodecContext* avctx, AVFrame* frame);
+
+ // libavcodec optional
+ AvRdftInitFn av_rdft_init;
+ AvRdftCalcFn av_rdft_calc;
+ AvRdftEndFn av_rdft_end;
+
+ // libavutil
+ void (*av_log_set_level)(int level);
+ void* (*av_malloc)(size_t size);
+ void (*av_freep)(void* ptr);
+ int (*av_image_check_size)(unsigned int w, unsigned int h, int log_offset,
+ void* log_ctx);
+ int (*av_image_get_buffer_size)(int pix_fmt, int width, int height,
+ int align);
+
+ // libavutil v55 and later only
+ AVFrame* (*av_frame_alloc)();
+ void (*av_frame_free)(AVFrame** frame);
+ void (*av_frame_unref)(AVFrame* frame);
+ AVBufferRef* (*av_buffer_create)(uint8_t* data, int size,
+ void (*free)(void* opaque, uint8_t* data),
+ void* opaque, int flags);
+
+ // libavutil >= v56
+ void* (*av_buffer_get_opaque)(const AVBufferRef* buf);
+
+ // libavutil optional
+ int (*av_frame_get_colorspace)(const AVFrame* frame);
+ int (*av_frame_get_color_range)(const AVFrame* frame);
+
+#ifdef MOZ_WAYLAND
+ const AVCodecHWConfig* (*avcodec_get_hw_config)(const AVCodec* codec,
+ int index);
+ AVBufferRef* (*av_hwdevice_ctx_alloc)(int);
+ int (*av_hwdevice_ctx_init)(AVBufferRef* ref);
+ AVVAAPIHWConfig* (*av_hwdevice_hwconfig_alloc)(AVBufferRef* device_ctx);
+ AVHWFramesConstraints* (*av_hwdevice_get_hwframe_constraints)(
+ AVBufferRef* ref, const void* hwconfig);
+ void (*av_hwframe_constraints_free)(AVHWFramesConstraints** constraints);
+
+ AVBufferRef* (*av_buffer_ref)(AVBufferRef* buf);
+ void (*av_buffer_unref)(AVBufferRef** buf);
+ int (*av_hwframe_transfer_get_formats)(AVBufferRef* hwframe_ctx, int dir,
+ int** formats, int flags);
+ int (*av_hwdevice_ctx_create_derived)(AVBufferRef** dst_ctx, int type,
+ AVBufferRef* src_ctx, int flags);
+ AVBufferRef* (*av_hwframe_ctx_alloc)(AVBufferRef* device_ctx);
+ int (*av_dict_set)(AVDictionary** pm, const char* key, const char* value,
+ int flags);
+ void (*av_dict_free)(AVDictionary** m);
+ const char* (*avcodec_get_name)(int id);
+ char* (*av_get_pix_fmt_string)(char* buf, int buf_size, int pix_fmt);
+
+ int (*vaExportSurfaceHandle)(void*, unsigned int, uint32_t, uint32_t, void*);
+ int (*vaSyncSurface)(void*, unsigned int);
+ int (*vaInitialize)(void* dpy, int* major_version, int* minor_version);
+ int (*vaTerminate)(void* dpy);
+ void* (*vaGetDisplayDRM)(int fd);
+#endif
+
+ PRLibrary* mAVCodecLib;
+ PRLibrary* mAVUtilLib;
+#ifdef MOZ_WAYLAND
+ PRLibrary* mVALib;
+ PRLibrary* mVALibDrm;
+#endif
+};
+
+} // namespace mozilla
+
+#endif // FFmpegLibWrapper
diff --git a/dom/media/platforms/ffmpeg/FFmpegLibs.h b/dom/media/platforms/ffmpeg/FFmpegLibs.h
new file mode 100644
index 0000000000..1c185cb95d
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/FFmpegLibs.h
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef __FFmpegLibs_h__
+#define __FFmpegLibs_h__
+
+extern "C" {
+#ifdef __GNUC__
+# pragma GCC visibility push(default)
+#endif
+#include "libavcodec/avcodec.h"
+#include "libavutil/avutil.h"
+#include "libavutil/mem.h"
+#ifdef MOZ_WAYLAND_USE_HWDECODE
+# include "libavutil/hwcontext_vaapi.h"
+# include "libavutil/hwcontext_drm.h"
+#endif
+#ifdef __GNUC__
+# pragma GCC visibility pop
+#endif
+}
+
+#if LIBAVCODEC_VERSION_MAJOR >= 58
+# include "libavcodec/codec_desc.h"
+#endif // LIBAVCODEC_VERSION_MAJOR >= 58
+
+#if LIBAVCODEC_VERSION_MAJOR < 55
+# define AV_CODEC_ID_VP6F CODEC_ID_VP6F
+# define AV_CODEC_ID_H264 CODEC_ID_H264
+# define AV_CODEC_ID_AAC CODEC_ID_AAC
+# define AV_CODEC_ID_MP3 CODEC_ID_MP3
+# define AV_CODEC_ID_VP8 CODEC_ID_VP8
+# define AV_CODEC_ID_NONE CODEC_ID_NONE
+# define AV_CODEC_ID_FLAC CODEC_ID_FLAC
+typedef CodecID AVCodecID;
+#endif
+#if LIBAVCODEC_VERSION_MAJOR <= 55
+# define AV_CODEC_FLAG_LOW_DELAY CODEC_FLAG_LOW_DELAY
+#endif
+
+#ifdef FFVPX_VERSION
+enum { LIBAV_VER = FFVPX_VERSION };
+#else
+enum { LIBAV_VER = LIBAVCODEC_VERSION_MAJOR };
+#endif
+
+#endif // __FFmpegLibs_h__
diff --git a/dom/media/platforms/ffmpeg/FFmpegLog.h b/dom/media/platforms/ffmpeg/FFmpegLog.h
new file mode 100644
index 0000000000..c7acabf26a
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/FFmpegLog.h
@@ -0,0 +1,23 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef __FFmpegLog_h__
+#define __FFmpegLog_h__
+
+#include "mozilla/Logging.h"
+
+#ifdef FFVPX_VERSION
+# define FFMPEG_LOG(str, ...) \
+ MOZ_LOG(sPDMLog, mozilla::LogLevel::Debug, ("FFVPX: " str, ##__VA_ARGS__))
+#else
+# define FFMPEG_LOG(str, ...) \
+ MOZ_LOG(sPDMLog, mozilla::LogLevel::Debug, ("FFMPEG: " str, ##__VA_ARGS__))
+#endif
+
+#define FFMPEG_LOGV(...) \
+ MOZ_LOG(sPDMLog, mozilla::LogLevel::Verbose, (__VA_ARGS__))
+
+#endif // __FFmpegLog_h__
diff --git a/dom/media/platforms/ffmpeg/FFmpegRDFTTypes.h b/dom/media/platforms/ffmpeg/FFmpegRDFTTypes.h
new file mode 100644
index 0000000000..cb3e2476fb
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/FFmpegRDFTTypes.h
@@ -0,0 +1,34 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#ifndef FFmpegRDFTTypes_h
+#define FFmpegRDFTTypes_h
+
+struct RDFTContext;
+
+typedef float FFTSample;
+
+enum RDFTransformType {
+ DFT_R2C,
+ IDFT_C2R,
+ IDFT_R2C,
+ DFT_C2R,
+};
+
+extern "C" {
+
+typedef RDFTContext* (*AvRdftInitFn)(int nbits, enum RDFTransformType trans);
+typedef void (*AvRdftCalcFn)(RDFTContext* s, FFTSample* data);
+typedef void (*AvRdftEndFn)(RDFTContext* s);
+}
+
+struct FFmpegRDFTFuncs {
+ AvRdftInitFn init;
+ AvRdftCalcFn calc;
+ AvRdftEndFn end;
+};
+
+#endif // FFmpegRDFTTypes_h
diff --git a/dom/media/platforms/ffmpeg/FFmpegRuntimeLinker.cpp b/dom/media/platforms/ffmpeg/FFmpegRuntimeLinker.cpp
new file mode 100644
index 0000000000..7716162e07
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/FFmpegRuntimeLinker.cpp
@@ -0,0 +1,199 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FFmpegRuntimeLinker.h"
+#include "FFmpegLibWrapper.h"
+#include "mozilla/ArrayUtils.h"
+#include "FFmpegLog.h"
+#include "prlink.h"
+
+namespace mozilla {
+
+FFmpegRuntimeLinker::LinkStatus FFmpegRuntimeLinker::sLinkStatus =
+ LinkStatus_INIT;
+const char* FFmpegRuntimeLinker::sLinkStatusLibraryName = "";
+
+template <int V>
+class FFmpegDecoderModule {
+ public:
+ static already_AddRefed<PlatformDecoderModule> Create(FFmpegLibWrapper*);
+};
+
+static FFmpegLibWrapper sLibAV;
+
+static const char* sLibs[] = {
+// clang-format off
+#if defined(XP_DARWIN)
+ "libavcodec.60.dylib",
+ "libavcodec.59.dylib",
+ "libavcodec.58.dylib",
+ "libavcodec.57.dylib",
+ "libavcodec.56.dylib",
+ "libavcodec.55.dylib",
+ "libavcodec.54.dylib",
+ "libavcodec.53.dylib",
+#elif defined(XP_OPENBSD)
+ "libavcodec.so", // OpenBSD hardly controls the major/minor library version
+ // of ffmpeg and update it regulary on ABI/API changes
+#else
+ "libavcodec.so.60",
+ "libavcodec.so.59",
+ "libavcodec.so.58",
+ "libavcodec-ffmpeg.so.58",
+ "libavcodec-ffmpeg.so.57",
+ "libavcodec-ffmpeg.so.56",
+ "libavcodec.so.57",
+ "libavcodec.so.56",
+ "libavcodec.so.55",
+ "libavcodec.so.54",
+ "libavcodec.so.53",
+#endif
+ // clang-format on
+};
+
+/* static */
+bool FFmpegRuntimeLinker::Init() {
+ if (sLinkStatus != LinkStatus_INIT) {
+ return sLinkStatus == LinkStatus_SUCCEEDED;
+ }
+
+#ifdef MOZ_WAYLAND
+ sLibAV.LinkVAAPILibs();
+#endif
+
+ // While going through all possible libs, this status will be updated with a
+ // more precise error if possible.
+ sLinkStatus = LinkStatus_NOT_FOUND;
+
+ for (size_t i = 0; i < ArrayLength(sLibs); i++) {
+ const char* lib = sLibs[i];
+ PRLibSpec lspec;
+ lspec.type = PR_LibSpec_Pathname;
+ lspec.value.pathname = lib;
+ sLibAV.mAVCodecLib =
+ PR_LoadLibraryWithFlags(lspec, PR_LD_NOW | PR_LD_LOCAL);
+ if (sLibAV.mAVCodecLib) {
+ sLibAV.mAVUtilLib = sLibAV.mAVCodecLib;
+ switch (sLibAV.Link()) {
+ case FFmpegLibWrapper::LinkResult::Success:
+ sLinkStatus = LinkStatus_SUCCEEDED;
+ sLinkStatusLibraryName = lib;
+ return true;
+ case FFmpegLibWrapper::LinkResult::NoProvidedLib:
+ MOZ_ASSERT_UNREACHABLE("Incorrectly-setup sLibAV");
+ break;
+ case FFmpegLibWrapper::LinkResult::NoAVCodecVersion:
+ if (sLinkStatus > LinkStatus_INVALID_CANDIDATE) {
+ sLinkStatus = LinkStatus_INVALID_CANDIDATE;
+ sLinkStatusLibraryName = lib;
+ }
+ break;
+ case FFmpegLibWrapper::LinkResult::CannotUseLibAV57:
+ if (sLinkStatus > LinkStatus_UNUSABLE_LIBAV57) {
+ sLinkStatus = LinkStatus_UNUSABLE_LIBAV57;
+ sLinkStatusLibraryName = lib;
+ }
+ break;
+ case FFmpegLibWrapper::LinkResult::BlockedOldLibAVVersion:
+ if (sLinkStatus > LinkStatus_OBSOLETE_LIBAV) {
+ sLinkStatus = LinkStatus_OBSOLETE_LIBAV;
+ sLinkStatusLibraryName = lib;
+ }
+ break;
+ case FFmpegLibWrapper::LinkResult::UnknownFutureLibAVVersion:
+ case FFmpegLibWrapper::LinkResult::MissingLibAVFunction:
+ if (sLinkStatus > LinkStatus_INVALID_LIBAV_CANDIDATE) {
+ sLinkStatus = LinkStatus_INVALID_LIBAV_CANDIDATE;
+ sLinkStatusLibraryName = lib;
+ }
+ break;
+ case FFmpegLibWrapper::LinkResult::UnknownFutureFFMpegVersion:
+ case FFmpegLibWrapper::LinkResult::MissingFFMpegFunction:
+ if (sLinkStatus > LinkStatus_INVALID_FFMPEG_CANDIDATE) {
+ sLinkStatus = LinkStatus_INVALID_FFMPEG_CANDIDATE;
+ sLinkStatusLibraryName = lib;
+ }
+ break;
+ case FFmpegLibWrapper::LinkResult::UnknownOlderFFMpegVersion:
+ if (sLinkStatus > LinkStatus_OBSOLETE_FFMPEG) {
+ sLinkStatus = LinkStatus_OBSOLETE_FFMPEG;
+ sLinkStatusLibraryName = lib;
+ }
+ break;
+ }
+ }
+ }
+
+ FFMPEG_LOG("H264/AAC codecs unsupported without [");
+ for (size_t i = 0; i < ArrayLength(sLibs); i++) {
+ FFMPEG_LOG("%s %s", i ? "," : " ", sLibs[i]);
+ }
+ FFMPEG_LOG(" ]\n");
+
+ return false;
+}
+
+/* static */
+already_AddRefed<PlatformDecoderModule> FFmpegRuntimeLinker::Create() {
+ if (!Init()) {
+ return nullptr;
+ }
+ RefPtr<PlatformDecoderModule> module;
+ switch (sLibAV.mVersion) {
+ case 53:
+ module = FFmpegDecoderModule<53>::Create(&sLibAV);
+ break;
+ case 54:
+ module = FFmpegDecoderModule<54>::Create(&sLibAV);
+ break;
+ case 55:
+ case 56:
+ module = FFmpegDecoderModule<55>::Create(&sLibAV);
+ break;
+ case 57:
+ module = FFmpegDecoderModule<57>::Create(&sLibAV);
+ break;
+ case 58:
+ module = FFmpegDecoderModule<58>::Create(&sLibAV);
+ break;
+ case 59:
+ module = FFmpegDecoderModule<59>::Create(&sLibAV);
+ break;
+ case 60:
+ module = FFmpegDecoderModule<60>::Create(&sLibAV);
+ break;
+ default:
+ module = nullptr;
+ }
+ return module.forget();
+}
+
+/* static */ const char* FFmpegRuntimeLinker::LinkStatusString() {
+ switch (sLinkStatus) {
+ case LinkStatus_INIT:
+ return "Libavcodec not initialized yet";
+ case LinkStatus_SUCCEEDED:
+ return "Libavcodec linking succeeded";
+ case LinkStatus_INVALID_FFMPEG_CANDIDATE:
+ return "Invalid FFMpeg libavcodec candidate";
+ case LinkStatus_UNUSABLE_LIBAV57:
+ return "Unusable LibAV's libavcodec 57";
+ case LinkStatus_INVALID_LIBAV_CANDIDATE:
+ return "Invalid LibAV libavcodec candidate";
+ case LinkStatus_OBSOLETE_FFMPEG:
+ return "Obsolete FFMpeg libavcodec candidate";
+ case LinkStatus_OBSOLETE_LIBAV:
+ return "Obsolete LibAV libavcodec candidate";
+ case LinkStatus_INVALID_CANDIDATE:
+ return "Invalid libavcodec candidate";
+ case LinkStatus_NOT_FOUND:
+ return "Libavcodec not found";
+ }
+ MOZ_ASSERT_UNREACHABLE("Unknown sLinkStatus value");
+ return "?";
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/ffmpeg/FFmpegRuntimeLinker.h b/dom/media/platforms/ffmpeg/FFmpegRuntimeLinker.h
new file mode 100644
index 0000000000..737db75f85
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/FFmpegRuntimeLinker.h
@@ -0,0 +1,45 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef __FFmpegRuntimeLinker_h__
+#define __FFmpegRuntimeLinker_h__
+
+#include "PlatformDecoderModule.h"
+
+namespace mozilla {
+
+class FFmpegRuntimeLinker {
+ public:
+ static bool Init();
+ static already_AddRefed<PlatformDecoderModule> Create();
+ enum LinkStatus {
+ LinkStatus_INIT = 0, // Never been linked.
+ LinkStatus_SUCCEEDED, // Found a usable library.
+ // The following error statuses are sorted from most to least preferred
+ // (i.e., if more than one happens, the top one is chosen.)
+ LinkStatus_INVALID_FFMPEG_CANDIDATE, // Found ffmpeg with unexpected
+ // contents.
+ LinkStatus_UNUSABLE_LIBAV57, // Found LibAV 57, which we cannot use.
+ LinkStatus_INVALID_LIBAV_CANDIDATE, // Found libav with unexpected
+ // contents.
+ LinkStatus_OBSOLETE_FFMPEG,
+ LinkStatus_OBSOLETE_LIBAV,
+ LinkStatus_INVALID_CANDIDATE, // Found some lib with unexpected contents.
+ LinkStatus_NOT_FOUND, // Haven't found any library with an expected name.
+ };
+ static LinkStatus LinkStatusCode() { return sLinkStatus; }
+ static const char* LinkStatusString();
+ // Library name to which the sLinkStatus applies, or "" if not applicable.
+ static const char* LinkStatusLibraryName() { return sLinkStatusLibraryName; }
+
+ private:
+ static LinkStatus sLinkStatus;
+ static const char* sLinkStatusLibraryName;
+};
+
+} // namespace mozilla
+
+#endif // __FFmpegRuntimeLinker_h__
diff --git a/dom/media/platforms/ffmpeg/FFmpegVideoDecoder.cpp b/dom/media/platforms/ffmpeg/FFmpegVideoDecoder.cpp
new file mode 100644
index 0000000000..353116d6f4
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/FFmpegVideoDecoder.cpp
@@ -0,0 +1,1627 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FFmpegVideoDecoder.h"
+
+#include "FFmpegLog.h"
+#include "ImageContainer.h"
+#include "MP4Decoder.h"
+#include "MediaInfo.h"
+#include "VideoUtils.h"
+#include "VPXDecoder.h"
+#include "mozilla/layers/KnowsCompositor.h"
+#if LIBAVCODEC_VERSION_MAJOR >= 57
+# include "mozilla/layers/TextureClient.h"
+#endif
+#if LIBAVCODEC_VERSION_MAJOR >= 58
+# include "mozilla/ProfilerMarkers.h"
+#endif
+#ifdef MOZ_WAYLAND_USE_HWDECODE
+# include "H264.h"
+# include "mozilla/gfx/gfxVars.h"
+# include "mozilla/layers/DMABUFSurfaceImage.h"
+# include "mozilla/widget/DMABufLibWrapper.h"
+# include "FFmpegVideoFramePool.h"
+# include "va/va.h"
+#endif
+
+#if defined(MOZ_AV1) && defined(MOZ_WAYLAND) && \
+ (defined(FFVPX_VERSION) || LIBAVCODEC_VERSION_MAJOR >= 59)
+# define FFMPEG_AV1_DECODE 1
+# include "AOMDecoder.h"
+#endif
+
+#include "libavutil/pixfmt.h"
+#if LIBAVCODEC_VERSION_MAJOR < 54
+# define AVPixelFormat PixelFormat
+# define AV_PIX_FMT_YUV420P PIX_FMT_YUV420P
+# define AV_PIX_FMT_YUVJ420P PIX_FMT_YUVJ420P
+# define AV_PIX_FMT_YUV420P10LE PIX_FMT_YUV420P10LE
+# define AV_PIX_FMT_YUV422P PIX_FMT_YUV422P
+# define AV_PIX_FMT_YUV422P10LE PIX_FMT_YUV422P10LE
+# define AV_PIX_FMT_YUV444P PIX_FMT_YUV444P
+# define AV_PIX_FMT_YUV444P10LE PIX_FMT_YUV444P10LE
+# define AV_PIX_FMT_GBRP PIX_FMT_GBRP
+# define AV_PIX_FMT_NONE PIX_FMT_NONE
+#endif
+#if LIBAVCODEC_VERSION_MAJOR > 58
+# define AV_PIX_FMT_VAAPI_VLD AV_PIX_FMT_VAAPI
+#endif
+#include "mozilla/PodOperations.h"
+#include "mozilla/StaticPrefs_gfx.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/TaskQueue.h"
+#include "nsThreadUtils.h"
+#include "prsystem.h"
+
+#ifdef XP_WIN
+# include "mozilla/gfx/DeviceManagerDx.h"
+# include "mozilla/gfx/gfxVars.h"
+#endif
+
+// Forward declare from va.h
+#ifdef MOZ_WAYLAND_USE_HWDECODE
+typedef int VAStatus;
+# define VA_EXPORT_SURFACE_READ_ONLY 0x0001
+# define VA_EXPORT_SURFACE_SEPARATE_LAYERS 0x0004
+# define VA_STATUS_SUCCESS 0x00000000
+#endif
+// Use some extra HW frames for potential rendering lags.
+#define EXTRA_HW_FRAMES 6
+// Defines number of delayed frames until we switch back to SW decode.
+#define HW_DECODE_LATE_FRAMES 15
+
+#if LIBAVCODEC_VERSION_MAJOR >= 57 && LIBAVUTIL_VERSION_MAJOR >= 56
+# define CUSTOMIZED_BUFFER_ALLOCATION 1
+#endif
+
+#define AV_LOG_DEBUG 48
+
+typedef mozilla::layers::Image Image;
+typedef mozilla::layers::PlanarYCbCrImage PlanarYCbCrImage;
+
+namespace mozilla {
+
+#ifdef MOZ_WAYLAND_USE_HWDECODE
+nsTArray<AVCodecID> FFmpegVideoDecoder<LIBAV_VER>::mAcceleratedFormats;
+#endif
+
+using media::TimeUnit;
+
+/**
+ * FFmpeg calls back to this function with a list of pixel formats it supports.
+ * We choose a pixel format that we support and return it.
+ * For now, we just look for YUV420P, YUVJ420P and YUV444 as those are the only
+ * only non-HW accelerated format supported by FFmpeg's H264 and VP9 decoder.
+ */
+static AVPixelFormat ChoosePixelFormat(AVCodecContext* aCodecContext,
+ const AVPixelFormat* aFormats) {
+ FFMPEG_LOG("Choosing FFmpeg pixel format for video decoding.");
+ for (; *aFormats > -1; aFormats++) {
+ switch (*aFormats) {
+ case AV_PIX_FMT_YUV420P:
+ FFMPEG_LOG("Requesting pixel format YUV420P.");
+ return AV_PIX_FMT_YUV420P;
+ case AV_PIX_FMT_YUVJ420P:
+ FFMPEG_LOG("Requesting pixel format YUVJ420P.");
+ return AV_PIX_FMT_YUVJ420P;
+ case AV_PIX_FMT_YUV420P10LE:
+ FFMPEG_LOG("Requesting pixel format YUV420P10LE.");
+ return AV_PIX_FMT_YUV420P10LE;
+ case AV_PIX_FMT_YUV422P:
+ FFMPEG_LOG("Requesting pixel format YUV422P.");
+ return AV_PIX_FMT_YUV422P;
+ case AV_PIX_FMT_YUV422P10LE:
+ FFMPEG_LOG("Requesting pixel format YUV422P10LE.");
+ return AV_PIX_FMT_YUV422P10LE;
+ case AV_PIX_FMT_YUV444P:
+ FFMPEG_LOG("Requesting pixel format YUV444P.");
+ return AV_PIX_FMT_YUV444P;
+ case AV_PIX_FMT_YUV444P10LE:
+ FFMPEG_LOG("Requesting pixel format YUV444P10LE.");
+ return AV_PIX_FMT_YUV444P10LE;
+#if LIBAVCODEC_VERSION_MAJOR >= 57
+ case AV_PIX_FMT_YUV420P12LE:
+ FFMPEG_LOG("Requesting pixel format YUV420P12LE.");
+ return AV_PIX_FMT_YUV420P12LE;
+ case AV_PIX_FMT_YUV422P12LE:
+ FFMPEG_LOG("Requesting pixel format YUV422P12LE.");
+ return AV_PIX_FMT_YUV422P12LE;
+ case AV_PIX_FMT_YUV444P12LE:
+ FFMPEG_LOG("Requesting pixel format YUV444P12LE.");
+ return AV_PIX_FMT_YUV444P12LE;
+#endif
+ case AV_PIX_FMT_GBRP:
+ FFMPEG_LOG("Requesting pixel format GBRP.");
+ return AV_PIX_FMT_GBRP;
+ default:
+ break;
+ }
+ }
+
+ NS_WARNING("FFmpeg does not share any supported pixel formats.");
+ return AV_PIX_FMT_NONE;
+}
+
+#ifdef MOZ_WAYLAND_USE_HWDECODE
+static AVPixelFormat ChooseVAAPIPixelFormat(AVCodecContext* aCodecContext,
+ const AVPixelFormat* aFormats) {
+ FFMPEG_LOG("Choosing FFmpeg pixel format for VA-API video decoding.");
+ for (; *aFormats > -1; aFormats++) {
+ switch (*aFormats) {
+ case AV_PIX_FMT_VAAPI_VLD:
+ FFMPEG_LOG("Requesting pixel format VAAPI_VLD");
+ return AV_PIX_FMT_VAAPI_VLD;
+ default:
+ break;
+ }
+ }
+ NS_WARNING("FFmpeg does not share any supported pixel formats.");
+ return AV_PIX_FMT_NONE;
+}
+
+AVCodec* FFmpegVideoDecoder<LIBAV_VER>::FindVAAPICodec() {
+ AVCodec* decoder = FindHardwareAVCodec(mLib, mCodecID);
+ if (!decoder) {
+ FFMPEG_LOG(" We're missing hardware accelerated decoder");
+ return nullptr;
+ }
+ for (int i = 0;; i++) {
+ const AVCodecHWConfig* config = mLib->avcodec_get_hw_config(decoder, i);
+ if (!config) {
+ break;
+ }
+ if (config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX &&
+ config->device_type == AV_HWDEVICE_TYPE_VAAPI) {
+ return decoder;
+ }
+ }
+
+ FFMPEG_LOG(" HW Decoder does not support VAAPI device type");
+ return nullptr;
+}
+
+template <int V>
+class VAAPIDisplayHolder {};
+
+template <>
+class VAAPIDisplayHolder<LIBAV_VER>;
+
+template <>
+class VAAPIDisplayHolder<LIBAV_VER> {
+ public:
+ VAAPIDisplayHolder(FFmpegLibWrapper* aLib, VADisplay aDisplay, int aDRMFd)
+ : mLib(aLib), mDisplay(aDisplay), mDRMFd(aDRMFd){};
+ ~VAAPIDisplayHolder() {
+ mLib->vaTerminate(mDisplay);
+ close(mDRMFd);
+ }
+
+ private:
+ FFmpegLibWrapper* mLib;
+ VADisplay mDisplay;
+ int mDRMFd;
+};
+
+static void VAAPIDisplayReleaseCallback(struct AVHWDeviceContext* hwctx) {
+ auto displayHolder =
+ static_cast<VAAPIDisplayHolder<LIBAV_VER>*>(hwctx->user_opaque);
+ delete displayHolder;
+}
+
+bool FFmpegVideoDecoder<LIBAV_VER>::CreateVAAPIDeviceContext() {
+ mVAAPIDeviceContext = mLib->av_hwdevice_ctx_alloc(AV_HWDEVICE_TYPE_VAAPI);
+ if (!mVAAPIDeviceContext) {
+ FFMPEG_LOG(" av_hwdevice_ctx_alloc failed.");
+ return false;
+ }
+
+ auto releaseVAAPIcontext =
+ MakeScopeExit([&] { mLib->av_buffer_unref(&mVAAPIDeviceContext); });
+
+ AVHWDeviceContext* hwctx = (AVHWDeviceContext*)mVAAPIDeviceContext->data;
+ AVVAAPIDeviceContext* vactx = (AVVAAPIDeviceContext*)hwctx->hwctx;
+
+ int drmFd = widget::GetDMABufDevice()->OpenDRMFd();
+ mDisplay = mLib->vaGetDisplayDRM(drmFd);
+ if (!mDisplay) {
+ FFMPEG_LOG(" Can't get DRM VA-API display.");
+ return false;
+ }
+
+ hwctx->user_opaque = new VAAPIDisplayHolder<LIBAV_VER>(mLib, mDisplay, drmFd);
+ hwctx->free = VAAPIDisplayReleaseCallback;
+
+ int major, minor;
+ int status = mLib->vaInitialize(mDisplay, &major, &minor);
+ if (status != VA_STATUS_SUCCESS) {
+ FFMPEG_LOG(" vaInitialize failed.");
+ return false;
+ }
+
+ vactx->display = mDisplay;
+ if (mLib->av_hwdevice_ctx_init(mVAAPIDeviceContext) < 0) {
+ FFMPEG_LOG(" av_hwdevice_ctx_init failed.");
+ return false;
+ }
+
+ mCodecContext->hw_device_ctx = mLib->av_buffer_ref(mVAAPIDeviceContext);
+ releaseVAAPIcontext.release();
+ return true;
+}
+
+MediaResult FFmpegVideoDecoder<LIBAV_VER>::InitVAAPIDecoder() {
+ FFMPEG_LOG("Initialising VA-API FFmpeg decoder");
+
+ StaticMutexAutoLock mon(sMutex);
+
+ // mAcceleratedFormats is already configured so check supported
+ // formats before we do anything.
+ if (mAcceleratedFormats.Length()) {
+ if (!IsFormatAccelerated(mCodecID)) {
+ FFMPEG_LOG(" Format %s is not accelerated",
+ mLib->avcodec_get_name(mCodecID));
+ return NS_ERROR_NOT_AVAILABLE;
+ } else {
+ FFMPEG_LOG(" Format %s is accelerated",
+ mLib->avcodec_get_name(mCodecID));
+ }
+ }
+
+ if (!mLib->IsVAAPIAvailable()) {
+ FFMPEG_LOG(" libva library or symbols are missing.");
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ AVCodec* codec = FindVAAPICodec();
+ if (!codec) {
+ FFMPEG_LOG(" couldn't find ffmpeg VA-API decoder");
+ return NS_ERROR_DOM_MEDIA_FATAL_ERR;
+ }
+ FFMPEG_LOG(" codec %s : %s", codec->name, codec->long_name);
+
+ if (!(mCodecContext = mLib->avcodec_alloc_context3(codec))) {
+ FFMPEG_LOG(" couldn't init VA-API ffmpeg context");
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ mCodecContext->opaque = this;
+
+ InitVAAPICodecContext();
+
+ auto releaseVAAPIdecoder = MakeScopeExit([&] {
+ if (mVAAPIDeviceContext) {
+ mLib->av_buffer_unref(&mVAAPIDeviceContext);
+ }
+ if (mCodecContext) {
+ mLib->av_freep(&mCodecContext);
+ }
+ });
+
+ if (!CreateVAAPIDeviceContext()) {
+ mLib->av_freep(&mCodecContext);
+ FFMPEG_LOG(" Failed to create VA-API device context");
+ return NS_ERROR_DOM_MEDIA_FATAL_ERR;
+ }
+
+ MediaResult ret = AllocateExtraData();
+ if (NS_FAILED(ret)) {
+ mLib->av_buffer_unref(&mVAAPIDeviceContext);
+ mLib->av_freep(&mCodecContext);
+ return ret;
+ }
+
+ if (mLib->avcodec_open2(mCodecContext, codec, nullptr) < 0) {
+ mLib->av_buffer_unref(&mVAAPIDeviceContext);
+ mLib->av_freep(&mCodecContext);
+ FFMPEG_LOG(" Couldn't initialise VA-API decoder");
+ return NS_ERROR_DOM_MEDIA_FATAL_ERR;
+ }
+
+ if (mAcceleratedFormats.IsEmpty()) {
+ mAcceleratedFormats = GetAcceleratedFormats();
+ if (!IsFormatAccelerated(mCodecID)) {
+ FFMPEG_LOG(" Format %s is not accelerated",
+ mLib->avcodec_get_name(mCodecID));
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ }
+
+ if (MOZ_LOG_TEST(sPDMLog, LogLevel::Debug)) {
+ mLib->av_log_set_level(AV_LOG_DEBUG);
+ }
+
+ FFMPEG_LOG(" VA-API FFmpeg init successful");
+ releaseVAAPIdecoder.release();
+ return NS_OK;
+}
+#endif
+
+FFmpegVideoDecoder<LIBAV_VER>::PtsCorrectionContext::PtsCorrectionContext()
+ : mNumFaultyPts(0),
+ mNumFaultyDts(0),
+ mLastPts(INT64_MIN),
+ mLastDts(INT64_MIN) {}
+
+int64_t FFmpegVideoDecoder<LIBAV_VER>::PtsCorrectionContext::GuessCorrectPts(
+ int64_t aPts, int64_t aDts) {
+ int64_t pts = AV_NOPTS_VALUE;
+
+ if (aDts != int64_t(AV_NOPTS_VALUE)) {
+ mNumFaultyDts += aDts <= mLastDts;
+ mLastDts = aDts;
+ }
+ if (aPts != int64_t(AV_NOPTS_VALUE)) {
+ mNumFaultyPts += aPts <= mLastPts;
+ mLastPts = aPts;
+ }
+ if ((mNumFaultyPts <= mNumFaultyDts || aDts == int64_t(AV_NOPTS_VALUE)) &&
+ aPts != int64_t(AV_NOPTS_VALUE)) {
+ pts = aPts;
+ } else {
+ pts = aDts;
+ }
+ return pts;
+}
+
+void FFmpegVideoDecoder<LIBAV_VER>::PtsCorrectionContext::Reset() {
+ mNumFaultyPts = 0;
+ mNumFaultyDts = 0;
+ mLastPts = INT64_MIN;
+ mLastDts = INT64_MIN;
+}
+
+#ifdef MOZ_WAYLAND_USE_HWDECODE
+void FFmpegVideoDecoder<LIBAV_VER>::InitHWDecodingPrefs() {
+ if (!mEnableHardwareDecoding) {
+ FFMPEG_LOG("VAAPI is disabled by parent decoder module.");
+ return;
+ }
+
+ bool supported = false;
+ switch (mCodecID) {
+ case AV_CODEC_ID_H264:
+ supported = gfx::gfxVars::UseH264HwDecode();
+ break;
+ case AV_CODEC_ID_VP8:
+ supported = gfx::gfxVars::UseVP8HwDecode();
+ break;
+ case AV_CODEC_ID_VP9:
+ supported = gfx::gfxVars::UseVP9HwDecode();
+ break;
+ case AV_CODEC_ID_AV1:
+ supported = gfx::gfxVars::UseAV1HwDecode();
+ break;
+ default:
+ break;
+ }
+ if (!supported) {
+ mEnableHardwareDecoding = false;
+ FFMPEG_LOG("Codec %s is not accelerated", mLib->avcodec_get_name(mCodecID));
+ return;
+ }
+
+ bool isHardwareWebRenderUsed = mImageAllocator &&
+ (mImageAllocator->GetCompositorBackendType() ==
+ layers::LayersBackend::LAYERS_WR) &&
+ !mImageAllocator->UsingSoftwareWebRender();
+ if (!isHardwareWebRenderUsed) {
+ mEnableHardwareDecoding = false;
+ FFMPEG_LOG("Hardware WebRender is off, VAAPI is disabled");
+ return;
+ }
+ if (!XRE_IsRDDProcess()) {
+ mEnableHardwareDecoding = false;
+ FFMPEG_LOG("VA-API works in RDD process only");
+ }
+}
+#endif
+
+FFmpegVideoDecoder<LIBAV_VER>::FFmpegVideoDecoder(
+ FFmpegLibWrapper* aLib, const VideoInfo& aConfig,
+ KnowsCompositor* aAllocator, ImageContainer* aImageContainer,
+ bool aLowLatency, bool aDisableHardwareDecoding,
+ Maybe<TrackingId> aTrackingId)
+ : FFmpegDataDecoder(aLib, GetCodecId(aConfig.mMimeType)),
+#ifdef MOZ_WAYLAND_USE_HWDECODE
+ mVAAPIDeviceContext(nullptr),
+ mEnableHardwareDecoding(!aDisableHardwareDecoding),
+ mDisplay(nullptr),
+#endif
+ mImageAllocator(aAllocator),
+ mImageContainer(aImageContainer),
+ mInfo(aConfig),
+ mDecodedFrames(0),
+#if LIBAVCODEC_VERSION_MAJOR >= 58
+ mDecodedFramesLate(0),
+ mMissedDecodeInAverangeTime(0),
+#endif
+ mAverangeDecodeTime(0),
+ mLowLatency(aLowLatency),
+ mTrackingId(std::move(aTrackingId)) {
+ FFMPEG_LOG("FFmpegVideoDecoder::FFmpegVideoDecoder MIME %s Codec ID %d",
+ aConfig.mMimeType.get(), mCodecID);
+ // Use a new MediaByteBuffer as the object will be modified during
+ // initialization.
+ mExtraData = new MediaByteBuffer;
+ mExtraData->AppendElements(*aConfig.mExtraData);
+#ifdef MOZ_WAYLAND_USE_HWDECODE
+ InitHWDecodingPrefs();
+#endif
+}
+
+FFmpegVideoDecoder<LIBAV_VER>::~FFmpegVideoDecoder() {
+#ifdef CUSTOMIZED_BUFFER_ALLOCATION
+ MOZ_DIAGNOSTIC_ASSERT(mAllocatedImages.IsEmpty(),
+ "Should release all shmem buffers before destroy!");
+#endif
+}
+
+RefPtr<MediaDataDecoder::InitPromise> FFmpegVideoDecoder<LIBAV_VER>::Init() {
+ MediaResult rv;
+
+#ifdef MOZ_WAYLAND_USE_HWDECODE
+ if (mEnableHardwareDecoding) {
+ rv = InitVAAPIDecoder();
+ if (NS_SUCCEEDED(rv)) {
+ return InitPromise::CreateAndResolve(TrackInfo::kVideoTrack, __func__);
+ }
+ mEnableHardwareDecoding = false;
+ }
+#endif
+
+ rv = InitDecoder();
+ if (NS_SUCCEEDED(rv)) {
+ return InitPromise::CreateAndResolve(TrackInfo::kVideoTrack, __func__);
+ }
+
+ return InitPromise::CreateAndReject(rv, __func__);
+}
+
+static gfx::ColorDepth GetColorDepth(const AVPixelFormat& aFormat) {
+ switch (aFormat) {
+ case AV_PIX_FMT_YUV420P:
+ case AV_PIX_FMT_YUVJ420P:
+ case AV_PIX_FMT_YUV422P:
+ case AV_PIX_FMT_YUV444P:
+ return gfx::ColorDepth::COLOR_8;
+ case AV_PIX_FMT_YUV420P10LE:
+ case AV_PIX_FMT_YUV422P10LE:
+ case AV_PIX_FMT_YUV444P10LE:
+ return gfx::ColorDepth::COLOR_10;
+#if LIBAVCODEC_VERSION_MAJOR >= 57
+ case AV_PIX_FMT_YUV420P12LE:
+ case AV_PIX_FMT_YUV422P12LE:
+ case AV_PIX_FMT_YUV444P12LE:
+ return gfx::ColorDepth::COLOR_12;
+#endif
+ default:
+ MOZ_ASSERT_UNREACHABLE("Not supported format?");
+ return gfx::ColorDepth::COLOR_8;
+ }
+}
+
+#ifdef CUSTOMIZED_BUFFER_ALLOCATION
+static int GetVideoBufferWrapper(struct AVCodecContext* aCodecContext,
+ AVFrame* aFrame, int aFlags) {
+ auto* decoder =
+ static_cast<FFmpegVideoDecoder<LIBAV_VER>*>(aCodecContext->opaque);
+ int rv = decoder->GetVideoBuffer(aCodecContext, aFrame, aFlags);
+ return rv < 0 ? decoder->GetVideoBufferDefault(aCodecContext, aFrame, aFlags)
+ : rv;
+}
+
+static void ReleaseVideoBufferWrapper(void* opaque, uint8_t* data) {
+ if (opaque) {
+ FFMPEG_LOGV("ReleaseVideoBufferWrapper: PlanarYCbCrImage=%p", opaque);
+ RefPtr<ImageBufferWrapper> image = static_cast<ImageBufferWrapper*>(opaque);
+ image->ReleaseBuffer();
+ }
+}
+
+static gfx::YUVColorSpace TransferAVColorSpaceToYUVColorSpace(
+ AVColorSpace aSpace) {
+ switch (aSpace) {
+ case AVCOL_SPC_BT2020_NCL:
+ case AVCOL_SPC_BT2020_CL:
+ return gfx::YUVColorSpace::BT2020;
+ case AVCOL_SPC_BT709:
+ return gfx::YUVColorSpace::BT709;
+ case AVCOL_SPC_SMPTE170M:
+ case AVCOL_SPC_BT470BG:
+ return gfx::YUVColorSpace::BT601;
+ default:
+ return gfx::YUVColorSpace::Default;
+ }
+}
+
+static bool IsColorFormatSupportedForUsingCustomizedBuffer(
+ const AVPixelFormat& aFormat) {
+# if XP_WIN
+ // Currently the web render doesn't support uploading R16 surface, so we can't
+ // use the shmem texture for 10 bit+ videos which would be uploaded by the
+ // web render. See Bug 1751498.
+ return aFormat == AV_PIX_FMT_YUV420P || aFormat == AV_PIX_FMT_YUVJ420P ||
+ aFormat == AV_PIX_FMT_YUV444P;
+# else
+ // For now, we only support for YUV420P, YUVJ420P and YUV444 which are the
+ // only non-HW accelerated format supported by FFmpeg's H264 and VP9 decoder.
+ return aFormat == AV_PIX_FMT_YUV420P || aFormat == AV_PIX_FMT_YUVJ420P ||
+ aFormat == AV_PIX_FMT_YUV420P10LE ||
+ aFormat == AV_PIX_FMT_YUV420P12LE || aFormat == AV_PIX_FMT_YUV444P ||
+ aFormat == AV_PIX_FMT_YUV444P10LE || aFormat == AV_PIX_FMT_YUV444P12LE;
+# endif
+}
+
+static bool IsYUV420Sampling(const AVPixelFormat& aFormat) {
+ return aFormat == AV_PIX_FMT_YUV420P || aFormat == AV_PIX_FMT_YUVJ420P ||
+ aFormat == AV_PIX_FMT_YUV420P10LE || aFormat == AV_PIX_FMT_YUV420P12LE;
+}
+
+layers::TextureClient*
+FFmpegVideoDecoder<LIBAV_VER>::AllocateTextureClientForImage(
+ struct AVCodecContext* aCodecContext, PlanarYCbCrImage* aImage) {
+ MOZ_ASSERT(
+ IsColorFormatSupportedForUsingCustomizedBuffer(aCodecContext->pix_fmt));
+
+ // FFmpeg will store images with color depth > 8 bits in 16 bits with extra
+ // padding.
+ const int32_t bytesPerChannel =
+ GetColorDepth(aCodecContext->pix_fmt) == gfx::ColorDepth::COLOR_8 ? 1 : 2;
+
+ // If adjusted Ysize is larger than the actual image size (coded_width *
+ // coded_height), that means ffmpeg decoder needs extra padding on both width
+ // and height. If that happens, the planes will need to be cropped later in
+ // order to avoid visible incorrect border on the right and bottom of the
+ // actual image.
+ //
+ // Here are examples of various sizes video in YUV420P format, the width and
+ // height would need to be adjusted in order to align padding.
+ //
+ // Eg1. video (1920*1080)
+ // plane Y
+ // width 1920 height 1080 -> adjusted-width 1920 adjusted-height 1088
+ // plane Cb/Cr
+ // width 960 height 540 -> adjusted-width 1024 adjusted-height 544
+ //
+ // Eg2. video (2560*1440)
+ // plane Y
+ // width 2560 height 1440 -> adjusted-width 2560 adjusted-height 1440
+ // plane Cb/Cr
+ // width 1280 height 720 -> adjusted-width 1280 adjusted-height 736
+ layers::PlanarYCbCrData data;
+ const auto yDims =
+ gfx::IntSize{aCodecContext->coded_width, aCodecContext->coded_height};
+ auto paddedYSize = yDims;
+ mLib->avcodec_align_dimensions(aCodecContext, &paddedYSize.width,
+ &paddedYSize.height);
+ data.mYStride = paddedYSize.Width() * bytesPerChannel;
+
+ MOZ_ASSERT(
+ IsColorFormatSupportedForUsingCustomizedBuffer(aCodecContext->pix_fmt));
+ auto uvDims = yDims;
+ if (IsYUV420Sampling(aCodecContext->pix_fmt)) {
+ uvDims.width = (uvDims.width + 1) / 2;
+ uvDims.height = (uvDims.height + 1) / 2;
+ data.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT;
+ }
+ auto paddedCbCrSize = uvDims;
+ mLib->avcodec_align_dimensions(aCodecContext, &paddedCbCrSize.width,
+ &paddedCbCrSize.height);
+ data.mCbCrStride = paddedCbCrSize.Width() * bytesPerChannel;
+
+ // Setting other attributes
+ data.mPictureRect = gfx::IntRect(
+ mInfo.ScaledImageRect(aCodecContext->width, aCodecContext->height)
+ .TopLeft(),
+ gfx::IntSize(aCodecContext->width, aCodecContext->height));
+ data.mStereoMode = mInfo.mStereoMode;
+ if (aCodecContext->colorspace != AVCOL_SPC_UNSPECIFIED) {
+ data.mYUVColorSpace =
+ TransferAVColorSpaceToYUVColorSpace(aCodecContext->colorspace);
+ } else {
+ data.mYUVColorSpace = mInfo.mColorSpace
+ ? *mInfo.mColorSpace
+ : DefaultColorSpace(data.mPictureRect.Size());
+ }
+ data.mColorDepth = GetColorDepth(aCodecContext->pix_fmt);
+ data.mColorRange = aCodecContext->color_range == AVCOL_RANGE_JPEG
+ ? gfx::ColorRange::FULL
+ : gfx::ColorRange::LIMITED;
+
+ FFMPEG_LOGV(
+ "Created plane data, YSize=(%d, %d), CbCrSize=(%d, %d), "
+ "CroppedYSize=(%d, %d), CroppedCbCrSize=(%d, %d), ColorDepth=%hhu",
+ paddedYSize.Width(), paddedYSize.Height(), paddedCbCrSize.Width(),
+ paddedCbCrSize.Height(), data.YPictureSize().Width(),
+ data.YPictureSize().Height(), data.CbCrPictureSize().Width(),
+ data.CbCrPictureSize().Height(), static_cast<uint8_t>(data.mColorDepth));
+
+ // Allocate a shmem buffer for image.
+ if (!aImage->CreateEmptyBuffer(data, paddedYSize, paddedCbCrSize)) {
+ return nullptr;
+ }
+ return aImage->GetTextureClient(mImageAllocator);
+}
+
+int FFmpegVideoDecoder<LIBAV_VER>::GetVideoBuffer(
+ struct AVCodecContext* aCodecContext, AVFrame* aFrame, int aFlags) {
+ FFMPEG_LOGV("GetVideoBuffer: aCodecContext=%p aFrame=%p", aCodecContext,
+ aFrame);
+ if (!StaticPrefs::media_ffmpeg_customized_buffer_allocation()) {
+ return AVERROR(EINVAL);
+ }
+
+ if (mIsUsingShmemBufferForDecode && !*mIsUsingShmemBufferForDecode) {
+ return AVERROR(EINVAL);
+ }
+
+ // Codec doesn't support custom allocator.
+ if (!(aCodecContext->codec->capabilities & AV_CODEC_CAP_DR1)) {
+ return AVERROR(EINVAL);
+ }
+
+ // Pre-allocation is only for sw decoding. During decoding, ffmpeg decoder
+ // will need to reference decoded frames, if those frames are on shmem buffer,
+ // then it would cause a need to read CPU data from GPU, which is slow.
+ if (IsHardwareAccelerated()) {
+ return AVERROR(EINVAL);
+ }
+
+# if XP_WIN
+ // Disable direct decode to shmem when video overlay could be used with the
+ // video frame
+ if (VideoData::UseUseNV12ForSoftwareDecodedVideoIfPossible(mImageAllocator) &&
+ aCodecContext->width % 2 == 0 && aCodecContext->height % 2 == 0 &&
+ aCodecContext->pix_fmt == AV_PIX_FMT_YUV420P &&
+ aCodecContext->color_range != AVCOL_RANGE_JPEG) {
+ return AVERROR(EINVAL);
+ }
+# endif
+
+ if (!IsColorFormatSupportedForUsingCustomizedBuffer(aCodecContext->pix_fmt)) {
+ FFMPEG_LOG("Not support color format %d", aCodecContext->pix_fmt);
+ return AVERROR(EINVAL);
+ }
+
+ if (aCodecContext->lowres != 0) {
+ FFMPEG_LOG("Not support low resolution decoding");
+ return AVERROR(EINVAL);
+ }
+
+ const gfx::IntSize size(aCodecContext->width, aCodecContext->height);
+ int rv = mLib->av_image_check_size(size.Width(), size.Height(), 0, nullptr);
+ if (rv < 0) {
+ FFMPEG_LOG("Invalid image size");
+ return rv;
+ }
+
+ CheckedInt32 dataSize = mLib->av_image_get_buffer_size(
+ aCodecContext->pix_fmt, aCodecContext->coded_width,
+ aCodecContext->coded_height, 16);
+ if (!dataSize.isValid()) {
+ FFMPEG_LOG("Data size overflow!");
+ return AVERROR(EINVAL);
+ }
+
+ if (!mImageContainer) {
+ FFMPEG_LOG("No Image container!");
+ return AVERROR(EINVAL);
+ }
+
+ RefPtr<PlanarYCbCrImage> image = mImageContainer->CreatePlanarYCbCrImage();
+ if (!image) {
+ FFMPEG_LOG("Failed to create YCbCr image");
+ return AVERROR(EINVAL);
+ }
+
+ RefPtr<layers::TextureClient> texture =
+ AllocateTextureClientForImage(aCodecContext, image);
+ if (!texture) {
+ FFMPEG_LOG("Failed to allocate a texture client");
+ return AVERROR(EINVAL);
+ }
+
+ if (!texture->Lock(layers::OpenMode::OPEN_WRITE)) {
+ FFMPEG_LOG("Failed to lock the texture");
+ return AVERROR(EINVAL);
+ }
+ auto autoUnlock = MakeScopeExit([&] { texture->Unlock(); });
+
+ layers::MappedYCbCrTextureData mapped;
+ if (!texture->BorrowMappedYCbCrData(mapped)) {
+ FFMPEG_LOG("Failed to borrow mapped data for the texture");
+ return AVERROR(EINVAL);
+ }
+
+ aFrame->data[0] = mapped.y.data;
+ aFrame->data[1] = mapped.cb.data;
+ aFrame->data[2] = mapped.cr.data;
+
+ aFrame->linesize[0] = mapped.y.stride;
+ aFrame->linesize[1] = mapped.cb.stride;
+ aFrame->linesize[2] = mapped.cr.stride;
+
+ aFrame->width = aCodecContext->coded_width;
+ aFrame->height = aCodecContext->coded_height;
+ aFrame->format = aCodecContext->pix_fmt;
+ aFrame->extended_data = aFrame->data;
+ aFrame->reordered_opaque = aCodecContext->reordered_opaque;
+ MOZ_ASSERT(aFrame->data[0] && aFrame->data[1] && aFrame->data[2]);
+
+ // This will hold a reference to image, and the reference would be dropped
+ // when ffmpeg tells us that the buffer is no longer needed.
+ auto imageWrapper = MakeRefPtr<ImageBufferWrapper>(image.get(), this);
+ aFrame->buf[0] =
+ mLib->av_buffer_create(aFrame->data[0], dataSize.value(),
+ ReleaseVideoBufferWrapper, imageWrapper.get(), 0);
+ if (!aFrame->buf[0]) {
+ FFMPEG_LOG("Failed to allocate buffer");
+ return AVERROR(EINVAL);
+ }
+
+ FFMPEG_LOG("Created av buffer, buf=%p, data=%p, image=%p, sz=%d",
+ aFrame->buf[0], aFrame->data[0], imageWrapper.get(),
+ dataSize.value());
+ mAllocatedImages.Insert(imageWrapper.get());
+ mIsUsingShmemBufferForDecode = Some(true);
+ return 0;
+}
+#endif
+
+void FFmpegVideoDecoder<LIBAV_VER>::InitCodecContext() {
+ mCodecContext->width = mInfo.mImage.width;
+ mCodecContext->height = mInfo.mImage.height;
+
+ // We use the same logic as libvpx in determining the number of threads to use
+ // so that we end up behaving in the same fashion when using ffmpeg as
+ // we would otherwise cause various crashes (see bug 1236167)
+ int decode_threads = 1;
+ if (mInfo.mDisplay.width >= 2048) {
+ decode_threads = 8;
+ } else if (mInfo.mDisplay.width >= 1024) {
+ decode_threads = 4;
+ } else if (mInfo.mDisplay.width >= 320) {
+ decode_threads = 2;
+ }
+
+ if (mLowLatency) {
+ mCodecContext->flags |= AV_CODEC_FLAG_LOW_DELAY;
+ // ffvp9 and ffvp8 at this stage do not support slice threading, but it may
+ // help with the h264 decoder if there's ever one.
+ mCodecContext->thread_type = FF_THREAD_SLICE;
+ } else {
+ decode_threads = std::min(decode_threads, PR_GetNumberOfProcessors() - 1);
+ decode_threads = std::max(decode_threads, 1);
+ mCodecContext->thread_count = decode_threads;
+ if (decode_threads > 1) {
+ mCodecContext->thread_type = FF_THREAD_SLICE | FF_THREAD_FRAME;
+ }
+ }
+
+ // FFmpeg will call back to this to negotiate a video pixel format.
+ mCodecContext->get_format = ChoosePixelFormat;
+#ifdef CUSTOMIZED_BUFFER_ALLOCATION
+ FFMPEG_LOG("Set get_buffer2 for customized buffer allocation");
+ mCodecContext->get_buffer2 = GetVideoBufferWrapper;
+ mCodecContext->opaque = this;
+# if FF_API_THREAD_SAFE_CALLBACKS
+ mCodecContext->thread_safe_callbacks = 1;
+# endif
+#endif
+}
+
+nsCString FFmpegVideoDecoder<LIBAV_VER>::GetCodecName() const {
+#if LIBAVCODEC_VERSION_MAJOR > 53
+ return nsCString(mLib->avcodec_descriptor_get(mCodecID)->name);
+#else
+ return nsLiteralCString("FFmpegAudioDecoder");
+#endif
+}
+
+#ifdef MOZ_WAYLAND_USE_HWDECODE
+void FFmpegVideoDecoder<LIBAV_VER>::InitVAAPICodecContext() {
+ mCodecContext->width = mInfo.mImage.width;
+ mCodecContext->height = mInfo.mImage.height;
+ mCodecContext->thread_count = 1;
+ mCodecContext->get_format = ChooseVAAPIPixelFormat;
+ if (mCodecID == AV_CODEC_ID_H264) {
+ mCodecContext->extra_hw_frames =
+ H264::ComputeMaxRefFrames(mInfo.mExtraData);
+ } else {
+ mCodecContext->extra_hw_frames = EXTRA_HW_FRAMES;
+ }
+ if (mLowLatency) {
+ mCodecContext->flags |= AV_CODEC_FLAG_LOW_DELAY;
+ }
+}
+#endif
+
+static int64_t GetFramePts(AVFrame* aFrame) {
+#if LIBAVCODEC_VERSION_MAJOR > 57
+ return aFrame->pts;
+#else
+ return aFrame->pkt_pts;
+#endif
+}
+
+void FFmpegVideoDecoder<LIBAV_VER>::UpdateDecodeTimes(TimeStamp aDecodeStart) {
+ mDecodedFrames++;
+ float decodeTime = (TimeStamp::Now() - aDecodeStart).ToMilliseconds();
+ mAverangeDecodeTime =
+ (mAverangeDecodeTime * (mDecodedFrames - 1) + decodeTime) /
+ mDecodedFrames;
+ FFMPEG_LOG(
+ "Frame decode finished, time %.2f ms averange decode time %.2f ms "
+ "decoded %d frames\n",
+ decodeTime, mAverangeDecodeTime, mDecodedFrames);
+#if LIBAVCODEC_VERSION_MAJOR >= 58
+ if (mFrame->pkt_duration > 0) {
+ // Switch frame duration to ms
+ float frameDuration = mFrame->pkt_duration / 1000.0f;
+ if (frameDuration < decodeTime) {
+ PROFILER_MARKER_TEXT("FFmpegVideoDecoder::DoDecode", MEDIA_PLAYBACK, {},
+ "frame decode takes too long");
+ mDecodedFramesLate++;
+ if (frameDuration < mAverangeDecodeTime) {
+ mMissedDecodeInAverangeTime++;
+ }
+ FFMPEG_LOG(
+ " slow decode: failed to decode in time, frame duration %.2f ms, "
+ "decode time %.2f\n",
+ frameDuration, decodeTime);
+ FFMPEG_LOG(" frames: all decoded %d late decoded %d over averange %d\n",
+ mDecodedFrames, mDecodedFramesLate,
+ mMissedDecodeInAverangeTime);
+ }
+ }
+#endif
+}
+
+MediaResult FFmpegVideoDecoder<LIBAV_VER>::DoDecode(
+ MediaRawData* aSample, uint8_t* aData, int aSize, bool* aGotFrame,
+ MediaDataDecoder::DecodedData& aResults) {
+ MOZ_ASSERT(mTaskQueue->IsOnCurrentThread());
+ AVPacket packet;
+ mLib->av_init_packet(&packet);
+
+ TimeStamp decodeStart = TimeStamp::Now();
+
+ packet.data = aData;
+ packet.size = aSize;
+ packet.dts = aSample->mTimecode.ToMicroseconds();
+ packet.pts = aSample->mTime.ToMicroseconds();
+ packet.flags = aSample->mKeyframe ? AV_PKT_FLAG_KEY : 0;
+ packet.pos = aSample->mOffset;
+
+ mTrackingId.apply([&](const auto& aId) {
+ MediaInfoFlag flag = MediaInfoFlag::None;
+ flag |= (aSample->mKeyframe ? MediaInfoFlag::KeyFrame
+ : MediaInfoFlag::NonKeyFrame);
+ flag |= (IsHardwareAccelerated() ? MediaInfoFlag::HardwareDecoding
+ : MediaInfoFlag::SoftwareDecoding);
+ switch (mCodecID) {
+ case AV_CODEC_ID_H264:
+ flag |= MediaInfoFlag::VIDEO_H264;
+ break;
+#if LIBAVCODEC_VERSION_MAJOR >= 54
+ case AV_CODEC_ID_VP8:
+ flag |= MediaInfoFlag::VIDEO_VP8;
+ break;
+#endif
+#if LIBAVCODEC_VERSION_MAJOR >= 55
+ case AV_CODEC_ID_VP9:
+ flag |= MediaInfoFlag::VIDEO_VP9;
+ break;
+#endif
+#ifdef FFMPEG_AV1_DECODE
+ case AV_CODEC_ID_AV1:
+ flag |= MediaInfoFlag::VIDEO_AV1;
+ break;
+#endif
+ default:
+ break;
+ }
+ mPerformanceRecorder.Start(
+ packet.dts,
+ nsPrintfCString("FFmpegVideoDecoder(%d)", LIBAVCODEC_VERSION_MAJOR),
+ aId, flag);
+ });
+
+#if LIBAVCODEC_VERSION_MAJOR >= 58
+ packet.duration = aSample->mDuration.ToMicroseconds();
+ int res = mLib->avcodec_send_packet(mCodecContext, &packet);
+ if (res < 0) {
+ // In theory, avcodec_send_packet could sent -EAGAIN should its internal
+ // buffers be full. In practice this can't happen as we only feed one frame
+ // at a time, and we immediately call avcodec_receive_frame right after.
+ char errStr[AV_ERROR_MAX_STRING_SIZE];
+ mLib->av_strerror(res, errStr, AV_ERROR_MAX_STRING_SIZE);
+ FFMPEG_LOG("avcodec_send_packet error: %s", errStr);
+ return MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ RESULT_DETAIL("avcodec_send_packet error: %s", errStr));
+ }
+ if (aGotFrame) {
+ *aGotFrame = false;
+ }
+ do {
+ if (!PrepareFrame()) {
+ NS_WARNING("FFmpeg decoder failed to allocate frame.");
+ return MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__);
+ }
+
+# ifdef MOZ_WAYLAND_USE_HWDECODE
+ // Release unused VA-API surfaces before avcodec_receive_frame() as
+ // ffmpeg recycles VASurface for HW decoding.
+ if (mVideoFramePool) {
+ mVideoFramePool->ReleaseUnusedVAAPIFrames();
+ }
+# endif
+
+ res = mLib->avcodec_receive_frame(mCodecContext, mFrame);
+ if (res == int(AVERROR_EOF)) {
+ FFMPEG_LOG(" End of stream.");
+ return NS_ERROR_DOM_MEDIA_END_OF_STREAM;
+ }
+ if (res == AVERROR(EAGAIN)) {
+ return NS_OK;
+ }
+ if (res < 0) {
+ char errStr[AV_ERROR_MAX_STRING_SIZE];
+ mLib->av_strerror(res, errStr, AV_ERROR_MAX_STRING_SIZE);
+ FFMPEG_LOG(" avcodec_receive_frame error: %s", errStr);
+ return MediaResult(
+ NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ RESULT_DETAIL("avcodec_receive_frame error: %s", errStr));
+ }
+
+ UpdateDecodeTimes(decodeStart);
+ decodeStart = TimeStamp::Now();
+
+ MediaResult rv;
+# ifdef MOZ_WAYLAND_USE_HWDECODE
+ if (IsHardwareAccelerated()) {
+ if (mMissedDecodeInAverangeTime > HW_DECODE_LATE_FRAMES) {
+ PROFILER_MARKER_TEXT("FFmpegVideoDecoder::DoDecode", MEDIA_PLAYBACK, {},
+ "Fallback to SW decode");
+ FFMPEG_LOG(" HW decoding is slow, switch back to SW decode");
+ return MediaResult(
+ NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ RESULT_DETAIL("HW decoding is slow, switch back to SW decode"));
+ }
+ rv = CreateImageVAAPI(mFrame->pkt_pos, GetFramePts(mFrame),
+ mFrame->pkt_duration, aResults);
+ // If VA-API playback failed, just quit. Decoder is going to be restarted
+ // without VA-API.
+ if (NS_FAILED(rv)) {
+ // Explicitly remove dmabuf surface pool as it's configured
+ // for VA-API support.
+ mVideoFramePool = nullptr;
+ return rv;
+ }
+ } else
+# endif
+ {
+ rv = CreateImage(mFrame->pkt_pos, GetFramePts(mFrame),
+ mFrame->pkt_duration, aResults);
+ }
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ mPerformanceRecorder.Record(mFrame->pkt_dts, [&](auto& aStage) {
+ aStage.SetResolution(mFrame->width, mFrame->height);
+ auto format = [&]() -> Maybe<DecodeStage::ImageFormat> {
+ switch (mCodecContext->pix_fmt) {
+ case AV_PIX_FMT_YUV420P:
+ case AV_PIX_FMT_YUVJ420P:
+ case AV_PIX_FMT_YUV420P10LE:
+# if LIBAVCODEC_VERSION_MAJOR >= 57
+ case AV_PIX_FMT_YUV420P12LE:
+# endif
+ return Some(DecodeStage::YUV420P);
+ case AV_PIX_FMT_YUV422P:
+ case AV_PIX_FMT_YUV422P10LE:
+# if LIBAVCODEC_VERSION_MAJOR >= 57
+ case AV_PIX_FMT_YUV422P12LE:
+# endif
+ return Some(DecodeStage::YUV422P);
+ case AV_PIX_FMT_YUV444P:
+ case AV_PIX_FMT_YUV444P10LE:
+# if LIBAVCODEC_VERSION_MAJOR >= 57
+ case AV_PIX_FMT_YUV444P12LE:
+# endif
+ return Some(DecodeStage::YUV444P);
+ case AV_PIX_FMT_GBRP:
+ return Some(DecodeStage::GBRP);
+ default:
+ return Nothing();
+ }
+ }();
+ format.apply([&](auto& aFmt) { aStage.SetImageFormat(aFmt); });
+ aStage.SetColorDepth(GetColorDepth(mCodecContext->pix_fmt));
+ aStage.SetYUVColorSpace(GetFrameColorSpace());
+ aStage.SetColorRange(GetFrameColorRange());
+ });
+ if (aGotFrame) {
+ *aGotFrame = true;
+ }
+ } while (true);
+#else
+ // LibAV provides no API to retrieve the decoded sample's duration.
+ // (FFmpeg >= 1.0 provides av_frame_get_pkt_duration)
+ // As such we instead use a map using the dts as key that we will retrieve
+ // later.
+ // The map will have a typical size of 16 entry.
+ mDurationMap.Insert(aSample->mTimecode.ToMicroseconds(),
+ aSample->mDuration.ToMicroseconds());
+
+ if (!PrepareFrame()) {
+ NS_WARNING("FFmpeg decoder failed to allocate frame.");
+ return MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__);
+ }
+
+ // Required with old version of FFmpeg/LibAV
+ mFrame->reordered_opaque = AV_NOPTS_VALUE;
+
+ int decoded;
+ int bytesConsumed =
+ mLib->avcodec_decode_video2(mCodecContext, mFrame, &decoded, &packet);
+
+ FFMPEG_LOG(
+ "DoDecodeFrame:decode_video: rv=%d decoded=%d "
+ "(Input: pts(%" PRId64 ") dts(%" PRId64 ") Output: pts(%" PRId64
+ ") "
+ "opaque(%" PRId64 ") pts(%" PRId64 ") pkt_dts(%" PRId64 "))",
+ bytesConsumed, decoded, packet.pts, packet.dts, mFrame->pts,
+ mFrame->reordered_opaque, mFrame->pts, mFrame->pkt_dts);
+
+ if (bytesConsumed < 0) {
+ return MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ RESULT_DETAIL("FFmpeg video error: %d", bytesConsumed));
+ }
+
+ if (!decoded) {
+ if (aGotFrame) {
+ *aGotFrame = false;
+ }
+ return NS_OK;
+ }
+
+ UpdateDecodeTimes(decodeStart);
+
+ // If we've decoded a frame then we need to output it
+ int64_t pts =
+ mPtsContext.GuessCorrectPts(GetFramePts(mFrame), mFrame->pkt_dts);
+ // Retrieve duration from dts.
+ // We use the first entry found matching this dts (this is done to
+ // handle damaged file with multiple frames with the same dts)
+
+ int64_t duration;
+ if (!mDurationMap.Find(mFrame->pkt_dts, duration)) {
+ NS_WARNING("Unable to retrieve duration from map");
+ duration = aSample->mDuration.ToMicroseconds();
+ // dts are probably incorrectly reported ; so clear the map as we're
+ // unlikely to find them in the future anyway. This also guards
+ // against the map becoming extremely big.
+ mDurationMap.Clear();
+ }
+
+ MediaResult rv = CreateImage(aSample->mOffset, pts, duration, aResults);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ mTrackingId.apply([&](const auto&) {
+ mPerformanceRecorder.Record(mFrame->pkt_dts, [&](DecodeStage& aStage) {
+ aStage.SetResolution(mFrame->width, mFrame->height);
+ auto format = [&]() -> Maybe<DecodeStage::ImageFormat> {
+ switch (mCodecContext->pix_fmt) {
+ case AV_PIX_FMT_YUV420P:
+ case AV_PIX_FMT_YUVJ420P:
+ case AV_PIX_FMT_YUV420P10LE:
+# if LIBAVCODEC_VERSION_MAJOR >= 57
+ case AV_PIX_FMT_YUV420P12LE:
+# endif
+ return Some(DecodeStage::YUV420P);
+ case AV_PIX_FMT_YUV422P:
+ case AV_PIX_FMT_YUV422P10LE:
+# if LIBAVCODEC_VERSION_MAJOR >= 57
+ case AV_PIX_FMT_YUV422P12LE:
+# endif
+ return Some(DecodeStage::YUV422P);
+ case AV_PIX_FMT_YUV444P:
+ case AV_PIX_FMT_YUV444P10LE:
+# if LIBAVCODEC_VERSION_MAJOR >= 57
+ case AV_PIX_FMT_YUV444P12LE:
+# endif
+ return Some(DecodeStage::YUV444P);
+ case AV_PIX_FMT_GBRP:
+ return Some(DecodeStage::GBRP);
+ default:
+ return Nothing();
+ }
+ }();
+ format.apply([&](auto& aFmt) { aStage.SetImageFormat(aFmt); });
+ aStage.SetColorDepth(GetColorDepth(mCodecContext->pix_fmt));
+ aStage.SetYUVColorSpace(GetFrameColorSpace());
+ aStage.SetColorRange(GetFrameColorRange());
+ });
+ });
+
+ if (aGotFrame) {
+ *aGotFrame = true;
+ }
+ return rv;
+#endif
+}
+
+gfx::YUVColorSpace FFmpegVideoDecoder<LIBAV_VER>::GetFrameColorSpace() const {
+#if LIBAVCODEC_VERSION_MAJOR > 58
+ switch (mFrame->colorspace) {
+#else
+ AVColorSpace colorSpace = AVCOL_SPC_UNSPECIFIED;
+ if (mLib->av_frame_get_colorspace) {
+ colorSpace = (AVColorSpace)mLib->av_frame_get_colorspace(mFrame);
+ }
+ switch (colorSpace) {
+#endif
+#if LIBAVCODEC_VERSION_MAJOR >= 55
+ case AVCOL_SPC_BT2020_NCL:
+ case AVCOL_SPC_BT2020_CL:
+ return gfx::YUVColorSpace::BT2020;
+#endif
+ case AVCOL_SPC_BT709:
+ return gfx::YUVColorSpace::BT709;
+ case AVCOL_SPC_SMPTE170M:
+ case AVCOL_SPC_BT470BG:
+ return gfx::YUVColorSpace::BT601;
+ case AVCOL_SPC_RGB:
+ return gfx::YUVColorSpace::Identity;
+ default:
+ return DefaultColorSpace({mFrame->width, mFrame->height});
+ }
+}
+
+gfx::ColorSpace2 FFmpegVideoDecoder<LIBAV_VER>::GetFrameColorPrimaries() const {
+ AVColorPrimaries colorPrimaries = AVCOL_PRI_UNSPECIFIED;
+#if LIBAVCODEC_VERSION_MAJOR > 57
+ colorPrimaries = mFrame->color_primaries;
+#endif
+ switch (colorPrimaries) {
+#if LIBAVCODEC_VERSION_MAJOR >= 55
+ case AVCOL_PRI_BT2020:
+ return gfx::ColorSpace2::BT2020;
+#endif
+ case AVCOL_PRI_BT709:
+ return gfx::ColorSpace2::BT709;
+ default:
+ return gfx::ColorSpace2::BT709;
+ }
+}
+
+gfx::ColorRange FFmpegVideoDecoder<LIBAV_VER>::GetFrameColorRange() const {
+ AVColorRange range = AVCOL_RANGE_UNSPECIFIED;
+#if LIBAVCODEC_VERSION_MAJOR > 58
+ range = mFrame->color_range;
+#else
+ if (mLib->av_frame_get_color_range) {
+ range = (AVColorRange)mLib->av_frame_get_color_range(mFrame);
+ }
+#endif
+ return range == AVCOL_RANGE_JPEG ? gfx::ColorRange::FULL
+ : gfx::ColorRange::LIMITED;
+}
+
+MediaResult FFmpegVideoDecoder<LIBAV_VER>::CreateImage(
+ int64_t aOffset, int64_t aPts, int64_t aDuration,
+ MediaDataDecoder::DecodedData& aResults) const {
+ FFMPEG_LOG("Got one frame output with pts=%" PRId64 " dts=%" PRId64
+ " duration=%" PRId64 " opaque=%" PRId64,
+ aPts, mFrame->pkt_dts, aDuration, mCodecContext->reordered_opaque);
+
+ VideoData::YCbCrBuffer b;
+ b.mPlanes[0].mData = mFrame->data[0];
+ b.mPlanes[1].mData = mFrame->data[1];
+ b.mPlanes[2].mData = mFrame->data[2];
+
+ b.mPlanes[0].mStride = mFrame->linesize[0];
+ b.mPlanes[1].mStride = mFrame->linesize[1];
+ b.mPlanes[2].mStride = mFrame->linesize[2];
+
+ b.mPlanes[0].mSkip = 0;
+ b.mPlanes[1].mSkip = 0;
+ b.mPlanes[2].mSkip = 0;
+
+ b.mPlanes[0].mWidth = mFrame->width;
+ b.mPlanes[0].mHeight = mFrame->height;
+ if (mCodecContext->pix_fmt == AV_PIX_FMT_YUV444P ||
+ mCodecContext->pix_fmt == AV_PIX_FMT_YUV444P10LE ||
+ mCodecContext->pix_fmt == AV_PIX_FMT_GBRP
+#if LIBAVCODEC_VERSION_MAJOR >= 57
+ || mCodecContext->pix_fmt == AV_PIX_FMT_YUV444P12LE
+#endif
+ ) {
+ b.mPlanes[1].mWidth = b.mPlanes[2].mWidth = mFrame->width;
+ b.mPlanes[1].mHeight = b.mPlanes[2].mHeight = mFrame->height;
+ if (mCodecContext->pix_fmt == AV_PIX_FMT_YUV444P10LE) {
+ b.mColorDepth = gfx::ColorDepth::COLOR_10;
+ }
+#if LIBAVCODEC_VERSION_MAJOR >= 57
+ else if (mCodecContext->pix_fmt == AV_PIX_FMT_YUV444P12LE) {
+ b.mColorDepth = gfx::ColorDepth::COLOR_12;
+ }
+#endif
+ } else if (mCodecContext->pix_fmt == AV_PIX_FMT_YUV422P ||
+ mCodecContext->pix_fmt == AV_PIX_FMT_YUV422P10LE
+#if LIBAVCODEC_VERSION_MAJOR >= 57
+ || mCodecContext->pix_fmt == AV_PIX_FMT_YUV422P12LE
+#endif
+ ) {
+ b.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH;
+ b.mPlanes[1].mWidth = b.mPlanes[2].mWidth = (mFrame->width + 1) >> 1;
+ b.mPlanes[1].mHeight = b.mPlanes[2].mHeight = mFrame->height;
+ if (mCodecContext->pix_fmt == AV_PIX_FMT_YUV422P10LE) {
+ b.mColorDepth = gfx::ColorDepth::COLOR_10;
+ }
+#if LIBAVCODEC_VERSION_MAJOR >= 57
+ else if (mCodecContext->pix_fmt == AV_PIX_FMT_YUV422P12LE) {
+ b.mColorDepth = gfx::ColorDepth::COLOR_12;
+ }
+#endif
+ } else {
+ b.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT;
+ b.mPlanes[1].mWidth = b.mPlanes[2].mWidth = (mFrame->width + 1) >> 1;
+ b.mPlanes[1].mHeight = b.mPlanes[2].mHeight = (mFrame->height + 1) >> 1;
+ if (mCodecContext->pix_fmt == AV_PIX_FMT_YUV420P10LE) {
+ b.mColorDepth = gfx::ColorDepth::COLOR_10;
+ }
+#if LIBAVCODEC_VERSION_MAJOR >= 57
+ else if (mCodecContext->pix_fmt == AV_PIX_FMT_YUV420P12LE) {
+ b.mColorDepth = gfx::ColorDepth::COLOR_12;
+ }
+#endif
+ }
+ b.mYUVColorSpace = GetFrameColorSpace();
+ b.mColorRange = GetFrameColorRange();
+
+ RefPtr<VideoData> v;
+#ifdef CUSTOMIZED_BUFFER_ALLOCATION
+ bool requiresCopy = false;
+# ifdef XP_MACOSX
+ // Bug 1765388: macOS needs to generate a MacIOSurfaceImage in order to
+ // properly display HDR video. The later call to ::CreateAndCopyData does
+ // that. If this shared memory buffer path also generated a
+ // MacIOSurfaceImage, then we could use it for HDR.
+ requiresCopy = (b.mColorDepth != gfx::ColorDepth::COLOR_8);
+# endif
+ if (mIsUsingShmemBufferForDecode && *mIsUsingShmemBufferForDecode &&
+ !requiresCopy) {
+ RefPtr<ImageBufferWrapper> wrapper = static_cast<ImageBufferWrapper*>(
+ mLib->av_buffer_get_opaque(mFrame->buf[0]));
+ MOZ_ASSERT(wrapper);
+ FFMPEG_LOGV("Create a video data from a shmem image=%p", wrapper.get());
+ v = VideoData::CreateFromImage(
+ mInfo.mDisplay, aOffset, TimeUnit::FromMicroseconds(aPts),
+ TimeUnit::FromMicroseconds(aDuration), wrapper->AsImage(),
+ !!mFrame->key_frame, TimeUnit::FromMicroseconds(-1));
+ }
+#endif
+ if (!v) {
+ v = VideoData::CreateAndCopyData(
+ mInfo, mImageContainer, aOffset, TimeUnit::FromMicroseconds(aPts),
+ TimeUnit::FromMicroseconds(aDuration), b, !!mFrame->key_frame,
+ TimeUnit::FromMicroseconds(-1),
+ mInfo.ScaledImageRect(mFrame->width, mFrame->height), mImageAllocator);
+ }
+
+ if (!v) {
+ return MediaResult(NS_ERROR_OUT_OF_MEMORY,
+ RESULT_DETAIL("image allocation error"));
+ }
+ aResults.AppendElement(std::move(v));
+ return NS_OK;
+}
+
+#ifdef MOZ_WAYLAND_USE_HWDECODE
+bool FFmpegVideoDecoder<LIBAV_VER>::GetVAAPISurfaceDescriptor(
+ VADRMPRIMESurfaceDescriptor* aVaDesc) {
+ VASurfaceID surface_id = (VASurfaceID)(uintptr_t)mFrame->data[3];
+ VAStatus vas = mLib->vaExportSurfaceHandle(
+ mDisplay, surface_id, VA_SURFACE_ATTRIB_MEM_TYPE_DRM_PRIME_2,
+ VA_EXPORT_SURFACE_READ_ONLY | VA_EXPORT_SURFACE_SEPARATE_LAYERS, aVaDesc);
+ if (vas != VA_STATUS_SUCCESS) {
+ return false;
+ }
+ vas = mLib->vaSyncSurface(mDisplay, surface_id);
+ if (vas != VA_STATUS_SUCCESS) {
+ NS_WARNING("vaSyncSurface() failed.");
+ }
+ return true;
+}
+
+MediaResult FFmpegVideoDecoder<LIBAV_VER>::CreateImageVAAPI(
+ int64_t aOffset, int64_t aPts, int64_t aDuration,
+ MediaDataDecoder::DecodedData& aResults) {
+ FFMPEG_LOG("VA-API Got one frame output with pts=%" PRId64 " dts=%" PRId64
+ " duration=%" PRId64 " opaque=%" PRId64,
+ aPts, mFrame->pkt_dts, aDuration, mCodecContext->reordered_opaque);
+
+ VADRMPRIMESurfaceDescriptor vaDesc;
+ if (!GetVAAPISurfaceDescriptor(&vaDesc)) {
+ return MediaResult(
+ NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ RESULT_DETAIL("Unable to get frame by vaExportSurfaceHandle()"));
+ }
+ auto releaseSurfaceDescriptor = MakeScopeExit(
+ [&] { DMABufSurfaceYUV::ReleaseVADRMPRIMESurfaceDescriptor(vaDesc); });
+
+ MOZ_ASSERT(mTaskQueue->IsOnCurrentThread());
+ if (!mVideoFramePool) {
+ AVHWFramesContext* context =
+ (AVHWFramesContext*)mCodecContext->hw_frames_ctx->data;
+ mVideoFramePool =
+ MakeUnique<VideoFramePool<LIBAV_VER>>(context->initial_pool_size);
+ }
+ auto surface = mVideoFramePool->GetVideoFrameSurface(
+ vaDesc, mFrame->width, mFrame->height, mCodecContext, mFrame, mLib);
+ if (!surface) {
+ return MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ RESULT_DETAIL("VAAPI dmabuf allocation error"));
+ }
+ surface->SetYUVColorSpace(GetFrameColorSpace());
+ surface->SetColorRange(GetFrameColorRange());
+
+ RefPtr<VideoData> vp = VideoData::CreateFromImage(
+ mInfo.mDisplay, aOffset, TimeUnit::FromMicroseconds(aPts),
+ TimeUnit::FromMicroseconds(aDuration), surface->GetAsImage(),
+ !!mFrame->key_frame, TimeUnit::FromMicroseconds(-1));
+
+ if (!vp) {
+ return MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ RESULT_DETAIL("VAAPI image allocation error"));
+ }
+
+ aResults.AppendElement(std::move(vp));
+ return NS_OK;
+}
+#endif
+
+RefPtr<MediaDataDecoder::FlushPromise>
+FFmpegVideoDecoder<LIBAV_VER>::ProcessFlush() {
+ FFMPEG_LOG("ProcessFlush()");
+ MOZ_ASSERT(mTaskQueue->IsOnCurrentThread());
+ mPtsContext.Reset();
+ mDurationMap.Clear();
+ mPerformanceRecorder.Record(std::numeric_limits<int64_t>::max());
+ return FFmpegDataDecoder::ProcessFlush();
+}
+
+AVCodecID FFmpegVideoDecoder<LIBAV_VER>::GetCodecId(
+ const nsACString& aMimeType) {
+ if (MP4Decoder::IsH264(aMimeType)) {
+ return AV_CODEC_ID_H264;
+ }
+
+ if (aMimeType.EqualsLiteral("video/x-vnd.on2.vp6")) {
+ return AV_CODEC_ID_VP6F;
+ }
+
+#if LIBAVCODEC_VERSION_MAJOR >= 54
+ if (VPXDecoder::IsVP8(aMimeType)) {
+ return AV_CODEC_ID_VP8;
+ }
+#endif
+
+#if LIBAVCODEC_VERSION_MAJOR >= 55
+ if (VPXDecoder::IsVP9(aMimeType)) {
+ return AV_CODEC_ID_VP9;
+ }
+#endif
+
+#if defined(FFMPEG_AV1_DECODE)
+ if (AOMDecoder::IsAV1(aMimeType)) {
+ return AV_CODEC_ID_AV1;
+ }
+#endif
+
+ return AV_CODEC_ID_NONE;
+}
+
+void FFmpegVideoDecoder<LIBAV_VER>::ProcessShutdown() {
+ MOZ_ASSERT(mTaskQueue->IsOnCurrentThread());
+#ifdef MOZ_WAYLAND_USE_HWDECODE
+ mVideoFramePool = nullptr;
+ if (IsHardwareAccelerated()) {
+ mLib->av_buffer_unref(&mVAAPIDeviceContext);
+ }
+#endif
+ FFmpegDataDecoder<LIBAV_VER>::ProcessShutdown();
+}
+
+bool FFmpegVideoDecoder<LIBAV_VER>::IsHardwareAccelerated(
+ nsACString& aFailureReason) const {
+#ifdef MOZ_WAYLAND_USE_HWDECODE
+ return !!mVAAPIDeviceContext;
+#else
+ return false;
+#endif
+}
+
+#ifdef MOZ_WAYLAND_USE_HWDECODE
+bool FFmpegVideoDecoder<LIBAV_VER>::IsFormatAccelerated(
+ AVCodecID aCodecID) const {
+ for (const auto& format : mAcceleratedFormats) {
+ if (format == aCodecID) {
+ return true;
+ }
+ }
+ return false;
+}
+
+// See ffmpeg / vaapi_decode.c how CodecID is mapped to VAProfile.
+static const struct {
+ enum AVCodecID codec_id;
+ VAProfile va_profile;
+ char name[100];
+} vaapi_profile_map[] = {
+# define MAP(c, v, n) {AV_CODEC_ID_##c, VAProfile##v, n}
+ MAP(H264, H264ConstrainedBaseline, "H264ConstrainedBaseline"),
+ MAP(H264, H264Main, "H264Main"),
+ MAP(H264, H264High, "H264High"),
+ MAP(VP8, VP8Version0_3, "VP8Version0_3"),
+ MAP(VP9, VP9Profile0, "VP9Profile0"),
+ MAP(VP9, VP9Profile2, "VP9Profile2"),
+ MAP(AV1, AV1Profile0, "AV1Profile0"),
+ MAP(AV1, AV1Profile1, "AV1Profile1"),
+# undef MAP
+};
+
+static AVCodecID VAProfileToCodecID(VAProfile aVAProfile) {
+ for (const auto& profile : vaapi_profile_map) {
+ if (profile.va_profile == aVAProfile) {
+ return profile.codec_id;
+ }
+ }
+ return AV_CODEC_ID_NONE;
+}
+
+static const char* VAProfileName(VAProfile aVAProfile) {
+ for (const auto& profile : vaapi_profile_map) {
+ if (profile.va_profile == aVAProfile) {
+ return profile.name;
+ }
+ }
+ return nullptr;
+}
+
+// This code is adopted from mpv project va-api routine
+// determine_working_formats()
+void FFmpegVideoDecoder<LIBAV_VER>::AddAcceleratedFormats(
+ nsTArray<AVCodecID>& aCodecList, AVCodecID aCodecID,
+ AVVAAPIHWConfig* hwconfig) {
+ AVHWFramesConstraints* fc =
+ mLib->av_hwdevice_get_hwframe_constraints(mVAAPIDeviceContext, hwconfig);
+ if (!fc) {
+ FFMPEG_LOG(" failed to retrieve libavutil frame constraints");
+ return;
+ }
+ auto autoRelease =
+ MakeScopeExit([&] { mLib->av_hwframe_constraints_free(&fc); });
+
+ bool foundSupportedFormat = false;
+ for (int n = 0;
+ fc->valid_sw_formats && fc->valid_sw_formats[n] != AV_PIX_FMT_NONE;
+ n++) {
+# ifdef MOZ_LOGGING
+ char formatDesc[1000];
+ FFMPEG_LOG(" codec %s format %s", mLib->avcodec_get_name(aCodecID),
+ mLib->av_get_pix_fmt_string(formatDesc, sizeof(formatDesc),
+ fc->valid_sw_formats[n]));
+# endif
+ if (fc->valid_sw_formats[n] == AV_PIX_FMT_NV12 ||
+ fc->valid_sw_formats[n] == AV_PIX_FMT_YUV420P) {
+ foundSupportedFormat = true;
+# ifndef MOZ_LOGGING
+ break;
+# endif
+ }
+ }
+
+ if (!foundSupportedFormat) {
+ FFMPEG_LOG(" %s target pixel format is not supported!",
+ mLib->avcodec_get_name(aCodecID));
+ return;
+ }
+
+ if (!aCodecList.Contains(aCodecID)) {
+ aCodecList.AppendElement(aCodecID);
+ }
+}
+
+nsTArray<AVCodecID> FFmpegVideoDecoder<LIBAV_VER>::GetAcceleratedFormats() {
+ FFMPEG_LOG("FFmpegVideoDecoder::GetAcceleratedFormats()");
+
+ VAProfile* profiles = nullptr;
+ VAEntrypoint* entryPoints = nullptr;
+
+ nsTArray<AVCodecID> supportedHWCodecs(AV_CODEC_ID_NONE);
+# ifdef MOZ_LOGGING
+ auto printCodecs = MakeScopeExit([&] {
+ FFMPEG_LOG(" Supported accelerated formats:");
+ for (unsigned i = 0; i < supportedHWCodecs.Length(); i++) {
+ FFMPEG_LOG(" %s", mLib->avcodec_get_name(supportedHWCodecs[i]));
+ }
+ });
+# endif
+
+ AVVAAPIHWConfig* hwconfig =
+ mLib->av_hwdevice_hwconfig_alloc(mVAAPIDeviceContext);
+ if (!hwconfig) {
+ FFMPEG_LOG(" failed to get AVVAAPIHWConfig");
+ return supportedHWCodecs;
+ }
+ auto autoRelease = MakeScopeExit([&] {
+ delete[] profiles;
+ delete[] entryPoints;
+ mLib->av_freep(&hwconfig);
+ });
+
+ int maxProfiles = vaMaxNumProfiles(mDisplay);
+ int maxEntryPoints = vaMaxNumEntrypoints(mDisplay);
+ if (MOZ_UNLIKELY(maxProfiles <= 0 || maxEntryPoints <= 0)) {
+ return supportedHWCodecs;
+ }
+
+ profiles = new VAProfile[maxProfiles];
+ int numProfiles = 0;
+ VAStatus status = vaQueryConfigProfiles(mDisplay, profiles, &numProfiles);
+ if (status != VA_STATUS_SUCCESS) {
+ FFMPEG_LOG(" vaQueryConfigProfiles() failed %s", vaErrorStr(status));
+ return supportedHWCodecs;
+ }
+ numProfiles = MIN(numProfiles, maxProfiles);
+
+ entryPoints = new VAEntrypoint[maxEntryPoints];
+ for (int p = 0; p < numProfiles; p++) {
+ VAProfile profile = profiles[p];
+
+ AVCodecID codecID = VAProfileToCodecID(profile);
+ if (codecID == AV_CODEC_ID_NONE) {
+ continue;
+ }
+
+ int numEntryPoints = 0;
+ status = vaQueryConfigEntrypoints(mDisplay, profile, entryPoints,
+ &numEntryPoints);
+ if (status != VA_STATUS_SUCCESS) {
+ FFMPEG_LOG(" vaQueryConfigEntrypoints() failed: '%s' for profile %d",
+ vaErrorStr(status), (int)profile);
+ continue;
+ }
+ numEntryPoints = MIN(numEntryPoints, maxEntryPoints);
+
+ FFMPEG_LOG(" Profile %s:", VAProfileName(profile));
+ for (int e = 0; e < numEntryPoints; e++) {
+ VAConfigID config = VA_INVALID_ID;
+ status = vaCreateConfig(mDisplay, profile, entryPoints[e], nullptr, 0,
+ &config);
+ if (status != VA_STATUS_SUCCESS) {
+ FFMPEG_LOG(" vaCreateConfig() failed: '%s' for profile %d",
+ vaErrorStr(status), (int)profile);
+ continue;
+ }
+ hwconfig->config_id = config;
+ AddAcceleratedFormats(supportedHWCodecs, codecID, hwconfig);
+ vaDestroyConfig(mDisplay, config);
+ }
+ }
+
+ return supportedHWCodecs;
+}
+
+#endif
+
+} // namespace mozilla
diff --git a/dom/media/platforms/ffmpeg/FFmpegVideoDecoder.h b/dom/media/platforms/ffmpeg/FFmpegVideoDecoder.h
new file mode 100644
index 0000000000..62372988a8
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/FFmpegVideoDecoder.h
@@ -0,0 +1,233 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef __FFmpegVideoDecoder_h__
+#define __FFmpegVideoDecoder_h__
+
+#include "ImageContainer.h"
+#include "FFmpegDataDecoder.h"
+#include "FFmpegLibWrapper.h"
+#include "PerformanceRecorder.h"
+#include "SimpleMap.h"
+#include "mozilla/ScopeExit.h"
+#include "nsTHashSet.h"
+#if LIBAVCODEC_VERSION_MAJOR >= 57 && LIBAVUTIL_VERSION_MAJOR >= 56
+# include "mozilla/layers/TextureClient.h"
+#endif
+#ifdef MOZ_WAYLAND_USE_HWDECODE
+# include "FFmpegVideoFramePool.h"
+#endif
+
+struct _VADRMPRIMESurfaceDescriptor;
+typedef struct _VADRMPRIMESurfaceDescriptor VADRMPRIMESurfaceDescriptor;
+
+namespace mozilla {
+
+class ImageBufferWrapper;
+
+template <int V>
+class FFmpegVideoDecoder : public FFmpegDataDecoder<V> {};
+
+template <>
+class FFmpegVideoDecoder<LIBAV_VER>;
+DDLoggedTypeNameAndBase(FFmpegVideoDecoder<LIBAV_VER>,
+ FFmpegDataDecoder<LIBAV_VER>);
+
+template <>
+class FFmpegVideoDecoder<LIBAV_VER>
+ : public FFmpegDataDecoder<LIBAV_VER>,
+ public DecoderDoctorLifeLogger<FFmpegVideoDecoder<LIBAV_VER>> {
+ typedef mozilla::layers::Image Image;
+ typedef mozilla::layers::ImageContainer ImageContainer;
+ typedef mozilla::layers::KnowsCompositor KnowsCompositor;
+ typedef SimpleMap<int64_t> DurationMap;
+
+ public:
+ FFmpegVideoDecoder(FFmpegLibWrapper* aLib, const VideoInfo& aConfig,
+ KnowsCompositor* aAllocator,
+ ImageContainer* aImageContainer, bool aLowLatency,
+ bool aDisableHardwareDecoding,
+ Maybe<TrackingId> aTrackingId);
+
+ ~FFmpegVideoDecoder();
+
+ RefPtr<InitPromise> Init() override;
+ void InitCodecContext() MOZ_REQUIRES(sMutex) override;
+ nsCString GetDescriptionName() const override {
+#ifdef USING_MOZFFVPX
+ return "ffvpx video decoder"_ns;
+#else
+ return "ffmpeg video decoder"_ns;
+#endif
+ }
+ nsCString GetCodecName() const override;
+ ConversionRequired NeedsConversion() const override {
+ return ConversionRequired::kNeedAVCC;
+ }
+
+ static AVCodecID GetCodecId(const nsACString& aMimeType);
+
+#if LIBAVCODEC_VERSION_MAJOR >= 57 && LIBAVUTIL_VERSION_MAJOR >= 56
+ int GetVideoBuffer(struct AVCodecContext* aCodecContext, AVFrame* aFrame,
+ int aFlags);
+ int GetVideoBufferDefault(struct AVCodecContext* aCodecContext,
+ AVFrame* aFrame, int aFlags) {
+ mIsUsingShmemBufferForDecode = Some(false);
+ return mLib->avcodec_default_get_buffer2(aCodecContext, aFrame, aFlags);
+ }
+ void ReleaseAllocatedImage(ImageBufferWrapper* aImage) {
+ mAllocatedImages.Remove(aImage);
+ }
+#endif
+
+ private:
+ RefPtr<FlushPromise> ProcessFlush() override;
+ void ProcessShutdown() override;
+ MediaResult DoDecode(MediaRawData* aSample, uint8_t* aData, int aSize,
+ bool* aGotFrame, DecodedData& aResults) override;
+ void OutputDelayedFrames();
+ bool NeedParser() const override {
+ return
+#if LIBAVCODEC_VERSION_MAJOR >= 58
+ false;
+#else
+# if LIBAVCODEC_VERSION_MAJOR >= 55
+ mCodecID == AV_CODEC_ID_VP9 ||
+# endif
+ mCodecID == AV_CODEC_ID_VP8;
+#endif
+ }
+ gfx::YUVColorSpace GetFrameColorSpace() const;
+ gfx::ColorSpace2 GetFrameColorPrimaries() const;
+ gfx::ColorRange GetFrameColorRange() const;
+
+ MediaResult CreateImage(int64_t aOffset, int64_t aPts, int64_t aDuration,
+ MediaDataDecoder::DecodedData& aResults) const;
+
+ bool IsHardwareAccelerated(nsACString& aFailureReason) const override;
+ bool IsHardwareAccelerated() const {
+ nsAutoCString dummy;
+ return IsHardwareAccelerated(dummy);
+ }
+ void UpdateDecodeTimes(TimeStamp aDecodeStart);
+
+#if LIBAVCODEC_VERSION_MAJOR >= 57 && LIBAVUTIL_VERSION_MAJOR >= 56
+ layers::TextureClient* AllocateTextureClientForImage(
+ struct AVCodecContext* aCodecContext, layers::PlanarYCbCrImage* aImage);
+
+ gfx::IntSize GetAlignmentVideoFrameSize(struct AVCodecContext* aCodecContext,
+ int32_t aWidth,
+ int32_t aHeight) const;
+#endif
+
+#ifdef MOZ_WAYLAND_USE_HWDECODE
+ void InitHWDecodingPrefs();
+ MediaResult InitVAAPIDecoder();
+ bool CreateVAAPIDeviceContext();
+ void InitVAAPICodecContext();
+ AVCodec* FindVAAPICodec();
+ bool GetVAAPISurfaceDescriptor(VADRMPRIMESurfaceDescriptor* aVaDesc);
+ void AddAcceleratedFormats(nsTArray<AVCodecID>& aCodecList,
+ AVCodecID aCodecID, AVVAAPIHWConfig* hwconfig);
+ nsTArray<AVCodecID> GetAcceleratedFormats();
+ bool IsFormatAccelerated(AVCodecID aCodecID) const;
+
+ MediaResult CreateImageVAAPI(int64_t aOffset, int64_t aPts, int64_t aDuration,
+ MediaDataDecoder::DecodedData& aResults);
+#endif
+
+#ifdef MOZ_WAYLAND_USE_HWDECODE
+ AVBufferRef* mVAAPIDeviceContext;
+ bool mEnableHardwareDecoding;
+ VADisplay mDisplay;
+ UniquePtr<VideoFramePool<LIBAV_VER>> mVideoFramePool;
+ static nsTArray<AVCodecID> mAcceleratedFormats;
+#endif
+ RefPtr<KnowsCompositor> mImageAllocator;
+ RefPtr<ImageContainer> mImageContainer;
+ VideoInfo mInfo;
+ int mDecodedFrames;
+#if LIBAVCODEC_VERSION_MAJOR >= 58
+ int mDecodedFramesLate;
+ // Tracks when decode time of recent frame and averange decode time of
+ // previous frames is bigger than frame interval,
+ // i.e. we fail to decode in time.
+ // We switch to SW decode when we hit HW_DECODE_LATE_FRAMES treshold.
+ int mMissedDecodeInAverangeTime;
+#endif
+ float mAverangeDecodeTime;
+
+ class PtsCorrectionContext {
+ public:
+ PtsCorrectionContext();
+ int64_t GuessCorrectPts(int64_t aPts, int64_t aDts);
+ void Reset();
+ int64_t LastDts() const { return mLastDts; }
+
+ private:
+ int64_t mNumFaultyPts; /// Number of incorrect PTS values so far
+ int64_t mNumFaultyDts; /// Number of incorrect DTS values so far
+ int64_t mLastPts; /// PTS of the last frame
+ int64_t mLastDts; /// DTS of the last frame
+ };
+
+ PtsCorrectionContext mPtsContext;
+
+ DurationMap mDurationMap;
+ const bool mLowLatency;
+ const Maybe<TrackingId> mTrackingId;
+ PerformanceRecorderMulti<DecodeStage> mPerformanceRecorder;
+
+ // True if we're allocating shmem for ffmpeg decode buffer.
+ Maybe<Atomic<bool>> mIsUsingShmemBufferForDecode;
+
+#if LIBAVCODEC_VERSION_MAJOR >= 57 && LIBAVUTIL_VERSION_MAJOR >= 56
+ // These images are buffers for ffmpeg in order to store decoded data when
+ // using custom allocator for decoding. We want to explictly track all images
+ // we allocate to ensure that we won't leak any of them.
+ //
+ // All images tracked by mAllocatedImages are used by ffmpeg,
+ // i.e. ffmpeg holds a reference to them and uses them in
+ // its internal decoding queue.
+ //
+ // When an image is removed from mAllocatedImages it's recycled
+ // for a new frame by AllocateTextureClientForImage() in
+ // FFmpegVideoDecoder::GetVideoBuffer().
+ nsTHashSet<RefPtr<ImageBufferWrapper>> mAllocatedImages;
+#endif
+};
+
+#if LIBAVCODEC_VERSION_MAJOR >= 57 && LIBAVUTIL_VERSION_MAJOR >= 56
+class ImageBufferWrapper final {
+ public:
+ typedef mozilla::layers::Image Image;
+ typedef mozilla::layers::PlanarYCbCrImage PlanarYCbCrImage;
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ImageBufferWrapper)
+
+ ImageBufferWrapper(Image* aImage, void* aDecoder)
+ : mImage(aImage), mDecoder(aDecoder) {
+ MOZ_ASSERT(aImage);
+ MOZ_ASSERT(mDecoder);
+ }
+
+ Image* AsImage() { return mImage; }
+
+ void ReleaseBuffer() {
+ auto* decoder = static_cast<FFmpegVideoDecoder<LIBAV_VER>*>(mDecoder);
+ decoder->ReleaseAllocatedImage(this);
+ }
+
+ private:
+ ~ImageBufferWrapper() = default;
+ const RefPtr<Image> mImage;
+ void* const MOZ_NON_OWNING_REF mDecoder;
+};
+#endif
+
+} // namespace mozilla
+
+#endif // __FFmpegVideoDecoder_h__
diff --git a/dom/media/platforms/ffmpeg/FFmpegVideoFramePool.cpp b/dom/media/platforms/ffmpeg/FFmpegVideoFramePool.cpp
new file mode 100644
index 0000000000..5f179f26ab
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/FFmpegVideoFramePool.cpp
@@ -0,0 +1,414 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FFmpegVideoFramePool.h"
+#include "PlatformDecoderModule.h"
+#include "FFmpegLog.h"
+#include "mozilla/widget/DMABufLibWrapper.h"
+#include "libavutil/pixfmt.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/widget/va_drmcommon.h"
+
+// DMABufLibWrapper defines its own version of this which collides with the
+// official version in drm_fourcc.h
+#ifdef DRM_FORMAT_MOD_INVALID
+# undef DRM_FORMAT_MOD_INVALID
+#endif
+#include "drm_fourcc.h"
+
+#ifdef MOZ_LOGGING
+# undef DMABUF_LOG
+extern mozilla::LazyLogModule gDmabufLog;
+# define DMABUF_LOG(str, ...) \
+ MOZ_LOG(gDmabufLog, mozilla::LogLevel::Debug, (str, ##__VA_ARGS__))
+#else
+# define DMABUF_LOG(args)
+#endif /* MOZ_LOGGING */
+
+// Start copying surfaces when free ffmpeg surface count is below 1/4 of all
+// available surfaces.
+#define SURFACE_COPY_THRESHOLD (1.0f / 4.0f)
+
+namespace mozilla {
+
+RefPtr<layers::Image> VideoFrameSurface<LIBAV_VER>::GetAsImage() {
+ return new layers::DMABUFSurfaceImage(mSurface);
+}
+
+VideoFrameSurface<LIBAV_VER>::VideoFrameSurface(DMABufSurface* aSurface)
+ : mSurface(aSurface),
+ mLib(nullptr),
+ mAVHWFrameContext(nullptr),
+ mHWAVBuffer(nullptr) {
+ // Create global refcount object to track mSurface usage over
+ // gects rendering engine. We can't release it until it's used
+ // by GL compositor / WebRender.
+ MOZ_ASSERT(mSurface);
+ MOZ_RELEASE_ASSERT(mSurface->GetAsDMABufSurfaceYUV());
+ mSurface->GlobalRefCountCreate();
+ DMABUF_LOG("VideoFrameSurface: creating surface UID %d", mSurface->GetUID());
+}
+
+VideoFrameSurface<LIBAV_VER>::~VideoFrameSurface() {
+ DMABUF_LOG("~VideoFrameSurface: deleting dmabuf surface UID %d",
+ mSurface->GetUID());
+ mSurface->GlobalRefCountDelete();
+ // We're about to quit, no need to recycle the frames.
+ if (mFFMPEGSurfaceID) {
+ ReleaseVAAPIData(/* aForFrameRecycle */ false);
+ }
+}
+
+void VideoFrameSurface<LIBAV_VER>::LockVAAPIData(
+ AVCodecContext* aAVCodecContext, AVFrame* aAVFrame,
+ FFmpegLibWrapper* aLib) {
+ mLib = aLib;
+
+ // V4L2 frames don't have hw_frames_ctx because the v4l2-wrapper codecs
+ // don't actually use hwaccel. In this case we don't need to add a
+ // HW frame context reference
+ if (aAVCodecContext->hw_frames_ctx) {
+ mAVHWFrameContext = aLib->av_buffer_ref(aAVCodecContext->hw_frames_ctx);
+ mHWAVBuffer = aLib->av_buffer_ref(aAVFrame->buf[0]);
+ DMABUF_LOG(
+ "VideoFrameSurface: VAAPI locking dmabuf surface UID %d FFMPEG ID 0x%x "
+ "mAVHWFrameContext %p mHWAVBuffer %p",
+ mSurface->GetUID(), mFFMPEGSurfaceID.value(), mAVHWFrameContext,
+ mHWAVBuffer);
+ } else {
+ mAVHWFrameContext = nullptr;
+ mHWAVBuffer = aLib->av_buffer_ref(aAVFrame->buf[0]);
+ DMABUF_LOG(
+ "VideoFrameSurface: V4L2 locking dmabuf surface UID %d FFMPEG ID 0x%x "
+ "mHWAVBuffer %p",
+ mSurface->GetUID(), mFFMPEGSurfaceID.value(), mHWAVBuffer);
+ }
+}
+
+void VideoFrameSurface<LIBAV_VER>::ReleaseVAAPIData(bool aForFrameRecycle) {
+ DMABUF_LOG(
+ "VideoFrameSurface: VAAPI releasing dmabuf surface UID %d FFMPEG ID 0x%x "
+ "aForFrameRecycle %d mLib %p mAVHWFrameContext %p mHWAVBuffer %p",
+ mSurface->GetUID(), mFFMPEGSurfaceID.value(), aForFrameRecycle, mLib,
+ mAVHWFrameContext, mHWAVBuffer);
+ // It's possible to unref GPU data while IsUsed() is still set.
+ // It can happen when VideoFramePool is deleted while decoder shutdown
+ // but related dmabuf surfaces are still used in another process.
+ // In such case we don't care as the dmabuf surface will not be
+ // recycled for another frame and stays here untill last fd of it
+ // is closed.
+ if (mLib) {
+ mLib->av_buffer_unref(&mHWAVBuffer);
+ if (mAVHWFrameContext) {
+ mLib->av_buffer_unref(&mAVHWFrameContext);
+ }
+ mLib = nullptr;
+ }
+
+ mFFMPEGSurfaceID = Nothing();
+ mSurface->ReleaseSurface();
+
+ if (aForFrameRecycle && IsUsed()) {
+ NS_WARNING("VA-API: Reusing live dmabuf surface, visual glitches ahead");
+ }
+}
+
+VideoFramePool<LIBAV_VER>::VideoFramePool(int aFFMPEGPoolSize)
+ : mSurfaceLock("VideoFramePoolSurfaceLock"),
+ mFFMPEGPoolSize(aFFMPEGPoolSize) {
+ DMABUF_LOG("VideoFramePool::VideoFramePool() pool size %d", mFFMPEGPoolSize);
+}
+
+VideoFramePool<LIBAV_VER>::~VideoFramePool() {
+ MutexAutoLock lock(mSurfaceLock);
+ mDMABufSurfaces.Clear();
+}
+
+void VideoFramePool<LIBAV_VER>::ReleaseUnusedVAAPIFrames() {
+ MutexAutoLock lock(mSurfaceLock);
+ for (const auto& surface : mDMABufSurfaces) {
+#ifdef DEBUG
+ if (!surface->mFFMPEGSurfaceID && surface->IsUsed()) {
+ NS_WARNING("VA-API: Untracked but still used dmabug surface!");
+ }
+#endif
+ if (surface->mFFMPEGSurfaceID && !surface->IsUsed()) {
+ surface->ReleaseVAAPIData();
+ }
+ }
+}
+
+RefPtr<VideoFrameSurface<LIBAV_VER>>
+VideoFramePool<LIBAV_VER>::GetFreeVideoFrameSurface() {
+ for (auto& surface : mDMABufSurfaces) {
+ if (!surface->mFFMPEGSurfaceID) {
+ return surface;
+ }
+ if (surface->IsUsed()) {
+ continue;
+ }
+ surface->ReleaseVAAPIData();
+ return surface;
+ }
+ return nullptr;
+}
+
+void VideoFramePool<LIBAV_VER>::CheckNewFFMPEGSurface(
+ VASurfaceID aNewSurfaceID) {
+ for (const auto& surface : mDMABufSurfaces) {
+ if (surface->IsUsed() && surface->IsFFMPEGSurface()) {
+ MOZ_DIAGNOSTIC_ASSERT(surface->mFFMPEGSurfaceID.value() != aNewSurfaceID);
+ }
+ }
+}
+
+bool VideoFramePool<LIBAV_VER>::ShouldCopySurface() {
+ // Number of used HW surfaces.
+ int surfacesUsed = 0;
+ int surfacesUsedFFmpeg = 0;
+ for (const auto& surface : mDMABufSurfaces) {
+ if (surface->IsUsed()) {
+ surfacesUsed++;
+ if (surface->IsFFMPEGSurface()) {
+ DMABUF_LOG(
+ "Used HW surface UID %d FFMPEG ID 0x%x\n",
+ surface->mSurface->GetUID(),
+ surface->mFFMPEGSurfaceID ? surface->mFFMPEGSurfaceID.value() : -1);
+ surfacesUsedFFmpeg++;
+ }
+ }
+ }
+ float freeRatio = 1.0f - (surfacesUsedFFmpeg / (float)mFFMPEGPoolSize);
+ DMABUF_LOG(
+ "Surface pool size %d used copied %d used ffmpeg %d (max %d) free ratio "
+ "%f",
+ (int)mDMABufSurfaces.Length(), surfacesUsed - surfacesUsedFFmpeg,
+ surfacesUsedFFmpeg, mFFMPEGPoolSize, freeRatio);
+ if (!gfx::gfxVars::HwDecodedVideoZeroCopy()) {
+ return true;
+ }
+ return freeRatio < SURFACE_COPY_THRESHOLD;
+}
+
+RefPtr<VideoFrameSurface<LIBAV_VER>>
+VideoFramePool<LIBAV_VER>::GetVideoFrameSurface(
+ VADRMPRIMESurfaceDescriptor& aVaDesc, int aWidth, int aHeight,
+ AVCodecContext* aAVCodecContext, AVFrame* aAVFrame,
+ FFmpegLibWrapper* aLib) {
+ if (aVaDesc.fourcc != VA_FOURCC_NV12 && aVaDesc.fourcc != VA_FOURCC_YV12 &&
+ aVaDesc.fourcc != VA_FOURCC_P010) {
+ DMABUF_LOG("Unsupported VA-API surface format %d", aVaDesc.fourcc);
+ return nullptr;
+ }
+
+ MutexAutoLock lock(mSurfaceLock);
+
+ RefPtr<DMABufSurfaceYUV> surface;
+ RefPtr<VideoFrameSurface<LIBAV_VER>> videoSurface =
+ GetFreeVideoFrameSurface();
+ if (!videoSurface) {
+ surface = new DMABufSurfaceYUV();
+ videoSurface = new VideoFrameSurface<LIBAV_VER>(surface);
+ mDMABufSurfaces.AppendElement(videoSurface);
+ } else {
+ surface = videoSurface->GetDMABufSurface();
+ }
+ VASurfaceID ffmpegSurfaceID = (uintptr_t)aAVFrame->data[3];
+ DMABUF_LOG("Using VA-API DMABufSurface UID %d FFMPEG ID 0x%x",
+ surface->GetUID(), ffmpegSurfaceID);
+
+ bool copySurface = mTextureCopyWorks && ShouldCopySurface();
+ if (!surface->UpdateYUVData(aVaDesc, aWidth, aHeight, copySurface)) {
+ if (!copySurface) {
+ // Failed without texture copy. We can't do more here.
+ return nullptr;
+ }
+ // Try again without texture copy
+ DMABUF_LOG(" DMABuf texture copy is broken");
+ copySurface = mTextureCopyWorks = false;
+ if (!surface->UpdateYUVData(aVaDesc, aWidth, aHeight, copySurface)) {
+ return nullptr;
+ }
+ }
+
+ if (MOZ_UNLIKELY(!mTextureCreationWorks)) {
+ mTextureCreationWorks = Some(surface->VerifyTextureCreation());
+ if (!*mTextureCreationWorks) {
+ DMABUF_LOG(" failed to create texture over DMABuf memory!");
+ return nullptr;
+ }
+ }
+
+ videoSurface->MarkAsUsed(ffmpegSurfaceID);
+
+ if (!copySurface) {
+ // Check that newly added ffmpeg surface isn't already used by different
+ // VideoFrameSurface.
+ CheckNewFFMPEGSurface(ffmpegSurfaceID);
+ videoSurface->LockVAAPIData(aAVCodecContext, aAVFrame, aLib);
+ }
+ return videoSurface;
+}
+
+// Convert an FFmpeg-specific DRM descriptor into a
+// VADRMPRIMESurfaceDescriptor. There is no fundamental difference between
+// the descriptor structs and using the latter means this can use all the
+// existing machinery in DMABufSurfaceYUV.
+static Maybe<VADRMPRIMESurfaceDescriptor> FFmpegDescToVA(
+ AVDRMFrameDescriptor& aDesc, AVFrame* aAVFrame) {
+ VADRMPRIMESurfaceDescriptor vaDesc{};
+
+ if (aAVFrame->format != AV_PIX_FMT_DRM_PRIME) {
+ DMABUF_LOG("Got non-DRM-PRIME frame from FFmpeg V4L2");
+ return Nothing();
+ }
+
+ if (aAVFrame->crop_top != 0 || aAVFrame->crop_left != 0) {
+ DMABUF_LOG("Top and left-side cropping are not supported");
+ return Nothing();
+ }
+
+ // Width and height after crop
+ vaDesc.width = aAVFrame->width;
+ vaDesc.height = aAVFrame->height - aAVFrame->crop_bottom;
+
+ // Native width and height before crop is applied
+ unsigned int uncrop_width = aDesc.layers[0].planes[0].pitch;
+ unsigned int uncrop_height = aAVFrame->height;
+
+ unsigned int offset = aDesc.layers[0].planes[0].offset;
+
+ if (aDesc.layers[0].format == DRM_FORMAT_YUV420) {
+ vaDesc.fourcc = VA_FOURCC_YV12;
+
+ // V4L2 expresses YUV420 as a single contiguous buffer containing
+ // all three planes. DMABufSurfaceYUV expects the three planes
+ // separately, so we have to split them out
+ MOZ_ASSERT(aDesc.nb_objects == 1);
+ MOZ_ASSERT(aDesc.nb_layers == 1);
+
+ vaDesc.num_objects = 1;
+ vaDesc.objects[0].drm_format_modifier = aDesc.objects[0].format_modifier;
+ vaDesc.objects[0].size = aDesc.objects[0].size;
+ vaDesc.objects[0].fd = aDesc.objects[0].fd;
+
+ vaDesc.num_layers = 3;
+ for (int i = 0; i < 3; i++) {
+ vaDesc.layers[i].drm_format = DRM_FORMAT_R8;
+ vaDesc.layers[i].num_planes = 1;
+ vaDesc.layers[i].object_index[0] = 0;
+ }
+ vaDesc.layers[0].offset[0] = offset;
+ vaDesc.layers[0].pitch[0] = uncrop_width;
+ vaDesc.layers[1].offset[0] = offset + uncrop_width * uncrop_height;
+ vaDesc.layers[1].pitch[0] = uncrop_width / 2;
+ vaDesc.layers[2].offset[0] = offset + uncrop_width * uncrop_height * 5 / 4;
+ vaDesc.layers[2].pitch[0] = uncrop_width / 2;
+ } else if (aDesc.layers[0].format == DRM_FORMAT_NV12) {
+ vaDesc.fourcc = VA_FOURCC_NV12;
+
+ // V4L2 expresses NV12 as a single contiguous buffer containing both
+ // planes. DMABufSurfaceYUV expects the two planes separately, so we have
+ // to split them out
+ MOZ_ASSERT(aDesc.nb_objects == 1);
+ MOZ_ASSERT(aDesc.nb_layers == 1);
+
+ vaDesc.num_objects = 1;
+ vaDesc.objects[0].drm_format_modifier = aDesc.objects[0].format_modifier;
+ vaDesc.objects[0].size = aDesc.objects[0].size;
+ vaDesc.objects[0].fd = aDesc.objects[0].fd;
+
+ vaDesc.num_layers = 2;
+ for (int i = 0; i < 2; i++) {
+ vaDesc.layers[i].num_planes = 1;
+ vaDesc.layers[i].object_index[0] = 0;
+ vaDesc.layers[i].pitch[0] = uncrop_width;
+ }
+ vaDesc.layers[0].drm_format = DRM_FORMAT_R8; // Y plane
+ vaDesc.layers[0].offset[0] = offset;
+ vaDesc.layers[1].drm_format = DRM_FORMAT_GR88; // UV plane
+ vaDesc.layers[1].offset[0] = offset + uncrop_width * uncrop_height;
+ } else {
+ DMABUF_LOG("Don't know how to deal with FOURCC 0x%x",
+ aDesc.layers[0].format);
+ return Nothing();
+ }
+
+ return Some(vaDesc);
+}
+
+RefPtr<VideoFrameSurface<LIBAV_VER>>
+VideoFramePool<LIBAV_VER>::GetVideoFrameSurface(AVDRMFrameDescriptor& aDesc,
+ int aWidth, int aHeight,
+ AVCodecContext* aAVCodecContext,
+ AVFrame* aAVFrame,
+ FFmpegLibWrapper* aLib) {
+ MOZ_ASSERT(aDesc.nb_layers > 0);
+
+ auto layerDesc = FFmpegDescToVA(aDesc, aAVFrame);
+ if (layerDesc.isNothing()) {
+ return nullptr;
+ }
+
+ // Width and height, after cropping
+ int crop_width = (int)layerDesc->width;
+ int crop_height = (int)layerDesc->height;
+
+ // Use the descriptor's address as an id to track ffmpeg surfaces
+ unsigned int ffmpegSurfaceID = (uintptr_t)&aDesc;
+
+ MutexAutoLock lock(mSurfaceLock);
+
+ RefPtr<DMABufSurfaceYUV> surface;
+ RefPtr<VideoFrameSurface<LIBAV_VER>> videoSurface =
+ GetFreeVideoFrameSurface();
+ if (!videoSurface) {
+ surface = new DMABufSurfaceYUV();
+ videoSurface = new VideoFrameSurface<LIBAV_VER>(surface);
+ mDMABufSurfaces.AppendElement(videoSurface);
+ } else {
+ surface = videoSurface->GetDMABufSurface();
+ }
+ DMABUF_LOG("Using V4L2 DMABufSurface UID %d FFMPEG ID 0x%x",
+ surface->GetUID(), ffmpegSurfaceID);
+
+ bool copySurface = mTextureCopyWorks && ShouldCopySurface();
+ if (!surface->UpdateYUVData(layerDesc.value(), crop_width, crop_height,
+ copySurface)) {
+ if (!copySurface) {
+ // Failed without texture copy. We can't do more here.
+ return nullptr;
+ }
+ // Try again without texture copy
+ DMABUF_LOG(" DMABuf texture copy is broken");
+ copySurface = mTextureCopyWorks = false;
+ if (!surface->UpdateYUVData(layerDesc.value(), crop_width, crop_height,
+ copySurface)) {
+ return nullptr;
+ }
+ }
+
+ if (MOZ_UNLIKELY(!mTextureCreationWorks)) {
+ mTextureCreationWorks = Some(surface->VerifyTextureCreation());
+ if (!*mTextureCreationWorks) {
+ DMABUF_LOG(" failed to create texture over DMABuf memory!");
+ return nullptr;
+ }
+ }
+
+ videoSurface->MarkAsUsed(ffmpegSurfaceID);
+
+ if (!copySurface) {
+ // Check that newly added ffmpeg surface isn't already used by different
+ // VideoFrameSurface.
+ CheckNewFFMPEGSurface(ffmpegSurfaceID);
+ videoSurface->LockVAAPIData(aAVCodecContext, aAVFrame, aLib);
+ }
+ return videoSurface;
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/ffmpeg/FFmpegVideoFramePool.h b/dom/media/platforms/ffmpeg/FFmpegVideoFramePool.h
new file mode 100644
index 0000000000..baf89c504d
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/FFmpegVideoFramePool.h
@@ -0,0 +1,155 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef __FFmpegVideoFramePool_h__
+#define __FFmpegVideoFramePool_h__
+
+#include "FFmpegLibWrapper.h"
+#include "FFmpegLibs.h"
+#include "FFmpegLog.h"
+
+#include "mozilla/layers/DMABUFSurfaceImage.h"
+#include "mozilla/widget/DMABufLibWrapper.h"
+#include "mozilla/widget/DMABufSurface.h"
+
+namespace mozilla {
+
+// VideoFrameSurface holds a reference to GPU data with a video frame.
+//
+// Actual GPU pixel data are stored at DMABufSurface and
+// DMABufSurface is passed to gecko GL rendering pipeline via.
+// DMABUFSurfaceImage.
+//
+// VideoFrameSurface can optionally hold VA-API ffmpeg related data to keep
+// GPU data locked untill we need them.
+//
+// VideoFrameSurface is used for both HW accelerated video decoding
+// (VA-API) and ffmpeg SW decoding.
+//
+// VA-API scenario
+//
+// When VA-API decoding is running, ffmpeg allocates AVHWFramesContext - a pool
+// of "hardware" frames. Every "hardware" frame (VASurface) is backed
+// by actual piece of GPU memory which holds the decoded image data.
+//
+// The VASurface is wrapped by DMABufSurface and transferred to
+// rendering queue by DMABUFSurfaceImage, where TextureClient is
+// created and VASurface is used as a texture there.
+//
+// As there's a limited number of VASurfaces, ffmpeg reuses them to decode
+// next frames ASAP even if they are still attached to DMABufSurface
+// and used as a texture in our rendering engine.
+//
+// Unfortunately there isn't any obvious way how to mark particular VASurface
+// as used. The best we can do is to hold a reference to particular AVBuffer
+// from decoded AVFrame and AVHWFramesContext which owns the AVBuffer.
+template <int V>
+class VideoFrameSurface {};
+template <>
+class VideoFrameSurface<LIBAV_VER>;
+
+template <int V>
+class VideoFramePool {};
+template <>
+class VideoFramePool<LIBAV_VER>;
+
+template <>
+class VideoFrameSurface<LIBAV_VER> {
+ friend class VideoFramePool<LIBAV_VER>;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(VideoFrameSurface)
+
+ explicit VideoFrameSurface(DMABufSurface* aSurface);
+
+ void SetYUVColorSpace(mozilla::gfx::YUVColorSpace aColorSpace) {
+ mSurface->GetAsDMABufSurfaceYUV()->SetYUVColorSpace(aColorSpace);
+ }
+ void SetColorRange(mozilla::gfx::ColorRange aColorRange) {
+ mSurface->GetAsDMABufSurfaceYUV()->SetColorRange(aColorRange);
+ }
+
+ RefPtr<DMABufSurfaceYUV> GetDMABufSurface() {
+ return mSurface->GetAsDMABufSurfaceYUV();
+ };
+
+ RefPtr<layers::Image> GetAsImage();
+
+ // Don't allow VideoFrameSurface plain copy as it leads to
+ // unexpected DMABufSurface/HW buffer releases and we don't want to
+ // deep copy them.
+ VideoFrameSurface(const VideoFrameSurface&) = delete;
+ const VideoFrameSurface& operator=(VideoFrameSurface const&) = delete;
+
+ protected:
+ // Lock VAAPI related data
+ void LockVAAPIData(AVCodecContext* aAVCodecContext, AVFrame* aAVFrame,
+ FFmpegLibWrapper* aLib);
+ // Release VAAPI related data, DMABufSurface can be reused
+ // for another frame.
+ void ReleaseVAAPIData(bool aForFrameRecycle = true);
+
+ // Check if DMABufSurface is used by any gecko rendering process
+ // (WebRender or GL compositor) or by DMABUFSurfaceImage/VideoData.
+ bool IsUsed() const { return mSurface->IsGlobalRefSet(); }
+
+ // Surface points to dmabuf memmory owned by ffmpeg.
+ bool IsFFMPEGSurface() const { return !!mLib; }
+
+ void MarkAsUsed(VASurfaceID aFFMPEGSurfaceID) {
+ mFFMPEGSurfaceID = Some(aFFMPEGSurfaceID);
+ }
+
+ private:
+ virtual ~VideoFrameSurface();
+
+ const RefPtr<DMABufSurface> mSurface;
+ const FFmpegLibWrapper* mLib;
+ AVBufferRef* mAVHWFrameContext;
+ AVBufferRef* mHWAVBuffer;
+ Maybe<VASurfaceID> mFFMPEGSurfaceID;
+};
+
+// VideoFramePool class is thread-safe.
+template <>
+class VideoFramePool<LIBAV_VER> {
+ public:
+ explicit VideoFramePool(int aFFMPEGPoolSize);
+ ~VideoFramePool();
+
+ RefPtr<VideoFrameSurface<LIBAV_VER>> GetVideoFrameSurface(
+ VADRMPRIMESurfaceDescriptor& aVaDesc, int aWidth, int aHeight,
+ AVCodecContext* aAVCodecContext, AVFrame* aAVFrame,
+ FFmpegLibWrapper* aLib);
+ RefPtr<VideoFrameSurface<LIBAV_VER>> GetVideoFrameSurface(
+ AVDRMFrameDescriptor& aDesc, int aWidth, int aHeight,
+ AVCodecContext* aAVCodecContext, AVFrame* aAVFrame,
+ FFmpegLibWrapper* aLib);
+
+ void ReleaseUnusedVAAPIFrames();
+
+ private:
+ RefPtr<VideoFrameSurface<LIBAV_VER>> GetFreeVideoFrameSurface();
+ bool ShouldCopySurface();
+ void CheckNewFFMPEGSurface(VASurfaceID aNewSurfaceID);
+
+ private:
+ // Protect mDMABufSurfaces pool access
+ Mutex mSurfaceLock MOZ_UNANNOTATED;
+ nsTArray<RefPtr<VideoFrameSurface<LIBAV_VER>>> mDMABufSurfaces;
+ // Number of dmabuf surfaces allocated by ffmpeg for decoded video frames.
+ // Can be adjusted by extra_hw_frames at InitVAAPICodecContext().
+ int mFFMPEGPoolSize;
+ // We may fail to create texture over DMABuf memory due to driver bugs so
+ // check that before we export first DMABuf video frame.
+ Maybe<bool> mTextureCreationWorks;
+ // We may fail to copy DMABuf memory on NVIDIA drivers.
+ bool mTextureCopyWorks = true;
+};
+
+} // namespace mozilla
+
+#endif // __FFmpegVideoFramePool_h__
diff --git a/dom/media/platforms/ffmpeg/README_mozilla b/dom/media/platforms/ffmpeg/README_mozilla
new file mode 100644
index 0000000000..19761f7514
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/README_mozilla
@@ -0,0 +1,11 @@
+These headers are taken from Libav versions 0.8.9, 0.9 and 1.0.
+
+These headers are licensed under the GNU Lesser General Public License version
+2.1. For more information see the file COPYING.LGPLv2.1
+
+While the function ABIs of FFmpeg/Libav tend to be stable between even major
+versions, the class layouts can change considerably. This can lead to major
+crashes if we build against the wrong headers. We include these headers to make
+sure we have the same version every time we build, and to ease the pain of
+acquiring the correct libraries for a build, especially for those distributions
+who oppose distributing such packages.
diff --git a/dom/media/platforms/ffmpeg/ffmpeg57/include/COPYING.LGPLv2.1 b/dom/media/platforms/ffmpeg/ffmpeg57/include/COPYING.LGPLv2.1
new file mode 100644
index 0000000000..00b4fedfe7
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg57/include/COPYING.LGPLv2.1
@@ -0,0 +1,504 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the library's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+ <signature of Ty Coon>, 1 April 1990
+ Ty Coon, President of Vice
+
+That's all there is to it!
+
+
diff --git a/dom/media/platforms/ffmpeg/ffmpeg57/include/libavcodec/avcodec.h b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavcodec/avcodec.h
new file mode 100644
index 0000000000..f365775f0b
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavcodec/avcodec.h
@@ -0,0 +1,5418 @@
+/*
+ * copyright (c) 2001 Fabrice Bellard
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_AVCODEC_H
+#define AVCODEC_AVCODEC_H
+
+/**
+ * @file
+ * @ingroup libavc
+ * Libavcodec external API header
+ */
+
+#include <errno.h>
+#include "libavutil/samplefmt.h"
+#include "libavutil/attributes.h"
+#include "libavutil/avutil.h"
+#include "libavutil/buffer.h"
+#include "libavutil/cpu.h"
+#include "libavutil/channel_layout.h"
+#include "libavutil/dict.h"
+#include "libavutil/frame.h"
+#include "libavutil/log.h"
+#include "libavutil/pixfmt.h"
+#include "libavutil/rational.h"
+
+#include "version.h"
+
+/**
+ * @defgroup libavc Encoding/Decoding Library
+ * @{
+ *
+ * @defgroup lavc_decoding Decoding
+ * @{
+ * @}
+ *
+ * @defgroup lavc_encoding Encoding
+ * @{
+ * @}
+ *
+ * @defgroup lavc_codec Codecs
+ * @{
+ * @defgroup lavc_codec_native Native Codecs
+ * @{
+ * @}
+ * @defgroup lavc_codec_wrappers External library wrappers
+ * @{
+ * @}
+ * @defgroup lavc_codec_hwaccel Hardware Accelerators bridge
+ * @{
+ * @}
+ * @}
+ * @defgroup lavc_internal Internal
+ * @{
+ * @}
+ * @}
+ *
+ */
+
+/**
+ * @defgroup lavc_core Core functions/structures.
+ * @ingroup libavc
+ *
+ * Basic definitions, functions for querying libavcodec capabilities,
+ * allocating core structures, etc.
+ * @{
+ */
+
+
+/**
+ * Identify the syntax and semantics of the bitstream.
+ * The principle is roughly:
+ * Two decoders with the same ID can decode the same streams.
+ * Two encoders with the same ID can encode compatible streams.
+ * There may be slight deviations from the principle due to implementation
+ * details.
+ *
+ * If you add a codec ID to this list, add it so that
+ * 1. no value of a existing codec ID changes (that would break ABI),
+ * 2. it is as close as possible to similar codecs
+ *
+ * After adding new codec IDs, do not forget to add an entry to the codec
+ * descriptor list and bump libavcodec minor version.
+ */
+enum AVCodecID {
+ AV_CODEC_ID_NONE,
+
+ /* video codecs */
+ AV_CODEC_ID_MPEG1VIDEO,
+ AV_CODEC_ID_MPEG2VIDEO, ///< preferred ID for MPEG-1/2 video decoding
+#if FF_API_XVMC
+ AV_CODEC_ID_MPEG2VIDEO_XVMC,
+#endif /* FF_API_XVMC */
+ AV_CODEC_ID_H261,
+ AV_CODEC_ID_H263,
+ AV_CODEC_ID_RV10,
+ AV_CODEC_ID_RV20,
+ AV_CODEC_ID_MJPEG,
+ AV_CODEC_ID_MJPEGB,
+ AV_CODEC_ID_LJPEG,
+ AV_CODEC_ID_SP5X,
+ AV_CODEC_ID_JPEGLS,
+ AV_CODEC_ID_MPEG4,
+ AV_CODEC_ID_RAWVIDEO,
+ AV_CODEC_ID_MSMPEG4V1,
+ AV_CODEC_ID_MSMPEG4V2,
+ AV_CODEC_ID_MSMPEG4V3,
+ AV_CODEC_ID_WMV1,
+ AV_CODEC_ID_WMV2,
+ AV_CODEC_ID_H263P,
+ AV_CODEC_ID_H263I,
+ AV_CODEC_ID_FLV1,
+ AV_CODEC_ID_SVQ1,
+ AV_CODEC_ID_SVQ3,
+ AV_CODEC_ID_DVVIDEO,
+ AV_CODEC_ID_HUFFYUV,
+ AV_CODEC_ID_CYUV,
+ AV_CODEC_ID_H264,
+ AV_CODEC_ID_INDEO3,
+ AV_CODEC_ID_VP3,
+ AV_CODEC_ID_THEORA,
+ AV_CODEC_ID_ASV1,
+ AV_CODEC_ID_ASV2,
+ AV_CODEC_ID_FFV1,
+ AV_CODEC_ID_4XM,
+ AV_CODEC_ID_VCR1,
+ AV_CODEC_ID_CLJR,
+ AV_CODEC_ID_MDEC,
+ AV_CODEC_ID_ROQ,
+ AV_CODEC_ID_INTERPLAY_VIDEO,
+ AV_CODEC_ID_XAN_WC3,
+ AV_CODEC_ID_XAN_WC4,
+ AV_CODEC_ID_RPZA,
+ AV_CODEC_ID_CINEPAK,
+ AV_CODEC_ID_WS_VQA,
+ AV_CODEC_ID_MSRLE,
+ AV_CODEC_ID_MSVIDEO1,
+ AV_CODEC_ID_IDCIN,
+ AV_CODEC_ID_8BPS,
+ AV_CODEC_ID_SMC,
+ AV_CODEC_ID_FLIC,
+ AV_CODEC_ID_TRUEMOTION1,
+ AV_CODEC_ID_VMDVIDEO,
+ AV_CODEC_ID_MSZH,
+ AV_CODEC_ID_ZLIB,
+ AV_CODEC_ID_QTRLE,
+ AV_CODEC_ID_TSCC,
+ AV_CODEC_ID_ULTI,
+ AV_CODEC_ID_QDRAW,
+ AV_CODEC_ID_VIXL,
+ AV_CODEC_ID_QPEG,
+ AV_CODEC_ID_PNG,
+ AV_CODEC_ID_PPM,
+ AV_CODEC_ID_PBM,
+ AV_CODEC_ID_PGM,
+ AV_CODEC_ID_PGMYUV,
+ AV_CODEC_ID_PAM,
+ AV_CODEC_ID_FFVHUFF,
+ AV_CODEC_ID_RV30,
+ AV_CODEC_ID_RV40,
+ AV_CODEC_ID_VC1,
+ AV_CODEC_ID_WMV3,
+ AV_CODEC_ID_LOCO,
+ AV_CODEC_ID_WNV1,
+ AV_CODEC_ID_AASC,
+ AV_CODEC_ID_INDEO2,
+ AV_CODEC_ID_FRAPS,
+ AV_CODEC_ID_TRUEMOTION2,
+ AV_CODEC_ID_BMP,
+ AV_CODEC_ID_CSCD,
+ AV_CODEC_ID_MMVIDEO,
+ AV_CODEC_ID_ZMBV,
+ AV_CODEC_ID_AVS,
+ AV_CODEC_ID_SMACKVIDEO,
+ AV_CODEC_ID_NUV,
+ AV_CODEC_ID_KMVC,
+ AV_CODEC_ID_FLASHSV,
+ AV_CODEC_ID_CAVS,
+ AV_CODEC_ID_JPEG2000,
+ AV_CODEC_ID_VMNC,
+ AV_CODEC_ID_VP5,
+ AV_CODEC_ID_VP6,
+ AV_CODEC_ID_VP6F,
+ AV_CODEC_ID_TARGA,
+ AV_CODEC_ID_DSICINVIDEO,
+ AV_CODEC_ID_TIERTEXSEQVIDEO,
+ AV_CODEC_ID_TIFF,
+ AV_CODEC_ID_GIF,
+ AV_CODEC_ID_DXA,
+ AV_CODEC_ID_DNXHD,
+ AV_CODEC_ID_THP,
+ AV_CODEC_ID_SGI,
+ AV_CODEC_ID_C93,
+ AV_CODEC_ID_BETHSOFTVID,
+ AV_CODEC_ID_PTX,
+ AV_CODEC_ID_TXD,
+ AV_CODEC_ID_VP6A,
+ AV_CODEC_ID_AMV,
+ AV_CODEC_ID_VB,
+ AV_CODEC_ID_PCX,
+ AV_CODEC_ID_SUNRAST,
+ AV_CODEC_ID_INDEO4,
+ AV_CODEC_ID_INDEO5,
+ AV_CODEC_ID_MIMIC,
+ AV_CODEC_ID_RL2,
+ AV_CODEC_ID_ESCAPE124,
+ AV_CODEC_ID_DIRAC,
+ AV_CODEC_ID_BFI,
+ AV_CODEC_ID_CMV,
+ AV_CODEC_ID_MOTIONPIXELS,
+ AV_CODEC_ID_TGV,
+ AV_CODEC_ID_TGQ,
+ AV_CODEC_ID_TQI,
+ AV_CODEC_ID_AURA,
+ AV_CODEC_ID_AURA2,
+ AV_CODEC_ID_V210X,
+ AV_CODEC_ID_TMV,
+ AV_CODEC_ID_V210,
+ AV_CODEC_ID_DPX,
+ AV_CODEC_ID_MAD,
+ AV_CODEC_ID_FRWU,
+ AV_CODEC_ID_FLASHSV2,
+ AV_CODEC_ID_CDGRAPHICS,
+ AV_CODEC_ID_R210,
+ AV_CODEC_ID_ANM,
+ AV_CODEC_ID_BINKVIDEO,
+ AV_CODEC_ID_IFF_ILBM,
+#define AV_CODEC_ID_IFF_BYTERUN1 AV_CODEC_ID_IFF_ILBM
+ AV_CODEC_ID_KGV1,
+ AV_CODEC_ID_YOP,
+ AV_CODEC_ID_VP8,
+ AV_CODEC_ID_PICTOR,
+ AV_CODEC_ID_ANSI,
+ AV_CODEC_ID_A64_MULTI,
+ AV_CODEC_ID_A64_MULTI5,
+ AV_CODEC_ID_R10K,
+ AV_CODEC_ID_MXPEG,
+ AV_CODEC_ID_LAGARITH,
+ AV_CODEC_ID_PRORES,
+ AV_CODEC_ID_JV,
+ AV_CODEC_ID_DFA,
+ AV_CODEC_ID_WMV3IMAGE,
+ AV_CODEC_ID_VC1IMAGE,
+ AV_CODEC_ID_UTVIDEO,
+ AV_CODEC_ID_BMV_VIDEO,
+ AV_CODEC_ID_VBLE,
+ AV_CODEC_ID_DXTORY,
+ AV_CODEC_ID_V410,
+ AV_CODEC_ID_XWD,
+ AV_CODEC_ID_CDXL,
+ AV_CODEC_ID_XBM,
+ AV_CODEC_ID_ZEROCODEC,
+ AV_CODEC_ID_MSS1,
+ AV_CODEC_ID_MSA1,
+ AV_CODEC_ID_TSCC2,
+ AV_CODEC_ID_MTS2,
+ AV_CODEC_ID_CLLC,
+ AV_CODEC_ID_MSS2,
+ AV_CODEC_ID_VP9,
+ AV_CODEC_ID_AIC,
+ AV_CODEC_ID_ESCAPE130,
+ AV_CODEC_ID_G2M,
+ AV_CODEC_ID_WEBP,
+ AV_CODEC_ID_HNM4_VIDEO,
+ AV_CODEC_ID_HEVC,
+#define AV_CODEC_ID_H265 AV_CODEC_ID_HEVC
+ AV_CODEC_ID_FIC,
+ AV_CODEC_ID_ALIAS_PIX,
+ AV_CODEC_ID_BRENDER_PIX,
+ AV_CODEC_ID_PAF_VIDEO,
+ AV_CODEC_ID_EXR,
+ AV_CODEC_ID_VP7,
+ AV_CODEC_ID_SANM,
+ AV_CODEC_ID_SGIRLE,
+ AV_CODEC_ID_MVC1,
+ AV_CODEC_ID_MVC2,
+ AV_CODEC_ID_HQX,
+ AV_CODEC_ID_TDSC,
+ AV_CODEC_ID_HQ_HQA,
+ AV_CODEC_ID_HAP,
+ AV_CODEC_ID_DDS,
+ AV_CODEC_ID_DXV,
+ AV_CODEC_ID_SCREENPRESSO,
+ AV_CODEC_ID_RSCC,
+
+ AV_CODEC_ID_Y41P = 0x8000,
+ AV_CODEC_ID_AVRP,
+ AV_CODEC_ID_012V,
+ AV_CODEC_ID_AVUI,
+ AV_CODEC_ID_AYUV,
+ AV_CODEC_ID_TARGA_Y216,
+ AV_CODEC_ID_V308,
+ AV_CODEC_ID_V408,
+ AV_CODEC_ID_YUV4,
+ AV_CODEC_ID_AVRN,
+ AV_CODEC_ID_CPIA,
+ AV_CODEC_ID_XFACE,
+ AV_CODEC_ID_SNOW,
+ AV_CODEC_ID_SMVJPEG,
+ AV_CODEC_ID_APNG,
+ AV_CODEC_ID_DAALA,
+
+ /* various PCM "codecs" */
+ AV_CODEC_ID_FIRST_AUDIO = 0x10000, ///< A dummy id pointing at the start of audio codecs
+ AV_CODEC_ID_PCM_S16LE = 0x10000,
+ AV_CODEC_ID_PCM_S16BE,
+ AV_CODEC_ID_PCM_U16LE,
+ AV_CODEC_ID_PCM_U16BE,
+ AV_CODEC_ID_PCM_S8,
+ AV_CODEC_ID_PCM_U8,
+ AV_CODEC_ID_PCM_MULAW,
+ AV_CODEC_ID_PCM_ALAW,
+ AV_CODEC_ID_PCM_S32LE,
+ AV_CODEC_ID_PCM_S32BE,
+ AV_CODEC_ID_PCM_U32LE,
+ AV_CODEC_ID_PCM_U32BE,
+ AV_CODEC_ID_PCM_S24LE,
+ AV_CODEC_ID_PCM_S24BE,
+ AV_CODEC_ID_PCM_U24LE,
+ AV_CODEC_ID_PCM_U24BE,
+ AV_CODEC_ID_PCM_S24DAUD,
+ AV_CODEC_ID_PCM_ZORK,
+ AV_CODEC_ID_PCM_S16LE_PLANAR,
+ AV_CODEC_ID_PCM_DVD,
+ AV_CODEC_ID_PCM_F32BE,
+ AV_CODEC_ID_PCM_F32LE,
+ AV_CODEC_ID_PCM_F64BE,
+ AV_CODEC_ID_PCM_F64LE,
+ AV_CODEC_ID_PCM_BLURAY,
+ AV_CODEC_ID_PCM_LXF,
+ AV_CODEC_ID_S302M,
+ AV_CODEC_ID_PCM_S8_PLANAR,
+ AV_CODEC_ID_PCM_S24LE_PLANAR,
+ AV_CODEC_ID_PCM_S32LE_PLANAR,
+ AV_CODEC_ID_PCM_S16BE_PLANAR,
+ /* new PCM "codecs" should be added right below this line starting with
+ * an explicit value of for example 0x10800
+ */
+
+ /* various ADPCM codecs */
+ AV_CODEC_ID_ADPCM_IMA_QT = 0x11000,
+ AV_CODEC_ID_ADPCM_IMA_WAV,
+ AV_CODEC_ID_ADPCM_IMA_DK3,
+ AV_CODEC_ID_ADPCM_IMA_DK4,
+ AV_CODEC_ID_ADPCM_IMA_WS,
+ AV_CODEC_ID_ADPCM_IMA_SMJPEG,
+ AV_CODEC_ID_ADPCM_MS,
+ AV_CODEC_ID_ADPCM_4XM,
+ AV_CODEC_ID_ADPCM_XA,
+ AV_CODEC_ID_ADPCM_ADX,
+ AV_CODEC_ID_ADPCM_EA,
+ AV_CODEC_ID_ADPCM_G726,
+ AV_CODEC_ID_ADPCM_CT,
+ AV_CODEC_ID_ADPCM_SWF,
+ AV_CODEC_ID_ADPCM_YAMAHA,
+ AV_CODEC_ID_ADPCM_SBPRO_4,
+ AV_CODEC_ID_ADPCM_SBPRO_3,
+ AV_CODEC_ID_ADPCM_SBPRO_2,
+ AV_CODEC_ID_ADPCM_THP,
+ AV_CODEC_ID_ADPCM_IMA_AMV,
+ AV_CODEC_ID_ADPCM_EA_R1,
+ AV_CODEC_ID_ADPCM_EA_R3,
+ AV_CODEC_ID_ADPCM_EA_R2,
+ AV_CODEC_ID_ADPCM_IMA_EA_SEAD,
+ AV_CODEC_ID_ADPCM_IMA_EA_EACS,
+ AV_CODEC_ID_ADPCM_EA_XAS,
+ AV_CODEC_ID_ADPCM_EA_MAXIS_XA,
+ AV_CODEC_ID_ADPCM_IMA_ISS,
+ AV_CODEC_ID_ADPCM_G722,
+ AV_CODEC_ID_ADPCM_IMA_APC,
+ AV_CODEC_ID_ADPCM_VIMA,
+#if FF_API_VIMA_DECODER
+ AV_CODEC_ID_VIMA = AV_CODEC_ID_ADPCM_VIMA,
+#endif
+
+ AV_CODEC_ID_ADPCM_AFC = 0x11800,
+ AV_CODEC_ID_ADPCM_IMA_OKI,
+ AV_CODEC_ID_ADPCM_DTK,
+ AV_CODEC_ID_ADPCM_IMA_RAD,
+ AV_CODEC_ID_ADPCM_G726LE,
+ AV_CODEC_ID_ADPCM_THP_LE,
+ AV_CODEC_ID_ADPCM_PSX,
+ AV_CODEC_ID_ADPCM_AICA,
+
+ /* AMR */
+ AV_CODEC_ID_AMR_NB = 0x12000,
+ AV_CODEC_ID_AMR_WB,
+
+ /* RealAudio codecs*/
+ AV_CODEC_ID_RA_144 = 0x13000,
+ AV_CODEC_ID_RA_288,
+
+ /* various DPCM codecs */
+ AV_CODEC_ID_ROQ_DPCM = 0x14000,
+ AV_CODEC_ID_INTERPLAY_DPCM,
+ AV_CODEC_ID_XAN_DPCM,
+ AV_CODEC_ID_SOL_DPCM,
+
+ AV_CODEC_ID_SDX2_DPCM = 0x14800,
+
+ /* audio codecs */
+ AV_CODEC_ID_MP2 = 0x15000,
+ AV_CODEC_ID_MP3, ///< preferred ID for decoding MPEG audio layer 1, 2 or 3
+ AV_CODEC_ID_AAC,
+ AV_CODEC_ID_AC3,
+ AV_CODEC_ID_DTS,
+ AV_CODEC_ID_VORBIS,
+ AV_CODEC_ID_DVAUDIO,
+ AV_CODEC_ID_WMAV1,
+ AV_CODEC_ID_WMAV2,
+ AV_CODEC_ID_MACE3,
+ AV_CODEC_ID_MACE6,
+ AV_CODEC_ID_VMDAUDIO,
+ AV_CODEC_ID_FLAC,
+ AV_CODEC_ID_MP3ADU,
+ AV_CODEC_ID_MP3ON4,
+ AV_CODEC_ID_SHORTEN,
+ AV_CODEC_ID_ALAC,
+ AV_CODEC_ID_WESTWOOD_SND1,
+ AV_CODEC_ID_GSM, ///< as in Berlin toast format
+ AV_CODEC_ID_QDM2,
+ AV_CODEC_ID_COOK,
+ AV_CODEC_ID_TRUESPEECH,
+ AV_CODEC_ID_TTA,
+ AV_CODEC_ID_SMACKAUDIO,
+ AV_CODEC_ID_QCELP,
+ AV_CODEC_ID_WAVPACK,
+ AV_CODEC_ID_DSICINAUDIO,
+ AV_CODEC_ID_IMC,
+ AV_CODEC_ID_MUSEPACK7,
+ AV_CODEC_ID_MLP,
+ AV_CODEC_ID_GSM_MS, /* as found in WAV */
+ AV_CODEC_ID_ATRAC3,
+#if FF_API_VOXWARE
+ AV_CODEC_ID_VOXWARE,
+#endif
+ AV_CODEC_ID_APE,
+ AV_CODEC_ID_NELLYMOSER,
+ AV_CODEC_ID_MUSEPACK8,
+ AV_CODEC_ID_SPEEX,
+ AV_CODEC_ID_WMAVOICE,
+ AV_CODEC_ID_WMAPRO,
+ AV_CODEC_ID_WMALOSSLESS,
+ AV_CODEC_ID_ATRAC3P,
+ AV_CODEC_ID_EAC3,
+ AV_CODEC_ID_SIPR,
+ AV_CODEC_ID_MP1,
+ AV_CODEC_ID_TWINVQ,
+ AV_CODEC_ID_TRUEHD,
+ AV_CODEC_ID_MP4ALS,
+ AV_CODEC_ID_ATRAC1,
+ AV_CODEC_ID_BINKAUDIO_RDFT,
+ AV_CODEC_ID_BINKAUDIO_DCT,
+ AV_CODEC_ID_AAC_LATM,
+ AV_CODEC_ID_QDMC,
+ AV_CODEC_ID_CELT,
+ AV_CODEC_ID_G723_1,
+ AV_CODEC_ID_G729,
+ AV_CODEC_ID_8SVX_EXP,
+ AV_CODEC_ID_8SVX_FIB,
+ AV_CODEC_ID_BMV_AUDIO,
+ AV_CODEC_ID_RALF,
+ AV_CODEC_ID_IAC,
+ AV_CODEC_ID_ILBC,
+ AV_CODEC_ID_OPUS,
+ AV_CODEC_ID_COMFORT_NOISE,
+ AV_CODEC_ID_TAK,
+ AV_CODEC_ID_METASOUND,
+ AV_CODEC_ID_PAF_AUDIO,
+ AV_CODEC_ID_ON2AVC,
+ AV_CODEC_ID_DSS_SP,
+
+ AV_CODEC_ID_FFWAVESYNTH = 0x15800,
+ AV_CODEC_ID_SONIC,
+ AV_CODEC_ID_SONIC_LS,
+ AV_CODEC_ID_EVRC,
+ AV_CODEC_ID_SMV,
+ AV_CODEC_ID_DSD_LSBF,
+ AV_CODEC_ID_DSD_MSBF,
+ AV_CODEC_ID_DSD_LSBF_PLANAR,
+ AV_CODEC_ID_DSD_MSBF_PLANAR,
+ AV_CODEC_ID_4GV,
+ AV_CODEC_ID_INTERPLAY_ACM,
+ AV_CODEC_ID_XMA1,
+ AV_CODEC_ID_XMA2,
+
+ /* subtitle codecs */
+ AV_CODEC_ID_FIRST_SUBTITLE = 0x17000, ///< A dummy ID pointing at the start of subtitle codecs.
+ AV_CODEC_ID_DVD_SUBTITLE = 0x17000,
+ AV_CODEC_ID_DVB_SUBTITLE,
+ AV_CODEC_ID_TEXT, ///< raw UTF-8 text
+ AV_CODEC_ID_XSUB,
+ AV_CODEC_ID_SSA,
+ AV_CODEC_ID_MOV_TEXT,
+ AV_CODEC_ID_HDMV_PGS_SUBTITLE,
+ AV_CODEC_ID_DVB_TELETEXT,
+ AV_CODEC_ID_SRT,
+
+ AV_CODEC_ID_MICRODVD = 0x17800,
+ AV_CODEC_ID_EIA_608,
+ AV_CODEC_ID_JACOSUB,
+ AV_CODEC_ID_SAMI,
+ AV_CODEC_ID_REALTEXT,
+ AV_CODEC_ID_STL,
+ AV_CODEC_ID_SUBVIEWER1,
+ AV_CODEC_ID_SUBVIEWER,
+ AV_CODEC_ID_SUBRIP,
+ AV_CODEC_ID_WEBVTT,
+ AV_CODEC_ID_MPL2,
+ AV_CODEC_ID_VPLAYER,
+ AV_CODEC_ID_PJS,
+ AV_CODEC_ID_ASS,
+ AV_CODEC_ID_HDMV_TEXT_SUBTITLE,
+
+ /* other specific kind of codecs (generally used for attachments) */
+ AV_CODEC_ID_FIRST_UNKNOWN = 0x18000, ///< A dummy ID pointing at the start of various fake codecs.
+ AV_CODEC_ID_TTF = 0x18000,
+
+ AV_CODEC_ID_BINTEXT = 0x18800,
+ AV_CODEC_ID_XBIN,
+ AV_CODEC_ID_IDF,
+ AV_CODEC_ID_OTF,
+ AV_CODEC_ID_SMPTE_KLV,
+ AV_CODEC_ID_DVD_NAV,
+ AV_CODEC_ID_TIMED_ID3,
+ AV_CODEC_ID_BIN_DATA,
+
+
+ AV_CODEC_ID_PROBE = 0x19000, ///< codec_id is not known (like AV_CODEC_ID_NONE) but lavf should attempt to identify it
+
+ AV_CODEC_ID_MPEG2TS = 0x20000, /**< _FAKE_ codec to indicate a raw MPEG-2 TS
+ * stream (only used by libavformat) */
+ AV_CODEC_ID_MPEG4SYSTEMS = 0x20001, /**< _FAKE_ codec to indicate a MPEG-4 Systems
+ * stream (only used by libavformat) */
+ AV_CODEC_ID_FFMETADATA = 0x21000, ///< Dummy codec for streams containing only metadata information.
+ AV_CODEC_ID_WRAPPED_AVFRAME = 0x21001, ///< Passthrough codec, AVFrames wrapped in AVPacket
+};
+
+/**
+ * This struct describes the properties of a single codec described by an
+ * AVCodecID.
+ * @see avcodec_descriptor_get()
+ */
+typedef struct AVCodecDescriptor {
+ enum AVCodecID id;
+ enum AVMediaType type;
+ /**
+ * Name of the codec described by this descriptor. It is non-empty and
+ * unique for each codec descriptor. It should contain alphanumeric
+ * characters and '_' only.
+ */
+ const char *name;
+ /**
+ * A more descriptive name for this codec. May be NULL.
+ */
+ const char *long_name;
+ /**
+ * Codec properties, a combination of AV_CODEC_PROP_* flags.
+ */
+ int props;
+ /**
+ * MIME type(s) associated with the codec.
+ * May be NULL; if not, a NULL-terminated array of MIME types.
+ * The first item is always non-NULL and is the preferred MIME type.
+ */
+ const char *const *mime_types;
+ /**
+ * If non-NULL, an array of profiles recognized for this codec.
+ * Terminated with FF_PROFILE_UNKNOWN.
+ */
+ const struct AVProfile *profiles;
+} AVCodecDescriptor;
+
+/**
+ * Codec uses only intra compression.
+ * Video codecs only.
+ */
+#define AV_CODEC_PROP_INTRA_ONLY (1 << 0)
+/**
+ * Codec supports lossy compression. Audio and video codecs only.
+ * @note a codec may support both lossy and lossless
+ * compression modes
+ */
+#define AV_CODEC_PROP_LOSSY (1 << 1)
+/**
+ * Codec supports lossless compression. Audio and video codecs only.
+ */
+#define AV_CODEC_PROP_LOSSLESS (1 << 2)
+/**
+ * Codec supports frame reordering. That is, the coded order (the order in which
+ * the encoded packets are output by the encoders / stored / input to the
+ * decoders) may be different from the presentation order of the corresponding
+ * frames.
+ *
+ * For codecs that do not have this property set, PTS and DTS should always be
+ * equal.
+ */
+#define AV_CODEC_PROP_REORDER (1 << 3)
+/**
+ * Subtitle codec is bitmap based
+ * Decoded AVSubtitle data can be read from the AVSubtitleRect->pict field.
+ */
+#define AV_CODEC_PROP_BITMAP_SUB (1 << 16)
+/**
+ * Subtitle codec is text based.
+ * Decoded AVSubtitle data can be read from the AVSubtitleRect->ass field.
+ */
+#define AV_CODEC_PROP_TEXT_SUB (1 << 17)
+
+/**
+ * @ingroup lavc_decoding
+ * Required number of additionally allocated bytes at the end of the input bitstream for decoding.
+ * This is mainly needed because some optimized bitstream readers read
+ * 32 or 64 bit at once and could read over the end.<br>
+ * Note: If the first 23 bits of the additional bytes are not 0, then damaged
+ * MPEG bitstreams could cause overread and segfault.
+ */
+#define AV_INPUT_BUFFER_PADDING_SIZE 32
+
+/**
+ * @ingroup lavc_encoding
+ * minimum encoding buffer size
+ * Used to avoid some checks during header writing.
+ */
+#define AV_INPUT_BUFFER_MIN_SIZE 16384
+
+#if FF_API_WITHOUT_PREFIX
+/**
+ * @deprecated use AV_INPUT_BUFFER_PADDING_SIZE instead
+ */
+#define FF_INPUT_BUFFER_PADDING_SIZE 32
+
+/**
+ * @deprecated use AV_INPUT_BUFFER_MIN_SIZE instead
+ */
+#define FF_MIN_BUFFER_SIZE 16384
+#endif /* FF_API_WITHOUT_PREFIX */
+
+/**
+ * @ingroup lavc_encoding
+ * motion estimation type.
+ * @deprecated use codec private option instead
+ */
+#if FF_API_MOTION_EST
+enum Motion_Est_ID {
+ ME_ZERO = 1, ///< no search, that is use 0,0 vector whenever one is needed
+ ME_FULL,
+ ME_LOG,
+ ME_PHODS,
+ ME_EPZS, ///< enhanced predictive zonal search
+ ME_X1, ///< reserved for experiments
+ ME_HEX, ///< hexagon based search
+ ME_UMH, ///< uneven multi-hexagon search
+ ME_TESA, ///< transformed exhaustive search algorithm
+ ME_ITER=50, ///< iterative search
+};
+#endif
+
+/**
+ * @ingroup lavc_decoding
+ */
+enum AVDiscard{
+ /* We leave some space between them for extensions (drop some
+ * keyframes for intra-only or drop just some bidir frames). */
+ AVDISCARD_NONE =-16, ///< discard nothing
+ AVDISCARD_DEFAULT = 0, ///< discard useless packets like 0 size packets in avi
+ AVDISCARD_NONREF = 8, ///< discard all non reference
+ AVDISCARD_BIDIR = 16, ///< discard all bidirectional frames
+ AVDISCARD_NONINTRA= 24, ///< discard all non intra frames
+ AVDISCARD_NONKEY = 32, ///< discard all frames except keyframes
+ AVDISCARD_ALL = 48, ///< discard all
+};
+
+enum AVAudioServiceType {
+ AV_AUDIO_SERVICE_TYPE_MAIN = 0,
+ AV_AUDIO_SERVICE_TYPE_EFFECTS = 1,
+ AV_AUDIO_SERVICE_TYPE_VISUALLY_IMPAIRED = 2,
+ AV_AUDIO_SERVICE_TYPE_HEARING_IMPAIRED = 3,
+ AV_AUDIO_SERVICE_TYPE_DIALOGUE = 4,
+ AV_AUDIO_SERVICE_TYPE_COMMENTARY = 5,
+ AV_AUDIO_SERVICE_TYPE_EMERGENCY = 6,
+ AV_AUDIO_SERVICE_TYPE_VOICE_OVER = 7,
+ AV_AUDIO_SERVICE_TYPE_KARAOKE = 8,
+ AV_AUDIO_SERVICE_TYPE_NB , ///< Not part of ABI
+};
+
+/**
+ * @ingroup lavc_encoding
+ */
+typedef struct RcOverride{
+ int start_frame;
+ int end_frame;
+ int qscale; // If this is 0 then quality_factor will be used instead.
+ float quality_factor;
+} RcOverride;
+
+#if FF_API_MAX_BFRAMES
+/**
+ * @deprecated there is no libavcodec-wide limit on the number of B-frames
+ */
+#define FF_MAX_B_FRAMES 16
+#endif
+
+/* encoding support
+ These flags can be passed in AVCodecContext.flags before initialization.
+ Note: Not everything is supported yet.
+*/
+
+/**
+ * Allow decoders to produce frames with data planes that are not aligned
+ * to CPU requirements (e.g. due to cropping).
+ */
+#define AV_CODEC_FLAG_UNALIGNED (1 << 0)
+/**
+ * Use fixed qscale.
+ */
+#define AV_CODEC_FLAG_QSCALE (1 << 1)
+/**
+ * 4 MV per MB allowed / advanced prediction for H.263.
+ */
+#define AV_CODEC_FLAG_4MV (1 << 2)
+/**
+ * Output even those frames that might be corrupted.
+ */
+#define AV_CODEC_FLAG_OUTPUT_CORRUPT (1 << 3)
+/**
+ * Use qpel MC.
+ */
+#define AV_CODEC_FLAG_QPEL (1 << 4)
+/**
+ * Use internal 2pass ratecontrol in first pass mode.
+ */
+#define AV_CODEC_FLAG_PASS1 (1 << 9)
+/**
+ * Use internal 2pass ratecontrol in second pass mode.
+ */
+#define AV_CODEC_FLAG_PASS2 (1 << 10)
+/**
+ * loop filter.
+ */
+#define AV_CODEC_FLAG_LOOP_FILTER (1 << 11)
+/**
+ * Only decode/encode grayscale.
+ */
+#define AV_CODEC_FLAG_GRAY (1 << 13)
+/**
+ * error[?] variables will be set during encoding.
+ */
+#define AV_CODEC_FLAG_PSNR (1 << 15)
+/**
+ * Input bitstream might be truncated at a random location
+ * instead of only at frame boundaries.
+ */
+#define AV_CODEC_FLAG_TRUNCATED (1 << 16)
+/**
+ * Use interlaced DCT.
+ */
+#define AV_CODEC_FLAG_INTERLACED_DCT (1 << 18)
+/**
+ * Force low delay.
+ */
+#define AV_CODEC_FLAG_LOW_DELAY (1 << 19)
+/**
+ * Place global headers in extradata instead of every keyframe.
+ */
+#define AV_CODEC_FLAG_GLOBAL_HEADER (1 << 22)
+/**
+ * Use only bitexact stuff (except (I)DCT).
+ */
+#define AV_CODEC_FLAG_BITEXACT (1 << 23)
+/* Fx : Flag for h263+ extra options */
+/**
+ * H.263 advanced intra coding / MPEG-4 AC prediction
+ */
+#define AV_CODEC_FLAG_AC_PRED (1 << 24)
+/**
+ * interlaced motion estimation
+ */
+#define AV_CODEC_FLAG_INTERLACED_ME (1 << 29)
+#define AV_CODEC_FLAG_CLOSED_GOP (1U << 31)
+
+/**
+ * Allow non spec compliant speedup tricks.
+ */
+#define AV_CODEC_FLAG2_FAST (1 << 0)
+/**
+ * Skip bitstream encoding.
+ */
+#define AV_CODEC_FLAG2_NO_OUTPUT (1 << 2)
+/**
+ * Place global headers at every keyframe instead of in extradata.
+ */
+#define AV_CODEC_FLAG2_LOCAL_HEADER (1 << 3)
+
+/**
+ * timecode is in drop frame format. DEPRECATED!!!!
+ */
+#define AV_CODEC_FLAG2_DROP_FRAME_TIMECODE (1 << 13)
+
+/**
+ * Input bitstream might be truncated at a packet boundaries
+ * instead of only at frame boundaries.
+ */
+#define AV_CODEC_FLAG2_CHUNKS (1 << 15)
+/**
+ * Discard cropping information from SPS.
+ */
+#define AV_CODEC_FLAG2_IGNORE_CROP (1 << 16)
+
+/**
+ * Show all frames before the first keyframe
+ */
+#define AV_CODEC_FLAG2_SHOW_ALL (1 << 22)
+/**
+ * Export motion vectors through frame side data
+ */
+#define AV_CODEC_FLAG2_EXPORT_MVS (1 << 28)
+/**
+ * Do not skip samples and export skip information as frame side data
+ */
+#define AV_CODEC_FLAG2_SKIP_MANUAL (1 << 29)
+
+/* Unsupported options :
+ * Syntax Arithmetic coding (SAC)
+ * Reference Picture Selection
+ * Independent Segment Decoding */
+/* /Fx */
+/* codec capabilities */
+
+/**
+ * Decoder can use draw_horiz_band callback.
+ */
+#define AV_CODEC_CAP_DRAW_HORIZ_BAND (1 << 0)
+/**
+ * Codec uses get_buffer() for allocating buffers and supports custom allocators.
+ * If not set, it might not use get_buffer() at all or use operations that
+ * assume the buffer was allocated by avcodec_default_get_buffer.
+ */
+#define AV_CODEC_CAP_DR1 (1 << 1)
+#define AV_CODEC_CAP_TRUNCATED (1 << 3)
+/**
+ * Encoder or decoder requires flushing with NULL input at the end in order to
+ * give the complete and correct output.
+ *
+ * NOTE: If this flag is not set, the codec is guaranteed to never be fed with
+ * with NULL data. The user can still send NULL data to the public encode
+ * or decode function, but libavcodec will not pass it along to the codec
+ * unless this flag is set.
+ *
+ * Decoders:
+ * The decoder has a non-zero delay and needs to be fed with avpkt->data=NULL,
+ * avpkt->size=0 at the end to get the delayed data until the decoder no longer
+ * returns frames.
+ *
+ * Encoders:
+ * The encoder needs to be fed with NULL data at the end of encoding until the
+ * encoder no longer returns data.
+ *
+ * NOTE: For encoders implementing the AVCodec.encode2() function, setting this
+ * flag also means that the encoder must set the pts and duration for
+ * each output packet. If this flag is not set, the pts and duration will
+ * be determined by libavcodec from the input frame.
+ */
+#define AV_CODEC_CAP_DELAY (1 << 5)
+/**
+ * Codec can be fed a final frame with a smaller size.
+ * This can be used to prevent truncation of the last audio samples.
+ */
+#define AV_CODEC_CAP_SMALL_LAST_FRAME (1 << 6)
+
+#if FF_API_CAP_VDPAU
+/**
+ * Codec can export data for HW decoding (VDPAU).
+ */
+#define AV_CODEC_CAP_HWACCEL_VDPAU (1 << 7)
+#endif
+
+/**
+ * Codec can output multiple frames per AVPacket
+ * Normally demuxers return one frame at a time, demuxers which do not do
+ * are connected to a parser to split what they return into proper frames.
+ * This flag is reserved to the very rare category of codecs which have a
+ * bitstream that cannot be split into frames without timeconsuming
+ * operations like full decoding. Demuxers carring such bitstreams thus
+ * may return multiple frames in a packet. This has many disadvantages like
+ * prohibiting stream copy in many cases thus it should only be considered
+ * as a last resort.
+ */
+#define AV_CODEC_CAP_SUBFRAMES (1 << 8)
+/**
+ * Codec is experimental and is thus avoided in favor of non experimental
+ * encoders
+ */
+#define AV_CODEC_CAP_EXPERIMENTAL (1 << 9)
+/**
+ * Codec should fill in channel configuration and samplerate instead of container
+ */
+#define AV_CODEC_CAP_CHANNEL_CONF (1 << 10)
+/**
+ * Codec supports frame-level multithreading.
+ */
+#define AV_CODEC_CAP_FRAME_THREADS (1 << 12)
+/**
+ * Codec supports slice-based (or partition-based) multithreading.
+ */
+#define AV_CODEC_CAP_SLICE_THREADS (1 << 13)
+/**
+ * Codec supports changed parameters at any point.
+ */
+#define AV_CODEC_CAP_PARAM_CHANGE (1 << 14)
+/**
+ * Codec supports avctx->thread_count == 0 (auto).
+ */
+#define AV_CODEC_CAP_AUTO_THREADS (1 << 15)
+/**
+ * Audio encoder supports receiving a different number of samples in each call.
+ */
+#define AV_CODEC_CAP_VARIABLE_FRAME_SIZE (1 << 16)
+/**
+ * Codec is intra only.
+ */
+#define AV_CODEC_CAP_INTRA_ONLY 0x40000000
+/**
+ * Codec is lossless.
+ */
+#define AV_CODEC_CAP_LOSSLESS 0x80000000
+
+
+#if FF_API_WITHOUT_PREFIX
+/**
+ * Allow decoders to produce frames with data planes that are not aligned
+ * to CPU requirements (e.g. due to cropping).
+ */
+#define CODEC_FLAG_UNALIGNED AV_CODEC_FLAG_UNALIGNED
+#define CODEC_FLAG_QSCALE AV_CODEC_FLAG_QSCALE
+#define CODEC_FLAG_4MV AV_CODEC_FLAG_4MV
+#define CODEC_FLAG_OUTPUT_CORRUPT AV_CODEC_FLAG_OUTPUT_CORRUPT
+#define CODEC_FLAG_QPEL AV_CODEC_FLAG_QPEL
+#if FF_API_GMC
+/**
+ * @deprecated use the "gmc" private option of the libxvid encoder
+ */
+#define CODEC_FLAG_GMC 0x0020 ///< Use GMC.
+#endif
+#if FF_API_MV0
+/**
+ * @deprecated use the flag "mv0" in the "mpv_flags" private option of the
+ * mpegvideo encoders
+ */
+#define CODEC_FLAG_MV0 0x0040
+#endif
+#if FF_API_INPUT_PRESERVED
+/**
+ * @deprecated passing reference-counted frames to the encoders replaces this
+ * flag
+ */
+#define CODEC_FLAG_INPUT_PRESERVED 0x0100
+#endif
+#define CODEC_FLAG_PASS1 AV_CODEC_FLAG_PASS1
+#define CODEC_FLAG_PASS2 AV_CODEC_FLAG_PASS2
+#define CODEC_FLAG_GRAY AV_CODEC_FLAG_GRAY
+#if FF_API_EMU_EDGE
+/**
+ * @deprecated edges are not used/required anymore. I.e. this flag is now always
+ * set.
+ */
+#define CODEC_FLAG_EMU_EDGE 0x4000
+#endif
+#define CODEC_FLAG_PSNR AV_CODEC_FLAG_PSNR
+#define CODEC_FLAG_TRUNCATED AV_CODEC_FLAG_TRUNCATED
+
+#if FF_API_NORMALIZE_AQP
+/**
+ * @deprecated use the flag "naq" in the "mpv_flags" private option of the
+ * mpegvideo encoders
+ */
+#define CODEC_FLAG_NORMALIZE_AQP 0x00020000
+#endif
+#define CODEC_FLAG_INTERLACED_DCT AV_CODEC_FLAG_INTERLACED_DCT
+#define CODEC_FLAG_LOW_DELAY AV_CODEC_FLAG_LOW_DELAY
+#define CODEC_FLAG_GLOBAL_HEADER AV_CODEC_FLAG_GLOBAL_HEADER
+#define CODEC_FLAG_BITEXACT AV_CODEC_FLAG_BITEXACT
+#define CODEC_FLAG_AC_PRED AV_CODEC_FLAG_AC_PRED
+#define CODEC_FLAG_LOOP_FILTER AV_CODEC_FLAG_LOOP_FILTER
+#define CODEC_FLAG_INTERLACED_ME AV_CODEC_FLAG_INTERLACED_ME
+#define CODEC_FLAG_CLOSED_GOP AV_CODEC_FLAG_CLOSED_GOP
+#define CODEC_FLAG2_FAST AV_CODEC_FLAG2_FAST
+#define CODEC_FLAG2_NO_OUTPUT AV_CODEC_FLAG2_NO_OUTPUT
+#define CODEC_FLAG2_LOCAL_HEADER AV_CODEC_FLAG2_LOCAL_HEADER
+#define CODEC_FLAG2_DROP_FRAME_TIMECODE AV_CODEC_FLAG2_DROP_FRAME_TIMECODE
+#define CODEC_FLAG2_IGNORE_CROP AV_CODEC_FLAG2_IGNORE_CROP
+
+#define CODEC_FLAG2_CHUNKS AV_CODEC_FLAG2_CHUNKS
+#define CODEC_FLAG2_SHOW_ALL AV_CODEC_FLAG2_SHOW_ALL
+#define CODEC_FLAG2_EXPORT_MVS AV_CODEC_FLAG2_EXPORT_MVS
+#define CODEC_FLAG2_SKIP_MANUAL AV_CODEC_FLAG2_SKIP_MANUAL
+
+/* Unsupported options :
+ * Syntax Arithmetic coding (SAC)
+ * Reference Picture Selection
+ * Independent Segment Decoding */
+/* /Fx */
+/* codec capabilities */
+
+#define CODEC_CAP_DRAW_HORIZ_BAND AV_CODEC_CAP_DRAW_HORIZ_BAND ///< Decoder can use draw_horiz_band callback.
+/**
+ * Codec uses get_buffer() for allocating buffers and supports custom allocators.
+ * If not set, it might not use get_buffer() at all or use operations that
+ * assume the buffer was allocated by avcodec_default_get_buffer.
+ */
+#define CODEC_CAP_DR1 AV_CODEC_CAP_DR1
+#define CODEC_CAP_TRUNCATED AV_CODEC_CAP_TRUNCATED
+#if FF_API_XVMC
+/* Codec can export data for HW decoding. This flag indicates that
+ * the codec would call get_format() with list that might contain HW accelerated
+ * pixel formats (XvMC, VDPAU, VAAPI, etc). The application can pick any of them
+ * including raw image format.
+ * The application can use the passed context to determine bitstream version,
+ * chroma format, resolution etc.
+ */
+#define CODEC_CAP_HWACCEL 0x0010
+#endif /* FF_API_XVMC */
+/**
+ * Encoder or decoder requires flushing with NULL input at the end in order to
+ * give the complete and correct output.
+ *
+ * NOTE: If this flag is not set, the codec is guaranteed to never be fed with
+ * with NULL data. The user can still send NULL data to the public encode
+ * or decode function, but libavcodec will not pass it along to the codec
+ * unless this flag is set.
+ *
+ * Decoders:
+ * The decoder has a non-zero delay and needs to be fed with avpkt->data=NULL,
+ * avpkt->size=0 at the end to get the delayed data until the decoder no longer
+ * returns frames.
+ *
+ * Encoders:
+ * The encoder needs to be fed with NULL data at the end of encoding until the
+ * encoder no longer returns data.
+ *
+ * NOTE: For encoders implementing the AVCodec.encode2() function, setting this
+ * flag also means that the encoder must set the pts and duration for
+ * each output packet. If this flag is not set, the pts and duration will
+ * be determined by libavcodec from the input frame.
+ */
+#define CODEC_CAP_DELAY AV_CODEC_CAP_DELAY
+/**
+ * Codec can be fed a final frame with a smaller size.
+ * This can be used to prevent truncation of the last audio samples.
+ */
+#define CODEC_CAP_SMALL_LAST_FRAME AV_CODEC_CAP_SMALL_LAST_FRAME
+#if FF_API_CAP_VDPAU
+/**
+ * Codec can export data for HW decoding (VDPAU).
+ */
+#define CODEC_CAP_HWACCEL_VDPAU AV_CODEC_CAP_HWACCEL_VDPAU
+#endif
+/**
+ * Codec can output multiple frames per AVPacket
+ * Normally demuxers return one frame at a time, demuxers which do not do
+ * are connected to a parser to split what they return into proper frames.
+ * This flag is reserved to the very rare category of codecs which have a
+ * bitstream that cannot be split into frames without timeconsuming
+ * operations like full decoding. Demuxers carring such bitstreams thus
+ * may return multiple frames in a packet. This has many disadvantages like
+ * prohibiting stream copy in many cases thus it should only be considered
+ * as a last resort.
+ */
+#define CODEC_CAP_SUBFRAMES AV_CODEC_CAP_SUBFRAMES
+/**
+ * Codec is experimental and is thus avoided in favor of non experimental
+ * encoders
+ */
+#define CODEC_CAP_EXPERIMENTAL AV_CODEC_CAP_EXPERIMENTAL
+/**
+ * Codec should fill in channel configuration and samplerate instead of container
+ */
+#define CODEC_CAP_CHANNEL_CONF AV_CODEC_CAP_CHANNEL_CONF
+#if FF_API_NEG_LINESIZES
+/**
+ * @deprecated no codecs use this capability
+ */
+#define CODEC_CAP_NEG_LINESIZES 0x0800
+#endif
+/**
+ * Codec supports frame-level multithreading.
+ */
+#define CODEC_CAP_FRAME_THREADS AV_CODEC_CAP_FRAME_THREADS
+/**
+ * Codec supports slice-based (or partition-based) multithreading.
+ */
+#define CODEC_CAP_SLICE_THREADS AV_CODEC_CAP_SLICE_THREADS
+/**
+ * Codec supports changed parameters at any point.
+ */
+#define CODEC_CAP_PARAM_CHANGE AV_CODEC_CAP_PARAM_CHANGE
+/**
+ * Codec supports avctx->thread_count == 0 (auto).
+ */
+#define CODEC_CAP_AUTO_THREADS AV_CODEC_CAP_AUTO_THREADS
+/**
+ * Audio encoder supports receiving a different number of samples in each call.
+ */
+#define CODEC_CAP_VARIABLE_FRAME_SIZE AV_CODEC_CAP_VARIABLE_FRAME_SIZE
+/**
+ * Codec is intra only.
+ */
+#define CODEC_CAP_INTRA_ONLY AV_CODEC_CAP_INTRA_ONLY
+/**
+ * Codec is lossless.
+ */
+#define CODEC_CAP_LOSSLESS AV_CODEC_CAP_LOSSLESS
+
+/**
+ * HWAccel is experimental and is thus avoided in favor of non experimental
+ * codecs
+ */
+#define HWACCEL_CODEC_CAP_EXPERIMENTAL 0x0200
+#endif /* FF_API_WITHOUT_PREFIX */
+
+#if FF_API_MB_TYPE
+//The following defines may change, don't expect compatibility if you use them.
+#define MB_TYPE_INTRA4x4 0x0001
+#define MB_TYPE_INTRA16x16 0x0002 //FIXME H.264-specific
+#define MB_TYPE_INTRA_PCM 0x0004 //FIXME H.264-specific
+#define MB_TYPE_16x16 0x0008
+#define MB_TYPE_16x8 0x0010
+#define MB_TYPE_8x16 0x0020
+#define MB_TYPE_8x8 0x0040
+#define MB_TYPE_INTERLACED 0x0080
+#define MB_TYPE_DIRECT2 0x0100 //FIXME
+#define MB_TYPE_ACPRED 0x0200
+#define MB_TYPE_GMC 0x0400
+#define MB_TYPE_SKIP 0x0800
+#define MB_TYPE_P0L0 0x1000
+#define MB_TYPE_P1L0 0x2000
+#define MB_TYPE_P0L1 0x4000
+#define MB_TYPE_P1L1 0x8000
+#define MB_TYPE_L0 (MB_TYPE_P0L0 | MB_TYPE_P1L0)
+#define MB_TYPE_L1 (MB_TYPE_P0L1 | MB_TYPE_P1L1)
+#define MB_TYPE_L0L1 (MB_TYPE_L0 | MB_TYPE_L1)
+#define MB_TYPE_QUANT 0x00010000
+#define MB_TYPE_CBP 0x00020000
+//Note bits 24-31 are reserved for codec specific use (h264 ref0, mpeg1 0mv, ...)
+#endif
+
+/**
+ * Pan Scan area.
+ * This specifies the area which should be displayed.
+ * Note there may be multiple such areas for one frame.
+ */
+typedef struct AVPanScan{
+ /**
+ * id
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int id;
+
+ /**
+ * width and height in 1/16 pel
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int width;
+ int height;
+
+ /**
+ * position of the top left corner in 1/16 pel for up to 3 fields/frames
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int16_t position[3][2];
+}AVPanScan;
+
+/**
+ * This structure describes the bitrate properties of an encoded bitstream. It
+ * roughly corresponds to a subset the VBV parameters for MPEG-2 or HRD
+ * parameters for H.264/HEVC.
+ */
+typedef struct AVCPBProperties {
+ /**
+ * Maximum bitrate of the stream, in bits per second.
+ * Zero if unknown or unspecified.
+ */
+ int max_bitrate;
+ /**
+ * Minimum bitrate of the stream, in bits per second.
+ * Zero if unknown or unspecified.
+ */
+ int min_bitrate;
+ /**
+ * Average bitrate of the stream, in bits per second.
+ * Zero if unknown or unspecified.
+ */
+ int avg_bitrate;
+
+ /**
+ * The size of the buffer to which the ratecontrol is applied, in bits.
+ * Zero if unknown or unspecified.
+ */
+ int buffer_size;
+
+ /**
+ * The delay between the time the packet this structure is associated with
+ * is received and the time when it should be decoded, in periods of a 27MHz
+ * clock.
+ *
+ * UINT64_MAX when unknown or unspecified.
+ */
+ uint64_t vbv_delay;
+} AVCPBProperties;
+
+#if FF_API_QSCALE_TYPE
+#define FF_QSCALE_TYPE_MPEG1 0
+#define FF_QSCALE_TYPE_MPEG2 1
+#define FF_QSCALE_TYPE_H264 2
+#define FF_QSCALE_TYPE_VP56 3
+#endif
+
+/**
+ * The decoder will keep a reference to the frame and may reuse it later.
+ */
+#define AV_GET_BUFFER_FLAG_REF (1 << 0)
+
+/**
+ * @defgroup lavc_packet AVPacket
+ *
+ * Types and functions for working with AVPacket.
+ * @{
+ */
+enum AVPacketSideDataType {
+ AV_PKT_DATA_PALETTE,
+ AV_PKT_DATA_NEW_EXTRADATA,
+
+ /**
+ * An AV_PKT_DATA_PARAM_CHANGE side data packet is laid out as follows:
+ * @code
+ * u32le param_flags
+ * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_COUNT)
+ * s32le channel_count
+ * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_LAYOUT)
+ * u64le channel_layout
+ * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_SAMPLE_RATE)
+ * s32le sample_rate
+ * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_DIMENSIONS)
+ * s32le width
+ * s32le height
+ * @endcode
+ */
+ AV_PKT_DATA_PARAM_CHANGE,
+
+ /**
+ * An AV_PKT_DATA_H263_MB_INFO side data packet contains a number of
+ * structures with info about macroblocks relevant to splitting the
+ * packet into smaller packets on macroblock edges (e.g. as for RFC 2190).
+ * That is, it does not necessarily contain info about all macroblocks,
+ * as long as the distance between macroblocks in the info is smaller
+ * than the target payload size.
+ * Each MB info structure is 12 bytes, and is laid out as follows:
+ * @code
+ * u32le bit offset from the start of the packet
+ * u8 current quantizer at the start of the macroblock
+ * u8 GOB number
+ * u16le macroblock address within the GOB
+ * u8 horizontal MV predictor
+ * u8 vertical MV predictor
+ * u8 horizontal MV predictor for block number 3
+ * u8 vertical MV predictor for block number 3
+ * @endcode
+ */
+ AV_PKT_DATA_H263_MB_INFO,
+
+ /**
+ * This side data should be associated with an audio stream and contains
+ * ReplayGain information in form of the AVReplayGain struct.
+ */
+ AV_PKT_DATA_REPLAYGAIN,
+
+ /**
+ * This side data contains a 3x3 transformation matrix describing an affine
+ * transformation that needs to be applied to the decoded video frames for
+ * correct presentation.
+ *
+ * See libavutil/display.h for a detailed description of the data.
+ */
+ AV_PKT_DATA_DISPLAYMATRIX,
+
+ /**
+ * This side data should be associated with a video stream and contains
+ * Stereoscopic 3D information in form of the AVStereo3D struct.
+ */
+ AV_PKT_DATA_STEREO3D,
+
+ /**
+ * This side data should be associated with an audio stream and corresponds
+ * to enum AVAudioServiceType.
+ */
+ AV_PKT_DATA_AUDIO_SERVICE_TYPE,
+
+ /**
+ * This side data contains quality related information from the encoder.
+ * @code
+ * u32le quality factor of the compressed frame. Allowed range is between 1 (good) and FF_LAMBDA_MAX (bad).
+ * u8 picture type
+ * u8 error count
+ * u16 reserved
+ * u64le[error count] sum of squared differences between encoder in and output
+ * @endcode
+ */
+ AV_PKT_DATA_QUALITY_STATS,
+
+ /**
+ * This side data contains an integer value representing the stream index
+ * of a "fallback" track. A fallback track indicates an alternate
+ * track to use when the current track can not be decoded for some reason.
+ * e.g. no decoder available for codec.
+ */
+ AV_PKT_DATA_FALLBACK_TRACK,
+
+ /**
+ * This side data corresponds to the AVCPBProperties struct.
+ */
+ AV_PKT_DATA_CPB_PROPERTIES,
+
+ /**
+ * Recommmends skipping the specified number of samples
+ * @code
+ * u32le number of samples to skip from start of this packet
+ * u32le number of samples to skip from end of this packet
+ * u8 reason for start skip
+ * u8 reason for end skip (0=padding silence, 1=convergence)
+ * @endcode
+ */
+ AV_PKT_DATA_SKIP_SAMPLES=70,
+
+ /**
+ * An AV_PKT_DATA_JP_DUALMONO side data packet indicates that
+ * the packet may contain "dual mono" audio specific to Japanese DTV
+ * and if it is true, recommends only the selected channel to be used.
+ * @code
+ * u8 selected channels (0=mail/left, 1=sub/right, 2=both)
+ * @endcode
+ */
+ AV_PKT_DATA_JP_DUALMONO,
+
+ /**
+ * A list of zero terminated key/value strings. There is no end marker for
+ * the list, so it is required to rely on the side data size to stop.
+ */
+ AV_PKT_DATA_STRINGS_METADATA,
+
+ /**
+ * Subtitle event position
+ * @code
+ * u32le x1
+ * u32le y1
+ * u32le x2
+ * u32le y2
+ * @endcode
+ */
+ AV_PKT_DATA_SUBTITLE_POSITION,
+
+ /**
+ * Data found in BlockAdditional element of matroska container. There is
+ * no end marker for the data, so it is required to rely on the side data
+ * size to recognize the end. 8 byte id (as found in BlockAddId) followed
+ * by data.
+ */
+ AV_PKT_DATA_MATROSKA_BLOCKADDITIONAL,
+
+ /**
+ * The optional first identifier line of a WebVTT cue.
+ */
+ AV_PKT_DATA_WEBVTT_IDENTIFIER,
+
+ /**
+ * The optional settings (rendering instructions) that immediately
+ * follow the timestamp specifier of a WebVTT cue.
+ */
+ AV_PKT_DATA_WEBVTT_SETTINGS,
+
+ /**
+ * A list of zero terminated key/value strings. There is no end marker for
+ * the list, so it is required to rely on the side data size to stop. This
+ * side data includes updated metadata which appeared in the stream.
+ */
+ AV_PKT_DATA_METADATA_UPDATE,
+};
+
+#define AV_PKT_DATA_QUALITY_FACTOR AV_PKT_DATA_QUALITY_STATS //DEPRECATED
+
+typedef struct AVPacketSideData {
+ uint8_t *data;
+ int size;
+ enum AVPacketSideDataType type;
+} AVPacketSideData;
+
+/**
+ * This structure stores compressed data. It is typically exported by demuxers
+ * and then passed as input to decoders, or received as output from encoders and
+ * then passed to muxers.
+ *
+ * For video, it should typically contain one compressed frame. For audio it may
+ * contain several compressed frames. Encoders are allowed to output empty
+ * packets, with no compressed data, containing only side data
+ * (e.g. to update some stream parameters at the end of encoding).
+ *
+ * AVPacket is one of the few structs in FFmpeg, whose size is a part of public
+ * ABI. Thus it may be allocated on stack and no new fields can be added to it
+ * without libavcodec and libavformat major bump.
+ *
+ * The semantics of data ownership depends on the buf field.
+ * If it is set, the packet data is dynamically allocated and is
+ * valid indefinitely until a call to av_packet_unref() reduces the
+ * reference count to 0.
+ *
+ * If the buf field is not set av_packet_ref() would make a copy instead
+ * of increasing the reference count.
+ *
+ * The side data is always allocated with av_malloc(), copied by
+ * av_packet_ref() and freed by av_packet_unref().
+ *
+ * @see av_packet_ref
+ * @see av_packet_unref
+ */
+typedef struct AVPacket {
+ /**
+ * A reference to the reference-counted buffer where the packet data is
+ * stored.
+ * May be NULL, then the packet data is not reference-counted.
+ */
+ AVBufferRef *buf;
+ /**
+ * Presentation timestamp in AVStream->time_base units; the time at which
+ * the decompressed packet will be presented to the user.
+ * Can be AV_NOPTS_VALUE if it is not stored in the file.
+ * pts MUST be larger or equal to dts as presentation cannot happen before
+ * decompression, unless one wants to view hex dumps. Some formats misuse
+ * the terms dts and pts/cts to mean something different. Such timestamps
+ * must be converted to true pts/dts before they are stored in AVPacket.
+ */
+ int64_t pts;
+ /**
+ * Decompression timestamp in AVStream->time_base units; the time at which
+ * the packet is decompressed.
+ * Can be AV_NOPTS_VALUE if it is not stored in the file.
+ */
+ int64_t dts;
+ uint8_t *data;
+ int size;
+ int stream_index;
+ /**
+ * A combination of AV_PKT_FLAG values
+ */
+ int flags;
+ /**
+ * Additional packet data that can be provided by the container.
+ * Packet can contain several types of side information.
+ */
+ AVPacketSideData *side_data;
+ int side_data_elems;
+
+ /**
+ * Duration of this packet in AVStream->time_base units, 0 if unknown.
+ * Equals next_pts - this_pts in presentation order.
+ */
+ int64_t duration;
+
+ int64_t pos; ///< byte position in stream, -1 if unknown
+
+#if FF_API_CONVERGENCE_DURATION
+ /**
+ * @deprecated Same as the duration field, but as int64_t. This was required
+ * for Matroska subtitles, whose duration values could overflow when the
+ * duration field was still an int.
+ */
+ attribute_deprecated
+ int64_t convergence_duration;
+#endif
+} AVPacket;
+#define AV_PKT_FLAG_KEY 0x0001 ///< The packet contains a keyframe
+#define AV_PKT_FLAG_CORRUPT 0x0002 ///< The packet content is corrupted
+
+enum AVSideDataParamChangeFlags {
+ AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_COUNT = 0x0001,
+ AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_LAYOUT = 0x0002,
+ AV_SIDE_DATA_PARAM_CHANGE_SAMPLE_RATE = 0x0004,
+ AV_SIDE_DATA_PARAM_CHANGE_DIMENSIONS = 0x0008,
+};
+/**
+ * @}
+ */
+
+struct AVCodecInternal;
+
+enum AVFieldOrder {
+ AV_FIELD_UNKNOWN,
+ AV_FIELD_PROGRESSIVE,
+ AV_FIELD_TT, //< Top coded_first, top displayed first
+ AV_FIELD_BB, //< Bottom coded first, bottom displayed first
+ AV_FIELD_TB, //< Top coded first, bottom displayed first
+ AV_FIELD_BT, //< Bottom coded first, top displayed first
+};
+
+/**
+ * main external API structure.
+ * New fields can be added to the end with minor version bumps.
+ * Removal, reordering and changes to existing fields require a major
+ * version bump.
+ * Please use AVOptions (av_opt* / av_set/get*()) to access these fields from user
+ * applications.
+ * sizeof(AVCodecContext) must not be used outside libav*.
+ */
+typedef struct AVCodecContext {
+ /**
+ * information on struct for av_log
+ * - set by avcodec_alloc_context3
+ */
+ const AVClass *av_class;
+ int log_level_offset;
+
+ enum AVMediaType codec_type; /* see AVMEDIA_TYPE_xxx */
+ const struct AVCodec *codec;
+#if FF_API_CODEC_NAME
+ /**
+ * @deprecated this field is not used for anything in libavcodec
+ */
+ attribute_deprecated
+ char codec_name[32];
+#endif
+ enum AVCodecID codec_id; /* see AV_CODEC_ID_xxx */
+
+ /**
+ * fourcc (LSB first, so "ABCD" -> ('D'<<24) + ('C'<<16) + ('B'<<8) + 'A').
+ * This is used to work around some encoder bugs.
+ * A demuxer should set this to what is stored in the field used to identify the codec.
+ * If there are multiple such fields in a container then the demuxer should choose the one
+ * which maximizes the information about the used codec.
+ * If the codec tag field in a container is larger than 32 bits then the demuxer should
+ * remap the longer ID to 32 bits with a table or other structure. Alternatively a new
+ * extra_codec_tag + size could be added but for this a clear advantage must be demonstrated
+ * first.
+ * - encoding: Set by user, if not then the default based on codec_id will be used.
+ * - decoding: Set by user, will be converted to uppercase by libavcodec during init.
+ */
+ unsigned int codec_tag;
+
+#if FF_API_STREAM_CODEC_TAG
+ /**
+ * @deprecated this field is unused
+ */
+ attribute_deprecated
+ unsigned int stream_codec_tag;
+#endif
+
+ void *priv_data;
+
+ /**
+ * Private context used for internal data.
+ *
+ * Unlike priv_data, this is not codec-specific. It is used in general
+ * libavcodec functions.
+ */
+ struct AVCodecInternal *internal;
+
+ /**
+ * Private data of the user, can be used to carry app specific stuff.
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ void *opaque;
+
+ /**
+ * the average bitrate
+ * - encoding: Set by user; unused for constant quantizer encoding.
+ * - decoding: Set by user, may be overwritten by libavcodec
+ * if this info is available in the stream
+ */
+ int64_t bit_rate;
+
+ /**
+ * number of bits the bitstream is allowed to diverge from the reference.
+ * the reference can be CBR (for CBR pass1) or VBR (for pass2)
+ * - encoding: Set by user; unused for constant quantizer encoding.
+ * - decoding: unused
+ */
+ int bit_rate_tolerance;
+
+ /**
+ * Global quality for codecs which cannot change it per frame.
+ * This should be proportional to MPEG-1/2/4 qscale.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int global_quality;
+
+ /**
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int compression_level;
+#define FF_COMPRESSION_DEFAULT -1
+
+ /**
+ * AV_CODEC_FLAG_*.
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int flags;
+
+ /**
+ * AV_CODEC_FLAG2_*
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int flags2;
+
+ /**
+ * some codecs need / can use extradata like Huffman tables.
+ * mjpeg: Huffman tables
+ * rv10: additional flags
+ * mpeg4: global headers (they can be in the bitstream or here)
+ * The allocated memory should be AV_INPUT_BUFFER_PADDING_SIZE bytes larger
+ * than extradata_size to avoid problems if it is read with the bitstream reader.
+ * The bytewise contents of extradata must not depend on the architecture or CPU endianness.
+ * - encoding: Set/allocated/freed by libavcodec.
+ * - decoding: Set/allocated/freed by user.
+ */
+ uint8_t *extradata;
+ int extradata_size;
+
+ /**
+ * This is the fundamental unit of time (in seconds) in terms
+ * of which frame timestamps are represented. For fixed-fps content,
+ * timebase should be 1/framerate and timestamp increments should be
+ * identically 1.
+ * This often, but not always is the inverse of the frame rate or field rate
+ * for video.
+ * - encoding: MUST be set by user.
+ * - decoding: the use of this field for decoding is deprecated.
+ * Use framerate instead.
+ */
+ AVRational time_base;
+
+ /**
+ * For some codecs, the time base is closer to the field rate than the frame rate.
+ * Most notably, H.264 and MPEG-2 specify time_base as half of frame duration
+ * if no telecine is used ...
+ *
+ * Set to time_base ticks per frame. Default 1, e.g., H.264/MPEG-2 set it to 2.
+ */
+ int ticks_per_frame;
+
+ /**
+ * Codec delay.
+ *
+ * Encoding: Number of frames delay there will be from the encoder input to
+ * the decoder output. (we assume the decoder matches the spec)
+ * Decoding: Number of frames delay in addition to what a standard decoder
+ * as specified in the spec would produce.
+ *
+ * Video:
+ * Number of frames the decoded output will be delayed relative to the
+ * encoded input.
+ *
+ * Audio:
+ * For encoding, this field is unused (see initial_padding).
+ *
+ * For decoding, this is the number of samples the decoder needs to
+ * output before the decoder's output is valid. When seeking, you should
+ * start decoding this many samples prior to your desired seek point.
+ *
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by libavcodec.
+ */
+ int delay;
+
+
+ /* video only */
+ /**
+ * picture width / height.
+ *
+ * @note Those fields may not match the values of the last
+ * AVFrame outputted by avcodec_decode_video2 due frame
+ * reordering.
+ *
+ * - encoding: MUST be set by user.
+ * - decoding: May be set by the user before opening the decoder if known e.g.
+ * from the container. Some decoders will require the dimensions
+ * to be set by the caller. During decoding, the decoder may
+ * overwrite those values as required while parsing the data.
+ */
+ int width, height;
+
+ /**
+ * Bitstream width / height, may be different from width/height e.g. when
+ * the decoded frame is cropped before being output or lowres is enabled.
+ *
+ * @note Those field may not match the value of the last
+ * AVFrame outputted by avcodec_decode_video2 due frame
+ * reordering.
+ *
+ * - encoding: unused
+ * - decoding: May be set by the user before opening the decoder if known
+ * e.g. from the container. During decoding, the decoder may
+ * overwrite those values as required while parsing the data.
+ */
+ int coded_width, coded_height;
+
+#if FF_API_ASPECT_EXTENDED
+#define FF_ASPECT_EXTENDED 15
+#endif
+
+ /**
+ * the number of pictures in a group of pictures, or 0 for intra_only
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int gop_size;
+
+ /**
+ * Pixel format, see AV_PIX_FMT_xxx.
+ * May be set by the demuxer if known from headers.
+ * May be overridden by the decoder if it knows better.
+ *
+ * @note This field may not match the value of the last
+ * AVFrame outputted by avcodec_decode_video2 due frame
+ * reordering.
+ *
+ * - encoding: Set by user.
+ * - decoding: Set by user if known, overridden by libavcodec while
+ * parsing the data.
+ */
+ enum AVPixelFormat pix_fmt;
+
+#if FF_API_MOTION_EST
+ /**
+ * This option does nothing
+ * @deprecated use codec private options instead
+ */
+ attribute_deprecated int me_method;
+#endif
+
+ /**
+ * If non NULL, 'draw_horiz_band' is called by the libavcodec
+ * decoder to draw a horizontal band. It improves cache usage. Not
+ * all codecs can do that. You must check the codec capabilities
+ * beforehand.
+ * When multithreading is used, it may be called from multiple threads
+ * at the same time; threads might draw different parts of the same AVFrame,
+ * or multiple AVFrames, and there is no guarantee that slices will be drawn
+ * in order.
+ * The function is also used by hardware acceleration APIs.
+ * It is called at least once during frame decoding to pass
+ * the data needed for hardware render.
+ * In that mode instead of pixel data, AVFrame points to
+ * a structure specific to the acceleration API. The application
+ * reads the structure and can change some fields to indicate progress
+ * or mark state.
+ * - encoding: unused
+ * - decoding: Set by user.
+ * @param height the height of the slice
+ * @param y the y position of the slice
+ * @param type 1->top field, 2->bottom field, 3->frame
+ * @param offset offset into the AVFrame.data from which the slice should be read
+ */
+ void (*draw_horiz_band)(struct AVCodecContext *s,
+ const AVFrame *src, int offset[AV_NUM_DATA_POINTERS],
+ int y, int type, int height);
+
+ /**
+ * callback to negotiate the pixelFormat
+ * @param fmt is the list of formats which are supported by the codec,
+ * it is terminated by -1 as 0 is a valid format, the formats are ordered by quality.
+ * The first is always the native one.
+ * @note The callback may be called again immediately if initialization for
+ * the selected (hardware-accelerated) pixel format failed.
+ * @warning Behavior is undefined if the callback returns a value not
+ * in the fmt list of formats.
+ * @return the chosen format
+ * - encoding: unused
+ * - decoding: Set by user, if not set the native format will be chosen.
+ */
+ enum AVPixelFormat (*get_format)(struct AVCodecContext *s, const enum AVPixelFormat * fmt);
+
+ /**
+ * maximum number of B-frames between non-B-frames
+ * Note: The output will be delayed by max_b_frames+1 relative to the input.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int max_b_frames;
+
+ /**
+ * qscale factor between IP and B-frames
+ * If > 0 then the last P-frame quantizer will be used (q= lastp_q*factor+offset).
+ * If < 0 then normal ratecontrol will be done (q= -normal_q*factor+offset).
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float b_quant_factor;
+
+#if FF_API_RC_STRATEGY
+ /** @deprecated use codec private option instead */
+ attribute_deprecated int rc_strategy;
+#define FF_RC_STRATEGY_XVID 1
+#endif
+
+ int b_frame_strategy;
+
+ /**
+ * qscale offset between IP and B-frames
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float b_quant_offset;
+
+ /**
+ * Size of the frame reordering buffer in the decoder.
+ * For MPEG-2 it is 1 IPB or 0 low delay IP.
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by libavcodec.
+ */
+ int has_b_frames;
+
+ /**
+ * 0-> h263 quant 1-> mpeg quant
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mpeg_quant;
+
+ /**
+ * qscale factor between P and I-frames
+ * If > 0 then the last p frame quantizer will be used (q= lastp_q*factor+offset).
+ * If < 0 then normal ratecontrol will be done (q= -normal_q*factor+offset).
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float i_quant_factor;
+
+ /**
+ * qscale offset between P and I-frames
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float i_quant_offset;
+
+ /**
+ * luminance masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float lumi_masking;
+
+ /**
+ * temporary complexity masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float temporal_cplx_masking;
+
+ /**
+ * spatial complexity masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float spatial_cplx_masking;
+
+ /**
+ * p block masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float p_masking;
+
+ /**
+ * darkness masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float dark_masking;
+
+ /**
+ * slice count
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by user (or 0).
+ */
+ int slice_count;
+ /**
+ * prediction method (needed for huffyuv)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int prediction_method;
+#define FF_PRED_LEFT 0
+#define FF_PRED_PLANE 1
+#define FF_PRED_MEDIAN 2
+
+ /**
+ * slice offsets in the frame in bytes
+ * - encoding: Set/allocated by libavcodec.
+ * - decoding: Set/allocated by user (or NULL).
+ */
+ int *slice_offset;
+
+ /**
+ * sample aspect ratio (0 if unknown)
+ * That is the width of a pixel divided by the height of the pixel.
+ * Numerator and denominator must be relatively prime and smaller than 256 for some video standards.
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ AVRational sample_aspect_ratio;
+
+ /**
+ * motion estimation comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_cmp;
+ /**
+ * subpixel motion estimation comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_sub_cmp;
+ /**
+ * macroblock comparison function (not supported yet)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mb_cmp;
+ /**
+ * interlaced DCT comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int ildct_cmp;
+#define FF_CMP_SAD 0
+#define FF_CMP_SSE 1
+#define FF_CMP_SATD 2
+#define FF_CMP_DCT 3
+#define FF_CMP_PSNR 4
+#define FF_CMP_BIT 5
+#define FF_CMP_RD 6
+#define FF_CMP_ZERO 7
+#define FF_CMP_VSAD 8
+#define FF_CMP_VSSE 9
+#define FF_CMP_NSSE 10
+#define FF_CMP_W53 11
+#define FF_CMP_W97 12
+#define FF_CMP_DCTMAX 13
+#define FF_CMP_DCT264 14
+#define FF_CMP_CHROMA 256
+
+ /**
+ * ME diamond size & shape
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int dia_size;
+
+ /**
+ * amount of previous MV predictors (2a+1 x 2a+1 square)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int last_predictor_count;
+
+ /**
+ * prepass for motion estimation
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int pre_me;
+
+ /**
+ * motion estimation prepass comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_pre_cmp;
+
+ /**
+ * ME prepass diamond size & shape
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int pre_dia_size;
+
+ /**
+ * subpel ME quality
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_subpel_quality;
+
+#if FF_API_AFD
+ /**
+ * DTG active format information (additional aspect ratio
+ * information only used in DVB MPEG-2 transport streams)
+ * 0 if not set.
+ *
+ * - encoding: unused
+ * - decoding: Set by decoder.
+ * @deprecated Deprecated in favor of AVSideData
+ */
+ attribute_deprecated int dtg_active_format;
+#define FF_DTG_AFD_SAME 8
+#define FF_DTG_AFD_4_3 9
+#define FF_DTG_AFD_16_9 10
+#define FF_DTG_AFD_14_9 11
+#define FF_DTG_AFD_4_3_SP_14_9 13
+#define FF_DTG_AFD_16_9_SP_14_9 14
+#define FF_DTG_AFD_SP_4_3 15
+#endif /* FF_API_AFD */
+
+ /**
+ * maximum motion estimation search range in subpel units
+ * If 0 then no limit.
+ *
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_range;
+
+#if FF_API_QUANT_BIAS
+ /**
+ * @deprecated use encoder private option instead
+ */
+ attribute_deprecated int intra_quant_bias;
+#define FF_DEFAULT_QUANT_BIAS 999999
+
+ /**
+ * @deprecated use encoder private option instead
+ */
+ attribute_deprecated int inter_quant_bias;
+#endif
+
+ /**
+ * slice flags
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int slice_flags;
+#define SLICE_FLAG_CODED_ORDER 0x0001 ///< draw_horiz_band() is called in coded order instead of display
+#define SLICE_FLAG_ALLOW_FIELD 0x0002 ///< allow draw_horiz_band() with field slices (MPEG2 field pics)
+#define SLICE_FLAG_ALLOW_PLANE 0x0004 ///< allow draw_horiz_band() with 1 component at a time (SVQ1)
+
+#if FF_API_XVMC
+ /**
+ * XVideo Motion Acceleration
+ * - encoding: forbidden
+ * - decoding: set by decoder
+ * @deprecated XvMC doesn't need it anymore.
+ */
+ attribute_deprecated int xvmc_acceleration;
+#endif /* FF_API_XVMC */
+
+ /**
+ * macroblock decision mode
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mb_decision;
+#define FF_MB_DECISION_SIMPLE 0 ///< uses mb_cmp
+#define FF_MB_DECISION_BITS 1 ///< chooses the one which needs the fewest bits
+#define FF_MB_DECISION_RD 2 ///< rate distortion
+
+ /**
+ * custom intra quantization matrix
+ * - encoding: Set by user, can be NULL.
+ * - decoding: Set by libavcodec.
+ */
+ uint16_t *intra_matrix;
+
+ /**
+ * custom inter quantization matrix
+ * - encoding: Set by user, can be NULL.
+ * - decoding: Set by libavcodec.
+ */
+ uint16_t *inter_matrix;
+
+ /**
+ * scene change detection threshold
+ * 0 is default, larger means fewer detected scene changes.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int scenechange_threshold;
+
+ /**
+ * noise reduction strength
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int noise_reduction;
+
+#if FF_API_MPV_OPT
+ /**
+ * @deprecated this field is unused
+ */
+ attribute_deprecated
+ int me_threshold;
+
+ /**
+ * @deprecated this field is unused
+ */
+ attribute_deprecated
+ int mb_threshold;
+#endif
+
+ /**
+ * precision of the intra DC coefficient - 8
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec
+ */
+ int intra_dc_precision;
+
+ /**
+ * Number of macroblock rows at the top which are skipped.
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int skip_top;
+
+ /**
+ * Number of macroblock rows at the bottom which are skipped.
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int skip_bottom;
+
+#if FF_API_MPV_OPT
+ /**
+ * @deprecated use encoder private options instead
+ */
+ attribute_deprecated
+ float border_masking;
+#endif
+
+ /**
+ * minimum MB lagrange multipler
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mb_lmin;
+
+ /**
+ * maximum MB lagrange multipler
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mb_lmax;
+
+ /**
+ *
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_penalty_compensation;
+
+ /**
+ *
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int bidir_refine;
+
+ /**
+ *
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int brd_scale;
+
+ /**
+ * minimum GOP size
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int keyint_min;
+
+ /**
+ * number of reference frames
+ * - encoding: Set by user.
+ * - decoding: Set by lavc.
+ */
+ int refs;
+
+ /**
+ * chroma qp offset from luma
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int chromaoffset;
+
+#if FF_API_UNUSED_MEMBERS
+ /**
+ * Multiplied by qscale for each frame and added to scene_change_score.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ attribute_deprecated int scenechange_factor;
+#endif
+
+ /**
+ *
+ * Note: Value depends upon the compare function used for fullpel ME.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mv0_threshold;
+
+ /**
+ * Adjust sensitivity of b_frame_strategy 1.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int b_sensitivity;
+
+ /**
+ * Chromaticity coordinates of the source primaries.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorPrimaries color_primaries;
+
+ /**
+ * Color Transfer Characteristic.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorTransferCharacteristic color_trc;
+
+ /**
+ * YUV colorspace type.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorSpace colorspace;
+
+ /**
+ * MPEG vs JPEG YUV range.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorRange color_range;
+
+ /**
+ * This defines the location of chroma samples.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVChromaLocation chroma_sample_location;
+
+ /**
+ * Number of slices.
+ * Indicates number of picture subdivisions. Used for parallelized
+ * decoding.
+ * - encoding: Set by user
+ * - decoding: unused
+ */
+ int slices;
+
+ /** Field order
+ * - encoding: set by libavcodec
+ * - decoding: Set by user.
+ */
+ enum AVFieldOrder field_order;
+
+ /* audio only */
+ int sample_rate; ///< samples per second
+ int channels; ///< number of audio channels
+
+ /**
+ * audio sample format
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ enum AVSampleFormat sample_fmt; ///< sample format
+
+ /* The following data should not be initialized. */
+ /**
+ * Number of samples per channel in an audio frame.
+ *
+ * - encoding: set by libavcodec in avcodec_open2(). Each submitted frame
+ * except the last must contain exactly frame_size samples per channel.
+ * May be 0 when the codec has AV_CODEC_CAP_VARIABLE_FRAME_SIZE set, then the
+ * frame size is not restricted.
+ * - decoding: may be set by some decoders to indicate constant frame size
+ */
+ int frame_size;
+
+ /**
+ * Frame counter, set by libavcodec.
+ *
+ * - decoding: total number of frames returned from the decoder so far.
+ * - encoding: total number of frames passed to the encoder so far.
+ *
+ * @note the counter is not incremented if encoding/decoding resulted in
+ * an error.
+ */
+ int frame_number;
+
+ /**
+ * number of bytes per packet if constant and known or 0
+ * Used by some WAV based audio codecs.
+ */
+ int block_align;
+
+ /**
+ * Audio cutoff bandwidth (0 means "automatic")
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int cutoff;
+
+ /**
+ * Audio channel layout.
+ * - encoding: set by user.
+ * - decoding: set by user, may be overwritten by libavcodec.
+ */
+ uint64_t channel_layout;
+
+ /**
+ * Request decoder to use this channel layout if it can (0 for default)
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ uint64_t request_channel_layout;
+
+ /**
+ * Type of service that the audio stream conveys.
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ enum AVAudioServiceType audio_service_type;
+
+ /**
+ * desired sample format
+ * - encoding: Not used.
+ * - decoding: Set by user.
+ * Decoder will decode to this format if it can.
+ */
+ enum AVSampleFormat request_sample_fmt;
+
+ /**
+ * This callback is called at the beginning of each frame to get data
+ * buffer(s) for it. There may be one contiguous buffer for all the data or
+ * there may be a buffer per each data plane or anything in between. What
+ * this means is, you may set however many entries in buf[] you feel necessary.
+ * Each buffer must be reference-counted using the AVBuffer API (see description
+ * of buf[] below).
+ *
+ * The following fields will be set in the frame before this callback is
+ * called:
+ * - format
+ * - width, height (video only)
+ * - sample_rate, channel_layout, nb_samples (audio only)
+ * Their values may differ from the corresponding values in
+ * AVCodecContext. This callback must use the frame values, not the codec
+ * context values, to calculate the required buffer size.
+ *
+ * This callback must fill the following fields in the frame:
+ * - data[]
+ * - linesize[]
+ * - extended_data:
+ * * if the data is planar audio with more than 8 channels, then this
+ * callback must allocate and fill extended_data to contain all pointers
+ * to all data planes. data[] must hold as many pointers as it can.
+ * extended_data must be allocated with av_malloc() and will be freed in
+ * av_frame_unref().
+ * * otherwise exended_data must point to data
+ * - buf[] must contain one or more pointers to AVBufferRef structures. Each of
+ * the frame's data and extended_data pointers must be contained in these. That
+ * is, one AVBufferRef for each allocated chunk of memory, not necessarily one
+ * AVBufferRef per data[] entry. See: av_buffer_create(), av_buffer_alloc(),
+ * and av_buffer_ref().
+ * - extended_buf and nb_extended_buf must be allocated with av_malloc() by
+ * this callback and filled with the extra buffers if there are more
+ * buffers than buf[] can hold. extended_buf will be freed in
+ * av_frame_unref().
+ *
+ * If AV_CODEC_CAP_DR1 is not set then get_buffer2() must call
+ * avcodec_default_get_buffer2() instead of providing buffers allocated by
+ * some other means.
+ *
+ * Each data plane must be aligned to the maximum required by the target
+ * CPU.
+ *
+ * @see avcodec_default_get_buffer2()
+ *
+ * Video:
+ *
+ * If AV_GET_BUFFER_FLAG_REF is set in flags then the frame may be reused
+ * (read and/or written to if it is writable) later by libavcodec.
+ *
+ * avcodec_align_dimensions2() should be used to find the required width and
+ * height, as they normally need to be rounded up to the next multiple of 16.
+ *
+ * Some decoders do not support linesizes changing between frames.
+ *
+ * If frame multithreading is used and thread_safe_callbacks is set,
+ * this callback may be called from a different thread, but not from more
+ * than one at once. Does not need to be reentrant.
+ *
+ * @see avcodec_align_dimensions2()
+ *
+ * Audio:
+ *
+ * Decoders request a buffer of a particular size by setting
+ * AVFrame.nb_samples prior to calling get_buffer2(). The decoder may,
+ * however, utilize only part of the buffer by setting AVFrame.nb_samples
+ * to a smaller value in the output frame.
+ *
+ * As a convenience, av_samples_get_buffer_size() and
+ * av_samples_fill_arrays() in libavutil may be used by custom get_buffer2()
+ * functions to find the required data size and to fill data pointers and
+ * linesize. In AVFrame.linesize, only linesize[0] may be set for audio
+ * since all planes must be the same size.
+ *
+ * @see av_samples_get_buffer_size(), av_samples_fill_arrays()
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec, user can override.
+ */
+ int (*get_buffer2)(struct AVCodecContext *s, AVFrame *frame, int flags);
+
+ /**
+ * If non-zero, the decoded audio and video frames returned from
+ * avcodec_decode_video2() and avcodec_decode_audio4() are reference-counted
+ * and are valid indefinitely. The caller must free them with
+ * av_frame_unref() when they are not needed anymore.
+ * Otherwise, the decoded frames must not be freed by the caller and are
+ * only valid until the next decode call.
+ *
+ * - encoding: unused
+ * - decoding: set by the caller before avcodec_open2().
+ */
+ int refcounted_frames;
+
+ /* - encoding parameters */
+ float qcompress; ///< amount of qscale change between easy & hard scenes (0.0-1.0)
+ float qblur; ///< amount of qscale smoothing over time (0.0-1.0)
+
+ /**
+ * minimum quantizer
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int qmin;
+
+ /**
+ * maximum quantizer
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int qmax;
+
+ /**
+ * maximum quantizer difference between frames
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int max_qdiff;
+
+#if FF_API_MPV_OPT
+ /**
+ * @deprecated use encoder private options instead
+ */
+ attribute_deprecated
+ float rc_qsquish;
+
+ attribute_deprecated
+ float rc_qmod_amp;
+ attribute_deprecated
+ int rc_qmod_freq;
+#endif
+
+ /**
+ * decoder bitstream buffer size
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int rc_buffer_size;
+
+ /**
+ * ratecontrol override, see RcOverride
+ * - encoding: Allocated/set/freed by user.
+ * - decoding: unused
+ */
+ int rc_override_count;
+ RcOverride *rc_override;
+
+#if FF_API_MPV_OPT
+ /**
+ * @deprecated use encoder private options instead
+ */
+ attribute_deprecated
+ const char *rc_eq;
+#endif
+
+ /**
+ * maximum bitrate
+ * - encoding: Set by user.
+ * - decoding: Set by user, may be overwritten by libavcodec.
+ */
+ int64_t rc_max_rate;
+
+ /**
+ * minimum bitrate
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int64_t rc_min_rate;
+
+#if FF_API_MPV_OPT
+ /**
+ * @deprecated use encoder private options instead
+ */
+ attribute_deprecated
+ float rc_buffer_aggressivity;
+
+ attribute_deprecated
+ float rc_initial_cplx;
+#endif
+
+ /**
+ * Ratecontrol attempt to use, at maximum, <value> of what can be used without an underflow.
+ * - encoding: Set by user.
+ * - decoding: unused.
+ */
+ float rc_max_available_vbv_use;
+
+ /**
+ * Ratecontrol attempt to use, at least, <value> times the amount needed to prevent a vbv overflow.
+ * - encoding: Set by user.
+ * - decoding: unused.
+ */
+ float rc_min_vbv_overflow_use;
+
+ /**
+ * Number of bits which should be loaded into the rc buffer before decoding starts.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int rc_initial_buffer_occupancy;
+
+#if FF_API_CODER_TYPE
+#define FF_CODER_TYPE_VLC 0
+#define FF_CODER_TYPE_AC 1
+#define FF_CODER_TYPE_RAW 2
+#define FF_CODER_TYPE_RLE 3
+#if FF_API_UNUSED_MEMBERS
+#define FF_CODER_TYPE_DEFLATE 4
+#endif /* FF_API_UNUSED_MEMBERS */
+ /**
+ * @deprecated use encoder private options instead
+ */
+ attribute_deprecated
+ int coder_type;
+#endif /* FF_API_CODER_TYPE */
+
+ /**
+ * context model
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int context_model;
+
+#if FF_API_MPV_OPT
+ /**
+ * @deprecated use encoder private options instead
+ */
+ attribute_deprecated
+ int lmin;
+
+ /**
+ * @deprecated use encoder private options instead
+ */
+ attribute_deprecated
+ int lmax;
+#endif
+
+ /**
+ * frame skip threshold
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int frame_skip_threshold;
+
+ /**
+ * frame skip factor
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int frame_skip_factor;
+
+ /**
+ * frame skip exponent
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int frame_skip_exp;
+
+ /**
+ * frame skip comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int frame_skip_cmp;
+
+ /**
+ * trellis RD quantization
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int trellis;
+
+ /**
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int min_prediction_order;
+
+ /**
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int max_prediction_order;
+
+ /**
+ * GOP timecode frame start number
+ * - encoding: Set by user, in non drop frame format
+ * - decoding: Set by libavcodec (timecode in the 25 bits format, -1 if unset)
+ */
+ int64_t timecode_frame_start;
+
+#if FF_API_RTP_CALLBACK
+ /**
+ * @deprecated unused
+ */
+ /* The RTP callback: This function is called */
+ /* every time the encoder has a packet to send. */
+ /* It depends on the encoder if the data starts */
+ /* with a Start Code (it should). H.263 does. */
+ /* mb_nb contains the number of macroblocks */
+ /* encoded in the RTP payload. */
+ attribute_deprecated
+ void (*rtp_callback)(struct AVCodecContext *avctx, void *data, int size, int mb_nb);
+#endif
+
+ int rtp_payload_size; /* The size of the RTP payload: the coder will */
+ /* do its best to deliver a chunk with size */
+ /* below rtp_payload_size, the chunk will start */
+ /* with a start code on some codecs like H.263. */
+ /* This doesn't take account of any particular */
+ /* headers inside the transmitted RTP payload. */
+
+#if FF_API_STAT_BITS
+ /* statistics, used for 2-pass encoding */
+ attribute_deprecated
+ int mv_bits;
+ attribute_deprecated
+ int header_bits;
+ attribute_deprecated
+ int i_tex_bits;
+ attribute_deprecated
+ int p_tex_bits;
+ attribute_deprecated
+ int i_count;
+ attribute_deprecated
+ int p_count;
+ attribute_deprecated
+ int skip_count;
+ attribute_deprecated
+ int misc_bits;
+
+ /** @deprecated this field is unused */
+ attribute_deprecated
+ int frame_bits;
+#endif
+
+ /**
+ * pass1 encoding statistics output buffer
+ * - encoding: Set by libavcodec.
+ * - decoding: unused
+ */
+ char *stats_out;
+
+ /**
+ * pass2 encoding statistics input buffer
+ * Concatenated stuff from stats_out of pass1 should be placed here.
+ * - encoding: Allocated/set/freed by user.
+ * - decoding: unused
+ */
+ char *stats_in;
+
+ /**
+ * Work around bugs in encoders which sometimes cannot be detected automatically.
+ * - encoding: Set by user
+ * - decoding: Set by user
+ */
+ int workaround_bugs;
+#define FF_BUG_AUTODETECT 1 ///< autodetection
+#if FF_API_OLD_MSMPEG4
+#define FF_BUG_OLD_MSMPEG4 2
+#endif
+#define FF_BUG_XVID_ILACE 4
+#define FF_BUG_UMP4 8
+#define FF_BUG_NO_PADDING 16
+#define FF_BUG_AMV 32
+#if FF_API_AC_VLC
+#define FF_BUG_AC_VLC 0 ///< Will be removed, libavcodec can now handle these non-compliant files by default.
+#endif
+#define FF_BUG_QPEL_CHROMA 64
+#define FF_BUG_STD_QPEL 128
+#define FF_BUG_QPEL_CHROMA2 256
+#define FF_BUG_DIRECT_BLOCKSIZE 512
+#define FF_BUG_EDGE 1024
+#define FF_BUG_HPEL_CHROMA 2048
+#define FF_BUG_DC_CLIP 4096
+#define FF_BUG_MS 8192 ///< Work around various bugs in Microsoft's broken decoders.
+#define FF_BUG_TRUNCATED 16384
+
+ /**
+ * strictly follow the standard (MPEG4, ...).
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ * Setting this to STRICT or higher means the encoder and decoder will
+ * generally do stupid things, whereas setting it to unofficial or lower
+ * will mean the encoder might produce output that is not supported by all
+ * spec-compliant decoders. Decoders don't differentiate between normal,
+ * unofficial and experimental (that is, they always try to decode things
+ * when they can) unless they are explicitly asked to behave stupidly
+ * (=strictly conform to the specs)
+ */
+ int strict_std_compliance;
+#define FF_COMPLIANCE_VERY_STRICT 2 ///< Strictly conform to an older more strict version of the spec or reference software.
+#define FF_COMPLIANCE_STRICT 1 ///< Strictly conform to all the things in the spec no matter what consequences.
+#define FF_COMPLIANCE_NORMAL 0
+#define FF_COMPLIANCE_UNOFFICIAL -1 ///< Allow unofficial extensions
+#define FF_COMPLIANCE_EXPERIMENTAL -2 ///< Allow nonstandardized experimental things.
+
+ /**
+ * error concealment flags
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int error_concealment;
+#define FF_EC_GUESS_MVS 1
+#define FF_EC_DEBLOCK 2
+#define FF_EC_FAVOR_INTER 256
+
+ /**
+ * debug
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int debug;
+#define FF_DEBUG_PICT_INFO 1
+#define FF_DEBUG_RC 2
+#define FF_DEBUG_BITSTREAM 4
+#define FF_DEBUG_MB_TYPE 8
+#define FF_DEBUG_QP 16
+#if FF_API_DEBUG_MV
+/**
+ * @deprecated this option does nothing
+ */
+#define FF_DEBUG_MV 32
+#endif
+#define FF_DEBUG_DCT_COEFF 0x00000040
+#define FF_DEBUG_SKIP 0x00000080
+#define FF_DEBUG_STARTCODE 0x00000100
+#if FF_API_UNUSED_MEMBERS
+#define FF_DEBUG_PTS 0x00000200
+#endif /* FF_API_UNUSED_MEMBERS */
+#define FF_DEBUG_ER 0x00000400
+#define FF_DEBUG_MMCO 0x00000800
+#define FF_DEBUG_BUGS 0x00001000
+#if FF_API_DEBUG_MV
+#define FF_DEBUG_VIS_QP 0x00002000 ///< only access through AVOptions from outside libavcodec
+#define FF_DEBUG_VIS_MB_TYPE 0x00004000 ///< only access through AVOptions from outside libavcodec
+#endif
+#define FF_DEBUG_BUFFERS 0x00008000
+#define FF_DEBUG_THREADS 0x00010000
+#define FF_DEBUG_GREEN_MD 0x00800000
+#define FF_DEBUG_NOMC 0x01000000
+
+#if FF_API_DEBUG_MV
+ /**
+ * debug
+ * Code outside libavcodec should access this field using AVOptions
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int debug_mv;
+#define FF_DEBUG_VIS_MV_P_FOR 0x00000001 //visualize forward predicted MVs of P frames
+#define FF_DEBUG_VIS_MV_B_FOR 0x00000002 //visualize forward predicted MVs of B frames
+#define FF_DEBUG_VIS_MV_B_BACK 0x00000004 //visualize backward predicted MVs of B frames
+#endif
+
+ /**
+ * Error recognition; may misdetect some more or less valid parts as errors.
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int err_recognition;
+
+/**
+ * Verify checksums embedded in the bitstream (could be of either encoded or
+ * decoded data, depending on the codec) and print an error message on mismatch.
+ * If AV_EF_EXPLODE is also set, a mismatching checksum will result in the
+ * decoder returning an error.
+ */
+#define AV_EF_CRCCHECK (1<<0)
+#define AV_EF_BITSTREAM (1<<1) ///< detect bitstream specification deviations
+#define AV_EF_BUFFER (1<<2) ///< detect improper bitstream length
+#define AV_EF_EXPLODE (1<<3) ///< abort decoding on minor error detection
+
+#define AV_EF_IGNORE_ERR (1<<15) ///< ignore errors and continue
+#define AV_EF_CAREFUL (1<<16) ///< consider things that violate the spec, are fast to calculate and have not been seen in the wild as errors
+#define AV_EF_COMPLIANT (1<<17) ///< consider all spec non compliances as errors
+#define AV_EF_AGGRESSIVE (1<<18) ///< consider things that a sane encoder should not do as an error
+
+
+ /**
+ * opaque 64bit number (generally a PTS) that will be reordered and
+ * output in AVFrame.reordered_opaque
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int64_t reordered_opaque;
+
+ /**
+ * Hardware accelerator in use
+ * - encoding: unused.
+ * - decoding: Set by libavcodec
+ */
+ struct AVHWAccel *hwaccel;
+
+ /**
+ * Hardware accelerator context.
+ * For some hardware accelerators, a global context needs to be
+ * provided by the user. In that case, this holds display-dependent
+ * data FFmpeg cannot instantiate itself. Please refer to the
+ * FFmpeg HW accelerator documentation to know how to fill this
+ * is. e.g. for VA API, this is a struct vaapi_context.
+ * - encoding: unused
+ * - decoding: Set by user
+ */
+ void *hwaccel_context;
+
+ /**
+ * error
+ * - encoding: Set by libavcodec if flags & AV_CODEC_FLAG_PSNR.
+ * - decoding: unused
+ */
+ uint64_t error[AV_NUM_DATA_POINTERS];
+
+ /**
+ * DCT algorithm, see FF_DCT_* below
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int dct_algo;
+#define FF_DCT_AUTO 0
+#define FF_DCT_FASTINT 1
+#define FF_DCT_INT 2
+#define FF_DCT_MMX 3
+#define FF_DCT_ALTIVEC 5
+#define FF_DCT_FAAN 6
+
+ /**
+ * IDCT algorithm, see FF_IDCT_* below.
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int idct_algo;
+#define FF_IDCT_AUTO 0
+#define FF_IDCT_INT 1
+#define FF_IDCT_SIMPLE 2
+#define FF_IDCT_SIMPLEMMX 3
+#define FF_IDCT_ARM 7
+#define FF_IDCT_ALTIVEC 8
+#if FF_API_ARCH_SH4
+#define FF_IDCT_SH4 9
+#endif
+#define FF_IDCT_SIMPLEARM 10
+#if FF_API_UNUSED_MEMBERS
+#define FF_IDCT_IPP 13
+#endif /* FF_API_UNUSED_MEMBERS */
+#define FF_IDCT_XVID 14
+#if FF_API_IDCT_XVIDMMX
+#define FF_IDCT_XVIDMMX 14
+#endif /* FF_API_IDCT_XVIDMMX */
+#define FF_IDCT_SIMPLEARMV5TE 16
+#define FF_IDCT_SIMPLEARMV6 17
+#if FF_API_ARCH_SPARC
+#define FF_IDCT_SIMPLEVIS 18
+#endif
+#define FF_IDCT_FAAN 20
+#define FF_IDCT_SIMPLENEON 22
+#if FF_API_ARCH_ALPHA
+#define FF_IDCT_SIMPLEALPHA 23
+#endif
+#define FF_IDCT_SIMPLEAUTO 128
+
+ /**
+ * bits per sample/pixel from the demuxer (needed for huffyuv).
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by user.
+ */
+ int bits_per_coded_sample;
+
+ /**
+ * Bits per sample/pixel of internal libavcodec pixel/sample format.
+ * - encoding: set by user.
+ * - decoding: set by libavcodec.
+ */
+ int bits_per_raw_sample;
+
+#if FF_API_LOWRES
+ /**
+ * low resolution decoding, 1-> 1/2 size, 2->1/4 size
+ * - encoding: unused
+ * - decoding: Set by user.
+ * Code outside libavcodec should access this field using:
+ * av_codec_{get,set}_lowres(avctx)
+ */
+ int lowres;
+#endif
+
+#if FF_API_CODED_FRAME
+ /**
+ * the picture in the bitstream
+ * - encoding: Set by libavcodec.
+ * - decoding: unused
+ *
+ * @deprecated use the quality factor packet side data instead
+ */
+ attribute_deprecated AVFrame *coded_frame;
+#endif
+
+ /**
+ * thread count
+ * is used to decide how many independent tasks should be passed to execute()
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int thread_count;
+
+ /**
+ * Which multithreading methods to use.
+ * Use of FF_THREAD_FRAME will increase decoding delay by one frame per thread,
+ * so clients which cannot provide future frames should not use it.
+ *
+ * - encoding: Set by user, otherwise the default is used.
+ * - decoding: Set by user, otherwise the default is used.
+ */
+ int thread_type;
+#define FF_THREAD_FRAME 1 ///< Decode more than one frame at once
+#define FF_THREAD_SLICE 2 ///< Decode more than one part of a single frame at once
+
+ /**
+ * Which multithreading methods are in use by the codec.
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by libavcodec.
+ */
+ int active_thread_type;
+
+ /**
+ * Set by the client if its custom get_buffer() callback can be called
+ * synchronously from another thread, which allows faster multithreaded decoding.
+ * draw_horiz_band() will be called from other threads regardless of this setting.
+ * Ignored if the default get_buffer() is used.
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int thread_safe_callbacks;
+
+ /**
+ * The codec may call this to execute several independent things.
+ * It will return only after finishing all tasks.
+ * The user may replace this with some multithreaded implementation,
+ * the default implementation will execute the parts serially.
+ * @param count the number of things to execute
+ * - encoding: Set by libavcodec, user can override.
+ * - decoding: Set by libavcodec, user can override.
+ */
+ int (*execute)(struct AVCodecContext *c, int (*func)(struct AVCodecContext *c2, void *arg), void *arg2, int *ret, int count, int size);
+
+ /**
+ * The codec may call this to execute several independent things.
+ * It will return only after finishing all tasks.
+ * The user may replace this with some multithreaded implementation,
+ * the default implementation will execute the parts serially.
+ * Also see avcodec_thread_init and e.g. the --enable-pthread configure option.
+ * @param c context passed also to func
+ * @param count the number of things to execute
+ * @param arg2 argument passed unchanged to func
+ * @param ret return values of executed functions, must have space for "count" values. May be NULL.
+ * @param func function that will be called count times, with jobnr from 0 to count-1.
+ * threadnr will be in the range 0 to c->thread_count-1 < MAX_THREADS and so that no
+ * two instances of func executing at the same time will have the same threadnr.
+ * @return always 0 currently, but code should handle a future improvement where when any call to func
+ * returns < 0 no further calls to func may be done and < 0 is returned.
+ * - encoding: Set by libavcodec, user can override.
+ * - decoding: Set by libavcodec, user can override.
+ */
+ int (*execute2)(struct AVCodecContext *c, int (*func)(struct AVCodecContext *c2, void *arg, int jobnr, int threadnr), void *arg2, int *ret, int count);
+
+ /**
+ * noise vs. sse weight for the nsse comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int nsse_weight;
+
+ /**
+ * profile
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int profile;
+#define FF_PROFILE_UNKNOWN -99
+#define FF_PROFILE_RESERVED -100
+
+#define FF_PROFILE_AAC_MAIN 0
+#define FF_PROFILE_AAC_LOW 1
+#define FF_PROFILE_AAC_SSR 2
+#define FF_PROFILE_AAC_LTP 3
+#define FF_PROFILE_AAC_HE 4
+#define FF_PROFILE_AAC_HE_V2 28
+#define FF_PROFILE_AAC_LD 22
+#define FF_PROFILE_AAC_ELD 38
+#define FF_PROFILE_MPEG2_AAC_LOW 128
+#define FF_PROFILE_MPEG2_AAC_HE 131
+
+#define FF_PROFILE_DTS 20
+#define FF_PROFILE_DTS_ES 30
+#define FF_PROFILE_DTS_96_24 40
+#define FF_PROFILE_DTS_HD_HRA 50
+#define FF_PROFILE_DTS_HD_MA 60
+#define FF_PROFILE_DTS_EXPRESS 70
+
+#define FF_PROFILE_MPEG2_422 0
+#define FF_PROFILE_MPEG2_HIGH 1
+#define FF_PROFILE_MPEG2_SS 2
+#define FF_PROFILE_MPEG2_SNR_SCALABLE 3
+#define FF_PROFILE_MPEG2_MAIN 4
+#define FF_PROFILE_MPEG2_SIMPLE 5
+
+#define FF_PROFILE_H264_CONSTRAINED (1<<9) // 8+1; constraint_set1_flag
+#define FF_PROFILE_H264_INTRA (1<<11) // 8+3; constraint_set3_flag
+
+#define FF_PROFILE_H264_BASELINE 66
+#define FF_PROFILE_H264_CONSTRAINED_BASELINE (66|FF_PROFILE_H264_CONSTRAINED)
+#define FF_PROFILE_H264_MAIN 77
+#define FF_PROFILE_H264_EXTENDED 88
+#define FF_PROFILE_H264_HIGH 100
+#define FF_PROFILE_H264_HIGH_10 110
+#define FF_PROFILE_H264_HIGH_10_INTRA (110|FF_PROFILE_H264_INTRA)
+#define FF_PROFILE_H264_HIGH_422 122
+#define FF_PROFILE_H264_HIGH_422_INTRA (122|FF_PROFILE_H264_INTRA)
+#define FF_PROFILE_H264_HIGH_444 144
+#define FF_PROFILE_H264_HIGH_444_PREDICTIVE 244
+#define FF_PROFILE_H264_HIGH_444_INTRA (244|FF_PROFILE_H264_INTRA)
+#define FF_PROFILE_H264_CAVLC_444 44
+
+#define FF_PROFILE_VC1_SIMPLE 0
+#define FF_PROFILE_VC1_MAIN 1
+#define FF_PROFILE_VC1_COMPLEX 2
+#define FF_PROFILE_VC1_ADVANCED 3
+
+#define FF_PROFILE_MPEG4_SIMPLE 0
+#define FF_PROFILE_MPEG4_SIMPLE_SCALABLE 1
+#define FF_PROFILE_MPEG4_CORE 2
+#define FF_PROFILE_MPEG4_MAIN 3
+#define FF_PROFILE_MPEG4_N_BIT 4
+#define FF_PROFILE_MPEG4_SCALABLE_TEXTURE 5
+#define FF_PROFILE_MPEG4_SIMPLE_FACE_ANIMATION 6
+#define FF_PROFILE_MPEG4_BASIC_ANIMATED_TEXTURE 7
+#define FF_PROFILE_MPEG4_HYBRID 8
+#define FF_PROFILE_MPEG4_ADVANCED_REAL_TIME 9
+#define FF_PROFILE_MPEG4_CORE_SCALABLE 10
+#define FF_PROFILE_MPEG4_ADVANCED_CODING 11
+#define FF_PROFILE_MPEG4_ADVANCED_CORE 12
+#define FF_PROFILE_MPEG4_ADVANCED_SCALABLE_TEXTURE 13
+#define FF_PROFILE_MPEG4_SIMPLE_STUDIO 14
+#define FF_PROFILE_MPEG4_ADVANCED_SIMPLE 15
+
+#define FF_PROFILE_JPEG2000_CSTREAM_RESTRICTION_0 0
+#define FF_PROFILE_JPEG2000_CSTREAM_RESTRICTION_1 1
+#define FF_PROFILE_JPEG2000_CSTREAM_NO_RESTRICTION 2
+#define FF_PROFILE_JPEG2000_DCINEMA_2K 3
+#define FF_PROFILE_JPEG2000_DCINEMA_4K 4
+
+#define FF_PROFILE_VP9_0 0
+#define FF_PROFILE_VP9_1 1
+#define FF_PROFILE_VP9_2 2
+#define FF_PROFILE_VP9_3 3
+
+#define FF_PROFILE_HEVC_MAIN 1
+#define FF_PROFILE_HEVC_MAIN_10 2
+#define FF_PROFILE_HEVC_MAIN_STILL_PICTURE 3
+#define FF_PROFILE_HEVC_REXT 4
+
+ /**
+ * level
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int level;
+#define FF_LEVEL_UNKNOWN -99
+
+ /**
+ * Skip loop filtering for selected frames.
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ enum AVDiscard skip_loop_filter;
+
+ /**
+ * Skip IDCT/dequantization for selected frames.
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ enum AVDiscard skip_idct;
+
+ /**
+ * Skip decoding for selected frames.
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ enum AVDiscard skip_frame;
+
+ /**
+ * Header containing style information for text subtitles.
+ * For SUBTITLE_ASS subtitle type, it should contain the whole ASS
+ * [Script Info] and [V4+ Styles] section, plus the [Events] line and
+ * the Format line following. It shouldn't include any Dialogue line.
+ * - encoding: Set/allocated/freed by user (before avcodec_open2())
+ * - decoding: Set/allocated/freed by libavcodec (by avcodec_open2())
+ */
+ uint8_t *subtitle_header;
+ int subtitle_header_size;
+
+#if FF_API_ERROR_RATE
+ /**
+ * @deprecated use the 'error_rate' private AVOption of the mpegvideo
+ * encoders
+ */
+ attribute_deprecated
+ int error_rate;
+#endif
+
+#if FF_API_VBV_DELAY
+ /**
+ * VBV delay coded in the last frame (in periods of a 27 MHz clock).
+ * Used for compliant TS muxing.
+ * - encoding: Set by libavcodec.
+ * - decoding: unused.
+ * @deprecated this value is now exported as a part of
+ * AV_PKT_DATA_CPB_PROPERTIES packet side data
+ */
+ attribute_deprecated
+ uint64_t vbv_delay;
+#endif
+
+#if FF_API_SIDEDATA_ONLY_PKT
+ /**
+ * Encoding only and set by default. Allow encoders to output packets
+ * that do not contain any encoded data, only side data.
+ *
+ * Some encoders need to output such packets, e.g. to update some stream
+ * parameters at the end of encoding.
+ *
+ * @deprecated this field disables the default behaviour and
+ * it is kept only for compatibility.
+ */
+ attribute_deprecated
+ int side_data_only_packets;
+#endif
+
+ /**
+ * Audio only. The number of "priming" samples (padding) inserted by the
+ * encoder at the beginning of the audio. I.e. this number of leading
+ * decoded samples must be discarded by the caller to get the original audio
+ * without leading padding.
+ *
+ * - decoding: unused
+ * - encoding: Set by libavcodec. The timestamps on the output packets are
+ * adjusted by the encoder so that they always refer to the
+ * first sample of the data actually contained in the packet,
+ * including any added padding. E.g. if the timebase is
+ * 1/samplerate and the timestamp of the first input sample is
+ * 0, the timestamp of the first output packet will be
+ * -initial_padding.
+ */
+ int initial_padding;
+
+ /**
+ * - decoding: For codecs that store a framerate value in the compressed
+ * bitstream, the decoder may export it here. { 0, 1} when
+ * unknown.
+ * - encoding: unused
+ */
+ AVRational framerate;
+
+ /**
+ * Nominal unaccelerated pixel format, see AV_PIX_FMT_xxx.
+ * - encoding: unused.
+ * - decoding: Set by libavcodec before calling get_format()
+ */
+ enum AVPixelFormat sw_pix_fmt;
+
+ /**
+ * Timebase in which pkt_dts/pts and AVPacket.dts/pts are.
+ * Code outside libavcodec should access this field using:
+ * av_codec_{get,set}_pkt_timebase(avctx)
+ * - encoding unused.
+ * - decoding set by user.
+ */
+ AVRational pkt_timebase;
+
+ /**
+ * AVCodecDescriptor
+ * Code outside libavcodec should access this field using:
+ * av_codec_{get,set}_codec_descriptor(avctx)
+ * - encoding: unused.
+ * - decoding: set by libavcodec.
+ */
+ const AVCodecDescriptor *codec_descriptor;
+
+#if !FF_API_LOWRES
+ /**
+ * low resolution decoding, 1-> 1/2 size, 2->1/4 size
+ * - encoding: unused
+ * - decoding: Set by user.
+ * Code outside libavcodec should access this field using:
+ * av_codec_{get,set}_lowres(avctx)
+ */
+ int lowres;
+#endif
+
+ /**
+ * Current statistics for PTS correction.
+ * - decoding: maintained and used by libavcodec, not intended to be used by user apps
+ * - encoding: unused
+ */
+ int64_t pts_correction_num_faulty_pts; /// Number of incorrect PTS values so far
+ int64_t pts_correction_num_faulty_dts; /// Number of incorrect DTS values so far
+ int64_t pts_correction_last_pts; /// PTS of the last frame
+ int64_t pts_correction_last_dts; /// DTS of the last frame
+
+ /**
+ * Character encoding of the input subtitles file.
+ * - decoding: set by user
+ * - encoding: unused
+ */
+ char *sub_charenc;
+
+ /**
+ * Subtitles character encoding mode. Formats or codecs might be adjusting
+ * this setting (if they are doing the conversion themselves for instance).
+ * - decoding: set by libavcodec
+ * - encoding: unused
+ */
+ int sub_charenc_mode;
+#define FF_SUB_CHARENC_MODE_DO_NOTHING -1 ///< do nothing (demuxer outputs a stream supposed to be already in UTF-8, or the codec is bitmap for instance)
+#define FF_SUB_CHARENC_MODE_AUTOMATIC 0 ///< libavcodec will select the mode itself
+#define FF_SUB_CHARENC_MODE_PRE_DECODER 1 ///< the AVPacket data needs to be recoded to UTF-8 before being fed to the decoder, requires iconv
+
+ /**
+ * Skip processing alpha if supported by codec.
+ * Note that if the format uses pre-multiplied alpha (common with VP6,
+ * and recommended due to better video quality/compression)
+ * the image will look as if alpha-blended onto a black background.
+ * However for formats that do not use pre-multiplied alpha
+ * there might be serious artefacts (though e.g. libswscale currently
+ * assumes pre-multiplied alpha anyway).
+ * Code outside libavcodec should access this field using AVOptions
+ *
+ * - decoding: set by user
+ * - encoding: unused
+ */
+ int skip_alpha;
+
+ /**
+ * Number of samples to skip after a discontinuity
+ * - decoding: unused
+ * - encoding: set by libavcodec
+ */
+ int seek_preroll;
+
+#if !FF_API_DEBUG_MV
+ /**
+ * debug motion vectors
+ * Code outside libavcodec should access this field using AVOptions
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int debug_mv;
+#define FF_DEBUG_VIS_MV_P_FOR 0x00000001 //visualize forward predicted MVs of P frames
+#define FF_DEBUG_VIS_MV_B_FOR 0x00000002 //visualize forward predicted MVs of B frames
+#define FF_DEBUG_VIS_MV_B_BACK 0x00000004 //visualize backward predicted MVs of B frames
+#endif
+
+ /**
+ * custom intra quantization matrix
+ * Code outside libavcodec should access this field using av_codec_g/set_chroma_intra_matrix()
+ * - encoding: Set by user, can be NULL.
+ * - decoding: unused.
+ */
+ uint16_t *chroma_intra_matrix;
+
+ /**
+ * dump format separator.
+ * can be ", " or "\n " or anything else
+ * Code outside libavcodec should access this field using AVOptions
+ * (NO direct access).
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ uint8_t *dump_separator;
+
+ /**
+ * ',' separated list of allowed decoders.
+ * If NULL then all are allowed
+ * - encoding: unused
+ * - decoding: set by user through AVOPtions (NO direct access)
+ */
+ char *codec_whitelist;
+
+ /*
+ * Properties of the stream that gets decoded
+ * To be accessed through av_codec_get_properties() (NO direct access)
+ * - encoding: unused
+ * - decoding: set by libavcodec
+ */
+ unsigned properties;
+#define FF_CODEC_PROPERTY_LOSSLESS 0x00000001
+#define FF_CODEC_PROPERTY_CLOSED_CAPTIONS 0x00000002
+
+ /**
+ * Additional data associated with the entire coded stream.
+ *
+ * - decoding: unused
+ * - encoding: may be set by libavcodec after avcodec_open2().
+ */
+ AVPacketSideData *coded_side_data;
+ int nb_coded_side_data;
+
+} AVCodecContext;
+
+AVRational av_codec_get_pkt_timebase (const AVCodecContext *avctx);
+void av_codec_set_pkt_timebase (AVCodecContext *avctx, AVRational val);
+
+const AVCodecDescriptor *av_codec_get_codec_descriptor(const AVCodecContext *avctx);
+void av_codec_set_codec_descriptor(AVCodecContext *avctx, const AVCodecDescriptor *desc);
+
+unsigned av_codec_get_codec_properties(const AVCodecContext *avctx);
+
+int av_codec_get_lowres(const AVCodecContext *avctx);
+void av_codec_set_lowres(AVCodecContext *avctx, int val);
+
+int av_codec_get_seek_preroll(const AVCodecContext *avctx);
+void av_codec_set_seek_preroll(AVCodecContext *avctx, int val);
+
+uint16_t *av_codec_get_chroma_intra_matrix(const AVCodecContext *avctx);
+void av_codec_set_chroma_intra_matrix(AVCodecContext *avctx, uint16_t *val);
+
+/**
+ * AVProfile.
+ */
+typedef struct AVProfile {
+ int profile;
+ const char *name; ///< short name for the profile
+} AVProfile;
+
+typedef struct AVCodecDefault AVCodecDefault;
+
+struct AVSubtitle;
+
+/**
+ * AVCodec.
+ */
+typedef struct AVCodec {
+ /**
+ * Name of the codec implementation.
+ * The name is globally unique among encoders and among decoders (but an
+ * encoder and a decoder can share the same name).
+ * This is the primary way to find a codec from the user perspective.
+ */
+ const char *name;
+ /**
+ * Descriptive name for the codec, meant to be more human readable than name.
+ * You should use the NULL_IF_CONFIG_SMALL() macro to define it.
+ */
+ const char *long_name;
+ enum AVMediaType type;
+ enum AVCodecID id;
+ /**
+ * Codec capabilities.
+ * see AV_CODEC_CAP_*
+ */
+ int capabilities;
+ const AVRational *supported_framerates; ///< array of supported framerates, or NULL if any, array is terminated by {0,0}
+ const enum AVPixelFormat *pix_fmts; ///< array of supported pixel formats, or NULL if unknown, array is terminated by -1
+ const int *supported_samplerates; ///< array of supported audio samplerates, or NULL if unknown, array is terminated by 0
+ const enum AVSampleFormat *sample_fmts; ///< array of supported sample formats, or NULL if unknown, array is terminated by -1
+ const uint64_t *channel_layouts; ///< array of support channel layouts, or NULL if unknown. array is terminated by 0
+ uint8_t max_lowres; ///< maximum value for lowres supported by the decoder, no direct access, use av_codec_get_max_lowres()
+ const AVClass *priv_class; ///< AVClass for the private context
+ const AVProfile *profiles; ///< array of recognized profiles, or NULL if unknown, array is terminated by {FF_PROFILE_UNKNOWN}
+
+ /*****************************************************************
+ * No fields below this line are part of the public API. They
+ * may not be used outside of libavcodec and can be changed and
+ * removed at will.
+ * New public fields should be added right above.
+ *****************************************************************
+ */
+ int priv_data_size;
+ struct AVCodec *next;
+ /**
+ * @name Frame-level threading support functions
+ * @{
+ */
+ /**
+ * If defined, called on thread contexts when they are created.
+ * If the codec allocates writable tables in init(), re-allocate them here.
+ * priv_data will be set to a copy of the original.
+ */
+ int (*init_thread_copy)(AVCodecContext *);
+ /**
+ * Copy necessary context variables from a previous thread context to the current one.
+ * If not defined, the next thread will start automatically; otherwise, the codec
+ * must call ff_thread_finish_setup().
+ *
+ * dst and src will (rarely) point to the same context, in which case memcpy should be skipped.
+ */
+ int (*update_thread_context)(AVCodecContext *dst, const AVCodecContext *src);
+ /** @} */
+
+ /**
+ * Private codec-specific defaults.
+ */
+ const AVCodecDefault *defaults;
+
+ /**
+ * Initialize codec static data, called from avcodec_register().
+ */
+ void (*init_static_data)(struct AVCodec *codec);
+
+ int (*init)(AVCodecContext *);
+ int (*encode_sub)(AVCodecContext *, uint8_t *buf, int buf_size,
+ const struct AVSubtitle *sub);
+ /**
+ * Encode data to an AVPacket.
+ *
+ * @param avctx codec context
+ * @param avpkt output AVPacket (may contain a user-provided buffer)
+ * @param[in] frame AVFrame containing the raw data to be encoded
+ * @param[out] got_packet_ptr encoder sets to 0 or 1 to indicate that a
+ * non-empty packet was returned in avpkt.
+ * @return 0 on success, negative error code on failure
+ */
+ int (*encode2)(AVCodecContext *avctx, AVPacket *avpkt, const AVFrame *frame,
+ int *got_packet_ptr);
+ int (*decode)(AVCodecContext *, void *outdata, int *outdata_size, AVPacket *avpkt);
+ int (*close)(AVCodecContext *);
+ /**
+ * Flush buffers.
+ * Will be called when seeking
+ */
+ void (*flush)(AVCodecContext *);
+ /**
+ * Internal codec capabilities.
+ * See FF_CODEC_CAP_* in internal.h
+ */
+ int caps_internal;
+} AVCodec;
+
+int av_codec_get_max_lowres(const AVCodec *codec);
+
+struct MpegEncContext;
+
+/**
+ * @defgroup lavc_hwaccel AVHWAccel
+ * @{
+ */
+typedef struct AVHWAccel {
+ /**
+ * Name of the hardware accelerated codec.
+ * The name is globally unique among encoders and among decoders (but an
+ * encoder and a decoder can share the same name).
+ */
+ const char *name;
+
+ /**
+ * Type of codec implemented by the hardware accelerator.
+ *
+ * See AVMEDIA_TYPE_xxx
+ */
+ enum AVMediaType type;
+
+ /**
+ * Codec implemented by the hardware accelerator.
+ *
+ * See AV_CODEC_ID_xxx
+ */
+ enum AVCodecID id;
+
+ /**
+ * Supported pixel format.
+ *
+ * Only hardware accelerated formats are supported here.
+ */
+ enum AVPixelFormat pix_fmt;
+
+ /**
+ * Hardware accelerated codec capabilities.
+ * see HWACCEL_CODEC_CAP_*
+ */
+ int capabilities;
+
+ /*****************************************************************
+ * No fields below this line are part of the public API. They
+ * may not be used outside of libavcodec and can be changed and
+ * removed at will.
+ * New public fields should be added right above.
+ *****************************************************************
+ */
+ struct AVHWAccel *next;
+
+ /**
+ * Allocate a custom buffer
+ */
+ int (*alloc_frame)(AVCodecContext *avctx, AVFrame *frame);
+
+ /**
+ * Called at the beginning of each frame or field picture.
+ *
+ * Meaningful frame information (codec specific) is guaranteed to
+ * be parsed at this point. This function is mandatory.
+ *
+ * Note that buf can be NULL along with buf_size set to 0.
+ * Otherwise, this means the whole frame is available at this point.
+ *
+ * @param avctx the codec context
+ * @param buf the frame data buffer base
+ * @param buf_size the size of the frame in bytes
+ * @return zero if successful, a negative value otherwise
+ */
+ int (*start_frame)(AVCodecContext *avctx, const uint8_t *buf, uint32_t buf_size);
+
+ /**
+ * Callback for each slice.
+ *
+ * Meaningful slice information (codec specific) is guaranteed to
+ * be parsed at this point. This function is mandatory.
+ * The only exception is XvMC, that works on MB level.
+ *
+ * @param avctx the codec context
+ * @param buf the slice data buffer base
+ * @param buf_size the size of the slice in bytes
+ * @return zero if successful, a negative value otherwise
+ */
+ int (*decode_slice)(AVCodecContext *avctx, const uint8_t *buf, uint32_t buf_size);
+
+ /**
+ * Called at the end of each frame or field picture.
+ *
+ * The whole picture is parsed at this point and can now be sent
+ * to the hardware accelerator. This function is mandatory.
+ *
+ * @param avctx the codec context
+ * @return zero if successful, a negative value otherwise
+ */
+ int (*end_frame)(AVCodecContext *avctx);
+
+ /**
+ * Size of per-frame hardware accelerator private data.
+ *
+ * Private data is allocated with av_mallocz() before
+ * AVCodecContext.get_buffer() and deallocated after
+ * AVCodecContext.release_buffer().
+ */
+ int frame_priv_data_size;
+
+ /**
+ * Called for every Macroblock in a slice.
+ *
+ * XvMC uses it to replace the ff_mpv_decode_mb().
+ * Instead of decoding to raw picture, MB parameters are
+ * stored in an array provided by the video driver.
+ *
+ * @param s the mpeg context
+ */
+ void (*decode_mb)(struct MpegEncContext *s);
+
+ /**
+ * Initialize the hwaccel private data.
+ *
+ * This will be called from ff_get_format(), after hwaccel and
+ * hwaccel_context are set and the hwaccel private data in AVCodecInternal
+ * is allocated.
+ */
+ int (*init)(AVCodecContext *avctx);
+
+ /**
+ * Uninitialize the hwaccel private data.
+ *
+ * This will be called from get_format() or avcodec_close(), after hwaccel
+ * and hwaccel_context are already uninitialized.
+ */
+ int (*uninit)(AVCodecContext *avctx);
+
+ /**
+ * Size of the private data to allocate in
+ * AVCodecInternal.hwaccel_priv_data.
+ */
+ int priv_data_size;
+} AVHWAccel;
+
+/**
+ * Hardware acceleration should be used for decoding even if the codec level
+ * used is unknown or higher than the maximum supported level reported by the
+ * hardware driver.
+ *
+ * It's generally a good idea to pass this flag unless you have a specific
+ * reason not to, as hardware tends to under-report supported levels.
+ */
+#define AV_HWACCEL_FLAG_IGNORE_LEVEL (1 << 0)
+
+/**
+ * Hardware acceleration can output YUV pixel formats with a different chroma
+ * sampling than 4:2:0 and/or other than 8 bits per component.
+ */
+#define AV_HWACCEL_FLAG_ALLOW_HIGH_DEPTH (1 << 1)
+
+/**
+ * @}
+ */
+
+#if FF_API_AVPICTURE
+/**
+ * @defgroup lavc_picture AVPicture
+ *
+ * Functions for working with AVPicture
+ * @{
+ */
+
+/**
+ * Picture data structure.
+ *
+ * Up to four components can be stored into it, the last component is
+ * alpha.
+ * @deprecated use AVFrame or imgutils functions instead
+ */
+typedef struct AVPicture {
+ attribute_deprecated
+ uint8_t *data[AV_NUM_DATA_POINTERS]; ///< pointers to the image data planes
+ attribute_deprecated
+ int linesize[AV_NUM_DATA_POINTERS]; ///< number of bytes per line
+} AVPicture;
+
+/**
+ * @}
+ */
+#endif
+
+enum AVSubtitleType {
+ SUBTITLE_NONE,
+
+ SUBTITLE_BITMAP, ///< A bitmap, pict will be set
+
+ /**
+ * Plain text, the text field must be set by the decoder and is
+ * authoritative. ass and pict fields may contain approximations.
+ */
+ SUBTITLE_TEXT,
+
+ /**
+ * Formatted text, the ass field must be set by the decoder and is
+ * authoritative. pict and text fields may contain approximations.
+ */
+ SUBTITLE_ASS,
+};
+
+#define AV_SUBTITLE_FLAG_FORCED 0x00000001
+
+typedef struct AVSubtitleRect {
+ int x; ///< top left corner of pict, undefined when pict is not set
+ int y; ///< top left corner of pict, undefined when pict is not set
+ int w; ///< width of pict, undefined when pict is not set
+ int h; ///< height of pict, undefined when pict is not set
+ int nb_colors; ///< number of colors in pict, undefined when pict is not set
+
+#if FF_API_AVPICTURE
+ /**
+ * @deprecated unused
+ */
+ attribute_deprecated
+ AVPicture pict;
+#endif
+ /**
+ * data+linesize for the bitmap of this subtitle.
+ * Can be set for text/ass as well once they are rendered.
+ */
+ uint8_t *data[4];
+ int linesize[4];
+
+ enum AVSubtitleType type;
+
+ char *text; ///< 0 terminated plain UTF-8 text
+
+ /**
+ * 0 terminated ASS/SSA compatible event line.
+ * The presentation of this is unaffected by the other values in this
+ * struct.
+ */
+ char *ass;
+
+ int flags;
+} AVSubtitleRect;
+
+typedef struct AVSubtitle {
+ uint16_t format; /* 0 = graphics */
+ uint32_t start_display_time; /* relative to packet pts, in ms */
+ uint32_t end_display_time; /* relative to packet pts, in ms */
+ unsigned num_rects;
+ AVSubtitleRect **rects;
+ int64_t pts; ///< Same as packet pts, in AV_TIME_BASE
+} AVSubtitle;
+
+/**
+ * If c is NULL, returns the first registered codec,
+ * if c is non-NULL, returns the next registered codec after c,
+ * or NULL if c is the last one.
+ */
+AVCodec *av_codec_next(const AVCodec *c);
+
+/**
+ * Return the LIBAVCODEC_VERSION_INT constant.
+ */
+unsigned avcodec_version(void);
+
+/**
+ * Return the libavcodec build-time configuration.
+ */
+const char *avcodec_configuration(void);
+
+/**
+ * Return the libavcodec license.
+ */
+const char *avcodec_license(void);
+
+/**
+ * Register the codec codec and initialize libavcodec.
+ *
+ * @warning either this function or avcodec_register_all() must be called
+ * before any other libavcodec functions.
+ *
+ * @see avcodec_register_all()
+ */
+void avcodec_register(AVCodec *codec);
+
+/**
+ * Register all the codecs, parsers and bitstream filters which were enabled at
+ * configuration time. If you do not call this function you can select exactly
+ * which formats you want to support, by using the individual registration
+ * functions.
+ *
+ * @see avcodec_register
+ * @see av_register_codec_parser
+ * @see av_register_bitstream_filter
+ */
+void avcodec_register_all(void);
+
+/**
+ * Allocate an AVCodecContext and set its fields to default values. The
+ * resulting struct should be freed with avcodec_free_context().
+ *
+ * @param codec if non-NULL, allocate private data and initialize defaults
+ * for the given codec. It is illegal to then call avcodec_open2()
+ * with a different codec.
+ * If NULL, then the codec-specific defaults won't be initialized,
+ * which may result in suboptimal default settings (this is
+ * important mainly for encoders, e.g. libx264).
+ *
+ * @return An AVCodecContext filled with default values or NULL on failure.
+ * @see avcodec_get_context_defaults
+ */
+AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);
+
+/**
+ * Free the codec context and everything associated with it and write NULL to
+ * the provided pointer.
+ */
+void avcodec_free_context(AVCodecContext **avctx);
+
+/**
+ * Set the fields of the given AVCodecContext to default values corresponding
+ * to the given codec (defaults may be codec-dependent).
+ *
+ * Do not call this function if a non-NULL codec has been passed
+ * to avcodec_alloc_context3() that allocated this AVCodecContext.
+ * If codec is non-NULL, it is illegal to call avcodec_open2() with a
+ * different codec on this AVCodecContext.
+ */
+int avcodec_get_context_defaults3(AVCodecContext *s, const AVCodec *codec);
+
+/**
+ * Get the AVClass for AVCodecContext. It can be used in combination with
+ * AV_OPT_SEARCH_FAKE_OBJ for examining options.
+ *
+ * @see av_opt_find().
+ */
+const AVClass *avcodec_get_class(void);
+
+/**
+ * Get the AVClass for AVFrame. It can be used in combination with
+ * AV_OPT_SEARCH_FAKE_OBJ for examining options.
+ *
+ * @see av_opt_find().
+ */
+const AVClass *avcodec_get_frame_class(void);
+
+/**
+ * Get the AVClass for AVSubtitleRect. It can be used in combination with
+ * AV_OPT_SEARCH_FAKE_OBJ for examining options.
+ *
+ * @see av_opt_find().
+ */
+const AVClass *avcodec_get_subtitle_rect_class(void);
+
+/**
+ * Copy the settings of the source AVCodecContext into the destination
+ * AVCodecContext. The resulting destination codec context will be
+ * unopened, i.e. you are required to call avcodec_open2() before you
+ * can use this AVCodecContext to decode/encode video/audio data.
+ *
+ * @param dest target codec context, should be initialized with
+ * avcodec_alloc_context3(NULL), but otherwise uninitialized
+ * @param src source codec context
+ * @return AVERROR() on error (e.g. memory allocation error), 0 on success
+ */
+int avcodec_copy_context(AVCodecContext *dest, const AVCodecContext *src);
+
+/**
+ * Initialize the AVCodecContext to use the given AVCodec. Prior to using this
+ * function the context has to be allocated with avcodec_alloc_context3().
+ *
+ * The functions avcodec_find_decoder_by_name(), avcodec_find_encoder_by_name(),
+ * avcodec_find_decoder() and avcodec_find_encoder() provide an easy way for
+ * retrieving a codec.
+ *
+ * @warning This function is not thread safe!
+ *
+ * @note Always call this function before using decoding routines (such as
+ * @ref avcodec_decode_video2()).
+ *
+ * @code
+ * avcodec_register_all();
+ * av_dict_set(&opts, "b", "2.5M", 0);
+ * codec = avcodec_find_decoder(AV_CODEC_ID_H264);
+ * if (!codec)
+ * exit(1);
+ *
+ * context = avcodec_alloc_context3(codec);
+ *
+ * if (avcodec_open2(context, codec, opts) < 0)
+ * exit(1);
+ * @endcode
+ *
+ * @param avctx The context to initialize.
+ * @param codec The codec to open this context for. If a non-NULL codec has been
+ * previously passed to avcodec_alloc_context3() or
+ * avcodec_get_context_defaults3() for this context, then this
+ * parameter MUST be either NULL or equal to the previously passed
+ * codec.
+ * @param options A dictionary filled with AVCodecContext and codec-private options.
+ * On return this object will be filled with options that were not found.
+ *
+ * @return zero on success, a negative value on error
+ * @see avcodec_alloc_context3(), avcodec_find_decoder(), avcodec_find_encoder(),
+ * av_dict_set(), av_opt_find().
+ */
+int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);
+
+/**
+ * Close a given AVCodecContext and free all the data associated with it
+ * (but not the AVCodecContext itself).
+ *
+ * Calling this function on an AVCodecContext that hasn't been opened will free
+ * the codec-specific data allocated in avcodec_alloc_context3() /
+ * avcodec_get_context_defaults3() with a non-NULL codec. Subsequent calls will
+ * do nothing.
+ */
+int avcodec_close(AVCodecContext *avctx);
+
+/**
+ * Free all allocated data in the given subtitle struct.
+ *
+ * @param sub AVSubtitle to free.
+ */
+void avsubtitle_free(AVSubtitle *sub);
+
+/**
+ * @}
+ */
+
+/**
+ * @addtogroup lavc_packet
+ * @{
+ */
+
+/**
+ * Allocate an AVPacket and set its fields to default values. The resulting
+ * struct must be freed using av_packet_free().
+ *
+ * @return An AVPacket filled with default values or NULL on failure.
+ *
+ * @note this only allocates the AVPacket itself, not the data buffers. Those
+ * must be allocated through other means such as av_new_packet.
+ *
+ * @see av_new_packet
+ */
+AVPacket *av_packet_alloc(void);
+
+/**
+ * Create a new packet that references the same data as src.
+ *
+ * This is a shortcut for av_packet_alloc()+av_packet_ref().
+ *
+ * @return newly created AVPacket on success, NULL on error.
+ *
+ * @see av_packet_alloc
+ * @see av_packet_ref
+ */
+AVPacket *av_packet_clone(AVPacket *src);
+
+/**
+ * Free the packet, if the packet is reference counted, it will be
+ * unreferenced first.
+ *
+ * @param packet packet to be freed. The pointer will be set to NULL.
+ * @note passing NULL is a no-op.
+ */
+void av_packet_free(AVPacket **pkt);
+
+/**
+ * Initialize optional fields of a packet with default values.
+ *
+ * Note, this does not touch the data and size members, which have to be
+ * initialized separately.
+ *
+ * @param pkt packet
+ */
+void av_init_packet(AVPacket *pkt);
+
+/**
+ * Allocate the payload of a packet and initialize its fields with
+ * default values.
+ *
+ * @param pkt packet
+ * @param size wanted payload size
+ * @return 0 if OK, AVERROR_xxx otherwise
+ */
+int av_new_packet(AVPacket *pkt, int size);
+
+/**
+ * Reduce packet size, correctly zeroing padding
+ *
+ * @param pkt packet
+ * @param size new size
+ */
+void av_shrink_packet(AVPacket *pkt, int size);
+
+/**
+ * Increase packet size, correctly zeroing padding
+ *
+ * @param pkt packet
+ * @param grow_by number of bytes by which to increase the size of the packet
+ */
+int av_grow_packet(AVPacket *pkt, int grow_by);
+
+/**
+ * Initialize a reference-counted packet from av_malloc()ed data.
+ *
+ * @param pkt packet to be initialized. This function will set the data, size,
+ * buf and destruct fields, all others are left untouched.
+ * @param data Data allocated by av_malloc() to be used as packet data. If this
+ * function returns successfully, the data is owned by the underlying AVBuffer.
+ * The caller may not access the data through other means.
+ * @param size size of data in bytes, without the padding. I.e. the full buffer
+ * size is assumed to be size + AV_INPUT_BUFFER_PADDING_SIZE.
+ *
+ * @return 0 on success, a negative AVERROR on error
+ */
+int av_packet_from_data(AVPacket *pkt, uint8_t *data, int size);
+
+#if FF_API_AVPACKET_OLD_API
+/**
+ * @warning This is a hack - the packet memory allocation stuff is broken. The
+ * packet is allocated if it was not really allocated.
+ *
+ * @deprecated Use av_packet_ref
+ */
+attribute_deprecated
+int av_dup_packet(AVPacket *pkt);
+/**
+ * Copy packet, including contents
+ *
+ * @return 0 on success, negative AVERROR on fail
+ */
+int av_copy_packet(AVPacket *dst, const AVPacket *src);
+
+/**
+ * Copy packet side data
+ *
+ * @return 0 on success, negative AVERROR on fail
+ */
+int av_copy_packet_side_data(AVPacket *dst, const AVPacket *src);
+
+/**
+ * Free a packet.
+ *
+ * @deprecated Use av_packet_unref
+ *
+ * @param pkt packet to free
+ */
+attribute_deprecated
+void av_free_packet(AVPacket *pkt);
+#endif
+/**
+ * Allocate new information of a packet.
+ *
+ * @param pkt packet
+ * @param type side information type
+ * @param size side information size
+ * @return pointer to fresh allocated data or NULL otherwise
+ */
+uint8_t* av_packet_new_side_data(AVPacket *pkt, enum AVPacketSideDataType type,
+ int size);
+
+/**
+ * Wrap an existing array as a packet side data.
+ *
+ * @param pkt packet
+ * @param type side information type
+ * @param data the side data array. It must be allocated with the av_malloc()
+ * family of functions. The ownership of the data is transferred to
+ * pkt.
+ * @param size side information size
+ * @return a non-negative number on success, a negative AVERROR code on
+ * failure. On failure, the packet is unchanged and the data remains
+ * owned by the caller.
+ */
+int av_packet_add_side_data(AVPacket *pkt, enum AVPacketSideDataType type,
+ uint8_t *data, size_t size);
+
+/**
+ * Shrink the already allocated side data buffer
+ *
+ * @param pkt packet
+ * @param type side information type
+ * @param size new side information size
+ * @return 0 on success, < 0 on failure
+ */
+int av_packet_shrink_side_data(AVPacket *pkt, enum AVPacketSideDataType type,
+ int size);
+
+/**
+ * Get side information from packet.
+ *
+ * @param pkt packet
+ * @param type desired side information type
+ * @param size pointer for side information size to store (optional)
+ * @return pointer to data if present or NULL otherwise
+ */
+uint8_t* av_packet_get_side_data(AVPacket *pkt, enum AVPacketSideDataType type,
+ int *size);
+
+int av_packet_merge_side_data(AVPacket *pkt);
+
+int av_packet_split_side_data(AVPacket *pkt);
+
+const char *av_packet_side_data_name(enum AVPacketSideDataType type);
+
+/**
+ * Pack a dictionary for use in side_data.
+ *
+ * @param dict The dictionary to pack.
+ * @param size pointer to store the size of the returned data
+ * @return pointer to data if successful, NULL otherwise
+ */
+uint8_t *av_packet_pack_dictionary(AVDictionary *dict, int *size);
+/**
+ * Unpack a dictionary from side_data.
+ *
+ * @param data data from side_data
+ * @param size size of the data
+ * @param dict the metadata storage dictionary
+ * @return 0 on success, < 0 on failure
+ */
+int av_packet_unpack_dictionary(const uint8_t *data, int size, AVDictionary **dict);
+
+
+/**
+ * Convenience function to free all the side data stored.
+ * All the other fields stay untouched.
+ *
+ * @param pkt packet
+ */
+void av_packet_free_side_data(AVPacket *pkt);
+
+/**
+ * Setup a new reference to the data described by a given packet
+ *
+ * If src is reference-counted, setup dst as a new reference to the
+ * buffer in src. Otherwise allocate a new buffer in dst and copy the
+ * data from src into it.
+ *
+ * All the other fields are copied from src.
+ *
+ * @see av_packet_unref
+ *
+ * @param dst Destination packet
+ * @param src Source packet
+ *
+ * @return 0 on success, a negative AVERROR on error.
+ */
+int av_packet_ref(AVPacket *dst, const AVPacket *src);
+
+/**
+ * Wipe the packet.
+ *
+ * Unreference the buffer referenced by the packet and reset the
+ * remaining packet fields to their default values.
+ *
+ * @param pkt The packet to be unreferenced.
+ */
+void av_packet_unref(AVPacket *pkt);
+
+/**
+ * Move every field in src to dst and reset src.
+ *
+ * @see av_packet_unref
+ *
+ * @param src Source packet, will be reset
+ * @param dst Destination packet
+ */
+void av_packet_move_ref(AVPacket *dst, AVPacket *src);
+
+/**
+ * Copy only "properties" fields from src to dst.
+ *
+ * Properties for the purpose of this function are all the fields
+ * beside those related to the packet data (buf, data, size)
+ *
+ * @param dst Destination packet
+ * @param src Source packet
+ *
+ * @return 0 on success AVERROR on failure.
+ *
+ */
+int av_packet_copy_props(AVPacket *dst, const AVPacket *src);
+
+/**
+ * Convert valid timing fields (timestamps / durations) in a packet from one
+ * timebase to another. Timestamps with unknown values (AV_NOPTS_VALUE) will be
+ * ignored.
+ *
+ * @param pkt packet on which the conversion will be performed
+ * @param tb_src source timebase, in which the timing fields in pkt are
+ * expressed
+ * @param tb_dst destination timebase, to which the timing fields will be
+ * converted
+ */
+void av_packet_rescale_ts(AVPacket *pkt, AVRational tb_src, AVRational tb_dst);
+
+/**
+ * @}
+ */
+
+/**
+ * @addtogroup lavc_decoding
+ * @{
+ */
+
+/**
+ * Find a registered decoder with a matching codec ID.
+ *
+ * @param id AVCodecID of the requested decoder
+ * @return A decoder if one was found, NULL otherwise.
+ */
+AVCodec *avcodec_find_decoder(enum AVCodecID id);
+
+/**
+ * Find a registered decoder with the specified name.
+ *
+ * @param name name of the requested decoder
+ * @return A decoder if one was found, NULL otherwise.
+ */
+AVCodec *avcodec_find_decoder_by_name(const char *name);
+
+/**
+ * The default callback for AVCodecContext.get_buffer2(). It is made public so
+ * it can be called by custom get_buffer2() implementations for decoders without
+ * AV_CODEC_CAP_DR1 set.
+ */
+int avcodec_default_get_buffer2(AVCodecContext *s, AVFrame *frame, int flags);
+
+#if FF_API_EMU_EDGE
+/**
+ * Return the amount of padding in pixels which the get_buffer callback must
+ * provide around the edge of the image for codecs which do not have the
+ * CODEC_FLAG_EMU_EDGE flag.
+ *
+ * @return Required padding in pixels.
+ *
+ * @deprecated CODEC_FLAG_EMU_EDGE is deprecated, so this function is no longer
+ * needed
+ */
+attribute_deprecated
+unsigned avcodec_get_edge_width(void);
+#endif
+
+/**
+ * Modify width and height values so that they will result in a memory
+ * buffer that is acceptable for the codec if you do not use any horizontal
+ * padding.
+ *
+ * May only be used if a codec with AV_CODEC_CAP_DR1 has been opened.
+ */
+void avcodec_align_dimensions(AVCodecContext *s, int *width, int *height);
+
+/**
+ * Modify width and height values so that they will result in a memory
+ * buffer that is acceptable for the codec if you also ensure that all
+ * line sizes are a multiple of the respective linesize_align[i].
+ *
+ * May only be used if a codec with AV_CODEC_CAP_DR1 has been opened.
+ */
+void avcodec_align_dimensions2(AVCodecContext *s, int *width, int *height,
+ int linesize_align[AV_NUM_DATA_POINTERS]);
+
+/**
+ * Converts AVChromaLocation to swscale x/y chroma position.
+ *
+ * The positions represent the chroma (0,0) position in a coordinates system
+ * with luma (0,0) representing the origin and luma(1,1) representing 256,256
+ *
+ * @param xpos horizontal chroma sample position
+ * @param ypos vertical chroma sample position
+ */
+int avcodec_enum_to_chroma_pos(int *xpos, int *ypos, enum AVChromaLocation pos);
+
+/**
+ * Converts swscale x/y chroma position to AVChromaLocation.
+ *
+ * The positions represent the chroma (0,0) position in a coordinates system
+ * with luma (0,0) representing the origin and luma(1,1) representing 256,256
+ *
+ * @param xpos horizontal chroma sample position
+ * @param ypos vertical chroma sample position
+ */
+enum AVChromaLocation avcodec_chroma_pos_to_enum(int xpos, int ypos);
+
+/**
+ * Decode the audio frame of size avpkt->size from avpkt->data into frame.
+ *
+ * Some decoders may support multiple frames in a single AVPacket. Such
+ * decoders would then just decode the first frame and the return value would be
+ * less than the packet size. In this case, avcodec_decode_audio4 has to be
+ * called again with an AVPacket containing the remaining data in order to
+ * decode the second frame, etc... Even if no frames are returned, the packet
+ * needs to be fed to the decoder with remaining data until it is completely
+ * consumed or an error occurs.
+ *
+ * Some decoders (those marked with AV_CODEC_CAP_DELAY) have a delay between input
+ * and output. This means that for some packets they will not immediately
+ * produce decoded output and need to be flushed at the end of decoding to get
+ * all the decoded data. Flushing is done by calling this function with packets
+ * with avpkt->data set to NULL and avpkt->size set to 0 until it stops
+ * returning samples. It is safe to flush even those decoders that are not
+ * marked with AV_CODEC_CAP_DELAY, then no samples will be returned.
+ *
+ * @warning The input buffer, avpkt->data must be AV_INPUT_BUFFER_PADDING_SIZE
+ * larger than the actual read bytes because some optimized bitstream
+ * readers read 32 or 64 bits at once and could read over the end.
+ *
+ * @note The AVCodecContext MUST have been opened with @ref avcodec_open2()
+ * before packets may be fed to the decoder.
+ *
+ * @param avctx the codec context
+ * @param[out] frame The AVFrame in which to store decoded audio samples.
+ * The decoder will allocate a buffer for the decoded frame by
+ * calling the AVCodecContext.get_buffer2() callback.
+ * When AVCodecContext.refcounted_frames is set to 1, the frame is
+ * reference counted and the returned reference belongs to the
+ * caller. The caller must release the frame using av_frame_unref()
+ * when the frame is no longer needed. The caller may safely write
+ * to the frame if av_frame_is_writable() returns 1.
+ * When AVCodecContext.refcounted_frames is set to 0, the returned
+ * reference belongs to the decoder and is valid only until the
+ * next call to this function or until closing or flushing the
+ * decoder. The caller may not write to it.
+ * @param[out] got_frame_ptr Zero if no frame could be decoded, otherwise it is
+ * non-zero. Note that this field being set to zero
+ * does not mean that an error has occurred. For
+ * decoders with AV_CODEC_CAP_DELAY set, no given decode
+ * call is guaranteed to produce a frame.
+ * @param[in] avpkt The input AVPacket containing the input buffer.
+ * At least avpkt->data and avpkt->size should be set. Some
+ * decoders might also require additional fields to be set.
+ * @return A negative error code is returned if an error occurred during
+ * decoding, otherwise the number of bytes consumed from the input
+ * AVPacket is returned.
+ */
+int avcodec_decode_audio4(AVCodecContext *avctx, AVFrame *frame,
+ int *got_frame_ptr, const AVPacket *avpkt);
+
+/**
+ * Decode the video frame of size avpkt->size from avpkt->data into picture.
+ * Some decoders may support multiple frames in a single AVPacket, such
+ * decoders would then just decode the first frame.
+ *
+ * @warning The input buffer must be AV_INPUT_BUFFER_PADDING_SIZE larger than
+ * the actual read bytes because some optimized bitstream readers read 32 or 64
+ * bits at once and could read over the end.
+ *
+ * @warning The end of the input buffer buf should be set to 0 to ensure that
+ * no overreading happens for damaged MPEG streams.
+ *
+ * @note Codecs which have the AV_CODEC_CAP_DELAY capability set have a delay
+ * between input and output, these need to be fed with avpkt->data=NULL,
+ * avpkt->size=0 at the end to return the remaining frames.
+ *
+ * @note The AVCodecContext MUST have been opened with @ref avcodec_open2()
+ * before packets may be fed to the decoder.
+ *
+ * @param avctx the codec context
+ * @param[out] picture The AVFrame in which the decoded video frame will be stored.
+ * Use av_frame_alloc() to get an AVFrame. The codec will
+ * allocate memory for the actual bitmap by calling the
+ * AVCodecContext.get_buffer2() callback.
+ * When AVCodecContext.refcounted_frames is set to 1, the frame is
+ * reference counted and the returned reference belongs to the
+ * caller. The caller must release the frame using av_frame_unref()
+ * when the frame is no longer needed. The caller may safely write
+ * to the frame if av_frame_is_writable() returns 1.
+ * When AVCodecContext.refcounted_frames is set to 0, the returned
+ * reference belongs to the decoder and is valid only until the
+ * next call to this function or until closing or flushing the
+ * decoder. The caller may not write to it.
+ *
+ * @param[in] avpkt The input AVPacket containing the input buffer.
+ * You can create such packet with av_init_packet() and by then setting
+ * data and size, some decoders might in addition need other fields like
+ * flags&AV_PKT_FLAG_KEY. All decoders are designed to use the least
+ * fields possible.
+ * @param[in,out] got_picture_ptr Zero if no frame could be decompressed, otherwise, it is nonzero.
+ * @return On error a negative value is returned, otherwise the number of bytes
+ * used or zero if no frame could be decompressed.
+ */
+int avcodec_decode_video2(AVCodecContext *avctx, AVFrame *picture,
+ int *got_picture_ptr,
+ const AVPacket *avpkt);
+
+/**
+ * Decode a subtitle message.
+ * Return a negative value on error, otherwise return the number of bytes used.
+ * If no subtitle could be decompressed, got_sub_ptr is zero.
+ * Otherwise, the subtitle is stored in *sub.
+ * Note that AV_CODEC_CAP_DR1 is not available for subtitle codecs. This is for
+ * simplicity, because the performance difference is expect to be negligible
+ * and reusing a get_buffer written for video codecs would probably perform badly
+ * due to a potentially very different allocation pattern.
+ *
+ * Some decoders (those marked with CODEC_CAP_DELAY) have a delay between input
+ * and output. This means that for some packets they will not immediately
+ * produce decoded output and need to be flushed at the end of decoding to get
+ * all the decoded data. Flushing is done by calling this function with packets
+ * with avpkt->data set to NULL and avpkt->size set to 0 until it stops
+ * returning subtitles. It is safe to flush even those decoders that are not
+ * marked with CODEC_CAP_DELAY, then no subtitles will be returned.
+ *
+ * @note The AVCodecContext MUST have been opened with @ref avcodec_open2()
+ * before packets may be fed to the decoder.
+ *
+ * @param avctx the codec context
+ * @param[out] sub The Preallocated AVSubtitle in which the decoded subtitle will be stored,
+ * must be freed with avsubtitle_free if *got_sub_ptr is set.
+ * @param[in,out] got_sub_ptr Zero if no subtitle could be decompressed, otherwise, it is nonzero.
+ * @param[in] avpkt The input AVPacket containing the input buffer.
+ */
+int avcodec_decode_subtitle2(AVCodecContext *avctx, AVSubtitle *sub,
+ int *got_sub_ptr,
+ AVPacket *avpkt);
+
+/**
+ * @defgroup lavc_parsing Frame parsing
+ * @{
+ */
+
+enum AVPictureStructure {
+ AV_PICTURE_STRUCTURE_UNKNOWN, //< unknown
+ AV_PICTURE_STRUCTURE_TOP_FIELD, //< coded as top field
+ AV_PICTURE_STRUCTURE_BOTTOM_FIELD, //< coded as bottom field
+ AV_PICTURE_STRUCTURE_FRAME, //< coded as frame
+};
+
+typedef struct AVCodecParserContext {
+ void *priv_data;
+ struct AVCodecParser *parser;
+ int64_t frame_offset; /* offset of the current frame */
+ int64_t cur_offset; /* current offset
+ (incremented by each av_parser_parse()) */
+ int64_t next_frame_offset; /* offset of the next frame */
+ /* video info */
+ int pict_type; /* XXX: Put it back in AVCodecContext. */
+ /**
+ * This field is used for proper frame duration computation in lavf.
+ * It signals, how much longer the frame duration of the current frame
+ * is compared to normal frame duration.
+ *
+ * frame_duration = (1 + repeat_pict) * time_base
+ *
+ * It is used by codecs like H.264 to display telecined material.
+ */
+ int repeat_pict; /* XXX: Put it back in AVCodecContext. */
+ int64_t pts; /* pts of the current frame */
+ int64_t dts; /* dts of the current frame */
+
+ /* private data */
+ int64_t last_pts;
+ int64_t last_dts;
+ int fetch_timestamp;
+
+#define AV_PARSER_PTS_NB 4
+ int cur_frame_start_index;
+ int64_t cur_frame_offset[AV_PARSER_PTS_NB];
+ int64_t cur_frame_pts[AV_PARSER_PTS_NB];
+ int64_t cur_frame_dts[AV_PARSER_PTS_NB];
+
+ int flags;
+#define PARSER_FLAG_COMPLETE_FRAMES 0x0001
+#define PARSER_FLAG_ONCE 0x0002
+/// Set if the parser has a valid file offset
+#define PARSER_FLAG_FETCHED_OFFSET 0x0004
+#define PARSER_FLAG_USE_CODEC_TS 0x1000
+
+ int64_t offset; ///< byte offset from starting packet start
+ int64_t cur_frame_end[AV_PARSER_PTS_NB];
+
+ /**
+ * Set by parser to 1 for key frames and 0 for non-key frames.
+ * It is initialized to -1, so if the parser doesn't set this flag,
+ * old-style fallback using AV_PICTURE_TYPE_I picture type as key frames
+ * will be used.
+ */
+ int key_frame;
+
+#if FF_API_CONVERGENCE_DURATION
+ /**
+ * @deprecated unused
+ */
+ attribute_deprecated
+ int64_t convergence_duration;
+#endif
+
+ // Timestamp generation support:
+ /**
+ * Synchronization point for start of timestamp generation.
+ *
+ * Set to >0 for sync point, 0 for no sync point and <0 for undefined
+ * (default).
+ *
+ * For example, this corresponds to presence of H.264 buffering period
+ * SEI message.
+ */
+ int dts_sync_point;
+
+ /**
+ * Offset of the current timestamp against last timestamp sync point in
+ * units of AVCodecContext.time_base.
+ *
+ * Set to INT_MIN when dts_sync_point unused. Otherwise, it must
+ * contain a valid timestamp offset.
+ *
+ * Note that the timestamp of sync point has usually a nonzero
+ * dts_ref_dts_delta, which refers to the previous sync point. Offset of
+ * the next frame after timestamp sync point will be usually 1.
+ *
+ * For example, this corresponds to H.264 cpb_removal_delay.
+ */
+ int dts_ref_dts_delta;
+
+ /**
+ * Presentation delay of current frame in units of AVCodecContext.time_base.
+ *
+ * Set to INT_MIN when dts_sync_point unused. Otherwise, it must
+ * contain valid non-negative timestamp delta (presentation time of a frame
+ * must not lie in the past).
+ *
+ * This delay represents the difference between decoding and presentation
+ * time of the frame.
+ *
+ * For example, this corresponds to H.264 dpb_output_delay.
+ */
+ int pts_dts_delta;
+
+ /**
+ * Position of the packet in file.
+ *
+ * Analogous to cur_frame_pts/dts
+ */
+ int64_t cur_frame_pos[AV_PARSER_PTS_NB];
+
+ /**
+ * Byte position of currently parsed frame in stream.
+ */
+ int64_t pos;
+
+ /**
+ * Previous frame byte position.
+ */
+ int64_t last_pos;
+
+ /**
+ * Duration of the current frame.
+ * For audio, this is in units of 1 / AVCodecContext.sample_rate.
+ * For all other types, this is in units of AVCodecContext.time_base.
+ */
+ int duration;
+
+ enum AVFieldOrder field_order;
+
+ /**
+ * Indicate whether a picture is coded as a frame, top field or bottom field.
+ *
+ * For example, H.264 field_pic_flag equal to 0 corresponds to
+ * AV_PICTURE_STRUCTURE_FRAME. An H.264 picture with field_pic_flag
+ * equal to 1 and bottom_field_flag equal to 0 corresponds to
+ * AV_PICTURE_STRUCTURE_TOP_FIELD.
+ */
+ enum AVPictureStructure picture_structure;
+
+ /**
+ * Picture number incremented in presentation or output order.
+ * This field may be reinitialized at the first picture of a new sequence.
+ *
+ * For example, this corresponds to H.264 PicOrderCnt.
+ */
+ int output_picture_number;
+
+ /**
+ * Dimensions of the decoded video intended for presentation.
+ */
+ int width;
+ int height;
+
+ /**
+ * Dimensions of the coded video.
+ */
+ int coded_width;
+ int coded_height;
+
+ /**
+ * The format of the coded data, corresponds to enum AVPixelFormat for video
+ * and for enum AVSampleFormat for audio.
+ *
+ * Note that a decoder can have considerable freedom in how exactly it
+ * decodes the data, so the format reported here might be different from the
+ * one returned by a decoder.
+ */
+ int format;
+} AVCodecParserContext;
+
+typedef struct AVCodecParser {
+ int codec_ids[5]; /* several codec IDs are permitted */
+ int priv_data_size;
+ int (*parser_init)(AVCodecParserContext *s);
+ /* This callback never returns an error, a negative value means that
+ * the frame start was in a previous packet. */
+ int (*parser_parse)(AVCodecParserContext *s,
+ AVCodecContext *avctx,
+ const uint8_t **poutbuf, int *poutbuf_size,
+ const uint8_t *buf, int buf_size);
+ void (*parser_close)(AVCodecParserContext *s);
+ int (*split)(AVCodecContext *avctx, const uint8_t *buf, int buf_size);
+ struct AVCodecParser *next;
+} AVCodecParser;
+
+AVCodecParser *av_parser_next(const AVCodecParser *c);
+
+void av_register_codec_parser(AVCodecParser *parser);
+AVCodecParserContext *av_parser_init(int codec_id);
+
+/**
+ * Parse a packet.
+ *
+ * @param s parser context.
+ * @param avctx codec context.
+ * @param poutbuf set to pointer to parsed buffer or NULL if not yet finished.
+ * @param poutbuf_size set to size of parsed buffer or zero if not yet finished.
+ * @param buf input buffer.
+ * @param buf_size input length, to signal EOF, this should be 0 (so that the last frame can be output).
+ * @param pts input presentation timestamp.
+ * @param dts input decoding timestamp.
+ * @param pos input byte position in stream.
+ * @return the number of bytes of the input bitstream used.
+ *
+ * Example:
+ * @code
+ * while(in_len){
+ * len = av_parser_parse2(myparser, AVCodecContext, &data, &size,
+ * in_data, in_len,
+ * pts, dts, pos);
+ * in_data += len;
+ * in_len -= len;
+ *
+ * if(size)
+ * decode_frame(data, size);
+ * }
+ * @endcode
+ */
+int av_parser_parse2(AVCodecParserContext *s,
+ AVCodecContext *avctx,
+ uint8_t **poutbuf, int *poutbuf_size,
+ const uint8_t *buf, int buf_size,
+ int64_t pts, int64_t dts,
+ int64_t pos);
+
+/**
+ * @return 0 if the output buffer is a subset of the input, 1 if it is allocated and must be freed
+ * @deprecated use AVBitStreamFilter
+ */
+int av_parser_change(AVCodecParserContext *s,
+ AVCodecContext *avctx,
+ uint8_t **poutbuf, int *poutbuf_size,
+ const uint8_t *buf, int buf_size, int keyframe);
+void av_parser_close(AVCodecParserContext *s);
+
+/**
+ * @}
+ * @}
+ */
+
+/**
+ * @addtogroup lavc_encoding
+ * @{
+ */
+
+/**
+ * Find a registered encoder with a matching codec ID.
+ *
+ * @param id AVCodecID of the requested encoder
+ * @return An encoder if one was found, NULL otherwise.
+ */
+AVCodec *avcodec_find_encoder(enum AVCodecID id);
+
+/**
+ * Find a registered encoder with the specified name.
+ *
+ * @param name name of the requested encoder
+ * @return An encoder if one was found, NULL otherwise.
+ */
+AVCodec *avcodec_find_encoder_by_name(const char *name);
+
+/**
+ * Encode a frame of audio.
+ *
+ * Takes input samples from frame and writes the next output packet, if
+ * available, to avpkt. The output packet does not necessarily contain data for
+ * the most recent frame, as encoders can delay, split, and combine input frames
+ * internally as needed.
+ *
+ * @param avctx codec context
+ * @param avpkt output AVPacket.
+ * The user can supply an output buffer by setting
+ * avpkt->data and avpkt->size prior to calling the
+ * function, but if the size of the user-provided data is not
+ * large enough, encoding will fail. If avpkt->data and
+ * avpkt->size are set, avpkt->destruct must also be set. All
+ * other AVPacket fields will be reset by the encoder using
+ * av_init_packet(). If avpkt->data is NULL, the encoder will
+ * allocate it. The encoder will set avpkt->size to the size
+ * of the output packet.
+ *
+ * If this function fails or produces no output, avpkt will be
+ * freed using av_packet_unref().
+ * @param[in] frame AVFrame containing the raw audio data to be encoded.
+ * May be NULL when flushing an encoder that has the
+ * AV_CODEC_CAP_DELAY capability set.
+ * If AV_CODEC_CAP_VARIABLE_FRAME_SIZE is set, then each frame
+ * can have any number of samples.
+ * If it is not set, frame->nb_samples must be equal to
+ * avctx->frame_size for all frames except the last.
+ * The final frame may be smaller than avctx->frame_size.
+ * @param[out] got_packet_ptr This field is set to 1 by libavcodec if the
+ * output packet is non-empty, and to 0 if it is
+ * empty. If the function returns an error, the
+ * packet can be assumed to be invalid, and the
+ * value of got_packet_ptr is undefined and should
+ * not be used.
+ * @return 0 on success, negative error code on failure
+ */
+int avcodec_encode_audio2(AVCodecContext *avctx, AVPacket *avpkt,
+ const AVFrame *frame, int *got_packet_ptr);
+
+/**
+ * Encode a frame of video.
+ *
+ * Takes input raw video data from frame and writes the next output packet, if
+ * available, to avpkt. The output packet does not necessarily contain data for
+ * the most recent frame, as encoders can delay and reorder input frames
+ * internally as needed.
+ *
+ * @param avctx codec context
+ * @param avpkt output AVPacket.
+ * The user can supply an output buffer by setting
+ * avpkt->data and avpkt->size prior to calling the
+ * function, but if the size of the user-provided data is not
+ * large enough, encoding will fail. All other AVPacket fields
+ * will be reset by the encoder using av_init_packet(). If
+ * avpkt->data is NULL, the encoder will allocate it.
+ * The encoder will set avpkt->size to the size of the
+ * output packet. The returned data (if any) belongs to the
+ * caller, he is responsible for freeing it.
+ *
+ * If this function fails or produces no output, avpkt will be
+ * freed using av_packet_unref().
+ * @param[in] frame AVFrame containing the raw video data to be encoded.
+ * May be NULL when flushing an encoder that has the
+ * AV_CODEC_CAP_DELAY capability set.
+ * @param[out] got_packet_ptr This field is set to 1 by libavcodec if the
+ * output packet is non-empty, and to 0 if it is
+ * empty. If the function returns an error, the
+ * packet can be assumed to be invalid, and the
+ * value of got_packet_ptr is undefined and should
+ * not be used.
+ * @return 0 on success, negative error code on failure
+ */
+int avcodec_encode_video2(AVCodecContext *avctx, AVPacket *avpkt,
+ const AVFrame *frame, int *got_packet_ptr);
+
+int avcodec_encode_subtitle(AVCodecContext *avctx, uint8_t *buf, int buf_size,
+ const AVSubtitle *sub);
+
+
+/**
+ * @}
+ */
+
+#if FF_API_AVCODEC_RESAMPLE
+/**
+ * @defgroup lavc_resample Audio resampling
+ * @ingroup libavc
+ * @deprecated use libswresample instead
+ *
+ * @{
+ */
+struct ReSampleContext;
+struct AVResampleContext;
+
+typedef struct ReSampleContext ReSampleContext;
+
+/**
+ * Initialize audio resampling context.
+ *
+ * @param output_channels number of output channels
+ * @param input_channels number of input channels
+ * @param output_rate output sample rate
+ * @param input_rate input sample rate
+ * @param sample_fmt_out requested output sample format
+ * @param sample_fmt_in input sample format
+ * @param filter_length length of each FIR filter in the filterbank relative to the cutoff frequency
+ * @param log2_phase_count log2 of the number of entries in the polyphase filterbank
+ * @param linear if 1 then the used FIR filter will be linearly interpolated
+ between the 2 closest, if 0 the closest will be used
+ * @param cutoff cutoff frequency, 1.0 corresponds to half the output sampling rate
+ * @return allocated ReSampleContext, NULL if error occurred
+ */
+attribute_deprecated
+ReSampleContext *av_audio_resample_init(int output_channels, int input_channels,
+ int output_rate, int input_rate,
+ enum AVSampleFormat sample_fmt_out,
+ enum AVSampleFormat sample_fmt_in,
+ int filter_length, int log2_phase_count,
+ int linear, double cutoff);
+
+attribute_deprecated
+int audio_resample(ReSampleContext *s, short *output, short *input, int nb_samples);
+
+/**
+ * Free resample context.
+ *
+ * @param s a non-NULL pointer to a resample context previously
+ * created with av_audio_resample_init()
+ */
+attribute_deprecated
+void audio_resample_close(ReSampleContext *s);
+
+
+/**
+ * Initialize an audio resampler.
+ * Note, if either rate is not an integer then simply scale both rates up so they are.
+ * @param filter_length length of each FIR filter in the filterbank relative to the cutoff freq
+ * @param log2_phase_count log2 of the number of entries in the polyphase filterbank
+ * @param linear If 1 then the used FIR filter will be linearly interpolated
+ between the 2 closest, if 0 the closest will be used
+ * @param cutoff cutoff frequency, 1.0 corresponds to half the output sampling rate
+ */
+attribute_deprecated
+struct AVResampleContext *av_resample_init(int out_rate, int in_rate, int filter_length, int log2_phase_count, int linear, double cutoff);
+
+/**
+ * Resample an array of samples using a previously configured context.
+ * @param src an array of unconsumed samples
+ * @param consumed the number of samples of src which have been consumed are returned here
+ * @param src_size the number of unconsumed samples available
+ * @param dst_size the amount of space in samples available in dst
+ * @param update_ctx If this is 0 then the context will not be modified, that way several channels can be resampled with the same context.
+ * @return the number of samples written in dst or -1 if an error occurred
+ */
+attribute_deprecated
+int av_resample(struct AVResampleContext *c, short *dst, short *src, int *consumed, int src_size, int dst_size, int update_ctx);
+
+
+/**
+ * Compensate samplerate/timestamp drift. The compensation is done by changing
+ * the resampler parameters, so no audible clicks or similar distortions occur
+ * @param compensation_distance distance in output samples over which the compensation should be performed
+ * @param sample_delta number of output samples which should be output less
+ *
+ * example: av_resample_compensate(c, 10, 500)
+ * here instead of 510 samples only 500 samples would be output
+ *
+ * note, due to rounding the actual compensation might be slightly different,
+ * especially if the compensation_distance is large and the in_rate used during init is small
+ */
+attribute_deprecated
+void av_resample_compensate(struct AVResampleContext *c, int sample_delta, int compensation_distance);
+attribute_deprecated
+void av_resample_close(struct AVResampleContext *c);
+
+/**
+ * @}
+ */
+#endif
+
+#if FF_API_AVPICTURE
+/**
+ * @addtogroup lavc_picture
+ * @{
+ */
+
+/**
+ * @deprecated unused
+ */
+attribute_deprecated
+int avpicture_alloc(AVPicture *picture, enum AVPixelFormat pix_fmt, int width, int height);
+
+/**
+ * @deprecated unused
+ */
+attribute_deprecated
+void avpicture_free(AVPicture *picture);
+
+/**
+ * @deprecated use av_image_fill_arrays() instead.
+ */
+attribute_deprecated
+int avpicture_fill(AVPicture *picture, const uint8_t *ptr,
+ enum AVPixelFormat pix_fmt, int width, int height);
+
+/**
+ * @deprecated use av_image_copy_to_buffer() instead.
+ */
+attribute_deprecated
+int avpicture_layout(const AVPicture *src, enum AVPixelFormat pix_fmt,
+ int width, int height,
+ unsigned char *dest, int dest_size);
+
+/**
+ * @deprecated use av_image_get_buffer_size() instead.
+ */
+attribute_deprecated
+int avpicture_get_size(enum AVPixelFormat pix_fmt, int width, int height);
+
+/**
+ * @deprecated av_image_copy() instead.
+ */
+attribute_deprecated
+void av_picture_copy(AVPicture *dst, const AVPicture *src,
+ enum AVPixelFormat pix_fmt, int width, int height);
+
+/**
+ * @deprecated unused
+ */
+attribute_deprecated
+int av_picture_crop(AVPicture *dst, const AVPicture *src,
+ enum AVPixelFormat pix_fmt, int top_band, int left_band);
+
+/**
+ * @deprecated unused
+ */
+attribute_deprecated
+int av_picture_pad(AVPicture *dst, const AVPicture *src, int height, int width, enum AVPixelFormat pix_fmt,
+ int padtop, int padbottom, int padleft, int padright, int *color);
+
+/**
+ * @}
+ */
+#endif
+
+/**
+ * @defgroup lavc_misc Utility functions
+ * @ingroup libavc
+ *
+ * Miscellaneous utility functions related to both encoding and decoding
+ * (or neither).
+ * @{
+ */
+
+/**
+ * @defgroup lavc_misc_pixfmt Pixel formats
+ *
+ * Functions for working with pixel formats.
+ * @{
+ */
+
+/**
+ * Utility function to access log2_chroma_w log2_chroma_h from
+ * the pixel format AVPixFmtDescriptor.
+ *
+ * This function asserts that pix_fmt is valid. See av_pix_fmt_get_chroma_sub_sample
+ * for one that returns a failure code and continues in case of invalid
+ * pix_fmts.
+ *
+ * @param[in] pix_fmt the pixel format
+ * @param[out] h_shift store log2_chroma_w
+ * @param[out] v_shift store log2_chroma_h
+ *
+ * @see av_pix_fmt_get_chroma_sub_sample
+ */
+
+void avcodec_get_chroma_sub_sample(enum AVPixelFormat pix_fmt, int *h_shift, int *v_shift);
+
+/**
+ * Return a value representing the fourCC code associated to the
+ * pixel format pix_fmt, or 0 if no associated fourCC code can be
+ * found.
+ */
+unsigned int avcodec_pix_fmt_to_codec_tag(enum AVPixelFormat pix_fmt);
+
+/**
+ * @deprecated see av_get_pix_fmt_loss()
+ */
+int avcodec_get_pix_fmt_loss(enum AVPixelFormat dst_pix_fmt, enum AVPixelFormat src_pix_fmt,
+ int has_alpha);
+
+/**
+ * Find the best pixel format to convert to given a certain source pixel
+ * format. When converting from one pixel format to another, information loss
+ * may occur. For example, when converting from RGB24 to GRAY, the color
+ * information will be lost. Similarly, other losses occur when converting from
+ * some formats to other formats. avcodec_find_best_pix_fmt_of_2() searches which of
+ * the given pixel formats should be used to suffer the least amount of loss.
+ * The pixel formats from which it chooses one, are determined by the
+ * pix_fmt_list parameter.
+ *
+ *
+ * @param[in] pix_fmt_list AV_PIX_FMT_NONE terminated array of pixel formats to choose from
+ * @param[in] src_pix_fmt source pixel format
+ * @param[in] has_alpha Whether the source pixel format alpha channel is used.
+ * @param[out] loss_ptr Combination of flags informing you what kind of losses will occur.
+ * @return The best pixel format to convert to or -1 if none was found.
+ */
+enum AVPixelFormat avcodec_find_best_pix_fmt_of_list(const enum AVPixelFormat *pix_fmt_list,
+ enum AVPixelFormat src_pix_fmt,
+ int has_alpha, int *loss_ptr);
+
+/**
+ * @deprecated see av_find_best_pix_fmt_of_2()
+ */
+enum AVPixelFormat avcodec_find_best_pix_fmt_of_2(enum AVPixelFormat dst_pix_fmt1, enum AVPixelFormat dst_pix_fmt2,
+ enum AVPixelFormat src_pix_fmt, int has_alpha, int *loss_ptr);
+
+attribute_deprecated
+#if AV_HAVE_INCOMPATIBLE_LIBAV_ABI
+enum AVPixelFormat avcodec_find_best_pix_fmt2(const enum AVPixelFormat *pix_fmt_list,
+ enum AVPixelFormat src_pix_fmt,
+ int has_alpha, int *loss_ptr);
+#else
+enum AVPixelFormat avcodec_find_best_pix_fmt2(enum AVPixelFormat dst_pix_fmt1, enum AVPixelFormat dst_pix_fmt2,
+ enum AVPixelFormat src_pix_fmt, int has_alpha, int *loss_ptr);
+#endif
+
+
+enum AVPixelFormat avcodec_default_get_format(struct AVCodecContext *s, const enum AVPixelFormat * fmt);
+
+/**
+ * @}
+ */
+
+#if FF_API_SET_DIMENSIONS
+/**
+ * @deprecated this function is not supposed to be used from outside of lavc
+ */
+attribute_deprecated
+void avcodec_set_dimensions(AVCodecContext *s, int width, int height);
+#endif
+
+/**
+ * Put a string representing the codec tag codec_tag in buf.
+ *
+ * @param buf buffer to place codec tag in
+ * @param buf_size size in bytes of buf
+ * @param codec_tag codec tag to assign
+ * @return the length of the string that would have been generated if
+ * enough space had been available, excluding the trailing null
+ */
+size_t av_get_codec_tag_string(char *buf, size_t buf_size, unsigned int codec_tag);
+
+void avcodec_string(char *buf, int buf_size, AVCodecContext *enc, int encode);
+
+/**
+ * Return a name for the specified profile, if available.
+ *
+ * @param codec the codec that is searched for the given profile
+ * @param profile the profile value for which a name is requested
+ * @return A name for the profile if found, NULL otherwise.
+ */
+const char *av_get_profile_name(const AVCodec *codec, int profile);
+
+/**
+ * Return a name for the specified profile, if available.
+ *
+ * @param codec_id the ID of the codec to which the requested profile belongs
+ * @param profile the profile value for which a name is requested
+ * @return A name for the profile if found, NULL otherwise.
+ *
+ * @note unlike av_get_profile_name(), which searches a list of profiles
+ * supported by a specific decoder or encoder implementation, this
+ * function searches the list of profiles from the AVCodecDescriptor
+ */
+const char *avcodec_profile_name(enum AVCodecID codec_id, int profile);
+
+int avcodec_default_execute(AVCodecContext *c, int (*func)(AVCodecContext *c2, void *arg2),void *arg, int *ret, int count, int size);
+int avcodec_default_execute2(AVCodecContext *c, int (*func)(AVCodecContext *c2, void *arg2, int, int),void *arg, int *ret, int count);
+//FIXME func typedef
+
+/**
+ * Fill AVFrame audio data and linesize pointers.
+ *
+ * The buffer buf must be a preallocated buffer with a size big enough
+ * to contain the specified samples amount. The filled AVFrame data
+ * pointers will point to this buffer.
+ *
+ * AVFrame extended_data channel pointers are allocated if necessary for
+ * planar audio.
+ *
+ * @param frame the AVFrame
+ * frame->nb_samples must be set prior to calling the
+ * function. This function fills in frame->data,
+ * frame->extended_data, frame->linesize[0].
+ * @param nb_channels channel count
+ * @param sample_fmt sample format
+ * @param buf buffer to use for frame data
+ * @param buf_size size of buffer
+ * @param align plane size sample alignment (0 = default)
+ * @return >=0 on success, negative error code on failure
+ * @todo return the size in bytes required to store the samples in
+ * case of success, at the next libavutil bump
+ */
+int avcodec_fill_audio_frame(AVFrame *frame, int nb_channels,
+ enum AVSampleFormat sample_fmt, const uint8_t *buf,
+ int buf_size, int align);
+
+/**
+ * Reset the internal decoder state / flush internal buffers. Should be called
+ * e.g. when seeking or when switching to a different stream.
+ *
+ * @note when refcounted frames are not used (i.e. avctx->refcounted_frames is 0),
+ * this invalidates the frames previously returned from the decoder. When
+ * refcounted frames are used, the decoder just releases any references it might
+ * keep internally, but the caller's reference remains valid.
+ */
+void avcodec_flush_buffers(AVCodecContext *avctx);
+
+/**
+ * Return codec bits per sample.
+ *
+ * @param[in] codec_id the codec
+ * @return Number of bits per sample or zero if unknown for the given codec.
+ */
+int av_get_bits_per_sample(enum AVCodecID codec_id);
+
+/**
+ * Return the PCM codec associated with a sample format.
+ * @param be endianness, 0 for little, 1 for big,
+ * -1 (or anything else) for native
+ * @return AV_CODEC_ID_PCM_* or AV_CODEC_ID_NONE
+ */
+enum AVCodecID av_get_pcm_codec(enum AVSampleFormat fmt, int be);
+
+/**
+ * Return codec bits per sample.
+ * Only return non-zero if the bits per sample is exactly correct, not an
+ * approximation.
+ *
+ * @param[in] codec_id the codec
+ * @return Number of bits per sample or zero if unknown for the given codec.
+ */
+int av_get_exact_bits_per_sample(enum AVCodecID codec_id);
+
+/**
+ * Return audio frame duration.
+ *
+ * @param avctx codec context
+ * @param frame_bytes size of the frame, or 0 if unknown
+ * @return frame duration, in samples, if known. 0 if not able to
+ * determine.
+ */
+int av_get_audio_frame_duration(AVCodecContext *avctx, int frame_bytes);
+
+
+typedef struct AVBitStreamFilterContext {
+ void *priv_data;
+ struct AVBitStreamFilter *filter;
+ AVCodecParserContext *parser;
+ struct AVBitStreamFilterContext *next;
+ /**
+ * Internal default arguments, used if NULL is passed to av_bitstream_filter_filter().
+ * Not for access by library users.
+ */
+ char *args;
+} AVBitStreamFilterContext;
+
+
+typedef struct AVBitStreamFilter {
+ const char *name;
+ int priv_data_size;
+ int (*filter)(AVBitStreamFilterContext *bsfc,
+ AVCodecContext *avctx, const char *args,
+ uint8_t **poutbuf, int *poutbuf_size,
+ const uint8_t *buf, int buf_size, int keyframe);
+ void (*close)(AVBitStreamFilterContext *bsfc);
+ struct AVBitStreamFilter *next;
+} AVBitStreamFilter;
+
+/**
+ * Register a bitstream filter.
+ *
+ * The filter will be accessible to the application code through
+ * av_bitstream_filter_next() or can be directly initialized with
+ * av_bitstream_filter_init().
+ *
+ * @see avcodec_register_all()
+ */
+void av_register_bitstream_filter(AVBitStreamFilter *bsf);
+
+/**
+ * Create and initialize a bitstream filter context given a bitstream
+ * filter name.
+ *
+ * The returned context must be freed with av_bitstream_filter_close().
+ *
+ * @param name the name of the bitstream filter
+ * @return a bitstream filter context if a matching filter was found
+ * and successfully initialized, NULL otherwise
+ */
+AVBitStreamFilterContext *av_bitstream_filter_init(const char *name);
+
+/**
+ * Filter bitstream.
+ *
+ * This function filters the buffer buf with size buf_size, and places the
+ * filtered buffer in the buffer pointed to by poutbuf.
+ *
+ * The output buffer must be freed by the caller.
+ *
+ * @param bsfc bitstream filter context created by av_bitstream_filter_init()
+ * @param avctx AVCodecContext accessed by the filter, may be NULL.
+ * If specified, this must point to the encoder context of the
+ * output stream the packet is sent to.
+ * @param args arguments which specify the filter configuration, may be NULL
+ * @param poutbuf pointer which is updated to point to the filtered buffer
+ * @param poutbuf_size pointer which is updated to the filtered buffer size in bytes
+ * @param buf buffer containing the data to filter
+ * @param buf_size size in bytes of buf
+ * @param keyframe set to non-zero if the buffer to filter corresponds to a key-frame packet data
+ * @return >= 0 in case of success, or a negative error code in case of failure
+ *
+ * If the return value is positive, an output buffer is allocated and
+ * is available in *poutbuf, and is distinct from the input buffer.
+ *
+ * If the return value is 0, the output buffer is not allocated and
+ * should be considered identical to the input buffer, or in case
+ * *poutbuf was set it points to the input buffer (not necessarily to
+ * its starting address).
+ */
+int av_bitstream_filter_filter(AVBitStreamFilterContext *bsfc,
+ AVCodecContext *avctx, const char *args,
+ uint8_t **poutbuf, int *poutbuf_size,
+ const uint8_t *buf, int buf_size, int keyframe);
+
+/**
+ * Release bitstream filter context.
+ *
+ * @param bsf the bitstream filter context created with
+ * av_bitstream_filter_init(), can be NULL
+ */
+void av_bitstream_filter_close(AVBitStreamFilterContext *bsf);
+
+/**
+ * If f is NULL, return the first registered bitstream filter,
+ * if f is non-NULL, return the next registered bitstream filter
+ * after f, or NULL if f is the last one.
+ *
+ * This function can be used to iterate over all registered bitstream
+ * filters.
+ */
+AVBitStreamFilter *av_bitstream_filter_next(const AVBitStreamFilter *f);
+
+/* memory */
+
+/**
+ * Same behaviour av_fast_malloc but the buffer has additional
+ * AV_INPUT_BUFFER_PADDING_SIZE at the end which will always be 0.
+ *
+ * In addition the whole buffer will initially and after resizes
+ * be 0-initialized so that no uninitialized data will ever appear.
+ */
+void av_fast_padded_malloc(void *ptr, unsigned int *size, size_t min_size);
+
+/**
+ * Same behaviour av_fast_padded_malloc except that buffer will always
+ * be 0-initialized after call.
+ */
+void av_fast_padded_mallocz(void *ptr, unsigned int *size, size_t min_size);
+
+/**
+ * Encode extradata length to a buffer. Used by xiph codecs.
+ *
+ * @param s buffer to write to; must be at least (v/255+1) bytes long
+ * @param v size of extradata in bytes
+ * @return number of bytes written to the buffer.
+ */
+unsigned int av_xiphlacing(unsigned char *s, unsigned int v);
+
+#if FF_API_MISSING_SAMPLE
+/**
+ * Log a generic warning message about a missing feature. This function is
+ * intended to be used internally by FFmpeg (libavcodec, libavformat, etc.)
+ * only, and would normally not be used by applications.
+ * @param[in] avc a pointer to an arbitrary struct of which the first field is
+ * a pointer to an AVClass struct
+ * @param[in] feature string containing the name of the missing feature
+ * @param[in] want_sample indicates if samples are wanted which exhibit this feature.
+ * If want_sample is non-zero, additional verbage will be added to the log
+ * message which tells the user how to report samples to the development
+ * mailing list.
+ * @deprecated Use avpriv_report_missing_feature() instead.
+ */
+attribute_deprecated
+void av_log_missing_feature(void *avc, const char *feature, int want_sample);
+
+/**
+ * Log a generic warning message asking for a sample. This function is
+ * intended to be used internally by FFmpeg (libavcodec, libavformat, etc.)
+ * only, and would normally not be used by applications.
+ * @param[in] avc a pointer to an arbitrary struct of which the first field is
+ * a pointer to an AVClass struct
+ * @param[in] msg string containing an optional message, or NULL if no message
+ * @deprecated Use avpriv_request_sample() instead.
+ */
+attribute_deprecated
+void av_log_ask_for_sample(void *avc, const char *msg, ...) av_printf_format(2, 3);
+#endif /* FF_API_MISSING_SAMPLE */
+
+/**
+ * Register the hardware accelerator hwaccel.
+ */
+void av_register_hwaccel(AVHWAccel *hwaccel);
+
+/**
+ * If hwaccel is NULL, returns the first registered hardware accelerator,
+ * if hwaccel is non-NULL, returns the next registered hardware accelerator
+ * after hwaccel, or NULL if hwaccel is the last one.
+ */
+AVHWAccel *av_hwaccel_next(const AVHWAccel *hwaccel);
+
+
+/**
+ * Lock operation used by lockmgr
+ */
+enum AVLockOp {
+ AV_LOCK_CREATE, ///< Create a mutex
+ AV_LOCK_OBTAIN, ///< Lock the mutex
+ AV_LOCK_RELEASE, ///< Unlock the mutex
+ AV_LOCK_DESTROY, ///< Free mutex resources
+};
+
+/**
+ * Register a user provided lock manager supporting the operations
+ * specified by AVLockOp. The "mutex" argument to the function points
+ * to a (void *) where the lockmgr should store/get a pointer to a user
+ * allocated mutex. It is NULL upon AV_LOCK_CREATE and equal to the
+ * value left by the last call for all other ops. If the lock manager is
+ * unable to perform the op then it should leave the mutex in the same
+ * state as when it was called and return a non-zero value. However,
+ * when called with AV_LOCK_DESTROY the mutex will always be assumed to
+ * have been successfully destroyed. If av_lockmgr_register succeeds
+ * it will return a non-negative value, if it fails it will return a
+ * negative value and destroy all mutex and unregister all callbacks.
+ * av_lockmgr_register is not thread-safe, it must be called from a
+ * single thread before any calls which make use of locking are used.
+ *
+ * @param cb User defined callback. av_lockmgr_register invokes calls
+ * to this callback and the previously registered callback.
+ * The callback will be used to create more than one mutex
+ * each of which must be backed by its own underlying locking
+ * mechanism (i.e. do not use a single static object to
+ * implement your lock manager). If cb is set to NULL the
+ * lockmgr will be unregistered.
+ */
+int av_lockmgr_register(int (*cb)(void **mutex, enum AVLockOp op));
+
+/**
+ * Get the type of the given codec.
+ */
+enum AVMediaType avcodec_get_type(enum AVCodecID codec_id);
+
+/**
+ * Get the name of a codec.
+ * @return a static string identifying the codec; never NULL
+ */
+const char *avcodec_get_name(enum AVCodecID id);
+
+/**
+ * @return a positive value if s is open (i.e. avcodec_open2() was called on it
+ * with no corresponding avcodec_close()), 0 otherwise.
+ */
+int avcodec_is_open(AVCodecContext *s);
+
+/**
+ * @return a non-zero number if codec is an encoder, zero otherwise
+ */
+int av_codec_is_encoder(const AVCodec *codec);
+
+/**
+ * @return a non-zero number if codec is a decoder, zero otherwise
+ */
+int av_codec_is_decoder(const AVCodec *codec);
+
+/**
+ * @return descriptor for given codec ID or NULL if no descriptor exists.
+ */
+const AVCodecDescriptor *avcodec_descriptor_get(enum AVCodecID id);
+
+/**
+ * Iterate over all codec descriptors known to libavcodec.
+ *
+ * @param prev previous descriptor. NULL to get the first descriptor.
+ *
+ * @return next descriptor or NULL after the last descriptor
+ */
+const AVCodecDescriptor *avcodec_descriptor_next(const AVCodecDescriptor *prev);
+
+/**
+ * @return codec descriptor with the given name or NULL if no such descriptor
+ * exists.
+ */
+const AVCodecDescriptor *avcodec_descriptor_get_by_name(const char *name);
+
+/**
+ * Allocate a CPB properties structure and initialize its fields to default
+ * values.
+ *
+ * @param size if non-NULL, the size of the allocated struct will be written
+ * here. This is useful for embedding it in side data.
+ *
+ * @return the newly allocated struct or NULL on failure
+ */
+AVCPBProperties *av_cpb_properties_alloc(size_t *size);
+
+/**
+ * @}
+ */
+
+#endif /* AVCODEC_AVCODEC_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg57/include/libavcodec/avfft.h b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavcodec/avfft.h
new file mode 100644
index 0000000000..0c0f9b8d8d
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavcodec/avfft.h
@@ -0,0 +1,118 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_AVFFT_H
+#define AVCODEC_AVFFT_H
+
+/**
+ * @file
+ * @ingroup lavc_fft
+ * FFT functions
+ */
+
+/**
+ * @defgroup lavc_fft FFT functions
+ * @ingroup lavc_misc
+ *
+ * @{
+ */
+
+typedef float FFTSample;
+
+typedef struct FFTComplex {
+ FFTSample re, im;
+} FFTComplex;
+
+typedef struct FFTContext FFTContext;
+
+/**
+ * Set up a complex FFT.
+ * @param nbits log2 of the length of the input array
+ * @param inverse if 0 perform the forward transform, if 1 perform the inverse
+ */
+FFTContext *av_fft_init(int nbits, int inverse);
+
+/**
+ * Do the permutation needed BEFORE calling ff_fft_calc().
+ */
+void av_fft_permute(FFTContext *s, FFTComplex *z);
+
+/**
+ * Do a complex FFT with the parameters defined in av_fft_init(). The
+ * input data must be permuted before. No 1.0/sqrt(n) normalization is done.
+ */
+void av_fft_calc(FFTContext *s, FFTComplex *z);
+
+void av_fft_end(FFTContext *s);
+
+FFTContext *av_mdct_init(int nbits, int inverse, double scale);
+void av_imdct_calc(FFTContext *s, FFTSample *output, const FFTSample *input);
+void av_imdct_half(FFTContext *s, FFTSample *output, const FFTSample *input);
+void av_mdct_calc(FFTContext *s, FFTSample *output, const FFTSample *input);
+void av_mdct_end(FFTContext *s);
+
+/* Real Discrete Fourier Transform */
+
+enum RDFTransformType {
+ DFT_R2C,
+ IDFT_C2R,
+ IDFT_R2C,
+ DFT_C2R,
+};
+
+typedef struct RDFTContext RDFTContext;
+
+/**
+ * Set up a real FFT.
+ * @param nbits log2 of the length of the input array
+ * @param trans the type of transform
+ */
+RDFTContext *av_rdft_init(int nbits, enum RDFTransformType trans);
+void av_rdft_calc(RDFTContext *s, FFTSample *data);
+void av_rdft_end(RDFTContext *s);
+
+/* Discrete Cosine Transform */
+
+typedef struct DCTContext DCTContext;
+
+enum DCTTransformType {
+ DCT_II = 0,
+ DCT_III,
+ DCT_I,
+ DST_I,
+};
+
+/**
+ * Set up DCT.
+ *
+ * @param nbits size of the input array:
+ * (1 << nbits) for DCT-II, DCT-III and DST-I
+ * (1 << nbits) + 1 for DCT-I
+ * @param type the type of transform
+ *
+ * @note the first element of the input of DST-I is ignored
+ */
+DCTContext *av_dct_init(int nbits, enum DCTTransformType type);
+void av_dct_calc(DCTContext *s, FFTSample *data);
+void av_dct_end (DCTContext *s);
+
+/**
+ * @}
+ */
+
+#endif /* AVCODEC_AVFFT_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg57/include/libavcodec/vaapi.h b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavcodec/vaapi.h
new file mode 100644
index 0000000000..7a29f6f881
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavcodec/vaapi.h
@@ -0,0 +1,189 @@
+/*
+ * Video Acceleration API (shared data between FFmpeg and the video player)
+ * HW decode acceleration for MPEG-2, MPEG-4, H.264 and VC-1
+ *
+ * Copyright (C) 2008-2009 Splitted-Desktop Systems
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_VAAPI_H
+#define AVCODEC_VAAPI_H
+
+/**
+ * @file
+ * @ingroup lavc_codec_hwaccel_vaapi
+ * Public libavcodec VA API header.
+ */
+
+#include <stdint.h>
+#include "libavutil/attributes.h"
+#include "version.h"
+
+/**
+ * @defgroup lavc_codec_hwaccel_vaapi VA API Decoding
+ * @ingroup lavc_codec_hwaccel
+ * @{
+ */
+
+/**
+ * This structure is used to share data between the FFmpeg library and
+ * the client video application.
+ * This shall be zero-allocated and available as
+ * AVCodecContext.hwaccel_context. All user members can be set once
+ * during initialization or through each AVCodecContext.get_buffer()
+ * function call. In any case, they must be valid prior to calling
+ * decoding functions.
+ */
+struct vaapi_context {
+ /**
+ * Window system dependent data
+ *
+ * - encoding: unused
+ * - decoding: Set by user
+ */
+ void *display;
+
+ /**
+ * Configuration ID
+ *
+ * - encoding: unused
+ * - decoding: Set by user
+ */
+ uint32_t config_id;
+
+ /**
+ * Context ID (video decode pipeline)
+ *
+ * - encoding: unused
+ * - decoding: Set by user
+ */
+ uint32_t context_id;
+
+#if FF_API_VAAPI_CONTEXT
+ /**
+ * VAPictureParameterBuffer ID
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ attribute_deprecated
+ uint32_t pic_param_buf_id;
+
+ /**
+ * VAIQMatrixBuffer ID
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ attribute_deprecated
+ uint32_t iq_matrix_buf_id;
+
+ /**
+ * VABitPlaneBuffer ID (for VC-1 decoding)
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ attribute_deprecated
+ uint32_t bitplane_buf_id;
+
+ /**
+ * Slice parameter/data buffer IDs
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ attribute_deprecated
+ uint32_t *slice_buf_ids;
+
+ /**
+ * Number of effective slice buffer IDs to send to the HW
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ attribute_deprecated
+ unsigned int n_slice_buf_ids;
+
+ /**
+ * Size of pre-allocated slice_buf_ids
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ attribute_deprecated
+ unsigned int slice_buf_ids_alloc;
+
+ /**
+ * Pointer to VASliceParameterBuffers
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ attribute_deprecated
+ void *slice_params;
+
+ /**
+ * Size of a VASliceParameterBuffer element
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ attribute_deprecated
+ unsigned int slice_param_size;
+
+ /**
+ * Size of pre-allocated slice_params
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ attribute_deprecated
+ unsigned int slice_params_alloc;
+
+ /**
+ * Number of slices currently filled in
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ attribute_deprecated
+ unsigned int slice_count;
+
+ /**
+ * Pointer to slice data buffer base
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ attribute_deprecated
+ const uint8_t *slice_data;
+
+ /**
+ * Current size of slice data
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ attribute_deprecated
+ uint32_t slice_data_size;
+#endif
+};
+
+/* @} */
+
+#endif /* AVCODEC_VAAPI_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg57/include/libavcodec/vdpau.h b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavcodec/vdpau.h
new file mode 100644
index 0000000000..e85e4d9e9a
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavcodec/vdpau.h
@@ -0,0 +1,253 @@
+/*
+ * The Video Decode and Presentation API for UNIX (VDPAU) is used for
+ * hardware-accelerated decoding of MPEG-1/2, H.264 and VC-1.
+ *
+ * Copyright (C) 2008 NVIDIA
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_VDPAU_H
+#define AVCODEC_VDPAU_H
+
+/**
+ * @file
+ * @ingroup lavc_codec_hwaccel_vdpau
+ * Public libavcodec VDPAU header.
+ */
+
+
+/**
+ * @defgroup lavc_codec_hwaccel_vdpau VDPAU Decoder and Renderer
+ * @ingroup lavc_codec_hwaccel
+ *
+ * VDPAU hardware acceleration has two modules
+ * - VDPAU decoding
+ * - VDPAU presentation
+ *
+ * The VDPAU decoding module parses all headers using FFmpeg
+ * parsing mechanisms and uses VDPAU for the actual decoding.
+ *
+ * As per the current implementation, the actual decoding
+ * and rendering (API calls) are done as part of the VDPAU
+ * presentation (vo_vdpau.c) module.
+ *
+ * @{
+ */
+
+#include <vdpau/vdpau.h>
+#include <vdpau/vdpau_x11.h>
+#include "libavutil/avconfig.h"
+#include "libavutil/attributes.h"
+
+#include "avcodec.h"
+#include "version.h"
+
+#if FF_API_BUFS_VDPAU
+union AVVDPAUPictureInfo {
+ VdpPictureInfoH264 h264;
+ VdpPictureInfoMPEG1Or2 mpeg;
+ VdpPictureInfoVC1 vc1;
+ VdpPictureInfoMPEG4Part2 mpeg4;
+};
+#endif
+
+struct AVCodecContext;
+struct AVFrame;
+
+typedef int (*AVVDPAU_Render2)(struct AVCodecContext *, struct AVFrame *,
+ const VdpPictureInfo *, uint32_t,
+ const VdpBitstreamBuffer *);
+
+/**
+ * This structure is used to share data between the libavcodec library and
+ * the client video application.
+ * The user shall allocate the structure via the av_alloc_vdpau_hwaccel
+ * function and make it available as
+ * AVCodecContext.hwaccel_context. Members can be set by the user once
+ * during initialization or through each AVCodecContext.get_buffer()
+ * function call. In any case, they must be valid prior to calling
+ * decoding functions.
+ *
+ * The size of this structure is not a part of the public ABI and must not
+ * be used outside of libavcodec. Use av_vdpau_alloc_context() to allocate an
+ * AVVDPAUContext.
+ */
+typedef struct AVVDPAUContext {
+ /**
+ * VDPAU decoder handle
+ *
+ * Set by user.
+ */
+ VdpDecoder decoder;
+
+ /**
+ * VDPAU decoder render callback
+ *
+ * Set by the user.
+ */
+ VdpDecoderRender *render;
+
+#if FF_API_BUFS_VDPAU
+ /**
+ * VDPAU picture information
+ *
+ * Set by libavcodec.
+ */
+ attribute_deprecated
+ union AVVDPAUPictureInfo info;
+
+ /**
+ * Allocated size of the bitstream_buffers table.
+ *
+ * Set by libavcodec.
+ */
+ attribute_deprecated
+ int bitstream_buffers_allocated;
+
+ /**
+ * Useful bitstream buffers in the bitstream buffers table.
+ *
+ * Set by libavcodec.
+ */
+ attribute_deprecated
+ int bitstream_buffers_used;
+
+ /**
+ * Table of bitstream buffers.
+ * The user is responsible for freeing this buffer using av_freep().
+ *
+ * Set by libavcodec.
+ */
+ attribute_deprecated
+ VdpBitstreamBuffer *bitstream_buffers;
+#endif
+ AVVDPAU_Render2 render2;
+} AVVDPAUContext;
+
+/**
+ * @brief allocation function for AVVDPAUContext
+ *
+ * Allows extending the struct without breaking API/ABI
+ */
+AVVDPAUContext *av_alloc_vdpaucontext(void);
+
+AVVDPAU_Render2 av_vdpau_hwaccel_get_render2(const AVVDPAUContext *);
+void av_vdpau_hwaccel_set_render2(AVVDPAUContext *, AVVDPAU_Render2);
+
+/**
+ * Associate a VDPAU device with a codec context for hardware acceleration.
+ * This function is meant to be called from the get_format() codec callback,
+ * or earlier. It can also be called after avcodec_flush_buffers() to change
+ * the underlying VDPAU device mid-stream (e.g. to recover from non-transparent
+ * display preemption).
+ *
+ * @note get_format() must return AV_PIX_FMT_VDPAU if this function completes
+ * successfully.
+ *
+ * @param avctx decoding context whose get_format() callback is invoked
+ * @param device VDPAU device handle to use for hardware acceleration
+ * @param get_proc_address VDPAU device driver
+ * @param flags zero of more OR'd AV_HWACCEL_FLAG_* flags
+ *
+ * @return 0 on success, an AVERROR code on failure.
+ */
+int av_vdpau_bind_context(AVCodecContext *avctx, VdpDevice device,
+ VdpGetProcAddress *get_proc_address, unsigned flags);
+
+/**
+ * Gets the parameters to create an adequate VDPAU video surface for the codec
+ * context using VDPAU hardware decoding acceleration.
+ *
+ * @note Behavior is undefined if the context was not successfully bound to a
+ * VDPAU device using av_vdpau_bind_context().
+ *
+ * @param avctx the codec context being used for decoding the stream
+ * @param type storage space for the VDPAU video surface chroma type
+ * (or NULL to ignore)
+ * @param width storage space for the VDPAU video surface pixel width
+ * (or NULL to ignore)
+ * @param height storage space for the VDPAU video surface pixel height
+ * (or NULL to ignore)
+ *
+ * @return 0 on success, a negative AVERROR code on failure.
+ */
+int av_vdpau_get_surface_parameters(AVCodecContext *avctx, VdpChromaType *type,
+ uint32_t *width, uint32_t *height);
+
+/**
+ * Allocate an AVVDPAUContext.
+ *
+ * @return Newly-allocated AVVDPAUContext or NULL on failure.
+ */
+AVVDPAUContext *av_vdpau_alloc_context(void);
+
+#if FF_API_VDPAU_PROFILE
+/**
+ * Get a decoder profile that should be used for initializing a VDPAU decoder.
+ * Should be called from the AVCodecContext.get_format() callback.
+ *
+ * @deprecated Use av_vdpau_bind_context() instead.
+ *
+ * @param avctx the codec context being used for decoding the stream
+ * @param profile a pointer into which the result will be written on success.
+ * The contents of profile are undefined if this function returns
+ * an error.
+ *
+ * @return 0 on success (non-negative), a negative AVERROR on failure.
+ */
+attribute_deprecated
+int av_vdpau_get_profile(AVCodecContext *avctx, VdpDecoderProfile *profile);
+#endif
+
+#if FF_API_CAP_VDPAU
+/** @brief The videoSurface is used for rendering. */
+#define FF_VDPAU_STATE_USED_FOR_RENDER 1
+
+/**
+ * @brief The videoSurface is needed for reference/prediction.
+ * The codec manipulates this.
+ */
+#define FF_VDPAU_STATE_USED_FOR_REFERENCE 2
+
+/**
+ * @brief This structure is used as a callback between the FFmpeg
+ * decoder (vd_) and presentation (vo_) module.
+ * This is used for defining a video frame containing surface,
+ * picture parameter, bitstream information etc which are passed
+ * between the FFmpeg decoder and its clients.
+ */
+struct vdpau_render_state {
+ VdpVideoSurface surface; ///< Used as rendered surface, never changed.
+
+ int state; ///< Holds FF_VDPAU_STATE_* values.
+
+ /** picture parameter information for all supported codecs */
+ union AVVDPAUPictureInfo info;
+
+ /** Describe size/location of the compressed video data.
+ Set to 0 when freeing bitstream_buffers. */
+ int bitstream_buffers_allocated;
+ int bitstream_buffers_used;
+ /** The user is responsible for freeing this buffer using av_freep(). */
+ VdpBitstreamBuffer *bitstream_buffers;
+};
+#endif
+
+/* @}*/
+
+#endif /* AVCODEC_VDPAU_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg57/include/libavcodec/version.h b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavcodec/version.h
new file mode 100644
index 0000000000..2b01e6f916
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavcodec/version.h
@@ -0,0 +1,210 @@
+/*
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_VERSION_H
+#define AVCODEC_VERSION_H
+
+/**
+ * @file
+ * @ingroup libavc
+ * Libavcodec version macros.
+ */
+
+#include "libavutil/version.h"
+
+#define LIBAVCODEC_VERSION_MAJOR 57
+#define LIBAVCODEC_VERSION_MINOR 22
+#define LIBAVCODEC_VERSION_MICRO 100
+
+#define LIBAVCODEC_VERSION_INT AV_VERSION_INT(LIBAVCODEC_VERSION_MAJOR, \
+ LIBAVCODEC_VERSION_MINOR, \
+ LIBAVCODEC_VERSION_MICRO)
+#define LIBAVCODEC_VERSION AV_VERSION(LIBAVCODEC_VERSION_MAJOR, \
+ LIBAVCODEC_VERSION_MINOR, \
+ LIBAVCODEC_VERSION_MICRO)
+#define LIBAVCODEC_BUILD LIBAVCODEC_VERSION_INT
+
+#define LIBAVCODEC_IDENT "Lavc" AV_STRINGIFY(LIBAVCODEC_VERSION)
+
+/**
+ * FF_API_* defines may be placed below to indicate public API that will be
+ * dropped at a future version bump. The defines themselves are not part of
+ * the public API and may change, break or disappear at any time.
+ *
+ * @note, when bumping the major version it is recommended to manually
+ * disable each FF_API_* in its own commit instead of disabling them all
+ * at once through the bump. This improves the git bisect-ability of the change.
+ */
+
+#ifndef FF_API_VIMA_DECODER
+#define FF_API_VIMA_DECODER (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_AUDIO_CONVERT
+#define FF_API_AUDIO_CONVERT (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_AVCODEC_RESAMPLE
+#define FF_API_AVCODEC_RESAMPLE FF_API_AUDIO_CONVERT
+#endif
+#ifndef FF_API_MISSING_SAMPLE
+#define FF_API_MISSING_SAMPLE (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_LOWRES
+#define FF_API_LOWRES (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_CAP_VDPAU
+#define FF_API_CAP_VDPAU (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_BUFS_VDPAU
+#define FF_API_BUFS_VDPAU (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_VOXWARE
+#define FF_API_VOXWARE (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_SET_DIMENSIONS
+#define FF_API_SET_DIMENSIONS (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_DEBUG_MV
+#define FF_API_DEBUG_MV (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_AC_VLC
+#define FF_API_AC_VLC (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_OLD_MSMPEG4
+#define FF_API_OLD_MSMPEG4 (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_ASPECT_EXTENDED
+#define FF_API_ASPECT_EXTENDED (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_ARCH_ALPHA
+#define FF_API_ARCH_ALPHA (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_XVMC
+#define FF_API_XVMC (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_ERROR_RATE
+#define FF_API_ERROR_RATE (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_QSCALE_TYPE
+#define FF_API_QSCALE_TYPE (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_MB_TYPE
+#define FF_API_MB_TYPE (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_MAX_BFRAMES
+#define FF_API_MAX_BFRAMES (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_NEG_LINESIZES
+#define FF_API_NEG_LINESIZES (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_EMU_EDGE
+#define FF_API_EMU_EDGE (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_ARCH_SH4
+#define FF_API_ARCH_SH4 (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_ARCH_SPARC
+#define FF_API_ARCH_SPARC (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_UNUSED_MEMBERS
+#define FF_API_UNUSED_MEMBERS (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_IDCT_XVIDMMX
+#define FF_API_IDCT_XVIDMMX (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_INPUT_PRESERVED
+#define FF_API_INPUT_PRESERVED (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_NORMALIZE_AQP
+#define FF_API_NORMALIZE_AQP (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_GMC
+#define FF_API_GMC (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_MV0
+#define FF_API_MV0 (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_CODEC_NAME
+#define FF_API_CODEC_NAME (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_AFD
+#define FF_API_AFD (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_VISMV
+/* XXX: don't forget to drop the -vismv documentation */
+#define FF_API_VISMV (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_AUDIOENC_DELAY
+#define FF_API_AUDIOENC_DELAY (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_VAAPI_CONTEXT
+#define FF_API_VAAPI_CONTEXT (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_AVCTX_TIMEBASE
+#define FF_API_AVCTX_TIMEBASE (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_MPV_OPT
+#define FF_API_MPV_OPT (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_STREAM_CODEC_TAG
+#define FF_API_STREAM_CODEC_TAG (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_QUANT_BIAS
+#define FF_API_QUANT_BIAS (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_RC_STRATEGY
+#define FF_API_RC_STRATEGY (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_CODED_FRAME
+#define FF_API_CODED_FRAME (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_MOTION_EST
+#define FF_API_MOTION_EST (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_WITHOUT_PREFIX
+#define FF_API_WITHOUT_PREFIX (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_SIDEDATA_ONLY_PKT
+#define FF_API_SIDEDATA_ONLY_PKT (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_VDPAU_PROFILE
+#define FF_API_VDPAU_PROFILE (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_CONVERGENCE_DURATION
+#define FF_API_CONVERGENCE_DURATION (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_AVPICTURE
+#define FF_API_AVPICTURE (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_AVPACKET_OLD_API
+#define FF_API_AVPACKET_OLD_API (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_RTP_CALLBACK
+#define FF_API_RTP_CALLBACK (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_VBV_DELAY
+#define FF_API_VBV_DELAY (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_CODER_TYPE
+#define FF_API_CODER_TYPE (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_STAT_BITS
+#define FF_API_STAT_BITS (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+
+#endif /* AVCODEC_VERSION_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/attributes.h b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/attributes.h
new file mode 100644
index 0000000000..5c6b9deecb
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/attributes.h
@@ -0,0 +1,168 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * Macro definitions for various function/variable attributes
+ */
+
+#ifndef AVUTIL_ATTRIBUTES_H
+#define AVUTIL_ATTRIBUTES_H
+
+#ifdef __GNUC__
+# define AV_GCC_VERSION_AT_LEAST(x,y) (__GNUC__ > (x) || __GNUC__ == (x) && __GNUC_MINOR__ >= (y))
+# define AV_GCC_VERSION_AT_MOST(x,y) (__GNUC__ < (x) || __GNUC__ == (x) && __GNUC_MINOR__ <= (y))
+#else
+# define AV_GCC_VERSION_AT_LEAST(x,y) 0
+# define AV_GCC_VERSION_AT_MOST(x,y) 0
+#endif
+
+#ifndef av_always_inline
+#if AV_GCC_VERSION_AT_LEAST(3,1)
+# define av_always_inline __attribute__((always_inline)) inline
+#elif defined(_MSC_VER)
+# define av_always_inline __forceinline
+#else
+# define av_always_inline inline
+#endif
+#endif
+
+#ifndef av_extern_inline
+#if defined(__ICL) && __ICL >= 1210 || defined(__GNUC_STDC_INLINE__)
+# define av_extern_inline extern inline
+#else
+# define av_extern_inline inline
+#endif
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3,4)
+# define av_warn_unused_result __attribute__((warn_unused_result))
+#else
+# define av_warn_unused_result
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3,1)
+# define av_noinline __attribute__((noinline))
+#elif defined(_MSC_VER)
+# define av_noinline __declspec(noinline)
+#else
+# define av_noinline
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3,1)
+# define av_pure __attribute__((pure))
+#else
+# define av_pure
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(2,6)
+# define av_const __attribute__((const))
+#else
+# define av_const
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(4,3)
+# define av_cold __attribute__((cold))
+#else
+# define av_cold
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(4,1) && !defined(__llvm__)
+# define av_flatten __attribute__((flatten))
+#else
+# define av_flatten
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3,1)
+# define attribute_deprecated __attribute__((deprecated))
+#elif defined(_MSC_VER)
+# define attribute_deprecated __declspec(deprecated)
+#else
+# define attribute_deprecated
+#endif
+
+/**
+ * Disable warnings about deprecated features
+ * This is useful for sections of code kept for backward compatibility and
+ * scheduled for removal.
+ */
+#ifndef AV_NOWARN_DEPRECATED
+#if AV_GCC_VERSION_AT_LEAST(4,6)
+# define AV_NOWARN_DEPRECATED(code) \
+ _Pragma("GCC diagnostic push") \
+ _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"") \
+ code \
+ _Pragma("GCC diagnostic pop")
+#elif defined(_MSC_VER)
+# define AV_NOWARN_DEPRECATED(code) \
+ __pragma(warning(push)) \
+ __pragma(warning(disable : 4996)) \
+ code; \
+ __pragma(warning(pop))
+#else
+# define AV_NOWARN_DEPRECATED(code) code
+#endif
+#endif
+
+
+#if defined(__GNUC__)
+# define av_unused __attribute__((unused))
+#else
+# define av_unused
+#endif
+
+/**
+ * Mark a variable as used and prevent the compiler from optimizing it
+ * away. This is useful for variables accessed only from inline
+ * assembler without the compiler being aware.
+ */
+#if AV_GCC_VERSION_AT_LEAST(3,1)
+# define av_used __attribute__((used))
+#else
+# define av_used
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3,3)
+# define av_alias __attribute__((may_alias))
+#else
+# define av_alias
+#endif
+
+#if defined(__GNUC__) && !defined(__INTEL_COMPILER) && !defined(__clang__)
+# define av_uninit(x) x=x
+#else
+# define av_uninit(x) x
+#endif
+
+#ifdef __GNUC__
+# define av_builtin_constant_p __builtin_constant_p
+# define av_printf_format(fmtpos, attrpos) __attribute__((__format__(__printf__, fmtpos, attrpos)))
+#else
+# define av_builtin_constant_p(x) 0
+# define av_printf_format(fmtpos, attrpos)
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(2,5)
+# define av_noreturn __attribute__((noreturn))
+#else
+# define av_noreturn
+#endif
+
+#endif /* AVUTIL_ATTRIBUTES_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/avconfig.h b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/avconfig.h
new file mode 100644
index 0000000000..36a8cd14da
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/avconfig.h
@@ -0,0 +1,7 @@
+/* Generated by ffconf */
+#ifndef AVUTIL_AVCONFIG_H
+#define AVUTIL_AVCONFIG_H
+#define AV_HAVE_BIGENDIAN 0
+#define AV_HAVE_FAST_UNALIGNED 1
+#define AV_HAVE_INCOMPATIBLE_LIBAV_ABI 0
+#endif /* AVUTIL_AVCONFIG_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/avutil.h b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/avutil.h
new file mode 100644
index 0000000000..9bcf674126
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/avutil.h
@@ -0,0 +1,343 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_AVUTIL_H
+#define AVUTIL_AVUTIL_H
+
+/**
+ * @file
+ * external API header
+ */
+
+/**
+ * @mainpage
+ *
+ * @section ffmpeg_intro Introduction
+ *
+ * This document describes the usage of the different libraries
+ * provided by FFmpeg.
+ *
+ * @li @ref libavc "libavcodec" encoding/decoding library
+ * @li @ref lavfi "libavfilter" graph-based frame editing library
+ * @li @ref libavf "libavformat" I/O and muxing/demuxing library
+ * @li @ref lavd "libavdevice" special devices muxing/demuxing library
+ * @li @ref lavu "libavutil" common utility library
+ * @li @ref lswr "libswresample" audio resampling, format conversion and mixing
+ * @li @ref lpp "libpostproc" post processing library
+ * @li @ref libsws "libswscale" color conversion and scaling library
+ *
+ * @section ffmpeg_versioning Versioning and compatibility
+ *
+ * Each of the FFmpeg libraries contains a version.h header, which defines a
+ * major, minor and micro version number with the
+ * <em>LIBRARYNAME_VERSION_{MAJOR,MINOR,MICRO}</em> macros. The major version
+ * number is incremented with backward incompatible changes - e.g. removing
+ * parts of the public API, reordering public struct members, etc. The minor
+ * version number is incremented for backward compatible API changes or major
+ * new features - e.g. adding a new public function or a new decoder. The micro
+ * version number is incremented for smaller changes that a calling program
+ * might still want to check for - e.g. changing behavior in a previously
+ * unspecified situation.
+ *
+ * FFmpeg guarantees backward API and ABI compatibility for each library as long
+ * as its major version number is unchanged. This means that no public symbols
+ * will be removed or renamed. Types and names of the public struct members and
+ * values of public macros and enums will remain the same (unless they were
+ * explicitly declared as not part of the public API). Documented behavior will
+ * not change.
+ *
+ * In other words, any correct program that works with a given FFmpeg snapshot
+ * should work just as well without any changes with any later snapshot with the
+ * same major versions. This applies to both rebuilding the program against new
+ * FFmpeg versions or to replacing the dynamic FFmpeg libraries that a program
+ * links against.
+ *
+ * However, new public symbols may be added and new members may be appended to
+ * public structs whose size is not part of public ABI (most public structs in
+ * FFmpeg). New macros and enum values may be added. Behavior in undocumented
+ * situations may change slightly (and be documented). All those are accompanied
+ * by an entry in doc/APIchanges and incrementing either the minor or micro
+ * version number.
+ */
+
+/**
+ * @defgroup lavu Common utility functions
+ *
+ * @brief
+ * libavutil contains the code shared across all the other FFmpeg
+ * libraries
+ *
+ * @note In order to use the functions provided by avutil you must include
+ * the specific header.
+ *
+ * @{
+ *
+ * @defgroup lavu_crypto Crypto and Hashing
+ *
+ * @{
+ * @}
+ *
+ * @defgroup lavu_math Maths
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_string String Manipulation
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_mem Memory Management
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_data Data Structures
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_audio Audio related
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_error Error Codes
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_log Logging Facility
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_misc Other
+ *
+ * @{
+ *
+ * @defgroup preproc_misc Preprocessor String Macros
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup version_utils Library Version Macros
+ *
+ * @{
+ *
+ * @}
+ */
+
+
+/**
+ * @addtogroup lavu_ver
+ * @{
+ */
+
+/**
+ * Return the LIBAVUTIL_VERSION_INT constant.
+ */
+unsigned avutil_version(void);
+
+/**
+ * Return an informative version string. This usually is the actual release
+ * version number or a git commit description. This string has no fixed format
+ * and can change any time. It should never be parsed by code.
+ */
+const char *av_version_info(void);
+
+/**
+ * Return the libavutil build-time configuration.
+ */
+const char *avutil_configuration(void);
+
+/**
+ * Return the libavutil license.
+ */
+const char *avutil_license(void);
+
+/**
+ * @}
+ */
+
+/**
+ * @addtogroup lavu_media Media Type
+ * @brief Media Type
+ */
+
+enum AVMediaType {
+ AVMEDIA_TYPE_UNKNOWN = -1, ///< Usually treated as AVMEDIA_TYPE_DATA
+ AVMEDIA_TYPE_VIDEO,
+ AVMEDIA_TYPE_AUDIO,
+ AVMEDIA_TYPE_DATA, ///< Opaque data information usually continuous
+ AVMEDIA_TYPE_SUBTITLE,
+ AVMEDIA_TYPE_ATTACHMENT, ///< Opaque data information usually sparse
+ AVMEDIA_TYPE_NB
+};
+
+/**
+ * Return a string describing the media_type enum, NULL if media_type
+ * is unknown.
+ */
+const char *av_get_media_type_string(enum AVMediaType media_type);
+
+/**
+ * @defgroup lavu_const Constants
+ * @{
+ *
+ * @defgroup lavu_enc Encoding specific
+ *
+ * @note those definition should move to avcodec
+ * @{
+ */
+
+#define FF_LAMBDA_SHIFT 7
+#define FF_LAMBDA_SCALE (1<<FF_LAMBDA_SHIFT)
+#define FF_QP2LAMBDA 118 ///< factor to convert from H.263 QP to lambda
+#define FF_LAMBDA_MAX (256*128-1)
+
+#define FF_QUALITY_SCALE FF_LAMBDA_SCALE //FIXME maybe remove
+
+/**
+ * @}
+ * @defgroup lavu_time Timestamp specific
+ *
+ * FFmpeg internal timebase and timestamp definitions
+ *
+ * @{
+ */
+
+/**
+ * @brief Undefined timestamp value
+ *
+ * Usually reported by demuxer that work on containers that do not provide
+ * either pts or dts.
+ */
+
+#define AV_NOPTS_VALUE ((int64_t)UINT64_C(0x8000000000000000))
+
+/**
+ * Internal time base represented as integer
+ */
+
+#define AV_TIME_BASE 1000000
+
+/**
+ * Internal time base represented as fractional value
+ */
+
+#define AV_TIME_BASE_Q (AVRational){1, AV_TIME_BASE}
+
+/**
+ * @}
+ * @}
+ * @defgroup lavu_picture Image related
+ *
+ * AVPicture types, pixel formats and basic image planes manipulation.
+ *
+ * @{
+ */
+
+enum AVPictureType {
+ AV_PICTURE_TYPE_NONE = 0, ///< Undefined
+ AV_PICTURE_TYPE_I, ///< Intra
+ AV_PICTURE_TYPE_P, ///< Predicted
+ AV_PICTURE_TYPE_B, ///< Bi-dir predicted
+ AV_PICTURE_TYPE_S, ///< S(GMC)-VOP MPEG4
+ AV_PICTURE_TYPE_SI, ///< Switching Intra
+ AV_PICTURE_TYPE_SP, ///< Switching Predicted
+ AV_PICTURE_TYPE_BI, ///< BI type
+};
+
+/**
+ * Return a single letter to describe the given picture type
+ * pict_type.
+ *
+ * @param[in] pict_type the picture type @return a single character
+ * representing the picture type, '?' if pict_type is unknown
+ */
+char av_get_picture_type_char(enum AVPictureType pict_type);
+
+/**
+ * @}
+ */
+
+#include "common.h"
+#include "error.h"
+#include "rational.h"
+#include "version.h"
+#include "macros.h"
+#include "mathematics.h"
+#include "log.h"
+#include "pixfmt.h"
+
+/**
+ * Return x default pointer in case p is NULL.
+ */
+static inline void *av_x_if_null(const void *p, const void *x)
+{
+ return (void *)(intptr_t)(p ? p : x);
+}
+
+/**
+ * Compute the length of an integer list.
+ *
+ * @param elsize size in bytes of each list element (only 1, 2, 4 or 8)
+ * @param term list terminator (usually 0 or -1)
+ * @param list pointer to the list
+ * @return length of the list, in elements, not counting the terminator
+ */
+unsigned av_int_list_length_for_size(unsigned elsize,
+ const void *list, uint64_t term) av_pure;
+
+/**
+ * Compute the length of an integer list.
+ *
+ * @param term list terminator (usually 0 or -1)
+ * @param list pointer to the list
+ * @return length of the list, in elements, not counting the terminator
+ */
+#define av_int_list_length(list, term) \
+ av_int_list_length_for_size(sizeof(*(list)), list, term)
+
+/**
+ * Open a file using a UTF-8 filename.
+ * The API of this function matches POSIX fopen(), errors are returned through
+ * errno.
+ */
+FILE *av_fopen_utf8(const char *path, const char *mode);
+
+/**
+ * Return the fractional representation of the internal time base.
+ */
+AVRational av_get_time_base_q(void);
+
+/**
+ * @}
+ * @}
+ */
+
+#endif /* AVUTIL_AVUTIL_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/buffer.h b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/buffer.h
new file mode 100644
index 0000000000..b4399fd39f
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/buffer.h
@@ -0,0 +1,274 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * @ingroup lavu_buffer
+ * refcounted data buffer API
+ */
+
+#ifndef AVUTIL_BUFFER_H
+#define AVUTIL_BUFFER_H
+
+#include <stdint.h>
+
+/**
+ * @defgroup lavu_buffer AVBuffer
+ * @ingroup lavu_data
+ *
+ * @{
+ * AVBuffer is an API for reference-counted data buffers.
+ *
+ * There are two core objects in this API -- AVBuffer and AVBufferRef. AVBuffer
+ * represents the data buffer itself; it is opaque and not meant to be accessed
+ * by the caller directly, but only through AVBufferRef. However, the caller may
+ * e.g. compare two AVBuffer pointers to check whether two different references
+ * are describing the same data buffer. AVBufferRef represents a single
+ * reference to an AVBuffer and it is the object that may be manipulated by the
+ * caller directly.
+ *
+ * There are two functions provided for creating a new AVBuffer with a single
+ * reference -- av_buffer_alloc() to just allocate a new buffer, and
+ * av_buffer_create() to wrap an existing array in an AVBuffer. From an existing
+ * reference, additional references may be created with av_buffer_ref().
+ * Use av_buffer_unref() to free a reference (this will automatically free the
+ * data once all the references are freed).
+ *
+ * The convention throughout this API and the rest of FFmpeg is such that the
+ * buffer is considered writable if there exists only one reference to it (and
+ * it has not been marked as read-only). The av_buffer_is_writable() function is
+ * provided to check whether this is true and av_buffer_make_writable() will
+ * automatically create a new writable buffer when necessary.
+ * Of course nothing prevents the calling code from violating this convention,
+ * however that is safe only when all the existing references are under its
+ * control.
+ *
+ * @note Referencing and unreferencing the buffers is thread-safe and thus
+ * may be done from multiple threads simultaneously without any need for
+ * additional locking.
+ *
+ * @note Two different references to the same buffer can point to different
+ * parts of the buffer (i.e. their AVBufferRef.data will not be equal).
+ */
+
+/**
+ * A reference counted buffer type. It is opaque and is meant to be used through
+ * references (AVBufferRef).
+ */
+typedef struct AVBuffer AVBuffer;
+
+/**
+ * A reference to a data buffer.
+ *
+ * The size of this struct is not a part of the public ABI and it is not meant
+ * to be allocated directly.
+ */
+typedef struct AVBufferRef {
+ AVBuffer *buffer;
+
+ /**
+ * The data buffer. It is considered writable if and only if
+ * this is the only reference to the buffer, in which case
+ * av_buffer_is_writable() returns 1.
+ */
+ uint8_t *data;
+ /**
+ * Size of data in bytes.
+ */
+ int size;
+} AVBufferRef;
+
+/**
+ * Allocate an AVBuffer of the given size using av_malloc().
+ *
+ * @return an AVBufferRef of given size or NULL when out of memory
+ */
+AVBufferRef *av_buffer_alloc(int size);
+
+/**
+ * Same as av_buffer_alloc(), except the returned buffer will be initialized
+ * to zero.
+ */
+AVBufferRef *av_buffer_allocz(int size);
+
+/**
+ * Always treat the buffer as read-only, even when it has only one
+ * reference.
+ */
+#define AV_BUFFER_FLAG_READONLY (1 << 0)
+
+/**
+ * Create an AVBuffer from an existing array.
+ *
+ * If this function is successful, data is owned by the AVBuffer. The caller may
+ * only access data through the returned AVBufferRef and references derived from
+ * it.
+ * If this function fails, data is left untouched.
+ * @param data data array
+ * @param size size of data in bytes
+ * @param free a callback for freeing this buffer's data
+ * @param opaque parameter to be got for processing or passed to free
+ * @param flags a combination of AV_BUFFER_FLAG_*
+ *
+ * @return an AVBufferRef referring to data on success, NULL on failure.
+ */
+AVBufferRef *av_buffer_create(uint8_t *data, int size,
+ void (*free)(void *opaque, uint8_t *data),
+ void *opaque, int flags);
+
+/**
+ * Default free callback, which calls av_free() on the buffer data.
+ * This function is meant to be passed to av_buffer_create(), not called
+ * directly.
+ */
+void av_buffer_default_free(void *opaque, uint8_t *data);
+
+/**
+ * Create a new reference to an AVBuffer.
+ *
+ * @return a new AVBufferRef referring to the same AVBuffer as buf or NULL on
+ * failure.
+ */
+AVBufferRef *av_buffer_ref(AVBufferRef *buf);
+
+/**
+ * Free a given reference and automatically free the buffer if there are no more
+ * references to it.
+ *
+ * @param buf the reference to be freed. The pointer is set to NULL on return.
+ */
+void av_buffer_unref(AVBufferRef **buf);
+
+/**
+ * @return 1 if the caller may write to the data referred to by buf (which is
+ * true if and only if buf is the only reference to the underlying AVBuffer).
+ * Return 0 otherwise.
+ * A positive answer is valid until av_buffer_ref() is called on buf.
+ */
+int av_buffer_is_writable(const AVBufferRef *buf);
+
+/**
+ * @return the opaque parameter set by av_buffer_create.
+ */
+void *av_buffer_get_opaque(const AVBufferRef *buf);
+
+int av_buffer_get_ref_count(const AVBufferRef *buf);
+
+/**
+ * Create a writable reference from a given buffer reference, avoiding data copy
+ * if possible.
+ *
+ * @param buf buffer reference to make writable. On success, buf is either left
+ * untouched, or it is unreferenced and a new writable AVBufferRef is
+ * written in its place. On failure, buf is left untouched.
+ * @return 0 on success, a negative AVERROR on failure.
+ */
+int av_buffer_make_writable(AVBufferRef **buf);
+
+/**
+ * Reallocate a given buffer.
+ *
+ * @param buf a buffer reference to reallocate. On success, buf will be
+ * unreferenced and a new reference with the required size will be
+ * written in its place. On failure buf will be left untouched. *buf
+ * may be NULL, then a new buffer is allocated.
+ * @param size required new buffer size.
+ * @return 0 on success, a negative AVERROR on failure.
+ *
+ * @note the buffer is actually reallocated with av_realloc() only if it was
+ * initially allocated through av_buffer_realloc(NULL) and there is only one
+ * reference to it (i.e. the one passed to this function). In all other cases
+ * a new buffer is allocated and the data is copied.
+ */
+int av_buffer_realloc(AVBufferRef **buf, int size);
+
+/**
+ * @}
+ */
+
+/**
+ * @defgroup lavu_bufferpool AVBufferPool
+ * @ingroup lavu_data
+ *
+ * @{
+ * AVBufferPool is an API for a lock-free thread-safe pool of AVBuffers.
+ *
+ * Frequently allocating and freeing large buffers may be slow. AVBufferPool is
+ * meant to solve this in cases when the caller needs a set of buffers of the
+ * same size (the most obvious use case being buffers for raw video or audio
+ * frames).
+ *
+ * At the beginning, the user must call av_buffer_pool_init() to create the
+ * buffer pool. Then whenever a buffer is needed, call av_buffer_pool_get() to
+ * get a reference to a new buffer, similar to av_buffer_alloc(). This new
+ * reference works in all aspects the same way as the one created by
+ * av_buffer_alloc(). However, when the last reference to this buffer is
+ * unreferenced, it is returned to the pool instead of being freed and will be
+ * reused for subsequent av_buffer_pool_get() calls.
+ *
+ * When the caller is done with the pool and no longer needs to allocate any new
+ * buffers, av_buffer_pool_uninit() must be called to mark the pool as freeable.
+ * Once all the buffers are released, it will automatically be freed.
+ *
+ * Allocating and releasing buffers with this API is thread-safe as long as
+ * either the default alloc callback is used, or the user-supplied one is
+ * thread-safe.
+ */
+
+/**
+ * The buffer pool. This structure is opaque and not meant to be accessed
+ * directly. It is allocated with av_buffer_pool_init() and freed with
+ * av_buffer_pool_uninit().
+ */
+typedef struct AVBufferPool AVBufferPool;
+
+/**
+ * Allocate and initialize a buffer pool.
+ *
+ * @param size size of each buffer in this pool
+ * @param alloc a function that will be used to allocate new buffers when the
+ * pool is empty. May be NULL, then the default allocator will be used
+ * (av_buffer_alloc()).
+ * @return newly created buffer pool on success, NULL on error.
+ */
+AVBufferPool *av_buffer_pool_init(int size, AVBufferRef* (*alloc)(int size));
+
+/**
+ * Mark the pool as being available for freeing. It will actually be freed only
+ * once all the allocated buffers associated with the pool are released. Thus it
+ * is safe to call this function while some of the allocated buffers are still
+ * in use.
+ *
+ * @param pool pointer to the pool to be freed. It will be set to NULL.
+ * @see av_buffer_pool_can_uninit()
+ */
+void av_buffer_pool_uninit(AVBufferPool **pool);
+
+/**
+ * Allocate a new AVBuffer, reusing an old buffer from the pool when available.
+ * This function may be called simultaneously from multiple threads.
+ *
+ * @return a reference to the new buffer on success, NULL on error.
+ */
+AVBufferRef *av_buffer_pool_get(AVBufferPool *pool);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_BUFFER_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/channel_layout.h b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/channel_layout.h
new file mode 100644
index 0000000000..ec7effead1
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/channel_layout.h
@@ -0,0 +1,223 @@
+/*
+ * Copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ * Copyright (c) 2008 Peter Ross
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_CHANNEL_LAYOUT_H
+#define AVUTIL_CHANNEL_LAYOUT_H
+
+#include <stdint.h>
+
+/**
+ * @file
+ * audio channel layout utility functions
+ */
+
+/**
+ * @addtogroup lavu_audio
+ * @{
+ */
+
+/**
+ * @defgroup channel_masks Audio channel masks
+ *
+ * A channel layout is a 64-bits integer with a bit set for every channel.
+ * The number of bits set must be equal to the number of channels.
+ * The value 0 means that the channel layout is not known.
+ * @note this data structure is not powerful enough to handle channels
+ * combinations that have the same channel multiple times, such as
+ * dual-mono.
+ *
+ * @{
+ */
+#define AV_CH_FRONT_LEFT 0x00000001
+#define AV_CH_FRONT_RIGHT 0x00000002
+#define AV_CH_FRONT_CENTER 0x00000004
+#define AV_CH_LOW_FREQUENCY 0x00000008
+#define AV_CH_BACK_LEFT 0x00000010
+#define AV_CH_BACK_RIGHT 0x00000020
+#define AV_CH_FRONT_LEFT_OF_CENTER 0x00000040
+#define AV_CH_FRONT_RIGHT_OF_CENTER 0x00000080
+#define AV_CH_BACK_CENTER 0x00000100
+#define AV_CH_SIDE_LEFT 0x00000200
+#define AV_CH_SIDE_RIGHT 0x00000400
+#define AV_CH_TOP_CENTER 0x00000800
+#define AV_CH_TOP_FRONT_LEFT 0x00001000
+#define AV_CH_TOP_FRONT_CENTER 0x00002000
+#define AV_CH_TOP_FRONT_RIGHT 0x00004000
+#define AV_CH_TOP_BACK_LEFT 0x00008000
+#define AV_CH_TOP_BACK_CENTER 0x00010000
+#define AV_CH_TOP_BACK_RIGHT 0x00020000
+#define AV_CH_STEREO_LEFT 0x20000000 ///< Stereo downmix.
+#define AV_CH_STEREO_RIGHT 0x40000000 ///< See AV_CH_STEREO_LEFT.
+#define AV_CH_WIDE_LEFT 0x0000000080000000ULL
+#define AV_CH_WIDE_RIGHT 0x0000000100000000ULL
+#define AV_CH_SURROUND_DIRECT_LEFT 0x0000000200000000ULL
+#define AV_CH_SURROUND_DIRECT_RIGHT 0x0000000400000000ULL
+#define AV_CH_LOW_FREQUENCY_2 0x0000000800000000ULL
+
+/** Channel mask value used for AVCodecContext.request_channel_layout
+ to indicate that the user requests the channel order of the decoder output
+ to be the native codec channel order. */
+#define AV_CH_LAYOUT_NATIVE 0x8000000000000000ULL
+
+/**
+ * @}
+ * @defgroup channel_mask_c Audio channel layouts
+ * @{
+ * */
+#define AV_CH_LAYOUT_MONO (AV_CH_FRONT_CENTER)
+#define AV_CH_LAYOUT_STEREO (AV_CH_FRONT_LEFT|AV_CH_FRONT_RIGHT)
+#define AV_CH_LAYOUT_2POINT1 (AV_CH_LAYOUT_STEREO|AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_2_1 (AV_CH_LAYOUT_STEREO|AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_SURROUND (AV_CH_LAYOUT_STEREO|AV_CH_FRONT_CENTER)
+#define AV_CH_LAYOUT_3POINT1 (AV_CH_LAYOUT_SURROUND|AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_4POINT0 (AV_CH_LAYOUT_SURROUND|AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_4POINT1 (AV_CH_LAYOUT_4POINT0|AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_2_2 (AV_CH_LAYOUT_STEREO|AV_CH_SIDE_LEFT|AV_CH_SIDE_RIGHT)
+#define AV_CH_LAYOUT_QUAD (AV_CH_LAYOUT_STEREO|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_5POINT0 (AV_CH_LAYOUT_SURROUND|AV_CH_SIDE_LEFT|AV_CH_SIDE_RIGHT)
+#define AV_CH_LAYOUT_5POINT1 (AV_CH_LAYOUT_5POINT0|AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_5POINT0_BACK (AV_CH_LAYOUT_SURROUND|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_5POINT1_BACK (AV_CH_LAYOUT_5POINT0_BACK|AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_6POINT0 (AV_CH_LAYOUT_5POINT0|AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_6POINT0_FRONT (AV_CH_LAYOUT_2_2|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER)
+#define AV_CH_LAYOUT_HEXAGONAL (AV_CH_LAYOUT_5POINT0_BACK|AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_6POINT1 (AV_CH_LAYOUT_5POINT1|AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_6POINT1_BACK (AV_CH_LAYOUT_5POINT1_BACK|AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_6POINT1_FRONT (AV_CH_LAYOUT_6POINT0_FRONT|AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_7POINT0 (AV_CH_LAYOUT_5POINT0|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_7POINT0_FRONT (AV_CH_LAYOUT_5POINT0|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER)
+#define AV_CH_LAYOUT_7POINT1 (AV_CH_LAYOUT_5POINT1|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_7POINT1_WIDE (AV_CH_LAYOUT_5POINT1|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER)
+#define AV_CH_LAYOUT_7POINT1_WIDE_BACK (AV_CH_LAYOUT_5POINT1_BACK|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER)
+#define AV_CH_LAYOUT_OCTAGONAL (AV_CH_LAYOUT_5POINT0|AV_CH_BACK_LEFT|AV_CH_BACK_CENTER|AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_HEXADECAGONAL (AV_CH_LAYOUT_OCTAGONAL|AV_CH_WIDE_LEFT|AV_CH_WIDE_RIGHT|AV_CH_TOP_BACK_LEFT|AV_CH_TOP_BACK_RIGHT|AV_CH_TOP_BACK_CENTER|AV_CH_TOP_FRONT_CENTER|AV_CH_TOP_FRONT_LEFT|AV_CH_TOP_FRONT_RIGHT)
+#define AV_CH_LAYOUT_STEREO_DOWNMIX (AV_CH_STEREO_LEFT|AV_CH_STEREO_RIGHT)
+
+enum AVMatrixEncoding {
+ AV_MATRIX_ENCODING_NONE,
+ AV_MATRIX_ENCODING_DOLBY,
+ AV_MATRIX_ENCODING_DPLII,
+ AV_MATRIX_ENCODING_DPLIIX,
+ AV_MATRIX_ENCODING_DPLIIZ,
+ AV_MATRIX_ENCODING_DOLBYEX,
+ AV_MATRIX_ENCODING_DOLBYHEADPHONE,
+ AV_MATRIX_ENCODING_NB
+};
+
+/**
+ * Return a channel layout id that matches name, or 0 if no match is found.
+ *
+ * name can be one or several of the following notations,
+ * separated by '+' or '|':
+ * - the name of an usual channel layout (mono, stereo, 4.0, quad, 5.0,
+ * 5.0(side), 5.1, 5.1(side), 7.1, 7.1(wide), downmix);
+ * - the name of a single channel (FL, FR, FC, LFE, BL, BR, FLC, FRC, BC,
+ * SL, SR, TC, TFL, TFC, TFR, TBL, TBC, TBR, DL, DR);
+ * - a number of channels, in decimal, optionally followed by 'c', yielding
+ * the default channel layout for that number of channels (@see
+ * av_get_default_channel_layout);
+ * - a channel layout mask, in hexadecimal starting with "0x" (see the
+ * AV_CH_* macros).
+ *
+ * @warning Starting from the next major bump the trailing character
+ * 'c' to specify a number of channels will be required, while a
+ * channel layout mask could also be specified as a decimal number
+ * (if and only if not followed by "c").
+ *
+ * Example: "stereo+FC" = "2c+FC" = "2c+1c" = "0x7"
+ */
+uint64_t av_get_channel_layout(const char *name);
+
+/**
+ * Return a description of a channel layout.
+ * If nb_channels is <= 0, it is guessed from the channel_layout.
+ *
+ * @param buf put here the string containing the channel layout
+ * @param buf_size size in bytes of the buffer
+ */
+void av_get_channel_layout_string(char *buf, int buf_size, int nb_channels, uint64_t channel_layout);
+
+struct AVBPrint;
+/**
+ * Append a description of a channel layout to a bprint buffer.
+ */
+void av_bprint_channel_layout(struct AVBPrint *bp, int nb_channels, uint64_t channel_layout);
+
+/**
+ * Return the number of channels in the channel layout.
+ */
+int av_get_channel_layout_nb_channels(uint64_t channel_layout);
+
+/**
+ * Return default channel layout for a given number of channels.
+ */
+int64_t av_get_default_channel_layout(int nb_channels);
+
+/**
+ * Get the index of a channel in channel_layout.
+ *
+ * @param channel a channel layout describing exactly one channel which must be
+ * present in channel_layout.
+ *
+ * @return index of channel in channel_layout on success, a negative AVERROR
+ * on error.
+ */
+int av_get_channel_layout_channel_index(uint64_t channel_layout,
+ uint64_t channel);
+
+/**
+ * Get the channel with the given index in channel_layout.
+ */
+uint64_t av_channel_layout_extract_channel(uint64_t channel_layout, int index);
+
+/**
+ * Get the name of a given channel.
+ *
+ * @return channel name on success, NULL on error.
+ */
+const char *av_get_channel_name(uint64_t channel);
+
+/**
+ * Get the description of a given channel.
+ *
+ * @param channel a channel layout with a single channel
+ * @return channel description on success, NULL on error
+ */
+const char *av_get_channel_description(uint64_t channel);
+
+/**
+ * Get the value and name of a standard channel layout.
+ *
+ * @param[in] index index in an internal list, starting at 0
+ * @param[out] layout channel layout mask
+ * @param[out] name name of the layout
+ * @return 0 if the layout exists,
+ * <0 if index is beyond the limits
+ */
+int av_get_standard_channel_layout(unsigned index, uint64_t *layout,
+ const char **name);
+
+/**
+ * @}
+ * @}
+ */
+
+#endif /* AVUTIL_CHANNEL_LAYOUT_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/common.h b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/common.h
new file mode 100644
index 0000000000..ea636431c1
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/common.h
@@ -0,0 +1,519 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * common internal and external API header
+ */
+
+#ifndef AVUTIL_COMMON_H
+#define AVUTIL_COMMON_H
+
+#if defined(__cplusplus) && !defined(__STDC_CONSTANT_MACROS) && !defined(UINT64_C)
+#error missing -D__STDC_CONSTANT_MACROS / #define __STDC_CONSTANT_MACROS
+#endif
+
+#include <errno.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <math.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "attributes.h"
+#include "macros.h"
+#include "version.h"
+#include "libavutil/avconfig.h"
+
+#if AV_HAVE_BIGENDIAN
+# define AV_NE(be, le) (be)
+#else
+# define AV_NE(be, le) (le)
+#endif
+
+//rounded division & shift
+#define RSHIFT(a,b) ((a) > 0 ? ((a) + ((1<<(b))>>1))>>(b) : ((a) + ((1<<(b))>>1)-1)>>(b))
+/* assume b>0 */
+#define ROUNDED_DIV(a,b) (((a)>0 ? (a) + ((b)>>1) : (a) - ((b)>>1))/(b))
+/* assume a>0 and b>0 */
+#define FF_CEIL_RSHIFT(a,b) (!av_builtin_constant_p(b) ? -((-(a)) >> (b)) \
+ : ((a) + (1<<(b)) - 1) >> (b))
+#define FFUDIV(a,b) (((a)>0 ?(a):(a)-(b)+1) / (b))
+#define FFUMOD(a,b) ((a)-(b)*FFUDIV(a,b))
+
+/**
+ * Absolute value, Note, INT_MIN / INT64_MIN result in undefined behavior as they
+ * are not representable as absolute values of their type. This is the same
+ * as with *abs()
+ * @see FFNABS()
+ */
+#define FFABS(a) ((a) >= 0 ? (a) : (-(a)))
+#define FFSIGN(a) ((a) > 0 ? 1 : -1)
+
+/**
+ * Negative Absolute value.
+ * this works for all integers of all types.
+ * As with many macros, this evaluates its argument twice, it thus must not have
+ * a sideeffect, that is FFNABS(x++) has undefined behavior.
+ */
+#define FFNABS(a) ((a) <= 0 ? (a) : (-(a)))
+
+/**
+ * Comparator.
+ * For two numerical expressions x and y, gives 1 if x > y, -1 if x < y, and 0
+ * if x == y. This is useful for instance in a qsort comparator callback.
+ * Furthermore, compilers are able to optimize this to branchless code, and
+ * there is no risk of overflow with signed types.
+ * As with many macros, this evaluates its argument multiple times, it thus
+ * must not have a side-effect.
+ */
+#define FFDIFFSIGN(x,y) (((x)>(y)) - ((x)<(y)))
+
+#define FFMAX(a,b) ((a) > (b) ? (a) : (b))
+#define FFMAX3(a,b,c) FFMAX(FFMAX(a,b),c)
+#define FFMIN(a,b) ((a) > (b) ? (b) : (a))
+#define FFMIN3(a,b,c) FFMIN(FFMIN(a,b),c)
+
+#define FFSWAP(type,a,b) do{type SWAP_tmp= b; b= a; a= SWAP_tmp;}while(0)
+#define FF_ARRAY_ELEMS(a) (sizeof(a) / sizeof((a)[0]))
+
+/* misc math functions */
+
+#ifdef HAVE_AV_CONFIG_H
+# include "config.h"
+# include "intmath.h"
+#endif
+
+/* Pull in unguarded fallback defines at the end of this file. */
+#include "common.h"
+
+#ifndef av_log2
+av_const int av_log2(unsigned v);
+#endif
+
+#ifndef av_log2_16bit
+av_const int av_log2_16bit(unsigned v);
+#endif
+
+/**
+ * Clip a signed integer value into the amin-amax range.
+ * @param a value to clip
+ * @param amin minimum value of the clip range
+ * @param amax maximum value of the clip range
+ * @return clipped value
+ */
+static av_always_inline av_const int av_clip_c(int a, int amin, int amax)
+{
+#if defined(HAVE_AV_CONFIG_H) && defined(ASSERT_LEVEL) && ASSERT_LEVEL >= 2
+ if (amin > amax) abort();
+#endif
+ if (a < amin) return amin;
+ else if (a > amax) return amax;
+ else return a;
+}
+
+/**
+ * Clip a signed 64bit integer value into the amin-amax range.
+ * @param a value to clip
+ * @param amin minimum value of the clip range
+ * @param amax maximum value of the clip range
+ * @return clipped value
+ */
+static av_always_inline av_const int64_t av_clip64_c(int64_t a, int64_t amin, int64_t amax)
+{
+#if defined(HAVE_AV_CONFIG_H) && defined(ASSERT_LEVEL) && ASSERT_LEVEL >= 2
+ if (amin > amax) abort();
+#endif
+ if (a < amin) return amin;
+ else if (a > amax) return amax;
+ else return a;
+}
+
+/**
+ * Clip a signed integer value into the 0-255 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const uint8_t av_clip_uint8_c(int a)
+{
+ if (a&(~0xFF)) return (-a)>>31;
+ else return a;
+}
+
+/**
+ * Clip a signed integer value into the -128,127 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const int8_t av_clip_int8_c(int a)
+{
+ if ((a+0x80U) & ~0xFF) return (a>>31) ^ 0x7F;
+ else return a;
+}
+
+/**
+ * Clip a signed integer value into the 0-65535 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const uint16_t av_clip_uint16_c(int a)
+{
+ if (a&(~0xFFFF)) return (-a)>>31;
+ else return a;
+}
+
+/**
+ * Clip a signed integer value into the -32768,32767 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const int16_t av_clip_int16_c(int a)
+{
+ if ((a+0x8000U) & ~0xFFFF) return (a>>31) ^ 0x7FFF;
+ else return a;
+}
+
+/**
+ * Clip a signed 64-bit integer value into the -2147483648,2147483647 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const int32_t av_clipl_int32_c(int64_t a)
+{
+ if ((a+0x80000000u) & ~UINT64_C(0xFFFFFFFF)) return (int32_t)((a>>63) ^ 0x7FFFFFFF);
+ else return (int32_t)a;
+}
+
+/**
+ * Clip a signed integer into the -(2^p),(2^p-1) range.
+ * @param a value to clip
+ * @param p bit position to clip at
+ * @return clipped value
+ */
+static av_always_inline av_const int av_clip_intp2_c(int a, int p)
+{
+ if ((a + (1 << p)) & ~((2 << p) - 1))
+ return (a >> 31) ^ ((1 << p) - 1);
+ else
+ return a;
+}
+
+/**
+ * Clip a signed integer to an unsigned power of two range.
+ * @param a value to clip
+ * @param p bit position to clip at
+ * @return clipped value
+ */
+static av_always_inline av_const unsigned av_clip_uintp2_c(int a, int p)
+{
+ if (a & ~((1<<p) - 1)) return -a >> 31 & ((1<<p) - 1);
+ else return a;
+}
+
+/**
+ * Clear high bits from an unsigned integer starting with specific bit position
+ * @param a value to clip
+ * @param p bit position to clip at
+ * @return clipped value
+ */
+static av_always_inline av_const unsigned av_mod_uintp2_c(unsigned a, unsigned p)
+{
+ return a & ((1 << p) - 1);
+}
+
+/**
+ * Add two signed 32-bit values with saturation.
+ *
+ * @param a one value
+ * @param b another value
+ * @return sum with signed saturation
+ */
+static av_always_inline int av_sat_add32_c(int a, int b)
+{
+ return av_clipl_int32((int64_t)a + b);
+}
+
+/**
+ * Add a doubled value to another value with saturation at both stages.
+ *
+ * @param a first value
+ * @param b value doubled and added to a
+ * @return sum with signed saturation
+ */
+static av_always_inline int av_sat_dadd32_c(int a, int b)
+{
+ return av_sat_add32(a, av_sat_add32(b, b));
+}
+
+/**
+ * Clip a float value into the amin-amax range.
+ * @param a value to clip
+ * @param amin minimum value of the clip range
+ * @param amax maximum value of the clip range
+ * @return clipped value
+ */
+static av_always_inline av_const float av_clipf_c(float a, float amin, float amax)
+{
+#if defined(HAVE_AV_CONFIG_H) && defined(ASSERT_LEVEL) && ASSERT_LEVEL >= 2
+ if (amin > amax) abort();
+#endif
+ if (a < amin) return amin;
+ else if (a > amax) return amax;
+ else return a;
+}
+
+/**
+ * Clip a double value into the amin-amax range.
+ * @param a value to clip
+ * @param amin minimum value of the clip range
+ * @param amax maximum value of the clip range
+ * @return clipped value
+ */
+static av_always_inline av_const double av_clipd_c(double a, double amin, double amax)
+{
+#if defined(HAVE_AV_CONFIG_H) && defined(ASSERT_LEVEL) && ASSERT_LEVEL >= 2
+ if (amin > amax) abort();
+#endif
+ if (a < amin) return amin;
+ else if (a > amax) return amax;
+ else return a;
+}
+
+/** Compute ceil(log2(x)).
+ * @param x value used to compute ceil(log2(x))
+ * @return computed ceiling of log2(x)
+ */
+static av_always_inline av_const int av_ceil_log2_c(int x)
+{
+ return av_log2((x - 1) << 1);
+}
+
+/**
+ * Count number of bits set to one in x
+ * @param x value to count bits of
+ * @return the number of bits set to one in x
+ */
+static av_always_inline av_const int av_popcount_c(uint32_t x)
+{
+ x -= (x >> 1) & 0x55555555;
+ x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
+ x = (x + (x >> 4)) & 0x0F0F0F0F;
+ x += x >> 8;
+ return (x + (x >> 16)) & 0x3F;
+}
+
+/**
+ * Count number of bits set to one in x
+ * @param x value to count bits of
+ * @return the number of bits set to one in x
+ */
+static av_always_inline av_const int av_popcount64_c(uint64_t x)
+{
+ return av_popcount((uint32_t)x) + av_popcount((uint32_t)(x >> 32));
+}
+
+#define MKTAG(a,b,c,d) ((a) | ((b) << 8) | ((c) << 16) | ((unsigned)(d) << 24))
+#define MKBETAG(a,b,c,d) ((d) | ((c) << 8) | ((b) << 16) | ((unsigned)(a) << 24))
+
+/**
+ * Convert a UTF-8 character (up to 4 bytes) to its 32-bit UCS-4 encoded form.
+ *
+ * @param val Output value, must be an lvalue of type uint32_t.
+ * @param GET_BYTE Expression reading one byte from the input.
+ * Evaluated up to 7 times (4 for the currently
+ * assigned Unicode range). With a memory buffer
+ * input, this could be *ptr++.
+ * @param ERROR Expression to be evaluated on invalid input,
+ * typically a goto statement.
+ *
+ * @warning ERROR should not contain a loop control statement which
+ * could interact with the internal while loop, and should force an
+ * exit from the macro code (e.g. through a goto or a return) in order
+ * to prevent undefined results.
+ */
+#define GET_UTF8(val, GET_BYTE, ERROR)\
+ val= GET_BYTE;\
+ {\
+ uint32_t top = (val & 128) >> 1;\
+ if ((val & 0xc0) == 0x80 || val >= 0xFE)\
+ ERROR\
+ while (val & top) {\
+ int tmp= GET_BYTE - 128;\
+ if(tmp>>6)\
+ ERROR\
+ val= (val<<6) + tmp;\
+ top <<= 5;\
+ }\
+ val &= (top << 1) - 1;\
+ }
+
+/**
+ * Convert a UTF-16 character (2 or 4 bytes) to its 32-bit UCS-4 encoded form.
+ *
+ * @param val Output value, must be an lvalue of type uint32_t.
+ * @param GET_16BIT Expression returning two bytes of UTF-16 data converted
+ * to native byte order. Evaluated one or two times.
+ * @param ERROR Expression to be evaluated on invalid input,
+ * typically a goto statement.
+ */
+#define GET_UTF16(val, GET_16BIT, ERROR)\
+ val = GET_16BIT;\
+ {\
+ unsigned int hi = val - 0xD800;\
+ if (hi < 0x800) {\
+ val = GET_16BIT - 0xDC00;\
+ if (val > 0x3FFU || hi > 0x3FFU)\
+ ERROR\
+ val += (hi<<10) + 0x10000;\
+ }\
+ }\
+
+/**
+ * @def PUT_UTF8(val, tmp, PUT_BYTE)
+ * Convert a 32-bit Unicode character to its UTF-8 encoded form (up to 4 bytes long).
+ * @param val is an input-only argument and should be of type uint32_t. It holds
+ * a UCS-4 encoded Unicode character that is to be converted to UTF-8. If
+ * val is given as a function it is executed only once.
+ * @param tmp is a temporary variable and should be of type uint8_t. It
+ * represents an intermediate value during conversion that is to be
+ * output by PUT_BYTE.
+ * @param PUT_BYTE writes the converted UTF-8 bytes to any proper destination.
+ * It could be a function or a statement, and uses tmp as the input byte.
+ * For example, PUT_BYTE could be "*output++ = tmp;" PUT_BYTE will be
+ * executed up to 4 times for values in the valid UTF-8 range and up to
+ * 7 times in the general case, depending on the length of the converted
+ * Unicode character.
+ */
+#define PUT_UTF8(val, tmp, PUT_BYTE)\
+ {\
+ int bytes, shift;\
+ uint32_t in = val;\
+ if (in < 0x80) {\
+ tmp = in;\
+ PUT_BYTE\
+ } else {\
+ bytes = (av_log2(in) + 4) / 5;\
+ shift = (bytes - 1) * 6;\
+ tmp = (256 - (256 >> bytes)) | (in >> shift);\
+ PUT_BYTE\
+ while (shift >= 6) {\
+ shift -= 6;\
+ tmp = 0x80 | ((in >> shift) & 0x3f);\
+ PUT_BYTE\
+ }\
+ }\
+ }
+
+/**
+ * @def PUT_UTF16(val, tmp, PUT_16BIT)
+ * Convert a 32-bit Unicode character to its UTF-16 encoded form (2 or 4 bytes).
+ * @param val is an input-only argument and should be of type uint32_t. It holds
+ * a UCS-4 encoded Unicode character that is to be converted to UTF-16. If
+ * val is given as a function it is executed only once.
+ * @param tmp is a temporary variable and should be of type uint16_t. It
+ * represents an intermediate value during conversion that is to be
+ * output by PUT_16BIT.
+ * @param PUT_16BIT writes the converted UTF-16 data to any proper destination
+ * in desired endianness. It could be a function or a statement, and uses tmp
+ * as the input byte. For example, PUT_BYTE could be "*output++ = tmp;"
+ * PUT_BYTE will be executed 1 or 2 times depending on input character.
+ */
+#define PUT_UTF16(val, tmp, PUT_16BIT)\
+ {\
+ uint32_t in = val;\
+ if (in < 0x10000) {\
+ tmp = in;\
+ PUT_16BIT\
+ } else {\
+ tmp = 0xD800 | ((in - 0x10000) >> 10);\
+ PUT_16BIT\
+ tmp = 0xDC00 | ((in - 0x10000) & 0x3FF);\
+ PUT_16BIT\
+ }\
+ }\
+
+
+
+#include "mem.h"
+
+#ifdef HAVE_AV_CONFIG_H
+# include "internal.h"
+#endif /* HAVE_AV_CONFIG_H */
+
+#endif /* AVUTIL_COMMON_H */
+
+/*
+ * The following definitions are outside the multiple inclusion guard
+ * to ensure they are immediately available in intmath.h.
+ */
+
+#ifndef av_ceil_log2
+# define av_ceil_log2 av_ceil_log2_c
+#endif
+#ifndef av_clip
+# define av_clip av_clip_c
+#endif
+#ifndef av_clip64
+# define av_clip64 av_clip64_c
+#endif
+#ifndef av_clip_uint8
+# define av_clip_uint8 av_clip_uint8_c
+#endif
+#ifndef av_clip_int8
+# define av_clip_int8 av_clip_int8_c
+#endif
+#ifndef av_clip_uint16
+# define av_clip_uint16 av_clip_uint16_c
+#endif
+#ifndef av_clip_int16
+# define av_clip_int16 av_clip_int16_c
+#endif
+#ifndef av_clipl_int32
+# define av_clipl_int32 av_clipl_int32_c
+#endif
+#ifndef av_clip_intp2
+# define av_clip_intp2 av_clip_intp2_c
+#endif
+#ifndef av_clip_uintp2
+# define av_clip_uintp2 av_clip_uintp2_c
+#endif
+#ifndef av_mod_uintp2
+# define av_mod_uintp2 av_mod_uintp2_c
+#endif
+#ifndef av_sat_add32
+# define av_sat_add32 av_sat_add32_c
+#endif
+#ifndef av_sat_dadd32
+# define av_sat_dadd32 av_sat_dadd32_c
+#endif
+#ifndef av_clipf
+# define av_clipf av_clipf_c
+#endif
+#ifndef av_clipd
+# define av_clipd av_clipd_c
+#endif
+#ifndef av_popcount
+# define av_popcount av_popcount_c
+#endif
+#ifndef av_popcount64
+# define av_popcount64 av_popcount64_c
+#endif
diff --git a/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/cpu.h b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/cpu.h
new file mode 100644
index 0000000000..cc4e30c4cd
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/cpu.h
@@ -0,0 +1,117 @@
+/*
+ * Copyright (c) 2000, 2001, 2002 Fabrice Bellard
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_CPU_H
+#define AVUTIL_CPU_H
+
+#include "attributes.h"
+
+#define AV_CPU_FLAG_FORCE 0x80000000 /* force usage of selected flags (OR) */
+
+ /* lower 16 bits - CPU features */
+#define AV_CPU_FLAG_MMX 0x0001 ///< standard MMX
+#define AV_CPU_FLAG_MMXEXT 0x0002 ///< SSE integer functions or AMD MMX ext
+#define AV_CPU_FLAG_MMX2 0x0002 ///< SSE integer functions or AMD MMX ext
+#define AV_CPU_FLAG_3DNOW 0x0004 ///< AMD 3DNOW
+#define AV_CPU_FLAG_SSE 0x0008 ///< SSE functions
+#define AV_CPU_FLAG_SSE2 0x0010 ///< PIV SSE2 functions
+#define AV_CPU_FLAG_SSE2SLOW 0x40000000 ///< SSE2 supported, but usually not faster
+ ///< than regular MMX/SSE (e.g. Core1)
+#define AV_CPU_FLAG_3DNOWEXT 0x0020 ///< AMD 3DNowExt
+#define AV_CPU_FLAG_SSE3 0x0040 ///< Prescott SSE3 functions
+#define AV_CPU_FLAG_SSE3SLOW 0x20000000 ///< SSE3 supported, but usually not faster
+ ///< than regular MMX/SSE (e.g. Core1)
+#define AV_CPU_FLAG_SSSE3 0x0080 ///< Conroe SSSE3 functions
+#define AV_CPU_FLAG_ATOM 0x10000000 ///< Atom processor, some SSSE3 instructions are slower
+#define AV_CPU_FLAG_SSE4 0x0100 ///< Penryn SSE4.1 functions
+#define AV_CPU_FLAG_SSE42 0x0200 ///< Nehalem SSE4.2 functions
+#define AV_CPU_FLAG_AESNI 0x80000 ///< Advanced Encryption Standard functions
+#define AV_CPU_FLAG_AVX 0x4000 ///< AVX functions: requires OS support even if YMM registers aren't used
+#define AV_CPU_FLAG_AVXSLOW 0x8000000 ///< AVX supported, but slow when using YMM registers (e.g. Bulldozer)
+#define AV_CPU_FLAG_XOP 0x0400 ///< Bulldozer XOP functions
+#define AV_CPU_FLAG_FMA4 0x0800 ///< Bulldozer FMA4 functions
+#define AV_CPU_FLAG_CMOV 0x1000 ///< supports cmov instruction
+#define AV_CPU_FLAG_AVX2 0x8000 ///< AVX2 functions: requires OS support even if YMM registers aren't used
+#define AV_CPU_FLAG_FMA3 0x10000 ///< Haswell FMA3 functions
+#define AV_CPU_FLAG_BMI1 0x20000 ///< Bit Manipulation Instruction Set 1
+#define AV_CPU_FLAG_BMI2 0x40000 ///< Bit Manipulation Instruction Set 2
+
+#define AV_CPU_FLAG_ALTIVEC 0x0001 ///< standard
+#define AV_CPU_FLAG_VSX 0x0002 ///< ISA 2.06
+#define AV_CPU_FLAG_POWER8 0x0004 ///< ISA 2.07
+
+#define AV_CPU_FLAG_ARMV5TE (1 << 0)
+#define AV_CPU_FLAG_ARMV6 (1 << 1)
+#define AV_CPU_FLAG_ARMV6T2 (1 << 2)
+#define AV_CPU_FLAG_VFP (1 << 3)
+#define AV_CPU_FLAG_VFPV3 (1 << 4)
+#define AV_CPU_FLAG_NEON (1 << 5)
+#define AV_CPU_FLAG_ARMV8 (1 << 6)
+#define AV_CPU_FLAG_VFP_VM (1 << 7) ///< VFPv2 vector mode, deprecated in ARMv7-A and unavailable in various CPUs implementations
+#define AV_CPU_FLAG_SETEND (1 <<16)
+
+/**
+ * Return the flags which specify extensions supported by the CPU.
+ * The returned value is affected by av_force_cpu_flags() if that was used
+ * before. So av_get_cpu_flags() can easily be used in a application to
+ * detect the enabled cpu flags.
+ */
+int av_get_cpu_flags(void);
+
+/**
+ * Disables cpu detection and forces the specified flags.
+ * -1 is a special case that disables forcing of specific flags.
+ */
+void av_force_cpu_flags(int flags);
+
+/**
+ * Set a mask on flags returned by av_get_cpu_flags().
+ * This function is mainly useful for testing.
+ * Please use av_force_cpu_flags() and av_get_cpu_flags() instead which are more flexible
+ *
+ * @warning this function is not thread safe.
+ */
+attribute_deprecated void av_set_cpu_flags_mask(int mask);
+
+/**
+ * Parse CPU flags from a string.
+ *
+ * The returned flags contain the specified flags as well as related unspecified flags.
+ *
+ * This function exists only for compatibility with libav.
+ * Please use av_parse_cpu_caps() when possible.
+ * @return a combination of AV_CPU_* flags, negative on error.
+ */
+attribute_deprecated
+int av_parse_cpu_flags(const char *s);
+
+/**
+ * Parse CPU caps from a string and update the given AV_CPU_* flags based on that.
+ *
+ * @return negative on error.
+ */
+int av_parse_cpu_caps(unsigned *flags, const char *s);
+
+/**
+ * @return the number of logical CPU cores present.
+ */
+int av_cpu_count(void);
+
+#endif /* AVUTIL_CPU_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/dict.h b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/dict.h
new file mode 100644
index 0000000000..5b8d003396
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/dict.h
@@ -0,0 +1,198 @@
+/*
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * Public dictionary API.
+ * @deprecated
+ * AVDictionary is provided for compatibility with libav. It is both in
+ * implementation as well as API inefficient. It does not scale and is
+ * extremely slow with large dictionaries.
+ * It is recommended that new code uses our tree container from tree.c/h
+ * where applicable, which uses AVL trees to achieve O(log n) performance.
+ */
+
+#ifndef AVUTIL_DICT_H
+#define AVUTIL_DICT_H
+
+#include <stdint.h>
+
+#include "version.h"
+
+/**
+ * @addtogroup lavu_dict AVDictionary
+ * @ingroup lavu_data
+ *
+ * @brief Simple key:value store
+ *
+ * @{
+ * Dictionaries are used for storing key:value pairs. To create
+ * an AVDictionary, simply pass an address of a NULL pointer to
+ * av_dict_set(). NULL can be used as an empty dictionary wherever
+ * a pointer to an AVDictionary is required.
+ * Use av_dict_get() to retrieve an entry or iterate over all
+ * entries and finally av_dict_free() to free the dictionary
+ * and all its contents.
+ *
+ @code
+ AVDictionary *d = NULL; // "create" an empty dictionary
+ AVDictionaryEntry *t = NULL;
+
+ av_dict_set(&d, "foo", "bar", 0); // add an entry
+
+ char *k = av_strdup("key"); // if your strings are already allocated,
+ char *v = av_strdup("value"); // you can avoid copying them like this
+ av_dict_set(&d, k, v, AV_DICT_DONT_STRDUP_KEY | AV_DICT_DONT_STRDUP_VAL);
+
+ while (t = av_dict_get(d, "", t, AV_DICT_IGNORE_SUFFIX)) {
+ <....> // iterate over all entries in d
+ }
+ av_dict_free(&d);
+ @endcode
+ *
+ */
+
+#define AV_DICT_MATCH_CASE 1 /**< Only get an entry with exact-case key match. Only relevant in av_dict_get(). */
+#define AV_DICT_IGNORE_SUFFIX 2 /**< Return first entry in a dictionary whose first part corresponds to the search key,
+ ignoring the suffix of the found key string. Only relevant in av_dict_get(). */
+#define AV_DICT_DONT_STRDUP_KEY 4 /**< Take ownership of a key that's been
+ allocated with av_malloc() or another memory allocation function. */
+#define AV_DICT_DONT_STRDUP_VAL 8 /**< Take ownership of a value that's been
+ allocated with av_malloc() or another memory allocation function. */
+#define AV_DICT_DONT_OVERWRITE 16 ///< Don't overwrite existing entries.
+#define AV_DICT_APPEND 32 /**< If the entry already exists, append to it. Note that no
+ delimiter is added, the strings are simply concatenated. */
+
+typedef struct AVDictionaryEntry {
+ char *key;
+ char *value;
+} AVDictionaryEntry;
+
+typedef struct AVDictionary AVDictionary;
+
+/**
+ * Get a dictionary entry with matching key.
+ *
+ * The returned entry key or value must not be changed, or it will
+ * cause undefined behavior.
+ *
+ * To iterate through all the dictionary entries, you can set the matching key
+ * to the null string "" and set the AV_DICT_IGNORE_SUFFIX flag.
+ *
+ * @param prev Set to the previous matching element to find the next.
+ * If set to NULL the first matching element is returned.
+ * @param key matching key
+ * @param flags a collection of AV_DICT_* flags controlling how the entry is retrieved
+ * @return found entry or NULL in case no matching entry was found in the dictionary
+ */
+AVDictionaryEntry *av_dict_get(const AVDictionary *m, const char *key,
+ const AVDictionaryEntry *prev, int flags);
+
+/**
+ * Get number of entries in dictionary.
+ *
+ * @param m dictionary
+ * @return number of entries in dictionary
+ */
+int av_dict_count(const AVDictionary *m);
+
+/**
+ * Set the given entry in *pm, overwriting an existing entry.
+ *
+ * Note: If AV_DICT_DONT_STRDUP_KEY or AV_DICT_DONT_STRDUP_VAL is set,
+ * these arguments will be freed on error.
+ *
+ * @param pm pointer to a pointer to a dictionary struct. If *pm is NULL
+ * a dictionary struct is allocated and put in *pm.
+ * @param key entry key to add to *pm (will be av_strduped depending on flags)
+ * @param value entry value to add to *pm (will be av_strduped depending on flags).
+ * Passing a NULL value will cause an existing entry to be deleted.
+ * @return >= 0 on success otherwise an error code <0
+ */
+int av_dict_set(AVDictionary **pm, const char *key, const char *value, int flags);
+
+/**
+ * Convenience wrapper for av_dict_set that converts the value to a string
+ * and stores it.
+ *
+ * Note: If AV_DICT_DONT_STRDUP_KEY is set, key will be freed on error.
+ */
+int av_dict_set_int(AVDictionary **pm, const char *key, int64_t value, int flags);
+
+/**
+ * Parse the key/value pairs list and add the parsed entries to a dictionary.
+ *
+ * In case of failure, all the successfully set entries are stored in
+ * *pm. You may need to manually free the created dictionary.
+ *
+ * @param key_val_sep a 0-terminated list of characters used to separate
+ * key from value
+ * @param pairs_sep a 0-terminated list of characters used to separate
+ * two pairs from each other
+ * @param flags flags to use when adding to dictionary.
+ * AV_DICT_DONT_STRDUP_KEY and AV_DICT_DONT_STRDUP_VAL
+ * are ignored since the key/value tokens will always
+ * be duplicated.
+ * @return 0 on success, negative AVERROR code on failure
+ */
+int av_dict_parse_string(AVDictionary **pm, const char *str,
+ const char *key_val_sep, const char *pairs_sep,
+ int flags);
+
+/**
+ * Copy entries from one AVDictionary struct into another.
+ * @param dst pointer to a pointer to a AVDictionary struct. If *dst is NULL,
+ * this function will allocate a struct for you and put it in *dst
+ * @param src pointer to source AVDictionary struct
+ * @param flags flags to use when setting entries in *dst
+ * @note metadata is read using the AV_DICT_IGNORE_SUFFIX flag
+ * @return 0 on success, negative AVERROR code on failure. If dst was allocated
+ * by this function, callers should free the associated memory.
+ */
+int av_dict_copy(AVDictionary **dst, const AVDictionary *src, int flags);
+
+/**
+ * Free all the memory allocated for an AVDictionary struct
+ * and all keys and values.
+ */
+void av_dict_free(AVDictionary **m);
+
+/**
+ * Get dictionary entries as a string.
+ *
+ * Create a string containing dictionary's entries.
+ * Such string may be passed back to av_dict_parse_string().
+ * @note String is escaped with backslashes ('\').
+ *
+ * @param[in] m dictionary
+ * @param[out] buffer Pointer to buffer that will be allocated with string containg entries.
+ * Buffer must be freed by the caller when is no longer needed.
+ * @param[in] key_val_sep character used to separate key from value
+ * @param[in] pairs_sep character used to separate two pairs from each other
+ * @return >= 0 on success, negative on error
+ * @warning Separators cannot be neither '\\' nor '\0'. They also cannot be the same.
+ */
+int av_dict_get_string(const AVDictionary *m, char **buffer,
+ const char key_val_sep, const char pairs_sep);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_DICT_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/error.h b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/error.h
new file mode 100644
index 0000000000..71df4da353
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/error.h
@@ -0,0 +1,126 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * error code definitions
+ */
+
+#ifndef AVUTIL_ERROR_H
+#define AVUTIL_ERROR_H
+
+#include <errno.h>
+#include <stddef.h>
+
+/**
+ * @addtogroup lavu_error
+ *
+ * @{
+ */
+
+
+/* error handling */
+#if EDOM > 0
+#define AVERROR(e) (-(e)) ///< Returns a negative error code from a POSIX error code, to return from library functions.
+#define AVUNERROR(e) (-(e)) ///< Returns a POSIX error code from a library function error return value.
+#else
+/* Some platforms have E* and errno already negated. */
+#define AVERROR(e) (e)
+#define AVUNERROR(e) (e)
+#endif
+
+#define FFERRTAG(a, b, c, d) (-(int)MKTAG(a, b, c, d))
+
+#define AVERROR_BSF_NOT_FOUND FFERRTAG(0xF8,'B','S','F') ///< Bitstream filter not found
+#define AVERROR_BUG FFERRTAG( 'B','U','G','!') ///< Internal bug, also see AVERROR_BUG2
+#define AVERROR_BUFFER_TOO_SMALL FFERRTAG( 'B','U','F','S') ///< Buffer too small
+#define AVERROR_DECODER_NOT_FOUND FFERRTAG(0xF8,'D','E','C') ///< Decoder not found
+#define AVERROR_DEMUXER_NOT_FOUND FFERRTAG(0xF8,'D','E','M') ///< Demuxer not found
+#define AVERROR_ENCODER_NOT_FOUND FFERRTAG(0xF8,'E','N','C') ///< Encoder not found
+#define AVERROR_EOF FFERRTAG( 'E','O','F',' ') ///< End of file
+#define AVERROR_EXIT FFERRTAG( 'E','X','I','T') ///< Immediate exit was requested; the called function should not be restarted
+#define AVERROR_EXTERNAL FFERRTAG( 'E','X','T',' ') ///< Generic error in an external library
+#define AVERROR_FILTER_NOT_FOUND FFERRTAG(0xF8,'F','I','L') ///< Filter not found
+#define AVERROR_INVALIDDATA FFERRTAG( 'I','N','D','A') ///< Invalid data found when processing input
+#define AVERROR_MUXER_NOT_FOUND FFERRTAG(0xF8,'M','U','X') ///< Muxer not found
+#define AVERROR_OPTION_NOT_FOUND FFERRTAG(0xF8,'O','P','T') ///< Option not found
+#define AVERROR_PATCHWELCOME FFERRTAG( 'P','A','W','E') ///< Not yet implemented in FFmpeg, patches welcome
+#define AVERROR_PROTOCOL_NOT_FOUND FFERRTAG(0xF8,'P','R','O') ///< Protocol not found
+
+#define AVERROR_STREAM_NOT_FOUND FFERRTAG(0xF8,'S','T','R') ///< Stream not found
+/**
+ * This is semantically identical to AVERROR_BUG
+ * it has been introduced in Libav after our AVERROR_BUG and with a modified value.
+ */
+#define AVERROR_BUG2 FFERRTAG( 'B','U','G',' ')
+#define AVERROR_UNKNOWN FFERRTAG( 'U','N','K','N') ///< Unknown error, typically from an external library
+#define AVERROR_EXPERIMENTAL (-0x2bb2afa8) ///< Requested feature is flagged experimental. Set strict_std_compliance if you really want to use it.
+#define AVERROR_INPUT_CHANGED (-0x636e6701) ///< Input changed between calls. Reconfiguration is required. (can be OR-ed with AVERROR_OUTPUT_CHANGED)
+#define AVERROR_OUTPUT_CHANGED (-0x636e6702) ///< Output changed between calls. Reconfiguration is required. (can be OR-ed with AVERROR_INPUT_CHANGED)
+/* HTTP & RTSP errors */
+#define AVERROR_HTTP_BAD_REQUEST FFERRTAG(0xF8,'4','0','0')
+#define AVERROR_HTTP_UNAUTHORIZED FFERRTAG(0xF8,'4','0','1')
+#define AVERROR_HTTP_FORBIDDEN FFERRTAG(0xF8,'4','0','3')
+#define AVERROR_HTTP_NOT_FOUND FFERRTAG(0xF8,'4','0','4')
+#define AVERROR_HTTP_OTHER_4XX FFERRTAG(0xF8,'4','X','X')
+#define AVERROR_HTTP_SERVER_ERROR FFERRTAG(0xF8,'5','X','X')
+
+#define AV_ERROR_MAX_STRING_SIZE 64
+
+/**
+ * Put a description of the AVERROR code errnum in errbuf.
+ * In case of failure the global variable errno is set to indicate the
+ * error. Even in case of failure av_strerror() will print a generic
+ * error message indicating the errnum provided to errbuf.
+ *
+ * @param errnum error code to describe
+ * @param errbuf buffer to which description is written
+ * @param errbuf_size the size in bytes of errbuf
+ * @return 0 on success, a negative value if a description for errnum
+ * cannot be found
+ */
+int av_strerror(int errnum, char *errbuf, size_t errbuf_size);
+
+/**
+ * Fill the provided buffer with a string containing an error string
+ * corresponding to the AVERROR code errnum.
+ *
+ * @param errbuf a buffer
+ * @param errbuf_size size in bytes of errbuf
+ * @param errnum error code to describe
+ * @return the buffer in input, filled with the error description
+ * @see av_strerror()
+ */
+static inline char *av_make_error_string(char *errbuf, size_t errbuf_size, int errnum)
+{
+ av_strerror(errnum, errbuf, errbuf_size);
+ return errbuf;
+}
+
+/**
+ * Convenience macro, the return value should be used only directly in
+ * function arguments but never stand-alone.
+ */
+#define av_err2str(errnum) \
+ av_make_error_string((char[AV_ERROR_MAX_STRING_SIZE]){0}, AV_ERROR_MAX_STRING_SIZE, errnum)
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_ERROR_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/frame.h b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/frame.h
new file mode 100644
index 0000000000..9c6061a11b
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/frame.h
@@ -0,0 +1,713 @@
+/*
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * @ingroup lavu_frame
+ * reference-counted frame API
+ */
+
+#ifndef AVUTIL_FRAME_H
+#define AVUTIL_FRAME_H
+
+#include <stdint.h>
+
+#include "avutil.h"
+#include "buffer.h"
+#include "dict.h"
+#include "rational.h"
+#include "samplefmt.h"
+#include "pixfmt.h"
+#include "version.h"
+
+
+/**
+ * @defgroup lavu_frame AVFrame
+ * @ingroup lavu_data
+ *
+ * @{
+ * AVFrame is an abstraction for reference-counted raw multimedia data.
+ */
+
+enum AVFrameSideDataType {
+ /**
+ * The data is the AVPanScan struct defined in libavcodec.
+ */
+ AV_FRAME_DATA_PANSCAN,
+ /**
+ * ATSC A53 Part 4 Closed Captions.
+ * A53 CC bitstream is stored as uint8_t in AVFrameSideData.data.
+ * The number of bytes of CC data is AVFrameSideData.size.
+ */
+ AV_FRAME_DATA_A53_CC,
+ /**
+ * Stereoscopic 3d metadata.
+ * The data is the AVStereo3D struct defined in libavutil/stereo3d.h.
+ */
+ AV_FRAME_DATA_STEREO3D,
+ /**
+ * The data is the AVMatrixEncoding enum defined in libavutil/channel_layout.h.
+ */
+ AV_FRAME_DATA_MATRIXENCODING,
+ /**
+ * Metadata relevant to a downmix procedure.
+ * The data is the AVDownmixInfo struct defined in libavutil/downmix_info.h.
+ */
+ AV_FRAME_DATA_DOWNMIX_INFO,
+ /**
+ * ReplayGain information in the form of the AVReplayGain struct.
+ */
+ AV_FRAME_DATA_REPLAYGAIN,
+ /**
+ * This side data contains a 3x3 transformation matrix describing an affine
+ * transformation that needs to be applied to the frame for correct
+ * presentation.
+ *
+ * See libavutil/display.h for a detailed description of the data.
+ */
+ AV_FRAME_DATA_DISPLAYMATRIX,
+ /**
+ * Active Format Description data consisting of a single byte as specified
+ * in ETSI TS 101 154 using AVActiveFormatDescription enum.
+ */
+ AV_FRAME_DATA_AFD,
+ /**
+ * Motion vectors exported by some codecs (on demand through the export_mvs
+ * flag set in the libavcodec AVCodecContext flags2 option).
+ * The data is the AVMotionVector struct defined in
+ * libavutil/motion_vector.h.
+ */
+ AV_FRAME_DATA_MOTION_VECTORS,
+ /**
+ * Recommmends skipping the specified number of samples. This is exported
+ * only if the "skip_manual" AVOption is set in libavcodec.
+ * This has the same format as AV_PKT_DATA_SKIP_SAMPLES.
+ * @code
+ * u32le number of samples to skip from start of this packet
+ * u32le number of samples to skip from end of this packet
+ * u8 reason for start skip
+ * u8 reason for end skip (0=padding silence, 1=convergence)
+ * @endcode
+ */
+ AV_FRAME_DATA_SKIP_SAMPLES,
+
+ /**
+ * This side data must be associated with an audio frame and corresponds to
+ * enum AVAudioServiceType defined in avcodec.h.
+ */
+ AV_FRAME_DATA_AUDIO_SERVICE_TYPE,
+};
+
+enum AVActiveFormatDescription {
+ AV_AFD_SAME = 8,
+ AV_AFD_4_3 = 9,
+ AV_AFD_16_9 = 10,
+ AV_AFD_14_9 = 11,
+ AV_AFD_4_3_SP_14_9 = 13,
+ AV_AFD_16_9_SP_14_9 = 14,
+ AV_AFD_SP_4_3 = 15,
+};
+
+
+/**
+ * Structure to hold side data for an AVFrame.
+ *
+ * sizeof(AVFrameSideData) is not a part of the public ABI, so new fields may be added
+ * to the end with a minor bump.
+ */
+typedef struct AVFrameSideData {
+ enum AVFrameSideDataType type;
+ uint8_t *data;
+ int size;
+ AVDictionary *metadata;
+ AVBufferRef *buf;
+} AVFrameSideData;
+
+/**
+ * This structure describes decoded (raw) audio or video data.
+ *
+ * AVFrame must be allocated using av_frame_alloc(). Note that this only
+ * allocates the AVFrame itself, the buffers for the data must be managed
+ * through other means (see below).
+ * AVFrame must be freed with av_frame_free().
+ *
+ * AVFrame is typically allocated once and then reused multiple times to hold
+ * different data (e.g. a single AVFrame to hold frames received from a
+ * decoder). In such a case, av_frame_unref() will free any references held by
+ * the frame and reset it to its original clean state before it
+ * is reused again.
+ *
+ * The data described by an AVFrame is usually reference counted through the
+ * AVBuffer API. The underlying buffer references are stored in AVFrame.buf /
+ * AVFrame.extended_buf. An AVFrame is considered to be reference counted if at
+ * least one reference is set, i.e. if AVFrame.buf[0] != NULL. In such a case,
+ * every single data plane must be contained in one of the buffers in
+ * AVFrame.buf or AVFrame.extended_buf.
+ * There may be a single buffer for all the data, or one separate buffer for
+ * each plane, or anything in between.
+ *
+ * sizeof(AVFrame) is not a part of the public ABI, so new fields may be added
+ * to the end with a minor bump.
+ * Similarly fields that are marked as to be only accessed by
+ * av_opt_ptr() can be reordered. This allows 2 forks to add fields
+ * without breaking compatibility with each other.
+ */
+typedef struct AVFrame {
+#define AV_NUM_DATA_POINTERS 8
+ /**
+ * pointer to the picture/channel planes.
+ * This might be different from the first allocated byte
+ *
+ * Some decoders access areas outside 0,0 - width,height, please
+ * see avcodec_align_dimensions2(). Some filters and swscale can read
+ * up to 16 bytes beyond the planes, if these filters are to be used,
+ * then 16 extra bytes must be allocated.
+ */
+ uint8_t *data[AV_NUM_DATA_POINTERS];
+
+ /**
+ * For video, size in bytes of each picture line.
+ * For audio, size in bytes of each plane.
+ *
+ * For audio, only linesize[0] may be set. For planar audio, each channel
+ * plane must be the same size.
+ *
+ * For video the linesizes should be multiples of the CPUs alignment
+ * preference, this is 16 or 32 for modern desktop CPUs.
+ * Some code requires such alignment other code can be slower without
+ * correct alignment, for yet other it makes no difference.
+ *
+ * @note The linesize may be larger than the size of usable data -- there
+ * may be extra padding present for performance reasons.
+ */
+ int linesize[AV_NUM_DATA_POINTERS];
+
+ /**
+ * pointers to the data planes/channels.
+ *
+ * For video, this should simply point to data[].
+ *
+ * For planar audio, each channel has a separate data pointer, and
+ * linesize[0] contains the size of each channel buffer.
+ * For packed audio, there is just one data pointer, and linesize[0]
+ * contains the total size of the buffer for all channels.
+ *
+ * Note: Both data and extended_data should always be set in a valid frame,
+ * but for planar audio with more channels that can fit in data,
+ * extended_data must be used in order to access all channels.
+ */
+ uint8_t **extended_data;
+
+ /**
+ * width and height of the video frame
+ */
+ int width, height;
+
+ /**
+ * number of audio samples (per channel) described by this frame
+ */
+ int nb_samples;
+
+ /**
+ * format of the frame, -1 if unknown or unset
+ * Values correspond to enum AVPixelFormat for video frames,
+ * enum AVSampleFormat for audio)
+ */
+ int format;
+
+ /**
+ * 1 -> keyframe, 0-> not
+ */
+ int key_frame;
+
+ /**
+ * Picture type of the frame.
+ */
+ enum AVPictureType pict_type;
+
+ /**
+ * Sample aspect ratio for the video frame, 0/1 if unknown/unspecified.
+ */
+ AVRational sample_aspect_ratio;
+
+ /**
+ * Presentation timestamp in time_base units (time when frame should be shown to user).
+ */
+ int64_t pts;
+
+ /**
+ * PTS copied from the AVPacket that was decoded to produce this frame.
+ */
+ int64_t pkt_pts;
+
+ /**
+ * DTS copied from the AVPacket that triggered returning this frame. (if frame threading isn't used)
+ * This is also the Presentation time of this AVFrame calculated from
+ * only AVPacket.dts values without pts values.
+ */
+ int64_t pkt_dts;
+
+ /**
+ * picture number in bitstream order
+ */
+ int coded_picture_number;
+ /**
+ * picture number in display order
+ */
+ int display_picture_number;
+
+ /**
+ * quality (between 1 (good) and FF_LAMBDA_MAX (bad))
+ */
+ int quality;
+
+ /**
+ * for some private data of the user
+ */
+ void *opaque;
+
+#if FF_API_ERROR_FRAME
+ /**
+ * @deprecated unused
+ */
+ attribute_deprecated
+ uint64_t error[AV_NUM_DATA_POINTERS];
+#endif
+
+ /**
+ * When decoding, this signals how much the picture must be delayed.
+ * extra_delay = repeat_pict / (2*fps)
+ */
+ int repeat_pict;
+
+ /**
+ * The content of the picture is interlaced.
+ */
+ int interlaced_frame;
+
+ /**
+ * If the content is interlaced, is top field displayed first.
+ */
+ int top_field_first;
+
+ /**
+ * Tell user application that palette has changed from previous frame.
+ */
+ int palette_has_changed;
+
+ /**
+ * reordered opaque 64bit (generally an integer or a double precision float
+ * PTS but can be anything).
+ * The user sets AVCodecContext.reordered_opaque to represent the input at
+ * that time,
+ * the decoder reorders values as needed and sets AVFrame.reordered_opaque
+ * to exactly one of the values provided by the user through AVCodecContext.reordered_opaque
+ * @deprecated in favor of pkt_pts
+ */
+ int64_t reordered_opaque;
+
+ /**
+ * Sample rate of the audio data.
+ */
+ int sample_rate;
+
+ /**
+ * Channel layout of the audio data.
+ */
+ uint64_t channel_layout;
+
+ /**
+ * AVBuffer references backing the data for this frame. If all elements of
+ * this array are NULL, then this frame is not reference counted. This array
+ * must be filled contiguously -- if buf[i] is non-NULL then buf[j] must
+ * also be non-NULL for all j < i.
+ *
+ * There may be at most one AVBuffer per data plane, so for video this array
+ * always contains all the references. For planar audio with more than
+ * AV_NUM_DATA_POINTERS channels, there may be more buffers than can fit in
+ * this array. Then the extra AVBufferRef pointers are stored in the
+ * extended_buf array.
+ */
+ AVBufferRef *buf[AV_NUM_DATA_POINTERS];
+
+ /**
+ * For planar audio which requires more than AV_NUM_DATA_POINTERS
+ * AVBufferRef pointers, this array will hold all the references which
+ * cannot fit into AVFrame.buf.
+ *
+ * Note that this is different from AVFrame.extended_data, which always
+ * contains all the pointers. This array only contains the extra pointers,
+ * which cannot fit into AVFrame.buf.
+ *
+ * This array is always allocated using av_malloc() by whoever constructs
+ * the frame. It is freed in av_frame_unref().
+ */
+ AVBufferRef **extended_buf;
+ /**
+ * Number of elements in extended_buf.
+ */
+ int nb_extended_buf;
+
+ AVFrameSideData **side_data;
+ int nb_side_data;
+
+/**
+ * @defgroup lavu_frame_flags AV_FRAME_FLAGS
+ * Flags describing additional frame properties.
+ *
+ * @{
+ */
+
+/**
+ * The frame data may be corrupted, e.g. due to decoding errors.
+ */
+#define AV_FRAME_FLAG_CORRUPT (1 << 0)
+/**
+ * @}
+ */
+
+ /**
+ * Frame flags, a combination of @ref lavu_frame_flags
+ */
+ int flags;
+
+ /**
+ * MPEG vs JPEG YUV range.
+ * It must be accessed using av_frame_get_color_range() and
+ * av_frame_set_color_range().
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorRange color_range;
+
+ enum AVColorPrimaries color_primaries;
+
+ enum AVColorTransferCharacteristic color_trc;
+
+ /**
+ * YUV colorspace type.
+ * It must be accessed using av_frame_get_colorspace() and
+ * av_frame_set_colorspace().
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorSpace colorspace;
+
+ enum AVChromaLocation chroma_location;
+
+ /**
+ * frame timestamp estimated using various heuristics, in stream time base
+ * Code outside libavutil should access this field using:
+ * av_frame_get_best_effort_timestamp(frame)
+ * - encoding: unused
+ * - decoding: set by libavcodec, read by user.
+ */
+ int64_t best_effort_timestamp;
+
+ /**
+ * reordered pos from the last AVPacket that has been input into the decoder
+ * Code outside libavutil should access this field using:
+ * av_frame_get_pkt_pos(frame)
+ * - encoding: unused
+ * - decoding: Read by user.
+ */
+ int64_t pkt_pos;
+
+ /**
+ * duration of the corresponding packet, expressed in
+ * AVStream->time_base units, 0 if unknown.
+ * Code outside libavutil should access this field using:
+ * av_frame_get_pkt_duration(frame)
+ * - encoding: unused
+ * - decoding: Read by user.
+ */
+ int64_t pkt_duration;
+
+ /**
+ * metadata.
+ * Code outside libavutil should access this field using:
+ * av_frame_get_metadata(frame)
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ AVDictionary *metadata;
+
+ /**
+ * decode error flags of the frame, set to a combination of
+ * FF_DECODE_ERROR_xxx flags if the decoder produced a frame, but there
+ * were errors during the decoding.
+ * Code outside libavutil should access this field using:
+ * av_frame_get_decode_error_flags(frame)
+ * - encoding: unused
+ * - decoding: set by libavcodec, read by user.
+ */
+ int decode_error_flags;
+#define FF_DECODE_ERROR_INVALID_BITSTREAM 1
+#define FF_DECODE_ERROR_MISSING_REFERENCE 2
+
+ /**
+ * number of audio channels, only used for audio.
+ * Code outside libavutil should access this field using:
+ * av_frame_get_channels(frame)
+ * - encoding: unused
+ * - decoding: Read by user.
+ */
+ int channels;
+
+ /**
+ * size of the corresponding packet containing the compressed
+ * frame. It must be accessed using av_frame_get_pkt_size() and
+ * av_frame_set_pkt_size().
+ * It is set to a negative value if unknown.
+ * - encoding: unused
+ * - decoding: set by libavcodec, read by user.
+ */
+ int pkt_size;
+
+#if FF_API_FRAME_QP
+ /**
+ * QP table
+ * Not to be accessed directly from outside libavutil
+ */
+ attribute_deprecated
+ int8_t *qscale_table;
+ /**
+ * QP store stride
+ * Not to be accessed directly from outside libavutil
+ */
+ attribute_deprecated
+ int qstride;
+
+ attribute_deprecated
+ int qscale_type;
+
+ /**
+ * Not to be accessed directly from outside libavutil
+ */
+ AVBufferRef *qp_table_buf;
+#endif
+} AVFrame;
+
+/**
+ * Accessors for some AVFrame fields.
+ * The position of these field in the structure is not part of the ABI,
+ * they should not be accessed directly outside libavutil.
+ */
+int64_t av_frame_get_best_effort_timestamp(const AVFrame *frame);
+void av_frame_set_best_effort_timestamp(AVFrame *frame, int64_t val);
+int64_t av_frame_get_pkt_duration (const AVFrame *frame);
+void av_frame_set_pkt_duration (AVFrame *frame, int64_t val);
+int64_t av_frame_get_pkt_pos (const AVFrame *frame);
+void av_frame_set_pkt_pos (AVFrame *frame, int64_t val);
+int64_t av_frame_get_channel_layout (const AVFrame *frame);
+void av_frame_set_channel_layout (AVFrame *frame, int64_t val);
+int av_frame_get_channels (const AVFrame *frame);
+void av_frame_set_channels (AVFrame *frame, int val);
+int av_frame_get_sample_rate (const AVFrame *frame);
+void av_frame_set_sample_rate (AVFrame *frame, int val);
+AVDictionary *av_frame_get_metadata (const AVFrame *frame);
+void av_frame_set_metadata (AVFrame *frame, AVDictionary *val);
+int av_frame_get_decode_error_flags (const AVFrame *frame);
+void av_frame_set_decode_error_flags (AVFrame *frame, int val);
+int av_frame_get_pkt_size(const AVFrame *frame);
+void av_frame_set_pkt_size(AVFrame *frame, int val);
+AVDictionary **avpriv_frame_get_metadatap(AVFrame *frame);
+#if FF_API_FRAME_QP
+int8_t *av_frame_get_qp_table(AVFrame *f, int *stride, int *type);
+int av_frame_set_qp_table(AVFrame *f, AVBufferRef *buf, int stride, int type);
+#endif
+enum AVColorSpace av_frame_get_colorspace(const AVFrame *frame);
+void av_frame_set_colorspace(AVFrame *frame, enum AVColorSpace val);
+enum AVColorRange av_frame_get_color_range(const AVFrame *frame);
+void av_frame_set_color_range(AVFrame *frame, enum AVColorRange val);
+
+/**
+ * Get the name of a colorspace.
+ * @return a static string identifying the colorspace; can be NULL.
+ */
+const char *av_get_colorspace_name(enum AVColorSpace val);
+
+/**
+ * Allocate an AVFrame and set its fields to default values. The resulting
+ * struct must be freed using av_frame_free().
+ *
+ * @return An AVFrame filled with default values or NULL on failure.
+ *
+ * @note this only allocates the AVFrame itself, not the data buffers. Those
+ * must be allocated through other means, e.g. with av_frame_get_buffer() or
+ * manually.
+ */
+AVFrame *av_frame_alloc(void);
+
+/**
+ * Free the frame and any dynamically allocated objects in it,
+ * e.g. extended_data. If the frame is reference counted, it will be
+ * unreferenced first.
+ *
+ * @param frame frame to be freed. The pointer will be set to NULL.
+ */
+void av_frame_free(AVFrame **frame);
+
+/**
+ * Set up a new reference to the data described by the source frame.
+ *
+ * Copy frame properties from src to dst and create a new reference for each
+ * AVBufferRef from src.
+ *
+ * If src is not reference counted, new buffers are allocated and the data is
+ * copied.
+ *
+ * @return 0 on success, a negative AVERROR on error
+ */
+int av_frame_ref(AVFrame *dst, const AVFrame *src);
+
+/**
+ * Create a new frame that references the same data as src.
+ *
+ * This is a shortcut for av_frame_alloc()+av_frame_ref().
+ *
+ * @return newly created AVFrame on success, NULL on error.
+ */
+AVFrame *av_frame_clone(const AVFrame *src);
+
+/**
+ * Unreference all the buffers referenced by frame and reset the frame fields.
+ */
+void av_frame_unref(AVFrame *frame);
+
+/**
+ * Move everything contained in src to dst and reset src.
+ */
+void av_frame_move_ref(AVFrame *dst, AVFrame *src);
+
+/**
+ * Allocate new buffer(s) for audio or video data.
+ *
+ * The following fields must be set on frame before calling this function:
+ * - format (pixel format for video, sample format for audio)
+ * - width and height for video
+ * - nb_samples and channel_layout for audio
+ *
+ * This function will fill AVFrame.data and AVFrame.buf arrays and, if
+ * necessary, allocate and fill AVFrame.extended_data and AVFrame.extended_buf.
+ * For planar formats, one buffer will be allocated for each plane.
+ *
+ * @param frame frame in which to store the new buffers.
+ * @param align required buffer size alignment
+ *
+ * @return 0 on success, a negative AVERROR on error.
+ */
+int av_frame_get_buffer(AVFrame *frame, int align);
+
+/**
+ * Check if the frame data is writable.
+ *
+ * @return A positive value if the frame data is writable (which is true if and
+ * only if each of the underlying buffers has only one reference, namely the one
+ * stored in this frame). Return 0 otherwise.
+ *
+ * If 1 is returned the answer is valid until av_buffer_ref() is called on any
+ * of the underlying AVBufferRefs (e.g. through av_frame_ref() or directly).
+ *
+ * @see av_frame_make_writable(), av_buffer_is_writable()
+ */
+int av_frame_is_writable(AVFrame *frame);
+
+/**
+ * Ensure that the frame data is writable, avoiding data copy if possible.
+ *
+ * Do nothing if the frame is writable, allocate new buffers and copy the data
+ * if it is not.
+ *
+ * @return 0 on success, a negative AVERROR on error.
+ *
+ * @see av_frame_is_writable(), av_buffer_is_writable(),
+ * av_buffer_make_writable()
+ */
+int av_frame_make_writable(AVFrame *frame);
+
+/**
+ * Copy the frame data from src to dst.
+ *
+ * This function does not allocate anything, dst must be already initialized and
+ * allocated with the same parameters as src.
+ *
+ * This function only copies the frame data (i.e. the contents of the data /
+ * extended data arrays), not any other properties.
+ *
+ * @return >= 0 on success, a negative AVERROR on error.
+ */
+int av_frame_copy(AVFrame *dst, const AVFrame *src);
+
+/**
+ * Copy only "metadata" fields from src to dst.
+ *
+ * Metadata for the purpose of this function are those fields that do not affect
+ * the data layout in the buffers. E.g. pts, sample rate (for audio) or sample
+ * aspect ratio (for video), but not width/height or channel layout.
+ * Side data is also copied.
+ */
+int av_frame_copy_props(AVFrame *dst, const AVFrame *src);
+
+/**
+ * Get the buffer reference a given data plane is stored in.
+ *
+ * @param plane index of the data plane of interest in frame->extended_data.
+ *
+ * @return the buffer reference that contains the plane or NULL if the input
+ * frame is not valid.
+ */
+AVBufferRef *av_frame_get_plane_buffer(AVFrame *frame, int plane);
+
+/**
+ * Add a new side data to a frame.
+ *
+ * @param frame a frame to which the side data should be added
+ * @param type type of the added side data
+ * @param size size of the side data
+ *
+ * @return newly added side data on success, NULL on error
+ */
+AVFrameSideData *av_frame_new_side_data(AVFrame *frame,
+ enum AVFrameSideDataType type,
+ int size);
+
+/**
+ * @return a pointer to the side data of a given type on success, NULL if there
+ * is no side data with such type in this frame.
+ */
+AVFrameSideData *av_frame_get_side_data(const AVFrame *frame,
+ enum AVFrameSideDataType type);
+
+/**
+ * If side data of the supplied type exists in the frame, free it and remove it
+ * from the frame.
+ */
+void av_frame_remove_side_data(AVFrame *frame, enum AVFrameSideDataType type);
+
+/**
+ * @return a string identifying the side data type
+ */
+const char *av_frame_side_data_name(enum AVFrameSideDataType type);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_FRAME_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/intfloat.h b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/intfloat.h
new file mode 100644
index 0000000000..fe3d7ec4a5
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/intfloat.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2011 Mans Rullgard
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_INTFLOAT_H
+#define AVUTIL_INTFLOAT_H
+
+#include <stdint.h>
+#include "attributes.h"
+
+union av_intfloat32 {
+ uint32_t i;
+ float f;
+};
+
+union av_intfloat64 {
+ uint64_t i;
+ double f;
+};
+
+/**
+ * Reinterpret a 32-bit integer as a float.
+ */
+static av_always_inline float av_int2float(uint32_t i)
+{
+ union av_intfloat32 v;
+ v.i = i;
+ return v.f;
+}
+
+/**
+ * Reinterpret a float as a 32-bit integer.
+ */
+static av_always_inline uint32_t av_float2int(float f)
+{
+ union av_intfloat32 v;
+ v.f = f;
+ return v.i;
+}
+
+/**
+ * Reinterpret a 64-bit integer as a double.
+ */
+static av_always_inline double av_int2double(uint64_t i)
+{
+ union av_intfloat64 v;
+ v.i = i;
+ return v.f;
+}
+
+/**
+ * Reinterpret a double as a 64-bit integer.
+ */
+static av_always_inline uint64_t av_double2int(double f)
+{
+ union av_intfloat64 v;
+ v.f = f;
+ return v.i;
+}
+
+#endif /* AVUTIL_INTFLOAT_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/log.h b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/log.h
new file mode 100644
index 0000000000..321748cd80
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/log.h
@@ -0,0 +1,359 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_LOG_H
+#define AVUTIL_LOG_H
+
+#include <stdarg.h>
+#include "avutil.h"
+#include "attributes.h"
+#include "version.h"
+
+typedef enum {
+ AV_CLASS_CATEGORY_NA = 0,
+ AV_CLASS_CATEGORY_INPUT,
+ AV_CLASS_CATEGORY_OUTPUT,
+ AV_CLASS_CATEGORY_MUXER,
+ AV_CLASS_CATEGORY_DEMUXER,
+ AV_CLASS_CATEGORY_ENCODER,
+ AV_CLASS_CATEGORY_DECODER,
+ AV_CLASS_CATEGORY_FILTER,
+ AV_CLASS_CATEGORY_BITSTREAM_FILTER,
+ AV_CLASS_CATEGORY_SWSCALER,
+ AV_CLASS_CATEGORY_SWRESAMPLER,
+ AV_CLASS_CATEGORY_DEVICE_VIDEO_OUTPUT = 40,
+ AV_CLASS_CATEGORY_DEVICE_VIDEO_INPUT,
+ AV_CLASS_CATEGORY_DEVICE_AUDIO_OUTPUT,
+ AV_CLASS_CATEGORY_DEVICE_AUDIO_INPUT,
+ AV_CLASS_CATEGORY_DEVICE_OUTPUT,
+ AV_CLASS_CATEGORY_DEVICE_INPUT,
+ AV_CLASS_CATEGORY_NB, ///< not part of ABI/API
+}AVClassCategory;
+
+#define AV_IS_INPUT_DEVICE(category) \
+ (((category) == AV_CLASS_CATEGORY_DEVICE_VIDEO_INPUT) || \
+ ((category) == AV_CLASS_CATEGORY_DEVICE_AUDIO_INPUT) || \
+ ((category) == AV_CLASS_CATEGORY_DEVICE_INPUT))
+
+#define AV_IS_OUTPUT_DEVICE(category) \
+ (((category) == AV_CLASS_CATEGORY_DEVICE_VIDEO_OUTPUT) || \
+ ((category) == AV_CLASS_CATEGORY_DEVICE_AUDIO_OUTPUT) || \
+ ((category) == AV_CLASS_CATEGORY_DEVICE_OUTPUT))
+
+struct AVOptionRanges;
+
+/**
+ * Describe the class of an AVClass context structure. That is an
+ * arbitrary struct of which the first field is a pointer to an
+ * AVClass struct (e.g. AVCodecContext, AVFormatContext etc.).
+ */
+typedef struct AVClass {
+ /**
+ * The name of the class; usually it is the same name as the
+ * context structure type to which the AVClass is associated.
+ */
+ const char* class_name;
+
+ /**
+ * A pointer to a function which returns the name of a context
+ * instance ctx associated with the class.
+ */
+ const char* (*item_name)(void* ctx);
+
+ /**
+ * a pointer to the first option specified in the class if any or NULL
+ *
+ * @see av_set_default_options()
+ */
+ const struct AVOption *option;
+
+ /**
+ * LIBAVUTIL_VERSION with which this structure was created.
+ * This is used to allow fields to be added without requiring major
+ * version bumps everywhere.
+ */
+
+ int version;
+
+ /**
+ * Offset in the structure where log_level_offset is stored.
+ * 0 means there is no such variable
+ */
+ int log_level_offset_offset;
+
+ /**
+ * Offset in the structure where a pointer to the parent context for
+ * logging is stored. For example a decoder could pass its AVCodecContext
+ * to eval as such a parent context, which an av_log() implementation
+ * could then leverage to display the parent context.
+ * The offset can be NULL.
+ */
+ int parent_log_context_offset;
+
+ /**
+ * Return next AVOptions-enabled child or NULL
+ */
+ void* (*child_next)(void *obj, void *prev);
+
+ /**
+ * Return an AVClass corresponding to the next potential
+ * AVOptions-enabled child.
+ *
+ * The difference between child_next and this is that
+ * child_next iterates over _already existing_ objects, while
+ * child_class_next iterates over _all possible_ children.
+ */
+ const struct AVClass* (*child_class_next)(const struct AVClass *prev);
+
+ /**
+ * Category used for visualization (like color)
+ * This is only set if the category is equal for all objects using this class.
+ * available since version (51 << 16 | 56 << 8 | 100)
+ */
+ AVClassCategory category;
+
+ /**
+ * Callback to return the category.
+ * available since version (51 << 16 | 59 << 8 | 100)
+ */
+ AVClassCategory (*get_category)(void* ctx);
+
+ /**
+ * Callback to return the supported/allowed ranges.
+ * available since version (52.12)
+ */
+ int (*query_ranges)(struct AVOptionRanges **, void *obj, const char *key, int flags);
+} AVClass;
+
+/**
+ * @addtogroup lavu_log
+ *
+ * @{
+ *
+ * @defgroup lavu_log_constants Logging Constants
+ *
+ * @{
+ */
+
+/**
+ * Print no output.
+ */
+#define AV_LOG_QUIET -8
+
+/**
+ * Something went really wrong and we will crash now.
+ */
+#define AV_LOG_PANIC 0
+
+/**
+ * Something went wrong and recovery is not possible.
+ * For example, no header was found for a format which depends
+ * on headers or an illegal combination of parameters is used.
+ */
+#define AV_LOG_FATAL 8
+
+/**
+ * Something went wrong and cannot losslessly be recovered.
+ * However, not all future data is affected.
+ */
+#define AV_LOG_ERROR 16
+
+/**
+ * Something somehow does not look correct. This may or may not
+ * lead to problems. An example would be the use of '-vstrict -2'.
+ */
+#define AV_LOG_WARNING 24
+
+/**
+ * Standard information.
+ */
+#define AV_LOG_INFO 32
+
+/**
+ * Detailed information.
+ */
+#define AV_LOG_VERBOSE 40
+
+/**
+ * Stuff which is only useful for libav* developers.
+ */
+#define AV_LOG_DEBUG 48
+
+/**
+ * Extremely verbose debugging, useful for libav* development.
+ */
+#define AV_LOG_TRACE 56
+
+#define AV_LOG_MAX_OFFSET (AV_LOG_TRACE - AV_LOG_QUIET)
+
+/**
+ * @}
+ */
+
+/**
+ * Sets additional colors for extended debugging sessions.
+ * @code
+ av_log(ctx, AV_LOG_DEBUG|AV_LOG_C(134), "Message in purple\n");
+ @endcode
+ * Requires 256color terminal support. Uses outside debugging is not
+ * recommended.
+ */
+#define AV_LOG_C(x) ((x) << 8)
+
+/**
+ * Send the specified message to the log if the level is less than or equal
+ * to the current av_log_level. By default, all logging messages are sent to
+ * stderr. This behavior can be altered by setting a different logging callback
+ * function.
+ * @see av_log_set_callback
+ *
+ * @param avcl A pointer to an arbitrary struct of which the first field is a
+ * pointer to an AVClass struct or NULL if general log.
+ * @param level The importance level of the message expressed using a @ref
+ * lavu_log_constants "Logging Constant".
+ * @param fmt The format string (printf-compatible) that specifies how
+ * subsequent arguments are converted to output.
+ */
+void av_log(void *avcl, int level, const char *fmt, ...) av_printf_format(3, 4);
+
+
+/**
+ * Send the specified message to the log if the level is less than or equal
+ * to the current av_log_level. By default, all logging messages are sent to
+ * stderr. This behavior can be altered by setting a different logging callback
+ * function.
+ * @see av_log_set_callback
+ *
+ * @param avcl A pointer to an arbitrary struct of which the first field is a
+ * pointer to an AVClass struct.
+ * @param level The importance level of the message expressed using a @ref
+ * lavu_log_constants "Logging Constant".
+ * @param fmt The format string (printf-compatible) that specifies how
+ * subsequent arguments are converted to output.
+ * @param vl The arguments referenced by the format string.
+ */
+void av_vlog(void *avcl, int level, const char *fmt, va_list vl);
+
+/**
+ * Get the current log level
+ *
+ * @see lavu_log_constants
+ *
+ * @return Current log level
+ */
+int av_log_get_level(void);
+
+/**
+ * Set the log level
+ *
+ * @see lavu_log_constants
+ *
+ * @param level Logging level
+ */
+void av_log_set_level(int level);
+
+/**
+ * Set the logging callback
+ *
+ * @note The callback must be thread safe, even if the application does not use
+ * threads itself as some codecs are multithreaded.
+ *
+ * @see av_log_default_callback
+ *
+ * @param callback A logging function with a compatible signature.
+ */
+void av_log_set_callback(void (*callback)(void*, int, const char*, va_list));
+
+/**
+ * Default logging callback
+ *
+ * It prints the message to stderr, optionally colorizing it.
+ *
+ * @param avcl A pointer to an arbitrary struct of which the first field is a
+ * pointer to an AVClass struct.
+ * @param level The importance level of the message expressed using a @ref
+ * lavu_log_constants "Logging Constant".
+ * @param fmt The format string (printf-compatible) that specifies how
+ * subsequent arguments are converted to output.
+ * @param vl The arguments referenced by the format string.
+ */
+void av_log_default_callback(void *avcl, int level, const char *fmt,
+ va_list vl);
+
+/**
+ * Return the context name
+ *
+ * @param ctx The AVClass context
+ *
+ * @return The AVClass class_name
+ */
+const char* av_default_item_name(void* ctx);
+AVClassCategory av_default_get_category(void *ptr);
+
+/**
+ * Format a line of log the same way as the default callback.
+ * @param line buffer to receive the formatted line
+ * @param line_size size of the buffer
+ * @param print_prefix used to store whether the prefix must be printed;
+ * must point to a persistent integer initially set to 1
+ */
+void av_log_format_line(void *ptr, int level, const char *fmt, va_list vl,
+ char *line, int line_size, int *print_prefix);
+
+#if FF_API_DLOG
+/**
+ * av_dlog macros
+ * @deprecated unused
+ * Useful to print debug messages that shouldn't get compiled in normally.
+ */
+
+#ifdef DEBUG
+# define av_dlog(pctx, ...) av_log(pctx, AV_LOG_DEBUG, __VA_ARGS__)
+#else
+# define av_dlog(pctx, ...) do { if (0) av_log(pctx, AV_LOG_DEBUG, __VA_ARGS__); } while (0)
+#endif
+#endif /* FF_API_DLOG */
+
+/**
+ * Skip repeated messages, this requires the user app to use av_log() instead of
+ * (f)printf as the 2 would otherwise interfere and lead to
+ * "Last message repeated x times" messages below (f)printf messages with some
+ * bad luck.
+ * Also to receive the last, "last repeated" line if any, the user app must
+ * call av_log(NULL, AV_LOG_QUIET, "%s", ""); at the end
+ */
+#define AV_LOG_SKIP_REPEATED 1
+
+/**
+ * Include the log severity in messages originating from codecs.
+ *
+ * Results in messages such as:
+ * [rawvideo @ 0xDEADBEEF] [error] encode did not produce valid pts
+ */
+#define AV_LOG_PRINT_LEVEL 2
+
+void av_log_set_flags(int arg);
+int av_log_get_flags(void);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_LOG_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/macros.h b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/macros.h
new file mode 100644
index 0000000000..2007ee5619
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/macros.h
@@ -0,0 +1,50 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * @ingroup lavu
+ * Utility Preprocessor macros
+ */
+
+#ifndef AVUTIL_MACROS_H
+#define AVUTIL_MACROS_H
+
+/**
+ * @addtogroup preproc_misc Preprocessor String Macros
+ *
+ * String manipulation macros
+ *
+ * @{
+ */
+
+#define AV_STRINGIFY(s) AV_TOSTRING(s)
+#define AV_TOSTRING(s) #s
+
+#define AV_GLUE(a, b) a ## b
+#define AV_JOIN(a, b) AV_GLUE(a, b)
+
+/**
+ * @}
+ */
+
+#define AV_PRAGMA(s) _Pragma(#s)
+
+#define FFALIGN(x, a) (((x)+(a)-1)&~((a)-1))
+
+#endif /* AVUTIL_MACROS_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/mathematics.h b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/mathematics.h
new file mode 100644
index 0000000000..57c44f845d
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/mathematics.h
@@ -0,0 +1,165 @@
+/*
+ * copyright (c) 2005-2012 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_MATHEMATICS_H
+#define AVUTIL_MATHEMATICS_H
+
+#include <stdint.h>
+#include <math.h>
+#include "attributes.h"
+#include "rational.h"
+#include "intfloat.h"
+
+#ifndef M_E
+#define M_E 2.7182818284590452354 /* e */
+#endif
+#ifndef M_LN2
+#define M_LN2 0.69314718055994530942 /* log_e 2 */
+#endif
+#ifndef M_LN10
+#define M_LN10 2.30258509299404568402 /* log_e 10 */
+#endif
+#ifndef M_LOG2_10
+#define M_LOG2_10 3.32192809488736234787 /* log_2 10 */
+#endif
+#ifndef M_PHI
+#define M_PHI 1.61803398874989484820 /* phi / golden ratio */
+#endif
+#ifndef M_PI
+#define M_PI 3.14159265358979323846 /* pi */
+#endif
+#ifndef M_PI_2
+#define M_PI_2 1.57079632679489661923 /* pi/2 */
+#endif
+#ifndef M_SQRT1_2
+#define M_SQRT1_2 0.70710678118654752440 /* 1/sqrt(2) */
+#endif
+#ifndef M_SQRT2
+#define M_SQRT2 1.41421356237309504880 /* sqrt(2) */
+#endif
+#ifndef NAN
+#define NAN av_int2float(0x7fc00000)
+#endif
+#ifndef INFINITY
+#define INFINITY av_int2float(0x7f800000)
+#endif
+
+/**
+ * @addtogroup lavu_math
+ * @{
+ */
+
+
+enum AVRounding {
+ AV_ROUND_ZERO = 0, ///< Round toward zero.
+ AV_ROUND_INF = 1, ///< Round away from zero.
+ AV_ROUND_DOWN = 2, ///< Round toward -infinity.
+ AV_ROUND_UP = 3, ///< Round toward +infinity.
+ AV_ROUND_NEAR_INF = 5, ///< Round to nearest and halfway cases away from zero.
+ AV_ROUND_PASS_MINMAX = 8192, ///< Flag to pass INT64_MIN/MAX through instead of rescaling, this avoids special cases for AV_NOPTS_VALUE
+};
+
+/**
+ * Compute the greatest common divisor of a and b.
+ *
+ * @return gcd of a and b up to sign; if a >= 0 and b >= 0, return value is >= 0;
+ * if a == 0 and b == 0, returns 0.
+ */
+int64_t av_const av_gcd(int64_t a, int64_t b);
+
+/**
+ * Rescale a 64-bit integer with rounding to nearest.
+ * A simple a*b/c isn't possible as it can overflow.
+ */
+int64_t av_rescale(int64_t a, int64_t b, int64_t c) av_const;
+
+/**
+ * Rescale a 64-bit integer with specified rounding.
+ * A simple a*b/c isn't possible as it can overflow.
+ *
+ * @return rescaled value a, or if AV_ROUND_PASS_MINMAX is set and a is
+ * INT64_MIN or INT64_MAX then a is passed through unchanged.
+ */
+int64_t av_rescale_rnd(int64_t a, int64_t b, int64_t c, enum AVRounding) av_const;
+
+/**
+ * Rescale a 64-bit integer by 2 rational numbers.
+ */
+int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq) av_const;
+
+/**
+ * Rescale a 64-bit integer by 2 rational numbers with specified rounding.
+ *
+ * @return rescaled value a, or if AV_ROUND_PASS_MINMAX is set and a is
+ * INT64_MIN or INT64_MAX then a is passed through unchanged.
+ */
+int64_t av_rescale_q_rnd(int64_t a, AVRational bq, AVRational cq,
+ enum AVRounding) av_const;
+
+/**
+ * Compare 2 timestamps each in its own timebases.
+ * The result of the function is undefined if one of the timestamps
+ * is outside the int64_t range when represented in the others timebase.
+ * @return -1 if ts_a is before ts_b, 1 if ts_a is after ts_b or 0 if they represent the same position
+ */
+int av_compare_ts(int64_t ts_a, AVRational tb_a, int64_t ts_b, AVRational tb_b);
+
+/**
+ * Compare 2 integers modulo mod.
+ * That is we compare integers a and b for which only the least
+ * significant log2(mod) bits are known.
+ *
+ * @param mod must be a power of 2
+ * @return a negative value if a is smaller than b
+ * a positive value if a is greater than b
+ * 0 if a equals b
+ */
+int64_t av_compare_mod(uint64_t a, uint64_t b, uint64_t mod);
+
+/**
+ * Rescale a timestamp while preserving known durations.
+ *
+ * @param in_ts Input timestamp
+ * @param in_tb Input timebase
+ * @param fs_tb Duration and *last timebase
+ * @param duration duration till the next call
+ * @param out_tb Output timebase
+ */
+int64_t av_rescale_delta(AVRational in_tb, int64_t in_ts, AVRational fs_tb, int duration, int64_t *last, AVRational out_tb);
+
+/**
+ * Add a value to a timestamp.
+ *
+ * This function guarantees that when the same value is repeatly added that
+ * no accumulation of rounding errors occurs.
+ *
+ * @param ts Input timestamp
+ * @param ts_tb Input timestamp timebase
+ * @param inc value to add to ts
+ * @param inc_tb inc timebase
+ */
+int64_t av_add_stable(AVRational ts_tb, int64_t ts, AVRational inc_tb, int64_t inc);
+
+
+ /**
+ * @}
+ */
+
+#endif /* AVUTIL_MATHEMATICS_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/mem.h b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/mem.h
new file mode 100644
index 0000000000..d25b3229b7
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/mem.h
@@ -0,0 +1,406 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * memory handling functions
+ */
+
+#ifndef AVUTIL_MEM_H
+#define AVUTIL_MEM_H
+
+#include <limits.h>
+#include <stdint.h>
+
+#include "attributes.h"
+#include "error.h"
+#include "avutil.h"
+
+/**
+ * @addtogroup lavu_mem
+ * @{
+ */
+
+
+#if defined(__INTEL_COMPILER) && __INTEL_COMPILER < 1110 || defined(__SUNPRO_C)
+ #define DECLARE_ALIGNED(n,t,v) t __attribute__ ((aligned (n))) v
+ #define DECLARE_ASM_CONST(n,t,v) const t __attribute__ ((aligned (n))) v
+#elif defined(__TI_COMPILER_VERSION__)
+ #define DECLARE_ALIGNED(n,t,v) \
+ AV_PRAGMA(DATA_ALIGN(v,n)) \
+ t __attribute__((aligned(n))) v
+ #define DECLARE_ASM_CONST(n,t,v) \
+ AV_PRAGMA(DATA_ALIGN(v,n)) \
+ static const t __attribute__((aligned(n))) v
+#elif defined(__GNUC__)
+ #define DECLARE_ALIGNED(n,t,v) t __attribute__ ((aligned (n))) v
+ #define DECLARE_ASM_CONST(n,t,v) static const t av_used __attribute__ ((aligned (n))) v
+#elif defined(_MSC_VER)
+ #define DECLARE_ALIGNED(n,t,v) __declspec(align(n)) t v
+ #define DECLARE_ASM_CONST(n,t,v) __declspec(align(n)) static const t v
+#else
+ #define DECLARE_ALIGNED(n,t,v) t v
+ #define DECLARE_ASM_CONST(n,t,v) static const t v
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3,1)
+ #define av_malloc_attrib __attribute__((__malloc__))
+#else
+ #define av_malloc_attrib
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(4,3)
+ #define av_alloc_size(...) __attribute__((alloc_size(__VA_ARGS__)))
+#else
+ #define av_alloc_size(...)
+#endif
+
+/**
+ * Allocate a block of size bytes with alignment suitable for all
+ * memory accesses (including vectors if available on the CPU).
+ * @param size Size in bytes for the memory block to be allocated.
+ * @return Pointer to the allocated block, NULL if the block cannot
+ * be allocated.
+ * @see av_mallocz()
+ */
+void *av_malloc(size_t size) av_malloc_attrib av_alloc_size(1);
+
+/**
+ * Allocate a block of size * nmemb bytes with av_malloc().
+ * @param nmemb Number of elements
+ * @param size Size of the single element
+ * @return Pointer to the allocated block, NULL if the block cannot
+ * be allocated.
+ * @see av_malloc()
+ */
+av_alloc_size(1, 2) static inline void *av_malloc_array(size_t nmemb, size_t size)
+{
+ if (!size || nmemb >= INT_MAX / size)
+ return NULL;
+ return av_malloc(nmemb * size);
+}
+
+/**
+ * Allocate or reallocate a block of memory.
+ * If ptr is NULL and size > 0, allocate a new block. If
+ * size is zero, free the memory block pointed to by ptr.
+ * @param ptr Pointer to a memory block already allocated with
+ * av_realloc() or NULL.
+ * @param size Size in bytes of the memory block to be allocated or
+ * reallocated.
+ * @return Pointer to a newly-reallocated block or NULL if the block
+ * cannot be reallocated or the function is used to free the memory block.
+ * @warning Pointers originating from the av_malloc() family of functions must
+ * not be passed to av_realloc(). The former can be implemented using
+ * memalign() (or other functions), and there is no guarantee that
+ * pointers from such functions can be passed to realloc() at all.
+ * The situation is undefined according to POSIX and may crash with
+ * some libc implementations.
+ * @see av_fast_realloc()
+ */
+void *av_realloc(void *ptr, size_t size) av_alloc_size(2);
+
+/**
+ * Allocate or reallocate a block of memory.
+ * This function does the same thing as av_realloc, except:
+ * - It takes two arguments and checks the result of the multiplication for
+ * integer overflow.
+ * - It frees the input block in case of failure, thus avoiding the memory
+ * leak with the classic "buf = realloc(buf); if (!buf) return -1;".
+ */
+void *av_realloc_f(void *ptr, size_t nelem, size_t elsize);
+
+/**
+ * Allocate or reallocate a block of memory.
+ * If *ptr is NULL and size > 0, allocate a new block. If
+ * size is zero, free the memory block pointed to by ptr.
+ * @param ptr Pointer to a pointer to a memory block already allocated
+ * with av_realloc(), or pointer to a pointer to NULL.
+ * The pointer is updated on success, or freed on failure.
+ * @param size Size in bytes for the memory block to be allocated or
+ * reallocated
+ * @return Zero on success, an AVERROR error code on failure.
+ * @warning Pointers originating from the av_malloc() family of functions must
+ * not be passed to av_reallocp(). The former can be implemented using
+ * memalign() (or other functions), and there is no guarantee that
+ * pointers from such functions can be passed to realloc() at all.
+ * The situation is undefined according to POSIX and may crash with
+ * some libc implementations.
+ */
+av_warn_unused_result
+int av_reallocp(void *ptr, size_t size);
+
+/**
+ * Allocate or reallocate an array.
+ * If ptr is NULL and nmemb > 0, allocate a new block. If
+ * nmemb is zero, free the memory block pointed to by ptr.
+ * @param ptr Pointer to a memory block already allocated with
+ * av_realloc() or NULL.
+ * @param nmemb Number of elements
+ * @param size Size of the single element
+ * @return Pointer to a newly-reallocated block or NULL if the block
+ * cannot be reallocated or the function is used to free the memory block.
+ * @warning Pointers originating from the av_malloc() family of functions must
+ * not be passed to av_realloc(). The former can be implemented using
+ * memalign() (or other functions), and there is no guarantee that
+ * pointers from such functions can be passed to realloc() at all.
+ * The situation is undefined according to POSIX and may crash with
+ * some libc implementations.
+ */
+av_alloc_size(2, 3) void *av_realloc_array(void *ptr, size_t nmemb, size_t size);
+
+/**
+ * Allocate or reallocate an array through a pointer to a pointer.
+ * If *ptr is NULL and nmemb > 0, allocate a new block. If
+ * nmemb is zero, free the memory block pointed to by ptr.
+ * @param ptr Pointer to a pointer to a memory block already allocated
+ * with av_realloc(), or pointer to a pointer to NULL.
+ * The pointer is updated on success, or freed on failure.
+ * @param nmemb Number of elements
+ * @param size Size of the single element
+ * @return Zero on success, an AVERROR error code on failure.
+ * @warning Pointers originating from the av_malloc() family of functions must
+ * not be passed to av_realloc(). The former can be implemented using
+ * memalign() (or other functions), and there is no guarantee that
+ * pointers from such functions can be passed to realloc() at all.
+ * The situation is undefined according to POSIX and may crash with
+ * some libc implementations.
+ */
+av_alloc_size(2, 3) int av_reallocp_array(void *ptr, size_t nmemb, size_t size);
+
+/**
+ * Free a memory block which has been allocated with av_malloc(z)() or
+ * av_realloc().
+ * @param ptr Pointer to the memory block which should be freed.
+ * @note ptr = NULL is explicitly allowed.
+ * @note It is recommended that you use av_freep() instead.
+ * @see av_freep()
+ */
+void av_free(void *ptr);
+
+/**
+ * Allocate a block of size bytes with alignment suitable for all
+ * memory accesses (including vectors if available on the CPU) and
+ * zero all the bytes of the block.
+ * @param size Size in bytes for the memory block to be allocated.
+ * @return Pointer to the allocated block, NULL if it cannot be allocated.
+ * @see av_malloc()
+ */
+void *av_mallocz(size_t size) av_malloc_attrib av_alloc_size(1);
+
+/**
+ * Allocate a block of nmemb * size bytes with alignment suitable for all
+ * memory accesses (including vectors if available on the CPU) and
+ * zero all the bytes of the block.
+ * The allocation will fail if nmemb * size is greater than or equal
+ * to INT_MAX.
+ * @param nmemb
+ * @param size
+ * @return Pointer to the allocated block, NULL if it cannot be allocated.
+ */
+void *av_calloc(size_t nmemb, size_t size) av_malloc_attrib;
+
+/**
+ * Allocate a block of size * nmemb bytes with av_mallocz().
+ * @param nmemb Number of elements
+ * @param size Size of the single element
+ * @return Pointer to the allocated block, NULL if the block cannot
+ * be allocated.
+ * @see av_mallocz()
+ * @see av_malloc_array()
+ */
+av_alloc_size(1, 2) static inline void *av_mallocz_array(size_t nmemb, size_t size)
+{
+ if (!size || nmemb >= INT_MAX / size)
+ return NULL;
+ return av_mallocz(nmemb * size);
+}
+
+/**
+ * Duplicate the string s.
+ * @param s string to be duplicated
+ * @return Pointer to a newly-allocated string containing a
+ * copy of s or NULL if the string cannot be allocated.
+ */
+char *av_strdup(const char *s) av_malloc_attrib;
+
+/**
+ * Duplicate a substring of the string s.
+ * @param s string to be duplicated
+ * @param len the maximum length of the resulting string (not counting the
+ * terminating byte).
+ * @return Pointer to a newly-allocated string containing a
+ * copy of s or NULL if the string cannot be allocated.
+ */
+char *av_strndup(const char *s, size_t len) av_malloc_attrib;
+
+/**
+ * Duplicate the buffer p.
+ * @param p buffer to be duplicated
+ * @return Pointer to a newly allocated buffer containing a
+ * copy of p or NULL if the buffer cannot be allocated.
+ */
+void *av_memdup(const void *p, size_t size);
+
+/**
+ * Free a memory block which has been allocated with av_malloc(z)() or
+ * av_realloc() and set the pointer pointing to it to NULL.
+ * @param ptr Pointer to the pointer to the memory block which should
+ * be freed.
+ * @note passing a pointer to a NULL pointer is safe and leads to no action.
+ * @see av_free()
+ */
+void av_freep(void *ptr);
+
+/**
+ * Add an element to a dynamic array.
+ *
+ * The array to grow is supposed to be an array of pointers to
+ * structures, and the element to add must be a pointer to an already
+ * allocated structure.
+ *
+ * The array is reallocated when its size reaches powers of 2.
+ * Therefore, the amortized cost of adding an element is constant.
+ *
+ * In case of success, the pointer to the array is updated in order to
+ * point to the new grown array, and the number pointed to by nb_ptr
+ * is incremented.
+ * In case of failure, the array is freed, *tab_ptr is set to NULL and
+ * *nb_ptr is set to 0.
+ *
+ * @param tab_ptr pointer to the array to grow
+ * @param nb_ptr pointer to the number of elements in the array
+ * @param elem element to add
+ * @see av_dynarray_add_nofree(), av_dynarray2_add()
+ */
+void av_dynarray_add(void *tab_ptr, int *nb_ptr, void *elem);
+
+/**
+ * Add an element to a dynamic array.
+ *
+ * Function has the same functionality as av_dynarray_add(),
+ * but it doesn't free memory on fails. It returns error code
+ * instead and leave current buffer untouched.
+ *
+ * @param tab_ptr pointer to the array to grow
+ * @param nb_ptr pointer to the number of elements in the array
+ * @param elem element to add
+ * @return >=0 on success, negative otherwise.
+ * @see av_dynarray_add(), av_dynarray2_add()
+ */
+av_warn_unused_result
+int av_dynarray_add_nofree(void *tab_ptr, int *nb_ptr, void *elem);
+
+/**
+ * Add an element of size elem_size to a dynamic array.
+ *
+ * The array is reallocated when its number of elements reaches powers of 2.
+ * Therefore, the amortized cost of adding an element is constant.
+ *
+ * In case of success, the pointer to the array is updated in order to
+ * point to the new grown array, and the number pointed to by nb_ptr
+ * is incremented.
+ * In case of failure, the array is freed, *tab_ptr is set to NULL and
+ * *nb_ptr is set to 0.
+ *
+ * @param tab_ptr pointer to the array to grow
+ * @param nb_ptr pointer to the number of elements in the array
+ * @param elem_size size in bytes of the elements in the array
+ * @param elem_data pointer to the data of the element to add. If NULL, the space of
+ * the new added element is not filled.
+ * @return pointer to the data of the element to copy in the new allocated space.
+ * If NULL, the new allocated space is left uninitialized."
+ * @see av_dynarray_add(), av_dynarray_add_nofree()
+ */
+void *av_dynarray2_add(void **tab_ptr, int *nb_ptr, size_t elem_size,
+ const uint8_t *elem_data);
+
+/**
+ * Multiply two size_t values checking for overflow.
+ * @return 0 if success, AVERROR(EINVAL) if overflow.
+ */
+static inline int av_size_mult(size_t a, size_t b, size_t *r)
+{
+ size_t t = a * b;
+ /* Hack inspired from glibc: only try the division if nelem and elsize
+ * are both greater than sqrt(SIZE_MAX). */
+ if ((a | b) >= ((size_t)1 << (sizeof(size_t) * 4)) && a && t / a != b)
+ return AVERROR(EINVAL);
+ *r = t;
+ return 0;
+}
+
+/**
+ * Set the maximum size that may me allocated in one block.
+ */
+void av_max_alloc(size_t max);
+
+/**
+ * deliberately overlapping memcpy implementation
+ * @param dst destination buffer
+ * @param back how many bytes back we start (the initial size of the overlapping window), must be > 0
+ * @param cnt number of bytes to copy, must be >= 0
+ *
+ * cnt > back is valid, this will copy the bytes we just copied,
+ * thus creating a repeating pattern with a period length of back.
+ */
+void av_memcpy_backptr(uint8_t *dst, int back, int cnt);
+
+/**
+ * Reallocate the given block if it is not large enough, otherwise do nothing.
+ *
+ * @see av_realloc
+ */
+void *av_fast_realloc(void *ptr, unsigned int *size, size_t min_size);
+
+/**
+ * Allocate a buffer, reusing the given one if large enough.
+ *
+ * Contrary to av_fast_realloc the current buffer contents might not be
+ * preserved and on error the old buffer is freed, thus no special
+ * handling to avoid memleaks is necessary.
+ *
+ * @param ptr pointer to pointer to already allocated buffer, overwritten with pointer to new buffer
+ * @param size size of the buffer *ptr points to
+ * @param min_size minimum size of *ptr buffer after returning, *ptr will be NULL and
+ * *size 0 if an error occurred.
+ */
+void av_fast_malloc(void *ptr, unsigned int *size, size_t min_size);
+
+/**
+ * Allocate a buffer, reusing the given one if large enough.
+ *
+ * All newly allocated space is initially cleared
+ * Contrary to av_fast_realloc the current buffer contents might not be
+ * preserved and on error the old buffer is freed, thus no special
+ * handling to avoid memleaks is necessary.
+ *
+ * @param ptr pointer to pointer to already allocated buffer, overwritten with pointer to new buffer
+ * @param size size of the buffer *ptr points to
+ * @param min_size minimum size of *ptr buffer after returning, *ptr will be NULL and
+ * *size 0 if an error occurred.
+ */
+void av_fast_mallocz(void *ptr, unsigned int *size, size_t min_size);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_MEM_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/pixfmt.h b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/pixfmt.h
new file mode 100644
index 0000000000..32044f0778
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/pixfmt.h
@@ -0,0 +1,469 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_PIXFMT_H
+#define AVUTIL_PIXFMT_H
+
+/**
+ * @file
+ * pixel format definitions
+ *
+ */
+
+#include "libavutil/avconfig.h"
+#include "version.h"
+
+#define AVPALETTE_SIZE 1024
+#define AVPALETTE_COUNT 256
+
+/**
+ * Pixel format.
+ *
+ * @note
+ * AV_PIX_FMT_RGB32 is handled in an endian-specific manner. An RGBA
+ * color is put together as:
+ * (A << 24) | (R << 16) | (G << 8) | B
+ * This is stored as BGRA on little-endian CPU architectures and ARGB on
+ * big-endian CPUs.
+ *
+ * @par
+ * When the pixel format is palettized RGB32 (AV_PIX_FMT_PAL8), the palettized
+ * image data is stored in AVFrame.data[0]. The palette is transported in
+ * AVFrame.data[1], is 1024 bytes long (256 4-byte entries) and is
+ * formatted the same as in AV_PIX_FMT_RGB32 described above (i.e., it is
+ * also endian-specific). Note also that the individual RGB32 palette
+ * components stored in AVFrame.data[1] should be in the range 0..255.
+ * This is important as many custom PAL8 video codecs that were designed
+ * to run on the IBM VGA graphics adapter use 6-bit palette components.
+ *
+ * @par
+ * For all the 8bit per pixel formats, an RGB32 palette is in data[1] like
+ * for pal8. This palette is filled in automatically by the function
+ * allocating the picture.
+ */
+enum AVPixelFormat {
+ AV_PIX_FMT_NONE = -1,
+ AV_PIX_FMT_YUV420P, ///< planar YUV 4:2:0, 12bpp, (1 Cr & Cb sample per 2x2 Y samples)
+ AV_PIX_FMT_YUYV422, ///< packed YUV 4:2:2, 16bpp, Y0 Cb Y1 Cr
+ AV_PIX_FMT_RGB24, ///< packed RGB 8:8:8, 24bpp, RGBRGB...
+ AV_PIX_FMT_BGR24, ///< packed RGB 8:8:8, 24bpp, BGRBGR...
+ AV_PIX_FMT_YUV422P, ///< planar YUV 4:2:2, 16bpp, (1 Cr & Cb sample per 2x1 Y samples)
+ AV_PIX_FMT_YUV444P, ///< planar YUV 4:4:4, 24bpp, (1 Cr & Cb sample per 1x1 Y samples)
+ AV_PIX_FMT_YUV410P, ///< planar YUV 4:1:0, 9bpp, (1 Cr & Cb sample per 4x4 Y samples)
+ AV_PIX_FMT_YUV411P, ///< planar YUV 4:1:1, 12bpp, (1 Cr & Cb sample per 4x1 Y samples)
+ AV_PIX_FMT_GRAY8, ///< Y , 8bpp
+ AV_PIX_FMT_MONOWHITE, ///< Y , 1bpp, 0 is white, 1 is black, in each byte pixels are ordered from the msb to the lsb
+ AV_PIX_FMT_MONOBLACK, ///< Y , 1bpp, 0 is black, 1 is white, in each byte pixels are ordered from the msb to the lsb
+ AV_PIX_FMT_PAL8, ///< 8 bit with AV_PIX_FMT_RGB32 palette
+ AV_PIX_FMT_YUVJ420P, ///< planar YUV 4:2:0, 12bpp, full scale (JPEG), deprecated in favor of AV_PIX_FMT_YUV420P and setting color_range
+ AV_PIX_FMT_YUVJ422P, ///< planar YUV 4:2:2, 16bpp, full scale (JPEG), deprecated in favor of AV_PIX_FMT_YUV422P and setting color_range
+ AV_PIX_FMT_YUVJ444P, ///< planar YUV 4:4:4, 24bpp, full scale (JPEG), deprecated in favor of AV_PIX_FMT_YUV444P and setting color_range
+#if FF_API_XVMC
+ AV_PIX_FMT_XVMC_MPEG2_MC,///< XVideo Motion Acceleration via common packet passing
+ AV_PIX_FMT_XVMC_MPEG2_IDCT,
+#define AV_PIX_FMT_XVMC AV_PIX_FMT_XVMC_MPEG2_IDCT
+#endif /* FF_API_XVMC */
+ AV_PIX_FMT_UYVY422, ///< packed YUV 4:2:2, 16bpp, Cb Y0 Cr Y1
+ AV_PIX_FMT_UYYVYY411, ///< packed YUV 4:1:1, 12bpp, Cb Y0 Y1 Cr Y2 Y3
+ AV_PIX_FMT_BGR8, ///< packed RGB 3:3:2, 8bpp, (msb)2B 3G 3R(lsb)
+ AV_PIX_FMT_BGR4, ///< packed RGB 1:2:1 bitstream, 4bpp, (msb)1B 2G 1R(lsb), a byte contains two pixels, the first pixel in the byte is the one composed by the 4 msb bits
+ AV_PIX_FMT_BGR4_BYTE, ///< packed RGB 1:2:1, 8bpp, (msb)1B 2G 1R(lsb)
+ AV_PIX_FMT_RGB8, ///< packed RGB 3:3:2, 8bpp, (msb)2R 3G 3B(lsb)
+ AV_PIX_FMT_RGB4, ///< packed RGB 1:2:1 bitstream, 4bpp, (msb)1R 2G 1B(lsb), a byte contains two pixels, the first pixel in the byte is the one composed by the 4 msb bits
+ AV_PIX_FMT_RGB4_BYTE, ///< packed RGB 1:2:1, 8bpp, (msb)1R 2G 1B(lsb)
+ AV_PIX_FMT_NV12, ///< planar YUV 4:2:0, 12bpp, 1 plane for Y and 1 plane for the UV components, which are interleaved (first byte U and the following byte V)
+ AV_PIX_FMT_NV21, ///< as above, but U and V bytes are swapped
+
+ AV_PIX_FMT_ARGB, ///< packed ARGB 8:8:8:8, 32bpp, ARGBARGB...
+ AV_PIX_FMT_RGBA, ///< packed RGBA 8:8:8:8, 32bpp, RGBARGBA...
+ AV_PIX_FMT_ABGR, ///< packed ABGR 8:8:8:8, 32bpp, ABGRABGR...
+ AV_PIX_FMT_BGRA, ///< packed BGRA 8:8:8:8, 32bpp, BGRABGRA...
+
+ AV_PIX_FMT_GRAY16BE, ///< Y , 16bpp, big-endian
+ AV_PIX_FMT_GRAY16LE, ///< Y , 16bpp, little-endian
+ AV_PIX_FMT_YUV440P, ///< planar YUV 4:4:0 (1 Cr & Cb sample per 1x2 Y samples)
+ AV_PIX_FMT_YUVJ440P, ///< planar YUV 4:4:0 full scale (JPEG), deprecated in favor of AV_PIX_FMT_YUV440P and setting color_range
+ AV_PIX_FMT_YUVA420P, ///< planar YUV 4:2:0, 20bpp, (1 Cr & Cb sample per 2x2 Y & A samples)
+#if FF_API_VDPAU
+ AV_PIX_FMT_VDPAU_H264,///< H.264 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ AV_PIX_FMT_VDPAU_MPEG1,///< MPEG-1 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ AV_PIX_FMT_VDPAU_MPEG2,///< MPEG-2 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ AV_PIX_FMT_VDPAU_WMV3,///< WMV3 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ AV_PIX_FMT_VDPAU_VC1, ///< VC-1 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+#endif
+ AV_PIX_FMT_RGB48BE, ///< packed RGB 16:16:16, 48bpp, 16R, 16G, 16B, the 2-byte value for each R/G/B component is stored as big-endian
+ AV_PIX_FMT_RGB48LE, ///< packed RGB 16:16:16, 48bpp, 16R, 16G, 16B, the 2-byte value for each R/G/B component is stored as little-endian
+
+ AV_PIX_FMT_RGB565BE, ///< packed RGB 5:6:5, 16bpp, (msb) 5R 6G 5B(lsb), big-endian
+ AV_PIX_FMT_RGB565LE, ///< packed RGB 5:6:5, 16bpp, (msb) 5R 6G 5B(lsb), little-endian
+ AV_PIX_FMT_RGB555BE, ///< packed RGB 5:5:5, 16bpp, (msb)1X 5R 5G 5B(lsb), big-endian , X=unused/undefined
+ AV_PIX_FMT_RGB555LE, ///< packed RGB 5:5:5, 16bpp, (msb)1X 5R 5G 5B(lsb), little-endian, X=unused/undefined
+
+ AV_PIX_FMT_BGR565BE, ///< packed BGR 5:6:5, 16bpp, (msb) 5B 6G 5R(lsb), big-endian
+ AV_PIX_FMT_BGR565LE, ///< packed BGR 5:6:5, 16bpp, (msb) 5B 6G 5R(lsb), little-endian
+ AV_PIX_FMT_BGR555BE, ///< packed BGR 5:5:5, 16bpp, (msb)1X 5B 5G 5R(lsb), big-endian , X=unused/undefined
+ AV_PIX_FMT_BGR555LE, ///< packed BGR 5:5:5, 16bpp, (msb)1X 5B 5G 5R(lsb), little-endian, X=unused/undefined
+
+#if FF_API_VAAPI
+ /** @name Deprecated pixel formats */
+ /**@{*/
+ AV_PIX_FMT_VAAPI_MOCO, ///< HW acceleration through VA API at motion compensation entry-point, Picture.data[3] contains a vaapi_render_state struct which contains macroblocks as well as various fields extracted from headers
+ AV_PIX_FMT_VAAPI_IDCT, ///< HW acceleration through VA API at IDCT entry-point, Picture.data[3] contains a vaapi_render_state struct which contains fields extracted from headers
+ AV_PIX_FMT_VAAPI_VLD, ///< HW decoding through VA API, Picture.data[3] contains a vaapi_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ /**@}*/
+ AV_PIX_FMT_VAAPI = AV_PIX_FMT_VAAPI_VLD,
+#else
+ /**
+ * Hardware acceleration through VA-API, data[3] contains a
+ * VASurfaceID.
+ */
+ AV_PIX_FMT_VAAPI,
+#endif
+
+ AV_PIX_FMT_YUV420P16LE, ///< planar YUV 4:2:0, 24bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian
+ AV_PIX_FMT_YUV420P16BE, ///< planar YUV 4:2:0, 24bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian
+ AV_PIX_FMT_YUV422P16LE, ///< planar YUV 4:2:2, 32bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ AV_PIX_FMT_YUV422P16BE, ///< planar YUV 4:2:2, 32bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+ AV_PIX_FMT_YUV444P16LE, ///< planar YUV 4:4:4, 48bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian
+ AV_PIX_FMT_YUV444P16BE, ///< planar YUV 4:4:4, 48bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian
+#if FF_API_VDPAU
+ AV_PIX_FMT_VDPAU_MPEG4, ///< MPEG4 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+#endif
+ AV_PIX_FMT_DXVA2_VLD, ///< HW decoding through DXVA2, Picture.data[3] contains a LPDIRECT3DSURFACE9 pointer
+
+ AV_PIX_FMT_RGB444LE, ///< packed RGB 4:4:4, 16bpp, (msb)4X 4R 4G 4B(lsb), little-endian, X=unused/undefined
+ AV_PIX_FMT_RGB444BE, ///< packed RGB 4:4:4, 16bpp, (msb)4X 4R 4G 4B(lsb), big-endian, X=unused/undefined
+ AV_PIX_FMT_BGR444LE, ///< packed BGR 4:4:4, 16bpp, (msb)4X 4B 4G 4R(lsb), little-endian, X=unused/undefined
+ AV_PIX_FMT_BGR444BE, ///< packed BGR 4:4:4, 16bpp, (msb)4X 4B 4G 4R(lsb), big-endian, X=unused/undefined
+ AV_PIX_FMT_YA8, ///< 8bit gray, 8bit alpha
+
+ AV_PIX_FMT_Y400A = AV_PIX_FMT_YA8, ///< alias for AV_PIX_FMT_YA8
+ AV_PIX_FMT_GRAY8A= AV_PIX_FMT_YA8, ///< alias for AV_PIX_FMT_YA8
+
+ AV_PIX_FMT_BGR48BE, ///< packed RGB 16:16:16, 48bpp, 16B, 16G, 16R, the 2-byte value for each R/G/B component is stored as big-endian
+ AV_PIX_FMT_BGR48LE, ///< packed RGB 16:16:16, 48bpp, 16B, 16G, 16R, the 2-byte value for each R/G/B component is stored as little-endian
+
+ /**
+ * The following 12 formats have the disadvantage of needing 1 format for each bit depth.
+ * Notice that each 9/10 bits sample is stored in 16 bits with extra padding.
+ * If you want to support multiple bit depths, then using AV_PIX_FMT_YUV420P16* with the bpp stored separately is better.
+ */
+ AV_PIX_FMT_YUV420P9BE, ///< planar YUV 4:2:0, 13.5bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian
+ AV_PIX_FMT_YUV420P9LE, ///< planar YUV 4:2:0, 13.5bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian
+ AV_PIX_FMT_YUV420P10BE,///< planar YUV 4:2:0, 15bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian
+ AV_PIX_FMT_YUV420P10LE,///< planar YUV 4:2:0, 15bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian
+ AV_PIX_FMT_YUV422P10BE,///< planar YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+ AV_PIX_FMT_YUV422P10LE,///< planar YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ AV_PIX_FMT_YUV444P9BE, ///< planar YUV 4:4:4, 27bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian
+ AV_PIX_FMT_YUV444P9LE, ///< planar YUV 4:4:4, 27bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian
+ AV_PIX_FMT_YUV444P10BE,///< planar YUV 4:4:4, 30bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian
+ AV_PIX_FMT_YUV444P10LE,///< planar YUV 4:4:4, 30bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian
+ AV_PIX_FMT_YUV422P9BE, ///< planar YUV 4:2:2, 18bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+ AV_PIX_FMT_YUV422P9LE, ///< planar YUV 4:2:2, 18bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ AV_PIX_FMT_VDA_VLD, ///< hardware decoding through VDA
+ AV_PIX_FMT_GBRP, ///< planar GBR 4:4:4 24bpp
+ AV_PIX_FMT_GBRP9BE, ///< planar GBR 4:4:4 27bpp, big-endian
+ AV_PIX_FMT_GBRP9LE, ///< planar GBR 4:4:4 27bpp, little-endian
+ AV_PIX_FMT_GBRP10BE, ///< planar GBR 4:4:4 30bpp, big-endian
+ AV_PIX_FMT_GBRP10LE, ///< planar GBR 4:4:4 30bpp, little-endian
+ AV_PIX_FMT_GBRP16BE, ///< planar GBR 4:4:4 48bpp, big-endian
+ AV_PIX_FMT_GBRP16LE, ///< planar GBR 4:4:4 48bpp, little-endian
+ AV_PIX_FMT_YUVA422P, ///< planar YUV 4:2:2 24bpp, (1 Cr & Cb sample per 2x1 Y & A samples)
+ AV_PIX_FMT_YUVA444P, ///< planar YUV 4:4:4 32bpp, (1 Cr & Cb sample per 1x1 Y & A samples)
+ AV_PIX_FMT_YUVA420P9BE, ///< planar YUV 4:2:0 22.5bpp, (1 Cr & Cb sample per 2x2 Y & A samples), big-endian
+ AV_PIX_FMT_YUVA420P9LE, ///< planar YUV 4:2:0 22.5bpp, (1 Cr & Cb sample per 2x2 Y & A samples), little-endian
+ AV_PIX_FMT_YUVA422P9BE, ///< planar YUV 4:2:2 27bpp, (1 Cr & Cb sample per 2x1 Y & A samples), big-endian
+ AV_PIX_FMT_YUVA422P9LE, ///< planar YUV 4:2:2 27bpp, (1 Cr & Cb sample per 2x1 Y & A samples), little-endian
+ AV_PIX_FMT_YUVA444P9BE, ///< planar YUV 4:4:4 36bpp, (1 Cr & Cb sample per 1x1 Y & A samples), big-endian
+ AV_PIX_FMT_YUVA444P9LE, ///< planar YUV 4:4:4 36bpp, (1 Cr & Cb sample per 1x1 Y & A samples), little-endian
+ AV_PIX_FMT_YUVA420P10BE, ///< planar YUV 4:2:0 25bpp, (1 Cr & Cb sample per 2x2 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA420P10LE, ///< planar YUV 4:2:0 25bpp, (1 Cr & Cb sample per 2x2 Y & A samples, little-endian)
+ AV_PIX_FMT_YUVA422P10BE, ///< planar YUV 4:2:2 30bpp, (1 Cr & Cb sample per 2x1 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA422P10LE, ///< planar YUV 4:2:2 30bpp, (1 Cr & Cb sample per 2x1 Y & A samples, little-endian)
+ AV_PIX_FMT_YUVA444P10BE, ///< planar YUV 4:4:4 40bpp, (1 Cr & Cb sample per 1x1 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA444P10LE, ///< planar YUV 4:4:4 40bpp, (1 Cr & Cb sample per 1x1 Y & A samples, little-endian)
+ AV_PIX_FMT_YUVA420P16BE, ///< planar YUV 4:2:0 40bpp, (1 Cr & Cb sample per 2x2 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA420P16LE, ///< planar YUV 4:2:0 40bpp, (1 Cr & Cb sample per 2x2 Y & A samples, little-endian)
+ AV_PIX_FMT_YUVA422P16BE, ///< planar YUV 4:2:2 48bpp, (1 Cr & Cb sample per 2x1 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA422P16LE, ///< planar YUV 4:2:2 48bpp, (1 Cr & Cb sample per 2x1 Y & A samples, little-endian)
+ AV_PIX_FMT_YUVA444P16BE, ///< planar YUV 4:4:4 64bpp, (1 Cr & Cb sample per 1x1 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA444P16LE, ///< planar YUV 4:4:4 64bpp, (1 Cr & Cb sample per 1x1 Y & A samples, little-endian)
+
+ AV_PIX_FMT_VDPAU, ///< HW acceleration through VDPAU, Picture.data[3] contains a VdpVideoSurface
+
+ AV_PIX_FMT_XYZ12LE, ///< packed XYZ 4:4:4, 36 bpp, (msb) 12X, 12Y, 12Z (lsb), the 2-byte value for each X/Y/Z is stored as little-endian, the 4 lower bits are set to 0
+ AV_PIX_FMT_XYZ12BE, ///< packed XYZ 4:4:4, 36 bpp, (msb) 12X, 12Y, 12Z (lsb), the 2-byte value for each X/Y/Z is stored as big-endian, the 4 lower bits are set to 0
+ AV_PIX_FMT_NV16, ///< interleaved chroma YUV 4:2:2, 16bpp, (1 Cr & Cb sample per 2x1 Y samples)
+ AV_PIX_FMT_NV20LE, ///< interleaved chroma YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ AV_PIX_FMT_NV20BE, ///< interleaved chroma YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+
+ AV_PIX_FMT_RGBA64BE, ///< packed RGBA 16:16:16:16, 64bpp, 16R, 16G, 16B, 16A, the 2-byte value for each R/G/B/A component is stored as big-endian
+ AV_PIX_FMT_RGBA64LE, ///< packed RGBA 16:16:16:16, 64bpp, 16R, 16G, 16B, 16A, the 2-byte value for each R/G/B/A component is stored as little-endian
+ AV_PIX_FMT_BGRA64BE, ///< packed RGBA 16:16:16:16, 64bpp, 16B, 16G, 16R, 16A, the 2-byte value for each R/G/B/A component is stored as big-endian
+ AV_PIX_FMT_BGRA64LE, ///< packed RGBA 16:16:16:16, 64bpp, 16B, 16G, 16R, 16A, the 2-byte value for each R/G/B/A component is stored as little-endian
+
+ AV_PIX_FMT_YVYU422, ///< packed YUV 4:2:2, 16bpp, Y0 Cr Y1 Cb
+
+ AV_PIX_FMT_VDA, ///< HW acceleration through VDA, data[3] contains a CVPixelBufferRef
+
+ AV_PIX_FMT_YA16BE, ///< 16bit gray, 16bit alpha (big-endian)
+ AV_PIX_FMT_YA16LE, ///< 16bit gray, 16bit alpha (little-endian)
+
+ AV_PIX_FMT_GBRAP, ///< planar GBRA 4:4:4:4 32bpp
+ AV_PIX_FMT_GBRAP16BE, ///< planar GBRA 4:4:4:4 64bpp, big-endian
+ AV_PIX_FMT_GBRAP16LE, ///< planar GBRA 4:4:4:4 64bpp, little-endian
+ /**
+ * HW acceleration through QSV, data[3] contains a pointer to the
+ * mfxFrameSurface1 structure.
+ */
+ AV_PIX_FMT_QSV,
+ /**
+ * HW acceleration though MMAL, data[3] contains a pointer to the
+ * MMAL_BUFFER_HEADER_T structure.
+ */
+ AV_PIX_FMT_MMAL,
+
+ AV_PIX_FMT_D3D11VA_VLD, ///< HW decoding through Direct3D11, Picture.data[3] contains a ID3D11VideoDecoderOutputView pointer
+
+ AV_PIX_FMT_0RGB=0x123+4,///< packed RGB 8:8:8, 32bpp, XRGBXRGB... X=unused/undefined
+ AV_PIX_FMT_RGB0, ///< packed RGB 8:8:8, 32bpp, RGBXRGBX... X=unused/undefined
+ AV_PIX_FMT_0BGR, ///< packed BGR 8:8:8, 32bpp, XBGRXBGR... X=unused/undefined
+ AV_PIX_FMT_BGR0, ///< packed BGR 8:8:8, 32bpp, BGRXBGRX... X=unused/undefined
+
+ AV_PIX_FMT_YUV420P12BE, ///< planar YUV 4:2:0,18bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian
+ AV_PIX_FMT_YUV420P12LE, ///< planar YUV 4:2:0,18bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian
+ AV_PIX_FMT_YUV420P14BE, ///< planar YUV 4:2:0,21bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian
+ AV_PIX_FMT_YUV420P14LE, ///< planar YUV 4:2:0,21bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian
+ AV_PIX_FMT_YUV422P12BE, ///< planar YUV 4:2:2,24bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+ AV_PIX_FMT_YUV422P12LE, ///< planar YUV 4:2:2,24bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ AV_PIX_FMT_YUV422P14BE, ///< planar YUV 4:2:2,28bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+ AV_PIX_FMT_YUV422P14LE, ///< planar YUV 4:2:2,28bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ AV_PIX_FMT_YUV444P12BE, ///< planar YUV 4:4:4,36bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian
+ AV_PIX_FMT_YUV444P12LE, ///< planar YUV 4:4:4,36bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian
+ AV_PIX_FMT_YUV444P14BE, ///< planar YUV 4:4:4,42bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian
+ AV_PIX_FMT_YUV444P14LE, ///< planar YUV 4:4:4,42bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian
+ AV_PIX_FMT_GBRP12BE, ///< planar GBR 4:4:4 36bpp, big-endian
+ AV_PIX_FMT_GBRP12LE, ///< planar GBR 4:4:4 36bpp, little-endian
+ AV_PIX_FMT_GBRP14BE, ///< planar GBR 4:4:4 42bpp, big-endian
+ AV_PIX_FMT_GBRP14LE, ///< planar GBR 4:4:4 42bpp, little-endian
+ AV_PIX_FMT_YUVJ411P, ///< planar YUV 4:1:1, 12bpp, (1 Cr & Cb sample per 4x1 Y samples) full scale (JPEG), deprecated in favor of AV_PIX_FMT_YUV411P and setting color_range
+
+ AV_PIX_FMT_BAYER_BGGR8, ///< bayer, BGBG..(odd line), GRGR..(even line), 8-bit samples */
+ AV_PIX_FMT_BAYER_RGGB8, ///< bayer, RGRG..(odd line), GBGB..(even line), 8-bit samples */
+ AV_PIX_FMT_BAYER_GBRG8, ///< bayer, GBGB..(odd line), RGRG..(even line), 8-bit samples */
+ AV_PIX_FMT_BAYER_GRBG8, ///< bayer, GRGR..(odd line), BGBG..(even line), 8-bit samples */
+ AV_PIX_FMT_BAYER_BGGR16LE, ///< bayer, BGBG..(odd line), GRGR..(even line), 16-bit samples, little-endian */
+ AV_PIX_FMT_BAYER_BGGR16BE, ///< bayer, BGBG..(odd line), GRGR..(even line), 16-bit samples, big-endian */
+ AV_PIX_FMT_BAYER_RGGB16LE, ///< bayer, RGRG..(odd line), GBGB..(even line), 16-bit samples, little-endian */
+ AV_PIX_FMT_BAYER_RGGB16BE, ///< bayer, RGRG..(odd line), GBGB..(even line), 16-bit samples, big-endian */
+ AV_PIX_FMT_BAYER_GBRG16LE, ///< bayer, GBGB..(odd line), RGRG..(even line), 16-bit samples, little-endian */
+ AV_PIX_FMT_BAYER_GBRG16BE, ///< bayer, GBGB..(odd line), RGRG..(even line), 16-bit samples, big-endian */
+ AV_PIX_FMT_BAYER_GRBG16LE, ///< bayer, GRGR..(odd line), BGBG..(even line), 16-bit samples, little-endian */
+ AV_PIX_FMT_BAYER_GRBG16BE, ///< bayer, GRGR..(odd line), BGBG..(even line), 16-bit samples, big-endian */
+#if !FF_API_XVMC
+ AV_PIX_FMT_XVMC,///< XVideo Motion Acceleration via common packet passing
+#endif /* !FF_API_XVMC */
+ AV_PIX_FMT_YUV440P10LE, ///< planar YUV 4:4:0,20bpp, (1 Cr & Cb sample per 1x2 Y samples), little-endian
+ AV_PIX_FMT_YUV440P10BE, ///< planar YUV 4:4:0,20bpp, (1 Cr & Cb sample per 1x2 Y samples), big-endian
+ AV_PIX_FMT_YUV440P12LE, ///< planar YUV 4:4:0,24bpp, (1 Cr & Cb sample per 1x2 Y samples), little-endian
+ AV_PIX_FMT_YUV440P12BE, ///< planar YUV 4:4:0,24bpp, (1 Cr & Cb sample per 1x2 Y samples), big-endian
+ AV_PIX_FMT_AYUV64LE, ///< packed AYUV 4:4:4,64bpp (1 Cr & Cb sample per 1x1 Y & A samples), little-endian
+ AV_PIX_FMT_AYUV64BE, ///< packed AYUV 4:4:4,64bpp (1 Cr & Cb sample per 1x1 Y & A samples), big-endian
+
+ AV_PIX_FMT_VIDEOTOOLBOX, ///< hardware decoding through Videotoolbox
+
+ AV_PIX_FMT_NB, ///< number of pixel formats, DO NOT USE THIS if you want to link with shared libav* because the number of formats might differ between versions
+};
+
+#define AV_PIX_FMT_Y400A AV_PIX_FMT_GRAY8A
+#define AV_PIX_FMT_GBR24P AV_PIX_FMT_GBRP
+
+#if AV_HAVE_BIGENDIAN
+# define AV_PIX_FMT_NE(be, le) AV_PIX_FMT_##be
+#else
+# define AV_PIX_FMT_NE(be, le) AV_PIX_FMT_##le
+#endif
+
+#define AV_PIX_FMT_RGB32 AV_PIX_FMT_NE(ARGB, BGRA)
+#define AV_PIX_FMT_RGB32_1 AV_PIX_FMT_NE(RGBA, ABGR)
+#define AV_PIX_FMT_BGR32 AV_PIX_FMT_NE(ABGR, RGBA)
+#define AV_PIX_FMT_BGR32_1 AV_PIX_FMT_NE(BGRA, ARGB)
+#define AV_PIX_FMT_0RGB32 AV_PIX_FMT_NE(0RGB, BGR0)
+#define AV_PIX_FMT_0BGR32 AV_PIX_FMT_NE(0BGR, RGB0)
+
+#define AV_PIX_FMT_GRAY16 AV_PIX_FMT_NE(GRAY16BE, GRAY16LE)
+#define AV_PIX_FMT_YA16 AV_PIX_FMT_NE(YA16BE, YA16LE)
+#define AV_PIX_FMT_RGB48 AV_PIX_FMT_NE(RGB48BE, RGB48LE)
+#define AV_PIX_FMT_RGB565 AV_PIX_FMT_NE(RGB565BE, RGB565LE)
+#define AV_PIX_FMT_RGB555 AV_PIX_FMT_NE(RGB555BE, RGB555LE)
+#define AV_PIX_FMT_RGB444 AV_PIX_FMT_NE(RGB444BE, RGB444LE)
+#define AV_PIX_FMT_RGBA64 AV_PIX_FMT_NE(RGBA64BE, RGBA64LE)
+#define AV_PIX_FMT_BGR48 AV_PIX_FMT_NE(BGR48BE, BGR48LE)
+#define AV_PIX_FMT_BGR565 AV_PIX_FMT_NE(BGR565BE, BGR565LE)
+#define AV_PIX_FMT_BGR555 AV_PIX_FMT_NE(BGR555BE, BGR555LE)
+#define AV_PIX_FMT_BGR444 AV_PIX_FMT_NE(BGR444BE, BGR444LE)
+#define AV_PIX_FMT_BGRA64 AV_PIX_FMT_NE(BGRA64BE, BGRA64LE)
+
+#define AV_PIX_FMT_YUV420P9 AV_PIX_FMT_NE(YUV420P9BE , YUV420P9LE)
+#define AV_PIX_FMT_YUV422P9 AV_PIX_FMT_NE(YUV422P9BE , YUV422P9LE)
+#define AV_PIX_FMT_YUV444P9 AV_PIX_FMT_NE(YUV444P9BE , YUV444P9LE)
+#define AV_PIX_FMT_YUV420P10 AV_PIX_FMT_NE(YUV420P10BE, YUV420P10LE)
+#define AV_PIX_FMT_YUV422P10 AV_PIX_FMT_NE(YUV422P10BE, YUV422P10LE)
+#define AV_PIX_FMT_YUV440P10 AV_PIX_FMT_NE(YUV440P10BE, YUV440P10LE)
+#define AV_PIX_FMT_YUV444P10 AV_PIX_FMT_NE(YUV444P10BE, YUV444P10LE)
+#define AV_PIX_FMT_YUV420P12 AV_PIX_FMT_NE(YUV420P12BE, YUV420P12LE)
+#define AV_PIX_FMT_YUV422P12 AV_PIX_FMT_NE(YUV422P12BE, YUV422P12LE)
+#define AV_PIX_FMT_YUV440P12 AV_PIX_FMT_NE(YUV440P12BE, YUV440P12LE)
+#define AV_PIX_FMT_YUV444P12 AV_PIX_FMT_NE(YUV444P12BE, YUV444P12LE)
+#define AV_PIX_FMT_YUV420P14 AV_PIX_FMT_NE(YUV420P14BE, YUV420P14LE)
+#define AV_PIX_FMT_YUV422P14 AV_PIX_FMT_NE(YUV422P14BE, YUV422P14LE)
+#define AV_PIX_FMT_YUV444P14 AV_PIX_FMT_NE(YUV444P14BE, YUV444P14LE)
+#define AV_PIX_FMT_YUV420P16 AV_PIX_FMT_NE(YUV420P16BE, YUV420P16LE)
+#define AV_PIX_FMT_YUV422P16 AV_PIX_FMT_NE(YUV422P16BE, YUV422P16LE)
+#define AV_PIX_FMT_YUV444P16 AV_PIX_FMT_NE(YUV444P16BE, YUV444P16LE)
+
+#define AV_PIX_FMT_GBRP9 AV_PIX_FMT_NE(GBRP9BE , GBRP9LE)
+#define AV_PIX_FMT_GBRP10 AV_PIX_FMT_NE(GBRP10BE, GBRP10LE)
+#define AV_PIX_FMT_GBRP12 AV_PIX_FMT_NE(GBRP12BE, GBRP12LE)
+#define AV_PIX_FMT_GBRP14 AV_PIX_FMT_NE(GBRP14BE, GBRP14LE)
+#define AV_PIX_FMT_GBRP16 AV_PIX_FMT_NE(GBRP16BE, GBRP16LE)
+#define AV_PIX_FMT_GBRAP16 AV_PIX_FMT_NE(GBRAP16BE, GBRAP16LE)
+
+#define AV_PIX_FMT_BAYER_BGGR16 AV_PIX_FMT_NE(BAYER_BGGR16BE, BAYER_BGGR16LE)
+#define AV_PIX_FMT_BAYER_RGGB16 AV_PIX_FMT_NE(BAYER_RGGB16BE, BAYER_RGGB16LE)
+#define AV_PIX_FMT_BAYER_GBRG16 AV_PIX_FMT_NE(BAYER_GBRG16BE, BAYER_GBRG16LE)
+#define AV_PIX_FMT_BAYER_GRBG16 AV_PIX_FMT_NE(BAYER_GRBG16BE, BAYER_GRBG16LE)
+
+
+#define AV_PIX_FMT_YUVA420P9 AV_PIX_FMT_NE(YUVA420P9BE , YUVA420P9LE)
+#define AV_PIX_FMT_YUVA422P9 AV_PIX_FMT_NE(YUVA422P9BE , YUVA422P9LE)
+#define AV_PIX_FMT_YUVA444P9 AV_PIX_FMT_NE(YUVA444P9BE , YUVA444P9LE)
+#define AV_PIX_FMT_YUVA420P10 AV_PIX_FMT_NE(YUVA420P10BE, YUVA420P10LE)
+#define AV_PIX_FMT_YUVA422P10 AV_PIX_FMT_NE(YUVA422P10BE, YUVA422P10LE)
+#define AV_PIX_FMT_YUVA444P10 AV_PIX_FMT_NE(YUVA444P10BE, YUVA444P10LE)
+#define AV_PIX_FMT_YUVA420P16 AV_PIX_FMT_NE(YUVA420P16BE, YUVA420P16LE)
+#define AV_PIX_FMT_YUVA422P16 AV_PIX_FMT_NE(YUVA422P16BE, YUVA422P16LE)
+#define AV_PIX_FMT_YUVA444P16 AV_PIX_FMT_NE(YUVA444P16BE, YUVA444P16LE)
+
+#define AV_PIX_FMT_XYZ12 AV_PIX_FMT_NE(XYZ12BE, XYZ12LE)
+#define AV_PIX_FMT_NV20 AV_PIX_FMT_NE(NV20BE, NV20LE)
+#define AV_PIX_FMT_AYUV64 AV_PIX_FMT_NE(AYUV64BE, AYUV64LE)
+
+/**
+ * Chromaticity coordinates of the source primaries.
+ */
+enum AVColorPrimaries {
+ AVCOL_PRI_RESERVED0 = 0,
+ AVCOL_PRI_BT709 = 1, ///< also ITU-R BT1361 / IEC 61966-2-4 / SMPTE RP177 Annex B
+ AVCOL_PRI_UNSPECIFIED = 2,
+ AVCOL_PRI_RESERVED = 3,
+ AVCOL_PRI_BT470M = 4, ///< also FCC Title 47 Code of Federal Regulations 73.682 (a)(20)
+
+ AVCOL_PRI_BT470BG = 5, ///< also ITU-R BT601-6 625 / ITU-R BT1358 625 / ITU-R BT1700 625 PAL & SECAM
+ AVCOL_PRI_SMPTE170M = 6, ///< also ITU-R BT601-6 525 / ITU-R BT1358 525 / ITU-R BT1700 NTSC
+ AVCOL_PRI_SMPTE240M = 7, ///< functionally identical to above
+ AVCOL_PRI_FILM = 8, ///< colour filters using Illuminant C
+ AVCOL_PRI_BT2020 = 9, ///< ITU-R BT2020
+ AVCOL_PRI_SMPTEST428_1= 10, ///< SMPTE ST 428-1 (CIE 1931 XYZ)
+ AVCOL_PRI_NB, ///< Not part of ABI
+};
+
+/**
+ * Color Transfer Characteristic.
+ */
+enum AVColorTransferCharacteristic {
+ AVCOL_TRC_RESERVED0 = 0,
+ AVCOL_TRC_BT709 = 1, ///< also ITU-R BT1361
+ AVCOL_TRC_UNSPECIFIED = 2,
+ AVCOL_TRC_RESERVED = 3,
+ AVCOL_TRC_GAMMA22 = 4, ///< also ITU-R BT470M / ITU-R BT1700 625 PAL & SECAM
+ AVCOL_TRC_GAMMA28 = 5, ///< also ITU-R BT470BG
+ AVCOL_TRC_SMPTE170M = 6, ///< also ITU-R BT601-6 525 or 625 / ITU-R BT1358 525 or 625 / ITU-R BT1700 NTSC
+ AVCOL_TRC_SMPTE240M = 7,
+ AVCOL_TRC_LINEAR = 8, ///< "Linear transfer characteristics"
+ AVCOL_TRC_LOG = 9, ///< "Logarithmic transfer characteristic (100:1 range)"
+ AVCOL_TRC_LOG_SQRT = 10, ///< "Logarithmic transfer characteristic (100 * Sqrt(10) : 1 range)"
+ AVCOL_TRC_IEC61966_2_4 = 11, ///< IEC 61966-2-4
+ AVCOL_TRC_BT1361_ECG = 12, ///< ITU-R BT1361 Extended Colour Gamut
+ AVCOL_TRC_IEC61966_2_1 = 13, ///< IEC 61966-2-1 (sRGB or sYCC)
+ AVCOL_TRC_BT2020_10 = 14, ///< ITU-R BT2020 for 10 bit system
+ AVCOL_TRC_BT2020_12 = 15, ///< ITU-R BT2020 for 12 bit system
+ AVCOL_TRC_SMPTEST2084 = 16, ///< SMPTE ST 2084 for 10, 12, 14 and 16 bit systems
+ AVCOL_TRC_SMPTEST428_1 = 17, ///< SMPTE ST 428-1
+ AVCOL_TRC_NB, ///< Not part of ABI
+};
+
+/**
+ * YUV colorspace type.
+ */
+enum AVColorSpace {
+ AVCOL_SPC_RGB = 0, ///< order of coefficients is actually GBR, also IEC 61966-2-1 (sRGB)
+ AVCOL_SPC_BT709 = 1, ///< also ITU-R BT1361 / IEC 61966-2-4 xvYCC709 / SMPTE RP177 Annex B
+ AVCOL_SPC_UNSPECIFIED = 2,
+ AVCOL_SPC_RESERVED = 3,
+ AVCOL_SPC_FCC = 4, ///< FCC Title 47 Code of Federal Regulations 73.682 (a)(20)
+ AVCOL_SPC_BT470BG = 5, ///< also ITU-R BT601-6 625 / ITU-R BT1358 625 / ITU-R BT1700 625 PAL & SECAM / IEC 61966-2-4 xvYCC601
+ AVCOL_SPC_SMPTE170M = 6, ///< also ITU-R BT601-6 525 / ITU-R BT1358 525 / ITU-R BT1700 NTSC / functionally identical to above
+ AVCOL_SPC_SMPTE240M = 7,
+ AVCOL_SPC_YCOCG = 8, ///< Used by Dirac / VC-2 and H.264 FRext, see ITU-T SG16
+ AVCOL_SPC_BT2020_NCL = 9, ///< ITU-R BT2020 non-constant luminance system
+ AVCOL_SPC_BT2020_CL = 10, ///< ITU-R BT2020 constant luminance system
+ AVCOL_SPC_NB, ///< Not part of ABI
+};
+#define AVCOL_SPC_YCGCO AVCOL_SPC_YCOCG
+
+
+/**
+ * MPEG vs JPEG YUV range.
+ */
+enum AVColorRange {
+ AVCOL_RANGE_UNSPECIFIED = 0,
+ AVCOL_RANGE_MPEG = 1, ///< the normal 219*2^(n-8) "MPEG" YUV ranges
+ AVCOL_RANGE_JPEG = 2, ///< the normal 2^n-1 "JPEG" YUV ranges
+ AVCOL_RANGE_NB, ///< Not part of ABI
+};
+
+/**
+ * Location of chroma samples.
+ *
+ * Illustration showing the location of the first (top left) chroma sample of the
+ * image, the left shows only luma, the right
+ * shows the location of the chroma sample, the 2 could be imagined to overlay
+ * each other but are drawn separately due to limitations of ASCII
+ *
+ * 1st 2nd 1st 2nd horizontal luma sample positions
+ * v v v v
+ * ______ ______
+ *1st luma line > |X X ... |3 4 X ... X are luma samples,
+ * | |1 2 1-6 are possible chroma positions
+ *2nd luma line > |X X ... |5 6 X ... 0 is undefined/unknown position
+ */
+enum AVChromaLocation {
+ AVCHROMA_LOC_UNSPECIFIED = 0,
+ AVCHROMA_LOC_LEFT = 1, ///< mpeg2/4 4:2:0, h264 default for 4:2:0
+ AVCHROMA_LOC_CENTER = 2, ///< mpeg1 4:2:0, jpeg 4:2:0, h263 4:2:0
+ AVCHROMA_LOC_TOPLEFT = 3, ///< ITU-R 601, SMPTE 274M 296M S314M(DV 4:1:1), mpeg2 4:2:2
+ AVCHROMA_LOC_TOP = 4,
+ AVCHROMA_LOC_BOTTOMLEFT = 5,
+ AVCHROMA_LOC_BOTTOM = 6,
+ AVCHROMA_LOC_NB, ///< Not part of ABI
+};
+
+#endif /* AVUTIL_PIXFMT_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/rational.h b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/rational.h
new file mode 100644
index 0000000000..2897469680
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/rational.h
@@ -0,0 +1,173 @@
+/*
+ * rational numbers
+ * Copyright (c) 2003 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * rational numbers
+ * @author Michael Niedermayer <michaelni@gmx.at>
+ */
+
+#ifndef AVUTIL_RATIONAL_H
+#define AVUTIL_RATIONAL_H
+
+#include <stdint.h>
+#include <limits.h>
+#include "attributes.h"
+
+/**
+ * @addtogroup lavu_math
+ * @{
+ */
+
+/**
+ * rational number numerator/denominator
+ */
+typedef struct AVRational{
+ int num; ///< numerator
+ int den; ///< denominator
+} AVRational;
+
+/**
+ * Create a rational.
+ * Useful for compilers that do not support compound literals.
+ * @note The return value is not reduced.
+ */
+static inline AVRational av_make_q(int num, int den)
+{
+ AVRational r = { num, den };
+ return r;
+}
+
+/**
+ * Compare two rationals.
+ * @param a first rational
+ * @param b second rational
+ * @return 0 if a==b, 1 if a>b, -1 if a<b, and INT_MIN if one of the
+ * values is of the form 0/0
+ */
+static inline int av_cmp_q(AVRational a, AVRational b){
+ const int64_t tmp= a.num * (int64_t)b.den - b.num * (int64_t)a.den;
+
+ if(tmp) return (int)((tmp ^ a.den ^ b.den)>>63)|1;
+ else if(b.den && a.den) return 0;
+ else if(a.num && b.num) return (a.num>>31) - (b.num>>31);
+ else return INT_MIN;
+}
+
+/**
+ * Convert rational to double.
+ * @param a rational to convert
+ * @return (double) a
+ */
+static inline double av_q2d(AVRational a){
+ return a.num / (double) a.den;
+}
+
+/**
+ * Reduce a fraction.
+ * This is useful for framerate calculations.
+ * @param dst_num destination numerator
+ * @param dst_den destination denominator
+ * @param num source numerator
+ * @param den source denominator
+ * @param max the maximum allowed for dst_num & dst_den
+ * @return 1 if exact, 0 otherwise
+ */
+int av_reduce(int *dst_num, int *dst_den, int64_t num, int64_t den, int64_t max);
+
+/**
+ * Multiply two rationals.
+ * @param b first rational
+ * @param c second rational
+ * @return b*c
+ */
+AVRational av_mul_q(AVRational b, AVRational c) av_const;
+
+/**
+ * Divide one rational by another.
+ * @param b first rational
+ * @param c second rational
+ * @return b/c
+ */
+AVRational av_div_q(AVRational b, AVRational c) av_const;
+
+/**
+ * Add two rationals.
+ * @param b first rational
+ * @param c second rational
+ * @return b+c
+ */
+AVRational av_add_q(AVRational b, AVRational c) av_const;
+
+/**
+ * Subtract one rational from another.
+ * @param b first rational
+ * @param c second rational
+ * @return b-c
+ */
+AVRational av_sub_q(AVRational b, AVRational c) av_const;
+
+/**
+ * Invert a rational.
+ * @param q value
+ * @return 1 / q
+ */
+static av_always_inline AVRational av_inv_q(AVRational q)
+{
+ AVRational r = { q.den, q.num };
+ return r;
+}
+
+/**
+ * Convert a double precision floating point number to a rational.
+ * inf is expressed as {1,0} or {-1,0} depending on the sign.
+ *
+ * @param d double to convert
+ * @param max the maximum allowed numerator and denominator
+ * @return (AVRational) d
+ */
+AVRational av_d2q(double d, int max) av_const;
+
+/**
+ * @return 1 if q1 is nearer to q than q2, -1 if q2 is nearer
+ * than q1, 0 if they have the same distance.
+ */
+int av_nearer_q(AVRational q, AVRational q1, AVRational q2);
+
+/**
+ * Find the nearest value in q_list to q.
+ * @param q_list an array of rationals terminated by {0, 0}
+ * @return the index of the nearest value found in the array
+ */
+int av_find_nearest_q_idx(AVRational q, const AVRational* q_list);
+
+/**
+ * Converts a AVRational to a IEEE 32bit float.
+ *
+ * The float is returned in a uint32_t and its value is platform indepenant.
+ */
+uint32_t av_q2intfloat(AVRational q);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_RATIONAL_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/samplefmt.h b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/samplefmt.h
new file mode 100644
index 0000000000..6a8a031c02
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/samplefmt.h
@@ -0,0 +1,271 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_SAMPLEFMT_H
+#define AVUTIL_SAMPLEFMT_H
+
+#include <stdint.h>
+
+#include "avutil.h"
+#include "attributes.h"
+
+/**
+ * @addtogroup lavu_audio
+ * @{
+ *
+ * @defgroup lavu_sampfmts Audio sample formats
+ *
+ * Audio sample format enumeration and related convenience functions.
+ * @{
+ *
+ */
+
+/**
+ * Audio sample formats
+ *
+ * - The data described by the sample format is always in native-endian order.
+ * Sample values can be expressed by native C types, hence the lack of a signed
+ * 24-bit sample format even though it is a common raw audio data format.
+ *
+ * - The floating-point formats are based on full volume being in the range
+ * [-1.0, 1.0]. Any values outside this range are beyond full volume level.
+ *
+ * - The data layout as used in av_samples_fill_arrays() and elsewhere in FFmpeg
+ * (such as AVFrame in libavcodec) is as follows:
+ *
+ * @par
+ * For planar sample formats, each audio channel is in a separate data plane,
+ * and linesize is the buffer size, in bytes, for a single plane. All data
+ * planes must be the same size. For packed sample formats, only the first data
+ * plane is used, and samples for each channel are interleaved. In this case,
+ * linesize is the buffer size, in bytes, for the 1 plane.
+ *
+ */
+enum AVSampleFormat {
+ AV_SAMPLE_FMT_NONE = -1,
+ AV_SAMPLE_FMT_U8, ///< unsigned 8 bits
+ AV_SAMPLE_FMT_S16, ///< signed 16 bits
+ AV_SAMPLE_FMT_S32, ///< signed 32 bits
+ AV_SAMPLE_FMT_FLT, ///< float
+ AV_SAMPLE_FMT_DBL, ///< double
+
+ AV_SAMPLE_FMT_U8P, ///< unsigned 8 bits, planar
+ AV_SAMPLE_FMT_S16P, ///< signed 16 bits, planar
+ AV_SAMPLE_FMT_S32P, ///< signed 32 bits, planar
+ AV_SAMPLE_FMT_FLTP, ///< float, planar
+ AV_SAMPLE_FMT_DBLP, ///< double, planar
+
+ AV_SAMPLE_FMT_NB ///< Number of sample formats. DO NOT USE if linking dynamically
+};
+
+/**
+ * Return the name of sample_fmt, or NULL if sample_fmt is not
+ * recognized.
+ */
+const char *av_get_sample_fmt_name(enum AVSampleFormat sample_fmt);
+
+/**
+ * Return a sample format corresponding to name, or AV_SAMPLE_FMT_NONE
+ * on error.
+ */
+enum AVSampleFormat av_get_sample_fmt(const char *name);
+
+/**
+ * Return the planar<->packed alternative form of the given sample format, or
+ * AV_SAMPLE_FMT_NONE on error. If the passed sample_fmt is already in the
+ * requested planar/packed format, the format returned is the same as the
+ * input.
+ */
+enum AVSampleFormat av_get_alt_sample_fmt(enum AVSampleFormat sample_fmt, int planar);
+
+/**
+ * Get the packed alternative form of the given sample format.
+ *
+ * If the passed sample_fmt is already in packed format, the format returned is
+ * the same as the input.
+ *
+ * @return the packed alternative form of the given sample format or
+ AV_SAMPLE_FMT_NONE on error.
+ */
+enum AVSampleFormat av_get_packed_sample_fmt(enum AVSampleFormat sample_fmt);
+
+/**
+ * Get the planar alternative form of the given sample format.
+ *
+ * If the passed sample_fmt is already in planar format, the format returned is
+ * the same as the input.
+ *
+ * @return the planar alternative form of the given sample format or
+ AV_SAMPLE_FMT_NONE on error.
+ */
+enum AVSampleFormat av_get_planar_sample_fmt(enum AVSampleFormat sample_fmt);
+
+/**
+ * Generate a string corresponding to the sample format with
+ * sample_fmt, or a header if sample_fmt is negative.
+ *
+ * @param buf the buffer where to write the string
+ * @param buf_size the size of buf
+ * @param sample_fmt the number of the sample format to print the
+ * corresponding info string, or a negative value to print the
+ * corresponding header.
+ * @return the pointer to the filled buffer or NULL if sample_fmt is
+ * unknown or in case of other errors
+ */
+char *av_get_sample_fmt_string(char *buf, int buf_size, enum AVSampleFormat sample_fmt);
+
+/**
+ * Return number of bytes per sample.
+ *
+ * @param sample_fmt the sample format
+ * @return number of bytes per sample or zero if unknown for the given
+ * sample format
+ */
+int av_get_bytes_per_sample(enum AVSampleFormat sample_fmt);
+
+/**
+ * Check if the sample format is planar.
+ *
+ * @param sample_fmt the sample format to inspect
+ * @return 1 if the sample format is planar, 0 if it is interleaved
+ */
+int av_sample_fmt_is_planar(enum AVSampleFormat sample_fmt);
+
+/**
+ * Get the required buffer size for the given audio parameters.
+ *
+ * @param[out] linesize calculated linesize, may be NULL
+ * @param nb_channels the number of channels
+ * @param nb_samples the number of samples in a single channel
+ * @param sample_fmt the sample format
+ * @param align buffer size alignment (0 = default, 1 = no alignment)
+ * @return required buffer size, or negative error code on failure
+ */
+int av_samples_get_buffer_size(int *linesize, int nb_channels, int nb_samples,
+ enum AVSampleFormat sample_fmt, int align);
+
+/**
+ * @}
+ *
+ * @defgroup lavu_sampmanip Samples manipulation
+ *
+ * Functions that manipulate audio samples
+ * @{
+ */
+
+/**
+ * Fill plane data pointers and linesize for samples with sample
+ * format sample_fmt.
+ *
+ * The audio_data array is filled with the pointers to the samples data planes:
+ * for planar, set the start point of each channel's data within the buffer,
+ * for packed, set the start point of the entire buffer only.
+ *
+ * The value pointed to by linesize is set to the aligned size of each
+ * channel's data buffer for planar layout, or to the aligned size of the
+ * buffer for all channels for packed layout.
+ *
+ * The buffer in buf must be big enough to contain all the samples
+ * (use av_samples_get_buffer_size() to compute its minimum size),
+ * otherwise the audio_data pointers will point to invalid data.
+ *
+ * @see enum AVSampleFormat
+ * The documentation for AVSampleFormat describes the data layout.
+ *
+ * @param[out] audio_data array to be filled with the pointer for each channel
+ * @param[out] linesize calculated linesize, may be NULL
+ * @param buf the pointer to a buffer containing the samples
+ * @param nb_channels the number of channels
+ * @param nb_samples the number of samples in a single channel
+ * @param sample_fmt the sample format
+ * @param align buffer size alignment (0 = default, 1 = no alignment)
+ * @return >=0 on success or a negative error code on failure
+ * @todo return minimum size in bytes required for the buffer in case
+ * of success at the next bump
+ */
+int av_samples_fill_arrays(uint8_t **audio_data, int *linesize,
+ const uint8_t *buf,
+ int nb_channels, int nb_samples,
+ enum AVSampleFormat sample_fmt, int align);
+
+/**
+ * Allocate a samples buffer for nb_samples samples, and fill data pointers and
+ * linesize accordingly.
+ * The allocated samples buffer can be freed by using av_freep(&audio_data[0])
+ * Allocated data will be initialized to silence.
+ *
+ * @see enum AVSampleFormat
+ * The documentation for AVSampleFormat describes the data layout.
+ *
+ * @param[out] audio_data array to be filled with the pointer for each channel
+ * @param[out] linesize aligned size for audio buffer(s), may be NULL
+ * @param nb_channels number of audio channels
+ * @param nb_samples number of samples per channel
+ * @param align buffer size alignment (0 = default, 1 = no alignment)
+ * @return >=0 on success or a negative error code on failure
+ * @todo return the size of the allocated buffer in case of success at the next bump
+ * @see av_samples_fill_arrays()
+ * @see av_samples_alloc_array_and_samples()
+ */
+int av_samples_alloc(uint8_t **audio_data, int *linesize, int nb_channels,
+ int nb_samples, enum AVSampleFormat sample_fmt, int align);
+
+/**
+ * Allocate a data pointers array, samples buffer for nb_samples
+ * samples, and fill data pointers and linesize accordingly.
+ *
+ * This is the same as av_samples_alloc(), but also allocates the data
+ * pointers array.
+ *
+ * @see av_samples_alloc()
+ */
+int av_samples_alloc_array_and_samples(uint8_t ***audio_data, int *linesize, int nb_channels,
+ int nb_samples, enum AVSampleFormat sample_fmt, int align);
+
+/**
+ * Copy samples from src to dst.
+ *
+ * @param dst destination array of pointers to data planes
+ * @param src source array of pointers to data planes
+ * @param dst_offset offset in samples at which the data will be written to dst
+ * @param src_offset offset in samples at which the data will be read from src
+ * @param nb_samples number of samples to be copied
+ * @param nb_channels number of audio channels
+ * @param sample_fmt audio sample format
+ */
+int av_samples_copy(uint8_t **dst, uint8_t * const *src, int dst_offset,
+ int src_offset, int nb_samples, int nb_channels,
+ enum AVSampleFormat sample_fmt);
+
+/**
+ * Fill an audio buffer with silence.
+ *
+ * @param audio_data array of pointers to data planes
+ * @param offset offset in samples at which to start filling
+ * @param nb_samples number of samples to fill
+ * @param nb_channels number of audio channels
+ * @param sample_fmt audio sample format
+ */
+int av_samples_set_silence(uint8_t **audio_data, int offset, int nb_samples,
+ int nb_channels, enum AVSampleFormat sample_fmt);
+
+/**
+ * @}
+ * @}
+ */
+#endif /* AVUTIL_SAMPLEFMT_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/version.h b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/version.h
new file mode 100644
index 0000000000..9ffa7a8666
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg57/include/libavutil/version.h
@@ -0,0 +1,129 @@
+/*
+ * copyright (c) 2003 Fabrice Bellard
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_VERSION_H
+#define AVUTIL_VERSION_H
+
+#include "macros.h"
+
+/**
+ * @addtogroup version_utils
+ *
+ * Useful to check and match library version in order to maintain
+ * backward compatibility.
+ *
+ * @{
+ */
+
+#define AV_VERSION_INT(a, b, c) ((a)<<16 | (b)<<8 | (c))
+#define AV_VERSION_DOT(a, b, c) a ##.## b ##.## c
+#define AV_VERSION(a, b, c) AV_VERSION_DOT(a, b, c)
+
+/**
+ * Extract version components from the full ::AV_VERSION_INT int as returned
+ * by functions like ::avformat_version() and ::avcodec_version()
+ */
+#define AV_VERSION_MAJOR(a) ((a) >> 16)
+#define AV_VERSION_MINOR(a) (((a) & 0x00FF00) >> 8)
+#define AV_VERSION_MICRO(a) ((a) & 0xFF)
+
+/**
+ * @}
+ */
+
+/**
+ * @file
+ * @ingroup lavu
+ * Libavutil version macros
+ */
+
+/**
+ * @defgroup lavu_ver Version and Build diagnostics
+ *
+ * Macros and function useful to check at compiletime and at runtime
+ * which version of libavutil is in use.
+ *
+ * @{
+ */
+
+#define LIBAVUTIL_VERSION_MAJOR 55
+#define LIBAVUTIL_VERSION_MINOR 12
+#define LIBAVUTIL_VERSION_MICRO 100
+
+#define LIBAVUTIL_VERSION_INT AV_VERSION_INT(LIBAVUTIL_VERSION_MAJOR, \
+ LIBAVUTIL_VERSION_MINOR, \
+ LIBAVUTIL_VERSION_MICRO)
+#define LIBAVUTIL_VERSION AV_VERSION(LIBAVUTIL_VERSION_MAJOR, \
+ LIBAVUTIL_VERSION_MINOR, \
+ LIBAVUTIL_VERSION_MICRO)
+#define LIBAVUTIL_BUILD LIBAVUTIL_VERSION_INT
+
+#define LIBAVUTIL_IDENT "Lavu" AV_STRINGIFY(LIBAVUTIL_VERSION)
+
+/**
+ * @}
+ *
+ * @defgroup depr_guards Deprecation guards
+ * FF_API_* defines may be placed below to indicate public API that will be
+ * dropped at a future version bump. The defines themselves are not part of
+ * the public API and may change, break or disappear at any time.
+ *
+ * @note, when bumping the major version it is recommended to manually
+ * disable each FF_API_* in its own commit instead of disabling them all
+ * at once through the bump. This improves the git bisect-ability of the change.
+ *
+ * @{
+ */
+
+#ifndef FF_API_VDPAU
+#define FF_API_VDPAU (LIBAVUTIL_VERSION_MAJOR < 56)
+#endif
+#ifndef FF_API_XVMC
+#define FF_API_XVMC (LIBAVUTIL_VERSION_MAJOR < 56)
+#endif
+#ifndef FF_API_OPT_TYPE_METADATA
+#define FF_API_OPT_TYPE_METADATA (LIBAVUTIL_VERSION_MAJOR < 56)
+#endif
+#ifndef FF_API_DLOG
+#define FF_API_DLOG (LIBAVUTIL_VERSION_MAJOR < 56)
+#endif
+#ifndef FF_API_VAAPI
+#define FF_API_VAAPI (LIBAVUTIL_VERSION_MAJOR < 56)
+#endif
+#ifndef FF_API_FRAME_QP
+#define FF_API_FRAME_QP (LIBAVUTIL_VERSION_MAJOR < 56)
+#endif
+#ifndef FF_API_PLUS1_MINUS1
+#define FF_API_PLUS1_MINUS1 (LIBAVUTIL_VERSION_MAJOR < 56)
+#endif
+#ifndef FF_API_ERROR_FRAME
+#define FF_API_ERROR_FRAME (LIBAVUTIL_VERSION_MAJOR < 56)
+#endif
+#ifndef FF_API_CRC_BIG_TABLE
+#define FF_API_CRC_BIG_TABLE (LIBAVUTIL_VERSION_MAJOR < 56)
+#endif
+
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_VERSION_H */
+
diff --git a/dom/media/platforms/ffmpeg/ffmpeg57/moz.build b/dom/media/platforms/ffmpeg/ffmpeg57/moz.build
new file mode 100644
index 0000000000..3a94853844
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg57/moz.build
@@ -0,0 +1,31 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES += [
+ '../FFmpegAudioDecoder.cpp',
+ '../FFmpegDataDecoder.cpp',
+ '../FFmpegDecoderModule.cpp',
+ '../FFmpegVideoDecoder.cpp',
+]
+LOCAL_INCLUDES += [
+ '..',
+ 'include',
+]
+
+if CONFIG['CC_TYPE'] in ('clang', 'gcc'):
+ CXXFLAGS += [ '-Wno-deprecated-declarations' ]
+if CONFIG['CC_TYPE'] == 'clang':
+ CXXFLAGS += [
+ '-Wno-unknown-attributes',
+ ]
+if CONFIG['CC_TYPE'] == 'gcc':
+ CXXFLAGS += [
+ '-Wno-attributes',
+ ]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = 'xul'
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/COPYING.LGPLv2.1 b/dom/media/platforms/ffmpeg/ffmpeg58/include/COPYING.LGPLv2.1
new file mode 100644
index 0000000000..00b4fedfe7
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/COPYING.LGPLv2.1
@@ -0,0 +1,504 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the library's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+ <signature of Ty Coon>, 1 April 1990
+ Ty Coon, President of Vice
+
+That's all there is to it!
+
+
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/avcodec.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/avcodec.h
new file mode 100644
index 0000000000..8a71c04230
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/avcodec.h
@@ -0,0 +1,4184 @@
+/*
+ * copyright (c) 2001 Fabrice Bellard
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_AVCODEC_H
+#define AVCODEC_AVCODEC_H
+
+/**
+ * @file
+ * @ingroup libavc
+ * Libavcodec external API header
+ */
+
+#include <errno.h>
+#include "libavutil/samplefmt.h"
+#include "libavutil/attributes.h"
+#include "libavutil/avutil.h"
+#include "libavutil/buffer.h"
+#include "libavutil/cpu.h"
+#include "libavutil/channel_layout.h"
+#include "libavutil/dict.h"
+#include "libavutil/frame.h"
+#include "libavutil/hwcontext.h"
+#include "libavutil/log.h"
+#include "libavutil/pixfmt.h"
+#include "libavutil/rational.h"
+
+#include "bsf.h"
+#include "codec.h"
+#include "codec_desc.h"
+#include "codec_par.h"
+#include "codec_id.h"
+#include "packet.h"
+#include "version.h"
+
+/**
+ * @defgroup libavc libavcodec
+ * Encoding/Decoding Library
+ *
+ * @{
+ *
+ * @defgroup lavc_decoding Decoding
+ * @{
+ * @}
+ *
+ * @defgroup lavc_encoding Encoding
+ * @{
+ * @}
+ *
+ * @defgroup lavc_codec Codecs
+ * @{
+ * @defgroup lavc_codec_native Native Codecs
+ * @{
+ * @}
+ * @defgroup lavc_codec_wrappers External library wrappers
+ * @{
+ * @}
+ * @defgroup lavc_codec_hwaccel Hardware Accelerators bridge
+ * @{
+ * @}
+ * @}
+ * @defgroup lavc_internal Internal
+ * @{
+ * @}
+ * @}
+ */
+
+/**
+ * @ingroup libavc
+ * @defgroup lavc_encdec send/receive encoding and decoding API overview
+ * @{
+ *
+ * The avcodec_send_packet()/avcodec_receive_frame()/avcodec_send_frame()/
+ * avcodec_receive_packet() functions provide an encode/decode API, which
+ * decouples input and output.
+ *
+ * The API is very similar for encoding/decoding and audio/video, and works as
+ * follows:
+ * - Set up and open the AVCodecContext as usual.
+ * - Send valid input:
+ * - For decoding, call avcodec_send_packet() to give the decoder raw
+ * compressed data in an AVPacket.
+ * - For encoding, call avcodec_send_frame() to give the encoder an AVFrame
+ * containing uncompressed audio or video.
+ *
+ * In both cases, it is recommended that AVPackets and AVFrames are
+ * refcounted, or libavcodec might have to copy the input data. (libavformat
+ * always returns refcounted AVPackets, and av_frame_get_buffer() allocates
+ * refcounted AVFrames.)
+ * - Receive output in a loop. Periodically call one of the avcodec_receive_*()
+ * functions and process their output:
+ * - For decoding, call avcodec_receive_frame(). On success, it will return
+ * an AVFrame containing uncompressed audio or video data.
+ * - For encoding, call avcodec_receive_packet(). On success, it will return
+ * an AVPacket with a compressed frame.
+ *
+ * Repeat this call until it returns AVERROR(EAGAIN) or an error. The
+ * AVERROR(EAGAIN) return value means that new input data is required to
+ * return new output. In this case, continue with sending input. For each
+ * input frame/packet, the codec will typically return 1 output frame/packet,
+ * but it can also be 0 or more than 1.
+ *
+ * At the beginning of decoding or encoding, the codec might accept multiple
+ * input frames/packets without returning a frame, until its internal buffers
+ * are filled. This situation is handled transparently if you follow the steps
+ * outlined above.
+ *
+ * In theory, sending input can result in EAGAIN - this should happen only if
+ * not all output was received. You can use this to structure alternative decode
+ * or encode loops other than the one suggested above. For example, you could
+ * try sending new input on each iteration, and try to receive output if that
+ * returns EAGAIN.
+ *
+ * End of stream situations. These require "flushing" (aka draining) the codec,
+ * as the codec might buffer multiple frames or packets internally for
+ * performance or out of necessity (consider B-frames).
+ * This is handled as follows:
+ * - Instead of valid input, send NULL to the avcodec_send_packet() (decoding)
+ * or avcodec_send_frame() (encoding) functions. This will enter draining
+ * mode.
+ * - Call avcodec_receive_frame() (decoding) or avcodec_receive_packet()
+ * (encoding) in a loop until AVERROR_EOF is returned. The functions will
+ * not return AVERROR(EAGAIN), unless you forgot to enter draining mode.
+ * - Before decoding can be resumed again, the codec has to be reset with
+ * avcodec_flush_buffers().
+ *
+ * Using the API as outlined above is highly recommended. But it is also
+ * possible to call functions outside of this rigid schema. For example, you can
+ * call avcodec_send_packet() repeatedly without calling
+ * avcodec_receive_frame(). In this case, avcodec_send_packet() will succeed
+ * until the codec's internal buffer has been filled up (which is typically of
+ * size 1 per output frame, after initial input), and then reject input with
+ * AVERROR(EAGAIN). Once it starts rejecting input, you have no choice but to
+ * read at least some output.
+ *
+ * Not all codecs will follow a rigid and predictable dataflow; the only
+ * guarantee is that an AVERROR(EAGAIN) return value on a send/receive call on
+ * one end implies that a receive/send call on the other end will succeed, or
+ * at least will not fail with AVERROR(EAGAIN). In general, no codec will
+ * permit unlimited buffering of input or output.
+ *
+ * This API replaces the following legacy functions:
+ * - avcodec_decode_video2() and avcodec_decode_audio4():
+ * Use avcodec_send_packet() to feed input to the decoder, then use
+ * avcodec_receive_frame() to receive decoded frames after each packet.
+ * Unlike with the old video decoding API, multiple frames might result from
+ * a packet. For audio, splitting the input packet into frames by partially
+ * decoding packets becomes transparent to the API user. You never need to
+ * feed an AVPacket to the API twice (unless it is rejected with AVERROR(EAGAIN) - then
+ * no data was read from the packet).
+ * Additionally, sending a flush/draining packet is required only once.
+ * - avcodec_encode_video2()/avcodec_encode_audio2():
+ * Use avcodec_send_frame() to feed input to the encoder, then use
+ * avcodec_receive_packet() to receive encoded packets.
+ * Providing user-allocated buffers for avcodec_receive_packet() is not
+ * possible.
+ * - The new API does not handle subtitles yet.
+ *
+ * Mixing new and old function calls on the same AVCodecContext is not allowed,
+ * and will result in undefined behavior.
+ *
+ * Some codecs might require using the new API; using the old API will return
+ * an error when calling it. All codecs support the new API.
+ *
+ * A codec is not allowed to return AVERROR(EAGAIN) for both sending and receiving. This
+ * would be an invalid state, which could put the codec user into an endless
+ * loop. The API has no concept of time either: it cannot happen that trying to
+ * do avcodec_send_packet() results in AVERROR(EAGAIN), but a repeated call 1 second
+ * later accepts the packet (with no other receive/flush API calls involved).
+ * The API is a strict state machine, and the passage of time is not supposed
+ * to influence it. Some timing-dependent behavior might still be deemed
+ * acceptable in certain cases. But it must never result in both send/receive
+ * returning EAGAIN at the same time at any point. It must also absolutely be
+ * avoided that the current state is "unstable" and can "flip-flop" between
+ * the send/receive APIs allowing progress. For example, it's not allowed that
+ * the codec randomly decides that it actually wants to consume a packet now
+ * instead of returning a frame, after it just returned AVERROR(EAGAIN) on an
+ * avcodec_send_packet() call.
+ * @}
+ */
+
+/**
+ * @defgroup lavc_core Core functions/structures.
+ * @ingroup libavc
+ *
+ * Basic definitions, functions for querying libavcodec capabilities,
+ * allocating core structures, etc.
+ * @{
+ */
+
+/**
+ * @ingroup lavc_decoding
+ * Required number of additionally allocated bytes at the end of the input bitstream for decoding.
+ * This is mainly needed because some optimized bitstream readers read
+ * 32 or 64 bit at once and could read over the end.<br>
+ * Note: If the first 23 bits of the additional bytes are not 0, then damaged
+ * MPEG bitstreams could cause overread and segfault.
+ */
+#define AV_INPUT_BUFFER_PADDING_SIZE 64
+
+/**
+ * @ingroup lavc_encoding
+ * minimum encoding buffer size
+ * Used to avoid some checks during header writing.
+ */
+#define AV_INPUT_BUFFER_MIN_SIZE 16384
+
+/**
+ * @ingroup lavc_decoding
+ */
+enum AVDiscard{
+ /* We leave some space between them for extensions (drop some
+ * keyframes for intra-only or drop just some bidir frames). */
+ AVDISCARD_NONE =-16, ///< discard nothing
+ AVDISCARD_DEFAULT = 0, ///< discard useless packets like 0 size packets in avi
+ AVDISCARD_NONREF = 8, ///< discard all non reference
+ AVDISCARD_BIDIR = 16, ///< discard all bidirectional frames
+ AVDISCARD_NONINTRA= 24, ///< discard all non intra frames
+ AVDISCARD_NONKEY = 32, ///< discard all frames except keyframes
+ AVDISCARD_ALL = 48, ///< discard all
+};
+
+enum AVAudioServiceType {
+ AV_AUDIO_SERVICE_TYPE_MAIN = 0,
+ AV_AUDIO_SERVICE_TYPE_EFFECTS = 1,
+ AV_AUDIO_SERVICE_TYPE_VISUALLY_IMPAIRED = 2,
+ AV_AUDIO_SERVICE_TYPE_HEARING_IMPAIRED = 3,
+ AV_AUDIO_SERVICE_TYPE_DIALOGUE = 4,
+ AV_AUDIO_SERVICE_TYPE_COMMENTARY = 5,
+ AV_AUDIO_SERVICE_TYPE_EMERGENCY = 6,
+ AV_AUDIO_SERVICE_TYPE_VOICE_OVER = 7,
+ AV_AUDIO_SERVICE_TYPE_KARAOKE = 8,
+ AV_AUDIO_SERVICE_TYPE_NB , ///< Not part of ABI
+};
+
+/**
+ * @ingroup lavc_encoding
+ */
+typedef struct RcOverride{
+ int start_frame;
+ int end_frame;
+ int qscale; // If this is 0 then quality_factor will be used instead.
+ float quality_factor;
+} RcOverride;
+
+/* encoding support
+ These flags can be passed in AVCodecContext.flags before initialization.
+ Note: Not everything is supported yet.
+*/
+
+/**
+ * Allow decoders to produce frames with data planes that are not aligned
+ * to CPU requirements (e.g. due to cropping).
+ */
+#define AV_CODEC_FLAG_UNALIGNED (1 << 0)
+/**
+ * Use fixed qscale.
+ */
+#define AV_CODEC_FLAG_QSCALE (1 << 1)
+/**
+ * 4 MV per MB allowed / advanced prediction for H.263.
+ */
+#define AV_CODEC_FLAG_4MV (1 << 2)
+/**
+ * Output even those frames that might be corrupted.
+ */
+#define AV_CODEC_FLAG_OUTPUT_CORRUPT (1 << 3)
+/**
+ * Use qpel MC.
+ */
+#define AV_CODEC_FLAG_QPEL (1 << 4)
+/**
+ * Don't output frames whose parameters differ from first
+ * decoded frame in stream.
+ */
+#define AV_CODEC_FLAG_DROPCHANGED (1 << 5)
+/**
+ * Use internal 2pass ratecontrol in first pass mode.
+ */
+#define AV_CODEC_FLAG_PASS1 (1 << 9)
+/**
+ * Use internal 2pass ratecontrol in second pass mode.
+ */
+#define AV_CODEC_FLAG_PASS2 (1 << 10)
+/**
+ * loop filter.
+ */
+#define AV_CODEC_FLAG_LOOP_FILTER (1 << 11)
+/**
+ * Only decode/encode grayscale.
+ */
+#define AV_CODEC_FLAG_GRAY (1 << 13)
+/**
+ * error[?] variables will be set during encoding.
+ */
+#define AV_CODEC_FLAG_PSNR (1 << 15)
+/**
+ * Input bitstream might be truncated at a random location
+ * instead of only at frame boundaries.
+ */
+#define AV_CODEC_FLAG_TRUNCATED (1 << 16)
+/**
+ * Use interlaced DCT.
+ */
+#define AV_CODEC_FLAG_INTERLACED_DCT (1 << 18)
+/**
+ * Force low delay.
+ */
+#define AV_CODEC_FLAG_LOW_DELAY (1 << 19)
+/**
+ * Place global headers in extradata instead of every keyframe.
+ */
+#define AV_CODEC_FLAG_GLOBAL_HEADER (1 << 22)
+/**
+ * Use only bitexact stuff (except (I)DCT).
+ */
+#define AV_CODEC_FLAG_BITEXACT (1 << 23)
+/* Fx : Flag for H.263+ extra options */
+/**
+ * H.263 advanced intra coding / MPEG-4 AC prediction
+ */
+#define AV_CODEC_FLAG_AC_PRED (1 << 24)
+/**
+ * interlaced motion estimation
+ */
+#define AV_CODEC_FLAG_INTERLACED_ME (1 << 29)
+#define AV_CODEC_FLAG_CLOSED_GOP (1U << 31)
+
+/**
+ * Allow non spec compliant speedup tricks.
+ */
+#define AV_CODEC_FLAG2_FAST (1 << 0)
+/**
+ * Skip bitstream encoding.
+ */
+#define AV_CODEC_FLAG2_NO_OUTPUT (1 << 2)
+/**
+ * Place global headers at every keyframe instead of in extradata.
+ */
+#define AV_CODEC_FLAG2_LOCAL_HEADER (1 << 3)
+
+/**
+ * timecode is in drop frame format. DEPRECATED!!!!
+ */
+#define AV_CODEC_FLAG2_DROP_FRAME_TIMECODE (1 << 13)
+
+/**
+ * Input bitstream might be truncated at a packet boundaries
+ * instead of only at frame boundaries.
+ */
+#define AV_CODEC_FLAG2_CHUNKS (1 << 15)
+/**
+ * Discard cropping information from SPS.
+ */
+#define AV_CODEC_FLAG2_IGNORE_CROP (1 << 16)
+
+/**
+ * Show all frames before the first keyframe
+ */
+#define AV_CODEC_FLAG2_SHOW_ALL (1 << 22)
+/**
+ * Export motion vectors through frame side data
+ */
+#define AV_CODEC_FLAG2_EXPORT_MVS (1 << 28)
+/**
+ * Do not skip samples and export skip information as frame side data
+ */
+#define AV_CODEC_FLAG2_SKIP_MANUAL (1 << 29)
+/**
+ * Do not reset ASS ReadOrder field on flush (subtitles decoding)
+ */
+#define AV_CODEC_FLAG2_RO_FLUSH_NOOP (1 << 30)
+
+/* Unsupported options :
+ * Syntax Arithmetic coding (SAC)
+ * Reference Picture Selection
+ * Independent Segment Decoding */
+/* /Fx */
+/* codec capabilities */
+
+/* Exported side data.
+ These flags can be passed in AVCodecContext.export_side_data before initialization.
+*/
+/**
+ * Export motion vectors through frame side data
+ */
+#define AV_CODEC_EXPORT_DATA_MVS (1 << 0)
+/**
+ * Export encoder Producer Reference Time through packet side data
+ */
+#define AV_CODEC_EXPORT_DATA_PRFT (1 << 1)
+/**
+ * Decoding only.
+ * Export the AVVideoEncParams structure through frame side data.
+ */
+#define AV_CODEC_EXPORT_DATA_VIDEO_ENC_PARAMS (1 << 2)
+/**
+ * Decoding only.
+ * Do not apply film grain, export it instead.
+ */
+#define AV_CODEC_EXPORT_DATA_FILM_GRAIN (1 << 3)
+
+/**
+ * Pan Scan area.
+ * This specifies the area which should be displayed.
+ * Note there may be multiple such areas for one frame.
+ */
+typedef struct AVPanScan {
+ /**
+ * id
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int id;
+
+ /**
+ * width and height in 1/16 pel
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int width;
+ int height;
+
+ /**
+ * position of the top left corner in 1/16 pel for up to 3 fields/frames
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int16_t position[3][2];
+} AVPanScan;
+
+/**
+ * This structure describes the bitrate properties of an encoded bitstream. It
+ * roughly corresponds to a subset the VBV parameters for MPEG-2 or HRD
+ * parameters for H.264/HEVC.
+ */
+typedef struct AVCPBProperties {
+ /**
+ * Maximum bitrate of the stream, in bits per second.
+ * Zero if unknown or unspecified.
+ */
+#if FF_API_UNSANITIZED_BITRATES
+ int max_bitrate;
+#else
+ int64_t max_bitrate;
+#endif
+ /**
+ * Minimum bitrate of the stream, in bits per second.
+ * Zero if unknown or unspecified.
+ */
+#if FF_API_UNSANITIZED_BITRATES
+ int min_bitrate;
+#else
+ int64_t min_bitrate;
+#endif
+ /**
+ * Average bitrate of the stream, in bits per second.
+ * Zero if unknown or unspecified.
+ */
+#if FF_API_UNSANITIZED_BITRATES
+ int avg_bitrate;
+#else
+ int64_t avg_bitrate;
+#endif
+
+ /**
+ * The size of the buffer to which the ratecontrol is applied, in bits.
+ * Zero if unknown or unspecified.
+ */
+ int buffer_size;
+
+ /**
+ * The delay between the time the packet this structure is associated with
+ * is received and the time when it should be decoded, in periods of a 27MHz
+ * clock.
+ *
+ * UINT64_MAX when unknown or unspecified.
+ */
+ uint64_t vbv_delay;
+} AVCPBProperties;
+
+/**
+ * This structure supplies correlation between a packet timestamp and a wall clock
+ * production time. The definition follows the Producer Reference Time ('prft')
+ * as defined in ISO/IEC 14496-12
+ */
+typedef struct AVProducerReferenceTime {
+ /**
+ * A UTC timestamp, in microseconds, since Unix epoch (e.g, av_gettime()).
+ */
+ int64_t wallclock;
+ int flags;
+} AVProducerReferenceTime;
+
+/**
+ * The decoder will keep a reference to the frame and may reuse it later.
+ */
+#define AV_GET_BUFFER_FLAG_REF (1 << 0)
+
+/**
+ * The encoder will keep a reference to the packet and may reuse it later.
+ */
+#define AV_GET_ENCODE_BUFFER_FLAG_REF (1 << 0)
+
+struct AVCodecInternal;
+
+/**
+ * main external API structure.
+ * New fields can be added to the end with minor version bumps.
+ * Removal, reordering and changes to existing fields require a major
+ * version bump.
+ * You can use AVOptions (av_opt* / av_set/get*()) to access these fields from user
+ * applications.
+ * The name string for AVOptions options matches the associated command line
+ * parameter name and can be found in libavcodec/options_table.h
+ * The AVOption/command line parameter names differ in some cases from the C
+ * structure field names for historic reasons or brevity.
+ * sizeof(AVCodecContext) must not be used outside libav*.
+ */
+typedef struct AVCodecContext {
+ /**
+ * information on struct for av_log
+ * - set by avcodec_alloc_context3
+ */
+ const AVClass *av_class;
+ int log_level_offset;
+
+ enum AVMediaType codec_type; /* see AVMEDIA_TYPE_xxx */
+ const struct AVCodec *codec;
+ enum AVCodecID codec_id; /* see AV_CODEC_ID_xxx */
+
+ /**
+ * fourcc (LSB first, so "ABCD" -> ('D'<<24) + ('C'<<16) + ('B'<<8) + 'A').
+ * This is used to work around some encoder bugs.
+ * A demuxer should set this to what is stored in the field used to identify the codec.
+ * If there are multiple such fields in a container then the demuxer should choose the one
+ * which maximizes the information about the used codec.
+ * If the codec tag field in a container is larger than 32 bits then the demuxer should
+ * remap the longer ID to 32 bits with a table or other structure. Alternatively a new
+ * extra_codec_tag + size could be added but for this a clear advantage must be demonstrated
+ * first.
+ * - encoding: Set by user, if not then the default based on codec_id will be used.
+ * - decoding: Set by user, will be converted to uppercase by libavcodec during init.
+ */
+ unsigned int codec_tag;
+
+ void *priv_data;
+
+ /**
+ * Private context used for internal data.
+ *
+ * Unlike priv_data, this is not codec-specific. It is used in general
+ * libavcodec functions.
+ */
+ struct AVCodecInternal *internal;
+
+ /**
+ * Private data of the user, can be used to carry app specific stuff.
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ void *opaque;
+
+ /**
+ * the average bitrate
+ * - encoding: Set by user; unused for constant quantizer encoding.
+ * - decoding: Set by user, may be overwritten by libavcodec
+ * if this info is available in the stream
+ */
+ int64_t bit_rate;
+
+ /**
+ * number of bits the bitstream is allowed to diverge from the reference.
+ * the reference can be CBR (for CBR pass1) or VBR (for pass2)
+ * - encoding: Set by user; unused for constant quantizer encoding.
+ * - decoding: unused
+ */
+ int bit_rate_tolerance;
+
+ /**
+ * Global quality for codecs which cannot change it per frame.
+ * This should be proportional to MPEG-1/2/4 qscale.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int global_quality;
+
+ /**
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int compression_level;
+#define FF_COMPRESSION_DEFAULT -1
+
+ /**
+ * AV_CODEC_FLAG_*.
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int flags;
+
+ /**
+ * AV_CODEC_FLAG2_*
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int flags2;
+
+ /**
+ * some codecs need / can use extradata like Huffman tables.
+ * MJPEG: Huffman tables
+ * rv10: additional flags
+ * MPEG-4: global headers (they can be in the bitstream or here)
+ * The allocated memory should be AV_INPUT_BUFFER_PADDING_SIZE bytes larger
+ * than extradata_size to avoid problems if it is read with the bitstream reader.
+ * The bytewise contents of extradata must not depend on the architecture or CPU endianness.
+ * Must be allocated with the av_malloc() family of functions.
+ * - encoding: Set/allocated/freed by libavcodec.
+ * - decoding: Set/allocated/freed by user.
+ */
+ uint8_t *extradata;
+ int extradata_size;
+
+ /**
+ * This is the fundamental unit of time (in seconds) in terms
+ * of which frame timestamps are represented. For fixed-fps content,
+ * timebase should be 1/framerate and timestamp increments should be
+ * identically 1.
+ * This often, but not always is the inverse of the frame rate or field rate
+ * for video. 1/time_base is not the average frame rate if the frame rate is not
+ * constant.
+ *
+ * Like containers, elementary streams also can store timestamps, 1/time_base
+ * is the unit in which these timestamps are specified.
+ * As example of such codec time base see ISO/IEC 14496-2:2001(E)
+ * vop_time_increment_resolution and fixed_vop_rate
+ * (fixed_vop_rate == 0 implies that it is different from the framerate)
+ *
+ * - encoding: MUST be set by user.
+ * - decoding: the use of this field for decoding is deprecated.
+ * Use framerate instead.
+ */
+ AVRational time_base;
+
+ /**
+ * For some codecs, the time base is closer to the field rate than the frame rate.
+ * Most notably, H.264 and MPEG-2 specify time_base as half of frame duration
+ * if no telecine is used ...
+ *
+ * Set to time_base ticks per frame. Default 1, e.g., H.264/MPEG-2 set it to 2.
+ */
+ int ticks_per_frame;
+
+ /**
+ * Codec delay.
+ *
+ * Encoding: Number of frames delay there will be from the encoder input to
+ * the decoder output. (we assume the decoder matches the spec)
+ * Decoding: Number of frames delay in addition to what a standard decoder
+ * as specified in the spec would produce.
+ *
+ * Video:
+ * Number of frames the decoded output will be delayed relative to the
+ * encoded input.
+ *
+ * Audio:
+ * For encoding, this field is unused (see initial_padding).
+ *
+ * For decoding, this is the number of samples the decoder needs to
+ * output before the decoder's output is valid. When seeking, you should
+ * start decoding this many samples prior to your desired seek point.
+ *
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by libavcodec.
+ */
+ int delay;
+
+
+ /* video only */
+ /**
+ * picture width / height.
+ *
+ * @note Those fields may not match the values of the last
+ * AVFrame output by avcodec_decode_video2 due frame
+ * reordering.
+ *
+ * - encoding: MUST be set by user.
+ * - decoding: May be set by the user before opening the decoder if known e.g.
+ * from the container. Some decoders will require the dimensions
+ * to be set by the caller. During decoding, the decoder may
+ * overwrite those values as required while parsing the data.
+ */
+ int width, height;
+
+ /**
+ * Bitstream width / height, may be different from width/height e.g. when
+ * the decoded frame is cropped before being output or lowres is enabled.
+ *
+ * @note Those field may not match the value of the last
+ * AVFrame output by avcodec_receive_frame() due frame
+ * reordering.
+ *
+ * - encoding: unused
+ * - decoding: May be set by the user before opening the decoder if known
+ * e.g. from the container. During decoding, the decoder may
+ * overwrite those values as required while parsing the data.
+ */
+ int coded_width, coded_height;
+
+ /**
+ * the number of pictures in a group of pictures, or 0 for intra_only
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int gop_size;
+
+ /**
+ * Pixel format, see AV_PIX_FMT_xxx.
+ * May be set by the demuxer if known from headers.
+ * May be overridden by the decoder if it knows better.
+ *
+ * @note This field may not match the value of the last
+ * AVFrame output by avcodec_receive_frame() due frame
+ * reordering.
+ *
+ * - encoding: Set by user.
+ * - decoding: Set by user if known, overridden by libavcodec while
+ * parsing the data.
+ */
+ enum AVPixelFormat pix_fmt;
+
+ /**
+ * If non NULL, 'draw_horiz_band' is called by the libavcodec
+ * decoder to draw a horizontal band. It improves cache usage. Not
+ * all codecs can do that. You must check the codec capabilities
+ * beforehand.
+ * When multithreading is used, it may be called from multiple threads
+ * at the same time; threads might draw different parts of the same AVFrame,
+ * or multiple AVFrames, and there is no guarantee that slices will be drawn
+ * in order.
+ * The function is also used by hardware acceleration APIs.
+ * It is called at least once during frame decoding to pass
+ * the data needed for hardware render.
+ * In that mode instead of pixel data, AVFrame points to
+ * a structure specific to the acceleration API. The application
+ * reads the structure and can change some fields to indicate progress
+ * or mark state.
+ * - encoding: unused
+ * - decoding: Set by user.
+ * @param height the height of the slice
+ * @param y the y position of the slice
+ * @param type 1->top field, 2->bottom field, 3->frame
+ * @param offset offset into the AVFrame.data from which the slice should be read
+ */
+ void (*draw_horiz_band)(struct AVCodecContext *s,
+ const AVFrame *src, int offset[AV_NUM_DATA_POINTERS],
+ int y, int type, int height);
+
+ /**
+ * callback to negotiate the pixelFormat
+ * @param fmt is the list of formats which are supported by the codec,
+ * it is terminated by -1 as 0 is a valid format, the formats are ordered by quality.
+ * The first is always the native one.
+ * @note The callback may be called again immediately if initialization for
+ * the selected (hardware-accelerated) pixel format failed.
+ * @warning Behavior is undefined if the callback returns a value not
+ * in the fmt list of formats.
+ * @return the chosen format
+ * - encoding: unused
+ * - decoding: Set by user, if not set the native format will be chosen.
+ */
+ enum AVPixelFormat (*get_format)(struct AVCodecContext *s, const enum AVPixelFormat * fmt);
+
+ /**
+ * maximum number of B-frames between non-B-frames
+ * Note: The output will be delayed by max_b_frames+1 relative to the input.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int max_b_frames;
+
+ /**
+ * qscale factor between IP and B-frames
+ * If > 0 then the last P-frame quantizer will be used (q= lastp_q*factor+offset).
+ * If < 0 then normal ratecontrol will be done (q= -normal_q*factor+offset).
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float b_quant_factor;
+
+#if FF_API_PRIVATE_OPT
+ /** @deprecated use encoder private options instead */
+ attribute_deprecated
+ int b_frame_strategy;
+#endif
+
+ /**
+ * qscale offset between IP and B-frames
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float b_quant_offset;
+
+ /**
+ * Size of the frame reordering buffer in the decoder.
+ * For MPEG-2 it is 1 IPB or 0 low delay IP.
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by libavcodec.
+ */
+ int has_b_frames;
+
+#if FF_API_PRIVATE_OPT
+ /** @deprecated use encoder private options instead */
+ attribute_deprecated
+ int mpeg_quant;
+#endif
+
+ /**
+ * qscale factor between P- and I-frames
+ * If > 0 then the last P-frame quantizer will be used (q = lastp_q * factor + offset).
+ * If < 0 then normal ratecontrol will be done (q= -normal_q*factor+offset).
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float i_quant_factor;
+
+ /**
+ * qscale offset between P and I-frames
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float i_quant_offset;
+
+ /**
+ * luminance masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float lumi_masking;
+
+ /**
+ * temporary complexity masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float temporal_cplx_masking;
+
+ /**
+ * spatial complexity masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float spatial_cplx_masking;
+
+ /**
+ * p block masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float p_masking;
+
+ /**
+ * darkness masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float dark_masking;
+
+ /**
+ * slice count
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by user (or 0).
+ */
+ int slice_count;
+
+#if FF_API_PRIVATE_OPT
+ /** @deprecated use encoder private options instead */
+ attribute_deprecated
+ int prediction_method;
+#define FF_PRED_LEFT 0
+#define FF_PRED_PLANE 1
+#define FF_PRED_MEDIAN 2
+#endif
+
+ /**
+ * slice offsets in the frame in bytes
+ * - encoding: Set/allocated by libavcodec.
+ * - decoding: Set/allocated by user (or NULL).
+ */
+ int *slice_offset;
+
+ /**
+ * sample aspect ratio (0 if unknown)
+ * That is the width of a pixel divided by the height of the pixel.
+ * Numerator and denominator must be relatively prime and smaller than 256 for some video standards.
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ AVRational sample_aspect_ratio;
+
+ /**
+ * motion estimation comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_cmp;
+ /**
+ * subpixel motion estimation comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_sub_cmp;
+ /**
+ * macroblock comparison function (not supported yet)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mb_cmp;
+ /**
+ * interlaced DCT comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int ildct_cmp;
+#define FF_CMP_SAD 0
+#define FF_CMP_SSE 1
+#define FF_CMP_SATD 2
+#define FF_CMP_DCT 3
+#define FF_CMP_PSNR 4
+#define FF_CMP_BIT 5
+#define FF_CMP_RD 6
+#define FF_CMP_ZERO 7
+#define FF_CMP_VSAD 8
+#define FF_CMP_VSSE 9
+#define FF_CMP_NSSE 10
+#define FF_CMP_W53 11
+#define FF_CMP_W97 12
+#define FF_CMP_DCTMAX 13
+#define FF_CMP_DCT264 14
+#define FF_CMP_MEDIAN_SAD 15
+#define FF_CMP_CHROMA 256
+
+ /**
+ * ME diamond size & shape
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int dia_size;
+
+ /**
+ * amount of previous MV predictors (2a+1 x 2a+1 square)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int last_predictor_count;
+
+#if FF_API_PRIVATE_OPT
+ /** @deprecated use encoder private options instead */
+ attribute_deprecated
+ int pre_me;
+#endif
+
+ /**
+ * motion estimation prepass comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_pre_cmp;
+
+ /**
+ * ME prepass diamond size & shape
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int pre_dia_size;
+
+ /**
+ * subpel ME quality
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_subpel_quality;
+
+ /**
+ * maximum motion estimation search range in subpel units
+ * If 0 then no limit.
+ *
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_range;
+
+ /**
+ * slice flags
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int slice_flags;
+#define SLICE_FLAG_CODED_ORDER 0x0001 ///< draw_horiz_band() is called in coded order instead of display
+#define SLICE_FLAG_ALLOW_FIELD 0x0002 ///< allow draw_horiz_band() with field slices (MPEG-2 field pics)
+#define SLICE_FLAG_ALLOW_PLANE 0x0004 ///< allow draw_horiz_band() with 1 component at a time (SVQ1)
+
+ /**
+ * macroblock decision mode
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mb_decision;
+#define FF_MB_DECISION_SIMPLE 0 ///< uses mb_cmp
+#define FF_MB_DECISION_BITS 1 ///< chooses the one which needs the fewest bits
+#define FF_MB_DECISION_RD 2 ///< rate distortion
+
+ /**
+ * custom intra quantization matrix
+ * Must be allocated with the av_malloc() family of functions, and will be freed in
+ * avcodec_free_context().
+ * - encoding: Set/allocated by user, freed by libavcodec. Can be NULL.
+ * - decoding: Set/allocated/freed by libavcodec.
+ */
+ uint16_t *intra_matrix;
+
+ /**
+ * custom inter quantization matrix
+ * Must be allocated with the av_malloc() family of functions, and will be freed in
+ * avcodec_free_context().
+ * - encoding: Set/allocated by user, freed by libavcodec. Can be NULL.
+ * - decoding: Set/allocated/freed by libavcodec.
+ */
+ uint16_t *inter_matrix;
+
+#if FF_API_PRIVATE_OPT
+ /** @deprecated use encoder private options instead */
+ attribute_deprecated
+ int scenechange_threshold;
+
+ /** @deprecated use encoder private options instead */
+ attribute_deprecated
+ int noise_reduction;
+#endif
+
+ /**
+ * precision of the intra DC coefficient - 8
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec
+ */
+ int intra_dc_precision;
+
+ /**
+ * Number of macroblock rows at the top which are skipped.
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int skip_top;
+
+ /**
+ * Number of macroblock rows at the bottom which are skipped.
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int skip_bottom;
+
+ /**
+ * minimum MB Lagrange multiplier
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mb_lmin;
+
+ /**
+ * maximum MB Lagrange multiplier
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mb_lmax;
+
+#if FF_API_PRIVATE_OPT
+ /**
+ * @deprecated use encoder private options instead
+ */
+ attribute_deprecated
+ int me_penalty_compensation;
+#endif
+
+ /**
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int bidir_refine;
+
+#if FF_API_PRIVATE_OPT
+ /** @deprecated use encoder private options instead */
+ attribute_deprecated
+ int brd_scale;
+#endif
+
+ /**
+ * minimum GOP size
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int keyint_min;
+
+ /**
+ * number of reference frames
+ * - encoding: Set by user.
+ * - decoding: Set by lavc.
+ */
+ int refs;
+
+#if FF_API_PRIVATE_OPT
+ /** @deprecated use encoder private options instead */
+ attribute_deprecated
+ int chromaoffset;
+#endif
+
+ /**
+ * Note: Value depends upon the compare function used for fullpel ME.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mv0_threshold;
+
+#if FF_API_PRIVATE_OPT
+ /** @deprecated use encoder private options instead */
+ attribute_deprecated
+ int b_sensitivity;
+#endif
+
+ /**
+ * Chromaticity coordinates of the source primaries.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorPrimaries color_primaries;
+
+ /**
+ * Color Transfer Characteristic.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorTransferCharacteristic color_trc;
+
+ /**
+ * YUV colorspace type.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorSpace colorspace;
+
+ /**
+ * MPEG vs JPEG YUV range.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorRange color_range;
+
+ /**
+ * This defines the location of chroma samples.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVChromaLocation chroma_sample_location;
+
+ /**
+ * Number of slices.
+ * Indicates number of picture subdivisions. Used for parallelized
+ * decoding.
+ * - encoding: Set by user
+ * - decoding: unused
+ */
+ int slices;
+
+ /** Field order
+ * - encoding: set by libavcodec
+ * - decoding: Set by user.
+ */
+ enum AVFieldOrder field_order;
+
+ /* audio only */
+ int sample_rate; ///< samples per second
+ int channels; ///< number of audio channels
+
+ /**
+ * audio sample format
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ enum AVSampleFormat sample_fmt; ///< sample format
+
+ /* The following data should not be initialized. */
+ /**
+ * Number of samples per channel in an audio frame.
+ *
+ * - encoding: set by libavcodec in avcodec_open2(). Each submitted frame
+ * except the last must contain exactly frame_size samples per channel.
+ * May be 0 when the codec has AV_CODEC_CAP_VARIABLE_FRAME_SIZE set, then the
+ * frame size is not restricted.
+ * - decoding: may be set by some decoders to indicate constant frame size
+ */
+ int frame_size;
+
+ /**
+ * Frame counter, set by libavcodec.
+ *
+ * - decoding: total number of frames returned from the decoder so far.
+ * - encoding: total number of frames passed to the encoder so far.
+ *
+ * @note the counter is not incremented if encoding/decoding resulted in
+ * an error.
+ */
+ int frame_number;
+
+ /**
+ * number of bytes per packet if constant and known or 0
+ * Used by some WAV based audio codecs.
+ */
+ int block_align;
+
+ /**
+ * Audio cutoff bandwidth (0 means "automatic")
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int cutoff;
+
+ /**
+ * Audio channel layout.
+ * - encoding: set by user.
+ * - decoding: set by user, may be overwritten by libavcodec.
+ */
+ uint64_t channel_layout;
+
+ /**
+ * Request decoder to use this channel layout if it can (0 for default)
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ uint64_t request_channel_layout;
+
+ /**
+ * Type of service that the audio stream conveys.
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ enum AVAudioServiceType audio_service_type;
+
+ /**
+ * desired sample format
+ * - encoding: Not used.
+ * - decoding: Set by user.
+ * Decoder will decode to this format if it can.
+ */
+ enum AVSampleFormat request_sample_fmt;
+
+ /**
+ * This callback is called at the beginning of each frame to get data
+ * buffer(s) for it. There may be one contiguous buffer for all the data or
+ * there may be a buffer per each data plane or anything in between. What
+ * this means is, you may set however many entries in buf[] you feel necessary.
+ * Each buffer must be reference-counted using the AVBuffer API (see description
+ * of buf[] below).
+ *
+ * The following fields will be set in the frame before this callback is
+ * called:
+ * - format
+ * - width, height (video only)
+ * - sample_rate, channel_layout, nb_samples (audio only)
+ * Their values may differ from the corresponding values in
+ * AVCodecContext. This callback must use the frame values, not the codec
+ * context values, to calculate the required buffer size.
+ *
+ * This callback must fill the following fields in the frame:
+ * - data[]
+ * - linesize[]
+ * - extended_data:
+ * * if the data is planar audio with more than 8 channels, then this
+ * callback must allocate and fill extended_data to contain all pointers
+ * to all data planes. data[] must hold as many pointers as it can.
+ * extended_data must be allocated with av_malloc() and will be freed in
+ * av_frame_unref().
+ * * otherwise extended_data must point to data
+ * - buf[] must contain one or more pointers to AVBufferRef structures. Each of
+ * the frame's data and extended_data pointers must be contained in these. That
+ * is, one AVBufferRef for each allocated chunk of memory, not necessarily one
+ * AVBufferRef per data[] entry. See: av_buffer_create(), av_buffer_alloc(),
+ * and av_buffer_ref().
+ * - extended_buf and nb_extended_buf must be allocated with av_malloc() by
+ * this callback and filled with the extra buffers if there are more
+ * buffers than buf[] can hold. extended_buf will be freed in
+ * av_frame_unref().
+ *
+ * If AV_CODEC_CAP_DR1 is not set then get_buffer2() must call
+ * avcodec_default_get_buffer2() instead of providing buffers allocated by
+ * some other means.
+ *
+ * Each data plane must be aligned to the maximum required by the target
+ * CPU.
+ *
+ * @see avcodec_default_get_buffer2()
+ *
+ * Video:
+ *
+ * If AV_GET_BUFFER_FLAG_REF is set in flags then the frame may be reused
+ * (read and/or written to if it is writable) later by libavcodec.
+ *
+ * avcodec_align_dimensions2() should be used to find the required width and
+ * height, as they normally need to be rounded up to the next multiple of 16.
+ *
+ * Some decoders do not support linesizes changing between frames.
+ *
+ * If frame multithreading is used, this callback may be called from a
+ * different thread, but not from more than one at once. Does not need to be
+ * reentrant.
+ *
+ * @see avcodec_align_dimensions2()
+ *
+ * Audio:
+ *
+ * Decoders request a buffer of a particular size by setting
+ * AVFrame.nb_samples prior to calling get_buffer2(). The decoder may,
+ * however, utilize only part of the buffer by setting AVFrame.nb_samples
+ * to a smaller value in the output frame.
+ *
+ * As a convenience, av_samples_get_buffer_size() and
+ * av_samples_fill_arrays() in libavutil may be used by custom get_buffer2()
+ * functions to find the required data size and to fill data pointers and
+ * linesize. In AVFrame.linesize, only linesize[0] may be set for audio
+ * since all planes must be the same size.
+ *
+ * @see av_samples_get_buffer_size(), av_samples_fill_arrays()
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec, user can override.
+ */
+ int (*get_buffer2)(struct AVCodecContext *s, AVFrame *frame, int flags);
+
+#if FF_API_OLD_ENCDEC
+ /**
+ * If non-zero, the decoded audio and video frames returned from
+ * avcodec_decode_video2() and avcodec_decode_audio4() are reference-counted
+ * and are valid indefinitely. The caller must free them with
+ * av_frame_unref() when they are not needed anymore.
+ * Otherwise, the decoded frames must not be freed by the caller and are
+ * only valid until the next decode call.
+ *
+ * This is always automatically enabled if avcodec_receive_frame() is used.
+ *
+ * - encoding: unused
+ * - decoding: set by the caller before avcodec_open2().
+ */
+ attribute_deprecated
+ int refcounted_frames;
+#endif
+
+ /* - encoding parameters */
+ float qcompress; ///< amount of qscale change between easy & hard scenes (0.0-1.0)
+ float qblur; ///< amount of qscale smoothing over time (0.0-1.0)
+
+ /**
+ * minimum quantizer
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int qmin;
+
+ /**
+ * maximum quantizer
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int qmax;
+
+ /**
+ * maximum quantizer difference between frames
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int max_qdiff;
+
+ /**
+ * decoder bitstream buffer size
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int rc_buffer_size;
+
+ /**
+ * ratecontrol override, see RcOverride
+ * - encoding: Allocated/set/freed by user.
+ * - decoding: unused
+ */
+ int rc_override_count;
+ RcOverride *rc_override;
+
+ /**
+ * maximum bitrate
+ * - encoding: Set by user.
+ * - decoding: Set by user, may be overwritten by libavcodec.
+ */
+ int64_t rc_max_rate;
+
+ /**
+ * minimum bitrate
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int64_t rc_min_rate;
+
+ /**
+ * Ratecontrol attempt to use, at maximum, <value> of what can be used without an underflow.
+ * - encoding: Set by user.
+ * - decoding: unused.
+ */
+ float rc_max_available_vbv_use;
+
+ /**
+ * Ratecontrol attempt to use, at least, <value> times the amount needed to prevent a vbv overflow.
+ * - encoding: Set by user.
+ * - decoding: unused.
+ */
+ float rc_min_vbv_overflow_use;
+
+ /**
+ * Number of bits which should be loaded into the rc buffer before decoding starts.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int rc_initial_buffer_occupancy;
+
+#if FF_API_CODER_TYPE
+#define FF_CODER_TYPE_VLC 0
+#define FF_CODER_TYPE_AC 1
+#define FF_CODER_TYPE_RAW 2
+#define FF_CODER_TYPE_RLE 3
+ /**
+ * @deprecated use encoder private options instead
+ */
+ attribute_deprecated
+ int coder_type;
+#endif /* FF_API_CODER_TYPE */
+
+#if FF_API_PRIVATE_OPT
+ /** @deprecated use encoder private options instead */
+ attribute_deprecated
+ int context_model;
+#endif
+
+#if FF_API_PRIVATE_OPT
+ /** @deprecated use encoder private options instead */
+ attribute_deprecated
+ int frame_skip_threshold;
+
+ /** @deprecated use encoder private options instead */
+ attribute_deprecated
+ int frame_skip_factor;
+
+ /** @deprecated use encoder private options instead */
+ attribute_deprecated
+ int frame_skip_exp;
+
+ /** @deprecated use encoder private options instead */
+ attribute_deprecated
+ int frame_skip_cmp;
+#endif /* FF_API_PRIVATE_OPT */
+
+ /**
+ * trellis RD quantization
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int trellis;
+
+#if FF_API_PRIVATE_OPT
+ /** @deprecated use encoder private options instead */
+ attribute_deprecated
+ int min_prediction_order;
+
+ /** @deprecated use encoder private options instead */
+ attribute_deprecated
+ int max_prediction_order;
+
+ /** @deprecated use encoder private options instead */
+ attribute_deprecated
+ int64_t timecode_frame_start;
+#endif
+
+#if FF_API_RTP_CALLBACK
+ /**
+ * @deprecated unused
+ */
+ /* The RTP callback: This function is called */
+ /* every time the encoder has a packet to send. */
+ /* It depends on the encoder if the data starts */
+ /* with a Start Code (it should). H.263 does. */
+ /* mb_nb contains the number of macroblocks */
+ /* encoded in the RTP payload. */
+ attribute_deprecated
+ void (*rtp_callback)(struct AVCodecContext *avctx, void *data, int size, int mb_nb);
+#endif
+
+#if FF_API_PRIVATE_OPT
+ /** @deprecated use encoder private options instead */
+ attribute_deprecated
+ int rtp_payload_size; /* The size of the RTP payload: the coder will */
+ /* do its best to deliver a chunk with size */
+ /* below rtp_payload_size, the chunk will start */
+ /* with a start code on some codecs like H.263. */
+ /* This doesn't take account of any particular */
+ /* headers inside the transmitted RTP payload. */
+#endif
+
+#if FF_API_STAT_BITS
+ /* statistics, used for 2-pass encoding */
+ attribute_deprecated
+ int mv_bits;
+ attribute_deprecated
+ int header_bits;
+ attribute_deprecated
+ int i_tex_bits;
+ attribute_deprecated
+ int p_tex_bits;
+ attribute_deprecated
+ int i_count;
+ attribute_deprecated
+ int p_count;
+ attribute_deprecated
+ int skip_count;
+ attribute_deprecated
+ int misc_bits;
+
+ /** @deprecated this field is unused */
+ attribute_deprecated
+ int frame_bits;
+#endif
+
+ /**
+ * pass1 encoding statistics output buffer
+ * - encoding: Set by libavcodec.
+ * - decoding: unused
+ */
+ char *stats_out;
+
+ /**
+ * pass2 encoding statistics input buffer
+ * Concatenated stuff from stats_out of pass1 should be placed here.
+ * - encoding: Allocated/set/freed by user.
+ * - decoding: unused
+ */
+ char *stats_in;
+
+ /**
+ * Work around bugs in encoders which sometimes cannot be detected automatically.
+ * - encoding: Set by user
+ * - decoding: Set by user
+ */
+ int workaround_bugs;
+#define FF_BUG_AUTODETECT 1 ///< autodetection
+#define FF_BUG_XVID_ILACE 4
+#define FF_BUG_UMP4 8
+#define FF_BUG_NO_PADDING 16
+#define FF_BUG_AMV 32
+#define FF_BUG_QPEL_CHROMA 64
+#define FF_BUG_STD_QPEL 128
+#define FF_BUG_QPEL_CHROMA2 256
+#define FF_BUG_DIRECT_BLOCKSIZE 512
+#define FF_BUG_EDGE 1024
+#define FF_BUG_HPEL_CHROMA 2048
+#define FF_BUG_DC_CLIP 4096
+#define FF_BUG_MS 8192 ///< Work around various bugs in Microsoft's broken decoders.
+#define FF_BUG_TRUNCATED 16384
+#define FF_BUG_IEDGE 32768
+
+ /**
+ * strictly follow the standard (MPEG-4, ...).
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ * Setting this to STRICT or higher means the encoder and decoder will
+ * generally do stupid things, whereas setting it to unofficial or lower
+ * will mean the encoder might produce output that is not supported by all
+ * spec-compliant decoders. Decoders don't differentiate between normal,
+ * unofficial and experimental (that is, they always try to decode things
+ * when they can) unless they are explicitly asked to behave stupidly
+ * (=strictly conform to the specs)
+ */
+ int strict_std_compliance;
+#define FF_COMPLIANCE_VERY_STRICT 2 ///< Strictly conform to an older more strict version of the spec or reference software.
+#define FF_COMPLIANCE_STRICT 1 ///< Strictly conform to all the things in the spec no matter what consequences.
+#define FF_COMPLIANCE_NORMAL 0
+#define FF_COMPLIANCE_UNOFFICIAL -1 ///< Allow unofficial extensions
+#define FF_COMPLIANCE_EXPERIMENTAL -2 ///< Allow nonstandardized experimental things.
+
+ /**
+ * error concealment flags
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int error_concealment;
+#define FF_EC_GUESS_MVS 1
+#define FF_EC_DEBLOCK 2
+#define FF_EC_FAVOR_INTER 256
+
+ /**
+ * debug
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int debug;
+#define FF_DEBUG_PICT_INFO 1
+#define FF_DEBUG_RC 2
+#define FF_DEBUG_BITSTREAM 4
+#define FF_DEBUG_MB_TYPE 8
+#define FF_DEBUG_QP 16
+#define FF_DEBUG_DCT_COEFF 0x00000040
+#define FF_DEBUG_SKIP 0x00000080
+#define FF_DEBUG_STARTCODE 0x00000100
+#define FF_DEBUG_ER 0x00000400
+#define FF_DEBUG_MMCO 0x00000800
+#define FF_DEBUG_BUGS 0x00001000
+#define FF_DEBUG_BUFFERS 0x00008000
+#define FF_DEBUG_THREADS 0x00010000
+#define FF_DEBUG_GREEN_MD 0x00800000
+#define FF_DEBUG_NOMC 0x01000000
+
+ /**
+ * Error recognition; may misdetect some more or less valid parts as errors.
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int err_recognition;
+
+/**
+ * Verify checksums embedded in the bitstream (could be of either encoded or
+ * decoded data, depending on the codec) and print an error message on mismatch.
+ * If AV_EF_EXPLODE is also set, a mismatching checksum will result in the
+ * decoder returning an error.
+ */
+#define AV_EF_CRCCHECK (1<<0)
+#define AV_EF_BITSTREAM (1<<1) ///< detect bitstream specification deviations
+#define AV_EF_BUFFER (1<<2) ///< detect improper bitstream length
+#define AV_EF_EXPLODE (1<<3) ///< abort decoding on minor error detection
+
+#define AV_EF_IGNORE_ERR (1<<15) ///< ignore errors and continue
+#define AV_EF_CAREFUL (1<<16) ///< consider things that violate the spec, are fast to calculate and have not been seen in the wild as errors
+#define AV_EF_COMPLIANT (1<<17) ///< consider all spec non compliances as errors
+#define AV_EF_AGGRESSIVE (1<<18) ///< consider things that a sane encoder should not do as an error
+
+
+ /**
+ * opaque 64-bit number (generally a PTS) that will be reordered and
+ * output in AVFrame.reordered_opaque
+ * - encoding: Set by libavcodec to the reordered_opaque of the input
+ * frame corresponding to the last returned packet. Only
+ * supported by encoders with the
+ * AV_CODEC_CAP_ENCODER_REORDERED_OPAQUE capability.
+ * - decoding: Set by user.
+ */
+ int64_t reordered_opaque;
+
+ /**
+ * Hardware accelerator in use
+ * - encoding: unused.
+ * - decoding: Set by libavcodec
+ */
+ const struct AVHWAccel *hwaccel;
+
+ /**
+ * Hardware accelerator context.
+ * For some hardware accelerators, a global context needs to be
+ * provided by the user. In that case, this holds display-dependent
+ * data FFmpeg cannot instantiate itself. Please refer to the
+ * FFmpeg HW accelerator documentation to know how to fill this
+ * is. e.g. for VA API, this is a struct vaapi_context.
+ * - encoding: unused
+ * - decoding: Set by user
+ */
+ void *hwaccel_context;
+
+ /**
+ * error
+ * - encoding: Set by libavcodec if flags & AV_CODEC_FLAG_PSNR.
+ * - decoding: unused
+ */
+ uint64_t error[AV_NUM_DATA_POINTERS];
+
+ /**
+ * DCT algorithm, see FF_DCT_* below
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int dct_algo;
+#define FF_DCT_AUTO 0
+#define FF_DCT_FASTINT 1
+#define FF_DCT_INT 2
+#define FF_DCT_MMX 3
+#define FF_DCT_ALTIVEC 5
+#define FF_DCT_FAAN 6
+
+ /**
+ * IDCT algorithm, see FF_IDCT_* below.
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int idct_algo;
+#define FF_IDCT_AUTO 0
+#define FF_IDCT_INT 1
+#define FF_IDCT_SIMPLE 2
+#define FF_IDCT_SIMPLEMMX 3
+#define FF_IDCT_ARM 7
+#define FF_IDCT_ALTIVEC 8
+#define FF_IDCT_SIMPLEARM 10
+#define FF_IDCT_XVID 14
+#define FF_IDCT_SIMPLEARMV5TE 16
+#define FF_IDCT_SIMPLEARMV6 17
+#define FF_IDCT_FAAN 20
+#define FF_IDCT_SIMPLENEON 22
+#define FF_IDCT_NONE 24 /* Used by XvMC to extract IDCT coefficients with FF_IDCT_PERM_NONE */
+#define FF_IDCT_SIMPLEAUTO 128
+
+ /**
+ * bits per sample/pixel from the demuxer (needed for huffyuv).
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by user.
+ */
+ int bits_per_coded_sample;
+
+ /**
+ * Bits per sample/pixel of internal libavcodec pixel/sample format.
+ * - encoding: set by user.
+ * - decoding: set by libavcodec.
+ */
+ int bits_per_raw_sample;
+
+ /**
+ * low resolution decoding, 1-> 1/2 size, 2->1/4 size
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int lowres;
+
+#if FF_API_CODED_FRAME
+ /**
+ * the picture in the bitstream
+ * - encoding: Set by libavcodec.
+ * - decoding: unused
+ *
+ * @deprecated use the quality factor packet side data instead
+ */
+ attribute_deprecated AVFrame *coded_frame;
+#endif
+
+ /**
+ * thread count
+ * is used to decide how many independent tasks should be passed to execute()
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int thread_count;
+
+ /**
+ * Which multithreading methods to use.
+ * Use of FF_THREAD_FRAME will increase decoding delay by one frame per thread,
+ * so clients which cannot provide future frames should not use it.
+ *
+ * - encoding: Set by user, otherwise the default is used.
+ * - decoding: Set by user, otherwise the default is used.
+ */
+ int thread_type;
+#define FF_THREAD_FRAME 1 ///< Decode more than one frame at once
+#define FF_THREAD_SLICE 2 ///< Decode more than one part of a single frame at once
+
+ /**
+ * Which multithreading methods are in use by the codec.
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by libavcodec.
+ */
+ int active_thread_type;
+
+#if FF_API_THREAD_SAFE_CALLBACKS
+ /**
+ * Set by the client if its custom get_buffer() callback can be called
+ * synchronously from another thread, which allows faster multithreaded decoding.
+ * draw_horiz_band() will be called from other threads regardless of this setting.
+ * Ignored if the default get_buffer() is used.
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ *
+ * @deprecated the custom get_buffer2() callback should always be
+ * thread-safe. Thread-unsafe get_buffer2() implementations will be
+ * invalid starting with LIBAVCODEC_VERSION_MAJOR=60; in other words,
+ * libavcodec will behave as if this field was always set to 1.
+ * Callers that want to be forward compatible with future libavcodec
+ * versions should wrap access to this field in
+ * #if LIBAVCODEC_VERSION_MAJOR < 60
+ */
+ attribute_deprecated
+ int thread_safe_callbacks;
+#endif
+
+ /**
+ * The codec may call this to execute several independent things.
+ * It will return only after finishing all tasks.
+ * The user may replace this with some multithreaded implementation,
+ * the default implementation will execute the parts serially.
+ * @param count the number of things to execute
+ * - encoding: Set by libavcodec, user can override.
+ * - decoding: Set by libavcodec, user can override.
+ */
+ int (*execute)(struct AVCodecContext *c, int (*func)(struct AVCodecContext *c2, void *arg), void *arg2, int *ret, int count, int size);
+
+ /**
+ * The codec may call this to execute several independent things.
+ * It will return only after finishing all tasks.
+ * The user may replace this with some multithreaded implementation,
+ * the default implementation will execute the parts serially.
+ * Also see avcodec_thread_init and e.g. the --enable-pthread configure option.
+ * @param c context passed also to func
+ * @param count the number of things to execute
+ * @param arg2 argument passed unchanged to func
+ * @param ret return values of executed functions, must have space for "count" values. May be NULL.
+ * @param func function that will be called count times, with jobnr from 0 to count-1.
+ * threadnr will be in the range 0 to c->thread_count-1 < MAX_THREADS and so that no
+ * two instances of func executing at the same time will have the same threadnr.
+ * @return always 0 currently, but code should handle a future improvement where when any call to func
+ * returns < 0 no further calls to func may be done and < 0 is returned.
+ * - encoding: Set by libavcodec, user can override.
+ * - decoding: Set by libavcodec, user can override.
+ */
+ int (*execute2)(struct AVCodecContext *c, int (*func)(struct AVCodecContext *c2, void *arg, int jobnr, int threadnr), void *arg2, int *ret, int count);
+
+ /**
+ * noise vs. sse weight for the nsse comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int nsse_weight;
+
+ /**
+ * profile
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int profile;
+#define FF_PROFILE_UNKNOWN -99
+#define FF_PROFILE_RESERVED -100
+
+#define FF_PROFILE_AAC_MAIN 0
+#define FF_PROFILE_AAC_LOW 1
+#define FF_PROFILE_AAC_SSR 2
+#define FF_PROFILE_AAC_LTP 3
+#define FF_PROFILE_AAC_HE 4
+#define FF_PROFILE_AAC_HE_V2 28
+#define FF_PROFILE_AAC_LD 22
+#define FF_PROFILE_AAC_ELD 38
+#define FF_PROFILE_MPEG2_AAC_LOW 128
+#define FF_PROFILE_MPEG2_AAC_HE 131
+
+#define FF_PROFILE_DNXHD 0
+#define FF_PROFILE_DNXHR_LB 1
+#define FF_PROFILE_DNXHR_SQ 2
+#define FF_PROFILE_DNXHR_HQ 3
+#define FF_PROFILE_DNXHR_HQX 4
+#define FF_PROFILE_DNXHR_444 5
+
+#define FF_PROFILE_DTS 20
+#define FF_PROFILE_DTS_ES 30
+#define FF_PROFILE_DTS_96_24 40
+#define FF_PROFILE_DTS_HD_HRA 50
+#define FF_PROFILE_DTS_HD_MA 60
+#define FF_PROFILE_DTS_EXPRESS 70
+
+#define FF_PROFILE_MPEG2_422 0
+#define FF_PROFILE_MPEG2_HIGH 1
+#define FF_PROFILE_MPEG2_SS 2
+#define FF_PROFILE_MPEG2_SNR_SCALABLE 3
+#define FF_PROFILE_MPEG2_MAIN 4
+#define FF_PROFILE_MPEG2_SIMPLE 5
+
+#define FF_PROFILE_H264_CONSTRAINED (1<<9) // 8+1; constraint_set1_flag
+#define FF_PROFILE_H264_INTRA (1<<11) // 8+3; constraint_set3_flag
+
+#define FF_PROFILE_H264_BASELINE 66
+#define FF_PROFILE_H264_CONSTRAINED_BASELINE (66|FF_PROFILE_H264_CONSTRAINED)
+#define FF_PROFILE_H264_MAIN 77
+#define FF_PROFILE_H264_EXTENDED 88
+#define FF_PROFILE_H264_HIGH 100
+#define FF_PROFILE_H264_HIGH_10 110
+#define FF_PROFILE_H264_HIGH_10_INTRA (110|FF_PROFILE_H264_INTRA)
+#define FF_PROFILE_H264_MULTIVIEW_HIGH 118
+#define FF_PROFILE_H264_HIGH_422 122
+#define FF_PROFILE_H264_HIGH_422_INTRA (122|FF_PROFILE_H264_INTRA)
+#define FF_PROFILE_H264_STEREO_HIGH 128
+#define FF_PROFILE_H264_HIGH_444 144
+#define FF_PROFILE_H264_HIGH_444_PREDICTIVE 244
+#define FF_PROFILE_H264_HIGH_444_INTRA (244|FF_PROFILE_H264_INTRA)
+#define FF_PROFILE_H264_CAVLC_444 44
+
+#define FF_PROFILE_VC1_SIMPLE 0
+#define FF_PROFILE_VC1_MAIN 1
+#define FF_PROFILE_VC1_COMPLEX 2
+#define FF_PROFILE_VC1_ADVANCED 3
+
+#define FF_PROFILE_MPEG4_SIMPLE 0
+#define FF_PROFILE_MPEG4_SIMPLE_SCALABLE 1
+#define FF_PROFILE_MPEG4_CORE 2
+#define FF_PROFILE_MPEG4_MAIN 3
+#define FF_PROFILE_MPEG4_N_BIT 4
+#define FF_PROFILE_MPEG4_SCALABLE_TEXTURE 5
+#define FF_PROFILE_MPEG4_SIMPLE_FACE_ANIMATION 6
+#define FF_PROFILE_MPEG4_BASIC_ANIMATED_TEXTURE 7
+#define FF_PROFILE_MPEG4_HYBRID 8
+#define FF_PROFILE_MPEG4_ADVANCED_REAL_TIME 9
+#define FF_PROFILE_MPEG4_CORE_SCALABLE 10
+#define FF_PROFILE_MPEG4_ADVANCED_CODING 11
+#define FF_PROFILE_MPEG4_ADVANCED_CORE 12
+#define FF_PROFILE_MPEG4_ADVANCED_SCALABLE_TEXTURE 13
+#define FF_PROFILE_MPEG4_SIMPLE_STUDIO 14
+#define FF_PROFILE_MPEG4_ADVANCED_SIMPLE 15
+
+#define FF_PROFILE_JPEG2000_CSTREAM_RESTRICTION_0 1
+#define FF_PROFILE_JPEG2000_CSTREAM_RESTRICTION_1 2
+#define FF_PROFILE_JPEG2000_CSTREAM_NO_RESTRICTION 32768
+#define FF_PROFILE_JPEG2000_DCINEMA_2K 3
+#define FF_PROFILE_JPEG2000_DCINEMA_4K 4
+
+#define FF_PROFILE_VP9_0 0
+#define FF_PROFILE_VP9_1 1
+#define FF_PROFILE_VP9_2 2
+#define FF_PROFILE_VP9_3 3
+
+#define FF_PROFILE_HEVC_MAIN 1
+#define FF_PROFILE_HEVC_MAIN_10 2
+#define FF_PROFILE_HEVC_MAIN_STILL_PICTURE 3
+#define FF_PROFILE_HEVC_REXT 4
+
+#define FF_PROFILE_VVC_MAIN_10 1
+#define FF_PROFILE_VVC_MAIN_10_444 33
+
+#define FF_PROFILE_AV1_MAIN 0
+#define FF_PROFILE_AV1_HIGH 1
+#define FF_PROFILE_AV1_PROFESSIONAL 2
+
+#define FF_PROFILE_MJPEG_HUFFMAN_BASELINE_DCT 0xc0
+#define FF_PROFILE_MJPEG_HUFFMAN_EXTENDED_SEQUENTIAL_DCT 0xc1
+#define FF_PROFILE_MJPEG_HUFFMAN_PROGRESSIVE_DCT 0xc2
+#define FF_PROFILE_MJPEG_HUFFMAN_LOSSLESS 0xc3
+#define FF_PROFILE_MJPEG_JPEG_LS 0xf7
+
+#define FF_PROFILE_SBC_MSBC 1
+
+#define FF_PROFILE_PRORES_PROXY 0
+#define FF_PROFILE_PRORES_LT 1
+#define FF_PROFILE_PRORES_STANDARD 2
+#define FF_PROFILE_PRORES_HQ 3
+#define FF_PROFILE_PRORES_4444 4
+#define FF_PROFILE_PRORES_XQ 5
+
+#define FF_PROFILE_ARIB_PROFILE_A 0
+#define FF_PROFILE_ARIB_PROFILE_C 1
+
+#define FF_PROFILE_KLVA_SYNC 0
+#define FF_PROFILE_KLVA_ASYNC 1
+
+ /**
+ * level
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int level;
+#define FF_LEVEL_UNKNOWN -99
+
+ /**
+ * Skip loop filtering for selected frames.
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ enum AVDiscard skip_loop_filter;
+
+ /**
+ * Skip IDCT/dequantization for selected frames.
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ enum AVDiscard skip_idct;
+
+ /**
+ * Skip decoding for selected frames.
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ enum AVDiscard skip_frame;
+
+ /**
+ * Header containing style information for text subtitles.
+ * For SUBTITLE_ASS subtitle type, it should contain the whole ASS
+ * [Script Info] and [V4+ Styles] section, plus the [Events] line and
+ * the Format line following. It shouldn't include any Dialogue line.
+ * - encoding: Set/allocated/freed by user (before avcodec_open2())
+ * - decoding: Set/allocated/freed by libavcodec (by avcodec_open2())
+ */
+ uint8_t *subtitle_header;
+ int subtitle_header_size;
+
+#if FF_API_VBV_DELAY
+ /**
+ * VBV delay coded in the last frame (in periods of a 27 MHz clock).
+ * Used for compliant TS muxing.
+ * - encoding: Set by libavcodec.
+ * - decoding: unused.
+ * @deprecated this value is now exported as a part of
+ * AV_PKT_DATA_CPB_PROPERTIES packet side data
+ */
+ attribute_deprecated
+ uint64_t vbv_delay;
+#endif
+
+#if FF_API_SIDEDATA_ONLY_PKT
+ /**
+ * Encoding only and set by default. Allow encoders to output packets
+ * that do not contain any encoded data, only side data.
+ *
+ * Some encoders need to output such packets, e.g. to update some stream
+ * parameters at the end of encoding.
+ *
+ * @deprecated this field disables the default behaviour and
+ * it is kept only for compatibility.
+ */
+ attribute_deprecated
+ int side_data_only_packets;
+#endif
+
+ /**
+ * Audio only. The number of "priming" samples (padding) inserted by the
+ * encoder at the beginning of the audio. I.e. this number of leading
+ * decoded samples must be discarded by the caller to get the original audio
+ * without leading padding.
+ *
+ * - decoding: unused
+ * - encoding: Set by libavcodec. The timestamps on the output packets are
+ * adjusted by the encoder so that they always refer to the
+ * first sample of the data actually contained in the packet,
+ * including any added padding. E.g. if the timebase is
+ * 1/samplerate and the timestamp of the first input sample is
+ * 0, the timestamp of the first output packet will be
+ * -initial_padding.
+ */
+ int initial_padding;
+
+ /**
+ * - decoding: For codecs that store a framerate value in the compressed
+ * bitstream, the decoder may export it here. { 0, 1} when
+ * unknown.
+ * - encoding: May be used to signal the framerate of CFR content to an
+ * encoder.
+ */
+ AVRational framerate;
+
+ /**
+ * Nominal unaccelerated pixel format, see AV_PIX_FMT_xxx.
+ * - encoding: unused.
+ * - decoding: Set by libavcodec before calling get_format()
+ */
+ enum AVPixelFormat sw_pix_fmt;
+
+ /**
+ * Timebase in which pkt_dts/pts and AVPacket.dts/pts are.
+ * - encoding unused.
+ * - decoding set by user.
+ */
+ AVRational pkt_timebase;
+
+ /**
+ * AVCodecDescriptor
+ * - encoding: unused.
+ * - decoding: set by libavcodec.
+ */
+ const AVCodecDescriptor *codec_descriptor;
+
+ /**
+ * Current statistics for PTS correction.
+ * - decoding: maintained and used by libavcodec, not intended to be used by user apps
+ * - encoding: unused
+ */
+ int64_t pts_correction_num_faulty_pts; /// Number of incorrect PTS values so far
+ int64_t pts_correction_num_faulty_dts; /// Number of incorrect DTS values so far
+ int64_t pts_correction_last_pts; /// PTS of the last frame
+ int64_t pts_correction_last_dts; /// DTS of the last frame
+
+ /**
+ * Character encoding of the input subtitles file.
+ * - decoding: set by user
+ * - encoding: unused
+ */
+ char *sub_charenc;
+
+ /**
+ * Subtitles character encoding mode. Formats or codecs might be adjusting
+ * this setting (if they are doing the conversion themselves for instance).
+ * - decoding: set by libavcodec
+ * - encoding: unused
+ */
+ int sub_charenc_mode;
+#define FF_SUB_CHARENC_MODE_DO_NOTHING -1 ///< do nothing (demuxer outputs a stream supposed to be already in UTF-8, or the codec is bitmap for instance)
+#define FF_SUB_CHARENC_MODE_AUTOMATIC 0 ///< libavcodec will select the mode itself
+#define FF_SUB_CHARENC_MODE_PRE_DECODER 1 ///< the AVPacket data needs to be recoded to UTF-8 before being fed to the decoder, requires iconv
+#define FF_SUB_CHARENC_MODE_IGNORE 2 ///< neither convert the subtitles, nor check them for valid UTF-8
+
+ /**
+ * Skip processing alpha if supported by codec.
+ * Note that if the format uses pre-multiplied alpha (common with VP6,
+ * and recommended due to better video quality/compression)
+ * the image will look as if alpha-blended onto a black background.
+ * However for formats that do not use pre-multiplied alpha
+ * there might be serious artefacts (though e.g. libswscale currently
+ * assumes pre-multiplied alpha anyway).
+ *
+ * - decoding: set by user
+ * - encoding: unused
+ */
+ int skip_alpha;
+
+ /**
+ * Number of samples to skip after a discontinuity
+ * - decoding: unused
+ * - encoding: set by libavcodec
+ */
+ int seek_preroll;
+
+#if FF_API_DEBUG_MV
+ /**
+ * @deprecated unused
+ */
+ attribute_deprecated
+ int debug_mv;
+#define FF_DEBUG_VIS_MV_P_FOR 0x00000001 //visualize forward predicted MVs of P frames
+#define FF_DEBUG_VIS_MV_B_FOR 0x00000002 //visualize forward predicted MVs of B frames
+#define FF_DEBUG_VIS_MV_B_BACK 0x00000004 //visualize backward predicted MVs of B frames
+#endif
+
+ /**
+ * custom intra quantization matrix
+ * - encoding: Set by user, can be NULL.
+ * - decoding: unused.
+ */
+ uint16_t *chroma_intra_matrix;
+
+ /**
+ * dump format separator.
+ * can be ", " or "\n " or anything else
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ uint8_t *dump_separator;
+
+ /**
+ * ',' separated list of allowed decoders.
+ * If NULL then all are allowed
+ * - encoding: unused
+ * - decoding: set by user
+ */
+ char *codec_whitelist;
+
+ /**
+ * Properties of the stream that gets decoded
+ * - encoding: unused
+ * - decoding: set by libavcodec
+ */
+ unsigned properties;
+#define FF_CODEC_PROPERTY_LOSSLESS 0x00000001
+#define FF_CODEC_PROPERTY_CLOSED_CAPTIONS 0x00000002
+
+ /**
+ * Additional data associated with the entire coded stream.
+ *
+ * - decoding: unused
+ * - encoding: may be set by libavcodec after avcodec_open2().
+ */
+ AVPacketSideData *coded_side_data;
+ int nb_coded_side_data;
+
+ /**
+ * A reference to the AVHWFramesContext describing the input (for encoding)
+ * or output (decoding) frames. The reference is set by the caller and
+ * afterwards owned (and freed) by libavcodec - it should never be read by
+ * the caller after being set.
+ *
+ * - decoding: This field should be set by the caller from the get_format()
+ * callback. The previous reference (if any) will always be
+ * unreffed by libavcodec before the get_format() call.
+ *
+ * If the default get_buffer2() is used with a hwaccel pixel
+ * format, then this AVHWFramesContext will be used for
+ * allocating the frame buffers.
+ *
+ * - encoding: For hardware encoders configured to use a hwaccel pixel
+ * format, this field should be set by the caller to a reference
+ * to the AVHWFramesContext describing input frames.
+ * AVHWFramesContext.format must be equal to
+ * AVCodecContext.pix_fmt.
+ *
+ * This field should be set before avcodec_open2() is called.
+ */
+ AVBufferRef *hw_frames_ctx;
+
+ /**
+ * Control the form of AVSubtitle.rects[N]->ass
+ * - decoding: set by user
+ * - encoding: unused
+ */
+ int sub_text_format;
+#define FF_SUB_TEXT_FMT_ASS 0
+#if FF_API_ASS_TIMING
+#define FF_SUB_TEXT_FMT_ASS_WITH_TIMINGS 1
+#endif
+
+ /**
+ * Audio only. The amount of padding (in samples) appended by the encoder to
+ * the end of the audio. I.e. this number of decoded samples must be
+ * discarded by the caller from the end of the stream to get the original
+ * audio without any trailing padding.
+ *
+ * - decoding: unused
+ * - encoding: unused
+ */
+ int trailing_padding;
+
+ /**
+ * The number of pixels per image to maximally accept.
+ *
+ * - decoding: set by user
+ * - encoding: set by user
+ */
+ int64_t max_pixels;
+
+ /**
+ * A reference to the AVHWDeviceContext describing the device which will
+ * be used by a hardware encoder/decoder. The reference is set by the
+ * caller and afterwards owned (and freed) by libavcodec.
+ *
+ * This should be used if either the codec device does not require
+ * hardware frames or any that are used are to be allocated internally by
+ * libavcodec. If the user wishes to supply any of the frames used as
+ * encoder input or decoder output then hw_frames_ctx should be used
+ * instead. When hw_frames_ctx is set in get_format() for a decoder, this
+ * field will be ignored while decoding the associated stream segment, but
+ * may again be used on a following one after another get_format() call.
+ *
+ * For both encoders and decoders this field should be set before
+ * avcodec_open2() is called and must not be written to thereafter.
+ *
+ * Note that some decoders may require this field to be set initially in
+ * order to support hw_frames_ctx at all - in that case, all frames
+ * contexts used must be created on the same device.
+ */
+ AVBufferRef *hw_device_ctx;
+
+ /**
+ * Bit set of AV_HWACCEL_FLAG_* flags, which affect hardware accelerated
+ * decoding (if active).
+ * - encoding: unused
+ * - decoding: Set by user (either before avcodec_open2(), or in the
+ * AVCodecContext.get_format callback)
+ */
+ int hwaccel_flags;
+
+ /**
+ * Video decoding only. Certain video codecs support cropping, meaning that
+ * only a sub-rectangle of the decoded frame is intended for display. This
+ * option controls how cropping is handled by libavcodec.
+ *
+ * When set to 1 (the default), libavcodec will apply cropping internally.
+ * I.e. it will modify the output frame width/height fields and offset the
+ * data pointers (only by as much as possible while preserving alignment, or
+ * by the full amount if the AV_CODEC_FLAG_UNALIGNED flag is set) so that
+ * the frames output by the decoder refer only to the cropped area. The
+ * crop_* fields of the output frames will be zero.
+ *
+ * When set to 0, the width/height fields of the output frames will be set
+ * to the coded dimensions and the crop_* fields will describe the cropping
+ * rectangle. Applying the cropping is left to the caller.
+ *
+ * @warning When hardware acceleration with opaque output frames is used,
+ * libavcodec is unable to apply cropping from the top/left border.
+ *
+ * @note when this option is set to zero, the width/height fields of the
+ * AVCodecContext and output AVFrames have different meanings. The codec
+ * context fields store display dimensions (with the coded dimensions in
+ * coded_width/height), while the frame fields store the coded dimensions
+ * (with the display dimensions being determined by the crop_* fields).
+ */
+ int apply_cropping;
+
+ /*
+ * Video decoding only. Sets the number of extra hardware frames which
+ * the decoder will allocate for use by the caller. This must be set
+ * before avcodec_open2() is called.
+ *
+ * Some hardware decoders require all frames that they will use for
+ * output to be defined in advance before decoding starts. For such
+ * decoders, the hardware frame pool must therefore be of a fixed size.
+ * The extra frames set here are on top of any number that the decoder
+ * needs internally in order to operate normally (for example, frames
+ * used as reference pictures).
+ */
+ int extra_hw_frames;
+
+ /**
+ * The percentage of damaged samples to discard a frame.
+ *
+ * - decoding: set by user
+ * - encoding: unused
+ */
+ int discard_damaged_percentage;
+
+ /**
+ * The number of samples per frame to maximally accept.
+ *
+ * - decoding: set by user
+ * - encoding: set by user
+ */
+ int64_t max_samples;
+
+ /**
+ * Bit set of AV_CODEC_EXPORT_DATA_* flags, which affects the kind of
+ * metadata exported in frame, packet, or coded stream side data by
+ * decoders and encoders.
+ *
+ * - decoding: set by user
+ * - encoding: set by user
+ */
+ int export_side_data;
+
+ /**
+ * This callback is called at the beginning of each packet to get a data
+ * buffer for it.
+ *
+ * The following field will be set in the packet before this callback is
+ * called:
+ * - size
+ * This callback must use the above value to calculate the required buffer size,
+ * which must padded by at least AV_INPUT_BUFFER_PADDING_SIZE bytes.
+ *
+ * This callback must fill the following fields in the packet:
+ * - data: alignment requirements for AVPacket apply, if any. Some architectures and
+ * encoders may benefit from having aligned data.
+ * - buf: must contain a pointer to an AVBufferRef structure. The packet's
+ * data pointer must be contained in it. See: av_buffer_create(), av_buffer_alloc(),
+ * and av_buffer_ref().
+ *
+ * If AV_CODEC_CAP_DR1 is not set then get_encode_buffer() must call
+ * avcodec_default_get_encode_buffer() instead of providing a buffer allocated by
+ * some other means.
+ *
+ * The flags field may contain a combination of AV_GET_ENCODE_BUFFER_FLAG_ flags.
+ * They may be used for example to hint what use the buffer may get after being
+ * created.
+ * Implementations of this callback may ignore flags they don't understand.
+ * If AV_GET_ENCODE_BUFFER_FLAG_REF is set in flags then the packet may be reused
+ * (read and/or written to if it is writable) later by libavcodec.
+ *
+ * This callback must be thread-safe, as when frame threading is used, it may
+ * be called from multiple threads simultaneously.
+ *
+ * @see avcodec_default_get_encode_buffer()
+ *
+ * - encoding: Set by libavcodec, user can override.
+ * - decoding: unused
+ */
+ int (*get_encode_buffer)(struct AVCodecContext *s, AVPacket *pkt, int flags);
+} AVCodecContext;
+
+#if FF_API_CODEC_GET_SET
+/**
+ * Accessors for some AVCodecContext fields. These used to be provided for ABI
+ * compatibility, and do not need to be used anymore.
+ */
+attribute_deprecated
+AVRational av_codec_get_pkt_timebase (const AVCodecContext *avctx);
+attribute_deprecated
+void av_codec_set_pkt_timebase (AVCodecContext *avctx, AVRational val);
+
+attribute_deprecated
+const AVCodecDescriptor *av_codec_get_codec_descriptor(const AVCodecContext *avctx);
+attribute_deprecated
+void av_codec_set_codec_descriptor(AVCodecContext *avctx, const AVCodecDescriptor *desc);
+
+attribute_deprecated
+unsigned av_codec_get_codec_properties(const AVCodecContext *avctx);
+
+attribute_deprecated
+int av_codec_get_lowres(const AVCodecContext *avctx);
+attribute_deprecated
+void av_codec_set_lowres(AVCodecContext *avctx, int val);
+
+attribute_deprecated
+int av_codec_get_seek_preroll(const AVCodecContext *avctx);
+attribute_deprecated
+void av_codec_set_seek_preroll(AVCodecContext *avctx, int val);
+
+attribute_deprecated
+uint16_t *av_codec_get_chroma_intra_matrix(const AVCodecContext *avctx);
+attribute_deprecated
+void av_codec_set_chroma_intra_matrix(AVCodecContext *avctx, uint16_t *val);
+#endif
+
+struct AVSubtitle;
+
+#if FF_API_CODEC_GET_SET
+attribute_deprecated
+int av_codec_get_max_lowres(const AVCodec *codec);
+#endif
+
+struct MpegEncContext;
+
+/**
+ * @defgroup lavc_hwaccel AVHWAccel
+ *
+ * @note Nothing in this structure should be accessed by the user. At some
+ * point in future it will not be externally visible at all.
+ *
+ * @{
+ */
+typedef struct AVHWAccel {
+ /**
+ * Name of the hardware accelerated codec.
+ * The name is globally unique among encoders and among decoders (but an
+ * encoder and a decoder can share the same name).
+ */
+ const char *name;
+
+ /**
+ * Type of codec implemented by the hardware accelerator.
+ *
+ * See AVMEDIA_TYPE_xxx
+ */
+ enum AVMediaType type;
+
+ /**
+ * Codec implemented by the hardware accelerator.
+ *
+ * See AV_CODEC_ID_xxx
+ */
+ enum AVCodecID id;
+
+ /**
+ * Supported pixel format.
+ *
+ * Only hardware accelerated formats are supported here.
+ */
+ enum AVPixelFormat pix_fmt;
+
+ /**
+ * Hardware accelerated codec capabilities.
+ * see AV_HWACCEL_CODEC_CAP_*
+ */
+ int capabilities;
+
+ /*****************************************************************
+ * No fields below this line are part of the public API. They
+ * may not be used outside of libavcodec and can be changed and
+ * removed at will.
+ * New public fields should be added right above.
+ *****************************************************************
+ */
+
+ /**
+ * Allocate a custom buffer
+ */
+ int (*alloc_frame)(AVCodecContext *avctx, AVFrame *frame);
+
+ /**
+ * Called at the beginning of each frame or field picture.
+ *
+ * Meaningful frame information (codec specific) is guaranteed to
+ * be parsed at this point. This function is mandatory.
+ *
+ * Note that buf can be NULL along with buf_size set to 0.
+ * Otherwise, this means the whole frame is available at this point.
+ *
+ * @param avctx the codec context
+ * @param buf the frame data buffer base
+ * @param buf_size the size of the frame in bytes
+ * @return zero if successful, a negative value otherwise
+ */
+ int (*start_frame)(AVCodecContext *avctx, const uint8_t *buf, uint32_t buf_size);
+
+ /**
+ * Callback for parameter data (SPS/PPS/VPS etc).
+ *
+ * Useful for hardware decoders which keep persistent state about the
+ * video parameters, and need to receive any changes to update that state.
+ *
+ * @param avctx the codec context
+ * @param type the nal unit type
+ * @param buf the nal unit data buffer
+ * @param buf_size the size of the nal unit in bytes
+ * @return zero if successful, a negative value otherwise
+ */
+ int (*decode_params)(AVCodecContext *avctx, int type, const uint8_t *buf, uint32_t buf_size);
+
+ /**
+ * Callback for each slice.
+ *
+ * Meaningful slice information (codec specific) is guaranteed to
+ * be parsed at this point. This function is mandatory.
+ * The only exception is XvMC, that works on MB level.
+ *
+ * @param avctx the codec context
+ * @param buf the slice data buffer base
+ * @param buf_size the size of the slice in bytes
+ * @return zero if successful, a negative value otherwise
+ */
+ int (*decode_slice)(AVCodecContext *avctx, const uint8_t *buf, uint32_t buf_size);
+
+ /**
+ * Called at the end of each frame or field picture.
+ *
+ * The whole picture is parsed at this point and can now be sent
+ * to the hardware accelerator. This function is mandatory.
+ *
+ * @param avctx the codec context
+ * @return zero if successful, a negative value otherwise
+ */
+ int (*end_frame)(AVCodecContext *avctx);
+
+ /**
+ * Size of per-frame hardware accelerator private data.
+ *
+ * Private data is allocated with av_mallocz() before
+ * AVCodecContext.get_buffer() and deallocated after
+ * AVCodecContext.release_buffer().
+ */
+ int frame_priv_data_size;
+
+ /**
+ * Called for every Macroblock in a slice.
+ *
+ * XvMC uses it to replace the ff_mpv_reconstruct_mb().
+ * Instead of decoding to raw picture, MB parameters are
+ * stored in an array provided by the video driver.
+ *
+ * @param s the mpeg context
+ */
+ void (*decode_mb)(struct MpegEncContext *s);
+
+ /**
+ * Initialize the hwaccel private data.
+ *
+ * This will be called from ff_get_format(), after hwaccel and
+ * hwaccel_context are set and the hwaccel private data in AVCodecInternal
+ * is allocated.
+ */
+ int (*init)(AVCodecContext *avctx);
+
+ /**
+ * Uninitialize the hwaccel private data.
+ *
+ * This will be called from get_format() or avcodec_close(), after hwaccel
+ * and hwaccel_context are already uninitialized.
+ */
+ int (*uninit)(AVCodecContext *avctx);
+
+ /**
+ * Size of the private data to allocate in
+ * AVCodecInternal.hwaccel_priv_data.
+ */
+ int priv_data_size;
+
+ /**
+ * Internal hwaccel capabilities.
+ */
+ int caps_internal;
+
+ /**
+ * Fill the given hw_frames context with current codec parameters. Called
+ * from get_format. Refer to avcodec_get_hw_frames_parameters() for
+ * details.
+ *
+ * This CAN be called before AVHWAccel.init is called, and you must assume
+ * that avctx->hwaccel_priv_data is invalid.
+ */
+ int (*frame_params)(AVCodecContext *avctx, AVBufferRef *hw_frames_ctx);
+} AVHWAccel;
+
+/**
+ * HWAccel is experimental and is thus avoided in favor of non experimental
+ * codecs
+ */
+#define AV_HWACCEL_CODEC_CAP_EXPERIMENTAL 0x0200
+
+/**
+ * Hardware acceleration should be used for decoding even if the codec level
+ * used is unknown or higher than the maximum supported level reported by the
+ * hardware driver.
+ *
+ * It's generally a good idea to pass this flag unless you have a specific
+ * reason not to, as hardware tends to under-report supported levels.
+ */
+#define AV_HWACCEL_FLAG_IGNORE_LEVEL (1 << 0)
+
+/**
+ * Hardware acceleration can output YUV pixel formats with a different chroma
+ * sampling than 4:2:0 and/or other than 8 bits per component.
+ */
+#define AV_HWACCEL_FLAG_ALLOW_HIGH_DEPTH (1 << 1)
+
+/**
+ * Hardware acceleration should still be attempted for decoding when the
+ * codec profile does not match the reported capabilities of the hardware.
+ *
+ * For example, this can be used to try to decode baseline profile H.264
+ * streams in hardware - it will often succeed, because many streams marked
+ * as baseline profile actually conform to constrained baseline profile.
+ *
+ * @warning If the stream is actually not supported then the behaviour is
+ * undefined, and may include returning entirely incorrect output
+ * while indicating success.
+ */
+#define AV_HWACCEL_FLAG_ALLOW_PROFILE_MISMATCH (1 << 2)
+
+/**
+ * @}
+ */
+
+#if FF_API_AVPICTURE
+/**
+ * @defgroup lavc_picture AVPicture
+ *
+ * Functions for working with AVPicture
+ * @{
+ */
+
+/**
+ * Picture data structure.
+ *
+ * Up to four components can be stored into it, the last component is
+ * alpha.
+ * @deprecated use AVFrame or imgutils functions instead
+ */
+typedef struct AVPicture {
+ attribute_deprecated
+ uint8_t *data[AV_NUM_DATA_POINTERS]; ///< pointers to the image data planes
+ attribute_deprecated
+ int linesize[AV_NUM_DATA_POINTERS]; ///< number of bytes per line
+} AVPicture;
+
+/**
+ * @}
+ */
+#endif
+
+enum AVSubtitleType {
+ SUBTITLE_NONE,
+
+ SUBTITLE_BITMAP, ///< A bitmap, pict will be set
+
+ /**
+ * Plain text, the text field must be set by the decoder and is
+ * authoritative. ass and pict fields may contain approximations.
+ */
+ SUBTITLE_TEXT,
+
+ /**
+ * Formatted text, the ass field must be set by the decoder and is
+ * authoritative. pict and text fields may contain approximations.
+ */
+ SUBTITLE_ASS,
+};
+
+#define AV_SUBTITLE_FLAG_FORCED 0x00000001
+
+typedef struct AVSubtitleRect {
+ int x; ///< top left corner of pict, undefined when pict is not set
+ int y; ///< top left corner of pict, undefined when pict is not set
+ int w; ///< width of pict, undefined when pict is not set
+ int h; ///< height of pict, undefined when pict is not set
+ int nb_colors; ///< number of colors in pict, undefined when pict is not set
+
+#if FF_API_AVPICTURE
+ /**
+ * @deprecated unused
+ */
+ attribute_deprecated
+ AVPicture pict;
+#endif
+ /**
+ * data+linesize for the bitmap of this subtitle.
+ * Can be set for text/ass as well once they are rendered.
+ */
+ uint8_t *data[4];
+ int linesize[4];
+
+ enum AVSubtitleType type;
+
+ char *text; ///< 0 terminated plain UTF-8 text
+
+ /**
+ * 0 terminated ASS/SSA compatible event line.
+ * The presentation of this is unaffected by the other values in this
+ * struct.
+ */
+ char *ass;
+
+ int flags;
+} AVSubtitleRect;
+
+typedef struct AVSubtitle {
+ uint16_t format; /* 0 = graphics */
+ uint32_t start_display_time; /* relative to packet pts, in ms */
+ uint32_t end_display_time; /* relative to packet pts, in ms */
+ unsigned num_rects;
+ AVSubtitleRect **rects;
+ int64_t pts; ///< Same as packet pts, in AV_TIME_BASE
+} AVSubtitle;
+
+#if FF_API_NEXT
+/**
+ * If c is NULL, returns the first registered codec,
+ * if c is non-NULL, returns the next registered codec after c,
+ * or NULL if c is the last one.
+ */
+attribute_deprecated
+AVCodec *av_codec_next(const AVCodec *c);
+#endif
+
+/**
+ * Return the LIBAVCODEC_VERSION_INT constant.
+ */
+unsigned avcodec_version(void);
+
+/**
+ * Return the libavcodec build-time configuration.
+ */
+const char *avcodec_configuration(void);
+
+/**
+ * Return the libavcodec license.
+ */
+const char *avcodec_license(void);
+
+#if FF_API_NEXT
+/**
+ * @deprecated Calling this function is unnecessary.
+ */
+attribute_deprecated
+void avcodec_register(AVCodec *codec);
+
+/**
+ * @deprecated Calling this function is unnecessary.
+ */
+attribute_deprecated
+void avcodec_register_all(void);
+#endif
+
+/**
+ * Allocate an AVCodecContext and set its fields to default values. The
+ * resulting struct should be freed with avcodec_free_context().
+ *
+ * @param codec if non-NULL, allocate private data and initialize defaults
+ * for the given codec. It is illegal to then call avcodec_open2()
+ * with a different codec.
+ * If NULL, then the codec-specific defaults won't be initialized,
+ * which may result in suboptimal default settings (this is
+ * important mainly for encoders, e.g. libx264).
+ *
+ * @return An AVCodecContext filled with default values or NULL on failure.
+ */
+AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);
+
+/**
+ * Free the codec context and everything associated with it and write NULL to
+ * the provided pointer.
+ */
+void avcodec_free_context(AVCodecContext **avctx);
+
+#if FF_API_GET_CONTEXT_DEFAULTS
+/**
+ * @deprecated This function should not be used, as closing and opening a codec
+ * context multiple time is not supported. A new codec context should be
+ * allocated for each new use.
+ */
+int avcodec_get_context_defaults3(AVCodecContext *s, const AVCodec *codec);
+#endif
+
+/**
+ * Get the AVClass for AVCodecContext. It can be used in combination with
+ * AV_OPT_SEARCH_FAKE_OBJ for examining options.
+ *
+ * @see av_opt_find().
+ */
+const AVClass *avcodec_get_class(void);
+
+#if FF_API_GET_FRAME_CLASS
+/**
+ * @deprecated This function should not be used.
+ */
+attribute_deprecated
+const AVClass *avcodec_get_frame_class(void);
+#endif
+
+/**
+ * Get the AVClass for AVSubtitleRect. It can be used in combination with
+ * AV_OPT_SEARCH_FAKE_OBJ for examining options.
+ *
+ * @see av_opt_find().
+ */
+const AVClass *avcodec_get_subtitle_rect_class(void);
+
+#if FF_API_COPY_CONTEXT
+/**
+ * Copy the settings of the source AVCodecContext into the destination
+ * AVCodecContext. The resulting destination codec context will be
+ * unopened, i.e. you are required to call avcodec_open2() before you
+ * can use this AVCodecContext to decode/encode video/audio data.
+ *
+ * @param dest target codec context, should be initialized with
+ * avcodec_alloc_context3(NULL), but otherwise uninitialized
+ * @param src source codec context
+ * @return AVERROR() on error (e.g. memory allocation error), 0 on success
+ *
+ * @deprecated The semantics of this function are ill-defined and it should not
+ * be used. If you need to transfer the stream parameters from one codec context
+ * to another, use an intermediate AVCodecParameters instance and the
+ * avcodec_parameters_from_context() / avcodec_parameters_to_context()
+ * functions.
+ */
+attribute_deprecated
+int avcodec_copy_context(AVCodecContext *dest, const AVCodecContext *src);
+#endif
+
+/**
+ * Fill the parameters struct based on the values from the supplied codec
+ * context. Any allocated fields in par are freed and replaced with duplicates
+ * of the corresponding fields in codec.
+ *
+ * @return >= 0 on success, a negative AVERROR code on failure
+ */
+int avcodec_parameters_from_context(AVCodecParameters *par,
+ const AVCodecContext *codec);
+
+/**
+ * Fill the codec context based on the values from the supplied codec
+ * parameters. Any allocated fields in codec that have a corresponding field in
+ * par are freed and replaced with duplicates of the corresponding field in par.
+ * Fields in codec that do not have a counterpart in par are not touched.
+ *
+ * @return >= 0 on success, a negative AVERROR code on failure.
+ */
+int avcodec_parameters_to_context(AVCodecContext *codec,
+ const AVCodecParameters *par);
+
+/**
+ * Initialize the AVCodecContext to use the given AVCodec. Prior to using this
+ * function the context has to be allocated with avcodec_alloc_context3().
+ *
+ * The functions avcodec_find_decoder_by_name(), avcodec_find_encoder_by_name(),
+ * avcodec_find_decoder() and avcodec_find_encoder() provide an easy way for
+ * retrieving a codec.
+ *
+ * @warning This function is not thread safe!
+ *
+ * @note Always call this function before using decoding routines (such as
+ * @ref avcodec_receive_frame()).
+ *
+ * @code
+ * av_dict_set(&opts, "b", "2.5M", 0);
+ * codec = avcodec_find_decoder(AV_CODEC_ID_H264);
+ * if (!codec)
+ * exit(1);
+ *
+ * context = avcodec_alloc_context3(codec);
+ *
+ * if (avcodec_open2(context, codec, opts) < 0)
+ * exit(1);
+ * @endcode
+ *
+ * @param avctx The context to initialize.
+ * @param codec The codec to open this context for. If a non-NULL codec has been
+ * previously passed to avcodec_alloc_context3() or
+ * for this context, then this parameter MUST be either NULL or
+ * equal to the previously passed codec.
+ * @param options A dictionary filled with AVCodecContext and codec-private options.
+ * On return this object will be filled with options that were not found.
+ *
+ * @return zero on success, a negative value on error
+ * @see avcodec_alloc_context3(), avcodec_find_decoder(), avcodec_find_encoder(),
+ * av_dict_set(), av_opt_find().
+ */
+int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);
+
+/**
+ * Close a given AVCodecContext and free all the data associated with it
+ * (but not the AVCodecContext itself).
+ *
+ * Calling this function on an AVCodecContext that hasn't been opened will free
+ * the codec-specific data allocated in avcodec_alloc_context3() with a non-NULL
+ * codec. Subsequent calls will do nothing.
+ *
+ * @note Do not use this function. Use avcodec_free_context() to destroy a
+ * codec context (either open or closed). Opening and closing a codec context
+ * multiple times is not supported anymore -- use multiple codec contexts
+ * instead.
+ */
+int avcodec_close(AVCodecContext *avctx);
+
+/**
+ * Free all allocated data in the given subtitle struct.
+ *
+ * @param sub AVSubtitle to free.
+ */
+void avsubtitle_free(AVSubtitle *sub);
+
+/**
+ * @}
+ */
+
+/**
+ * @addtogroup lavc_decoding
+ * @{
+ */
+
+/**
+ * The default callback for AVCodecContext.get_buffer2(). It is made public so
+ * it can be called by custom get_buffer2() implementations for decoders without
+ * AV_CODEC_CAP_DR1 set.
+ */
+int avcodec_default_get_buffer2(AVCodecContext *s, AVFrame *frame, int flags);
+
+/**
+ * The default callback for AVCodecContext.get_encode_buffer(). It is made public so
+ * it can be called by custom get_encode_buffer() implementations for encoders without
+ * AV_CODEC_CAP_DR1 set.
+ */
+int avcodec_default_get_encode_buffer(AVCodecContext *s, AVPacket *pkt, int flags);
+
+/**
+ * Modify width and height values so that they will result in a memory
+ * buffer that is acceptable for the codec if you do not use any horizontal
+ * padding.
+ *
+ * May only be used if a codec with AV_CODEC_CAP_DR1 has been opened.
+ */
+void avcodec_align_dimensions(AVCodecContext *s, int *width, int *height);
+
+/**
+ * Modify width and height values so that they will result in a memory
+ * buffer that is acceptable for the codec if you also ensure that all
+ * line sizes are a multiple of the respective linesize_align[i].
+ *
+ * May only be used if a codec with AV_CODEC_CAP_DR1 has been opened.
+ */
+void avcodec_align_dimensions2(AVCodecContext *s, int *width, int *height,
+ int linesize_align[AV_NUM_DATA_POINTERS]);
+
+/**
+ * Converts AVChromaLocation to swscale x/y chroma position.
+ *
+ * The positions represent the chroma (0,0) position in a coordinates system
+ * with luma (0,0) representing the origin and luma(1,1) representing 256,256
+ *
+ * @param xpos horizontal chroma sample position
+ * @param ypos vertical chroma sample position
+ */
+int avcodec_enum_to_chroma_pos(int *xpos, int *ypos, enum AVChromaLocation pos);
+
+/**
+ * Converts swscale x/y chroma position to AVChromaLocation.
+ *
+ * The positions represent the chroma (0,0) position in a coordinates system
+ * with luma (0,0) representing the origin and luma(1,1) representing 256,256
+ *
+ * @param xpos horizontal chroma sample position
+ * @param ypos vertical chroma sample position
+ */
+enum AVChromaLocation avcodec_chroma_pos_to_enum(int xpos, int ypos);
+
+#if FF_API_OLD_ENCDEC
+/**
+ * Decode the audio frame of size avpkt->size from avpkt->data into frame.
+ *
+ * Some decoders may support multiple frames in a single AVPacket. Such
+ * decoders would then just decode the first frame and the return value would be
+ * less than the packet size. In this case, avcodec_decode_audio4 has to be
+ * called again with an AVPacket containing the remaining data in order to
+ * decode the second frame, etc... Even if no frames are returned, the packet
+ * needs to be fed to the decoder with remaining data until it is completely
+ * consumed or an error occurs.
+ *
+ * Some decoders (those marked with AV_CODEC_CAP_DELAY) have a delay between input
+ * and output. This means that for some packets they will not immediately
+ * produce decoded output and need to be flushed at the end of decoding to get
+ * all the decoded data. Flushing is done by calling this function with packets
+ * with avpkt->data set to NULL and avpkt->size set to 0 until it stops
+ * returning samples. It is safe to flush even those decoders that are not
+ * marked with AV_CODEC_CAP_DELAY, then no samples will be returned.
+ *
+ * @warning The input buffer, avpkt->data must be AV_INPUT_BUFFER_PADDING_SIZE
+ * larger than the actual read bytes because some optimized bitstream
+ * readers read 32 or 64 bits at once and could read over the end.
+ *
+ * @note The AVCodecContext MUST have been opened with @ref avcodec_open2()
+ * before packets may be fed to the decoder.
+ *
+ * @param avctx the codec context
+ * @param[out] frame The AVFrame in which to store decoded audio samples.
+ * The decoder will allocate a buffer for the decoded frame by
+ * calling the AVCodecContext.get_buffer2() callback.
+ * When AVCodecContext.refcounted_frames is set to 1, the frame is
+ * reference counted and the returned reference belongs to the
+ * caller. The caller must release the frame using av_frame_unref()
+ * when the frame is no longer needed. The caller may safely write
+ * to the frame if av_frame_is_writable() returns 1.
+ * When AVCodecContext.refcounted_frames is set to 0, the returned
+ * reference belongs to the decoder and is valid only until the
+ * next call to this function or until closing or flushing the
+ * decoder. The caller may not write to it.
+ * @param[out] got_frame_ptr Zero if no frame could be decoded, otherwise it is
+ * non-zero. Note that this field being set to zero
+ * does not mean that an error has occurred. For
+ * decoders with AV_CODEC_CAP_DELAY set, no given decode
+ * call is guaranteed to produce a frame.
+ * @param[in] avpkt The input AVPacket containing the input buffer.
+ * At least avpkt->data and avpkt->size should be set. Some
+ * decoders might also require additional fields to be set.
+ * @return A negative error code is returned if an error occurred during
+ * decoding, otherwise the number of bytes consumed from the input
+ * AVPacket is returned.
+ *
+* @deprecated Use avcodec_send_packet() and avcodec_receive_frame().
+ */
+attribute_deprecated
+int avcodec_decode_audio4(AVCodecContext *avctx, AVFrame *frame,
+ int *got_frame_ptr, const AVPacket *avpkt);
+
+/**
+ * Decode the video frame of size avpkt->size from avpkt->data into picture.
+ * Some decoders may support multiple frames in a single AVPacket, such
+ * decoders would then just decode the first frame.
+ *
+ * @warning The input buffer must be AV_INPUT_BUFFER_PADDING_SIZE larger than
+ * the actual read bytes because some optimized bitstream readers read 32 or 64
+ * bits at once and could read over the end.
+ *
+ * @warning The end of the input buffer buf should be set to 0 to ensure that
+ * no overreading happens for damaged MPEG streams.
+ *
+ * @note Codecs which have the AV_CODEC_CAP_DELAY capability set have a delay
+ * between input and output, these need to be fed with avpkt->data=NULL,
+ * avpkt->size=0 at the end to return the remaining frames.
+ *
+ * @note The AVCodecContext MUST have been opened with @ref avcodec_open2()
+ * before packets may be fed to the decoder.
+ *
+ * @param avctx the codec context
+ * @param[out] picture The AVFrame in which the decoded video frame will be stored.
+ * Use av_frame_alloc() to get an AVFrame. The codec will
+ * allocate memory for the actual bitmap by calling the
+ * AVCodecContext.get_buffer2() callback.
+ * When AVCodecContext.refcounted_frames is set to 1, the frame is
+ * reference counted and the returned reference belongs to the
+ * caller. The caller must release the frame using av_frame_unref()
+ * when the frame is no longer needed. The caller may safely write
+ * to the frame if av_frame_is_writable() returns 1.
+ * When AVCodecContext.refcounted_frames is set to 0, the returned
+ * reference belongs to the decoder and is valid only until the
+ * next call to this function or until closing or flushing the
+ * decoder. The caller may not write to it.
+ *
+ * @param[in] avpkt The input AVPacket containing the input buffer.
+ * You can create such packet with av_init_packet() and by then setting
+ * data and size, some decoders might in addition need other fields like
+ * flags&AV_PKT_FLAG_KEY. All decoders are designed to use the least
+ * fields possible.
+ * @param[in,out] got_picture_ptr Zero if no frame could be decompressed, otherwise, it is nonzero.
+ * @return On error a negative value is returned, otherwise the number of bytes
+ * used or zero if no frame could be decompressed.
+ *
+ * @deprecated Use avcodec_send_packet() and avcodec_receive_frame().
+ */
+attribute_deprecated
+int avcodec_decode_video2(AVCodecContext *avctx, AVFrame *picture,
+ int *got_picture_ptr,
+ const AVPacket *avpkt);
+#endif
+
+/**
+ * Decode a subtitle message.
+ * Return a negative value on error, otherwise return the number of bytes used.
+ * If no subtitle could be decompressed, got_sub_ptr is zero.
+ * Otherwise, the subtitle is stored in *sub.
+ * Note that AV_CODEC_CAP_DR1 is not available for subtitle codecs. This is for
+ * simplicity, because the performance difference is expected to be negligible
+ * and reusing a get_buffer written for video codecs would probably perform badly
+ * due to a potentially very different allocation pattern.
+ *
+ * Some decoders (those marked with AV_CODEC_CAP_DELAY) have a delay between input
+ * and output. This means that for some packets they will not immediately
+ * produce decoded output and need to be flushed at the end of decoding to get
+ * all the decoded data. Flushing is done by calling this function with packets
+ * with avpkt->data set to NULL and avpkt->size set to 0 until it stops
+ * returning subtitles. It is safe to flush even those decoders that are not
+ * marked with AV_CODEC_CAP_DELAY, then no subtitles will be returned.
+ *
+ * @note The AVCodecContext MUST have been opened with @ref avcodec_open2()
+ * before packets may be fed to the decoder.
+ *
+ * @param avctx the codec context
+ * @param[out] sub The preallocated AVSubtitle in which the decoded subtitle will be stored,
+ * must be freed with avsubtitle_free if *got_sub_ptr is set.
+ * @param[in,out] got_sub_ptr Zero if no subtitle could be decompressed, otherwise, it is nonzero.
+ * @param[in] avpkt The input AVPacket containing the input buffer.
+ */
+int avcodec_decode_subtitle2(AVCodecContext *avctx, AVSubtitle *sub,
+ int *got_sub_ptr,
+ AVPacket *avpkt);
+
+/**
+ * Supply raw packet data as input to a decoder.
+ *
+ * Internally, this call will copy relevant AVCodecContext fields, which can
+ * influence decoding per-packet, and apply them when the packet is actually
+ * decoded. (For example AVCodecContext.skip_frame, which might direct the
+ * decoder to drop the frame contained by the packet sent with this function.)
+ *
+ * @warning The input buffer, avpkt->data must be AV_INPUT_BUFFER_PADDING_SIZE
+ * larger than the actual read bytes because some optimized bitstream
+ * readers read 32 or 64 bits at once and could read over the end.
+ *
+ * @warning Do not mix this API with the legacy API (like avcodec_decode_video2())
+ * on the same AVCodecContext. It will return unexpected results now
+ * or in future libavcodec versions.
+ *
+ * @note The AVCodecContext MUST have been opened with @ref avcodec_open2()
+ * before packets may be fed to the decoder.
+ *
+ * @param avctx codec context
+ * @param[in] avpkt The input AVPacket. Usually, this will be a single video
+ * frame, or several complete audio frames.
+ * Ownership of the packet remains with the caller, and the
+ * decoder will not write to the packet. The decoder may create
+ * a reference to the packet data (or copy it if the packet is
+ * not reference-counted).
+ * Unlike with older APIs, the packet is always fully consumed,
+ * and if it contains multiple frames (e.g. some audio codecs),
+ * will require you to call avcodec_receive_frame() multiple
+ * times afterwards before you can send a new packet.
+ * It can be NULL (or an AVPacket with data set to NULL and
+ * size set to 0); in this case, it is considered a flush
+ * packet, which signals the end of the stream. Sending the
+ * first flush packet will return success. Subsequent ones are
+ * unnecessary and will return AVERROR_EOF. If the decoder
+ * still has frames buffered, it will return them after sending
+ * a flush packet.
+ *
+ * @return 0 on success, otherwise negative error code:
+ * AVERROR(EAGAIN): input is not accepted in the current state - user
+ * must read output with avcodec_receive_frame() (once
+ * all output is read, the packet should be resent, and
+ * the call will not fail with EAGAIN).
+ * AVERROR_EOF: the decoder has been flushed, and no new packets can
+ * be sent to it (also returned if more than 1 flush
+ * packet is sent)
+ * AVERROR(EINVAL): codec not opened, it is an encoder, or requires flush
+ * AVERROR(ENOMEM): failed to add packet to internal queue, or similar
+ * other errors: legitimate decoding errors
+ */
+int avcodec_send_packet(AVCodecContext *avctx, const AVPacket *avpkt);
+
+/**
+ * Return decoded output data from a decoder.
+ *
+ * @param avctx codec context
+ * @param frame This will be set to a reference-counted video or audio
+ * frame (depending on the decoder type) allocated by the
+ * decoder. Note that the function will always call
+ * av_frame_unref(frame) before doing anything else.
+ *
+ * @return
+ * 0: success, a frame was returned
+ * AVERROR(EAGAIN): output is not available in this state - user must try
+ * to send new input
+ * AVERROR_EOF: the decoder has been fully flushed, and there will be
+ * no more output frames
+ * AVERROR(EINVAL): codec not opened, or it is an encoder
+ * AVERROR_INPUT_CHANGED: current decoded frame has changed parameters
+ * with respect to first decoded frame. Applicable
+ * when flag AV_CODEC_FLAG_DROPCHANGED is set.
+ * other negative values: legitimate decoding errors
+ */
+int avcodec_receive_frame(AVCodecContext *avctx, AVFrame *frame);
+
+/**
+ * Supply a raw video or audio frame to the encoder. Use avcodec_receive_packet()
+ * to retrieve buffered output packets.
+ *
+ * @param avctx codec context
+ * @param[in] frame AVFrame containing the raw audio or video frame to be encoded.
+ * Ownership of the frame remains with the caller, and the
+ * encoder will not write to the frame. The encoder may create
+ * a reference to the frame data (or copy it if the frame is
+ * not reference-counted).
+ * It can be NULL, in which case it is considered a flush
+ * packet. This signals the end of the stream. If the encoder
+ * still has packets buffered, it will return them after this
+ * call. Once flushing mode has been entered, additional flush
+ * packets are ignored, and sending frames will return
+ * AVERROR_EOF.
+ *
+ * For audio:
+ * If AV_CODEC_CAP_VARIABLE_FRAME_SIZE is set, then each frame
+ * can have any number of samples.
+ * If it is not set, frame->nb_samples must be equal to
+ * avctx->frame_size for all frames except the last.
+ * The final frame may be smaller than avctx->frame_size.
+ * @return 0 on success, otherwise negative error code:
+ * AVERROR(EAGAIN): input is not accepted in the current state - user
+ * must read output with avcodec_receive_packet() (once
+ * all output is read, the packet should be resent, and
+ * the call will not fail with EAGAIN).
+ * AVERROR_EOF: the encoder has been flushed, and no new frames can
+ * be sent to it
+ * AVERROR(EINVAL): codec not opened, refcounted_frames not set, it is a
+ * decoder, or requires flush
+ * AVERROR(ENOMEM): failed to add packet to internal queue, or similar
+ * other errors: legitimate encoding errors
+ */
+int avcodec_send_frame(AVCodecContext *avctx, const AVFrame *frame);
+
+/**
+ * Read encoded data from the encoder.
+ *
+ * @param avctx codec context
+ * @param avpkt This will be set to a reference-counted packet allocated by the
+ * encoder. Note that the function will always call
+ * av_packet_unref(avpkt) before doing anything else.
+ * @return 0 on success, otherwise negative error code:
+ * AVERROR(EAGAIN): output is not available in the current state - user
+ * must try to send input
+ * AVERROR_EOF: the encoder has been fully flushed, and there will be
+ * no more output packets
+ * AVERROR(EINVAL): codec not opened, or it is a decoder
+ * other errors: legitimate encoding errors
+ */
+int avcodec_receive_packet(AVCodecContext *avctx, AVPacket *avpkt);
+
+/**
+ * Create and return a AVHWFramesContext with values adequate for hardware
+ * decoding. This is meant to get called from the get_format callback, and is
+ * a helper for preparing a AVHWFramesContext for AVCodecContext.hw_frames_ctx.
+ * This API is for decoding with certain hardware acceleration modes/APIs only.
+ *
+ * The returned AVHWFramesContext is not initialized. The caller must do this
+ * with av_hwframe_ctx_init().
+ *
+ * Calling this function is not a requirement, but makes it simpler to avoid
+ * codec or hardware API specific details when manually allocating frames.
+ *
+ * Alternatively to this, an API user can set AVCodecContext.hw_device_ctx,
+ * which sets up AVCodecContext.hw_frames_ctx fully automatically, and makes
+ * it unnecessary to call this function or having to care about
+ * AVHWFramesContext initialization at all.
+ *
+ * There are a number of requirements for calling this function:
+ *
+ * - It must be called from get_format with the same avctx parameter that was
+ * passed to get_format. Calling it outside of get_format is not allowed, and
+ * can trigger undefined behavior.
+ * - The function is not always supported (see description of return values).
+ * Even if this function returns successfully, hwaccel initialization could
+ * fail later. (The degree to which implementations check whether the stream
+ * is actually supported varies. Some do this check only after the user's
+ * get_format callback returns.)
+ * - The hw_pix_fmt must be one of the choices suggested by get_format. If the
+ * user decides to use a AVHWFramesContext prepared with this API function,
+ * the user must return the same hw_pix_fmt from get_format.
+ * - The device_ref passed to this function must support the given hw_pix_fmt.
+ * - After calling this API function, it is the user's responsibility to
+ * initialize the AVHWFramesContext (returned by the out_frames_ref parameter),
+ * and to set AVCodecContext.hw_frames_ctx to it. If done, this must be done
+ * before returning from get_format (this is implied by the normal
+ * AVCodecContext.hw_frames_ctx API rules).
+ * - The AVHWFramesContext parameters may change every time time get_format is
+ * called. Also, AVCodecContext.hw_frames_ctx is reset before get_format. So
+ * you are inherently required to go through this process again on every
+ * get_format call.
+ * - It is perfectly possible to call this function without actually using
+ * the resulting AVHWFramesContext. One use-case might be trying to reuse a
+ * previously initialized AVHWFramesContext, and calling this API function
+ * only to test whether the required frame parameters have changed.
+ * - Fields that use dynamically allocated values of any kind must not be set
+ * by the user unless setting them is explicitly allowed by the documentation.
+ * If the user sets AVHWFramesContext.free and AVHWFramesContext.user_opaque,
+ * the new free callback must call the potentially set previous free callback.
+ * This API call may set any dynamically allocated fields, including the free
+ * callback.
+ *
+ * The function will set at least the following fields on AVHWFramesContext
+ * (potentially more, depending on hwaccel API):
+ *
+ * - All fields set by av_hwframe_ctx_alloc().
+ * - Set the format field to hw_pix_fmt.
+ * - Set the sw_format field to the most suited and most versatile format. (An
+ * implication is that this will prefer generic formats over opaque formats
+ * with arbitrary restrictions, if possible.)
+ * - Set the width/height fields to the coded frame size, rounded up to the
+ * API-specific minimum alignment.
+ * - Only _if_ the hwaccel requires a pre-allocated pool: set the initial_pool_size
+ * field to the number of maximum reference surfaces possible with the codec,
+ * plus 1 surface for the user to work (meaning the user can safely reference
+ * at most 1 decoded surface at a time), plus additional buffering introduced
+ * by frame threading. If the hwaccel does not require pre-allocation, the
+ * field is left to 0, and the decoder will allocate new surfaces on demand
+ * during decoding.
+ * - Possibly AVHWFramesContext.hwctx fields, depending on the underlying
+ * hardware API.
+ *
+ * Essentially, out_frames_ref returns the same as av_hwframe_ctx_alloc(), but
+ * with basic frame parameters set.
+ *
+ * The function is stateless, and does not change the AVCodecContext or the
+ * device_ref AVHWDeviceContext.
+ *
+ * @param avctx The context which is currently calling get_format, and which
+ * implicitly contains all state needed for filling the returned
+ * AVHWFramesContext properly.
+ * @param device_ref A reference to the AVHWDeviceContext describing the device
+ * which will be used by the hardware decoder.
+ * @param hw_pix_fmt The hwaccel format you are going to return from get_format.
+ * @param out_frames_ref On success, set to a reference to an _uninitialized_
+ * AVHWFramesContext, created from the given device_ref.
+ * Fields will be set to values required for decoding.
+ * Not changed if an error is returned.
+ * @return zero on success, a negative value on error. The following error codes
+ * have special semantics:
+ * AVERROR(ENOENT): the decoder does not support this functionality. Setup
+ * is always manual, or it is a decoder which does not
+ * support setting AVCodecContext.hw_frames_ctx at all,
+ * or it is a software format.
+ * AVERROR(EINVAL): it is known that hardware decoding is not supported for
+ * this configuration, or the device_ref is not supported
+ * for the hwaccel referenced by hw_pix_fmt.
+ */
+int avcodec_get_hw_frames_parameters(AVCodecContext *avctx,
+ AVBufferRef *device_ref,
+ enum AVPixelFormat hw_pix_fmt,
+ AVBufferRef **out_frames_ref);
+
+
+
+/**
+ * @defgroup lavc_parsing Frame parsing
+ * @{
+ */
+
+enum AVPictureStructure {
+ AV_PICTURE_STRUCTURE_UNKNOWN, //< unknown
+ AV_PICTURE_STRUCTURE_TOP_FIELD, //< coded as top field
+ AV_PICTURE_STRUCTURE_BOTTOM_FIELD, //< coded as bottom field
+ AV_PICTURE_STRUCTURE_FRAME, //< coded as frame
+};
+
+typedef struct AVCodecParserContext {
+ void *priv_data;
+ struct AVCodecParser *parser;
+ int64_t frame_offset; /* offset of the current frame */
+ int64_t cur_offset; /* current offset
+ (incremented by each av_parser_parse()) */
+ int64_t next_frame_offset; /* offset of the next frame */
+ /* video info */
+ int pict_type; /* XXX: Put it back in AVCodecContext. */
+ /**
+ * This field is used for proper frame duration computation in lavf.
+ * It signals, how much longer the frame duration of the current frame
+ * is compared to normal frame duration.
+ *
+ * frame_duration = (1 + repeat_pict) * time_base
+ *
+ * It is used by codecs like H.264 to display telecined material.
+ */
+ int repeat_pict; /* XXX: Put it back in AVCodecContext. */
+ int64_t pts; /* pts of the current frame */
+ int64_t dts; /* dts of the current frame */
+
+ /* private data */
+ int64_t last_pts;
+ int64_t last_dts;
+ int fetch_timestamp;
+
+#define AV_PARSER_PTS_NB 4
+ int cur_frame_start_index;
+ int64_t cur_frame_offset[AV_PARSER_PTS_NB];
+ int64_t cur_frame_pts[AV_PARSER_PTS_NB];
+ int64_t cur_frame_dts[AV_PARSER_PTS_NB];
+
+ int flags;
+#define PARSER_FLAG_COMPLETE_FRAMES 0x0001
+#define PARSER_FLAG_ONCE 0x0002
+/// Set if the parser has a valid file offset
+#define PARSER_FLAG_FETCHED_OFFSET 0x0004
+#define PARSER_FLAG_USE_CODEC_TS 0x1000
+
+ int64_t offset; ///< byte offset from starting packet start
+ int64_t cur_frame_end[AV_PARSER_PTS_NB];
+
+ /**
+ * Set by parser to 1 for key frames and 0 for non-key frames.
+ * It is initialized to -1, so if the parser doesn't set this flag,
+ * old-style fallback using AV_PICTURE_TYPE_I picture type as key frames
+ * will be used.
+ */
+ int key_frame;
+
+#if FF_API_CONVERGENCE_DURATION
+ /**
+ * @deprecated unused
+ */
+ attribute_deprecated
+ int64_t convergence_duration;
+#endif
+
+ // Timestamp generation support:
+ /**
+ * Synchronization point for start of timestamp generation.
+ *
+ * Set to >0 for sync point, 0 for no sync point and <0 for undefined
+ * (default).
+ *
+ * For example, this corresponds to presence of H.264 buffering period
+ * SEI message.
+ */
+ int dts_sync_point;
+
+ /**
+ * Offset of the current timestamp against last timestamp sync point in
+ * units of AVCodecContext.time_base.
+ *
+ * Set to INT_MIN when dts_sync_point unused. Otherwise, it must
+ * contain a valid timestamp offset.
+ *
+ * Note that the timestamp of sync point has usually a nonzero
+ * dts_ref_dts_delta, which refers to the previous sync point. Offset of
+ * the next frame after timestamp sync point will be usually 1.
+ *
+ * For example, this corresponds to H.264 cpb_removal_delay.
+ */
+ int dts_ref_dts_delta;
+
+ /**
+ * Presentation delay of current frame in units of AVCodecContext.time_base.
+ *
+ * Set to INT_MIN when dts_sync_point unused. Otherwise, it must
+ * contain valid non-negative timestamp delta (presentation time of a frame
+ * must not lie in the past).
+ *
+ * This delay represents the difference between decoding and presentation
+ * time of the frame.
+ *
+ * For example, this corresponds to H.264 dpb_output_delay.
+ */
+ int pts_dts_delta;
+
+ /**
+ * Position of the packet in file.
+ *
+ * Analogous to cur_frame_pts/dts
+ */
+ int64_t cur_frame_pos[AV_PARSER_PTS_NB];
+
+ /**
+ * Byte position of currently parsed frame in stream.
+ */
+ int64_t pos;
+
+ /**
+ * Previous frame byte position.
+ */
+ int64_t last_pos;
+
+ /**
+ * Duration of the current frame.
+ * For audio, this is in units of 1 / AVCodecContext.sample_rate.
+ * For all other types, this is in units of AVCodecContext.time_base.
+ */
+ int duration;
+
+ enum AVFieldOrder field_order;
+
+ /**
+ * Indicate whether a picture is coded as a frame, top field or bottom field.
+ *
+ * For example, H.264 field_pic_flag equal to 0 corresponds to
+ * AV_PICTURE_STRUCTURE_FRAME. An H.264 picture with field_pic_flag
+ * equal to 1 and bottom_field_flag equal to 0 corresponds to
+ * AV_PICTURE_STRUCTURE_TOP_FIELD.
+ */
+ enum AVPictureStructure picture_structure;
+
+ /**
+ * Picture number incremented in presentation or output order.
+ * This field may be reinitialized at the first picture of a new sequence.
+ *
+ * For example, this corresponds to H.264 PicOrderCnt.
+ */
+ int output_picture_number;
+
+ /**
+ * Dimensions of the decoded video intended for presentation.
+ */
+ int width;
+ int height;
+
+ /**
+ * Dimensions of the coded video.
+ */
+ int coded_width;
+ int coded_height;
+
+ /**
+ * The format of the coded data, corresponds to enum AVPixelFormat for video
+ * and for enum AVSampleFormat for audio.
+ *
+ * Note that a decoder can have considerable freedom in how exactly it
+ * decodes the data, so the format reported here might be different from the
+ * one returned by a decoder.
+ */
+ int format;
+} AVCodecParserContext;
+
+typedef struct AVCodecParser {
+ int codec_ids[5]; /* several codec IDs are permitted */
+ int priv_data_size;
+ int (*parser_init)(AVCodecParserContext *s);
+ /* This callback never returns an error, a negative value means that
+ * the frame start was in a previous packet. */
+ int (*parser_parse)(AVCodecParserContext *s,
+ AVCodecContext *avctx,
+ const uint8_t **poutbuf, int *poutbuf_size,
+ const uint8_t *buf, int buf_size);
+ void (*parser_close)(AVCodecParserContext *s);
+ int (*split)(AVCodecContext *avctx, const uint8_t *buf, int buf_size);
+#if FF_API_NEXT
+ attribute_deprecated
+ struct AVCodecParser *next;
+#endif
+} AVCodecParser;
+
+/**
+ * Iterate over all registered codec parsers.
+ *
+ * @param opaque a pointer where libavcodec will store the iteration state. Must
+ * point to NULL to start the iteration.
+ *
+ * @return the next registered codec parser or NULL when the iteration is
+ * finished
+ */
+const AVCodecParser *av_parser_iterate(void **opaque);
+
+#if FF_API_NEXT
+attribute_deprecated
+AVCodecParser *av_parser_next(const AVCodecParser *c);
+
+attribute_deprecated
+void av_register_codec_parser(AVCodecParser *parser);
+#endif
+AVCodecParserContext *av_parser_init(int codec_id);
+
+/**
+ * Parse a packet.
+ *
+ * @param s parser context.
+ * @param avctx codec context.
+ * @param poutbuf set to pointer to parsed buffer or NULL if not yet finished.
+ * @param poutbuf_size set to size of parsed buffer or zero if not yet finished.
+ * @param buf input buffer.
+ * @param buf_size buffer size in bytes without the padding. I.e. the full buffer
+ size is assumed to be buf_size + AV_INPUT_BUFFER_PADDING_SIZE.
+ To signal EOF, this should be 0 (so that the last frame
+ can be output).
+ * @param pts input presentation timestamp.
+ * @param dts input decoding timestamp.
+ * @param pos input byte position in stream.
+ * @return the number of bytes of the input bitstream used.
+ *
+ * Example:
+ * @code
+ * while(in_len){
+ * len = av_parser_parse2(myparser, AVCodecContext, &data, &size,
+ * in_data, in_len,
+ * pts, dts, pos);
+ * in_data += len;
+ * in_len -= len;
+ *
+ * if(size)
+ * decode_frame(data, size);
+ * }
+ * @endcode
+ */
+int av_parser_parse2(AVCodecParserContext *s,
+ AVCodecContext *avctx,
+ uint8_t **poutbuf, int *poutbuf_size,
+ const uint8_t *buf, int buf_size,
+ int64_t pts, int64_t dts,
+ int64_t pos);
+
+#if FF_API_PARSER_CHANGE
+/**
+ * @return 0 if the output buffer is a subset of the input, 1 if it is allocated and must be freed
+ * @deprecated Use dump_extradata, remove_extra or extract_extradata
+ * bitstream filters instead.
+ */
+attribute_deprecated
+int av_parser_change(AVCodecParserContext *s,
+ AVCodecContext *avctx,
+ uint8_t **poutbuf, int *poutbuf_size,
+ const uint8_t *buf, int buf_size, int keyframe);
+#endif
+void av_parser_close(AVCodecParserContext *s);
+
+/**
+ * @}
+ * @}
+ */
+
+/**
+ * @addtogroup lavc_encoding
+ * @{
+ */
+
+#if FF_API_OLD_ENCDEC
+/**
+ * Encode a frame of audio.
+ *
+ * Takes input samples from frame and writes the next output packet, if
+ * available, to avpkt. The output packet does not necessarily contain data for
+ * the most recent frame, as encoders can delay, split, and combine input frames
+ * internally as needed.
+ *
+ * @param avctx codec context
+ * @param avpkt output AVPacket.
+ * The user can supply an output buffer by setting
+ * avpkt->data and avpkt->size prior to calling the
+ * function, but if the size of the user-provided data is not
+ * large enough, encoding will fail. If avpkt->data and
+ * avpkt->size are set, avpkt->destruct must also be set. All
+ * other AVPacket fields will be reset by the encoder using
+ * av_init_packet(). If avpkt->data is NULL, the encoder will
+ * allocate it. The encoder will set avpkt->size to the size
+ * of the output packet.
+ *
+ * If this function fails or produces no output, avpkt will be
+ * freed using av_packet_unref().
+ * @param[in] frame AVFrame containing the raw audio data to be encoded.
+ * May be NULL when flushing an encoder that has the
+ * AV_CODEC_CAP_DELAY capability set.
+ * If AV_CODEC_CAP_VARIABLE_FRAME_SIZE is set, then each frame
+ * can have any number of samples.
+ * If it is not set, frame->nb_samples must be equal to
+ * avctx->frame_size for all frames except the last.
+ * The final frame may be smaller than avctx->frame_size.
+ * @param[out] got_packet_ptr This field is set to 1 by libavcodec if the
+ * output packet is non-empty, and to 0 if it is
+ * empty. If the function returns an error, the
+ * packet can be assumed to be invalid, and the
+ * value of got_packet_ptr is undefined and should
+ * not be used.
+ * @return 0 on success, negative error code on failure
+ *
+ * @deprecated use avcodec_send_frame()/avcodec_receive_packet() instead.
+ * If allowed and required, set AVCodecContext.get_encode_buffer to
+ * a custom function to pass user supplied output buffers.
+ */
+attribute_deprecated
+int avcodec_encode_audio2(AVCodecContext *avctx, AVPacket *avpkt,
+ const AVFrame *frame, int *got_packet_ptr);
+
+/**
+ * Encode a frame of video.
+ *
+ * Takes input raw video data from frame and writes the next output packet, if
+ * available, to avpkt. The output packet does not necessarily contain data for
+ * the most recent frame, as encoders can delay and reorder input frames
+ * internally as needed.
+ *
+ * @param avctx codec context
+ * @param avpkt output AVPacket.
+ * The user can supply an output buffer by setting
+ * avpkt->data and avpkt->size prior to calling the
+ * function, but if the size of the user-provided data is not
+ * large enough, encoding will fail. All other AVPacket fields
+ * will be reset by the encoder using av_init_packet(). If
+ * avpkt->data is NULL, the encoder will allocate it.
+ * The encoder will set avpkt->size to the size of the
+ * output packet. The returned data (if any) belongs to the
+ * caller, he is responsible for freeing it.
+ *
+ * If this function fails or produces no output, avpkt will be
+ * freed using av_packet_unref().
+ * @param[in] frame AVFrame containing the raw video data to be encoded.
+ * May be NULL when flushing an encoder that has the
+ * AV_CODEC_CAP_DELAY capability set.
+ * @param[out] got_packet_ptr This field is set to 1 by libavcodec if the
+ * output packet is non-empty, and to 0 if it is
+ * empty. If the function returns an error, the
+ * packet can be assumed to be invalid, and the
+ * value of got_packet_ptr is undefined and should
+ * not be used.
+ * @return 0 on success, negative error code on failure
+ *
+ * @deprecated use avcodec_send_frame()/avcodec_receive_packet() instead.
+ * If allowed and required, set AVCodecContext.get_encode_buffer to
+ * a custom function to pass user supplied output buffers.
+ */
+attribute_deprecated
+int avcodec_encode_video2(AVCodecContext *avctx, AVPacket *avpkt,
+ const AVFrame *frame, int *got_packet_ptr);
+#endif
+
+int avcodec_encode_subtitle(AVCodecContext *avctx, uint8_t *buf, int buf_size,
+ const AVSubtitle *sub);
+
+
+/**
+ * @}
+ */
+
+#if FF_API_AVPICTURE
+/**
+ * @addtogroup lavc_picture
+ * @{
+ */
+
+/**
+ * @deprecated unused
+ */
+attribute_deprecated
+int avpicture_alloc(AVPicture *picture, enum AVPixelFormat pix_fmt, int width, int height);
+
+/**
+ * @deprecated unused
+ */
+attribute_deprecated
+void avpicture_free(AVPicture *picture);
+
+/**
+ * @deprecated use av_image_fill_arrays() instead.
+ */
+attribute_deprecated
+int avpicture_fill(AVPicture *picture, const uint8_t *ptr,
+ enum AVPixelFormat pix_fmt, int width, int height);
+
+/**
+ * @deprecated use av_image_copy_to_buffer() instead.
+ */
+attribute_deprecated
+int avpicture_layout(const AVPicture *src, enum AVPixelFormat pix_fmt,
+ int width, int height,
+ unsigned char *dest, int dest_size);
+
+/**
+ * @deprecated use av_image_get_buffer_size() instead.
+ */
+attribute_deprecated
+int avpicture_get_size(enum AVPixelFormat pix_fmt, int width, int height);
+
+/**
+ * @deprecated av_image_copy() instead.
+ */
+attribute_deprecated
+void av_picture_copy(AVPicture *dst, const AVPicture *src,
+ enum AVPixelFormat pix_fmt, int width, int height);
+
+/**
+ * @deprecated unused
+ */
+attribute_deprecated
+int av_picture_crop(AVPicture *dst, const AVPicture *src,
+ enum AVPixelFormat pix_fmt, int top_band, int left_band);
+
+/**
+ * @deprecated unused
+ */
+attribute_deprecated
+int av_picture_pad(AVPicture *dst, const AVPicture *src, int height, int width, enum AVPixelFormat pix_fmt,
+ int padtop, int padbottom, int padleft, int padright, int *color);
+
+/**
+ * @}
+ */
+#endif
+
+/**
+ * @defgroup lavc_misc Utility functions
+ * @ingroup libavc
+ *
+ * Miscellaneous utility functions related to both encoding and decoding
+ * (or neither).
+ * @{
+ */
+
+/**
+ * @defgroup lavc_misc_pixfmt Pixel formats
+ *
+ * Functions for working with pixel formats.
+ * @{
+ */
+
+#if FF_API_GETCHROMA
+/**
+ * @deprecated Use av_pix_fmt_get_chroma_sub_sample
+ */
+
+attribute_deprecated
+void avcodec_get_chroma_sub_sample(enum AVPixelFormat pix_fmt, int *h_shift, int *v_shift);
+#endif
+
+/**
+ * Return a value representing the fourCC code associated to the
+ * pixel format pix_fmt, or 0 if no associated fourCC code can be
+ * found.
+ */
+unsigned int avcodec_pix_fmt_to_codec_tag(enum AVPixelFormat pix_fmt);
+
+/**
+ * Find the best pixel format to convert to given a certain source pixel
+ * format. When converting from one pixel format to another, information loss
+ * may occur. For example, when converting from RGB24 to GRAY, the color
+ * information will be lost. Similarly, other losses occur when converting from
+ * some formats to other formats. avcodec_find_best_pix_fmt_of_2() searches which of
+ * the given pixel formats should be used to suffer the least amount of loss.
+ * The pixel formats from which it chooses one, are determined by the
+ * pix_fmt_list parameter.
+ *
+ *
+ * @param[in] pix_fmt_list AV_PIX_FMT_NONE terminated array of pixel formats to choose from
+ * @param[in] src_pix_fmt source pixel format
+ * @param[in] has_alpha Whether the source pixel format alpha channel is used.
+ * @param[out] loss_ptr Combination of flags informing you what kind of losses will occur.
+ * @return The best pixel format to convert to or -1 if none was found.
+ */
+enum AVPixelFormat avcodec_find_best_pix_fmt_of_list(const enum AVPixelFormat *pix_fmt_list,
+ enum AVPixelFormat src_pix_fmt,
+ int has_alpha, int *loss_ptr);
+
+#if FF_API_AVCODEC_PIX_FMT
+/**
+ * @deprecated see av_get_pix_fmt_loss()
+ */
+attribute_deprecated
+int avcodec_get_pix_fmt_loss(enum AVPixelFormat dst_pix_fmt, enum AVPixelFormat src_pix_fmt,
+ int has_alpha);
+/**
+ * @deprecated see av_find_best_pix_fmt_of_2()
+ */
+attribute_deprecated
+enum AVPixelFormat avcodec_find_best_pix_fmt_of_2(enum AVPixelFormat dst_pix_fmt1, enum AVPixelFormat dst_pix_fmt2,
+ enum AVPixelFormat src_pix_fmt, int has_alpha, int *loss_ptr);
+
+attribute_deprecated
+enum AVPixelFormat avcodec_find_best_pix_fmt2(enum AVPixelFormat dst_pix_fmt1, enum AVPixelFormat dst_pix_fmt2,
+ enum AVPixelFormat src_pix_fmt, int has_alpha, int *loss_ptr);
+#endif
+
+enum AVPixelFormat avcodec_default_get_format(struct AVCodecContext *s, const enum AVPixelFormat * fmt);
+
+/**
+ * @}
+ */
+
+#if FF_API_TAG_STRING
+/**
+ * Put a string representing the codec tag codec_tag in buf.
+ *
+ * @param buf buffer to place codec tag in
+ * @param buf_size size in bytes of buf
+ * @param codec_tag codec tag to assign
+ * @return the length of the string that would have been generated if
+ * enough space had been available, excluding the trailing null
+ *
+ * @deprecated see av_fourcc_make_string() and av_fourcc2str().
+ */
+attribute_deprecated
+size_t av_get_codec_tag_string(char *buf, size_t buf_size, unsigned int codec_tag);
+#endif
+
+void avcodec_string(char *buf, int buf_size, AVCodecContext *enc, int encode);
+
+/**
+ * Return a name for the specified profile, if available.
+ *
+ * @param codec the codec that is searched for the given profile
+ * @param profile the profile value for which a name is requested
+ * @return A name for the profile if found, NULL otherwise.
+ */
+const char *av_get_profile_name(const AVCodec *codec, int profile);
+
+/**
+ * Return a name for the specified profile, if available.
+ *
+ * @param codec_id the ID of the codec to which the requested profile belongs
+ * @param profile the profile value for which a name is requested
+ * @return A name for the profile if found, NULL otherwise.
+ *
+ * @note unlike av_get_profile_name(), which searches a list of profiles
+ * supported by a specific decoder or encoder implementation, this
+ * function searches the list of profiles from the AVCodecDescriptor
+ */
+const char *avcodec_profile_name(enum AVCodecID codec_id, int profile);
+
+int avcodec_default_execute(AVCodecContext *c, int (*func)(AVCodecContext *c2, void *arg2),void *arg, int *ret, int count, int size);
+int avcodec_default_execute2(AVCodecContext *c, int (*func)(AVCodecContext *c2, void *arg2, int, int),void *arg, int *ret, int count);
+//FIXME func typedef
+
+/**
+ * Fill AVFrame audio data and linesize pointers.
+ *
+ * The buffer buf must be a preallocated buffer with a size big enough
+ * to contain the specified samples amount. The filled AVFrame data
+ * pointers will point to this buffer.
+ *
+ * AVFrame extended_data channel pointers are allocated if necessary for
+ * planar audio.
+ *
+ * @param frame the AVFrame
+ * frame->nb_samples must be set prior to calling the
+ * function. This function fills in frame->data,
+ * frame->extended_data, frame->linesize[0].
+ * @param nb_channels channel count
+ * @param sample_fmt sample format
+ * @param buf buffer to use for frame data
+ * @param buf_size size of buffer
+ * @param align plane size sample alignment (0 = default)
+ * @return >=0 on success, negative error code on failure
+ * @todo return the size in bytes required to store the samples in
+ * case of success, at the next libavutil bump
+ */
+int avcodec_fill_audio_frame(AVFrame *frame, int nb_channels,
+ enum AVSampleFormat sample_fmt, const uint8_t *buf,
+ int buf_size, int align);
+
+/**
+ * Reset the internal codec state / flush internal buffers. Should be called
+ * e.g. when seeking or when switching to a different stream.
+ *
+ * @note for decoders, when refcounted frames are not used
+ * (i.e. avctx->refcounted_frames is 0), this invalidates the frames previously
+ * returned from the decoder. When refcounted frames are used, the decoder just
+ * releases any references it might keep internally, but the caller's reference
+ * remains valid.
+ *
+ * @note for encoders, this function will only do something if the encoder
+ * declares support for AV_CODEC_CAP_ENCODER_FLUSH. When called, the encoder
+ * will drain any remaining packets, and can then be re-used for a different
+ * stream (as opposed to sending a null frame which will leave the encoder
+ * in a permanent EOF state after draining). This can be desirable if the
+ * cost of tearing down and replacing the encoder instance is high.
+ */
+void avcodec_flush_buffers(AVCodecContext *avctx);
+
+/**
+ * Return codec bits per sample.
+ *
+ * @param[in] codec_id the codec
+ * @return Number of bits per sample or zero if unknown for the given codec.
+ */
+int av_get_bits_per_sample(enum AVCodecID codec_id);
+
+/**
+ * Return the PCM codec associated with a sample format.
+ * @param be endianness, 0 for little, 1 for big,
+ * -1 (or anything else) for native
+ * @return AV_CODEC_ID_PCM_* or AV_CODEC_ID_NONE
+ */
+enum AVCodecID av_get_pcm_codec(enum AVSampleFormat fmt, int be);
+
+/**
+ * Return codec bits per sample.
+ * Only return non-zero if the bits per sample is exactly correct, not an
+ * approximation.
+ *
+ * @param[in] codec_id the codec
+ * @return Number of bits per sample or zero if unknown for the given codec.
+ */
+int av_get_exact_bits_per_sample(enum AVCodecID codec_id);
+
+/**
+ * Return audio frame duration.
+ *
+ * @param avctx codec context
+ * @param frame_bytes size of the frame, or 0 if unknown
+ * @return frame duration, in samples, if known. 0 if not able to
+ * determine.
+ */
+int av_get_audio_frame_duration(AVCodecContext *avctx, int frame_bytes);
+
+/**
+ * This function is the same as av_get_audio_frame_duration(), except it works
+ * with AVCodecParameters instead of an AVCodecContext.
+ */
+int av_get_audio_frame_duration2(AVCodecParameters *par, int frame_bytes);
+
+#if FF_API_OLD_BSF
+typedef struct AVBitStreamFilterContext {
+ void *priv_data;
+ const struct AVBitStreamFilter *filter;
+ AVCodecParserContext *parser;
+ struct AVBitStreamFilterContext *next;
+ /**
+ * Internal default arguments, used if NULL is passed to av_bitstream_filter_filter().
+ * Not for access by library users.
+ */
+ char *args;
+} AVBitStreamFilterContext;
+
+/**
+ * @deprecated the old bitstream filtering API (using AVBitStreamFilterContext)
+ * is deprecated. Use the new bitstream filtering API (using AVBSFContext).
+ */
+attribute_deprecated
+void av_register_bitstream_filter(AVBitStreamFilter *bsf);
+/**
+ * @deprecated the old bitstream filtering API (using AVBitStreamFilterContext)
+ * is deprecated. Use av_bsf_get_by_name(), av_bsf_alloc(), and av_bsf_init()
+ * from the new bitstream filtering API (using AVBSFContext).
+ */
+attribute_deprecated
+AVBitStreamFilterContext *av_bitstream_filter_init(const char *name);
+/**
+ * @deprecated the old bitstream filtering API (using AVBitStreamFilterContext)
+ * is deprecated. Use av_bsf_send_packet() and av_bsf_receive_packet() from the
+ * new bitstream filtering API (using AVBSFContext).
+ */
+attribute_deprecated
+int av_bitstream_filter_filter(AVBitStreamFilterContext *bsfc,
+ AVCodecContext *avctx, const char *args,
+ uint8_t **poutbuf, int *poutbuf_size,
+ const uint8_t *buf, int buf_size, int keyframe);
+/**
+ * @deprecated the old bitstream filtering API (using AVBitStreamFilterContext)
+ * is deprecated. Use av_bsf_free() from the new bitstream filtering API (using
+ * AVBSFContext).
+ */
+attribute_deprecated
+void av_bitstream_filter_close(AVBitStreamFilterContext *bsf);
+/**
+ * @deprecated the old bitstream filtering API (using AVBitStreamFilterContext)
+ * is deprecated. Use av_bsf_iterate() from the new bitstream filtering API (using
+ * AVBSFContext).
+ */
+attribute_deprecated
+const AVBitStreamFilter *av_bitstream_filter_next(const AVBitStreamFilter *f);
+#endif
+
+#if FF_API_NEXT
+attribute_deprecated
+const AVBitStreamFilter *av_bsf_next(void **opaque);
+#endif
+
+/* memory */
+
+/**
+ * Same behaviour av_fast_malloc but the buffer has additional
+ * AV_INPUT_BUFFER_PADDING_SIZE at the end which will always be 0.
+ *
+ * In addition the whole buffer will initially and after resizes
+ * be 0-initialized so that no uninitialized data will ever appear.
+ */
+void av_fast_padded_malloc(void *ptr, unsigned int *size, size_t min_size);
+
+/**
+ * Same behaviour av_fast_padded_malloc except that buffer will always
+ * be 0-initialized after call.
+ */
+void av_fast_padded_mallocz(void *ptr, unsigned int *size, size_t min_size);
+
+/**
+ * Encode extradata length to a buffer. Used by xiph codecs.
+ *
+ * @param s buffer to write to; must be at least (v/255+1) bytes long
+ * @param v size of extradata in bytes
+ * @return number of bytes written to the buffer.
+ */
+unsigned int av_xiphlacing(unsigned char *s, unsigned int v);
+
+#if FF_API_USER_VISIBLE_AVHWACCEL
+/**
+ * Register the hardware accelerator hwaccel.
+ *
+ * @deprecated This function doesn't do anything.
+ */
+attribute_deprecated
+void av_register_hwaccel(AVHWAccel *hwaccel);
+
+/**
+ * If hwaccel is NULL, returns the first registered hardware accelerator,
+ * if hwaccel is non-NULL, returns the next registered hardware accelerator
+ * after hwaccel, or NULL if hwaccel is the last one.
+ *
+ * @deprecated AVHWaccel structures contain no user-serviceable parts, so
+ * this function should not be used.
+ */
+attribute_deprecated
+AVHWAccel *av_hwaccel_next(const AVHWAccel *hwaccel);
+#endif
+
+#if FF_API_LOCKMGR
+/**
+ * Lock operation used by lockmgr
+ *
+ * @deprecated Deprecated together with av_lockmgr_register().
+ */
+enum AVLockOp {
+ AV_LOCK_CREATE, ///< Create a mutex
+ AV_LOCK_OBTAIN, ///< Lock the mutex
+ AV_LOCK_RELEASE, ///< Unlock the mutex
+ AV_LOCK_DESTROY, ///< Free mutex resources
+};
+
+/**
+ * Register a user provided lock manager supporting the operations
+ * specified by AVLockOp. The "mutex" argument to the function points
+ * to a (void *) where the lockmgr should store/get a pointer to a user
+ * allocated mutex. It is NULL upon AV_LOCK_CREATE and equal to the
+ * value left by the last call for all other ops. If the lock manager is
+ * unable to perform the op then it should leave the mutex in the same
+ * state as when it was called and return a non-zero value. However,
+ * when called with AV_LOCK_DESTROY the mutex will always be assumed to
+ * have been successfully destroyed. If av_lockmgr_register succeeds
+ * it will return a non-negative value, if it fails it will return a
+ * negative value and destroy all mutex and unregister all callbacks.
+ * av_lockmgr_register is not thread-safe, it must be called from a
+ * single thread before any calls which make use of locking are used.
+ *
+ * @param cb User defined callback. av_lockmgr_register invokes calls
+ * to this callback and the previously registered callback.
+ * The callback will be used to create more than one mutex
+ * each of which must be backed by its own underlying locking
+ * mechanism (i.e. do not use a single static object to
+ * implement your lock manager). If cb is set to NULL the
+ * lockmgr will be unregistered.
+ *
+ * @deprecated This function does nothing, and always returns 0. Be sure to
+ * build with thread support to get basic thread safety.
+ */
+attribute_deprecated
+int av_lockmgr_register(int (*cb)(void **mutex, enum AVLockOp op));
+#endif
+
+/**
+ * @return a positive value if s is open (i.e. avcodec_open2() was called on it
+ * with no corresponding avcodec_close()), 0 otherwise.
+ */
+int avcodec_is_open(AVCodecContext *s);
+
+/**
+ * Allocate a CPB properties structure and initialize its fields to default
+ * values.
+ *
+ * @param size if non-NULL, the size of the allocated struct will be written
+ * here. This is useful for embedding it in side data.
+ *
+ * @return the newly allocated struct or NULL on failure
+ */
+AVCPBProperties *av_cpb_properties_alloc(size_t *size);
+
+/**
+ * @}
+ */
+
+#endif /* AVCODEC_AVCODEC_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/avfft.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/avfft.h
new file mode 100644
index 0000000000..0c0f9b8d8d
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/avfft.h
@@ -0,0 +1,118 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_AVFFT_H
+#define AVCODEC_AVFFT_H
+
+/**
+ * @file
+ * @ingroup lavc_fft
+ * FFT functions
+ */
+
+/**
+ * @defgroup lavc_fft FFT functions
+ * @ingroup lavc_misc
+ *
+ * @{
+ */
+
+typedef float FFTSample;
+
+typedef struct FFTComplex {
+ FFTSample re, im;
+} FFTComplex;
+
+typedef struct FFTContext FFTContext;
+
+/**
+ * Set up a complex FFT.
+ * @param nbits log2 of the length of the input array
+ * @param inverse if 0 perform the forward transform, if 1 perform the inverse
+ */
+FFTContext *av_fft_init(int nbits, int inverse);
+
+/**
+ * Do the permutation needed BEFORE calling ff_fft_calc().
+ */
+void av_fft_permute(FFTContext *s, FFTComplex *z);
+
+/**
+ * Do a complex FFT with the parameters defined in av_fft_init(). The
+ * input data must be permuted before. No 1.0/sqrt(n) normalization is done.
+ */
+void av_fft_calc(FFTContext *s, FFTComplex *z);
+
+void av_fft_end(FFTContext *s);
+
+FFTContext *av_mdct_init(int nbits, int inverse, double scale);
+void av_imdct_calc(FFTContext *s, FFTSample *output, const FFTSample *input);
+void av_imdct_half(FFTContext *s, FFTSample *output, const FFTSample *input);
+void av_mdct_calc(FFTContext *s, FFTSample *output, const FFTSample *input);
+void av_mdct_end(FFTContext *s);
+
+/* Real Discrete Fourier Transform */
+
+enum RDFTransformType {
+ DFT_R2C,
+ IDFT_C2R,
+ IDFT_R2C,
+ DFT_C2R,
+};
+
+typedef struct RDFTContext RDFTContext;
+
+/**
+ * Set up a real FFT.
+ * @param nbits log2 of the length of the input array
+ * @param trans the type of transform
+ */
+RDFTContext *av_rdft_init(int nbits, enum RDFTransformType trans);
+void av_rdft_calc(RDFTContext *s, FFTSample *data);
+void av_rdft_end(RDFTContext *s);
+
+/* Discrete Cosine Transform */
+
+typedef struct DCTContext DCTContext;
+
+enum DCTTransformType {
+ DCT_II = 0,
+ DCT_III,
+ DCT_I,
+ DST_I,
+};
+
+/**
+ * Set up DCT.
+ *
+ * @param nbits size of the input array:
+ * (1 << nbits) for DCT-II, DCT-III and DST-I
+ * (1 << nbits) + 1 for DCT-I
+ * @param type the type of transform
+ *
+ * @note the first element of the input of DST-I is ignored
+ */
+DCTContext *av_dct_init(int nbits, enum DCTTransformType type);
+void av_dct_calc(DCTContext *s, FFTSample *data);
+void av_dct_end (DCTContext *s);
+
+/**
+ * @}
+ */
+
+#endif /* AVCODEC_AVFFT_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/bsf.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/bsf.h
new file mode 100644
index 0000000000..3b5faa85cb
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/bsf.h
@@ -0,0 +1,325 @@
+/*
+ * Bitstream filters public API
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_BSF_H
+#define AVCODEC_BSF_H
+
+#include "libavutil/dict.h"
+#include "libavutil/log.h"
+#include "libavutil/rational.h"
+
+#include "codec_id.h"
+#include "codec_par.h"
+#include "packet.h"
+
+/**
+ * @addtogroup lavc_core
+ * @{
+ */
+
+typedef struct AVBSFInternal AVBSFInternal;
+
+/**
+ * The bitstream filter state.
+ *
+ * This struct must be allocated with av_bsf_alloc() and freed with
+ * av_bsf_free().
+ *
+ * The fields in the struct will only be changed (by the caller or by the
+ * filter) as described in their documentation, and are to be considered
+ * immutable otherwise.
+ */
+typedef struct AVBSFContext {
+ /**
+ * A class for logging and AVOptions
+ */
+ const AVClass *av_class;
+
+ /**
+ * The bitstream filter this context is an instance of.
+ */
+ const struct AVBitStreamFilter *filter;
+
+ /**
+ * Opaque libavcodec internal data. Must not be touched by the caller in any
+ * way.
+ */
+ AVBSFInternal *internal;
+
+ /**
+ * Opaque filter-specific private data. If filter->priv_class is non-NULL,
+ * this is an AVOptions-enabled struct.
+ */
+ void *priv_data;
+
+ /**
+ * Parameters of the input stream. This field is allocated in
+ * av_bsf_alloc(), it needs to be filled by the caller before
+ * av_bsf_init().
+ */
+ AVCodecParameters *par_in;
+
+ /**
+ * Parameters of the output stream. This field is allocated in
+ * av_bsf_alloc(), it is set by the filter in av_bsf_init().
+ */
+ AVCodecParameters *par_out;
+
+ /**
+ * The timebase used for the timestamps of the input packets. Set by the
+ * caller before av_bsf_init().
+ */
+ AVRational time_base_in;
+
+ /**
+ * The timebase used for the timestamps of the output packets. Set by the
+ * filter in av_bsf_init().
+ */
+ AVRational time_base_out;
+} AVBSFContext;
+
+typedef struct AVBitStreamFilter {
+ const char *name;
+
+ /**
+ * A list of codec ids supported by the filter, terminated by
+ * AV_CODEC_ID_NONE.
+ * May be NULL, in that case the bitstream filter works with any codec id.
+ */
+ const enum AVCodecID *codec_ids;
+
+ /**
+ * A class for the private data, used to declare bitstream filter private
+ * AVOptions. This field is NULL for bitstream filters that do not declare
+ * any options.
+ *
+ * If this field is non-NULL, the first member of the filter private data
+ * must be a pointer to AVClass, which will be set by libavcodec generic
+ * code to this class.
+ */
+ const AVClass *priv_class;
+
+ /*****************************************************************
+ * No fields below this line are part of the public API. They
+ * may not be used outside of libavcodec and can be changed and
+ * removed at will.
+ * New public fields should be added right above.
+ *****************************************************************
+ */
+
+ int priv_data_size;
+ int (*init)(AVBSFContext *ctx);
+ int (*filter)(AVBSFContext *ctx, AVPacket *pkt);
+ void (*close)(AVBSFContext *ctx);
+ void (*flush)(AVBSFContext *ctx);
+} AVBitStreamFilter;
+
+/**
+ * @return a bitstream filter with the specified name or NULL if no such
+ * bitstream filter exists.
+ */
+const AVBitStreamFilter *av_bsf_get_by_name(const char *name);
+
+/**
+ * Iterate over all registered bitstream filters.
+ *
+ * @param opaque a pointer where libavcodec will store the iteration state. Must
+ * point to NULL to start the iteration.
+ *
+ * @return the next registered bitstream filter or NULL when the iteration is
+ * finished
+ */
+const AVBitStreamFilter *av_bsf_iterate(void **opaque);
+
+/**
+ * Allocate a context for a given bitstream filter. The caller must fill in the
+ * context parameters as described in the documentation and then call
+ * av_bsf_init() before sending any data to the filter.
+ *
+ * @param filter the filter for which to allocate an instance.
+ * @param ctx a pointer into which the pointer to the newly-allocated context
+ * will be written. It must be freed with av_bsf_free() after the
+ * filtering is done.
+ *
+ * @return 0 on success, a negative AVERROR code on failure
+ */
+int av_bsf_alloc(const AVBitStreamFilter *filter, AVBSFContext **ctx);
+
+/**
+ * Prepare the filter for use, after all the parameters and options have been
+ * set.
+ */
+int av_bsf_init(AVBSFContext *ctx);
+
+/**
+ * Submit a packet for filtering.
+ *
+ * After sending each packet, the filter must be completely drained by calling
+ * av_bsf_receive_packet() repeatedly until it returns AVERROR(EAGAIN) or
+ * AVERROR_EOF.
+ *
+ * @param pkt the packet to filter. The bitstream filter will take ownership of
+ * the packet and reset the contents of pkt. pkt is not touched if an error occurs.
+ * If pkt is empty (i.e. NULL, or pkt->data is NULL and pkt->side_data_elems zero),
+ * it signals the end of the stream (i.e. no more non-empty packets will be sent;
+ * sending more empty packets does nothing) and will cause the filter to output
+ * any packets it may have buffered internally.
+ *
+ * @return 0 on success. AVERROR(EAGAIN) if packets need to be retrieved from the
+ * filter (using av_bsf_receive_packet()) before new input can be consumed. Another
+ * negative AVERROR value if an error occurs.
+ */
+int av_bsf_send_packet(AVBSFContext *ctx, AVPacket *pkt);
+
+/**
+ * Retrieve a filtered packet.
+ *
+ * @param[out] pkt this struct will be filled with the contents of the filtered
+ * packet. It is owned by the caller and must be freed using
+ * av_packet_unref() when it is no longer needed.
+ * This parameter should be "clean" (i.e. freshly allocated
+ * with av_packet_alloc() or unreffed with av_packet_unref())
+ * when this function is called. If this function returns
+ * successfully, the contents of pkt will be completely
+ * overwritten by the returned data. On failure, pkt is not
+ * touched.
+ *
+ * @return 0 on success. AVERROR(EAGAIN) if more packets need to be sent to the
+ * filter (using av_bsf_send_packet()) to get more output. AVERROR_EOF if there
+ * will be no further output from the filter. Another negative AVERROR value if
+ * an error occurs.
+ *
+ * @note one input packet may result in several output packets, so after sending
+ * a packet with av_bsf_send_packet(), this function needs to be called
+ * repeatedly until it stops returning 0. It is also possible for a filter to
+ * output fewer packets than were sent to it, so this function may return
+ * AVERROR(EAGAIN) immediately after a successful av_bsf_send_packet() call.
+ */
+int av_bsf_receive_packet(AVBSFContext *ctx, AVPacket *pkt);
+
+/**
+ * Reset the internal bitstream filter state. Should be called e.g. when seeking.
+ */
+void av_bsf_flush(AVBSFContext *ctx);
+
+/**
+ * Free a bitstream filter context and everything associated with it; write NULL
+ * into the supplied pointer.
+ */
+void av_bsf_free(AVBSFContext **ctx);
+
+/**
+ * Get the AVClass for AVBSFContext. It can be used in combination with
+ * AV_OPT_SEARCH_FAKE_OBJ for examining options.
+ *
+ * @see av_opt_find().
+ */
+const AVClass *av_bsf_get_class(void);
+
+/**
+ * Structure for chain/list of bitstream filters.
+ * Empty list can be allocated by av_bsf_list_alloc().
+ */
+typedef struct AVBSFList AVBSFList;
+
+/**
+ * Allocate empty list of bitstream filters.
+ * The list must be later freed by av_bsf_list_free()
+ * or finalized by av_bsf_list_finalize().
+ *
+ * @return Pointer to @ref AVBSFList on success, NULL in case of failure
+ */
+AVBSFList *av_bsf_list_alloc(void);
+
+/**
+ * Free list of bitstream filters.
+ *
+ * @param lst Pointer to pointer returned by av_bsf_list_alloc()
+ */
+void av_bsf_list_free(AVBSFList **lst);
+
+/**
+ * Append bitstream filter to the list of bitstream filters.
+ *
+ * @param lst List to append to
+ * @param bsf Filter context to be appended
+ *
+ * @return >=0 on success, negative AVERROR in case of failure
+ */
+int av_bsf_list_append(AVBSFList *lst, AVBSFContext *bsf);
+
+/**
+ * Construct new bitstream filter context given it's name and options
+ * and append it to the list of bitstream filters.
+ *
+ * @param lst List to append to
+ * @param bsf_name Name of the bitstream filter
+ * @param options Options for the bitstream filter, can be set to NULL
+ *
+ * @return >=0 on success, negative AVERROR in case of failure
+ */
+int av_bsf_list_append2(AVBSFList *lst, const char * bsf_name, AVDictionary **options);
+/**
+ * Finalize list of bitstream filters.
+ *
+ * This function will transform @ref AVBSFList to single @ref AVBSFContext,
+ * so the whole chain of bitstream filters can be treated as single filter
+ * freshly allocated by av_bsf_alloc().
+ * If the call is successful, @ref AVBSFList structure is freed and lst
+ * will be set to NULL. In case of failure, caller is responsible for
+ * freeing the structure by av_bsf_list_free()
+ *
+ * @param lst Filter list structure to be transformed
+ * @param[out] bsf Pointer to be set to newly created @ref AVBSFContext structure
+ * representing the chain of bitstream filters
+ *
+ * @return >=0 on success, negative AVERROR in case of failure
+ */
+int av_bsf_list_finalize(AVBSFList **lst, AVBSFContext **bsf);
+
+/**
+ * Parse string describing list of bitstream filters and create single
+ * @ref AVBSFContext describing the whole chain of bitstream filters.
+ * Resulting @ref AVBSFContext can be treated as any other @ref AVBSFContext freshly
+ * allocated by av_bsf_alloc().
+ *
+ * @param str String describing chain of bitstream filters in format
+ * `bsf1[=opt1=val1:opt2=val2][,bsf2]`
+ * @param[out] bsf Pointer to be set to newly created @ref AVBSFContext structure
+ * representing the chain of bitstream filters
+ *
+ * @return >=0 on success, negative AVERROR in case of failure
+ */
+int av_bsf_list_parse_str(const char *str, AVBSFContext **bsf);
+
+/**
+ * Get null/pass-through bitstream filter.
+ *
+ * @param[out] bsf Pointer to be set to new instance of pass-through bitstream filter
+ *
+ * @return
+ */
+int av_bsf_get_null_filter(AVBSFContext **bsf);
+
+/**
+ * @}
+ */
+
+#endif // AVCODEC_BSF_H
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/codec.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/codec.h
new file mode 100644
index 0000000000..50a22f6e3c
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/codec.h
@@ -0,0 +1,480 @@
+/*
+ * AVCodec public API
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_CODEC_H
+#define AVCODEC_CODEC_H
+
+#include <stdint.h>
+
+#include "libavutil/avutil.h"
+#include "libavutil/hwcontext.h"
+#include "libavutil/log.h"
+#include "libavutil/pixfmt.h"
+#include "libavutil/rational.h"
+#include "libavutil/samplefmt.h"
+
+#include "libavcodec/codec_id.h"
+#include "libavcodec/version.h"
+
+/**
+ * @addtogroup lavc_core
+ * @{
+ */
+
+/**
+ * Decoder can use draw_horiz_band callback.
+ */
+#define AV_CODEC_CAP_DRAW_HORIZ_BAND (1 << 0)
+/**
+ * Codec uses get_buffer() or get_encode_buffer() for allocating buffers and
+ * supports custom allocators.
+ * If not set, it might not use get_buffer() or get_encode_buffer() at all, or
+ * use operations that assume the buffer was allocated by
+ * avcodec_default_get_buffer2 or avcodec_default_get_encode_buffer.
+ */
+#define AV_CODEC_CAP_DR1 (1 << 1)
+#define AV_CODEC_CAP_TRUNCATED (1 << 3)
+/**
+ * Encoder or decoder requires flushing with NULL input at the end in order to
+ * give the complete and correct output.
+ *
+ * NOTE: If this flag is not set, the codec is guaranteed to never be fed with
+ * with NULL data. The user can still send NULL data to the public encode
+ * or decode function, but libavcodec will not pass it along to the codec
+ * unless this flag is set.
+ *
+ * Decoders:
+ * The decoder has a non-zero delay and needs to be fed with avpkt->data=NULL,
+ * avpkt->size=0 at the end to get the delayed data until the decoder no longer
+ * returns frames.
+ *
+ * Encoders:
+ * The encoder needs to be fed with NULL data at the end of encoding until the
+ * encoder no longer returns data.
+ *
+ * NOTE: For encoders implementing the AVCodec.encode2() function, setting this
+ * flag also means that the encoder must set the pts and duration for
+ * each output packet. If this flag is not set, the pts and duration will
+ * be determined by libavcodec from the input frame.
+ */
+#define AV_CODEC_CAP_DELAY (1 << 5)
+/**
+ * Codec can be fed a final frame with a smaller size.
+ * This can be used to prevent truncation of the last audio samples.
+ */
+#define AV_CODEC_CAP_SMALL_LAST_FRAME (1 << 6)
+
+/**
+ * Codec can output multiple frames per AVPacket
+ * Normally demuxers return one frame at a time, demuxers which do not do
+ * are connected to a parser to split what they return into proper frames.
+ * This flag is reserved to the very rare category of codecs which have a
+ * bitstream that cannot be split into frames without timeconsuming
+ * operations like full decoding. Demuxers carrying such bitstreams thus
+ * may return multiple frames in a packet. This has many disadvantages like
+ * prohibiting stream copy in many cases thus it should only be considered
+ * as a last resort.
+ */
+#define AV_CODEC_CAP_SUBFRAMES (1 << 8)
+/**
+ * Codec is experimental and is thus avoided in favor of non experimental
+ * encoders
+ */
+#define AV_CODEC_CAP_EXPERIMENTAL (1 << 9)
+/**
+ * Codec should fill in channel configuration and samplerate instead of container
+ */
+#define AV_CODEC_CAP_CHANNEL_CONF (1 << 10)
+/**
+ * Codec supports frame-level multithreading.
+ */
+#define AV_CODEC_CAP_FRAME_THREADS (1 << 12)
+/**
+ * Codec supports slice-based (or partition-based) multithreading.
+ */
+#define AV_CODEC_CAP_SLICE_THREADS (1 << 13)
+/**
+ * Codec supports changed parameters at any point.
+ */
+#define AV_CODEC_CAP_PARAM_CHANGE (1 << 14)
+/**
+ * Codec supports multithreading through a method other than slice- or
+ * frame-level multithreading. Typically this marks wrappers around
+ * multithreading-capable external libraries.
+ */
+#define AV_CODEC_CAP_OTHER_THREADS (1 << 15)
+#if FF_API_AUTO_THREADS
+#define AV_CODEC_CAP_AUTO_THREADS AV_CODEC_CAP_OTHER_THREADS
+#endif
+/**
+ * Audio encoder supports receiving a different number of samples in each call.
+ */
+#define AV_CODEC_CAP_VARIABLE_FRAME_SIZE (1 << 16)
+/**
+ * Decoder is not a preferred choice for probing.
+ * This indicates that the decoder is not a good choice for probing.
+ * It could for example be an expensive to spin up hardware decoder,
+ * or it could simply not provide a lot of useful information about
+ * the stream.
+ * A decoder marked with this flag should only be used as last resort
+ * choice for probing.
+ */
+#define AV_CODEC_CAP_AVOID_PROBING (1 << 17)
+
+#if FF_API_UNUSED_CODEC_CAPS
+/**
+ * Deprecated and unused. Use AVCodecDescriptor.props instead
+ */
+#define AV_CODEC_CAP_INTRA_ONLY 0x40000000
+/**
+ * Deprecated and unused. Use AVCodecDescriptor.props instead
+ */
+#define AV_CODEC_CAP_LOSSLESS 0x80000000
+#endif
+
+/**
+ * Codec is backed by a hardware implementation. Typically used to
+ * identify a non-hwaccel hardware decoder. For information about hwaccels, use
+ * avcodec_get_hw_config() instead.
+ */
+#define AV_CODEC_CAP_HARDWARE (1 << 18)
+
+/**
+ * Codec is potentially backed by a hardware implementation, but not
+ * necessarily. This is used instead of AV_CODEC_CAP_HARDWARE, if the
+ * implementation provides some sort of internal fallback.
+ */
+#define AV_CODEC_CAP_HYBRID (1 << 19)
+
+/**
+ * This codec takes the reordered_opaque field from input AVFrames
+ * and returns it in the corresponding field in AVCodecContext after
+ * encoding.
+ */
+#define AV_CODEC_CAP_ENCODER_REORDERED_OPAQUE (1 << 20)
+
+/**
+ * This encoder can be flushed using avcodec_flush_buffers(). If this flag is
+ * not set, the encoder must be closed and reopened to ensure that no frames
+ * remain pending.
+ */
+#define AV_CODEC_CAP_ENCODER_FLUSH (1 << 21)
+
+/**
+ * AVProfile.
+ */
+typedef struct AVProfile {
+ int profile;
+ const char *name; ///< short name for the profile
+} AVProfile;
+
+typedef struct AVCodecDefault AVCodecDefault;
+
+struct AVCodecContext;
+struct AVSubtitle;
+struct AVPacket;
+
+/**
+ * AVCodec.
+ */
+typedef struct AVCodec {
+ /**
+ * Name of the codec implementation.
+ * The name is globally unique among encoders and among decoders (but an
+ * encoder and a decoder can share the same name).
+ * This is the primary way to find a codec from the user perspective.
+ */
+ const char *name;
+ /**
+ * Descriptive name for the codec, meant to be more human readable than name.
+ * You should use the NULL_IF_CONFIG_SMALL() macro to define it.
+ */
+ const char *long_name;
+ enum AVMediaType type;
+ enum AVCodecID id;
+ /**
+ * Codec capabilities.
+ * see AV_CODEC_CAP_*
+ */
+ int capabilities;
+ const AVRational *supported_framerates; ///< array of supported framerates, or NULL if any, array is terminated by {0,0}
+ const enum AVPixelFormat *pix_fmts; ///< array of supported pixel formats, or NULL if unknown, array is terminated by -1
+ const int *supported_samplerates; ///< array of supported audio samplerates, or NULL if unknown, array is terminated by 0
+ const enum AVSampleFormat *sample_fmts; ///< array of supported sample formats, or NULL if unknown, array is terminated by -1
+ const uint64_t *channel_layouts; ///< array of support channel layouts, or NULL if unknown. array is terminated by 0
+ uint8_t max_lowres; ///< maximum value for lowres supported by the decoder
+ const AVClass *priv_class; ///< AVClass for the private context
+ const AVProfile *profiles; ///< array of recognized profiles, or NULL if unknown, array is terminated by {FF_PROFILE_UNKNOWN}
+
+ /**
+ * Group name of the codec implementation.
+ * This is a short symbolic name of the wrapper backing this codec. A
+ * wrapper uses some kind of external implementation for the codec, such
+ * as an external library, or a codec implementation provided by the OS or
+ * the hardware.
+ * If this field is NULL, this is a builtin, libavcodec native codec.
+ * If non-NULL, this will be the suffix in AVCodec.name in most cases
+ * (usually AVCodec.name will be of the form "<codec_name>_<wrapper_name>").
+ */
+ const char *wrapper_name;
+
+ /*****************************************************************
+ * No fields below this line are part of the public API. They
+ * may not be used outside of libavcodec and can be changed and
+ * removed at will.
+ * New public fields should be added right above.
+ *****************************************************************
+ */
+ int priv_data_size;
+#if FF_API_NEXT
+ struct AVCodec *next;
+#endif
+ /**
+ * @name Frame-level threading support functions
+ * @{
+ */
+ /**
+ * Copy necessary context variables from a previous thread context to the current one.
+ * If not defined, the next thread will start automatically; otherwise, the codec
+ * must call ff_thread_finish_setup().
+ *
+ * dst and src will (rarely) point to the same context, in which case memcpy should be skipped.
+ */
+ int (*update_thread_context)(struct AVCodecContext *dst, const struct AVCodecContext *src);
+ /** @} */
+
+ /**
+ * Private codec-specific defaults.
+ */
+ const AVCodecDefault *defaults;
+
+ /**
+ * Initialize codec static data, called from av_codec_iterate().
+ *
+ * This is not intended for time consuming operations as it is
+ * run for every codec regardless of that codec being used.
+ */
+ void (*init_static_data)(struct AVCodec *codec);
+
+ int (*init)(struct AVCodecContext *);
+ int (*encode_sub)(struct AVCodecContext *, uint8_t *buf, int buf_size,
+ const struct AVSubtitle *sub);
+ /**
+ * Encode data to an AVPacket.
+ *
+ * @param avctx codec context
+ * @param avpkt output AVPacket
+ * @param[in] frame AVFrame containing the raw data to be encoded
+ * @param[out] got_packet_ptr encoder sets to 0 or 1 to indicate that a
+ * non-empty packet was returned in avpkt.
+ * @return 0 on success, negative error code on failure
+ */
+ int (*encode2)(struct AVCodecContext *avctx, struct AVPacket *avpkt,
+ const struct AVFrame *frame, int *got_packet_ptr);
+ /**
+ * Decode picture or subtitle data.
+ *
+ * @param avctx codec context
+ * @param outdata codec type dependent output struct
+ * @param[out] got_frame_ptr decoder sets to 0 or 1 to indicate that a
+ * non-empty frame or subtitle was returned in
+ * outdata.
+ * @param[in] avpkt AVPacket containing the data to be decoded
+ * @return amount of bytes read from the packet on success, negative error
+ * code on failure
+ */
+ int (*decode)(struct AVCodecContext *avctx, void *outdata,
+ int *got_frame_ptr, struct AVPacket *avpkt);
+ int (*close)(struct AVCodecContext *);
+ /**
+ * Encode API with decoupled frame/packet dataflow. This function is called
+ * to get one output packet. It should call ff_encode_get_frame() to obtain
+ * input data.
+ */
+ int (*receive_packet)(struct AVCodecContext *avctx, struct AVPacket *avpkt);
+
+ /**
+ * Decode API with decoupled packet/frame dataflow. This function is called
+ * to get one output frame. It should call ff_decode_get_packet() to obtain
+ * input data.
+ */
+ int (*receive_frame)(struct AVCodecContext *avctx, struct AVFrame *frame);
+ /**
+ * Flush buffers.
+ * Will be called when seeking
+ */
+ void (*flush)(struct AVCodecContext *);
+ /**
+ * Internal codec capabilities.
+ * See FF_CODEC_CAP_* in internal.h
+ */
+ int caps_internal;
+
+ /**
+ * Decoding only, a comma-separated list of bitstream filters to apply to
+ * packets before decoding.
+ */
+ const char *bsfs;
+
+ /**
+ * Array of pointers to hardware configurations supported by the codec,
+ * or NULL if no hardware supported. The array is terminated by a NULL
+ * pointer.
+ *
+ * The user can only access this field via avcodec_get_hw_config().
+ */
+ const struct AVCodecHWConfigInternal *const *hw_configs;
+
+ /**
+ * List of supported codec_tags, terminated by FF_CODEC_TAGS_END.
+ */
+ const uint32_t *codec_tags;
+} AVCodec;
+
+/**
+ * Iterate over all registered codecs.
+ *
+ * @param opaque a pointer where libavcodec will store the iteration state. Must
+ * point to NULL to start the iteration.
+ *
+ * @return the next registered codec or NULL when the iteration is
+ * finished
+ */
+const AVCodec *av_codec_iterate(void **opaque);
+
+/**
+ * Find a registered decoder with a matching codec ID.
+ *
+ * @param id AVCodecID of the requested decoder
+ * @return A decoder if one was found, NULL otherwise.
+ */
+AVCodec *avcodec_find_decoder(enum AVCodecID id);
+
+/**
+ * Find a registered decoder with the specified name.
+ *
+ * @param name name of the requested decoder
+ * @return A decoder if one was found, NULL otherwise.
+ */
+AVCodec *avcodec_find_decoder_by_name(const char *name);
+
+/**
+ * Find a registered encoder with a matching codec ID.
+ *
+ * @param id AVCodecID of the requested encoder
+ * @return An encoder if one was found, NULL otherwise.
+ */
+AVCodec *avcodec_find_encoder(enum AVCodecID id);
+
+/**
+ * Find a registered encoder with the specified name.
+ *
+ * @param name name of the requested encoder
+ * @return An encoder if one was found, NULL otherwise.
+ */
+AVCodec *avcodec_find_encoder_by_name(const char *name);
+/**
+ * @return a non-zero number if codec is an encoder, zero otherwise
+ */
+int av_codec_is_encoder(const AVCodec *codec);
+
+/**
+ * @return a non-zero number if codec is a decoder, zero otherwise
+ */
+int av_codec_is_decoder(const AVCodec *codec);
+
+enum {
+ /**
+ * The codec supports this format via the hw_device_ctx interface.
+ *
+ * When selecting this format, AVCodecContext.hw_device_ctx should
+ * have been set to a device of the specified type before calling
+ * avcodec_open2().
+ */
+ AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX = 0x01,
+ /**
+ * The codec supports this format via the hw_frames_ctx interface.
+ *
+ * When selecting this format for a decoder,
+ * AVCodecContext.hw_frames_ctx should be set to a suitable frames
+ * context inside the get_format() callback. The frames context
+ * must have been created on a device of the specified type.
+ *
+ * When selecting this format for an encoder,
+ * AVCodecContext.hw_frames_ctx should be set to the context which
+ * will be used for the input frames before calling avcodec_open2().
+ */
+ AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX = 0x02,
+ /**
+ * The codec supports this format by some internal method.
+ *
+ * This format can be selected without any additional configuration -
+ * no device or frames context is required.
+ */
+ AV_CODEC_HW_CONFIG_METHOD_INTERNAL = 0x04,
+ /**
+ * The codec supports this format by some ad-hoc method.
+ *
+ * Additional settings and/or function calls are required. See the
+ * codec-specific documentation for details. (Methods requiring
+ * this sort of configuration are deprecated and others should be
+ * used in preference.)
+ */
+ AV_CODEC_HW_CONFIG_METHOD_AD_HOC = 0x08,
+};
+
+typedef struct AVCodecHWConfig {
+ /**
+ * For decoders, a hardware pixel format which that decoder may be
+ * able to decode to if suitable hardware is available.
+ *
+ * For encoders, a pixel format which the encoder may be able to
+ * accept. If set to AV_PIX_FMT_NONE, this applies to all pixel
+ * formats supported by the codec.
+ */
+ enum AVPixelFormat pix_fmt;
+ /**
+ * Bit set of AV_CODEC_HW_CONFIG_METHOD_* flags, describing the possible
+ * setup methods which can be used with this configuration.
+ */
+ int methods;
+ /**
+ * The device type associated with the configuration.
+ *
+ * Must be set for AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX and
+ * AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX, otherwise unused.
+ */
+ enum AVHWDeviceType device_type;
+} AVCodecHWConfig;
+
+/**
+ * Retrieve supported hardware configurations for a codec.
+ *
+ * Values of index from zero to some maximum return the indexed configuration
+ * descriptor; all other values return NULL. If the codec does not support
+ * any hardware configurations then it will always return NULL.
+ */
+const AVCodecHWConfig *avcodec_get_hw_config(const AVCodec *codec, int index);
+
+/**
+ * @}
+ */
+
+#endif /* AVCODEC_CODEC_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/codec_desc.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/codec_desc.h
new file mode 100644
index 0000000000..126b52df47
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/codec_desc.h
@@ -0,0 +1,128 @@
+/*
+ * Codec descriptors public API
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_CODEC_DESC_H
+#define AVCODEC_CODEC_DESC_H
+
+#include "libavutil/avutil.h"
+
+#include "codec_id.h"
+
+/**
+ * @addtogroup lavc_core
+ * @{
+ */
+
+/**
+ * This struct describes the properties of a single codec described by an
+ * AVCodecID.
+ * @see avcodec_descriptor_get()
+ */
+typedef struct AVCodecDescriptor {
+ enum AVCodecID id;
+ enum AVMediaType type;
+ /**
+ * Name of the codec described by this descriptor. It is non-empty and
+ * unique for each codec descriptor. It should contain alphanumeric
+ * characters and '_' only.
+ */
+ const char *name;
+ /**
+ * A more descriptive name for this codec. May be NULL.
+ */
+ const char *long_name;
+ /**
+ * Codec properties, a combination of AV_CODEC_PROP_* flags.
+ */
+ int props;
+ /**
+ * MIME type(s) associated with the codec.
+ * May be NULL; if not, a NULL-terminated array of MIME types.
+ * The first item is always non-NULL and is the preferred MIME type.
+ */
+ const char *const *mime_types;
+ /**
+ * If non-NULL, an array of profiles recognized for this codec.
+ * Terminated with FF_PROFILE_UNKNOWN.
+ */
+ const struct AVProfile *profiles;
+} AVCodecDescriptor;
+
+/**
+ * Codec uses only intra compression.
+ * Video and audio codecs only.
+ */
+#define AV_CODEC_PROP_INTRA_ONLY (1 << 0)
+/**
+ * Codec supports lossy compression. Audio and video codecs only.
+ * @note a codec may support both lossy and lossless
+ * compression modes
+ */
+#define AV_CODEC_PROP_LOSSY (1 << 1)
+/**
+ * Codec supports lossless compression. Audio and video codecs only.
+ */
+#define AV_CODEC_PROP_LOSSLESS (1 << 2)
+/**
+ * Codec supports frame reordering. That is, the coded order (the order in which
+ * the encoded packets are output by the encoders / stored / input to the
+ * decoders) may be different from the presentation order of the corresponding
+ * frames.
+ *
+ * For codecs that do not have this property set, PTS and DTS should always be
+ * equal.
+ */
+#define AV_CODEC_PROP_REORDER (1 << 3)
+/**
+ * Subtitle codec is bitmap based
+ * Decoded AVSubtitle data can be read from the AVSubtitleRect->pict field.
+ */
+#define AV_CODEC_PROP_BITMAP_SUB (1 << 16)
+/**
+ * Subtitle codec is text based.
+ * Decoded AVSubtitle data can be read from the AVSubtitleRect->ass field.
+ */
+#define AV_CODEC_PROP_TEXT_SUB (1 << 17)
+
+/**
+ * @return descriptor for given codec ID or NULL if no descriptor exists.
+ */
+const AVCodecDescriptor *avcodec_descriptor_get(enum AVCodecID id);
+
+/**
+ * Iterate over all codec descriptors known to libavcodec.
+ *
+ * @param prev previous descriptor. NULL to get the first descriptor.
+ *
+ * @return next descriptor or NULL after the last descriptor
+ */
+const AVCodecDescriptor *avcodec_descriptor_next(const AVCodecDescriptor *prev);
+
+/**
+ * @return codec descriptor with the given name or NULL if no such descriptor
+ * exists.
+ */
+const AVCodecDescriptor *avcodec_descriptor_get_by_name(const char *name);
+
+/**
+ * @}
+ */
+
+#endif // AVCODEC_CODEC_DESC_H
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/codec_id.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/codec_id.h
new file mode 100644
index 0000000000..ab265ec584
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/codec_id.h
@@ -0,0 +1,629 @@
+/*
+ * Codec IDs
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_CODEC_ID_H
+#define AVCODEC_CODEC_ID_H
+
+#include "libavutil/avutil.h"
+#include "libavutil/samplefmt.h"
+
+/**
+ * @addtogroup lavc_core
+ * @{
+ */
+
+/**
+ * Identify the syntax and semantics of the bitstream.
+ * The principle is roughly:
+ * Two decoders with the same ID can decode the same streams.
+ * Two encoders with the same ID can encode compatible streams.
+ * There may be slight deviations from the principle due to implementation
+ * details.
+ *
+ * If you add a codec ID to this list, add it so that
+ * 1. no value of an existing codec ID changes (that would break ABI),
+ * 2. it is as close as possible to similar codecs
+ *
+ * After adding new codec IDs, do not forget to add an entry to the codec
+ * descriptor list and bump libavcodec minor version.
+ */
+enum AVCodecID {
+ AV_CODEC_ID_NONE,
+
+ /* video codecs */
+ AV_CODEC_ID_MPEG1VIDEO,
+ AV_CODEC_ID_MPEG2VIDEO, ///< preferred ID for MPEG-1/2 video decoding
+ AV_CODEC_ID_H261,
+ AV_CODEC_ID_H263,
+ AV_CODEC_ID_RV10,
+ AV_CODEC_ID_RV20,
+ AV_CODEC_ID_MJPEG,
+ AV_CODEC_ID_MJPEGB,
+ AV_CODEC_ID_LJPEG,
+ AV_CODEC_ID_SP5X,
+ AV_CODEC_ID_JPEGLS,
+ AV_CODEC_ID_MPEG4,
+ AV_CODEC_ID_RAWVIDEO,
+ AV_CODEC_ID_MSMPEG4V1,
+ AV_CODEC_ID_MSMPEG4V2,
+ AV_CODEC_ID_MSMPEG4V3,
+ AV_CODEC_ID_WMV1,
+ AV_CODEC_ID_WMV2,
+ AV_CODEC_ID_H263P,
+ AV_CODEC_ID_H263I,
+ AV_CODEC_ID_FLV1,
+ AV_CODEC_ID_SVQ1,
+ AV_CODEC_ID_SVQ3,
+ AV_CODEC_ID_DVVIDEO,
+ AV_CODEC_ID_HUFFYUV,
+ AV_CODEC_ID_CYUV,
+ AV_CODEC_ID_H264,
+ AV_CODEC_ID_INDEO3,
+ AV_CODEC_ID_VP3,
+ AV_CODEC_ID_THEORA,
+ AV_CODEC_ID_ASV1,
+ AV_CODEC_ID_ASV2,
+ AV_CODEC_ID_FFV1,
+ AV_CODEC_ID_4XM,
+ AV_CODEC_ID_VCR1,
+ AV_CODEC_ID_CLJR,
+ AV_CODEC_ID_MDEC,
+ AV_CODEC_ID_ROQ,
+ AV_CODEC_ID_INTERPLAY_VIDEO,
+ AV_CODEC_ID_XAN_WC3,
+ AV_CODEC_ID_XAN_WC4,
+ AV_CODEC_ID_RPZA,
+ AV_CODEC_ID_CINEPAK,
+ AV_CODEC_ID_WS_VQA,
+ AV_CODEC_ID_MSRLE,
+ AV_CODEC_ID_MSVIDEO1,
+ AV_CODEC_ID_IDCIN,
+ AV_CODEC_ID_8BPS,
+ AV_CODEC_ID_SMC,
+ AV_CODEC_ID_FLIC,
+ AV_CODEC_ID_TRUEMOTION1,
+ AV_CODEC_ID_VMDVIDEO,
+ AV_CODEC_ID_MSZH,
+ AV_CODEC_ID_ZLIB,
+ AV_CODEC_ID_QTRLE,
+ AV_CODEC_ID_TSCC,
+ AV_CODEC_ID_ULTI,
+ AV_CODEC_ID_QDRAW,
+ AV_CODEC_ID_VIXL,
+ AV_CODEC_ID_QPEG,
+ AV_CODEC_ID_PNG,
+ AV_CODEC_ID_PPM,
+ AV_CODEC_ID_PBM,
+ AV_CODEC_ID_PGM,
+ AV_CODEC_ID_PGMYUV,
+ AV_CODEC_ID_PAM,
+ AV_CODEC_ID_FFVHUFF,
+ AV_CODEC_ID_RV30,
+ AV_CODEC_ID_RV40,
+ AV_CODEC_ID_VC1,
+ AV_CODEC_ID_WMV3,
+ AV_CODEC_ID_LOCO,
+ AV_CODEC_ID_WNV1,
+ AV_CODEC_ID_AASC,
+ AV_CODEC_ID_INDEO2,
+ AV_CODEC_ID_FRAPS,
+ AV_CODEC_ID_TRUEMOTION2,
+ AV_CODEC_ID_BMP,
+ AV_CODEC_ID_CSCD,
+ AV_CODEC_ID_MMVIDEO,
+ AV_CODEC_ID_ZMBV,
+ AV_CODEC_ID_AVS,
+ AV_CODEC_ID_SMACKVIDEO,
+ AV_CODEC_ID_NUV,
+ AV_CODEC_ID_KMVC,
+ AV_CODEC_ID_FLASHSV,
+ AV_CODEC_ID_CAVS,
+ AV_CODEC_ID_JPEG2000,
+ AV_CODEC_ID_VMNC,
+ AV_CODEC_ID_VP5,
+ AV_CODEC_ID_VP6,
+ AV_CODEC_ID_VP6F,
+ AV_CODEC_ID_TARGA,
+ AV_CODEC_ID_DSICINVIDEO,
+ AV_CODEC_ID_TIERTEXSEQVIDEO,
+ AV_CODEC_ID_TIFF,
+ AV_CODEC_ID_GIF,
+ AV_CODEC_ID_DXA,
+ AV_CODEC_ID_DNXHD,
+ AV_CODEC_ID_THP,
+ AV_CODEC_ID_SGI,
+ AV_CODEC_ID_C93,
+ AV_CODEC_ID_BETHSOFTVID,
+ AV_CODEC_ID_PTX,
+ AV_CODEC_ID_TXD,
+ AV_CODEC_ID_VP6A,
+ AV_CODEC_ID_AMV,
+ AV_CODEC_ID_VB,
+ AV_CODEC_ID_PCX,
+ AV_CODEC_ID_SUNRAST,
+ AV_CODEC_ID_INDEO4,
+ AV_CODEC_ID_INDEO5,
+ AV_CODEC_ID_MIMIC,
+ AV_CODEC_ID_RL2,
+ AV_CODEC_ID_ESCAPE124,
+ AV_CODEC_ID_DIRAC,
+ AV_CODEC_ID_BFI,
+ AV_CODEC_ID_CMV,
+ AV_CODEC_ID_MOTIONPIXELS,
+ AV_CODEC_ID_TGV,
+ AV_CODEC_ID_TGQ,
+ AV_CODEC_ID_TQI,
+ AV_CODEC_ID_AURA,
+ AV_CODEC_ID_AURA2,
+ AV_CODEC_ID_V210X,
+ AV_CODEC_ID_TMV,
+ AV_CODEC_ID_V210,
+ AV_CODEC_ID_DPX,
+ AV_CODEC_ID_MAD,
+ AV_CODEC_ID_FRWU,
+ AV_CODEC_ID_FLASHSV2,
+ AV_CODEC_ID_CDGRAPHICS,
+ AV_CODEC_ID_R210,
+ AV_CODEC_ID_ANM,
+ AV_CODEC_ID_BINKVIDEO,
+ AV_CODEC_ID_IFF_ILBM,
+#define AV_CODEC_ID_IFF_BYTERUN1 AV_CODEC_ID_IFF_ILBM
+ AV_CODEC_ID_KGV1,
+ AV_CODEC_ID_YOP,
+ AV_CODEC_ID_VP8,
+ AV_CODEC_ID_PICTOR,
+ AV_CODEC_ID_ANSI,
+ AV_CODEC_ID_A64_MULTI,
+ AV_CODEC_ID_A64_MULTI5,
+ AV_CODEC_ID_R10K,
+ AV_CODEC_ID_MXPEG,
+ AV_CODEC_ID_LAGARITH,
+ AV_CODEC_ID_PRORES,
+ AV_CODEC_ID_JV,
+ AV_CODEC_ID_DFA,
+ AV_CODEC_ID_WMV3IMAGE,
+ AV_CODEC_ID_VC1IMAGE,
+ AV_CODEC_ID_UTVIDEO,
+ AV_CODEC_ID_BMV_VIDEO,
+ AV_CODEC_ID_VBLE,
+ AV_CODEC_ID_DXTORY,
+ AV_CODEC_ID_V410,
+ AV_CODEC_ID_XWD,
+ AV_CODEC_ID_CDXL,
+ AV_CODEC_ID_XBM,
+ AV_CODEC_ID_ZEROCODEC,
+ AV_CODEC_ID_MSS1,
+ AV_CODEC_ID_MSA1,
+ AV_CODEC_ID_TSCC2,
+ AV_CODEC_ID_MTS2,
+ AV_CODEC_ID_CLLC,
+ AV_CODEC_ID_MSS2,
+ AV_CODEC_ID_VP9,
+ AV_CODEC_ID_AIC,
+ AV_CODEC_ID_ESCAPE130,
+ AV_CODEC_ID_G2M,
+ AV_CODEC_ID_WEBP,
+ AV_CODEC_ID_HNM4_VIDEO,
+ AV_CODEC_ID_HEVC,
+#define AV_CODEC_ID_H265 AV_CODEC_ID_HEVC
+ AV_CODEC_ID_FIC,
+ AV_CODEC_ID_ALIAS_PIX,
+ AV_CODEC_ID_BRENDER_PIX,
+ AV_CODEC_ID_PAF_VIDEO,
+ AV_CODEC_ID_EXR,
+ AV_CODEC_ID_VP7,
+ AV_CODEC_ID_SANM,
+ AV_CODEC_ID_SGIRLE,
+ AV_CODEC_ID_MVC1,
+ AV_CODEC_ID_MVC2,
+ AV_CODEC_ID_HQX,
+ AV_CODEC_ID_TDSC,
+ AV_CODEC_ID_HQ_HQA,
+ AV_CODEC_ID_HAP,
+ AV_CODEC_ID_DDS,
+ AV_CODEC_ID_DXV,
+ AV_CODEC_ID_SCREENPRESSO,
+ AV_CODEC_ID_RSCC,
+ AV_CODEC_ID_AVS2,
+ AV_CODEC_ID_PGX,
+ AV_CODEC_ID_AVS3,
+ AV_CODEC_ID_MSP2,
+ AV_CODEC_ID_VVC,
+#define AV_CODEC_ID_H266 AV_CODEC_ID_VVC
+ AV_CODEC_ID_Y41P,
+ AV_CODEC_ID_AVRP,
+ AV_CODEC_ID_012V,
+ AV_CODEC_ID_AVUI,
+ AV_CODEC_ID_AYUV,
+ AV_CODEC_ID_TARGA_Y216,
+ AV_CODEC_ID_V308,
+ AV_CODEC_ID_V408,
+ AV_CODEC_ID_YUV4,
+ AV_CODEC_ID_AVRN,
+ AV_CODEC_ID_CPIA,
+ AV_CODEC_ID_XFACE,
+ AV_CODEC_ID_SNOW,
+ AV_CODEC_ID_SMVJPEG,
+ AV_CODEC_ID_APNG,
+ AV_CODEC_ID_DAALA,
+ AV_CODEC_ID_CFHD,
+ AV_CODEC_ID_TRUEMOTION2RT,
+ AV_CODEC_ID_M101,
+ AV_CODEC_ID_MAGICYUV,
+ AV_CODEC_ID_SHEERVIDEO,
+ AV_CODEC_ID_YLC,
+ AV_CODEC_ID_PSD,
+ AV_CODEC_ID_PIXLET,
+ AV_CODEC_ID_SPEEDHQ,
+ AV_CODEC_ID_FMVC,
+ AV_CODEC_ID_SCPR,
+ AV_CODEC_ID_CLEARVIDEO,
+ AV_CODEC_ID_XPM,
+ AV_CODEC_ID_AV1,
+ AV_CODEC_ID_BITPACKED,
+ AV_CODEC_ID_MSCC,
+ AV_CODEC_ID_SRGC,
+ AV_CODEC_ID_SVG,
+ AV_CODEC_ID_GDV,
+ AV_CODEC_ID_FITS,
+ AV_CODEC_ID_IMM4,
+ AV_CODEC_ID_PROSUMER,
+ AV_CODEC_ID_MWSC,
+ AV_CODEC_ID_WCMV,
+ AV_CODEC_ID_RASC,
+ AV_CODEC_ID_HYMT,
+ AV_CODEC_ID_ARBC,
+ AV_CODEC_ID_AGM,
+ AV_CODEC_ID_LSCR,
+ AV_CODEC_ID_VP4,
+ AV_CODEC_ID_IMM5,
+ AV_CODEC_ID_MVDV,
+ AV_CODEC_ID_MVHA,
+ AV_CODEC_ID_CDTOONS,
+ AV_CODEC_ID_MV30,
+ AV_CODEC_ID_NOTCHLC,
+ AV_CODEC_ID_PFM,
+ AV_CODEC_ID_MOBICLIP,
+ AV_CODEC_ID_PHOTOCD,
+ AV_CODEC_ID_IPU,
+ AV_CODEC_ID_ARGO,
+ AV_CODEC_ID_CRI,
+ AV_CODEC_ID_SIMBIOSIS_IMX,
+ AV_CODEC_ID_SGA_VIDEO,
+ AV_CODEC_ID_GEM,
+
+ /* various PCM "codecs" */
+ AV_CODEC_ID_FIRST_AUDIO = 0x10000, ///< A dummy id pointing at the start of audio codecs
+ AV_CODEC_ID_PCM_S16LE = 0x10000,
+ AV_CODEC_ID_PCM_S16BE,
+ AV_CODEC_ID_PCM_U16LE,
+ AV_CODEC_ID_PCM_U16BE,
+ AV_CODEC_ID_PCM_S8,
+ AV_CODEC_ID_PCM_U8,
+ AV_CODEC_ID_PCM_MULAW,
+ AV_CODEC_ID_PCM_ALAW,
+ AV_CODEC_ID_PCM_S32LE,
+ AV_CODEC_ID_PCM_S32BE,
+ AV_CODEC_ID_PCM_U32LE,
+ AV_CODEC_ID_PCM_U32BE,
+ AV_CODEC_ID_PCM_S24LE,
+ AV_CODEC_ID_PCM_S24BE,
+ AV_CODEC_ID_PCM_U24LE,
+ AV_CODEC_ID_PCM_U24BE,
+ AV_CODEC_ID_PCM_S24DAUD,
+ AV_CODEC_ID_PCM_ZORK,
+ AV_CODEC_ID_PCM_S16LE_PLANAR,
+ AV_CODEC_ID_PCM_DVD,
+ AV_CODEC_ID_PCM_F32BE,
+ AV_CODEC_ID_PCM_F32LE,
+ AV_CODEC_ID_PCM_F64BE,
+ AV_CODEC_ID_PCM_F64LE,
+ AV_CODEC_ID_PCM_BLURAY,
+ AV_CODEC_ID_PCM_LXF,
+ AV_CODEC_ID_S302M,
+ AV_CODEC_ID_PCM_S8_PLANAR,
+ AV_CODEC_ID_PCM_S24LE_PLANAR,
+ AV_CODEC_ID_PCM_S32LE_PLANAR,
+ AV_CODEC_ID_PCM_S16BE_PLANAR,
+ AV_CODEC_ID_PCM_S64LE,
+ AV_CODEC_ID_PCM_S64BE,
+ AV_CODEC_ID_PCM_F16LE,
+ AV_CODEC_ID_PCM_F24LE,
+ AV_CODEC_ID_PCM_VIDC,
+ AV_CODEC_ID_PCM_SGA,
+
+ /* various ADPCM codecs */
+ AV_CODEC_ID_ADPCM_IMA_QT = 0x11000,
+ AV_CODEC_ID_ADPCM_IMA_WAV,
+ AV_CODEC_ID_ADPCM_IMA_DK3,
+ AV_CODEC_ID_ADPCM_IMA_DK4,
+ AV_CODEC_ID_ADPCM_IMA_WS,
+ AV_CODEC_ID_ADPCM_IMA_SMJPEG,
+ AV_CODEC_ID_ADPCM_MS,
+ AV_CODEC_ID_ADPCM_4XM,
+ AV_CODEC_ID_ADPCM_XA,
+ AV_CODEC_ID_ADPCM_ADX,
+ AV_CODEC_ID_ADPCM_EA,
+ AV_CODEC_ID_ADPCM_G726,
+ AV_CODEC_ID_ADPCM_CT,
+ AV_CODEC_ID_ADPCM_SWF,
+ AV_CODEC_ID_ADPCM_YAMAHA,
+ AV_CODEC_ID_ADPCM_SBPRO_4,
+ AV_CODEC_ID_ADPCM_SBPRO_3,
+ AV_CODEC_ID_ADPCM_SBPRO_2,
+ AV_CODEC_ID_ADPCM_THP,
+ AV_CODEC_ID_ADPCM_IMA_AMV,
+ AV_CODEC_ID_ADPCM_EA_R1,
+ AV_CODEC_ID_ADPCM_EA_R3,
+ AV_CODEC_ID_ADPCM_EA_R2,
+ AV_CODEC_ID_ADPCM_IMA_EA_SEAD,
+ AV_CODEC_ID_ADPCM_IMA_EA_EACS,
+ AV_CODEC_ID_ADPCM_EA_XAS,
+ AV_CODEC_ID_ADPCM_EA_MAXIS_XA,
+ AV_CODEC_ID_ADPCM_IMA_ISS,
+ AV_CODEC_ID_ADPCM_G722,
+ AV_CODEC_ID_ADPCM_IMA_APC,
+ AV_CODEC_ID_ADPCM_VIMA,
+ AV_CODEC_ID_ADPCM_AFC,
+ AV_CODEC_ID_ADPCM_IMA_OKI,
+ AV_CODEC_ID_ADPCM_DTK,
+ AV_CODEC_ID_ADPCM_IMA_RAD,
+ AV_CODEC_ID_ADPCM_G726LE,
+ AV_CODEC_ID_ADPCM_THP_LE,
+ AV_CODEC_ID_ADPCM_PSX,
+ AV_CODEC_ID_ADPCM_AICA,
+ AV_CODEC_ID_ADPCM_IMA_DAT4,
+ AV_CODEC_ID_ADPCM_MTAF,
+ AV_CODEC_ID_ADPCM_AGM,
+ AV_CODEC_ID_ADPCM_ARGO,
+ AV_CODEC_ID_ADPCM_IMA_SSI,
+ AV_CODEC_ID_ADPCM_ZORK,
+ AV_CODEC_ID_ADPCM_IMA_APM,
+ AV_CODEC_ID_ADPCM_IMA_ALP,
+ AV_CODEC_ID_ADPCM_IMA_MTF,
+ AV_CODEC_ID_ADPCM_IMA_CUNNING,
+ AV_CODEC_ID_ADPCM_IMA_MOFLEX,
+ AV_CODEC_ID_ADPCM_IMA_ACORN,
+
+ /* AMR */
+ AV_CODEC_ID_AMR_NB = 0x12000,
+ AV_CODEC_ID_AMR_WB,
+
+ /* RealAudio codecs*/
+ AV_CODEC_ID_RA_144 = 0x13000,
+ AV_CODEC_ID_RA_288,
+
+ /* various DPCM codecs */
+ AV_CODEC_ID_ROQ_DPCM = 0x14000,
+ AV_CODEC_ID_INTERPLAY_DPCM,
+ AV_CODEC_ID_XAN_DPCM,
+ AV_CODEC_ID_SOL_DPCM,
+ AV_CODEC_ID_SDX2_DPCM,
+ AV_CODEC_ID_GREMLIN_DPCM,
+ AV_CODEC_ID_DERF_DPCM,
+
+ /* audio codecs */
+ AV_CODEC_ID_MP2 = 0x15000,
+ AV_CODEC_ID_MP3, ///< preferred ID for decoding MPEG audio layer 1, 2 or 3
+ AV_CODEC_ID_AAC,
+ AV_CODEC_ID_AC3,
+ AV_CODEC_ID_DTS,
+ AV_CODEC_ID_VORBIS,
+ AV_CODEC_ID_DVAUDIO,
+ AV_CODEC_ID_WMAV1,
+ AV_CODEC_ID_WMAV2,
+ AV_CODEC_ID_MACE3,
+ AV_CODEC_ID_MACE6,
+ AV_CODEC_ID_VMDAUDIO,
+ AV_CODEC_ID_FLAC,
+ AV_CODEC_ID_MP3ADU,
+ AV_CODEC_ID_MP3ON4,
+ AV_CODEC_ID_SHORTEN,
+ AV_CODEC_ID_ALAC,
+ AV_CODEC_ID_WESTWOOD_SND1,
+ AV_CODEC_ID_GSM, ///< as in Berlin toast format
+ AV_CODEC_ID_QDM2,
+ AV_CODEC_ID_COOK,
+ AV_CODEC_ID_TRUESPEECH,
+ AV_CODEC_ID_TTA,
+ AV_CODEC_ID_SMACKAUDIO,
+ AV_CODEC_ID_QCELP,
+ AV_CODEC_ID_WAVPACK,
+ AV_CODEC_ID_DSICINAUDIO,
+ AV_CODEC_ID_IMC,
+ AV_CODEC_ID_MUSEPACK7,
+ AV_CODEC_ID_MLP,
+ AV_CODEC_ID_GSM_MS, /* as found in WAV */
+ AV_CODEC_ID_ATRAC3,
+ AV_CODEC_ID_APE,
+ AV_CODEC_ID_NELLYMOSER,
+ AV_CODEC_ID_MUSEPACK8,
+ AV_CODEC_ID_SPEEX,
+ AV_CODEC_ID_WMAVOICE,
+ AV_CODEC_ID_WMAPRO,
+ AV_CODEC_ID_WMALOSSLESS,
+ AV_CODEC_ID_ATRAC3P,
+ AV_CODEC_ID_EAC3,
+ AV_CODEC_ID_SIPR,
+ AV_CODEC_ID_MP1,
+ AV_CODEC_ID_TWINVQ,
+ AV_CODEC_ID_TRUEHD,
+ AV_CODEC_ID_MP4ALS,
+ AV_CODEC_ID_ATRAC1,
+ AV_CODEC_ID_BINKAUDIO_RDFT,
+ AV_CODEC_ID_BINKAUDIO_DCT,
+ AV_CODEC_ID_AAC_LATM,
+ AV_CODEC_ID_QDMC,
+ AV_CODEC_ID_CELT,
+ AV_CODEC_ID_G723_1,
+ AV_CODEC_ID_G729,
+ AV_CODEC_ID_8SVX_EXP,
+ AV_CODEC_ID_8SVX_FIB,
+ AV_CODEC_ID_BMV_AUDIO,
+ AV_CODEC_ID_RALF,
+ AV_CODEC_ID_IAC,
+ AV_CODEC_ID_ILBC,
+ AV_CODEC_ID_OPUS,
+ AV_CODEC_ID_COMFORT_NOISE,
+ AV_CODEC_ID_TAK,
+ AV_CODEC_ID_METASOUND,
+ AV_CODEC_ID_PAF_AUDIO,
+ AV_CODEC_ID_ON2AVC,
+ AV_CODEC_ID_DSS_SP,
+ AV_CODEC_ID_CODEC2,
+ AV_CODEC_ID_FFWAVESYNTH,
+ AV_CODEC_ID_SONIC,
+ AV_CODEC_ID_SONIC_LS,
+ AV_CODEC_ID_EVRC,
+ AV_CODEC_ID_SMV,
+ AV_CODEC_ID_DSD_LSBF,
+ AV_CODEC_ID_DSD_MSBF,
+ AV_CODEC_ID_DSD_LSBF_PLANAR,
+ AV_CODEC_ID_DSD_MSBF_PLANAR,
+ AV_CODEC_ID_4GV,
+ AV_CODEC_ID_INTERPLAY_ACM,
+ AV_CODEC_ID_XMA1,
+ AV_CODEC_ID_XMA2,
+ AV_CODEC_ID_DST,
+ AV_CODEC_ID_ATRAC3AL,
+ AV_CODEC_ID_ATRAC3PAL,
+ AV_CODEC_ID_DOLBY_E,
+ AV_CODEC_ID_APTX,
+ AV_CODEC_ID_APTX_HD,
+ AV_CODEC_ID_SBC,
+ AV_CODEC_ID_ATRAC9,
+ AV_CODEC_ID_HCOM,
+ AV_CODEC_ID_ACELP_KELVIN,
+ AV_CODEC_ID_MPEGH_3D_AUDIO,
+ AV_CODEC_ID_SIREN,
+ AV_CODEC_ID_HCA,
+ AV_CODEC_ID_FASTAUDIO,
+ AV_CODEC_ID_MSNSIREN,
+
+ /* subtitle codecs */
+ AV_CODEC_ID_FIRST_SUBTITLE = 0x17000, ///< A dummy ID pointing at the start of subtitle codecs.
+ AV_CODEC_ID_DVD_SUBTITLE = 0x17000,
+ AV_CODEC_ID_DVB_SUBTITLE,
+ AV_CODEC_ID_TEXT, ///< raw UTF-8 text
+ AV_CODEC_ID_XSUB,
+ AV_CODEC_ID_SSA,
+ AV_CODEC_ID_MOV_TEXT,
+ AV_CODEC_ID_HDMV_PGS_SUBTITLE,
+ AV_CODEC_ID_DVB_TELETEXT,
+ AV_CODEC_ID_SRT,
+ AV_CODEC_ID_MICRODVD,
+ AV_CODEC_ID_EIA_608,
+ AV_CODEC_ID_JACOSUB,
+ AV_CODEC_ID_SAMI,
+ AV_CODEC_ID_REALTEXT,
+ AV_CODEC_ID_STL,
+ AV_CODEC_ID_SUBVIEWER1,
+ AV_CODEC_ID_SUBVIEWER,
+ AV_CODEC_ID_SUBRIP,
+ AV_CODEC_ID_WEBVTT,
+ AV_CODEC_ID_MPL2,
+ AV_CODEC_ID_VPLAYER,
+ AV_CODEC_ID_PJS,
+ AV_CODEC_ID_ASS,
+ AV_CODEC_ID_HDMV_TEXT_SUBTITLE,
+ AV_CODEC_ID_TTML,
+ AV_CODEC_ID_ARIB_CAPTION,
+
+ /* other specific kind of codecs (generally used for attachments) */
+ AV_CODEC_ID_FIRST_UNKNOWN = 0x18000, ///< A dummy ID pointing at the start of various fake codecs.
+ AV_CODEC_ID_TTF = 0x18000,
+
+ AV_CODEC_ID_SCTE_35, ///< Contain timestamp estimated through PCR of program stream.
+ AV_CODEC_ID_EPG,
+ AV_CODEC_ID_BINTEXT,
+ AV_CODEC_ID_XBIN,
+ AV_CODEC_ID_IDF,
+ AV_CODEC_ID_OTF,
+ AV_CODEC_ID_SMPTE_KLV,
+ AV_CODEC_ID_DVD_NAV,
+ AV_CODEC_ID_TIMED_ID3,
+ AV_CODEC_ID_BIN_DATA,
+
+
+ AV_CODEC_ID_PROBE = 0x19000, ///< codec_id is not known (like AV_CODEC_ID_NONE) but lavf should attempt to identify it
+
+ AV_CODEC_ID_MPEG2TS = 0x20000, /**< _FAKE_ codec to indicate a raw MPEG-2 TS
+ * stream (only used by libavformat) */
+ AV_CODEC_ID_MPEG4SYSTEMS = 0x20001, /**< _FAKE_ codec to indicate a MPEG-4 Systems
+ * stream (only used by libavformat) */
+ AV_CODEC_ID_FFMETADATA = 0x21000, ///< Dummy codec for streams containing only metadata information.
+ AV_CODEC_ID_WRAPPED_AVFRAME = 0x21001, ///< Passthrough codec, AVFrames wrapped in AVPacket
+};
+
+/**
+ * Get the type of the given codec.
+ */
+enum AVMediaType avcodec_get_type(enum AVCodecID codec_id);
+
+/**
+ * Get the name of a codec.
+ * @return a static string identifying the codec; never NULL
+ */
+const char *avcodec_get_name(enum AVCodecID id);
+
+/**
+ * Return codec bits per sample.
+ *
+ * @param[in] codec_id the codec
+ * @return Number of bits per sample or zero if unknown for the given codec.
+ */
+int av_get_bits_per_sample(enum AVCodecID codec_id);
+
+/**
+ * Return codec bits per sample.
+ * Only return non-zero if the bits per sample is exactly correct, not an
+ * approximation.
+ *
+ * @param[in] codec_id the codec
+ * @return Number of bits per sample or zero if unknown for the given codec.
+ */
+int av_get_exact_bits_per_sample(enum AVCodecID codec_id);
+
+/**
+ * Return a name for the specified profile, if available.
+ *
+ * @param codec_id the ID of the codec to which the requested profile belongs
+ * @param profile the profile value for which a name is requested
+ * @return A name for the profile if found, NULL otherwise.
+ *
+ * @note unlike av_get_profile_name(), which searches a list of profiles
+ * supported by a specific decoder or encoder implementation, this
+ * function searches the list of profiles from the AVCodecDescriptor
+ */
+const char *avcodec_profile_name(enum AVCodecID codec_id, int profile);
+
+/**
+ * Return the PCM codec associated with a sample format.
+ * @param be endianness, 0 for little, 1 for big,
+ * -1 (or anything else) for native
+ * @return AV_CODEC_ID_PCM_* or AV_CODEC_ID_NONE
+ */
+enum AVCodecID av_get_pcm_codec(enum AVSampleFormat fmt, int be);
+
+/**
+ * @}
+ */
+
+#endif // AVCODEC_CODEC_ID_H
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/codec_par.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/codec_par.h
new file mode 100644
index 0000000000..10cf79dff1
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/codec_par.h
@@ -0,0 +1,234 @@
+/*
+ * Codec parameters public API
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_CODEC_PAR_H
+#define AVCODEC_CODEC_PAR_H
+
+#include <stdint.h>
+
+#include "libavutil/avutil.h"
+#include "libavutil/rational.h"
+#include "libavutil/pixfmt.h"
+
+#include "codec_id.h"
+
+/**
+ * @addtogroup lavc_core
+ */
+
+enum AVFieldOrder {
+ AV_FIELD_UNKNOWN,
+ AV_FIELD_PROGRESSIVE,
+ AV_FIELD_TT, //< Top coded_first, top displayed first
+ AV_FIELD_BB, //< Bottom coded first, bottom displayed first
+ AV_FIELD_TB, //< Top coded first, bottom displayed first
+ AV_FIELD_BT, //< Bottom coded first, top displayed first
+};
+
+/**
+ * This struct describes the properties of an encoded stream.
+ *
+ * sizeof(AVCodecParameters) is not a part of the public ABI, this struct must
+ * be allocated with avcodec_parameters_alloc() and freed with
+ * avcodec_parameters_free().
+ */
+typedef struct AVCodecParameters {
+ /**
+ * General type of the encoded data.
+ */
+ enum AVMediaType codec_type;
+ /**
+ * Specific type of the encoded data (the codec used).
+ */
+ enum AVCodecID codec_id;
+ /**
+ * Additional information about the codec (corresponds to the AVI FOURCC).
+ */
+ uint32_t codec_tag;
+
+ /**
+ * Extra binary data needed for initializing the decoder, codec-dependent.
+ *
+ * Must be allocated with av_malloc() and will be freed by
+ * avcodec_parameters_free(). The allocated size of extradata must be at
+ * least extradata_size + AV_INPUT_BUFFER_PADDING_SIZE, with the padding
+ * bytes zeroed.
+ */
+ uint8_t *extradata;
+ /**
+ * Size of the extradata content in bytes.
+ */
+ int extradata_size;
+
+ /**
+ * - video: the pixel format, the value corresponds to enum AVPixelFormat.
+ * - audio: the sample format, the value corresponds to enum AVSampleFormat.
+ */
+ int format;
+
+ /**
+ * The average bitrate of the encoded data (in bits per second).
+ */
+ int64_t bit_rate;
+
+ /**
+ * The number of bits per sample in the codedwords.
+ *
+ * This is basically the bitrate per sample. It is mandatory for a bunch of
+ * formats to actually decode them. It's the number of bits for one sample in
+ * the actual coded bitstream.
+ *
+ * This could be for example 4 for ADPCM
+ * For PCM formats this matches bits_per_raw_sample
+ * Can be 0
+ */
+ int bits_per_coded_sample;
+
+ /**
+ * This is the number of valid bits in each output sample. If the
+ * sample format has more bits, the least significant bits are additional
+ * padding bits, which are always 0. Use right shifts to reduce the sample
+ * to its actual size. For example, audio formats with 24 bit samples will
+ * have bits_per_raw_sample set to 24, and format set to AV_SAMPLE_FMT_S32.
+ * To get the original sample use "(int32_t)sample >> 8"."
+ *
+ * For ADPCM this might be 12 or 16 or similar
+ * Can be 0
+ */
+ int bits_per_raw_sample;
+
+ /**
+ * Codec-specific bitstream restrictions that the stream conforms to.
+ */
+ int profile;
+ int level;
+
+ /**
+ * Video only. The dimensions of the video frame in pixels.
+ */
+ int width;
+ int height;
+
+ /**
+ * Video only. The aspect ratio (width / height) which a single pixel
+ * should have when displayed.
+ *
+ * When the aspect ratio is unknown / undefined, the numerator should be
+ * set to 0 (the denominator may have any value).
+ */
+ AVRational sample_aspect_ratio;
+
+ /**
+ * Video only. The order of the fields in interlaced video.
+ */
+ enum AVFieldOrder field_order;
+
+ /**
+ * Video only. Additional colorspace characteristics.
+ */
+ enum AVColorRange color_range;
+ enum AVColorPrimaries color_primaries;
+ enum AVColorTransferCharacteristic color_trc;
+ enum AVColorSpace color_space;
+ enum AVChromaLocation chroma_location;
+
+ /**
+ * Video only. Number of delayed frames.
+ */
+ int video_delay;
+
+ /**
+ * Audio only. The channel layout bitmask. May be 0 if the channel layout is
+ * unknown or unspecified, otherwise the number of bits set must be equal to
+ * the channels field.
+ */
+ uint64_t channel_layout;
+ /**
+ * Audio only. The number of audio channels.
+ */
+ int channels;
+ /**
+ * Audio only. The number of audio samples per second.
+ */
+ int sample_rate;
+ /**
+ * Audio only. The number of bytes per coded audio frame, required by some
+ * formats.
+ *
+ * Corresponds to nBlockAlign in WAVEFORMATEX.
+ */
+ int block_align;
+ /**
+ * Audio only. Audio frame size, if known. Required by some formats to be static.
+ */
+ int frame_size;
+
+ /**
+ * Audio only. The amount of padding (in samples) inserted by the encoder at
+ * the beginning of the audio. I.e. this number of leading decoded samples
+ * must be discarded by the caller to get the original audio without leading
+ * padding.
+ */
+ int initial_padding;
+ /**
+ * Audio only. The amount of padding (in samples) appended by the encoder to
+ * the end of the audio. I.e. this number of decoded samples must be
+ * discarded by the caller from the end of the stream to get the original
+ * audio without any trailing padding.
+ */
+ int trailing_padding;
+ /**
+ * Audio only. Number of samples to skip after a discontinuity.
+ */
+ int seek_preroll;
+} AVCodecParameters;
+
+/**
+ * Allocate a new AVCodecParameters and set its fields to default values
+ * (unknown/invalid/0). The returned struct must be freed with
+ * avcodec_parameters_free().
+ */
+AVCodecParameters *avcodec_parameters_alloc(void);
+
+/**
+ * Free an AVCodecParameters instance and everything associated with it and
+ * write NULL to the supplied pointer.
+ */
+void avcodec_parameters_free(AVCodecParameters **par);
+
+/**
+ * Copy the contents of src to dst. Any allocated fields in dst are freed and
+ * replaced with newly allocated duplicates of the corresponding fields in src.
+ *
+ * @return >= 0 on success, a negative AVERROR code on failure.
+ */
+int avcodec_parameters_copy(AVCodecParameters *dst, const AVCodecParameters *src);
+
+/**
+ * This function is the same as av_get_audio_frame_duration(), except it works
+ * with AVCodecParameters instead of an AVCodecContext.
+ */
+int av_get_audio_frame_duration2(AVCodecParameters *par, int frame_bytes);
+
+/**
+ * @}
+ */
+
+#endif // AVCODEC_CODEC_PAR_H
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/packet.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/packet.h
new file mode 100644
index 0000000000..ca18ae631f
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/packet.h
@@ -0,0 +1,774 @@
+/*
+ * AVPacket public API
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_PACKET_H
+#define AVCODEC_PACKET_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "libavutil/attributes.h"
+#include "libavutil/buffer.h"
+#include "libavutil/dict.h"
+#include "libavutil/rational.h"
+
+#include "libavcodec/version.h"
+
+/**
+ * @defgroup lavc_packet AVPacket
+ *
+ * Types and functions for working with AVPacket.
+ * @{
+ */
+enum AVPacketSideDataType {
+ /**
+ * An AV_PKT_DATA_PALETTE side data packet contains exactly AVPALETTE_SIZE
+ * bytes worth of palette. This side data signals that a new palette is
+ * present.
+ */
+ AV_PKT_DATA_PALETTE,
+
+ /**
+ * The AV_PKT_DATA_NEW_EXTRADATA is used to notify the codec or the format
+ * that the extradata buffer was changed and the receiving side should
+ * act upon it appropriately. The new extradata is embedded in the side
+ * data buffer and should be immediately used for processing the current
+ * frame or packet.
+ */
+ AV_PKT_DATA_NEW_EXTRADATA,
+
+ /**
+ * An AV_PKT_DATA_PARAM_CHANGE side data packet is laid out as follows:
+ * @code
+ * u32le param_flags
+ * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_COUNT)
+ * s32le channel_count
+ * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_LAYOUT)
+ * u64le channel_layout
+ * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_SAMPLE_RATE)
+ * s32le sample_rate
+ * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_DIMENSIONS)
+ * s32le width
+ * s32le height
+ * @endcode
+ */
+ AV_PKT_DATA_PARAM_CHANGE,
+
+ /**
+ * An AV_PKT_DATA_H263_MB_INFO side data packet contains a number of
+ * structures with info about macroblocks relevant to splitting the
+ * packet into smaller packets on macroblock edges (e.g. as for RFC 2190).
+ * That is, it does not necessarily contain info about all macroblocks,
+ * as long as the distance between macroblocks in the info is smaller
+ * than the target payload size.
+ * Each MB info structure is 12 bytes, and is laid out as follows:
+ * @code
+ * u32le bit offset from the start of the packet
+ * u8 current quantizer at the start of the macroblock
+ * u8 GOB number
+ * u16le macroblock address within the GOB
+ * u8 horizontal MV predictor
+ * u8 vertical MV predictor
+ * u8 horizontal MV predictor for block number 3
+ * u8 vertical MV predictor for block number 3
+ * @endcode
+ */
+ AV_PKT_DATA_H263_MB_INFO,
+
+ /**
+ * This side data should be associated with an audio stream and contains
+ * ReplayGain information in form of the AVReplayGain struct.
+ */
+ AV_PKT_DATA_REPLAYGAIN,
+
+ /**
+ * This side data contains a 3x3 transformation matrix describing an affine
+ * transformation that needs to be applied to the decoded video frames for
+ * correct presentation.
+ *
+ * See libavutil/display.h for a detailed description of the data.
+ */
+ AV_PKT_DATA_DISPLAYMATRIX,
+
+ /**
+ * This side data should be associated with a video stream and contains
+ * Stereoscopic 3D information in form of the AVStereo3D struct.
+ */
+ AV_PKT_DATA_STEREO3D,
+
+ /**
+ * This side data should be associated with an audio stream and corresponds
+ * to enum AVAudioServiceType.
+ */
+ AV_PKT_DATA_AUDIO_SERVICE_TYPE,
+
+ /**
+ * This side data contains quality related information from the encoder.
+ * @code
+ * u32le quality factor of the compressed frame. Allowed range is between 1 (good) and FF_LAMBDA_MAX (bad).
+ * u8 picture type
+ * u8 error count
+ * u16 reserved
+ * u64le[error count] sum of squared differences between encoder in and output
+ * @endcode
+ */
+ AV_PKT_DATA_QUALITY_STATS,
+
+ /**
+ * This side data contains an integer value representing the stream index
+ * of a "fallback" track. A fallback track indicates an alternate
+ * track to use when the current track can not be decoded for some reason.
+ * e.g. no decoder available for codec.
+ */
+ AV_PKT_DATA_FALLBACK_TRACK,
+
+ /**
+ * This side data corresponds to the AVCPBProperties struct.
+ */
+ AV_PKT_DATA_CPB_PROPERTIES,
+
+ /**
+ * Recommmends skipping the specified number of samples
+ * @code
+ * u32le number of samples to skip from start of this packet
+ * u32le number of samples to skip from end of this packet
+ * u8 reason for start skip
+ * u8 reason for end skip (0=padding silence, 1=convergence)
+ * @endcode
+ */
+ AV_PKT_DATA_SKIP_SAMPLES,
+
+ /**
+ * An AV_PKT_DATA_JP_DUALMONO side data packet indicates that
+ * the packet may contain "dual mono" audio specific to Japanese DTV
+ * and if it is true, recommends only the selected channel to be used.
+ * @code
+ * u8 selected channels (0=mail/left, 1=sub/right, 2=both)
+ * @endcode
+ */
+ AV_PKT_DATA_JP_DUALMONO,
+
+ /**
+ * A list of zero terminated key/value strings. There is no end marker for
+ * the list, so it is required to rely on the side data size to stop.
+ */
+ AV_PKT_DATA_STRINGS_METADATA,
+
+ /**
+ * Subtitle event position
+ * @code
+ * u32le x1
+ * u32le y1
+ * u32le x2
+ * u32le y2
+ * @endcode
+ */
+ AV_PKT_DATA_SUBTITLE_POSITION,
+
+ /**
+ * Data found in BlockAdditional element of matroska container. There is
+ * no end marker for the data, so it is required to rely on the side data
+ * size to recognize the end. 8 byte id (as found in BlockAddId) followed
+ * by data.
+ */
+ AV_PKT_DATA_MATROSKA_BLOCKADDITIONAL,
+
+ /**
+ * The optional first identifier line of a WebVTT cue.
+ */
+ AV_PKT_DATA_WEBVTT_IDENTIFIER,
+
+ /**
+ * The optional settings (rendering instructions) that immediately
+ * follow the timestamp specifier of a WebVTT cue.
+ */
+ AV_PKT_DATA_WEBVTT_SETTINGS,
+
+ /**
+ * A list of zero terminated key/value strings. There is no end marker for
+ * the list, so it is required to rely on the side data size to stop. This
+ * side data includes updated metadata which appeared in the stream.
+ */
+ AV_PKT_DATA_METADATA_UPDATE,
+
+ /**
+ * MPEGTS stream ID as uint8_t, this is required to pass the stream ID
+ * information from the demuxer to the corresponding muxer.
+ */
+ AV_PKT_DATA_MPEGTS_STREAM_ID,
+
+ /**
+ * Mastering display metadata (based on SMPTE-2086:2014). This metadata
+ * should be associated with a video stream and contains data in the form
+ * of the AVMasteringDisplayMetadata struct.
+ */
+ AV_PKT_DATA_MASTERING_DISPLAY_METADATA,
+
+ /**
+ * This side data should be associated with a video stream and corresponds
+ * to the AVSphericalMapping structure.
+ */
+ AV_PKT_DATA_SPHERICAL,
+
+ /**
+ * Content light level (based on CTA-861.3). This metadata should be
+ * associated with a video stream and contains data in the form of the
+ * AVContentLightMetadata struct.
+ */
+ AV_PKT_DATA_CONTENT_LIGHT_LEVEL,
+
+ /**
+ * ATSC A53 Part 4 Closed Captions. This metadata should be associated with
+ * a video stream. A53 CC bitstream is stored as uint8_t in AVPacketSideData.data.
+ * The number of bytes of CC data is AVPacketSideData.size.
+ */
+ AV_PKT_DATA_A53_CC,
+
+ /**
+ * This side data is encryption initialization data.
+ * The format is not part of ABI, use av_encryption_init_info_* methods to
+ * access.
+ */
+ AV_PKT_DATA_ENCRYPTION_INIT_INFO,
+
+ /**
+ * This side data contains encryption info for how to decrypt the packet.
+ * The format is not part of ABI, use av_encryption_info_* methods to access.
+ */
+ AV_PKT_DATA_ENCRYPTION_INFO,
+
+ /**
+ * Active Format Description data consisting of a single byte as specified
+ * in ETSI TS 101 154 using AVActiveFormatDescription enum.
+ */
+ AV_PKT_DATA_AFD,
+
+ /**
+ * Producer Reference Time data corresponding to the AVProducerReferenceTime struct,
+ * usually exported by some encoders (on demand through the prft flag set in the
+ * AVCodecContext export_side_data field).
+ */
+ AV_PKT_DATA_PRFT,
+
+ /**
+ * ICC profile data consisting of an opaque octet buffer following the
+ * format described by ISO 15076-1.
+ */
+ AV_PKT_DATA_ICC_PROFILE,
+
+ /**
+ * DOVI configuration
+ * ref:
+ * dolby-vision-bitstreams-within-the-iso-base-media-file-format-v2.1.2, section 2.2
+ * dolby-vision-bitstreams-in-mpeg-2-transport-stream-multiplex-v1.2, section 3.3
+ * Tags are stored in struct AVDOVIDecoderConfigurationRecord.
+ */
+ AV_PKT_DATA_DOVI_CONF,
+
+ /**
+ * Timecode which conforms to SMPTE ST 12-1:2014. The data is an array of 4 uint32_t
+ * where the first uint32_t describes how many (1-3) of the other timecodes are used.
+ * The timecode format is described in the documentation of av_timecode_get_smpte_from_framenum()
+ * function in libavutil/timecode.h.
+ */
+ AV_PKT_DATA_S12M_TIMECODE,
+
+ /**
+ * The number of side data types.
+ * This is not part of the public API/ABI in the sense that it may
+ * change when new side data types are added.
+ * This must stay the last enum value.
+ * If its value becomes huge, some code using it
+ * needs to be updated as it assumes it to be smaller than other limits.
+ */
+ AV_PKT_DATA_NB
+};
+
+#define AV_PKT_DATA_QUALITY_FACTOR AV_PKT_DATA_QUALITY_STATS //DEPRECATED
+
+typedef struct AVPacketSideData {
+ uint8_t *data;
+#if FF_API_BUFFER_SIZE_T
+ int size;
+#else
+ size_t size;
+#endif
+ enum AVPacketSideDataType type;
+} AVPacketSideData;
+
+/**
+ * This structure stores compressed data. It is typically exported by demuxers
+ * and then passed as input to decoders, or received as output from encoders and
+ * then passed to muxers.
+ *
+ * For video, it should typically contain one compressed frame. For audio it may
+ * contain several compressed frames. Encoders are allowed to output empty
+ * packets, with no compressed data, containing only side data
+ * (e.g. to update some stream parameters at the end of encoding).
+ *
+ * The semantics of data ownership depends on the buf field.
+ * If it is set, the packet data is dynamically allocated and is
+ * valid indefinitely until a call to av_packet_unref() reduces the
+ * reference count to 0.
+ *
+ * If the buf field is not set av_packet_ref() would make a copy instead
+ * of increasing the reference count.
+ *
+ * The side data is always allocated with av_malloc(), copied by
+ * av_packet_ref() and freed by av_packet_unref().
+ *
+ * sizeof(AVPacket) being a part of the public ABI is deprecated. once
+ * av_init_packet() is removed, new packets will only be able to be allocated
+ * with av_packet_alloc(), and new fields may be added to the end of the struct
+ * with a minor bump.
+ *
+ * @see av_packet_alloc
+ * @see av_packet_ref
+ * @see av_packet_unref
+ */
+typedef struct AVPacket {
+ /**
+ * A reference to the reference-counted buffer where the packet data is
+ * stored.
+ * May be NULL, then the packet data is not reference-counted.
+ */
+ AVBufferRef *buf;
+ /**
+ * Presentation timestamp in AVStream->time_base units; the time at which
+ * the decompressed packet will be presented to the user.
+ * Can be AV_NOPTS_VALUE if it is not stored in the file.
+ * pts MUST be larger or equal to dts as presentation cannot happen before
+ * decompression, unless one wants to view hex dumps. Some formats misuse
+ * the terms dts and pts/cts to mean something different. Such timestamps
+ * must be converted to true pts/dts before they are stored in AVPacket.
+ */
+ int64_t pts;
+ /**
+ * Decompression timestamp in AVStream->time_base units; the time at which
+ * the packet is decompressed.
+ * Can be AV_NOPTS_VALUE if it is not stored in the file.
+ */
+ int64_t dts;
+ uint8_t *data;
+ int size;
+ int stream_index;
+ /**
+ * A combination of AV_PKT_FLAG values
+ */
+ int flags;
+ /**
+ * Additional packet data that can be provided by the container.
+ * Packet can contain several types of side information.
+ */
+ AVPacketSideData *side_data;
+ int side_data_elems;
+
+ /**
+ * Duration of this packet in AVStream->time_base units, 0 if unknown.
+ * Equals next_pts - this_pts in presentation order.
+ */
+ int64_t duration;
+
+ int64_t pos; ///< byte position in stream, -1 if unknown
+
+#if FF_API_CONVERGENCE_DURATION
+ /**
+ * @deprecated Same as the duration field, but as int64_t. This was required
+ * for Matroska subtitles, whose duration values could overflow when the
+ * duration field was still an int.
+ */
+ attribute_deprecated
+ int64_t convergence_duration;
+#endif
+} AVPacket;
+
+#if FF_API_INIT_PACKET
+attribute_deprecated
+typedef struct AVPacketList {
+ AVPacket pkt;
+ struct AVPacketList *next;
+} AVPacketList;
+#endif
+
+#define AV_PKT_FLAG_KEY 0x0001 ///< The packet contains a keyframe
+#define AV_PKT_FLAG_CORRUPT 0x0002 ///< The packet content is corrupted
+/**
+ * Flag is used to discard packets which are required to maintain valid
+ * decoder state but are not required for output and should be dropped
+ * after decoding.
+ **/
+#define AV_PKT_FLAG_DISCARD 0x0004
+/**
+ * The packet comes from a trusted source.
+ *
+ * Otherwise-unsafe constructs such as arbitrary pointers to data
+ * outside the packet may be followed.
+ */
+#define AV_PKT_FLAG_TRUSTED 0x0008
+/**
+ * Flag is used to indicate packets that contain frames that can
+ * be discarded by the decoder. I.e. Non-reference frames.
+ */
+#define AV_PKT_FLAG_DISPOSABLE 0x0010
+
+enum AVSideDataParamChangeFlags {
+ AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_COUNT = 0x0001,
+ AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_LAYOUT = 0x0002,
+ AV_SIDE_DATA_PARAM_CHANGE_SAMPLE_RATE = 0x0004,
+ AV_SIDE_DATA_PARAM_CHANGE_DIMENSIONS = 0x0008,
+};
+
+/**
+ * Allocate an AVPacket and set its fields to default values. The resulting
+ * struct must be freed using av_packet_free().
+ *
+ * @return An AVPacket filled with default values or NULL on failure.
+ *
+ * @note this only allocates the AVPacket itself, not the data buffers. Those
+ * must be allocated through other means such as av_new_packet.
+ *
+ * @see av_new_packet
+ */
+AVPacket *av_packet_alloc(void);
+
+/**
+ * Create a new packet that references the same data as src.
+ *
+ * This is a shortcut for av_packet_alloc()+av_packet_ref().
+ *
+ * @return newly created AVPacket on success, NULL on error.
+ *
+ * @see av_packet_alloc
+ * @see av_packet_ref
+ */
+AVPacket *av_packet_clone(const AVPacket *src);
+
+/**
+ * Free the packet, if the packet is reference counted, it will be
+ * unreferenced first.
+ *
+ * @param pkt packet to be freed. The pointer will be set to NULL.
+ * @note passing NULL is a no-op.
+ */
+void av_packet_free(AVPacket **pkt);
+
+#if FF_API_INIT_PACKET
+/**
+ * Initialize optional fields of a packet with default values.
+ *
+ * Note, this does not touch the data and size members, which have to be
+ * initialized separately.
+ *
+ * @param pkt packet
+ *
+ * @see av_packet_alloc
+ * @see av_packet_unref
+ *
+ * @deprecated This function is deprecated. Once it's removed,
+ sizeof(AVPacket) will not be a part of the ABI anymore.
+ */
+attribute_deprecated
+void av_init_packet(AVPacket *pkt);
+#endif
+
+/**
+ * Allocate the payload of a packet and initialize its fields with
+ * default values.
+ *
+ * @param pkt packet
+ * @param size wanted payload size
+ * @return 0 if OK, AVERROR_xxx otherwise
+ */
+int av_new_packet(AVPacket *pkt, int size);
+
+/**
+ * Reduce packet size, correctly zeroing padding
+ *
+ * @param pkt packet
+ * @param size new size
+ */
+void av_shrink_packet(AVPacket *pkt, int size);
+
+/**
+ * Increase packet size, correctly zeroing padding
+ *
+ * @param pkt packet
+ * @param grow_by number of bytes by which to increase the size of the packet
+ */
+int av_grow_packet(AVPacket *pkt, int grow_by);
+
+/**
+ * Initialize a reference-counted packet from av_malloc()ed data.
+ *
+ * @param pkt packet to be initialized. This function will set the data, size,
+ * and buf fields, all others are left untouched.
+ * @param data Data allocated by av_malloc() to be used as packet data. If this
+ * function returns successfully, the data is owned by the underlying AVBuffer.
+ * The caller may not access the data through other means.
+ * @param size size of data in bytes, without the padding. I.e. the full buffer
+ * size is assumed to be size + AV_INPUT_BUFFER_PADDING_SIZE.
+ *
+ * @return 0 on success, a negative AVERROR on error
+ */
+int av_packet_from_data(AVPacket *pkt, uint8_t *data, int size);
+
+#if FF_API_AVPACKET_OLD_API
+/**
+ * @warning This is a hack - the packet memory allocation stuff is broken. The
+ * packet is allocated if it was not really allocated.
+ *
+ * @deprecated Use av_packet_ref or av_packet_make_refcounted
+ */
+attribute_deprecated
+int av_dup_packet(AVPacket *pkt);
+/**
+ * Copy packet, including contents
+ *
+ * @return 0 on success, negative AVERROR on fail
+ *
+ * @deprecated Use av_packet_ref
+ */
+attribute_deprecated
+int av_copy_packet(AVPacket *dst, const AVPacket *src);
+
+/**
+ * Copy packet side data
+ *
+ * @return 0 on success, negative AVERROR on fail
+ *
+ * @deprecated Use av_packet_copy_props
+ */
+attribute_deprecated
+int av_copy_packet_side_data(AVPacket *dst, const AVPacket *src);
+
+/**
+ * Free a packet.
+ *
+ * @deprecated Use av_packet_unref
+ *
+ * @param pkt packet to free
+ */
+attribute_deprecated
+void av_free_packet(AVPacket *pkt);
+#endif
+/**
+ * Allocate new information of a packet.
+ *
+ * @param pkt packet
+ * @param type side information type
+ * @param size side information size
+ * @return pointer to fresh allocated data or NULL otherwise
+ */
+uint8_t* av_packet_new_side_data(AVPacket *pkt, enum AVPacketSideDataType type,
+#if FF_API_BUFFER_SIZE_T
+ int size);
+#else
+ size_t size);
+#endif
+
+/**
+ * Wrap an existing array as a packet side data.
+ *
+ * @param pkt packet
+ * @param type side information type
+ * @param data the side data array. It must be allocated with the av_malloc()
+ * family of functions. The ownership of the data is transferred to
+ * pkt.
+ * @param size side information size
+ * @return a non-negative number on success, a negative AVERROR code on
+ * failure. On failure, the packet is unchanged and the data remains
+ * owned by the caller.
+ */
+int av_packet_add_side_data(AVPacket *pkt, enum AVPacketSideDataType type,
+ uint8_t *data, size_t size);
+
+/**
+ * Shrink the already allocated side data buffer
+ *
+ * @param pkt packet
+ * @param type side information type
+ * @param size new side information size
+ * @return 0 on success, < 0 on failure
+ */
+int av_packet_shrink_side_data(AVPacket *pkt, enum AVPacketSideDataType type,
+#if FF_API_BUFFER_SIZE_T
+ int size);
+#else
+ size_t size);
+#endif
+
+/**
+ * Get side information from packet.
+ *
+ * @param pkt packet
+ * @param type desired side information type
+ * @param size If supplied, *size will be set to the size of the side data
+ * or to zero if the desired side data is not present.
+ * @return pointer to data if present or NULL otherwise
+ */
+uint8_t* av_packet_get_side_data(const AVPacket *pkt, enum AVPacketSideDataType type,
+#if FF_API_BUFFER_SIZE_T
+ int *size);
+#else
+ size_t *size);
+#endif
+
+#if FF_API_MERGE_SD_API
+attribute_deprecated
+int av_packet_merge_side_data(AVPacket *pkt);
+
+attribute_deprecated
+int av_packet_split_side_data(AVPacket *pkt);
+#endif
+
+const char *av_packet_side_data_name(enum AVPacketSideDataType type);
+
+/**
+ * Pack a dictionary for use in side_data.
+ *
+ * @param dict The dictionary to pack.
+ * @param size pointer to store the size of the returned data
+ * @return pointer to data if successful, NULL otherwise
+ */
+#if FF_API_BUFFER_SIZE_T
+uint8_t *av_packet_pack_dictionary(AVDictionary *dict, int *size);
+#else
+uint8_t *av_packet_pack_dictionary(AVDictionary *dict, size_t *size);
+#endif
+/**
+ * Unpack a dictionary from side_data.
+ *
+ * @param data data from side_data
+ * @param size size of the data
+ * @param dict the metadata storage dictionary
+ * @return 0 on success, < 0 on failure
+ */
+#if FF_API_BUFFER_SIZE_T
+int av_packet_unpack_dictionary(const uint8_t *data, int size, AVDictionary **dict);
+#else
+int av_packet_unpack_dictionary(const uint8_t *data, size_t size,
+ AVDictionary **dict);
+#endif
+
+/**
+ * Convenience function to free all the side data stored.
+ * All the other fields stay untouched.
+ *
+ * @param pkt packet
+ */
+void av_packet_free_side_data(AVPacket *pkt);
+
+/**
+ * Setup a new reference to the data described by a given packet
+ *
+ * If src is reference-counted, setup dst as a new reference to the
+ * buffer in src. Otherwise allocate a new buffer in dst and copy the
+ * data from src into it.
+ *
+ * All the other fields are copied from src.
+ *
+ * @see av_packet_unref
+ *
+ * @param dst Destination packet. Will be completely overwritten.
+ * @param src Source packet
+ *
+ * @return 0 on success, a negative AVERROR on error. On error, dst
+ * will be blank (as if returned by av_packet_alloc()).
+ */
+int av_packet_ref(AVPacket *dst, const AVPacket *src);
+
+/**
+ * Wipe the packet.
+ *
+ * Unreference the buffer referenced by the packet and reset the
+ * remaining packet fields to their default values.
+ *
+ * @param pkt The packet to be unreferenced.
+ */
+void av_packet_unref(AVPacket *pkt);
+
+/**
+ * Move every field in src to dst and reset src.
+ *
+ * @see av_packet_unref
+ *
+ * @param src Source packet, will be reset
+ * @param dst Destination packet
+ */
+void av_packet_move_ref(AVPacket *dst, AVPacket *src);
+
+/**
+ * Copy only "properties" fields from src to dst.
+ *
+ * Properties for the purpose of this function are all the fields
+ * beside those related to the packet data (buf, data, size)
+ *
+ * @param dst Destination packet
+ * @param src Source packet
+ *
+ * @return 0 on success AVERROR on failure.
+ */
+int av_packet_copy_props(AVPacket *dst, const AVPacket *src);
+
+/**
+ * Ensure the data described by a given packet is reference counted.
+ *
+ * @note This function does not ensure that the reference will be writable.
+ * Use av_packet_make_writable instead for that purpose.
+ *
+ * @see av_packet_ref
+ * @see av_packet_make_writable
+ *
+ * @param pkt packet whose data should be made reference counted.
+ *
+ * @return 0 on success, a negative AVERROR on error. On failure, the
+ * packet is unchanged.
+ */
+int av_packet_make_refcounted(AVPacket *pkt);
+
+/**
+ * Create a writable reference for the data described by a given packet,
+ * avoiding data copy if possible.
+ *
+ * @param pkt Packet whose data should be made writable.
+ *
+ * @return 0 on success, a negative AVERROR on failure. On failure, the
+ * packet is unchanged.
+ */
+int av_packet_make_writable(AVPacket *pkt);
+
+/**
+ * Convert valid timing fields (timestamps / durations) in a packet from one
+ * timebase to another. Timestamps with unknown values (AV_NOPTS_VALUE) will be
+ * ignored.
+ *
+ * @param pkt packet on which the conversion will be performed
+ * @param tb_src source timebase, in which the timing fields in pkt are
+ * expressed
+ * @param tb_dst destination timebase, to which the timing fields will be
+ * converted
+ */
+void av_packet_rescale_ts(AVPacket *pkt, AVRational tb_src, AVRational tb_dst);
+
+/**
+ * @}
+ */
+
+#endif // AVCODEC_PACKET_H
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/vaapi.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/vaapi.h
new file mode 100644
index 0000000000..2cf7da5889
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/vaapi.h
@@ -0,0 +1,86 @@
+/*
+ * Video Acceleration API (shared data between FFmpeg and the video player)
+ * HW decode acceleration for MPEG-2, MPEG-4, H.264 and VC-1
+ *
+ * Copyright (C) 2008-2009 Splitted-Desktop Systems
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_VAAPI_H
+#define AVCODEC_VAAPI_H
+
+/**
+ * @file
+ * @ingroup lavc_codec_hwaccel_vaapi
+ * Public libavcodec VA API header.
+ */
+
+#include <stdint.h>
+#include "libavutil/attributes.h"
+#include "version.h"
+
+#if FF_API_STRUCT_VAAPI_CONTEXT
+
+/**
+ * @defgroup lavc_codec_hwaccel_vaapi VA API Decoding
+ * @ingroup lavc_codec_hwaccel
+ * @{
+ */
+
+/**
+ * This structure is used to share data between the FFmpeg library and
+ * the client video application.
+ * This shall be zero-allocated and available as
+ * AVCodecContext.hwaccel_context. All user members can be set once
+ * during initialization or through each AVCodecContext.get_buffer()
+ * function call. In any case, they must be valid prior to calling
+ * decoding functions.
+ *
+ * Deprecated: use AVCodecContext.hw_frames_ctx instead.
+ */
+struct attribute_deprecated vaapi_context {
+ /**
+ * Window system dependent data
+ *
+ * - encoding: unused
+ * - decoding: Set by user
+ */
+ void *display;
+
+ /**
+ * Configuration ID
+ *
+ * - encoding: unused
+ * - decoding: Set by user
+ */
+ uint32_t config_id;
+
+ /**
+ * Context ID (video decode pipeline)
+ *
+ * - encoding: unused
+ * - decoding: Set by user
+ */
+ uint32_t context_id;
+};
+
+/* @} */
+
+#endif /* FF_API_STRUCT_VAAPI_CONTEXT */
+
+#endif /* AVCODEC_VAAPI_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/vdpau.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/vdpau.h
new file mode 100644
index 0000000000..4d99943369
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/vdpau.h
@@ -0,0 +1,176 @@
+/*
+ * The Video Decode and Presentation API for UNIX (VDPAU) is used for
+ * hardware-accelerated decoding of MPEG-1/2, H.264 and VC-1.
+ *
+ * Copyright (C) 2008 NVIDIA
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_VDPAU_H
+#define AVCODEC_VDPAU_H
+
+/**
+ * @file
+ * @ingroup lavc_codec_hwaccel_vdpau
+ * Public libavcodec VDPAU header.
+ */
+
+
+/**
+ * @defgroup lavc_codec_hwaccel_vdpau VDPAU Decoder and Renderer
+ * @ingroup lavc_codec_hwaccel
+ *
+ * VDPAU hardware acceleration has two modules
+ * - VDPAU decoding
+ * - VDPAU presentation
+ *
+ * The VDPAU decoding module parses all headers using FFmpeg
+ * parsing mechanisms and uses VDPAU for the actual decoding.
+ *
+ * As per the current implementation, the actual decoding
+ * and rendering (API calls) are done as part of the VDPAU
+ * presentation (vo_vdpau.c) module.
+ *
+ * @{
+ */
+
+#include <vdpau/vdpau.h>
+
+#include "libavutil/avconfig.h"
+#include "libavutil/attributes.h"
+
+#include "avcodec.h"
+#include "version.h"
+
+struct AVCodecContext;
+struct AVFrame;
+
+typedef int (*AVVDPAU_Render2)(struct AVCodecContext *, struct AVFrame *,
+ const VdpPictureInfo *, uint32_t,
+ const VdpBitstreamBuffer *);
+
+/**
+ * This structure is used to share data between the libavcodec library and
+ * the client video application.
+ * The user shall allocate the structure via the av_alloc_vdpau_hwaccel
+ * function and make it available as
+ * AVCodecContext.hwaccel_context. Members can be set by the user once
+ * during initialization or through each AVCodecContext.get_buffer()
+ * function call. In any case, they must be valid prior to calling
+ * decoding functions.
+ *
+ * The size of this structure is not a part of the public ABI and must not
+ * be used outside of libavcodec. Use av_vdpau_alloc_context() to allocate an
+ * AVVDPAUContext.
+ */
+typedef struct AVVDPAUContext {
+ /**
+ * VDPAU decoder handle
+ *
+ * Set by user.
+ */
+ VdpDecoder decoder;
+
+ /**
+ * VDPAU decoder render callback
+ *
+ * Set by the user.
+ */
+ VdpDecoderRender *render;
+
+ AVVDPAU_Render2 render2;
+} AVVDPAUContext;
+
+/**
+ * @brief allocation function for AVVDPAUContext
+ *
+ * Allows extending the struct without breaking API/ABI
+ */
+AVVDPAUContext *av_alloc_vdpaucontext(void);
+
+AVVDPAU_Render2 av_vdpau_hwaccel_get_render2(const AVVDPAUContext *);
+void av_vdpau_hwaccel_set_render2(AVVDPAUContext *, AVVDPAU_Render2);
+
+/**
+ * Associate a VDPAU device with a codec context for hardware acceleration.
+ * This function is meant to be called from the get_format() codec callback,
+ * or earlier. It can also be called after avcodec_flush_buffers() to change
+ * the underlying VDPAU device mid-stream (e.g. to recover from non-transparent
+ * display preemption).
+ *
+ * @note get_format() must return AV_PIX_FMT_VDPAU if this function completes
+ * successfully.
+ *
+ * @param avctx decoding context whose get_format() callback is invoked
+ * @param device VDPAU device handle to use for hardware acceleration
+ * @param get_proc_address VDPAU device driver
+ * @param flags zero of more OR'd AV_HWACCEL_FLAG_* flags
+ *
+ * @return 0 on success, an AVERROR code on failure.
+ */
+int av_vdpau_bind_context(AVCodecContext *avctx, VdpDevice device,
+ VdpGetProcAddress *get_proc_address, unsigned flags);
+
+/**
+ * Gets the parameters to create an adequate VDPAU video surface for the codec
+ * context using VDPAU hardware decoding acceleration.
+ *
+ * @note Behavior is undefined if the context was not successfully bound to a
+ * VDPAU device using av_vdpau_bind_context().
+ *
+ * @param avctx the codec context being used for decoding the stream
+ * @param type storage space for the VDPAU video surface chroma type
+ * (or NULL to ignore)
+ * @param width storage space for the VDPAU video surface pixel width
+ * (or NULL to ignore)
+ * @param height storage space for the VDPAU video surface pixel height
+ * (or NULL to ignore)
+ *
+ * @return 0 on success, a negative AVERROR code on failure.
+ */
+int av_vdpau_get_surface_parameters(AVCodecContext *avctx, VdpChromaType *type,
+ uint32_t *width, uint32_t *height);
+
+/**
+ * Allocate an AVVDPAUContext.
+ *
+ * @return Newly-allocated AVVDPAUContext or NULL on failure.
+ */
+AVVDPAUContext *av_vdpau_alloc_context(void);
+
+#if FF_API_VDPAU_PROFILE
+/**
+ * Get a decoder profile that should be used for initializing a VDPAU decoder.
+ * Should be called from the AVCodecContext.get_format() callback.
+ *
+ * @deprecated Use av_vdpau_bind_context() instead.
+ *
+ * @param avctx the codec context being used for decoding the stream
+ * @param profile a pointer into which the result will be written on success.
+ * The contents of profile are undefined if this function returns
+ * an error.
+ *
+ * @return 0 on success (non-negative), a negative AVERROR on failure.
+ */
+attribute_deprecated
+int av_vdpau_get_profile(AVCodecContext *avctx, VdpDecoderProfile *profile);
+#endif
+
+/* @}*/
+
+#endif /* AVCODEC_VDPAU_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/version.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/version.h
new file mode 100644
index 0000000000..6895f1a461
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavcodec/version.h
@@ -0,0 +1,137 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_VERSION_H
+#define AVCODEC_VERSION_H
+
+/**
+ * @file
+ * @ingroup libavc
+ * Libavcodec version macros.
+ */
+
+#include "libavutil/version.h"
+
+#define LIBAVCODEC_VERSION_MAJOR 58
+#define LIBAVCODEC_VERSION_MINOR 18
+#define LIBAVCODEC_VERSION_MICRO 100
+
+#define LIBAVCODEC_VERSION_INT AV_VERSION_INT(LIBAVCODEC_VERSION_MAJOR, \
+ LIBAVCODEC_VERSION_MINOR, \
+ LIBAVCODEC_VERSION_MICRO)
+#define LIBAVCODEC_VERSION AV_VERSION(LIBAVCODEC_VERSION_MAJOR, \
+ LIBAVCODEC_VERSION_MINOR, \
+ LIBAVCODEC_VERSION_MICRO)
+#define LIBAVCODEC_BUILD LIBAVCODEC_VERSION_INT
+
+#define LIBAVCODEC_IDENT "Lavc" AV_STRINGIFY(LIBAVCODEC_VERSION)
+
+/**
+ * FF_API_* defines may be placed below to indicate public API that will be
+ * dropped at a future version bump. The defines themselves are not part of
+ * the public API and may change, break or disappear at any time.
+ *
+ * @note, when bumping the major version it is recommended to manually
+ * disable each FF_API_* in its own commit instead of disabling them all
+ * at once through the bump. This improves the git bisect-ability of the change.
+ */
+
+#ifndef FF_API_LOWRES
+#define FF_API_LOWRES (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_DEBUG_MV
+#define FF_API_DEBUG_MV (LIBAVCODEC_VERSION_MAJOR < 58)
+#endif
+#ifndef FF_API_AVCTX_TIMEBASE
+#define FF_API_AVCTX_TIMEBASE (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_CODED_FRAME
+#define FF_API_CODED_FRAME (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_SIDEDATA_ONLY_PKT
+#define FF_API_SIDEDATA_ONLY_PKT (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_VDPAU_PROFILE
+#define FF_API_VDPAU_PROFILE (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_CONVERGENCE_DURATION
+#define FF_API_CONVERGENCE_DURATION (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_AVPICTURE
+#define FF_API_AVPICTURE (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_AVPACKET_OLD_API
+#define FF_API_AVPACKET_OLD_API (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_RTP_CALLBACK
+#define FF_API_RTP_CALLBACK (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_VBV_DELAY
+#define FF_API_VBV_DELAY (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_CODER_TYPE
+#define FF_API_CODER_TYPE (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_STAT_BITS
+#define FF_API_STAT_BITS (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_PRIVATE_OPT
+#define FF_API_PRIVATE_OPT (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_ASS_TIMING
+#define FF_API_ASS_TIMING (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_OLD_BSF
+#define FF_API_OLD_BSF (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_COPY_CONTEXT
+#define FF_API_COPY_CONTEXT (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_GET_CONTEXT_DEFAULTS
+#define FF_API_GET_CONTEXT_DEFAULTS (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_NVENC_OLD_NAME
+#define FF_API_NVENC_OLD_NAME (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_STRUCT_VAAPI_CONTEXT
+#define FF_API_STRUCT_VAAPI_CONTEXT (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_MERGE_SD_API
+#define FF_API_MERGE_SD_API (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_TAG_STRING
+#define FF_API_TAG_STRING (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_GETCHROMA
+#define FF_API_GETCHROMA (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_CODEC_GET_SET
+#define FF_API_CODEC_GET_SET (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_USER_VISIBLE_AVHWACCEL
+#define FF_API_USER_VISIBLE_AVHWACCEL (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_LOCKMGR
+#define FF_API_LOCKMGR (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+#ifndef FF_API_NEXT
+#define FF_API_NEXT (LIBAVCODEC_VERSION_MAJOR < 59)
+#endif
+
+
+#endif /* AVCODEC_VERSION_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/attributes.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/attributes.h
new file mode 100644
index 0000000000..ced108aa2c
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/attributes.h
@@ -0,0 +1,167 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * Macro definitions for various function/variable attributes
+ */
+
+#ifndef AVUTIL_ATTRIBUTES_H
+#define AVUTIL_ATTRIBUTES_H
+
+#ifdef __GNUC__
+# define AV_GCC_VERSION_AT_LEAST(x,y) (__GNUC__ > (x) || __GNUC__ == (x) && __GNUC_MINOR__ >= (y))
+# define AV_GCC_VERSION_AT_MOST(x,y) (__GNUC__ < (x) || __GNUC__ == (x) && __GNUC_MINOR__ <= (y))
+#else
+# define AV_GCC_VERSION_AT_LEAST(x,y) 0
+# define AV_GCC_VERSION_AT_MOST(x,y) 0
+#endif
+
+#ifndef av_always_inline
+#if AV_GCC_VERSION_AT_LEAST(3,1)
+# define av_always_inline __attribute__((always_inline)) inline
+#elif defined(_MSC_VER)
+# define av_always_inline __forceinline
+#else
+# define av_always_inline inline
+#endif
+#endif
+
+#ifndef av_extern_inline
+#if defined(__ICL) && __ICL >= 1210 || defined(__GNUC_STDC_INLINE__)
+# define av_extern_inline extern inline
+#else
+# define av_extern_inline inline
+#endif
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3,4)
+# define av_warn_unused_result __attribute__((warn_unused_result))
+#else
+# define av_warn_unused_result
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3,1)
+# define av_noinline __attribute__((noinline))
+#elif defined(_MSC_VER)
+# define av_noinline __declspec(noinline)
+#else
+# define av_noinline
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3,1) || defined(__clang__)
+# define av_pure __attribute__((pure))
+#else
+# define av_pure
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(2,6) || defined(__clang__)
+# define av_const __attribute__((const))
+#else
+# define av_const
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(4,3) || defined(__clang__)
+# define av_cold __attribute__((cold))
+#else
+# define av_cold
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(4,1) && !defined(__llvm__)
+# define av_flatten __attribute__((flatten))
+#else
+# define av_flatten
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3,1)
+# define attribute_deprecated __attribute__((deprecated))
+#elif defined(_MSC_VER)
+# define attribute_deprecated __declspec(deprecated)
+#else
+# define attribute_deprecated
+#endif
+
+/**
+ * Disable warnings about deprecated features
+ * This is useful for sections of code kept for backward compatibility and
+ * scheduled for removal.
+ */
+#ifndef AV_NOWARN_DEPRECATED
+#if AV_GCC_VERSION_AT_LEAST(4,6)
+# define AV_NOWARN_DEPRECATED(code) \
+ _Pragma("GCC diagnostic push") \
+ _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"") \
+ code \
+ _Pragma("GCC diagnostic pop")
+#elif defined(_MSC_VER)
+# define AV_NOWARN_DEPRECATED(code) \
+ __pragma(warning(push)) \
+ __pragma(warning(disable : 4996)) \
+ code; \
+ __pragma(warning(pop))
+#else
+# define AV_NOWARN_DEPRECATED(code) code
+#endif
+#endif
+
+#if defined(__GNUC__) || defined(__clang__)
+# define av_unused __attribute__((unused))
+#else
+# define av_unused
+#endif
+
+/**
+ * Mark a variable as used and prevent the compiler from optimizing it
+ * away. This is useful for variables accessed only from inline
+ * assembler without the compiler being aware.
+ */
+#if AV_GCC_VERSION_AT_LEAST(3,1) || defined(__clang__)
+# define av_used __attribute__((used))
+#else
+# define av_used
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3,3) || defined(__clang__)
+# define av_alias __attribute__((may_alias))
+#else
+# define av_alias
+#endif
+
+#if (defined(__GNUC__) || defined(__clang__)) && !defined(__INTEL_COMPILER)
+# define av_uninit(x) x=x
+#else
+# define av_uninit(x) x
+#endif
+
+#if defined(__GNUC__) || defined(__clang__)
+# define av_builtin_constant_p __builtin_constant_p
+# define av_printf_format(fmtpos, attrpos) __attribute__((__format__(__printf__, fmtpos, attrpos)))
+#else
+# define av_builtin_constant_p(x) 0
+# define av_printf_format(fmtpos, attrpos)
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(2,5) || defined(__clang__)
+# define av_noreturn __attribute__((noreturn))
+#else
+# define av_noreturn
+#endif
+
+#endif /* AVUTIL_ATTRIBUTES_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/avconfig.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/avconfig.h
new file mode 100644
index 0000000000..c289fbb551
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/avconfig.h
@@ -0,0 +1,6 @@
+/* Generated by ffmpeg configure */
+#ifndef AVUTIL_AVCONFIG_H
+#define AVUTIL_AVCONFIG_H
+#define AV_HAVE_BIGENDIAN 0
+#define AV_HAVE_FAST_UNALIGNED 1
+#endif /* AVUTIL_AVCONFIG_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/avutil.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/avutil.h
new file mode 100644
index 0000000000..4d633156d1
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/avutil.h
@@ -0,0 +1,365 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_AVUTIL_H
+#define AVUTIL_AVUTIL_H
+
+/**
+ * @file
+ * @ingroup lavu
+ * Convenience header that includes @ref lavu "libavutil"'s core.
+ */
+
+/**
+ * @mainpage
+ *
+ * @section ffmpeg_intro Introduction
+ *
+ * This document describes the usage of the different libraries
+ * provided by FFmpeg.
+ *
+ * @li @ref libavc "libavcodec" encoding/decoding library
+ * @li @ref lavfi "libavfilter" graph-based frame editing library
+ * @li @ref libavf "libavformat" I/O and muxing/demuxing library
+ * @li @ref lavd "libavdevice" special devices muxing/demuxing library
+ * @li @ref lavu "libavutil" common utility library
+ * @li @ref lswr "libswresample" audio resampling, format conversion and mixing
+ * @li @ref lpp "libpostproc" post processing library
+ * @li @ref libsws "libswscale" color conversion and scaling library
+ *
+ * @section ffmpeg_versioning Versioning and compatibility
+ *
+ * Each of the FFmpeg libraries contains a version.h header, which defines a
+ * major, minor and micro version number with the
+ * <em>LIBRARYNAME_VERSION_{MAJOR,MINOR,MICRO}</em> macros. The major version
+ * number is incremented with backward incompatible changes - e.g. removing
+ * parts of the public API, reordering public struct members, etc. The minor
+ * version number is incremented for backward compatible API changes or major
+ * new features - e.g. adding a new public function or a new decoder. The micro
+ * version number is incremented for smaller changes that a calling program
+ * might still want to check for - e.g. changing behavior in a previously
+ * unspecified situation.
+ *
+ * FFmpeg guarantees backward API and ABI compatibility for each library as long
+ * as its major version number is unchanged. This means that no public symbols
+ * will be removed or renamed. Types and names of the public struct members and
+ * values of public macros and enums will remain the same (unless they were
+ * explicitly declared as not part of the public API). Documented behavior will
+ * not change.
+ *
+ * In other words, any correct program that works with a given FFmpeg snapshot
+ * should work just as well without any changes with any later snapshot with the
+ * same major versions. This applies to both rebuilding the program against new
+ * FFmpeg versions or to replacing the dynamic FFmpeg libraries that a program
+ * links against.
+ *
+ * However, new public symbols may be added and new members may be appended to
+ * public structs whose size is not part of public ABI (most public structs in
+ * FFmpeg). New macros and enum values may be added. Behavior in undocumented
+ * situations may change slightly (and be documented). All those are accompanied
+ * by an entry in doc/APIchanges and incrementing either the minor or micro
+ * version number.
+ */
+
+/**
+ * @defgroup lavu libavutil
+ * Common code shared across all FFmpeg libraries.
+ *
+ * @note
+ * libavutil is designed to be modular. In most cases, in order to use the
+ * functions provided by one component of libavutil you must explicitly include
+ * the specific header containing that feature. If you are only using
+ * media-related components, you could simply include libavutil/avutil.h, which
+ * brings in most of the "core" components.
+ *
+ * @{
+ *
+ * @defgroup lavu_crypto Crypto and Hashing
+ *
+ * @{
+ * @}
+ *
+ * @defgroup lavu_math Mathematics
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_string String Manipulation
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_mem Memory Management
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_data Data Structures
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_video Video related
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_audio Audio related
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_error Error Codes
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_log Logging Facility
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_misc Other
+ *
+ * @{
+ *
+ * @defgroup preproc_misc Preprocessor String Macros
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup version_utils Library Version Macros
+ *
+ * @{
+ *
+ * @}
+ */
+
+
+/**
+ * @addtogroup lavu_ver
+ * @{
+ */
+
+/**
+ * Return the LIBAVUTIL_VERSION_INT constant.
+ */
+unsigned avutil_version(void);
+
+/**
+ * Return an informative version string. This usually is the actual release
+ * version number or a git commit description. This string has no fixed format
+ * and can change any time. It should never be parsed by code.
+ */
+const char *av_version_info(void);
+
+/**
+ * Return the libavutil build-time configuration.
+ */
+const char *avutil_configuration(void);
+
+/**
+ * Return the libavutil license.
+ */
+const char *avutil_license(void);
+
+/**
+ * @}
+ */
+
+/**
+ * @addtogroup lavu_media Media Type
+ * @brief Media Type
+ */
+
+enum AVMediaType {
+ AVMEDIA_TYPE_UNKNOWN = -1, ///< Usually treated as AVMEDIA_TYPE_DATA
+ AVMEDIA_TYPE_VIDEO,
+ AVMEDIA_TYPE_AUDIO,
+ AVMEDIA_TYPE_DATA, ///< Opaque data information usually continuous
+ AVMEDIA_TYPE_SUBTITLE,
+ AVMEDIA_TYPE_ATTACHMENT, ///< Opaque data information usually sparse
+ AVMEDIA_TYPE_NB
+};
+
+/**
+ * Return a string describing the media_type enum, NULL if media_type
+ * is unknown.
+ */
+const char *av_get_media_type_string(enum AVMediaType media_type);
+
+/**
+ * @defgroup lavu_const Constants
+ * @{
+ *
+ * @defgroup lavu_enc Encoding specific
+ *
+ * @note those definition should move to avcodec
+ * @{
+ */
+
+#define FF_LAMBDA_SHIFT 7
+#define FF_LAMBDA_SCALE (1<<FF_LAMBDA_SHIFT)
+#define FF_QP2LAMBDA 118 ///< factor to convert from H.263 QP to lambda
+#define FF_LAMBDA_MAX (256*128-1)
+
+#define FF_QUALITY_SCALE FF_LAMBDA_SCALE //FIXME maybe remove
+
+/**
+ * @}
+ * @defgroup lavu_time Timestamp specific
+ *
+ * FFmpeg internal timebase and timestamp definitions
+ *
+ * @{
+ */
+
+/**
+ * @brief Undefined timestamp value
+ *
+ * Usually reported by demuxer that work on containers that do not provide
+ * either pts or dts.
+ */
+
+#define AV_NOPTS_VALUE ((int64_t)UINT64_C(0x8000000000000000))
+
+/**
+ * Internal time base represented as integer
+ */
+
+#define AV_TIME_BASE 1000000
+
+/**
+ * Internal time base represented as fractional value
+ */
+
+#define AV_TIME_BASE_Q (AVRational){1, AV_TIME_BASE}
+
+/**
+ * @}
+ * @}
+ * @defgroup lavu_picture Image related
+ *
+ * AVPicture types, pixel formats and basic image planes manipulation.
+ *
+ * @{
+ */
+
+enum AVPictureType {
+ AV_PICTURE_TYPE_NONE = 0, ///< Undefined
+ AV_PICTURE_TYPE_I, ///< Intra
+ AV_PICTURE_TYPE_P, ///< Predicted
+ AV_PICTURE_TYPE_B, ///< Bi-dir predicted
+ AV_PICTURE_TYPE_S, ///< S(GMC)-VOP MPEG-4
+ AV_PICTURE_TYPE_SI, ///< Switching Intra
+ AV_PICTURE_TYPE_SP, ///< Switching Predicted
+ AV_PICTURE_TYPE_BI, ///< BI type
+};
+
+/**
+ * Return a single letter to describe the given picture type
+ * pict_type.
+ *
+ * @param[in] pict_type the picture type @return a single character
+ * representing the picture type, '?' if pict_type is unknown
+ */
+char av_get_picture_type_char(enum AVPictureType pict_type);
+
+/**
+ * @}
+ */
+
+#include "common.h"
+#include "error.h"
+#include "rational.h"
+#include "version.h"
+#include "macros.h"
+#include "mathematics.h"
+#include "log.h"
+#include "pixfmt.h"
+
+/**
+ * Return x default pointer in case p is NULL.
+ */
+static inline void *av_x_if_null(const void *p, const void *x)
+{
+ return (void *)(intptr_t)(p ? p : x);
+}
+
+/**
+ * Compute the length of an integer list.
+ *
+ * @param elsize size in bytes of each list element (only 1, 2, 4 or 8)
+ * @param term list terminator (usually 0 or -1)
+ * @param list pointer to the list
+ * @return length of the list, in elements, not counting the terminator
+ */
+unsigned av_int_list_length_for_size(unsigned elsize,
+ const void *list, uint64_t term) av_pure;
+
+/**
+ * Compute the length of an integer list.
+ *
+ * @param term list terminator (usually 0 or -1)
+ * @param list pointer to the list
+ * @return length of the list, in elements, not counting the terminator
+ */
+#define av_int_list_length(list, term) \
+ av_int_list_length_for_size(sizeof(*(list)), list, term)
+
+/**
+ * Open a file using a UTF-8 filename.
+ * The API of this function matches POSIX fopen(), errors are returned through
+ * errno.
+ */
+FILE *av_fopen_utf8(const char *path, const char *mode);
+
+/**
+ * Return the fractional representation of the internal time base.
+ */
+AVRational av_get_time_base_q(void);
+
+#define AV_FOURCC_MAX_STRING_SIZE 32
+
+#define av_fourcc2str(fourcc) av_fourcc_make_string((char[AV_FOURCC_MAX_STRING_SIZE]){0}, fourcc)
+
+/**
+ * Fill the provided buffer with a string containing a FourCC (four-character
+ * code) representation.
+ *
+ * @param buf a buffer with size in bytes of at least AV_FOURCC_MAX_STRING_SIZE
+ * @param fourcc the fourcc to represent
+ * @return the buffer in input
+ */
+char *av_fourcc_make_string(char *buf, uint32_t fourcc);
+
+/**
+ * @}
+ * @}
+ */
+
+#endif /* AVUTIL_AVUTIL_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/buffer.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/buffer.h
new file mode 100644
index 0000000000..73b6bd0b14
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/buffer.h
@@ -0,0 +1,291 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * @ingroup lavu_buffer
+ * refcounted data buffer API
+ */
+
+#ifndef AVUTIL_BUFFER_H
+#define AVUTIL_BUFFER_H
+
+#include <stdint.h>
+
+/**
+ * @defgroup lavu_buffer AVBuffer
+ * @ingroup lavu_data
+ *
+ * @{
+ * AVBuffer is an API for reference-counted data buffers.
+ *
+ * There are two core objects in this API -- AVBuffer and AVBufferRef. AVBuffer
+ * represents the data buffer itself; it is opaque and not meant to be accessed
+ * by the caller directly, but only through AVBufferRef. However, the caller may
+ * e.g. compare two AVBuffer pointers to check whether two different references
+ * are describing the same data buffer. AVBufferRef represents a single
+ * reference to an AVBuffer and it is the object that may be manipulated by the
+ * caller directly.
+ *
+ * There are two functions provided for creating a new AVBuffer with a single
+ * reference -- av_buffer_alloc() to just allocate a new buffer, and
+ * av_buffer_create() to wrap an existing array in an AVBuffer. From an existing
+ * reference, additional references may be created with av_buffer_ref().
+ * Use av_buffer_unref() to free a reference (this will automatically free the
+ * data once all the references are freed).
+ *
+ * The convention throughout this API and the rest of FFmpeg is such that the
+ * buffer is considered writable if there exists only one reference to it (and
+ * it has not been marked as read-only). The av_buffer_is_writable() function is
+ * provided to check whether this is true and av_buffer_make_writable() will
+ * automatically create a new writable buffer when necessary.
+ * Of course nothing prevents the calling code from violating this convention,
+ * however that is safe only when all the existing references are under its
+ * control.
+ *
+ * @note Referencing and unreferencing the buffers is thread-safe and thus
+ * may be done from multiple threads simultaneously without any need for
+ * additional locking.
+ *
+ * @note Two different references to the same buffer can point to different
+ * parts of the buffer (i.e. their AVBufferRef.data will not be equal).
+ */
+
+/**
+ * A reference counted buffer type. It is opaque and is meant to be used through
+ * references (AVBufferRef).
+ */
+typedef struct AVBuffer AVBuffer;
+
+/**
+ * A reference to a data buffer.
+ *
+ * The size of this struct is not a part of the public ABI and it is not meant
+ * to be allocated directly.
+ */
+typedef struct AVBufferRef {
+ AVBuffer *buffer;
+
+ /**
+ * The data buffer. It is considered writable if and only if
+ * this is the only reference to the buffer, in which case
+ * av_buffer_is_writable() returns 1.
+ */
+ uint8_t *data;
+ /**
+ * Size of data in bytes.
+ */
+ int size;
+} AVBufferRef;
+
+/**
+ * Allocate an AVBuffer of the given size using av_malloc().
+ *
+ * @return an AVBufferRef of given size or NULL when out of memory
+ */
+AVBufferRef *av_buffer_alloc(int size);
+
+/**
+ * Same as av_buffer_alloc(), except the returned buffer will be initialized
+ * to zero.
+ */
+AVBufferRef *av_buffer_allocz(int size);
+
+/**
+ * Always treat the buffer as read-only, even when it has only one
+ * reference.
+ */
+#define AV_BUFFER_FLAG_READONLY (1 << 0)
+
+/**
+ * Create an AVBuffer from an existing array.
+ *
+ * If this function is successful, data is owned by the AVBuffer. The caller may
+ * only access data through the returned AVBufferRef and references derived from
+ * it.
+ * If this function fails, data is left untouched.
+ * @param data data array
+ * @param size size of data in bytes
+ * @param free a callback for freeing this buffer's data
+ * @param opaque parameter to be got for processing or passed to free
+ * @param flags a combination of AV_BUFFER_FLAG_*
+ *
+ * @return an AVBufferRef referring to data on success, NULL on failure.
+ */
+AVBufferRef *av_buffer_create(uint8_t *data, int size,
+ void (*free)(void *opaque, uint8_t *data),
+ void *opaque, int flags);
+
+/**
+ * Default free callback, which calls av_free() on the buffer data.
+ * This function is meant to be passed to av_buffer_create(), not called
+ * directly.
+ */
+void av_buffer_default_free(void *opaque, uint8_t *data);
+
+/**
+ * Create a new reference to an AVBuffer.
+ *
+ * @return a new AVBufferRef referring to the same AVBuffer as buf or NULL on
+ * failure.
+ */
+AVBufferRef *av_buffer_ref(AVBufferRef *buf);
+
+/**
+ * Free a given reference and automatically free the buffer if there are no more
+ * references to it.
+ *
+ * @param buf the reference to be freed. The pointer is set to NULL on return.
+ */
+void av_buffer_unref(AVBufferRef **buf);
+
+/**
+ * @return 1 if the caller may write to the data referred to by buf (which is
+ * true if and only if buf is the only reference to the underlying AVBuffer).
+ * Return 0 otherwise.
+ * A positive answer is valid until av_buffer_ref() is called on buf.
+ */
+int av_buffer_is_writable(const AVBufferRef *buf);
+
+/**
+ * @return the opaque parameter set by av_buffer_create.
+ */
+void *av_buffer_get_opaque(const AVBufferRef *buf);
+
+int av_buffer_get_ref_count(const AVBufferRef *buf);
+
+/**
+ * Create a writable reference from a given buffer reference, avoiding data copy
+ * if possible.
+ *
+ * @param buf buffer reference to make writable. On success, buf is either left
+ * untouched, or it is unreferenced and a new writable AVBufferRef is
+ * written in its place. On failure, buf is left untouched.
+ * @return 0 on success, a negative AVERROR on failure.
+ */
+int av_buffer_make_writable(AVBufferRef **buf);
+
+/**
+ * Reallocate a given buffer.
+ *
+ * @param buf a buffer reference to reallocate. On success, buf will be
+ * unreferenced and a new reference with the required size will be
+ * written in its place. On failure buf will be left untouched. *buf
+ * may be NULL, then a new buffer is allocated.
+ * @param size required new buffer size.
+ * @return 0 on success, a negative AVERROR on failure.
+ *
+ * @note the buffer is actually reallocated with av_realloc() only if it was
+ * initially allocated through av_buffer_realloc(NULL) and there is only one
+ * reference to it (i.e. the one passed to this function). In all other cases
+ * a new buffer is allocated and the data is copied.
+ */
+int av_buffer_realloc(AVBufferRef **buf, int size);
+
+/**
+ * @}
+ */
+
+/**
+ * @defgroup lavu_bufferpool AVBufferPool
+ * @ingroup lavu_data
+ *
+ * @{
+ * AVBufferPool is an API for a lock-free thread-safe pool of AVBuffers.
+ *
+ * Frequently allocating and freeing large buffers may be slow. AVBufferPool is
+ * meant to solve this in cases when the caller needs a set of buffers of the
+ * same size (the most obvious use case being buffers for raw video or audio
+ * frames).
+ *
+ * At the beginning, the user must call av_buffer_pool_init() to create the
+ * buffer pool. Then whenever a buffer is needed, call av_buffer_pool_get() to
+ * get a reference to a new buffer, similar to av_buffer_alloc(). This new
+ * reference works in all aspects the same way as the one created by
+ * av_buffer_alloc(). However, when the last reference to this buffer is
+ * unreferenced, it is returned to the pool instead of being freed and will be
+ * reused for subsequent av_buffer_pool_get() calls.
+ *
+ * When the caller is done with the pool and no longer needs to allocate any new
+ * buffers, av_buffer_pool_uninit() must be called to mark the pool as freeable.
+ * Once all the buffers are released, it will automatically be freed.
+ *
+ * Allocating and releasing buffers with this API is thread-safe as long as
+ * either the default alloc callback is used, or the user-supplied one is
+ * thread-safe.
+ */
+
+/**
+ * The buffer pool. This structure is opaque and not meant to be accessed
+ * directly. It is allocated with av_buffer_pool_init() and freed with
+ * av_buffer_pool_uninit().
+ */
+typedef struct AVBufferPool AVBufferPool;
+
+/**
+ * Allocate and initialize a buffer pool.
+ *
+ * @param size size of each buffer in this pool
+ * @param alloc a function that will be used to allocate new buffers when the
+ * pool is empty. May be NULL, then the default allocator will be used
+ * (av_buffer_alloc()).
+ * @return newly created buffer pool on success, NULL on error.
+ */
+AVBufferPool *av_buffer_pool_init(int size, AVBufferRef* (*alloc)(int size));
+
+/**
+ * Allocate and initialize a buffer pool with a more complex allocator.
+ *
+ * @param size size of each buffer in this pool
+ * @param opaque arbitrary user data used by the allocator
+ * @param alloc a function that will be used to allocate new buffers when the
+ * pool is empty.
+ * @param pool_free a function that will be called immediately before the pool
+ * is freed. I.e. after av_buffer_pool_uninit() is called
+ * by the caller and all the frames are returned to the pool
+ * and freed. It is intended to uninitialize the user opaque
+ * data.
+ * @return newly created buffer pool on success, NULL on error.
+ */
+AVBufferPool *av_buffer_pool_init2(int size, void *opaque,
+ AVBufferRef* (*alloc)(void *opaque, int size),
+ void (*pool_free)(void *opaque));
+
+/**
+ * Mark the pool as being available for freeing. It will actually be freed only
+ * once all the allocated buffers associated with the pool are released. Thus it
+ * is safe to call this function while some of the allocated buffers are still
+ * in use.
+ *
+ * @param pool pointer to the pool to be freed. It will be set to NULL.
+ */
+void av_buffer_pool_uninit(AVBufferPool **pool);
+
+/**
+ * Allocate a new AVBuffer, reusing an old buffer from the pool when available.
+ * This function may be called simultaneously from multiple threads.
+ *
+ * @return a reference to the new buffer on success, NULL on error.
+ */
+AVBufferRef *av_buffer_pool_get(AVBufferPool *pool);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_BUFFER_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/channel_layout.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/channel_layout.h
new file mode 100644
index 0000000000..50bb8f03c5
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/channel_layout.h
@@ -0,0 +1,232 @@
+/*
+ * Copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ * Copyright (c) 2008 Peter Ross
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_CHANNEL_LAYOUT_H
+#define AVUTIL_CHANNEL_LAYOUT_H
+
+#include <stdint.h>
+
+/**
+ * @file
+ * audio channel layout utility functions
+ */
+
+/**
+ * @addtogroup lavu_audio
+ * @{
+ */
+
+/**
+ * @defgroup channel_masks Audio channel masks
+ *
+ * A channel layout is a 64-bits integer with a bit set for every channel.
+ * The number of bits set must be equal to the number of channels.
+ * The value 0 means that the channel layout is not known.
+ * @note this data structure is not powerful enough to handle channels
+ * combinations that have the same channel multiple times, such as
+ * dual-mono.
+ *
+ * @{
+ */
+#define AV_CH_FRONT_LEFT 0x00000001
+#define AV_CH_FRONT_RIGHT 0x00000002
+#define AV_CH_FRONT_CENTER 0x00000004
+#define AV_CH_LOW_FREQUENCY 0x00000008
+#define AV_CH_BACK_LEFT 0x00000010
+#define AV_CH_BACK_RIGHT 0x00000020
+#define AV_CH_FRONT_LEFT_OF_CENTER 0x00000040
+#define AV_CH_FRONT_RIGHT_OF_CENTER 0x00000080
+#define AV_CH_BACK_CENTER 0x00000100
+#define AV_CH_SIDE_LEFT 0x00000200
+#define AV_CH_SIDE_RIGHT 0x00000400
+#define AV_CH_TOP_CENTER 0x00000800
+#define AV_CH_TOP_FRONT_LEFT 0x00001000
+#define AV_CH_TOP_FRONT_CENTER 0x00002000
+#define AV_CH_TOP_FRONT_RIGHT 0x00004000
+#define AV_CH_TOP_BACK_LEFT 0x00008000
+#define AV_CH_TOP_BACK_CENTER 0x00010000
+#define AV_CH_TOP_BACK_RIGHT 0x00020000
+#define AV_CH_STEREO_LEFT 0x20000000 ///< Stereo downmix.
+#define AV_CH_STEREO_RIGHT 0x40000000 ///< See AV_CH_STEREO_LEFT.
+#define AV_CH_WIDE_LEFT 0x0000000080000000ULL
+#define AV_CH_WIDE_RIGHT 0x0000000100000000ULL
+#define AV_CH_SURROUND_DIRECT_LEFT 0x0000000200000000ULL
+#define AV_CH_SURROUND_DIRECT_RIGHT 0x0000000400000000ULL
+#define AV_CH_LOW_FREQUENCY_2 0x0000000800000000ULL
+
+/** Channel mask value used for AVCodecContext.request_channel_layout
+ to indicate that the user requests the channel order of the decoder output
+ to be the native codec channel order. */
+#define AV_CH_LAYOUT_NATIVE 0x8000000000000000ULL
+
+/**
+ * @}
+ * @defgroup channel_mask_c Audio channel layouts
+ * @{
+ * */
+#define AV_CH_LAYOUT_MONO (AV_CH_FRONT_CENTER)
+#define AV_CH_LAYOUT_STEREO (AV_CH_FRONT_LEFT|AV_CH_FRONT_RIGHT)
+#define AV_CH_LAYOUT_2POINT1 (AV_CH_LAYOUT_STEREO|AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_2_1 (AV_CH_LAYOUT_STEREO|AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_SURROUND (AV_CH_LAYOUT_STEREO|AV_CH_FRONT_CENTER)
+#define AV_CH_LAYOUT_3POINT1 (AV_CH_LAYOUT_SURROUND|AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_4POINT0 (AV_CH_LAYOUT_SURROUND|AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_4POINT1 (AV_CH_LAYOUT_4POINT0|AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_2_2 (AV_CH_LAYOUT_STEREO|AV_CH_SIDE_LEFT|AV_CH_SIDE_RIGHT)
+#define AV_CH_LAYOUT_QUAD (AV_CH_LAYOUT_STEREO|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_5POINT0 (AV_CH_LAYOUT_SURROUND|AV_CH_SIDE_LEFT|AV_CH_SIDE_RIGHT)
+#define AV_CH_LAYOUT_5POINT1 (AV_CH_LAYOUT_5POINT0|AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_5POINT0_BACK (AV_CH_LAYOUT_SURROUND|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_5POINT1_BACK (AV_CH_LAYOUT_5POINT0_BACK|AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_6POINT0 (AV_CH_LAYOUT_5POINT0|AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_6POINT0_FRONT (AV_CH_LAYOUT_2_2|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER)
+#define AV_CH_LAYOUT_HEXAGONAL (AV_CH_LAYOUT_5POINT0_BACK|AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_6POINT1 (AV_CH_LAYOUT_5POINT1|AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_6POINT1_BACK (AV_CH_LAYOUT_5POINT1_BACK|AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_6POINT1_FRONT (AV_CH_LAYOUT_6POINT0_FRONT|AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_7POINT0 (AV_CH_LAYOUT_5POINT0|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_7POINT0_FRONT (AV_CH_LAYOUT_5POINT0|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER)
+#define AV_CH_LAYOUT_7POINT1 (AV_CH_LAYOUT_5POINT1|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_7POINT1_WIDE (AV_CH_LAYOUT_5POINT1|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER)
+#define AV_CH_LAYOUT_7POINT1_WIDE_BACK (AV_CH_LAYOUT_5POINT1_BACK|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER)
+#define AV_CH_LAYOUT_OCTAGONAL (AV_CH_LAYOUT_5POINT0|AV_CH_BACK_LEFT|AV_CH_BACK_CENTER|AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_HEXADECAGONAL (AV_CH_LAYOUT_OCTAGONAL|AV_CH_WIDE_LEFT|AV_CH_WIDE_RIGHT|AV_CH_TOP_BACK_LEFT|AV_CH_TOP_BACK_RIGHT|AV_CH_TOP_BACK_CENTER|AV_CH_TOP_FRONT_CENTER|AV_CH_TOP_FRONT_LEFT|AV_CH_TOP_FRONT_RIGHT)
+#define AV_CH_LAYOUT_STEREO_DOWNMIX (AV_CH_STEREO_LEFT|AV_CH_STEREO_RIGHT)
+
+enum AVMatrixEncoding {
+ AV_MATRIX_ENCODING_NONE,
+ AV_MATRIX_ENCODING_DOLBY,
+ AV_MATRIX_ENCODING_DPLII,
+ AV_MATRIX_ENCODING_DPLIIX,
+ AV_MATRIX_ENCODING_DPLIIZ,
+ AV_MATRIX_ENCODING_DOLBYEX,
+ AV_MATRIX_ENCODING_DOLBYHEADPHONE,
+ AV_MATRIX_ENCODING_NB
+};
+
+/**
+ * Return a channel layout id that matches name, or 0 if no match is found.
+ *
+ * name can be one or several of the following notations,
+ * separated by '+' or '|':
+ * - the name of an usual channel layout (mono, stereo, 4.0, quad, 5.0,
+ * 5.0(side), 5.1, 5.1(side), 7.1, 7.1(wide), downmix);
+ * - the name of a single channel (FL, FR, FC, LFE, BL, BR, FLC, FRC, BC,
+ * SL, SR, TC, TFL, TFC, TFR, TBL, TBC, TBR, DL, DR);
+ * - a number of channels, in decimal, followed by 'c', yielding
+ * the default channel layout for that number of channels (@see
+ * av_get_default_channel_layout);
+ * - a channel layout mask, in hexadecimal starting with "0x" (see the
+ * AV_CH_* macros).
+ *
+ * Example: "stereo+FC" = "2c+FC" = "2c+1c" = "0x7"
+ */
+uint64_t av_get_channel_layout(const char *name);
+
+/**
+ * Return a channel layout and the number of channels based on the specified name.
+ *
+ * This function is similar to (@see av_get_channel_layout), but can also parse
+ * unknown channel layout specifications.
+ *
+ * @param[in] name channel layout specification string
+ * @param[out] channel_layout parsed channel layout (0 if unknown)
+ * @param[out] nb_channels number of channels
+ *
+ * @return 0 on success, AVERROR(EINVAL) if the parsing fails.
+ */
+int av_get_extended_channel_layout(const char *name, uint64_t* channel_layout, int* nb_channels);
+
+/**
+ * Return a description of a channel layout.
+ * If nb_channels is <= 0, it is guessed from the channel_layout.
+ *
+ * @param buf put here the string containing the channel layout
+ * @param buf_size size in bytes of the buffer
+ */
+void av_get_channel_layout_string(char *buf, int buf_size, int nb_channels, uint64_t channel_layout);
+
+struct AVBPrint;
+/**
+ * Append a description of a channel layout to a bprint buffer.
+ */
+void av_bprint_channel_layout(struct AVBPrint *bp, int nb_channels, uint64_t channel_layout);
+
+/**
+ * Return the number of channels in the channel layout.
+ */
+int av_get_channel_layout_nb_channels(uint64_t channel_layout);
+
+/**
+ * Return default channel layout for a given number of channels.
+ */
+int64_t av_get_default_channel_layout(int nb_channels);
+
+/**
+ * Get the index of a channel in channel_layout.
+ *
+ * @param channel a channel layout describing exactly one channel which must be
+ * present in channel_layout.
+ *
+ * @return index of channel in channel_layout on success, a negative AVERROR
+ * on error.
+ */
+int av_get_channel_layout_channel_index(uint64_t channel_layout,
+ uint64_t channel);
+
+/**
+ * Get the channel with the given index in channel_layout.
+ */
+uint64_t av_channel_layout_extract_channel(uint64_t channel_layout, int index);
+
+/**
+ * Get the name of a given channel.
+ *
+ * @return channel name on success, NULL on error.
+ */
+const char *av_get_channel_name(uint64_t channel);
+
+/**
+ * Get the description of a given channel.
+ *
+ * @param channel a channel layout with a single channel
+ * @return channel description on success, NULL on error
+ */
+const char *av_get_channel_description(uint64_t channel);
+
+/**
+ * Get the value and name of a standard channel layout.
+ *
+ * @param[in] index index in an internal list, starting at 0
+ * @param[out] layout channel layout mask
+ * @param[out] name name of the layout
+ * @return 0 if the layout exists,
+ * <0 if index is beyond the limits
+ */
+int av_get_standard_channel_layout(unsigned index, uint64_t *layout,
+ const char **name);
+
+/**
+ * @}
+ * @}
+ */
+
+#endif /* AVUTIL_CHANNEL_LAYOUT_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/common.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/common.h
new file mode 100644
index 0000000000..0fffa67714
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/common.h
@@ -0,0 +1,560 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * common internal and external API header
+ */
+
+#ifndef AVUTIL_COMMON_H
+#define AVUTIL_COMMON_H
+
+#if defined(__cplusplus) && !defined(__STDC_CONSTANT_MACROS) && !defined(UINT64_C)
+#error missing -D__STDC_CONSTANT_MACROS / #define __STDC_CONSTANT_MACROS
+#endif
+
+#include <errno.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <math.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "attributes.h"
+#include "macros.h"
+#include "version.h"
+#include "libavutil/avconfig.h"
+
+#if AV_HAVE_BIGENDIAN
+# define AV_NE(be, le) (be)
+#else
+# define AV_NE(be, le) (le)
+#endif
+
+//rounded division & shift
+#define RSHIFT(a,b) ((a) > 0 ? ((a) + ((1<<(b))>>1))>>(b) : ((a) + ((1<<(b))>>1)-1)>>(b))
+/* assume b>0 */
+#define ROUNDED_DIV(a,b) (((a)>0 ? (a) + ((b)>>1) : (a) - ((b)>>1))/(b))
+/* Fast a/(1<<b) rounded toward +inf. Assume a>=0 and b>=0 */
+#define AV_CEIL_RSHIFT(a,b) (!av_builtin_constant_p(b) ? -((-(a)) >> (b)) \
+ : ((a) + (1<<(b)) - 1) >> (b))
+/* Backwards compat. */
+#define FF_CEIL_RSHIFT AV_CEIL_RSHIFT
+
+#define FFUDIV(a,b) (((a)>0 ?(a):(a)-(b)+1) / (b))
+#define FFUMOD(a,b) ((a)-(b)*FFUDIV(a,b))
+
+/**
+ * Absolute value, Note, INT_MIN / INT64_MIN result in undefined behavior as they
+ * are not representable as absolute values of their type. This is the same
+ * as with *abs()
+ * @see FFNABS()
+ */
+#define FFABS(a) ((a) >= 0 ? (a) : (-(a)))
+#define FFSIGN(a) ((a) > 0 ? 1 : -1)
+
+/**
+ * Negative Absolute value.
+ * this works for all integers of all types.
+ * As with many macros, this evaluates its argument twice, it thus must not have
+ * a sideeffect, that is FFNABS(x++) has undefined behavior.
+ */
+#define FFNABS(a) ((a) <= 0 ? (a) : (-(a)))
+
+/**
+ * Comparator.
+ * For two numerical expressions x and y, gives 1 if x > y, -1 if x < y, and 0
+ * if x == y. This is useful for instance in a qsort comparator callback.
+ * Furthermore, compilers are able to optimize this to branchless code, and
+ * there is no risk of overflow with signed types.
+ * As with many macros, this evaluates its argument multiple times, it thus
+ * must not have a side-effect.
+ */
+#define FFDIFFSIGN(x,y) (((x)>(y)) - ((x)<(y)))
+
+#define FFMAX(a,b) ((a) > (b) ? (a) : (b))
+#define FFMAX3(a,b,c) FFMAX(FFMAX(a,b),c)
+#define FFMIN(a,b) ((a) > (b) ? (b) : (a))
+#define FFMIN3(a,b,c) FFMIN(FFMIN(a,b),c)
+
+#define FFSWAP(type,a,b) do{type SWAP_tmp= b; b= a; a= SWAP_tmp;}while(0)
+#define FF_ARRAY_ELEMS(a) (sizeof(a) / sizeof((a)[0]))
+
+/* misc math functions */
+
+#ifdef HAVE_AV_CONFIG_H
+# include "config.h"
+# include "intmath.h"
+#endif
+
+/* Pull in unguarded fallback defines at the end of this file. */
+#include "common.h"
+
+#ifndef av_log2
+av_const int av_log2(unsigned v);
+#endif
+
+#ifndef av_log2_16bit
+av_const int av_log2_16bit(unsigned v);
+#endif
+
+/**
+ * Clip a signed integer value into the amin-amax range.
+ * @param a value to clip
+ * @param amin minimum value of the clip range
+ * @param amax maximum value of the clip range
+ * @return clipped value
+ */
+static av_always_inline av_const int av_clip_c(int a, int amin, int amax)
+{
+#if defined(HAVE_AV_CONFIG_H) && defined(ASSERT_LEVEL) && ASSERT_LEVEL >= 2
+ if (amin > amax) abort();
+#endif
+ if (a < amin) return amin;
+ else if (a > amax) return amax;
+ else return a;
+}
+
+/**
+ * Clip a signed 64bit integer value into the amin-amax range.
+ * @param a value to clip
+ * @param amin minimum value of the clip range
+ * @param amax maximum value of the clip range
+ * @return clipped value
+ */
+static av_always_inline av_const int64_t av_clip64_c(int64_t a, int64_t amin, int64_t amax)
+{
+#if defined(HAVE_AV_CONFIG_H) && defined(ASSERT_LEVEL) && ASSERT_LEVEL >= 2
+ if (amin > amax) abort();
+#endif
+ if (a < amin) return amin;
+ else if (a > amax) return amax;
+ else return a;
+}
+
+/**
+ * Clip a signed integer value into the 0-255 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const uint8_t av_clip_uint8_c(int a)
+{
+ if (a&(~0xFF)) return (~a)>>31;
+ else return a;
+}
+
+/**
+ * Clip a signed integer value into the -128,127 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const int8_t av_clip_int8_c(int a)
+{
+ if ((a+0x80U) & ~0xFF) return (a>>31) ^ 0x7F;
+ else return a;
+}
+
+/**
+ * Clip a signed integer value into the 0-65535 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const uint16_t av_clip_uint16_c(int a)
+{
+ if (a&(~0xFFFF)) return (~a)>>31;
+ else return a;
+}
+
+/**
+ * Clip a signed integer value into the -32768,32767 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const int16_t av_clip_int16_c(int a)
+{
+ if ((a+0x8000U) & ~0xFFFF) return (a>>31) ^ 0x7FFF;
+ else return a;
+}
+
+/**
+ * Clip a signed 64-bit integer value into the -2147483648,2147483647 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const int32_t av_clipl_int32_c(int64_t a)
+{
+ if ((a+0x80000000u) & ~UINT64_C(0xFFFFFFFF)) return (int32_t)((a>>63) ^ 0x7FFFFFFF);
+ else return (int32_t)a;
+}
+
+/**
+ * Clip a signed integer into the -(2^p),(2^p-1) range.
+ * @param a value to clip
+ * @param p bit position to clip at
+ * @return clipped value
+ */
+static av_always_inline av_const int av_clip_intp2_c(int a, int p)
+{
+ if (((unsigned)a + (1 << p)) & ~((2 << p) - 1))
+ return (a >> 31) ^ ((1 << p) - 1);
+ else
+ return a;
+}
+
+/**
+ * Clip a signed integer to an unsigned power of two range.
+ * @param a value to clip
+ * @param p bit position to clip at
+ * @return clipped value
+ */
+static av_always_inline av_const unsigned av_clip_uintp2_c(int a, int p)
+{
+ if (a & ~((1<<p) - 1)) return -a >> 31 & ((1<<p) - 1);
+ else return a;
+}
+
+/**
+ * Clear high bits from an unsigned integer starting with specific bit position
+ * @param a value to clip
+ * @param p bit position to clip at
+ * @return clipped value
+ */
+static av_always_inline av_const unsigned av_mod_uintp2_c(unsigned a, unsigned p)
+{
+ return a & ((1 << p) - 1);
+}
+
+/**
+ * Add two signed 32-bit values with saturation.
+ *
+ * @param a one value
+ * @param b another value
+ * @return sum with signed saturation
+ */
+static av_always_inline int av_sat_add32_c(int a, int b)
+{
+ return av_clipl_int32((int64_t)a + b);
+}
+
+/**
+ * Add a doubled value to another value with saturation at both stages.
+ *
+ * @param a first value
+ * @param b value doubled and added to a
+ * @return sum sat(a + sat(2*b)) with signed saturation
+ */
+static av_always_inline int av_sat_dadd32_c(int a, int b)
+{
+ return av_sat_add32(a, av_sat_add32(b, b));
+}
+
+/**
+ * Subtract two signed 32-bit values with saturation.
+ *
+ * @param a one value
+ * @param b another value
+ * @return difference with signed saturation
+ */
+static av_always_inline int av_sat_sub32_c(int a, int b)
+{
+ return av_clipl_int32((int64_t)a - b);
+}
+
+/**
+ * Subtract a doubled value from another value with saturation at both stages.
+ *
+ * @param a first value
+ * @param b value doubled and subtracted from a
+ * @return difference sat(a - sat(2*b)) with signed saturation
+ */
+static av_always_inline int av_sat_dsub32_c(int a, int b)
+{
+ return av_sat_sub32(a, av_sat_add32(b, b));
+}
+
+/**
+ * Clip a float value into the amin-amax range.
+ * @param a value to clip
+ * @param amin minimum value of the clip range
+ * @param amax maximum value of the clip range
+ * @return clipped value
+ */
+static av_always_inline av_const float av_clipf_c(float a, float amin, float amax)
+{
+#if defined(HAVE_AV_CONFIG_H) && defined(ASSERT_LEVEL) && ASSERT_LEVEL >= 2
+ if (amin > amax) abort();
+#endif
+ if (a < amin) return amin;
+ else if (a > amax) return amax;
+ else return a;
+}
+
+/**
+ * Clip a double value into the amin-amax range.
+ * @param a value to clip
+ * @param amin minimum value of the clip range
+ * @param amax maximum value of the clip range
+ * @return clipped value
+ */
+static av_always_inline av_const double av_clipd_c(double a, double amin, double amax)
+{
+#if defined(HAVE_AV_CONFIG_H) && defined(ASSERT_LEVEL) && ASSERT_LEVEL >= 2
+ if (amin > amax) abort();
+#endif
+ if (a < amin) return amin;
+ else if (a > amax) return amax;
+ else return a;
+}
+
+/** Compute ceil(log2(x)).
+ * @param x value used to compute ceil(log2(x))
+ * @return computed ceiling of log2(x)
+ */
+static av_always_inline av_const int av_ceil_log2_c(int x)
+{
+ return av_log2((x - 1) << 1);
+}
+
+/**
+ * Count number of bits set to one in x
+ * @param x value to count bits of
+ * @return the number of bits set to one in x
+ */
+static av_always_inline av_const int av_popcount_c(uint32_t x)
+{
+ x -= (x >> 1) & 0x55555555;
+ x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
+ x = (x + (x >> 4)) & 0x0F0F0F0F;
+ x += x >> 8;
+ return (x + (x >> 16)) & 0x3F;
+}
+
+/**
+ * Count number of bits set to one in x
+ * @param x value to count bits of
+ * @return the number of bits set to one in x
+ */
+static av_always_inline av_const int av_popcount64_c(uint64_t x)
+{
+ return av_popcount((uint32_t)x) + av_popcount((uint32_t)(x >> 32));
+}
+
+static av_always_inline av_const int av_parity_c(uint32_t v)
+{
+ return av_popcount(v) & 1;
+}
+
+#define MKTAG(a,b,c,d) ((a) | ((b) << 8) | ((c) << 16) | ((unsigned)(d) << 24))
+#define MKBETAG(a,b,c,d) ((d) | ((c) << 8) | ((b) << 16) | ((unsigned)(a) << 24))
+
+/**
+ * Convert a UTF-8 character (up to 4 bytes) to its 32-bit UCS-4 encoded form.
+ *
+ * @param val Output value, must be an lvalue of type uint32_t.
+ * @param GET_BYTE Expression reading one byte from the input.
+ * Evaluated up to 7 times (4 for the currently
+ * assigned Unicode range). With a memory buffer
+ * input, this could be *ptr++.
+ * @param ERROR Expression to be evaluated on invalid input,
+ * typically a goto statement.
+ *
+ * @warning ERROR should not contain a loop control statement which
+ * could interact with the internal while loop, and should force an
+ * exit from the macro code (e.g. through a goto or a return) in order
+ * to prevent undefined results.
+ */
+#define GET_UTF8(val, GET_BYTE, ERROR)\
+ val= (GET_BYTE);\
+ {\
+ uint32_t top = (val & 128) >> 1;\
+ if ((val & 0xc0) == 0x80 || val >= 0xFE)\
+ ERROR\
+ while (val & top) {\
+ int tmp= (GET_BYTE) - 128;\
+ if(tmp>>6)\
+ ERROR\
+ val= (val<<6) + tmp;\
+ top <<= 5;\
+ }\
+ val &= (top << 1) - 1;\
+ }
+
+/**
+ * Convert a UTF-16 character (2 or 4 bytes) to its 32-bit UCS-4 encoded form.
+ *
+ * @param val Output value, must be an lvalue of type uint32_t.
+ * @param GET_16BIT Expression returning two bytes of UTF-16 data converted
+ * to native byte order. Evaluated one or two times.
+ * @param ERROR Expression to be evaluated on invalid input,
+ * typically a goto statement.
+ */
+#define GET_UTF16(val, GET_16BIT, ERROR)\
+ val = GET_16BIT;\
+ {\
+ unsigned int hi = val - 0xD800;\
+ if (hi < 0x800) {\
+ val = GET_16BIT - 0xDC00;\
+ if (val > 0x3FFU || hi > 0x3FFU)\
+ ERROR\
+ val += (hi<<10) + 0x10000;\
+ }\
+ }\
+
+/**
+ * @def PUT_UTF8(val, tmp, PUT_BYTE)
+ * Convert a 32-bit Unicode character to its UTF-8 encoded form (up to 4 bytes long).
+ * @param val is an input-only argument and should be of type uint32_t. It holds
+ * a UCS-4 encoded Unicode character that is to be converted to UTF-8. If
+ * val is given as a function it is executed only once.
+ * @param tmp is a temporary variable and should be of type uint8_t. It
+ * represents an intermediate value during conversion that is to be
+ * output by PUT_BYTE.
+ * @param PUT_BYTE writes the converted UTF-8 bytes to any proper destination.
+ * It could be a function or a statement, and uses tmp as the input byte.
+ * For example, PUT_BYTE could be "*output++ = tmp;" PUT_BYTE will be
+ * executed up to 4 times for values in the valid UTF-8 range and up to
+ * 7 times in the general case, depending on the length of the converted
+ * Unicode character.
+ */
+#define PUT_UTF8(val, tmp, PUT_BYTE)\
+ {\
+ int bytes, shift;\
+ uint32_t in = val;\
+ if (in < 0x80) {\
+ tmp = in;\
+ PUT_BYTE\
+ } else {\
+ bytes = (av_log2(in) + 4) / 5;\
+ shift = (bytes - 1) * 6;\
+ tmp = (256 - (256 >> bytes)) | (in >> shift);\
+ PUT_BYTE\
+ while (shift >= 6) {\
+ shift -= 6;\
+ tmp = 0x80 | ((in >> shift) & 0x3f);\
+ PUT_BYTE\
+ }\
+ }\
+ }
+
+/**
+ * @def PUT_UTF16(val, tmp, PUT_16BIT)
+ * Convert a 32-bit Unicode character to its UTF-16 encoded form (2 or 4 bytes).
+ * @param val is an input-only argument and should be of type uint32_t. It holds
+ * a UCS-4 encoded Unicode character that is to be converted to UTF-16. If
+ * val is given as a function it is executed only once.
+ * @param tmp is a temporary variable and should be of type uint16_t. It
+ * represents an intermediate value during conversion that is to be
+ * output by PUT_16BIT.
+ * @param PUT_16BIT writes the converted UTF-16 data to any proper destination
+ * in desired endianness. It could be a function or a statement, and uses tmp
+ * as the input byte. For example, PUT_BYTE could be "*output++ = tmp;"
+ * PUT_BYTE will be executed 1 or 2 times depending on input character.
+ */
+#define PUT_UTF16(val, tmp, PUT_16BIT)\
+ {\
+ uint32_t in = val;\
+ if (in < 0x10000) {\
+ tmp = in;\
+ PUT_16BIT\
+ } else {\
+ tmp = 0xD800 | ((in - 0x10000) >> 10);\
+ PUT_16BIT\
+ tmp = 0xDC00 | ((in - 0x10000) & 0x3FF);\
+ PUT_16BIT\
+ }\
+ }\
+
+
+
+#include "mem.h"
+
+#ifdef HAVE_AV_CONFIG_H
+# include "internal.h"
+#endif /* HAVE_AV_CONFIG_H */
+
+#endif /* AVUTIL_COMMON_H */
+
+/*
+ * The following definitions are outside the multiple inclusion guard
+ * to ensure they are immediately available in intmath.h.
+ */
+
+#ifndef av_ceil_log2
+# define av_ceil_log2 av_ceil_log2_c
+#endif
+#ifndef av_clip
+# define av_clip av_clip_c
+#endif
+#ifndef av_clip64
+# define av_clip64 av_clip64_c
+#endif
+#ifndef av_clip_uint8
+# define av_clip_uint8 av_clip_uint8_c
+#endif
+#ifndef av_clip_int8
+# define av_clip_int8 av_clip_int8_c
+#endif
+#ifndef av_clip_uint16
+# define av_clip_uint16 av_clip_uint16_c
+#endif
+#ifndef av_clip_int16
+# define av_clip_int16 av_clip_int16_c
+#endif
+#ifndef av_clipl_int32
+# define av_clipl_int32 av_clipl_int32_c
+#endif
+#ifndef av_clip_intp2
+# define av_clip_intp2 av_clip_intp2_c
+#endif
+#ifndef av_clip_uintp2
+# define av_clip_uintp2 av_clip_uintp2_c
+#endif
+#ifndef av_mod_uintp2
+# define av_mod_uintp2 av_mod_uintp2_c
+#endif
+#ifndef av_sat_add32
+# define av_sat_add32 av_sat_add32_c
+#endif
+#ifndef av_sat_dadd32
+# define av_sat_dadd32 av_sat_dadd32_c
+#endif
+#ifndef av_sat_sub32
+# define av_sat_sub32 av_sat_sub32_c
+#endif
+#ifndef av_sat_dsub32
+# define av_sat_dsub32 av_sat_dsub32_c
+#endif
+#ifndef av_clipf
+# define av_clipf av_clipf_c
+#endif
+#ifndef av_clipd
+# define av_clipd av_clipd_c
+#endif
+#ifndef av_popcount
+# define av_popcount av_popcount_c
+#endif
+#ifndef av_popcount64
+# define av_popcount64 av_popcount64_c
+#endif
+#ifndef av_parity
+# define av_parity av_parity_c
+#endif
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/cpu.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/cpu.h
new file mode 100644
index 0000000000..8bb9eb606b
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/cpu.h
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2000, 2001, 2002 Fabrice Bellard
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_CPU_H
+#define AVUTIL_CPU_H
+
+#include <stddef.h>
+
+#include "attributes.h"
+
+#define AV_CPU_FLAG_FORCE 0x80000000 /* force usage of selected flags (OR) */
+
+ /* lower 16 bits - CPU features */
+#define AV_CPU_FLAG_MMX 0x0001 ///< standard MMX
+#define AV_CPU_FLAG_MMXEXT 0x0002 ///< SSE integer functions or AMD MMX ext
+#define AV_CPU_FLAG_MMX2 0x0002 ///< SSE integer functions or AMD MMX ext
+#define AV_CPU_FLAG_3DNOW 0x0004 ///< AMD 3DNOW
+#define AV_CPU_FLAG_SSE 0x0008 ///< SSE functions
+#define AV_CPU_FLAG_SSE2 0x0010 ///< PIV SSE2 functions
+#define AV_CPU_FLAG_SSE2SLOW 0x40000000 ///< SSE2 supported, but usually not faster
+ ///< than regular MMX/SSE (e.g. Core1)
+#define AV_CPU_FLAG_3DNOWEXT 0x0020 ///< AMD 3DNowExt
+#define AV_CPU_FLAG_SSE3 0x0040 ///< Prescott SSE3 functions
+#define AV_CPU_FLAG_SSE3SLOW 0x20000000 ///< SSE3 supported, but usually not faster
+ ///< than regular MMX/SSE (e.g. Core1)
+#define AV_CPU_FLAG_SSSE3 0x0080 ///< Conroe SSSE3 functions
+#define AV_CPU_FLAG_SSSE3SLOW 0x4000000 ///< SSSE3 supported, but usually not faster
+#define AV_CPU_FLAG_ATOM 0x10000000 ///< Atom processor, some SSSE3 instructions are slower
+#define AV_CPU_FLAG_SSE4 0x0100 ///< Penryn SSE4.1 functions
+#define AV_CPU_FLAG_SSE42 0x0200 ///< Nehalem SSE4.2 functions
+#define AV_CPU_FLAG_AESNI 0x80000 ///< Advanced Encryption Standard functions
+#define AV_CPU_FLAG_AVX 0x4000 ///< AVX functions: requires OS support even if YMM registers aren't used
+#define AV_CPU_FLAG_AVXSLOW 0x8000000 ///< AVX supported, but slow when using YMM registers (e.g. Bulldozer)
+#define AV_CPU_FLAG_XOP 0x0400 ///< Bulldozer XOP functions
+#define AV_CPU_FLAG_FMA4 0x0800 ///< Bulldozer FMA4 functions
+#define AV_CPU_FLAG_CMOV 0x1000 ///< supports cmov instruction
+#define AV_CPU_FLAG_AVX2 0x8000 ///< AVX2 functions: requires OS support even if YMM registers aren't used
+#define AV_CPU_FLAG_FMA3 0x10000 ///< Haswell FMA3 functions
+#define AV_CPU_FLAG_BMI1 0x20000 ///< Bit Manipulation Instruction Set 1
+#define AV_CPU_FLAG_BMI2 0x40000 ///< Bit Manipulation Instruction Set 2
+#define AV_CPU_FLAG_AVX512 0x100000 ///< AVX-512 functions: requires OS support even if YMM/ZMM registers aren't used
+
+#define AV_CPU_FLAG_ALTIVEC 0x0001 ///< standard
+#define AV_CPU_FLAG_VSX 0x0002 ///< ISA 2.06
+#define AV_CPU_FLAG_POWER8 0x0004 ///< ISA 2.07
+
+#define AV_CPU_FLAG_ARMV5TE (1 << 0)
+#define AV_CPU_FLAG_ARMV6 (1 << 1)
+#define AV_CPU_FLAG_ARMV6T2 (1 << 2)
+#define AV_CPU_FLAG_VFP (1 << 3)
+#define AV_CPU_FLAG_VFPV3 (1 << 4)
+#define AV_CPU_FLAG_NEON (1 << 5)
+#define AV_CPU_FLAG_ARMV8 (1 << 6)
+#define AV_CPU_FLAG_VFP_VM (1 << 7) ///< VFPv2 vector mode, deprecated in ARMv7-A and unavailable in various CPUs implementations
+#define AV_CPU_FLAG_SETEND (1 <<16)
+
+/**
+ * Return the flags which specify extensions supported by the CPU.
+ * The returned value is affected by av_force_cpu_flags() if that was used
+ * before. So av_get_cpu_flags() can easily be used in an application to
+ * detect the enabled cpu flags.
+ */
+int av_get_cpu_flags(void);
+
+/**
+ * Disables cpu detection and forces the specified flags.
+ * -1 is a special case that disables forcing of specific flags.
+ */
+void av_force_cpu_flags(int flags);
+
+/**
+ * Set a mask on flags returned by av_get_cpu_flags().
+ * This function is mainly useful for testing.
+ * Please use av_force_cpu_flags() and av_get_cpu_flags() instead which are more flexible
+ */
+attribute_deprecated void av_set_cpu_flags_mask(int mask);
+
+/**
+ * Parse CPU flags from a string.
+ *
+ * The returned flags contain the specified flags as well as related unspecified flags.
+ *
+ * This function exists only for compatibility with libav.
+ * Please use av_parse_cpu_caps() when possible.
+ * @return a combination of AV_CPU_* flags, negative on error.
+ */
+attribute_deprecated
+int av_parse_cpu_flags(const char *s);
+
+/**
+ * Parse CPU caps from a string and update the given AV_CPU_* flags based on that.
+ *
+ * @return negative on error.
+ */
+int av_parse_cpu_caps(unsigned *flags, const char *s);
+
+/**
+ * @return the number of logical CPU cores present.
+ */
+int av_cpu_count(void);
+
+/**
+ * Get the maximum data alignment that may be required by FFmpeg.
+ *
+ * Note that this is affected by the build configuration and the CPU flags mask,
+ * so e.g. if the CPU supports AVX, but libavutil has been built with
+ * --disable-avx or the AV_CPU_FLAG_AVX flag has been disabled through
+ * av_set_cpu_flags_mask(), then this function will behave as if AVX is not
+ * present.
+ */
+size_t av_cpu_max_align(void);
+
+#endif /* AVUTIL_CPU_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/dict.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/dict.h
new file mode 100644
index 0000000000..118f1f00ed
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/dict.h
@@ -0,0 +1,200 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * Public dictionary API.
+ * @deprecated
+ * AVDictionary is provided for compatibility with libav. It is both in
+ * implementation as well as API inefficient. It does not scale and is
+ * extremely slow with large dictionaries.
+ * It is recommended that new code uses our tree container from tree.c/h
+ * where applicable, which uses AVL trees to achieve O(log n) performance.
+ */
+
+#ifndef AVUTIL_DICT_H
+#define AVUTIL_DICT_H
+
+#include <stdint.h>
+
+#include "version.h"
+
+/**
+ * @addtogroup lavu_dict AVDictionary
+ * @ingroup lavu_data
+ *
+ * @brief Simple key:value store
+ *
+ * @{
+ * Dictionaries are used for storing key:value pairs. To create
+ * an AVDictionary, simply pass an address of a NULL pointer to
+ * av_dict_set(). NULL can be used as an empty dictionary wherever
+ * a pointer to an AVDictionary is required.
+ * Use av_dict_get() to retrieve an entry or iterate over all
+ * entries and finally av_dict_free() to free the dictionary
+ * and all its contents.
+ *
+ @code
+ AVDictionary *d = NULL; // "create" an empty dictionary
+ AVDictionaryEntry *t = NULL;
+
+ av_dict_set(&d, "foo", "bar", 0); // add an entry
+
+ char *k = av_strdup("key"); // if your strings are already allocated,
+ char *v = av_strdup("value"); // you can avoid copying them like this
+ av_dict_set(&d, k, v, AV_DICT_DONT_STRDUP_KEY | AV_DICT_DONT_STRDUP_VAL);
+
+ while (t = av_dict_get(d, "", t, AV_DICT_IGNORE_SUFFIX)) {
+ <....> // iterate over all entries in d
+ }
+ av_dict_free(&d);
+ @endcode
+ */
+
+#define AV_DICT_MATCH_CASE 1 /**< Only get an entry with exact-case key match. Only relevant in av_dict_get(). */
+#define AV_DICT_IGNORE_SUFFIX 2 /**< Return first entry in a dictionary whose first part corresponds to the search key,
+ ignoring the suffix of the found key string. Only relevant in av_dict_get(). */
+#define AV_DICT_DONT_STRDUP_KEY 4 /**< Take ownership of a key that's been
+ allocated with av_malloc() or another memory allocation function. */
+#define AV_DICT_DONT_STRDUP_VAL 8 /**< Take ownership of a value that's been
+ allocated with av_malloc() or another memory allocation function. */
+#define AV_DICT_DONT_OVERWRITE 16 ///< Don't overwrite existing entries.
+#define AV_DICT_APPEND 32 /**< If the entry already exists, append to it. Note that no
+ delimiter is added, the strings are simply concatenated. */
+#define AV_DICT_MULTIKEY 64 /**< Allow to store several equal keys in the dictionary */
+
+typedef struct AVDictionaryEntry {
+ char *key;
+ char *value;
+} AVDictionaryEntry;
+
+typedef struct AVDictionary AVDictionary;
+
+/**
+ * Get a dictionary entry with matching key.
+ *
+ * The returned entry key or value must not be changed, or it will
+ * cause undefined behavior.
+ *
+ * To iterate through all the dictionary entries, you can set the matching key
+ * to the null string "" and set the AV_DICT_IGNORE_SUFFIX flag.
+ *
+ * @param prev Set to the previous matching element to find the next.
+ * If set to NULL the first matching element is returned.
+ * @param key matching key
+ * @param flags a collection of AV_DICT_* flags controlling how the entry is retrieved
+ * @return found entry or NULL in case no matching entry was found in the dictionary
+ */
+AVDictionaryEntry *av_dict_get(const AVDictionary *m, const char *key,
+ const AVDictionaryEntry *prev, int flags);
+
+/**
+ * Get number of entries in dictionary.
+ *
+ * @param m dictionary
+ * @return number of entries in dictionary
+ */
+int av_dict_count(const AVDictionary *m);
+
+/**
+ * Set the given entry in *pm, overwriting an existing entry.
+ *
+ * Note: If AV_DICT_DONT_STRDUP_KEY or AV_DICT_DONT_STRDUP_VAL is set,
+ * these arguments will be freed on error.
+ *
+ * Warning: Adding a new entry to a dictionary invalidates all existing entries
+ * previously returned with av_dict_get.
+ *
+ * @param pm pointer to a pointer to a dictionary struct. If *pm is NULL
+ * a dictionary struct is allocated and put in *pm.
+ * @param key entry key to add to *pm (will either be av_strduped or added as a new key depending on flags)
+ * @param value entry value to add to *pm (will be av_strduped or added as a new key depending on flags).
+ * Passing a NULL value will cause an existing entry to be deleted.
+ * @return >= 0 on success otherwise an error code <0
+ */
+int av_dict_set(AVDictionary **pm, const char *key, const char *value, int flags);
+
+/**
+ * Convenience wrapper for av_dict_set that converts the value to a string
+ * and stores it.
+ *
+ * Note: If AV_DICT_DONT_STRDUP_KEY is set, key will be freed on error.
+ */
+int av_dict_set_int(AVDictionary **pm, const char *key, int64_t value, int flags);
+
+/**
+ * Parse the key/value pairs list and add the parsed entries to a dictionary.
+ *
+ * In case of failure, all the successfully set entries are stored in
+ * *pm. You may need to manually free the created dictionary.
+ *
+ * @param key_val_sep a 0-terminated list of characters used to separate
+ * key from value
+ * @param pairs_sep a 0-terminated list of characters used to separate
+ * two pairs from each other
+ * @param flags flags to use when adding to dictionary.
+ * AV_DICT_DONT_STRDUP_KEY and AV_DICT_DONT_STRDUP_VAL
+ * are ignored since the key/value tokens will always
+ * be duplicated.
+ * @return 0 on success, negative AVERROR code on failure
+ */
+int av_dict_parse_string(AVDictionary **pm, const char *str,
+ const char *key_val_sep, const char *pairs_sep,
+ int flags);
+
+/**
+ * Copy entries from one AVDictionary struct into another.
+ * @param dst pointer to a pointer to a AVDictionary struct. If *dst is NULL,
+ * this function will allocate a struct for you and put it in *dst
+ * @param src pointer to source AVDictionary struct
+ * @param flags flags to use when setting entries in *dst
+ * @note metadata is read using the AV_DICT_IGNORE_SUFFIX flag
+ * @return 0 on success, negative AVERROR code on failure. If dst was allocated
+ * by this function, callers should free the associated memory.
+ */
+int av_dict_copy(AVDictionary **dst, const AVDictionary *src, int flags);
+
+/**
+ * Free all the memory allocated for an AVDictionary struct
+ * and all keys and values.
+ */
+void av_dict_free(AVDictionary **m);
+
+/**
+ * Get dictionary entries as a string.
+ *
+ * Create a string containing dictionary's entries.
+ * Such string may be passed back to av_dict_parse_string().
+ * @note String is escaped with backslashes ('\').
+ *
+ * @param[in] m dictionary
+ * @param[out] buffer Pointer to buffer that will be allocated with string containg entries.
+ * Buffer must be freed by the caller when is no longer needed.
+ * @param[in] key_val_sep character used to separate key from value
+ * @param[in] pairs_sep character used to separate two pairs from each other
+ * @return >= 0 on success, negative on error
+ * @warning Separators cannot be neither '\\' nor '\0'. They also cannot be the same.
+ */
+int av_dict_get_string(const AVDictionary *m, char **buffer,
+ const char key_val_sep, const char pairs_sep);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_DICT_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/error.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/error.h
new file mode 100644
index 0000000000..71df4da353
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/error.h
@@ -0,0 +1,126 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * error code definitions
+ */
+
+#ifndef AVUTIL_ERROR_H
+#define AVUTIL_ERROR_H
+
+#include <errno.h>
+#include <stddef.h>
+
+/**
+ * @addtogroup lavu_error
+ *
+ * @{
+ */
+
+
+/* error handling */
+#if EDOM > 0
+#define AVERROR(e) (-(e)) ///< Returns a negative error code from a POSIX error code, to return from library functions.
+#define AVUNERROR(e) (-(e)) ///< Returns a POSIX error code from a library function error return value.
+#else
+/* Some platforms have E* and errno already negated. */
+#define AVERROR(e) (e)
+#define AVUNERROR(e) (e)
+#endif
+
+#define FFERRTAG(a, b, c, d) (-(int)MKTAG(a, b, c, d))
+
+#define AVERROR_BSF_NOT_FOUND FFERRTAG(0xF8,'B','S','F') ///< Bitstream filter not found
+#define AVERROR_BUG FFERRTAG( 'B','U','G','!') ///< Internal bug, also see AVERROR_BUG2
+#define AVERROR_BUFFER_TOO_SMALL FFERRTAG( 'B','U','F','S') ///< Buffer too small
+#define AVERROR_DECODER_NOT_FOUND FFERRTAG(0xF8,'D','E','C') ///< Decoder not found
+#define AVERROR_DEMUXER_NOT_FOUND FFERRTAG(0xF8,'D','E','M') ///< Demuxer not found
+#define AVERROR_ENCODER_NOT_FOUND FFERRTAG(0xF8,'E','N','C') ///< Encoder not found
+#define AVERROR_EOF FFERRTAG( 'E','O','F',' ') ///< End of file
+#define AVERROR_EXIT FFERRTAG( 'E','X','I','T') ///< Immediate exit was requested; the called function should not be restarted
+#define AVERROR_EXTERNAL FFERRTAG( 'E','X','T',' ') ///< Generic error in an external library
+#define AVERROR_FILTER_NOT_FOUND FFERRTAG(0xF8,'F','I','L') ///< Filter not found
+#define AVERROR_INVALIDDATA FFERRTAG( 'I','N','D','A') ///< Invalid data found when processing input
+#define AVERROR_MUXER_NOT_FOUND FFERRTAG(0xF8,'M','U','X') ///< Muxer not found
+#define AVERROR_OPTION_NOT_FOUND FFERRTAG(0xF8,'O','P','T') ///< Option not found
+#define AVERROR_PATCHWELCOME FFERRTAG( 'P','A','W','E') ///< Not yet implemented in FFmpeg, patches welcome
+#define AVERROR_PROTOCOL_NOT_FOUND FFERRTAG(0xF8,'P','R','O') ///< Protocol not found
+
+#define AVERROR_STREAM_NOT_FOUND FFERRTAG(0xF8,'S','T','R') ///< Stream not found
+/**
+ * This is semantically identical to AVERROR_BUG
+ * it has been introduced in Libav after our AVERROR_BUG and with a modified value.
+ */
+#define AVERROR_BUG2 FFERRTAG( 'B','U','G',' ')
+#define AVERROR_UNKNOWN FFERRTAG( 'U','N','K','N') ///< Unknown error, typically from an external library
+#define AVERROR_EXPERIMENTAL (-0x2bb2afa8) ///< Requested feature is flagged experimental. Set strict_std_compliance if you really want to use it.
+#define AVERROR_INPUT_CHANGED (-0x636e6701) ///< Input changed between calls. Reconfiguration is required. (can be OR-ed with AVERROR_OUTPUT_CHANGED)
+#define AVERROR_OUTPUT_CHANGED (-0x636e6702) ///< Output changed between calls. Reconfiguration is required. (can be OR-ed with AVERROR_INPUT_CHANGED)
+/* HTTP & RTSP errors */
+#define AVERROR_HTTP_BAD_REQUEST FFERRTAG(0xF8,'4','0','0')
+#define AVERROR_HTTP_UNAUTHORIZED FFERRTAG(0xF8,'4','0','1')
+#define AVERROR_HTTP_FORBIDDEN FFERRTAG(0xF8,'4','0','3')
+#define AVERROR_HTTP_NOT_FOUND FFERRTAG(0xF8,'4','0','4')
+#define AVERROR_HTTP_OTHER_4XX FFERRTAG(0xF8,'4','X','X')
+#define AVERROR_HTTP_SERVER_ERROR FFERRTAG(0xF8,'5','X','X')
+
+#define AV_ERROR_MAX_STRING_SIZE 64
+
+/**
+ * Put a description of the AVERROR code errnum in errbuf.
+ * In case of failure the global variable errno is set to indicate the
+ * error. Even in case of failure av_strerror() will print a generic
+ * error message indicating the errnum provided to errbuf.
+ *
+ * @param errnum error code to describe
+ * @param errbuf buffer to which description is written
+ * @param errbuf_size the size in bytes of errbuf
+ * @return 0 on success, a negative value if a description for errnum
+ * cannot be found
+ */
+int av_strerror(int errnum, char *errbuf, size_t errbuf_size);
+
+/**
+ * Fill the provided buffer with a string containing an error string
+ * corresponding to the AVERROR code errnum.
+ *
+ * @param errbuf a buffer
+ * @param errbuf_size size in bytes of errbuf
+ * @param errnum error code to describe
+ * @return the buffer in input, filled with the error description
+ * @see av_strerror()
+ */
+static inline char *av_make_error_string(char *errbuf, size_t errbuf_size, int errnum)
+{
+ av_strerror(errnum, errbuf, errbuf_size);
+ return errbuf;
+}
+
+/**
+ * Convenience macro, the return value should be used only directly in
+ * function arguments but never stand-alone.
+ */
+#define av_err2str(errnum) \
+ av_make_error_string((char[AV_ERROR_MAX_STRING_SIZE]){0}, AV_ERROR_MAX_STRING_SIZE, errnum)
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_ERROR_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/frame.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/frame.h
new file mode 100644
index 0000000000..9d57d6ce66
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/frame.h
@@ -0,0 +1,893 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * @ingroup lavu_frame
+ * reference-counted frame API
+ */
+
+#ifndef AVUTIL_FRAME_H
+#define AVUTIL_FRAME_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "avutil.h"
+#include "buffer.h"
+#include "dict.h"
+#include "rational.h"
+#include "samplefmt.h"
+#include "pixfmt.h"
+#include "version.h"
+
+
+/**
+ * @defgroup lavu_frame AVFrame
+ * @ingroup lavu_data
+ *
+ * @{
+ * AVFrame is an abstraction for reference-counted raw multimedia data.
+ */
+
+enum AVFrameSideDataType {
+ /**
+ * The data is the AVPanScan struct defined in libavcodec.
+ */
+ AV_FRAME_DATA_PANSCAN,
+ /**
+ * ATSC A53 Part 4 Closed Captions.
+ * A53 CC bitstream is stored as uint8_t in AVFrameSideData.data.
+ * The number of bytes of CC data is AVFrameSideData.size.
+ */
+ AV_FRAME_DATA_A53_CC,
+ /**
+ * Stereoscopic 3d metadata.
+ * The data is the AVStereo3D struct defined in libavutil/stereo3d.h.
+ */
+ AV_FRAME_DATA_STEREO3D,
+ /**
+ * The data is the AVMatrixEncoding enum defined in libavutil/channel_layout.h.
+ */
+ AV_FRAME_DATA_MATRIXENCODING,
+ /**
+ * Metadata relevant to a downmix procedure.
+ * The data is the AVDownmixInfo struct defined in libavutil/downmix_info.h.
+ */
+ AV_FRAME_DATA_DOWNMIX_INFO,
+ /**
+ * ReplayGain information in the form of the AVReplayGain struct.
+ */
+ AV_FRAME_DATA_REPLAYGAIN,
+ /**
+ * This side data contains a 3x3 transformation matrix describing an affine
+ * transformation that needs to be applied to the frame for correct
+ * presentation.
+ *
+ * See libavutil/display.h for a detailed description of the data.
+ */
+ AV_FRAME_DATA_DISPLAYMATRIX,
+ /**
+ * Active Format Description data consisting of a single byte as specified
+ * in ETSI TS 101 154 using AVActiveFormatDescription enum.
+ */
+ AV_FRAME_DATA_AFD,
+ /**
+ * Motion vectors exported by some codecs (on demand through the export_mvs
+ * flag set in the libavcodec AVCodecContext flags2 option).
+ * The data is the AVMotionVector struct defined in
+ * libavutil/motion_vector.h.
+ */
+ AV_FRAME_DATA_MOTION_VECTORS,
+ /**
+ * Recommmends skipping the specified number of samples. This is exported
+ * only if the "skip_manual" AVOption is set in libavcodec.
+ * This has the same format as AV_PKT_DATA_SKIP_SAMPLES.
+ * @code
+ * u32le number of samples to skip from start of this packet
+ * u32le number of samples to skip from end of this packet
+ * u8 reason for start skip
+ * u8 reason for end skip (0=padding silence, 1=convergence)
+ * @endcode
+ */
+ AV_FRAME_DATA_SKIP_SAMPLES,
+ /**
+ * This side data must be associated with an audio frame and corresponds to
+ * enum AVAudioServiceType defined in avcodec.h.
+ */
+ AV_FRAME_DATA_AUDIO_SERVICE_TYPE,
+ /**
+ * Mastering display metadata associated with a video frame. The payload is
+ * an AVMasteringDisplayMetadata type and contains information about the
+ * mastering display color volume.
+ */
+ AV_FRAME_DATA_MASTERING_DISPLAY_METADATA,
+ /**
+ * The GOP timecode in 25 bit timecode format. Data format is 64-bit integer.
+ * This is set on the first frame of a GOP that has a temporal reference of 0.
+ */
+ AV_FRAME_DATA_GOP_TIMECODE,
+
+ /**
+ * The data represents the AVSphericalMapping structure defined in
+ * libavutil/spherical.h.
+ */
+ AV_FRAME_DATA_SPHERICAL,
+
+ /**
+ * Content light level (based on CTA-861.3). This payload contains data in
+ * the form of the AVContentLightMetadata struct.
+ */
+ AV_FRAME_DATA_CONTENT_LIGHT_LEVEL,
+
+ /**
+ * The data contains an ICC profile as an opaque octet buffer following the
+ * format described by ISO 15076-1 with an optional name defined in the
+ * metadata key entry "name".
+ */
+ AV_FRAME_DATA_ICC_PROFILE,
+
+#if FF_API_FRAME_QP
+ /**
+ * Implementation-specific description of the format of AV_FRAME_QP_TABLE_DATA.
+ * The contents of this side data are undocumented and internal; use
+ * av_frame_set_qp_table() and av_frame_get_qp_table() to access this in a
+ * meaningful way instead.
+ */
+ AV_FRAME_DATA_QP_TABLE_PROPERTIES,
+
+ /**
+ * Raw QP table data. Its format is described by
+ * AV_FRAME_DATA_QP_TABLE_PROPERTIES. Use av_frame_set_qp_table() and
+ * av_frame_get_qp_table() to access this instead.
+ */
+ AV_FRAME_DATA_QP_TABLE_DATA,
+#endif
+};
+
+enum AVActiveFormatDescription {
+ AV_AFD_SAME = 8,
+ AV_AFD_4_3 = 9,
+ AV_AFD_16_9 = 10,
+ AV_AFD_14_9 = 11,
+ AV_AFD_4_3_SP_14_9 = 13,
+ AV_AFD_16_9_SP_14_9 = 14,
+ AV_AFD_SP_4_3 = 15,
+};
+
+
+/**
+ * Structure to hold side data for an AVFrame.
+ *
+ * sizeof(AVFrameSideData) is not a part of the public ABI, so new fields may be added
+ * to the end with a minor bump.
+ */
+typedef struct AVFrameSideData {
+ enum AVFrameSideDataType type;
+ uint8_t *data;
+ int size;
+ AVDictionary *metadata;
+ AVBufferRef *buf;
+} AVFrameSideData;
+
+/**
+ * This structure describes decoded (raw) audio or video data.
+ *
+ * AVFrame must be allocated using av_frame_alloc(). Note that this only
+ * allocates the AVFrame itself, the buffers for the data must be managed
+ * through other means (see below).
+ * AVFrame must be freed with av_frame_free().
+ *
+ * AVFrame is typically allocated once and then reused multiple times to hold
+ * different data (e.g. a single AVFrame to hold frames received from a
+ * decoder). In such a case, av_frame_unref() will free any references held by
+ * the frame and reset it to its original clean state before it
+ * is reused again.
+ *
+ * The data described by an AVFrame is usually reference counted through the
+ * AVBuffer API. The underlying buffer references are stored in AVFrame.buf /
+ * AVFrame.extended_buf. An AVFrame is considered to be reference counted if at
+ * least one reference is set, i.e. if AVFrame.buf[0] != NULL. In such a case,
+ * every single data plane must be contained in one of the buffers in
+ * AVFrame.buf or AVFrame.extended_buf.
+ * There may be a single buffer for all the data, or one separate buffer for
+ * each plane, or anything in between.
+ *
+ * sizeof(AVFrame) is not a part of the public ABI, so new fields may be added
+ * to the end with a minor bump.
+ *
+ * Fields can be accessed through AVOptions, the name string used, matches the
+ * C structure field name for fields accessible through AVOptions. The AVClass
+ * for AVFrame can be obtained from avcodec_get_frame_class()
+ */
+typedef struct AVFrame {
+#define AV_NUM_DATA_POINTERS 8
+ /**
+ * pointer to the picture/channel planes.
+ * This might be different from the first allocated byte
+ *
+ * Some decoders access areas outside 0,0 - width,height, please
+ * see avcodec_align_dimensions2(). Some filters and swscale can read
+ * up to 16 bytes beyond the planes, if these filters are to be used,
+ * then 16 extra bytes must be allocated.
+ *
+ * NOTE: Except for hwaccel formats, pointers not needed by the format
+ * MUST be set to NULL.
+ */
+ uint8_t *data[AV_NUM_DATA_POINTERS];
+
+ /**
+ * For video, size in bytes of each picture line.
+ * For audio, size in bytes of each plane.
+ *
+ * For audio, only linesize[0] may be set. For planar audio, each channel
+ * plane must be the same size.
+ *
+ * For video the linesizes should be multiples of the CPUs alignment
+ * preference, this is 16 or 32 for modern desktop CPUs.
+ * Some code requires such alignment other code can be slower without
+ * correct alignment, for yet other it makes no difference.
+ *
+ * @note The linesize may be larger than the size of usable data -- there
+ * may be extra padding present for performance reasons.
+ */
+ int linesize[AV_NUM_DATA_POINTERS];
+
+ /**
+ * pointers to the data planes/channels.
+ *
+ * For video, this should simply point to data[].
+ *
+ * For planar audio, each channel has a separate data pointer, and
+ * linesize[0] contains the size of each channel buffer.
+ * For packed audio, there is just one data pointer, and linesize[0]
+ * contains the total size of the buffer for all channels.
+ *
+ * Note: Both data and extended_data should always be set in a valid frame,
+ * but for planar audio with more channels that can fit in data,
+ * extended_data must be used in order to access all channels.
+ */
+ uint8_t **extended_data;
+
+ /**
+ * @name Video dimensions
+ * Video frames only. The coded dimensions (in pixels) of the video frame,
+ * i.e. the size of the rectangle that contains some well-defined values.
+ *
+ * @note The part of the frame intended for display/presentation is further
+ * restricted by the @ref cropping "Cropping rectangle".
+ * @{
+ */
+ int width, height;
+ /**
+ * @}
+ */
+
+ /**
+ * number of audio samples (per channel) described by this frame
+ */
+ int nb_samples;
+
+ /**
+ * format of the frame, -1 if unknown or unset
+ * Values correspond to enum AVPixelFormat for video frames,
+ * enum AVSampleFormat for audio)
+ */
+ int format;
+
+ /**
+ * 1 -> keyframe, 0-> not
+ */
+ int key_frame;
+
+ /**
+ * Picture type of the frame.
+ */
+ enum AVPictureType pict_type;
+
+ /**
+ * Sample aspect ratio for the video frame, 0/1 if unknown/unspecified.
+ */
+ AVRational sample_aspect_ratio;
+
+ /**
+ * Presentation timestamp in time_base units (time when frame should be shown to user).
+ */
+ int64_t pts;
+
+#if FF_API_PKT_PTS
+ /**
+ * PTS copied from the AVPacket that was decoded to produce this frame.
+ * @deprecated use the pts field instead
+ */
+ attribute_deprecated
+ int64_t pkt_pts;
+#endif
+
+ /**
+ * DTS copied from the AVPacket that triggered returning this frame. (if frame threading isn't used)
+ * This is also the Presentation time of this AVFrame calculated from
+ * only AVPacket.dts values without pts values.
+ */
+ int64_t pkt_dts;
+
+ /**
+ * picture number in bitstream order
+ */
+ int coded_picture_number;
+ /**
+ * picture number in display order
+ */
+ int display_picture_number;
+
+ /**
+ * quality (between 1 (good) and FF_LAMBDA_MAX (bad))
+ */
+ int quality;
+
+ /**
+ * for some private data of the user
+ */
+ void *opaque;
+
+#if FF_API_ERROR_FRAME
+ /**
+ * @deprecated unused
+ */
+ attribute_deprecated
+ uint64_t error[AV_NUM_DATA_POINTERS];
+#endif
+
+ /**
+ * When decoding, this signals how much the picture must be delayed.
+ * extra_delay = repeat_pict / (2*fps)
+ */
+ int repeat_pict;
+
+ /**
+ * The content of the picture is interlaced.
+ */
+ int interlaced_frame;
+
+ /**
+ * If the content is interlaced, is top field displayed first.
+ */
+ int top_field_first;
+
+ /**
+ * Tell user application that palette has changed from previous frame.
+ */
+ int palette_has_changed;
+
+ /**
+ * reordered opaque 64 bits (generally an integer or a double precision float
+ * PTS but can be anything).
+ * The user sets AVCodecContext.reordered_opaque to represent the input at
+ * that time,
+ * the decoder reorders values as needed and sets AVFrame.reordered_opaque
+ * to exactly one of the values provided by the user through AVCodecContext.reordered_opaque
+ * @deprecated in favor of pkt_pts
+ */
+ int64_t reordered_opaque;
+
+ /**
+ * Sample rate of the audio data.
+ */
+ int sample_rate;
+
+ /**
+ * Channel layout of the audio data.
+ */
+ uint64_t channel_layout;
+
+ /**
+ * AVBuffer references backing the data for this frame. If all elements of
+ * this array are NULL, then this frame is not reference counted. This array
+ * must be filled contiguously -- if buf[i] is non-NULL then buf[j] must
+ * also be non-NULL for all j < i.
+ *
+ * There may be at most one AVBuffer per data plane, so for video this array
+ * always contains all the references. For planar audio with more than
+ * AV_NUM_DATA_POINTERS channels, there may be more buffers than can fit in
+ * this array. Then the extra AVBufferRef pointers are stored in the
+ * extended_buf array.
+ */
+ AVBufferRef *buf[AV_NUM_DATA_POINTERS];
+
+ /**
+ * For planar audio which requires more than AV_NUM_DATA_POINTERS
+ * AVBufferRef pointers, this array will hold all the references which
+ * cannot fit into AVFrame.buf.
+ *
+ * Note that this is different from AVFrame.extended_data, which always
+ * contains all the pointers. This array only contains the extra pointers,
+ * which cannot fit into AVFrame.buf.
+ *
+ * This array is always allocated using av_malloc() by whoever constructs
+ * the frame. It is freed in av_frame_unref().
+ */
+ AVBufferRef **extended_buf;
+ /**
+ * Number of elements in extended_buf.
+ */
+ int nb_extended_buf;
+
+ AVFrameSideData **side_data;
+ int nb_side_data;
+
+/**
+ * @defgroup lavu_frame_flags AV_FRAME_FLAGS
+ * @ingroup lavu_frame
+ * Flags describing additional frame properties.
+ *
+ * @{
+ */
+
+/**
+ * The frame data may be corrupted, e.g. due to decoding errors.
+ */
+#define AV_FRAME_FLAG_CORRUPT (1 << 0)
+/**
+ * A flag to mark the frames which need to be decoded, but shouldn't be output.
+ */
+#define AV_FRAME_FLAG_DISCARD (1 << 2)
+/**
+ * @}
+ */
+
+ /**
+ * Frame flags, a combination of @ref lavu_frame_flags
+ */
+ int flags;
+
+ /**
+ * MPEG vs JPEG YUV range.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorRange color_range;
+
+ enum AVColorPrimaries color_primaries;
+
+ enum AVColorTransferCharacteristic color_trc;
+
+ /**
+ * YUV colorspace type.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorSpace colorspace;
+
+ enum AVChromaLocation chroma_location;
+
+ /**
+ * frame timestamp estimated using various heuristics, in stream time base
+ * - encoding: unused
+ * - decoding: set by libavcodec, read by user.
+ */
+ int64_t best_effort_timestamp;
+
+ /**
+ * reordered pos from the last AVPacket that has been input into the decoder
+ * - encoding: unused
+ * - decoding: Read by user.
+ */
+ int64_t pkt_pos;
+
+ /**
+ * duration of the corresponding packet, expressed in
+ * AVStream->time_base units, 0 if unknown.
+ * - encoding: unused
+ * - decoding: Read by user.
+ */
+ int64_t pkt_duration;
+
+ /**
+ * metadata.
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ AVDictionary *metadata;
+
+ /**
+ * decode error flags of the frame, set to a combination of
+ * FF_DECODE_ERROR_xxx flags if the decoder produced a frame, but there
+ * were errors during the decoding.
+ * - encoding: unused
+ * - decoding: set by libavcodec, read by user.
+ */
+ int decode_error_flags;
+#define FF_DECODE_ERROR_INVALID_BITSTREAM 1
+#define FF_DECODE_ERROR_MISSING_REFERENCE 2
+
+ /**
+ * number of audio channels, only used for audio.
+ * - encoding: unused
+ * - decoding: Read by user.
+ */
+ int channels;
+
+ /**
+ * size of the corresponding packet containing the compressed
+ * frame.
+ * It is set to a negative value if unknown.
+ * - encoding: unused
+ * - decoding: set by libavcodec, read by user.
+ */
+ int pkt_size;
+
+#if FF_API_FRAME_QP
+ /**
+ * QP table
+ */
+ attribute_deprecated
+ int8_t *qscale_table;
+ /**
+ * QP store stride
+ */
+ attribute_deprecated
+ int qstride;
+
+ attribute_deprecated
+ int qscale_type;
+
+ attribute_deprecated
+ AVBufferRef *qp_table_buf;
+#endif
+ /**
+ * For hwaccel-format frames, this should be a reference to the
+ * AVHWFramesContext describing the frame.
+ */
+ AVBufferRef *hw_frames_ctx;
+
+ /**
+ * AVBufferRef for free use by the API user. FFmpeg will never check the
+ * contents of the buffer ref. FFmpeg calls av_buffer_unref() on it when
+ * the frame is unreferenced. av_frame_copy_props() calls create a new
+ * reference with av_buffer_ref() for the target frame's opaque_ref field.
+ *
+ * This is unrelated to the opaque field, although it serves a similar
+ * purpose.
+ */
+ AVBufferRef *opaque_ref;
+
+ /**
+ * @anchor cropping
+ * @name Cropping
+ * Video frames only. The number of pixels to discard from the the
+ * top/bottom/left/right border of the frame to obtain the sub-rectangle of
+ * the frame intended for presentation.
+ * @{
+ */
+ size_t crop_top;
+ size_t crop_bottom;
+ size_t crop_left;
+ size_t crop_right;
+ /**
+ * @}
+ */
+
+ /**
+ * AVBufferRef for internal use by a single libav* library.
+ * Must not be used to transfer data between libraries.
+ * Has to be NULL when ownership of the frame leaves the respective library.
+ *
+ * Code outside the FFmpeg libs should never check or change the contents of the buffer ref.
+ *
+ * FFmpeg calls av_buffer_unref() on it when the frame is unreferenced.
+ * av_frame_copy_props() calls create a new reference with av_buffer_ref()
+ * for the target frame's private_ref field.
+ */
+ AVBufferRef *private_ref;
+} AVFrame;
+
+#if FF_API_FRAME_GET_SET
+/**
+ * Accessors for some AVFrame fields. These used to be provided for ABI
+ * compatibility, and do not need to be used anymore.
+ */
+attribute_deprecated
+int64_t av_frame_get_best_effort_timestamp(const AVFrame *frame);
+attribute_deprecated
+void av_frame_set_best_effort_timestamp(AVFrame *frame, int64_t val);
+attribute_deprecated
+int64_t av_frame_get_pkt_duration (const AVFrame *frame);
+attribute_deprecated
+void av_frame_set_pkt_duration (AVFrame *frame, int64_t val);
+attribute_deprecated
+int64_t av_frame_get_pkt_pos (const AVFrame *frame);
+attribute_deprecated
+void av_frame_set_pkt_pos (AVFrame *frame, int64_t val);
+attribute_deprecated
+int64_t av_frame_get_channel_layout (const AVFrame *frame);
+attribute_deprecated
+void av_frame_set_channel_layout (AVFrame *frame, int64_t val);
+attribute_deprecated
+int av_frame_get_channels (const AVFrame *frame);
+attribute_deprecated
+void av_frame_set_channels (AVFrame *frame, int val);
+attribute_deprecated
+int av_frame_get_sample_rate (const AVFrame *frame);
+attribute_deprecated
+void av_frame_set_sample_rate (AVFrame *frame, int val);
+attribute_deprecated
+AVDictionary *av_frame_get_metadata (const AVFrame *frame);
+attribute_deprecated
+void av_frame_set_metadata (AVFrame *frame, AVDictionary *val);
+attribute_deprecated
+int av_frame_get_decode_error_flags (const AVFrame *frame);
+attribute_deprecated
+void av_frame_set_decode_error_flags (AVFrame *frame, int val);
+attribute_deprecated
+int av_frame_get_pkt_size(const AVFrame *frame);
+attribute_deprecated
+void av_frame_set_pkt_size(AVFrame *frame, int val);
+#if FF_API_FRAME_QP
+attribute_deprecated
+int8_t *av_frame_get_qp_table(AVFrame *f, int *stride, int *type);
+attribute_deprecated
+int av_frame_set_qp_table(AVFrame *f, AVBufferRef *buf, int stride, int type);
+#endif
+attribute_deprecated
+enum AVColorSpace av_frame_get_colorspace(const AVFrame *frame);
+attribute_deprecated
+void av_frame_set_colorspace(AVFrame *frame, enum AVColorSpace val);
+attribute_deprecated
+enum AVColorRange av_frame_get_color_range(const AVFrame *frame);
+attribute_deprecated
+void av_frame_set_color_range(AVFrame *frame, enum AVColorRange val);
+#endif
+
+/**
+ * Get the name of a colorspace.
+ * @return a static string identifying the colorspace; can be NULL.
+ */
+const char *av_get_colorspace_name(enum AVColorSpace val);
+
+/**
+ * Allocate an AVFrame and set its fields to default values. The resulting
+ * struct must be freed using av_frame_free().
+ *
+ * @return An AVFrame filled with default values or NULL on failure.
+ *
+ * @note this only allocates the AVFrame itself, not the data buffers. Those
+ * must be allocated through other means, e.g. with av_frame_get_buffer() or
+ * manually.
+ */
+AVFrame *av_frame_alloc(void);
+
+/**
+ * Free the frame and any dynamically allocated objects in it,
+ * e.g. extended_data. If the frame is reference counted, it will be
+ * unreferenced first.
+ *
+ * @param frame frame to be freed. The pointer will be set to NULL.
+ */
+void av_frame_free(AVFrame **frame);
+
+/**
+ * Set up a new reference to the data described by the source frame.
+ *
+ * Copy frame properties from src to dst and create a new reference for each
+ * AVBufferRef from src.
+ *
+ * If src is not reference counted, new buffers are allocated and the data is
+ * copied.
+ *
+ * @warning: dst MUST have been either unreferenced with av_frame_unref(dst),
+ * or newly allocated with av_frame_alloc() before calling this
+ * function, or undefined behavior will occur.
+ *
+ * @return 0 on success, a negative AVERROR on error
+ */
+int av_frame_ref(AVFrame *dst, const AVFrame *src);
+
+/**
+ * Create a new frame that references the same data as src.
+ *
+ * This is a shortcut for av_frame_alloc()+av_frame_ref().
+ *
+ * @return newly created AVFrame on success, NULL on error.
+ */
+AVFrame *av_frame_clone(const AVFrame *src);
+
+/**
+ * Unreference all the buffers referenced by frame and reset the frame fields.
+ */
+void av_frame_unref(AVFrame *frame);
+
+/**
+ * Move everything contained in src to dst and reset src.
+ *
+ * @warning: dst is not unreferenced, but directly overwritten without reading
+ * or deallocating its contents. Call av_frame_unref(dst) manually
+ * before calling this function to ensure that no memory is leaked.
+ */
+void av_frame_move_ref(AVFrame *dst, AVFrame *src);
+
+/**
+ * Allocate new buffer(s) for audio or video data.
+ *
+ * The following fields must be set on frame before calling this function:
+ * - format (pixel format for video, sample format for audio)
+ * - width and height for video
+ * - nb_samples and channel_layout for audio
+ *
+ * This function will fill AVFrame.data and AVFrame.buf arrays and, if
+ * necessary, allocate and fill AVFrame.extended_data and AVFrame.extended_buf.
+ * For planar formats, one buffer will be allocated for each plane.
+ *
+ * @warning: if frame already has been allocated, calling this function will
+ * leak memory. In addition, undefined behavior can occur in certain
+ * cases.
+ *
+ * @param frame frame in which to store the new buffers.
+ * @param align Required buffer size alignment. If equal to 0, alignment will be
+ * chosen automatically for the current CPU. It is highly
+ * recommended to pass 0 here unless you know what you are doing.
+ *
+ * @return 0 on success, a negative AVERROR on error.
+ */
+int av_frame_get_buffer(AVFrame *frame, int align);
+
+/**
+ * Check if the frame data is writable.
+ *
+ * @return A positive value if the frame data is writable (which is true if and
+ * only if each of the underlying buffers has only one reference, namely the one
+ * stored in this frame). Return 0 otherwise.
+ *
+ * If 1 is returned the answer is valid until av_buffer_ref() is called on any
+ * of the underlying AVBufferRefs (e.g. through av_frame_ref() or directly).
+ *
+ * @see av_frame_make_writable(), av_buffer_is_writable()
+ */
+int av_frame_is_writable(AVFrame *frame);
+
+/**
+ * Ensure that the frame data is writable, avoiding data copy if possible.
+ *
+ * Do nothing if the frame is writable, allocate new buffers and copy the data
+ * if it is not.
+ *
+ * @return 0 on success, a negative AVERROR on error.
+ *
+ * @see av_frame_is_writable(), av_buffer_is_writable(),
+ * av_buffer_make_writable()
+ */
+int av_frame_make_writable(AVFrame *frame);
+
+/**
+ * Copy the frame data from src to dst.
+ *
+ * This function does not allocate anything, dst must be already initialized and
+ * allocated with the same parameters as src.
+ *
+ * This function only copies the frame data (i.e. the contents of the data /
+ * extended data arrays), not any other properties.
+ *
+ * @return >= 0 on success, a negative AVERROR on error.
+ */
+int av_frame_copy(AVFrame *dst, const AVFrame *src);
+
+/**
+ * Copy only "metadata" fields from src to dst.
+ *
+ * Metadata for the purpose of this function are those fields that do not affect
+ * the data layout in the buffers. E.g. pts, sample rate (for audio) or sample
+ * aspect ratio (for video), but not width/height or channel layout.
+ * Side data is also copied.
+ */
+int av_frame_copy_props(AVFrame *dst, const AVFrame *src);
+
+/**
+ * Get the buffer reference a given data plane is stored in.
+ *
+ * @param plane index of the data plane of interest in frame->extended_data.
+ *
+ * @return the buffer reference that contains the plane or NULL if the input
+ * frame is not valid.
+ */
+AVBufferRef *av_frame_get_plane_buffer(AVFrame *frame, int plane);
+
+/**
+ * Add a new side data to a frame.
+ *
+ * @param frame a frame to which the side data should be added
+ * @param type type of the added side data
+ * @param size size of the side data
+ *
+ * @return newly added side data on success, NULL on error
+ */
+AVFrameSideData *av_frame_new_side_data(AVFrame *frame,
+ enum AVFrameSideDataType type,
+ int size);
+
+/**
+ * Add a new side data to a frame from an existing AVBufferRef
+ *
+ * @param frame a frame to which the side data should be added
+ * @param type the type of the added side data
+ * @param buf an AVBufferRef to add as side data. The ownership of
+ * the reference is transferred to the frame.
+ *
+ * @return newly added side data on success, NULL on error. On failure
+ * the frame is unchanged and the AVBufferRef remains owned by
+ * the caller.
+ */
+AVFrameSideData *av_frame_new_side_data_from_buf(AVFrame *frame,
+ enum AVFrameSideDataType type,
+ AVBufferRef *buf);
+
+/**
+ * @return a pointer to the side data of a given type on success, NULL if there
+ * is no side data with such type in this frame.
+ */
+AVFrameSideData *av_frame_get_side_data(const AVFrame *frame,
+ enum AVFrameSideDataType type);
+
+/**
+ * If side data of the supplied type exists in the frame, free it and remove it
+ * from the frame.
+ */
+void av_frame_remove_side_data(AVFrame *frame, enum AVFrameSideDataType type);
+
+
+/**
+ * Flags for frame cropping.
+ */
+enum {
+ /**
+ * Apply the maximum possible cropping, even if it requires setting the
+ * AVFrame.data[] entries to unaligned pointers. Passing unaligned data
+ * to FFmpeg API is generally not allowed, and causes undefined behavior
+ * (such as crashes). You can pass unaligned data only to FFmpeg APIs that
+ * are explicitly documented to accept it. Use this flag only if you
+ * absolutely know what you are doing.
+ */
+ AV_FRAME_CROP_UNALIGNED = 1 << 0,
+};
+
+/**
+ * Crop the given video AVFrame according to its crop_left/crop_top/crop_right/
+ * crop_bottom fields. If cropping is successful, the function will adjust the
+ * data pointers and the width/height fields, and set the crop fields to 0.
+ *
+ * In all cases, the cropping boundaries will be rounded to the inherent
+ * alignment of the pixel format. In some cases, such as for opaque hwaccel
+ * formats, the left/top cropping is ignored. The crop fields are set to 0 even
+ * if the cropping was rounded or ignored.
+ *
+ * @param frame the frame which should be cropped
+ * @param flags Some combination of AV_FRAME_CROP_* flags, or 0.
+ *
+ * @return >= 0 on success, a negative AVERROR on error. If the cropping fields
+ * were invalid, AVERROR(ERANGE) is returned, and nothing is changed.
+ */
+int av_frame_apply_cropping(AVFrame *frame, int flags);
+
+/**
+ * @return a string identifying the side data type
+ */
+const char *av_frame_side_data_name(enum AVFrameSideDataType type);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_FRAME_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/hwcontext.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/hwcontext.h
new file mode 100644
index 0000000000..f5a4b62387
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/hwcontext.h
@@ -0,0 +1,584 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_HWCONTEXT_H
+#define AVUTIL_HWCONTEXT_H
+
+#include "buffer.h"
+#include "frame.h"
+#include "log.h"
+#include "pixfmt.h"
+
+enum AVHWDeviceType {
+ AV_HWDEVICE_TYPE_NONE,
+ AV_HWDEVICE_TYPE_VDPAU,
+ AV_HWDEVICE_TYPE_CUDA,
+ AV_HWDEVICE_TYPE_VAAPI,
+ AV_HWDEVICE_TYPE_DXVA2,
+ AV_HWDEVICE_TYPE_QSV,
+ AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
+ AV_HWDEVICE_TYPE_D3D11VA,
+ AV_HWDEVICE_TYPE_DRM,
+ AV_HWDEVICE_TYPE_OPENCL,
+ AV_HWDEVICE_TYPE_MEDIACODEC,
+};
+
+typedef struct AVHWDeviceInternal AVHWDeviceInternal;
+
+/**
+ * This struct aggregates all the (hardware/vendor-specific) "high-level" state,
+ * i.e. state that is not tied to a concrete processing configuration.
+ * E.g., in an API that supports hardware-accelerated encoding and decoding,
+ * this struct will (if possible) wrap the state that is common to both encoding
+ * and decoding and from which specific instances of encoders or decoders can be
+ * derived.
+ *
+ * This struct is reference-counted with the AVBuffer mechanism. The
+ * av_hwdevice_ctx_alloc() constructor yields a reference, whose data field
+ * points to the actual AVHWDeviceContext. Further objects derived from
+ * AVHWDeviceContext (such as AVHWFramesContext, describing a frame pool with
+ * specific properties) will hold an internal reference to it. After all the
+ * references are released, the AVHWDeviceContext itself will be freed,
+ * optionally invoking a user-specified callback for uninitializing the hardware
+ * state.
+ */
+typedef struct AVHWDeviceContext {
+ /**
+ * A class for logging. Set by av_hwdevice_ctx_alloc().
+ */
+ const AVClass *av_class;
+
+ /**
+ * Private data used internally by libavutil. Must not be accessed in any
+ * way by the caller.
+ */
+ AVHWDeviceInternal *internal;
+
+ /**
+ * This field identifies the underlying API used for hardware access.
+ *
+ * This field is set when this struct is allocated and never changed
+ * afterwards.
+ */
+ enum AVHWDeviceType type;
+
+ /**
+ * The format-specific data, allocated and freed by libavutil along with
+ * this context.
+ *
+ * Should be cast by the user to the format-specific context defined in the
+ * corresponding header (hwcontext_*.h) and filled as described in the
+ * documentation before calling av_hwdevice_ctx_init().
+ *
+ * After calling av_hwdevice_ctx_init() this struct should not be modified
+ * by the caller.
+ */
+ void *hwctx;
+
+ /**
+ * This field may be set by the caller before calling av_hwdevice_ctx_init().
+ *
+ * If non-NULL, this callback will be called when the last reference to
+ * this context is unreferenced, immediately before it is freed.
+ *
+ * @note when other objects (e.g an AVHWFramesContext) are derived from this
+ * struct, this callback will be invoked after all such child objects
+ * are fully uninitialized and their respective destructors invoked.
+ */
+ void (*free)(struct AVHWDeviceContext *ctx);
+
+ /**
+ * Arbitrary user data, to be used e.g. by the free() callback.
+ */
+ void *user_opaque;
+} AVHWDeviceContext;
+
+typedef struct AVHWFramesInternal AVHWFramesInternal;
+
+/**
+ * This struct describes a set or pool of "hardware" frames (i.e. those with
+ * data not located in normal system memory). All the frames in the pool are
+ * assumed to be allocated in the same way and interchangeable.
+ *
+ * This struct is reference-counted with the AVBuffer mechanism and tied to a
+ * given AVHWDeviceContext instance. The av_hwframe_ctx_alloc() constructor
+ * yields a reference, whose data field points to the actual AVHWFramesContext
+ * struct.
+ */
+typedef struct AVHWFramesContext {
+ /**
+ * A class for logging.
+ */
+ const AVClass *av_class;
+
+ /**
+ * Private data used internally by libavutil. Must not be accessed in any
+ * way by the caller.
+ */
+ AVHWFramesInternal *internal;
+
+ /**
+ * A reference to the parent AVHWDeviceContext. This reference is owned and
+ * managed by the enclosing AVHWFramesContext, but the caller may derive
+ * additional references from it.
+ */
+ AVBufferRef *device_ref;
+
+ /**
+ * The parent AVHWDeviceContext. This is simply a pointer to
+ * device_ref->data provided for convenience.
+ *
+ * Set by libavutil in av_hwframe_ctx_init().
+ */
+ AVHWDeviceContext *device_ctx;
+
+ /**
+ * The format-specific data, allocated and freed automatically along with
+ * this context.
+ *
+ * Should be cast by the user to the format-specific context defined in the
+ * corresponding header (hwframe_*.h) and filled as described in the
+ * documentation before calling av_hwframe_ctx_init().
+ *
+ * After any frames using this context are created, the contents of this
+ * struct should not be modified by the caller.
+ */
+ void *hwctx;
+
+ /**
+ * This field may be set by the caller before calling av_hwframe_ctx_init().
+ *
+ * If non-NULL, this callback will be called when the last reference to
+ * this context is unreferenced, immediately before it is freed.
+ */
+ void (*free)(struct AVHWFramesContext *ctx);
+
+ /**
+ * Arbitrary user data, to be used e.g. by the free() callback.
+ */
+ void *user_opaque;
+
+ /**
+ * A pool from which the frames are allocated by av_hwframe_get_buffer().
+ * This field may be set by the caller before calling av_hwframe_ctx_init().
+ * The buffers returned by calling av_buffer_pool_get() on this pool must
+ * have the properties described in the documentation in the corresponding hw
+ * type's header (hwcontext_*.h). The pool will be freed strictly before
+ * this struct's free() callback is invoked.
+ *
+ * This field may be NULL, then libavutil will attempt to allocate a pool
+ * internally. Note that certain device types enforce pools allocated at
+ * fixed size (frame count), which cannot be extended dynamically. In such a
+ * case, initial_pool_size must be set appropriately.
+ */
+ AVBufferPool *pool;
+
+ /**
+ * Initial size of the frame pool. If a device type does not support
+ * dynamically resizing the pool, then this is also the maximum pool size.
+ *
+ * May be set by the caller before calling av_hwframe_ctx_init(). Must be
+ * set if pool is NULL and the device type does not support dynamic pools.
+ */
+ int initial_pool_size;
+
+ /**
+ * The pixel format identifying the underlying HW surface type.
+ *
+ * Must be a hwaccel format, i.e. the corresponding descriptor must have the
+ * AV_PIX_FMT_FLAG_HWACCEL flag set.
+ *
+ * Must be set by the user before calling av_hwframe_ctx_init().
+ */
+ enum AVPixelFormat format;
+
+ /**
+ * The pixel format identifying the actual data layout of the hardware
+ * frames.
+ *
+ * Must be set by the caller before calling av_hwframe_ctx_init().
+ *
+ * @note when the underlying API does not provide the exact data layout, but
+ * only the colorspace/bit depth, this field should be set to the fully
+ * planar version of that format (e.g. for 8-bit 420 YUV it should be
+ * AV_PIX_FMT_YUV420P, not AV_PIX_FMT_NV12 or anything else).
+ */
+ enum AVPixelFormat sw_format;
+
+ /**
+ * The allocated dimensions of the frames in this pool.
+ *
+ * Must be set by the user before calling av_hwframe_ctx_init().
+ */
+ int width, height;
+} AVHWFramesContext;
+
+/**
+ * Look up an AVHWDeviceType by name.
+ *
+ * @param name String name of the device type (case-insensitive).
+ * @return The type from enum AVHWDeviceType, or AV_HWDEVICE_TYPE_NONE if
+ * not found.
+ */
+enum AVHWDeviceType av_hwdevice_find_type_by_name(const char *name);
+
+/** Get the string name of an AVHWDeviceType.
+ *
+ * @param type Type from enum AVHWDeviceType.
+ * @return Pointer to a static string containing the name, or NULL if the type
+ * is not valid.
+ */
+const char *av_hwdevice_get_type_name(enum AVHWDeviceType type);
+
+/**
+ * Iterate over supported device types.
+ *
+ * @param type AV_HWDEVICE_TYPE_NONE initially, then the previous type
+ * returned by this function in subsequent iterations.
+ * @return The next usable device type from enum AVHWDeviceType, or
+ * AV_HWDEVICE_TYPE_NONE if there are no more.
+ */
+enum AVHWDeviceType av_hwdevice_iterate_types(enum AVHWDeviceType prev);
+
+/**
+ * Allocate an AVHWDeviceContext for a given hardware type.
+ *
+ * @param type the type of the hardware device to allocate.
+ * @return a reference to the newly created AVHWDeviceContext on success or NULL
+ * on failure.
+ */
+AVBufferRef *av_hwdevice_ctx_alloc(enum AVHWDeviceType type);
+
+/**
+ * Finalize the device context before use. This function must be called after
+ * the context is filled with all the required information and before it is
+ * used in any way.
+ *
+ * @param ref a reference to the AVHWDeviceContext
+ * @return 0 on success, a negative AVERROR code on failure
+ */
+int av_hwdevice_ctx_init(AVBufferRef *ref);
+
+/**
+ * Open a device of the specified type and create an AVHWDeviceContext for it.
+ *
+ * This is a convenience function intended to cover the simple cases. Callers
+ * who need to fine-tune device creation/management should open the device
+ * manually and then wrap it in an AVHWDeviceContext using
+ * av_hwdevice_ctx_alloc()/av_hwdevice_ctx_init().
+ *
+ * The returned context is already initialized and ready for use, the caller
+ * should not call av_hwdevice_ctx_init() on it. The user_opaque/free fields of
+ * the created AVHWDeviceContext are set by this function and should not be
+ * touched by the caller.
+ *
+ * @param device_ctx On success, a reference to the newly-created device context
+ * will be written here. The reference is owned by the caller
+ * and must be released with av_buffer_unref() when no longer
+ * needed. On failure, NULL will be written to this pointer.
+ * @param type The type of the device to create.
+ * @param device A type-specific string identifying the device to open.
+ * @param opts A dictionary of additional (type-specific) options to use in
+ * opening the device. The dictionary remains owned by the caller.
+ * @param flags currently unused
+ *
+ * @return 0 on success, a negative AVERROR code on failure.
+ */
+int av_hwdevice_ctx_create(AVBufferRef **device_ctx, enum AVHWDeviceType type,
+ const char *device, AVDictionary *opts, int flags);
+
+/**
+ * Create a new device of the specified type from an existing device.
+ *
+ * If the source device is a device of the target type or was originally
+ * derived from such a device (possibly through one or more intermediate
+ * devices of other types), then this will return a reference to the
+ * existing device of the same type as is requested.
+ *
+ * Otherwise, it will attempt to derive a new device from the given source
+ * device. If direct derivation to the new type is not implemented, it will
+ * attempt the same derivation from each ancestor of the source device in
+ * turn looking for an implemented derivation method.
+ *
+ * @param dst_ctx On success, a reference to the newly-created
+ * AVHWDeviceContext.
+ * @param type The type of the new device to create.
+ * @param src_ctx A reference to an existing AVHWDeviceContext which will be
+ * used to create the new device.
+ * @param flags Currently unused; should be set to zero.
+ * @return Zero on success, a negative AVERROR code on failure.
+ */
+int av_hwdevice_ctx_create_derived(AVBufferRef **dst_ctx,
+ enum AVHWDeviceType type,
+ AVBufferRef *src_ctx, int flags);
+
+
+/**
+ * Allocate an AVHWFramesContext tied to a given device context.
+ *
+ * @param device_ctx a reference to a AVHWDeviceContext. This function will make
+ * a new reference for internal use, the one passed to the
+ * function remains owned by the caller.
+ * @return a reference to the newly created AVHWFramesContext on success or NULL
+ * on failure.
+ */
+AVBufferRef *av_hwframe_ctx_alloc(AVBufferRef *device_ctx);
+
+/**
+ * Finalize the context before use. This function must be called after the
+ * context is filled with all the required information and before it is attached
+ * to any frames.
+ *
+ * @param ref a reference to the AVHWFramesContext
+ * @return 0 on success, a negative AVERROR code on failure
+ */
+int av_hwframe_ctx_init(AVBufferRef *ref);
+
+/**
+ * Allocate a new frame attached to the given AVHWFramesContext.
+ *
+ * @param hwframe_ctx a reference to an AVHWFramesContext
+ * @param frame an empty (freshly allocated or unreffed) frame to be filled with
+ * newly allocated buffers.
+ * @param flags currently unused, should be set to zero
+ * @return 0 on success, a negative AVERROR code on failure
+ */
+int av_hwframe_get_buffer(AVBufferRef *hwframe_ctx, AVFrame *frame, int flags);
+
+/**
+ * Copy data to or from a hw surface. At least one of dst/src must have an
+ * AVHWFramesContext attached.
+ *
+ * If src has an AVHWFramesContext attached, then the format of dst (if set)
+ * must use one of the formats returned by av_hwframe_transfer_get_formats(src,
+ * AV_HWFRAME_TRANSFER_DIRECTION_FROM).
+ * If dst has an AVHWFramesContext attached, then the format of src must use one
+ * of the formats returned by av_hwframe_transfer_get_formats(dst,
+ * AV_HWFRAME_TRANSFER_DIRECTION_TO)
+ *
+ * dst may be "clean" (i.e. with data/buf pointers unset), in which case the
+ * data buffers will be allocated by this function using av_frame_get_buffer().
+ * If dst->format is set, then this format will be used, otherwise (when
+ * dst->format is AV_PIX_FMT_NONE) the first acceptable format will be chosen.
+ *
+ * The two frames must have matching allocated dimensions (i.e. equal to
+ * AVHWFramesContext.width/height), since not all device types support
+ * transferring a sub-rectangle of the whole surface. The display dimensions
+ * (i.e. AVFrame.width/height) may be smaller than the allocated dimensions, but
+ * also have to be equal for both frames. When the display dimensions are
+ * smaller than the allocated dimensions, the content of the padding in the
+ * destination frame is unspecified.
+ *
+ * @param dst the destination frame. dst is not touched on failure.
+ * @param src the source frame.
+ * @param flags currently unused, should be set to zero
+ * @return 0 on success, a negative AVERROR error code on failure.
+ */
+int av_hwframe_transfer_data(AVFrame *dst, const AVFrame *src, int flags);
+
+enum AVHWFrameTransferDirection {
+ /**
+ * Transfer the data from the queried hw frame.
+ */
+ AV_HWFRAME_TRANSFER_DIRECTION_FROM,
+
+ /**
+ * Transfer the data to the queried hw frame.
+ */
+ AV_HWFRAME_TRANSFER_DIRECTION_TO,
+};
+
+/**
+ * Get a list of possible source or target formats usable in
+ * av_hwframe_transfer_data().
+ *
+ * @param hwframe_ctx the frame context to obtain the information for
+ * @param dir the direction of the transfer
+ * @param formats the pointer to the output format list will be written here.
+ * The list is terminated with AV_PIX_FMT_NONE and must be freed
+ * by the caller when no longer needed using av_free().
+ * If this function returns successfully, the format list will
+ * have at least one item (not counting the terminator).
+ * On failure, the contents of this pointer are unspecified.
+ * @param flags currently unused, should be set to zero
+ * @return 0 on success, a negative AVERROR code on failure.
+ */
+int av_hwframe_transfer_get_formats(AVBufferRef *hwframe_ctx,
+ enum AVHWFrameTransferDirection dir,
+ enum AVPixelFormat **formats, int flags);
+
+
+/**
+ * This struct describes the constraints on hardware frames attached to
+ * a given device with a hardware-specific configuration. This is returned
+ * by av_hwdevice_get_hwframe_constraints() and must be freed by
+ * av_hwframe_constraints_free() after use.
+ */
+typedef struct AVHWFramesConstraints {
+ /**
+ * A list of possible values for format in the hw_frames_ctx,
+ * terminated by AV_PIX_FMT_NONE. This member will always be filled.
+ */
+ enum AVPixelFormat *valid_hw_formats;
+
+ /**
+ * A list of possible values for sw_format in the hw_frames_ctx,
+ * terminated by AV_PIX_FMT_NONE. Can be NULL if this information is
+ * not known.
+ */
+ enum AVPixelFormat *valid_sw_formats;
+
+ /**
+ * The minimum size of frames in this hw_frames_ctx.
+ * (Zero if not known.)
+ */
+ int min_width;
+ int min_height;
+
+ /**
+ * The maximum size of frames in this hw_frames_ctx.
+ * (INT_MAX if not known / no limit.)
+ */
+ int max_width;
+ int max_height;
+} AVHWFramesConstraints;
+
+/**
+ * Allocate a HW-specific configuration structure for a given HW device.
+ * After use, the user must free all members as required by the specific
+ * hardware structure being used, then free the structure itself with
+ * av_free().
+ *
+ * @param device_ctx a reference to the associated AVHWDeviceContext.
+ * @return The newly created HW-specific configuration structure on
+ * success or NULL on failure.
+ */
+void *av_hwdevice_hwconfig_alloc(AVBufferRef *device_ctx);
+
+/**
+ * Get the constraints on HW frames given a device and the HW-specific
+ * configuration to be used with that device. If no HW-specific
+ * configuration is provided, returns the maximum possible capabilities
+ * of the device.
+ *
+ * @param ref a reference to the associated AVHWDeviceContext.
+ * @param hwconfig a filled HW-specific configuration structure, or NULL
+ * to return the maximum possible capabilities of the device.
+ * @return AVHWFramesConstraints structure describing the constraints
+ * on the device, or NULL if not available.
+ */
+AVHWFramesConstraints *av_hwdevice_get_hwframe_constraints(AVBufferRef *ref,
+ const void *hwconfig);
+
+/**
+ * Free an AVHWFrameConstraints structure.
+ *
+ * @param constraints The (filled or unfilled) AVHWFrameConstraints structure.
+ */
+void av_hwframe_constraints_free(AVHWFramesConstraints **constraints);
+
+
+/**
+ * Flags to apply to frame mappings.
+ */
+enum {
+ /**
+ * The mapping must be readable.
+ */
+ AV_HWFRAME_MAP_READ = 1 << 0,
+ /**
+ * The mapping must be writeable.
+ */
+ AV_HWFRAME_MAP_WRITE = 1 << 1,
+ /**
+ * The mapped frame will be overwritten completely in subsequent
+ * operations, so the current frame data need not be loaded. Any values
+ * which are not overwritten are unspecified.
+ */
+ AV_HWFRAME_MAP_OVERWRITE = 1 << 2,
+ /**
+ * The mapping must be direct. That is, there must not be any copying in
+ * the map or unmap steps. Note that performance of direct mappings may
+ * be much lower than normal memory.
+ */
+ AV_HWFRAME_MAP_DIRECT = 1 << 3,
+};
+
+/**
+ * Map a hardware frame.
+ *
+ * This has a number of different possible effects, depending on the format
+ * and origin of the src and dst frames. On input, src should be a usable
+ * frame with valid buffers and dst should be blank (typically as just created
+ * by av_frame_alloc()). src should have an associated hwframe context, and
+ * dst may optionally have a format and associated hwframe context.
+ *
+ * If src was created by mapping a frame from the hwframe context of dst,
+ * then this function undoes the mapping - dst is replaced by a reference to
+ * the frame that src was originally mapped from.
+ *
+ * If both src and dst have an associated hwframe context, then this function
+ * attempts to map the src frame from its hardware context to that of dst and
+ * then fill dst with appropriate data to be usable there. This will only be
+ * possible if the hwframe contexts and associated devices are compatible -
+ * given compatible devices, av_hwframe_ctx_create_derived() can be used to
+ * create a hwframe context for dst in which mapping should be possible.
+ *
+ * If src has a hwframe context but dst does not, then the src frame is
+ * mapped to normal memory and should thereafter be usable as a normal frame.
+ * If the format is set on dst, then the mapping will attempt to create dst
+ * with that format and fail if it is not possible. If format is unset (is
+ * AV_PIX_FMT_NONE) then dst will be mapped with whatever the most appropriate
+ * format to use is (probably the sw_format of the src hwframe context).
+ *
+ * A return value of AVERROR(ENOSYS) indicates that the mapping is not
+ * possible with the given arguments and hwframe setup, while other return
+ * values indicate that it failed somehow.
+ *
+ * @param dst Destination frame, to contain the mapping.
+ * @param src Source frame, to be mapped.
+ * @param flags Some combination of AV_HWFRAME_MAP_* flags.
+ * @return Zero on success, negative AVERROR code on failure.
+ */
+int av_hwframe_map(AVFrame *dst, const AVFrame *src, int flags);
+
+
+/**
+ * Create and initialise an AVHWFramesContext as a mapping of another existing
+ * AVHWFramesContext on a different device.
+ *
+ * av_hwframe_ctx_init() should not be called after this.
+ *
+ * @param derived_frame_ctx On success, a reference to the newly created
+ * AVHWFramesContext.
+ * @param derived_device_ctx A reference to the device to create the new
+ * AVHWFramesContext on.
+ * @param source_frame_ctx A reference to an existing AVHWFramesContext
+ * which will be mapped to the derived context.
+ * @param flags Some combination of AV_HWFRAME_MAP_* flags, defining the
+ * mapping parameters to apply to frames which are allocated
+ * in the derived device.
+ * @return Zero on success, negative AVERROR code on failure.
+ */
+int av_hwframe_ctx_create_derived(AVBufferRef **derived_frame_ctx,
+ enum AVPixelFormat format,
+ AVBufferRef *derived_device_ctx,
+ AVBufferRef *source_frame_ctx,
+ int flags);
+
+#endif /* AVUTIL_HWCONTEXT_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/hwcontext_drm.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/hwcontext_drm.h
new file mode 100644
index 0000000000..42709f215e
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/hwcontext_drm.h
@@ -0,0 +1,169 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_HWCONTEXT_DRM_H
+#define AVUTIL_HWCONTEXT_DRM_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+/**
+ * @file
+ * API-specific header for AV_HWDEVICE_TYPE_DRM.
+ *
+ * Internal frame allocation is not currently supported - all frames
+ * must be allocated by the user. Thus AVHWFramesContext is always
+ * NULL, though this may change if support for frame allocation is
+ * added in future.
+ */
+
+enum {
+ /**
+ * The maximum number of layers/planes in a DRM frame.
+ */
+ AV_DRM_MAX_PLANES = 4
+};
+
+/**
+ * DRM object descriptor.
+ *
+ * Describes a single DRM object, addressing it as a PRIME file
+ * descriptor.
+ */
+typedef struct AVDRMObjectDescriptor {
+ /**
+ * DRM PRIME fd for the object.
+ */
+ int fd;
+ /**
+ * Total size of the object.
+ *
+ * (This includes any parts not which do not contain image data.)
+ */
+ size_t size;
+ /**
+ * Format modifier applied to the object (DRM_FORMAT_MOD_*).
+ *
+ * If the format modifier is unknown then this should be set to
+ * DRM_FORMAT_MOD_INVALID.
+ */
+ uint64_t format_modifier;
+} AVDRMObjectDescriptor;
+
+/**
+ * DRM plane descriptor.
+ *
+ * Describes a single plane of a layer, which is contained within
+ * a single object.
+ */
+typedef struct AVDRMPlaneDescriptor {
+ /**
+ * Index of the object containing this plane in the objects
+ * array of the enclosing frame descriptor.
+ */
+ int object_index;
+ /**
+ * Offset within that object of this plane.
+ */
+ ptrdiff_t offset;
+ /**
+ * Pitch (linesize) of this plane.
+ */
+ ptrdiff_t pitch;
+} AVDRMPlaneDescriptor;
+
+/**
+ * DRM layer descriptor.
+ *
+ * Describes a single layer within a frame. This has the structure
+ * defined by its format, and will contain one or more planes.
+ */
+typedef struct AVDRMLayerDescriptor {
+ /**
+ * Format of the layer (DRM_FORMAT_*).
+ */
+ uint32_t format;
+ /**
+ * Number of planes in the layer.
+ *
+ * This must match the number of planes required by format.
+ */
+ int nb_planes;
+ /**
+ * Array of planes in this layer.
+ */
+ AVDRMPlaneDescriptor planes[AV_DRM_MAX_PLANES];
+} AVDRMLayerDescriptor;
+
+/**
+ * DRM frame descriptor.
+ *
+ * This is used as the data pointer for AV_PIX_FMT_DRM_PRIME frames.
+ * It is also used by user-allocated frame pools - allocating in
+ * AVHWFramesContext.pool must return AVBufferRefs which contain
+ * an object of this type.
+ *
+ * The fields of this structure should be set such it can be
+ * imported directly by EGL using the EGL_EXT_image_dma_buf_import
+ * and EGL_EXT_image_dma_buf_import_modifiers extensions.
+ * (Note that the exact layout of a particular format may vary between
+ * platforms - we only specify that the same platform should be able
+ * to import it.)
+ *
+ * The total number of planes must not exceed AV_DRM_MAX_PLANES, and
+ * the order of the planes by increasing layer index followed by
+ * increasing plane index must be the same as the order which would
+ * be used for the data pointers in the equivalent software format.
+ */
+typedef struct AVDRMFrameDescriptor {
+ /**
+ * Number of DRM objects making up this frame.
+ */
+ int nb_objects;
+ /**
+ * Array of objects making up the frame.
+ */
+ AVDRMObjectDescriptor objects[AV_DRM_MAX_PLANES];
+ /**
+ * Number of layers in the frame.
+ */
+ int nb_layers;
+ /**
+ * Array of layers in the frame.
+ */
+ AVDRMLayerDescriptor layers[AV_DRM_MAX_PLANES];
+} AVDRMFrameDescriptor;
+
+/**
+ * DRM device.
+ *
+ * Allocated as AVHWDeviceContext.hwctx.
+ */
+typedef struct AVDRMDeviceContext {
+ /**
+ * File descriptor of DRM device.
+ *
+ * This is used as the device to create frames on, and may also be
+ * used in some derivation and mapping operations.
+ *
+ * If no device is required, set to -1.
+ */
+ int fd;
+} AVDRMDeviceContext;
+
+#endif /* AVUTIL_HWCONTEXT_DRM_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/hwcontext_vaapi.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/hwcontext_vaapi.h
new file mode 100644
index 0000000000..9c4d8028bc
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/hwcontext_vaapi.h
@@ -0,0 +1,117 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_HWCONTEXT_VAAPI_H
+#define AVUTIL_HWCONTEXT_VAAPI_H
+
+#include "va/va.h"
+
+/**
+ * @file
+ * API-specific header for AV_HWDEVICE_TYPE_VAAPI.
+ *
+ * Dynamic frame pools are supported, but note that any pool used as a render
+ * target is required to be of fixed size in order to be be usable as an
+ * argument to vaCreateContext().
+ *
+ * For user-allocated pools, AVHWFramesContext.pool must return AVBufferRefs
+ * with the data pointer set to a VASurfaceID.
+ */
+
+enum {
+ /**
+ * The quirks field has been set by the user and should not be detected
+ * automatically by av_hwdevice_ctx_init().
+ */
+ AV_VAAPI_DRIVER_QUIRK_USER_SET = (1 << 0),
+ /**
+ * The driver does not destroy parameter buffers when they are used by
+ * vaRenderPicture(). Additional code will be required to destroy them
+ * separately afterwards.
+ */
+ AV_VAAPI_DRIVER_QUIRK_RENDER_PARAM_BUFFERS = (1 << 1),
+
+ /**
+ * The driver does not support the VASurfaceAttribMemoryType attribute,
+ * so the surface allocation code will not try to use it.
+ */
+ AV_VAAPI_DRIVER_QUIRK_ATTRIB_MEMTYPE = (1 << 2),
+
+ /**
+ * The driver does not support surface attributes at all.
+ * The surface allocation code will never pass them to surface allocation,
+ * and the results of the vaQuerySurfaceAttributes() call will be faked.
+ */
+ AV_VAAPI_DRIVER_QUIRK_SURFACE_ATTRIBUTES = (1 << 3),
+};
+
+/**
+ * VAAPI connection details.
+ *
+ * Allocated as AVHWDeviceContext.hwctx
+ */
+typedef struct AVVAAPIDeviceContext {
+ /**
+ * The VADisplay handle, to be filled by the user.
+ */
+ VADisplay display;
+ /**
+ * Driver quirks to apply - this is filled by av_hwdevice_ctx_init(),
+ * with reference to a table of known drivers, unless the
+ * AV_VAAPI_DRIVER_QUIRK_USER_SET bit is already present. The user
+ * may need to refer to this field when performing any later
+ * operations using VAAPI with the same VADisplay.
+ */
+ unsigned int driver_quirks;
+} AVVAAPIDeviceContext;
+
+/**
+ * VAAPI-specific data associated with a frame pool.
+ *
+ * Allocated as AVHWFramesContext.hwctx.
+ */
+typedef struct AVVAAPIFramesContext {
+ /**
+ * Set by the user to apply surface attributes to all surfaces in
+ * the frame pool. If null, default settings are used.
+ */
+ VASurfaceAttrib *attributes;
+ int nb_attributes;
+ /**
+ * The surfaces IDs of all surfaces in the pool after creation.
+ * Only valid if AVHWFramesContext.initial_pool_size was positive.
+ * These are intended to be used as the render_targets arguments to
+ * vaCreateContext().
+ */
+ VASurfaceID *surface_ids;
+ int nb_surfaces;
+} AVVAAPIFramesContext;
+
+/**
+ * VAAPI hardware pipeline configuration details.
+ *
+ * Allocated with av_hwdevice_hwconfig_alloc().
+ */
+typedef struct AVVAAPIHWConfig {
+ /**
+ * ID of a VAAPI pipeline configuration.
+ */
+ VAConfigID config_id;
+} AVVAAPIHWConfig;
+
+#endif /* AVUTIL_HWCONTEXT_VAAPI_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/intfloat.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/intfloat.h
new file mode 100644
index 0000000000..fe3d7ec4a5
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/intfloat.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2011 Mans Rullgard
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_INTFLOAT_H
+#define AVUTIL_INTFLOAT_H
+
+#include <stdint.h>
+#include "attributes.h"
+
+union av_intfloat32 {
+ uint32_t i;
+ float f;
+};
+
+union av_intfloat64 {
+ uint64_t i;
+ double f;
+};
+
+/**
+ * Reinterpret a 32-bit integer as a float.
+ */
+static av_always_inline float av_int2float(uint32_t i)
+{
+ union av_intfloat32 v;
+ v.i = i;
+ return v.f;
+}
+
+/**
+ * Reinterpret a float as a 32-bit integer.
+ */
+static av_always_inline uint32_t av_float2int(float f)
+{
+ union av_intfloat32 v;
+ v.f = f;
+ return v.i;
+}
+
+/**
+ * Reinterpret a 64-bit integer as a double.
+ */
+static av_always_inline double av_int2double(uint64_t i)
+{
+ union av_intfloat64 v;
+ v.i = i;
+ return v.f;
+}
+
+/**
+ * Reinterpret a double as a 64-bit integer.
+ */
+static av_always_inline uint64_t av_double2int(double f)
+{
+ union av_intfloat64 v;
+ v.f = f;
+ return v.i;
+}
+
+#endif /* AVUTIL_INTFLOAT_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/log.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/log.h
new file mode 100644
index 0000000000..d9554e609d
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/log.h
@@ -0,0 +1,362 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_LOG_H
+#define AVUTIL_LOG_H
+
+#include <stdarg.h>
+#include "avutil.h"
+#include "attributes.h"
+#include "version.h"
+
+typedef enum {
+ AV_CLASS_CATEGORY_NA = 0,
+ AV_CLASS_CATEGORY_INPUT,
+ AV_CLASS_CATEGORY_OUTPUT,
+ AV_CLASS_CATEGORY_MUXER,
+ AV_CLASS_CATEGORY_DEMUXER,
+ AV_CLASS_CATEGORY_ENCODER,
+ AV_CLASS_CATEGORY_DECODER,
+ AV_CLASS_CATEGORY_FILTER,
+ AV_CLASS_CATEGORY_BITSTREAM_FILTER,
+ AV_CLASS_CATEGORY_SWSCALER,
+ AV_CLASS_CATEGORY_SWRESAMPLER,
+ AV_CLASS_CATEGORY_DEVICE_VIDEO_OUTPUT = 40,
+ AV_CLASS_CATEGORY_DEVICE_VIDEO_INPUT,
+ AV_CLASS_CATEGORY_DEVICE_AUDIO_OUTPUT,
+ AV_CLASS_CATEGORY_DEVICE_AUDIO_INPUT,
+ AV_CLASS_CATEGORY_DEVICE_OUTPUT,
+ AV_CLASS_CATEGORY_DEVICE_INPUT,
+ AV_CLASS_CATEGORY_NB ///< not part of ABI/API
+}AVClassCategory;
+
+#define AV_IS_INPUT_DEVICE(category) \
+ (((category) == AV_CLASS_CATEGORY_DEVICE_VIDEO_INPUT) || \
+ ((category) == AV_CLASS_CATEGORY_DEVICE_AUDIO_INPUT) || \
+ ((category) == AV_CLASS_CATEGORY_DEVICE_INPUT))
+
+#define AV_IS_OUTPUT_DEVICE(category) \
+ (((category) == AV_CLASS_CATEGORY_DEVICE_VIDEO_OUTPUT) || \
+ ((category) == AV_CLASS_CATEGORY_DEVICE_AUDIO_OUTPUT) || \
+ ((category) == AV_CLASS_CATEGORY_DEVICE_OUTPUT))
+
+struct AVOptionRanges;
+
+/**
+ * Describe the class of an AVClass context structure. That is an
+ * arbitrary struct of which the first field is a pointer to an
+ * AVClass struct (e.g. AVCodecContext, AVFormatContext etc.).
+ */
+typedef struct AVClass {
+ /**
+ * The name of the class; usually it is the same name as the
+ * context structure type to which the AVClass is associated.
+ */
+ const char* class_name;
+
+ /**
+ * A pointer to a function which returns the name of a context
+ * instance ctx associated with the class.
+ */
+ const char* (*item_name)(void* ctx);
+
+ /**
+ * a pointer to the first option specified in the class if any or NULL
+ *
+ * @see av_set_default_options()
+ */
+ const struct AVOption *option;
+
+ /**
+ * LIBAVUTIL_VERSION with which this structure was created.
+ * This is used to allow fields to be added without requiring major
+ * version bumps everywhere.
+ */
+
+ int version;
+
+ /**
+ * Offset in the structure where log_level_offset is stored.
+ * 0 means there is no such variable
+ */
+ int log_level_offset_offset;
+
+ /**
+ * Offset in the structure where a pointer to the parent context for
+ * logging is stored. For example a decoder could pass its AVCodecContext
+ * to eval as such a parent context, which an av_log() implementation
+ * could then leverage to display the parent context.
+ * The offset can be NULL.
+ */
+ int parent_log_context_offset;
+
+ /**
+ * Return next AVOptions-enabled child or NULL
+ */
+ void* (*child_next)(void *obj, void *prev);
+
+ /**
+ * Return an AVClass corresponding to the next potential
+ * AVOptions-enabled child.
+ *
+ * The difference between child_next and this is that
+ * child_next iterates over _already existing_ objects, while
+ * child_class_next iterates over _all possible_ children.
+ */
+ const struct AVClass* (*child_class_next)(const struct AVClass *prev);
+
+ /**
+ * Category used for visualization (like color)
+ * This is only set if the category is equal for all objects using this class.
+ * available since version (51 << 16 | 56 << 8 | 100)
+ */
+ AVClassCategory category;
+
+ /**
+ * Callback to return the category.
+ * available since version (51 << 16 | 59 << 8 | 100)
+ */
+ AVClassCategory (*get_category)(void* ctx);
+
+ /**
+ * Callback to return the supported/allowed ranges.
+ * available since version (52.12)
+ */
+ int (*query_ranges)(struct AVOptionRanges **, void *obj, const char *key, int flags);
+} AVClass;
+
+/**
+ * @addtogroup lavu_log
+ *
+ * @{
+ *
+ * @defgroup lavu_log_constants Logging Constants
+ *
+ * @{
+ */
+
+/**
+ * Print no output.
+ */
+#define AV_LOG_QUIET -8
+
+/**
+ * Something went really wrong and we will crash now.
+ */
+#define AV_LOG_PANIC 0
+
+/**
+ * Something went wrong and recovery is not possible.
+ * For example, no header was found for a format which depends
+ * on headers or an illegal combination of parameters is used.
+ */
+#define AV_LOG_FATAL 8
+
+/**
+ * Something went wrong and cannot losslessly be recovered.
+ * However, not all future data is affected.
+ */
+#define AV_LOG_ERROR 16
+
+/**
+ * Something somehow does not look correct. This may or may not
+ * lead to problems. An example would be the use of '-vstrict -2'.
+ */
+#define AV_LOG_WARNING 24
+
+/**
+ * Standard information.
+ */
+#define AV_LOG_INFO 32
+
+/**
+ * Detailed information.
+ */
+#define AV_LOG_VERBOSE 40
+
+/**
+ * Stuff which is only useful for libav* developers.
+ */
+#define AV_LOG_DEBUG 48
+
+/**
+ * Extremely verbose debugging, useful for libav* development.
+ */
+#define AV_LOG_TRACE 56
+
+#define AV_LOG_MAX_OFFSET (AV_LOG_TRACE - AV_LOG_QUIET)
+
+/**
+ * @}
+ */
+
+/**
+ * Sets additional colors for extended debugging sessions.
+ * @code
+ av_log(ctx, AV_LOG_DEBUG|AV_LOG_C(134), "Message in purple\n");
+ @endcode
+ * Requires 256color terminal support. Uses outside debugging is not
+ * recommended.
+ */
+#define AV_LOG_C(x) ((x) << 8)
+
+/**
+ * Send the specified message to the log if the level is less than or equal
+ * to the current av_log_level. By default, all logging messages are sent to
+ * stderr. This behavior can be altered by setting a different logging callback
+ * function.
+ * @see av_log_set_callback
+ *
+ * @param avcl A pointer to an arbitrary struct of which the first field is a
+ * pointer to an AVClass struct or NULL if general log.
+ * @param level The importance level of the message expressed using a @ref
+ * lavu_log_constants "Logging Constant".
+ * @param fmt The format string (printf-compatible) that specifies how
+ * subsequent arguments are converted to output.
+ */
+void av_log(void *avcl, int level, const char *fmt, ...) av_printf_format(3, 4);
+
+
+/**
+ * Send the specified message to the log if the level is less than or equal
+ * to the current av_log_level. By default, all logging messages are sent to
+ * stderr. This behavior can be altered by setting a different logging callback
+ * function.
+ * @see av_log_set_callback
+ *
+ * @param avcl A pointer to an arbitrary struct of which the first field is a
+ * pointer to an AVClass struct.
+ * @param level The importance level of the message expressed using a @ref
+ * lavu_log_constants "Logging Constant".
+ * @param fmt The format string (printf-compatible) that specifies how
+ * subsequent arguments are converted to output.
+ * @param vl The arguments referenced by the format string.
+ */
+void av_vlog(void *avcl, int level, const char *fmt, va_list vl);
+
+/**
+ * Get the current log level
+ *
+ * @see lavu_log_constants
+ *
+ * @return Current log level
+ */
+int av_log_get_level(void);
+
+/**
+ * Set the log level
+ *
+ * @see lavu_log_constants
+ *
+ * @param level Logging level
+ */
+void av_log_set_level(int level);
+
+/**
+ * Set the logging callback
+ *
+ * @note The callback must be thread safe, even if the application does not use
+ * threads itself as some codecs are multithreaded.
+ *
+ * @see av_log_default_callback
+ *
+ * @param callback A logging function with a compatible signature.
+ */
+void av_log_set_callback(void (*callback)(void*, int, const char*, va_list));
+
+/**
+ * Default logging callback
+ *
+ * It prints the message to stderr, optionally colorizing it.
+ *
+ * @param avcl A pointer to an arbitrary struct of which the first field is a
+ * pointer to an AVClass struct.
+ * @param level The importance level of the message expressed using a @ref
+ * lavu_log_constants "Logging Constant".
+ * @param fmt The format string (printf-compatible) that specifies how
+ * subsequent arguments are converted to output.
+ * @param vl The arguments referenced by the format string.
+ */
+void av_log_default_callback(void *avcl, int level, const char *fmt,
+ va_list vl);
+
+/**
+ * Return the context name
+ *
+ * @param ctx The AVClass context
+ *
+ * @return The AVClass class_name
+ */
+const char* av_default_item_name(void* ctx);
+AVClassCategory av_default_get_category(void *ptr);
+
+/**
+ * Format a line of log the same way as the default callback.
+ * @param line buffer to receive the formatted line
+ * @param line_size size of the buffer
+ * @param print_prefix used to store whether the prefix must be printed;
+ * must point to a persistent integer initially set to 1
+ */
+void av_log_format_line(void *ptr, int level, const char *fmt, va_list vl,
+ char *line, int line_size, int *print_prefix);
+
+/**
+ * Format a line of log the same way as the default callback.
+ * @param line buffer to receive the formatted line;
+ * may be NULL if line_size is 0
+ * @param line_size size of the buffer; at most line_size-1 characters will
+ * be written to the buffer, plus one null terminator
+ * @param print_prefix used to store whether the prefix must be printed;
+ * must point to a persistent integer initially set to 1
+ * @return Returns a negative value if an error occurred, otherwise returns
+ * the number of characters that would have been written for a
+ * sufficiently large buffer, not including the terminating null
+ * character. If the return value is not less than line_size, it means
+ * that the log message was truncated to fit the buffer.
+ */
+int av_log_format_line2(void *ptr, int level, const char *fmt, va_list vl,
+ char *line, int line_size, int *print_prefix);
+
+/**
+ * Skip repeated messages, this requires the user app to use av_log() instead of
+ * (f)printf as the 2 would otherwise interfere and lead to
+ * "Last message repeated x times" messages below (f)printf messages with some
+ * bad luck.
+ * Also to receive the last, "last repeated" line if any, the user app must
+ * call av_log(NULL, AV_LOG_QUIET, "%s", ""); at the end
+ */
+#define AV_LOG_SKIP_REPEATED 1
+
+/**
+ * Include the log severity in messages originating from codecs.
+ *
+ * Results in messages such as:
+ * [rawvideo @ 0xDEADBEEF] [error] encode did not produce valid pts
+ */
+#define AV_LOG_PRINT_LEVEL 2
+
+void av_log_set_flags(int arg);
+int av_log_get_flags(void);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_LOG_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/macros.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/macros.h
new file mode 100644
index 0000000000..2007ee5619
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/macros.h
@@ -0,0 +1,50 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * @ingroup lavu
+ * Utility Preprocessor macros
+ */
+
+#ifndef AVUTIL_MACROS_H
+#define AVUTIL_MACROS_H
+
+/**
+ * @addtogroup preproc_misc Preprocessor String Macros
+ *
+ * String manipulation macros
+ *
+ * @{
+ */
+
+#define AV_STRINGIFY(s) AV_TOSTRING(s)
+#define AV_TOSTRING(s) #s
+
+#define AV_GLUE(a, b) a ## b
+#define AV_JOIN(a, b) AV_GLUE(a, b)
+
+/**
+ * @}
+ */
+
+#define AV_PRAGMA(s) _Pragma(#s)
+
+#define FFALIGN(x, a) (((x)+(a)-1)&~((a)-1))
+
+#endif /* AVUTIL_MACROS_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/mathematics.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/mathematics.h
new file mode 100644
index 0000000000..54901800ba
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/mathematics.h
@@ -0,0 +1,242 @@
+/*
+ * copyright (c) 2005-2012 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * @addtogroup lavu_math
+ * Mathematical utilities for working with timestamp and time base.
+ */
+
+#ifndef AVUTIL_MATHEMATICS_H
+#define AVUTIL_MATHEMATICS_H
+
+#include <stdint.h>
+#include <math.h>
+#include "attributes.h"
+#include "rational.h"
+#include "intfloat.h"
+
+#ifndef M_E
+#define M_E 2.7182818284590452354 /* e */
+#endif
+#ifndef M_LN2
+#define M_LN2 0.69314718055994530942 /* log_e 2 */
+#endif
+#ifndef M_LN10
+#define M_LN10 2.30258509299404568402 /* log_e 10 */
+#endif
+#ifndef M_LOG2_10
+#define M_LOG2_10 3.32192809488736234787 /* log_2 10 */
+#endif
+#ifndef M_PHI
+#define M_PHI 1.61803398874989484820 /* phi / golden ratio */
+#endif
+#ifndef M_PI
+#define M_PI 3.14159265358979323846 /* pi */
+#endif
+#ifndef M_PI_2
+#define M_PI_2 1.57079632679489661923 /* pi/2 */
+#endif
+#ifndef M_SQRT1_2
+#define M_SQRT1_2 0.70710678118654752440 /* 1/sqrt(2) */
+#endif
+#ifndef M_SQRT2
+#define M_SQRT2 1.41421356237309504880 /* sqrt(2) */
+#endif
+#ifndef NAN
+#define NAN av_int2float(0x7fc00000)
+#endif
+#ifndef INFINITY
+#define INFINITY av_int2float(0x7f800000)
+#endif
+
+/**
+ * @addtogroup lavu_math
+ *
+ * @{
+ */
+
+/**
+ * Rounding methods.
+ */
+enum AVRounding {
+ AV_ROUND_ZERO = 0, ///< Round toward zero.
+ AV_ROUND_INF = 1, ///< Round away from zero.
+ AV_ROUND_DOWN = 2, ///< Round toward -infinity.
+ AV_ROUND_UP = 3, ///< Round toward +infinity.
+ AV_ROUND_NEAR_INF = 5, ///< Round to nearest and halfway cases away from zero.
+ /**
+ * Flag telling rescaling functions to pass `INT64_MIN`/`MAX` through
+ * unchanged, avoiding special cases for #AV_NOPTS_VALUE.
+ *
+ * Unlike other values of the enumeration AVRounding, this value is a
+ * bitmask that must be used in conjunction with another value of the
+ * enumeration through a bitwise OR, in order to set behavior for normal
+ * cases.
+ *
+ * @code{.c}
+ * av_rescale_rnd(3, 1, 2, AV_ROUND_UP | AV_ROUND_PASS_MINMAX);
+ * // Rescaling 3:
+ * // Calculating 3 * 1 / 2
+ * // 3 / 2 is rounded up to 2
+ * // => 2
+ *
+ * av_rescale_rnd(AV_NOPTS_VALUE, 1, 2, AV_ROUND_UP | AV_ROUND_PASS_MINMAX);
+ * // Rescaling AV_NOPTS_VALUE:
+ * // AV_NOPTS_VALUE == INT64_MIN
+ * // AV_NOPTS_VALUE is passed through
+ * // => AV_NOPTS_VALUE
+ * @endcode
+ */
+ AV_ROUND_PASS_MINMAX = 8192,
+};
+
+/**
+ * Compute the greatest common divisor of two integer operands.
+ *
+ * @param a,b Operands
+ * @return GCD of a and b up to sign; if a >= 0 and b >= 0, return value is >= 0;
+ * if a == 0 and b == 0, returns 0.
+ */
+int64_t av_const av_gcd(int64_t a, int64_t b);
+
+/**
+ * Rescale a 64-bit integer with rounding to nearest.
+ *
+ * The operation is mathematically equivalent to `a * b / c`, but writing that
+ * directly can overflow.
+ *
+ * This function is equivalent to av_rescale_rnd() with #AV_ROUND_NEAR_INF.
+ *
+ * @see av_rescale_rnd(), av_rescale_q(), av_rescale_q_rnd()
+ */
+int64_t av_rescale(int64_t a, int64_t b, int64_t c) av_const;
+
+/**
+ * Rescale a 64-bit integer with specified rounding.
+ *
+ * The operation is mathematically equivalent to `a * b / c`, but writing that
+ * directly can overflow, and does not support different rounding methods.
+ *
+ * @see av_rescale(), av_rescale_q(), av_rescale_q_rnd()
+ */
+int64_t av_rescale_rnd(int64_t a, int64_t b, int64_t c, enum AVRounding rnd) av_const;
+
+/**
+ * Rescale a 64-bit integer by 2 rational numbers.
+ *
+ * The operation is mathematically equivalent to `a * bq / cq`.
+ *
+ * This function is equivalent to av_rescale_q_rnd() with #AV_ROUND_NEAR_INF.
+ *
+ * @see av_rescale(), av_rescale_rnd(), av_rescale_q_rnd()
+ */
+int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq) av_const;
+
+/**
+ * Rescale a 64-bit integer by 2 rational numbers with specified rounding.
+ *
+ * The operation is mathematically equivalent to `a * bq / cq`.
+ *
+ * @see av_rescale(), av_rescale_rnd(), av_rescale_q()
+ */
+int64_t av_rescale_q_rnd(int64_t a, AVRational bq, AVRational cq,
+ enum AVRounding rnd) av_const;
+
+/**
+ * Compare two timestamps each in its own time base.
+ *
+ * @return One of the following values:
+ * - -1 if `ts_a` is before `ts_b`
+ * - 1 if `ts_a` is after `ts_b`
+ * - 0 if they represent the same position
+ *
+ * @warning
+ * The result of the function is undefined if one of the timestamps is outside
+ * the `int64_t` range when represented in the other's timebase.
+ */
+int av_compare_ts(int64_t ts_a, AVRational tb_a, int64_t ts_b, AVRational tb_b);
+
+/**
+ * Compare the remainders of two integer operands divided by a common divisor.
+ *
+ * In other words, compare the least significant `log2(mod)` bits of integers
+ * `a` and `b`.
+ *
+ * @code{.c}
+ * av_compare_mod(0x11, 0x02, 0x10) < 0 // since 0x11 % 0x10 (0x1) < 0x02 % 0x10 (0x2)
+ * av_compare_mod(0x11, 0x02, 0x20) > 0 // since 0x11 % 0x20 (0x11) > 0x02 % 0x20 (0x02)
+ * @endcode
+ *
+ * @param a,b Operands
+ * @param mod Divisor; must be a power of 2
+ * @return
+ * - a negative value if `a % mod < b % mod`
+ * - a positive value if `a % mod > b % mod`
+ * - zero if `a % mod == b % mod`
+ */
+int64_t av_compare_mod(uint64_t a, uint64_t b, uint64_t mod);
+
+/**
+ * Rescale a timestamp while preserving known durations.
+ *
+ * This function is designed to be called per audio packet to scale the input
+ * timestamp to a different time base. Compared to a simple av_rescale_q()
+ * call, this function is robust against possible inconsistent frame durations.
+ *
+ * The `last` parameter is a state variable that must be preserved for all
+ * subsequent calls for the same stream. For the first call, `*last` should be
+ * initialized to #AV_NOPTS_VALUE.
+ *
+ * @param[in] in_tb Input time base
+ * @param[in] in_ts Input timestamp
+ * @param[in] fs_tb Duration time base; typically this is finer-grained
+ * (greater) than `in_tb` and `out_tb`
+ * @param[in] duration Duration till the next call to this function (i.e.
+ * duration of the current packet/frame)
+ * @param[in,out] last Pointer to a timestamp expressed in terms of
+ * `fs_tb`, acting as a state variable
+ * @param[in] out_tb Output timebase
+ * @return Timestamp expressed in terms of `out_tb`
+ *
+ * @note In the context of this function, "duration" is in term of samples, not
+ * seconds.
+ */
+int64_t av_rescale_delta(AVRational in_tb, int64_t in_ts, AVRational fs_tb, int duration, int64_t *last, AVRational out_tb);
+
+/**
+ * Add a value to a timestamp.
+ *
+ * This function guarantees that when the same value is repeatly added that
+ * no accumulation of rounding errors occurs.
+ *
+ * @param[in] ts Input timestamp
+ * @param[in] ts_tb Input timestamp time base
+ * @param[in] inc Value to be added
+ * @param[in] inc_tb Time base of `inc`
+ */
+int64_t av_add_stable(AVRational ts_tb, int64_t ts, AVRational inc_tb, int64_t inc);
+
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_MATHEMATICS_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/mem.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/mem.h
new file mode 100644
index 0000000000..7e0b12a8a7
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/mem.h
@@ -0,0 +1,700 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * @ingroup lavu_mem
+ * Memory handling functions
+ */
+
+#ifndef AVUTIL_MEM_H
+#define AVUTIL_MEM_H
+
+#include <limits.h>
+#include <stdint.h>
+
+#include "attributes.h"
+#include "error.h"
+#include "avutil.h"
+
+/**
+ * @addtogroup lavu_mem
+ * Utilities for manipulating memory.
+ *
+ * FFmpeg has several applications of memory that are not required of a typical
+ * program. For example, the computing-heavy components like video decoding and
+ * encoding can be sped up significantly through the use of aligned memory.
+ *
+ * However, for each of FFmpeg's applications of memory, there might not be a
+ * recognized or standardized API for that specific use. Memory alignment, for
+ * instance, varies wildly depending on operating systems, architectures, and
+ * compilers. Hence, this component of @ref libavutil is created to make
+ * dealing with memory consistently possible on all platforms.
+ *
+ * @{
+ *
+ * @defgroup lavu_mem_macros Alignment Macros
+ * Helper macros for declaring aligned variables.
+ * @{
+ */
+
+/**
+ * @def DECLARE_ALIGNED(n,t,v)
+ * Declare a variable that is aligned in memory.
+ *
+ * @code{.c}
+ * DECLARE_ALIGNED(16, uint16_t, aligned_int) = 42;
+ * DECLARE_ALIGNED(32, uint8_t, aligned_array)[128];
+ *
+ * // The default-alignment equivalent would be
+ * uint16_t aligned_int = 42;
+ * uint8_t aligned_array[128];
+ * @endcode
+ *
+ * @param n Minimum alignment in bytes
+ * @param t Type of the variable (or array element)
+ * @param v Name of the variable
+ */
+
+/**
+ * @def DECLARE_ASM_ALIGNED(n,t,v)
+ * Declare an aligned variable appropriate for use in inline assembly code.
+ *
+ * @code{.c}
+ * DECLARE_ASM_ALIGNED(16, uint64_t, pw_08) = UINT64_C(0x0008000800080008);
+ * @endcode
+ *
+ * @param n Minimum alignment in bytes
+ * @param t Type of the variable (or array element)
+ * @param v Name of the variable
+ */
+
+/**
+ * @def DECLARE_ASM_CONST(n,t,v)
+ * Declare a static constant aligned variable appropriate for use in inline
+ * assembly code.
+ *
+ * @code{.c}
+ * DECLARE_ASM_CONST(16, uint64_t, pw_08) = UINT64_C(0x0008000800080008);
+ * @endcode
+ *
+ * @param n Minimum alignment in bytes
+ * @param t Type of the variable (or array element)
+ * @param v Name of the variable
+ */
+
+#if defined(__INTEL_COMPILER) && __INTEL_COMPILER < 1110 || defined(__SUNPRO_C)
+ #define DECLARE_ALIGNED(n,t,v) t __attribute__ ((aligned (n))) v
+ #define DECLARE_ASM_ALIGNED(n,t,v) t __attribute__ ((aligned (n))) v
+ #define DECLARE_ASM_CONST(n,t,v) const t __attribute__ ((aligned (n))) v
+#elif defined(__DJGPP__)
+ #define DECLARE_ALIGNED(n,t,v) t __attribute__ ((aligned (FFMIN(n, 16)))) v
+ #define DECLARE_ASM_ALIGNED(n,t,v) t av_used __attribute__ ((aligned (FFMIN(n, 16)))) v
+ #define DECLARE_ASM_CONST(n,t,v) static const t av_used __attribute__ ((aligned (FFMIN(n, 16)))) v
+#elif defined(__GNUC__) || defined(__clang__)
+ #define DECLARE_ALIGNED(n,t,v) t __attribute__ ((aligned (n))) v
+ #define DECLARE_ASM_ALIGNED(n,t,v) t av_used __attribute__ ((aligned (n))) v
+ #define DECLARE_ASM_CONST(n,t,v) static const t av_used __attribute__ ((aligned (n))) v
+#elif defined(_MSC_VER)
+ #define DECLARE_ALIGNED(n,t,v) __declspec(align(n)) t v
+ #define DECLARE_ASM_ALIGNED(n,t,v) __declspec(align(n)) t v
+ #define DECLARE_ASM_CONST(n,t,v) __declspec(align(n)) static const t v
+#else
+ #define DECLARE_ALIGNED(n,t,v) t v
+ #define DECLARE_ASM_ALIGNED(n,t,v) t v
+ #define DECLARE_ASM_CONST(n,t,v) static const t v
+#endif
+
+/**
+ * @}
+ */
+
+/**
+ * @defgroup lavu_mem_attrs Function Attributes
+ * Function attributes applicable to memory handling functions.
+ *
+ * These function attributes can help compilers emit more useful warnings, or
+ * generate better code.
+ * @{
+ */
+
+/**
+ * @def av_malloc_attrib
+ * Function attribute denoting a malloc-like function.
+ *
+ * @see <a href="https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#index-g_t_0040code_007bmalloc_007d-function-attribute-3251">Function attribute `malloc` in GCC's documentation</a>
+ */
+
+#if AV_GCC_VERSION_AT_LEAST(3,1)
+ #define av_malloc_attrib __attribute__((__malloc__))
+#else
+ #define av_malloc_attrib
+#endif
+
+/**
+ * @def av_alloc_size(...)
+ * Function attribute used on a function that allocates memory, whose size is
+ * given by the specified parameter(s).
+ *
+ * @code{.c}
+ * void *av_malloc(size_t size) av_alloc_size(1);
+ * void *av_calloc(size_t nmemb, size_t size) av_alloc_size(1, 2);
+ * @endcode
+ *
+ * @param ... One or two parameter indexes, separated by a comma
+ *
+ * @see <a href="https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#index-g_t_0040code_007balloc_005fsize_007d-function-attribute-3220">Function attribute `alloc_size` in GCC's documentation</a>
+ */
+
+#if AV_GCC_VERSION_AT_LEAST(4,3)
+ #define av_alloc_size(...) __attribute__((alloc_size(__VA_ARGS__)))
+#else
+ #define av_alloc_size(...)
+#endif
+
+/**
+ * @}
+ */
+
+/**
+ * @defgroup lavu_mem_funcs Heap Management
+ * Functions responsible for allocating, freeing, and copying memory.
+ *
+ * All memory allocation functions have a built-in upper limit of `INT_MAX`
+ * bytes. This may be changed with av_max_alloc(), although exercise extreme
+ * caution when doing so.
+ *
+ * @{
+ */
+
+/**
+ * Allocate a memory block with alignment suitable for all memory accesses
+ * (including vectors if available on the CPU).
+ *
+ * @param size Size in bytes for the memory block to be allocated
+ * @return Pointer to the allocated block, or `NULL` if the block cannot
+ * be allocated
+ * @see av_mallocz()
+ */
+void *av_malloc(size_t size) av_malloc_attrib av_alloc_size(1);
+
+/**
+ * Allocate a memory block with alignment suitable for all memory accesses
+ * (including vectors if available on the CPU) and zero all the bytes of the
+ * block.
+ *
+ * @param size Size in bytes for the memory block to be allocated
+ * @return Pointer to the allocated block, or `NULL` if it cannot be allocated
+ * @see av_malloc()
+ */
+void *av_mallocz(size_t size) av_malloc_attrib av_alloc_size(1);
+
+/**
+ * Allocate a memory block for an array with av_malloc().
+ *
+ * The allocated memory will have size `size * nmemb` bytes.
+ *
+ * @param nmemb Number of element
+ * @param size Size of a single element
+ * @return Pointer to the allocated block, or `NULL` if the block cannot
+ * be allocated
+ * @see av_malloc()
+ */
+av_alloc_size(1, 2) void *av_malloc_array(size_t nmemb, size_t size);
+
+/**
+ * Allocate a memory block for an array with av_mallocz().
+ *
+ * The allocated memory will have size `size * nmemb` bytes.
+ *
+ * @param nmemb Number of elements
+ * @param size Size of the single element
+ * @return Pointer to the allocated block, or `NULL` if the block cannot
+ * be allocated
+ *
+ * @see av_mallocz()
+ * @see av_malloc_array()
+ */
+av_alloc_size(1, 2) void *av_mallocz_array(size_t nmemb, size_t size);
+
+/**
+ * Non-inlined equivalent of av_mallocz_array().
+ *
+ * Created for symmetry with the calloc() C function.
+ */
+void *av_calloc(size_t nmemb, size_t size) av_malloc_attrib;
+
+/**
+ * Allocate, reallocate, or free a block of memory.
+ *
+ * If `ptr` is `NULL` and `size` > 0, allocate a new block. If `size` is
+ * zero, free the memory block pointed to by `ptr`. Otherwise, expand or
+ * shrink that block of memory according to `size`.
+ *
+ * @param ptr Pointer to a memory block already allocated with
+ * av_realloc() or `NULL`
+ * @param size Size in bytes of the memory block to be allocated or
+ * reallocated
+ *
+ * @return Pointer to a newly-reallocated block or `NULL` if the block
+ * cannot be reallocated or the function is used to free the memory block
+ *
+ * @warning Unlike av_malloc(), the returned pointer is not guaranteed to be
+ * correctly aligned.
+ * @see av_fast_realloc()
+ * @see av_reallocp()
+ */
+void *av_realloc(void *ptr, size_t size) av_alloc_size(2);
+
+/**
+ * Allocate, reallocate, or free a block of memory through a pointer to a
+ * pointer.
+ *
+ * If `*ptr` is `NULL` and `size` > 0, allocate a new block. If `size` is
+ * zero, free the memory block pointed to by `*ptr`. Otherwise, expand or
+ * shrink that block of memory according to `size`.
+ *
+ * @param[in,out] ptr Pointer to a pointer to a memory block already allocated
+ * with av_realloc(), or a pointer to `NULL`. The pointer
+ * is updated on success, or freed on failure.
+ * @param[in] size Size in bytes for the memory block to be allocated or
+ * reallocated
+ *
+ * @return Zero on success, an AVERROR error code on failure
+ *
+ * @warning Unlike av_malloc(), the allocated memory is not guaranteed to be
+ * correctly aligned.
+ */
+av_warn_unused_result
+int av_reallocp(void *ptr, size_t size);
+
+/**
+ * Allocate, reallocate, or free a block of memory.
+ *
+ * This function does the same thing as av_realloc(), except:
+ * - It takes two size arguments and allocates `nelem * elsize` bytes,
+ * after checking the result of the multiplication for integer overflow.
+ * - It frees the input block in case of failure, thus avoiding the memory
+ * leak with the classic
+ * @code{.c}
+ * buf = realloc(buf);
+ * if (!buf)
+ * return -1;
+ * @endcode
+ * pattern.
+ */
+void *av_realloc_f(void *ptr, size_t nelem, size_t elsize);
+
+/**
+ * Allocate, reallocate, or free an array.
+ *
+ * If `ptr` is `NULL` and `nmemb` > 0, allocate a new block. If
+ * `nmemb` is zero, free the memory block pointed to by `ptr`.
+ *
+ * @param ptr Pointer to a memory block already allocated with
+ * av_realloc() or `NULL`
+ * @param nmemb Number of elements in the array
+ * @param size Size of the single element of the array
+ *
+ * @return Pointer to a newly-reallocated block or NULL if the block
+ * cannot be reallocated or the function is used to free the memory block
+ *
+ * @warning Unlike av_malloc(), the allocated memory is not guaranteed to be
+ * correctly aligned.
+ * @see av_reallocp_array()
+ */
+av_alloc_size(2, 3) void *av_realloc_array(void *ptr, size_t nmemb, size_t size);
+
+/**
+ * Allocate, reallocate, or free an array through a pointer to a pointer.
+ *
+ * If `*ptr` is `NULL` and `nmemb` > 0, allocate a new block. If `nmemb` is
+ * zero, free the memory block pointed to by `*ptr`.
+ *
+ * @param[in,out] ptr Pointer to a pointer to a memory block already
+ * allocated with av_realloc(), or a pointer to `NULL`.
+ * The pointer is updated on success, or freed on failure.
+ * @param[in] nmemb Number of elements
+ * @param[in] size Size of the single element
+ *
+ * @return Zero on success, an AVERROR error code on failure
+ *
+ * @warning Unlike av_malloc(), the allocated memory is not guaranteed to be
+ * correctly aligned.
+ */
+av_alloc_size(2, 3) int av_reallocp_array(void *ptr, size_t nmemb, size_t size);
+
+/**
+ * Reallocate the given buffer if it is not large enough, otherwise do nothing.
+ *
+ * If the given buffer is `NULL`, then a new uninitialized buffer is allocated.
+ *
+ * If the given buffer is not large enough, and reallocation fails, `NULL` is
+ * returned and `*size` is set to 0, but the original buffer is not changed or
+ * freed.
+ *
+ * A typical use pattern follows:
+ *
+ * @code{.c}
+ * uint8_t *buf = ...;
+ * uint8_t *new_buf = av_fast_realloc(buf, &current_size, size_needed);
+ * if (!new_buf) {
+ * // Allocation failed; clean up original buffer
+ * av_freep(&buf);
+ * return AVERROR(ENOMEM);
+ * }
+ * @endcode
+ *
+ * @param[in,out] ptr Already allocated buffer, or `NULL`
+ * @param[in,out] size Pointer to current size of buffer `ptr`. `*size` is
+ * changed to `min_size` in case of success or 0 in
+ * case of failure
+ * @param[in] min_size New size of buffer `ptr`
+ * @return `ptr` if the buffer is large enough, a pointer to newly reallocated
+ * buffer if the buffer was not large enough, or `NULL` in case of
+ * error
+ * @see av_realloc()
+ * @see av_fast_malloc()
+ */
+void *av_fast_realloc(void *ptr, unsigned int *size, size_t min_size);
+
+/**
+ * Allocate a buffer, reusing the given one if large enough.
+ *
+ * Contrary to av_fast_realloc(), the current buffer contents might not be
+ * preserved and on error the old buffer is freed, thus no special handling to
+ * avoid memleaks is necessary.
+ *
+ * `*ptr` is allowed to be `NULL`, in which case allocation always happens if
+ * `size_needed` is greater than 0.
+ *
+ * @code{.c}
+ * uint8_t *buf = ...;
+ * av_fast_malloc(&buf, &current_size, size_needed);
+ * if (!buf) {
+ * // Allocation failed; buf already freed
+ * return AVERROR(ENOMEM);
+ * }
+ * @endcode
+ *
+ * @param[in,out] ptr Pointer to pointer to an already allocated buffer.
+ * `*ptr` will be overwritten with pointer to new
+ * buffer on success or `NULL` on failure
+ * @param[in,out] size Pointer to current size of buffer `*ptr`. `*size` is
+ * changed to `min_size` in case of success or 0 in
+ * case of failure
+ * @param[in] min_size New size of buffer `*ptr`
+ * @see av_realloc()
+ * @see av_fast_mallocz()
+ */
+void av_fast_malloc(void *ptr, unsigned int *size, size_t min_size);
+
+/**
+ * Allocate and clear a buffer, reusing the given one if large enough.
+ *
+ * Like av_fast_malloc(), but all newly allocated space is initially cleared.
+ * Reused buffer is not cleared.
+ *
+ * `*ptr` is allowed to be `NULL`, in which case allocation always happens if
+ * `size_needed` is greater than 0.
+ *
+ * @param[in,out] ptr Pointer to pointer to an already allocated buffer.
+ * `*ptr` will be overwritten with pointer to new
+ * buffer on success or `NULL` on failure
+ * @param[in,out] size Pointer to current size of buffer `*ptr`. `*size` is
+ * changed to `min_size` in case of success or 0 in
+ * case of failure
+ * @param[in] min_size New size of buffer `*ptr`
+ * @see av_fast_malloc()
+ */
+void av_fast_mallocz(void *ptr, unsigned int *size, size_t min_size);
+
+/**
+ * Free a memory block which has been allocated with a function of av_malloc()
+ * or av_realloc() family.
+ *
+ * @param ptr Pointer to the memory block which should be freed.
+ *
+ * @note `ptr = NULL` is explicitly allowed.
+ * @note It is recommended that you use av_freep() instead, to prevent leaving
+ * behind dangling pointers.
+ * @see av_freep()
+ */
+void av_free(void *ptr);
+
+/**
+ * Free a memory block which has been allocated with a function of av_malloc()
+ * or av_realloc() family, and set the pointer pointing to it to `NULL`.
+ *
+ * @code{.c}
+ * uint8_t *buf = av_malloc(16);
+ * av_free(buf);
+ * // buf now contains a dangling pointer to freed memory, and accidental
+ * // dereference of buf will result in a use-after-free, which may be a
+ * // security risk.
+ *
+ * uint8_t *buf = av_malloc(16);
+ * av_freep(&buf);
+ * // buf is now NULL, and accidental dereference will only result in a
+ * // NULL-pointer dereference.
+ * @endcode
+ *
+ * @param ptr Pointer to the pointer to the memory block which should be freed
+ * @note `*ptr = NULL` is safe and leads to no action.
+ * @see av_free()
+ */
+void av_freep(void *ptr);
+
+/**
+ * Duplicate a string.
+ *
+ * @param s String to be duplicated
+ * @return Pointer to a newly-allocated string containing a
+ * copy of `s` or `NULL` if the string cannot be allocated
+ * @see av_strndup()
+ */
+char *av_strdup(const char *s) av_malloc_attrib;
+
+/**
+ * Duplicate a substring of a string.
+ *
+ * @param s String to be duplicated
+ * @param len Maximum length of the resulting string (not counting the
+ * terminating byte)
+ * @return Pointer to a newly-allocated string containing a
+ * substring of `s` or `NULL` if the string cannot be allocated
+ */
+char *av_strndup(const char *s, size_t len) av_malloc_attrib;
+
+/**
+ * Duplicate a buffer with av_malloc().
+ *
+ * @param p Buffer to be duplicated
+ * @param size Size in bytes of the buffer copied
+ * @return Pointer to a newly allocated buffer containing a
+ * copy of `p` or `NULL` if the buffer cannot be allocated
+ */
+void *av_memdup(const void *p, size_t size);
+
+/**
+ * Overlapping memcpy() implementation.
+ *
+ * @param dst Destination buffer
+ * @param back Number of bytes back to start copying (i.e. the initial size of
+ * the overlapping window); must be > 0
+ * @param cnt Number of bytes to copy; must be >= 0
+ *
+ * @note `cnt > back` is valid, this will copy the bytes we just copied,
+ * thus creating a repeating pattern with a period length of `back`.
+ */
+void av_memcpy_backptr(uint8_t *dst, int back, int cnt);
+
+/**
+ * @}
+ */
+
+/**
+ * @defgroup lavu_mem_dynarray Dynamic Array
+ *
+ * Utilities to make an array grow when needed.
+ *
+ * Sometimes, the programmer would want to have an array that can grow when
+ * needed. The libavutil dynamic array utilities fill that need.
+ *
+ * libavutil supports two systems of appending elements onto a dynamically
+ * allocated array, the first one storing the pointer to the value in the
+ * array, and the second storing the value directly. In both systems, the
+ * caller is responsible for maintaining a variable containing the length of
+ * the array, as well as freeing of the array after use.
+ *
+ * The first system stores pointers to values in a block of dynamically
+ * allocated memory. Since only pointers are stored, the function does not need
+ * to know the size of the type. Both av_dynarray_add() and
+ * av_dynarray_add_nofree() implement this system.
+ *
+ * @code
+ * type **array = NULL; //< an array of pointers to values
+ * int nb = 0; //< a variable to keep track of the length of the array
+ *
+ * type to_be_added = ...;
+ * type to_be_added2 = ...;
+ *
+ * av_dynarray_add(&array, &nb, &to_be_added);
+ * if (nb == 0)
+ * return AVERROR(ENOMEM);
+ *
+ * av_dynarray_add(&array, &nb, &to_be_added2);
+ * if (nb == 0)
+ * return AVERROR(ENOMEM);
+ *
+ * // Now:
+ * // nb == 2
+ * // &to_be_added == array[0]
+ * // &to_be_added2 == array[1]
+ *
+ * av_freep(&array);
+ * @endcode
+ *
+ * The second system stores the value directly in a block of memory. As a
+ * result, the function has to know the size of the type. av_dynarray2_add()
+ * implements this mechanism.
+ *
+ * @code
+ * type *array = NULL; //< an array of values
+ * int nb = 0; //< a variable to keep track of the length of the array
+ *
+ * type to_be_added = ...;
+ * type to_be_added2 = ...;
+ *
+ * type *addr = av_dynarray2_add((void **)&array, &nb, sizeof(*array), NULL);
+ * if (!addr)
+ * return AVERROR(ENOMEM);
+ * memcpy(addr, &to_be_added, sizeof(to_be_added));
+ *
+ * // Shortcut of the above.
+ * type *addr = av_dynarray2_add((void **)&array, &nb, sizeof(*array),
+ * (const void *)&to_be_added2);
+ * if (!addr)
+ * return AVERROR(ENOMEM);
+ *
+ * // Now:
+ * // nb == 2
+ * // to_be_added == array[0]
+ * // to_be_added2 == array[1]
+ *
+ * av_freep(&array);
+ * @endcode
+ *
+ * @{
+ */
+
+/**
+ * Add the pointer to an element to a dynamic array.
+ *
+ * The array to grow is supposed to be an array of pointers to
+ * structures, and the element to add must be a pointer to an already
+ * allocated structure.
+ *
+ * The array is reallocated when its size reaches powers of 2.
+ * Therefore, the amortized cost of adding an element is constant.
+ *
+ * In case of success, the pointer to the array is updated in order to
+ * point to the new grown array, and the number pointed to by `nb_ptr`
+ * is incremented.
+ * In case of failure, the array is freed, `*tab_ptr` is set to `NULL` and
+ * `*nb_ptr` is set to 0.
+ *
+ * @param[in,out] tab_ptr Pointer to the array to grow
+ * @param[in,out] nb_ptr Pointer to the number of elements in the array
+ * @param[in] elem Element to add
+ * @see av_dynarray_add_nofree(), av_dynarray2_add()
+ */
+void av_dynarray_add(void *tab_ptr, int *nb_ptr, void *elem);
+
+/**
+ * Add an element to a dynamic array.
+ *
+ * Function has the same functionality as av_dynarray_add(),
+ * but it doesn't free memory on fails. It returns error code
+ * instead and leave current buffer untouched.
+ *
+ * @return >=0 on success, negative otherwise
+ * @see av_dynarray_add(), av_dynarray2_add()
+ */
+av_warn_unused_result
+int av_dynarray_add_nofree(void *tab_ptr, int *nb_ptr, void *elem);
+
+/**
+ * Add an element of size `elem_size` to a dynamic array.
+ *
+ * The array is reallocated when its number of elements reaches powers of 2.
+ * Therefore, the amortized cost of adding an element is constant.
+ *
+ * In case of success, the pointer to the array is updated in order to
+ * point to the new grown array, and the number pointed to by `nb_ptr`
+ * is incremented.
+ * In case of failure, the array is freed, `*tab_ptr` is set to `NULL` and
+ * `*nb_ptr` is set to 0.
+ *
+ * @param[in,out] tab_ptr Pointer to the array to grow
+ * @param[in,out] nb_ptr Pointer to the number of elements in the array
+ * @param[in] elem_size Size in bytes of an element in the array
+ * @param[in] elem_data Pointer to the data of the element to add. If
+ * `NULL`, the space of the newly added element is
+ * allocated but left uninitialized.
+ *
+ * @return Pointer to the data of the element to copy in the newly allocated
+ * space
+ * @see av_dynarray_add(), av_dynarray_add_nofree()
+ */
+void *av_dynarray2_add(void **tab_ptr, int *nb_ptr, size_t elem_size,
+ const uint8_t *elem_data);
+
+/**
+ * @}
+ */
+
+/**
+ * @defgroup lavu_mem_misc Miscellaneous Functions
+ *
+ * Other functions related to memory allocation.
+ *
+ * @{
+ */
+
+/**
+ * Multiply two `size_t` values checking for overflow.
+ *
+ * @param[in] a,b Operands of multiplication
+ * @param[out] r Pointer to the result of the operation
+ * @return 0 on success, AVERROR(EINVAL) on overflow
+ */
+static inline int av_size_mult(size_t a, size_t b, size_t *r)
+{
+ size_t t = a * b;
+ /* Hack inspired from glibc: don't try the division if nelem and elsize
+ * are both less than sqrt(SIZE_MAX). */
+ if ((a | b) >= ((size_t)1 << (sizeof(size_t) * 4)) && a && t / a != b)
+ return AVERROR(EINVAL);
+ *r = t;
+ return 0;
+}
+
+/**
+ * Set the maximum size that may be allocated in one block.
+ *
+ * The value specified with this function is effective for all libavutil's @ref
+ * lavu_mem_funcs "heap management functions."
+ *
+ * By default, the max value is defined as `INT_MAX`.
+ *
+ * @param max Value to be set as the new maximum size
+ *
+ * @warning Exercise extreme caution when using this function. Don't touch
+ * this if you do not understand the full consequence of doing so.
+ */
+void av_max_alloc(size_t max);
+
+/**
+ * @}
+ * @}
+ */
+
+#endif /* AVUTIL_MEM_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/pixfmt.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/pixfmt.h
new file mode 100644
index 0000000000..e184a56672
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/pixfmt.h
@@ -0,0 +1,529 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_PIXFMT_H
+#define AVUTIL_PIXFMT_H
+
+/**
+ * @file
+ * pixel format definitions
+ */
+
+#include "libavutil/avconfig.h"
+#include "version.h"
+
+#define AVPALETTE_SIZE 1024
+#define AVPALETTE_COUNT 256
+
+/**
+ * Pixel format.
+ *
+ * @note
+ * AV_PIX_FMT_RGB32 is handled in an endian-specific manner. An RGBA
+ * color is put together as:
+ * (A << 24) | (R << 16) | (G << 8) | B
+ * This is stored as BGRA on little-endian CPU architectures and ARGB on
+ * big-endian CPUs.
+ *
+ * @par
+ * When the pixel format is palettized RGB32 (AV_PIX_FMT_PAL8), the palettized
+ * image data is stored in AVFrame.data[0]. The palette is transported in
+ * AVFrame.data[1], is 1024 bytes long (256 4-byte entries) and is
+ * formatted the same as in AV_PIX_FMT_RGB32 described above (i.e., it is
+ * also endian-specific). Note also that the individual RGB32 palette
+ * components stored in AVFrame.data[1] should be in the range 0..255.
+ * This is important as many custom PAL8 video codecs that were designed
+ * to run on the IBM VGA graphics adapter use 6-bit palette components.
+ *
+ * @par
+ * For all the 8 bits per pixel formats, an RGB32 palette is in data[1] like
+ * for pal8. This palette is filled in automatically by the function
+ * allocating the picture.
+ */
+enum AVPixelFormat {
+ AV_PIX_FMT_NONE = -1,
+ AV_PIX_FMT_YUV420P, ///< planar YUV 4:2:0, 12bpp, (1 Cr & Cb sample per 2x2 Y samples)
+ AV_PIX_FMT_YUYV422, ///< packed YUV 4:2:2, 16bpp, Y0 Cb Y1 Cr
+ AV_PIX_FMT_RGB24, ///< packed RGB 8:8:8, 24bpp, RGBRGB...
+ AV_PIX_FMT_BGR24, ///< packed RGB 8:8:8, 24bpp, BGRBGR...
+ AV_PIX_FMT_YUV422P, ///< planar YUV 4:2:2, 16bpp, (1 Cr & Cb sample per 2x1 Y samples)
+ AV_PIX_FMT_YUV444P, ///< planar YUV 4:4:4, 24bpp, (1 Cr & Cb sample per 1x1 Y samples)
+ AV_PIX_FMT_YUV410P, ///< planar YUV 4:1:0, 9bpp, (1 Cr & Cb sample per 4x4 Y samples)
+ AV_PIX_FMT_YUV411P, ///< planar YUV 4:1:1, 12bpp, (1 Cr & Cb sample per 4x1 Y samples)
+ AV_PIX_FMT_GRAY8, ///< Y , 8bpp
+ AV_PIX_FMT_MONOWHITE, ///< Y , 1bpp, 0 is white, 1 is black, in each byte pixels are ordered from the msb to the lsb
+ AV_PIX_FMT_MONOBLACK, ///< Y , 1bpp, 0 is black, 1 is white, in each byte pixels are ordered from the msb to the lsb
+ AV_PIX_FMT_PAL8, ///< 8 bits with AV_PIX_FMT_RGB32 palette
+ AV_PIX_FMT_YUVJ420P, ///< planar YUV 4:2:0, 12bpp, full scale (JPEG), deprecated in favor of AV_PIX_FMT_YUV420P and setting color_range
+ AV_PIX_FMT_YUVJ422P, ///< planar YUV 4:2:2, 16bpp, full scale (JPEG), deprecated in favor of AV_PIX_FMT_YUV422P and setting color_range
+ AV_PIX_FMT_YUVJ444P, ///< planar YUV 4:4:4, 24bpp, full scale (JPEG), deprecated in favor of AV_PIX_FMT_YUV444P and setting color_range
+ AV_PIX_FMT_UYVY422, ///< packed YUV 4:2:2, 16bpp, Cb Y0 Cr Y1
+ AV_PIX_FMT_UYYVYY411, ///< packed YUV 4:1:1, 12bpp, Cb Y0 Y1 Cr Y2 Y3
+ AV_PIX_FMT_BGR8, ///< packed RGB 3:3:2, 8bpp, (msb)2B 3G 3R(lsb)
+ AV_PIX_FMT_BGR4, ///< packed RGB 1:2:1 bitstream, 4bpp, (msb)1B 2G 1R(lsb), a byte contains two pixels, the first pixel in the byte is the one composed by the 4 msb bits
+ AV_PIX_FMT_BGR4_BYTE, ///< packed RGB 1:2:1, 8bpp, (msb)1B 2G 1R(lsb)
+ AV_PIX_FMT_RGB8, ///< packed RGB 3:3:2, 8bpp, (msb)2R 3G 3B(lsb)
+ AV_PIX_FMT_RGB4, ///< packed RGB 1:2:1 bitstream, 4bpp, (msb)1R 2G 1B(lsb), a byte contains two pixels, the first pixel in the byte is the one composed by the 4 msb bits
+ AV_PIX_FMT_RGB4_BYTE, ///< packed RGB 1:2:1, 8bpp, (msb)1R 2G 1B(lsb)
+ AV_PIX_FMT_NV12, ///< planar YUV 4:2:0, 12bpp, 1 plane for Y and 1 plane for the UV components, which are interleaved (first byte U and the following byte V)
+ AV_PIX_FMT_NV21, ///< as above, but U and V bytes are swapped
+
+ AV_PIX_FMT_ARGB, ///< packed ARGB 8:8:8:8, 32bpp, ARGBARGB...
+ AV_PIX_FMT_RGBA, ///< packed RGBA 8:8:8:8, 32bpp, RGBARGBA...
+ AV_PIX_FMT_ABGR, ///< packed ABGR 8:8:8:8, 32bpp, ABGRABGR...
+ AV_PIX_FMT_BGRA, ///< packed BGRA 8:8:8:8, 32bpp, BGRABGRA...
+
+ AV_PIX_FMT_GRAY16BE, ///< Y , 16bpp, big-endian
+ AV_PIX_FMT_GRAY16LE, ///< Y , 16bpp, little-endian
+ AV_PIX_FMT_YUV440P, ///< planar YUV 4:4:0 (1 Cr & Cb sample per 1x2 Y samples)
+ AV_PIX_FMT_YUVJ440P, ///< planar YUV 4:4:0 full scale (JPEG), deprecated in favor of AV_PIX_FMT_YUV440P and setting color_range
+ AV_PIX_FMT_YUVA420P, ///< planar YUV 4:2:0, 20bpp, (1 Cr & Cb sample per 2x2 Y & A samples)
+ AV_PIX_FMT_RGB48BE, ///< packed RGB 16:16:16, 48bpp, 16R, 16G, 16B, the 2-byte value for each R/G/B component is stored as big-endian
+ AV_PIX_FMT_RGB48LE, ///< packed RGB 16:16:16, 48bpp, 16R, 16G, 16B, the 2-byte value for each R/G/B component is stored as little-endian
+
+ AV_PIX_FMT_RGB565BE, ///< packed RGB 5:6:5, 16bpp, (msb) 5R 6G 5B(lsb), big-endian
+ AV_PIX_FMT_RGB565LE, ///< packed RGB 5:6:5, 16bpp, (msb) 5R 6G 5B(lsb), little-endian
+ AV_PIX_FMT_RGB555BE, ///< packed RGB 5:5:5, 16bpp, (msb)1X 5R 5G 5B(lsb), big-endian , X=unused/undefined
+ AV_PIX_FMT_RGB555LE, ///< packed RGB 5:5:5, 16bpp, (msb)1X 5R 5G 5B(lsb), little-endian, X=unused/undefined
+
+ AV_PIX_FMT_BGR565BE, ///< packed BGR 5:6:5, 16bpp, (msb) 5B 6G 5R(lsb), big-endian
+ AV_PIX_FMT_BGR565LE, ///< packed BGR 5:6:5, 16bpp, (msb) 5B 6G 5R(lsb), little-endian
+ AV_PIX_FMT_BGR555BE, ///< packed BGR 5:5:5, 16bpp, (msb)1X 5B 5G 5R(lsb), big-endian , X=unused/undefined
+ AV_PIX_FMT_BGR555LE, ///< packed BGR 5:5:5, 16bpp, (msb)1X 5B 5G 5R(lsb), little-endian, X=unused/undefined
+
+#if FF_API_VAAPI
+ /** @name Deprecated pixel formats */
+ /**@{*/
+ AV_PIX_FMT_VAAPI_MOCO, ///< HW acceleration through VA API at motion compensation entry-point, Picture.data[3] contains a vaapi_render_state struct which contains macroblocks as well as various fields extracted from headers
+ AV_PIX_FMT_VAAPI_IDCT, ///< HW acceleration through VA API at IDCT entry-point, Picture.data[3] contains a vaapi_render_state struct which contains fields extracted from headers
+ AV_PIX_FMT_VAAPI_VLD, ///< HW decoding through VA API, Picture.data[3] contains a VASurfaceID
+ /**@}*/
+ AV_PIX_FMT_VAAPI = AV_PIX_FMT_VAAPI_VLD,
+#else
+ /**
+ * Hardware acceleration through VA-API, data[3] contains a
+ * VASurfaceID.
+ */
+ AV_PIX_FMT_VAAPI,
+#endif
+
+ AV_PIX_FMT_YUV420P16LE, ///< planar YUV 4:2:0, 24bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian
+ AV_PIX_FMT_YUV420P16BE, ///< planar YUV 4:2:0, 24bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian
+ AV_PIX_FMT_YUV422P16LE, ///< planar YUV 4:2:2, 32bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ AV_PIX_FMT_YUV422P16BE, ///< planar YUV 4:2:2, 32bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+ AV_PIX_FMT_YUV444P16LE, ///< planar YUV 4:4:4, 48bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian
+ AV_PIX_FMT_YUV444P16BE, ///< planar YUV 4:4:4, 48bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian
+ AV_PIX_FMT_DXVA2_VLD, ///< HW decoding through DXVA2, Picture.data[3] contains a LPDIRECT3DSURFACE9 pointer
+
+ AV_PIX_FMT_RGB444LE, ///< packed RGB 4:4:4, 16bpp, (msb)4X 4R 4G 4B(lsb), little-endian, X=unused/undefined
+ AV_PIX_FMT_RGB444BE, ///< packed RGB 4:4:4, 16bpp, (msb)4X 4R 4G 4B(lsb), big-endian, X=unused/undefined
+ AV_PIX_FMT_BGR444LE, ///< packed BGR 4:4:4, 16bpp, (msb)4X 4B 4G 4R(lsb), little-endian, X=unused/undefined
+ AV_PIX_FMT_BGR444BE, ///< packed BGR 4:4:4, 16bpp, (msb)4X 4B 4G 4R(lsb), big-endian, X=unused/undefined
+ AV_PIX_FMT_YA8, ///< 8 bits gray, 8 bits alpha
+
+ AV_PIX_FMT_Y400A = AV_PIX_FMT_YA8, ///< alias for AV_PIX_FMT_YA8
+ AV_PIX_FMT_GRAY8A= AV_PIX_FMT_YA8, ///< alias for AV_PIX_FMT_YA8
+
+ AV_PIX_FMT_BGR48BE, ///< packed RGB 16:16:16, 48bpp, 16B, 16G, 16R, the 2-byte value for each R/G/B component is stored as big-endian
+ AV_PIX_FMT_BGR48LE, ///< packed RGB 16:16:16, 48bpp, 16B, 16G, 16R, the 2-byte value for each R/G/B component is stored as little-endian
+
+ /**
+ * The following 12 formats have the disadvantage of needing 1 format for each bit depth.
+ * Notice that each 9/10 bits sample is stored in 16 bits with extra padding.
+ * If you want to support multiple bit depths, then using AV_PIX_FMT_YUV420P16* with the bpp stored separately is better.
+ */
+ AV_PIX_FMT_YUV420P9BE, ///< planar YUV 4:2:0, 13.5bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian
+ AV_PIX_FMT_YUV420P9LE, ///< planar YUV 4:2:0, 13.5bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian
+ AV_PIX_FMT_YUV420P10BE,///< planar YUV 4:2:0, 15bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian
+ AV_PIX_FMT_YUV420P10LE,///< planar YUV 4:2:0, 15bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian
+ AV_PIX_FMT_YUV422P10BE,///< planar YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+ AV_PIX_FMT_YUV422P10LE,///< planar YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ AV_PIX_FMT_YUV444P9BE, ///< planar YUV 4:4:4, 27bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian
+ AV_PIX_FMT_YUV444P9LE, ///< planar YUV 4:4:4, 27bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian
+ AV_PIX_FMT_YUV444P10BE,///< planar YUV 4:4:4, 30bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian
+ AV_PIX_FMT_YUV444P10LE,///< planar YUV 4:4:4, 30bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian
+ AV_PIX_FMT_YUV422P9BE, ///< planar YUV 4:2:2, 18bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+ AV_PIX_FMT_YUV422P9LE, ///< planar YUV 4:2:2, 18bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ AV_PIX_FMT_GBRP, ///< planar GBR 4:4:4 24bpp
+ AV_PIX_FMT_GBR24P = AV_PIX_FMT_GBRP, // alias for #AV_PIX_FMT_GBRP
+ AV_PIX_FMT_GBRP9BE, ///< planar GBR 4:4:4 27bpp, big-endian
+ AV_PIX_FMT_GBRP9LE, ///< planar GBR 4:4:4 27bpp, little-endian
+ AV_PIX_FMT_GBRP10BE, ///< planar GBR 4:4:4 30bpp, big-endian
+ AV_PIX_FMT_GBRP10LE, ///< planar GBR 4:4:4 30bpp, little-endian
+ AV_PIX_FMT_GBRP16BE, ///< planar GBR 4:4:4 48bpp, big-endian
+ AV_PIX_FMT_GBRP16LE, ///< planar GBR 4:4:4 48bpp, little-endian
+ AV_PIX_FMT_YUVA422P, ///< planar YUV 4:2:2 24bpp, (1 Cr & Cb sample per 2x1 Y & A samples)
+ AV_PIX_FMT_YUVA444P, ///< planar YUV 4:4:4 32bpp, (1 Cr & Cb sample per 1x1 Y & A samples)
+ AV_PIX_FMT_YUVA420P9BE, ///< planar YUV 4:2:0 22.5bpp, (1 Cr & Cb sample per 2x2 Y & A samples), big-endian
+ AV_PIX_FMT_YUVA420P9LE, ///< planar YUV 4:2:0 22.5bpp, (1 Cr & Cb sample per 2x2 Y & A samples), little-endian
+ AV_PIX_FMT_YUVA422P9BE, ///< planar YUV 4:2:2 27bpp, (1 Cr & Cb sample per 2x1 Y & A samples), big-endian
+ AV_PIX_FMT_YUVA422P9LE, ///< planar YUV 4:2:2 27bpp, (1 Cr & Cb sample per 2x1 Y & A samples), little-endian
+ AV_PIX_FMT_YUVA444P9BE, ///< planar YUV 4:4:4 36bpp, (1 Cr & Cb sample per 1x1 Y & A samples), big-endian
+ AV_PIX_FMT_YUVA444P9LE, ///< planar YUV 4:4:4 36bpp, (1 Cr & Cb sample per 1x1 Y & A samples), little-endian
+ AV_PIX_FMT_YUVA420P10BE, ///< planar YUV 4:2:0 25bpp, (1 Cr & Cb sample per 2x2 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA420P10LE, ///< planar YUV 4:2:0 25bpp, (1 Cr & Cb sample per 2x2 Y & A samples, little-endian)
+ AV_PIX_FMT_YUVA422P10BE, ///< planar YUV 4:2:2 30bpp, (1 Cr & Cb sample per 2x1 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA422P10LE, ///< planar YUV 4:2:2 30bpp, (1 Cr & Cb sample per 2x1 Y & A samples, little-endian)
+ AV_PIX_FMT_YUVA444P10BE, ///< planar YUV 4:4:4 40bpp, (1 Cr & Cb sample per 1x1 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA444P10LE, ///< planar YUV 4:4:4 40bpp, (1 Cr & Cb sample per 1x1 Y & A samples, little-endian)
+ AV_PIX_FMT_YUVA420P16BE, ///< planar YUV 4:2:0 40bpp, (1 Cr & Cb sample per 2x2 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA420P16LE, ///< planar YUV 4:2:0 40bpp, (1 Cr & Cb sample per 2x2 Y & A samples, little-endian)
+ AV_PIX_FMT_YUVA422P16BE, ///< planar YUV 4:2:2 48bpp, (1 Cr & Cb sample per 2x1 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA422P16LE, ///< planar YUV 4:2:2 48bpp, (1 Cr & Cb sample per 2x1 Y & A samples, little-endian)
+ AV_PIX_FMT_YUVA444P16BE, ///< planar YUV 4:4:4 64bpp, (1 Cr & Cb sample per 1x1 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA444P16LE, ///< planar YUV 4:4:4 64bpp, (1 Cr & Cb sample per 1x1 Y & A samples, little-endian)
+
+ AV_PIX_FMT_VDPAU, ///< HW acceleration through VDPAU, Picture.data[3] contains a VdpVideoSurface
+
+ AV_PIX_FMT_XYZ12LE, ///< packed XYZ 4:4:4, 36 bpp, (msb) 12X, 12Y, 12Z (lsb), the 2-byte value for each X/Y/Z is stored as little-endian, the 4 lower bits are set to 0
+ AV_PIX_FMT_XYZ12BE, ///< packed XYZ 4:4:4, 36 bpp, (msb) 12X, 12Y, 12Z (lsb), the 2-byte value for each X/Y/Z is stored as big-endian, the 4 lower bits are set to 0
+ AV_PIX_FMT_NV16, ///< interleaved chroma YUV 4:2:2, 16bpp, (1 Cr & Cb sample per 2x1 Y samples)
+ AV_PIX_FMT_NV20LE, ///< interleaved chroma YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ AV_PIX_FMT_NV20BE, ///< interleaved chroma YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+
+ AV_PIX_FMT_RGBA64BE, ///< packed RGBA 16:16:16:16, 64bpp, 16R, 16G, 16B, 16A, the 2-byte value for each R/G/B/A component is stored as big-endian
+ AV_PIX_FMT_RGBA64LE, ///< packed RGBA 16:16:16:16, 64bpp, 16R, 16G, 16B, 16A, the 2-byte value for each R/G/B/A component is stored as little-endian
+ AV_PIX_FMT_BGRA64BE, ///< packed RGBA 16:16:16:16, 64bpp, 16B, 16G, 16R, 16A, the 2-byte value for each R/G/B/A component is stored as big-endian
+ AV_PIX_FMT_BGRA64LE, ///< packed RGBA 16:16:16:16, 64bpp, 16B, 16G, 16R, 16A, the 2-byte value for each R/G/B/A component is stored as little-endian
+
+ AV_PIX_FMT_YVYU422, ///< packed YUV 4:2:2, 16bpp, Y0 Cr Y1 Cb
+
+ AV_PIX_FMT_YA16BE, ///< 16 bits gray, 16 bits alpha (big-endian)
+ AV_PIX_FMT_YA16LE, ///< 16 bits gray, 16 bits alpha (little-endian)
+
+ AV_PIX_FMT_GBRAP, ///< planar GBRA 4:4:4:4 32bpp
+ AV_PIX_FMT_GBRAP16BE, ///< planar GBRA 4:4:4:4 64bpp, big-endian
+ AV_PIX_FMT_GBRAP16LE, ///< planar GBRA 4:4:4:4 64bpp, little-endian
+ /**
+ * HW acceleration through QSV, data[3] contains a pointer to the
+ * mfxFrameSurface1 structure.
+ */
+ AV_PIX_FMT_QSV,
+ /**
+ * HW acceleration though MMAL, data[3] contains a pointer to the
+ * MMAL_BUFFER_HEADER_T structure.
+ */
+ AV_PIX_FMT_MMAL,
+
+ AV_PIX_FMT_D3D11VA_VLD, ///< HW decoding through Direct3D11 via old API, Picture.data[3] contains a ID3D11VideoDecoderOutputView pointer
+
+ /**
+ * HW acceleration through CUDA. data[i] contain CUdeviceptr pointers
+ * exactly as for system memory frames.
+ */
+ AV_PIX_FMT_CUDA,
+
+ AV_PIX_FMT_0RGB, ///< packed RGB 8:8:8, 32bpp, XRGBXRGB... X=unused/undefined
+ AV_PIX_FMT_RGB0, ///< packed RGB 8:8:8, 32bpp, RGBXRGBX... X=unused/undefined
+ AV_PIX_FMT_0BGR, ///< packed BGR 8:8:8, 32bpp, XBGRXBGR... X=unused/undefined
+ AV_PIX_FMT_BGR0, ///< packed BGR 8:8:8, 32bpp, BGRXBGRX... X=unused/undefined
+
+ AV_PIX_FMT_YUV420P12BE, ///< planar YUV 4:2:0,18bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian
+ AV_PIX_FMT_YUV420P12LE, ///< planar YUV 4:2:0,18bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian
+ AV_PIX_FMT_YUV420P14BE, ///< planar YUV 4:2:0,21bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian
+ AV_PIX_FMT_YUV420P14LE, ///< planar YUV 4:2:0,21bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian
+ AV_PIX_FMT_YUV422P12BE, ///< planar YUV 4:2:2,24bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+ AV_PIX_FMT_YUV422P12LE, ///< planar YUV 4:2:2,24bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ AV_PIX_FMT_YUV422P14BE, ///< planar YUV 4:2:2,28bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+ AV_PIX_FMT_YUV422P14LE, ///< planar YUV 4:2:2,28bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ AV_PIX_FMT_YUV444P12BE, ///< planar YUV 4:4:4,36bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian
+ AV_PIX_FMT_YUV444P12LE, ///< planar YUV 4:4:4,36bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian
+ AV_PIX_FMT_YUV444P14BE, ///< planar YUV 4:4:4,42bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian
+ AV_PIX_FMT_YUV444P14LE, ///< planar YUV 4:4:4,42bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian
+ AV_PIX_FMT_GBRP12BE, ///< planar GBR 4:4:4 36bpp, big-endian
+ AV_PIX_FMT_GBRP12LE, ///< planar GBR 4:4:4 36bpp, little-endian
+ AV_PIX_FMT_GBRP14BE, ///< planar GBR 4:4:4 42bpp, big-endian
+ AV_PIX_FMT_GBRP14LE, ///< planar GBR 4:4:4 42bpp, little-endian
+ AV_PIX_FMT_YUVJ411P, ///< planar YUV 4:1:1, 12bpp, (1 Cr & Cb sample per 4x1 Y samples) full scale (JPEG), deprecated in favor of AV_PIX_FMT_YUV411P and setting color_range
+
+ AV_PIX_FMT_BAYER_BGGR8, ///< bayer, BGBG..(odd line), GRGR..(even line), 8-bit samples */
+ AV_PIX_FMT_BAYER_RGGB8, ///< bayer, RGRG..(odd line), GBGB..(even line), 8-bit samples */
+ AV_PIX_FMT_BAYER_GBRG8, ///< bayer, GBGB..(odd line), RGRG..(even line), 8-bit samples */
+ AV_PIX_FMT_BAYER_GRBG8, ///< bayer, GRGR..(odd line), BGBG..(even line), 8-bit samples */
+ AV_PIX_FMT_BAYER_BGGR16LE, ///< bayer, BGBG..(odd line), GRGR..(even line), 16-bit samples, little-endian */
+ AV_PIX_FMT_BAYER_BGGR16BE, ///< bayer, BGBG..(odd line), GRGR..(even line), 16-bit samples, big-endian */
+ AV_PIX_FMT_BAYER_RGGB16LE, ///< bayer, RGRG..(odd line), GBGB..(even line), 16-bit samples, little-endian */
+ AV_PIX_FMT_BAYER_RGGB16BE, ///< bayer, RGRG..(odd line), GBGB..(even line), 16-bit samples, big-endian */
+ AV_PIX_FMT_BAYER_GBRG16LE, ///< bayer, GBGB..(odd line), RGRG..(even line), 16-bit samples, little-endian */
+ AV_PIX_FMT_BAYER_GBRG16BE, ///< bayer, GBGB..(odd line), RGRG..(even line), 16-bit samples, big-endian */
+ AV_PIX_FMT_BAYER_GRBG16LE, ///< bayer, GRGR..(odd line), BGBG..(even line), 16-bit samples, little-endian */
+ AV_PIX_FMT_BAYER_GRBG16BE, ///< bayer, GRGR..(odd line), BGBG..(even line), 16-bit samples, big-endian */
+
+ AV_PIX_FMT_XVMC,///< XVideo Motion Acceleration via common packet passing
+
+ AV_PIX_FMT_YUV440P10LE, ///< planar YUV 4:4:0,20bpp, (1 Cr & Cb sample per 1x2 Y samples), little-endian
+ AV_PIX_FMT_YUV440P10BE, ///< planar YUV 4:4:0,20bpp, (1 Cr & Cb sample per 1x2 Y samples), big-endian
+ AV_PIX_FMT_YUV440P12LE, ///< planar YUV 4:4:0,24bpp, (1 Cr & Cb sample per 1x2 Y samples), little-endian
+ AV_PIX_FMT_YUV440P12BE, ///< planar YUV 4:4:0,24bpp, (1 Cr & Cb sample per 1x2 Y samples), big-endian
+ AV_PIX_FMT_AYUV64LE, ///< packed AYUV 4:4:4,64bpp (1 Cr & Cb sample per 1x1 Y & A samples), little-endian
+ AV_PIX_FMT_AYUV64BE, ///< packed AYUV 4:4:4,64bpp (1 Cr & Cb sample per 1x1 Y & A samples), big-endian
+
+ AV_PIX_FMT_VIDEOTOOLBOX, ///< hardware decoding through Videotoolbox
+
+ AV_PIX_FMT_P010LE, ///< like NV12, with 10bpp per component, data in the high bits, zeros in the low bits, little-endian
+ AV_PIX_FMT_P010BE, ///< like NV12, with 10bpp per component, data in the high bits, zeros in the low bits, big-endian
+
+ AV_PIX_FMT_GBRAP12BE, ///< planar GBR 4:4:4:4 48bpp, big-endian
+ AV_PIX_FMT_GBRAP12LE, ///< planar GBR 4:4:4:4 48bpp, little-endian
+
+ AV_PIX_FMT_GBRAP10BE, ///< planar GBR 4:4:4:4 40bpp, big-endian
+ AV_PIX_FMT_GBRAP10LE, ///< planar GBR 4:4:4:4 40bpp, little-endian
+
+ AV_PIX_FMT_MEDIACODEC, ///< hardware decoding through MediaCodec
+
+ AV_PIX_FMT_GRAY12BE, ///< Y , 12bpp, big-endian
+ AV_PIX_FMT_GRAY12LE, ///< Y , 12bpp, little-endian
+ AV_PIX_FMT_GRAY10BE, ///< Y , 10bpp, big-endian
+ AV_PIX_FMT_GRAY10LE, ///< Y , 10bpp, little-endian
+
+ AV_PIX_FMT_P016LE, ///< like NV12, with 16bpp per component, little-endian
+ AV_PIX_FMT_P016BE, ///< like NV12, with 16bpp per component, big-endian
+
+ /**
+ * Hardware surfaces for Direct3D11.
+ *
+ * This is preferred over the legacy AV_PIX_FMT_D3D11VA_VLD. The new D3D11
+ * hwaccel API and filtering support AV_PIX_FMT_D3D11 only.
+ *
+ * data[0] contains a ID3D11Texture2D pointer, and data[1] contains the
+ * texture array index of the frame as intptr_t if the ID3D11Texture2D is
+ * an array texture (or always 0 if it's a normal texture).
+ */
+ AV_PIX_FMT_D3D11,
+
+ AV_PIX_FMT_GRAY9BE, ///< Y , 9bpp, big-endian
+ AV_PIX_FMT_GRAY9LE, ///< Y , 9bpp, little-endian
+
+ AV_PIX_FMT_GBRPF32BE, ///< IEEE-754 single precision planar GBR 4:4:4, 96bpp, big-endian
+ AV_PIX_FMT_GBRPF32LE, ///< IEEE-754 single precision planar GBR 4:4:4, 96bpp, little-endian
+ AV_PIX_FMT_GBRAPF32BE, ///< IEEE-754 single precision planar GBRA 4:4:4:4, 128bpp, big-endian
+ AV_PIX_FMT_GBRAPF32LE, ///< IEEE-754 single precision planar GBRA 4:4:4:4, 128bpp, little-endian
+
+ /**
+ * DRM-managed buffers exposed through PRIME buffer sharing.
+ *
+ * data[0] points to an AVDRMFrameDescriptor.
+ */
+ AV_PIX_FMT_DRM_PRIME,
+ /**
+ * Hardware surfaces for OpenCL.
+ *
+ * data[i] contain 2D image objects (typed in C as cl_mem, used
+ * in OpenCL as image2d_t) for each plane of the surface.
+ */
+ AV_PIX_FMT_OPENCL,
+
+ AV_PIX_FMT_NB ///< number of pixel formats, DO NOT USE THIS if you want to link with shared libav* because the number of formats might differ between versions
+};
+
+#if AV_HAVE_BIGENDIAN
+# define AV_PIX_FMT_NE(be, le) AV_PIX_FMT_##be
+#else
+# define AV_PIX_FMT_NE(be, le) AV_PIX_FMT_##le
+#endif
+
+#define AV_PIX_FMT_RGB32 AV_PIX_FMT_NE(ARGB, BGRA)
+#define AV_PIX_FMT_RGB32_1 AV_PIX_FMT_NE(RGBA, ABGR)
+#define AV_PIX_FMT_BGR32 AV_PIX_FMT_NE(ABGR, RGBA)
+#define AV_PIX_FMT_BGR32_1 AV_PIX_FMT_NE(BGRA, ARGB)
+#define AV_PIX_FMT_0RGB32 AV_PIX_FMT_NE(0RGB, BGR0)
+#define AV_PIX_FMT_0BGR32 AV_PIX_FMT_NE(0BGR, RGB0)
+
+#define AV_PIX_FMT_GRAY9 AV_PIX_FMT_NE(GRAY9BE, GRAY9LE)
+#define AV_PIX_FMT_GRAY10 AV_PIX_FMT_NE(GRAY10BE, GRAY10LE)
+#define AV_PIX_FMT_GRAY12 AV_PIX_FMT_NE(GRAY12BE, GRAY12LE)
+#define AV_PIX_FMT_GRAY16 AV_PIX_FMT_NE(GRAY16BE, GRAY16LE)
+#define AV_PIX_FMT_YA16 AV_PIX_FMT_NE(YA16BE, YA16LE)
+#define AV_PIX_FMT_RGB48 AV_PIX_FMT_NE(RGB48BE, RGB48LE)
+#define AV_PIX_FMT_RGB565 AV_PIX_FMT_NE(RGB565BE, RGB565LE)
+#define AV_PIX_FMT_RGB555 AV_PIX_FMT_NE(RGB555BE, RGB555LE)
+#define AV_PIX_FMT_RGB444 AV_PIX_FMT_NE(RGB444BE, RGB444LE)
+#define AV_PIX_FMT_RGBA64 AV_PIX_FMT_NE(RGBA64BE, RGBA64LE)
+#define AV_PIX_FMT_BGR48 AV_PIX_FMT_NE(BGR48BE, BGR48LE)
+#define AV_PIX_FMT_BGR565 AV_PIX_FMT_NE(BGR565BE, BGR565LE)
+#define AV_PIX_FMT_BGR555 AV_PIX_FMT_NE(BGR555BE, BGR555LE)
+#define AV_PIX_FMT_BGR444 AV_PIX_FMT_NE(BGR444BE, BGR444LE)
+#define AV_PIX_FMT_BGRA64 AV_PIX_FMT_NE(BGRA64BE, BGRA64LE)
+
+#define AV_PIX_FMT_YUV420P9 AV_PIX_FMT_NE(YUV420P9BE , YUV420P9LE)
+#define AV_PIX_FMT_YUV422P9 AV_PIX_FMT_NE(YUV422P9BE , YUV422P9LE)
+#define AV_PIX_FMT_YUV444P9 AV_PIX_FMT_NE(YUV444P9BE , YUV444P9LE)
+#define AV_PIX_FMT_YUV420P10 AV_PIX_FMT_NE(YUV420P10BE, YUV420P10LE)
+#define AV_PIX_FMT_YUV422P10 AV_PIX_FMT_NE(YUV422P10BE, YUV422P10LE)
+#define AV_PIX_FMT_YUV440P10 AV_PIX_FMT_NE(YUV440P10BE, YUV440P10LE)
+#define AV_PIX_FMT_YUV444P10 AV_PIX_FMT_NE(YUV444P10BE, YUV444P10LE)
+#define AV_PIX_FMT_YUV420P12 AV_PIX_FMT_NE(YUV420P12BE, YUV420P12LE)
+#define AV_PIX_FMT_YUV422P12 AV_PIX_FMT_NE(YUV422P12BE, YUV422P12LE)
+#define AV_PIX_FMT_YUV440P12 AV_PIX_FMT_NE(YUV440P12BE, YUV440P12LE)
+#define AV_PIX_FMT_YUV444P12 AV_PIX_FMT_NE(YUV444P12BE, YUV444P12LE)
+#define AV_PIX_FMT_YUV420P14 AV_PIX_FMT_NE(YUV420P14BE, YUV420P14LE)
+#define AV_PIX_FMT_YUV422P14 AV_PIX_FMT_NE(YUV422P14BE, YUV422P14LE)
+#define AV_PIX_FMT_YUV444P14 AV_PIX_FMT_NE(YUV444P14BE, YUV444P14LE)
+#define AV_PIX_FMT_YUV420P16 AV_PIX_FMT_NE(YUV420P16BE, YUV420P16LE)
+#define AV_PIX_FMT_YUV422P16 AV_PIX_FMT_NE(YUV422P16BE, YUV422P16LE)
+#define AV_PIX_FMT_YUV444P16 AV_PIX_FMT_NE(YUV444P16BE, YUV444P16LE)
+
+#define AV_PIX_FMT_GBRP9 AV_PIX_FMT_NE(GBRP9BE , GBRP9LE)
+#define AV_PIX_FMT_GBRP10 AV_PIX_FMT_NE(GBRP10BE, GBRP10LE)
+#define AV_PIX_FMT_GBRP12 AV_PIX_FMT_NE(GBRP12BE, GBRP12LE)
+#define AV_PIX_FMT_GBRP14 AV_PIX_FMT_NE(GBRP14BE, GBRP14LE)
+#define AV_PIX_FMT_GBRP16 AV_PIX_FMT_NE(GBRP16BE, GBRP16LE)
+#define AV_PIX_FMT_GBRAP10 AV_PIX_FMT_NE(GBRAP10BE, GBRAP10LE)
+#define AV_PIX_FMT_GBRAP12 AV_PIX_FMT_NE(GBRAP12BE, GBRAP12LE)
+#define AV_PIX_FMT_GBRAP16 AV_PIX_FMT_NE(GBRAP16BE, GBRAP16LE)
+
+#define AV_PIX_FMT_BAYER_BGGR16 AV_PIX_FMT_NE(BAYER_BGGR16BE, BAYER_BGGR16LE)
+#define AV_PIX_FMT_BAYER_RGGB16 AV_PIX_FMT_NE(BAYER_RGGB16BE, BAYER_RGGB16LE)
+#define AV_PIX_FMT_BAYER_GBRG16 AV_PIX_FMT_NE(BAYER_GBRG16BE, BAYER_GBRG16LE)
+#define AV_PIX_FMT_BAYER_GRBG16 AV_PIX_FMT_NE(BAYER_GRBG16BE, BAYER_GRBG16LE)
+
+#define AV_PIX_FMT_GBRPF32 AV_PIX_FMT_NE(GBRPF32BE, GBRPF32LE)
+#define AV_PIX_FMT_GBRAPF32 AV_PIX_FMT_NE(GBRAPF32BE, GBRAPF32LE)
+
+#define AV_PIX_FMT_YUVA420P9 AV_PIX_FMT_NE(YUVA420P9BE , YUVA420P9LE)
+#define AV_PIX_FMT_YUVA422P9 AV_PIX_FMT_NE(YUVA422P9BE , YUVA422P9LE)
+#define AV_PIX_FMT_YUVA444P9 AV_PIX_FMT_NE(YUVA444P9BE , YUVA444P9LE)
+#define AV_PIX_FMT_YUVA420P10 AV_PIX_FMT_NE(YUVA420P10BE, YUVA420P10LE)
+#define AV_PIX_FMT_YUVA422P10 AV_PIX_FMT_NE(YUVA422P10BE, YUVA422P10LE)
+#define AV_PIX_FMT_YUVA444P10 AV_PIX_FMT_NE(YUVA444P10BE, YUVA444P10LE)
+#define AV_PIX_FMT_YUVA420P16 AV_PIX_FMT_NE(YUVA420P16BE, YUVA420P16LE)
+#define AV_PIX_FMT_YUVA422P16 AV_PIX_FMT_NE(YUVA422P16BE, YUVA422P16LE)
+#define AV_PIX_FMT_YUVA444P16 AV_PIX_FMT_NE(YUVA444P16BE, YUVA444P16LE)
+
+#define AV_PIX_FMT_XYZ12 AV_PIX_FMT_NE(XYZ12BE, XYZ12LE)
+#define AV_PIX_FMT_NV20 AV_PIX_FMT_NE(NV20BE, NV20LE)
+#define AV_PIX_FMT_AYUV64 AV_PIX_FMT_NE(AYUV64BE, AYUV64LE)
+#define AV_PIX_FMT_P010 AV_PIX_FMT_NE(P010BE, P010LE)
+#define AV_PIX_FMT_P016 AV_PIX_FMT_NE(P016BE, P016LE)
+
+/**
+ * Chromaticity coordinates of the source primaries.
+ * These values match the ones defined by ISO/IEC 23001-8_2013 § 7.1.
+ */
+enum AVColorPrimaries {
+ AVCOL_PRI_RESERVED0 = 0,
+ AVCOL_PRI_BT709 = 1, ///< also ITU-R BT1361 / IEC 61966-2-4 / SMPTE RP177 Annex B
+ AVCOL_PRI_UNSPECIFIED = 2,
+ AVCOL_PRI_RESERVED = 3,
+ AVCOL_PRI_BT470M = 4, ///< also FCC Title 47 Code of Federal Regulations 73.682 (a)(20)
+
+ AVCOL_PRI_BT470BG = 5, ///< also ITU-R BT601-6 625 / ITU-R BT1358 625 / ITU-R BT1700 625 PAL & SECAM
+ AVCOL_PRI_SMPTE170M = 6, ///< also ITU-R BT601-6 525 / ITU-R BT1358 525 / ITU-R BT1700 NTSC
+ AVCOL_PRI_SMPTE240M = 7, ///< functionally identical to above
+ AVCOL_PRI_FILM = 8, ///< colour filters using Illuminant C
+ AVCOL_PRI_BT2020 = 9, ///< ITU-R BT2020
+ AVCOL_PRI_SMPTE428 = 10, ///< SMPTE ST 428-1 (CIE 1931 XYZ)
+ AVCOL_PRI_SMPTEST428_1 = AVCOL_PRI_SMPTE428,
+ AVCOL_PRI_SMPTE431 = 11, ///< SMPTE ST 431-2 (2011) / DCI P3
+ AVCOL_PRI_SMPTE432 = 12, ///< SMPTE ST 432-1 (2010) / P3 D65 / Display P3
+ AVCOL_PRI_JEDEC_P22 = 22, ///< JEDEC P22 phosphors
+ AVCOL_PRI_NB ///< Not part of ABI
+};
+
+/**
+ * Color Transfer Characteristic.
+ * These values match the ones defined by ISO/IEC 23001-8_2013 § 7.2.
+ */
+enum AVColorTransferCharacteristic {
+ AVCOL_TRC_RESERVED0 = 0,
+ AVCOL_TRC_BT709 = 1, ///< also ITU-R BT1361
+ AVCOL_TRC_UNSPECIFIED = 2,
+ AVCOL_TRC_RESERVED = 3,
+ AVCOL_TRC_GAMMA22 = 4, ///< also ITU-R BT470M / ITU-R BT1700 625 PAL & SECAM
+ AVCOL_TRC_GAMMA28 = 5, ///< also ITU-R BT470BG
+ AVCOL_TRC_SMPTE170M = 6, ///< also ITU-R BT601-6 525 or 625 / ITU-R BT1358 525 or 625 / ITU-R BT1700 NTSC
+ AVCOL_TRC_SMPTE240M = 7,
+ AVCOL_TRC_LINEAR = 8, ///< "Linear transfer characteristics"
+ AVCOL_TRC_LOG = 9, ///< "Logarithmic transfer characteristic (100:1 range)"
+ AVCOL_TRC_LOG_SQRT = 10, ///< "Logarithmic transfer characteristic (100 * Sqrt(10) : 1 range)"
+ AVCOL_TRC_IEC61966_2_4 = 11, ///< IEC 61966-2-4
+ AVCOL_TRC_BT1361_ECG = 12, ///< ITU-R BT1361 Extended Colour Gamut
+ AVCOL_TRC_IEC61966_2_1 = 13, ///< IEC 61966-2-1 (sRGB or sYCC)
+ AVCOL_TRC_BT2020_10 = 14, ///< ITU-R BT2020 for 10-bit system
+ AVCOL_TRC_BT2020_12 = 15, ///< ITU-R BT2020 for 12-bit system
+ AVCOL_TRC_SMPTE2084 = 16, ///< SMPTE ST 2084 for 10-, 12-, 14- and 16-bit systems
+ AVCOL_TRC_SMPTEST2084 = AVCOL_TRC_SMPTE2084,
+ AVCOL_TRC_SMPTE428 = 17, ///< SMPTE ST 428-1
+ AVCOL_TRC_SMPTEST428_1 = AVCOL_TRC_SMPTE428,
+ AVCOL_TRC_ARIB_STD_B67 = 18, ///< ARIB STD-B67, known as "Hybrid log-gamma"
+ AVCOL_TRC_NB ///< Not part of ABI
+};
+
+/**
+ * YUV colorspace type.
+ * These values match the ones defined by ISO/IEC 23001-8_2013 § 7.3.
+ */
+enum AVColorSpace {
+ AVCOL_SPC_RGB = 0, ///< order of coefficients is actually GBR, also IEC 61966-2-1 (sRGB)
+ AVCOL_SPC_BT709 = 1, ///< also ITU-R BT1361 / IEC 61966-2-4 xvYCC709 / SMPTE RP177 Annex B
+ AVCOL_SPC_UNSPECIFIED = 2,
+ AVCOL_SPC_RESERVED = 3,
+ AVCOL_SPC_FCC = 4, ///< FCC Title 47 Code of Federal Regulations 73.682 (a)(20)
+ AVCOL_SPC_BT470BG = 5, ///< also ITU-R BT601-6 625 / ITU-R BT1358 625 / ITU-R BT1700 625 PAL & SECAM / IEC 61966-2-4 xvYCC601
+ AVCOL_SPC_SMPTE170M = 6, ///< also ITU-R BT601-6 525 / ITU-R BT1358 525 / ITU-R BT1700 NTSC
+ AVCOL_SPC_SMPTE240M = 7, ///< functionally identical to above
+ AVCOL_SPC_YCGCO = 8, ///< Used by Dirac / VC-2 and H.264 FRext, see ITU-T SG16
+ AVCOL_SPC_YCOCG = AVCOL_SPC_YCGCO,
+ AVCOL_SPC_BT2020_NCL = 9, ///< ITU-R BT2020 non-constant luminance system
+ AVCOL_SPC_BT2020_CL = 10, ///< ITU-R BT2020 constant luminance system
+ AVCOL_SPC_SMPTE2085 = 11, ///< SMPTE 2085, Y'D'zD'x
+ AVCOL_SPC_CHROMA_DERIVED_NCL = 12, ///< Chromaticity-derived non-constant luminance system
+ AVCOL_SPC_CHROMA_DERIVED_CL = 13, ///< Chromaticity-derived constant luminance system
+ AVCOL_SPC_ICTCP = 14, ///< ITU-R BT.2100-0, ICtCp
+ AVCOL_SPC_NB ///< Not part of ABI
+};
+
+/**
+ * MPEG vs JPEG YUV range.
+ */
+enum AVColorRange {
+ AVCOL_RANGE_UNSPECIFIED = 0,
+ AVCOL_RANGE_MPEG = 1, ///< the normal 219*2^(n-8) "MPEG" YUV ranges
+ AVCOL_RANGE_JPEG = 2, ///< the normal 2^n-1 "JPEG" YUV ranges
+ AVCOL_RANGE_NB ///< Not part of ABI
+};
+
+/**
+ * Location of chroma samples.
+ *
+ * Illustration showing the location of the first (top left) chroma sample of the
+ * image, the left shows only luma, the right
+ * shows the location of the chroma sample, the 2 could be imagined to overlay
+ * each other but are drawn separately due to limitations of ASCII
+ *
+ * 1st 2nd 1st 2nd horizontal luma sample positions
+ * v v v v
+ * ______ ______
+ *1st luma line > |X X ... |3 4 X ... X are luma samples,
+ * | |1 2 1-6 are possible chroma positions
+ *2nd luma line > |X X ... |5 6 X ... 0 is undefined/unknown position
+ */
+enum AVChromaLocation {
+ AVCHROMA_LOC_UNSPECIFIED = 0,
+ AVCHROMA_LOC_LEFT = 1, ///< MPEG-2/4 4:2:0, H.264 default for 4:2:0
+ AVCHROMA_LOC_CENTER = 2, ///< MPEG-1 4:2:0, JPEG 4:2:0, H.263 4:2:0
+ AVCHROMA_LOC_TOPLEFT = 3, ///< ITU-R 601, SMPTE 274M 296M S314M(DV 4:1:1), mpeg2 4:2:2
+ AVCHROMA_LOC_TOP = 4,
+ AVCHROMA_LOC_BOTTOMLEFT = 5,
+ AVCHROMA_LOC_BOTTOM = 6,
+ AVCHROMA_LOC_NB ///< Not part of ABI
+};
+
+#endif /* AVUTIL_PIXFMT_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/rational.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/rational.h
new file mode 100644
index 0000000000..5c6b67b4e9
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/rational.h
@@ -0,0 +1,214 @@
+/*
+ * rational numbers
+ * Copyright (c) 2003 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * @ingroup lavu_math_rational
+ * Utilties for rational number calculation.
+ * @author Michael Niedermayer <michaelni@gmx.at>
+ */
+
+#ifndef AVUTIL_RATIONAL_H
+#define AVUTIL_RATIONAL_H
+
+#include <stdint.h>
+#include <limits.h>
+#include "attributes.h"
+
+/**
+ * @defgroup lavu_math_rational AVRational
+ * @ingroup lavu_math
+ * Rational number calculation.
+ *
+ * While rational numbers can be expressed as floating-point numbers, the
+ * conversion process is a lossy one, so are floating-point operations. On the
+ * other hand, the nature of FFmpeg demands highly accurate calculation of
+ * timestamps. This set of rational number utilities serves as a generic
+ * interface for manipulating rational numbers as pairs of numerators and
+ * denominators.
+ *
+ * Many of the functions that operate on AVRational's have the suffix `_q`, in
+ * reference to the mathematical symbol "ℚ" (Q) which denotes the set of all
+ * rational numbers.
+ *
+ * @{
+ */
+
+/**
+ * Rational number (pair of numerator and denominator).
+ */
+typedef struct AVRational{
+ int num; ///< Numerator
+ int den; ///< Denominator
+} AVRational;
+
+/**
+ * Create an AVRational.
+ *
+ * Useful for compilers that do not support compound literals.
+ *
+ * @note The return value is not reduced.
+ * @see av_reduce()
+ */
+static inline AVRational av_make_q(int num, int den)
+{
+ AVRational r = { num, den };
+ return r;
+}
+
+/**
+ * Compare two rationals.
+ *
+ * @param a First rational
+ * @param b Second rational
+ *
+ * @return One of the following values:
+ * - 0 if `a == b`
+ * - 1 if `a > b`
+ * - -1 if `a < b`
+ * - `INT_MIN` if one of the values is of the form `0 / 0`
+ */
+static inline int av_cmp_q(AVRational a, AVRational b){
+ const int64_t tmp= a.num * (int64_t)b.den - b.num * (int64_t)a.den;
+
+ if(tmp) return (int)((tmp ^ a.den ^ b.den)>>63)|1;
+ else if(b.den && a.den) return 0;
+ else if(a.num && b.num) return (a.num>>31) - (b.num>>31);
+ else return INT_MIN;
+}
+
+/**
+ * Convert an AVRational to a `double`.
+ * @param a AVRational to convert
+ * @return `a` in floating-point form
+ * @see av_d2q()
+ */
+static inline double av_q2d(AVRational a){
+ return a.num / (double) a.den;
+}
+
+/**
+ * Reduce a fraction.
+ *
+ * This is useful for framerate calculations.
+ *
+ * @param[out] dst_num Destination numerator
+ * @param[out] dst_den Destination denominator
+ * @param[in] num Source numerator
+ * @param[in] den Source denominator
+ * @param[in] max Maximum allowed values for `dst_num` & `dst_den`
+ * @return 1 if the operation is exact, 0 otherwise
+ */
+int av_reduce(int *dst_num, int *dst_den, int64_t num, int64_t den, int64_t max);
+
+/**
+ * Multiply two rationals.
+ * @param b First rational
+ * @param c Second rational
+ * @return b*c
+ */
+AVRational av_mul_q(AVRational b, AVRational c) av_const;
+
+/**
+ * Divide one rational by another.
+ * @param b First rational
+ * @param c Second rational
+ * @return b/c
+ */
+AVRational av_div_q(AVRational b, AVRational c) av_const;
+
+/**
+ * Add two rationals.
+ * @param b First rational
+ * @param c Second rational
+ * @return b+c
+ */
+AVRational av_add_q(AVRational b, AVRational c) av_const;
+
+/**
+ * Subtract one rational from another.
+ * @param b First rational
+ * @param c Second rational
+ * @return b-c
+ */
+AVRational av_sub_q(AVRational b, AVRational c) av_const;
+
+/**
+ * Invert a rational.
+ * @param q value
+ * @return 1 / q
+ */
+static av_always_inline AVRational av_inv_q(AVRational q)
+{
+ AVRational r = { q.den, q.num };
+ return r;
+}
+
+/**
+ * Convert a double precision floating point number to a rational.
+ *
+ * In case of infinity, the returned value is expressed as `{1, 0}` or
+ * `{-1, 0}` depending on the sign.
+ *
+ * @param d `double` to convert
+ * @param max Maximum allowed numerator and denominator
+ * @return `d` in AVRational form
+ * @see av_q2d()
+ */
+AVRational av_d2q(double d, int max) av_const;
+
+/**
+ * Find which of the two rationals is closer to another rational.
+ *
+ * @param q Rational to be compared against
+ * @param q1,q2 Rationals to be tested
+ * @return One of the following values:
+ * - 1 if `q1` is nearer to `q` than `q2`
+ * - -1 if `q2` is nearer to `q` than `q1`
+ * - 0 if they have the same distance
+ */
+int av_nearer_q(AVRational q, AVRational q1, AVRational q2);
+
+/**
+ * Find the value in a list of rationals nearest a given reference rational.
+ *
+ * @param q Reference rational
+ * @param q_list Array of rationals terminated by `{0, 0}`
+ * @return Index of the nearest value found in the array
+ */
+int av_find_nearest_q_idx(AVRational q, const AVRational* q_list);
+
+/**
+ * Convert an AVRational to a IEEE 32-bit `float` expressed in fixed-point
+ * format.
+ *
+ * @param q Rational to be converted
+ * @return Equivalent floating-point value, expressed as an unsigned 32-bit
+ * integer.
+ * @note The returned value is platform-indepedant.
+ */
+uint32_t av_q2intfloat(AVRational q);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_RATIONAL_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/samplefmt.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/samplefmt.h
new file mode 100644
index 0000000000..8cd43ae856
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/samplefmt.h
@@ -0,0 +1,272 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_SAMPLEFMT_H
+#define AVUTIL_SAMPLEFMT_H
+
+#include <stdint.h>
+
+#include "avutil.h"
+#include "attributes.h"
+
+/**
+ * @addtogroup lavu_audio
+ * @{
+ *
+ * @defgroup lavu_sampfmts Audio sample formats
+ *
+ * Audio sample format enumeration and related convenience functions.
+ * @{
+ */
+
+/**
+ * Audio sample formats
+ *
+ * - The data described by the sample format is always in native-endian order.
+ * Sample values can be expressed by native C types, hence the lack of a signed
+ * 24-bit sample format even though it is a common raw audio data format.
+ *
+ * - The floating-point formats are based on full volume being in the range
+ * [-1.0, 1.0]. Any values outside this range are beyond full volume level.
+ *
+ * - The data layout as used in av_samples_fill_arrays() and elsewhere in FFmpeg
+ * (such as AVFrame in libavcodec) is as follows:
+ *
+ * @par
+ * For planar sample formats, each audio channel is in a separate data plane,
+ * and linesize is the buffer size, in bytes, for a single plane. All data
+ * planes must be the same size. For packed sample formats, only the first data
+ * plane is used, and samples for each channel are interleaved. In this case,
+ * linesize is the buffer size, in bytes, for the 1 plane.
+ *
+ */
+enum AVSampleFormat {
+ AV_SAMPLE_FMT_NONE = -1,
+ AV_SAMPLE_FMT_U8, ///< unsigned 8 bits
+ AV_SAMPLE_FMT_S16, ///< signed 16 bits
+ AV_SAMPLE_FMT_S32, ///< signed 32 bits
+ AV_SAMPLE_FMT_FLT, ///< float
+ AV_SAMPLE_FMT_DBL, ///< double
+
+ AV_SAMPLE_FMT_U8P, ///< unsigned 8 bits, planar
+ AV_SAMPLE_FMT_S16P, ///< signed 16 bits, planar
+ AV_SAMPLE_FMT_S32P, ///< signed 32 bits, planar
+ AV_SAMPLE_FMT_FLTP, ///< float, planar
+ AV_SAMPLE_FMT_DBLP, ///< double, planar
+ AV_SAMPLE_FMT_S64, ///< signed 64 bits
+ AV_SAMPLE_FMT_S64P, ///< signed 64 bits, planar
+
+ AV_SAMPLE_FMT_NB ///< Number of sample formats. DO NOT USE if linking dynamically
+};
+
+/**
+ * Return the name of sample_fmt, or NULL if sample_fmt is not
+ * recognized.
+ */
+const char *av_get_sample_fmt_name(enum AVSampleFormat sample_fmt);
+
+/**
+ * Return a sample format corresponding to name, or AV_SAMPLE_FMT_NONE
+ * on error.
+ */
+enum AVSampleFormat av_get_sample_fmt(const char *name);
+
+/**
+ * Return the planar<->packed alternative form of the given sample format, or
+ * AV_SAMPLE_FMT_NONE on error. If the passed sample_fmt is already in the
+ * requested planar/packed format, the format returned is the same as the
+ * input.
+ */
+enum AVSampleFormat av_get_alt_sample_fmt(enum AVSampleFormat sample_fmt, int planar);
+
+/**
+ * Get the packed alternative form of the given sample format.
+ *
+ * If the passed sample_fmt is already in packed format, the format returned is
+ * the same as the input.
+ *
+ * @return the packed alternative form of the given sample format or
+ AV_SAMPLE_FMT_NONE on error.
+ */
+enum AVSampleFormat av_get_packed_sample_fmt(enum AVSampleFormat sample_fmt);
+
+/**
+ * Get the planar alternative form of the given sample format.
+ *
+ * If the passed sample_fmt is already in planar format, the format returned is
+ * the same as the input.
+ *
+ * @return the planar alternative form of the given sample format or
+ AV_SAMPLE_FMT_NONE on error.
+ */
+enum AVSampleFormat av_get_planar_sample_fmt(enum AVSampleFormat sample_fmt);
+
+/**
+ * Generate a string corresponding to the sample format with
+ * sample_fmt, or a header if sample_fmt is negative.
+ *
+ * @param buf the buffer where to write the string
+ * @param buf_size the size of buf
+ * @param sample_fmt the number of the sample format to print the
+ * corresponding info string, or a negative value to print the
+ * corresponding header.
+ * @return the pointer to the filled buffer or NULL if sample_fmt is
+ * unknown or in case of other errors
+ */
+char *av_get_sample_fmt_string(char *buf, int buf_size, enum AVSampleFormat sample_fmt);
+
+/**
+ * Return number of bytes per sample.
+ *
+ * @param sample_fmt the sample format
+ * @return number of bytes per sample or zero if unknown for the given
+ * sample format
+ */
+int av_get_bytes_per_sample(enum AVSampleFormat sample_fmt);
+
+/**
+ * Check if the sample format is planar.
+ *
+ * @param sample_fmt the sample format to inspect
+ * @return 1 if the sample format is planar, 0 if it is interleaved
+ */
+int av_sample_fmt_is_planar(enum AVSampleFormat sample_fmt);
+
+/**
+ * Get the required buffer size for the given audio parameters.
+ *
+ * @param[out] linesize calculated linesize, may be NULL
+ * @param nb_channels the number of channels
+ * @param nb_samples the number of samples in a single channel
+ * @param sample_fmt the sample format
+ * @param align buffer size alignment (0 = default, 1 = no alignment)
+ * @return required buffer size, or negative error code on failure
+ */
+int av_samples_get_buffer_size(int *linesize, int nb_channels, int nb_samples,
+ enum AVSampleFormat sample_fmt, int align);
+
+/**
+ * @}
+ *
+ * @defgroup lavu_sampmanip Samples manipulation
+ *
+ * Functions that manipulate audio samples
+ * @{
+ */
+
+/**
+ * Fill plane data pointers and linesize for samples with sample
+ * format sample_fmt.
+ *
+ * The audio_data array is filled with the pointers to the samples data planes:
+ * for planar, set the start point of each channel's data within the buffer,
+ * for packed, set the start point of the entire buffer only.
+ *
+ * The value pointed to by linesize is set to the aligned size of each
+ * channel's data buffer for planar layout, or to the aligned size of the
+ * buffer for all channels for packed layout.
+ *
+ * The buffer in buf must be big enough to contain all the samples
+ * (use av_samples_get_buffer_size() to compute its minimum size),
+ * otherwise the audio_data pointers will point to invalid data.
+ *
+ * @see enum AVSampleFormat
+ * The documentation for AVSampleFormat describes the data layout.
+ *
+ * @param[out] audio_data array to be filled with the pointer for each channel
+ * @param[out] linesize calculated linesize, may be NULL
+ * @param buf the pointer to a buffer containing the samples
+ * @param nb_channels the number of channels
+ * @param nb_samples the number of samples in a single channel
+ * @param sample_fmt the sample format
+ * @param align buffer size alignment (0 = default, 1 = no alignment)
+ * @return >=0 on success or a negative error code on failure
+ * @todo return minimum size in bytes required for the buffer in case
+ * of success at the next bump
+ */
+int av_samples_fill_arrays(uint8_t **audio_data, int *linesize,
+ const uint8_t *buf,
+ int nb_channels, int nb_samples,
+ enum AVSampleFormat sample_fmt, int align);
+
+/**
+ * Allocate a samples buffer for nb_samples samples, and fill data pointers and
+ * linesize accordingly.
+ * The allocated samples buffer can be freed by using av_freep(&audio_data[0])
+ * Allocated data will be initialized to silence.
+ *
+ * @see enum AVSampleFormat
+ * The documentation for AVSampleFormat describes the data layout.
+ *
+ * @param[out] audio_data array to be filled with the pointer for each channel
+ * @param[out] linesize aligned size for audio buffer(s), may be NULL
+ * @param nb_channels number of audio channels
+ * @param nb_samples number of samples per channel
+ * @param align buffer size alignment (0 = default, 1 = no alignment)
+ * @return >=0 on success or a negative error code on failure
+ * @todo return the size of the allocated buffer in case of success at the next bump
+ * @see av_samples_fill_arrays()
+ * @see av_samples_alloc_array_and_samples()
+ */
+int av_samples_alloc(uint8_t **audio_data, int *linesize, int nb_channels,
+ int nb_samples, enum AVSampleFormat sample_fmt, int align);
+
+/**
+ * Allocate a data pointers array, samples buffer for nb_samples
+ * samples, and fill data pointers and linesize accordingly.
+ *
+ * This is the same as av_samples_alloc(), but also allocates the data
+ * pointers array.
+ *
+ * @see av_samples_alloc()
+ */
+int av_samples_alloc_array_and_samples(uint8_t ***audio_data, int *linesize, int nb_channels,
+ int nb_samples, enum AVSampleFormat sample_fmt, int align);
+
+/**
+ * Copy samples from src to dst.
+ *
+ * @param dst destination array of pointers to data planes
+ * @param src source array of pointers to data planes
+ * @param dst_offset offset in samples at which the data will be written to dst
+ * @param src_offset offset in samples at which the data will be read from src
+ * @param nb_samples number of samples to be copied
+ * @param nb_channels number of audio channels
+ * @param sample_fmt audio sample format
+ */
+int av_samples_copy(uint8_t **dst, uint8_t * const *src, int dst_offset,
+ int src_offset, int nb_samples, int nb_channels,
+ enum AVSampleFormat sample_fmt);
+
+/**
+ * Fill an audio buffer with silence.
+ *
+ * @param audio_data array of pointers to data planes
+ * @param offset offset in samples at which to start filling
+ * @param nb_samples number of samples to fill
+ * @param nb_channels number of audio channels
+ * @param sample_fmt audio sample format
+ */
+int av_samples_set_silence(uint8_t **audio_data, int offset, int nb_samples,
+ int nb_channels, enum AVSampleFormat sample_fmt);
+
+/**
+ * @}
+ * @}
+ */
+#endif /* AVUTIL_SAMPLEFMT_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/version.h b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/version.h
new file mode 100644
index 0000000000..3a63e6355f
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/include/libavutil/version.h
@@ -0,0 +1,139 @@
+/*
+ * copyright (c) 2003 Fabrice Bellard
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * @ingroup lavu
+ * Libavutil version macros
+ */
+
+#ifndef AVUTIL_VERSION_H
+#define AVUTIL_VERSION_H
+
+#include "macros.h"
+
+/**
+ * @addtogroup version_utils
+ *
+ * Useful to check and match library version in order to maintain
+ * backward compatibility.
+ *
+ * The FFmpeg libraries follow a versioning sheme very similar to
+ * Semantic Versioning (http://semver.org/)
+ * The difference is that the component called PATCH is called MICRO in FFmpeg
+ * and its value is reset to 100 instead of 0 to keep it above or equal to 100.
+ * Also we do not increase MICRO for every bugfix or change in git master.
+ *
+ * Prior to FFmpeg 3.2 point releases did not change any lib version number to
+ * avoid aliassing different git master checkouts.
+ * Starting with FFmpeg 3.2, the released library versions will occupy
+ * a separate MAJOR.MINOR that is not used on the master development branch.
+ * That is if we branch a release of master 55.10.123 we will bump to 55.11.100
+ * for the release and master will continue at 55.12.100 after it. Each new
+ * point release will then bump the MICRO improving the usefulness of the lib
+ * versions.
+ *
+ * @{
+ */
+
+#define AV_VERSION_INT(a, b, c) ((a)<<16 | (b)<<8 | (c))
+#define AV_VERSION_DOT(a, b, c) a ##.## b ##.## c
+#define AV_VERSION(a, b, c) AV_VERSION_DOT(a, b, c)
+
+/**
+ * Extract version components from the full ::AV_VERSION_INT int as returned
+ * by functions like ::avformat_version() and ::avcodec_version()
+ */
+#define AV_VERSION_MAJOR(a) ((a) >> 16)
+#define AV_VERSION_MINOR(a) (((a) & 0x00FF00) >> 8)
+#define AV_VERSION_MICRO(a) ((a) & 0xFF)
+
+/**
+ * @}
+ */
+
+/**
+ * @defgroup lavu_ver Version and Build diagnostics
+ *
+ * Macros and function useful to check at compiletime and at runtime
+ * which version of libavutil is in use.
+ *
+ * @{
+ */
+
+#define LIBAVUTIL_VERSION_MAJOR 56
+#define LIBAVUTIL_VERSION_MINOR 14
+#define LIBAVUTIL_VERSION_MICRO 100
+
+#define LIBAVUTIL_VERSION_INT AV_VERSION_INT(LIBAVUTIL_VERSION_MAJOR, \
+ LIBAVUTIL_VERSION_MINOR, \
+ LIBAVUTIL_VERSION_MICRO)
+#define LIBAVUTIL_VERSION AV_VERSION(LIBAVUTIL_VERSION_MAJOR, \
+ LIBAVUTIL_VERSION_MINOR, \
+ LIBAVUTIL_VERSION_MICRO)
+#define LIBAVUTIL_BUILD LIBAVUTIL_VERSION_INT
+
+#define LIBAVUTIL_IDENT "Lavu" AV_STRINGIFY(LIBAVUTIL_VERSION)
+
+/**
+ * @defgroup lavu_depr_guards Deprecation Guards
+ * FF_API_* defines may be placed below to indicate public API that will be
+ * dropped at a future version bump. The defines themselves are not part of
+ * the public API and may change, break or disappear at any time.
+ *
+ * @note, when bumping the major version it is recommended to manually
+ * disable each FF_API_* in its own commit instead of disabling them all
+ * at once through the bump. This improves the git bisect-ability of the change.
+ *
+ * @{
+ */
+
+#ifndef FF_API_VAAPI
+#define FF_API_VAAPI (LIBAVUTIL_VERSION_MAJOR < 57)
+#endif
+#ifndef FF_API_FRAME_QP
+#define FF_API_FRAME_QP (LIBAVUTIL_VERSION_MAJOR < 57)
+#endif
+#ifndef FF_API_PLUS1_MINUS1
+#define FF_API_PLUS1_MINUS1 (LIBAVUTIL_VERSION_MAJOR < 57)
+#endif
+#ifndef FF_API_ERROR_FRAME
+#define FF_API_ERROR_FRAME (LIBAVUTIL_VERSION_MAJOR < 57)
+#endif
+#ifndef FF_API_PKT_PTS
+#define FF_API_PKT_PTS (LIBAVUTIL_VERSION_MAJOR < 57)
+#endif
+#ifndef FF_API_CRYPTO_SIZE_T
+#define FF_API_CRYPTO_SIZE_T (LIBAVUTIL_VERSION_MAJOR < 57)
+#endif
+#ifndef FF_API_FRAME_GET_SET
+#define FF_API_FRAME_GET_SET (LIBAVUTIL_VERSION_MAJOR < 57)
+#endif
+#ifndef FF_API_PSEUDOPAL
+#define FF_API_PSEUDOPAL (LIBAVUTIL_VERSION_MAJOR < 57)
+#endif
+
+
+/**
+ * @}
+ * @}
+ */
+
+#endif /* AVUTIL_VERSION_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg58/moz.build b/dom/media/platforms/ffmpeg/ffmpeg58/moz.build
new file mode 100644
index 0000000000..df0d2d1599
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg58/moz.build
@@ -0,0 +1,39 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES += [
+ '../FFmpegAudioDecoder.cpp',
+ '../FFmpegDataDecoder.cpp',
+ '../FFmpegDecoderModule.cpp',
+ '../FFmpegVideoDecoder.cpp',
+]
+LOCAL_INCLUDES += [
+ '..',
+ '/media/mozva',
+ 'include',
+]
+
+if CONFIG['CC_TYPE'] in ('clang', 'gcc'):
+ CXXFLAGS += [ '-Wno-deprecated-declarations' ]
+if CONFIG['CC_TYPE'] == 'clang':
+ CXXFLAGS += [
+ '-Wno-unknown-attributes',
+ ]
+if CONFIG['CC_TYPE'] == 'gcc':
+ CXXFLAGS += [
+ '-Wno-attributes',
+ ]
+if CONFIG['MOZ_WAYLAND']:
+ CXXFLAGS += CONFIG['MOZ_GTK3_CFLAGS']
+if CONFIG['MOZ_ENABLE_VAAPI'] or CONFIG['MOZ_ENABLE_V4L2']:
+ UNIFIED_SOURCES += ['../FFmpegVideoFramePool.cpp']
+ LOCAL_INCLUDES += ['/third_party/drm/drm/include/libdrm/']
+ USE_LIBS += ['mozva']
+ DEFINES['MOZ_WAYLAND_USE_HWDECODE'] = 1
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = 'xul'
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/COPYING.LGPLv2.1 b/dom/media/platforms/ffmpeg/ffmpeg59/include/COPYING.LGPLv2.1
new file mode 100644
index 0000000000..00b4fedfe7
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/COPYING.LGPLv2.1
@@ -0,0 +1,504 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the library's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+ <signature of Ty Coon>, 1 April 1990
+ Ty Coon, President of Vice
+
+That's all there is to it!
+
+
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/avcodec.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/avcodec.h
new file mode 100644
index 0000000000..e25f95c386
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/avcodec.h
@@ -0,0 +1,3204 @@
+/*
+ * copyright (c) 2001 Fabrice Bellard
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_AVCODEC_H
+#define AVCODEC_AVCODEC_H
+
+/**
+ * @file
+ * @ingroup libavc
+ * Libavcodec external API header
+ */
+
+#include "libavutil/samplefmt.h"
+#include "libavutil/attributes.h"
+#include "libavutil/avutil.h"
+#include "libavutil/buffer.h"
+#include "libavutil/dict.h"
+#include "libavutil/frame.h"
+#include "libavutil/log.h"
+#include "libavutil/pixfmt.h"
+#include "libavutil/rational.h"
+
+#include "codec.h"
+#include "codec_desc.h"
+#include "codec_par.h"
+#include "codec_id.h"
+#include "defs.h"
+#include "packet.h"
+#include "version.h"
+
+/**
+ * @defgroup libavc libavcodec
+ * Encoding/Decoding Library
+ *
+ * @{
+ *
+ * @defgroup lavc_decoding Decoding
+ * @{
+ * @}
+ *
+ * @defgroup lavc_encoding Encoding
+ * @{
+ * @}
+ *
+ * @defgroup lavc_codec Codecs
+ * @{
+ * @defgroup lavc_codec_native Native Codecs
+ * @{
+ * @}
+ * @defgroup lavc_codec_wrappers External library wrappers
+ * @{
+ * @}
+ * @defgroup lavc_codec_hwaccel Hardware Accelerators bridge
+ * @{
+ * @}
+ * @}
+ * @defgroup lavc_internal Internal
+ * @{
+ * @}
+ * @}
+ */
+
+/**
+ * @ingroup libavc
+ * @defgroup lavc_encdec send/receive encoding and decoding API overview
+ * @{
+ *
+ * The avcodec_send_packet()/avcodec_receive_frame()/avcodec_send_frame()/
+ * avcodec_receive_packet() functions provide an encode/decode API, which
+ * decouples input and output.
+ *
+ * The API is very similar for encoding/decoding and audio/video, and works as
+ * follows:
+ * - Set up and open the AVCodecContext as usual.
+ * - Send valid input:
+ * - For decoding, call avcodec_send_packet() to give the decoder raw
+ * compressed data in an AVPacket.
+ * - For encoding, call avcodec_send_frame() to give the encoder an AVFrame
+ * containing uncompressed audio or video.
+ *
+ * In both cases, it is recommended that AVPackets and AVFrames are
+ * refcounted, or libavcodec might have to copy the input data. (libavformat
+ * always returns refcounted AVPackets, and av_frame_get_buffer() allocates
+ * refcounted AVFrames.)
+ * - Receive output in a loop. Periodically call one of the avcodec_receive_*()
+ * functions and process their output:
+ * - For decoding, call avcodec_receive_frame(). On success, it will return
+ * an AVFrame containing uncompressed audio or video data.
+ * - For encoding, call avcodec_receive_packet(). On success, it will return
+ * an AVPacket with a compressed frame.
+ *
+ * Repeat this call until it returns AVERROR(EAGAIN) or an error. The
+ * AVERROR(EAGAIN) return value means that new input data is required to
+ * return new output. In this case, continue with sending input. For each
+ * input frame/packet, the codec will typically return 1 output frame/packet,
+ * but it can also be 0 or more than 1.
+ *
+ * At the beginning of decoding or encoding, the codec might accept multiple
+ * input frames/packets without returning a frame, until its internal buffers
+ * are filled. This situation is handled transparently if you follow the steps
+ * outlined above.
+ *
+ * In theory, sending input can result in EAGAIN - this should happen only if
+ * not all output was received. You can use this to structure alternative decode
+ * or encode loops other than the one suggested above. For example, you could
+ * try sending new input on each iteration, and try to receive output if that
+ * returns EAGAIN.
+ *
+ * End of stream situations. These require "flushing" (aka draining) the codec,
+ * as the codec might buffer multiple frames or packets internally for
+ * performance or out of necessity (consider B-frames).
+ * This is handled as follows:
+ * - Instead of valid input, send NULL to the avcodec_send_packet() (decoding)
+ * or avcodec_send_frame() (encoding) functions. This will enter draining
+ * mode.
+ * - Call avcodec_receive_frame() (decoding) or avcodec_receive_packet()
+ * (encoding) in a loop until AVERROR_EOF is returned. The functions will
+ * not return AVERROR(EAGAIN), unless you forgot to enter draining mode.
+ * - Before decoding can be resumed again, the codec has to be reset with
+ * avcodec_flush_buffers().
+ *
+ * Using the API as outlined above is highly recommended. But it is also
+ * possible to call functions outside of this rigid schema. For example, you can
+ * call avcodec_send_packet() repeatedly without calling
+ * avcodec_receive_frame(). In this case, avcodec_send_packet() will succeed
+ * until the codec's internal buffer has been filled up (which is typically of
+ * size 1 per output frame, after initial input), and then reject input with
+ * AVERROR(EAGAIN). Once it starts rejecting input, you have no choice but to
+ * read at least some output.
+ *
+ * Not all codecs will follow a rigid and predictable dataflow; the only
+ * guarantee is that an AVERROR(EAGAIN) return value on a send/receive call on
+ * one end implies that a receive/send call on the other end will succeed, or
+ * at least will not fail with AVERROR(EAGAIN). In general, no codec will
+ * permit unlimited buffering of input or output.
+ *
+ * A codec is not allowed to return AVERROR(EAGAIN) for both sending and
+ * receiving. This would be an invalid state, which could put the codec user
+ * into an endless loop. The API has no concept of time either: it cannot happen
+ * that trying to do avcodec_send_packet() results in AVERROR(EAGAIN), but a
+ * repeated call 1 second later accepts the packet (with no other receive/flush
+ * API calls involved). The API is a strict state machine, and the passage of
+ * time is not supposed to influence it. Some timing-dependent behavior might
+ * still be deemed acceptable in certain cases. But it must never result in both
+ * send/receive returning EAGAIN at the same time at any point. It must also
+ * absolutely be avoided that the current state is "unstable" and can
+ * "flip-flop" between the send/receive APIs allowing progress. For example,
+ * it's not allowed that the codec randomly decides that it actually wants to
+ * consume a packet now instead of returning a frame, after it just returned
+ * AVERROR(EAGAIN) on an avcodec_send_packet() call.
+ * @}
+ */
+
+/**
+ * @defgroup lavc_core Core functions/structures.
+ * @ingroup libavc
+ *
+ * Basic definitions, functions for querying libavcodec capabilities,
+ * allocating core structures, etc.
+ * @{
+ */
+
+/**
+ * @ingroup lavc_encoding
+ * minimum encoding buffer size
+ * Used to avoid some checks during header writing.
+ */
+#define AV_INPUT_BUFFER_MIN_SIZE 16384
+
+/**
+ * @ingroup lavc_encoding
+ */
+typedef struct RcOverride {
+ int start_frame;
+ int end_frame;
+ int qscale; // If this is 0 then quality_factor will be used instead.
+ float quality_factor;
+} RcOverride;
+
+/* encoding support
+ These flags can be passed in AVCodecContext.flags before initialization.
+ Note: Not everything is supported yet.
+*/
+
+/**
+ * Allow decoders to produce frames with data planes that are not aligned
+ * to CPU requirements (e.g. due to cropping).
+ */
+#define AV_CODEC_FLAG_UNALIGNED (1 << 0)
+/**
+ * Use fixed qscale.
+ */
+#define AV_CODEC_FLAG_QSCALE (1 << 1)
+/**
+ * 4 MV per MB allowed / advanced prediction for H.263.
+ */
+#define AV_CODEC_FLAG_4MV (1 << 2)
+/**
+ * Output even those frames that might be corrupted.
+ */
+#define AV_CODEC_FLAG_OUTPUT_CORRUPT (1 << 3)
+/**
+ * Use qpel MC.
+ */
+#define AV_CODEC_FLAG_QPEL (1 << 4)
+/**
+ * Don't output frames whose parameters differ from first
+ * decoded frame in stream.
+ */
+#define AV_CODEC_FLAG_DROPCHANGED (1 << 5)
+/**
+ * Use internal 2pass ratecontrol in first pass mode.
+ */
+#define AV_CODEC_FLAG_PASS1 (1 << 9)
+/**
+ * Use internal 2pass ratecontrol in second pass mode.
+ */
+#define AV_CODEC_FLAG_PASS2 (1 << 10)
+/**
+ * loop filter.
+ */
+#define AV_CODEC_FLAG_LOOP_FILTER (1 << 11)
+/**
+ * Only decode/encode grayscale.
+ */
+#define AV_CODEC_FLAG_GRAY (1 << 13)
+/**
+ * error[?] variables will be set during encoding.
+ */
+#define AV_CODEC_FLAG_PSNR (1 << 15)
+#if FF_API_FLAG_TRUNCATED
+/**
+ * Input bitstream might be truncated at a random location
+ * instead of only at frame boundaries.
+ *
+ * @deprecated use codec parsers for packetizing input
+ */
+# define AV_CODEC_FLAG_TRUNCATED (1 << 16)
+#endif
+/**
+ * Use interlaced DCT.
+ */
+#define AV_CODEC_FLAG_INTERLACED_DCT (1 << 18)
+/**
+ * Force low delay.
+ */
+#define AV_CODEC_FLAG_LOW_DELAY (1 << 19)
+/**
+ * Place global headers in extradata instead of every keyframe.
+ */
+#define AV_CODEC_FLAG_GLOBAL_HEADER (1 << 22)
+/**
+ * Use only bitexact stuff (except (I)DCT).
+ */
+#define AV_CODEC_FLAG_BITEXACT (1 << 23)
+/* Fx : Flag for H.263+ extra options */
+/**
+ * H.263 advanced intra coding / MPEG-4 AC prediction
+ */
+#define AV_CODEC_FLAG_AC_PRED (1 << 24)
+/**
+ * interlaced motion estimation
+ */
+#define AV_CODEC_FLAG_INTERLACED_ME (1 << 29)
+#define AV_CODEC_FLAG_CLOSED_GOP (1U << 31)
+
+/**
+ * Allow non spec compliant speedup tricks.
+ */
+#define AV_CODEC_FLAG2_FAST (1 << 0)
+/**
+ * Skip bitstream encoding.
+ */
+#define AV_CODEC_FLAG2_NO_OUTPUT (1 << 2)
+/**
+ * Place global headers at every keyframe instead of in extradata.
+ */
+#define AV_CODEC_FLAG2_LOCAL_HEADER (1 << 3)
+
+/**
+ * timecode is in drop frame format. DEPRECATED!!!!
+ */
+#define AV_CODEC_FLAG2_DROP_FRAME_TIMECODE (1 << 13)
+
+/**
+ * Input bitstream might be truncated at a packet boundaries
+ * instead of only at frame boundaries.
+ */
+#define AV_CODEC_FLAG2_CHUNKS (1 << 15)
+/**
+ * Discard cropping information from SPS.
+ */
+#define AV_CODEC_FLAG2_IGNORE_CROP (1 << 16)
+
+/**
+ * Show all frames before the first keyframe
+ */
+#define AV_CODEC_FLAG2_SHOW_ALL (1 << 22)
+/**
+ * Export motion vectors through frame side data
+ */
+#define AV_CODEC_FLAG2_EXPORT_MVS (1 << 28)
+/**
+ * Do not skip samples and export skip information as frame side data
+ */
+#define AV_CODEC_FLAG2_SKIP_MANUAL (1 << 29)
+/**
+ * Do not reset ASS ReadOrder field on flush (subtitles decoding)
+ */
+#define AV_CODEC_FLAG2_RO_FLUSH_NOOP (1 << 30)
+
+/* Unsupported options :
+ * Syntax Arithmetic coding (SAC)
+ * Reference Picture Selection
+ * Independent Segment Decoding */
+/* /Fx */
+/* codec capabilities */
+
+/* Exported side data.
+ These flags can be passed in AVCodecContext.export_side_data before
+ initialization.
+*/
+/**
+ * Export motion vectors through frame side data
+ */
+#define AV_CODEC_EXPORT_DATA_MVS (1 << 0)
+/**
+ * Export encoder Producer Reference Time through packet side data
+ */
+#define AV_CODEC_EXPORT_DATA_PRFT (1 << 1)
+/**
+ * Decoding only.
+ * Export the AVVideoEncParams structure through frame side data.
+ */
+#define AV_CODEC_EXPORT_DATA_VIDEO_ENC_PARAMS (1 << 2)
+/**
+ * Decoding only.
+ * Do not apply film grain, export it instead.
+ */
+#define AV_CODEC_EXPORT_DATA_FILM_GRAIN (1 << 3)
+
+/**
+ * The decoder will keep a reference to the frame and may reuse it later.
+ */
+#define AV_GET_BUFFER_FLAG_REF (1 << 0)
+
+/**
+ * The encoder will keep a reference to the packet and may reuse it later.
+ */
+#define AV_GET_ENCODE_BUFFER_FLAG_REF (1 << 0)
+
+struct AVCodecInternal;
+
+/**
+ * main external API structure.
+ * New fields can be added to the end with minor version bumps.
+ * Removal, reordering and changes to existing fields require a major
+ * version bump.
+ * You can use AVOptions (av_opt* / av_set/get*()) to access these fields from
+ * user applications. The name string for AVOptions options matches the
+ * associated command line parameter name and can be found in
+ * libavcodec/options_table.h The AVOption/command line parameter names differ
+ * in some cases from the C structure field names for historic reasons or
+ * brevity. sizeof(AVCodecContext) must not be used outside libav*.
+ */
+typedef struct AVCodecContext {
+ /**
+ * information on struct for av_log
+ * - set by avcodec_alloc_context3
+ */
+ const AVClass* av_class;
+ int log_level_offset;
+
+ enum AVMediaType codec_type; /* see AVMEDIA_TYPE_xxx */
+ const struct AVCodec* codec;
+ enum AVCodecID codec_id; /* see AV_CODEC_ID_xxx */
+
+ /**
+ * fourcc (LSB first, so "ABCD" -> ('D'<<24) + ('C'<<16) + ('B'<<8) + 'A').
+ * This is used to work around some encoder bugs.
+ * A demuxer should set this to what is stored in the field used to identify
+ * the codec. If there are multiple such fields in a container then the
+ * demuxer should choose the one which maximizes the information about the
+ * used codec. If the codec tag field in a container is larger than 32 bits
+ * then the demuxer should remap the longer ID to 32 bits with a table or
+ * other structure. Alternatively a new extra_codec_tag + size could be added
+ * but for this a clear advantage must be demonstrated first.
+ * - encoding: Set by user, if not then the default based on codec_id will be
+ * used.
+ * - decoding: Set by user, will be converted to uppercase by libavcodec
+ * during init.
+ */
+ unsigned int codec_tag;
+
+ void* priv_data;
+
+ /**
+ * Private context used for internal data.
+ *
+ * Unlike priv_data, this is not codec-specific. It is used in general
+ * libavcodec functions.
+ */
+ struct AVCodecInternal* internal;
+
+ /**
+ * Private data of the user, can be used to carry app specific stuff.
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ void* opaque;
+
+ /**
+ * the average bitrate
+ * - encoding: Set by user; unused for constant quantizer encoding.
+ * - decoding: Set by user, may be overwritten by libavcodec
+ * if this info is available in the stream
+ */
+ int64_t bit_rate;
+
+ /**
+ * number of bits the bitstream is allowed to diverge from the reference.
+ * the reference can be CBR (for CBR pass1) or VBR (for pass2)
+ * - encoding: Set by user; unused for constant quantizer encoding.
+ * - decoding: unused
+ */
+ int bit_rate_tolerance;
+
+ /**
+ * Global quality for codecs which cannot change it per frame.
+ * This should be proportional to MPEG-1/2/4 qscale.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int global_quality;
+
+ /**
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int compression_level;
+#define FF_COMPRESSION_DEFAULT -1
+
+ /**
+ * AV_CODEC_FLAG_*.
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int flags;
+
+ /**
+ * AV_CODEC_FLAG2_*
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int flags2;
+
+ /**
+ * some codecs need / can use extradata like Huffman tables.
+ * MJPEG: Huffman tables
+ * rv10: additional flags
+ * MPEG-4: global headers (they can be in the bitstream or here)
+ * The allocated memory should be AV_INPUT_BUFFER_PADDING_SIZE bytes larger
+ * than extradata_size to avoid problems if it is read with the bitstream
+ * reader. The bytewise contents of extradata must not depend on the
+ * architecture or CPU endianness. Must be allocated with the av_malloc()
+ * family of functions.
+ * - encoding: Set/allocated/freed by libavcodec.
+ * - decoding: Set/allocated/freed by user.
+ */
+ uint8_t* extradata;
+ int extradata_size;
+
+ /**
+ * This is the fundamental unit of time (in seconds) in terms
+ * of which frame timestamps are represented. For fixed-fps content,
+ * timebase should be 1/framerate and timestamp increments should be
+ * identically 1.
+ * This often, but not always is the inverse of the frame rate or field rate
+ * for video. 1/time_base is not the average frame rate if the frame rate is
+ * not constant.
+ *
+ * Like containers, elementary streams also can store timestamps, 1/time_base
+ * is the unit in which these timestamps are specified.
+ * As example of such codec time base see ISO/IEC 14496-2:2001(E)
+ * vop_time_increment_resolution and fixed_vop_rate
+ * (fixed_vop_rate == 0 implies that it is different from the framerate)
+ *
+ * - encoding: MUST be set by user.
+ * - decoding: the use of this field for decoding is deprecated.
+ * Use framerate instead.
+ */
+ AVRational time_base;
+
+ /**
+ * For some codecs, the time base is closer to the field rate than the frame
+ * rate. Most notably, H.264 and MPEG-2 specify time_base as half of frame
+ * duration if no telecine is used ...
+ *
+ * Set to time_base ticks per frame. Default 1, e.g., H.264/MPEG-2 set it
+ * to 2.
+ */
+ int ticks_per_frame;
+
+ /**
+ * Codec delay.
+ *
+ * Encoding: Number of frames delay there will be from the encoder input to
+ * the decoder output. (we assume the decoder matches the spec)
+ * Decoding: Number of frames delay in addition to what a standard decoder
+ * as specified in the spec would produce.
+ *
+ * Video:
+ * Number of frames the decoded output will be delayed relative to the
+ * encoded input.
+ *
+ * Audio:
+ * For encoding, this field is unused (see initial_padding).
+ *
+ * For decoding, this is the number of samples the decoder needs to
+ * output before the decoder's output is valid. When seeking, you should
+ * start decoding this many samples prior to your desired seek point.
+ *
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by libavcodec.
+ */
+ int delay;
+
+ /* video only */
+ /**
+ * picture width / height.
+ *
+ * @note Those fields may not match the values of the last
+ * AVFrame output by avcodec_receive_frame() due frame
+ * reordering.
+ *
+ * - encoding: MUST be set by user.
+ * - decoding: May be set by the user before opening the decoder if known e.g.
+ * from the container. Some decoders will require the dimensions
+ * to be set by the caller. During decoding, the decoder may
+ * overwrite those values as required while parsing the data.
+ */
+ int width, height;
+
+ /**
+ * Bitstream width / height, may be different from width/height e.g. when
+ * the decoded frame is cropped before being output or lowres is enabled.
+ *
+ * @note Those field may not match the value of the last
+ * AVFrame output by avcodec_receive_frame() due frame
+ * reordering.
+ *
+ * - encoding: unused
+ * - decoding: May be set by the user before opening the decoder if known
+ * e.g. from the container. During decoding, the decoder may
+ * overwrite those values as required while parsing the data.
+ */
+ int coded_width, coded_height;
+
+ /**
+ * the number of pictures in a group of pictures, or 0 for intra_only
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int gop_size;
+
+ /**
+ * Pixel format, see AV_PIX_FMT_xxx.
+ * May be set by the demuxer if known from headers.
+ * May be overridden by the decoder if it knows better.
+ *
+ * @note This field may not match the value of the last
+ * AVFrame output by avcodec_receive_frame() due frame
+ * reordering.
+ *
+ * - encoding: Set by user.
+ * - decoding: Set by user if known, overridden by libavcodec while
+ * parsing the data.
+ */
+ enum AVPixelFormat pix_fmt;
+
+ /**
+ * If non NULL, 'draw_horiz_band' is called by the libavcodec
+ * decoder to draw a horizontal band. It improves cache usage. Not
+ * all codecs can do that. You must check the codec capabilities
+ * beforehand.
+ * When multithreading is used, it may be called from multiple threads
+ * at the same time; threads might draw different parts of the same AVFrame,
+ * or multiple AVFrames, and there is no guarantee that slices will be drawn
+ * in order.
+ * The function is also used by hardware acceleration APIs.
+ * It is called at least once during frame decoding to pass
+ * the data needed for hardware render.
+ * In that mode instead of pixel data, AVFrame points to
+ * a structure specific to the acceleration API. The application
+ * reads the structure and can change some fields to indicate progress
+ * or mark state.
+ * - encoding: unused
+ * - decoding: Set by user.
+ * @param height the height of the slice
+ * @param y the y position of the slice
+ * @param type 1->top field, 2->bottom field, 3->frame
+ * @param offset offset into the AVFrame.data from which the slice should be
+ * read
+ */
+ void (*draw_horiz_band)(struct AVCodecContext* s, const AVFrame* src,
+ int offset[AV_NUM_DATA_POINTERS], int y, int type,
+ int height);
+
+ /**
+ * Callback to negotiate the pixel format. Decoding only, may be set by the
+ * caller before avcodec_open2().
+ *
+ * Called by some decoders to select the pixel format that will be used for
+ * the output frames. This is mainly used to set up hardware acceleration,
+ * then the provided format list contains the corresponding hwaccel pixel
+ * formats alongside the "software" one. The software pixel format may also
+ * be retrieved from \ref sw_pix_fmt.
+ *
+ * This callback will be called when the coded frame properties (such as
+ * resolution, pixel format, etc.) change and more than one output format is
+ * supported for those new properties. If a hardware pixel format is chosen
+ * and initialization for it fails, the callback may be called again
+ * immediately.
+ *
+ * This callback may be called from different threads if the decoder is
+ * multi-threaded, but not from more than one thread simultaneously.
+ *
+ * @param fmt list of formats which may be used in the current
+ * configuration, terminated by AV_PIX_FMT_NONE.
+ * @warning Behavior is undefined if the callback returns a value other
+ * than one of the formats in fmt or AV_PIX_FMT_NONE.
+ * @return the chosen format or AV_PIX_FMT_NONE
+ */
+ enum AVPixelFormat (*get_format)(struct AVCodecContext* s,
+ const enum AVPixelFormat* fmt);
+
+ /**
+ * maximum number of B-frames between non-B-frames
+ * Note: The output will be delayed by max_b_frames+1 relative to the input.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int max_b_frames;
+
+ /**
+ * qscale factor between IP and B-frames
+ * If > 0 then the last P-frame quantizer will be used (q=
+ * lastp_q*factor+offset). If < 0 then normal ratecontrol will be done (q=
+ * -normal_q*factor+offset).
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float b_quant_factor;
+
+ /**
+ * qscale offset between IP and B-frames
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float b_quant_offset;
+
+ /**
+ * Size of the frame reordering buffer in the decoder.
+ * For MPEG-2 it is 1 IPB or 0 low delay IP.
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by libavcodec.
+ */
+ int has_b_frames;
+
+ /**
+ * qscale factor between P- and I-frames
+ * If > 0 then the last P-frame quantizer will be used (q = lastp_q * factor +
+ * offset). If < 0 then normal ratecontrol will be done (q=
+ * -normal_q*factor+offset).
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float i_quant_factor;
+
+ /**
+ * qscale offset between P and I-frames
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float i_quant_offset;
+
+ /**
+ * luminance masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float lumi_masking;
+
+ /**
+ * temporary complexity masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float temporal_cplx_masking;
+
+ /**
+ * spatial complexity masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float spatial_cplx_masking;
+
+ /**
+ * p block masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float p_masking;
+
+ /**
+ * darkness masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float dark_masking;
+
+ /**
+ * slice count
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by user (or 0).
+ */
+ int slice_count;
+
+ /**
+ * slice offsets in the frame in bytes
+ * - encoding: Set/allocated by libavcodec.
+ * - decoding: Set/allocated by user (or NULL).
+ */
+ int* slice_offset;
+
+ /**
+ * sample aspect ratio (0 if unknown)
+ * That is the width of a pixel divided by the height of the pixel.
+ * Numerator and denominator must be relatively prime and smaller than 256 for
+ * some video standards.
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ AVRational sample_aspect_ratio;
+
+ /**
+ * motion estimation comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_cmp;
+ /**
+ * subpixel motion estimation comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_sub_cmp;
+ /**
+ * macroblock comparison function (not supported yet)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mb_cmp;
+ /**
+ * interlaced DCT comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int ildct_cmp;
+#define FF_CMP_SAD 0
+#define FF_CMP_SSE 1
+#define FF_CMP_SATD 2
+#define FF_CMP_DCT 3
+#define FF_CMP_PSNR 4
+#define FF_CMP_BIT 5
+#define FF_CMP_RD 6
+#define FF_CMP_ZERO 7
+#define FF_CMP_VSAD 8
+#define FF_CMP_VSSE 9
+#define FF_CMP_NSSE 10
+#define FF_CMP_W53 11
+#define FF_CMP_W97 12
+#define FF_CMP_DCTMAX 13
+#define FF_CMP_DCT264 14
+#define FF_CMP_MEDIAN_SAD 15
+#define FF_CMP_CHROMA 256
+
+ /**
+ * ME diamond size & shape
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int dia_size;
+
+ /**
+ * amount of previous MV predictors (2a+1 x 2a+1 square)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int last_predictor_count;
+
+ /**
+ * motion estimation prepass comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_pre_cmp;
+
+ /**
+ * ME prepass diamond size & shape
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int pre_dia_size;
+
+ /**
+ * subpel ME quality
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_subpel_quality;
+
+ /**
+ * maximum motion estimation search range in subpel units
+ * If 0 then no limit.
+ *
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_range;
+
+ /**
+ * slice flags
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int slice_flags;
+#define SLICE_FLAG_CODED_ORDER \
+ 0x0001 ///< draw_horiz_band() is called in coded order instead of display
+#define SLICE_FLAG_ALLOW_FIELD \
+ 0x0002 ///< allow draw_horiz_band() with field slices (MPEG-2 field pics)
+#define SLICE_FLAG_ALLOW_PLANE \
+ 0x0004 ///< allow draw_horiz_band() with 1 component at a time (SVQ1)
+
+ /**
+ * macroblock decision mode
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mb_decision;
+#define FF_MB_DECISION_SIMPLE 0 ///< uses mb_cmp
+#define FF_MB_DECISION_BITS 1 ///< chooses the one which needs the fewest bits
+#define FF_MB_DECISION_RD 2 ///< rate distortion
+
+ /**
+ * custom intra quantization matrix
+ * Must be allocated with the av_malloc() family of functions, and will be
+ * freed in avcodec_free_context().
+ * - encoding: Set/allocated by user, freed by libavcodec. Can be NULL.
+ * - decoding: Set/allocated/freed by libavcodec.
+ */
+ uint16_t* intra_matrix;
+
+ /**
+ * custom inter quantization matrix
+ * Must be allocated with the av_malloc() family of functions, and will be
+ * freed in avcodec_free_context().
+ * - encoding: Set/allocated by user, freed by libavcodec. Can be NULL.
+ * - decoding: Set/allocated/freed by libavcodec.
+ */
+ uint16_t* inter_matrix;
+
+ /**
+ * precision of the intra DC coefficient - 8
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec
+ */
+ int intra_dc_precision;
+
+ /**
+ * Number of macroblock rows at the top which are skipped.
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int skip_top;
+
+ /**
+ * Number of macroblock rows at the bottom which are skipped.
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int skip_bottom;
+
+ /**
+ * minimum MB Lagrange multiplier
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mb_lmin;
+
+ /**
+ * maximum MB Lagrange multiplier
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mb_lmax;
+
+ /**
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int bidir_refine;
+
+ /**
+ * minimum GOP size
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int keyint_min;
+
+ /**
+ * number of reference frames
+ * - encoding: Set by user.
+ * - decoding: Set by lavc.
+ */
+ int refs;
+
+ /**
+ * Note: Value depends upon the compare function used for fullpel ME.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mv0_threshold;
+
+ /**
+ * Chromaticity coordinates of the source primaries.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorPrimaries color_primaries;
+
+ /**
+ * Color Transfer Characteristic.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorTransferCharacteristic color_trc;
+
+ /**
+ * YUV colorspace type.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorSpace colorspace;
+
+ /**
+ * MPEG vs JPEG YUV range.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorRange color_range;
+
+ /**
+ * This defines the location of chroma samples.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVChromaLocation chroma_sample_location;
+
+ /**
+ * Number of slices.
+ * Indicates number of picture subdivisions. Used for parallelized
+ * decoding.
+ * - encoding: Set by user
+ * - decoding: unused
+ */
+ int slices;
+
+ /** Field order
+ * - encoding: set by libavcodec
+ * - decoding: Set by user.
+ */
+ enum AVFieldOrder field_order;
+
+ /* audio only */
+ int sample_rate; ///< samples per second
+ int channels; ///< number of audio channels
+
+ /**
+ * audio sample format
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ enum AVSampleFormat sample_fmt; ///< sample format
+
+ /* The following data should not be initialized. */
+ /**
+ * Number of samples per channel in an audio frame.
+ *
+ * - encoding: set by libavcodec in avcodec_open2(). Each submitted frame
+ * except the last must contain exactly frame_size samples per channel.
+ * May be 0 when the codec has AV_CODEC_CAP_VARIABLE_FRAME_SIZE set, then
+ * the frame size is not restricted.
+ * - decoding: may be set by some decoders to indicate constant frame size
+ */
+ int frame_size;
+
+ /**
+ * Frame counter, set by libavcodec.
+ *
+ * - decoding: total number of frames returned from the decoder so far.
+ * - encoding: total number of frames passed to the encoder so far.
+ *
+ * @note the counter is not incremented if encoding/decoding resulted in
+ * an error.
+ */
+ int frame_number;
+
+ /**
+ * number of bytes per packet if constant and known or 0
+ * Used by some WAV based audio codecs.
+ */
+ int block_align;
+
+ /**
+ * Audio cutoff bandwidth (0 means "automatic")
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int cutoff;
+
+ /**
+ * Audio channel layout.
+ * - encoding: set by user.
+ * - decoding: set by user, may be overwritten by libavcodec.
+ */
+ uint64_t channel_layout;
+
+ /**
+ * Request decoder to use this channel layout if it can (0 for default)
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ uint64_t request_channel_layout;
+
+ /**
+ * Type of service that the audio stream conveys.
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ enum AVAudioServiceType audio_service_type;
+
+ /**
+ * desired sample format
+ * - encoding: Not used.
+ * - decoding: Set by user.
+ * Decoder will decode to this format if it can.
+ */
+ enum AVSampleFormat request_sample_fmt;
+
+ /**
+ * This callback is called at the beginning of each frame to get data
+ * buffer(s) for it. There may be one contiguous buffer for all the data or
+ * there may be a buffer per each data plane or anything in between. What
+ * this means is, you may set however many entries in buf[] you feel
+ * necessary. Each buffer must be reference-counted using the AVBuffer API
+ * (see description of buf[] below).
+ *
+ * The following fields will be set in the frame before this callback is
+ * called:
+ * - format
+ * - width, height (video only)
+ * - sample_rate, channel_layout, nb_samples (audio only)
+ * Their values may differ from the corresponding values in
+ * AVCodecContext. This callback must use the frame values, not the codec
+ * context values, to calculate the required buffer size.
+ *
+ * This callback must fill the following fields in the frame:
+ * - data[]
+ * - linesize[]
+ * - extended_data:
+ * * if the data is planar audio with more than 8 channels, then this
+ * callback must allocate and fill extended_data to contain all pointers
+ * to all data planes. data[] must hold as many pointers as it can.
+ * extended_data must be allocated with av_malloc() and will be freed in
+ * av_frame_unref().
+ * * otherwise extended_data must point to data
+ * - buf[] must contain one or more pointers to AVBufferRef structures. Each
+ * of the frame's data and extended_data pointers must be contained in these.
+ * That is, one AVBufferRef for each allocated chunk of memory, not
+ * necessarily one AVBufferRef per data[] entry. See: av_buffer_create(),
+ * av_buffer_alloc(), and av_buffer_ref().
+ * - extended_buf and nb_extended_buf must be allocated with av_malloc() by
+ * this callback and filled with the extra buffers if there are more
+ * buffers than buf[] can hold. extended_buf will be freed in
+ * av_frame_unref().
+ *
+ * If AV_CODEC_CAP_DR1 is not set then get_buffer2() must call
+ * avcodec_default_get_buffer2() instead of providing buffers allocated by
+ * some other means.
+ *
+ * Each data plane must be aligned to the maximum required by the target
+ * CPU.
+ *
+ * @see avcodec_default_get_buffer2()
+ *
+ * Video:
+ *
+ * If AV_GET_BUFFER_FLAG_REF is set in flags then the frame may be reused
+ * (read and/or written to if it is writable) later by libavcodec.
+ *
+ * avcodec_align_dimensions2() should be used to find the required width and
+ * height, as they normally need to be rounded up to the next multiple of 16.
+ *
+ * Some decoders do not support linesizes changing between frames.
+ *
+ * If frame multithreading is used, this callback may be called from a
+ * different thread, but not from more than one at once. Does not need to be
+ * reentrant.
+ *
+ * @see avcodec_align_dimensions2()
+ *
+ * Audio:
+ *
+ * Decoders request a buffer of a particular size by setting
+ * AVFrame.nb_samples prior to calling get_buffer2(). The decoder may,
+ * however, utilize only part of the buffer by setting AVFrame.nb_samples
+ * to a smaller value in the output frame.
+ *
+ * As a convenience, av_samples_get_buffer_size() and
+ * av_samples_fill_arrays() in libavutil may be used by custom get_buffer2()
+ * functions to find the required data size and to fill data pointers and
+ * linesize. In AVFrame.linesize, only linesize[0] may be set for audio
+ * since all planes must be the same size.
+ *
+ * @see av_samples_get_buffer_size(), av_samples_fill_arrays()
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec, user can override.
+ */
+ int (*get_buffer2)(struct AVCodecContext* s, AVFrame* frame, int flags);
+
+ /* - encoding parameters */
+ float qcompress; ///< amount of qscale change between easy & hard scenes
+ ///< (0.0-1.0)
+ float qblur; ///< amount of qscale smoothing over time (0.0-1.0)
+
+ /**
+ * minimum quantizer
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int qmin;
+
+ /**
+ * maximum quantizer
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int qmax;
+
+ /**
+ * maximum quantizer difference between frames
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int max_qdiff;
+
+ /**
+ * decoder bitstream buffer size
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int rc_buffer_size;
+
+ /**
+ * ratecontrol override, see RcOverride
+ * - encoding: Allocated/set/freed by user.
+ * - decoding: unused
+ */
+ int rc_override_count;
+ RcOverride* rc_override;
+
+ /**
+ * maximum bitrate
+ * - encoding: Set by user.
+ * - decoding: Set by user, may be overwritten by libavcodec.
+ */
+ int64_t rc_max_rate;
+
+ /**
+ * minimum bitrate
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int64_t rc_min_rate;
+
+ /**
+ * Ratecontrol attempt to use, at maximum, <value> of what can be used without
+ * an underflow.
+ * - encoding: Set by user.
+ * - decoding: unused.
+ */
+ float rc_max_available_vbv_use;
+
+ /**
+ * Ratecontrol attempt to use, at least, <value> times the amount needed to
+ * prevent a vbv overflow.
+ * - encoding: Set by user.
+ * - decoding: unused.
+ */
+ float rc_min_vbv_overflow_use;
+
+ /**
+ * Number of bits which should be loaded into the rc buffer before decoding
+ * starts.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int rc_initial_buffer_occupancy;
+
+ /**
+ * trellis RD quantization
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int trellis;
+
+ /**
+ * pass1 encoding statistics output buffer
+ * - encoding: Set by libavcodec.
+ * - decoding: unused
+ */
+ char* stats_out;
+
+ /**
+ * pass2 encoding statistics input buffer
+ * Concatenated stuff from stats_out of pass1 should be placed here.
+ * - encoding: Allocated/set/freed by user.
+ * - decoding: unused
+ */
+ char* stats_in;
+
+ /**
+ * Work around bugs in encoders which sometimes cannot be detected
+ * automatically.
+ * - encoding: Set by user
+ * - decoding: Set by user
+ */
+ int workaround_bugs;
+#define FF_BUG_AUTODETECT 1 ///< autodetection
+#define FF_BUG_XVID_ILACE 4
+#define FF_BUG_UMP4 8
+#define FF_BUG_NO_PADDING 16
+#define FF_BUG_AMV 32
+#define FF_BUG_QPEL_CHROMA 64
+#define FF_BUG_STD_QPEL 128
+#define FF_BUG_QPEL_CHROMA2 256
+#define FF_BUG_DIRECT_BLOCKSIZE 512
+#define FF_BUG_EDGE 1024
+#define FF_BUG_HPEL_CHROMA 2048
+#define FF_BUG_DC_CLIP 4096
+#define FF_BUG_MS \
+ 8192 ///< Work around various bugs in Microsoft's broken decoders.
+#define FF_BUG_TRUNCATED 16384
+#define FF_BUG_IEDGE 32768
+
+ /**
+ * strictly follow the standard (MPEG-4, ...).
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ * Setting this to STRICT or higher means the encoder and decoder will
+ * generally do stupid things, whereas setting it to unofficial or lower
+ * will mean the encoder might produce output that is not supported by all
+ * spec-compliant decoders. Decoders don't differentiate between normal,
+ * unofficial and experimental (that is, they always try to decode things
+ * when they can) unless they are explicitly asked to behave stupidly
+ * (=strictly conform to the specs)
+ */
+ int strict_std_compliance;
+#define FF_COMPLIANCE_VERY_STRICT \
+ 2 ///< Strictly conform to an older more strict version of the spec or
+ ///< reference software.
+#define FF_COMPLIANCE_STRICT \
+ 1 ///< Strictly conform to all the things in the spec no matter what
+ ///< consequences.
+#define FF_COMPLIANCE_NORMAL 0
+#define FF_COMPLIANCE_UNOFFICIAL -1 ///< Allow unofficial extensions
+#define FF_COMPLIANCE_EXPERIMENTAL \
+ -2 ///< Allow nonstandardized experimental things.
+
+ /**
+ * error concealment flags
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int error_concealment;
+#define FF_EC_GUESS_MVS 1
+#define FF_EC_DEBLOCK 2
+#define FF_EC_FAVOR_INTER 256
+
+ /**
+ * debug
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int debug;
+#define FF_DEBUG_PICT_INFO 1
+#define FF_DEBUG_RC 2
+#define FF_DEBUG_BITSTREAM 4
+#define FF_DEBUG_MB_TYPE 8
+#define FF_DEBUG_QP 16
+#define FF_DEBUG_DCT_COEFF 0x00000040
+#define FF_DEBUG_SKIP 0x00000080
+#define FF_DEBUG_STARTCODE 0x00000100
+#define FF_DEBUG_ER 0x00000400
+#define FF_DEBUG_MMCO 0x00000800
+#define FF_DEBUG_BUGS 0x00001000
+#define FF_DEBUG_BUFFERS 0x00008000
+#define FF_DEBUG_THREADS 0x00010000
+#define FF_DEBUG_GREEN_MD 0x00800000
+#define FF_DEBUG_NOMC 0x01000000
+
+ /**
+ * Error recognition; may misdetect some more or less valid parts as errors.
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int err_recognition;
+
+/**
+ * Verify checksums embedded in the bitstream (could be of either encoded or
+ * decoded data, depending on the codec) and print an error message on mismatch.
+ * If AV_EF_EXPLODE is also set, a mismatching checksum will result in the
+ * decoder returning an error.
+ */
+#define AV_EF_CRCCHECK (1 << 0)
+#define AV_EF_BITSTREAM (1 << 1) ///< detect bitstream specification deviations
+#define AV_EF_BUFFER (1 << 2) ///< detect improper bitstream length
+#define AV_EF_EXPLODE (1 << 3) ///< abort decoding on minor error detection
+
+#define AV_EF_IGNORE_ERR (1 << 15) ///< ignore errors and continue
+#define AV_EF_CAREFUL \
+ (1 << 16) ///< consider things that violate the spec, are fast to calculate
+ ///< and have not been seen in the wild as errors
+#define AV_EF_COMPLIANT \
+ (1 << 17) ///< consider all spec non compliances as errors
+#define AV_EF_AGGRESSIVE \
+ (1 << 18) ///< consider things that a sane encoder should not do as an error
+
+ /**
+ * opaque 64-bit number (generally a PTS) that will be reordered and
+ * output in AVFrame.reordered_opaque
+ * - encoding: Set by libavcodec to the reordered_opaque of the input
+ * frame corresponding to the last returned packet. Only
+ * supported by encoders with the
+ * AV_CODEC_CAP_ENCODER_REORDERED_OPAQUE capability.
+ * - decoding: Set by user.
+ */
+ int64_t reordered_opaque;
+
+ /**
+ * Hardware accelerator in use
+ * - encoding: unused.
+ * - decoding: Set by libavcodec
+ */
+ const struct AVHWAccel* hwaccel;
+
+ /**
+ * Hardware accelerator context.
+ * For some hardware accelerators, a global context needs to be
+ * provided by the user. In that case, this holds display-dependent
+ * data FFmpeg cannot instantiate itself. Please refer to the
+ * FFmpeg HW accelerator documentation to know how to fill this.
+ * - encoding: unused
+ * - decoding: Set by user
+ */
+ void* hwaccel_context;
+
+ /**
+ * error
+ * - encoding: Set by libavcodec if flags & AV_CODEC_FLAG_PSNR.
+ * - decoding: unused
+ */
+ uint64_t error[AV_NUM_DATA_POINTERS];
+
+ /**
+ * DCT algorithm, see FF_DCT_* below
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int dct_algo;
+#define FF_DCT_AUTO 0
+#define FF_DCT_FASTINT 1
+#define FF_DCT_INT 2
+#define FF_DCT_MMX 3
+#define FF_DCT_ALTIVEC 5
+#define FF_DCT_FAAN 6
+
+ /**
+ * IDCT algorithm, see FF_IDCT_* below.
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int idct_algo;
+#define FF_IDCT_AUTO 0
+#define FF_IDCT_INT 1
+#define FF_IDCT_SIMPLE 2
+#define FF_IDCT_SIMPLEMMX 3
+#define FF_IDCT_ARM 7
+#define FF_IDCT_ALTIVEC 8
+#define FF_IDCT_SIMPLEARM 10
+#define FF_IDCT_XVID 14
+#define FF_IDCT_SIMPLEARMV5TE 16
+#define FF_IDCT_SIMPLEARMV6 17
+#define FF_IDCT_FAAN 20
+#define FF_IDCT_SIMPLENEON 22
+#define FF_IDCT_NONE \
+ 24 /* Used by XvMC to extract IDCT coefficients with FF_IDCT_PERM_NONE */
+#define FF_IDCT_SIMPLEAUTO 128
+
+ /**
+ * bits per sample/pixel from the demuxer (needed for huffyuv).
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by user.
+ */
+ int bits_per_coded_sample;
+
+ /**
+ * Bits per sample/pixel of internal libavcodec pixel/sample format.
+ * - encoding: set by user.
+ * - decoding: set by libavcodec.
+ */
+ int bits_per_raw_sample;
+
+ /**
+ * low resolution decoding, 1-> 1/2 size, 2->1/4 size
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int lowres;
+
+ /**
+ * thread count
+ * is used to decide how many independent tasks should be passed to execute()
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int thread_count;
+
+ /**
+ * Which multithreading methods to use.
+ * Use of FF_THREAD_FRAME will increase decoding delay by one frame per
+ * thread, so clients which cannot provide future frames should not use it.
+ *
+ * - encoding: Set by user, otherwise the default is used.
+ * - decoding: Set by user, otherwise the default is used.
+ */
+ int thread_type;
+#define FF_THREAD_FRAME 1 ///< Decode more than one frame at once
+#define FF_THREAD_SLICE \
+ 2 ///< Decode more than one part of a single frame at once
+
+ /**
+ * Which multithreading methods are in use by the codec.
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by libavcodec.
+ */
+ int active_thread_type;
+
+#if FF_API_THREAD_SAFE_CALLBACKS
+ /**
+ * Set by the client if its custom get_buffer() callback can be called
+ * synchronously from another thread, which allows faster multithreaded
+ * decoding. draw_horiz_band() will be called from other threads regardless of
+ * this setting. Ignored if the default get_buffer() is used.
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ *
+ * @deprecated the custom get_buffer2() callback should always be
+ * thread-safe. Thread-unsafe get_buffer2() implementations will be
+ * invalid starting with LIBAVCODEC_VERSION_MAJOR=60; in other words,
+ * libavcodec will behave as if this field was always set to 1.
+ * Callers that want to be forward compatible with future libavcodec
+ * versions should wrap access to this field in
+ * #if LIBAVCODEC_VERSION_MAJOR < 60
+ */
+ attribute_deprecated int thread_safe_callbacks;
+#endif
+
+ /**
+ * The codec may call this to execute several independent things.
+ * It will return only after finishing all tasks.
+ * The user may replace this with some multithreaded implementation,
+ * the default implementation will execute the parts serially.
+ * @param count the number of things to execute
+ * - encoding: Set by libavcodec, user can override.
+ * - decoding: Set by libavcodec, user can override.
+ */
+ int (*execute)(struct AVCodecContext* c,
+ int (*func)(struct AVCodecContext* c2, void* arg), void* arg2,
+ int* ret, int count, int size);
+
+ /**
+ * The codec may call this to execute several independent things.
+ * It will return only after finishing all tasks.
+ * The user may replace this with some multithreaded implementation,
+ * the default implementation will execute the parts serially.
+ * Also see avcodec_thread_init and e.g. the --enable-pthread configure
+ * option.
+ * @param c context passed also to func
+ * @param count the number of things to execute
+ * @param arg2 argument passed unchanged to func
+ * @param ret return values of executed functions, must have space for "count"
+ * values. May be NULL.
+ * @param func function that will be called count times, with jobnr from 0 to
+ * count-1. threadnr will be in the range 0 to c->thread_count-1 < MAX_THREADS
+ * and so that no two instances of func executing at the same time will have
+ * the same threadnr.
+ * @return always 0 currently, but code should handle a future improvement
+ * where when any call to func returns < 0 no further calls to func may be
+ * done and < 0 is returned.
+ * - encoding: Set by libavcodec, user can override.
+ * - decoding: Set by libavcodec, user can override.
+ */
+ int (*execute2)(struct AVCodecContext* c,
+ int (*func)(struct AVCodecContext* c2, void* arg, int jobnr,
+ int threadnr),
+ void* arg2, int* ret, int count);
+
+ /**
+ * noise vs. sse weight for the nsse comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int nsse_weight;
+
+ /**
+ * profile
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int profile;
+#define FF_PROFILE_UNKNOWN -99
+#define FF_PROFILE_RESERVED -100
+
+#define FF_PROFILE_AAC_MAIN 0
+#define FF_PROFILE_AAC_LOW 1
+#define FF_PROFILE_AAC_SSR 2
+#define FF_PROFILE_AAC_LTP 3
+#define FF_PROFILE_AAC_HE 4
+#define FF_PROFILE_AAC_HE_V2 28
+#define FF_PROFILE_AAC_LD 22
+#define FF_PROFILE_AAC_ELD 38
+#define FF_PROFILE_MPEG2_AAC_LOW 128
+#define FF_PROFILE_MPEG2_AAC_HE 131
+
+#define FF_PROFILE_DNXHD 0
+#define FF_PROFILE_DNXHR_LB 1
+#define FF_PROFILE_DNXHR_SQ 2
+#define FF_PROFILE_DNXHR_HQ 3
+#define FF_PROFILE_DNXHR_HQX 4
+#define FF_PROFILE_DNXHR_444 5
+
+#define FF_PROFILE_DTS 20
+#define FF_PROFILE_DTS_ES 30
+#define FF_PROFILE_DTS_96_24 40
+#define FF_PROFILE_DTS_HD_HRA 50
+#define FF_PROFILE_DTS_HD_MA 60
+#define FF_PROFILE_DTS_EXPRESS 70
+
+#define FF_PROFILE_MPEG2_422 0
+#define FF_PROFILE_MPEG2_HIGH 1
+#define FF_PROFILE_MPEG2_SS 2
+#define FF_PROFILE_MPEG2_SNR_SCALABLE 3
+#define FF_PROFILE_MPEG2_MAIN 4
+#define FF_PROFILE_MPEG2_SIMPLE 5
+
+#define FF_PROFILE_H264_CONSTRAINED (1 << 9) // 8+1; constraint_set1_flag
+#define FF_PROFILE_H264_INTRA (1 << 11) // 8+3; constraint_set3_flag
+
+#define FF_PROFILE_H264_BASELINE 66
+#define FF_PROFILE_H264_CONSTRAINED_BASELINE (66 | FF_PROFILE_H264_CONSTRAINED)
+#define FF_PROFILE_H264_MAIN 77
+#define FF_PROFILE_H264_EXTENDED 88
+#define FF_PROFILE_H264_HIGH 100
+#define FF_PROFILE_H264_HIGH_10 110
+#define FF_PROFILE_H264_HIGH_10_INTRA (110 | FF_PROFILE_H264_INTRA)
+#define FF_PROFILE_H264_MULTIVIEW_HIGH 118
+#define FF_PROFILE_H264_HIGH_422 122
+#define FF_PROFILE_H264_HIGH_422_INTRA (122 | FF_PROFILE_H264_INTRA)
+#define FF_PROFILE_H264_STEREO_HIGH 128
+#define FF_PROFILE_H264_HIGH_444 144
+#define FF_PROFILE_H264_HIGH_444_PREDICTIVE 244
+#define FF_PROFILE_H264_HIGH_444_INTRA (244 | FF_PROFILE_H264_INTRA)
+#define FF_PROFILE_H264_CAVLC_444 44
+
+#define FF_PROFILE_VC1_SIMPLE 0
+#define FF_PROFILE_VC1_MAIN 1
+#define FF_PROFILE_VC1_COMPLEX 2
+#define FF_PROFILE_VC1_ADVANCED 3
+
+#define FF_PROFILE_MPEG4_SIMPLE 0
+#define FF_PROFILE_MPEG4_SIMPLE_SCALABLE 1
+#define FF_PROFILE_MPEG4_CORE 2
+#define FF_PROFILE_MPEG4_MAIN 3
+#define FF_PROFILE_MPEG4_N_BIT 4
+#define FF_PROFILE_MPEG4_SCALABLE_TEXTURE 5
+#define FF_PROFILE_MPEG4_SIMPLE_FACE_ANIMATION 6
+#define FF_PROFILE_MPEG4_BASIC_ANIMATED_TEXTURE 7
+#define FF_PROFILE_MPEG4_HYBRID 8
+#define FF_PROFILE_MPEG4_ADVANCED_REAL_TIME 9
+#define FF_PROFILE_MPEG4_CORE_SCALABLE 10
+#define FF_PROFILE_MPEG4_ADVANCED_CODING 11
+#define FF_PROFILE_MPEG4_ADVANCED_CORE 12
+#define FF_PROFILE_MPEG4_ADVANCED_SCALABLE_TEXTURE 13
+#define FF_PROFILE_MPEG4_SIMPLE_STUDIO 14
+#define FF_PROFILE_MPEG4_ADVANCED_SIMPLE 15
+
+#define FF_PROFILE_JPEG2000_CSTREAM_RESTRICTION_0 1
+#define FF_PROFILE_JPEG2000_CSTREAM_RESTRICTION_1 2
+#define FF_PROFILE_JPEG2000_CSTREAM_NO_RESTRICTION 32768
+#define FF_PROFILE_JPEG2000_DCINEMA_2K 3
+#define FF_PROFILE_JPEG2000_DCINEMA_4K 4
+
+#define FF_PROFILE_VP9_0 0
+#define FF_PROFILE_VP9_1 1
+#define FF_PROFILE_VP9_2 2
+#define FF_PROFILE_VP9_3 3
+
+#define FF_PROFILE_HEVC_MAIN 1
+#define FF_PROFILE_HEVC_MAIN_10 2
+#define FF_PROFILE_HEVC_MAIN_STILL_PICTURE 3
+#define FF_PROFILE_HEVC_REXT 4
+
+#define FF_PROFILE_VVC_MAIN_10 1
+#define FF_PROFILE_VVC_MAIN_10_444 33
+
+#define FF_PROFILE_AV1_MAIN 0
+#define FF_PROFILE_AV1_HIGH 1
+#define FF_PROFILE_AV1_PROFESSIONAL 2
+
+#define FF_PROFILE_MJPEG_HUFFMAN_BASELINE_DCT 0xc0
+#define FF_PROFILE_MJPEG_HUFFMAN_EXTENDED_SEQUENTIAL_DCT 0xc1
+#define FF_PROFILE_MJPEG_HUFFMAN_PROGRESSIVE_DCT 0xc2
+#define FF_PROFILE_MJPEG_HUFFMAN_LOSSLESS 0xc3
+#define FF_PROFILE_MJPEG_JPEG_LS 0xf7
+
+#define FF_PROFILE_SBC_MSBC 1
+
+#define FF_PROFILE_PRORES_PROXY 0
+#define FF_PROFILE_PRORES_LT 1
+#define FF_PROFILE_PRORES_STANDARD 2
+#define FF_PROFILE_PRORES_HQ 3
+#define FF_PROFILE_PRORES_4444 4
+#define FF_PROFILE_PRORES_XQ 5
+
+#define FF_PROFILE_ARIB_PROFILE_A 0
+#define FF_PROFILE_ARIB_PROFILE_C 1
+
+#define FF_PROFILE_KLVA_SYNC 0
+#define FF_PROFILE_KLVA_ASYNC 1
+
+ /**
+ * level
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int level;
+#define FF_LEVEL_UNKNOWN -99
+
+ /**
+ * Skip loop filtering for selected frames.
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ enum AVDiscard skip_loop_filter;
+
+ /**
+ * Skip IDCT/dequantization for selected frames.
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ enum AVDiscard skip_idct;
+
+ /**
+ * Skip decoding for selected frames.
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ enum AVDiscard skip_frame;
+
+ /**
+ * Header containing style information for text subtitles.
+ * For SUBTITLE_ASS subtitle type, it should contain the whole ASS
+ * [Script Info] and [V4+ Styles] section, plus the [Events] line and
+ * the Format line following. It shouldn't include any Dialogue line.
+ * - encoding: Set/allocated/freed by user (before avcodec_open2())
+ * - decoding: Set/allocated/freed by libavcodec (by avcodec_open2())
+ */
+ uint8_t* subtitle_header;
+ int subtitle_header_size;
+
+ /**
+ * Audio only. The number of "priming" samples (padding) inserted by the
+ * encoder at the beginning of the audio. I.e. this number of leading
+ * decoded samples must be discarded by the caller to get the original audio
+ * without leading padding.
+ *
+ * - decoding: unused
+ * - encoding: Set by libavcodec. The timestamps on the output packets are
+ * adjusted by the encoder so that they always refer to the
+ * first sample of the data actually contained in the packet,
+ * including any added padding. E.g. if the timebase is
+ * 1/samplerate and the timestamp of the first input sample is
+ * 0, the timestamp of the first output packet will be
+ * -initial_padding.
+ */
+ int initial_padding;
+
+ /**
+ * - decoding: For codecs that store a framerate value in the compressed
+ * bitstream, the decoder may export it here. { 0, 1} when
+ * unknown.
+ * - encoding: May be used to signal the framerate of CFR content to an
+ * encoder.
+ */
+ AVRational framerate;
+
+ /**
+ * Nominal unaccelerated pixel format, see AV_PIX_FMT_xxx.
+ * - encoding: unused.
+ * - decoding: Set by libavcodec before calling get_format()
+ */
+ enum AVPixelFormat sw_pix_fmt;
+
+ /**
+ * Timebase in which pkt_dts/pts and AVPacket.dts/pts are.
+ * - encoding unused.
+ * - decoding set by user.
+ */
+ AVRational pkt_timebase;
+
+ /**
+ * AVCodecDescriptor
+ * - encoding: unused.
+ * - decoding: set by libavcodec.
+ */
+ const AVCodecDescriptor* codec_descriptor;
+
+ /**
+ * Current statistics for PTS correction.
+ * - decoding: maintained and used by libavcodec, not intended to be used by
+ * user apps
+ * - encoding: unused
+ */
+ int64_t
+ pts_correction_num_faulty_pts; /// Number of incorrect PTS values so far
+ int64_t
+ pts_correction_num_faulty_dts; /// Number of incorrect DTS values so far
+ int64_t pts_correction_last_pts; /// PTS of the last frame
+ int64_t pts_correction_last_dts; /// DTS of the last frame
+
+ /**
+ * Character encoding of the input subtitles file.
+ * - decoding: set by user
+ * - encoding: unused
+ */
+ char* sub_charenc;
+
+ /**
+ * Subtitles character encoding mode. Formats or codecs might be adjusting
+ * this setting (if they are doing the conversion themselves for instance).
+ * - decoding: set by libavcodec
+ * - encoding: unused
+ */
+ int sub_charenc_mode;
+#define FF_SUB_CHARENC_MODE_DO_NOTHING \
+ -1 ///< do nothing (demuxer outputs a stream supposed to be already in UTF-8,
+ ///< or the codec is bitmap for instance)
+#define FF_SUB_CHARENC_MODE_AUTOMATIC \
+ 0 ///< libavcodec will select the mode itself
+#define FF_SUB_CHARENC_MODE_PRE_DECODER \
+ 1 ///< the AVPacket data needs to be recoded to UTF-8 before being fed to the
+ ///< decoder, requires iconv
+#define FF_SUB_CHARENC_MODE_IGNORE \
+ 2 ///< neither convert the subtitles, nor check them for valid UTF-8
+
+ /**
+ * Skip processing alpha if supported by codec.
+ * Note that if the format uses pre-multiplied alpha (common with VP6,
+ * and recommended due to better video quality/compression)
+ * the image will look as if alpha-blended onto a black background.
+ * However for formats that do not use pre-multiplied alpha
+ * there might be serious artefacts (though e.g. libswscale currently
+ * assumes pre-multiplied alpha anyway).
+ *
+ * - decoding: set by user
+ * - encoding: unused
+ */
+ int skip_alpha;
+
+ /**
+ * Number of samples to skip after a discontinuity
+ * - decoding: unused
+ * - encoding: set by libavcodec
+ */
+ int seek_preroll;
+
+#if FF_API_DEBUG_MV
+ /**
+ * @deprecated unused
+ */
+ attribute_deprecated int debug_mv;
+# define FF_DEBUG_VIS_MV_P_FOR \
+ 0x00000001 // visualize forward predicted MVs of P frames
+# define FF_DEBUG_VIS_MV_B_FOR \
+ 0x00000002 // visualize forward predicted MVs of B frames
+# define FF_DEBUG_VIS_MV_B_BACK \
+ 0x00000004 // visualize backward predicted MVs of B frames
+#endif
+
+ /**
+ * custom intra quantization matrix
+ * - encoding: Set by user, can be NULL.
+ * - decoding: unused.
+ */
+ uint16_t* chroma_intra_matrix;
+
+ /**
+ * dump format separator.
+ * can be ", " or "\n " or anything else
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ uint8_t* dump_separator;
+
+ /**
+ * ',' separated list of allowed decoders.
+ * If NULL then all are allowed
+ * - encoding: unused
+ * - decoding: set by user
+ */
+ char* codec_whitelist;
+
+ /**
+ * Properties of the stream that gets decoded
+ * - encoding: unused
+ * - decoding: set by libavcodec
+ */
+ unsigned properties;
+#define FF_CODEC_PROPERTY_LOSSLESS 0x00000001
+#define FF_CODEC_PROPERTY_CLOSED_CAPTIONS 0x00000002
+#define FF_CODEC_PROPERTY_FILM_GRAIN 0x00000004
+
+ /**
+ * Additional data associated with the entire coded stream.
+ *
+ * - decoding: unused
+ * - encoding: may be set by libavcodec after avcodec_open2().
+ */
+ AVPacketSideData* coded_side_data;
+ int nb_coded_side_data;
+
+ /**
+ * A reference to the AVHWFramesContext describing the input (for encoding)
+ * or output (decoding) frames. The reference is set by the caller and
+ * afterwards owned (and freed) by libavcodec - it should never be read by
+ * the caller after being set.
+ *
+ * - decoding: This field should be set by the caller from the get_format()
+ * callback. The previous reference (if any) will always be
+ * unreffed by libavcodec before the get_format() call.
+ *
+ * If the default get_buffer2() is used with a hwaccel pixel
+ * format, then this AVHWFramesContext will be used for
+ * allocating the frame buffers.
+ *
+ * - encoding: For hardware encoders configured to use a hwaccel pixel
+ * format, this field should be set by the caller to a reference
+ * to the AVHWFramesContext describing input frames.
+ * AVHWFramesContext.format must be equal to
+ * AVCodecContext.pix_fmt.
+ *
+ * This field should be set before avcodec_open2() is called.
+ */
+ AVBufferRef* hw_frames_ctx;
+
+#if FF_API_SUB_TEXT_FORMAT
+ /**
+ * @deprecated unused
+ */
+ attribute_deprecated int sub_text_format;
+# define FF_SUB_TEXT_FMT_ASS 0
+#endif
+
+ /**
+ * Audio only. The amount of padding (in samples) appended by the encoder to
+ * the end of the audio. I.e. this number of decoded samples must be
+ * discarded by the caller from the end of the stream to get the original
+ * audio without any trailing padding.
+ *
+ * - decoding: unused
+ * - encoding: unused
+ */
+ int trailing_padding;
+
+ /**
+ * The number of pixels per image to maximally accept.
+ *
+ * - decoding: set by user
+ * - encoding: set by user
+ */
+ int64_t max_pixels;
+
+ /**
+ * A reference to the AVHWDeviceContext describing the device which will
+ * be used by a hardware encoder/decoder. The reference is set by the
+ * caller and afterwards owned (and freed) by libavcodec.
+ *
+ * This should be used if either the codec device does not require
+ * hardware frames or any that are used are to be allocated internally by
+ * libavcodec. If the user wishes to supply any of the frames used as
+ * encoder input or decoder output then hw_frames_ctx should be used
+ * instead. When hw_frames_ctx is set in get_format() for a decoder, this
+ * field will be ignored while decoding the associated stream segment, but
+ * may again be used on a following one after another get_format() call.
+ *
+ * For both encoders and decoders this field should be set before
+ * avcodec_open2() is called and must not be written to thereafter.
+ *
+ * Note that some decoders may require this field to be set initially in
+ * order to support hw_frames_ctx at all - in that case, all frames
+ * contexts used must be created on the same device.
+ */
+ AVBufferRef* hw_device_ctx;
+
+ /**
+ * Bit set of AV_HWACCEL_FLAG_* flags, which affect hardware accelerated
+ * decoding (if active).
+ * - encoding: unused
+ * - decoding: Set by user (either before avcodec_open2(), or in the
+ * AVCodecContext.get_format callback)
+ */
+ int hwaccel_flags;
+
+ /**
+ * Video decoding only. Certain video codecs support cropping, meaning that
+ * only a sub-rectangle of the decoded frame is intended for display. This
+ * option controls how cropping is handled by libavcodec.
+ *
+ * When set to 1 (the default), libavcodec will apply cropping internally.
+ * I.e. it will modify the output frame width/height fields and offset the
+ * data pointers (only by as much as possible while preserving alignment, or
+ * by the full amount if the AV_CODEC_FLAG_UNALIGNED flag is set) so that
+ * the frames output by the decoder refer only to the cropped area. The
+ * crop_* fields of the output frames will be zero.
+ *
+ * When set to 0, the width/height fields of the output frames will be set
+ * to the coded dimensions and the crop_* fields will describe the cropping
+ * rectangle. Applying the cropping is left to the caller.
+ *
+ * @warning When hardware acceleration with opaque output frames is used,
+ * libavcodec is unable to apply cropping from the top/left border.
+ *
+ * @note when this option is set to zero, the width/height fields of the
+ * AVCodecContext and output AVFrames have different meanings. The codec
+ * context fields store display dimensions (with the coded dimensions in
+ * coded_width/height), while the frame fields store the coded dimensions
+ * (with the display dimensions being determined by the crop_* fields).
+ */
+ int apply_cropping;
+
+ /*
+ * Video decoding only. Sets the number of extra hardware frames which
+ * the decoder will allocate for use by the caller. This must be set
+ * before avcodec_open2() is called.
+ *
+ * Some hardware decoders require all frames that they will use for
+ * output to be defined in advance before decoding starts. For such
+ * decoders, the hardware frame pool must therefore be of a fixed size.
+ * The extra frames set here are on top of any number that the decoder
+ * needs internally in order to operate normally (for example, frames
+ * used as reference pictures).
+ */
+ int extra_hw_frames;
+
+ /**
+ * The percentage of damaged samples to discard a frame.
+ *
+ * - decoding: set by user
+ * - encoding: unused
+ */
+ int discard_damaged_percentage;
+
+ /**
+ * The number of samples per frame to maximally accept.
+ *
+ * - decoding: set by user
+ * - encoding: set by user
+ */
+ int64_t max_samples;
+
+ /**
+ * Bit set of AV_CODEC_EXPORT_DATA_* flags, which affects the kind of
+ * metadata exported in frame, packet, or coded stream side data by
+ * decoders and encoders.
+ *
+ * - decoding: set by user
+ * - encoding: set by user
+ */
+ int export_side_data;
+
+ /**
+ * This callback is called at the beginning of each packet to get a data
+ * buffer for it.
+ *
+ * The following field will be set in the packet before this callback is
+ * called:
+ * - size
+ * This callback must use the above value to calculate the required buffer
+ * size, which must padded by at least AV_INPUT_BUFFER_PADDING_SIZE bytes.
+ *
+ * In some specific cases, the encoder may not use the entire buffer allocated
+ * by this callback. This will be reflected in the size value in the packet
+ * once returned by avcodec_receive_packet().
+ *
+ * This callback must fill the following fields in the packet:
+ * - data: alignment requirements for AVPacket apply, if any. Some
+ * architectures and encoders may benefit from having aligned data.
+ * - buf: must contain a pointer to an AVBufferRef structure. The packet's
+ * data pointer must be contained in it. See: av_buffer_create(),
+ * av_buffer_alloc(), and av_buffer_ref().
+ *
+ * If AV_CODEC_CAP_DR1 is not set then get_encode_buffer() must call
+ * avcodec_default_get_encode_buffer() instead of providing a buffer allocated
+ * by some other means.
+ *
+ * The flags field may contain a combination of AV_GET_ENCODE_BUFFER_FLAG_
+ * flags. They may be used for example to hint what use the buffer may get
+ * after being created. Implementations of this callback may ignore flags they
+ * don't understand. If AV_GET_ENCODE_BUFFER_FLAG_REF is set in flags then the
+ * packet may be reused (read and/or written to if it is writable) later by
+ * libavcodec.
+ *
+ * This callback must be thread-safe, as when frame threading is used, it may
+ * be called from multiple threads simultaneously.
+ *
+ * @see avcodec_default_get_encode_buffer()
+ *
+ * - encoding: Set by libavcodec, user can override.
+ * - decoding: unused
+ */
+ int (*get_encode_buffer)(struct AVCodecContext* s, AVPacket* pkt, int flags);
+} AVCodecContext;
+
+struct MpegEncContext;
+
+/**
+ * @defgroup lavc_hwaccel AVHWAccel
+ *
+ * @note Nothing in this structure should be accessed by the user. At some
+ * point in future it will not be externally visible at all.
+ *
+ * @{
+ */
+typedef struct AVHWAccel {
+ /**
+ * Name of the hardware accelerated codec.
+ * The name is globally unique among encoders and among decoders (but an
+ * encoder and a decoder can share the same name).
+ */
+ const char* name;
+
+ /**
+ * Type of codec implemented by the hardware accelerator.
+ *
+ * See AVMEDIA_TYPE_xxx
+ */
+ enum AVMediaType type;
+
+ /**
+ * Codec implemented by the hardware accelerator.
+ *
+ * See AV_CODEC_ID_xxx
+ */
+ enum AVCodecID id;
+
+ /**
+ * Supported pixel format.
+ *
+ * Only hardware accelerated formats are supported here.
+ */
+ enum AVPixelFormat pix_fmt;
+
+ /**
+ * Hardware accelerated codec capabilities.
+ * see AV_HWACCEL_CODEC_CAP_*
+ */
+ int capabilities;
+
+ /*****************************************************************
+ * No fields below this line are part of the public API. They
+ * may not be used outside of libavcodec and can be changed and
+ * removed at will.
+ * New public fields should be added right above.
+ *****************************************************************
+ */
+
+ /**
+ * Allocate a custom buffer
+ */
+ int (*alloc_frame)(AVCodecContext* avctx, AVFrame* frame);
+
+ /**
+ * Called at the beginning of each frame or field picture.
+ *
+ * Meaningful frame information (codec specific) is guaranteed to
+ * be parsed at this point. This function is mandatory.
+ *
+ * Note that buf can be NULL along with buf_size set to 0.
+ * Otherwise, this means the whole frame is available at this point.
+ *
+ * @param avctx the codec context
+ * @param buf the frame data buffer base
+ * @param buf_size the size of the frame in bytes
+ * @return zero if successful, a negative value otherwise
+ */
+ int (*start_frame)(AVCodecContext* avctx, const uint8_t* buf,
+ uint32_t buf_size);
+
+ /**
+ * Callback for parameter data (SPS/PPS/VPS etc).
+ *
+ * Useful for hardware decoders which keep persistent state about the
+ * video parameters, and need to receive any changes to update that state.
+ *
+ * @param avctx the codec context
+ * @param type the nal unit type
+ * @param buf the nal unit data buffer
+ * @param buf_size the size of the nal unit in bytes
+ * @return zero if successful, a negative value otherwise
+ */
+ int (*decode_params)(AVCodecContext* avctx, int type, const uint8_t* buf,
+ uint32_t buf_size);
+
+ /**
+ * Callback for each slice.
+ *
+ * Meaningful slice information (codec specific) is guaranteed to
+ * be parsed at this point. This function is mandatory.
+ * The only exception is XvMC, that works on MB level.
+ *
+ * @param avctx the codec context
+ * @param buf the slice data buffer base
+ * @param buf_size the size of the slice in bytes
+ * @return zero if successful, a negative value otherwise
+ */
+ int (*decode_slice)(AVCodecContext* avctx, const uint8_t* buf,
+ uint32_t buf_size);
+
+ /**
+ * Called at the end of each frame or field picture.
+ *
+ * The whole picture is parsed at this point and can now be sent
+ * to the hardware accelerator. This function is mandatory.
+ *
+ * @param avctx the codec context
+ * @return zero if successful, a negative value otherwise
+ */
+ int (*end_frame)(AVCodecContext* avctx);
+
+ /**
+ * Size of per-frame hardware accelerator private data.
+ *
+ * Private data is allocated with av_mallocz() before
+ * AVCodecContext.get_buffer() and deallocated after
+ * AVCodecContext.release_buffer().
+ */
+ int frame_priv_data_size;
+
+ /**
+ * Called for every Macroblock in a slice.
+ *
+ * XvMC uses it to replace the ff_mpv_reconstruct_mb().
+ * Instead of decoding to raw picture, MB parameters are
+ * stored in an array provided by the video driver.
+ *
+ * @param s the mpeg context
+ */
+ void (*decode_mb)(struct MpegEncContext* s);
+
+ /**
+ * Initialize the hwaccel private data.
+ *
+ * This will be called from ff_get_format(), after hwaccel and
+ * hwaccel_context are set and the hwaccel private data in AVCodecInternal
+ * is allocated.
+ */
+ int (*init)(AVCodecContext* avctx);
+
+ /**
+ * Uninitialize the hwaccel private data.
+ *
+ * This will be called from get_format() or avcodec_close(), after hwaccel
+ * and hwaccel_context are already uninitialized.
+ */
+ int (*uninit)(AVCodecContext* avctx);
+
+ /**
+ * Size of the private data to allocate in
+ * AVCodecInternal.hwaccel_priv_data.
+ */
+ int priv_data_size;
+
+ /**
+ * Internal hwaccel capabilities.
+ */
+ int caps_internal;
+
+ /**
+ * Fill the given hw_frames context with current codec parameters. Called
+ * from get_format. Refer to avcodec_get_hw_frames_parameters() for
+ * details.
+ *
+ * This CAN be called before AVHWAccel.init is called, and you must assume
+ * that avctx->hwaccel_priv_data is invalid.
+ */
+ int (*frame_params)(AVCodecContext* avctx, AVBufferRef* hw_frames_ctx);
+} AVHWAccel;
+
+/**
+ * HWAccel is experimental and is thus avoided in favor of non experimental
+ * codecs
+ */
+#define AV_HWACCEL_CODEC_CAP_EXPERIMENTAL 0x0200
+
+/**
+ * Hardware acceleration should be used for decoding even if the codec level
+ * used is unknown or higher than the maximum supported level reported by the
+ * hardware driver.
+ *
+ * It's generally a good idea to pass this flag unless you have a specific
+ * reason not to, as hardware tends to under-report supported levels.
+ */
+#define AV_HWACCEL_FLAG_IGNORE_LEVEL (1 << 0)
+
+/**
+ * Hardware acceleration can output YUV pixel formats with a different chroma
+ * sampling than 4:2:0 and/or other than 8 bits per component.
+ */
+#define AV_HWACCEL_FLAG_ALLOW_HIGH_DEPTH (1 << 1)
+
+/**
+ * Hardware acceleration should still be attempted for decoding when the
+ * codec profile does not match the reported capabilities of the hardware.
+ *
+ * For example, this can be used to try to decode baseline profile H.264
+ * streams in hardware - it will often succeed, because many streams marked
+ * as baseline profile actually conform to constrained baseline profile.
+ *
+ * @warning If the stream is actually not supported then the behaviour is
+ * undefined, and may include returning entirely incorrect output
+ * while indicating success.
+ */
+#define AV_HWACCEL_FLAG_ALLOW_PROFILE_MISMATCH (1 << 2)
+
+/**
+ * @}
+ */
+
+enum AVSubtitleType {
+ SUBTITLE_NONE,
+
+ SUBTITLE_BITMAP, ///< A bitmap, pict will be set
+
+ /**
+ * Plain text, the text field must be set by the decoder and is
+ * authoritative. ass and pict fields may contain approximations.
+ */
+ SUBTITLE_TEXT,
+
+ /**
+ * Formatted text, the ass field must be set by the decoder and is
+ * authoritative. pict and text fields may contain approximations.
+ */
+ SUBTITLE_ASS,
+};
+
+#define AV_SUBTITLE_FLAG_FORCED 0x00000001
+
+typedef struct AVSubtitleRect {
+ int x; ///< top left corner of pict, undefined when pict is not set
+ int y; ///< top left corner of pict, undefined when pict is not set
+ int w; ///< width of pict, undefined when pict is not set
+ int h; ///< height of pict, undefined when pict is not set
+ int nb_colors; ///< number of colors in pict, undefined when pict is not set
+
+ /**
+ * data+linesize for the bitmap of this subtitle.
+ * Can be set for text/ass as well once they are rendered.
+ */
+ uint8_t* data[4];
+ int linesize[4];
+
+ enum AVSubtitleType type;
+
+ char* text; ///< 0 terminated plain UTF-8 text
+
+ /**
+ * 0 terminated ASS/SSA compatible event line.
+ * The presentation of this is unaffected by the other values in this
+ * struct.
+ */
+ char* ass;
+
+ int flags;
+} AVSubtitleRect;
+
+typedef struct AVSubtitle {
+ uint16_t format; /* 0 = graphics */
+ uint32_t start_display_time; /* relative to packet pts, in ms */
+ uint32_t end_display_time; /* relative to packet pts, in ms */
+ unsigned num_rects;
+ AVSubtitleRect** rects;
+ int64_t pts; ///< Same as packet pts, in AV_TIME_BASE
+} AVSubtitle;
+
+/**
+ * Return the LIBAVCODEC_VERSION_INT constant.
+ */
+unsigned avcodec_version(void);
+
+/**
+ * Return the libavcodec build-time configuration.
+ */
+const char* avcodec_configuration(void);
+
+/**
+ * Return the libavcodec license.
+ */
+const char* avcodec_license(void);
+
+/**
+ * Allocate an AVCodecContext and set its fields to default values. The
+ * resulting struct should be freed with avcodec_free_context().
+ *
+ * @param codec if non-NULL, allocate private data and initialize defaults
+ * for the given codec. It is illegal to then call avcodec_open2()
+ * with a different codec.
+ * If NULL, then the codec-specific defaults won't be initialized,
+ * which may result in suboptimal default settings (this is
+ * important mainly for encoders, e.g. libx264).
+ *
+ * @return An AVCodecContext filled with default values or NULL on failure.
+ */
+AVCodecContext* avcodec_alloc_context3(const AVCodec* codec);
+
+/**
+ * Free the codec context and everything associated with it and write NULL to
+ * the provided pointer.
+ */
+void avcodec_free_context(AVCodecContext** avctx);
+
+/**
+ * Get the AVClass for AVCodecContext. It can be used in combination with
+ * AV_OPT_SEARCH_FAKE_OBJ for examining options.
+ *
+ * @see av_opt_find().
+ */
+const AVClass* avcodec_get_class(void);
+
+#if FF_API_GET_FRAME_CLASS
+/**
+ * @deprecated This function should not be used.
+ */
+attribute_deprecated const AVClass* avcodec_get_frame_class(void);
+#endif
+
+/**
+ * Get the AVClass for AVSubtitleRect. It can be used in combination with
+ * AV_OPT_SEARCH_FAKE_OBJ for examining options.
+ *
+ * @see av_opt_find().
+ */
+const AVClass* avcodec_get_subtitle_rect_class(void);
+
+/**
+ * Fill the parameters struct based on the values from the supplied codec
+ * context. Any allocated fields in par are freed and replaced with duplicates
+ * of the corresponding fields in codec.
+ *
+ * @return >= 0 on success, a negative AVERROR code on failure
+ */
+int avcodec_parameters_from_context(AVCodecParameters* par,
+ const AVCodecContext* codec);
+
+/**
+ * Fill the codec context based on the values from the supplied codec
+ * parameters. Any allocated fields in codec that have a corresponding field in
+ * par are freed and replaced with duplicates of the corresponding field in par.
+ * Fields in codec that do not have a counterpart in par are not touched.
+ *
+ * @return >= 0 on success, a negative AVERROR code on failure.
+ */
+int avcodec_parameters_to_context(AVCodecContext* codec,
+ const AVCodecParameters* par);
+
+/**
+ * Initialize the AVCodecContext to use the given AVCodec. Prior to using this
+ * function the context has to be allocated with avcodec_alloc_context3().
+ *
+ * The functions avcodec_find_decoder_by_name(), avcodec_find_encoder_by_name(),
+ * avcodec_find_decoder() and avcodec_find_encoder() provide an easy way for
+ * retrieving a codec.
+ *
+ * @warning This function is not thread safe!
+ *
+ * @note Always call this function before using decoding routines (such as
+ * @ref avcodec_receive_frame()).
+ *
+ * @code
+ * av_dict_set(&opts, "b", "2.5M", 0);
+ * codec = avcodec_find_decoder(AV_CODEC_ID_H264);
+ * if (!codec)
+ * exit(1);
+ *
+ * context = avcodec_alloc_context3(codec);
+ *
+ * if (avcodec_open2(context, codec, opts) < 0)
+ * exit(1);
+ * @endcode
+ *
+ * @param avctx The context to initialize.
+ * @param codec The codec to open this context for. If a non-NULL codec has been
+ * previously passed to avcodec_alloc_context3() or
+ * for this context, then this parameter MUST be either NULL or
+ * equal to the previously passed codec.
+ * @param options A dictionary filled with AVCodecContext and codec-private
+ * options. On return this object will be filled with options that were not
+ * found.
+ *
+ * @return zero on success, a negative value on error
+ * @see avcodec_alloc_context3(), avcodec_find_decoder(),
+ * avcodec_find_encoder(), av_dict_set(), av_opt_find().
+ */
+int avcodec_open2(AVCodecContext* avctx, const AVCodec* codec,
+ AVDictionary** options);
+
+/**
+ * Close a given AVCodecContext and free all the data associated with it
+ * (but not the AVCodecContext itself).
+ *
+ * Calling this function on an AVCodecContext that hasn't been opened will free
+ * the codec-specific data allocated in avcodec_alloc_context3() with a non-NULL
+ * codec. Subsequent calls will do nothing.
+ *
+ * @note Do not use this function. Use avcodec_free_context() to destroy a
+ * codec context (either open or closed). Opening and closing a codec context
+ * multiple times is not supported anymore -- use multiple codec contexts
+ * instead.
+ */
+int avcodec_close(AVCodecContext* avctx);
+
+/**
+ * Free all allocated data in the given subtitle struct.
+ *
+ * @param sub AVSubtitle to free.
+ */
+void avsubtitle_free(AVSubtitle* sub);
+
+/**
+ * @}
+ */
+
+/**
+ * @addtogroup lavc_decoding
+ * @{
+ */
+
+/**
+ * The default callback for AVCodecContext.get_buffer2(). It is made public so
+ * it can be called by custom get_buffer2() implementations for decoders without
+ * AV_CODEC_CAP_DR1 set.
+ */
+int avcodec_default_get_buffer2(AVCodecContext* s, AVFrame* frame, int flags);
+
+/**
+ * The default callback for AVCodecContext.get_encode_buffer(). It is made
+ * public so it can be called by custom get_encode_buffer() implementations for
+ * encoders without AV_CODEC_CAP_DR1 set.
+ */
+int avcodec_default_get_encode_buffer(AVCodecContext* s, AVPacket* pkt,
+ int flags);
+
+/**
+ * Modify width and height values so that they will result in a memory
+ * buffer that is acceptable for the codec if you do not use any horizontal
+ * padding.
+ *
+ * May only be used if a codec with AV_CODEC_CAP_DR1 has been opened.
+ */
+void avcodec_align_dimensions(AVCodecContext* s, int* width, int* height);
+
+/**
+ * Modify width and height values so that they will result in a memory
+ * buffer that is acceptable for the codec if you also ensure that all
+ * line sizes are a multiple of the respective linesize_align[i].
+ *
+ * May only be used if a codec with AV_CODEC_CAP_DR1 has been opened.
+ */
+void avcodec_align_dimensions2(AVCodecContext* s, int* width, int* height,
+ int linesize_align[AV_NUM_DATA_POINTERS]);
+
+/**
+ * Converts AVChromaLocation to swscale x/y chroma position.
+ *
+ * The positions represent the chroma (0,0) position in a coordinates system
+ * with luma (0,0) representing the origin and luma(1,1) representing 256,256
+ *
+ * @param xpos horizontal chroma sample position
+ * @param ypos vertical chroma sample position
+ */
+int avcodec_enum_to_chroma_pos(int* xpos, int* ypos, enum AVChromaLocation pos);
+
+/**
+ * Converts swscale x/y chroma position to AVChromaLocation.
+ *
+ * The positions represent the chroma (0,0) position in a coordinates system
+ * with luma (0,0) representing the origin and luma(1,1) representing 256,256
+ *
+ * @param xpos horizontal chroma sample position
+ * @param ypos vertical chroma sample position
+ */
+enum AVChromaLocation avcodec_chroma_pos_to_enum(int xpos, int ypos);
+
+/**
+ * Decode a subtitle message.
+ * Return a negative value on error, otherwise return the number of bytes used.
+ * If no subtitle could be decompressed, got_sub_ptr is zero.
+ * Otherwise, the subtitle is stored in *sub.
+ * Note that AV_CODEC_CAP_DR1 is not available for subtitle codecs. This is for
+ * simplicity, because the performance difference is expected to be negligible
+ * and reusing a get_buffer written for video codecs would probably perform
+ * badly due to a potentially very different allocation pattern.
+ *
+ * Some decoders (those marked with AV_CODEC_CAP_DELAY) have a delay between
+ * input and output. This means that for some packets they will not immediately
+ * produce decoded output and need to be flushed at the end of decoding to get
+ * all the decoded data. Flushing is done by calling this function with packets
+ * with avpkt->data set to NULL and avpkt->size set to 0 until it stops
+ * returning subtitles. It is safe to flush even those decoders that are not
+ * marked with AV_CODEC_CAP_DELAY, then no subtitles will be returned.
+ *
+ * @note The AVCodecContext MUST have been opened with @ref avcodec_open2()
+ * before packets may be fed to the decoder.
+ *
+ * @param avctx the codec context
+ * @param[out] sub The preallocated AVSubtitle in which the decoded subtitle
+ * will be stored, must be freed with avsubtitle_free if *got_sub_ptr is set.
+ * @param[in,out] got_sub_ptr Zero if no subtitle could be decompressed,
+ * otherwise, it is nonzero.
+ * @param[in] avpkt The input AVPacket containing the input buffer.
+ */
+int avcodec_decode_subtitle2(AVCodecContext* avctx, AVSubtitle* sub,
+ int* got_sub_ptr, AVPacket* avpkt);
+
+/**
+ * Supply raw packet data as input to a decoder.
+ *
+ * Internally, this call will copy relevant AVCodecContext fields, which can
+ * influence decoding per-packet, and apply them when the packet is actually
+ * decoded. (For example AVCodecContext.skip_frame, which might direct the
+ * decoder to drop the frame contained by the packet sent with this function.)
+ *
+ * @warning The input buffer, avpkt->data must be AV_INPUT_BUFFER_PADDING_SIZE
+ * larger than the actual read bytes because some optimized bitstream
+ * readers read 32 or 64 bits at once and could read over the end.
+ *
+ * @note The AVCodecContext MUST have been opened with @ref avcodec_open2()
+ * before packets may be fed to the decoder.
+ *
+ * @param avctx codec context
+ * @param[in] avpkt The input AVPacket. Usually, this will be a single video
+ * frame, or several complete audio frames.
+ * Ownership of the packet remains with the caller, and the
+ * decoder will not write to the packet. The decoder may create
+ * a reference to the packet data (or copy it if the packet is
+ * not reference-counted).
+ * Unlike with older APIs, the packet is always fully consumed,
+ * and if it contains multiple frames (e.g. some audio codecs),
+ * will require you to call avcodec_receive_frame() multiple
+ * times afterwards before you can send a new packet.
+ * It can be NULL (or an AVPacket with data set to NULL and
+ * size set to 0); in this case, it is considered a flush
+ * packet, which signals the end of the stream. Sending the
+ * first flush packet will return success. Subsequent ones are
+ * unnecessary and will return AVERROR_EOF. If the decoder
+ * still has frames buffered, it will return them after sending
+ * a flush packet.
+ *
+ * @return 0 on success, otherwise negative error code:
+ * AVERROR(EAGAIN): input is not accepted in the current state - user
+ * must read output with avcodec_receive_frame() (once
+ * all output is read, the packet should be resent, and
+ * the call will not fail with EAGAIN).
+ * AVERROR_EOF: the decoder has been flushed, and no new packets can
+ * be sent to it (also returned if more than 1 flush
+ * packet is sent)
+ * AVERROR(EINVAL): codec not opened, it is an encoder, or requires flush
+ * AVERROR(ENOMEM): failed to add packet to internal queue, or similar
+ * other errors: legitimate decoding errors
+ */
+int avcodec_send_packet(AVCodecContext* avctx, const AVPacket* avpkt);
+
+/**
+ * Return decoded output data from a decoder.
+ *
+ * @param avctx codec context
+ * @param frame This will be set to a reference-counted video or audio
+ * frame (depending on the decoder type) allocated by the
+ * decoder. Note that the function will always call
+ * av_frame_unref(frame) before doing anything else.
+ *
+ * @return
+ * 0: success, a frame was returned
+ * AVERROR(EAGAIN): output is not available in this state - user must try
+ * to send new input
+ * AVERROR_EOF: the decoder has been fully flushed, and there will be
+ * no more output frames
+ * AVERROR(EINVAL): codec not opened, or it is an encoder
+ * AVERROR_INPUT_CHANGED: current decoded frame has changed parameters
+ * with respect to first decoded frame. Applicable
+ * when flag AV_CODEC_FLAG_DROPCHANGED is set.
+ * other negative values: legitimate decoding errors
+ */
+int avcodec_receive_frame(AVCodecContext* avctx, AVFrame* frame);
+
+/**
+ * Supply a raw video or audio frame to the encoder. Use
+ * avcodec_receive_packet() to retrieve buffered output packets.
+ *
+ * @param avctx codec context
+ * @param[in] frame AVFrame containing the raw audio or video frame to be
+ * encoded. Ownership of the frame remains with the caller, and the encoder will
+ * not write to the frame. The encoder may create a reference to the frame data
+ * (or copy it if the frame is not reference-counted). It can be NULL, in which
+ * case it is considered a flush packet. This signals the end of the stream. If
+ * the encoder still has packets buffered, it will return them after this call.
+ * Once flushing mode has been entered, additional flush packets are ignored,
+ * and sending frames will return AVERROR_EOF.
+ *
+ * For audio:
+ * If AV_CODEC_CAP_VARIABLE_FRAME_SIZE is set, then each frame
+ * can have any number of samples.
+ * If it is not set, frame->nb_samples must be equal to
+ * avctx->frame_size for all frames except the last.
+ * The final frame may be smaller than avctx->frame_size.
+ * @return 0 on success, otherwise negative error code:
+ * AVERROR(EAGAIN): input is not accepted in the current state - user
+ * must read output with avcodec_receive_packet() (once
+ * all output is read, the packet should be resent, and
+ * the call will not fail with EAGAIN).
+ * AVERROR_EOF: the encoder has been flushed, and no new frames can
+ * be sent to it
+ * AVERROR(EINVAL): codec not opened, it is a decoder, or requires flush
+ * AVERROR(ENOMEM): failed to add packet to internal queue, or similar
+ * other errors: legitimate encoding errors
+ */
+int avcodec_send_frame(AVCodecContext* avctx, const AVFrame* frame);
+
+/**
+ * Read encoded data from the encoder.
+ *
+ * @param avctx codec context
+ * @param avpkt This will be set to a reference-counted packet allocated by the
+ * encoder. Note that the function will always call
+ * av_packet_unref(avpkt) before doing anything else.
+ * @return 0 on success, otherwise negative error code:
+ * AVERROR(EAGAIN): output is not available in the current state - user
+ * must try to send input
+ * AVERROR_EOF: the encoder has been fully flushed, and there will be
+ * no more output packets
+ * AVERROR(EINVAL): codec not opened, or it is a decoder
+ * other errors: legitimate encoding errors
+ */
+int avcodec_receive_packet(AVCodecContext* avctx, AVPacket* avpkt);
+
+/**
+ * Create and return a AVHWFramesContext with values adequate for hardware
+ * decoding. This is meant to get called from the get_format callback, and is
+ * a helper for preparing a AVHWFramesContext for AVCodecContext.hw_frames_ctx.
+ * This API is for decoding with certain hardware acceleration modes/APIs only.
+ *
+ * The returned AVHWFramesContext is not initialized. The caller must do this
+ * with av_hwframe_ctx_init().
+ *
+ * Calling this function is not a requirement, but makes it simpler to avoid
+ * codec or hardware API specific details when manually allocating frames.
+ *
+ * Alternatively to this, an API user can set AVCodecContext.hw_device_ctx,
+ * which sets up AVCodecContext.hw_frames_ctx fully automatically, and makes
+ * it unnecessary to call this function or having to care about
+ * AVHWFramesContext initialization at all.
+ *
+ * There are a number of requirements for calling this function:
+ *
+ * - It must be called from get_format with the same avctx parameter that was
+ * passed to get_format. Calling it outside of get_format is not allowed, and
+ * can trigger undefined behavior.
+ * - The function is not always supported (see description of return values).
+ * Even if this function returns successfully, hwaccel initialization could
+ * fail later. (The degree to which implementations check whether the stream
+ * is actually supported varies. Some do this check only after the user's
+ * get_format callback returns.)
+ * - The hw_pix_fmt must be one of the choices suggested by get_format. If the
+ * user decides to use a AVHWFramesContext prepared with this API function,
+ * the user must return the same hw_pix_fmt from get_format.
+ * - The device_ref passed to this function must support the given hw_pix_fmt.
+ * - After calling this API function, it is the user's responsibility to
+ * initialize the AVHWFramesContext (returned by the out_frames_ref
+ * parameter), and to set AVCodecContext.hw_frames_ctx to it. If done, this must
+ * be done before returning from get_format (this is implied by the normal
+ * AVCodecContext.hw_frames_ctx API rules).
+ * - The AVHWFramesContext parameters may change every time time get_format is
+ * called. Also, AVCodecContext.hw_frames_ctx is reset before get_format. So
+ * you are inherently required to go through this process again on every
+ * get_format call.
+ * - It is perfectly possible to call this function without actually using
+ * the resulting AVHWFramesContext. One use-case might be trying to reuse a
+ * previously initialized AVHWFramesContext, and calling this API function
+ * only to test whether the required frame parameters have changed.
+ * - Fields that use dynamically allocated values of any kind must not be set
+ * by the user unless setting them is explicitly allowed by the documentation.
+ * If the user sets AVHWFramesContext.free and AVHWFramesContext.user_opaque,
+ * the new free callback must call the potentially set previous free callback.
+ * This API call may set any dynamically allocated fields, including the free
+ * callback.
+ *
+ * The function will set at least the following fields on AVHWFramesContext
+ * (potentially more, depending on hwaccel API):
+ *
+ * - All fields set by av_hwframe_ctx_alloc().
+ * - Set the format field to hw_pix_fmt.
+ * - Set the sw_format field to the most suited and most versatile format. (An
+ * implication is that this will prefer generic formats over opaque formats
+ * with arbitrary restrictions, if possible.)
+ * - Set the width/height fields to the coded frame size, rounded up to the
+ * API-specific minimum alignment.
+ * - Only _if_ the hwaccel requires a pre-allocated pool: set the
+ * initial_pool_size field to the number of maximum reference surfaces possible
+ * with the codec, plus 1 surface for the user to work (meaning the user can
+ * safely reference at most 1 decoded surface at a time), plus additional
+ * buffering introduced by frame threading. If the hwaccel does not require
+ * pre-allocation, the field is left to 0, and the decoder will allocate new
+ * surfaces on demand during decoding.
+ * - Possibly AVHWFramesContext.hwctx fields, depending on the underlying
+ * hardware API.
+ *
+ * Essentially, out_frames_ref returns the same as av_hwframe_ctx_alloc(), but
+ * with basic frame parameters set.
+ *
+ * The function is stateless, and does not change the AVCodecContext or the
+ * device_ref AVHWDeviceContext.
+ *
+ * @param avctx The context which is currently calling get_format, and which
+ * implicitly contains all state needed for filling the returned
+ * AVHWFramesContext properly.
+ * @param device_ref A reference to the AVHWDeviceContext describing the device
+ * which will be used by the hardware decoder.
+ * @param hw_pix_fmt The hwaccel format you are going to return from get_format.
+ * @param out_frames_ref On success, set to a reference to an _uninitialized_
+ * AVHWFramesContext, created from the given device_ref.
+ * Fields will be set to values required for decoding.
+ * Not changed if an error is returned.
+ * @return zero on success, a negative value on error. The following error codes
+ * have special semantics:
+ * AVERROR(ENOENT): the decoder does not support this functionality. Setup
+ * is always manual, or it is a decoder which does not
+ * support setting AVCodecContext.hw_frames_ctx at all,
+ * or it is a software format.
+ * AVERROR(EINVAL): it is known that hardware decoding is not supported for
+ * this configuration, or the device_ref is not supported
+ * for the hwaccel referenced by hw_pix_fmt.
+ */
+int avcodec_get_hw_frames_parameters(AVCodecContext* avctx,
+ AVBufferRef* device_ref,
+ enum AVPixelFormat hw_pix_fmt,
+ AVBufferRef** out_frames_ref);
+
+/**
+ * @defgroup lavc_parsing Frame parsing
+ * @{
+ */
+
+enum AVPictureStructure {
+ AV_PICTURE_STRUCTURE_UNKNOWN, //< unknown
+ AV_PICTURE_STRUCTURE_TOP_FIELD, //< coded as top field
+ AV_PICTURE_STRUCTURE_BOTTOM_FIELD, //< coded as bottom field
+ AV_PICTURE_STRUCTURE_FRAME, //< coded as frame
+};
+
+typedef struct AVCodecParserContext {
+ void* priv_data;
+ const struct AVCodecParser* parser;
+ int64_t frame_offset; /* offset of the current frame */
+ int64_t cur_offset; /* current offset
+ (incremented by each av_parser_parse()) */
+ int64_t next_frame_offset; /* offset of the next frame */
+ /* video info */
+ int pict_type; /* XXX: Put it back in AVCodecContext. */
+ /**
+ * This field is used for proper frame duration computation in lavf.
+ * It signals, how much longer the frame duration of the current frame
+ * is compared to normal frame duration.
+ *
+ * frame_duration = (1 + repeat_pict) * time_base
+ *
+ * It is used by codecs like H.264 to display telecined material.
+ */
+ int repeat_pict; /* XXX: Put it back in AVCodecContext. */
+ int64_t pts; /* pts of the current frame */
+ int64_t dts; /* dts of the current frame */
+
+ /* private data */
+ int64_t last_pts;
+ int64_t last_dts;
+ int fetch_timestamp;
+
+#define AV_PARSER_PTS_NB 4
+ int cur_frame_start_index;
+ int64_t cur_frame_offset[AV_PARSER_PTS_NB];
+ int64_t cur_frame_pts[AV_PARSER_PTS_NB];
+ int64_t cur_frame_dts[AV_PARSER_PTS_NB];
+
+ int flags;
+#define PARSER_FLAG_COMPLETE_FRAMES 0x0001
+#define PARSER_FLAG_ONCE 0x0002
+/// Set if the parser has a valid file offset
+#define PARSER_FLAG_FETCHED_OFFSET 0x0004
+#define PARSER_FLAG_USE_CODEC_TS 0x1000
+
+ int64_t offset; ///< byte offset from starting packet start
+ int64_t cur_frame_end[AV_PARSER_PTS_NB];
+
+ /**
+ * Set by parser to 1 for key frames and 0 for non-key frames.
+ * It is initialized to -1, so if the parser doesn't set this flag,
+ * old-style fallback using AV_PICTURE_TYPE_I picture type as key frames
+ * will be used.
+ */
+ int key_frame;
+
+ // Timestamp generation support:
+ /**
+ * Synchronization point for start of timestamp generation.
+ *
+ * Set to >0 for sync point, 0 for no sync point and <0 for undefined
+ * (default).
+ *
+ * For example, this corresponds to presence of H.264 buffering period
+ * SEI message.
+ */
+ int dts_sync_point;
+
+ /**
+ * Offset of the current timestamp against last timestamp sync point in
+ * units of AVCodecContext.time_base.
+ *
+ * Set to INT_MIN when dts_sync_point unused. Otherwise, it must
+ * contain a valid timestamp offset.
+ *
+ * Note that the timestamp of sync point has usually a nonzero
+ * dts_ref_dts_delta, which refers to the previous sync point. Offset of
+ * the next frame after timestamp sync point will be usually 1.
+ *
+ * For example, this corresponds to H.264 cpb_removal_delay.
+ */
+ int dts_ref_dts_delta;
+
+ /**
+ * Presentation delay of current frame in units of AVCodecContext.time_base.
+ *
+ * Set to INT_MIN when dts_sync_point unused. Otherwise, it must
+ * contain valid non-negative timestamp delta (presentation time of a frame
+ * must not lie in the past).
+ *
+ * This delay represents the difference between decoding and presentation
+ * time of the frame.
+ *
+ * For example, this corresponds to H.264 dpb_output_delay.
+ */
+ int pts_dts_delta;
+
+ /**
+ * Position of the packet in file.
+ *
+ * Analogous to cur_frame_pts/dts
+ */
+ int64_t cur_frame_pos[AV_PARSER_PTS_NB];
+
+ /**
+ * Byte position of currently parsed frame in stream.
+ */
+ int64_t pos;
+
+ /**
+ * Previous frame byte position.
+ */
+ int64_t last_pos;
+
+ /**
+ * Duration of the current frame.
+ * For audio, this is in units of 1 / AVCodecContext.sample_rate.
+ * For all other types, this is in units of AVCodecContext.time_base.
+ */
+ int duration;
+
+ enum AVFieldOrder field_order;
+
+ /**
+ * Indicate whether a picture is coded as a frame, top field or bottom field.
+ *
+ * For example, H.264 field_pic_flag equal to 0 corresponds to
+ * AV_PICTURE_STRUCTURE_FRAME. An H.264 picture with field_pic_flag
+ * equal to 1 and bottom_field_flag equal to 0 corresponds to
+ * AV_PICTURE_STRUCTURE_TOP_FIELD.
+ */
+ enum AVPictureStructure picture_structure;
+
+ /**
+ * Picture number incremented in presentation or output order.
+ * This field may be reinitialized at the first picture of a new sequence.
+ *
+ * For example, this corresponds to H.264 PicOrderCnt.
+ */
+ int output_picture_number;
+
+ /**
+ * Dimensions of the decoded video intended for presentation.
+ */
+ int width;
+ int height;
+
+ /**
+ * Dimensions of the coded video.
+ */
+ int coded_width;
+ int coded_height;
+
+ /**
+ * The format of the coded data, corresponds to enum AVPixelFormat for video
+ * and for enum AVSampleFormat for audio.
+ *
+ * Note that a decoder can have considerable freedom in how exactly it
+ * decodes the data, so the format reported here might be different from the
+ * one returned by a decoder.
+ */
+ int format;
+} AVCodecParserContext;
+
+typedef struct AVCodecParser {
+ int codec_ids[7]; /* several codec IDs are permitted */
+ int priv_data_size;
+ int (*parser_init)(AVCodecParserContext* s);
+ /* This callback never returns an error, a negative value means that
+ * the frame start was in a previous packet. */
+ int (*parser_parse)(AVCodecParserContext* s, AVCodecContext* avctx,
+ const uint8_t** poutbuf, int* poutbuf_size,
+ const uint8_t* buf, int buf_size);
+ void (*parser_close)(AVCodecParserContext* s);
+ int (*split)(AVCodecContext* avctx, const uint8_t* buf, int buf_size);
+} AVCodecParser;
+
+/**
+ * Iterate over all registered codec parsers.
+ *
+ * @param opaque a pointer where libavcodec will store the iteration state. Must
+ * point to NULL to start the iteration.
+ *
+ * @return the next registered codec parser or NULL when the iteration is
+ * finished
+ */
+const AVCodecParser* av_parser_iterate(void** opaque);
+
+AVCodecParserContext* av_parser_init(int codec_id);
+
+/**
+ * Parse a packet.
+ *
+ * @param s parser context.
+ * @param avctx codec context.
+ * @param poutbuf set to pointer to parsed buffer or NULL if not yet
+ finished.
+ * @param poutbuf_size set to size of parsed buffer or zero if not yet
+ finished.
+ * @param buf input buffer.
+ * @param buf_size buffer size in bytes without the padding. I.e. the full
+ buffer size is assumed to be buf_size + AV_INPUT_BUFFER_PADDING_SIZE. To signal
+ EOF, this should be 0 (so that the last frame can be output).
+ * @param pts input presentation timestamp.
+ * @param dts input decoding timestamp.
+ * @param pos input byte position in stream.
+ * @return the number of bytes of the input bitstream used.
+ *
+ * Example:
+ * @code
+ * while(in_len){
+ * len = av_parser_parse2(myparser, AVCodecContext, &data, &size,
+ * in_data, in_len,
+ * pts, dts, pos);
+ * in_data += len;
+ * in_len -= len;
+ *
+ * if(size)
+ * decode_frame(data, size);
+ * }
+ * @endcode
+ */
+int av_parser_parse2(AVCodecParserContext* s, AVCodecContext* avctx,
+ uint8_t** poutbuf, int* poutbuf_size, const uint8_t* buf,
+ int buf_size, int64_t pts, int64_t dts, int64_t pos);
+
+void av_parser_close(AVCodecParserContext* s);
+
+/**
+ * @}
+ * @}
+ */
+
+/**
+ * @addtogroup lavc_encoding
+ * @{
+ */
+
+int avcodec_encode_subtitle(AVCodecContext* avctx, uint8_t* buf, int buf_size,
+ const AVSubtitle* sub);
+
+/**
+ * @}
+ */
+
+/**
+ * @defgroup lavc_misc Utility functions
+ * @ingroup libavc
+ *
+ * Miscellaneous utility functions related to both encoding and decoding
+ * (or neither).
+ * @{
+ */
+
+/**
+ * @defgroup lavc_misc_pixfmt Pixel formats
+ *
+ * Functions for working with pixel formats.
+ * @{
+ */
+
+/**
+ * Return a value representing the fourCC code associated to the
+ * pixel format pix_fmt, or 0 if no associated fourCC code can be
+ * found.
+ */
+unsigned int avcodec_pix_fmt_to_codec_tag(enum AVPixelFormat pix_fmt);
+
+/**
+ * Find the best pixel format to convert to given a certain source pixel
+ * format. When converting from one pixel format to another, information loss
+ * may occur. For example, when converting from RGB24 to GRAY, the color
+ * information will be lost. Similarly, other losses occur when converting from
+ * some formats to other formats. avcodec_find_best_pix_fmt_of_2() searches
+ * which of the given pixel formats should be used to suffer the least amount of
+ * loss. The pixel formats from which it chooses one, are determined by the
+ * pix_fmt_list parameter.
+ *
+ *
+ * @param[in] pix_fmt_list AV_PIX_FMT_NONE terminated array of pixel formats to
+ * choose from
+ * @param[in] src_pix_fmt source pixel format
+ * @param[in] has_alpha Whether the source pixel format alpha channel is used.
+ * @param[out] loss_ptr Combination of flags informing you what kind of losses
+ * will occur.
+ * @return The best pixel format to convert to or -1 if none was found.
+ */
+enum AVPixelFormat avcodec_find_best_pix_fmt_of_list(
+ const enum AVPixelFormat* pix_fmt_list, enum AVPixelFormat src_pix_fmt,
+ int has_alpha, int* loss_ptr);
+
+enum AVPixelFormat avcodec_default_get_format(struct AVCodecContext* s,
+ const enum AVPixelFormat* fmt);
+
+/**
+ * @}
+ */
+
+void avcodec_string(char* buf, int buf_size, AVCodecContext* enc, int encode);
+
+int avcodec_default_execute(AVCodecContext* c,
+ int (*func)(AVCodecContext* c2, void* arg2),
+ void* arg, int* ret, int count, int size);
+int avcodec_default_execute2(AVCodecContext* c,
+ int (*func)(AVCodecContext* c2, void* arg2, int,
+ int),
+ void* arg, int* ret, int count);
+// FIXME func typedef
+
+/**
+ * Fill AVFrame audio data and linesize pointers.
+ *
+ * The buffer buf must be a preallocated buffer with a size big enough
+ * to contain the specified samples amount. The filled AVFrame data
+ * pointers will point to this buffer.
+ *
+ * AVFrame extended_data channel pointers are allocated if necessary for
+ * planar audio.
+ *
+ * @param frame the AVFrame
+ * frame->nb_samples must be set prior to calling the
+ * function. This function fills in frame->data,
+ * frame->extended_data, frame->linesize[0].
+ * @param nb_channels channel count
+ * @param sample_fmt sample format
+ * @param buf buffer to use for frame data
+ * @param buf_size size of buffer
+ * @param align plane size sample alignment (0 = default)
+ * @return >=0 on success, negative error code on failure
+ * @todo return the size in bytes required to store the samples in
+ * case of success, at the next libavutil bump
+ */
+int avcodec_fill_audio_frame(AVFrame* frame, int nb_channels,
+ enum AVSampleFormat sample_fmt, const uint8_t* buf,
+ int buf_size, int align);
+
+/**
+ * Reset the internal codec state / flush internal buffers. Should be called
+ * e.g. when seeking or when switching to a different stream.
+ *
+ * @note for decoders, this function just releases any references the decoder
+ * might keep internally, but the caller's references remain valid.
+ *
+ * @note for encoders, this function will only do something if the encoder
+ * declares support for AV_CODEC_CAP_ENCODER_FLUSH. When called, the encoder
+ * will drain any remaining packets, and can then be re-used for a different
+ * stream (as opposed to sending a null frame which will leave the encoder
+ * in a permanent EOF state after draining). This can be desirable if the
+ * cost of tearing down and replacing the encoder instance is high.
+ */
+void avcodec_flush_buffers(AVCodecContext* avctx);
+
+/**
+ * Return audio frame duration.
+ *
+ * @param avctx codec context
+ * @param frame_bytes size of the frame, or 0 if unknown
+ * @return frame duration, in samples, if known. 0 if not able to
+ * determine.
+ */
+int av_get_audio_frame_duration(AVCodecContext* avctx, int frame_bytes);
+
+/* memory */
+
+/**
+ * Same behaviour av_fast_malloc but the buffer has additional
+ * AV_INPUT_BUFFER_PADDING_SIZE at the end which will always be 0.
+ *
+ * In addition the whole buffer will initially and after resizes
+ * be 0-initialized so that no uninitialized data will ever appear.
+ */
+void av_fast_padded_malloc(void* ptr, unsigned int* size, size_t min_size);
+
+/**
+ * Same behaviour av_fast_padded_malloc except that buffer will always
+ * be 0-initialized after call.
+ */
+void av_fast_padded_mallocz(void* ptr, unsigned int* size, size_t min_size);
+
+/**
+ * @return a positive value if s is open (i.e. avcodec_open2() was called on it
+ * with no corresponding avcodec_close()), 0 otherwise.
+ */
+int avcodec_is_open(AVCodecContext* s);
+
+/**
+ * @}
+ */
+
+#endif /* AVCODEC_AVCODEC_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/avfft.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/avfft.h
new file mode 100644
index 0000000000..115370eb48
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/avfft.h
@@ -0,0 +1,119 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_AVFFT_H
+#define AVCODEC_AVFFT_H
+
+/**
+ * @file
+ * @ingroup lavc_fft
+ * FFT functions
+ */
+
+/**
+ * @defgroup lavc_fft FFT functions
+ * @ingroup lavc_misc
+ *
+ * @{
+ */
+
+typedef float FFTSample;
+
+typedef struct FFTComplex {
+ FFTSample re, im;
+} FFTComplex;
+
+typedef struct FFTContext FFTContext;
+
+/**
+ * Set up a complex FFT.
+ * @param nbits log2 of the length of the input array
+ * @param inverse if 0 perform the forward transform, if 1 perform the
+ * inverse
+ */
+FFTContext* av_fft_init(int nbits, int inverse);
+
+/**
+ * Do the permutation needed BEFORE calling ff_fft_calc().
+ */
+void av_fft_permute(FFTContext* s, FFTComplex* z);
+
+/**
+ * Do a complex FFT with the parameters defined in av_fft_init(). The
+ * input data must be permuted before. No 1.0/sqrt(n) normalization is done.
+ */
+void av_fft_calc(FFTContext* s, FFTComplex* z);
+
+void av_fft_end(FFTContext* s);
+
+FFTContext* av_mdct_init(int nbits, int inverse, double scale);
+void av_imdct_calc(FFTContext* s, FFTSample* output, const FFTSample* input);
+void av_imdct_half(FFTContext* s, FFTSample* output, const FFTSample* input);
+void av_mdct_calc(FFTContext* s, FFTSample* output, const FFTSample* input);
+void av_mdct_end(FFTContext* s);
+
+/* Real Discrete Fourier Transform */
+
+enum RDFTransformType {
+ DFT_R2C,
+ IDFT_C2R,
+ IDFT_R2C,
+ DFT_C2R,
+};
+
+typedef struct RDFTContext RDFTContext;
+
+/**
+ * Set up a real FFT.
+ * @param nbits log2 of the length of the input array
+ * @param trans the type of transform
+ */
+RDFTContext* av_rdft_init(int nbits, enum RDFTransformType trans);
+void av_rdft_calc(RDFTContext* s, FFTSample* data);
+void av_rdft_end(RDFTContext* s);
+
+/* Discrete Cosine Transform */
+
+typedef struct DCTContext DCTContext;
+
+enum DCTTransformType {
+ DCT_II = 0,
+ DCT_III,
+ DCT_I,
+ DST_I,
+};
+
+/**
+ * Set up DCT.
+ *
+ * @param nbits size of the input array:
+ * (1 << nbits) for DCT-II, DCT-III and DST-I
+ * (1 << nbits) + 1 for DCT-I
+ * @param type the type of transform
+ *
+ * @note the first element of the input of DST-I is ignored
+ */
+DCTContext* av_dct_init(int nbits, enum DCTTransformType type);
+void av_dct_calc(DCTContext* s, FFTSample* data);
+void av_dct_end(DCTContext* s);
+
+/**
+ * @}
+ */
+
+#endif /* AVCODEC_AVFFT_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/bsf.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/bsf.h
new file mode 100644
index 0000000000..f88089f89f
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/bsf.h
@@ -0,0 +1,320 @@
+/*
+ * Bitstream filters public API
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_BSF_H
+#define AVCODEC_BSF_H
+
+#include "libavutil/dict.h"
+#include "libavutil/log.h"
+#include "libavutil/rational.h"
+
+#include "codec_id.h"
+#include "codec_par.h"
+#include "packet.h"
+
+/**
+ * @addtogroup lavc_core
+ * @{
+ */
+
+/**
+ * The bitstream filter state.
+ *
+ * This struct must be allocated with av_bsf_alloc() and freed with
+ * av_bsf_free().
+ *
+ * The fields in the struct will only be changed (by the caller or by the
+ * filter) as described in their documentation, and are to be considered
+ * immutable otherwise.
+ */
+typedef struct AVBSFContext {
+ /**
+ * A class for logging and AVOptions
+ */
+ const AVClass* av_class;
+
+ /**
+ * The bitstream filter this context is an instance of.
+ */
+ const struct AVBitStreamFilter* filter;
+
+ /**
+ * Opaque filter-specific private data. If filter->priv_class is non-NULL,
+ * this is an AVOptions-enabled struct.
+ */
+ void* priv_data;
+
+ /**
+ * Parameters of the input stream. This field is allocated in
+ * av_bsf_alloc(), it needs to be filled by the caller before
+ * av_bsf_init().
+ */
+ AVCodecParameters* par_in;
+
+ /**
+ * Parameters of the output stream. This field is allocated in
+ * av_bsf_alloc(), it is set by the filter in av_bsf_init().
+ */
+ AVCodecParameters* par_out;
+
+ /**
+ * The timebase used for the timestamps of the input packets. Set by the
+ * caller before av_bsf_init().
+ */
+ AVRational time_base_in;
+
+ /**
+ * The timebase used for the timestamps of the output packets. Set by the
+ * filter in av_bsf_init().
+ */
+ AVRational time_base_out;
+} AVBSFContext;
+
+typedef struct AVBitStreamFilter {
+ const char* name;
+
+ /**
+ * A list of codec ids supported by the filter, terminated by
+ * AV_CODEC_ID_NONE.
+ * May be NULL, in that case the bitstream filter works with any codec id.
+ */
+ const enum AVCodecID* codec_ids;
+
+ /**
+ * A class for the private data, used to declare bitstream filter private
+ * AVOptions. This field is NULL for bitstream filters that do not declare
+ * any options.
+ *
+ * If this field is non-NULL, the first member of the filter private data
+ * must be a pointer to AVClass, which will be set by libavcodec generic
+ * code to this class.
+ */
+ const AVClass* priv_class;
+
+ /*****************************************************************
+ * No fields below this line are part of the public API. They
+ * may not be used outside of libavcodec and can be changed and
+ * removed at will.
+ * New public fields should be added right above.
+ *****************************************************************
+ */
+
+ int priv_data_size;
+ int (*init)(AVBSFContext* ctx);
+ int (*filter)(AVBSFContext* ctx, AVPacket* pkt);
+ void (*close)(AVBSFContext* ctx);
+ void (*flush)(AVBSFContext* ctx);
+} AVBitStreamFilter;
+
+/**
+ * @return a bitstream filter with the specified name or NULL if no such
+ * bitstream filter exists.
+ */
+const AVBitStreamFilter* av_bsf_get_by_name(const char* name);
+
+/**
+ * Iterate over all registered bitstream filters.
+ *
+ * @param opaque a pointer where libavcodec will store the iteration state. Must
+ * point to NULL to start the iteration.
+ *
+ * @return the next registered bitstream filter or NULL when the iteration is
+ * finished
+ */
+const AVBitStreamFilter* av_bsf_iterate(void** opaque);
+
+/**
+ * Allocate a context for a given bitstream filter. The caller must fill in the
+ * context parameters as described in the documentation and then call
+ * av_bsf_init() before sending any data to the filter.
+ *
+ * @param filter the filter for which to allocate an instance.
+ * @param ctx a pointer into which the pointer to the newly-allocated context
+ * will be written. It must be freed with av_bsf_free() after the
+ * filtering is done.
+ *
+ * @return 0 on success, a negative AVERROR code on failure
+ */
+int av_bsf_alloc(const AVBitStreamFilter* filter, AVBSFContext** ctx);
+
+/**
+ * Prepare the filter for use, after all the parameters and options have been
+ * set.
+ */
+int av_bsf_init(AVBSFContext* ctx);
+
+/**
+ * Submit a packet for filtering.
+ *
+ * After sending each packet, the filter must be completely drained by calling
+ * av_bsf_receive_packet() repeatedly until it returns AVERROR(EAGAIN) or
+ * AVERROR_EOF.
+ *
+ * @param pkt the packet to filter. The bitstream filter will take ownership of
+ * the packet and reset the contents of pkt. pkt is not touched if an error
+ * occurs. If pkt is empty (i.e. NULL, or pkt->data is NULL and
+ * pkt->side_data_elems zero), it signals the end of the stream (i.e. no more
+ * non-empty packets will be sent; sending more empty packets does nothing) and
+ * will cause the filter to output any packets it may have buffered internally.
+ *
+ * @return 0 on success. AVERROR(EAGAIN) if packets need to be retrieved from
+ * the filter (using av_bsf_receive_packet()) before new input can be consumed.
+ * Another negative AVERROR value if an error occurs.
+ */
+int av_bsf_send_packet(AVBSFContext* ctx, AVPacket* pkt);
+
+/**
+ * Retrieve a filtered packet.
+ *
+ * @param[out] pkt this struct will be filled with the contents of the filtered
+ * packet. It is owned by the caller and must be freed using
+ * av_packet_unref() when it is no longer needed.
+ * This parameter should be "clean" (i.e. freshly allocated
+ * with av_packet_alloc() or unreffed with av_packet_unref())
+ * when this function is called. If this function returns
+ * successfully, the contents of pkt will be completely
+ * overwritten by the returned data. On failure, pkt is not
+ * touched.
+ *
+ * @return 0 on success. AVERROR(EAGAIN) if more packets need to be sent to the
+ * filter (using av_bsf_send_packet()) to get more output. AVERROR_EOF if there
+ * will be no further output from the filter. Another negative AVERROR value if
+ * an error occurs.
+ *
+ * @note one input packet may result in several output packets, so after sending
+ * a packet with av_bsf_send_packet(), this function needs to be called
+ * repeatedly until it stops returning 0. It is also possible for a filter to
+ * output fewer packets than were sent to it, so this function may return
+ * AVERROR(EAGAIN) immediately after a successful av_bsf_send_packet() call.
+ */
+int av_bsf_receive_packet(AVBSFContext* ctx, AVPacket* pkt);
+
+/**
+ * Reset the internal bitstream filter state. Should be called e.g. when
+ * seeking.
+ */
+void av_bsf_flush(AVBSFContext* ctx);
+
+/**
+ * Free a bitstream filter context and everything associated with it; write NULL
+ * into the supplied pointer.
+ */
+void av_bsf_free(AVBSFContext** ctx);
+
+/**
+ * Get the AVClass for AVBSFContext. It can be used in combination with
+ * AV_OPT_SEARCH_FAKE_OBJ for examining options.
+ *
+ * @see av_opt_find().
+ */
+const AVClass* av_bsf_get_class(void);
+
+/**
+ * Structure for chain/list of bitstream filters.
+ * Empty list can be allocated by av_bsf_list_alloc().
+ */
+typedef struct AVBSFList AVBSFList;
+
+/**
+ * Allocate empty list of bitstream filters.
+ * The list must be later freed by av_bsf_list_free()
+ * or finalized by av_bsf_list_finalize().
+ *
+ * @return Pointer to @ref AVBSFList on success, NULL in case of failure
+ */
+AVBSFList* av_bsf_list_alloc(void);
+
+/**
+ * Free list of bitstream filters.
+ *
+ * @param lst Pointer to pointer returned by av_bsf_list_alloc()
+ */
+void av_bsf_list_free(AVBSFList** lst);
+
+/**
+ * Append bitstream filter to the list of bitstream filters.
+ *
+ * @param lst List to append to
+ * @param bsf Filter context to be appended
+ *
+ * @return >=0 on success, negative AVERROR in case of failure
+ */
+int av_bsf_list_append(AVBSFList* lst, AVBSFContext* bsf);
+
+/**
+ * Construct new bitstream filter context given it's name and options
+ * and append it to the list of bitstream filters.
+ *
+ * @param lst List to append to
+ * @param bsf_name Name of the bitstream filter
+ * @param options Options for the bitstream filter, can be set to NULL
+ *
+ * @return >=0 on success, negative AVERROR in case of failure
+ */
+int av_bsf_list_append2(AVBSFList* lst, const char* bsf_name,
+ AVDictionary** options);
+/**
+ * Finalize list of bitstream filters.
+ *
+ * This function will transform @ref AVBSFList to single @ref AVBSFContext,
+ * so the whole chain of bitstream filters can be treated as single filter
+ * freshly allocated by av_bsf_alloc().
+ * If the call is successful, @ref AVBSFList structure is freed and lst
+ * will be set to NULL. In case of failure, caller is responsible for
+ * freeing the structure by av_bsf_list_free()
+ *
+ * @param lst Filter list structure to be transformed
+ * @param[out] bsf Pointer to be set to newly created @ref AVBSFContext
+ * structure representing the chain of bitstream filters
+ *
+ * @return >=0 on success, negative AVERROR in case of failure
+ */
+int av_bsf_list_finalize(AVBSFList** lst, AVBSFContext** bsf);
+
+/**
+ * Parse string describing list of bitstream filters and create single
+ * @ref AVBSFContext describing the whole chain of bitstream filters.
+ * Resulting @ref AVBSFContext can be treated as any other @ref AVBSFContext
+ * freshly allocated by av_bsf_alloc().
+ *
+ * @param str String describing chain of bitstream filters in format
+ * `bsf1[=opt1=val1:opt2=val2][,bsf2]`
+ * @param[out] bsf Pointer to be set to newly created @ref AVBSFContext
+ * structure representing the chain of bitstream filters
+ *
+ * @return >=0 on success, negative AVERROR in case of failure
+ */
+int av_bsf_list_parse_str(const char* str, AVBSFContext** bsf);
+
+/**
+ * Get null/pass-through bitstream filter.
+ *
+ * @param[out] bsf Pointer to be set to new instance of pass-through bitstream
+ * filter
+ *
+ * @return
+ */
+int av_bsf_get_null_filter(AVBSFContext** bsf);
+
+/**
+ * @}
+ */
+
+#endif // AVCODEC_BSF_H
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/codec.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/codec.h
new file mode 100644
index 0000000000..7b0dbddcf9
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/codec.h
@@ -0,0 +1,513 @@
+/*
+ * AVCodec public API
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_CODEC_H
+#define AVCODEC_CODEC_H
+
+#include <stdint.h>
+
+#include "libavutil/avutil.h"
+#include "libavutil/hwcontext.h"
+#include "libavutil/log.h"
+#include "libavutil/pixfmt.h"
+#include "libavutil/rational.h"
+#include "libavutil/samplefmt.h"
+
+#include "libavcodec/codec_id.h"
+#include "libavcodec/version.h"
+
+/**
+ * @addtogroup lavc_core
+ * @{
+ */
+
+/**
+ * Decoder can use draw_horiz_band callback.
+ */
+#define AV_CODEC_CAP_DRAW_HORIZ_BAND (1 << 0)
+/**
+ * Codec uses get_buffer() or get_encode_buffer() for allocating buffers and
+ * supports custom allocators.
+ * If not set, it might not use get_buffer() or get_encode_buffer() at all, or
+ * use operations that assume the buffer was allocated by
+ * avcodec_default_get_buffer2 or avcodec_default_get_encode_buffer.
+ */
+#define AV_CODEC_CAP_DR1 (1 << 1)
+#if FF_API_FLAG_TRUNCATED
+/**
+ * @deprecated Use parsers to always send proper frames.
+ */
+# define AV_CODEC_CAP_TRUNCATED (1 << 3)
+#endif
+/**
+ * Encoder or decoder requires flushing with NULL input at the end in order to
+ * give the complete and correct output.
+ *
+ * NOTE: If this flag is not set, the codec is guaranteed to never be fed with
+ * with NULL data. The user can still send NULL data to the public encode
+ * or decode function, but libavcodec will not pass it along to the codec
+ * unless this flag is set.
+ *
+ * Decoders:
+ * The decoder has a non-zero delay and needs to be fed with avpkt->data=NULL,
+ * avpkt->size=0 at the end to get the delayed data until the decoder no longer
+ * returns frames.
+ *
+ * Encoders:
+ * The encoder needs to be fed with NULL data at the end of encoding until the
+ * encoder no longer returns data.
+ *
+ * NOTE: For encoders implementing the AVCodec.encode2() function, setting this
+ * flag also means that the encoder must set the pts and duration for
+ * each output packet. If this flag is not set, the pts and duration will
+ * be determined by libavcodec from the input frame.
+ */
+#define AV_CODEC_CAP_DELAY (1 << 5)
+/**
+ * Codec can be fed a final frame with a smaller size.
+ * This can be used to prevent truncation of the last audio samples.
+ */
+#define AV_CODEC_CAP_SMALL_LAST_FRAME (1 << 6)
+
+/**
+ * Codec can output multiple frames per AVPacket
+ * Normally demuxers return one frame at a time, demuxers which do not do
+ * are connected to a parser to split what they return into proper frames.
+ * This flag is reserved to the very rare category of codecs which have a
+ * bitstream that cannot be split into frames without timeconsuming
+ * operations like full decoding. Demuxers carrying such bitstreams thus
+ * may return multiple frames in a packet. This has many disadvantages like
+ * prohibiting stream copy in many cases thus it should only be considered
+ * as a last resort.
+ */
+#define AV_CODEC_CAP_SUBFRAMES (1 << 8)
+/**
+ * Codec is experimental and is thus avoided in favor of non experimental
+ * encoders
+ */
+#define AV_CODEC_CAP_EXPERIMENTAL (1 << 9)
+/**
+ * Codec should fill in channel configuration and samplerate instead of
+ * container
+ */
+#define AV_CODEC_CAP_CHANNEL_CONF (1 << 10)
+/**
+ * Codec supports frame-level multithreading.
+ */
+#define AV_CODEC_CAP_FRAME_THREADS (1 << 12)
+/**
+ * Codec supports slice-based (or partition-based) multithreading.
+ */
+#define AV_CODEC_CAP_SLICE_THREADS (1 << 13)
+/**
+ * Codec supports changed parameters at any point.
+ */
+#define AV_CODEC_CAP_PARAM_CHANGE (1 << 14)
+/**
+ * Codec supports multithreading through a method other than slice- or
+ * frame-level multithreading. Typically this marks wrappers around
+ * multithreading-capable external libraries.
+ */
+#define AV_CODEC_CAP_OTHER_THREADS (1 << 15)
+#if FF_API_AUTO_THREADS
+# define AV_CODEC_CAP_AUTO_THREADS AV_CODEC_CAP_OTHER_THREADS
+#endif
+/**
+ * Audio encoder supports receiving a different number of samples in each call.
+ */
+#define AV_CODEC_CAP_VARIABLE_FRAME_SIZE (1 << 16)
+/**
+ * Decoder is not a preferred choice for probing.
+ * This indicates that the decoder is not a good choice for probing.
+ * It could for example be an expensive to spin up hardware decoder,
+ * or it could simply not provide a lot of useful information about
+ * the stream.
+ * A decoder marked with this flag should only be used as last resort
+ * choice for probing.
+ */
+#define AV_CODEC_CAP_AVOID_PROBING (1 << 17)
+
+#if FF_API_UNUSED_CODEC_CAPS
+/**
+ * Deprecated and unused. Use AVCodecDescriptor.props instead
+ */
+# define AV_CODEC_CAP_INTRA_ONLY 0x40000000
+/**
+ * Deprecated and unused. Use AVCodecDescriptor.props instead
+ */
+# define AV_CODEC_CAP_LOSSLESS 0x80000000
+#endif
+
+/**
+ * Codec is backed by a hardware implementation. Typically used to
+ * identify a non-hwaccel hardware decoder. For information about hwaccels, use
+ * avcodec_get_hw_config() instead.
+ */
+#define AV_CODEC_CAP_HARDWARE (1 << 18)
+
+/**
+ * Codec is potentially backed by a hardware implementation, but not
+ * necessarily. This is used instead of AV_CODEC_CAP_HARDWARE, if the
+ * implementation provides some sort of internal fallback.
+ */
+#define AV_CODEC_CAP_HYBRID (1 << 19)
+
+/**
+ * This codec takes the reordered_opaque field from input AVFrames
+ * and returns it in the corresponding field in AVCodecContext after
+ * encoding.
+ */
+#define AV_CODEC_CAP_ENCODER_REORDERED_OPAQUE (1 << 20)
+
+/**
+ * This encoder can be flushed using avcodec_flush_buffers(). If this flag is
+ * not set, the encoder must be closed and reopened to ensure that no frames
+ * remain pending.
+ */
+#define AV_CODEC_CAP_ENCODER_FLUSH (1 << 21)
+
+/**
+ * AVProfile.
+ */
+typedef struct AVProfile {
+ int profile;
+ const char* name; ///< short name for the profile
+} AVProfile;
+
+typedef struct AVCodecDefault AVCodecDefault;
+
+struct AVCodecContext;
+struct AVSubtitle;
+struct AVPacket;
+
+/**
+ * AVCodec.
+ */
+typedef struct AVCodec {
+ /**
+ * Name of the codec implementation.
+ * The name is globally unique among encoders and among decoders (but an
+ * encoder and a decoder can share the same name).
+ * This is the primary way to find a codec from the user perspective.
+ */
+ const char* name;
+ /**
+ * Descriptive name for the codec, meant to be more human readable than name.
+ * You should use the NULL_IF_CONFIG_SMALL() macro to define it.
+ */
+ const char* long_name;
+ enum AVMediaType type;
+ enum AVCodecID id;
+ /**
+ * Codec capabilities.
+ * see AV_CODEC_CAP_*
+ */
+ int capabilities;
+ uint8_t max_lowres; ///< maximum value for lowres supported by the decoder
+ const AVRational*
+ supported_framerates; ///< array of supported framerates, or NULL if any,
+ ///< array is terminated by {0,0}
+ const enum AVPixelFormat*
+ pix_fmts; ///< array of supported pixel formats, or NULL if unknown,
+ ///< array is terminated by -1
+ const int*
+ supported_samplerates; ///< array of supported audio samplerates, or NULL
+ ///< if unknown, array is terminated by 0
+ const enum AVSampleFormat*
+ sample_fmts; ///< array of supported sample formats, or NULL if unknown,
+ ///< array is terminated by -1
+ const uint64_t*
+ channel_layouts; ///< array of support channel layouts, or NULL if
+ ///< unknown. array is terminated by 0
+ const AVClass* priv_class; ///< AVClass for the private context
+ const AVProfile*
+ profiles; ///< array of recognized profiles, or NULL if unknown, array is
+ ///< terminated by {FF_PROFILE_UNKNOWN}
+
+ /**
+ * Group name of the codec implementation.
+ * This is a short symbolic name of the wrapper backing this codec. A
+ * wrapper uses some kind of external implementation for the codec, such
+ * as an external library, or a codec implementation provided by the OS or
+ * the hardware.
+ * If this field is NULL, this is a builtin, libavcodec native codec.
+ * If non-NULL, this will be the suffix in AVCodec.name in most cases
+ * (usually AVCodec.name will be of the form "<codec_name>_<wrapper_name>").
+ */
+ const char* wrapper_name;
+
+ /*****************************************************************
+ * No fields below this line are part of the public API. They
+ * may not be used outside of libavcodec and can be changed and
+ * removed at will.
+ * New public fields should be added right above.
+ *****************************************************************
+ */
+ /**
+ * Internal codec capabilities.
+ * See FF_CODEC_CAP_* in internal.h
+ */
+ int caps_internal;
+
+ int priv_data_size;
+ /**
+ * @name Frame-level threading support functions
+ * @{
+ */
+ /**
+ * Copy necessary context variables from a previous thread context to the
+ * current one. If not defined, the next thread will start automatically;
+ * otherwise, the codec must call ff_thread_finish_setup().
+ *
+ * dst and src will (rarely) point to the same context, in which case memcpy
+ * should be skipped.
+ */
+ int (*update_thread_context)(struct AVCodecContext* dst,
+ const struct AVCodecContext* src);
+
+ /**
+ * Copy variables back to the user-facing context
+ */
+ int (*update_thread_context_for_user)(struct AVCodecContext* dst,
+ const struct AVCodecContext* src);
+ /** @} */
+
+ /**
+ * Private codec-specific defaults.
+ */
+ const AVCodecDefault* defaults;
+
+ /**
+ * Initialize codec static data, called from av_codec_iterate().
+ *
+ * This is not intended for time consuming operations as it is
+ * run for every codec regardless of that codec being used.
+ */
+ void (*init_static_data)(struct AVCodec* codec);
+
+ int (*init)(struct AVCodecContext*);
+ int (*encode_sub)(struct AVCodecContext*, uint8_t* buf, int buf_size,
+ const struct AVSubtitle* sub);
+ /**
+ * Encode data to an AVPacket.
+ *
+ * @param avctx codec context
+ * @param avpkt output AVPacket
+ * @param[in] frame AVFrame containing the raw data to be encoded
+ * @param[out] got_packet_ptr encoder sets to 0 or 1 to indicate that a
+ * non-empty packet was returned in avpkt.
+ * @return 0 on success, negative error code on failure
+ */
+ int (*encode2)(struct AVCodecContext* avctx, struct AVPacket* avpkt,
+ const struct AVFrame* frame, int* got_packet_ptr);
+ /**
+ * Decode picture or subtitle data.
+ *
+ * @param avctx codec context
+ * @param outdata codec type dependent output struct
+ * @param[out] got_frame_ptr decoder sets to 0 or 1 to indicate that a
+ * non-empty frame or subtitle was returned in
+ * outdata.
+ * @param[in] avpkt AVPacket containing the data to be decoded
+ * @return amount of bytes read from the packet on success, negative error
+ * code on failure
+ */
+ int (*decode)(struct AVCodecContext* avctx, void* outdata, int* got_frame_ptr,
+ struct AVPacket* avpkt);
+ int (*close)(struct AVCodecContext*);
+ /**
+ * Encode API with decoupled frame/packet dataflow. This function is called
+ * to get one output packet. It should call ff_encode_get_frame() to obtain
+ * input data.
+ */
+ int (*receive_packet)(struct AVCodecContext* avctx, struct AVPacket* avpkt);
+
+ /**
+ * Decode API with decoupled packet/frame dataflow. This function is called
+ * to get one output frame. It should call ff_decode_get_packet() to obtain
+ * input data.
+ */
+ int (*receive_frame)(struct AVCodecContext* avctx, struct AVFrame* frame);
+ /**
+ * Flush buffers.
+ * Will be called when seeking
+ */
+ void (*flush)(struct AVCodecContext*);
+
+ /**
+ * Decoding only, a comma-separated list of bitstream filters to apply to
+ * packets before decoding.
+ */
+ const char* bsfs;
+
+ /**
+ * Array of pointers to hardware configurations supported by the codec,
+ * or NULL if no hardware supported. The array is terminated by a NULL
+ * pointer.
+ *
+ * The user can only access this field via avcodec_get_hw_config().
+ */
+ const struct AVCodecHWConfigInternal* const* hw_configs;
+
+ /**
+ * List of supported codec_tags, terminated by FF_CODEC_TAGS_END.
+ */
+ const uint32_t* codec_tags;
+} AVCodec;
+
+/**
+ * Iterate over all registered codecs.
+ *
+ * @param opaque a pointer where libavcodec will store the iteration state. Must
+ * point to NULL to start the iteration.
+ *
+ * @return the next registered codec or NULL when the iteration is
+ * finished
+ */
+const AVCodec* av_codec_iterate(void** opaque);
+
+/**
+ * Find a registered decoder with a matching codec ID.
+ *
+ * @param id AVCodecID of the requested decoder
+ * @return A decoder if one was found, NULL otherwise.
+ */
+const AVCodec* avcodec_find_decoder(enum AVCodecID id);
+
+/**
+ * Find a registered decoder with the specified name.
+ *
+ * @param name name of the requested decoder
+ * @return A decoder if one was found, NULL otherwise.
+ */
+const AVCodec* avcodec_find_decoder_by_name(const char* name);
+
+/**
+ * Find a registered encoder with a matching codec ID.
+ *
+ * @param id AVCodecID of the requested encoder
+ * @return An encoder if one was found, NULL otherwise.
+ */
+const AVCodec* avcodec_find_encoder(enum AVCodecID id);
+
+/**
+ * Find a registered encoder with the specified name.
+ *
+ * @param name name of the requested encoder
+ * @return An encoder if one was found, NULL otherwise.
+ */
+const AVCodec* avcodec_find_encoder_by_name(const char* name);
+/**
+ * @return a non-zero number if codec is an encoder, zero otherwise
+ */
+int av_codec_is_encoder(const AVCodec* codec);
+
+/**
+ * @return a non-zero number if codec is a decoder, zero otherwise
+ */
+int av_codec_is_decoder(const AVCodec* codec);
+
+/**
+ * Return a name for the specified profile, if available.
+ *
+ * @param codec the codec that is searched for the given profile
+ * @param profile the profile value for which a name is requested
+ * @return A name for the profile if found, NULL otherwise.
+ */
+const char* av_get_profile_name(const AVCodec* codec, int profile);
+
+enum {
+ /**
+ * The codec supports this format via the hw_device_ctx interface.
+ *
+ * When selecting this format, AVCodecContext.hw_device_ctx should
+ * have been set to a device of the specified type before calling
+ * avcodec_open2().
+ */
+ AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX = 0x01,
+ /**
+ * The codec supports this format via the hw_frames_ctx interface.
+ *
+ * When selecting this format for a decoder,
+ * AVCodecContext.hw_frames_ctx should be set to a suitable frames
+ * context inside the get_format() callback. The frames context
+ * must have been created on a device of the specified type.
+ *
+ * When selecting this format for an encoder,
+ * AVCodecContext.hw_frames_ctx should be set to the context which
+ * will be used for the input frames before calling avcodec_open2().
+ */
+ AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX = 0x02,
+ /**
+ * The codec supports this format by some internal method.
+ *
+ * This format can be selected without any additional configuration -
+ * no device or frames context is required.
+ */
+ AV_CODEC_HW_CONFIG_METHOD_INTERNAL = 0x04,
+ /**
+ * The codec supports this format by some ad-hoc method.
+ *
+ * Additional settings and/or function calls are required. See the
+ * codec-specific documentation for details. (Methods requiring
+ * this sort of configuration are deprecated and others should be
+ * used in preference.)
+ */
+ AV_CODEC_HW_CONFIG_METHOD_AD_HOC = 0x08,
+};
+
+typedef struct AVCodecHWConfig {
+ /**
+ * For decoders, a hardware pixel format which that decoder may be
+ * able to decode to if suitable hardware is available.
+ *
+ * For encoders, a pixel format which the encoder may be able to
+ * accept. If set to AV_PIX_FMT_NONE, this applies to all pixel
+ * formats supported by the codec.
+ */
+ enum AVPixelFormat pix_fmt;
+ /**
+ * Bit set of AV_CODEC_HW_CONFIG_METHOD_* flags, describing the possible
+ * setup methods which can be used with this configuration.
+ */
+ int methods;
+ /**
+ * The device type associated with the configuration.
+ *
+ * Must be set for AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX and
+ * AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX, otherwise unused.
+ */
+ enum AVHWDeviceType device_type;
+} AVCodecHWConfig;
+
+/**
+ * Retrieve supported hardware configurations for a codec.
+ *
+ * Values of index from zero to some maximum return the indexed configuration
+ * descriptor; all other values return NULL. If the codec does not support
+ * any hardware configurations then it will always return NULL.
+ */
+const AVCodecHWConfig* avcodec_get_hw_config(const AVCodec* codec, int index);
+
+/**
+ * @}
+ */
+
+#endif /* AVCODEC_CODEC_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/codec_desc.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/codec_desc.h
new file mode 100644
index 0000000000..0884491d3e
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/codec_desc.h
@@ -0,0 +1,128 @@
+/*
+ * Codec descriptors public API
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_CODEC_DESC_H
+#define AVCODEC_CODEC_DESC_H
+
+#include "libavutil/avutil.h"
+
+#include "codec_id.h"
+
+/**
+ * @addtogroup lavc_core
+ * @{
+ */
+
+/**
+ * This struct describes the properties of a single codec described by an
+ * AVCodecID.
+ * @see avcodec_descriptor_get()
+ */
+typedef struct AVCodecDescriptor {
+ enum AVCodecID id;
+ enum AVMediaType type;
+ /**
+ * Name of the codec described by this descriptor. It is non-empty and
+ * unique for each codec descriptor. It should contain alphanumeric
+ * characters and '_' only.
+ */
+ const char* name;
+ /**
+ * A more descriptive name for this codec. May be NULL.
+ */
+ const char* long_name;
+ /**
+ * Codec properties, a combination of AV_CODEC_PROP_* flags.
+ */
+ int props;
+ /**
+ * MIME type(s) associated with the codec.
+ * May be NULL; if not, a NULL-terminated array of MIME types.
+ * The first item is always non-NULL and is the preferred MIME type.
+ */
+ const char* const* mime_types;
+ /**
+ * If non-NULL, an array of profiles recognized for this codec.
+ * Terminated with FF_PROFILE_UNKNOWN.
+ */
+ const struct AVProfile* profiles;
+} AVCodecDescriptor;
+
+/**
+ * Codec uses only intra compression.
+ * Video and audio codecs only.
+ */
+#define AV_CODEC_PROP_INTRA_ONLY (1 << 0)
+/**
+ * Codec supports lossy compression. Audio and video codecs only.
+ * @note a codec may support both lossy and lossless
+ * compression modes
+ */
+#define AV_CODEC_PROP_LOSSY (1 << 1)
+/**
+ * Codec supports lossless compression. Audio and video codecs only.
+ */
+#define AV_CODEC_PROP_LOSSLESS (1 << 2)
+/**
+ * Codec supports frame reordering. That is, the coded order (the order in which
+ * the encoded packets are output by the encoders / stored / input to the
+ * decoders) may be different from the presentation order of the corresponding
+ * frames.
+ *
+ * For codecs that do not have this property set, PTS and DTS should always be
+ * equal.
+ */
+#define AV_CODEC_PROP_REORDER (1 << 3)
+/**
+ * Subtitle codec is bitmap based
+ * Decoded AVSubtitle data can be read from the AVSubtitleRect->pict field.
+ */
+#define AV_CODEC_PROP_BITMAP_SUB (1 << 16)
+/**
+ * Subtitle codec is text based.
+ * Decoded AVSubtitle data can be read from the AVSubtitleRect->ass field.
+ */
+#define AV_CODEC_PROP_TEXT_SUB (1 << 17)
+
+/**
+ * @return descriptor for given codec ID or NULL if no descriptor exists.
+ */
+const AVCodecDescriptor* avcodec_descriptor_get(enum AVCodecID id);
+
+/**
+ * Iterate over all codec descriptors known to libavcodec.
+ *
+ * @param prev previous descriptor. NULL to get the first descriptor.
+ *
+ * @return next descriptor or NULL after the last descriptor
+ */
+const AVCodecDescriptor* avcodec_descriptor_next(const AVCodecDescriptor* prev);
+
+/**
+ * @return codec descriptor with the given name or NULL if no such descriptor
+ * exists.
+ */
+const AVCodecDescriptor* avcodec_descriptor_get_by_name(const char* name);
+
+/**
+ * @}
+ */
+
+#endif // AVCODEC_CODEC_DESC_H
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/codec_id.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/codec_id.h
new file mode 100644
index 0000000000..020989b5c5
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/codec_id.h
@@ -0,0 +1,637 @@
+/*
+ * Codec IDs
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_CODEC_ID_H
+#define AVCODEC_CODEC_ID_H
+
+#include "libavutil/avutil.h"
+#include "libavutil/samplefmt.h"
+
+/**
+ * @addtogroup lavc_core
+ * @{
+ */
+
+/**
+ * Identify the syntax and semantics of the bitstream.
+ * The principle is roughly:
+ * Two decoders with the same ID can decode the same streams.
+ * Two encoders with the same ID can encode compatible streams.
+ * There may be slight deviations from the principle due to implementation
+ * details.
+ *
+ * If you add a codec ID to this list, add it so that
+ * 1. no value of an existing codec ID changes (that would break ABI),
+ * 2. it is as close as possible to similar codecs
+ *
+ * After adding new codec IDs, do not forget to add an entry to the codec
+ * descriptor list and bump libavcodec minor version.
+ */
+enum AVCodecID {
+ AV_CODEC_ID_NONE,
+
+ /* video codecs */
+ AV_CODEC_ID_MPEG1VIDEO,
+ AV_CODEC_ID_MPEG2VIDEO, ///< preferred ID for MPEG-1/2 video decoding
+ AV_CODEC_ID_H261,
+ AV_CODEC_ID_H263,
+ AV_CODEC_ID_RV10,
+ AV_CODEC_ID_RV20,
+ AV_CODEC_ID_MJPEG,
+ AV_CODEC_ID_MJPEGB,
+ AV_CODEC_ID_LJPEG,
+ AV_CODEC_ID_SP5X,
+ AV_CODEC_ID_JPEGLS,
+ AV_CODEC_ID_MPEG4,
+ AV_CODEC_ID_RAWVIDEO,
+ AV_CODEC_ID_MSMPEG4V1,
+ AV_CODEC_ID_MSMPEG4V2,
+ AV_CODEC_ID_MSMPEG4V3,
+ AV_CODEC_ID_WMV1,
+ AV_CODEC_ID_WMV2,
+ AV_CODEC_ID_H263P,
+ AV_CODEC_ID_H263I,
+ AV_CODEC_ID_FLV1,
+ AV_CODEC_ID_SVQ1,
+ AV_CODEC_ID_SVQ3,
+ AV_CODEC_ID_DVVIDEO,
+ AV_CODEC_ID_HUFFYUV,
+ AV_CODEC_ID_CYUV,
+ AV_CODEC_ID_H264,
+ AV_CODEC_ID_INDEO3,
+ AV_CODEC_ID_VP3,
+ AV_CODEC_ID_THEORA,
+ AV_CODEC_ID_ASV1,
+ AV_CODEC_ID_ASV2,
+ AV_CODEC_ID_FFV1,
+ AV_CODEC_ID_4XM,
+ AV_CODEC_ID_VCR1,
+ AV_CODEC_ID_CLJR,
+ AV_CODEC_ID_MDEC,
+ AV_CODEC_ID_ROQ,
+ AV_CODEC_ID_INTERPLAY_VIDEO,
+ AV_CODEC_ID_XAN_WC3,
+ AV_CODEC_ID_XAN_WC4,
+ AV_CODEC_ID_RPZA,
+ AV_CODEC_ID_CINEPAK,
+ AV_CODEC_ID_WS_VQA,
+ AV_CODEC_ID_MSRLE,
+ AV_CODEC_ID_MSVIDEO1,
+ AV_CODEC_ID_IDCIN,
+ AV_CODEC_ID_8BPS,
+ AV_CODEC_ID_SMC,
+ AV_CODEC_ID_FLIC,
+ AV_CODEC_ID_TRUEMOTION1,
+ AV_CODEC_ID_VMDVIDEO,
+ AV_CODEC_ID_MSZH,
+ AV_CODEC_ID_ZLIB,
+ AV_CODEC_ID_QTRLE,
+ AV_CODEC_ID_TSCC,
+ AV_CODEC_ID_ULTI,
+ AV_CODEC_ID_QDRAW,
+ AV_CODEC_ID_VIXL,
+ AV_CODEC_ID_QPEG,
+ AV_CODEC_ID_PNG,
+ AV_CODEC_ID_PPM,
+ AV_CODEC_ID_PBM,
+ AV_CODEC_ID_PGM,
+ AV_CODEC_ID_PGMYUV,
+ AV_CODEC_ID_PAM,
+ AV_CODEC_ID_FFVHUFF,
+ AV_CODEC_ID_RV30,
+ AV_CODEC_ID_RV40,
+ AV_CODEC_ID_VC1,
+ AV_CODEC_ID_WMV3,
+ AV_CODEC_ID_LOCO,
+ AV_CODEC_ID_WNV1,
+ AV_CODEC_ID_AASC,
+ AV_CODEC_ID_INDEO2,
+ AV_CODEC_ID_FRAPS,
+ AV_CODEC_ID_TRUEMOTION2,
+ AV_CODEC_ID_BMP,
+ AV_CODEC_ID_CSCD,
+ AV_CODEC_ID_MMVIDEO,
+ AV_CODEC_ID_ZMBV,
+ AV_CODEC_ID_AVS,
+ AV_CODEC_ID_SMACKVIDEO,
+ AV_CODEC_ID_NUV,
+ AV_CODEC_ID_KMVC,
+ AV_CODEC_ID_FLASHSV,
+ AV_CODEC_ID_CAVS,
+ AV_CODEC_ID_JPEG2000,
+ AV_CODEC_ID_VMNC,
+ AV_CODEC_ID_VP5,
+ AV_CODEC_ID_VP6,
+ AV_CODEC_ID_VP6F,
+ AV_CODEC_ID_TARGA,
+ AV_CODEC_ID_DSICINVIDEO,
+ AV_CODEC_ID_TIERTEXSEQVIDEO,
+ AV_CODEC_ID_TIFF,
+ AV_CODEC_ID_GIF,
+ AV_CODEC_ID_DXA,
+ AV_CODEC_ID_DNXHD,
+ AV_CODEC_ID_THP,
+ AV_CODEC_ID_SGI,
+ AV_CODEC_ID_C93,
+ AV_CODEC_ID_BETHSOFTVID,
+ AV_CODEC_ID_PTX,
+ AV_CODEC_ID_TXD,
+ AV_CODEC_ID_VP6A,
+ AV_CODEC_ID_AMV,
+ AV_CODEC_ID_VB,
+ AV_CODEC_ID_PCX,
+ AV_CODEC_ID_SUNRAST,
+ AV_CODEC_ID_INDEO4,
+ AV_CODEC_ID_INDEO5,
+ AV_CODEC_ID_MIMIC,
+ AV_CODEC_ID_RL2,
+ AV_CODEC_ID_ESCAPE124,
+ AV_CODEC_ID_DIRAC,
+ AV_CODEC_ID_BFI,
+ AV_CODEC_ID_CMV,
+ AV_CODEC_ID_MOTIONPIXELS,
+ AV_CODEC_ID_TGV,
+ AV_CODEC_ID_TGQ,
+ AV_CODEC_ID_TQI,
+ AV_CODEC_ID_AURA,
+ AV_CODEC_ID_AURA2,
+ AV_CODEC_ID_V210X,
+ AV_CODEC_ID_TMV,
+ AV_CODEC_ID_V210,
+ AV_CODEC_ID_DPX,
+ AV_CODEC_ID_MAD,
+ AV_CODEC_ID_FRWU,
+ AV_CODEC_ID_FLASHSV2,
+ AV_CODEC_ID_CDGRAPHICS,
+ AV_CODEC_ID_R210,
+ AV_CODEC_ID_ANM,
+ AV_CODEC_ID_BINKVIDEO,
+ AV_CODEC_ID_IFF_ILBM,
+#define AV_CODEC_ID_IFF_BYTERUN1 AV_CODEC_ID_IFF_ILBM
+ AV_CODEC_ID_KGV1,
+ AV_CODEC_ID_YOP,
+ AV_CODEC_ID_VP8,
+ AV_CODEC_ID_PICTOR,
+ AV_CODEC_ID_ANSI,
+ AV_CODEC_ID_A64_MULTI,
+ AV_CODEC_ID_A64_MULTI5,
+ AV_CODEC_ID_R10K,
+ AV_CODEC_ID_MXPEG,
+ AV_CODEC_ID_LAGARITH,
+ AV_CODEC_ID_PRORES,
+ AV_CODEC_ID_JV,
+ AV_CODEC_ID_DFA,
+ AV_CODEC_ID_WMV3IMAGE,
+ AV_CODEC_ID_VC1IMAGE,
+ AV_CODEC_ID_UTVIDEO,
+ AV_CODEC_ID_BMV_VIDEO,
+ AV_CODEC_ID_VBLE,
+ AV_CODEC_ID_DXTORY,
+ AV_CODEC_ID_V410,
+ AV_CODEC_ID_XWD,
+ AV_CODEC_ID_CDXL,
+ AV_CODEC_ID_XBM,
+ AV_CODEC_ID_ZEROCODEC,
+ AV_CODEC_ID_MSS1,
+ AV_CODEC_ID_MSA1,
+ AV_CODEC_ID_TSCC2,
+ AV_CODEC_ID_MTS2,
+ AV_CODEC_ID_CLLC,
+ AV_CODEC_ID_MSS2,
+ AV_CODEC_ID_VP9,
+ AV_CODEC_ID_AIC,
+ AV_CODEC_ID_ESCAPE130,
+ AV_CODEC_ID_G2M,
+ AV_CODEC_ID_WEBP,
+ AV_CODEC_ID_HNM4_VIDEO,
+ AV_CODEC_ID_HEVC,
+#define AV_CODEC_ID_H265 AV_CODEC_ID_HEVC
+ AV_CODEC_ID_FIC,
+ AV_CODEC_ID_ALIAS_PIX,
+ AV_CODEC_ID_BRENDER_PIX,
+ AV_CODEC_ID_PAF_VIDEO,
+ AV_CODEC_ID_EXR,
+ AV_CODEC_ID_VP7,
+ AV_CODEC_ID_SANM,
+ AV_CODEC_ID_SGIRLE,
+ AV_CODEC_ID_MVC1,
+ AV_CODEC_ID_MVC2,
+ AV_CODEC_ID_HQX,
+ AV_CODEC_ID_TDSC,
+ AV_CODEC_ID_HQ_HQA,
+ AV_CODEC_ID_HAP,
+ AV_CODEC_ID_DDS,
+ AV_CODEC_ID_DXV,
+ AV_CODEC_ID_SCREENPRESSO,
+ AV_CODEC_ID_RSCC,
+ AV_CODEC_ID_AVS2,
+ AV_CODEC_ID_PGX,
+ AV_CODEC_ID_AVS3,
+ AV_CODEC_ID_MSP2,
+ AV_CODEC_ID_VVC,
+#define AV_CODEC_ID_H266 AV_CODEC_ID_VVC
+ AV_CODEC_ID_Y41P,
+ AV_CODEC_ID_AVRP,
+ AV_CODEC_ID_012V,
+ AV_CODEC_ID_AVUI,
+ AV_CODEC_ID_AYUV,
+ AV_CODEC_ID_TARGA_Y216,
+ AV_CODEC_ID_V308,
+ AV_CODEC_ID_V408,
+ AV_CODEC_ID_YUV4,
+ AV_CODEC_ID_AVRN,
+ AV_CODEC_ID_CPIA,
+ AV_CODEC_ID_XFACE,
+ AV_CODEC_ID_SNOW,
+ AV_CODEC_ID_SMVJPEG,
+ AV_CODEC_ID_APNG,
+ AV_CODEC_ID_DAALA,
+ AV_CODEC_ID_CFHD,
+ AV_CODEC_ID_TRUEMOTION2RT,
+ AV_CODEC_ID_M101,
+ AV_CODEC_ID_MAGICYUV,
+ AV_CODEC_ID_SHEERVIDEO,
+ AV_CODEC_ID_YLC,
+ AV_CODEC_ID_PSD,
+ AV_CODEC_ID_PIXLET,
+ AV_CODEC_ID_SPEEDHQ,
+ AV_CODEC_ID_FMVC,
+ AV_CODEC_ID_SCPR,
+ AV_CODEC_ID_CLEARVIDEO,
+ AV_CODEC_ID_XPM,
+ AV_CODEC_ID_AV1,
+ AV_CODEC_ID_BITPACKED,
+ AV_CODEC_ID_MSCC,
+ AV_CODEC_ID_SRGC,
+ AV_CODEC_ID_SVG,
+ AV_CODEC_ID_GDV,
+ AV_CODEC_ID_FITS,
+ AV_CODEC_ID_IMM4,
+ AV_CODEC_ID_PROSUMER,
+ AV_CODEC_ID_MWSC,
+ AV_CODEC_ID_WCMV,
+ AV_CODEC_ID_RASC,
+ AV_CODEC_ID_HYMT,
+ AV_CODEC_ID_ARBC,
+ AV_CODEC_ID_AGM,
+ AV_CODEC_ID_LSCR,
+ AV_CODEC_ID_VP4,
+ AV_CODEC_ID_IMM5,
+ AV_CODEC_ID_MVDV,
+ AV_CODEC_ID_MVHA,
+ AV_CODEC_ID_CDTOONS,
+ AV_CODEC_ID_MV30,
+ AV_CODEC_ID_NOTCHLC,
+ AV_CODEC_ID_PFM,
+ AV_CODEC_ID_MOBICLIP,
+ AV_CODEC_ID_PHOTOCD,
+ AV_CODEC_ID_IPU,
+ AV_CODEC_ID_ARGO,
+ AV_CODEC_ID_CRI,
+ AV_CODEC_ID_SIMBIOSIS_IMX,
+ AV_CODEC_ID_SGA_VIDEO,
+ AV_CODEC_ID_GEM,
+
+ /* various PCM "codecs" */
+ AV_CODEC_ID_FIRST_AUDIO =
+ 0x10000, ///< A dummy id pointing at the start of audio codecs
+ AV_CODEC_ID_PCM_S16LE = 0x10000,
+ AV_CODEC_ID_PCM_S16BE,
+ AV_CODEC_ID_PCM_U16LE,
+ AV_CODEC_ID_PCM_U16BE,
+ AV_CODEC_ID_PCM_S8,
+ AV_CODEC_ID_PCM_U8,
+ AV_CODEC_ID_PCM_MULAW,
+ AV_CODEC_ID_PCM_ALAW,
+ AV_CODEC_ID_PCM_S32LE,
+ AV_CODEC_ID_PCM_S32BE,
+ AV_CODEC_ID_PCM_U32LE,
+ AV_CODEC_ID_PCM_U32BE,
+ AV_CODEC_ID_PCM_S24LE,
+ AV_CODEC_ID_PCM_S24BE,
+ AV_CODEC_ID_PCM_U24LE,
+ AV_CODEC_ID_PCM_U24BE,
+ AV_CODEC_ID_PCM_S24DAUD,
+ AV_CODEC_ID_PCM_ZORK,
+ AV_CODEC_ID_PCM_S16LE_PLANAR,
+ AV_CODEC_ID_PCM_DVD,
+ AV_CODEC_ID_PCM_F32BE,
+ AV_CODEC_ID_PCM_F32LE,
+ AV_CODEC_ID_PCM_F64BE,
+ AV_CODEC_ID_PCM_F64LE,
+ AV_CODEC_ID_PCM_BLURAY,
+ AV_CODEC_ID_PCM_LXF,
+ AV_CODEC_ID_S302M,
+ AV_CODEC_ID_PCM_S8_PLANAR,
+ AV_CODEC_ID_PCM_S24LE_PLANAR,
+ AV_CODEC_ID_PCM_S32LE_PLANAR,
+ AV_CODEC_ID_PCM_S16BE_PLANAR,
+ AV_CODEC_ID_PCM_S64LE,
+ AV_CODEC_ID_PCM_S64BE,
+ AV_CODEC_ID_PCM_F16LE,
+ AV_CODEC_ID_PCM_F24LE,
+ AV_CODEC_ID_PCM_VIDC,
+ AV_CODEC_ID_PCM_SGA,
+
+ /* various ADPCM codecs */
+ AV_CODEC_ID_ADPCM_IMA_QT = 0x11000,
+ AV_CODEC_ID_ADPCM_IMA_WAV,
+ AV_CODEC_ID_ADPCM_IMA_DK3,
+ AV_CODEC_ID_ADPCM_IMA_DK4,
+ AV_CODEC_ID_ADPCM_IMA_WS,
+ AV_CODEC_ID_ADPCM_IMA_SMJPEG,
+ AV_CODEC_ID_ADPCM_MS,
+ AV_CODEC_ID_ADPCM_4XM,
+ AV_CODEC_ID_ADPCM_XA,
+ AV_CODEC_ID_ADPCM_ADX,
+ AV_CODEC_ID_ADPCM_EA,
+ AV_CODEC_ID_ADPCM_G726,
+ AV_CODEC_ID_ADPCM_CT,
+ AV_CODEC_ID_ADPCM_SWF,
+ AV_CODEC_ID_ADPCM_YAMAHA,
+ AV_CODEC_ID_ADPCM_SBPRO_4,
+ AV_CODEC_ID_ADPCM_SBPRO_3,
+ AV_CODEC_ID_ADPCM_SBPRO_2,
+ AV_CODEC_ID_ADPCM_THP,
+ AV_CODEC_ID_ADPCM_IMA_AMV,
+ AV_CODEC_ID_ADPCM_EA_R1,
+ AV_CODEC_ID_ADPCM_EA_R3,
+ AV_CODEC_ID_ADPCM_EA_R2,
+ AV_CODEC_ID_ADPCM_IMA_EA_SEAD,
+ AV_CODEC_ID_ADPCM_IMA_EA_EACS,
+ AV_CODEC_ID_ADPCM_EA_XAS,
+ AV_CODEC_ID_ADPCM_EA_MAXIS_XA,
+ AV_CODEC_ID_ADPCM_IMA_ISS,
+ AV_CODEC_ID_ADPCM_G722,
+ AV_CODEC_ID_ADPCM_IMA_APC,
+ AV_CODEC_ID_ADPCM_VIMA,
+ AV_CODEC_ID_ADPCM_AFC,
+ AV_CODEC_ID_ADPCM_IMA_OKI,
+ AV_CODEC_ID_ADPCM_DTK,
+ AV_CODEC_ID_ADPCM_IMA_RAD,
+ AV_CODEC_ID_ADPCM_G726LE,
+ AV_CODEC_ID_ADPCM_THP_LE,
+ AV_CODEC_ID_ADPCM_PSX,
+ AV_CODEC_ID_ADPCM_AICA,
+ AV_CODEC_ID_ADPCM_IMA_DAT4,
+ AV_CODEC_ID_ADPCM_MTAF,
+ AV_CODEC_ID_ADPCM_AGM,
+ AV_CODEC_ID_ADPCM_ARGO,
+ AV_CODEC_ID_ADPCM_IMA_SSI,
+ AV_CODEC_ID_ADPCM_ZORK,
+ AV_CODEC_ID_ADPCM_IMA_APM,
+ AV_CODEC_ID_ADPCM_IMA_ALP,
+ AV_CODEC_ID_ADPCM_IMA_MTF,
+ AV_CODEC_ID_ADPCM_IMA_CUNNING,
+ AV_CODEC_ID_ADPCM_IMA_MOFLEX,
+ AV_CODEC_ID_ADPCM_IMA_ACORN,
+
+ /* AMR */
+ AV_CODEC_ID_AMR_NB = 0x12000,
+ AV_CODEC_ID_AMR_WB,
+
+ /* RealAudio codecs*/
+ AV_CODEC_ID_RA_144 = 0x13000,
+ AV_CODEC_ID_RA_288,
+
+ /* various DPCM codecs */
+ AV_CODEC_ID_ROQ_DPCM = 0x14000,
+ AV_CODEC_ID_INTERPLAY_DPCM,
+ AV_CODEC_ID_XAN_DPCM,
+ AV_CODEC_ID_SOL_DPCM,
+ AV_CODEC_ID_SDX2_DPCM,
+ AV_CODEC_ID_GREMLIN_DPCM,
+ AV_CODEC_ID_DERF_DPCM,
+
+ /* audio codecs */
+ AV_CODEC_ID_MP2 = 0x15000,
+ AV_CODEC_ID_MP3, ///< preferred ID for decoding MPEG audio layer 1, 2 or 3
+ AV_CODEC_ID_AAC,
+ AV_CODEC_ID_AC3,
+ AV_CODEC_ID_DTS,
+ AV_CODEC_ID_VORBIS,
+ AV_CODEC_ID_DVAUDIO,
+ AV_CODEC_ID_WMAV1,
+ AV_CODEC_ID_WMAV2,
+ AV_CODEC_ID_MACE3,
+ AV_CODEC_ID_MACE6,
+ AV_CODEC_ID_VMDAUDIO,
+ AV_CODEC_ID_FLAC,
+ AV_CODEC_ID_MP3ADU,
+ AV_CODEC_ID_MP3ON4,
+ AV_CODEC_ID_SHORTEN,
+ AV_CODEC_ID_ALAC,
+ AV_CODEC_ID_WESTWOOD_SND1,
+ AV_CODEC_ID_GSM, ///< as in Berlin toast format
+ AV_CODEC_ID_QDM2,
+ AV_CODEC_ID_COOK,
+ AV_CODEC_ID_TRUESPEECH,
+ AV_CODEC_ID_TTA,
+ AV_CODEC_ID_SMACKAUDIO,
+ AV_CODEC_ID_QCELP,
+ AV_CODEC_ID_WAVPACK,
+ AV_CODEC_ID_DSICINAUDIO,
+ AV_CODEC_ID_IMC,
+ AV_CODEC_ID_MUSEPACK7,
+ AV_CODEC_ID_MLP,
+ AV_CODEC_ID_GSM_MS, /* as found in WAV */
+ AV_CODEC_ID_ATRAC3,
+ AV_CODEC_ID_APE,
+ AV_CODEC_ID_NELLYMOSER,
+ AV_CODEC_ID_MUSEPACK8,
+ AV_CODEC_ID_SPEEX,
+ AV_CODEC_ID_WMAVOICE,
+ AV_CODEC_ID_WMAPRO,
+ AV_CODEC_ID_WMALOSSLESS,
+ AV_CODEC_ID_ATRAC3P,
+ AV_CODEC_ID_EAC3,
+ AV_CODEC_ID_SIPR,
+ AV_CODEC_ID_MP1,
+ AV_CODEC_ID_TWINVQ,
+ AV_CODEC_ID_TRUEHD,
+ AV_CODEC_ID_MP4ALS,
+ AV_CODEC_ID_ATRAC1,
+ AV_CODEC_ID_BINKAUDIO_RDFT,
+ AV_CODEC_ID_BINKAUDIO_DCT,
+ AV_CODEC_ID_AAC_LATM,
+ AV_CODEC_ID_QDMC,
+ AV_CODEC_ID_CELT,
+ AV_CODEC_ID_G723_1,
+ AV_CODEC_ID_G729,
+ AV_CODEC_ID_8SVX_EXP,
+ AV_CODEC_ID_8SVX_FIB,
+ AV_CODEC_ID_BMV_AUDIO,
+ AV_CODEC_ID_RALF,
+ AV_CODEC_ID_IAC,
+ AV_CODEC_ID_ILBC,
+ AV_CODEC_ID_OPUS,
+ AV_CODEC_ID_COMFORT_NOISE,
+ AV_CODEC_ID_TAK,
+ AV_CODEC_ID_METASOUND,
+ AV_CODEC_ID_PAF_AUDIO,
+ AV_CODEC_ID_ON2AVC,
+ AV_CODEC_ID_DSS_SP,
+ AV_CODEC_ID_CODEC2,
+ AV_CODEC_ID_FFWAVESYNTH,
+ AV_CODEC_ID_SONIC,
+ AV_CODEC_ID_SONIC_LS,
+ AV_CODEC_ID_EVRC,
+ AV_CODEC_ID_SMV,
+ AV_CODEC_ID_DSD_LSBF,
+ AV_CODEC_ID_DSD_MSBF,
+ AV_CODEC_ID_DSD_LSBF_PLANAR,
+ AV_CODEC_ID_DSD_MSBF_PLANAR,
+ AV_CODEC_ID_4GV,
+ AV_CODEC_ID_INTERPLAY_ACM,
+ AV_CODEC_ID_XMA1,
+ AV_CODEC_ID_XMA2,
+ AV_CODEC_ID_DST,
+ AV_CODEC_ID_ATRAC3AL,
+ AV_CODEC_ID_ATRAC3PAL,
+ AV_CODEC_ID_DOLBY_E,
+ AV_CODEC_ID_APTX,
+ AV_CODEC_ID_APTX_HD,
+ AV_CODEC_ID_SBC,
+ AV_CODEC_ID_ATRAC9,
+ AV_CODEC_ID_HCOM,
+ AV_CODEC_ID_ACELP_KELVIN,
+ AV_CODEC_ID_MPEGH_3D_AUDIO,
+ AV_CODEC_ID_SIREN,
+ AV_CODEC_ID_HCA,
+ AV_CODEC_ID_FASTAUDIO,
+ AV_CODEC_ID_MSNSIREN,
+
+ /* subtitle codecs */
+ AV_CODEC_ID_FIRST_SUBTITLE =
+ 0x17000, ///< A dummy ID pointing at the start of subtitle codecs.
+ AV_CODEC_ID_DVD_SUBTITLE = 0x17000,
+ AV_CODEC_ID_DVB_SUBTITLE,
+ AV_CODEC_ID_TEXT, ///< raw UTF-8 text
+ AV_CODEC_ID_XSUB,
+ AV_CODEC_ID_SSA,
+ AV_CODEC_ID_MOV_TEXT,
+ AV_CODEC_ID_HDMV_PGS_SUBTITLE,
+ AV_CODEC_ID_DVB_TELETEXT,
+ AV_CODEC_ID_SRT,
+ AV_CODEC_ID_MICRODVD,
+ AV_CODEC_ID_EIA_608,
+ AV_CODEC_ID_JACOSUB,
+ AV_CODEC_ID_SAMI,
+ AV_CODEC_ID_REALTEXT,
+ AV_CODEC_ID_STL,
+ AV_CODEC_ID_SUBVIEWER1,
+ AV_CODEC_ID_SUBVIEWER,
+ AV_CODEC_ID_SUBRIP,
+ AV_CODEC_ID_WEBVTT,
+ AV_CODEC_ID_MPL2,
+ AV_CODEC_ID_VPLAYER,
+ AV_CODEC_ID_PJS,
+ AV_CODEC_ID_ASS,
+ AV_CODEC_ID_HDMV_TEXT_SUBTITLE,
+ AV_CODEC_ID_TTML,
+ AV_CODEC_ID_ARIB_CAPTION,
+
+ /* other specific kind of codecs (generally used for attachments) */
+ AV_CODEC_ID_FIRST_UNKNOWN =
+ 0x18000, ///< A dummy ID pointing at the start of various fake codecs.
+ AV_CODEC_ID_TTF = 0x18000,
+
+ AV_CODEC_ID_SCTE_35, ///< Contain timestamp estimated through PCR of program
+ ///< stream.
+ AV_CODEC_ID_EPG,
+ AV_CODEC_ID_BINTEXT,
+ AV_CODEC_ID_XBIN,
+ AV_CODEC_ID_IDF,
+ AV_CODEC_ID_OTF,
+ AV_CODEC_ID_SMPTE_KLV,
+ AV_CODEC_ID_DVD_NAV,
+ AV_CODEC_ID_TIMED_ID3,
+ AV_CODEC_ID_BIN_DATA,
+
+ AV_CODEC_ID_PROBE =
+ 0x19000, ///< codec_id is not known (like AV_CODEC_ID_NONE) but lavf
+ ///< should attempt to identify it
+
+ AV_CODEC_ID_MPEG2TS = 0x20000, /**< _FAKE_ codec to indicate a raw MPEG-2 TS
+ * stream (only used by libavformat) */
+ AV_CODEC_ID_MPEG4SYSTEMS =
+ 0x20001, /**< _FAKE_ codec to indicate a MPEG-4 Systems
+ * stream (only used by libavformat) */
+ AV_CODEC_ID_FFMETADATA = 0x21000, ///< Dummy codec for streams containing
+ ///< only metadata information.
+ AV_CODEC_ID_WRAPPED_AVFRAME =
+ 0x21001, ///< Passthrough codec, AVFrames wrapped in AVPacket
+};
+
+/**
+ * Get the type of the given codec.
+ */
+enum AVMediaType avcodec_get_type(enum AVCodecID codec_id);
+
+/**
+ * Get the name of a codec.
+ * @return a static string identifying the codec; never NULL
+ */
+const char* avcodec_get_name(enum AVCodecID id);
+
+/**
+ * Return codec bits per sample.
+ *
+ * @param[in] codec_id the codec
+ * @return Number of bits per sample or zero if unknown for the given codec.
+ */
+int av_get_bits_per_sample(enum AVCodecID codec_id);
+
+/**
+ * Return codec bits per sample.
+ * Only return non-zero if the bits per sample is exactly correct, not an
+ * approximation.
+ *
+ * @param[in] codec_id the codec
+ * @return Number of bits per sample or zero if unknown for the given codec.
+ */
+int av_get_exact_bits_per_sample(enum AVCodecID codec_id);
+
+/**
+ * Return a name for the specified profile, if available.
+ *
+ * @param codec_id the ID of the codec to which the requested profile belongs
+ * @param profile the profile value for which a name is requested
+ * @return A name for the profile if found, NULL otherwise.
+ *
+ * @note unlike av_get_profile_name(), which searches a list of profiles
+ * supported by a specific decoder or encoder implementation, this
+ * function searches the list of profiles from the AVCodecDescriptor
+ */
+const char* avcodec_profile_name(enum AVCodecID codec_id, int profile);
+
+/**
+ * Return the PCM codec associated with a sample format.
+ * @param be endianness, 0 for little, 1 for big,
+ * -1 (or anything else) for native
+ * @return AV_CODEC_ID_PCM_* or AV_CODEC_ID_NONE
+ */
+enum AVCodecID av_get_pcm_codec(enum AVSampleFormat fmt, int be);
+
+/**
+ * @}
+ */
+
+#endif // AVCODEC_CODEC_ID_H
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/codec_par.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/codec_par.h
new file mode 100644
index 0000000000..aaa6dc830b
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/codec_par.h
@@ -0,0 +1,236 @@
+/*
+ * Codec parameters public API
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_CODEC_PAR_H
+#define AVCODEC_CODEC_PAR_H
+
+#include <stdint.h>
+
+#include "libavutil/avutil.h"
+#include "libavutil/rational.h"
+#include "libavutil/pixfmt.h"
+
+#include "codec_id.h"
+
+/**
+ * @addtogroup lavc_core
+ */
+
+enum AVFieldOrder {
+ AV_FIELD_UNKNOWN,
+ AV_FIELD_PROGRESSIVE,
+ AV_FIELD_TT, //< Top coded_first, top displayed first
+ AV_FIELD_BB, //< Bottom coded first, bottom displayed first
+ AV_FIELD_TB, //< Top coded first, bottom displayed first
+ AV_FIELD_BT, //< Bottom coded first, top displayed first
+};
+
+/**
+ * This struct describes the properties of an encoded stream.
+ *
+ * sizeof(AVCodecParameters) is not a part of the public ABI, this struct must
+ * be allocated with avcodec_parameters_alloc() and freed with
+ * avcodec_parameters_free().
+ */
+typedef struct AVCodecParameters {
+ /**
+ * General type of the encoded data.
+ */
+ enum AVMediaType codec_type;
+ /**
+ * Specific type of the encoded data (the codec used).
+ */
+ enum AVCodecID codec_id;
+ /**
+ * Additional information about the codec (corresponds to the AVI FOURCC).
+ */
+ uint32_t codec_tag;
+
+ /**
+ * Extra binary data needed for initializing the decoder, codec-dependent.
+ *
+ * Must be allocated with av_malloc() and will be freed by
+ * avcodec_parameters_free(). The allocated size of extradata must be at
+ * least extradata_size + AV_INPUT_BUFFER_PADDING_SIZE, with the padding
+ * bytes zeroed.
+ */
+ uint8_t* extradata;
+ /**
+ * Size of the extradata content in bytes.
+ */
+ int extradata_size;
+
+ /**
+ * - video: the pixel format, the value corresponds to enum AVPixelFormat.
+ * - audio: the sample format, the value corresponds to enum AVSampleFormat.
+ */
+ int format;
+
+ /**
+ * The average bitrate of the encoded data (in bits per second).
+ */
+ int64_t bit_rate;
+
+ /**
+ * The number of bits per sample in the codedwords.
+ *
+ * This is basically the bitrate per sample. It is mandatory for a bunch of
+ * formats to actually decode them. It's the number of bits for one sample in
+ * the actual coded bitstream.
+ *
+ * This could be for example 4 for ADPCM
+ * For PCM formats this matches bits_per_raw_sample
+ * Can be 0
+ */
+ int bits_per_coded_sample;
+
+ /**
+ * This is the number of valid bits in each output sample. If the
+ * sample format has more bits, the least significant bits are additional
+ * padding bits, which are always 0. Use right shifts to reduce the sample
+ * to its actual size. For example, audio formats with 24 bit samples will
+ * have bits_per_raw_sample set to 24, and format set to AV_SAMPLE_FMT_S32.
+ * To get the original sample use "(int32_t)sample >> 8"."
+ *
+ * For ADPCM this might be 12 or 16 or similar
+ * Can be 0
+ */
+ int bits_per_raw_sample;
+
+ /**
+ * Codec-specific bitstream restrictions that the stream conforms to.
+ */
+ int profile;
+ int level;
+
+ /**
+ * Video only. The dimensions of the video frame in pixels.
+ */
+ int width;
+ int height;
+
+ /**
+ * Video only. The aspect ratio (width / height) which a single pixel
+ * should have when displayed.
+ *
+ * When the aspect ratio is unknown / undefined, the numerator should be
+ * set to 0 (the denominator may have any value).
+ */
+ AVRational sample_aspect_ratio;
+
+ /**
+ * Video only. The order of the fields in interlaced video.
+ */
+ enum AVFieldOrder field_order;
+
+ /**
+ * Video only. Additional colorspace characteristics.
+ */
+ enum AVColorRange color_range;
+ enum AVColorPrimaries color_primaries;
+ enum AVColorTransferCharacteristic color_trc;
+ enum AVColorSpace color_space;
+ enum AVChromaLocation chroma_location;
+
+ /**
+ * Video only. Number of delayed frames.
+ */
+ int video_delay;
+
+ /**
+ * Audio only. The channel layout bitmask. May be 0 if the channel layout is
+ * unknown or unspecified, otherwise the number of bits set must be equal to
+ * the channels field.
+ */
+ uint64_t channel_layout;
+ /**
+ * Audio only. The number of audio channels.
+ */
+ int channels;
+ /**
+ * Audio only. The number of audio samples per second.
+ */
+ int sample_rate;
+ /**
+ * Audio only. The number of bytes per coded audio frame, required by some
+ * formats.
+ *
+ * Corresponds to nBlockAlign in WAVEFORMATEX.
+ */
+ int block_align;
+ /**
+ * Audio only. Audio frame size, if known. Required by some formats to be
+ * static.
+ */
+ int frame_size;
+
+ /**
+ * Audio only. The amount of padding (in samples) inserted by the encoder at
+ * the beginning of the audio. I.e. this number of leading decoded samples
+ * must be discarded by the caller to get the original audio without leading
+ * padding.
+ */
+ int initial_padding;
+ /**
+ * Audio only. The amount of padding (in samples) appended by the encoder to
+ * the end of the audio. I.e. this number of decoded samples must be
+ * discarded by the caller from the end of the stream to get the original
+ * audio without any trailing padding.
+ */
+ int trailing_padding;
+ /**
+ * Audio only. Number of samples to skip after a discontinuity.
+ */
+ int seek_preroll;
+} AVCodecParameters;
+
+/**
+ * Allocate a new AVCodecParameters and set its fields to default values
+ * (unknown/invalid/0). The returned struct must be freed with
+ * avcodec_parameters_free().
+ */
+AVCodecParameters* avcodec_parameters_alloc(void);
+
+/**
+ * Free an AVCodecParameters instance and everything associated with it and
+ * write NULL to the supplied pointer.
+ */
+void avcodec_parameters_free(AVCodecParameters** par);
+
+/**
+ * Copy the contents of src to dst. Any allocated fields in dst are freed and
+ * replaced with newly allocated duplicates of the corresponding fields in src.
+ *
+ * @return >= 0 on success, a negative AVERROR code on failure.
+ */
+int avcodec_parameters_copy(AVCodecParameters* dst,
+ const AVCodecParameters* src);
+
+/**
+ * This function is the same as av_get_audio_frame_duration(), except it works
+ * with AVCodecParameters instead of an AVCodecContext.
+ */
+int av_get_audio_frame_duration2(AVCodecParameters* par, int frame_bytes);
+
+/**
+ * @}
+ */
+
+#endif // AVCODEC_CODEC_PAR_H
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/defs.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/defs.h
new file mode 100644
index 0000000000..f97f8809bb
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/defs.h
@@ -0,0 +1,171 @@
+/*
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_DEFS_H
+#define AVCODEC_DEFS_H
+
+/**
+ * @file
+ * @ingroup libavc
+ * Misc types and constants that do not belong anywhere else.
+ */
+
+#include <stdint.h>
+#include <stdlib.h>
+
+/**
+ * @ingroup lavc_decoding
+ * Required number of additionally allocated bytes at the end of the input
+ * bitstream for decoding. This is mainly needed because some optimized
+ * bitstream readers read 32 or 64 bit at once and could read over the end.<br>
+ * Note: If the first 23 bits of the additional bytes are not 0, then damaged
+ * MPEG bitstreams could cause overread and segfault.
+ */
+#define AV_INPUT_BUFFER_PADDING_SIZE 64
+
+/**
+ * @ingroup lavc_decoding
+ */
+enum AVDiscard {
+ /* We leave some space between them for extensions (drop some
+ * keyframes for intra-only or drop just some bidir frames). */
+ AVDISCARD_NONE = -16, ///< discard nothing
+ AVDISCARD_DEFAULT =
+ 0, ///< discard useless packets like 0 size packets in avi
+ AVDISCARD_NONREF = 8, ///< discard all non reference
+ AVDISCARD_BIDIR = 16, ///< discard all bidirectional frames
+ AVDISCARD_NONINTRA = 24, ///< discard all non intra frames
+ AVDISCARD_NONKEY = 32, ///< discard all frames except keyframes
+ AVDISCARD_ALL = 48, ///< discard all
+};
+
+enum AVAudioServiceType {
+ AV_AUDIO_SERVICE_TYPE_MAIN = 0,
+ AV_AUDIO_SERVICE_TYPE_EFFECTS = 1,
+ AV_AUDIO_SERVICE_TYPE_VISUALLY_IMPAIRED = 2,
+ AV_AUDIO_SERVICE_TYPE_HEARING_IMPAIRED = 3,
+ AV_AUDIO_SERVICE_TYPE_DIALOGUE = 4,
+ AV_AUDIO_SERVICE_TYPE_COMMENTARY = 5,
+ AV_AUDIO_SERVICE_TYPE_EMERGENCY = 6,
+ AV_AUDIO_SERVICE_TYPE_VOICE_OVER = 7,
+ AV_AUDIO_SERVICE_TYPE_KARAOKE = 8,
+ AV_AUDIO_SERVICE_TYPE_NB, ///< Not part of ABI
+};
+
+/**
+ * Pan Scan area.
+ * This specifies the area which should be displayed.
+ * Note there may be multiple such areas for one frame.
+ */
+typedef struct AVPanScan {
+ /**
+ * id
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int id;
+
+ /**
+ * width and height in 1/16 pel
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int width;
+ int height;
+
+ /**
+ * position of the top left corner in 1/16 pel for up to 3 fields/frames
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int16_t position[3][2];
+} AVPanScan;
+
+/**
+ * This structure describes the bitrate properties of an encoded bitstream. It
+ * roughly corresponds to a subset the VBV parameters for MPEG-2 or HRD
+ * parameters for H.264/HEVC.
+ */
+typedef struct AVCPBProperties {
+ /**
+ * Maximum bitrate of the stream, in bits per second.
+ * Zero if unknown or unspecified.
+ */
+ int64_t max_bitrate;
+ /**
+ * Minimum bitrate of the stream, in bits per second.
+ * Zero if unknown or unspecified.
+ */
+ int64_t min_bitrate;
+ /**
+ * Average bitrate of the stream, in bits per second.
+ * Zero if unknown or unspecified.
+ */
+ int64_t avg_bitrate;
+
+ /**
+ * The size of the buffer to which the ratecontrol is applied, in bits.
+ * Zero if unknown or unspecified.
+ */
+ int64_t buffer_size;
+
+ /**
+ * The delay between the time the packet this structure is associated with
+ * is received and the time when it should be decoded, in periods of a 27MHz
+ * clock.
+ *
+ * UINT64_MAX when unknown or unspecified.
+ */
+ uint64_t vbv_delay;
+} AVCPBProperties;
+
+/**
+ * Allocate a CPB properties structure and initialize its fields to default
+ * values.
+ *
+ * @param size if non-NULL, the size of the allocated struct will be written
+ * here. This is useful for embedding it in side data.
+ *
+ * @return the newly allocated struct or NULL on failure
+ */
+AVCPBProperties* av_cpb_properties_alloc(size_t* size);
+
+/**
+ * This structure supplies correlation between a packet timestamp and a wall
+ * clock production time. The definition follows the Producer Reference Time
+ * ('prft') as defined in ISO/IEC 14496-12
+ */
+typedef struct AVProducerReferenceTime {
+ /**
+ * A UTC timestamp, in microseconds, since Unix epoch (e.g, av_gettime()).
+ */
+ int64_t wallclock;
+ int flags;
+} AVProducerReferenceTime;
+
+/**
+ * Encode extradata length to a buffer. Used by xiph codecs.
+ *
+ * @param s buffer to write to; must be at least (v/255+1) bytes long
+ * @param v size of extradata in bytes
+ * @return number of bytes written to the buffer.
+ */
+unsigned int av_xiphlacing(unsigned char* s, unsigned int v);
+
+#endif // AVCODEC_DEFS_H
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/packet.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/packet.h
new file mode 100644
index 0000000000..dc57cabb52
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/packet.h
@@ -0,0 +1,724 @@
+/*
+ * AVPacket public API
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_PACKET_H
+#define AVCODEC_PACKET_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "libavutil/attributes.h"
+#include "libavutil/buffer.h"
+#include "libavutil/dict.h"
+#include "libavutil/rational.h"
+
+#include "libavcodec/version.h"
+
+/**
+ * @defgroup lavc_packet AVPacket
+ *
+ * Types and functions for working with AVPacket.
+ * @{
+ */
+enum AVPacketSideDataType {
+ /**
+ * An AV_PKT_DATA_PALETTE side data packet contains exactly AVPALETTE_SIZE
+ * bytes worth of palette. This side data signals that a new palette is
+ * present.
+ */
+ AV_PKT_DATA_PALETTE,
+
+ /**
+ * The AV_PKT_DATA_NEW_EXTRADATA is used to notify the codec or the format
+ * that the extradata buffer was changed and the receiving side should
+ * act upon it appropriately. The new extradata is embedded in the side
+ * data buffer and should be immediately used for processing the current
+ * frame or packet.
+ */
+ AV_PKT_DATA_NEW_EXTRADATA,
+
+ /**
+ * An AV_PKT_DATA_PARAM_CHANGE side data packet is laid out as follows:
+ * @code
+ * u32le param_flags
+ * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_COUNT)
+ * s32le channel_count
+ * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_LAYOUT)
+ * u64le channel_layout
+ * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_SAMPLE_RATE)
+ * s32le sample_rate
+ * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_DIMENSIONS)
+ * s32le width
+ * s32le height
+ * @endcode
+ */
+ AV_PKT_DATA_PARAM_CHANGE,
+
+ /**
+ * An AV_PKT_DATA_H263_MB_INFO side data packet contains a number of
+ * structures with info about macroblocks relevant to splitting the
+ * packet into smaller packets on macroblock edges (e.g. as for RFC 2190).
+ * That is, it does not necessarily contain info about all macroblocks,
+ * as long as the distance between macroblocks in the info is smaller
+ * than the target payload size.
+ * Each MB info structure is 12 bytes, and is laid out as follows:
+ * @code
+ * u32le bit offset from the start of the packet
+ * u8 current quantizer at the start of the macroblock
+ * u8 GOB number
+ * u16le macroblock address within the GOB
+ * u8 horizontal MV predictor
+ * u8 vertical MV predictor
+ * u8 horizontal MV predictor for block number 3
+ * u8 vertical MV predictor for block number 3
+ * @endcode
+ */
+ AV_PKT_DATA_H263_MB_INFO,
+
+ /**
+ * This side data should be associated with an audio stream and contains
+ * ReplayGain information in form of the AVReplayGain struct.
+ */
+ AV_PKT_DATA_REPLAYGAIN,
+
+ /**
+ * This side data contains a 3x3 transformation matrix describing an affine
+ * transformation that needs to be applied to the decoded video frames for
+ * correct presentation.
+ *
+ * See libavutil/display.h for a detailed description of the data.
+ */
+ AV_PKT_DATA_DISPLAYMATRIX,
+
+ /**
+ * This side data should be associated with a video stream and contains
+ * Stereoscopic 3D information in form of the AVStereo3D struct.
+ */
+ AV_PKT_DATA_STEREO3D,
+
+ /**
+ * This side data should be associated with an audio stream and corresponds
+ * to enum AVAudioServiceType.
+ */
+ AV_PKT_DATA_AUDIO_SERVICE_TYPE,
+
+ /**
+ * This side data contains quality related information from the encoder.
+ * @code
+ * u32le quality factor of the compressed frame. Allowed range is between 1
+ * (good) and FF_LAMBDA_MAX (bad). u8 picture type u8 error count u16
+ * reserved u64le[error count] sum of squared differences between encoder in
+ * and output
+ * @endcode
+ */
+ AV_PKT_DATA_QUALITY_STATS,
+
+ /**
+ * This side data contains an integer value representing the stream index
+ * of a "fallback" track. A fallback track indicates an alternate
+ * track to use when the current track can not be decoded for some reason.
+ * e.g. no decoder available for codec.
+ */
+ AV_PKT_DATA_FALLBACK_TRACK,
+
+ /**
+ * This side data corresponds to the AVCPBProperties struct.
+ */
+ AV_PKT_DATA_CPB_PROPERTIES,
+
+ /**
+ * Recommmends skipping the specified number of samples
+ * @code
+ * u32le number of samples to skip from start of this packet
+ * u32le number of samples to skip from end of this packet
+ * u8 reason for start skip
+ * u8 reason for end skip (0=padding silence, 1=convergence)
+ * @endcode
+ */
+ AV_PKT_DATA_SKIP_SAMPLES,
+
+ /**
+ * An AV_PKT_DATA_JP_DUALMONO side data packet indicates that
+ * the packet may contain "dual mono" audio specific to Japanese DTV
+ * and if it is true, recommends only the selected channel to be used.
+ * @code
+ * u8 selected channels (0=mail/left, 1=sub/right, 2=both)
+ * @endcode
+ */
+ AV_PKT_DATA_JP_DUALMONO,
+
+ /**
+ * A list of zero terminated key/value strings. There is no end marker for
+ * the list, so it is required to rely on the side data size to stop.
+ */
+ AV_PKT_DATA_STRINGS_METADATA,
+
+ /**
+ * Subtitle event position
+ * @code
+ * u32le x1
+ * u32le y1
+ * u32le x2
+ * u32le y2
+ * @endcode
+ */
+ AV_PKT_DATA_SUBTITLE_POSITION,
+
+ /**
+ * Data found in BlockAdditional element of matroska container. There is
+ * no end marker for the data, so it is required to rely on the side data
+ * size to recognize the end. 8 byte id (as found in BlockAddId) followed
+ * by data.
+ */
+ AV_PKT_DATA_MATROSKA_BLOCKADDITIONAL,
+
+ /**
+ * The optional first identifier line of a WebVTT cue.
+ */
+ AV_PKT_DATA_WEBVTT_IDENTIFIER,
+
+ /**
+ * The optional settings (rendering instructions) that immediately
+ * follow the timestamp specifier of a WebVTT cue.
+ */
+ AV_PKT_DATA_WEBVTT_SETTINGS,
+
+ /**
+ * A list of zero terminated key/value strings. There is no end marker for
+ * the list, so it is required to rely on the side data size to stop. This
+ * side data includes updated metadata which appeared in the stream.
+ */
+ AV_PKT_DATA_METADATA_UPDATE,
+
+ /**
+ * MPEGTS stream ID as uint8_t, this is required to pass the stream ID
+ * information from the demuxer to the corresponding muxer.
+ */
+ AV_PKT_DATA_MPEGTS_STREAM_ID,
+
+ /**
+ * Mastering display metadata (based on SMPTE-2086:2014). This metadata
+ * should be associated with a video stream and contains data in the form
+ * of the AVMasteringDisplayMetadata struct.
+ */
+ AV_PKT_DATA_MASTERING_DISPLAY_METADATA,
+
+ /**
+ * This side data should be associated with a video stream and corresponds
+ * to the AVSphericalMapping structure.
+ */
+ AV_PKT_DATA_SPHERICAL,
+
+ /**
+ * Content light level (based on CTA-861.3). This metadata should be
+ * associated with a video stream and contains data in the form of the
+ * AVContentLightMetadata struct.
+ */
+ AV_PKT_DATA_CONTENT_LIGHT_LEVEL,
+
+ /**
+ * ATSC A53 Part 4 Closed Captions. This metadata should be associated with
+ * a video stream. A53 CC bitstream is stored as uint8_t in
+ * AVPacketSideData.data. The number of bytes of CC data is
+ * AVPacketSideData.size.
+ */
+ AV_PKT_DATA_A53_CC,
+
+ /**
+ * This side data is encryption initialization data.
+ * The format is not part of ABI, use av_encryption_init_info_* methods to
+ * access.
+ */
+ AV_PKT_DATA_ENCRYPTION_INIT_INFO,
+
+ /**
+ * This side data contains encryption info for how to decrypt the packet.
+ * The format is not part of ABI, use av_encryption_info_* methods to access.
+ */
+ AV_PKT_DATA_ENCRYPTION_INFO,
+
+ /**
+ * Active Format Description data consisting of a single byte as specified
+ * in ETSI TS 101 154 using AVActiveFormatDescription enum.
+ */
+ AV_PKT_DATA_AFD,
+
+ /**
+ * Producer Reference Time data corresponding to the AVProducerReferenceTime
+ * struct, usually exported by some encoders (on demand through the prft flag
+ * set in the AVCodecContext export_side_data field).
+ */
+ AV_PKT_DATA_PRFT,
+
+ /**
+ * ICC profile data consisting of an opaque octet buffer following the
+ * format described by ISO 15076-1.
+ */
+ AV_PKT_DATA_ICC_PROFILE,
+
+ /**
+ * DOVI configuration
+ * ref:
+ * dolby-vision-bitstreams-within-the-iso-base-media-file-format-v2.1.2,
+ * section 2.2
+ * dolby-vision-bitstreams-in-mpeg-2-transport-stream-multiplex-v1.2,
+ * section 3.3 Tags are stored in struct AVDOVIDecoderConfigurationRecord.
+ */
+ AV_PKT_DATA_DOVI_CONF,
+
+ /**
+ * Timecode which conforms to SMPTE ST 12-1:2014. The data is an array of 4
+ * uint32_t where the first uint32_t describes how many (1-3) of the other
+ * timecodes are used. The timecode format is described in the documentation
+ * of av_timecode_get_smpte_from_framenum() function in libavutil/timecode.h.
+ */
+ AV_PKT_DATA_S12M_TIMECODE,
+
+ /**
+ * HDR10+ dynamic metadata associated with a video frame. The metadata is in
+ * the form of the AVDynamicHDRPlus struct and contains
+ * information for color volume transform - application 4 of
+ * SMPTE 2094-40:2016 standard.
+ */
+ AV_PKT_DATA_DYNAMIC_HDR10_PLUS,
+
+ /**
+ * The number of side data types.
+ * This is not part of the public API/ABI in the sense that it may
+ * change when new side data types are added.
+ * This must stay the last enum value.
+ * If its value becomes huge, some code using it
+ * needs to be updated as it assumes it to be smaller than other limits.
+ */
+ AV_PKT_DATA_NB
+};
+
+#define AV_PKT_DATA_QUALITY_FACTOR AV_PKT_DATA_QUALITY_STATS // DEPRECATED
+
+typedef struct AVPacketSideData {
+ uint8_t* data;
+ size_t size;
+ enum AVPacketSideDataType type;
+} AVPacketSideData;
+
+/**
+ * This structure stores compressed data. It is typically exported by demuxers
+ * and then passed as input to decoders, or received as output from encoders and
+ * then passed to muxers.
+ *
+ * For video, it should typically contain one compressed frame. For audio it may
+ * contain several compressed frames. Encoders are allowed to output empty
+ * packets, with no compressed data, containing only side data
+ * (e.g. to update some stream parameters at the end of encoding).
+ *
+ * The semantics of data ownership depends on the buf field.
+ * If it is set, the packet data is dynamically allocated and is
+ * valid indefinitely until a call to av_packet_unref() reduces the
+ * reference count to 0.
+ *
+ * If the buf field is not set av_packet_ref() would make a copy instead
+ * of increasing the reference count.
+ *
+ * The side data is always allocated with av_malloc(), copied by
+ * av_packet_ref() and freed by av_packet_unref().
+ *
+ * sizeof(AVPacket) being a part of the public ABI is deprecated. once
+ * av_init_packet() is removed, new packets will only be able to be allocated
+ * with av_packet_alloc(), and new fields may be added to the end of the struct
+ * with a minor bump.
+ *
+ * @see av_packet_alloc
+ * @see av_packet_ref
+ * @see av_packet_unref
+ */
+typedef struct AVPacket {
+ /**
+ * A reference to the reference-counted buffer where the packet data is
+ * stored.
+ * May be NULL, then the packet data is not reference-counted.
+ */
+ AVBufferRef* buf;
+ /**
+ * Presentation timestamp in AVStream->time_base units; the time at which
+ * the decompressed packet will be presented to the user.
+ * Can be AV_NOPTS_VALUE if it is not stored in the file.
+ * pts MUST be larger or equal to dts as presentation cannot happen before
+ * decompression, unless one wants to view hex dumps. Some formats misuse
+ * the terms dts and pts/cts to mean something different. Such timestamps
+ * must be converted to true pts/dts before they are stored in AVPacket.
+ */
+ int64_t pts;
+ /**
+ * Decompression timestamp in AVStream->time_base units; the time at which
+ * the packet is decompressed.
+ * Can be AV_NOPTS_VALUE if it is not stored in the file.
+ */
+ int64_t dts;
+ uint8_t* data;
+ int size;
+ int stream_index;
+ /**
+ * A combination of AV_PKT_FLAG values
+ */
+ int flags;
+ /**
+ * Additional packet data that can be provided by the container.
+ * Packet can contain several types of side information.
+ */
+ AVPacketSideData* side_data;
+ int side_data_elems;
+
+ /**
+ * Duration of this packet in AVStream->time_base units, 0 if unknown.
+ * Equals next_pts - this_pts in presentation order.
+ */
+ int64_t duration;
+
+ int64_t pos; ///< byte position in stream, -1 if unknown
+
+ /**
+ * for some private data of the user
+ */
+ void* opaque;
+
+ /**
+ * AVBufferRef for free use by the API user. FFmpeg will never check the
+ * contents of the buffer ref. FFmpeg calls av_buffer_unref() on it when
+ * the packet is unreferenced. av_packet_copy_props() calls create a new
+ * reference with av_buffer_ref() for the target packet's opaque_ref field.
+ *
+ * This is unrelated to the opaque field, although it serves a similar
+ * purpose.
+ */
+ AVBufferRef* opaque_ref;
+
+ /**
+ * Time base of the packet's timestamps.
+ * In the future, this field may be set on packets output by encoders or
+ * demuxers, but its value will be by default ignored on input to decoders
+ * or muxers.
+ */
+ AVRational time_base;
+} AVPacket;
+
+#if FF_API_INIT_PACKET
+attribute_deprecated typedef struct AVPacketList {
+ AVPacket pkt;
+ struct AVPacketList* next;
+} AVPacketList;
+#endif
+
+#define AV_PKT_FLAG_KEY 0x0001 ///< The packet contains a keyframe
+#define AV_PKT_FLAG_CORRUPT 0x0002 ///< The packet content is corrupted
+/**
+ * Flag is used to discard packets which are required to maintain valid
+ * decoder state but are not required for output and should be dropped
+ * after decoding.
+ **/
+#define AV_PKT_FLAG_DISCARD 0x0004
+/**
+ * The packet comes from a trusted source.
+ *
+ * Otherwise-unsafe constructs such as arbitrary pointers to data
+ * outside the packet may be followed.
+ */
+#define AV_PKT_FLAG_TRUSTED 0x0008
+/**
+ * Flag is used to indicate packets that contain frames that can
+ * be discarded by the decoder. I.e. Non-reference frames.
+ */
+#define AV_PKT_FLAG_DISPOSABLE 0x0010
+
+enum AVSideDataParamChangeFlags {
+ AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_COUNT = 0x0001,
+ AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_LAYOUT = 0x0002,
+ AV_SIDE_DATA_PARAM_CHANGE_SAMPLE_RATE = 0x0004,
+ AV_SIDE_DATA_PARAM_CHANGE_DIMENSIONS = 0x0008,
+};
+
+/**
+ * Allocate an AVPacket and set its fields to default values. The resulting
+ * struct must be freed using av_packet_free().
+ *
+ * @return An AVPacket filled with default values or NULL on failure.
+ *
+ * @note this only allocates the AVPacket itself, not the data buffers. Those
+ * must be allocated through other means such as av_new_packet.
+ *
+ * @see av_new_packet
+ */
+AVPacket* av_packet_alloc(void);
+
+/**
+ * Create a new packet that references the same data as src.
+ *
+ * This is a shortcut for av_packet_alloc()+av_packet_ref().
+ *
+ * @return newly created AVPacket on success, NULL on error.
+ *
+ * @see av_packet_alloc
+ * @see av_packet_ref
+ */
+AVPacket* av_packet_clone(const AVPacket* src);
+
+/**
+ * Free the packet, if the packet is reference counted, it will be
+ * unreferenced first.
+ *
+ * @param pkt packet to be freed. The pointer will be set to NULL.
+ * @note passing NULL is a no-op.
+ */
+void av_packet_free(AVPacket** pkt);
+
+#if FF_API_INIT_PACKET
+/**
+ * Initialize optional fields of a packet with default values.
+ *
+ * Note, this does not touch the data and size members, which have to be
+ * initialized separately.
+ *
+ * @param pkt packet
+ *
+ * @see av_packet_alloc
+ * @see av_packet_unref
+ *
+ * @deprecated This function is deprecated. Once it's removed,
+ sizeof(AVPacket) will not be a part of the ABI anymore.
+ */
+attribute_deprecated void av_init_packet(AVPacket* pkt);
+#endif
+
+/**
+ * Allocate the payload of a packet and initialize its fields with
+ * default values.
+ *
+ * @param pkt packet
+ * @param size wanted payload size
+ * @return 0 if OK, AVERROR_xxx otherwise
+ */
+int av_new_packet(AVPacket* pkt, int size);
+
+/**
+ * Reduce packet size, correctly zeroing padding
+ *
+ * @param pkt packet
+ * @param size new size
+ */
+void av_shrink_packet(AVPacket* pkt, int size);
+
+/**
+ * Increase packet size, correctly zeroing padding
+ *
+ * @param pkt packet
+ * @param grow_by number of bytes by which to increase the size of the packet
+ */
+int av_grow_packet(AVPacket* pkt, int grow_by);
+
+/**
+ * Initialize a reference-counted packet from av_malloc()ed data.
+ *
+ * @param pkt packet to be initialized. This function will set the data, size,
+ * and buf fields, all others are left untouched.
+ * @param data Data allocated by av_malloc() to be used as packet data. If this
+ * function returns successfully, the data is owned by the underlying
+ * AVBuffer. The caller may not access the data through other means.
+ * @param size size of data in bytes, without the padding. I.e. the full buffer
+ * size is assumed to be size + AV_INPUT_BUFFER_PADDING_SIZE.
+ *
+ * @return 0 on success, a negative AVERROR on error
+ */
+int av_packet_from_data(AVPacket* pkt, uint8_t* data, int size);
+
+/**
+ * Allocate new information of a packet.
+ *
+ * @param pkt packet
+ * @param type side information type
+ * @param size side information size
+ * @return pointer to fresh allocated data or NULL otherwise
+ */
+uint8_t* av_packet_new_side_data(AVPacket* pkt, enum AVPacketSideDataType type,
+ size_t size);
+
+/**
+ * Wrap an existing array as a packet side data.
+ *
+ * @param pkt packet
+ * @param type side information type
+ * @param data the side data array. It must be allocated with the av_malloc()
+ * family of functions. The ownership of the data is transferred to
+ * pkt.
+ * @param size side information size
+ * @return a non-negative number on success, a negative AVERROR code on
+ * failure. On failure, the packet is unchanged and the data remains
+ * owned by the caller.
+ */
+int av_packet_add_side_data(AVPacket* pkt, enum AVPacketSideDataType type,
+ uint8_t* data, size_t size);
+
+/**
+ * Shrink the already allocated side data buffer
+ *
+ * @param pkt packet
+ * @param type side information type
+ * @param size new side information size
+ * @return 0 on success, < 0 on failure
+ */
+int av_packet_shrink_side_data(AVPacket* pkt, enum AVPacketSideDataType type,
+ size_t size);
+
+/**
+ * Get side information from packet.
+ *
+ * @param pkt packet
+ * @param type desired side information type
+ * @param size If supplied, *size will be set to the size of the side data
+ * or to zero if the desired side data is not present.
+ * @return pointer to data if present or NULL otherwise
+ */
+uint8_t* av_packet_get_side_data(const AVPacket* pkt,
+ enum AVPacketSideDataType type, size_t* size);
+
+const char* av_packet_side_data_name(enum AVPacketSideDataType type);
+
+/**
+ * Pack a dictionary for use in side_data.
+ *
+ * @param dict The dictionary to pack.
+ * @param size pointer to store the size of the returned data
+ * @return pointer to data if successful, NULL otherwise
+ */
+uint8_t* av_packet_pack_dictionary(AVDictionary* dict, size_t* size);
+/**
+ * Unpack a dictionary from side_data.
+ *
+ * @param data data from side_data
+ * @param size size of the data
+ * @param dict the metadata storage dictionary
+ * @return 0 on success, < 0 on failure
+ */
+int av_packet_unpack_dictionary(const uint8_t* data, size_t size,
+ AVDictionary** dict);
+
+/**
+ * Convenience function to free all the side data stored.
+ * All the other fields stay untouched.
+ *
+ * @param pkt packet
+ */
+void av_packet_free_side_data(AVPacket* pkt);
+
+/**
+ * Setup a new reference to the data described by a given packet
+ *
+ * If src is reference-counted, setup dst as a new reference to the
+ * buffer in src. Otherwise allocate a new buffer in dst and copy the
+ * data from src into it.
+ *
+ * All the other fields are copied from src.
+ *
+ * @see av_packet_unref
+ *
+ * @param dst Destination packet. Will be completely overwritten.
+ * @param src Source packet
+ *
+ * @return 0 on success, a negative AVERROR on error. On error, dst
+ * will be blank (as if returned by av_packet_alloc()).
+ */
+int av_packet_ref(AVPacket* dst, const AVPacket* src);
+
+/**
+ * Wipe the packet.
+ *
+ * Unreference the buffer referenced by the packet and reset the
+ * remaining packet fields to their default values.
+ *
+ * @param pkt The packet to be unreferenced.
+ */
+void av_packet_unref(AVPacket* pkt);
+
+/**
+ * Move every field in src to dst and reset src.
+ *
+ * @see av_packet_unref
+ *
+ * @param src Source packet, will be reset
+ * @param dst Destination packet
+ */
+void av_packet_move_ref(AVPacket* dst, AVPacket* src);
+
+/**
+ * Copy only "properties" fields from src to dst.
+ *
+ * Properties for the purpose of this function are all the fields
+ * beside those related to the packet data (buf, data, size)
+ *
+ * @param dst Destination packet
+ * @param src Source packet
+ *
+ * @return 0 on success AVERROR on failure.
+ */
+int av_packet_copy_props(AVPacket* dst, const AVPacket* src);
+
+/**
+ * Ensure the data described by a given packet is reference counted.
+ *
+ * @note This function does not ensure that the reference will be writable.
+ * Use av_packet_make_writable instead for that purpose.
+ *
+ * @see av_packet_ref
+ * @see av_packet_make_writable
+ *
+ * @param pkt packet whose data should be made reference counted.
+ *
+ * @return 0 on success, a negative AVERROR on error. On failure, the
+ * packet is unchanged.
+ */
+int av_packet_make_refcounted(AVPacket* pkt);
+
+/**
+ * Create a writable reference for the data described by a given packet,
+ * avoiding data copy if possible.
+ *
+ * @param pkt Packet whose data should be made writable.
+ *
+ * @return 0 on success, a negative AVERROR on failure. On failure, the
+ * packet is unchanged.
+ */
+int av_packet_make_writable(AVPacket* pkt);
+
+/**
+ * Convert valid timing fields (timestamps / durations) in a packet from one
+ * timebase to another. Timestamps with unknown values (AV_NOPTS_VALUE) will be
+ * ignored.
+ *
+ * @param pkt packet on which the conversion will be performed
+ * @param tb_src source timebase, in which the timing fields in pkt are
+ * expressed
+ * @param tb_dst destination timebase, to which the timing fields will be
+ * converted
+ */
+void av_packet_rescale_ts(AVPacket* pkt, AVRational tb_src, AVRational tb_dst);
+
+/**
+ * @}
+ */
+
+#endif // AVCODEC_PACKET_H
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/vdpau.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/vdpau.h
new file mode 100644
index 0000000000..1feea28fa3
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/vdpau.h
@@ -0,0 +1,156 @@
+/*
+ * The Video Decode and Presentation API for UNIX (VDPAU) is used for
+ * hardware-accelerated decoding of MPEG-1/2, H.264 and VC-1.
+ *
+ * Copyright (C) 2008 NVIDIA
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_VDPAU_H
+#define AVCODEC_VDPAU_H
+
+/**
+ * @file
+ * @ingroup lavc_codec_hwaccel_vdpau
+ * Public libavcodec VDPAU header.
+ */
+
+/**
+ * @defgroup lavc_codec_hwaccel_vdpau VDPAU Decoder and Renderer
+ * @ingroup lavc_codec_hwaccel
+ *
+ * VDPAU hardware acceleration has two modules
+ * - VDPAU decoding
+ * - VDPAU presentation
+ *
+ * The VDPAU decoding module parses all headers using FFmpeg
+ * parsing mechanisms and uses VDPAU for the actual decoding.
+ *
+ * As per the current implementation, the actual decoding
+ * and rendering (API calls) are done as part of the VDPAU
+ * presentation (vo_vdpau.c) module.
+ *
+ * @{
+ */
+
+#include <vdpau/vdpau.h>
+
+#include "libavutil/avconfig.h"
+#include "libavutil/attributes.h"
+
+#include "avcodec.h"
+
+struct AVCodecContext;
+struct AVFrame;
+
+typedef int (*AVVDPAU_Render2)(struct AVCodecContext*, struct AVFrame*,
+ const VdpPictureInfo*, uint32_t,
+ const VdpBitstreamBuffer*);
+
+/**
+ * This structure is used to share data between the libavcodec library and
+ * the client video application.
+ * The user shall allocate the structure via the av_alloc_vdpau_hwaccel
+ * function and make it available as
+ * AVCodecContext.hwaccel_context. Members can be set by the user once
+ * during initialization or through each AVCodecContext.get_buffer()
+ * function call. In any case, they must be valid prior to calling
+ * decoding functions.
+ *
+ * The size of this structure is not a part of the public ABI and must not
+ * be used outside of libavcodec. Use av_vdpau_alloc_context() to allocate an
+ * AVVDPAUContext.
+ */
+typedef struct AVVDPAUContext {
+ /**
+ * VDPAU decoder handle
+ *
+ * Set by user.
+ */
+ VdpDecoder decoder;
+
+ /**
+ * VDPAU decoder render callback
+ *
+ * Set by the user.
+ */
+ VdpDecoderRender* render;
+
+ AVVDPAU_Render2 render2;
+} AVVDPAUContext;
+
+/**
+ * @brief allocation function for AVVDPAUContext
+ *
+ * Allows extending the struct without breaking API/ABI
+ */
+AVVDPAUContext* av_alloc_vdpaucontext(void);
+
+AVVDPAU_Render2 av_vdpau_hwaccel_get_render2(const AVVDPAUContext*);
+void av_vdpau_hwaccel_set_render2(AVVDPAUContext*, AVVDPAU_Render2);
+
+/**
+ * Associate a VDPAU device with a codec context for hardware acceleration.
+ * This function is meant to be called from the get_format() codec callback,
+ * or earlier. It can also be called after avcodec_flush_buffers() to change
+ * the underlying VDPAU device mid-stream (e.g. to recover from non-transparent
+ * display preemption).
+ *
+ * @note get_format() must return AV_PIX_FMT_VDPAU if this function completes
+ * successfully.
+ *
+ * @param avctx decoding context whose get_format() callback is invoked
+ * @param device VDPAU device handle to use for hardware acceleration
+ * @param get_proc_address VDPAU device driver
+ * @param flags zero of more OR'd AV_HWACCEL_FLAG_* flags
+ *
+ * @return 0 on success, an AVERROR code on failure.
+ */
+int av_vdpau_bind_context(AVCodecContext* avctx, VdpDevice device,
+ VdpGetProcAddress* get_proc_address, unsigned flags);
+
+/**
+ * Gets the parameters to create an adequate VDPAU video surface for the codec
+ * context using VDPAU hardware decoding acceleration.
+ *
+ * @note Behavior is undefined if the context was not successfully bound to a
+ * VDPAU device using av_vdpau_bind_context().
+ *
+ * @param avctx the codec context being used for decoding the stream
+ * @param type storage space for the VDPAU video surface chroma type
+ * (or NULL to ignore)
+ * @param width storage space for the VDPAU video surface pixel width
+ * (or NULL to ignore)
+ * @param height storage space for the VDPAU video surface pixel height
+ * (or NULL to ignore)
+ *
+ * @return 0 on success, a negative AVERROR code on failure.
+ */
+int av_vdpau_get_surface_parameters(AVCodecContext* avctx, VdpChromaType* type,
+ uint32_t* width, uint32_t* height);
+
+/**
+ * Allocate an AVVDPAUContext.
+ *
+ * @return Newly-allocated AVVDPAUContext or NULL on failure.
+ */
+AVVDPAUContext* av_vdpau_alloc_context(void);
+
+/* @}*/
+
+#endif /* AVCODEC_VDPAU_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/version.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/version.h
new file mode 100644
index 0000000000..092441f4ec
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavcodec/version.h
@@ -0,0 +1,67 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_VERSION_H
+#define AVCODEC_VERSION_H
+
+/**
+ * @file
+ * @ingroup libavc
+ * Libavcodec version macros.
+ */
+
+#include "libavutil/version.h"
+
+#define LIBAVCODEC_VERSION_MAJOR 59
+#define LIBAVCODEC_VERSION_MINOR 18
+#define LIBAVCODEC_VERSION_MICRO 100
+
+#define LIBAVCODEC_VERSION_INT \
+ AV_VERSION_INT(LIBAVCODEC_VERSION_MAJOR, LIBAVCODEC_VERSION_MINOR, \
+ LIBAVCODEC_VERSION_MICRO)
+#define LIBAVCODEC_VERSION \
+ AV_VERSION(LIBAVCODEC_VERSION_MAJOR, LIBAVCODEC_VERSION_MINOR, \
+ LIBAVCODEC_VERSION_MICRO)
+#define LIBAVCODEC_BUILD LIBAVCODEC_VERSION_INT
+
+#define LIBAVCODEC_IDENT "Lavc" AV_STRINGIFY(LIBAVCODEC_VERSION)
+
+/**
+ * FF_API_* defines may be placed below to indicate public API that will be
+ * dropped at a future version bump. The defines themselves are not part of
+ * the public API and may change, break or disappear at any time.
+ *
+ * @note, when bumping the major version it is recommended to manually
+ * disable each FF_API_* in its own commit instead of disabling them all
+ * at once through the bump. This improves the git bisect-ability of the change.
+ */
+
+#define FF_API_OPENH264_SLICE_MODE (LIBAVCODEC_VERSION_MAJOR < 60)
+#define FF_API_OPENH264_CABAC (LIBAVCODEC_VERSION_MAJOR < 60)
+#define FF_API_UNUSED_CODEC_CAPS (LIBAVCODEC_VERSION_MAJOR < 60)
+#define FF_API_THREAD_SAFE_CALLBACKS (LIBAVCODEC_VERSION_MAJOR < 60)
+#define FF_API_DEBUG_MV (LIBAVCODEC_VERSION_MAJOR < 60)
+#define FF_API_GET_FRAME_CLASS (LIBAVCODEC_VERSION_MAJOR < 60)
+#define FF_API_AUTO_THREADS (LIBAVCODEC_VERSION_MAJOR < 60)
+#define FF_API_INIT_PACKET (LIBAVCODEC_VERSION_MAJOR < 60)
+#define FF_API_AVCTX_TIMEBASE (LIBAVCODEC_VERSION_MAJOR < 60)
+#define FF_API_MPEGVIDEO_OPTS (LIBAVCODEC_VERSION_MAJOR < 60)
+#define FF_API_FLAG_TRUNCATED (LIBAVCODEC_VERSION_MAJOR < 60)
+#define FF_API_SUB_TEXT_FORMAT (LIBAVCODEC_VERSION_MAJOR < 60)
+
+#endif /* AVCODEC_VERSION_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/attributes.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/attributes.h
new file mode 100644
index 0000000000..9763dda157
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/attributes.h
@@ -0,0 +1,173 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * Macro definitions for various function/variable attributes
+ */
+
+#ifndef AVUTIL_ATTRIBUTES_H
+#define AVUTIL_ATTRIBUTES_H
+
+#ifdef __GNUC__
+# define AV_GCC_VERSION_AT_LEAST(x, y) \
+ (__GNUC__ > (x) || __GNUC__ == (x) && __GNUC_MINOR__ >= (y))
+# define AV_GCC_VERSION_AT_MOST(x, y) \
+ (__GNUC__ < (x) || __GNUC__ == (x) && __GNUC_MINOR__ <= (y))
+#else
+# define AV_GCC_VERSION_AT_LEAST(x, y) 0
+# define AV_GCC_VERSION_AT_MOST(x, y) 0
+#endif
+
+#ifdef __has_builtin
+# define AV_HAS_BUILTIN(x) __has_builtin(x)
+#else
+# define AV_HAS_BUILTIN(x) 0
+#endif
+
+#ifndef av_always_inline
+# if AV_GCC_VERSION_AT_LEAST(3, 1)
+# define av_always_inline __attribute__((always_inline)) inline
+# elif defined(_MSC_VER)
+# define av_always_inline __forceinline
+# else
+# define av_always_inline inline
+# endif
+#endif
+
+#ifndef av_extern_inline
+# if defined(__ICL) && __ICL >= 1210 || defined(__GNUC_STDC_INLINE__)
+# define av_extern_inline extern inline
+# else
+# define av_extern_inline inline
+# endif
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3, 4)
+# define av_warn_unused_result __attribute__((warn_unused_result))
+#else
+# define av_warn_unused_result
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3, 1)
+# define av_noinline __attribute__((noinline))
+#elif defined(_MSC_VER)
+# define av_noinline __declspec(noinline)
+#else
+# define av_noinline
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3, 1) || defined(__clang__)
+# define av_pure __attribute__((pure))
+#else
+# define av_pure
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(2, 6) || defined(__clang__)
+# define av_const __attribute__((const))
+#else
+# define av_const
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(4, 3) || defined(__clang__)
+# define av_cold __attribute__((cold))
+#else
+# define av_cold
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(4, 1) && !defined(__llvm__)
+# define av_flatten __attribute__((flatten))
+#else
+# define av_flatten
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3, 1)
+# define attribute_deprecated __attribute__((deprecated))
+#elif defined(_MSC_VER)
+# define attribute_deprecated __declspec(deprecated)
+#else
+# define attribute_deprecated
+#endif
+
+/**
+ * Disable warnings about deprecated features
+ * This is useful for sections of code kept for backward compatibility and
+ * scheduled for removal.
+ */
+#ifndef AV_NOWARN_DEPRECATED
+# if AV_GCC_VERSION_AT_LEAST(4, 6)
+# define AV_NOWARN_DEPRECATED(code) \
+ _Pragma("GCC diagnostic push") \
+ _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"") \
+ code _Pragma("GCC diagnostic pop")
+# elif defined(_MSC_VER)
+# define AV_NOWARN_DEPRECATED(code) \
+ __pragma(warning(push)) __pragma(warning(disable : 4996)) code; \
+ __pragma(warning(pop))
+# else
+# define AV_NOWARN_DEPRECATED(code) code
+# endif
+#endif
+
+#if defined(__GNUC__) || defined(__clang__)
+# define av_unused __attribute__((unused))
+#else
+# define av_unused
+#endif
+
+/**
+ * Mark a variable as used and prevent the compiler from optimizing it
+ * away. This is useful for variables accessed only from inline
+ * assembler without the compiler being aware.
+ */
+#if AV_GCC_VERSION_AT_LEAST(3, 1) || defined(__clang__)
+# define av_used __attribute__((used))
+#else
+# define av_used
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3, 3) || defined(__clang__)
+# define av_alias __attribute__((may_alias))
+#else
+# define av_alias
+#endif
+
+#if (defined(__GNUC__) || defined(__clang__)) && !defined(__INTEL_COMPILER)
+# define av_uninit(x) x = x
+#else
+# define av_uninit(x) x
+#endif
+
+#if defined(__GNUC__) || defined(__clang__)
+# define av_builtin_constant_p __builtin_constant_p
+# define av_printf_format(fmtpos, attrpos) \
+ __attribute__((__format__(__printf__, fmtpos, attrpos)))
+#else
+# define av_builtin_constant_p(x) 0
+# define av_printf_format(fmtpos, attrpos)
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(2, 5) || defined(__clang__)
+# define av_noreturn __attribute__((noreturn))
+#else
+# define av_noreturn
+#endif
+
+#endif /* AVUTIL_ATTRIBUTES_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/avconfig.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/avconfig.h
new file mode 100644
index 0000000000..c289fbb551
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/avconfig.h
@@ -0,0 +1,6 @@
+/* Generated by ffmpeg configure */
+#ifndef AVUTIL_AVCONFIG_H
+#define AVUTIL_AVCONFIG_H
+#define AV_HAVE_BIGENDIAN 0
+#define AV_HAVE_FAST_UNALIGNED 1
+#endif /* AVUTIL_AVCONFIG_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/avutil.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/avutil.h
new file mode 100644
index 0000000000..57703ca7e0
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/avutil.h
@@ -0,0 +1,366 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_AVUTIL_H
+#define AVUTIL_AVUTIL_H
+
+/**
+ * @file
+ * @ingroup lavu
+ * Convenience header that includes @ref lavu "libavutil"'s core.
+ */
+
+/**
+ * @mainpage
+ *
+ * @section ffmpeg_intro Introduction
+ *
+ * This document describes the usage of the different libraries
+ * provided by FFmpeg.
+ *
+ * @li @ref libavc "libavcodec" encoding/decoding library
+ * @li @ref lavfi "libavfilter" graph-based frame editing library
+ * @li @ref libavf "libavformat" I/O and muxing/demuxing library
+ * @li @ref lavd "libavdevice" special devices muxing/demuxing library
+ * @li @ref lavu "libavutil" common utility library
+ * @li @ref lswr "libswresample" audio resampling, format conversion and mixing
+ * @li @ref lpp "libpostproc" post processing library
+ * @li @ref libsws "libswscale" color conversion and scaling library
+ *
+ * @section ffmpeg_versioning Versioning and compatibility
+ *
+ * Each of the FFmpeg libraries contains a version.h header, which defines a
+ * major, minor and micro version number with the
+ * <em>LIBRARYNAME_VERSION_{MAJOR,MINOR,MICRO}</em> macros. The major version
+ * number is incremented with backward incompatible changes - e.g. removing
+ * parts of the public API, reordering public struct members, etc. The minor
+ * version number is incremented for backward compatible API changes or major
+ * new features - e.g. adding a new public function or a new decoder. The micro
+ * version number is incremented for smaller changes that a calling program
+ * might still want to check for - e.g. changing behavior in a previously
+ * unspecified situation.
+ *
+ * FFmpeg guarantees backward API and ABI compatibility for each library as long
+ * as its major version number is unchanged. This means that no public symbols
+ * will be removed or renamed. Types and names of the public struct members and
+ * values of public macros and enums will remain the same (unless they were
+ * explicitly declared as not part of the public API). Documented behavior will
+ * not change.
+ *
+ * In other words, any correct program that works with a given FFmpeg snapshot
+ * should work just as well without any changes with any later snapshot with the
+ * same major versions. This applies to both rebuilding the program against new
+ * FFmpeg versions or to replacing the dynamic FFmpeg libraries that a program
+ * links against.
+ *
+ * However, new public symbols may be added and new members may be appended to
+ * public structs whose size is not part of public ABI (most public structs in
+ * FFmpeg). New macros and enum values may be added. Behavior in undocumented
+ * situations may change slightly (and be documented). All those are accompanied
+ * by an entry in doc/APIchanges and incrementing either the minor or micro
+ * version number.
+ */
+
+/**
+ * @defgroup lavu libavutil
+ * Common code shared across all FFmpeg libraries.
+ *
+ * @note
+ * libavutil is designed to be modular. In most cases, in order to use the
+ * functions provided by one component of libavutil you must explicitly include
+ * the specific header containing that feature. If you are only using
+ * media-related components, you could simply include libavutil/avutil.h, which
+ * brings in most of the "core" components.
+ *
+ * @{
+ *
+ * @defgroup lavu_crypto Crypto and Hashing
+ *
+ * @{
+ * @}
+ *
+ * @defgroup lavu_math Mathematics
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_string String Manipulation
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_mem Memory Management
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_data Data Structures
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_video Video related
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_audio Audio related
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_error Error Codes
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_log Logging Facility
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_misc Other
+ *
+ * @{
+ *
+ * @defgroup preproc_misc Preprocessor String Macros
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup version_utils Library Version Macros
+ *
+ * @{
+ *
+ * @}
+ */
+
+/**
+ * @addtogroup lavu_ver
+ * @{
+ */
+
+/**
+ * Return the LIBAVUTIL_VERSION_INT constant.
+ */
+unsigned avutil_version(void);
+
+/**
+ * Return an informative version string. This usually is the actual release
+ * version number or a git commit description. This string has no fixed format
+ * and can change any time. It should never be parsed by code.
+ */
+const char* av_version_info(void);
+
+/**
+ * Return the libavutil build-time configuration.
+ */
+const char* avutil_configuration(void);
+
+/**
+ * Return the libavutil license.
+ */
+const char* avutil_license(void);
+
+/**
+ * @}
+ */
+
+/**
+ * @addtogroup lavu_media Media Type
+ * @brief Media Type
+ */
+
+enum AVMediaType {
+ AVMEDIA_TYPE_UNKNOWN = -1, ///< Usually treated as AVMEDIA_TYPE_DATA
+ AVMEDIA_TYPE_VIDEO,
+ AVMEDIA_TYPE_AUDIO,
+ AVMEDIA_TYPE_DATA, ///< Opaque data information usually continuous
+ AVMEDIA_TYPE_SUBTITLE,
+ AVMEDIA_TYPE_ATTACHMENT, ///< Opaque data information usually sparse
+ AVMEDIA_TYPE_NB
+};
+
+/**
+ * Return a string describing the media_type enum, NULL if media_type
+ * is unknown.
+ */
+const char* av_get_media_type_string(enum AVMediaType media_type);
+
+/**
+ * @defgroup lavu_const Constants
+ * @{
+ *
+ * @defgroup lavu_enc Encoding specific
+ *
+ * @note those definition should move to avcodec
+ * @{
+ */
+
+#define FF_LAMBDA_SHIFT 7
+#define FF_LAMBDA_SCALE (1 << FF_LAMBDA_SHIFT)
+#define FF_QP2LAMBDA 118 ///< factor to convert from H.263 QP to lambda
+#define FF_LAMBDA_MAX (256 * 128 - 1)
+
+#define FF_QUALITY_SCALE FF_LAMBDA_SCALE // FIXME maybe remove
+
+/**
+ * @}
+ * @defgroup lavu_time Timestamp specific
+ *
+ * FFmpeg internal timebase and timestamp definitions
+ *
+ * @{
+ */
+
+/**
+ * @brief Undefined timestamp value
+ *
+ * Usually reported by demuxer that work on containers that do not provide
+ * either pts or dts.
+ */
+
+#define AV_NOPTS_VALUE ((int64_t)UINT64_C(0x8000000000000000))
+
+/**
+ * Internal time base represented as integer
+ */
+
+#define AV_TIME_BASE 1000000
+
+/**
+ * Internal time base represented as fractional value
+ */
+
+#define AV_TIME_BASE_Q \
+ (AVRational) { 1, AV_TIME_BASE }
+
+/**
+ * @}
+ * @}
+ * @defgroup lavu_picture Image related
+ *
+ * AVPicture types, pixel formats and basic image planes manipulation.
+ *
+ * @{
+ */
+
+enum AVPictureType {
+ AV_PICTURE_TYPE_NONE = 0, ///< Undefined
+ AV_PICTURE_TYPE_I, ///< Intra
+ AV_PICTURE_TYPE_P, ///< Predicted
+ AV_PICTURE_TYPE_B, ///< Bi-dir predicted
+ AV_PICTURE_TYPE_S, ///< S(GMC)-VOP MPEG-4
+ AV_PICTURE_TYPE_SI, ///< Switching Intra
+ AV_PICTURE_TYPE_SP, ///< Switching Predicted
+ AV_PICTURE_TYPE_BI, ///< BI type
+};
+
+/**
+ * Return a single letter to describe the given picture type
+ * pict_type.
+ *
+ * @param[in] pict_type the picture type @return a single character
+ * representing the picture type, '?' if pict_type is unknown
+ */
+char av_get_picture_type_char(enum AVPictureType pict_type);
+
+/**
+ * @}
+ */
+
+#include "common.h"
+#include "error.h"
+#include "rational.h"
+#include "version.h"
+#include "macros.h"
+#include "mathematics.h"
+#include "log.h"
+#include "pixfmt.h"
+
+/**
+ * Return x default pointer in case p is NULL.
+ */
+static inline void* av_x_if_null(const void* p, const void* x) {
+ return (void*)(intptr_t)(p ? p : x);
+}
+
+/**
+ * Compute the length of an integer list.
+ *
+ * @param elsize size in bytes of each list element (only 1, 2, 4 or 8)
+ * @param term list terminator (usually 0 or -1)
+ * @param list pointer to the list
+ * @return length of the list, in elements, not counting the terminator
+ */
+unsigned av_int_list_length_for_size(unsigned elsize, const void* list,
+ uint64_t term) av_pure;
+
+/**
+ * Compute the length of an integer list.
+ *
+ * @param term list terminator (usually 0 or -1)
+ * @param list pointer to the list
+ * @return length of the list, in elements, not counting the terminator
+ */
+#define av_int_list_length(list, term) \
+ av_int_list_length_for_size(sizeof(*(list)), list, term)
+
+/**
+ * Open a file using a UTF-8 filename.
+ * The API of this function matches POSIX fopen(), errors are returned through
+ * errno.
+ */
+FILE* av_fopen_utf8(const char* path, const char* mode);
+
+/**
+ * Return the fractional representation of the internal time base.
+ */
+AVRational av_get_time_base_q(void);
+
+#define AV_FOURCC_MAX_STRING_SIZE 32
+
+#define av_fourcc2str(fourcc) \
+ av_fourcc_make_string((char[AV_FOURCC_MAX_STRING_SIZE]){0}, fourcc)
+
+/**
+ * Fill the provided buffer with a string containing a FourCC (four-character
+ * code) representation.
+ *
+ * @param buf a buffer with size in bytes of at least
+ * AV_FOURCC_MAX_STRING_SIZE
+ * @param fourcc the fourcc to represent
+ * @return the buffer in input
+ */
+char* av_fourcc_make_string(char* buf, uint32_t fourcc);
+
+/**
+ * @}
+ * @}
+ */
+
+#endif /* AVUTIL_AVUTIL_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/buffer.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/buffer.h
new file mode 100644
index 0000000000..372de093f9
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/buffer.h
@@ -0,0 +1,324 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * @ingroup lavu_buffer
+ * refcounted data buffer API
+ */
+
+#ifndef AVUTIL_BUFFER_H
+#define AVUTIL_BUFFER_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+/**
+ * @defgroup lavu_buffer AVBuffer
+ * @ingroup lavu_data
+ *
+ * @{
+ * AVBuffer is an API for reference-counted data buffers.
+ *
+ * There are two core objects in this API -- AVBuffer and AVBufferRef. AVBuffer
+ * represents the data buffer itself; it is opaque and not meant to be accessed
+ * by the caller directly, but only through AVBufferRef. However, the caller may
+ * e.g. compare two AVBuffer pointers to check whether two different references
+ * are describing the same data buffer. AVBufferRef represents a single
+ * reference to an AVBuffer and it is the object that may be manipulated by the
+ * caller directly.
+ *
+ * There are two functions provided for creating a new AVBuffer with a single
+ * reference -- av_buffer_alloc() to just allocate a new buffer, and
+ * av_buffer_create() to wrap an existing array in an AVBuffer. From an existing
+ * reference, additional references may be created with av_buffer_ref().
+ * Use av_buffer_unref() to free a reference (this will automatically free the
+ * data once all the references are freed).
+ *
+ * The convention throughout this API and the rest of FFmpeg is such that the
+ * buffer is considered writable if there exists only one reference to it (and
+ * it has not been marked as read-only). The av_buffer_is_writable() function is
+ * provided to check whether this is true and av_buffer_make_writable() will
+ * automatically create a new writable buffer when necessary.
+ * Of course nothing prevents the calling code from violating this convention,
+ * however that is safe only when all the existing references are under its
+ * control.
+ *
+ * @note Referencing and unreferencing the buffers is thread-safe and thus
+ * may be done from multiple threads simultaneously without any need for
+ * additional locking.
+ *
+ * @note Two different references to the same buffer can point to different
+ * parts of the buffer (i.e. their AVBufferRef.data will not be equal).
+ */
+
+/**
+ * A reference counted buffer type. It is opaque and is meant to be used through
+ * references (AVBufferRef).
+ */
+typedef struct AVBuffer AVBuffer;
+
+/**
+ * A reference to a data buffer.
+ *
+ * The size of this struct is not a part of the public ABI and it is not meant
+ * to be allocated directly.
+ */
+typedef struct AVBufferRef {
+ AVBuffer* buffer;
+
+ /**
+ * The data buffer. It is considered writable if and only if
+ * this is the only reference to the buffer, in which case
+ * av_buffer_is_writable() returns 1.
+ */
+ uint8_t* data;
+ /**
+ * Size of data in bytes.
+ */
+ size_t size;
+} AVBufferRef;
+
+/**
+ * Allocate an AVBuffer of the given size using av_malloc().
+ *
+ * @return an AVBufferRef of given size or NULL when out of memory
+ */
+AVBufferRef* av_buffer_alloc(size_t size);
+
+/**
+ * Same as av_buffer_alloc(), except the returned buffer will be initialized
+ * to zero.
+ */
+AVBufferRef* av_buffer_allocz(size_t size);
+
+/**
+ * Always treat the buffer as read-only, even when it has only one
+ * reference.
+ */
+#define AV_BUFFER_FLAG_READONLY (1 << 0)
+
+/**
+ * Create an AVBuffer from an existing array.
+ *
+ * If this function is successful, data is owned by the AVBuffer. The caller may
+ * only access data through the returned AVBufferRef and references derived from
+ * it.
+ * If this function fails, data is left untouched.
+ * @param data data array
+ * @param size size of data in bytes
+ * @param free a callback for freeing this buffer's data
+ * @param opaque parameter to be got for processing or passed to free
+ * @param flags a combination of AV_BUFFER_FLAG_*
+ *
+ * @return an AVBufferRef referring to data on success, NULL on failure.
+ */
+AVBufferRef* av_buffer_create(uint8_t* data, size_t size,
+ void (*free)(void* opaque, uint8_t* data),
+ void* opaque, int flags);
+
+/**
+ * Default free callback, which calls av_free() on the buffer data.
+ * This function is meant to be passed to av_buffer_create(), not called
+ * directly.
+ */
+void av_buffer_default_free(void* opaque, uint8_t* data);
+
+/**
+ * Create a new reference to an AVBuffer.
+ *
+ * @return a new AVBufferRef referring to the same AVBuffer as buf or NULL on
+ * failure.
+ */
+AVBufferRef* av_buffer_ref(const AVBufferRef* buf);
+
+/**
+ * Free a given reference and automatically free the buffer if there are no more
+ * references to it.
+ *
+ * @param buf the reference to be freed. The pointer is set to NULL on return.
+ */
+void av_buffer_unref(AVBufferRef** buf);
+
+/**
+ * @return 1 if the caller may write to the data referred to by buf (which is
+ * true if and only if buf is the only reference to the underlying AVBuffer).
+ * Return 0 otherwise.
+ * A positive answer is valid until av_buffer_ref() is called on buf.
+ */
+int av_buffer_is_writable(const AVBufferRef* buf);
+
+/**
+ * @return the opaque parameter set by av_buffer_create.
+ */
+void* av_buffer_get_opaque(const AVBufferRef* buf);
+
+int av_buffer_get_ref_count(const AVBufferRef* buf);
+
+/**
+ * Create a writable reference from a given buffer reference, avoiding data copy
+ * if possible.
+ *
+ * @param buf buffer reference to make writable. On success, buf is either left
+ * untouched, or it is unreferenced and a new writable AVBufferRef is
+ * written in its place. On failure, buf is left untouched.
+ * @return 0 on success, a negative AVERROR on failure.
+ */
+int av_buffer_make_writable(AVBufferRef** buf);
+
+/**
+ * Reallocate a given buffer.
+ *
+ * @param buf a buffer reference to reallocate. On success, buf will be
+ * unreferenced and a new reference with the required size will be
+ * written in its place. On failure buf will be left untouched. *buf
+ * may be NULL, then a new buffer is allocated.
+ * @param size required new buffer size.
+ * @return 0 on success, a negative AVERROR on failure.
+ *
+ * @note the buffer is actually reallocated with av_realloc() only if it was
+ * initially allocated through av_buffer_realloc(NULL) and there is only one
+ * reference to it (i.e. the one passed to this function). In all other cases
+ * a new buffer is allocated and the data is copied.
+ */
+int av_buffer_realloc(AVBufferRef** buf, size_t size);
+
+/**
+ * Ensure dst refers to the same data as src.
+ *
+ * When *dst is already equivalent to src, do nothing. Otherwise unreference dst
+ * and replace it with a new reference to src.
+ *
+ * @param dst Pointer to either a valid buffer reference or NULL. On success,
+ * this will point to a buffer reference equivalent to src. On
+ * failure, dst will be left untouched.
+ * @param src A buffer reference to replace dst with. May be NULL, then this
+ * function is equivalent to av_buffer_unref(dst).
+ * @return 0 on success
+ * AVERROR(ENOMEM) on memory allocation failure.
+ */
+int av_buffer_replace(AVBufferRef** dst, const AVBufferRef* src);
+
+/**
+ * @}
+ */
+
+/**
+ * @defgroup lavu_bufferpool AVBufferPool
+ * @ingroup lavu_data
+ *
+ * @{
+ * AVBufferPool is an API for a lock-free thread-safe pool of AVBuffers.
+ *
+ * Frequently allocating and freeing large buffers may be slow. AVBufferPool is
+ * meant to solve this in cases when the caller needs a set of buffers of the
+ * same size (the most obvious use case being buffers for raw video or audio
+ * frames).
+ *
+ * At the beginning, the user must call av_buffer_pool_init() to create the
+ * buffer pool. Then whenever a buffer is needed, call av_buffer_pool_get() to
+ * get a reference to a new buffer, similar to av_buffer_alloc(). This new
+ * reference works in all aspects the same way as the one created by
+ * av_buffer_alloc(). However, when the last reference to this buffer is
+ * unreferenced, it is returned to the pool instead of being freed and will be
+ * reused for subsequent av_buffer_pool_get() calls.
+ *
+ * When the caller is done with the pool and no longer needs to allocate any new
+ * buffers, av_buffer_pool_uninit() must be called to mark the pool as freeable.
+ * Once all the buffers are released, it will automatically be freed.
+ *
+ * Allocating and releasing buffers with this API is thread-safe as long as
+ * either the default alloc callback is used, or the user-supplied one is
+ * thread-safe.
+ */
+
+/**
+ * The buffer pool. This structure is opaque and not meant to be accessed
+ * directly. It is allocated with av_buffer_pool_init() and freed with
+ * av_buffer_pool_uninit().
+ */
+typedef struct AVBufferPool AVBufferPool;
+
+/**
+ * Allocate and initialize a buffer pool.
+ *
+ * @param size size of each buffer in this pool
+ * @param alloc a function that will be used to allocate new buffers when the
+ * pool is empty. May be NULL, then the default allocator will be used
+ * (av_buffer_alloc()).
+ * @return newly created buffer pool on success, NULL on error.
+ */
+AVBufferPool* av_buffer_pool_init(size_t size,
+ AVBufferRef* (*alloc)(size_t size));
+
+/**
+ * Allocate and initialize a buffer pool with a more complex allocator.
+ *
+ * @param size size of each buffer in this pool
+ * @param opaque arbitrary user data used by the allocator
+ * @param alloc a function that will be used to allocate new buffers when the
+ * pool is empty. May be NULL, then the default allocator will be
+ * used (av_buffer_alloc()).
+ * @param pool_free a function that will be called immediately before the pool
+ * is freed. I.e. after av_buffer_pool_uninit() is called
+ * by the caller and all the frames are returned to the pool
+ * and freed. It is intended to uninitialize the user opaque
+ * data. May be NULL.
+ * @return newly created buffer pool on success, NULL on error.
+ */
+AVBufferPool* av_buffer_pool_init2(size_t size, void* opaque,
+ AVBufferRef* (*alloc)(void* opaque,
+ size_t size),
+ void (*pool_free)(void* opaque));
+
+/**
+ * Mark the pool as being available for freeing. It will actually be freed only
+ * once all the allocated buffers associated with the pool are released. Thus it
+ * is safe to call this function while some of the allocated buffers are still
+ * in use.
+ *
+ * @param pool pointer to the pool to be freed. It will be set to NULL.
+ */
+void av_buffer_pool_uninit(AVBufferPool** pool);
+
+/**
+ * Allocate a new AVBuffer, reusing an old buffer from the pool when available.
+ * This function may be called simultaneously from multiple threads.
+ *
+ * @return a reference to the new buffer on success, NULL on error.
+ */
+AVBufferRef* av_buffer_pool_get(AVBufferPool* pool);
+
+/**
+ * Query the original opaque parameter of an allocated buffer in the pool.
+ *
+ * @param ref a buffer reference to a buffer returned by av_buffer_pool_get.
+ * @return the opaque parameter set by the buffer allocator function of the
+ * buffer pool.
+ *
+ * @note the opaque parameter of ref is used by the buffer pool implementation,
+ * therefore you have to use this function to access the original opaque
+ * parameter of an allocated buffer.
+ */
+void* av_buffer_pool_buffer_get_opaque(const AVBufferRef* ref);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_BUFFER_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/channel_layout.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/channel_layout.h
new file mode 100644
index 0000000000..ce06396099
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/channel_layout.h
@@ -0,0 +1,270 @@
+/*
+ * Copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ * Copyright (c) 2008 Peter Ross
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_CHANNEL_LAYOUT_H
+#define AVUTIL_CHANNEL_LAYOUT_H
+
+#include <stdint.h>
+
+/**
+ * @file
+ * audio channel layout utility functions
+ */
+
+/**
+ * @addtogroup lavu_audio
+ * @{
+ */
+
+/**
+ * @defgroup channel_masks Audio channel masks
+ *
+ * A channel layout is a 64-bits integer with a bit set for every channel.
+ * The number of bits set must be equal to the number of channels.
+ * The value 0 means that the channel layout is not known.
+ * @note this data structure is not powerful enough to handle channels
+ * combinations that have the same channel multiple times, such as
+ * dual-mono.
+ *
+ * @{
+ */
+#define AV_CH_FRONT_LEFT 0x00000001
+#define AV_CH_FRONT_RIGHT 0x00000002
+#define AV_CH_FRONT_CENTER 0x00000004
+#define AV_CH_LOW_FREQUENCY 0x00000008
+#define AV_CH_BACK_LEFT 0x00000010
+#define AV_CH_BACK_RIGHT 0x00000020
+#define AV_CH_FRONT_LEFT_OF_CENTER 0x00000040
+#define AV_CH_FRONT_RIGHT_OF_CENTER 0x00000080
+#define AV_CH_BACK_CENTER 0x00000100
+#define AV_CH_SIDE_LEFT 0x00000200
+#define AV_CH_SIDE_RIGHT 0x00000400
+#define AV_CH_TOP_CENTER 0x00000800
+#define AV_CH_TOP_FRONT_LEFT 0x00001000
+#define AV_CH_TOP_FRONT_CENTER 0x00002000
+#define AV_CH_TOP_FRONT_RIGHT 0x00004000
+#define AV_CH_TOP_BACK_LEFT 0x00008000
+#define AV_CH_TOP_BACK_CENTER 0x00010000
+#define AV_CH_TOP_BACK_RIGHT 0x00020000
+#define AV_CH_STEREO_LEFT 0x20000000 ///< Stereo downmix.
+#define AV_CH_STEREO_RIGHT 0x40000000 ///< See AV_CH_STEREO_LEFT.
+#define AV_CH_WIDE_LEFT 0x0000000080000000ULL
+#define AV_CH_WIDE_RIGHT 0x0000000100000000ULL
+#define AV_CH_SURROUND_DIRECT_LEFT 0x0000000200000000ULL
+#define AV_CH_SURROUND_DIRECT_RIGHT 0x0000000400000000ULL
+#define AV_CH_LOW_FREQUENCY_2 0x0000000800000000ULL
+#define AV_CH_TOP_SIDE_LEFT 0x0000001000000000ULL
+#define AV_CH_TOP_SIDE_RIGHT 0x0000002000000000ULL
+#define AV_CH_BOTTOM_FRONT_CENTER 0x0000004000000000ULL
+#define AV_CH_BOTTOM_FRONT_LEFT 0x0000008000000000ULL
+#define AV_CH_BOTTOM_FRONT_RIGHT 0x0000010000000000ULL
+
+/** Channel mask value used for AVCodecContext.request_channel_layout
+ to indicate that the user requests the channel order of the decoder output
+ to be the native codec channel order. */
+#define AV_CH_LAYOUT_NATIVE 0x8000000000000000ULL
+
+/**
+ * @}
+ * @defgroup channel_mask_c Audio channel layouts
+ * @{
+ * */
+#define AV_CH_LAYOUT_MONO (AV_CH_FRONT_CENTER)
+#define AV_CH_LAYOUT_STEREO (AV_CH_FRONT_LEFT | AV_CH_FRONT_RIGHT)
+#define AV_CH_LAYOUT_2POINT1 (AV_CH_LAYOUT_STEREO | AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_2_1 (AV_CH_LAYOUT_STEREO | AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_SURROUND (AV_CH_LAYOUT_STEREO | AV_CH_FRONT_CENTER)
+#define AV_CH_LAYOUT_3POINT1 (AV_CH_LAYOUT_SURROUND | AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_4POINT0 (AV_CH_LAYOUT_SURROUND | AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_4POINT1 (AV_CH_LAYOUT_4POINT0 | AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_2_2 \
+ (AV_CH_LAYOUT_STEREO | AV_CH_SIDE_LEFT | AV_CH_SIDE_RIGHT)
+#define AV_CH_LAYOUT_QUAD \
+ (AV_CH_LAYOUT_STEREO | AV_CH_BACK_LEFT | AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_5POINT0 \
+ (AV_CH_LAYOUT_SURROUND | AV_CH_SIDE_LEFT | AV_CH_SIDE_RIGHT)
+#define AV_CH_LAYOUT_5POINT1 (AV_CH_LAYOUT_5POINT0 | AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_5POINT0_BACK \
+ (AV_CH_LAYOUT_SURROUND | AV_CH_BACK_LEFT | AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_5POINT1_BACK \
+ (AV_CH_LAYOUT_5POINT0_BACK | AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_6POINT0 (AV_CH_LAYOUT_5POINT0 | AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_6POINT0_FRONT \
+ (AV_CH_LAYOUT_2_2 | AV_CH_FRONT_LEFT_OF_CENTER | AV_CH_FRONT_RIGHT_OF_CENTER)
+#define AV_CH_LAYOUT_HEXAGONAL (AV_CH_LAYOUT_5POINT0_BACK | AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_6POINT1 (AV_CH_LAYOUT_5POINT1 | AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_6POINT1_BACK \
+ (AV_CH_LAYOUT_5POINT1_BACK | AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_6POINT1_FRONT \
+ (AV_CH_LAYOUT_6POINT0_FRONT | AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_7POINT0 \
+ (AV_CH_LAYOUT_5POINT0 | AV_CH_BACK_LEFT | AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_7POINT0_FRONT \
+ (AV_CH_LAYOUT_5POINT0 | AV_CH_FRONT_LEFT_OF_CENTER | \
+ AV_CH_FRONT_RIGHT_OF_CENTER)
+#define AV_CH_LAYOUT_7POINT1 \
+ (AV_CH_LAYOUT_5POINT1 | AV_CH_BACK_LEFT | AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_7POINT1_WIDE \
+ (AV_CH_LAYOUT_5POINT1 | AV_CH_FRONT_LEFT_OF_CENTER | \
+ AV_CH_FRONT_RIGHT_OF_CENTER)
+#define AV_CH_LAYOUT_7POINT1_WIDE_BACK \
+ (AV_CH_LAYOUT_5POINT1_BACK | AV_CH_FRONT_LEFT_OF_CENTER | \
+ AV_CH_FRONT_RIGHT_OF_CENTER)
+#define AV_CH_LAYOUT_OCTAGONAL \
+ (AV_CH_LAYOUT_5POINT0 | AV_CH_BACK_LEFT | AV_CH_BACK_CENTER | \
+ AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_HEXADECAGONAL \
+ (AV_CH_LAYOUT_OCTAGONAL | AV_CH_WIDE_LEFT | AV_CH_WIDE_RIGHT | \
+ AV_CH_TOP_BACK_LEFT | AV_CH_TOP_BACK_RIGHT | AV_CH_TOP_BACK_CENTER | \
+ AV_CH_TOP_FRONT_CENTER | AV_CH_TOP_FRONT_LEFT | AV_CH_TOP_FRONT_RIGHT)
+#define AV_CH_LAYOUT_STEREO_DOWNMIX (AV_CH_STEREO_LEFT | AV_CH_STEREO_RIGHT)
+#define AV_CH_LAYOUT_22POINT2 \
+ (AV_CH_LAYOUT_5POINT1_BACK | AV_CH_FRONT_LEFT_OF_CENTER | \
+ AV_CH_FRONT_RIGHT_OF_CENTER | AV_CH_BACK_CENTER | AV_CH_LOW_FREQUENCY_2 | \
+ AV_CH_SIDE_LEFT | AV_CH_SIDE_RIGHT | AV_CH_TOP_FRONT_LEFT | \
+ AV_CH_TOP_FRONT_RIGHT | AV_CH_TOP_FRONT_CENTER | AV_CH_TOP_CENTER | \
+ AV_CH_TOP_BACK_LEFT | AV_CH_TOP_BACK_RIGHT | AV_CH_TOP_SIDE_LEFT | \
+ AV_CH_TOP_SIDE_RIGHT | AV_CH_TOP_BACK_CENTER | AV_CH_BOTTOM_FRONT_CENTER | \
+ AV_CH_BOTTOM_FRONT_LEFT | AV_CH_BOTTOM_FRONT_RIGHT)
+
+enum AVMatrixEncoding {
+ AV_MATRIX_ENCODING_NONE,
+ AV_MATRIX_ENCODING_DOLBY,
+ AV_MATRIX_ENCODING_DPLII,
+ AV_MATRIX_ENCODING_DPLIIX,
+ AV_MATRIX_ENCODING_DPLIIZ,
+ AV_MATRIX_ENCODING_DOLBYEX,
+ AV_MATRIX_ENCODING_DOLBYHEADPHONE,
+ AV_MATRIX_ENCODING_NB
+};
+
+/**
+ * Return a channel layout id that matches name, or 0 if no match is found.
+ *
+ * name can be one or several of the following notations,
+ * separated by '+' or '|':
+ * - the name of an usual channel layout (mono, stereo, 4.0, quad, 5.0,
+ * 5.0(side), 5.1, 5.1(side), 7.1, 7.1(wide), downmix);
+ * - the name of a single channel (FL, FR, FC, LFE, BL, BR, FLC, FRC, BC,
+ * SL, SR, TC, TFL, TFC, TFR, TBL, TBC, TBR, DL, DR);
+ * - a number of channels, in decimal, followed by 'c', yielding
+ * the default channel layout for that number of channels (@see
+ * av_get_default_channel_layout);
+ * - a channel layout mask, in hexadecimal starting with "0x" (see the
+ * AV_CH_* macros).
+ *
+ * Example: "stereo+FC" = "2c+FC" = "2c+1c" = "0x7"
+ */
+uint64_t av_get_channel_layout(const char* name);
+
+/**
+ * Return a channel layout and the number of channels based on the specified
+ * name.
+ *
+ * This function is similar to (@see av_get_channel_layout), but can also parse
+ * unknown channel layout specifications.
+ *
+ * @param[in] name channel layout specification string
+ * @param[out] channel_layout parsed channel layout (0 if unknown)
+ * @param[out] nb_channels number of channels
+ *
+ * @return 0 on success, AVERROR(EINVAL) if the parsing fails.
+ */
+int av_get_extended_channel_layout(const char* name, uint64_t* channel_layout,
+ int* nb_channels);
+
+/**
+ * Return a description of a channel layout.
+ * If nb_channels is <= 0, it is guessed from the channel_layout.
+ *
+ * @param buf put here the string containing the channel layout
+ * @param buf_size size in bytes of the buffer
+ */
+void av_get_channel_layout_string(char* buf, int buf_size, int nb_channels,
+ uint64_t channel_layout);
+
+struct AVBPrint;
+/**
+ * Append a description of a channel layout to a bprint buffer.
+ */
+void av_bprint_channel_layout(struct AVBPrint* bp, int nb_channels,
+ uint64_t channel_layout);
+
+/**
+ * Return the number of channels in the channel layout.
+ */
+int av_get_channel_layout_nb_channels(uint64_t channel_layout);
+
+/**
+ * Return default channel layout for a given number of channels.
+ */
+int64_t av_get_default_channel_layout(int nb_channels);
+
+/**
+ * Get the index of a channel in channel_layout.
+ *
+ * @param channel a channel layout describing exactly one channel which must be
+ * present in channel_layout.
+ *
+ * @return index of channel in channel_layout on success, a negative AVERROR
+ * on error.
+ */
+int av_get_channel_layout_channel_index(uint64_t channel_layout,
+ uint64_t channel);
+
+/**
+ * Get the channel with the given index in channel_layout.
+ */
+uint64_t av_channel_layout_extract_channel(uint64_t channel_layout, int index);
+
+/**
+ * Get the name of a given channel.
+ *
+ * @return channel name on success, NULL on error.
+ */
+const char* av_get_channel_name(uint64_t channel);
+
+/**
+ * Get the description of a given channel.
+ *
+ * @param channel a channel layout with a single channel
+ * @return channel description on success, NULL on error
+ */
+const char* av_get_channel_description(uint64_t channel);
+
+/**
+ * Get the value and name of a standard channel layout.
+ *
+ * @param[in] index index in an internal list, starting at 0
+ * @param[out] layout channel layout mask
+ * @param[out] name name of the layout
+ * @return 0 if the layout exists,
+ * <0 if index is beyond the limits
+ */
+int av_get_standard_channel_layout(unsigned index, uint64_t* layout,
+ const char** name);
+
+/**
+ * @}
+ * @}
+ */
+
+#endif /* AVUTIL_CHANNEL_LAYOUT_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/common.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/common.h
new file mode 100644
index 0000000000..7ba1e43c6b
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/common.h
@@ -0,0 +1,590 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * common internal and external API header
+ */
+
+#ifndef AVUTIL_COMMON_H
+#define AVUTIL_COMMON_H
+
+#if defined(__cplusplus) && !defined(__STDC_CONSTANT_MACROS) && \
+ !defined(UINT64_C)
+# error missing -D__STDC_CONSTANT_MACROS / #define __STDC_CONSTANT_MACROS
+#endif
+
+#include <errno.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <math.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "attributes.h"
+#include "macros.h"
+#include "version.h"
+
+// rounded division & shift
+#define RSHIFT(a, b) \
+ ((a) > 0 ? ((a) + ((1 << (b)) >> 1)) >> (b) \
+ : ((a) + ((1 << (b)) >> 1) - 1) >> (b))
+/* assume b>0 */
+#define ROUNDED_DIV(a, b) \
+ (((a) >= 0 ? (a) + ((b) >> 1) : (a) - ((b) >> 1)) / (b))
+/* Fast a/(1<<b) rounded toward +inf. Assume a>=0 and b>=0 */
+#define AV_CEIL_RSHIFT(a, b) \
+ (!av_builtin_constant_p(b) ? -((-(a)) >> (b)) : ((a) + (1 << (b)) - 1) >> (b))
+/* Backwards compat. */
+#define FF_CEIL_RSHIFT AV_CEIL_RSHIFT
+
+#define FFUDIV(a, b) (((a) > 0 ? (a) : (a) - (b) + 1) / (b))
+#define FFUMOD(a, b) ((a) - (b)*FFUDIV(a, b))
+
+/**
+ * Absolute value, Note, INT_MIN / INT64_MIN result in undefined behavior as
+ * they are not representable as absolute values of their type. This is the same
+ * as with *abs()
+ * @see FFNABS()
+ */
+#define FFABS(a) ((a) >= 0 ? (a) : (-(a)))
+#define FFSIGN(a) ((a) > 0 ? 1 : -1)
+
+/**
+ * Negative Absolute value.
+ * this works for all integers of all types.
+ * As with many macros, this evaluates its argument twice, it thus must not have
+ * a sideeffect, that is FFNABS(x++) has undefined behavior.
+ */
+#define FFNABS(a) ((a) <= 0 ? (a) : (-(a)))
+
+/**
+ * Unsigned Absolute value.
+ * This takes the absolute value of a signed int and returns it as a unsigned.
+ * This also works with INT_MIN which would otherwise not be representable
+ * As with many macros, this evaluates its argument twice.
+ */
+#define FFABSU(a) ((a) <= 0 ? -(unsigned)(a) : (unsigned)(a))
+#define FFABS64U(a) ((a) <= 0 ? -(uint64_t)(a) : (uint64_t)(a))
+
+/* misc math functions */
+
+#ifdef HAVE_AV_CONFIG_H
+# include "config.h"
+# include "intmath.h"
+#endif
+
+#ifndef av_ceil_log2
+# define av_ceil_log2 av_ceil_log2_c
+#endif
+#ifndef av_clip
+# define av_clip av_clip_c
+#endif
+#ifndef av_clip64
+# define av_clip64 av_clip64_c
+#endif
+#ifndef av_clip_uint8
+# define av_clip_uint8 av_clip_uint8_c
+#endif
+#ifndef av_clip_int8
+# define av_clip_int8 av_clip_int8_c
+#endif
+#ifndef av_clip_uint16
+# define av_clip_uint16 av_clip_uint16_c
+#endif
+#ifndef av_clip_int16
+# define av_clip_int16 av_clip_int16_c
+#endif
+#ifndef av_clipl_int32
+# define av_clipl_int32 av_clipl_int32_c
+#endif
+#ifndef av_clip_intp2
+# define av_clip_intp2 av_clip_intp2_c
+#endif
+#ifndef av_clip_uintp2
+# define av_clip_uintp2 av_clip_uintp2_c
+#endif
+#ifndef av_mod_uintp2
+# define av_mod_uintp2 av_mod_uintp2_c
+#endif
+#ifndef av_sat_add32
+# define av_sat_add32 av_sat_add32_c
+#endif
+#ifndef av_sat_dadd32
+# define av_sat_dadd32 av_sat_dadd32_c
+#endif
+#ifndef av_sat_sub32
+# define av_sat_sub32 av_sat_sub32_c
+#endif
+#ifndef av_sat_dsub32
+# define av_sat_dsub32 av_sat_dsub32_c
+#endif
+#ifndef av_sat_add64
+# define av_sat_add64 av_sat_add64_c
+#endif
+#ifndef av_sat_sub64
+# define av_sat_sub64 av_sat_sub64_c
+#endif
+#ifndef av_clipf
+# define av_clipf av_clipf_c
+#endif
+#ifndef av_clipd
+# define av_clipd av_clipd_c
+#endif
+#ifndef av_popcount
+# define av_popcount av_popcount_c
+#endif
+#ifndef av_popcount64
+# define av_popcount64 av_popcount64_c
+#endif
+#ifndef av_parity
+# define av_parity av_parity_c
+#endif
+
+#ifndef av_log2
+av_const int av_log2(unsigned v);
+#endif
+
+#ifndef av_log2_16bit
+av_const int av_log2_16bit(unsigned v);
+#endif
+
+/**
+ * Clip a signed integer value into the amin-amax range.
+ * @param a value to clip
+ * @param amin minimum value of the clip range
+ * @param amax maximum value of the clip range
+ * @return clipped value
+ */
+static av_always_inline av_const int av_clip_c(int a, int amin, int amax) {
+#if defined(HAVE_AV_CONFIG_H) && defined(ASSERT_LEVEL) && ASSERT_LEVEL >= 2
+ if (amin > amax) abort();
+#endif
+ if (a < amin)
+ return amin;
+ else if (a > amax)
+ return amax;
+ else
+ return a;
+}
+
+/**
+ * Clip a signed 64bit integer value into the amin-amax range.
+ * @param a value to clip
+ * @param amin minimum value of the clip range
+ * @param amax maximum value of the clip range
+ * @return clipped value
+ */
+static av_always_inline av_const int64_t av_clip64_c(int64_t a, int64_t amin,
+ int64_t amax) {
+#if defined(HAVE_AV_CONFIG_H) && defined(ASSERT_LEVEL) && ASSERT_LEVEL >= 2
+ if (amin > amax) abort();
+#endif
+ if (a < amin)
+ return amin;
+ else if (a > amax)
+ return amax;
+ else
+ return a;
+}
+
+/**
+ * Clip a signed integer value into the 0-255 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const uint8_t av_clip_uint8_c(int a) {
+ if (a & (~0xFF))
+ return (~a) >> 31;
+ else
+ return a;
+}
+
+/**
+ * Clip a signed integer value into the -128,127 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const int8_t av_clip_int8_c(int a) {
+ if ((a + 0x80U) & ~0xFF)
+ return (a >> 31) ^ 0x7F;
+ else
+ return a;
+}
+
+/**
+ * Clip a signed integer value into the 0-65535 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const uint16_t av_clip_uint16_c(int a) {
+ if (a & (~0xFFFF))
+ return (~a) >> 31;
+ else
+ return a;
+}
+
+/**
+ * Clip a signed integer value into the -32768,32767 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const int16_t av_clip_int16_c(int a) {
+ if ((a + 0x8000U) & ~0xFFFF)
+ return (a >> 31) ^ 0x7FFF;
+ else
+ return a;
+}
+
+/**
+ * Clip a signed 64-bit integer value into the -2147483648,2147483647 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const int32_t av_clipl_int32_c(int64_t a) {
+ if ((a + 0x80000000u) & ~UINT64_C(0xFFFFFFFF))
+ return (int32_t)((a >> 63) ^ 0x7FFFFFFF);
+ else
+ return (int32_t)a;
+}
+
+/**
+ * Clip a signed integer into the -(2^p),(2^p-1) range.
+ * @param a value to clip
+ * @param p bit position to clip at
+ * @return clipped value
+ */
+static av_always_inline av_const int av_clip_intp2_c(int a, int p) {
+ if (((unsigned)a + (1 << p)) & ~((2 << p) - 1))
+ return (a >> 31) ^ ((1 << p) - 1);
+ else
+ return a;
+}
+
+/**
+ * Clip a signed integer to an unsigned power of two range.
+ * @param a value to clip
+ * @param p bit position to clip at
+ * @return clipped value
+ */
+static av_always_inline av_const unsigned av_clip_uintp2_c(int a, int p) {
+ if (a & ~((1 << p) - 1))
+ return (~a) >> 31 & ((1 << p) - 1);
+ else
+ return a;
+}
+
+/**
+ * Clear high bits from an unsigned integer starting with specific bit position
+ * @param a value to clip
+ * @param p bit position to clip at
+ * @return clipped value
+ */
+static av_always_inline av_const unsigned av_mod_uintp2_c(unsigned a,
+ unsigned p) {
+ return a & ((1U << p) - 1);
+}
+
+/**
+ * Add two signed 32-bit values with saturation.
+ *
+ * @param a one value
+ * @param b another value
+ * @return sum with signed saturation
+ */
+static av_always_inline int av_sat_add32_c(int a, int b) {
+ return av_clipl_int32((int64_t)a + b);
+}
+
+/**
+ * Add a doubled value to another value with saturation at both stages.
+ *
+ * @param a first value
+ * @param b value doubled and added to a
+ * @return sum sat(a + sat(2*b)) with signed saturation
+ */
+static av_always_inline int av_sat_dadd32_c(int a, int b) {
+ return av_sat_add32(a, av_sat_add32(b, b));
+}
+
+/**
+ * Subtract two signed 32-bit values with saturation.
+ *
+ * @param a one value
+ * @param b another value
+ * @return difference with signed saturation
+ */
+static av_always_inline int av_sat_sub32_c(int a, int b) {
+ return av_clipl_int32((int64_t)a - b);
+}
+
+/**
+ * Subtract a doubled value from another value with saturation at both stages.
+ *
+ * @param a first value
+ * @param b value doubled and subtracted from a
+ * @return difference sat(a - sat(2*b)) with signed saturation
+ */
+static av_always_inline int av_sat_dsub32_c(int a, int b) {
+ return av_sat_sub32(a, av_sat_add32(b, b));
+}
+
+/**
+ * Add two signed 64-bit values with saturation.
+ *
+ * @param a one value
+ * @param b another value
+ * @return sum with signed saturation
+ */
+static av_always_inline int64_t av_sat_add64_c(int64_t a, int64_t b) {
+#if (!defined(__INTEL_COMPILER) && AV_GCC_VERSION_AT_LEAST(5, 1)) || \
+ AV_HAS_BUILTIN(__builtin_add_overflow)
+ int64_t tmp;
+ return !__builtin_add_overflow(a, b, &tmp)
+ ? tmp
+ : (tmp < 0 ? INT64_MAX : INT64_MIN);
+#else
+ int64_t s = a + (uint64_t)b;
+ if ((int64_t)(a ^ b | ~s ^ b) >= 0) return INT64_MAX ^ (b >> 63);
+ return s;
+#endif
+}
+
+/**
+ * Subtract two signed 64-bit values with saturation.
+ *
+ * @param a one value
+ * @param b another value
+ * @return difference with signed saturation
+ */
+static av_always_inline int64_t av_sat_sub64_c(int64_t a, int64_t b) {
+#if (!defined(__INTEL_COMPILER) && AV_GCC_VERSION_AT_LEAST(5, 1)) || \
+ AV_HAS_BUILTIN(__builtin_sub_overflow)
+ int64_t tmp;
+ return !__builtin_sub_overflow(a, b, &tmp)
+ ? tmp
+ : (tmp < 0 ? INT64_MAX : INT64_MIN);
+#else
+ if (b <= 0 && a >= INT64_MAX + b) return INT64_MAX;
+ if (b >= 0 && a <= INT64_MIN + b) return INT64_MIN;
+ return a - b;
+#endif
+}
+
+/**
+ * Clip a float value into the amin-amax range.
+ * If a is nan or -inf amin will be returned.
+ * If a is +inf amax will be returned.
+ * @param a value to clip
+ * @param amin minimum value of the clip range
+ * @param amax maximum value of the clip range
+ * @return clipped value
+ */
+static av_always_inline av_const float av_clipf_c(float a, float amin,
+ float amax) {
+#if defined(HAVE_AV_CONFIG_H) && defined(ASSERT_LEVEL) && ASSERT_LEVEL >= 2
+ if (amin > amax) abort();
+#endif
+ return FFMIN(FFMAX(a, amin), amax);
+}
+
+/**
+ * Clip a double value into the amin-amax range.
+ * If a is nan or -inf amin will be returned.
+ * If a is +inf amax will be returned.
+ * @param a value to clip
+ * @param amin minimum value of the clip range
+ * @param amax maximum value of the clip range
+ * @return clipped value
+ */
+static av_always_inline av_const double av_clipd_c(double a, double amin,
+ double amax) {
+#if defined(HAVE_AV_CONFIG_H) && defined(ASSERT_LEVEL) && ASSERT_LEVEL >= 2
+ if (amin > amax) abort();
+#endif
+ return FFMIN(FFMAX(a, amin), amax);
+}
+
+/** Compute ceil(log2(x)).
+ * @param x value used to compute ceil(log2(x))
+ * @return computed ceiling of log2(x)
+ */
+static av_always_inline av_const int av_ceil_log2_c(int x) {
+ return av_log2((x - 1U) << 1);
+}
+
+/**
+ * Count number of bits set to one in x
+ * @param x value to count bits of
+ * @return the number of bits set to one in x
+ */
+static av_always_inline av_const int av_popcount_c(uint32_t x) {
+ x -= (x >> 1) & 0x55555555;
+ x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
+ x = (x + (x >> 4)) & 0x0F0F0F0F;
+ x += x >> 8;
+ return (x + (x >> 16)) & 0x3F;
+}
+
+/**
+ * Count number of bits set to one in x
+ * @param x value to count bits of
+ * @return the number of bits set to one in x
+ */
+static av_always_inline av_const int av_popcount64_c(uint64_t x) {
+ return av_popcount((uint32_t)x) + av_popcount((uint32_t)(x >> 32));
+}
+
+static av_always_inline av_const int av_parity_c(uint32_t v) {
+ return av_popcount(v) & 1;
+}
+
+/**
+ * Convert a UTF-8 character (up to 4 bytes) to its 32-bit UCS-4 encoded form.
+ *
+ * @param val Output value, must be an lvalue of type uint32_t.
+ * @param GET_BYTE Expression reading one byte from the input.
+ * Evaluated up to 7 times (4 for the currently
+ * assigned Unicode range). With a memory buffer
+ * input, this could be *ptr++, or if you want to make sure
+ * that *ptr stops at the end of a NULL terminated string then
+ * *ptr ? *ptr++ : 0
+ * @param ERROR Expression to be evaluated on invalid input,
+ * typically a goto statement.
+ *
+ * @warning ERROR should not contain a loop control statement which
+ * could interact with the internal while loop, and should force an
+ * exit from the macro code (e.g. through a goto or a return) in order
+ * to prevent undefined results.
+ */
+#define GET_UTF8(val, GET_BYTE, ERROR) \
+ val = (GET_BYTE); \
+ { \
+ uint32_t top = (val & 128) >> 1; \
+ if ((val & 0xc0) == 0x80 || val >= 0xFE) { \
+ ERROR \
+ } \
+ while (val & top) { \
+ unsigned int tmp = (GET_BYTE)-128; \
+ if (tmp >> 6) { \
+ ERROR \
+ } \
+ val = (val << 6) + tmp; \
+ top <<= 5; \
+ } \
+ val &= (top << 1) - 1; \
+ }
+
+/**
+ * Convert a UTF-16 character (2 or 4 bytes) to its 32-bit UCS-4 encoded form.
+ *
+ * @param val Output value, must be an lvalue of type uint32_t.
+ * @param GET_16BIT Expression returning two bytes of UTF-16 data converted
+ * to native byte order. Evaluated one or two times.
+ * @param ERROR Expression to be evaluated on invalid input,
+ * typically a goto statement.
+ */
+#define GET_UTF16(val, GET_16BIT, ERROR) \
+ val = (GET_16BIT); \
+ { \
+ unsigned int hi = val - 0xD800; \
+ if (hi < 0x800) { \
+ val = (GET_16BIT)-0xDC00; \
+ if (val > 0x3FFU || hi > 0x3FFU) { \
+ ERROR \
+ } \
+ val += (hi << 10) + 0x10000; \
+ } \
+ }
+
+/**
+ * @def PUT_UTF8(val, tmp, PUT_BYTE)
+ * Convert a 32-bit Unicode character to its UTF-8 encoded form (up to 4 bytes
+ * long).
+ * @param val is an input-only argument and should be of type uint32_t. It holds
+ * a UCS-4 encoded Unicode character that is to be converted to UTF-8. If
+ * val is given as a function it is executed only once.
+ * @param tmp is a temporary variable and should be of type uint8_t. It
+ * represents an intermediate value during conversion that is to be
+ * output by PUT_BYTE.
+ * @param PUT_BYTE writes the converted UTF-8 bytes to any proper destination.
+ * It could be a function or a statement, and uses tmp as the input byte.
+ * For example, PUT_BYTE could be "*output++ = tmp;" PUT_BYTE will be
+ * executed up to 4 times for values in the valid UTF-8 range and up to
+ * 7 times in the general case, depending on the length of the converted
+ * Unicode character.
+ */
+#define PUT_UTF8(val, tmp, PUT_BYTE) \
+ { \
+ int bytes, shift; \
+ uint32_t in = val; \
+ if (in < 0x80) { \
+ tmp = in; \
+ PUT_BYTE \
+ } else { \
+ bytes = (av_log2(in) + 4) / 5; \
+ shift = (bytes - 1) * 6; \
+ tmp = (256 - (256 >> bytes)) | (in >> shift); \
+ PUT_BYTE \
+ while (shift >= 6) { \
+ shift -= 6; \
+ tmp = 0x80 | ((in >> shift) & 0x3f); \
+ PUT_BYTE \
+ } \
+ } \
+ }
+
+/**
+ * @def PUT_UTF16(val, tmp, PUT_16BIT)
+ * Convert a 32-bit Unicode character to its UTF-16 encoded form (2 or 4 bytes).
+ * @param val is an input-only argument and should be of type uint32_t. It holds
+ * a UCS-4 encoded Unicode character that is to be converted to UTF-16. If
+ * val is given as a function it is executed only once.
+ * @param tmp is a temporary variable and should be of type uint16_t. It
+ * represents an intermediate value during conversion that is to be
+ * output by PUT_16BIT.
+ * @param PUT_16BIT writes the converted UTF-16 data to any proper destination
+ * in desired endianness. It could be a function or a statement, and uses tmp
+ * as the input byte. For example, PUT_BYTE could be "*output++ = tmp;"
+ * PUT_BYTE will be executed 1 or 2 times depending on input character.
+ */
+#define PUT_UTF16(val, tmp, PUT_16BIT) \
+ { \
+ uint32_t in = val; \
+ if (in < 0x10000) { \
+ tmp = in; \
+ PUT_16BIT \
+ } else { \
+ tmp = 0xD800 | ((in - 0x10000) >> 10); \
+ PUT_16BIT \
+ tmp = 0xDC00 | ((in - 0x10000) & 0x3FF); \
+ PUT_16BIT \
+ } \
+ }
+
+#include "mem.h"
+
+#ifdef HAVE_AV_CONFIG_H
+# include "internal.h"
+#endif /* HAVE_AV_CONFIG_H */
+
+#endif /* AVUTIL_COMMON_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/cpu.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/cpu.h
new file mode 100644
index 0000000000..1c383f3b06
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/cpu.h
@@ -0,0 +1,138 @@
+/*
+ * Copyright (c) 2000, 2001, 2002 Fabrice Bellard
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_CPU_H
+#define AVUTIL_CPU_H
+
+#include <stddef.h>
+
+#define AV_CPU_FLAG_FORCE 0x80000000 /* force usage of selected flags (OR) */
+
+/* lower 16 bits - CPU features */
+#define AV_CPU_FLAG_MMX 0x0001 ///< standard MMX
+#define AV_CPU_FLAG_MMXEXT 0x0002 ///< SSE integer functions or AMD MMX ext
+#define AV_CPU_FLAG_MMX2 0x0002 ///< SSE integer functions or AMD MMX ext
+#define AV_CPU_FLAG_3DNOW 0x0004 ///< AMD 3DNOW
+#define AV_CPU_FLAG_SSE 0x0008 ///< SSE functions
+#define AV_CPU_FLAG_SSE2 0x0010 ///< PIV SSE2 functions
+#define AV_CPU_FLAG_SSE2SLOW \
+ 0x40000000 ///< SSE2 supported, but usually not faster
+ ///< than regular MMX/SSE (e.g. Core1)
+#define AV_CPU_FLAG_3DNOWEXT 0x0020 ///< AMD 3DNowExt
+#define AV_CPU_FLAG_SSE3 0x0040 ///< Prescott SSE3 functions
+#define AV_CPU_FLAG_SSE3SLOW \
+ 0x20000000 ///< SSE3 supported, but usually not faster
+ ///< than regular MMX/SSE (e.g. Core1)
+#define AV_CPU_FLAG_SSSE3 0x0080 ///< Conroe SSSE3 functions
+#define AV_CPU_FLAG_SSSE3SLOW \
+ 0x4000000 ///< SSSE3 supported, but usually not faster
+#define AV_CPU_FLAG_ATOM \
+ 0x10000000 ///< Atom processor, some SSSE3 instructions are slower
+#define AV_CPU_FLAG_SSE4 0x0100 ///< Penryn SSE4.1 functions
+#define AV_CPU_FLAG_SSE42 0x0200 ///< Nehalem SSE4.2 functions
+#define AV_CPU_FLAG_AESNI 0x80000 ///< Advanced Encryption Standard functions
+#define AV_CPU_FLAG_AVX \
+ 0x4000 ///< AVX functions: requires OS support even if YMM registers aren't
+ ///< used
+#define AV_CPU_FLAG_AVXSLOW \
+ 0x8000000 ///< AVX supported, but slow when using YMM registers (e.g.
+ ///< Bulldozer)
+#define AV_CPU_FLAG_XOP 0x0400 ///< Bulldozer XOP functions
+#define AV_CPU_FLAG_FMA4 0x0800 ///< Bulldozer FMA4 functions
+#define AV_CPU_FLAG_CMOV 0x1000 ///< supports cmov instruction
+#define AV_CPU_FLAG_AVX2 \
+ 0x8000 ///< AVX2 functions: requires OS support even if YMM registers aren't
+ ///< used
+#define AV_CPU_FLAG_FMA3 0x10000 ///< Haswell FMA3 functions
+#define AV_CPU_FLAG_BMI1 0x20000 ///< Bit Manipulation Instruction Set 1
+#define AV_CPU_FLAG_BMI2 0x40000 ///< Bit Manipulation Instruction Set 2
+#define AV_CPU_FLAG_AVX512 \
+ 0x100000 ///< AVX-512 functions: requires OS support even if YMM/ZMM
+ ///< registers aren't used
+#define AV_CPU_FLAG_SLOW_GATHER 0x2000000 ///< CPU has slow gathers.
+
+#define AV_CPU_FLAG_ALTIVEC 0x0001 ///< standard
+#define AV_CPU_FLAG_VSX 0x0002 ///< ISA 2.06
+#define AV_CPU_FLAG_POWER8 0x0004 ///< ISA 2.07
+
+#define AV_CPU_FLAG_ARMV5TE (1 << 0)
+#define AV_CPU_FLAG_ARMV6 (1 << 1)
+#define AV_CPU_FLAG_ARMV6T2 (1 << 2)
+#define AV_CPU_FLAG_VFP (1 << 3)
+#define AV_CPU_FLAG_VFPV3 (1 << 4)
+#define AV_CPU_FLAG_NEON (1 << 5)
+#define AV_CPU_FLAG_ARMV8 (1 << 6)
+#define AV_CPU_FLAG_VFP_VM \
+ (1 << 7) ///< VFPv2 vector mode, deprecated in ARMv7-A and unavailable in
+ ///< various CPUs implementations
+#define AV_CPU_FLAG_SETEND (1 << 16)
+
+#define AV_CPU_FLAG_MMI (1 << 0)
+#define AV_CPU_FLAG_MSA (1 << 1)
+
+// Loongarch SIMD extension.
+#define AV_CPU_FLAG_LSX (1 << 0)
+#define AV_CPU_FLAG_LASX (1 << 1)
+
+/**
+ * Return the flags which specify extensions supported by the CPU.
+ * The returned value is affected by av_force_cpu_flags() if that was used
+ * before. So av_get_cpu_flags() can easily be used in an application to
+ * detect the enabled cpu flags.
+ */
+int av_get_cpu_flags(void);
+
+/**
+ * Disables cpu detection and forces the specified flags.
+ * -1 is a special case that disables forcing of specific flags.
+ */
+void av_force_cpu_flags(int flags);
+
+/**
+ * Parse CPU caps from a string and update the given AV_CPU_* flags based on
+ * that.
+ *
+ * @return negative on error.
+ */
+int av_parse_cpu_caps(unsigned* flags, const char* s);
+
+/**
+ * @return the number of logical CPU cores present.
+ */
+int av_cpu_count(void);
+
+/**
+ * Overrides cpu count detection and forces the specified count.
+ * Count < 1 disables forcing of specific count.
+ */
+void av_cpu_force_count(int count);
+
+/**
+ * Get the maximum data alignment that may be required by FFmpeg.
+ *
+ * Note that this is affected by the build configuration and the CPU flags mask,
+ * so e.g. if the CPU supports AVX, but libavutil has been built with
+ * --disable-avx or the AV_CPU_FLAG_AVX flag has been disabled through
+ * av_set_cpu_flags_mask(), then this function will behave as if AVX is not
+ * present.
+ */
+size_t av_cpu_max_align(void);
+
+#endif /* AVUTIL_CPU_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/dict.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/dict.h
new file mode 100644
index 0000000000..6b7b0bf74b
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/dict.h
@@ -0,0 +1,215 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * Public dictionary API.
+ * @deprecated
+ * AVDictionary is provided for compatibility with libav. It is both in
+ * implementation as well as API inefficient. It does not scale and is
+ * extremely slow with large dictionaries.
+ * It is recommended that new code uses our tree container from tree.c/h
+ * where applicable, which uses AVL trees to achieve O(log n) performance.
+ */
+
+#ifndef AVUTIL_DICT_H
+#define AVUTIL_DICT_H
+
+#include <stdint.h>
+
+/**
+ * @addtogroup lavu_dict AVDictionary
+ * @ingroup lavu_data
+ *
+ * @brief Simple key:value store
+ *
+ * @{
+ * Dictionaries are used for storing key:value pairs. To create
+ * an AVDictionary, simply pass an address of a NULL pointer to
+ * av_dict_set(). NULL can be used as an empty dictionary wherever
+ * a pointer to an AVDictionary is required.
+ * Use av_dict_get() to retrieve an entry or iterate over all
+ * entries and finally av_dict_free() to free the dictionary
+ * and all its contents.
+ *
+ @code
+ AVDictionary *d = NULL; // "create" an empty dictionary
+ AVDictionaryEntry *t = NULL;
+
+ av_dict_set(&d, "foo", "bar", 0); // add an entry
+
+ char *k = av_strdup("key"); // if your strings are already allocated,
+ char *v = av_strdup("value"); // you can avoid copying them like this
+ av_dict_set(&d, k, v, AV_DICT_DONT_STRDUP_KEY | AV_DICT_DONT_STRDUP_VAL);
+
+ while (t = av_dict_get(d, "", t, AV_DICT_IGNORE_SUFFIX)) {
+ <....> // iterate over all entries in d
+ }
+ av_dict_free(&d);
+ @endcode
+ */
+
+#define AV_DICT_MATCH_CASE \
+ 1 /**< Only get an entry with exact-case key match. Only relevant in \
+ av_dict_get(). */
+#define AV_DICT_IGNORE_SUFFIX \
+ 2 /**< Return first entry in a dictionary whose first part corresponds to \
+ the search key, ignoring the suffix of the found key string. Only \
+ relevant in av_dict_get(). */
+#define AV_DICT_DONT_STRDUP_KEY \
+ 4 /**< Take ownership of a key that's been \
+ allocated with av_malloc() or another memory allocation function. */
+#define AV_DICT_DONT_STRDUP_VAL \
+ 8 /**< Take ownership of a value that's been \
+ allocated with av_malloc() or another memory allocation function. */
+#define AV_DICT_DONT_OVERWRITE 16 ///< Don't overwrite existing entries.
+#define AV_DICT_APPEND \
+ 32 /**< If the entry already exists, append to it. Note that no \
+ delimiter is added, the strings are simply concatenated. */
+#define AV_DICT_MULTIKEY \
+ 64 /**< Allow to store several equal keys in the dictionary */
+
+typedef struct AVDictionaryEntry {
+ char* key;
+ char* value;
+} AVDictionaryEntry;
+
+typedef struct AVDictionary AVDictionary;
+
+/**
+ * Get a dictionary entry with matching key.
+ *
+ * The returned entry key or value must not be changed, or it will
+ * cause undefined behavior.
+ *
+ * To iterate through all the dictionary entries, you can set the matching key
+ * to the null string "" and set the AV_DICT_IGNORE_SUFFIX flag.
+ *
+ * @param prev Set to the previous matching element to find the next.
+ * If set to NULL the first matching element is returned.
+ * @param key matching key
+ * @param flags a collection of AV_DICT_* flags controlling how the entry is
+ * retrieved
+ * @return found entry or NULL in case no matching entry was found in the
+ * dictionary
+ */
+AVDictionaryEntry* av_dict_get(const AVDictionary* m, const char* key,
+ const AVDictionaryEntry* prev, int flags);
+
+/**
+ * Get number of entries in dictionary.
+ *
+ * @param m dictionary
+ * @return number of entries in dictionary
+ */
+int av_dict_count(const AVDictionary* m);
+
+/**
+ * Set the given entry in *pm, overwriting an existing entry.
+ *
+ * Note: If AV_DICT_DONT_STRDUP_KEY or AV_DICT_DONT_STRDUP_VAL is set,
+ * these arguments will be freed on error.
+ *
+ * Warning: Adding a new entry to a dictionary invalidates all existing entries
+ * previously returned with av_dict_get.
+ *
+ * @param pm pointer to a pointer to a dictionary struct. If *pm is NULL
+ * a dictionary struct is allocated and put in *pm.
+ * @param key entry key to add to *pm (will either be av_strduped or added as a
+ * new key depending on flags)
+ * @param value entry value to add to *pm (will be av_strduped or added as a new
+ * key depending on flags). Passing a NULL value will cause an existing entry to
+ * be deleted.
+ * @return >= 0 on success otherwise an error code <0
+ */
+int av_dict_set(AVDictionary** pm, const char* key, const char* value,
+ int flags);
+
+/**
+ * Convenience wrapper for av_dict_set that converts the value to a string
+ * and stores it.
+ *
+ * Note: If AV_DICT_DONT_STRDUP_KEY is set, key will be freed on error.
+ */
+int av_dict_set_int(AVDictionary** pm, const char* key, int64_t value,
+ int flags);
+
+/**
+ * Parse the key/value pairs list and add the parsed entries to a dictionary.
+ *
+ * In case of failure, all the successfully set entries are stored in
+ * *pm. You may need to manually free the created dictionary.
+ *
+ * @param key_val_sep a 0-terminated list of characters used to separate
+ * key from value
+ * @param pairs_sep a 0-terminated list of characters used to separate
+ * two pairs from each other
+ * @param flags flags to use when adding to dictionary.
+ * AV_DICT_DONT_STRDUP_KEY and AV_DICT_DONT_STRDUP_VAL
+ * are ignored since the key/value tokens will always
+ * be duplicated.
+ * @return 0 on success, negative AVERROR code on failure
+ */
+int av_dict_parse_string(AVDictionary** pm, const char* str,
+ const char* key_val_sep, const char* pairs_sep,
+ int flags);
+
+/**
+ * Copy entries from one AVDictionary struct into another.
+ * @param dst pointer to a pointer to a AVDictionary struct. If *dst is NULL,
+ * this function will allocate a struct for you and put it in *dst
+ * @param src pointer to source AVDictionary struct
+ * @param flags flags to use when setting entries in *dst
+ * @note metadata is read using the AV_DICT_IGNORE_SUFFIX flag
+ * @return 0 on success, negative AVERROR code on failure. If dst was allocated
+ * by this function, callers should free the associated memory.
+ */
+int av_dict_copy(AVDictionary** dst, const AVDictionary* src, int flags);
+
+/**
+ * Free all the memory allocated for an AVDictionary struct
+ * and all keys and values.
+ */
+void av_dict_free(AVDictionary** m);
+
+/**
+ * Get dictionary entries as a string.
+ *
+ * Create a string containing dictionary's entries.
+ * Such string may be passed back to av_dict_parse_string().
+ * @note String is escaped with backslashes ('\').
+ *
+ * @param[in] m dictionary
+ * @param[out] buffer Pointer to buffer that will be allocated with
+ * string containg entries. Buffer must be freed by the caller when is no longer
+ * needed.
+ * @param[in] key_val_sep character used to separate key from value
+ * @param[in] pairs_sep character used to separate two pairs from each
+ * other
+ * @return >= 0 on success, negative on error
+ * @warning Separators cannot be neither '\\' nor '\0'. They also cannot be the
+ * same.
+ */
+int av_dict_get_string(const AVDictionary* m, char** buffer,
+ const char key_val_sep, const char pairs_sep);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_DICT_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/error.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/error.h
new file mode 100644
index 0000000000..74af5b1534
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/error.h
@@ -0,0 +1,158 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * error code definitions
+ */
+
+#ifndef AVUTIL_ERROR_H
+#define AVUTIL_ERROR_H
+
+#include <errno.h>
+#include <stddef.h>
+
+#include "macros.h"
+
+/**
+ * @addtogroup lavu_error
+ *
+ * @{
+ */
+
+/* error handling */
+#if EDOM > 0
+# define AVERROR(e) \
+ (-(e)) ///< Returns a negative error code from a POSIX error code, to
+ ///< return from library functions.
+# define AVUNERROR(e) \
+ (-(e)) ///< Returns a POSIX error code from a library function error return
+ ///< value.
+#else
+/* Some platforms have E* and errno already negated. */
+# define AVERROR(e) (e)
+# define AVUNERROR(e) (e)
+#endif
+
+#define FFERRTAG(a, b, c, d) (-(int)MKTAG(a, b, c, d))
+
+#define AVERROR_BSF_NOT_FOUND \
+ FFERRTAG(0xF8, 'B', 'S', 'F') ///< Bitstream filter not found
+#define AVERROR_BUG \
+ FFERRTAG('B', 'U', 'G', '!') ///< Internal bug, also see AVERROR_BUG2
+#define AVERROR_BUFFER_TOO_SMALL \
+ FFERRTAG('B', 'U', 'F', 'S') ///< Buffer too small
+#define AVERROR_DECODER_NOT_FOUND \
+ FFERRTAG(0xF8, 'D', 'E', 'C') ///< Decoder not found
+#define AVERROR_DEMUXER_NOT_FOUND \
+ FFERRTAG(0xF8, 'D', 'E', 'M') ///< Demuxer not found
+#define AVERROR_ENCODER_NOT_FOUND \
+ FFERRTAG(0xF8, 'E', 'N', 'C') ///< Encoder not found
+#define AVERROR_EOF FFERRTAG('E', 'O', 'F', ' ') ///< End of file
+#define AVERROR_EXIT \
+ FFERRTAG('E', 'X', 'I', 'T') ///< Immediate exit was requested; the called
+ ///< function should not be restarted
+#define AVERROR_EXTERNAL \
+ FFERRTAG('E', 'X', 'T', ' ') ///< Generic error in an external library
+#define AVERROR_FILTER_NOT_FOUND \
+ FFERRTAG(0xF8, 'F', 'I', 'L') ///< Filter not found
+#define AVERROR_INVALIDDATA \
+ FFERRTAG('I', 'N', 'D', 'A') ///< Invalid data found when processing input
+#define AVERROR_MUXER_NOT_FOUND \
+ FFERRTAG(0xF8, 'M', 'U', 'X') ///< Muxer not found
+#define AVERROR_OPTION_NOT_FOUND \
+ FFERRTAG(0xF8, 'O', 'P', 'T') ///< Option not found
+#define AVERROR_PATCHWELCOME \
+ FFERRTAG('P', 'A', 'W', \
+ 'E') ///< Not yet implemented in FFmpeg, patches welcome
+#define AVERROR_PROTOCOL_NOT_FOUND \
+ FFERRTAG(0xF8, 'P', 'R', 'O') ///< Protocol not found
+
+#define AVERROR_STREAM_NOT_FOUND \
+ FFERRTAG(0xF8, 'S', 'T', 'R') ///< Stream not found
+/**
+ * This is semantically identical to AVERROR_BUG
+ * it has been introduced in Libav after our AVERROR_BUG and with a modified
+ * value.
+ */
+#define AVERROR_BUG2 FFERRTAG('B', 'U', 'G', ' ')
+#define AVERROR_UNKNOWN \
+ FFERRTAG('U', 'N', 'K', \
+ 'N') ///< Unknown error, typically from an external library
+#define AVERROR_EXPERIMENTAL \
+ (-0x2bb2afa8) ///< Requested feature is flagged experimental. Set
+ ///< strict_std_compliance if you really want to use it.
+#define AVERROR_INPUT_CHANGED \
+ (-0x636e6701) ///< Input changed between calls. Reconfiguration is required.
+ ///< (can be OR-ed with AVERROR_OUTPUT_CHANGED)
+#define AVERROR_OUTPUT_CHANGED \
+ (-0x636e6702) ///< Output changed between calls. Reconfiguration is required.
+ ///< (can be OR-ed with AVERROR_INPUT_CHANGED)
+/* HTTP & RTSP errors */
+#define AVERROR_HTTP_BAD_REQUEST FFERRTAG(0xF8, '4', '0', '0')
+#define AVERROR_HTTP_UNAUTHORIZED FFERRTAG(0xF8, '4', '0', '1')
+#define AVERROR_HTTP_FORBIDDEN FFERRTAG(0xF8, '4', '0', '3')
+#define AVERROR_HTTP_NOT_FOUND FFERRTAG(0xF8, '4', '0', '4')
+#define AVERROR_HTTP_OTHER_4XX FFERRTAG(0xF8, '4', 'X', 'X')
+#define AVERROR_HTTP_SERVER_ERROR FFERRTAG(0xF8, '5', 'X', 'X')
+
+#define AV_ERROR_MAX_STRING_SIZE 64
+
+/**
+ * Put a description of the AVERROR code errnum in errbuf.
+ * In case of failure the global variable errno is set to indicate the
+ * error. Even in case of failure av_strerror() will print a generic
+ * error message indicating the errnum provided to errbuf.
+ *
+ * @param errnum error code to describe
+ * @param errbuf buffer to which description is written
+ * @param errbuf_size the size in bytes of errbuf
+ * @return 0 on success, a negative value if a description for errnum
+ * cannot be found
+ */
+int av_strerror(int errnum, char* errbuf, size_t errbuf_size);
+
+/**
+ * Fill the provided buffer with a string containing an error string
+ * corresponding to the AVERROR code errnum.
+ *
+ * @param errbuf a buffer
+ * @param errbuf_size size in bytes of errbuf
+ * @param errnum error code to describe
+ * @return the buffer in input, filled with the error description
+ * @see av_strerror()
+ */
+static inline char* av_make_error_string(char* errbuf, size_t errbuf_size,
+ int errnum) {
+ av_strerror(errnum, errbuf, errbuf_size);
+ return errbuf;
+}
+
+/**
+ * Convenience macro, the return value should be used only directly in
+ * function arguments but never stand-alone.
+ */
+#define av_err2str(errnum) \
+ av_make_error_string((char[AV_ERROR_MAX_STRING_SIZE]){0}, \
+ AV_ERROR_MAX_STRING_SIZE, errnum)
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_ERROR_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/frame.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/frame.h
new file mode 100644
index 0000000000..457d67da0a
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/frame.h
@@ -0,0 +1,927 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * @ingroup lavu_frame
+ * reference-counted frame API
+ */
+
+#ifndef AVUTIL_FRAME_H
+#define AVUTIL_FRAME_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "avutil.h"
+#include "buffer.h"
+#include "dict.h"
+#include "rational.h"
+#include "samplefmt.h"
+#include "pixfmt.h"
+#include "version.h"
+
+/**
+ * @defgroup lavu_frame AVFrame
+ * @ingroup lavu_data
+ *
+ * @{
+ * AVFrame is an abstraction for reference-counted raw multimedia data.
+ */
+
+enum AVFrameSideDataType {
+ /**
+ * The data is the AVPanScan struct defined in libavcodec.
+ */
+ AV_FRAME_DATA_PANSCAN,
+ /**
+ * ATSC A53 Part 4 Closed Captions.
+ * A53 CC bitstream is stored as uint8_t in AVFrameSideData.data.
+ * The number of bytes of CC data is AVFrameSideData.size.
+ */
+ AV_FRAME_DATA_A53_CC,
+ /**
+ * Stereoscopic 3d metadata.
+ * The data is the AVStereo3D struct defined in libavutil/stereo3d.h.
+ */
+ AV_FRAME_DATA_STEREO3D,
+ /**
+ * The data is the AVMatrixEncoding enum defined in
+ * libavutil/channel_layout.h.
+ */
+ AV_FRAME_DATA_MATRIXENCODING,
+ /**
+ * Metadata relevant to a downmix procedure.
+ * The data is the AVDownmixInfo struct defined in libavutil/downmix_info.h.
+ */
+ AV_FRAME_DATA_DOWNMIX_INFO,
+ /**
+ * ReplayGain information in the form of the AVReplayGain struct.
+ */
+ AV_FRAME_DATA_REPLAYGAIN,
+ /**
+ * This side data contains a 3x3 transformation matrix describing an affine
+ * transformation that needs to be applied to the frame for correct
+ * presentation.
+ *
+ * See libavutil/display.h for a detailed description of the data.
+ */
+ AV_FRAME_DATA_DISPLAYMATRIX,
+ /**
+ * Active Format Description data consisting of a single byte as specified
+ * in ETSI TS 101 154 using AVActiveFormatDescription enum.
+ */
+ AV_FRAME_DATA_AFD,
+ /**
+ * Motion vectors exported by some codecs (on demand through the export_mvs
+ * flag set in the libavcodec AVCodecContext flags2 option).
+ * The data is the AVMotionVector struct defined in
+ * libavutil/motion_vector.h.
+ */
+ AV_FRAME_DATA_MOTION_VECTORS,
+ /**
+ * Recommmends skipping the specified number of samples. This is exported
+ * only if the "skip_manual" AVOption is set in libavcodec.
+ * This has the same format as AV_PKT_DATA_SKIP_SAMPLES.
+ * @code
+ * u32le number of samples to skip from start of this packet
+ * u32le number of samples to skip from end of this packet
+ * u8 reason for start skip
+ * u8 reason for end skip (0=padding silence, 1=convergence)
+ * @endcode
+ */
+ AV_FRAME_DATA_SKIP_SAMPLES,
+ /**
+ * This side data must be associated with an audio frame and corresponds to
+ * enum AVAudioServiceType defined in avcodec.h.
+ */
+ AV_FRAME_DATA_AUDIO_SERVICE_TYPE,
+ /**
+ * Mastering display metadata associated with a video frame. The payload is
+ * an AVMasteringDisplayMetadata type and contains information about the
+ * mastering display color volume.
+ */
+ AV_FRAME_DATA_MASTERING_DISPLAY_METADATA,
+ /**
+ * The GOP timecode in 25 bit timecode format. Data format is 64-bit integer.
+ * This is set on the first frame of a GOP that has a temporal reference of 0.
+ */
+ AV_FRAME_DATA_GOP_TIMECODE,
+
+ /**
+ * The data represents the AVSphericalMapping structure defined in
+ * libavutil/spherical.h.
+ */
+ AV_FRAME_DATA_SPHERICAL,
+
+ /**
+ * Content light level (based on CTA-861.3). This payload contains data in
+ * the form of the AVContentLightMetadata struct.
+ */
+ AV_FRAME_DATA_CONTENT_LIGHT_LEVEL,
+
+ /**
+ * The data contains an ICC profile as an opaque octet buffer following the
+ * format described by ISO 15076-1 with an optional name defined in the
+ * metadata key entry "name".
+ */
+ AV_FRAME_DATA_ICC_PROFILE,
+
+ /**
+ * Timecode which conforms to SMPTE ST 12-1. The data is an array of 4
+ * uint32_t where the first uint32_t describes how many (1-3) of the other
+ * timecodes are used. The timecode format is described in the documentation
+ * of av_timecode_get_smpte_from_framenum() function in libavutil/timecode.h.
+ */
+ AV_FRAME_DATA_S12M_TIMECODE,
+
+ /**
+ * HDR dynamic metadata associated with a video frame. The payload is
+ * an AVDynamicHDRPlus type and contains information for color
+ * volume transform - application 4 of SMPTE 2094-40:2016 standard.
+ */
+ AV_FRAME_DATA_DYNAMIC_HDR_PLUS,
+
+ /**
+ * Regions Of Interest, the data is an array of AVRegionOfInterest type, the
+ * number of array element is implied by AVFrameSideData.size /
+ * AVRegionOfInterest.self_size.
+ */
+ AV_FRAME_DATA_REGIONS_OF_INTEREST,
+
+ /**
+ * Encoding parameters for a video frame, as described by AVVideoEncParams.
+ */
+ AV_FRAME_DATA_VIDEO_ENC_PARAMS,
+
+ /**
+ * User data unregistered metadata associated with a video frame.
+ * This is the H.26[45] UDU SEI message, and shouldn't be used for any other
+ * purpose The data is stored as uint8_t in AVFrameSideData.data which is 16
+ * bytes of uuid_iso_iec_11578 followed by AVFrameSideData.size - 16 bytes of
+ * user_data_payload_byte.
+ */
+ AV_FRAME_DATA_SEI_UNREGISTERED,
+
+ /**
+ * Film grain parameters for a frame, described by AVFilmGrainParams.
+ * Must be present for every frame which should have film grain applied.
+ */
+ AV_FRAME_DATA_FILM_GRAIN_PARAMS,
+
+ /**
+ * Bounding boxes for object detection and classification,
+ * as described by AVDetectionBBoxHeader.
+ */
+ AV_FRAME_DATA_DETECTION_BBOXES,
+
+ /**
+ * Dolby Vision RPU raw data, suitable for passing to x265
+ * or other libraries. Array of uint8_t, with NAL emulation
+ * bytes intact.
+ */
+ AV_FRAME_DATA_DOVI_RPU_BUFFER,
+
+ /**
+ * Parsed Dolby Vision metadata, suitable for passing to a software
+ * implementation. The payload is the AVDOVIMetadata struct defined in
+ * libavutil/dovi_meta.h.
+ */
+ AV_FRAME_DATA_DOVI_METADATA,
+};
+
+enum AVActiveFormatDescription {
+ AV_AFD_SAME = 8,
+ AV_AFD_4_3 = 9,
+ AV_AFD_16_9 = 10,
+ AV_AFD_14_9 = 11,
+ AV_AFD_4_3_SP_14_9 = 13,
+ AV_AFD_16_9_SP_14_9 = 14,
+ AV_AFD_SP_4_3 = 15,
+};
+
+/**
+ * Structure to hold side data for an AVFrame.
+ *
+ * sizeof(AVFrameSideData) is not a part of the public ABI, so new fields may be
+ * added to the end with a minor bump.
+ */
+typedef struct AVFrameSideData {
+ enum AVFrameSideDataType type;
+ uint8_t* data;
+ size_t size;
+ AVDictionary* metadata;
+ AVBufferRef* buf;
+} AVFrameSideData;
+
+/**
+ * Structure describing a single Region Of Interest.
+ *
+ * When multiple regions are defined in a single side-data block, they
+ * should be ordered from most to least important - some encoders are only
+ * capable of supporting a limited number of distinct regions, so will have
+ * to truncate the list.
+ *
+ * When overlapping regions are defined, the first region containing a given
+ * area of the frame applies.
+ */
+typedef struct AVRegionOfInterest {
+ /**
+ * Must be set to the size of this data structure (that is,
+ * sizeof(AVRegionOfInterest)).
+ */
+ uint32_t self_size;
+ /**
+ * Distance in pixels from the top edge of the frame to the top and
+ * bottom edges and from the left edge of the frame to the left and
+ * right edges of the rectangle defining this region of interest.
+ *
+ * The constraints on a region are encoder dependent, so the region
+ * actually affected may be slightly larger for alignment or other
+ * reasons.
+ */
+ int top;
+ int bottom;
+ int left;
+ int right;
+ /**
+ * Quantisation offset.
+ *
+ * Must be in the range -1 to +1. A value of zero indicates no quality
+ * change. A negative value asks for better quality (less quantisation),
+ * while a positive value asks for worse quality (greater quantisation).
+ *
+ * The range is calibrated so that the extreme values indicate the
+ * largest possible offset - if the rest of the frame is encoded with the
+ * worst possible quality, an offset of -1 indicates that this region
+ * should be encoded with the best possible quality anyway. Intermediate
+ * values are then interpolated in some codec-dependent way.
+ *
+ * For example, in 10-bit H.264 the quantisation parameter varies between
+ * -12 and 51. A typical qoffset value of -1/10 therefore indicates that
+ * this region should be encoded with a QP around one-tenth of the full
+ * range better than the rest of the frame. So, if most of the frame
+ * were to be encoded with a QP of around 30, this region would get a QP
+ * of around 24 (an offset of approximately -1/10 * (51 - -12) = -6.3).
+ * An extreme value of -1 would indicate that this region should be
+ * encoded with the best possible quality regardless of the treatment of
+ * the rest of the frame - that is, should be encoded at a QP of -12.
+ */
+ AVRational qoffset;
+} AVRegionOfInterest;
+
+/**
+ * This structure describes decoded (raw) audio or video data.
+ *
+ * AVFrame must be allocated using av_frame_alloc(). Note that this only
+ * allocates the AVFrame itself, the buffers for the data must be managed
+ * through other means (see below).
+ * AVFrame must be freed with av_frame_free().
+ *
+ * AVFrame is typically allocated once and then reused multiple times to hold
+ * different data (e.g. a single AVFrame to hold frames received from a
+ * decoder). In such a case, av_frame_unref() will free any references held by
+ * the frame and reset it to its original clean state before it
+ * is reused again.
+ *
+ * The data described by an AVFrame is usually reference counted through the
+ * AVBuffer API. The underlying buffer references are stored in AVFrame.buf /
+ * AVFrame.extended_buf. An AVFrame is considered to be reference counted if at
+ * least one reference is set, i.e. if AVFrame.buf[0] != NULL. In such a case,
+ * every single data plane must be contained in one of the buffers in
+ * AVFrame.buf or AVFrame.extended_buf.
+ * There may be a single buffer for all the data, or one separate buffer for
+ * each plane, or anything in between.
+ *
+ * sizeof(AVFrame) is not a part of the public ABI, so new fields may be added
+ * to the end with a minor bump.
+ *
+ * Fields can be accessed through AVOptions, the name string used, matches the
+ * C structure field name for fields accessible through AVOptions. The AVClass
+ * for AVFrame can be obtained from avcodec_get_frame_class()
+ */
+typedef struct AVFrame {
+#define AV_NUM_DATA_POINTERS 8
+ /**
+ * pointer to the picture/channel planes.
+ * This might be different from the first allocated byte. For video,
+ * it could even point to the end of the image data.
+ *
+ * All pointers in data and extended_data must point into one of the
+ * AVBufferRef in buf or extended_buf.
+ *
+ * Some decoders access areas outside 0,0 - width,height, please
+ * see avcodec_align_dimensions2(). Some filters and swscale can read
+ * up to 16 bytes beyond the planes, if these filters are to be used,
+ * then 16 extra bytes must be allocated.
+ *
+ * NOTE: Pointers not needed by the format MUST be set to NULL.
+ *
+ * @attention In case of video, the data[] pointers can point to the
+ * end of image data in order to reverse line order, when used in
+ * combination with negative values in the linesize[] array.
+ */
+ uint8_t* data[AV_NUM_DATA_POINTERS];
+
+ /**
+ * For video, a positive or negative value, which is typically indicating
+ * the size in bytes of each picture line, but it can also be:
+ * - the negative byte size of lines for vertical flipping
+ * (with data[n] pointing to the end of the data
+ * - a positive or negative multiple of the byte size as for accessing
+ * even and odd fields of a frame (possibly flipped)
+ *
+ * For audio, only linesize[0] may be set. For planar audio, each channel
+ * plane must be the same size.
+ *
+ * For video the linesizes should be multiples of the CPUs alignment
+ * preference, this is 16 or 32 for modern desktop CPUs.
+ * Some code requires such alignment other code can be slower without
+ * correct alignment, for yet other it makes no difference.
+ *
+ * @note The linesize may be larger than the size of usable data -- there
+ * may be extra padding present for performance reasons.
+ *
+ * @attention In case of video, line size values can be negative to achieve
+ * a vertically inverted iteration over image lines.
+ */
+ int linesize[AV_NUM_DATA_POINTERS];
+
+ /**
+ * pointers to the data planes/channels.
+ *
+ * For video, this should simply point to data[].
+ *
+ * For planar audio, each channel has a separate data pointer, and
+ * linesize[0] contains the size of each channel buffer.
+ * For packed audio, there is just one data pointer, and linesize[0]
+ * contains the total size of the buffer for all channels.
+ *
+ * Note: Both data and extended_data should always be set in a valid frame,
+ * but for planar audio with more channels that can fit in data,
+ * extended_data must be used in order to access all channels.
+ */
+ uint8_t** extended_data;
+
+ /**
+ * @name Video dimensions
+ * Video frames only. The coded dimensions (in pixels) of the video frame,
+ * i.e. the size of the rectangle that contains some well-defined values.
+ *
+ * @note The part of the frame intended for display/presentation is further
+ * restricted by the @ref cropping "Cropping rectangle".
+ * @{
+ */
+ int width, height;
+ /**
+ * @}
+ */
+
+ /**
+ * number of audio samples (per channel) described by this frame
+ */
+ int nb_samples;
+
+ /**
+ * format of the frame, -1 if unknown or unset
+ * Values correspond to enum AVPixelFormat for video frames,
+ * enum AVSampleFormat for audio)
+ */
+ int format;
+
+ /**
+ * 1 -> keyframe, 0-> not
+ */
+ int key_frame;
+
+ /**
+ * Picture type of the frame.
+ */
+ enum AVPictureType pict_type;
+
+ /**
+ * Sample aspect ratio for the video frame, 0/1 if unknown/unspecified.
+ */
+ AVRational sample_aspect_ratio;
+
+ /**
+ * Presentation timestamp in time_base units (time when frame should be shown
+ * to user).
+ */
+ int64_t pts;
+
+ /**
+ * DTS copied from the AVPacket that triggered returning this frame. (if frame
+ * threading isn't used) This is also the Presentation time of this AVFrame
+ * calculated from only AVPacket.dts values without pts values.
+ */
+ int64_t pkt_dts;
+
+ /**
+ * Time base for the timestamps in this frame.
+ * In the future, this field may be set on frames output by decoders or
+ * filters, but its value will be by default ignored on input to encoders
+ * or filters.
+ */
+ AVRational time_base;
+
+ /**
+ * picture number in bitstream order
+ */
+ int coded_picture_number;
+ /**
+ * picture number in display order
+ */
+ int display_picture_number;
+
+ /**
+ * quality (between 1 (good) and FF_LAMBDA_MAX (bad))
+ */
+ int quality;
+
+ /**
+ * for some private data of the user
+ */
+ void* opaque;
+
+ /**
+ * When decoding, this signals how much the picture must be delayed.
+ * extra_delay = repeat_pict / (2*fps)
+ */
+ int repeat_pict;
+
+ /**
+ * The content of the picture is interlaced.
+ */
+ int interlaced_frame;
+
+ /**
+ * If the content is interlaced, is top field displayed first.
+ */
+ int top_field_first;
+
+ /**
+ * Tell user application that palette has changed from previous frame.
+ */
+ int palette_has_changed;
+
+ /**
+ * reordered opaque 64 bits (generally an integer or a double precision float
+ * PTS but can be anything).
+ * The user sets AVCodecContext.reordered_opaque to represent the input at
+ * that time,
+ * the decoder reorders values as needed and sets AVFrame.reordered_opaque
+ * to exactly one of the values provided by the user through
+ * AVCodecContext.reordered_opaque
+ */
+ int64_t reordered_opaque;
+
+ /**
+ * Sample rate of the audio data.
+ */
+ int sample_rate;
+
+ /**
+ * Channel layout of the audio data.
+ */
+ uint64_t channel_layout;
+
+ /**
+ * AVBuffer references backing the data for this frame. All the pointers in
+ * data and extended_data must point inside one of the buffers in buf or
+ * extended_buf. This array must be filled contiguously -- if buf[i] is
+ * non-NULL then buf[j] must also be non-NULL for all j < i.
+ *
+ * There may be at most one AVBuffer per data plane, so for video this array
+ * always contains all the references. For planar audio with more than
+ * AV_NUM_DATA_POINTERS channels, there may be more buffers than can fit in
+ * this array. Then the extra AVBufferRef pointers are stored in the
+ * extended_buf array.
+ */
+ AVBufferRef* buf[AV_NUM_DATA_POINTERS];
+
+ /**
+ * For planar audio which requires more than AV_NUM_DATA_POINTERS
+ * AVBufferRef pointers, this array will hold all the references which
+ * cannot fit into AVFrame.buf.
+ *
+ * Note that this is different from AVFrame.extended_data, which always
+ * contains all the pointers. This array only contains the extra pointers,
+ * which cannot fit into AVFrame.buf.
+ *
+ * This array is always allocated using av_malloc() by whoever constructs
+ * the frame. It is freed in av_frame_unref().
+ */
+ AVBufferRef** extended_buf;
+ /**
+ * Number of elements in extended_buf.
+ */
+ int nb_extended_buf;
+
+ AVFrameSideData** side_data;
+ int nb_side_data;
+
+/**
+ * @defgroup lavu_frame_flags AV_FRAME_FLAGS
+ * @ingroup lavu_frame
+ * Flags describing additional frame properties.
+ *
+ * @{
+ */
+
+/**
+ * The frame data may be corrupted, e.g. due to decoding errors.
+ */
+#define AV_FRAME_FLAG_CORRUPT (1 << 0)
+/**
+ * A flag to mark the frames which need to be decoded, but shouldn't be output.
+ */
+#define AV_FRAME_FLAG_DISCARD (1 << 2)
+ /**
+ * @}
+ */
+
+ /**
+ * Frame flags, a combination of @ref lavu_frame_flags
+ */
+ int flags;
+
+ /**
+ * MPEG vs JPEG YUV range.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorRange color_range;
+
+ enum AVColorPrimaries color_primaries;
+
+ enum AVColorTransferCharacteristic color_trc;
+
+ /**
+ * YUV colorspace type.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorSpace colorspace;
+
+ enum AVChromaLocation chroma_location;
+
+ /**
+ * frame timestamp estimated using various heuristics, in stream time base
+ * - encoding: unused
+ * - decoding: set by libavcodec, read by user.
+ */
+ int64_t best_effort_timestamp;
+
+ /**
+ * reordered pos from the last AVPacket that has been input into the decoder
+ * - encoding: unused
+ * - decoding: Read by user.
+ */
+ int64_t pkt_pos;
+
+ /**
+ * duration of the corresponding packet, expressed in
+ * AVStream->time_base units, 0 if unknown.
+ * - encoding: unused
+ * - decoding: Read by user.
+ */
+ int64_t pkt_duration;
+
+ /**
+ * metadata.
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ AVDictionary* metadata;
+
+ /**
+ * decode error flags of the frame, set to a combination of
+ * FF_DECODE_ERROR_xxx flags if the decoder produced a frame, but there
+ * were errors during the decoding.
+ * - encoding: unused
+ * - decoding: set by libavcodec, read by user.
+ */
+ int decode_error_flags;
+#define FF_DECODE_ERROR_INVALID_BITSTREAM 1
+#define FF_DECODE_ERROR_MISSING_REFERENCE 2
+#define FF_DECODE_ERROR_CONCEALMENT_ACTIVE 4
+#define FF_DECODE_ERROR_DECODE_SLICES 8
+
+ /**
+ * number of audio channels, only used for audio.
+ * - encoding: unused
+ * - decoding: Read by user.
+ */
+ int channels;
+
+ /**
+ * size of the corresponding packet containing the compressed
+ * frame.
+ * It is set to a negative value if unknown.
+ * - encoding: unused
+ * - decoding: set by libavcodec, read by user.
+ */
+ int pkt_size;
+
+ /**
+ * For hwaccel-format frames, this should be a reference to the
+ * AVHWFramesContext describing the frame.
+ */
+ AVBufferRef* hw_frames_ctx;
+
+ /**
+ * AVBufferRef for free use by the API user. FFmpeg will never check the
+ * contents of the buffer ref. FFmpeg calls av_buffer_unref() on it when
+ * the frame is unreferenced. av_frame_copy_props() calls create a new
+ * reference with av_buffer_ref() for the target frame's opaque_ref field.
+ *
+ * This is unrelated to the opaque field, although it serves a similar
+ * purpose.
+ */
+ AVBufferRef* opaque_ref;
+
+ /**
+ * @anchor cropping
+ * @name Cropping
+ * Video frames only. The number of pixels to discard from the the
+ * top/bottom/left/right border of the frame to obtain the sub-rectangle of
+ * the frame intended for presentation.
+ * @{
+ */
+ size_t crop_top;
+ size_t crop_bottom;
+ size_t crop_left;
+ size_t crop_right;
+ /**
+ * @}
+ */
+
+ /**
+ * AVBufferRef for internal use by a single libav* library.
+ * Must not be used to transfer data between libraries.
+ * Has to be NULL when ownership of the frame leaves the respective library.
+ *
+ * Code outside the FFmpeg libs should never check or change the contents of
+ * the buffer ref.
+ *
+ * FFmpeg calls av_buffer_unref() on it when the frame is unreferenced.
+ * av_frame_copy_props() calls create a new reference with av_buffer_ref()
+ * for the target frame's private_ref field.
+ */
+ AVBufferRef* private_ref;
+} AVFrame;
+
+#if FF_API_COLORSPACE_NAME
+/**
+ * Get the name of a colorspace.
+ * @return a static string identifying the colorspace; can be NULL.
+ * @deprecated use av_color_space_name()
+ */
+attribute_deprecated const char* av_get_colorspace_name(enum AVColorSpace val);
+#endif
+/**
+ * Allocate an AVFrame and set its fields to default values. The resulting
+ * struct must be freed using av_frame_free().
+ *
+ * @return An AVFrame filled with default values or NULL on failure.
+ *
+ * @note this only allocates the AVFrame itself, not the data buffers. Those
+ * must be allocated through other means, e.g. with av_frame_get_buffer() or
+ * manually.
+ */
+AVFrame* av_frame_alloc(void);
+
+/**
+ * Free the frame and any dynamically allocated objects in it,
+ * e.g. extended_data. If the frame is reference counted, it will be
+ * unreferenced first.
+ *
+ * @param frame frame to be freed. The pointer will be set to NULL.
+ */
+void av_frame_free(AVFrame** frame);
+
+/**
+ * Set up a new reference to the data described by the source frame.
+ *
+ * Copy frame properties from src to dst and create a new reference for each
+ * AVBufferRef from src.
+ *
+ * If src is not reference counted, new buffers are allocated and the data is
+ * copied.
+ *
+ * @warning: dst MUST have been either unreferenced with av_frame_unref(dst),
+ * or newly allocated with av_frame_alloc() before calling this
+ * function, or undefined behavior will occur.
+ *
+ * @return 0 on success, a negative AVERROR on error
+ */
+int av_frame_ref(AVFrame* dst, const AVFrame* src);
+
+/**
+ * Create a new frame that references the same data as src.
+ *
+ * This is a shortcut for av_frame_alloc()+av_frame_ref().
+ *
+ * @return newly created AVFrame on success, NULL on error.
+ */
+AVFrame* av_frame_clone(const AVFrame* src);
+
+/**
+ * Unreference all the buffers referenced by frame and reset the frame fields.
+ */
+void av_frame_unref(AVFrame* frame);
+
+/**
+ * Move everything contained in src to dst and reset src.
+ *
+ * @warning: dst is not unreferenced, but directly overwritten without reading
+ * or deallocating its contents. Call av_frame_unref(dst) manually
+ * before calling this function to ensure that no memory is leaked.
+ */
+void av_frame_move_ref(AVFrame* dst, AVFrame* src);
+
+/**
+ * Allocate new buffer(s) for audio or video data.
+ *
+ * The following fields must be set on frame before calling this function:
+ * - format (pixel format for video, sample format for audio)
+ * - width and height for video
+ * - nb_samples and channel_layout for audio
+ *
+ * This function will fill AVFrame.data and AVFrame.buf arrays and, if
+ * necessary, allocate and fill AVFrame.extended_data and AVFrame.extended_buf.
+ * For planar formats, one buffer will be allocated for each plane.
+ *
+ * @warning: if frame already has been allocated, calling this function will
+ * leak memory. In addition, undefined behavior can occur in certain
+ * cases.
+ *
+ * @param frame frame in which to store the new buffers.
+ * @param align Required buffer size alignment. If equal to 0, alignment will be
+ * chosen automatically for the current CPU. It is highly
+ * recommended to pass 0 here unless you know what you are doing.
+ *
+ * @return 0 on success, a negative AVERROR on error.
+ */
+int av_frame_get_buffer(AVFrame* frame, int align);
+
+/**
+ * Check if the frame data is writable.
+ *
+ * @return A positive value if the frame data is writable (which is true if and
+ * only if each of the underlying buffers has only one reference, namely the one
+ * stored in this frame). Return 0 otherwise.
+ *
+ * If 1 is returned the answer is valid until av_buffer_ref() is called on any
+ * of the underlying AVBufferRefs (e.g. through av_frame_ref() or directly).
+ *
+ * @see av_frame_make_writable(), av_buffer_is_writable()
+ */
+int av_frame_is_writable(AVFrame* frame);
+
+/**
+ * Ensure that the frame data is writable, avoiding data copy if possible.
+ *
+ * Do nothing if the frame is writable, allocate new buffers and copy the data
+ * if it is not.
+ *
+ * @return 0 on success, a negative AVERROR on error.
+ *
+ * @see av_frame_is_writable(), av_buffer_is_writable(),
+ * av_buffer_make_writable()
+ */
+int av_frame_make_writable(AVFrame* frame);
+
+/**
+ * Copy the frame data from src to dst.
+ *
+ * This function does not allocate anything, dst must be already initialized and
+ * allocated with the same parameters as src.
+ *
+ * This function only copies the frame data (i.e. the contents of the data /
+ * extended data arrays), not any other properties.
+ *
+ * @return >= 0 on success, a negative AVERROR on error.
+ */
+int av_frame_copy(AVFrame* dst, const AVFrame* src);
+
+/**
+ * Copy only "metadata" fields from src to dst.
+ *
+ * Metadata for the purpose of this function are those fields that do not affect
+ * the data layout in the buffers. E.g. pts, sample rate (for audio) or sample
+ * aspect ratio (for video), but not width/height or channel layout.
+ * Side data is also copied.
+ */
+int av_frame_copy_props(AVFrame* dst, const AVFrame* src);
+
+/**
+ * Get the buffer reference a given data plane is stored in.
+ *
+ * @param plane index of the data plane of interest in frame->extended_data.
+ *
+ * @return the buffer reference that contains the plane or NULL if the input
+ * frame is not valid.
+ */
+AVBufferRef* av_frame_get_plane_buffer(AVFrame* frame, int plane);
+
+/**
+ * Add a new side data to a frame.
+ *
+ * @param frame a frame to which the side data should be added
+ * @param type type of the added side data
+ * @param size size of the side data
+ *
+ * @return newly added side data on success, NULL on error
+ */
+AVFrameSideData* av_frame_new_side_data(AVFrame* frame,
+ enum AVFrameSideDataType type,
+ size_t size);
+
+/**
+ * Add a new side data to a frame from an existing AVBufferRef
+ *
+ * @param frame a frame to which the side data should be added
+ * @param type the type of the added side data
+ * @param buf an AVBufferRef to add as side data. The ownership of
+ * the reference is transferred to the frame.
+ *
+ * @return newly added side data on success, NULL on error. On failure
+ * the frame is unchanged and the AVBufferRef remains owned by
+ * the caller.
+ */
+AVFrameSideData* av_frame_new_side_data_from_buf(AVFrame* frame,
+ enum AVFrameSideDataType type,
+ AVBufferRef* buf);
+
+/**
+ * @return a pointer to the side data of a given type on success, NULL if there
+ * is no side data with such type in this frame.
+ */
+AVFrameSideData* av_frame_get_side_data(const AVFrame* frame,
+ enum AVFrameSideDataType type);
+
+/**
+ * Remove and free all side data instances of the given type.
+ */
+void av_frame_remove_side_data(AVFrame* frame, enum AVFrameSideDataType type);
+
+/**
+ * Flags for frame cropping.
+ */
+enum {
+ /**
+ * Apply the maximum possible cropping, even if it requires setting the
+ * AVFrame.data[] entries to unaligned pointers. Passing unaligned data
+ * to FFmpeg API is generally not allowed, and causes undefined behavior
+ * (such as crashes). You can pass unaligned data only to FFmpeg APIs that
+ * are explicitly documented to accept it. Use this flag only if you
+ * absolutely know what you are doing.
+ */
+ AV_FRAME_CROP_UNALIGNED = 1 << 0,
+};
+
+/**
+ * Crop the given video AVFrame according to its crop_left/crop_top/crop_right/
+ * crop_bottom fields. If cropping is successful, the function will adjust the
+ * data pointers and the width/height fields, and set the crop fields to 0.
+ *
+ * In all cases, the cropping boundaries will be rounded to the inherent
+ * alignment of the pixel format. In some cases, such as for opaque hwaccel
+ * formats, the left/top cropping is ignored. The crop fields are set to 0 even
+ * if the cropping was rounded or ignored.
+ *
+ * @param frame the frame which should be cropped
+ * @param flags Some combination of AV_FRAME_CROP_* flags, or 0.
+ *
+ * @return >= 0 on success, a negative AVERROR on error. If the cropping fields
+ * were invalid, AVERROR(ERANGE) is returned, and nothing is changed.
+ */
+int av_frame_apply_cropping(AVFrame* frame, int flags);
+
+/**
+ * @return a string identifying the side data type
+ */
+const char* av_frame_side_data_name(enum AVFrameSideDataType type);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_FRAME_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/hwcontext.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/hwcontext.h
new file mode 100644
index 0000000000..fb6877c25f
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/hwcontext.h
@@ -0,0 +1,601 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_HWCONTEXT_H
+#define AVUTIL_HWCONTEXT_H
+
+#include "buffer.h"
+#include "frame.h"
+#include "log.h"
+#include "pixfmt.h"
+
+enum AVHWDeviceType {
+ AV_HWDEVICE_TYPE_NONE,
+ AV_HWDEVICE_TYPE_VDPAU,
+ AV_HWDEVICE_TYPE_CUDA,
+ AV_HWDEVICE_TYPE_VAAPI,
+ AV_HWDEVICE_TYPE_DXVA2,
+ AV_HWDEVICE_TYPE_QSV,
+ AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
+ AV_HWDEVICE_TYPE_D3D11VA,
+ AV_HWDEVICE_TYPE_DRM,
+ AV_HWDEVICE_TYPE_OPENCL,
+ AV_HWDEVICE_TYPE_MEDIACODEC,
+ AV_HWDEVICE_TYPE_VULKAN,
+};
+
+typedef struct AVHWDeviceInternal AVHWDeviceInternal;
+
+/**
+ * This struct aggregates all the (hardware/vendor-specific) "high-level" state,
+ * i.e. state that is not tied to a concrete processing configuration.
+ * E.g., in an API that supports hardware-accelerated encoding and decoding,
+ * this struct will (if possible) wrap the state that is common to both encoding
+ * and decoding and from which specific instances of encoders or decoders can be
+ * derived.
+ *
+ * This struct is reference-counted with the AVBuffer mechanism. The
+ * av_hwdevice_ctx_alloc() constructor yields a reference, whose data field
+ * points to the actual AVHWDeviceContext. Further objects derived from
+ * AVHWDeviceContext (such as AVHWFramesContext, describing a frame pool with
+ * specific properties) will hold an internal reference to it. After all the
+ * references are released, the AVHWDeviceContext itself will be freed,
+ * optionally invoking a user-specified callback for uninitializing the hardware
+ * state.
+ */
+typedef struct AVHWDeviceContext {
+ /**
+ * A class for logging. Set by av_hwdevice_ctx_alloc().
+ */
+ const AVClass* av_class;
+
+ /**
+ * Private data used internally by libavutil. Must not be accessed in any
+ * way by the caller.
+ */
+ AVHWDeviceInternal* internal;
+
+ /**
+ * This field identifies the underlying API used for hardware access.
+ *
+ * This field is set when this struct is allocated and never changed
+ * afterwards.
+ */
+ enum AVHWDeviceType type;
+
+ /**
+ * The format-specific data, allocated and freed by libavutil along with
+ * this context.
+ *
+ * Should be cast by the user to the format-specific context defined in the
+ * corresponding header (hwcontext_*.h) and filled as described in the
+ * documentation before calling av_hwdevice_ctx_init().
+ *
+ * After calling av_hwdevice_ctx_init() this struct should not be modified
+ * by the caller.
+ */
+ void* hwctx;
+
+ /**
+ * This field may be set by the caller before calling av_hwdevice_ctx_init().
+ *
+ * If non-NULL, this callback will be called when the last reference to
+ * this context is unreferenced, immediately before it is freed.
+ *
+ * @note when other objects (e.g an AVHWFramesContext) are derived from this
+ * struct, this callback will be invoked after all such child objects
+ * are fully uninitialized and their respective destructors invoked.
+ */
+ void (*free)(struct AVHWDeviceContext* ctx);
+
+ /**
+ * Arbitrary user data, to be used e.g. by the free() callback.
+ */
+ void* user_opaque;
+} AVHWDeviceContext;
+
+typedef struct AVHWFramesInternal AVHWFramesInternal;
+
+/**
+ * This struct describes a set or pool of "hardware" frames (i.e. those with
+ * data not located in normal system memory). All the frames in the pool are
+ * assumed to be allocated in the same way and interchangeable.
+ *
+ * This struct is reference-counted with the AVBuffer mechanism and tied to a
+ * given AVHWDeviceContext instance. The av_hwframe_ctx_alloc() constructor
+ * yields a reference, whose data field points to the actual AVHWFramesContext
+ * struct.
+ */
+typedef struct AVHWFramesContext {
+ /**
+ * A class for logging.
+ */
+ const AVClass* av_class;
+
+ /**
+ * Private data used internally by libavutil. Must not be accessed in any
+ * way by the caller.
+ */
+ AVHWFramesInternal* internal;
+
+ /**
+ * A reference to the parent AVHWDeviceContext. This reference is owned and
+ * managed by the enclosing AVHWFramesContext, but the caller may derive
+ * additional references from it.
+ */
+ AVBufferRef* device_ref;
+
+ /**
+ * The parent AVHWDeviceContext. This is simply a pointer to
+ * device_ref->data provided for convenience.
+ *
+ * Set by libavutil in av_hwframe_ctx_init().
+ */
+ AVHWDeviceContext* device_ctx;
+
+ /**
+ * The format-specific data, allocated and freed automatically along with
+ * this context.
+ *
+ * Should be cast by the user to the format-specific context defined in the
+ * corresponding header (hwframe_*.h) and filled as described in the
+ * documentation before calling av_hwframe_ctx_init().
+ *
+ * After any frames using this context are created, the contents of this
+ * struct should not be modified by the caller.
+ */
+ void* hwctx;
+
+ /**
+ * This field may be set by the caller before calling av_hwframe_ctx_init().
+ *
+ * If non-NULL, this callback will be called when the last reference to
+ * this context is unreferenced, immediately before it is freed.
+ */
+ void (*free)(struct AVHWFramesContext* ctx);
+
+ /**
+ * Arbitrary user data, to be used e.g. by the free() callback.
+ */
+ void* user_opaque;
+
+ /**
+ * A pool from which the frames are allocated by av_hwframe_get_buffer().
+ * This field may be set by the caller before calling av_hwframe_ctx_init().
+ * The buffers returned by calling av_buffer_pool_get() on this pool must
+ * have the properties described in the documentation in the corresponding hw
+ * type's header (hwcontext_*.h). The pool will be freed strictly before
+ * this struct's free() callback is invoked.
+ *
+ * This field may be NULL, then libavutil will attempt to allocate a pool
+ * internally. Note that certain device types enforce pools allocated at
+ * fixed size (frame count), which cannot be extended dynamically. In such a
+ * case, initial_pool_size must be set appropriately.
+ */
+ AVBufferPool* pool;
+
+ /**
+ * Initial size of the frame pool. If a device type does not support
+ * dynamically resizing the pool, then this is also the maximum pool size.
+ *
+ * May be set by the caller before calling av_hwframe_ctx_init(). Must be
+ * set if pool is NULL and the device type does not support dynamic pools.
+ */
+ int initial_pool_size;
+
+ /**
+ * The pixel format identifying the underlying HW surface type.
+ *
+ * Must be a hwaccel format, i.e. the corresponding descriptor must have the
+ * AV_PIX_FMT_FLAG_HWACCEL flag set.
+ *
+ * Must be set by the user before calling av_hwframe_ctx_init().
+ */
+ enum AVPixelFormat format;
+
+ /**
+ * The pixel format identifying the actual data layout of the hardware
+ * frames.
+ *
+ * Must be set by the caller before calling av_hwframe_ctx_init().
+ *
+ * @note when the underlying API does not provide the exact data layout, but
+ * only the colorspace/bit depth, this field should be set to the fully
+ * planar version of that format (e.g. for 8-bit 420 YUV it should be
+ * AV_PIX_FMT_YUV420P, not AV_PIX_FMT_NV12 or anything else).
+ */
+ enum AVPixelFormat sw_format;
+
+ /**
+ * The allocated dimensions of the frames in this pool.
+ *
+ * Must be set by the user before calling av_hwframe_ctx_init().
+ */
+ int width, height;
+} AVHWFramesContext;
+
+/**
+ * Look up an AVHWDeviceType by name.
+ *
+ * @param name String name of the device type (case-insensitive).
+ * @return The type from enum AVHWDeviceType, or AV_HWDEVICE_TYPE_NONE if
+ * not found.
+ */
+enum AVHWDeviceType av_hwdevice_find_type_by_name(const char* name);
+
+/** Get the string name of an AVHWDeviceType.
+ *
+ * @param type Type from enum AVHWDeviceType.
+ * @return Pointer to a static string containing the name, or NULL if the type
+ * is not valid.
+ */
+const char* av_hwdevice_get_type_name(enum AVHWDeviceType type);
+
+/**
+ * Iterate over supported device types.
+ *
+ * @param type AV_HWDEVICE_TYPE_NONE initially, then the previous type
+ * returned by this function in subsequent iterations.
+ * @return The next usable device type from enum AVHWDeviceType, or
+ * AV_HWDEVICE_TYPE_NONE if there are no more.
+ */
+enum AVHWDeviceType av_hwdevice_iterate_types(enum AVHWDeviceType prev);
+
+/**
+ * Allocate an AVHWDeviceContext for a given hardware type.
+ *
+ * @param type the type of the hardware device to allocate.
+ * @return a reference to the newly created AVHWDeviceContext on success or NULL
+ * on failure.
+ */
+AVBufferRef* av_hwdevice_ctx_alloc(enum AVHWDeviceType type);
+
+/**
+ * Finalize the device context before use. This function must be called after
+ * the context is filled with all the required information and before it is
+ * used in any way.
+ *
+ * @param ref a reference to the AVHWDeviceContext
+ * @return 0 on success, a negative AVERROR code on failure
+ */
+int av_hwdevice_ctx_init(AVBufferRef* ref);
+
+/**
+ * Open a device of the specified type and create an AVHWDeviceContext for it.
+ *
+ * This is a convenience function intended to cover the simple cases. Callers
+ * who need to fine-tune device creation/management should open the device
+ * manually and then wrap it in an AVHWDeviceContext using
+ * av_hwdevice_ctx_alloc()/av_hwdevice_ctx_init().
+ *
+ * The returned context is already initialized and ready for use, the caller
+ * should not call av_hwdevice_ctx_init() on it. The user_opaque/free fields of
+ * the created AVHWDeviceContext are set by this function and should not be
+ * touched by the caller.
+ *
+ * @param device_ctx On success, a reference to the newly-created device context
+ * will be written here. The reference is owned by the caller
+ * and must be released with av_buffer_unref() when no longer
+ * needed. On failure, NULL will be written to this pointer.
+ * @param type The type of the device to create.
+ * @param device A type-specific string identifying the device to open.
+ * @param opts A dictionary of additional (type-specific) options to use in
+ * opening the device. The dictionary remains owned by the caller.
+ * @param flags currently unused
+ *
+ * @return 0 on success, a negative AVERROR code on failure.
+ */
+int av_hwdevice_ctx_create(AVBufferRef** device_ctx, enum AVHWDeviceType type,
+ const char* device, AVDictionary* opts, int flags);
+
+/**
+ * Create a new device of the specified type from an existing device.
+ *
+ * If the source device is a device of the target type or was originally
+ * derived from such a device (possibly through one or more intermediate
+ * devices of other types), then this will return a reference to the
+ * existing device of the same type as is requested.
+ *
+ * Otherwise, it will attempt to derive a new device from the given source
+ * device. If direct derivation to the new type is not implemented, it will
+ * attempt the same derivation from each ancestor of the source device in
+ * turn looking for an implemented derivation method.
+ *
+ * @param dst_ctx On success, a reference to the newly-created
+ * AVHWDeviceContext.
+ * @param type The type of the new device to create.
+ * @param src_ctx A reference to an existing AVHWDeviceContext which will be
+ * used to create the new device.
+ * @param flags Currently unused; should be set to zero.
+ * @return Zero on success, a negative AVERROR code on failure.
+ */
+int av_hwdevice_ctx_create_derived(AVBufferRef** dst_ctx,
+ enum AVHWDeviceType type,
+ AVBufferRef* src_ctx, int flags);
+
+/**
+ * Create a new device of the specified type from an existing device.
+ *
+ * This function performs the same action as av_hwdevice_ctx_create_derived,
+ * however, it is able to set options for the new device to be derived.
+ *
+ * @param dst_ctx On success, a reference to the newly-created
+ * AVHWDeviceContext.
+ * @param type The type of the new device to create.
+ * @param src_ctx A reference to an existing AVHWDeviceContext which will be
+ * used to create the new device.
+ * @param options Options for the new device to create, same format as in
+ * av_hwdevice_ctx_create.
+ * @param flags Currently unused; should be set to zero.
+ * @return Zero on success, a negative AVERROR code on failure.
+ */
+int av_hwdevice_ctx_create_derived_opts(AVBufferRef** dst_ctx,
+ enum AVHWDeviceType type,
+ AVBufferRef* src_ctx,
+ AVDictionary* options, int flags);
+
+/**
+ * Allocate an AVHWFramesContext tied to a given device context.
+ *
+ * @param device_ctx a reference to a AVHWDeviceContext. This function will make
+ * a new reference for internal use, the one passed to the
+ * function remains owned by the caller.
+ * @return a reference to the newly created AVHWFramesContext on success or NULL
+ * on failure.
+ */
+AVBufferRef* av_hwframe_ctx_alloc(AVBufferRef* device_ctx);
+
+/**
+ * Finalize the context before use. This function must be called after the
+ * context is filled with all the required information and before it is attached
+ * to any frames.
+ *
+ * @param ref a reference to the AVHWFramesContext
+ * @return 0 on success, a negative AVERROR code on failure
+ */
+int av_hwframe_ctx_init(AVBufferRef* ref);
+
+/**
+ * Allocate a new frame attached to the given AVHWFramesContext.
+ *
+ * @param hwframe_ctx a reference to an AVHWFramesContext
+ * @param frame an empty (freshly allocated or unreffed) frame to be filled with
+ * newly allocated buffers.
+ * @param flags currently unused, should be set to zero
+ * @return 0 on success, a negative AVERROR code on failure
+ */
+int av_hwframe_get_buffer(AVBufferRef* hwframe_ctx, AVFrame* frame, int flags);
+
+/**
+ * Copy data to or from a hw surface. At least one of dst/src must have an
+ * AVHWFramesContext attached.
+ *
+ * If src has an AVHWFramesContext attached, then the format of dst (if set)
+ * must use one of the formats returned by av_hwframe_transfer_get_formats(src,
+ * AV_HWFRAME_TRANSFER_DIRECTION_FROM).
+ * If dst has an AVHWFramesContext attached, then the format of src must use one
+ * of the formats returned by av_hwframe_transfer_get_formats(dst,
+ * AV_HWFRAME_TRANSFER_DIRECTION_TO)
+ *
+ * dst may be "clean" (i.e. with data/buf pointers unset), in which case the
+ * data buffers will be allocated by this function using av_frame_get_buffer().
+ * If dst->format is set, then this format will be used, otherwise (when
+ * dst->format is AV_PIX_FMT_NONE) the first acceptable format will be chosen.
+ *
+ * The two frames must have matching allocated dimensions (i.e. equal to
+ * AVHWFramesContext.width/height), since not all device types support
+ * transferring a sub-rectangle of the whole surface. The display dimensions
+ * (i.e. AVFrame.width/height) may be smaller than the allocated dimensions, but
+ * also have to be equal for both frames. When the display dimensions are
+ * smaller than the allocated dimensions, the content of the padding in the
+ * destination frame is unspecified.
+ *
+ * @param dst the destination frame. dst is not touched on failure.
+ * @param src the source frame.
+ * @param flags currently unused, should be set to zero
+ * @return 0 on success, a negative AVERROR error code on failure.
+ */
+int av_hwframe_transfer_data(AVFrame* dst, const AVFrame* src, int flags);
+
+enum AVHWFrameTransferDirection {
+ /**
+ * Transfer the data from the queried hw frame.
+ */
+ AV_HWFRAME_TRANSFER_DIRECTION_FROM,
+
+ /**
+ * Transfer the data to the queried hw frame.
+ */
+ AV_HWFRAME_TRANSFER_DIRECTION_TO,
+};
+
+/**
+ * Get a list of possible source or target formats usable in
+ * av_hwframe_transfer_data().
+ *
+ * @param hwframe_ctx the frame context to obtain the information for
+ * @param dir the direction of the transfer
+ * @param formats the pointer to the output format list will be written here.
+ * The list is terminated with AV_PIX_FMT_NONE and must be freed
+ * by the caller when no longer needed using av_free().
+ * If this function returns successfully, the format list will
+ * have at least one item (not counting the terminator).
+ * On failure, the contents of this pointer are unspecified.
+ * @param flags currently unused, should be set to zero
+ * @return 0 on success, a negative AVERROR code on failure.
+ */
+int av_hwframe_transfer_get_formats(AVBufferRef* hwframe_ctx,
+ enum AVHWFrameTransferDirection dir,
+ enum AVPixelFormat** formats, int flags);
+
+/**
+ * This struct describes the constraints on hardware frames attached to
+ * a given device with a hardware-specific configuration. This is returned
+ * by av_hwdevice_get_hwframe_constraints() and must be freed by
+ * av_hwframe_constraints_free() after use.
+ */
+typedef struct AVHWFramesConstraints {
+ /**
+ * A list of possible values for format in the hw_frames_ctx,
+ * terminated by AV_PIX_FMT_NONE. This member will always be filled.
+ */
+ enum AVPixelFormat* valid_hw_formats;
+
+ /**
+ * A list of possible values for sw_format in the hw_frames_ctx,
+ * terminated by AV_PIX_FMT_NONE. Can be NULL if this information is
+ * not known.
+ */
+ enum AVPixelFormat* valid_sw_formats;
+
+ /**
+ * The minimum size of frames in this hw_frames_ctx.
+ * (Zero if not known.)
+ */
+ int min_width;
+ int min_height;
+
+ /**
+ * The maximum size of frames in this hw_frames_ctx.
+ * (INT_MAX if not known / no limit.)
+ */
+ int max_width;
+ int max_height;
+} AVHWFramesConstraints;
+
+/**
+ * Allocate a HW-specific configuration structure for a given HW device.
+ * After use, the user must free all members as required by the specific
+ * hardware structure being used, then free the structure itself with
+ * av_free().
+ *
+ * @param device_ctx a reference to the associated AVHWDeviceContext.
+ * @return The newly created HW-specific configuration structure on
+ * success or NULL on failure.
+ */
+void* av_hwdevice_hwconfig_alloc(AVBufferRef* device_ctx);
+
+/**
+ * Get the constraints on HW frames given a device and the HW-specific
+ * configuration to be used with that device. If no HW-specific
+ * configuration is provided, returns the maximum possible capabilities
+ * of the device.
+ *
+ * @param ref a reference to the associated AVHWDeviceContext.
+ * @param hwconfig a filled HW-specific configuration structure, or NULL
+ * to return the maximum possible capabilities of the device.
+ * @return AVHWFramesConstraints structure describing the constraints
+ * on the device, or NULL if not available.
+ */
+AVHWFramesConstraints* av_hwdevice_get_hwframe_constraints(
+ AVBufferRef* ref, const void* hwconfig);
+
+/**
+ * Free an AVHWFrameConstraints structure.
+ *
+ * @param constraints The (filled or unfilled) AVHWFrameConstraints structure.
+ */
+void av_hwframe_constraints_free(AVHWFramesConstraints** constraints);
+
+/**
+ * Flags to apply to frame mappings.
+ */
+enum {
+ /**
+ * The mapping must be readable.
+ */
+ AV_HWFRAME_MAP_READ = 1 << 0,
+ /**
+ * The mapping must be writeable.
+ */
+ AV_HWFRAME_MAP_WRITE = 1 << 1,
+ /**
+ * The mapped frame will be overwritten completely in subsequent
+ * operations, so the current frame data need not be loaded. Any values
+ * which are not overwritten are unspecified.
+ */
+ AV_HWFRAME_MAP_OVERWRITE = 1 << 2,
+ /**
+ * The mapping must be direct. That is, there must not be any copying in
+ * the map or unmap steps. Note that performance of direct mappings may
+ * be much lower than normal memory.
+ */
+ AV_HWFRAME_MAP_DIRECT = 1 << 3,
+};
+
+/**
+ * Map a hardware frame.
+ *
+ * This has a number of different possible effects, depending on the format
+ * and origin of the src and dst frames. On input, src should be a usable
+ * frame with valid buffers and dst should be blank (typically as just created
+ * by av_frame_alloc()). src should have an associated hwframe context, and
+ * dst may optionally have a format and associated hwframe context.
+ *
+ * If src was created by mapping a frame from the hwframe context of dst,
+ * then this function undoes the mapping - dst is replaced by a reference to
+ * the frame that src was originally mapped from.
+ *
+ * If both src and dst have an associated hwframe context, then this function
+ * attempts to map the src frame from its hardware context to that of dst and
+ * then fill dst with appropriate data to be usable there. This will only be
+ * possible if the hwframe contexts and associated devices are compatible -
+ * given compatible devices, av_hwframe_ctx_create_derived() can be used to
+ * create a hwframe context for dst in which mapping should be possible.
+ *
+ * If src has a hwframe context but dst does not, then the src frame is
+ * mapped to normal memory and should thereafter be usable as a normal frame.
+ * If the format is set on dst, then the mapping will attempt to create dst
+ * with that format and fail if it is not possible. If format is unset (is
+ * AV_PIX_FMT_NONE) then dst will be mapped with whatever the most appropriate
+ * format to use is (probably the sw_format of the src hwframe context).
+ *
+ * A return value of AVERROR(ENOSYS) indicates that the mapping is not
+ * possible with the given arguments and hwframe setup, while other return
+ * values indicate that it failed somehow.
+ *
+ * @param dst Destination frame, to contain the mapping.
+ * @param src Source frame, to be mapped.
+ * @param flags Some combination of AV_HWFRAME_MAP_* flags.
+ * @return Zero on success, negative AVERROR code on failure.
+ */
+int av_hwframe_map(AVFrame* dst, const AVFrame* src, int flags);
+
+/**
+ * Create and initialise an AVHWFramesContext as a mapping of another existing
+ * AVHWFramesContext on a different device.
+ *
+ * av_hwframe_ctx_init() should not be called after this.
+ *
+ * @param derived_frame_ctx On success, a reference to the newly created
+ * AVHWFramesContext.
+ * @param derived_device_ctx A reference to the device to create the new
+ * AVHWFramesContext on.
+ * @param source_frame_ctx A reference to an existing AVHWFramesContext
+ * which will be mapped to the derived context.
+ * @param flags Some combination of AV_HWFRAME_MAP_* flags, defining the
+ * mapping parameters to apply to frames which are allocated
+ * in the derived device.
+ * @return Zero on success, negative AVERROR code on failure.
+ */
+int av_hwframe_ctx_create_derived(AVBufferRef** derived_frame_ctx,
+ enum AVPixelFormat format,
+ AVBufferRef* derived_device_ctx,
+ AVBufferRef* source_frame_ctx, int flags);
+
+#endif /* AVUTIL_HWCONTEXT_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/hwcontext_drm.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/hwcontext_drm.h
new file mode 100644
index 0000000000..42709f215e
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/hwcontext_drm.h
@@ -0,0 +1,169 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_HWCONTEXT_DRM_H
+#define AVUTIL_HWCONTEXT_DRM_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+/**
+ * @file
+ * API-specific header for AV_HWDEVICE_TYPE_DRM.
+ *
+ * Internal frame allocation is not currently supported - all frames
+ * must be allocated by the user. Thus AVHWFramesContext is always
+ * NULL, though this may change if support for frame allocation is
+ * added in future.
+ */
+
+enum {
+ /**
+ * The maximum number of layers/planes in a DRM frame.
+ */
+ AV_DRM_MAX_PLANES = 4
+};
+
+/**
+ * DRM object descriptor.
+ *
+ * Describes a single DRM object, addressing it as a PRIME file
+ * descriptor.
+ */
+typedef struct AVDRMObjectDescriptor {
+ /**
+ * DRM PRIME fd for the object.
+ */
+ int fd;
+ /**
+ * Total size of the object.
+ *
+ * (This includes any parts not which do not contain image data.)
+ */
+ size_t size;
+ /**
+ * Format modifier applied to the object (DRM_FORMAT_MOD_*).
+ *
+ * If the format modifier is unknown then this should be set to
+ * DRM_FORMAT_MOD_INVALID.
+ */
+ uint64_t format_modifier;
+} AVDRMObjectDescriptor;
+
+/**
+ * DRM plane descriptor.
+ *
+ * Describes a single plane of a layer, which is contained within
+ * a single object.
+ */
+typedef struct AVDRMPlaneDescriptor {
+ /**
+ * Index of the object containing this plane in the objects
+ * array of the enclosing frame descriptor.
+ */
+ int object_index;
+ /**
+ * Offset within that object of this plane.
+ */
+ ptrdiff_t offset;
+ /**
+ * Pitch (linesize) of this plane.
+ */
+ ptrdiff_t pitch;
+} AVDRMPlaneDescriptor;
+
+/**
+ * DRM layer descriptor.
+ *
+ * Describes a single layer within a frame. This has the structure
+ * defined by its format, and will contain one or more planes.
+ */
+typedef struct AVDRMLayerDescriptor {
+ /**
+ * Format of the layer (DRM_FORMAT_*).
+ */
+ uint32_t format;
+ /**
+ * Number of planes in the layer.
+ *
+ * This must match the number of planes required by format.
+ */
+ int nb_planes;
+ /**
+ * Array of planes in this layer.
+ */
+ AVDRMPlaneDescriptor planes[AV_DRM_MAX_PLANES];
+} AVDRMLayerDescriptor;
+
+/**
+ * DRM frame descriptor.
+ *
+ * This is used as the data pointer for AV_PIX_FMT_DRM_PRIME frames.
+ * It is also used by user-allocated frame pools - allocating in
+ * AVHWFramesContext.pool must return AVBufferRefs which contain
+ * an object of this type.
+ *
+ * The fields of this structure should be set such it can be
+ * imported directly by EGL using the EGL_EXT_image_dma_buf_import
+ * and EGL_EXT_image_dma_buf_import_modifiers extensions.
+ * (Note that the exact layout of a particular format may vary between
+ * platforms - we only specify that the same platform should be able
+ * to import it.)
+ *
+ * The total number of planes must not exceed AV_DRM_MAX_PLANES, and
+ * the order of the planes by increasing layer index followed by
+ * increasing plane index must be the same as the order which would
+ * be used for the data pointers in the equivalent software format.
+ */
+typedef struct AVDRMFrameDescriptor {
+ /**
+ * Number of DRM objects making up this frame.
+ */
+ int nb_objects;
+ /**
+ * Array of objects making up the frame.
+ */
+ AVDRMObjectDescriptor objects[AV_DRM_MAX_PLANES];
+ /**
+ * Number of layers in the frame.
+ */
+ int nb_layers;
+ /**
+ * Array of layers in the frame.
+ */
+ AVDRMLayerDescriptor layers[AV_DRM_MAX_PLANES];
+} AVDRMFrameDescriptor;
+
+/**
+ * DRM device.
+ *
+ * Allocated as AVHWDeviceContext.hwctx.
+ */
+typedef struct AVDRMDeviceContext {
+ /**
+ * File descriptor of DRM device.
+ *
+ * This is used as the device to create frames on, and may also be
+ * used in some derivation and mapping operations.
+ *
+ * If no device is required, set to -1.
+ */
+ int fd;
+} AVDRMDeviceContext;
+
+#endif /* AVUTIL_HWCONTEXT_DRM_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/hwcontext_vaapi.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/hwcontext_vaapi.h
new file mode 100644
index 0000000000..058b5f110d
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/hwcontext_vaapi.h
@@ -0,0 +1,117 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_HWCONTEXT_VAAPI_H
+#define AVUTIL_HWCONTEXT_VAAPI_H
+
+#include <va/va.h>
+
+/**
+ * @file
+ * API-specific header for AV_HWDEVICE_TYPE_VAAPI.
+ *
+ * Dynamic frame pools are supported, but note that any pool used as a render
+ * target is required to be of fixed size in order to be be usable as an
+ * argument to vaCreateContext().
+ *
+ * For user-allocated pools, AVHWFramesContext.pool must return AVBufferRefs
+ * with the data pointer set to a VASurfaceID.
+ */
+
+enum {
+ /**
+ * The quirks field has been set by the user and should not be detected
+ * automatically by av_hwdevice_ctx_init().
+ */
+ AV_VAAPI_DRIVER_QUIRK_USER_SET = (1 << 0),
+ /**
+ * The driver does not destroy parameter buffers when they are used by
+ * vaRenderPicture(). Additional code will be required to destroy them
+ * separately afterwards.
+ */
+ AV_VAAPI_DRIVER_QUIRK_RENDER_PARAM_BUFFERS = (1 << 1),
+
+ /**
+ * The driver does not support the VASurfaceAttribMemoryType attribute,
+ * so the surface allocation code will not try to use it.
+ */
+ AV_VAAPI_DRIVER_QUIRK_ATTRIB_MEMTYPE = (1 << 2),
+
+ /**
+ * The driver does not support surface attributes at all.
+ * The surface allocation code will never pass them to surface allocation,
+ * and the results of the vaQuerySurfaceAttributes() call will be faked.
+ */
+ AV_VAAPI_DRIVER_QUIRK_SURFACE_ATTRIBUTES = (1 << 3),
+};
+
+/**
+ * VAAPI connection details.
+ *
+ * Allocated as AVHWDeviceContext.hwctx
+ */
+typedef struct AVVAAPIDeviceContext {
+ /**
+ * The VADisplay handle, to be filled by the user.
+ */
+ VADisplay display;
+ /**
+ * Driver quirks to apply - this is filled by av_hwdevice_ctx_init(),
+ * with reference to a table of known drivers, unless the
+ * AV_VAAPI_DRIVER_QUIRK_USER_SET bit is already present. The user
+ * may need to refer to this field when performing any later
+ * operations using VAAPI with the same VADisplay.
+ */
+ unsigned int driver_quirks;
+} AVVAAPIDeviceContext;
+
+/**
+ * VAAPI-specific data associated with a frame pool.
+ *
+ * Allocated as AVHWFramesContext.hwctx.
+ */
+typedef struct AVVAAPIFramesContext {
+ /**
+ * Set by the user to apply surface attributes to all surfaces in
+ * the frame pool. If null, default settings are used.
+ */
+ VASurfaceAttrib* attributes;
+ int nb_attributes;
+ /**
+ * The surfaces IDs of all surfaces in the pool after creation.
+ * Only valid if AVHWFramesContext.initial_pool_size was positive.
+ * These are intended to be used as the render_targets arguments to
+ * vaCreateContext().
+ */
+ VASurfaceID* surface_ids;
+ int nb_surfaces;
+} AVVAAPIFramesContext;
+
+/**
+ * VAAPI hardware pipeline configuration details.
+ *
+ * Allocated with av_hwdevice_hwconfig_alloc().
+ */
+typedef struct AVVAAPIHWConfig {
+ /**
+ * ID of a VAAPI pipeline configuration.
+ */
+ VAConfigID config_id;
+} AVVAAPIHWConfig;
+
+#endif /* AVUTIL_HWCONTEXT_VAAPI_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/intfloat.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/intfloat.h
new file mode 100644
index 0000000000..f373c97796
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/intfloat.h
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2011 Mans Rullgard
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_INTFLOAT_H
+#define AVUTIL_INTFLOAT_H
+
+#include <stdint.h>
+#include "attributes.h"
+
+union av_intfloat32 {
+ uint32_t i;
+ float f;
+};
+
+union av_intfloat64 {
+ uint64_t i;
+ double f;
+};
+
+/**
+ * Reinterpret a 32-bit integer as a float.
+ */
+static av_always_inline float av_int2float(uint32_t i) {
+ union av_intfloat32 v;
+ v.i = i;
+ return v.f;
+}
+
+/**
+ * Reinterpret a float as a 32-bit integer.
+ */
+static av_always_inline uint32_t av_float2int(float f) {
+ union av_intfloat32 v;
+ v.f = f;
+ return v.i;
+}
+
+/**
+ * Reinterpret a 64-bit integer as a double.
+ */
+static av_always_inline double av_int2double(uint64_t i) {
+ union av_intfloat64 v;
+ v.i = i;
+ return v.f;
+}
+
+/**
+ * Reinterpret a double as a 64-bit integer.
+ */
+static av_always_inline uint64_t av_double2int(double f) {
+ union av_intfloat64 v;
+ v.f = f;
+ return v.i;
+}
+
+#endif /* AVUTIL_INTFLOAT_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/log.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/log.h
new file mode 100644
index 0000000000..3b36df50c8
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/log.h
@@ -0,0 +1,388 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_LOG_H
+#define AVUTIL_LOG_H
+
+#include <stdarg.h>
+#include "avutil.h"
+#include "attributes.h"
+
+typedef enum {
+ AV_CLASS_CATEGORY_NA = 0,
+ AV_CLASS_CATEGORY_INPUT,
+ AV_CLASS_CATEGORY_OUTPUT,
+ AV_CLASS_CATEGORY_MUXER,
+ AV_CLASS_CATEGORY_DEMUXER,
+ AV_CLASS_CATEGORY_ENCODER,
+ AV_CLASS_CATEGORY_DECODER,
+ AV_CLASS_CATEGORY_FILTER,
+ AV_CLASS_CATEGORY_BITSTREAM_FILTER,
+ AV_CLASS_CATEGORY_SWSCALER,
+ AV_CLASS_CATEGORY_SWRESAMPLER,
+ AV_CLASS_CATEGORY_DEVICE_VIDEO_OUTPUT = 40,
+ AV_CLASS_CATEGORY_DEVICE_VIDEO_INPUT,
+ AV_CLASS_CATEGORY_DEVICE_AUDIO_OUTPUT,
+ AV_CLASS_CATEGORY_DEVICE_AUDIO_INPUT,
+ AV_CLASS_CATEGORY_DEVICE_OUTPUT,
+ AV_CLASS_CATEGORY_DEVICE_INPUT,
+ AV_CLASS_CATEGORY_NB ///< not part of ABI/API
+} AVClassCategory;
+
+#define AV_IS_INPUT_DEVICE(category) \
+ (((category) == AV_CLASS_CATEGORY_DEVICE_VIDEO_INPUT) || \
+ ((category) == AV_CLASS_CATEGORY_DEVICE_AUDIO_INPUT) || \
+ ((category) == AV_CLASS_CATEGORY_DEVICE_INPUT))
+
+#define AV_IS_OUTPUT_DEVICE(category) \
+ (((category) == AV_CLASS_CATEGORY_DEVICE_VIDEO_OUTPUT) || \
+ ((category) == AV_CLASS_CATEGORY_DEVICE_AUDIO_OUTPUT) || \
+ ((category) == AV_CLASS_CATEGORY_DEVICE_OUTPUT))
+
+struct AVOptionRanges;
+
+/**
+ * Describe the class of an AVClass context structure. That is an
+ * arbitrary struct of which the first field is a pointer to an
+ * AVClass struct (e.g. AVCodecContext, AVFormatContext etc.).
+ */
+typedef struct AVClass {
+ /**
+ * The name of the class; usually it is the same name as the
+ * context structure type to which the AVClass is associated.
+ */
+ const char* class_name;
+
+ /**
+ * A pointer to a function which returns the name of a context
+ * instance ctx associated with the class.
+ */
+ const char* (*item_name)(void* ctx);
+
+ /**
+ * a pointer to the first option specified in the class if any or NULL
+ *
+ * @see av_set_default_options()
+ */
+ const struct AVOption* option;
+
+ /**
+ * LIBAVUTIL_VERSION with which this structure was created.
+ * This is used to allow fields to be added without requiring major
+ * version bumps everywhere.
+ */
+
+ int version;
+
+ /**
+ * Offset in the structure where log_level_offset is stored.
+ * 0 means there is no such variable
+ */
+ int log_level_offset_offset;
+
+ /**
+ * Offset in the structure where a pointer to the parent context for
+ * logging is stored. For example a decoder could pass its AVCodecContext
+ * to eval as such a parent context, which an av_log() implementation
+ * could then leverage to display the parent context.
+ * The offset can be NULL.
+ */
+ int parent_log_context_offset;
+
+ /**
+ * Category used for visualization (like color)
+ * This is only set if the category is equal for all objects using this class.
+ * available since version (51 << 16 | 56 << 8 | 100)
+ */
+ AVClassCategory category;
+
+ /**
+ * Callback to return the category.
+ * available since version (51 << 16 | 59 << 8 | 100)
+ */
+ AVClassCategory (*get_category)(void* ctx);
+
+ /**
+ * Callback to return the supported/allowed ranges.
+ * available since version (52.12)
+ */
+ int (*query_ranges)(struct AVOptionRanges**, void* obj, const char* key,
+ int flags);
+
+ /**
+ * Return next AVOptions-enabled child or NULL
+ */
+ void* (*child_next)(void* obj, void* prev);
+
+ /**
+ * Iterate over the AVClasses corresponding to potential AVOptions-enabled
+ * children.
+ *
+ * @param iter pointer to opaque iteration state. The caller must initialize
+ * *iter to NULL before the first call.
+ * @return AVClass for the next AVOptions-enabled child or NULL if there are
+ * no more such children.
+ *
+ * @note The difference between child_next and this is that child_next
+ * iterates over _already existing_ objects, while child_class_iterate
+ * iterates over _all possible_ children.
+ */
+ const struct AVClass* (*child_class_iterate)(void** iter);
+} AVClass;
+
+/**
+ * @addtogroup lavu_log
+ *
+ * @{
+ *
+ * @defgroup lavu_log_constants Logging Constants
+ *
+ * @{
+ */
+
+/**
+ * Print no output.
+ */
+#define AV_LOG_QUIET -8
+
+/**
+ * Something went really wrong and we will crash now.
+ */
+#define AV_LOG_PANIC 0
+
+/**
+ * Something went wrong and recovery is not possible.
+ * For example, no header was found for a format which depends
+ * on headers or an illegal combination of parameters is used.
+ */
+#define AV_LOG_FATAL 8
+
+/**
+ * Something went wrong and cannot losslessly be recovered.
+ * However, not all future data is affected.
+ */
+#define AV_LOG_ERROR 16
+
+/**
+ * Something somehow does not look correct. This may or may not
+ * lead to problems. An example would be the use of '-vstrict -2'.
+ */
+#define AV_LOG_WARNING 24
+
+/**
+ * Standard information.
+ */
+#define AV_LOG_INFO 32
+
+/**
+ * Detailed information.
+ */
+#define AV_LOG_VERBOSE 40
+
+/**
+ * Stuff which is only useful for libav* developers.
+ */
+#define AV_LOG_DEBUG 48
+
+/**
+ * Extremely verbose debugging, useful for libav* development.
+ */
+#define AV_LOG_TRACE 56
+
+#define AV_LOG_MAX_OFFSET (AV_LOG_TRACE - AV_LOG_QUIET)
+
+/**
+ * @}
+ */
+
+/**
+ * Sets additional colors for extended debugging sessions.
+ * @code
+ av_log(ctx, AV_LOG_DEBUG|AV_LOG_C(134), "Message in purple\n");
+ @endcode
+ * Requires 256color terminal support. Uses outside debugging is not
+ * recommended.
+ */
+#define AV_LOG_C(x) ((x) << 8)
+
+/**
+ * Send the specified message to the log if the level is less than or equal
+ * to the current av_log_level. By default, all logging messages are sent to
+ * stderr. This behavior can be altered by setting a different logging callback
+ * function.
+ * @see av_log_set_callback
+ *
+ * @param avcl A pointer to an arbitrary struct of which the first field is a
+ * pointer to an AVClass struct or NULL if general log.
+ * @param level The importance level of the message expressed using a @ref
+ * lavu_log_constants "Logging Constant".
+ * @param fmt The format string (printf-compatible) that specifies how
+ * subsequent arguments are converted to output.
+ */
+void av_log(void* avcl, int level, const char* fmt, ...) av_printf_format(3, 4);
+
+/**
+ * Send the specified message to the log once with the initial_level and then
+ * with the subsequent_level. By default, all logging messages are sent to
+ * stderr. This behavior can be altered by setting a different logging callback
+ * function.
+ * @see av_log
+ *
+ * @param avcl A pointer to an arbitrary struct of which the first field is a
+ * pointer to an AVClass struct or NULL if general log.
+ * @param initial_level importance level of the message expressed using a @ref
+ * lavu_log_constants "Logging Constant" for the first occurance.
+ * @param subsequent_level importance level of the message expressed using a
+ * @ref lavu_log_constants "Logging Constant" after the first occurance.
+ * @param fmt The format string (printf-compatible) that specifies how
+ * subsequent arguments are converted to output.
+ * @param state a variable to keep trak of if a message has already been printed
+ * this must be initialized to 0 before the first use. The same state
+ * must not be accessed by 2 Threads simultaneously.
+ */
+void av_log_once(void* avcl, int initial_level, int subsequent_level,
+ int* state, const char* fmt, ...) av_printf_format(5, 6);
+
+/**
+ * Send the specified message to the log if the level is less than or equal
+ * to the current av_log_level. By default, all logging messages are sent to
+ * stderr. This behavior can be altered by setting a different logging callback
+ * function.
+ * @see av_log_set_callback
+ *
+ * @param avcl A pointer to an arbitrary struct of which the first field is a
+ * pointer to an AVClass struct.
+ * @param level The importance level of the message expressed using a @ref
+ * lavu_log_constants "Logging Constant".
+ * @param fmt The format string (printf-compatible) that specifies how
+ * subsequent arguments are converted to output.
+ * @param vl The arguments referenced by the format string.
+ */
+void av_vlog(void* avcl, int level, const char* fmt, va_list vl);
+
+/**
+ * Get the current log level
+ *
+ * @see lavu_log_constants
+ *
+ * @return Current log level
+ */
+int av_log_get_level(void);
+
+/**
+ * Set the log level
+ *
+ * @see lavu_log_constants
+ *
+ * @param level Logging level
+ */
+void av_log_set_level(int level);
+
+/**
+ * Set the logging callback
+ *
+ * @note The callback must be thread safe, even if the application does not use
+ * threads itself as some codecs are multithreaded.
+ *
+ * @see av_log_default_callback
+ *
+ * @param callback A logging function with a compatible signature.
+ */
+void av_log_set_callback(void (*callback)(void*, int, const char*, va_list));
+
+/**
+ * Default logging callback
+ *
+ * It prints the message to stderr, optionally colorizing it.
+ *
+ * @param avcl A pointer to an arbitrary struct of which the first field is a
+ * pointer to an AVClass struct.
+ * @param level The importance level of the message expressed using a @ref
+ * lavu_log_constants "Logging Constant".
+ * @param fmt The format string (printf-compatible) that specifies how
+ * subsequent arguments are converted to output.
+ * @param vl The arguments referenced by the format string.
+ */
+void av_log_default_callback(void* avcl, int level, const char* fmt,
+ va_list vl);
+
+/**
+ * Return the context name
+ *
+ * @param ctx The AVClass context
+ *
+ * @return The AVClass class_name
+ */
+const char* av_default_item_name(void* ctx);
+AVClassCategory av_default_get_category(void* ptr);
+
+/**
+ * Format a line of log the same way as the default callback.
+ * @param line buffer to receive the formatted line
+ * @param line_size size of the buffer
+ * @param print_prefix used to store whether the prefix must be printed;
+ * must point to a persistent integer initially set to 1
+ */
+void av_log_format_line(void* ptr, int level, const char* fmt, va_list vl,
+ char* line, int line_size, int* print_prefix);
+
+/**
+ * Format a line of log the same way as the default callback.
+ * @param line buffer to receive the formatted line;
+ * may be NULL if line_size is 0
+ * @param line_size size of the buffer; at most line_size-1 characters will
+ * be written to the buffer, plus one null terminator
+ * @param print_prefix used to store whether the prefix must be printed;
+ * must point to a persistent integer initially set to 1
+ * @return Returns a negative value if an error occurred, otherwise returns
+ * the number of characters that would have been written for a
+ * sufficiently large buffer, not including the terminating null
+ * character. If the return value is not less than line_size, it means
+ * that the log message was truncated to fit the buffer.
+ */
+int av_log_format_line2(void* ptr, int level, const char* fmt, va_list vl,
+ char* line, int line_size, int* print_prefix);
+
+/**
+ * Skip repeated messages, this requires the user app to use av_log() instead of
+ * (f)printf as the 2 would otherwise interfere and lead to
+ * "Last message repeated x times" messages below (f)printf messages with some
+ * bad luck.
+ * Also to receive the last, "last repeated" line if any, the user app must
+ * call av_log(NULL, AV_LOG_QUIET, "%s", ""); at the end
+ */
+#define AV_LOG_SKIP_REPEATED 1
+
+/**
+ * Include the log severity in messages originating from codecs.
+ *
+ * Results in messages such as:
+ * [rawvideo @ 0xDEADBEEF] [error] encode did not produce valid pts
+ */
+#define AV_LOG_PRINT_LEVEL 2
+
+void av_log_set_flags(int arg);
+int av_log_get_flags(void);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_LOG_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/macros.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/macros.h
new file mode 100644
index 0000000000..1578d1a345
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/macros.h
@@ -0,0 +1,87 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * @ingroup lavu
+ * Utility Preprocessor macros
+ */
+
+#ifndef AVUTIL_MACROS_H
+#define AVUTIL_MACROS_H
+
+#include "libavutil/avconfig.h"
+
+#if AV_HAVE_BIGENDIAN
+# define AV_NE(be, le) (be)
+#else
+# define AV_NE(be, le) (le)
+#endif
+
+/**
+ * Comparator.
+ * For two numerical expressions x and y, gives 1 if x > y, -1 if x < y, and 0
+ * if x == y. This is useful for instance in a qsort comparator callback.
+ * Furthermore, compilers are able to optimize this to branchless code, and
+ * there is no risk of overflow with signed types.
+ * As with many macros, this evaluates its argument multiple times, it thus
+ * must not have a side-effect.
+ */
+#define FFDIFFSIGN(x, y) (((x) > (y)) - ((x) < (y)))
+
+#define FFMAX(a, b) ((a) > (b) ? (a) : (b))
+#define FFMAX3(a, b, c) FFMAX(FFMAX(a, b), c)
+#define FFMIN(a, b) ((a) > (b) ? (b) : (a))
+#define FFMIN3(a, b, c) FFMIN(FFMIN(a, b), c)
+
+#define FFSWAP(type, a, b) \
+ do { \
+ type SWAP_tmp = b; \
+ b = a; \
+ a = SWAP_tmp; \
+ } while (0)
+#define FF_ARRAY_ELEMS(a) (sizeof(a) / sizeof((a)[0]))
+
+#define MKTAG(a, b, c, d) \
+ ((a) | ((b) << 8) | ((c) << 16) | ((unsigned)(d) << 24))
+#define MKBETAG(a, b, c, d) \
+ ((d) | ((c) << 8) | ((b) << 16) | ((unsigned)(a) << 24))
+
+/**
+ * @addtogroup preproc_misc Preprocessor String Macros
+ *
+ * String manipulation macros
+ *
+ * @{
+ */
+
+#define AV_STRINGIFY(s) AV_TOSTRING(s)
+#define AV_TOSTRING(s) #s
+
+#define AV_GLUE(a, b) a##b
+#define AV_JOIN(a, b) AV_GLUE(a, b)
+
+/**
+ * @}
+ */
+
+#define AV_PRAGMA(s) _Pragma(#s)
+
+#define FFALIGN(x, a) (((x) + (a)-1) & ~((a)-1))
+
+#endif /* AVUTIL_MACROS_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/mathematics.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/mathematics.h
new file mode 100644
index 0000000000..3fa1e990ea
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/mathematics.h
@@ -0,0 +1,247 @@
+/*
+ * copyright (c) 2005-2012 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * @addtogroup lavu_math
+ * Mathematical utilities for working with timestamp and time base.
+ */
+
+#ifndef AVUTIL_MATHEMATICS_H
+#define AVUTIL_MATHEMATICS_H
+
+#include <stdint.h>
+#include <math.h>
+#include "attributes.h"
+#include "rational.h"
+#include "intfloat.h"
+
+#ifndef M_E
+# define M_E 2.7182818284590452354 /* e */
+#endif
+#ifndef M_LN2
+# define M_LN2 0.69314718055994530942 /* log_e 2 */
+#endif
+#ifndef M_LN10
+# define M_LN10 2.30258509299404568402 /* log_e 10 */
+#endif
+#ifndef M_LOG2_10
+# define M_LOG2_10 3.32192809488736234787 /* log_2 10 */
+#endif
+#ifndef M_PHI
+# define M_PHI 1.61803398874989484820 /* phi / golden ratio */
+#endif
+#ifndef M_PI
+# define M_PI 3.14159265358979323846 /* pi */
+#endif
+#ifndef M_PI_2
+# define M_PI_2 1.57079632679489661923 /* pi/2 */
+#endif
+#ifndef M_SQRT1_2
+# define M_SQRT1_2 0.70710678118654752440 /* 1/sqrt(2) */
+#endif
+#ifndef M_SQRT2
+# define M_SQRT2 1.41421356237309504880 /* sqrt(2) */
+#endif
+#ifndef NAN
+# define NAN av_int2float(0x7fc00000)
+#endif
+#ifndef INFINITY
+# define INFINITY av_int2float(0x7f800000)
+#endif
+
+/**
+ * @addtogroup lavu_math
+ *
+ * @{
+ */
+
+/**
+ * Rounding methods.
+ */
+enum AVRounding {
+ AV_ROUND_ZERO = 0, ///< Round toward zero.
+ AV_ROUND_INF = 1, ///< Round away from zero.
+ AV_ROUND_DOWN = 2, ///< Round toward -infinity.
+ AV_ROUND_UP = 3, ///< Round toward +infinity.
+ AV_ROUND_NEAR_INF =
+ 5, ///< Round to nearest and halfway cases away from zero.
+ /**
+ * Flag telling rescaling functions to pass `INT64_MIN`/`MAX` through
+ * unchanged, avoiding special cases for #AV_NOPTS_VALUE.
+ *
+ * Unlike other values of the enumeration AVRounding, this value is a
+ * bitmask that must be used in conjunction with another value of the
+ * enumeration through a bitwise OR, in order to set behavior for normal
+ * cases.
+ *
+ * @code{.c}
+ * av_rescale_rnd(3, 1, 2, AV_ROUND_UP | AV_ROUND_PASS_MINMAX);
+ * // Rescaling 3:
+ * // Calculating 3 * 1 / 2
+ * // 3 / 2 is rounded up to 2
+ * // => 2
+ *
+ * av_rescale_rnd(AV_NOPTS_VALUE, 1, 2, AV_ROUND_UP | AV_ROUND_PASS_MINMAX);
+ * // Rescaling AV_NOPTS_VALUE:
+ * // AV_NOPTS_VALUE == INT64_MIN
+ * // AV_NOPTS_VALUE is passed through
+ * // => AV_NOPTS_VALUE
+ * @endcode
+ */
+ AV_ROUND_PASS_MINMAX = 8192,
+};
+
+/**
+ * Compute the greatest common divisor of two integer operands.
+ *
+ * @param a,b Operands
+ * @return GCD of a and b up to sign; if a >= 0 and b >= 0, return value is >=
+ * 0; if a == 0 and b == 0, returns 0.
+ */
+int64_t av_const av_gcd(int64_t a, int64_t b);
+
+/**
+ * Rescale a 64-bit integer with rounding to nearest.
+ *
+ * The operation is mathematically equivalent to `a * b / c`, but writing that
+ * directly can overflow.
+ *
+ * This function is equivalent to av_rescale_rnd() with #AV_ROUND_NEAR_INF.
+ *
+ * @see av_rescale_rnd(), av_rescale_q(), av_rescale_q_rnd()
+ */
+int64_t av_rescale(int64_t a, int64_t b, int64_t c) av_const;
+
+/**
+ * Rescale a 64-bit integer with specified rounding.
+ *
+ * The operation is mathematically equivalent to `a * b / c`, but writing that
+ * directly can overflow, and does not support different rounding methods.
+ * If the result is not representable then INT64_MIN is returned.
+ *
+ * @see av_rescale(), av_rescale_q(), av_rescale_q_rnd()
+ */
+int64_t av_rescale_rnd(int64_t a, int64_t b, int64_t c,
+ enum AVRounding rnd) av_const;
+
+/**
+ * Rescale a 64-bit integer by 2 rational numbers.
+ *
+ * The operation is mathematically equivalent to `a * bq / cq`.
+ *
+ * This function is equivalent to av_rescale_q_rnd() with #AV_ROUND_NEAR_INF.
+ *
+ * @see av_rescale(), av_rescale_rnd(), av_rescale_q_rnd()
+ */
+int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq) av_const;
+
+/**
+ * Rescale a 64-bit integer by 2 rational numbers with specified rounding.
+ *
+ * The operation is mathematically equivalent to `a * bq / cq`.
+ *
+ * @see av_rescale(), av_rescale_rnd(), av_rescale_q()
+ */
+int64_t av_rescale_q_rnd(int64_t a, AVRational bq, AVRational cq,
+ enum AVRounding rnd) av_const;
+
+/**
+ * Compare two timestamps each in its own time base.
+ *
+ * @return One of the following values:
+ * - -1 if `ts_a` is before `ts_b`
+ * - 1 if `ts_a` is after `ts_b`
+ * - 0 if they represent the same position
+ *
+ * @warning
+ * The result of the function is undefined if one of the timestamps is outside
+ * the `int64_t` range when represented in the other's timebase.
+ */
+int av_compare_ts(int64_t ts_a, AVRational tb_a, int64_t ts_b, AVRational tb_b);
+
+/**
+ * Compare the remainders of two integer operands divided by a common divisor.
+ *
+ * In other words, compare the least significant `log2(mod)` bits of integers
+ * `a` and `b`.
+ *
+ * @code{.c}
+ * av_compare_mod(0x11, 0x02, 0x10) < 0 // since 0x11 % 0x10 (0x1) < 0x02 %
+ * 0x10 (0x2) av_compare_mod(0x11, 0x02, 0x20) > 0 // since 0x11 % 0x20 (0x11)
+ * > 0x02 % 0x20 (0x02)
+ * @endcode
+ *
+ * @param a,b Operands
+ * @param mod Divisor; must be a power of 2
+ * @return
+ * - a negative value if `a % mod < b % mod`
+ * - a positive value if `a % mod > b % mod`
+ * - zero if `a % mod == b % mod`
+ */
+int64_t av_compare_mod(uint64_t a, uint64_t b, uint64_t mod);
+
+/**
+ * Rescale a timestamp while preserving known durations.
+ *
+ * This function is designed to be called per audio packet to scale the input
+ * timestamp to a different time base. Compared to a simple av_rescale_q()
+ * call, this function is robust against possible inconsistent frame durations.
+ *
+ * The `last` parameter is a state variable that must be preserved for all
+ * subsequent calls for the same stream. For the first call, `*last` should be
+ * initialized to #AV_NOPTS_VALUE.
+ *
+ * @param[in] in_tb Input time base
+ * @param[in] in_ts Input timestamp
+ * @param[in] fs_tb Duration time base; typically this is finer-grained
+ * (greater) than `in_tb` and `out_tb`
+ * @param[in] duration Duration till the next call to this function (i.e.
+ * duration of the current packet/frame)
+ * @param[in,out] last Pointer to a timestamp expressed in terms of
+ * `fs_tb`, acting as a state variable
+ * @param[in] out_tb Output timebase
+ * @return Timestamp expressed in terms of `out_tb`
+ *
+ * @note In the context of this function, "duration" is in term of samples, not
+ * seconds.
+ */
+int64_t av_rescale_delta(AVRational in_tb, int64_t in_ts, AVRational fs_tb,
+ int duration, int64_t* last, AVRational out_tb);
+
+/**
+ * Add a value to a timestamp.
+ *
+ * This function guarantees that when the same value is repeatly added that
+ * no accumulation of rounding errors occurs.
+ *
+ * @param[in] ts Input timestamp
+ * @param[in] ts_tb Input timestamp time base
+ * @param[in] inc Value to be added
+ * @param[in] inc_tb Time base of `inc`
+ */
+int64_t av_add_stable(AVRational ts_tb, int64_t ts, AVRational inc_tb,
+ int64_t inc);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_MATHEMATICS_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/mem.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/mem.h
new file mode 100644
index 0000000000..3385d1fe9e
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/mem.h
@@ -0,0 +1,708 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * @ingroup lavu_mem
+ * Memory handling functions
+ */
+
+#ifndef AVUTIL_MEM_H
+#define AVUTIL_MEM_H
+
+#include <limits.h>
+#include <stdint.h>
+
+#include "attributes.h"
+#include "avutil.h"
+#include "version.h"
+
+/**
+ * @addtogroup lavu_mem
+ * Utilities for manipulating memory.
+ *
+ * FFmpeg has several applications of memory that are not required of a typical
+ * program. For example, the computing-heavy components like video decoding and
+ * encoding can be sped up significantly through the use of aligned memory.
+ *
+ * However, for each of FFmpeg's applications of memory, there might not be a
+ * recognized or standardized API for that specific use. Memory alignment, for
+ * instance, varies wildly depending on operating systems, architectures, and
+ * compilers. Hence, this component of @ref libavutil is created to make
+ * dealing with memory consistently possible on all platforms.
+ *
+ * @{
+ */
+
+#if FF_API_DECLARE_ALIGNED
+/**
+ *
+ * @defgroup lavu_mem_macros Alignment Macros
+ * Helper macros for declaring aligned variables.
+ * @{
+ */
+
+/**
+ * @def DECLARE_ALIGNED(n,t,v)
+ * Declare a variable that is aligned in memory.
+ *
+ * @code{.c}
+ * DECLARE_ALIGNED(16, uint16_t, aligned_int) = 42;
+ * DECLARE_ALIGNED(32, uint8_t, aligned_array)[128];
+ *
+ * // The default-alignment equivalent would be
+ * uint16_t aligned_int = 42;
+ * uint8_t aligned_array[128];
+ * @endcode
+ *
+ * @param n Minimum alignment in bytes
+ * @param t Type of the variable (or array element)
+ * @param v Name of the variable
+ */
+
+/**
+ * @def DECLARE_ASM_ALIGNED(n,t,v)
+ * Declare an aligned variable appropriate for use in inline assembly code.
+ *
+ * @code{.c}
+ * DECLARE_ASM_ALIGNED(16, uint64_t, pw_08) = UINT64_C(0x0008000800080008);
+ * @endcode
+ *
+ * @param n Minimum alignment in bytes
+ * @param t Type of the variable (or array element)
+ * @param v Name of the variable
+ */
+
+/**
+ * @def DECLARE_ASM_CONST(n,t,v)
+ * Declare a static constant aligned variable appropriate for use in inline
+ * assembly code.
+ *
+ * @code{.c}
+ * DECLARE_ASM_CONST(16, uint64_t, pw_08) = UINT64_C(0x0008000800080008);
+ * @endcode
+ *
+ * @param n Minimum alignment in bytes
+ * @param t Type of the variable (or array element)
+ * @param v Name of the variable
+ */
+
+# if defined(__INTEL_COMPILER) && __INTEL_COMPILER < 1110 || \
+ defined(__SUNPRO_C)
+# define DECLARE_ALIGNED(n, t, v) t __attribute__((aligned(n))) v
+# define DECLARE_ASM_ALIGNED(n, t, v) t __attribute__((aligned(n))) v
+# define DECLARE_ASM_CONST(n, t, v) const t __attribute__((aligned(n))) v
+# elif defined(__DJGPP__)
+# define DECLARE_ALIGNED(n, t, v) t __attribute__((aligned(FFMIN(n, 16)))) v
+# define DECLARE_ASM_ALIGNED(n, t, v) \
+ t av_used __attribute__((aligned(FFMIN(n, 16)))) v
+# define DECLARE_ASM_CONST(n, t, v) \
+ static const t av_used __attribute__((aligned(FFMIN(n, 16)))) v
+# elif defined(__GNUC__) || defined(__clang__)
+# define DECLARE_ALIGNED(n, t, v) t __attribute__((aligned(n))) v
+# define DECLARE_ASM_ALIGNED(n, t, v) t av_used __attribute__((aligned(n))) v
+# define DECLARE_ASM_CONST(n, t, v) \
+ static const t av_used __attribute__((aligned(n))) v
+# elif defined(_MSC_VER)
+# define DECLARE_ALIGNED(n, t, v) __declspec(align(n)) t v
+# define DECLARE_ASM_ALIGNED(n, t, v) __declspec(align(n)) t v
+# define DECLARE_ASM_CONST(n, t, v) __declspec(align(n)) static const t v
+# else
+# define DECLARE_ALIGNED(n, t, v) t v
+# define DECLARE_ASM_ALIGNED(n, t, v) t v
+# define DECLARE_ASM_CONST(n, t, v) static const t v
+# endif
+
+/**
+ * @}
+ */
+#endif
+
+/**
+ * @defgroup lavu_mem_attrs Function Attributes
+ * Function attributes applicable to memory handling functions.
+ *
+ * These function attributes can help compilers emit more useful warnings, or
+ * generate better code.
+ * @{
+ */
+
+/**
+ * @def av_malloc_attrib
+ * Function attribute denoting a malloc-like function.
+ *
+ * @see <a
+ * href="https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#index-g_t_0040code_007bmalloc_007d-function-attribute-3251">Function
+ * attribute `malloc` in GCC's documentation</a>
+ */
+
+#if AV_GCC_VERSION_AT_LEAST(3, 1)
+# define av_malloc_attrib __attribute__((__malloc__))
+#else
+# define av_malloc_attrib
+#endif
+
+/**
+ * @def av_alloc_size(...)
+ * Function attribute used on a function that allocates memory, whose size is
+ * given by the specified parameter(s).
+ *
+ * @code{.c}
+ * void *av_malloc(size_t size) av_alloc_size(1);
+ * void *av_calloc(size_t nmemb, size_t size) av_alloc_size(1, 2);
+ * @endcode
+ *
+ * @param ... One or two parameter indexes, separated by a comma
+ *
+ * @see <a
+ * href="https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#index-g_t_0040code_007balloc_005fsize_007d-function-attribute-3220">Function
+ * attribute `alloc_size` in GCC's documentation</a>
+ */
+
+#if AV_GCC_VERSION_AT_LEAST(4, 3)
+# define av_alloc_size(...) __attribute__((alloc_size(__VA_ARGS__)))
+#else
+# define av_alloc_size(...)
+#endif
+
+/**
+ * @}
+ */
+
+/**
+ * @defgroup lavu_mem_funcs Heap Management
+ * Functions responsible for allocating, freeing, and copying memory.
+ *
+ * All memory allocation functions have a built-in upper limit of `INT_MAX`
+ * bytes. This may be changed with av_max_alloc(), although exercise extreme
+ * caution when doing so.
+ *
+ * @{
+ */
+
+/**
+ * Allocate a memory block with alignment suitable for all memory accesses
+ * (including vectors if available on the CPU).
+ *
+ * @param size Size in bytes for the memory block to be allocated
+ * @return Pointer to the allocated block, or `NULL` if the block cannot
+ * be allocated
+ * @see av_mallocz()
+ */
+void* av_malloc(size_t size) av_malloc_attrib av_alloc_size(1);
+
+/**
+ * Allocate a memory block with alignment suitable for all memory accesses
+ * (including vectors if available on the CPU) and zero all the bytes of the
+ * block.
+ *
+ * @param size Size in bytes for the memory block to be allocated
+ * @return Pointer to the allocated block, or `NULL` if it cannot be allocated
+ * @see av_malloc()
+ */
+void* av_mallocz(size_t size) av_malloc_attrib av_alloc_size(1);
+
+/**
+ * Allocate a memory block for an array with av_malloc().
+ *
+ * The allocated memory will have size `size * nmemb` bytes.
+ *
+ * @param nmemb Number of element
+ * @param size Size of a single element
+ * @return Pointer to the allocated block, or `NULL` if the block cannot
+ * be allocated
+ * @see av_malloc()
+ */
+av_alloc_size(1, 2) void* av_malloc_array(size_t nmemb, size_t size);
+
+/**
+ * Allocate a memory block for an array with av_mallocz().
+ *
+ * The allocated memory will have size `size * nmemb` bytes.
+ *
+ * @param nmemb Number of elements
+ * @param size Size of the single element
+ * @return Pointer to the allocated block, or `NULL` if the block cannot
+ * be allocated
+ *
+ * @see av_mallocz()
+ * @see av_malloc_array()
+ */
+void* av_calloc(size_t nmemb, size_t size) av_malloc_attrib av_alloc_size(1, 2);
+
+#if FF_API_AV_MALLOCZ_ARRAY
+/**
+ * @deprecated use av_calloc()
+ */
+attribute_deprecated void* av_mallocz_array(size_t nmemb,
+ size_t size) av_malloc_attrib
+ av_alloc_size(1, 2);
+#endif
+
+/**
+ * Allocate, reallocate, or free a block of memory.
+ *
+ * If `ptr` is `NULL` and `size` > 0, allocate a new block. If `size` is
+ * zero, free the memory block pointed to by `ptr`. Otherwise, expand or
+ * shrink that block of memory according to `size`.
+ *
+ * @param ptr Pointer to a memory block already allocated with
+ * av_realloc() or `NULL`
+ * @param size Size in bytes of the memory block to be allocated or
+ * reallocated
+ *
+ * @return Pointer to a newly-reallocated block or `NULL` if the block
+ * cannot be reallocated or the function is used to free the memory
+ * block
+ *
+ * @warning Unlike av_malloc(), the returned pointer is not guaranteed to be
+ * correctly aligned.
+ * @see av_fast_realloc()
+ * @see av_reallocp()
+ */
+void* av_realloc(void* ptr, size_t size) av_alloc_size(2);
+
+/**
+ * Allocate, reallocate, or free a block of memory through a pointer to a
+ * pointer.
+ *
+ * If `*ptr` is `NULL` and `size` > 0, allocate a new block. If `size` is
+ * zero, free the memory block pointed to by `*ptr`. Otherwise, expand or
+ * shrink that block of memory according to `size`.
+ *
+ * @param[in,out] ptr Pointer to a pointer to a memory block already allocated
+ * with av_realloc(), or a pointer to `NULL`. The pointer
+ * is updated on success, or freed on failure.
+ * @param[in] size Size in bytes for the memory block to be allocated or
+ * reallocated
+ *
+ * @return Zero on success, an AVERROR error code on failure
+ *
+ * @warning Unlike av_malloc(), the allocated memory is not guaranteed to be
+ * correctly aligned.
+ */
+av_warn_unused_result int av_reallocp(void* ptr, size_t size);
+
+/**
+ * Allocate, reallocate, or free a block of memory.
+ *
+ * This function does the same thing as av_realloc(), except:
+ * - It takes two size arguments and allocates `nelem * elsize` bytes,
+ * after checking the result of the multiplication for integer overflow.
+ * - It frees the input block in case of failure, thus avoiding the memory
+ * leak with the classic
+ * @code{.c}
+ * buf = realloc(buf);
+ * if (!buf)
+ * return -1;
+ * @endcode
+ * pattern.
+ */
+void* av_realloc_f(void* ptr, size_t nelem, size_t elsize);
+
+/**
+ * Allocate, reallocate, or free an array.
+ *
+ * If `ptr` is `NULL` and `nmemb` > 0, allocate a new block. If
+ * `nmemb` is zero, free the memory block pointed to by `ptr`.
+ *
+ * @param ptr Pointer to a memory block already allocated with
+ * av_realloc() or `NULL`
+ * @param nmemb Number of elements in the array
+ * @param size Size of the single element of the array
+ *
+ * @return Pointer to a newly-reallocated block or NULL if the block
+ * cannot be reallocated or the function is used to free the memory
+ * block
+ *
+ * @warning Unlike av_malloc(), the allocated memory is not guaranteed to be
+ * correctly aligned.
+ * @see av_reallocp_array()
+ */
+av_alloc_size(2, 3) void* av_realloc_array(void* ptr, size_t nmemb,
+ size_t size);
+
+/**
+ * Allocate, reallocate, or free an array through a pointer to a pointer.
+ *
+ * If `*ptr` is `NULL` and `nmemb` > 0, allocate a new block. If `nmemb` is
+ * zero, free the memory block pointed to by `*ptr`.
+ *
+ * @param[in,out] ptr Pointer to a pointer to a memory block already
+ * allocated with av_realloc(), or a pointer to `NULL`.
+ * The pointer is updated on success, or freed on failure.
+ * @param[in] nmemb Number of elements
+ * @param[in] size Size of the single element
+ *
+ * @return Zero on success, an AVERROR error code on failure
+ *
+ * @warning Unlike av_malloc(), the allocated memory is not guaranteed to be
+ * correctly aligned.
+ */
+int av_reallocp_array(void* ptr, size_t nmemb, size_t size);
+
+/**
+ * Reallocate the given buffer if it is not large enough, otherwise do nothing.
+ *
+ * If the given buffer is `NULL`, then a new uninitialized buffer is allocated.
+ *
+ * If the given buffer is not large enough, and reallocation fails, `NULL` is
+ * returned and `*size` is set to 0, but the original buffer is not changed or
+ * freed.
+ *
+ * A typical use pattern follows:
+ *
+ * @code{.c}
+ * uint8_t *buf = ...;
+ * uint8_t *new_buf = av_fast_realloc(buf, &current_size, size_needed);
+ * if (!new_buf) {
+ * // Allocation failed; clean up original buffer
+ * av_freep(&buf);
+ * return AVERROR(ENOMEM);
+ * }
+ * @endcode
+ *
+ * @param[in,out] ptr Already allocated buffer, or `NULL`
+ * @param[in,out] size Pointer to the size of buffer `ptr`. `*size` is
+ * updated to the new allocated size, in particular 0
+ * in case of failure.
+ * @param[in] min_size Desired minimal size of buffer `ptr`
+ * @return `ptr` if the buffer is large enough, a pointer to newly reallocated
+ * buffer if the buffer was not large enough, or `NULL` in case of
+ * error
+ * @see av_realloc()
+ * @see av_fast_malloc()
+ */
+void* av_fast_realloc(void* ptr, unsigned int* size, size_t min_size);
+
+/**
+ * Allocate a buffer, reusing the given one if large enough.
+ *
+ * Contrary to av_fast_realloc(), the current buffer contents might not be
+ * preserved and on error the old buffer is freed, thus no special handling to
+ * avoid memleaks is necessary.
+ *
+ * `*ptr` is allowed to be `NULL`, in which case allocation always happens if
+ * `size_needed` is greater than 0.
+ *
+ * @code{.c}
+ * uint8_t *buf = ...;
+ * av_fast_malloc(&buf, &current_size, size_needed);
+ * if (!buf) {
+ * // Allocation failed; buf already freed
+ * return AVERROR(ENOMEM);
+ * }
+ * @endcode
+ *
+ * @param[in,out] ptr Pointer to pointer to an already allocated buffer.
+ * `*ptr` will be overwritten with pointer to new
+ * buffer on success or `NULL` on failure
+ * @param[in,out] size Pointer to the size of buffer `*ptr`. `*size` is
+ * updated to the new allocated size, in particular 0
+ * in case of failure.
+ * @param[in] min_size Desired minimal size of buffer `*ptr`
+ * @see av_realloc()
+ * @see av_fast_mallocz()
+ */
+void av_fast_malloc(void* ptr, unsigned int* size, size_t min_size);
+
+/**
+ * Allocate and clear a buffer, reusing the given one if large enough.
+ *
+ * Like av_fast_malloc(), but all newly allocated space is initially cleared.
+ * Reused buffer is not cleared.
+ *
+ * `*ptr` is allowed to be `NULL`, in which case allocation always happens if
+ * `size_needed` is greater than 0.
+ *
+ * @param[in,out] ptr Pointer to pointer to an already allocated buffer.
+ * `*ptr` will be overwritten with pointer to new
+ * buffer on success or `NULL` on failure
+ * @param[in,out] size Pointer to the size of buffer `*ptr`. `*size` is
+ * updated to the new allocated size, in particular 0
+ * in case of failure.
+ * @param[in] min_size Desired minimal size of buffer `*ptr`
+ * @see av_fast_malloc()
+ */
+void av_fast_mallocz(void* ptr, unsigned int* size, size_t min_size);
+
+/**
+ * Free a memory block which has been allocated with a function of av_malloc()
+ * or av_realloc() family.
+ *
+ * @param ptr Pointer to the memory block which should be freed.
+ *
+ * @note `ptr = NULL` is explicitly allowed.
+ * @note It is recommended that you use av_freep() instead, to prevent leaving
+ * behind dangling pointers.
+ * @see av_freep()
+ */
+void av_free(void* ptr);
+
+/**
+ * Free a memory block which has been allocated with a function of av_malloc()
+ * or av_realloc() family, and set the pointer pointing to it to `NULL`.
+ *
+ * @code{.c}
+ * uint8_t *buf = av_malloc(16);
+ * av_free(buf);
+ * // buf now contains a dangling pointer to freed memory, and accidental
+ * // dereference of buf will result in a use-after-free, which may be a
+ * // security risk.
+ *
+ * uint8_t *buf = av_malloc(16);
+ * av_freep(&buf);
+ * // buf is now NULL, and accidental dereference will only result in a
+ * // NULL-pointer dereference.
+ * @endcode
+ *
+ * @param ptr Pointer to the pointer to the memory block which should be freed
+ * @note `*ptr = NULL` is safe and leads to no action.
+ * @see av_free()
+ */
+void av_freep(void* ptr);
+
+/**
+ * Duplicate a string.
+ *
+ * @param s String to be duplicated
+ * @return Pointer to a newly-allocated string containing a
+ * copy of `s` or `NULL` if the string cannot be allocated
+ * @see av_strndup()
+ */
+char* av_strdup(const char* s) av_malloc_attrib;
+
+/**
+ * Duplicate a substring of a string.
+ *
+ * @param s String to be duplicated
+ * @param len Maximum length of the resulting string (not counting the
+ * terminating byte)
+ * @return Pointer to a newly-allocated string containing a
+ * substring of `s` or `NULL` if the string cannot be allocated
+ */
+char* av_strndup(const char* s, size_t len) av_malloc_attrib;
+
+/**
+ * Duplicate a buffer with av_malloc().
+ *
+ * @param p Buffer to be duplicated
+ * @param size Size in bytes of the buffer copied
+ * @return Pointer to a newly allocated buffer containing a
+ * copy of `p` or `NULL` if the buffer cannot be allocated
+ */
+void* av_memdup(const void* p, size_t size);
+
+/**
+ * Overlapping memcpy() implementation.
+ *
+ * @param dst Destination buffer
+ * @param back Number of bytes back to start copying (i.e. the initial size of
+ * the overlapping window); must be > 0
+ * @param cnt Number of bytes to copy; must be >= 0
+ *
+ * @note `cnt > back` is valid, this will copy the bytes we just copied,
+ * thus creating a repeating pattern with a period length of `back`.
+ */
+void av_memcpy_backptr(uint8_t* dst, int back, int cnt);
+
+/**
+ * @}
+ */
+
+/**
+ * @defgroup lavu_mem_dynarray Dynamic Array
+ *
+ * Utilities to make an array grow when needed.
+ *
+ * Sometimes, the programmer would want to have an array that can grow when
+ * needed. The libavutil dynamic array utilities fill that need.
+ *
+ * libavutil supports two systems of appending elements onto a dynamically
+ * allocated array, the first one storing the pointer to the value in the
+ * array, and the second storing the value directly. In both systems, the
+ * caller is responsible for maintaining a variable containing the length of
+ * the array, as well as freeing of the array after use.
+ *
+ * The first system stores pointers to values in a block of dynamically
+ * allocated memory. Since only pointers are stored, the function does not need
+ * to know the size of the type. Both av_dynarray_add() and
+ * av_dynarray_add_nofree() implement this system.
+ *
+ * @code
+ * type **array = NULL; //< an array of pointers to values
+ * int nb = 0; //< a variable to keep track of the length of the array
+ *
+ * type to_be_added = ...;
+ * type to_be_added2 = ...;
+ *
+ * av_dynarray_add(&array, &nb, &to_be_added);
+ * if (nb == 0)
+ * return AVERROR(ENOMEM);
+ *
+ * av_dynarray_add(&array, &nb, &to_be_added2);
+ * if (nb == 0)
+ * return AVERROR(ENOMEM);
+ *
+ * // Now:
+ * // nb == 2
+ * // &to_be_added == array[0]
+ * // &to_be_added2 == array[1]
+ *
+ * av_freep(&array);
+ * @endcode
+ *
+ * The second system stores the value directly in a block of memory. As a
+ * result, the function has to know the size of the type. av_dynarray2_add()
+ * implements this mechanism.
+ *
+ * @code
+ * type *array = NULL; //< an array of values
+ * int nb = 0; //< a variable to keep track of the length of the array
+ *
+ * type to_be_added = ...;
+ * type to_be_added2 = ...;
+ *
+ * type *addr = av_dynarray2_add((void **)&array, &nb, sizeof(*array), NULL);
+ * if (!addr)
+ * return AVERROR(ENOMEM);
+ * memcpy(addr, &to_be_added, sizeof(to_be_added));
+ *
+ * // Shortcut of the above.
+ * type *addr = av_dynarray2_add((void **)&array, &nb, sizeof(*array),
+ * (const void *)&to_be_added2);
+ * if (!addr)
+ * return AVERROR(ENOMEM);
+ *
+ * // Now:
+ * // nb == 2
+ * // to_be_added == array[0]
+ * // to_be_added2 == array[1]
+ *
+ * av_freep(&array);
+ * @endcode
+ *
+ * @{
+ */
+
+/**
+ * Add the pointer to an element to a dynamic array.
+ *
+ * The array to grow is supposed to be an array of pointers to
+ * structures, and the element to add must be a pointer to an already
+ * allocated structure.
+ *
+ * The array is reallocated when its size reaches powers of 2.
+ * Therefore, the amortized cost of adding an element is constant.
+ *
+ * In case of success, the pointer to the array is updated in order to
+ * point to the new grown array, and the number pointed to by `nb_ptr`
+ * is incremented.
+ * In case of failure, the array is freed, `*tab_ptr` is set to `NULL` and
+ * `*nb_ptr` is set to 0.
+ *
+ * @param[in,out] tab_ptr Pointer to the array to grow
+ * @param[in,out] nb_ptr Pointer to the number of elements in the array
+ * @param[in] elem Element to add
+ * @see av_dynarray_add_nofree(), av_dynarray2_add()
+ */
+void av_dynarray_add(void* tab_ptr, int* nb_ptr, void* elem);
+
+/**
+ * Add an element to a dynamic array.
+ *
+ * Function has the same functionality as av_dynarray_add(),
+ * but it doesn't free memory on fails. It returns error code
+ * instead and leave current buffer untouched.
+ *
+ * @return >=0 on success, negative otherwise
+ * @see av_dynarray_add(), av_dynarray2_add()
+ */
+av_warn_unused_result int av_dynarray_add_nofree(void* tab_ptr, int* nb_ptr,
+ void* elem);
+
+/**
+ * Add an element of size `elem_size` to a dynamic array.
+ *
+ * The array is reallocated when its number of elements reaches powers of 2.
+ * Therefore, the amortized cost of adding an element is constant.
+ *
+ * In case of success, the pointer to the array is updated in order to
+ * point to the new grown array, and the number pointed to by `nb_ptr`
+ * is incremented.
+ * In case of failure, the array is freed, `*tab_ptr` is set to `NULL` and
+ * `*nb_ptr` is set to 0.
+ *
+ * @param[in,out] tab_ptr Pointer to the array to grow
+ * @param[in,out] nb_ptr Pointer to the number of elements in the array
+ * @param[in] elem_size Size in bytes of an element in the array
+ * @param[in] elem_data Pointer to the data of the element to add. If
+ * `NULL`, the space of the newly added element is
+ * allocated but left uninitialized.
+ *
+ * @return Pointer to the data of the element to copy in the newly allocated
+ * space
+ * @see av_dynarray_add(), av_dynarray_add_nofree()
+ */
+void* av_dynarray2_add(void** tab_ptr, int* nb_ptr, size_t elem_size,
+ const uint8_t* elem_data);
+
+/**
+ * @}
+ */
+
+/**
+ * @defgroup lavu_mem_misc Miscellaneous Functions
+ *
+ * Other functions related to memory allocation.
+ *
+ * @{
+ */
+
+/**
+ * Multiply two `size_t` values checking for overflow.
+ *
+ * @param[in] a,b Operands of multiplication
+ * @param[out] r Pointer to the result of the operation
+ * @return 0 on success, AVERROR(EINVAL) on overflow
+ */
+int av_size_mult(size_t a, size_t b, size_t* r);
+
+/**
+ * Set the maximum size that may be allocated in one block.
+ *
+ * The value specified with this function is effective for all libavutil's @ref
+ * lavu_mem_funcs "heap management functions."
+ *
+ * By default, the max value is defined as `INT_MAX`.
+ *
+ * @param max Value to be set as the new maximum size
+ *
+ * @warning Exercise extreme caution when using this function. Don't touch
+ * this if you do not understand the full consequence of doing so.
+ */
+void av_max_alloc(size_t max);
+
+/**
+ * @}
+ * @}
+ */
+
+#endif /* AVUTIL_MEM_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/pixfmt.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/pixfmt.h
new file mode 100644
index 0000000000..c94fc9fdae
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/pixfmt.h
@@ -0,0 +1,808 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_PIXFMT_H
+#define AVUTIL_PIXFMT_H
+
+/**
+ * @file
+ * pixel format definitions
+ */
+
+#include "libavutil/avconfig.h"
+#include "version.h"
+
+#define AVPALETTE_SIZE 1024
+#define AVPALETTE_COUNT 256
+
+/**
+ * Pixel format.
+ *
+ * @note
+ * AV_PIX_FMT_RGB32 is handled in an endian-specific manner. An RGBA
+ * color is put together as:
+ * (A << 24) | (R << 16) | (G << 8) | B
+ * This is stored as BGRA on little-endian CPU architectures and ARGB on
+ * big-endian CPUs.
+ *
+ * @note
+ * If the resolution is not a multiple of the chroma subsampling factor
+ * then the chroma plane resolution must be rounded up.
+ *
+ * @par
+ * When the pixel format is palettized RGB32 (AV_PIX_FMT_PAL8), the palettized
+ * image data is stored in AVFrame.data[0]. The palette is transported in
+ * AVFrame.data[1], is 1024 bytes long (256 4-byte entries) and is
+ * formatted the same as in AV_PIX_FMT_RGB32 described above (i.e., it is
+ * also endian-specific). Note also that the individual RGB32 palette
+ * components stored in AVFrame.data[1] should be in the range 0..255.
+ * This is important as many custom PAL8 video codecs that were designed
+ * to run on the IBM VGA graphics adapter use 6-bit palette components.
+ *
+ * @par
+ * For all the 8 bits per pixel formats, an RGB32 palette is in data[1] like
+ * for pal8. This palette is filled in automatically by the function
+ * allocating the picture.
+ */
+enum AVPixelFormat {
+ AV_PIX_FMT_NONE = -1,
+ AV_PIX_FMT_YUV420P, ///< planar YUV 4:2:0, 12bpp, (1 Cr & Cb sample per 2x2 Y
+ ///< samples)
+ AV_PIX_FMT_YUYV422, ///< packed YUV 4:2:2, 16bpp, Y0 Cb Y1 Cr
+ AV_PIX_FMT_RGB24, ///< packed RGB 8:8:8, 24bpp, RGBRGB...
+ AV_PIX_FMT_BGR24, ///< packed RGB 8:8:8, 24bpp, BGRBGR...
+ AV_PIX_FMT_YUV422P, ///< planar YUV 4:2:2, 16bpp, (1 Cr & Cb sample per 2x1 Y
+ ///< samples)
+ AV_PIX_FMT_YUV444P, ///< planar YUV 4:4:4, 24bpp, (1 Cr & Cb sample per 1x1 Y
+ ///< samples)
+ AV_PIX_FMT_YUV410P, ///< planar YUV 4:1:0, 9bpp, (1 Cr & Cb sample per 4x4 Y
+ ///< samples)
+ AV_PIX_FMT_YUV411P, ///< planar YUV 4:1:1, 12bpp, (1 Cr & Cb sample per 4x1 Y
+ ///< samples)
+ AV_PIX_FMT_GRAY8, ///< Y , 8bpp
+ AV_PIX_FMT_MONOWHITE, ///< Y , 1bpp, 0 is white, 1 is black,
+ ///< in each byte pixels are ordered from the
+ ///< msb to the lsb
+ AV_PIX_FMT_MONOBLACK, ///< Y , 1bpp, 0 is black, 1 is white,
+ ///< in each byte pixels are ordered from the
+ ///< msb to the lsb
+ AV_PIX_FMT_PAL8, ///< 8 bits with AV_PIX_FMT_RGB32 palette
+ AV_PIX_FMT_YUVJ420P, ///< planar YUV 4:2:0, 12bpp, full scale (JPEG),
+ ///< deprecated in favor of AV_PIX_FMT_YUV420P and
+ ///< setting color_range
+ AV_PIX_FMT_YUVJ422P, ///< planar YUV 4:2:2, 16bpp, full scale (JPEG),
+ ///< deprecated in favor of AV_PIX_FMT_YUV422P and
+ ///< setting color_range
+ AV_PIX_FMT_YUVJ444P, ///< planar YUV 4:4:4, 24bpp, full scale (JPEG),
+ ///< deprecated in favor of AV_PIX_FMT_YUV444P and
+ ///< setting color_range
+ AV_PIX_FMT_UYVY422, ///< packed YUV 4:2:2, 16bpp, Cb Y0 Cr Y1
+ AV_PIX_FMT_UYYVYY411, ///< packed YUV 4:1:1, 12bpp, Cb Y0 Y1 Cr Y2 Y3
+ AV_PIX_FMT_BGR8, ///< packed RGB 3:3:2, 8bpp, (msb)2B 3G 3R(lsb)
+ AV_PIX_FMT_BGR4, ///< packed RGB 1:2:1 bitstream, 4bpp, (msb)1B 2G 1R(lsb),
+ ///< a byte contains two pixels, the first pixel in the byte
+ ///< is the one composed by the 4 msb bits
+ AV_PIX_FMT_BGR4_BYTE, ///< packed RGB 1:2:1, 8bpp, (msb)1B 2G 1R(lsb)
+ AV_PIX_FMT_RGB8, ///< packed RGB 3:3:2, 8bpp, (msb)2R 3G 3B(lsb)
+ AV_PIX_FMT_RGB4, ///< packed RGB 1:2:1 bitstream, 4bpp, (msb)1R 2G 1B(lsb),
+ ///< a byte contains two pixels, the first pixel in the byte
+ ///< is the one composed by the 4 msb bits
+ AV_PIX_FMT_RGB4_BYTE, ///< packed RGB 1:2:1, 8bpp, (msb)1R 2G 1B(lsb)
+ AV_PIX_FMT_NV12, ///< planar YUV 4:2:0, 12bpp, 1 plane for Y and 1 plane for
+ ///< the UV components, which are interleaved (first byte U
+ ///< and the following byte V)
+ AV_PIX_FMT_NV21, ///< as above, but U and V bytes are swapped
+
+ AV_PIX_FMT_ARGB, ///< packed ARGB 8:8:8:8, 32bpp, ARGBARGB...
+ AV_PIX_FMT_RGBA, ///< packed RGBA 8:8:8:8, 32bpp, RGBARGBA...
+ AV_PIX_FMT_ABGR, ///< packed ABGR 8:8:8:8, 32bpp, ABGRABGR...
+ AV_PIX_FMT_BGRA, ///< packed BGRA 8:8:8:8, 32bpp, BGRABGRA...
+
+ AV_PIX_FMT_GRAY16BE, ///< Y , 16bpp, big-endian
+ AV_PIX_FMT_GRAY16LE, ///< Y , 16bpp, little-endian
+ AV_PIX_FMT_YUV440P, ///< planar YUV 4:4:0 (1 Cr & Cb sample per 1x2 Y
+ ///< samples)
+ AV_PIX_FMT_YUVJ440P, ///< planar YUV 4:4:0 full scale (JPEG), deprecated in
+ ///< favor of AV_PIX_FMT_YUV440P and setting color_range
+ AV_PIX_FMT_YUVA420P, ///< planar YUV 4:2:0, 20bpp, (1 Cr & Cb sample per 2x2
+ ///< Y & A samples)
+ AV_PIX_FMT_RGB48BE, ///< packed RGB 16:16:16, 48bpp, 16R, 16G, 16B, the
+ ///< 2-byte value for each R/G/B component is stored as
+ ///< big-endian
+ AV_PIX_FMT_RGB48LE, ///< packed RGB 16:16:16, 48bpp, 16R, 16G, 16B, the
+ ///< 2-byte value for each R/G/B component is stored as
+ ///< little-endian
+
+ AV_PIX_FMT_RGB565BE, ///< packed RGB 5:6:5, 16bpp, (msb) 5R 6G 5B(lsb),
+ ///< big-endian
+ AV_PIX_FMT_RGB565LE, ///< packed RGB 5:6:5, 16bpp, (msb) 5R 6G 5B(lsb),
+ ///< little-endian
+ AV_PIX_FMT_RGB555BE, ///< packed RGB 5:5:5, 16bpp, (msb)1X 5R 5G 5B(lsb),
+ ///< big-endian , X=unused/undefined
+ AV_PIX_FMT_RGB555LE, ///< packed RGB 5:5:5, 16bpp, (msb)1X 5R 5G 5B(lsb),
+ ///< little-endian, X=unused/undefined
+
+ AV_PIX_FMT_BGR565BE, ///< packed BGR 5:6:5, 16bpp, (msb) 5B 6G 5R(lsb),
+ ///< big-endian
+ AV_PIX_FMT_BGR565LE, ///< packed BGR 5:6:5, 16bpp, (msb) 5B 6G 5R(lsb),
+ ///< little-endian
+ AV_PIX_FMT_BGR555BE, ///< packed BGR 5:5:5, 16bpp, (msb)1X 5B 5G 5R(lsb),
+ ///< big-endian , X=unused/undefined
+ AV_PIX_FMT_BGR555LE, ///< packed BGR 5:5:5, 16bpp, (msb)1X 5B 5G 5R(lsb),
+ ///< little-endian, X=unused/undefined
+
+ /**
+ * Hardware acceleration through VA-API, data[3] contains a
+ * VASurfaceID.
+ */
+ AV_PIX_FMT_VAAPI,
+
+ AV_PIX_FMT_YUV420P16LE, ///< planar YUV 4:2:0, 24bpp, (1 Cr & Cb sample per
+ ///< 2x2 Y samples), little-endian
+ AV_PIX_FMT_YUV420P16BE, ///< planar YUV 4:2:0, 24bpp, (1 Cr & Cb sample per
+ ///< 2x2 Y samples), big-endian
+ AV_PIX_FMT_YUV422P16LE, ///< planar YUV 4:2:2, 32bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y samples), little-endian
+ AV_PIX_FMT_YUV422P16BE, ///< planar YUV 4:2:2, 32bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y samples), big-endian
+ AV_PIX_FMT_YUV444P16LE, ///< planar YUV 4:4:4, 48bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y samples), little-endian
+ AV_PIX_FMT_YUV444P16BE, ///< planar YUV 4:4:4, 48bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y samples), big-endian
+ AV_PIX_FMT_DXVA2_VLD, ///< HW decoding through DXVA2, Picture.data[3]
+ ///< contains a LPDIRECT3DSURFACE9 pointer
+
+ AV_PIX_FMT_RGB444LE, ///< packed RGB 4:4:4, 16bpp, (msb)4X 4R 4G 4B(lsb),
+ ///< little-endian, X=unused/undefined
+ AV_PIX_FMT_RGB444BE, ///< packed RGB 4:4:4, 16bpp, (msb)4X 4R 4G 4B(lsb),
+ ///< big-endian, X=unused/undefined
+ AV_PIX_FMT_BGR444LE, ///< packed BGR 4:4:4, 16bpp, (msb)4X 4B 4G 4R(lsb),
+ ///< little-endian, X=unused/undefined
+ AV_PIX_FMT_BGR444BE, ///< packed BGR 4:4:4, 16bpp, (msb)4X 4B 4G 4R(lsb),
+ ///< big-endian, X=unused/undefined
+ AV_PIX_FMT_YA8, ///< 8 bits gray, 8 bits alpha
+
+ AV_PIX_FMT_Y400A = AV_PIX_FMT_YA8, ///< alias for AV_PIX_FMT_YA8
+ AV_PIX_FMT_GRAY8A = AV_PIX_FMT_YA8, ///< alias for AV_PIX_FMT_YA8
+
+ AV_PIX_FMT_BGR48BE, ///< packed RGB 16:16:16, 48bpp, 16B, 16G, 16R, the
+ ///< 2-byte value for each R/G/B component is stored as
+ ///< big-endian
+ AV_PIX_FMT_BGR48LE, ///< packed RGB 16:16:16, 48bpp, 16B, 16G, 16R, the
+ ///< 2-byte value for each R/G/B component is stored as
+ ///< little-endian
+
+ /**
+ * The following 12 formats have the disadvantage of needing 1 format for each
+ * bit depth. Notice that each 9/10 bits sample is stored in 16 bits with
+ * extra padding. If you want to support multiple bit depths, then using
+ * AV_PIX_FMT_YUV420P16* with the bpp stored separately is better.
+ */
+ AV_PIX_FMT_YUV420P9BE, ///< planar YUV 4:2:0, 13.5bpp, (1 Cr & Cb sample per
+ ///< 2x2 Y samples), big-endian
+ AV_PIX_FMT_YUV420P9LE, ///< planar YUV 4:2:0, 13.5bpp, (1 Cr & Cb sample per
+ ///< 2x2 Y samples), little-endian
+ AV_PIX_FMT_YUV420P10BE, ///< planar YUV 4:2:0, 15bpp, (1 Cr & Cb sample per
+ ///< 2x2 Y samples), big-endian
+ AV_PIX_FMT_YUV420P10LE, ///< planar YUV 4:2:0, 15bpp, (1 Cr & Cb sample per
+ ///< 2x2 Y samples), little-endian
+ AV_PIX_FMT_YUV422P10BE, ///< planar YUV 4:2:2, 20bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y samples), big-endian
+ AV_PIX_FMT_YUV422P10LE, ///< planar YUV 4:2:2, 20bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y samples), little-endian
+ AV_PIX_FMT_YUV444P9BE, ///< planar YUV 4:4:4, 27bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y samples), big-endian
+ AV_PIX_FMT_YUV444P9LE, ///< planar YUV 4:4:4, 27bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y samples), little-endian
+ AV_PIX_FMT_YUV444P10BE, ///< planar YUV 4:4:4, 30bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y samples), big-endian
+ AV_PIX_FMT_YUV444P10LE, ///< planar YUV 4:4:4, 30bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y samples), little-endian
+ AV_PIX_FMT_YUV422P9BE, ///< planar YUV 4:2:2, 18bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y samples), big-endian
+ AV_PIX_FMT_YUV422P9LE, ///< planar YUV 4:2:2, 18bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y samples), little-endian
+ AV_PIX_FMT_GBRP, ///< planar GBR 4:4:4 24bpp
+ AV_PIX_FMT_GBR24P = AV_PIX_FMT_GBRP, // alias for #AV_PIX_FMT_GBRP
+ AV_PIX_FMT_GBRP9BE, ///< planar GBR 4:4:4 27bpp, big-endian
+ AV_PIX_FMT_GBRP9LE, ///< planar GBR 4:4:4 27bpp, little-endian
+ AV_PIX_FMT_GBRP10BE, ///< planar GBR 4:4:4 30bpp, big-endian
+ AV_PIX_FMT_GBRP10LE, ///< planar GBR 4:4:4 30bpp, little-endian
+ AV_PIX_FMT_GBRP16BE, ///< planar GBR 4:4:4 48bpp, big-endian
+ AV_PIX_FMT_GBRP16LE, ///< planar GBR 4:4:4 48bpp, little-endian
+ AV_PIX_FMT_YUVA422P, ///< planar YUV 4:2:2 24bpp, (1 Cr & Cb sample per 2x1 Y
+ ///< & A samples)
+ AV_PIX_FMT_YUVA444P, ///< planar YUV 4:4:4 32bpp, (1 Cr & Cb sample per 1x1 Y
+ ///< & A samples)
+ AV_PIX_FMT_YUVA420P9BE, ///< planar YUV 4:2:0 22.5bpp, (1 Cr & Cb sample per
+ ///< 2x2 Y & A samples), big-endian
+ AV_PIX_FMT_YUVA420P9LE, ///< planar YUV 4:2:0 22.5bpp, (1 Cr & Cb sample per
+ ///< 2x2 Y & A samples), little-endian
+ AV_PIX_FMT_YUVA422P9BE, ///< planar YUV 4:2:2 27bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y & A samples), big-endian
+ AV_PIX_FMT_YUVA422P9LE, ///< planar YUV 4:2:2 27bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y & A samples), little-endian
+ AV_PIX_FMT_YUVA444P9BE, ///< planar YUV 4:4:4 36bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y & A samples), big-endian
+ AV_PIX_FMT_YUVA444P9LE, ///< planar YUV 4:4:4 36bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y & A samples), little-endian
+ AV_PIX_FMT_YUVA420P10BE, ///< planar YUV 4:2:0 25bpp, (1 Cr & Cb sample per
+ ///< 2x2 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA420P10LE, ///< planar YUV 4:2:0 25bpp, (1 Cr & Cb sample per
+ ///< 2x2 Y & A samples, little-endian)
+ AV_PIX_FMT_YUVA422P10BE, ///< planar YUV 4:2:2 30bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA422P10LE, ///< planar YUV 4:2:2 30bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y & A samples, little-endian)
+ AV_PIX_FMT_YUVA444P10BE, ///< planar YUV 4:4:4 40bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA444P10LE, ///< planar YUV 4:4:4 40bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y & A samples, little-endian)
+ AV_PIX_FMT_YUVA420P16BE, ///< planar YUV 4:2:0 40bpp, (1 Cr & Cb sample per
+ ///< 2x2 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA420P16LE, ///< planar YUV 4:2:0 40bpp, (1 Cr & Cb sample per
+ ///< 2x2 Y & A samples, little-endian)
+ AV_PIX_FMT_YUVA422P16BE, ///< planar YUV 4:2:2 48bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA422P16LE, ///< planar YUV 4:2:2 48bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y & A samples, little-endian)
+ AV_PIX_FMT_YUVA444P16BE, ///< planar YUV 4:4:4 64bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA444P16LE, ///< planar YUV 4:4:4 64bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y & A samples, little-endian)
+
+ AV_PIX_FMT_VDPAU, ///< HW acceleration through VDPAU, Picture.data[3]
+ ///< contains a VdpVideoSurface
+
+ AV_PIX_FMT_XYZ12LE, ///< packed XYZ 4:4:4, 36 bpp, (msb) 12X, 12Y, 12Z (lsb),
+ ///< the 2-byte value for each X/Y/Z is stored as
+ ///< little-endian, the 4 lower bits are set to 0
+ AV_PIX_FMT_XYZ12BE, ///< packed XYZ 4:4:4, 36 bpp, (msb) 12X, 12Y, 12Z (lsb),
+ ///< the 2-byte value for each X/Y/Z is stored as
+ ///< big-endian, the 4 lower bits are set to 0
+ AV_PIX_FMT_NV16, ///< interleaved chroma YUV 4:2:2, 16bpp, (1 Cr & Cb sample
+ ///< per 2x1 Y samples)
+ AV_PIX_FMT_NV20LE, ///< interleaved chroma YUV 4:2:2, 20bpp, (1 Cr & Cb
+ ///< sample per 2x1 Y samples), little-endian
+ AV_PIX_FMT_NV20BE, ///< interleaved chroma YUV 4:2:2, 20bpp, (1 Cr & Cb
+ ///< sample per 2x1 Y samples), big-endian
+
+ AV_PIX_FMT_RGBA64BE, ///< packed RGBA 16:16:16:16, 64bpp, 16R, 16G, 16B, 16A,
+ ///< the 2-byte value for each R/G/B/A component is
+ ///< stored as big-endian
+ AV_PIX_FMT_RGBA64LE, ///< packed RGBA 16:16:16:16, 64bpp, 16R, 16G, 16B, 16A,
+ ///< the 2-byte value for each R/G/B/A component is
+ ///< stored as little-endian
+ AV_PIX_FMT_BGRA64BE, ///< packed RGBA 16:16:16:16, 64bpp, 16B, 16G, 16R, 16A,
+ ///< the 2-byte value for each R/G/B/A component is
+ ///< stored as big-endian
+ AV_PIX_FMT_BGRA64LE, ///< packed RGBA 16:16:16:16, 64bpp, 16B, 16G, 16R, 16A,
+ ///< the 2-byte value for each R/G/B/A component is
+ ///< stored as little-endian
+
+ AV_PIX_FMT_YVYU422, ///< packed YUV 4:2:2, 16bpp, Y0 Cr Y1 Cb
+
+ AV_PIX_FMT_YA16BE, ///< 16 bits gray, 16 bits alpha (big-endian)
+ AV_PIX_FMT_YA16LE, ///< 16 bits gray, 16 bits alpha (little-endian)
+
+ AV_PIX_FMT_GBRAP, ///< planar GBRA 4:4:4:4 32bpp
+ AV_PIX_FMT_GBRAP16BE, ///< planar GBRA 4:4:4:4 64bpp, big-endian
+ AV_PIX_FMT_GBRAP16LE, ///< planar GBRA 4:4:4:4 64bpp, little-endian
+ /**
+ * HW acceleration through QSV, data[3] contains a pointer to the
+ * mfxFrameSurface1 structure.
+ */
+ AV_PIX_FMT_QSV,
+ /**
+ * HW acceleration though MMAL, data[3] contains a pointer to the
+ * MMAL_BUFFER_HEADER_T structure.
+ */
+ AV_PIX_FMT_MMAL,
+
+ AV_PIX_FMT_D3D11VA_VLD, ///< HW decoding through Direct3D11 via old API,
+ ///< Picture.data[3] contains a
+ ///< ID3D11VideoDecoderOutputView pointer
+
+ /**
+ * HW acceleration through CUDA. data[i] contain CUdeviceptr pointers
+ * exactly as for system memory frames.
+ */
+ AV_PIX_FMT_CUDA,
+
+ AV_PIX_FMT_0RGB, ///< packed RGB 8:8:8, 32bpp, XRGBXRGB... X=unused/undefined
+ AV_PIX_FMT_RGB0, ///< packed RGB 8:8:8, 32bpp, RGBXRGBX... X=unused/undefined
+ AV_PIX_FMT_0BGR, ///< packed BGR 8:8:8, 32bpp, XBGRXBGR... X=unused/undefined
+ AV_PIX_FMT_BGR0, ///< packed BGR 8:8:8, 32bpp, BGRXBGRX... X=unused/undefined
+
+ AV_PIX_FMT_YUV420P12BE, ///< planar YUV 4:2:0,18bpp, (1 Cr & Cb sample per
+ ///< 2x2 Y samples), big-endian
+ AV_PIX_FMT_YUV420P12LE, ///< planar YUV 4:2:0,18bpp, (1 Cr & Cb sample per
+ ///< 2x2 Y samples), little-endian
+ AV_PIX_FMT_YUV420P14BE, ///< planar YUV 4:2:0,21bpp, (1 Cr & Cb sample per
+ ///< 2x2 Y samples), big-endian
+ AV_PIX_FMT_YUV420P14LE, ///< planar YUV 4:2:0,21bpp, (1 Cr & Cb sample per
+ ///< 2x2 Y samples), little-endian
+ AV_PIX_FMT_YUV422P12BE, ///< planar YUV 4:2:2,24bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y samples), big-endian
+ AV_PIX_FMT_YUV422P12LE, ///< planar YUV 4:2:2,24bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y samples), little-endian
+ AV_PIX_FMT_YUV422P14BE, ///< planar YUV 4:2:2,28bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y samples), big-endian
+ AV_PIX_FMT_YUV422P14LE, ///< planar YUV 4:2:2,28bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y samples), little-endian
+ AV_PIX_FMT_YUV444P12BE, ///< planar YUV 4:4:4,36bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y samples), big-endian
+ AV_PIX_FMT_YUV444P12LE, ///< planar YUV 4:4:4,36bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y samples), little-endian
+ AV_PIX_FMT_YUV444P14BE, ///< planar YUV 4:4:4,42bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y samples), big-endian
+ AV_PIX_FMT_YUV444P14LE, ///< planar YUV 4:4:4,42bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y samples), little-endian
+ AV_PIX_FMT_GBRP12BE, ///< planar GBR 4:4:4 36bpp, big-endian
+ AV_PIX_FMT_GBRP12LE, ///< planar GBR 4:4:4 36bpp, little-endian
+ AV_PIX_FMT_GBRP14BE, ///< planar GBR 4:4:4 42bpp, big-endian
+ AV_PIX_FMT_GBRP14LE, ///< planar GBR 4:4:4 42bpp, little-endian
+ AV_PIX_FMT_YUVJ411P, ///< planar YUV 4:1:1, 12bpp, (1 Cr & Cb sample per 4x1
+ ///< Y samples) full scale (JPEG), deprecated in favor
+ ///< of AV_PIX_FMT_YUV411P and setting color_range
+
+ AV_PIX_FMT_BAYER_BGGR8, ///< bayer, BGBG..(odd line), GRGR..(even line),
+ ///< 8-bit samples
+ AV_PIX_FMT_BAYER_RGGB8, ///< bayer, RGRG..(odd line), GBGB..(even line),
+ ///< 8-bit samples
+ AV_PIX_FMT_BAYER_GBRG8, ///< bayer, GBGB..(odd line), RGRG..(even line),
+ ///< 8-bit samples
+ AV_PIX_FMT_BAYER_GRBG8, ///< bayer, GRGR..(odd line), BGBG..(even line),
+ ///< 8-bit samples
+ AV_PIX_FMT_BAYER_BGGR16LE, ///< bayer, BGBG..(odd line), GRGR..(even line),
+ ///< 16-bit samples, little-endian
+ AV_PIX_FMT_BAYER_BGGR16BE, ///< bayer, BGBG..(odd line), GRGR..(even line),
+ ///< 16-bit samples, big-endian
+ AV_PIX_FMT_BAYER_RGGB16LE, ///< bayer, RGRG..(odd line), GBGB..(even line),
+ ///< 16-bit samples, little-endian
+ AV_PIX_FMT_BAYER_RGGB16BE, ///< bayer, RGRG..(odd line), GBGB..(even line),
+ ///< 16-bit samples, big-endian
+ AV_PIX_FMT_BAYER_GBRG16LE, ///< bayer, GBGB..(odd line), RGRG..(even line),
+ ///< 16-bit samples, little-endian
+ AV_PIX_FMT_BAYER_GBRG16BE, ///< bayer, GBGB..(odd line), RGRG..(even line),
+ ///< 16-bit samples, big-endian
+ AV_PIX_FMT_BAYER_GRBG16LE, ///< bayer, GRGR..(odd line), BGBG..(even line),
+ ///< 16-bit samples, little-endian
+ AV_PIX_FMT_BAYER_GRBG16BE, ///< bayer, GRGR..(odd line), BGBG..(even line),
+ ///< 16-bit samples, big-endian
+
+ AV_PIX_FMT_XVMC, ///< XVideo Motion Acceleration via common packet passing
+
+ AV_PIX_FMT_YUV440P10LE, ///< planar YUV 4:4:0,20bpp, (1 Cr & Cb sample per
+ ///< 1x2 Y samples), little-endian
+ AV_PIX_FMT_YUV440P10BE, ///< planar YUV 4:4:0,20bpp, (1 Cr & Cb sample per
+ ///< 1x2 Y samples), big-endian
+ AV_PIX_FMT_YUV440P12LE, ///< planar YUV 4:4:0,24bpp, (1 Cr & Cb sample per
+ ///< 1x2 Y samples), little-endian
+ AV_PIX_FMT_YUV440P12BE, ///< planar YUV 4:4:0,24bpp, (1 Cr & Cb sample per
+ ///< 1x2 Y samples), big-endian
+ AV_PIX_FMT_AYUV64LE, ///< packed AYUV 4:4:4,64bpp (1 Cr & Cb sample per 1x1 Y
+ ///< & A samples), little-endian
+ AV_PIX_FMT_AYUV64BE, ///< packed AYUV 4:4:4,64bpp (1 Cr & Cb sample per 1x1 Y
+ ///< & A samples), big-endian
+
+ AV_PIX_FMT_VIDEOTOOLBOX, ///< hardware decoding through Videotoolbox
+
+ AV_PIX_FMT_P010LE, ///< like NV12, with 10bpp per component, data in the high
+ ///< bits, zeros in the low bits, little-endian
+ AV_PIX_FMT_P010BE, ///< like NV12, with 10bpp per component, data in the high
+ ///< bits, zeros in the low bits, big-endian
+
+ AV_PIX_FMT_GBRAP12BE, ///< planar GBR 4:4:4:4 48bpp, big-endian
+ AV_PIX_FMT_GBRAP12LE, ///< planar GBR 4:4:4:4 48bpp, little-endian
+
+ AV_PIX_FMT_GBRAP10BE, ///< planar GBR 4:4:4:4 40bpp, big-endian
+ AV_PIX_FMT_GBRAP10LE, ///< planar GBR 4:4:4:4 40bpp, little-endian
+
+ AV_PIX_FMT_MEDIACODEC, ///< hardware decoding through MediaCodec
+
+ AV_PIX_FMT_GRAY12BE, ///< Y , 12bpp, big-endian
+ AV_PIX_FMT_GRAY12LE, ///< Y , 12bpp, little-endian
+ AV_PIX_FMT_GRAY10BE, ///< Y , 10bpp, big-endian
+ AV_PIX_FMT_GRAY10LE, ///< Y , 10bpp, little-endian
+
+ AV_PIX_FMT_P016LE, ///< like NV12, with 16bpp per component, little-endian
+ AV_PIX_FMT_P016BE, ///< like NV12, with 16bpp per component, big-endian
+
+ /**
+ * Hardware surfaces for Direct3D11.
+ *
+ * This is preferred over the legacy AV_PIX_FMT_D3D11VA_VLD. The new D3D11
+ * hwaccel API and filtering support AV_PIX_FMT_D3D11 only.
+ *
+ * data[0] contains a ID3D11Texture2D pointer, and data[1] contains the
+ * texture array index of the frame as intptr_t if the ID3D11Texture2D is
+ * an array texture (or always 0 if it's a normal texture).
+ */
+ AV_PIX_FMT_D3D11,
+
+ AV_PIX_FMT_GRAY9BE, ///< Y , 9bpp, big-endian
+ AV_PIX_FMT_GRAY9LE, ///< Y , 9bpp, little-endian
+
+ AV_PIX_FMT_GBRPF32BE, ///< IEEE-754 single precision planar GBR 4:4:4, 96bpp,
+ ///< big-endian
+ AV_PIX_FMT_GBRPF32LE, ///< IEEE-754 single precision planar GBR 4:4:4, 96bpp,
+ ///< little-endian
+ AV_PIX_FMT_GBRAPF32BE, ///< IEEE-754 single precision planar GBRA 4:4:4:4,
+ ///< 128bpp, big-endian
+ AV_PIX_FMT_GBRAPF32LE, ///< IEEE-754 single precision planar GBRA 4:4:4:4,
+ ///< 128bpp, little-endian
+
+ /**
+ * DRM-managed buffers exposed through PRIME buffer sharing.
+ *
+ * data[0] points to an AVDRMFrameDescriptor.
+ */
+ AV_PIX_FMT_DRM_PRIME,
+ /**
+ * Hardware surfaces for OpenCL.
+ *
+ * data[i] contain 2D image objects (typed in C as cl_mem, used
+ * in OpenCL as image2d_t) for each plane of the surface.
+ */
+ AV_PIX_FMT_OPENCL,
+
+ AV_PIX_FMT_GRAY14BE, ///< Y , 14bpp, big-endian
+ AV_PIX_FMT_GRAY14LE, ///< Y , 14bpp, little-endian
+
+ AV_PIX_FMT_GRAYF32BE, ///< IEEE-754 single precision Y, 32bpp, big-endian
+ AV_PIX_FMT_GRAYF32LE, ///< IEEE-754 single precision Y, 32bpp, little-endian
+
+ AV_PIX_FMT_YUVA422P12BE, ///< planar YUV 4:2:2,24bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y samples), 12b alpha, big-endian
+ AV_PIX_FMT_YUVA422P12LE, ///< planar YUV 4:2:2,24bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y samples), 12b alpha, little-endian
+ AV_PIX_FMT_YUVA444P12BE, ///< planar YUV 4:4:4,36bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y samples), 12b alpha, big-endian
+ AV_PIX_FMT_YUVA444P12LE, ///< planar YUV 4:4:4,36bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y samples), 12b alpha, little-endian
+
+ AV_PIX_FMT_NV24, ///< planar YUV 4:4:4, 24bpp, 1 plane for Y and 1 plane for
+ ///< the UV components, which are interleaved (first byte U
+ ///< and the following byte V)
+ AV_PIX_FMT_NV42, ///< as above, but U and V bytes are swapped
+
+ /**
+ * Vulkan hardware images.
+ *
+ * data[0] points to an AVVkFrame
+ */
+ AV_PIX_FMT_VULKAN,
+
+ AV_PIX_FMT_Y210BE, ///< packed YUV 4:2:2 like YUYV422, 20bpp, data in the
+ ///< high bits, big-endian
+ AV_PIX_FMT_Y210LE, ///< packed YUV 4:2:2 like YUYV422, 20bpp, data in the
+ ///< high bits, little-endian
+
+ AV_PIX_FMT_X2RGB10LE, ///< packed RGB 10:10:10, 30bpp, (msb)2X 10R 10G
+ ///< 10B(lsb), little-endian, X=unused/undefined
+ AV_PIX_FMT_X2RGB10BE, ///< packed RGB 10:10:10, 30bpp, (msb)2X 10R 10G
+ ///< 10B(lsb), big-endian, X=unused/undefined
+ AV_PIX_FMT_X2BGR10LE, ///< packed BGR 10:10:10, 30bpp, (msb)2X 10B 10G
+ ///< 10R(lsb), little-endian, X=unused/undefined
+ AV_PIX_FMT_X2BGR10BE, ///< packed BGR 10:10:10, 30bpp, (msb)2X 10B 10G
+ ///< 10R(lsb), big-endian, X=unused/undefined
+
+ AV_PIX_FMT_P210BE, ///< interleaved chroma YUV 4:2:2, 20bpp, data in the high
+ ///< bits, big-endian
+ AV_PIX_FMT_P210LE, ///< interleaved chroma YUV 4:2:2, 20bpp, data in the high
+ ///< bits, little-endian
+
+ AV_PIX_FMT_P410BE, ///< interleaved chroma YUV 4:4:4, 30bpp, data in the high
+ ///< bits, big-endian
+ AV_PIX_FMT_P410LE, ///< interleaved chroma YUV 4:4:4, 30bpp, data in the high
+ ///< bits, little-endian
+
+ AV_PIX_FMT_P216BE, ///< interleaved chroma YUV 4:2:2, 32bpp, big-endian
+ AV_PIX_FMT_P216LE, ///< interleaved chroma YUV 4:2:2, 32bpp, liddle-endian
+
+ AV_PIX_FMT_P416BE, ///< interleaved chroma YUV 4:4:4, 48bpp, big-endian
+ AV_PIX_FMT_P416LE, ///< interleaved chroma YUV 4:4:4, 48bpp, little-endian
+
+ AV_PIX_FMT_NB ///< number of pixel formats, DO NOT USE THIS if you want to
+ ///< link with shared libav* because the number of formats
+ ///< might differ between versions
+};
+
+#if AV_HAVE_BIGENDIAN
+# define AV_PIX_FMT_NE(be, le) AV_PIX_FMT_##be
+#else
+# define AV_PIX_FMT_NE(be, le) AV_PIX_FMT_##le
+#endif
+
+#define AV_PIX_FMT_RGB32 AV_PIX_FMT_NE(ARGB, BGRA)
+#define AV_PIX_FMT_RGB32_1 AV_PIX_FMT_NE(RGBA, ABGR)
+#define AV_PIX_FMT_BGR32 AV_PIX_FMT_NE(ABGR, RGBA)
+#define AV_PIX_FMT_BGR32_1 AV_PIX_FMT_NE(BGRA, ARGB)
+#define AV_PIX_FMT_0RGB32 AV_PIX_FMT_NE(0RGB, BGR0)
+#define AV_PIX_FMT_0BGR32 AV_PIX_FMT_NE(0BGR, RGB0)
+
+#define AV_PIX_FMT_GRAY9 AV_PIX_FMT_NE(GRAY9BE, GRAY9LE)
+#define AV_PIX_FMT_GRAY10 AV_PIX_FMT_NE(GRAY10BE, GRAY10LE)
+#define AV_PIX_FMT_GRAY12 AV_PIX_FMT_NE(GRAY12BE, GRAY12LE)
+#define AV_PIX_FMT_GRAY14 AV_PIX_FMT_NE(GRAY14BE, GRAY14LE)
+#define AV_PIX_FMT_GRAY16 AV_PIX_FMT_NE(GRAY16BE, GRAY16LE)
+#define AV_PIX_FMT_YA16 AV_PIX_FMT_NE(YA16BE, YA16LE)
+#define AV_PIX_FMT_RGB48 AV_PIX_FMT_NE(RGB48BE, RGB48LE)
+#define AV_PIX_FMT_RGB565 AV_PIX_FMT_NE(RGB565BE, RGB565LE)
+#define AV_PIX_FMT_RGB555 AV_PIX_FMT_NE(RGB555BE, RGB555LE)
+#define AV_PIX_FMT_RGB444 AV_PIX_FMT_NE(RGB444BE, RGB444LE)
+#define AV_PIX_FMT_RGBA64 AV_PIX_FMT_NE(RGBA64BE, RGBA64LE)
+#define AV_PIX_FMT_BGR48 AV_PIX_FMT_NE(BGR48BE, BGR48LE)
+#define AV_PIX_FMT_BGR565 AV_PIX_FMT_NE(BGR565BE, BGR565LE)
+#define AV_PIX_FMT_BGR555 AV_PIX_FMT_NE(BGR555BE, BGR555LE)
+#define AV_PIX_FMT_BGR444 AV_PIX_FMT_NE(BGR444BE, BGR444LE)
+#define AV_PIX_FMT_BGRA64 AV_PIX_FMT_NE(BGRA64BE, BGRA64LE)
+
+#define AV_PIX_FMT_YUV420P9 AV_PIX_FMT_NE(YUV420P9BE, YUV420P9LE)
+#define AV_PIX_FMT_YUV422P9 AV_PIX_FMT_NE(YUV422P9BE, YUV422P9LE)
+#define AV_PIX_FMT_YUV444P9 AV_PIX_FMT_NE(YUV444P9BE, YUV444P9LE)
+#define AV_PIX_FMT_YUV420P10 AV_PIX_FMT_NE(YUV420P10BE, YUV420P10LE)
+#define AV_PIX_FMT_YUV422P10 AV_PIX_FMT_NE(YUV422P10BE, YUV422P10LE)
+#define AV_PIX_FMT_YUV440P10 AV_PIX_FMT_NE(YUV440P10BE, YUV440P10LE)
+#define AV_PIX_FMT_YUV444P10 AV_PIX_FMT_NE(YUV444P10BE, YUV444P10LE)
+#define AV_PIX_FMT_YUV420P12 AV_PIX_FMT_NE(YUV420P12BE, YUV420P12LE)
+#define AV_PIX_FMT_YUV422P12 AV_PIX_FMT_NE(YUV422P12BE, YUV422P12LE)
+#define AV_PIX_FMT_YUV440P12 AV_PIX_FMT_NE(YUV440P12BE, YUV440P12LE)
+#define AV_PIX_FMT_YUV444P12 AV_PIX_FMT_NE(YUV444P12BE, YUV444P12LE)
+#define AV_PIX_FMT_YUV420P14 AV_PIX_FMT_NE(YUV420P14BE, YUV420P14LE)
+#define AV_PIX_FMT_YUV422P14 AV_PIX_FMT_NE(YUV422P14BE, YUV422P14LE)
+#define AV_PIX_FMT_YUV444P14 AV_PIX_FMT_NE(YUV444P14BE, YUV444P14LE)
+#define AV_PIX_FMT_YUV420P16 AV_PIX_FMT_NE(YUV420P16BE, YUV420P16LE)
+#define AV_PIX_FMT_YUV422P16 AV_PIX_FMT_NE(YUV422P16BE, YUV422P16LE)
+#define AV_PIX_FMT_YUV444P16 AV_PIX_FMT_NE(YUV444P16BE, YUV444P16LE)
+
+#define AV_PIX_FMT_GBRP9 AV_PIX_FMT_NE(GBRP9BE, GBRP9LE)
+#define AV_PIX_FMT_GBRP10 AV_PIX_FMT_NE(GBRP10BE, GBRP10LE)
+#define AV_PIX_FMT_GBRP12 AV_PIX_FMT_NE(GBRP12BE, GBRP12LE)
+#define AV_PIX_FMT_GBRP14 AV_PIX_FMT_NE(GBRP14BE, GBRP14LE)
+#define AV_PIX_FMT_GBRP16 AV_PIX_FMT_NE(GBRP16BE, GBRP16LE)
+#define AV_PIX_FMT_GBRAP10 AV_PIX_FMT_NE(GBRAP10BE, GBRAP10LE)
+#define AV_PIX_FMT_GBRAP12 AV_PIX_FMT_NE(GBRAP12BE, GBRAP12LE)
+#define AV_PIX_FMT_GBRAP16 AV_PIX_FMT_NE(GBRAP16BE, GBRAP16LE)
+
+#define AV_PIX_FMT_BAYER_BGGR16 AV_PIX_FMT_NE(BAYER_BGGR16BE, BAYER_BGGR16LE)
+#define AV_PIX_FMT_BAYER_RGGB16 AV_PIX_FMT_NE(BAYER_RGGB16BE, BAYER_RGGB16LE)
+#define AV_PIX_FMT_BAYER_GBRG16 AV_PIX_FMT_NE(BAYER_GBRG16BE, BAYER_GBRG16LE)
+#define AV_PIX_FMT_BAYER_GRBG16 AV_PIX_FMT_NE(BAYER_GRBG16BE, BAYER_GRBG16LE)
+
+#define AV_PIX_FMT_GBRPF32 AV_PIX_FMT_NE(GBRPF32BE, GBRPF32LE)
+#define AV_PIX_FMT_GBRAPF32 AV_PIX_FMT_NE(GBRAPF32BE, GBRAPF32LE)
+
+#define AV_PIX_FMT_GRAYF32 AV_PIX_FMT_NE(GRAYF32BE, GRAYF32LE)
+
+#define AV_PIX_FMT_YUVA420P9 AV_PIX_FMT_NE(YUVA420P9BE, YUVA420P9LE)
+#define AV_PIX_FMT_YUVA422P9 AV_PIX_FMT_NE(YUVA422P9BE, YUVA422P9LE)
+#define AV_PIX_FMT_YUVA444P9 AV_PIX_FMT_NE(YUVA444P9BE, YUVA444P9LE)
+#define AV_PIX_FMT_YUVA420P10 AV_PIX_FMT_NE(YUVA420P10BE, YUVA420P10LE)
+#define AV_PIX_FMT_YUVA422P10 AV_PIX_FMT_NE(YUVA422P10BE, YUVA422P10LE)
+#define AV_PIX_FMT_YUVA444P10 AV_PIX_FMT_NE(YUVA444P10BE, YUVA444P10LE)
+#define AV_PIX_FMT_YUVA422P12 AV_PIX_FMT_NE(YUVA422P12BE, YUVA422P12LE)
+#define AV_PIX_FMT_YUVA444P12 AV_PIX_FMT_NE(YUVA444P12BE, YUVA444P12LE)
+#define AV_PIX_FMT_YUVA420P16 AV_PIX_FMT_NE(YUVA420P16BE, YUVA420P16LE)
+#define AV_PIX_FMT_YUVA422P16 AV_PIX_FMT_NE(YUVA422P16BE, YUVA422P16LE)
+#define AV_PIX_FMT_YUVA444P16 AV_PIX_FMT_NE(YUVA444P16BE, YUVA444P16LE)
+
+#define AV_PIX_FMT_XYZ12 AV_PIX_FMT_NE(XYZ12BE, XYZ12LE)
+#define AV_PIX_FMT_NV20 AV_PIX_FMT_NE(NV20BE, NV20LE)
+#define AV_PIX_FMT_AYUV64 AV_PIX_FMT_NE(AYUV64BE, AYUV64LE)
+#define AV_PIX_FMT_P010 AV_PIX_FMT_NE(P010BE, P010LE)
+#define AV_PIX_FMT_P016 AV_PIX_FMT_NE(P016BE, P016LE)
+
+#define AV_PIX_FMT_Y210 AV_PIX_FMT_NE(Y210BE, Y210LE)
+#define AV_PIX_FMT_X2RGB10 AV_PIX_FMT_NE(X2RGB10BE, X2RGB10LE)
+#define AV_PIX_FMT_X2BGR10 AV_PIX_FMT_NE(X2BGR10BE, X2BGR10LE)
+
+#define AV_PIX_FMT_P210 AV_PIX_FMT_NE(P210BE, P210LE)
+#define AV_PIX_FMT_P410 AV_PIX_FMT_NE(P410BE, P410LE)
+#define AV_PIX_FMT_P216 AV_PIX_FMT_NE(P216BE, P216LE)
+#define AV_PIX_FMT_P416 AV_PIX_FMT_NE(P416BE, P416LE)
+
+/**
+ * Chromaticity coordinates of the source primaries.
+ * These values match the ones defined by ISO/IEC 23091-2_2019 subclause 8.1 and
+ * ITU-T H.273.
+ */
+enum AVColorPrimaries {
+ AVCOL_PRI_RESERVED0 = 0,
+ AVCOL_PRI_BT709 =
+ 1, ///< also ITU-R BT1361 / IEC 61966-2-4 / SMPTE RP 177 Annex B
+ AVCOL_PRI_UNSPECIFIED = 2,
+ AVCOL_PRI_RESERVED = 3,
+ AVCOL_PRI_BT470M =
+ 4, ///< also FCC Title 47 Code of Federal Regulations 73.682 (a)(20)
+
+ AVCOL_PRI_BT470BG = 5, ///< also ITU-R BT601-6 625 / ITU-R BT1358 625 / ITU-R
+ ///< BT1700 625 PAL & SECAM
+ AVCOL_PRI_SMPTE170M =
+ 6, ///< also ITU-R BT601-6 525 / ITU-R BT1358 525 / ITU-R BT1700 NTSC
+ AVCOL_PRI_SMPTE240M =
+ 7, ///< identical to above, also called "SMPTE C" even though it uses D65
+ AVCOL_PRI_FILM = 8, ///< colour filters using Illuminant C
+ AVCOL_PRI_BT2020 = 9, ///< ITU-R BT2020
+ AVCOL_PRI_SMPTE428 = 10, ///< SMPTE ST 428-1 (CIE 1931 XYZ)
+ AVCOL_PRI_SMPTEST428_1 = AVCOL_PRI_SMPTE428,
+ AVCOL_PRI_SMPTE431 = 11, ///< SMPTE ST 431-2 (2011) / DCI P3
+ AVCOL_PRI_SMPTE432 = 12, ///< SMPTE ST 432-1 (2010) / P3 D65 / Display P3
+ AVCOL_PRI_EBU3213 = 22, ///< EBU Tech. 3213-E (nothing there) / one of JEDEC
+ ///< P22 group phosphors
+ AVCOL_PRI_JEDEC_P22 = AVCOL_PRI_EBU3213,
+ AVCOL_PRI_NB ///< Not part of ABI
+};
+
+/**
+ * Color Transfer Characteristic.
+ * These values match the ones defined by ISO/IEC 23091-2_2019 subclause 8.2.
+ */
+enum AVColorTransferCharacteristic {
+ AVCOL_TRC_RESERVED0 = 0,
+ AVCOL_TRC_BT709 = 1, ///< also ITU-R BT1361
+ AVCOL_TRC_UNSPECIFIED = 2,
+ AVCOL_TRC_RESERVED = 3,
+ AVCOL_TRC_GAMMA22 = 4, ///< also ITU-R BT470M / ITU-R BT1700 625 PAL & SECAM
+ AVCOL_TRC_GAMMA28 = 5, ///< also ITU-R BT470BG
+ AVCOL_TRC_SMPTE170M = 6, ///< also ITU-R BT601-6 525 or 625 / ITU-R BT1358
+ ///< 525 or 625 / ITU-R BT1700 NTSC
+ AVCOL_TRC_SMPTE240M = 7,
+ AVCOL_TRC_LINEAR = 8, ///< "Linear transfer characteristics"
+ AVCOL_TRC_LOG = 9, ///< "Logarithmic transfer characteristic (100:1 range)"
+ AVCOL_TRC_LOG_SQRT =
+ 10, ///< "Logarithmic transfer characteristic (100 * Sqrt(10) : 1 range)"
+ AVCOL_TRC_IEC61966_2_4 = 11, ///< IEC 61966-2-4
+ AVCOL_TRC_BT1361_ECG = 12, ///< ITU-R BT1361 Extended Colour Gamut
+ AVCOL_TRC_IEC61966_2_1 = 13, ///< IEC 61966-2-1 (sRGB or sYCC)
+ AVCOL_TRC_BT2020_10 = 14, ///< ITU-R BT2020 for 10-bit system
+ AVCOL_TRC_BT2020_12 = 15, ///< ITU-R BT2020 for 12-bit system
+ AVCOL_TRC_SMPTE2084 =
+ 16, ///< SMPTE ST 2084 for 10-, 12-, 14- and 16-bit systems
+ AVCOL_TRC_SMPTEST2084 = AVCOL_TRC_SMPTE2084,
+ AVCOL_TRC_SMPTE428 = 17, ///< SMPTE ST 428-1
+ AVCOL_TRC_SMPTEST428_1 = AVCOL_TRC_SMPTE428,
+ AVCOL_TRC_ARIB_STD_B67 = 18, ///< ARIB STD-B67, known as "Hybrid log-gamma"
+ AVCOL_TRC_NB ///< Not part of ABI
+};
+
+/**
+ * YUV colorspace type.
+ * These values match the ones defined by ISO/IEC 23091-2_2019 subclause 8.3.
+ */
+enum AVColorSpace {
+ AVCOL_SPC_RGB = 0, ///< order of coefficients is actually GBR, also IEC
+ ///< 61966-2-1 (sRGB), YZX and ST 428-1
+ AVCOL_SPC_BT709 = 1, ///< also ITU-R BT1361 / IEC 61966-2-4 xvYCC709 /
+ ///< derived in SMPTE RP 177 Annex B
+ AVCOL_SPC_UNSPECIFIED = 2,
+ AVCOL_SPC_RESERVED =
+ 3, ///< reserved for future use by ITU-T and ISO/IEC just like 15-255 are
+ AVCOL_SPC_FCC =
+ 4, ///< FCC Title 47 Code of Federal Regulations 73.682 (a)(20)
+ AVCOL_SPC_BT470BG = 5, ///< also ITU-R BT601-6 625 / ITU-R BT1358 625 / ITU-R
+ ///< BT1700 625 PAL & SECAM / IEC 61966-2-4 xvYCC601
+ AVCOL_SPC_SMPTE170M =
+ 6, ///< also ITU-R BT601-6 525 / ITU-R BT1358 525 / ITU-R BT1700 NTSC /
+ ///< functionally identical to above
+ AVCOL_SPC_SMPTE240M =
+ 7, ///< derived from 170M primaries and D65 white point, 170M is derived
+ ///< from BT470 System M's primaries
+ AVCOL_SPC_YCGCO =
+ 8, ///< used by Dirac / VC-2 and H.264 FRext, see ITU-T SG16
+ AVCOL_SPC_YCOCG = AVCOL_SPC_YCGCO,
+ AVCOL_SPC_BT2020_NCL = 9, ///< ITU-R BT2020 non-constant luminance system
+ AVCOL_SPC_BT2020_CL = 10, ///< ITU-R BT2020 constant luminance system
+ AVCOL_SPC_SMPTE2085 = 11, ///< SMPTE 2085, Y'D'zD'x
+ AVCOL_SPC_CHROMA_DERIVED_NCL =
+ 12, ///< Chromaticity-derived non-constant luminance system
+ AVCOL_SPC_CHROMA_DERIVED_CL =
+ 13, ///< Chromaticity-derived constant luminance system
+ AVCOL_SPC_ICTCP = 14, ///< ITU-R BT.2100-0, ICtCp
+ AVCOL_SPC_NB ///< Not part of ABI
+};
+
+/**
+ * Visual content value range.
+ *
+ * These values are based on definitions that can be found in multiple
+ * specifications, such as ITU-T BT.709 (3.4 - Quantization of RGB, luminance
+ * and colour-difference signals), ITU-T BT.2020 (Table 5 - Digital
+ * Representation) as well as ITU-T BT.2100 (Table 9 - Digital 10- and 12-bit
+ * integer representation). At the time of writing, the BT.2100 one is
+ * recommended, as it also defines the full range representation.
+ *
+ * Common definitions:
+ * - For RGB and luma planes such as Y in YCbCr and I in ICtCp,
+ * 'E' is the original value in range of 0.0 to 1.0.
+ * - For chroma planes such as Cb,Cr and Ct,Cp, 'E' is the original
+ * value in range of -0.5 to 0.5.
+ * - 'n' is the output bit depth.
+ * - For additional definitions such as rounding and clipping to valid n
+ * bit unsigned integer range, please refer to BT.2100 (Table 9).
+ */
+enum AVColorRange {
+ AVCOL_RANGE_UNSPECIFIED = 0,
+
+ /**
+ * Narrow or limited range content.
+ *
+ * - For luma planes:
+ *
+ * (219 * E + 16) * 2^(n-8)
+ *
+ * F.ex. the range of 16-235 for 8 bits
+ *
+ * - For chroma planes:
+ *
+ * (224 * E + 128) * 2^(n-8)
+ *
+ * F.ex. the range of 16-240 for 8 bits
+ */
+ AVCOL_RANGE_MPEG = 1,
+
+ /**
+ * Full range content.
+ *
+ * - For RGB and luma planes:
+ *
+ * (2^n - 1) * E
+ *
+ * F.ex. the range of 0-255 for 8 bits
+ *
+ * - For chroma planes:
+ *
+ * (2^n - 1) * E + 2^(n - 1)
+ *
+ * F.ex. the range of 1-255 for 8 bits
+ */
+ AVCOL_RANGE_JPEG = 2,
+ AVCOL_RANGE_NB ///< Not part of ABI
+};
+
+/**
+ * Location of chroma samples.
+ *
+ * Illustration showing the location of the first (top left) chroma sample of
+ *the image, the left shows only luma, the right shows the location of the
+ *chroma sample, the 2 could be imagined to overlay each other but are drawn
+ *separately due to limitations of ASCII
+ *
+ * 1st 2nd 1st 2nd horizontal luma sample positions
+ * v v v v
+ * ______ ______
+ *1st luma line > |X X ... |3 4 X ... X are luma samples,
+ * | |1 2 1-6 are possible chroma positions
+ *2nd luma line > |X X ... |5 6 X ... 0 is undefined/unknown position
+ */
+enum AVChromaLocation {
+ AVCHROMA_LOC_UNSPECIFIED = 0,
+ AVCHROMA_LOC_LEFT = 1, ///< MPEG-2/4 4:2:0, H.264 default for 4:2:0
+ AVCHROMA_LOC_CENTER = 2, ///< MPEG-1 4:2:0, JPEG 4:2:0, H.263 4:2:0
+ AVCHROMA_LOC_TOPLEFT =
+ 3, ///< ITU-R 601, SMPTE 274M 296M S314M(DV 4:1:1), mpeg2 4:2:2
+ AVCHROMA_LOC_TOP = 4,
+ AVCHROMA_LOC_BOTTOMLEFT = 5,
+ AVCHROMA_LOC_BOTTOM = 6,
+ AVCHROMA_LOC_NB ///< Not part of ABI
+};
+
+#endif /* AVUTIL_PIXFMT_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/rational.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/rational.h
new file mode 100644
index 0000000000..8d4b80cfdc
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/rational.h
@@ -0,0 +1,221 @@
+/*
+ * rational numbers
+ * Copyright (c) 2003 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * @ingroup lavu_math_rational
+ * Utilties for rational number calculation.
+ * @author Michael Niedermayer <michaelni@gmx.at>
+ */
+
+#ifndef AVUTIL_RATIONAL_H
+#define AVUTIL_RATIONAL_H
+
+#include <stdint.h>
+#include <limits.h>
+#include "attributes.h"
+
+/**
+ * @defgroup lavu_math_rational AVRational
+ * @ingroup lavu_math
+ * Rational number calculation.
+ *
+ * While rational numbers can be expressed as floating-point numbers, the
+ * conversion process is a lossy one, so are floating-point operations. On the
+ * other hand, the nature of FFmpeg demands highly accurate calculation of
+ * timestamps. This set of rational number utilities serves as a generic
+ * interface for manipulating rational numbers as pairs of numerators and
+ * denominators.
+ *
+ * Many of the functions that operate on AVRational's have the suffix `_q`, in
+ * reference to the mathematical symbol "ℚ" (Q) which denotes the set of all
+ * rational numbers.
+ *
+ * @{
+ */
+
+/**
+ * Rational number (pair of numerator and denominator).
+ */
+typedef struct AVRational {
+ int num; ///< Numerator
+ int den; ///< Denominator
+} AVRational;
+
+/**
+ * Create an AVRational.
+ *
+ * Useful for compilers that do not support compound literals.
+ *
+ * @note The return value is not reduced.
+ * @see av_reduce()
+ */
+static inline AVRational av_make_q(int num, int den) {
+ AVRational r = {num, den};
+ return r;
+}
+
+/**
+ * Compare two rationals.
+ *
+ * @param a First rational
+ * @param b Second rational
+ *
+ * @return One of the following values:
+ * - 0 if `a == b`
+ * - 1 if `a > b`
+ * - -1 if `a < b`
+ * - `INT_MIN` if one of the values is of the form `0 / 0`
+ */
+static inline int av_cmp_q(AVRational a, AVRational b) {
+ const int64_t tmp = a.num * (int64_t)b.den - b.num * (int64_t)a.den;
+
+ if (tmp)
+ return (int)((tmp ^ a.den ^ b.den) >> 63) | 1;
+ else if (b.den && a.den)
+ return 0;
+ else if (a.num && b.num)
+ return (a.num >> 31) - (b.num >> 31);
+ else
+ return INT_MIN;
+}
+
+/**
+ * Convert an AVRational to a `double`.
+ * @param a AVRational to convert
+ * @return `a` in floating-point form
+ * @see av_d2q()
+ */
+static inline double av_q2d(AVRational a) { return a.num / (double)a.den; }
+
+/**
+ * Reduce a fraction.
+ *
+ * This is useful for framerate calculations.
+ *
+ * @param[out] dst_num Destination numerator
+ * @param[out] dst_den Destination denominator
+ * @param[in] num Source numerator
+ * @param[in] den Source denominator
+ * @param[in] max Maximum allowed values for `dst_num` & `dst_den`
+ * @return 1 if the operation is exact, 0 otherwise
+ */
+int av_reduce(int* dst_num, int* dst_den, int64_t num, int64_t den,
+ int64_t max);
+
+/**
+ * Multiply two rationals.
+ * @param b First rational
+ * @param c Second rational
+ * @return b*c
+ */
+AVRational av_mul_q(AVRational b, AVRational c) av_const;
+
+/**
+ * Divide one rational by another.
+ * @param b First rational
+ * @param c Second rational
+ * @return b/c
+ */
+AVRational av_div_q(AVRational b, AVRational c) av_const;
+
+/**
+ * Add two rationals.
+ * @param b First rational
+ * @param c Second rational
+ * @return b+c
+ */
+AVRational av_add_q(AVRational b, AVRational c) av_const;
+
+/**
+ * Subtract one rational from another.
+ * @param b First rational
+ * @param c Second rational
+ * @return b-c
+ */
+AVRational av_sub_q(AVRational b, AVRational c) av_const;
+
+/**
+ * Invert a rational.
+ * @param q value
+ * @return 1 / q
+ */
+static av_always_inline AVRational av_inv_q(AVRational q) {
+ AVRational r = {q.den, q.num};
+ return r;
+}
+
+/**
+ * Convert a double precision floating point number to a rational.
+ *
+ * In case of infinity, the returned value is expressed as `{1, 0}` or
+ * `{-1, 0}` depending on the sign.
+ *
+ * @param d `double` to convert
+ * @param max Maximum allowed numerator and denominator
+ * @return `d` in AVRational form
+ * @see av_q2d()
+ */
+AVRational av_d2q(double d, int max) av_const;
+
+/**
+ * Find which of the two rationals is closer to another rational.
+ *
+ * @param q Rational to be compared against
+ * @param q1,q2 Rationals to be tested
+ * @return One of the following values:
+ * - 1 if `q1` is nearer to `q` than `q2`
+ * - -1 if `q2` is nearer to `q` than `q1`
+ * - 0 if they have the same distance
+ */
+int av_nearer_q(AVRational q, AVRational q1, AVRational q2);
+
+/**
+ * Find the value in a list of rationals nearest a given reference rational.
+ *
+ * @param q Reference rational
+ * @param q_list Array of rationals terminated by `{0, 0}`
+ * @return Index of the nearest value found in the array
+ */
+int av_find_nearest_q_idx(AVRational q, const AVRational* q_list);
+
+/**
+ * Convert an AVRational to a IEEE 32-bit `float` expressed in fixed-point
+ * format.
+ *
+ * @param q Rational to be converted
+ * @return Equivalent floating-point value, expressed as an unsigned 32-bit
+ * integer.
+ * @note The returned value is platform-indepedant.
+ */
+uint32_t av_q2intfloat(AVRational q);
+
+/**
+ * Return the best rational so that a and b are multiple of it.
+ * If the resulting denominator is larger than max_den, return def.
+ */
+AVRational av_gcd_q(AVRational a, AVRational b, int max_den, AVRational def);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_RATIONAL_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/samplefmt.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/samplefmt.h
new file mode 100644
index 0000000000..fe148aeae6
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/samplefmt.h
@@ -0,0 +1,276 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_SAMPLEFMT_H
+#define AVUTIL_SAMPLEFMT_H
+
+#include <stdint.h>
+
+#include "avutil.h"
+#include "attributes.h"
+
+/**
+ * @addtogroup lavu_audio
+ * @{
+ *
+ * @defgroup lavu_sampfmts Audio sample formats
+ *
+ * Audio sample format enumeration and related convenience functions.
+ * @{
+ */
+
+/**
+ * Audio sample formats
+ *
+ * - The data described by the sample format is always in native-endian order.
+ * Sample values can be expressed by native C types, hence the lack of a
+ * signed 24-bit sample format even though it is a common raw audio data format.
+ *
+ * - The floating-point formats are based on full volume being in the range
+ * [-1.0, 1.0]. Any values outside this range are beyond full volume level.
+ *
+ * - The data layout as used in av_samples_fill_arrays() and elsewhere in FFmpeg
+ * (such as AVFrame in libavcodec) is as follows:
+ *
+ * @par
+ * For planar sample formats, each audio channel is in a separate data plane,
+ * and linesize is the buffer size, in bytes, for a single plane. All data
+ * planes must be the same size. For packed sample formats, only the first data
+ * plane is used, and samples for each channel are interleaved. In this case,
+ * linesize is the buffer size, in bytes, for the 1 plane.
+ *
+ */
+enum AVSampleFormat {
+ AV_SAMPLE_FMT_NONE = -1,
+ AV_SAMPLE_FMT_U8, ///< unsigned 8 bits
+ AV_SAMPLE_FMT_S16, ///< signed 16 bits
+ AV_SAMPLE_FMT_S32, ///< signed 32 bits
+ AV_SAMPLE_FMT_FLT, ///< float
+ AV_SAMPLE_FMT_DBL, ///< double
+
+ AV_SAMPLE_FMT_U8P, ///< unsigned 8 bits, planar
+ AV_SAMPLE_FMT_S16P, ///< signed 16 bits, planar
+ AV_SAMPLE_FMT_S32P, ///< signed 32 bits, planar
+ AV_SAMPLE_FMT_FLTP, ///< float, planar
+ AV_SAMPLE_FMT_DBLP, ///< double, planar
+ AV_SAMPLE_FMT_S64, ///< signed 64 bits
+ AV_SAMPLE_FMT_S64P, ///< signed 64 bits, planar
+
+ AV_SAMPLE_FMT_NB ///< Number of sample formats. DO NOT USE if linking
+ ///< dynamically
+};
+
+/**
+ * Return the name of sample_fmt, or NULL if sample_fmt is not
+ * recognized.
+ */
+const char* av_get_sample_fmt_name(enum AVSampleFormat sample_fmt);
+
+/**
+ * Return a sample format corresponding to name, or AV_SAMPLE_FMT_NONE
+ * on error.
+ */
+enum AVSampleFormat av_get_sample_fmt(const char* name);
+
+/**
+ * Return the planar<->packed alternative form of the given sample format, or
+ * AV_SAMPLE_FMT_NONE on error. If the passed sample_fmt is already in the
+ * requested planar/packed format, the format returned is the same as the
+ * input.
+ */
+enum AVSampleFormat av_get_alt_sample_fmt(enum AVSampleFormat sample_fmt,
+ int planar);
+
+/**
+ * Get the packed alternative form of the given sample format.
+ *
+ * If the passed sample_fmt is already in packed format, the format returned is
+ * the same as the input.
+ *
+ * @return the packed alternative form of the given sample format or
+ AV_SAMPLE_FMT_NONE on error.
+ */
+enum AVSampleFormat av_get_packed_sample_fmt(enum AVSampleFormat sample_fmt);
+
+/**
+ * Get the planar alternative form of the given sample format.
+ *
+ * If the passed sample_fmt is already in planar format, the format returned is
+ * the same as the input.
+ *
+ * @return the planar alternative form of the given sample format or
+ AV_SAMPLE_FMT_NONE on error.
+ */
+enum AVSampleFormat av_get_planar_sample_fmt(enum AVSampleFormat sample_fmt);
+
+/**
+ * Generate a string corresponding to the sample format with
+ * sample_fmt, or a header if sample_fmt is negative.
+ *
+ * @param buf the buffer where to write the string
+ * @param buf_size the size of buf
+ * @param sample_fmt the number of the sample format to print the
+ * corresponding info string, or a negative value to print the
+ * corresponding header.
+ * @return the pointer to the filled buffer or NULL if sample_fmt is
+ * unknown or in case of other errors
+ */
+char* av_get_sample_fmt_string(char* buf, int buf_size,
+ enum AVSampleFormat sample_fmt);
+
+/**
+ * Return number of bytes per sample.
+ *
+ * @param sample_fmt the sample format
+ * @return number of bytes per sample or zero if unknown for the given
+ * sample format
+ */
+int av_get_bytes_per_sample(enum AVSampleFormat sample_fmt);
+
+/**
+ * Check if the sample format is planar.
+ *
+ * @param sample_fmt the sample format to inspect
+ * @return 1 if the sample format is planar, 0 if it is interleaved
+ */
+int av_sample_fmt_is_planar(enum AVSampleFormat sample_fmt);
+
+/**
+ * Get the required buffer size for the given audio parameters.
+ *
+ * @param[out] linesize calculated linesize, may be NULL
+ * @param nb_channels the number of channels
+ * @param nb_samples the number of samples in a single channel
+ * @param sample_fmt the sample format
+ * @param align buffer size alignment (0 = default, 1 = no alignment)
+ * @return required buffer size, or negative error code on failure
+ */
+int av_samples_get_buffer_size(int* linesize, int nb_channels, int nb_samples,
+ enum AVSampleFormat sample_fmt, int align);
+
+/**
+ * @}
+ *
+ * @defgroup lavu_sampmanip Samples manipulation
+ *
+ * Functions that manipulate audio samples
+ * @{
+ */
+
+/**
+ * Fill plane data pointers and linesize for samples with sample
+ * format sample_fmt.
+ *
+ * The audio_data array is filled with the pointers to the samples data planes:
+ * for planar, set the start point of each channel's data within the buffer,
+ * for packed, set the start point of the entire buffer only.
+ *
+ * The value pointed to by linesize is set to the aligned size of each
+ * channel's data buffer for planar layout, or to the aligned size of the
+ * buffer for all channels for packed layout.
+ *
+ * The buffer in buf must be big enough to contain all the samples
+ * (use av_samples_get_buffer_size() to compute its minimum size),
+ * otherwise the audio_data pointers will point to invalid data.
+ *
+ * @see enum AVSampleFormat
+ * The documentation for AVSampleFormat describes the data layout.
+ *
+ * @param[out] audio_data array to be filled with the pointer for each channel
+ * @param[out] linesize calculated linesize, may be NULL
+ * @param buf the pointer to a buffer containing the samples
+ * @param nb_channels the number of channels
+ * @param nb_samples the number of samples in a single channel
+ * @param sample_fmt the sample format
+ * @param align buffer size alignment (0 = default, 1 = no alignment)
+ * @return minimum size in bytes required for the buffer on
+ * success, or a negative error code on failure
+ */
+int av_samples_fill_arrays(uint8_t** audio_data, int* linesize,
+ const uint8_t* buf, int nb_channels, int nb_samples,
+ enum AVSampleFormat sample_fmt, int align);
+
+/**
+ * Allocate a samples buffer for nb_samples samples, and fill data pointers and
+ * linesize accordingly.
+ * The allocated samples buffer can be freed by using av_freep(&audio_data[0])
+ * Allocated data will be initialized to silence.
+ *
+ * @see enum AVSampleFormat
+ * The documentation for AVSampleFormat describes the data layout.
+ *
+ * @param[out] audio_data array to be filled with the pointer for each channel
+ * @param[out] linesize aligned size for audio buffer(s), may be NULL
+ * @param nb_channels number of audio channels
+ * @param nb_samples number of samples per channel
+ * @param align buffer size alignment (0 = default, 1 = no alignment)
+ * @return >=0 on success or a negative error code on failure
+ * @todo return the size of the allocated buffer in case of success at the next
+ * bump
+ * @see av_samples_fill_arrays()
+ * @see av_samples_alloc_array_and_samples()
+ */
+int av_samples_alloc(uint8_t** audio_data, int* linesize, int nb_channels,
+ int nb_samples, enum AVSampleFormat sample_fmt, int align);
+
+/**
+ * Allocate a data pointers array, samples buffer for nb_samples
+ * samples, and fill data pointers and linesize accordingly.
+ *
+ * This is the same as av_samples_alloc(), but also allocates the data
+ * pointers array.
+ *
+ * @see av_samples_alloc()
+ */
+int av_samples_alloc_array_and_samples(uint8_t*** audio_data, int* linesize,
+ int nb_channels, int nb_samples,
+ enum AVSampleFormat sample_fmt,
+ int align);
+
+/**
+ * Copy samples from src to dst.
+ *
+ * @param dst destination array of pointers to data planes
+ * @param src source array of pointers to data planes
+ * @param dst_offset offset in samples at which the data will be written to dst
+ * @param src_offset offset in samples at which the data will be read from src
+ * @param nb_samples number of samples to be copied
+ * @param nb_channels number of audio channels
+ * @param sample_fmt audio sample format
+ */
+int av_samples_copy(uint8_t** dst, uint8_t* const* src, int dst_offset,
+ int src_offset, int nb_samples, int nb_channels,
+ enum AVSampleFormat sample_fmt);
+
+/**
+ * Fill an audio buffer with silence.
+ *
+ * @param audio_data array of pointers to data planes
+ * @param offset offset in samples at which to start filling
+ * @param nb_samples number of samples to fill
+ * @param nb_channels number of audio channels
+ * @param sample_fmt audio sample format
+ */
+int av_samples_set_silence(uint8_t** audio_data, int offset, int nb_samples,
+ int nb_channels, enum AVSampleFormat sample_fmt);
+
+/**
+ * @}
+ * @}
+ */
+#endif /* AVUTIL_SAMPLEFMT_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/version.h b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/version.h
new file mode 100644
index 0000000000..e3d8dbcbb5
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/include/libavutil/version.h
@@ -0,0 +1,118 @@
+/*
+ * copyright (c) 2003 Fabrice Bellard
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * @ingroup lavu
+ * Libavutil version macros
+ */
+
+#ifndef AVUTIL_VERSION_H
+#define AVUTIL_VERSION_H
+
+#include "macros.h"
+
+/**
+ * @addtogroup version_utils
+ *
+ * Useful to check and match library version in order to maintain
+ * backward compatibility.
+ *
+ * The FFmpeg libraries follow a versioning sheme very similar to
+ * Semantic Versioning (http://semver.org/)
+ * The difference is that the component called PATCH is called MICRO in FFmpeg
+ * and its value is reset to 100 instead of 0 to keep it above or equal to 100.
+ * Also we do not increase MICRO for every bugfix or change in git master.
+ *
+ * Prior to FFmpeg 3.2 point releases did not change any lib version number to
+ * avoid aliassing different git master checkouts.
+ * Starting with FFmpeg 3.2, the released library versions will occupy
+ * a separate MAJOR.MINOR that is not used on the master development branch.
+ * That is if we branch a release of master 55.10.123 we will bump to 55.11.100
+ * for the release and master will continue at 55.12.100 after it. Each new
+ * point release will then bump the MICRO improving the usefulness of the lib
+ * versions.
+ *
+ * @{
+ */
+
+#define AV_VERSION_INT(a, b, c) ((a) << 16 | (b) << 8 | (c))
+#define AV_VERSION_DOT(a, b, c) a##.##b##.##c
+#define AV_VERSION(a, b, c) AV_VERSION_DOT(a, b, c)
+
+/**
+ * Extract version components from the full ::AV_VERSION_INT int as returned
+ * by functions like ::avformat_version() and ::avcodec_version()
+ */
+#define AV_VERSION_MAJOR(a) ((a) >> 16)
+#define AV_VERSION_MINOR(a) (((a)&0x00FF00) >> 8)
+#define AV_VERSION_MICRO(a) ((a)&0xFF)
+
+/**
+ * @}
+ */
+
+/**
+ * @defgroup lavu_ver Version and Build diagnostics
+ *
+ * Macros and function useful to check at compiletime and at runtime
+ * which version of libavutil is in use.
+ *
+ * @{
+ */
+
+#define LIBAVUTIL_VERSION_MAJOR 57
+#define LIBAVUTIL_VERSION_MINOR 17
+#define LIBAVUTIL_VERSION_MICRO 100
+
+#define LIBAVUTIL_VERSION_INT \
+ AV_VERSION_INT(LIBAVUTIL_VERSION_MAJOR, LIBAVUTIL_VERSION_MINOR, \
+ LIBAVUTIL_VERSION_MICRO)
+#define LIBAVUTIL_VERSION \
+ AV_VERSION(LIBAVUTIL_VERSION_MAJOR, LIBAVUTIL_VERSION_MINOR, \
+ LIBAVUTIL_VERSION_MICRO)
+#define LIBAVUTIL_BUILD LIBAVUTIL_VERSION_INT
+
+#define LIBAVUTIL_IDENT "Lavu" AV_STRINGIFY(LIBAVUTIL_VERSION)
+
+/**
+ * @defgroup lavu_depr_guards Deprecation Guards
+ * FF_API_* defines may be placed below to indicate public API that will be
+ * dropped at a future version bump. The defines themselves are not part of
+ * the public API and may change, break or disappear at any time.
+ *
+ * @note, when bumping the major version it is recommended to manually
+ * disable each FF_API_* in its own commit instead of disabling them all
+ * at once through the bump. This improves the git bisect-ability of the change.
+ *
+ * @{
+ */
+
+#define FF_API_D2STR (LIBAVUTIL_VERSION_MAJOR < 58)
+#define FF_API_DECLARE_ALIGNED (LIBAVUTIL_VERSION_MAJOR < 58)
+#define FF_API_COLORSPACE_NAME (LIBAVUTIL_VERSION_MAJOR < 58)
+#define FF_API_AV_MALLOCZ_ARRAY (LIBAVUTIL_VERSION_MAJOR < 58)
+
+/**
+ * @}
+ * @}
+ */
+
+#endif /* AVUTIL_VERSION_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg59/moz.build b/dom/media/platforms/ffmpeg/ffmpeg59/moz.build
new file mode 100644
index 0000000000..c300f7c0d6
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg59/moz.build
@@ -0,0 +1,39 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES += [
+ "../FFmpegAudioDecoder.cpp",
+ "../FFmpegDataDecoder.cpp",
+ "../FFmpegDecoderModule.cpp",
+ "../FFmpegVideoDecoder.cpp",
+]
+LOCAL_INCLUDES += [
+ "..",
+ "/media/mozva",
+ "include",
+]
+
+if CONFIG["CC_TYPE"] in ("clang", "gcc"):
+ CXXFLAGS += ["-Wno-deprecated-declarations"]
+if CONFIG["CC_TYPE"] == "clang":
+ CXXFLAGS += [
+ "-Wno-unknown-attributes",
+ ]
+if CONFIG["CC_TYPE"] == "gcc":
+ CXXFLAGS += [
+ "-Wno-attributes",
+ ]
+if CONFIG["MOZ_WAYLAND"]:
+ CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]
+if CONFIG["MOZ_ENABLE_VAAPI"] or CONFIG["MOZ_ENABLE_V4L2"]:
+ UNIFIED_SOURCES += ["../FFmpegVideoFramePool.cpp"]
+ LOCAL_INCLUDES += ["/third_party/drm/drm/include/libdrm/"]
+ USE_LIBS += ["mozva"]
+ DEFINES["MOZ_WAYLAND_USE_HWDECODE"] = 1
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/COPYING.LGPLv2.1 b/dom/media/platforms/ffmpeg/ffmpeg60/include/COPYING.LGPLv2.1
new file mode 100644
index 0000000000..00b4fedfe7
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/COPYING.LGPLv2.1
@@ -0,0 +1,504 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the library's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+ <signature of Ty Coon>, 1 April 1990
+ Ty Coon, President of Vice
+
+That's all there is to it!
+
+
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/avcodec.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/avcodec.h
new file mode 100644
index 0000000000..c7e6b609d5
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/avcodec.h
@@ -0,0 +1,3230 @@
+/*
+ * copyright (c) 2001 Fabrice Bellard
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_AVCODEC_H
+#define AVCODEC_AVCODEC_H
+
+/**
+ * @file
+ * @ingroup libavc
+ * Libavcodec external API header
+ */
+
+#include "libavutil/samplefmt.h"
+#include "libavutil/attributes.h"
+#include "libavutil/avutil.h"
+#include "libavutil/buffer.h"
+#include "libavutil/dict.h"
+#include "libavutil/frame.h"
+#include "libavutil/log.h"
+#include "libavutil/pixfmt.h"
+#include "libavutil/rational.h"
+
+#include "codec.h"
+#include "codec_desc.h"
+#include "codec_par.h"
+#include "codec_id.h"
+#include "defs.h"
+#include "packet.h"
+#include "version_major.h"
+#ifndef HAVE_AV_CONFIG_H
+/* When included as part of the ffmpeg build, only include the major version
+ * to avoid unnecessary rebuilds. When included externally, keep including
+ * the full version information. */
+# include "version.h"
+#endif
+
+/**
+ * @defgroup libavc libavcodec
+ * Encoding/Decoding Library
+ *
+ * @{
+ *
+ * @defgroup lavc_decoding Decoding
+ * @{
+ * @}
+ *
+ * @defgroup lavc_encoding Encoding
+ * @{
+ * @}
+ *
+ * @defgroup lavc_codec Codecs
+ * @{
+ * @defgroup lavc_codec_native Native Codecs
+ * @{
+ * @}
+ * @defgroup lavc_codec_wrappers External library wrappers
+ * @{
+ * @}
+ * @defgroup lavc_codec_hwaccel Hardware Accelerators bridge
+ * @{
+ * @}
+ * @}
+ * @defgroup lavc_internal Internal
+ * @{
+ * @}
+ * @}
+ */
+
+/**
+ * @ingroup libavc
+ * @defgroup lavc_encdec send/receive encoding and decoding API overview
+ * @{
+ *
+ * The avcodec_send_packet()/avcodec_receive_frame()/avcodec_send_frame()/
+ * avcodec_receive_packet() functions provide an encode/decode API, which
+ * decouples input and output.
+ *
+ * The API is very similar for encoding/decoding and audio/video, and works as
+ * follows:
+ * - Set up and open the AVCodecContext as usual.
+ * - Send valid input:
+ * - For decoding, call avcodec_send_packet() to give the decoder raw
+ * compressed data in an AVPacket.
+ * - For encoding, call avcodec_send_frame() to give the encoder an AVFrame
+ * containing uncompressed audio or video.
+ *
+ * In both cases, it is recommended that AVPackets and AVFrames are
+ * refcounted, or libavcodec might have to copy the input data. (libavformat
+ * always returns refcounted AVPackets, and av_frame_get_buffer() allocates
+ * refcounted AVFrames.)
+ * - Receive output in a loop. Periodically call one of the avcodec_receive_*()
+ * functions and process their output:
+ * - For decoding, call avcodec_receive_frame(). On success, it will return
+ * an AVFrame containing uncompressed audio or video data.
+ * - For encoding, call avcodec_receive_packet(). On success, it will return
+ * an AVPacket with a compressed frame.
+ *
+ * Repeat this call until it returns AVERROR(EAGAIN) or an error. The
+ * AVERROR(EAGAIN) return value means that new input data is required to
+ * return new output. In this case, continue with sending input. For each
+ * input frame/packet, the codec will typically return 1 output frame/packet,
+ * but it can also be 0 or more than 1.
+ *
+ * At the beginning of decoding or encoding, the codec might accept multiple
+ * input frames/packets without returning a frame, until its internal buffers
+ * are filled. This situation is handled transparently if you follow the steps
+ * outlined above.
+ *
+ * In theory, sending input can result in EAGAIN - this should happen only if
+ * not all output was received. You can use this to structure alternative decode
+ * or encode loops other than the one suggested above. For example, you could
+ * try sending new input on each iteration, and try to receive output if that
+ * returns EAGAIN.
+ *
+ * End of stream situations. These require "flushing" (aka draining) the codec,
+ * as the codec might buffer multiple frames or packets internally for
+ * performance or out of necessity (consider B-frames).
+ * This is handled as follows:
+ * - Instead of valid input, send NULL to the avcodec_send_packet() (decoding)
+ * or avcodec_send_frame() (encoding) functions. This will enter draining
+ * mode.
+ * - Call avcodec_receive_frame() (decoding) or avcodec_receive_packet()
+ * (encoding) in a loop until AVERROR_EOF is returned. The functions will
+ * not return AVERROR(EAGAIN), unless you forgot to enter draining mode.
+ * - Before decoding can be resumed again, the codec has to be reset with
+ * avcodec_flush_buffers().
+ *
+ * Using the API as outlined above is highly recommended. But it is also
+ * possible to call functions outside of this rigid schema. For example, you can
+ * call avcodec_send_packet() repeatedly without calling
+ * avcodec_receive_frame(). In this case, avcodec_send_packet() will succeed
+ * until the codec's internal buffer has been filled up (which is typically of
+ * size 1 per output frame, after initial input), and then reject input with
+ * AVERROR(EAGAIN). Once it starts rejecting input, you have no choice but to
+ * read at least some output.
+ *
+ * Not all codecs will follow a rigid and predictable dataflow; the only
+ * guarantee is that an AVERROR(EAGAIN) return value on a send/receive call on
+ * one end implies that a receive/send call on the other end will succeed, or
+ * at least will not fail with AVERROR(EAGAIN). In general, no codec will
+ * permit unlimited buffering of input or output.
+ *
+ * A codec is not allowed to return AVERROR(EAGAIN) for both sending and
+ * receiving. This would be an invalid state, which could put the codec user
+ * into an endless loop. The API has no concept of time either: it cannot happen
+ * that trying to do avcodec_send_packet() results in AVERROR(EAGAIN), but a
+ * repeated call 1 second later accepts the packet (with no other receive/flush
+ * API calls involved). The API is a strict state machine, and the passage of
+ * time is not supposed to influence it. Some timing-dependent behavior might
+ * still be deemed acceptable in certain cases. But it must never result in both
+ * send/receive returning EAGAIN at the same time at any point. It must also
+ * absolutely be avoided that the current state is "unstable" and can
+ * "flip-flop" between the send/receive APIs allowing progress. For example,
+ * it's not allowed that the codec randomly decides that it actually wants to
+ * consume a packet now instead of returning a frame, after it just returned
+ * AVERROR(EAGAIN) on an avcodec_send_packet() call.
+ * @}
+ */
+
+/**
+ * @defgroup lavc_core Core functions/structures.
+ * @ingroup libavc
+ *
+ * Basic definitions, functions for querying libavcodec capabilities,
+ * allocating core structures, etc.
+ * @{
+ */
+
+/**
+ * @ingroup lavc_encoding
+ * minimum encoding buffer size
+ * Used to avoid some checks during header writing.
+ */
+#define AV_INPUT_BUFFER_MIN_SIZE 16384
+
+/**
+ * @ingroup lavc_encoding
+ */
+typedef struct RcOverride {
+ int start_frame;
+ int end_frame;
+ int qscale; // If this is 0 then quality_factor will be used instead.
+ float quality_factor;
+} RcOverride;
+
+/* encoding support
+ These flags can be passed in AVCodecContext.flags before initialization.
+ Note: Not everything is supported yet.
+*/
+
+/**
+ * Allow decoders to produce frames with data planes that are not aligned
+ * to CPU requirements (e.g. due to cropping).
+ */
+#define AV_CODEC_FLAG_UNALIGNED (1 << 0)
+/**
+ * Use fixed qscale.
+ */
+#define AV_CODEC_FLAG_QSCALE (1 << 1)
+/**
+ * 4 MV per MB allowed / advanced prediction for H.263.
+ */
+#define AV_CODEC_FLAG_4MV (1 << 2)
+/**
+ * Output even those frames that might be corrupted.
+ */
+#define AV_CODEC_FLAG_OUTPUT_CORRUPT (1 << 3)
+/**
+ * Use qpel MC.
+ */
+#define AV_CODEC_FLAG_QPEL (1 << 4)
+/**
+ * Don't output frames whose parameters differ from first
+ * decoded frame in stream.
+ */
+#define AV_CODEC_FLAG_DROPCHANGED (1 << 5)
+/**
+ * Request the encoder to output reconstructed frames, i.e.\ frames that would
+ * be produced by decoding the encoded bistream. These frames may be retrieved
+ * by calling avcodec_receive_frame() immediately after a successful call to
+ * avcodec_receive_packet().
+ *
+ * Should only be used with encoders flagged with the
+ * @ref AV_CODEC_CAP_ENCODER_RECON_FRAME capability.
+ */
+#define AV_CODEC_FLAG_RECON_FRAME (1 << 6)
+/**
+ * @par decoding
+ * Request the decoder to propagate each packets AVPacket.opaque and
+ * AVPacket.opaque_ref to its corresponding output AVFrame.
+ *
+ * @par encoding:
+ * Request the encoder to propagate each frame's AVFrame.opaque and
+ * AVFrame.opaque_ref values to its corresponding output AVPacket.
+ *
+ * @par
+ * May only be set on encoders that have the
+ * @ref AV_CODEC_CAP_ENCODER_REORDERED_OPAQUE capability flag.
+ *
+ * @note
+ * While in typical cases one input frame produces exactly one output packet
+ * (perhaps after a delay), in general the mapping of frames to packets is
+ * M-to-N, so
+ * - Any number of input frames may be associated with any given output packet.
+ * This includes zero - e.g. some encoders may output packets that carry only
+ * metadata about the whole stream.
+ * - A given input frame may be associated with any number of output packets.
+ * Again this includes zero - e.g. some encoders may drop frames under certain
+ * conditions.
+ * .
+ * This implies that when using this flag, the caller must NOT assume that
+ * - a given input frame's opaques will necessarily appear on some output
+ * packet;
+ * - every output packet will have some non-NULL opaque value.
+ * .
+ * When an output packet contains multiple frames, the opaque values will be
+ * taken from the first of those.
+ *
+ * @note
+ * The converse holds for decoders, with frames and packets switched.
+ */
+#define AV_CODEC_FLAG_COPY_OPAQUE (1 << 7)
+/**
+ * Signal to the encoder that the values of AVFrame.duration are valid and
+ * should be used (typically for transferring them to output packets).
+ *
+ * If this flag is not set, frame durations are ignored.
+ */
+#define AV_CODEC_FLAG_FRAME_DURATION (1 << 8)
+/**
+ * Use internal 2pass ratecontrol in first pass mode.
+ */
+#define AV_CODEC_FLAG_PASS1 (1 << 9)
+/**
+ * Use internal 2pass ratecontrol in second pass mode.
+ */
+#define AV_CODEC_FLAG_PASS2 (1 << 10)
+/**
+ * loop filter.
+ */
+#define AV_CODEC_FLAG_LOOP_FILTER (1 << 11)
+/**
+ * Only decode/encode grayscale.
+ */
+#define AV_CODEC_FLAG_GRAY (1 << 13)
+/**
+ * error[?] variables will be set during encoding.
+ */
+#define AV_CODEC_FLAG_PSNR (1 << 15)
+/**
+ * Use interlaced DCT.
+ */
+#define AV_CODEC_FLAG_INTERLACED_DCT (1 << 18)
+/**
+ * Force low delay.
+ */
+#define AV_CODEC_FLAG_LOW_DELAY (1 << 19)
+/**
+ * Place global headers in extradata instead of every keyframe.
+ */
+#define AV_CODEC_FLAG_GLOBAL_HEADER (1 << 22)
+/**
+ * Use only bitexact stuff (except (I)DCT).
+ */
+#define AV_CODEC_FLAG_BITEXACT (1 << 23)
+/* Fx : Flag for H.263+ extra options */
+/**
+ * H.263 advanced intra coding / MPEG-4 AC prediction
+ */
+#define AV_CODEC_FLAG_AC_PRED (1 << 24)
+/**
+ * interlaced motion estimation
+ */
+#define AV_CODEC_FLAG_INTERLACED_ME (1 << 29)
+#define AV_CODEC_FLAG_CLOSED_GOP (1U << 31)
+
+/**
+ * Allow non spec compliant speedup tricks.
+ */
+#define AV_CODEC_FLAG2_FAST (1 << 0)
+/**
+ * Skip bitstream encoding.
+ */
+#define AV_CODEC_FLAG2_NO_OUTPUT (1 << 2)
+/**
+ * Place global headers at every keyframe instead of in extradata.
+ */
+#define AV_CODEC_FLAG2_LOCAL_HEADER (1 << 3)
+
+/**
+ * Input bitstream might be truncated at a packet boundaries
+ * instead of only at frame boundaries.
+ */
+#define AV_CODEC_FLAG2_CHUNKS (1 << 15)
+/**
+ * Discard cropping information from SPS.
+ */
+#define AV_CODEC_FLAG2_IGNORE_CROP (1 << 16)
+
+/**
+ * Show all frames before the first keyframe
+ */
+#define AV_CODEC_FLAG2_SHOW_ALL (1 << 22)
+/**
+ * Export motion vectors through frame side data
+ */
+#define AV_CODEC_FLAG2_EXPORT_MVS (1 << 28)
+/**
+ * Do not skip samples and export skip information as frame side data
+ */
+#define AV_CODEC_FLAG2_SKIP_MANUAL (1 << 29)
+/**
+ * Do not reset ASS ReadOrder field on flush (subtitles decoding)
+ */
+#define AV_CODEC_FLAG2_RO_FLUSH_NOOP (1 << 30)
+/**
+ * Generate/parse ICC profiles on encode/decode, as appropriate for the type of
+ * file. No effect on codecs which cannot contain embedded ICC profiles, or
+ * when compiled without support for lcms2.
+ */
+#define AV_CODEC_FLAG2_ICC_PROFILES (1U << 31)
+
+/* Exported side data.
+ These flags can be passed in AVCodecContext.export_side_data before
+ initialization.
+*/
+/**
+ * Export motion vectors through frame side data
+ */
+#define AV_CODEC_EXPORT_DATA_MVS (1 << 0)
+/**
+ * Export encoder Producer Reference Time through packet side data
+ */
+#define AV_CODEC_EXPORT_DATA_PRFT (1 << 1)
+/**
+ * Decoding only.
+ * Export the AVVideoEncParams structure through frame side data.
+ */
+#define AV_CODEC_EXPORT_DATA_VIDEO_ENC_PARAMS (1 << 2)
+/**
+ * Decoding only.
+ * Do not apply film grain, export it instead.
+ */
+#define AV_CODEC_EXPORT_DATA_FILM_GRAIN (1 << 3)
+
+/**
+ * The decoder will keep a reference to the frame and may reuse it later.
+ */
+#define AV_GET_BUFFER_FLAG_REF (1 << 0)
+
+/**
+ * The encoder will keep a reference to the packet and may reuse it later.
+ */
+#define AV_GET_ENCODE_BUFFER_FLAG_REF (1 << 0)
+
+struct AVCodecInternal;
+
+/**
+ * main external API structure.
+ * New fields can be added to the end with minor version bumps.
+ * Removal, reordering and changes to existing fields require a major
+ * version bump.
+ * You can use AVOptions (av_opt* / av_set/get*()) to access these fields from
+ * user applications. The name string for AVOptions options matches the
+ * associated command line parameter name and can be found in
+ * libavcodec/options_table.h The AVOption/command line parameter names differ
+ * in some cases from the C structure field names for historic reasons or
+ * brevity. sizeof(AVCodecContext) must not be used outside libav*.
+ */
+typedef struct AVCodecContext {
+ /**
+ * information on struct for av_log
+ * - set by avcodec_alloc_context3
+ */
+ const AVClass* av_class;
+ int log_level_offset;
+
+ enum AVMediaType codec_type; /* see AVMEDIA_TYPE_xxx */
+ const struct AVCodec* codec;
+ enum AVCodecID codec_id; /* see AV_CODEC_ID_xxx */
+
+ /**
+ * fourcc (LSB first, so "ABCD" -> ('D'<<24) + ('C'<<16) + ('B'<<8) + 'A').
+ * This is used to work around some encoder bugs.
+ * A demuxer should set this to what is stored in the field used to identify
+ * the codec. If there are multiple such fields in a container then the
+ * demuxer should choose the one which maximizes the information about the
+ * used codec. If the codec tag field in a container is larger than 32 bits
+ * then the demuxer should remap the longer ID to 32 bits with a table or
+ * other structure. Alternatively a new extra_codec_tag + size could be added
+ * but for this a clear advantage must be demonstrated first.
+ * - encoding: Set by user, if not then the default based on codec_id will be
+ * used.
+ * - decoding: Set by user, will be converted to uppercase by libavcodec
+ * during init.
+ */
+ unsigned int codec_tag;
+
+ void* priv_data;
+
+ /**
+ * Private context used for internal data.
+ *
+ * Unlike priv_data, this is not codec-specific. It is used in general
+ * libavcodec functions.
+ */
+ struct AVCodecInternal* internal;
+
+ /**
+ * Private data of the user, can be used to carry app specific stuff.
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ void* opaque;
+
+ /**
+ * the average bitrate
+ * - encoding: Set by user; unused for constant quantizer encoding.
+ * - decoding: Set by user, may be overwritten by libavcodec
+ * if this info is available in the stream
+ */
+ int64_t bit_rate;
+
+ /**
+ * number of bits the bitstream is allowed to diverge from the reference.
+ * the reference can be CBR (for CBR pass1) or VBR (for pass2)
+ * - encoding: Set by user; unused for constant quantizer encoding.
+ * - decoding: unused
+ */
+ int bit_rate_tolerance;
+
+ /**
+ * Global quality for codecs which cannot change it per frame.
+ * This should be proportional to MPEG-1/2/4 qscale.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int global_quality;
+
+ /**
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int compression_level;
+#define FF_COMPRESSION_DEFAULT -1
+
+ /**
+ * AV_CODEC_FLAG_*.
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int flags;
+
+ /**
+ * AV_CODEC_FLAG2_*
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int flags2;
+
+ /**
+ * some codecs need / can use extradata like Huffman tables.
+ * MJPEG: Huffman tables
+ * rv10: additional flags
+ * MPEG-4: global headers (they can be in the bitstream or here)
+ * The allocated memory should be AV_INPUT_BUFFER_PADDING_SIZE bytes larger
+ * than extradata_size to avoid problems if it is read with the bitstream
+ * reader. The bytewise contents of extradata must not depend on the
+ * architecture or CPU endianness. Must be allocated with the av_malloc()
+ * family of functions.
+ * - encoding: Set/allocated/freed by libavcodec.
+ * - decoding: Set/allocated/freed by user.
+ */
+ uint8_t* extradata;
+ int extradata_size;
+
+ /**
+ * This is the fundamental unit of time (in seconds) in terms
+ * of which frame timestamps are represented. For fixed-fps content,
+ * timebase should be 1/framerate and timestamp increments should be
+ * identically 1.
+ * This often, but not always is the inverse of the frame rate or field rate
+ * for video. 1/time_base is not the average frame rate if the frame rate is
+ * not constant.
+ *
+ * Like containers, elementary streams also can store timestamps, 1/time_base
+ * is the unit in which these timestamps are specified.
+ * As example of such codec time base see ISO/IEC 14496-2:2001(E)
+ * vop_time_increment_resolution and fixed_vop_rate
+ * (fixed_vop_rate == 0 implies that it is different from the framerate)
+ *
+ * - encoding: MUST be set by user.
+ * - decoding: unused.
+ */
+ AVRational time_base;
+
+ /**
+ * For some codecs, the time base is closer to the field rate than the frame
+ * rate. Most notably, H.264 and MPEG-2 specify time_base as half of frame
+ * duration if no telecine is used ...
+ *
+ * Set to time_base ticks per frame. Default 1, e.g., H.264/MPEG-2 set it
+ * to 2.
+ */
+ int ticks_per_frame;
+
+ /**
+ * Codec delay.
+ *
+ * Encoding: Number of frames delay there will be from the encoder input to
+ * the decoder output. (we assume the decoder matches the spec)
+ * Decoding: Number of frames delay in addition to what a standard decoder
+ * as specified in the spec would produce.
+ *
+ * Video:
+ * Number of frames the decoded output will be delayed relative to the
+ * encoded input.
+ *
+ * Audio:
+ * For encoding, this field is unused (see initial_padding).
+ *
+ * For decoding, this is the number of samples the decoder needs to
+ * output before the decoder's output is valid. When seeking, you should
+ * start decoding this many samples prior to your desired seek point.
+ *
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by libavcodec.
+ */
+ int delay;
+
+ /* video only */
+ /**
+ * picture width / height.
+ *
+ * @note Those fields may not match the values of the last
+ * AVFrame output by avcodec_receive_frame() due frame
+ * reordering.
+ *
+ * - encoding: MUST be set by user.
+ * - decoding: May be set by the user before opening the decoder if known e.g.
+ * from the container. Some decoders will require the dimensions
+ * to be set by the caller. During decoding, the decoder may
+ * overwrite those values as required while parsing the data.
+ */
+ int width, height;
+
+ /**
+ * Bitstream width / height, may be different from width/height e.g. when
+ * the decoded frame is cropped before being output or lowres is enabled.
+ *
+ * @note Those field may not match the value of the last
+ * AVFrame output by avcodec_receive_frame() due frame
+ * reordering.
+ *
+ * - encoding: unused
+ * - decoding: May be set by the user before opening the decoder if known
+ * e.g. from the container. During decoding, the decoder may
+ * overwrite those values as required while parsing the data.
+ */
+ int coded_width, coded_height;
+
+ /**
+ * the number of pictures in a group of pictures, or 0 for intra_only
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int gop_size;
+
+ /**
+ * Pixel format, see AV_PIX_FMT_xxx.
+ * May be set by the demuxer if known from headers.
+ * May be overridden by the decoder if it knows better.
+ *
+ * @note This field may not match the value of the last
+ * AVFrame output by avcodec_receive_frame() due frame
+ * reordering.
+ *
+ * - encoding: Set by user.
+ * - decoding: Set by user if known, overridden by libavcodec while
+ * parsing the data.
+ */
+ enum AVPixelFormat pix_fmt;
+
+ /**
+ * If non NULL, 'draw_horiz_band' is called by the libavcodec
+ * decoder to draw a horizontal band. It improves cache usage. Not
+ * all codecs can do that. You must check the codec capabilities
+ * beforehand.
+ * When multithreading is used, it may be called from multiple threads
+ * at the same time; threads might draw different parts of the same AVFrame,
+ * or multiple AVFrames, and there is no guarantee that slices will be drawn
+ * in order.
+ * The function is also used by hardware acceleration APIs.
+ * It is called at least once during frame decoding to pass
+ * the data needed for hardware render.
+ * In that mode instead of pixel data, AVFrame points to
+ * a structure specific to the acceleration API. The application
+ * reads the structure and can change some fields to indicate progress
+ * or mark state.
+ * - encoding: unused
+ * - decoding: Set by user.
+ * @param height the height of the slice
+ * @param y the y position of the slice
+ * @param type 1->top field, 2->bottom field, 3->frame
+ * @param offset offset into the AVFrame.data from which the slice should be
+ * read
+ */
+ void (*draw_horiz_band)(struct AVCodecContext* s, const AVFrame* src,
+ int offset[AV_NUM_DATA_POINTERS], int y, int type,
+ int height);
+
+ /**
+ * Callback to negotiate the pixel format. Decoding only, may be set by the
+ * caller before avcodec_open2().
+ *
+ * Called by some decoders to select the pixel format that will be used for
+ * the output frames. This is mainly used to set up hardware acceleration,
+ * then the provided format list contains the corresponding hwaccel pixel
+ * formats alongside the "software" one. The software pixel format may also
+ * be retrieved from \ref sw_pix_fmt.
+ *
+ * This callback will be called when the coded frame properties (such as
+ * resolution, pixel format, etc.) change and more than one output format is
+ * supported for those new properties. If a hardware pixel format is chosen
+ * and initialization for it fails, the callback may be called again
+ * immediately.
+ *
+ * This callback may be called from different threads if the decoder is
+ * multi-threaded, but not from more than one thread simultaneously.
+ *
+ * @param fmt list of formats which may be used in the current
+ * configuration, terminated by AV_PIX_FMT_NONE.
+ * @warning Behavior is undefined if the callback returns a value other
+ * than one of the formats in fmt or AV_PIX_FMT_NONE.
+ * @return the chosen format or AV_PIX_FMT_NONE
+ */
+ enum AVPixelFormat (*get_format)(struct AVCodecContext* s,
+ const enum AVPixelFormat* fmt);
+
+ /**
+ * maximum number of B-frames between non-B-frames
+ * Note: The output will be delayed by max_b_frames+1 relative to the input.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int max_b_frames;
+
+ /**
+ * qscale factor between IP and B-frames
+ * If > 0 then the last P-frame quantizer will be used (q=
+ * lastp_q*factor+offset). If < 0 then normal ratecontrol will be done (q=
+ * -normal_q*factor+offset).
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float b_quant_factor;
+
+ /**
+ * qscale offset between IP and B-frames
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float b_quant_offset;
+
+ /**
+ * Size of the frame reordering buffer in the decoder.
+ * For MPEG-2 it is 1 IPB or 0 low delay IP.
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by libavcodec.
+ */
+ int has_b_frames;
+
+ /**
+ * qscale factor between P- and I-frames
+ * If > 0 then the last P-frame quantizer will be used (q = lastp_q * factor +
+ * offset). If < 0 then normal ratecontrol will be done (q=
+ * -normal_q*factor+offset).
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float i_quant_factor;
+
+ /**
+ * qscale offset between P and I-frames
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float i_quant_offset;
+
+ /**
+ * luminance masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float lumi_masking;
+
+ /**
+ * temporary complexity masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float temporal_cplx_masking;
+
+ /**
+ * spatial complexity masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float spatial_cplx_masking;
+
+ /**
+ * p block masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float p_masking;
+
+ /**
+ * darkness masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float dark_masking;
+
+ /**
+ * slice count
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by user (or 0).
+ */
+ int slice_count;
+
+ /**
+ * slice offsets in the frame in bytes
+ * - encoding: Set/allocated by libavcodec.
+ * - decoding: Set/allocated by user (or NULL).
+ */
+ int* slice_offset;
+
+ /**
+ * sample aspect ratio (0 if unknown)
+ * That is the width of a pixel divided by the height of the pixel.
+ * Numerator and denominator must be relatively prime and smaller than 256 for
+ * some video standards.
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ AVRational sample_aspect_ratio;
+
+ /**
+ * motion estimation comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_cmp;
+ /**
+ * subpixel motion estimation comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_sub_cmp;
+ /**
+ * macroblock comparison function (not supported yet)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mb_cmp;
+ /**
+ * interlaced DCT comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int ildct_cmp;
+#define FF_CMP_SAD 0
+#define FF_CMP_SSE 1
+#define FF_CMP_SATD 2
+#define FF_CMP_DCT 3
+#define FF_CMP_PSNR 4
+#define FF_CMP_BIT 5
+#define FF_CMP_RD 6
+#define FF_CMP_ZERO 7
+#define FF_CMP_VSAD 8
+#define FF_CMP_VSSE 9
+#define FF_CMP_NSSE 10
+#define FF_CMP_W53 11
+#define FF_CMP_W97 12
+#define FF_CMP_DCTMAX 13
+#define FF_CMP_DCT264 14
+#define FF_CMP_MEDIAN_SAD 15
+#define FF_CMP_CHROMA 256
+
+ /**
+ * ME diamond size & shape
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int dia_size;
+
+ /**
+ * amount of previous MV predictors (2a+1 x 2a+1 square)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int last_predictor_count;
+
+ /**
+ * motion estimation prepass comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_pre_cmp;
+
+ /**
+ * ME prepass diamond size & shape
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int pre_dia_size;
+
+ /**
+ * subpel ME quality
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_subpel_quality;
+
+ /**
+ * maximum motion estimation search range in subpel units
+ * If 0 then no limit.
+ *
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_range;
+
+ /**
+ * slice flags
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int slice_flags;
+#define SLICE_FLAG_CODED_ORDER \
+ 0x0001 ///< draw_horiz_band() is called in coded order instead of display
+#define SLICE_FLAG_ALLOW_FIELD \
+ 0x0002 ///< allow draw_horiz_band() with field slices (MPEG-2 field pics)
+#define SLICE_FLAG_ALLOW_PLANE \
+ 0x0004 ///< allow draw_horiz_band() with 1 component at a time (SVQ1)
+
+ /**
+ * macroblock decision mode
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mb_decision;
+#define FF_MB_DECISION_SIMPLE 0 ///< uses mb_cmp
+#define FF_MB_DECISION_BITS 1 ///< chooses the one which needs the fewest bits
+#define FF_MB_DECISION_RD 2 ///< rate distortion
+
+ /**
+ * custom intra quantization matrix
+ * Must be allocated with the av_malloc() family of functions, and will be
+ * freed in avcodec_free_context().
+ * - encoding: Set/allocated by user, freed by libavcodec. Can be NULL.
+ * - decoding: Set/allocated/freed by libavcodec.
+ */
+ uint16_t* intra_matrix;
+
+ /**
+ * custom inter quantization matrix
+ * Must be allocated with the av_malloc() family of functions, and will be
+ * freed in avcodec_free_context().
+ * - encoding: Set/allocated by user, freed by libavcodec. Can be NULL.
+ * - decoding: Set/allocated/freed by libavcodec.
+ */
+ uint16_t* inter_matrix;
+
+ /**
+ * precision of the intra DC coefficient - 8
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec
+ */
+ int intra_dc_precision;
+
+ /**
+ * Number of macroblock rows at the top which are skipped.
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int skip_top;
+
+ /**
+ * Number of macroblock rows at the bottom which are skipped.
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int skip_bottom;
+
+ /**
+ * minimum MB Lagrange multiplier
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mb_lmin;
+
+ /**
+ * maximum MB Lagrange multiplier
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mb_lmax;
+
+ /**
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int bidir_refine;
+
+ /**
+ * minimum GOP size
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int keyint_min;
+
+ /**
+ * number of reference frames
+ * - encoding: Set by user.
+ * - decoding: Set by lavc.
+ */
+ int refs;
+
+ /**
+ * Note: Value depends upon the compare function used for fullpel ME.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mv0_threshold;
+
+ /**
+ * Chromaticity coordinates of the source primaries.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorPrimaries color_primaries;
+
+ /**
+ * Color Transfer Characteristic.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorTransferCharacteristic color_trc;
+
+ /**
+ * YUV colorspace type.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorSpace colorspace;
+
+ /**
+ * MPEG vs JPEG YUV range.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorRange color_range;
+
+ /**
+ * This defines the location of chroma samples.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVChromaLocation chroma_sample_location;
+
+ /**
+ * Number of slices.
+ * Indicates number of picture subdivisions. Used for parallelized
+ * decoding.
+ * - encoding: Set by user
+ * - decoding: unused
+ */
+ int slices;
+
+ /** Field order
+ * - encoding: set by libavcodec
+ * - decoding: Set by user.
+ */
+ enum AVFieldOrder field_order;
+
+ /* audio only */
+ int sample_rate; ///< samples per second
+
+#if FF_API_OLD_CHANNEL_LAYOUT
+ /**
+ * number of audio channels
+ * @deprecated use ch_layout.nb_channels
+ */
+ attribute_deprecated int channels;
+#endif
+
+ /**
+ * audio sample format
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ enum AVSampleFormat sample_fmt; ///< sample format
+
+ /* The following data should not be initialized. */
+ /**
+ * Number of samples per channel in an audio frame.
+ *
+ * - encoding: set by libavcodec in avcodec_open2(). Each submitted frame
+ * except the last must contain exactly frame_size samples per channel.
+ * May be 0 when the codec has AV_CODEC_CAP_VARIABLE_FRAME_SIZE set, then
+ * the frame size is not restricted.
+ * - decoding: may be set by some decoders to indicate constant frame size
+ */
+ int frame_size;
+
+#if FF_API_AVCTX_FRAME_NUMBER
+ /**
+ * Frame counter, set by libavcodec.
+ *
+ * - decoding: total number of frames returned from the decoder so far.
+ * - encoding: total number of frames passed to the encoder so far.
+ *
+ * @note the counter is not incremented if encoding/decoding resulted in
+ * an error.
+ * @deprecated use frame_num instead
+ */
+ attribute_deprecated int frame_number;
+#endif
+
+ /**
+ * number of bytes per packet if constant and known or 0
+ * Used by some WAV based audio codecs.
+ */
+ int block_align;
+
+ /**
+ * Audio cutoff bandwidth (0 means "automatic")
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int cutoff;
+
+#if FF_API_OLD_CHANNEL_LAYOUT
+ /**
+ * Audio channel layout.
+ * - encoding: set by user.
+ * - decoding: set by user, may be overwritten by libavcodec.
+ * @deprecated use ch_layout
+ */
+ attribute_deprecated uint64_t channel_layout;
+
+ /**
+ * Request decoder to use this channel layout if it can (0 for default)
+ * - encoding: unused
+ * - decoding: Set by user.
+ * @deprecated use "downmix" codec private option
+ */
+ attribute_deprecated uint64_t request_channel_layout;
+#endif
+
+ /**
+ * Type of service that the audio stream conveys.
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ enum AVAudioServiceType audio_service_type;
+
+ /**
+ * desired sample format
+ * - encoding: Not used.
+ * - decoding: Set by user.
+ * Decoder will decode to this format if it can.
+ */
+ enum AVSampleFormat request_sample_fmt;
+
+ /**
+ * This callback is called at the beginning of each frame to get data
+ * buffer(s) for it. There may be one contiguous buffer for all the data or
+ * there may be a buffer per each data plane or anything in between. What
+ * this means is, you may set however many entries in buf[] you feel
+ * necessary. Each buffer must be reference-counted using the AVBuffer API
+ * (see description of buf[] below).
+ *
+ * The following fields will be set in the frame before this callback is
+ * called:
+ * - format
+ * - width, height (video only)
+ * - sample_rate, channel_layout, nb_samples (audio only)
+ * Their values may differ from the corresponding values in
+ * AVCodecContext. This callback must use the frame values, not the codec
+ * context values, to calculate the required buffer size.
+ *
+ * This callback must fill the following fields in the frame:
+ * - data[]
+ * - linesize[]
+ * - extended_data:
+ * * if the data is planar audio with more than 8 channels, then this
+ * callback must allocate and fill extended_data to contain all pointers
+ * to all data planes. data[] must hold as many pointers as it can.
+ * extended_data must be allocated with av_malloc() and will be freed in
+ * av_frame_unref().
+ * * otherwise extended_data must point to data
+ * - buf[] must contain one or more pointers to AVBufferRef structures. Each
+ * of the frame's data and extended_data pointers must be contained in these.
+ * That is, one AVBufferRef for each allocated chunk of memory, not
+ * necessarily one AVBufferRef per data[] entry. See: av_buffer_create(),
+ * av_buffer_alloc(), and av_buffer_ref().
+ * - extended_buf and nb_extended_buf must be allocated with av_malloc() by
+ * this callback and filled with the extra buffers if there are more
+ * buffers than buf[] can hold. extended_buf will be freed in
+ * av_frame_unref().
+ *
+ * If AV_CODEC_CAP_DR1 is not set then get_buffer2() must call
+ * avcodec_default_get_buffer2() instead of providing buffers allocated by
+ * some other means.
+ *
+ * Each data plane must be aligned to the maximum required by the target
+ * CPU.
+ *
+ * @see avcodec_default_get_buffer2()
+ *
+ * Video:
+ *
+ * If AV_GET_BUFFER_FLAG_REF is set in flags then the frame may be reused
+ * (read and/or written to if it is writable) later by libavcodec.
+ *
+ * avcodec_align_dimensions2() should be used to find the required width and
+ * height, as they normally need to be rounded up to the next multiple of 16.
+ *
+ * Some decoders do not support linesizes changing between frames.
+ *
+ * If frame multithreading is used, this callback may be called from a
+ * different thread, but not from more than one at once. Does not need to be
+ * reentrant.
+ *
+ * @see avcodec_align_dimensions2()
+ *
+ * Audio:
+ *
+ * Decoders request a buffer of a particular size by setting
+ * AVFrame.nb_samples prior to calling get_buffer2(). The decoder may,
+ * however, utilize only part of the buffer by setting AVFrame.nb_samples
+ * to a smaller value in the output frame.
+ *
+ * As a convenience, av_samples_get_buffer_size() and
+ * av_samples_fill_arrays() in libavutil may be used by custom get_buffer2()
+ * functions to find the required data size and to fill data pointers and
+ * linesize. In AVFrame.linesize, only linesize[0] may be set for audio
+ * since all planes must be the same size.
+ *
+ * @see av_samples_get_buffer_size(), av_samples_fill_arrays()
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec, user can override.
+ */
+ int (*get_buffer2)(struct AVCodecContext* s, AVFrame* frame, int flags);
+
+ /* - encoding parameters */
+ float qcompress; ///< amount of qscale change between easy & hard scenes
+ ///< (0.0-1.0)
+ float qblur; ///< amount of qscale smoothing over time (0.0-1.0)
+
+ /**
+ * minimum quantizer
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int qmin;
+
+ /**
+ * maximum quantizer
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int qmax;
+
+ /**
+ * maximum quantizer difference between frames
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int max_qdiff;
+
+ /**
+ * decoder bitstream buffer size
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int rc_buffer_size;
+
+ /**
+ * ratecontrol override, see RcOverride
+ * - encoding: Allocated/set/freed by user.
+ * - decoding: unused
+ */
+ int rc_override_count;
+ RcOverride* rc_override;
+
+ /**
+ * maximum bitrate
+ * - encoding: Set by user.
+ * - decoding: Set by user, may be overwritten by libavcodec.
+ */
+ int64_t rc_max_rate;
+
+ /**
+ * minimum bitrate
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int64_t rc_min_rate;
+
+ /**
+ * Ratecontrol attempt to use, at maximum, <value> of what can be used without
+ * an underflow.
+ * - encoding: Set by user.
+ * - decoding: unused.
+ */
+ float rc_max_available_vbv_use;
+
+ /**
+ * Ratecontrol attempt to use, at least, <value> times the amount needed to
+ * prevent a vbv overflow.
+ * - encoding: Set by user.
+ * - decoding: unused.
+ */
+ float rc_min_vbv_overflow_use;
+
+ /**
+ * Number of bits which should be loaded into the rc buffer before decoding
+ * starts.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int rc_initial_buffer_occupancy;
+
+ /**
+ * trellis RD quantization
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int trellis;
+
+ /**
+ * pass1 encoding statistics output buffer
+ * - encoding: Set by libavcodec.
+ * - decoding: unused
+ */
+ char* stats_out;
+
+ /**
+ * pass2 encoding statistics input buffer
+ * Concatenated stuff from stats_out of pass1 should be placed here.
+ * - encoding: Allocated/set/freed by user.
+ * - decoding: unused
+ */
+ char* stats_in;
+
+ /**
+ * Work around bugs in encoders which sometimes cannot be detected
+ * automatically.
+ * - encoding: Set by user
+ * - decoding: Set by user
+ */
+ int workaround_bugs;
+#define FF_BUG_AUTODETECT 1 ///< autodetection
+#define FF_BUG_XVID_ILACE 4
+#define FF_BUG_UMP4 8
+#define FF_BUG_NO_PADDING 16
+#define FF_BUG_AMV 32
+#define FF_BUG_QPEL_CHROMA 64
+#define FF_BUG_STD_QPEL 128
+#define FF_BUG_QPEL_CHROMA2 256
+#define FF_BUG_DIRECT_BLOCKSIZE 512
+#define FF_BUG_EDGE 1024
+#define FF_BUG_HPEL_CHROMA 2048
+#define FF_BUG_DC_CLIP 4096
+#define FF_BUG_MS \
+ 8192 ///< Work around various bugs in Microsoft's broken decoders.
+#define FF_BUG_TRUNCATED 16384
+#define FF_BUG_IEDGE 32768
+
+ /**
+ * strictly follow the standard (MPEG-4, ...).
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ * Setting this to STRICT or higher means the encoder and decoder will
+ * generally do stupid things, whereas setting it to unofficial or lower
+ * will mean the encoder might produce output that is not supported by all
+ * spec-compliant decoders. Decoders don't differentiate between normal,
+ * unofficial and experimental (that is, they always try to decode things
+ * when they can) unless they are explicitly asked to behave stupidly
+ * (=strictly conform to the specs)
+ * This may only be set to one of the FF_COMPLIANCE_* values in defs.h.
+ */
+ int strict_std_compliance;
+
+ /**
+ * error concealment flags
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int error_concealment;
+#define FF_EC_GUESS_MVS 1
+#define FF_EC_DEBLOCK 2
+#define FF_EC_FAVOR_INTER 256
+
+ /**
+ * debug
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int debug;
+#define FF_DEBUG_PICT_INFO 1
+#define FF_DEBUG_RC 2
+#define FF_DEBUG_BITSTREAM 4
+#define FF_DEBUG_MB_TYPE 8
+#define FF_DEBUG_QP 16
+#define FF_DEBUG_DCT_COEFF 0x00000040
+#define FF_DEBUG_SKIP 0x00000080
+#define FF_DEBUG_STARTCODE 0x00000100
+#define FF_DEBUG_ER 0x00000400
+#define FF_DEBUG_MMCO 0x00000800
+#define FF_DEBUG_BUGS 0x00001000
+#define FF_DEBUG_BUFFERS 0x00008000
+#define FF_DEBUG_THREADS 0x00010000
+#define FF_DEBUG_GREEN_MD 0x00800000
+#define FF_DEBUG_NOMC 0x01000000
+
+ /**
+ * Error recognition; may misdetect some more or less valid parts as errors.
+ * This is a bitfield of the AV_EF_* values defined in defs.h.
+ *
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int err_recognition;
+
+#if FF_API_REORDERED_OPAQUE
+ /**
+ * opaque 64-bit number (generally a PTS) that will be reordered and
+ * output in AVFrame.reordered_opaque
+ * - encoding: Set by libavcodec to the reordered_opaque of the input
+ * frame corresponding to the last returned packet. Only
+ * supported by encoders with the
+ * AV_CODEC_CAP_ENCODER_REORDERED_OPAQUE capability.
+ * - decoding: Set by user.
+ *
+ * @deprecated Use AV_CODEC_FLAG_COPY_OPAQUE instead
+ */
+ attribute_deprecated int64_t reordered_opaque;
+#endif
+
+ /**
+ * Hardware accelerator in use
+ * - encoding: unused.
+ * - decoding: Set by libavcodec
+ */
+ const struct AVHWAccel* hwaccel;
+
+ /**
+ * Legacy hardware accelerator context.
+ *
+ * For some hardware acceleration methods, the caller may use this field to
+ * signal hwaccel-specific data to the codec. The struct pointed to by this
+ * pointer is hwaccel-dependent and defined in the respective header. Please
+ * refer to the FFmpeg HW accelerator documentation to know how to fill
+ * this.
+ *
+ * In most cases this field is optional - the necessary information may also
+ * be provided to libavcodec through @ref hw_frames_ctx or @ref
+ * hw_device_ctx (see avcodec_get_hw_config()). However, in some cases it
+ * may be the only method of signalling some (optional) information.
+ *
+ * The struct and its contents are owned by the caller.
+ *
+ * - encoding: May be set by the caller before avcodec_open2(). Must remain
+ * valid until avcodec_free_context().
+ * - decoding: May be set by the caller in the get_format() callback.
+ * Must remain valid until the next get_format() call,
+ * or avcodec_free_context() (whichever comes first).
+ */
+ void* hwaccel_context;
+
+ /**
+ * error
+ * - encoding: Set by libavcodec if flags & AV_CODEC_FLAG_PSNR.
+ * - decoding: unused
+ */
+ uint64_t error[AV_NUM_DATA_POINTERS];
+
+ /**
+ * DCT algorithm, see FF_DCT_* below
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int dct_algo;
+#define FF_DCT_AUTO 0
+#define FF_DCT_FASTINT 1
+#define FF_DCT_INT 2
+#define FF_DCT_MMX 3
+#define FF_DCT_ALTIVEC 5
+#define FF_DCT_FAAN 6
+
+ /**
+ * IDCT algorithm, see FF_IDCT_* below.
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int idct_algo;
+#define FF_IDCT_AUTO 0
+#define FF_IDCT_INT 1
+#define FF_IDCT_SIMPLE 2
+#define FF_IDCT_SIMPLEMMX 3
+#define FF_IDCT_ARM 7
+#define FF_IDCT_ALTIVEC 8
+#define FF_IDCT_SIMPLEARM 10
+#define FF_IDCT_XVID 14
+#define FF_IDCT_SIMPLEARMV5TE 16
+#define FF_IDCT_SIMPLEARMV6 17
+#define FF_IDCT_FAAN 20
+#define FF_IDCT_SIMPLENEON 22
+#if FF_API_IDCT_NONE
+// formerly used by xvmc
+# define FF_IDCT_NONE 24
+#endif
+#define FF_IDCT_SIMPLEAUTO 128
+
+ /**
+ * bits per sample/pixel from the demuxer (needed for huffyuv).
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by user.
+ */
+ int bits_per_coded_sample;
+
+ /**
+ * Bits per sample/pixel of internal libavcodec pixel/sample format.
+ * - encoding: set by user.
+ * - decoding: set by libavcodec.
+ */
+ int bits_per_raw_sample;
+
+ /**
+ * low resolution decoding, 1-> 1/2 size, 2->1/4 size
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int lowres;
+
+ /**
+ * thread count
+ * is used to decide how many independent tasks should be passed to execute()
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int thread_count;
+
+ /**
+ * Which multithreading methods to use.
+ * Use of FF_THREAD_FRAME will increase decoding delay by one frame per
+ * thread, so clients which cannot provide future frames should not use it.
+ *
+ * - encoding: Set by user, otherwise the default is used.
+ * - decoding: Set by user, otherwise the default is used.
+ */
+ int thread_type;
+#define FF_THREAD_FRAME 1 ///< Decode more than one frame at once
+#define FF_THREAD_SLICE \
+ 2 ///< Decode more than one part of a single frame at once
+
+ /**
+ * Which multithreading methods are in use by the codec.
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by libavcodec.
+ */
+ int active_thread_type;
+
+ /**
+ * The codec may call this to execute several independent things.
+ * It will return only after finishing all tasks.
+ * The user may replace this with some multithreaded implementation,
+ * the default implementation will execute the parts serially.
+ * @param count the number of things to execute
+ * - encoding: Set by libavcodec, user can override.
+ * - decoding: Set by libavcodec, user can override.
+ */
+ int (*execute)(struct AVCodecContext* c,
+ int (*func)(struct AVCodecContext* c2, void* arg), void* arg2,
+ int* ret, int count, int size);
+
+ /**
+ * The codec may call this to execute several independent things.
+ * It will return only after finishing all tasks.
+ * The user may replace this with some multithreaded implementation,
+ * the default implementation will execute the parts serially.
+ * @param c context passed also to func
+ * @param count the number of things to execute
+ * @param arg2 argument passed unchanged to func
+ * @param ret return values of executed functions, must have space for "count"
+ * values. May be NULL.
+ * @param func function that will be called count times, with jobnr from 0 to
+ * count-1. threadnr will be in the range 0 to c->thread_count-1 < MAX_THREADS
+ * and so that no two instances of func executing at the same time will have
+ * the same threadnr.
+ * @return always 0 currently, but code should handle a future improvement
+ * where when any call to func returns < 0 no further calls to func may be
+ * done and < 0 is returned.
+ * - encoding: Set by libavcodec, user can override.
+ * - decoding: Set by libavcodec, user can override.
+ */
+ int (*execute2)(struct AVCodecContext* c,
+ int (*func)(struct AVCodecContext* c2, void* arg, int jobnr,
+ int threadnr),
+ void* arg2, int* ret, int count);
+
+ /**
+ * noise vs. sse weight for the nsse comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int nsse_weight;
+
+ /**
+ * profile
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int profile;
+#define FF_PROFILE_UNKNOWN -99
+#define FF_PROFILE_RESERVED -100
+
+#define FF_PROFILE_AAC_MAIN 0
+#define FF_PROFILE_AAC_LOW 1
+#define FF_PROFILE_AAC_SSR 2
+#define FF_PROFILE_AAC_LTP 3
+#define FF_PROFILE_AAC_HE 4
+#define FF_PROFILE_AAC_HE_V2 28
+#define FF_PROFILE_AAC_LD 22
+#define FF_PROFILE_AAC_ELD 38
+#define FF_PROFILE_MPEG2_AAC_LOW 128
+#define FF_PROFILE_MPEG2_AAC_HE 131
+
+#define FF_PROFILE_DNXHD 0
+#define FF_PROFILE_DNXHR_LB 1
+#define FF_PROFILE_DNXHR_SQ 2
+#define FF_PROFILE_DNXHR_HQ 3
+#define FF_PROFILE_DNXHR_HQX 4
+#define FF_PROFILE_DNXHR_444 5
+
+#define FF_PROFILE_DTS 20
+#define FF_PROFILE_DTS_ES 30
+#define FF_PROFILE_DTS_96_24 40
+#define FF_PROFILE_DTS_HD_HRA 50
+#define FF_PROFILE_DTS_HD_MA 60
+#define FF_PROFILE_DTS_EXPRESS 70
+
+#define FF_PROFILE_MPEG2_422 0
+#define FF_PROFILE_MPEG2_HIGH 1
+#define FF_PROFILE_MPEG2_SS 2
+#define FF_PROFILE_MPEG2_SNR_SCALABLE 3
+#define FF_PROFILE_MPEG2_MAIN 4
+#define FF_PROFILE_MPEG2_SIMPLE 5
+
+#define FF_PROFILE_H264_CONSTRAINED (1 << 9) // 8+1; constraint_set1_flag
+#define FF_PROFILE_H264_INTRA (1 << 11) // 8+3; constraint_set3_flag
+
+#define FF_PROFILE_H264_BASELINE 66
+#define FF_PROFILE_H264_CONSTRAINED_BASELINE (66 | FF_PROFILE_H264_CONSTRAINED)
+#define FF_PROFILE_H264_MAIN 77
+#define FF_PROFILE_H264_EXTENDED 88
+#define FF_PROFILE_H264_HIGH 100
+#define FF_PROFILE_H264_HIGH_10 110
+#define FF_PROFILE_H264_HIGH_10_INTRA (110 | FF_PROFILE_H264_INTRA)
+#define FF_PROFILE_H264_MULTIVIEW_HIGH 118
+#define FF_PROFILE_H264_HIGH_422 122
+#define FF_PROFILE_H264_HIGH_422_INTRA (122 | FF_PROFILE_H264_INTRA)
+#define FF_PROFILE_H264_STEREO_HIGH 128
+#define FF_PROFILE_H264_HIGH_444 144
+#define FF_PROFILE_H264_HIGH_444_PREDICTIVE 244
+#define FF_PROFILE_H264_HIGH_444_INTRA (244 | FF_PROFILE_H264_INTRA)
+#define FF_PROFILE_H264_CAVLC_444 44
+
+#define FF_PROFILE_VC1_SIMPLE 0
+#define FF_PROFILE_VC1_MAIN 1
+#define FF_PROFILE_VC1_COMPLEX 2
+#define FF_PROFILE_VC1_ADVANCED 3
+
+#define FF_PROFILE_MPEG4_SIMPLE 0
+#define FF_PROFILE_MPEG4_SIMPLE_SCALABLE 1
+#define FF_PROFILE_MPEG4_CORE 2
+#define FF_PROFILE_MPEG4_MAIN 3
+#define FF_PROFILE_MPEG4_N_BIT 4
+#define FF_PROFILE_MPEG4_SCALABLE_TEXTURE 5
+#define FF_PROFILE_MPEG4_SIMPLE_FACE_ANIMATION 6
+#define FF_PROFILE_MPEG4_BASIC_ANIMATED_TEXTURE 7
+#define FF_PROFILE_MPEG4_HYBRID 8
+#define FF_PROFILE_MPEG4_ADVANCED_REAL_TIME 9
+#define FF_PROFILE_MPEG4_CORE_SCALABLE 10
+#define FF_PROFILE_MPEG4_ADVANCED_CODING 11
+#define FF_PROFILE_MPEG4_ADVANCED_CORE 12
+#define FF_PROFILE_MPEG4_ADVANCED_SCALABLE_TEXTURE 13
+#define FF_PROFILE_MPEG4_SIMPLE_STUDIO 14
+#define FF_PROFILE_MPEG4_ADVANCED_SIMPLE 15
+
+#define FF_PROFILE_JPEG2000_CSTREAM_RESTRICTION_0 1
+#define FF_PROFILE_JPEG2000_CSTREAM_RESTRICTION_1 2
+#define FF_PROFILE_JPEG2000_CSTREAM_NO_RESTRICTION 32768
+#define FF_PROFILE_JPEG2000_DCINEMA_2K 3
+#define FF_PROFILE_JPEG2000_DCINEMA_4K 4
+
+#define FF_PROFILE_VP9_0 0
+#define FF_PROFILE_VP9_1 1
+#define FF_PROFILE_VP9_2 2
+#define FF_PROFILE_VP9_3 3
+
+#define FF_PROFILE_HEVC_MAIN 1
+#define FF_PROFILE_HEVC_MAIN_10 2
+#define FF_PROFILE_HEVC_MAIN_STILL_PICTURE 3
+#define FF_PROFILE_HEVC_REXT 4
+#define FF_PROFILE_HEVC_SCC 9
+
+#define FF_PROFILE_VVC_MAIN_10 1
+#define FF_PROFILE_VVC_MAIN_10_444 33
+
+#define FF_PROFILE_AV1_MAIN 0
+#define FF_PROFILE_AV1_HIGH 1
+#define FF_PROFILE_AV1_PROFESSIONAL 2
+
+#define FF_PROFILE_MJPEG_HUFFMAN_BASELINE_DCT 0xc0
+#define FF_PROFILE_MJPEG_HUFFMAN_EXTENDED_SEQUENTIAL_DCT 0xc1
+#define FF_PROFILE_MJPEG_HUFFMAN_PROGRESSIVE_DCT 0xc2
+#define FF_PROFILE_MJPEG_HUFFMAN_LOSSLESS 0xc3
+#define FF_PROFILE_MJPEG_JPEG_LS 0xf7
+
+#define FF_PROFILE_SBC_MSBC 1
+
+#define FF_PROFILE_PRORES_PROXY 0
+#define FF_PROFILE_PRORES_LT 1
+#define FF_PROFILE_PRORES_STANDARD 2
+#define FF_PROFILE_PRORES_HQ 3
+#define FF_PROFILE_PRORES_4444 4
+#define FF_PROFILE_PRORES_XQ 5
+
+#define FF_PROFILE_ARIB_PROFILE_A 0
+#define FF_PROFILE_ARIB_PROFILE_C 1
+
+#define FF_PROFILE_KLVA_SYNC 0
+#define FF_PROFILE_KLVA_ASYNC 1
+
+ /**
+ * level
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int level;
+#define FF_LEVEL_UNKNOWN -99
+
+ /**
+ * Skip loop filtering for selected frames.
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ enum AVDiscard skip_loop_filter;
+
+ /**
+ * Skip IDCT/dequantization for selected frames.
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ enum AVDiscard skip_idct;
+
+ /**
+ * Skip decoding for selected frames.
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ enum AVDiscard skip_frame;
+
+ /**
+ * Header containing style information for text subtitles.
+ * For SUBTITLE_ASS subtitle type, it should contain the whole ASS
+ * [Script Info] and [V4+ Styles] section, plus the [Events] line and
+ * the Format line following. It shouldn't include any Dialogue line.
+ * - encoding: Set/allocated/freed by user (before avcodec_open2())
+ * - decoding: Set/allocated/freed by libavcodec (by avcodec_open2())
+ */
+ uint8_t* subtitle_header;
+ int subtitle_header_size;
+
+ /**
+ * Audio only. The number of "priming" samples (padding) inserted by the
+ * encoder at the beginning of the audio. I.e. this number of leading
+ * decoded samples must be discarded by the caller to get the original audio
+ * without leading padding.
+ *
+ * - decoding: unused
+ * - encoding: Set by libavcodec. The timestamps on the output packets are
+ * adjusted by the encoder so that they always refer to the
+ * first sample of the data actually contained in the packet,
+ * including any added padding. E.g. if the timebase is
+ * 1/samplerate and the timestamp of the first input sample is
+ * 0, the timestamp of the first output packet will be
+ * -initial_padding.
+ */
+ int initial_padding;
+
+ /**
+ * - decoding: For codecs that store a framerate value in the compressed
+ * bitstream, the decoder may export it here. { 0, 1} when
+ * unknown.
+ * - encoding: May be used to signal the framerate of CFR content to an
+ * encoder.
+ */
+ AVRational framerate;
+
+ /**
+ * Nominal unaccelerated pixel format, see AV_PIX_FMT_xxx.
+ * - encoding: unused.
+ * - decoding: Set by libavcodec before calling get_format()
+ */
+ enum AVPixelFormat sw_pix_fmt;
+
+ /**
+ * Timebase in which pkt_dts/pts and AVPacket.dts/pts are.
+ * - encoding unused.
+ * - decoding set by user.
+ */
+ AVRational pkt_timebase;
+
+ /**
+ * AVCodecDescriptor
+ * - encoding: unused.
+ * - decoding: set by libavcodec.
+ */
+ const AVCodecDescriptor* codec_descriptor;
+
+ /**
+ * Current statistics for PTS correction.
+ * - decoding: maintained and used by libavcodec, not intended to be used by
+ * user apps
+ * - encoding: unused
+ */
+ int64_t
+ pts_correction_num_faulty_pts; /// Number of incorrect PTS values so far
+ int64_t
+ pts_correction_num_faulty_dts; /// Number of incorrect DTS values so far
+ int64_t pts_correction_last_pts; /// PTS of the last frame
+ int64_t pts_correction_last_dts; /// DTS of the last frame
+
+ /**
+ * Character encoding of the input subtitles file.
+ * - decoding: set by user
+ * - encoding: unused
+ */
+ char* sub_charenc;
+
+ /**
+ * Subtitles character encoding mode. Formats or codecs might be adjusting
+ * this setting (if they are doing the conversion themselves for instance).
+ * - decoding: set by libavcodec
+ * - encoding: unused
+ */
+ int sub_charenc_mode;
+#define FF_SUB_CHARENC_MODE_DO_NOTHING \
+ -1 ///< do nothing (demuxer outputs a stream supposed to be already in UTF-8,
+ ///< or the codec is bitmap for instance)
+#define FF_SUB_CHARENC_MODE_AUTOMATIC \
+ 0 ///< libavcodec will select the mode itself
+#define FF_SUB_CHARENC_MODE_PRE_DECODER \
+ 1 ///< the AVPacket data needs to be recoded to UTF-8 before being fed to the
+ ///< decoder, requires iconv
+#define FF_SUB_CHARENC_MODE_IGNORE \
+ 2 ///< neither convert the subtitles, nor check them for valid UTF-8
+
+ /**
+ * Skip processing alpha if supported by codec.
+ * Note that if the format uses pre-multiplied alpha (common with VP6,
+ * and recommended due to better video quality/compression)
+ * the image will look as if alpha-blended onto a black background.
+ * However for formats that do not use pre-multiplied alpha
+ * there might be serious artefacts (though e.g. libswscale currently
+ * assumes pre-multiplied alpha anyway).
+ *
+ * - decoding: set by user
+ * - encoding: unused
+ */
+ int skip_alpha;
+
+ /**
+ * Number of samples to skip after a discontinuity
+ * - decoding: unused
+ * - encoding: set by libavcodec
+ */
+ int seek_preroll;
+
+ /**
+ * custom intra quantization matrix
+ * - encoding: Set by user, can be NULL.
+ * - decoding: unused.
+ */
+ uint16_t* chroma_intra_matrix;
+
+ /**
+ * dump format separator.
+ * can be ", " or "\n " or anything else
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ uint8_t* dump_separator;
+
+ /**
+ * ',' separated list of allowed decoders.
+ * If NULL then all are allowed
+ * - encoding: unused
+ * - decoding: set by user
+ */
+ char* codec_whitelist;
+
+ /**
+ * Properties of the stream that gets decoded
+ * - encoding: unused
+ * - decoding: set by libavcodec
+ */
+ unsigned properties;
+#define FF_CODEC_PROPERTY_LOSSLESS 0x00000001
+#define FF_CODEC_PROPERTY_CLOSED_CAPTIONS 0x00000002
+#define FF_CODEC_PROPERTY_FILM_GRAIN 0x00000004
+
+ /**
+ * Additional data associated with the entire coded stream.
+ *
+ * - decoding: unused
+ * - encoding: may be set by libavcodec after avcodec_open2().
+ */
+ AVPacketSideData* coded_side_data;
+ int nb_coded_side_data;
+
+ /**
+ * A reference to the AVHWFramesContext describing the input (for encoding)
+ * or output (decoding) frames. The reference is set by the caller and
+ * afterwards owned (and freed) by libavcodec - it should never be read by
+ * the caller after being set.
+ *
+ * - decoding: This field should be set by the caller from the get_format()
+ * callback. The previous reference (if any) will always be
+ * unreffed by libavcodec before the get_format() call.
+ *
+ * If the default get_buffer2() is used with a hwaccel pixel
+ * format, then this AVHWFramesContext will be used for
+ * allocating the frame buffers.
+ *
+ * - encoding: For hardware encoders configured to use a hwaccel pixel
+ * format, this field should be set by the caller to a reference
+ * to the AVHWFramesContext describing input frames.
+ * AVHWFramesContext.format must be equal to
+ * AVCodecContext.pix_fmt.
+ *
+ * This field should be set before avcodec_open2() is called.
+ */
+ AVBufferRef* hw_frames_ctx;
+
+ /**
+ * Audio only. The amount of padding (in samples) appended by the encoder to
+ * the end of the audio. I.e. this number of decoded samples must be
+ * discarded by the caller from the end of the stream to get the original
+ * audio without any trailing padding.
+ *
+ * - decoding: unused
+ * - encoding: unused
+ */
+ int trailing_padding;
+
+ /**
+ * The number of pixels per image to maximally accept.
+ *
+ * - decoding: set by user
+ * - encoding: set by user
+ */
+ int64_t max_pixels;
+
+ /**
+ * A reference to the AVHWDeviceContext describing the device which will
+ * be used by a hardware encoder/decoder. The reference is set by the
+ * caller and afterwards owned (and freed) by libavcodec.
+ *
+ * This should be used if either the codec device does not require
+ * hardware frames or any that are used are to be allocated internally by
+ * libavcodec. If the user wishes to supply any of the frames used as
+ * encoder input or decoder output then hw_frames_ctx should be used
+ * instead. When hw_frames_ctx is set in get_format() for a decoder, this
+ * field will be ignored while decoding the associated stream segment, but
+ * may again be used on a following one after another get_format() call.
+ *
+ * For both encoders and decoders this field should be set before
+ * avcodec_open2() is called and must not be written to thereafter.
+ *
+ * Note that some decoders may require this field to be set initially in
+ * order to support hw_frames_ctx at all - in that case, all frames
+ * contexts used must be created on the same device.
+ */
+ AVBufferRef* hw_device_ctx;
+
+ /**
+ * Bit set of AV_HWACCEL_FLAG_* flags, which affect hardware accelerated
+ * decoding (if active).
+ * - encoding: unused
+ * - decoding: Set by user (either before avcodec_open2(), or in the
+ * AVCodecContext.get_format callback)
+ */
+ int hwaccel_flags;
+
+ /**
+ * Video decoding only. Certain video codecs support cropping, meaning that
+ * only a sub-rectangle of the decoded frame is intended for display. This
+ * option controls how cropping is handled by libavcodec.
+ *
+ * When set to 1 (the default), libavcodec will apply cropping internally.
+ * I.e. it will modify the output frame width/height fields and offset the
+ * data pointers (only by as much as possible while preserving alignment, or
+ * by the full amount if the AV_CODEC_FLAG_UNALIGNED flag is set) so that
+ * the frames output by the decoder refer only to the cropped area. The
+ * crop_* fields of the output frames will be zero.
+ *
+ * When set to 0, the width/height fields of the output frames will be set
+ * to the coded dimensions and the crop_* fields will describe the cropping
+ * rectangle. Applying the cropping is left to the caller.
+ *
+ * @warning When hardware acceleration with opaque output frames is used,
+ * libavcodec is unable to apply cropping from the top/left border.
+ *
+ * @note when this option is set to zero, the width/height fields of the
+ * AVCodecContext and output AVFrames have different meanings. The codec
+ * context fields store display dimensions (with the coded dimensions in
+ * coded_width/height), while the frame fields store the coded dimensions
+ * (with the display dimensions being determined by the crop_* fields).
+ */
+ int apply_cropping;
+
+ /*
+ * Video decoding only. Sets the number of extra hardware frames which
+ * the decoder will allocate for use by the caller. This must be set
+ * before avcodec_open2() is called.
+ *
+ * Some hardware decoders require all frames that they will use for
+ * output to be defined in advance before decoding starts. For such
+ * decoders, the hardware frame pool must therefore be of a fixed size.
+ * The extra frames set here are on top of any number that the decoder
+ * needs internally in order to operate normally (for example, frames
+ * used as reference pictures).
+ */
+ int extra_hw_frames;
+
+ /**
+ * The percentage of damaged samples to discard a frame.
+ *
+ * - decoding: set by user
+ * - encoding: unused
+ */
+ int discard_damaged_percentage;
+
+ /**
+ * The number of samples per frame to maximally accept.
+ *
+ * - decoding: set by user
+ * - encoding: set by user
+ */
+ int64_t max_samples;
+
+ /**
+ * Bit set of AV_CODEC_EXPORT_DATA_* flags, which affects the kind of
+ * metadata exported in frame, packet, or coded stream side data by
+ * decoders and encoders.
+ *
+ * - decoding: set by user
+ * - encoding: set by user
+ */
+ int export_side_data;
+
+ /**
+ * This callback is called at the beginning of each packet to get a data
+ * buffer for it.
+ *
+ * The following field will be set in the packet before this callback is
+ * called:
+ * - size
+ * This callback must use the above value to calculate the required buffer
+ * size, which must padded by at least AV_INPUT_BUFFER_PADDING_SIZE bytes.
+ *
+ * In some specific cases, the encoder may not use the entire buffer allocated
+ * by this callback. This will be reflected in the size value in the packet
+ * once returned by avcodec_receive_packet().
+ *
+ * This callback must fill the following fields in the packet:
+ * - data: alignment requirements for AVPacket apply, if any. Some
+ * architectures and encoders may benefit from having aligned data.
+ * - buf: must contain a pointer to an AVBufferRef structure. The packet's
+ * data pointer must be contained in it. See: av_buffer_create(),
+ * av_buffer_alloc(), and av_buffer_ref().
+ *
+ * If AV_CODEC_CAP_DR1 is not set then get_encode_buffer() must call
+ * avcodec_default_get_encode_buffer() instead of providing a buffer allocated
+ * by some other means.
+ *
+ * The flags field may contain a combination of AV_GET_ENCODE_BUFFER_FLAG_
+ * flags. They may be used for example to hint what use the buffer may get
+ * after being created. Implementations of this callback may ignore flags they
+ * don't understand. If AV_GET_ENCODE_BUFFER_FLAG_REF is set in flags then the
+ * packet may be reused (read and/or written to if it is writable) later by
+ * libavcodec.
+ *
+ * This callback must be thread-safe, as when frame threading is used, it may
+ * be called from multiple threads simultaneously.
+ *
+ * @see avcodec_default_get_encode_buffer()
+ *
+ * - encoding: Set by libavcodec, user can override.
+ * - decoding: unused
+ */
+ int (*get_encode_buffer)(struct AVCodecContext* s, AVPacket* pkt, int flags);
+
+ /**
+ * Audio channel layout.
+ * - encoding: must be set by the caller, to one of AVCodec.ch_layouts.
+ * - decoding: may be set by the caller if known e.g. from the container.
+ * The decoder can then override during decoding as needed.
+ */
+ AVChannelLayout ch_layout;
+
+ /**
+ * Frame counter, set by libavcodec.
+ *
+ * - decoding: total number of frames returned from the decoder so far.
+ * - encoding: total number of frames passed to the encoder so far.
+ *
+ * @note the counter is not incremented if encoding/decoding resulted in
+ * an error.
+ */
+ int64_t frame_num;
+} AVCodecContext;
+
+/**
+ * @defgroup lavc_hwaccel AVHWAccel
+ *
+ * @note Nothing in this structure should be accessed by the user. At some
+ * point in future it will not be externally visible at all.
+ *
+ * @{
+ */
+typedef struct AVHWAccel {
+ /**
+ * Name of the hardware accelerated codec.
+ * The name is globally unique among encoders and among decoders (but an
+ * encoder and a decoder can share the same name).
+ */
+ const char* name;
+
+ /**
+ * Type of codec implemented by the hardware accelerator.
+ *
+ * See AVMEDIA_TYPE_xxx
+ */
+ enum AVMediaType type;
+
+ /**
+ * Codec implemented by the hardware accelerator.
+ *
+ * See AV_CODEC_ID_xxx
+ */
+ enum AVCodecID id;
+
+ /**
+ * Supported pixel format.
+ *
+ * Only hardware accelerated formats are supported here.
+ */
+ enum AVPixelFormat pix_fmt;
+
+ /**
+ * Hardware accelerated codec capabilities.
+ * see AV_HWACCEL_CODEC_CAP_*
+ */
+ int capabilities;
+
+ /*****************************************************************
+ * No fields below this line are part of the public API. They
+ * may not be used outside of libavcodec and can be changed and
+ * removed at will.
+ * New public fields should be added right above.
+ *****************************************************************
+ */
+
+ /**
+ * Allocate a custom buffer
+ */
+ int (*alloc_frame)(AVCodecContext* avctx, AVFrame* frame);
+
+ /**
+ * Called at the beginning of each frame or field picture.
+ *
+ * Meaningful frame information (codec specific) is guaranteed to
+ * be parsed at this point. This function is mandatory.
+ *
+ * Note that buf can be NULL along with buf_size set to 0.
+ * Otherwise, this means the whole frame is available at this point.
+ *
+ * @param avctx the codec context
+ * @param buf the frame data buffer base
+ * @param buf_size the size of the frame in bytes
+ * @return zero if successful, a negative value otherwise
+ */
+ int (*start_frame)(AVCodecContext* avctx, const uint8_t* buf,
+ uint32_t buf_size);
+
+ /**
+ * Callback for parameter data (SPS/PPS/VPS etc).
+ *
+ * Useful for hardware decoders which keep persistent state about the
+ * video parameters, and need to receive any changes to update that state.
+ *
+ * @param avctx the codec context
+ * @param type the nal unit type
+ * @param buf the nal unit data buffer
+ * @param buf_size the size of the nal unit in bytes
+ * @return zero if successful, a negative value otherwise
+ */
+ int (*decode_params)(AVCodecContext* avctx, int type, const uint8_t* buf,
+ uint32_t buf_size);
+
+ /**
+ * Callback for each slice.
+ *
+ * Meaningful slice information (codec specific) is guaranteed to
+ * be parsed at this point. This function is mandatory.
+ *
+ * @param avctx the codec context
+ * @param buf the slice data buffer base
+ * @param buf_size the size of the slice in bytes
+ * @return zero if successful, a negative value otherwise
+ */
+ int (*decode_slice)(AVCodecContext* avctx, const uint8_t* buf,
+ uint32_t buf_size);
+
+ /**
+ * Called at the end of each frame or field picture.
+ *
+ * The whole picture is parsed at this point and can now be sent
+ * to the hardware accelerator. This function is mandatory.
+ *
+ * @param avctx the codec context
+ * @return zero if successful, a negative value otherwise
+ */
+ int (*end_frame)(AVCodecContext* avctx);
+
+ /**
+ * Size of per-frame hardware accelerator private data.
+ *
+ * Private data is allocated with av_mallocz() before
+ * AVCodecContext.get_buffer() and deallocated after
+ * AVCodecContext.release_buffer().
+ */
+ int frame_priv_data_size;
+
+ /**
+ * Initialize the hwaccel private data.
+ *
+ * This will be called from ff_get_format(), after hwaccel and
+ * hwaccel_context are set and the hwaccel private data in AVCodecInternal
+ * is allocated.
+ */
+ int (*init)(AVCodecContext* avctx);
+
+ /**
+ * Uninitialize the hwaccel private data.
+ *
+ * This will be called from get_format() or avcodec_close(), after hwaccel
+ * and hwaccel_context are already uninitialized.
+ */
+ int (*uninit)(AVCodecContext* avctx);
+
+ /**
+ * Size of the private data to allocate in
+ * AVCodecInternal.hwaccel_priv_data.
+ */
+ int priv_data_size;
+
+ /**
+ * Internal hwaccel capabilities.
+ */
+ int caps_internal;
+
+ /**
+ * Fill the given hw_frames context with current codec parameters. Called
+ * from get_format. Refer to avcodec_get_hw_frames_parameters() for
+ * details.
+ *
+ * This CAN be called before AVHWAccel.init is called, and you must assume
+ * that avctx->hwaccel_priv_data is invalid.
+ */
+ int (*frame_params)(AVCodecContext* avctx, AVBufferRef* hw_frames_ctx);
+} AVHWAccel;
+
+/**
+ * HWAccel is experimental and is thus avoided in favor of non experimental
+ * codecs
+ */
+#define AV_HWACCEL_CODEC_CAP_EXPERIMENTAL 0x0200
+
+/**
+ * Hardware acceleration should be used for decoding even if the codec level
+ * used is unknown or higher than the maximum supported level reported by the
+ * hardware driver.
+ *
+ * It's generally a good idea to pass this flag unless you have a specific
+ * reason not to, as hardware tends to under-report supported levels.
+ */
+#define AV_HWACCEL_FLAG_IGNORE_LEVEL (1 << 0)
+
+/**
+ * Hardware acceleration can output YUV pixel formats with a different chroma
+ * sampling than 4:2:0 and/or other than 8 bits per component.
+ */
+#define AV_HWACCEL_FLAG_ALLOW_HIGH_DEPTH (1 << 1)
+
+/**
+ * Hardware acceleration should still be attempted for decoding when the
+ * codec profile does not match the reported capabilities of the hardware.
+ *
+ * For example, this can be used to try to decode baseline profile H.264
+ * streams in hardware - it will often succeed, because many streams marked
+ * as baseline profile actually conform to constrained baseline profile.
+ *
+ * @warning If the stream is actually not supported then the behaviour is
+ * undefined, and may include returning entirely incorrect output
+ * while indicating success.
+ */
+#define AV_HWACCEL_FLAG_ALLOW_PROFILE_MISMATCH (1 << 2)
+
+/**
+ * Some hardware decoders (namely nvdec) can either output direct decoder
+ * surfaces, or make an on-device copy and return said copy.
+ * There is a hard limit on how many decoder surfaces there can be, and it
+ * cannot be accurately guessed ahead of time.
+ * For some processing chains, this can be okay, but others will run into the
+ * limit and in turn produce very confusing errors that require fine tuning of
+ * more or less obscure options by the user, or in extreme cases cannot be
+ * resolved at all without inserting an avfilter that forces a copy.
+ *
+ * Thus, the hwaccel will by default make a copy for safety and resilience.
+ * If a users really wants to minimize the amount of copies, they can set this
+ * flag and ensure their processing chain does not exhaust the surface pool.
+ */
+#define AV_HWACCEL_FLAG_UNSAFE_OUTPUT (1 << 3)
+
+/**
+ * @}
+ */
+
+enum AVSubtitleType {
+ SUBTITLE_NONE,
+
+ SUBTITLE_BITMAP, ///< A bitmap, pict will be set
+
+ /**
+ * Plain text, the text field must be set by the decoder and is
+ * authoritative. ass and pict fields may contain approximations.
+ */
+ SUBTITLE_TEXT,
+
+ /**
+ * Formatted text, the ass field must be set by the decoder and is
+ * authoritative. pict and text fields may contain approximations.
+ */
+ SUBTITLE_ASS,
+};
+
+#define AV_SUBTITLE_FLAG_FORCED 0x00000001
+
+typedef struct AVSubtitleRect {
+ int x; ///< top left corner of pict, undefined when pict is not set
+ int y; ///< top left corner of pict, undefined when pict is not set
+ int w; ///< width of pict, undefined when pict is not set
+ int h; ///< height of pict, undefined when pict is not set
+ int nb_colors; ///< number of colors in pict, undefined when pict is not set
+
+ /**
+ * data+linesize for the bitmap of this subtitle.
+ * Can be set for text/ass as well once they are rendered.
+ */
+ uint8_t* data[4];
+ int linesize[4];
+
+ enum AVSubtitleType type;
+
+ char* text; ///< 0 terminated plain UTF-8 text
+
+ /**
+ * 0 terminated ASS/SSA compatible event line.
+ * The presentation of this is unaffected by the other values in this
+ * struct.
+ */
+ char* ass;
+
+ int flags;
+} AVSubtitleRect;
+
+typedef struct AVSubtitle {
+ uint16_t format; /* 0 = graphics */
+ uint32_t start_display_time; /* relative to packet pts, in ms */
+ uint32_t end_display_time; /* relative to packet pts, in ms */
+ unsigned num_rects;
+ AVSubtitleRect** rects;
+ int64_t pts; ///< Same as packet pts, in AV_TIME_BASE
+} AVSubtitle;
+
+/**
+ * Return the LIBAVCODEC_VERSION_INT constant.
+ */
+unsigned avcodec_version(void);
+
+/**
+ * Return the libavcodec build-time configuration.
+ */
+const char* avcodec_configuration(void);
+
+/**
+ * Return the libavcodec license.
+ */
+const char* avcodec_license(void);
+
+/**
+ * Allocate an AVCodecContext and set its fields to default values. The
+ * resulting struct should be freed with avcodec_free_context().
+ *
+ * @param codec if non-NULL, allocate private data and initialize defaults
+ * for the given codec. It is illegal to then call avcodec_open2()
+ * with a different codec.
+ * If NULL, then the codec-specific defaults won't be initialized,
+ * which may result in suboptimal default settings (this is
+ * important mainly for encoders, e.g. libx264).
+ *
+ * @return An AVCodecContext filled with default values or NULL on failure.
+ */
+AVCodecContext* avcodec_alloc_context3(const AVCodec* codec);
+
+/**
+ * Free the codec context and everything associated with it and write NULL to
+ * the provided pointer.
+ */
+void avcodec_free_context(AVCodecContext** avctx);
+
+/**
+ * Get the AVClass for AVCodecContext. It can be used in combination with
+ * AV_OPT_SEARCH_FAKE_OBJ for examining options.
+ *
+ * @see av_opt_find().
+ */
+const AVClass* avcodec_get_class(void);
+
+/**
+ * Get the AVClass for AVSubtitleRect. It can be used in combination with
+ * AV_OPT_SEARCH_FAKE_OBJ for examining options.
+ *
+ * @see av_opt_find().
+ */
+const AVClass* avcodec_get_subtitle_rect_class(void);
+
+/**
+ * Fill the parameters struct based on the values from the supplied codec
+ * context. Any allocated fields in par are freed and replaced with duplicates
+ * of the corresponding fields in codec.
+ *
+ * @return >= 0 on success, a negative AVERROR code on failure
+ */
+int avcodec_parameters_from_context(AVCodecParameters* par,
+ const AVCodecContext* codec);
+
+/**
+ * Fill the codec context based on the values from the supplied codec
+ * parameters. Any allocated fields in codec that have a corresponding field in
+ * par are freed and replaced with duplicates of the corresponding field in par.
+ * Fields in codec that do not have a counterpart in par are not touched.
+ *
+ * @return >= 0 on success, a negative AVERROR code on failure.
+ */
+int avcodec_parameters_to_context(AVCodecContext* codec,
+ const AVCodecParameters* par);
+
+/**
+ * Initialize the AVCodecContext to use the given AVCodec. Prior to using this
+ * function the context has to be allocated with avcodec_alloc_context3().
+ *
+ * The functions avcodec_find_decoder_by_name(), avcodec_find_encoder_by_name(),
+ * avcodec_find_decoder() and avcodec_find_encoder() provide an easy way for
+ * retrieving a codec.
+ *
+ * @note Always call this function before using decoding routines (such as
+ * @ref avcodec_receive_frame()).
+ *
+ * @code
+ * av_dict_set(&opts, "b", "2.5M", 0);
+ * codec = avcodec_find_decoder(AV_CODEC_ID_H264);
+ * if (!codec)
+ * exit(1);
+ *
+ * context = avcodec_alloc_context3(codec);
+ *
+ * if (avcodec_open2(context, codec, opts) < 0)
+ * exit(1);
+ * @endcode
+ *
+ * @param avctx The context to initialize.
+ * @param codec The codec to open this context for. If a non-NULL codec has been
+ * previously passed to avcodec_alloc_context3() or
+ * for this context, then this parameter MUST be either NULL or
+ * equal to the previously passed codec.
+ * @param options A dictionary filled with AVCodecContext and codec-private
+ * options. On return this object will be filled with options that were not
+ * found.
+ *
+ * @return zero on success, a negative value on error
+ * @see avcodec_alloc_context3(), avcodec_find_decoder(),
+ * avcodec_find_encoder(), av_dict_set(), av_opt_find().
+ */
+int avcodec_open2(AVCodecContext* avctx, const AVCodec* codec,
+ AVDictionary** options);
+
+/**
+ * Close a given AVCodecContext and free all the data associated with it
+ * (but not the AVCodecContext itself).
+ *
+ * Calling this function on an AVCodecContext that hasn't been opened will free
+ * the codec-specific data allocated in avcodec_alloc_context3() with a non-NULL
+ * codec. Subsequent calls will do nothing.
+ *
+ * @note Do not use this function. Use avcodec_free_context() to destroy a
+ * codec context (either open or closed). Opening and closing a codec context
+ * multiple times is not supported anymore -- use multiple codec contexts
+ * instead.
+ */
+int avcodec_close(AVCodecContext* avctx);
+
+/**
+ * Free all allocated data in the given subtitle struct.
+ *
+ * @param sub AVSubtitle to free.
+ */
+void avsubtitle_free(AVSubtitle* sub);
+
+/**
+ * @}
+ */
+
+/**
+ * @addtogroup lavc_decoding
+ * @{
+ */
+
+/**
+ * The default callback for AVCodecContext.get_buffer2(). It is made public so
+ * it can be called by custom get_buffer2() implementations for decoders without
+ * AV_CODEC_CAP_DR1 set.
+ */
+int avcodec_default_get_buffer2(AVCodecContext* s, AVFrame* frame, int flags);
+
+/**
+ * The default callback for AVCodecContext.get_encode_buffer(). It is made
+ * public so it can be called by custom get_encode_buffer() implementations for
+ * encoders without AV_CODEC_CAP_DR1 set.
+ */
+int avcodec_default_get_encode_buffer(AVCodecContext* s, AVPacket* pkt,
+ int flags);
+
+/**
+ * Modify width and height values so that they will result in a memory
+ * buffer that is acceptable for the codec if you do not use any horizontal
+ * padding.
+ *
+ * May only be used if a codec with AV_CODEC_CAP_DR1 has been opened.
+ */
+void avcodec_align_dimensions(AVCodecContext* s, int* width, int* height);
+
+/**
+ * Modify width and height values so that they will result in a memory
+ * buffer that is acceptable for the codec if you also ensure that all
+ * line sizes are a multiple of the respective linesize_align[i].
+ *
+ * May only be used if a codec with AV_CODEC_CAP_DR1 has been opened.
+ */
+void avcodec_align_dimensions2(AVCodecContext* s, int* width, int* height,
+ int linesize_align[AV_NUM_DATA_POINTERS]);
+
+#ifdef FF_API_AVCODEC_CHROMA_POS
+/**
+ * Converts AVChromaLocation to swscale x/y chroma position.
+ *
+ * The positions represent the chroma (0,0) position in a coordinates system
+ * with luma (0,0) representing the origin and luma(1,1) representing 256,256
+ *
+ * @param xpos horizontal chroma sample position
+ * @param ypos vertical chroma sample position
+ * @deprecated Use av_chroma_location_enum_to_pos() instead.
+ */
+attribute_deprecated int avcodec_enum_to_chroma_pos(int* xpos, int* ypos,
+ enum AVChromaLocation pos);
+
+/**
+ * Converts swscale x/y chroma position to AVChromaLocation.
+ *
+ * The positions represent the chroma (0,0) position in a coordinates system
+ * with luma (0,0) representing the origin and luma(1,1) representing 256,256
+ *
+ * @param xpos horizontal chroma sample position
+ * @param ypos vertical chroma sample position
+ * @deprecated Use av_chroma_location_pos_to_enum() instead.
+ */
+attribute_deprecated enum AVChromaLocation avcodec_chroma_pos_to_enum(int xpos,
+ int ypos);
+#endif
+
+/**
+ * Decode a subtitle message.
+ * Return a negative value on error, otherwise return the number of bytes used.
+ * If no subtitle could be decompressed, got_sub_ptr is zero.
+ * Otherwise, the subtitle is stored in *sub.
+ * Note that AV_CODEC_CAP_DR1 is not available for subtitle codecs. This is for
+ * simplicity, because the performance difference is expected to be negligible
+ * and reusing a get_buffer written for video codecs would probably perform
+ * badly due to a potentially very different allocation pattern.
+ *
+ * Some decoders (those marked with AV_CODEC_CAP_DELAY) have a delay between
+ * input and output. This means that for some packets they will not immediately
+ * produce decoded output and need to be flushed at the end of decoding to get
+ * all the decoded data. Flushing is done by calling this function with packets
+ * with avpkt->data set to NULL and avpkt->size set to 0 until it stops
+ * returning subtitles. It is safe to flush even those decoders that are not
+ * marked with AV_CODEC_CAP_DELAY, then no subtitles will be returned.
+ *
+ * @note The AVCodecContext MUST have been opened with @ref avcodec_open2()
+ * before packets may be fed to the decoder.
+ *
+ * @param avctx the codec context
+ * @param[out] sub The preallocated AVSubtitle in which the decoded subtitle
+ * will be stored, must be freed with avsubtitle_free if *got_sub_ptr is set.
+ * @param[in,out] got_sub_ptr Zero if no subtitle could be decompressed,
+ * otherwise, it is nonzero.
+ * @param[in] avpkt The input AVPacket containing the input buffer.
+ */
+int avcodec_decode_subtitle2(AVCodecContext* avctx, AVSubtitle* sub,
+ int* got_sub_ptr, const AVPacket* avpkt);
+
+/**
+ * Supply raw packet data as input to a decoder.
+ *
+ * Internally, this call will copy relevant AVCodecContext fields, which can
+ * influence decoding per-packet, and apply them when the packet is actually
+ * decoded. (For example AVCodecContext.skip_frame, which might direct the
+ * decoder to drop the frame contained by the packet sent with this function.)
+ *
+ * @warning The input buffer, avpkt->data must be AV_INPUT_BUFFER_PADDING_SIZE
+ * larger than the actual read bytes because some optimized bitstream
+ * readers read 32 or 64 bits at once and could read over the end.
+ *
+ * @note The AVCodecContext MUST have been opened with @ref avcodec_open2()
+ * before packets may be fed to the decoder.
+ *
+ * @param avctx codec context
+ * @param[in] avpkt The input AVPacket. Usually, this will be a single video
+ * frame, or several complete audio frames.
+ * Ownership of the packet remains with the caller, and the
+ * decoder will not write to the packet. The decoder may create
+ * a reference to the packet data (or copy it if the packet is
+ * not reference-counted).
+ * Unlike with older APIs, the packet is always fully consumed,
+ * and if it contains multiple frames (e.g. some audio codecs),
+ * will require you to call avcodec_receive_frame() multiple
+ * times afterwards before you can send a new packet.
+ * It can be NULL (or an AVPacket with data set to NULL and
+ * size set to 0); in this case, it is considered a flush
+ * packet, which signals the end of the stream. Sending the
+ * first flush packet will return success. Subsequent ones are
+ * unnecessary and will return AVERROR_EOF. If the decoder
+ * still has frames buffered, it will return them after sending
+ * a flush packet.
+ *
+ * @retval 0 success
+ * @retval AVERROR(EAGAIN) input is not accepted in the current state - user
+ * must read output with avcodec_receive_frame() (once
+ * all output is read, the packet should be resent,
+ * and the call will not fail with EAGAIN).
+ * @retval AVERROR_EOF the decoder has been flushed, and no new packets
+ * can be sent to it (also returned if more than 1 flush packet is sent)
+ * @retval AVERROR(EINVAL) codec not opened, it is an encoder, or requires
+ * flush
+ * @retval AVERROR(ENOMEM) failed to add packet to internal queue, or similar
+ * @retval "another negative error code" legitimate decoding errors
+ */
+int avcodec_send_packet(AVCodecContext* avctx, const AVPacket* avpkt);
+
+/**
+ * Return decoded output data from a decoder or encoder (when the
+ * AV_CODEC_FLAG_RECON_FRAME flag is used).
+ *
+ * @param avctx codec context
+ * @param frame This will be set to a reference-counted video or audio
+ * frame (depending on the decoder type) allocated by the
+ * codec. Note that the function will always call
+ * av_frame_unref(frame) before doing anything else.
+ *
+ * @retval 0 success, a frame was returned
+ * @retval AVERROR(EAGAIN) output is not available in this state - user must
+ * try to send new input
+ * @retval AVERROR_EOF the codec has been fully flushed, and there will be
+ * no more output frames
+ * @retval AVERROR(EINVAL) codec not opened, or it is an encoder without the
+ * AV_CODEC_FLAG_RECON_FRAME flag enabled
+ * @retval AVERROR_INPUT_CHANGED current decoded frame has changed parameters
+ * with respect to first decoded frame. Applicable when flag
+ * AV_CODEC_FLAG_DROPCHANGED is set.
+ * @retval "other negative error code" legitimate decoding errors
+ */
+int avcodec_receive_frame(AVCodecContext* avctx, AVFrame* frame);
+
+/**
+ * Supply a raw video or audio frame to the encoder. Use
+ * avcodec_receive_packet() to retrieve buffered output packets.
+ *
+ * @param avctx codec context
+ * @param[in] frame AVFrame containing the raw audio or video frame to be
+ * encoded. Ownership of the frame remains with the caller, and the encoder will
+ * not write to the frame. The encoder may create a reference to the frame data
+ * (or copy it if the frame is not reference-counted). It can be NULL, in which
+ * case it is considered a flush packet. This signals the end of the stream. If
+ * the encoder still has packets buffered, it will return them after this call.
+ * Once flushing mode has been entered, additional flush packets are ignored,
+ * and sending frames will return AVERROR_EOF.
+ *
+ * For audio:
+ * If AV_CODEC_CAP_VARIABLE_FRAME_SIZE is set, then each frame
+ * can have any number of samples.
+ * If it is not set, frame->nb_samples must be equal to
+ * avctx->frame_size for all frames except the last.
+ * The final frame may be smaller than avctx->frame_size.
+ * @retval 0 success
+ * @retval AVERROR(EAGAIN) input is not accepted in the current state - user
+ * must read output with avcodec_receive_packet() (once all output is read, the
+ * packet should be resent, and the call will not fail with EAGAIN).
+ * @retval AVERROR_EOF the encoder has been flushed, and no new frames can
+ * be sent to it
+ * @retval AVERROR(EINVAL) codec not opened, it is a decoder, or requires
+ * flush
+ * @retval AVERROR(ENOMEM) failed to add packet to internal queue, or similar
+ * @retval "another negative error code" legitimate encoding errors
+ */
+int avcodec_send_frame(AVCodecContext* avctx, const AVFrame* frame);
+
+/**
+ * Read encoded data from the encoder.
+ *
+ * @param avctx codec context
+ * @param avpkt This will be set to a reference-counted packet allocated by the
+ * encoder. Note that the function will always call
+ * av_packet_unref(avpkt) before doing anything else.
+ * @retval 0 success
+ * @retval AVERROR(EAGAIN) output is not available in the current state - user
+ * must try to send input
+ * @retval AVERROR_EOF the encoder has been fully flushed, and there will be
+ * no more output packets
+ * @retval AVERROR(EINVAL) codec not opened, or it is a decoder
+ * @retval "another negative error code" legitimate encoding errors
+ */
+int avcodec_receive_packet(AVCodecContext* avctx, AVPacket* avpkt);
+
+/**
+ * Create and return a AVHWFramesContext with values adequate for hardware
+ * decoding. This is meant to get called from the get_format callback, and is
+ * a helper for preparing a AVHWFramesContext for AVCodecContext.hw_frames_ctx.
+ * This API is for decoding with certain hardware acceleration modes/APIs only.
+ *
+ * The returned AVHWFramesContext is not initialized. The caller must do this
+ * with av_hwframe_ctx_init().
+ *
+ * Calling this function is not a requirement, but makes it simpler to avoid
+ * codec or hardware API specific details when manually allocating frames.
+ *
+ * Alternatively to this, an API user can set AVCodecContext.hw_device_ctx,
+ * which sets up AVCodecContext.hw_frames_ctx fully automatically, and makes
+ * it unnecessary to call this function or having to care about
+ * AVHWFramesContext initialization at all.
+ *
+ * There are a number of requirements for calling this function:
+ *
+ * - It must be called from get_format with the same avctx parameter that was
+ * passed to get_format. Calling it outside of get_format is not allowed, and
+ * can trigger undefined behavior.
+ * - The function is not always supported (see description of return values).
+ * Even if this function returns successfully, hwaccel initialization could
+ * fail later. (The degree to which implementations check whether the stream
+ * is actually supported varies. Some do this check only after the user's
+ * get_format callback returns.)
+ * - The hw_pix_fmt must be one of the choices suggested by get_format. If the
+ * user decides to use a AVHWFramesContext prepared with this API function,
+ * the user must return the same hw_pix_fmt from get_format.
+ * - The device_ref passed to this function must support the given hw_pix_fmt.
+ * - After calling this API function, it is the user's responsibility to
+ * initialize the AVHWFramesContext (returned by the out_frames_ref
+ * parameter), and to set AVCodecContext.hw_frames_ctx to it. If done, this must
+ * be done before returning from get_format (this is implied by the normal
+ * AVCodecContext.hw_frames_ctx API rules).
+ * - The AVHWFramesContext parameters may change every time time get_format is
+ * called. Also, AVCodecContext.hw_frames_ctx is reset before get_format. So
+ * you are inherently required to go through this process again on every
+ * get_format call.
+ * - It is perfectly possible to call this function without actually using
+ * the resulting AVHWFramesContext. One use-case might be trying to reuse a
+ * previously initialized AVHWFramesContext, and calling this API function
+ * only to test whether the required frame parameters have changed.
+ * - Fields that use dynamically allocated values of any kind must not be set
+ * by the user unless setting them is explicitly allowed by the documentation.
+ * If the user sets AVHWFramesContext.free and AVHWFramesContext.user_opaque,
+ * the new free callback must call the potentially set previous free callback.
+ * This API call may set any dynamically allocated fields, including the free
+ * callback.
+ *
+ * The function will set at least the following fields on AVHWFramesContext
+ * (potentially more, depending on hwaccel API):
+ *
+ * - All fields set by av_hwframe_ctx_alloc().
+ * - Set the format field to hw_pix_fmt.
+ * - Set the sw_format field to the most suited and most versatile format. (An
+ * implication is that this will prefer generic formats over opaque formats
+ * with arbitrary restrictions, if possible.)
+ * - Set the width/height fields to the coded frame size, rounded up to the
+ * API-specific minimum alignment.
+ * - Only _if_ the hwaccel requires a pre-allocated pool: set the
+ * initial_pool_size field to the number of maximum reference surfaces possible
+ * with the codec, plus 1 surface for the user to work (meaning the user can
+ * safely reference at most 1 decoded surface at a time), plus additional
+ * buffering introduced by frame threading. If the hwaccel does not require
+ * pre-allocation, the field is left to 0, and the decoder will allocate new
+ * surfaces on demand during decoding.
+ * - Possibly AVHWFramesContext.hwctx fields, depending on the underlying
+ * hardware API.
+ *
+ * Essentially, out_frames_ref returns the same as av_hwframe_ctx_alloc(), but
+ * with basic frame parameters set.
+ *
+ * The function is stateless, and does not change the AVCodecContext or the
+ * device_ref AVHWDeviceContext.
+ *
+ * @param avctx The context which is currently calling get_format, and which
+ * implicitly contains all state needed for filling the returned
+ * AVHWFramesContext properly.
+ * @param device_ref A reference to the AVHWDeviceContext describing the device
+ * which will be used by the hardware decoder.
+ * @param hw_pix_fmt The hwaccel format you are going to return from get_format.
+ * @param out_frames_ref On success, set to a reference to an _uninitialized_
+ * AVHWFramesContext, created from the given device_ref.
+ * Fields will be set to values required for decoding.
+ * Not changed if an error is returned.
+ * @return zero on success, a negative value on error. The following error codes
+ * have special semantics:
+ * AVERROR(ENOENT): the decoder does not support this functionality. Setup
+ * is always manual, or it is a decoder which does not
+ * support setting AVCodecContext.hw_frames_ctx at all,
+ * or it is a software format.
+ * AVERROR(EINVAL): it is known that hardware decoding is not supported for
+ * this configuration, or the device_ref is not supported
+ * for the hwaccel referenced by hw_pix_fmt.
+ */
+int avcodec_get_hw_frames_parameters(AVCodecContext* avctx,
+ AVBufferRef* device_ref,
+ enum AVPixelFormat hw_pix_fmt,
+ AVBufferRef** out_frames_ref);
+
+/**
+ * @defgroup lavc_parsing Frame parsing
+ * @{
+ */
+
+enum AVPictureStructure {
+ AV_PICTURE_STRUCTURE_UNKNOWN, ///< unknown
+ AV_PICTURE_STRUCTURE_TOP_FIELD, ///< coded as top field
+ AV_PICTURE_STRUCTURE_BOTTOM_FIELD, ///< coded as bottom field
+ AV_PICTURE_STRUCTURE_FRAME, ///< coded as frame
+};
+
+typedef struct AVCodecParserContext {
+ void* priv_data;
+ const struct AVCodecParser* parser;
+ int64_t frame_offset; /* offset of the current frame */
+ int64_t cur_offset; /* current offset
+ (incremented by each av_parser_parse()) */
+ int64_t next_frame_offset; /* offset of the next frame */
+ /* video info */
+ int pict_type; /* XXX: Put it back in AVCodecContext. */
+ /**
+ * This field is used for proper frame duration computation in lavf.
+ * It signals, how much longer the frame duration of the current frame
+ * is compared to normal frame duration.
+ *
+ * frame_duration = (1 + repeat_pict) * time_base
+ *
+ * It is used by codecs like H.264 to display telecined material.
+ */
+ int repeat_pict; /* XXX: Put it back in AVCodecContext. */
+ int64_t pts; /* pts of the current frame */
+ int64_t dts; /* dts of the current frame */
+
+ /* private data */
+ int64_t last_pts;
+ int64_t last_dts;
+ int fetch_timestamp;
+
+#define AV_PARSER_PTS_NB 4
+ int cur_frame_start_index;
+ int64_t cur_frame_offset[AV_PARSER_PTS_NB];
+ int64_t cur_frame_pts[AV_PARSER_PTS_NB];
+ int64_t cur_frame_dts[AV_PARSER_PTS_NB];
+
+ int flags;
+#define PARSER_FLAG_COMPLETE_FRAMES 0x0001
+#define PARSER_FLAG_ONCE 0x0002
+/// Set if the parser has a valid file offset
+#define PARSER_FLAG_FETCHED_OFFSET 0x0004
+#define PARSER_FLAG_USE_CODEC_TS 0x1000
+
+ int64_t offset; ///< byte offset from starting packet start
+ int64_t cur_frame_end[AV_PARSER_PTS_NB];
+
+ /**
+ * Set by parser to 1 for key frames and 0 for non-key frames.
+ * It is initialized to -1, so if the parser doesn't set this flag,
+ * old-style fallback using AV_PICTURE_TYPE_I picture type as key frames
+ * will be used.
+ */
+ int key_frame;
+
+ // Timestamp generation support:
+ /**
+ * Synchronization point for start of timestamp generation.
+ *
+ * Set to >0 for sync point, 0 for no sync point and <0 for undefined
+ * (default).
+ *
+ * For example, this corresponds to presence of H.264 buffering period
+ * SEI message.
+ */
+ int dts_sync_point;
+
+ /**
+ * Offset of the current timestamp against last timestamp sync point in
+ * units of AVCodecContext.time_base.
+ *
+ * Set to INT_MIN when dts_sync_point unused. Otherwise, it must
+ * contain a valid timestamp offset.
+ *
+ * Note that the timestamp of sync point has usually a nonzero
+ * dts_ref_dts_delta, which refers to the previous sync point. Offset of
+ * the next frame after timestamp sync point will be usually 1.
+ *
+ * For example, this corresponds to H.264 cpb_removal_delay.
+ */
+ int dts_ref_dts_delta;
+
+ /**
+ * Presentation delay of current frame in units of AVCodecContext.time_base.
+ *
+ * Set to INT_MIN when dts_sync_point unused. Otherwise, it must
+ * contain valid non-negative timestamp delta (presentation time of a frame
+ * must not lie in the past).
+ *
+ * This delay represents the difference between decoding and presentation
+ * time of the frame.
+ *
+ * For example, this corresponds to H.264 dpb_output_delay.
+ */
+ int pts_dts_delta;
+
+ /**
+ * Position of the packet in file.
+ *
+ * Analogous to cur_frame_pts/dts
+ */
+ int64_t cur_frame_pos[AV_PARSER_PTS_NB];
+
+ /**
+ * Byte position of currently parsed frame in stream.
+ */
+ int64_t pos;
+
+ /**
+ * Previous frame byte position.
+ */
+ int64_t last_pos;
+
+ /**
+ * Duration of the current frame.
+ * For audio, this is in units of 1 / AVCodecContext.sample_rate.
+ * For all other types, this is in units of AVCodecContext.time_base.
+ */
+ int duration;
+
+ enum AVFieldOrder field_order;
+
+ /**
+ * Indicate whether a picture is coded as a frame, top field or bottom field.
+ *
+ * For example, H.264 field_pic_flag equal to 0 corresponds to
+ * AV_PICTURE_STRUCTURE_FRAME. An H.264 picture with field_pic_flag
+ * equal to 1 and bottom_field_flag equal to 0 corresponds to
+ * AV_PICTURE_STRUCTURE_TOP_FIELD.
+ */
+ enum AVPictureStructure picture_structure;
+
+ /**
+ * Picture number incremented in presentation or output order.
+ * This field may be reinitialized at the first picture of a new sequence.
+ *
+ * For example, this corresponds to H.264 PicOrderCnt.
+ */
+ int output_picture_number;
+
+ /**
+ * Dimensions of the decoded video intended for presentation.
+ */
+ int width;
+ int height;
+
+ /**
+ * Dimensions of the coded video.
+ */
+ int coded_width;
+ int coded_height;
+
+ /**
+ * The format of the coded data, corresponds to enum AVPixelFormat for video
+ * and for enum AVSampleFormat for audio.
+ *
+ * Note that a decoder can have considerable freedom in how exactly it
+ * decodes the data, so the format reported here might be different from the
+ * one returned by a decoder.
+ */
+ int format;
+} AVCodecParserContext;
+
+typedef struct AVCodecParser {
+ int codec_ids[7]; /* several codec IDs are permitted */
+ int priv_data_size;
+ int (*parser_init)(AVCodecParserContext* s);
+ /* This callback never returns an error, a negative value means that
+ * the frame start was in a previous packet. */
+ int (*parser_parse)(AVCodecParserContext* s, AVCodecContext* avctx,
+ const uint8_t** poutbuf, int* poutbuf_size,
+ const uint8_t* buf, int buf_size);
+ void (*parser_close)(AVCodecParserContext* s);
+ int (*split)(AVCodecContext* avctx, const uint8_t* buf, int buf_size);
+} AVCodecParser;
+
+/**
+ * Iterate over all registered codec parsers.
+ *
+ * @param opaque a pointer where libavcodec will store the iteration state. Must
+ * point to NULL to start the iteration.
+ *
+ * @return the next registered codec parser or NULL when the iteration is
+ * finished
+ */
+const AVCodecParser* av_parser_iterate(void** opaque);
+
+AVCodecParserContext* av_parser_init(int codec_id);
+
+/**
+ * Parse a packet.
+ *
+ * @param s parser context.
+ * @param avctx codec context.
+ * @param poutbuf set to pointer to parsed buffer or NULL if not yet
+ finished.
+ * @param poutbuf_size set to size of parsed buffer or zero if not yet
+ finished.
+ * @param buf input buffer.
+ * @param buf_size buffer size in bytes without the padding. I.e. the full
+ buffer size is assumed to be buf_size + AV_INPUT_BUFFER_PADDING_SIZE. To signal
+ EOF, this should be 0 (so that the last frame can be output).
+ * @param pts input presentation timestamp.
+ * @param dts input decoding timestamp.
+ * @param pos input byte position in stream.
+ * @return the number of bytes of the input bitstream used.
+ *
+ * Example:
+ * @code
+ * while(in_len){
+ * len = av_parser_parse2(myparser, AVCodecContext, &data, &size,
+ * in_data, in_len,
+ * pts, dts, pos);
+ * in_data += len;
+ * in_len -= len;
+ *
+ * if(size)
+ * decode_frame(data, size);
+ * }
+ * @endcode
+ */
+int av_parser_parse2(AVCodecParserContext* s, AVCodecContext* avctx,
+ uint8_t** poutbuf, int* poutbuf_size, const uint8_t* buf,
+ int buf_size, int64_t pts, int64_t dts, int64_t pos);
+
+void av_parser_close(AVCodecParserContext* s);
+
+/**
+ * @}
+ * @}
+ */
+
+/**
+ * @addtogroup lavc_encoding
+ * @{
+ */
+
+int avcodec_encode_subtitle(AVCodecContext* avctx, uint8_t* buf, int buf_size,
+ const AVSubtitle* sub);
+
+/**
+ * @}
+ */
+
+/**
+ * @defgroup lavc_misc Utility functions
+ * @ingroup libavc
+ *
+ * Miscellaneous utility functions related to both encoding and decoding
+ * (or neither).
+ * @{
+ */
+
+/**
+ * @defgroup lavc_misc_pixfmt Pixel formats
+ *
+ * Functions for working with pixel formats.
+ * @{
+ */
+
+/**
+ * Return a value representing the fourCC code associated to the
+ * pixel format pix_fmt, or 0 if no associated fourCC code can be
+ * found.
+ */
+unsigned int avcodec_pix_fmt_to_codec_tag(enum AVPixelFormat pix_fmt);
+
+/**
+ * Find the best pixel format to convert to given a certain source pixel
+ * format. When converting from one pixel format to another, information loss
+ * may occur. For example, when converting from RGB24 to GRAY, the color
+ * information will be lost. Similarly, other losses occur when converting from
+ * some formats to other formats. avcodec_find_best_pix_fmt_of_2() searches
+ * which of the given pixel formats should be used to suffer the least amount of
+ * loss. The pixel formats from which it chooses one, are determined by the
+ * pix_fmt_list parameter.
+ *
+ *
+ * @param[in] pix_fmt_list AV_PIX_FMT_NONE terminated array of pixel formats to
+ * choose from
+ * @param[in] src_pix_fmt source pixel format
+ * @param[in] has_alpha Whether the source pixel format alpha channel is used.
+ * @param[out] loss_ptr Combination of flags informing you what kind of losses
+ * will occur.
+ * @return The best pixel format to convert to or -1 if none was found.
+ */
+enum AVPixelFormat avcodec_find_best_pix_fmt_of_list(
+ const enum AVPixelFormat* pix_fmt_list, enum AVPixelFormat src_pix_fmt,
+ int has_alpha, int* loss_ptr);
+
+enum AVPixelFormat avcodec_default_get_format(struct AVCodecContext* s,
+ const enum AVPixelFormat* fmt);
+
+/**
+ * @}
+ */
+
+void avcodec_string(char* buf, int buf_size, AVCodecContext* enc, int encode);
+
+int avcodec_default_execute(AVCodecContext* c,
+ int (*func)(AVCodecContext* c2, void* arg2),
+ void* arg, int* ret, int count, int size);
+int avcodec_default_execute2(AVCodecContext* c,
+ int (*func)(AVCodecContext* c2, void* arg2, int,
+ int),
+ void* arg, int* ret, int count);
+// FIXME func typedef
+
+/**
+ * Fill AVFrame audio data and linesize pointers.
+ *
+ * The buffer buf must be a preallocated buffer with a size big enough
+ * to contain the specified samples amount. The filled AVFrame data
+ * pointers will point to this buffer.
+ *
+ * AVFrame extended_data channel pointers are allocated if necessary for
+ * planar audio.
+ *
+ * @param frame the AVFrame
+ * frame->nb_samples must be set prior to calling the
+ * function. This function fills in frame->data,
+ * frame->extended_data, frame->linesize[0].
+ * @param nb_channels channel count
+ * @param sample_fmt sample format
+ * @param buf buffer to use for frame data
+ * @param buf_size size of buffer
+ * @param align plane size sample alignment (0 = default)
+ * @return >=0 on success, negative error code on failure
+ * @todo return the size in bytes required to store the samples in
+ * case of success, at the next libavutil bump
+ */
+int avcodec_fill_audio_frame(AVFrame* frame, int nb_channels,
+ enum AVSampleFormat sample_fmt, const uint8_t* buf,
+ int buf_size, int align);
+
+/**
+ * Reset the internal codec state / flush internal buffers. Should be called
+ * e.g. when seeking or when switching to a different stream.
+ *
+ * @note for decoders, this function just releases any references the decoder
+ * might keep internally, but the caller's references remain valid.
+ *
+ * @note for encoders, this function will only do something if the encoder
+ * declares support for AV_CODEC_CAP_ENCODER_FLUSH. When called, the encoder
+ * will drain any remaining packets, and can then be re-used for a different
+ * stream (as opposed to sending a null frame which will leave the encoder
+ * in a permanent EOF state after draining). This can be desirable if the
+ * cost of tearing down and replacing the encoder instance is high.
+ */
+void avcodec_flush_buffers(AVCodecContext* avctx);
+
+/**
+ * Return audio frame duration.
+ *
+ * @param avctx codec context
+ * @param frame_bytes size of the frame, or 0 if unknown
+ * @return frame duration, in samples, if known. 0 if not able to
+ * determine.
+ */
+int av_get_audio_frame_duration(AVCodecContext* avctx, int frame_bytes);
+
+/* memory */
+
+/**
+ * Same behaviour av_fast_malloc but the buffer has additional
+ * AV_INPUT_BUFFER_PADDING_SIZE at the end which will always be 0.
+ *
+ * In addition the whole buffer will initially and after resizes
+ * be 0-initialized so that no uninitialized data will ever appear.
+ */
+void av_fast_padded_malloc(void* ptr, unsigned int* size, size_t min_size);
+
+/**
+ * Same behaviour av_fast_padded_malloc except that buffer will always
+ * be 0-initialized after call.
+ */
+void av_fast_padded_mallocz(void* ptr, unsigned int* size, size_t min_size);
+
+/**
+ * @return a positive value if s is open (i.e. avcodec_open2() was called on it
+ * with no corresponding avcodec_close()), 0 otherwise.
+ */
+int avcodec_is_open(AVCodecContext* s);
+
+/**
+ * @}
+ */
+
+#endif /* AVCODEC_AVCODEC_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/avdct.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/avdct.h
new file mode 100644
index 0000000000..9edf4c187e
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/avdct.h
@@ -0,0 +1,85 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_AVDCT_H
+#define AVCODEC_AVDCT_H
+
+#include "libavutil/opt.h"
+
+/**
+ * AVDCT context.
+ * @note function pointers can be NULL if the specific features have been
+ * disabled at build time.
+ */
+typedef struct AVDCT {
+ const AVClass* av_class;
+
+ void (*idct)(int16_t* block /* align 16 */);
+
+ /**
+ * IDCT input permutation.
+ * Several optimized IDCTs need a permutated input (relative to the
+ * normal order of the reference IDCT).
+ * This permutation must be performed before the idct_put/add.
+ * Note, normally this can be merged with the zigzag/alternate scan<br>
+ * An example to avoid confusion:
+ * - (->decode coeffs -> zigzag reorder -> dequant -> reference IDCT -> ...)
+ * - (x -> reference DCT -> reference IDCT -> x)
+ * - (x -> reference DCT -> simple_mmx_perm = idct_permutation
+ * -> simple_idct_mmx -> x)
+ * - (-> decode coeffs -> zigzag reorder -> simple_mmx_perm -> dequant
+ * -> simple_idct_mmx -> ...)
+ */
+ uint8_t idct_permutation[64];
+
+ void (*fdct)(int16_t* block /* align 16 */);
+
+ /**
+ * DCT algorithm.
+ * must use AVOptions to set this field.
+ */
+ int dct_algo;
+
+ /**
+ * IDCT algorithm.
+ * must use AVOptions to set this field.
+ */
+ int idct_algo;
+
+ void (*get_pixels)(int16_t* block /* align 16 */,
+ const uint8_t* pixels /* align 8 */, ptrdiff_t line_size);
+
+ int bits_per_sample;
+
+ void (*get_pixels_unaligned)(int16_t* block /* align 16 */,
+ const uint8_t* pixels, ptrdiff_t line_size);
+} AVDCT;
+
+/**
+ * Allocates a AVDCT context.
+ * This needs to be initialized with avcodec_dct_init() after optionally
+ * configuring it with AVOptions.
+ *
+ * To free it use av_free()
+ */
+AVDCT* avcodec_dct_alloc(void);
+int avcodec_dct_init(AVDCT*);
+
+const AVClass* avcodec_dct_get_class(void);
+
+#endif /* AVCODEC_AVDCT_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/avfft.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/avfft.h
new file mode 100644
index 0000000000..115370eb48
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/avfft.h
@@ -0,0 +1,119 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_AVFFT_H
+#define AVCODEC_AVFFT_H
+
+/**
+ * @file
+ * @ingroup lavc_fft
+ * FFT functions
+ */
+
+/**
+ * @defgroup lavc_fft FFT functions
+ * @ingroup lavc_misc
+ *
+ * @{
+ */
+
+typedef float FFTSample;
+
+typedef struct FFTComplex {
+ FFTSample re, im;
+} FFTComplex;
+
+typedef struct FFTContext FFTContext;
+
+/**
+ * Set up a complex FFT.
+ * @param nbits log2 of the length of the input array
+ * @param inverse if 0 perform the forward transform, if 1 perform the
+ * inverse
+ */
+FFTContext* av_fft_init(int nbits, int inverse);
+
+/**
+ * Do the permutation needed BEFORE calling ff_fft_calc().
+ */
+void av_fft_permute(FFTContext* s, FFTComplex* z);
+
+/**
+ * Do a complex FFT with the parameters defined in av_fft_init(). The
+ * input data must be permuted before. No 1.0/sqrt(n) normalization is done.
+ */
+void av_fft_calc(FFTContext* s, FFTComplex* z);
+
+void av_fft_end(FFTContext* s);
+
+FFTContext* av_mdct_init(int nbits, int inverse, double scale);
+void av_imdct_calc(FFTContext* s, FFTSample* output, const FFTSample* input);
+void av_imdct_half(FFTContext* s, FFTSample* output, const FFTSample* input);
+void av_mdct_calc(FFTContext* s, FFTSample* output, const FFTSample* input);
+void av_mdct_end(FFTContext* s);
+
+/* Real Discrete Fourier Transform */
+
+enum RDFTransformType {
+ DFT_R2C,
+ IDFT_C2R,
+ IDFT_R2C,
+ DFT_C2R,
+};
+
+typedef struct RDFTContext RDFTContext;
+
+/**
+ * Set up a real FFT.
+ * @param nbits log2 of the length of the input array
+ * @param trans the type of transform
+ */
+RDFTContext* av_rdft_init(int nbits, enum RDFTransformType trans);
+void av_rdft_calc(RDFTContext* s, FFTSample* data);
+void av_rdft_end(RDFTContext* s);
+
+/* Discrete Cosine Transform */
+
+typedef struct DCTContext DCTContext;
+
+enum DCTTransformType {
+ DCT_II = 0,
+ DCT_III,
+ DCT_I,
+ DST_I,
+};
+
+/**
+ * Set up DCT.
+ *
+ * @param nbits size of the input array:
+ * (1 << nbits) for DCT-II, DCT-III and DST-I
+ * (1 << nbits) + 1 for DCT-I
+ * @param type the type of transform
+ *
+ * @note the first element of the input of DST-I is ignored
+ */
+DCTContext* av_dct_init(int nbits, enum DCTTransformType type);
+void av_dct_calc(DCTContext* s, FFTSample* data);
+void av_dct_end(DCTContext* s);
+
+/**
+ * @}
+ */
+
+#endif /* AVCODEC_AVFFT_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/bsf.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/bsf.h
new file mode 100644
index 0000000000..044a0597bf
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/bsf.h
@@ -0,0 +1,335 @@
+/*
+ * Bitstream filters public API
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_BSF_H
+#define AVCODEC_BSF_H
+
+#include "libavutil/dict.h"
+#include "libavutil/log.h"
+#include "libavutil/rational.h"
+
+#include "codec_id.h"
+#include "codec_par.h"
+#include "packet.h"
+
+/**
+ * @defgroup lavc_bsf Bitstream filters
+ * @ingroup libavc
+ *
+ * Bitstream filters transform encoded media data without decoding it. This
+ * allows e.g. manipulating various header values. Bitstream filters operate on
+ * @ref AVPacket "AVPackets".
+ *
+ * The bitstream filtering API is centered around two structures:
+ * AVBitStreamFilter and AVBSFContext. The former represents a bitstream filter
+ * in abstract, the latter a specific filtering process. Obtain an
+ * AVBitStreamFilter using av_bsf_get_by_name() or av_bsf_iterate(), then pass
+ * it to av_bsf_alloc() to create an AVBSFContext. Fill in the user-settable
+ * AVBSFContext fields, as described in its documentation, then call
+ * av_bsf_init() to prepare the filter context for use.
+ *
+ * Submit packets for filtering using av_bsf_send_packet(), obtain filtered
+ * results with av_bsf_receive_packet(). When no more input packets will be
+ * sent, submit a NULL AVPacket to signal the end of the stream to the filter.
+ * av_bsf_receive_packet() will then return trailing packets, if any are
+ * produced by the filter.
+ *
+ * Finally, free the filter context with av_bsf_free().
+ * @{
+ */
+
+/**
+ * The bitstream filter state.
+ *
+ * This struct must be allocated with av_bsf_alloc() and freed with
+ * av_bsf_free().
+ *
+ * The fields in the struct will only be changed (by the caller or by the
+ * filter) as described in their documentation, and are to be considered
+ * immutable otherwise.
+ */
+typedef struct AVBSFContext {
+ /**
+ * A class for logging and AVOptions
+ */
+ const AVClass* av_class;
+
+ /**
+ * The bitstream filter this context is an instance of.
+ */
+ const struct AVBitStreamFilter* filter;
+
+ /**
+ * Opaque filter-specific private data. If filter->priv_class is non-NULL,
+ * this is an AVOptions-enabled struct.
+ */
+ void* priv_data;
+
+ /**
+ * Parameters of the input stream. This field is allocated in
+ * av_bsf_alloc(), it needs to be filled by the caller before
+ * av_bsf_init().
+ */
+ AVCodecParameters* par_in;
+
+ /**
+ * Parameters of the output stream. This field is allocated in
+ * av_bsf_alloc(), it is set by the filter in av_bsf_init().
+ */
+ AVCodecParameters* par_out;
+
+ /**
+ * The timebase used for the timestamps of the input packets. Set by the
+ * caller before av_bsf_init().
+ */
+ AVRational time_base_in;
+
+ /**
+ * The timebase used for the timestamps of the output packets. Set by the
+ * filter in av_bsf_init().
+ */
+ AVRational time_base_out;
+} AVBSFContext;
+
+typedef struct AVBitStreamFilter {
+ const char* name;
+
+ /**
+ * A list of codec ids supported by the filter, terminated by
+ * AV_CODEC_ID_NONE.
+ * May be NULL, in that case the bitstream filter works with any codec id.
+ */
+ const enum AVCodecID* codec_ids;
+
+ /**
+ * A class for the private data, used to declare bitstream filter private
+ * AVOptions. This field is NULL for bitstream filters that do not declare
+ * any options.
+ *
+ * If this field is non-NULL, the first member of the filter private data
+ * must be a pointer to AVClass, which will be set by libavcodec generic
+ * code to this class.
+ */
+ const AVClass* priv_class;
+} AVBitStreamFilter;
+
+/**
+ * @return a bitstream filter with the specified name or NULL if no such
+ * bitstream filter exists.
+ */
+const AVBitStreamFilter* av_bsf_get_by_name(const char* name);
+
+/**
+ * Iterate over all registered bitstream filters.
+ *
+ * @param opaque a pointer where libavcodec will store the iteration state. Must
+ * point to NULL to start the iteration.
+ *
+ * @return the next registered bitstream filter or NULL when the iteration is
+ * finished
+ */
+const AVBitStreamFilter* av_bsf_iterate(void** opaque);
+
+/**
+ * Allocate a context for a given bitstream filter. The caller must fill in the
+ * context parameters as described in the documentation and then call
+ * av_bsf_init() before sending any data to the filter.
+ *
+ * @param filter the filter for which to allocate an instance.
+ * @param[out] ctx a pointer into which the pointer to the newly-allocated
+ * context will be written. It must be freed with av_bsf_free() after the
+ * filtering is done.
+ *
+ * @return 0 on success, a negative AVERROR code on failure
+ */
+int av_bsf_alloc(const AVBitStreamFilter* filter, AVBSFContext** ctx);
+
+/**
+ * Prepare the filter for use, after all the parameters and options have been
+ * set.
+ *
+ * @param ctx a AVBSFContext previously allocated with av_bsf_alloc()
+ */
+int av_bsf_init(AVBSFContext* ctx);
+
+/**
+ * Submit a packet for filtering.
+ *
+ * After sending each packet, the filter must be completely drained by calling
+ * av_bsf_receive_packet() repeatedly until it returns AVERROR(EAGAIN) or
+ * AVERROR_EOF.
+ *
+ * @param ctx an initialized AVBSFContext
+ * @param pkt the packet to filter. The bitstream filter will take ownership of
+ * the packet and reset the contents of pkt. pkt is not touched if an error
+ * occurs. If pkt is empty (i.e. NULL, or pkt->data is NULL and
+ * pkt->side_data_elems zero), it signals the end of the stream (i.e. no more
+ * non-empty packets will be sent; sending more empty packets does nothing) and
+ * will cause the filter to output any packets it may have buffered internally.
+ *
+ * @return
+ * - 0 on success.
+ * - AVERROR(EAGAIN) if packets need to be retrieved from the filter (using
+ * av_bsf_receive_packet()) before new input can be consumed.
+ * - Another negative AVERROR value if an error occurs.
+ */
+int av_bsf_send_packet(AVBSFContext* ctx, AVPacket* pkt);
+
+/**
+ * Retrieve a filtered packet.
+ *
+ * @param ctx an initialized AVBSFContext
+ * @param[out] pkt this struct will be filled with the contents of the filtered
+ * packet. It is owned by the caller and must be freed using
+ * av_packet_unref() when it is no longer needed.
+ * This parameter should be "clean" (i.e. freshly allocated
+ * with av_packet_alloc() or unreffed with av_packet_unref())
+ * when this function is called. If this function returns
+ * successfully, the contents of pkt will be completely
+ * overwritten by the returned data. On failure, pkt is not
+ * touched.
+ *
+ * @return
+ * - 0 on success.
+ * - AVERROR(EAGAIN) if more packets need to be sent to the filter (using
+ * av_bsf_send_packet()) to get more output.
+ * - AVERROR_EOF if there will be no further output from the filter.
+ * - Another negative AVERROR value if an error occurs.
+ *
+ * @note one input packet may result in several output packets, so after sending
+ * a packet with av_bsf_send_packet(), this function needs to be called
+ * repeatedly until it stops returning 0. It is also possible for a filter to
+ * output fewer packets than were sent to it, so this function may return
+ * AVERROR(EAGAIN) immediately after a successful av_bsf_send_packet() call.
+ */
+int av_bsf_receive_packet(AVBSFContext* ctx, AVPacket* pkt);
+
+/**
+ * Reset the internal bitstream filter state. Should be called e.g. when
+ * seeking.
+ */
+void av_bsf_flush(AVBSFContext* ctx);
+
+/**
+ * Free a bitstream filter context and everything associated with it; write NULL
+ * into the supplied pointer.
+ */
+void av_bsf_free(AVBSFContext** ctx);
+
+/**
+ * Get the AVClass for AVBSFContext. It can be used in combination with
+ * AV_OPT_SEARCH_FAKE_OBJ for examining options.
+ *
+ * @see av_opt_find().
+ */
+const AVClass* av_bsf_get_class(void);
+
+/**
+ * Structure for chain/list of bitstream filters.
+ * Empty list can be allocated by av_bsf_list_alloc().
+ */
+typedef struct AVBSFList AVBSFList;
+
+/**
+ * Allocate empty list of bitstream filters.
+ * The list must be later freed by av_bsf_list_free()
+ * or finalized by av_bsf_list_finalize().
+ *
+ * @return Pointer to @ref AVBSFList on success, NULL in case of failure
+ */
+AVBSFList* av_bsf_list_alloc(void);
+
+/**
+ * Free list of bitstream filters.
+ *
+ * @param lst Pointer to pointer returned by av_bsf_list_alloc()
+ */
+void av_bsf_list_free(AVBSFList** lst);
+
+/**
+ * Append bitstream filter to the list of bitstream filters.
+ *
+ * @param lst List to append to
+ * @param bsf Filter context to be appended
+ *
+ * @return >=0 on success, negative AVERROR in case of failure
+ */
+int av_bsf_list_append(AVBSFList* lst, AVBSFContext* bsf);
+
+/**
+ * Construct new bitstream filter context given it's name and options
+ * and append it to the list of bitstream filters.
+ *
+ * @param lst List to append to
+ * @param bsf_name Name of the bitstream filter
+ * @param options Options for the bitstream filter, can be set to NULL
+ *
+ * @return >=0 on success, negative AVERROR in case of failure
+ */
+int av_bsf_list_append2(AVBSFList* lst, const char* bsf_name,
+ AVDictionary** options);
+/**
+ * Finalize list of bitstream filters.
+ *
+ * This function will transform @ref AVBSFList to single @ref AVBSFContext,
+ * so the whole chain of bitstream filters can be treated as single filter
+ * freshly allocated by av_bsf_alloc().
+ * If the call is successful, @ref AVBSFList structure is freed and lst
+ * will be set to NULL. In case of failure, caller is responsible for
+ * freeing the structure by av_bsf_list_free()
+ *
+ * @param lst Filter list structure to be transformed
+ * @param[out] bsf Pointer to be set to newly created @ref AVBSFContext
+ * structure representing the chain of bitstream filters
+ *
+ * @return >=0 on success, negative AVERROR in case of failure
+ */
+int av_bsf_list_finalize(AVBSFList** lst, AVBSFContext** bsf);
+
+/**
+ * Parse string describing list of bitstream filters and create single
+ * @ref AVBSFContext describing the whole chain of bitstream filters.
+ * Resulting @ref AVBSFContext can be treated as any other @ref AVBSFContext
+ * freshly allocated by av_bsf_alloc().
+ *
+ * @param str String describing chain of bitstream filters in format
+ * `bsf1[=opt1=val1:opt2=val2][,bsf2]`
+ * @param[out] bsf Pointer to be set to newly created @ref AVBSFContext
+ * structure representing the chain of bitstream filters
+ *
+ * @return >=0 on success, negative AVERROR in case of failure
+ */
+int av_bsf_list_parse_str(const char* str, AVBSFContext** bsf);
+
+/**
+ * Get null/pass-through bitstream filter.
+ *
+ * @param[out] bsf Pointer to be set to new instance of pass-through bitstream
+ * filter
+ *
+ * @return
+ */
+int av_bsf_get_null_filter(AVBSFContext** bsf);
+
+/**
+ * @}
+ */
+
+#endif // AVCODEC_BSF_H
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/codec.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/codec.h
new file mode 100644
index 0000000000..4340d8503f
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/codec.h
@@ -0,0 +1,387 @@
+/*
+ * AVCodec public API
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_CODEC_H
+#define AVCODEC_CODEC_H
+
+#include <stdint.h>
+
+#include "libavutil/avutil.h"
+#include "libavutil/hwcontext.h"
+#include "libavutil/log.h"
+#include "libavutil/pixfmt.h"
+#include "libavutil/rational.h"
+#include "libavutil/samplefmt.h"
+
+#include "libavcodec/codec_id.h"
+#include "libavcodec/version_major.h"
+
+/**
+ * @addtogroup lavc_core
+ * @{
+ */
+
+/**
+ * Decoder can use draw_horiz_band callback.
+ */
+#define AV_CODEC_CAP_DRAW_HORIZ_BAND (1 << 0)
+/**
+ * Codec uses get_buffer() or get_encode_buffer() for allocating buffers and
+ * supports custom allocators.
+ * If not set, it might not use get_buffer() or get_encode_buffer() at all, or
+ * use operations that assume the buffer was allocated by
+ * avcodec_default_get_buffer2 or avcodec_default_get_encode_buffer.
+ */
+#define AV_CODEC_CAP_DR1 (1 << 1)
+/**
+ * Encoder or decoder requires flushing with NULL input at the end in order to
+ * give the complete and correct output.
+ *
+ * NOTE: If this flag is not set, the codec is guaranteed to never be fed with
+ * with NULL data. The user can still send NULL data to the public encode
+ * or decode function, but libavcodec will not pass it along to the codec
+ * unless this flag is set.
+ *
+ * Decoders:
+ * The decoder has a non-zero delay and needs to be fed with avpkt->data=NULL,
+ * avpkt->size=0 at the end to get the delayed data until the decoder no longer
+ * returns frames.
+ *
+ * Encoders:
+ * The encoder needs to be fed with NULL data at the end of encoding until the
+ * encoder no longer returns data.
+ *
+ * NOTE: For encoders implementing the AVCodec.encode2() function, setting this
+ * flag also means that the encoder must set the pts and duration for
+ * each output packet. If this flag is not set, the pts and duration will
+ * be determined by libavcodec from the input frame.
+ */
+#define AV_CODEC_CAP_DELAY (1 << 5)
+/**
+ * Codec can be fed a final frame with a smaller size.
+ * This can be used to prevent truncation of the last audio samples.
+ */
+#define AV_CODEC_CAP_SMALL_LAST_FRAME (1 << 6)
+
+/**
+ * Codec can output multiple frames per AVPacket
+ * Normally demuxers return one frame at a time, demuxers which do not do
+ * are connected to a parser to split what they return into proper frames.
+ * This flag is reserved to the very rare category of codecs which have a
+ * bitstream that cannot be split into frames without timeconsuming
+ * operations like full decoding. Demuxers carrying such bitstreams thus
+ * may return multiple frames in a packet. This has many disadvantages like
+ * prohibiting stream copy in many cases thus it should only be considered
+ * as a last resort.
+ */
+#define AV_CODEC_CAP_SUBFRAMES (1 << 8)
+/**
+ * Codec is experimental and is thus avoided in favor of non experimental
+ * encoders
+ */
+#define AV_CODEC_CAP_EXPERIMENTAL (1 << 9)
+/**
+ * Codec should fill in channel configuration and samplerate instead of
+ * container
+ */
+#define AV_CODEC_CAP_CHANNEL_CONF (1 << 10)
+/**
+ * Codec supports frame-level multithreading.
+ */
+#define AV_CODEC_CAP_FRAME_THREADS (1 << 12)
+/**
+ * Codec supports slice-based (or partition-based) multithreading.
+ */
+#define AV_CODEC_CAP_SLICE_THREADS (1 << 13)
+/**
+ * Codec supports changed parameters at any point.
+ */
+#define AV_CODEC_CAP_PARAM_CHANGE (1 << 14)
+/**
+ * Codec supports multithreading through a method other than slice- or
+ * frame-level multithreading. Typically this marks wrappers around
+ * multithreading-capable external libraries.
+ */
+#define AV_CODEC_CAP_OTHER_THREADS (1 << 15)
+/**
+ * Audio encoder supports receiving a different number of samples in each call.
+ */
+#define AV_CODEC_CAP_VARIABLE_FRAME_SIZE (1 << 16)
+/**
+ * Decoder is not a preferred choice for probing.
+ * This indicates that the decoder is not a good choice for probing.
+ * It could for example be an expensive to spin up hardware decoder,
+ * or it could simply not provide a lot of useful information about
+ * the stream.
+ * A decoder marked with this flag should only be used as last resort
+ * choice for probing.
+ */
+#define AV_CODEC_CAP_AVOID_PROBING (1 << 17)
+
+/**
+ * Codec is backed by a hardware implementation. Typically used to
+ * identify a non-hwaccel hardware decoder. For information about hwaccels, use
+ * avcodec_get_hw_config() instead.
+ */
+#define AV_CODEC_CAP_HARDWARE (1 << 18)
+
+/**
+ * Codec is potentially backed by a hardware implementation, but not
+ * necessarily. This is used instead of AV_CODEC_CAP_HARDWARE, if the
+ * implementation provides some sort of internal fallback.
+ */
+#define AV_CODEC_CAP_HYBRID (1 << 19)
+
+/**
+ * This encoder can reorder user opaque values from input AVFrames and return
+ * them with corresponding output packets.
+ * @see AV_CODEC_FLAG_COPY_OPAQUE
+ */
+#define AV_CODEC_CAP_ENCODER_REORDERED_OPAQUE (1 << 20)
+
+/**
+ * This encoder can be flushed using avcodec_flush_buffers(). If this flag is
+ * not set, the encoder must be closed and reopened to ensure that no frames
+ * remain pending.
+ */
+#define AV_CODEC_CAP_ENCODER_FLUSH (1 << 21)
+
+/**
+ * The encoder is able to output reconstructed frame data, i.e. raw frames that
+ * would be produced by decoding the encoded bitstream.
+ *
+ * Reconstructed frame output is enabled by the AV_CODEC_FLAG_RECON_FRAME flag.
+ */
+#define AV_CODEC_CAP_ENCODER_RECON_FRAME (1 << 22)
+
+/**
+ * AVProfile.
+ */
+typedef struct AVProfile {
+ int profile;
+ const char* name; ///< short name for the profile
+} AVProfile;
+
+/**
+ * AVCodec.
+ */
+typedef struct AVCodec {
+ /**
+ * Name of the codec implementation.
+ * The name is globally unique among encoders and among decoders (but an
+ * encoder and a decoder can share the same name).
+ * This is the primary way to find a codec from the user perspective.
+ */
+ const char* name;
+ /**
+ * Descriptive name for the codec, meant to be more human readable than name.
+ * You should use the NULL_IF_CONFIG_SMALL() macro to define it.
+ */
+ const char* long_name;
+ enum AVMediaType type;
+ enum AVCodecID id;
+ /**
+ * Codec capabilities.
+ * see AV_CODEC_CAP_*
+ */
+ int capabilities;
+ uint8_t max_lowres; ///< maximum value for lowres supported by the decoder
+ const AVRational*
+ supported_framerates; ///< array of supported framerates, or NULL if any,
+ ///< array is terminated by {0,0}
+ const enum AVPixelFormat*
+ pix_fmts; ///< array of supported pixel formats, or NULL if unknown,
+ ///< array is terminated by -1
+ const int*
+ supported_samplerates; ///< array of supported audio samplerates, or NULL
+ ///< if unknown, array is terminated by 0
+ const enum AVSampleFormat*
+ sample_fmts; ///< array of supported sample formats, or NULL if unknown,
+ ///< array is terminated by -1
+#if FF_API_OLD_CHANNEL_LAYOUT
+ /**
+ * @deprecated use ch_layouts instead
+ */
+ attribute_deprecated const uint64_t*
+ channel_layouts; ///< array of support channel layouts, or NULL if
+ ///< unknown. array is terminated by 0
+#endif
+ const AVClass* priv_class; ///< AVClass for the private context
+ const AVProfile*
+ profiles; ///< array of recognized profiles, or NULL if unknown, array is
+ ///< terminated by {FF_PROFILE_UNKNOWN}
+
+ /**
+ * Group name of the codec implementation.
+ * This is a short symbolic name of the wrapper backing this codec. A
+ * wrapper uses some kind of external implementation for the codec, such
+ * as an external library, or a codec implementation provided by the OS or
+ * the hardware.
+ * If this field is NULL, this is a builtin, libavcodec native codec.
+ * If non-NULL, this will be the suffix in AVCodec.name in most cases
+ * (usually AVCodec.name will be of the form "<codec_name>_<wrapper_name>").
+ */
+ const char* wrapper_name;
+
+ /**
+ * Array of supported channel layouts, terminated with a zeroed layout.
+ */
+ const AVChannelLayout* ch_layouts;
+} AVCodec;
+
+/**
+ * Iterate over all registered codecs.
+ *
+ * @param opaque a pointer where libavcodec will store the iteration state. Must
+ * point to NULL to start the iteration.
+ *
+ * @return the next registered codec or NULL when the iteration is
+ * finished
+ */
+const AVCodec* av_codec_iterate(void** opaque);
+
+/**
+ * Find a registered decoder with a matching codec ID.
+ *
+ * @param id AVCodecID of the requested decoder
+ * @return A decoder if one was found, NULL otherwise.
+ */
+const AVCodec* avcodec_find_decoder(enum AVCodecID id);
+
+/**
+ * Find a registered decoder with the specified name.
+ *
+ * @param name name of the requested decoder
+ * @return A decoder if one was found, NULL otherwise.
+ */
+const AVCodec* avcodec_find_decoder_by_name(const char* name);
+
+/**
+ * Find a registered encoder with a matching codec ID.
+ *
+ * @param id AVCodecID of the requested encoder
+ * @return An encoder if one was found, NULL otherwise.
+ */
+const AVCodec* avcodec_find_encoder(enum AVCodecID id);
+
+/**
+ * Find a registered encoder with the specified name.
+ *
+ * @param name name of the requested encoder
+ * @return An encoder if one was found, NULL otherwise.
+ */
+const AVCodec* avcodec_find_encoder_by_name(const char* name);
+/**
+ * @return a non-zero number if codec is an encoder, zero otherwise
+ */
+int av_codec_is_encoder(const AVCodec* codec);
+
+/**
+ * @return a non-zero number if codec is a decoder, zero otherwise
+ */
+int av_codec_is_decoder(const AVCodec* codec);
+
+/**
+ * Return a name for the specified profile, if available.
+ *
+ * @param codec the codec that is searched for the given profile
+ * @param profile the profile value for which a name is requested
+ * @return A name for the profile if found, NULL otherwise.
+ */
+const char* av_get_profile_name(const AVCodec* codec, int profile);
+
+enum {
+ /**
+ * The codec supports this format via the hw_device_ctx interface.
+ *
+ * When selecting this format, AVCodecContext.hw_device_ctx should
+ * have been set to a device of the specified type before calling
+ * avcodec_open2().
+ */
+ AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX = 0x01,
+ /**
+ * The codec supports this format via the hw_frames_ctx interface.
+ *
+ * When selecting this format for a decoder,
+ * AVCodecContext.hw_frames_ctx should be set to a suitable frames
+ * context inside the get_format() callback. The frames context
+ * must have been created on a device of the specified type.
+ *
+ * When selecting this format for an encoder,
+ * AVCodecContext.hw_frames_ctx should be set to the context which
+ * will be used for the input frames before calling avcodec_open2().
+ */
+ AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX = 0x02,
+ /**
+ * The codec supports this format by some internal method.
+ *
+ * This format can be selected without any additional configuration -
+ * no device or frames context is required.
+ */
+ AV_CODEC_HW_CONFIG_METHOD_INTERNAL = 0x04,
+ /**
+ * The codec supports this format by some ad-hoc method.
+ *
+ * Additional settings and/or function calls are required. See the
+ * codec-specific documentation for details. (Methods requiring
+ * this sort of configuration are deprecated and others should be
+ * used in preference.)
+ */
+ AV_CODEC_HW_CONFIG_METHOD_AD_HOC = 0x08,
+};
+
+typedef struct AVCodecHWConfig {
+ /**
+ * For decoders, a hardware pixel format which that decoder may be
+ * able to decode to if suitable hardware is available.
+ *
+ * For encoders, a pixel format which the encoder may be able to
+ * accept. If set to AV_PIX_FMT_NONE, this applies to all pixel
+ * formats supported by the codec.
+ */
+ enum AVPixelFormat pix_fmt;
+ /**
+ * Bit set of AV_CODEC_HW_CONFIG_METHOD_* flags, describing the possible
+ * setup methods which can be used with this configuration.
+ */
+ int methods;
+ /**
+ * The device type associated with the configuration.
+ *
+ * Must be set for AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX and
+ * AV_CODEC_HW_CONFIG_METHOD_HW_FRAMES_CTX, otherwise unused.
+ */
+ enum AVHWDeviceType device_type;
+} AVCodecHWConfig;
+
+/**
+ * Retrieve supported hardware configurations for a codec.
+ *
+ * Values of index from zero to some maximum return the indexed configuration
+ * descriptor; all other values return NULL. If the codec does not support
+ * any hardware configurations then it will always return NULL.
+ */
+const AVCodecHWConfig* avcodec_get_hw_config(const AVCodec* codec, int index);
+
+/**
+ * @}
+ */
+
+#endif /* AVCODEC_CODEC_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/codec_desc.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/codec_desc.h
new file mode 100644
index 0000000000..0884491d3e
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/codec_desc.h
@@ -0,0 +1,128 @@
+/*
+ * Codec descriptors public API
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_CODEC_DESC_H
+#define AVCODEC_CODEC_DESC_H
+
+#include "libavutil/avutil.h"
+
+#include "codec_id.h"
+
+/**
+ * @addtogroup lavc_core
+ * @{
+ */
+
+/**
+ * This struct describes the properties of a single codec described by an
+ * AVCodecID.
+ * @see avcodec_descriptor_get()
+ */
+typedef struct AVCodecDescriptor {
+ enum AVCodecID id;
+ enum AVMediaType type;
+ /**
+ * Name of the codec described by this descriptor. It is non-empty and
+ * unique for each codec descriptor. It should contain alphanumeric
+ * characters and '_' only.
+ */
+ const char* name;
+ /**
+ * A more descriptive name for this codec. May be NULL.
+ */
+ const char* long_name;
+ /**
+ * Codec properties, a combination of AV_CODEC_PROP_* flags.
+ */
+ int props;
+ /**
+ * MIME type(s) associated with the codec.
+ * May be NULL; if not, a NULL-terminated array of MIME types.
+ * The first item is always non-NULL and is the preferred MIME type.
+ */
+ const char* const* mime_types;
+ /**
+ * If non-NULL, an array of profiles recognized for this codec.
+ * Terminated with FF_PROFILE_UNKNOWN.
+ */
+ const struct AVProfile* profiles;
+} AVCodecDescriptor;
+
+/**
+ * Codec uses only intra compression.
+ * Video and audio codecs only.
+ */
+#define AV_CODEC_PROP_INTRA_ONLY (1 << 0)
+/**
+ * Codec supports lossy compression. Audio and video codecs only.
+ * @note a codec may support both lossy and lossless
+ * compression modes
+ */
+#define AV_CODEC_PROP_LOSSY (1 << 1)
+/**
+ * Codec supports lossless compression. Audio and video codecs only.
+ */
+#define AV_CODEC_PROP_LOSSLESS (1 << 2)
+/**
+ * Codec supports frame reordering. That is, the coded order (the order in which
+ * the encoded packets are output by the encoders / stored / input to the
+ * decoders) may be different from the presentation order of the corresponding
+ * frames.
+ *
+ * For codecs that do not have this property set, PTS and DTS should always be
+ * equal.
+ */
+#define AV_CODEC_PROP_REORDER (1 << 3)
+/**
+ * Subtitle codec is bitmap based
+ * Decoded AVSubtitle data can be read from the AVSubtitleRect->pict field.
+ */
+#define AV_CODEC_PROP_BITMAP_SUB (1 << 16)
+/**
+ * Subtitle codec is text based.
+ * Decoded AVSubtitle data can be read from the AVSubtitleRect->ass field.
+ */
+#define AV_CODEC_PROP_TEXT_SUB (1 << 17)
+
+/**
+ * @return descriptor for given codec ID or NULL if no descriptor exists.
+ */
+const AVCodecDescriptor* avcodec_descriptor_get(enum AVCodecID id);
+
+/**
+ * Iterate over all codec descriptors known to libavcodec.
+ *
+ * @param prev previous descriptor. NULL to get the first descriptor.
+ *
+ * @return next descriptor or NULL after the last descriptor
+ */
+const AVCodecDescriptor* avcodec_descriptor_next(const AVCodecDescriptor* prev);
+
+/**
+ * @return codec descriptor with the given name or NULL if no such descriptor
+ * exists.
+ */
+const AVCodecDescriptor* avcodec_descriptor_get_by_name(const char* name);
+
+/**
+ * @}
+ */
+
+#endif // AVCODEC_CODEC_DESC_H
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/codec_id.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/codec_id.h
new file mode 100644
index 0000000000..b3dd07ea6e
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/codec_id.h
@@ -0,0 +1,669 @@
+/*
+ * Codec IDs
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_CODEC_ID_H
+#define AVCODEC_CODEC_ID_H
+
+#include "libavutil/avutil.h"
+#include "libavutil/samplefmt.h"
+
+#include "version_major.h"
+
+/**
+ * @addtogroup lavc_core
+ * @{
+ */
+
+/**
+ * Identify the syntax and semantics of the bitstream.
+ * The principle is roughly:
+ * Two decoders with the same ID can decode the same streams.
+ * Two encoders with the same ID can encode compatible streams.
+ * There may be slight deviations from the principle due to implementation
+ * details.
+ *
+ * If you add a codec ID to this list, add it so that
+ * 1. no value of an existing codec ID changes (that would break ABI),
+ * 2. it is as close as possible to similar codecs
+ *
+ * After adding new codec IDs, do not forget to add an entry to the codec
+ * descriptor list and bump libavcodec minor version.
+ */
+enum AVCodecID {
+ AV_CODEC_ID_NONE,
+
+ /* video codecs */
+ AV_CODEC_ID_MPEG1VIDEO,
+ AV_CODEC_ID_MPEG2VIDEO, ///< preferred ID for MPEG-1/2 video decoding
+ AV_CODEC_ID_H261,
+ AV_CODEC_ID_H263,
+ AV_CODEC_ID_RV10,
+ AV_CODEC_ID_RV20,
+ AV_CODEC_ID_MJPEG,
+ AV_CODEC_ID_MJPEGB,
+ AV_CODEC_ID_LJPEG,
+ AV_CODEC_ID_SP5X,
+ AV_CODEC_ID_JPEGLS,
+ AV_CODEC_ID_MPEG4,
+ AV_CODEC_ID_RAWVIDEO,
+ AV_CODEC_ID_MSMPEG4V1,
+ AV_CODEC_ID_MSMPEG4V2,
+ AV_CODEC_ID_MSMPEG4V3,
+ AV_CODEC_ID_WMV1,
+ AV_CODEC_ID_WMV2,
+ AV_CODEC_ID_H263P,
+ AV_CODEC_ID_H263I,
+ AV_CODEC_ID_FLV1,
+ AV_CODEC_ID_SVQ1,
+ AV_CODEC_ID_SVQ3,
+ AV_CODEC_ID_DVVIDEO,
+ AV_CODEC_ID_HUFFYUV,
+ AV_CODEC_ID_CYUV,
+ AV_CODEC_ID_H264,
+ AV_CODEC_ID_INDEO3,
+ AV_CODEC_ID_VP3,
+ AV_CODEC_ID_THEORA,
+ AV_CODEC_ID_ASV1,
+ AV_CODEC_ID_ASV2,
+ AV_CODEC_ID_FFV1,
+ AV_CODEC_ID_4XM,
+ AV_CODEC_ID_VCR1,
+ AV_CODEC_ID_CLJR,
+ AV_CODEC_ID_MDEC,
+ AV_CODEC_ID_ROQ,
+ AV_CODEC_ID_INTERPLAY_VIDEO,
+ AV_CODEC_ID_XAN_WC3,
+ AV_CODEC_ID_XAN_WC4,
+ AV_CODEC_ID_RPZA,
+ AV_CODEC_ID_CINEPAK,
+ AV_CODEC_ID_WS_VQA,
+ AV_CODEC_ID_MSRLE,
+ AV_CODEC_ID_MSVIDEO1,
+ AV_CODEC_ID_IDCIN,
+ AV_CODEC_ID_8BPS,
+ AV_CODEC_ID_SMC,
+ AV_CODEC_ID_FLIC,
+ AV_CODEC_ID_TRUEMOTION1,
+ AV_CODEC_ID_VMDVIDEO,
+ AV_CODEC_ID_MSZH,
+ AV_CODEC_ID_ZLIB,
+ AV_CODEC_ID_QTRLE,
+ AV_CODEC_ID_TSCC,
+ AV_CODEC_ID_ULTI,
+ AV_CODEC_ID_QDRAW,
+ AV_CODEC_ID_VIXL,
+ AV_CODEC_ID_QPEG,
+ AV_CODEC_ID_PNG,
+ AV_CODEC_ID_PPM,
+ AV_CODEC_ID_PBM,
+ AV_CODEC_ID_PGM,
+ AV_CODEC_ID_PGMYUV,
+ AV_CODEC_ID_PAM,
+ AV_CODEC_ID_FFVHUFF,
+ AV_CODEC_ID_RV30,
+ AV_CODEC_ID_RV40,
+ AV_CODEC_ID_VC1,
+ AV_CODEC_ID_WMV3,
+ AV_CODEC_ID_LOCO,
+ AV_CODEC_ID_WNV1,
+ AV_CODEC_ID_AASC,
+ AV_CODEC_ID_INDEO2,
+ AV_CODEC_ID_FRAPS,
+ AV_CODEC_ID_TRUEMOTION2,
+ AV_CODEC_ID_BMP,
+ AV_CODEC_ID_CSCD,
+ AV_CODEC_ID_MMVIDEO,
+ AV_CODEC_ID_ZMBV,
+ AV_CODEC_ID_AVS,
+ AV_CODEC_ID_SMACKVIDEO,
+ AV_CODEC_ID_NUV,
+ AV_CODEC_ID_KMVC,
+ AV_CODEC_ID_FLASHSV,
+ AV_CODEC_ID_CAVS,
+ AV_CODEC_ID_JPEG2000,
+ AV_CODEC_ID_VMNC,
+ AV_CODEC_ID_VP5,
+ AV_CODEC_ID_VP6,
+ AV_CODEC_ID_VP6F,
+ AV_CODEC_ID_TARGA,
+ AV_CODEC_ID_DSICINVIDEO,
+ AV_CODEC_ID_TIERTEXSEQVIDEO,
+ AV_CODEC_ID_TIFF,
+ AV_CODEC_ID_GIF,
+ AV_CODEC_ID_DXA,
+ AV_CODEC_ID_DNXHD,
+ AV_CODEC_ID_THP,
+ AV_CODEC_ID_SGI,
+ AV_CODEC_ID_C93,
+ AV_CODEC_ID_BETHSOFTVID,
+ AV_CODEC_ID_PTX,
+ AV_CODEC_ID_TXD,
+ AV_CODEC_ID_VP6A,
+ AV_CODEC_ID_AMV,
+ AV_CODEC_ID_VB,
+ AV_CODEC_ID_PCX,
+ AV_CODEC_ID_SUNRAST,
+ AV_CODEC_ID_INDEO4,
+ AV_CODEC_ID_INDEO5,
+ AV_CODEC_ID_MIMIC,
+ AV_CODEC_ID_RL2,
+ AV_CODEC_ID_ESCAPE124,
+ AV_CODEC_ID_DIRAC,
+ AV_CODEC_ID_BFI,
+ AV_CODEC_ID_CMV,
+ AV_CODEC_ID_MOTIONPIXELS,
+ AV_CODEC_ID_TGV,
+ AV_CODEC_ID_TGQ,
+ AV_CODEC_ID_TQI,
+ AV_CODEC_ID_AURA,
+ AV_CODEC_ID_AURA2,
+ AV_CODEC_ID_V210X,
+ AV_CODEC_ID_TMV,
+ AV_CODEC_ID_V210,
+ AV_CODEC_ID_DPX,
+ AV_CODEC_ID_MAD,
+ AV_CODEC_ID_FRWU,
+ AV_CODEC_ID_FLASHSV2,
+ AV_CODEC_ID_CDGRAPHICS,
+ AV_CODEC_ID_R210,
+ AV_CODEC_ID_ANM,
+ AV_CODEC_ID_BINKVIDEO,
+ AV_CODEC_ID_IFF_ILBM,
+#define AV_CODEC_ID_IFF_BYTERUN1 AV_CODEC_ID_IFF_ILBM
+ AV_CODEC_ID_KGV1,
+ AV_CODEC_ID_YOP,
+ AV_CODEC_ID_VP8,
+ AV_CODEC_ID_PICTOR,
+ AV_CODEC_ID_ANSI,
+ AV_CODEC_ID_A64_MULTI,
+ AV_CODEC_ID_A64_MULTI5,
+ AV_CODEC_ID_R10K,
+ AV_CODEC_ID_MXPEG,
+ AV_CODEC_ID_LAGARITH,
+ AV_CODEC_ID_PRORES,
+ AV_CODEC_ID_JV,
+ AV_CODEC_ID_DFA,
+ AV_CODEC_ID_WMV3IMAGE,
+ AV_CODEC_ID_VC1IMAGE,
+ AV_CODEC_ID_UTVIDEO,
+ AV_CODEC_ID_BMV_VIDEO,
+ AV_CODEC_ID_VBLE,
+ AV_CODEC_ID_DXTORY,
+ AV_CODEC_ID_V410,
+ AV_CODEC_ID_XWD,
+ AV_CODEC_ID_CDXL,
+ AV_CODEC_ID_XBM,
+ AV_CODEC_ID_ZEROCODEC,
+ AV_CODEC_ID_MSS1,
+ AV_CODEC_ID_MSA1,
+ AV_CODEC_ID_TSCC2,
+ AV_CODEC_ID_MTS2,
+ AV_CODEC_ID_CLLC,
+ AV_CODEC_ID_MSS2,
+ AV_CODEC_ID_VP9,
+ AV_CODEC_ID_AIC,
+ AV_CODEC_ID_ESCAPE130,
+ AV_CODEC_ID_G2M,
+ AV_CODEC_ID_WEBP,
+ AV_CODEC_ID_HNM4_VIDEO,
+ AV_CODEC_ID_HEVC,
+#define AV_CODEC_ID_H265 AV_CODEC_ID_HEVC
+ AV_CODEC_ID_FIC,
+ AV_CODEC_ID_ALIAS_PIX,
+ AV_CODEC_ID_BRENDER_PIX,
+ AV_CODEC_ID_PAF_VIDEO,
+ AV_CODEC_ID_EXR,
+ AV_CODEC_ID_VP7,
+ AV_CODEC_ID_SANM,
+ AV_CODEC_ID_SGIRLE,
+ AV_CODEC_ID_MVC1,
+ AV_CODEC_ID_MVC2,
+ AV_CODEC_ID_HQX,
+ AV_CODEC_ID_TDSC,
+ AV_CODEC_ID_HQ_HQA,
+ AV_CODEC_ID_HAP,
+ AV_CODEC_ID_DDS,
+ AV_CODEC_ID_DXV,
+ AV_CODEC_ID_SCREENPRESSO,
+ AV_CODEC_ID_RSCC,
+ AV_CODEC_ID_AVS2,
+ AV_CODEC_ID_PGX,
+ AV_CODEC_ID_AVS3,
+ AV_CODEC_ID_MSP2,
+ AV_CODEC_ID_VVC,
+#define AV_CODEC_ID_H266 AV_CODEC_ID_VVC
+ AV_CODEC_ID_Y41P,
+ AV_CODEC_ID_AVRP,
+ AV_CODEC_ID_012V,
+ AV_CODEC_ID_AVUI,
+#if FF_API_AYUV_CODECID
+ AV_CODEC_ID_AYUV,
+#endif
+ AV_CODEC_ID_TARGA_Y216,
+ AV_CODEC_ID_V308,
+ AV_CODEC_ID_V408,
+ AV_CODEC_ID_YUV4,
+ AV_CODEC_ID_AVRN,
+ AV_CODEC_ID_CPIA,
+ AV_CODEC_ID_XFACE,
+ AV_CODEC_ID_SNOW,
+ AV_CODEC_ID_SMVJPEG,
+ AV_CODEC_ID_APNG,
+ AV_CODEC_ID_DAALA,
+ AV_CODEC_ID_CFHD,
+ AV_CODEC_ID_TRUEMOTION2RT,
+ AV_CODEC_ID_M101,
+ AV_CODEC_ID_MAGICYUV,
+ AV_CODEC_ID_SHEERVIDEO,
+ AV_CODEC_ID_YLC,
+ AV_CODEC_ID_PSD,
+ AV_CODEC_ID_PIXLET,
+ AV_CODEC_ID_SPEEDHQ,
+ AV_CODEC_ID_FMVC,
+ AV_CODEC_ID_SCPR,
+ AV_CODEC_ID_CLEARVIDEO,
+ AV_CODEC_ID_XPM,
+ AV_CODEC_ID_AV1,
+ AV_CODEC_ID_BITPACKED,
+ AV_CODEC_ID_MSCC,
+ AV_CODEC_ID_SRGC,
+ AV_CODEC_ID_SVG,
+ AV_CODEC_ID_GDV,
+ AV_CODEC_ID_FITS,
+ AV_CODEC_ID_IMM4,
+ AV_CODEC_ID_PROSUMER,
+ AV_CODEC_ID_MWSC,
+ AV_CODEC_ID_WCMV,
+ AV_CODEC_ID_RASC,
+ AV_CODEC_ID_HYMT,
+ AV_CODEC_ID_ARBC,
+ AV_CODEC_ID_AGM,
+ AV_CODEC_ID_LSCR,
+ AV_CODEC_ID_VP4,
+ AV_CODEC_ID_IMM5,
+ AV_CODEC_ID_MVDV,
+ AV_CODEC_ID_MVHA,
+ AV_CODEC_ID_CDTOONS,
+ AV_CODEC_ID_MV30,
+ AV_CODEC_ID_NOTCHLC,
+ AV_CODEC_ID_PFM,
+ AV_CODEC_ID_MOBICLIP,
+ AV_CODEC_ID_PHOTOCD,
+ AV_CODEC_ID_IPU,
+ AV_CODEC_ID_ARGO,
+ AV_CODEC_ID_CRI,
+ AV_CODEC_ID_SIMBIOSIS_IMX,
+ AV_CODEC_ID_SGA_VIDEO,
+ AV_CODEC_ID_GEM,
+ AV_CODEC_ID_VBN,
+ AV_CODEC_ID_JPEGXL,
+ AV_CODEC_ID_QOI,
+ AV_CODEC_ID_PHM,
+ AV_CODEC_ID_RADIANCE_HDR,
+ AV_CODEC_ID_WBMP,
+ AV_CODEC_ID_MEDIA100,
+ AV_CODEC_ID_VQC,
+
+ /* various PCM "codecs" */
+ AV_CODEC_ID_FIRST_AUDIO =
+ 0x10000, ///< A dummy id pointing at the start of audio codecs
+ AV_CODEC_ID_PCM_S16LE = 0x10000,
+ AV_CODEC_ID_PCM_S16BE,
+ AV_CODEC_ID_PCM_U16LE,
+ AV_CODEC_ID_PCM_U16BE,
+ AV_CODEC_ID_PCM_S8,
+ AV_CODEC_ID_PCM_U8,
+ AV_CODEC_ID_PCM_MULAW,
+ AV_CODEC_ID_PCM_ALAW,
+ AV_CODEC_ID_PCM_S32LE,
+ AV_CODEC_ID_PCM_S32BE,
+ AV_CODEC_ID_PCM_U32LE,
+ AV_CODEC_ID_PCM_U32BE,
+ AV_CODEC_ID_PCM_S24LE,
+ AV_CODEC_ID_PCM_S24BE,
+ AV_CODEC_ID_PCM_U24LE,
+ AV_CODEC_ID_PCM_U24BE,
+ AV_CODEC_ID_PCM_S24DAUD,
+ AV_CODEC_ID_PCM_ZORK,
+ AV_CODEC_ID_PCM_S16LE_PLANAR,
+ AV_CODEC_ID_PCM_DVD,
+ AV_CODEC_ID_PCM_F32BE,
+ AV_CODEC_ID_PCM_F32LE,
+ AV_CODEC_ID_PCM_F64BE,
+ AV_CODEC_ID_PCM_F64LE,
+ AV_CODEC_ID_PCM_BLURAY,
+ AV_CODEC_ID_PCM_LXF,
+ AV_CODEC_ID_S302M,
+ AV_CODEC_ID_PCM_S8_PLANAR,
+ AV_CODEC_ID_PCM_S24LE_PLANAR,
+ AV_CODEC_ID_PCM_S32LE_PLANAR,
+ AV_CODEC_ID_PCM_S16BE_PLANAR,
+ AV_CODEC_ID_PCM_S64LE,
+ AV_CODEC_ID_PCM_S64BE,
+ AV_CODEC_ID_PCM_F16LE,
+ AV_CODEC_ID_PCM_F24LE,
+ AV_CODEC_ID_PCM_VIDC,
+ AV_CODEC_ID_PCM_SGA,
+
+ /* various ADPCM codecs */
+ AV_CODEC_ID_ADPCM_IMA_QT = 0x11000,
+ AV_CODEC_ID_ADPCM_IMA_WAV,
+ AV_CODEC_ID_ADPCM_IMA_DK3,
+ AV_CODEC_ID_ADPCM_IMA_DK4,
+ AV_CODEC_ID_ADPCM_IMA_WS,
+ AV_CODEC_ID_ADPCM_IMA_SMJPEG,
+ AV_CODEC_ID_ADPCM_MS,
+ AV_CODEC_ID_ADPCM_4XM,
+ AV_CODEC_ID_ADPCM_XA,
+ AV_CODEC_ID_ADPCM_ADX,
+ AV_CODEC_ID_ADPCM_EA,
+ AV_CODEC_ID_ADPCM_G726,
+ AV_CODEC_ID_ADPCM_CT,
+ AV_CODEC_ID_ADPCM_SWF,
+ AV_CODEC_ID_ADPCM_YAMAHA,
+ AV_CODEC_ID_ADPCM_SBPRO_4,
+ AV_CODEC_ID_ADPCM_SBPRO_3,
+ AV_CODEC_ID_ADPCM_SBPRO_2,
+ AV_CODEC_ID_ADPCM_THP,
+ AV_CODEC_ID_ADPCM_IMA_AMV,
+ AV_CODEC_ID_ADPCM_EA_R1,
+ AV_CODEC_ID_ADPCM_EA_R3,
+ AV_CODEC_ID_ADPCM_EA_R2,
+ AV_CODEC_ID_ADPCM_IMA_EA_SEAD,
+ AV_CODEC_ID_ADPCM_IMA_EA_EACS,
+ AV_CODEC_ID_ADPCM_EA_XAS,
+ AV_CODEC_ID_ADPCM_EA_MAXIS_XA,
+ AV_CODEC_ID_ADPCM_IMA_ISS,
+ AV_CODEC_ID_ADPCM_G722,
+ AV_CODEC_ID_ADPCM_IMA_APC,
+ AV_CODEC_ID_ADPCM_VIMA,
+ AV_CODEC_ID_ADPCM_AFC,
+ AV_CODEC_ID_ADPCM_IMA_OKI,
+ AV_CODEC_ID_ADPCM_DTK,
+ AV_CODEC_ID_ADPCM_IMA_RAD,
+ AV_CODEC_ID_ADPCM_G726LE,
+ AV_CODEC_ID_ADPCM_THP_LE,
+ AV_CODEC_ID_ADPCM_PSX,
+ AV_CODEC_ID_ADPCM_AICA,
+ AV_CODEC_ID_ADPCM_IMA_DAT4,
+ AV_CODEC_ID_ADPCM_MTAF,
+ AV_CODEC_ID_ADPCM_AGM,
+ AV_CODEC_ID_ADPCM_ARGO,
+ AV_CODEC_ID_ADPCM_IMA_SSI,
+ AV_CODEC_ID_ADPCM_ZORK,
+ AV_CODEC_ID_ADPCM_IMA_APM,
+ AV_CODEC_ID_ADPCM_IMA_ALP,
+ AV_CODEC_ID_ADPCM_IMA_MTF,
+ AV_CODEC_ID_ADPCM_IMA_CUNNING,
+ AV_CODEC_ID_ADPCM_IMA_MOFLEX,
+ AV_CODEC_ID_ADPCM_IMA_ACORN,
+ AV_CODEC_ID_ADPCM_XMD,
+
+ /* AMR */
+ AV_CODEC_ID_AMR_NB = 0x12000,
+ AV_CODEC_ID_AMR_WB,
+
+ /* RealAudio codecs*/
+ AV_CODEC_ID_RA_144 = 0x13000,
+ AV_CODEC_ID_RA_288,
+
+ /* various DPCM codecs */
+ AV_CODEC_ID_ROQ_DPCM = 0x14000,
+ AV_CODEC_ID_INTERPLAY_DPCM,
+ AV_CODEC_ID_XAN_DPCM,
+ AV_CODEC_ID_SOL_DPCM,
+ AV_CODEC_ID_SDX2_DPCM,
+ AV_CODEC_ID_GREMLIN_DPCM,
+ AV_CODEC_ID_DERF_DPCM,
+ AV_CODEC_ID_WADY_DPCM,
+ AV_CODEC_ID_CBD2_DPCM,
+
+ /* audio codecs */
+ AV_CODEC_ID_MP2 = 0x15000,
+ AV_CODEC_ID_MP3, ///< preferred ID for decoding MPEG audio layer 1, 2 or 3
+ AV_CODEC_ID_AAC,
+ AV_CODEC_ID_AC3,
+ AV_CODEC_ID_DTS,
+ AV_CODEC_ID_VORBIS,
+ AV_CODEC_ID_DVAUDIO,
+ AV_CODEC_ID_WMAV1,
+ AV_CODEC_ID_WMAV2,
+ AV_CODEC_ID_MACE3,
+ AV_CODEC_ID_MACE6,
+ AV_CODEC_ID_VMDAUDIO,
+ AV_CODEC_ID_FLAC,
+ AV_CODEC_ID_MP3ADU,
+ AV_CODEC_ID_MP3ON4,
+ AV_CODEC_ID_SHORTEN,
+ AV_CODEC_ID_ALAC,
+ AV_CODEC_ID_WESTWOOD_SND1,
+ AV_CODEC_ID_GSM, ///< as in Berlin toast format
+ AV_CODEC_ID_QDM2,
+ AV_CODEC_ID_COOK,
+ AV_CODEC_ID_TRUESPEECH,
+ AV_CODEC_ID_TTA,
+ AV_CODEC_ID_SMACKAUDIO,
+ AV_CODEC_ID_QCELP,
+ AV_CODEC_ID_WAVPACK,
+ AV_CODEC_ID_DSICINAUDIO,
+ AV_CODEC_ID_IMC,
+ AV_CODEC_ID_MUSEPACK7,
+ AV_CODEC_ID_MLP,
+ AV_CODEC_ID_GSM_MS, /* as found in WAV */
+ AV_CODEC_ID_ATRAC3,
+ AV_CODEC_ID_APE,
+ AV_CODEC_ID_NELLYMOSER,
+ AV_CODEC_ID_MUSEPACK8,
+ AV_CODEC_ID_SPEEX,
+ AV_CODEC_ID_WMAVOICE,
+ AV_CODEC_ID_WMAPRO,
+ AV_CODEC_ID_WMALOSSLESS,
+ AV_CODEC_ID_ATRAC3P,
+ AV_CODEC_ID_EAC3,
+ AV_CODEC_ID_SIPR,
+ AV_CODEC_ID_MP1,
+ AV_CODEC_ID_TWINVQ,
+ AV_CODEC_ID_TRUEHD,
+ AV_CODEC_ID_MP4ALS,
+ AV_CODEC_ID_ATRAC1,
+ AV_CODEC_ID_BINKAUDIO_RDFT,
+ AV_CODEC_ID_BINKAUDIO_DCT,
+ AV_CODEC_ID_AAC_LATM,
+ AV_CODEC_ID_QDMC,
+ AV_CODEC_ID_CELT,
+ AV_CODEC_ID_G723_1,
+ AV_CODEC_ID_G729,
+ AV_CODEC_ID_8SVX_EXP,
+ AV_CODEC_ID_8SVX_FIB,
+ AV_CODEC_ID_BMV_AUDIO,
+ AV_CODEC_ID_RALF,
+ AV_CODEC_ID_IAC,
+ AV_CODEC_ID_ILBC,
+ AV_CODEC_ID_OPUS,
+ AV_CODEC_ID_COMFORT_NOISE,
+ AV_CODEC_ID_TAK,
+ AV_CODEC_ID_METASOUND,
+ AV_CODEC_ID_PAF_AUDIO,
+ AV_CODEC_ID_ON2AVC,
+ AV_CODEC_ID_DSS_SP,
+ AV_CODEC_ID_CODEC2,
+ AV_CODEC_ID_FFWAVESYNTH,
+ AV_CODEC_ID_SONIC,
+ AV_CODEC_ID_SONIC_LS,
+ AV_CODEC_ID_EVRC,
+ AV_CODEC_ID_SMV,
+ AV_CODEC_ID_DSD_LSBF,
+ AV_CODEC_ID_DSD_MSBF,
+ AV_CODEC_ID_DSD_LSBF_PLANAR,
+ AV_CODEC_ID_DSD_MSBF_PLANAR,
+ AV_CODEC_ID_4GV,
+ AV_CODEC_ID_INTERPLAY_ACM,
+ AV_CODEC_ID_XMA1,
+ AV_CODEC_ID_XMA2,
+ AV_CODEC_ID_DST,
+ AV_CODEC_ID_ATRAC3AL,
+ AV_CODEC_ID_ATRAC3PAL,
+ AV_CODEC_ID_DOLBY_E,
+ AV_CODEC_ID_APTX,
+ AV_CODEC_ID_APTX_HD,
+ AV_CODEC_ID_SBC,
+ AV_CODEC_ID_ATRAC9,
+ AV_CODEC_ID_HCOM,
+ AV_CODEC_ID_ACELP_KELVIN,
+ AV_CODEC_ID_MPEGH_3D_AUDIO,
+ AV_CODEC_ID_SIREN,
+ AV_CODEC_ID_HCA,
+ AV_CODEC_ID_FASTAUDIO,
+ AV_CODEC_ID_MSNSIREN,
+ AV_CODEC_ID_DFPWM,
+ AV_CODEC_ID_BONK,
+ AV_CODEC_ID_MISC4,
+ AV_CODEC_ID_APAC,
+ AV_CODEC_ID_FTR,
+ AV_CODEC_ID_WAVARC,
+ AV_CODEC_ID_RKA,
+
+ /* subtitle codecs */
+ AV_CODEC_ID_FIRST_SUBTITLE =
+ 0x17000, ///< A dummy ID pointing at the start of subtitle codecs.
+ AV_CODEC_ID_DVD_SUBTITLE = 0x17000,
+ AV_CODEC_ID_DVB_SUBTITLE,
+ AV_CODEC_ID_TEXT, ///< raw UTF-8 text
+ AV_CODEC_ID_XSUB,
+ AV_CODEC_ID_SSA,
+ AV_CODEC_ID_MOV_TEXT,
+ AV_CODEC_ID_HDMV_PGS_SUBTITLE,
+ AV_CODEC_ID_DVB_TELETEXT,
+ AV_CODEC_ID_SRT,
+ AV_CODEC_ID_MICRODVD,
+ AV_CODEC_ID_EIA_608,
+ AV_CODEC_ID_JACOSUB,
+ AV_CODEC_ID_SAMI,
+ AV_CODEC_ID_REALTEXT,
+ AV_CODEC_ID_STL,
+ AV_CODEC_ID_SUBVIEWER1,
+ AV_CODEC_ID_SUBVIEWER,
+ AV_CODEC_ID_SUBRIP,
+ AV_CODEC_ID_WEBVTT,
+ AV_CODEC_ID_MPL2,
+ AV_CODEC_ID_VPLAYER,
+ AV_CODEC_ID_PJS,
+ AV_CODEC_ID_ASS,
+ AV_CODEC_ID_HDMV_TEXT_SUBTITLE,
+ AV_CODEC_ID_TTML,
+ AV_CODEC_ID_ARIB_CAPTION,
+
+ /* other specific kind of codecs (generally used for attachments) */
+ AV_CODEC_ID_FIRST_UNKNOWN =
+ 0x18000, ///< A dummy ID pointing at the start of various fake codecs.
+ AV_CODEC_ID_TTF = 0x18000,
+
+ AV_CODEC_ID_SCTE_35, ///< Contain timestamp estimated through PCR of program
+ ///< stream.
+ AV_CODEC_ID_EPG,
+ AV_CODEC_ID_BINTEXT,
+ AV_CODEC_ID_XBIN,
+ AV_CODEC_ID_IDF,
+ AV_CODEC_ID_OTF,
+ AV_CODEC_ID_SMPTE_KLV,
+ AV_CODEC_ID_DVD_NAV,
+ AV_CODEC_ID_TIMED_ID3,
+ AV_CODEC_ID_BIN_DATA,
+
+ AV_CODEC_ID_PROBE =
+ 0x19000, ///< codec_id is not known (like AV_CODEC_ID_NONE) but lavf
+ ///< should attempt to identify it
+
+ AV_CODEC_ID_MPEG2TS = 0x20000, /**< _FAKE_ codec to indicate a raw MPEG-2 TS
+ * stream (only used by libavformat) */
+ AV_CODEC_ID_MPEG4SYSTEMS =
+ 0x20001, /**< _FAKE_ codec to indicate a MPEG-4 Systems
+ * stream (only used by libavformat) */
+ AV_CODEC_ID_FFMETADATA = 0x21000, ///< Dummy codec for streams containing
+ ///< only metadata information.
+ AV_CODEC_ID_WRAPPED_AVFRAME =
+ 0x21001, ///< Passthrough codec, AVFrames wrapped in AVPacket
+ /**
+ * Dummy null video codec, useful mainly for development and debugging.
+ * Null encoder/decoder discard all input and never return any output.
+ */
+ AV_CODEC_ID_VNULL,
+ /**
+ * Dummy null audio codec, useful mainly for development and debugging.
+ * Null encoder/decoder discard all input and never return any output.
+ */
+ AV_CODEC_ID_ANULL,
+};
+
+/**
+ * Get the type of the given codec.
+ */
+enum AVMediaType avcodec_get_type(enum AVCodecID codec_id);
+
+/**
+ * Get the name of a codec.
+ * @return a static string identifying the codec; never NULL
+ */
+const char* avcodec_get_name(enum AVCodecID id);
+
+/**
+ * Return codec bits per sample.
+ *
+ * @param[in] codec_id the codec
+ * @return Number of bits per sample or zero if unknown for the given codec.
+ */
+int av_get_bits_per_sample(enum AVCodecID codec_id);
+
+/**
+ * Return codec bits per sample.
+ * Only return non-zero if the bits per sample is exactly correct, not an
+ * approximation.
+ *
+ * @param[in] codec_id the codec
+ * @return Number of bits per sample or zero if unknown for the given codec.
+ */
+int av_get_exact_bits_per_sample(enum AVCodecID codec_id);
+
+/**
+ * Return a name for the specified profile, if available.
+ *
+ * @param codec_id the ID of the codec to which the requested profile belongs
+ * @param profile the profile value for which a name is requested
+ * @return A name for the profile if found, NULL otherwise.
+ *
+ * @note unlike av_get_profile_name(), which searches a list of profiles
+ * supported by a specific decoder or encoder implementation, this
+ * function searches the list of profiles from the AVCodecDescriptor
+ */
+const char* avcodec_profile_name(enum AVCodecID codec_id, int profile);
+
+/**
+ * Return the PCM codec associated with a sample format.
+ * @param be endianness, 0 for little, 1 for big,
+ * -1 (or anything else) for native
+ * @return AV_CODEC_ID_PCM_* or AV_CODEC_ID_NONE
+ */
+enum AVCodecID av_get_pcm_codec(enum AVSampleFormat fmt, int be);
+
+/**
+ * @}
+ */
+
+#endif // AVCODEC_CODEC_ID_H
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/codec_par.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/codec_par.h
new file mode 100644
index 0000000000..dd9e40d0d6
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/codec_par.h
@@ -0,0 +1,247 @@
+/*
+ * Codec parameters public API
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_CODEC_PAR_H
+#define AVCODEC_CODEC_PAR_H
+
+#include <stdint.h>
+
+#include "libavutil/avutil.h"
+#include "libavutil/channel_layout.h"
+#include "libavutil/rational.h"
+#include "libavutil/pixfmt.h"
+
+#include "codec_id.h"
+
+/**
+ * @addtogroup lavc_core
+ * @{
+ */
+
+enum AVFieldOrder {
+ AV_FIELD_UNKNOWN,
+ AV_FIELD_PROGRESSIVE,
+ AV_FIELD_TT, ///< Top coded_first, top displayed first
+ AV_FIELD_BB, ///< Bottom coded first, bottom displayed first
+ AV_FIELD_TB, ///< Top coded first, bottom displayed first
+ AV_FIELD_BT, ///< Bottom coded first, top displayed first
+};
+
+/**
+ * This struct describes the properties of an encoded stream.
+ *
+ * sizeof(AVCodecParameters) is not a part of the public ABI, this struct must
+ * be allocated with avcodec_parameters_alloc() and freed with
+ * avcodec_parameters_free().
+ */
+typedef struct AVCodecParameters {
+ /**
+ * General type of the encoded data.
+ */
+ enum AVMediaType codec_type;
+ /**
+ * Specific type of the encoded data (the codec used).
+ */
+ enum AVCodecID codec_id;
+ /**
+ * Additional information about the codec (corresponds to the AVI FOURCC).
+ */
+ uint32_t codec_tag;
+
+ /**
+ * Extra binary data needed for initializing the decoder, codec-dependent.
+ *
+ * Must be allocated with av_malloc() and will be freed by
+ * avcodec_parameters_free(). The allocated size of extradata must be at
+ * least extradata_size + AV_INPUT_BUFFER_PADDING_SIZE, with the padding
+ * bytes zeroed.
+ */
+ uint8_t* extradata;
+ /**
+ * Size of the extradata content in bytes.
+ */
+ int extradata_size;
+
+ /**
+ * - video: the pixel format, the value corresponds to enum AVPixelFormat.
+ * - audio: the sample format, the value corresponds to enum AVSampleFormat.
+ */
+ int format;
+
+ /**
+ * The average bitrate of the encoded data (in bits per second).
+ */
+ int64_t bit_rate;
+
+ /**
+ * The number of bits per sample in the codedwords.
+ *
+ * This is basically the bitrate per sample. It is mandatory for a bunch of
+ * formats to actually decode them. It's the number of bits for one sample in
+ * the actual coded bitstream.
+ *
+ * This could be for example 4 for ADPCM
+ * For PCM formats this matches bits_per_raw_sample
+ * Can be 0
+ */
+ int bits_per_coded_sample;
+
+ /**
+ * This is the number of valid bits in each output sample. If the
+ * sample format has more bits, the least significant bits are additional
+ * padding bits, which are always 0. Use right shifts to reduce the sample
+ * to its actual size. For example, audio formats with 24 bit samples will
+ * have bits_per_raw_sample set to 24, and format set to AV_SAMPLE_FMT_S32.
+ * To get the original sample use "(int32_t)sample >> 8"."
+ *
+ * For ADPCM this might be 12 or 16 or similar
+ * Can be 0
+ */
+ int bits_per_raw_sample;
+
+ /**
+ * Codec-specific bitstream restrictions that the stream conforms to.
+ */
+ int profile;
+ int level;
+
+ /**
+ * Video only. The dimensions of the video frame in pixels.
+ */
+ int width;
+ int height;
+
+ /**
+ * Video only. The aspect ratio (width / height) which a single pixel
+ * should have when displayed.
+ *
+ * When the aspect ratio is unknown / undefined, the numerator should be
+ * set to 0 (the denominator may have any value).
+ */
+ AVRational sample_aspect_ratio;
+
+ /**
+ * Video only. The order of the fields in interlaced video.
+ */
+ enum AVFieldOrder field_order;
+
+ /**
+ * Video only. Additional colorspace characteristics.
+ */
+ enum AVColorRange color_range;
+ enum AVColorPrimaries color_primaries;
+ enum AVColorTransferCharacteristic color_trc;
+ enum AVColorSpace color_space;
+ enum AVChromaLocation chroma_location;
+
+ /**
+ * Video only. Number of delayed frames.
+ */
+ int video_delay;
+
+#if FF_API_OLD_CHANNEL_LAYOUT
+ /**
+ * Audio only. The channel layout bitmask. May be 0 if the channel layout is
+ * unknown or unspecified, otherwise the number of bits set must be equal to
+ * the channels field.
+ * @deprecated use ch_layout
+ */
+ attribute_deprecated uint64_t channel_layout;
+ /**
+ * Audio only. The number of audio channels.
+ * @deprecated use ch_layout.nb_channels
+ */
+ attribute_deprecated int channels;
+#endif
+ /**
+ * Audio only. The number of audio samples per second.
+ */
+ int sample_rate;
+ /**
+ * Audio only. The number of bytes per coded audio frame, required by some
+ * formats.
+ *
+ * Corresponds to nBlockAlign in WAVEFORMATEX.
+ */
+ int block_align;
+ /**
+ * Audio only. Audio frame size, if known. Required by some formats to be
+ * static.
+ */
+ int frame_size;
+
+ /**
+ * Audio only. The amount of padding (in samples) inserted by the encoder at
+ * the beginning of the audio. I.e. this number of leading decoded samples
+ * must be discarded by the caller to get the original audio without leading
+ * padding.
+ */
+ int initial_padding;
+ /**
+ * Audio only. The amount of padding (in samples) appended by the encoder to
+ * the end of the audio. I.e. this number of decoded samples must be
+ * discarded by the caller from the end of the stream to get the original
+ * audio without any trailing padding.
+ */
+ int trailing_padding;
+ /**
+ * Audio only. Number of samples to skip after a discontinuity.
+ */
+ int seek_preroll;
+
+ /**
+ * Audio only. The channel layout and number of channels.
+ */
+ AVChannelLayout ch_layout;
+} AVCodecParameters;
+
+/**
+ * Allocate a new AVCodecParameters and set its fields to default values
+ * (unknown/invalid/0). The returned struct must be freed with
+ * avcodec_parameters_free().
+ */
+AVCodecParameters* avcodec_parameters_alloc(void);
+
+/**
+ * Free an AVCodecParameters instance and everything associated with it and
+ * write NULL to the supplied pointer.
+ */
+void avcodec_parameters_free(AVCodecParameters** par);
+
+/**
+ * Copy the contents of src to dst. Any allocated fields in dst are freed and
+ * replaced with newly allocated duplicates of the corresponding fields in src.
+ *
+ * @return >= 0 on success, a negative AVERROR code on failure.
+ */
+int avcodec_parameters_copy(AVCodecParameters* dst,
+ const AVCodecParameters* src);
+
+/**
+ * This function is the same as av_get_audio_frame_duration(), except it works
+ * with AVCodecParameters instead of an AVCodecContext.
+ */
+int av_get_audio_frame_duration2(AVCodecParameters* par, int frame_bytes);
+
+/**
+ * @}
+ */
+
+#endif // AVCODEC_CODEC_PAR_H
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/defs.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/defs.h
new file mode 100644
index 0000000000..9a4632cee4
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/defs.h
@@ -0,0 +1,203 @@
+/*
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_DEFS_H
+#define AVCODEC_DEFS_H
+
+/**
+ * @file
+ * @ingroup libavc
+ * Misc types and constants that do not belong anywhere else.
+ */
+
+#include <stdint.h>
+#include <stdlib.h>
+
+/**
+ * @ingroup lavc_decoding
+ * Required number of additionally allocated bytes at the end of the input
+ * bitstream for decoding. This is mainly needed because some optimized
+ * bitstream readers read 32 or 64 bit at once and could read over the end.<br>
+ * Note: If the first 23 bits of the additional bytes are not 0, then damaged
+ * MPEG bitstreams could cause overread and segfault.
+ */
+#define AV_INPUT_BUFFER_PADDING_SIZE 64
+
+/**
+ * Verify checksums embedded in the bitstream (could be of either encoded or
+ * decoded data, depending on the format) and print an error message on
+ * mismatch. If AV_EF_EXPLODE is also set, a mismatching checksum will result in
+ * the decoder/demuxer returning an error.
+ */
+#define AV_EF_CRCCHECK (1 << 0)
+#define AV_EF_BITSTREAM (1 << 1) ///< detect bitstream specification deviations
+#define AV_EF_BUFFER (1 << 2) ///< detect improper bitstream length
+#define AV_EF_EXPLODE (1 << 3) ///< abort decoding on minor error detection
+
+#define AV_EF_IGNORE_ERR (1 << 15) ///< ignore errors and continue
+#define AV_EF_CAREFUL \
+ (1 << 16) ///< consider things that violate the spec, are fast to calculate
+ ///< and have not been seen in the wild as errors
+#define AV_EF_COMPLIANT \
+ (1 << 17) ///< consider all spec non compliances as errors
+#define AV_EF_AGGRESSIVE \
+ (1 << 18) ///< consider things that a sane encoder/muxer should not do as an
+ ///< error
+
+#define FF_COMPLIANCE_VERY_STRICT \
+ 2 ///< Strictly conform to an older more strict version of the spec or
+ ///< reference software.
+#define FF_COMPLIANCE_STRICT \
+ 1 ///< Strictly conform to all the things in the spec no matter what
+ ///< consequences.
+#define FF_COMPLIANCE_NORMAL 0
+#define FF_COMPLIANCE_UNOFFICIAL -1 ///< Allow unofficial extensions
+#define FF_COMPLIANCE_EXPERIMENTAL \
+ -2 ///< Allow nonstandardized experimental things.
+
+/**
+ * @ingroup lavc_decoding
+ */
+enum AVDiscard {
+ /* We leave some space between them for extensions (drop some
+ * keyframes for intra-only or drop just some bidir frames). */
+ AVDISCARD_NONE = -16, ///< discard nothing
+ AVDISCARD_DEFAULT =
+ 0, ///< discard useless packets like 0 size packets in avi
+ AVDISCARD_NONREF = 8, ///< discard all non reference
+ AVDISCARD_BIDIR = 16, ///< discard all bidirectional frames
+ AVDISCARD_NONINTRA = 24, ///< discard all non intra frames
+ AVDISCARD_NONKEY = 32, ///< discard all frames except keyframes
+ AVDISCARD_ALL = 48, ///< discard all
+};
+
+enum AVAudioServiceType {
+ AV_AUDIO_SERVICE_TYPE_MAIN = 0,
+ AV_AUDIO_SERVICE_TYPE_EFFECTS = 1,
+ AV_AUDIO_SERVICE_TYPE_VISUALLY_IMPAIRED = 2,
+ AV_AUDIO_SERVICE_TYPE_HEARING_IMPAIRED = 3,
+ AV_AUDIO_SERVICE_TYPE_DIALOGUE = 4,
+ AV_AUDIO_SERVICE_TYPE_COMMENTARY = 5,
+ AV_AUDIO_SERVICE_TYPE_EMERGENCY = 6,
+ AV_AUDIO_SERVICE_TYPE_VOICE_OVER = 7,
+ AV_AUDIO_SERVICE_TYPE_KARAOKE = 8,
+ AV_AUDIO_SERVICE_TYPE_NB, ///< Not part of ABI
+};
+
+/**
+ * Pan Scan area.
+ * This specifies the area which should be displayed.
+ * Note there may be multiple such areas for one frame.
+ */
+typedef struct AVPanScan {
+ /**
+ * id
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int id;
+
+ /**
+ * width and height in 1/16 pel
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int width;
+ int height;
+
+ /**
+ * position of the top left corner in 1/16 pel for up to 3 fields/frames
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int16_t position[3][2];
+} AVPanScan;
+
+/**
+ * This structure describes the bitrate properties of an encoded bitstream. It
+ * roughly corresponds to a subset the VBV parameters for MPEG-2 or HRD
+ * parameters for H.264/HEVC.
+ */
+typedef struct AVCPBProperties {
+ /**
+ * Maximum bitrate of the stream, in bits per second.
+ * Zero if unknown or unspecified.
+ */
+ int64_t max_bitrate;
+ /**
+ * Minimum bitrate of the stream, in bits per second.
+ * Zero if unknown or unspecified.
+ */
+ int64_t min_bitrate;
+ /**
+ * Average bitrate of the stream, in bits per second.
+ * Zero if unknown or unspecified.
+ */
+ int64_t avg_bitrate;
+
+ /**
+ * The size of the buffer to which the ratecontrol is applied, in bits.
+ * Zero if unknown or unspecified.
+ */
+ int64_t buffer_size;
+
+ /**
+ * The delay between the time the packet this structure is associated with
+ * is received and the time when it should be decoded, in periods of a 27MHz
+ * clock.
+ *
+ * UINT64_MAX when unknown or unspecified.
+ */
+ uint64_t vbv_delay;
+} AVCPBProperties;
+
+/**
+ * Allocate a CPB properties structure and initialize its fields to default
+ * values.
+ *
+ * @param size if non-NULL, the size of the allocated struct will be written
+ * here. This is useful for embedding it in side data.
+ *
+ * @return the newly allocated struct or NULL on failure
+ */
+AVCPBProperties* av_cpb_properties_alloc(size_t* size);
+
+/**
+ * This structure supplies correlation between a packet timestamp and a wall
+ * clock production time. The definition follows the Producer Reference Time
+ * ('prft') as defined in ISO/IEC 14496-12
+ */
+typedef struct AVProducerReferenceTime {
+ /**
+ * A UTC timestamp, in microseconds, since Unix epoch (e.g, av_gettime()).
+ */
+ int64_t wallclock;
+ int flags;
+} AVProducerReferenceTime;
+
+/**
+ * Encode extradata length to a buffer. Used by xiph codecs.
+ *
+ * @param s buffer to write to; must be at least (v/255+1) bytes long
+ * @param v size of extradata in bytes
+ * @return number of bytes written to the buffer.
+ */
+unsigned int av_xiphlacing(unsigned char* s, unsigned int v);
+
+#endif // AVCODEC_DEFS_H
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/packet.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/packet.h
new file mode 100644
index 0000000000..d85cf6a84b
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/packet.h
@@ -0,0 +1,730 @@
+/*
+ * AVPacket public API
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_PACKET_H
+#define AVCODEC_PACKET_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "libavutil/attributes.h"
+#include "libavutil/buffer.h"
+#include "libavutil/dict.h"
+#include "libavutil/rational.h"
+#include "libavutil/version.h"
+
+#include "libavcodec/version_major.h"
+
+/**
+ * @defgroup lavc_packet AVPacket
+ *
+ * Types and functions for working with AVPacket.
+ * @{
+ */
+enum AVPacketSideDataType {
+ /**
+ * An AV_PKT_DATA_PALETTE side data packet contains exactly AVPALETTE_SIZE
+ * bytes worth of palette. This side data signals that a new palette is
+ * present.
+ */
+ AV_PKT_DATA_PALETTE,
+
+ /**
+ * The AV_PKT_DATA_NEW_EXTRADATA is used to notify the codec or the format
+ * that the extradata buffer was changed and the receiving side should
+ * act upon it appropriately. The new extradata is embedded in the side
+ * data buffer and should be immediately used for processing the current
+ * frame or packet.
+ */
+ AV_PKT_DATA_NEW_EXTRADATA,
+
+ /**
+ * An AV_PKT_DATA_PARAM_CHANGE side data packet is laid out as follows:
+ * @code
+ * u32le param_flags
+ * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_COUNT)
+ * s32le channel_count
+ * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_LAYOUT)
+ * u64le channel_layout
+ * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_SAMPLE_RATE)
+ * s32le sample_rate
+ * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_DIMENSIONS)
+ * s32le width
+ * s32le height
+ * @endcode
+ */
+ AV_PKT_DATA_PARAM_CHANGE,
+
+ /**
+ * An AV_PKT_DATA_H263_MB_INFO side data packet contains a number of
+ * structures with info about macroblocks relevant to splitting the
+ * packet into smaller packets on macroblock edges (e.g. as for RFC 2190).
+ * That is, it does not necessarily contain info about all macroblocks,
+ * as long as the distance between macroblocks in the info is smaller
+ * than the target payload size.
+ * Each MB info structure is 12 bytes, and is laid out as follows:
+ * @code
+ * u32le bit offset from the start of the packet
+ * u8 current quantizer at the start of the macroblock
+ * u8 GOB number
+ * u16le macroblock address within the GOB
+ * u8 horizontal MV predictor
+ * u8 vertical MV predictor
+ * u8 horizontal MV predictor for block number 3
+ * u8 vertical MV predictor for block number 3
+ * @endcode
+ */
+ AV_PKT_DATA_H263_MB_INFO,
+
+ /**
+ * This side data should be associated with an audio stream and contains
+ * ReplayGain information in form of the AVReplayGain struct.
+ */
+ AV_PKT_DATA_REPLAYGAIN,
+
+ /**
+ * This side data contains a 3x3 transformation matrix describing an affine
+ * transformation that needs to be applied to the decoded video frames for
+ * correct presentation.
+ *
+ * See libavutil/display.h for a detailed description of the data.
+ */
+ AV_PKT_DATA_DISPLAYMATRIX,
+
+ /**
+ * This side data should be associated with a video stream and contains
+ * Stereoscopic 3D information in form of the AVStereo3D struct.
+ */
+ AV_PKT_DATA_STEREO3D,
+
+ /**
+ * This side data should be associated with an audio stream and corresponds
+ * to enum AVAudioServiceType.
+ */
+ AV_PKT_DATA_AUDIO_SERVICE_TYPE,
+
+ /**
+ * This side data contains quality related information from the encoder.
+ * @code
+ * u32le quality factor of the compressed frame. Allowed range is between 1
+ * (good) and FF_LAMBDA_MAX (bad). u8 picture type u8 error count u16
+ * reserved u64le[error count] sum of squared differences between encoder in
+ * and output
+ * @endcode
+ */
+ AV_PKT_DATA_QUALITY_STATS,
+
+ /**
+ * This side data contains an integer value representing the stream index
+ * of a "fallback" track. A fallback track indicates an alternate
+ * track to use when the current track can not be decoded for some reason.
+ * e.g. no decoder available for codec.
+ */
+ AV_PKT_DATA_FALLBACK_TRACK,
+
+ /**
+ * This side data corresponds to the AVCPBProperties struct.
+ */
+ AV_PKT_DATA_CPB_PROPERTIES,
+
+ /**
+ * Recommmends skipping the specified number of samples
+ * @code
+ * u32le number of samples to skip from start of this packet
+ * u32le number of samples to skip from end of this packet
+ * u8 reason for start skip
+ * u8 reason for end skip (0=padding silence, 1=convergence)
+ * @endcode
+ */
+ AV_PKT_DATA_SKIP_SAMPLES,
+
+ /**
+ * An AV_PKT_DATA_JP_DUALMONO side data packet indicates that
+ * the packet may contain "dual mono" audio specific to Japanese DTV
+ * and if it is true, recommends only the selected channel to be used.
+ * @code
+ * u8 selected channels (0=main/left, 1=sub/right, 2=both)
+ * @endcode
+ */
+ AV_PKT_DATA_JP_DUALMONO,
+
+ /**
+ * A list of zero terminated key/value strings. There is no end marker for
+ * the list, so it is required to rely on the side data size to stop.
+ */
+ AV_PKT_DATA_STRINGS_METADATA,
+
+ /**
+ * Subtitle event position
+ * @code
+ * u32le x1
+ * u32le y1
+ * u32le x2
+ * u32le y2
+ * @endcode
+ */
+ AV_PKT_DATA_SUBTITLE_POSITION,
+
+ /**
+ * Data found in BlockAdditional element of matroska container. There is
+ * no end marker for the data, so it is required to rely on the side data
+ * size to recognize the end. 8 byte id (as found in BlockAddId) followed
+ * by data.
+ */
+ AV_PKT_DATA_MATROSKA_BLOCKADDITIONAL,
+
+ /**
+ * The optional first identifier line of a WebVTT cue.
+ */
+ AV_PKT_DATA_WEBVTT_IDENTIFIER,
+
+ /**
+ * The optional settings (rendering instructions) that immediately
+ * follow the timestamp specifier of a WebVTT cue.
+ */
+ AV_PKT_DATA_WEBVTT_SETTINGS,
+
+ /**
+ * A list of zero terminated key/value strings. There is no end marker for
+ * the list, so it is required to rely on the side data size to stop. This
+ * side data includes updated metadata which appeared in the stream.
+ */
+ AV_PKT_DATA_METADATA_UPDATE,
+
+ /**
+ * MPEGTS stream ID as uint8_t, this is required to pass the stream ID
+ * information from the demuxer to the corresponding muxer.
+ */
+ AV_PKT_DATA_MPEGTS_STREAM_ID,
+
+ /**
+ * Mastering display metadata (based on SMPTE-2086:2014). This metadata
+ * should be associated with a video stream and contains data in the form
+ * of the AVMasteringDisplayMetadata struct.
+ */
+ AV_PKT_DATA_MASTERING_DISPLAY_METADATA,
+
+ /**
+ * This side data should be associated with a video stream and corresponds
+ * to the AVSphericalMapping structure.
+ */
+ AV_PKT_DATA_SPHERICAL,
+
+ /**
+ * Content light level (based on CTA-861.3). This metadata should be
+ * associated with a video stream and contains data in the form of the
+ * AVContentLightMetadata struct.
+ */
+ AV_PKT_DATA_CONTENT_LIGHT_LEVEL,
+
+ /**
+ * ATSC A53 Part 4 Closed Captions. This metadata should be associated with
+ * a video stream. A53 CC bitstream is stored as uint8_t in
+ * AVPacketSideData.data. The number of bytes of CC data is
+ * AVPacketSideData.size.
+ */
+ AV_PKT_DATA_A53_CC,
+
+ /**
+ * This side data is encryption initialization data.
+ * The format is not part of ABI, use av_encryption_init_info_* methods to
+ * access.
+ */
+ AV_PKT_DATA_ENCRYPTION_INIT_INFO,
+
+ /**
+ * This side data contains encryption info for how to decrypt the packet.
+ * The format is not part of ABI, use av_encryption_info_* methods to access.
+ */
+ AV_PKT_DATA_ENCRYPTION_INFO,
+
+ /**
+ * Active Format Description data consisting of a single byte as specified
+ * in ETSI TS 101 154 using AVActiveFormatDescription enum.
+ */
+ AV_PKT_DATA_AFD,
+
+ /**
+ * Producer Reference Time data corresponding to the AVProducerReferenceTime
+ * struct, usually exported by some encoders (on demand through the prft flag
+ * set in the AVCodecContext export_side_data field).
+ */
+ AV_PKT_DATA_PRFT,
+
+ /**
+ * ICC profile data consisting of an opaque octet buffer following the
+ * format described by ISO 15076-1.
+ */
+ AV_PKT_DATA_ICC_PROFILE,
+
+ /**
+ * DOVI configuration
+ * ref:
+ * dolby-vision-bitstreams-within-the-iso-base-media-file-format-v2.1.2,
+ * section 2.2
+ * dolby-vision-bitstreams-in-mpeg-2-transport-stream-multiplex-v1.2,
+ * section 3.3 Tags are stored in struct AVDOVIDecoderConfigurationRecord.
+ */
+ AV_PKT_DATA_DOVI_CONF,
+
+ /**
+ * Timecode which conforms to SMPTE ST 12-1:2014. The data is an array of 4
+ * uint32_t where the first uint32_t describes how many (1-3) of the other
+ * timecodes are used. The timecode format is described in the documentation
+ * of av_timecode_get_smpte_from_framenum() function in libavutil/timecode.h.
+ */
+ AV_PKT_DATA_S12M_TIMECODE,
+
+ /**
+ * HDR10+ dynamic metadata associated with a video frame. The metadata is in
+ * the form of the AVDynamicHDRPlus struct and contains
+ * information for color volume transform - application 4 of
+ * SMPTE 2094-40:2016 standard.
+ */
+ AV_PKT_DATA_DYNAMIC_HDR10_PLUS,
+
+ /**
+ * The number of side data types.
+ * This is not part of the public API/ABI in the sense that it may
+ * change when new side data types are added.
+ * This must stay the last enum value.
+ * If its value becomes huge, some code using it
+ * needs to be updated as it assumes it to be smaller than other limits.
+ */
+ AV_PKT_DATA_NB
+};
+
+#define AV_PKT_DATA_QUALITY_FACTOR AV_PKT_DATA_QUALITY_STATS // DEPRECATED
+
+typedef struct AVPacketSideData {
+ uint8_t* data;
+ size_t size;
+ enum AVPacketSideDataType type;
+} AVPacketSideData;
+
+/**
+ * This structure stores compressed data. It is typically exported by demuxers
+ * and then passed as input to decoders, or received as output from encoders and
+ * then passed to muxers.
+ *
+ * For video, it should typically contain one compressed frame. For audio it may
+ * contain several compressed frames. Encoders are allowed to output empty
+ * packets, with no compressed data, containing only side data
+ * (e.g. to update some stream parameters at the end of encoding).
+ *
+ * The semantics of data ownership depends on the buf field.
+ * If it is set, the packet data is dynamically allocated and is
+ * valid indefinitely until a call to av_packet_unref() reduces the
+ * reference count to 0.
+ *
+ * If the buf field is not set av_packet_ref() would make a copy instead
+ * of increasing the reference count.
+ *
+ * The side data is always allocated with av_malloc(), copied by
+ * av_packet_ref() and freed by av_packet_unref().
+ *
+ * sizeof(AVPacket) being a part of the public ABI is deprecated. once
+ * av_init_packet() is removed, new packets will only be able to be allocated
+ * with av_packet_alloc(), and new fields may be added to the end of the struct
+ * with a minor bump.
+ *
+ * @see av_packet_alloc
+ * @see av_packet_ref
+ * @see av_packet_unref
+ */
+typedef struct AVPacket {
+ /**
+ * A reference to the reference-counted buffer where the packet data is
+ * stored.
+ * May be NULL, then the packet data is not reference-counted.
+ */
+ AVBufferRef* buf;
+ /**
+ * Presentation timestamp in AVStream->time_base units; the time at which
+ * the decompressed packet will be presented to the user.
+ * Can be AV_NOPTS_VALUE if it is not stored in the file.
+ * pts MUST be larger or equal to dts as presentation cannot happen before
+ * decompression, unless one wants to view hex dumps. Some formats misuse
+ * the terms dts and pts/cts to mean something different. Such timestamps
+ * must be converted to true pts/dts before they are stored in AVPacket.
+ */
+ int64_t pts;
+ /**
+ * Decompression timestamp in AVStream->time_base units; the time at which
+ * the packet is decompressed.
+ * Can be AV_NOPTS_VALUE if it is not stored in the file.
+ */
+ int64_t dts;
+ uint8_t* data;
+ int size;
+ int stream_index;
+ /**
+ * A combination of AV_PKT_FLAG values
+ */
+ int flags;
+ /**
+ * Additional packet data that can be provided by the container.
+ * Packet can contain several types of side information.
+ */
+ AVPacketSideData* side_data;
+ int side_data_elems;
+
+ /**
+ * Duration of this packet in AVStream->time_base units, 0 if unknown.
+ * Equals next_pts - this_pts in presentation order.
+ */
+ int64_t duration;
+
+ int64_t pos; ///< byte position in stream, -1 if unknown
+
+ /**
+ * for some private data of the user
+ */
+ void* opaque;
+
+ /**
+ * AVBufferRef for free use by the API user. FFmpeg will never check the
+ * contents of the buffer ref. FFmpeg calls av_buffer_unref() on it when
+ * the packet is unreferenced. av_packet_copy_props() calls create a new
+ * reference with av_buffer_ref() for the target packet's opaque_ref field.
+ *
+ * This is unrelated to the opaque field, although it serves a similar
+ * purpose.
+ */
+ AVBufferRef* opaque_ref;
+
+ /**
+ * Time base of the packet's timestamps.
+ * In the future, this field may be set on packets output by encoders or
+ * demuxers, but its value will be by default ignored on input to decoders
+ * or muxers.
+ */
+ AVRational time_base;
+} AVPacket;
+
+#if FF_API_INIT_PACKET
+attribute_deprecated typedef struct AVPacketList {
+ AVPacket pkt;
+ struct AVPacketList* next;
+} AVPacketList;
+#endif
+
+#define AV_PKT_FLAG_KEY 0x0001 ///< The packet contains a keyframe
+#define AV_PKT_FLAG_CORRUPT 0x0002 ///< The packet content is corrupted
+/**
+ * Flag is used to discard packets which are required to maintain valid
+ * decoder state but are not required for output and should be dropped
+ * after decoding.
+ **/
+#define AV_PKT_FLAG_DISCARD 0x0004
+/**
+ * The packet comes from a trusted source.
+ *
+ * Otherwise-unsafe constructs such as arbitrary pointers to data
+ * outside the packet may be followed.
+ */
+#define AV_PKT_FLAG_TRUSTED 0x0008
+/**
+ * Flag is used to indicate packets that contain frames that can
+ * be discarded by the decoder. I.e. Non-reference frames.
+ */
+#define AV_PKT_FLAG_DISPOSABLE 0x0010
+
+enum AVSideDataParamChangeFlags {
+#if FF_API_OLD_CHANNEL_LAYOUT
+ /**
+ * @deprecated those are not used by any decoder
+ */
+ AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_COUNT = 0x0001,
+ AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_LAYOUT = 0x0002,
+#endif
+ AV_SIDE_DATA_PARAM_CHANGE_SAMPLE_RATE = 0x0004,
+ AV_SIDE_DATA_PARAM_CHANGE_DIMENSIONS = 0x0008,
+};
+
+/**
+ * Allocate an AVPacket and set its fields to default values. The resulting
+ * struct must be freed using av_packet_free().
+ *
+ * @return An AVPacket filled with default values or NULL on failure.
+ *
+ * @note this only allocates the AVPacket itself, not the data buffers. Those
+ * must be allocated through other means such as av_new_packet.
+ *
+ * @see av_new_packet
+ */
+AVPacket* av_packet_alloc(void);
+
+/**
+ * Create a new packet that references the same data as src.
+ *
+ * This is a shortcut for av_packet_alloc()+av_packet_ref().
+ *
+ * @return newly created AVPacket on success, NULL on error.
+ *
+ * @see av_packet_alloc
+ * @see av_packet_ref
+ */
+AVPacket* av_packet_clone(const AVPacket* src);
+
+/**
+ * Free the packet, if the packet is reference counted, it will be
+ * unreferenced first.
+ *
+ * @param pkt packet to be freed. The pointer will be set to NULL.
+ * @note passing NULL is a no-op.
+ */
+void av_packet_free(AVPacket** pkt);
+
+#if FF_API_INIT_PACKET
+/**
+ * Initialize optional fields of a packet with default values.
+ *
+ * Note, this does not touch the data and size members, which have to be
+ * initialized separately.
+ *
+ * @param pkt packet
+ *
+ * @see av_packet_alloc
+ * @see av_packet_unref
+ *
+ * @deprecated This function is deprecated. Once it's removed,
+ sizeof(AVPacket) will not be a part of the ABI anymore.
+ */
+attribute_deprecated void av_init_packet(AVPacket* pkt);
+#endif
+
+/**
+ * Allocate the payload of a packet and initialize its fields with
+ * default values.
+ *
+ * @param pkt packet
+ * @param size wanted payload size
+ * @return 0 if OK, AVERROR_xxx otherwise
+ */
+int av_new_packet(AVPacket* pkt, int size);
+
+/**
+ * Reduce packet size, correctly zeroing padding
+ *
+ * @param pkt packet
+ * @param size new size
+ */
+void av_shrink_packet(AVPacket* pkt, int size);
+
+/**
+ * Increase packet size, correctly zeroing padding
+ *
+ * @param pkt packet
+ * @param grow_by number of bytes by which to increase the size of the packet
+ */
+int av_grow_packet(AVPacket* pkt, int grow_by);
+
+/**
+ * Initialize a reference-counted packet from av_malloc()ed data.
+ *
+ * @param pkt packet to be initialized. This function will set the data, size,
+ * and buf fields, all others are left untouched.
+ * @param data Data allocated by av_malloc() to be used as packet data. If this
+ * function returns successfully, the data is owned by the underlying
+ * AVBuffer. The caller may not access the data through other means.
+ * @param size size of data in bytes, without the padding. I.e. the full buffer
+ * size is assumed to be size + AV_INPUT_BUFFER_PADDING_SIZE.
+ *
+ * @return 0 on success, a negative AVERROR on error
+ */
+int av_packet_from_data(AVPacket* pkt, uint8_t* data, int size);
+
+/**
+ * Allocate new information of a packet.
+ *
+ * @param pkt packet
+ * @param type side information type
+ * @param size side information size
+ * @return pointer to fresh allocated data or NULL otherwise
+ */
+uint8_t* av_packet_new_side_data(AVPacket* pkt, enum AVPacketSideDataType type,
+ size_t size);
+
+/**
+ * Wrap an existing array as a packet side data.
+ *
+ * @param pkt packet
+ * @param type side information type
+ * @param data the side data array. It must be allocated with the av_malloc()
+ * family of functions. The ownership of the data is transferred to
+ * pkt.
+ * @param size side information size
+ * @return a non-negative number on success, a negative AVERROR code on
+ * failure. On failure, the packet is unchanged and the data remains
+ * owned by the caller.
+ */
+int av_packet_add_side_data(AVPacket* pkt, enum AVPacketSideDataType type,
+ uint8_t* data, size_t size);
+
+/**
+ * Shrink the already allocated side data buffer
+ *
+ * @param pkt packet
+ * @param type side information type
+ * @param size new side information size
+ * @return 0 on success, < 0 on failure
+ */
+int av_packet_shrink_side_data(AVPacket* pkt, enum AVPacketSideDataType type,
+ size_t size);
+
+/**
+ * Get side information from packet.
+ *
+ * @param pkt packet
+ * @param type desired side information type
+ * @param size If supplied, *size will be set to the size of the side data
+ * or to zero if the desired side data is not present.
+ * @return pointer to data if present or NULL otherwise
+ */
+uint8_t* av_packet_get_side_data(const AVPacket* pkt,
+ enum AVPacketSideDataType type, size_t* size);
+
+const char* av_packet_side_data_name(enum AVPacketSideDataType type);
+
+/**
+ * Pack a dictionary for use in side_data.
+ *
+ * @param dict The dictionary to pack.
+ * @param size pointer to store the size of the returned data
+ * @return pointer to data if successful, NULL otherwise
+ */
+uint8_t* av_packet_pack_dictionary(AVDictionary* dict, size_t* size);
+/**
+ * Unpack a dictionary from side_data.
+ *
+ * @param data data from side_data
+ * @param size size of the data
+ * @param dict the metadata storage dictionary
+ * @return 0 on success, < 0 on failure
+ */
+int av_packet_unpack_dictionary(const uint8_t* data, size_t size,
+ AVDictionary** dict);
+
+/**
+ * Convenience function to free all the side data stored.
+ * All the other fields stay untouched.
+ *
+ * @param pkt packet
+ */
+void av_packet_free_side_data(AVPacket* pkt);
+
+/**
+ * Setup a new reference to the data described by a given packet
+ *
+ * If src is reference-counted, setup dst as a new reference to the
+ * buffer in src. Otherwise allocate a new buffer in dst and copy the
+ * data from src into it.
+ *
+ * All the other fields are copied from src.
+ *
+ * @see av_packet_unref
+ *
+ * @param dst Destination packet. Will be completely overwritten.
+ * @param src Source packet
+ *
+ * @return 0 on success, a negative AVERROR on error. On error, dst
+ * will be blank (as if returned by av_packet_alloc()).
+ */
+int av_packet_ref(AVPacket* dst, const AVPacket* src);
+
+/**
+ * Wipe the packet.
+ *
+ * Unreference the buffer referenced by the packet and reset the
+ * remaining packet fields to their default values.
+ *
+ * @param pkt The packet to be unreferenced.
+ */
+void av_packet_unref(AVPacket* pkt);
+
+/**
+ * Move every field in src to dst and reset src.
+ *
+ * @see av_packet_unref
+ *
+ * @param src Source packet, will be reset
+ * @param dst Destination packet
+ */
+void av_packet_move_ref(AVPacket* dst, AVPacket* src);
+
+/**
+ * Copy only "properties" fields from src to dst.
+ *
+ * Properties for the purpose of this function are all the fields
+ * beside those related to the packet data (buf, data, size)
+ *
+ * @param dst Destination packet
+ * @param src Source packet
+ *
+ * @return 0 on success AVERROR on failure.
+ */
+int av_packet_copy_props(AVPacket* dst, const AVPacket* src);
+
+/**
+ * Ensure the data described by a given packet is reference counted.
+ *
+ * @note This function does not ensure that the reference will be writable.
+ * Use av_packet_make_writable instead for that purpose.
+ *
+ * @see av_packet_ref
+ * @see av_packet_make_writable
+ *
+ * @param pkt packet whose data should be made reference counted.
+ *
+ * @return 0 on success, a negative AVERROR on error. On failure, the
+ * packet is unchanged.
+ */
+int av_packet_make_refcounted(AVPacket* pkt);
+
+/**
+ * Create a writable reference for the data described by a given packet,
+ * avoiding data copy if possible.
+ *
+ * @param pkt Packet whose data should be made writable.
+ *
+ * @return 0 on success, a negative AVERROR on failure. On failure, the
+ * packet is unchanged.
+ */
+int av_packet_make_writable(AVPacket* pkt);
+
+/**
+ * Convert valid timing fields (timestamps / durations) in a packet from one
+ * timebase to another. Timestamps with unknown values (AV_NOPTS_VALUE) will be
+ * ignored.
+ *
+ * @param pkt packet on which the conversion will be performed
+ * @param tb_src source timebase, in which the timing fields in pkt are
+ * expressed
+ * @param tb_dst destination timebase, to which the timing fields will be
+ * converted
+ */
+void av_packet_rescale_ts(AVPacket* pkt, AVRational tb_src, AVRational tb_dst);
+
+/**
+ * @}
+ */
+
+#endif // AVCODEC_PACKET_H
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/vdpau.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/vdpau.h
new file mode 100644
index 0000000000..0b91baec82
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/vdpau.h
@@ -0,0 +1,156 @@
+/*
+ * The Video Decode and Presentation API for UNIX (VDPAU) is used for
+ * hardware-accelerated decoding of MPEG-1/2, H.264 and VC-1.
+ *
+ * Copyright (C) 2008 NVIDIA
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_VDPAU_H
+#define AVCODEC_VDPAU_H
+
+/**
+ * @file
+ * @ingroup lavc_codec_hwaccel_vdpau
+ * Public libavcodec VDPAU header.
+ */
+
+/**
+ * @defgroup lavc_codec_hwaccel_vdpau VDPAU Decoder and Renderer
+ * @ingroup lavc_codec_hwaccel
+ *
+ * VDPAU hardware acceleration has two modules
+ * - VDPAU decoding
+ * - VDPAU presentation
+ *
+ * The VDPAU decoding module parses all headers using FFmpeg
+ * parsing mechanisms and uses VDPAU for the actual decoding.
+ *
+ * As per the current implementation, the actual decoding
+ * and rendering (API calls) are done as part of the VDPAU
+ * presentation (vo_vdpau.c) module.
+ *
+ * @{
+ */
+
+#include <vdpau/vdpau.h>
+
+#include "libavutil/avconfig.h"
+#include "libavutil/attributes.h"
+
+#include "avcodec.h"
+
+struct AVCodecContext;
+struct AVFrame;
+
+typedef int (*AVVDPAU_Render2)(struct AVCodecContext*, struct AVFrame*,
+ const VdpPictureInfo*, uint32_t,
+ const VdpBitstreamBuffer*);
+
+/**
+ * This structure is used to share data between the libavcodec library and
+ * the client video application.
+ * The user shall allocate the structure via the av_alloc_vdpau_hwaccel
+ * function and make it available as
+ * AVCodecContext.hwaccel_context. Members can be set by the user once
+ * during initialization or through each AVCodecContext.get_buffer()
+ * function call. In any case, they must be valid prior to calling
+ * decoding functions.
+ *
+ * The size of this structure is not a part of the public ABI and must not
+ * be used outside of libavcodec. Use av_vdpau_alloc_context() to allocate an
+ * AVVDPAUContext.
+ */
+typedef struct AVVDPAUContext {
+ /**
+ * VDPAU decoder handle
+ *
+ * Set by user.
+ */
+ VdpDecoder decoder;
+
+ /**
+ * VDPAU decoder render callback
+ *
+ * Set by the user.
+ */
+ VdpDecoderRender* render;
+
+ AVVDPAU_Render2 render2;
+} AVVDPAUContext;
+
+/**
+ * @brief allocation function for AVVDPAUContext
+ *
+ * Allows extending the struct without breaking API/ABI
+ */
+AVVDPAUContext* av_alloc_vdpaucontext(void);
+
+AVVDPAU_Render2 av_vdpau_hwaccel_get_render2(const AVVDPAUContext*);
+void av_vdpau_hwaccel_set_render2(AVVDPAUContext*, AVVDPAU_Render2);
+
+/**
+ * Associate a VDPAU device with a codec context for hardware acceleration.
+ * This function is meant to be called from the get_format() codec callback,
+ * or earlier. It can also be called after avcodec_flush_buffers() to change
+ * the underlying VDPAU device mid-stream (e.g. to recover from non-transparent
+ * display preemption).
+ *
+ * @note get_format() must return AV_PIX_FMT_VDPAU if this function completes
+ * successfully.
+ *
+ * @param avctx decoding context whose get_format() callback is invoked
+ * @param device VDPAU device handle to use for hardware acceleration
+ * @param get_proc_address VDPAU device driver
+ * @param flags zero of more OR'd AV_HWACCEL_FLAG_* flags
+ *
+ * @return 0 on success, an AVERROR code on failure.
+ */
+int av_vdpau_bind_context(AVCodecContext* avctx, VdpDevice device,
+ VdpGetProcAddress* get_proc_address, unsigned flags);
+
+/**
+ * Gets the parameters to create an adequate VDPAU video surface for the codec
+ * context using VDPAU hardware decoding acceleration.
+ *
+ * @note Behavior is undefined if the context was not successfully bound to a
+ * VDPAU device using av_vdpau_bind_context().
+ *
+ * @param avctx the codec context being used for decoding the stream
+ * @param type storage space for the VDPAU video surface chroma type
+ * (or NULL to ignore)
+ * @param width storage space for the VDPAU video surface pixel width
+ * (or NULL to ignore)
+ * @param height storage space for the VDPAU video surface pixel height
+ * (or NULL to ignore)
+ *
+ * @return 0 on success, a negative AVERROR code on failure.
+ */
+int av_vdpau_get_surface_parameters(AVCodecContext* avctx, VdpChromaType* type,
+ uint32_t* width, uint32_t* height);
+
+/**
+ * Allocate an AVVDPAUContext.
+ *
+ * @return Newly-allocated AVVDPAUContext or NULL on failure.
+ */
+AVVDPAUContext* av_vdpau_alloc_context(void);
+
+/** @} */
+
+#endif /* AVCODEC_VDPAU_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/version.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/version.h
new file mode 100644
index 0000000000..d3f6020c86
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/version.h
@@ -0,0 +1,45 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_VERSION_H
+#define AVCODEC_VERSION_H
+
+/**
+ * @file
+ * @ingroup libavc
+ * Libavcodec version macros.
+ */
+
+#include "libavutil/version.h"
+
+#include "version_major.h"
+
+#define LIBAVCODEC_VERSION_MINOR 5
+#define LIBAVCODEC_VERSION_MICRO 100
+
+#define LIBAVCODEC_VERSION_INT \
+ AV_VERSION_INT(LIBAVCODEC_VERSION_MAJOR, LIBAVCODEC_VERSION_MINOR, \
+ LIBAVCODEC_VERSION_MICRO)
+#define LIBAVCODEC_VERSION \
+ AV_VERSION(LIBAVCODEC_VERSION_MAJOR, LIBAVCODEC_VERSION_MINOR, \
+ LIBAVCODEC_VERSION_MICRO)
+#define LIBAVCODEC_BUILD LIBAVCODEC_VERSION_INT
+
+#define LIBAVCODEC_IDENT "Lavc" AV_STRINGIFY(LIBAVCODEC_VERSION)
+
+#endif /* AVCODEC_VERSION_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/version_major.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/version_major.h
new file mode 100644
index 0000000000..e9cd92cb3a
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavcodec/version_major.h
@@ -0,0 +1,52 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_VERSION_MAJOR_H
+#define AVCODEC_VERSION_MAJOR_H
+
+/**
+ * @file
+ * @ingroup libavc
+ * Libavcodec version macros.
+ */
+
+#define LIBAVCODEC_VERSION_MAJOR 60
+
+/**
+ * FF_API_* defines may be placed below to indicate public API that will be
+ * dropped at a future version bump. The defines themselves are not part of
+ * the public API and may change, break or disappear at any time.
+ *
+ * @note, when bumping the major version it is recommended to manually
+ * disable each FF_API_* in its own commit instead of disabling them all
+ * at once through the bump. This improves the git bisect-ability of the change.
+ */
+
+#define FF_API_INIT_PACKET (LIBAVCODEC_VERSION_MAJOR < 61)
+#define FF_API_IDCT_NONE (LIBAVCODEC_VERSION_MAJOR < 61)
+#define FF_API_SVTAV1_OPTS (LIBAVCODEC_VERSION_MAJOR < 61)
+#define FF_API_AYUV_CODECID (LIBAVCODEC_VERSION_MAJOR < 61)
+#define FF_API_VT_OUTPUT_CALLBACK (LIBAVCODEC_VERSION_MAJOR < 61)
+#define FF_API_AVCODEC_CHROMA_POS (LIBAVCODEC_VERSION_MAJOR < 61)
+#define FF_API_VT_HWACCEL_CONTEXT (LIBAVCODEC_VERSION_MAJOR < 61)
+#define FF_API_AVCTX_FRAME_NUMBER (LIBAVCODEC_VERSION_MAJOR < 61)
+
+// reminder to remove CrystalHD decoders on next major bump
+#define FF_CODEC_CRYSTAL_HD (LIBAVCODEC_VERSION_MAJOR < 61)
+
+#endif /* AVCODEC_VERSION_MAJOR_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/attributes.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/attributes.h
new file mode 100644
index 0000000000..774d1fe916
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/attributes.h
@@ -0,0 +1,173 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * Macro definitions for various function/variable attributes
+ */
+
+#ifndef AVUTIL_ATTRIBUTES_H
+#define AVUTIL_ATTRIBUTES_H
+
+#ifdef __GNUC__
+# define AV_GCC_VERSION_AT_LEAST(x, y) \
+ (__GNUC__ > (x) || __GNUC__ == (x) && __GNUC_MINOR__ >= (y))
+# define AV_GCC_VERSION_AT_MOST(x, y) \
+ (__GNUC__ < (x) || __GNUC__ == (x) && __GNUC_MINOR__ <= (y))
+#else
+# define AV_GCC_VERSION_AT_LEAST(x, y) 0
+# define AV_GCC_VERSION_AT_MOST(x, y) 0
+#endif
+
+#ifdef __has_builtin
+# define AV_HAS_BUILTIN(x) __has_builtin(x)
+#else
+# define AV_HAS_BUILTIN(x) 0
+#endif
+
+#ifndef av_always_inline
+# if AV_GCC_VERSION_AT_LEAST(3, 1)
+# define av_always_inline __attribute__((always_inline)) inline
+# elif defined(_MSC_VER)
+# define av_always_inline __forceinline
+# else
+# define av_always_inline inline
+# endif
+#endif
+
+#ifndef av_extern_inline
+# if defined(__ICL) && __ICL >= 1210 || defined(__GNUC_STDC_INLINE__)
+# define av_extern_inline extern inline
+# else
+# define av_extern_inline inline
+# endif
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3, 4)
+# define av_warn_unused_result __attribute__((warn_unused_result))
+#else
+# define av_warn_unused_result
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3, 1)
+# define av_noinline __attribute__((noinline))
+#elif defined(_MSC_VER)
+# define av_noinline __declspec(noinline)
+#else
+# define av_noinline
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3, 1) || defined(__clang__)
+# define av_pure __attribute__((pure))
+#else
+# define av_pure
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(2, 6) || defined(__clang__)
+# define av_const __attribute__((const))
+#else
+# define av_const
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(4, 3) || defined(__clang__)
+# define av_cold __attribute__((cold))
+#else
+# define av_cold
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(4, 1) && !defined(__llvm__)
+# define av_flatten __attribute__((flatten))
+#else
+# define av_flatten
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3, 1)
+# define attribute_deprecated __attribute__((deprecated))
+#elif defined(_MSC_VER)
+# define attribute_deprecated __declspec(deprecated)
+#else
+# define attribute_deprecated
+#endif
+
+/**
+ * Disable warnings about deprecated features
+ * This is useful for sections of code kept for backward compatibility and
+ * scheduled for removal.
+ */
+#ifndef AV_NOWARN_DEPRECATED
+# if AV_GCC_VERSION_AT_LEAST(4, 6) || defined(__clang__)
+# define AV_NOWARN_DEPRECATED(code) \
+ _Pragma("GCC diagnostic push") \
+ _Pragma("GCC diagnostic ignored \"-Wdeprecated-declarations\"") \
+ code _Pragma("GCC diagnostic pop")
+# elif defined(_MSC_VER)
+# define AV_NOWARN_DEPRECATED(code) \
+ __pragma(warning(push)) __pragma(warning(disable : 4996)) code; \
+ __pragma(warning(pop))
+# else
+# define AV_NOWARN_DEPRECATED(code) code
+# endif
+#endif
+
+#if defined(__GNUC__) || defined(__clang__)
+# define av_unused __attribute__((unused))
+#else
+# define av_unused
+#endif
+
+/**
+ * Mark a variable as used and prevent the compiler from optimizing it
+ * away. This is useful for variables accessed only from inline
+ * assembler without the compiler being aware.
+ */
+#if AV_GCC_VERSION_AT_LEAST(3, 1) || defined(__clang__)
+# define av_used __attribute__((used))
+#else
+# define av_used
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3, 3) || defined(__clang__)
+# define av_alias __attribute__((may_alias))
+#else
+# define av_alias
+#endif
+
+#if (defined(__GNUC__) || defined(__clang__)) && !defined(__INTEL_COMPILER)
+# define av_uninit(x) x = x
+#else
+# define av_uninit(x) x
+#endif
+
+#if defined(__GNUC__) || defined(__clang__)
+# define av_builtin_constant_p __builtin_constant_p
+# define av_printf_format(fmtpos, attrpos) \
+ __attribute__((__format__(__printf__, fmtpos, attrpos)))
+#else
+# define av_builtin_constant_p(x) 0
+# define av_printf_format(fmtpos, attrpos)
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(2, 5) || defined(__clang__)
+# define av_noreturn __attribute__((noreturn))
+#else
+# define av_noreturn
+#endif
+
+#endif /* AVUTIL_ATTRIBUTES_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/avconfig.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/avconfig.h
new file mode 100644
index 0000000000..c289fbb551
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/avconfig.h
@@ -0,0 +1,6 @@
+/* Generated by ffmpeg configure */
+#ifndef AVUTIL_AVCONFIG_H
+#define AVUTIL_AVCONFIG_H
+#define AV_HAVE_BIGENDIAN 0
+#define AV_HAVE_FAST_UNALIGNED 1
+#endif /* AVUTIL_AVCONFIG_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/avutil.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/avutil.h
new file mode 100644
index 0000000000..8f8343bd04
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/avutil.h
@@ -0,0 +1,371 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_AVUTIL_H
+#define AVUTIL_AVUTIL_H
+
+/**
+ * @file
+ * @ingroup lavu
+ * Convenience header that includes @ref lavu "libavutil"'s core.
+ */
+
+/**
+ * @mainpage
+ *
+ * @section ffmpeg_intro Introduction
+ *
+ * This document describes the usage of the different libraries
+ * provided by FFmpeg.
+ *
+ * @li @ref libavc "libavcodec" encoding/decoding library
+ * @li @ref lavfi "libavfilter" graph-based frame editing library
+ * @li @ref libavf "libavformat" I/O and muxing/demuxing library
+ * @li @ref lavd "libavdevice" special devices muxing/demuxing library
+ * @li @ref lavu "libavutil" common utility library
+ * @li @ref lswr "libswresample" audio resampling, format conversion and mixing
+ * @li @ref lpp "libpostproc" post processing library
+ * @li @ref libsws "libswscale" color conversion and scaling library
+ *
+ * @section ffmpeg_versioning Versioning and compatibility
+ *
+ * Each of the FFmpeg libraries contains a version.h header, which defines a
+ * major, minor and micro version number with the
+ * <em>LIBRARYNAME_VERSION_{MAJOR,MINOR,MICRO}</em> macros. The major version
+ * number is incremented with backward incompatible changes - e.g. removing
+ * parts of the public API, reordering public struct members, etc. The minor
+ * version number is incremented for backward compatible API changes or major
+ * new features - e.g. adding a new public function or a new decoder. The micro
+ * version number is incremented for smaller changes that a calling program
+ * might still want to check for - e.g. changing behavior in a previously
+ * unspecified situation.
+ *
+ * FFmpeg guarantees backward API and ABI compatibility for each library as long
+ * as its major version number is unchanged. This means that no public symbols
+ * will be removed or renamed. Types and names of the public struct members and
+ * values of public macros and enums will remain the same (unless they were
+ * explicitly declared as not part of the public API). Documented behavior will
+ * not change.
+ *
+ * In other words, any correct program that works with a given FFmpeg snapshot
+ * should work just as well without any changes with any later snapshot with the
+ * same major versions. This applies to both rebuilding the program against new
+ * FFmpeg versions or to replacing the dynamic FFmpeg libraries that a program
+ * links against.
+ *
+ * However, new public symbols may be added and new members may be appended to
+ * public structs whose size is not part of public ABI (most public structs in
+ * FFmpeg). New macros and enum values may be added. Behavior in undocumented
+ * situations may change slightly (and be documented). All those are accompanied
+ * by an entry in doc/APIchanges and incrementing either the minor or micro
+ * version number.
+ */
+
+/**
+ * @defgroup lavu libavutil
+ * Common code shared across all FFmpeg libraries.
+ *
+ * @note
+ * libavutil is designed to be modular. In most cases, in order to use the
+ * functions provided by one component of libavutil you must explicitly include
+ * the specific header containing that feature. If you are only using
+ * media-related components, you could simply include libavutil/avutil.h, which
+ * brings in most of the "core" components.
+ *
+ * @{
+ *
+ * @defgroup lavu_crypto Crypto and Hashing
+ *
+ * @{
+ * @}
+ *
+ * @defgroup lavu_math Mathematics
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_string String Manipulation
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_mem Memory Management
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_data Data Structures
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_video Video related
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_audio Audio related
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_error Error Codes
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_log Logging Facility
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_misc Other
+ *
+ * @{
+ *
+ * @defgroup preproc_misc Preprocessor String Macros
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup version_utils Library Version Macros
+ *
+ * @{
+ *
+ * @}
+ */
+
+/**
+ * @addtogroup lavu_ver
+ * @{
+ */
+
+/**
+ * Return the LIBAVUTIL_VERSION_INT constant.
+ */
+unsigned avutil_version(void);
+
+/**
+ * Return an informative version string. This usually is the actual release
+ * version number or a git commit description. This string has no fixed format
+ * and can change any time. It should never be parsed by code.
+ */
+const char* av_version_info(void);
+
+/**
+ * Return the libavutil build-time configuration.
+ */
+const char* avutil_configuration(void);
+
+/**
+ * Return the libavutil license.
+ */
+const char* avutil_license(void);
+
+/**
+ * @}
+ */
+
+/**
+ * @addtogroup lavu_media Media Type
+ * @brief Media Type
+ */
+
+enum AVMediaType {
+ AVMEDIA_TYPE_UNKNOWN = -1, ///< Usually treated as AVMEDIA_TYPE_DATA
+ AVMEDIA_TYPE_VIDEO,
+ AVMEDIA_TYPE_AUDIO,
+ AVMEDIA_TYPE_DATA, ///< Opaque data information usually continuous
+ AVMEDIA_TYPE_SUBTITLE,
+ AVMEDIA_TYPE_ATTACHMENT, ///< Opaque data information usually sparse
+ AVMEDIA_TYPE_NB
+};
+
+/**
+ * Return a string describing the media_type enum, NULL if media_type
+ * is unknown.
+ */
+const char* av_get_media_type_string(enum AVMediaType media_type);
+
+/**
+ * @defgroup lavu_const Constants
+ * @{
+ *
+ * @defgroup lavu_enc Encoding specific
+ *
+ * @note those definition should move to avcodec
+ * @{
+ */
+
+#define FF_LAMBDA_SHIFT 7
+#define FF_LAMBDA_SCALE (1 << FF_LAMBDA_SHIFT)
+#define FF_QP2LAMBDA 118 ///< factor to convert from H.263 QP to lambda
+#define FF_LAMBDA_MAX (256 * 128 - 1)
+
+#define FF_QUALITY_SCALE FF_LAMBDA_SCALE // FIXME maybe remove
+
+/**
+ * @}
+ * @defgroup lavu_time Timestamp specific
+ *
+ * FFmpeg internal timebase and timestamp definitions
+ *
+ * @{
+ */
+
+/**
+ * @brief Undefined timestamp value
+ *
+ * Usually reported by demuxer that work on containers that do not provide
+ * either pts or dts.
+ */
+
+#define AV_NOPTS_VALUE ((int64_t)UINT64_C(0x8000000000000000))
+
+/**
+ * Internal time base represented as integer
+ */
+
+#define AV_TIME_BASE 1000000
+
+/**
+ * Internal time base represented as fractional value
+ */
+
+#define AV_TIME_BASE_Q \
+ (AVRational) { 1, AV_TIME_BASE }
+
+/**
+ * @}
+ * @}
+ * @defgroup lavu_picture Image related
+ *
+ * AVPicture types, pixel formats and basic image planes manipulation.
+ *
+ * @{
+ */
+
+enum AVPictureType {
+ AV_PICTURE_TYPE_NONE = 0, ///< Undefined
+ AV_PICTURE_TYPE_I, ///< Intra
+ AV_PICTURE_TYPE_P, ///< Predicted
+ AV_PICTURE_TYPE_B, ///< Bi-dir predicted
+ AV_PICTURE_TYPE_S, ///< S(GMC)-VOP MPEG-4
+ AV_PICTURE_TYPE_SI, ///< Switching Intra
+ AV_PICTURE_TYPE_SP, ///< Switching Predicted
+ AV_PICTURE_TYPE_BI, ///< BI type
+};
+
+/**
+ * Return a single letter to describe the given picture type
+ * pict_type.
+ *
+ * @param[in] pict_type the picture type @return a single character
+ * representing the picture type, '?' if pict_type is unknown
+ */
+char av_get_picture_type_char(enum AVPictureType pict_type);
+
+/**
+ * @}
+ */
+
+#include "common.h"
+#include "error.h"
+#include "rational.h"
+#include "version.h"
+#include "macros.h"
+#include "mathematics.h"
+#include "log.h"
+#include "pixfmt.h"
+
+/**
+ * Return x default pointer in case p is NULL.
+ */
+static inline void* av_x_if_null(const void* p, const void* x) {
+ return (void*)(intptr_t)(p ? p : x);
+}
+
+/**
+ * Compute the length of an integer list.
+ *
+ * @param elsize size in bytes of each list element (only 1, 2, 4 or 8)
+ * @param term list terminator (usually 0 or -1)
+ * @param list pointer to the list
+ * @return length of the list, in elements, not counting the terminator
+ */
+unsigned av_int_list_length_for_size(unsigned elsize, const void* list,
+ uint64_t term) av_pure;
+
+/**
+ * Compute the length of an integer list.
+ *
+ * @param term list terminator (usually 0 or -1)
+ * @param list pointer to the list
+ * @return length of the list, in elements, not counting the terminator
+ */
+#define av_int_list_length(list, term) \
+ av_int_list_length_for_size(sizeof(*(list)), list, term)
+
+#if FF_API_AV_FOPEN_UTF8
+/**
+ * Open a file using a UTF-8 filename.
+ * The API of this function matches POSIX fopen(), errors are returned through
+ * errno.
+ * @deprecated Avoid using it, as on Windows, the FILE* allocated by this
+ * function may be allocated with a different CRT than the caller
+ * who uses the FILE*. No replacement provided in public API.
+ */
+attribute_deprecated FILE* av_fopen_utf8(const char* path, const char* mode);
+#endif
+
+/**
+ * Return the fractional representation of the internal time base.
+ */
+AVRational av_get_time_base_q(void);
+
+#define AV_FOURCC_MAX_STRING_SIZE 32
+
+#define av_fourcc2str(fourcc) \
+ av_fourcc_make_string((char[AV_FOURCC_MAX_STRING_SIZE]){0}, fourcc)
+
+/**
+ * Fill the provided buffer with a string containing a FourCC (four-character
+ * code) representation.
+ *
+ * @param buf a buffer with size in bytes of at least
+ * AV_FOURCC_MAX_STRING_SIZE
+ * @param fourcc the fourcc to represent
+ * @return the buffer in input
+ */
+char* av_fourcc_make_string(char* buf, uint32_t fourcc);
+
+/**
+ * @}
+ * @}
+ */
+
+#endif /* AVUTIL_AVUTIL_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/buffer.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/buffer.h
new file mode 100644
index 0000000000..372de093f9
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/buffer.h
@@ -0,0 +1,324 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * @ingroup lavu_buffer
+ * refcounted data buffer API
+ */
+
+#ifndef AVUTIL_BUFFER_H
+#define AVUTIL_BUFFER_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+/**
+ * @defgroup lavu_buffer AVBuffer
+ * @ingroup lavu_data
+ *
+ * @{
+ * AVBuffer is an API for reference-counted data buffers.
+ *
+ * There are two core objects in this API -- AVBuffer and AVBufferRef. AVBuffer
+ * represents the data buffer itself; it is opaque and not meant to be accessed
+ * by the caller directly, but only through AVBufferRef. However, the caller may
+ * e.g. compare two AVBuffer pointers to check whether two different references
+ * are describing the same data buffer. AVBufferRef represents a single
+ * reference to an AVBuffer and it is the object that may be manipulated by the
+ * caller directly.
+ *
+ * There are two functions provided for creating a new AVBuffer with a single
+ * reference -- av_buffer_alloc() to just allocate a new buffer, and
+ * av_buffer_create() to wrap an existing array in an AVBuffer. From an existing
+ * reference, additional references may be created with av_buffer_ref().
+ * Use av_buffer_unref() to free a reference (this will automatically free the
+ * data once all the references are freed).
+ *
+ * The convention throughout this API and the rest of FFmpeg is such that the
+ * buffer is considered writable if there exists only one reference to it (and
+ * it has not been marked as read-only). The av_buffer_is_writable() function is
+ * provided to check whether this is true and av_buffer_make_writable() will
+ * automatically create a new writable buffer when necessary.
+ * Of course nothing prevents the calling code from violating this convention,
+ * however that is safe only when all the existing references are under its
+ * control.
+ *
+ * @note Referencing and unreferencing the buffers is thread-safe and thus
+ * may be done from multiple threads simultaneously without any need for
+ * additional locking.
+ *
+ * @note Two different references to the same buffer can point to different
+ * parts of the buffer (i.e. their AVBufferRef.data will not be equal).
+ */
+
+/**
+ * A reference counted buffer type. It is opaque and is meant to be used through
+ * references (AVBufferRef).
+ */
+typedef struct AVBuffer AVBuffer;
+
+/**
+ * A reference to a data buffer.
+ *
+ * The size of this struct is not a part of the public ABI and it is not meant
+ * to be allocated directly.
+ */
+typedef struct AVBufferRef {
+ AVBuffer* buffer;
+
+ /**
+ * The data buffer. It is considered writable if and only if
+ * this is the only reference to the buffer, in which case
+ * av_buffer_is_writable() returns 1.
+ */
+ uint8_t* data;
+ /**
+ * Size of data in bytes.
+ */
+ size_t size;
+} AVBufferRef;
+
+/**
+ * Allocate an AVBuffer of the given size using av_malloc().
+ *
+ * @return an AVBufferRef of given size or NULL when out of memory
+ */
+AVBufferRef* av_buffer_alloc(size_t size);
+
+/**
+ * Same as av_buffer_alloc(), except the returned buffer will be initialized
+ * to zero.
+ */
+AVBufferRef* av_buffer_allocz(size_t size);
+
+/**
+ * Always treat the buffer as read-only, even when it has only one
+ * reference.
+ */
+#define AV_BUFFER_FLAG_READONLY (1 << 0)
+
+/**
+ * Create an AVBuffer from an existing array.
+ *
+ * If this function is successful, data is owned by the AVBuffer. The caller may
+ * only access data through the returned AVBufferRef and references derived from
+ * it.
+ * If this function fails, data is left untouched.
+ * @param data data array
+ * @param size size of data in bytes
+ * @param free a callback for freeing this buffer's data
+ * @param opaque parameter to be got for processing or passed to free
+ * @param flags a combination of AV_BUFFER_FLAG_*
+ *
+ * @return an AVBufferRef referring to data on success, NULL on failure.
+ */
+AVBufferRef* av_buffer_create(uint8_t* data, size_t size,
+ void (*free)(void* opaque, uint8_t* data),
+ void* opaque, int flags);
+
+/**
+ * Default free callback, which calls av_free() on the buffer data.
+ * This function is meant to be passed to av_buffer_create(), not called
+ * directly.
+ */
+void av_buffer_default_free(void* opaque, uint8_t* data);
+
+/**
+ * Create a new reference to an AVBuffer.
+ *
+ * @return a new AVBufferRef referring to the same AVBuffer as buf or NULL on
+ * failure.
+ */
+AVBufferRef* av_buffer_ref(const AVBufferRef* buf);
+
+/**
+ * Free a given reference and automatically free the buffer if there are no more
+ * references to it.
+ *
+ * @param buf the reference to be freed. The pointer is set to NULL on return.
+ */
+void av_buffer_unref(AVBufferRef** buf);
+
+/**
+ * @return 1 if the caller may write to the data referred to by buf (which is
+ * true if and only if buf is the only reference to the underlying AVBuffer).
+ * Return 0 otherwise.
+ * A positive answer is valid until av_buffer_ref() is called on buf.
+ */
+int av_buffer_is_writable(const AVBufferRef* buf);
+
+/**
+ * @return the opaque parameter set by av_buffer_create.
+ */
+void* av_buffer_get_opaque(const AVBufferRef* buf);
+
+int av_buffer_get_ref_count(const AVBufferRef* buf);
+
+/**
+ * Create a writable reference from a given buffer reference, avoiding data copy
+ * if possible.
+ *
+ * @param buf buffer reference to make writable. On success, buf is either left
+ * untouched, or it is unreferenced and a new writable AVBufferRef is
+ * written in its place. On failure, buf is left untouched.
+ * @return 0 on success, a negative AVERROR on failure.
+ */
+int av_buffer_make_writable(AVBufferRef** buf);
+
+/**
+ * Reallocate a given buffer.
+ *
+ * @param buf a buffer reference to reallocate. On success, buf will be
+ * unreferenced and a new reference with the required size will be
+ * written in its place. On failure buf will be left untouched. *buf
+ * may be NULL, then a new buffer is allocated.
+ * @param size required new buffer size.
+ * @return 0 on success, a negative AVERROR on failure.
+ *
+ * @note the buffer is actually reallocated with av_realloc() only if it was
+ * initially allocated through av_buffer_realloc(NULL) and there is only one
+ * reference to it (i.e. the one passed to this function). In all other cases
+ * a new buffer is allocated and the data is copied.
+ */
+int av_buffer_realloc(AVBufferRef** buf, size_t size);
+
+/**
+ * Ensure dst refers to the same data as src.
+ *
+ * When *dst is already equivalent to src, do nothing. Otherwise unreference dst
+ * and replace it with a new reference to src.
+ *
+ * @param dst Pointer to either a valid buffer reference or NULL. On success,
+ * this will point to a buffer reference equivalent to src. On
+ * failure, dst will be left untouched.
+ * @param src A buffer reference to replace dst with. May be NULL, then this
+ * function is equivalent to av_buffer_unref(dst).
+ * @return 0 on success
+ * AVERROR(ENOMEM) on memory allocation failure.
+ */
+int av_buffer_replace(AVBufferRef** dst, const AVBufferRef* src);
+
+/**
+ * @}
+ */
+
+/**
+ * @defgroup lavu_bufferpool AVBufferPool
+ * @ingroup lavu_data
+ *
+ * @{
+ * AVBufferPool is an API for a lock-free thread-safe pool of AVBuffers.
+ *
+ * Frequently allocating and freeing large buffers may be slow. AVBufferPool is
+ * meant to solve this in cases when the caller needs a set of buffers of the
+ * same size (the most obvious use case being buffers for raw video or audio
+ * frames).
+ *
+ * At the beginning, the user must call av_buffer_pool_init() to create the
+ * buffer pool. Then whenever a buffer is needed, call av_buffer_pool_get() to
+ * get a reference to a new buffer, similar to av_buffer_alloc(). This new
+ * reference works in all aspects the same way as the one created by
+ * av_buffer_alloc(). However, when the last reference to this buffer is
+ * unreferenced, it is returned to the pool instead of being freed and will be
+ * reused for subsequent av_buffer_pool_get() calls.
+ *
+ * When the caller is done with the pool and no longer needs to allocate any new
+ * buffers, av_buffer_pool_uninit() must be called to mark the pool as freeable.
+ * Once all the buffers are released, it will automatically be freed.
+ *
+ * Allocating and releasing buffers with this API is thread-safe as long as
+ * either the default alloc callback is used, or the user-supplied one is
+ * thread-safe.
+ */
+
+/**
+ * The buffer pool. This structure is opaque and not meant to be accessed
+ * directly. It is allocated with av_buffer_pool_init() and freed with
+ * av_buffer_pool_uninit().
+ */
+typedef struct AVBufferPool AVBufferPool;
+
+/**
+ * Allocate and initialize a buffer pool.
+ *
+ * @param size size of each buffer in this pool
+ * @param alloc a function that will be used to allocate new buffers when the
+ * pool is empty. May be NULL, then the default allocator will be used
+ * (av_buffer_alloc()).
+ * @return newly created buffer pool on success, NULL on error.
+ */
+AVBufferPool* av_buffer_pool_init(size_t size,
+ AVBufferRef* (*alloc)(size_t size));
+
+/**
+ * Allocate and initialize a buffer pool with a more complex allocator.
+ *
+ * @param size size of each buffer in this pool
+ * @param opaque arbitrary user data used by the allocator
+ * @param alloc a function that will be used to allocate new buffers when the
+ * pool is empty. May be NULL, then the default allocator will be
+ * used (av_buffer_alloc()).
+ * @param pool_free a function that will be called immediately before the pool
+ * is freed. I.e. after av_buffer_pool_uninit() is called
+ * by the caller and all the frames are returned to the pool
+ * and freed. It is intended to uninitialize the user opaque
+ * data. May be NULL.
+ * @return newly created buffer pool on success, NULL on error.
+ */
+AVBufferPool* av_buffer_pool_init2(size_t size, void* opaque,
+ AVBufferRef* (*alloc)(void* opaque,
+ size_t size),
+ void (*pool_free)(void* opaque));
+
+/**
+ * Mark the pool as being available for freeing. It will actually be freed only
+ * once all the allocated buffers associated with the pool are released. Thus it
+ * is safe to call this function while some of the allocated buffers are still
+ * in use.
+ *
+ * @param pool pointer to the pool to be freed. It will be set to NULL.
+ */
+void av_buffer_pool_uninit(AVBufferPool** pool);
+
+/**
+ * Allocate a new AVBuffer, reusing an old buffer from the pool when available.
+ * This function may be called simultaneously from multiple threads.
+ *
+ * @return a reference to the new buffer on success, NULL on error.
+ */
+AVBufferRef* av_buffer_pool_get(AVBufferPool* pool);
+
+/**
+ * Query the original opaque parameter of an allocated buffer in the pool.
+ *
+ * @param ref a buffer reference to a buffer returned by av_buffer_pool_get.
+ * @return the opaque parameter set by the buffer allocator function of the
+ * buffer pool.
+ *
+ * @note the opaque parameter of ref is used by the buffer pool implementation,
+ * therefore you have to use this function to access the original opaque
+ * parameter of an allocated buffer.
+ */
+void* av_buffer_pool_buffer_get_opaque(const AVBufferRef* ref);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_BUFFER_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/channel_layout.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/channel_layout.h
new file mode 100644
index 0000000000..1ce5bb32d1
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/channel_layout.h
@@ -0,0 +1,842 @@
+/*
+ * Copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ * Copyright (c) 2008 Peter Ross
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_CHANNEL_LAYOUT_H
+#define AVUTIL_CHANNEL_LAYOUT_H
+
+#include <stdint.h>
+#include <stdlib.h>
+
+#include "version.h"
+#include "attributes.h"
+
+/**
+ * @file
+ * @ingroup lavu_audio_channels
+ * Public libavutil channel layout APIs header.
+ */
+
+/**
+ * @defgroup lavu_audio_channels Audio channels
+ * @ingroup lavu_audio
+ *
+ * Audio channel layout utility functions
+ *
+ * @{
+ */
+
+enum AVChannel {
+ ///< Invalid channel index
+ AV_CHAN_NONE = -1,
+ AV_CHAN_FRONT_LEFT,
+ AV_CHAN_FRONT_RIGHT,
+ AV_CHAN_FRONT_CENTER,
+ AV_CHAN_LOW_FREQUENCY,
+ AV_CHAN_BACK_LEFT,
+ AV_CHAN_BACK_RIGHT,
+ AV_CHAN_FRONT_LEFT_OF_CENTER,
+ AV_CHAN_FRONT_RIGHT_OF_CENTER,
+ AV_CHAN_BACK_CENTER,
+ AV_CHAN_SIDE_LEFT,
+ AV_CHAN_SIDE_RIGHT,
+ AV_CHAN_TOP_CENTER,
+ AV_CHAN_TOP_FRONT_LEFT,
+ AV_CHAN_TOP_FRONT_CENTER,
+ AV_CHAN_TOP_FRONT_RIGHT,
+ AV_CHAN_TOP_BACK_LEFT,
+ AV_CHAN_TOP_BACK_CENTER,
+ AV_CHAN_TOP_BACK_RIGHT,
+ /** Stereo downmix. */
+ AV_CHAN_STEREO_LEFT = 29,
+ /** See above. */
+ AV_CHAN_STEREO_RIGHT,
+ AV_CHAN_WIDE_LEFT,
+ AV_CHAN_WIDE_RIGHT,
+ AV_CHAN_SURROUND_DIRECT_LEFT,
+ AV_CHAN_SURROUND_DIRECT_RIGHT,
+ AV_CHAN_LOW_FREQUENCY_2,
+ AV_CHAN_TOP_SIDE_LEFT,
+ AV_CHAN_TOP_SIDE_RIGHT,
+ AV_CHAN_BOTTOM_FRONT_CENTER,
+ AV_CHAN_BOTTOM_FRONT_LEFT,
+ AV_CHAN_BOTTOM_FRONT_RIGHT,
+
+ /** Channel is empty can be safely skipped. */
+ AV_CHAN_UNUSED = 0x200,
+
+ /** Channel contains data, but its position is unknown. */
+ AV_CHAN_UNKNOWN = 0x300,
+
+ /**
+ * Range of channels between AV_CHAN_AMBISONIC_BASE and
+ * AV_CHAN_AMBISONIC_END represent Ambisonic components using the ACN system.
+ *
+ * Given a channel id `<i>` between AV_CHAN_AMBISONIC_BASE and
+ * AV_CHAN_AMBISONIC_END (inclusive), the ACN index of the channel `<n>` is
+ * `<n> = <i> - AV_CHAN_AMBISONIC_BASE`.
+ *
+ * @note these values are only used for AV_CHANNEL_ORDER_CUSTOM channel
+ * orderings, the AV_CHANNEL_ORDER_AMBISONIC ordering orders the channels
+ * implicitly by their position in the stream.
+ */
+ AV_CHAN_AMBISONIC_BASE = 0x400,
+ // leave space for 1024 ids, which correspond to maximum order-32 harmonics,
+ // which should be enough for the foreseeable use cases
+ AV_CHAN_AMBISONIC_END = 0x7ff,
+};
+
+enum AVChannelOrder {
+ /**
+ * Only the channel count is specified, without any further information
+ * about the channel order.
+ */
+ AV_CHANNEL_ORDER_UNSPEC,
+ /**
+ * The native channel order, i.e. the channels are in the same order in
+ * which they are defined in the AVChannel enum. This supports up to 63
+ * different channels.
+ */
+ AV_CHANNEL_ORDER_NATIVE,
+ /**
+ * The channel order does not correspond to any other predefined order and
+ * is stored as an explicit map. For example, this could be used to support
+ * layouts with 64 or more channels, or with empty/skipped (AV_CHAN_SILENCE)
+ * channels at arbitrary positions.
+ */
+ AV_CHANNEL_ORDER_CUSTOM,
+ /**
+ * The audio is represented as the decomposition of the sound field into
+ * spherical harmonics. Each channel corresponds to a single expansion
+ * component. Channels are ordered according to ACN (Ambisonic Channel
+ * Number).
+ *
+ * The channel with the index n in the stream contains the spherical
+ * harmonic of degree l and order m given by
+ * @code{.unparsed}
+ * l = floor(sqrt(n)),
+ * m = n - l * (l + 1).
+ * @endcode
+ *
+ * Conversely given a spherical harmonic of degree l and order m, the
+ * corresponding channel index n is given by
+ * @code{.unparsed}
+ * n = l * (l + 1) + m.
+ * @endcode
+ *
+ * Normalization is assumed to be SN3D (Schmidt Semi-Normalization)
+ * as defined in AmbiX format $ 2.1.
+ */
+ AV_CHANNEL_ORDER_AMBISONIC,
+};
+
+/**
+ * @defgroup channel_masks Audio channel masks
+ *
+ * A channel layout is a 64-bits integer with a bit set for every channel.
+ * The number of bits set must be equal to the number of channels.
+ * The value 0 means that the channel layout is not known.
+ * @note this data structure is not powerful enough to handle channels
+ * combinations that have the same channel multiple times, such as
+ * dual-mono.
+ *
+ * @{
+ */
+#define AV_CH_FRONT_LEFT (1ULL << AV_CHAN_FRONT_LEFT)
+#define AV_CH_FRONT_RIGHT (1ULL << AV_CHAN_FRONT_RIGHT)
+#define AV_CH_FRONT_CENTER (1ULL << AV_CHAN_FRONT_CENTER)
+#define AV_CH_LOW_FREQUENCY (1ULL << AV_CHAN_LOW_FREQUENCY)
+#define AV_CH_BACK_LEFT (1ULL << AV_CHAN_BACK_LEFT)
+#define AV_CH_BACK_RIGHT (1ULL << AV_CHAN_BACK_RIGHT)
+#define AV_CH_FRONT_LEFT_OF_CENTER (1ULL << AV_CHAN_FRONT_LEFT_OF_CENTER)
+#define AV_CH_FRONT_RIGHT_OF_CENTER (1ULL << AV_CHAN_FRONT_RIGHT_OF_CENTER)
+#define AV_CH_BACK_CENTER (1ULL << AV_CHAN_BACK_CENTER)
+#define AV_CH_SIDE_LEFT (1ULL << AV_CHAN_SIDE_LEFT)
+#define AV_CH_SIDE_RIGHT (1ULL << AV_CHAN_SIDE_RIGHT)
+#define AV_CH_TOP_CENTER (1ULL << AV_CHAN_TOP_CENTER)
+#define AV_CH_TOP_FRONT_LEFT (1ULL << AV_CHAN_TOP_FRONT_LEFT)
+#define AV_CH_TOP_FRONT_CENTER (1ULL << AV_CHAN_TOP_FRONT_CENTER)
+#define AV_CH_TOP_FRONT_RIGHT (1ULL << AV_CHAN_TOP_FRONT_RIGHT)
+#define AV_CH_TOP_BACK_LEFT (1ULL << AV_CHAN_TOP_BACK_LEFT)
+#define AV_CH_TOP_BACK_CENTER (1ULL << AV_CHAN_TOP_BACK_CENTER)
+#define AV_CH_TOP_BACK_RIGHT (1ULL << AV_CHAN_TOP_BACK_RIGHT)
+#define AV_CH_STEREO_LEFT (1ULL << AV_CHAN_STEREO_LEFT)
+#define AV_CH_STEREO_RIGHT (1ULL << AV_CHAN_STEREO_RIGHT)
+#define AV_CH_WIDE_LEFT (1ULL << AV_CHAN_WIDE_LEFT)
+#define AV_CH_WIDE_RIGHT (1ULL << AV_CHAN_WIDE_RIGHT)
+#define AV_CH_SURROUND_DIRECT_LEFT (1ULL << AV_CHAN_SURROUND_DIRECT_LEFT)
+#define AV_CH_SURROUND_DIRECT_RIGHT (1ULL << AV_CHAN_SURROUND_DIRECT_RIGHT)
+#define AV_CH_LOW_FREQUENCY_2 (1ULL << AV_CHAN_LOW_FREQUENCY_2)
+#define AV_CH_TOP_SIDE_LEFT (1ULL << AV_CHAN_TOP_SIDE_LEFT)
+#define AV_CH_TOP_SIDE_RIGHT (1ULL << AV_CHAN_TOP_SIDE_RIGHT)
+#define AV_CH_BOTTOM_FRONT_CENTER (1ULL << AV_CHAN_BOTTOM_FRONT_CENTER)
+#define AV_CH_BOTTOM_FRONT_LEFT (1ULL << AV_CHAN_BOTTOM_FRONT_LEFT)
+#define AV_CH_BOTTOM_FRONT_RIGHT (1ULL << AV_CHAN_BOTTOM_FRONT_RIGHT)
+
+#if FF_API_OLD_CHANNEL_LAYOUT
+/** Channel mask value used for AVCodecContext.request_channel_layout
+ to indicate that the user requests the channel order of the decoder output
+ to be the native codec channel order.
+ @deprecated channel order is now indicated in a special field in
+ AVChannelLayout
+ */
+# define AV_CH_LAYOUT_NATIVE 0x8000000000000000ULL
+#endif
+
+/**
+ * @}
+ * @defgroup channel_mask_c Audio channel layouts
+ * @{
+ * */
+#define AV_CH_LAYOUT_MONO (AV_CH_FRONT_CENTER)
+#define AV_CH_LAYOUT_STEREO (AV_CH_FRONT_LEFT | AV_CH_FRONT_RIGHT)
+#define AV_CH_LAYOUT_2POINT1 (AV_CH_LAYOUT_STEREO | AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_2_1 (AV_CH_LAYOUT_STEREO | AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_SURROUND (AV_CH_LAYOUT_STEREO | AV_CH_FRONT_CENTER)
+#define AV_CH_LAYOUT_3POINT1 (AV_CH_LAYOUT_SURROUND | AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_4POINT0 (AV_CH_LAYOUT_SURROUND | AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_4POINT1 (AV_CH_LAYOUT_4POINT0 | AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_2_2 \
+ (AV_CH_LAYOUT_STEREO | AV_CH_SIDE_LEFT | AV_CH_SIDE_RIGHT)
+#define AV_CH_LAYOUT_QUAD \
+ (AV_CH_LAYOUT_STEREO | AV_CH_BACK_LEFT | AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_5POINT0 \
+ (AV_CH_LAYOUT_SURROUND | AV_CH_SIDE_LEFT | AV_CH_SIDE_RIGHT)
+#define AV_CH_LAYOUT_5POINT1 (AV_CH_LAYOUT_5POINT0 | AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_5POINT0_BACK \
+ (AV_CH_LAYOUT_SURROUND | AV_CH_BACK_LEFT | AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_5POINT1_BACK \
+ (AV_CH_LAYOUT_5POINT0_BACK | AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_6POINT0 (AV_CH_LAYOUT_5POINT0 | AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_6POINT0_FRONT \
+ (AV_CH_LAYOUT_2_2 | AV_CH_FRONT_LEFT_OF_CENTER | AV_CH_FRONT_RIGHT_OF_CENTER)
+#define AV_CH_LAYOUT_HEXAGONAL (AV_CH_LAYOUT_5POINT0_BACK | AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_6POINT1 (AV_CH_LAYOUT_5POINT1 | AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_6POINT1_BACK \
+ (AV_CH_LAYOUT_5POINT1_BACK | AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_6POINT1_FRONT \
+ (AV_CH_LAYOUT_6POINT0_FRONT | AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_7POINT0 \
+ (AV_CH_LAYOUT_5POINT0 | AV_CH_BACK_LEFT | AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_7POINT0_FRONT \
+ (AV_CH_LAYOUT_5POINT0 | AV_CH_FRONT_LEFT_OF_CENTER | \
+ AV_CH_FRONT_RIGHT_OF_CENTER)
+#define AV_CH_LAYOUT_7POINT1 \
+ (AV_CH_LAYOUT_5POINT1 | AV_CH_BACK_LEFT | AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_7POINT1_WIDE \
+ (AV_CH_LAYOUT_5POINT1 | AV_CH_FRONT_LEFT_OF_CENTER | \
+ AV_CH_FRONT_RIGHT_OF_CENTER)
+#define AV_CH_LAYOUT_7POINT1_WIDE_BACK \
+ (AV_CH_LAYOUT_5POINT1_BACK | AV_CH_FRONT_LEFT_OF_CENTER | \
+ AV_CH_FRONT_RIGHT_OF_CENTER)
+#define AV_CH_LAYOUT_7POINT1_TOP_BACK \
+ (AV_CH_LAYOUT_5POINT1_BACK | AV_CH_TOP_FRONT_LEFT | AV_CH_TOP_FRONT_RIGHT)
+#define AV_CH_LAYOUT_OCTAGONAL \
+ (AV_CH_LAYOUT_5POINT0 | AV_CH_BACK_LEFT | AV_CH_BACK_CENTER | \
+ AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_CUBE \
+ (AV_CH_LAYOUT_QUAD | AV_CH_TOP_FRONT_LEFT | AV_CH_TOP_FRONT_RIGHT | \
+ AV_CH_TOP_BACK_LEFT | AV_CH_TOP_BACK_RIGHT)
+#define AV_CH_LAYOUT_HEXADECAGONAL \
+ (AV_CH_LAYOUT_OCTAGONAL | AV_CH_WIDE_LEFT | AV_CH_WIDE_RIGHT | \
+ AV_CH_TOP_BACK_LEFT | AV_CH_TOP_BACK_RIGHT | AV_CH_TOP_BACK_CENTER | \
+ AV_CH_TOP_FRONT_CENTER | AV_CH_TOP_FRONT_LEFT | AV_CH_TOP_FRONT_RIGHT)
+#define AV_CH_LAYOUT_STEREO_DOWNMIX (AV_CH_STEREO_LEFT | AV_CH_STEREO_RIGHT)
+#define AV_CH_LAYOUT_22POINT2 \
+ (AV_CH_LAYOUT_5POINT1_BACK | AV_CH_FRONT_LEFT_OF_CENTER | \
+ AV_CH_FRONT_RIGHT_OF_CENTER | AV_CH_BACK_CENTER | AV_CH_LOW_FREQUENCY_2 | \
+ AV_CH_SIDE_LEFT | AV_CH_SIDE_RIGHT | AV_CH_TOP_FRONT_LEFT | \
+ AV_CH_TOP_FRONT_RIGHT | AV_CH_TOP_FRONT_CENTER | AV_CH_TOP_CENTER | \
+ AV_CH_TOP_BACK_LEFT | AV_CH_TOP_BACK_RIGHT | AV_CH_TOP_SIDE_LEFT | \
+ AV_CH_TOP_SIDE_RIGHT | AV_CH_TOP_BACK_CENTER | AV_CH_BOTTOM_FRONT_CENTER | \
+ AV_CH_BOTTOM_FRONT_LEFT | AV_CH_BOTTOM_FRONT_RIGHT)
+
+enum AVMatrixEncoding {
+ AV_MATRIX_ENCODING_NONE,
+ AV_MATRIX_ENCODING_DOLBY,
+ AV_MATRIX_ENCODING_DPLII,
+ AV_MATRIX_ENCODING_DPLIIX,
+ AV_MATRIX_ENCODING_DPLIIZ,
+ AV_MATRIX_ENCODING_DOLBYEX,
+ AV_MATRIX_ENCODING_DOLBYHEADPHONE,
+ AV_MATRIX_ENCODING_NB
+};
+
+/**
+ * @}
+ */
+
+/**
+ * An AVChannelCustom defines a single channel within a custom order layout
+ *
+ * Unlike most structures in FFmpeg, sizeof(AVChannelCustom) is a part of the
+ * public ABI.
+ *
+ * No new fields may be added to it without a major version bump.
+ */
+typedef struct AVChannelCustom {
+ enum AVChannel id;
+ char name[16];
+ void* opaque;
+} AVChannelCustom;
+
+/**
+ * An AVChannelLayout holds information about the channel layout of audio data.
+ *
+ * A channel layout here is defined as a set of channels ordered in a specific
+ * way (unless the channel order is AV_CHANNEL_ORDER_UNSPEC, in which case an
+ * AVChannelLayout carries only the channel count).
+ * All orders may be treated as if they were AV_CHANNEL_ORDER_UNSPEC by
+ * ignoring everything but the channel count, as long as
+ * av_channel_layout_check() considers they are valid.
+ *
+ * Unlike most structures in FFmpeg, sizeof(AVChannelLayout) is a part of the
+ * public ABI and may be used by the caller. E.g. it may be allocated on stack
+ * or embedded in caller-defined structs.
+ *
+ * AVChannelLayout can be initialized as follows:
+ * - default initialization with {0}, followed by setting all used fields
+ * correctly;
+ * - by assigning one of the predefined AV_CHANNEL_LAYOUT_* initializers;
+ * - with a constructor function, such as av_channel_layout_default(),
+ * av_channel_layout_from_mask() or av_channel_layout_from_string().
+ *
+ * The channel layout must be unitialized with av_channel_layout_uninit()
+ *
+ * Copying an AVChannelLayout via assigning is forbidden,
+ * av_channel_layout_copy() must be used instead (and its return value should
+ * be checked)
+ *
+ * No new fields may be added to it without a major version bump, except for
+ * new elements of the union fitting in sizeof(uint64_t).
+ */
+typedef struct AVChannelLayout {
+ /**
+ * Channel order used in this layout.
+ * This is a mandatory field.
+ */
+ enum AVChannelOrder order;
+
+ /**
+ * Number of channels in this layout. Mandatory field.
+ */
+ int nb_channels;
+
+ /**
+ * Details about which channels are present in this layout.
+ * For AV_CHANNEL_ORDER_UNSPEC, this field is undefined and must not be
+ * used.
+ */
+ union {
+ /**
+ * This member must be used for AV_CHANNEL_ORDER_NATIVE, and may be used
+ * for AV_CHANNEL_ORDER_AMBISONIC to signal non-diegetic channels.
+ * It is a bitmask, where the position of each set bit means that the
+ * AVChannel with the corresponding value is present.
+ *
+ * I.e. when (mask & (1 << AV_CHAN_FOO)) is non-zero, then AV_CHAN_FOO
+ * is present in the layout. Otherwise it is not present.
+ *
+ * @note when a channel layout using a bitmask is constructed or
+ * modified manually (i.e. not using any of the av_channel_layout_*
+ * functions), the code doing it must ensure that the number of set bits
+ * is equal to nb_channels.
+ */
+ uint64_t mask;
+ /**
+ * This member must be used when the channel order is
+ * AV_CHANNEL_ORDER_CUSTOM. It is a nb_channels-sized array, with each
+ * element signalling the presence of the AVChannel with the
+ * corresponding value in map[i].id.
+ *
+ * I.e. when map[i].id is equal to AV_CHAN_FOO, then AV_CH_FOO is the
+ * i-th channel in the audio data.
+ *
+ * When map[i].id is in the range between AV_CHAN_AMBISONIC_BASE and
+ * AV_CHAN_AMBISONIC_END (inclusive), the channel contains an ambisonic
+ * component with ACN index (as defined above)
+ * n = map[i].id - AV_CHAN_AMBISONIC_BASE.
+ *
+ * map[i].name may be filled with a 0-terminated string, in which case
+ * it will be used for the purpose of identifying the channel with the
+ * convenience functions below. Otherise it must be zeroed.
+ */
+ AVChannelCustom* map;
+ } u;
+
+ /**
+ * For some private data of the user.
+ */
+ void* opaque;
+} AVChannelLayout;
+
+#define AV_CHANNEL_LAYOUT_MASK(nb, m) \
+ { \
+ .order = AV_CHANNEL_ORDER_NATIVE, .nb_channels = (nb), .u = {.mask = (m) } \
+ }
+
+/**
+ * @name Common pre-defined channel layouts
+ * @{
+ */
+#define AV_CHANNEL_LAYOUT_MONO AV_CHANNEL_LAYOUT_MASK(1, AV_CH_LAYOUT_MONO)
+#define AV_CHANNEL_LAYOUT_STEREO AV_CHANNEL_LAYOUT_MASK(2, AV_CH_LAYOUT_STEREO)
+#define AV_CHANNEL_LAYOUT_2POINT1 \
+ AV_CHANNEL_LAYOUT_MASK(3, AV_CH_LAYOUT_2POINT1)
+#define AV_CHANNEL_LAYOUT_2_1 AV_CHANNEL_LAYOUT_MASK(3, AV_CH_LAYOUT_2_1)
+#define AV_CHANNEL_LAYOUT_SURROUND \
+ AV_CHANNEL_LAYOUT_MASK(3, AV_CH_LAYOUT_SURROUND)
+#define AV_CHANNEL_LAYOUT_3POINT1 \
+ AV_CHANNEL_LAYOUT_MASK(4, AV_CH_LAYOUT_3POINT1)
+#define AV_CHANNEL_LAYOUT_4POINT0 \
+ AV_CHANNEL_LAYOUT_MASK(4, AV_CH_LAYOUT_4POINT0)
+#define AV_CHANNEL_LAYOUT_4POINT1 \
+ AV_CHANNEL_LAYOUT_MASK(5, AV_CH_LAYOUT_4POINT1)
+#define AV_CHANNEL_LAYOUT_2_2 AV_CHANNEL_LAYOUT_MASK(4, AV_CH_LAYOUT_2_2)
+#define AV_CHANNEL_LAYOUT_QUAD AV_CHANNEL_LAYOUT_MASK(4, AV_CH_LAYOUT_QUAD)
+#define AV_CHANNEL_LAYOUT_5POINT0 \
+ AV_CHANNEL_LAYOUT_MASK(5, AV_CH_LAYOUT_5POINT0)
+#define AV_CHANNEL_LAYOUT_5POINT1 \
+ AV_CHANNEL_LAYOUT_MASK(6, AV_CH_LAYOUT_5POINT1)
+#define AV_CHANNEL_LAYOUT_5POINT0_BACK \
+ AV_CHANNEL_LAYOUT_MASK(5, AV_CH_LAYOUT_5POINT0_BACK)
+#define AV_CHANNEL_LAYOUT_5POINT1_BACK \
+ AV_CHANNEL_LAYOUT_MASK(6, AV_CH_LAYOUT_5POINT1_BACK)
+#define AV_CHANNEL_LAYOUT_6POINT0 \
+ AV_CHANNEL_LAYOUT_MASK(6, AV_CH_LAYOUT_6POINT0)
+#define AV_CHANNEL_LAYOUT_6POINT0_FRONT \
+ AV_CHANNEL_LAYOUT_MASK(6, AV_CH_LAYOUT_6POINT0_FRONT)
+#define AV_CHANNEL_LAYOUT_HEXAGONAL \
+ AV_CHANNEL_LAYOUT_MASK(6, AV_CH_LAYOUT_HEXAGONAL)
+#define AV_CHANNEL_LAYOUT_6POINT1 \
+ AV_CHANNEL_LAYOUT_MASK(7, AV_CH_LAYOUT_6POINT1)
+#define AV_CHANNEL_LAYOUT_6POINT1_BACK \
+ AV_CHANNEL_LAYOUT_MASK(7, AV_CH_LAYOUT_6POINT1_BACK)
+#define AV_CHANNEL_LAYOUT_6POINT1_FRONT \
+ AV_CHANNEL_LAYOUT_MASK(7, AV_CH_LAYOUT_6POINT1_FRONT)
+#define AV_CHANNEL_LAYOUT_7POINT0 \
+ AV_CHANNEL_LAYOUT_MASK(7, AV_CH_LAYOUT_7POINT0)
+#define AV_CHANNEL_LAYOUT_7POINT0_FRONT \
+ AV_CHANNEL_LAYOUT_MASK(7, AV_CH_LAYOUT_7POINT0_FRONT)
+#define AV_CHANNEL_LAYOUT_7POINT1 \
+ AV_CHANNEL_LAYOUT_MASK(8, AV_CH_LAYOUT_7POINT1)
+#define AV_CHANNEL_LAYOUT_7POINT1_WIDE \
+ AV_CHANNEL_LAYOUT_MASK(8, AV_CH_LAYOUT_7POINT1_WIDE)
+#define AV_CHANNEL_LAYOUT_7POINT1_WIDE_BACK \
+ AV_CHANNEL_LAYOUT_MASK(8, AV_CH_LAYOUT_7POINT1_WIDE_BACK)
+#define AV_CHANNEL_LAYOUT_7POINT1_TOP_BACK \
+ AV_CHANNEL_LAYOUT_MASK(8, AV_CH_LAYOUT_7POINT1_TOP_BACK)
+#define AV_CHANNEL_LAYOUT_OCTAGONAL \
+ AV_CHANNEL_LAYOUT_MASK(8, AV_CH_LAYOUT_OCTAGONAL)
+#define AV_CHANNEL_LAYOUT_CUBE AV_CHANNEL_LAYOUT_MASK(8, AV_CH_LAYOUT_CUBE)
+#define AV_CHANNEL_LAYOUT_HEXADECAGONAL \
+ AV_CHANNEL_LAYOUT_MASK(16, AV_CH_LAYOUT_HEXADECAGONAL)
+#define AV_CHANNEL_LAYOUT_STEREO_DOWNMIX \
+ AV_CHANNEL_LAYOUT_MASK(2, AV_CH_LAYOUT_STEREO_DOWNMIX)
+#define AV_CHANNEL_LAYOUT_22POINT2 \
+ AV_CHANNEL_LAYOUT_MASK(24, AV_CH_LAYOUT_22POINT2)
+#define AV_CHANNEL_LAYOUT_AMBISONIC_FIRST_ORDER \
+ { \
+ .order = AV_CHANNEL_ORDER_AMBISONIC, .nb_channels = 4, .u = {.mask = 0 } \
+ }
+/** @} */
+
+struct AVBPrint;
+
+#if FF_API_OLD_CHANNEL_LAYOUT
+/**
+ * @name Deprecated Functions
+ * @{
+ */
+
+/**
+ * Return a channel layout id that matches name, or 0 if no match is found.
+ *
+ * name can be one or several of the following notations,
+ * separated by '+' or '|':
+ * - the name of an usual channel layout (mono, stereo, 4.0, quad, 5.0,
+ * 5.0(side), 5.1, 5.1(side), 7.1, 7.1(wide), downmix);
+ * - the name of a single channel (FL, FR, FC, LFE, BL, BR, FLC, FRC, BC,
+ * SL, SR, TC, TFL, TFC, TFR, TBL, TBC, TBR, DL, DR);
+ * - a number of channels, in decimal, followed by 'c', yielding
+ * the default channel layout for that number of channels (@see
+ * av_get_default_channel_layout);
+ * - a channel layout mask, in hexadecimal starting with "0x" (see the
+ * AV_CH_* macros).
+ *
+ * Example: "stereo+FC" = "2c+FC" = "2c+1c" = "0x7"
+ *
+ * @deprecated use av_channel_layout_from_string()
+ */
+attribute_deprecated uint64_t av_get_channel_layout(const char* name);
+
+/**
+ * Return a channel layout and the number of channels based on the specified
+ * name.
+ *
+ * This function is similar to (@see av_get_channel_layout), but can also parse
+ * unknown channel layout specifications.
+ *
+ * @param[in] name channel layout specification string
+ * @param[out] channel_layout parsed channel layout (0 if unknown)
+ * @param[out] nb_channels number of channels
+ *
+ * @return 0 on success, AVERROR(EINVAL) if the parsing fails.
+ * @deprecated use av_channel_layout_from_string()
+ */
+attribute_deprecated int av_get_extended_channel_layout(
+ const char* name, uint64_t* channel_layout, int* nb_channels);
+
+/**
+ * Return a description of a channel layout.
+ * If nb_channels is <= 0, it is guessed from the channel_layout.
+ *
+ * @param buf put here the string containing the channel layout
+ * @param buf_size size in bytes of the buffer
+ * @param nb_channels number of channels
+ * @param channel_layout channel layout bitset
+ * @deprecated use av_channel_layout_describe()
+ */
+attribute_deprecated void av_get_channel_layout_string(char* buf, int buf_size,
+ int nb_channels,
+ uint64_t channel_layout);
+
+/**
+ * Append a description of a channel layout to a bprint buffer.
+ * @deprecated use av_channel_layout_describe()
+ */
+attribute_deprecated void av_bprint_channel_layout(struct AVBPrint* bp,
+ int nb_channels,
+ uint64_t channel_layout);
+
+/**
+ * Return the number of channels in the channel layout.
+ * @deprecated use AVChannelLayout.nb_channels
+ */
+attribute_deprecated int av_get_channel_layout_nb_channels(
+ uint64_t channel_layout);
+
+/**
+ * Return default channel layout for a given number of channels.
+ *
+ * @deprecated use av_channel_layout_default()
+ */
+attribute_deprecated int64_t av_get_default_channel_layout(int nb_channels);
+
+/**
+ * Get the index of a channel in channel_layout.
+ *
+ * @param channel_layout channel layout bitset
+ * @param channel a channel layout describing exactly one channel which must be
+ * present in channel_layout.
+ *
+ * @return index of channel in channel_layout on success, a negative AVERROR
+ * on error.
+ *
+ * @deprecated use av_channel_layout_index_from_channel()
+ */
+attribute_deprecated int av_get_channel_layout_channel_index(
+ uint64_t channel_layout, uint64_t channel);
+
+/**
+ * Get the channel with the given index in channel_layout.
+ * @deprecated use av_channel_layout_channel_from_index()
+ */
+attribute_deprecated uint64_t
+av_channel_layout_extract_channel(uint64_t channel_layout, int index);
+
+/**
+ * Get the name of a given channel.
+ *
+ * @return channel name on success, NULL on error.
+ *
+ * @deprecated use av_channel_name()
+ */
+attribute_deprecated const char* av_get_channel_name(uint64_t channel);
+
+/**
+ * Get the description of a given channel.
+ *
+ * @param channel a channel layout with a single channel
+ * @return channel description on success, NULL on error
+ * @deprecated use av_channel_description()
+ */
+attribute_deprecated const char* av_get_channel_description(uint64_t channel);
+
+/**
+ * Get the value and name of a standard channel layout.
+ *
+ * @param[in] index index in an internal list, starting at 0
+ * @param[out] layout channel layout mask
+ * @param[out] name name of the layout
+ * @return 0 if the layout exists,
+ * <0 if index is beyond the limits
+ * @deprecated use av_channel_layout_standard()
+ */
+attribute_deprecated int av_get_standard_channel_layout(unsigned index,
+ uint64_t* layout,
+ const char** name);
+/**
+ * @}
+ */
+#endif
+
+/**
+ * Get a human readable string in an abbreviated form describing a given
+ * channel. This is the inverse function of @ref av_channel_from_string().
+ *
+ * @param buf pre-allocated buffer where to put the generated string
+ * @param buf_size size in bytes of the buffer.
+ * @param channel the AVChannel whose name to get
+ * @return amount of bytes needed to hold the output string, or a negative
+ * AVERROR on failure. If the returned value is bigger than buf_size, then the
+ * string was truncated.
+ */
+int av_channel_name(char* buf, size_t buf_size, enum AVChannel channel);
+
+/**
+ * bprint variant of av_channel_name().
+ *
+ * @note the string will be appended to the bprint buffer.
+ */
+void av_channel_name_bprint(struct AVBPrint* bp, enum AVChannel channel_id);
+
+/**
+ * Get a human readable string describing a given channel.
+ *
+ * @param buf pre-allocated buffer where to put the generated string
+ * @param buf_size size in bytes of the buffer.
+ * @param channel the AVChannel whose description to get
+ * @return amount of bytes needed to hold the output string, or a negative
+ * AVERROR on failure. If the returned value is bigger than buf_size, then the
+ * string was truncated.
+ */
+int av_channel_description(char* buf, size_t buf_size, enum AVChannel channel);
+
+/**
+ * bprint variant of av_channel_description().
+ *
+ * @note the string will be appended to the bprint buffer.
+ */
+void av_channel_description_bprint(struct AVBPrint* bp,
+ enum AVChannel channel_id);
+
+/**
+ * This is the inverse function of @ref av_channel_name().
+ *
+ * @return the channel with the given name
+ * AV_CHAN_NONE when name does not identify a known channel
+ */
+enum AVChannel av_channel_from_string(const char* name);
+
+/**
+ * Initialize a native channel layout from a bitmask indicating which channels
+ * are present.
+ *
+ * @param channel_layout the layout structure to be initialized
+ * @param mask bitmask describing the channel layout
+ *
+ * @return 0 on success
+ * AVERROR(EINVAL) for invalid mask values
+ */
+int av_channel_layout_from_mask(AVChannelLayout* channel_layout, uint64_t mask);
+
+/**
+ * Initialize a channel layout from a given string description.
+ * The input string can be represented by:
+ * - the formal channel layout name (returned by av_channel_layout_describe())
+ * - single or multiple channel names (returned by av_channel_name(), eg. "FL",
+ * or concatenated with "+", each optionally containing a custom name after
+ * a "@", eg. "FL@Left+FR@Right+LFE")
+ * - a decimal or hexadecimal value of a native channel layout (eg. "4" or
+ * "0x4")
+ * - the number of channels with default layout (eg. "4c")
+ * - the number of unordered channels (eg. "4C" or "4 channels")
+ * - the ambisonic order followed by optional non-diegetic channels (eg.
+ * "ambisonic 2+stereo")
+ *
+ * @param channel_layout input channel layout
+ * @param str string describing the channel layout
+ * @return 0 channel layout was detected, AVERROR_INVALIDATATA otherwise
+ */
+int av_channel_layout_from_string(AVChannelLayout* channel_layout,
+ const char* str);
+
+/**
+ * Get the default channel layout for a given number of channels.
+ *
+ * @param ch_layout the layout structure to be initialized
+ * @param nb_channels number of channels
+ */
+void av_channel_layout_default(AVChannelLayout* ch_layout, int nb_channels);
+
+/**
+ * Iterate over all standard channel layouts.
+ *
+ * @param opaque a pointer where libavutil will store the iteration state. Must
+ * point to NULL to start the iteration.
+ *
+ * @return the standard channel layout or NULL when the iteration is
+ * finished
+ */
+const AVChannelLayout* av_channel_layout_standard(void** opaque);
+
+/**
+ * Free any allocated data in the channel layout and reset the channel
+ * count to 0.
+ *
+ * @param channel_layout the layout structure to be uninitialized
+ */
+void av_channel_layout_uninit(AVChannelLayout* channel_layout);
+
+/**
+ * Make a copy of a channel layout. This differs from just assigning src to dst
+ * in that it allocates and copies the map for AV_CHANNEL_ORDER_CUSTOM.
+ *
+ * @note the destination channel_layout will be always uninitialized before
+ * copy.
+ *
+ * @param dst destination channel layout
+ * @param src source channel layout
+ * @return 0 on success, a negative AVERROR on error.
+ */
+int av_channel_layout_copy(AVChannelLayout* dst, const AVChannelLayout* src);
+
+/**
+ * Get a human-readable string describing the channel layout properties.
+ * The string will be in the same format that is accepted by
+ * @ref av_channel_layout_from_string(), allowing to rebuild the same
+ * channel layout, except for opaque pointers.
+ *
+ * @param channel_layout channel layout to be described
+ * @param buf pre-allocated buffer where to put the generated string
+ * @param buf_size size in bytes of the buffer.
+ * @return amount of bytes needed to hold the output string, or a negative
+ * AVERROR on failure. If the returned value is bigger than buf_size, then the
+ * string was truncated.
+ */
+int av_channel_layout_describe(const AVChannelLayout* channel_layout, char* buf,
+ size_t buf_size);
+
+/**
+ * bprint variant of av_channel_layout_describe().
+ *
+ * @note the string will be appended to the bprint buffer.
+ * @return 0 on success, or a negative AVERROR value on failure.
+ */
+int av_channel_layout_describe_bprint(const AVChannelLayout* channel_layout,
+ struct AVBPrint* bp);
+
+/**
+ * Get the channel with the given index in a channel layout.
+ *
+ * @param channel_layout input channel layout
+ * @param idx index of the channel
+ * @return channel with the index idx in channel_layout on success or
+ * AV_CHAN_NONE on failure (if idx is not valid or the channel order is
+ * unspecified)
+ */
+enum AVChannel av_channel_layout_channel_from_index(
+ const AVChannelLayout* channel_layout, unsigned int idx);
+
+/**
+ * Get the index of a given channel in a channel layout. In case multiple
+ * channels are found, only the first match will be returned.
+ *
+ * @param channel_layout input channel layout
+ * @param channel the channel whose index to obtain
+ * @return index of channel in channel_layout on success or a negative number if
+ * channel is not present in channel_layout.
+ */
+int av_channel_layout_index_from_channel(const AVChannelLayout* channel_layout,
+ enum AVChannel channel);
+
+/**
+ * Get the index in a channel layout of a channel described by the given string.
+ * In case multiple channels are found, only the first match will be returned.
+ *
+ * This function accepts channel names in the same format as
+ * @ref av_channel_from_string().
+ *
+ * @param channel_layout input channel layout
+ * @param name string describing the channel whose index to obtain
+ * @return a channel index described by the given string, or a negative AVERROR
+ * value.
+ */
+int av_channel_layout_index_from_string(const AVChannelLayout* channel_layout,
+ const char* name);
+
+/**
+ * Get a channel described by the given string.
+ *
+ * This function accepts channel names in the same format as
+ * @ref av_channel_from_string().
+ *
+ * @param channel_layout input channel layout
+ * @param name string describing the channel to obtain
+ * @return a channel described by the given string in channel_layout on success
+ * or AV_CHAN_NONE on failure (if the string is not valid or the channel
+ * order is unspecified)
+ */
+enum AVChannel av_channel_layout_channel_from_string(
+ const AVChannelLayout* channel_layout, const char* name);
+
+/**
+ * Find out what channels from a given set are present in a channel layout,
+ * without regard for their positions.
+ *
+ * @param channel_layout input channel layout
+ * @param mask a combination of AV_CH_* representing a set of channels
+ * @return a bitfield representing all the channels from mask that are present
+ * in channel_layout
+ */
+uint64_t av_channel_layout_subset(const AVChannelLayout* channel_layout,
+ uint64_t mask);
+
+/**
+ * Check whether a channel layout is valid, i.e. can possibly describe audio
+ * data.
+ *
+ * @param channel_layout input channel layout
+ * @return 1 if channel_layout is valid, 0 otherwise.
+ */
+int av_channel_layout_check(const AVChannelLayout* channel_layout);
+
+/**
+ * Check whether two channel layouts are semantically the same, i.e. the same
+ * channels are present on the same positions in both.
+ *
+ * If one of the channel layouts is AV_CHANNEL_ORDER_UNSPEC, while the other is
+ * not, they are considered to be unequal. If both are AV_CHANNEL_ORDER_UNSPEC,
+ * they are considered equal iff the channel counts are the same in both.
+ *
+ * @param chl input channel layout
+ * @param chl1 input channel layout
+ * @return 0 if chl and chl1 are equal, 1 if they are not equal. A negative
+ * AVERROR code if one or both are invalid.
+ */
+int av_channel_layout_compare(const AVChannelLayout* chl,
+ const AVChannelLayout* chl1);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_CHANNEL_LAYOUT_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/common.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/common.h
new file mode 100644
index 0000000000..6c09e40bc0
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/common.h
@@ -0,0 +1,589 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * common internal and external API header
+ */
+
+#ifndef AVUTIL_COMMON_H
+#define AVUTIL_COMMON_H
+
+#if defined(__cplusplus) && !defined(__STDC_CONSTANT_MACROS) && \
+ !defined(UINT64_C)
+# error missing -D__STDC_CONSTANT_MACROS / #define __STDC_CONSTANT_MACROS
+#endif
+
+#include <errno.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <math.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "attributes.h"
+#include "macros.h"
+
+// rounded division & shift
+#define RSHIFT(a, b) \
+ ((a) > 0 ? ((a) + ((1 << (b)) >> 1)) >> (b) \
+ : ((a) + ((1 << (b)) >> 1) - 1) >> (b))
+/* assume b>0 */
+#define ROUNDED_DIV(a, b) \
+ (((a) >= 0 ? (a) + ((b) >> 1) : (a) - ((b) >> 1)) / (b))
+/* Fast a/(1<<b) rounded toward +inf. Assume a>=0 and b>=0 */
+#define AV_CEIL_RSHIFT(a, b) \
+ (!av_builtin_constant_p(b) ? -((-(a)) >> (b)) : ((a) + (1 << (b)) - 1) >> (b))
+/* Backwards compat. */
+#define FF_CEIL_RSHIFT AV_CEIL_RSHIFT
+
+#define FFUDIV(a, b) (((a) > 0 ? (a) : (a) - (b) + 1) / (b))
+#define FFUMOD(a, b) ((a) - (b)*FFUDIV(a, b))
+
+/**
+ * Absolute value, Note, INT_MIN / INT64_MIN result in undefined behavior as
+ * they are not representable as absolute values of their type. This is the same
+ * as with *abs()
+ * @see FFNABS()
+ */
+#define FFABS(a) ((a) >= 0 ? (a) : (-(a)))
+#define FFSIGN(a) ((a) > 0 ? 1 : -1)
+
+/**
+ * Negative Absolute value.
+ * this works for all integers of all types.
+ * As with many macros, this evaluates its argument twice, it thus must not have
+ * a sideeffect, that is FFNABS(x++) has undefined behavior.
+ */
+#define FFNABS(a) ((a) <= 0 ? (a) : (-(a)))
+
+/**
+ * Unsigned Absolute value.
+ * This takes the absolute value of a signed int and returns it as a unsigned.
+ * This also works with INT_MIN which would otherwise not be representable
+ * As with many macros, this evaluates its argument twice.
+ */
+#define FFABSU(a) ((a) <= 0 ? -(unsigned)(a) : (unsigned)(a))
+#define FFABS64U(a) ((a) <= 0 ? -(uint64_t)(a) : (uint64_t)(a))
+
+/* misc math functions */
+
+#ifdef HAVE_AV_CONFIG_H
+# include "config.h"
+# include "intmath.h"
+#endif
+
+#ifndef av_ceil_log2
+# define av_ceil_log2 av_ceil_log2_c
+#endif
+#ifndef av_clip
+# define av_clip av_clip_c
+#endif
+#ifndef av_clip64
+# define av_clip64 av_clip64_c
+#endif
+#ifndef av_clip_uint8
+# define av_clip_uint8 av_clip_uint8_c
+#endif
+#ifndef av_clip_int8
+# define av_clip_int8 av_clip_int8_c
+#endif
+#ifndef av_clip_uint16
+# define av_clip_uint16 av_clip_uint16_c
+#endif
+#ifndef av_clip_int16
+# define av_clip_int16 av_clip_int16_c
+#endif
+#ifndef av_clipl_int32
+# define av_clipl_int32 av_clipl_int32_c
+#endif
+#ifndef av_clip_intp2
+# define av_clip_intp2 av_clip_intp2_c
+#endif
+#ifndef av_clip_uintp2
+# define av_clip_uintp2 av_clip_uintp2_c
+#endif
+#ifndef av_mod_uintp2
+# define av_mod_uintp2 av_mod_uintp2_c
+#endif
+#ifndef av_sat_add32
+# define av_sat_add32 av_sat_add32_c
+#endif
+#ifndef av_sat_dadd32
+# define av_sat_dadd32 av_sat_dadd32_c
+#endif
+#ifndef av_sat_sub32
+# define av_sat_sub32 av_sat_sub32_c
+#endif
+#ifndef av_sat_dsub32
+# define av_sat_dsub32 av_sat_dsub32_c
+#endif
+#ifndef av_sat_add64
+# define av_sat_add64 av_sat_add64_c
+#endif
+#ifndef av_sat_sub64
+# define av_sat_sub64 av_sat_sub64_c
+#endif
+#ifndef av_clipf
+# define av_clipf av_clipf_c
+#endif
+#ifndef av_clipd
+# define av_clipd av_clipd_c
+#endif
+#ifndef av_popcount
+# define av_popcount av_popcount_c
+#endif
+#ifndef av_popcount64
+# define av_popcount64 av_popcount64_c
+#endif
+#ifndef av_parity
+# define av_parity av_parity_c
+#endif
+
+#ifndef av_log2
+av_const int av_log2(unsigned v);
+#endif
+
+#ifndef av_log2_16bit
+av_const int av_log2_16bit(unsigned v);
+#endif
+
+/**
+ * Clip a signed integer value into the amin-amax range.
+ * @param a value to clip
+ * @param amin minimum value of the clip range
+ * @param amax maximum value of the clip range
+ * @return clipped value
+ */
+static av_always_inline av_const int av_clip_c(int a, int amin, int amax) {
+#if defined(HAVE_AV_CONFIG_H) && defined(ASSERT_LEVEL) && ASSERT_LEVEL >= 2
+ if (amin > amax) abort();
+#endif
+ if (a < amin)
+ return amin;
+ else if (a > amax)
+ return amax;
+ else
+ return a;
+}
+
+/**
+ * Clip a signed 64bit integer value into the amin-amax range.
+ * @param a value to clip
+ * @param amin minimum value of the clip range
+ * @param amax maximum value of the clip range
+ * @return clipped value
+ */
+static av_always_inline av_const int64_t av_clip64_c(int64_t a, int64_t amin,
+ int64_t amax) {
+#if defined(HAVE_AV_CONFIG_H) && defined(ASSERT_LEVEL) && ASSERT_LEVEL >= 2
+ if (amin > amax) abort();
+#endif
+ if (a < amin)
+ return amin;
+ else if (a > amax)
+ return amax;
+ else
+ return a;
+}
+
+/**
+ * Clip a signed integer value into the 0-255 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const uint8_t av_clip_uint8_c(int a) {
+ if (a & (~0xFF))
+ return (~a) >> 31;
+ else
+ return a;
+}
+
+/**
+ * Clip a signed integer value into the -128,127 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const int8_t av_clip_int8_c(int a) {
+ if ((a + 0x80U) & ~0xFF)
+ return (a >> 31) ^ 0x7F;
+ else
+ return a;
+}
+
+/**
+ * Clip a signed integer value into the 0-65535 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const uint16_t av_clip_uint16_c(int a) {
+ if (a & (~0xFFFF))
+ return (~a) >> 31;
+ else
+ return a;
+}
+
+/**
+ * Clip a signed integer value into the -32768,32767 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const int16_t av_clip_int16_c(int a) {
+ if ((a + 0x8000U) & ~0xFFFF)
+ return (a >> 31) ^ 0x7FFF;
+ else
+ return a;
+}
+
+/**
+ * Clip a signed 64-bit integer value into the -2147483648,2147483647 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const int32_t av_clipl_int32_c(int64_t a) {
+ if ((a + 0x80000000u) & ~UINT64_C(0xFFFFFFFF))
+ return (int32_t)((a >> 63) ^ 0x7FFFFFFF);
+ else
+ return (int32_t)a;
+}
+
+/**
+ * Clip a signed integer into the -(2^p),(2^p-1) range.
+ * @param a value to clip
+ * @param p bit position to clip at
+ * @return clipped value
+ */
+static av_always_inline av_const int av_clip_intp2_c(int a, int p) {
+ if (((unsigned)a + (1 << p)) & ~((2 << p) - 1))
+ return (a >> 31) ^ ((1 << p) - 1);
+ else
+ return a;
+}
+
+/**
+ * Clip a signed integer to an unsigned power of two range.
+ * @param a value to clip
+ * @param p bit position to clip at
+ * @return clipped value
+ */
+static av_always_inline av_const unsigned av_clip_uintp2_c(int a, int p) {
+ if (a & ~((1 << p) - 1))
+ return (~a) >> 31 & ((1 << p) - 1);
+ else
+ return a;
+}
+
+/**
+ * Clear high bits from an unsigned integer starting with specific bit position
+ * @param a value to clip
+ * @param p bit position to clip at
+ * @return clipped value
+ */
+static av_always_inline av_const unsigned av_mod_uintp2_c(unsigned a,
+ unsigned p) {
+ return a & ((1U << p) - 1);
+}
+
+/**
+ * Add two signed 32-bit values with saturation.
+ *
+ * @param a one value
+ * @param b another value
+ * @return sum with signed saturation
+ */
+static av_always_inline int av_sat_add32_c(int a, int b) {
+ return av_clipl_int32((int64_t)a + b);
+}
+
+/**
+ * Add a doubled value to another value with saturation at both stages.
+ *
+ * @param a first value
+ * @param b value doubled and added to a
+ * @return sum sat(a + sat(2*b)) with signed saturation
+ */
+static av_always_inline int av_sat_dadd32_c(int a, int b) {
+ return av_sat_add32(a, av_sat_add32(b, b));
+}
+
+/**
+ * Subtract two signed 32-bit values with saturation.
+ *
+ * @param a one value
+ * @param b another value
+ * @return difference with signed saturation
+ */
+static av_always_inline int av_sat_sub32_c(int a, int b) {
+ return av_clipl_int32((int64_t)a - b);
+}
+
+/**
+ * Subtract a doubled value from another value with saturation at both stages.
+ *
+ * @param a first value
+ * @param b value doubled and subtracted from a
+ * @return difference sat(a - sat(2*b)) with signed saturation
+ */
+static av_always_inline int av_sat_dsub32_c(int a, int b) {
+ return av_sat_sub32(a, av_sat_add32(b, b));
+}
+
+/**
+ * Add two signed 64-bit values with saturation.
+ *
+ * @param a one value
+ * @param b another value
+ * @return sum with signed saturation
+ */
+static av_always_inline int64_t av_sat_add64_c(int64_t a, int64_t b) {
+#if (!defined(__INTEL_COMPILER) && AV_GCC_VERSION_AT_LEAST(5, 1)) || \
+ AV_HAS_BUILTIN(__builtin_add_overflow)
+ int64_t tmp;
+ return !__builtin_add_overflow(a, b, &tmp)
+ ? tmp
+ : (tmp < 0 ? INT64_MAX : INT64_MIN);
+#else
+ int64_t s = a + (uint64_t)b;
+ if ((int64_t)(a ^ b | ~s ^ b) >= 0) return INT64_MAX ^ (b >> 63);
+ return s;
+#endif
+}
+
+/**
+ * Subtract two signed 64-bit values with saturation.
+ *
+ * @param a one value
+ * @param b another value
+ * @return difference with signed saturation
+ */
+static av_always_inline int64_t av_sat_sub64_c(int64_t a, int64_t b) {
+#if (!defined(__INTEL_COMPILER) && AV_GCC_VERSION_AT_LEAST(5, 1)) || \
+ AV_HAS_BUILTIN(__builtin_sub_overflow)
+ int64_t tmp;
+ return !__builtin_sub_overflow(a, b, &tmp)
+ ? tmp
+ : (tmp < 0 ? INT64_MAX : INT64_MIN);
+#else
+ if (b <= 0 && a >= INT64_MAX + b) return INT64_MAX;
+ if (b >= 0 && a <= INT64_MIN + b) return INT64_MIN;
+ return a - b;
+#endif
+}
+
+/**
+ * Clip a float value into the amin-amax range.
+ * If a is nan or -inf amin will be returned.
+ * If a is +inf amax will be returned.
+ * @param a value to clip
+ * @param amin minimum value of the clip range
+ * @param amax maximum value of the clip range
+ * @return clipped value
+ */
+static av_always_inline av_const float av_clipf_c(float a, float amin,
+ float amax) {
+#if defined(HAVE_AV_CONFIG_H) && defined(ASSERT_LEVEL) && ASSERT_LEVEL >= 2
+ if (amin > amax) abort();
+#endif
+ return FFMIN(FFMAX(a, amin), amax);
+}
+
+/**
+ * Clip a double value into the amin-amax range.
+ * If a is nan or -inf amin will be returned.
+ * If a is +inf amax will be returned.
+ * @param a value to clip
+ * @param amin minimum value of the clip range
+ * @param amax maximum value of the clip range
+ * @return clipped value
+ */
+static av_always_inline av_const double av_clipd_c(double a, double amin,
+ double amax) {
+#if defined(HAVE_AV_CONFIG_H) && defined(ASSERT_LEVEL) && ASSERT_LEVEL >= 2
+ if (amin > amax) abort();
+#endif
+ return FFMIN(FFMAX(a, amin), amax);
+}
+
+/** Compute ceil(log2(x)).
+ * @param x value used to compute ceil(log2(x))
+ * @return computed ceiling of log2(x)
+ */
+static av_always_inline av_const int av_ceil_log2_c(int x) {
+ return av_log2((x - 1U) << 1);
+}
+
+/**
+ * Count number of bits set to one in x
+ * @param x value to count bits of
+ * @return the number of bits set to one in x
+ */
+static av_always_inline av_const int av_popcount_c(uint32_t x) {
+ x -= (x >> 1) & 0x55555555;
+ x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
+ x = (x + (x >> 4)) & 0x0F0F0F0F;
+ x += x >> 8;
+ return (x + (x >> 16)) & 0x3F;
+}
+
+/**
+ * Count number of bits set to one in x
+ * @param x value to count bits of
+ * @return the number of bits set to one in x
+ */
+static av_always_inline av_const int av_popcount64_c(uint64_t x) {
+ return av_popcount((uint32_t)x) + av_popcount((uint32_t)(x >> 32));
+}
+
+static av_always_inline av_const int av_parity_c(uint32_t v) {
+ return av_popcount(v) & 1;
+}
+
+/**
+ * Convert a UTF-8 character (up to 4 bytes) to its 32-bit UCS-4 encoded form.
+ *
+ * @param val Output value, must be an lvalue of type uint32_t.
+ * @param GET_BYTE Expression reading one byte from the input.
+ * Evaluated up to 7 times (4 for the currently
+ * assigned Unicode range). With a memory buffer
+ * input, this could be *ptr++, or if you want to make sure
+ * that *ptr stops at the end of a NULL terminated string then
+ * *ptr ? *ptr++ : 0
+ * @param ERROR Expression to be evaluated on invalid input,
+ * typically a goto statement.
+ *
+ * @warning ERROR should not contain a loop control statement which
+ * could interact with the internal while loop, and should force an
+ * exit from the macro code (e.g. through a goto or a return) in order
+ * to prevent undefined results.
+ */
+#define GET_UTF8(val, GET_BYTE, ERROR) \
+ val = (GET_BYTE); \
+ { \
+ uint32_t top = (val & 128) >> 1; \
+ if ((val & 0xc0) == 0x80 || val >= 0xFE) { \
+ ERROR \
+ } \
+ while (val & top) { \
+ unsigned int tmp = (GET_BYTE)-128; \
+ if (tmp >> 6) { \
+ ERROR \
+ } \
+ val = (val << 6) + tmp; \
+ top <<= 5; \
+ } \
+ val &= (top << 1) - 1; \
+ }
+
+/**
+ * Convert a UTF-16 character (2 or 4 bytes) to its 32-bit UCS-4 encoded form.
+ *
+ * @param val Output value, must be an lvalue of type uint32_t.
+ * @param GET_16BIT Expression returning two bytes of UTF-16 data converted
+ * to native byte order. Evaluated one or two times.
+ * @param ERROR Expression to be evaluated on invalid input,
+ * typically a goto statement.
+ */
+#define GET_UTF16(val, GET_16BIT, ERROR) \
+ val = (GET_16BIT); \
+ { \
+ unsigned int hi = val - 0xD800; \
+ if (hi < 0x800) { \
+ val = (GET_16BIT)-0xDC00; \
+ if (val > 0x3FFU || hi > 0x3FFU) { \
+ ERROR \
+ } \
+ val += (hi << 10) + 0x10000; \
+ } \
+ }
+
+/**
+ * @def PUT_UTF8(val, tmp, PUT_BYTE)
+ * Convert a 32-bit Unicode character to its UTF-8 encoded form (up to 4 bytes
+ * long).
+ * @param val is an input-only argument and should be of type uint32_t. It holds
+ * a UCS-4 encoded Unicode character that is to be converted to UTF-8. If
+ * val is given as a function it is executed only once.
+ * @param tmp is a temporary variable and should be of type uint8_t. It
+ * represents an intermediate value during conversion that is to be
+ * output by PUT_BYTE.
+ * @param PUT_BYTE writes the converted UTF-8 bytes to any proper destination.
+ * It could be a function or a statement, and uses tmp as the input byte.
+ * For example, PUT_BYTE could be "*output++ = tmp;" PUT_BYTE will be
+ * executed up to 4 times for values in the valid UTF-8 range and up to
+ * 7 times in the general case, depending on the length of the converted
+ * Unicode character.
+ */
+#define PUT_UTF8(val, tmp, PUT_BYTE) \
+ { \
+ int bytes, shift; \
+ uint32_t in = val; \
+ if (in < 0x80) { \
+ tmp = in; \
+ PUT_BYTE \
+ } else { \
+ bytes = (av_log2(in) + 4) / 5; \
+ shift = (bytes - 1) * 6; \
+ tmp = (256 - (256 >> bytes)) | (in >> shift); \
+ PUT_BYTE \
+ while (shift >= 6) { \
+ shift -= 6; \
+ tmp = 0x80 | ((in >> shift) & 0x3f); \
+ PUT_BYTE \
+ } \
+ } \
+ }
+
+/**
+ * @def PUT_UTF16(val, tmp, PUT_16BIT)
+ * Convert a 32-bit Unicode character to its UTF-16 encoded form (2 or 4 bytes).
+ * @param val is an input-only argument and should be of type uint32_t. It holds
+ * a UCS-4 encoded Unicode character that is to be converted to UTF-16. If
+ * val is given as a function it is executed only once.
+ * @param tmp is a temporary variable and should be of type uint16_t. It
+ * represents an intermediate value during conversion that is to be
+ * output by PUT_16BIT.
+ * @param PUT_16BIT writes the converted UTF-16 data to any proper destination
+ * in desired endianness. It could be a function or a statement, and uses tmp
+ * as the input byte. For example, PUT_BYTE could be "*output++ = tmp;"
+ * PUT_BYTE will be executed 1 or 2 times depending on input character.
+ */
+#define PUT_UTF16(val, tmp, PUT_16BIT) \
+ { \
+ uint32_t in = val; \
+ if (in < 0x10000) { \
+ tmp = in; \
+ PUT_16BIT \
+ } else { \
+ tmp = 0xD800 | ((in - 0x10000) >> 10); \
+ PUT_16BIT \
+ tmp = 0xDC00 | ((in - 0x10000) & 0x3FF); \
+ PUT_16BIT \
+ } \
+ }
+
+#include "mem.h"
+
+#ifdef HAVE_AV_CONFIG_H
+# include "internal.h"
+#endif /* HAVE_AV_CONFIG_H */
+
+#endif /* AVUTIL_COMMON_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/cpu.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/cpu.h
new file mode 100644
index 0000000000..8a48d944eb
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/cpu.h
@@ -0,0 +1,150 @@
+/*
+ * Copyright (c) 2000, 2001, 2002 Fabrice Bellard
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_CPU_H
+#define AVUTIL_CPU_H
+
+#include <stddef.h>
+
+#define AV_CPU_FLAG_FORCE 0x80000000 /* force usage of selected flags (OR) */
+
+/* lower 16 bits - CPU features */
+#define AV_CPU_FLAG_MMX 0x0001 ///< standard MMX
+#define AV_CPU_FLAG_MMXEXT 0x0002 ///< SSE integer functions or AMD MMX ext
+#define AV_CPU_FLAG_MMX2 0x0002 ///< SSE integer functions or AMD MMX ext
+#define AV_CPU_FLAG_3DNOW 0x0004 ///< AMD 3DNOW
+#define AV_CPU_FLAG_SSE 0x0008 ///< SSE functions
+#define AV_CPU_FLAG_SSE2 0x0010 ///< PIV SSE2 functions
+#define AV_CPU_FLAG_SSE2SLOW \
+ 0x40000000 ///< SSE2 supported, but usually not faster
+ ///< than regular MMX/SSE (e.g. Core1)
+#define AV_CPU_FLAG_3DNOWEXT 0x0020 ///< AMD 3DNowExt
+#define AV_CPU_FLAG_SSE3 0x0040 ///< Prescott SSE3 functions
+#define AV_CPU_FLAG_SSE3SLOW \
+ 0x20000000 ///< SSE3 supported, but usually not faster
+ ///< than regular MMX/SSE (e.g. Core1)
+#define AV_CPU_FLAG_SSSE3 0x0080 ///< Conroe SSSE3 functions
+#define AV_CPU_FLAG_SSSE3SLOW \
+ 0x4000000 ///< SSSE3 supported, but usually not faster
+#define AV_CPU_FLAG_ATOM \
+ 0x10000000 ///< Atom processor, some SSSE3 instructions are slower
+#define AV_CPU_FLAG_SSE4 0x0100 ///< Penryn SSE4.1 functions
+#define AV_CPU_FLAG_SSE42 0x0200 ///< Nehalem SSE4.2 functions
+#define AV_CPU_FLAG_AESNI 0x80000 ///< Advanced Encryption Standard functions
+#define AV_CPU_FLAG_AVX \
+ 0x4000 ///< AVX functions: requires OS support even if YMM registers aren't
+ ///< used
+#define AV_CPU_FLAG_AVXSLOW \
+ 0x8000000 ///< AVX supported, but slow when using YMM registers (e.g.
+ ///< Bulldozer)
+#define AV_CPU_FLAG_XOP 0x0400 ///< Bulldozer XOP functions
+#define AV_CPU_FLAG_FMA4 0x0800 ///< Bulldozer FMA4 functions
+#define AV_CPU_FLAG_CMOV 0x1000 ///< supports cmov instruction
+#define AV_CPU_FLAG_AVX2 \
+ 0x8000 ///< AVX2 functions: requires OS support even if YMM registers aren't
+ ///< used
+#define AV_CPU_FLAG_FMA3 0x10000 ///< Haswell FMA3 functions
+#define AV_CPU_FLAG_BMI1 0x20000 ///< Bit Manipulation Instruction Set 1
+#define AV_CPU_FLAG_BMI2 0x40000 ///< Bit Manipulation Instruction Set 2
+#define AV_CPU_FLAG_AVX512 \
+ 0x100000 ///< AVX-512 functions: requires OS support even if YMM/ZMM
+ ///< registers aren't used
+#define AV_CPU_FLAG_AVX512ICL \
+ 0x200000 ///< F/CD/BW/DQ/VL/VNNI/IFMA/VBMI/VBMI2/VPOPCNTDQ/BITALG/GFNI/VAES/VPCLMULQDQ
+#define AV_CPU_FLAG_SLOW_GATHER 0x2000000 ///< CPU has slow gathers.
+
+#define AV_CPU_FLAG_ALTIVEC 0x0001 ///< standard
+#define AV_CPU_FLAG_VSX 0x0002 ///< ISA 2.06
+#define AV_CPU_FLAG_POWER8 0x0004 ///< ISA 2.07
+
+#define AV_CPU_FLAG_ARMV5TE (1 << 0)
+#define AV_CPU_FLAG_ARMV6 (1 << 1)
+#define AV_CPU_FLAG_ARMV6T2 (1 << 2)
+#define AV_CPU_FLAG_VFP (1 << 3)
+#define AV_CPU_FLAG_VFPV3 (1 << 4)
+#define AV_CPU_FLAG_NEON (1 << 5)
+#define AV_CPU_FLAG_ARMV8 (1 << 6)
+#define AV_CPU_FLAG_VFP_VM \
+ (1 << 7) ///< VFPv2 vector mode, deprecated in ARMv7-A and unavailable in
+ ///< various CPUs implementations
+#define AV_CPU_FLAG_SETEND (1 << 16)
+
+#define AV_CPU_FLAG_MMI (1 << 0)
+#define AV_CPU_FLAG_MSA (1 << 1)
+
+// Loongarch SIMD extension.
+#define AV_CPU_FLAG_LSX (1 << 0)
+#define AV_CPU_FLAG_LASX (1 << 1)
+
+// RISC-V extensions
+#define AV_CPU_FLAG_RVI (1 << 0) ///< I (full GPR bank)
+#define AV_CPU_FLAG_RVF (1 << 1) ///< F (single precision FP)
+#define AV_CPU_FLAG_RVD (1 << 2) ///< D (double precision FP)
+#define AV_CPU_FLAG_RVV_I32 (1 << 3) ///< Vectors of 8/16/32-bit int's */
+#define AV_CPU_FLAG_RVV_F32 (1 << 4) ///< Vectors of float's */
+#define AV_CPU_FLAG_RVV_I64 (1 << 5) ///< Vectors of 64-bit int's */
+#define AV_CPU_FLAG_RVV_F64 (1 << 6) ///< Vectors of double's
+#define AV_CPU_FLAG_RVB_BASIC (1 << 7) ///< Basic bit-manipulations
+
+/**
+ * Return the flags which specify extensions supported by the CPU.
+ * The returned value is affected by av_force_cpu_flags() if that was used
+ * before. So av_get_cpu_flags() can easily be used in an application to
+ * detect the enabled cpu flags.
+ */
+int av_get_cpu_flags(void);
+
+/**
+ * Disables cpu detection and forces the specified flags.
+ * -1 is a special case that disables forcing of specific flags.
+ */
+void av_force_cpu_flags(int flags);
+
+/**
+ * Parse CPU caps from a string and update the given AV_CPU_* flags based on
+ * that.
+ *
+ * @return negative on error.
+ */
+int av_parse_cpu_caps(unsigned* flags, const char* s);
+
+/**
+ * @return the number of logical CPU cores present.
+ */
+int av_cpu_count(void);
+
+/**
+ * Overrides cpu count detection and forces the specified count.
+ * Count < 1 disables forcing of specific count.
+ */
+void av_cpu_force_count(int count);
+
+/**
+ * Get the maximum data alignment that may be required by FFmpeg.
+ *
+ * Note that this is affected by the build configuration and the CPU flags mask,
+ * so e.g. if the CPU supports AVX, but libavutil has been built with
+ * --disable-avx or the AV_CPU_FLAG_AVX flag has been disabled through
+ * av_set_cpu_flags_mask(), then this function will behave as if AVX is not
+ * present.
+ */
+size_t av_cpu_max_align(void);
+
+#endif /* AVUTIL_CPU_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/dict.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/dict.h
new file mode 100644
index 0000000000..967a1c8041
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/dict.h
@@ -0,0 +1,259 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * Public dictionary API.
+ * @deprecated
+ * AVDictionary is provided for compatibility with libav. It is both in
+ * implementation as well as API inefficient. It does not scale and is
+ * extremely slow with large dictionaries.
+ * It is recommended that new code uses our tree container from tree.c/h
+ * where applicable, which uses AVL trees to achieve O(log n) performance.
+ */
+
+#ifndef AVUTIL_DICT_H
+#define AVUTIL_DICT_H
+
+#include <stdint.h>
+
+/**
+ * @addtogroup lavu_dict AVDictionary
+ * @ingroup lavu_data
+ *
+ * @brief Simple key:value store
+ *
+ * @{
+ * Dictionaries are used for storing key-value pairs.
+ *
+ * - To **create an AVDictionary**, simply pass an address of a NULL
+ * pointer to av_dict_set(). NULL can be used as an empty dictionary
+ * wherever a pointer to an AVDictionary is required.
+ * - To **insert an entry**, use av_dict_set().
+ * - Use av_dict_get() to **retrieve an entry**.
+ * - To **iterate over all entries**, use av_dict_iterate().
+ * - In order to **free the dictionary and all its contents**, use
+ av_dict_free().
+ *
+ @code
+ AVDictionary *d = NULL; // "create" an empty dictionary
+ AVDictionaryEntry *t = NULL;
+
+ av_dict_set(&d, "foo", "bar", 0); // add an entry
+
+ char *k = av_strdup("key"); // if your strings are already allocated,
+ char *v = av_strdup("value"); // you can avoid copying them like this
+ av_dict_set(&d, k, v, AV_DICT_DONT_STRDUP_KEY | AV_DICT_DONT_STRDUP_VAL);
+
+ while ((t = av_dict_iterate(d, t))) {
+ <....> // iterate over all entries in d
+ }
+ av_dict_free(&d);
+ @endcode
+ */
+
+/**
+ * @name AVDictionary Flags
+ * Flags that influence behavior of the matching of keys or insertion to the
+ * dictionary.
+ * @{
+ */
+#define AV_DICT_MATCH_CASE \
+ 1 /**< Only get an entry with exact-case key match. Only relevant in \
+ av_dict_get(). */
+#define AV_DICT_IGNORE_SUFFIX \
+ 2 /**< Return first entry in a dictionary whose first part corresponds to \
+ the search key, ignoring the suffix of the found key string. Only \
+ relevant in av_dict_get(). */
+#define AV_DICT_DONT_STRDUP_KEY \
+ 4 /**< Take ownership of a key that's been \
+ allocated with av_malloc() or another memory allocation function. */
+#define AV_DICT_DONT_STRDUP_VAL \
+ 8 /**< Take ownership of a value that's been \
+ allocated with av_malloc() or another memory allocation function. */
+#define AV_DICT_DONT_OVERWRITE 16 /**< Don't overwrite existing entries. */
+#define AV_DICT_APPEND \
+ 32 /**< If the entry already exists, append to it. Note that no \
+ delimiter is added, the strings are simply concatenated. */
+#define AV_DICT_MULTIKEY \
+ 64 /**< Allow to store several equal keys in the dictionary */
+/**
+ * @}
+ */
+
+typedef struct AVDictionaryEntry {
+ char* key;
+ char* value;
+} AVDictionaryEntry;
+
+typedef struct AVDictionary AVDictionary;
+
+/**
+ * Get a dictionary entry with matching key.
+ *
+ * The returned entry key or value must not be changed, or it will
+ * cause undefined behavior.
+ *
+ * @param prev Set to the previous matching element to find the next.
+ * If set to NULL the first matching element is returned.
+ * @param key Matching key
+ * @param flags A collection of AV_DICT_* flags controlling how the
+ * entry is retrieved
+ *
+ * @return Found entry or NULL in case no matching entry was found in the
+ * dictionary
+ */
+AVDictionaryEntry* av_dict_get(const AVDictionary* m, const char* key,
+ const AVDictionaryEntry* prev, int flags);
+
+/**
+ * Iterate over a dictionary
+ *
+ * Iterates through all entries in the dictionary.
+ *
+ * @warning The returned AVDictionaryEntry key/value must not be changed.
+ *
+ * @warning As av_dict_set() invalidates all previous entries returned
+ * by this function, it must not be called while iterating over the dict.
+ *
+ * Typical usage:
+ * @code
+ * const AVDictionaryEntry *e = NULL;
+ * while ((e = av_dict_iterate(m, e))) {
+ * // ...
+ * }
+ * @endcode
+ *
+ * @param m The dictionary to iterate over
+ * @param prev Pointer to the previous AVDictionaryEntry, NULL initially
+ *
+ * @retval AVDictionaryEntry* The next element in the dictionary
+ * @retval NULL No more elements in the dictionary
+ */
+const AVDictionaryEntry* av_dict_iterate(const AVDictionary* m,
+ const AVDictionaryEntry* prev);
+
+/**
+ * Get number of entries in dictionary.
+ *
+ * @param m dictionary
+ * @return number of entries in dictionary
+ */
+int av_dict_count(const AVDictionary* m);
+
+/**
+ * Set the given entry in *pm, overwriting an existing entry.
+ *
+ * Note: If AV_DICT_DONT_STRDUP_KEY or AV_DICT_DONT_STRDUP_VAL is set,
+ * these arguments will be freed on error.
+ *
+ * @warning Adding a new entry to a dictionary invalidates all existing entries
+ * previously returned with av_dict_get() or av_dict_iterate().
+ *
+ * @param pm Pointer to a pointer to a dictionary struct. If *pm is NULL
+ * a dictionary struct is allocated and put in *pm.
+ * @param key Entry key to add to *pm (will either be av_strduped or added
+ * as a new key depending on flags)
+ * @param value Entry value to add to *pm (will be av_strduped or added as a
+ * new key depending on flags). Passing a NULL value will cause an existing
+ * entry to be deleted.
+ *
+ * @return >= 0 on success otherwise an error code <0
+ */
+int av_dict_set(AVDictionary** pm, const char* key, const char* value,
+ int flags);
+
+/**
+ * Convenience wrapper for av_dict_set() that converts the value to a string
+ * and stores it.
+ *
+ * Note: If ::AV_DICT_DONT_STRDUP_KEY is set, key will be freed on error.
+ */
+int av_dict_set_int(AVDictionary** pm, const char* key, int64_t value,
+ int flags);
+
+/**
+ * Parse the key/value pairs list and add the parsed entries to a dictionary.
+ *
+ * In case of failure, all the successfully set entries are stored in
+ * *pm. You may need to manually free the created dictionary.
+ *
+ * @param key_val_sep A 0-terminated list of characters used to separate
+ * key from value
+ * @param pairs_sep A 0-terminated list of characters used to separate
+ * two pairs from each other
+ * @param flags Flags to use when adding to the dictionary.
+ * ::AV_DICT_DONT_STRDUP_KEY and ::AV_DICT_DONT_STRDUP_VAL
+ * are ignored since the key/value tokens will always
+ * be duplicated.
+ *
+ * @return 0 on success, negative AVERROR code on failure
+ */
+int av_dict_parse_string(AVDictionary** pm, const char* str,
+ const char* key_val_sep, const char* pairs_sep,
+ int flags);
+
+/**
+ * Copy entries from one AVDictionary struct into another.
+ *
+ * @note Metadata is read using the ::AV_DICT_IGNORE_SUFFIX flag
+ *
+ * @param dst Pointer to a pointer to a AVDictionary struct to copy into. If
+ * *dst is NULL, this function will allocate a struct for you and put it in *dst
+ * @param src Pointer to the source AVDictionary struct to copy items from.
+ * @param flags Flags to use when setting entries in *dst
+ *
+ * @return 0 on success, negative AVERROR code on failure. If dst was allocated
+ * by this function, callers should free the associated memory.
+ */
+int av_dict_copy(AVDictionary** dst, const AVDictionary* src, int flags);
+
+/**
+ * Free all the memory allocated for an AVDictionary struct
+ * and all keys and values.
+ */
+void av_dict_free(AVDictionary** m);
+
+/**
+ * Get dictionary entries as a string.
+ *
+ * Create a string containing dictionary's entries.
+ * Such string may be passed back to av_dict_parse_string().
+ * @note String is escaped with backslashes ('\').
+ *
+ * @warning Separators cannot be neither '\\' nor '\0'. They also cannot be the
+ * same.
+ *
+ * @param[in] m The dictionary
+ * @param[out] buffer Pointer to buffer that will be allocated with
+ * string containg entries. Buffer must be freed by the caller when is no longer
+ * needed.
+ * @param[in] key_val_sep Character used to separate key from value
+ * @param[in] pairs_sep Character used to separate two pairs from each
+ * other
+ *
+ * @return >= 0 on success, negative on error
+ */
+int av_dict_get_string(const AVDictionary* m, char** buffer,
+ const char key_val_sep, const char pairs_sep);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_DICT_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/error.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/error.h
new file mode 100644
index 0000000000..74af5b1534
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/error.h
@@ -0,0 +1,158 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * error code definitions
+ */
+
+#ifndef AVUTIL_ERROR_H
+#define AVUTIL_ERROR_H
+
+#include <errno.h>
+#include <stddef.h>
+
+#include "macros.h"
+
+/**
+ * @addtogroup lavu_error
+ *
+ * @{
+ */
+
+/* error handling */
+#if EDOM > 0
+# define AVERROR(e) \
+ (-(e)) ///< Returns a negative error code from a POSIX error code, to
+ ///< return from library functions.
+# define AVUNERROR(e) \
+ (-(e)) ///< Returns a POSIX error code from a library function error return
+ ///< value.
+#else
+/* Some platforms have E* and errno already negated. */
+# define AVERROR(e) (e)
+# define AVUNERROR(e) (e)
+#endif
+
+#define FFERRTAG(a, b, c, d) (-(int)MKTAG(a, b, c, d))
+
+#define AVERROR_BSF_NOT_FOUND \
+ FFERRTAG(0xF8, 'B', 'S', 'F') ///< Bitstream filter not found
+#define AVERROR_BUG \
+ FFERRTAG('B', 'U', 'G', '!') ///< Internal bug, also see AVERROR_BUG2
+#define AVERROR_BUFFER_TOO_SMALL \
+ FFERRTAG('B', 'U', 'F', 'S') ///< Buffer too small
+#define AVERROR_DECODER_NOT_FOUND \
+ FFERRTAG(0xF8, 'D', 'E', 'C') ///< Decoder not found
+#define AVERROR_DEMUXER_NOT_FOUND \
+ FFERRTAG(0xF8, 'D', 'E', 'M') ///< Demuxer not found
+#define AVERROR_ENCODER_NOT_FOUND \
+ FFERRTAG(0xF8, 'E', 'N', 'C') ///< Encoder not found
+#define AVERROR_EOF FFERRTAG('E', 'O', 'F', ' ') ///< End of file
+#define AVERROR_EXIT \
+ FFERRTAG('E', 'X', 'I', 'T') ///< Immediate exit was requested; the called
+ ///< function should not be restarted
+#define AVERROR_EXTERNAL \
+ FFERRTAG('E', 'X', 'T', ' ') ///< Generic error in an external library
+#define AVERROR_FILTER_NOT_FOUND \
+ FFERRTAG(0xF8, 'F', 'I', 'L') ///< Filter not found
+#define AVERROR_INVALIDDATA \
+ FFERRTAG('I', 'N', 'D', 'A') ///< Invalid data found when processing input
+#define AVERROR_MUXER_NOT_FOUND \
+ FFERRTAG(0xF8, 'M', 'U', 'X') ///< Muxer not found
+#define AVERROR_OPTION_NOT_FOUND \
+ FFERRTAG(0xF8, 'O', 'P', 'T') ///< Option not found
+#define AVERROR_PATCHWELCOME \
+ FFERRTAG('P', 'A', 'W', \
+ 'E') ///< Not yet implemented in FFmpeg, patches welcome
+#define AVERROR_PROTOCOL_NOT_FOUND \
+ FFERRTAG(0xF8, 'P', 'R', 'O') ///< Protocol not found
+
+#define AVERROR_STREAM_NOT_FOUND \
+ FFERRTAG(0xF8, 'S', 'T', 'R') ///< Stream not found
+/**
+ * This is semantically identical to AVERROR_BUG
+ * it has been introduced in Libav after our AVERROR_BUG and with a modified
+ * value.
+ */
+#define AVERROR_BUG2 FFERRTAG('B', 'U', 'G', ' ')
+#define AVERROR_UNKNOWN \
+ FFERRTAG('U', 'N', 'K', \
+ 'N') ///< Unknown error, typically from an external library
+#define AVERROR_EXPERIMENTAL \
+ (-0x2bb2afa8) ///< Requested feature is flagged experimental. Set
+ ///< strict_std_compliance if you really want to use it.
+#define AVERROR_INPUT_CHANGED \
+ (-0x636e6701) ///< Input changed between calls. Reconfiguration is required.
+ ///< (can be OR-ed with AVERROR_OUTPUT_CHANGED)
+#define AVERROR_OUTPUT_CHANGED \
+ (-0x636e6702) ///< Output changed between calls. Reconfiguration is required.
+ ///< (can be OR-ed with AVERROR_INPUT_CHANGED)
+/* HTTP & RTSP errors */
+#define AVERROR_HTTP_BAD_REQUEST FFERRTAG(0xF8, '4', '0', '0')
+#define AVERROR_HTTP_UNAUTHORIZED FFERRTAG(0xF8, '4', '0', '1')
+#define AVERROR_HTTP_FORBIDDEN FFERRTAG(0xF8, '4', '0', '3')
+#define AVERROR_HTTP_NOT_FOUND FFERRTAG(0xF8, '4', '0', '4')
+#define AVERROR_HTTP_OTHER_4XX FFERRTAG(0xF8, '4', 'X', 'X')
+#define AVERROR_HTTP_SERVER_ERROR FFERRTAG(0xF8, '5', 'X', 'X')
+
+#define AV_ERROR_MAX_STRING_SIZE 64
+
+/**
+ * Put a description of the AVERROR code errnum in errbuf.
+ * In case of failure the global variable errno is set to indicate the
+ * error. Even in case of failure av_strerror() will print a generic
+ * error message indicating the errnum provided to errbuf.
+ *
+ * @param errnum error code to describe
+ * @param errbuf buffer to which description is written
+ * @param errbuf_size the size in bytes of errbuf
+ * @return 0 on success, a negative value if a description for errnum
+ * cannot be found
+ */
+int av_strerror(int errnum, char* errbuf, size_t errbuf_size);
+
+/**
+ * Fill the provided buffer with a string containing an error string
+ * corresponding to the AVERROR code errnum.
+ *
+ * @param errbuf a buffer
+ * @param errbuf_size size in bytes of errbuf
+ * @param errnum error code to describe
+ * @return the buffer in input, filled with the error description
+ * @see av_strerror()
+ */
+static inline char* av_make_error_string(char* errbuf, size_t errbuf_size,
+ int errnum) {
+ av_strerror(errnum, errbuf, errbuf_size);
+ return errbuf;
+}
+
+/**
+ * Convenience macro, the return value should be used only directly in
+ * function arguments but never stand-alone.
+ */
+#define av_err2str(errnum) \
+ av_make_error_string((char[AV_ERROR_MAX_STRING_SIZE]){0}, \
+ AV_ERROR_MAX_STRING_SIZE, errnum)
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_ERROR_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/frame.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/frame.h
new file mode 100644
index 0000000000..3b4f1e84bc
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/frame.h
@@ -0,0 +1,960 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * @ingroup lavu_frame
+ * reference-counted frame API
+ */
+
+#ifndef AVUTIL_FRAME_H
+#define AVUTIL_FRAME_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include "avutil.h"
+#include "buffer.h"
+#include "channel_layout.h"
+#include "dict.h"
+#include "rational.h"
+#include "samplefmt.h"
+#include "pixfmt.h"
+#include "version.h"
+
+/**
+ * @defgroup lavu_frame AVFrame
+ * @ingroup lavu_data
+ *
+ * @{
+ * AVFrame is an abstraction for reference-counted raw multimedia data.
+ */
+
+enum AVFrameSideDataType {
+ /**
+ * The data is the AVPanScan struct defined in libavcodec.
+ */
+ AV_FRAME_DATA_PANSCAN,
+ /**
+ * ATSC A53 Part 4 Closed Captions.
+ * A53 CC bitstream is stored as uint8_t in AVFrameSideData.data.
+ * The number of bytes of CC data is AVFrameSideData.size.
+ */
+ AV_FRAME_DATA_A53_CC,
+ /**
+ * Stereoscopic 3d metadata.
+ * The data is the AVStereo3D struct defined in libavutil/stereo3d.h.
+ */
+ AV_FRAME_DATA_STEREO3D,
+ /**
+ * The data is the AVMatrixEncoding enum defined in
+ * libavutil/channel_layout.h.
+ */
+ AV_FRAME_DATA_MATRIXENCODING,
+ /**
+ * Metadata relevant to a downmix procedure.
+ * The data is the AVDownmixInfo struct defined in libavutil/downmix_info.h.
+ */
+ AV_FRAME_DATA_DOWNMIX_INFO,
+ /**
+ * ReplayGain information in the form of the AVReplayGain struct.
+ */
+ AV_FRAME_DATA_REPLAYGAIN,
+ /**
+ * This side data contains a 3x3 transformation matrix describing an affine
+ * transformation that needs to be applied to the frame for correct
+ * presentation.
+ *
+ * See libavutil/display.h for a detailed description of the data.
+ */
+ AV_FRAME_DATA_DISPLAYMATRIX,
+ /**
+ * Active Format Description data consisting of a single byte as specified
+ * in ETSI TS 101 154 using AVActiveFormatDescription enum.
+ */
+ AV_FRAME_DATA_AFD,
+ /**
+ * Motion vectors exported by some codecs (on demand through the export_mvs
+ * flag set in the libavcodec AVCodecContext flags2 option).
+ * The data is the AVMotionVector struct defined in
+ * libavutil/motion_vector.h.
+ */
+ AV_FRAME_DATA_MOTION_VECTORS,
+ /**
+ * Recommmends skipping the specified number of samples. This is exported
+ * only if the "skip_manual" AVOption is set in libavcodec.
+ * This has the same format as AV_PKT_DATA_SKIP_SAMPLES.
+ * @code
+ * u32le number of samples to skip from start of this packet
+ * u32le number of samples to skip from end of this packet
+ * u8 reason for start skip
+ * u8 reason for end skip (0=padding silence, 1=convergence)
+ * @endcode
+ */
+ AV_FRAME_DATA_SKIP_SAMPLES,
+ /**
+ * This side data must be associated with an audio frame and corresponds to
+ * enum AVAudioServiceType defined in avcodec.h.
+ */
+ AV_FRAME_DATA_AUDIO_SERVICE_TYPE,
+ /**
+ * Mastering display metadata associated with a video frame. The payload is
+ * an AVMasteringDisplayMetadata type and contains information about the
+ * mastering display color volume.
+ */
+ AV_FRAME_DATA_MASTERING_DISPLAY_METADATA,
+ /**
+ * The GOP timecode in 25 bit timecode format. Data format is 64-bit integer.
+ * This is set on the first frame of a GOP that has a temporal reference of 0.
+ */
+ AV_FRAME_DATA_GOP_TIMECODE,
+
+ /**
+ * The data represents the AVSphericalMapping structure defined in
+ * libavutil/spherical.h.
+ */
+ AV_FRAME_DATA_SPHERICAL,
+
+ /**
+ * Content light level (based on CTA-861.3). This payload contains data in
+ * the form of the AVContentLightMetadata struct.
+ */
+ AV_FRAME_DATA_CONTENT_LIGHT_LEVEL,
+
+ /**
+ * The data contains an ICC profile as an opaque octet buffer following the
+ * format described by ISO 15076-1 with an optional name defined in the
+ * metadata key entry "name".
+ */
+ AV_FRAME_DATA_ICC_PROFILE,
+
+ /**
+ * Timecode which conforms to SMPTE ST 12-1. The data is an array of 4
+ * uint32_t where the first uint32_t describes how many (1-3) of the other
+ * timecodes are used. The timecode format is described in the documentation
+ * of av_timecode_get_smpte_from_framenum() function in libavutil/timecode.h.
+ */
+ AV_FRAME_DATA_S12M_TIMECODE,
+
+ /**
+ * HDR dynamic metadata associated with a video frame. The payload is
+ * an AVDynamicHDRPlus type and contains information for color
+ * volume transform - application 4 of SMPTE 2094-40:2016 standard.
+ */
+ AV_FRAME_DATA_DYNAMIC_HDR_PLUS,
+
+ /**
+ * Regions Of Interest, the data is an array of AVRegionOfInterest type, the
+ * number of array element is implied by AVFrameSideData.size /
+ * AVRegionOfInterest.self_size.
+ */
+ AV_FRAME_DATA_REGIONS_OF_INTEREST,
+
+ /**
+ * Encoding parameters for a video frame, as described by AVVideoEncParams.
+ */
+ AV_FRAME_DATA_VIDEO_ENC_PARAMS,
+
+ /**
+ * User data unregistered metadata associated with a video frame.
+ * This is the H.26[45] UDU SEI message, and shouldn't be used for any other
+ * purpose The data is stored as uint8_t in AVFrameSideData.data which is 16
+ * bytes of uuid_iso_iec_11578 followed by AVFrameSideData.size - 16 bytes of
+ * user_data_payload_byte.
+ */
+ AV_FRAME_DATA_SEI_UNREGISTERED,
+
+ /**
+ * Film grain parameters for a frame, described by AVFilmGrainParams.
+ * Must be present for every frame which should have film grain applied.
+ */
+ AV_FRAME_DATA_FILM_GRAIN_PARAMS,
+
+ /**
+ * Bounding boxes for object detection and classification,
+ * as described by AVDetectionBBoxHeader.
+ */
+ AV_FRAME_DATA_DETECTION_BBOXES,
+
+ /**
+ * Dolby Vision RPU raw data, suitable for passing to x265
+ * or other libraries. Array of uint8_t, with NAL emulation
+ * bytes intact.
+ */
+ AV_FRAME_DATA_DOVI_RPU_BUFFER,
+
+ /**
+ * Parsed Dolby Vision metadata, suitable for passing to a software
+ * implementation. The payload is the AVDOVIMetadata struct defined in
+ * libavutil/dovi_meta.h.
+ */
+ AV_FRAME_DATA_DOVI_METADATA,
+
+ /**
+ * HDR Vivid dynamic metadata associated with a video frame. The payload is
+ * an AVDynamicHDRVivid type and contains information for color
+ * volume transform - CUVA 005.1-2021.
+ */
+ AV_FRAME_DATA_DYNAMIC_HDR_VIVID,
+
+ /**
+ * Ambient viewing environment metadata, as defined by H.274.
+ */
+ AV_FRAME_DATA_AMBIENT_VIEWING_ENVIRONMENT,
+};
+
+enum AVActiveFormatDescription {
+ AV_AFD_SAME = 8,
+ AV_AFD_4_3 = 9,
+ AV_AFD_16_9 = 10,
+ AV_AFD_14_9 = 11,
+ AV_AFD_4_3_SP_14_9 = 13,
+ AV_AFD_16_9_SP_14_9 = 14,
+ AV_AFD_SP_4_3 = 15,
+};
+
+/**
+ * Structure to hold side data for an AVFrame.
+ *
+ * sizeof(AVFrameSideData) is not a part of the public ABI, so new fields may be
+ * added to the end with a minor bump.
+ */
+typedef struct AVFrameSideData {
+ enum AVFrameSideDataType type;
+ uint8_t* data;
+ size_t size;
+ AVDictionary* metadata;
+ AVBufferRef* buf;
+} AVFrameSideData;
+
+/**
+ * Structure describing a single Region Of Interest.
+ *
+ * When multiple regions are defined in a single side-data block, they
+ * should be ordered from most to least important - some encoders are only
+ * capable of supporting a limited number of distinct regions, so will have
+ * to truncate the list.
+ *
+ * When overlapping regions are defined, the first region containing a given
+ * area of the frame applies.
+ */
+typedef struct AVRegionOfInterest {
+ /**
+ * Must be set to the size of this data structure (that is,
+ * sizeof(AVRegionOfInterest)).
+ */
+ uint32_t self_size;
+ /**
+ * Distance in pixels from the top edge of the frame to the top and
+ * bottom edges and from the left edge of the frame to the left and
+ * right edges of the rectangle defining this region of interest.
+ *
+ * The constraints on a region are encoder dependent, so the region
+ * actually affected may be slightly larger for alignment or other
+ * reasons.
+ */
+ int top;
+ int bottom;
+ int left;
+ int right;
+ /**
+ * Quantisation offset.
+ *
+ * Must be in the range -1 to +1. A value of zero indicates no quality
+ * change. A negative value asks for better quality (less quantisation),
+ * while a positive value asks for worse quality (greater quantisation).
+ *
+ * The range is calibrated so that the extreme values indicate the
+ * largest possible offset - if the rest of the frame is encoded with the
+ * worst possible quality, an offset of -1 indicates that this region
+ * should be encoded with the best possible quality anyway. Intermediate
+ * values are then interpolated in some codec-dependent way.
+ *
+ * For example, in 10-bit H.264 the quantisation parameter varies between
+ * -12 and 51. A typical qoffset value of -1/10 therefore indicates that
+ * this region should be encoded with a QP around one-tenth of the full
+ * range better than the rest of the frame. So, if most of the frame
+ * were to be encoded with a QP of around 30, this region would get a QP
+ * of around 24 (an offset of approximately -1/10 * (51 - -12) = -6.3).
+ * An extreme value of -1 would indicate that this region should be
+ * encoded with the best possible quality regardless of the treatment of
+ * the rest of the frame - that is, should be encoded at a QP of -12.
+ */
+ AVRational qoffset;
+} AVRegionOfInterest;
+
+/**
+ * This structure describes decoded (raw) audio or video data.
+ *
+ * AVFrame must be allocated using av_frame_alloc(). Note that this only
+ * allocates the AVFrame itself, the buffers for the data must be managed
+ * through other means (see below).
+ * AVFrame must be freed with av_frame_free().
+ *
+ * AVFrame is typically allocated once and then reused multiple times to hold
+ * different data (e.g. a single AVFrame to hold frames received from a
+ * decoder). In such a case, av_frame_unref() will free any references held by
+ * the frame and reset it to its original clean state before it
+ * is reused again.
+ *
+ * The data described by an AVFrame is usually reference counted through the
+ * AVBuffer API. The underlying buffer references are stored in AVFrame.buf /
+ * AVFrame.extended_buf. An AVFrame is considered to be reference counted if at
+ * least one reference is set, i.e. if AVFrame.buf[0] != NULL. In such a case,
+ * every single data plane must be contained in one of the buffers in
+ * AVFrame.buf or AVFrame.extended_buf.
+ * There may be a single buffer for all the data, or one separate buffer for
+ * each plane, or anything in between.
+ *
+ * sizeof(AVFrame) is not a part of the public ABI, so new fields may be added
+ * to the end with a minor bump.
+ *
+ * Fields can be accessed through AVOptions, the name string used, matches the
+ * C structure field name for fields accessible through AVOptions. The AVClass
+ * for AVFrame can be obtained from avcodec_get_frame_class()
+ */
+typedef struct AVFrame {
+#define AV_NUM_DATA_POINTERS 8
+ /**
+ * pointer to the picture/channel planes.
+ * This might be different from the first allocated byte. For video,
+ * it could even point to the end of the image data.
+ *
+ * All pointers in data and extended_data must point into one of the
+ * AVBufferRef in buf or extended_buf.
+ *
+ * Some decoders access areas outside 0,0 - width,height, please
+ * see avcodec_align_dimensions2(). Some filters and swscale can read
+ * up to 16 bytes beyond the planes, if these filters are to be used,
+ * then 16 extra bytes must be allocated.
+ *
+ * NOTE: Pointers not needed by the format MUST be set to NULL.
+ *
+ * @attention In case of video, the data[] pointers can point to the
+ * end of image data in order to reverse line order, when used in
+ * combination with negative values in the linesize[] array.
+ */
+ uint8_t* data[AV_NUM_DATA_POINTERS];
+
+ /**
+ * For video, a positive or negative value, which is typically indicating
+ * the size in bytes of each picture line, but it can also be:
+ * - the negative byte size of lines for vertical flipping
+ * (with data[n] pointing to the end of the data
+ * - a positive or negative multiple of the byte size as for accessing
+ * even and odd fields of a frame (possibly flipped)
+ *
+ * For audio, only linesize[0] may be set. For planar audio, each channel
+ * plane must be the same size.
+ *
+ * For video the linesizes should be multiples of the CPUs alignment
+ * preference, this is 16 or 32 for modern desktop CPUs.
+ * Some code requires such alignment other code can be slower without
+ * correct alignment, for yet other it makes no difference.
+ *
+ * @note The linesize may be larger than the size of usable data -- there
+ * may be extra padding present for performance reasons.
+ *
+ * @attention In case of video, line size values can be negative to achieve
+ * a vertically inverted iteration over image lines.
+ */
+ int linesize[AV_NUM_DATA_POINTERS];
+
+ /**
+ * pointers to the data planes/channels.
+ *
+ * For video, this should simply point to data[].
+ *
+ * For planar audio, each channel has a separate data pointer, and
+ * linesize[0] contains the size of each channel buffer.
+ * For packed audio, there is just one data pointer, and linesize[0]
+ * contains the total size of the buffer for all channels.
+ *
+ * Note: Both data and extended_data should always be set in a valid frame,
+ * but for planar audio with more channels that can fit in data,
+ * extended_data must be used in order to access all channels.
+ */
+ uint8_t** extended_data;
+
+ /**
+ * @name Video dimensions
+ * Video frames only. The coded dimensions (in pixels) of the video frame,
+ * i.e. the size of the rectangle that contains some well-defined values.
+ *
+ * @note The part of the frame intended for display/presentation is further
+ * restricted by the @ref cropping "Cropping rectangle".
+ * @{
+ */
+ int width, height;
+ /**
+ * @}
+ */
+
+ /**
+ * number of audio samples (per channel) described by this frame
+ */
+ int nb_samples;
+
+ /**
+ * format of the frame, -1 if unknown or unset
+ * Values correspond to enum AVPixelFormat for video frames,
+ * enum AVSampleFormat for audio)
+ */
+ int format;
+
+ /**
+ * 1 -> keyframe, 0-> not
+ */
+ int key_frame;
+
+ /**
+ * Picture type of the frame.
+ */
+ enum AVPictureType pict_type;
+
+ /**
+ * Sample aspect ratio for the video frame, 0/1 if unknown/unspecified.
+ */
+ AVRational sample_aspect_ratio;
+
+ /**
+ * Presentation timestamp in time_base units (time when frame should be shown
+ * to user).
+ */
+ int64_t pts;
+
+ /**
+ * DTS copied from the AVPacket that triggered returning this frame. (if frame
+ * threading isn't used) This is also the Presentation time of this AVFrame
+ * calculated from only AVPacket.dts values without pts values.
+ */
+ int64_t pkt_dts;
+
+ /**
+ * Time base for the timestamps in this frame.
+ * In the future, this field may be set on frames output by decoders or
+ * filters, but its value will be by default ignored on input to encoders
+ * or filters.
+ */
+ AVRational time_base;
+
+#if FF_API_FRAME_PICTURE_NUMBER
+ /**
+ * picture number in bitstream order
+ */
+ attribute_deprecated int coded_picture_number;
+ /**
+ * picture number in display order
+ */
+ attribute_deprecated int display_picture_number;
+#endif
+
+ /**
+ * quality (between 1 (good) and FF_LAMBDA_MAX (bad))
+ */
+ int quality;
+
+ /**
+ * for some private data of the user
+ */
+ void* opaque;
+
+ /**
+ * When decoding, this signals how much the picture must be delayed.
+ * extra_delay = repeat_pict / (2*fps)
+ */
+ int repeat_pict;
+
+ /**
+ * The content of the picture is interlaced.
+ */
+ int interlaced_frame;
+
+ /**
+ * If the content is interlaced, is top field displayed first.
+ */
+ int top_field_first;
+
+ /**
+ * Tell user application that palette has changed from previous frame.
+ */
+ int palette_has_changed;
+
+#if FF_API_REORDERED_OPAQUE
+ /**
+ * reordered opaque 64 bits (generally an integer or a double precision float
+ * PTS but can be anything).
+ * The user sets AVCodecContext.reordered_opaque to represent the input at
+ * that time,
+ * the decoder reorders values as needed and sets AVFrame.reordered_opaque
+ * to exactly one of the values provided by the user through
+ * AVCodecContext.reordered_opaque
+ *
+ * @deprecated Use AV_CODEC_FLAG_COPY_OPAQUE instead
+ */
+ attribute_deprecated int64_t reordered_opaque;
+#endif
+
+ /**
+ * Sample rate of the audio data.
+ */
+ int sample_rate;
+
+#if FF_API_OLD_CHANNEL_LAYOUT
+ /**
+ * Channel layout of the audio data.
+ * @deprecated use ch_layout instead
+ */
+ attribute_deprecated uint64_t channel_layout;
+#endif
+
+ /**
+ * AVBuffer references backing the data for this frame. All the pointers in
+ * data and extended_data must point inside one of the buffers in buf or
+ * extended_buf. This array must be filled contiguously -- if buf[i] is
+ * non-NULL then buf[j] must also be non-NULL for all j < i.
+ *
+ * There may be at most one AVBuffer per data plane, so for video this array
+ * always contains all the references. For planar audio with more than
+ * AV_NUM_DATA_POINTERS channels, there may be more buffers than can fit in
+ * this array. Then the extra AVBufferRef pointers are stored in the
+ * extended_buf array.
+ */
+ AVBufferRef* buf[AV_NUM_DATA_POINTERS];
+
+ /**
+ * For planar audio which requires more than AV_NUM_DATA_POINTERS
+ * AVBufferRef pointers, this array will hold all the references which
+ * cannot fit into AVFrame.buf.
+ *
+ * Note that this is different from AVFrame.extended_data, which always
+ * contains all the pointers. This array only contains the extra pointers,
+ * which cannot fit into AVFrame.buf.
+ *
+ * This array is always allocated using av_malloc() by whoever constructs
+ * the frame. It is freed in av_frame_unref().
+ */
+ AVBufferRef** extended_buf;
+ /**
+ * Number of elements in extended_buf.
+ */
+ int nb_extended_buf;
+
+ AVFrameSideData** side_data;
+ int nb_side_data;
+
+/**
+ * @defgroup lavu_frame_flags AV_FRAME_FLAGS
+ * @ingroup lavu_frame
+ * Flags describing additional frame properties.
+ *
+ * @{
+ */
+
+/**
+ * The frame data may be corrupted, e.g. due to decoding errors.
+ */
+#define AV_FRAME_FLAG_CORRUPT (1 << 0)
+/**
+ * A flag to mark the frames which need to be decoded, but shouldn't be output.
+ */
+#define AV_FRAME_FLAG_DISCARD (1 << 2)
+ /**
+ * @}
+ */
+
+ /**
+ * Frame flags, a combination of @ref lavu_frame_flags
+ */
+ int flags;
+
+ /**
+ * MPEG vs JPEG YUV range.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorRange color_range;
+
+ enum AVColorPrimaries color_primaries;
+
+ enum AVColorTransferCharacteristic color_trc;
+
+ /**
+ * YUV colorspace type.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorSpace colorspace;
+
+ enum AVChromaLocation chroma_location;
+
+ /**
+ * frame timestamp estimated using various heuristics, in stream time base
+ * - encoding: unused
+ * - decoding: set by libavcodec, read by user.
+ */
+ int64_t best_effort_timestamp;
+
+ /**
+ * reordered pos from the last AVPacket that has been input into the decoder
+ * - encoding: unused
+ * - decoding: Read by user.
+ */
+ int64_t pkt_pos;
+
+#if FF_API_PKT_DURATION
+ /**
+ * duration of the corresponding packet, expressed in
+ * AVStream->time_base units, 0 if unknown.
+ * - encoding: unused
+ * - decoding: Read by user.
+ *
+ * @deprecated use duration instead
+ */
+ attribute_deprecated int64_t pkt_duration;
+#endif
+
+ /**
+ * metadata.
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ AVDictionary* metadata;
+
+ /**
+ * decode error flags of the frame, set to a combination of
+ * FF_DECODE_ERROR_xxx flags if the decoder produced a frame, but there
+ * were errors during the decoding.
+ * - encoding: unused
+ * - decoding: set by libavcodec, read by user.
+ */
+ int decode_error_flags;
+#define FF_DECODE_ERROR_INVALID_BITSTREAM 1
+#define FF_DECODE_ERROR_MISSING_REFERENCE 2
+#define FF_DECODE_ERROR_CONCEALMENT_ACTIVE 4
+#define FF_DECODE_ERROR_DECODE_SLICES 8
+
+#if FF_API_OLD_CHANNEL_LAYOUT
+ /**
+ * number of audio channels, only used for audio.
+ * - encoding: unused
+ * - decoding: Read by user.
+ * @deprecated use ch_layout instead
+ */
+ attribute_deprecated int channels;
+#endif
+
+ /**
+ * size of the corresponding packet containing the compressed
+ * frame.
+ * It is set to a negative value if unknown.
+ * - encoding: unused
+ * - decoding: set by libavcodec, read by user.
+ */
+ int pkt_size;
+
+ /**
+ * For hwaccel-format frames, this should be a reference to the
+ * AVHWFramesContext describing the frame.
+ */
+ AVBufferRef* hw_frames_ctx;
+
+ /**
+ * AVBufferRef for free use by the API user. FFmpeg will never check the
+ * contents of the buffer ref. FFmpeg calls av_buffer_unref() on it when
+ * the frame is unreferenced. av_frame_copy_props() calls create a new
+ * reference with av_buffer_ref() for the target frame's opaque_ref field.
+ *
+ * This is unrelated to the opaque field, although it serves a similar
+ * purpose.
+ */
+ AVBufferRef* opaque_ref;
+
+ /**
+ * @anchor cropping
+ * @name Cropping
+ * Video frames only. The number of pixels to discard from the the
+ * top/bottom/left/right border of the frame to obtain the sub-rectangle of
+ * the frame intended for presentation.
+ * @{
+ */
+ size_t crop_top;
+ size_t crop_bottom;
+ size_t crop_left;
+ size_t crop_right;
+ /**
+ * @}
+ */
+
+ /**
+ * AVBufferRef for internal use by a single libav* library.
+ * Must not be used to transfer data between libraries.
+ * Has to be NULL when ownership of the frame leaves the respective library.
+ *
+ * Code outside the FFmpeg libs should never check or change the contents of
+ * the buffer ref.
+ *
+ * FFmpeg calls av_buffer_unref() on it when the frame is unreferenced.
+ * av_frame_copy_props() calls create a new reference with av_buffer_ref()
+ * for the target frame's private_ref field.
+ */
+ AVBufferRef* private_ref;
+
+ /**
+ * Channel layout of the audio data.
+ */
+ AVChannelLayout ch_layout;
+
+ /**
+ * Duration of the frame, in the same units as pts. 0 if unknown.
+ */
+ int64_t duration;
+} AVFrame;
+
+/**
+ * Allocate an AVFrame and set its fields to default values. The resulting
+ * struct must be freed using av_frame_free().
+ *
+ * @return An AVFrame filled with default values or NULL on failure.
+ *
+ * @note this only allocates the AVFrame itself, not the data buffers. Those
+ * must be allocated through other means, e.g. with av_frame_get_buffer() or
+ * manually.
+ */
+AVFrame* av_frame_alloc(void);
+
+/**
+ * Free the frame and any dynamically allocated objects in it,
+ * e.g. extended_data. If the frame is reference counted, it will be
+ * unreferenced first.
+ *
+ * @param frame frame to be freed. The pointer will be set to NULL.
+ */
+void av_frame_free(AVFrame** frame);
+
+/**
+ * Set up a new reference to the data described by the source frame.
+ *
+ * Copy frame properties from src to dst and create a new reference for each
+ * AVBufferRef from src.
+ *
+ * If src is not reference counted, new buffers are allocated and the data is
+ * copied.
+ *
+ * @warning: dst MUST have been either unreferenced with av_frame_unref(dst),
+ * or newly allocated with av_frame_alloc() before calling this
+ * function, or undefined behavior will occur.
+ *
+ * @return 0 on success, a negative AVERROR on error
+ */
+int av_frame_ref(AVFrame* dst, const AVFrame* src);
+
+/**
+ * Create a new frame that references the same data as src.
+ *
+ * This is a shortcut for av_frame_alloc()+av_frame_ref().
+ *
+ * @return newly created AVFrame on success, NULL on error.
+ */
+AVFrame* av_frame_clone(const AVFrame* src);
+
+/**
+ * Unreference all the buffers referenced by frame and reset the frame fields.
+ */
+void av_frame_unref(AVFrame* frame);
+
+/**
+ * Move everything contained in src to dst and reset src.
+ *
+ * @warning: dst is not unreferenced, but directly overwritten without reading
+ * or deallocating its contents. Call av_frame_unref(dst) manually
+ * before calling this function to ensure that no memory is leaked.
+ */
+void av_frame_move_ref(AVFrame* dst, AVFrame* src);
+
+/**
+ * Allocate new buffer(s) for audio or video data.
+ *
+ * The following fields must be set on frame before calling this function:
+ * - format (pixel format for video, sample format for audio)
+ * - width and height for video
+ * - nb_samples and ch_layout for audio
+ *
+ * This function will fill AVFrame.data and AVFrame.buf arrays and, if
+ * necessary, allocate and fill AVFrame.extended_data and AVFrame.extended_buf.
+ * For planar formats, one buffer will be allocated for each plane.
+ *
+ * @warning: if frame already has been allocated, calling this function will
+ * leak memory. In addition, undefined behavior can occur in certain
+ * cases.
+ *
+ * @param frame frame in which to store the new buffers.
+ * @param align Required buffer size alignment. If equal to 0, alignment will be
+ * chosen automatically for the current CPU. It is highly
+ * recommended to pass 0 here unless you know what you are doing.
+ *
+ * @return 0 on success, a negative AVERROR on error.
+ */
+int av_frame_get_buffer(AVFrame* frame, int align);
+
+/**
+ * Check if the frame data is writable.
+ *
+ * @return A positive value if the frame data is writable (which is true if and
+ * only if each of the underlying buffers has only one reference, namely the one
+ * stored in this frame). Return 0 otherwise.
+ *
+ * If 1 is returned the answer is valid until av_buffer_ref() is called on any
+ * of the underlying AVBufferRefs (e.g. through av_frame_ref() or directly).
+ *
+ * @see av_frame_make_writable(), av_buffer_is_writable()
+ */
+int av_frame_is_writable(AVFrame* frame);
+
+/**
+ * Ensure that the frame data is writable, avoiding data copy if possible.
+ *
+ * Do nothing if the frame is writable, allocate new buffers and copy the data
+ * if it is not. Non-refcounted frames behave as non-writable, i.e. a copy
+ * is always made.
+ *
+ * @return 0 on success, a negative AVERROR on error.
+ *
+ * @see av_frame_is_writable(), av_buffer_is_writable(),
+ * av_buffer_make_writable()
+ */
+int av_frame_make_writable(AVFrame* frame);
+
+/**
+ * Copy the frame data from src to dst.
+ *
+ * This function does not allocate anything, dst must be already initialized and
+ * allocated with the same parameters as src.
+ *
+ * This function only copies the frame data (i.e. the contents of the data /
+ * extended data arrays), not any other properties.
+ *
+ * @return >= 0 on success, a negative AVERROR on error.
+ */
+int av_frame_copy(AVFrame* dst, const AVFrame* src);
+
+/**
+ * Copy only "metadata" fields from src to dst.
+ *
+ * Metadata for the purpose of this function are those fields that do not affect
+ * the data layout in the buffers. E.g. pts, sample rate (for audio) or sample
+ * aspect ratio (for video), but not width/height or channel layout.
+ * Side data is also copied.
+ */
+int av_frame_copy_props(AVFrame* dst, const AVFrame* src);
+
+/**
+ * Get the buffer reference a given data plane is stored in.
+ *
+ * @param frame the frame to get the plane's buffer from
+ * @param plane index of the data plane of interest in frame->extended_data.
+ *
+ * @return the buffer reference that contains the plane or NULL if the input
+ * frame is not valid.
+ */
+AVBufferRef* av_frame_get_plane_buffer(AVFrame* frame, int plane);
+
+/**
+ * Add a new side data to a frame.
+ *
+ * @param frame a frame to which the side data should be added
+ * @param type type of the added side data
+ * @param size size of the side data
+ *
+ * @return newly added side data on success, NULL on error
+ */
+AVFrameSideData* av_frame_new_side_data(AVFrame* frame,
+ enum AVFrameSideDataType type,
+ size_t size);
+
+/**
+ * Add a new side data to a frame from an existing AVBufferRef
+ *
+ * @param frame a frame to which the side data should be added
+ * @param type the type of the added side data
+ * @param buf an AVBufferRef to add as side data. The ownership of
+ * the reference is transferred to the frame.
+ *
+ * @return newly added side data on success, NULL on error. On failure
+ * the frame is unchanged and the AVBufferRef remains owned by
+ * the caller.
+ */
+AVFrameSideData* av_frame_new_side_data_from_buf(AVFrame* frame,
+ enum AVFrameSideDataType type,
+ AVBufferRef* buf);
+
+/**
+ * @return a pointer to the side data of a given type on success, NULL if there
+ * is no side data with such type in this frame.
+ */
+AVFrameSideData* av_frame_get_side_data(const AVFrame* frame,
+ enum AVFrameSideDataType type);
+
+/**
+ * Remove and free all side data instances of the given type.
+ */
+void av_frame_remove_side_data(AVFrame* frame, enum AVFrameSideDataType type);
+
+/**
+ * Flags for frame cropping.
+ */
+enum {
+ /**
+ * Apply the maximum possible cropping, even if it requires setting the
+ * AVFrame.data[] entries to unaligned pointers. Passing unaligned data
+ * to FFmpeg API is generally not allowed, and causes undefined behavior
+ * (such as crashes). You can pass unaligned data only to FFmpeg APIs that
+ * are explicitly documented to accept it. Use this flag only if you
+ * absolutely know what you are doing.
+ */
+ AV_FRAME_CROP_UNALIGNED = 1 << 0,
+};
+
+/**
+ * Crop the given video AVFrame according to its crop_left/crop_top/crop_right/
+ * crop_bottom fields. If cropping is successful, the function will adjust the
+ * data pointers and the width/height fields, and set the crop fields to 0.
+ *
+ * In all cases, the cropping boundaries will be rounded to the inherent
+ * alignment of the pixel format. In some cases, such as for opaque hwaccel
+ * formats, the left/top cropping is ignored. The crop fields are set to 0 even
+ * if the cropping was rounded or ignored.
+ *
+ * @param frame the frame which should be cropped
+ * @param flags Some combination of AV_FRAME_CROP_* flags, or 0.
+ *
+ * @return >= 0 on success, a negative AVERROR on error. If the cropping fields
+ * were invalid, AVERROR(ERANGE) is returned, and nothing is changed.
+ */
+int av_frame_apply_cropping(AVFrame* frame, int flags);
+
+/**
+ * @return a string identifying the side data type
+ */
+const char* av_frame_side_data_name(enum AVFrameSideDataType type);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_FRAME_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/hwcontext.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/hwcontext.h
new file mode 100644
index 0000000000..fb376ce499
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/hwcontext.h
@@ -0,0 +1,606 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_HWCONTEXT_H
+#define AVUTIL_HWCONTEXT_H
+
+#include "buffer.h"
+#include "frame.h"
+#include "log.h"
+#include "pixfmt.h"
+
+enum AVHWDeviceType {
+ AV_HWDEVICE_TYPE_NONE,
+ AV_HWDEVICE_TYPE_VDPAU,
+ AV_HWDEVICE_TYPE_CUDA,
+ AV_HWDEVICE_TYPE_VAAPI,
+ AV_HWDEVICE_TYPE_DXVA2,
+ AV_HWDEVICE_TYPE_QSV,
+ AV_HWDEVICE_TYPE_VIDEOTOOLBOX,
+ AV_HWDEVICE_TYPE_D3D11VA,
+ AV_HWDEVICE_TYPE_DRM,
+ AV_HWDEVICE_TYPE_OPENCL,
+ AV_HWDEVICE_TYPE_MEDIACODEC,
+ AV_HWDEVICE_TYPE_VULKAN,
+};
+
+typedef struct AVHWDeviceInternal AVHWDeviceInternal;
+
+/**
+ * This struct aggregates all the (hardware/vendor-specific) "high-level" state,
+ * i.e. state that is not tied to a concrete processing configuration.
+ * E.g., in an API that supports hardware-accelerated encoding and decoding,
+ * this struct will (if possible) wrap the state that is common to both encoding
+ * and decoding and from which specific instances of encoders or decoders can be
+ * derived.
+ *
+ * This struct is reference-counted with the AVBuffer mechanism. The
+ * av_hwdevice_ctx_alloc() constructor yields a reference, whose data field
+ * points to the actual AVHWDeviceContext. Further objects derived from
+ * AVHWDeviceContext (such as AVHWFramesContext, describing a frame pool with
+ * specific properties) will hold an internal reference to it. After all the
+ * references are released, the AVHWDeviceContext itself will be freed,
+ * optionally invoking a user-specified callback for uninitializing the hardware
+ * state.
+ */
+typedef struct AVHWDeviceContext {
+ /**
+ * A class for logging. Set by av_hwdevice_ctx_alloc().
+ */
+ const AVClass* av_class;
+
+ /**
+ * Private data used internally by libavutil. Must not be accessed in any
+ * way by the caller.
+ */
+ AVHWDeviceInternal* internal;
+
+ /**
+ * This field identifies the underlying API used for hardware access.
+ *
+ * This field is set when this struct is allocated and never changed
+ * afterwards.
+ */
+ enum AVHWDeviceType type;
+
+ /**
+ * The format-specific data, allocated and freed by libavutil along with
+ * this context.
+ *
+ * Should be cast by the user to the format-specific context defined in the
+ * corresponding header (hwcontext_*.h) and filled as described in the
+ * documentation before calling av_hwdevice_ctx_init().
+ *
+ * After calling av_hwdevice_ctx_init() this struct should not be modified
+ * by the caller.
+ */
+ void* hwctx;
+
+ /**
+ * This field may be set by the caller before calling av_hwdevice_ctx_init().
+ *
+ * If non-NULL, this callback will be called when the last reference to
+ * this context is unreferenced, immediately before it is freed.
+ *
+ * @note when other objects (e.g an AVHWFramesContext) are derived from this
+ * struct, this callback will be invoked after all such child objects
+ * are fully uninitialized and their respective destructors invoked.
+ */
+ void (*free)(struct AVHWDeviceContext* ctx);
+
+ /**
+ * Arbitrary user data, to be used e.g. by the free() callback.
+ */
+ void* user_opaque;
+} AVHWDeviceContext;
+
+typedef struct AVHWFramesInternal AVHWFramesInternal;
+
+/**
+ * This struct describes a set or pool of "hardware" frames (i.e. those with
+ * data not located in normal system memory). All the frames in the pool are
+ * assumed to be allocated in the same way and interchangeable.
+ *
+ * This struct is reference-counted with the AVBuffer mechanism and tied to a
+ * given AVHWDeviceContext instance. The av_hwframe_ctx_alloc() constructor
+ * yields a reference, whose data field points to the actual AVHWFramesContext
+ * struct.
+ */
+typedef struct AVHWFramesContext {
+ /**
+ * A class for logging.
+ */
+ const AVClass* av_class;
+
+ /**
+ * Private data used internally by libavutil. Must not be accessed in any
+ * way by the caller.
+ */
+ AVHWFramesInternal* internal;
+
+ /**
+ * A reference to the parent AVHWDeviceContext. This reference is owned and
+ * managed by the enclosing AVHWFramesContext, but the caller may derive
+ * additional references from it.
+ */
+ AVBufferRef* device_ref;
+
+ /**
+ * The parent AVHWDeviceContext. This is simply a pointer to
+ * device_ref->data provided for convenience.
+ *
+ * Set by libavutil in av_hwframe_ctx_init().
+ */
+ AVHWDeviceContext* device_ctx;
+
+ /**
+ * The format-specific data, allocated and freed automatically along with
+ * this context.
+ *
+ * Should be cast by the user to the format-specific context defined in the
+ * corresponding header (hwframe_*.h) and filled as described in the
+ * documentation before calling av_hwframe_ctx_init().
+ *
+ * After any frames using this context are created, the contents of this
+ * struct should not be modified by the caller.
+ */
+ void* hwctx;
+
+ /**
+ * This field may be set by the caller before calling av_hwframe_ctx_init().
+ *
+ * If non-NULL, this callback will be called when the last reference to
+ * this context is unreferenced, immediately before it is freed.
+ */
+ void (*free)(struct AVHWFramesContext* ctx);
+
+ /**
+ * Arbitrary user data, to be used e.g. by the free() callback.
+ */
+ void* user_opaque;
+
+ /**
+ * A pool from which the frames are allocated by av_hwframe_get_buffer().
+ * This field may be set by the caller before calling av_hwframe_ctx_init().
+ * The buffers returned by calling av_buffer_pool_get() on this pool must
+ * have the properties described in the documentation in the corresponding hw
+ * type's header (hwcontext_*.h). The pool will be freed strictly before
+ * this struct's free() callback is invoked.
+ *
+ * This field may be NULL, then libavutil will attempt to allocate a pool
+ * internally. Note that certain device types enforce pools allocated at
+ * fixed size (frame count), which cannot be extended dynamically. In such a
+ * case, initial_pool_size must be set appropriately.
+ */
+ AVBufferPool* pool;
+
+ /**
+ * Initial size of the frame pool. If a device type does not support
+ * dynamically resizing the pool, then this is also the maximum pool size.
+ *
+ * May be set by the caller before calling av_hwframe_ctx_init(). Must be
+ * set if pool is NULL and the device type does not support dynamic pools.
+ */
+ int initial_pool_size;
+
+ /**
+ * The pixel format identifying the underlying HW surface type.
+ *
+ * Must be a hwaccel format, i.e. the corresponding descriptor must have the
+ * AV_PIX_FMT_FLAG_HWACCEL flag set.
+ *
+ * Must be set by the user before calling av_hwframe_ctx_init().
+ */
+ enum AVPixelFormat format;
+
+ /**
+ * The pixel format identifying the actual data layout of the hardware
+ * frames.
+ *
+ * Must be set by the caller before calling av_hwframe_ctx_init().
+ *
+ * @note when the underlying API does not provide the exact data layout, but
+ * only the colorspace/bit depth, this field should be set to the fully
+ * planar version of that format (e.g. for 8-bit 420 YUV it should be
+ * AV_PIX_FMT_YUV420P, not AV_PIX_FMT_NV12 or anything else).
+ */
+ enum AVPixelFormat sw_format;
+
+ /**
+ * The allocated dimensions of the frames in this pool.
+ *
+ * Must be set by the user before calling av_hwframe_ctx_init().
+ */
+ int width, height;
+} AVHWFramesContext;
+
+/**
+ * Look up an AVHWDeviceType by name.
+ *
+ * @param name String name of the device type (case-insensitive).
+ * @return The type from enum AVHWDeviceType, or AV_HWDEVICE_TYPE_NONE if
+ * not found.
+ */
+enum AVHWDeviceType av_hwdevice_find_type_by_name(const char* name);
+
+/** Get the string name of an AVHWDeviceType.
+ *
+ * @param type Type from enum AVHWDeviceType.
+ * @return Pointer to a static string containing the name, or NULL if the type
+ * is not valid.
+ */
+const char* av_hwdevice_get_type_name(enum AVHWDeviceType type);
+
+/**
+ * Iterate over supported device types.
+ *
+ * @param prev AV_HWDEVICE_TYPE_NONE initially, then the previous type
+ * returned by this function in subsequent iterations.
+ * @return The next usable device type from enum AVHWDeviceType, or
+ * AV_HWDEVICE_TYPE_NONE if there are no more.
+ */
+enum AVHWDeviceType av_hwdevice_iterate_types(enum AVHWDeviceType prev);
+
+/**
+ * Allocate an AVHWDeviceContext for a given hardware type.
+ *
+ * @param type the type of the hardware device to allocate.
+ * @return a reference to the newly created AVHWDeviceContext on success or NULL
+ * on failure.
+ */
+AVBufferRef* av_hwdevice_ctx_alloc(enum AVHWDeviceType type);
+
+/**
+ * Finalize the device context before use. This function must be called after
+ * the context is filled with all the required information and before it is
+ * used in any way.
+ *
+ * @param ref a reference to the AVHWDeviceContext
+ * @return 0 on success, a negative AVERROR code on failure
+ */
+int av_hwdevice_ctx_init(AVBufferRef* ref);
+
+/**
+ * Open a device of the specified type and create an AVHWDeviceContext for it.
+ *
+ * This is a convenience function intended to cover the simple cases. Callers
+ * who need to fine-tune device creation/management should open the device
+ * manually and then wrap it in an AVHWDeviceContext using
+ * av_hwdevice_ctx_alloc()/av_hwdevice_ctx_init().
+ *
+ * The returned context is already initialized and ready for use, the caller
+ * should not call av_hwdevice_ctx_init() on it. The user_opaque/free fields of
+ * the created AVHWDeviceContext are set by this function and should not be
+ * touched by the caller.
+ *
+ * @param device_ctx On success, a reference to the newly-created device context
+ * will be written here. The reference is owned by the caller
+ * and must be released with av_buffer_unref() when no longer
+ * needed. On failure, NULL will be written to this pointer.
+ * @param type The type of the device to create.
+ * @param device A type-specific string identifying the device to open.
+ * @param opts A dictionary of additional (type-specific) options to use in
+ * opening the device. The dictionary remains owned by the caller.
+ * @param flags currently unused
+ *
+ * @return 0 on success, a negative AVERROR code on failure.
+ */
+int av_hwdevice_ctx_create(AVBufferRef** device_ctx, enum AVHWDeviceType type,
+ const char* device, AVDictionary* opts, int flags);
+
+/**
+ * Create a new device of the specified type from an existing device.
+ *
+ * If the source device is a device of the target type or was originally
+ * derived from such a device (possibly through one or more intermediate
+ * devices of other types), then this will return a reference to the
+ * existing device of the same type as is requested.
+ *
+ * Otherwise, it will attempt to derive a new device from the given source
+ * device. If direct derivation to the new type is not implemented, it will
+ * attempt the same derivation from each ancestor of the source device in
+ * turn looking for an implemented derivation method.
+ *
+ * @param dst_ctx On success, a reference to the newly-created
+ * AVHWDeviceContext.
+ * @param type The type of the new device to create.
+ * @param src_ctx A reference to an existing AVHWDeviceContext which will be
+ * used to create the new device.
+ * @param flags Currently unused; should be set to zero.
+ * @return Zero on success, a negative AVERROR code on failure.
+ */
+int av_hwdevice_ctx_create_derived(AVBufferRef** dst_ctx,
+ enum AVHWDeviceType type,
+ AVBufferRef* src_ctx, int flags);
+
+/**
+ * Create a new device of the specified type from an existing device.
+ *
+ * This function performs the same action as av_hwdevice_ctx_create_derived,
+ * however, it is able to set options for the new device to be derived.
+ *
+ * @param dst_ctx On success, a reference to the newly-created
+ * AVHWDeviceContext.
+ * @param type The type of the new device to create.
+ * @param src_ctx A reference to an existing AVHWDeviceContext which will be
+ * used to create the new device.
+ * @param options Options for the new device to create, same format as in
+ * av_hwdevice_ctx_create.
+ * @param flags Currently unused; should be set to zero.
+ * @return Zero on success, a negative AVERROR code on failure.
+ */
+int av_hwdevice_ctx_create_derived_opts(AVBufferRef** dst_ctx,
+ enum AVHWDeviceType type,
+ AVBufferRef* src_ctx,
+ AVDictionary* options, int flags);
+
+/**
+ * Allocate an AVHWFramesContext tied to a given device context.
+ *
+ * @param device_ctx a reference to a AVHWDeviceContext. This function will make
+ * a new reference for internal use, the one passed to the
+ * function remains owned by the caller.
+ * @return a reference to the newly created AVHWFramesContext on success or NULL
+ * on failure.
+ */
+AVBufferRef* av_hwframe_ctx_alloc(AVBufferRef* device_ctx);
+
+/**
+ * Finalize the context before use. This function must be called after the
+ * context is filled with all the required information and before it is attached
+ * to any frames.
+ *
+ * @param ref a reference to the AVHWFramesContext
+ * @return 0 on success, a negative AVERROR code on failure
+ */
+int av_hwframe_ctx_init(AVBufferRef* ref);
+
+/**
+ * Allocate a new frame attached to the given AVHWFramesContext.
+ *
+ * @param hwframe_ctx a reference to an AVHWFramesContext
+ * @param frame an empty (freshly allocated or unreffed) frame to be filled with
+ * newly allocated buffers.
+ * @param flags currently unused, should be set to zero
+ * @return 0 on success, a negative AVERROR code on failure
+ */
+int av_hwframe_get_buffer(AVBufferRef* hwframe_ctx, AVFrame* frame, int flags);
+
+/**
+ * Copy data to or from a hw surface. At least one of dst/src must have an
+ * AVHWFramesContext attached.
+ *
+ * If src has an AVHWFramesContext attached, then the format of dst (if set)
+ * must use one of the formats returned by av_hwframe_transfer_get_formats(src,
+ * AV_HWFRAME_TRANSFER_DIRECTION_FROM).
+ * If dst has an AVHWFramesContext attached, then the format of src must use one
+ * of the formats returned by av_hwframe_transfer_get_formats(dst,
+ * AV_HWFRAME_TRANSFER_DIRECTION_TO)
+ *
+ * dst may be "clean" (i.e. with data/buf pointers unset), in which case the
+ * data buffers will be allocated by this function using av_frame_get_buffer().
+ * If dst->format is set, then this format will be used, otherwise (when
+ * dst->format is AV_PIX_FMT_NONE) the first acceptable format will be chosen.
+ *
+ * The two frames must have matching allocated dimensions (i.e. equal to
+ * AVHWFramesContext.width/height), since not all device types support
+ * transferring a sub-rectangle of the whole surface. The display dimensions
+ * (i.e. AVFrame.width/height) may be smaller than the allocated dimensions, but
+ * also have to be equal for both frames. When the display dimensions are
+ * smaller than the allocated dimensions, the content of the padding in the
+ * destination frame is unspecified.
+ *
+ * @param dst the destination frame. dst is not touched on failure.
+ * @param src the source frame.
+ * @param flags currently unused, should be set to zero
+ * @return 0 on success, a negative AVERROR error code on failure.
+ */
+int av_hwframe_transfer_data(AVFrame* dst, const AVFrame* src, int flags);
+
+enum AVHWFrameTransferDirection {
+ /**
+ * Transfer the data from the queried hw frame.
+ */
+ AV_HWFRAME_TRANSFER_DIRECTION_FROM,
+
+ /**
+ * Transfer the data to the queried hw frame.
+ */
+ AV_HWFRAME_TRANSFER_DIRECTION_TO,
+};
+
+/**
+ * Get a list of possible source or target formats usable in
+ * av_hwframe_transfer_data().
+ *
+ * @param hwframe_ctx the frame context to obtain the information for
+ * @param dir the direction of the transfer
+ * @param formats the pointer to the output format list will be written here.
+ * The list is terminated with AV_PIX_FMT_NONE and must be freed
+ * by the caller when no longer needed using av_free().
+ * If this function returns successfully, the format list will
+ * have at least one item (not counting the terminator).
+ * On failure, the contents of this pointer are unspecified.
+ * @param flags currently unused, should be set to zero
+ * @return 0 on success, a negative AVERROR code on failure.
+ */
+int av_hwframe_transfer_get_formats(AVBufferRef* hwframe_ctx,
+ enum AVHWFrameTransferDirection dir,
+ enum AVPixelFormat** formats, int flags);
+
+/**
+ * This struct describes the constraints on hardware frames attached to
+ * a given device with a hardware-specific configuration. This is returned
+ * by av_hwdevice_get_hwframe_constraints() and must be freed by
+ * av_hwframe_constraints_free() after use.
+ */
+typedef struct AVHWFramesConstraints {
+ /**
+ * A list of possible values for format in the hw_frames_ctx,
+ * terminated by AV_PIX_FMT_NONE. This member will always be filled.
+ */
+ enum AVPixelFormat* valid_hw_formats;
+
+ /**
+ * A list of possible values for sw_format in the hw_frames_ctx,
+ * terminated by AV_PIX_FMT_NONE. Can be NULL if this information is
+ * not known.
+ */
+ enum AVPixelFormat* valid_sw_formats;
+
+ /**
+ * The minimum size of frames in this hw_frames_ctx.
+ * (Zero if not known.)
+ */
+ int min_width;
+ int min_height;
+
+ /**
+ * The maximum size of frames in this hw_frames_ctx.
+ * (INT_MAX if not known / no limit.)
+ */
+ int max_width;
+ int max_height;
+} AVHWFramesConstraints;
+
+/**
+ * Allocate a HW-specific configuration structure for a given HW device.
+ * After use, the user must free all members as required by the specific
+ * hardware structure being used, then free the structure itself with
+ * av_free().
+ *
+ * @param device_ctx a reference to the associated AVHWDeviceContext.
+ * @return The newly created HW-specific configuration structure on
+ * success or NULL on failure.
+ */
+void* av_hwdevice_hwconfig_alloc(AVBufferRef* device_ctx);
+
+/**
+ * Get the constraints on HW frames given a device and the HW-specific
+ * configuration to be used with that device. If no HW-specific
+ * configuration is provided, returns the maximum possible capabilities
+ * of the device.
+ *
+ * @param ref a reference to the associated AVHWDeviceContext.
+ * @param hwconfig a filled HW-specific configuration structure, or NULL
+ * to return the maximum possible capabilities of the device.
+ * @return AVHWFramesConstraints structure describing the constraints
+ * on the device, or NULL if not available.
+ */
+AVHWFramesConstraints* av_hwdevice_get_hwframe_constraints(
+ AVBufferRef* ref, const void* hwconfig);
+
+/**
+ * Free an AVHWFrameConstraints structure.
+ *
+ * @param constraints The (filled or unfilled) AVHWFrameConstraints structure.
+ */
+void av_hwframe_constraints_free(AVHWFramesConstraints** constraints);
+
+/**
+ * Flags to apply to frame mappings.
+ */
+enum {
+ /**
+ * The mapping must be readable.
+ */
+ AV_HWFRAME_MAP_READ = 1 << 0,
+ /**
+ * The mapping must be writeable.
+ */
+ AV_HWFRAME_MAP_WRITE = 1 << 1,
+ /**
+ * The mapped frame will be overwritten completely in subsequent
+ * operations, so the current frame data need not be loaded. Any values
+ * which are not overwritten are unspecified.
+ */
+ AV_HWFRAME_MAP_OVERWRITE = 1 << 2,
+ /**
+ * The mapping must be direct. That is, there must not be any copying in
+ * the map or unmap steps. Note that performance of direct mappings may
+ * be much lower than normal memory.
+ */
+ AV_HWFRAME_MAP_DIRECT = 1 << 3,
+};
+
+/**
+ * Map a hardware frame.
+ *
+ * This has a number of different possible effects, depending on the format
+ * and origin of the src and dst frames. On input, src should be a usable
+ * frame with valid buffers and dst should be blank (typically as just created
+ * by av_frame_alloc()). src should have an associated hwframe context, and
+ * dst may optionally have a format and associated hwframe context.
+ *
+ * If src was created by mapping a frame from the hwframe context of dst,
+ * then this function undoes the mapping - dst is replaced by a reference to
+ * the frame that src was originally mapped from.
+ *
+ * If both src and dst have an associated hwframe context, then this function
+ * attempts to map the src frame from its hardware context to that of dst and
+ * then fill dst with appropriate data to be usable there. This will only be
+ * possible if the hwframe contexts and associated devices are compatible -
+ * given compatible devices, av_hwframe_ctx_create_derived() can be used to
+ * create a hwframe context for dst in which mapping should be possible.
+ *
+ * If src has a hwframe context but dst does not, then the src frame is
+ * mapped to normal memory and should thereafter be usable as a normal frame.
+ * If the format is set on dst, then the mapping will attempt to create dst
+ * with that format and fail if it is not possible. If format is unset (is
+ * AV_PIX_FMT_NONE) then dst will be mapped with whatever the most appropriate
+ * format to use is (probably the sw_format of the src hwframe context).
+ *
+ * A return value of AVERROR(ENOSYS) indicates that the mapping is not
+ * possible with the given arguments and hwframe setup, while other return
+ * values indicate that it failed somehow.
+ *
+ * On failure, the destination frame will be left blank, except for the
+ * hw_frames_ctx/format fields thay may have been set by the caller - those will
+ * be preserved as they were.
+ *
+ * @param dst Destination frame, to contain the mapping.
+ * @param src Source frame, to be mapped.
+ * @param flags Some combination of AV_HWFRAME_MAP_* flags.
+ * @return Zero on success, negative AVERROR code on failure.
+ */
+int av_hwframe_map(AVFrame* dst, const AVFrame* src, int flags);
+
+/**
+ * Create and initialise an AVHWFramesContext as a mapping of another existing
+ * AVHWFramesContext on a different device.
+ *
+ * av_hwframe_ctx_init() should not be called after this.
+ *
+ * @param derived_frame_ctx On success, a reference to the newly created
+ * AVHWFramesContext.
+ * @param format The AVPixelFormat for the derived context.
+ * @param derived_device_ctx A reference to the device to create the new
+ * AVHWFramesContext on.
+ * @param source_frame_ctx A reference to an existing AVHWFramesContext
+ * which will be mapped to the derived context.
+ * @param flags Some combination of AV_HWFRAME_MAP_* flags, defining the
+ * mapping parameters to apply to frames which are allocated
+ * in the derived device.
+ * @return Zero on success, negative AVERROR code on failure.
+ */
+int av_hwframe_ctx_create_derived(AVBufferRef** derived_frame_ctx,
+ enum AVPixelFormat format,
+ AVBufferRef* derived_device_ctx,
+ AVBufferRef* source_frame_ctx, int flags);
+
+#endif /* AVUTIL_HWCONTEXT_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/hwcontext_drm.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/hwcontext_drm.h
new file mode 100644
index 0000000000..42709f215e
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/hwcontext_drm.h
@@ -0,0 +1,169 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_HWCONTEXT_DRM_H
+#define AVUTIL_HWCONTEXT_DRM_H
+
+#include <stddef.h>
+#include <stdint.h>
+
+/**
+ * @file
+ * API-specific header for AV_HWDEVICE_TYPE_DRM.
+ *
+ * Internal frame allocation is not currently supported - all frames
+ * must be allocated by the user. Thus AVHWFramesContext is always
+ * NULL, though this may change if support for frame allocation is
+ * added in future.
+ */
+
+enum {
+ /**
+ * The maximum number of layers/planes in a DRM frame.
+ */
+ AV_DRM_MAX_PLANES = 4
+};
+
+/**
+ * DRM object descriptor.
+ *
+ * Describes a single DRM object, addressing it as a PRIME file
+ * descriptor.
+ */
+typedef struct AVDRMObjectDescriptor {
+ /**
+ * DRM PRIME fd for the object.
+ */
+ int fd;
+ /**
+ * Total size of the object.
+ *
+ * (This includes any parts not which do not contain image data.)
+ */
+ size_t size;
+ /**
+ * Format modifier applied to the object (DRM_FORMAT_MOD_*).
+ *
+ * If the format modifier is unknown then this should be set to
+ * DRM_FORMAT_MOD_INVALID.
+ */
+ uint64_t format_modifier;
+} AVDRMObjectDescriptor;
+
+/**
+ * DRM plane descriptor.
+ *
+ * Describes a single plane of a layer, which is contained within
+ * a single object.
+ */
+typedef struct AVDRMPlaneDescriptor {
+ /**
+ * Index of the object containing this plane in the objects
+ * array of the enclosing frame descriptor.
+ */
+ int object_index;
+ /**
+ * Offset within that object of this plane.
+ */
+ ptrdiff_t offset;
+ /**
+ * Pitch (linesize) of this plane.
+ */
+ ptrdiff_t pitch;
+} AVDRMPlaneDescriptor;
+
+/**
+ * DRM layer descriptor.
+ *
+ * Describes a single layer within a frame. This has the structure
+ * defined by its format, and will contain one or more planes.
+ */
+typedef struct AVDRMLayerDescriptor {
+ /**
+ * Format of the layer (DRM_FORMAT_*).
+ */
+ uint32_t format;
+ /**
+ * Number of planes in the layer.
+ *
+ * This must match the number of planes required by format.
+ */
+ int nb_planes;
+ /**
+ * Array of planes in this layer.
+ */
+ AVDRMPlaneDescriptor planes[AV_DRM_MAX_PLANES];
+} AVDRMLayerDescriptor;
+
+/**
+ * DRM frame descriptor.
+ *
+ * This is used as the data pointer for AV_PIX_FMT_DRM_PRIME frames.
+ * It is also used by user-allocated frame pools - allocating in
+ * AVHWFramesContext.pool must return AVBufferRefs which contain
+ * an object of this type.
+ *
+ * The fields of this structure should be set such it can be
+ * imported directly by EGL using the EGL_EXT_image_dma_buf_import
+ * and EGL_EXT_image_dma_buf_import_modifiers extensions.
+ * (Note that the exact layout of a particular format may vary between
+ * platforms - we only specify that the same platform should be able
+ * to import it.)
+ *
+ * The total number of planes must not exceed AV_DRM_MAX_PLANES, and
+ * the order of the planes by increasing layer index followed by
+ * increasing plane index must be the same as the order which would
+ * be used for the data pointers in the equivalent software format.
+ */
+typedef struct AVDRMFrameDescriptor {
+ /**
+ * Number of DRM objects making up this frame.
+ */
+ int nb_objects;
+ /**
+ * Array of objects making up the frame.
+ */
+ AVDRMObjectDescriptor objects[AV_DRM_MAX_PLANES];
+ /**
+ * Number of layers in the frame.
+ */
+ int nb_layers;
+ /**
+ * Array of layers in the frame.
+ */
+ AVDRMLayerDescriptor layers[AV_DRM_MAX_PLANES];
+} AVDRMFrameDescriptor;
+
+/**
+ * DRM device.
+ *
+ * Allocated as AVHWDeviceContext.hwctx.
+ */
+typedef struct AVDRMDeviceContext {
+ /**
+ * File descriptor of DRM device.
+ *
+ * This is used as the device to create frames on, and may also be
+ * used in some derivation and mapping operations.
+ *
+ * If no device is required, set to -1.
+ */
+ int fd;
+} AVDRMDeviceContext;
+
+#endif /* AVUTIL_HWCONTEXT_DRM_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/hwcontext_vaapi.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/hwcontext_vaapi.h
new file mode 100644
index 0000000000..058b5f110d
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/hwcontext_vaapi.h
@@ -0,0 +1,117 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_HWCONTEXT_VAAPI_H
+#define AVUTIL_HWCONTEXT_VAAPI_H
+
+#include <va/va.h>
+
+/**
+ * @file
+ * API-specific header for AV_HWDEVICE_TYPE_VAAPI.
+ *
+ * Dynamic frame pools are supported, but note that any pool used as a render
+ * target is required to be of fixed size in order to be be usable as an
+ * argument to vaCreateContext().
+ *
+ * For user-allocated pools, AVHWFramesContext.pool must return AVBufferRefs
+ * with the data pointer set to a VASurfaceID.
+ */
+
+enum {
+ /**
+ * The quirks field has been set by the user and should not be detected
+ * automatically by av_hwdevice_ctx_init().
+ */
+ AV_VAAPI_DRIVER_QUIRK_USER_SET = (1 << 0),
+ /**
+ * The driver does not destroy parameter buffers when they are used by
+ * vaRenderPicture(). Additional code will be required to destroy them
+ * separately afterwards.
+ */
+ AV_VAAPI_DRIVER_QUIRK_RENDER_PARAM_BUFFERS = (1 << 1),
+
+ /**
+ * The driver does not support the VASurfaceAttribMemoryType attribute,
+ * so the surface allocation code will not try to use it.
+ */
+ AV_VAAPI_DRIVER_QUIRK_ATTRIB_MEMTYPE = (1 << 2),
+
+ /**
+ * The driver does not support surface attributes at all.
+ * The surface allocation code will never pass them to surface allocation,
+ * and the results of the vaQuerySurfaceAttributes() call will be faked.
+ */
+ AV_VAAPI_DRIVER_QUIRK_SURFACE_ATTRIBUTES = (1 << 3),
+};
+
+/**
+ * VAAPI connection details.
+ *
+ * Allocated as AVHWDeviceContext.hwctx
+ */
+typedef struct AVVAAPIDeviceContext {
+ /**
+ * The VADisplay handle, to be filled by the user.
+ */
+ VADisplay display;
+ /**
+ * Driver quirks to apply - this is filled by av_hwdevice_ctx_init(),
+ * with reference to a table of known drivers, unless the
+ * AV_VAAPI_DRIVER_QUIRK_USER_SET bit is already present. The user
+ * may need to refer to this field when performing any later
+ * operations using VAAPI with the same VADisplay.
+ */
+ unsigned int driver_quirks;
+} AVVAAPIDeviceContext;
+
+/**
+ * VAAPI-specific data associated with a frame pool.
+ *
+ * Allocated as AVHWFramesContext.hwctx.
+ */
+typedef struct AVVAAPIFramesContext {
+ /**
+ * Set by the user to apply surface attributes to all surfaces in
+ * the frame pool. If null, default settings are used.
+ */
+ VASurfaceAttrib* attributes;
+ int nb_attributes;
+ /**
+ * The surfaces IDs of all surfaces in the pool after creation.
+ * Only valid if AVHWFramesContext.initial_pool_size was positive.
+ * These are intended to be used as the render_targets arguments to
+ * vaCreateContext().
+ */
+ VASurfaceID* surface_ids;
+ int nb_surfaces;
+} AVVAAPIFramesContext;
+
+/**
+ * VAAPI hardware pipeline configuration details.
+ *
+ * Allocated with av_hwdevice_hwconfig_alloc().
+ */
+typedef struct AVVAAPIHWConfig {
+ /**
+ * ID of a VAAPI pipeline configuration.
+ */
+ VAConfigID config_id;
+} AVVAAPIHWConfig;
+
+#endif /* AVUTIL_HWCONTEXT_VAAPI_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/intfloat.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/intfloat.h
new file mode 100644
index 0000000000..f373c97796
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/intfloat.h
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2011 Mans Rullgard
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_INTFLOAT_H
+#define AVUTIL_INTFLOAT_H
+
+#include <stdint.h>
+#include "attributes.h"
+
+union av_intfloat32 {
+ uint32_t i;
+ float f;
+};
+
+union av_intfloat64 {
+ uint64_t i;
+ double f;
+};
+
+/**
+ * Reinterpret a 32-bit integer as a float.
+ */
+static av_always_inline float av_int2float(uint32_t i) {
+ union av_intfloat32 v;
+ v.i = i;
+ return v.f;
+}
+
+/**
+ * Reinterpret a float as a 32-bit integer.
+ */
+static av_always_inline uint32_t av_float2int(float f) {
+ union av_intfloat32 v;
+ v.f = f;
+ return v.i;
+}
+
+/**
+ * Reinterpret a 64-bit integer as a double.
+ */
+static av_always_inline double av_int2double(uint64_t i) {
+ union av_intfloat64 v;
+ v.i = i;
+ return v.f;
+}
+
+/**
+ * Reinterpret a double as a 64-bit integer.
+ */
+static av_always_inline uint64_t av_double2int(double f) {
+ union av_intfloat64 v;
+ v.f = f;
+ return v.i;
+}
+
+#endif /* AVUTIL_INTFLOAT_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/log.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/log.h
new file mode 100644
index 0000000000..e1f2af7b18
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/log.h
@@ -0,0 +1,388 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_LOG_H
+#define AVUTIL_LOG_H
+
+#include <stdarg.h>
+#include "attributes.h"
+#include "version.h"
+
+typedef enum {
+ AV_CLASS_CATEGORY_NA = 0,
+ AV_CLASS_CATEGORY_INPUT,
+ AV_CLASS_CATEGORY_OUTPUT,
+ AV_CLASS_CATEGORY_MUXER,
+ AV_CLASS_CATEGORY_DEMUXER,
+ AV_CLASS_CATEGORY_ENCODER,
+ AV_CLASS_CATEGORY_DECODER,
+ AV_CLASS_CATEGORY_FILTER,
+ AV_CLASS_CATEGORY_BITSTREAM_FILTER,
+ AV_CLASS_CATEGORY_SWSCALER,
+ AV_CLASS_CATEGORY_SWRESAMPLER,
+ AV_CLASS_CATEGORY_DEVICE_VIDEO_OUTPUT = 40,
+ AV_CLASS_CATEGORY_DEVICE_VIDEO_INPUT,
+ AV_CLASS_CATEGORY_DEVICE_AUDIO_OUTPUT,
+ AV_CLASS_CATEGORY_DEVICE_AUDIO_INPUT,
+ AV_CLASS_CATEGORY_DEVICE_OUTPUT,
+ AV_CLASS_CATEGORY_DEVICE_INPUT,
+ AV_CLASS_CATEGORY_NB ///< not part of ABI/API
+} AVClassCategory;
+
+#define AV_IS_INPUT_DEVICE(category) \
+ (((category) == AV_CLASS_CATEGORY_DEVICE_VIDEO_INPUT) || \
+ ((category) == AV_CLASS_CATEGORY_DEVICE_AUDIO_INPUT) || \
+ ((category) == AV_CLASS_CATEGORY_DEVICE_INPUT))
+
+#define AV_IS_OUTPUT_DEVICE(category) \
+ (((category) == AV_CLASS_CATEGORY_DEVICE_VIDEO_OUTPUT) || \
+ ((category) == AV_CLASS_CATEGORY_DEVICE_AUDIO_OUTPUT) || \
+ ((category) == AV_CLASS_CATEGORY_DEVICE_OUTPUT))
+
+struct AVOptionRanges;
+
+/**
+ * Describe the class of an AVClass context structure. That is an
+ * arbitrary struct of which the first field is a pointer to an
+ * AVClass struct (e.g. AVCodecContext, AVFormatContext etc.).
+ */
+typedef struct AVClass {
+ /**
+ * The name of the class; usually it is the same name as the
+ * context structure type to which the AVClass is associated.
+ */
+ const char* class_name;
+
+ /**
+ * A pointer to a function which returns the name of a context
+ * instance ctx associated with the class.
+ */
+ const char* (*item_name)(void* ctx);
+
+ /**
+ * a pointer to the first option specified in the class if any or NULL
+ *
+ * @see av_set_default_options()
+ */
+ const struct AVOption* option;
+
+ /**
+ * LIBAVUTIL_VERSION with which this structure was created.
+ * This is used to allow fields to be added without requiring major
+ * version bumps everywhere.
+ */
+
+ int version;
+
+ /**
+ * Offset in the structure where log_level_offset is stored.
+ * 0 means there is no such variable
+ */
+ int log_level_offset_offset;
+
+ /**
+ * Offset in the structure where a pointer to the parent context for
+ * logging is stored. For example a decoder could pass its AVCodecContext
+ * to eval as such a parent context, which an av_log() implementation
+ * could then leverage to display the parent context.
+ * The offset can be NULL.
+ */
+ int parent_log_context_offset;
+
+ /**
+ * Category used for visualization (like color)
+ * This is only set if the category is equal for all objects using this class.
+ * available since version (51 << 16 | 56 << 8 | 100)
+ */
+ AVClassCategory category;
+
+ /**
+ * Callback to return the category.
+ * available since version (51 << 16 | 59 << 8 | 100)
+ */
+ AVClassCategory (*get_category)(void* ctx);
+
+ /**
+ * Callback to return the supported/allowed ranges.
+ * available since version (52.12)
+ */
+ int (*query_ranges)(struct AVOptionRanges**, void* obj, const char* key,
+ int flags);
+
+ /**
+ * Return next AVOptions-enabled child or NULL
+ */
+ void* (*child_next)(void* obj, void* prev);
+
+ /**
+ * Iterate over the AVClasses corresponding to potential AVOptions-enabled
+ * children.
+ *
+ * @param iter pointer to opaque iteration state. The caller must initialize
+ * *iter to NULL before the first call.
+ * @return AVClass for the next AVOptions-enabled child or NULL if there are
+ * no more such children.
+ *
+ * @note The difference between child_next and this is that child_next
+ * iterates over _already existing_ objects, while child_class_iterate
+ * iterates over _all possible_ children.
+ */
+ const struct AVClass* (*child_class_iterate)(void** iter);
+} AVClass;
+
+/**
+ * @addtogroup lavu_log
+ *
+ * @{
+ *
+ * @defgroup lavu_log_constants Logging Constants
+ *
+ * @{
+ */
+
+/**
+ * Print no output.
+ */
+#define AV_LOG_QUIET -8
+
+/**
+ * Something went really wrong and we will crash now.
+ */
+#define AV_LOG_PANIC 0
+
+/**
+ * Something went wrong and recovery is not possible.
+ * For example, no header was found for a format which depends
+ * on headers or an illegal combination of parameters is used.
+ */
+#define AV_LOG_FATAL 8
+
+/**
+ * Something went wrong and cannot losslessly be recovered.
+ * However, not all future data is affected.
+ */
+#define AV_LOG_ERROR 16
+
+/**
+ * Something somehow does not look correct. This may or may not
+ * lead to problems. An example would be the use of '-vstrict -2'.
+ */
+#define AV_LOG_WARNING 24
+
+/**
+ * Standard information.
+ */
+#define AV_LOG_INFO 32
+
+/**
+ * Detailed information.
+ */
+#define AV_LOG_VERBOSE 40
+
+/**
+ * Stuff which is only useful for libav* developers.
+ */
+#define AV_LOG_DEBUG 48
+
+/**
+ * Extremely verbose debugging, useful for libav* development.
+ */
+#define AV_LOG_TRACE 56
+
+#define AV_LOG_MAX_OFFSET (AV_LOG_TRACE - AV_LOG_QUIET)
+
+/**
+ * @}
+ */
+
+/**
+ * Sets additional colors for extended debugging sessions.
+ * @code
+ av_log(ctx, AV_LOG_DEBUG|AV_LOG_C(134), "Message in purple\n");
+ @endcode
+ * Requires 256color terminal support. Uses outside debugging is not
+ * recommended.
+ */
+#define AV_LOG_C(x) ((x) << 8)
+
+/**
+ * Send the specified message to the log if the level is less than or equal
+ * to the current av_log_level. By default, all logging messages are sent to
+ * stderr. This behavior can be altered by setting a different logging callback
+ * function.
+ * @see av_log_set_callback
+ *
+ * @param avcl A pointer to an arbitrary struct of which the first field is a
+ * pointer to an AVClass struct or NULL if general log.
+ * @param level The importance level of the message expressed using a @ref
+ * lavu_log_constants "Logging Constant".
+ * @param fmt The format string (printf-compatible) that specifies how
+ * subsequent arguments are converted to output.
+ */
+void av_log(void* avcl, int level, const char* fmt, ...) av_printf_format(3, 4);
+
+/**
+ * Send the specified message to the log once with the initial_level and then
+ * with the subsequent_level. By default, all logging messages are sent to
+ * stderr. This behavior can be altered by setting a different logging callback
+ * function.
+ * @see av_log
+ *
+ * @param avcl A pointer to an arbitrary struct of which the first field is a
+ * pointer to an AVClass struct or NULL if general log.
+ * @param initial_level importance level of the message expressed using a @ref
+ * lavu_log_constants "Logging Constant" for the first occurance.
+ * @param subsequent_level importance level of the message expressed using a
+ * @ref lavu_log_constants "Logging Constant" after the first occurance.
+ * @param fmt The format string (printf-compatible) that specifies how
+ * subsequent arguments are converted to output.
+ * @param state a variable to keep trak of if a message has already been printed
+ * this must be initialized to 0 before the first use. The same state
+ * must not be accessed by 2 Threads simultaneously.
+ */
+void av_log_once(void* avcl, int initial_level, int subsequent_level,
+ int* state, const char* fmt, ...) av_printf_format(5, 6);
+
+/**
+ * Send the specified message to the log if the level is less than or equal
+ * to the current av_log_level. By default, all logging messages are sent to
+ * stderr. This behavior can be altered by setting a different logging callback
+ * function.
+ * @see av_log_set_callback
+ *
+ * @param avcl A pointer to an arbitrary struct of which the first field is a
+ * pointer to an AVClass struct.
+ * @param level The importance level of the message expressed using a @ref
+ * lavu_log_constants "Logging Constant".
+ * @param fmt The format string (printf-compatible) that specifies how
+ * subsequent arguments are converted to output.
+ * @param vl The arguments referenced by the format string.
+ */
+void av_vlog(void* avcl, int level, const char* fmt, va_list vl);
+
+/**
+ * Get the current log level
+ *
+ * @see lavu_log_constants
+ *
+ * @return Current log level
+ */
+int av_log_get_level(void);
+
+/**
+ * Set the log level
+ *
+ * @see lavu_log_constants
+ *
+ * @param level Logging level
+ */
+void av_log_set_level(int level);
+
+/**
+ * Set the logging callback
+ *
+ * @note The callback must be thread safe, even if the application does not use
+ * threads itself as some codecs are multithreaded.
+ *
+ * @see av_log_default_callback
+ *
+ * @param callback A logging function with a compatible signature.
+ */
+void av_log_set_callback(void (*callback)(void*, int, const char*, va_list));
+
+/**
+ * Default logging callback
+ *
+ * It prints the message to stderr, optionally colorizing it.
+ *
+ * @param avcl A pointer to an arbitrary struct of which the first field is a
+ * pointer to an AVClass struct.
+ * @param level The importance level of the message expressed using a @ref
+ * lavu_log_constants "Logging Constant".
+ * @param fmt The format string (printf-compatible) that specifies how
+ * subsequent arguments are converted to output.
+ * @param vl The arguments referenced by the format string.
+ */
+void av_log_default_callback(void* avcl, int level, const char* fmt,
+ va_list vl);
+
+/**
+ * Return the context name
+ *
+ * @param ctx The AVClass context
+ *
+ * @return The AVClass class_name
+ */
+const char* av_default_item_name(void* ctx);
+AVClassCategory av_default_get_category(void* ptr);
+
+/**
+ * Format a line of log the same way as the default callback.
+ * @param line buffer to receive the formatted line
+ * @param line_size size of the buffer
+ * @param print_prefix used to store whether the prefix must be printed;
+ * must point to a persistent integer initially set to 1
+ */
+void av_log_format_line(void* ptr, int level, const char* fmt, va_list vl,
+ char* line, int line_size, int* print_prefix);
+
+/**
+ * Format a line of log the same way as the default callback.
+ * @param line buffer to receive the formatted line;
+ * may be NULL if line_size is 0
+ * @param line_size size of the buffer; at most line_size-1 characters will
+ * be written to the buffer, plus one null terminator
+ * @param print_prefix used to store whether the prefix must be printed;
+ * must point to a persistent integer initially set to 1
+ * @return Returns a negative value if an error occurred, otherwise returns
+ * the number of characters that would have been written for a
+ * sufficiently large buffer, not including the terminating null
+ * character. If the return value is not less than line_size, it means
+ * that the log message was truncated to fit the buffer.
+ */
+int av_log_format_line2(void* ptr, int level, const char* fmt, va_list vl,
+ char* line, int line_size, int* print_prefix);
+
+/**
+ * Skip repeated messages, this requires the user app to use av_log() instead of
+ * (f)printf as the 2 would otherwise interfere and lead to
+ * "Last message repeated x times" messages below (f)printf messages with some
+ * bad luck.
+ * Also to receive the last, "last repeated" line if any, the user app must
+ * call av_log(NULL, AV_LOG_QUIET, "%s", ""); at the end
+ */
+#define AV_LOG_SKIP_REPEATED 1
+
+/**
+ * Include the log severity in messages originating from codecs.
+ *
+ * Results in messages such as:
+ * [rawvideo @ 0xDEADBEEF] [error] encode did not produce valid pts
+ */
+#define AV_LOG_PRINT_LEVEL 2
+
+void av_log_set_flags(int arg);
+int av_log_get_flags(void);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_LOG_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/macros.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/macros.h
new file mode 100644
index 0000000000..1578d1a345
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/macros.h
@@ -0,0 +1,87 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * @ingroup lavu
+ * Utility Preprocessor macros
+ */
+
+#ifndef AVUTIL_MACROS_H
+#define AVUTIL_MACROS_H
+
+#include "libavutil/avconfig.h"
+
+#if AV_HAVE_BIGENDIAN
+# define AV_NE(be, le) (be)
+#else
+# define AV_NE(be, le) (le)
+#endif
+
+/**
+ * Comparator.
+ * For two numerical expressions x and y, gives 1 if x > y, -1 if x < y, and 0
+ * if x == y. This is useful for instance in a qsort comparator callback.
+ * Furthermore, compilers are able to optimize this to branchless code, and
+ * there is no risk of overflow with signed types.
+ * As with many macros, this evaluates its argument multiple times, it thus
+ * must not have a side-effect.
+ */
+#define FFDIFFSIGN(x, y) (((x) > (y)) - ((x) < (y)))
+
+#define FFMAX(a, b) ((a) > (b) ? (a) : (b))
+#define FFMAX3(a, b, c) FFMAX(FFMAX(a, b), c)
+#define FFMIN(a, b) ((a) > (b) ? (b) : (a))
+#define FFMIN3(a, b, c) FFMIN(FFMIN(a, b), c)
+
+#define FFSWAP(type, a, b) \
+ do { \
+ type SWAP_tmp = b; \
+ b = a; \
+ a = SWAP_tmp; \
+ } while (0)
+#define FF_ARRAY_ELEMS(a) (sizeof(a) / sizeof((a)[0]))
+
+#define MKTAG(a, b, c, d) \
+ ((a) | ((b) << 8) | ((c) << 16) | ((unsigned)(d) << 24))
+#define MKBETAG(a, b, c, d) \
+ ((d) | ((c) << 8) | ((b) << 16) | ((unsigned)(a) << 24))
+
+/**
+ * @addtogroup preproc_misc Preprocessor String Macros
+ *
+ * String manipulation macros
+ *
+ * @{
+ */
+
+#define AV_STRINGIFY(s) AV_TOSTRING(s)
+#define AV_TOSTRING(s) #s
+
+#define AV_GLUE(a, b) a##b
+#define AV_JOIN(a, b) AV_GLUE(a, b)
+
+/**
+ * @}
+ */
+
+#define AV_PRAGMA(s) _Pragma(#s)
+
+#define FFALIGN(x, a) (((x) + (a)-1) & ~((a)-1))
+
+#endif /* AVUTIL_MACROS_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/mathematics.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/mathematics.h
new file mode 100644
index 0000000000..5ebf81e64f
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/mathematics.h
@@ -0,0 +1,249 @@
+/*
+ * copyright (c) 2005-2012 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * @addtogroup lavu_math
+ * Mathematical utilities for working with timestamp and time base.
+ */
+
+#ifndef AVUTIL_MATHEMATICS_H
+#define AVUTIL_MATHEMATICS_H
+
+#include <stdint.h>
+#include <math.h>
+#include "attributes.h"
+#include "rational.h"
+#include "intfloat.h"
+
+#ifndef M_E
+# define M_E 2.7182818284590452354 /* e */
+#endif
+#ifndef M_LN2
+# define M_LN2 0.69314718055994530942 /* log_e 2 */
+#endif
+#ifndef M_LN10
+# define M_LN10 2.30258509299404568402 /* log_e 10 */
+#endif
+#ifndef M_LOG2_10
+# define M_LOG2_10 3.32192809488736234787 /* log_2 10 */
+#endif
+#ifndef M_PHI
+# define M_PHI 1.61803398874989484820 /* phi / golden ratio */
+#endif
+#ifndef M_PI
+# define M_PI 3.14159265358979323846 /* pi */
+#endif
+#ifndef M_PI_2
+# define M_PI_2 1.57079632679489661923 /* pi/2 */
+#endif
+#ifndef M_SQRT1_2
+# define M_SQRT1_2 0.70710678118654752440 /* 1/sqrt(2) */
+#endif
+#ifndef M_SQRT2
+# define M_SQRT2 1.41421356237309504880 /* sqrt(2) */
+#endif
+#ifndef NAN
+# define NAN av_int2float(0x7fc00000)
+#endif
+#ifndef INFINITY
+# define INFINITY av_int2float(0x7f800000)
+#endif
+
+/**
+ * @addtogroup lavu_math
+ *
+ * @{
+ */
+
+/**
+ * Rounding methods.
+ */
+enum AVRounding {
+ AV_ROUND_ZERO = 0, ///< Round toward zero.
+ AV_ROUND_INF = 1, ///< Round away from zero.
+ AV_ROUND_DOWN = 2, ///< Round toward -infinity.
+ AV_ROUND_UP = 3, ///< Round toward +infinity.
+ AV_ROUND_NEAR_INF =
+ 5, ///< Round to nearest and halfway cases away from zero.
+ /**
+ * Flag telling rescaling functions to pass `INT64_MIN`/`MAX` through
+ * unchanged, avoiding special cases for #AV_NOPTS_VALUE.
+ *
+ * Unlike other values of the enumeration AVRounding, this value is a
+ * bitmask that must be used in conjunction with another value of the
+ * enumeration through a bitwise OR, in order to set behavior for normal
+ * cases.
+ *
+ * @code{.c}
+ * av_rescale_rnd(3, 1, 2, AV_ROUND_UP | AV_ROUND_PASS_MINMAX);
+ * // Rescaling 3:
+ * // Calculating 3 * 1 / 2
+ * // 3 / 2 is rounded up to 2
+ * // => 2
+ *
+ * av_rescale_rnd(AV_NOPTS_VALUE, 1, 2, AV_ROUND_UP | AV_ROUND_PASS_MINMAX);
+ * // Rescaling AV_NOPTS_VALUE:
+ * // AV_NOPTS_VALUE == INT64_MIN
+ * // AV_NOPTS_VALUE is passed through
+ * // => AV_NOPTS_VALUE
+ * @endcode
+ */
+ AV_ROUND_PASS_MINMAX = 8192,
+};
+
+/**
+ * Compute the greatest common divisor of two integer operands.
+ *
+ * @param a Operand
+ * @param b Operand
+ * @return GCD of a and b up to sign; if a >= 0 and b >= 0, return value is >=
+ * 0; if a == 0 and b == 0, returns 0.
+ */
+int64_t av_const av_gcd(int64_t a, int64_t b);
+
+/**
+ * Rescale a 64-bit integer with rounding to nearest.
+ *
+ * The operation is mathematically equivalent to `a * b / c`, but writing that
+ * directly can overflow.
+ *
+ * This function is equivalent to av_rescale_rnd() with #AV_ROUND_NEAR_INF.
+ *
+ * @see av_rescale_rnd(), av_rescale_q(), av_rescale_q_rnd()
+ */
+int64_t av_rescale(int64_t a, int64_t b, int64_t c) av_const;
+
+/**
+ * Rescale a 64-bit integer with specified rounding.
+ *
+ * The operation is mathematically equivalent to `a * b / c`, but writing that
+ * directly can overflow, and does not support different rounding methods.
+ * If the result is not representable then INT64_MIN is returned.
+ *
+ * @see av_rescale(), av_rescale_q(), av_rescale_q_rnd()
+ */
+int64_t av_rescale_rnd(int64_t a, int64_t b, int64_t c,
+ enum AVRounding rnd) av_const;
+
+/**
+ * Rescale a 64-bit integer by 2 rational numbers.
+ *
+ * The operation is mathematically equivalent to `a * bq / cq`.
+ *
+ * This function is equivalent to av_rescale_q_rnd() with #AV_ROUND_NEAR_INF.
+ *
+ * @see av_rescale(), av_rescale_rnd(), av_rescale_q_rnd()
+ */
+int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq) av_const;
+
+/**
+ * Rescale a 64-bit integer by 2 rational numbers with specified rounding.
+ *
+ * The operation is mathematically equivalent to `a * bq / cq`.
+ *
+ * @see av_rescale(), av_rescale_rnd(), av_rescale_q()
+ */
+int64_t av_rescale_q_rnd(int64_t a, AVRational bq, AVRational cq,
+ enum AVRounding rnd) av_const;
+
+/**
+ * Compare two timestamps each in its own time base.
+ *
+ * @return One of the following values:
+ * - -1 if `ts_a` is before `ts_b`
+ * - 1 if `ts_a` is after `ts_b`
+ * - 0 if they represent the same position
+ *
+ * @warning
+ * The result of the function is undefined if one of the timestamps is outside
+ * the `int64_t` range when represented in the other's timebase.
+ */
+int av_compare_ts(int64_t ts_a, AVRational tb_a, int64_t ts_b, AVRational tb_b);
+
+/**
+ * Compare the remainders of two integer operands divided by a common divisor.
+ *
+ * In other words, compare the least significant `log2(mod)` bits of integers
+ * `a` and `b`.
+ *
+ * @code{.c}
+ * av_compare_mod(0x11, 0x02, 0x10) < 0 // since 0x11 % 0x10 (0x1) < 0x02 %
+ * 0x10 (0x2) av_compare_mod(0x11, 0x02, 0x20) > 0 // since 0x11 % 0x20 (0x11)
+ * > 0x02 % 0x20 (0x02)
+ * @endcode
+ *
+ * @param a Operand
+ * @param b Operand
+ * @param mod Divisor; must be a power of 2
+ * @return
+ * - a negative value if `a % mod < b % mod`
+ * - a positive value if `a % mod > b % mod`
+ * - zero if `a % mod == b % mod`
+ */
+int64_t av_compare_mod(uint64_t a, uint64_t b, uint64_t mod);
+
+/**
+ * Rescale a timestamp while preserving known durations.
+ *
+ * This function is designed to be called per audio packet to scale the input
+ * timestamp to a different time base. Compared to a simple av_rescale_q()
+ * call, this function is robust against possible inconsistent frame durations.
+ *
+ * The `last` parameter is a state variable that must be preserved for all
+ * subsequent calls for the same stream. For the first call, `*last` should be
+ * initialized to #AV_NOPTS_VALUE.
+ *
+ * @param[in] in_tb Input time base
+ * @param[in] in_ts Input timestamp
+ * @param[in] fs_tb Duration time base; typically this is finer-grained
+ * (greater) than `in_tb` and `out_tb`
+ * @param[in] duration Duration till the next call to this function (i.e.
+ * duration of the current packet/frame)
+ * @param[in,out] last Pointer to a timestamp expressed in terms of
+ * `fs_tb`, acting as a state variable
+ * @param[in] out_tb Output timebase
+ * @return Timestamp expressed in terms of `out_tb`
+ *
+ * @note In the context of this function, "duration" is in term of samples, not
+ * seconds.
+ */
+int64_t av_rescale_delta(AVRational in_tb, int64_t in_ts, AVRational fs_tb,
+ int duration, int64_t* last, AVRational out_tb);
+
+/**
+ * Add a value to a timestamp.
+ *
+ * This function guarantees that when the same value is repeatly added that
+ * no accumulation of rounding errors occurs.
+ *
+ * @param[in] ts Input timestamp
+ * @param[in] ts_tb Input timestamp time base
+ * @param[in] inc Value to be added
+ * @param[in] inc_tb Time base of `inc`
+ */
+int64_t av_add_stable(AVRational ts_tb, int64_t ts, AVRational inc_tb,
+ int64_t inc);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_MATHEMATICS_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/mem.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/mem.h
new file mode 100644
index 0000000000..8f5d8c756e
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/mem.h
@@ -0,0 +1,613 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * @ingroup lavu_mem
+ * Memory handling functions
+ */
+
+#ifndef AVUTIL_MEM_H
+#define AVUTIL_MEM_H
+
+#include <limits.h>
+#include <stdint.h>
+
+#include "attributes.h"
+#include "avutil.h"
+#include "version.h"
+
+/**
+ * @addtogroup lavu_mem
+ * Utilities for manipulating memory.
+ *
+ * FFmpeg has several applications of memory that are not required of a typical
+ * program. For example, the computing-heavy components like video decoding and
+ * encoding can be sped up significantly through the use of aligned memory.
+ *
+ * However, for each of FFmpeg's applications of memory, there might not be a
+ * recognized or standardized API for that specific use. Memory alignment, for
+ * instance, varies wildly depending on operating systems, architectures, and
+ * compilers. Hence, this component of @ref libavutil is created to make
+ * dealing with memory consistently possible on all platforms.
+ *
+ * @{
+ */
+
+/**
+ * @defgroup lavu_mem_attrs Function Attributes
+ * Function attributes applicable to memory handling functions.
+ *
+ * These function attributes can help compilers emit more useful warnings, or
+ * generate better code.
+ * @{
+ */
+
+/**
+ * @def av_malloc_attrib
+ * Function attribute denoting a malloc-like function.
+ *
+ * @see <a
+ * href="https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#index-g_t_0040code_007bmalloc_007d-function-attribute-3251">Function
+ * attribute `malloc` in GCC's documentation</a>
+ */
+
+#if AV_GCC_VERSION_AT_LEAST(3, 1)
+# define av_malloc_attrib __attribute__((__malloc__))
+#else
+# define av_malloc_attrib
+#endif
+
+/**
+ * @def av_alloc_size(...)
+ * Function attribute used on a function that allocates memory, whose size is
+ * given by the specified parameter(s).
+ *
+ * @code{.c}
+ * void *av_malloc(size_t size) av_alloc_size(1);
+ * void *av_calloc(size_t nmemb, size_t size) av_alloc_size(1, 2);
+ * @endcode
+ *
+ * @param ... One or two parameter indexes, separated by a comma
+ *
+ * @see <a
+ * href="https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#index-g_t_0040code_007balloc_005fsize_007d-function-attribute-3220">Function
+ * attribute `alloc_size` in GCC's documentation</a>
+ */
+
+#if AV_GCC_VERSION_AT_LEAST(4, 3)
+# define av_alloc_size(...) __attribute__((alloc_size(__VA_ARGS__)))
+#else
+# define av_alloc_size(...)
+#endif
+
+/**
+ * @}
+ */
+
+/**
+ * @defgroup lavu_mem_funcs Heap Management
+ * Functions responsible for allocating, freeing, and copying memory.
+ *
+ * All memory allocation functions have a built-in upper limit of `INT_MAX`
+ * bytes. This may be changed with av_max_alloc(), although exercise extreme
+ * caution when doing so.
+ *
+ * @{
+ */
+
+/**
+ * Allocate a memory block with alignment suitable for all memory accesses
+ * (including vectors if available on the CPU).
+ *
+ * @param size Size in bytes for the memory block to be allocated
+ * @return Pointer to the allocated block, or `NULL` if the block cannot
+ * be allocated
+ * @see av_mallocz()
+ */
+void* av_malloc(size_t size) av_malloc_attrib av_alloc_size(1);
+
+/**
+ * Allocate a memory block with alignment suitable for all memory accesses
+ * (including vectors if available on the CPU) and zero all the bytes of the
+ * block.
+ *
+ * @param size Size in bytes for the memory block to be allocated
+ * @return Pointer to the allocated block, or `NULL` if it cannot be allocated
+ * @see av_malloc()
+ */
+void* av_mallocz(size_t size) av_malloc_attrib av_alloc_size(1);
+
+/**
+ * Allocate a memory block for an array with av_malloc().
+ *
+ * The allocated memory will have size `size * nmemb` bytes.
+ *
+ * @param nmemb Number of element
+ * @param size Size of a single element
+ * @return Pointer to the allocated block, or `NULL` if the block cannot
+ * be allocated
+ * @see av_malloc()
+ */
+av_alloc_size(1, 2) void* av_malloc_array(size_t nmemb, size_t size);
+
+/**
+ * Allocate a memory block for an array with av_mallocz().
+ *
+ * The allocated memory will have size `size * nmemb` bytes.
+ *
+ * @param nmemb Number of elements
+ * @param size Size of the single element
+ * @return Pointer to the allocated block, or `NULL` if the block cannot
+ * be allocated
+ *
+ * @see av_mallocz()
+ * @see av_malloc_array()
+ */
+void* av_calloc(size_t nmemb, size_t size) av_malloc_attrib av_alloc_size(1, 2);
+
+/**
+ * Allocate, reallocate, or free a block of memory.
+ *
+ * If `ptr` is `NULL` and `size` > 0, allocate a new block. Otherwise, expand or
+ * shrink that block of memory according to `size`.
+ *
+ * @param ptr Pointer to a memory block already allocated with
+ * av_realloc() or `NULL`
+ * @param size Size in bytes of the memory block to be allocated or
+ * reallocated
+ *
+ * @return Pointer to a newly-reallocated block or `NULL` if the block
+ * cannot be reallocated
+ *
+ * @warning Unlike av_malloc(), the returned pointer is not guaranteed to be
+ * correctly aligned. The returned pointer must be freed after even
+ * if size is zero.
+ * @see av_fast_realloc()
+ * @see av_reallocp()
+ */
+void* av_realloc(void* ptr, size_t size) av_alloc_size(2);
+
+/**
+ * Allocate, reallocate, or free a block of memory through a pointer to a
+ * pointer.
+ *
+ * If `*ptr` is `NULL` and `size` > 0, allocate a new block. If `size` is
+ * zero, free the memory block pointed to by `*ptr`. Otherwise, expand or
+ * shrink that block of memory according to `size`.
+ *
+ * @param[in,out] ptr Pointer to a pointer to a memory block already allocated
+ * with av_realloc(), or a pointer to `NULL`. The pointer
+ * is updated on success, or freed on failure.
+ * @param[in] size Size in bytes for the memory block to be allocated or
+ * reallocated
+ *
+ * @return Zero on success, an AVERROR error code on failure
+ *
+ * @warning Unlike av_malloc(), the allocated memory is not guaranteed to be
+ * correctly aligned.
+ */
+av_warn_unused_result int av_reallocp(void* ptr, size_t size);
+
+/**
+ * Allocate, reallocate, or free a block of memory.
+ *
+ * This function does the same thing as av_realloc(), except:
+ * - It takes two size arguments and allocates `nelem * elsize` bytes,
+ * after checking the result of the multiplication for integer overflow.
+ * - It frees the input block in case of failure, thus avoiding the memory
+ * leak with the classic
+ * @code{.c}
+ * buf = realloc(buf);
+ * if (!buf)
+ * return -1;
+ * @endcode
+ * pattern.
+ */
+void* av_realloc_f(void* ptr, size_t nelem, size_t elsize);
+
+/**
+ * Allocate, reallocate, or free an array.
+ *
+ * If `ptr` is `NULL` and `nmemb` > 0, allocate a new block.
+ *
+ * @param ptr Pointer to a memory block already allocated with
+ * av_realloc() or `NULL`
+ * @param nmemb Number of elements in the array
+ * @param size Size of the single element of the array
+ *
+ * @return Pointer to a newly-reallocated block or NULL if the block
+ * cannot be reallocated
+ *
+ * @warning Unlike av_malloc(), the allocated memory is not guaranteed to be
+ * correctly aligned. The returned pointer must be freed after even if
+ * nmemb is zero.
+ * @see av_reallocp_array()
+ */
+av_alloc_size(2, 3) void* av_realloc_array(void* ptr, size_t nmemb,
+ size_t size);
+
+/**
+ * Allocate, reallocate an array through a pointer to a pointer.
+ *
+ * If `*ptr` is `NULL` and `nmemb` > 0, allocate a new block.
+ *
+ * @param[in,out] ptr Pointer to a pointer to a memory block already
+ * allocated with av_realloc(), or a pointer to `NULL`.
+ * The pointer is updated on success, or freed on failure.
+ * @param[in] nmemb Number of elements
+ * @param[in] size Size of the single element
+ *
+ * @return Zero on success, an AVERROR error code on failure
+ *
+ * @warning Unlike av_malloc(), the allocated memory is not guaranteed to be
+ * correctly aligned. *ptr must be freed after even if nmemb is zero.
+ */
+int av_reallocp_array(void* ptr, size_t nmemb, size_t size);
+
+/**
+ * Reallocate the given buffer if it is not large enough, otherwise do nothing.
+ *
+ * If the given buffer is `NULL`, then a new uninitialized buffer is allocated.
+ *
+ * If the given buffer is not large enough, and reallocation fails, `NULL` is
+ * returned and `*size` is set to 0, but the original buffer is not changed or
+ * freed.
+ *
+ * A typical use pattern follows:
+ *
+ * @code{.c}
+ * uint8_t *buf = ...;
+ * uint8_t *new_buf = av_fast_realloc(buf, &current_size, size_needed);
+ * if (!new_buf) {
+ * // Allocation failed; clean up original buffer
+ * av_freep(&buf);
+ * return AVERROR(ENOMEM);
+ * }
+ * @endcode
+ *
+ * @param[in,out] ptr Already allocated buffer, or `NULL`
+ * @param[in,out] size Pointer to the size of buffer `ptr`. `*size` is
+ * updated to the new allocated size, in particular 0
+ * in case of failure.
+ * @param[in] min_size Desired minimal size of buffer `ptr`
+ * @return `ptr` if the buffer is large enough, a pointer to newly reallocated
+ * buffer if the buffer was not large enough, or `NULL` in case of
+ * error
+ * @see av_realloc()
+ * @see av_fast_malloc()
+ */
+void* av_fast_realloc(void* ptr, unsigned int* size, size_t min_size);
+
+/**
+ * Allocate a buffer, reusing the given one if large enough.
+ *
+ * Contrary to av_fast_realloc(), the current buffer contents might not be
+ * preserved and on error the old buffer is freed, thus no special handling to
+ * avoid memleaks is necessary.
+ *
+ * `*ptr` is allowed to be `NULL`, in which case allocation always happens if
+ * `size_needed` is greater than 0.
+ *
+ * @code{.c}
+ * uint8_t *buf = ...;
+ * av_fast_malloc(&buf, &current_size, size_needed);
+ * if (!buf) {
+ * // Allocation failed; buf already freed
+ * return AVERROR(ENOMEM);
+ * }
+ * @endcode
+ *
+ * @param[in,out] ptr Pointer to pointer to an already allocated buffer.
+ * `*ptr` will be overwritten with pointer to new
+ * buffer on success or `NULL` on failure
+ * @param[in,out] size Pointer to the size of buffer `*ptr`. `*size` is
+ * updated to the new allocated size, in particular 0
+ * in case of failure.
+ * @param[in] min_size Desired minimal size of buffer `*ptr`
+ * @see av_realloc()
+ * @see av_fast_mallocz()
+ */
+void av_fast_malloc(void* ptr, unsigned int* size, size_t min_size);
+
+/**
+ * Allocate and clear a buffer, reusing the given one if large enough.
+ *
+ * Like av_fast_malloc(), but all newly allocated space is initially cleared.
+ * Reused buffer is not cleared.
+ *
+ * `*ptr` is allowed to be `NULL`, in which case allocation always happens if
+ * `size_needed` is greater than 0.
+ *
+ * @param[in,out] ptr Pointer to pointer to an already allocated buffer.
+ * `*ptr` will be overwritten with pointer to new
+ * buffer on success or `NULL` on failure
+ * @param[in,out] size Pointer to the size of buffer `*ptr`. `*size` is
+ * updated to the new allocated size, in particular 0
+ * in case of failure.
+ * @param[in] min_size Desired minimal size of buffer `*ptr`
+ * @see av_fast_malloc()
+ */
+void av_fast_mallocz(void* ptr, unsigned int* size, size_t min_size);
+
+/**
+ * Free a memory block which has been allocated with a function of av_malloc()
+ * or av_realloc() family.
+ *
+ * @param ptr Pointer to the memory block which should be freed.
+ *
+ * @note `ptr = NULL` is explicitly allowed.
+ * @note It is recommended that you use av_freep() instead, to prevent leaving
+ * behind dangling pointers.
+ * @see av_freep()
+ */
+void av_free(void* ptr);
+
+/**
+ * Free a memory block which has been allocated with a function of av_malloc()
+ * or av_realloc() family, and set the pointer pointing to it to `NULL`.
+ *
+ * @code{.c}
+ * uint8_t *buf = av_malloc(16);
+ * av_free(buf);
+ * // buf now contains a dangling pointer to freed memory, and accidental
+ * // dereference of buf will result in a use-after-free, which may be a
+ * // security risk.
+ *
+ * uint8_t *buf = av_malloc(16);
+ * av_freep(&buf);
+ * // buf is now NULL, and accidental dereference will only result in a
+ * // NULL-pointer dereference.
+ * @endcode
+ *
+ * @param ptr Pointer to the pointer to the memory block which should be freed
+ * @note `*ptr = NULL` is safe and leads to no action.
+ * @see av_free()
+ */
+void av_freep(void* ptr);
+
+/**
+ * Duplicate a string.
+ *
+ * @param s String to be duplicated
+ * @return Pointer to a newly-allocated string containing a
+ * copy of `s` or `NULL` if the string cannot be allocated
+ * @see av_strndup()
+ */
+char* av_strdup(const char* s) av_malloc_attrib;
+
+/**
+ * Duplicate a substring of a string.
+ *
+ * @param s String to be duplicated
+ * @param len Maximum length of the resulting string (not counting the
+ * terminating byte)
+ * @return Pointer to a newly-allocated string containing a
+ * substring of `s` or `NULL` if the string cannot be allocated
+ */
+char* av_strndup(const char* s, size_t len) av_malloc_attrib;
+
+/**
+ * Duplicate a buffer with av_malloc().
+ *
+ * @param p Buffer to be duplicated
+ * @param size Size in bytes of the buffer copied
+ * @return Pointer to a newly allocated buffer containing a
+ * copy of `p` or `NULL` if the buffer cannot be allocated
+ */
+void* av_memdup(const void* p, size_t size);
+
+/**
+ * Overlapping memcpy() implementation.
+ *
+ * @param dst Destination buffer
+ * @param back Number of bytes back to start copying (i.e. the initial size of
+ * the overlapping window); must be > 0
+ * @param cnt Number of bytes to copy; must be >= 0
+ *
+ * @note `cnt > back` is valid, this will copy the bytes we just copied,
+ * thus creating a repeating pattern with a period length of `back`.
+ */
+void av_memcpy_backptr(uint8_t* dst, int back, int cnt);
+
+/**
+ * @}
+ */
+
+/**
+ * @defgroup lavu_mem_dynarray Dynamic Array
+ *
+ * Utilities to make an array grow when needed.
+ *
+ * Sometimes, the programmer would want to have an array that can grow when
+ * needed. The libavutil dynamic array utilities fill that need.
+ *
+ * libavutil supports two systems of appending elements onto a dynamically
+ * allocated array, the first one storing the pointer to the value in the
+ * array, and the second storing the value directly. In both systems, the
+ * caller is responsible for maintaining a variable containing the length of
+ * the array, as well as freeing of the array after use.
+ *
+ * The first system stores pointers to values in a block of dynamically
+ * allocated memory. Since only pointers are stored, the function does not need
+ * to know the size of the type. Both av_dynarray_add() and
+ * av_dynarray_add_nofree() implement this system.
+ *
+ * @code
+ * type **array = NULL; //< an array of pointers to values
+ * int nb = 0; //< a variable to keep track of the length of the array
+ *
+ * type to_be_added = ...;
+ * type to_be_added2 = ...;
+ *
+ * av_dynarray_add(&array, &nb, &to_be_added);
+ * if (nb == 0)
+ * return AVERROR(ENOMEM);
+ *
+ * av_dynarray_add(&array, &nb, &to_be_added2);
+ * if (nb == 0)
+ * return AVERROR(ENOMEM);
+ *
+ * // Now:
+ * // nb == 2
+ * // &to_be_added == array[0]
+ * // &to_be_added2 == array[1]
+ *
+ * av_freep(&array);
+ * @endcode
+ *
+ * The second system stores the value directly in a block of memory. As a
+ * result, the function has to know the size of the type. av_dynarray2_add()
+ * implements this mechanism.
+ *
+ * @code
+ * type *array = NULL; //< an array of values
+ * int nb = 0; //< a variable to keep track of the length of the array
+ *
+ * type to_be_added = ...;
+ * type to_be_added2 = ...;
+ *
+ * type *addr = av_dynarray2_add((void **)&array, &nb, sizeof(*array), NULL);
+ * if (!addr)
+ * return AVERROR(ENOMEM);
+ * memcpy(addr, &to_be_added, sizeof(to_be_added));
+ *
+ * // Shortcut of the above.
+ * type *addr = av_dynarray2_add((void **)&array, &nb, sizeof(*array),
+ * (const void *)&to_be_added2);
+ * if (!addr)
+ * return AVERROR(ENOMEM);
+ *
+ * // Now:
+ * // nb == 2
+ * // to_be_added == array[0]
+ * // to_be_added2 == array[1]
+ *
+ * av_freep(&array);
+ * @endcode
+ *
+ * @{
+ */
+
+/**
+ * Add the pointer to an element to a dynamic array.
+ *
+ * The array to grow is supposed to be an array of pointers to
+ * structures, and the element to add must be a pointer to an already
+ * allocated structure.
+ *
+ * The array is reallocated when its size reaches powers of 2.
+ * Therefore, the amortized cost of adding an element is constant.
+ *
+ * In case of success, the pointer to the array is updated in order to
+ * point to the new grown array, and the number pointed to by `nb_ptr`
+ * is incremented.
+ * In case of failure, the array is freed, `*tab_ptr` is set to `NULL` and
+ * `*nb_ptr` is set to 0.
+ *
+ * @param[in,out] tab_ptr Pointer to the array to grow
+ * @param[in,out] nb_ptr Pointer to the number of elements in the array
+ * @param[in] elem Element to add
+ * @see av_dynarray_add_nofree(), av_dynarray2_add()
+ */
+void av_dynarray_add(void* tab_ptr, int* nb_ptr, void* elem);
+
+/**
+ * Add an element to a dynamic array.
+ *
+ * Function has the same functionality as av_dynarray_add(),
+ * but it doesn't free memory on fails. It returns error code
+ * instead and leave current buffer untouched.
+ *
+ * @return >=0 on success, negative otherwise
+ * @see av_dynarray_add(), av_dynarray2_add()
+ */
+av_warn_unused_result int av_dynarray_add_nofree(void* tab_ptr, int* nb_ptr,
+ void* elem);
+
+/**
+ * Add an element of size `elem_size` to a dynamic array.
+ *
+ * The array is reallocated when its number of elements reaches powers of 2.
+ * Therefore, the amortized cost of adding an element is constant.
+ *
+ * In case of success, the pointer to the array is updated in order to
+ * point to the new grown array, and the number pointed to by `nb_ptr`
+ * is incremented.
+ * In case of failure, the array is freed, `*tab_ptr` is set to `NULL` and
+ * `*nb_ptr` is set to 0.
+ *
+ * @param[in,out] tab_ptr Pointer to the array to grow
+ * @param[in,out] nb_ptr Pointer to the number of elements in the array
+ * @param[in] elem_size Size in bytes of an element in the array
+ * @param[in] elem_data Pointer to the data of the element to add. If
+ * `NULL`, the space of the newly added element is
+ * allocated but left uninitialized.
+ *
+ * @return Pointer to the data of the element to copy in the newly allocated
+ * space
+ * @see av_dynarray_add(), av_dynarray_add_nofree()
+ */
+void* av_dynarray2_add(void** tab_ptr, int* nb_ptr, size_t elem_size,
+ const uint8_t* elem_data);
+
+/**
+ * @}
+ */
+
+/**
+ * @defgroup lavu_mem_misc Miscellaneous Functions
+ *
+ * Other functions related to memory allocation.
+ *
+ * @{
+ */
+
+/**
+ * Multiply two `size_t` values checking for overflow.
+ *
+ * @param[in] a Operand of multiplication
+ * @param[in] b Operand of multiplication
+ * @param[out] r Pointer to the result of the operation
+ * @return 0 on success, AVERROR(EINVAL) on overflow
+ */
+int av_size_mult(size_t a, size_t b, size_t* r);
+
+/**
+ * Set the maximum size that may be allocated in one block.
+ *
+ * The value specified with this function is effective for all libavutil's @ref
+ * lavu_mem_funcs "heap management functions."
+ *
+ * By default, the max value is defined as `INT_MAX`.
+ *
+ * @param max Value to be set as the new maximum size
+ *
+ * @warning Exercise extreme caution when using this function. Don't touch
+ * this if you do not understand the full consequence of doing so.
+ */
+void av_max_alloc(size_t max);
+
+/**
+ * @}
+ * @}
+ */
+
+#endif /* AVUTIL_MEM_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/pixfmt.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/pixfmt.h
new file mode 100644
index 0000000000..b7516d9e3b
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/pixfmt.h
@@ -0,0 +1,891 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_PIXFMT_H
+#define AVUTIL_PIXFMT_H
+
+/**
+ * @file
+ * pixel format definitions
+ */
+
+#include "libavutil/avconfig.h"
+#include "version.h"
+
+#define AVPALETTE_SIZE 1024
+#define AVPALETTE_COUNT 256
+
+/**
+ * Pixel format.
+ *
+ * @note
+ * AV_PIX_FMT_RGB32 is handled in an endian-specific manner. An RGBA
+ * color is put together as:
+ * (A << 24) | (R << 16) | (G << 8) | B
+ * This is stored as BGRA on little-endian CPU architectures and ARGB on
+ * big-endian CPUs.
+ *
+ * @note
+ * If the resolution is not a multiple of the chroma subsampling factor
+ * then the chroma plane resolution must be rounded up.
+ *
+ * @par
+ * When the pixel format is palettized RGB32 (AV_PIX_FMT_PAL8), the palettized
+ * image data is stored in AVFrame.data[0]. The palette is transported in
+ * AVFrame.data[1], is 1024 bytes long (256 4-byte entries) and is
+ * formatted the same as in AV_PIX_FMT_RGB32 described above (i.e., it is
+ * also endian-specific). Note also that the individual RGB32 palette
+ * components stored in AVFrame.data[1] should be in the range 0..255.
+ * This is important as many custom PAL8 video codecs that were designed
+ * to run on the IBM VGA graphics adapter use 6-bit palette components.
+ *
+ * @par
+ * For all the 8 bits per pixel formats, an RGB32 palette is in data[1] like
+ * for pal8. This palette is filled in automatically by the function
+ * allocating the picture.
+ */
+enum AVPixelFormat {
+ AV_PIX_FMT_NONE = -1,
+ AV_PIX_FMT_YUV420P, ///< planar YUV 4:2:0, 12bpp, (1 Cr & Cb sample per 2x2 Y
+ ///< samples)
+ AV_PIX_FMT_YUYV422, ///< packed YUV 4:2:2, 16bpp, Y0 Cb Y1 Cr
+ AV_PIX_FMT_RGB24, ///< packed RGB 8:8:8, 24bpp, RGBRGB...
+ AV_PIX_FMT_BGR24, ///< packed RGB 8:8:8, 24bpp, BGRBGR...
+ AV_PIX_FMT_YUV422P, ///< planar YUV 4:2:2, 16bpp, (1 Cr & Cb sample per 2x1 Y
+ ///< samples)
+ AV_PIX_FMT_YUV444P, ///< planar YUV 4:4:4, 24bpp, (1 Cr & Cb sample per 1x1 Y
+ ///< samples)
+ AV_PIX_FMT_YUV410P, ///< planar YUV 4:1:0, 9bpp, (1 Cr & Cb sample per 4x4 Y
+ ///< samples)
+ AV_PIX_FMT_YUV411P, ///< planar YUV 4:1:1, 12bpp, (1 Cr & Cb sample per 4x1 Y
+ ///< samples)
+ AV_PIX_FMT_GRAY8, ///< Y , 8bpp
+ AV_PIX_FMT_MONOWHITE, ///< Y , 1bpp, 0 is white, 1 is black,
+ ///< in each byte pixels are ordered from the
+ ///< msb to the lsb
+ AV_PIX_FMT_MONOBLACK, ///< Y , 1bpp, 0 is black, 1 is white,
+ ///< in each byte pixels are ordered from the
+ ///< msb to the lsb
+ AV_PIX_FMT_PAL8, ///< 8 bits with AV_PIX_FMT_RGB32 palette
+ AV_PIX_FMT_YUVJ420P, ///< planar YUV 4:2:0, 12bpp, full scale (JPEG),
+ ///< deprecated in favor of AV_PIX_FMT_YUV420P and
+ ///< setting color_range
+ AV_PIX_FMT_YUVJ422P, ///< planar YUV 4:2:2, 16bpp, full scale (JPEG),
+ ///< deprecated in favor of AV_PIX_FMT_YUV422P and
+ ///< setting color_range
+ AV_PIX_FMT_YUVJ444P, ///< planar YUV 4:4:4, 24bpp, full scale (JPEG),
+ ///< deprecated in favor of AV_PIX_FMT_YUV444P and
+ ///< setting color_range
+ AV_PIX_FMT_UYVY422, ///< packed YUV 4:2:2, 16bpp, Cb Y0 Cr Y1
+ AV_PIX_FMT_UYYVYY411, ///< packed YUV 4:1:1, 12bpp, Cb Y0 Y1 Cr Y2 Y3
+ AV_PIX_FMT_BGR8, ///< packed RGB 3:3:2, 8bpp, (msb)2B 3G 3R(lsb)
+ AV_PIX_FMT_BGR4, ///< packed RGB 1:2:1 bitstream, 4bpp, (msb)1B 2G 1R(lsb),
+ ///< a byte contains two pixels, the first pixel in the byte
+ ///< is the one composed by the 4 msb bits
+ AV_PIX_FMT_BGR4_BYTE, ///< packed RGB 1:2:1, 8bpp, (msb)1B 2G 1R(lsb)
+ AV_PIX_FMT_RGB8, ///< packed RGB 3:3:2, 8bpp, (msb)2R 3G 3B(lsb)
+ AV_PIX_FMT_RGB4, ///< packed RGB 1:2:1 bitstream, 4bpp, (msb)1R 2G 1B(lsb),
+ ///< a byte contains two pixels, the first pixel in the byte
+ ///< is the one composed by the 4 msb bits
+ AV_PIX_FMT_RGB4_BYTE, ///< packed RGB 1:2:1, 8bpp, (msb)1R 2G 1B(lsb)
+ AV_PIX_FMT_NV12, ///< planar YUV 4:2:0, 12bpp, 1 plane for Y and 1 plane for
+ ///< the UV components, which are interleaved (first byte U
+ ///< and the following byte V)
+ AV_PIX_FMT_NV21, ///< as above, but U and V bytes are swapped
+
+ AV_PIX_FMT_ARGB, ///< packed ARGB 8:8:8:8, 32bpp, ARGBARGB...
+ AV_PIX_FMT_RGBA, ///< packed RGBA 8:8:8:8, 32bpp, RGBARGBA...
+ AV_PIX_FMT_ABGR, ///< packed ABGR 8:8:8:8, 32bpp, ABGRABGR...
+ AV_PIX_FMT_BGRA, ///< packed BGRA 8:8:8:8, 32bpp, BGRABGRA...
+
+ AV_PIX_FMT_GRAY16BE, ///< Y , 16bpp, big-endian
+ AV_PIX_FMT_GRAY16LE, ///< Y , 16bpp, little-endian
+ AV_PIX_FMT_YUV440P, ///< planar YUV 4:4:0 (1 Cr & Cb sample per 1x2 Y
+ ///< samples)
+ AV_PIX_FMT_YUVJ440P, ///< planar YUV 4:4:0 full scale (JPEG), deprecated in
+ ///< favor of AV_PIX_FMT_YUV440P and setting color_range
+ AV_PIX_FMT_YUVA420P, ///< planar YUV 4:2:0, 20bpp, (1 Cr & Cb sample per 2x2
+ ///< Y & A samples)
+ AV_PIX_FMT_RGB48BE, ///< packed RGB 16:16:16, 48bpp, 16R, 16G, 16B, the
+ ///< 2-byte value for each R/G/B component is stored as
+ ///< big-endian
+ AV_PIX_FMT_RGB48LE, ///< packed RGB 16:16:16, 48bpp, 16R, 16G, 16B, the
+ ///< 2-byte value for each R/G/B component is stored as
+ ///< little-endian
+
+ AV_PIX_FMT_RGB565BE, ///< packed RGB 5:6:5, 16bpp, (msb) 5R 6G 5B(lsb),
+ ///< big-endian
+ AV_PIX_FMT_RGB565LE, ///< packed RGB 5:6:5, 16bpp, (msb) 5R 6G 5B(lsb),
+ ///< little-endian
+ AV_PIX_FMT_RGB555BE, ///< packed RGB 5:5:5, 16bpp, (msb)1X 5R 5G 5B(lsb),
+ ///< big-endian , X=unused/undefined
+ AV_PIX_FMT_RGB555LE, ///< packed RGB 5:5:5, 16bpp, (msb)1X 5R 5G 5B(lsb),
+ ///< little-endian, X=unused/undefined
+
+ AV_PIX_FMT_BGR565BE, ///< packed BGR 5:6:5, 16bpp, (msb) 5B 6G 5R(lsb),
+ ///< big-endian
+ AV_PIX_FMT_BGR565LE, ///< packed BGR 5:6:5, 16bpp, (msb) 5B 6G 5R(lsb),
+ ///< little-endian
+ AV_PIX_FMT_BGR555BE, ///< packed BGR 5:5:5, 16bpp, (msb)1X 5B 5G 5R(lsb),
+ ///< big-endian , X=unused/undefined
+ AV_PIX_FMT_BGR555LE, ///< packed BGR 5:5:5, 16bpp, (msb)1X 5B 5G 5R(lsb),
+ ///< little-endian, X=unused/undefined
+
+ /**
+ * Hardware acceleration through VA-API, data[3] contains a
+ * VASurfaceID.
+ */
+ AV_PIX_FMT_VAAPI,
+
+ AV_PIX_FMT_YUV420P16LE, ///< planar YUV 4:2:0, 24bpp, (1 Cr & Cb sample per
+ ///< 2x2 Y samples), little-endian
+ AV_PIX_FMT_YUV420P16BE, ///< planar YUV 4:2:0, 24bpp, (1 Cr & Cb sample per
+ ///< 2x2 Y samples), big-endian
+ AV_PIX_FMT_YUV422P16LE, ///< planar YUV 4:2:2, 32bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y samples), little-endian
+ AV_PIX_FMT_YUV422P16BE, ///< planar YUV 4:2:2, 32bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y samples), big-endian
+ AV_PIX_FMT_YUV444P16LE, ///< planar YUV 4:4:4, 48bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y samples), little-endian
+ AV_PIX_FMT_YUV444P16BE, ///< planar YUV 4:4:4, 48bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y samples), big-endian
+ AV_PIX_FMT_DXVA2_VLD, ///< HW decoding through DXVA2, Picture.data[3]
+ ///< contains a LPDIRECT3DSURFACE9 pointer
+
+ AV_PIX_FMT_RGB444LE, ///< packed RGB 4:4:4, 16bpp, (msb)4X 4R 4G 4B(lsb),
+ ///< little-endian, X=unused/undefined
+ AV_PIX_FMT_RGB444BE, ///< packed RGB 4:4:4, 16bpp, (msb)4X 4R 4G 4B(lsb),
+ ///< big-endian, X=unused/undefined
+ AV_PIX_FMT_BGR444LE, ///< packed BGR 4:4:4, 16bpp, (msb)4X 4B 4G 4R(lsb),
+ ///< little-endian, X=unused/undefined
+ AV_PIX_FMT_BGR444BE, ///< packed BGR 4:4:4, 16bpp, (msb)4X 4B 4G 4R(lsb),
+ ///< big-endian, X=unused/undefined
+ AV_PIX_FMT_YA8, ///< 8 bits gray, 8 bits alpha
+
+ AV_PIX_FMT_Y400A = AV_PIX_FMT_YA8, ///< alias for AV_PIX_FMT_YA8
+ AV_PIX_FMT_GRAY8A = AV_PIX_FMT_YA8, ///< alias for AV_PIX_FMT_YA8
+
+ AV_PIX_FMT_BGR48BE, ///< packed RGB 16:16:16, 48bpp, 16B, 16G, 16R, the
+ ///< 2-byte value for each R/G/B component is stored as
+ ///< big-endian
+ AV_PIX_FMT_BGR48LE, ///< packed RGB 16:16:16, 48bpp, 16B, 16G, 16R, the
+ ///< 2-byte value for each R/G/B component is stored as
+ ///< little-endian
+
+ /**
+ * The following 12 formats have the disadvantage of needing 1 format for each
+ * bit depth. Notice that each 9/10 bits sample is stored in 16 bits with
+ * extra padding. If you want to support multiple bit depths, then using
+ * AV_PIX_FMT_YUV420P16* with the bpp stored separately is better.
+ */
+ AV_PIX_FMT_YUV420P9BE, ///< planar YUV 4:2:0, 13.5bpp, (1 Cr & Cb sample per
+ ///< 2x2 Y samples), big-endian
+ AV_PIX_FMT_YUV420P9LE, ///< planar YUV 4:2:0, 13.5bpp, (1 Cr & Cb sample per
+ ///< 2x2 Y samples), little-endian
+ AV_PIX_FMT_YUV420P10BE, ///< planar YUV 4:2:0, 15bpp, (1 Cr & Cb sample per
+ ///< 2x2 Y samples), big-endian
+ AV_PIX_FMT_YUV420P10LE, ///< planar YUV 4:2:0, 15bpp, (1 Cr & Cb sample per
+ ///< 2x2 Y samples), little-endian
+ AV_PIX_FMT_YUV422P10BE, ///< planar YUV 4:2:2, 20bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y samples), big-endian
+ AV_PIX_FMT_YUV422P10LE, ///< planar YUV 4:2:2, 20bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y samples), little-endian
+ AV_PIX_FMT_YUV444P9BE, ///< planar YUV 4:4:4, 27bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y samples), big-endian
+ AV_PIX_FMT_YUV444P9LE, ///< planar YUV 4:4:4, 27bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y samples), little-endian
+ AV_PIX_FMT_YUV444P10BE, ///< planar YUV 4:4:4, 30bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y samples), big-endian
+ AV_PIX_FMT_YUV444P10LE, ///< planar YUV 4:4:4, 30bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y samples), little-endian
+ AV_PIX_FMT_YUV422P9BE, ///< planar YUV 4:2:2, 18bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y samples), big-endian
+ AV_PIX_FMT_YUV422P9LE, ///< planar YUV 4:2:2, 18bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y samples), little-endian
+ AV_PIX_FMT_GBRP, ///< planar GBR 4:4:4 24bpp
+ AV_PIX_FMT_GBR24P = AV_PIX_FMT_GBRP, // alias for #AV_PIX_FMT_GBRP
+ AV_PIX_FMT_GBRP9BE, ///< planar GBR 4:4:4 27bpp, big-endian
+ AV_PIX_FMT_GBRP9LE, ///< planar GBR 4:4:4 27bpp, little-endian
+ AV_PIX_FMT_GBRP10BE, ///< planar GBR 4:4:4 30bpp, big-endian
+ AV_PIX_FMT_GBRP10LE, ///< planar GBR 4:4:4 30bpp, little-endian
+ AV_PIX_FMT_GBRP16BE, ///< planar GBR 4:4:4 48bpp, big-endian
+ AV_PIX_FMT_GBRP16LE, ///< planar GBR 4:4:4 48bpp, little-endian
+ AV_PIX_FMT_YUVA422P, ///< planar YUV 4:2:2 24bpp, (1 Cr & Cb sample per 2x1 Y
+ ///< & A samples)
+ AV_PIX_FMT_YUVA444P, ///< planar YUV 4:4:4 32bpp, (1 Cr & Cb sample per 1x1 Y
+ ///< & A samples)
+ AV_PIX_FMT_YUVA420P9BE, ///< planar YUV 4:2:0 22.5bpp, (1 Cr & Cb sample per
+ ///< 2x2 Y & A samples), big-endian
+ AV_PIX_FMT_YUVA420P9LE, ///< planar YUV 4:2:0 22.5bpp, (1 Cr & Cb sample per
+ ///< 2x2 Y & A samples), little-endian
+ AV_PIX_FMT_YUVA422P9BE, ///< planar YUV 4:2:2 27bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y & A samples), big-endian
+ AV_PIX_FMT_YUVA422P9LE, ///< planar YUV 4:2:2 27bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y & A samples), little-endian
+ AV_PIX_FMT_YUVA444P9BE, ///< planar YUV 4:4:4 36bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y & A samples), big-endian
+ AV_PIX_FMT_YUVA444P9LE, ///< planar YUV 4:4:4 36bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y & A samples), little-endian
+ AV_PIX_FMT_YUVA420P10BE, ///< planar YUV 4:2:0 25bpp, (1 Cr & Cb sample per
+ ///< 2x2 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA420P10LE, ///< planar YUV 4:2:0 25bpp, (1 Cr & Cb sample per
+ ///< 2x2 Y & A samples, little-endian)
+ AV_PIX_FMT_YUVA422P10BE, ///< planar YUV 4:2:2 30bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA422P10LE, ///< planar YUV 4:2:2 30bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y & A samples, little-endian)
+ AV_PIX_FMT_YUVA444P10BE, ///< planar YUV 4:4:4 40bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA444P10LE, ///< planar YUV 4:4:4 40bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y & A samples, little-endian)
+ AV_PIX_FMT_YUVA420P16BE, ///< planar YUV 4:2:0 40bpp, (1 Cr & Cb sample per
+ ///< 2x2 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA420P16LE, ///< planar YUV 4:2:0 40bpp, (1 Cr & Cb sample per
+ ///< 2x2 Y & A samples, little-endian)
+ AV_PIX_FMT_YUVA422P16BE, ///< planar YUV 4:2:2 48bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA422P16LE, ///< planar YUV 4:2:2 48bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y & A samples, little-endian)
+ AV_PIX_FMT_YUVA444P16BE, ///< planar YUV 4:4:4 64bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA444P16LE, ///< planar YUV 4:4:4 64bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y & A samples, little-endian)
+
+ AV_PIX_FMT_VDPAU, ///< HW acceleration through VDPAU, Picture.data[3]
+ ///< contains a VdpVideoSurface
+
+ AV_PIX_FMT_XYZ12LE, ///< packed XYZ 4:4:4, 36 bpp, (msb) 12X, 12Y, 12Z (lsb),
+ ///< the 2-byte value for each X/Y/Z is stored as
+ ///< little-endian, the 4 lower bits are set to 0
+ AV_PIX_FMT_XYZ12BE, ///< packed XYZ 4:4:4, 36 bpp, (msb) 12X, 12Y, 12Z (lsb),
+ ///< the 2-byte value for each X/Y/Z is stored as
+ ///< big-endian, the 4 lower bits are set to 0
+ AV_PIX_FMT_NV16, ///< interleaved chroma YUV 4:2:2, 16bpp, (1 Cr & Cb sample
+ ///< per 2x1 Y samples)
+ AV_PIX_FMT_NV20LE, ///< interleaved chroma YUV 4:2:2, 20bpp, (1 Cr & Cb
+ ///< sample per 2x1 Y samples), little-endian
+ AV_PIX_FMT_NV20BE, ///< interleaved chroma YUV 4:2:2, 20bpp, (1 Cr & Cb
+ ///< sample per 2x1 Y samples), big-endian
+
+ AV_PIX_FMT_RGBA64BE, ///< packed RGBA 16:16:16:16, 64bpp, 16R, 16G, 16B, 16A,
+ ///< the 2-byte value for each R/G/B/A component is
+ ///< stored as big-endian
+ AV_PIX_FMT_RGBA64LE, ///< packed RGBA 16:16:16:16, 64bpp, 16R, 16G, 16B, 16A,
+ ///< the 2-byte value for each R/G/B/A component is
+ ///< stored as little-endian
+ AV_PIX_FMT_BGRA64BE, ///< packed RGBA 16:16:16:16, 64bpp, 16B, 16G, 16R, 16A,
+ ///< the 2-byte value for each R/G/B/A component is
+ ///< stored as big-endian
+ AV_PIX_FMT_BGRA64LE, ///< packed RGBA 16:16:16:16, 64bpp, 16B, 16G, 16R, 16A,
+ ///< the 2-byte value for each R/G/B/A component is
+ ///< stored as little-endian
+
+ AV_PIX_FMT_YVYU422, ///< packed YUV 4:2:2, 16bpp, Y0 Cr Y1 Cb
+
+ AV_PIX_FMT_YA16BE, ///< 16 bits gray, 16 bits alpha (big-endian)
+ AV_PIX_FMT_YA16LE, ///< 16 bits gray, 16 bits alpha (little-endian)
+
+ AV_PIX_FMT_GBRAP, ///< planar GBRA 4:4:4:4 32bpp
+ AV_PIX_FMT_GBRAP16BE, ///< planar GBRA 4:4:4:4 64bpp, big-endian
+ AV_PIX_FMT_GBRAP16LE, ///< planar GBRA 4:4:4:4 64bpp, little-endian
+ /**
+ * HW acceleration through QSV, data[3] contains a pointer to the
+ * mfxFrameSurface1 structure.
+ *
+ * Before FFmpeg 5.0:
+ * mfxFrameSurface1.Data.MemId contains a pointer when importing
+ * the following frames as QSV frames:
+ *
+ * VAAPI:
+ * mfxFrameSurface1.Data.MemId contains a pointer to VASurfaceID
+ *
+ * DXVA2:
+ * mfxFrameSurface1.Data.MemId contains a pointer to IDirect3DSurface9
+ *
+ * FFmpeg 5.0 and above:
+ * mfxFrameSurface1.Data.MemId contains a pointer to the mfxHDLPair
+ * structure when importing the following frames as QSV frames:
+ *
+ * VAAPI:
+ * mfxHDLPair.first contains a VASurfaceID pointer.
+ * mfxHDLPair.second is always MFX_INFINITE.
+ *
+ * DXVA2:
+ * mfxHDLPair.first contains IDirect3DSurface9 pointer.
+ * mfxHDLPair.second is always MFX_INFINITE.
+ *
+ * D3D11:
+ * mfxHDLPair.first contains a ID3D11Texture2D pointer.
+ * mfxHDLPair.second contains the texture array index of the frame if the
+ * ID3D11Texture2D is an array texture, or always MFX_INFINITE if it is a
+ * normal texture.
+ */
+ AV_PIX_FMT_QSV,
+ /**
+ * HW acceleration though MMAL, data[3] contains a pointer to the
+ * MMAL_BUFFER_HEADER_T structure.
+ */
+ AV_PIX_FMT_MMAL,
+
+ AV_PIX_FMT_D3D11VA_VLD, ///< HW decoding through Direct3D11 via old API,
+ ///< Picture.data[3] contains a
+ ///< ID3D11VideoDecoderOutputView pointer
+
+ /**
+ * HW acceleration through CUDA. data[i] contain CUdeviceptr pointers
+ * exactly as for system memory frames.
+ */
+ AV_PIX_FMT_CUDA,
+
+ AV_PIX_FMT_0RGB, ///< packed RGB 8:8:8, 32bpp, XRGBXRGB... X=unused/undefined
+ AV_PIX_FMT_RGB0, ///< packed RGB 8:8:8, 32bpp, RGBXRGBX... X=unused/undefined
+ AV_PIX_FMT_0BGR, ///< packed BGR 8:8:8, 32bpp, XBGRXBGR... X=unused/undefined
+ AV_PIX_FMT_BGR0, ///< packed BGR 8:8:8, 32bpp, BGRXBGRX... X=unused/undefined
+
+ AV_PIX_FMT_YUV420P12BE, ///< planar YUV 4:2:0,18bpp, (1 Cr & Cb sample per
+ ///< 2x2 Y samples), big-endian
+ AV_PIX_FMT_YUV420P12LE, ///< planar YUV 4:2:0,18bpp, (1 Cr & Cb sample per
+ ///< 2x2 Y samples), little-endian
+ AV_PIX_FMT_YUV420P14BE, ///< planar YUV 4:2:0,21bpp, (1 Cr & Cb sample per
+ ///< 2x2 Y samples), big-endian
+ AV_PIX_FMT_YUV420P14LE, ///< planar YUV 4:2:0,21bpp, (1 Cr & Cb sample per
+ ///< 2x2 Y samples), little-endian
+ AV_PIX_FMT_YUV422P12BE, ///< planar YUV 4:2:2,24bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y samples), big-endian
+ AV_PIX_FMT_YUV422P12LE, ///< planar YUV 4:2:2,24bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y samples), little-endian
+ AV_PIX_FMT_YUV422P14BE, ///< planar YUV 4:2:2,28bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y samples), big-endian
+ AV_PIX_FMT_YUV422P14LE, ///< planar YUV 4:2:2,28bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y samples), little-endian
+ AV_PIX_FMT_YUV444P12BE, ///< planar YUV 4:4:4,36bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y samples), big-endian
+ AV_PIX_FMT_YUV444P12LE, ///< planar YUV 4:4:4,36bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y samples), little-endian
+ AV_PIX_FMT_YUV444P14BE, ///< planar YUV 4:4:4,42bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y samples), big-endian
+ AV_PIX_FMT_YUV444P14LE, ///< planar YUV 4:4:4,42bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y samples), little-endian
+ AV_PIX_FMT_GBRP12BE, ///< planar GBR 4:4:4 36bpp, big-endian
+ AV_PIX_FMT_GBRP12LE, ///< planar GBR 4:4:4 36bpp, little-endian
+ AV_PIX_FMT_GBRP14BE, ///< planar GBR 4:4:4 42bpp, big-endian
+ AV_PIX_FMT_GBRP14LE, ///< planar GBR 4:4:4 42bpp, little-endian
+ AV_PIX_FMT_YUVJ411P, ///< planar YUV 4:1:1, 12bpp, (1 Cr & Cb sample per 4x1
+ ///< Y samples) full scale (JPEG), deprecated in favor
+ ///< of AV_PIX_FMT_YUV411P and setting color_range
+
+ AV_PIX_FMT_BAYER_BGGR8, ///< bayer, BGBG..(odd line), GRGR..(even line),
+ ///< 8-bit samples
+ AV_PIX_FMT_BAYER_RGGB8, ///< bayer, RGRG..(odd line), GBGB..(even line),
+ ///< 8-bit samples
+ AV_PIX_FMT_BAYER_GBRG8, ///< bayer, GBGB..(odd line), RGRG..(even line),
+ ///< 8-bit samples
+ AV_PIX_FMT_BAYER_GRBG8, ///< bayer, GRGR..(odd line), BGBG..(even line),
+ ///< 8-bit samples
+ AV_PIX_FMT_BAYER_BGGR16LE, ///< bayer, BGBG..(odd line), GRGR..(even line),
+ ///< 16-bit samples, little-endian
+ AV_PIX_FMT_BAYER_BGGR16BE, ///< bayer, BGBG..(odd line), GRGR..(even line),
+ ///< 16-bit samples, big-endian
+ AV_PIX_FMT_BAYER_RGGB16LE, ///< bayer, RGRG..(odd line), GBGB..(even line),
+ ///< 16-bit samples, little-endian
+ AV_PIX_FMT_BAYER_RGGB16BE, ///< bayer, RGRG..(odd line), GBGB..(even line),
+ ///< 16-bit samples, big-endian
+ AV_PIX_FMT_BAYER_GBRG16LE, ///< bayer, GBGB..(odd line), RGRG..(even line),
+ ///< 16-bit samples, little-endian
+ AV_PIX_FMT_BAYER_GBRG16BE, ///< bayer, GBGB..(odd line), RGRG..(even line),
+ ///< 16-bit samples, big-endian
+ AV_PIX_FMT_BAYER_GRBG16LE, ///< bayer, GRGR..(odd line), BGBG..(even line),
+ ///< 16-bit samples, little-endian
+ AV_PIX_FMT_BAYER_GRBG16BE, ///< bayer, GRGR..(odd line), BGBG..(even line),
+ ///< 16-bit samples, big-endian
+
+#if FF_API_XVMC
+ AV_PIX_FMT_XVMC, ///< XVideo Motion Acceleration via common packet passing
+#endif
+
+ AV_PIX_FMT_YUV440P10LE, ///< planar YUV 4:4:0,20bpp, (1 Cr & Cb sample per
+ ///< 1x2 Y samples), little-endian
+ AV_PIX_FMT_YUV440P10BE, ///< planar YUV 4:4:0,20bpp, (1 Cr & Cb sample per
+ ///< 1x2 Y samples), big-endian
+ AV_PIX_FMT_YUV440P12LE, ///< planar YUV 4:4:0,24bpp, (1 Cr & Cb sample per
+ ///< 1x2 Y samples), little-endian
+ AV_PIX_FMT_YUV440P12BE, ///< planar YUV 4:4:0,24bpp, (1 Cr & Cb sample per
+ ///< 1x2 Y samples), big-endian
+ AV_PIX_FMT_AYUV64LE, ///< packed AYUV 4:4:4,64bpp (1 Cr & Cb sample per 1x1 Y
+ ///< & A samples), little-endian
+ AV_PIX_FMT_AYUV64BE, ///< packed AYUV 4:4:4,64bpp (1 Cr & Cb sample per 1x1 Y
+ ///< & A samples), big-endian
+
+ AV_PIX_FMT_VIDEOTOOLBOX, ///< hardware decoding through Videotoolbox
+
+ AV_PIX_FMT_P010LE, ///< like NV12, with 10bpp per component, data in the high
+ ///< bits, zeros in the low bits, little-endian
+ AV_PIX_FMT_P010BE, ///< like NV12, with 10bpp per component, data in the high
+ ///< bits, zeros in the low bits, big-endian
+
+ AV_PIX_FMT_GBRAP12BE, ///< planar GBR 4:4:4:4 48bpp, big-endian
+ AV_PIX_FMT_GBRAP12LE, ///< planar GBR 4:4:4:4 48bpp, little-endian
+
+ AV_PIX_FMT_GBRAP10BE, ///< planar GBR 4:4:4:4 40bpp, big-endian
+ AV_PIX_FMT_GBRAP10LE, ///< planar GBR 4:4:4:4 40bpp, little-endian
+
+ AV_PIX_FMT_MEDIACODEC, ///< hardware decoding through MediaCodec
+
+ AV_PIX_FMT_GRAY12BE, ///< Y , 12bpp, big-endian
+ AV_PIX_FMT_GRAY12LE, ///< Y , 12bpp, little-endian
+ AV_PIX_FMT_GRAY10BE, ///< Y , 10bpp, big-endian
+ AV_PIX_FMT_GRAY10LE, ///< Y , 10bpp, little-endian
+
+ AV_PIX_FMT_P016LE, ///< like NV12, with 16bpp per component, little-endian
+ AV_PIX_FMT_P016BE, ///< like NV12, with 16bpp per component, big-endian
+
+ /**
+ * Hardware surfaces for Direct3D11.
+ *
+ * This is preferred over the legacy AV_PIX_FMT_D3D11VA_VLD. The new D3D11
+ * hwaccel API and filtering support AV_PIX_FMT_D3D11 only.
+ *
+ * data[0] contains a ID3D11Texture2D pointer, and data[1] contains the
+ * texture array index of the frame as intptr_t if the ID3D11Texture2D is
+ * an array texture (or always 0 if it's a normal texture).
+ */
+ AV_PIX_FMT_D3D11,
+
+ AV_PIX_FMT_GRAY9BE, ///< Y , 9bpp, big-endian
+ AV_PIX_FMT_GRAY9LE, ///< Y , 9bpp, little-endian
+
+ AV_PIX_FMT_GBRPF32BE, ///< IEEE-754 single precision planar GBR 4:4:4, 96bpp,
+ ///< big-endian
+ AV_PIX_FMT_GBRPF32LE, ///< IEEE-754 single precision planar GBR 4:4:4, 96bpp,
+ ///< little-endian
+ AV_PIX_FMT_GBRAPF32BE, ///< IEEE-754 single precision planar GBRA 4:4:4:4,
+ ///< 128bpp, big-endian
+ AV_PIX_FMT_GBRAPF32LE, ///< IEEE-754 single precision planar GBRA 4:4:4:4,
+ ///< 128bpp, little-endian
+
+ /**
+ * DRM-managed buffers exposed through PRIME buffer sharing.
+ *
+ * data[0] points to an AVDRMFrameDescriptor.
+ */
+ AV_PIX_FMT_DRM_PRIME,
+ /**
+ * Hardware surfaces for OpenCL.
+ *
+ * data[i] contain 2D image objects (typed in C as cl_mem, used
+ * in OpenCL as image2d_t) for each plane of the surface.
+ */
+ AV_PIX_FMT_OPENCL,
+
+ AV_PIX_FMT_GRAY14BE, ///< Y , 14bpp, big-endian
+ AV_PIX_FMT_GRAY14LE, ///< Y , 14bpp, little-endian
+
+ AV_PIX_FMT_GRAYF32BE, ///< IEEE-754 single precision Y, 32bpp, big-endian
+ AV_PIX_FMT_GRAYF32LE, ///< IEEE-754 single precision Y, 32bpp, little-endian
+
+ AV_PIX_FMT_YUVA422P12BE, ///< planar YUV 4:2:2,24bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y samples), 12b alpha, big-endian
+ AV_PIX_FMT_YUVA422P12LE, ///< planar YUV 4:2:2,24bpp, (1 Cr & Cb sample per
+ ///< 2x1 Y samples), 12b alpha, little-endian
+ AV_PIX_FMT_YUVA444P12BE, ///< planar YUV 4:4:4,36bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y samples), 12b alpha, big-endian
+ AV_PIX_FMT_YUVA444P12LE, ///< planar YUV 4:4:4,36bpp, (1 Cr & Cb sample per
+ ///< 1x1 Y samples), 12b alpha, little-endian
+
+ AV_PIX_FMT_NV24, ///< planar YUV 4:4:4, 24bpp, 1 plane for Y and 1 plane for
+ ///< the UV components, which are interleaved (first byte U
+ ///< and the following byte V)
+ AV_PIX_FMT_NV42, ///< as above, but U and V bytes are swapped
+
+ /**
+ * Vulkan hardware images.
+ *
+ * data[0] points to an AVVkFrame
+ */
+ AV_PIX_FMT_VULKAN,
+
+ AV_PIX_FMT_Y210BE, ///< packed YUV 4:2:2 like YUYV422, 20bpp, data in the
+ ///< high bits, big-endian
+ AV_PIX_FMT_Y210LE, ///< packed YUV 4:2:2 like YUYV422, 20bpp, data in the
+ ///< high bits, little-endian
+
+ AV_PIX_FMT_X2RGB10LE, ///< packed RGB 10:10:10, 30bpp, (msb)2X 10R 10G
+ ///< 10B(lsb), little-endian, X=unused/undefined
+ AV_PIX_FMT_X2RGB10BE, ///< packed RGB 10:10:10, 30bpp, (msb)2X 10R 10G
+ ///< 10B(lsb), big-endian, X=unused/undefined
+ AV_PIX_FMT_X2BGR10LE, ///< packed BGR 10:10:10, 30bpp, (msb)2X 10B 10G
+ ///< 10R(lsb), little-endian, X=unused/undefined
+ AV_PIX_FMT_X2BGR10BE, ///< packed BGR 10:10:10, 30bpp, (msb)2X 10B 10G
+ ///< 10R(lsb), big-endian, X=unused/undefined
+
+ AV_PIX_FMT_P210BE, ///< interleaved chroma YUV 4:2:2, 20bpp, data in the high
+ ///< bits, big-endian
+ AV_PIX_FMT_P210LE, ///< interleaved chroma YUV 4:2:2, 20bpp, data in the high
+ ///< bits, little-endian
+
+ AV_PIX_FMT_P410BE, ///< interleaved chroma YUV 4:4:4, 30bpp, data in the high
+ ///< bits, big-endian
+ AV_PIX_FMT_P410LE, ///< interleaved chroma YUV 4:4:4, 30bpp, data in the high
+ ///< bits, little-endian
+
+ AV_PIX_FMT_P216BE, ///< interleaved chroma YUV 4:2:2, 32bpp, big-endian
+ AV_PIX_FMT_P216LE, ///< interleaved chroma YUV 4:2:2, 32bpp, little-endian
+
+ AV_PIX_FMT_P416BE, ///< interleaved chroma YUV 4:4:4, 48bpp, big-endian
+ AV_PIX_FMT_P416LE, ///< interleaved chroma YUV 4:4:4, 48bpp, little-endian
+
+ AV_PIX_FMT_VUYA, ///< packed VUYA 4:4:4, 32bpp, VUYAVUYA...
+
+ AV_PIX_FMT_RGBAF16BE, ///< IEEE-754 half precision packed RGBA 16:16:16:16,
+ ///< 64bpp, RGBARGBA..., big-endian
+ AV_PIX_FMT_RGBAF16LE, ///< IEEE-754 half precision packed RGBA 16:16:16:16,
+ ///< 64bpp, RGBARGBA..., little-endian
+
+ AV_PIX_FMT_VUYX, ///< packed VUYX 4:4:4, 32bpp, Variant of VUYA where alpha
+ ///< channel is left undefined
+
+ AV_PIX_FMT_P012LE, ///< like NV12, with 12bpp per component, data in the high
+ ///< bits, zeros in the low bits, little-endian
+ AV_PIX_FMT_P012BE, ///< like NV12, with 12bpp per component, data in the high
+ ///< bits, zeros in the low bits, big-endian
+
+ AV_PIX_FMT_Y212BE, ///< packed YUV 4:2:2 like YUYV422, 24bpp, data in the
+ ///< high bits, zeros in the low bits, big-endian
+ AV_PIX_FMT_Y212LE, ///< packed YUV 4:2:2 like YUYV422, 24bpp, data in the
+ ///< high bits, zeros in the low bits, little-endian
+
+ AV_PIX_FMT_XV30BE, ///< packed XVYU 4:4:4, 32bpp, (msb)2X 10V 10Y 10U(lsb),
+ ///< big-endian, variant of Y410 where alpha channel is
+ ///< left undefined
+ AV_PIX_FMT_XV30LE, ///< packed XVYU 4:4:4, 32bpp, (msb)2X 10V 10Y 10U(lsb),
+ ///< little-endian, variant of Y410 where alpha channel is
+ ///< left undefined
+
+ AV_PIX_FMT_XV36BE, ///< packed XVYU 4:4:4, 48bpp, data in the high bits,
+ ///< zeros in the low bits, big-endian, variant of Y412
+ ///< where alpha channel is left undefined
+ AV_PIX_FMT_XV36LE, ///< packed XVYU 4:4:4, 48bpp, data in the high bits,
+ ///< zeros in the low bits, little-endian, variant of Y412
+ ///< where alpha channel is left undefined
+
+ AV_PIX_FMT_RGBF32BE, ///< IEEE-754 single precision packed RGB 32:32:32,
+ ///< 96bpp, RGBRGB..., big-endian
+ AV_PIX_FMT_RGBF32LE, ///< IEEE-754 single precision packed RGB 32:32:32,
+ ///< 96bpp, RGBRGB..., little-endian
+
+ AV_PIX_FMT_RGBAF32BE, ///< IEEE-754 single precision packed RGBA 32:32:32:32,
+ ///< 128bpp, RGBARGBA..., big-endian
+ AV_PIX_FMT_RGBAF32LE, ///< IEEE-754 single precision packed RGBA 32:32:32:32,
+ ///< 128bpp, RGBARGBA..., little-endian
+
+ AV_PIX_FMT_NB ///< number of pixel formats, DO NOT USE THIS if you want to
+ ///< link with shared libav* because the number of formats
+ ///< might differ between versions
+};
+
+#if AV_HAVE_BIGENDIAN
+# define AV_PIX_FMT_NE(be, le) AV_PIX_FMT_##be
+#else
+# define AV_PIX_FMT_NE(be, le) AV_PIX_FMT_##le
+#endif
+
+#define AV_PIX_FMT_RGB32 AV_PIX_FMT_NE(ARGB, BGRA)
+#define AV_PIX_FMT_RGB32_1 AV_PIX_FMT_NE(RGBA, ABGR)
+#define AV_PIX_FMT_BGR32 AV_PIX_FMT_NE(ABGR, RGBA)
+#define AV_PIX_FMT_BGR32_1 AV_PIX_FMT_NE(BGRA, ARGB)
+#define AV_PIX_FMT_0RGB32 AV_PIX_FMT_NE(0RGB, BGR0)
+#define AV_PIX_FMT_0BGR32 AV_PIX_FMT_NE(0BGR, RGB0)
+
+#define AV_PIX_FMT_GRAY9 AV_PIX_FMT_NE(GRAY9BE, GRAY9LE)
+#define AV_PIX_FMT_GRAY10 AV_PIX_FMT_NE(GRAY10BE, GRAY10LE)
+#define AV_PIX_FMT_GRAY12 AV_PIX_FMT_NE(GRAY12BE, GRAY12LE)
+#define AV_PIX_FMT_GRAY14 AV_PIX_FMT_NE(GRAY14BE, GRAY14LE)
+#define AV_PIX_FMT_GRAY16 AV_PIX_FMT_NE(GRAY16BE, GRAY16LE)
+#define AV_PIX_FMT_YA16 AV_PIX_FMT_NE(YA16BE, YA16LE)
+#define AV_PIX_FMT_RGB48 AV_PIX_FMT_NE(RGB48BE, RGB48LE)
+#define AV_PIX_FMT_RGB565 AV_PIX_FMT_NE(RGB565BE, RGB565LE)
+#define AV_PIX_FMT_RGB555 AV_PIX_FMT_NE(RGB555BE, RGB555LE)
+#define AV_PIX_FMT_RGB444 AV_PIX_FMT_NE(RGB444BE, RGB444LE)
+#define AV_PIX_FMT_RGBA64 AV_PIX_FMT_NE(RGBA64BE, RGBA64LE)
+#define AV_PIX_FMT_BGR48 AV_PIX_FMT_NE(BGR48BE, BGR48LE)
+#define AV_PIX_FMT_BGR565 AV_PIX_FMT_NE(BGR565BE, BGR565LE)
+#define AV_PIX_FMT_BGR555 AV_PIX_FMT_NE(BGR555BE, BGR555LE)
+#define AV_PIX_FMT_BGR444 AV_PIX_FMT_NE(BGR444BE, BGR444LE)
+#define AV_PIX_FMT_BGRA64 AV_PIX_FMT_NE(BGRA64BE, BGRA64LE)
+
+#define AV_PIX_FMT_YUV420P9 AV_PIX_FMT_NE(YUV420P9BE, YUV420P9LE)
+#define AV_PIX_FMT_YUV422P9 AV_PIX_FMT_NE(YUV422P9BE, YUV422P9LE)
+#define AV_PIX_FMT_YUV444P9 AV_PIX_FMT_NE(YUV444P9BE, YUV444P9LE)
+#define AV_PIX_FMT_YUV420P10 AV_PIX_FMT_NE(YUV420P10BE, YUV420P10LE)
+#define AV_PIX_FMT_YUV422P10 AV_PIX_FMT_NE(YUV422P10BE, YUV422P10LE)
+#define AV_PIX_FMT_YUV440P10 AV_PIX_FMT_NE(YUV440P10BE, YUV440P10LE)
+#define AV_PIX_FMT_YUV444P10 AV_PIX_FMT_NE(YUV444P10BE, YUV444P10LE)
+#define AV_PIX_FMT_YUV420P12 AV_PIX_FMT_NE(YUV420P12BE, YUV420P12LE)
+#define AV_PIX_FMT_YUV422P12 AV_PIX_FMT_NE(YUV422P12BE, YUV422P12LE)
+#define AV_PIX_FMT_YUV440P12 AV_PIX_FMT_NE(YUV440P12BE, YUV440P12LE)
+#define AV_PIX_FMT_YUV444P12 AV_PIX_FMT_NE(YUV444P12BE, YUV444P12LE)
+#define AV_PIX_FMT_YUV420P14 AV_PIX_FMT_NE(YUV420P14BE, YUV420P14LE)
+#define AV_PIX_FMT_YUV422P14 AV_PIX_FMT_NE(YUV422P14BE, YUV422P14LE)
+#define AV_PIX_FMT_YUV444P14 AV_PIX_FMT_NE(YUV444P14BE, YUV444P14LE)
+#define AV_PIX_FMT_YUV420P16 AV_PIX_FMT_NE(YUV420P16BE, YUV420P16LE)
+#define AV_PIX_FMT_YUV422P16 AV_PIX_FMT_NE(YUV422P16BE, YUV422P16LE)
+#define AV_PIX_FMT_YUV444P16 AV_PIX_FMT_NE(YUV444P16BE, YUV444P16LE)
+
+#define AV_PIX_FMT_GBRP9 AV_PIX_FMT_NE(GBRP9BE, GBRP9LE)
+#define AV_PIX_FMT_GBRP10 AV_PIX_FMT_NE(GBRP10BE, GBRP10LE)
+#define AV_PIX_FMT_GBRP12 AV_PIX_FMT_NE(GBRP12BE, GBRP12LE)
+#define AV_PIX_FMT_GBRP14 AV_PIX_FMT_NE(GBRP14BE, GBRP14LE)
+#define AV_PIX_FMT_GBRP16 AV_PIX_FMT_NE(GBRP16BE, GBRP16LE)
+#define AV_PIX_FMT_GBRAP10 AV_PIX_FMT_NE(GBRAP10BE, GBRAP10LE)
+#define AV_PIX_FMT_GBRAP12 AV_PIX_FMT_NE(GBRAP12BE, GBRAP12LE)
+#define AV_PIX_FMT_GBRAP16 AV_PIX_FMT_NE(GBRAP16BE, GBRAP16LE)
+
+#define AV_PIX_FMT_BAYER_BGGR16 AV_PIX_FMT_NE(BAYER_BGGR16BE, BAYER_BGGR16LE)
+#define AV_PIX_FMT_BAYER_RGGB16 AV_PIX_FMT_NE(BAYER_RGGB16BE, BAYER_RGGB16LE)
+#define AV_PIX_FMT_BAYER_GBRG16 AV_PIX_FMT_NE(BAYER_GBRG16BE, BAYER_GBRG16LE)
+#define AV_PIX_FMT_BAYER_GRBG16 AV_PIX_FMT_NE(BAYER_GRBG16BE, BAYER_GRBG16LE)
+
+#define AV_PIX_FMT_GBRPF32 AV_PIX_FMT_NE(GBRPF32BE, GBRPF32LE)
+#define AV_PIX_FMT_GBRAPF32 AV_PIX_FMT_NE(GBRAPF32BE, GBRAPF32LE)
+
+#define AV_PIX_FMT_GRAYF32 AV_PIX_FMT_NE(GRAYF32BE, GRAYF32LE)
+
+#define AV_PIX_FMT_YUVA420P9 AV_PIX_FMT_NE(YUVA420P9BE, YUVA420P9LE)
+#define AV_PIX_FMT_YUVA422P9 AV_PIX_FMT_NE(YUVA422P9BE, YUVA422P9LE)
+#define AV_PIX_FMT_YUVA444P9 AV_PIX_FMT_NE(YUVA444P9BE, YUVA444P9LE)
+#define AV_PIX_FMT_YUVA420P10 AV_PIX_FMT_NE(YUVA420P10BE, YUVA420P10LE)
+#define AV_PIX_FMT_YUVA422P10 AV_PIX_FMT_NE(YUVA422P10BE, YUVA422P10LE)
+#define AV_PIX_FMT_YUVA444P10 AV_PIX_FMT_NE(YUVA444P10BE, YUVA444P10LE)
+#define AV_PIX_FMT_YUVA422P12 AV_PIX_FMT_NE(YUVA422P12BE, YUVA422P12LE)
+#define AV_PIX_FMT_YUVA444P12 AV_PIX_FMT_NE(YUVA444P12BE, YUVA444P12LE)
+#define AV_PIX_FMT_YUVA420P16 AV_PIX_FMT_NE(YUVA420P16BE, YUVA420P16LE)
+#define AV_PIX_FMT_YUVA422P16 AV_PIX_FMT_NE(YUVA422P16BE, YUVA422P16LE)
+#define AV_PIX_FMT_YUVA444P16 AV_PIX_FMT_NE(YUVA444P16BE, YUVA444P16LE)
+
+#define AV_PIX_FMT_XYZ12 AV_PIX_FMT_NE(XYZ12BE, XYZ12LE)
+#define AV_PIX_FMT_NV20 AV_PIX_FMT_NE(NV20BE, NV20LE)
+#define AV_PIX_FMT_AYUV64 AV_PIX_FMT_NE(AYUV64BE, AYUV64LE)
+#define AV_PIX_FMT_P010 AV_PIX_FMT_NE(P010BE, P010LE)
+#define AV_PIX_FMT_P012 AV_PIX_FMT_NE(P012BE, P012LE)
+#define AV_PIX_FMT_P016 AV_PIX_FMT_NE(P016BE, P016LE)
+
+#define AV_PIX_FMT_Y210 AV_PIX_FMT_NE(Y210BE, Y210LE)
+#define AV_PIX_FMT_Y212 AV_PIX_FMT_NE(Y212BE, Y212LE)
+#define AV_PIX_FMT_XV30 AV_PIX_FMT_NE(XV30BE, XV30LE)
+#define AV_PIX_FMT_XV36 AV_PIX_FMT_NE(XV36BE, XV36LE)
+#define AV_PIX_FMT_X2RGB10 AV_PIX_FMT_NE(X2RGB10BE, X2RGB10LE)
+#define AV_PIX_FMT_X2BGR10 AV_PIX_FMT_NE(X2BGR10BE, X2BGR10LE)
+
+#define AV_PIX_FMT_P210 AV_PIX_FMT_NE(P210BE, P210LE)
+#define AV_PIX_FMT_P410 AV_PIX_FMT_NE(P410BE, P410LE)
+#define AV_PIX_FMT_P216 AV_PIX_FMT_NE(P216BE, P216LE)
+#define AV_PIX_FMT_P416 AV_PIX_FMT_NE(P416BE, P416LE)
+
+#define AV_PIX_FMT_RGBAF16 AV_PIX_FMT_NE(RGBAF16BE, RGBAF16LE)
+
+#define AV_PIX_FMT_RGBF32 AV_PIX_FMT_NE(RGBF32BE, RGBF32LE)
+#define AV_PIX_FMT_RGBAF32 AV_PIX_FMT_NE(RGBAF32BE, RGBAF32LE)
+
+/**
+ * Chromaticity coordinates of the source primaries.
+ * These values match the ones defined by ISO/IEC 23091-2_2019 subclause 8.1 and
+ * ITU-T H.273.
+ */
+enum AVColorPrimaries {
+ AVCOL_PRI_RESERVED0 = 0,
+ AVCOL_PRI_BT709 =
+ 1, ///< also ITU-R BT1361 / IEC 61966-2-4 / SMPTE RP 177 Annex B
+ AVCOL_PRI_UNSPECIFIED = 2,
+ AVCOL_PRI_RESERVED = 3,
+ AVCOL_PRI_BT470M =
+ 4, ///< also FCC Title 47 Code of Federal Regulations 73.682 (a)(20)
+
+ AVCOL_PRI_BT470BG = 5, ///< also ITU-R BT601-6 625 / ITU-R BT1358 625 / ITU-R
+ ///< BT1700 625 PAL & SECAM
+ AVCOL_PRI_SMPTE170M =
+ 6, ///< also ITU-R BT601-6 525 / ITU-R BT1358 525 / ITU-R BT1700 NTSC
+ AVCOL_PRI_SMPTE240M =
+ 7, ///< identical to above, also called "SMPTE C" even though it uses D65
+ AVCOL_PRI_FILM = 8, ///< colour filters using Illuminant C
+ AVCOL_PRI_BT2020 = 9, ///< ITU-R BT2020
+ AVCOL_PRI_SMPTE428 = 10, ///< SMPTE ST 428-1 (CIE 1931 XYZ)
+ AVCOL_PRI_SMPTEST428_1 = AVCOL_PRI_SMPTE428,
+ AVCOL_PRI_SMPTE431 = 11, ///< SMPTE ST 431-2 (2011) / DCI P3
+ AVCOL_PRI_SMPTE432 = 12, ///< SMPTE ST 432-1 (2010) / P3 D65 / Display P3
+ AVCOL_PRI_EBU3213 = 22, ///< EBU Tech. 3213-E (nothing there) / one of JEDEC
+ ///< P22 group phosphors
+ AVCOL_PRI_JEDEC_P22 = AVCOL_PRI_EBU3213,
+ AVCOL_PRI_NB ///< Not part of ABI
+};
+
+/**
+ * Color Transfer Characteristic.
+ * These values match the ones defined by ISO/IEC 23091-2_2019 subclause 8.2.
+ */
+enum AVColorTransferCharacteristic {
+ AVCOL_TRC_RESERVED0 = 0,
+ AVCOL_TRC_BT709 = 1, ///< also ITU-R BT1361
+ AVCOL_TRC_UNSPECIFIED = 2,
+ AVCOL_TRC_RESERVED = 3,
+ AVCOL_TRC_GAMMA22 = 4, ///< also ITU-R BT470M / ITU-R BT1700 625 PAL & SECAM
+ AVCOL_TRC_GAMMA28 = 5, ///< also ITU-R BT470BG
+ AVCOL_TRC_SMPTE170M = 6, ///< also ITU-R BT601-6 525 or 625 / ITU-R BT1358
+ ///< 525 or 625 / ITU-R BT1700 NTSC
+ AVCOL_TRC_SMPTE240M = 7,
+ AVCOL_TRC_LINEAR = 8, ///< "Linear transfer characteristics"
+ AVCOL_TRC_LOG = 9, ///< "Logarithmic transfer characteristic (100:1 range)"
+ AVCOL_TRC_LOG_SQRT =
+ 10, ///< "Logarithmic transfer characteristic (100 * Sqrt(10) : 1 range)"
+ AVCOL_TRC_IEC61966_2_4 = 11, ///< IEC 61966-2-4
+ AVCOL_TRC_BT1361_ECG = 12, ///< ITU-R BT1361 Extended Colour Gamut
+ AVCOL_TRC_IEC61966_2_1 = 13, ///< IEC 61966-2-1 (sRGB or sYCC)
+ AVCOL_TRC_BT2020_10 = 14, ///< ITU-R BT2020 for 10-bit system
+ AVCOL_TRC_BT2020_12 = 15, ///< ITU-R BT2020 for 12-bit system
+ AVCOL_TRC_SMPTE2084 =
+ 16, ///< SMPTE ST 2084 for 10-, 12-, 14- and 16-bit systems
+ AVCOL_TRC_SMPTEST2084 = AVCOL_TRC_SMPTE2084,
+ AVCOL_TRC_SMPTE428 = 17, ///< SMPTE ST 428-1
+ AVCOL_TRC_SMPTEST428_1 = AVCOL_TRC_SMPTE428,
+ AVCOL_TRC_ARIB_STD_B67 = 18, ///< ARIB STD-B67, known as "Hybrid log-gamma"
+ AVCOL_TRC_NB ///< Not part of ABI
+};
+
+/**
+ * YUV colorspace type.
+ * These values match the ones defined by ISO/IEC 23091-2_2019 subclause 8.3.
+ */
+enum AVColorSpace {
+ AVCOL_SPC_RGB = 0, ///< order of coefficients is actually GBR, also IEC
+ ///< 61966-2-1 (sRGB), YZX and ST 428-1
+ AVCOL_SPC_BT709 = 1, ///< also ITU-R BT1361 / IEC 61966-2-4 xvYCC709 /
+ ///< derived in SMPTE RP 177 Annex B
+ AVCOL_SPC_UNSPECIFIED = 2,
+ AVCOL_SPC_RESERVED =
+ 3, ///< reserved for future use by ITU-T and ISO/IEC just like 15-255 are
+ AVCOL_SPC_FCC =
+ 4, ///< FCC Title 47 Code of Federal Regulations 73.682 (a)(20)
+ AVCOL_SPC_BT470BG = 5, ///< also ITU-R BT601-6 625 / ITU-R BT1358 625 / ITU-R
+ ///< BT1700 625 PAL & SECAM / IEC 61966-2-4 xvYCC601
+ AVCOL_SPC_SMPTE170M =
+ 6, ///< also ITU-R BT601-6 525 / ITU-R BT1358 525 / ITU-R BT1700 NTSC /
+ ///< functionally identical to above
+ AVCOL_SPC_SMPTE240M =
+ 7, ///< derived from 170M primaries and D65 white point, 170M is derived
+ ///< from BT470 System M's primaries
+ AVCOL_SPC_YCGCO =
+ 8, ///< used by Dirac / VC-2 and H.264 FRext, see ITU-T SG16
+ AVCOL_SPC_YCOCG = AVCOL_SPC_YCGCO,
+ AVCOL_SPC_BT2020_NCL = 9, ///< ITU-R BT2020 non-constant luminance system
+ AVCOL_SPC_BT2020_CL = 10, ///< ITU-R BT2020 constant luminance system
+ AVCOL_SPC_SMPTE2085 = 11, ///< SMPTE 2085, Y'D'zD'x
+ AVCOL_SPC_CHROMA_DERIVED_NCL =
+ 12, ///< Chromaticity-derived non-constant luminance system
+ AVCOL_SPC_CHROMA_DERIVED_CL =
+ 13, ///< Chromaticity-derived constant luminance system
+ AVCOL_SPC_ICTCP = 14, ///< ITU-R BT.2100-0, ICtCp
+ AVCOL_SPC_NB ///< Not part of ABI
+};
+
+/**
+ * Visual content value range.
+ *
+ * These values are based on definitions that can be found in multiple
+ * specifications, such as ITU-T BT.709 (3.4 - Quantization of RGB, luminance
+ * and colour-difference signals), ITU-T BT.2020 (Table 5 - Digital
+ * Representation) as well as ITU-T BT.2100 (Table 9 - Digital 10- and 12-bit
+ * integer representation). At the time of writing, the BT.2100 one is
+ * recommended, as it also defines the full range representation.
+ *
+ * Common definitions:
+ * - For RGB and luma planes such as Y in YCbCr and I in ICtCp,
+ * 'E' is the original value in range of 0.0 to 1.0.
+ * - For chroma planes such as Cb,Cr and Ct,Cp, 'E' is the original
+ * value in range of -0.5 to 0.5.
+ * - 'n' is the output bit depth.
+ * - For additional definitions such as rounding and clipping to valid n
+ * bit unsigned integer range, please refer to BT.2100 (Table 9).
+ */
+enum AVColorRange {
+ AVCOL_RANGE_UNSPECIFIED = 0,
+
+ /**
+ * Narrow or limited range content.
+ *
+ * - For luma planes:
+ *
+ * (219 * E + 16) * 2^(n-8)
+ *
+ * F.ex. the range of 16-235 for 8 bits
+ *
+ * - For chroma planes:
+ *
+ * (224 * E + 128) * 2^(n-8)
+ *
+ * F.ex. the range of 16-240 for 8 bits
+ */
+ AVCOL_RANGE_MPEG = 1,
+
+ /**
+ * Full range content.
+ *
+ * - For RGB and luma planes:
+ *
+ * (2^n - 1) * E
+ *
+ * F.ex. the range of 0-255 for 8 bits
+ *
+ * - For chroma planes:
+ *
+ * (2^n - 1) * E + 2^(n - 1)
+ *
+ * F.ex. the range of 1-255 for 8 bits
+ */
+ AVCOL_RANGE_JPEG = 2,
+ AVCOL_RANGE_NB ///< Not part of ABI
+};
+
+/**
+ * Location of chroma samples.
+ *
+ * Illustration showing the location of the first (top left) chroma sample of
+ *the image, the left shows only luma, the right shows the location of the
+ *chroma sample, the 2 could be imagined to overlay each other but are drawn
+ *separately due to limitations of ASCII
+ *
+ * 1st 2nd 1st 2nd horizontal luma sample positions
+ * v v v v
+ * ______ ______
+ *1st luma line > |X X ... |3 4 X ... X are luma samples,
+ * | |1 2 1-6 are possible chroma positions
+ *2nd luma line > |X X ... |5 6 X ... 0 is undefined/unknown position
+ */
+enum AVChromaLocation {
+ AVCHROMA_LOC_UNSPECIFIED = 0,
+ AVCHROMA_LOC_LEFT = 1, ///< MPEG-2/4 4:2:0, H.264 default for 4:2:0
+ AVCHROMA_LOC_CENTER = 2, ///< MPEG-1 4:2:0, JPEG 4:2:0, H.263 4:2:0
+ AVCHROMA_LOC_TOPLEFT =
+ 3, ///< ITU-R 601, SMPTE 274M 296M S314M(DV 4:1:1), mpeg2 4:2:2
+ AVCHROMA_LOC_TOP = 4,
+ AVCHROMA_LOC_BOTTOMLEFT = 5,
+ AVCHROMA_LOC_BOTTOM = 6,
+ AVCHROMA_LOC_NB ///< Not part of ABI
+};
+
+#endif /* AVUTIL_PIXFMT_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/rational.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/rational.h
new file mode 100644
index 0000000000..1ce38d0968
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/rational.h
@@ -0,0 +1,222 @@
+/*
+ * rational numbers
+ * Copyright (c) 2003 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * @ingroup lavu_math_rational
+ * Utilties for rational number calculation.
+ * @author Michael Niedermayer <michaelni@gmx.at>
+ */
+
+#ifndef AVUTIL_RATIONAL_H
+#define AVUTIL_RATIONAL_H
+
+#include <stdint.h>
+#include <limits.h>
+#include "attributes.h"
+
+/**
+ * @defgroup lavu_math_rational AVRational
+ * @ingroup lavu_math
+ * Rational number calculation.
+ *
+ * While rational numbers can be expressed as floating-point numbers, the
+ * conversion process is a lossy one, so are floating-point operations. On the
+ * other hand, the nature of FFmpeg demands highly accurate calculation of
+ * timestamps. This set of rational number utilities serves as a generic
+ * interface for manipulating rational numbers as pairs of numerators and
+ * denominators.
+ *
+ * Many of the functions that operate on AVRational's have the suffix `_q`, in
+ * reference to the mathematical symbol "ℚ" (Q) which denotes the set of all
+ * rational numbers.
+ *
+ * @{
+ */
+
+/**
+ * Rational number (pair of numerator and denominator).
+ */
+typedef struct AVRational {
+ int num; ///< Numerator
+ int den; ///< Denominator
+} AVRational;
+
+/**
+ * Create an AVRational.
+ *
+ * Useful for compilers that do not support compound literals.
+ *
+ * @note The return value is not reduced.
+ * @see av_reduce()
+ */
+static inline AVRational av_make_q(int num, int den) {
+ AVRational r = {num, den};
+ return r;
+}
+
+/**
+ * Compare two rationals.
+ *
+ * @param a First rational
+ * @param b Second rational
+ *
+ * @return One of the following values:
+ * - 0 if `a == b`
+ * - 1 if `a > b`
+ * - -1 if `a < b`
+ * - `INT_MIN` if one of the values is of the form `0 / 0`
+ */
+static inline int av_cmp_q(AVRational a, AVRational b) {
+ const int64_t tmp = a.num * (int64_t)b.den - b.num * (int64_t)a.den;
+
+ if (tmp)
+ return (int)((tmp ^ a.den ^ b.den) >> 63) | 1;
+ else if (b.den && a.den)
+ return 0;
+ else if (a.num && b.num)
+ return (a.num >> 31) - (b.num >> 31);
+ else
+ return INT_MIN;
+}
+
+/**
+ * Convert an AVRational to a `double`.
+ * @param a AVRational to convert
+ * @return `a` in floating-point form
+ * @see av_d2q()
+ */
+static inline double av_q2d(AVRational a) { return a.num / (double)a.den; }
+
+/**
+ * Reduce a fraction.
+ *
+ * This is useful for framerate calculations.
+ *
+ * @param[out] dst_num Destination numerator
+ * @param[out] dst_den Destination denominator
+ * @param[in] num Source numerator
+ * @param[in] den Source denominator
+ * @param[in] max Maximum allowed values for `dst_num` & `dst_den`
+ * @return 1 if the operation is exact, 0 otherwise
+ */
+int av_reduce(int* dst_num, int* dst_den, int64_t num, int64_t den,
+ int64_t max);
+
+/**
+ * Multiply two rationals.
+ * @param b First rational
+ * @param c Second rational
+ * @return b*c
+ */
+AVRational av_mul_q(AVRational b, AVRational c) av_const;
+
+/**
+ * Divide one rational by another.
+ * @param b First rational
+ * @param c Second rational
+ * @return b/c
+ */
+AVRational av_div_q(AVRational b, AVRational c) av_const;
+
+/**
+ * Add two rationals.
+ * @param b First rational
+ * @param c Second rational
+ * @return b+c
+ */
+AVRational av_add_q(AVRational b, AVRational c) av_const;
+
+/**
+ * Subtract one rational from another.
+ * @param b First rational
+ * @param c Second rational
+ * @return b-c
+ */
+AVRational av_sub_q(AVRational b, AVRational c) av_const;
+
+/**
+ * Invert a rational.
+ * @param q value
+ * @return 1 / q
+ */
+static av_always_inline AVRational av_inv_q(AVRational q) {
+ AVRational r = {q.den, q.num};
+ return r;
+}
+
+/**
+ * Convert a double precision floating point number to a rational.
+ *
+ * In case of infinity, the returned value is expressed as `{1, 0}` or
+ * `{-1, 0}` depending on the sign.
+ *
+ * @param d `double` to convert
+ * @param max Maximum allowed numerator and denominator
+ * @return `d` in AVRational form
+ * @see av_q2d()
+ */
+AVRational av_d2q(double d, int max) av_const;
+
+/**
+ * Find which of the two rationals is closer to another rational.
+ *
+ * @param q Rational to be compared against
+ * @param q1 Rational to be tested
+ * @param q2 Rational to be tested
+ * @return One of the following values:
+ * - 1 if `q1` is nearer to `q` than `q2`
+ * - -1 if `q2` is nearer to `q` than `q1`
+ * - 0 if they have the same distance
+ */
+int av_nearer_q(AVRational q, AVRational q1, AVRational q2);
+
+/**
+ * Find the value in a list of rationals nearest a given reference rational.
+ *
+ * @param q Reference rational
+ * @param q_list Array of rationals terminated by `{0, 0}`
+ * @return Index of the nearest value found in the array
+ */
+int av_find_nearest_q_idx(AVRational q, const AVRational* q_list);
+
+/**
+ * Convert an AVRational to a IEEE 32-bit `float` expressed in fixed-point
+ * format.
+ *
+ * @param q Rational to be converted
+ * @return Equivalent floating-point value, expressed as an unsigned 32-bit
+ * integer.
+ * @note The returned value is platform-indepedant.
+ */
+uint32_t av_q2intfloat(AVRational q);
+
+/**
+ * Return the best rational so that a and b are multiple of it.
+ * If the resulting denominator is larger than max_den, return def.
+ */
+AVRational av_gcd_q(AVRational a, AVRational b, int max_den, AVRational def);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_RATIONAL_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/samplefmt.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/samplefmt.h
new file mode 100644
index 0000000000..31cbca7659
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/samplefmt.h
@@ -0,0 +1,274 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_SAMPLEFMT_H
+#define AVUTIL_SAMPLEFMT_H
+
+#include <stdint.h>
+
+/**
+ * @addtogroup lavu_audio
+ * @{
+ *
+ * @defgroup lavu_sampfmts Audio sample formats
+ *
+ * Audio sample format enumeration and related convenience functions.
+ * @{
+ */
+
+/**
+ * Audio sample formats
+ *
+ * - The data described by the sample format is always in native-endian order.
+ * Sample values can be expressed by native C types, hence the lack of a
+ * signed 24-bit sample format even though it is a common raw audio data format.
+ *
+ * - The floating-point formats are based on full volume being in the range
+ * [-1.0, 1.0]. Any values outside this range are beyond full volume level.
+ *
+ * - The data layout as used in av_samples_fill_arrays() and elsewhere in FFmpeg
+ * (such as AVFrame in libavcodec) is as follows:
+ *
+ * @par
+ * For planar sample formats, each audio channel is in a separate data plane,
+ * and linesize is the buffer size, in bytes, for a single plane. All data
+ * planes must be the same size. For packed sample formats, only the first data
+ * plane is used, and samples for each channel are interleaved. In this case,
+ * linesize is the buffer size, in bytes, for the 1 plane.
+ *
+ */
+enum AVSampleFormat {
+ AV_SAMPLE_FMT_NONE = -1,
+ AV_SAMPLE_FMT_U8, ///< unsigned 8 bits
+ AV_SAMPLE_FMT_S16, ///< signed 16 bits
+ AV_SAMPLE_FMT_S32, ///< signed 32 bits
+ AV_SAMPLE_FMT_FLT, ///< float
+ AV_SAMPLE_FMT_DBL, ///< double
+
+ AV_SAMPLE_FMT_U8P, ///< unsigned 8 bits, planar
+ AV_SAMPLE_FMT_S16P, ///< signed 16 bits, planar
+ AV_SAMPLE_FMT_S32P, ///< signed 32 bits, planar
+ AV_SAMPLE_FMT_FLTP, ///< float, planar
+ AV_SAMPLE_FMT_DBLP, ///< double, planar
+ AV_SAMPLE_FMT_S64, ///< signed 64 bits
+ AV_SAMPLE_FMT_S64P, ///< signed 64 bits, planar
+
+ AV_SAMPLE_FMT_NB ///< Number of sample formats. DO NOT USE if linking
+ ///< dynamically
+};
+
+/**
+ * Return the name of sample_fmt, or NULL if sample_fmt is not
+ * recognized.
+ */
+const char* av_get_sample_fmt_name(enum AVSampleFormat sample_fmt);
+
+/**
+ * Return a sample format corresponding to name, or AV_SAMPLE_FMT_NONE
+ * on error.
+ */
+enum AVSampleFormat av_get_sample_fmt(const char* name);
+
+/**
+ * Return the planar<->packed alternative form of the given sample format, or
+ * AV_SAMPLE_FMT_NONE on error. If the passed sample_fmt is already in the
+ * requested planar/packed format, the format returned is the same as the
+ * input.
+ */
+enum AVSampleFormat av_get_alt_sample_fmt(enum AVSampleFormat sample_fmt,
+ int planar);
+
+/**
+ * Get the packed alternative form of the given sample format.
+ *
+ * If the passed sample_fmt is already in packed format, the format returned is
+ * the same as the input.
+ *
+ * @return the packed alternative form of the given sample format or
+ AV_SAMPLE_FMT_NONE on error.
+ */
+enum AVSampleFormat av_get_packed_sample_fmt(enum AVSampleFormat sample_fmt);
+
+/**
+ * Get the planar alternative form of the given sample format.
+ *
+ * If the passed sample_fmt is already in planar format, the format returned is
+ * the same as the input.
+ *
+ * @return the planar alternative form of the given sample format or
+ AV_SAMPLE_FMT_NONE on error.
+ */
+enum AVSampleFormat av_get_planar_sample_fmt(enum AVSampleFormat sample_fmt);
+
+/**
+ * Generate a string corresponding to the sample format with
+ * sample_fmt, or a header if sample_fmt is negative.
+ *
+ * @param buf the buffer where to write the string
+ * @param buf_size the size of buf
+ * @param sample_fmt the number of the sample format to print the
+ * corresponding info string, or a negative value to print the
+ * corresponding header.
+ * @return the pointer to the filled buffer or NULL if sample_fmt is
+ * unknown or in case of other errors
+ */
+char* av_get_sample_fmt_string(char* buf, int buf_size,
+ enum AVSampleFormat sample_fmt);
+
+/**
+ * Return number of bytes per sample.
+ *
+ * @param sample_fmt the sample format
+ * @return number of bytes per sample or zero if unknown for the given
+ * sample format
+ */
+int av_get_bytes_per_sample(enum AVSampleFormat sample_fmt);
+
+/**
+ * Check if the sample format is planar.
+ *
+ * @param sample_fmt the sample format to inspect
+ * @return 1 if the sample format is planar, 0 if it is interleaved
+ */
+int av_sample_fmt_is_planar(enum AVSampleFormat sample_fmt);
+
+/**
+ * Get the required buffer size for the given audio parameters.
+ *
+ * @param[out] linesize calculated linesize, may be NULL
+ * @param nb_channels the number of channels
+ * @param nb_samples the number of samples in a single channel
+ * @param sample_fmt the sample format
+ * @param align buffer size alignment (0 = default, 1 = no alignment)
+ * @return required buffer size, or negative error code on failure
+ */
+int av_samples_get_buffer_size(int* linesize, int nb_channels, int nb_samples,
+ enum AVSampleFormat sample_fmt, int align);
+
+/**
+ * @}
+ *
+ * @defgroup lavu_sampmanip Samples manipulation
+ *
+ * Functions that manipulate audio samples
+ * @{
+ */
+
+/**
+ * Fill plane data pointers and linesize for samples with sample
+ * format sample_fmt.
+ *
+ * The audio_data array is filled with the pointers to the samples data planes:
+ * for planar, set the start point of each channel's data within the buffer,
+ * for packed, set the start point of the entire buffer only.
+ *
+ * The value pointed to by linesize is set to the aligned size of each
+ * channel's data buffer for planar layout, or to the aligned size of the
+ * buffer for all channels for packed layout.
+ *
+ * The buffer in buf must be big enough to contain all the samples
+ * (use av_samples_get_buffer_size() to compute its minimum size),
+ * otherwise the audio_data pointers will point to invalid data.
+ *
+ * @see enum AVSampleFormat
+ * The documentation for AVSampleFormat describes the data layout.
+ *
+ * @param[out] audio_data array to be filled with the pointer for each channel
+ * @param[out] linesize calculated linesize, may be NULL
+ * @param buf the pointer to a buffer containing the samples
+ * @param nb_channels the number of channels
+ * @param nb_samples the number of samples in a single channel
+ * @param sample_fmt the sample format
+ * @param align buffer size alignment (0 = default, 1 = no alignment)
+ * @return minimum size in bytes required for the buffer on
+ * success, or a negative error code on failure
+ */
+int av_samples_fill_arrays(uint8_t** audio_data, int* linesize,
+ const uint8_t* buf, int nb_channels, int nb_samples,
+ enum AVSampleFormat sample_fmt, int align);
+
+/**
+ * Allocate a samples buffer for nb_samples samples, and fill data pointers and
+ * linesize accordingly.
+ * The allocated samples buffer can be freed by using av_freep(&audio_data[0])
+ * Allocated data will be initialized to silence.
+ *
+ * @see enum AVSampleFormat
+ * The documentation for AVSampleFormat describes the data layout.
+ *
+ * @param[out] audio_data array to be filled with the pointer for each channel
+ * @param[out] linesize aligned size for audio buffer(s), may be NULL
+ * @param nb_channels number of audio channels
+ * @param nb_samples number of samples per channel
+ * @param sample_fmt the sample format
+ * @param align buffer size alignment (0 = default, 1 = no alignment)
+ * @return >=0 on success or a negative error code on failure
+ * @todo return the size of the allocated buffer in case of success at the next
+ * bump
+ * @see av_samples_fill_arrays()
+ * @see av_samples_alloc_array_and_samples()
+ */
+int av_samples_alloc(uint8_t** audio_data, int* linesize, int nb_channels,
+ int nb_samples, enum AVSampleFormat sample_fmt, int align);
+
+/**
+ * Allocate a data pointers array, samples buffer for nb_samples
+ * samples, and fill data pointers and linesize accordingly.
+ *
+ * This is the same as av_samples_alloc(), but also allocates the data
+ * pointers array.
+ *
+ * @see av_samples_alloc()
+ */
+int av_samples_alloc_array_and_samples(uint8_t*** audio_data, int* linesize,
+ int nb_channels, int nb_samples,
+ enum AVSampleFormat sample_fmt,
+ int align);
+
+/**
+ * Copy samples from src to dst.
+ *
+ * @param dst destination array of pointers to data planes
+ * @param src source array of pointers to data planes
+ * @param dst_offset offset in samples at which the data will be written to dst
+ * @param src_offset offset in samples at which the data will be read from src
+ * @param nb_samples number of samples to be copied
+ * @param nb_channels number of audio channels
+ * @param sample_fmt audio sample format
+ */
+int av_samples_copy(uint8_t** dst, uint8_t* const* src, int dst_offset,
+ int src_offset, int nb_samples, int nb_channels,
+ enum AVSampleFormat sample_fmt);
+
+/**
+ * Fill an audio buffer with silence.
+ *
+ * @param audio_data array of pointers to data planes
+ * @param offset offset in samples at which to start filling
+ * @param nb_samples number of samples to fill
+ * @param nb_channels number of audio channels
+ * @param sample_fmt audio sample format
+ */
+int av_samples_set_silence(uint8_t** audio_data, int offset, int nb_samples,
+ int nb_channels, enum AVSampleFormat sample_fmt);
+
+/**
+ * @}
+ * @}
+ */
+#endif /* AVUTIL_SAMPLEFMT_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/version.h b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/version.h
new file mode 100644
index 0000000000..ae72840c0a
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/include/libavutil/version.h
@@ -0,0 +1,122 @@
+/*
+ * copyright (c) 2003 Fabrice Bellard
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * @ingroup lavu
+ * Libavutil version macros
+ */
+
+#ifndef AVUTIL_VERSION_H
+#define AVUTIL_VERSION_H
+
+#include "macros.h"
+
+/**
+ * @addtogroup version_utils
+ *
+ * Useful to check and match library version in order to maintain
+ * backward compatibility.
+ *
+ * The FFmpeg libraries follow a versioning sheme very similar to
+ * Semantic Versioning (http://semver.org/)
+ * The difference is that the component called PATCH is called MICRO in FFmpeg
+ * and its value is reset to 100 instead of 0 to keep it above or equal to 100.
+ * Also we do not increase MICRO for every bugfix or change in git master.
+ *
+ * Prior to FFmpeg 3.2 point releases did not change any lib version number to
+ * avoid aliassing different git master checkouts.
+ * Starting with FFmpeg 3.2, the released library versions will occupy
+ * a separate MAJOR.MINOR that is not used on the master development branch.
+ * That is if we branch a release of master 55.10.123 we will bump to 55.11.100
+ * for the release and master will continue at 55.12.100 after it. Each new
+ * point release will then bump the MICRO improving the usefulness of the lib
+ * versions.
+ *
+ * @{
+ */
+
+#define AV_VERSION_INT(a, b, c) ((a) << 16 | (b) << 8 | (c))
+#define AV_VERSION_DOT(a, b, c) a##.##b##.##c
+#define AV_VERSION(a, b, c) AV_VERSION_DOT(a, b, c)
+
+/**
+ * Extract version components from the full ::AV_VERSION_INT int as returned
+ * by functions like ::avformat_version() and ::avcodec_version()
+ */
+#define AV_VERSION_MAJOR(a) ((a) >> 16)
+#define AV_VERSION_MINOR(a) (((a)&0x00FF00) >> 8)
+#define AV_VERSION_MICRO(a) ((a)&0xFF)
+
+/**
+ * @}
+ */
+
+/**
+ * @defgroup lavu_ver Version and Build diagnostics
+ *
+ * Macros and function useful to check at compiletime and at runtime
+ * which version of libavutil is in use.
+ *
+ * @{
+ */
+
+#define LIBAVUTIL_VERSION_MAJOR 58
+#define LIBAVUTIL_VERSION_MINOR 3
+#define LIBAVUTIL_VERSION_MICRO 100
+
+#define LIBAVUTIL_VERSION_INT \
+ AV_VERSION_INT(LIBAVUTIL_VERSION_MAJOR, LIBAVUTIL_VERSION_MINOR, \
+ LIBAVUTIL_VERSION_MICRO)
+#define LIBAVUTIL_VERSION \
+ AV_VERSION(LIBAVUTIL_VERSION_MAJOR, LIBAVUTIL_VERSION_MINOR, \
+ LIBAVUTIL_VERSION_MICRO)
+#define LIBAVUTIL_BUILD LIBAVUTIL_VERSION_INT
+
+#define LIBAVUTIL_IDENT "Lavu" AV_STRINGIFY(LIBAVUTIL_VERSION)
+
+/**
+ * @defgroup lavu_depr_guards Deprecation Guards
+ * FF_API_* defines may be placed below to indicate public API that will be
+ * dropped at a future version bump. The defines themselves are not part of
+ * the public API and may change, break or disappear at any time.
+ *
+ * @note, when bumping the major version it is recommended to manually
+ * disable each FF_API_* in its own commit instead of disabling them all
+ * at once through the bump. This improves the git bisect-ability of the change.
+ *
+ * @{
+ */
+
+#define FF_API_FIFO_PEEK2 (LIBAVUTIL_VERSION_MAJOR < 59)
+#define FF_API_FIFO_OLD_API (LIBAVUTIL_VERSION_MAJOR < 59)
+#define FF_API_XVMC (LIBAVUTIL_VERSION_MAJOR < 59)
+#define FF_API_OLD_CHANNEL_LAYOUT (LIBAVUTIL_VERSION_MAJOR < 59)
+#define FF_API_AV_FOPEN_UTF8 (LIBAVUTIL_VERSION_MAJOR < 59)
+#define FF_API_PKT_DURATION (LIBAVUTIL_VERSION_MAJOR < 59)
+#define FF_API_REORDERED_OPAQUE (LIBAVUTIL_VERSION_MAJOR < 59)
+#define FF_API_FRAME_PICTURE_NUMBER (LIBAVUTIL_VERSION_MAJOR < 59)
+
+/**
+ * @}
+ * @}
+ */
+
+#endif /* AVUTIL_VERSION_H */
diff --git a/dom/media/platforms/ffmpeg/ffmpeg60/moz.build b/dom/media/platforms/ffmpeg/ffmpeg60/moz.build
new file mode 100644
index 0000000000..c300f7c0d6
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffmpeg60/moz.build
@@ -0,0 +1,39 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES += [
+ "../FFmpegAudioDecoder.cpp",
+ "../FFmpegDataDecoder.cpp",
+ "../FFmpegDecoderModule.cpp",
+ "../FFmpegVideoDecoder.cpp",
+]
+LOCAL_INCLUDES += [
+ "..",
+ "/media/mozva",
+ "include",
+]
+
+if CONFIG["CC_TYPE"] in ("clang", "gcc"):
+ CXXFLAGS += ["-Wno-deprecated-declarations"]
+if CONFIG["CC_TYPE"] == "clang":
+ CXXFLAGS += [
+ "-Wno-unknown-attributes",
+ ]
+if CONFIG["CC_TYPE"] == "gcc":
+ CXXFLAGS += [
+ "-Wno-attributes",
+ ]
+if CONFIG["MOZ_WAYLAND"]:
+ CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]
+if CONFIG["MOZ_ENABLE_VAAPI"] or CONFIG["MOZ_ENABLE_V4L2"]:
+ UNIFIED_SOURCES += ["../FFmpegVideoFramePool.cpp"]
+ LOCAL_INCLUDES += ["/third_party/drm/drm/include/libdrm/"]
+ USE_LIBS += ["mozva"]
+ DEFINES["MOZ_WAYLAND_USE_HWDECODE"] = 1
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/platforms/ffmpeg/ffvpx/FFVPXRuntimeLinker.cpp b/dom/media/platforms/ffmpeg/ffvpx/FFVPXRuntimeLinker.cpp
new file mode 100644
index 0000000000..44cc4ec4bd
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffvpx/FFVPXRuntimeLinker.cpp
@@ -0,0 +1,130 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "FFVPXRuntimeLinker.h"
+#include "FFmpegLibWrapper.h"
+#include "FFmpegLog.h"
+#include "BinaryPath.h"
+#include "mozilla/FileUtils.h"
+#include "nsLocalFile.h"
+#include "prmem.h"
+#include "prlink.h"
+#ifdef XP_WIN
+# include <windows.h>
+#endif
+
+namespace mozilla {
+
+template <int V>
+class FFmpegDecoderModule {
+ public:
+ static already_AddRefed<PlatformDecoderModule> Create(FFmpegLibWrapper*);
+};
+
+static FFmpegLibWrapper sFFVPXLib;
+
+FFVPXRuntimeLinker::LinkStatus FFVPXRuntimeLinker::sLinkStatus =
+ LinkStatus_INIT;
+
+static PRLibrary* MozAVLink(nsIFile* aFile) {
+ PRLibSpec lspec;
+ PathString path = aFile->NativePath();
+#ifdef XP_WIN
+ lspec.type = PR_LibSpec_PathnameU;
+ lspec.value.pathname_u = path.get();
+#else
+ lspec.type = PR_LibSpec_Pathname;
+ lspec.value.pathname = path.get();
+#endif
+#ifdef MOZ_WIDGET_ANDROID
+ PRLibrary* lib = PR_LoadLibraryWithFlags(lspec, PR_LD_NOW | PR_LD_GLOBAL);
+#else
+ PRLibrary* lib = PR_LoadLibraryWithFlags(lspec, PR_LD_NOW | PR_LD_LOCAL);
+#endif
+ if (!lib) {
+ FFMPEG_LOG("unable to load library %s", aFile->HumanReadablePath().get());
+ }
+ return lib;
+}
+
+/* static */
+bool FFVPXRuntimeLinker::Init() {
+ if (sLinkStatus) {
+ return sLinkStatus == LinkStatus_SUCCEEDED;
+ }
+
+ MOZ_ASSERT(NS_IsMainThread());
+ sLinkStatus = LinkStatus_FAILED;
+
+#ifdef MOZ_WAYLAND
+ sFFVPXLib.LinkVAAPILibs();
+#endif
+
+ nsCOMPtr<nsIFile> libFile;
+ if (NS_FAILED(mozilla::BinaryPath::GetFile(getter_AddRefs(libFile)))) {
+ return false;
+ }
+
+#ifdef XP_DARWIN
+ if (!XRE_IsParentProcess() &&
+ (XRE_GetChildProcBinPathType(XRE_GetProcessType()) ==
+ BinPathType::PluginContainer)) {
+ // On macOS, PluginContainer processes have their binary in a
+ // plugin-container.app/Content/MacOS/ directory.
+ nsCOMPtr<nsIFile> parentDir1, parentDir2;
+ if (NS_FAILED(libFile->GetParent(getter_AddRefs(parentDir1)))) {
+ return false;
+ }
+ if (NS_FAILED(parentDir1->GetParent(getter_AddRefs(parentDir2)))) {
+ return false;
+ }
+ if (NS_FAILED(parentDir2->GetParent(getter_AddRefs(libFile)))) {
+ return false;
+ }
+ }
+#endif
+
+ if (NS_FAILED(libFile->SetNativeLeafName(MOZ_DLL_PREFIX
+ "mozavutil" MOZ_DLL_SUFFIX ""_ns))) {
+ return false;
+ }
+ sFFVPXLib.mAVUtilLib = MozAVLink(libFile);
+
+ if (NS_FAILED(libFile->SetNativeLeafName(
+ MOZ_DLL_PREFIX "mozavcodec" MOZ_DLL_SUFFIX ""_ns))) {
+ return false;
+ }
+ sFFVPXLib.mAVCodecLib = MozAVLink(libFile);
+ if (sFFVPXLib.Link() == FFmpegLibWrapper::LinkResult::Success) {
+ sLinkStatus = LinkStatus_SUCCEEDED;
+ return true;
+ }
+ return false;
+}
+
+/* static */
+already_AddRefed<PlatformDecoderModule> FFVPXRuntimeLinker::Create() {
+ if (!Init()) {
+ return nullptr;
+ }
+ return FFmpegDecoderModule<FFVPX_VERSION>::Create(&sFFVPXLib);
+}
+
+/* static */
+void FFVPXRuntimeLinker::GetRDFTFuncs(FFmpegRDFTFuncs* aOutFuncs) {
+ MOZ_ASSERT(sLinkStatus != LinkStatus_INIT);
+ if (sFFVPXLib.av_rdft_init && sFFVPXLib.av_rdft_calc &&
+ sFFVPXLib.av_rdft_end) {
+ aOutFuncs->init = sFFVPXLib.av_rdft_init;
+ aOutFuncs->calc = sFFVPXLib.av_rdft_calc;
+ aOutFuncs->end = sFFVPXLib.av_rdft_end;
+ } else {
+ NS_WARNING("RDFT functions expected but not found");
+ *aOutFuncs = FFmpegRDFTFuncs(); // zero
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/ffmpeg/ffvpx/FFVPXRuntimeLinker.h b/dom/media/platforms/ffmpeg/ffvpx/FFVPXRuntimeLinker.h
new file mode 100644
index 0000000000..7c3e99b1c8
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffvpx/FFVPXRuntimeLinker.h
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef __FFVPXRuntimeLinker_h__
+#define __FFVPXRuntimeLinker_h__
+
+#include "PlatformDecoderModule.h"
+
+struct FFmpegRDFTFuncs;
+
+namespace mozilla {
+
+class FFVPXRuntimeLinker {
+ public:
+ // Main thread only.
+ static bool Init();
+ // Main thread or after Init().
+ static already_AddRefed<PlatformDecoderModule> Create();
+
+ // Call (on any thread) after Init().
+ static void GetRDFTFuncs(FFmpegRDFTFuncs* aOutFuncs);
+
+ private:
+ // Set once on the main thread and then read from other threads.
+ static enum LinkStatus {
+ LinkStatus_INIT = 0,
+ LinkStatus_FAILED,
+ LinkStatus_SUCCEEDED
+ } sLinkStatus;
+};
+
+} // namespace mozilla
+
+#endif /* __FFVPXRuntimeLinker_h__ */
diff --git a/dom/media/platforms/ffmpeg/ffvpx/moz.build b/dom/media/platforms/ffmpeg/ffvpx/moz.build
new file mode 100644
index 0000000000..23c90f9c9d
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/ffvpx/moz.build
@@ -0,0 +1,50 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+LOCAL_INCLUDES += ["/xpcom/build"]
+EXPORTS += [
+ "FFVPXRuntimeLinker.h",
+]
+
+UNIFIED_SOURCES += [
+ "../FFmpegAudioDecoder.cpp",
+ "../FFmpegDataDecoder.cpp",
+ "../FFmpegDecoderModule.cpp",
+ "../FFmpegVideoDecoder.cpp",
+]
+SOURCES += [
+ "FFVPXRuntimeLinker.cpp",
+]
+LOCAL_INCLUDES += [
+ "..",
+ "../ffmpeg60/include",
+ "/media/mozva",
+]
+
+CXXFLAGS += ["-Wno-deprecated-declarations"]
+if CONFIG["CC_TYPE"] == "clang":
+ CXXFLAGS += [
+ "-Wno-unknown-attributes",
+ ]
+if CONFIG["CC_TYPE"] == "gcc":
+ CXXFLAGS += [
+ "-Wno-attributes",
+ ]
+
+DEFINES["FFVPX_VERSION"] = 46465650
+DEFINES["USING_MOZFFVPX"] = True
+
+if CONFIG["MOZ_WAYLAND"]:
+ CXXFLAGS += CONFIG["MOZ_GTK3_CFLAGS"]
+if CONFIG["MOZ_ENABLE_VAAPI"] or CONFIG["MOZ_ENABLE_V4L2"]:
+ UNIFIED_SOURCES += ["../FFmpegVideoFramePool.cpp"]
+ LOCAL_INCLUDES += ["/third_party/drm/drm/include/libdrm/"]
+ USE_LIBS += ["mozva"]
+ DEFINES["MOZ_WAYLAND_USE_HWDECODE"] = 1
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/platforms/ffmpeg/libav53/include/COPYING.LGPLv2.1 b/dom/media/platforms/ffmpeg/libav53/include/COPYING.LGPLv2.1
new file mode 100644
index 0000000000..00b4fedfe7
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/COPYING.LGPLv2.1
@@ -0,0 +1,504 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the library's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+ <signature of Ty Coon>, 1 April 1990
+ Ty Coon, President of Vice
+
+That's all there is to it!
+
+
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavcodec/avcodec.h b/dom/media/platforms/ffmpeg/libav53/include/libavcodec/avcodec.h
new file mode 100644
index 0000000000..2451294c1b
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavcodec/avcodec.h
@@ -0,0 +1,4761 @@
+/*
+ * copyright (c) 2001 Fabrice Bellard
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_AVCODEC_H
+#define AVCODEC_AVCODEC_H
+
+/**
+ * @file
+ * external API header
+ */
+
+#include <errno.h>
+#include "libavutil/samplefmt.h"
+#include "libavutil/avutil.h"
+#include "libavutil/cpu.h"
+#include "libavutil/dict.h"
+#include "libavutil/log.h"
+#include "libavutil/pixfmt.h"
+#include "libavutil/rational.h"
+
+#include "libavcodec/version.h"
+/**
+ * @defgroup libavc Encoding/Decoding Library
+ * @{
+ *
+ * @defgroup lavc_decoding Decoding
+ * @{
+ * @}
+ *
+ * @defgroup lavc_encoding Encoding
+ * @{
+ * @}
+ *
+ * @defgroup lavc_codec Codecs
+ * @{
+ * @defgroup lavc_codec_native Native Codecs
+ * @{
+ * @}
+ * @defgroup lavc_codec_wrappers External library wrappers
+ * @{
+ * @}
+ * @defgroup lavc_codec_hwaccel Hardware Accelerators bridge
+ * @{
+ * @}
+ * @}
+ * @defgroup lavc_internal Internal
+ * @{
+ * @}
+ * @}
+ *
+ */
+
+
+/**
+ * Identify the syntax and semantics of the bitstream.
+ * The principle is roughly:
+ * Two decoders with the same ID can decode the same streams.
+ * Two encoders with the same ID can encode compatible streams.
+ * There may be slight deviations from the principle due to implementation
+ * details.
+ *
+ * If you add a codec ID to this list, add it so that
+ * 1. no value of a existing codec ID changes (that would break ABI),
+ * 2. it is as close as possible to similar codecs.
+ */
+enum CodecID {
+ CODEC_ID_NONE,
+
+ /* video codecs */
+ CODEC_ID_MPEG1VIDEO,
+ CODEC_ID_MPEG2VIDEO, ///< preferred ID for MPEG-1/2 video decoding
+ CODEC_ID_MPEG2VIDEO_XVMC,
+ CODEC_ID_H261,
+ CODEC_ID_H263,
+ CODEC_ID_RV10,
+ CODEC_ID_RV20,
+ CODEC_ID_MJPEG,
+ CODEC_ID_MJPEGB,
+ CODEC_ID_LJPEG,
+ CODEC_ID_SP5X,
+ CODEC_ID_JPEGLS,
+ CODEC_ID_MPEG4,
+ CODEC_ID_RAWVIDEO,
+ CODEC_ID_MSMPEG4V1,
+ CODEC_ID_MSMPEG4V2,
+ CODEC_ID_MSMPEG4V3,
+ CODEC_ID_WMV1,
+ CODEC_ID_WMV2,
+ CODEC_ID_H263P,
+ CODEC_ID_H263I,
+ CODEC_ID_FLV1,
+ CODEC_ID_SVQ1,
+ CODEC_ID_SVQ3,
+ CODEC_ID_DVVIDEO,
+ CODEC_ID_HUFFYUV,
+ CODEC_ID_CYUV,
+ CODEC_ID_H264,
+ CODEC_ID_INDEO3,
+ CODEC_ID_VP3,
+ CODEC_ID_THEORA,
+ CODEC_ID_ASV1,
+ CODEC_ID_ASV2,
+ CODEC_ID_FFV1,
+ CODEC_ID_4XM,
+ CODEC_ID_VCR1,
+ CODEC_ID_CLJR,
+ CODEC_ID_MDEC,
+ CODEC_ID_ROQ,
+ CODEC_ID_INTERPLAY_VIDEO,
+ CODEC_ID_XAN_WC3,
+ CODEC_ID_XAN_WC4,
+ CODEC_ID_RPZA,
+ CODEC_ID_CINEPAK,
+ CODEC_ID_WS_VQA,
+ CODEC_ID_MSRLE,
+ CODEC_ID_MSVIDEO1,
+ CODEC_ID_IDCIN,
+ CODEC_ID_8BPS,
+ CODEC_ID_SMC,
+ CODEC_ID_FLIC,
+ CODEC_ID_TRUEMOTION1,
+ CODEC_ID_VMDVIDEO,
+ CODEC_ID_MSZH,
+ CODEC_ID_ZLIB,
+ CODEC_ID_QTRLE,
+ CODEC_ID_SNOW,
+ CODEC_ID_TSCC,
+ CODEC_ID_ULTI,
+ CODEC_ID_QDRAW,
+ CODEC_ID_VIXL,
+ CODEC_ID_QPEG,
+ CODEC_ID_PNG,
+ CODEC_ID_PPM,
+ CODEC_ID_PBM,
+ CODEC_ID_PGM,
+ CODEC_ID_PGMYUV,
+ CODEC_ID_PAM,
+ CODEC_ID_FFVHUFF,
+ CODEC_ID_RV30,
+ CODEC_ID_RV40,
+ CODEC_ID_VC1,
+ CODEC_ID_WMV3,
+ CODEC_ID_LOCO,
+ CODEC_ID_WNV1,
+ CODEC_ID_AASC,
+ CODEC_ID_INDEO2,
+ CODEC_ID_FRAPS,
+ CODEC_ID_TRUEMOTION2,
+ CODEC_ID_BMP,
+ CODEC_ID_CSCD,
+ CODEC_ID_MMVIDEO,
+ CODEC_ID_ZMBV,
+ CODEC_ID_AVS,
+ CODEC_ID_SMACKVIDEO,
+ CODEC_ID_NUV,
+ CODEC_ID_KMVC,
+ CODEC_ID_FLASHSV,
+ CODEC_ID_CAVS,
+ CODEC_ID_JPEG2000,
+ CODEC_ID_VMNC,
+ CODEC_ID_VP5,
+ CODEC_ID_VP6,
+ CODEC_ID_VP6F,
+ CODEC_ID_TARGA,
+ CODEC_ID_DSICINVIDEO,
+ CODEC_ID_TIERTEXSEQVIDEO,
+ CODEC_ID_TIFF,
+ CODEC_ID_GIF,
+#if LIBAVCODEC_VERSION_MAJOR == 53
+ CODEC_ID_FFH264,
+#endif
+ CODEC_ID_DXA,
+ CODEC_ID_DNXHD,
+ CODEC_ID_THP,
+ CODEC_ID_SGI,
+ CODEC_ID_C93,
+ CODEC_ID_BETHSOFTVID,
+ CODEC_ID_PTX,
+ CODEC_ID_TXD,
+ CODEC_ID_VP6A,
+ CODEC_ID_AMV,
+ CODEC_ID_VB,
+ CODEC_ID_PCX,
+ CODEC_ID_SUNRAST,
+ CODEC_ID_INDEO4,
+ CODEC_ID_INDEO5,
+ CODEC_ID_MIMIC,
+ CODEC_ID_RL2,
+#if LIBAVCODEC_VERSION_MAJOR == 53
+ CODEC_ID_8SVX_EXP,
+ CODEC_ID_8SVX_FIB,
+#endif
+ CODEC_ID_ESCAPE124,
+ CODEC_ID_DIRAC,
+ CODEC_ID_BFI,
+ CODEC_ID_CMV,
+ CODEC_ID_MOTIONPIXELS,
+ CODEC_ID_TGV,
+ CODEC_ID_TGQ,
+ CODEC_ID_TQI,
+ CODEC_ID_AURA,
+ CODEC_ID_AURA2,
+ CODEC_ID_V210X,
+ CODEC_ID_TMV,
+ CODEC_ID_V210,
+ CODEC_ID_DPX,
+ CODEC_ID_MAD,
+ CODEC_ID_FRWU,
+ CODEC_ID_FLASHSV2,
+ CODEC_ID_CDGRAPHICS,
+ CODEC_ID_R210,
+ CODEC_ID_ANM,
+ CODEC_ID_BINKVIDEO,
+ CODEC_ID_IFF_ILBM,
+ CODEC_ID_IFF_BYTERUN1,
+ CODEC_ID_KGV1,
+ CODEC_ID_YOP,
+ CODEC_ID_VP8,
+ CODEC_ID_PICTOR,
+ CODEC_ID_ANSI,
+ CODEC_ID_A64_MULTI,
+ CODEC_ID_A64_MULTI5,
+ CODEC_ID_R10K,
+ CODEC_ID_MXPEG,
+ CODEC_ID_LAGARITH,
+ CODEC_ID_PRORES,
+ CODEC_ID_JV,
+ CODEC_ID_DFA,
+ CODEC_ID_WMV3IMAGE,
+ CODEC_ID_VC1IMAGE,
+#if LIBAVCODEC_VERSION_MAJOR == 53
+ CODEC_ID_G723_1,
+ CODEC_ID_G729,
+#endif
+ CODEC_ID_UTVIDEO,
+ CODEC_ID_BMV_VIDEO,
+ CODEC_ID_VBLE,
+ CODEC_ID_DXTORY,
+ CODEC_ID_V410,
+
+ /* various PCM "codecs" */
+ CODEC_ID_FIRST_AUDIO = 0x10000, ///< A dummy id pointing at the start of audio codecs
+ CODEC_ID_PCM_S16LE = 0x10000,
+ CODEC_ID_PCM_S16BE,
+ CODEC_ID_PCM_U16LE,
+ CODEC_ID_PCM_U16BE,
+ CODEC_ID_PCM_S8,
+ CODEC_ID_PCM_U8,
+ CODEC_ID_PCM_MULAW,
+ CODEC_ID_PCM_ALAW,
+ CODEC_ID_PCM_S32LE,
+ CODEC_ID_PCM_S32BE,
+ CODEC_ID_PCM_U32LE,
+ CODEC_ID_PCM_U32BE,
+ CODEC_ID_PCM_S24LE,
+ CODEC_ID_PCM_S24BE,
+ CODEC_ID_PCM_U24LE,
+ CODEC_ID_PCM_U24BE,
+ CODEC_ID_PCM_S24DAUD,
+ CODEC_ID_PCM_ZORK,
+ CODEC_ID_PCM_S16LE_PLANAR,
+ CODEC_ID_PCM_DVD,
+ CODEC_ID_PCM_F32BE,
+ CODEC_ID_PCM_F32LE,
+ CODEC_ID_PCM_F64BE,
+ CODEC_ID_PCM_F64LE,
+ CODEC_ID_PCM_BLURAY,
+ CODEC_ID_PCM_LXF,
+ CODEC_ID_S302M,
+ CODEC_ID_PCM_S8_PLANAR,
+
+ /* various ADPCM codecs */
+ CODEC_ID_ADPCM_IMA_QT = 0x11000,
+ CODEC_ID_ADPCM_IMA_WAV,
+ CODEC_ID_ADPCM_IMA_DK3,
+ CODEC_ID_ADPCM_IMA_DK4,
+ CODEC_ID_ADPCM_IMA_WS,
+ CODEC_ID_ADPCM_IMA_SMJPEG,
+ CODEC_ID_ADPCM_MS,
+ CODEC_ID_ADPCM_4XM,
+ CODEC_ID_ADPCM_XA,
+ CODEC_ID_ADPCM_ADX,
+ CODEC_ID_ADPCM_EA,
+ CODEC_ID_ADPCM_G726,
+ CODEC_ID_ADPCM_CT,
+ CODEC_ID_ADPCM_SWF,
+ CODEC_ID_ADPCM_YAMAHA,
+ CODEC_ID_ADPCM_SBPRO_4,
+ CODEC_ID_ADPCM_SBPRO_3,
+ CODEC_ID_ADPCM_SBPRO_2,
+ CODEC_ID_ADPCM_THP,
+ CODEC_ID_ADPCM_IMA_AMV,
+ CODEC_ID_ADPCM_EA_R1,
+ CODEC_ID_ADPCM_EA_R3,
+ CODEC_ID_ADPCM_EA_R2,
+ CODEC_ID_ADPCM_IMA_EA_SEAD,
+ CODEC_ID_ADPCM_IMA_EA_EACS,
+ CODEC_ID_ADPCM_EA_XAS,
+ CODEC_ID_ADPCM_EA_MAXIS_XA,
+ CODEC_ID_ADPCM_IMA_ISS,
+ CODEC_ID_ADPCM_G722,
+
+ /* AMR */
+ CODEC_ID_AMR_NB = 0x12000,
+ CODEC_ID_AMR_WB,
+
+ /* RealAudio codecs*/
+ CODEC_ID_RA_144 = 0x13000,
+ CODEC_ID_RA_288,
+
+ /* various DPCM codecs */
+ CODEC_ID_ROQ_DPCM = 0x14000,
+ CODEC_ID_INTERPLAY_DPCM,
+ CODEC_ID_XAN_DPCM,
+ CODEC_ID_SOL_DPCM,
+
+ /* audio codecs */
+ CODEC_ID_MP2 = 0x15000,
+ CODEC_ID_MP3, ///< preferred ID for decoding MPEG audio layer 1, 2 or 3
+ CODEC_ID_AAC,
+ CODEC_ID_AC3,
+ CODEC_ID_DTS,
+ CODEC_ID_VORBIS,
+ CODEC_ID_DVAUDIO,
+ CODEC_ID_WMAV1,
+ CODEC_ID_WMAV2,
+ CODEC_ID_MACE3,
+ CODEC_ID_MACE6,
+ CODEC_ID_VMDAUDIO,
+#if LIBAVCODEC_VERSION_MAJOR == 53
+ CODEC_ID_SONIC,
+ CODEC_ID_SONIC_LS,
+#endif
+ CODEC_ID_FLAC,
+ CODEC_ID_MP3ADU,
+ CODEC_ID_MP3ON4,
+ CODEC_ID_SHORTEN,
+ CODEC_ID_ALAC,
+ CODEC_ID_WESTWOOD_SND1,
+ CODEC_ID_GSM, ///< as in Berlin toast format
+ CODEC_ID_QDM2,
+ CODEC_ID_COOK,
+ CODEC_ID_TRUESPEECH,
+ CODEC_ID_TTA,
+ CODEC_ID_SMACKAUDIO,
+ CODEC_ID_QCELP,
+ CODEC_ID_WAVPACK,
+ CODEC_ID_DSICINAUDIO,
+ CODEC_ID_IMC,
+ CODEC_ID_MUSEPACK7,
+ CODEC_ID_MLP,
+ CODEC_ID_GSM_MS, /* as found in WAV */
+ CODEC_ID_ATRAC3,
+ CODEC_ID_VOXWARE,
+ CODEC_ID_APE,
+ CODEC_ID_NELLYMOSER,
+ CODEC_ID_MUSEPACK8,
+ CODEC_ID_SPEEX,
+ CODEC_ID_WMAVOICE,
+ CODEC_ID_WMAPRO,
+ CODEC_ID_WMALOSSLESS,
+ CODEC_ID_ATRAC3P,
+ CODEC_ID_EAC3,
+ CODEC_ID_SIPR,
+ CODEC_ID_MP1,
+ CODEC_ID_TWINVQ,
+ CODEC_ID_TRUEHD,
+ CODEC_ID_MP4ALS,
+ CODEC_ID_ATRAC1,
+ CODEC_ID_BINKAUDIO_RDFT,
+ CODEC_ID_BINKAUDIO_DCT,
+ CODEC_ID_AAC_LATM,
+ CODEC_ID_QDMC,
+ CODEC_ID_CELT,
+#if LIBAVCODEC_VERSION_MAJOR > 53
+ CODEC_ID_G723_1,
+ CODEC_ID_G729,
+ CODEC_ID_8SVX_EXP,
+ CODEC_ID_8SVX_FIB,
+#endif
+ CODEC_ID_BMV_AUDIO,
+
+ /* subtitle codecs */
+ CODEC_ID_FIRST_SUBTITLE = 0x17000, ///< A dummy ID pointing at the start of subtitle codecs.
+ CODEC_ID_DVD_SUBTITLE = 0x17000,
+ CODEC_ID_DVB_SUBTITLE,
+ CODEC_ID_TEXT, ///< raw UTF-8 text
+ CODEC_ID_XSUB,
+ CODEC_ID_SSA,
+ CODEC_ID_MOV_TEXT,
+ CODEC_ID_HDMV_PGS_SUBTITLE,
+ CODEC_ID_DVB_TELETEXT,
+ CODEC_ID_SRT,
+
+ /* other specific kind of codecs (generally used for attachments) */
+ CODEC_ID_FIRST_UNKNOWN = 0x18000, ///< A dummy ID pointing at the start of various fake codecs.
+ CODEC_ID_TTF = 0x18000,
+
+ CODEC_ID_PROBE = 0x19000, ///< codec_id is not known (like CODEC_ID_NONE) but lavf should attempt to identify it
+
+ CODEC_ID_MPEG2TS = 0x20000, /**< _FAKE_ codec to indicate a raw MPEG-2 TS
+ * stream (only used by libavformat) */
+ CODEC_ID_MPEG4SYSTEMS = 0x20001, /**< _FAKE_ codec to indicate a MPEG-4 Systems
+ * stream (only used by libavformat) */
+ CODEC_ID_FFMETADATA = 0x21000, ///< Dummy codec for streams containing only metadata information.
+};
+
+#if FF_API_OLD_SAMPLE_FMT
+#define SampleFormat AVSampleFormat
+
+#define SAMPLE_FMT_NONE AV_SAMPLE_FMT_NONE
+#define SAMPLE_FMT_U8 AV_SAMPLE_FMT_U8
+#define SAMPLE_FMT_S16 AV_SAMPLE_FMT_S16
+#define SAMPLE_FMT_S32 AV_SAMPLE_FMT_S32
+#define SAMPLE_FMT_FLT AV_SAMPLE_FMT_FLT
+#define SAMPLE_FMT_DBL AV_SAMPLE_FMT_DBL
+#define SAMPLE_FMT_NB AV_SAMPLE_FMT_NB
+#endif
+
+#if FF_API_OLD_AUDIOCONVERT
+#include "libavutil/audioconvert.h"
+
+/* Audio channel masks */
+#define CH_FRONT_LEFT AV_CH_FRONT_LEFT
+#define CH_FRONT_RIGHT AV_CH_FRONT_RIGHT
+#define CH_FRONT_CENTER AV_CH_FRONT_CENTER
+#define CH_LOW_FREQUENCY AV_CH_LOW_FREQUENCY
+#define CH_BACK_LEFT AV_CH_BACK_LEFT
+#define CH_BACK_RIGHT AV_CH_BACK_RIGHT
+#define CH_FRONT_LEFT_OF_CENTER AV_CH_FRONT_LEFT_OF_CENTER
+#define CH_FRONT_RIGHT_OF_CENTER AV_CH_FRONT_RIGHT_OF_CENTER
+#define CH_BACK_CENTER AV_CH_BACK_CENTER
+#define CH_SIDE_LEFT AV_CH_SIDE_LEFT
+#define CH_SIDE_RIGHT AV_CH_SIDE_RIGHT
+#define CH_TOP_CENTER AV_CH_TOP_CENTER
+#define CH_TOP_FRONT_LEFT AV_CH_TOP_FRONT_LEFT
+#define CH_TOP_FRONT_CENTER AV_CH_TOP_FRONT_CENTER
+#define CH_TOP_FRONT_RIGHT AV_CH_TOP_FRONT_RIGHT
+#define CH_TOP_BACK_LEFT AV_CH_TOP_BACK_LEFT
+#define CH_TOP_BACK_CENTER AV_CH_TOP_BACK_CENTER
+#define CH_TOP_BACK_RIGHT AV_CH_TOP_BACK_RIGHT
+#define CH_STEREO_LEFT AV_CH_STEREO_LEFT
+#define CH_STEREO_RIGHT AV_CH_STEREO_RIGHT
+
+/** Channel mask value used for AVCodecContext.request_channel_layout
+ to indicate that the user requests the channel order of the decoder output
+ to be the native codec channel order. */
+#define CH_LAYOUT_NATIVE AV_CH_LAYOUT_NATIVE
+
+/* Audio channel convenience macros */
+#define CH_LAYOUT_MONO AV_CH_LAYOUT_MONO
+#define CH_LAYOUT_STEREO AV_CH_LAYOUT_STEREO
+#define CH_LAYOUT_2_1 AV_CH_LAYOUT_2_1
+#define CH_LAYOUT_SURROUND AV_CH_LAYOUT_SURROUND
+#define CH_LAYOUT_4POINT0 AV_CH_LAYOUT_4POINT0
+#define CH_LAYOUT_2_2 AV_CH_LAYOUT_2_2
+#define CH_LAYOUT_QUAD AV_CH_LAYOUT_QUAD
+#define CH_LAYOUT_5POINT0 AV_CH_LAYOUT_5POINT0
+#define CH_LAYOUT_5POINT1 AV_CH_LAYOUT_5POINT1
+#define CH_LAYOUT_5POINT0_BACK AV_CH_LAYOUT_5POINT0_BACK
+#define CH_LAYOUT_5POINT1_BACK AV_CH_LAYOUT_5POINT1_BACK
+#define CH_LAYOUT_7POINT0 AV_CH_LAYOUT_7POINT0
+#define CH_LAYOUT_7POINT1 AV_CH_LAYOUT_7POINT1
+#define CH_LAYOUT_7POINT1_WIDE AV_CH_LAYOUT_7POINT1_WIDE
+#define CH_LAYOUT_STEREO_DOWNMIX AV_CH_LAYOUT_STEREO_DOWNMIX
+#endif
+
+#if FF_API_OLD_DECODE_AUDIO
+/* in bytes */
+#define AVCODEC_MAX_AUDIO_FRAME_SIZE 192000 // 1 second of 48khz 32bit audio
+#endif
+
+/**
+ * Required number of additionally allocated bytes at the end of the input bitstream for decoding.
+ * This is mainly needed because some optimized bitstream readers read
+ * 32 or 64 bit at once and could read over the end.<br>
+ * Note: If the first 23 bits of the additional bytes are not 0, then damaged
+ * MPEG bitstreams could cause overread and segfault.
+ */
+#define FF_INPUT_BUFFER_PADDING_SIZE 8
+
+/**
+ * minimum encoding buffer size
+ * Used to avoid some checks during header writing.
+ */
+#define FF_MIN_BUFFER_SIZE 16384
+
+
+/**
+ * motion estimation type.
+ */
+enum Motion_Est_ID {
+ ME_ZERO = 1, ///< no search, that is use 0,0 vector whenever one is needed
+ ME_FULL,
+ ME_LOG,
+ ME_PHODS,
+ ME_EPZS, ///< enhanced predictive zonal search
+ ME_X1, ///< reserved for experiments
+ ME_HEX, ///< hexagon based search
+ ME_UMH, ///< uneven multi-hexagon search
+ ME_ITER, ///< iterative search
+ ME_TESA, ///< transformed exhaustive search algorithm
+};
+
+enum AVDiscard{
+ /* We leave some space between them for extensions (drop some
+ * keyframes for intra-only or drop just some bidir frames). */
+ AVDISCARD_NONE =-16, ///< discard nothing
+ AVDISCARD_DEFAULT= 0, ///< discard useless packets like 0 size packets in avi
+ AVDISCARD_NONREF = 8, ///< discard all non reference
+ AVDISCARD_BIDIR = 16, ///< discard all bidirectional frames
+ AVDISCARD_NONKEY = 32, ///< discard all frames except keyframes
+ AVDISCARD_ALL = 48, ///< discard all
+};
+
+enum AVColorPrimaries{
+ AVCOL_PRI_BT709 =1, ///< also ITU-R BT1361 / IEC 61966-2-4 / SMPTE RP177 Annex B
+ AVCOL_PRI_UNSPECIFIED=2,
+ AVCOL_PRI_BT470M =4,
+ AVCOL_PRI_BT470BG =5, ///< also ITU-R BT601-6 625 / ITU-R BT1358 625 / ITU-R BT1700 625 PAL & SECAM
+ AVCOL_PRI_SMPTE170M =6, ///< also ITU-R BT601-6 525 / ITU-R BT1358 525 / ITU-R BT1700 NTSC
+ AVCOL_PRI_SMPTE240M =7, ///< functionally identical to above
+ AVCOL_PRI_FILM =8,
+ AVCOL_PRI_NB , ///< Not part of ABI
+};
+
+enum AVColorTransferCharacteristic{
+ AVCOL_TRC_BT709 =1, ///< also ITU-R BT1361
+ AVCOL_TRC_UNSPECIFIED=2,
+ AVCOL_TRC_GAMMA22 =4, ///< also ITU-R BT470M / ITU-R BT1700 625 PAL & SECAM
+ AVCOL_TRC_GAMMA28 =5, ///< also ITU-R BT470BG
+ AVCOL_TRC_NB , ///< Not part of ABI
+};
+
+enum AVColorSpace{
+ AVCOL_SPC_RGB =0,
+ AVCOL_SPC_BT709 =1, ///< also ITU-R BT1361 / IEC 61966-2-4 xvYCC709 / SMPTE RP177 Annex B
+ AVCOL_SPC_UNSPECIFIED=2,
+ AVCOL_SPC_FCC =4,
+ AVCOL_SPC_BT470BG =5, ///< also ITU-R BT601-6 625 / ITU-R BT1358 625 / ITU-R BT1700 625 PAL & SECAM / IEC 61966-2-4 xvYCC601
+ AVCOL_SPC_SMPTE170M =6, ///< also ITU-R BT601-6 525 / ITU-R BT1358 525 / ITU-R BT1700 NTSC / functionally identical to above
+ AVCOL_SPC_SMPTE240M =7,
+ AVCOL_SPC_NB , ///< Not part of ABI
+};
+
+enum AVColorRange{
+ AVCOL_RANGE_UNSPECIFIED=0,
+ AVCOL_RANGE_MPEG =1, ///< the normal 219*2^(n-8) "MPEG" YUV ranges
+ AVCOL_RANGE_JPEG =2, ///< the normal 2^n-1 "JPEG" YUV ranges
+ AVCOL_RANGE_NB , ///< Not part of ABI
+};
+
+/**
+ * X X 3 4 X X are luma samples,
+ * 1 2 1-6 are possible chroma positions
+ * X X 5 6 X 0 is undefined/unknown position
+ */
+enum AVChromaLocation{
+ AVCHROMA_LOC_UNSPECIFIED=0,
+ AVCHROMA_LOC_LEFT =1, ///< mpeg2/4, h264 default
+ AVCHROMA_LOC_CENTER =2, ///< mpeg1, jpeg, h263
+ AVCHROMA_LOC_TOPLEFT =3, ///< DV
+ AVCHROMA_LOC_TOP =4,
+ AVCHROMA_LOC_BOTTOMLEFT =5,
+ AVCHROMA_LOC_BOTTOM =6,
+ AVCHROMA_LOC_NB , ///< Not part of ABI
+};
+
+#if FF_API_FLAC_GLOBAL_OPTS
+/**
+ * LPC analysis type
+ */
+enum AVLPCType {
+ AV_LPC_TYPE_DEFAULT = -1, ///< use the codec default LPC type
+ AV_LPC_TYPE_NONE = 0, ///< do not use LPC prediction or use all zero coefficients
+ AV_LPC_TYPE_FIXED = 1, ///< fixed LPC coefficients
+ AV_LPC_TYPE_LEVINSON = 2, ///< Levinson-Durbin recursion
+ AV_LPC_TYPE_CHOLESKY = 3, ///< Cholesky factorization
+ AV_LPC_TYPE_NB , ///< Not part of ABI
+};
+#endif
+
+enum AVAudioServiceType {
+ AV_AUDIO_SERVICE_TYPE_MAIN = 0,
+ AV_AUDIO_SERVICE_TYPE_EFFECTS = 1,
+ AV_AUDIO_SERVICE_TYPE_VISUALLY_IMPAIRED = 2,
+ AV_AUDIO_SERVICE_TYPE_HEARING_IMPAIRED = 3,
+ AV_AUDIO_SERVICE_TYPE_DIALOGUE = 4,
+ AV_AUDIO_SERVICE_TYPE_COMMENTARY = 5,
+ AV_AUDIO_SERVICE_TYPE_EMERGENCY = 6,
+ AV_AUDIO_SERVICE_TYPE_VOICE_OVER = 7,
+ AV_AUDIO_SERVICE_TYPE_KARAOKE = 8,
+ AV_AUDIO_SERVICE_TYPE_NB , ///< Not part of ABI
+};
+
+typedef struct RcOverride{
+ int start_frame;
+ int end_frame;
+ int qscale; // If this is 0 then quality_factor will be used instead.
+ float quality_factor;
+} RcOverride;
+
+#define FF_MAX_B_FRAMES 16
+
+/* encoding support
+ These flags can be passed in AVCodecContext.flags before initialization.
+ Note: Not everything is supported yet.
+*/
+
+#define CODEC_FLAG_QSCALE 0x0002 ///< Use fixed qscale.
+#define CODEC_FLAG_4MV 0x0004 ///< 4 MV per MB allowed / advanced prediction for H.263.
+#define CODEC_FLAG_QPEL 0x0010 ///< Use qpel MC.
+#define CODEC_FLAG_GMC 0x0020 ///< Use GMC.
+#define CODEC_FLAG_MV0 0x0040 ///< Always try a MB with MV=<0,0>.
+/**
+ * The parent program guarantees that the input for B-frames containing
+ * streams is not written to for at least s->max_b_frames+1 frames, if
+ * this is not set the input will be copied.
+ */
+#define CODEC_FLAG_INPUT_PRESERVED 0x0100
+#define CODEC_FLAG_PASS1 0x0200 ///< Use internal 2pass ratecontrol in first pass mode.
+#define CODEC_FLAG_PASS2 0x0400 ///< Use internal 2pass ratecontrol in second pass mode.
+#define CODEC_FLAG_GRAY 0x2000 ///< Only decode/encode grayscale.
+#define CODEC_FLAG_EMU_EDGE 0x4000 ///< Don't draw edges.
+#define CODEC_FLAG_PSNR 0x8000 ///< error[?] variables will be set during encoding.
+#define CODEC_FLAG_TRUNCATED 0x00010000 /** Input bitstream might be truncated at a random
+ location instead of only at frame boundaries. */
+#define CODEC_FLAG_NORMALIZE_AQP 0x00020000 ///< Normalize adaptive quantization.
+#define CODEC_FLAG_INTERLACED_DCT 0x00040000 ///< Use interlaced DCT.
+#define CODEC_FLAG_LOW_DELAY 0x00080000 ///< Force low delay.
+#define CODEC_FLAG_GLOBAL_HEADER 0x00400000 ///< Place global headers in extradata instead of every keyframe.
+#define CODEC_FLAG_BITEXACT 0x00800000 ///< Use only bitexact stuff (except (I)DCT).
+/* Fx : Flag for h263+ extra options */
+#define CODEC_FLAG_AC_PRED 0x01000000 ///< H.263 advanced intra coding / MPEG-4 AC prediction
+#define CODEC_FLAG_CBP_RD 0x04000000 ///< Use rate distortion optimization for cbp.
+#define CODEC_FLAG_QP_RD 0x08000000 ///< Use rate distortion optimization for qp selectioon.
+#define CODEC_FLAG_LOOP_FILTER 0x00000800 ///< loop filter
+#define CODEC_FLAG_INTERLACED_ME 0x20000000 ///< interlaced motion estimation
+#define CODEC_FLAG_CLOSED_GOP 0x80000000
+#define CODEC_FLAG2_FAST 0x00000001 ///< Allow non spec compliant speedup tricks.
+#define CODEC_FLAG2_STRICT_GOP 0x00000002 ///< Strictly enforce GOP size.
+#define CODEC_FLAG2_NO_OUTPUT 0x00000004 ///< Skip bitstream encoding.
+#define CODEC_FLAG2_LOCAL_HEADER 0x00000008 ///< Place global headers at every keyframe instead of in extradata.
+#define CODEC_FLAG2_SKIP_RD 0x00004000 ///< RD optimal MB level residual skipping
+#define CODEC_FLAG2_CHUNKS 0x00008000 ///< Input bitstream might be truncated at a packet boundaries instead of only at frame boundaries.
+/**
+ * @defgroup deprecated_flags Deprecated codec flags
+ * Use corresponding private codec options instead.
+ * @{
+ */
+#if FF_API_MPEGVIDEO_GLOBAL_OPTS
+#define CODEC_FLAG_OBMC 0x00000001 ///< OBMC
+#define CODEC_FLAG_H263P_AIV 0x00000008 ///< H.263 alternative inter VLC
+#define CODEC_FLAG_PART 0x0080 ///< Use data partitioning.
+#define CODEC_FLAG_ALT_SCAN 0x00100000 ///< Use alternate scan.
+#define CODEC_FLAG_H263P_UMV 0x02000000 ///< unlimited motion vector
+#define CODEC_FLAG_H263P_SLICE_STRUCT 0x10000000
+#define CODEC_FLAG_SVCD_SCAN_OFFSET 0x40000000 ///< Will reserve space for SVCD scan offset user data.
+#define CODEC_FLAG2_INTRA_VLC 0x00000800 ///< Use MPEG-2 intra VLC table.
+#define CODEC_FLAG2_DROP_FRAME_TIMECODE 0x00002000 ///< timecode is in drop frame format.
+#define CODEC_FLAG2_NON_LINEAR_QUANT 0x00010000 ///< Use MPEG-2 nonlinear quantizer.
+#endif
+#if FF_API_MJPEG_GLOBAL_OPTS
+#define CODEC_FLAG_EXTERN_HUFF 0x1000 ///< Use external Huffman table (for MJPEG).
+#endif
+#if FF_API_X264_GLOBAL_OPTS
+#define CODEC_FLAG2_BPYRAMID 0x00000010 ///< H.264 allow B-frames to be used as references.
+#define CODEC_FLAG2_WPRED 0x00000020 ///< H.264 weighted biprediction for B-frames
+#define CODEC_FLAG2_MIXED_REFS 0x00000040 ///< H.264 one reference per partition, as opposed to one reference per macroblock
+#define CODEC_FLAG2_8X8DCT 0x00000080 ///< H.264 high profile 8x8 transform
+#define CODEC_FLAG2_FASTPSKIP 0x00000100 ///< H.264 fast pskip
+#define CODEC_FLAG2_AUD 0x00000200 ///< H.264 access unit delimiters
+#define CODEC_FLAG2_BRDO 0x00000400 ///< B-frame rate-distortion optimization
+#define CODEC_FLAG2_MBTREE 0x00040000 ///< Use macroblock tree ratecontrol (x264 only)
+#define CODEC_FLAG2_PSY 0x00080000 ///< Use psycho visual optimizations.
+#define CODEC_FLAG2_SSIM 0x00100000 ///< Compute SSIM during encoding, error[] values are undefined.
+#define CODEC_FLAG2_INTRA_REFRESH 0x00200000 ///< Use periodic insertion of intra blocks instead of keyframes.
+#endif
+#if FF_API_SNOW_GLOBAL_OPTS
+#define CODEC_FLAG2_MEMC_ONLY 0x00001000 ///< Only do ME/MC (I frames -> ref, P frame -> ME+MC).
+#endif
+#if FF_API_LAME_GLOBAL_OPTS
+#define CODEC_FLAG2_BIT_RESERVOIR 0x00020000 ///< Use a bit reservoir when encoding if possible
+#endif
+/**
+ * @}
+ */
+
+/* Unsupported options :
+ * Syntax Arithmetic coding (SAC)
+ * Reference Picture Selection
+ * Independent Segment Decoding */
+/* /Fx */
+/* codec capabilities */
+
+#define CODEC_CAP_DRAW_HORIZ_BAND 0x0001 ///< Decoder can use draw_horiz_band callback.
+/**
+ * Codec uses get_buffer() for allocating buffers and supports custom allocators.
+ * If not set, it might not use get_buffer() at all or use operations that
+ * assume the buffer was allocated by avcodec_default_get_buffer.
+ */
+#define CODEC_CAP_DR1 0x0002
+#if FF_API_PARSE_FRAME
+/* If 'parse_only' field is true, then avcodec_parse_frame() can be used. */
+#define CODEC_CAP_PARSE_ONLY 0x0004
+#endif
+#define CODEC_CAP_TRUNCATED 0x0008
+/* Codec can export data for HW decoding (XvMC). */
+#define CODEC_CAP_HWACCEL 0x0010
+/**
+ * Encoder or decoder requires flushing with NULL input at the end in order to
+ * give the complete and correct output.
+ *
+ * NOTE: If this flag is not set, the codec is guaranteed to never be fed with
+ * with NULL data. The user can still send NULL data to the public encode
+ * or decode function, but libavcodec will not pass it along to the codec
+ * unless this flag is set.
+ *
+ * Decoders:
+ * The decoder has a non-zero delay and needs to be fed with avpkt->data=NULL,
+ * avpkt->size=0 at the end to get the delayed data until the decoder no longer
+ * returns frames.
+ *
+ * Encoders:
+ * The encoder needs to be fed with NULL data at the end of encoding until the
+ * encoder no longer returns data.
+ *
+ * NOTE: For encoders implementing the AVCodec.encode2() function, setting this
+ * flag also means that the encoder must set the pts and duration for
+ * each output packet. If this flag is not set, the pts and duration will
+ * be determined by libavcodec from the input frame.
+ */
+#define CODEC_CAP_DELAY 0x0020
+/**
+ * Codec can be fed a final frame with a smaller size.
+ * This can be used to prevent truncation of the last audio samples.
+ */
+#define CODEC_CAP_SMALL_LAST_FRAME 0x0040
+/**
+ * Codec can export data for HW decoding (VDPAU).
+ */
+#define CODEC_CAP_HWACCEL_VDPAU 0x0080
+/**
+ * Codec can output multiple frames per AVPacket
+ * Normally demuxers return one frame at a time, demuxers which do not do
+ * are connected to a parser to split what they return into proper frames.
+ * This flag is reserved to the very rare category of codecs which have a
+ * bitstream that cannot be split into frames without timeconsuming
+ * operations like full decoding. Demuxers carring such bitstreams thus
+ * may return multiple frames in a packet. This has many disadvantages like
+ * prohibiting stream copy in many cases thus it should only be considered
+ * as a last resort.
+ */
+#define CODEC_CAP_SUBFRAMES 0x0100
+/**
+ * Codec is experimental and is thus avoided in favor of non experimental
+ * encoders
+ */
+#define CODEC_CAP_EXPERIMENTAL 0x0200
+/**
+ * Codec should fill in channel configuration and samplerate instead of container
+ */
+#define CODEC_CAP_CHANNEL_CONF 0x0400
+/**
+ * Codec is able to deal with negative linesizes
+ */
+#define CODEC_CAP_NEG_LINESIZES 0x0800
+/**
+ * Codec supports frame-level multithreading.
+ */
+#define CODEC_CAP_FRAME_THREADS 0x1000
+/**
+ * Codec supports slice-based (or partition-based) multithreading.
+ */
+#define CODEC_CAP_SLICE_THREADS 0x2000
+/**
+ * Codec supports changed parameters at any point.
+ */
+#define CODEC_CAP_PARAM_CHANGE 0x4000
+/**
+ * Codec supports avctx->thread_count == 0 (auto).
+ */
+#define CODEC_CAP_AUTO_THREADS 0x8000
+/**
+ * Audio encoder supports receiving a different number of samples in each call.
+ */
+#define CODEC_CAP_VARIABLE_FRAME_SIZE 0x10000
+
+//The following defines may change, don't expect compatibility if you use them.
+#define MB_TYPE_INTRA4x4 0x0001
+#define MB_TYPE_INTRA16x16 0x0002 //FIXME H.264-specific
+#define MB_TYPE_INTRA_PCM 0x0004 //FIXME H.264-specific
+#define MB_TYPE_16x16 0x0008
+#define MB_TYPE_16x8 0x0010
+#define MB_TYPE_8x16 0x0020
+#define MB_TYPE_8x8 0x0040
+#define MB_TYPE_INTERLACED 0x0080
+#define MB_TYPE_DIRECT2 0x0100 //FIXME
+#define MB_TYPE_ACPRED 0x0200
+#define MB_TYPE_GMC 0x0400
+#define MB_TYPE_SKIP 0x0800
+#define MB_TYPE_P0L0 0x1000
+#define MB_TYPE_P1L0 0x2000
+#define MB_TYPE_P0L1 0x4000
+#define MB_TYPE_P1L1 0x8000
+#define MB_TYPE_L0 (MB_TYPE_P0L0 | MB_TYPE_P1L0)
+#define MB_TYPE_L1 (MB_TYPE_P0L1 | MB_TYPE_P1L1)
+#define MB_TYPE_L0L1 (MB_TYPE_L0 | MB_TYPE_L1)
+#define MB_TYPE_QUANT 0x00010000
+#define MB_TYPE_CBP 0x00020000
+//Note bits 24-31 are reserved for codec specific use (h264 ref0, mpeg1 0mv, ...)
+
+/**
+ * Pan Scan area.
+ * This specifies the area which should be displayed.
+ * Note there may be multiple such areas for one frame.
+ */
+typedef struct AVPanScan{
+ /**
+ * id
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int id;
+
+ /**
+ * width and height in 1/16 pel
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int width;
+ int height;
+
+ /**
+ * position of the top left corner in 1/16 pel for up to 3 fields/frames
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int16_t position[3][2];
+}AVPanScan;
+
+#define FF_QSCALE_TYPE_MPEG1 0
+#define FF_QSCALE_TYPE_MPEG2 1
+#define FF_QSCALE_TYPE_H264 2
+#define FF_QSCALE_TYPE_VP56 3
+
+#define FF_BUFFER_TYPE_INTERNAL 1
+#define FF_BUFFER_TYPE_USER 2 ///< direct rendering buffers (image is (de)allocated by user)
+#define FF_BUFFER_TYPE_SHARED 4 ///< Buffer from somewhere else; don't deallocate image (data/base), all other tables are not shared.
+#define FF_BUFFER_TYPE_COPY 8 ///< Just a (modified) copy of some other buffer, don't deallocate anything.
+
+#if FF_API_OLD_FF_PICT_TYPES
+/* DEPRECATED, directly use the AV_PICTURE_TYPE_* enum values */
+#define FF_I_TYPE AV_PICTURE_TYPE_I ///< Intra
+#define FF_P_TYPE AV_PICTURE_TYPE_P ///< Predicted
+#define FF_B_TYPE AV_PICTURE_TYPE_B ///< Bi-dir predicted
+#define FF_S_TYPE AV_PICTURE_TYPE_S ///< S(GMC)-VOP MPEG4
+#define FF_SI_TYPE AV_PICTURE_TYPE_SI ///< Switching Intra
+#define FF_SP_TYPE AV_PICTURE_TYPE_SP ///< Switching Predicted
+#define FF_BI_TYPE AV_PICTURE_TYPE_BI
+#endif
+
+#define FF_BUFFER_HINTS_VALID 0x01 // Buffer hints value is meaningful (if 0 ignore).
+#define FF_BUFFER_HINTS_READABLE 0x02 // Codec will read from buffer.
+#define FF_BUFFER_HINTS_PRESERVE 0x04 // User must not alter buffer content.
+#define FF_BUFFER_HINTS_REUSABLE 0x08 // Codec will reuse the buffer (update).
+
+enum AVPacketSideDataType {
+ AV_PKT_DATA_PALETTE,
+ AV_PKT_DATA_NEW_EXTRADATA,
+ AV_PKT_DATA_PARAM_CHANGE,
+};
+
+typedef struct AVPacket {
+ /**
+ * Presentation timestamp in AVStream->time_base units; the time at which
+ * the decompressed packet will be presented to the user.
+ * Can be AV_NOPTS_VALUE if it is not stored in the file.
+ * pts MUST be larger or equal to dts as presentation cannot happen before
+ * decompression, unless one wants to view hex dumps. Some formats misuse
+ * the terms dts and pts/cts to mean something different. Such timestamps
+ * must be converted to true pts/dts before they are stored in AVPacket.
+ */
+ int64_t pts;
+ /**
+ * Decompression timestamp in AVStream->time_base units; the time at which
+ * the packet is decompressed.
+ * Can be AV_NOPTS_VALUE if it is not stored in the file.
+ */
+ int64_t dts;
+ uint8_t *data;
+ int size;
+ int stream_index;
+ /**
+ * A combination of AV_PKT_FLAG values
+ */
+ int flags;
+ /**
+ * Additional packet data that can be provided by the container.
+ * Packet can contain several types of side information.
+ */
+ struct {
+ uint8_t *data;
+ int size;
+ enum AVPacketSideDataType type;
+ } *side_data;
+ int side_data_elems;
+
+ /**
+ * Duration of this packet in AVStream->time_base units, 0 if unknown.
+ * Equals next_pts - this_pts in presentation order.
+ */
+ int duration;
+ void (*destruct)(struct AVPacket *);
+ void *priv;
+ int64_t pos; ///< byte position in stream, -1 if unknown
+
+ /**
+ * Time difference in AVStream->time_base units from the pts of this
+ * packet to the point at which the output from the decoder has converged
+ * independent from the availability of previous frames. That is, the
+ * frames are virtually identical no matter if decoding started from
+ * the very first frame or from this keyframe.
+ * Is AV_NOPTS_VALUE if unknown.
+ * This field is not the display duration of the current packet.
+ * This field has no meaning if the packet does not have AV_PKT_FLAG_KEY
+ * set.
+ *
+ * The purpose of this field is to allow seeking in streams that have no
+ * keyframes in the conventional sense. It corresponds to the
+ * recovery point SEI in H.264 and match_time_delta in NUT. It is also
+ * essential for some types of subtitle streams to ensure that all
+ * subtitles are correctly displayed after seeking.
+ */
+ int64_t convergence_duration;
+} AVPacket;
+#define AV_PKT_FLAG_KEY 0x0001 ///< The packet contains a keyframe
+#define AV_PKT_FLAG_CORRUPT 0x0002 ///< The packet content is corrupted
+
+/**
+ * An AV_PKT_DATA_PARAM_CHANGE side data packet is laid out as follows:
+ * u32le param_flags
+ * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_COUNT)
+ * s32le channel_count
+ * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_LAYOUT)
+ * u64le channel_layout
+ * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_SAMPLE_RATE)
+ * s32le sample_rate
+ * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_DIMENSIONS)
+ * s32le width
+ * s32le height
+ */
+
+enum AVSideDataParamChangeFlags {
+ AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_COUNT = 0x0001,
+ AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_LAYOUT = 0x0002,
+ AV_SIDE_DATA_PARAM_CHANGE_SAMPLE_RATE = 0x0004,
+ AV_SIDE_DATA_PARAM_CHANGE_DIMENSIONS = 0x0008,
+};
+
+/**
+ * Audio Video Frame.
+ * New fields can be added to the end of AVFRAME with minor version
+ * bumps. Removal, reordering and changes to existing fields require
+ * a major version bump.
+ * sizeof(AVFrame) must not be used outside libav*.
+ */
+typedef struct AVFrame {
+#if FF_API_DATA_POINTERS
+#define AV_NUM_DATA_POINTERS 4
+#else
+#define AV_NUM_DATA_POINTERS 8
+#endif
+ /**
+ * pointer to the picture/channel planes.
+ * This might be different from the first allocated byte
+ * - encoding: Set by user
+ * - decoding: set by AVCodecContext.get_buffer()
+ */
+ uint8_t *data[AV_NUM_DATA_POINTERS];
+
+ /**
+ * Size, in bytes, of the data for each picture/channel plane.
+ *
+ * For audio, only linesize[0] may be set. For planar audio, each channel
+ * plane must be the same size.
+ *
+ * - encoding: Set by user (video only)
+ * - decoding: set by AVCodecContext.get_buffer()
+ */
+ int linesize[AV_NUM_DATA_POINTERS];
+
+ /**
+ * pointer to the first allocated byte of the picture. Can be used in get_buffer/release_buffer.
+ * This isn't used by libavcodec unless the default get/release_buffer() is used.
+ * - encoding:
+ * - decoding:
+ */
+ uint8_t *base[AV_NUM_DATA_POINTERS];
+ /**
+ * 1 -> keyframe, 0-> not
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by libavcodec.
+ */
+ int key_frame;
+
+ /**
+ * Picture type of the frame, see ?_TYPE below.
+ * - encoding: Set by libavcodec. for coded_picture (and set by user for input).
+ * - decoding: Set by libavcodec.
+ */
+ enum AVPictureType pict_type;
+
+ /**
+ * presentation timestamp in time_base units (time when frame should be shown to user)
+ * If AV_NOPTS_VALUE then frame_rate = 1/time_base will be assumed.
+ * - encoding: MUST be set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int64_t pts;
+
+ /**
+ * picture number in bitstream order
+ * - encoding: set by
+ * - decoding: Set by libavcodec.
+ */
+ int coded_picture_number;
+ /**
+ * picture number in display order
+ * - encoding: set by
+ * - decoding: Set by libavcodec.
+ */
+ int display_picture_number;
+
+ /**
+ * quality (between 1 (good) and FF_LAMBDA_MAX (bad))
+ * - encoding: Set by libavcodec. for coded_picture (and set by user for input).
+ * - decoding: Set by libavcodec.
+ */
+ int quality;
+
+#if FF_API_AVFRAME_AGE
+ /**
+ * @deprecated unused
+ */
+ attribute_deprecated int age;
+#endif
+
+ /**
+ * is this picture used as reference
+ * The values for this are the same as the MpegEncContext.picture_structure
+ * variable, that is 1->top field, 2->bottom field, 3->frame/both fields.
+ * Set to 4 for delayed, non-reference frames.
+ * - encoding: unused
+ * - decoding: Set by libavcodec. (before get_buffer() call)).
+ */
+ int reference;
+
+ /**
+ * QP table
+ * - encoding: unused
+ * - decoding: Set by libavcodec.
+ */
+ int8_t *qscale_table;
+ /**
+ * QP store stride
+ * - encoding: unused
+ * - decoding: Set by libavcodec.
+ */
+ int qstride;
+
+ /**
+ * mbskip_table[mb]>=1 if MB didn't change
+ * stride= mb_width = (width+15)>>4
+ * - encoding: unused
+ * - decoding: Set by libavcodec.
+ */
+ uint8_t *mbskip_table;
+
+ /**
+ * motion vector table
+ * @code
+ * example:
+ * int mv_sample_log2= 4 - motion_subsample_log2;
+ * int mb_width= (width+15)>>4;
+ * int mv_stride= (mb_width << mv_sample_log2) + 1;
+ * motion_val[direction][x + y*mv_stride][0->mv_x, 1->mv_y];
+ * @endcode
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int16_t (*motion_val[2])[2];
+
+ /**
+ * macroblock type table
+ * mb_type_base + mb_width + 2
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ uint32_t *mb_type;
+
+ /**
+ * log2 of the size of the block which a single vector in motion_val represents:
+ * (4->16x16, 3->8x8, 2-> 4x4, 1-> 2x2)
+ * - encoding: unused
+ * - decoding: Set by libavcodec.
+ */
+ uint8_t motion_subsample_log2;
+
+ /**
+ * for some private data of the user
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ void *opaque;
+
+ /**
+ * error
+ * - encoding: Set by libavcodec. if flags&CODEC_FLAG_PSNR.
+ * - decoding: unused
+ */
+ uint64_t error[AV_NUM_DATA_POINTERS];
+
+ /**
+ * type of the buffer (to keep track of who has to deallocate data[*])
+ * - encoding: Set by the one who allocates it.
+ * - decoding: Set by the one who allocates it.
+ * Note: User allocated (direct rendering) & internal buffers cannot coexist currently.
+ */
+ int type;
+
+ /**
+ * When decoding, this signals how much the picture must be delayed.
+ * extra_delay = repeat_pict / (2*fps)
+ * - encoding: unused
+ * - decoding: Set by libavcodec.
+ */
+ int repeat_pict;
+
+ /**
+ *
+ */
+ int qscale_type;
+
+ /**
+ * The content of the picture is interlaced.
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec. (default 0)
+ */
+ int interlaced_frame;
+
+ /**
+ * If the content is interlaced, is top field displayed first.
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int top_field_first;
+
+ /**
+ * Pan scan.
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ AVPanScan *pan_scan;
+
+ /**
+ * Tell user application that palette has changed from previous frame.
+ * - encoding: ??? (no palette-enabled encoder yet)
+ * - decoding: Set by libavcodec. (default 0).
+ */
+ int palette_has_changed;
+
+ /**
+ * codec suggestion on buffer type if != 0
+ * - encoding: unused
+ * - decoding: Set by libavcodec. (before get_buffer() call)).
+ */
+ int buffer_hints;
+
+ /**
+ * DCT coefficients
+ * - encoding: unused
+ * - decoding: Set by libavcodec.
+ */
+ short *dct_coeff;
+
+ /**
+ * motion reference frame index
+ * the order in which these are stored can depend on the codec.
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int8_t *ref_index[2];
+
+ /**
+ * reordered opaque 64bit (generally an integer or a double precision float
+ * PTS but can be anything).
+ * The user sets AVCodecContext.reordered_opaque to represent the input at
+ * that time,
+ * the decoder reorders values as needed and sets AVFrame.reordered_opaque
+ * to exactly one of the values provided by the user through AVCodecContext.reordered_opaque
+ * @deprecated in favor of pkt_pts
+ * - encoding: unused
+ * - decoding: Read by user.
+ */
+ int64_t reordered_opaque;
+
+ /**
+ * hardware accelerator private data (Libav-allocated)
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ void *hwaccel_picture_private;
+
+ /**
+ * reordered pts from the last AVPacket that has been input into the decoder
+ * - encoding: unused
+ * - decoding: Read by user.
+ */
+ int64_t pkt_pts;
+
+ /**
+ * dts from the last AVPacket that has been input into the decoder
+ * - encoding: unused
+ * - decoding: Read by user.
+ */
+ int64_t pkt_dts;
+
+ /**
+ * the AVCodecContext which ff_thread_get_buffer() was last called on
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by libavcodec.
+ */
+ struct AVCodecContext *owner;
+
+ /**
+ * used by multithreading to store frame-specific info
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by libavcodec.
+ */
+ void *thread_opaque;
+
+ /**
+ * number of audio samples (per channel) described by this frame
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ int nb_samples;
+
+ /**
+ * pointers to the data planes/channels.
+ *
+ * For video, this should simply point to data[].
+ *
+ * For planar audio, each channel has a separate data pointer, and
+ * linesize[0] contains the size of each channel buffer.
+ * For packed audio, there is just one data pointer, and linesize[0]
+ * contains the total size of the buffer for all channels.
+ *
+ * Note: Both data and extended_data will always be set by get_buffer(),
+ * but for planar audio with more channels that can fit in data,
+ * extended_data must be used by the decoder in order to access all
+ * channels.
+ *
+ * encoding: unused
+ * decoding: set by AVCodecContext.get_buffer()
+ */
+ uint8_t **extended_data;
+
+ /**
+ * sample aspect ratio for the video frame, 0/1 if unknown\unspecified
+ * - encoding: unused
+ * - decoding: Read by user.
+ */
+ AVRational sample_aspect_ratio;
+
+ /**
+ * width and height of the video frame
+ * - encoding: unused
+ * - decoding: Read by user.
+ */
+ int width, height;
+
+ /**
+ * format of the frame, -1 if unknown or unset
+ * Values correspond to enum PixelFormat for video frames,
+ * enum AVSampleFormat for audio)
+ * - encoding: unused
+ * - decoding: Read by user.
+ */
+ int format;
+} AVFrame;
+
+struct AVCodecInternal;
+
+enum AVFieldOrder {
+ AV_FIELD_UNKNOWN,
+ AV_FIELD_PROGRESSIVE,
+ AV_FIELD_TT, //< Top coded_first, top displayed first
+ AV_FIELD_BB, //< Bottom coded first, bottom displayed first
+ AV_FIELD_TB, //< Top coded first, bottom displayed first
+ AV_FIELD_BT, //< Bottom coded first, top displayed first
+};
+
+/**
+ * main external API structure.
+ * New fields can be added to the end with minor version bumps.
+ * Removal, reordering and changes to existing fields require a major
+ * version bump.
+ * sizeof(AVCodecContext) must not be used outside libav*.
+ */
+typedef struct AVCodecContext {
+ /**
+ * information on struct for av_log
+ * - set by avcodec_alloc_context3
+ */
+ const AVClass *av_class;
+ /**
+ * the average bitrate
+ * - encoding: Set by user; unused for constant quantizer encoding.
+ * - decoding: Set by libavcodec. 0 or some bitrate if this info is available in the stream.
+ */
+ int bit_rate;
+
+ /**
+ * number of bits the bitstream is allowed to diverge from the reference.
+ * the reference can be CBR (for CBR pass1) or VBR (for pass2)
+ * - encoding: Set by user; unused for constant quantizer encoding.
+ * - decoding: unused
+ */
+ int bit_rate_tolerance;
+
+ /**
+ * CODEC_FLAG_*.
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int flags;
+
+ /**
+ * Some codecs need additional format info. It is stored here.
+ * If any muxer uses this then ALL demuxers/parsers AND encoders for the
+ * specific codec MUST set it correctly otherwise stream copy breaks.
+ * In general use of this field by muxers is not recommended.
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by libavcodec. (FIXME: Is this OK?)
+ */
+ int sub_id;
+
+ /**
+ * Motion estimation algorithm used for video coding.
+ * 1 (zero), 2 (full), 3 (log), 4 (phods), 5 (epzs), 6 (x1), 7 (hex),
+ * 8 (umh), 9 (iter), 10 (tesa) [7, 8, 10 are x264 specific, 9 is snow specific]
+ * - encoding: MUST be set by user.
+ * - decoding: unused
+ */
+ int me_method;
+
+ /**
+ * some codecs need / can use extradata like Huffman tables.
+ * mjpeg: Huffman tables
+ * rv10: additional flags
+ * mpeg4: global headers (they can be in the bitstream or here)
+ * The allocated memory should be FF_INPUT_BUFFER_PADDING_SIZE bytes larger
+ * than extradata_size to avoid prolems if it is read with the bitstream reader.
+ * The bytewise contents of extradata must not depend on the architecture or CPU endianness.
+ * - encoding: Set/allocated/freed by libavcodec.
+ * - decoding: Set/allocated/freed by user.
+ */
+ uint8_t *extradata;
+ int extradata_size;
+
+ /**
+ * This is the fundamental unit of time (in seconds) in terms
+ * of which frame timestamps are represented. For fixed-fps content,
+ * timebase should be 1/framerate and timestamp increments should be
+ * identically 1.
+ * - encoding: MUST be set by user.
+ * - decoding: Set by libavcodec.
+ */
+ AVRational time_base;
+
+ /* video only */
+ /**
+ * picture width / height.
+ * - encoding: MUST be set by user.
+ * - decoding: Set by libavcodec.
+ * Note: For compatibility it is possible to set this instead of
+ * coded_width/height before decoding.
+ */
+ int width, height;
+
+#define FF_ASPECT_EXTENDED 15
+
+ /**
+ * the number of pictures in a group of pictures, or 0 for intra_only
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int gop_size;
+
+ /**
+ * Pixel format, see PIX_FMT_xxx.
+ * May be set by the demuxer if known from headers.
+ * May be overriden by the decoder if it knows better.
+ * - encoding: Set by user.
+ * - decoding: Set by user if known, overridden by libavcodec if known
+ */
+ enum PixelFormat pix_fmt;
+
+ /**
+ * If non NULL, 'draw_horiz_band' is called by the libavcodec
+ * decoder to draw a horizontal band. It improves cache usage. Not
+ * all codecs can do that. You must check the codec capabilities
+ * beforehand.
+ * When multithreading is used, it may be called from multiple threads
+ * at the same time; threads might draw different parts of the same AVFrame,
+ * or multiple AVFrames, and there is no guarantee that slices will be drawn
+ * in order.
+ * The function is also used by hardware acceleration APIs.
+ * It is called at least once during frame decoding to pass
+ * the data needed for hardware render.
+ * In that mode instead of pixel data, AVFrame points to
+ * a structure specific to the acceleration API. The application
+ * reads the structure and can change some fields to indicate progress
+ * or mark state.
+ * - encoding: unused
+ * - decoding: Set by user.
+ * @param height the height of the slice
+ * @param y the y position of the slice
+ * @param type 1->top field, 2->bottom field, 3->frame
+ * @param offset offset into the AVFrame.data from which the slice should be read
+ */
+ void (*draw_horiz_band)(struct AVCodecContext *s,
+ const AVFrame *src, int offset[AV_NUM_DATA_POINTERS],
+ int y, int type, int height);
+
+ /* audio only */
+ int sample_rate; ///< samples per second
+ int channels; ///< number of audio channels
+
+ /**
+ * audio sample format
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ enum AVSampleFormat sample_fmt; ///< sample format
+
+ /* The following data should not be initialized. */
+ /**
+ * Samples per packet, initialized when calling 'init'.
+ */
+ int frame_size;
+ int frame_number; ///< audio or video frame number
+
+ /**
+ * Number of frames the decoded output will be delayed relative to
+ * the encoded input.
+ * - encoding: Set by libavcodec.
+ * - decoding: unused
+ */
+ int delay;
+
+ /* - encoding parameters */
+ float qcompress; ///< amount of qscale change between easy & hard scenes (0.0-1.0)
+ float qblur; ///< amount of qscale smoothing over time (0.0-1.0)
+
+ /**
+ * minimum quantizer
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int qmin;
+
+ /**
+ * maximum quantizer
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int qmax;
+
+ /**
+ * maximum quantizer difference between frames
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int max_qdiff;
+
+ /**
+ * maximum number of B-frames between non-B-frames
+ * Note: The output will be delayed by max_b_frames+1 relative to the input.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int max_b_frames;
+
+ /**
+ * qscale factor between IP and B-frames
+ * If > 0 then the last P-frame quantizer will be used (q= lastp_q*factor+offset).
+ * If < 0 then normal ratecontrol will be done (q= -normal_q*factor+offset).
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float b_quant_factor;
+
+ /** obsolete FIXME remove */
+ int rc_strategy;
+#define FF_RC_STRATEGY_XVID 1
+
+ int b_frame_strategy;
+
+ struct AVCodec *codec;
+
+ void *priv_data;
+
+ int rtp_payload_size; /* The size of the RTP payload: the coder will */
+ /* do its best to deliver a chunk with size */
+ /* below rtp_payload_size, the chunk will start */
+ /* with a start code on some codecs like H.263. */
+ /* This doesn't take account of any particular */
+ /* headers inside the transmitted RTP payload. */
+
+
+ /* The RTP callback: This function is called */
+ /* every time the encoder has a packet to send. */
+ /* It depends on the encoder if the data starts */
+ /* with a Start Code (it should). H.263 does. */
+ /* mb_nb contains the number of macroblocks */
+ /* encoded in the RTP payload. */
+ void (*rtp_callback)(struct AVCodecContext *avctx, void *data, int size, int mb_nb);
+
+ /* statistics, used for 2-pass encoding */
+ int mv_bits;
+ int header_bits;
+ int i_tex_bits;
+ int p_tex_bits;
+ int i_count;
+ int p_count;
+ int skip_count;
+ int misc_bits;
+
+ /**
+ * number of bits used for the previously encoded frame
+ * - encoding: Set by libavcodec.
+ * - decoding: unused
+ */
+ int frame_bits;
+
+ /**
+ * Private data of the user, can be used to carry app specific stuff.
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ void *opaque;
+
+ char codec_name[32];
+ enum AVMediaType codec_type; /* see AVMEDIA_TYPE_xxx */
+ enum CodecID codec_id; /* see CODEC_ID_xxx */
+
+ /**
+ * fourcc (LSB first, so "ABCD" -> ('D'<<24) + ('C'<<16) + ('B'<<8) + 'A').
+ * This is used to work around some encoder bugs.
+ * A demuxer should set this to what is stored in the field used to identify the codec.
+ * If there are multiple such fields in a container then the demuxer should choose the one
+ * which maximizes the information about the used codec.
+ * If the codec tag field in a container is larger than 32 bits then the demuxer should
+ * remap the longer ID to 32 bits with a table or other structure. Alternatively a new
+ * extra_codec_tag + size could be added but for this a clear advantage must be demonstrated
+ * first.
+ * - encoding: Set by user, if not then the default based on codec_id will be used.
+ * - decoding: Set by user, will be converted to uppercase by libavcodec during init.
+ */
+ unsigned int codec_tag;
+
+ /**
+ * Work around bugs in encoders which sometimes cannot be detected automatically.
+ * - encoding: Set by user
+ * - decoding: Set by user
+ */
+ int workaround_bugs;
+#define FF_BUG_AUTODETECT 1 ///< autodetection
+#define FF_BUG_OLD_MSMPEG4 2
+#define FF_BUG_XVID_ILACE 4
+#define FF_BUG_UMP4 8
+#define FF_BUG_NO_PADDING 16
+#define FF_BUG_AMV 32
+#define FF_BUG_AC_VLC 0 ///< Will be removed, libavcodec can now handle these non-compliant files by default.
+#define FF_BUG_QPEL_CHROMA 64
+#define FF_BUG_STD_QPEL 128
+#define FF_BUG_QPEL_CHROMA2 256
+#define FF_BUG_DIRECT_BLOCKSIZE 512
+#define FF_BUG_EDGE 1024
+#define FF_BUG_HPEL_CHROMA 2048
+#define FF_BUG_DC_CLIP 4096
+#define FF_BUG_MS 8192 ///< Work around various bugs in Microsoft's broken decoders.
+#define FF_BUG_TRUNCATED 16384
+//#define FF_BUG_FAKE_SCALABILITY 16 //Autodetection should work 100%.
+
+ /**
+ * luma single coefficient elimination threshold
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int luma_elim_threshold;
+
+ /**
+ * chroma single coeff elimination threshold
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int chroma_elim_threshold;
+
+ /**
+ * strictly follow the standard (MPEG4, ...).
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ * Setting this to STRICT or higher means the encoder and decoder will
+ * generally do stupid things, whereas setting it to unofficial or lower
+ * will mean the encoder might produce output that is not supported by all
+ * spec-compliant decoders. Decoders don't differentiate between normal,
+ * unofficial and experimental (that is, they always try to decode things
+ * when they can) unless they are explicitly asked to behave stupidly
+ * (=strictly conform to the specs)
+ */
+ int strict_std_compliance;
+#define FF_COMPLIANCE_VERY_STRICT 2 ///< Strictly conform to an older more strict version of the spec or reference software.
+#define FF_COMPLIANCE_STRICT 1 ///< Strictly conform to all the things in the spec no matter what consequences.
+#define FF_COMPLIANCE_NORMAL 0
+#define FF_COMPLIANCE_UNOFFICIAL -1 ///< Allow unofficial extensions
+#define FF_COMPLIANCE_EXPERIMENTAL -2 ///< Allow nonstandardized experimental things.
+
+ /**
+ * qscale offset between IP and B-frames
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float b_quant_offset;
+
+#if FF_API_ER
+ /**
+ * Error recognition; higher values will detect more errors but may
+ * misdetect some more or less valid parts as errors.
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ attribute_deprecated int error_recognition;
+#define FF_ER_CAREFUL 1
+#define FF_ER_COMPLIANT 2
+#define FF_ER_AGGRESSIVE 3
+#define FF_ER_VERY_AGGRESSIVE 4
+#define FF_ER_EXPLODE 5
+#endif /* FF_API_ER */
+
+ /**
+ * Called at the beginning of each frame to get a buffer for it.
+ *
+ * The function will set AVFrame.data[], AVFrame.linesize[].
+ * AVFrame.extended_data[] must also be set, but it should be the same as
+ * AVFrame.data[] except for planar audio with more channels than can fit
+ * in AVFrame.data[]. In that case, AVFrame.data[] shall still contain as
+ * many data pointers as it can hold.
+ *
+ * if CODEC_CAP_DR1 is not set then get_buffer() must call
+ * avcodec_default_get_buffer() instead of providing buffers allocated by
+ * some other means.
+ *
+ * AVFrame.data[] should be 32- or 16-byte-aligned unless the CPU doesn't
+ * need it. avcodec_default_get_buffer() aligns the output buffer properly,
+ * but if get_buffer() is overridden then alignment considerations should
+ * be taken into account.
+ *
+ * @see avcodec_default_get_buffer()
+ *
+ * Video:
+ *
+ * If pic.reference is set then the frame will be read later by libavcodec.
+ * avcodec_align_dimensions2() should be used to find the required width and
+ * height, as they normally need to be rounded up to the next multiple of 16.
+ *
+ * If frame multithreading is used and thread_safe_callbacks is set,
+ * it may be called from a different thread, but not from more than one at
+ * once. Does not need to be reentrant.
+ *
+ * @see release_buffer(), reget_buffer()
+ * @see avcodec_align_dimensions2()
+ *
+ * Audio:
+ *
+ * Decoders request a buffer of a particular size by setting
+ * AVFrame.nb_samples prior to calling get_buffer(). The decoder may,
+ * however, utilize only part of the buffer by setting AVFrame.nb_samples
+ * to a smaller value in the output frame.
+ *
+ * Decoders cannot use the buffer after returning from
+ * avcodec_decode_audio4(), so they will not call release_buffer(), as it
+ * is assumed to be released immediately upon return.
+ *
+ * As a convenience, av_samples_get_buffer_size() and
+ * av_samples_fill_arrays() in libavutil may be used by custom get_buffer()
+ * functions to find the required data size and to fill data pointers and
+ * linesize. In AVFrame.linesize, only linesize[0] may be set for audio
+ * since all planes must be the same size.
+ *
+ * @see av_samples_get_buffer_size(), av_samples_fill_arrays()
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec, user can override.
+ */
+ int (*get_buffer)(struct AVCodecContext *c, AVFrame *pic);
+
+ /**
+ * Called to release buffers which were allocated with get_buffer.
+ * A released buffer can be reused in get_buffer().
+ * pic.data[*] must be set to NULL.
+ * May be called from a different thread if frame multithreading is used,
+ * but not by more than one thread at once, so does not need to be reentrant.
+ * - encoding: unused
+ * - decoding: Set by libavcodec, user can override.
+ */
+ void (*release_buffer)(struct AVCodecContext *c, AVFrame *pic);
+
+ /**
+ * Size of the frame reordering buffer in the decoder.
+ * For MPEG-2 it is 1 IPB or 0 low delay IP.
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by libavcodec.
+ */
+ int has_b_frames;
+
+ /**
+ * number of bytes per packet if constant and known or 0
+ * Used by some WAV based audio codecs.
+ */
+ int block_align;
+
+#if FF_API_PARSE_FRAME
+ /**
+ * If true, only parsing is done. The frame data is returned.
+ * Only MPEG audio decoders support this now.
+ * - encoding: unused
+ * - decoding: Set by user
+ */
+ attribute_deprecated int parse_only;
+#endif
+
+ /**
+ * 0-> h263 quant 1-> mpeg quant
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mpeg_quant;
+
+ /**
+ * pass1 encoding statistics output buffer
+ * - encoding: Set by libavcodec.
+ * - decoding: unused
+ */
+ char *stats_out;
+
+ /**
+ * pass2 encoding statistics input buffer
+ * Concatenated stuff from stats_out of pass1 should be placed here.
+ * - encoding: Allocated/set/freed by user.
+ * - decoding: unused
+ */
+ char *stats_in;
+
+ /**
+ * ratecontrol qmin qmax limiting method
+ * 0-> clipping, 1-> use a nice continous function to limit qscale wthin qmin/qmax.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float rc_qsquish;
+
+ float rc_qmod_amp;
+ int rc_qmod_freq;
+
+ /**
+ * ratecontrol override, see RcOverride
+ * - encoding: Allocated/set/freed by user.
+ * - decoding: unused
+ */
+ RcOverride *rc_override;
+ int rc_override_count;
+
+ /**
+ * rate control equation
+ * - encoding: Set by user
+ * - decoding: unused
+ */
+ const char *rc_eq;
+
+ /**
+ * maximum bitrate
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int rc_max_rate;
+
+ /**
+ * minimum bitrate
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int rc_min_rate;
+
+ /**
+ * decoder bitstream buffer size
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int rc_buffer_size;
+ float rc_buffer_aggressivity;
+
+ /**
+ * qscale factor between P and I-frames
+ * If > 0 then the last p frame quantizer will be used (q= lastp_q*factor+offset).
+ * If < 0 then normal ratecontrol will be done (q= -normal_q*factor+offset).
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float i_quant_factor;
+
+ /**
+ * qscale offset between P and I-frames
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float i_quant_offset;
+
+ /**
+ * initial complexity for pass1 ratecontrol
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float rc_initial_cplx;
+
+ /**
+ * DCT algorithm, see FF_DCT_* below
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int dct_algo;
+#define FF_DCT_AUTO 0
+#define FF_DCT_FASTINT 1
+#define FF_DCT_INT 2
+#define FF_DCT_MMX 3
+#define FF_DCT_MLIB 4
+#define FF_DCT_ALTIVEC 5
+#define FF_DCT_FAAN 6
+
+ /**
+ * luminance masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float lumi_masking;
+
+ /**
+ * temporary complexity masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float temporal_cplx_masking;
+
+ /**
+ * spatial complexity masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float spatial_cplx_masking;
+
+ /**
+ * p block masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float p_masking;
+
+ /**
+ * darkness masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float dark_masking;
+
+ /**
+ * IDCT algorithm, see FF_IDCT_* below.
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int idct_algo;
+#define FF_IDCT_AUTO 0
+#define FF_IDCT_INT 1
+#define FF_IDCT_SIMPLE 2
+#define FF_IDCT_SIMPLEMMX 3
+#define FF_IDCT_LIBMPEG2MMX 4
+#define FF_IDCT_PS2 5
+#define FF_IDCT_MLIB 6
+#define FF_IDCT_ARM 7
+#define FF_IDCT_ALTIVEC 8
+#define FF_IDCT_SH4 9
+#define FF_IDCT_SIMPLEARM 10
+#define FF_IDCT_H264 11
+#define FF_IDCT_VP3 12
+#define FF_IDCT_IPP 13
+#define FF_IDCT_XVIDMMX 14
+#define FF_IDCT_CAVS 15
+#define FF_IDCT_SIMPLEARMV5TE 16
+#define FF_IDCT_SIMPLEARMV6 17
+#define FF_IDCT_SIMPLEVIS 18
+#define FF_IDCT_WMV2 19
+#define FF_IDCT_FAAN 20
+#define FF_IDCT_EA 21
+#define FF_IDCT_SIMPLENEON 22
+#define FF_IDCT_SIMPLEALPHA 23
+#define FF_IDCT_BINK 24
+
+ /**
+ * slice count
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by user (or 0).
+ */
+ int slice_count;
+ /**
+ * slice offsets in the frame in bytes
+ * - encoding: Set/allocated by libavcodec.
+ * - decoding: Set/allocated by user (or NULL).
+ */
+ int *slice_offset;
+
+ /**
+ * error concealment flags
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int error_concealment;
+#define FF_EC_GUESS_MVS 1
+#define FF_EC_DEBLOCK 2
+
+ /**
+ * dsp_mask could be add used to disable unwanted CPU features
+ * CPU features (i.e. MMX, SSE. ...)
+ *
+ * With the FORCE flag you may instead enable given CPU features.
+ * (Dangerous: Usable in case of misdetection, improper usage however will
+ * result into program crash.)
+ */
+ unsigned dsp_mask;
+
+ /**
+ * bits per sample/pixel from the demuxer (needed for huffyuv).
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by user.
+ */
+ int bits_per_coded_sample;
+
+ /**
+ * prediction method (needed for huffyuv)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int prediction_method;
+#define FF_PRED_LEFT 0
+#define FF_PRED_PLANE 1
+#define FF_PRED_MEDIAN 2
+
+ /**
+ * sample aspect ratio (0 if unknown)
+ * That is the width of a pixel divided by the height of the pixel.
+ * Numerator and denominator must be relatively prime and smaller than 256 for some video standards.
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ AVRational sample_aspect_ratio;
+
+ /**
+ * the picture in the bitstream
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by libavcodec.
+ */
+ AVFrame *coded_frame;
+
+ /**
+ * debug
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int debug;
+#define FF_DEBUG_PICT_INFO 1
+#define FF_DEBUG_RC 2
+#define FF_DEBUG_BITSTREAM 4
+#define FF_DEBUG_MB_TYPE 8
+#define FF_DEBUG_QP 16
+#define FF_DEBUG_MV 32
+#define FF_DEBUG_DCT_COEFF 0x00000040
+#define FF_DEBUG_SKIP 0x00000080
+#define FF_DEBUG_STARTCODE 0x00000100
+#define FF_DEBUG_PTS 0x00000200
+#define FF_DEBUG_ER 0x00000400
+#define FF_DEBUG_MMCO 0x00000800
+#define FF_DEBUG_BUGS 0x00001000
+#define FF_DEBUG_VIS_QP 0x00002000
+#define FF_DEBUG_VIS_MB_TYPE 0x00004000
+#define FF_DEBUG_BUFFERS 0x00008000
+#define FF_DEBUG_THREADS 0x00010000
+
+ /**
+ * debug
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int debug_mv;
+#define FF_DEBUG_VIS_MV_P_FOR 0x00000001 //visualize forward predicted MVs of P frames
+#define FF_DEBUG_VIS_MV_B_FOR 0x00000002 //visualize forward predicted MVs of B frames
+#define FF_DEBUG_VIS_MV_B_BACK 0x00000004 //visualize backward predicted MVs of B frames
+
+ /**
+ * error
+ * - encoding: Set by libavcodec if flags&CODEC_FLAG_PSNR.
+ * - decoding: unused
+ */
+ uint64_t error[AV_NUM_DATA_POINTERS];
+
+ /**
+ * motion estimation comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_cmp;
+ /**
+ * subpixel motion estimation comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_sub_cmp;
+ /**
+ * macroblock comparison function (not supported yet)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mb_cmp;
+ /**
+ * interlaced DCT comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int ildct_cmp;
+#define FF_CMP_SAD 0
+#define FF_CMP_SSE 1
+#define FF_CMP_SATD 2
+#define FF_CMP_DCT 3
+#define FF_CMP_PSNR 4
+#define FF_CMP_BIT 5
+#define FF_CMP_RD 6
+#define FF_CMP_ZERO 7
+#define FF_CMP_VSAD 8
+#define FF_CMP_VSSE 9
+#define FF_CMP_NSSE 10
+#define FF_CMP_W53 11
+#define FF_CMP_W97 12
+#define FF_CMP_DCTMAX 13
+#define FF_CMP_DCT264 14
+#define FF_CMP_CHROMA 256
+
+ /**
+ * ME diamond size & shape
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int dia_size;
+
+ /**
+ * amount of previous MV predictors (2a+1 x 2a+1 square)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int last_predictor_count;
+
+ /**
+ * prepass for motion estimation
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int pre_me;
+
+ /**
+ * motion estimation prepass comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_pre_cmp;
+
+ /**
+ * ME prepass diamond size & shape
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int pre_dia_size;
+
+ /**
+ * subpel ME quality
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_subpel_quality;
+
+ /**
+ * callback to negotiate the pixelFormat
+ * @param fmt is the list of formats which are supported by the codec,
+ * it is terminated by -1 as 0 is a valid format, the formats are ordered by quality.
+ * The first is always the native one.
+ * @return the chosen format
+ * - encoding: unused
+ * - decoding: Set by user, if not set the native format will be chosen.
+ */
+ enum PixelFormat (*get_format)(struct AVCodecContext *s, const enum PixelFormat * fmt);
+
+ /**
+ * DTG active format information (additional aspect ratio
+ * information only used in DVB MPEG-2 transport streams)
+ * 0 if not set.
+ *
+ * - encoding: unused
+ * - decoding: Set by decoder.
+ */
+ int dtg_active_format;
+#define FF_DTG_AFD_SAME 8
+#define FF_DTG_AFD_4_3 9
+#define FF_DTG_AFD_16_9 10
+#define FF_DTG_AFD_14_9 11
+#define FF_DTG_AFD_4_3_SP_14_9 13
+#define FF_DTG_AFD_16_9_SP_14_9 14
+#define FF_DTG_AFD_SP_4_3 15
+
+ /**
+ * maximum motion estimation search range in subpel units
+ * If 0 then no limit.
+ *
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_range;
+
+ /**
+ * intra quantizer bias
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int intra_quant_bias;
+#define FF_DEFAULT_QUANT_BIAS 999999
+
+ /**
+ * inter quantizer bias
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int inter_quant_bias;
+
+ /**
+ * color table ID
+ * - encoding: unused
+ * - decoding: Which clrtable should be used for 8bit RGB images.
+ * Tables have to be stored somewhere. FIXME
+ */
+ int color_table_id;
+
+#if FF_API_INTERNAL_CONTEXT
+ /**
+ * internal_buffer count
+ * Don't touch, used by libavcodec default_get_buffer().
+ * @deprecated this field was moved to an internal context
+ */
+ attribute_deprecated int internal_buffer_count;
+
+ /**
+ * internal_buffers
+ * Don't touch, used by libavcodec default_get_buffer().
+ * @deprecated this field was moved to an internal context
+ */
+ attribute_deprecated void *internal_buffer;
+#endif
+
+ /**
+ * Global quality for codecs which cannot change it per frame.
+ * This should be proportional to MPEG-1/2/4 qscale.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int global_quality;
+
+#define FF_CODER_TYPE_VLC 0
+#define FF_CODER_TYPE_AC 1
+#define FF_CODER_TYPE_RAW 2
+#define FF_CODER_TYPE_RLE 3
+#define FF_CODER_TYPE_DEFLATE 4
+ /**
+ * coder type
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int coder_type;
+
+ /**
+ * context model
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int context_model;
+#if 0
+ /**
+ *
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ uint8_t * (*realloc)(struct AVCodecContext *s, uint8_t *buf, int buf_size);
+#endif
+
+ /**
+ * slice flags
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int slice_flags;
+#define SLICE_FLAG_CODED_ORDER 0x0001 ///< draw_horiz_band() is called in coded order instead of display
+#define SLICE_FLAG_ALLOW_FIELD 0x0002 ///< allow draw_horiz_band() with field slices (MPEG2 field pics)
+#define SLICE_FLAG_ALLOW_PLANE 0x0004 ///< allow draw_horiz_band() with 1 component at a time (SVQ1)
+
+ /**
+ * XVideo Motion Acceleration
+ * - encoding: forbidden
+ * - decoding: set by decoder
+ */
+ int xvmc_acceleration;
+
+ /**
+ * macroblock decision mode
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mb_decision;
+#define FF_MB_DECISION_SIMPLE 0 ///< uses mb_cmp
+#define FF_MB_DECISION_BITS 1 ///< chooses the one which needs the fewest bits
+#define FF_MB_DECISION_RD 2 ///< rate distortion
+
+ /**
+ * custom intra quantization matrix
+ * - encoding: Set by user, can be NULL.
+ * - decoding: Set by libavcodec.
+ */
+ uint16_t *intra_matrix;
+
+ /**
+ * custom inter quantization matrix
+ * - encoding: Set by user, can be NULL.
+ * - decoding: Set by libavcodec.
+ */
+ uint16_t *inter_matrix;
+
+ /**
+ * fourcc from the AVI stream header (LSB first, so "ABCD" -> ('D'<<24) + ('C'<<16) + ('B'<<8) + 'A').
+ * This is used to work around some encoder bugs.
+ * - encoding: unused
+ * - decoding: Set by user, will be converted to uppercase by libavcodec during init.
+ */
+ unsigned int stream_codec_tag;
+
+ /**
+ * scene change detection threshold
+ * 0 is default, larger means fewer detected scene changes.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int scenechange_threshold;
+
+ /**
+ * minimum Lagrange multipler
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int lmin;
+
+ /**
+ * maximum Lagrange multipler
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int lmax;
+
+#if FF_API_PALETTE_CONTROL
+ /**
+ * palette control structure
+ * - encoding: ??? (no palette-enabled encoder yet)
+ * - decoding: Set by user.
+ */
+ struct AVPaletteControl *palctrl;
+#endif
+
+ /**
+ * noise reduction strength
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int noise_reduction;
+
+ /**
+ * Called at the beginning of a frame to get cr buffer for it.
+ * Buffer type (size, hints) must be the same. libavcodec won't check it.
+ * libavcodec will pass previous buffer in pic, function should return
+ * same buffer or new buffer with old frame "painted" into it.
+ * If pic.data[0] == NULL must behave like get_buffer().
+ * if CODEC_CAP_DR1 is not set then reget_buffer() must call
+ * avcodec_default_reget_buffer() instead of providing buffers allocated by
+ * some other means.
+ * - encoding: unused
+ * - decoding: Set by libavcodec, user can override.
+ */
+ int (*reget_buffer)(struct AVCodecContext *c, AVFrame *pic);
+
+ /**
+ * Number of bits which should be loaded into the rc buffer before decoding starts.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int rc_initial_buffer_occupancy;
+
+ /**
+ *
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int inter_threshold;
+
+ /**
+ * CODEC_FLAG2_*
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int flags2;
+
+ /**
+ * Simulates errors in the bitstream to test error concealment.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int error_rate;
+
+#if FF_API_ANTIALIAS_ALGO
+ /**
+ * MP3 antialias algorithm, see FF_AA_* below.
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ attribute_deprecated int antialias_algo;
+#define FF_AA_AUTO 0
+#define FF_AA_FASTINT 1 //not implemented yet
+#define FF_AA_INT 2
+#define FF_AA_FLOAT 3
+#endif
+
+ /**
+ * quantizer noise shaping
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int quantizer_noise_shaping;
+
+ /**
+ * thread count
+ * is used to decide how many independent tasks should be passed to execute()
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int thread_count;
+
+ /**
+ * The codec may call this to execute several independent things.
+ * It will return only after finishing all tasks.
+ * The user may replace this with some multithreaded implementation,
+ * the default implementation will execute the parts serially.
+ * @param count the number of things to execute
+ * - encoding: Set by libavcodec, user can override.
+ * - decoding: Set by libavcodec, user can override.
+ */
+ int (*execute)(struct AVCodecContext *c, int (*func)(struct AVCodecContext *c2, void *arg), void *arg2, int *ret, int count, int size);
+
+ /**
+ * thread opaque
+ * Can be used by execute() to store some per AVCodecContext stuff.
+ * - encoding: set by execute()
+ * - decoding: set by execute()
+ */
+ void *thread_opaque;
+
+ /**
+ * Motion estimation threshold below which no motion estimation is
+ * performed, but instead the user specified motion vectors are used.
+ *
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_threshold;
+
+ /**
+ * Macroblock threshold below which the user specified macroblock types will be used.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mb_threshold;
+
+ /**
+ * precision of the intra DC coefficient - 8
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int intra_dc_precision;
+
+ /**
+ * noise vs. sse weight for the nsse comparsion function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int nsse_weight;
+
+ /**
+ * Number of macroblock rows at the top which are skipped.
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int skip_top;
+
+ /**
+ * Number of macroblock rows at the bottom which are skipped.
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int skip_bottom;
+
+ /**
+ * profile
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int profile;
+#define FF_PROFILE_UNKNOWN -99
+#define FF_PROFILE_RESERVED -100
+
+#define FF_PROFILE_AAC_MAIN 0
+#define FF_PROFILE_AAC_LOW 1
+#define FF_PROFILE_AAC_SSR 2
+#define FF_PROFILE_AAC_LTP 3
+
+#define FF_PROFILE_DTS 20
+#define FF_PROFILE_DTS_ES 30
+#define FF_PROFILE_DTS_96_24 40
+#define FF_PROFILE_DTS_HD_HRA 50
+#define FF_PROFILE_DTS_HD_MA 60
+
+#define FF_PROFILE_MPEG2_422 0
+#define FF_PROFILE_MPEG2_HIGH 1
+#define FF_PROFILE_MPEG2_SS 2
+#define FF_PROFILE_MPEG2_SNR_SCALABLE 3
+#define FF_PROFILE_MPEG2_MAIN 4
+#define FF_PROFILE_MPEG2_SIMPLE 5
+
+#define FF_PROFILE_H264_CONSTRAINED (1<<9) // 8+1; constraint_set1_flag
+#define FF_PROFILE_H264_INTRA (1<<11) // 8+3; constraint_set3_flag
+
+#define FF_PROFILE_H264_BASELINE 66
+#define FF_PROFILE_H264_CONSTRAINED_BASELINE (66|FF_PROFILE_H264_CONSTRAINED)
+#define FF_PROFILE_H264_MAIN 77
+#define FF_PROFILE_H264_EXTENDED 88
+#define FF_PROFILE_H264_HIGH 100
+#define FF_PROFILE_H264_HIGH_10 110
+#define FF_PROFILE_H264_HIGH_10_INTRA (110|FF_PROFILE_H264_INTRA)
+#define FF_PROFILE_H264_HIGH_422 122
+#define FF_PROFILE_H264_HIGH_422_INTRA (122|FF_PROFILE_H264_INTRA)
+#define FF_PROFILE_H264_HIGH_444 144
+#define FF_PROFILE_H264_HIGH_444_PREDICTIVE 244
+#define FF_PROFILE_H264_HIGH_444_INTRA (244|FF_PROFILE_H264_INTRA)
+#define FF_PROFILE_H264_CAVLC_444 44
+
+#define FF_PROFILE_VC1_SIMPLE 0
+#define FF_PROFILE_VC1_MAIN 1
+#define FF_PROFILE_VC1_COMPLEX 2
+#define FF_PROFILE_VC1_ADVANCED 3
+
+#define FF_PROFILE_MPEG4_SIMPLE 0
+#define FF_PROFILE_MPEG4_SIMPLE_SCALABLE 1
+#define FF_PROFILE_MPEG4_CORE 2
+#define FF_PROFILE_MPEG4_MAIN 3
+#define FF_PROFILE_MPEG4_N_BIT 4
+#define FF_PROFILE_MPEG4_SCALABLE_TEXTURE 5
+#define FF_PROFILE_MPEG4_SIMPLE_FACE_ANIMATION 6
+#define FF_PROFILE_MPEG4_BASIC_ANIMATED_TEXTURE 7
+#define FF_PROFILE_MPEG4_HYBRID 8
+#define FF_PROFILE_MPEG4_ADVANCED_REAL_TIME 9
+#define FF_PROFILE_MPEG4_CORE_SCALABLE 10
+#define FF_PROFILE_MPEG4_ADVANCED_CODING 11
+#define FF_PROFILE_MPEG4_ADVANCED_CORE 12
+#define FF_PROFILE_MPEG4_ADVANCED_SCALABLE_TEXTURE 13
+#define FF_PROFILE_MPEG4_SIMPLE_STUDIO 14
+#define FF_PROFILE_MPEG4_ADVANCED_SIMPLE 15
+
+ /**
+ * level
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int level;
+#define FF_LEVEL_UNKNOWN -99
+
+ /**
+ * low resolution decoding, 1-> 1/2 size, 2->1/4 size
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int lowres;
+
+ /**
+ * Bitstream width / height, may be different from width/height if lowres enabled.
+ * - encoding: unused
+ * - decoding: Set by user before init if known. Codec should override / dynamically change if needed.
+ */
+ int coded_width, coded_height;
+
+ /**
+ * frame skip threshold
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int frame_skip_threshold;
+
+ /**
+ * frame skip factor
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int frame_skip_factor;
+
+ /**
+ * frame skip exponent
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int frame_skip_exp;
+
+ /**
+ * frame skip comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int frame_skip_cmp;
+
+ /**
+ * Border processing masking, raises the quantizer for mbs on the borders
+ * of the picture.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float border_masking;
+
+ /**
+ * minimum MB lagrange multipler
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mb_lmin;
+
+ /**
+ * maximum MB lagrange multipler
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mb_lmax;
+
+ /**
+ *
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_penalty_compensation;
+
+ /**
+ *
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ enum AVDiscard skip_loop_filter;
+
+ /**
+ *
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ enum AVDiscard skip_idct;
+
+ /**
+ *
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ enum AVDiscard skip_frame;
+
+ /**
+ *
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int bidir_refine;
+
+ /**
+ *
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int brd_scale;
+
+#if FF_API_X264_GLOBAL_OPTS
+ /**
+ * constant rate factor - quality-based VBR - values ~correspond to qps
+ * - encoding: Set by user.
+ * - decoding: unused
+ * @deprecated use 'crf' libx264 private option
+ */
+ attribute_deprecated float crf;
+
+ /**
+ * constant quantization parameter rate control method
+ * - encoding: Set by user.
+ * - decoding: unused
+ * @deprecated use 'cqp' libx264 private option
+ */
+ attribute_deprecated int cqp;
+#endif
+
+ /**
+ * minimum GOP size
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int keyint_min;
+
+ /**
+ * number of reference frames
+ * - encoding: Set by user.
+ * - decoding: Set by lavc.
+ */
+ int refs;
+
+ /**
+ * chroma qp offset from luma
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int chromaoffset;
+
+#if FF_API_X264_GLOBAL_OPTS
+ /**
+ * Influence how often B-frames are used.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ attribute_deprecated int bframebias;
+#endif
+
+ /**
+ * trellis RD quantization
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int trellis;
+
+#if FF_API_X264_GLOBAL_OPTS
+ /**
+ * Reduce fluctuations in qp (before curve compression).
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ attribute_deprecated float complexityblur;
+
+ /**
+ * in-loop deblocking filter alphac0 parameter
+ * alpha is in the range -6...6
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ attribute_deprecated int deblockalpha;
+
+ /**
+ * in-loop deblocking filter beta parameter
+ * beta is in the range -6...6
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ attribute_deprecated int deblockbeta;
+
+ /**
+ * macroblock subpartition sizes to consider - p8x8, p4x4, b8x8, i8x8, i4x4
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ attribute_deprecated int partitions;
+#define X264_PART_I4X4 0x001 /* Analyze i4x4 */
+#define X264_PART_I8X8 0x002 /* Analyze i8x8 (requires 8x8 transform) */
+#define X264_PART_P8X8 0x010 /* Analyze p16x8, p8x16 and p8x8 */
+#define X264_PART_P4X4 0x020 /* Analyze p8x4, p4x8, p4x4 */
+#define X264_PART_B8X8 0x100 /* Analyze b16x8, b8x16 and b8x8 */
+
+ /**
+ * direct MV prediction mode - 0 (none), 1 (spatial), 2 (temporal), 3 (auto)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ attribute_deprecated int directpred;
+#endif
+
+ /**
+ * Audio cutoff bandwidth (0 means "automatic")
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int cutoff;
+
+ /**
+ * Multiplied by qscale for each frame and added to scene_change_score.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int scenechange_factor;
+
+ /**
+ *
+ * Note: Value depends upon the compare function used for fullpel ME.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mv0_threshold;
+
+ /**
+ * Adjust sensitivity of b_frame_strategy 1.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int b_sensitivity;
+
+ /**
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int compression_level;
+#define FF_COMPRESSION_DEFAULT -1
+
+ /**
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int min_prediction_order;
+
+ /**
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int max_prediction_order;
+
+#if FF_API_FLAC_GLOBAL_OPTS
+ /**
+ * @name FLAC options
+ * @deprecated Use FLAC encoder private options instead.
+ * @{
+ */
+
+ /**
+ * LPC coefficient precision - used by FLAC encoder
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ attribute_deprecated int lpc_coeff_precision;
+
+ /**
+ * search method for selecting prediction order
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ attribute_deprecated int prediction_order_method;
+
+ /**
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ attribute_deprecated int min_partition_order;
+
+ /**
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ attribute_deprecated int max_partition_order;
+ /**
+ * @}
+ */
+#endif
+
+ /**
+ * GOP timecode frame start number, in non drop frame format
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int64_t timecode_frame_start;
+
+#if FF_API_REQUEST_CHANNELS
+ /**
+ * Decoder should decode to this many channels if it can (0 for default)
+ * - encoding: unused
+ * - decoding: Set by user.
+ * @deprecated Deprecated in favor of request_channel_layout.
+ */
+ int request_channels;
+#endif
+
+#if FF_API_DRC_SCALE
+ /**
+ * Percentage of dynamic range compression to be applied by the decoder.
+ * The default value is 1.0, corresponding to full compression.
+ * - encoding: unused
+ * - decoding: Set by user.
+ * @deprecated use AC3 decoder private option instead.
+ */
+ attribute_deprecated float drc_scale;
+#endif
+
+ /**
+ * opaque 64bit number (generally a PTS) that will be reordered and
+ * output in AVFrame.reordered_opaque
+ * @deprecated in favor of pkt_pts
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int64_t reordered_opaque;
+
+ /**
+ * Bits per sample/pixel of internal libavcodec pixel/sample format.
+ * - encoding: set by user.
+ * - decoding: set by libavcodec.
+ */
+ int bits_per_raw_sample;
+
+ /**
+ * Audio channel layout.
+ * - encoding: set by user.
+ * - decoding: set by libavcodec.
+ */
+ uint64_t channel_layout;
+
+ /**
+ * Request decoder to use this channel layout if it can (0 for default)
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ uint64_t request_channel_layout;
+
+ /**
+ * Ratecontrol attempt to use, at maximum, <value> of what can be used without an underflow.
+ * - encoding: Set by user.
+ * - decoding: unused.
+ */
+ float rc_max_available_vbv_use;
+
+ /**
+ * Ratecontrol attempt to use, at least, <value> times the amount needed to prevent a vbv overflow.
+ * - encoding: Set by user.
+ * - decoding: unused.
+ */
+ float rc_min_vbv_overflow_use;
+
+ /**
+ * Hardware accelerator in use
+ * - encoding: unused.
+ * - decoding: Set by libavcodec
+ */
+ struct AVHWAccel *hwaccel;
+
+ /**
+ * For some codecs, the time base is closer to the field rate than the frame rate.
+ * Most notably, H.264 and MPEG-2 specify time_base as half of frame duration
+ * if no telecine is used ...
+ *
+ * Set to time_base ticks per frame. Default 1, e.g., H.264/MPEG-2 set it to 2.
+ */
+ int ticks_per_frame;
+
+ /**
+ * Hardware accelerator context.
+ * For some hardware accelerators, a global context needs to be
+ * provided by the user. In that case, this holds display-dependent
+ * data Libav cannot instantiate itself. Please refer to the
+ * Libav HW accelerator documentation to know how to fill this
+ * is. e.g. for VA API, this is a struct vaapi_context.
+ * - encoding: unused
+ * - decoding: Set by user
+ */
+ void *hwaccel_context;
+
+ /**
+ * Chromaticity coordinates of the source primaries.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorPrimaries color_primaries;
+
+ /**
+ * Color Transfer Characteristic.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorTransferCharacteristic color_trc;
+
+ /**
+ * YUV colorspace type.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorSpace colorspace;
+
+ /**
+ * MPEG vs JPEG YUV range.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorRange color_range;
+
+ /**
+ * This defines the location of chroma samples.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVChromaLocation chroma_sample_location;
+
+ /**
+ * The codec may call this to execute several independent things.
+ * It will return only after finishing all tasks.
+ * The user may replace this with some multithreaded implementation,
+ * the default implementation will execute the parts serially.
+ * Also see avcodec_thread_init and e.g. the --enable-pthread configure option.
+ * @param c context passed also to func
+ * @param count the number of things to execute
+ * @param arg2 argument passed unchanged to func
+ * @param ret return values of executed functions, must have space for "count" values. May be NULL.
+ * @param func function that will be called count times, with jobnr from 0 to count-1.
+ * threadnr will be in the range 0 to c->thread_count-1 < MAX_THREADS and so that no
+ * two instances of func executing at the same time will have the same threadnr.
+ * @return always 0 currently, but code should handle a future improvement where when any call to func
+ * returns < 0 no further calls to func may be done and < 0 is returned.
+ * - encoding: Set by libavcodec, user can override.
+ * - decoding: Set by libavcodec, user can override.
+ */
+ int (*execute2)(struct AVCodecContext *c, int (*func)(struct AVCodecContext *c2, void *arg, int jobnr, int threadnr), void *arg2, int *ret, int count);
+
+#if FF_API_X264_GLOBAL_OPTS
+ /**
+ * explicit P-frame weighted prediction analysis method
+ * 0: off
+ * 1: fast blind weighting (one reference duplicate with -1 offset)
+ * 2: smart weighting (full fade detection analysis)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ attribute_deprecated int weighted_p_pred;
+
+ /**
+ * AQ mode
+ * 0: Disabled
+ * 1: Variance AQ (complexity mask)
+ * 2: Auto-variance AQ (experimental)
+ * - encoding: Set by user
+ * - decoding: unused
+ */
+ attribute_deprecated int aq_mode;
+
+ /**
+ * AQ strength
+ * Reduces blocking and blurring in flat and textured areas.
+ * - encoding: Set by user
+ * - decoding: unused
+ */
+ attribute_deprecated float aq_strength;
+
+ /**
+ * PSY RD
+ * Strength of psychovisual optimization
+ * - encoding: Set by user
+ * - decoding: unused
+ */
+ attribute_deprecated float psy_rd;
+
+ /**
+ * PSY trellis
+ * Strength of psychovisual optimization
+ * - encoding: Set by user
+ * - decoding: unused
+ */
+ attribute_deprecated float psy_trellis;
+
+ /**
+ * RC lookahead
+ * Number of frames for frametype and ratecontrol lookahead
+ * - encoding: Set by user
+ * - decoding: unused
+ */
+ attribute_deprecated int rc_lookahead;
+
+ /**
+ * Constant rate factor maximum
+ * With CRF encoding mode and VBV restrictions enabled, prevents quality from being worse
+ * than crf_max, even if doing so would violate VBV restrictions.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ attribute_deprecated float crf_max;
+#endif
+
+ int log_level_offset;
+
+#if FF_API_FLAC_GLOBAL_OPTS
+ /**
+ * Determine which LPC analysis algorithm to use.
+ * - encoding: Set by user
+ * - decoding: unused
+ */
+ attribute_deprecated enum AVLPCType lpc_type;
+
+ /**
+ * Number of passes to use for Cholesky factorization during LPC analysis
+ * - encoding: Set by user
+ * - decoding: unused
+ */
+ attribute_deprecated int lpc_passes;
+#endif
+
+ /**
+ * Number of slices.
+ * Indicates number of picture subdivisions. Used for parallelized
+ * decoding.
+ * - encoding: Set by user
+ * - decoding: unused
+ */
+ int slices;
+
+ /**
+ * Header containing style information for text subtitles.
+ * For SUBTITLE_ASS subtitle type, it should contain the whole ASS
+ * [Script Info] and [V4+ Styles] section, plus the [Events] line and
+ * the Format line following. It shouldn't include any Dialogue line.
+ * - encoding: Set/allocated/freed by user (before avcodec_open2())
+ * - decoding: Set/allocated/freed by libavcodec (by avcodec_open2())
+ */
+ uint8_t *subtitle_header;
+ int subtitle_header_size;
+
+ /**
+ * Current packet as passed into the decoder, to avoid having
+ * to pass the packet into every function. Currently only valid
+ * inside lavc and get/release_buffer callbacks.
+ * - decoding: set by avcodec_decode_*, read by get_buffer() for setting pkt_pts
+ * - encoding: unused
+ */
+ AVPacket *pkt;
+
+#if FF_API_INTERNAL_CONTEXT
+ /**
+ * Whether this is a copy of the context which had init() called on it.
+ * This is used by multithreading - shared tables and picture pointers
+ * should be freed from the original context only.
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by libavcodec.
+ *
+ * @deprecated this field has been moved to an internal context
+ */
+ attribute_deprecated int is_copy;
+#endif
+
+ /**
+ * Which multithreading methods to use.
+ * Use of FF_THREAD_FRAME will increase decoding delay by one frame per thread,
+ * so clients which cannot provide future frames should not use it.
+ *
+ * - encoding: Set by user, otherwise the default is used.
+ * - decoding: Set by user, otherwise the default is used.
+ */
+ int thread_type;
+#define FF_THREAD_FRAME 1 ///< Decode more than one frame at once
+#define FF_THREAD_SLICE 2 ///< Decode more than one part of a single frame at once
+
+ /**
+ * Which multithreading methods are in use by the codec.
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by libavcodec.
+ */
+ int active_thread_type;
+
+ /**
+ * Set by the client if its custom get_buffer() callback can be called
+ * from another thread, which allows faster multithreaded decoding.
+ * draw_horiz_band() will be called from other threads regardless of this setting.
+ * Ignored if the default get_buffer() is used.
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int thread_safe_callbacks;
+
+ /**
+ * VBV delay coded in the last frame (in periods of a 27 MHz clock).
+ * Used for compliant TS muxing.
+ * - encoding: Set by libavcodec.
+ * - decoding: unused.
+ */
+ uint64_t vbv_delay;
+
+ /**
+ * Type of service that the audio stream conveys.
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ enum AVAudioServiceType audio_service_type;
+
+ /**
+ * Used to request a sample format from the decoder.
+ * - encoding: unused.
+ * - decoding: Set by user.
+ */
+ enum AVSampleFormat request_sample_fmt;
+
+ /**
+ * Error recognition; may misdetect some more or less valid parts as errors.
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int err_recognition;
+#define AV_EF_CRCCHECK (1<<0)
+#define AV_EF_BITSTREAM (1<<1)
+#define AV_EF_BUFFER (1<<2)
+#define AV_EF_EXPLODE (1<<3)
+
+ /**
+ * Private context used for internal data.
+ *
+ * Unlike priv_data, this is not codec-specific. It is used in general
+ * libavcodec functions.
+ */
+ struct AVCodecInternal *internal;
+
+ /** Field order
+ * - encoding: set by libavcodec
+ * - decoding: Set by libavcodec
+ */
+ enum AVFieldOrder field_order;
+} AVCodecContext;
+
+/**
+ * AVProfile.
+ */
+typedef struct AVProfile {
+ int profile;
+ const char *name; ///< short name for the profile
+} AVProfile;
+
+typedef struct AVCodecDefault AVCodecDefault;
+
+/**
+ * AVCodec.
+ */
+typedef struct AVCodec {
+ /**
+ * Name of the codec implementation.
+ * The name is globally unique among encoders and among decoders (but an
+ * encoder and a decoder can share the same name).
+ * This is the primary way to find a codec from the user perspective.
+ */
+ const char *name;
+ enum AVMediaType type;
+ enum CodecID id;
+ int priv_data_size;
+ int (*init)(AVCodecContext *);
+ int (*encode)(AVCodecContext *, uint8_t *buf, int buf_size, void *data);
+ int (*close)(AVCodecContext *);
+ int (*decode)(AVCodecContext *, void *outdata, int *outdata_size, AVPacket *avpkt);
+ /**
+ * Codec capabilities.
+ * see CODEC_CAP_*
+ */
+ int capabilities;
+ struct AVCodec *next;
+ /**
+ * Flush buffers.
+ * Will be called when seeking
+ */
+ void (*flush)(AVCodecContext *);
+ const AVRational *supported_framerates; ///< array of supported framerates, or NULL if any, array is terminated by {0,0}
+ const enum PixelFormat *pix_fmts; ///< array of supported pixel formats, or NULL if unknown, array is terminated by -1
+ /**
+ * Descriptive name for the codec, meant to be more human readable than name.
+ * You should use the NULL_IF_CONFIG_SMALL() macro to define it.
+ */
+ const char *long_name;
+ const int *supported_samplerates; ///< array of supported audio samplerates, or NULL if unknown, array is terminated by 0
+ const enum AVSampleFormat *sample_fmts; ///< array of supported sample formats, or NULL if unknown, array is terminated by -1
+ const uint64_t *channel_layouts; ///< array of support channel layouts, or NULL if unknown. array is terminated by 0
+ uint8_t max_lowres; ///< maximum value for lowres supported by the decoder
+ const AVClass *priv_class; ///< AVClass for the private context
+ const AVProfile *profiles; ///< array of recognized profiles, or NULL if unknown, array is terminated by {FF_PROFILE_UNKNOWN}
+
+ /**
+ * @name Frame-level threading support functions
+ * @{
+ */
+ /**
+ * If defined, called on thread contexts when they are created.
+ * If the codec allocates writable tables in init(), re-allocate them here.
+ * priv_data will be set to a copy of the original.
+ */
+ int (*init_thread_copy)(AVCodecContext *);
+ /**
+ * Copy necessary context variables from a previous thread context to the current one.
+ * If not defined, the next thread will start automatically; otherwise, the codec
+ * must call ff_thread_finish_setup().
+ *
+ * dst and src will (rarely) point to the same context, in which case memcpy should be skipped.
+ */
+ int (*update_thread_context)(AVCodecContext *dst, const AVCodecContext *src);
+ /** @} */
+
+ /**
+ * Private codec-specific defaults.
+ */
+ const AVCodecDefault *defaults;
+
+ /**
+ * Initialize codec static data, called from avcodec_register().
+ */
+ void (*init_static_data)(struct AVCodec *codec);
+
+ /**
+ * Encode data to an AVPacket.
+ *
+ * @param avctx codec context
+ * @param avpkt output AVPacket (may contain a user-provided buffer)
+ * @param[in] frame AVFrame containing the raw data to be encoded
+ * @param[out] got_packet_ptr encoder sets to 0 or 1 to indicate that a
+ * non-empty packet was returned in avpkt.
+ * @return 0 on success, negative error code on failure
+ */
+ int (*encode2)(AVCodecContext *avctx, AVPacket *avpkt, const AVFrame *frame,
+ int *got_packet_ptr);
+} AVCodec;
+
+/**
+ * AVHWAccel.
+ */
+typedef struct AVHWAccel {
+ /**
+ * Name of the hardware accelerated codec.
+ * The name is globally unique among encoders and among decoders (but an
+ * encoder and a decoder can share the same name).
+ */
+ const char *name;
+
+ /**
+ * Type of codec implemented by the hardware accelerator.
+ *
+ * See AVMEDIA_TYPE_xxx
+ */
+ enum AVMediaType type;
+
+ /**
+ * Codec implemented by the hardware accelerator.
+ *
+ * See CODEC_ID_xxx
+ */
+ enum CodecID id;
+
+ /**
+ * Supported pixel format.
+ *
+ * Only hardware accelerated formats are supported here.
+ */
+ enum PixelFormat pix_fmt;
+
+ /**
+ * Hardware accelerated codec capabilities.
+ * see FF_HWACCEL_CODEC_CAP_*
+ */
+ int capabilities;
+
+ struct AVHWAccel *next;
+
+ /**
+ * Called at the beginning of each frame or field picture.
+ *
+ * Meaningful frame information (codec specific) is guaranteed to
+ * be parsed at this point. This function is mandatory.
+ *
+ * Note that buf can be NULL along with buf_size set to 0.
+ * Otherwise, this means the whole frame is available at this point.
+ *
+ * @param avctx the codec context
+ * @param buf the frame data buffer base
+ * @param buf_size the size of the frame in bytes
+ * @return zero if successful, a negative value otherwise
+ */
+ int (*start_frame)(AVCodecContext *avctx, const uint8_t *buf, uint32_t buf_size);
+
+ /**
+ * Callback for each slice.
+ *
+ * Meaningful slice information (codec specific) is guaranteed to
+ * be parsed at this point. This function is mandatory.
+ *
+ * @param avctx the codec context
+ * @param buf the slice data buffer base
+ * @param buf_size the size of the slice in bytes
+ * @return zero if successful, a negative value otherwise
+ */
+ int (*decode_slice)(AVCodecContext *avctx, const uint8_t *buf, uint32_t buf_size);
+
+ /**
+ * Called at the end of each frame or field picture.
+ *
+ * The whole picture is parsed at this point and can now be sent
+ * to the hardware accelerator. This function is mandatory.
+ *
+ * @param avctx the codec context
+ * @return zero if successful, a negative value otherwise
+ */
+ int (*end_frame)(AVCodecContext *avctx);
+
+ /**
+ * Size of HW accelerator private data.
+ *
+ * Private data is allocated with av_mallocz() before
+ * AVCodecContext.get_buffer() and deallocated after
+ * AVCodecContext.release_buffer().
+ */
+ int priv_data_size;
+} AVHWAccel;
+
+/**
+ * four components are given, that's all.
+ * the last component is alpha
+ */
+typedef struct AVPicture {
+ uint8_t *data[AV_NUM_DATA_POINTERS];
+ int linesize[AV_NUM_DATA_POINTERS]; ///< number of bytes per line
+} AVPicture;
+
+#define AVPALETTE_SIZE 1024
+#define AVPALETTE_COUNT 256
+#if FF_API_PALETTE_CONTROL
+/**
+ * AVPaletteControl
+ * This structure defines a method for communicating palette changes
+ * between and demuxer and a decoder.
+ *
+ * @deprecated Use AVPacket to send palette changes instead.
+ * This is totally broken.
+ */
+typedef struct AVPaletteControl {
+
+ /* Demuxer sets this to 1 to indicate the palette has changed;
+ * decoder resets to 0. */
+ int palette_changed;
+
+ /* 4-byte ARGB palette entries, stored in native byte order; note that
+ * the individual palette components should be on a 8-bit scale; if
+ * the palette data comes from an IBM VGA native format, the component
+ * data is probably 6 bits in size and needs to be scaled. */
+ unsigned int palette[AVPALETTE_COUNT];
+
+} AVPaletteControl attribute_deprecated;
+#endif
+
+enum AVSubtitleType {
+ SUBTITLE_NONE,
+
+ SUBTITLE_BITMAP, ///< A bitmap, pict will be set
+
+ /**
+ * Plain text, the text field must be set by the decoder and is
+ * authoritative. ass and pict fields may contain approximations.
+ */
+ SUBTITLE_TEXT,
+
+ /**
+ * Formatted text, the ass field must be set by the decoder and is
+ * authoritative. pict and text fields may contain approximations.
+ */
+ SUBTITLE_ASS,
+};
+
+typedef struct AVSubtitleRect {
+ int x; ///< top left corner of pict, undefined when pict is not set
+ int y; ///< top left corner of pict, undefined when pict is not set
+ int w; ///< width of pict, undefined when pict is not set
+ int h; ///< height of pict, undefined when pict is not set
+ int nb_colors; ///< number of colors in pict, undefined when pict is not set
+
+ /**
+ * data+linesize for the bitmap of this subtitle.
+ * can be set for text/ass as well once they where rendered
+ */
+ AVPicture pict;
+ enum AVSubtitleType type;
+
+ char *text; ///< 0 terminated plain UTF-8 text
+
+ /**
+ * 0 terminated ASS/SSA compatible event line.
+ * The pressentation of this is unaffected by the other values in this
+ * struct.
+ */
+ char *ass;
+} AVSubtitleRect;
+
+typedef struct AVSubtitle {
+ uint16_t format; /* 0 = graphics */
+ uint32_t start_display_time; /* relative to packet pts, in ms */
+ uint32_t end_display_time; /* relative to packet pts, in ms */
+ unsigned num_rects;
+ AVSubtitleRect **rects;
+ int64_t pts; ///< Same as packet pts, in AV_TIME_BASE
+} AVSubtitle;
+
+/* packet functions */
+
+/**
+ * @deprecated use NULL instead
+ */
+attribute_deprecated void av_destruct_packet_nofree(AVPacket *pkt);
+
+/**
+ * Default packet destructor.
+ */
+void av_destruct_packet(AVPacket *pkt);
+
+/**
+ * Initialize optional fields of a packet with default values.
+ *
+ * @param pkt packet
+ */
+void av_init_packet(AVPacket *pkt);
+
+/**
+ * Allocate the payload of a packet and initialize its fields with
+ * default values.
+ *
+ * @param pkt packet
+ * @param size wanted payload size
+ * @return 0 if OK, AVERROR_xxx otherwise
+ */
+int av_new_packet(AVPacket *pkt, int size);
+
+/**
+ * Reduce packet size, correctly zeroing padding
+ *
+ * @param pkt packet
+ * @param size new size
+ */
+void av_shrink_packet(AVPacket *pkt, int size);
+
+/**
+ * Increase packet size, correctly zeroing padding
+ *
+ * @param pkt packet
+ * @param grow_by number of bytes by which to increase the size of the packet
+ */
+int av_grow_packet(AVPacket *pkt, int grow_by);
+
+/**
+ * @warning This is a hack - the packet memory allocation stuff is broken. The
+ * packet is allocated if it was not really allocated.
+ */
+int av_dup_packet(AVPacket *pkt);
+
+/**
+ * Free a packet.
+ *
+ * @param pkt packet to free
+ */
+void av_free_packet(AVPacket *pkt);
+
+/**
+ * Allocate new information of a packet.
+ *
+ * @param pkt packet
+ * @param type side information type
+ * @param size side information size
+ * @return pointer to fresh allocated data or NULL otherwise
+ */
+uint8_t* av_packet_new_side_data(AVPacket *pkt, enum AVPacketSideDataType type,
+ int size);
+
+/**
+ * Get side information from packet.
+ *
+ * @param pkt packet
+ * @param type desired side information type
+ * @param size pointer for side information size to store (optional)
+ * @return pointer to data if present or NULL otherwise
+ */
+uint8_t* av_packet_get_side_data(AVPacket *pkt, enum AVPacketSideDataType type,
+ int *size);
+
+/* resample.c */
+
+struct ReSampleContext;
+struct AVResampleContext;
+
+typedef struct ReSampleContext ReSampleContext;
+
+/**
+ * Initialize audio resampling context.
+ *
+ * @param output_channels number of output channels
+ * @param input_channels number of input channels
+ * @param output_rate output sample rate
+ * @param input_rate input sample rate
+ * @param sample_fmt_out requested output sample format
+ * @param sample_fmt_in input sample format
+ * @param filter_length length of each FIR filter in the filterbank relative to the cutoff frequency
+ * @param log2_phase_count log2 of the number of entries in the polyphase filterbank
+ * @param linear if 1 then the used FIR filter will be linearly interpolated
+ between the 2 closest, if 0 the closest will be used
+ * @param cutoff cutoff frequency, 1.0 corresponds to half the output sampling rate
+ * @return allocated ReSampleContext, NULL if error occurred
+ */
+ReSampleContext *av_audio_resample_init(int output_channels, int input_channels,
+ int output_rate, int input_rate,
+ enum AVSampleFormat sample_fmt_out,
+ enum AVSampleFormat sample_fmt_in,
+ int filter_length, int log2_phase_count,
+ int linear, double cutoff);
+
+int audio_resample(ReSampleContext *s, short *output, short *input, int nb_samples);
+
+/**
+ * Free resample context.
+ *
+ * @param s a non-NULL pointer to a resample context previously
+ * created with av_audio_resample_init()
+ */
+void audio_resample_close(ReSampleContext *s);
+
+
+/**
+ * Initialize an audio resampler.
+ * Note, if either rate is not an integer then simply scale both rates up so they are.
+ * @param filter_length length of each FIR filter in the filterbank relative to the cutoff freq
+ * @param log2_phase_count log2 of the number of entries in the polyphase filterbank
+ * @param linear If 1 then the used FIR filter will be linearly interpolated
+ between the 2 closest, if 0 the closest will be used
+ * @param cutoff cutoff frequency, 1.0 corresponds to half the output sampling rate
+ */
+struct AVResampleContext *av_resample_init(int out_rate, int in_rate, int filter_length, int log2_phase_count, int linear, double cutoff);
+
+/**
+ * Resample an array of samples using a previously configured context.
+ * @param src an array of unconsumed samples
+ * @param consumed the number of samples of src which have been consumed are returned here
+ * @param src_size the number of unconsumed samples available
+ * @param dst_size the amount of space in samples available in dst
+ * @param update_ctx If this is 0 then the context will not be modified, that way several channels can be resampled with the same context.
+ * @return the number of samples written in dst or -1 if an error occurred
+ */
+int av_resample(struct AVResampleContext *c, short *dst, short *src, int *consumed, int src_size, int dst_size, int update_ctx);
+
+
+/**
+ * Compensate samplerate/timestamp drift. The compensation is done by changing
+ * the resampler parameters, so no audible clicks or similar distortions occur
+ * @param compensation_distance distance in output samples over which the compensation should be performed
+ * @param sample_delta number of output samples which should be output less
+ *
+ * example: av_resample_compensate(c, 10, 500)
+ * here instead of 510 samples only 500 samples would be output
+ *
+ * note, due to rounding the actual compensation might be slightly different,
+ * especially if the compensation_distance is large and the in_rate used during init is small
+ */
+void av_resample_compensate(struct AVResampleContext *c, int sample_delta, int compensation_distance);
+void av_resample_close(struct AVResampleContext *c);
+
+/**
+ * Allocate memory for a picture. Call avpicture_free() to free it.
+ *
+ * @see avpicture_fill()
+ *
+ * @param picture the picture to be filled in
+ * @param pix_fmt the format of the picture
+ * @param width the width of the picture
+ * @param height the height of the picture
+ * @return zero if successful, a negative value if not
+ */
+int avpicture_alloc(AVPicture *picture, enum PixelFormat pix_fmt, int width, int height);
+
+/**
+ * Free a picture previously allocated by avpicture_alloc().
+ * The data buffer used by the AVPicture is freed, but the AVPicture structure
+ * itself is not.
+ *
+ * @param picture the AVPicture to be freed
+ */
+void avpicture_free(AVPicture *picture);
+
+/**
+ * Fill in the AVPicture fields.
+ * The fields of the given AVPicture are filled in by using the 'ptr' address
+ * which points to the image data buffer. Depending on the specified picture
+ * format, one or multiple image data pointers and line sizes will be set.
+ * If a planar format is specified, several pointers will be set pointing to
+ * the different picture planes and the line sizes of the different planes
+ * will be stored in the lines_sizes array.
+ * Call with ptr == NULL to get the required size for the ptr buffer.
+ *
+ * To allocate the buffer and fill in the AVPicture fields in one call,
+ * use avpicture_alloc().
+ *
+ * @param picture AVPicture whose fields are to be filled in
+ * @param ptr Buffer which will contain or contains the actual image data
+ * @param pix_fmt The format in which the picture data is stored.
+ * @param width the width of the image in pixels
+ * @param height the height of the image in pixels
+ * @return size of the image data in bytes
+ */
+int avpicture_fill(AVPicture *picture, uint8_t *ptr,
+ enum PixelFormat pix_fmt, int width, int height);
+
+/**
+ * Copy pixel data from an AVPicture into a buffer.
+ * The data is stored compactly, without any gaps for alignment or padding
+ * which may be applied by avpicture_fill().
+ *
+ * @see avpicture_get_size()
+ *
+ * @param[in] src AVPicture containing image data
+ * @param[in] pix_fmt The format in which the picture data is stored.
+ * @param[in] width the width of the image in pixels.
+ * @param[in] height the height of the image in pixels.
+ * @param[out] dest A buffer into which picture data will be copied.
+ * @param[in] dest_size The size of 'dest'.
+ * @return The number of bytes written to dest, or a negative value (error code) on error.
+ */
+int avpicture_layout(const AVPicture* src, enum PixelFormat pix_fmt, int width, int height,
+ unsigned char *dest, int dest_size);
+
+/**
+ * Calculate the size in bytes that a picture of the given width and height
+ * would occupy if stored in the given picture format.
+ * Note that this returns the size of a compact representation as generated
+ * by avpicture_layout(), which can be smaller than the size required for e.g.
+ * avpicture_fill().
+ *
+ * @param pix_fmt the given picture format
+ * @param width the width of the image
+ * @param height the height of the image
+ * @return Image data size in bytes or -1 on error (e.g. too large dimensions).
+ */
+int avpicture_get_size(enum PixelFormat pix_fmt, int width, int height);
+void avcodec_get_chroma_sub_sample(enum PixelFormat pix_fmt, int *h_shift, int *v_shift);
+
+#if FF_API_GET_PIX_FMT_NAME
+/**
+ * @deprecated Deprecated in favor of av_get_pix_fmt_name().
+ */
+attribute_deprecated
+const char *avcodec_get_pix_fmt_name(enum PixelFormat pix_fmt);
+#endif
+
+void avcodec_set_dimensions(AVCodecContext *s, int width, int height);
+
+/**
+ * Return a value representing the fourCC code associated to the
+ * pixel format pix_fmt, or 0 if no associated fourCC code can be
+ * found.
+ */
+unsigned int avcodec_pix_fmt_to_codec_tag(enum PixelFormat pix_fmt);
+
+/**
+ * Put a string representing the codec tag codec_tag in buf.
+ *
+ * @param buf_size size in bytes of buf
+ * @return the length of the string that would have been generated if
+ * enough space had been available, excluding the trailing null
+ */
+size_t av_get_codec_tag_string(char *buf, size_t buf_size, unsigned int codec_tag);
+
+#define FF_LOSS_RESOLUTION 0x0001 /**< loss due to resolution change */
+#define FF_LOSS_DEPTH 0x0002 /**< loss due to color depth change */
+#define FF_LOSS_COLORSPACE 0x0004 /**< loss due to color space conversion */
+#define FF_LOSS_ALPHA 0x0008 /**< loss of alpha bits */
+#define FF_LOSS_COLORQUANT 0x0010 /**< loss due to color quantization */
+#define FF_LOSS_CHROMA 0x0020 /**< loss of chroma (e.g. RGB to gray conversion) */
+
+/**
+ * Compute what kind of losses will occur when converting from one specific
+ * pixel format to another.
+ * When converting from one pixel format to another, information loss may occur.
+ * For example, when converting from RGB24 to GRAY, the color information will
+ * be lost. Similarly, other losses occur when converting from some formats to
+ * other formats. These losses can involve loss of chroma, but also loss of
+ * resolution, loss of color depth, loss due to the color space conversion, loss
+ * of the alpha bits or loss due to color quantization.
+ * avcodec_get_fix_fmt_loss() informs you about the various types of losses
+ * which will occur when converting from one pixel format to another.
+ *
+ * @param[in] dst_pix_fmt destination pixel format
+ * @param[in] src_pix_fmt source pixel format
+ * @param[in] has_alpha Whether the source pixel format alpha channel is used.
+ * @return Combination of flags informing you what kind of losses will occur.
+ */
+int avcodec_get_pix_fmt_loss(enum PixelFormat dst_pix_fmt, enum PixelFormat src_pix_fmt,
+ int has_alpha);
+
+/**
+ * Find the best pixel format to convert to given a certain source pixel
+ * format. When converting from one pixel format to another, information loss
+ * may occur. For example, when converting from RGB24 to GRAY, the color
+ * information will be lost. Similarly, other losses occur when converting from
+ * some formats to other formats. avcodec_find_best_pix_fmt() searches which of
+ * the given pixel formats should be used to suffer the least amount of loss.
+ * The pixel formats from which it chooses one, are determined by the
+ * pix_fmt_mask parameter.
+ *
+ * @code
+ * src_pix_fmt = PIX_FMT_YUV420P;
+ * pix_fmt_mask = (1 << PIX_FMT_YUV422P) || (1 << PIX_FMT_RGB24);
+ * dst_pix_fmt = avcodec_find_best_pix_fmt(pix_fmt_mask, src_pix_fmt, alpha, &loss);
+ * @endcode
+ *
+ * @param[in] pix_fmt_mask bitmask determining which pixel format to choose from
+ * @param[in] src_pix_fmt source pixel format
+ * @param[in] has_alpha Whether the source pixel format alpha channel is used.
+ * @param[out] loss_ptr Combination of flags informing you what kind of losses will occur.
+ * @return The best pixel format to convert to or -1 if none was found.
+ */
+enum PixelFormat avcodec_find_best_pix_fmt(int64_t pix_fmt_mask, enum PixelFormat src_pix_fmt,
+ int has_alpha, int *loss_ptr);
+
+#if FF_API_GET_ALPHA_INFO
+#define FF_ALPHA_TRANSP 0x0001 /* image has some totally transparent pixels */
+#define FF_ALPHA_SEMI_TRANSP 0x0002 /* image has some transparent pixels */
+
+/**
+ * Tell if an image really has transparent alpha values.
+ * @return ored mask of FF_ALPHA_xxx constants
+ */
+attribute_deprecated
+int img_get_alpha_info(const AVPicture *src,
+ enum PixelFormat pix_fmt, int width, int height);
+#endif
+
+/* deinterlace a picture */
+/* deinterlace - if not supported return -1 */
+int avpicture_deinterlace(AVPicture *dst, const AVPicture *src,
+ enum PixelFormat pix_fmt, int width, int height);
+
+/* external high level API */
+
+/**
+ * If c is NULL, returns the first registered codec,
+ * if c is non-NULL, returns the next registered codec after c,
+ * or NULL if c is the last one.
+ */
+AVCodec *av_codec_next(AVCodec *c);
+
+/**
+ * Return the LIBAVCODEC_VERSION_INT constant.
+ */
+unsigned avcodec_version(void);
+
+/**
+ * Return the libavcodec build-time configuration.
+ */
+const char *avcodec_configuration(void);
+
+/**
+ * Return the libavcodec license.
+ */
+const char *avcodec_license(void);
+
+#if FF_API_AVCODEC_INIT
+/**
+ * @deprecated this function is called automatically from avcodec_register()
+ * and avcodec_register_all(), there is no need to call it manually
+ */
+attribute_deprecated
+void avcodec_init(void);
+#endif
+
+/**
+ * Register the codec codec and initialize libavcodec.
+ *
+ * @warning either this function or avcodec_register_all() must be called
+ * before any other libavcodec functions.
+ *
+ * @see avcodec_register_all()
+ */
+void avcodec_register(AVCodec *codec);
+
+/**
+ * Find a registered encoder with a matching codec ID.
+ *
+ * @param id CodecID of the requested encoder
+ * @return An encoder if one was found, NULL otherwise.
+ */
+AVCodec *avcodec_find_encoder(enum CodecID id);
+
+/**
+ * Find a registered encoder with the specified name.
+ *
+ * @param name name of the requested encoder
+ * @return An encoder if one was found, NULL otherwise.
+ */
+AVCodec *avcodec_find_encoder_by_name(const char *name);
+
+/**
+ * Find a registered decoder with a matching codec ID.
+ *
+ * @param id CodecID of the requested decoder
+ * @return A decoder if one was found, NULL otherwise.
+ */
+AVCodec *avcodec_find_decoder(enum CodecID id);
+
+/**
+ * Find a registered decoder with the specified name.
+ *
+ * @param name name of the requested decoder
+ * @return A decoder if one was found, NULL otherwise.
+ */
+AVCodec *avcodec_find_decoder_by_name(const char *name);
+void avcodec_string(char *buf, int buf_size, AVCodecContext *enc, int encode);
+
+/**
+ * Return a name for the specified profile, if available.
+ *
+ * @param codec the codec that is searched for the given profile
+ * @param profile the profile value for which a name is requested
+ * @return A name for the profile if found, NULL otherwise.
+ */
+const char *av_get_profile_name(const AVCodec *codec, int profile);
+
+#if FF_API_ALLOC_CONTEXT
+/**
+ * Set the fields of the given AVCodecContext to default values.
+ *
+ * @param s The AVCodecContext of which the fields should be set to default values.
+ * @deprecated use avcodec_get_context_defaults3
+ */
+attribute_deprecated
+void avcodec_get_context_defaults(AVCodecContext *s);
+
+/** THIS FUNCTION IS NOT YET PART OF THE PUBLIC API!
+ * we WILL change its arguments and name a few times! */
+attribute_deprecated
+void avcodec_get_context_defaults2(AVCodecContext *s, enum AVMediaType);
+#endif
+
+/**
+ * Set the fields of the given AVCodecContext to default values corresponding
+ * to the given codec (defaults may be codec-dependent).
+ *
+ * Do not call this function if a non-NULL codec has been passed
+ * to avcodec_alloc_context3() that allocated this AVCodecContext.
+ * If codec is non-NULL, it is illegal to call avcodec_open2() with a
+ * different codec on this AVCodecContext.
+ */
+int avcodec_get_context_defaults3(AVCodecContext *s, AVCodec *codec);
+
+#if FF_API_ALLOC_CONTEXT
+/**
+ * Allocate an AVCodecContext and set its fields to default values. The
+ * resulting struct can be deallocated by simply calling av_free().
+ *
+ * @return An AVCodecContext filled with default values or NULL on failure.
+ * @see avcodec_get_context_defaults
+ *
+ * @deprecated use avcodec_alloc_context3()
+ */
+attribute_deprecated
+AVCodecContext *avcodec_alloc_context(void);
+
+/** THIS FUNCTION IS NOT YET PART OF THE PUBLIC API!
+ * we WILL change its arguments and name a few times! */
+attribute_deprecated
+AVCodecContext *avcodec_alloc_context2(enum AVMediaType);
+#endif
+
+/**
+ * Allocate an AVCodecContext and set its fields to default values. The
+ * resulting struct can be deallocated by calling avcodec_close() on it followed
+ * by av_free().
+ *
+ * @param codec if non-NULL, allocate private data and initialize defaults
+ * for the given codec. It is illegal to then call avcodec_open2()
+ * with a different codec.
+ *
+ * @return An AVCodecContext filled with default values or NULL on failure.
+ * @see avcodec_get_context_defaults
+ */
+AVCodecContext *avcodec_alloc_context3(AVCodec *codec);
+
+/**
+ * Copy the settings of the source AVCodecContext into the destination
+ * AVCodecContext. The resulting destination codec context will be
+ * unopened, i.e. you are required to call avcodec_open2() before you
+ * can use this AVCodecContext to decode/encode video/audio data.
+ *
+ * @param dest target codec context, should be initialized with
+ * avcodec_alloc_context3(), but otherwise uninitialized
+ * @param src source codec context
+ * @return AVERROR() on error (e.g. memory allocation error), 0 on success
+ */
+int avcodec_copy_context(AVCodecContext *dest, const AVCodecContext *src);
+
+/**
+ * Set the fields of the given AVFrame to default values.
+ *
+ * @param pic The AVFrame of which the fields should be set to default values.
+ */
+void avcodec_get_frame_defaults(AVFrame *pic);
+
+/**
+ * Allocate an AVFrame and set its fields to default values. The resulting
+ * struct can be deallocated by simply calling av_free().
+ *
+ * @return An AVFrame filled with default values or NULL on failure.
+ * @see avcodec_get_frame_defaults
+ */
+AVFrame *avcodec_alloc_frame(void);
+
+int avcodec_default_get_buffer(AVCodecContext *s, AVFrame *pic);
+void avcodec_default_release_buffer(AVCodecContext *s, AVFrame *pic);
+int avcodec_default_reget_buffer(AVCodecContext *s, AVFrame *pic);
+
+/**
+ * Return the amount of padding in pixels which the get_buffer callback must
+ * provide around the edge of the image for codecs which do not have the
+ * CODEC_FLAG_EMU_EDGE flag.
+ *
+ * @return Required padding in pixels.
+ */
+unsigned avcodec_get_edge_width(void);
+/**
+ * Modify width and height values so that they will result in a memory
+ * buffer that is acceptable for the codec if you do not use any horizontal
+ * padding.
+ *
+ * May only be used if a codec with CODEC_CAP_DR1 has been opened.
+ * If CODEC_FLAG_EMU_EDGE is not set, the dimensions must have been increased
+ * according to avcodec_get_edge_width() before.
+ */
+void avcodec_align_dimensions(AVCodecContext *s, int *width, int *height);
+/**
+ * Modify width and height values so that they will result in a memory
+ * buffer that is acceptable for the codec if you also ensure that all
+ * line sizes are a multiple of the respective linesize_align[i].
+ *
+ * May only be used if a codec with CODEC_CAP_DR1 has been opened.
+ * If CODEC_FLAG_EMU_EDGE is not set, the dimensions must have been increased
+ * according to avcodec_get_edge_width() before.
+ */
+void avcodec_align_dimensions2(AVCodecContext *s, int *width, int *height,
+ int linesize_align[AV_NUM_DATA_POINTERS]);
+
+enum PixelFormat avcodec_default_get_format(struct AVCodecContext *s, const enum PixelFormat * fmt);
+
+#if FF_API_THREAD_INIT
+/**
+ * @deprecated Set s->thread_count before calling avcodec_open2() instead of calling this.
+ */
+attribute_deprecated
+int avcodec_thread_init(AVCodecContext *s, int thread_count);
+#endif
+
+int avcodec_default_execute(AVCodecContext *c, int (*func)(AVCodecContext *c2, void *arg2),void *arg, int *ret, int count, int size);
+int avcodec_default_execute2(AVCodecContext *c, int (*func)(AVCodecContext *c2, void *arg2, int, int),void *arg, int *ret, int count);
+//FIXME func typedef
+
+#if FF_API_AVCODEC_OPEN
+/**
+ * Initialize the AVCodecContext to use the given AVCodec. Prior to using this
+ * function the context has to be allocated.
+ *
+ * The functions avcodec_find_decoder_by_name(), avcodec_find_encoder_by_name(),
+ * avcodec_find_decoder() and avcodec_find_encoder() provide an easy way for
+ * retrieving a codec.
+ *
+ * @warning This function is not thread safe!
+ *
+ * @code
+ * avcodec_register_all();
+ * codec = avcodec_find_decoder(CODEC_ID_H264);
+ * if (!codec)
+ * exit(1);
+ *
+ * context = avcodec_alloc_context3(codec);
+ *
+ * if (avcodec_open(context, codec) < 0)
+ * exit(1);
+ * @endcode
+ *
+ * @param avctx The context which will be set up to use the given codec.
+ * @param codec The codec to use within the context.
+ * @return zero on success, a negative value on error
+ * @see avcodec_alloc_context3, avcodec_find_decoder, avcodec_find_encoder, avcodec_close
+ *
+ * @deprecated use avcodec_open2
+ */
+attribute_deprecated
+int avcodec_open(AVCodecContext *avctx, AVCodec *codec);
+#endif
+
+/**
+ * Initialize the AVCodecContext to use the given AVCodec. Prior to using this
+ * function the context has to be allocated with avcodec_alloc_context3().
+ *
+ * The functions avcodec_find_decoder_by_name(), avcodec_find_encoder_by_name(),
+ * avcodec_find_decoder() and avcodec_find_encoder() provide an easy way for
+ * retrieving a codec.
+ *
+ * @warning This function is not thread safe!
+ *
+ * @code
+ * avcodec_register_all();
+ * av_dict_set(&opts, "b", "2.5M", 0);
+ * codec = avcodec_find_decoder(CODEC_ID_H264);
+ * if (!codec)
+ * exit(1);
+ *
+ * context = avcodec_alloc_context3(codec);
+ *
+ * if (avcodec_open2(context, codec, opts) < 0)
+ * exit(1);
+ * @endcode
+ *
+ * @param avctx The context to initialize.
+ * @param codec The codec to open this context for. If a non-NULL codec has been
+ * previously passed to avcodec_alloc_context3() or
+ * avcodec_get_context_defaults3() for this context, then this
+ * parameter MUST be either NULL or equal to the previously passed
+ * codec.
+ * @param options A dictionary filled with AVCodecContext and codec-private options.
+ * On return this object will be filled with options that were not found.
+ *
+ * @return zero on success, a negative value on error
+ * @see avcodec_alloc_context3(), avcodec_find_decoder(), avcodec_find_encoder(),
+ * av_dict_set(), av_opt_find().
+ */
+int avcodec_open2(AVCodecContext *avctx, AVCodec *codec, AVDictionary **options);
+
+#if FF_API_OLD_DECODE_AUDIO
+/**
+ * Wrapper function which calls avcodec_decode_audio4.
+ *
+ * @deprecated Use avcodec_decode_audio4 instead.
+ *
+ * Decode the audio frame of size avpkt->size from avpkt->data into samples.
+ * Some decoders may support multiple frames in a single AVPacket, such
+ * decoders would then just decode the first frame. In this case,
+ * avcodec_decode_audio3 has to be called again with an AVPacket that contains
+ * the remaining data in order to decode the second frame etc.
+ * If no frame
+ * could be outputted, frame_size_ptr is zero. Otherwise, it is the
+ * decompressed frame size in bytes.
+ *
+ * @warning You must set frame_size_ptr to the allocated size of the
+ * output buffer before calling avcodec_decode_audio3().
+ *
+ * @warning The input buffer must be FF_INPUT_BUFFER_PADDING_SIZE larger than
+ * the actual read bytes because some optimized bitstream readers read 32 or 64
+ * bits at once and could read over the end.
+ *
+ * @warning The end of the input buffer avpkt->data should be set to 0 to ensure that
+ * no overreading happens for damaged MPEG streams.
+ *
+ * @warning You must not provide a custom get_buffer() when using
+ * avcodec_decode_audio3(). Doing so will override it with
+ * avcodec_default_get_buffer. Use avcodec_decode_audio4() instead,
+ * which does allow the application to provide a custom get_buffer().
+ *
+ * @note You might have to align the input buffer avpkt->data and output buffer
+ * samples. The alignment requirements depend on the CPU: On some CPUs it isn't
+ * necessary at all, on others it won't work at all if not aligned and on others
+ * it will work but it will have an impact on performance.
+ *
+ * In practice, avpkt->data should have 4 byte alignment at minimum and
+ * samples should be 16 byte aligned unless the CPU doesn't need it
+ * (AltiVec and SSE do).
+ *
+ * @note Codecs which have the CODEC_CAP_DELAY capability set have a delay
+ * between input and output, these need to be fed with avpkt->data=NULL,
+ * avpkt->size=0 at the end to return the remaining frames.
+ *
+ * @param avctx the codec context
+ * @param[out] samples the output buffer, sample type in avctx->sample_fmt
+ * If the sample format is planar, each channel plane will
+ * be the same size, with no padding between channels.
+ * @param[in,out] frame_size_ptr the output buffer size in bytes
+ * @param[in] avpkt The input AVPacket containing the input buffer.
+ * You can create such packet with av_init_packet() and by then setting
+ * data and size, some decoders might in addition need other fields.
+ * All decoders are designed to use the least fields possible though.
+ * @return On error a negative value is returned, otherwise the number of bytes
+ * used or zero if no frame data was decompressed (used) from the input AVPacket.
+ */
+attribute_deprecated int avcodec_decode_audio3(AVCodecContext *avctx, int16_t *samples,
+ int *frame_size_ptr,
+ AVPacket *avpkt);
+#endif
+
+/**
+ * Decode the audio frame of size avpkt->size from avpkt->data into frame.
+ *
+ * Some decoders may support multiple frames in a single AVPacket. Such
+ * decoders would then just decode the first frame. In this case,
+ * avcodec_decode_audio4 has to be called again with an AVPacket containing
+ * the remaining data in order to decode the second frame, etc...
+ * Even if no frames are returned, the packet needs to be fed to the decoder
+ * with remaining data until it is completely consumed or an error occurs.
+ *
+ * @warning The input buffer, avpkt->data must be FF_INPUT_BUFFER_PADDING_SIZE
+ * larger than the actual read bytes because some optimized bitstream
+ * readers read 32 or 64 bits at once and could read over the end.
+ *
+ * @note You might have to align the input buffer. The alignment requirements
+ * depend on the CPU and the decoder.
+ *
+ * @param avctx the codec context
+ * @param[out] frame The AVFrame in which to store decoded audio samples.
+ * Decoders request a buffer of a particular size by setting
+ * AVFrame.nb_samples prior to calling get_buffer(). The
+ * decoder may, however, only utilize part of the buffer by
+ * setting AVFrame.nb_samples to a smaller value in the
+ * output frame.
+ * @param[out] got_frame_ptr Zero if no frame could be decoded, otherwise it is
+ * non-zero.
+ * @param[in] avpkt The input AVPacket containing the input buffer.
+ * At least avpkt->data and avpkt->size should be set. Some
+ * decoders might also require additional fields to be set.
+ * @return A negative error code is returned if an error occurred during
+ * decoding, otherwise the number of bytes consumed from the input
+ * AVPacket is returned.
+ */
+int avcodec_decode_audio4(AVCodecContext *avctx, AVFrame *frame,
+ int *got_frame_ptr, AVPacket *avpkt);
+
+/**
+ * Decode the video frame of size avpkt->size from avpkt->data into picture.
+ * Some decoders may support multiple frames in a single AVPacket, such
+ * decoders would then just decode the first frame.
+ *
+ * @warning The input buffer must be FF_INPUT_BUFFER_PADDING_SIZE larger than
+ * the actual read bytes because some optimized bitstream readers read 32 or 64
+ * bits at once and could read over the end.
+ *
+ * @warning The end of the input buffer buf should be set to 0 to ensure that
+ * no overreading happens for damaged MPEG streams.
+ *
+ * @note You might have to align the input buffer avpkt->data.
+ * The alignment requirements depend on the CPU: on some CPUs it isn't
+ * necessary at all, on others it won't work at all if not aligned and on others
+ * it will work but it will have an impact on performance.
+ *
+ * In practice, avpkt->data should have 4 byte alignment at minimum.
+ *
+ * @note Codecs which have the CODEC_CAP_DELAY capability set have a delay
+ * between input and output, these need to be fed with avpkt->data=NULL,
+ * avpkt->size=0 at the end to return the remaining frames.
+ *
+ * @param avctx the codec context
+ * @param[out] picture The AVFrame in which the decoded video frame will be stored.
+ * Use avcodec_alloc_frame to get an AVFrame, the codec will
+ * allocate memory for the actual bitmap.
+ * with default get/release_buffer(), the decoder frees/reuses the bitmap as it sees fit.
+ * with overridden get/release_buffer() (needs CODEC_CAP_DR1) the user decides into what buffer the decoder
+ * decodes and the decoder tells the user once it does not need the data anymore,
+ * the user app can at this point free/reuse/keep the memory as it sees fit.
+ *
+ * @param[in] avpkt The input AVpacket containing the input buffer.
+ * You can create such packet with av_init_packet() and by then setting
+ * data and size, some decoders might in addition need other fields like
+ * flags&AV_PKT_FLAG_KEY. All decoders are designed to use the least
+ * fields possible.
+ * @param[in,out] got_picture_ptr Zero if no frame could be decompressed, otherwise, it is nonzero.
+ * @return On error a negative value is returned, otherwise the number of bytes
+ * used or zero if no frame could be decompressed.
+ */
+int avcodec_decode_video2(AVCodecContext *avctx, AVFrame *picture,
+ int *got_picture_ptr,
+ AVPacket *avpkt);
+
+/**
+ * Decode a subtitle message.
+ * Return a negative value on error, otherwise return the number of bytes used.
+ * If no subtitle could be decompressed, got_sub_ptr is zero.
+ * Otherwise, the subtitle is stored in *sub.
+ * Note that CODEC_CAP_DR1 is not available for subtitle codecs. This is for
+ * simplicity, because the performance difference is expect to be negligible
+ * and reusing a get_buffer written for video codecs would probably perform badly
+ * due to a potentially very different allocation pattern.
+ *
+ * @param avctx the codec context
+ * @param[out] sub The AVSubtitle in which the decoded subtitle will be stored, must be
+ freed with avsubtitle_free if *got_sub_ptr is set.
+ * @param[in,out] got_sub_ptr Zero if no subtitle could be decompressed, otherwise, it is nonzero.
+ * @param[in] avpkt The input AVPacket containing the input buffer.
+ */
+int avcodec_decode_subtitle2(AVCodecContext *avctx, AVSubtitle *sub,
+ int *got_sub_ptr,
+ AVPacket *avpkt);
+
+/**
+ * Free all allocated data in the given subtitle struct.
+ *
+ * @param sub AVSubtitle to free.
+ */
+void avsubtitle_free(AVSubtitle *sub);
+
+#if FF_API_OLD_ENCODE_AUDIO
+/**
+ * Encode an audio frame from samples into buf.
+ *
+ * @deprecated Use avcodec_encode_audio2 instead.
+ *
+ * @note The output buffer should be at least FF_MIN_BUFFER_SIZE bytes large.
+ * However, for codecs with avctx->frame_size equal to 0 (e.g. PCM) the user
+ * will know how much space is needed because it depends on the value passed
+ * in buf_size as described below. In that case a lower value can be used.
+ *
+ * @param avctx the codec context
+ * @param[out] buf the output buffer
+ * @param[in] buf_size the output buffer size
+ * @param[in] samples the input buffer containing the samples
+ * The number of samples read from this buffer is frame_size*channels,
+ * both of which are defined in avctx.
+ * For codecs which have avctx->frame_size equal to 0 (e.g. PCM) the number of
+ * samples read from samples is equal to:
+ * buf_size * 8 / (avctx->channels * av_get_bits_per_sample(avctx->codec_id))
+ * This also implies that av_get_bits_per_sample() must not return 0 for these
+ * codecs.
+ * @return On error a negative value is returned, on success zero or the number
+ * of bytes used to encode the data read from the input buffer.
+ */
+int attribute_deprecated avcodec_encode_audio(AVCodecContext *avctx,
+ uint8_t *buf, int buf_size,
+ const short *samples);
+#endif
+
+/**
+ * Encode a frame of audio.
+ *
+ * Takes input samples from frame and writes the next output packet, if
+ * available, to avpkt. The output packet does not necessarily contain data for
+ * the most recent frame, as encoders can delay, split, and combine input frames
+ * internally as needed.
+ *
+ * @param avctx codec context
+ * @param avpkt output AVPacket.
+ * The user can supply an output buffer by setting
+ * avpkt->data and avpkt->size prior to calling the
+ * function, but if the size of the user-provided data is not
+ * large enough, encoding will fail. All other AVPacket fields
+ * will be reset by the encoder using av_init_packet(). If
+ * avpkt->data is NULL, the encoder will allocate it.
+ * The encoder will set avpkt->size to the size of the
+ * output packet.
+ * @param[in] frame AVFrame containing the raw audio data to be encoded.
+ * May be NULL when flushing an encoder that has the
+ * CODEC_CAP_DELAY capability set.
+ * There are 2 codec capabilities that affect the allowed
+ * values of frame->nb_samples.
+ * If CODEC_CAP_SMALL_LAST_FRAME is set, then only the final
+ * frame may be smaller than avctx->frame_size, and all other
+ * frames must be equal to avctx->frame_size.
+ * If CODEC_CAP_VARIABLE_FRAME_SIZE is set, then each frame
+ * can have any number of samples.
+ * If neither is set, frame->nb_samples must be equal to
+ * avctx->frame_size for all frames.
+ * @param[out] got_packet_ptr This field is set to 1 by libavcodec if the
+ * output packet is non-empty, and to 0 if it is
+ * empty. If the function returns an error, the
+ * packet can be assumed to be invalid, and the
+ * value of got_packet_ptr is undefined and should
+ * not be used.
+ * @return 0 on success, negative error code on failure
+ */
+int avcodec_encode_audio2(AVCodecContext *avctx, AVPacket *avpkt,
+ const AVFrame *frame, int *got_packet_ptr);
+
+/**
+ * Fill audio frame data and linesize.
+ * AVFrame extended_data channel pointers are allocated if necessary for
+ * planar audio.
+ *
+ * @param frame the AVFrame
+ * frame->nb_samples must be set prior to calling the
+ * function. This function fills in frame->data,
+ * frame->extended_data, frame->linesize[0].
+ * @param nb_channels channel count
+ * @param sample_fmt sample format
+ * @param buf buffer to use for frame data
+ * @param buf_size size of buffer
+ * @param align plane size sample alignment
+ * @return 0 on success, negative error code on failure
+ */
+int avcodec_fill_audio_frame(AVFrame *frame, int nb_channels,
+ enum AVSampleFormat sample_fmt, const uint8_t *buf,
+ int buf_size, int align);
+
+/**
+ * Encode a video frame from pict into buf.
+ * The input picture should be
+ * stored using a specific format, namely avctx.pix_fmt.
+ *
+ * @param avctx the codec context
+ * @param[out] buf the output buffer for the bitstream of encoded frame
+ * @param[in] buf_size the size of the output buffer in bytes
+ * @param[in] pict the input picture to encode
+ * @return On error a negative value is returned, on success zero or the number
+ * of bytes used from the output buffer.
+ */
+int avcodec_encode_video(AVCodecContext *avctx, uint8_t *buf, int buf_size,
+ const AVFrame *pict);
+int avcodec_encode_subtitle(AVCodecContext *avctx, uint8_t *buf, int buf_size,
+ const AVSubtitle *sub);
+
+/**
+ * Close a given AVCodecContext and free all the data associated with it
+ * (but not the AVCodecContext itself).
+ *
+ * Calling this function on an AVCodecContext that hasn't been opened will free
+ * the codec-specific data allocated in avcodec_alloc_context3() /
+ * avcodec_get_context_defaults3() with a non-NULL codec. Subsequent calls will
+ * do nothing.
+ */
+int avcodec_close(AVCodecContext *avctx);
+
+/**
+ * Register all the codecs, parsers and bitstream filters which were enabled at
+ * configuration time. If you do not call this function you can select exactly
+ * which formats you want to support, by using the individual registration
+ * functions.
+ *
+ * @see avcodec_register
+ * @see av_register_codec_parser
+ * @see av_register_bitstream_filter
+ */
+void avcodec_register_all(void);
+
+/**
+ * Flush buffers, should be called when seeking or when switching to a different stream.
+ */
+void avcodec_flush_buffers(AVCodecContext *avctx);
+
+void avcodec_default_free_buffers(AVCodecContext *s);
+
+/* misc useful functions */
+
+#if FF_API_OLD_FF_PICT_TYPES
+/**
+ * Return a single letter to describe the given picture type pict_type.
+ *
+ * @param[in] pict_type the picture type
+ * @return A single character representing the picture type.
+ * @deprecated Use av_get_picture_type_char() instead.
+ */
+attribute_deprecated
+char av_get_pict_type_char(int pict_type);
+#endif
+
+/**
+ * Return codec bits per sample.
+ *
+ * @param[in] codec_id the codec
+ * @return Number of bits per sample or zero if unknown for the given codec.
+ */
+int av_get_bits_per_sample(enum CodecID codec_id);
+
+#if FF_API_OLD_SAMPLE_FMT
+/**
+ * @deprecated Use av_get_bytes_per_sample() instead.
+ */
+attribute_deprecated
+int av_get_bits_per_sample_format(enum AVSampleFormat sample_fmt);
+#endif
+
+/* frame parsing */
+typedef struct AVCodecParserContext {
+ void *priv_data;
+ struct AVCodecParser *parser;
+ int64_t frame_offset; /* offset of the current frame */
+ int64_t cur_offset; /* current offset
+ (incremented by each av_parser_parse()) */
+ int64_t next_frame_offset; /* offset of the next frame */
+ /* video info */
+ int pict_type; /* XXX: Put it back in AVCodecContext. */
+ /**
+ * This field is used for proper frame duration computation in lavf.
+ * It signals, how much longer the frame duration of the current frame
+ * is compared to normal frame duration.
+ *
+ * frame_duration = (1 + repeat_pict) * time_base
+ *
+ * It is used by codecs like H.264 to display telecined material.
+ */
+ int repeat_pict; /* XXX: Put it back in AVCodecContext. */
+ int64_t pts; /* pts of the current frame */
+ int64_t dts; /* dts of the current frame */
+
+ /* private data */
+ int64_t last_pts;
+ int64_t last_dts;
+ int fetch_timestamp;
+
+#define AV_PARSER_PTS_NB 4
+ int cur_frame_start_index;
+ int64_t cur_frame_offset[AV_PARSER_PTS_NB];
+ int64_t cur_frame_pts[AV_PARSER_PTS_NB];
+ int64_t cur_frame_dts[AV_PARSER_PTS_NB];
+
+ int flags;
+#define PARSER_FLAG_COMPLETE_FRAMES 0x0001
+#define PARSER_FLAG_ONCE 0x0002
+/// Set if the parser has a valid file offset
+#define PARSER_FLAG_FETCHED_OFFSET 0x0004
+
+ int64_t offset; ///< byte offset from starting packet start
+ int64_t cur_frame_end[AV_PARSER_PTS_NB];
+
+ /**
+ * Set by parser to 1 for key frames and 0 for non-key frames.
+ * It is initialized to -1, so if the parser doesn't set this flag,
+ * old-style fallback using AV_PICTURE_TYPE_I picture type as key frames
+ * will be used.
+ */
+ int key_frame;
+
+ /**
+ * Time difference in stream time base units from the pts of this
+ * packet to the point at which the output from the decoder has converged
+ * independent from the availability of previous frames. That is, the
+ * frames are virtually identical no matter if decoding started from
+ * the very first frame or from this keyframe.
+ * Is AV_NOPTS_VALUE if unknown.
+ * This field is not the display duration of the current frame.
+ * This field has no meaning if the packet does not have AV_PKT_FLAG_KEY
+ * set.
+ *
+ * The purpose of this field is to allow seeking in streams that have no
+ * keyframes in the conventional sense. It corresponds to the
+ * recovery point SEI in H.264 and match_time_delta in NUT. It is also
+ * essential for some types of subtitle streams to ensure that all
+ * subtitles are correctly displayed after seeking.
+ */
+ int64_t convergence_duration;
+
+ // Timestamp generation support:
+ /**
+ * Synchronization point for start of timestamp generation.
+ *
+ * Set to >0 for sync point, 0 for no sync point and <0 for undefined
+ * (default).
+ *
+ * For example, this corresponds to presence of H.264 buffering period
+ * SEI message.
+ */
+ int dts_sync_point;
+
+ /**
+ * Offset of the current timestamp against last timestamp sync point in
+ * units of AVCodecContext.time_base.
+ *
+ * Set to INT_MIN when dts_sync_point unused. Otherwise, it must
+ * contain a valid timestamp offset.
+ *
+ * Note that the timestamp of sync point has usually a nonzero
+ * dts_ref_dts_delta, which refers to the previous sync point. Offset of
+ * the next frame after timestamp sync point will be usually 1.
+ *
+ * For example, this corresponds to H.264 cpb_removal_delay.
+ */
+ int dts_ref_dts_delta;
+
+ /**
+ * Presentation delay of current frame in units of AVCodecContext.time_base.
+ *
+ * Set to INT_MIN when dts_sync_point unused. Otherwise, it must
+ * contain valid non-negative timestamp delta (presentation time of a frame
+ * must not lie in the past).
+ *
+ * This delay represents the difference between decoding and presentation
+ * time of the frame.
+ *
+ * For example, this corresponds to H.264 dpb_output_delay.
+ */
+ int pts_dts_delta;
+
+ /**
+ * Position of the packet in file.
+ *
+ * Analogous to cur_frame_pts/dts
+ */
+ int64_t cur_frame_pos[AV_PARSER_PTS_NB];
+
+ /**
+ * Byte position of currently parsed frame in stream.
+ */
+ int64_t pos;
+
+ /**
+ * Previous frame byte position.
+ */
+ int64_t last_pos;
+} AVCodecParserContext;
+
+typedef struct AVCodecParser {
+ int codec_ids[5]; /* several codec IDs are permitted */
+ int priv_data_size;
+ int (*parser_init)(AVCodecParserContext *s);
+ int (*parser_parse)(AVCodecParserContext *s,
+ AVCodecContext *avctx,
+ const uint8_t **poutbuf, int *poutbuf_size,
+ const uint8_t *buf, int buf_size);
+ void (*parser_close)(AVCodecParserContext *s);
+ int (*split)(AVCodecContext *avctx, const uint8_t *buf, int buf_size);
+ struct AVCodecParser *next;
+} AVCodecParser;
+
+AVCodecParser *av_parser_next(AVCodecParser *c);
+
+void av_register_codec_parser(AVCodecParser *parser);
+AVCodecParserContext *av_parser_init(int codec_id);
+
+/**
+ * Parse a packet.
+ *
+ * @param s parser context.
+ * @param avctx codec context.
+ * @param poutbuf set to pointer to parsed buffer or NULL if not yet finished.
+ * @param poutbuf_size set to size of parsed buffer or zero if not yet finished.
+ * @param buf input buffer.
+ * @param buf_size input length, to signal EOF, this should be 0 (so that the last frame can be output).
+ * @param pts input presentation timestamp.
+ * @param dts input decoding timestamp.
+ * @param pos input byte position in stream.
+ * @return the number of bytes of the input bitstream used.
+ *
+ * Example:
+ * @code
+ * while(in_len){
+ * len = av_parser_parse2(myparser, AVCodecContext, &data, &size,
+ * in_data, in_len,
+ * pts, dts, pos);
+ * in_data += len;
+ * in_len -= len;
+ *
+ * if(size)
+ * decode_frame(data, size);
+ * }
+ * @endcode
+ */
+int av_parser_parse2(AVCodecParserContext *s,
+ AVCodecContext *avctx,
+ uint8_t **poutbuf, int *poutbuf_size,
+ const uint8_t *buf, int buf_size,
+ int64_t pts, int64_t dts,
+ int64_t pos);
+
+int av_parser_change(AVCodecParserContext *s,
+ AVCodecContext *avctx,
+ uint8_t **poutbuf, int *poutbuf_size,
+ const uint8_t *buf, int buf_size, int keyframe);
+void av_parser_close(AVCodecParserContext *s);
+
+
+typedef struct AVBitStreamFilterContext {
+ void *priv_data;
+ struct AVBitStreamFilter *filter;
+ AVCodecParserContext *parser;
+ struct AVBitStreamFilterContext *next;
+} AVBitStreamFilterContext;
+
+
+typedef struct AVBitStreamFilter {
+ const char *name;
+ int priv_data_size;
+ int (*filter)(AVBitStreamFilterContext *bsfc,
+ AVCodecContext *avctx, const char *args,
+ uint8_t **poutbuf, int *poutbuf_size,
+ const uint8_t *buf, int buf_size, int keyframe);
+ void (*close)(AVBitStreamFilterContext *bsfc);
+ struct AVBitStreamFilter *next;
+} AVBitStreamFilter;
+
+void av_register_bitstream_filter(AVBitStreamFilter *bsf);
+AVBitStreamFilterContext *av_bitstream_filter_init(const char *name);
+int av_bitstream_filter_filter(AVBitStreamFilterContext *bsfc,
+ AVCodecContext *avctx, const char *args,
+ uint8_t **poutbuf, int *poutbuf_size,
+ const uint8_t *buf, int buf_size, int keyframe);
+void av_bitstream_filter_close(AVBitStreamFilterContext *bsf);
+
+AVBitStreamFilter *av_bitstream_filter_next(AVBitStreamFilter *f);
+
+/* memory */
+
+/**
+ * Reallocate the given block if it is not large enough, otherwise do nothing.
+ *
+ * @see av_realloc
+ */
+void *av_fast_realloc(void *ptr, unsigned int *size, size_t min_size);
+
+/**
+ * Allocate a buffer, reusing the given one if large enough.
+ *
+ * Contrary to av_fast_realloc the current buffer contents might not be
+ * preserved and on error the old buffer is freed, thus no special
+ * handling to avoid memleaks is necessary.
+ *
+ * @param ptr pointer to pointer to already allocated buffer, overwritten with pointer to new buffer
+ * @param size size of the buffer *ptr points to
+ * @param min_size minimum size of *ptr buffer after returning, *ptr will be NULL and
+ * *size 0 if an error occurred.
+ */
+void av_fast_malloc(void *ptr, unsigned int *size, size_t min_size);
+
+/**
+ * Copy image src to dst. Wraps av_picture_data_copy() above.
+ */
+void av_picture_copy(AVPicture *dst, const AVPicture *src,
+ enum PixelFormat pix_fmt, int width, int height);
+
+/**
+ * Crop image top and left side.
+ */
+int av_picture_crop(AVPicture *dst, const AVPicture *src,
+ enum PixelFormat pix_fmt, int top_band, int left_band);
+
+/**
+ * Pad image.
+ */
+int av_picture_pad(AVPicture *dst, const AVPicture *src, int height, int width, enum PixelFormat pix_fmt,
+ int padtop, int padbottom, int padleft, int padright, int *color);
+
+/**
+ * Encode extradata length to a buffer. Used by xiph codecs.
+ *
+ * @param s buffer to write to; must be at least (v/255+1) bytes long
+ * @param v size of extradata in bytes
+ * @return number of bytes written to the buffer.
+ */
+unsigned int av_xiphlacing(unsigned char *s, unsigned int v);
+
+/**
+ * Log a generic warning message about a missing feature. This function is
+ * intended to be used internally by Libav (libavcodec, libavformat, etc.)
+ * only, and would normally not be used by applications.
+ * @param[in] avc a pointer to an arbitrary struct of which the first field is
+ * a pointer to an AVClass struct
+ * @param[in] feature string containing the name of the missing feature
+ * @param[in] want_sample indicates if samples are wanted which exhibit this feature.
+ * If want_sample is non-zero, additional verbage will be added to the log
+ * message which tells the user how to report samples to the development
+ * mailing list.
+ */
+void av_log_missing_feature(void *avc, const char *feature, int want_sample);
+
+/**
+ * Log a generic warning message asking for a sample. This function is
+ * intended to be used internally by Libav (libavcodec, libavformat, etc.)
+ * only, and would normally not be used by applications.
+ * @param[in] avc a pointer to an arbitrary struct of which the first field is
+ * a pointer to an AVClass struct
+ * @param[in] msg string containing an optional message, or NULL if no message
+ */
+void av_log_ask_for_sample(void *avc, const char *msg, ...) av_printf_format(2, 3);
+
+/**
+ * Register the hardware accelerator hwaccel.
+ */
+void av_register_hwaccel(AVHWAccel *hwaccel);
+
+/**
+ * If hwaccel is NULL, returns the first registered hardware accelerator,
+ * if hwaccel is non-NULL, returns the next registered hardware accelerator
+ * after hwaccel, or NULL if hwaccel is the last one.
+ */
+AVHWAccel *av_hwaccel_next(AVHWAccel *hwaccel);
+
+
+/**
+ * Lock operation used by lockmgr
+ */
+enum AVLockOp {
+ AV_LOCK_CREATE, ///< Create a mutex
+ AV_LOCK_OBTAIN, ///< Lock the mutex
+ AV_LOCK_RELEASE, ///< Unlock the mutex
+ AV_LOCK_DESTROY, ///< Free mutex resources
+};
+
+/**
+ * Register a user provided lock manager supporting the operations
+ * specified by AVLockOp. mutex points to a (void *) where the
+ * lockmgr should store/get a pointer to a user allocated mutex. It's
+ * NULL upon AV_LOCK_CREATE and != NULL for all other ops.
+ *
+ * @param cb User defined callback. Note: Libav may invoke calls to this
+ * callback during the call to av_lockmgr_register().
+ * Thus, the application must be prepared to handle that.
+ * If cb is set to NULL the lockmgr will be unregistered.
+ * Also note that during unregistration the previously registered
+ * lockmgr callback may also be invoked.
+ */
+int av_lockmgr_register(int (*cb)(void **mutex, enum AVLockOp op));
+
+/**
+ * Get the type of the given codec.
+ */
+enum AVMediaType avcodec_get_type(enum CodecID codec_id);
+
+/**
+ * Get the AVClass for AVCodecContext. It can be used in combination with
+ * AV_OPT_SEARCH_FAKE_OBJ for examining options.
+ *
+ * @see av_opt_find().
+ */
+const AVClass *avcodec_get_class(void);
+
+/**
+ * @return a positive value if s is open (i.e. avcodec_open2() was called on it
+ * with no corresponding avcodec_close()), 0 otherwise.
+ */
+int avcodec_is_open(AVCodecContext *s);
+
+#endif /* AVCODEC_AVCODEC_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavcodec/avfft.h b/dom/media/platforms/ffmpeg/libav53/include/libavcodec/avfft.h
new file mode 100644
index 0000000000..91fe2f4297
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavcodec/avfft.h
@@ -0,0 +1,99 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_AVFFT_H
+#define AVCODEC_AVFFT_H
+
+typedef float FFTSample;
+
+typedef struct FFTComplex {
+ FFTSample re, im;
+} FFTComplex;
+
+typedef struct FFTContext FFTContext;
+
+/**
+ * Set up a complex FFT.
+ * @param nbits log2 of the length of the input array
+ * @param inverse if 0 perform the forward transform, if 1 perform the inverse
+ */
+FFTContext *av_fft_init(int nbits, int inverse);
+
+/**
+ * Do the permutation needed BEFORE calling ff_fft_calc().
+ */
+void av_fft_permute(FFTContext *s, FFTComplex *z);
+
+/**
+ * Do a complex FFT with the parameters defined in av_fft_init(). The
+ * input data must be permuted before. No 1.0/sqrt(n) normalization is done.
+ */
+void av_fft_calc(FFTContext *s, FFTComplex *z);
+
+void av_fft_end(FFTContext *s);
+
+FFTContext *av_mdct_init(int nbits, int inverse, double scale);
+void av_imdct_calc(FFTContext *s, FFTSample *output, const FFTSample *input);
+void av_imdct_half(FFTContext *s, FFTSample *output, const FFTSample *input);
+void av_mdct_calc(FFTContext *s, FFTSample *output, const FFTSample *input);
+void av_mdct_end(FFTContext *s);
+
+/* Real Discrete Fourier Transform */
+
+enum RDFTransformType {
+ DFT_R2C,
+ IDFT_C2R,
+ IDFT_R2C,
+ DFT_C2R,
+};
+
+typedef struct RDFTContext RDFTContext;
+
+/**
+ * Set up a real FFT.
+ * @param nbits log2 of the length of the input array
+ * @param trans the type of transform
+ */
+RDFTContext *av_rdft_init(int nbits, enum RDFTransformType trans);
+void av_rdft_calc(RDFTContext *s, FFTSample *data);
+void av_rdft_end(RDFTContext *s);
+
+/* Discrete Cosine Transform */
+
+typedef struct DCTContext DCTContext;
+
+enum DCTTransformType {
+ DCT_II = 0,
+ DCT_III,
+ DCT_I,
+ DST_I,
+};
+
+/**
+ * Set up DCT.
+ * @param nbits size of the input array:
+ * (1 << nbits) for DCT-II, DCT-III and DST-I
+ * (1 << nbits) + 1 for DCT-I
+ *
+ * @note the first element of the input of DST-I is ignored
+ */
+DCTContext *av_dct_init(int nbits, enum DCTTransformType type);
+void av_dct_calc(DCTContext *s, FFTSample *data);
+void av_dct_end (DCTContext *s);
+
+#endif /* AVCODEC_AVFFT_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavcodec/dxva2.h b/dom/media/platforms/ffmpeg/libav53/include/libavcodec/dxva2.h
new file mode 100644
index 0000000000..374ae039ac
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavcodec/dxva2.h
@@ -0,0 +1,71 @@
+/*
+ * DXVA2 HW acceleration
+ *
+ * copyright (c) 2009 Laurent Aimar
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_DXVA_H
+#define AVCODEC_DXVA_H
+
+#include <stdint.h>
+
+#include <d3d9.h>
+#include <dxva2api.h>
+
+#define FF_DXVA2_WORKAROUND_SCALING_LIST_ZIGZAG 1 ///< Work around for DXVA2 and old UVD/UVD+ ATI video cards
+
+/**
+ * This structure is used to provides the necessary configurations and data
+ * to the DXVA2 Libav HWAccel implementation.
+ *
+ * The application must make it available as AVCodecContext.hwaccel_context.
+ */
+struct dxva_context {
+ /**
+ * DXVA2 decoder object
+ */
+ IDirectXVideoDecoder *decoder;
+
+ /**
+ * DXVA2 configuration used to create the decoder
+ */
+ const DXVA2_ConfigPictureDecode *cfg;
+
+ /**
+ * The number of surface in the surface array
+ */
+ unsigned surface_count;
+
+ /**
+ * The array of Direct3D surfaces used to create the decoder
+ */
+ LPDIRECT3DSURFACE9 *surface;
+
+ /**
+ * A bit field configuring the workarounds needed for using the decoder
+ */
+ uint64_t workaround;
+
+ /**
+ * Private to the Libav AVHWAccel implementation
+ */
+ unsigned report_id;
+};
+
+#endif /* AVCODEC_DXVA_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavcodec/old_codec_ids.h b/dom/media/platforms/ffmpeg/libav53/include/libavcodec/old_codec_ids.h
new file mode 100644
index 0000000000..ded4cc7877
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavcodec/old_codec_ids.h
@@ -0,0 +1,398 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_OLD_CODEC_IDS_H
+#define AVCODEC_OLD_CODEC_IDS_H
+
+#include "libavutil/common.h"
+
+/*
+ * This header exists to prevent new codec IDs from being accidentally added to
+ * the deprecated list.
+ * Do not include it directly. It will be removed on next major bump
+ *
+ * Do not add new items to this list. Use the AVCodecID enum instead.
+ */
+
+ CODEC_ID_NONE = AV_CODEC_ID_NONE,
+
+ /* video codecs */
+ CODEC_ID_MPEG1VIDEO,
+ CODEC_ID_MPEG2VIDEO, ///< preferred ID for MPEG-1/2 video decoding
+ CODEC_ID_MPEG2VIDEO_XVMC,
+ CODEC_ID_H261,
+ CODEC_ID_H263,
+ CODEC_ID_RV10,
+ CODEC_ID_RV20,
+ CODEC_ID_MJPEG,
+ CODEC_ID_MJPEGB,
+ CODEC_ID_LJPEG,
+ CODEC_ID_SP5X,
+ CODEC_ID_JPEGLS,
+ CODEC_ID_MPEG4,
+ CODEC_ID_RAWVIDEO,
+ CODEC_ID_MSMPEG4V1,
+ CODEC_ID_MSMPEG4V2,
+ CODEC_ID_MSMPEG4V3,
+ CODEC_ID_WMV1,
+ CODEC_ID_WMV2,
+ CODEC_ID_H263P,
+ CODEC_ID_H263I,
+ CODEC_ID_FLV1,
+ CODEC_ID_SVQ1,
+ CODEC_ID_SVQ3,
+ CODEC_ID_DVVIDEO,
+ CODEC_ID_HUFFYUV,
+ CODEC_ID_CYUV,
+ CODEC_ID_H264,
+ CODEC_ID_INDEO3,
+ CODEC_ID_VP3,
+ CODEC_ID_THEORA,
+ CODEC_ID_ASV1,
+ CODEC_ID_ASV2,
+ CODEC_ID_FFV1,
+ CODEC_ID_4XM,
+ CODEC_ID_VCR1,
+ CODEC_ID_CLJR,
+ CODEC_ID_MDEC,
+ CODEC_ID_ROQ,
+ CODEC_ID_INTERPLAY_VIDEO,
+ CODEC_ID_XAN_WC3,
+ CODEC_ID_XAN_WC4,
+ CODEC_ID_RPZA,
+ CODEC_ID_CINEPAK,
+ CODEC_ID_WS_VQA,
+ CODEC_ID_MSRLE,
+ CODEC_ID_MSVIDEO1,
+ CODEC_ID_IDCIN,
+ CODEC_ID_8BPS,
+ CODEC_ID_SMC,
+ CODEC_ID_FLIC,
+ CODEC_ID_TRUEMOTION1,
+ CODEC_ID_VMDVIDEO,
+ CODEC_ID_MSZH,
+ CODEC_ID_ZLIB,
+ CODEC_ID_QTRLE,
+ CODEC_ID_SNOW,
+ CODEC_ID_TSCC,
+ CODEC_ID_ULTI,
+ CODEC_ID_QDRAW,
+ CODEC_ID_VIXL,
+ CODEC_ID_QPEG,
+ CODEC_ID_PNG,
+ CODEC_ID_PPM,
+ CODEC_ID_PBM,
+ CODEC_ID_PGM,
+ CODEC_ID_PGMYUV,
+ CODEC_ID_PAM,
+ CODEC_ID_FFVHUFF,
+ CODEC_ID_RV30,
+ CODEC_ID_RV40,
+ CODEC_ID_VC1,
+ CODEC_ID_WMV3,
+ CODEC_ID_LOCO,
+ CODEC_ID_WNV1,
+ CODEC_ID_AASC,
+ CODEC_ID_INDEO2,
+ CODEC_ID_FRAPS,
+ CODEC_ID_TRUEMOTION2,
+ CODEC_ID_BMP,
+ CODEC_ID_CSCD,
+ CODEC_ID_MMVIDEO,
+ CODEC_ID_ZMBV,
+ CODEC_ID_AVS,
+ CODEC_ID_SMACKVIDEO,
+ CODEC_ID_NUV,
+ CODEC_ID_KMVC,
+ CODEC_ID_FLASHSV,
+ CODEC_ID_CAVS,
+ CODEC_ID_JPEG2000,
+ CODEC_ID_VMNC,
+ CODEC_ID_VP5,
+ CODEC_ID_VP6,
+ CODEC_ID_VP6F,
+ CODEC_ID_TARGA,
+ CODEC_ID_DSICINVIDEO,
+ CODEC_ID_TIERTEXSEQVIDEO,
+ CODEC_ID_TIFF,
+ CODEC_ID_GIF,
+ CODEC_ID_DXA,
+ CODEC_ID_DNXHD,
+ CODEC_ID_THP,
+ CODEC_ID_SGI,
+ CODEC_ID_C93,
+ CODEC_ID_BETHSOFTVID,
+ CODEC_ID_PTX,
+ CODEC_ID_TXD,
+ CODEC_ID_VP6A,
+ CODEC_ID_AMV,
+ CODEC_ID_VB,
+ CODEC_ID_PCX,
+ CODEC_ID_SUNRAST,
+ CODEC_ID_INDEO4,
+ CODEC_ID_INDEO5,
+ CODEC_ID_MIMIC,
+ CODEC_ID_RL2,
+ CODEC_ID_ESCAPE124,
+ CODEC_ID_DIRAC,
+ CODEC_ID_BFI,
+ CODEC_ID_CMV,
+ CODEC_ID_MOTIONPIXELS,
+ CODEC_ID_TGV,
+ CODEC_ID_TGQ,
+ CODEC_ID_TQI,
+ CODEC_ID_AURA,
+ CODEC_ID_AURA2,
+ CODEC_ID_V210X,
+ CODEC_ID_TMV,
+ CODEC_ID_V210,
+ CODEC_ID_DPX,
+ CODEC_ID_MAD,
+ CODEC_ID_FRWU,
+ CODEC_ID_FLASHSV2,
+ CODEC_ID_CDGRAPHICS,
+ CODEC_ID_R210,
+ CODEC_ID_ANM,
+ CODEC_ID_BINKVIDEO,
+ CODEC_ID_IFF_ILBM,
+ CODEC_ID_IFF_BYTERUN1,
+ CODEC_ID_KGV1,
+ CODEC_ID_YOP,
+ CODEC_ID_VP8,
+ CODEC_ID_PICTOR,
+ CODEC_ID_ANSI,
+ CODEC_ID_A64_MULTI,
+ CODEC_ID_A64_MULTI5,
+ CODEC_ID_R10K,
+ CODEC_ID_MXPEG,
+ CODEC_ID_LAGARITH,
+ CODEC_ID_PRORES,
+ CODEC_ID_JV,
+ CODEC_ID_DFA,
+ CODEC_ID_WMV3IMAGE,
+ CODEC_ID_VC1IMAGE,
+ CODEC_ID_UTVIDEO,
+ CODEC_ID_BMV_VIDEO,
+ CODEC_ID_VBLE,
+ CODEC_ID_DXTORY,
+ CODEC_ID_V410,
+ CODEC_ID_XWD,
+ CODEC_ID_CDXL,
+ CODEC_ID_XBM,
+ CODEC_ID_ZEROCODEC,
+ CODEC_ID_MSS1,
+ CODEC_ID_MSA1,
+ CODEC_ID_TSCC2,
+ CODEC_ID_MTS2,
+ CODEC_ID_CLLC,
+ CODEC_ID_Y41P = MKBETAG('Y','4','1','P'),
+ CODEC_ID_ESCAPE130 = MKBETAG('E','1','3','0'),
+ CODEC_ID_EXR = MKBETAG('0','E','X','R'),
+ CODEC_ID_AVRP = MKBETAG('A','V','R','P'),
+
+ CODEC_ID_G2M = MKBETAG( 0 ,'G','2','M'),
+ CODEC_ID_AVUI = MKBETAG('A','V','U','I'),
+ CODEC_ID_AYUV = MKBETAG('A','Y','U','V'),
+ CODEC_ID_V308 = MKBETAG('V','3','0','8'),
+ CODEC_ID_V408 = MKBETAG('V','4','0','8'),
+ CODEC_ID_YUV4 = MKBETAG('Y','U','V','4'),
+ CODEC_ID_SANM = MKBETAG('S','A','N','M'),
+ CODEC_ID_PAF_VIDEO = MKBETAG('P','A','F','V'),
+
+ /* various PCM "codecs" */
+ CODEC_ID_FIRST_AUDIO = 0x10000, ///< A dummy id pointing at the start of audio codecs
+ CODEC_ID_PCM_S16LE = 0x10000,
+ CODEC_ID_PCM_S16BE,
+ CODEC_ID_PCM_U16LE,
+ CODEC_ID_PCM_U16BE,
+ CODEC_ID_PCM_S8,
+ CODEC_ID_PCM_U8,
+ CODEC_ID_PCM_MULAW,
+ CODEC_ID_PCM_ALAW,
+ CODEC_ID_PCM_S32LE,
+ CODEC_ID_PCM_S32BE,
+ CODEC_ID_PCM_U32LE,
+ CODEC_ID_PCM_U32BE,
+ CODEC_ID_PCM_S24LE,
+ CODEC_ID_PCM_S24BE,
+ CODEC_ID_PCM_U24LE,
+ CODEC_ID_PCM_U24BE,
+ CODEC_ID_PCM_S24DAUD,
+ CODEC_ID_PCM_ZORK,
+ CODEC_ID_PCM_S16LE_PLANAR,
+ CODEC_ID_PCM_DVD,
+ CODEC_ID_PCM_F32BE,
+ CODEC_ID_PCM_F32LE,
+ CODEC_ID_PCM_F64BE,
+ CODEC_ID_PCM_F64LE,
+ CODEC_ID_PCM_BLURAY,
+ CODEC_ID_PCM_LXF,
+ CODEC_ID_S302M,
+ CODEC_ID_PCM_S8_PLANAR,
+
+ /* various ADPCM codecs */
+ CODEC_ID_ADPCM_IMA_QT = 0x11000,
+ CODEC_ID_ADPCM_IMA_WAV,
+ CODEC_ID_ADPCM_IMA_DK3,
+ CODEC_ID_ADPCM_IMA_DK4,
+ CODEC_ID_ADPCM_IMA_WS,
+ CODEC_ID_ADPCM_IMA_SMJPEG,
+ CODEC_ID_ADPCM_MS,
+ CODEC_ID_ADPCM_4XM,
+ CODEC_ID_ADPCM_XA,
+ CODEC_ID_ADPCM_ADX,
+ CODEC_ID_ADPCM_EA,
+ CODEC_ID_ADPCM_G726,
+ CODEC_ID_ADPCM_CT,
+ CODEC_ID_ADPCM_SWF,
+ CODEC_ID_ADPCM_YAMAHA,
+ CODEC_ID_ADPCM_SBPRO_4,
+ CODEC_ID_ADPCM_SBPRO_3,
+ CODEC_ID_ADPCM_SBPRO_2,
+ CODEC_ID_ADPCM_THP,
+ CODEC_ID_ADPCM_IMA_AMV,
+ CODEC_ID_ADPCM_EA_R1,
+ CODEC_ID_ADPCM_EA_R3,
+ CODEC_ID_ADPCM_EA_R2,
+ CODEC_ID_ADPCM_IMA_EA_SEAD,
+ CODEC_ID_ADPCM_IMA_EA_EACS,
+ CODEC_ID_ADPCM_EA_XAS,
+ CODEC_ID_ADPCM_EA_MAXIS_XA,
+ CODEC_ID_ADPCM_IMA_ISS,
+ CODEC_ID_ADPCM_G722,
+ CODEC_ID_ADPCM_IMA_APC,
+ CODEC_ID_VIMA = MKBETAG('V','I','M','A'),
+
+ /* AMR */
+ CODEC_ID_AMR_NB = 0x12000,
+ CODEC_ID_AMR_WB,
+
+ /* RealAudio codecs*/
+ CODEC_ID_RA_144 = 0x13000,
+ CODEC_ID_RA_288,
+
+ /* various DPCM codecs */
+ CODEC_ID_ROQ_DPCM = 0x14000,
+ CODEC_ID_INTERPLAY_DPCM,
+ CODEC_ID_XAN_DPCM,
+ CODEC_ID_SOL_DPCM,
+
+ /* audio codecs */
+ CODEC_ID_MP2 = 0x15000,
+ CODEC_ID_MP3, ///< preferred ID for decoding MPEG audio layer 1, 2 or 3
+ CODEC_ID_AAC,
+ CODEC_ID_AC3,
+ CODEC_ID_DTS,
+ CODEC_ID_VORBIS,
+ CODEC_ID_DVAUDIO,
+ CODEC_ID_WMAV1,
+ CODEC_ID_WMAV2,
+ CODEC_ID_MACE3,
+ CODEC_ID_MACE6,
+ CODEC_ID_VMDAUDIO,
+ CODEC_ID_FLAC,
+ CODEC_ID_MP3ADU,
+ CODEC_ID_MP3ON4,
+ CODEC_ID_SHORTEN,
+ CODEC_ID_ALAC,
+ CODEC_ID_WESTWOOD_SND1,
+ CODEC_ID_GSM, ///< as in Berlin toast format
+ CODEC_ID_QDM2,
+ CODEC_ID_COOK,
+ CODEC_ID_TRUESPEECH,
+ CODEC_ID_TTA,
+ CODEC_ID_SMACKAUDIO,
+ CODEC_ID_QCELP,
+ CODEC_ID_WAVPACK,
+ CODEC_ID_DSICINAUDIO,
+ CODEC_ID_IMC,
+ CODEC_ID_MUSEPACK7,
+ CODEC_ID_MLP,
+ CODEC_ID_GSM_MS, /* as found in WAV */
+ CODEC_ID_ATRAC3,
+ CODEC_ID_VOXWARE,
+ CODEC_ID_APE,
+ CODEC_ID_NELLYMOSER,
+ CODEC_ID_MUSEPACK8,
+ CODEC_ID_SPEEX,
+ CODEC_ID_WMAVOICE,
+ CODEC_ID_WMAPRO,
+ CODEC_ID_WMALOSSLESS,
+ CODEC_ID_ATRAC3P,
+ CODEC_ID_EAC3,
+ CODEC_ID_SIPR,
+ CODEC_ID_MP1,
+ CODEC_ID_TWINVQ,
+ CODEC_ID_TRUEHD,
+ CODEC_ID_MP4ALS,
+ CODEC_ID_ATRAC1,
+ CODEC_ID_BINKAUDIO_RDFT,
+ CODEC_ID_BINKAUDIO_DCT,
+ CODEC_ID_AAC_LATM,
+ CODEC_ID_QDMC,
+ CODEC_ID_CELT,
+ CODEC_ID_G723_1,
+ CODEC_ID_G729,
+ CODEC_ID_8SVX_EXP,
+ CODEC_ID_8SVX_FIB,
+ CODEC_ID_BMV_AUDIO,
+ CODEC_ID_RALF,
+ CODEC_ID_IAC,
+ CODEC_ID_ILBC,
+ CODEC_ID_FFWAVESYNTH = MKBETAG('F','F','W','S'),
+ CODEC_ID_8SVX_RAW = MKBETAG('8','S','V','X'),
+ CODEC_ID_SONIC = MKBETAG('S','O','N','C'),
+ CODEC_ID_SONIC_LS = MKBETAG('S','O','N','L'),
+ CODEC_ID_PAF_AUDIO = MKBETAG('P','A','F','A'),
+ CODEC_ID_OPUS = MKBETAG('O','P','U','S'),
+
+ /* subtitle codecs */
+ CODEC_ID_FIRST_SUBTITLE = 0x17000, ///< A dummy ID pointing at the start of subtitle codecs.
+ CODEC_ID_DVD_SUBTITLE = 0x17000,
+ CODEC_ID_DVB_SUBTITLE,
+ CODEC_ID_TEXT, ///< raw UTF-8 text
+ CODEC_ID_XSUB,
+ CODEC_ID_SSA,
+ CODEC_ID_MOV_TEXT,
+ CODEC_ID_HDMV_PGS_SUBTITLE,
+ CODEC_ID_DVB_TELETEXT,
+ CODEC_ID_SRT,
+ CODEC_ID_MICRODVD = MKBETAG('m','D','V','D'),
+ CODEC_ID_EIA_608 = MKBETAG('c','6','0','8'),
+ CODEC_ID_JACOSUB = MKBETAG('J','S','U','B'),
+ CODEC_ID_SAMI = MKBETAG('S','A','M','I'),
+ CODEC_ID_REALTEXT = MKBETAG('R','T','X','T'),
+ CODEC_ID_SUBVIEWER = MKBETAG('S','u','b','V'),
+
+ /* other specific kind of codecs (generally used for attachments) */
+ CODEC_ID_FIRST_UNKNOWN = 0x18000, ///< A dummy ID pointing at the start of various fake codecs.
+ CODEC_ID_TTF = 0x18000,
+ CODEC_ID_BINTEXT = MKBETAG('B','T','X','T'),
+ CODEC_ID_XBIN = MKBETAG('X','B','I','N'),
+ CODEC_ID_IDF = MKBETAG( 0 ,'I','D','F'),
+ CODEC_ID_OTF = MKBETAG( 0 ,'O','T','F'),
+
+ CODEC_ID_PROBE = 0x19000, ///< codec_id is not known (like CODEC_ID_NONE) but lavf should attempt to identify it
+
+ CODEC_ID_MPEG2TS = 0x20000, /**< _FAKE_ codec to indicate a raw MPEG-2 TS
+ * stream (only used by libavformat) */
+ CODEC_ID_MPEG4SYSTEMS = 0x20001, /**< _FAKE_ codec to indicate a MPEG-4 Systems
+ * stream (only used by libavformat) */
+ CODEC_ID_FFMETADATA = 0x21000, ///< Dummy codec for streams containing only metadata information.
+
+#endif /* AVCODEC_OLD_CODEC_IDS_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavcodec/opt.h b/dom/media/platforms/ffmpeg/libav53/include/libavcodec/opt.h
new file mode 100644
index 0000000000..2380e74332
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavcodec/opt.h
@@ -0,0 +1,34 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * This header is provided for compatibility only and will be removed
+ * on next major bump
+ */
+
+#ifndef AVCODEC_OPT_H
+#define AVCODEC_OPT_H
+
+#include "libavcodec/version.h"
+
+#if FF_API_OPT_H
+#include "libavutil/opt.h"
+#endif
+
+#endif /* AVCODEC_OPT_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavcodec/vaapi.h b/dom/media/platforms/ffmpeg/libav53/include/libavcodec/vaapi.h
new file mode 100644
index 0000000000..36fb386acf
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavcodec/vaapi.h
@@ -0,0 +1,167 @@
+/*
+ * Video Acceleration API (shared data between Libav and the video player)
+ * HW decode acceleration for MPEG-2, MPEG-4, H.264 and VC-1
+ *
+ * Copyright (C) 2008-2009 Splitted-Desktop Systems
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_VAAPI_H
+#define AVCODEC_VAAPI_H
+
+#include <stdint.h>
+
+/**
+ * @defgroup VAAPI_Decoding VA API Decoding
+ * @ingroup Decoder
+ * @{
+ */
+
+/**
+ * This structure is used to share data between the Libav library and
+ * the client video application.
+ * This shall be zero-allocated and available as
+ * AVCodecContext.hwaccel_context. All user members can be set once
+ * during initialization or through each AVCodecContext.get_buffer()
+ * function call. In any case, they must be valid prior to calling
+ * decoding functions.
+ */
+struct vaapi_context {
+ /**
+ * Window system dependent data
+ *
+ * - encoding: unused
+ * - decoding: Set by user
+ */
+ void *display;
+
+ /**
+ * Configuration ID
+ *
+ * - encoding: unused
+ * - decoding: Set by user
+ */
+ uint32_t config_id;
+
+ /**
+ * Context ID (video decode pipeline)
+ *
+ * - encoding: unused
+ * - decoding: Set by user
+ */
+ uint32_t context_id;
+
+ /**
+ * VAPictureParameterBuffer ID
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ uint32_t pic_param_buf_id;
+
+ /**
+ * VAIQMatrixBuffer ID
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ uint32_t iq_matrix_buf_id;
+
+ /**
+ * VABitPlaneBuffer ID (for VC-1 decoding)
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ uint32_t bitplane_buf_id;
+
+ /**
+ * Slice parameter/data buffer IDs
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ uint32_t *slice_buf_ids;
+
+ /**
+ * Number of effective slice buffer IDs to send to the HW
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ unsigned int n_slice_buf_ids;
+
+ /**
+ * Size of pre-allocated slice_buf_ids
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ unsigned int slice_buf_ids_alloc;
+
+ /**
+ * Pointer to VASliceParameterBuffers
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ void *slice_params;
+
+ /**
+ * Size of a VASliceParameterBuffer element
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ unsigned int slice_param_size;
+
+ /**
+ * Size of pre-allocated slice_params
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ unsigned int slice_params_alloc;
+
+ /**
+ * Number of slices currently filled in
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ unsigned int slice_count;
+
+ /**
+ * Pointer to slice data buffer base
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ const uint8_t *slice_data;
+
+ /**
+ * Current size of slice data
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ uint32_t slice_data_size;
+};
+
+/* @} */
+
+#endif /* AVCODEC_VAAPI_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavcodec/vda.h b/dom/media/platforms/ffmpeg/libav53/include/libavcodec/vda.h
new file mode 100644
index 0000000000..2cb51c5f53
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavcodec/vda.h
@@ -0,0 +1,144 @@
+/*
+ * VDA HW acceleration
+ *
+ * copyright (c) 2011 Sebastien Zwickert
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_VDA_H
+#define AVCODEC_VDA_H
+
+#include <pthread.h>
+#include <stdint.h>
+
+// emmintrin.h is unable to compile with -std=c99 -Werror=missing-prototypes
+// http://openradar.appspot.com/8026390
+#undef __GNUC_STDC_INLINE__
+
+#define Picture QuickdrawPicture
+#include <VideoDecodeAcceleration/VDADecoder.h>
+#undef Picture
+
+/**
+ * This structure is used to store a decoded frame information and data.
+ */
+typedef struct vda_frame {
+ /**
+ * The PTS of the frame.
+ *
+ * - encoding: unused
+ * - decoding: Set/Unset by libavcodec.
+ */
+ int64_t pts;
+
+ /**
+ * The CoreVideo buffer that contains the decoded data.
+ *
+ * - encoding: unused
+ * - decoding: Set/Unset by libavcodec.
+ */
+ CVPixelBufferRef cv_buffer;
+
+ /**
+ * A pointer to the next frame.
+ *
+ * - encoding: unused
+ * - decoding: Set/Unset by libavcodec.
+ */
+ struct vda_frame *next_frame;
+} vda_frame;
+
+/**
+ * This structure is used to provide the necessary configurations and data
+ * to the VDA Libav HWAccel implementation.
+ *
+ * The application must make it available as AVCodecContext.hwaccel_context.
+ */
+struct vda_context {
+ /**
+ * VDA decoder object.
+ *
+ * - encoding: unused
+ * - decoding: Set/Unset by libavcodec.
+ */
+ VDADecoder decoder;
+
+ /**
+ * VDA frames queue ordered by presentation timestamp.
+ *
+ * - encoding: unused
+ * - decoding: Set/Unset by libavcodec.
+ */
+ vda_frame *queue;
+
+ /**
+ * Mutex for locking queue operations.
+ *
+ * - encoding: unused
+ * - decoding: Set/Unset by libavcodec.
+ */
+ pthread_mutex_t queue_mutex;
+
+ /**
+ * The frame width.
+ *
+ * - encoding: unused
+ * - decoding: Set/Unset by user.
+ */
+ int width;
+
+ /**
+ * The frame height.
+ *
+ * - encoding: unused
+ * - decoding: Set/Unset by user.
+ */
+ int height;
+
+ /**
+ * The frame format.
+ *
+ * - encoding: unused
+ * - decoding: Set/Unset by user.
+ */
+ int format;
+
+ /**
+ * The pixel format for output image buffers.
+ *
+ * - encoding: unused
+ * - decoding: Set/Unset by user.
+ */
+ OSType cv_pix_fmt_type;
+};
+
+/** Create the video decoder. */
+int ff_vda_create_decoder(struct vda_context *vda_ctx,
+ uint8_t *extradata,
+ int extradata_size);
+
+/** Destroy the video decoder. */
+int ff_vda_destroy_decoder(struct vda_context *vda_ctx);
+
+/** Return the top frame of the queue. */
+vda_frame *ff_vda_queue_pop(struct vda_context *vda_ctx);
+
+/** Release the given frame. */
+void ff_vda_release_vda_frame(vda_frame *frame);
+
+#endif /* AVCODEC_VDA_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavcodec/vdpau.h b/dom/media/platforms/ffmpeg/libav53/include/libavcodec/vdpau.h
new file mode 100644
index 0000000000..6f1386067b
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavcodec/vdpau.h
@@ -0,0 +1,88 @@
+/*
+ * The Video Decode and Presentation API for UNIX (VDPAU) is used for
+ * hardware-accelerated decoding of MPEG-1/2, H.264 and VC-1.
+ *
+ * Copyright (C) 2008 NVIDIA
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_VDPAU_H
+#define AVCODEC_VDPAU_H
+
+/**
+ * @defgroup Decoder VDPAU Decoder and Renderer
+ *
+ * VDPAU hardware acceleration has two modules
+ * - VDPAU decoding
+ * - VDPAU presentation
+ *
+ * The VDPAU decoding module parses all headers using Libav
+ * parsing mechanisms and uses VDPAU for the actual decoding.
+ *
+ * As per the current implementation, the actual decoding
+ * and rendering (API calls) are done as part of the VDPAU
+ * presentation (vo_vdpau.c) module.
+ *
+ * @defgroup VDPAU_Decoding VDPAU Decoding
+ * @ingroup Decoder
+ * @{
+ */
+
+#include <vdpau/vdpau.h>
+#include <vdpau/vdpau_x11.h>
+
+/** @brief The videoSurface is used for rendering. */
+#define FF_VDPAU_STATE_USED_FOR_RENDER 1
+
+/**
+ * @brief The videoSurface is needed for reference/prediction.
+ * The codec manipulates this.
+ */
+#define FF_VDPAU_STATE_USED_FOR_REFERENCE 2
+
+/**
+ * @brief This structure is used as a callback between the Libav
+ * decoder (vd_) and presentation (vo_) module.
+ * This is used for defining a video frame containing surface,
+ * picture parameter, bitstream information etc which are passed
+ * between the Libav decoder and its clients.
+ */
+struct vdpau_render_state {
+ VdpVideoSurface surface; ///< Used as rendered surface, never changed.
+
+ int state; ///< Holds FF_VDPAU_STATE_* values.
+
+ /** picture parameter information for all supported codecs */
+ union VdpPictureInfo {
+ VdpPictureInfoH264 h264;
+ VdpPictureInfoMPEG1Or2 mpeg;
+ VdpPictureInfoVC1 vc1;
+ VdpPictureInfoMPEG4Part2 mpeg4;
+ } info;
+
+ /** Describe size/location of the compressed video data.
+ Set to 0 when freeing bitstream_buffers. */
+ int bitstream_buffers_allocated;
+ int bitstream_buffers_used;
+ /** The user is responsible for freeing this buffer using av_freep(). */
+ VdpBitstreamBuffer *bitstream_buffers;
+};
+
+/* @}*/
+
+#endif /* AVCODEC_VDPAU_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavcodec/version.h b/dom/media/platforms/ffmpeg/libav53/include/libavcodec/version.h
new file mode 100644
index 0000000000..77e16823f9
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavcodec/version.h
@@ -0,0 +1,126 @@
+/*
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_VERSION_H
+#define AVCODEC_VERSION_H
+
+#define LIBAVCODEC_VERSION_MAJOR 53
+#define LIBAVCODEC_VERSION_MINOR 35
+#define LIBAVCODEC_VERSION_MICRO 0
+
+#define LIBAVCODEC_VERSION_INT AV_VERSION_INT(LIBAVCODEC_VERSION_MAJOR, \
+ LIBAVCODEC_VERSION_MINOR, \
+ LIBAVCODEC_VERSION_MICRO)
+#define LIBAVCODEC_VERSION AV_VERSION(LIBAVCODEC_VERSION_MAJOR, \
+ LIBAVCODEC_VERSION_MINOR, \
+ LIBAVCODEC_VERSION_MICRO)
+#define LIBAVCODEC_BUILD LIBAVCODEC_VERSION_INT
+
+#define LIBAVCODEC_IDENT "Lavc" AV_STRINGIFY(LIBAVCODEC_VERSION)
+
+/**
+ * Those FF_API_* defines are not part of public API.
+ * They may change, break or disappear at any time.
+ */
+#ifndef FF_API_PALETTE_CONTROL
+#define FF_API_PALETTE_CONTROL (LIBAVCODEC_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_OLD_SAMPLE_FMT
+#define FF_API_OLD_SAMPLE_FMT (LIBAVCODEC_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_OLD_AUDIOCONVERT
+#define FF_API_OLD_AUDIOCONVERT (LIBAVCODEC_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_ANTIALIAS_ALGO
+#define FF_API_ANTIALIAS_ALGO (LIBAVCODEC_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_REQUEST_CHANNELS
+#define FF_API_REQUEST_CHANNELS (LIBAVCODEC_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_OPT_H
+#define FF_API_OPT_H (LIBAVCODEC_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_THREAD_INIT
+#define FF_API_THREAD_INIT (LIBAVCODEC_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_OLD_FF_PICT_TYPES
+#define FF_API_OLD_FF_PICT_TYPES (LIBAVCODEC_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_FLAC_GLOBAL_OPTS
+#define FF_API_FLAC_GLOBAL_OPTS (LIBAVCODEC_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_GET_PIX_FMT_NAME
+#define FF_API_GET_PIX_FMT_NAME (LIBAVCODEC_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_ALLOC_CONTEXT
+#define FF_API_ALLOC_CONTEXT (LIBAVCODEC_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_AVCODEC_OPEN
+#define FF_API_AVCODEC_OPEN (LIBAVCODEC_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_DRC_SCALE
+#define FF_API_DRC_SCALE (LIBAVCODEC_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_ER
+#define FF_API_ER (LIBAVCODEC_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_AVCODEC_INIT
+#define FF_API_AVCODEC_INIT (LIBAVCODEC_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_X264_GLOBAL_OPTS
+#define FF_API_X264_GLOBAL_OPTS (LIBAVCODEC_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_MPEGVIDEO_GLOBAL_OPTS
+#define FF_API_MPEGVIDEO_GLOBAL_OPTS (LIBAVCODEC_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_LAME_GLOBAL_OPTS
+#define FF_API_LAME_GLOBAL_OPTS (LIBAVCODEC_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_SNOW_GLOBAL_OPTS
+#define FF_API_SNOW_GLOBAL_OPTS (LIBAVCODEC_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_MJPEG_GLOBAL_OPTS
+#define FF_API_MJPEG_GLOBAL_OPTS (LIBAVCODEC_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_GET_ALPHA_INFO
+#define FF_API_GET_ALPHA_INFO (LIBAVCODEC_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_PARSE_FRAME
+#define FF_API_PARSE_FRAME (LIBAVCODEC_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_INTERNAL_CONTEXT
+#define FF_API_INTERNAL_CONTEXT (LIBAVCODEC_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_TIFFENC_COMPLEVEL
+#define FF_API_TIFFENC_COMPLEVEL (LIBAVCODEC_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_DATA_POINTERS
+#define FF_API_DATA_POINTERS (LIBAVCODEC_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_OLD_DECODE_AUDIO
+#define FF_API_OLD_DECODE_AUDIO (LIBAVCODEC_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_AVFRAME_AGE
+#define FF_API_AVFRAME_AGE (LIBAVCODEC_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_OLD_ENCODE_AUDIO
+#define FF_API_OLD_ENCODE_AUDIO (LIBAVCODEC_VERSION_MAJOR < 54)
+#endif
+
+#endif /* AVCODEC_VERSION_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavcodec/xvmc.h b/dom/media/platforms/ffmpeg/libav53/include/libavcodec/xvmc.h
new file mode 100644
index 0000000000..1239015fcd
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavcodec/xvmc.h
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2003 Ivan Kalvachev
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_XVMC_H
+#define AVCODEC_XVMC_H
+
+#include <X11/extensions/XvMC.h>
+
+#include "avcodec.h"
+
+#define AV_XVMC_ID 0x1DC711C0 /**< special value to ensure that regular pixel routines haven't corrupted the struct
+ the number is 1337 speak for the letters IDCT MCo (motion compensation) */
+
+struct xvmc_pix_fmt {
+ /** The field contains the special constant value AV_XVMC_ID.
+ It is used as a test that the application correctly uses the API,
+ and that there is no corruption caused by pixel routines.
+ - application - set during initialization
+ - libavcodec - unchanged
+ */
+ int xvmc_id;
+
+ /** Pointer to the block array allocated by XvMCCreateBlocks().
+ The array has to be freed by XvMCDestroyBlocks().
+ Each group of 64 values represents one data block of differential
+ pixel information (in MoCo mode) or coefficients for IDCT.
+ - application - set the pointer during initialization
+ - libavcodec - fills coefficients/pixel data into the array
+ */
+ short* data_blocks;
+
+ /** Pointer to the macroblock description array allocated by
+ XvMCCreateMacroBlocks() and freed by XvMCDestroyMacroBlocks().
+ - application - set the pointer during initialization
+ - libavcodec - fills description data into the array
+ */
+ XvMCMacroBlock* mv_blocks;
+
+ /** Number of macroblock descriptions that can be stored in the mv_blocks
+ array.
+ - application - set during initialization
+ - libavcodec - unchanged
+ */
+ int allocated_mv_blocks;
+
+ /** Number of blocks that can be stored at once in the data_blocks array.
+ - application - set during initialization
+ - libavcodec - unchanged
+ */
+ int allocated_data_blocks;
+
+ /** Indicate that the hardware would interpret data_blocks as IDCT
+ coefficients and perform IDCT on them.
+ - application - set during initialization
+ - libavcodec - unchanged
+ */
+ int idct;
+
+ /** In MoCo mode it indicates that intra macroblocks are assumed to be in
+ unsigned format; same as the XVMC_INTRA_UNSIGNED flag.
+ - application - set during initialization
+ - libavcodec - unchanged
+ */
+ int unsigned_intra;
+
+ /** Pointer to the surface allocated by XvMCCreateSurface().
+ It has to be freed by XvMCDestroySurface() on application exit.
+ It identifies the frame and its state on the video hardware.
+ - application - set during initialization
+ - libavcodec - unchanged
+ */
+ XvMCSurface* p_surface;
+
+/** Set by the decoder before calling ff_draw_horiz_band(),
+ needed by the XvMCRenderSurface function. */
+//@{
+ /** Pointer to the surface used as past reference
+ - application - unchanged
+ - libavcodec - set
+ */
+ XvMCSurface* p_past_surface;
+
+ /** Pointer to the surface used as future reference
+ - application - unchanged
+ - libavcodec - set
+ */
+ XvMCSurface* p_future_surface;
+
+ /** top/bottom field or frame
+ - application - unchanged
+ - libavcodec - set
+ */
+ unsigned int picture_structure;
+
+ /** XVMC_SECOND_FIELD - 1st or 2nd field in the sequence
+ - application - unchanged
+ - libavcodec - set
+ */
+ unsigned int flags;
+//}@
+
+ /** Number of macroblock descriptions in the mv_blocks array
+ that have already been passed to the hardware.
+ - application - zeroes it on get_buffer().
+ A successful ff_draw_horiz_band() may increment it
+ with filled_mb_block_num or zero both.
+ - libavcodec - unchanged
+ */
+ int start_mv_blocks_num;
+
+ /** Number of new macroblock descriptions in the mv_blocks array (after
+ start_mv_blocks_num) that are filled by libavcodec and have to be
+ passed to the hardware.
+ - application - zeroes it on get_buffer() or after successful
+ ff_draw_horiz_band().
+ - libavcodec - increment with one of each stored MB
+ */
+ int filled_mv_blocks_num;
+
+ /** Number of the the next free data block; one data block consists of
+ 64 short values in the data_blocks array.
+ All blocks before this one have already been claimed by placing their
+ position into the corresponding block description structure field,
+ that are part of the mv_blocks array.
+ - application - zeroes it on get_buffer().
+ A successful ff_draw_horiz_band() may zero it together
+ with start_mb_blocks_num.
+ - libavcodec - each decoded macroblock increases it by the number
+ of coded blocks it contains.
+ */
+ int next_free_data_block_num;
+};
+
+#endif /* AVCODEC_XVMC_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/adler32.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/adler32.h
new file mode 100644
index 0000000000..a8ff6f9d41
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/adler32.h
@@ -0,0 +1,43 @@
+/*
+ * copyright (c) 2006 Mans Rullgard
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_ADLER32_H
+#define AVUTIL_ADLER32_H
+
+#include <stdint.h>
+#include "attributes.h"
+
+/**
+ * @ingroup lavu_crypto
+ * Calculate the Adler32 checksum of a buffer.
+ *
+ * Passing the return value to a subsequent av_adler32_update() call
+ * allows the checksum of multiple buffers to be calculated as though
+ * they were concatenated.
+ *
+ * @param adler initial checksum value
+ * @param buf pointer to input buffer
+ * @param len size of input buffer
+ * @return updated checksum
+ */
+unsigned long av_adler32_update(unsigned long adler, const uint8_t *buf,
+ unsigned int len) av_pure;
+
+#endif /* AVUTIL_ADLER32_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/aes.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/aes.h
new file mode 100644
index 0000000000..cf7b462092
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/aes.h
@@ -0,0 +1,57 @@
+/*
+ * copyright (c) 2007 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_AES_H
+#define AVUTIL_AES_H
+
+#include <stdint.h>
+
+/**
+ * @defgroup lavu_aes AES
+ * @ingroup lavu_crypto
+ * @{
+ */
+
+extern const int av_aes_size;
+
+struct AVAES;
+
+/**
+ * Initialize an AVAES context.
+ * @param key_bits 128, 192 or 256
+ * @param decrypt 0 for encryption, 1 for decryption
+ */
+int av_aes_init(struct AVAES *a, const uint8_t *key, int key_bits, int decrypt);
+
+/**
+ * Encrypt or decrypt a buffer using a previously initialized context.
+ * @param count number of 16 byte blocks
+ * @param dst destination array, can be equal to src
+ * @param src source array, can be equal to dst
+ * @param iv initialization vector for CBC mode, if NULL then ECB will be used
+ * @param decrypt 0 for encryption, 1 for decryption
+ */
+void av_aes_crypt(struct AVAES *a, uint8_t *dst, const uint8_t *src, int count, uint8_t *iv, int decrypt);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_AES_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/attributes.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/attributes.h
new file mode 100644
index 0000000000..ef990a1d4f
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/attributes.h
@@ -0,0 +1,136 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * Macro definitions for various function/variable attributes
+ */
+
+#ifndef AVUTIL_ATTRIBUTES_H
+#define AVUTIL_ATTRIBUTES_H
+
+#ifdef __GNUC__
+# define AV_GCC_VERSION_AT_LEAST(x,y) (__GNUC__ > x || __GNUC__ == x && __GNUC_MINOR__ >= y)
+#else
+# define AV_GCC_VERSION_AT_LEAST(x,y) 0
+#endif
+
+#ifndef av_always_inline
+#if AV_GCC_VERSION_AT_LEAST(3,1)
+# define av_always_inline __attribute__((always_inline)) inline
+#else
+# define av_always_inline inline
+#endif
+#endif
+
+#ifndef av_noinline
+#if AV_GCC_VERSION_AT_LEAST(3,1)
+# define av_noinline __attribute__((noinline))
+#else
+# define av_noinline
+#endif
+#endif
+
+#ifndef av_pure
+#if AV_GCC_VERSION_AT_LEAST(3,1)
+# define av_pure __attribute__((pure))
+#else
+# define av_pure
+#endif
+#endif
+
+#ifndef av_const
+#if AV_GCC_VERSION_AT_LEAST(2,6)
+# define av_const __attribute__((const))
+#else
+# define av_const
+#endif
+#endif
+
+#ifndef av_cold
+#if AV_GCC_VERSION_AT_LEAST(4,3)
+# define av_cold __attribute__((cold))
+#else
+# define av_cold
+#endif
+#endif
+
+#ifndef av_flatten
+#if AV_GCC_VERSION_AT_LEAST(4,1)
+# define av_flatten __attribute__((flatten))
+#else
+# define av_flatten
+#endif
+#endif
+
+#ifndef attribute_deprecated
+#if AV_GCC_VERSION_AT_LEAST(3,1)
+# define attribute_deprecated __attribute__((deprecated))
+#else
+# define attribute_deprecated
+#endif
+#endif
+
+#ifndef av_unused
+#if defined(__GNUC__)
+# define av_unused __attribute__((unused))
+#else
+# define av_unused
+#endif
+#endif
+
+/**
+ * Mark a variable as used and prevent the compiler from optimizing it
+ * away. This is useful for variables accessed only from inline
+ * assembler without the compiler being aware.
+ */
+#ifndef av_used
+#if AV_GCC_VERSION_AT_LEAST(3,1)
+# define av_used __attribute__((used))
+#else
+# define av_used
+#endif
+#endif
+
+#ifndef av_alias
+#if AV_GCC_VERSION_AT_LEAST(3,3)
+# define av_alias __attribute__((may_alias))
+#else
+# define av_alias
+#endif
+#endif
+
+#ifndef av_uninit
+#if defined(__GNUC__) && !defined(__ICC)
+# define av_uninit(x) x=x
+#else
+# define av_uninit(x) x
+#endif
+#endif
+
+#ifdef __GNUC__
+# define av_builtin_constant_p __builtin_constant_p
+# define av_printf_format(fmtpos, attrpos) __attribute__((__format__(__printf__, fmtpos, attrpos)))
+#else
+# define av_builtin_constant_p(x) 0
+# define av_printf_format(fmtpos, attrpos)
+#endif
+
+#endif /* AVUTIL_ATTRIBUTES_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/audio_fifo.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/audio_fifo.h
new file mode 100644
index 0000000000..8c76388255
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/audio_fifo.h
@@ -0,0 +1,146 @@
+/*
+ * Audio FIFO
+ * Copyright (c) 2012 Justin Ruggles <justin.ruggles@gmail.com>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * Audio FIFO Buffer
+ */
+
+#ifndef AVUTIL_AUDIO_FIFO_H
+#define AVUTIL_AUDIO_FIFO_H
+
+#include "avutil.h"
+#include "fifo.h"
+#include "samplefmt.h"
+
+/**
+ * @addtogroup lavu_audio
+ * @{
+ */
+
+/**
+ * Context for an Audio FIFO Buffer.
+ *
+ * - Operates at the sample level rather than the byte level.
+ * - Supports multiple channels with either planar or packed sample format.
+ * - Automatic reallocation when writing to a full buffer.
+ */
+typedef struct AVAudioFifo AVAudioFifo;
+
+/**
+ * Free an AVAudioFifo.
+ *
+ * @param af AVAudioFifo to free
+ */
+void av_audio_fifo_free(AVAudioFifo *af);
+
+/**
+ * Allocate an AVAudioFifo.
+ *
+ * @param sample_fmt sample format
+ * @param channels number of channels
+ * @param nb_samples initial allocation size, in samples
+ * @return newly allocated AVAudioFifo, or NULL on error
+ */
+AVAudioFifo *av_audio_fifo_alloc(enum AVSampleFormat sample_fmt, int channels,
+ int nb_samples);
+
+/**
+ * Reallocate an AVAudioFifo.
+ *
+ * @param af AVAudioFifo to reallocate
+ * @param nb_samples new allocation size, in samples
+ * @return 0 if OK, or negative AVERROR code on failure
+ */
+int av_audio_fifo_realloc(AVAudioFifo *af, int nb_samples);
+
+/**
+ * Write data to an AVAudioFifo.
+ *
+ * The AVAudioFifo will be reallocated automatically if the available space
+ * is less than nb_samples.
+ *
+ * @see enum AVSampleFormat
+ * The documentation for AVSampleFormat describes the data layout.
+ *
+ * @param af AVAudioFifo to write to
+ * @param data audio data plane pointers
+ * @param nb_samples number of samples to write
+ * @return number of samples actually written, or negative AVERROR
+ * code on failure.
+ */
+int av_audio_fifo_write(AVAudioFifo *af, void **data, int nb_samples);
+
+/**
+ * Read data from an AVAudioFifo.
+ *
+ * @see enum AVSampleFormat
+ * The documentation for AVSampleFormat describes the data layout.
+ *
+ * @param af AVAudioFifo to read from
+ * @param data audio data plane pointers
+ * @param nb_samples number of samples to read
+ * @return number of samples actually read, or negative AVERROR code
+ * on failure.
+ */
+int av_audio_fifo_read(AVAudioFifo *af, void **data, int nb_samples);
+
+/**
+ * Drain data from an AVAudioFifo.
+ *
+ * Removes the data without reading it.
+ *
+ * @param af AVAudioFifo to drain
+ * @param nb_samples number of samples to drain
+ * @return 0 if OK, or negative AVERROR code on failure
+ */
+int av_audio_fifo_drain(AVAudioFifo *af, int nb_samples);
+
+/**
+ * Reset the AVAudioFifo buffer.
+ *
+ * This empties all data in the buffer.
+ *
+ * @param af AVAudioFifo to reset
+ */
+void av_audio_fifo_reset(AVAudioFifo *af);
+
+/**
+ * Get the current number of samples in the AVAudioFifo available for reading.
+ *
+ * @param af the AVAudioFifo to query
+ * @return number of samples available for reading
+ */
+int av_audio_fifo_size(AVAudioFifo *af);
+
+/**
+ * Get the current number of samples in the AVAudioFifo available for writing.
+ *
+ * @param af the AVAudioFifo to query
+ * @return number of samples available for writing
+ */
+int av_audio_fifo_space(AVAudioFifo *af);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_AUDIO_FIFO_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/audioconvert.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/audioconvert.h
new file mode 100644
index 0000000000..00ed0ff7b8
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/audioconvert.h
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ * Copyright (c) 2008 Peter Ross
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_AUDIOCONVERT_H
+#define AVUTIL_AUDIOCONVERT_H
+
+#include <stdint.h>
+
+/**
+ * @file
+ * audio conversion routines
+ */
+
+/**
+ * @addtogroup lavu_audio
+ * @{
+ */
+
+/**
+ * @defgroup channel_masks Audio channel masks
+ * @{
+ */
+#define AV_CH_FRONT_LEFT 0x00000001
+#define AV_CH_FRONT_RIGHT 0x00000002
+#define AV_CH_FRONT_CENTER 0x00000004
+#define AV_CH_LOW_FREQUENCY 0x00000008
+#define AV_CH_BACK_LEFT 0x00000010
+#define AV_CH_BACK_RIGHT 0x00000020
+#define AV_CH_FRONT_LEFT_OF_CENTER 0x00000040
+#define AV_CH_FRONT_RIGHT_OF_CENTER 0x00000080
+#define AV_CH_BACK_CENTER 0x00000100
+#define AV_CH_SIDE_LEFT 0x00000200
+#define AV_CH_SIDE_RIGHT 0x00000400
+#define AV_CH_TOP_CENTER 0x00000800
+#define AV_CH_TOP_FRONT_LEFT 0x00001000
+#define AV_CH_TOP_FRONT_CENTER 0x00002000
+#define AV_CH_TOP_FRONT_RIGHT 0x00004000
+#define AV_CH_TOP_BACK_LEFT 0x00008000
+#define AV_CH_TOP_BACK_CENTER 0x00010000
+#define AV_CH_TOP_BACK_RIGHT 0x00020000
+#define AV_CH_STEREO_LEFT 0x20000000 ///< Stereo downmix.
+#define AV_CH_STEREO_RIGHT 0x40000000 ///< See AV_CH_STEREO_LEFT.
+#define AV_CH_WIDE_LEFT 0x0000000080000000ULL
+#define AV_CH_WIDE_RIGHT 0x0000000100000000ULL
+#define AV_CH_SURROUND_DIRECT_LEFT 0x0000000200000000ULL
+#define AV_CH_SURROUND_DIRECT_RIGHT 0x0000000400000000ULL
+
+/** Channel mask value used for AVCodecContext.request_channel_layout
+ to indicate that the user requests the channel order of the decoder output
+ to be the native codec channel order. */
+#define AV_CH_LAYOUT_NATIVE 0x8000000000000000ULL
+
+/**
+ * @}
+ * @defgroup channel_mask_c Audio channel convenience macros
+ * @{
+ * */
+#define AV_CH_LAYOUT_MONO (AV_CH_FRONT_CENTER)
+#define AV_CH_LAYOUT_STEREO (AV_CH_FRONT_LEFT|AV_CH_FRONT_RIGHT)
+#define AV_CH_LAYOUT_2POINT1 (AV_CH_LAYOUT_STEREO|AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_2_1 (AV_CH_LAYOUT_STEREO|AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_SURROUND (AV_CH_LAYOUT_STEREO|AV_CH_FRONT_CENTER)
+#define AV_CH_LAYOUT_3POINT1 (AV_CH_LAYOUT_SURROUND|AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_4POINT0 (AV_CH_LAYOUT_SURROUND|AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_4POINT1 (AV_CH_LAYOUT_4POINT0|AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_2_2 (AV_CH_LAYOUT_STEREO|AV_CH_SIDE_LEFT|AV_CH_SIDE_RIGHT)
+#define AV_CH_LAYOUT_QUAD (AV_CH_LAYOUT_STEREO|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_5POINT0 (AV_CH_LAYOUT_SURROUND|AV_CH_SIDE_LEFT|AV_CH_SIDE_RIGHT)
+#define AV_CH_LAYOUT_5POINT1 (AV_CH_LAYOUT_5POINT0|AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_5POINT0_BACK (AV_CH_LAYOUT_SURROUND|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_5POINT1_BACK (AV_CH_LAYOUT_5POINT0_BACK|AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_6POINT0 (AV_CH_LAYOUT_5POINT0|AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_6POINT0_FRONT (AV_CH_LAYOUT_2_2|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER)
+#define AV_CH_LAYOUT_HEXAGONAL (AV_CH_LAYOUT_5POINT0_BACK|AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_6POINT1 (AV_CH_LAYOUT_5POINT1|AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_6POINT1_BACK (AV_CH_LAYOUT_5POINT1_BACK|AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_6POINT1_FRONT (AV_CH_LAYOUT_6POINT0_FRONT|AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_7POINT0 (AV_CH_LAYOUT_5POINT0|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_7POINT0_FRONT (AV_CH_LAYOUT_5POINT0|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER)
+#define AV_CH_LAYOUT_7POINT1 (AV_CH_LAYOUT_5POINT1|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_7POINT1_WIDE (AV_CH_LAYOUT_5POINT1|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER)
+#define AV_CH_LAYOUT_OCTAGONAL (AV_CH_LAYOUT_5POINT0|AV_CH_BACK_LEFT|AV_CH_BACK_CENTER|AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_STEREO_DOWNMIX (AV_CH_STEREO_LEFT|AV_CH_STEREO_RIGHT)
+
+/**
+ * @}
+ */
+
+/**
+ * Return a channel layout id that matches name, 0 if no match.
+ */
+uint64_t av_get_channel_layout(const char *name);
+
+/**
+ * Return a description of a channel layout.
+ * If nb_channels is <= 0, it is guessed from the channel_layout.
+ *
+ * @param buf put here the string containing the channel layout
+ * @param buf_size size in bytes of the buffer
+ */
+void av_get_channel_layout_string(char *buf, int buf_size, int nb_channels, uint64_t channel_layout);
+
+/**
+ * Return the number of channels in the channel layout.
+ */
+int av_get_channel_layout_nb_channels(uint64_t channel_layout);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_AUDIOCONVERT_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/avassert.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/avassert.h
new file mode 100644
index 0000000000..b223d26e8d
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/avassert.h
@@ -0,0 +1,66 @@
+/*
+ * copyright (c) 2010 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * simple assert() macros that are a bit more flexible than ISO C assert().
+ * @author Michael Niedermayer <michaelni@gmx.at>
+ */
+
+#ifndef AVUTIL_AVASSERT_H
+#define AVUTIL_AVASSERT_H
+
+#include <stdlib.h>
+#include "avutil.h"
+#include "log.h"
+
+/**
+ * assert() equivalent, that is always enabled.
+ */
+#define av_assert0(cond) do { \
+ if (!(cond)) { \
+ av_log(NULL, AV_LOG_FATAL, "Assertion %s failed at %s:%d\n", \
+ AV_STRINGIFY(cond), __FILE__, __LINE__); \
+ abort(); \
+ } \
+} while (0)
+
+
+/**
+ * assert() equivalent, that does not lie in speed critical code.
+ * These asserts() thus can be enabled without fearing speedloss.
+ */
+#if defined(ASSERT_LEVEL) && ASSERT_LEVEL > 0
+#define av_assert1(cond) av_assert0(cond)
+#else
+#define av_assert1(cond) ((void)0)
+#endif
+
+
+/**
+ * assert() equivalent, that does lie in speed critical code.
+ */
+#if defined(ASSERT_LEVEL) && ASSERT_LEVEL > 1
+#define av_assert2(cond) av_assert0(cond)
+#else
+#define av_assert2(cond) ((void)0)
+#endif
+
+#endif /* AVUTIL_AVASSERT_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/avconfig.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/avconfig.h
new file mode 100644
index 0000000000..f10aa6186b
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/avconfig.h
@@ -0,0 +1,6 @@
+/* Generated by ffconf */
+#ifndef AVUTIL_AVCONFIG_H
+#define AVUTIL_AVCONFIG_H
+#define AV_HAVE_BIGENDIAN 0
+#define AV_HAVE_FAST_UNALIGNED 1
+#endif /* AVUTIL_AVCONFIG_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/avstring.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/avstring.h
new file mode 100644
index 0000000000..ed4e465cbc
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/avstring.h
@@ -0,0 +1,175 @@
+/*
+ * Copyright (c) 2007 Mans Rullgard
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_AVSTRING_H
+#define AVUTIL_AVSTRING_H
+
+#include <stddef.h>
+#include "attributes.h"
+
+/**
+ * @addtogroup lavu_string
+ * @{
+ */
+
+/**
+ * Return non-zero if pfx is a prefix of str. If it is, *ptr is set to
+ * the address of the first character in str after the prefix.
+ *
+ * @param str input string
+ * @param pfx prefix to test
+ * @param ptr updated if the prefix is matched inside str
+ * @return non-zero if the prefix matches, zero otherwise
+ */
+int av_strstart(const char *str, const char *pfx, const char **ptr);
+
+/**
+ * Return non-zero if pfx is a prefix of str independent of case. If
+ * it is, *ptr is set to the address of the first character in str
+ * after the prefix.
+ *
+ * @param str input string
+ * @param pfx prefix to test
+ * @param ptr updated if the prefix is matched inside str
+ * @return non-zero if the prefix matches, zero otherwise
+ */
+int av_stristart(const char *str, const char *pfx, const char **ptr);
+
+/**
+ * Locate the first case-independent occurrence in the string haystack
+ * of the string needle. A zero-length string needle is considered to
+ * match at the start of haystack.
+ *
+ * This function is a case-insensitive version of the standard strstr().
+ *
+ * @param haystack string to search in
+ * @param needle string to search for
+ * @return pointer to the located match within haystack
+ * or a null pointer if no match
+ */
+char *av_stristr(const char *haystack, const char *needle);
+
+/**
+ * Copy the string src to dst, but no more than size - 1 bytes, and
+ * null-terminate dst.
+ *
+ * This function is the same as BSD strlcpy().
+ *
+ * @param dst destination buffer
+ * @param src source string
+ * @param size size of destination buffer
+ * @return the length of src
+ *
+ * @warning since the return value is the length of src, src absolutely
+ * _must_ be a properly 0-terminated string, otherwise this will read beyond
+ * the end of the buffer and possibly crash.
+ */
+size_t av_strlcpy(char *dst, const char *src, size_t size);
+
+/**
+ * Append the string src to the string dst, but to a total length of
+ * no more than size - 1 bytes, and null-terminate dst.
+ *
+ * This function is similar to BSD strlcat(), but differs when
+ * size <= strlen(dst).
+ *
+ * @param dst destination buffer
+ * @param src source string
+ * @param size size of destination buffer
+ * @return the total length of src and dst
+ *
+ * @warning since the return value use the length of src and dst, these
+ * absolutely _must_ be a properly 0-terminated strings, otherwise this
+ * will read beyond the end of the buffer and possibly crash.
+ */
+size_t av_strlcat(char *dst, const char *src, size_t size);
+
+/**
+ * Append output to a string, according to a format. Never write out of
+ * the destination buffer, and always put a terminating 0 within
+ * the buffer.
+ * @param dst destination buffer (string to which the output is
+ * appended)
+ * @param size total size of the destination buffer
+ * @param fmt printf-compatible format string, specifying how the
+ * following parameters are used
+ * @return the length of the string that would have been generated
+ * if enough space had been available
+ */
+size_t av_strlcatf(char *dst, size_t size, const char *fmt, ...) av_printf_format(3, 4);
+
+/**
+ * Convert a number to a av_malloced string.
+ */
+char *av_d2str(double d);
+
+/**
+ * Unescape the given string until a non escaped terminating char,
+ * and return the token corresponding to the unescaped string.
+ *
+ * The normal \ and ' escaping is supported. Leading and trailing
+ * whitespaces are removed, unless they are escaped with '\' or are
+ * enclosed between ''.
+ *
+ * @param buf the buffer to parse, buf will be updated to point to the
+ * terminating char
+ * @param term a 0-terminated list of terminating chars
+ * @return the malloced unescaped string, which must be av_freed by
+ * the user, NULL in case of allocation failure
+ */
+char *av_get_token(const char **buf, const char *term);
+
+/**
+ * Locale-independent conversion of ASCII characters to uppercase.
+ */
+static inline int av_toupper(int c)
+{
+ if (c >= 'a' && c <= 'z')
+ c ^= 0x20;
+ return c;
+}
+
+/**
+ * Locale-independent conversion of ASCII characters to lowercase.
+ */
+static inline int av_tolower(int c)
+{
+ if (c >= 'A' && c <= 'Z')
+ c ^= 0x20;
+ return c;
+}
+
+/*
+ * Locale-independent case-insensitive compare.
+ * @note This means only ASCII-range characters are case-insensitive
+ */
+int av_strcasecmp(const char *a, const char *b);
+
+/**
+ * Locale-independent case-insensitive compare.
+ * @note This means only ASCII-range characters are case-insensitive
+ */
+int av_strncasecmp(const char *a, const char *b, size_t n);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_AVSTRING_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/avutil.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/avutil.h
new file mode 100644
index 0000000000..05e9248375
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/avutil.h
@@ -0,0 +1,326 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_AVUTIL_H
+#define AVUTIL_AVUTIL_H
+
+/**
+ * @file
+ * external API header
+ */
+
+/**
+ * @mainpage
+ *
+ * @section libav_intro Introduction
+ *
+ * This document describe the usage of the different libraries
+ * provided by Libav.
+ *
+ * @li @ref libavc "libavcodec" encoding/decoding library
+ * @li @subpage libavfilter graph based frame editing library
+ * @li @ref libavf "libavformat" I/O and muxing/demuxing library
+ * @li @ref lavd "libavdevice" special devices muxing/demuxing library
+ * @li @ref lavu "libavutil" common utility library
+ * @li @subpage libpostproc post processing library
+ * @li @subpage libswscale color conversion and scaling library
+ *
+ */
+
+/**
+ * @defgroup lavu Common utility functions
+ *
+ * @brief
+ * libavutil contains the code shared across all the other Libav
+ * libraries
+ *
+ * @note In order to use the functions provided by avutil you must include
+ * the specific header.
+ *
+ * @{
+ *
+ * @defgroup lavu_crypto Crypto and Hashing
+ *
+ * @{
+ * @}
+ *
+ * @defgroup lavu_math Maths
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_string String Manipulation
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_mem Memory Management
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_data Data Structures
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_audio Audio related
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_error Error Codes
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_misc Other
+ *
+ * @{
+ *
+ * @defgroup lavu_internal Internal
+ *
+ * Not exported functions, for internal usage only
+ *
+ * @{
+ *
+ * @}
+ */
+
+
+/**
+ * @defgroup preproc_misc Preprocessor String Macros
+ *
+ * String manipulation macros
+ *
+ * @{
+ */
+
+#define AV_STRINGIFY(s) AV_TOSTRING(s)
+#define AV_TOSTRING(s) #s
+
+#define AV_GLUE(a, b) a ## b
+#define AV_JOIN(a, b) AV_GLUE(a, b)
+
+#define AV_PRAGMA(s) _Pragma(#s)
+
+/**
+ * @}
+ */
+
+/**
+ * @defgroup version_utils Library Version Macros
+ *
+ * Useful to check and match library version in order to maintain
+ * backward compatibility.
+ *
+ * @{
+ */
+
+#define AV_VERSION_INT(a, b, c) (a<<16 | b<<8 | c)
+#define AV_VERSION_DOT(a, b, c) a ##.## b ##.## c
+#define AV_VERSION(a, b, c) AV_VERSION_DOT(a, b, c)
+
+/**
+ * @}
+ *
+ * @defgroup lavu_ver Version and Build diagnostics
+ *
+ * Macros and function useful to check at compiletime and at runtime
+ * which version of libavutil is in use.
+ *
+ * @{
+ */
+
+#define LIBAVUTIL_VERSION_MAJOR 51
+#define LIBAVUTIL_VERSION_MINOR 22
+#define LIBAVUTIL_VERSION_MICRO 1
+
+#define LIBAVUTIL_VERSION_INT AV_VERSION_INT(LIBAVUTIL_VERSION_MAJOR, \
+ LIBAVUTIL_VERSION_MINOR, \
+ LIBAVUTIL_VERSION_MICRO)
+#define LIBAVUTIL_VERSION AV_VERSION(LIBAVUTIL_VERSION_MAJOR, \
+ LIBAVUTIL_VERSION_MINOR, \
+ LIBAVUTIL_VERSION_MICRO)
+#define LIBAVUTIL_BUILD LIBAVUTIL_VERSION_INT
+
+#define LIBAVUTIL_IDENT "Lavu" AV_STRINGIFY(LIBAVUTIL_VERSION)
+
+/**
+ * @}
+ *
+ * @defgroup depr_guards Deprecation guards
+ * Those FF_API_* defines are not part of public API.
+ * They may change, break or disappear at any time.
+ *
+ * They are used mostly internally to mark code that will be removed
+ * on the next major version.
+ *
+ * @{
+ */
+#ifndef FF_API_GET_BITS_PER_SAMPLE_FMT
+#define FF_API_GET_BITS_PER_SAMPLE_FMT (LIBAVUTIL_VERSION_MAJOR < 52)
+#endif
+#ifndef FF_API_FIND_OPT
+#define FF_API_FIND_OPT (LIBAVUTIL_VERSION_MAJOR < 52)
+#endif
+#ifndef FF_API_AV_FIFO_PEEK
+#define FF_API_AV_FIFO_PEEK (LIBAVUTIL_VERSION_MAJOR < 52)
+#endif
+#ifndef FF_API_OLD_AVOPTIONS
+#define FF_API_OLD_AVOPTIONS (LIBAVUTIL_VERSION_MAJOR < 52)
+#endif
+
+/**
+ * @}
+ */
+
+/**
+ * @addtogroup lavu_ver
+ * @{
+ */
+
+/**
+ * Return the LIBAVUTIL_VERSION_INT constant.
+ */
+unsigned avutil_version(void);
+
+/**
+ * Return the libavutil build-time configuration.
+ */
+const char *avutil_configuration(void);
+
+/**
+ * Return the libavutil license.
+ */
+const char *avutil_license(void);
+
+/**
+ * @}
+ */
+
+/**
+ * @addtogroup lavu_media Media Type
+ * @brief Media Type
+ */
+
+enum AVMediaType {
+ AVMEDIA_TYPE_UNKNOWN = -1, ///< Usually treated as AVMEDIA_TYPE_DATA
+ AVMEDIA_TYPE_VIDEO,
+ AVMEDIA_TYPE_AUDIO,
+ AVMEDIA_TYPE_DATA, ///< Opaque data information usually continuous
+ AVMEDIA_TYPE_SUBTITLE,
+ AVMEDIA_TYPE_ATTACHMENT, ///< Opaque data information usually sparse
+ AVMEDIA_TYPE_NB
+};
+
+/**
+ * @defgroup lavu_const Constants
+ * @{
+ *
+ * @defgroup lavu_enc Encoding specific
+ *
+ * @note those definition should move to avcodec
+ * @{
+ */
+
+#define FF_LAMBDA_SHIFT 7
+#define FF_LAMBDA_SCALE (1<<FF_LAMBDA_SHIFT)
+#define FF_QP2LAMBDA 118 ///< factor to convert from H.263 QP to lambda
+#define FF_LAMBDA_MAX (256*128-1)
+
+#define FF_QUALITY_SCALE FF_LAMBDA_SCALE //FIXME maybe remove
+
+/**
+ * @}
+ * @defgroup lavu_time Timestamp specific
+ *
+ * Libav internal timebase and timestamp definitions
+ *
+ * @{
+ */
+
+/**
+ * @brief Undefined timestamp value
+ *
+ * Usually reported by demuxer that work on containers that do not provide
+ * either pts or dts.
+ */
+
+#define AV_NOPTS_VALUE INT64_C(0x8000000000000000)
+
+/**
+ * Internal time base represented as integer
+ */
+
+#define AV_TIME_BASE 1000000
+
+/**
+ * Internal time base represented as fractional value
+ */
+
+#define AV_TIME_BASE_Q (AVRational){1, AV_TIME_BASE}
+
+/**
+ * @}
+ * @}
+ * @defgroup lavu_picture Image related
+ *
+ * AVPicture types, pixel formats and basic image planes manipulation.
+ *
+ * @{
+ */
+
+enum AVPictureType {
+ AV_PICTURE_TYPE_I = 1, ///< Intra
+ AV_PICTURE_TYPE_P, ///< Predicted
+ AV_PICTURE_TYPE_B, ///< Bi-dir predicted
+ AV_PICTURE_TYPE_S, ///< S(GMC)-VOP MPEG4
+ AV_PICTURE_TYPE_SI, ///< Switching Intra
+ AV_PICTURE_TYPE_SP, ///< Switching Predicted
+ AV_PICTURE_TYPE_BI, ///< BI type
+};
+
+/**
+ * Return a single letter to describe the given picture type
+ * pict_type.
+ *
+ * @param[in] pict_type the picture type @return a single character
+ * representing the picture type, '?' if pict_type is unknown
+ */
+char av_get_picture_type_char(enum AVPictureType pict_type);
+
+/**
+ * @}
+ */
+
+#include "common.h"
+#include "error.h"
+
+/**
+ * @}
+ * @}
+ */
+
+#endif /* AVUTIL_AVUTIL_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/base64.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/base64.h
new file mode 100644
index 0000000000..4750cf5c72
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/base64.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2006 Ryan Martell. (rdm4@martellventures.com)
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_BASE64_H
+#define AVUTIL_BASE64_H
+
+#include <stdint.h>
+
+/**
+ * @defgroup lavu_base64 Base64
+ * @ingroup lavu_crypto
+ * @{
+ */
+
+
+/**
+ * Decode a base64-encoded string.
+ *
+ * @param out buffer for decoded data
+ * @param in null-terminated input string
+ * @param out_size size in bytes of the out buffer, must be at
+ * least 3/4 of the length of in
+ * @return number of bytes written, or a negative value in case of
+ * invalid input
+ */
+int av_base64_decode(uint8_t *out, const char *in, int out_size);
+
+/**
+ * Encode data to base64 and null-terminate.
+ *
+ * @param out buffer for encoded data
+ * @param out_size size in bytes of the output buffer, must be at
+ * least AV_BASE64_SIZE(in_size)
+ * @param in_size size in bytes of the 'in' buffer
+ * @return 'out' or NULL in case of error
+ */
+char *av_base64_encode(char *out, int out_size, const uint8_t *in, int in_size);
+
+/**
+ * Calculate the output size needed to base64-encode x bytes.
+ */
+#define AV_BASE64_SIZE(x) (((x)+2) / 3 * 4 + 1)
+
+ /**
+ * @}
+ */
+
+#endif /* AVUTIL_BASE64_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/blowfish.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/blowfish.h
new file mode 100644
index 0000000000..0b004532de
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/blowfish.h
@@ -0,0 +1,77 @@
+/*
+ * Blowfish algorithm
+ * Copyright (c) 2012 Samuel Pitoiset
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_BLOWFISH_H
+#define AVUTIL_BLOWFISH_H
+
+#include <stdint.h>
+
+/**
+ * @defgroup lavu_blowfish Blowfish
+ * @ingroup lavu_crypto
+ * @{
+ */
+
+#define AV_BF_ROUNDS 16
+
+typedef struct AVBlowfish {
+ uint32_t p[AV_BF_ROUNDS + 2];
+ uint32_t s[4][256];
+} AVBlowfish;
+
+/**
+ * Initialize an AVBlowfish context.
+ *
+ * @param ctx an AVBlowfish context
+ * @param key a key
+ * @param key_len length of the key
+ */
+void av_blowfish_init(struct AVBlowfish *ctx, const uint8_t *key, int key_len);
+
+/**
+ * Encrypt or decrypt a buffer using a previously initialized context.
+ *
+ * @param ctx an AVBlowfish context
+ * @param xl left four bytes halves of input to be encrypted
+ * @param xr right four bytes halves of input to be encrypted
+ * @param decrypt 0 for encryption, 1 for decryption
+ */
+void av_blowfish_crypt_ecb(struct AVBlowfish *ctx, uint32_t *xl, uint32_t *xr,
+ int decrypt);
+
+/**
+ * Encrypt or decrypt a buffer using a previously initialized context.
+ *
+ * @param ctx an AVBlowfish context
+ * @param dst destination array, can be equal to src
+ * @param src source array, can be equal to dst
+ * @param count number of 8 byte blocks
+ * @param iv initialization vector for CBC mode, if NULL ECB will be used
+ * @param decrypt 0 for encryption, 1 for decryption
+ */
+void av_blowfish_crypt(struct AVBlowfish *ctx, uint8_t *dst, const uint8_t *src,
+ int count, uint8_t *iv, int decrypt);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_BLOWFISH_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/bprint.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/bprint.h
new file mode 100644
index 0000000000..c09b61f20f
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/bprint.h
@@ -0,0 +1,169 @@
+/*
+ * Copyright (c) 2012 Nicolas George
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_BPRINT_H
+#define AVUTIL_BPRINT_H
+
+#include "attributes.h"
+
+/**
+ * Define a structure with extra padding to a fixed size
+ * This helps ensuring binary compatibility with future versions.
+ */
+#define FF_PAD_STRUCTURE(size, ...) \
+ __VA_ARGS__ \
+ char reserved_padding[size - sizeof(struct { __VA_ARGS__ })];
+
+/**
+ * Buffer to print data progressively
+ *
+ * The string buffer grows as necessary and is always 0-terminated.
+ * The content of the string is never accessed, and thus is
+ * encoding-agnostic and can even hold binary data.
+ *
+ * Small buffers are kept in the structure itself, and thus require no
+ * memory allocation at all (unless the contents of the buffer is needed
+ * after the structure goes out of scope). This is almost as lightweight as
+ * declaring a local "char buf[512]".
+ *
+ * The length of the string can go beyond the allocated size: the buffer is
+ * then truncated, but the functions still keep account of the actual total
+ * length.
+ *
+ * In other words, buf->len can be greater than buf->size and records the
+ * total length of what would have been to the buffer if there had been
+ * enough memory.
+ *
+ * Append operations do not need to be tested for failure: if a memory
+ * allocation fails, data stop being appended to the buffer, but the length
+ * is still updated. This situation can be tested with
+ * av_bprint_is_complete().
+ *
+ * The size_max field determines several possible behaviours:
+ *
+ * size_max = -1 (= UINT_MAX) or any large value will let the buffer be
+ * reallocated as necessary, with an amortized linear cost.
+ *
+ * size_max = 0 prevents writing anything to the buffer: only the total
+ * length is computed. The write operations can then possibly be repeated in
+ * a buffer with exactly the necessary size
+ * (using size_init = size_max = len + 1).
+ *
+ * size_max = 1 is automatically replaced by the exact size available in the
+ * structure itself, thus ensuring no dynamic memory allocation. The
+ * internal buffer is large enough to hold a reasonable paragraph of text,
+ * such as the current paragraph.
+ */
+typedef struct AVBPrint {
+ FF_PAD_STRUCTURE(1024,
+ char *str; /** string so far */
+ unsigned len; /** length so far */
+ unsigned size; /** allocated memory */
+ unsigned size_max; /** maximum allocated memory */
+ char reserved_internal_buffer[1];
+ )
+} AVBPrint;
+
+/**
+ * Convenience macros for special values for av_bprint_init() size_max
+ * parameter.
+ */
+#define AV_BPRINT_SIZE_UNLIMITED ((unsigned)-1)
+#define AV_BPRINT_SIZE_AUTOMATIC 1
+#define AV_BPRINT_SIZE_COUNT_ONLY 0
+
+/**
+ * Init a print buffer.
+ *
+ * @param buf buffer to init
+ * @param size_init initial size (including the final 0)
+ * @param size_max maximum size;
+ * 0 means do not write anything, just count the length;
+ * 1 is replaced by the maximum value for automatic storage;
+ * any large value means that the internal buffer will be
+ * reallocated as needed up to that limit; -1 is converted to
+ * UINT_MAX, the largest limit possible.
+ * Check also AV_BPRINT_SIZE_* macros.
+ */
+void av_bprint_init(AVBPrint *buf, unsigned size_init, unsigned size_max);
+
+/**
+ * Init a print buffer using a pre-existing buffer.
+ *
+ * The buffer will not be reallocated.
+ *
+ * @param buf buffer structure to init
+ * @param buffer byte buffer to use for the string data
+ * @param size size of buffer
+ */
+void av_bprint_init_for_buffer(AVBPrint *buf, char *buffer, unsigned size);
+
+/**
+ * Append a formated string to a print buffer.
+ */
+void av_bprintf(AVBPrint *buf, const char *fmt, ...) av_printf_format(2, 3);
+
+/**
+ * Append char c n times to a print buffer.
+ */
+void av_bprint_chars(AVBPrint *buf, char c, unsigned n);
+
+/**
+ * Allocate bytes in the buffer for external use.
+ *
+ * @param[in] buf buffer structure
+ * @param[in] size required size
+ * @param[out] mem pointer to the memory area
+ * @param[out] actual_size size of the memory area after allocation;
+ * can be larger or smaller than size
+ */
+void av_bprint_get_buffer(AVBPrint *buf, unsigned size,
+ unsigned char **mem, unsigned *actual_size);
+
+/**
+ * Reset the string to "" but keep internal allocated data.
+ */
+void av_bprint_clear(AVBPrint *buf);
+
+/**
+ * Test if the print buffer is complete (not truncated).
+ *
+ * It may have been truncated due to a memory allocation failure
+ * or the size_max limit (compare size and size_max if necessary).
+ */
+static inline int av_bprint_is_complete(AVBPrint *buf)
+{
+ return buf->len < buf->size;
+}
+
+/**
+ * Finalize a print buffer.
+ *
+ * The print buffer can no longer be used afterwards,
+ * but the len and size fields are still valid.
+ *
+ * @arg[out] ret_str if not NULL, used to return a permanent copy of the
+ * buffer contents, or NULL if memory allocation fails;
+ * if NULL, the buffer is discarded and freed
+ * @return 0 for success or error code (probably AVERROR(ENOMEM))
+ */
+int av_bprint_finalize(AVBPrint *buf, char **ret_str);
+
+#endif /* AVUTIL_BPRINT_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/bswap.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/bswap.h
new file mode 100644
index 0000000000..8a350e1cd5
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/bswap.h
@@ -0,0 +1,109 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * byte swapping routines
+ */
+
+#ifndef AVUTIL_BSWAP_H
+#define AVUTIL_BSWAP_H
+
+#include <stdint.h>
+#include "libavutil/avconfig.h"
+#include "attributes.h"
+
+#ifdef HAVE_AV_CONFIG_H
+
+#include "config.h"
+
+#if ARCH_ARM
+# include "arm/bswap.h"
+#elif ARCH_AVR32
+# include "avr32/bswap.h"
+#elif ARCH_BFIN
+# include "bfin/bswap.h"
+#elif ARCH_SH4
+# include "sh4/bswap.h"
+#elif ARCH_X86
+# include "x86/bswap.h"
+#endif
+
+#endif /* HAVE_AV_CONFIG_H */
+
+#define AV_BSWAP16C(x) (((x) << 8 & 0xff00) | ((x) >> 8 & 0x00ff))
+#define AV_BSWAP32C(x) (AV_BSWAP16C(x) << 16 | AV_BSWAP16C((x) >> 16))
+#define AV_BSWAP64C(x) (AV_BSWAP32C(x) << 32 | AV_BSWAP32C((x) >> 32))
+
+#define AV_BSWAPC(s, x) AV_BSWAP##s##C(x)
+
+#ifndef av_bswap16
+static av_always_inline av_const uint16_t av_bswap16(uint16_t x)
+{
+ x= (x>>8) | (x<<8);
+ return x;
+}
+#endif
+
+#ifndef av_bswap32
+static av_always_inline av_const uint32_t av_bswap32(uint32_t x)
+{
+ return AV_BSWAP32C(x);
+}
+#endif
+
+#ifndef av_bswap64
+static inline uint64_t av_const av_bswap64(uint64_t x)
+{
+ return (uint64_t)av_bswap32(x) << 32 | av_bswap32(x >> 32);
+}
+#endif
+
+// be2ne ... big-endian to native-endian
+// le2ne ... little-endian to native-endian
+
+#if AV_HAVE_BIGENDIAN
+#define av_be2ne16(x) (x)
+#define av_be2ne32(x) (x)
+#define av_be2ne64(x) (x)
+#define av_le2ne16(x) av_bswap16(x)
+#define av_le2ne32(x) av_bswap32(x)
+#define av_le2ne64(x) av_bswap64(x)
+#define AV_BE2NEC(s, x) (x)
+#define AV_LE2NEC(s, x) AV_BSWAPC(s, x)
+#else
+#define av_be2ne16(x) av_bswap16(x)
+#define av_be2ne32(x) av_bswap32(x)
+#define av_be2ne64(x) av_bswap64(x)
+#define av_le2ne16(x) (x)
+#define av_le2ne32(x) (x)
+#define av_le2ne64(x) (x)
+#define AV_BE2NEC(s, x) AV_BSWAPC(s, x)
+#define AV_LE2NEC(s, x) (x)
+#endif
+
+#define AV_BE2NE16C(x) AV_BE2NEC(16, x)
+#define AV_BE2NE32C(x) AV_BE2NEC(32, x)
+#define AV_BE2NE64C(x) AV_BE2NEC(64, x)
+#define AV_LE2NE16C(x) AV_LE2NEC(16, x)
+#define AV_LE2NE32C(x) AV_LE2NEC(32, x)
+#define AV_LE2NE64C(x) AV_LE2NEC(64, x)
+
+#endif /* AVUTIL_BSWAP_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/common.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/common.h
new file mode 100644
index 0000000000..c99d858472
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/common.h
@@ -0,0 +1,398 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * common internal and external API header
+ */
+
+#ifndef AVUTIL_COMMON_H
+#define AVUTIL_COMMON_H
+
+#include <ctype.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include "attributes.h"
+#include "libavutil/avconfig.h"
+
+#if AV_HAVE_BIGENDIAN
+# define AV_NE(be, le) (be)
+#else
+# define AV_NE(be, le) (le)
+#endif
+
+//rounded division & shift
+#define RSHIFT(a,b) ((a) > 0 ? ((a) + ((1<<(b))>>1))>>(b) : ((a) + ((1<<(b))>>1)-1)>>(b))
+/* assume b>0 */
+#define ROUNDED_DIV(a,b) (((a)>0 ? (a) + ((b)>>1) : (a) - ((b)>>1))/(b))
+#define FFABS(a) ((a) >= 0 ? (a) : (-(a)))
+#define FFSIGN(a) ((a) > 0 ? 1 : -1)
+
+#define FFMAX(a,b) ((a) > (b) ? (a) : (b))
+#define FFMAX3(a,b,c) FFMAX(FFMAX(a,b),c)
+#define FFMIN(a,b) ((a) > (b) ? (b) : (a))
+#define FFMIN3(a,b,c) FFMIN(FFMIN(a,b),c)
+
+#define FFSWAP(type,a,b) do{type SWAP_tmp= b; b= a; a= SWAP_tmp;}while(0)
+#define FF_ARRAY_ELEMS(a) (sizeof(a) / sizeof((a)[0]))
+#define FFALIGN(x, a) (((x)+(a)-1)&~((a)-1))
+
+/* misc math functions */
+extern const uint8_t ff_log2_tab[256];
+
+extern const uint8_t av_reverse[256];
+
+static av_always_inline av_const int av_log2_c(unsigned int v)
+{
+ int n = 0;
+ if (v & 0xffff0000) {
+ v >>= 16;
+ n += 16;
+ }
+ if (v & 0xff00) {
+ v >>= 8;
+ n += 8;
+ }
+ n += ff_log2_tab[v];
+
+ return n;
+}
+
+static av_always_inline av_const int av_log2_16bit_c(unsigned int v)
+{
+ int n = 0;
+ if (v & 0xff00) {
+ v >>= 8;
+ n += 8;
+ }
+ n += ff_log2_tab[v];
+
+ return n;
+}
+
+#ifdef HAVE_AV_CONFIG_H
+# include "config.h"
+# include "intmath.h"
+#endif
+
+/* Pull in unguarded fallback defines at the end of this file. */
+#include "common.h"
+
+/**
+ * Clip a signed integer value into the amin-amax range.
+ * @param a value to clip
+ * @param amin minimum value of the clip range
+ * @param amax maximum value of the clip range
+ * @return clipped value
+ */
+static av_always_inline av_const int av_clip_c(int a, int amin, int amax)
+{
+ if (a < amin) return amin;
+ else if (a > amax) return amax;
+ else return a;
+}
+
+/**
+ * Clip a signed integer value into the 0-255 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const uint8_t av_clip_uint8_c(int a)
+{
+ if (a&(~0xFF)) return (-a)>>31;
+ else return a;
+}
+
+/**
+ * Clip a signed integer value into the -128,127 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const int8_t av_clip_int8_c(int a)
+{
+ if ((a+0x80) & ~0xFF) return (a>>31) ^ 0x7F;
+ else return a;
+}
+
+/**
+ * Clip a signed integer value into the 0-65535 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const uint16_t av_clip_uint16_c(int a)
+{
+ if (a&(~0xFFFF)) return (-a)>>31;
+ else return a;
+}
+
+/**
+ * Clip a signed integer value into the -32768,32767 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const int16_t av_clip_int16_c(int a)
+{
+ if ((a+0x8000) & ~0xFFFF) return (a>>31) ^ 0x7FFF;
+ else return a;
+}
+
+/**
+ * Clip a signed 64-bit integer value into the -2147483648,2147483647 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const int32_t av_clipl_int32_c(int64_t a)
+{
+ if ((a+0x80000000u) & ~UINT64_C(0xFFFFFFFF)) return (a>>63) ^ 0x7FFFFFFF;
+ else return a;
+}
+
+/**
+ * Clip a signed integer to an unsigned power of two range.
+ * @param a value to clip
+ * @param p bit position to clip at
+ * @return clipped value
+ */
+static av_always_inline av_const unsigned av_clip_uintp2_c(int a, int p)
+{
+ if (a & ~((1<<p) - 1)) return -a >> 31 & ((1<<p) - 1);
+ else return a;
+}
+
+/**
+ * Clip a float value into the amin-amax range.
+ * @param a value to clip
+ * @param amin minimum value of the clip range
+ * @param amax maximum value of the clip range
+ * @return clipped value
+ */
+static av_always_inline av_const float av_clipf_c(float a, float amin, float amax)
+{
+ if (a < amin) return amin;
+ else if (a > amax) return amax;
+ else return a;
+}
+
+/** Compute ceil(log2(x)).
+ * @param x value used to compute ceil(log2(x))
+ * @return computed ceiling of log2(x)
+ */
+static av_always_inline av_const int av_ceil_log2_c(int x)
+{
+ return av_log2((x - 1) << 1);
+}
+
+/**
+ * Count number of bits set to one in x
+ * @param x value to count bits of
+ * @return the number of bits set to one in x
+ */
+static av_always_inline av_const int av_popcount_c(uint32_t x)
+{
+ x -= (x >> 1) & 0x55555555;
+ x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
+ x = (x + (x >> 4)) & 0x0F0F0F0F;
+ x += x >> 8;
+ return (x + (x >> 16)) & 0x3F;
+}
+
+/**
+ * Count number of bits set to one in x
+ * @param x value to count bits of
+ * @return the number of bits set to one in x
+ */
+static av_always_inline av_const int av_popcount64_c(uint64_t x)
+{
+ return av_popcount(x) + av_popcount(x >> 32);
+}
+
+#define MKTAG(a,b,c,d) ((a) | ((b) << 8) | ((c) << 16) | ((unsigned)(d) << 24))
+#define MKBETAG(a,b,c,d) ((d) | ((c) << 8) | ((b) << 16) | ((unsigned)(a) << 24))
+
+/**
+ * Convert a UTF-8 character (up to 4 bytes) to its 32-bit UCS-4 encoded form.
+ *
+ * @param val Output value, must be an lvalue of type uint32_t.
+ * @param GET_BYTE Expression reading one byte from the input.
+ * Evaluated up to 7 times (4 for the currently
+ * assigned Unicode range). With a memory buffer
+ * input, this could be *ptr++.
+ * @param ERROR Expression to be evaluated on invalid input,
+ * typically a goto statement.
+ */
+#define GET_UTF8(val, GET_BYTE, ERROR)\
+ val= GET_BYTE;\
+ {\
+ int ones= 7 - av_log2(val ^ 255);\
+ if(ones==1)\
+ ERROR\
+ val&= 127>>ones;\
+ while(--ones > 0){\
+ int tmp= GET_BYTE - 128;\
+ if(tmp>>6)\
+ ERROR\
+ val= (val<<6) + tmp;\
+ }\
+ }
+
+/**
+ * Convert a UTF-16 character (2 or 4 bytes) to its 32-bit UCS-4 encoded form.
+ *
+ * @param val Output value, must be an lvalue of type uint32_t.
+ * @param GET_16BIT Expression returning two bytes of UTF-16 data converted
+ * to native byte order. Evaluated one or two times.
+ * @param ERROR Expression to be evaluated on invalid input,
+ * typically a goto statement.
+ */
+#define GET_UTF16(val, GET_16BIT, ERROR)\
+ val = GET_16BIT;\
+ {\
+ unsigned int hi = val - 0xD800;\
+ if (hi < 0x800) {\
+ val = GET_16BIT - 0xDC00;\
+ if (val > 0x3FFU || hi > 0x3FFU)\
+ ERROR\
+ val += (hi<<10) + 0x10000;\
+ }\
+ }\
+
+/**
+ * @def PUT_UTF8(val, tmp, PUT_BYTE)
+ * Convert a 32-bit Unicode character to its UTF-8 encoded form (up to 4 bytes long).
+ * @param val is an input-only argument and should be of type uint32_t. It holds
+ * a UCS-4 encoded Unicode character that is to be converted to UTF-8. If
+ * val is given as a function it is executed only once.
+ * @param tmp is a temporary variable and should be of type uint8_t. It
+ * represents an intermediate value during conversion that is to be
+ * output by PUT_BYTE.
+ * @param PUT_BYTE writes the converted UTF-8 bytes to any proper destination.
+ * It could be a function or a statement, and uses tmp as the input byte.
+ * For example, PUT_BYTE could be "*output++ = tmp;" PUT_BYTE will be
+ * executed up to 4 times for values in the valid UTF-8 range and up to
+ * 7 times in the general case, depending on the length of the converted
+ * Unicode character.
+ */
+#define PUT_UTF8(val, tmp, PUT_BYTE)\
+ {\
+ int bytes, shift;\
+ uint32_t in = val;\
+ if (in < 0x80) {\
+ tmp = in;\
+ PUT_BYTE\
+ } else {\
+ bytes = (av_log2(in) + 4) / 5;\
+ shift = (bytes - 1) * 6;\
+ tmp = (256 - (256 >> bytes)) | (in >> shift);\
+ PUT_BYTE\
+ while (shift >= 6) {\
+ shift -= 6;\
+ tmp = 0x80 | ((in >> shift) & 0x3f);\
+ PUT_BYTE\
+ }\
+ }\
+ }
+
+/**
+ * @def PUT_UTF16(val, tmp, PUT_16BIT)
+ * Convert a 32-bit Unicode character to its UTF-16 encoded form (2 or 4 bytes).
+ * @param val is an input-only argument and should be of type uint32_t. It holds
+ * a UCS-4 encoded Unicode character that is to be converted to UTF-16. If
+ * val is given as a function it is executed only once.
+ * @param tmp is a temporary variable and should be of type uint16_t. It
+ * represents an intermediate value during conversion that is to be
+ * output by PUT_16BIT.
+ * @param PUT_16BIT writes the converted UTF-16 data to any proper destination
+ * in desired endianness. It could be a function or a statement, and uses tmp
+ * as the input byte. For example, PUT_BYTE could be "*output++ = tmp;"
+ * PUT_BYTE will be executed 1 or 2 times depending on input character.
+ */
+#define PUT_UTF16(val, tmp, PUT_16BIT)\
+ {\
+ uint32_t in = val;\
+ if (in < 0x10000) {\
+ tmp = in;\
+ PUT_16BIT\
+ } else {\
+ tmp = 0xD800 | ((in - 0x10000) >> 10);\
+ PUT_16BIT\
+ tmp = 0xDC00 | ((in - 0x10000) & 0x3FF);\
+ PUT_16BIT\
+ }\
+ }\
+
+
+
+#include "mem.h"
+
+#ifdef HAVE_AV_CONFIG_H
+# include "internal.h"
+#endif /* HAVE_AV_CONFIG_H */
+
+#endif /* AVUTIL_COMMON_H */
+
+/*
+ * The following definitions are outside the multiple inclusion guard
+ * to ensure they are immediately available in intmath.h.
+ */
+
+#ifndef av_log2
+# define av_log2 av_log2_c
+#endif
+#ifndef av_log2_16bit
+# define av_log2_16bit av_log2_16bit_c
+#endif
+#ifndef av_ceil_log2
+# define av_ceil_log2 av_ceil_log2_c
+#endif
+#ifndef av_clip
+# define av_clip av_clip_c
+#endif
+#ifndef av_clip_uint8
+# define av_clip_uint8 av_clip_uint8_c
+#endif
+#ifndef av_clip_int8
+# define av_clip_int8 av_clip_int8_c
+#endif
+#ifndef av_clip_uint16
+# define av_clip_uint16 av_clip_uint16_c
+#endif
+#ifndef av_clip_int16
+# define av_clip_int16 av_clip_int16_c
+#endif
+#ifndef av_clipl_int32
+# define av_clipl_int32 av_clipl_int32_c
+#endif
+#ifndef av_clip_uintp2
+# define av_clip_uintp2 av_clip_uintp2_c
+#endif
+#ifndef av_clipf
+# define av_clipf av_clipf_c
+#endif
+#ifndef av_popcount
+# define av_popcount av_popcount_c
+#endif
+#ifndef av_popcount64
+# define av_popcount64 av_popcount64_c
+#endif
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/cpu.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/cpu.h
new file mode 100644
index 0000000000..df7bf4421a
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/cpu.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2000, 2001, 2002 Fabrice Bellard
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_CPU_H
+#define AVUTIL_CPU_H
+
+#define AV_CPU_FLAG_FORCE 0x80000000 /* force usage of selected flags (OR) */
+
+ /* lower 16 bits - CPU features */
+#define AV_CPU_FLAG_MMX 0x0001 ///< standard MMX
+#define AV_CPU_FLAG_MMX2 0x0002 ///< SSE integer functions or AMD MMX ext
+#define AV_CPU_FLAG_3DNOW 0x0004 ///< AMD 3DNOW
+#define AV_CPU_FLAG_SSE 0x0008 ///< SSE functions
+#define AV_CPU_FLAG_SSE2 0x0010 ///< PIV SSE2 functions
+#define AV_CPU_FLAG_SSE2SLOW 0x40000000 ///< SSE2 supported, but usually not faster
+#define AV_CPU_FLAG_3DNOWEXT 0x0020 ///< AMD 3DNowExt
+#define AV_CPU_FLAG_SSE3 0x0040 ///< Prescott SSE3 functions
+#define AV_CPU_FLAG_SSE3SLOW 0x20000000 ///< SSE3 supported, but usually not faster
+#define AV_CPU_FLAG_SSSE3 0x0080 ///< Conroe SSSE3 functions
+#define AV_CPU_FLAG_ATOM 0x10000000 ///< Atom processor, some SSSE3 instructions are slower
+#define AV_CPU_FLAG_SSE4 0x0100 ///< Penryn SSE4.1 functions
+#define AV_CPU_FLAG_SSE42 0x0200 ///< Nehalem SSE4.2 functions
+#define AV_CPU_FLAG_AVX 0x4000 ///< AVX functions: requires OS support even if YMM registers aren't used
+#define AV_CPU_FLAG_XOP 0x0400 ///< Bulldozer XOP functions
+#define AV_CPU_FLAG_FMA4 0x0800 ///< Bulldozer FMA4 functions
+#define AV_CPU_FLAG_IWMMXT 0x0100 ///< XScale IWMMXT
+#define AV_CPU_FLAG_ALTIVEC 0x0001 ///< standard
+
+/**
+ * Return the flags which specify extensions supported by the CPU.
+ */
+int av_get_cpu_flags(void);
+
+/* The following CPU-specific functions shall not be called directly. */
+int ff_get_cpu_flags_arm(void);
+int ff_get_cpu_flags_ppc(void);
+int ff_get_cpu_flags_x86(void);
+
+#endif /* AVUTIL_CPU_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/crc.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/crc.h
new file mode 100644
index 0000000000..a934119413
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/crc.h
@@ -0,0 +1,44 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_CRC_H
+#define AVUTIL_CRC_H
+
+#include <stdint.h>
+#include <stddef.h>
+#include "attributes.h"
+
+typedef uint32_t AVCRC;
+
+typedef enum {
+ AV_CRC_8_ATM,
+ AV_CRC_16_ANSI,
+ AV_CRC_16_CCITT,
+ AV_CRC_32_IEEE,
+ AV_CRC_32_IEEE_LE, /*< reversed bitorder version of AV_CRC_32_IEEE */
+ AV_CRC_MAX, /*< Not part of public API! Do not use outside libavutil. */
+}AVCRCId;
+
+int av_crc_init(AVCRC *ctx, int le, int bits, uint32_t poly, int ctx_size);
+const AVCRC *av_crc_get_table(AVCRCId crc_id);
+uint32_t av_crc(const AVCRC *ctx, uint32_t start_crc, const uint8_t *buffer, size_t length) av_pure;
+
+#endif /* AVUTIL_CRC_H */
+
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/dict.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/dict.h
new file mode 100644
index 0000000000..6e28b61406
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/dict.h
@@ -0,0 +1,121 @@
+/*
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * Public dictionary API.
+ */
+
+#ifndef AVUTIL_DICT_H
+#define AVUTIL_DICT_H
+
+/**
+ * @addtogroup lavu_dict AVDictionary
+ * @ingroup lavu_data
+ *
+ * @brief Simple key:value store
+ *
+ * @{
+ * Dictionaries are used for storing key:value pairs. To create
+ * an AVDictionary, simply pass an address of a NULL pointer to
+ * av_dict_set(). NULL can be used as an empty dictionary wherever
+ * a pointer to an AVDictionary is required.
+ * Use av_dict_get() to retrieve an entry or iterate over all
+ * entries and finally av_dict_free() to free the dictionary
+ * and all its contents.
+ *
+ * @code
+ * AVDictionary *d = NULL; // "create" an empty dictionary
+ * av_dict_set(&d, "foo", "bar", 0); // add an entry
+ *
+ * char *k = av_strdup("key"); // if your strings are already allocated,
+ * char *v = av_strdup("value"); // you can avoid copying them like this
+ * av_dict_set(&d, k, v, AV_DICT_DONT_STRDUP_KEY | AV_DICT_DONT_STRDUP_VAL);
+ *
+ * AVDictionaryEntry *t = NULL;
+ * while (t = av_dict_get(d, "", t, AV_DICT_IGNORE_SUFFIX)) {
+ * <....> // iterate over all entries in d
+ * }
+ *
+ * av_dict_free(&d);
+ * @endcode
+ *
+ */
+
+#define AV_DICT_MATCH_CASE 1
+#define AV_DICT_IGNORE_SUFFIX 2
+#define AV_DICT_DONT_STRDUP_KEY 4 /**< Take ownership of a key that's been
+ allocated with av_malloc() and children. */
+#define AV_DICT_DONT_STRDUP_VAL 8 /**< Take ownership of a value that's been
+ allocated with av_malloc() and chilren. */
+#define AV_DICT_DONT_OVERWRITE 16 ///< Don't overwrite existing entries.
+#define AV_DICT_APPEND 32 /**< If the entry already exists, append to it. Note that no
+ delimiter is added, the strings are simply concatenated. */
+
+typedef struct {
+ char *key;
+ char *value;
+} AVDictionaryEntry;
+
+typedef struct AVDictionary AVDictionary;
+
+/**
+ * Get a dictionary entry with matching key.
+ *
+ * @param prev Set to the previous matching element to find the next.
+ * If set to NULL the first matching element is returned.
+ * @param flags Allows case as well as suffix-insensitive comparisons.
+ * @return Found entry or NULL, changing key or value leads to undefined behavior.
+ */
+AVDictionaryEntry *
+av_dict_get(AVDictionary *m, const char *key, const AVDictionaryEntry *prev, int flags);
+
+/**
+ * Set the given entry in *pm, overwriting an existing entry.
+ *
+ * @param pm pointer to a pointer to a dictionary struct. If *pm is NULL
+ * a dictionary struct is allocated and put in *pm.
+ * @param key entry key to add to *pm (will be av_strduped depending on flags)
+ * @param value entry value to add to *pm (will be av_strduped depending on flags).
+ * Passing a NULL value will cause an existing tag to be deleted.
+ * @return >= 0 on success otherwise an error code <0
+ */
+int av_dict_set(AVDictionary **pm, const char *key, const char *value, int flags);
+
+/**
+ * Copy entries from one AVDictionary struct into another.
+ * @param dst pointer to a pointer to a AVDictionary struct. If *dst is NULL,
+ * this function will allocate a struct for you and put it in *dst
+ * @param src pointer to source AVDictionary struct
+ * @param flags flags to use when setting entries in *dst
+ * @note metadata is read using the AV_DICT_IGNORE_SUFFIX flag
+ */
+void av_dict_copy(AVDictionary **dst, AVDictionary *src, int flags);
+
+/**
+ * Free all the memory allocated for an AVDictionary struct
+ * and all keys and values.
+ */
+void av_dict_free(AVDictionary **m);
+
+/**
+ * @}
+ */
+
+#endif // AVUTIL_DICT_H
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/error.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/error.h
new file mode 100644
index 0000000000..11bcc5c4c4
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/error.h
@@ -0,0 +1,81 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * error code definitions
+ */
+
+#ifndef AVUTIL_ERROR_H
+#define AVUTIL_ERROR_H
+
+#include <errno.h>
+#include "avutil.h"
+
+/**
+ * @addtogroup lavu_error
+ *
+ * @{
+ */
+
+
+/* error handling */
+#if EDOM > 0
+#define AVERROR(e) (-(e)) ///< Returns a negative error code from a POSIX error code, to return from library functions.
+#define AVUNERROR(e) (-(e)) ///< Returns a POSIX error code from a library function error return value.
+#else
+/* Some platforms have E* and errno already negated. */
+#define AVERROR(e) (e)
+#define AVUNERROR(e) (e)
+#endif
+
+#define AVERROR_BSF_NOT_FOUND (-MKTAG(0xF8,'B','S','F')) ///< Bitstream filter not found
+#define AVERROR_DECODER_NOT_FOUND (-MKTAG(0xF8,'D','E','C')) ///< Decoder not found
+#define AVERROR_DEMUXER_NOT_FOUND (-MKTAG(0xF8,'D','E','M')) ///< Demuxer not found
+#define AVERROR_ENCODER_NOT_FOUND (-MKTAG(0xF8,'E','N','C')) ///< Encoder not found
+#define AVERROR_EOF (-MKTAG( 'E','O','F',' ')) ///< End of file
+#define AVERROR_EXIT (-MKTAG( 'E','X','I','T')) ///< Immediate exit was requested; the called function should not be restarted
+#define AVERROR_FILTER_NOT_FOUND (-MKTAG(0xF8,'F','I','L')) ///< Filter not found
+#define AVERROR_INVALIDDATA (-MKTAG( 'I','N','D','A')) ///< Invalid data found when processing input
+#define AVERROR_MUXER_NOT_FOUND (-MKTAG(0xF8,'M','U','X')) ///< Muxer not found
+#define AVERROR_OPTION_NOT_FOUND (-MKTAG(0xF8,'O','P','T')) ///< Option not found
+#define AVERROR_PATCHWELCOME (-MKTAG( 'P','A','W','E')) ///< Not yet implemented in Libav, patches welcome
+#define AVERROR_PROTOCOL_NOT_FOUND (-MKTAG(0xF8,'P','R','O')) ///< Protocol not found
+#define AVERROR_STREAM_NOT_FOUND (-MKTAG(0xF8,'S','T','R')) ///< Stream not found
+#define AVERROR_BUG (-MKTAG( 'B','U','G',' ')) ///< Bug detected, please report the issue
+#define AVERROR_UNKNOWN (-MKTAG( 'U','N','K','N')) ///< Unknown error, typically from an external library
+
+/**
+ * Put a description of the AVERROR code errnum in errbuf.
+ * In case of failure the global variable errno is set to indicate the
+ * error. Even in case of failure av_strerror() will print a generic
+ * error message indicating the errnum provided to errbuf.
+ *
+ * @param errnum error code to describe
+ * @param errbuf buffer to which description is written
+ * @param errbuf_size the size in bytes of errbuf
+ * @return 0 on success, a negative value if a description for errnum
+ * cannot be found
+ */
+int av_strerror(int errnum, char *errbuf, size_t errbuf_size);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_ERROR_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/eval.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/eval.h
new file mode 100644
index 0000000000..ccb29e7a33
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/eval.h
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2002 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * simple arithmetic expression evaluator
+ */
+
+#ifndef AVUTIL_EVAL_H
+#define AVUTIL_EVAL_H
+
+#include "avutil.h"
+
+typedef struct AVExpr AVExpr;
+
+/**
+ * Parse and evaluate an expression.
+ * Note, this is significantly slower than av_expr_eval().
+ *
+ * @param res a pointer to a double where is put the result value of
+ * the expression, or NAN in case of error
+ * @param s expression as a zero terminated string, for example "1+2^3+5*5+sin(2/3)"
+ * @param const_names NULL terminated array of zero terminated strings of constant identifiers, for example {"PI", "E", 0}
+ * @param const_values a zero terminated array of values for the identifiers from const_names
+ * @param func1_names NULL terminated array of zero terminated strings of funcs1 identifiers
+ * @param funcs1 NULL terminated array of function pointers for functions which take 1 argument
+ * @param func2_names NULL terminated array of zero terminated strings of funcs2 identifiers
+ * @param funcs2 NULL terminated array of function pointers for functions which take 2 arguments
+ * @param opaque a pointer which will be passed to all functions from funcs1 and funcs2
+ * @param log_ctx parent logging context
+ * @return 0 in case of success, a negative value corresponding to an
+ * AVERROR code otherwise
+ */
+int av_expr_parse_and_eval(double *res, const char *s,
+ const char * const *const_names, const double *const_values,
+ const char * const *func1_names, double (* const *funcs1)(void *, double),
+ const char * const *func2_names, double (* const *funcs2)(void *, double, double),
+ void *opaque, int log_offset, void *log_ctx);
+
+/**
+ * Parse an expression.
+ *
+ * @param expr a pointer where is put an AVExpr containing the parsed
+ * value in case of successful parsing, or NULL otherwise.
+ * The pointed to AVExpr must be freed with av_expr_free() by the user
+ * when it is not needed anymore.
+ * @param s expression as a zero terminated string, for example "1+2^3+5*5+sin(2/3)"
+ * @param const_names NULL terminated array of zero terminated strings of constant identifiers, for example {"PI", "E", 0}
+ * @param func1_names NULL terminated array of zero terminated strings of funcs1 identifiers
+ * @param funcs1 NULL terminated array of function pointers for functions which take 1 argument
+ * @param func2_names NULL terminated array of zero terminated strings of funcs2 identifiers
+ * @param funcs2 NULL terminated array of function pointers for functions which take 2 arguments
+ * @param log_ctx parent logging context
+ * @return 0 in case of success, a negative value corresponding to an
+ * AVERROR code otherwise
+ */
+int av_expr_parse(AVExpr **expr, const char *s,
+ const char * const *const_names,
+ const char * const *func1_names, double (* const *funcs1)(void *, double),
+ const char * const *func2_names, double (* const *funcs2)(void *, double, double),
+ int log_offset, void *log_ctx);
+
+/**
+ * Evaluate a previously parsed expression.
+ *
+ * @param const_values a zero terminated array of values for the identifiers from av_expr_parse() const_names
+ * @param opaque a pointer which will be passed to all functions from funcs1 and funcs2
+ * @return the value of the expression
+ */
+double av_expr_eval(AVExpr *e, const double *const_values, void *opaque);
+
+/**
+ * Free a parsed expression previously created with av_expr_parse().
+ */
+void av_expr_free(AVExpr *e);
+
+/**
+ * Parse the string in numstr and return its value as a double. If
+ * the string is empty, contains only whitespaces, or does not contain
+ * an initial substring that has the expected syntax for a
+ * floating-point number, no conversion is performed. In this case,
+ * returns a value of zero and the value returned in tail is the value
+ * of numstr.
+ *
+ * @param numstr a string representing a number, may contain one of
+ * the International System number postfixes, for example 'K', 'M',
+ * 'G'. If 'i' is appended after the postfix, powers of 2 are used
+ * instead of powers of 10. The 'B' postfix multiplies the value for
+ * 8, and can be appended after another postfix or used alone. This
+ * allows using for example 'KB', 'MiB', 'G' and 'B' as postfix.
+ * @param tail if non-NULL puts here the pointer to the char next
+ * after the last parsed character
+ */
+double av_strtod(const char *numstr, char **tail);
+
+#endif /* AVUTIL_EVAL_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/fifo.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/fifo.h
new file mode 100644
index 0000000000..f106239304
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/fifo.h
@@ -0,0 +1,141 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * a very simple circular buffer FIFO implementation
+ */
+
+#ifndef AVUTIL_FIFO_H
+#define AVUTIL_FIFO_H
+
+#include <stdint.h>
+#include "avutil.h"
+
+typedef struct AVFifoBuffer {
+ uint8_t *buffer;
+ uint8_t *rptr, *wptr, *end;
+ uint32_t rndx, wndx;
+} AVFifoBuffer;
+
+/**
+ * Initialize an AVFifoBuffer.
+ * @param size of FIFO
+ * @return AVFifoBuffer or NULL in case of memory allocation failure
+ */
+AVFifoBuffer *av_fifo_alloc(unsigned int size);
+
+/**
+ * Free an AVFifoBuffer.
+ * @param f AVFifoBuffer to free
+ */
+void av_fifo_free(AVFifoBuffer *f);
+
+/**
+ * Reset the AVFifoBuffer to the state right after av_fifo_alloc, in particular it is emptied.
+ * @param f AVFifoBuffer to reset
+ */
+void av_fifo_reset(AVFifoBuffer *f);
+
+/**
+ * Return the amount of data in bytes in the AVFifoBuffer, that is the
+ * amount of data you can read from it.
+ * @param f AVFifoBuffer to read from
+ * @return size
+ */
+int av_fifo_size(AVFifoBuffer *f);
+
+/**
+ * Return the amount of space in bytes in the AVFifoBuffer, that is the
+ * amount of data you can write into it.
+ * @param f AVFifoBuffer to write into
+ * @return size
+ */
+int av_fifo_space(AVFifoBuffer *f);
+
+/**
+ * Feed data from an AVFifoBuffer to a user-supplied callback.
+ * @param f AVFifoBuffer to read from
+ * @param buf_size number of bytes to read
+ * @param func generic read function
+ * @param dest data destination
+ */
+int av_fifo_generic_read(AVFifoBuffer *f, void *dest, int buf_size, void (*func)(void*, void*, int));
+
+/**
+ * Feed data from a user-supplied callback to an AVFifoBuffer.
+ * @param f AVFifoBuffer to write to
+ * @param src data source; non-const since it may be used as a
+ * modifiable context by the function defined in func
+ * @param size number of bytes to write
+ * @param func generic write function; the first parameter is src,
+ * the second is dest_buf, the third is dest_buf_size.
+ * func must return the number of bytes written to dest_buf, or <= 0 to
+ * indicate no more data available to write.
+ * If func is NULL, src is interpreted as a simple byte array for source data.
+ * @return the number of bytes written to the FIFO
+ */
+int av_fifo_generic_write(AVFifoBuffer *f, void *src, int size, int (*func)(void*, void*, int));
+
+/**
+ * Resize an AVFifoBuffer.
+ * @param f AVFifoBuffer to resize
+ * @param size new AVFifoBuffer size in bytes
+ * @return <0 for failure, >=0 otherwise
+ */
+int av_fifo_realloc2(AVFifoBuffer *f, unsigned int size);
+
+/**
+ * Read and discard the specified amount of data from an AVFifoBuffer.
+ * @param f AVFifoBuffer to read from
+ * @param size amount of data to read in bytes
+ */
+void av_fifo_drain(AVFifoBuffer *f, int size);
+
+/**
+ * Return a pointer to the data stored in a FIFO buffer at a certain offset.
+ * The FIFO buffer is not modified.
+ *
+ * @param f AVFifoBuffer to peek at, f must be non-NULL
+ * @param offs an offset in bytes, its absolute value must be less
+ * than the used buffer size or the returned pointer will
+ * point outside to the buffer data.
+ * The used buffer size can be checked with av_fifo_size().
+ */
+static inline uint8_t *av_fifo_peek2(const AVFifoBuffer *f, int offs)
+{
+ uint8_t *ptr = f->rptr + offs;
+ if (ptr >= f->end)
+ ptr = f->buffer + (ptr - f->end);
+ else if (ptr < f->buffer)
+ ptr = f->end - (f->buffer - ptr);
+ return ptr;
+}
+
+#if FF_API_AV_FIFO_PEEK
+/**
+ * @deprecated Use av_fifo_peek2() instead.
+ */
+attribute_deprecated
+static inline uint8_t av_fifo_peek(AVFifoBuffer *f, int offs)
+{
+ return *av_fifo_peek2(f, offs);
+}
+#endif
+
+#endif /* AVUTIL_FIFO_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/file.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/file.h
new file mode 100644
index 0000000000..c481c37f93
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/file.h
@@ -0,0 +1,52 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_FILE_H
+#define AVUTIL_FILE_H
+
+#include "avutil.h"
+
+/**
+ * @file
+ * Misc file utilities.
+ */
+
+/**
+ * Read the file with name filename, and put its content in a newly
+ * allocated buffer or map it with mmap() when available.
+ * In case of success set *bufptr to the read or mmapped buffer, and
+ * *size to the size in bytes of the buffer in *bufptr.
+ * The returned buffer must be released with av_file_unmap().
+ *
+ * @param log_offset loglevel offset used for logging
+ * @param log_ctx context used for logging
+ * @return a non negative number in case of success, a negative value
+ * corresponding to an AVERROR error code in case of failure
+ */
+int av_file_map(const char *filename, uint8_t **bufptr, size_t *size,
+ int log_offset, void *log_ctx);
+
+/**
+ * Unmap or free the buffer bufptr created by av_file_map().
+ *
+ * @param size size in bytes of bufptr, must be the same as returned
+ * by av_file_map()
+ */
+void av_file_unmap(uint8_t *bufptr, size_t size);
+
+#endif /* AVUTIL_FILE_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/imgutils.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/imgutils.h
new file mode 100644
index 0000000000..3815a49ae4
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/imgutils.h
@@ -0,0 +1,138 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_IMGUTILS_H
+#define AVUTIL_IMGUTILS_H
+
+/**
+ * @file
+ * misc image utilities
+ *
+ * @addtogroup lavu_picture
+ * @{
+ */
+
+#include "avutil.h"
+#include "pixdesc.h"
+
+/**
+ * Compute the max pixel step for each plane of an image with a
+ * format described by pixdesc.
+ *
+ * The pixel step is the distance in bytes between the first byte of
+ * the group of bytes which describe a pixel component and the first
+ * byte of the successive group in the same plane for the same
+ * component.
+ *
+ * @param max_pixsteps an array which is filled with the max pixel step
+ * for each plane. Since a plane may contain different pixel
+ * components, the computed max_pixsteps[plane] is relative to the
+ * component in the plane with the max pixel step.
+ * @param max_pixstep_comps an array which is filled with the component
+ * for each plane which has the max pixel step. May be NULL.
+ */
+void av_image_fill_max_pixsteps(int max_pixsteps[4], int max_pixstep_comps[4],
+ const AVPixFmtDescriptor *pixdesc);
+
+/**
+ * Compute the size of an image line with format pix_fmt and width
+ * width for the plane plane.
+ *
+ * @return the computed size in bytes
+ */
+int av_image_get_linesize(enum PixelFormat pix_fmt, int width, int plane);
+
+/**
+ * Fill plane linesizes for an image with pixel format pix_fmt and
+ * width width.
+ *
+ * @param linesizes array to be filled with the linesize for each plane
+ * @return >= 0 in case of success, a negative error code otherwise
+ */
+int av_image_fill_linesizes(int linesizes[4], enum PixelFormat pix_fmt, int width);
+
+/**
+ * Fill plane data pointers for an image with pixel format pix_fmt and
+ * height height.
+ *
+ * @param data pointers array to be filled with the pointer for each image plane
+ * @param ptr the pointer to a buffer which will contain the image
+ * @param linesizes the array containing the linesize for each
+ * plane, should be filled by av_image_fill_linesizes()
+ * @return the size in bytes required for the image buffer, a negative
+ * error code in case of failure
+ */
+int av_image_fill_pointers(uint8_t *data[4], enum PixelFormat pix_fmt, int height,
+ uint8_t *ptr, const int linesizes[4]);
+
+/**
+ * Allocate an image with size w and h and pixel format pix_fmt, and
+ * fill pointers and linesizes accordingly.
+ * The allocated image buffer has to be freed by using
+ * av_freep(&pointers[0]).
+ *
+ * @param align the value to use for buffer size alignment
+ * @return the size in bytes required for the image buffer, a negative
+ * error code in case of failure
+ */
+int av_image_alloc(uint8_t *pointers[4], int linesizes[4],
+ int w, int h, enum PixelFormat pix_fmt, int align);
+
+/**
+ * Copy image plane from src to dst.
+ * That is, copy "height" number of lines of "bytewidth" bytes each.
+ * The first byte of each successive line is separated by *_linesize
+ * bytes.
+ *
+ * @param dst_linesize linesize for the image plane in dst
+ * @param src_linesize linesize for the image plane in src
+ */
+void av_image_copy_plane(uint8_t *dst, int dst_linesize,
+ const uint8_t *src, int src_linesize,
+ int bytewidth, int height);
+
+/**
+ * Copy image in src_data to dst_data.
+ *
+ * @param dst_linesizes linesizes for the image in dst_data
+ * @param src_linesizes linesizes for the image in src_data
+ */
+void av_image_copy(uint8_t *dst_data[4], int dst_linesizes[4],
+ const uint8_t *src_data[4], const int src_linesizes[4],
+ enum PixelFormat pix_fmt, int width, int height);
+
+/**
+ * Check if the given dimension of an image is valid, meaning that all
+ * bytes of the image can be addressed with a signed int.
+ *
+ * @param w the width of the picture
+ * @param h the height of the picture
+ * @param log_offset the offset to sum to the log level for logging with log_ctx
+ * @param log_ctx the parent logging context, it may be NULL
+ * @return >= 0 if valid, a negative error code otherwise
+ */
+int av_image_check_size(unsigned int w, unsigned int h, int log_offset, void *log_ctx);
+
+int ff_set_systematic_pal2(uint32_t pal[256], enum PixelFormat pix_fmt);
+
+/**
+ * @}
+ */
+
+
+#endif /* AVUTIL_IMGUTILS_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/intfloat.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/intfloat.h
new file mode 100644
index 0000000000..9db624a6ce
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/intfloat.h
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2011 Mans Rullgard
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_INTFLOAT_H
+#define AVUTIL_INTFLOAT_H
+
+#include <stdint.h>
+#include "attributes.h"
+
+union av_intfloat32 {
+ uint32_t i;
+ float f;
+};
+
+union av_intfloat64 {
+ uint64_t i;
+ double f;
+};
+
+/**
+ * Reinterpret a 32-bit integer as a float.
+ */
+static av_always_inline float av_int2float(uint32_t i)
+{
+ union av_intfloat32 v = { .i = i };
+ return v.f;
+}
+
+/**
+ * Reinterpret a float as a 32-bit integer.
+ */
+static av_always_inline uint32_t av_float2int(float f)
+{
+ union av_intfloat32 v = { .f = f };
+ return v.i;
+}
+
+/**
+ * Reinterpret a 64-bit integer as a double.
+ */
+static av_always_inline double av_int2double(uint64_t i)
+{
+ union av_intfloat64 v = { .i = i };
+ return v.f;
+}
+
+/**
+ * Reinterpret a double as a 64-bit integer.
+ */
+static av_always_inline uint64_t av_double2int(double f)
+{
+ union av_intfloat64 v = { .f = f };
+ return v.i;
+}
+
+#endif /* AVUTIL_INTFLOAT_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/intfloat_readwrite.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/intfloat_readwrite.h
new file mode 100644
index 0000000000..f093b92cd2
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/intfloat_readwrite.h
@@ -0,0 +1,40 @@
+/*
+ * copyright (c) 2005 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_INTFLOAT_READWRITE_H
+#define AVUTIL_INTFLOAT_READWRITE_H
+
+#include <stdint.h>
+#include "attributes.h"
+
+/* IEEE 80 bits extended float */
+typedef struct AVExtFloat {
+ uint8_t exponent[2];
+ uint8_t mantissa[8];
+} AVExtFloat;
+
+attribute_deprecated double av_int2dbl(int64_t v) av_const;
+attribute_deprecated float av_int2flt(int32_t v) av_const;
+attribute_deprecated double av_ext2dbl(const AVExtFloat ext) av_const;
+attribute_deprecated int64_t av_dbl2int(double d) av_const;
+attribute_deprecated int32_t av_flt2int(float d) av_const;
+attribute_deprecated AVExtFloat av_dbl2ext(double d) av_const;
+
+#endif /* AVUTIL_INTFLOAT_READWRITE_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/intreadwrite.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/intreadwrite.h
new file mode 100644
index 0000000000..01eb27804a
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/intreadwrite.h
@@ -0,0 +1,522 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_INTREADWRITE_H
+#define AVUTIL_INTREADWRITE_H
+
+#include <stdint.h>
+#include "libavutil/avconfig.h"
+#include "attributes.h"
+#include "bswap.h"
+
+typedef union {
+ uint64_t u64;
+ uint32_t u32[2];
+ uint16_t u16[4];
+ uint8_t u8 [8];
+ double f64;
+ float f32[2];
+} av_alias av_alias64;
+
+typedef union {
+ uint32_t u32;
+ uint16_t u16[2];
+ uint8_t u8 [4];
+ float f32;
+} av_alias av_alias32;
+
+typedef union {
+ uint16_t u16;
+ uint8_t u8 [2];
+} av_alias av_alias16;
+
+/*
+ * Arch-specific headers can provide any combination of
+ * AV_[RW][BLN](16|24|32|64) and AV_(COPY|SWAP|ZERO)(64|128) macros.
+ * Preprocessor symbols must be defined, even if these are implemented
+ * as inline functions.
+ */
+
+#ifdef HAVE_AV_CONFIG_H
+
+#include "config.h"
+
+#if ARCH_ARM
+# include "arm/intreadwrite.h"
+#elif ARCH_AVR32
+# include "avr32/intreadwrite.h"
+#elif ARCH_MIPS
+# include "mips/intreadwrite.h"
+#elif ARCH_PPC
+# include "ppc/intreadwrite.h"
+#elif ARCH_TOMI
+# include "tomi/intreadwrite.h"
+#elif ARCH_X86
+# include "x86/intreadwrite.h"
+#endif
+
+#endif /* HAVE_AV_CONFIG_H */
+
+/*
+ * Map AV_RNXX <-> AV_R[BL]XX for all variants provided by per-arch headers.
+ */
+
+#if AV_HAVE_BIGENDIAN
+
+# if defined(AV_RN16) && !defined(AV_RB16)
+# define AV_RB16(p) AV_RN16(p)
+# elif !defined(AV_RN16) && defined(AV_RB16)
+# define AV_RN16(p) AV_RB16(p)
+# endif
+
+# if defined(AV_WN16) && !defined(AV_WB16)
+# define AV_WB16(p, v) AV_WN16(p, v)
+# elif !defined(AV_WN16) && defined(AV_WB16)
+# define AV_WN16(p, v) AV_WB16(p, v)
+# endif
+
+# if defined(AV_RN24) && !defined(AV_RB24)
+# define AV_RB24(p) AV_RN24(p)
+# elif !defined(AV_RN24) && defined(AV_RB24)
+# define AV_RN24(p) AV_RB24(p)
+# endif
+
+# if defined(AV_WN24) && !defined(AV_WB24)
+# define AV_WB24(p, v) AV_WN24(p, v)
+# elif !defined(AV_WN24) && defined(AV_WB24)
+# define AV_WN24(p, v) AV_WB24(p, v)
+# endif
+
+# if defined(AV_RN32) && !defined(AV_RB32)
+# define AV_RB32(p) AV_RN32(p)
+# elif !defined(AV_RN32) && defined(AV_RB32)
+# define AV_RN32(p) AV_RB32(p)
+# endif
+
+# if defined(AV_WN32) && !defined(AV_WB32)
+# define AV_WB32(p, v) AV_WN32(p, v)
+# elif !defined(AV_WN32) && defined(AV_WB32)
+# define AV_WN32(p, v) AV_WB32(p, v)
+# endif
+
+# if defined(AV_RN64) && !defined(AV_RB64)
+# define AV_RB64(p) AV_RN64(p)
+# elif !defined(AV_RN64) && defined(AV_RB64)
+# define AV_RN64(p) AV_RB64(p)
+# endif
+
+# if defined(AV_WN64) && !defined(AV_WB64)
+# define AV_WB64(p, v) AV_WN64(p, v)
+# elif !defined(AV_WN64) && defined(AV_WB64)
+# define AV_WN64(p, v) AV_WB64(p, v)
+# endif
+
+#else /* AV_HAVE_BIGENDIAN */
+
+# if defined(AV_RN16) && !defined(AV_RL16)
+# define AV_RL16(p) AV_RN16(p)
+# elif !defined(AV_RN16) && defined(AV_RL16)
+# define AV_RN16(p) AV_RL16(p)
+# endif
+
+# if defined(AV_WN16) && !defined(AV_WL16)
+# define AV_WL16(p, v) AV_WN16(p, v)
+# elif !defined(AV_WN16) && defined(AV_WL16)
+# define AV_WN16(p, v) AV_WL16(p, v)
+# endif
+
+# if defined(AV_RN24) && !defined(AV_RL24)
+# define AV_RL24(p) AV_RN24(p)
+# elif !defined(AV_RN24) && defined(AV_RL24)
+# define AV_RN24(p) AV_RL24(p)
+# endif
+
+# if defined(AV_WN24) && !defined(AV_WL24)
+# define AV_WL24(p, v) AV_WN24(p, v)
+# elif !defined(AV_WN24) && defined(AV_WL24)
+# define AV_WN24(p, v) AV_WL24(p, v)
+# endif
+
+# if defined(AV_RN32) && !defined(AV_RL32)
+# define AV_RL32(p) AV_RN32(p)
+# elif !defined(AV_RN32) && defined(AV_RL32)
+# define AV_RN32(p) AV_RL32(p)
+# endif
+
+# if defined(AV_WN32) && !defined(AV_WL32)
+# define AV_WL32(p, v) AV_WN32(p, v)
+# elif !defined(AV_WN32) && defined(AV_WL32)
+# define AV_WN32(p, v) AV_WL32(p, v)
+# endif
+
+# if defined(AV_RN64) && !defined(AV_RL64)
+# define AV_RL64(p) AV_RN64(p)
+# elif !defined(AV_RN64) && defined(AV_RL64)
+# define AV_RN64(p) AV_RL64(p)
+# endif
+
+# if defined(AV_WN64) && !defined(AV_WL64)
+# define AV_WL64(p, v) AV_WN64(p, v)
+# elif !defined(AV_WN64) && defined(AV_WL64)
+# define AV_WN64(p, v) AV_WL64(p, v)
+# endif
+
+#endif /* !AV_HAVE_BIGENDIAN */
+
+/*
+ * Define AV_[RW]N helper macros to simplify definitions not provided
+ * by per-arch headers.
+ */
+
+#if defined(__GNUC__) && !defined(__TI_COMPILER_VERSION__)
+
+union unaligned_64 { uint64_t l; } __attribute__((packed)) av_alias;
+union unaligned_32 { uint32_t l; } __attribute__((packed)) av_alias;
+union unaligned_16 { uint16_t l; } __attribute__((packed)) av_alias;
+
+# define AV_RN(s, p) (((const union unaligned_##s *) (p))->l)
+# define AV_WN(s, p, v) ((((union unaligned_##s *) (p))->l) = (v))
+
+#elif defined(__DECC)
+
+# define AV_RN(s, p) (*((const __unaligned uint##s##_t*)(p)))
+# define AV_WN(s, p, v) (*((__unaligned uint##s##_t*)(p)) = (v))
+
+#elif AV_HAVE_FAST_UNALIGNED
+
+# define AV_RN(s, p) (((const av_alias##s*)(p))->u##s)
+# define AV_WN(s, p, v) (((av_alias##s*)(p))->u##s = (v))
+
+#else
+
+#ifndef AV_RB16
+# define AV_RB16(x) \
+ ((((const uint8_t*)(x))[0] << 8) | \
+ ((const uint8_t*)(x))[1])
+#endif
+#ifndef AV_WB16
+# define AV_WB16(p, d) do { \
+ ((uint8_t*)(p))[1] = (d); \
+ ((uint8_t*)(p))[0] = (d)>>8; \
+ } while(0)
+#endif
+
+#ifndef AV_RL16
+# define AV_RL16(x) \
+ ((((const uint8_t*)(x))[1] << 8) | \
+ ((const uint8_t*)(x))[0])
+#endif
+#ifndef AV_WL16
+# define AV_WL16(p, d) do { \
+ ((uint8_t*)(p))[0] = (d); \
+ ((uint8_t*)(p))[1] = (d)>>8; \
+ } while(0)
+#endif
+
+#ifndef AV_RB32
+# define AV_RB32(x) \
+ (((uint32_t)((const uint8_t*)(x))[0] << 24) | \
+ (((const uint8_t*)(x))[1] << 16) | \
+ (((const uint8_t*)(x))[2] << 8) | \
+ ((const uint8_t*)(x))[3])
+#endif
+#ifndef AV_WB32
+# define AV_WB32(p, d) do { \
+ ((uint8_t*)(p))[3] = (d); \
+ ((uint8_t*)(p))[2] = (d)>>8; \
+ ((uint8_t*)(p))[1] = (d)>>16; \
+ ((uint8_t*)(p))[0] = (d)>>24; \
+ } while(0)
+#endif
+
+#ifndef AV_RL32
+# define AV_RL32(x) \
+ (((uint32_t)((const uint8_t*)(x))[3] << 24) | \
+ (((const uint8_t*)(x))[2] << 16) | \
+ (((const uint8_t*)(x))[1] << 8) | \
+ ((const uint8_t*)(x))[0])
+#endif
+#ifndef AV_WL32
+# define AV_WL32(p, d) do { \
+ ((uint8_t*)(p))[0] = (d); \
+ ((uint8_t*)(p))[1] = (d)>>8; \
+ ((uint8_t*)(p))[2] = (d)>>16; \
+ ((uint8_t*)(p))[3] = (d)>>24; \
+ } while(0)
+#endif
+
+#ifndef AV_RB64
+# define AV_RB64(x) \
+ (((uint64_t)((const uint8_t*)(x))[0] << 56) | \
+ ((uint64_t)((const uint8_t*)(x))[1] << 48) | \
+ ((uint64_t)((const uint8_t*)(x))[2] << 40) | \
+ ((uint64_t)((const uint8_t*)(x))[3] << 32) | \
+ ((uint64_t)((const uint8_t*)(x))[4] << 24) | \
+ ((uint64_t)((const uint8_t*)(x))[5] << 16) | \
+ ((uint64_t)((const uint8_t*)(x))[6] << 8) | \
+ (uint64_t)((const uint8_t*)(x))[7])
+#endif
+#ifndef AV_WB64
+# define AV_WB64(p, d) do { \
+ ((uint8_t*)(p))[7] = (d); \
+ ((uint8_t*)(p))[6] = (d)>>8; \
+ ((uint8_t*)(p))[5] = (d)>>16; \
+ ((uint8_t*)(p))[4] = (d)>>24; \
+ ((uint8_t*)(p))[3] = (d)>>32; \
+ ((uint8_t*)(p))[2] = (d)>>40; \
+ ((uint8_t*)(p))[1] = (d)>>48; \
+ ((uint8_t*)(p))[0] = (d)>>56; \
+ } while(0)
+#endif
+
+#ifndef AV_RL64
+# define AV_RL64(x) \
+ (((uint64_t)((const uint8_t*)(x))[7] << 56) | \
+ ((uint64_t)((const uint8_t*)(x))[6] << 48) | \
+ ((uint64_t)((const uint8_t*)(x))[5] << 40) | \
+ ((uint64_t)((const uint8_t*)(x))[4] << 32) | \
+ ((uint64_t)((const uint8_t*)(x))[3] << 24) | \
+ ((uint64_t)((const uint8_t*)(x))[2] << 16) | \
+ ((uint64_t)((const uint8_t*)(x))[1] << 8) | \
+ (uint64_t)((const uint8_t*)(x))[0])
+#endif
+#ifndef AV_WL64
+# define AV_WL64(p, d) do { \
+ ((uint8_t*)(p))[0] = (d); \
+ ((uint8_t*)(p))[1] = (d)>>8; \
+ ((uint8_t*)(p))[2] = (d)>>16; \
+ ((uint8_t*)(p))[3] = (d)>>24; \
+ ((uint8_t*)(p))[4] = (d)>>32; \
+ ((uint8_t*)(p))[5] = (d)>>40; \
+ ((uint8_t*)(p))[6] = (d)>>48; \
+ ((uint8_t*)(p))[7] = (d)>>56; \
+ } while(0)
+#endif
+
+#if AV_HAVE_BIGENDIAN
+# define AV_RN(s, p) AV_RB##s(p)
+# define AV_WN(s, p, v) AV_WB##s(p, v)
+#else
+# define AV_RN(s, p) AV_RL##s(p)
+# define AV_WN(s, p, v) AV_WL##s(p, v)
+#endif
+
+#endif /* HAVE_FAST_UNALIGNED */
+
+#ifndef AV_RN16
+# define AV_RN16(p) AV_RN(16, p)
+#endif
+
+#ifndef AV_RN32
+# define AV_RN32(p) AV_RN(32, p)
+#endif
+
+#ifndef AV_RN64
+# define AV_RN64(p) AV_RN(64, p)
+#endif
+
+#ifndef AV_WN16
+# define AV_WN16(p, v) AV_WN(16, p, v)
+#endif
+
+#ifndef AV_WN32
+# define AV_WN32(p, v) AV_WN(32, p, v)
+#endif
+
+#ifndef AV_WN64
+# define AV_WN64(p, v) AV_WN(64, p, v)
+#endif
+
+#if AV_HAVE_BIGENDIAN
+# define AV_RB(s, p) AV_RN##s(p)
+# define AV_WB(s, p, v) AV_WN##s(p, v)
+# define AV_RL(s, p) av_bswap##s(AV_RN##s(p))
+# define AV_WL(s, p, v) AV_WN##s(p, av_bswap##s(v))
+#else
+# define AV_RB(s, p) av_bswap##s(AV_RN##s(p))
+# define AV_WB(s, p, v) AV_WN##s(p, av_bswap##s(v))
+# define AV_RL(s, p) AV_RN##s(p)
+# define AV_WL(s, p, v) AV_WN##s(p, v)
+#endif
+
+#define AV_RB8(x) (((const uint8_t*)(x))[0])
+#define AV_WB8(p, d) do { ((uint8_t*)(p))[0] = (d); } while(0)
+
+#define AV_RL8(x) AV_RB8(x)
+#define AV_WL8(p, d) AV_WB8(p, d)
+
+#ifndef AV_RB16
+# define AV_RB16(p) AV_RB(16, p)
+#endif
+#ifndef AV_WB16
+# define AV_WB16(p, v) AV_WB(16, p, v)
+#endif
+
+#ifndef AV_RL16
+# define AV_RL16(p) AV_RL(16, p)
+#endif
+#ifndef AV_WL16
+# define AV_WL16(p, v) AV_WL(16, p, v)
+#endif
+
+#ifndef AV_RB32
+# define AV_RB32(p) AV_RB(32, p)
+#endif
+#ifndef AV_WB32
+# define AV_WB32(p, v) AV_WB(32, p, v)
+#endif
+
+#ifndef AV_RL32
+# define AV_RL32(p) AV_RL(32, p)
+#endif
+#ifndef AV_WL32
+# define AV_WL32(p, v) AV_WL(32, p, v)
+#endif
+
+#ifndef AV_RB64
+# define AV_RB64(p) AV_RB(64, p)
+#endif
+#ifndef AV_WB64
+# define AV_WB64(p, v) AV_WB(64, p, v)
+#endif
+
+#ifndef AV_RL64
+# define AV_RL64(p) AV_RL(64, p)
+#endif
+#ifndef AV_WL64
+# define AV_WL64(p, v) AV_WL(64, p, v)
+#endif
+
+#ifndef AV_RB24
+# define AV_RB24(x) \
+ ((((const uint8_t*)(x))[0] << 16) | \
+ (((const uint8_t*)(x))[1] << 8) | \
+ ((const uint8_t*)(x))[2])
+#endif
+#ifndef AV_WB24
+# define AV_WB24(p, d) do { \
+ ((uint8_t*)(p))[2] = (d); \
+ ((uint8_t*)(p))[1] = (d)>>8; \
+ ((uint8_t*)(p))[0] = (d)>>16; \
+ } while(0)
+#endif
+
+#ifndef AV_RL24
+# define AV_RL24(x) \
+ ((((const uint8_t*)(x))[2] << 16) | \
+ (((const uint8_t*)(x))[1] << 8) | \
+ ((const uint8_t*)(x))[0])
+#endif
+#ifndef AV_WL24
+# define AV_WL24(p, d) do { \
+ ((uint8_t*)(p))[0] = (d); \
+ ((uint8_t*)(p))[1] = (d)>>8; \
+ ((uint8_t*)(p))[2] = (d)>>16; \
+ } while(0)
+#endif
+
+/*
+ * The AV_[RW]NA macros access naturally aligned data
+ * in a type-safe way.
+ */
+
+#define AV_RNA(s, p) (((const av_alias##s*)(p))->u##s)
+#define AV_WNA(s, p, v) (((av_alias##s*)(p))->u##s = (v))
+
+#ifndef AV_RN16A
+# define AV_RN16A(p) AV_RNA(16, p)
+#endif
+
+#ifndef AV_RN32A
+# define AV_RN32A(p) AV_RNA(32, p)
+#endif
+
+#ifndef AV_RN64A
+# define AV_RN64A(p) AV_RNA(64, p)
+#endif
+
+#ifndef AV_WN16A
+# define AV_WN16A(p, v) AV_WNA(16, p, v)
+#endif
+
+#ifndef AV_WN32A
+# define AV_WN32A(p, v) AV_WNA(32, p, v)
+#endif
+
+#ifndef AV_WN64A
+# define AV_WN64A(p, v) AV_WNA(64, p, v)
+#endif
+
+/* Parameters for AV_COPY*, AV_SWAP*, AV_ZERO* must be
+ * naturally aligned. They may be implemented using MMX,
+ * so emms_c() must be called before using any float code
+ * afterwards.
+ */
+
+#define AV_COPY(n, d, s) \
+ (((av_alias##n*)(d))->u##n = ((const av_alias##n*)(s))->u##n)
+
+#ifndef AV_COPY16
+# define AV_COPY16(d, s) AV_COPY(16, d, s)
+#endif
+
+#ifndef AV_COPY32
+# define AV_COPY32(d, s) AV_COPY(32, d, s)
+#endif
+
+#ifndef AV_COPY64
+# define AV_COPY64(d, s) AV_COPY(64, d, s)
+#endif
+
+#ifndef AV_COPY128
+# define AV_COPY128(d, s) \
+ do { \
+ AV_COPY64(d, s); \
+ AV_COPY64((char*)(d)+8, (char*)(s)+8); \
+ } while(0)
+#endif
+
+#define AV_SWAP(n, a, b) FFSWAP(av_alias##n, *(av_alias##n*)(a), *(av_alias##n*)(b))
+
+#ifndef AV_SWAP64
+# define AV_SWAP64(a, b) AV_SWAP(64, a, b)
+#endif
+
+#define AV_ZERO(n, d) (((av_alias##n*)(d))->u##n = 0)
+
+#ifndef AV_ZERO16
+# define AV_ZERO16(d) AV_ZERO(16, d)
+#endif
+
+#ifndef AV_ZERO32
+# define AV_ZERO32(d) AV_ZERO(32, d)
+#endif
+
+#ifndef AV_ZERO64
+# define AV_ZERO64(d) AV_ZERO(64, d)
+#endif
+
+#ifndef AV_ZERO128
+# define AV_ZERO128(d) \
+ do { \
+ AV_ZERO64(d); \
+ AV_ZERO64((char*)(d)+8); \
+ } while(0)
+#endif
+
+#endif /* AVUTIL_INTREADWRITE_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/lfg.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/lfg.h
new file mode 100644
index 0000000000..904d00a669
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/lfg.h
@@ -0,0 +1,62 @@
+/*
+ * Lagged Fibonacci PRNG
+ * Copyright (c) 2008 Michael Niedermayer
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_LFG_H
+#define AVUTIL_LFG_H
+
+typedef struct {
+ unsigned int state[64];
+ int index;
+} AVLFG;
+
+void av_lfg_init(AVLFG *c, unsigned int seed);
+
+/**
+ * Get the next random unsigned 32-bit number using an ALFG.
+ *
+ * Please also consider a simple LCG like state= state*1664525+1013904223,
+ * it may be good enough and faster for your specific use case.
+ */
+static inline unsigned int av_lfg_get(AVLFG *c){
+ c->state[c->index & 63] = c->state[(c->index-24) & 63] + c->state[(c->index-55) & 63];
+ return c->state[c->index++ & 63];
+}
+
+/**
+ * Get the next random unsigned 32-bit number using a MLFG.
+ *
+ * Please also consider av_lfg_get() above, it is faster.
+ */
+static inline unsigned int av_mlfg_get(AVLFG *c){
+ unsigned int a= c->state[(c->index-55) & 63];
+ unsigned int b= c->state[(c->index-24) & 63];
+ return c->state[c->index++ & 63] = 2*a*b+a+b;
+}
+
+/**
+ * Get the next two numbers generated by a Box-Muller Gaussian
+ * generator using the random numbers issued by lfg.
+ *
+ * @param out array where the two generated numbers are placed
+ */
+void av_bmg_get(AVLFG *lfg, double out[2]);
+
+#endif /* AVUTIL_LFG_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/log.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/log.h
new file mode 100644
index 0000000000..0678e1a3b0
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/log.h
@@ -0,0 +1,172 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_LOG_H
+#define AVUTIL_LOG_H
+
+#include <stdarg.h>
+#include "avutil.h"
+#include "attributes.h"
+
+/**
+ * Describe the class of an AVClass context structure. That is an
+ * arbitrary struct of which the first field is a pointer to an
+ * AVClass struct (e.g. AVCodecContext, AVFormatContext etc.).
+ */
+typedef struct AVClass {
+ /**
+ * The name of the class; usually it is the same name as the
+ * context structure type to which the AVClass is associated.
+ */
+ const char* class_name;
+
+ /**
+ * A pointer to a function which returns the name of a context
+ * instance ctx associated with the class.
+ */
+ const char* (*item_name)(void* ctx);
+
+ /**
+ * a pointer to the first option specified in the class if any or NULL
+ *
+ * @see av_set_default_options()
+ */
+ const struct AVOption *option;
+
+ /**
+ * LIBAVUTIL_VERSION with which this structure was created.
+ * This is used to allow fields to be added without requiring major
+ * version bumps everywhere.
+ */
+
+ int version;
+
+ /**
+ * Offset in the structure where log_level_offset is stored.
+ * 0 means there is no such variable
+ */
+ int log_level_offset_offset;
+
+ /**
+ * Offset in the structure where a pointer to the parent context for loging is stored.
+ * for example a decoder that uses eval.c could pass its AVCodecContext to eval as such
+ * parent context. And a av_log() implementation could then display the parent context
+ * can be NULL of course
+ */
+ int parent_log_context_offset;
+
+ /**
+ * Return next AVOptions-enabled child or NULL
+ */
+ void* (*child_next)(void *obj, void *prev);
+
+ /**
+ * Return an AVClass corresponding to next potential
+ * AVOptions-enabled child.
+ *
+ * The difference between child_next and this is that
+ * child_next iterates over _already existing_ objects, while
+ * child_class_next iterates over _all possible_ children.
+ */
+ const struct AVClass* (*child_class_next)(const struct AVClass *prev);
+} AVClass;
+
+/* av_log API */
+
+#define AV_LOG_QUIET -8
+
+/**
+ * Something went really wrong and we will crash now.
+ */
+#define AV_LOG_PANIC 0
+
+/**
+ * Something went wrong and recovery is not possible.
+ * For example, no header was found for a format which depends
+ * on headers or an illegal combination of parameters is used.
+ */
+#define AV_LOG_FATAL 8
+
+/**
+ * Something went wrong and cannot losslessly be recovered.
+ * However, not all future data is affected.
+ */
+#define AV_LOG_ERROR 16
+
+/**
+ * Something somehow does not look correct. This may or may not
+ * lead to problems. An example would be the use of '-vstrict -2'.
+ */
+#define AV_LOG_WARNING 24
+
+#define AV_LOG_INFO 32
+#define AV_LOG_VERBOSE 40
+
+/**
+ * Stuff which is only useful for libav* developers.
+ */
+#define AV_LOG_DEBUG 48
+
+/**
+ * Send the specified message to the log if the level is less than or equal
+ * to the current av_log_level. By default, all logging messages are sent to
+ * stderr. This behavior can be altered by setting a different av_vlog callback
+ * function.
+ *
+ * @param avcl A pointer to an arbitrary struct of which the first field is a
+ * pointer to an AVClass struct.
+ * @param level The importance level of the message, lower values signifying
+ * higher importance.
+ * @param fmt The format string (printf-compatible) that specifies how
+ * subsequent arguments are converted to output.
+ * @see av_vlog
+ */
+void av_log(void *avcl, int level, const char *fmt, ...) av_printf_format(3, 4);
+
+void av_vlog(void *avcl, int level, const char *fmt, va_list);
+int av_log_get_level(void);
+void av_log_set_level(int);
+void av_log_set_callback(void (*)(void*, int, const char*, va_list));
+void av_log_default_callback(void* ptr, int level, const char* fmt, va_list vl);
+const char* av_default_item_name(void* ctx);
+
+/**
+ * av_dlog macros
+ * Useful to print debug messages that shouldn't get compiled in normally.
+ */
+
+#ifdef DEBUG
+# define av_dlog(pctx, ...) av_log(pctx, AV_LOG_DEBUG, __VA_ARGS__)
+#else
+# define av_dlog(pctx, ...)
+#endif
+
+/**
+ * Skip repeated messages, this requires the user app to use av_log() instead of
+ * (f)printf as the 2 would otherwise interfere and lead to
+ * "Last message repeated x times" messages below (f)printf messages with some
+ * bad luck.
+ * Also to receive the last, "last repeated" line if any, the user app must
+ * call av_log(NULL, AV_LOG_QUIET, ""); at the end
+ */
+#define AV_LOG_SKIP_REPEATED 1
+void av_log_set_flags(int arg);
+
+#endif /* AVUTIL_LOG_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/lzo.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/lzo.h
new file mode 100644
index 0000000000..1b774a53bc
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/lzo.h
@@ -0,0 +1,77 @@
+/*
+ * LZO 1x decompression
+ * copyright (c) 2006 Reimar Doeffinger
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_LZO_H
+#define AVUTIL_LZO_H
+
+/**
+ * @defgroup lavu_lzo LZO
+ * @ingroup lavu_crypto
+ *
+ * @{
+ */
+
+#include <stdint.h>
+
+/** @name Error flags returned by av_lzo1x_decode
+ * @{ */
+/// end of the input buffer reached before decoding finished
+#define AV_LZO_INPUT_DEPLETED 1
+/// decoded data did not fit into output buffer
+#define AV_LZO_OUTPUT_FULL 2
+/// a reference to previously decoded data was wrong
+#define AV_LZO_INVALID_BACKPTR 4
+/// a non-specific error in the compressed bitstream
+#define AV_LZO_ERROR 8
+/** @} */
+
+#define AV_LZO_INPUT_PADDING 8
+#define AV_LZO_OUTPUT_PADDING 12
+
+/**
+ * @brief Decodes LZO 1x compressed data.
+ * @param out output buffer
+ * @param outlen size of output buffer, number of bytes left are returned here
+ * @param in input buffer
+ * @param inlen size of input buffer, number of bytes left are returned here
+ * @return 0 on success, otherwise a combination of the error flags above
+ *
+ * Make sure all buffers are appropriately padded, in must provide
+ * AV_LZO_INPUT_PADDING, out must provide AV_LZO_OUTPUT_PADDING additional bytes.
+ */
+int av_lzo1x_decode(void *out, int *outlen, const void *in, int *inlen);
+
+/**
+ * @brief deliberately overlapping memcpy implementation
+ * @param dst destination buffer; must be padded with 12 additional bytes
+ * @param back how many bytes back we start (the initial size of the overlapping window)
+ * @param cnt number of bytes to copy, must be >= 0
+ *
+ * cnt > back is valid, this will copy the bytes we just copied,
+ * thus creating a repeating pattern with a period length of back.
+ */
+void av_memcpy_backptr(uint8_t *dst, int back, int cnt);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_LZO_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/mathematics.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/mathematics.h
new file mode 100644
index 0000000000..0b072ebe63
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/mathematics.h
@@ -0,0 +1,122 @@
+/*
+ * copyright (c) 2005 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_MATHEMATICS_H
+#define AVUTIL_MATHEMATICS_H
+
+#include <stdint.h>
+#include <math.h>
+#include "attributes.h"
+#include "rational.h"
+
+#ifndef M_E
+#define M_E 2.7182818284590452354 /* e */
+#endif
+#ifndef M_LN2
+#define M_LN2 0.69314718055994530942 /* log_e 2 */
+#endif
+#ifndef M_LN10
+#define M_LN10 2.30258509299404568402 /* log_e 10 */
+#endif
+#ifndef M_LOG2_10
+#define M_LOG2_10 3.32192809488736234787 /* log_2 10 */
+#endif
+#ifndef M_PHI
+#define M_PHI 1.61803398874989484820 /* phi / golden ratio */
+#endif
+#ifndef M_PI
+#define M_PI 3.14159265358979323846 /* pi */
+#endif
+#ifndef M_SQRT1_2
+#define M_SQRT1_2 0.70710678118654752440 /* 1/sqrt(2) */
+#endif
+#ifndef M_SQRT2
+#define M_SQRT2 1.41421356237309504880 /* sqrt(2) */
+#endif
+#ifndef NAN
+#define NAN (0.0/0.0)
+#endif
+#ifndef INFINITY
+#define INFINITY (1.0/0.0)
+#endif
+
+/**
+ * @addtogroup lavu_math
+ * @{
+ */
+
+
+enum AVRounding {
+ AV_ROUND_ZERO = 0, ///< Round toward zero.
+ AV_ROUND_INF = 1, ///< Round away from zero.
+ AV_ROUND_DOWN = 2, ///< Round toward -infinity.
+ AV_ROUND_UP = 3, ///< Round toward +infinity.
+ AV_ROUND_NEAR_INF = 5, ///< Round to nearest and halfway cases away from zero.
+};
+
+/**
+ * Return the greatest common divisor of a and b.
+ * If both a and b are 0 or either or both are <0 then behavior is
+ * undefined.
+ */
+int64_t av_const av_gcd(int64_t a, int64_t b);
+
+/**
+ * Rescale a 64-bit integer with rounding to nearest.
+ * A simple a*b/c isn't possible as it can overflow.
+ */
+int64_t av_rescale(int64_t a, int64_t b, int64_t c) av_const;
+
+/**
+ * Rescale a 64-bit integer with specified rounding.
+ * A simple a*b/c isn't possible as it can overflow.
+ */
+int64_t av_rescale_rnd(int64_t a, int64_t b, int64_t c, enum AVRounding) av_const;
+
+/**
+ * Rescale a 64-bit integer by 2 rational numbers.
+ */
+int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq) av_const;
+
+/**
+ * Compare 2 timestamps each in its own timebases.
+ * The result of the function is undefined if one of the timestamps
+ * is outside the int64_t range when represented in the others timebase.
+ * @return -1 if ts_a is before ts_b, 1 if ts_a is after ts_b or 0 if they represent the same position
+ */
+int av_compare_ts(int64_t ts_a, AVRational tb_a, int64_t ts_b, AVRational tb_b);
+
+/**
+ * Compare 2 integers modulo mod.
+ * That is we compare integers a and b for which only the least
+ * significant log2(mod) bits are known.
+ *
+ * @param mod must be a power of 2
+ * @return a negative value if a is smaller than b
+ * a positive value if a is greater than b
+ * 0 if a equals b
+ */
+int64_t av_compare_mod(uint64_t a, uint64_t b, uint64_t mod);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_MATHEMATICS_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/md5.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/md5.h
new file mode 100644
index 0000000000..1412ee2401
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/md5.h
@@ -0,0 +1,46 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_MD5_H
+#define AVUTIL_MD5_H
+
+#include <stdint.h>
+
+/**
+ * @defgroup lavu_md5 MD5
+ * @ingroup lavu_crypto
+ * @{
+ */
+
+extern const int av_md5_size;
+
+struct AVMD5;
+
+void av_md5_init(struct AVMD5 *ctx);
+void av_md5_update(struct AVMD5 *ctx, const uint8_t *src, const int len);
+void av_md5_final(struct AVMD5 *ctx, uint8_t *dst);
+void av_md5_sum(uint8_t *dst, const uint8_t *src, const int len);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_MD5_H */
+
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/mem.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/mem.h
new file mode 100644
index 0000000000..cd8490b2da
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/mem.h
@@ -0,0 +1,136 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * memory handling functions
+ */
+
+#ifndef AVUTIL_MEM_H
+#define AVUTIL_MEM_H
+
+#include "attributes.h"
+#include "avutil.h"
+
+/**
+ * @addtogroup lavu_mem
+ * @{
+ */
+
+
+#if defined(__ICC) && _ICC < 1200 || defined(__SUNPRO_C)
+ #define DECLARE_ALIGNED(n,t,v) t __attribute__ ((aligned (n))) v
+ #define DECLARE_ASM_CONST(n,t,v) const t __attribute__ ((aligned (n))) v
+#elif defined(__TI_COMPILER_VERSION__)
+ #define DECLARE_ALIGNED(n,t,v) \
+ AV_PRAGMA(DATA_ALIGN(v,n)) \
+ t __attribute__((aligned(n))) v
+ #define DECLARE_ASM_CONST(n,t,v) \
+ AV_PRAGMA(DATA_ALIGN(v,n)) \
+ static const t __attribute__((aligned(n))) v
+#elif defined(__GNUC__)
+ #define DECLARE_ALIGNED(n,t,v) t __attribute__ ((aligned (n))) v
+ #define DECLARE_ASM_CONST(n,t,v) static const t av_used __attribute__ ((aligned (n))) v
+#elif defined(_MSC_VER)
+ #define DECLARE_ALIGNED(n,t,v) __declspec(align(n)) t v
+ #define DECLARE_ASM_CONST(n,t,v) __declspec(align(n)) static const t v
+#else
+ #define DECLARE_ALIGNED(n,t,v) t v
+ #define DECLARE_ASM_CONST(n,t,v) static const t v
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3,1)
+ #define av_malloc_attrib __attribute__((__malloc__))
+#else
+ #define av_malloc_attrib
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(4,3)
+ #define av_alloc_size(n) __attribute__((alloc_size(n)))
+#else
+ #define av_alloc_size(n)
+#endif
+
+/**
+ * Allocate a block of size bytes with alignment suitable for all
+ * memory accesses (including vectors if available on the CPU).
+ * @param size Size in bytes for the memory block to be allocated.
+ * @return Pointer to the allocated block, NULL if the block cannot
+ * be allocated.
+ * @see av_mallocz()
+ */
+void *av_malloc(size_t size) av_malloc_attrib av_alloc_size(1);
+
+/**
+ * Allocate or reallocate a block of memory.
+ * If ptr is NULL and size > 0, allocate a new block. If
+ * size is zero, free the memory block pointed to by ptr.
+ * @param ptr Pointer to a memory block already allocated with
+ * av_malloc(z)() or av_realloc() or NULL.
+ * @param size Size in bytes for the memory block to be allocated or
+ * reallocated.
+ * @return Pointer to a newly reallocated block or NULL if the block
+ * cannot be reallocated or the function is used to free the memory block.
+ * @see av_fast_realloc()
+ */
+void *av_realloc(void *ptr, size_t size) av_alloc_size(2);
+
+/**
+ * Free a memory block which has been allocated with av_malloc(z)() or
+ * av_realloc().
+ * @param ptr Pointer to the memory block which should be freed.
+ * @note ptr = NULL is explicitly allowed.
+ * @note It is recommended that you use av_freep() instead.
+ * @see av_freep()
+ */
+void av_free(void *ptr);
+
+/**
+ * Allocate a block of size bytes with alignment suitable for all
+ * memory accesses (including vectors if available on the CPU) and
+ * zero all the bytes of the block.
+ * @param size Size in bytes for the memory block to be allocated.
+ * @return Pointer to the allocated block, NULL if it cannot be allocated.
+ * @see av_malloc()
+ */
+void *av_mallocz(size_t size) av_malloc_attrib av_alloc_size(1);
+
+/**
+ * Duplicate the string s.
+ * @param s string to be duplicated
+ * @return Pointer to a newly allocated string containing a
+ * copy of s or NULL if the string cannot be allocated.
+ */
+char *av_strdup(const char *s) av_malloc_attrib;
+
+/**
+ * Free a memory block which has been allocated with av_malloc(z)() or
+ * av_realloc() and set the pointer pointing to it to NULL.
+ * @param ptr Pointer to the pointer to the memory block which should
+ * be freed.
+ * @see av_free()
+ */
+void av_freep(void *ptr);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_MEM_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/old_pix_fmts.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/old_pix_fmts.h
new file mode 100644
index 0000000000..57b699220f
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/old_pix_fmts.h
@@ -0,0 +1,171 @@
+/*
+ * copyright (c) 2006-2012 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_OLD_PIX_FMTS_H
+#define AVUTIL_OLD_PIX_FMTS_H
+
+/*
+ * This header exists to prevent new pixel formats from being accidentally added
+ * to the deprecated list.
+ * Do not include it directly. It will be removed on next major bump
+ *
+ * Do not add new items to this list. Use the AVPixelFormat enum instead.
+ */
+ PIX_FMT_NONE = AV_PIX_FMT_NONE,
+ PIX_FMT_YUV420P, ///< planar YUV 4:2:0, 12bpp, (1 Cr & Cb sample per 2x2 Y samples)
+ PIX_FMT_YUYV422, ///< packed YUV 4:2:2, 16bpp, Y0 Cb Y1 Cr
+ PIX_FMT_RGB24, ///< packed RGB 8:8:8, 24bpp, RGBRGB...
+ PIX_FMT_BGR24, ///< packed RGB 8:8:8, 24bpp, BGRBGR...
+ PIX_FMT_YUV422P, ///< planar YUV 4:2:2, 16bpp, (1 Cr & Cb sample per 2x1 Y samples)
+ PIX_FMT_YUV444P, ///< planar YUV 4:4:4, 24bpp, (1 Cr & Cb sample per 1x1 Y samples)
+ PIX_FMT_YUV410P, ///< planar YUV 4:1:0, 9bpp, (1 Cr & Cb sample per 4x4 Y samples)
+ PIX_FMT_YUV411P, ///< planar YUV 4:1:1, 12bpp, (1 Cr & Cb sample per 4x1 Y samples)
+ PIX_FMT_GRAY8, ///< Y , 8bpp
+ PIX_FMT_MONOWHITE, ///< Y , 1bpp, 0 is white, 1 is black, in each byte pixels are ordered from the msb to the lsb
+ PIX_FMT_MONOBLACK, ///< Y , 1bpp, 0 is black, 1 is white, in each byte pixels are ordered from the msb to the lsb
+ PIX_FMT_PAL8, ///< 8 bit with PIX_FMT_RGB32 palette
+ PIX_FMT_YUVJ420P, ///< planar YUV 4:2:0, 12bpp, full scale (JPEG), deprecated in favor of PIX_FMT_YUV420P and setting color_range
+ PIX_FMT_YUVJ422P, ///< planar YUV 4:2:2, 16bpp, full scale (JPEG), deprecated in favor of PIX_FMT_YUV422P and setting color_range
+ PIX_FMT_YUVJ444P, ///< planar YUV 4:4:4, 24bpp, full scale (JPEG), deprecated in favor of PIX_FMT_YUV444P and setting color_range
+ PIX_FMT_XVMC_MPEG2_MC,///< XVideo Motion Acceleration via common packet passing
+ PIX_FMT_XVMC_MPEG2_IDCT,
+ PIX_FMT_UYVY422, ///< packed YUV 4:2:2, 16bpp, Cb Y0 Cr Y1
+ PIX_FMT_UYYVYY411, ///< packed YUV 4:1:1, 12bpp, Cb Y0 Y1 Cr Y2 Y3
+ PIX_FMT_BGR8, ///< packed RGB 3:3:2, 8bpp, (msb)2B 3G 3R(lsb)
+ PIX_FMT_BGR4, ///< packed RGB 1:2:1 bitstream, 4bpp, (msb)1B 2G 1R(lsb), a byte contains two pixels, the first pixel in the byte is the one composed by the 4 msb bits
+ PIX_FMT_BGR4_BYTE, ///< packed RGB 1:2:1, 8bpp, (msb)1B 2G 1R(lsb)
+ PIX_FMT_RGB8, ///< packed RGB 3:3:2, 8bpp, (msb)2R 3G 3B(lsb)
+ PIX_FMT_RGB4, ///< packed RGB 1:2:1 bitstream, 4bpp, (msb)1R 2G 1B(lsb), a byte contains two pixels, the first pixel in the byte is the one composed by the 4 msb bits
+ PIX_FMT_RGB4_BYTE, ///< packed RGB 1:2:1, 8bpp, (msb)1R 2G 1B(lsb)
+ PIX_FMT_NV12, ///< planar YUV 4:2:0, 12bpp, 1 plane for Y and 1 plane for the UV components, which are interleaved (first byte U and the following byte V)
+ PIX_FMT_NV21, ///< as above, but U and V bytes are swapped
+
+ PIX_FMT_ARGB, ///< packed ARGB 8:8:8:8, 32bpp, ARGBARGB...
+ PIX_FMT_RGBA, ///< packed RGBA 8:8:8:8, 32bpp, RGBARGBA...
+ PIX_FMT_ABGR, ///< packed ABGR 8:8:8:8, 32bpp, ABGRABGR...
+ PIX_FMT_BGRA, ///< packed BGRA 8:8:8:8, 32bpp, BGRABGRA...
+
+ PIX_FMT_GRAY16BE, ///< Y , 16bpp, big-endian
+ PIX_FMT_GRAY16LE, ///< Y , 16bpp, little-endian
+ PIX_FMT_YUV440P, ///< planar YUV 4:4:0 (1 Cr & Cb sample per 1x2 Y samples)
+ PIX_FMT_YUVJ440P, ///< planar YUV 4:4:0 full scale (JPEG), deprecated in favor of PIX_FMT_YUV440P and setting color_range
+ PIX_FMT_YUVA420P, ///< planar YUV 4:2:0, 20bpp, (1 Cr & Cb sample per 2x2 Y & A samples)
+ PIX_FMT_VDPAU_H264,///< H.264 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ PIX_FMT_VDPAU_MPEG1,///< MPEG-1 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ PIX_FMT_VDPAU_MPEG2,///< MPEG-2 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ PIX_FMT_VDPAU_WMV3,///< WMV3 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ PIX_FMT_VDPAU_VC1, ///< VC-1 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ PIX_FMT_RGB48BE, ///< packed RGB 16:16:16, 48bpp, 16R, 16G, 16B, the 2-byte value for each R/G/B component is stored as big-endian
+ PIX_FMT_RGB48LE, ///< packed RGB 16:16:16, 48bpp, 16R, 16G, 16B, the 2-byte value for each R/G/B component is stored as little-endian
+
+ PIX_FMT_RGB565BE, ///< packed RGB 5:6:5, 16bpp, (msb) 5R 6G 5B(lsb), big-endian
+ PIX_FMT_RGB565LE, ///< packed RGB 5:6:5, 16bpp, (msb) 5R 6G 5B(lsb), little-endian
+ PIX_FMT_RGB555BE, ///< packed RGB 5:5:5, 16bpp, (msb)1A 5R 5G 5B(lsb), big-endian, most significant bit to 0
+ PIX_FMT_RGB555LE, ///< packed RGB 5:5:5, 16bpp, (msb)1A 5R 5G 5B(lsb), little-endian, most significant bit to 0
+
+ PIX_FMT_BGR565BE, ///< packed BGR 5:6:5, 16bpp, (msb) 5B 6G 5R(lsb), big-endian
+ PIX_FMT_BGR565LE, ///< packed BGR 5:6:5, 16bpp, (msb) 5B 6G 5R(lsb), little-endian
+ PIX_FMT_BGR555BE, ///< packed BGR 5:5:5, 16bpp, (msb)1A 5B 5G 5R(lsb), big-endian, most significant bit to 1
+ PIX_FMT_BGR555LE, ///< packed BGR 5:5:5, 16bpp, (msb)1A 5B 5G 5R(lsb), little-endian, most significant bit to 1
+
+ PIX_FMT_VAAPI_MOCO, ///< HW acceleration through VA API at motion compensation entry-point, Picture.data[3] contains a vaapi_render_state struct which contains macroblocks as well as various fields extracted from headers
+ PIX_FMT_VAAPI_IDCT, ///< HW acceleration through VA API at IDCT entry-point, Picture.data[3] contains a vaapi_render_state struct which contains fields extracted from headers
+ PIX_FMT_VAAPI_VLD, ///< HW decoding through VA API, Picture.data[3] contains a vaapi_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+
+ PIX_FMT_YUV420P16LE, ///< planar YUV 4:2:0, 24bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian
+ PIX_FMT_YUV420P16BE, ///< planar YUV 4:2:0, 24bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian
+ PIX_FMT_YUV422P16LE, ///< planar YUV 4:2:2, 32bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ PIX_FMT_YUV422P16BE, ///< planar YUV 4:2:2, 32bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+ PIX_FMT_YUV444P16LE, ///< planar YUV 4:4:4, 48bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian
+ PIX_FMT_YUV444P16BE, ///< planar YUV 4:4:4, 48bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian
+ PIX_FMT_VDPAU_MPEG4, ///< MPEG4 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ PIX_FMT_DXVA2_VLD, ///< HW decoding through DXVA2, Picture.data[3] contains a LPDIRECT3DSURFACE9 pointer
+
+ PIX_FMT_RGB444LE, ///< packed RGB 4:4:4, 16bpp, (msb)4A 4R 4G 4B(lsb), little-endian, most significant bits to 0
+ PIX_FMT_RGB444BE, ///< packed RGB 4:4:4, 16bpp, (msb)4A 4R 4G 4B(lsb), big-endian, most significant bits to 0
+ PIX_FMT_BGR444LE, ///< packed BGR 4:4:4, 16bpp, (msb)4A 4B 4G 4R(lsb), little-endian, most significant bits to 1
+ PIX_FMT_BGR444BE, ///< packed BGR 4:4:4, 16bpp, (msb)4A 4B 4G 4R(lsb), big-endian, most significant bits to 1
+ PIX_FMT_GRAY8A, ///< 8bit gray, 8bit alpha
+ PIX_FMT_BGR48BE, ///< packed RGB 16:16:16, 48bpp, 16B, 16G, 16R, the 2-byte value for each R/G/B component is stored as big-endian
+ PIX_FMT_BGR48LE, ///< packed RGB 16:16:16, 48bpp, 16B, 16G, 16R, the 2-byte value for each R/G/B component is stored as little-endian
+
+ //the following 10 formats have the disadvantage of needing 1 format for each bit depth, thus
+ //If you want to support multiple bit depths, then using PIX_FMT_YUV420P16* with the bpp stored separately
+ //is better
+ PIX_FMT_YUV420P9BE, ///< planar YUV 4:2:0, 13.5bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian
+ PIX_FMT_YUV420P9LE, ///< planar YUV 4:2:0, 13.5bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian
+ PIX_FMT_YUV420P10BE,///< planar YUV 4:2:0, 15bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian
+ PIX_FMT_YUV420P10LE,///< planar YUV 4:2:0, 15bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian
+ PIX_FMT_YUV422P10BE,///< planar YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+ PIX_FMT_YUV422P10LE,///< planar YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ PIX_FMT_YUV444P9BE, ///< planar YUV 4:4:4, 27bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian
+ PIX_FMT_YUV444P9LE, ///< planar YUV 4:4:4, 27bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian
+ PIX_FMT_YUV444P10BE,///< planar YUV 4:4:4, 30bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian
+ PIX_FMT_YUV444P10LE,///< planar YUV 4:4:4, 30bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian
+ PIX_FMT_YUV422P9BE, ///< planar YUV 4:2:2, 18bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+ PIX_FMT_YUV422P9LE, ///< planar YUV 4:2:2, 18bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ PIX_FMT_VDA_VLD, ///< hardware decoding through VDA
+
+#ifdef AV_PIX_FMT_ABI_GIT_MASTER
+ PIX_FMT_RGBA64BE, ///< packed RGBA 16:16:16:16, 64bpp, 16R, 16G, 16B, 16A, the 2-byte value for each R/G/B/A component is stored as big-endian
+ PIX_FMT_RGBA64LE, ///< packed RGBA 16:16:16:16, 64bpp, 16R, 16G, 16B, 16A, the 2-byte value for each R/G/B/A component is stored as little-endian
+ PIX_FMT_BGRA64BE, ///< packed RGBA 16:16:16:16, 64bpp, 16B, 16G, 16R, 16A, the 2-byte value for each R/G/B/A component is stored as big-endian
+ PIX_FMT_BGRA64LE, ///< packed RGBA 16:16:16:16, 64bpp, 16B, 16G, 16R, 16A, the 2-byte value for each R/G/B/A component is stored as little-endian
+#endif
+ PIX_FMT_GBRP, ///< planar GBR 4:4:4 24bpp
+ PIX_FMT_GBRP9BE, ///< planar GBR 4:4:4 27bpp, big endian
+ PIX_FMT_GBRP9LE, ///< planar GBR 4:4:4 27bpp, little endian
+ PIX_FMT_GBRP10BE, ///< planar GBR 4:4:4 30bpp, big endian
+ PIX_FMT_GBRP10LE, ///< planar GBR 4:4:4 30bpp, little endian
+ PIX_FMT_GBRP16BE, ///< planar GBR 4:4:4 48bpp, big endian
+ PIX_FMT_GBRP16LE, ///< planar GBR 4:4:4 48bpp, little endian
+
+#ifndef AV_PIX_FMT_ABI_GIT_MASTER
+ PIX_FMT_RGBA64BE=0x123, ///< packed RGBA 16:16:16:16, 64bpp, 16R, 16G, 16B, 16A, the 2-byte value for each R/G/B/A component is stored as big-endian
+ PIX_FMT_RGBA64LE, ///< packed RGBA 16:16:16:16, 64bpp, 16R, 16G, 16B, 16A, the 2-byte value for each R/G/B/A component is stored as little-endian
+ PIX_FMT_BGRA64BE, ///< packed RGBA 16:16:16:16, 64bpp, 16B, 16G, 16R, 16A, the 2-byte value for each R/G/B/A component is stored as big-endian
+ PIX_FMT_BGRA64LE, ///< packed RGBA 16:16:16:16, 64bpp, 16B, 16G, 16R, 16A, the 2-byte value for each R/G/B/A component is stored as little-endian
+#endif
+ PIX_FMT_0RGB=0x123+4, ///< packed RGB 8:8:8, 32bpp, 0RGB0RGB...
+ PIX_FMT_RGB0, ///< packed RGB 8:8:8, 32bpp, RGB0RGB0...
+ PIX_FMT_0BGR, ///< packed BGR 8:8:8, 32bpp, 0BGR0BGR...
+ PIX_FMT_BGR0, ///< packed BGR 8:8:8, 32bpp, BGR0BGR0...
+ PIX_FMT_YUVA444P, ///< planar YUV 4:4:4 32bpp, (1 Cr & Cb sample per 1x1 Y & A samples)
+ PIX_FMT_YUVA422P, ///< planar YUV 4:2:2 24bpp, (1 Cr & Cb sample per 2x1 Y & A samples)
+
+ PIX_FMT_YUV420P12BE, ///< planar YUV 4:2:0,18bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian
+ PIX_FMT_YUV420P12LE, ///< planar YUV 4:2:0,18bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian
+ PIX_FMT_YUV420P14BE, ///< planar YUV 4:2:0,21bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian
+ PIX_FMT_YUV420P14LE, ///< planar YUV 4:2:0,21bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian
+ PIX_FMT_YUV422P12BE, ///< planar YUV 4:2:2,24bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+ PIX_FMT_YUV422P12LE, ///< planar YUV 4:2:2,24bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ PIX_FMT_YUV422P14BE, ///< planar YUV 4:2:2,28bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+ PIX_FMT_YUV422P14LE, ///< planar YUV 4:2:2,28bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ PIX_FMT_YUV444P12BE, ///< planar YUV 4:4:4,36bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian
+ PIX_FMT_YUV444P12LE, ///< planar YUV 4:4:4,36bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian
+ PIX_FMT_YUV444P14BE, ///< planar YUV 4:4:4,42bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian
+ PIX_FMT_YUV444P14LE, ///< planar YUV 4:4:4,42bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian
+ PIX_FMT_GBRP12BE, ///< planar GBR 4:4:4 36bpp, big endian
+ PIX_FMT_GBRP12LE, ///< planar GBR 4:4:4 36bpp, little endian
+ PIX_FMT_GBRP14BE, ///< planar GBR 4:4:4 42bpp, big endian
+ PIX_FMT_GBRP14LE, ///< planar GBR 4:4:4 42bpp, little endian
+
+ PIX_FMT_NB, ///< number of pixel formats, DO NOT USE THIS if you want to link with shared libav* because the number of formats might differ between versions
+#endif /* AVUTIL_OLD_PIX_FMTS_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/opt.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/opt.h
new file mode 100644
index 0000000000..19549408e2
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/opt.h
@@ -0,0 +1,591 @@
+/*
+ * AVOptions
+ * copyright (c) 2005 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_OPT_H
+#define AVUTIL_OPT_H
+
+/**
+ * @file
+ * AVOptions
+ */
+
+#include "rational.h"
+#include "avutil.h"
+#include "dict.h"
+#include "log.h"
+
+/**
+ * @defgroup avoptions AVOptions
+ * @ingroup lavu_data
+ * @{
+ * AVOptions provide a generic system to declare options on arbitrary structs
+ * ("objects"). An option can have a help text, a type and a range of possible
+ * values. Options may then be enumerated, read and written to.
+ *
+ * @section avoptions_implement Implementing AVOptions
+ * This section describes how to add AVOptions capabilities to a struct.
+ *
+ * All AVOptions-related information is stored in an AVClass. Therefore
+ * the first member of the struct must be a pointer to an AVClass describing it.
+ * The option field of the AVClass must be set to a NULL-terminated static array
+ * of AVOptions. Each AVOption must have a non-empty name, a type, a default
+ * value and for number-type AVOptions also a range of allowed values. It must
+ * also declare an offset in bytes from the start of the struct, where the field
+ * associated with this AVOption is located. Other fields in the AVOption struct
+ * should also be set when applicable, but are not required.
+ *
+ * The following example illustrates an AVOptions-enabled struct:
+ * @code
+ * typedef struct test_struct {
+ * AVClass *class;
+ * int int_opt;
+ * char *str_opt;
+ * uint8_t *bin_opt;
+ * int bin_len;
+ * } test_struct;
+ *
+ * static const AVOption options[] = {
+ * { "test_int", "This is a test option of int type.", offsetof(test_struct, int_opt),
+ * AV_OPT_TYPE_INT, { -1 }, INT_MIN, INT_MAX },
+ * { "test_str", "This is a test option of string type.", offsetof(test_struct, str_opt),
+ * AV_OPT_TYPE_STRING },
+ * { "test_bin", "This is a test option of binary type.", offsetof(test_struct, bin_opt),
+ * AV_OPT_TYPE_BINARY },
+ * { NULL },
+ * };
+ *
+ * static const AVClass test_class = {
+ * .class_name = "test class",
+ * .item_name = av_default_item_name,
+ * .option = options,
+ * .version = LIBAVUTIL_VERSION_INT,
+ * };
+ * @endcode
+ *
+ * Next, when allocating your struct, you must ensure that the AVClass pointer
+ * is set to the correct value. Then, av_opt_set_defaults() must be called to
+ * initialize defaults. After that the struct is ready to be used with the
+ * AVOptions API.
+ *
+ * When cleaning up, you may use the av_opt_free() function to automatically
+ * free all the allocated string and binary options.
+ *
+ * Continuing with the above example:
+ *
+ * @code
+ * test_struct *alloc_test_struct(void)
+ * {
+ * test_struct *ret = av_malloc(sizeof(*ret));
+ * ret->class = &test_class;
+ * av_opt_set_defaults(ret);
+ * return ret;
+ * }
+ * void free_test_struct(test_struct **foo)
+ * {
+ * av_opt_free(*foo);
+ * av_freep(foo);
+ * }
+ * @endcode
+ *
+ * @subsection avoptions_implement_nesting Nesting
+ * It may happen that an AVOptions-enabled struct contains another
+ * AVOptions-enabled struct as a member (e.g. AVCodecContext in
+ * libavcodec exports generic options, while its priv_data field exports
+ * codec-specific options). In such a case, it is possible to set up the
+ * parent struct to export a child's options. To do that, simply
+ * implement AVClass.child_next() and AVClass.child_class_next() in the
+ * parent struct's AVClass.
+ * Assuming that the test_struct from above now also contains a
+ * child_struct field:
+ *
+ * @code
+ * typedef struct child_struct {
+ * AVClass *class;
+ * int flags_opt;
+ * } child_struct;
+ * static const AVOption child_opts[] = {
+ * { "test_flags", "This is a test option of flags type.",
+ * offsetof(child_struct, flags_opt), AV_OPT_TYPE_FLAGS, { 0 }, INT_MIN, INT_MAX },
+ * { NULL },
+ * };
+ * static const AVClass child_class = {
+ * .class_name = "child class",
+ * .item_name = av_default_item_name,
+ * .option = child_opts,
+ * .version = LIBAVUTIL_VERSION_INT,
+ * };
+ *
+ * void *child_next(void *obj, void *prev)
+ * {
+ * test_struct *t = obj;
+ * if (!prev && t->child_struct)
+ * return t->child_struct;
+ * return NULL
+ * }
+ * const AVClass child_class_next(const AVClass *prev)
+ * {
+ * return prev ? NULL : &child_class;
+ * }
+ * @endcode
+ * Putting child_next() and child_class_next() as defined above into
+ * test_class will now make child_struct's options accessible through
+ * test_struct (again, proper setup as described above needs to be done on
+ * child_struct right after it is created).
+ *
+ * From the above example it might not be clear why both child_next()
+ * and child_class_next() are needed. The distinction is that child_next()
+ * iterates over actually existing objects, while child_class_next()
+ * iterates over all possible child classes. E.g. if an AVCodecContext
+ * was initialized to use a codec which has private options, then its
+ * child_next() will return AVCodecContext.priv_data and finish
+ * iterating. OTOH child_class_next() on AVCodecContext.av_class will
+ * iterate over all available codecs with private options.
+ *
+ * @subsection avoptions_implement_named_constants Named constants
+ * It is possible to create named constants for options. Simply set the unit
+ * field of the option the constants should apply to to a string and
+ * create the constants themselves as options of type AV_OPT_TYPE_CONST
+ * with their unit field set to the same string.
+ * Their default_val field should contain the value of the named
+ * constant.
+ * For example, to add some named constants for the test_flags option
+ * above, put the following into the child_opts array:
+ * @code
+ * { "test_flags", "This is a test option of flags type.",
+ * offsetof(child_struct, flags_opt), AV_OPT_TYPE_FLAGS, { 0 }, INT_MIN, INT_MAX, "test_unit" },
+ * { "flag1", "This is a flag with value 16", 0, AV_OPT_TYPE_CONST, { 16 }, 0, 0, "test_unit" },
+ * @endcode
+ *
+ * @section avoptions_use Using AVOptions
+ * This section deals with accessing options in an AVOptions-enabled struct.
+ * Such structs in Libav are e.g. AVCodecContext in libavcodec or
+ * AVFormatContext in libavformat.
+ *
+ * @subsection avoptions_use_examine Examining AVOptions
+ * The basic functions for examining options are av_opt_next(), which iterates
+ * over all options defined for one object, and av_opt_find(), which searches
+ * for an option with the given name.
+ *
+ * The situation is more complicated with nesting. An AVOptions-enabled struct
+ * may have AVOptions-enabled children. Passing the AV_OPT_SEARCH_CHILDREN flag
+ * to av_opt_find() will make the function search children recursively.
+ *
+ * For enumerating there are basically two cases. The first is when you want to
+ * get all options that may potentially exist on the struct and its children
+ * (e.g. when constructing documentation). In that case you should call
+ * av_opt_child_class_next() recursively on the parent struct's AVClass. The
+ * second case is when you have an already initialized struct with all its
+ * children and you want to get all options that can be actually written or read
+ * from it. In that case you should call av_opt_child_next() recursively (and
+ * av_opt_next() on each result).
+ *
+ * @subsection avoptions_use_get_set Reading and writing AVOptions
+ * When setting options, you often have a string read directly from the
+ * user. In such a case, simply passing it to av_opt_set() is enough. For
+ * non-string type options, av_opt_set() will parse the string according to the
+ * option type.
+ *
+ * Similarly av_opt_get() will read any option type and convert it to a string
+ * which will be returned. Do not forget that the string is allocated, so you
+ * have to free it with av_free().
+ *
+ * In some cases it may be more convenient to put all options into an
+ * AVDictionary and call av_opt_set_dict() on it. A specific case of this
+ * are the format/codec open functions in lavf/lavc which take a dictionary
+ * filled with option as a parameter. This allows to set some options
+ * that cannot be set otherwise, since e.g. the input file format is not known
+ * before the file is actually opened.
+ */
+
+enum AVOptionType{
+ AV_OPT_TYPE_FLAGS,
+ AV_OPT_TYPE_INT,
+ AV_OPT_TYPE_INT64,
+ AV_OPT_TYPE_DOUBLE,
+ AV_OPT_TYPE_FLOAT,
+ AV_OPT_TYPE_STRING,
+ AV_OPT_TYPE_RATIONAL,
+ AV_OPT_TYPE_BINARY, ///< offset must point to a pointer immediately followed by an int for the length
+ AV_OPT_TYPE_CONST = 128,
+#if FF_API_OLD_AVOPTIONS
+ FF_OPT_TYPE_FLAGS = 0,
+ FF_OPT_TYPE_INT,
+ FF_OPT_TYPE_INT64,
+ FF_OPT_TYPE_DOUBLE,
+ FF_OPT_TYPE_FLOAT,
+ FF_OPT_TYPE_STRING,
+ FF_OPT_TYPE_RATIONAL,
+ FF_OPT_TYPE_BINARY, ///< offset must point to a pointer immediately followed by an int for the length
+ FF_OPT_TYPE_CONST=128,
+#endif
+};
+
+/**
+ * AVOption
+ */
+typedef struct AVOption {
+ const char *name;
+
+ /**
+ * short English help text
+ * @todo What about other languages?
+ */
+ const char *help;
+
+ /**
+ * The offset relative to the context structure where the option
+ * value is stored. It should be 0 for named constants.
+ */
+ int offset;
+ enum AVOptionType type;
+
+ /**
+ * the default value for scalar options
+ */
+ union {
+ double dbl;
+ const char *str;
+ /* TODO those are unused now */
+ int64_t i64;
+ AVRational q;
+ } default_val;
+ double min; ///< minimum valid value for the option
+ double max; ///< maximum valid value for the option
+
+ int flags;
+#define AV_OPT_FLAG_ENCODING_PARAM 1 ///< a generic parameter which can be set by the user for muxing or encoding
+#define AV_OPT_FLAG_DECODING_PARAM 2 ///< a generic parameter which can be set by the user for demuxing or decoding
+#define AV_OPT_FLAG_METADATA 4 ///< some data extracted or inserted into the file like title, comment, ...
+#define AV_OPT_FLAG_AUDIO_PARAM 8
+#define AV_OPT_FLAG_VIDEO_PARAM 16
+#define AV_OPT_FLAG_SUBTITLE_PARAM 32
+//FIXME think about enc-audio, ... style flags
+
+ /**
+ * The logical unit to which the option belongs. Non-constant
+ * options and corresponding named constants share the same
+ * unit. May be NULL.
+ */
+ const char *unit;
+} AVOption;
+
+#if FF_API_FIND_OPT
+/**
+ * Look for an option in obj. Look only for the options which
+ * have the flags set as specified in mask and flags (that is,
+ * for which it is the case that opt->flags & mask == flags).
+ *
+ * @param[in] obj a pointer to a struct whose first element is a
+ * pointer to an AVClass
+ * @param[in] name the name of the option to look for
+ * @param[in] unit the unit of the option to look for, or any if NULL
+ * @return a pointer to the option found, or NULL if no option
+ * has been found
+ *
+ * @deprecated use av_opt_find.
+ */
+attribute_deprecated
+const AVOption *av_find_opt(void *obj, const char *name, const char *unit, int mask, int flags);
+#endif
+
+#if FF_API_OLD_AVOPTIONS
+/**
+ * Set the field of obj with the given name to value.
+ *
+ * @param[in] obj A struct whose first element is a pointer to an
+ * AVClass.
+ * @param[in] name the name of the field to set
+ * @param[in] val The value to set. If the field is not of a string
+ * type, then the given string is parsed.
+ * SI postfixes and some named scalars are supported.
+ * If the field is of a numeric type, it has to be a numeric or named
+ * scalar. Behavior with more than one scalar and +- infix operators
+ * is undefined.
+ * If the field is of a flags type, it has to be a sequence of numeric
+ * scalars or named flags separated by '+' or '-'. Prefixing a flag
+ * with '+' causes it to be set without affecting the other flags;
+ * similarly, '-' unsets a flag.
+ * @param[out] o_out if non-NULL put here a pointer to the AVOption
+ * found
+ * @param alloc this parameter is currently ignored
+ * @return 0 if the value has been set, or an AVERROR code in case of
+ * error:
+ * AVERROR_OPTION_NOT_FOUND if no matching option exists
+ * AVERROR(ERANGE) if the value is out of range
+ * AVERROR(EINVAL) if the value is not valid
+ * @deprecated use av_opt_set()
+ */
+attribute_deprecated
+int av_set_string3(void *obj, const char *name, const char *val, int alloc, const AVOption **o_out);
+
+attribute_deprecated const AVOption *av_set_double(void *obj, const char *name, double n);
+attribute_deprecated const AVOption *av_set_q(void *obj, const char *name, AVRational n);
+attribute_deprecated const AVOption *av_set_int(void *obj, const char *name, int64_t n);
+
+attribute_deprecated double av_get_double(void *obj, const char *name, const AVOption **o_out);
+attribute_deprecated AVRational av_get_q(void *obj, const char *name, const AVOption **o_out);
+attribute_deprecated int64_t av_get_int(void *obj, const char *name, const AVOption **o_out);
+attribute_deprecated const char *av_get_string(void *obj, const char *name, const AVOption **o_out, char *buf, int buf_len);
+attribute_deprecated const AVOption *av_next_option(void *obj, const AVOption *last);
+#endif
+
+/**
+ * Show the obj options.
+ *
+ * @param req_flags requested flags for the options to show. Show only the
+ * options for which it is opt->flags & req_flags.
+ * @param rej_flags rejected flags for the options to show. Show only the
+ * options for which it is !(opt->flags & req_flags).
+ * @param av_log_obj log context to use for showing the options
+ */
+int av_opt_show2(void *obj, void *av_log_obj, int req_flags, int rej_flags);
+
+/**
+ * Set the values of all AVOption fields to their default values.
+ *
+ * @param s an AVOption-enabled struct (its first member must be a pointer to AVClass)
+ */
+void av_opt_set_defaults(void *s);
+
+#if FF_API_OLD_AVOPTIONS
+attribute_deprecated
+void av_opt_set_defaults2(void *s, int mask, int flags);
+#endif
+
+/**
+ * Parse the key/value pairs list in opts. For each key/value pair
+ * found, stores the value in the field in ctx that is named like the
+ * key. ctx must be an AVClass context, storing is done using
+ * AVOptions.
+ *
+ * @param key_val_sep a 0-terminated list of characters used to
+ * separate key from value
+ * @param pairs_sep a 0-terminated list of characters used to separate
+ * two pairs from each other
+ * @return the number of successfully set key/value pairs, or a negative
+ * value corresponding to an AVERROR code in case of error:
+ * AVERROR(EINVAL) if opts cannot be parsed,
+ * the error code issued by av_set_string3() if a key/value pair
+ * cannot be set
+ */
+int av_set_options_string(void *ctx, const char *opts,
+ const char *key_val_sep, const char *pairs_sep);
+
+/**
+ * Free all string and binary options in obj.
+ */
+void av_opt_free(void *obj);
+
+/**
+ * Check whether a particular flag is set in a flags field.
+ *
+ * @param field_name the name of the flag field option
+ * @param flag_name the name of the flag to check
+ * @return non-zero if the flag is set, zero if the flag isn't set,
+ * isn't of the right type, or the flags field doesn't exist.
+ */
+int av_opt_flag_is_set(void *obj, const char *field_name, const char *flag_name);
+
+/*
+ * Set all the options from a given dictionary on an object.
+ *
+ * @param obj a struct whose first element is a pointer to AVClass
+ * @param options options to process. This dictionary will be freed and replaced
+ * by a new one containing all options not found in obj.
+ * Of course this new dictionary needs to be freed by caller
+ * with av_dict_free().
+ *
+ * @return 0 on success, a negative AVERROR if some option was found in obj,
+ * but could not be set.
+ *
+ * @see av_dict_copy()
+ */
+int av_opt_set_dict(void *obj, struct AVDictionary **options);
+
+/**
+ * @defgroup opt_eval_funcs Evaluating option strings
+ * @{
+ * This group of functions can be used to evaluate option strings
+ * and get numbers out of them. They do the same thing as av_opt_set(),
+ * except the result is written into the caller-supplied pointer.
+ *
+ * @param obj a struct whose first element is a pointer to AVClass.
+ * @param o an option for which the string is to be evaluated.
+ * @param val string to be evaluated.
+ * @param *_out value of the string will be written here.
+ *
+ * @return 0 on success, a negative number on failure.
+ */
+int av_opt_eval_flags (void *obj, const AVOption *o, const char *val, int *flags_out);
+int av_opt_eval_int (void *obj, const AVOption *o, const char *val, int *int_out);
+int av_opt_eval_int64 (void *obj, const AVOption *o, const char *val, int64_t *int64_out);
+int av_opt_eval_float (void *obj, const AVOption *o, const char *val, float *float_out);
+int av_opt_eval_double(void *obj, const AVOption *o, const char *val, double *double_out);
+int av_opt_eval_q (void *obj, const AVOption *o, const char *val, AVRational *q_out);
+/**
+ * @}
+ */
+
+#define AV_OPT_SEARCH_CHILDREN 0x0001 /**< Search in possible children of the
+ given object first. */
+/**
+ * The obj passed to av_opt_find() is fake -- only a double pointer to AVClass
+ * instead of a required pointer to a struct containing AVClass. This is
+ * useful for searching for options without needing to allocate the corresponding
+ * object.
+ */
+#define AV_OPT_SEARCH_FAKE_OBJ 0x0002
+
+/**
+ * Look for an option in an object. Consider only options which
+ * have all the specified flags set.
+ *
+ * @param[in] obj A pointer to a struct whose first element is a
+ * pointer to an AVClass.
+ * Alternatively a double pointer to an AVClass, if
+ * AV_OPT_SEARCH_FAKE_OBJ search flag is set.
+ * @param[in] name The name of the option to look for.
+ * @param[in] unit When searching for named constants, name of the unit
+ * it belongs to.
+ * @param opt_flags Find only options with all the specified flags set (AV_OPT_FLAG).
+ * @param search_flags A combination of AV_OPT_SEARCH_*.
+ *
+ * @return A pointer to the option found, or NULL if no option
+ * was found.
+ *
+ * @note Options found with AV_OPT_SEARCH_CHILDREN flag may not be settable
+ * directly with av_set_string3(). Use special calls which take an options
+ * AVDictionary (e.g. avformat_open_input()) to set options found with this
+ * flag.
+ */
+const AVOption *av_opt_find(void *obj, const char *name, const char *unit,
+ int opt_flags, int search_flags);
+
+/**
+ * Look for an option in an object. Consider only options which
+ * have all the specified flags set.
+ *
+ * @param[in] obj A pointer to a struct whose first element is a
+ * pointer to an AVClass.
+ * Alternatively a double pointer to an AVClass, if
+ * AV_OPT_SEARCH_FAKE_OBJ search flag is set.
+ * @param[in] name The name of the option to look for.
+ * @param[in] unit When searching for named constants, name of the unit
+ * it belongs to.
+ * @param opt_flags Find only options with all the specified flags set (AV_OPT_FLAG).
+ * @param search_flags A combination of AV_OPT_SEARCH_*.
+ * @param[out] target_obj if non-NULL, an object to which the option belongs will be
+ * written here. It may be different from obj if AV_OPT_SEARCH_CHILDREN is present
+ * in search_flags. This parameter is ignored if search_flags contain
+ * AV_OPT_SEARCH_FAKE_OBJ.
+ *
+ * @return A pointer to the option found, or NULL if no option
+ * was found.
+ */
+const AVOption *av_opt_find2(void *obj, const char *name, const char *unit,
+ int opt_flags, int search_flags, void **target_obj);
+
+/**
+ * Iterate over all AVOptions belonging to obj.
+ *
+ * @param obj an AVOptions-enabled struct or a double pointer to an
+ * AVClass describing it.
+ * @param prev result of the previous call to av_opt_next() on this object
+ * or NULL
+ * @return next AVOption or NULL
+ */
+const AVOption *av_opt_next(void *obj, const AVOption *prev);
+
+/**
+ * Iterate over AVOptions-enabled children of obj.
+ *
+ * @param prev result of a previous call to this function or NULL
+ * @return next AVOptions-enabled child or NULL
+ */
+void *av_opt_child_next(void *obj, void *prev);
+
+/**
+ * Iterate over potential AVOptions-enabled children of parent.
+ *
+ * @param prev result of a previous call to this function or NULL
+ * @return AVClass corresponding to next potential child or NULL
+ */
+const AVClass *av_opt_child_class_next(const AVClass *parent, const AVClass *prev);
+
+/**
+ * @defgroup opt_set_funcs Option setting functions
+ * @{
+ * Those functions set the field of obj with the given name to value.
+ *
+ * @param[in] obj A struct whose first element is a pointer to an AVClass.
+ * @param[in] name the name of the field to set
+ * @param[in] val The value to set. In case of av_opt_set() if the field is not
+ * of a string type, then the given string is parsed.
+ * SI postfixes and some named scalars are supported.
+ * If the field is of a numeric type, it has to be a numeric or named
+ * scalar. Behavior with more than one scalar and +- infix operators
+ * is undefined.
+ * If the field is of a flags type, it has to be a sequence of numeric
+ * scalars or named flags separated by '+' or '-'. Prefixing a flag
+ * with '+' causes it to be set without affecting the other flags;
+ * similarly, '-' unsets a flag.
+ * @param search_flags flags passed to av_opt_find2. I.e. if AV_OPT_SEARCH_CHILDREN
+ * is passed here, then the option may be set on a child of obj.
+ *
+ * @return 0 if the value has been set, or an AVERROR code in case of
+ * error:
+ * AVERROR_OPTION_NOT_FOUND if no matching option exists
+ * AVERROR(ERANGE) if the value is out of range
+ * AVERROR(EINVAL) if the value is not valid
+ */
+int av_opt_set (void *obj, const char *name, const char *val, int search_flags);
+int av_opt_set_int (void *obj, const char *name, int64_t val, int search_flags);
+int av_opt_set_double(void *obj, const char *name, double val, int search_flags);
+int av_opt_set_q (void *obj, const char *name, AVRational val, int search_flags);
+/**
+ * @}
+ */
+
+/**
+ * @defgroup opt_get_funcs Option getting functions
+ * @{
+ * Those functions get a value of the option with the given name from an object.
+ *
+ * @param[in] obj a struct whose first element is a pointer to an AVClass.
+ * @param[in] name name of the option to get.
+ * @param[in] search_flags flags passed to av_opt_find2. I.e. if AV_OPT_SEARCH_CHILDREN
+ * is passed here, then the option may be found in a child of obj.
+ * @param[out] out_val value of the option will be written here
+ * @return 0 on success, a negative error code otherwise
+ */
+/**
+ * @note the returned string will av_malloc()ed and must be av_free()ed by the caller
+ */
+int av_opt_get (void *obj, const char *name, int search_flags, uint8_t **out_val);
+int av_opt_get_int (void *obj, const char *name, int search_flags, int64_t *out_val);
+int av_opt_get_double(void *obj, const char *name, int search_flags, double *out_val);
+int av_opt_get_q (void *obj, const char *name, int search_flags, AVRational *out_val);
+/**
+ * @}
+ * @}
+ */
+
+#endif /* AVUTIL_OPT_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/parseutils.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/parseutils.h
new file mode 100644
index 0000000000..0844abb2f0
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/parseutils.h
@@ -0,0 +1,124 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_PARSEUTILS_H
+#define AVUTIL_PARSEUTILS_H
+
+#include <time.h>
+
+#include "rational.h"
+
+/**
+ * @file
+ * misc parsing utilities
+ */
+
+/**
+ * Parse str and put in width_ptr and height_ptr the detected values.
+ *
+ * @param[in,out] width_ptr pointer to the variable which will contain the detected
+ * width value
+ * @param[in,out] height_ptr pointer to the variable which will contain the detected
+ * height value
+ * @param[in] str the string to parse: it has to be a string in the format
+ * width x height or a valid video size abbreviation.
+ * @return >= 0 on success, a negative error code otherwise
+ */
+int av_parse_video_size(int *width_ptr, int *height_ptr, const char *str);
+
+/**
+ * Parse str and store the detected values in *rate.
+ *
+ * @param[in,out] rate pointer to the AVRational which will contain the detected
+ * frame rate
+ * @param[in] str the string to parse: it has to be a string in the format
+ * rate_num / rate_den, a float number or a valid video rate abbreviation
+ * @return >= 0 on success, a negative error code otherwise
+ */
+int av_parse_video_rate(AVRational *rate, const char *str);
+
+/**
+ * Put the RGBA values that correspond to color_string in rgba_color.
+ *
+ * @param color_string a string specifying a color. It can be the name of
+ * a color (case insensitive match) or a [0x|#]RRGGBB[AA] sequence,
+ * possibly followed by "@" and a string representing the alpha
+ * component.
+ * The alpha component may be a string composed by "0x" followed by an
+ * hexadecimal number or a decimal number between 0.0 and 1.0, which
+ * represents the opacity value (0x00/0.0 means completely transparent,
+ * 0xff/1.0 completely opaque).
+ * If the alpha component is not specified then 0xff is assumed.
+ * The string "random" will result in a random color.
+ * @param slen length of the initial part of color_string containing the
+ * color. It can be set to -1 if color_string is a null terminated string
+ * containing nothing else than the color.
+ * @return >= 0 in case of success, a negative value in case of
+ * failure (for example if color_string cannot be parsed).
+ */
+int av_parse_color(uint8_t *rgba_color, const char *color_string, int slen,
+ void *log_ctx);
+
+/**
+ * Parse timestr and return in *time a corresponding number of
+ * microseconds.
+ *
+ * @param timeval puts here the number of microseconds corresponding
+ * to the string in timestr. If the string represents a duration, it
+ * is the number of microseconds contained in the time interval. If
+ * the string is a date, is the number of microseconds since 1st of
+ * January, 1970 up to the time of the parsed date. If timestr cannot
+ * be successfully parsed, set *time to INT64_MIN.
+
+ * @param timestr a string representing a date or a duration.
+ * - If a date the syntax is:
+ * @code
+ * [{YYYY-MM-DD|YYYYMMDD}[T|t| ]]{{HH[:MM[:SS[.m...]]]}|{HH[MM[SS[.m...]]]}}[Z]
+ * now
+ * @endcode
+ * If the value is "now" it takes the current time.
+ * Time is local time unless Z is appended, in which case it is
+ * interpreted as UTC.
+ * If the year-month-day part is not specified it takes the current
+ * year-month-day.
+ * - If a duration the syntax is:
+ * @code
+ * [-]HH[:MM[:SS[.m...]]]
+ * [-]S+[.m...]
+ * @endcode
+ * @param duration flag which tells how to interpret timestr, if not
+ * zero timestr is interpreted as a duration, otherwise as a date
+ * @return 0 in case of success, a negative value corresponding to an
+ * AVERROR code otherwise
+ */
+int av_parse_time(int64_t *timeval, const char *timestr, int duration);
+
+/**
+ * Attempt to find a specific tag in a URL.
+ *
+ * syntax: '?tag1=val1&tag2=val2...'. Little URL decoding is done.
+ * Return 1 if found.
+ */
+int av_find_info_tag(char *arg, int arg_size, const char *tag1, const char *info);
+
+/**
+ * Convert the decomposed UTC time in tm to a time_t value.
+ */
+time_t av_timegm(struct tm *tm);
+
+#endif /* AVUTIL_PARSEUTILS_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/pixdesc.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/pixdesc.h
new file mode 100644
index 0000000000..b5972c78ff
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/pixdesc.h
@@ -0,0 +1,177 @@
+/*
+ * pixel format descriptor
+ * Copyright (c) 2009 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_PIXDESC_H
+#define AVUTIL_PIXDESC_H
+
+#include <inttypes.h>
+#include "pixfmt.h"
+
+typedef struct AVComponentDescriptor{
+ uint16_t plane :2; ///< which of the 4 planes contains the component
+
+ /**
+ * Number of elements between 2 horizontally consecutive pixels minus 1.
+ * Elements are bits for bitstream formats, bytes otherwise.
+ */
+ uint16_t step_minus1 :3;
+
+ /**
+ * Number of elements before the component of the first pixel plus 1.
+ * Elements are bits for bitstream formats, bytes otherwise.
+ */
+ uint16_t offset_plus1 :3;
+ uint16_t shift :3; ///< number of least significant bits that must be shifted away to get the value
+ uint16_t depth_minus1 :4; ///< number of bits in the component minus 1
+}AVComponentDescriptor;
+
+/**
+ * Descriptor that unambiguously describes how the bits of a pixel are
+ * stored in the up to 4 data planes of an image. It also stores the
+ * subsampling factors and number of components.
+ *
+ * @note This is separate of the colorspace (RGB, YCbCr, YPbPr, JPEG-style YUV
+ * and all the YUV variants) AVPixFmtDescriptor just stores how values
+ * are stored not what these values represent.
+ */
+typedef struct AVPixFmtDescriptor{
+ const char *name;
+ uint8_t nb_components; ///< The number of components each pixel has, (1-4)
+
+ /**
+ * Amount to shift the luma width right to find the chroma width.
+ * For YV12 this is 1 for example.
+ * chroma_width = -((-luma_width) >> log2_chroma_w)
+ * The note above is needed to ensure rounding up.
+ * This value only refers to the chroma components.
+ */
+ uint8_t log2_chroma_w; ///< chroma_width = -((-luma_width )>>log2_chroma_w)
+
+ /**
+ * Amount to shift the luma height right to find the chroma height.
+ * For YV12 this is 1 for example.
+ * chroma_height= -((-luma_height) >> log2_chroma_h)
+ * The note above is needed to ensure rounding up.
+ * This value only refers to the chroma components.
+ */
+ uint8_t log2_chroma_h;
+ uint8_t flags;
+
+ /**
+ * Parameters that describe how pixels are packed. If the format
+ * has chroma components, they must be stored in comp[1] and
+ * comp[2].
+ */
+ AVComponentDescriptor comp[4];
+}AVPixFmtDescriptor;
+
+#define PIX_FMT_BE 1 ///< Pixel format is big-endian.
+#define PIX_FMT_PAL 2 ///< Pixel format has a palette in data[1], values are indexes in this palette.
+#define PIX_FMT_BITSTREAM 4 ///< All values of a component are bit-wise packed end to end.
+#define PIX_FMT_HWACCEL 8 ///< Pixel format is an HW accelerated format.
+#define PIX_FMT_PLANAR 16 ///< At least one pixel component is not in the first data plane
+#define PIX_FMT_RGB 32 ///< The pixel format contains RGB-like data (as opposed to YUV/grayscale)
+
+/**
+ * The array of all the pixel format descriptors.
+ */
+extern const AVPixFmtDescriptor av_pix_fmt_descriptors[];
+
+/**
+ * Read a line from an image, and write the values of the
+ * pixel format component c to dst.
+ *
+ * @param data the array containing the pointers to the planes of the image
+ * @param linesize the array containing the linesizes of the image
+ * @param desc the pixel format descriptor for the image
+ * @param x the horizontal coordinate of the first pixel to read
+ * @param y the vertical coordinate of the first pixel to read
+ * @param w the width of the line to read, that is the number of
+ * values to write to dst
+ * @param read_pal_component if not zero and the format is a paletted
+ * format writes the values corresponding to the palette
+ * component c in data[1] to dst, rather than the palette indexes in
+ * data[0]. The behavior is undefined if the format is not paletted.
+ */
+void av_read_image_line(uint16_t *dst, const uint8_t *data[4], const int linesize[4],
+ const AVPixFmtDescriptor *desc, int x, int y, int c, int w, int read_pal_component);
+
+/**
+ * Write the values from src to the pixel format component c of an
+ * image line.
+ *
+ * @param src array containing the values to write
+ * @param data the array containing the pointers to the planes of the
+ * image to write into. It is supposed to be zeroed.
+ * @param linesize the array containing the linesizes of the image
+ * @param desc the pixel format descriptor for the image
+ * @param x the horizontal coordinate of the first pixel to write
+ * @param y the vertical coordinate of the first pixel to write
+ * @param w the width of the line to write, that is the number of
+ * values to write to the image line
+ */
+void av_write_image_line(const uint16_t *src, uint8_t *data[4], const int linesize[4],
+ const AVPixFmtDescriptor *desc, int x, int y, int c, int w);
+
+/**
+ * Return the pixel format corresponding to name.
+ *
+ * If there is no pixel format with name name, then looks for a
+ * pixel format with the name corresponding to the native endian
+ * format of name.
+ * For example in a little-endian system, first looks for "gray16",
+ * then for "gray16le".
+ *
+ * Finally if no pixel format has been found, returns PIX_FMT_NONE.
+ */
+enum PixelFormat av_get_pix_fmt(const char *name);
+
+/**
+ * Return the short name for a pixel format, NULL in case pix_fmt is
+ * unknown.
+ *
+ * @see av_get_pix_fmt(), av_get_pix_fmt_string()
+ */
+const char *av_get_pix_fmt_name(enum PixelFormat pix_fmt);
+
+/**
+ * Print in buf the string corresponding to the pixel format with
+ * number pix_fmt, or an header if pix_fmt is negative.
+ *
+ * @param buf the buffer where to write the string
+ * @param buf_size the size of buf
+ * @param pix_fmt the number of the pixel format to print the
+ * corresponding info string, or a negative value to print the
+ * corresponding header.
+ */
+char *av_get_pix_fmt_string (char *buf, int buf_size, enum PixelFormat pix_fmt);
+
+/**
+ * Return the number of bits per pixel used by the pixel format
+ * described by pixdesc.
+ *
+ * The returned number of bits refers to the number of bits actually
+ * used for storing the pixel information, that is padding bits are
+ * not counted.
+ */
+int av_get_bits_per_pixel(const AVPixFmtDescriptor *pixdesc);
+
+#endif /* AVUTIL_PIXDESC_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/pixfmt.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/pixfmt.h
new file mode 100644
index 0000000000..bd898bdc8e
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/pixfmt.h
@@ -0,0 +1,198 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_PIXFMT_H
+#define AVUTIL_PIXFMT_H
+
+/**
+ * @file
+ * pixel format definitions
+ *
+ */
+
+#include "libavutil/avconfig.h"
+
+/**
+ * Pixel format.
+ *
+ * @note
+ * PIX_FMT_RGB32 is handled in an endian-specific manner. An RGBA
+ * color is put together as:
+ * (A << 24) | (R << 16) | (G << 8) | B
+ * This is stored as BGRA on little-endian CPU architectures and ARGB on
+ * big-endian CPUs.
+ *
+ * @par
+ * When the pixel format is palettized RGB (PIX_FMT_PAL8), the palettized
+ * image data is stored in AVFrame.data[0]. The palette is transported in
+ * AVFrame.data[1], is 1024 bytes long (256 4-byte entries) and is
+ * formatted the same as in PIX_FMT_RGB32 described above (i.e., it is
+ * also endian-specific). Note also that the individual RGB palette
+ * components stored in AVFrame.data[1] should be in the range 0..255.
+ * This is important as many custom PAL8 video codecs that were designed
+ * to run on the IBM VGA graphics adapter use 6-bit palette components.
+ *
+ * @par
+ * For all the 8bit per pixel formats, an RGB32 palette is in data[1] like
+ * for pal8. This palette is filled in automatically by the function
+ * allocating the picture.
+ *
+ * @note
+ * make sure that all newly added big endian formats have pix_fmt&1==1
+ * and that all newly added little endian formats have pix_fmt&1==0
+ * this allows simpler detection of big vs little endian.
+ */
+enum PixelFormat {
+ PIX_FMT_NONE= -1,
+ PIX_FMT_YUV420P, ///< planar YUV 4:2:0, 12bpp, (1 Cr & Cb sample per 2x2 Y samples)
+ PIX_FMT_YUYV422, ///< packed YUV 4:2:2, 16bpp, Y0 Cb Y1 Cr
+ PIX_FMT_RGB24, ///< packed RGB 8:8:8, 24bpp, RGBRGB...
+ PIX_FMT_BGR24, ///< packed RGB 8:8:8, 24bpp, BGRBGR...
+ PIX_FMT_YUV422P, ///< planar YUV 4:2:2, 16bpp, (1 Cr & Cb sample per 2x1 Y samples)
+ PIX_FMT_YUV444P, ///< planar YUV 4:4:4, 24bpp, (1 Cr & Cb sample per 1x1 Y samples)
+ PIX_FMT_YUV410P, ///< planar YUV 4:1:0, 9bpp, (1 Cr & Cb sample per 4x4 Y samples)
+ PIX_FMT_YUV411P, ///< planar YUV 4:1:1, 12bpp, (1 Cr & Cb sample per 4x1 Y samples)
+ PIX_FMT_GRAY8, ///< Y , 8bpp
+ PIX_FMT_MONOWHITE, ///< Y , 1bpp, 0 is white, 1 is black, in each byte pixels are ordered from the msb to the lsb
+ PIX_FMT_MONOBLACK, ///< Y , 1bpp, 0 is black, 1 is white, in each byte pixels are ordered from the msb to the lsb
+ PIX_FMT_PAL8, ///< 8 bit with PIX_FMT_RGB32 palette
+ PIX_FMT_YUVJ420P, ///< planar YUV 4:2:0, 12bpp, full scale (JPEG), deprecated in favor of PIX_FMT_YUV420P and setting color_range
+ PIX_FMT_YUVJ422P, ///< planar YUV 4:2:2, 16bpp, full scale (JPEG), deprecated in favor of PIX_FMT_YUV422P and setting color_range
+ PIX_FMT_YUVJ444P, ///< planar YUV 4:4:4, 24bpp, full scale (JPEG), deprecated in favor of PIX_FMT_YUV444P and setting color_range
+ PIX_FMT_XVMC_MPEG2_MC,///< XVideo Motion Acceleration via common packet passing
+ PIX_FMT_XVMC_MPEG2_IDCT,
+ PIX_FMT_UYVY422, ///< packed YUV 4:2:2, 16bpp, Cb Y0 Cr Y1
+ PIX_FMT_UYYVYY411, ///< packed YUV 4:1:1, 12bpp, Cb Y0 Y1 Cr Y2 Y3
+ PIX_FMT_BGR8, ///< packed RGB 3:3:2, 8bpp, (msb)2B 3G 3R(lsb)
+ PIX_FMT_BGR4, ///< packed RGB 1:2:1 bitstream, 4bpp, (msb)1B 2G 1R(lsb), a byte contains two pixels, the first pixel in the byte is the one composed by the 4 msb bits
+ PIX_FMT_BGR4_BYTE, ///< packed RGB 1:2:1, 8bpp, (msb)1B 2G 1R(lsb)
+ PIX_FMT_RGB8, ///< packed RGB 3:3:2, 8bpp, (msb)2R 3G 3B(lsb)
+ PIX_FMT_RGB4, ///< packed RGB 1:2:1 bitstream, 4bpp, (msb)1R 2G 1B(lsb), a byte contains two pixels, the first pixel in the byte is the one composed by the 4 msb bits
+ PIX_FMT_RGB4_BYTE, ///< packed RGB 1:2:1, 8bpp, (msb)1R 2G 1B(lsb)
+ PIX_FMT_NV12, ///< planar YUV 4:2:0, 12bpp, 1 plane for Y and 1 plane for the UV components, which are interleaved (first byte U and the following byte V)
+ PIX_FMT_NV21, ///< as above, but U and V bytes are swapped
+
+ PIX_FMT_ARGB, ///< packed ARGB 8:8:8:8, 32bpp, ARGBARGB...
+ PIX_FMT_RGBA, ///< packed RGBA 8:8:8:8, 32bpp, RGBARGBA...
+ PIX_FMT_ABGR, ///< packed ABGR 8:8:8:8, 32bpp, ABGRABGR...
+ PIX_FMT_BGRA, ///< packed BGRA 8:8:8:8, 32bpp, BGRABGRA...
+
+ PIX_FMT_GRAY16BE, ///< Y , 16bpp, big-endian
+ PIX_FMT_GRAY16LE, ///< Y , 16bpp, little-endian
+ PIX_FMT_YUV440P, ///< planar YUV 4:4:0 (1 Cr & Cb sample per 1x2 Y samples)
+ PIX_FMT_YUVJ440P, ///< planar YUV 4:4:0 full scale (JPEG), deprecated in favor of PIX_FMT_YUV440P and setting color_range
+ PIX_FMT_YUVA420P, ///< planar YUV 4:2:0, 20bpp, (1 Cr & Cb sample per 2x2 Y & A samples)
+ PIX_FMT_VDPAU_H264,///< H.264 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ PIX_FMT_VDPAU_MPEG1,///< MPEG-1 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ PIX_FMT_VDPAU_MPEG2,///< MPEG-2 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ PIX_FMT_VDPAU_WMV3,///< WMV3 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ PIX_FMT_VDPAU_VC1, ///< VC-1 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ PIX_FMT_RGB48BE, ///< packed RGB 16:16:16, 48bpp, 16R, 16G, 16B, the 2-byte value for each R/G/B component is stored as big-endian
+ PIX_FMT_RGB48LE, ///< packed RGB 16:16:16, 48bpp, 16R, 16G, 16B, the 2-byte value for each R/G/B component is stored as little-endian
+
+ PIX_FMT_RGB565BE, ///< packed RGB 5:6:5, 16bpp, (msb) 5R 6G 5B(lsb), big-endian
+ PIX_FMT_RGB565LE, ///< packed RGB 5:6:5, 16bpp, (msb) 5R 6G 5B(lsb), little-endian
+ PIX_FMT_RGB555BE, ///< packed RGB 5:5:5, 16bpp, (msb)1A 5R 5G 5B(lsb), big-endian, most significant bit to 0
+ PIX_FMT_RGB555LE, ///< packed RGB 5:5:5, 16bpp, (msb)1A 5R 5G 5B(lsb), little-endian, most significant bit to 0
+
+ PIX_FMT_BGR565BE, ///< packed BGR 5:6:5, 16bpp, (msb) 5B 6G 5R(lsb), big-endian
+ PIX_FMT_BGR565LE, ///< packed BGR 5:6:5, 16bpp, (msb) 5B 6G 5R(lsb), little-endian
+ PIX_FMT_BGR555BE, ///< packed BGR 5:5:5, 16bpp, (msb)1A 5B 5G 5R(lsb), big-endian, most significant bit to 1
+ PIX_FMT_BGR555LE, ///< packed BGR 5:5:5, 16bpp, (msb)1A 5B 5G 5R(lsb), little-endian, most significant bit to 1
+
+ PIX_FMT_VAAPI_MOCO, ///< HW acceleration through VA API at motion compensation entry-point, Picture.data[3] contains a vaapi_render_state struct which contains macroblocks as well as various fields extracted from headers
+ PIX_FMT_VAAPI_IDCT, ///< HW acceleration through VA API at IDCT entry-point, Picture.data[3] contains a vaapi_render_state struct which contains fields extracted from headers
+ PIX_FMT_VAAPI_VLD, ///< HW decoding through VA API, Picture.data[3] contains a vaapi_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+
+ PIX_FMT_YUV420P16LE, ///< planar YUV 4:2:0, 24bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian
+ PIX_FMT_YUV420P16BE, ///< planar YUV 4:2:0, 24bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian
+ PIX_FMT_YUV422P16LE, ///< planar YUV 4:2:2, 32bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ PIX_FMT_YUV422P16BE, ///< planar YUV 4:2:2, 32bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+ PIX_FMT_YUV444P16LE, ///< planar YUV 4:4:4, 48bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian
+ PIX_FMT_YUV444P16BE, ///< planar YUV 4:4:4, 48bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian
+ PIX_FMT_VDPAU_MPEG4, ///< MPEG4 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ PIX_FMT_DXVA2_VLD, ///< HW decoding through DXVA2, Picture.data[3] contains a LPDIRECT3DSURFACE9 pointer
+
+ PIX_FMT_RGB444LE, ///< packed RGB 4:4:4, 16bpp, (msb)4A 4R 4G 4B(lsb), little-endian, most significant bits to 0
+ PIX_FMT_RGB444BE, ///< packed RGB 4:4:4, 16bpp, (msb)4A 4R 4G 4B(lsb), big-endian, most significant bits to 0
+ PIX_FMT_BGR444LE, ///< packed BGR 4:4:4, 16bpp, (msb)4A 4B 4G 4R(lsb), little-endian, most significant bits to 1
+ PIX_FMT_BGR444BE, ///< packed BGR 4:4:4, 16bpp, (msb)4A 4B 4G 4R(lsb), big-endian, most significant bits to 1
+ PIX_FMT_Y400A, ///< 8bit gray, 8bit alpha
+ PIX_FMT_BGR48BE, ///< packed RGB 16:16:16, 48bpp, 16B, 16G, 16R, the 2-byte value for each R/G/B component is stored as big-endian
+ PIX_FMT_BGR48LE, ///< packed RGB 16:16:16, 48bpp, 16B, 16G, 16R, the 2-byte value for each R/G/B component is stored as little-endian
+ PIX_FMT_YUV420P9BE, ///< planar YUV 4:2:0, 13.5bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian
+ PIX_FMT_YUV420P9LE, ///< planar YUV 4:2:0, 13.5bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian
+ PIX_FMT_YUV420P10BE,///< planar YUV 4:2:0, 15bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian
+ PIX_FMT_YUV420P10LE,///< planar YUV 4:2:0, 15bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian
+ PIX_FMT_YUV422P10BE,///< planar YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+ PIX_FMT_YUV422P10LE,///< planar YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ PIX_FMT_YUV444P9BE, ///< planar YUV 4:4:4, 27bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian
+ PIX_FMT_YUV444P9LE, ///< planar YUV 4:4:4, 27bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian
+ PIX_FMT_YUV444P10BE,///< planar YUV 4:4:4, 30bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian
+ PIX_FMT_YUV444P10LE,///< planar YUV 4:4:4, 30bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian
+ PIX_FMT_YUV422P9BE, ///< planar YUV 4:2:2, 18bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+ PIX_FMT_YUV422P9LE, ///< planar YUV 4:2:2, 18bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ PIX_FMT_VDA_VLD, ///< hardware decoding through VDA
+ PIX_FMT_GBRP, ///< planar GBR 4:4:4 24bpp
+ PIX_FMT_GBRP9BE, ///< planar GBR 4:4:4 27bpp, big endian
+ PIX_FMT_GBRP9LE, ///< planar GBR 4:4:4 27bpp, little endian
+ PIX_FMT_GBRP10BE, ///< planar GBR 4:4:4 30bpp, big endian
+ PIX_FMT_GBRP10LE, ///< planar GBR 4:4:4 30bpp, little endian
+ PIX_FMT_GBRP16BE, ///< planar GBR 4:4:4 48bpp, big endian
+ PIX_FMT_GBRP16LE, ///< planar GBR 4:4:4 48bpp, little endian
+ PIX_FMT_NB, ///< number of pixel formats, DO NOT USE THIS if you want to link with shared libav* because the number of formats might differ between versions
+};
+
+#if AV_HAVE_BIGENDIAN
+# define PIX_FMT_NE(be, le) PIX_FMT_##be
+#else
+# define PIX_FMT_NE(be, le) PIX_FMT_##le
+#endif
+
+#define PIX_FMT_RGB32 PIX_FMT_NE(ARGB, BGRA)
+#define PIX_FMT_RGB32_1 PIX_FMT_NE(RGBA, ABGR)
+#define PIX_FMT_BGR32 PIX_FMT_NE(ABGR, RGBA)
+#define PIX_FMT_BGR32_1 PIX_FMT_NE(BGRA, ARGB)
+
+#define PIX_FMT_GRAY16 PIX_FMT_NE(GRAY16BE, GRAY16LE)
+#define PIX_FMT_RGB48 PIX_FMT_NE(RGB48BE, RGB48LE)
+#define PIX_FMT_RGB565 PIX_FMT_NE(RGB565BE, RGB565LE)
+#define PIX_FMT_RGB555 PIX_FMT_NE(RGB555BE, RGB555LE)
+#define PIX_FMT_RGB444 PIX_FMT_NE(RGB444BE, RGB444LE)
+#define PIX_FMT_BGR48 PIX_FMT_NE(BGR48BE, BGR48LE)
+#define PIX_FMT_BGR565 PIX_FMT_NE(BGR565BE, BGR565LE)
+#define PIX_FMT_BGR555 PIX_FMT_NE(BGR555BE, BGR555LE)
+#define PIX_FMT_BGR444 PIX_FMT_NE(BGR444BE, BGR444LE)
+
+#define PIX_FMT_YUV420P9 PIX_FMT_NE(YUV420P9BE , YUV420P9LE)
+#define PIX_FMT_YUV422P9 PIX_FMT_NE(YUV422P9BE , YUV422P9LE)
+#define PIX_FMT_YUV444P9 PIX_FMT_NE(YUV444P9BE , YUV444P9LE)
+#define PIX_FMT_YUV420P10 PIX_FMT_NE(YUV420P10BE, YUV420P10LE)
+#define PIX_FMT_YUV422P10 PIX_FMT_NE(YUV422P10BE, YUV422P10LE)
+#define PIX_FMT_YUV444P10 PIX_FMT_NE(YUV444P10BE, YUV444P10LE)
+#define PIX_FMT_YUV420P16 PIX_FMT_NE(YUV420P16BE, YUV420P16LE)
+#define PIX_FMT_YUV422P16 PIX_FMT_NE(YUV422P16BE, YUV422P16LE)
+#define PIX_FMT_YUV444P16 PIX_FMT_NE(YUV444P16BE, YUV444P16LE)
+
+#define PIX_FMT_GBRP9 PIX_FMT_NE(GBRP9BE , GBRP9LE)
+#define PIX_FMT_GBRP10 PIX_FMT_NE(GBRP10BE, GBRP10LE)
+#define PIX_FMT_GBRP16 PIX_FMT_NE(GBRP16BE, GBRP16LE)
+
+#endif /* AVUTIL_PIXFMT_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/random_seed.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/random_seed.h
new file mode 100644
index 0000000000..b1fad13d07
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/random_seed.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2009 Baptiste Coudurier <baptiste.coudurier@gmail.com>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_RANDOM_SEED_H
+#define AVUTIL_RANDOM_SEED_H
+
+#include <stdint.h>
+/**
+ * @addtogroup lavu_crypto
+ * @{
+ */
+
+/**
+ * Get random data.
+ *
+ * This function can be called repeatedly to generate more random bits
+ * as needed. It is generally quite slow, and usually used to seed a
+ * PRNG. As it uses /dev/urandom and /dev/random, the quality of the
+ * returned random data depends on the platform.
+ */
+uint32_t av_get_random_seed(void);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_RANDOM_SEED_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/rational.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/rational.h
new file mode 100644
index 0000000000..0ec18ec969
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/rational.h
@@ -0,0 +1,144 @@
+/*
+ * rational numbers
+ * Copyright (c) 2003 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * rational numbers
+ * @author Michael Niedermayer <michaelni@gmx.at>
+ */
+
+#ifndef AVUTIL_RATIONAL_H
+#define AVUTIL_RATIONAL_H
+
+#include <stdint.h>
+#include <limits.h>
+#include "attributes.h"
+
+/**
+ * @addtogroup lavu_math
+ * @{
+ */
+
+/**
+ * rational number numerator/denominator
+ */
+typedef struct AVRational{
+ int num; ///< numerator
+ int den; ///< denominator
+} AVRational;
+
+/**
+ * Compare two rationals.
+ * @param a first rational
+ * @param b second rational
+ * @return 0 if a==b, 1 if a>b, -1 if a<b, and INT_MIN if one of the
+ * values is of the form 0/0
+ */
+static inline int av_cmp_q(AVRational a, AVRational b){
+ const int64_t tmp= a.num * (int64_t)b.den - b.num * (int64_t)a.den;
+
+ if(tmp) return ((tmp ^ a.den ^ b.den)>>63)|1;
+ else if(b.den && a.den) return 0;
+ else if(a.num && b.num) return (a.num>>31) - (b.num>>31);
+ else return INT_MIN;
+}
+
+/**
+ * Convert rational to double.
+ * @param a rational to convert
+ * @return (double) a
+ */
+static inline double av_q2d(AVRational a){
+ return a.num / (double) a.den;
+}
+
+/**
+ * Reduce a fraction.
+ * This is useful for framerate calculations.
+ * @param dst_num destination numerator
+ * @param dst_den destination denominator
+ * @param num source numerator
+ * @param den source denominator
+ * @param max the maximum allowed for dst_num & dst_den
+ * @return 1 if exact, 0 otherwise
+ */
+int av_reduce(int *dst_num, int *dst_den, int64_t num, int64_t den, int64_t max);
+
+/**
+ * Multiply two rationals.
+ * @param b first rational
+ * @param c second rational
+ * @return b*c
+ */
+AVRational av_mul_q(AVRational b, AVRational c) av_const;
+
+/**
+ * Divide one rational by another.
+ * @param b first rational
+ * @param c second rational
+ * @return b/c
+ */
+AVRational av_div_q(AVRational b, AVRational c) av_const;
+
+/**
+ * Add two rationals.
+ * @param b first rational
+ * @param c second rational
+ * @return b+c
+ */
+AVRational av_add_q(AVRational b, AVRational c) av_const;
+
+/**
+ * Subtract one rational from another.
+ * @param b first rational
+ * @param c second rational
+ * @return b-c
+ */
+AVRational av_sub_q(AVRational b, AVRational c) av_const;
+
+/**
+ * Convert a double precision floating point number to a rational.
+ * inf is expressed as {1,0} or {-1,0} depending on the sign.
+ *
+ * @param d double to convert
+ * @param max the maximum allowed numerator and denominator
+ * @return (AVRational) d
+ */
+AVRational av_d2q(double d, int max) av_const;
+
+/**
+ * @return 1 if q1 is nearer to q than q2, -1 if q2 is nearer
+ * than q1, 0 if they have the same distance.
+ */
+int av_nearer_q(AVRational q, AVRational q1, AVRational q2);
+
+/**
+ * Find the nearest value in q_list to q.
+ * @param q_list an array of rationals terminated by {0, 0}
+ * @return the index of the nearest value found in the array
+ */
+int av_find_nearest_q_idx(AVRational q, const AVRational* q_list);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_RATIONAL_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/samplefmt.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/samplefmt.h
new file mode 100644
index 0000000000..b6715561d4
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/samplefmt.h
@@ -0,0 +1,148 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_SAMPLEFMT_H
+#define AVUTIL_SAMPLEFMT_H
+
+#include "avutil.h"
+
+/**
+ * all in native-endian format
+ */
+enum AVSampleFormat {
+ AV_SAMPLE_FMT_NONE = -1,
+ AV_SAMPLE_FMT_U8, ///< unsigned 8 bits
+ AV_SAMPLE_FMT_S16, ///< signed 16 bits
+ AV_SAMPLE_FMT_S32, ///< signed 32 bits
+ AV_SAMPLE_FMT_FLT, ///< float
+ AV_SAMPLE_FMT_DBL, ///< double
+
+ AV_SAMPLE_FMT_U8P, ///< unsigned 8 bits, planar
+ AV_SAMPLE_FMT_S16P, ///< signed 16 bits, planar
+ AV_SAMPLE_FMT_S32P, ///< signed 32 bits, planar
+ AV_SAMPLE_FMT_FLTP, ///< float, planar
+ AV_SAMPLE_FMT_DBLP, ///< double, planar
+
+ AV_SAMPLE_FMT_NB ///< Number of sample formats. DO NOT USE if linking dynamically
+};
+
+/**
+ * Return the name of sample_fmt, or NULL if sample_fmt is not
+ * recognized.
+ */
+const char *av_get_sample_fmt_name(enum AVSampleFormat sample_fmt);
+
+/**
+ * Return a sample format corresponding to name, or AV_SAMPLE_FMT_NONE
+ * on error.
+ */
+enum AVSampleFormat av_get_sample_fmt(const char *name);
+
+/**
+ * Generate a string corresponding to the sample format with
+ * sample_fmt, or a header if sample_fmt is negative.
+ *
+ * @param buf the buffer where to write the string
+ * @param buf_size the size of buf
+ * @param sample_fmt the number of the sample format to print the
+ * corresponding info string, or a negative value to print the
+ * corresponding header.
+ * @return the pointer to the filled buffer or NULL if sample_fmt is
+ * unknown or in case of other errors
+ */
+char *av_get_sample_fmt_string(char *buf, int buf_size, enum AVSampleFormat sample_fmt);
+
+#if FF_API_GET_BITS_PER_SAMPLE_FMT
+/**
+ * @deprecated Use av_get_bytes_per_sample() instead.
+ */
+attribute_deprecated
+int av_get_bits_per_sample_fmt(enum AVSampleFormat sample_fmt);
+#endif
+
+/**
+ * Return number of bytes per sample.
+ *
+ * @param sample_fmt the sample format
+ * @return number of bytes per sample or zero if unknown for the given
+ * sample format
+ */
+int av_get_bytes_per_sample(enum AVSampleFormat sample_fmt);
+
+/**
+ * Check if the sample format is planar.
+ *
+ * @param sample_fmt the sample format to inspect
+ * @return 1 if the sample format is planar, 0 if it is interleaved
+ */
+int av_sample_fmt_is_planar(enum AVSampleFormat sample_fmt);
+
+/**
+ * Get the required buffer size for the given audio parameters.
+ *
+ * @param[out] linesize calculated linesize, may be NULL
+ * @param nb_channels the number of channels
+ * @param nb_samples the number of samples in a single channel
+ * @param sample_fmt the sample format
+ * @return required buffer size, or negative error code on failure
+ */
+int av_samples_get_buffer_size(int *linesize, int nb_channels, int nb_samples,
+ enum AVSampleFormat sample_fmt, int align);
+
+/**
+ * Fill channel data pointers and linesize for samples with sample
+ * format sample_fmt.
+ *
+ * The pointers array is filled with the pointers to the samples data:
+ * for planar, set the start point of each channel's data within the buffer,
+ * for packed, set the start point of the entire buffer only.
+ *
+ * The linesize array is filled with the aligned size of each channel's data
+ * buffer for planar layout, or the aligned size of the buffer for all channels
+ * for packed layout.
+ *
+ * @param[out] audio_data array to be filled with the pointer for each channel
+ * @param[out] linesize calculated linesize
+ * @param buf the pointer to a buffer containing the samples
+ * @param nb_channels the number of channels
+ * @param nb_samples the number of samples in a single channel
+ * @param sample_fmt the sample format
+ * @param align buffer size alignment (1 = no alignment required)
+ * @return 0 on success or a negative error code on failure
+ */
+int av_samples_fill_arrays(uint8_t **audio_data, int *linesize, uint8_t *buf,
+ int nb_channels, int nb_samples,
+ enum AVSampleFormat sample_fmt, int align);
+
+/**
+ * Allocate a samples buffer for nb_samples samples, and fill data pointers and
+ * linesize accordingly.
+ * The allocated samples buffer can be freed by using av_freep(&audio_data[0])
+ *
+ * @param[out] audio_data array to be filled with the pointer for each channel
+ * @param[out] linesize aligned size for audio buffer(s)
+ * @param nb_channels number of audio channels
+ * @param nb_samples number of samples per channel
+ * @param align buffer size alignment (1 = no alignment required)
+ * @return 0 on success or a negative error code on failure
+ * @see av_samples_fill_arrays()
+ */
+int av_samples_alloc(uint8_t **audio_data, int *linesize, int nb_channels,
+ int nb_samples, enum AVSampleFormat sample_fmt, int align);
+
+#endif /* AVUTIL_SAMPLEFMT_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/sha.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/sha.h
new file mode 100644
index 0000000000..8350954c4b
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/sha.h
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2007 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_SHA_H
+#define AVUTIL_SHA_H
+
+#include <stdint.h>
+
+/**
+ * @defgroup lavu_sha SHA
+ * @ingroup lavu_crypto
+ * @{
+ */
+
+extern const int av_sha_size;
+
+struct AVSHA;
+
+/**
+ * Initialize SHA-1 or SHA-2 hashing.
+ *
+ * @param context pointer to the function context (of size av_sha_size)
+ * @param bits number of bits in digest (SHA-1 - 160 bits, SHA-2 224 or 256 bits)
+ * @return zero if initialization succeeded, -1 otherwise
+ */
+int av_sha_init(struct AVSHA* context, int bits);
+
+/**
+ * Update hash value.
+ *
+ * @param context hash function context
+ * @param data input data to update hash with
+ * @param len input data length
+ */
+void av_sha_update(struct AVSHA* context, const uint8_t* data, unsigned int len);
+
+/**
+ * Finish hashing and output digest value.
+ *
+ * @param context hash function context
+ * @param digest buffer where output digest value is stored
+ */
+void av_sha_final(struct AVSHA* context, uint8_t *digest);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_SHA_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/time.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/time.h
new file mode 100644
index 0000000000..90eb436949
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/time.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2000-2003 Fabrice Bellard
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_TIME_H
+#define AVUTIL_TIME_H
+
+#include <stdint.h>
+
+/**
+ * Get the current time in microseconds.
+ */
+int64_t av_gettime(void);
+
+/**
+ * Sleep for a period of time. Although the duration is expressed in
+ * microseconds, the actual delay may be rounded to the precision of the
+ * system timer.
+ *
+ * @param usec Number of microseconds to sleep.
+ * @return zero on success or (negative) error code.
+ */
+int av_usleep(unsigned usec);
+
+#endif /* AVUTIL_TIME_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/timecode.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/timecode.h
new file mode 100644
index 0000000000..56e3975fd8
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/timecode.h
@@ -0,0 +1,140 @@
+/*
+ * Copyright (c) 2006 Smartjog S.A.S, Baptiste Coudurier <baptiste.coudurier@gmail.com>
+ * Copyright (c) 2011-2012 Smartjog S.A.S, Clément Bœsch <clement.boesch@smartjog.com>
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * Timecode helpers header
+ */
+
+#ifndef AVUTIL_TIMECODE_H
+#define AVUTIL_TIMECODE_H
+
+#include <stdint.h>
+#include "rational.h"
+
+#define AV_TIMECODE_STR_SIZE 16
+
+enum AVTimecodeFlag {
+ AV_TIMECODE_FLAG_DROPFRAME = 1<<0, ///< timecode is drop frame
+ AV_TIMECODE_FLAG_24HOURSMAX = 1<<1, ///< timecode wraps after 24 hours
+ AV_TIMECODE_FLAG_ALLOWNEGATIVE = 1<<2, ///< negative time values are allowed
+};
+
+typedef struct {
+ int start; ///< timecode frame start (first base frame number)
+ uint32_t flags; ///< flags such as drop frame, +24 hours support, ...
+ AVRational rate; ///< frame rate in rational form
+ unsigned fps; ///< frame per second; must be consistent with the rate field
+} AVTimecode;
+
+/**
+ * Adjust frame number for NTSC drop frame time code.
+ *
+ * @param framenum frame number to adjust
+ * @param fps frame per second, 30 or 60
+ * @return adjusted frame number
+ * @warning adjustment is only valid in NTSC 29.97 and 59.94
+ */
+int av_timecode_adjust_ntsc_framenum2(int framenum, int fps);
+
+/**
+ * Convert frame number to SMPTE 12M binary representation.
+ *
+ * @param tc timecode data correctly initialized
+ * @param framenum frame number
+ * @return the SMPTE binary representation
+ *
+ * @note Frame number adjustment is automatically done in case of drop timecode,
+ * you do NOT have to call av_timecode_adjust_ntsc_framenum2().
+ * @note The frame number is relative to tc->start.
+ * @note Color frame (CF), binary group flags (BGF) and biphase mark polarity
+ * correction (PC) bits are set to zero.
+ */
+uint32_t av_timecode_get_smpte_from_framenum(const AVTimecode *tc, int framenum);
+
+/**
+ * Load timecode string in buf.
+ *
+ * @param buf destination buffer, must be at least AV_TIMECODE_STR_SIZE long
+ * @param tc timecode data correctly initialized
+ * @param framenum frame number
+ * @return the buf parameter
+ *
+ * @note Timecode representation can be a negative timecode and have more than
+ * 24 hours, but will only be honored if the flags are correctly set.
+ * @note The frame number is relative to tc->start.
+ */
+char *av_timecode_make_string(const AVTimecode *tc, char *buf, int framenum);
+
+/**
+ * Get the timecode string from the SMPTE timecode format.
+ *
+ * @param buf destination buffer, must be at least AV_TIMECODE_STR_SIZE long
+ * @param tcsmpte the 32-bit SMPTE timecode
+ * @param prevent_df prevent the use of a drop flag when it is known the DF bit
+ * is arbitrary
+ * @return the buf parameter
+ */
+char *av_timecode_make_smpte_tc_string(char *buf, uint32_t tcsmpte, int prevent_df);
+
+/**
+ * Get the timecode string from the 25-bit timecode format (MPEG GOP format).
+ *
+ * @param buf destination buffer, must be at least AV_TIMECODE_STR_SIZE long
+ * @param tc25bit the 25-bits timecode
+ * @return the buf parameter
+ */
+char *av_timecode_make_mpeg_tc_string(char *buf, uint32_t tc25bit);
+
+/**
+ * Init a timecode struct with the passed parameters.
+ *
+ * @param log_ctx a pointer to an arbitrary struct of which the first field
+ * is a pointer to an AVClass struct (used for av_log)
+ * @param tc pointer to an allocated AVTimecode
+ * @param rate frame rate in rational form
+ * @param flags miscellaneous flags such as drop frame, +24 hours, ...
+ * (see AVTimecodeFlag)
+ * @param frame_start the first frame number
+ * @return 0 on success, AVERROR otherwise
+ */
+int av_timecode_init(AVTimecode *tc, AVRational rate, int flags, int frame_start, void *log_ctx);
+
+/**
+ * Parse timecode representation (hh:mm:ss[:;.]ff).
+ *
+ * @param log_ctx a pointer to an arbitrary struct of which the first field is a
+ * pointer to an AVClass struct (used for av_log).
+ * @param tc pointer to an allocated AVTimecode
+ * @param rate frame rate in rational form
+ * @param str timecode string which will determine the frame start
+ * @return 0 on success, AVERROR otherwise
+ */
+int av_timecode_init_from_string(AVTimecode *tc, AVRational rate, const char *str, void *log_ctx);
+
+/**
+ * Check if the timecode feature is available for the given frame rate
+ *
+ * @return 0 if supported, <0 otherwise
+ */
+int av_timecode_check_frame_rate(AVRational rate);
+
+#endif /* AVUTIL_TIMECODE_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/timestamp.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/timestamp.h
new file mode 100644
index 0000000000..c7348d8f12
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/timestamp.h
@@ -0,0 +1,74 @@
+/*
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * timestamp utils, mostly useful for debugging/logging purposes
+ */
+
+#ifndef AVUTIL_TIMESTAMP_H
+#define AVUTIL_TIMESTAMP_H
+
+#include "common.h"
+
+#define AV_TS_MAX_STRING_SIZE 32
+
+/**
+ * Fill the provided buffer with a string containing a timestamp
+ * representation.
+ *
+ * @param buf a buffer with size in bytes of at least AV_TS_MAX_STRING_SIZE
+ * @param ts the timestamp to represent
+ * @return the buffer in input
+ */
+static inline char *av_ts_make_string(char *buf, int64_t ts)
+{
+ if (ts == AV_NOPTS_VALUE) snprintf(buf, AV_TS_MAX_STRING_SIZE, "NOPTS");
+ else snprintf(buf, AV_TS_MAX_STRING_SIZE, "%"PRId64"", ts);
+ return buf;
+}
+
+/**
+ * Convenience macro, the return value should be used only directly in
+ * function arguments but never stand-alone.
+ */
+#define av_ts2str(ts) av_ts_make_string((char[AV_TS_MAX_STRING_SIZE]){0}, ts)
+
+/**
+ * Fill the provided buffer with a string containing a timestamp time
+ * representation.
+ *
+ * @param buf a buffer with size in bytes of at least AV_TS_MAX_STRING_SIZE
+ * @param ts the timestamp to represent
+ * @param tb the timebase of the timestamp
+ * @return the buffer in input
+ */
+static inline char *av_ts_make_time_string(char *buf, int64_t ts, AVRational *tb)
+{
+ if (ts == AV_NOPTS_VALUE) snprintf(buf, AV_TS_MAX_STRING_SIZE, "NOPTS");
+ else snprintf(buf, AV_TS_MAX_STRING_SIZE, "%.6g", av_q2d(*tb) * ts);
+ return buf;
+}
+
+/**
+ * Convenience macro, the return value should be used only directly in
+ * function arguments but never stand-alone.
+ */
+#define av_ts2timestr(ts, tb) av_ts_make_time_string((char[AV_TS_MAX_STRING_SIZE]){0}, ts, tb)
+
+#endif /* AVUTIL_TIMESTAMP_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/version.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/version.h
new file mode 100644
index 0000000000..4ad244f406
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/version.h
@@ -0,0 +1,132 @@
+/*
+ * copyright (c) 2003 Fabrice Bellard
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_VERSION_H
+#define AVUTIL_VERSION_H
+
+/**
+ * @defgroup preproc_misc Preprocessor String Macros
+ *
+ * String manipulation macros
+ *
+ * @{
+ */
+
+#define AV_STRINGIFY(s) AV_TOSTRING(s)
+#define AV_TOSTRING(s) #s
+
+#define AV_GLUE(a, b) a ## b
+#define AV_JOIN(a, b) AV_GLUE(a, b)
+
+#define AV_PRAGMA(s) _Pragma(#s)
+
+/**
+ * @}
+ */
+
+/**
+ * @defgroup version_utils Library Version Macros
+ *
+ * Useful to check and match library version in order to maintain
+ * backward compatibility.
+ *
+ * @{
+ */
+
+#define AV_VERSION_INT(a, b, c) (a<<16 | b<<8 | c)
+#define AV_VERSION_DOT(a, b, c) a ##.## b ##.## c
+#define AV_VERSION(a, b, c) AV_VERSION_DOT(a, b, c)
+
+/**
+ * @}
+ */
+
+
+/**
+ * @file
+ * @ingroup lavu
+ * Libavutil version macros
+ */
+
+/**
+ * @defgroup lavu_ver Version and Build diagnostics
+ *
+ * Macros and function useful to check at compiletime and at runtime
+ * which version of libavutil is in use.
+ *
+ * @{
+ */
+
+#define LIBAVUTIL_VERSION_MAJOR 52
+#define LIBAVUTIL_VERSION_MINOR 5
+#define LIBAVUTIL_VERSION_MICRO 100
+
+#define LIBAVUTIL_VERSION_INT AV_VERSION_INT(LIBAVUTIL_VERSION_MAJOR, \
+ LIBAVUTIL_VERSION_MINOR, \
+ LIBAVUTIL_VERSION_MICRO)
+#define LIBAVUTIL_VERSION AV_VERSION(LIBAVUTIL_VERSION_MAJOR, \
+ LIBAVUTIL_VERSION_MINOR, \
+ LIBAVUTIL_VERSION_MICRO)
+#define LIBAVUTIL_BUILD LIBAVUTIL_VERSION_INT
+
+#define LIBAVUTIL_IDENT "Lavu" AV_STRINGIFY(LIBAVUTIL_VERSION)
+
+/**
+ * @}
+ *
+ * @defgroup depr_guards Deprecation guards
+ * FF_API_* defines may be placed below to indicate public API that will be
+ * dropped at a future version bump. The defines themselves are not part of
+ * the public API and may change, break or disappear at any time.
+ *
+ * @{
+ */
+
+#ifndef FF_API_OLD_EVAL_NAMES
+#define FF_API_OLD_EVAL_NAMES (LIBAVUTIL_VERSION_MAJOR < 52)
+#endif
+#ifndef FF_API_GET_BITS_PER_SAMPLE_FMT
+#define FF_API_GET_BITS_PER_SAMPLE_FMT (LIBAVUTIL_VERSION_MAJOR < 53)
+#endif
+#ifndef FF_API_FIND_OPT
+#define FF_API_FIND_OPT (LIBAVUTIL_VERSION_MAJOR < 53)
+#endif
+#ifndef FF_API_OLD_AVOPTIONS
+#define FF_API_OLD_AVOPTIONS (LIBAVUTIL_VERSION_MAJOR < 53)
+#endif
+#ifndef FF_API_PIX_FMT
+#define FF_API_PIX_FMT (LIBAVUTIL_VERSION_MAJOR < 53)
+#endif
+#ifndef FF_API_CONTEXT_SIZE
+#define FF_API_CONTEXT_SIZE (LIBAVUTIL_VERSION_MAJOR < 53)
+#endif
+#ifndef FF_API_PIX_FMT_DESC
+#define FF_API_PIX_FMT_DESC (LIBAVUTIL_VERSION_MAJOR < 53)
+#endif
+#ifndef FF_API_AV_REVERSE
+#define FF_API_AV_REVERSE (LIBAVUTIL_VERSION_MAJOR < 53)
+#endif
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_VERSION_H */
+
diff --git a/dom/media/platforms/ffmpeg/libav53/include/libavutil/xtea.h b/dom/media/platforms/ffmpeg/libav53/include/libavutil/xtea.h
new file mode 100644
index 0000000000..0899c92bc8
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/include/libavutil/xtea.h
@@ -0,0 +1,62 @@
+/*
+ * A 32-bit implementation of the XTEA algorithm
+ * Copyright (c) 2012 Samuel Pitoiset
+ *
+ * This file is part of FFmpeg.
+ *
+ * FFmpeg is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * FFmpeg is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with FFmpeg; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_XTEA_H
+#define AVUTIL_XTEA_H
+
+#include <stdint.h>
+
+/**
+ * @defgroup lavu_xtea XTEA
+ * @ingroup lavu_crypto
+ * @{
+ */
+
+typedef struct AVXTEA {
+ uint32_t key[16];
+} AVXTEA;
+
+/**
+ * Initialize an AVXTEA context.
+ *
+ * @param ctx an AVXTEA context
+ * @param key a key of 16 bytes used for encryption/decryption
+ */
+void av_xtea_init(struct AVXTEA *ctx, const uint8_t key[16]);
+
+/**
+ * Encrypt or decrypt a buffer using a previously initialized context.
+ *
+ * @param ctx an AVXTEA context
+ * @param dst destination array, can be equal to src
+ * @param src source array, can be equal to dst
+ * @param count number of 8 byte blocks
+ * @param iv initialization vector for CBC mode, if NULL then ECB will be used
+ * @param decrypt 0 for encryption, 1 for decryption
+ */
+void av_xtea_crypt(struct AVXTEA *ctx, uint8_t *dst, const uint8_t *src,
+ int count, uint8_t *iv, int decrypt);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_XTEA_H */
diff --git a/dom/media/platforms/ffmpeg/libav53/moz.build b/dom/media/platforms/ffmpeg/libav53/moz.build
new file mode 100644
index 0000000000..d956f077f3
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav53/moz.build
@@ -0,0 +1,23 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES += [
+ '../FFmpegAudioDecoder.cpp',
+ '../FFmpegDataDecoder.cpp',
+ '../FFmpegDecoderModule.cpp',
+ '../FFmpegVideoDecoder.cpp',
+]
+LOCAL_INCLUDES += [
+ '..',
+ 'include',
+]
+
+FINAL_LIBRARY = 'xul'
+
+if CONFIG['CC_TYPE'] == 'clang':
+ CXXFLAGS += [
+ '-Wno-unknown-attributes',
+ ]
diff --git a/dom/media/platforms/ffmpeg/libav54/include/COPYING.LGPLv2.1 b/dom/media/platforms/ffmpeg/libav54/include/COPYING.LGPLv2.1
new file mode 100644
index 0000000000..00b4fedfe7
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/COPYING.LGPLv2.1
@@ -0,0 +1,504 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the library's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+ <signature of Ty Coon>, 1 April 1990
+ Ty Coon, President of Vice
+
+That's all there is to it!
+
+
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavcodec/avcodec.h b/dom/media/platforms/ffmpeg/libav54/include/libavcodec/avcodec.h
new file mode 100644
index 0000000000..e6b8ec626f
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavcodec/avcodec.h
@@ -0,0 +1,4658 @@
+/*
+ * copyright (c) 2001 Fabrice Bellard
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_AVCODEC_H
+#define AVCODEC_AVCODEC_H
+
+/**
+ * @file
+ * external API header
+ */
+
+#include <errno.h>
+#include "libavutil/samplefmt.h"
+#include "libavutil/avutil.h"
+#include "libavutil/cpu.h"
+#include "libavutil/dict.h"
+#include "libavutil/log.h"
+#include "libavutil/pixfmt.h"
+#include "libavutil/rational.h"
+
+#include "libavcodec/version.h"
+/**
+ * @defgroup libavc Encoding/Decoding Library
+ * @{
+ *
+ * @defgroup lavc_decoding Decoding
+ * @{
+ * @}
+ *
+ * @defgroup lavc_encoding Encoding
+ * @{
+ * @}
+ *
+ * @defgroup lavc_codec Codecs
+ * @{
+ * @defgroup lavc_codec_native Native Codecs
+ * @{
+ * @}
+ * @defgroup lavc_codec_wrappers External library wrappers
+ * @{
+ * @}
+ * @defgroup lavc_codec_hwaccel Hardware Accelerators bridge
+ * @{
+ * @}
+ * @}
+ * @defgroup lavc_internal Internal
+ * @{
+ * @}
+ * @}
+ *
+ */
+
+/**
+ * @defgroup lavc_core Core functions/structures.
+ * @ingroup libavc
+ *
+ * Basic definitions, functions for querying libavcodec capabilities,
+ * allocating core structures, etc.
+ * @{
+ */
+
+
+/**
+ * Identify the syntax and semantics of the bitstream.
+ * The principle is roughly:
+ * Two decoders with the same ID can decode the same streams.
+ * Two encoders with the same ID can encode compatible streams.
+ * There may be slight deviations from the principle due to implementation
+ * details.
+ *
+ * If you add a codec ID to this list, add it so that
+ * 1. no value of a existing codec ID changes (that would break ABI),
+ * 2. it is as close as possible to similar codecs.
+ *
+ * After adding new codec IDs, do not forget to add an entry to the codec
+ * descriptor list and bump libavcodec minor version.
+ */
+enum AVCodecID {
+ AV_CODEC_ID_NONE,
+
+ /* video codecs */
+ AV_CODEC_ID_MPEG1VIDEO,
+ AV_CODEC_ID_MPEG2VIDEO, ///< preferred ID for MPEG-1/2 video decoding
+ AV_CODEC_ID_MPEG2VIDEO_XVMC,
+ AV_CODEC_ID_H261,
+ AV_CODEC_ID_H263,
+ AV_CODEC_ID_RV10,
+ AV_CODEC_ID_RV20,
+ AV_CODEC_ID_MJPEG,
+ AV_CODEC_ID_MJPEGB,
+ AV_CODEC_ID_LJPEG,
+ AV_CODEC_ID_SP5X,
+ AV_CODEC_ID_JPEGLS,
+ AV_CODEC_ID_MPEG4,
+ AV_CODEC_ID_RAWVIDEO,
+ AV_CODEC_ID_MSMPEG4V1,
+ AV_CODEC_ID_MSMPEG4V2,
+ AV_CODEC_ID_MSMPEG4V3,
+ AV_CODEC_ID_WMV1,
+ AV_CODEC_ID_WMV2,
+ AV_CODEC_ID_H263P,
+ AV_CODEC_ID_H263I,
+ AV_CODEC_ID_FLV1,
+ AV_CODEC_ID_SVQ1,
+ AV_CODEC_ID_SVQ3,
+ AV_CODEC_ID_DVVIDEO,
+ AV_CODEC_ID_HUFFYUV,
+ AV_CODEC_ID_CYUV,
+ AV_CODEC_ID_H264,
+ AV_CODEC_ID_INDEO3,
+ AV_CODEC_ID_VP3,
+ AV_CODEC_ID_THEORA,
+ AV_CODEC_ID_ASV1,
+ AV_CODEC_ID_ASV2,
+ AV_CODEC_ID_FFV1,
+ AV_CODEC_ID_4XM,
+ AV_CODEC_ID_VCR1,
+ AV_CODEC_ID_CLJR,
+ AV_CODEC_ID_MDEC,
+ AV_CODEC_ID_ROQ,
+ AV_CODEC_ID_INTERPLAY_VIDEO,
+ AV_CODEC_ID_XAN_WC3,
+ AV_CODEC_ID_XAN_WC4,
+ AV_CODEC_ID_RPZA,
+ AV_CODEC_ID_CINEPAK,
+ AV_CODEC_ID_WS_VQA,
+ AV_CODEC_ID_MSRLE,
+ AV_CODEC_ID_MSVIDEO1,
+ AV_CODEC_ID_IDCIN,
+ AV_CODEC_ID_8BPS,
+ AV_CODEC_ID_SMC,
+ AV_CODEC_ID_FLIC,
+ AV_CODEC_ID_TRUEMOTION1,
+ AV_CODEC_ID_VMDVIDEO,
+ AV_CODEC_ID_MSZH,
+ AV_CODEC_ID_ZLIB,
+ AV_CODEC_ID_QTRLE,
+ AV_CODEC_ID_SNOW,
+ AV_CODEC_ID_TSCC,
+ AV_CODEC_ID_ULTI,
+ AV_CODEC_ID_QDRAW,
+ AV_CODEC_ID_VIXL,
+ AV_CODEC_ID_QPEG,
+ AV_CODEC_ID_PNG,
+ AV_CODEC_ID_PPM,
+ AV_CODEC_ID_PBM,
+ AV_CODEC_ID_PGM,
+ AV_CODEC_ID_PGMYUV,
+ AV_CODEC_ID_PAM,
+ AV_CODEC_ID_FFVHUFF,
+ AV_CODEC_ID_RV30,
+ AV_CODEC_ID_RV40,
+ AV_CODEC_ID_VC1,
+ AV_CODEC_ID_WMV3,
+ AV_CODEC_ID_LOCO,
+ AV_CODEC_ID_WNV1,
+ AV_CODEC_ID_AASC,
+ AV_CODEC_ID_INDEO2,
+ AV_CODEC_ID_FRAPS,
+ AV_CODEC_ID_TRUEMOTION2,
+ AV_CODEC_ID_BMP,
+ AV_CODEC_ID_CSCD,
+ AV_CODEC_ID_MMVIDEO,
+ AV_CODEC_ID_ZMBV,
+ AV_CODEC_ID_AVS,
+ AV_CODEC_ID_SMACKVIDEO,
+ AV_CODEC_ID_NUV,
+ AV_CODEC_ID_KMVC,
+ AV_CODEC_ID_FLASHSV,
+ AV_CODEC_ID_CAVS,
+ AV_CODEC_ID_JPEG2000,
+ AV_CODEC_ID_VMNC,
+ AV_CODEC_ID_VP5,
+ AV_CODEC_ID_VP6,
+ AV_CODEC_ID_VP6F,
+ AV_CODEC_ID_TARGA,
+ AV_CODEC_ID_DSICINVIDEO,
+ AV_CODEC_ID_TIERTEXSEQVIDEO,
+ AV_CODEC_ID_TIFF,
+ AV_CODEC_ID_GIF,
+ AV_CODEC_ID_DXA,
+ AV_CODEC_ID_DNXHD,
+ AV_CODEC_ID_THP,
+ AV_CODEC_ID_SGI,
+ AV_CODEC_ID_C93,
+ AV_CODEC_ID_BETHSOFTVID,
+ AV_CODEC_ID_PTX,
+ AV_CODEC_ID_TXD,
+ AV_CODEC_ID_VP6A,
+ AV_CODEC_ID_AMV,
+ AV_CODEC_ID_VB,
+ AV_CODEC_ID_PCX,
+ AV_CODEC_ID_SUNRAST,
+ AV_CODEC_ID_INDEO4,
+ AV_CODEC_ID_INDEO5,
+ AV_CODEC_ID_MIMIC,
+ AV_CODEC_ID_RL2,
+ AV_CODEC_ID_ESCAPE124,
+ AV_CODEC_ID_DIRAC,
+ AV_CODEC_ID_BFI,
+ AV_CODEC_ID_CMV,
+ AV_CODEC_ID_MOTIONPIXELS,
+ AV_CODEC_ID_TGV,
+ AV_CODEC_ID_TGQ,
+ AV_CODEC_ID_TQI,
+ AV_CODEC_ID_AURA,
+ AV_CODEC_ID_AURA2,
+ AV_CODEC_ID_V210X,
+ AV_CODEC_ID_TMV,
+ AV_CODEC_ID_V210,
+ AV_CODEC_ID_DPX,
+ AV_CODEC_ID_MAD,
+ AV_CODEC_ID_FRWU,
+ AV_CODEC_ID_FLASHSV2,
+ AV_CODEC_ID_CDGRAPHICS,
+ AV_CODEC_ID_R210,
+ AV_CODEC_ID_ANM,
+ AV_CODEC_ID_BINKVIDEO,
+ AV_CODEC_ID_IFF_ILBM,
+ AV_CODEC_ID_IFF_BYTERUN1,
+ AV_CODEC_ID_KGV1,
+ AV_CODEC_ID_YOP,
+ AV_CODEC_ID_VP8,
+ AV_CODEC_ID_PICTOR,
+ AV_CODEC_ID_ANSI,
+ AV_CODEC_ID_A64_MULTI,
+ AV_CODEC_ID_A64_MULTI5,
+ AV_CODEC_ID_R10K,
+ AV_CODEC_ID_MXPEG,
+ AV_CODEC_ID_LAGARITH,
+ AV_CODEC_ID_PRORES,
+ AV_CODEC_ID_JV,
+ AV_CODEC_ID_DFA,
+ AV_CODEC_ID_WMV3IMAGE,
+ AV_CODEC_ID_VC1IMAGE,
+ AV_CODEC_ID_UTVIDEO,
+ AV_CODEC_ID_BMV_VIDEO,
+ AV_CODEC_ID_VBLE,
+ AV_CODEC_ID_DXTORY,
+ AV_CODEC_ID_V410,
+ AV_CODEC_ID_XWD,
+ AV_CODEC_ID_CDXL,
+ AV_CODEC_ID_XBM,
+ AV_CODEC_ID_ZEROCODEC,
+ AV_CODEC_ID_MSS1,
+ AV_CODEC_ID_MSA1,
+ AV_CODEC_ID_TSCC2,
+ AV_CODEC_ID_MTS2,
+ AV_CODEC_ID_CLLC,
+ AV_CODEC_ID_MSS2,
+
+ /* various PCM "codecs" */
+ AV_CODEC_ID_FIRST_AUDIO = 0x10000, ///< A dummy id pointing at the start of audio codecs
+ AV_CODEC_ID_PCM_S16LE = 0x10000,
+ AV_CODEC_ID_PCM_S16BE,
+ AV_CODEC_ID_PCM_U16LE,
+ AV_CODEC_ID_PCM_U16BE,
+ AV_CODEC_ID_PCM_S8,
+ AV_CODEC_ID_PCM_U8,
+ AV_CODEC_ID_PCM_MULAW,
+ AV_CODEC_ID_PCM_ALAW,
+ AV_CODEC_ID_PCM_S32LE,
+ AV_CODEC_ID_PCM_S32BE,
+ AV_CODEC_ID_PCM_U32LE,
+ AV_CODEC_ID_PCM_U32BE,
+ AV_CODEC_ID_PCM_S24LE,
+ AV_CODEC_ID_PCM_S24BE,
+ AV_CODEC_ID_PCM_U24LE,
+ AV_CODEC_ID_PCM_U24BE,
+ AV_CODEC_ID_PCM_S24DAUD,
+ AV_CODEC_ID_PCM_ZORK,
+ AV_CODEC_ID_PCM_S16LE_PLANAR,
+ AV_CODEC_ID_PCM_DVD,
+ AV_CODEC_ID_PCM_F32BE,
+ AV_CODEC_ID_PCM_F32LE,
+ AV_CODEC_ID_PCM_F64BE,
+ AV_CODEC_ID_PCM_F64LE,
+ AV_CODEC_ID_PCM_BLURAY,
+ AV_CODEC_ID_PCM_LXF,
+ AV_CODEC_ID_S302M,
+ AV_CODEC_ID_PCM_S8_PLANAR,
+
+ /* various ADPCM codecs */
+ AV_CODEC_ID_ADPCM_IMA_QT = 0x11000,
+ AV_CODEC_ID_ADPCM_IMA_WAV,
+ AV_CODEC_ID_ADPCM_IMA_DK3,
+ AV_CODEC_ID_ADPCM_IMA_DK4,
+ AV_CODEC_ID_ADPCM_IMA_WS,
+ AV_CODEC_ID_ADPCM_IMA_SMJPEG,
+ AV_CODEC_ID_ADPCM_MS,
+ AV_CODEC_ID_ADPCM_4XM,
+ AV_CODEC_ID_ADPCM_XA,
+ AV_CODEC_ID_ADPCM_ADX,
+ AV_CODEC_ID_ADPCM_EA,
+ AV_CODEC_ID_ADPCM_G726,
+ AV_CODEC_ID_ADPCM_CT,
+ AV_CODEC_ID_ADPCM_SWF,
+ AV_CODEC_ID_ADPCM_YAMAHA,
+ AV_CODEC_ID_ADPCM_SBPRO_4,
+ AV_CODEC_ID_ADPCM_SBPRO_3,
+ AV_CODEC_ID_ADPCM_SBPRO_2,
+ AV_CODEC_ID_ADPCM_THP,
+ AV_CODEC_ID_ADPCM_IMA_AMV,
+ AV_CODEC_ID_ADPCM_EA_R1,
+ AV_CODEC_ID_ADPCM_EA_R3,
+ AV_CODEC_ID_ADPCM_EA_R2,
+ AV_CODEC_ID_ADPCM_IMA_EA_SEAD,
+ AV_CODEC_ID_ADPCM_IMA_EA_EACS,
+ AV_CODEC_ID_ADPCM_EA_XAS,
+ AV_CODEC_ID_ADPCM_EA_MAXIS_XA,
+ AV_CODEC_ID_ADPCM_IMA_ISS,
+ AV_CODEC_ID_ADPCM_G722,
+ AV_CODEC_ID_ADPCM_IMA_APC,
+
+ /* AMR */
+ AV_CODEC_ID_AMR_NB = 0x12000,
+ AV_CODEC_ID_AMR_WB,
+
+ /* RealAudio codecs*/
+ AV_CODEC_ID_RA_144 = 0x13000,
+ AV_CODEC_ID_RA_288,
+
+ /* various DPCM codecs */
+ AV_CODEC_ID_ROQ_DPCM = 0x14000,
+ AV_CODEC_ID_INTERPLAY_DPCM,
+ AV_CODEC_ID_XAN_DPCM,
+ AV_CODEC_ID_SOL_DPCM,
+
+ /* audio codecs */
+ AV_CODEC_ID_MP2 = 0x15000,
+ AV_CODEC_ID_MP3, ///< preferred ID for decoding MPEG audio layer 1, 2 or 3
+ AV_CODEC_ID_AAC,
+ AV_CODEC_ID_AC3,
+ AV_CODEC_ID_DTS,
+ AV_CODEC_ID_VORBIS,
+ AV_CODEC_ID_DVAUDIO,
+ AV_CODEC_ID_WMAV1,
+ AV_CODEC_ID_WMAV2,
+ AV_CODEC_ID_MACE3,
+ AV_CODEC_ID_MACE6,
+ AV_CODEC_ID_VMDAUDIO,
+ AV_CODEC_ID_FLAC,
+ AV_CODEC_ID_MP3ADU,
+ AV_CODEC_ID_MP3ON4,
+ AV_CODEC_ID_SHORTEN,
+ AV_CODEC_ID_ALAC,
+ AV_CODEC_ID_WESTWOOD_SND1,
+ AV_CODEC_ID_GSM, ///< as in Berlin toast format
+ AV_CODEC_ID_QDM2,
+ AV_CODEC_ID_COOK,
+ AV_CODEC_ID_TRUESPEECH,
+ AV_CODEC_ID_TTA,
+ AV_CODEC_ID_SMACKAUDIO,
+ AV_CODEC_ID_QCELP,
+ AV_CODEC_ID_WAVPACK,
+ AV_CODEC_ID_DSICINAUDIO,
+ AV_CODEC_ID_IMC,
+ AV_CODEC_ID_MUSEPACK7,
+ AV_CODEC_ID_MLP,
+ AV_CODEC_ID_GSM_MS, /* as found in WAV */
+ AV_CODEC_ID_ATRAC3,
+ AV_CODEC_ID_VOXWARE,
+ AV_CODEC_ID_APE,
+ AV_CODEC_ID_NELLYMOSER,
+ AV_CODEC_ID_MUSEPACK8,
+ AV_CODEC_ID_SPEEX,
+ AV_CODEC_ID_WMAVOICE,
+ AV_CODEC_ID_WMAPRO,
+ AV_CODEC_ID_WMALOSSLESS,
+ AV_CODEC_ID_ATRAC3P,
+ AV_CODEC_ID_EAC3,
+ AV_CODEC_ID_SIPR,
+ AV_CODEC_ID_MP1,
+ AV_CODEC_ID_TWINVQ,
+ AV_CODEC_ID_TRUEHD,
+ AV_CODEC_ID_MP4ALS,
+ AV_CODEC_ID_ATRAC1,
+ AV_CODEC_ID_BINKAUDIO_RDFT,
+ AV_CODEC_ID_BINKAUDIO_DCT,
+ AV_CODEC_ID_AAC_LATM,
+ AV_CODEC_ID_QDMC,
+ AV_CODEC_ID_CELT,
+ AV_CODEC_ID_G723_1,
+ AV_CODEC_ID_G729,
+ AV_CODEC_ID_8SVX_EXP,
+ AV_CODEC_ID_8SVX_FIB,
+ AV_CODEC_ID_BMV_AUDIO,
+ AV_CODEC_ID_RALF,
+ AV_CODEC_ID_IAC,
+ AV_CODEC_ID_ILBC,
+ AV_CODEC_ID_OPUS,
+ AV_CODEC_ID_COMFORT_NOISE,
+ AV_CODEC_ID_TAK,
+
+ /* subtitle codecs */
+ AV_CODEC_ID_FIRST_SUBTITLE = 0x17000, ///< A dummy ID pointing at the start of subtitle codecs.
+ AV_CODEC_ID_DVD_SUBTITLE = 0x17000,
+ AV_CODEC_ID_DVB_SUBTITLE,
+ AV_CODEC_ID_TEXT, ///< raw UTF-8 text
+ AV_CODEC_ID_XSUB,
+ AV_CODEC_ID_SSA,
+ AV_CODEC_ID_MOV_TEXT,
+ AV_CODEC_ID_HDMV_PGS_SUBTITLE,
+ AV_CODEC_ID_DVB_TELETEXT,
+ AV_CODEC_ID_SRT,
+
+ /* other specific kind of codecs (generally used for attachments) */
+ AV_CODEC_ID_FIRST_UNKNOWN = 0x18000, ///< A dummy ID pointing at the start of various fake codecs.
+ AV_CODEC_ID_TTF = 0x18000,
+
+ AV_CODEC_ID_PROBE = 0x19000, ///< codec_id is not known (like AV_CODEC_ID_NONE) but lavf should attempt to identify it
+
+ AV_CODEC_ID_MPEG2TS = 0x20000, /**< _FAKE_ codec to indicate a raw MPEG-2 TS
+ * stream (only used by libavformat) */
+ AV_CODEC_ID_MPEG4SYSTEMS = 0x20001, /**< _FAKE_ codec to indicate a MPEG-4 Systems
+ * stream (only used by libavformat) */
+ AV_CODEC_ID_FFMETADATA = 0x21000, ///< Dummy codec for streams containing only metadata information.
+
+#if FF_API_CODEC_ID
+#include "old_codec_ids.h"
+#endif
+};
+
+#if FF_API_CODEC_ID
+#define CodecID AVCodecID
+#endif
+
+/**
+ * This struct describes the properties of a single codec described by an
+ * AVCodecID.
+ * @see avcodec_get_descriptor()
+ */
+typedef struct AVCodecDescriptor {
+ enum AVCodecID id;
+ enum AVMediaType type;
+ /**
+ * Name of the codec described by this descriptor. It is non-empty and
+ * unique for each codec descriptor. It should contain alphanumeric
+ * characters and '_' only.
+ */
+ const char *name;
+ /**
+ * A more descriptive name for this codec. May be NULL.
+ */
+ const char *long_name;
+ /**
+ * Codec properties, a combination of AV_CODEC_PROP_* flags.
+ */
+ int props;
+} AVCodecDescriptor;
+
+/**
+ * Codec uses only intra compression.
+ * Video codecs only.
+ */
+#define AV_CODEC_PROP_INTRA_ONLY (1 << 0)
+/**
+ * Codec supports lossy compression. Audio and video codecs only.
+ * @note a codec may support both lossy and lossless
+ * compression modes
+ */
+#define AV_CODEC_PROP_LOSSY (1 << 1)
+/**
+ * Codec supports lossless compression. Audio and video codecs only.
+ */
+#define AV_CODEC_PROP_LOSSLESS (1 << 2)
+
+#if FF_API_OLD_DECODE_AUDIO
+/* in bytes */
+#define AVCODEC_MAX_AUDIO_FRAME_SIZE 192000 // 1 second of 48khz 32bit audio
+#endif
+
+/**
+ * @ingroup lavc_decoding
+ * Required number of additionally allocated bytes at the end of the input bitstream for decoding.
+ * This is mainly needed because some optimized bitstream readers read
+ * 32 or 64 bit at once and could read over the end.<br>
+ * Note: If the first 23 bits of the additional bytes are not 0, then damaged
+ * MPEG bitstreams could cause overread and segfault.
+ */
+#define FF_INPUT_BUFFER_PADDING_SIZE 8
+
+/**
+ * @ingroup lavc_encoding
+ * minimum encoding buffer size
+ * Used to avoid some checks during header writing.
+ */
+#define FF_MIN_BUFFER_SIZE 16384
+
+
+/**
+ * @ingroup lavc_encoding
+ * motion estimation type.
+ */
+enum Motion_Est_ID {
+ ME_ZERO = 1, ///< no search, that is use 0,0 vector whenever one is needed
+ ME_FULL,
+ ME_LOG,
+ ME_PHODS,
+ ME_EPZS, ///< enhanced predictive zonal search
+ ME_X1, ///< reserved for experiments
+ ME_HEX, ///< hexagon based search
+ ME_UMH, ///< uneven multi-hexagon search
+ ME_ITER, ///< iterative search
+ ME_TESA, ///< transformed exhaustive search algorithm
+};
+
+/**
+ * @ingroup lavc_decoding
+ */
+enum AVDiscard{
+ /* We leave some space between them for extensions (drop some
+ * keyframes for intra-only or drop just some bidir frames). */
+ AVDISCARD_NONE =-16, ///< discard nothing
+ AVDISCARD_DEFAULT = 0, ///< discard useless packets like 0 size packets in avi
+ AVDISCARD_NONREF = 8, ///< discard all non reference
+ AVDISCARD_BIDIR = 16, ///< discard all bidirectional frames
+ AVDISCARD_NONKEY = 32, ///< discard all frames except keyframes
+ AVDISCARD_ALL = 48, ///< discard all
+};
+
+enum AVColorPrimaries{
+ AVCOL_PRI_BT709 = 1, ///< also ITU-R BT1361 / IEC 61966-2-4 / SMPTE RP177 Annex B
+ AVCOL_PRI_UNSPECIFIED = 2,
+ AVCOL_PRI_BT470M = 4,
+ AVCOL_PRI_BT470BG = 5, ///< also ITU-R BT601-6 625 / ITU-R BT1358 625 / ITU-R BT1700 625 PAL & SECAM
+ AVCOL_PRI_SMPTE170M = 6, ///< also ITU-R BT601-6 525 / ITU-R BT1358 525 / ITU-R BT1700 NTSC
+ AVCOL_PRI_SMPTE240M = 7, ///< functionally identical to above
+ AVCOL_PRI_FILM = 8,
+ AVCOL_PRI_NB , ///< Not part of ABI
+};
+
+enum AVColorTransferCharacteristic{
+ AVCOL_TRC_BT709 = 1, ///< also ITU-R BT1361
+ AVCOL_TRC_UNSPECIFIED = 2,
+ AVCOL_TRC_GAMMA22 = 4, ///< also ITU-R BT470M / ITU-R BT1700 625 PAL & SECAM
+ AVCOL_TRC_GAMMA28 = 5, ///< also ITU-R BT470BG
+ AVCOL_TRC_SMPTE240M = 7,
+ AVCOL_TRC_NB , ///< Not part of ABI
+};
+
+enum AVColorSpace{
+ AVCOL_SPC_RGB = 0,
+ AVCOL_SPC_BT709 = 1, ///< also ITU-R BT1361 / IEC 61966-2-4 xvYCC709 / SMPTE RP177 Annex B
+ AVCOL_SPC_UNSPECIFIED = 2,
+ AVCOL_SPC_FCC = 4,
+ AVCOL_SPC_BT470BG = 5, ///< also ITU-R BT601-6 625 / ITU-R BT1358 625 / ITU-R BT1700 625 PAL & SECAM / IEC 61966-2-4 xvYCC601
+ AVCOL_SPC_SMPTE170M = 6, ///< also ITU-R BT601-6 525 / ITU-R BT1358 525 / ITU-R BT1700 NTSC / functionally identical to above
+ AVCOL_SPC_SMPTE240M = 7,
+ AVCOL_SPC_YCOCG = 8, ///< Used by Dirac / VC-2 and H.264 FRext, see ITU-T SG16
+ AVCOL_SPC_NB , ///< Not part of ABI
+};
+
+enum AVColorRange{
+ AVCOL_RANGE_UNSPECIFIED = 0,
+ AVCOL_RANGE_MPEG = 1, ///< the normal 219*2^(n-8) "MPEG" YUV ranges
+ AVCOL_RANGE_JPEG = 2, ///< the normal 2^n-1 "JPEG" YUV ranges
+ AVCOL_RANGE_NB , ///< Not part of ABI
+};
+
+/**
+ * X X 3 4 X X are luma samples,
+ * 1 2 1-6 are possible chroma positions
+ * X X 5 6 X 0 is undefined/unknown position
+ */
+enum AVChromaLocation{
+ AVCHROMA_LOC_UNSPECIFIED = 0,
+ AVCHROMA_LOC_LEFT = 1, ///< mpeg2/4, h264 default
+ AVCHROMA_LOC_CENTER = 2, ///< mpeg1, jpeg, h263
+ AVCHROMA_LOC_TOPLEFT = 3, ///< DV
+ AVCHROMA_LOC_TOP = 4,
+ AVCHROMA_LOC_BOTTOMLEFT = 5,
+ AVCHROMA_LOC_BOTTOM = 6,
+ AVCHROMA_LOC_NB , ///< Not part of ABI
+};
+
+enum AVAudioServiceType {
+ AV_AUDIO_SERVICE_TYPE_MAIN = 0,
+ AV_AUDIO_SERVICE_TYPE_EFFECTS = 1,
+ AV_AUDIO_SERVICE_TYPE_VISUALLY_IMPAIRED = 2,
+ AV_AUDIO_SERVICE_TYPE_HEARING_IMPAIRED = 3,
+ AV_AUDIO_SERVICE_TYPE_DIALOGUE = 4,
+ AV_AUDIO_SERVICE_TYPE_COMMENTARY = 5,
+ AV_AUDIO_SERVICE_TYPE_EMERGENCY = 6,
+ AV_AUDIO_SERVICE_TYPE_VOICE_OVER = 7,
+ AV_AUDIO_SERVICE_TYPE_KARAOKE = 8,
+ AV_AUDIO_SERVICE_TYPE_NB , ///< Not part of ABI
+};
+
+/**
+ * @ingroup lavc_encoding
+ */
+typedef struct RcOverride{
+ int start_frame;
+ int end_frame;
+ int qscale; // If this is 0 then quality_factor will be used instead.
+ float quality_factor;
+} RcOverride;
+
+#define FF_MAX_B_FRAMES 16
+
+/* encoding support
+ These flags can be passed in AVCodecContext.flags before initialization.
+ Note: Not everything is supported yet.
+*/
+
+#define CODEC_FLAG_QSCALE 0x0002 ///< Use fixed qscale.
+#define CODEC_FLAG_4MV 0x0004 ///< 4 MV per MB allowed / advanced prediction for H.263.
+#define CODEC_FLAG_QPEL 0x0010 ///< Use qpel MC.
+#define CODEC_FLAG_GMC 0x0020 ///< Use GMC.
+#define CODEC_FLAG_MV0 0x0040 ///< Always try a MB with MV=<0,0>.
+/**
+ * The parent program guarantees that the input for B-frames containing
+ * streams is not written to for at least s->max_b_frames+1 frames, if
+ * this is not set the input will be copied.
+ */
+#define CODEC_FLAG_INPUT_PRESERVED 0x0100
+#define CODEC_FLAG_PASS1 0x0200 ///< Use internal 2pass ratecontrol in first pass mode.
+#define CODEC_FLAG_PASS2 0x0400 ///< Use internal 2pass ratecontrol in second pass mode.
+#define CODEC_FLAG_GRAY 0x2000 ///< Only decode/encode grayscale.
+#define CODEC_FLAG_EMU_EDGE 0x4000 ///< Don't draw edges.
+#define CODEC_FLAG_PSNR 0x8000 ///< error[?] variables will be set during encoding.
+#define CODEC_FLAG_TRUNCATED 0x00010000 /** Input bitstream might be truncated at a random
+ location instead of only at frame boundaries. */
+#define CODEC_FLAG_NORMALIZE_AQP 0x00020000 ///< Normalize adaptive quantization.
+#define CODEC_FLAG_INTERLACED_DCT 0x00040000 ///< Use interlaced DCT.
+#define CODEC_FLAG_LOW_DELAY 0x00080000 ///< Force low delay.
+#define CODEC_FLAG_GLOBAL_HEADER 0x00400000 ///< Place global headers in extradata instead of every keyframe.
+#define CODEC_FLAG_BITEXACT 0x00800000 ///< Use only bitexact stuff (except (I)DCT).
+/* Fx : Flag for h263+ extra options */
+#define CODEC_FLAG_AC_PRED 0x01000000 ///< H.263 advanced intra coding / MPEG-4 AC prediction
+#define CODEC_FLAG_LOOP_FILTER 0x00000800 ///< loop filter
+#define CODEC_FLAG_INTERLACED_ME 0x20000000 ///< interlaced motion estimation
+#define CODEC_FLAG_CLOSED_GOP 0x80000000
+#define CODEC_FLAG2_FAST 0x00000001 ///< Allow non spec compliant speedup tricks.
+#define CODEC_FLAG2_NO_OUTPUT 0x00000004 ///< Skip bitstream encoding.
+#define CODEC_FLAG2_LOCAL_HEADER 0x00000008 ///< Place global headers at every keyframe instead of in extradata.
+#if FF_API_MPV_GLOBAL_OPTS
+#define CODEC_FLAG_CBP_RD 0x04000000 ///< Use rate distortion optimization for cbp.
+#define CODEC_FLAG_QP_RD 0x08000000 ///< Use rate distortion optimization for qp selectioon.
+#define CODEC_FLAG2_STRICT_GOP 0x00000002 ///< Strictly enforce GOP size.
+#define CODEC_FLAG2_SKIP_RD 0x00004000 ///< RD optimal MB level residual skipping
+#endif
+#define CODEC_FLAG2_CHUNKS 0x00008000 ///< Input bitstream might be truncated at a packet boundaries instead of only at frame boundaries.
+
+/* Unsupported options :
+ * Syntax Arithmetic coding (SAC)
+ * Reference Picture Selection
+ * Independent Segment Decoding */
+/* /Fx */
+/* codec capabilities */
+
+#define CODEC_CAP_DRAW_HORIZ_BAND 0x0001 ///< Decoder can use draw_horiz_band callback.
+/**
+ * Codec uses get_buffer() for allocating buffers and supports custom allocators.
+ * If not set, it might not use get_buffer() at all or use operations that
+ * assume the buffer was allocated by avcodec_default_get_buffer.
+ */
+#define CODEC_CAP_DR1 0x0002
+#define CODEC_CAP_TRUNCATED 0x0008
+/* Codec can export data for HW decoding (XvMC). */
+#define CODEC_CAP_HWACCEL 0x0010
+/**
+ * Encoder or decoder requires flushing with NULL input at the end in order to
+ * give the complete and correct output.
+ *
+ * NOTE: If this flag is not set, the codec is guaranteed to never be fed with
+ * with NULL data. The user can still send NULL data to the public encode
+ * or decode function, but libavcodec will not pass it along to the codec
+ * unless this flag is set.
+ *
+ * Decoders:
+ * The decoder has a non-zero delay and needs to be fed with avpkt->data=NULL,
+ * avpkt->size=0 at the end to get the delayed data until the decoder no longer
+ * returns frames.
+ *
+ * Encoders:
+ * The encoder needs to be fed with NULL data at the end of encoding until the
+ * encoder no longer returns data.
+ *
+ * NOTE: For encoders implementing the AVCodec.encode2() function, setting this
+ * flag also means that the encoder must set the pts and duration for
+ * each output packet. If this flag is not set, the pts and duration will
+ * be determined by libavcodec from the input frame.
+ */
+#define CODEC_CAP_DELAY 0x0020
+/**
+ * Codec can be fed a final frame with a smaller size.
+ * This can be used to prevent truncation of the last audio samples.
+ */
+#define CODEC_CAP_SMALL_LAST_FRAME 0x0040
+/**
+ * Codec can export data for HW decoding (VDPAU).
+ */
+#define CODEC_CAP_HWACCEL_VDPAU 0x0080
+/**
+ * Codec can output multiple frames per AVPacket
+ * Normally demuxers return one frame at a time, demuxers which do not do
+ * are connected to a parser to split what they return into proper frames.
+ * This flag is reserved to the very rare category of codecs which have a
+ * bitstream that cannot be split into frames without timeconsuming
+ * operations like full decoding. Demuxers carring such bitstreams thus
+ * may return multiple frames in a packet. This has many disadvantages like
+ * prohibiting stream copy in many cases thus it should only be considered
+ * as a last resort.
+ */
+#define CODEC_CAP_SUBFRAMES 0x0100
+/**
+ * Codec is experimental and is thus avoided in favor of non experimental
+ * encoders
+ */
+#define CODEC_CAP_EXPERIMENTAL 0x0200
+/**
+ * Codec should fill in channel configuration and samplerate instead of container
+ */
+#define CODEC_CAP_CHANNEL_CONF 0x0400
+/**
+ * Codec is able to deal with negative linesizes
+ */
+#define CODEC_CAP_NEG_LINESIZES 0x0800
+/**
+ * Codec supports frame-level multithreading.
+ */
+#define CODEC_CAP_FRAME_THREADS 0x1000
+/**
+ * Codec supports slice-based (or partition-based) multithreading.
+ */
+#define CODEC_CAP_SLICE_THREADS 0x2000
+/**
+ * Codec supports changed parameters at any point.
+ */
+#define CODEC_CAP_PARAM_CHANGE 0x4000
+/**
+ * Codec supports avctx->thread_count == 0 (auto).
+ */
+#define CODEC_CAP_AUTO_THREADS 0x8000
+/**
+ * Audio encoder supports receiving a different number of samples in each call.
+ */
+#define CODEC_CAP_VARIABLE_FRAME_SIZE 0x10000
+
+//The following defines may change, don't expect compatibility if you use them.
+#define MB_TYPE_INTRA4x4 0x0001
+#define MB_TYPE_INTRA16x16 0x0002 //FIXME H.264-specific
+#define MB_TYPE_INTRA_PCM 0x0004 //FIXME H.264-specific
+#define MB_TYPE_16x16 0x0008
+#define MB_TYPE_16x8 0x0010
+#define MB_TYPE_8x16 0x0020
+#define MB_TYPE_8x8 0x0040
+#define MB_TYPE_INTERLACED 0x0080
+#define MB_TYPE_DIRECT2 0x0100 //FIXME
+#define MB_TYPE_ACPRED 0x0200
+#define MB_TYPE_GMC 0x0400
+#define MB_TYPE_SKIP 0x0800
+#define MB_TYPE_P0L0 0x1000
+#define MB_TYPE_P1L0 0x2000
+#define MB_TYPE_P0L1 0x4000
+#define MB_TYPE_P1L1 0x8000
+#define MB_TYPE_L0 (MB_TYPE_P0L0 | MB_TYPE_P1L0)
+#define MB_TYPE_L1 (MB_TYPE_P0L1 | MB_TYPE_P1L1)
+#define MB_TYPE_L0L1 (MB_TYPE_L0 | MB_TYPE_L1)
+#define MB_TYPE_QUANT 0x00010000
+#define MB_TYPE_CBP 0x00020000
+//Note bits 24-31 are reserved for codec specific use (h264 ref0, mpeg1 0mv, ...)
+
+/**
+ * Pan Scan area.
+ * This specifies the area which should be displayed.
+ * Note there may be multiple such areas for one frame.
+ */
+typedef struct AVPanScan{
+ /**
+ * id
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int id;
+
+ /**
+ * width and height in 1/16 pel
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int width;
+ int height;
+
+ /**
+ * position of the top left corner in 1/16 pel for up to 3 fields/frames
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int16_t position[3][2];
+}AVPanScan;
+
+#define FF_QSCALE_TYPE_MPEG1 0
+#define FF_QSCALE_TYPE_MPEG2 1
+#define FF_QSCALE_TYPE_H264 2
+#define FF_QSCALE_TYPE_VP56 3
+
+#define FF_BUFFER_TYPE_INTERNAL 1
+#define FF_BUFFER_TYPE_USER 2 ///< direct rendering buffers (image is (de)allocated by user)
+#define FF_BUFFER_TYPE_SHARED 4 ///< Buffer from somewhere else; don't deallocate image (data/base), all other tables are not shared.
+#define FF_BUFFER_TYPE_COPY 8 ///< Just a (modified) copy of some other buffer, don't deallocate anything.
+
+#define FF_BUFFER_HINTS_VALID 0x01 // Buffer hints value is meaningful (if 0 ignore).
+#define FF_BUFFER_HINTS_READABLE 0x02 // Codec will read from buffer.
+#define FF_BUFFER_HINTS_PRESERVE 0x04 // User must not alter buffer content.
+#define FF_BUFFER_HINTS_REUSABLE 0x08 // Codec will reuse the buffer (update).
+
+/**
+ * @defgroup lavc_packet AVPacket
+ *
+ * Types and functions for working with AVPacket.
+ * @{
+ */
+enum AVPacketSideDataType {
+ AV_PKT_DATA_PALETTE,
+ AV_PKT_DATA_NEW_EXTRADATA,
+
+ /**
+ * An AV_PKT_DATA_PARAM_CHANGE side data packet is laid out as follows:
+ * @code
+ * u32le param_flags
+ * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_COUNT)
+ * s32le channel_count
+ * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_LAYOUT)
+ * u64le channel_layout
+ * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_SAMPLE_RATE)
+ * s32le sample_rate
+ * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_DIMENSIONS)
+ * s32le width
+ * s32le height
+ * @endcode
+ */
+ AV_PKT_DATA_PARAM_CHANGE,
+
+ /**
+ * An AV_PKT_DATA_H263_MB_INFO side data packet contains a number of
+ * structures with info about macroblocks relevant to splitting the
+ * packet into smaller packets on macroblock edges (e.g. as for RFC 2190).
+ * That is, it does not necessarily contain info about all macroblocks,
+ * as long as the distance between macroblocks in the info is smaller
+ * than the target payload size.
+ * Each MB info structure is 12 bytes, and is laid out as follows:
+ * @code
+ * u32le bit offset from the start of the packet
+ * u8 current quantizer at the start of the macroblock
+ * u8 GOB number
+ * u16le macroblock address within the GOB
+ * u8 horizontal MV predictor
+ * u8 vertical MV predictor
+ * u8 horizontal MV predictor for block number 3
+ * u8 vertical MV predictor for block number 3
+ * @endcode
+ */
+ AV_PKT_DATA_H263_MB_INFO,
+};
+
+/**
+ * This structure stores compressed data. It is typically exported by demuxers
+ * and then passed as input to decoders, or received as output from encoders and
+ * then passed to muxers.
+ *
+ * For video, it should typically contain one compressed frame. For audio it may
+ * contain several compressed frames.
+ *
+ * AVPacket is one of the few structs in Libav, whose size is a part of public
+ * ABI. Thus it may be allocated on stack and no new fields can be added to it
+ * without libavcodec and libavformat major bump.
+ *
+ * The semantics of data ownership depends on the destruct field.
+ * If it is set, the packet data is dynamically allocated and is valid
+ * indefinitely until av_free_packet() is called (which in turn calls the
+ * destruct callback to free the data). If destruct is not set, the packet data
+ * is typically backed by some static buffer somewhere and is only valid for a
+ * limited time (e.g. until the next read call when demuxing).
+ *
+ * The side data is always allocated with av_malloc() and is freed in
+ * av_free_packet().
+ */
+typedef struct AVPacket {
+ /**
+ * Presentation timestamp in AVStream->time_base units; the time at which
+ * the decompressed packet will be presented to the user.
+ * Can be AV_NOPTS_VALUE if it is not stored in the file.
+ * pts MUST be larger or equal to dts as presentation cannot happen before
+ * decompression, unless one wants to view hex dumps. Some formats misuse
+ * the terms dts and pts/cts to mean something different. Such timestamps
+ * must be converted to true pts/dts before they are stored in AVPacket.
+ */
+ int64_t pts;
+ /**
+ * Decompression timestamp in AVStream->time_base units; the time at which
+ * the packet is decompressed.
+ * Can be AV_NOPTS_VALUE if it is not stored in the file.
+ */
+ int64_t dts;
+ uint8_t *data;
+ int size;
+ int stream_index;
+ /**
+ * A combination of AV_PKT_FLAG values
+ */
+ int flags;
+ /**
+ * Additional packet data that can be provided by the container.
+ * Packet can contain several types of side information.
+ */
+ struct {
+ uint8_t *data;
+ int size;
+ enum AVPacketSideDataType type;
+ } *side_data;
+ int side_data_elems;
+
+ /**
+ * Duration of this packet in AVStream->time_base units, 0 if unknown.
+ * Equals next_pts - this_pts in presentation order.
+ */
+ int duration;
+ void (*destruct)(struct AVPacket *);
+ void *priv;
+ int64_t pos; ///< byte position in stream, -1 if unknown
+
+ /**
+ * Time difference in AVStream->time_base units from the pts of this
+ * packet to the point at which the output from the decoder has converged
+ * independent from the availability of previous frames. That is, the
+ * frames are virtually identical no matter if decoding started from
+ * the very first frame or from this keyframe.
+ * Is AV_NOPTS_VALUE if unknown.
+ * This field is not the display duration of the current packet.
+ * This field has no meaning if the packet does not have AV_PKT_FLAG_KEY
+ * set.
+ *
+ * The purpose of this field is to allow seeking in streams that have no
+ * keyframes in the conventional sense. It corresponds to the
+ * recovery point SEI in H.264 and match_time_delta in NUT. It is also
+ * essential for some types of subtitle streams to ensure that all
+ * subtitles are correctly displayed after seeking.
+ */
+ int64_t convergence_duration;
+} AVPacket;
+#define AV_PKT_FLAG_KEY 0x0001 ///< The packet contains a keyframe
+#define AV_PKT_FLAG_CORRUPT 0x0002 ///< The packet content is corrupted
+
+enum AVSideDataParamChangeFlags {
+ AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_COUNT = 0x0001,
+ AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_LAYOUT = 0x0002,
+ AV_SIDE_DATA_PARAM_CHANGE_SAMPLE_RATE = 0x0004,
+ AV_SIDE_DATA_PARAM_CHANGE_DIMENSIONS = 0x0008,
+};
+/**
+ * @}
+ */
+
+/**
+ * This structure describes decoded (raw) audio or video data.
+ *
+ * AVFrame must be allocated using avcodec_alloc_frame() and freed with
+ * avcodec_free_frame(). Note that this allocates only the AVFrame itself. The
+ * buffers for the data must be managed through other means.
+ *
+ * AVFrame is typically allocated once and then reused multiple times to hold
+ * different data (e.g. a single AVFrame to hold frames received from a
+ * decoder). In such a case, avcodec_get_frame_defaults() should be used to
+ * reset the frame to its original clean state before it is reused again.
+ *
+ * sizeof(AVFrame) is not a part of the public ABI, so new fields may be added
+ * to the end with a minor bump.
+ */
+typedef struct AVFrame {
+#define AV_NUM_DATA_POINTERS 8
+ /**
+ * pointer to the picture/channel planes.
+ * This might be different from the first allocated byte
+ * - encoding: Set by user
+ * - decoding: set by AVCodecContext.get_buffer()
+ */
+ uint8_t *data[AV_NUM_DATA_POINTERS];
+
+ /**
+ * Size, in bytes, of the data for each picture/channel plane.
+ *
+ * For audio, only linesize[0] may be set. For planar audio, each channel
+ * plane must be the same size.
+ *
+ * - encoding: Set by user
+ * - decoding: set by AVCodecContext.get_buffer()
+ */
+ int linesize[AV_NUM_DATA_POINTERS];
+
+ /**
+ * pointers to the data planes/channels.
+ *
+ * For video, this should simply point to data[].
+ *
+ * For planar audio, each channel has a separate data pointer, and
+ * linesize[0] contains the size of each channel buffer.
+ * For packed audio, there is just one data pointer, and linesize[0]
+ * contains the total size of the buffer for all channels.
+ *
+ * Note: Both data and extended_data will always be set by get_buffer(),
+ * but for planar audio with more channels that can fit in data,
+ * extended_data must be used by the decoder in order to access all
+ * channels.
+ *
+ * encoding: set by user
+ * decoding: set by AVCodecContext.get_buffer()
+ */
+ uint8_t **extended_data;
+
+ /**
+ * width and height of the video frame
+ * - encoding: unused
+ * - decoding: Read by user.
+ */
+ int width, height;
+
+ /**
+ * number of audio samples (per channel) described by this frame
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ int nb_samples;
+
+ /**
+ * format of the frame, -1 if unknown or unset
+ * Values correspond to enum AVPixelFormat for video frames,
+ * enum AVSampleFormat for audio)
+ * - encoding: unused
+ * - decoding: Read by user.
+ */
+ int format;
+
+ /**
+ * 1 -> keyframe, 0-> not
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by libavcodec.
+ */
+ int key_frame;
+
+ /**
+ * Picture type of the frame, see ?_TYPE below.
+ * - encoding: Set by libavcodec. for coded_picture (and set by user for input).
+ * - decoding: Set by libavcodec.
+ */
+ enum AVPictureType pict_type;
+
+ /**
+ * pointer to the first allocated byte of the picture. Can be used in get_buffer/release_buffer.
+ * This isn't used by libavcodec unless the default get/release_buffer() is used.
+ * - encoding:
+ * - decoding:
+ */
+ uint8_t *base[AV_NUM_DATA_POINTERS];
+
+ /**
+ * sample aspect ratio for the video frame, 0/1 if unknown/unspecified
+ * - encoding: unused
+ * - decoding: Read by user.
+ */
+ AVRational sample_aspect_ratio;
+
+ /**
+ * presentation timestamp in time_base units (time when frame should be shown to user)
+ * If AV_NOPTS_VALUE then frame_rate = 1/time_base will be assumed.
+ * - encoding: MUST be set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int64_t pts;
+
+ /**
+ * pts copied from the AVPacket that was decoded to produce this frame
+ * - encoding: unused
+ * - decoding: Read by user.
+ */
+ int64_t pkt_pts;
+
+ /**
+ * dts copied from the AVPacket that triggered returning this frame
+ * - encoding: unused
+ * - decoding: Read by user.
+ */
+ int64_t pkt_dts;
+
+ /**
+ * picture number in bitstream order
+ * - encoding: set by
+ * - decoding: Set by libavcodec.
+ */
+ int coded_picture_number;
+ /**
+ * picture number in display order
+ * - encoding: set by
+ * - decoding: Set by libavcodec.
+ */
+ int display_picture_number;
+
+ /**
+ * quality (between 1 (good) and FF_LAMBDA_MAX (bad))
+ * - encoding: Set by libavcodec. for coded_picture (and set by user for input).
+ * - decoding: Set by libavcodec.
+ */
+ int quality;
+
+ /**
+ * is this picture used as reference
+ * The values for this are the same as the MpegEncContext.picture_structure
+ * variable, that is 1->top field, 2->bottom field, 3->frame/both fields.
+ * Set to 4 for delayed, non-reference frames.
+ * - encoding: unused
+ * - decoding: Set by libavcodec. (before get_buffer() call)).
+ */
+ int reference;
+
+ /**
+ * QP table
+ * - encoding: unused
+ * - decoding: Set by libavcodec.
+ */
+ int8_t *qscale_table;
+ /**
+ * QP store stride
+ * - encoding: unused
+ * - decoding: Set by libavcodec.
+ */
+ int qstride;
+
+ /**
+ *
+ */
+ int qscale_type;
+
+ /**
+ * mbskip_table[mb]>=1 if MB didn't change
+ * stride= mb_width = (width+15)>>4
+ * - encoding: unused
+ * - decoding: Set by libavcodec.
+ */
+ uint8_t *mbskip_table;
+
+ /**
+ * motion vector table
+ * @code
+ * example:
+ * int mv_sample_log2= 4 - motion_subsample_log2;
+ * int mb_width= (width+15)>>4;
+ * int mv_stride= (mb_width << mv_sample_log2) + 1;
+ * motion_val[direction][x + y*mv_stride][0->mv_x, 1->mv_y];
+ * @endcode
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int16_t (*motion_val[2])[2];
+
+ /**
+ * macroblock type table
+ * mb_type_base + mb_width + 2
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ uint32_t *mb_type;
+
+ /**
+ * DCT coefficients
+ * - encoding: unused
+ * - decoding: Set by libavcodec.
+ */
+ short *dct_coeff;
+
+ /**
+ * motion reference frame index
+ * the order in which these are stored can depend on the codec.
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int8_t *ref_index[2];
+
+ /**
+ * for some private data of the user
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ void *opaque;
+
+ /**
+ * error
+ * - encoding: Set by libavcodec. if flags&CODEC_FLAG_PSNR.
+ * - decoding: unused
+ */
+ uint64_t error[AV_NUM_DATA_POINTERS];
+
+ /**
+ * type of the buffer (to keep track of who has to deallocate data[*])
+ * - encoding: Set by the one who allocates it.
+ * - decoding: Set by the one who allocates it.
+ * Note: User allocated (direct rendering) & internal buffers cannot coexist currently.
+ */
+ int type;
+
+ /**
+ * When decoding, this signals how much the picture must be delayed.
+ * extra_delay = repeat_pict / (2*fps)
+ * - encoding: unused
+ * - decoding: Set by libavcodec.
+ */
+ int repeat_pict;
+
+ /**
+ * The content of the picture is interlaced.
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec. (default 0)
+ */
+ int interlaced_frame;
+
+ /**
+ * If the content is interlaced, is top field displayed first.
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int top_field_first;
+
+ /**
+ * Tell user application that palette has changed from previous frame.
+ * - encoding: ??? (no palette-enabled encoder yet)
+ * - decoding: Set by libavcodec. (default 0).
+ */
+ int palette_has_changed;
+
+ /**
+ * codec suggestion on buffer type if != 0
+ * - encoding: unused
+ * - decoding: Set by libavcodec. (before get_buffer() call)).
+ */
+ int buffer_hints;
+
+ /**
+ * Pan scan.
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ AVPanScan *pan_scan;
+
+ /**
+ * reordered opaque 64bit (generally an integer or a double precision float
+ * PTS but can be anything).
+ * The user sets AVCodecContext.reordered_opaque to represent the input at
+ * that time,
+ * the decoder reorders values as needed and sets AVFrame.reordered_opaque
+ * to exactly one of the values provided by the user through AVCodecContext.reordered_opaque
+ * @deprecated in favor of pkt_pts
+ * - encoding: unused
+ * - decoding: Read by user.
+ */
+ int64_t reordered_opaque;
+
+ /**
+ * hardware accelerator private data (Libav-allocated)
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ void *hwaccel_picture_private;
+
+ /**
+ * the AVCodecContext which ff_thread_get_buffer() was last called on
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by libavcodec.
+ */
+ struct AVCodecContext *owner;
+
+ /**
+ * used by multithreading to store frame-specific info
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by libavcodec.
+ */
+ void *thread_opaque;
+
+ /**
+ * log2 of the size of the block which a single vector in motion_val represents:
+ * (4->16x16, 3->8x8, 2-> 4x4, 1-> 2x2)
+ * - encoding: unused
+ * - decoding: Set by libavcodec.
+ */
+ uint8_t motion_subsample_log2;
+
+ /**
+ * Sample rate of the audio data.
+ *
+ * - encoding: unused
+ * - decoding: set by get_buffer()
+ */
+ int sample_rate;
+
+ /**
+ * Channel layout of the audio data.
+ *
+ * - encoding: unused
+ * - decoding: set by get_buffer()
+ */
+ uint64_t channel_layout;
+} AVFrame;
+
+struct AVCodecInternal;
+
+enum AVFieldOrder {
+ AV_FIELD_UNKNOWN,
+ AV_FIELD_PROGRESSIVE,
+ AV_FIELD_TT, //< Top coded_first, top displayed first
+ AV_FIELD_BB, //< Bottom coded first, bottom displayed first
+ AV_FIELD_TB, //< Top coded first, bottom displayed first
+ AV_FIELD_BT, //< Bottom coded first, top displayed first
+};
+
+/**
+ * main external API structure.
+ * New fields can be added to the end with minor version bumps.
+ * Removal, reordering and changes to existing fields require a major
+ * version bump.
+ * sizeof(AVCodecContext) must not be used outside libav*.
+ */
+typedef struct AVCodecContext {
+ /**
+ * information on struct for av_log
+ * - set by avcodec_alloc_context3
+ */
+ const AVClass *av_class;
+ int log_level_offset;
+
+ enum AVMediaType codec_type; /* see AVMEDIA_TYPE_xxx */
+ const struct AVCodec *codec;
+ char codec_name[32];
+ enum AVCodecID codec_id; /* see AV_CODEC_ID_xxx */
+
+ /**
+ * fourcc (LSB first, so "ABCD" -> ('D'<<24) + ('C'<<16) + ('B'<<8) + 'A').
+ * This is used to work around some encoder bugs.
+ * A demuxer should set this to what is stored in the field used to identify the codec.
+ * If there are multiple such fields in a container then the demuxer should choose the one
+ * which maximizes the information about the used codec.
+ * If the codec tag field in a container is larger than 32 bits then the demuxer should
+ * remap the longer ID to 32 bits with a table or other structure. Alternatively a new
+ * extra_codec_tag + size could be added but for this a clear advantage must be demonstrated
+ * first.
+ * - encoding: Set by user, if not then the default based on codec_id will be used.
+ * - decoding: Set by user, will be converted to uppercase by libavcodec during init.
+ */
+ unsigned int codec_tag;
+
+ /**
+ * fourcc from the AVI stream header (LSB first, so "ABCD" -> ('D'<<24) + ('C'<<16) + ('B'<<8) + 'A').
+ * This is used to work around some encoder bugs.
+ * - encoding: unused
+ * - decoding: Set by user, will be converted to uppercase by libavcodec during init.
+ */
+ unsigned int stream_codec_tag;
+
+#if FF_API_SUB_ID
+ /**
+ * @deprecated this field is unused
+ */
+ attribute_deprecated int sub_id;
+#endif
+
+ void *priv_data;
+
+ /**
+ * Private context used for internal data.
+ *
+ * Unlike priv_data, this is not codec-specific. It is used in general
+ * libavcodec functions.
+ */
+ struct AVCodecInternal *internal;
+
+ /**
+ * Private data of the user, can be used to carry app specific stuff.
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ void *opaque;
+
+ /**
+ * the average bitrate
+ * - encoding: Set by user; unused for constant quantizer encoding.
+ * - decoding: Set by libavcodec. 0 or some bitrate if this info is available in the stream.
+ */
+ int bit_rate;
+
+ /**
+ * number of bits the bitstream is allowed to diverge from the reference.
+ * the reference can be CBR (for CBR pass1) or VBR (for pass2)
+ * - encoding: Set by user; unused for constant quantizer encoding.
+ * - decoding: unused
+ */
+ int bit_rate_tolerance;
+
+ /**
+ * Global quality for codecs which cannot change it per frame.
+ * This should be proportional to MPEG-1/2/4 qscale.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int global_quality;
+
+ /**
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int compression_level;
+#define FF_COMPRESSION_DEFAULT -1
+
+ /**
+ * CODEC_FLAG_*.
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int flags;
+
+ /**
+ * CODEC_FLAG2_*
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int flags2;
+
+ /**
+ * some codecs need / can use extradata like Huffman tables.
+ * mjpeg: Huffman tables
+ * rv10: additional flags
+ * mpeg4: global headers (they can be in the bitstream or here)
+ * The allocated memory should be FF_INPUT_BUFFER_PADDING_SIZE bytes larger
+ * than extradata_size to avoid prolems if it is read with the bitstream reader.
+ * The bytewise contents of extradata must not depend on the architecture or CPU endianness.
+ * - encoding: Set/allocated/freed by libavcodec.
+ * - decoding: Set/allocated/freed by user.
+ */
+ uint8_t *extradata;
+ int extradata_size;
+
+ /**
+ * This is the fundamental unit of time (in seconds) in terms
+ * of which frame timestamps are represented. For fixed-fps content,
+ * timebase should be 1/framerate and timestamp increments should be
+ * identically 1.
+ * - encoding: MUST be set by user.
+ * - decoding: Set by libavcodec.
+ */
+ AVRational time_base;
+
+ /**
+ * For some codecs, the time base is closer to the field rate than the frame rate.
+ * Most notably, H.264 and MPEG-2 specify time_base as half of frame duration
+ * if no telecine is used ...
+ *
+ * Set to time_base ticks per frame. Default 1, e.g., H.264/MPEG-2 set it to 2.
+ */
+ int ticks_per_frame;
+
+ /**
+ * Codec delay.
+ *
+ * Video:
+ * Number of frames the decoded output will be delayed relative to the
+ * encoded input.
+ *
+ * Audio:
+ * For encoding, this is the number of "priming" samples added to the
+ * beginning of the stream. The decoded output will be delayed by this
+ * many samples relative to the input to the encoder. Note that this
+ * field is purely informational and does not directly affect the pts
+ * output by the encoder, which should always be based on the actual
+ * presentation time, including any delay.
+ * For decoding, this is the number of samples the decoder needs to
+ * output before the decoder's output is valid. When seeking, you should
+ * start decoding this many samples prior to your desired seek point.
+ *
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by libavcodec.
+ */
+ int delay;
+
+
+ /* video only */
+ /**
+ * picture width / height.
+ * - encoding: MUST be set by user.
+ * - decoding: Set by libavcodec.
+ * Note: For compatibility it is possible to set this instead of
+ * coded_width/height before decoding.
+ */
+ int width, height;
+
+ /**
+ * Bitstream width / height, may be different from width/height.
+ * - encoding: unused
+ * - decoding: Set by user before init if known. Codec should override / dynamically change if needed.
+ */
+ int coded_width, coded_height;
+
+#define FF_ASPECT_EXTENDED 15
+
+ /**
+ * the number of pictures in a group of pictures, or 0 for intra_only
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int gop_size;
+
+ /**
+ * Pixel format, see AV_PIX_FMT_xxx.
+ * May be set by the demuxer if known from headers.
+ * May be overriden by the decoder if it knows better.
+ * - encoding: Set by user.
+ * - decoding: Set by user if known, overridden by libavcodec if known
+ */
+ enum AVPixelFormat pix_fmt;
+
+ /**
+ * Motion estimation algorithm used for video coding.
+ * 1 (zero), 2 (full), 3 (log), 4 (phods), 5 (epzs), 6 (x1), 7 (hex),
+ * 8 (umh), 9 (iter), 10 (tesa) [7, 8, 10 are x264 specific, 9 is snow specific]
+ * - encoding: MUST be set by user.
+ * - decoding: unused
+ */
+ int me_method;
+
+ /**
+ * If non NULL, 'draw_horiz_band' is called by the libavcodec
+ * decoder to draw a horizontal band. It improves cache usage. Not
+ * all codecs can do that. You must check the codec capabilities
+ * beforehand.
+ * When multithreading is used, it may be called from multiple threads
+ * at the same time; threads might draw different parts of the same AVFrame,
+ * or multiple AVFrames, and there is no guarantee that slices will be drawn
+ * in order.
+ * The function is also used by hardware acceleration APIs.
+ * It is called at least once during frame decoding to pass
+ * the data needed for hardware render.
+ * In that mode instead of pixel data, AVFrame points to
+ * a structure specific to the acceleration API. The application
+ * reads the structure and can change some fields to indicate progress
+ * or mark state.
+ * - encoding: unused
+ * - decoding: Set by user.
+ * @param height the height of the slice
+ * @param y the y position of the slice
+ * @param type 1->top field, 2->bottom field, 3->frame
+ * @param offset offset into the AVFrame.data from which the slice should be read
+ */
+ void (*draw_horiz_band)(struct AVCodecContext *s,
+ const AVFrame *src, int offset[AV_NUM_DATA_POINTERS],
+ int y, int type, int height);
+
+ /**
+ * callback to negotiate the pixelFormat
+ * @param fmt is the list of formats which are supported by the codec,
+ * it is terminated by -1 as 0 is a valid format, the formats are ordered by quality.
+ * The first is always the native one.
+ * @return the chosen format
+ * - encoding: unused
+ * - decoding: Set by user, if not set the native format will be chosen.
+ */
+ enum AVPixelFormat (*get_format)(struct AVCodecContext *s, const enum AVPixelFormat * fmt);
+
+ /**
+ * maximum number of B-frames between non-B-frames
+ * Note: The output will be delayed by max_b_frames+1 relative to the input.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int max_b_frames;
+
+ /**
+ * qscale factor between IP and B-frames
+ * If > 0 then the last P-frame quantizer will be used (q= lastp_q*factor+offset).
+ * If < 0 then normal ratecontrol will be done (q= -normal_q*factor+offset).
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float b_quant_factor;
+
+ /** obsolete FIXME remove */
+ int rc_strategy;
+#define FF_RC_STRATEGY_XVID 1
+
+ int b_frame_strategy;
+
+#if FF_API_MPV_GLOBAL_OPTS
+ /**
+ * luma single coefficient elimination threshold
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ attribute_deprecated int luma_elim_threshold;
+
+ /**
+ * chroma single coeff elimination threshold
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ attribute_deprecated int chroma_elim_threshold;
+#endif
+
+ /**
+ * qscale offset between IP and B-frames
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float b_quant_offset;
+
+ /**
+ * Size of the frame reordering buffer in the decoder.
+ * For MPEG-2 it is 1 IPB or 0 low delay IP.
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by libavcodec.
+ */
+ int has_b_frames;
+
+ /**
+ * 0-> h263 quant 1-> mpeg quant
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mpeg_quant;
+
+ /**
+ * qscale factor between P and I-frames
+ * If > 0 then the last p frame quantizer will be used (q= lastp_q*factor+offset).
+ * If < 0 then normal ratecontrol will be done (q= -normal_q*factor+offset).
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float i_quant_factor;
+
+ /**
+ * qscale offset between P and I-frames
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float i_quant_offset;
+
+ /**
+ * luminance masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float lumi_masking;
+
+ /**
+ * temporary complexity masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float temporal_cplx_masking;
+
+ /**
+ * spatial complexity masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float spatial_cplx_masking;
+
+ /**
+ * p block masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float p_masking;
+
+ /**
+ * darkness masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float dark_masking;
+
+ /**
+ * slice count
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by user (or 0).
+ */
+ int slice_count;
+ /**
+ * prediction method (needed for huffyuv)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int prediction_method;
+#define FF_PRED_LEFT 0
+#define FF_PRED_PLANE 1
+#define FF_PRED_MEDIAN 2
+
+ /**
+ * slice offsets in the frame in bytes
+ * - encoding: Set/allocated by libavcodec.
+ * - decoding: Set/allocated by user (or NULL).
+ */
+ int *slice_offset;
+
+ /**
+ * sample aspect ratio (0 if unknown)
+ * That is the width of a pixel divided by the height of the pixel.
+ * Numerator and denominator must be relatively prime and smaller than 256 for some video standards.
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ AVRational sample_aspect_ratio;
+
+ /**
+ * motion estimation comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_cmp;
+ /**
+ * subpixel motion estimation comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_sub_cmp;
+ /**
+ * macroblock comparison function (not supported yet)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mb_cmp;
+ /**
+ * interlaced DCT comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int ildct_cmp;
+#define FF_CMP_SAD 0
+#define FF_CMP_SSE 1
+#define FF_CMP_SATD 2
+#define FF_CMP_DCT 3
+#define FF_CMP_PSNR 4
+#define FF_CMP_BIT 5
+#define FF_CMP_RD 6
+#define FF_CMP_ZERO 7
+#define FF_CMP_VSAD 8
+#define FF_CMP_VSSE 9
+#define FF_CMP_NSSE 10
+#define FF_CMP_W53 11
+#define FF_CMP_W97 12
+#define FF_CMP_DCTMAX 13
+#define FF_CMP_DCT264 14
+#define FF_CMP_CHROMA 256
+
+ /**
+ * ME diamond size & shape
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int dia_size;
+
+ /**
+ * amount of previous MV predictors (2a+1 x 2a+1 square)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int last_predictor_count;
+
+ /**
+ * prepass for motion estimation
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int pre_me;
+
+ /**
+ * motion estimation prepass comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_pre_cmp;
+
+ /**
+ * ME prepass diamond size & shape
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int pre_dia_size;
+
+ /**
+ * subpel ME quality
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_subpel_quality;
+
+ /**
+ * DTG active format information (additional aspect ratio
+ * information only used in DVB MPEG-2 transport streams)
+ * 0 if not set.
+ *
+ * - encoding: unused
+ * - decoding: Set by decoder.
+ */
+ int dtg_active_format;
+#define FF_DTG_AFD_SAME 8
+#define FF_DTG_AFD_4_3 9
+#define FF_DTG_AFD_16_9 10
+#define FF_DTG_AFD_14_9 11
+#define FF_DTG_AFD_4_3_SP_14_9 13
+#define FF_DTG_AFD_16_9_SP_14_9 14
+#define FF_DTG_AFD_SP_4_3 15
+
+ /**
+ * maximum motion estimation search range in subpel units
+ * If 0 then no limit.
+ *
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_range;
+
+ /**
+ * intra quantizer bias
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int intra_quant_bias;
+#define FF_DEFAULT_QUANT_BIAS 999999
+
+ /**
+ * inter quantizer bias
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int inter_quant_bias;
+
+#if FF_API_COLOR_TABLE_ID
+ /**
+ * color table ID
+ * - encoding: unused
+ * - decoding: Which clrtable should be used for 8bit RGB images.
+ * Tables have to be stored somewhere. FIXME
+ */
+ attribute_deprecated int color_table_id;
+#endif
+
+ /**
+ * slice flags
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int slice_flags;
+#define SLICE_FLAG_CODED_ORDER 0x0001 ///< draw_horiz_band() is called in coded order instead of display
+#define SLICE_FLAG_ALLOW_FIELD 0x0002 ///< allow draw_horiz_band() with field slices (MPEG2 field pics)
+#define SLICE_FLAG_ALLOW_PLANE 0x0004 ///< allow draw_horiz_band() with 1 component at a time (SVQ1)
+
+ /**
+ * XVideo Motion Acceleration
+ * - encoding: forbidden
+ * - decoding: set by decoder
+ */
+ int xvmc_acceleration;
+
+ /**
+ * macroblock decision mode
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mb_decision;
+#define FF_MB_DECISION_SIMPLE 0 ///< uses mb_cmp
+#define FF_MB_DECISION_BITS 1 ///< chooses the one which needs the fewest bits
+#define FF_MB_DECISION_RD 2 ///< rate distortion
+
+ /**
+ * custom intra quantization matrix
+ * - encoding: Set by user, can be NULL.
+ * - decoding: Set by libavcodec.
+ */
+ uint16_t *intra_matrix;
+
+ /**
+ * custom inter quantization matrix
+ * - encoding: Set by user, can be NULL.
+ * - decoding: Set by libavcodec.
+ */
+ uint16_t *inter_matrix;
+
+ /**
+ * scene change detection threshold
+ * 0 is default, larger means fewer detected scene changes.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int scenechange_threshold;
+
+ /**
+ * noise reduction strength
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int noise_reduction;
+
+#if FF_API_INTER_THRESHOLD
+ /**
+ * @deprecated this field is unused
+ */
+ attribute_deprecated int inter_threshold;
+#endif
+
+#if FF_API_MPV_GLOBAL_OPTS
+ /**
+ * @deprecated use mpegvideo private options instead
+ */
+ attribute_deprecated int quantizer_noise_shaping;
+#endif
+
+ /**
+ * Motion estimation threshold below which no motion estimation is
+ * performed, but instead the user specified motion vectors are used.
+ *
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_threshold;
+
+ /**
+ * Macroblock threshold below which the user specified macroblock types will be used.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mb_threshold;
+
+ /**
+ * precision of the intra DC coefficient - 8
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int intra_dc_precision;
+
+ /**
+ * Number of macroblock rows at the top which are skipped.
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int skip_top;
+
+ /**
+ * Number of macroblock rows at the bottom which are skipped.
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int skip_bottom;
+
+ /**
+ * Border processing masking, raises the quantizer for mbs on the borders
+ * of the picture.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float border_masking;
+
+ /**
+ * minimum MB lagrange multipler
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mb_lmin;
+
+ /**
+ * maximum MB lagrange multipler
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mb_lmax;
+
+ /**
+ *
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_penalty_compensation;
+
+ /**
+ *
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int bidir_refine;
+
+ /**
+ *
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int brd_scale;
+
+ /**
+ * minimum GOP size
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int keyint_min;
+
+ /**
+ * number of reference frames
+ * - encoding: Set by user.
+ * - decoding: Set by lavc.
+ */
+ int refs;
+
+ /**
+ * chroma qp offset from luma
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int chromaoffset;
+
+ /**
+ * Multiplied by qscale for each frame and added to scene_change_score.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int scenechange_factor;
+
+ /**
+ *
+ * Note: Value depends upon the compare function used for fullpel ME.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mv0_threshold;
+
+ /**
+ * Adjust sensitivity of b_frame_strategy 1.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int b_sensitivity;
+
+ /**
+ * Chromaticity coordinates of the source primaries.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorPrimaries color_primaries;
+
+ /**
+ * Color Transfer Characteristic.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorTransferCharacteristic color_trc;
+
+ /**
+ * YUV colorspace type.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorSpace colorspace;
+
+ /**
+ * MPEG vs JPEG YUV range.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorRange color_range;
+
+ /**
+ * This defines the location of chroma samples.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVChromaLocation chroma_sample_location;
+
+ /**
+ * Number of slices.
+ * Indicates number of picture subdivisions. Used for parallelized
+ * decoding.
+ * - encoding: Set by user
+ * - decoding: unused
+ */
+ int slices;
+
+ /** Field order
+ * - encoding: set by libavcodec
+ * - decoding: Set by libavcodec
+ */
+ enum AVFieldOrder field_order;
+
+ /* audio only */
+ int sample_rate; ///< samples per second
+ int channels; ///< number of audio channels
+
+ /**
+ * audio sample format
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ enum AVSampleFormat sample_fmt; ///< sample format
+
+ /* The following data should not be initialized. */
+ /**
+ * Number of samples per channel in an audio frame.
+ *
+ * - encoding: set by libavcodec in avcodec_open2(). Each submitted frame
+ * except the last must contain exactly frame_size samples per channel.
+ * May be 0 when the codec has CODEC_CAP_VARIABLE_FRAME_SIZE set, then the
+ * frame size is not restricted.
+ * - decoding: may be set by some decoders to indicate constant frame size
+ */
+ int frame_size;
+
+ /**
+ * Frame counter, set by libavcodec.
+ *
+ * - decoding: total number of frames returned from the decoder so far.
+ * - encoding: total number of frames passed to the encoder so far.
+ *
+ * @note the counter is not incremented if encoding/decoding resulted in
+ * an error.
+ */
+ int frame_number;
+
+ /**
+ * number of bytes per packet if constant and known or 0
+ * Used by some WAV based audio codecs.
+ */
+ int block_align;
+
+ /**
+ * Audio cutoff bandwidth (0 means "automatic")
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int cutoff;
+
+#if FF_API_REQUEST_CHANNELS
+ /**
+ * Decoder should decode to this many channels if it can (0 for default)
+ * - encoding: unused
+ * - decoding: Set by user.
+ * @deprecated Deprecated in favor of request_channel_layout.
+ */
+ int request_channels;
+#endif
+
+ /**
+ * Audio channel layout.
+ * - encoding: set by user.
+ * - decoding: set by libavcodec.
+ */
+ uint64_t channel_layout;
+
+ /**
+ * Request decoder to use this channel layout if it can (0 for default)
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ uint64_t request_channel_layout;
+
+ /**
+ * Type of service that the audio stream conveys.
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ enum AVAudioServiceType audio_service_type;
+
+ /**
+ * Used to request a sample format from the decoder.
+ * - encoding: unused.
+ * - decoding: Set by user.
+ */
+ enum AVSampleFormat request_sample_fmt;
+
+ /**
+ * Called at the beginning of each frame to get a buffer for it.
+ *
+ * The function will set AVFrame.data[], AVFrame.linesize[].
+ * AVFrame.extended_data[] must also be set, but it should be the same as
+ * AVFrame.data[] except for planar audio with more channels than can fit
+ * in AVFrame.data[]. In that case, AVFrame.data[] shall still contain as
+ * many data pointers as it can hold.
+ *
+ * if CODEC_CAP_DR1 is not set then get_buffer() must call
+ * avcodec_default_get_buffer() instead of providing buffers allocated by
+ * some other means.
+ *
+ * AVFrame.data[] should be 32- or 16-byte-aligned unless the CPU doesn't
+ * need it. avcodec_default_get_buffer() aligns the output buffer properly,
+ * but if get_buffer() is overridden then alignment considerations should
+ * be taken into account.
+ *
+ * @see avcodec_default_get_buffer()
+ *
+ * Video:
+ *
+ * If pic.reference is set then the frame will be read later by libavcodec.
+ * avcodec_align_dimensions2() should be used to find the required width and
+ * height, as they normally need to be rounded up to the next multiple of 16.
+ *
+ * If frame multithreading is used and thread_safe_callbacks is set,
+ * it may be called from a different thread, but not from more than one at
+ * once. Does not need to be reentrant.
+ *
+ * @see release_buffer(), reget_buffer()
+ * @see avcodec_align_dimensions2()
+ *
+ * Audio:
+ *
+ * Decoders request a buffer of a particular size by setting
+ * AVFrame.nb_samples prior to calling get_buffer(). The decoder may,
+ * however, utilize only part of the buffer by setting AVFrame.nb_samples
+ * to a smaller value in the output frame.
+ *
+ * Decoders cannot use the buffer after returning from
+ * avcodec_decode_audio4(), so they will not call release_buffer(), as it
+ * is assumed to be released immediately upon return. In some rare cases,
+ * a decoder may need to call get_buffer() more than once in a single
+ * call to avcodec_decode_audio4(). In that case, when get_buffer() is
+ * called again after it has already been called once, the previously
+ * acquired buffer is assumed to be released at that time and may not be
+ * reused by the decoder.
+ *
+ * As a convenience, av_samples_get_buffer_size() and
+ * av_samples_fill_arrays() in libavutil may be used by custom get_buffer()
+ * functions to find the required data size and to fill data pointers and
+ * linesize. In AVFrame.linesize, only linesize[0] may be set for audio
+ * since all planes must be the same size.
+ *
+ * @see av_samples_get_buffer_size(), av_samples_fill_arrays()
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec, user can override.
+ */
+ int (*get_buffer)(struct AVCodecContext *c, AVFrame *pic);
+
+ /**
+ * Called to release buffers which were allocated with get_buffer.
+ * A released buffer can be reused in get_buffer().
+ * pic.data[*] must be set to NULL.
+ * May be called from a different thread if frame multithreading is used,
+ * but not by more than one thread at once, so does not need to be reentrant.
+ * - encoding: unused
+ * - decoding: Set by libavcodec, user can override.
+ */
+ void (*release_buffer)(struct AVCodecContext *c, AVFrame *pic);
+
+ /**
+ * Called at the beginning of a frame to get cr buffer for it.
+ * Buffer type (size, hints) must be the same. libavcodec won't check it.
+ * libavcodec will pass previous buffer in pic, function should return
+ * same buffer or new buffer with old frame "painted" into it.
+ * If pic.data[0] == NULL must behave like get_buffer().
+ * if CODEC_CAP_DR1 is not set then reget_buffer() must call
+ * avcodec_default_reget_buffer() instead of providing buffers allocated by
+ * some other means.
+ * - encoding: unused
+ * - decoding: Set by libavcodec, user can override.
+ */
+ int (*reget_buffer)(struct AVCodecContext *c, AVFrame *pic);
+
+
+ /* - encoding parameters */
+ float qcompress; ///< amount of qscale change between easy & hard scenes (0.0-1.0)
+ float qblur; ///< amount of qscale smoothing over time (0.0-1.0)
+
+ /**
+ * minimum quantizer
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int qmin;
+
+ /**
+ * maximum quantizer
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int qmax;
+
+ /**
+ * maximum quantizer difference between frames
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int max_qdiff;
+
+ /**
+ * ratecontrol qmin qmax limiting method
+ * 0-> clipping, 1-> use a nice continuous function to limit qscale wthin qmin/qmax.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float rc_qsquish;
+
+ float rc_qmod_amp;
+ int rc_qmod_freq;
+
+ /**
+ * decoder bitstream buffer size
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int rc_buffer_size;
+
+ /**
+ * ratecontrol override, see RcOverride
+ * - encoding: Allocated/set/freed by user.
+ * - decoding: unused
+ */
+ int rc_override_count;
+ RcOverride *rc_override;
+
+ /**
+ * rate control equation
+ * - encoding: Set by user
+ * - decoding: unused
+ */
+ const char *rc_eq;
+
+ /**
+ * maximum bitrate
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int rc_max_rate;
+
+ /**
+ * minimum bitrate
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int rc_min_rate;
+
+ float rc_buffer_aggressivity;
+
+ /**
+ * initial complexity for pass1 ratecontrol
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float rc_initial_cplx;
+
+ /**
+ * Ratecontrol attempt to use, at maximum, <value> of what can be used without an underflow.
+ * - encoding: Set by user.
+ * - decoding: unused.
+ */
+ float rc_max_available_vbv_use;
+
+ /**
+ * Ratecontrol attempt to use, at least, <value> times the amount needed to prevent a vbv overflow.
+ * - encoding: Set by user.
+ * - decoding: unused.
+ */
+ float rc_min_vbv_overflow_use;
+
+ /**
+ * Number of bits which should be loaded into the rc buffer before decoding starts.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int rc_initial_buffer_occupancy;
+
+#define FF_CODER_TYPE_VLC 0
+#define FF_CODER_TYPE_AC 1
+#define FF_CODER_TYPE_RAW 2
+#define FF_CODER_TYPE_RLE 3
+#define FF_CODER_TYPE_DEFLATE 4
+ /**
+ * coder type
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int coder_type;
+
+ /**
+ * context model
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int context_model;
+
+ /**
+ * minimum Lagrange multipler
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int lmin;
+
+ /**
+ * maximum Lagrange multipler
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int lmax;
+
+ /**
+ * frame skip threshold
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int frame_skip_threshold;
+
+ /**
+ * frame skip factor
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int frame_skip_factor;
+
+ /**
+ * frame skip exponent
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int frame_skip_exp;
+
+ /**
+ * frame skip comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int frame_skip_cmp;
+
+ /**
+ * trellis RD quantization
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int trellis;
+
+ /**
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int min_prediction_order;
+
+ /**
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int max_prediction_order;
+
+ /**
+ * GOP timecode frame start number, in non drop frame format
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int64_t timecode_frame_start;
+
+ /* The RTP callback: This function is called */
+ /* every time the encoder has a packet to send. */
+ /* It depends on the encoder if the data starts */
+ /* with a Start Code (it should). H.263 does. */
+ /* mb_nb contains the number of macroblocks */
+ /* encoded in the RTP payload. */
+ void (*rtp_callback)(struct AVCodecContext *avctx, void *data, int size, int mb_nb);
+
+ int rtp_payload_size; /* The size of the RTP payload: the coder will */
+ /* do its best to deliver a chunk with size */
+ /* below rtp_payload_size, the chunk will start */
+ /* with a start code on some codecs like H.263. */
+ /* This doesn't take account of any particular */
+ /* headers inside the transmitted RTP payload. */
+
+ /* statistics, used for 2-pass encoding */
+ int mv_bits;
+ int header_bits;
+ int i_tex_bits;
+ int p_tex_bits;
+ int i_count;
+ int p_count;
+ int skip_count;
+ int misc_bits;
+
+ /**
+ * number of bits used for the previously encoded frame
+ * - encoding: Set by libavcodec.
+ * - decoding: unused
+ */
+ int frame_bits;
+
+ /**
+ * pass1 encoding statistics output buffer
+ * - encoding: Set by libavcodec.
+ * - decoding: unused
+ */
+ char *stats_out;
+
+ /**
+ * pass2 encoding statistics input buffer
+ * Concatenated stuff from stats_out of pass1 should be placed here.
+ * - encoding: Allocated/set/freed by user.
+ * - decoding: unused
+ */
+ char *stats_in;
+
+ /**
+ * Work around bugs in encoders which sometimes cannot be detected automatically.
+ * - encoding: Set by user
+ * - decoding: Set by user
+ */
+ int workaround_bugs;
+#define FF_BUG_AUTODETECT 1 ///< autodetection
+#define FF_BUG_OLD_MSMPEG4 2
+#define FF_BUG_XVID_ILACE 4
+#define FF_BUG_UMP4 8
+#define FF_BUG_NO_PADDING 16
+#define FF_BUG_AMV 32
+#define FF_BUG_AC_VLC 0 ///< Will be removed, libavcodec can now handle these non-compliant files by default.
+#define FF_BUG_QPEL_CHROMA 64
+#define FF_BUG_STD_QPEL 128
+#define FF_BUG_QPEL_CHROMA2 256
+#define FF_BUG_DIRECT_BLOCKSIZE 512
+#define FF_BUG_EDGE 1024
+#define FF_BUG_HPEL_CHROMA 2048
+#define FF_BUG_DC_CLIP 4096
+#define FF_BUG_MS 8192 ///< Work around various bugs in Microsoft's broken decoders.
+#define FF_BUG_TRUNCATED 16384
+
+ /**
+ * strictly follow the standard (MPEG4, ...).
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ * Setting this to STRICT or higher means the encoder and decoder will
+ * generally do stupid things, whereas setting it to unofficial or lower
+ * will mean the encoder might produce output that is not supported by all
+ * spec-compliant decoders. Decoders don't differentiate between normal,
+ * unofficial and experimental (that is, they always try to decode things
+ * when they can) unless they are explicitly asked to behave stupidly
+ * (=strictly conform to the specs)
+ */
+ int strict_std_compliance;
+#define FF_COMPLIANCE_VERY_STRICT 2 ///< Strictly conform to an older more strict version of the spec or reference software.
+#define FF_COMPLIANCE_STRICT 1 ///< Strictly conform to all the things in the spec no matter what consequences.
+#define FF_COMPLIANCE_NORMAL 0
+#define FF_COMPLIANCE_UNOFFICIAL -1 ///< Allow unofficial extensions
+#define FF_COMPLIANCE_EXPERIMENTAL -2 ///< Allow nonstandardized experimental things.
+
+ /**
+ * error concealment flags
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int error_concealment;
+#define FF_EC_GUESS_MVS 1
+#define FF_EC_DEBLOCK 2
+
+ /**
+ * debug
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int debug;
+#define FF_DEBUG_PICT_INFO 1
+#define FF_DEBUG_RC 2
+#define FF_DEBUG_BITSTREAM 4
+#define FF_DEBUG_MB_TYPE 8
+#define FF_DEBUG_QP 16
+#define FF_DEBUG_MV 32
+#define FF_DEBUG_DCT_COEFF 0x00000040
+#define FF_DEBUG_SKIP 0x00000080
+#define FF_DEBUG_STARTCODE 0x00000100
+#define FF_DEBUG_PTS 0x00000200
+#define FF_DEBUG_ER 0x00000400
+#define FF_DEBUG_MMCO 0x00000800
+#define FF_DEBUG_BUGS 0x00001000
+#define FF_DEBUG_VIS_QP 0x00002000
+#define FF_DEBUG_VIS_MB_TYPE 0x00004000
+#define FF_DEBUG_BUFFERS 0x00008000
+#define FF_DEBUG_THREADS 0x00010000
+
+ /**
+ * debug
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int debug_mv;
+#define FF_DEBUG_VIS_MV_P_FOR 0x00000001 //visualize forward predicted MVs of P frames
+#define FF_DEBUG_VIS_MV_B_FOR 0x00000002 //visualize forward predicted MVs of B frames
+#define FF_DEBUG_VIS_MV_B_BACK 0x00000004 //visualize backward predicted MVs of B frames
+
+ /**
+ * Error recognition; may misdetect some more or less valid parts as errors.
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int err_recognition;
+#define AV_EF_CRCCHECK (1<<0)
+#define AV_EF_BITSTREAM (1<<1)
+#define AV_EF_BUFFER (1<<2)
+#define AV_EF_EXPLODE (1<<3)
+
+ /**
+ * opaque 64bit number (generally a PTS) that will be reordered and
+ * output in AVFrame.reordered_opaque
+ * @deprecated in favor of pkt_pts
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int64_t reordered_opaque;
+
+ /**
+ * Hardware accelerator in use
+ * - encoding: unused.
+ * - decoding: Set by libavcodec
+ */
+ struct AVHWAccel *hwaccel;
+
+ /**
+ * Hardware accelerator context.
+ * For some hardware accelerators, a global context needs to be
+ * provided by the user. In that case, this holds display-dependent
+ * data Libav cannot instantiate itself. Please refer to the
+ * Libav HW accelerator documentation to know how to fill this
+ * is. e.g. for VA API, this is a struct vaapi_context.
+ * - encoding: unused
+ * - decoding: Set by user
+ */
+ void *hwaccel_context;
+
+ /**
+ * error
+ * - encoding: Set by libavcodec if flags&CODEC_FLAG_PSNR.
+ * - decoding: unused
+ */
+ uint64_t error[AV_NUM_DATA_POINTERS];
+
+ /**
+ * DCT algorithm, see FF_DCT_* below
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int dct_algo;
+#define FF_DCT_AUTO 0
+#define FF_DCT_FASTINT 1
+#define FF_DCT_INT 2
+#define FF_DCT_MMX 3
+#define FF_DCT_ALTIVEC 5
+#define FF_DCT_FAAN 6
+
+ /**
+ * IDCT algorithm, see FF_IDCT_* below.
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int idct_algo;
+#define FF_IDCT_AUTO 0
+#define FF_IDCT_INT 1
+#define FF_IDCT_SIMPLE 2
+#define FF_IDCT_SIMPLEMMX 3
+#if FF_API_LIBMPEG2
+#define FF_IDCT_LIBMPEG2MMX 4
+#endif
+#if FF_API_MMI
+#define FF_IDCT_MMI 5
+#endif
+#define FF_IDCT_ARM 7
+#define FF_IDCT_ALTIVEC 8
+#define FF_IDCT_SH4 9
+#define FF_IDCT_SIMPLEARM 10
+#define FF_IDCT_H264 11
+#define FF_IDCT_VP3 12
+#define FF_IDCT_IPP 13
+#define FF_IDCT_XVIDMMX 14
+#define FF_IDCT_CAVS 15
+#define FF_IDCT_SIMPLEARMV5TE 16
+#define FF_IDCT_SIMPLEARMV6 17
+#define FF_IDCT_SIMPLEVIS 18
+#define FF_IDCT_WMV2 19
+#define FF_IDCT_FAAN 20
+#define FF_IDCT_EA 21
+#define FF_IDCT_SIMPLENEON 22
+#define FF_IDCT_SIMPLEALPHA 23
+#define FF_IDCT_BINK 24
+
+#if FF_API_DSP_MASK
+ /**
+ * Unused.
+ * @deprecated use av_set_cpu_flags_mask() instead.
+ */
+ attribute_deprecated unsigned dsp_mask;
+#endif
+
+ /**
+ * bits per sample/pixel from the demuxer (needed for huffyuv).
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by user.
+ */
+ int bits_per_coded_sample;
+
+ /**
+ * Bits per sample/pixel of internal libavcodec pixel/sample format.
+ * - encoding: set by user.
+ * - decoding: set by libavcodec.
+ */
+ int bits_per_raw_sample;
+
+ /**
+ * low resolution decoding, 1-> 1/2 size, 2->1/4 size
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ attribute_deprecated int lowres;
+
+ /**
+ * the picture in the bitstream
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by libavcodec.
+ */
+ AVFrame *coded_frame;
+
+ /**
+ * thread count
+ * is used to decide how many independent tasks should be passed to execute()
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int thread_count;
+
+ /**
+ * Which multithreading methods to use.
+ * Use of FF_THREAD_FRAME will increase decoding delay by one frame per thread,
+ * so clients which cannot provide future frames should not use it.
+ *
+ * - encoding: Set by user, otherwise the default is used.
+ * - decoding: Set by user, otherwise the default is used.
+ */
+ int thread_type;
+#define FF_THREAD_FRAME 1 ///< Decode more than one frame at once
+#define FF_THREAD_SLICE 2 ///< Decode more than one part of a single frame at once
+
+ /**
+ * Which multithreading methods are in use by the codec.
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by libavcodec.
+ */
+ int active_thread_type;
+
+ /**
+ * Set by the client if its custom get_buffer() callback can be called
+ * synchronously from another thread, which allows faster multithreaded decoding.
+ * draw_horiz_band() will be called from other threads regardless of this setting.
+ * Ignored if the default get_buffer() is used.
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int thread_safe_callbacks;
+
+ /**
+ * The codec may call this to execute several independent things.
+ * It will return only after finishing all tasks.
+ * The user may replace this with some multithreaded implementation,
+ * the default implementation will execute the parts serially.
+ * @param count the number of things to execute
+ * - encoding: Set by libavcodec, user can override.
+ * - decoding: Set by libavcodec, user can override.
+ */
+ int (*execute)(struct AVCodecContext *c, int (*func)(struct AVCodecContext *c2, void *arg), void *arg2, int *ret, int count, int size);
+
+ /**
+ * The codec may call this to execute several independent things.
+ * It will return only after finishing all tasks.
+ * The user may replace this with some multithreaded implementation,
+ * the default implementation will execute the parts serially.
+ * Also see avcodec_thread_init and e.g. the --enable-pthread configure option.
+ * @param c context passed also to func
+ * @param count the number of things to execute
+ * @param arg2 argument passed unchanged to func
+ * @param ret return values of executed functions, must have space for "count" values. May be NULL.
+ * @param func function that will be called count times, with jobnr from 0 to count-1.
+ * threadnr will be in the range 0 to c->thread_count-1 < MAX_THREADS and so that no
+ * two instances of func executing at the same time will have the same threadnr.
+ * @return always 0 currently, but code should handle a future improvement where when any call to func
+ * returns < 0 no further calls to func may be done and < 0 is returned.
+ * - encoding: Set by libavcodec, user can override.
+ * - decoding: Set by libavcodec, user can override.
+ */
+ int (*execute2)(struct AVCodecContext *c, int (*func)(struct AVCodecContext *c2, void *arg, int jobnr, int threadnr), void *arg2, int *ret, int count);
+
+ /**
+ * thread opaque
+ * Can be used by execute() to store some per AVCodecContext stuff.
+ * - encoding: set by execute()
+ * - decoding: set by execute()
+ */
+ void *thread_opaque;
+
+ /**
+ * noise vs. sse weight for the nsse comparsion function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int nsse_weight;
+
+ /**
+ * profile
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int profile;
+#define FF_PROFILE_UNKNOWN -99
+#define FF_PROFILE_RESERVED -100
+
+#define FF_PROFILE_AAC_MAIN 0
+#define FF_PROFILE_AAC_LOW 1
+#define FF_PROFILE_AAC_SSR 2
+#define FF_PROFILE_AAC_LTP 3
+#define FF_PROFILE_AAC_HE 4
+#define FF_PROFILE_AAC_HE_V2 28
+#define FF_PROFILE_AAC_LD 22
+#define FF_PROFILE_AAC_ELD 38
+
+#define FF_PROFILE_DTS 20
+#define FF_PROFILE_DTS_ES 30
+#define FF_PROFILE_DTS_96_24 40
+#define FF_PROFILE_DTS_HD_HRA 50
+#define FF_PROFILE_DTS_HD_MA 60
+
+#define FF_PROFILE_MPEG2_422 0
+#define FF_PROFILE_MPEG2_HIGH 1
+#define FF_PROFILE_MPEG2_SS 2
+#define FF_PROFILE_MPEG2_SNR_SCALABLE 3
+#define FF_PROFILE_MPEG2_MAIN 4
+#define FF_PROFILE_MPEG2_SIMPLE 5
+
+#define FF_PROFILE_H264_CONSTRAINED (1<<9) // 8+1; constraint_set1_flag
+#define FF_PROFILE_H264_INTRA (1<<11) // 8+3; constraint_set3_flag
+
+#define FF_PROFILE_H264_BASELINE 66
+#define FF_PROFILE_H264_CONSTRAINED_BASELINE (66|FF_PROFILE_H264_CONSTRAINED)
+#define FF_PROFILE_H264_MAIN 77
+#define FF_PROFILE_H264_EXTENDED 88
+#define FF_PROFILE_H264_HIGH 100
+#define FF_PROFILE_H264_HIGH_10 110
+#define FF_PROFILE_H264_HIGH_10_INTRA (110|FF_PROFILE_H264_INTRA)
+#define FF_PROFILE_H264_HIGH_422 122
+#define FF_PROFILE_H264_HIGH_422_INTRA (122|FF_PROFILE_H264_INTRA)
+#define FF_PROFILE_H264_HIGH_444 144
+#define FF_PROFILE_H264_HIGH_444_PREDICTIVE 244
+#define FF_PROFILE_H264_HIGH_444_INTRA (244|FF_PROFILE_H264_INTRA)
+#define FF_PROFILE_H264_CAVLC_444 44
+
+#define FF_PROFILE_VC1_SIMPLE 0
+#define FF_PROFILE_VC1_MAIN 1
+#define FF_PROFILE_VC1_COMPLEX 2
+#define FF_PROFILE_VC1_ADVANCED 3
+
+#define FF_PROFILE_MPEG4_SIMPLE 0
+#define FF_PROFILE_MPEG4_SIMPLE_SCALABLE 1
+#define FF_PROFILE_MPEG4_CORE 2
+#define FF_PROFILE_MPEG4_MAIN 3
+#define FF_PROFILE_MPEG4_N_BIT 4
+#define FF_PROFILE_MPEG4_SCALABLE_TEXTURE 5
+#define FF_PROFILE_MPEG4_SIMPLE_FACE_ANIMATION 6
+#define FF_PROFILE_MPEG4_BASIC_ANIMATED_TEXTURE 7
+#define FF_PROFILE_MPEG4_HYBRID 8
+#define FF_PROFILE_MPEG4_ADVANCED_REAL_TIME 9
+#define FF_PROFILE_MPEG4_CORE_SCALABLE 10
+#define FF_PROFILE_MPEG4_ADVANCED_CODING 11
+#define FF_PROFILE_MPEG4_ADVANCED_CORE 12
+#define FF_PROFILE_MPEG4_ADVANCED_SCALABLE_TEXTURE 13
+#define FF_PROFILE_MPEG4_SIMPLE_STUDIO 14
+#define FF_PROFILE_MPEG4_ADVANCED_SIMPLE 15
+
+ /**
+ * level
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int level;
+#define FF_LEVEL_UNKNOWN -99
+
+ /**
+ *
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ enum AVDiscard skip_loop_filter;
+
+ /**
+ *
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ enum AVDiscard skip_idct;
+
+ /**
+ *
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ enum AVDiscard skip_frame;
+
+ /**
+ * Header containing style information for text subtitles.
+ * For SUBTITLE_ASS subtitle type, it should contain the whole ASS
+ * [Script Info] and [V4+ Styles] section, plus the [Events] line and
+ * the Format line following. It shouldn't include any Dialogue line.
+ * - encoding: Set/allocated/freed by user (before avcodec_open2())
+ * - decoding: Set/allocated/freed by libavcodec (by avcodec_open2())
+ */
+ uint8_t *subtitle_header;
+ int subtitle_header_size;
+
+ /**
+ * Simulates errors in the bitstream to test error concealment.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int error_rate;
+
+ /**
+ * Current packet as passed into the decoder, to avoid having
+ * to pass the packet into every function. Currently only valid
+ * inside lavc and get/release_buffer callbacks.
+ * - decoding: set by avcodec_decode_*, read by get_buffer() for setting pkt_pts
+ * - encoding: unused
+ */
+ AVPacket *pkt;
+
+ /**
+ * VBV delay coded in the last frame (in periods of a 27 MHz clock).
+ * Used for compliant TS muxing.
+ * - encoding: Set by libavcodec.
+ * - decoding: unused.
+ */
+ uint64_t vbv_delay;
+} AVCodecContext;
+
+/**
+ * AVProfile.
+ */
+typedef struct AVProfile {
+ int profile;
+ const char *name; ///< short name for the profile
+} AVProfile;
+
+typedef struct AVCodecDefault AVCodecDefault;
+
+struct AVSubtitle;
+
+/**
+ * AVCodec.
+ */
+typedef struct AVCodec {
+ /**
+ * Name of the codec implementation.
+ * The name is globally unique among encoders and among decoders (but an
+ * encoder and a decoder can share the same name).
+ * This is the primary way to find a codec from the user perspective.
+ */
+ const char *name;
+ /**
+ * Descriptive name for the codec, meant to be more human readable than name.
+ * You should use the NULL_IF_CONFIG_SMALL() macro to define it.
+ */
+ const char *long_name;
+ enum AVMediaType type;
+ enum AVCodecID id;
+ /**
+ * Codec capabilities.
+ * see CODEC_CAP_*
+ */
+ int capabilities;
+ const AVRational *supported_framerates; ///< array of supported framerates, or NULL if any, array is terminated by {0,0}
+ const enum AVPixelFormat *pix_fmts; ///< array of supported pixel formats, or NULL if unknown, array is terminated by -1
+ const int *supported_samplerates; ///< array of supported audio samplerates, or NULL if unknown, array is terminated by 0
+ const enum AVSampleFormat *sample_fmts; ///< array of supported sample formats, or NULL if unknown, array is terminated by -1
+ const uint64_t *channel_layouts; ///< array of support channel layouts, or NULL if unknown. array is terminated by 0
+ attribute_deprecated uint8_t max_lowres; ///< maximum value for lowres supported by the decoder
+ const AVClass *priv_class; ///< AVClass for the private context
+ const AVProfile *profiles; ///< array of recognized profiles, or NULL if unknown, array is terminated by {FF_PROFILE_UNKNOWN}
+
+ /*****************************************************************
+ * No fields below this line are part of the public API. They
+ * may not be used outside of libavcodec and can be changed and
+ * removed at will.
+ * New public fields should be added right above.
+ *****************************************************************
+ */
+ int priv_data_size;
+ struct AVCodec *next;
+ /**
+ * @name Frame-level threading support functions
+ * @{
+ */
+ /**
+ * If defined, called on thread contexts when they are created.
+ * If the codec allocates writable tables in init(), re-allocate them here.
+ * priv_data will be set to a copy of the original.
+ */
+ int (*init_thread_copy)(AVCodecContext *);
+ /**
+ * Copy necessary context variables from a previous thread context to the current one.
+ * If not defined, the next thread will start automatically; otherwise, the codec
+ * must call ff_thread_finish_setup().
+ *
+ * dst and src will (rarely) point to the same context, in which case memcpy should be skipped.
+ */
+ int (*update_thread_context)(AVCodecContext *dst, const AVCodecContext *src);
+ /** @} */
+
+ /**
+ * Private codec-specific defaults.
+ */
+ const AVCodecDefault *defaults;
+
+ /**
+ * Initialize codec static data, called from avcodec_register().
+ */
+ void (*init_static_data)(struct AVCodec *codec);
+
+ int (*init)(AVCodecContext *);
+ int (*encode_sub)(AVCodecContext *, uint8_t *buf, int buf_size,
+ const struct AVSubtitle *sub);
+ /**
+ * Encode data to an AVPacket.
+ *
+ * @param avctx codec context
+ * @param avpkt output AVPacket (may contain a user-provided buffer)
+ * @param[in] frame AVFrame containing the raw data to be encoded
+ * @param[out] got_packet_ptr encoder sets to 0 or 1 to indicate that a
+ * non-empty packet was returned in avpkt.
+ * @return 0 on success, negative error code on failure
+ */
+ int (*encode2)(AVCodecContext *avctx, AVPacket *avpkt, const AVFrame *frame,
+ int *got_packet_ptr);
+ int (*decode)(AVCodecContext *, void *outdata, int *outdata_size, AVPacket *avpkt);
+ int (*close)(AVCodecContext *);
+ /**
+ * Flush buffers.
+ * Will be called when seeking
+ */
+ void (*flush)(AVCodecContext *);
+} AVCodec;
+
+/**
+ * AVHWAccel.
+ */
+typedef struct AVHWAccel {
+ /**
+ * Name of the hardware accelerated codec.
+ * The name is globally unique among encoders and among decoders (but an
+ * encoder and a decoder can share the same name).
+ */
+ const char *name;
+
+ /**
+ * Type of codec implemented by the hardware accelerator.
+ *
+ * See AVMEDIA_TYPE_xxx
+ */
+ enum AVMediaType type;
+
+ /**
+ * Codec implemented by the hardware accelerator.
+ *
+ * See AV_CODEC_ID_xxx
+ */
+ enum AVCodecID id;
+
+ /**
+ * Supported pixel format.
+ *
+ * Only hardware accelerated formats are supported here.
+ */
+ enum AVPixelFormat pix_fmt;
+
+ /**
+ * Hardware accelerated codec capabilities.
+ * see FF_HWACCEL_CODEC_CAP_*
+ */
+ int capabilities;
+
+ struct AVHWAccel *next;
+
+ /**
+ * Called at the beginning of each frame or field picture.
+ *
+ * Meaningful frame information (codec specific) is guaranteed to
+ * be parsed at this point. This function is mandatory.
+ *
+ * Note that buf can be NULL along with buf_size set to 0.
+ * Otherwise, this means the whole frame is available at this point.
+ *
+ * @param avctx the codec context
+ * @param buf the frame data buffer base
+ * @param buf_size the size of the frame in bytes
+ * @return zero if successful, a negative value otherwise
+ */
+ int (*start_frame)(AVCodecContext *avctx, const uint8_t *buf, uint32_t buf_size);
+
+ /**
+ * Callback for each slice.
+ *
+ * Meaningful slice information (codec specific) is guaranteed to
+ * be parsed at this point. This function is mandatory.
+ *
+ * @param avctx the codec context
+ * @param buf the slice data buffer base
+ * @param buf_size the size of the slice in bytes
+ * @return zero if successful, a negative value otherwise
+ */
+ int (*decode_slice)(AVCodecContext *avctx, const uint8_t *buf, uint32_t buf_size);
+
+ /**
+ * Called at the end of each frame or field picture.
+ *
+ * The whole picture is parsed at this point and can now be sent
+ * to the hardware accelerator. This function is mandatory.
+ *
+ * @param avctx the codec context
+ * @return zero if successful, a negative value otherwise
+ */
+ int (*end_frame)(AVCodecContext *avctx);
+
+ /**
+ * Size of HW accelerator private data.
+ *
+ * Private data is allocated with av_mallocz() before
+ * AVCodecContext.get_buffer() and deallocated after
+ * AVCodecContext.release_buffer().
+ */
+ int priv_data_size;
+} AVHWAccel;
+
+/**
+ * @defgroup lavc_picture AVPicture
+ *
+ * Functions for working with AVPicture
+ * @{
+ */
+
+/**
+ * four components are given, that's all.
+ * the last component is alpha
+ */
+typedef struct AVPicture {
+ uint8_t *data[AV_NUM_DATA_POINTERS];
+ int linesize[AV_NUM_DATA_POINTERS]; ///< number of bytes per line
+} AVPicture;
+
+/**
+ * @}
+ */
+
+#define AVPALETTE_SIZE 1024
+#define AVPALETTE_COUNT 256
+
+enum AVSubtitleType {
+ SUBTITLE_NONE,
+
+ SUBTITLE_BITMAP, ///< A bitmap, pict will be set
+
+ /**
+ * Plain text, the text field must be set by the decoder and is
+ * authoritative. ass and pict fields may contain approximations.
+ */
+ SUBTITLE_TEXT,
+
+ /**
+ * Formatted text, the ass field must be set by the decoder and is
+ * authoritative. pict and text fields may contain approximations.
+ */
+ SUBTITLE_ASS,
+};
+
+#define AV_SUBTITLE_FLAG_FORCED 0x00000001
+
+typedef struct AVSubtitleRect {
+ int x; ///< top left corner of pict, undefined when pict is not set
+ int y; ///< top left corner of pict, undefined when pict is not set
+ int w; ///< width of pict, undefined when pict is not set
+ int h; ///< height of pict, undefined when pict is not set
+ int nb_colors; ///< number of colors in pict, undefined when pict is not set
+
+ /**
+ * data+linesize for the bitmap of this subtitle.
+ * can be set for text/ass as well once they where rendered
+ */
+ AVPicture pict;
+ enum AVSubtitleType type;
+
+ char *text; ///< 0 terminated plain UTF-8 text
+
+ /**
+ * 0 terminated ASS/SSA compatible event line.
+ * The pressentation of this is unaffected by the other values in this
+ * struct.
+ */
+ char *ass;
+ int flags;
+} AVSubtitleRect;
+
+typedef struct AVSubtitle {
+ uint16_t format; /* 0 = graphics */
+ uint32_t start_display_time; /* relative to packet pts, in ms */
+ uint32_t end_display_time; /* relative to packet pts, in ms */
+ unsigned num_rects;
+ AVSubtitleRect **rects;
+ int64_t pts; ///< Same as packet pts, in AV_TIME_BASE
+} AVSubtitle;
+
+/**
+ * If c is NULL, returns the first registered codec,
+ * if c is non-NULL, returns the next registered codec after c,
+ * or NULL if c is the last one.
+ */
+AVCodec *av_codec_next(const AVCodec *c);
+
+/**
+ * Return the LIBAVCODEC_VERSION_INT constant.
+ */
+unsigned avcodec_version(void);
+
+/**
+ * Return the libavcodec build-time configuration.
+ */
+const char *avcodec_configuration(void);
+
+/**
+ * Return the libavcodec license.
+ */
+const char *avcodec_license(void);
+
+/**
+ * Register the codec codec and initialize libavcodec.
+ *
+ * @warning either this function or avcodec_register_all() must be called
+ * before any other libavcodec functions.
+ *
+ * @see avcodec_register_all()
+ */
+void avcodec_register(AVCodec *codec);
+
+/**
+ * Register all the codecs, parsers and bitstream filters which were enabled at
+ * configuration time. If you do not call this function you can select exactly
+ * which formats you want to support, by using the individual registration
+ * functions.
+ *
+ * @see avcodec_register
+ * @see av_register_codec_parser
+ * @see av_register_bitstream_filter
+ */
+void avcodec_register_all(void);
+
+/**
+ * Allocate an AVCodecContext and set its fields to default values. The
+ * resulting struct can be deallocated by calling avcodec_close() on it followed
+ * by av_free().
+ *
+ * @param codec if non-NULL, allocate private data and initialize defaults
+ * for the given codec. It is illegal to then call avcodec_open2()
+ * with a different codec.
+ * If NULL, then the codec-specific defaults won't be initialized,
+ * which may result in suboptimal default settings (this is
+ * important mainly for encoders, e.g. libx264).
+ *
+ * @return An AVCodecContext filled with default values or NULL on failure.
+ * @see avcodec_get_context_defaults
+ */
+AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);
+
+/**
+ * Set the fields of the given AVCodecContext to default values corresponding
+ * to the given codec (defaults may be codec-dependent).
+ *
+ * Do not call this function if a non-NULL codec has been passed
+ * to avcodec_alloc_context3() that allocated this AVCodecContext.
+ * If codec is non-NULL, it is illegal to call avcodec_open2() with a
+ * different codec on this AVCodecContext.
+ */
+int avcodec_get_context_defaults3(AVCodecContext *s, const AVCodec *codec);
+
+/**
+ * Get the AVClass for AVCodecContext. It can be used in combination with
+ * AV_OPT_SEARCH_FAKE_OBJ for examining options.
+ *
+ * @see av_opt_find().
+ */
+const AVClass *avcodec_get_class(void);
+
+/**
+ * Copy the settings of the source AVCodecContext into the destination
+ * AVCodecContext. The resulting destination codec context will be
+ * unopened, i.e. you are required to call avcodec_open2() before you
+ * can use this AVCodecContext to decode/encode video/audio data.
+ *
+ * @param dest target codec context, should be initialized with
+ * avcodec_alloc_context3(), but otherwise uninitialized
+ * @param src source codec context
+ * @return AVERROR() on error (e.g. memory allocation error), 0 on success
+ */
+int avcodec_copy_context(AVCodecContext *dest, const AVCodecContext *src);
+
+/**
+ * Allocate an AVFrame and set its fields to default values. The resulting
+ * struct must be freed using avcodec_free_frame().
+ *
+ * @return An AVFrame filled with default values or NULL on failure.
+ * @see avcodec_get_frame_defaults
+ */
+AVFrame *avcodec_alloc_frame(void);
+
+/**
+ * Set the fields of the given AVFrame to default values.
+ *
+ * @param frame The AVFrame of which the fields should be set to default values.
+ */
+void avcodec_get_frame_defaults(AVFrame *frame);
+
+/**
+ * Free the frame and any dynamically allocated objects in it,
+ * e.g. extended_data.
+ *
+ * @param frame frame to be freed. The pointer will be set to NULL.
+ *
+ * @warning this function does NOT free the data buffers themselves
+ * (it does not know how, since they might have been allocated with
+ * a custom get_buffer()).
+ */
+void avcodec_free_frame(AVFrame **frame);
+
+/**
+ * Initialize the AVCodecContext to use the given AVCodec. Prior to using this
+ * function the context has to be allocated with avcodec_alloc_context3().
+ *
+ * The functions avcodec_find_decoder_by_name(), avcodec_find_encoder_by_name(),
+ * avcodec_find_decoder() and avcodec_find_encoder() provide an easy way for
+ * retrieving a codec.
+ *
+ * @warning This function is not thread safe!
+ *
+ * @code
+ * avcodec_register_all();
+ * av_dict_set(&opts, "b", "2.5M", 0);
+ * codec = avcodec_find_decoder(AV_CODEC_ID_H264);
+ * if (!codec)
+ * exit(1);
+ *
+ * context = avcodec_alloc_context3(codec);
+ *
+ * if (avcodec_open2(context, codec, opts) < 0)
+ * exit(1);
+ * @endcode
+ *
+ * @param avctx The context to initialize.
+ * @param codec The codec to open this context for. If a non-NULL codec has been
+ * previously passed to avcodec_alloc_context3() or
+ * avcodec_get_context_defaults3() for this context, then this
+ * parameter MUST be either NULL or equal to the previously passed
+ * codec.
+ * @param options A dictionary filled with AVCodecContext and codec-private options.
+ * On return this object will be filled with options that were not found.
+ *
+ * @return zero on success, a negative value on error
+ * @see avcodec_alloc_context3(), avcodec_find_decoder(), avcodec_find_encoder(),
+ * av_dict_set(), av_opt_find().
+ */
+int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);
+
+/**
+ * Close a given AVCodecContext and free all the data associated with it
+ * (but not the AVCodecContext itself).
+ *
+ * Calling this function on an AVCodecContext that hasn't been opened will free
+ * the codec-specific data allocated in avcodec_alloc_context3() /
+ * avcodec_get_context_defaults3() with a non-NULL codec. Subsequent calls will
+ * do nothing.
+ */
+int avcodec_close(AVCodecContext *avctx);
+
+/**
+ * Free all allocated data in the given subtitle struct.
+ *
+ * @param sub AVSubtitle to free.
+ */
+void avsubtitle_free(AVSubtitle *sub);
+
+/**
+ * @}
+ */
+
+/**
+ * @addtogroup lavc_packet
+ * @{
+ */
+
+/**
+ * Default packet destructor.
+ */
+void av_destruct_packet(AVPacket *pkt);
+
+/**
+ * Initialize optional fields of a packet with default values.
+ *
+ * Note, this does not touch the data and size members, which have to be
+ * initialized separately.
+ *
+ * @param pkt packet
+ */
+void av_init_packet(AVPacket *pkt);
+
+/**
+ * Allocate the payload of a packet and initialize its fields with
+ * default values.
+ *
+ * @param pkt packet
+ * @param size wanted payload size
+ * @return 0 if OK, AVERROR_xxx otherwise
+ */
+int av_new_packet(AVPacket *pkt, int size);
+
+/**
+ * Reduce packet size, correctly zeroing padding
+ *
+ * @param pkt packet
+ * @param size new size
+ */
+void av_shrink_packet(AVPacket *pkt, int size);
+
+/**
+ * Increase packet size, correctly zeroing padding
+ *
+ * @param pkt packet
+ * @param grow_by number of bytes by which to increase the size of the packet
+ */
+int av_grow_packet(AVPacket *pkt, int grow_by);
+
+/**
+ * @warning This is a hack - the packet memory allocation stuff is broken. The
+ * packet is allocated if it was not really allocated.
+ */
+int av_dup_packet(AVPacket *pkt);
+
+/**
+ * Free a packet.
+ *
+ * @param pkt packet to free
+ */
+void av_free_packet(AVPacket *pkt);
+
+/**
+ * Allocate new information of a packet.
+ *
+ * @param pkt packet
+ * @param type side information type
+ * @param size side information size
+ * @return pointer to fresh allocated data or NULL otherwise
+ */
+uint8_t* av_packet_new_side_data(AVPacket *pkt, enum AVPacketSideDataType type,
+ int size);
+
+/**
+ * Shrink the already allocated side data buffer
+ *
+ * @param pkt packet
+ * @param type side information type
+ * @param size new side information size
+ * @return 0 on success, < 0 on failure
+ */
+int av_packet_shrink_side_data(AVPacket *pkt, enum AVPacketSideDataType type,
+ int size);
+
+/**
+ * Get side information from packet.
+ *
+ * @param pkt packet
+ * @param type desired side information type
+ * @param size pointer for side information size to store (optional)
+ * @return pointer to data if present or NULL otherwise
+ */
+uint8_t* av_packet_get_side_data(AVPacket *pkt, enum AVPacketSideDataType type,
+ int *size);
+
+/**
+ * @}
+ */
+
+/**
+ * @addtogroup lavc_decoding
+ * @{
+ */
+
+/**
+ * Find a registered decoder with a matching codec ID.
+ *
+ * @param id AVCodecID of the requested decoder
+ * @return A decoder if one was found, NULL otherwise.
+ */
+AVCodec *avcodec_find_decoder(enum AVCodecID id);
+
+/**
+ * Find a registered decoder with the specified name.
+ *
+ * @param name name of the requested decoder
+ * @return A decoder if one was found, NULL otherwise.
+ */
+AVCodec *avcodec_find_decoder_by_name(const char *name);
+
+int avcodec_default_get_buffer(AVCodecContext *s, AVFrame *pic);
+void avcodec_default_release_buffer(AVCodecContext *s, AVFrame *pic);
+int avcodec_default_reget_buffer(AVCodecContext *s, AVFrame *pic);
+
+/**
+ * Return the amount of padding in pixels which the get_buffer callback must
+ * provide around the edge of the image for codecs which do not have the
+ * CODEC_FLAG_EMU_EDGE flag.
+ *
+ * @return Required padding in pixels.
+ */
+unsigned avcodec_get_edge_width(void);
+
+/**
+ * Modify width and height values so that they will result in a memory
+ * buffer that is acceptable for the codec if you do not use any horizontal
+ * padding.
+ *
+ * May only be used if a codec with CODEC_CAP_DR1 has been opened.
+ * If CODEC_FLAG_EMU_EDGE is not set, the dimensions must have been increased
+ * according to avcodec_get_edge_width() before.
+ */
+void avcodec_align_dimensions(AVCodecContext *s, int *width, int *height);
+
+/**
+ * Modify width and height values so that they will result in a memory
+ * buffer that is acceptable for the codec if you also ensure that all
+ * line sizes are a multiple of the respective linesize_align[i].
+ *
+ * May only be used if a codec with CODEC_CAP_DR1 has been opened.
+ * If CODEC_FLAG_EMU_EDGE is not set, the dimensions must have been increased
+ * according to avcodec_get_edge_width() before.
+ */
+void avcodec_align_dimensions2(AVCodecContext *s, int *width, int *height,
+ int linesize_align[AV_NUM_DATA_POINTERS]);
+
+#if FF_API_OLD_DECODE_AUDIO
+/**
+ * Wrapper function which calls avcodec_decode_audio4.
+ *
+ * @deprecated Use avcodec_decode_audio4 instead.
+ *
+ * Decode the audio frame of size avpkt->size from avpkt->data into samples.
+ * Some decoders may support multiple frames in a single AVPacket, such
+ * decoders would then just decode the first frame. In this case,
+ * avcodec_decode_audio3 has to be called again with an AVPacket that contains
+ * the remaining data in order to decode the second frame etc.
+ * If no frame
+ * could be outputted, frame_size_ptr is zero. Otherwise, it is the
+ * decompressed frame size in bytes.
+ *
+ * @warning You must set frame_size_ptr to the allocated size of the
+ * output buffer before calling avcodec_decode_audio3().
+ *
+ * @warning The input buffer must be FF_INPUT_BUFFER_PADDING_SIZE larger than
+ * the actual read bytes because some optimized bitstream readers read 32 or 64
+ * bits at once and could read over the end.
+ *
+ * @warning The end of the input buffer avpkt->data should be set to 0 to ensure that
+ * no overreading happens for damaged MPEG streams.
+ *
+ * @warning You must not provide a custom get_buffer() when using
+ * avcodec_decode_audio3(). Doing so will override it with
+ * avcodec_default_get_buffer. Use avcodec_decode_audio4() instead,
+ * which does allow the application to provide a custom get_buffer().
+ *
+ * @note You might have to align the input buffer avpkt->data and output buffer
+ * samples. The alignment requirements depend on the CPU: On some CPUs it isn't
+ * necessary at all, on others it won't work at all if not aligned and on others
+ * it will work but it will have an impact on performance.
+ *
+ * In practice, avpkt->data should have 4 byte alignment at minimum and
+ * samples should be 16 byte aligned unless the CPU doesn't need it
+ * (AltiVec and SSE do).
+ *
+ * @note Codecs which have the CODEC_CAP_DELAY capability set have a delay
+ * between input and output, these need to be fed with avpkt->data=NULL,
+ * avpkt->size=0 at the end to return the remaining frames.
+ *
+ * @param avctx the codec context
+ * @param[out] samples the output buffer, sample type in avctx->sample_fmt
+ * If the sample format is planar, each channel plane will
+ * be the same size, with no padding between channels.
+ * @param[in,out] frame_size_ptr the output buffer size in bytes
+ * @param[in] avpkt The input AVPacket containing the input buffer.
+ * You can create such packet with av_init_packet() and by then setting
+ * data and size, some decoders might in addition need other fields.
+ * All decoders are designed to use the least fields possible though.
+ * @return On error a negative value is returned, otherwise the number of bytes
+ * used or zero if no frame data was decompressed (used) from the input AVPacket.
+ */
+attribute_deprecated int avcodec_decode_audio3(AVCodecContext *avctx, int16_t *samples,
+ int *frame_size_ptr,
+ AVPacket *avpkt);
+#endif
+
+/**
+ * Decode the audio frame of size avpkt->size from avpkt->data into frame.
+ *
+ * Some decoders may support multiple frames in a single AVPacket. Such
+ * decoders would then just decode the first frame. In this case,
+ * avcodec_decode_audio4 has to be called again with an AVPacket containing
+ * the remaining data in order to decode the second frame, etc...
+ * Even if no frames are returned, the packet needs to be fed to the decoder
+ * with remaining data until it is completely consumed or an error occurs.
+ *
+ * @warning The input buffer, avpkt->data must be FF_INPUT_BUFFER_PADDING_SIZE
+ * larger than the actual read bytes because some optimized bitstream
+ * readers read 32 or 64 bits at once and could read over the end.
+ *
+ * @note You might have to align the input buffer. The alignment requirements
+ * depend on the CPU and the decoder.
+ *
+ * @param avctx the codec context
+ * @param[out] frame The AVFrame in which to store decoded audio samples.
+ * Decoders request a buffer of a particular size by setting
+ * AVFrame.nb_samples prior to calling get_buffer(). The
+ * decoder may, however, only utilize part of the buffer by
+ * setting AVFrame.nb_samples to a smaller value in the
+ * output frame.
+ * @param[out] got_frame_ptr Zero if no frame could be decoded, otherwise it is
+ * non-zero.
+ * @param[in] avpkt The input AVPacket containing the input buffer.
+ * At least avpkt->data and avpkt->size should be set. Some
+ * decoders might also require additional fields to be set.
+ * @return A negative error code is returned if an error occurred during
+ * decoding, otherwise the number of bytes consumed from the input
+ * AVPacket is returned.
+ */
+int avcodec_decode_audio4(AVCodecContext *avctx, AVFrame *frame,
+ int *got_frame_ptr, AVPacket *avpkt);
+
+/**
+ * Decode the video frame of size avpkt->size from avpkt->data into picture.
+ * Some decoders may support multiple frames in a single AVPacket, such
+ * decoders would then just decode the first frame.
+ *
+ * @warning The input buffer must be FF_INPUT_BUFFER_PADDING_SIZE larger than
+ * the actual read bytes because some optimized bitstream readers read 32 or 64
+ * bits at once and could read over the end.
+ *
+ * @warning The end of the input buffer buf should be set to 0 to ensure that
+ * no overreading happens for damaged MPEG streams.
+ *
+ * @note You might have to align the input buffer avpkt->data.
+ * The alignment requirements depend on the CPU: on some CPUs it isn't
+ * necessary at all, on others it won't work at all if not aligned and on others
+ * it will work but it will have an impact on performance.
+ *
+ * In practice, avpkt->data should have 4 byte alignment at minimum.
+ *
+ * @note Codecs which have the CODEC_CAP_DELAY capability set have a delay
+ * between input and output, these need to be fed with avpkt->data=NULL,
+ * avpkt->size=0 at the end to return the remaining frames.
+ *
+ * @param avctx the codec context
+ * @param[out] picture The AVFrame in which the decoded video frame will be stored.
+ * Use avcodec_alloc_frame to get an AVFrame, the codec will
+ * allocate memory for the actual bitmap.
+ * with default get/release_buffer(), the decoder frees/reuses the bitmap as it sees fit.
+ * with overridden get/release_buffer() (needs CODEC_CAP_DR1) the user decides into what buffer the decoder
+ * decodes and the decoder tells the user once it does not need the data anymore,
+ * the user app can at this point free/reuse/keep the memory as it sees fit.
+ *
+ * @param[in] avpkt The input AVpacket containing the input buffer.
+ * You can create such packet with av_init_packet() and by then setting
+ * data and size, some decoders might in addition need other fields like
+ * flags&AV_PKT_FLAG_KEY. All decoders are designed to use the least
+ * fields possible.
+ * @param[in,out] got_picture_ptr Zero if no frame could be decompressed, otherwise, it is nonzero.
+ * @return On error a negative value is returned, otherwise the number of bytes
+ * used or zero if no frame could be decompressed.
+ */
+int avcodec_decode_video2(AVCodecContext *avctx, AVFrame *picture,
+ int *got_picture_ptr,
+ AVPacket *avpkt);
+
+/**
+ * Decode a subtitle message.
+ * Return a negative value on error, otherwise return the number of bytes used.
+ * If no subtitle could be decompressed, got_sub_ptr is zero.
+ * Otherwise, the subtitle is stored in *sub.
+ * Note that CODEC_CAP_DR1 is not available for subtitle codecs. This is for
+ * simplicity, because the performance difference is expect to be negligible
+ * and reusing a get_buffer written for video codecs would probably perform badly
+ * due to a potentially very different allocation pattern.
+ *
+ * @param avctx the codec context
+ * @param[out] sub The AVSubtitle in which the decoded subtitle will be stored, must be
+ freed with avsubtitle_free if *got_sub_ptr is set.
+ * @param[in,out] got_sub_ptr Zero if no subtitle could be decompressed, otherwise, it is nonzero.
+ * @param[in] avpkt The input AVPacket containing the input buffer.
+ */
+int avcodec_decode_subtitle2(AVCodecContext *avctx, AVSubtitle *sub,
+ int *got_sub_ptr,
+ AVPacket *avpkt);
+
+/**
+ * @defgroup lavc_parsing Frame parsing
+ * @{
+ */
+
+typedef struct AVCodecParserContext {
+ void *priv_data;
+ struct AVCodecParser *parser;
+ int64_t frame_offset; /* offset of the current frame */
+ int64_t cur_offset; /* current offset
+ (incremented by each av_parser_parse()) */
+ int64_t next_frame_offset; /* offset of the next frame */
+ /* video info */
+ int pict_type; /* XXX: Put it back in AVCodecContext. */
+ /**
+ * This field is used for proper frame duration computation in lavf.
+ * It signals, how much longer the frame duration of the current frame
+ * is compared to normal frame duration.
+ *
+ * frame_duration = (1 + repeat_pict) * time_base
+ *
+ * It is used by codecs like H.264 to display telecined material.
+ */
+ int repeat_pict; /* XXX: Put it back in AVCodecContext. */
+ int64_t pts; /* pts of the current frame */
+ int64_t dts; /* dts of the current frame */
+
+ /* private data */
+ int64_t last_pts;
+ int64_t last_dts;
+ int fetch_timestamp;
+
+#define AV_PARSER_PTS_NB 4
+ int cur_frame_start_index;
+ int64_t cur_frame_offset[AV_PARSER_PTS_NB];
+ int64_t cur_frame_pts[AV_PARSER_PTS_NB];
+ int64_t cur_frame_dts[AV_PARSER_PTS_NB];
+
+ int flags;
+#define PARSER_FLAG_COMPLETE_FRAMES 0x0001
+#define PARSER_FLAG_ONCE 0x0002
+/// Set if the parser has a valid file offset
+#define PARSER_FLAG_FETCHED_OFFSET 0x0004
+
+ int64_t offset; ///< byte offset from starting packet start
+ int64_t cur_frame_end[AV_PARSER_PTS_NB];
+
+ /**
+ * Set by parser to 1 for key frames and 0 for non-key frames.
+ * It is initialized to -1, so if the parser doesn't set this flag,
+ * old-style fallback using AV_PICTURE_TYPE_I picture type as key frames
+ * will be used.
+ */
+ int key_frame;
+
+ /**
+ * Time difference in stream time base units from the pts of this
+ * packet to the point at which the output from the decoder has converged
+ * independent from the availability of previous frames. That is, the
+ * frames are virtually identical no matter if decoding started from
+ * the very first frame or from this keyframe.
+ * Is AV_NOPTS_VALUE if unknown.
+ * This field is not the display duration of the current frame.
+ * This field has no meaning if the packet does not have AV_PKT_FLAG_KEY
+ * set.
+ *
+ * The purpose of this field is to allow seeking in streams that have no
+ * keyframes in the conventional sense. It corresponds to the
+ * recovery point SEI in H.264 and match_time_delta in NUT. It is also
+ * essential for some types of subtitle streams to ensure that all
+ * subtitles are correctly displayed after seeking.
+ */
+ int64_t convergence_duration;
+
+ // Timestamp generation support:
+ /**
+ * Synchronization point for start of timestamp generation.
+ *
+ * Set to >0 for sync point, 0 for no sync point and <0 for undefined
+ * (default).
+ *
+ * For example, this corresponds to presence of H.264 buffering period
+ * SEI message.
+ */
+ int dts_sync_point;
+
+ /**
+ * Offset of the current timestamp against last timestamp sync point in
+ * units of AVCodecContext.time_base.
+ *
+ * Set to INT_MIN when dts_sync_point unused. Otherwise, it must
+ * contain a valid timestamp offset.
+ *
+ * Note that the timestamp of sync point has usually a nonzero
+ * dts_ref_dts_delta, which refers to the previous sync point. Offset of
+ * the next frame after timestamp sync point will be usually 1.
+ *
+ * For example, this corresponds to H.264 cpb_removal_delay.
+ */
+ int dts_ref_dts_delta;
+
+ /**
+ * Presentation delay of current frame in units of AVCodecContext.time_base.
+ *
+ * Set to INT_MIN when dts_sync_point unused. Otherwise, it must
+ * contain valid non-negative timestamp delta (presentation time of a frame
+ * must not lie in the past).
+ *
+ * This delay represents the difference between decoding and presentation
+ * time of the frame.
+ *
+ * For example, this corresponds to H.264 dpb_output_delay.
+ */
+ int pts_dts_delta;
+
+ /**
+ * Position of the packet in file.
+ *
+ * Analogous to cur_frame_pts/dts
+ */
+ int64_t cur_frame_pos[AV_PARSER_PTS_NB];
+
+ /**
+ * Byte position of currently parsed frame in stream.
+ */
+ int64_t pos;
+
+ /**
+ * Previous frame byte position.
+ */
+ int64_t last_pos;
+
+ /**
+ * Duration of the current frame.
+ * For audio, this is in units of 1 / AVCodecContext.sample_rate.
+ * For all other types, this is in units of AVCodecContext.time_base.
+ */
+ int duration;
+} AVCodecParserContext;
+
+typedef struct AVCodecParser {
+ int codec_ids[5]; /* several codec IDs are permitted */
+ int priv_data_size;
+ int (*parser_init)(AVCodecParserContext *s);
+ int (*parser_parse)(AVCodecParserContext *s,
+ AVCodecContext *avctx,
+ const uint8_t **poutbuf, int *poutbuf_size,
+ const uint8_t *buf, int buf_size);
+ void (*parser_close)(AVCodecParserContext *s);
+ int (*split)(AVCodecContext *avctx, const uint8_t *buf, int buf_size);
+ struct AVCodecParser *next;
+} AVCodecParser;
+
+AVCodecParser *av_parser_next(AVCodecParser *c);
+
+void av_register_codec_parser(AVCodecParser *parser);
+AVCodecParserContext *av_parser_init(int codec_id);
+
+/**
+ * Parse a packet.
+ *
+ * @param s parser context.
+ * @param avctx codec context.
+ * @param poutbuf set to pointer to parsed buffer or NULL if not yet finished.
+ * @param poutbuf_size set to size of parsed buffer or zero if not yet finished.
+ * @param buf input buffer.
+ * @param buf_size input length, to signal EOF, this should be 0 (so that the last frame can be output).
+ * @param pts input presentation timestamp.
+ * @param dts input decoding timestamp.
+ * @param pos input byte position in stream.
+ * @return the number of bytes of the input bitstream used.
+ *
+ * Example:
+ * @code
+ * while(in_len){
+ * len = av_parser_parse2(myparser, AVCodecContext, &data, &size,
+ * in_data, in_len,
+ * pts, dts, pos);
+ * in_data += len;
+ * in_len -= len;
+ *
+ * if(size)
+ * decode_frame(data, size);
+ * }
+ * @endcode
+ */
+int av_parser_parse2(AVCodecParserContext *s,
+ AVCodecContext *avctx,
+ uint8_t **poutbuf, int *poutbuf_size,
+ const uint8_t *buf, int buf_size,
+ int64_t pts, int64_t dts,
+ int64_t pos);
+
+/**
+ * @return 0 if the output buffer is a subset of the input, 1 if it is allocated and must be freed
+ * @deprecated use AVBitstreamFilter
+ */
+int av_parser_change(AVCodecParserContext *s,
+ AVCodecContext *avctx,
+ uint8_t **poutbuf, int *poutbuf_size,
+ const uint8_t *buf, int buf_size, int keyframe);
+void av_parser_close(AVCodecParserContext *s);
+
+/**
+ * @}
+ * @}
+ */
+
+/**
+ * @addtogroup lavc_encoding
+ * @{
+ */
+
+/**
+ * Find a registered encoder with a matching codec ID.
+ *
+ * @param id AVCodecID of the requested encoder
+ * @return An encoder if one was found, NULL otherwise.
+ */
+AVCodec *avcodec_find_encoder(enum AVCodecID id);
+
+/**
+ * Find a registered encoder with the specified name.
+ *
+ * @param name name of the requested encoder
+ * @return An encoder if one was found, NULL otherwise.
+ */
+AVCodec *avcodec_find_encoder_by_name(const char *name);
+
+#if FF_API_OLD_ENCODE_AUDIO
+/**
+ * Encode an audio frame from samples into buf.
+ *
+ * @deprecated Use avcodec_encode_audio2 instead.
+ *
+ * @note The output buffer should be at least FF_MIN_BUFFER_SIZE bytes large.
+ * However, for codecs with avctx->frame_size equal to 0 (e.g. PCM) the user
+ * will know how much space is needed because it depends on the value passed
+ * in buf_size as described below. In that case a lower value can be used.
+ *
+ * @param avctx the codec context
+ * @param[out] buf the output buffer
+ * @param[in] buf_size the output buffer size
+ * @param[in] samples the input buffer containing the samples
+ * The number of samples read from this buffer is frame_size*channels,
+ * both of which are defined in avctx.
+ * For codecs which have avctx->frame_size equal to 0 (e.g. PCM) the number of
+ * samples read from samples is equal to:
+ * buf_size * 8 / (avctx->channels * av_get_bits_per_sample(avctx->codec_id))
+ * This also implies that av_get_bits_per_sample() must not return 0 for these
+ * codecs.
+ * @return On error a negative value is returned, on success zero or the number
+ * of bytes used to encode the data read from the input buffer.
+ */
+int attribute_deprecated avcodec_encode_audio(AVCodecContext *avctx,
+ uint8_t *buf, int buf_size,
+ const short *samples);
+#endif
+
+/**
+ * Encode a frame of audio.
+ *
+ * Takes input samples from frame and writes the next output packet, if
+ * available, to avpkt. The output packet does not necessarily contain data for
+ * the most recent frame, as encoders can delay, split, and combine input frames
+ * internally as needed.
+ *
+ * @param avctx codec context
+ * @param avpkt output AVPacket.
+ * The user can supply an output buffer by setting
+ * avpkt->data and avpkt->size prior to calling the
+ * function, but if the size of the user-provided data is not
+ * large enough, encoding will fail. All other AVPacket fields
+ * will be reset by the encoder using av_init_packet(). If
+ * avpkt->data is NULL, the encoder will allocate it.
+ * The encoder will set avpkt->size to the size of the
+ * output packet.
+ *
+ * If this function fails or produces no output, avpkt will be
+ * freed using av_free_packet() (i.e. avpkt->destruct will be
+ * called to free the user supplied buffer).
+ * @param[in] frame AVFrame containing the raw audio data to be encoded.
+ * May be NULL when flushing an encoder that has the
+ * CODEC_CAP_DELAY capability set.
+ * If CODEC_CAP_VARIABLE_FRAME_SIZE is set, then each frame
+ * can have any number of samples.
+ * If it is not set, frame->nb_samples must be equal to
+ * avctx->frame_size for all frames except the last.
+ * The final frame may be smaller than avctx->frame_size.
+ * @param[out] got_packet_ptr This field is set to 1 by libavcodec if the
+ * output packet is non-empty, and to 0 if it is
+ * empty. If the function returns an error, the
+ * packet can be assumed to be invalid, and the
+ * value of got_packet_ptr is undefined and should
+ * not be used.
+ * @return 0 on success, negative error code on failure
+ */
+int avcodec_encode_audio2(AVCodecContext *avctx, AVPacket *avpkt,
+ const AVFrame *frame, int *got_packet_ptr);
+
+#if FF_API_OLD_ENCODE_VIDEO
+/**
+ * @deprecated use avcodec_encode_video2() instead.
+ *
+ * Encode a video frame from pict into buf.
+ * The input picture should be
+ * stored using a specific format, namely avctx.pix_fmt.
+ *
+ * @param avctx the codec context
+ * @param[out] buf the output buffer for the bitstream of encoded frame
+ * @param[in] buf_size the size of the output buffer in bytes
+ * @param[in] pict the input picture to encode
+ * @return On error a negative value is returned, on success zero or the number
+ * of bytes used from the output buffer.
+ */
+attribute_deprecated
+int avcodec_encode_video(AVCodecContext *avctx, uint8_t *buf, int buf_size,
+ const AVFrame *pict);
+#endif
+
+/**
+ * Encode a frame of video.
+ *
+ * Takes input raw video data from frame and writes the next output packet, if
+ * available, to avpkt. The output packet does not necessarily contain data for
+ * the most recent frame, as encoders can delay and reorder input frames
+ * internally as needed.
+ *
+ * @param avctx codec context
+ * @param avpkt output AVPacket.
+ * The user can supply an output buffer by setting
+ * avpkt->data and avpkt->size prior to calling the
+ * function, but if the size of the user-provided data is not
+ * large enough, encoding will fail. All other AVPacket fields
+ * will be reset by the encoder using av_init_packet(). If
+ * avpkt->data is NULL, the encoder will allocate it.
+ * The encoder will set avpkt->size to the size of the
+ * output packet. The returned data (if any) belongs to the
+ * caller, he is responsible for freeing it.
+ *
+ * If this function fails or produces no output, avpkt will be
+ * freed using av_free_packet() (i.e. avpkt->destruct will be
+ * called to free the user supplied buffer).
+ * @param[in] frame AVFrame containing the raw video data to be encoded.
+ * May be NULL when flushing an encoder that has the
+ * CODEC_CAP_DELAY capability set.
+ * @param[out] got_packet_ptr This field is set to 1 by libavcodec if the
+ * output packet is non-empty, and to 0 if it is
+ * empty. If the function returns an error, the
+ * packet can be assumed to be invalid, and the
+ * value of got_packet_ptr is undefined and should
+ * not be used.
+ * @return 0 on success, negative error code on failure
+ */
+int avcodec_encode_video2(AVCodecContext *avctx, AVPacket *avpkt,
+ const AVFrame *frame, int *got_packet_ptr);
+
+int avcodec_encode_subtitle(AVCodecContext *avctx, uint8_t *buf, int buf_size,
+ const AVSubtitle *sub);
+
+
+/**
+ * @}
+ */
+
+#if FF_API_AVCODEC_RESAMPLE
+/**
+ * @defgroup lavc_resample Audio resampling
+ * @ingroup libavc
+ * @deprecated use libavresample instead
+ *
+ * @{
+ */
+struct ReSampleContext;
+struct AVResampleContext;
+
+typedef struct ReSampleContext ReSampleContext;
+
+/**
+ * Initialize audio resampling context.
+ *
+ * @param output_channels number of output channels
+ * @param input_channels number of input channels
+ * @param output_rate output sample rate
+ * @param input_rate input sample rate
+ * @param sample_fmt_out requested output sample format
+ * @param sample_fmt_in input sample format
+ * @param filter_length length of each FIR filter in the filterbank relative to the cutoff frequency
+ * @param log2_phase_count log2 of the number of entries in the polyphase filterbank
+ * @param linear if 1 then the used FIR filter will be linearly interpolated
+ between the 2 closest, if 0 the closest will be used
+ * @param cutoff cutoff frequency, 1.0 corresponds to half the output sampling rate
+ * @return allocated ReSampleContext, NULL if error occurred
+ */
+attribute_deprecated
+ReSampleContext *av_audio_resample_init(int output_channels, int input_channels,
+ int output_rate, int input_rate,
+ enum AVSampleFormat sample_fmt_out,
+ enum AVSampleFormat sample_fmt_in,
+ int filter_length, int log2_phase_count,
+ int linear, double cutoff);
+
+attribute_deprecated
+int audio_resample(ReSampleContext *s, short *output, short *input, int nb_samples);
+
+/**
+ * Free resample context.
+ *
+ * @param s a non-NULL pointer to a resample context previously
+ * created with av_audio_resample_init()
+ */
+attribute_deprecated
+void audio_resample_close(ReSampleContext *s);
+
+
+/**
+ * Initialize an audio resampler.
+ * Note, if either rate is not an integer then simply scale both rates up so they are.
+ * @param filter_length length of each FIR filter in the filterbank relative to the cutoff freq
+ * @param log2_phase_count log2 of the number of entries in the polyphase filterbank
+ * @param linear If 1 then the used FIR filter will be linearly interpolated
+ between the 2 closest, if 0 the closest will be used
+ * @param cutoff cutoff frequency, 1.0 corresponds to half the output sampling rate
+ */
+attribute_deprecated
+struct AVResampleContext *av_resample_init(int out_rate, int in_rate, int filter_length, int log2_phase_count, int linear, double cutoff);
+
+/**
+ * Resample an array of samples using a previously configured context.
+ * @param src an array of unconsumed samples
+ * @param consumed the number of samples of src which have been consumed are returned here
+ * @param src_size the number of unconsumed samples available
+ * @param dst_size the amount of space in samples available in dst
+ * @param update_ctx If this is 0 then the context will not be modified, that way several channels can be resampled with the same context.
+ * @return the number of samples written in dst or -1 if an error occurred
+ */
+attribute_deprecated
+int av_resample(struct AVResampleContext *c, short *dst, short *src, int *consumed, int src_size, int dst_size, int update_ctx);
+
+
+/**
+ * Compensate samplerate/timestamp drift. The compensation is done by changing
+ * the resampler parameters, so no audible clicks or similar distortions occur
+ * @param compensation_distance distance in output samples over which the compensation should be performed
+ * @param sample_delta number of output samples which should be output less
+ *
+ * example: av_resample_compensate(c, 10, 500)
+ * here instead of 510 samples only 500 samples would be output
+ *
+ * note, due to rounding the actual compensation might be slightly different,
+ * especially if the compensation_distance is large and the in_rate used during init is small
+ */
+attribute_deprecated
+void av_resample_compensate(struct AVResampleContext *c, int sample_delta, int compensation_distance);
+attribute_deprecated
+void av_resample_close(struct AVResampleContext *c);
+
+/**
+ * @}
+ */
+#endif
+
+/**
+ * @addtogroup lavc_picture
+ * @{
+ */
+
+/**
+ * Allocate memory for a picture. Call avpicture_free() to free it.
+ *
+ * @see avpicture_fill()
+ *
+ * @param picture the picture to be filled in
+ * @param pix_fmt the format of the picture
+ * @param width the width of the picture
+ * @param height the height of the picture
+ * @return zero if successful, a negative value if not
+ */
+int avpicture_alloc(AVPicture *picture, enum AVPixelFormat pix_fmt, int width, int height);
+
+/**
+ * Free a picture previously allocated by avpicture_alloc().
+ * The data buffer used by the AVPicture is freed, but the AVPicture structure
+ * itself is not.
+ *
+ * @param picture the AVPicture to be freed
+ */
+void avpicture_free(AVPicture *picture);
+
+/**
+ * Fill in the AVPicture fields.
+ * The fields of the given AVPicture are filled in by using the 'ptr' address
+ * which points to the image data buffer. Depending on the specified picture
+ * format, one or multiple image data pointers and line sizes will be set.
+ * If a planar format is specified, several pointers will be set pointing to
+ * the different picture planes and the line sizes of the different planes
+ * will be stored in the lines_sizes array.
+ * Call with ptr == NULL to get the required size for the ptr buffer.
+ *
+ * To allocate the buffer and fill in the AVPicture fields in one call,
+ * use avpicture_alloc().
+ *
+ * @param picture AVPicture whose fields are to be filled in
+ * @param ptr Buffer which will contain or contains the actual image data
+ * @param pix_fmt The format in which the picture data is stored.
+ * @param width the width of the image in pixels
+ * @param height the height of the image in pixels
+ * @return size of the image data in bytes
+ */
+int avpicture_fill(AVPicture *picture, uint8_t *ptr,
+ enum AVPixelFormat pix_fmt, int width, int height);
+
+/**
+ * Copy pixel data from an AVPicture into a buffer.
+ * The data is stored compactly, without any gaps for alignment or padding
+ * which may be applied by avpicture_fill().
+ *
+ * @see avpicture_get_size()
+ *
+ * @param[in] src AVPicture containing image data
+ * @param[in] pix_fmt The format in which the picture data is stored.
+ * @param[in] width the width of the image in pixels.
+ * @param[in] height the height of the image in pixels.
+ * @param[out] dest A buffer into which picture data will be copied.
+ * @param[in] dest_size The size of 'dest'.
+ * @return The number of bytes written to dest, or a negative value (error code) on error.
+ */
+int avpicture_layout(const AVPicture* src, enum AVPixelFormat pix_fmt,
+ int width, int height,
+ unsigned char *dest, int dest_size);
+
+/**
+ * Calculate the size in bytes that a picture of the given width and height
+ * would occupy if stored in the given picture format.
+ * Note that this returns the size of a compact representation as generated
+ * by avpicture_layout(), which can be smaller than the size required for e.g.
+ * avpicture_fill().
+ *
+ * @param pix_fmt the given picture format
+ * @param width the width of the image
+ * @param height the height of the image
+ * @return Image data size in bytes or -1 on error (e.g. too large dimensions).
+ */
+int avpicture_get_size(enum AVPixelFormat pix_fmt, int width, int height);
+
+/**
+ * deinterlace - if not supported return -1
+ */
+int avpicture_deinterlace(AVPicture *dst, const AVPicture *src,
+ enum AVPixelFormat pix_fmt, int width, int height);
+/**
+ * Copy image src to dst. Wraps av_picture_data_copy() above.
+ */
+void av_picture_copy(AVPicture *dst, const AVPicture *src,
+ enum AVPixelFormat pix_fmt, int width, int height);
+
+/**
+ * Crop image top and left side.
+ */
+int av_picture_crop(AVPicture *dst, const AVPicture *src,
+ enum AVPixelFormat pix_fmt, int top_band, int left_band);
+
+/**
+ * Pad image.
+ */
+int av_picture_pad(AVPicture *dst, const AVPicture *src, int height, int width, enum AVPixelFormat pix_fmt,
+ int padtop, int padbottom, int padleft, int padright, int *color);
+
+/**
+ * @}
+ */
+
+/**
+ * @defgroup lavc_misc Utility functions
+ * @ingroup libavc
+ *
+ * Miscellaneous utility functions related to both encoding and decoding
+ * (or neither).
+ * @{
+ */
+
+/**
+ * @defgroup lavc_misc_pixfmt Pixel formats
+ *
+ * Functions for working with pixel formats.
+ * @{
+ */
+
+/**
+ * @deprecated Use av_pix_fmt_get_chroma_sub_sample
+ */
+
+void attribute_deprecated avcodec_get_chroma_sub_sample(enum AVPixelFormat pix_fmt, int *h_shift, int *v_shift);
+
+/**
+ * Return a value representing the fourCC code associated to the
+ * pixel format pix_fmt, or 0 if no associated fourCC code can be
+ * found.
+ */
+unsigned int avcodec_pix_fmt_to_codec_tag(enum AVPixelFormat pix_fmt);
+
+#define FF_LOSS_RESOLUTION 0x0001 /**< loss due to resolution change */
+#define FF_LOSS_DEPTH 0x0002 /**< loss due to color depth change */
+#define FF_LOSS_COLORSPACE 0x0004 /**< loss due to color space conversion */
+#define FF_LOSS_ALPHA 0x0008 /**< loss of alpha bits */
+#define FF_LOSS_COLORQUANT 0x0010 /**< loss due to color quantization */
+#define FF_LOSS_CHROMA 0x0020 /**< loss of chroma (e.g. RGB to gray conversion) */
+
+/**
+ * Compute what kind of losses will occur when converting from one specific
+ * pixel format to another.
+ * When converting from one pixel format to another, information loss may occur.
+ * For example, when converting from RGB24 to GRAY, the color information will
+ * be lost. Similarly, other losses occur when converting from some formats to
+ * other formats. These losses can involve loss of chroma, but also loss of
+ * resolution, loss of color depth, loss due to the color space conversion, loss
+ * of the alpha bits or loss due to color quantization.
+ * avcodec_get_fix_fmt_loss() informs you about the various types of losses
+ * which will occur when converting from one pixel format to another.
+ *
+ * @param[in] dst_pix_fmt destination pixel format
+ * @param[in] src_pix_fmt source pixel format
+ * @param[in] has_alpha Whether the source pixel format alpha channel is used.
+ * @return Combination of flags informing you what kind of losses will occur.
+ */
+int avcodec_get_pix_fmt_loss(enum AVPixelFormat dst_pix_fmt, enum AVPixelFormat src_pix_fmt,
+ int has_alpha);
+
+#if FF_API_FIND_BEST_PIX_FMT
+/**
+ * @deprecated use avcodec_find_best_pix_fmt2() instead.
+ *
+ * Find the best pixel format to convert to given a certain source pixel
+ * format. When converting from one pixel format to another, information loss
+ * may occur. For example, when converting from RGB24 to GRAY, the color
+ * information will be lost. Similarly, other losses occur when converting from
+ * some formats to other formats. avcodec_find_best_pix_fmt() searches which of
+ * the given pixel formats should be used to suffer the least amount of loss.
+ * The pixel formats from which it chooses one, are determined by the
+ * pix_fmt_mask parameter.
+ *
+ * @code
+ * src_pix_fmt = AV_PIX_FMT_YUV420P;
+ * pix_fmt_mask = (1 << AV_PIX_FMT_YUV422P) || (1 << AV_PIX_FMT_RGB24);
+ * dst_pix_fmt = avcodec_find_best_pix_fmt(pix_fmt_mask, src_pix_fmt, alpha, &loss);
+ * @endcode
+ *
+ * @param[in] pix_fmt_mask bitmask determining which pixel format to choose from
+ * @param[in] src_pix_fmt source pixel format
+ * @param[in] has_alpha Whether the source pixel format alpha channel is used.
+ * @param[out] loss_ptr Combination of flags informing you what kind of losses will occur.
+ * @return The best pixel format to convert to or -1 if none was found.
+ */
+attribute_deprecated
+enum AVPixelFormat avcodec_find_best_pix_fmt(int64_t pix_fmt_mask, enum AVPixelFormat src_pix_fmt,
+ int has_alpha, int *loss_ptr);
+#endif /* FF_API_FIND_BEST_PIX_FMT */
+
+/**
+ * Find the best pixel format to convert to given a certain source pixel
+ * format. When converting from one pixel format to another, information loss
+ * may occur. For example, when converting from RGB24 to GRAY, the color
+ * information will be lost. Similarly, other losses occur when converting from
+ * some formats to other formats. avcodec_find_best_pix_fmt2() searches which of
+ * the given pixel formats should be used to suffer the least amount of loss.
+ * The pixel formats from which it chooses one, are determined by the
+ * pix_fmt_list parameter.
+ *
+ *
+ * @param[in] pix_fmt_list AV_PIX_FMT_NONE terminated array of pixel formats to choose from
+ * @param[in] src_pix_fmt source pixel format
+ * @param[in] has_alpha Whether the source pixel format alpha channel is used.
+ * @param[out] loss_ptr Combination of flags informing you what kind of losses will occur.
+ * @return The best pixel format to convert to or -1 if none was found.
+ */
+enum AVPixelFormat avcodec_find_best_pix_fmt2(enum AVPixelFormat *pix_fmt_list,
+ enum AVPixelFormat src_pix_fmt,
+ int has_alpha, int *loss_ptr);
+
+enum AVPixelFormat avcodec_default_get_format(struct AVCodecContext *s, const enum AVPixelFormat * fmt);
+
+/**
+ * @}
+ */
+
+void avcodec_set_dimensions(AVCodecContext *s, int width, int height);
+
+/**
+ * Put a string representing the codec tag codec_tag in buf.
+ *
+ * @param buf_size size in bytes of buf
+ * @return the length of the string that would have been generated if
+ * enough space had been available, excluding the trailing null
+ */
+size_t av_get_codec_tag_string(char *buf, size_t buf_size, unsigned int codec_tag);
+
+void avcodec_string(char *buf, int buf_size, AVCodecContext *enc, int encode);
+
+/**
+ * Return a name for the specified profile, if available.
+ *
+ * @param codec the codec that is searched for the given profile
+ * @param profile the profile value for which a name is requested
+ * @return A name for the profile if found, NULL otherwise.
+ */
+const char *av_get_profile_name(const AVCodec *codec, int profile);
+
+int avcodec_default_execute(AVCodecContext *c, int (*func)(AVCodecContext *c2, void *arg2),void *arg, int *ret, int count, int size);
+int avcodec_default_execute2(AVCodecContext *c, int (*func)(AVCodecContext *c2, void *arg2, int, int),void *arg, int *ret, int count);
+//FIXME func typedef
+
+/**
+ * Fill audio frame data and linesize.
+ * AVFrame extended_data channel pointers are allocated if necessary for
+ * planar audio.
+ *
+ * @param frame the AVFrame
+ * frame->nb_samples must be set prior to calling the
+ * function. This function fills in frame->data,
+ * frame->extended_data, frame->linesize[0].
+ * @param nb_channels channel count
+ * @param sample_fmt sample format
+ * @param buf buffer to use for frame data
+ * @param buf_size size of buffer
+ * @param align plane size sample alignment (0 = default)
+ * @return 0 on success, negative error code on failure
+ */
+int avcodec_fill_audio_frame(AVFrame *frame, int nb_channels,
+ enum AVSampleFormat sample_fmt, const uint8_t *buf,
+ int buf_size, int align);
+
+/**
+ * Flush buffers, should be called when seeking or when switching to a different stream.
+ */
+void avcodec_flush_buffers(AVCodecContext *avctx);
+
+void avcodec_default_free_buffers(AVCodecContext *s);
+
+/**
+ * Return codec bits per sample.
+ *
+ * @param[in] codec_id the codec
+ * @return Number of bits per sample or zero if unknown for the given codec.
+ */
+int av_get_bits_per_sample(enum AVCodecID codec_id);
+
+/**
+ * Return codec bits per sample.
+ * Only return non-zero if the bits per sample is exactly correct, not an
+ * approximation.
+ *
+ * @param[in] codec_id the codec
+ * @return Number of bits per sample or zero if unknown for the given codec.
+ */
+int av_get_exact_bits_per_sample(enum AVCodecID codec_id);
+
+/**
+ * Return audio frame duration.
+ *
+ * @param avctx codec context
+ * @param frame_bytes size of the frame, or 0 if unknown
+ * @return frame duration, in samples, if known. 0 if not able to
+ * determine.
+ */
+int av_get_audio_frame_duration(AVCodecContext *avctx, int frame_bytes);
+
+
+typedef struct AVBitStreamFilterContext {
+ void *priv_data;
+ struct AVBitStreamFilter *filter;
+ AVCodecParserContext *parser;
+ struct AVBitStreamFilterContext *next;
+} AVBitStreamFilterContext;
+
+
+typedef struct AVBitStreamFilter {
+ const char *name;
+ int priv_data_size;
+ int (*filter)(AVBitStreamFilterContext *bsfc,
+ AVCodecContext *avctx, const char *args,
+ uint8_t **poutbuf, int *poutbuf_size,
+ const uint8_t *buf, int buf_size, int keyframe);
+ void (*close)(AVBitStreamFilterContext *bsfc);
+ struct AVBitStreamFilter *next;
+} AVBitStreamFilter;
+
+void av_register_bitstream_filter(AVBitStreamFilter *bsf);
+AVBitStreamFilterContext *av_bitstream_filter_init(const char *name);
+int av_bitstream_filter_filter(AVBitStreamFilterContext *bsfc,
+ AVCodecContext *avctx, const char *args,
+ uint8_t **poutbuf, int *poutbuf_size,
+ const uint8_t *buf, int buf_size, int keyframe);
+void av_bitstream_filter_close(AVBitStreamFilterContext *bsf);
+
+AVBitStreamFilter *av_bitstream_filter_next(AVBitStreamFilter *f);
+
+/* memory */
+
+/**
+ * Reallocate the given block if it is not large enough, otherwise do nothing.
+ *
+ * @see av_realloc
+ */
+void *av_fast_realloc(void *ptr, unsigned int *size, size_t min_size);
+
+/**
+ * Allocate a buffer, reusing the given one if large enough.
+ *
+ * Contrary to av_fast_realloc the current buffer contents might not be
+ * preserved and on error the old buffer is freed, thus no special
+ * handling to avoid memleaks is necessary.
+ *
+ * @param ptr pointer to pointer to already allocated buffer, overwritten with pointer to new buffer
+ * @param size size of the buffer *ptr points to
+ * @param min_size minimum size of *ptr buffer after returning, *ptr will be NULL and
+ * *size 0 if an error occurred.
+ */
+void av_fast_malloc(void *ptr, unsigned int *size, size_t min_size);
+
+/**
+ * Allocate a buffer with padding, reusing the given one if large enough.
+ *
+ * Same behaviour av_fast_malloc but the buffer has additional
+ * FF_INPUT_PADDING_SIZE at the end which will always memset to 0.
+ *
+ */
+void av_fast_padded_malloc(void *ptr, unsigned int *size, size_t min_size);
+
+/**
+ * Encode extradata length to a buffer. Used by xiph codecs.
+ *
+ * @param s buffer to write to; must be at least (v/255+1) bytes long
+ * @param v size of extradata in bytes
+ * @return number of bytes written to the buffer.
+ */
+unsigned int av_xiphlacing(unsigned char *s, unsigned int v);
+
+/**
+ * Log a generic warning message about a missing feature. This function is
+ * intended to be used internally by Libav (libavcodec, libavformat, etc.)
+ * only, and would normally not be used by applications.
+ * @param[in] avc a pointer to an arbitrary struct of which the first field is
+ * a pointer to an AVClass struct
+ * @param[in] feature string containing the name of the missing feature
+ * @param[in] want_sample indicates if samples are wanted which exhibit this feature.
+ * If want_sample is non-zero, additional verbage will be added to the log
+ * message which tells the user how to report samples to the development
+ * mailing list.
+ */
+void av_log_missing_feature(void *avc, const char *feature, int want_sample);
+
+/**
+ * Log a generic warning message asking for a sample. This function is
+ * intended to be used internally by Libav (libavcodec, libavformat, etc.)
+ * only, and would normally not be used by applications.
+ * @param[in] avc a pointer to an arbitrary struct of which the first field is
+ * a pointer to an AVClass struct
+ * @param[in] msg string containing an optional message, or NULL if no message
+ */
+void av_log_ask_for_sample(void *avc, const char *msg, ...) av_printf_format(2, 3);
+
+/**
+ * Register the hardware accelerator hwaccel.
+ */
+void av_register_hwaccel(AVHWAccel *hwaccel);
+
+/**
+ * If hwaccel is NULL, returns the first registered hardware accelerator,
+ * if hwaccel is non-NULL, returns the next registered hardware accelerator
+ * after hwaccel, or NULL if hwaccel is the last one.
+ */
+AVHWAccel *av_hwaccel_next(AVHWAccel *hwaccel);
+
+
+/**
+ * Lock operation used by lockmgr
+ */
+enum AVLockOp {
+ AV_LOCK_CREATE, ///< Create a mutex
+ AV_LOCK_OBTAIN, ///< Lock the mutex
+ AV_LOCK_RELEASE, ///< Unlock the mutex
+ AV_LOCK_DESTROY, ///< Free mutex resources
+};
+
+/**
+ * Register a user provided lock manager supporting the operations
+ * specified by AVLockOp. mutex points to a (void *) where the
+ * lockmgr should store/get a pointer to a user allocated mutex. It's
+ * NULL upon AV_LOCK_CREATE and != NULL for all other ops.
+ *
+ * @param cb User defined callback. Note: Libav may invoke calls to this
+ * callback during the call to av_lockmgr_register().
+ * Thus, the application must be prepared to handle that.
+ * If cb is set to NULL the lockmgr will be unregistered.
+ * Also note that during unregistration the previously registered
+ * lockmgr callback may also be invoked.
+ */
+int av_lockmgr_register(int (*cb)(void **mutex, enum AVLockOp op));
+
+/**
+ * Get the type of the given codec.
+ */
+enum AVMediaType avcodec_get_type(enum AVCodecID codec_id);
+
+/**
+ * @return a positive value if s is open (i.e. avcodec_open2() was called on it
+ * with no corresponding avcodec_close()), 0 otherwise.
+ */
+int avcodec_is_open(AVCodecContext *s);
+
+/**
+ * @return a non-zero number if codec is an encoder, zero otherwise
+ */
+int av_codec_is_encoder(const AVCodec *codec);
+
+/**
+ * @return a non-zero number if codec is a decoder, zero otherwise
+ */
+int av_codec_is_decoder(const AVCodec *codec);
+
+/**
+ * @return descriptor for given codec ID or NULL if no descriptor exists.
+ */
+const AVCodecDescriptor *avcodec_descriptor_get(enum AVCodecID id);
+
+/**
+ * Iterate over all codec descriptors known to libavcodec.
+ *
+ * @param prev previous descriptor. NULL to get the first descriptor.
+ *
+ * @return next descriptor or NULL after the last descriptor
+ */
+const AVCodecDescriptor *avcodec_descriptor_next(const AVCodecDescriptor *prev);
+
+/**
+ * @return codec descriptor with the given name or NULL if no such descriptor
+ * exists.
+ */
+const AVCodecDescriptor *avcodec_descriptor_get_by_name(const char *name);
+
+/**
+ * @}
+ */
+
+#endif /* AVCODEC_AVCODEC_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavcodec/avfft.h b/dom/media/platforms/ffmpeg/libav54/include/libavcodec/avfft.h
new file mode 100644
index 0000000000..b89618258e
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavcodec/avfft.h
@@ -0,0 +1,116 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_AVFFT_H
+#define AVCODEC_AVFFT_H
+
+/**
+ * @file
+ * @ingroup lavc_fft
+ * FFT functions
+ */
+
+/**
+ * @defgroup lavc_fft FFT functions
+ * @ingroup lavc_misc
+ *
+ * @{
+ */
+
+typedef float FFTSample;
+
+typedef struct FFTComplex {
+ FFTSample re, im;
+} FFTComplex;
+
+typedef struct FFTContext FFTContext;
+
+/**
+ * Set up a complex FFT.
+ * @param nbits log2 of the length of the input array
+ * @param inverse if 0 perform the forward transform, if 1 perform the inverse
+ */
+FFTContext *av_fft_init(int nbits, int inverse);
+
+/**
+ * Do the permutation needed BEFORE calling ff_fft_calc().
+ */
+void av_fft_permute(FFTContext *s, FFTComplex *z);
+
+/**
+ * Do a complex FFT with the parameters defined in av_fft_init(). The
+ * input data must be permuted before. No 1.0/sqrt(n) normalization is done.
+ */
+void av_fft_calc(FFTContext *s, FFTComplex *z);
+
+void av_fft_end(FFTContext *s);
+
+FFTContext *av_mdct_init(int nbits, int inverse, double scale);
+void av_imdct_calc(FFTContext *s, FFTSample *output, const FFTSample *input);
+void av_imdct_half(FFTContext *s, FFTSample *output, const FFTSample *input);
+void av_mdct_calc(FFTContext *s, FFTSample *output, const FFTSample *input);
+void av_mdct_end(FFTContext *s);
+
+/* Real Discrete Fourier Transform */
+
+enum RDFTransformType {
+ DFT_R2C,
+ IDFT_C2R,
+ IDFT_R2C,
+ DFT_C2R,
+};
+
+typedef struct RDFTContext RDFTContext;
+
+/**
+ * Set up a real FFT.
+ * @param nbits log2 of the length of the input array
+ * @param trans the type of transform
+ */
+RDFTContext *av_rdft_init(int nbits, enum RDFTransformType trans);
+void av_rdft_calc(RDFTContext *s, FFTSample *data);
+void av_rdft_end(RDFTContext *s);
+
+/* Discrete Cosine Transform */
+
+typedef struct DCTContext DCTContext;
+
+enum DCTTransformType {
+ DCT_II = 0,
+ DCT_III,
+ DCT_I,
+ DST_I,
+};
+
+/**
+ * Set up DCT.
+ * @param nbits size of the input array:
+ * (1 << nbits) for DCT-II, DCT-III and DST-I
+ * (1 << nbits) + 1 for DCT-I
+ *
+ * @note the first element of the input of DST-I is ignored
+ */
+DCTContext *av_dct_init(int nbits, enum DCTTransformType type);
+void av_dct_calc(DCTContext *s, FFTSample *data);
+void av_dct_end (DCTContext *s);
+
+/**
+ * @}
+ */
+
+#endif /* AVCODEC_AVFFT_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavcodec/dxva2.h b/dom/media/platforms/ffmpeg/libav54/include/libavcodec/dxva2.h
new file mode 100644
index 0000000000..c06f1f3332
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavcodec/dxva2.h
@@ -0,0 +1,88 @@
+/*
+ * DXVA2 HW acceleration
+ *
+ * copyright (c) 2009 Laurent Aimar
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_DXVA_H
+#define AVCODEC_DXVA_H
+
+/**
+ * @file
+ * @ingroup lavc_codec_hwaccel_dxva2
+ * Public libavcodec DXVA2 header.
+ */
+
+#include <stdint.h>
+
+#include <d3d9.h>
+#include <dxva2api.h>
+
+/**
+ * @defgroup lavc_codec_hwaccel_dxva2 DXVA2
+ * @ingroup lavc_codec_hwaccel
+ *
+ * @{
+ */
+
+#define FF_DXVA2_WORKAROUND_SCALING_LIST_ZIGZAG 1 ///< Work around for DXVA2 and old UVD/UVD+ ATI video cards
+
+/**
+ * This structure is used to provides the necessary configurations and data
+ * to the DXVA2 Libav HWAccel implementation.
+ *
+ * The application must make it available as AVCodecContext.hwaccel_context.
+ */
+struct dxva_context {
+ /**
+ * DXVA2 decoder object
+ */
+ IDirectXVideoDecoder *decoder;
+
+ /**
+ * DXVA2 configuration used to create the decoder
+ */
+ const DXVA2_ConfigPictureDecode *cfg;
+
+ /**
+ * The number of surface in the surface array
+ */
+ unsigned surface_count;
+
+ /**
+ * The array of Direct3D surfaces used to create the decoder
+ */
+ LPDIRECT3DSURFACE9 *surface;
+
+ /**
+ * A bit field configuring the workarounds needed for using the decoder
+ */
+ uint64_t workaround;
+
+ /**
+ * Private to the Libav AVHWAccel implementation
+ */
+ unsigned report_id;
+};
+
+/**
+ * @}
+ */
+
+#endif /* AVCODEC_DXVA_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavcodec/old_codec_ids.h b/dom/media/platforms/ffmpeg/libav54/include/libavcodec/old_codec_ids.h
new file mode 100644
index 0000000000..2b72e38d20
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavcodec/old_codec_ids.h
@@ -0,0 +1,366 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_OLD_CODEC_IDS_H
+#define AVCODEC_OLD_CODEC_IDS_H
+
+/*
+ * This header exists to prevent new codec IDs from being accidentally added to
+ * the deprecated list.
+ * Do not include it directly. It will be removed on next major bump
+ *
+ * Do not add new items to this list. Use the AVCodecID enum instead.
+ */
+
+ CODEC_ID_NONE = AV_CODEC_ID_NONE,
+
+ /* video codecs */
+ CODEC_ID_MPEG1VIDEO,
+ CODEC_ID_MPEG2VIDEO, ///< preferred ID for MPEG-1/2 video decoding
+ CODEC_ID_MPEG2VIDEO_XVMC,
+ CODEC_ID_H261,
+ CODEC_ID_H263,
+ CODEC_ID_RV10,
+ CODEC_ID_RV20,
+ CODEC_ID_MJPEG,
+ CODEC_ID_MJPEGB,
+ CODEC_ID_LJPEG,
+ CODEC_ID_SP5X,
+ CODEC_ID_JPEGLS,
+ CODEC_ID_MPEG4,
+ CODEC_ID_RAWVIDEO,
+ CODEC_ID_MSMPEG4V1,
+ CODEC_ID_MSMPEG4V2,
+ CODEC_ID_MSMPEG4V3,
+ CODEC_ID_WMV1,
+ CODEC_ID_WMV2,
+ CODEC_ID_H263P,
+ CODEC_ID_H263I,
+ CODEC_ID_FLV1,
+ CODEC_ID_SVQ1,
+ CODEC_ID_SVQ3,
+ CODEC_ID_DVVIDEO,
+ CODEC_ID_HUFFYUV,
+ CODEC_ID_CYUV,
+ CODEC_ID_H264,
+ CODEC_ID_INDEO3,
+ CODEC_ID_VP3,
+ CODEC_ID_THEORA,
+ CODEC_ID_ASV1,
+ CODEC_ID_ASV2,
+ CODEC_ID_FFV1,
+ CODEC_ID_4XM,
+ CODEC_ID_VCR1,
+ CODEC_ID_CLJR,
+ CODEC_ID_MDEC,
+ CODEC_ID_ROQ,
+ CODEC_ID_INTERPLAY_VIDEO,
+ CODEC_ID_XAN_WC3,
+ CODEC_ID_XAN_WC4,
+ CODEC_ID_RPZA,
+ CODEC_ID_CINEPAK,
+ CODEC_ID_WS_VQA,
+ CODEC_ID_MSRLE,
+ CODEC_ID_MSVIDEO1,
+ CODEC_ID_IDCIN,
+ CODEC_ID_8BPS,
+ CODEC_ID_SMC,
+ CODEC_ID_FLIC,
+ CODEC_ID_TRUEMOTION1,
+ CODEC_ID_VMDVIDEO,
+ CODEC_ID_MSZH,
+ CODEC_ID_ZLIB,
+ CODEC_ID_QTRLE,
+ CODEC_ID_SNOW,
+ CODEC_ID_TSCC,
+ CODEC_ID_ULTI,
+ CODEC_ID_QDRAW,
+ CODEC_ID_VIXL,
+ CODEC_ID_QPEG,
+ CODEC_ID_PNG,
+ CODEC_ID_PPM,
+ CODEC_ID_PBM,
+ CODEC_ID_PGM,
+ CODEC_ID_PGMYUV,
+ CODEC_ID_PAM,
+ CODEC_ID_FFVHUFF,
+ CODEC_ID_RV30,
+ CODEC_ID_RV40,
+ CODEC_ID_VC1,
+ CODEC_ID_WMV3,
+ CODEC_ID_LOCO,
+ CODEC_ID_WNV1,
+ CODEC_ID_AASC,
+ CODEC_ID_INDEO2,
+ CODEC_ID_FRAPS,
+ CODEC_ID_TRUEMOTION2,
+ CODEC_ID_BMP,
+ CODEC_ID_CSCD,
+ CODEC_ID_MMVIDEO,
+ CODEC_ID_ZMBV,
+ CODEC_ID_AVS,
+ CODEC_ID_SMACKVIDEO,
+ CODEC_ID_NUV,
+ CODEC_ID_KMVC,
+ CODEC_ID_FLASHSV,
+ CODEC_ID_CAVS,
+ CODEC_ID_JPEG2000,
+ CODEC_ID_VMNC,
+ CODEC_ID_VP5,
+ CODEC_ID_VP6,
+ CODEC_ID_VP6F,
+ CODEC_ID_TARGA,
+ CODEC_ID_DSICINVIDEO,
+ CODEC_ID_TIERTEXSEQVIDEO,
+ CODEC_ID_TIFF,
+ CODEC_ID_GIF,
+ CODEC_ID_DXA,
+ CODEC_ID_DNXHD,
+ CODEC_ID_THP,
+ CODEC_ID_SGI,
+ CODEC_ID_C93,
+ CODEC_ID_BETHSOFTVID,
+ CODEC_ID_PTX,
+ CODEC_ID_TXD,
+ CODEC_ID_VP6A,
+ CODEC_ID_AMV,
+ CODEC_ID_VB,
+ CODEC_ID_PCX,
+ CODEC_ID_SUNRAST,
+ CODEC_ID_INDEO4,
+ CODEC_ID_INDEO5,
+ CODEC_ID_MIMIC,
+ CODEC_ID_RL2,
+ CODEC_ID_ESCAPE124,
+ CODEC_ID_DIRAC,
+ CODEC_ID_BFI,
+ CODEC_ID_CMV,
+ CODEC_ID_MOTIONPIXELS,
+ CODEC_ID_TGV,
+ CODEC_ID_TGQ,
+ CODEC_ID_TQI,
+ CODEC_ID_AURA,
+ CODEC_ID_AURA2,
+ CODEC_ID_V210X,
+ CODEC_ID_TMV,
+ CODEC_ID_V210,
+ CODEC_ID_DPX,
+ CODEC_ID_MAD,
+ CODEC_ID_FRWU,
+ CODEC_ID_FLASHSV2,
+ CODEC_ID_CDGRAPHICS,
+ CODEC_ID_R210,
+ CODEC_ID_ANM,
+ CODEC_ID_BINKVIDEO,
+ CODEC_ID_IFF_ILBM,
+ CODEC_ID_IFF_BYTERUN1,
+ CODEC_ID_KGV1,
+ CODEC_ID_YOP,
+ CODEC_ID_VP8,
+ CODEC_ID_PICTOR,
+ CODEC_ID_ANSI,
+ CODEC_ID_A64_MULTI,
+ CODEC_ID_A64_MULTI5,
+ CODEC_ID_R10K,
+ CODEC_ID_MXPEG,
+ CODEC_ID_LAGARITH,
+ CODEC_ID_PRORES,
+ CODEC_ID_JV,
+ CODEC_ID_DFA,
+ CODEC_ID_WMV3IMAGE,
+ CODEC_ID_VC1IMAGE,
+ CODEC_ID_UTVIDEO,
+ CODEC_ID_BMV_VIDEO,
+ CODEC_ID_VBLE,
+ CODEC_ID_DXTORY,
+ CODEC_ID_V410,
+ CODEC_ID_XWD,
+ CODEC_ID_CDXL,
+ CODEC_ID_XBM,
+ CODEC_ID_ZEROCODEC,
+ CODEC_ID_MSS1,
+ CODEC_ID_MSA1,
+ CODEC_ID_TSCC2,
+ CODEC_ID_MTS2,
+ CODEC_ID_CLLC,
+
+ /* various PCM "codecs" */
+ CODEC_ID_FIRST_AUDIO = 0x10000, ///< A dummy id pointing at the start of audio codecs
+ CODEC_ID_PCM_S16LE = 0x10000,
+ CODEC_ID_PCM_S16BE,
+ CODEC_ID_PCM_U16LE,
+ CODEC_ID_PCM_U16BE,
+ CODEC_ID_PCM_S8,
+ CODEC_ID_PCM_U8,
+ CODEC_ID_PCM_MULAW,
+ CODEC_ID_PCM_ALAW,
+ CODEC_ID_PCM_S32LE,
+ CODEC_ID_PCM_S32BE,
+ CODEC_ID_PCM_U32LE,
+ CODEC_ID_PCM_U32BE,
+ CODEC_ID_PCM_S24LE,
+ CODEC_ID_PCM_S24BE,
+ CODEC_ID_PCM_U24LE,
+ CODEC_ID_PCM_U24BE,
+ CODEC_ID_PCM_S24DAUD,
+ CODEC_ID_PCM_ZORK,
+ CODEC_ID_PCM_S16LE_PLANAR,
+ CODEC_ID_PCM_DVD,
+ CODEC_ID_PCM_F32BE,
+ CODEC_ID_PCM_F32LE,
+ CODEC_ID_PCM_F64BE,
+ CODEC_ID_PCM_F64LE,
+ CODEC_ID_PCM_BLURAY,
+ CODEC_ID_PCM_LXF,
+ CODEC_ID_S302M,
+ CODEC_ID_PCM_S8_PLANAR,
+
+ /* various ADPCM codecs */
+ CODEC_ID_ADPCM_IMA_QT = 0x11000,
+ CODEC_ID_ADPCM_IMA_WAV,
+ CODEC_ID_ADPCM_IMA_DK3,
+ CODEC_ID_ADPCM_IMA_DK4,
+ CODEC_ID_ADPCM_IMA_WS,
+ CODEC_ID_ADPCM_IMA_SMJPEG,
+ CODEC_ID_ADPCM_MS,
+ CODEC_ID_ADPCM_4XM,
+ CODEC_ID_ADPCM_XA,
+ CODEC_ID_ADPCM_ADX,
+ CODEC_ID_ADPCM_EA,
+ CODEC_ID_ADPCM_G726,
+ CODEC_ID_ADPCM_CT,
+ CODEC_ID_ADPCM_SWF,
+ CODEC_ID_ADPCM_YAMAHA,
+ CODEC_ID_ADPCM_SBPRO_4,
+ CODEC_ID_ADPCM_SBPRO_3,
+ CODEC_ID_ADPCM_SBPRO_2,
+ CODEC_ID_ADPCM_THP,
+ CODEC_ID_ADPCM_IMA_AMV,
+ CODEC_ID_ADPCM_EA_R1,
+ CODEC_ID_ADPCM_EA_R3,
+ CODEC_ID_ADPCM_EA_R2,
+ CODEC_ID_ADPCM_IMA_EA_SEAD,
+ CODEC_ID_ADPCM_IMA_EA_EACS,
+ CODEC_ID_ADPCM_EA_XAS,
+ CODEC_ID_ADPCM_EA_MAXIS_XA,
+ CODEC_ID_ADPCM_IMA_ISS,
+ CODEC_ID_ADPCM_G722,
+ CODEC_ID_ADPCM_IMA_APC,
+
+ /* AMR */
+ CODEC_ID_AMR_NB = 0x12000,
+ CODEC_ID_AMR_WB,
+
+ /* RealAudio codecs*/
+ CODEC_ID_RA_144 = 0x13000,
+ CODEC_ID_RA_288,
+
+ /* various DPCM codecs */
+ CODEC_ID_ROQ_DPCM = 0x14000,
+ CODEC_ID_INTERPLAY_DPCM,
+ CODEC_ID_XAN_DPCM,
+ CODEC_ID_SOL_DPCM,
+
+ /* audio codecs */
+ CODEC_ID_MP2 = 0x15000,
+ CODEC_ID_MP3, ///< preferred ID for decoding MPEG audio layer 1, 2 or 3
+ CODEC_ID_AAC,
+ CODEC_ID_AC3,
+ CODEC_ID_DTS,
+ CODEC_ID_VORBIS,
+ CODEC_ID_DVAUDIO,
+ CODEC_ID_WMAV1,
+ CODEC_ID_WMAV2,
+ CODEC_ID_MACE3,
+ CODEC_ID_MACE6,
+ CODEC_ID_VMDAUDIO,
+ CODEC_ID_FLAC,
+ CODEC_ID_MP3ADU,
+ CODEC_ID_MP3ON4,
+ CODEC_ID_SHORTEN,
+ CODEC_ID_ALAC,
+ CODEC_ID_WESTWOOD_SND1,
+ CODEC_ID_GSM, ///< as in Berlin toast format
+ CODEC_ID_QDM2,
+ CODEC_ID_COOK,
+ CODEC_ID_TRUESPEECH,
+ CODEC_ID_TTA,
+ CODEC_ID_SMACKAUDIO,
+ CODEC_ID_QCELP,
+ CODEC_ID_WAVPACK,
+ CODEC_ID_DSICINAUDIO,
+ CODEC_ID_IMC,
+ CODEC_ID_MUSEPACK7,
+ CODEC_ID_MLP,
+ CODEC_ID_GSM_MS, /* as found in WAV */
+ CODEC_ID_ATRAC3,
+ CODEC_ID_VOXWARE,
+ CODEC_ID_APE,
+ CODEC_ID_NELLYMOSER,
+ CODEC_ID_MUSEPACK8,
+ CODEC_ID_SPEEX,
+ CODEC_ID_WMAVOICE,
+ CODEC_ID_WMAPRO,
+ CODEC_ID_WMALOSSLESS,
+ CODEC_ID_ATRAC3P,
+ CODEC_ID_EAC3,
+ CODEC_ID_SIPR,
+ CODEC_ID_MP1,
+ CODEC_ID_TWINVQ,
+ CODEC_ID_TRUEHD,
+ CODEC_ID_MP4ALS,
+ CODEC_ID_ATRAC1,
+ CODEC_ID_BINKAUDIO_RDFT,
+ CODEC_ID_BINKAUDIO_DCT,
+ CODEC_ID_AAC_LATM,
+ CODEC_ID_QDMC,
+ CODEC_ID_CELT,
+ CODEC_ID_G723_1,
+ CODEC_ID_G729,
+ CODEC_ID_8SVX_EXP,
+ CODEC_ID_8SVX_FIB,
+ CODEC_ID_BMV_AUDIO,
+ CODEC_ID_RALF,
+ CODEC_ID_IAC,
+ CODEC_ID_ILBC,
+
+ /* subtitle codecs */
+ CODEC_ID_FIRST_SUBTITLE = 0x17000, ///< A dummy ID pointing at the start of subtitle codecs.
+ CODEC_ID_DVD_SUBTITLE = 0x17000,
+ CODEC_ID_DVB_SUBTITLE,
+ CODEC_ID_TEXT, ///< raw UTF-8 text
+ CODEC_ID_XSUB,
+ CODEC_ID_SSA,
+ CODEC_ID_MOV_TEXT,
+ CODEC_ID_HDMV_PGS_SUBTITLE,
+ CODEC_ID_DVB_TELETEXT,
+ CODEC_ID_SRT,
+
+ /* other specific kind of codecs (generally used for attachments) */
+ CODEC_ID_FIRST_UNKNOWN = 0x18000, ///< A dummy ID pointing at the start of various fake codecs.
+ CODEC_ID_TTF = 0x18000,
+
+ CODEC_ID_PROBE = 0x19000, ///< codec_id is not known (like CODEC_ID_NONE) but lavf should attempt to identify it
+
+ CODEC_ID_MPEG2TS = 0x20000, /**< _FAKE_ codec to indicate a raw MPEG-2 TS
+ * stream (only used by libavformat) */
+ CODEC_ID_MPEG4SYSTEMS = 0x20001, /**< _FAKE_ codec to indicate a MPEG-4 Systems
+ * stream (only used by libavformat) */
+ CODEC_ID_FFMETADATA = 0x21000, ///< Dummy codec for streams containing only metadata information.
+
+#endif /* AVCODEC_OLD_CODEC_IDS_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavcodec/vaapi.h b/dom/media/platforms/ffmpeg/libav54/include/libavcodec/vaapi.h
new file mode 100644
index 0000000000..39e88259d6
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavcodec/vaapi.h
@@ -0,0 +1,173 @@
+/*
+ * Video Acceleration API (shared data between Libav and the video player)
+ * HW decode acceleration for MPEG-2, MPEG-4, H.264 and VC-1
+ *
+ * Copyright (C) 2008-2009 Splitted-Desktop Systems
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_VAAPI_H
+#define AVCODEC_VAAPI_H
+
+/**
+ * @file
+ * @ingroup lavc_codec_hwaccel_vaapi
+ * Public libavcodec VA API header.
+ */
+
+#include <stdint.h>
+
+/**
+ * @defgroup lavc_codec_hwaccel_vaapi VA API Decoding
+ * @ingroup lavc_codec_hwaccel
+ * @{
+ */
+
+/**
+ * This structure is used to share data between the Libav library and
+ * the client video application.
+ * This shall be zero-allocated and available as
+ * AVCodecContext.hwaccel_context. All user members can be set once
+ * during initialization or through each AVCodecContext.get_buffer()
+ * function call. In any case, they must be valid prior to calling
+ * decoding functions.
+ */
+struct vaapi_context {
+ /**
+ * Window system dependent data
+ *
+ * - encoding: unused
+ * - decoding: Set by user
+ */
+ void *display;
+
+ /**
+ * Configuration ID
+ *
+ * - encoding: unused
+ * - decoding: Set by user
+ */
+ uint32_t config_id;
+
+ /**
+ * Context ID (video decode pipeline)
+ *
+ * - encoding: unused
+ * - decoding: Set by user
+ */
+ uint32_t context_id;
+
+ /**
+ * VAPictureParameterBuffer ID
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ uint32_t pic_param_buf_id;
+
+ /**
+ * VAIQMatrixBuffer ID
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ uint32_t iq_matrix_buf_id;
+
+ /**
+ * VABitPlaneBuffer ID (for VC-1 decoding)
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ uint32_t bitplane_buf_id;
+
+ /**
+ * Slice parameter/data buffer IDs
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ uint32_t *slice_buf_ids;
+
+ /**
+ * Number of effective slice buffer IDs to send to the HW
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ unsigned int n_slice_buf_ids;
+
+ /**
+ * Size of pre-allocated slice_buf_ids
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ unsigned int slice_buf_ids_alloc;
+
+ /**
+ * Pointer to VASliceParameterBuffers
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ void *slice_params;
+
+ /**
+ * Size of a VASliceParameterBuffer element
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ unsigned int slice_param_size;
+
+ /**
+ * Size of pre-allocated slice_params
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ unsigned int slice_params_alloc;
+
+ /**
+ * Number of slices currently filled in
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ unsigned int slice_count;
+
+ /**
+ * Pointer to slice data buffer base
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ const uint8_t *slice_data;
+
+ /**
+ * Current size of slice data
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ uint32_t slice_data_size;
+};
+
+/* @} */
+
+#endif /* AVCODEC_VAAPI_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavcodec/vda.h b/dom/media/platforms/ffmpeg/libav54/include/libavcodec/vda.h
new file mode 100644
index 0000000000..f0ec2bfec3
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavcodec/vda.h
@@ -0,0 +1,217 @@
+/*
+ * VDA HW acceleration
+ *
+ * copyright (c) 2011 Sebastien Zwickert
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_VDA_H
+#define AVCODEC_VDA_H
+
+/**
+ * @file
+ * @ingroup lavc_codec_hwaccel_vda
+ * Public libavcodec VDA header.
+ */
+
+#include "libavcodec/version.h"
+
+#if FF_API_VDA_ASYNC
+#include <pthread.h>
+#endif
+
+#include <stdint.h>
+
+// emmintrin.h is unable to compile with -std=c99 -Werror=missing-prototypes
+// http://openradar.appspot.com/8026390
+#undef __GNUC_STDC_INLINE__
+
+#define Picture QuickdrawPicture
+#include <VideoDecodeAcceleration/VDADecoder.h>
+#undef Picture
+
+/**
+ * @defgroup lavc_codec_hwaccel_vda VDA
+ * @ingroup lavc_codec_hwaccel
+ *
+ * @{
+ */
+
+#if FF_API_VDA_ASYNC
+/**
+ * This structure is used to store decoded frame information and data.
+ *
+ * @deprecated Use synchronous decoding mode.
+ */
+typedef struct vda_frame {
+ /**
+ * The PTS of the frame.
+ *
+ * - encoding: unused
+ * - decoding: Set/Unset by libavcodec.
+ */
+ int64_t pts;
+
+ /**
+ * The CoreVideo buffer that contains the decoded data.
+ *
+ * - encoding: unused
+ * - decoding: Set/Unset by libavcodec.
+ */
+ CVPixelBufferRef cv_buffer;
+
+ /**
+ * A pointer to the next frame.
+ *
+ * - encoding: unused
+ * - decoding: Set/Unset by libavcodec.
+ */
+ struct vda_frame *next_frame;
+} vda_frame;
+#endif
+
+/**
+ * This structure is used to provide the necessary configurations and data
+ * to the VDA Libav HWAccel implementation.
+ *
+ * The application must make it available as AVCodecContext.hwaccel_context.
+ */
+struct vda_context {
+ /**
+ * VDA decoder object.
+ *
+ * - encoding: unused
+ * - decoding: Set/Unset by libavcodec.
+ */
+ VDADecoder decoder;
+
+ /**
+ * The Core Video pixel buffer that contains the current image data.
+ *
+ * encoding: unused
+ * decoding: Set by libavcodec. Unset by user.
+ */
+ CVPixelBufferRef cv_buffer;
+
+ /**
+ * Use the hardware decoder in synchronous mode.
+ *
+ * encoding: unused
+ * decoding: Set by user.
+ */
+ int use_sync_decoding;
+
+#if FF_API_VDA_ASYNC
+ /**
+ * VDA frames queue ordered by presentation timestamp.
+ *
+ * @deprecated Use synchronous decoding mode.
+ *
+ * - encoding: unused
+ * - decoding: Set/Unset by libavcodec.
+ */
+ vda_frame *queue;
+
+ /**
+ * Mutex for locking queue operations.
+ *
+ * @deprecated Use synchronous decoding mode.
+ *
+ * - encoding: unused
+ * - decoding: Set/Unset by libavcodec.
+ */
+ pthread_mutex_t queue_mutex;
+#endif
+
+ /**
+ * The frame width.
+ *
+ * - encoding: unused
+ * - decoding: Set/Unset by user.
+ */
+ int width;
+
+ /**
+ * The frame height.
+ *
+ * - encoding: unused
+ * - decoding: Set/Unset by user.
+ */
+ int height;
+
+ /**
+ * The frame format.
+ *
+ * - encoding: unused
+ * - decoding: Set/Unset by user.
+ */
+ int format;
+
+ /**
+ * The pixel format for output image buffers.
+ *
+ * - encoding: unused
+ * - decoding: Set/Unset by user.
+ */
+ OSType cv_pix_fmt_type;
+
+ /**
+ * The current bitstream buffer.
+ */
+ uint8_t *priv_bitstream;
+
+ /**
+ * The current size of the bitstream.
+ */
+ int priv_bitstream_size;
+
+ /**
+ * The reference size used for fast reallocation.
+ */
+ int priv_allocated_size;
+};
+
+/** Create the video decoder. */
+int ff_vda_create_decoder(struct vda_context *vda_ctx,
+ uint8_t *extradata,
+ int extradata_size);
+
+/** Destroy the video decoder. */
+int ff_vda_destroy_decoder(struct vda_context *vda_ctx);
+
+#if FF_API_VDA_ASYNC
+/**
+ * Return the top frame of the queue.
+ *
+ * @deprecated Use synchronous decoding mode.
+ */
+vda_frame *ff_vda_queue_pop(struct vda_context *vda_ctx);
+
+/**
+ * Release the given frame.
+ *
+ * @deprecated Use synchronous decoding mode.
+ */
+void ff_vda_release_vda_frame(vda_frame *frame);
+#endif
+
+/**
+ * @}
+ */
+
+#endif /* AVCODEC_VDA_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavcodec/vdpau.h b/dom/media/platforms/ffmpeg/libav54/include/libavcodec/vdpau.h
new file mode 100644
index 0000000000..241ff19051
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavcodec/vdpau.h
@@ -0,0 +1,94 @@
+/*
+ * The Video Decode and Presentation API for UNIX (VDPAU) is used for
+ * hardware-accelerated decoding of MPEG-1/2, H.264 and VC-1.
+ *
+ * Copyright (C) 2008 NVIDIA
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_VDPAU_H
+#define AVCODEC_VDPAU_H
+
+/**
+ * @file
+ * @ingroup lavc_codec_hwaccel_vdpau
+ * Public libavcodec VDPAU header.
+ */
+
+
+/**
+ * @defgroup lavc_codec_hwaccel_vdpau VDPAU Decoder and Renderer
+ * @ingroup lavc_codec_hwaccel
+ *
+ * VDPAU hardware acceleration has two modules
+ * - VDPAU decoding
+ * - VDPAU presentation
+ *
+ * The VDPAU decoding module parses all headers using Libav
+ * parsing mechanisms and uses VDPAU for the actual decoding.
+ *
+ * As per the current implementation, the actual decoding
+ * and rendering (API calls) are done as part of the VDPAU
+ * presentation (vo_vdpau.c) module.
+ *
+ * @{
+ */
+
+#include <vdpau/vdpau.h>
+#include <vdpau/vdpau_x11.h>
+
+/** @brief The videoSurface is used for rendering. */
+#define FF_VDPAU_STATE_USED_FOR_RENDER 1
+
+/**
+ * @brief The videoSurface is needed for reference/prediction.
+ * The codec manipulates this.
+ */
+#define FF_VDPAU_STATE_USED_FOR_REFERENCE 2
+
+/**
+ * @brief This structure is used as a callback between the Libav
+ * decoder (vd_) and presentation (vo_) module.
+ * This is used for defining a video frame containing surface,
+ * picture parameter, bitstream information etc which are passed
+ * between the Libav decoder and its clients.
+ */
+struct vdpau_render_state {
+ VdpVideoSurface surface; ///< Used as rendered surface, never changed.
+
+ int state; ///< Holds FF_VDPAU_STATE_* values.
+
+ /** picture parameter information for all supported codecs */
+ union VdpPictureInfo {
+ VdpPictureInfoH264 h264;
+ VdpPictureInfoMPEG1Or2 mpeg;
+ VdpPictureInfoVC1 vc1;
+ VdpPictureInfoMPEG4Part2 mpeg4;
+ } info;
+
+ /** Describe size/location of the compressed video data.
+ Set to 0 when freeing bitstream_buffers. */
+ int bitstream_buffers_allocated;
+ int bitstream_buffers_used;
+ /** The user is responsible for freeing this buffer using av_freep(). */
+ VdpBitstreamBuffer *bitstream_buffers;
+};
+
+/* @}*/
+
+#endif /* AVCODEC_VDPAU_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavcodec/version.h b/dom/media/platforms/ffmpeg/libav54/include/libavcodec/version.h
new file mode 100644
index 0000000000..348ce99f2a
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavcodec/version.h
@@ -0,0 +1,95 @@
+/*
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_VERSION_H
+#define AVCODEC_VERSION_H
+
+/**
+ * @file
+ * @ingroup libavc
+ * Libavcodec version macros.
+ */
+
+#define LIBAVCODEC_VERSION_MAJOR 54
+#define LIBAVCODEC_VERSION_MINOR 35
+#define LIBAVCODEC_VERSION_MICRO 0
+
+#define LIBAVCODEC_VERSION_INT AV_VERSION_INT(LIBAVCODEC_VERSION_MAJOR, \
+ LIBAVCODEC_VERSION_MINOR, \
+ LIBAVCODEC_VERSION_MICRO)
+#define LIBAVCODEC_VERSION AV_VERSION(LIBAVCODEC_VERSION_MAJOR, \
+ LIBAVCODEC_VERSION_MINOR, \
+ LIBAVCODEC_VERSION_MICRO)
+#define LIBAVCODEC_BUILD LIBAVCODEC_VERSION_INT
+
+#define LIBAVCODEC_IDENT "Lavc" AV_STRINGIFY(LIBAVCODEC_VERSION)
+
+/**
+ * FF_API_* defines may be placed below to indicate public API that will be
+ * dropped at a future version bump. The defines themselves are not part of
+ * the public API and may change, break or disappear at any time.
+ */
+
+#ifndef FF_API_REQUEST_CHANNELS
+#define FF_API_REQUEST_CHANNELS (LIBAVCODEC_VERSION_MAJOR < 55)
+#endif
+#ifndef FF_API_OLD_DECODE_AUDIO
+#define FF_API_OLD_DECODE_AUDIO (LIBAVCODEC_VERSION_MAJOR < 55)
+#endif
+#ifndef FF_API_OLD_ENCODE_AUDIO
+#define FF_API_OLD_ENCODE_AUDIO (LIBAVCODEC_VERSION_MAJOR < 55)
+#endif
+#ifndef FF_API_OLD_ENCODE_VIDEO
+#define FF_API_OLD_ENCODE_VIDEO (LIBAVCODEC_VERSION_MAJOR < 55)
+#endif
+#ifndef FF_API_MPV_GLOBAL_OPTS
+#define FF_API_MPV_GLOBAL_OPTS (LIBAVCODEC_VERSION_MAJOR < 55)
+#endif
+#ifndef FF_API_COLOR_TABLE_ID
+#define FF_API_COLOR_TABLE_ID (LIBAVCODEC_VERSION_MAJOR < 55)
+#endif
+#ifndef FF_API_INTER_THRESHOLD
+#define FF_API_INTER_THRESHOLD (LIBAVCODEC_VERSION_MAJOR < 55)
+#endif
+#ifndef FF_API_SUB_ID
+#define FF_API_SUB_ID (LIBAVCODEC_VERSION_MAJOR < 55)
+#endif
+#ifndef FF_API_DSP_MASK
+#define FF_API_DSP_MASK (LIBAVCODEC_VERSION_MAJOR < 55)
+#endif
+#ifndef FF_API_FIND_BEST_PIX_FMT
+#define FF_API_FIND_BEST_PIX_FMT (LIBAVCODEC_VERSION_MAJOR < 55)
+#endif
+#ifndef FF_API_CODEC_ID
+#define FF_API_CODEC_ID (LIBAVCODEC_VERSION_MAJOR < 55)
+#endif
+#ifndef FF_API_VDA_ASYNC
+#define FF_API_VDA_ASYNC (LIBAVCODEC_VERSION_MAJOR < 55)
+#endif
+#ifndef FF_API_AVCODEC_RESAMPLE
+#define FF_API_AVCODEC_RESAMPLE (LIBAVCODEC_VERSION_MAJOR < 55)
+#endif
+#ifndef FF_API_LIBMPEG2
+#define FF_API_LIBMPEG2 (LIBAVCODEC_VERSION_MAJOR < 55)
+#endif
+#ifndef FF_API_MMI
+#define FF_API_MMI (LIBAVCODEC_VERSION_MAJOR < 55)
+#endif
+
+#endif /* AVCODEC_VERSION_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavcodec/xvmc.h b/dom/media/platforms/ffmpeg/libav54/include/libavcodec/xvmc.h
new file mode 100644
index 0000000000..1f77e4efca
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavcodec/xvmc.h
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2003 Ivan Kalvachev
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_XVMC_H
+#define AVCODEC_XVMC_H
+
+/**
+ * @file
+ * @ingroup lavc_codec_hwaccel_xvmc
+ * Public libavcodec XvMC header.
+ */
+
+#include <X11/extensions/XvMC.h>
+
+#include "avcodec.h"
+
+/**
+ * @defgroup lavc_codec_hwaccel_xvmc XvMC
+ * @ingroup lavc_codec_hwaccel
+ *
+ * @{
+ */
+
+#define AV_XVMC_ID 0x1DC711C0 /**< special value to ensure that regular pixel routines haven't corrupted the struct
+ the number is 1337 speak for the letters IDCT MCo (motion compensation) */
+
+struct xvmc_pix_fmt {
+ /** The field contains the special constant value AV_XVMC_ID.
+ It is used as a test that the application correctly uses the API,
+ and that there is no corruption caused by pixel routines.
+ - application - set during initialization
+ - libavcodec - unchanged
+ */
+ int xvmc_id;
+
+ /** Pointer to the block array allocated by XvMCCreateBlocks().
+ The array has to be freed by XvMCDestroyBlocks().
+ Each group of 64 values represents one data block of differential
+ pixel information (in MoCo mode) or coefficients for IDCT.
+ - application - set the pointer during initialization
+ - libavcodec - fills coefficients/pixel data into the array
+ */
+ short* data_blocks;
+
+ /** Pointer to the macroblock description array allocated by
+ XvMCCreateMacroBlocks() and freed by XvMCDestroyMacroBlocks().
+ - application - set the pointer during initialization
+ - libavcodec - fills description data into the array
+ */
+ XvMCMacroBlock* mv_blocks;
+
+ /** Number of macroblock descriptions that can be stored in the mv_blocks
+ array.
+ - application - set during initialization
+ - libavcodec - unchanged
+ */
+ int allocated_mv_blocks;
+
+ /** Number of blocks that can be stored at once in the data_blocks array.
+ - application - set during initialization
+ - libavcodec - unchanged
+ */
+ int allocated_data_blocks;
+
+ /** Indicate that the hardware would interpret data_blocks as IDCT
+ coefficients and perform IDCT on them.
+ - application - set during initialization
+ - libavcodec - unchanged
+ */
+ int idct;
+
+ /** In MoCo mode it indicates that intra macroblocks are assumed to be in
+ unsigned format; same as the XVMC_INTRA_UNSIGNED flag.
+ - application - set during initialization
+ - libavcodec - unchanged
+ */
+ int unsigned_intra;
+
+ /** Pointer to the surface allocated by XvMCCreateSurface().
+ It has to be freed by XvMCDestroySurface() on application exit.
+ It identifies the frame and its state on the video hardware.
+ - application - set during initialization
+ - libavcodec - unchanged
+ */
+ XvMCSurface* p_surface;
+
+/** Set by the decoder before calling ff_draw_horiz_band(),
+ needed by the XvMCRenderSurface function. */
+//@{
+ /** Pointer to the surface used as past reference
+ - application - unchanged
+ - libavcodec - set
+ */
+ XvMCSurface* p_past_surface;
+
+ /** Pointer to the surface used as future reference
+ - application - unchanged
+ - libavcodec - set
+ */
+ XvMCSurface* p_future_surface;
+
+ /** top/bottom field or frame
+ - application - unchanged
+ - libavcodec - set
+ */
+ unsigned int picture_structure;
+
+ /** XVMC_SECOND_FIELD - 1st or 2nd field in the sequence
+ - application - unchanged
+ - libavcodec - set
+ */
+ unsigned int flags;
+//}@
+
+ /** Number of macroblock descriptions in the mv_blocks array
+ that have already been passed to the hardware.
+ - application - zeroes it on get_buffer().
+ A successful ff_draw_horiz_band() may increment it
+ with filled_mb_block_num or zero both.
+ - libavcodec - unchanged
+ */
+ int start_mv_blocks_num;
+
+ /** Number of new macroblock descriptions in the mv_blocks array (after
+ start_mv_blocks_num) that are filled by libavcodec and have to be
+ passed to the hardware.
+ - application - zeroes it on get_buffer() or after successful
+ ff_draw_horiz_band().
+ - libavcodec - increment with one of each stored MB
+ */
+ int filled_mv_blocks_num;
+
+ /** Number of the next free data block; one data block consists of
+ 64 short values in the data_blocks array.
+ All blocks before this one have already been claimed by placing their
+ position into the corresponding block description structure field,
+ that are part of the mv_blocks array.
+ - application - zeroes it on get_buffer().
+ A successful ff_draw_horiz_band() may zero it together
+ with start_mb_blocks_num.
+ - libavcodec - each decoded macroblock increases it by the number
+ of coded blocks it contains.
+ */
+ int next_free_data_block_num;
+};
+
+/**
+ * @}
+ */
+
+#endif /* AVCODEC_XVMC_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/adler32.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/adler32.h
new file mode 100644
index 0000000000..a8ff6f9d41
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/adler32.h
@@ -0,0 +1,43 @@
+/*
+ * copyright (c) 2006 Mans Rullgard
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_ADLER32_H
+#define AVUTIL_ADLER32_H
+
+#include <stdint.h>
+#include "attributes.h"
+
+/**
+ * @ingroup lavu_crypto
+ * Calculate the Adler32 checksum of a buffer.
+ *
+ * Passing the return value to a subsequent av_adler32_update() call
+ * allows the checksum of multiple buffers to be calculated as though
+ * they were concatenated.
+ *
+ * @param adler initial checksum value
+ * @param buf pointer to input buffer
+ * @param len size of input buffer
+ * @return updated checksum
+ */
+unsigned long av_adler32_update(unsigned long adler, const uint8_t *buf,
+ unsigned int len) av_pure;
+
+#endif /* AVUTIL_ADLER32_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/aes.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/aes.h
new file mode 100644
index 0000000000..edff275b7a
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/aes.h
@@ -0,0 +1,67 @@
+/*
+ * copyright (c) 2007 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_AES_H
+#define AVUTIL_AES_H
+
+#include <stdint.h>
+
+#include "attributes.h"
+#include "version.h"
+
+/**
+ * @defgroup lavu_aes AES
+ * @ingroup lavu_crypto
+ * @{
+ */
+
+#if FF_API_CONTEXT_SIZE
+extern attribute_deprecated const int av_aes_size;
+#endif
+
+struct AVAES;
+
+/**
+ * Allocate an AVAES context.
+ */
+struct AVAES *av_aes_alloc(void);
+
+/**
+ * Initialize an AVAES context.
+ * @param key_bits 128, 192 or 256
+ * @param decrypt 0 for encryption, 1 for decryption
+ */
+int av_aes_init(struct AVAES *a, const uint8_t *key, int key_bits, int decrypt);
+
+/**
+ * Encrypt or decrypt a buffer using a previously initialized context.
+ * @param count number of 16 byte blocks
+ * @param dst destination array, can be equal to src
+ * @param src source array, can be equal to dst
+ * @param iv initialization vector for CBC mode, if NULL then ECB will be used
+ * @param decrypt 0 for encryption, 1 for decryption
+ */
+void av_aes_crypt(struct AVAES *a, uint8_t *dst, const uint8_t *src, int count, uint8_t *iv, int decrypt);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_AES_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/attributes.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/attributes.h
new file mode 100644
index 0000000000..292a0a1a88
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/attributes.h
@@ -0,0 +1,122 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * Macro definitions for various function/variable attributes
+ */
+
+#ifndef AVUTIL_ATTRIBUTES_H
+#define AVUTIL_ATTRIBUTES_H
+
+#ifdef __GNUC__
+# define AV_GCC_VERSION_AT_LEAST(x,y) (__GNUC__ > x || __GNUC__ == x && __GNUC_MINOR__ >= y)
+#else
+# define AV_GCC_VERSION_AT_LEAST(x,y) 0
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3,1)
+# define av_always_inline __attribute__((always_inline)) inline
+#elif defined(_MSC_VER)
+# define av_always_inline __forceinline
+#else
+# define av_always_inline inline
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3,1)
+# define av_noinline __attribute__((noinline))
+#else
+# define av_noinline
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3,1)
+# define av_pure __attribute__((pure))
+#else
+# define av_pure
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(2,6)
+# define av_const __attribute__((const))
+#else
+# define av_const
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(4,3)
+# define av_cold __attribute__((cold))
+#else
+# define av_cold
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(4,1)
+# define av_flatten __attribute__((flatten))
+#else
+# define av_flatten
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3,1)
+# define attribute_deprecated __attribute__((deprecated))
+#else
+# define attribute_deprecated
+#endif
+
+#if defined(__GNUC__)
+# define av_unused __attribute__((unused))
+#else
+# define av_unused
+#endif
+
+/**
+ * Mark a variable as used and prevent the compiler from optimizing it
+ * away. This is useful for variables accessed only from inline
+ * assembler without the compiler being aware.
+ */
+#if AV_GCC_VERSION_AT_LEAST(3,1)
+# define av_used __attribute__((used))
+#else
+# define av_used
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3,3)
+# define av_alias __attribute__((may_alias))
+#else
+# define av_alias
+#endif
+
+#if defined(__GNUC__) && !defined(__ICC)
+# define av_uninit(x) x=x
+#else
+# define av_uninit(x) x
+#endif
+
+#ifdef __GNUC__
+# define av_builtin_constant_p __builtin_constant_p
+# define av_printf_format(fmtpos, attrpos) __attribute__((__format__(__printf__, fmtpos, attrpos)))
+#else
+# define av_builtin_constant_p(x) 0
+# define av_printf_format(fmtpos, attrpos)
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(2,5)
+# define av_noreturn __attribute__((noreturn))
+#else
+# define av_noreturn
+#endif
+
+#endif /* AVUTIL_ATTRIBUTES_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/audio_fifo.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/audio_fifo.h
new file mode 100644
index 0000000000..8c76388255
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/audio_fifo.h
@@ -0,0 +1,146 @@
+/*
+ * Audio FIFO
+ * Copyright (c) 2012 Justin Ruggles <justin.ruggles@gmail.com>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * Audio FIFO Buffer
+ */
+
+#ifndef AVUTIL_AUDIO_FIFO_H
+#define AVUTIL_AUDIO_FIFO_H
+
+#include "avutil.h"
+#include "fifo.h"
+#include "samplefmt.h"
+
+/**
+ * @addtogroup lavu_audio
+ * @{
+ */
+
+/**
+ * Context for an Audio FIFO Buffer.
+ *
+ * - Operates at the sample level rather than the byte level.
+ * - Supports multiple channels with either planar or packed sample format.
+ * - Automatic reallocation when writing to a full buffer.
+ */
+typedef struct AVAudioFifo AVAudioFifo;
+
+/**
+ * Free an AVAudioFifo.
+ *
+ * @param af AVAudioFifo to free
+ */
+void av_audio_fifo_free(AVAudioFifo *af);
+
+/**
+ * Allocate an AVAudioFifo.
+ *
+ * @param sample_fmt sample format
+ * @param channels number of channels
+ * @param nb_samples initial allocation size, in samples
+ * @return newly allocated AVAudioFifo, or NULL on error
+ */
+AVAudioFifo *av_audio_fifo_alloc(enum AVSampleFormat sample_fmt, int channels,
+ int nb_samples);
+
+/**
+ * Reallocate an AVAudioFifo.
+ *
+ * @param af AVAudioFifo to reallocate
+ * @param nb_samples new allocation size, in samples
+ * @return 0 if OK, or negative AVERROR code on failure
+ */
+int av_audio_fifo_realloc(AVAudioFifo *af, int nb_samples);
+
+/**
+ * Write data to an AVAudioFifo.
+ *
+ * The AVAudioFifo will be reallocated automatically if the available space
+ * is less than nb_samples.
+ *
+ * @see enum AVSampleFormat
+ * The documentation for AVSampleFormat describes the data layout.
+ *
+ * @param af AVAudioFifo to write to
+ * @param data audio data plane pointers
+ * @param nb_samples number of samples to write
+ * @return number of samples actually written, or negative AVERROR
+ * code on failure.
+ */
+int av_audio_fifo_write(AVAudioFifo *af, void **data, int nb_samples);
+
+/**
+ * Read data from an AVAudioFifo.
+ *
+ * @see enum AVSampleFormat
+ * The documentation for AVSampleFormat describes the data layout.
+ *
+ * @param af AVAudioFifo to read from
+ * @param data audio data plane pointers
+ * @param nb_samples number of samples to read
+ * @return number of samples actually read, or negative AVERROR code
+ * on failure.
+ */
+int av_audio_fifo_read(AVAudioFifo *af, void **data, int nb_samples);
+
+/**
+ * Drain data from an AVAudioFifo.
+ *
+ * Removes the data without reading it.
+ *
+ * @param af AVAudioFifo to drain
+ * @param nb_samples number of samples to drain
+ * @return 0 if OK, or negative AVERROR code on failure
+ */
+int av_audio_fifo_drain(AVAudioFifo *af, int nb_samples);
+
+/**
+ * Reset the AVAudioFifo buffer.
+ *
+ * This empties all data in the buffer.
+ *
+ * @param af AVAudioFifo to reset
+ */
+void av_audio_fifo_reset(AVAudioFifo *af);
+
+/**
+ * Get the current number of samples in the AVAudioFifo available for reading.
+ *
+ * @param af the AVAudioFifo to query
+ * @return number of samples available for reading
+ */
+int av_audio_fifo_size(AVAudioFifo *af);
+
+/**
+ * Get the current number of samples in the AVAudioFifo available for writing.
+ *
+ * @param af the AVAudioFifo to query
+ * @return number of samples available for writing
+ */
+int av_audio_fifo_space(AVAudioFifo *af);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_AUDIO_FIFO_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/audioconvert.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/audioconvert.h
new file mode 100644
index 0000000000..300a67cd3d
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/audioconvert.h
@@ -0,0 +1,6 @@
+
+#include "version.h"
+
+#if FF_API_AUDIOCONVERT
+#include "channel_layout.h"
+#endif
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/avassert.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/avassert.h
new file mode 100644
index 0000000000..b223d26e8d
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/avassert.h
@@ -0,0 +1,66 @@
+/*
+ * copyright (c) 2010 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * simple assert() macros that are a bit more flexible than ISO C assert().
+ * @author Michael Niedermayer <michaelni@gmx.at>
+ */
+
+#ifndef AVUTIL_AVASSERT_H
+#define AVUTIL_AVASSERT_H
+
+#include <stdlib.h>
+#include "avutil.h"
+#include "log.h"
+
+/**
+ * assert() equivalent, that is always enabled.
+ */
+#define av_assert0(cond) do { \
+ if (!(cond)) { \
+ av_log(NULL, AV_LOG_FATAL, "Assertion %s failed at %s:%d\n", \
+ AV_STRINGIFY(cond), __FILE__, __LINE__); \
+ abort(); \
+ } \
+} while (0)
+
+
+/**
+ * assert() equivalent, that does not lie in speed critical code.
+ * These asserts() thus can be enabled without fearing speedloss.
+ */
+#if defined(ASSERT_LEVEL) && ASSERT_LEVEL > 0
+#define av_assert1(cond) av_assert0(cond)
+#else
+#define av_assert1(cond) ((void)0)
+#endif
+
+
+/**
+ * assert() equivalent, that does lie in speed critical code.
+ */
+#if defined(ASSERT_LEVEL) && ASSERT_LEVEL > 1
+#define av_assert2(cond) av_assert0(cond)
+#else
+#define av_assert2(cond) ((void)0)
+#endif
+
+#endif /* AVUTIL_AVASSERT_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/avconfig.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/avconfig.h
new file mode 100644
index 0000000000..f10aa6186b
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/avconfig.h
@@ -0,0 +1,6 @@
+/* Generated by ffconf */
+#ifndef AVUTIL_AVCONFIG_H
+#define AVUTIL_AVCONFIG_H
+#define AV_HAVE_BIGENDIAN 0
+#define AV_HAVE_FAST_UNALIGNED 1
+#endif /* AVUTIL_AVCONFIG_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/avstring.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/avstring.h
new file mode 100644
index 0000000000..acd6610d38
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/avstring.h
@@ -0,0 +1,191 @@
+/*
+ * Copyright (c) 2007 Mans Rullgard
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_AVSTRING_H
+#define AVUTIL_AVSTRING_H
+
+#include <stddef.h>
+#include "attributes.h"
+
+/**
+ * @addtogroup lavu_string
+ * @{
+ */
+
+/**
+ * Return non-zero if pfx is a prefix of str. If it is, *ptr is set to
+ * the address of the first character in str after the prefix.
+ *
+ * @param str input string
+ * @param pfx prefix to test
+ * @param ptr updated if the prefix is matched inside str
+ * @return non-zero if the prefix matches, zero otherwise
+ */
+int av_strstart(const char *str, const char *pfx, const char **ptr);
+
+/**
+ * Return non-zero if pfx is a prefix of str independent of case. If
+ * it is, *ptr is set to the address of the first character in str
+ * after the prefix.
+ *
+ * @param str input string
+ * @param pfx prefix to test
+ * @param ptr updated if the prefix is matched inside str
+ * @return non-zero if the prefix matches, zero otherwise
+ */
+int av_stristart(const char *str, const char *pfx, const char **ptr);
+
+/**
+ * Locate the first case-independent occurrence in the string haystack
+ * of the string needle. A zero-length string needle is considered to
+ * match at the start of haystack.
+ *
+ * This function is a case-insensitive version of the standard strstr().
+ *
+ * @param haystack string to search in
+ * @param needle string to search for
+ * @return pointer to the located match within haystack
+ * or a null pointer if no match
+ */
+char *av_stristr(const char *haystack, const char *needle);
+
+/**
+ * Copy the string src to dst, but no more than size - 1 bytes, and
+ * null-terminate dst.
+ *
+ * This function is the same as BSD strlcpy().
+ *
+ * @param dst destination buffer
+ * @param src source string
+ * @param size size of destination buffer
+ * @return the length of src
+ *
+ * @warning since the return value is the length of src, src absolutely
+ * _must_ be a properly 0-terminated string, otherwise this will read beyond
+ * the end of the buffer and possibly crash.
+ */
+size_t av_strlcpy(char *dst, const char *src, size_t size);
+
+/**
+ * Append the string src to the string dst, but to a total length of
+ * no more than size - 1 bytes, and null-terminate dst.
+ *
+ * This function is similar to BSD strlcat(), but differs when
+ * size <= strlen(dst).
+ *
+ * @param dst destination buffer
+ * @param src source string
+ * @param size size of destination buffer
+ * @return the total length of src and dst
+ *
+ * @warning since the return value use the length of src and dst, these
+ * absolutely _must_ be a properly 0-terminated strings, otherwise this
+ * will read beyond the end of the buffer and possibly crash.
+ */
+size_t av_strlcat(char *dst, const char *src, size_t size);
+
+/**
+ * Append output to a string, according to a format. Never write out of
+ * the destination buffer, and always put a terminating 0 within
+ * the buffer.
+ * @param dst destination buffer (string to which the output is
+ * appended)
+ * @param size total size of the destination buffer
+ * @param fmt printf-compatible format string, specifying how the
+ * following parameters are used
+ * @return the length of the string that would have been generated
+ * if enough space had been available
+ */
+size_t av_strlcatf(char *dst, size_t size, const char *fmt, ...) av_printf_format(3, 4);
+
+/**
+ * Convert a number to a av_malloced string.
+ */
+char *av_d2str(double d);
+
+/**
+ * Unescape the given string until a non escaped terminating char,
+ * and return the token corresponding to the unescaped string.
+ *
+ * The normal \ and ' escaping is supported. Leading and trailing
+ * whitespaces are removed, unless they are escaped with '\' or are
+ * enclosed between ''.
+ *
+ * @param buf the buffer to parse, buf will be updated to point to the
+ * terminating char
+ * @param term a 0-terminated list of terminating chars
+ * @return the malloced unescaped string, which must be av_freed by
+ * the user, NULL in case of allocation failure
+ */
+char *av_get_token(const char **buf, const char *term);
+
+/**
+ * Locale-independent conversion of ASCII characters to uppercase.
+ */
+static inline int av_toupper(int c)
+{
+ if (c >= 'a' && c <= 'z')
+ c ^= 0x20;
+ return c;
+}
+
+/**
+ * Locale-independent conversion of ASCII characters to lowercase.
+ */
+static inline int av_tolower(int c)
+{
+ if (c >= 'A' && c <= 'Z')
+ c ^= 0x20;
+ return c;
+}
+
+/*
+ * Locale-independent case-insensitive compare.
+ * @note This means only ASCII-range characters are case-insensitive
+ */
+int av_strcasecmp(const char *a, const char *b);
+
+/**
+ * Locale-independent case-insensitive compare.
+ * @note This means only ASCII-range characters are case-insensitive
+ */
+int av_strncasecmp(const char *a, const char *b, size_t n);
+
+
+/**
+ * Thread safe basename.
+ * @param path the path, on DOS both \ and / are considered separators.
+ * @return pointer to the basename substring.
+ */
+const char *av_basename(const char *path);
+
+/**
+ * Thread safe dirname.
+ * @param path the path, on DOS both \ and / are considered separators.
+ * @return the path with the separator replaced by the string terminator or ".".
+ * @note the function may change the input string.
+ */
+const char *av_dirname(char *path);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_AVSTRING_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/avutil.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/avutil.h
new file mode 100644
index 0000000000..33f9bea723
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/avutil.h
@@ -0,0 +1,275 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_AVUTIL_H
+#define AVUTIL_AVUTIL_H
+
+/**
+ * @file
+ * external API header
+ */
+
+/**
+ * @mainpage
+ *
+ * @section libav_intro Introduction
+ *
+ * This document describes the usage of the different libraries
+ * provided by Libav.
+ *
+ * @li @ref libavc "libavcodec" encoding/decoding library
+ * @li @subpage libavfilter graph based frame editing library
+ * @li @ref libavf "libavformat" I/O and muxing/demuxing library
+ * @li @ref lavd "libavdevice" special devices muxing/demuxing library
+ * @li @ref lavu "libavutil" common utility library
+ * @li @ref lavr "libavresample" audio resampling, format conversion and mixing
+ * @li @subpage libswscale color conversion and scaling library
+ */
+
+/**
+ * @defgroup lavu Common utility functions
+ *
+ * @brief
+ * libavutil contains the code shared across all the other Libav
+ * libraries
+ *
+ * @note In order to use the functions provided by avutil you must include
+ * the specific header.
+ *
+ * @{
+ *
+ * @defgroup lavu_crypto Crypto and Hashing
+ *
+ * @{
+ * @}
+ *
+ * @defgroup lavu_math Maths
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_string String Manipulation
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_mem Memory Management
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_data Data Structures
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_audio Audio related
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_error Error Codes
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_misc Other
+ *
+ * @{
+ *
+ * @defgroup lavu_internal Internal
+ *
+ * Not exported functions, for internal usage only
+ *
+ * @{
+ *
+ * @}
+ */
+
+
+/**
+ * @defgroup preproc_misc Preprocessor String Macros
+ *
+ * String manipulation macros
+ *
+ * @{
+ */
+
+#define AV_STRINGIFY(s) AV_TOSTRING(s)
+#define AV_TOSTRING(s) #s
+
+#define AV_GLUE(a, b) a ## b
+#define AV_JOIN(a, b) AV_GLUE(a, b)
+
+#define AV_PRAGMA(s) _Pragma(#s)
+
+/**
+ * @}
+ */
+
+/**
+ * @defgroup version_utils Library Version Macros
+ *
+ * Useful to check and match library version in order to maintain
+ * backward compatibility.
+ *
+ * @{
+ */
+
+#define AV_VERSION_INT(a, b, c) (a<<16 | b<<8 | c)
+#define AV_VERSION_DOT(a, b, c) a ##.## b ##.## c
+#define AV_VERSION(a, b, c) AV_VERSION_DOT(a, b, c)
+
+/**
+ * @}
+ */
+
+/**
+ * @addtogroup lavu_ver
+ * @{
+ */
+
+/**
+ * Return the LIBAVUTIL_VERSION_INT constant.
+ */
+unsigned avutil_version(void);
+
+/**
+ * Return the libavutil build-time configuration.
+ */
+const char *avutil_configuration(void);
+
+/**
+ * Return the libavutil license.
+ */
+const char *avutil_license(void);
+
+/**
+ * @}
+ */
+
+/**
+ * @addtogroup lavu_media Media Type
+ * @brief Media Type
+ */
+
+enum AVMediaType {
+ AVMEDIA_TYPE_UNKNOWN = -1, ///< Usually treated as AVMEDIA_TYPE_DATA
+ AVMEDIA_TYPE_VIDEO,
+ AVMEDIA_TYPE_AUDIO,
+ AVMEDIA_TYPE_DATA, ///< Opaque data information usually continuous
+ AVMEDIA_TYPE_SUBTITLE,
+ AVMEDIA_TYPE_ATTACHMENT, ///< Opaque data information usually sparse
+ AVMEDIA_TYPE_NB
+};
+
+/**
+ * @defgroup lavu_const Constants
+ * @{
+ *
+ * @defgroup lavu_enc Encoding specific
+ *
+ * @note those definition should move to avcodec
+ * @{
+ */
+
+#define FF_LAMBDA_SHIFT 7
+#define FF_LAMBDA_SCALE (1<<FF_LAMBDA_SHIFT)
+#define FF_QP2LAMBDA 118 ///< factor to convert from H.263 QP to lambda
+#define FF_LAMBDA_MAX (256*128-1)
+
+#define FF_QUALITY_SCALE FF_LAMBDA_SCALE //FIXME maybe remove
+
+/**
+ * @}
+ * @defgroup lavu_time Timestamp specific
+ *
+ * Libav internal timebase and timestamp definitions
+ *
+ * @{
+ */
+
+/**
+ * @brief Undefined timestamp value
+ *
+ * Usually reported by demuxer that work on containers that do not provide
+ * either pts or dts.
+ */
+
+#define AV_NOPTS_VALUE INT64_C(0x8000000000000000)
+
+/**
+ * Internal time base represented as integer
+ */
+
+#define AV_TIME_BASE 1000000
+
+/**
+ * Internal time base represented as fractional value
+ */
+
+#define AV_TIME_BASE_Q (AVRational){1, AV_TIME_BASE}
+
+/**
+ * @}
+ * @}
+ * @defgroup lavu_picture Image related
+ *
+ * AVPicture types, pixel formats and basic image planes manipulation.
+ *
+ * @{
+ */
+
+enum AVPictureType {
+ AV_PICTURE_TYPE_I = 1, ///< Intra
+ AV_PICTURE_TYPE_P, ///< Predicted
+ AV_PICTURE_TYPE_B, ///< Bi-dir predicted
+ AV_PICTURE_TYPE_S, ///< S(GMC)-VOP MPEG4
+ AV_PICTURE_TYPE_SI, ///< Switching Intra
+ AV_PICTURE_TYPE_SP, ///< Switching Predicted
+ AV_PICTURE_TYPE_BI, ///< BI type
+};
+
+/**
+ * Return a single letter to describe the given picture type
+ * pict_type.
+ *
+ * @param[in] pict_type the picture type @return a single character
+ * representing the picture type, '?' if pict_type is unknown
+ */
+char av_get_picture_type_char(enum AVPictureType pict_type);
+
+/**
+ * @}
+ */
+
+#include "error.h"
+#include "version.h"
+
+/**
+ * @}
+ * @}
+ */
+
+#endif /* AVUTIL_AVUTIL_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/base64.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/base64.h
new file mode 100644
index 0000000000..4750cf5c72
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/base64.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2006 Ryan Martell. (rdm4@martellventures.com)
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_BASE64_H
+#define AVUTIL_BASE64_H
+
+#include <stdint.h>
+
+/**
+ * @defgroup lavu_base64 Base64
+ * @ingroup lavu_crypto
+ * @{
+ */
+
+
+/**
+ * Decode a base64-encoded string.
+ *
+ * @param out buffer for decoded data
+ * @param in null-terminated input string
+ * @param out_size size in bytes of the out buffer, must be at
+ * least 3/4 of the length of in
+ * @return number of bytes written, or a negative value in case of
+ * invalid input
+ */
+int av_base64_decode(uint8_t *out, const char *in, int out_size);
+
+/**
+ * Encode data to base64 and null-terminate.
+ *
+ * @param out buffer for encoded data
+ * @param out_size size in bytes of the output buffer, must be at
+ * least AV_BASE64_SIZE(in_size)
+ * @param in_size size in bytes of the 'in' buffer
+ * @return 'out' or NULL in case of error
+ */
+char *av_base64_encode(char *out, int out_size, const uint8_t *in, int in_size);
+
+/**
+ * Calculate the output size needed to base64-encode x bytes.
+ */
+#define AV_BASE64_SIZE(x) (((x)+2) / 3 * 4 + 1)
+
+ /**
+ * @}
+ */
+
+#endif /* AVUTIL_BASE64_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/blowfish.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/blowfish.h
new file mode 100644
index 0000000000..8c29536cfe
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/blowfish.h
@@ -0,0 +1,76 @@
+/*
+ * Blowfish algorithm
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_BLOWFISH_H
+#define AVUTIL_BLOWFISH_H
+
+#include <stdint.h>
+
+/**
+ * @defgroup lavu_blowfish Blowfish
+ * @ingroup lavu_crypto
+ * @{
+ */
+
+#define AV_BF_ROUNDS 16
+
+typedef struct AVBlowfish {
+ uint32_t p[AV_BF_ROUNDS + 2];
+ uint32_t s[4][256];
+} AVBlowfish;
+
+/**
+ * Initialize an AVBlowfish context.
+ *
+ * @param ctx an AVBlowfish context
+ * @param key a key
+ * @param key_len length of the key
+ */
+void av_blowfish_init(struct AVBlowfish *ctx, const uint8_t *key, int key_len);
+
+/**
+ * Encrypt or decrypt a buffer using a previously initialized context.
+ *
+ * @param ctx an AVBlowfish context
+ * @param xl left four bytes halves of input to be encrypted
+ * @param xr right four bytes halves of input to be encrypted
+ * @param decrypt 0 for encryption, 1 for decryption
+ */
+void av_blowfish_crypt_ecb(struct AVBlowfish *ctx, uint32_t *xl, uint32_t *xr,
+ int decrypt);
+
+/**
+ * Encrypt or decrypt a buffer using a previously initialized context.
+ *
+ * @param ctx an AVBlowfish context
+ * @param dst destination array, can be equal to src
+ * @param src source array, can be equal to dst
+ * @param count number of 8 byte blocks
+ * @param iv initialization vector for CBC mode, if NULL ECB will be used
+ * @param decrypt 0 for encryption, 1 for decryption
+ */
+void av_blowfish_crypt(struct AVBlowfish *ctx, uint8_t *dst, const uint8_t *src,
+ int count, uint8_t *iv, int decrypt);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_BLOWFISH_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/bswap.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/bswap.h
new file mode 100644
index 0000000000..8a350e1cd5
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/bswap.h
@@ -0,0 +1,109 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * byte swapping routines
+ */
+
+#ifndef AVUTIL_BSWAP_H
+#define AVUTIL_BSWAP_H
+
+#include <stdint.h>
+#include "libavutil/avconfig.h"
+#include "attributes.h"
+
+#ifdef HAVE_AV_CONFIG_H
+
+#include "config.h"
+
+#if ARCH_ARM
+# include "arm/bswap.h"
+#elif ARCH_AVR32
+# include "avr32/bswap.h"
+#elif ARCH_BFIN
+# include "bfin/bswap.h"
+#elif ARCH_SH4
+# include "sh4/bswap.h"
+#elif ARCH_X86
+# include "x86/bswap.h"
+#endif
+
+#endif /* HAVE_AV_CONFIG_H */
+
+#define AV_BSWAP16C(x) (((x) << 8 & 0xff00) | ((x) >> 8 & 0x00ff))
+#define AV_BSWAP32C(x) (AV_BSWAP16C(x) << 16 | AV_BSWAP16C((x) >> 16))
+#define AV_BSWAP64C(x) (AV_BSWAP32C(x) << 32 | AV_BSWAP32C((x) >> 32))
+
+#define AV_BSWAPC(s, x) AV_BSWAP##s##C(x)
+
+#ifndef av_bswap16
+static av_always_inline av_const uint16_t av_bswap16(uint16_t x)
+{
+ x= (x>>8) | (x<<8);
+ return x;
+}
+#endif
+
+#ifndef av_bswap32
+static av_always_inline av_const uint32_t av_bswap32(uint32_t x)
+{
+ return AV_BSWAP32C(x);
+}
+#endif
+
+#ifndef av_bswap64
+static inline uint64_t av_const av_bswap64(uint64_t x)
+{
+ return (uint64_t)av_bswap32(x) << 32 | av_bswap32(x >> 32);
+}
+#endif
+
+// be2ne ... big-endian to native-endian
+// le2ne ... little-endian to native-endian
+
+#if AV_HAVE_BIGENDIAN
+#define av_be2ne16(x) (x)
+#define av_be2ne32(x) (x)
+#define av_be2ne64(x) (x)
+#define av_le2ne16(x) av_bswap16(x)
+#define av_le2ne32(x) av_bswap32(x)
+#define av_le2ne64(x) av_bswap64(x)
+#define AV_BE2NEC(s, x) (x)
+#define AV_LE2NEC(s, x) AV_BSWAPC(s, x)
+#else
+#define av_be2ne16(x) av_bswap16(x)
+#define av_be2ne32(x) av_bswap32(x)
+#define av_be2ne64(x) av_bswap64(x)
+#define av_le2ne16(x) (x)
+#define av_le2ne32(x) (x)
+#define av_le2ne64(x) (x)
+#define AV_BE2NEC(s, x) AV_BSWAPC(s, x)
+#define AV_LE2NEC(s, x) (x)
+#endif
+
+#define AV_BE2NE16C(x) AV_BE2NEC(16, x)
+#define AV_BE2NE32C(x) AV_BE2NEC(32, x)
+#define AV_BE2NE64C(x) AV_BE2NEC(64, x)
+#define AV_LE2NE16C(x) AV_LE2NEC(16, x)
+#define AV_LE2NE32C(x) AV_LE2NEC(32, x)
+#define AV_LE2NE64C(x) AV_LE2NEC(64, x)
+
+#endif /* AVUTIL_BSWAP_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/channel_layout.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/channel_layout.h
new file mode 100644
index 0000000000..15b6887a67
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/channel_layout.h
@@ -0,0 +1,182 @@
+/*
+ * Copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ * Copyright (c) 2008 Peter Ross
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_CHANNEL_LAYOUT_H
+#define AVUTIL_CHANNEL_LAYOUT_H
+
+#include <stdint.h>
+
+/**
+ * @file
+ * audio channel layout utility functions
+ */
+
+/**
+ * @addtogroup lavu_audio
+ * @{
+ */
+
+/**
+ * @defgroup channel_masks Audio channel masks
+ * @{
+ */
+#define AV_CH_FRONT_LEFT 0x00000001
+#define AV_CH_FRONT_RIGHT 0x00000002
+#define AV_CH_FRONT_CENTER 0x00000004
+#define AV_CH_LOW_FREQUENCY 0x00000008
+#define AV_CH_BACK_LEFT 0x00000010
+#define AV_CH_BACK_RIGHT 0x00000020
+#define AV_CH_FRONT_LEFT_OF_CENTER 0x00000040
+#define AV_CH_FRONT_RIGHT_OF_CENTER 0x00000080
+#define AV_CH_BACK_CENTER 0x00000100
+#define AV_CH_SIDE_LEFT 0x00000200
+#define AV_CH_SIDE_RIGHT 0x00000400
+#define AV_CH_TOP_CENTER 0x00000800
+#define AV_CH_TOP_FRONT_LEFT 0x00001000
+#define AV_CH_TOP_FRONT_CENTER 0x00002000
+#define AV_CH_TOP_FRONT_RIGHT 0x00004000
+#define AV_CH_TOP_BACK_LEFT 0x00008000
+#define AV_CH_TOP_BACK_CENTER 0x00010000
+#define AV_CH_TOP_BACK_RIGHT 0x00020000
+#define AV_CH_STEREO_LEFT 0x20000000 ///< Stereo downmix.
+#define AV_CH_STEREO_RIGHT 0x40000000 ///< See AV_CH_STEREO_LEFT.
+#define AV_CH_WIDE_LEFT 0x0000000080000000ULL
+#define AV_CH_WIDE_RIGHT 0x0000000100000000ULL
+#define AV_CH_SURROUND_DIRECT_LEFT 0x0000000200000000ULL
+#define AV_CH_SURROUND_DIRECT_RIGHT 0x0000000400000000ULL
+#define AV_CH_LOW_FREQUENCY_2 0x0000000800000000ULL
+
+/** Channel mask value used for AVCodecContext.request_channel_layout
+ to indicate that the user requests the channel order of the decoder output
+ to be the native codec channel order. */
+#define AV_CH_LAYOUT_NATIVE 0x8000000000000000ULL
+
+/**
+ * @}
+ * @defgroup channel_mask_c Audio channel convenience macros
+ * @{
+ * */
+#define AV_CH_LAYOUT_MONO (AV_CH_FRONT_CENTER)
+#define AV_CH_LAYOUT_STEREO (AV_CH_FRONT_LEFT|AV_CH_FRONT_RIGHT)
+#define AV_CH_LAYOUT_2POINT1 (AV_CH_LAYOUT_STEREO|AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_2_1 (AV_CH_LAYOUT_STEREO|AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_SURROUND (AV_CH_LAYOUT_STEREO|AV_CH_FRONT_CENTER)
+#define AV_CH_LAYOUT_3POINT1 (AV_CH_LAYOUT_SURROUND|AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_4POINT0 (AV_CH_LAYOUT_SURROUND|AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_4POINT1 (AV_CH_LAYOUT_4POINT0|AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_2_2 (AV_CH_LAYOUT_STEREO|AV_CH_SIDE_LEFT|AV_CH_SIDE_RIGHT)
+#define AV_CH_LAYOUT_QUAD (AV_CH_LAYOUT_STEREO|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_5POINT0 (AV_CH_LAYOUT_SURROUND|AV_CH_SIDE_LEFT|AV_CH_SIDE_RIGHT)
+#define AV_CH_LAYOUT_5POINT1 (AV_CH_LAYOUT_5POINT0|AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_5POINT0_BACK (AV_CH_LAYOUT_SURROUND|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_5POINT1_BACK (AV_CH_LAYOUT_5POINT0_BACK|AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_6POINT0 (AV_CH_LAYOUT_5POINT0|AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_6POINT0_FRONT (AV_CH_LAYOUT_2_2|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER)
+#define AV_CH_LAYOUT_HEXAGONAL (AV_CH_LAYOUT_5POINT0_BACK|AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_6POINT1 (AV_CH_LAYOUT_5POINT1|AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_6POINT1_BACK (AV_CH_LAYOUT_5POINT1_BACK|AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_6POINT1_FRONT (AV_CH_LAYOUT_6POINT0_FRONT|AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_7POINT0 (AV_CH_LAYOUT_5POINT0|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_7POINT0_FRONT (AV_CH_LAYOUT_5POINT0|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER)
+#define AV_CH_LAYOUT_7POINT1 (AV_CH_LAYOUT_5POINT1|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_7POINT1_WIDE (AV_CH_LAYOUT_5POINT1|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER)
+#define AV_CH_LAYOUT_7POINT1_WIDE_BACK (AV_CH_LAYOUT_5POINT1_BACK|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER)
+#define AV_CH_LAYOUT_OCTAGONAL (AV_CH_LAYOUT_5POINT0|AV_CH_BACK_LEFT|AV_CH_BACK_CENTER|AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_STEREO_DOWNMIX (AV_CH_STEREO_LEFT|AV_CH_STEREO_RIGHT)
+
+enum AVMatrixEncoding {
+ AV_MATRIX_ENCODING_NONE,
+ AV_MATRIX_ENCODING_DOLBY,
+ AV_MATRIX_ENCODING_DPLII,
+ AV_MATRIX_ENCODING_NB
+};
+
+/**
+ * @}
+ */
+
+/**
+ * Return a channel layout id that matches name, or 0 if no match is found.
+ *
+ * name can be one or several of the following notations,
+ * separated by '+' or '|':
+ * - the name of an usual channel layout (mono, stereo, 4.0, quad, 5.0,
+ * 5.0(side), 5.1, 5.1(side), 7.1, 7.1(wide), downmix);
+ * - the name of a single channel (FL, FR, FC, LFE, BL, BR, FLC, FRC, BC,
+ * SL, SR, TC, TFL, TFC, TFR, TBL, TBC, TBR, DL, DR);
+ * - a number of channels, in decimal, optionally followed by 'c', yielding
+ * the default channel layout for that number of channels (@see
+ * av_get_default_channel_layout);
+ * - a channel layout mask, in hexadecimal starting with "0x" (see the
+ * AV_CH_* macros).
+ *
+ * Example: "stereo+FC" = "2+FC" = "2c+1c" = "0x7"
+ */
+uint64_t av_get_channel_layout(const char *name);
+
+/**
+ * Return a description of a channel layout.
+ * If nb_channels is <= 0, it is guessed from the channel_layout.
+ *
+ * @param buf put here the string containing the channel layout
+ * @param buf_size size in bytes of the buffer
+ */
+void av_get_channel_layout_string(char *buf, int buf_size, int nb_channels, uint64_t channel_layout);
+
+/**
+ * Return the number of channels in the channel layout.
+ */
+int av_get_channel_layout_nb_channels(uint64_t channel_layout);
+
+/**
+ * Return default channel layout for a given number of channels.
+ */
+uint64_t av_get_default_channel_layout(int nb_channels);
+
+/**
+ * Get the index of a channel in channel_layout.
+ *
+ * @param channel a channel layout describing exactly one channel which must be
+ * present in channel_layout.
+ *
+ * @return index of channel in channel_layout on success, a negative AVERROR
+ * on error.
+ */
+int av_get_channel_layout_channel_index(uint64_t channel_layout,
+ uint64_t channel);
+
+/**
+ * Get the channel with the given index in channel_layout.
+ */
+uint64_t av_channel_layout_extract_channel(uint64_t channel_layout, int index);
+
+/**
+ * Get the name of a given channel.
+ *
+ * @return channel name on success, NULL on error.
+ */
+const char *av_get_channel_name(uint64_t channel);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_CHANNEL_LAYOUT_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/common.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/common.h
new file mode 100644
index 0000000000..cc4df16e4a
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/common.h
@@ -0,0 +1,406 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * common internal and external API header
+ */
+
+#ifndef AVUTIL_COMMON_H
+#define AVUTIL_COMMON_H
+
+#include <ctype.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <math.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "attributes.h"
+#include "version.h"
+#include "libavutil/avconfig.h"
+
+#if AV_HAVE_BIGENDIAN
+# define AV_NE(be, le) (be)
+#else
+# define AV_NE(be, le) (le)
+#endif
+
+//rounded division & shift
+#define RSHIFT(a,b) ((a) > 0 ? ((a) + ((1<<(b))>>1))>>(b) : ((a) + ((1<<(b))>>1)-1)>>(b))
+/* assume b>0 */
+#define ROUNDED_DIV(a,b) (((a)>0 ? (a) + ((b)>>1) : (a) - ((b)>>1))/(b))
+#define FFABS(a) ((a) >= 0 ? (a) : (-(a)))
+#define FFSIGN(a) ((a) > 0 ? 1 : -1)
+
+#define FFMAX(a,b) ((a) > (b) ? (a) : (b))
+#define FFMAX3(a,b,c) FFMAX(FFMAX(a,b),c)
+#define FFMIN(a,b) ((a) > (b) ? (b) : (a))
+#define FFMIN3(a,b,c) FFMIN(FFMIN(a,b),c)
+
+#define FFSWAP(type,a,b) do{type SWAP_tmp= b; b= a; a= SWAP_tmp;}while(0)
+#define FF_ARRAY_ELEMS(a) (sizeof(a) / sizeof((a)[0]))
+#define FFALIGN(x, a) (((x)+(a)-1)&~((a)-1))
+
+/* misc math functions */
+
+#if FF_API_AV_REVERSE
+extern attribute_deprecated const uint8_t av_reverse[256];
+#endif
+
+#ifdef HAVE_AV_CONFIG_H
+# include "config.h"
+# include "intmath.h"
+#endif
+
+/* Pull in unguarded fallback defines at the end of this file. */
+#include "common.h"
+
+#ifndef av_log2
+av_const int av_log2(unsigned v);
+#endif
+
+#ifndef av_log2_16bit
+av_const int av_log2_16bit(unsigned v);
+#endif
+
+/**
+ * Clip a signed integer value into the amin-amax range.
+ * @param a value to clip
+ * @param amin minimum value of the clip range
+ * @param amax maximum value of the clip range
+ * @return clipped value
+ */
+static av_always_inline av_const int av_clip_c(int a, int amin, int amax)
+{
+ if (a < amin) return amin;
+ else if (a > amax) return amax;
+ else return a;
+}
+
+/**
+ * Clip a signed integer value into the 0-255 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const uint8_t av_clip_uint8_c(int a)
+{
+ if (a&(~0xFF)) return (-a)>>31;
+ else return a;
+}
+
+/**
+ * Clip a signed integer value into the -128,127 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const int8_t av_clip_int8_c(int a)
+{
+ if ((a+0x80) & ~0xFF) return (a>>31) ^ 0x7F;
+ else return a;
+}
+
+/**
+ * Clip a signed integer value into the 0-65535 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const uint16_t av_clip_uint16_c(int a)
+{
+ if (a&(~0xFFFF)) return (-a)>>31;
+ else return a;
+}
+
+/**
+ * Clip a signed integer value into the -32768,32767 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const int16_t av_clip_int16_c(int a)
+{
+ if ((a+0x8000) & ~0xFFFF) return (a>>31) ^ 0x7FFF;
+ else return a;
+}
+
+/**
+ * Clip a signed 64-bit integer value into the -2147483648,2147483647 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const int32_t av_clipl_int32_c(int64_t a)
+{
+ if ((a+0x80000000u) & ~UINT64_C(0xFFFFFFFF)) return (a>>63) ^ 0x7FFFFFFF;
+ else return a;
+}
+
+/**
+ * Clip a signed integer to an unsigned power of two range.
+ * @param a value to clip
+ * @param p bit position to clip at
+ * @return clipped value
+ */
+static av_always_inline av_const unsigned av_clip_uintp2_c(int a, int p)
+{
+ if (a & ~((1<<p) - 1)) return -a >> 31 & ((1<<p) - 1);
+ else return a;
+}
+
+/**
+ * Add two signed 32-bit values with saturation.
+ *
+ * @param a one value
+ * @param b another value
+ * @return sum with signed saturation
+ */
+static av_always_inline int av_sat_add32_c(int a, int b)
+{
+ return av_clipl_int32((int64_t)a + b);
+}
+
+/**
+ * Add a doubled value to another value with saturation at both stages.
+ *
+ * @param a first value
+ * @param b value doubled and added to a
+ * @return sum with signed saturation
+ */
+static av_always_inline int av_sat_dadd32_c(int a, int b)
+{
+ return av_sat_add32(a, av_sat_add32(b, b));
+}
+
+/**
+ * Clip a float value into the amin-amax range.
+ * @param a value to clip
+ * @param amin minimum value of the clip range
+ * @param amax maximum value of the clip range
+ * @return clipped value
+ */
+static av_always_inline av_const float av_clipf_c(float a, float amin, float amax)
+{
+ if (a < amin) return amin;
+ else if (a > amax) return amax;
+ else return a;
+}
+
+/** Compute ceil(log2(x)).
+ * @param x value used to compute ceil(log2(x))
+ * @return computed ceiling of log2(x)
+ */
+static av_always_inline av_const int av_ceil_log2_c(int x)
+{
+ return av_log2((x - 1) << 1);
+}
+
+/**
+ * Count number of bits set to one in x
+ * @param x value to count bits of
+ * @return the number of bits set to one in x
+ */
+static av_always_inline av_const int av_popcount_c(uint32_t x)
+{
+ x -= (x >> 1) & 0x55555555;
+ x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
+ x = (x + (x >> 4)) & 0x0F0F0F0F;
+ x += x >> 8;
+ return (x + (x >> 16)) & 0x3F;
+}
+
+/**
+ * Count number of bits set to one in x
+ * @param x value to count bits of
+ * @return the number of bits set to one in x
+ */
+static av_always_inline av_const int av_popcount64_c(uint64_t x)
+{
+ return av_popcount(x) + av_popcount(x >> 32);
+}
+
+#define MKTAG(a,b,c,d) ((a) | ((b) << 8) | ((c) << 16) | ((unsigned)(d) << 24))
+#define MKBETAG(a,b,c,d) ((d) | ((c) << 8) | ((b) << 16) | ((unsigned)(a) << 24))
+
+/**
+ * Convert a UTF-8 character (up to 4 bytes) to its 32-bit UCS-4 encoded form.
+ *
+ * @param val Output value, must be an lvalue of type uint32_t.
+ * @param GET_BYTE Expression reading one byte from the input.
+ * Evaluated up to 7 times (4 for the currently
+ * assigned Unicode range). With a memory buffer
+ * input, this could be *ptr++.
+ * @param ERROR Expression to be evaluated on invalid input,
+ * typically a goto statement.
+ */
+#define GET_UTF8(val, GET_BYTE, ERROR)\
+ val= GET_BYTE;\
+ {\
+ uint32_t top = (val & 128) >> 1;\
+ if ((val & 0xc0) == 0x80)\
+ ERROR\
+ while (val & top) {\
+ int tmp= GET_BYTE - 128;\
+ if(tmp>>6)\
+ ERROR\
+ val= (val<<6) + tmp;\
+ top <<= 5;\
+ }\
+ val &= (top << 1) - 1;\
+ }
+
+/**
+ * Convert a UTF-16 character (2 or 4 bytes) to its 32-bit UCS-4 encoded form.
+ *
+ * @param val Output value, must be an lvalue of type uint32_t.
+ * @param GET_16BIT Expression returning two bytes of UTF-16 data converted
+ * to native byte order. Evaluated one or two times.
+ * @param ERROR Expression to be evaluated on invalid input,
+ * typically a goto statement.
+ */
+#define GET_UTF16(val, GET_16BIT, ERROR)\
+ val = GET_16BIT;\
+ {\
+ unsigned int hi = val - 0xD800;\
+ if (hi < 0x800) {\
+ val = GET_16BIT - 0xDC00;\
+ if (val > 0x3FFU || hi > 0x3FFU)\
+ ERROR\
+ val += (hi<<10) + 0x10000;\
+ }\
+ }\
+
+/**
+ * @def PUT_UTF8(val, tmp, PUT_BYTE)
+ * Convert a 32-bit Unicode character to its UTF-8 encoded form (up to 4 bytes long).
+ * @param val is an input-only argument and should be of type uint32_t. It holds
+ * a UCS-4 encoded Unicode character that is to be converted to UTF-8. If
+ * val is given as a function it is executed only once.
+ * @param tmp is a temporary variable and should be of type uint8_t. It
+ * represents an intermediate value during conversion that is to be
+ * output by PUT_BYTE.
+ * @param PUT_BYTE writes the converted UTF-8 bytes to any proper destination.
+ * It could be a function or a statement, and uses tmp as the input byte.
+ * For example, PUT_BYTE could be "*output++ = tmp;" PUT_BYTE will be
+ * executed up to 4 times for values in the valid UTF-8 range and up to
+ * 7 times in the general case, depending on the length of the converted
+ * Unicode character.
+ */
+#define PUT_UTF8(val, tmp, PUT_BYTE)\
+ {\
+ int bytes, shift;\
+ uint32_t in = val;\
+ if (in < 0x80) {\
+ tmp = in;\
+ PUT_BYTE\
+ } else {\
+ bytes = (av_log2(in) + 4) / 5;\
+ shift = (bytes - 1) * 6;\
+ tmp = (256 - (256 >> bytes)) | (in >> shift);\
+ PUT_BYTE\
+ while (shift >= 6) {\
+ shift -= 6;\
+ tmp = 0x80 | ((in >> shift) & 0x3f);\
+ PUT_BYTE\
+ }\
+ }\
+ }
+
+/**
+ * @def PUT_UTF16(val, tmp, PUT_16BIT)
+ * Convert a 32-bit Unicode character to its UTF-16 encoded form (2 or 4 bytes).
+ * @param val is an input-only argument and should be of type uint32_t. It holds
+ * a UCS-4 encoded Unicode character that is to be converted to UTF-16. If
+ * val is given as a function it is executed only once.
+ * @param tmp is a temporary variable and should be of type uint16_t. It
+ * represents an intermediate value during conversion that is to be
+ * output by PUT_16BIT.
+ * @param PUT_16BIT writes the converted UTF-16 data to any proper destination
+ * in desired endianness. It could be a function or a statement, and uses tmp
+ * as the input byte. For example, PUT_BYTE could be "*output++ = tmp;"
+ * PUT_BYTE will be executed 1 or 2 times depending on input character.
+ */
+#define PUT_UTF16(val, tmp, PUT_16BIT)\
+ {\
+ uint32_t in = val;\
+ if (in < 0x10000) {\
+ tmp = in;\
+ PUT_16BIT\
+ } else {\
+ tmp = 0xD800 | ((in - 0x10000) >> 10);\
+ PUT_16BIT\
+ tmp = 0xDC00 | ((in - 0x10000) & 0x3FF);\
+ PUT_16BIT\
+ }\
+ }\
+
+
+
+#include "mem.h"
+
+#ifdef HAVE_AV_CONFIG_H
+# include "internal.h"
+#endif /* HAVE_AV_CONFIG_H */
+
+#endif /* AVUTIL_COMMON_H */
+
+/*
+ * The following definitions are outside the multiple inclusion guard
+ * to ensure they are immediately available in intmath.h.
+ */
+
+#ifndef av_ceil_log2
+# define av_ceil_log2 av_ceil_log2_c
+#endif
+#ifndef av_clip
+# define av_clip av_clip_c
+#endif
+#ifndef av_clip_uint8
+# define av_clip_uint8 av_clip_uint8_c
+#endif
+#ifndef av_clip_int8
+# define av_clip_int8 av_clip_int8_c
+#endif
+#ifndef av_clip_uint16
+# define av_clip_uint16 av_clip_uint16_c
+#endif
+#ifndef av_clip_int16
+# define av_clip_int16 av_clip_int16_c
+#endif
+#ifndef av_clipl_int32
+# define av_clipl_int32 av_clipl_int32_c
+#endif
+#ifndef av_clip_uintp2
+# define av_clip_uintp2 av_clip_uintp2_c
+#endif
+#ifndef av_sat_add32
+# define av_sat_add32 av_sat_add32_c
+#endif
+#ifndef av_sat_dadd32
+# define av_sat_dadd32 av_sat_dadd32_c
+#endif
+#ifndef av_clipf
+# define av_clipf av_clipf_c
+#endif
+#ifndef av_popcount
+# define av_popcount av_popcount_c
+#endif
+#ifndef av_popcount64
+# define av_popcount64 av_popcount64_c
+#endif
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/cpu.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/cpu.h
new file mode 100644
index 0000000000..4929512c66
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/cpu.h
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2000, 2001, 2002 Fabrice Bellard
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_CPU_H
+#define AVUTIL_CPU_H
+
+#include "version.h"
+
+#define AV_CPU_FLAG_FORCE 0x80000000 /* force usage of selected flags (OR) */
+
+ /* lower 16 bits - CPU features */
+#define AV_CPU_FLAG_MMX 0x0001 ///< standard MMX
+#define AV_CPU_FLAG_MMXEXT 0x0002 ///< SSE integer functions or AMD MMX ext
+#if FF_API_CPU_FLAG_MMX2
+#define AV_CPU_FLAG_MMX2 0x0002 ///< SSE integer functions or AMD MMX ext
+#endif
+#define AV_CPU_FLAG_3DNOW 0x0004 ///< AMD 3DNOW
+#define AV_CPU_FLAG_SSE 0x0008 ///< SSE functions
+#define AV_CPU_FLAG_SSE2 0x0010 ///< PIV SSE2 functions
+#define AV_CPU_FLAG_SSE2SLOW 0x40000000 ///< SSE2 supported, but usually not faster
+#define AV_CPU_FLAG_3DNOWEXT 0x0020 ///< AMD 3DNowExt
+#define AV_CPU_FLAG_SSE3 0x0040 ///< Prescott SSE3 functions
+#define AV_CPU_FLAG_SSE3SLOW 0x20000000 ///< SSE3 supported, but usually not faster
+#define AV_CPU_FLAG_SSSE3 0x0080 ///< Conroe SSSE3 functions
+#define AV_CPU_FLAG_ATOM 0x10000000 ///< Atom processor, some SSSE3 instructions are slower
+#define AV_CPU_FLAG_SSE4 0x0100 ///< Penryn SSE4.1 functions
+#define AV_CPU_FLAG_SSE42 0x0200 ///< Nehalem SSE4.2 functions
+#define AV_CPU_FLAG_AVX 0x4000 ///< AVX functions: requires OS support even if YMM registers aren't used
+#define AV_CPU_FLAG_XOP 0x0400 ///< Bulldozer XOP functions
+#define AV_CPU_FLAG_FMA4 0x0800 ///< Bulldozer FMA4 functions
+#define AV_CPU_FLAG_CMOV 0x1000 ///< i686 cmov
+
+#define AV_CPU_FLAG_ALTIVEC 0x0001 ///< standard
+
+#define AV_CPU_FLAG_ARMV5TE (1 << 0)
+#define AV_CPU_FLAG_ARMV6 (1 << 1)
+#define AV_CPU_FLAG_ARMV6T2 (1 << 2)
+#define AV_CPU_FLAG_VFP (1 << 3)
+#define AV_CPU_FLAG_VFPV3 (1 << 4)
+#define AV_CPU_FLAG_NEON (1 << 5)
+
+/**
+ * Return the flags which specify extensions supported by the CPU.
+ */
+int av_get_cpu_flags(void);
+
+/**
+ * Set a mask on flags returned by av_get_cpu_flags().
+ * This function is mainly useful for testing.
+ *
+ * @warning this function is not thread safe.
+ */
+void av_set_cpu_flags_mask(int mask);
+
+/**
+ * Parse CPU flags from a string.
+ *
+ * @return a combination of AV_CPU_* flags, negative on error.
+ */
+int av_parse_cpu_flags(const char *s);
+
+/* The following CPU-specific functions shall not be called directly. */
+int ff_get_cpu_flags_arm(void);
+int ff_get_cpu_flags_ppc(void);
+int ff_get_cpu_flags_x86(void);
+
+#endif /* AVUTIL_CPU_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/crc.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/crc.h
new file mode 100644
index 0000000000..0540619d20
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/crc.h
@@ -0,0 +1,74 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_CRC_H
+#define AVUTIL_CRC_H
+
+#include <stdint.h>
+#include <stddef.h>
+#include "attributes.h"
+
+typedef uint32_t AVCRC;
+
+typedef enum {
+ AV_CRC_8_ATM,
+ AV_CRC_16_ANSI,
+ AV_CRC_16_CCITT,
+ AV_CRC_32_IEEE,
+ AV_CRC_32_IEEE_LE, /*< reversed bitorder version of AV_CRC_32_IEEE */
+ AV_CRC_MAX, /*< Not part of public API! Do not use outside libavutil. */
+}AVCRCId;
+
+/**
+ * Initialize a CRC table.
+ * @param ctx must be an array of size sizeof(AVCRC)*257 or sizeof(AVCRC)*1024
+ * @param le If 1, the lowest bit represents the coefficient for the highest
+ * exponent of the corresponding polynomial (both for poly and
+ * actual CRC).
+ * If 0, you must swap the CRC parameter and the result of av_crc
+ * if you need the standard representation (can be simplified in
+ * most cases to e.g. bswap16):
+ * av_bswap32(crc << (32-bits))
+ * @param bits number of bits for the CRC
+ * @param poly generator polynomial without the x**bits coefficient, in the
+ * representation as specified by le
+ * @param ctx_size size of ctx in bytes
+ * @return <0 on failure
+ */
+int av_crc_init(AVCRC *ctx, int le, int bits, uint32_t poly, int ctx_size);
+
+/**
+ * Get an initialized standard CRC table.
+ * @param crc_id ID of a standard CRC
+ * @return a pointer to the CRC table or NULL on failure
+ */
+const AVCRC *av_crc_get_table(AVCRCId crc_id);
+
+/**
+ * Calculate the CRC of a block.
+ * @param crc CRC of previous blocks if any or initial value for CRC
+ * @return CRC updated with the data from the given block
+ *
+ * @see av_crc_init() "le" parameter
+ */
+uint32_t av_crc(const AVCRC *ctx, uint32_t crc,
+ const uint8_t *buffer, size_t length) av_pure;
+
+#endif /* AVUTIL_CRC_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/dict.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/dict.h
new file mode 100644
index 0000000000..492da9a41c
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/dict.h
@@ -0,0 +1,129 @@
+/*
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * Public dictionary API.
+ */
+
+#ifndef AVUTIL_DICT_H
+#define AVUTIL_DICT_H
+
+/**
+ * @addtogroup lavu_dict AVDictionary
+ * @ingroup lavu_data
+ *
+ * @brief Simple key:value store
+ *
+ * @{
+ * Dictionaries are used for storing key:value pairs. To create
+ * an AVDictionary, simply pass an address of a NULL pointer to
+ * av_dict_set(). NULL can be used as an empty dictionary wherever
+ * a pointer to an AVDictionary is required.
+ * Use av_dict_get() to retrieve an entry or iterate over all
+ * entries and finally av_dict_free() to free the dictionary
+ * and all its contents.
+ *
+ * @code
+ * AVDictionary *d = NULL; // "create" an empty dictionary
+ * av_dict_set(&d, "foo", "bar", 0); // add an entry
+ *
+ * char *k = av_strdup("key"); // if your strings are already allocated,
+ * char *v = av_strdup("value"); // you can avoid copying them like this
+ * av_dict_set(&d, k, v, AV_DICT_DONT_STRDUP_KEY | AV_DICT_DONT_STRDUP_VAL);
+ *
+ * AVDictionaryEntry *t = NULL;
+ * while (t = av_dict_get(d, "", t, AV_DICT_IGNORE_SUFFIX)) {
+ * <....> // iterate over all entries in d
+ * }
+ *
+ * av_dict_free(&d);
+ * @endcode
+ *
+ */
+
+#define AV_DICT_MATCH_CASE 1
+#define AV_DICT_IGNORE_SUFFIX 2
+#define AV_DICT_DONT_STRDUP_KEY 4 /**< Take ownership of a key that's been
+ allocated with av_malloc() and children. */
+#define AV_DICT_DONT_STRDUP_VAL 8 /**< Take ownership of a value that's been
+ allocated with av_malloc() and chilren. */
+#define AV_DICT_DONT_OVERWRITE 16 ///< Don't overwrite existing entries.
+#define AV_DICT_APPEND 32 /**< If the entry already exists, append to it. Note that no
+ delimiter is added, the strings are simply concatenated. */
+
+typedef struct AVDictionaryEntry {
+ char *key;
+ char *value;
+} AVDictionaryEntry;
+
+typedef struct AVDictionary AVDictionary;
+
+/**
+ * Get a dictionary entry with matching key.
+ *
+ * @param prev Set to the previous matching element to find the next.
+ * If set to NULL the first matching element is returned.
+ * @param flags Allows case as well as suffix-insensitive comparisons.
+ * @return Found entry or NULL, changing key or value leads to undefined behavior.
+ */
+AVDictionaryEntry *
+av_dict_get(AVDictionary *m, const char *key, const AVDictionaryEntry *prev, int flags);
+
+/**
+ * Get number of entries in dictionary.
+ *
+ * @param m dictionary
+ * @return number of entries in dictionary
+ */
+int av_dict_count(const AVDictionary *m);
+
+/**
+ * Set the given entry in *pm, overwriting an existing entry.
+ *
+ * @param pm pointer to a pointer to a dictionary struct. If *pm is NULL
+ * a dictionary struct is allocated and put in *pm.
+ * @param key entry key to add to *pm (will be av_strduped depending on flags)
+ * @param value entry value to add to *pm (will be av_strduped depending on flags).
+ * Passing a NULL value will cause an existing entry to be deleted.
+ * @return >= 0 on success otherwise an error code <0
+ */
+int av_dict_set(AVDictionary **pm, const char *key, const char *value, int flags);
+
+/**
+ * Copy entries from one AVDictionary struct into another.
+ * @param dst pointer to a pointer to a AVDictionary struct. If *dst is NULL,
+ * this function will allocate a struct for you and put it in *dst
+ * @param src pointer to source AVDictionary struct
+ * @param flags flags to use when setting entries in *dst
+ * @note metadata is read using the AV_DICT_IGNORE_SUFFIX flag
+ */
+void av_dict_copy(AVDictionary **dst, AVDictionary *src, int flags);
+
+/**
+ * Free all the memory allocated for an AVDictionary struct
+ * and all keys and values.
+ */
+void av_dict_free(AVDictionary **m);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_DICT_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/error.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/error.h
new file mode 100644
index 0000000000..3dfd8807fe
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/error.h
@@ -0,0 +1,83 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * error code definitions
+ */
+
+#ifndef AVUTIL_ERROR_H
+#define AVUTIL_ERROR_H
+
+#include <errno.h>
+#include <stddef.h>
+#include "avutil.h"
+
+/**
+ * @addtogroup lavu_error
+ *
+ * @{
+ */
+
+
+/* error handling */
+#if EDOM > 0
+#define AVERROR(e) (-(e)) ///< Returns a negative error code from a POSIX error code, to return from library functions.
+#define AVUNERROR(e) (-(e)) ///< Returns a POSIX error code from a library function error return value.
+#else
+/* Some platforms have E* and errno already negated. */
+#define AVERROR(e) (e)
+#define AVUNERROR(e) (e)
+#endif
+
+#define AVERROR_BSF_NOT_FOUND (-0x39acbd08) ///< Bitstream filter not found
+#define AVERROR_DECODER_NOT_FOUND (-0x3cbabb08) ///< Decoder not found
+#define AVERROR_DEMUXER_NOT_FOUND (-0x32babb08) ///< Demuxer not found
+#define AVERROR_ENCODER_NOT_FOUND (-0x3cb1ba08) ///< Encoder not found
+#define AVERROR_EOF (-0x5fb9b0bb) ///< End of file
+#define AVERROR_EXIT (-0x2bb6a7bb) ///< Immediate exit was requested; the called function should not be restarted
+#define AVERROR_FILTER_NOT_FOUND (-0x33b6b908) ///< Filter not found
+#define AVERROR_INVALIDDATA (-0x3ebbb1b7) ///< Invalid data found when processing input
+#define AVERROR_MUXER_NOT_FOUND (-0x27aab208) ///< Muxer not found
+#define AVERROR_OPTION_NOT_FOUND (-0x2bafb008) ///< Option not found
+#define AVERROR_PATCHWELCOME (-0x3aa8beb0) ///< Not yet implemented in Libav, patches welcome
+#define AVERROR_PROTOCOL_NOT_FOUND (-0x30adaf08) ///< Protocol not found
+#define AVERROR_STREAM_NOT_FOUND (-0x2dabac08) ///< Stream not found
+#define AVERROR_BUG (-0x5fb8aabe) ///< Bug detected, please report the issue
+#define AVERROR_UNKNOWN (-0x31b4b1ab) ///< Unknown error, typically from an external library
+#define AVERROR_EXPERIMENTAL (-0x2bb2afa8) ///< Requested feature is flagged experimental. Set strict_std_compliance if you really want to use it.
+
+/**
+ * Put a description of the AVERROR code errnum in errbuf.
+ * In case of failure the global variable errno is set to indicate the
+ * error. Even in case of failure av_strerror() will print a generic
+ * error message indicating the errnum provided to errbuf.
+ *
+ * @param errnum error code to describe
+ * @param errbuf buffer to which description is written
+ * @param errbuf_size the size in bytes of errbuf
+ * @return 0 on success, a negative value if a description for errnum
+ * cannot be found
+ */
+int av_strerror(int errnum, char *errbuf, size_t errbuf_size);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_ERROR_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/eval.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/eval.h
new file mode 100644
index 0000000000..ccb29e7a33
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/eval.h
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2002 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * simple arithmetic expression evaluator
+ */
+
+#ifndef AVUTIL_EVAL_H
+#define AVUTIL_EVAL_H
+
+#include "avutil.h"
+
+typedef struct AVExpr AVExpr;
+
+/**
+ * Parse and evaluate an expression.
+ * Note, this is significantly slower than av_expr_eval().
+ *
+ * @param res a pointer to a double where is put the result value of
+ * the expression, or NAN in case of error
+ * @param s expression as a zero terminated string, for example "1+2^3+5*5+sin(2/3)"
+ * @param const_names NULL terminated array of zero terminated strings of constant identifiers, for example {"PI", "E", 0}
+ * @param const_values a zero terminated array of values for the identifiers from const_names
+ * @param func1_names NULL terminated array of zero terminated strings of funcs1 identifiers
+ * @param funcs1 NULL terminated array of function pointers for functions which take 1 argument
+ * @param func2_names NULL terminated array of zero terminated strings of funcs2 identifiers
+ * @param funcs2 NULL terminated array of function pointers for functions which take 2 arguments
+ * @param opaque a pointer which will be passed to all functions from funcs1 and funcs2
+ * @param log_ctx parent logging context
+ * @return 0 in case of success, a negative value corresponding to an
+ * AVERROR code otherwise
+ */
+int av_expr_parse_and_eval(double *res, const char *s,
+ const char * const *const_names, const double *const_values,
+ const char * const *func1_names, double (* const *funcs1)(void *, double),
+ const char * const *func2_names, double (* const *funcs2)(void *, double, double),
+ void *opaque, int log_offset, void *log_ctx);
+
+/**
+ * Parse an expression.
+ *
+ * @param expr a pointer where is put an AVExpr containing the parsed
+ * value in case of successful parsing, or NULL otherwise.
+ * The pointed to AVExpr must be freed with av_expr_free() by the user
+ * when it is not needed anymore.
+ * @param s expression as a zero terminated string, for example "1+2^3+5*5+sin(2/3)"
+ * @param const_names NULL terminated array of zero terminated strings of constant identifiers, for example {"PI", "E", 0}
+ * @param func1_names NULL terminated array of zero terminated strings of funcs1 identifiers
+ * @param funcs1 NULL terminated array of function pointers for functions which take 1 argument
+ * @param func2_names NULL terminated array of zero terminated strings of funcs2 identifiers
+ * @param funcs2 NULL terminated array of function pointers for functions which take 2 arguments
+ * @param log_ctx parent logging context
+ * @return 0 in case of success, a negative value corresponding to an
+ * AVERROR code otherwise
+ */
+int av_expr_parse(AVExpr **expr, const char *s,
+ const char * const *const_names,
+ const char * const *func1_names, double (* const *funcs1)(void *, double),
+ const char * const *func2_names, double (* const *funcs2)(void *, double, double),
+ int log_offset, void *log_ctx);
+
+/**
+ * Evaluate a previously parsed expression.
+ *
+ * @param const_values a zero terminated array of values for the identifiers from av_expr_parse() const_names
+ * @param opaque a pointer which will be passed to all functions from funcs1 and funcs2
+ * @return the value of the expression
+ */
+double av_expr_eval(AVExpr *e, const double *const_values, void *opaque);
+
+/**
+ * Free a parsed expression previously created with av_expr_parse().
+ */
+void av_expr_free(AVExpr *e);
+
+/**
+ * Parse the string in numstr and return its value as a double. If
+ * the string is empty, contains only whitespaces, or does not contain
+ * an initial substring that has the expected syntax for a
+ * floating-point number, no conversion is performed. In this case,
+ * returns a value of zero and the value returned in tail is the value
+ * of numstr.
+ *
+ * @param numstr a string representing a number, may contain one of
+ * the International System number postfixes, for example 'K', 'M',
+ * 'G'. If 'i' is appended after the postfix, powers of 2 are used
+ * instead of powers of 10. The 'B' postfix multiplies the value for
+ * 8, and can be appended after another postfix or used alone. This
+ * allows using for example 'KB', 'MiB', 'G' and 'B' as postfix.
+ * @param tail if non-NULL puts here the pointer to the char next
+ * after the last parsed character
+ */
+double av_strtod(const char *numstr, char **tail);
+
+#endif /* AVUTIL_EVAL_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/fifo.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/fifo.h
new file mode 100644
index 0000000000..ea30f5d2bd
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/fifo.h
@@ -0,0 +1,131 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * a very simple circular buffer FIFO implementation
+ */
+
+#ifndef AVUTIL_FIFO_H
+#define AVUTIL_FIFO_H
+
+#include <stdint.h>
+#include "avutil.h"
+#include "attributes.h"
+
+typedef struct AVFifoBuffer {
+ uint8_t *buffer;
+ uint8_t *rptr, *wptr, *end;
+ uint32_t rndx, wndx;
+} AVFifoBuffer;
+
+/**
+ * Initialize an AVFifoBuffer.
+ * @param size of FIFO
+ * @return AVFifoBuffer or NULL in case of memory allocation failure
+ */
+AVFifoBuffer *av_fifo_alloc(unsigned int size);
+
+/**
+ * Free an AVFifoBuffer.
+ * @param f AVFifoBuffer to free
+ */
+void av_fifo_free(AVFifoBuffer *f);
+
+/**
+ * Reset the AVFifoBuffer to the state right after av_fifo_alloc, in particular it is emptied.
+ * @param f AVFifoBuffer to reset
+ */
+void av_fifo_reset(AVFifoBuffer *f);
+
+/**
+ * Return the amount of data in bytes in the AVFifoBuffer, that is the
+ * amount of data you can read from it.
+ * @param f AVFifoBuffer to read from
+ * @return size
+ */
+int av_fifo_size(AVFifoBuffer *f);
+
+/**
+ * Return the amount of space in bytes in the AVFifoBuffer, that is the
+ * amount of data you can write into it.
+ * @param f AVFifoBuffer to write into
+ * @return size
+ */
+int av_fifo_space(AVFifoBuffer *f);
+
+/**
+ * Feed data from an AVFifoBuffer to a user-supplied callback.
+ * @param f AVFifoBuffer to read from
+ * @param buf_size number of bytes to read
+ * @param func generic read function
+ * @param dest data destination
+ */
+int av_fifo_generic_read(AVFifoBuffer *f, void *dest, int buf_size, void (*func)(void*, void*, int));
+
+/**
+ * Feed data from a user-supplied callback to an AVFifoBuffer.
+ * @param f AVFifoBuffer to write to
+ * @param src data source; non-const since it may be used as a
+ * modifiable context by the function defined in func
+ * @param size number of bytes to write
+ * @param func generic write function; the first parameter is src,
+ * the second is dest_buf, the third is dest_buf_size.
+ * func must return the number of bytes written to dest_buf, or <= 0 to
+ * indicate no more data available to write.
+ * If func is NULL, src is interpreted as a simple byte array for source data.
+ * @return the number of bytes written to the FIFO
+ */
+int av_fifo_generic_write(AVFifoBuffer *f, void *src, int size, int (*func)(void*, void*, int));
+
+/**
+ * Resize an AVFifoBuffer.
+ * @param f AVFifoBuffer to resize
+ * @param size new AVFifoBuffer size in bytes
+ * @return <0 for failure, >=0 otherwise
+ */
+int av_fifo_realloc2(AVFifoBuffer *f, unsigned int size);
+
+/**
+ * Read and discard the specified amount of data from an AVFifoBuffer.
+ * @param f AVFifoBuffer to read from
+ * @param size amount of data to read in bytes
+ */
+void av_fifo_drain(AVFifoBuffer *f, int size);
+
+/**
+ * Return a pointer to the data stored in a FIFO buffer at a certain offset.
+ * The FIFO buffer is not modified.
+ *
+ * @param f AVFifoBuffer to peek at, f must be non-NULL
+ * @param offs an offset in bytes, its absolute value must be less
+ * than the used buffer size or the returned pointer will
+ * point outside to the buffer data.
+ * The used buffer size can be checked with av_fifo_size().
+ */
+static inline uint8_t *av_fifo_peek2(const AVFifoBuffer *f, int offs)
+{
+ uint8_t *ptr = f->rptr + offs;
+ if (ptr >= f->end)
+ ptr = f->buffer + (ptr - f->end);
+ else if (ptr < f->buffer)
+ ptr = f->end - (f->buffer - ptr);
+ return ptr;
+}
+
+#endif /* AVUTIL_FIFO_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/file.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/file.h
new file mode 100644
index 0000000000..e3f02a8308
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/file.h
@@ -0,0 +1,54 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_FILE_H
+#define AVUTIL_FILE_H
+
+#include <stdint.h>
+
+#include "avutil.h"
+
+/**
+ * @file
+ * Misc file utilities.
+ */
+
+/**
+ * Read the file with name filename, and put its content in a newly
+ * allocated buffer or map it with mmap() when available.
+ * In case of success set *bufptr to the read or mmapped buffer, and
+ * *size to the size in bytes of the buffer in *bufptr.
+ * The returned buffer must be released with av_file_unmap().
+ *
+ * @param log_offset loglevel offset used for logging
+ * @param log_ctx context used for logging
+ * @return a non negative number in case of success, a negative value
+ * corresponding to an AVERROR error code in case of failure
+ */
+int av_file_map(const char *filename, uint8_t **bufptr, size_t *size,
+ int log_offset, void *log_ctx);
+
+/**
+ * Unmap or free the buffer bufptr created by av_file_map().
+ *
+ * @param size size in bytes of bufptr, must be the same as returned
+ * by av_file_map()
+ */
+void av_file_unmap(uint8_t *bufptr, size_t size);
+
+#endif /* AVUTIL_FILE_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/imgutils.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/imgutils.h
new file mode 100644
index 0000000000..71510132ae
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/imgutils.h
@@ -0,0 +1,138 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_IMGUTILS_H
+#define AVUTIL_IMGUTILS_H
+
+/**
+ * @file
+ * misc image utilities
+ *
+ * @addtogroup lavu_picture
+ * @{
+ */
+
+#include "avutil.h"
+#include "pixdesc.h"
+
+/**
+ * Compute the max pixel step for each plane of an image with a
+ * format described by pixdesc.
+ *
+ * The pixel step is the distance in bytes between the first byte of
+ * the group of bytes which describe a pixel component and the first
+ * byte of the successive group in the same plane for the same
+ * component.
+ *
+ * @param max_pixsteps an array which is filled with the max pixel step
+ * for each plane. Since a plane may contain different pixel
+ * components, the computed max_pixsteps[plane] is relative to the
+ * component in the plane with the max pixel step.
+ * @param max_pixstep_comps an array which is filled with the component
+ * for each plane which has the max pixel step. May be NULL.
+ */
+void av_image_fill_max_pixsteps(int max_pixsteps[4], int max_pixstep_comps[4],
+ const AVPixFmtDescriptor *pixdesc);
+
+/**
+ * Compute the size of an image line with format pix_fmt and width
+ * width for the plane plane.
+ *
+ * @return the computed size in bytes
+ */
+int av_image_get_linesize(enum AVPixelFormat pix_fmt, int width, int plane);
+
+/**
+ * Fill plane linesizes for an image with pixel format pix_fmt and
+ * width width.
+ *
+ * @param linesizes array to be filled with the linesize for each plane
+ * @return >= 0 in case of success, a negative error code otherwise
+ */
+int av_image_fill_linesizes(int linesizes[4], enum AVPixelFormat pix_fmt, int width);
+
+/**
+ * Fill plane data pointers for an image with pixel format pix_fmt and
+ * height height.
+ *
+ * @param data pointers array to be filled with the pointer for each image plane
+ * @param ptr the pointer to a buffer which will contain the image
+ * @param linesizes the array containing the linesize for each
+ * plane, should be filled by av_image_fill_linesizes()
+ * @return the size in bytes required for the image buffer, a negative
+ * error code in case of failure
+ */
+int av_image_fill_pointers(uint8_t *data[4], enum AVPixelFormat pix_fmt, int height,
+ uint8_t *ptr, const int linesizes[4]);
+
+/**
+ * Allocate an image with size w and h and pixel format pix_fmt, and
+ * fill pointers and linesizes accordingly.
+ * The allocated image buffer has to be freed by using
+ * av_freep(&pointers[0]).
+ *
+ * @param align the value to use for buffer size alignment
+ * @return the size in bytes required for the image buffer, a negative
+ * error code in case of failure
+ */
+int av_image_alloc(uint8_t *pointers[4], int linesizes[4],
+ int w, int h, enum AVPixelFormat pix_fmt, int align);
+
+/**
+ * Copy image plane from src to dst.
+ * That is, copy "height" number of lines of "bytewidth" bytes each.
+ * The first byte of each successive line is separated by *_linesize
+ * bytes.
+ *
+ * @param dst_linesize linesize for the image plane in dst
+ * @param src_linesize linesize for the image plane in src
+ */
+void av_image_copy_plane(uint8_t *dst, int dst_linesize,
+ const uint8_t *src, int src_linesize,
+ int bytewidth, int height);
+
+/**
+ * Copy image in src_data to dst_data.
+ *
+ * @param dst_linesizes linesizes for the image in dst_data
+ * @param src_linesizes linesizes for the image in src_data
+ */
+void av_image_copy(uint8_t *dst_data[4], int dst_linesizes[4],
+ const uint8_t *src_data[4], const int src_linesizes[4],
+ enum AVPixelFormat pix_fmt, int width, int height);
+
+/**
+ * Check if the given dimension of an image is valid, meaning that all
+ * bytes of the image can be addressed with a signed int.
+ *
+ * @param w the width of the picture
+ * @param h the height of the picture
+ * @param log_offset the offset to sum to the log level for logging with log_ctx
+ * @param log_ctx the parent logging context, it may be NULL
+ * @return >= 0 if valid, a negative error code otherwise
+ */
+int av_image_check_size(unsigned int w, unsigned int h, int log_offset, void *log_ctx);
+
+int avpriv_set_systematic_pal2(uint32_t pal[256], enum AVPixelFormat pix_fmt);
+
+/**
+ * @}
+ */
+
+
+#endif /* AVUTIL_IMGUTILS_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/intfloat.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/intfloat.h
new file mode 100644
index 0000000000..38d26ad87e
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/intfloat.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2011 Mans Rullgard
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_INTFLOAT_H
+#define AVUTIL_INTFLOAT_H
+
+#include <stdint.h>
+#include "attributes.h"
+
+union av_intfloat32 {
+ uint32_t i;
+ float f;
+};
+
+union av_intfloat64 {
+ uint64_t i;
+ double f;
+};
+
+/**
+ * Reinterpret a 32-bit integer as a float.
+ */
+static av_always_inline float av_int2float(uint32_t i)
+{
+ union av_intfloat32 v;
+ v.i = i;
+ return v.f;
+}
+
+/**
+ * Reinterpret a float as a 32-bit integer.
+ */
+static av_always_inline uint32_t av_float2int(float f)
+{
+ union av_intfloat32 v;
+ v.f = f;
+ return v.i;
+}
+
+/**
+ * Reinterpret a 64-bit integer as a double.
+ */
+static av_always_inline double av_int2double(uint64_t i)
+{
+ union av_intfloat64 v;
+ v.i = i;
+ return v.f;
+}
+
+/**
+ * Reinterpret a double as a 64-bit integer.
+ */
+static av_always_inline uint64_t av_double2int(double f)
+{
+ union av_intfloat64 v;
+ v.f = f;
+ return v.i;
+}
+
+#endif /* AVUTIL_INTFLOAT_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/intfloat_readwrite.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/intfloat_readwrite.h
new file mode 100644
index 0000000000..f093b92cd2
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/intfloat_readwrite.h
@@ -0,0 +1,40 @@
+/*
+ * copyright (c) 2005 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_INTFLOAT_READWRITE_H
+#define AVUTIL_INTFLOAT_READWRITE_H
+
+#include <stdint.h>
+#include "attributes.h"
+
+/* IEEE 80 bits extended float */
+typedef struct AVExtFloat {
+ uint8_t exponent[2];
+ uint8_t mantissa[8];
+} AVExtFloat;
+
+attribute_deprecated double av_int2dbl(int64_t v) av_const;
+attribute_deprecated float av_int2flt(int32_t v) av_const;
+attribute_deprecated double av_ext2dbl(const AVExtFloat ext) av_const;
+attribute_deprecated int64_t av_dbl2int(double d) av_const;
+attribute_deprecated int32_t av_flt2int(float d) av_const;
+attribute_deprecated AVExtFloat av_dbl2ext(double d) av_const;
+
+#endif /* AVUTIL_INTFLOAT_READWRITE_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/intreadwrite.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/intreadwrite.h
new file mode 100644
index 0000000000..f77fd60f38
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/intreadwrite.h
@@ -0,0 +1,549 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_INTREADWRITE_H
+#define AVUTIL_INTREADWRITE_H
+
+#include <stdint.h>
+#include "libavutil/avconfig.h"
+#include "attributes.h"
+#include "bswap.h"
+
+typedef union {
+ uint64_t u64;
+ uint32_t u32[2];
+ uint16_t u16[4];
+ uint8_t u8 [8];
+ double f64;
+ float f32[2];
+} av_alias av_alias64;
+
+typedef union {
+ uint32_t u32;
+ uint16_t u16[2];
+ uint8_t u8 [4];
+ float f32;
+} av_alias av_alias32;
+
+typedef union {
+ uint16_t u16;
+ uint8_t u8 [2];
+} av_alias av_alias16;
+
+/*
+ * Arch-specific headers can provide any combination of
+ * AV_[RW][BLN](16|24|32|64) and AV_(COPY|SWAP|ZERO)(64|128) macros.
+ * Preprocessor symbols must be defined, even if these are implemented
+ * as inline functions.
+ */
+
+#ifdef HAVE_AV_CONFIG_H
+
+#include "config.h"
+
+#if ARCH_ARM
+# include "arm/intreadwrite.h"
+#elif ARCH_AVR32
+# include "avr32/intreadwrite.h"
+#elif ARCH_MIPS
+# include "mips/intreadwrite.h"
+#elif ARCH_PPC
+# include "ppc/intreadwrite.h"
+#elif ARCH_TOMI
+# include "tomi/intreadwrite.h"
+#elif ARCH_X86
+# include "x86/intreadwrite.h"
+#endif
+
+#endif /* HAVE_AV_CONFIG_H */
+
+/*
+ * Map AV_RNXX <-> AV_R[BL]XX for all variants provided by per-arch headers.
+ */
+
+#if AV_HAVE_BIGENDIAN
+
+# if defined(AV_RN16) && !defined(AV_RB16)
+# define AV_RB16(p) AV_RN16(p)
+# elif !defined(AV_RN16) && defined(AV_RB16)
+# define AV_RN16(p) AV_RB16(p)
+# endif
+
+# if defined(AV_WN16) && !defined(AV_WB16)
+# define AV_WB16(p, v) AV_WN16(p, v)
+# elif !defined(AV_WN16) && defined(AV_WB16)
+# define AV_WN16(p, v) AV_WB16(p, v)
+# endif
+
+# if defined(AV_RN24) && !defined(AV_RB24)
+# define AV_RB24(p) AV_RN24(p)
+# elif !defined(AV_RN24) && defined(AV_RB24)
+# define AV_RN24(p) AV_RB24(p)
+# endif
+
+# if defined(AV_WN24) && !defined(AV_WB24)
+# define AV_WB24(p, v) AV_WN24(p, v)
+# elif !defined(AV_WN24) && defined(AV_WB24)
+# define AV_WN24(p, v) AV_WB24(p, v)
+# endif
+
+# if defined(AV_RN32) && !defined(AV_RB32)
+# define AV_RB32(p) AV_RN32(p)
+# elif !defined(AV_RN32) && defined(AV_RB32)
+# define AV_RN32(p) AV_RB32(p)
+# endif
+
+# if defined(AV_WN32) && !defined(AV_WB32)
+# define AV_WB32(p, v) AV_WN32(p, v)
+# elif !defined(AV_WN32) && defined(AV_WB32)
+# define AV_WN32(p, v) AV_WB32(p, v)
+# endif
+
+# if defined(AV_RN64) && !defined(AV_RB64)
+# define AV_RB64(p) AV_RN64(p)
+# elif !defined(AV_RN64) && defined(AV_RB64)
+# define AV_RN64(p) AV_RB64(p)
+# endif
+
+# if defined(AV_WN64) && !defined(AV_WB64)
+# define AV_WB64(p, v) AV_WN64(p, v)
+# elif !defined(AV_WN64) && defined(AV_WB64)
+# define AV_WN64(p, v) AV_WB64(p, v)
+# endif
+
+#else /* AV_HAVE_BIGENDIAN */
+
+# if defined(AV_RN16) && !defined(AV_RL16)
+# define AV_RL16(p) AV_RN16(p)
+# elif !defined(AV_RN16) && defined(AV_RL16)
+# define AV_RN16(p) AV_RL16(p)
+# endif
+
+# if defined(AV_WN16) && !defined(AV_WL16)
+# define AV_WL16(p, v) AV_WN16(p, v)
+# elif !defined(AV_WN16) && defined(AV_WL16)
+# define AV_WN16(p, v) AV_WL16(p, v)
+# endif
+
+# if defined(AV_RN24) && !defined(AV_RL24)
+# define AV_RL24(p) AV_RN24(p)
+# elif !defined(AV_RN24) && defined(AV_RL24)
+# define AV_RN24(p) AV_RL24(p)
+# endif
+
+# if defined(AV_WN24) && !defined(AV_WL24)
+# define AV_WL24(p, v) AV_WN24(p, v)
+# elif !defined(AV_WN24) && defined(AV_WL24)
+# define AV_WN24(p, v) AV_WL24(p, v)
+# endif
+
+# if defined(AV_RN32) && !defined(AV_RL32)
+# define AV_RL32(p) AV_RN32(p)
+# elif !defined(AV_RN32) && defined(AV_RL32)
+# define AV_RN32(p) AV_RL32(p)
+# endif
+
+# if defined(AV_WN32) && !defined(AV_WL32)
+# define AV_WL32(p, v) AV_WN32(p, v)
+# elif !defined(AV_WN32) && defined(AV_WL32)
+# define AV_WN32(p, v) AV_WL32(p, v)
+# endif
+
+# if defined(AV_RN64) && !defined(AV_RL64)
+# define AV_RL64(p) AV_RN64(p)
+# elif !defined(AV_RN64) && defined(AV_RL64)
+# define AV_RN64(p) AV_RL64(p)
+# endif
+
+# if defined(AV_WN64) && !defined(AV_WL64)
+# define AV_WL64(p, v) AV_WN64(p, v)
+# elif !defined(AV_WN64) && defined(AV_WL64)
+# define AV_WN64(p, v) AV_WL64(p, v)
+# endif
+
+#endif /* !AV_HAVE_BIGENDIAN */
+
+/*
+ * Define AV_[RW]N helper macros to simplify definitions not provided
+ * by per-arch headers.
+ */
+
+#if defined(__GNUC__) && !defined(__TI_COMPILER_VERSION__)
+
+union unaligned_64 { uint64_t l; } __attribute__((packed)) av_alias;
+union unaligned_32 { uint32_t l; } __attribute__((packed)) av_alias;
+union unaligned_16 { uint16_t l; } __attribute__((packed)) av_alias;
+
+# define AV_RN(s, p) (((const union unaligned_##s *) (p))->l)
+# define AV_WN(s, p, v) ((((union unaligned_##s *) (p))->l) = (v))
+
+#elif defined(__DECC)
+
+# define AV_RN(s, p) (*((const __unaligned uint##s##_t*)(p)))
+# define AV_WN(s, p, v) (*((__unaligned uint##s##_t*)(p)) = (v))
+
+#elif AV_HAVE_FAST_UNALIGNED
+
+# define AV_RN(s, p) (((const av_alias##s*)(p))->u##s)
+# define AV_WN(s, p, v) (((av_alias##s*)(p))->u##s = (v))
+
+#else
+
+#ifndef AV_RB16
+# define AV_RB16(x) \
+ ((((const uint8_t*)(x))[0] << 8) | \
+ ((const uint8_t*)(x))[1])
+#endif
+#ifndef AV_WB16
+# define AV_WB16(p, d) do { \
+ ((uint8_t*)(p))[1] = (d); \
+ ((uint8_t*)(p))[0] = (d)>>8; \
+ } while(0)
+#endif
+
+#ifndef AV_RL16
+# define AV_RL16(x) \
+ ((((const uint8_t*)(x))[1] << 8) | \
+ ((const uint8_t*)(x))[0])
+#endif
+#ifndef AV_WL16
+# define AV_WL16(p, d) do { \
+ ((uint8_t*)(p))[0] = (d); \
+ ((uint8_t*)(p))[1] = (d)>>8; \
+ } while(0)
+#endif
+
+#ifndef AV_RB32
+# define AV_RB32(x) \
+ (((uint32_t)((const uint8_t*)(x))[0] << 24) | \
+ (((const uint8_t*)(x))[1] << 16) | \
+ (((const uint8_t*)(x))[2] << 8) | \
+ ((const uint8_t*)(x))[3])
+#endif
+#ifndef AV_WB32
+# define AV_WB32(p, d) do { \
+ ((uint8_t*)(p))[3] = (d); \
+ ((uint8_t*)(p))[2] = (d)>>8; \
+ ((uint8_t*)(p))[1] = (d)>>16; \
+ ((uint8_t*)(p))[0] = (d)>>24; \
+ } while(0)
+#endif
+
+#ifndef AV_RL32
+# define AV_RL32(x) \
+ (((uint32_t)((const uint8_t*)(x))[3] << 24) | \
+ (((const uint8_t*)(x))[2] << 16) | \
+ (((const uint8_t*)(x))[1] << 8) | \
+ ((const uint8_t*)(x))[0])
+#endif
+#ifndef AV_WL32
+# define AV_WL32(p, d) do { \
+ ((uint8_t*)(p))[0] = (d); \
+ ((uint8_t*)(p))[1] = (d)>>8; \
+ ((uint8_t*)(p))[2] = (d)>>16; \
+ ((uint8_t*)(p))[3] = (d)>>24; \
+ } while(0)
+#endif
+
+#ifndef AV_RB64
+# define AV_RB64(x) \
+ (((uint64_t)((const uint8_t*)(x))[0] << 56) | \
+ ((uint64_t)((const uint8_t*)(x))[1] << 48) | \
+ ((uint64_t)((const uint8_t*)(x))[2] << 40) | \
+ ((uint64_t)((const uint8_t*)(x))[3] << 32) | \
+ ((uint64_t)((const uint8_t*)(x))[4] << 24) | \
+ ((uint64_t)((const uint8_t*)(x))[5] << 16) | \
+ ((uint64_t)((const uint8_t*)(x))[6] << 8) | \
+ (uint64_t)((const uint8_t*)(x))[7])
+#endif
+#ifndef AV_WB64
+# define AV_WB64(p, d) do { \
+ ((uint8_t*)(p))[7] = (d); \
+ ((uint8_t*)(p))[6] = (d)>>8; \
+ ((uint8_t*)(p))[5] = (d)>>16; \
+ ((uint8_t*)(p))[4] = (d)>>24; \
+ ((uint8_t*)(p))[3] = (d)>>32; \
+ ((uint8_t*)(p))[2] = (d)>>40; \
+ ((uint8_t*)(p))[1] = (d)>>48; \
+ ((uint8_t*)(p))[0] = (d)>>56; \
+ } while(0)
+#endif
+
+#ifndef AV_RL64
+# define AV_RL64(x) \
+ (((uint64_t)((const uint8_t*)(x))[7] << 56) | \
+ ((uint64_t)((const uint8_t*)(x))[6] << 48) | \
+ ((uint64_t)((const uint8_t*)(x))[5] << 40) | \
+ ((uint64_t)((const uint8_t*)(x))[4] << 32) | \
+ ((uint64_t)((const uint8_t*)(x))[3] << 24) | \
+ ((uint64_t)((const uint8_t*)(x))[2] << 16) | \
+ ((uint64_t)((const uint8_t*)(x))[1] << 8) | \
+ (uint64_t)((const uint8_t*)(x))[0])
+#endif
+#ifndef AV_WL64
+# define AV_WL64(p, d) do { \
+ ((uint8_t*)(p))[0] = (d); \
+ ((uint8_t*)(p))[1] = (d)>>8; \
+ ((uint8_t*)(p))[2] = (d)>>16; \
+ ((uint8_t*)(p))[3] = (d)>>24; \
+ ((uint8_t*)(p))[4] = (d)>>32; \
+ ((uint8_t*)(p))[5] = (d)>>40; \
+ ((uint8_t*)(p))[6] = (d)>>48; \
+ ((uint8_t*)(p))[7] = (d)>>56; \
+ } while(0)
+#endif
+
+#if AV_HAVE_BIGENDIAN
+# define AV_RN(s, p) AV_RB##s(p)
+# define AV_WN(s, p, v) AV_WB##s(p, v)
+#else
+# define AV_RN(s, p) AV_RL##s(p)
+# define AV_WN(s, p, v) AV_WL##s(p, v)
+#endif
+
+#endif /* HAVE_FAST_UNALIGNED */
+
+#ifndef AV_RN16
+# define AV_RN16(p) AV_RN(16, p)
+#endif
+
+#ifndef AV_RN32
+# define AV_RN32(p) AV_RN(32, p)
+#endif
+
+#ifndef AV_RN64
+# define AV_RN64(p) AV_RN(64, p)
+#endif
+
+#ifndef AV_WN16
+# define AV_WN16(p, v) AV_WN(16, p, v)
+#endif
+
+#ifndef AV_WN32
+# define AV_WN32(p, v) AV_WN(32, p, v)
+#endif
+
+#ifndef AV_WN64
+# define AV_WN64(p, v) AV_WN(64, p, v)
+#endif
+
+#if AV_HAVE_BIGENDIAN
+# define AV_RB(s, p) AV_RN##s(p)
+# define AV_WB(s, p, v) AV_WN##s(p, v)
+# define AV_RL(s, p) av_bswap##s(AV_RN##s(p))
+# define AV_WL(s, p, v) AV_WN##s(p, av_bswap##s(v))
+#else
+# define AV_RB(s, p) av_bswap##s(AV_RN##s(p))
+# define AV_WB(s, p, v) AV_WN##s(p, av_bswap##s(v))
+# define AV_RL(s, p) AV_RN##s(p)
+# define AV_WL(s, p, v) AV_WN##s(p, v)
+#endif
+
+#define AV_RB8(x) (((const uint8_t*)(x))[0])
+#define AV_WB8(p, d) do { ((uint8_t*)(p))[0] = (d); } while(0)
+
+#define AV_RL8(x) AV_RB8(x)
+#define AV_WL8(p, d) AV_WB8(p, d)
+
+#ifndef AV_RB16
+# define AV_RB16(p) AV_RB(16, p)
+#endif
+#ifndef AV_WB16
+# define AV_WB16(p, v) AV_WB(16, p, v)
+#endif
+
+#ifndef AV_RL16
+# define AV_RL16(p) AV_RL(16, p)
+#endif
+#ifndef AV_WL16
+# define AV_WL16(p, v) AV_WL(16, p, v)
+#endif
+
+#ifndef AV_RB32
+# define AV_RB32(p) AV_RB(32, p)
+#endif
+#ifndef AV_WB32
+# define AV_WB32(p, v) AV_WB(32, p, v)
+#endif
+
+#ifndef AV_RL32
+# define AV_RL32(p) AV_RL(32, p)
+#endif
+#ifndef AV_WL32
+# define AV_WL32(p, v) AV_WL(32, p, v)
+#endif
+
+#ifndef AV_RB64
+# define AV_RB64(p) AV_RB(64, p)
+#endif
+#ifndef AV_WB64
+# define AV_WB64(p, v) AV_WB(64, p, v)
+#endif
+
+#ifndef AV_RL64
+# define AV_RL64(p) AV_RL(64, p)
+#endif
+#ifndef AV_WL64
+# define AV_WL64(p, v) AV_WL(64, p, v)
+#endif
+
+#ifndef AV_RB24
+# define AV_RB24(x) \
+ ((((const uint8_t*)(x))[0] << 16) | \
+ (((const uint8_t*)(x))[1] << 8) | \
+ ((const uint8_t*)(x))[2])
+#endif
+#ifndef AV_WB24
+# define AV_WB24(p, d) do { \
+ ((uint8_t*)(p))[2] = (d); \
+ ((uint8_t*)(p))[1] = (d)>>8; \
+ ((uint8_t*)(p))[0] = (d)>>16; \
+ } while(0)
+#endif
+
+#ifndef AV_RL24
+# define AV_RL24(x) \
+ ((((const uint8_t*)(x))[2] << 16) | \
+ (((const uint8_t*)(x))[1] << 8) | \
+ ((const uint8_t*)(x))[0])
+#endif
+#ifndef AV_WL24
+# define AV_WL24(p, d) do { \
+ ((uint8_t*)(p))[0] = (d); \
+ ((uint8_t*)(p))[1] = (d)>>8; \
+ ((uint8_t*)(p))[2] = (d)>>16; \
+ } while(0)
+#endif
+
+/*
+ * The AV_[RW]NA macros access naturally aligned data
+ * in a type-safe way.
+ */
+
+#define AV_RNA(s, p) (((const av_alias##s*)(p))->u##s)
+#define AV_WNA(s, p, v) (((av_alias##s*)(p))->u##s = (v))
+
+#ifndef AV_RN16A
+# define AV_RN16A(p) AV_RNA(16, p)
+#endif
+
+#ifndef AV_RN32A
+# define AV_RN32A(p) AV_RNA(32, p)
+#endif
+
+#ifndef AV_RN64A
+# define AV_RN64A(p) AV_RNA(64, p)
+#endif
+
+#ifndef AV_WN16A
+# define AV_WN16A(p, v) AV_WNA(16, p, v)
+#endif
+
+#ifndef AV_WN32A
+# define AV_WN32A(p, v) AV_WNA(32, p, v)
+#endif
+
+#ifndef AV_WN64A
+# define AV_WN64A(p, v) AV_WNA(64, p, v)
+#endif
+
+/*
+ * The AV_COPYxxU macros are suitable for copying data to/from unaligned
+ * memory locations.
+ */
+
+#define AV_COPYU(n, d, s) AV_WN##n(d, AV_RN##n(s));
+
+#ifndef AV_COPY16U
+# define AV_COPY16U(d, s) AV_COPYU(16, d, s)
+#endif
+
+#ifndef AV_COPY32U
+# define AV_COPY32U(d, s) AV_COPYU(32, d, s)
+#endif
+
+#ifndef AV_COPY64U
+# define AV_COPY64U(d, s) AV_COPYU(64, d, s)
+#endif
+
+#ifndef AV_COPY128U
+# define AV_COPY128U(d, s) \
+ do { \
+ AV_COPY64U(d, s); \
+ AV_COPY64U((char *)(d) + 8, (const char *)(s) + 8); \
+ } while(0)
+#endif
+
+/* Parameters for AV_COPY*, AV_SWAP*, AV_ZERO* must be
+ * naturally aligned. They may be implemented using MMX,
+ * so emms_c() must be called before using any float code
+ * afterwards.
+ */
+
+#define AV_COPY(n, d, s) \
+ (((av_alias##n*)(d))->u##n = ((const av_alias##n*)(s))->u##n)
+
+#ifndef AV_COPY16
+# define AV_COPY16(d, s) AV_COPY(16, d, s)
+#endif
+
+#ifndef AV_COPY32
+# define AV_COPY32(d, s) AV_COPY(32, d, s)
+#endif
+
+#ifndef AV_COPY64
+# define AV_COPY64(d, s) AV_COPY(64, d, s)
+#endif
+
+#ifndef AV_COPY128
+# define AV_COPY128(d, s) \
+ do { \
+ AV_COPY64(d, s); \
+ AV_COPY64((char*)(d)+8, (char*)(s)+8); \
+ } while(0)
+#endif
+
+#define AV_SWAP(n, a, b) FFSWAP(av_alias##n, *(av_alias##n*)(a), *(av_alias##n*)(b))
+
+#ifndef AV_SWAP64
+# define AV_SWAP64(a, b) AV_SWAP(64, a, b)
+#endif
+
+#define AV_ZERO(n, d) (((av_alias##n*)(d))->u##n = 0)
+
+#ifndef AV_ZERO16
+# define AV_ZERO16(d) AV_ZERO(16, d)
+#endif
+
+#ifndef AV_ZERO32
+# define AV_ZERO32(d) AV_ZERO(32, d)
+#endif
+
+#ifndef AV_ZERO64
+# define AV_ZERO64(d) AV_ZERO(64, d)
+#endif
+
+#ifndef AV_ZERO128
+# define AV_ZERO128(d) \
+ do { \
+ AV_ZERO64(d); \
+ AV_ZERO64((char*)(d)+8); \
+ } while(0)
+#endif
+
+#endif /* AVUTIL_INTREADWRITE_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/lfg.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/lfg.h
new file mode 100644
index 0000000000..5e526c1dae
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/lfg.h
@@ -0,0 +1,62 @@
+/*
+ * Lagged Fibonacci PRNG
+ * Copyright (c) 2008 Michael Niedermayer
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_LFG_H
+#define AVUTIL_LFG_H
+
+typedef struct AVLFG {
+ unsigned int state[64];
+ int index;
+} AVLFG;
+
+void av_lfg_init(AVLFG *c, unsigned int seed);
+
+/**
+ * Get the next random unsigned 32-bit number using an ALFG.
+ *
+ * Please also consider a simple LCG like state= state*1664525+1013904223,
+ * it may be good enough and faster for your specific use case.
+ */
+static inline unsigned int av_lfg_get(AVLFG *c){
+ c->state[c->index & 63] = c->state[(c->index-24) & 63] + c->state[(c->index-55) & 63];
+ return c->state[c->index++ & 63];
+}
+
+/**
+ * Get the next random unsigned 32-bit number using a MLFG.
+ *
+ * Please also consider av_lfg_get() above, it is faster.
+ */
+static inline unsigned int av_mlfg_get(AVLFG *c){
+ unsigned int a= c->state[(c->index-55) & 63];
+ unsigned int b= c->state[(c->index-24) & 63];
+ return c->state[c->index++ & 63] = 2*a*b+a+b;
+}
+
+/**
+ * Get the next two numbers generated by a Box-Muller Gaussian
+ * generator using the random numbers issued by lfg.
+ *
+ * @param out array where the two generated numbers are placed
+ */
+void av_bmg_get(AVLFG *lfg, double out[2]);
+
+#endif /* AVUTIL_LFG_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/log.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/log.h
new file mode 100644
index 0000000000..7b173302f8
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/log.h
@@ -0,0 +1,173 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_LOG_H
+#define AVUTIL_LOG_H
+
+#include <stdarg.h>
+#include "avutil.h"
+#include "attributes.h"
+
+/**
+ * Describe the class of an AVClass context structure. That is an
+ * arbitrary struct of which the first field is a pointer to an
+ * AVClass struct (e.g. AVCodecContext, AVFormatContext etc.).
+ */
+typedef struct AVClass {
+ /**
+ * The name of the class; usually it is the same name as the
+ * context structure type to which the AVClass is associated.
+ */
+ const char* class_name;
+
+ /**
+ * A pointer to a function which returns the name of a context
+ * instance ctx associated with the class.
+ */
+ const char* (*item_name)(void* ctx);
+
+ /**
+ * a pointer to the first option specified in the class if any or NULL
+ *
+ * @see av_set_default_options()
+ */
+ const struct AVOption *option;
+
+ /**
+ * LIBAVUTIL_VERSION with which this structure was created.
+ * This is used to allow fields to be added without requiring major
+ * version bumps everywhere.
+ */
+
+ int version;
+
+ /**
+ * Offset in the structure where log_level_offset is stored.
+ * 0 means there is no such variable
+ */
+ int log_level_offset_offset;
+
+ /**
+ * Offset in the structure where a pointer to the parent context for
+ * logging is stored. For example a decoder could pass its AVCodecContext
+ * to eval as such a parent context, which an av_log() implementation
+ * could then leverage to display the parent context.
+ * The offset can be NULL.
+ */
+ int parent_log_context_offset;
+
+ /**
+ * Return next AVOptions-enabled child or NULL
+ */
+ void* (*child_next)(void *obj, void *prev);
+
+ /**
+ * Return an AVClass corresponding to the next potential
+ * AVOptions-enabled child.
+ *
+ * The difference between child_next and this is that
+ * child_next iterates over _already existing_ objects, while
+ * child_class_next iterates over _all possible_ children.
+ */
+ const struct AVClass* (*child_class_next)(const struct AVClass *prev);
+} AVClass;
+
+/* av_log API */
+
+#define AV_LOG_QUIET -8
+
+/**
+ * Something went really wrong and we will crash now.
+ */
+#define AV_LOG_PANIC 0
+
+/**
+ * Something went wrong and recovery is not possible.
+ * For example, no header was found for a format which depends
+ * on headers or an illegal combination of parameters is used.
+ */
+#define AV_LOG_FATAL 8
+
+/**
+ * Something went wrong and cannot losslessly be recovered.
+ * However, not all future data is affected.
+ */
+#define AV_LOG_ERROR 16
+
+/**
+ * Something somehow does not look correct. This may or may not
+ * lead to problems. An example would be the use of '-vstrict -2'.
+ */
+#define AV_LOG_WARNING 24
+
+#define AV_LOG_INFO 32
+#define AV_LOG_VERBOSE 40
+
+/**
+ * Stuff which is only useful for libav* developers.
+ */
+#define AV_LOG_DEBUG 48
+
+/**
+ * Send the specified message to the log if the level is less than or equal
+ * to the current av_log_level. By default, all logging messages are sent to
+ * stderr. This behavior can be altered by setting a different av_vlog callback
+ * function.
+ *
+ * @param avcl A pointer to an arbitrary struct of which the first field is a
+ * pointer to an AVClass struct.
+ * @param level The importance level of the message, lower values signifying
+ * higher importance.
+ * @param fmt The format string (printf-compatible) that specifies how
+ * subsequent arguments are converted to output.
+ * @see av_vlog
+ */
+void av_log(void *avcl, int level, const char *fmt, ...) av_printf_format(3, 4);
+
+void av_vlog(void *avcl, int level, const char *fmt, va_list);
+int av_log_get_level(void);
+void av_log_set_level(int);
+void av_log_set_callback(void (*)(void*, int, const char*, va_list));
+void av_log_default_callback(void* ptr, int level, const char* fmt, va_list vl);
+const char* av_default_item_name(void* ctx);
+
+/**
+ * av_dlog macros
+ * Useful to print debug messages that shouldn't get compiled in normally.
+ */
+
+#ifdef DEBUG
+# define av_dlog(pctx, ...) av_log(pctx, AV_LOG_DEBUG, __VA_ARGS__)
+#else
+# define av_dlog(pctx, ...)
+#endif
+
+/**
+ * Skip repeated messages, this requires the user app to use av_log() instead of
+ * (f)printf as the 2 would otherwise interfere and lead to
+ * "Last message repeated x times" messages below (f)printf messages with some
+ * bad luck.
+ * Also to receive the last, "last repeated" line if any, the user app must
+ * call av_log(NULL, AV_LOG_QUIET, ""); at the end
+ */
+#define AV_LOG_SKIP_REPEATED 1
+void av_log_set_flags(int arg);
+
+#endif /* AVUTIL_LOG_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/lzo.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/lzo.h
new file mode 100644
index 0000000000..9d7e8f1dc1
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/lzo.h
@@ -0,0 +1,66 @@
+/*
+ * LZO 1x decompression
+ * copyright (c) 2006 Reimar Doeffinger
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_LZO_H
+#define AVUTIL_LZO_H
+
+/**
+ * @defgroup lavu_lzo LZO
+ * @ingroup lavu_crypto
+ *
+ * @{
+ */
+
+#include <stdint.h>
+
+/** @name Error flags returned by av_lzo1x_decode
+ * @{ */
+/// end of the input buffer reached before decoding finished
+#define AV_LZO_INPUT_DEPLETED 1
+/// decoded data did not fit into output buffer
+#define AV_LZO_OUTPUT_FULL 2
+/// a reference to previously decoded data was wrong
+#define AV_LZO_INVALID_BACKPTR 4
+/// a non-specific error in the compressed bitstream
+#define AV_LZO_ERROR 8
+/** @} */
+
+#define AV_LZO_INPUT_PADDING 8
+#define AV_LZO_OUTPUT_PADDING 12
+
+/**
+ * @brief Decodes LZO 1x compressed data.
+ * @param out output buffer
+ * @param outlen size of output buffer, number of bytes left are returned here
+ * @param in input buffer
+ * @param inlen size of input buffer, number of bytes left are returned here
+ * @return 0 on success, otherwise a combination of the error flags above
+ *
+ * Make sure all buffers are appropriately padded, in must provide
+ * AV_LZO_INPUT_PADDING, out must provide AV_LZO_OUTPUT_PADDING additional bytes.
+ */
+int av_lzo1x_decode(void *out, int *outlen, const void *in, int *inlen);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_LZO_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/mathematics.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/mathematics.h
new file mode 100644
index 0000000000..043dd0fafe
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/mathematics.h
@@ -0,0 +1,111 @@
+/*
+ * copyright (c) 2005 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_MATHEMATICS_H
+#define AVUTIL_MATHEMATICS_H
+
+#include <stdint.h>
+#include <math.h>
+#include "attributes.h"
+#include "rational.h"
+#include "intfloat.h"
+
+#ifndef M_LOG2_10
+#define M_LOG2_10 3.32192809488736234787 /* log_2 10 */
+#endif
+#ifndef M_PHI
+#define M_PHI 1.61803398874989484820 /* phi / golden ratio */
+#endif
+#ifndef NAN
+#define NAN av_int2float(0x7fc00000)
+#endif
+#ifndef INFINITY
+#define INFINITY av_int2float(0x7f800000)
+#endif
+
+/**
+ * @addtogroup lavu_math
+ * @{
+ */
+
+
+enum AVRounding {
+ AV_ROUND_ZERO = 0, ///< Round toward zero.
+ AV_ROUND_INF = 1, ///< Round away from zero.
+ AV_ROUND_DOWN = 2, ///< Round toward -infinity.
+ AV_ROUND_UP = 3, ///< Round toward +infinity.
+ AV_ROUND_NEAR_INF = 5, ///< Round to nearest and halfway cases away from zero.
+};
+
+/**
+ * Return the greatest common divisor of a and b.
+ * If both a and b are 0 or either or both are <0 then behavior is
+ * undefined.
+ */
+int64_t av_const av_gcd(int64_t a, int64_t b);
+
+/**
+ * Rescale a 64-bit integer with rounding to nearest.
+ * A simple a*b/c isn't possible as it can overflow.
+ */
+int64_t av_rescale(int64_t a, int64_t b, int64_t c) av_const;
+
+/**
+ * Rescale a 64-bit integer with specified rounding.
+ * A simple a*b/c isn't possible as it can overflow.
+ */
+int64_t av_rescale_rnd(int64_t a, int64_t b, int64_t c, enum AVRounding) av_const;
+
+/**
+ * Rescale a 64-bit integer by 2 rational numbers.
+ */
+int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq) av_const;
+
+/**
+ * Rescale a 64-bit integer by 2 rational numbers with specified rounding.
+ */
+int64_t av_rescale_q_rnd(int64_t a, AVRational bq, AVRational cq,
+ enum AVRounding) av_const;
+
+/**
+ * Compare 2 timestamps each in its own timebases.
+ * The result of the function is undefined if one of the timestamps
+ * is outside the int64_t range when represented in the others timebase.
+ * @return -1 if ts_a is before ts_b, 1 if ts_a is after ts_b or 0 if they represent the same position
+ */
+int av_compare_ts(int64_t ts_a, AVRational tb_a, int64_t ts_b, AVRational tb_b);
+
+/**
+ * Compare 2 integers modulo mod.
+ * That is we compare integers a and b for which only the least
+ * significant log2(mod) bits are known.
+ *
+ * @param mod must be a power of 2
+ * @return a negative value if a is smaller than b
+ * a positive value if a is greater than b
+ * 0 if a equals b
+ */
+int64_t av_compare_mod(uint64_t a, uint64_t b, uint64_t mod);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_MATHEMATICS_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/md5.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/md5.h
new file mode 100644
index 0000000000..29e4e7c2ba
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/md5.h
@@ -0,0 +1,51 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_MD5_H
+#define AVUTIL_MD5_H
+
+#include <stdint.h>
+
+#include "attributes.h"
+#include "version.h"
+
+/**
+ * @defgroup lavu_md5 MD5
+ * @ingroup lavu_crypto
+ * @{
+ */
+
+#if FF_API_CONTEXT_SIZE
+extern attribute_deprecated const int av_md5_size;
+#endif
+
+struct AVMD5;
+
+struct AVMD5 *av_md5_alloc(void);
+void av_md5_init(struct AVMD5 *ctx);
+void av_md5_update(struct AVMD5 *ctx, const uint8_t *src, const int len);
+void av_md5_final(struct AVMD5 *ctx, uint8_t *dst);
+void av_md5_sum(uint8_t *dst, const uint8_t *src, const int len);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_MD5_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/mem.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/mem.h
new file mode 100644
index 0000000000..8f4722447d
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/mem.h
@@ -0,0 +1,183 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * memory handling functions
+ */
+
+#ifndef AVUTIL_MEM_H
+#define AVUTIL_MEM_H
+
+#include <limits.h>
+#include <stdint.h>
+
+#include "attributes.h"
+#include "avutil.h"
+
+/**
+ * @addtogroup lavu_mem
+ * @{
+ */
+
+
+#if defined(__ICC) && __ICC < 1200 || defined(__SUNPRO_C)
+ #define DECLARE_ALIGNED(n,t,v) t __attribute__ ((aligned (n))) v
+ #define DECLARE_ASM_CONST(n,t,v) const t __attribute__ ((aligned (n))) v
+#elif defined(__TI_COMPILER_VERSION__)
+ #define DECLARE_ALIGNED(n,t,v) \
+ AV_PRAGMA(DATA_ALIGN(v,n)) \
+ t __attribute__((aligned(n))) v
+ #define DECLARE_ASM_CONST(n,t,v) \
+ AV_PRAGMA(DATA_ALIGN(v,n)) \
+ static const t __attribute__((aligned(n))) v
+#elif defined(__GNUC__)
+ #define DECLARE_ALIGNED(n,t,v) t __attribute__ ((aligned (n))) v
+ #define DECLARE_ASM_CONST(n,t,v) static const t av_used __attribute__ ((aligned (n))) v
+#elif defined(_MSC_VER)
+ #define DECLARE_ALIGNED(n,t,v) __declspec(align(n)) t v
+ #define DECLARE_ASM_CONST(n,t,v) __declspec(align(n)) static const t v
+#else
+ #define DECLARE_ALIGNED(n,t,v) t v
+ #define DECLARE_ASM_CONST(n,t,v) static const t v
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3,1)
+ #define av_malloc_attrib __attribute__((__malloc__))
+#else
+ #define av_malloc_attrib
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(4,3)
+ #define av_alloc_size(...) __attribute__((alloc_size(__VA_ARGS__)))
+#else
+ #define av_alloc_size(...)
+#endif
+
+/**
+ * Allocate a block of size bytes with alignment suitable for all
+ * memory accesses (including vectors if available on the CPU).
+ * @param size Size in bytes for the memory block to be allocated.
+ * @return Pointer to the allocated block, NULL if the block cannot
+ * be allocated.
+ * @see av_mallocz()
+ */
+void *av_malloc(size_t size) av_malloc_attrib av_alloc_size(1);
+
+/**
+ * Helper function to allocate a block of size * nmemb bytes with
+ * using av_malloc()
+ * @param nmemb Number of elements
+ * @param size Size of the single element
+ * @return Pointer to the allocated block, NULL if the block cannot
+ * be allocated.
+ * @see av_malloc()
+ */
+av_alloc_size(1, 2) static inline void *av_malloc_array(size_t nmemb, size_t size)
+{
+ if (size <= 0 || nmemb >= INT_MAX / size)
+ return NULL;
+ return av_malloc(nmemb * size);
+}
+
+/**
+ * Allocate or reallocate a block of memory.
+ * If ptr is NULL and size > 0, allocate a new block. If
+ * size is zero, free the memory block pointed to by ptr.
+ * @param ptr Pointer to a memory block already allocated with
+ * av_malloc(z)() or av_realloc() or NULL.
+ * @param size Size in bytes for the memory block to be allocated or
+ * reallocated.
+ * @return Pointer to a newly reallocated block or NULL if the block
+ * cannot be reallocated or the function is used to free the memory block.
+ * @see av_fast_realloc()
+ */
+void *av_realloc(void *ptr, size_t size) av_alloc_size(2);
+
+/**
+ * Free a memory block which has been allocated with av_malloc(z)() or
+ * av_realloc().
+ * @param ptr Pointer to the memory block which should be freed.
+ * @note ptr = NULL is explicitly allowed.
+ * @note It is recommended that you use av_freep() instead.
+ * @see av_freep()
+ */
+void av_free(void *ptr);
+
+/**
+ * Allocate a block of size bytes with alignment suitable for all
+ * memory accesses (including vectors if available on the CPU) and
+ * zero all the bytes of the block.
+ * @param size Size in bytes for the memory block to be allocated.
+ * @return Pointer to the allocated block, NULL if it cannot be allocated.
+ * @see av_malloc()
+ */
+void *av_mallocz(size_t size) av_malloc_attrib av_alloc_size(1);
+
+/**
+ * Helper function to allocate a block of size * nmemb bytes with
+ * using av_mallocz()
+ * @param nmemb Number of elements
+ * @param size Size of the single element
+ * @return Pointer to the allocated block, NULL if the block cannot
+ * be allocated.
+ * @see av_mallocz()
+ * @see av_malloc_array()
+ */
+av_alloc_size(1, 2) static inline void *av_mallocz_array(size_t nmemb, size_t size)
+{
+ if (size <= 0 || nmemb >= INT_MAX / size)
+ return NULL;
+ return av_mallocz(nmemb * size);
+}
+
+/**
+ * Duplicate the string s.
+ * @param s string to be duplicated
+ * @return Pointer to a newly allocated string containing a
+ * copy of s or NULL if the string cannot be allocated.
+ */
+char *av_strdup(const char *s) av_malloc_attrib;
+
+/**
+ * Free a memory block which has been allocated with av_malloc(z)() or
+ * av_realloc() and set the pointer pointing to it to NULL.
+ * @param ptr Pointer to the pointer to the memory block which should
+ * be freed.
+ * @see av_free()
+ */
+void av_freep(void *ptr);
+
+/**
+ * @brief deliberately overlapping memcpy implementation
+ * @param dst destination buffer
+ * @param back how many bytes back we start (the initial size of the overlapping window)
+ * @param cnt number of bytes to copy, must be >= 0
+ *
+ * cnt > back is valid, this will copy the bytes we just copied,
+ * thus creating a repeating pattern with a period length of back.
+ */
+void av_memcpy_backptr(uint8_t *dst, int back, int cnt);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_MEM_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/old_pix_fmts.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/old_pix_fmts.h
new file mode 100644
index 0000000000..31765aed50
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/old_pix_fmts.h
@@ -0,0 +1,128 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_OLD_PIX_FMTS_H
+#define AVUTIL_OLD_PIX_FMTS_H
+
+/*
+ * This header exists to prevent new pixel formats from being accidentally added
+ * to the deprecated list.
+ * Do not include it directly. It will be removed on next major bump
+ *
+ * Do not add new items to this list. Use the AVPixelFormat enum instead.
+ */
+ PIX_FMT_NONE = AV_PIX_FMT_NONE,
+ PIX_FMT_YUV420P, ///< planar YUV 4:2:0, 12bpp, (1 Cr & Cb sample per 2x2 Y samples)
+ PIX_FMT_YUYV422, ///< packed YUV 4:2:2, 16bpp, Y0 Cb Y1 Cr
+ PIX_FMT_RGB24, ///< packed RGB 8:8:8, 24bpp, RGBRGB...
+ PIX_FMT_BGR24, ///< packed RGB 8:8:8, 24bpp, BGRBGR...
+ PIX_FMT_YUV422P, ///< planar YUV 4:2:2, 16bpp, (1 Cr & Cb sample per 2x1 Y samples)
+ PIX_FMT_YUV444P, ///< planar YUV 4:4:4, 24bpp, (1 Cr & Cb sample per 1x1 Y samples)
+ PIX_FMT_YUV410P, ///< planar YUV 4:1:0, 9bpp, (1 Cr & Cb sample per 4x4 Y samples)
+ PIX_FMT_YUV411P, ///< planar YUV 4:1:1, 12bpp, (1 Cr & Cb sample per 4x1 Y samples)
+ PIX_FMT_GRAY8, ///< Y , 8bpp
+ PIX_FMT_MONOWHITE, ///< Y , 1bpp, 0 is white, 1 is black, in each byte pixels are ordered from the msb to the lsb
+ PIX_FMT_MONOBLACK, ///< Y , 1bpp, 0 is black, 1 is white, in each byte pixels are ordered from the msb to the lsb
+ PIX_FMT_PAL8, ///< 8 bit with PIX_FMT_RGB32 palette
+ PIX_FMT_YUVJ420P, ///< planar YUV 4:2:0, 12bpp, full scale (JPEG), deprecated in favor of PIX_FMT_YUV420P and setting color_range
+ PIX_FMT_YUVJ422P, ///< planar YUV 4:2:2, 16bpp, full scale (JPEG), deprecated in favor of PIX_FMT_YUV422P and setting color_range
+ PIX_FMT_YUVJ444P, ///< planar YUV 4:4:4, 24bpp, full scale (JPEG), deprecated in favor of PIX_FMT_YUV444P and setting color_range
+ PIX_FMT_XVMC_MPEG2_MC,///< XVideo Motion Acceleration via common packet passing
+ PIX_FMT_XVMC_MPEG2_IDCT,
+ PIX_FMT_UYVY422, ///< packed YUV 4:2:2, 16bpp, Cb Y0 Cr Y1
+ PIX_FMT_UYYVYY411, ///< packed YUV 4:1:1, 12bpp, Cb Y0 Y1 Cr Y2 Y3
+ PIX_FMT_BGR8, ///< packed RGB 3:3:2, 8bpp, (msb)2B 3G 3R(lsb)
+ PIX_FMT_BGR4, ///< packed RGB 1:2:1 bitstream, 4bpp, (msb)1B 2G 1R(lsb), a byte contains two pixels, the first pixel in the byte is the one composed by the 4 msb bits
+ PIX_FMT_BGR4_BYTE, ///< packed RGB 1:2:1, 8bpp, (msb)1B 2G 1R(lsb)
+ PIX_FMT_RGB8, ///< packed RGB 3:3:2, 8bpp, (msb)2R 3G 3B(lsb)
+ PIX_FMT_RGB4, ///< packed RGB 1:2:1 bitstream, 4bpp, (msb)1R 2G 1B(lsb), a byte contains two pixels, the first pixel in the byte is the one composed by the 4 msb bits
+ PIX_FMT_RGB4_BYTE, ///< packed RGB 1:2:1, 8bpp, (msb)1R 2G 1B(lsb)
+ PIX_FMT_NV12, ///< planar YUV 4:2:0, 12bpp, 1 plane for Y and 1 plane for the UV components, which are interleaved (first byte U and the following byte V)
+ PIX_FMT_NV21, ///< as above, but U and V bytes are swapped
+
+ PIX_FMT_ARGB, ///< packed ARGB 8:8:8:8, 32bpp, ARGBARGB...
+ PIX_FMT_RGBA, ///< packed RGBA 8:8:8:8, 32bpp, RGBARGBA...
+ PIX_FMT_ABGR, ///< packed ABGR 8:8:8:8, 32bpp, ABGRABGR...
+ PIX_FMT_BGRA, ///< packed BGRA 8:8:8:8, 32bpp, BGRABGRA...
+
+ PIX_FMT_GRAY16BE, ///< Y , 16bpp, big-endian
+ PIX_FMT_GRAY16LE, ///< Y , 16bpp, little-endian
+ PIX_FMT_YUV440P, ///< planar YUV 4:4:0 (1 Cr & Cb sample per 1x2 Y samples)
+ PIX_FMT_YUVJ440P, ///< planar YUV 4:4:0 full scale (JPEG), deprecated in favor of PIX_FMT_YUV440P and setting color_range
+ PIX_FMT_YUVA420P, ///< planar YUV 4:2:0, 20bpp, (1 Cr & Cb sample per 2x2 Y & A samples)
+ PIX_FMT_VDPAU_H264,///< H.264 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ PIX_FMT_VDPAU_MPEG1,///< MPEG-1 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ PIX_FMT_VDPAU_MPEG2,///< MPEG-2 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ PIX_FMT_VDPAU_WMV3,///< WMV3 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ PIX_FMT_VDPAU_VC1, ///< VC-1 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ PIX_FMT_RGB48BE, ///< packed RGB 16:16:16, 48bpp, 16R, 16G, 16B, the 2-byte value for each R/G/B component is stored as big-endian
+ PIX_FMT_RGB48LE, ///< packed RGB 16:16:16, 48bpp, 16R, 16G, 16B, the 2-byte value for each R/G/B component is stored as little-endian
+
+ PIX_FMT_RGB565BE, ///< packed RGB 5:6:5, 16bpp, (msb) 5R 6G 5B(lsb), big-endian
+ PIX_FMT_RGB565LE, ///< packed RGB 5:6:5, 16bpp, (msb) 5R 6G 5B(lsb), little-endian
+ PIX_FMT_RGB555BE, ///< packed RGB 5:5:5, 16bpp, (msb)1A 5R 5G 5B(lsb), big-endian, most significant bit to 0
+ PIX_FMT_RGB555LE, ///< packed RGB 5:5:5, 16bpp, (msb)1A 5R 5G 5B(lsb), little-endian, most significant bit to 0
+
+ PIX_FMT_BGR565BE, ///< packed BGR 5:6:5, 16bpp, (msb) 5B 6G 5R(lsb), big-endian
+ PIX_FMT_BGR565LE, ///< packed BGR 5:6:5, 16bpp, (msb) 5B 6G 5R(lsb), little-endian
+ PIX_FMT_BGR555BE, ///< packed BGR 5:5:5, 16bpp, (msb)1A 5B 5G 5R(lsb), big-endian, most significant bit to 1
+ PIX_FMT_BGR555LE, ///< packed BGR 5:5:5, 16bpp, (msb)1A 5B 5G 5R(lsb), little-endian, most significant bit to 1
+
+ PIX_FMT_VAAPI_MOCO, ///< HW acceleration through VA API at motion compensation entry-point, Picture.data[3] contains a vaapi_render_state struct which contains macroblocks as well as various fields extracted from headers
+ PIX_FMT_VAAPI_IDCT, ///< HW acceleration through VA API at IDCT entry-point, Picture.data[3] contains a vaapi_render_state struct which contains fields extracted from headers
+ PIX_FMT_VAAPI_VLD, ///< HW decoding through VA API, Picture.data[3] contains a vaapi_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+
+ PIX_FMT_YUV420P16LE, ///< planar YUV 4:2:0, 24bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian
+ PIX_FMT_YUV420P16BE, ///< planar YUV 4:2:0, 24bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian
+ PIX_FMT_YUV422P16LE, ///< planar YUV 4:2:2, 32bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ PIX_FMT_YUV422P16BE, ///< planar YUV 4:2:2, 32bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+ PIX_FMT_YUV444P16LE, ///< planar YUV 4:4:4, 48bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian
+ PIX_FMT_YUV444P16BE, ///< planar YUV 4:4:4, 48bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian
+ PIX_FMT_VDPAU_MPEG4, ///< MPEG4 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ PIX_FMT_DXVA2_VLD, ///< HW decoding through DXVA2, Picture.data[3] contains a LPDIRECT3DSURFACE9 pointer
+
+ PIX_FMT_RGB444LE, ///< packed RGB 4:4:4, 16bpp, (msb)4A 4R 4G 4B(lsb), little-endian, most significant bits to 0
+ PIX_FMT_RGB444BE, ///< packed RGB 4:4:4, 16bpp, (msb)4A 4R 4G 4B(lsb), big-endian, most significant bits to 0
+ PIX_FMT_BGR444LE, ///< packed BGR 4:4:4, 16bpp, (msb)4A 4B 4G 4R(lsb), little-endian, most significant bits to 1
+ PIX_FMT_BGR444BE, ///< packed BGR 4:4:4, 16bpp, (msb)4A 4B 4G 4R(lsb), big-endian, most significant bits to 1
+ PIX_FMT_Y400A, ///< 8bit gray, 8bit alpha
+ PIX_FMT_BGR48BE, ///< packed RGB 16:16:16, 48bpp, 16B, 16G, 16R, the 2-byte value for each R/G/B component is stored as big-endian
+ PIX_FMT_BGR48LE, ///< packed RGB 16:16:16, 48bpp, 16B, 16G, 16R, the 2-byte value for each R/G/B component is stored as little-endian
+ PIX_FMT_YUV420P9BE, ///< planar YUV 4:2:0, 13.5bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian
+ PIX_FMT_YUV420P9LE, ///< planar YUV 4:2:0, 13.5bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian
+ PIX_FMT_YUV420P10BE,///< planar YUV 4:2:0, 15bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian
+ PIX_FMT_YUV420P10LE,///< planar YUV 4:2:0, 15bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian
+ PIX_FMT_YUV422P10BE,///< planar YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+ PIX_FMT_YUV422P10LE,///< planar YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ PIX_FMT_YUV444P9BE, ///< planar YUV 4:4:4, 27bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian
+ PIX_FMT_YUV444P9LE, ///< planar YUV 4:4:4, 27bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian
+ PIX_FMT_YUV444P10BE,///< planar YUV 4:4:4, 30bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian
+ PIX_FMT_YUV444P10LE,///< planar YUV 4:4:4, 30bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian
+ PIX_FMT_YUV422P9BE, ///< planar YUV 4:2:2, 18bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+ PIX_FMT_YUV422P9LE, ///< planar YUV 4:2:2, 18bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ PIX_FMT_VDA_VLD, ///< hardware decoding through VDA
+ PIX_FMT_GBRP, ///< planar GBR 4:4:4 24bpp
+ PIX_FMT_GBRP9BE, ///< planar GBR 4:4:4 27bpp, big endian
+ PIX_FMT_GBRP9LE, ///< planar GBR 4:4:4 27bpp, little endian
+ PIX_FMT_GBRP10BE, ///< planar GBR 4:4:4 30bpp, big endian
+ PIX_FMT_GBRP10LE, ///< planar GBR 4:4:4 30bpp, little endian
+ PIX_FMT_GBRP16BE, ///< planar GBR 4:4:4 48bpp, big endian
+ PIX_FMT_GBRP16LE, ///< planar GBR 4:4:4 48bpp, little endian
+ PIX_FMT_NB, ///< number of pixel formats, DO NOT USE THIS if you want to link with shared libav* because the number of formats might differ between versions
+
+#endif /* AVUTIL_OLD_PIX_FMTS_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/opt.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/opt.h
new file mode 100644
index 0000000000..2d3cc731ee
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/opt.h
@@ -0,0 +1,516 @@
+/*
+ * AVOptions
+ * copyright (c) 2005 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_OPT_H
+#define AVUTIL_OPT_H
+
+/**
+ * @file
+ * AVOptions
+ */
+
+#include "rational.h"
+#include "avutil.h"
+#include "dict.h"
+#include "log.h"
+
+/**
+ * @defgroup avoptions AVOptions
+ * @ingroup lavu_data
+ * @{
+ * AVOptions provide a generic system to declare options on arbitrary structs
+ * ("objects"). An option can have a help text, a type and a range of possible
+ * values. Options may then be enumerated, read and written to.
+ *
+ * @section avoptions_implement Implementing AVOptions
+ * This section describes how to add AVOptions capabilities to a struct.
+ *
+ * All AVOptions-related information is stored in an AVClass. Therefore
+ * the first member of the struct must be a pointer to an AVClass describing it.
+ * The option field of the AVClass must be set to a NULL-terminated static array
+ * of AVOptions. Each AVOption must have a non-empty name, a type, a default
+ * value and for number-type AVOptions also a range of allowed values. It must
+ * also declare an offset in bytes from the start of the struct, where the field
+ * associated with this AVOption is located. Other fields in the AVOption struct
+ * should also be set when applicable, but are not required.
+ *
+ * The following example illustrates an AVOptions-enabled struct:
+ * @code
+ * typedef struct test_struct {
+ * AVClass *class;
+ * int int_opt;
+ * char *str_opt;
+ * uint8_t *bin_opt;
+ * int bin_len;
+ * } test_struct;
+ *
+ * static const AVOption options[] = {
+ * { "test_int", "This is a test option of int type.", offsetof(test_struct, int_opt),
+ * AV_OPT_TYPE_INT, { .i64 = -1 }, INT_MIN, INT_MAX },
+ * { "test_str", "This is a test option of string type.", offsetof(test_struct, str_opt),
+ * AV_OPT_TYPE_STRING },
+ * { "test_bin", "This is a test option of binary type.", offsetof(test_struct, bin_opt),
+ * AV_OPT_TYPE_BINARY },
+ * { NULL },
+ * };
+ *
+ * static const AVClass test_class = {
+ * .class_name = "test class",
+ * .item_name = av_default_item_name,
+ * .option = options,
+ * .version = LIBAVUTIL_VERSION_INT,
+ * };
+ * @endcode
+ *
+ * Next, when allocating your struct, you must ensure that the AVClass pointer
+ * is set to the correct value. Then, av_opt_set_defaults() must be called to
+ * initialize defaults. After that the struct is ready to be used with the
+ * AVOptions API.
+ *
+ * When cleaning up, you may use the av_opt_free() function to automatically
+ * free all the allocated string and binary options.
+ *
+ * Continuing with the above example:
+ *
+ * @code
+ * test_struct *alloc_test_struct(void)
+ * {
+ * test_struct *ret = av_malloc(sizeof(*ret));
+ * ret->class = &test_class;
+ * av_opt_set_defaults(ret);
+ * return ret;
+ * }
+ * void free_test_struct(test_struct **foo)
+ * {
+ * av_opt_free(*foo);
+ * av_freep(foo);
+ * }
+ * @endcode
+ *
+ * @subsection avoptions_implement_nesting Nesting
+ * It may happen that an AVOptions-enabled struct contains another
+ * AVOptions-enabled struct as a member (e.g. AVCodecContext in
+ * libavcodec exports generic options, while its priv_data field exports
+ * codec-specific options). In such a case, it is possible to set up the
+ * parent struct to export a child's options. To do that, simply
+ * implement AVClass.child_next() and AVClass.child_class_next() in the
+ * parent struct's AVClass.
+ * Assuming that the test_struct from above now also contains a
+ * child_struct field:
+ *
+ * @code
+ * typedef struct child_struct {
+ * AVClass *class;
+ * int flags_opt;
+ * } child_struct;
+ * static const AVOption child_opts[] = {
+ * { "test_flags", "This is a test option of flags type.",
+ * offsetof(child_struct, flags_opt), AV_OPT_TYPE_FLAGS, { .i64 = 0 }, INT_MIN, INT_MAX },
+ * { NULL },
+ * };
+ * static const AVClass child_class = {
+ * .class_name = "child class",
+ * .item_name = av_default_item_name,
+ * .option = child_opts,
+ * .version = LIBAVUTIL_VERSION_INT,
+ * };
+ *
+ * void *child_next(void *obj, void *prev)
+ * {
+ * test_struct *t = obj;
+ * if (!prev && t->child_struct)
+ * return t->child_struct;
+ * return NULL
+ * }
+ * const AVClass child_class_next(const AVClass *prev)
+ * {
+ * return prev ? NULL : &child_class;
+ * }
+ * @endcode
+ * Putting child_next() and child_class_next() as defined above into
+ * test_class will now make child_struct's options accessible through
+ * test_struct (again, proper setup as described above needs to be done on
+ * child_struct right after it is created).
+ *
+ * From the above example it might not be clear why both child_next()
+ * and child_class_next() are needed. The distinction is that child_next()
+ * iterates over actually existing objects, while child_class_next()
+ * iterates over all possible child classes. E.g. if an AVCodecContext
+ * was initialized to use a codec which has private options, then its
+ * child_next() will return AVCodecContext.priv_data and finish
+ * iterating. OTOH child_class_next() on AVCodecContext.av_class will
+ * iterate over all available codecs with private options.
+ *
+ * @subsection avoptions_implement_named_constants Named constants
+ * It is possible to create named constants for options. Simply set the unit
+ * field of the option the constants should apply to to a string and
+ * create the constants themselves as options of type AV_OPT_TYPE_CONST
+ * with their unit field set to the same string.
+ * Their default_val field should contain the value of the named
+ * constant.
+ * For example, to add some named constants for the test_flags option
+ * above, put the following into the child_opts array:
+ * @code
+ * { "test_flags", "This is a test option of flags type.",
+ * offsetof(child_struct, flags_opt), AV_OPT_TYPE_FLAGS, { .i64 = 0 }, INT_MIN, INT_MAX, "test_unit" },
+ * { "flag1", "This is a flag with value 16", 0, AV_OPT_TYPE_CONST, { .i64 = 16 }, 0, 0, "test_unit" },
+ * @endcode
+ *
+ * @section avoptions_use Using AVOptions
+ * This section deals with accessing options in an AVOptions-enabled struct.
+ * Such structs in Libav are e.g. AVCodecContext in libavcodec or
+ * AVFormatContext in libavformat.
+ *
+ * @subsection avoptions_use_examine Examining AVOptions
+ * The basic functions for examining options are av_opt_next(), which iterates
+ * over all options defined for one object, and av_opt_find(), which searches
+ * for an option with the given name.
+ *
+ * The situation is more complicated with nesting. An AVOptions-enabled struct
+ * may have AVOptions-enabled children. Passing the AV_OPT_SEARCH_CHILDREN flag
+ * to av_opt_find() will make the function search children recursively.
+ *
+ * For enumerating there are basically two cases. The first is when you want to
+ * get all options that may potentially exist on the struct and its children
+ * (e.g. when constructing documentation). In that case you should call
+ * av_opt_child_class_next() recursively on the parent struct's AVClass. The
+ * second case is when you have an already initialized struct with all its
+ * children and you want to get all options that can be actually written or read
+ * from it. In that case you should call av_opt_child_next() recursively (and
+ * av_opt_next() on each result).
+ *
+ * @subsection avoptions_use_get_set Reading and writing AVOptions
+ * When setting options, you often have a string read directly from the
+ * user. In such a case, simply passing it to av_opt_set() is enough. For
+ * non-string type options, av_opt_set() will parse the string according to the
+ * option type.
+ *
+ * Similarly av_opt_get() will read any option type and convert it to a string
+ * which will be returned. Do not forget that the string is allocated, so you
+ * have to free it with av_free().
+ *
+ * In some cases it may be more convenient to put all options into an
+ * AVDictionary and call av_opt_set_dict() on it. A specific case of this
+ * are the format/codec open functions in lavf/lavc which take a dictionary
+ * filled with option as a parameter. This allows to set some options
+ * that cannot be set otherwise, since e.g. the input file format is not known
+ * before the file is actually opened.
+ */
+
+enum AVOptionType{
+ AV_OPT_TYPE_FLAGS,
+ AV_OPT_TYPE_INT,
+ AV_OPT_TYPE_INT64,
+ AV_OPT_TYPE_DOUBLE,
+ AV_OPT_TYPE_FLOAT,
+ AV_OPT_TYPE_STRING,
+ AV_OPT_TYPE_RATIONAL,
+ AV_OPT_TYPE_BINARY, ///< offset must point to a pointer immediately followed by an int for the length
+ AV_OPT_TYPE_CONST = 128,
+};
+
+/**
+ * AVOption
+ */
+typedef struct AVOption {
+ const char *name;
+
+ /**
+ * short English help text
+ * @todo What about other languages?
+ */
+ const char *help;
+
+ /**
+ * The offset relative to the context structure where the option
+ * value is stored. It should be 0 for named constants.
+ */
+ int offset;
+ enum AVOptionType type;
+
+ /**
+ * the default value for scalar options
+ */
+ union {
+ int64_t i64;
+ double dbl;
+ const char *str;
+ /* TODO those are unused now */
+ AVRational q;
+ } default_val;
+ double min; ///< minimum valid value for the option
+ double max; ///< maximum valid value for the option
+
+ int flags;
+#define AV_OPT_FLAG_ENCODING_PARAM 1 ///< a generic parameter which can be set by the user for muxing or encoding
+#define AV_OPT_FLAG_DECODING_PARAM 2 ///< a generic parameter which can be set by the user for demuxing or decoding
+#define AV_OPT_FLAG_METADATA 4 ///< some data extracted or inserted into the file like title, comment, ...
+#define AV_OPT_FLAG_AUDIO_PARAM 8
+#define AV_OPT_FLAG_VIDEO_PARAM 16
+#define AV_OPT_FLAG_SUBTITLE_PARAM 32
+//FIXME think about enc-audio, ... style flags
+
+ /**
+ * The logical unit to which the option belongs. Non-constant
+ * options and corresponding named constants share the same
+ * unit. May be NULL.
+ */
+ const char *unit;
+} AVOption;
+
+/**
+ * Show the obj options.
+ *
+ * @param req_flags requested flags for the options to show. Show only the
+ * options for which it is opt->flags & req_flags.
+ * @param rej_flags rejected flags for the options to show. Show only the
+ * options for which it is !(opt->flags & req_flags).
+ * @param av_log_obj log context to use for showing the options
+ */
+int av_opt_show2(void *obj, void *av_log_obj, int req_flags, int rej_flags);
+
+/**
+ * Set the values of all AVOption fields to their default values.
+ *
+ * @param s an AVOption-enabled struct (its first member must be a pointer to AVClass)
+ */
+void av_opt_set_defaults(void *s);
+
+/**
+ * Parse the key/value pairs list in opts. For each key/value pair
+ * found, stores the value in the field in ctx that is named like the
+ * key. ctx must be an AVClass context, storing is done using
+ * AVOptions.
+ *
+ * @param key_val_sep a 0-terminated list of characters used to
+ * separate key from value
+ * @param pairs_sep a 0-terminated list of characters used to separate
+ * two pairs from each other
+ * @return the number of successfully set key/value pairs, or a negative
+ * value corresponding to an AVERROR code in case of error:
+ * AVERROR(EINVAL) if opts cannot be parsed,
+ * the error code issued by av_set_string3() if a key/value pair
+ * cannot be set
+ */
+int av_set_options_string(void *ctx, const char *opts,
+ const char *key_val_sep, const char *pairs_sep);
+
+/**
+ * Free all string and binary options in obj.
+ */
+void av_opt_free(void *obj);
+
+/**
+ * Check whether a particular flag is set in a flags field.
+ *
+ * @param field_name the name of the flag field option
+ * @param flag_name the name of the flag to check
+ * @return non-zero if the flag is set, zero if the flag isn't set,
+ * isn't of the right type, or the flags field doesn't exist.
+ */
+int av_opt_flag_is_set(void *obj, const char *field_name, const char *flag_name);
+
+/*
+ * Set all the options from a given dictionary on an object.
+ *
+ * @param obj a struct whose first element is a pointer to AVClass
+ * @param options options to process. This dictionary will be freed and replaced
+ * by a new one containing all options not found in obj.
+ * Of course this new dictionary needs to be freed by caller
+ * with av_dict_free().
+ *
+ * @return 0 on success, a negative AVERROR if some option was found in obj,
+ * but could not be set.
+ *
+ * @see av_dict_copy()
+ */
+int av_opt_set_dict(void *obj, struct AVDictionary **options);
+
+/**
+ * @defgroup opt_eval_funcs Evaluating option strings
+ * @{
+ * This group of functions can be used to evaluate option strings
+ * and get numbers out of them. They do the same thing as av_opt_set(),
+ * except the result is written into the caller-supplied pointer.
+ *
+ * @param obj a struct whose first element is a pointer to AVClass.
+ * @param o an option for which the string is to be evaluated.
+ * @param val string to be evaluated.
+ * @param *_out value of the string will be written here.
+ *
+ * @return 0 on success, a negative number on failure.
+ */
+int av_opt_eval_flags (void *obj, const AVOption *o, const char *val, int *flags_out);
+int av_opt_eval_int (void *obj, const AVOption *o, const char *val, int *int_out);
+int av_opt_eval_int64 (void *obj, const AVOption *o, const char *val, int64_t *int64_out);
+int av_opt_eval_float (void *obj, const AVOption *o, const char *val, float *float_out);
+int av_opt_eval_double(void *obj, const AVOption *o, const char *val, double *double_out);
+int av_opt_eval_q (void *obj, const AVOption *o, const char *val, AVRational *q_out);
+/**
+ * @}
+ */
+
+#define AV_OPT_SEARCH_CHILDREN 0x0001 /**< Search in possible children of the
+ given object first. */
+/**
+ * The obj passed to av_opt_find() is fake -- only a double pointer to AVClass
+ * instead of a required pointer to a struct containing AVClass. This is
+ * useful for searching for options without needing to allocate the corresponding
+ * object.
+ */
+#define AV_OPT_SEARCH_FAKE_OBJ 0x0002
+
+/**
+ * Look for an option in an object. Consider only options which
+ * have all the specified flags set.
+ *
+ * @param[in] obj A pointer to a struct whose first element is a
+ * pointer to an AVClass.
+ * Alternatively a double pointer to an AVClass, if
+ * AV_OPT_SEARCH_FAKE_OBJ search flag is set.
+ * @param[in] name The name of the option to look for.
+ * @param[in] unit When searching for named constants, name of the unit
+ * it belongs to.
+ * @param opt_flags Find only options with all the specified flags set (AV_OPT_FLAG).
+ * @param search_flags A combination of AV_OPT_SEARCH_*.
+ *
+ * @return A pointer to the option found, or NULL if no option
+ * was found.
+ *
+ * @note Options found with AV_OPT_SEARCH_CHILDREN flag may not be settable
+ * directly with av_set_string3(). Use special calls which take an options
+ * AVDictionary (e.g. avformat_open_input()) to set options found with this
+ * flag.
+ */
+const AVOption *av_opt_find(void *obj, const char *name, const char *unit,
+ int opt_flags, int search_flags);
+
+/**
+ * Look for an option in an object. Consider only options which
+ * have all the specified flags set.
+ *
+ * @param[in] obj A pointer to a struct whose first element is a
+ * pointer to an AVClass.
+ * Alternatively a double pointer to an AVClass, if
+ * AV_OPT_SEARCH_FAKE_OBJ search flag is set.
+ * @param[in] name The name of the option to look for.
+ * @param[in] unit When searching for named constants, name of the unit
+ * it belongs to.
+ * @param opt_flags Find only options with all the specified flags set (AV_OPT_FLAG).
+ * @param search_flags A combination of AV_OPT_SEARCH_*.
+ * @param[out] target_obj if non-NULL, an object to which the option belongs will be
+ * written here. It may be different from obj if AV_OPT_SEARCH_CHILDREN is present
+ * in search_flags. This parameter is ignored if search_flags contain
+ * AV_OPT_SEARCH_FAKE_OBJ.
+ *
+ * @return A pointer to the option found, or NULL if no option
+ * was found.
+ */
+const AVOption *av_opt_find2(void *obj, const char *name, const char *unit,
+ int opt_flags, int search_flags, void **target_obj);
+
+/**
+ * Iterate over all AVOptions belonging to obj.
+ *
+ * @param obj an AVOptions-enabled struct or a double pointer to an
+ * AVClass describing it.
+ * @param prev result of the previous call to av_opt_next() on this object
+ * or NULL
+ * @return next AVOption or NULL
+ */
+const AVOption *av_opt_next(void *obj, const AVOption *prev);
+
+/**
+ * Iterate over AVOptions-enabled children of obj.
+ *
+ * @param prev result of a previous call to this function or NULL
+ * @return next AVOptions-enabled child or NULL
+ */
+void *av_opt_child_next(void *obj, void *prev);
+
+/**
+ * Iterate over potential AVOptions-enabled children of parent.
+ *
+ * @param prev result of a previous call to this function or NULL
+ * @return AVClass corresponding to next potential child or NULL
+ */
+const AVClass *av_opt_child_class_next(const AVClass *parent, const AVClass *prev);
+
+/**
+ * @defgroup opt_set_funcs Option setting functions
+ * @{
+ * Those functions set the field of obj with the given name to value.
+ *
+ * @param[in] obj A struct whose first element is a pointer to an AVClass.
+ * @param[in] name the name of the field to set
+ * @param[in] val The value to set. In case of av_opt_set() if the field is not
+ * of a string type, then the given string is parsed.
+ * SI postfixes and some named scalars are supported.
+ * If the field is of a numeric type, it has to be a numeric or named
+ * scalar. Behavior with more than one scalar and +- infix operators
+ * is undefined.
+ * If the field is of a flags type, it has to be a sequence of numeric
+ * scalars or named flags separated by '+' or '-'. Prefixing a flag
+ * with '+' causes it to be set without affecting the other flags;
+ * similarly, '-' unsets a flag.
+ * @param search_flags flags passed to av_opt_find2. I.e. if AV_OPT_SEARCH_CHILDREN
+ * is passed here, then the option may be set on a child of obj.
+ *
+ * @return 0 if the value has been set, or an AVERROR code in case of
+ * error:
+ * AVERROR_OPTION_NOT_FOUND if no matching option exists
+ * AVERROR(ERANGE) if the value is out of range
+ * AVERROR(EINVAL) if the value is not valid
+ */
+int av_opt_set (void *obj, const char *name, const char *val, int search_flags);
+int av_opt_set_int (void *obj, const char *name, int64_t val, int search_flags);
+int av_opt_set_double(void *obj, const char *name, double val, int search_flags);
+int av_opt_set_q (void *obj, const char *name, AVRational val, int search_flags);
+int av_opt_set_bin (void *obj, const char *name, const uint8_t *val, int size, int search_flags);
+/**
+ * @}
+ */
+
+/**
+ * @defgroup opt_get_funcs Option getting functions
+ * @{
+ * Those functions get a value of the option with the given name from an object.
+ *
+ * @param[in] obj a struct whose first element is a pointer to an AVClass.
+ * @param[in] name name of the option to get.
+ * @param[in] search_flags flags passed to av_opt_find2. I.e. if AV_OPT_SEARCH_CHILDREN
+ * is passed here, then the option may be found in a child of obj.
+ * @param[out] out_val value of the option will be written here
+ * @return 0 on success, a negative error code otherwise
+ */
+/**
+ * @note the returned string will av_malloc()ed and must be av_free()ed by the caller
+ */
+int av_opt_get (void *obj, const char *name, int search_flags, uint8_t **out_val);
+int av_opt_get_int (void *obj, const char *name, int search_flags, int64_t *out_val);
+int av_opt_get_double(void *obj, const char *name, int search_flags, double *out_val);
+int av_opt_get_q (void *obj, const char *name, int search_flags, AVRational *out_val);
+/**
+ * @}
+ * @}
+ */
+
+#endif /* AVUTIL_OPT_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/parseutils.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/parseutils.h
new file mode 100644
index 0000000000..0844abb2f0
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/parseutils.h
@@ -0,0 +1,124 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_PARSEUTILS_H
+#define AVUTIL_PARSEUTILS_H
+
+#include <time.h>
+
+#include "rational.h"
+
+/**
+ * @file
+ * misc parsing utilities
+ */
+
+/**
+ * Parse str and put in width_ptr and height_ptr the detected values.
+ *
+ * @param[in,out] width_ptr pointer to the variable which will contain the detected
+ * width value
+ * @param[in,out] height_ptr pointer to the variable which will contain the detected
+ * height value
+ * @param[in] str the string to parse: it has to be a string in the format
+ * width x height or a valid video size abbreviation.
+ * @return >= 0 on success, a negative error code otherwise
+ */
+int av_parse_video_size(int *width_ptr, int *height_ptr, const char *str);
+
+/**
+ * Parse str and store the detected values in *rate.
+ *
+ * @param[in,out] rate pointer to the AVRational which will contain the detected
+ * frame rate
+ * @param[in] str the string to parse: it has to be a string in the format
+ * rate_num / rate_den, a float number or a valid video rate abbreviation
+ * @return >= 0 on success, a negative error code otherwise
+ */
+int av_parse_video_rate(AVRational *rate, const char *str);
+
+/**
+ * Put the RGBA values that correspond to color_string in rgba_color.
+ *
+ * @param color_string a string specifying a color. It can be the name of
+ * a color (case insensitive match) or a [0x|#]RRGGBB[AA] sequence,
+ * possibly followed by "@" and a string representing the alpha
+ * component.
+ * The alpha component may be a string composed by "0x" followed by an
+ * hexadecimal number or a decimal number between 0.0 and 1.0, which
+ * represents the opacity value (0x00/0.0 means completely transparent,
+ * 0xff/1.0 completely opaque).
+ * If the alpha component is not specified then 0xff is assumed.
+ * The string "random" will result in a random color.
+ * @param slen length of the initial part of color_string containing the
+ * color. It can be set to -1 if color_string is a null terminated string
+ * containing nothing else than the color.
+ * @return >= 0 in case of success, a negative value in case of
+ * failure (for example if color_string cannot be parsed).
+ */
+int av_parse_color(uint8_t *rgba_color, const char *color_string, int slen,
+ void *log_ctx);
+
+/**
+ * Parse timestr and return in *time a corresponding number of
+ * microseconds.
+ *
+ * @param timeval puts here the number of microseconds corresponding
+ * to the string in timestr. If the string represents a duration, it
+ * is the number of microseconds contained in the time interval. If
+ * the string is a date, is the number of microseconds since 1st of
+ * January, 1970 up to the time of the parsed date. If timestr cannot
+ * be successfully parsed, set *time to INT64_MIN.
+
+ * @param timestr a string representing a date or a duration.
+ * - If a date the syntax is:
+ * @code
+ * [{YYYY-MM-DD|YYYYMMDD}[T|t| ]]{{HH[:MM[:SS[.m...]]]}|{HH[MM[SS[.m...]]]}}[Z]
+ * now
+ * @endcode
+ * If the value is "now" it takes the current time.
+ * Time is local time unless Z is appended, in which case it is
+ * interpreted as UTC.
+ * If the year-month-day part is not specified it takes the current
+ * year-month-day.
+ * - If a duration the syntax is:
+ * @code
+ * [-]HH[:MM[:SS[.m...]]]
+ * [-]S+[.m...]
+ * @endcode
+ * @param duration flag which tells how to interpret timestr, if not
+ * zero timestr is interpreted as a duration, otherwise as a date
+ * @return 0 in case of success, a negative value corresponding to an
+ * AVERROR code otherwise
+ */
+int av_parse_time(int64_t *timeval, const char *timestr, int duration);
+
+/**
+ * Attempt to find a specific tag in a URL.
+ *
+ * syntax: '?tag1=val1&tag2=val2...'. Little URL decoding is done.
+ * Return 1 if found.
+ */
+int av_find_info_tag(char *arg, int arg_size, const char *tag1, const char *info);
+
+/**
+ * Convert the decomposed UTC time in tm to a time_t value.
+ */
+time_t av_timegm(struct tm *tm);
+
+#endif /* AVUTIL_PARSEUTILS_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/pixdesc.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/pixdesc.h
new file mode 100644
index 0000000000..47e6bb838d
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/pixdesc.h
@@ -0,0 +1,223 @@
+/*
+ * pixel format descriptor
+ * Copyright (c) 2009 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_PIXDESC_H
+#define AVUTIL_PIXDESC_H
+
+#include <inttypes.h>
+#include "pixfmt.h"
+
+typedef struct AVComponentDescriptor{
+ uint16_t plane :2; ///< which of the 4 planes contains the component
+
+ /**
+ * Number of elements between 2 horizontally consecutive pixels minus 1.
+ * Elements are bits for bitstream formats, bytes otherwise.
+ */
+ uint16_t step_minus1 :3;
+
+ /**
+ * Number of elements before the component of the first pixel plus 1.
+ * Elements are bits for bitstream formats, bytes otherwise.
+ */
+ uint16_t offset_plus1 :3;
+ uint16_t shift :3; ///< number of least significant bits that must be shifted away to get the value
+ uint16_t depth_minus1 :4; ///< number of bits in the component minus 1
+}AVComponentDescriptor;
+
+/**
+ * Descriptor that unambiguously describes how the bits of a pixel are
+ * stored in the up to 4 data planes of an image. It also stores the
+ * subsampling factors and number of components.
+ *
+ * @note This is separate of the colorspace (RGB, YCbCr, YPbPr, JPEG-style YUV
+ * and all the YUV variants) AVPixFmtDescriptor just stores how values
+ * are stored not what these values represent.
+ */
+typedef struct AVPixFmtDescriptor{
+ const char *name;
+ uint8_t nb_components; ///< The number of components each pixel has, (1-4)
+
+ /**
+ * Amount to shift the luma width right to find the chroma width.
+ * For YV12 this is 1 for example.
+ * chroma_width = -((-luma_width) >> log2_chroma_w)
+ * The note above is needed to ensure rounding up.
+ * This value only refers to the chroma components.
+ */
+ uint8_t log2_chroma_w; ///< chroma_width = -((-luma_width )>>log2_chroma_w)
+
+ /**
+ * Amount to shift the luma height right to find the chroma height.
+ * For YV12 this is 1 for example.
+ * chroma_height= -((-luma_height) >> log2_chroma_h)
+ * The note above is needed to ensure rounding up.
+ * This value only refers to the chroma components.
+ */
+ uint8_t log2_chroma_h;
+ uint8_t flags;
+
+ /**
+ * Parameters that describe how pixels are packed. If the format
+ * has chroma components, they must be stored in comp[1] and
+ * comp[2].
+ */
+ AVComponentDescriptor comp[4];
+}AVPixFmtDescriptor;
+
+#define PIX_FMT_BE 1 ///< Pixel format is big-endian.
+#define PIX_FMT_PAL 2 ///< Pixel format has a palette in data[1], values are indexes in this palette.
+#define PIX_FMT_BITSTREAM 4 ///< All values of a component are bit-wise packed end to end.
+#define PIX_FMT_HWACCEL 8 ///< Pixel format is an HW accelerated format.
+#define PIX_FMT_PLANAR 16 ///< At least one pixel component is not in the first data plane
+#define PIX_FMT_RGB 32 ///< The pixel format contains RGB-like data (as opposed to YUV/grayscale)
+/**
+ * The pixel format is "pseudo-paletted". This means that Libav treats it as
+ * paletted internally, but the palette is generated by the decoder and is not
+ * stored in the file.
+ */
+#define PIX_FMT_PSEUDOPAL 64
+
+#define PIX_FMT_ALPHA 128 ///< The pixel format has an alpha channel
+
+
+#if FF_API_PIX_FMT_DESC
+/**
+ * The array of all the pixel format descriptors.
+ */
+extern const AVPixFmtDescriptor av_pix_fmt_descriptors[];
+#endif
+
+/**
+ * Read a line from an image, and write the values of the
+ * pixel format component c to dst.
+ *
+ * @param data the array containing the pointers to the planes of the image
+ * @param linesize the array containing the linesizes of the image
+ * @param desc the pixel format descriptor for the image
+ * @param x the horizontal coordinate of the first pixel to read
+ * @param y the vertical coordinate of the first pixel to read
+ * @param w the width of the line to read, that is the number of
+ * values to write to dst
+ * @param read_pal_component if not zero and the format is a paletted
+ * format writes the values corresponding to the palette
+ * component c in data[1] to dst, rather than the palette indexes in
+ * data[0]. The behavior is undefined if the format is not paletted.
+ */
+void av_read_image_line(uint16_t *dst, const uint8_t *data[4], const int linesize[4],
+ const AVPixFmtDescriptor *desc, int x, int y, int c, int w, int read_pal_component);
+
+/**
+ * Write the values from src to the pixel format component c of an
+ * image line.
+ *
+ * @param src array containing the values to write
+ * @param data the array containing the pointers to the planes of the
+ * image to write into. It is supposed to be zeroed.
+ * @param linesize the array containing the linesizes of the image
+ * @param desc the pixel format descriptor for the image
+ * @param x the horizontal coordinate of the first pixel to write
+ * @param y the vertical coordinate of the first pixel to write
+ * @param w the width of the line to write, that is the number of
+ * values to write to the image line
+ */
+void av_write_image_line(const uint16_t *src, uint8_t *data[4], const int linesize[4],
+ const AVPixFmtDescriptor *desc, int x, int y, int c, int w);
+
+/**
+ * Return the pixel format corresponding to name.
+ *
+ * If there is no pixel format with name name, then looks for a
+ * pixel format with the name corresponding to the native endian
+ * format of name.
+ * For example in a little-endian system, first looks for "gray16",
+ * then for "gray16le".
+ *
+ * Finally if no pixel format has been found, returns PIX_FMT_NONE.
+ */
+enum AVPixelFormat av_get_pix_fmt(const char *name);
+
+/**
+ * Return the short name for a pixel format, NULL in case pix_fmt is
+ * unknown.
+ *
+ * @see av_get_pix_fmt(), av_get_pix_fmt_string()
+ */
+const char *av_get_pix_fmt_name(enum AVPixelFormat pix_fmt);
+
+/**
+ * Print in buf the string corresponding to the pixel format with
+ * number pix_fmt, or an header if pix_fmt is negative.
+ *
+ * @param buf the buffer where to write the string
+ * @param buf_size the size of buf
+ * @param pix_fmt the number of the pixel format to print the
+ * corresponding info string, or a negative value to print the
+ * corresponding header.
+ */
+char *av_get_pix_fmt_string (char *buf, int buf_size, enum AVPixelFormat pix_fmt);
+
+/**
+ * Return the number of bits per pixel used by the pixel format
+ * described by pixdesc.
+ *
+ * The returned number of bits refers to the number of bits actually
+ * used for storing the pixel information, that is padding bits are
+ * not counted.
+ */
+int av_get_bits_per_pixel(const AVPixFmtDescriptor *pixdesc);
+
+/**
+ * @return a pixel format descriptor for provided pixel format or NULL if
+ * this pixel format is unknown.
+ */
+const AVPixFmtDescriptor *av_pix_fmt_desc_get(enum AVPixelFormat pix_fmt);
+
+/**
+ * Iterate over all pixel format descriptors known to libavutil.
+ *
+ * @param prev previous descriptor. NULL to get the first descriptor.
+ *
+ * @return next descriptor or NULL after the last descriptor
+ */
+const AVPixFmtDescriptor *av_pix_fmt_desc_next(const AVPixFmtDescriptor *prev);
+
+/**
+ * @return an AVPixelFormat id described by desc, or AV_PIX_FMT_NONE if desc
+ * is not a valid pointer to a pixel format descriptor.
+ */
+enum AVPixelFormat av_pix_fmt_desc_get_id(const AVPixFmtDescriptor *desc);
+
+/**
+ * Utility function to access log2_chroma_w log2_chroma_h from
+ * the pixel format AVPixFmtDescriptor.
+ *
+ * @param[in] pix_fmt the pixel format
+ * @param[out] h_shift store log2_chroma_h
+ * @param[out] v_shift store log2_chroma_w
+ *
+ * @return 0 on success, AVERROR(ENOSYS) on invalid or unknown pixel format
+ */
+int av_pix_fmt_get_chroma_sub_sample(enum AVPixelFormat pix_fmt,
+ int *h_shift, int *v_shift);
+
+
+#endif /* AVUTIL_PIXDESC_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/pixfmt.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/pixfmt.h
new file mode 100644
index 0000000000..1072f00895
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/pixfmt.h
@@ -0,0 +1,268 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_PIXFMT_H
+#define AVUTIL_PIXFMT_H
+
+/**
+ * @file
+ * pixel format definitions
+ *
+ */
+
+#include "libavutil/avconfig.h"
+#include "libavutil/version.h"
+
+/**
+ * Pixel format.
+ *
+ * @note
+ * PIX_FMT_RGB32 is handled in an endian-specific manner. An RGBA
+ * color is put together as:
+ * (A << 24) | (R << 16) | (G << 8) | B
+ * This is stored as BGRA on little-endian CPU architectures and ARGB on
+ * big-endian CPUs.
+ *
+ * @par
+ * When the pixel format is palettized RGB (PIX_FMT_PAL8), the palettized
+ * image data is stored in AVFrame.data[0]. The palette is transported in
+ * AVFrame.data[1], is 1024 bytes long (256 4-byte entries) and is
+ * formatted the same as in PIX_FMT_RGB32 described above (i.e., it is
+ * also endian-specific). Note also that the individual RGB palette
+ * components stored in AVFrame.data[1] should be in the range 0..255.
+ * This is important as many custom PAL8 video codecs that were designed
+ * to run on the IBM VGA graphics adapter use 6-bit palette components.
+ *
+ * @par
+ * For all the 8bit per pixel formats, an RGB32 palette is in data[1] like
+ * for pal8. This palette is filled in automatically by the function
+ * allocating the picture.
+ *
+ * @note
+ * Make sure that all newly added big-endian formats have pix_fmt & 1 == 1
+ * and that all newly added little-endian formats have pix_fmt & 1 == 0.
+ * This allows simpler detection of big vs little-endian.
+ */
+enum AVPixelFormat {
+ AV_PIX_FMT_NONE = -1,
+ AV_PIX_FMT_YUV420P, ///< planar YUV 4:2:0, 12bpp, (1 Cr & Cb sample per 2x2 Y samples)
+ AV_PIX_FMT_YUYV422, ///< packed YUV 4:2:2, 16bpp, Y0 Cb Y1 Cr
+ AV_PIX_FMT_RGB24, ///< packed RGB 8:8:8, 24bpp, RGBRGB...
+ AV_PIX_FMT_BGR24, ///< packed RGB 8:8:8, 24bpp, BGRBGR...
+ AV_PIX_FMT_YUV422P, ///< planar YUV 4:2:2, 16bpp, (1 Cr & Cb sample per 2x1 Y samples)
+ AV_PIX_FMT_YUV444P, ///< planar YUV 4:4:4, 24bpp, (1 Cr & Cb sample per 1x1 Y samples)
+ AV_PIX_FMT_YUV410P, ///< planar YUV 4:1:0, 9bpp, (1 Cr & Cb sample per 4x4 Y samples)
+ AV_PIX_FMT_YUV411P, ///< planar YUV 4:1:1, 12bpp, (1 Cr & Cb sample per 4x1 Y samples)
+ AV_PIX_FMT_GRAY8, ///< Y , 8bpp
+ AV_PIX_FMT_MONOWHITE, ///< Y , 1bpp, 0 is white, 1 is black, in each byte pixels are ordered from the msb to the lsb
+ AV_PIX_FMT_MONOBLACK, ///< Y , 1bpp, 0 is black, 1 is white, in each byte pixels are ordered from the msb to the lsb
+ AV_PIX_FMT_PAL8, ///< 8 bit with PIX_FMT_RGB32 palette
+ AV_PIX_FMT_YUVJ420P, ///< planar YUV 4:2:0, 12bpp, full scale (JPEG), deprecated in favor of PIX_FMT_YUV420P and setting color_range
+ AV_PIX_FMT_YUVJ422P, ///< planar YUV 4:2:2, 16bpp, full scale (JPEG), deprecated in favor of PIX_FMT_YUV422P and setting color_range
+ AV_PIX_FMT_YUVJ444P, ///< planar YUV 4:4:4, 24bpp, full scale (JPEG), deprecated in favor of PIX_FMT_YUV444P and setting color_range
+ AV_PIX_FMT_XVMC_MPEG2_MC,///< XVideo Motion Acceleration via common packet passing
+ AV_PIX_FMT_XVMC_MPEG2_IDCT,
+ AV_PIX_FMT_UYVY422, ///< packed YUV 4:2:2, 16bpp, Cb Y0 Cr Y1
+ AV_PIX_FMT_UYYVYY411, ///< packed YUV 4:1:1, 12bpp, Cb Y0 Y1 Cr Y2 Y3
+ AV_PIX_FMT_BGR8, ///< packed RGB 3:3:2, 8bpp, (msb)2B 3G 3R(lsb)
+ AV_PIX_FMT_BGR4, ///< packed RGB 1:2:1 bitstream, 4bpp, (msb)1B 2G 1R(lsb), a byte contains two pixels, the first pixel in the byte is the one composed by the 4 msb bits
+ AV_PIX_FMT_BGR4_BYTE, ///< packed RGB 1:2:1, 8bpp, (msb)1B 2G 1R(lsb)
+ AV_PIX_FMT_RGB8, ///< packed RGB 3:3:2, 8bpp, (msb)2R 3G 3B(lsb)
+ AV_PIX_FMT_RGB4, ///< packed RGB 1:2:1 bitstream, 4bpp, (msb)1R 2G 1B(lsb), a byte contains two pixels, the first pixel in the byte is the one composed by the 4 msb bits
+ AV_PIX_FMT_RGB4_BYTE, ///< packed RGB 1:2:1, 8bpp, (msb)1R 2G 1B(lsb)
+ AV_PIX_FMT_NV12, ///< planar YUV 4:2:0, 12bpp, 1 plane for Y and 1 plane for the UV components, which are interleaved (first byte U and the following byte V)
+ AV_PIX_FMT_NV21, ///< as above, but U and V bytes are swapped
+
+ AV_PIX_FMT_ARGB, ///< packed ARGB 8:8:8:8, 32bpp, ARGBARGB...
+ AV_PIX_FMT_RGBA, ///< packed RGBA 8:8:8:8, 32bpp, RGBARGBA...
+ AV_PIX_FMT_ABGR, ///< packed ABGR 8:8:8:8, 32bpp, ABGRABGR...
+ AV_PIX_FMT_BGRA, ///< packed BGRA 8:8:8:8, 32bpp, BGRABGRA...
+
+ AV_PIX_FMT_GRAY16BE, ///< Y , 16bpp, big-endian
+ AV_PIX_FMT_GRAY16LE, ///< Y , 16bpp, little-endian
+ AV_PIX_FMT_YUV440P, ///< planar YUV 4:4:0 (1 Cr & Cb sample per 1x2 Y samples)
+ AV_PIX_FMT_YUVJ440P, ///< planar YUV 4:4:0 full scale (JPEG), deprecated in favor of PIX_FMT_YUV440P and setting color_range
+ AV_PIX_FMT_YUVA420P, ///< planar YUV 4:2:0, 20bpp, (1 Cr & Cb sample per 2x2 Y & A samples)
+ AV_PIX_FMT_VDPAU_H264,///< H.264 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ AV_PIX_FMT_VDPAU_MPEG1,///< MPEG-1 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ AV_PIX_FMT_VDPAU_MPEG2,///< MPEG-2 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ AV_PIX_FMT_VDPAU_WMV3,///< WMV3 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ AV_PIX_FMT_VDPAU_VC1, ///< VC-1 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ AV_PIX_FMT_RGB48BE, ///< packed RGB 16:16:16, 48bpp, 16R, 16G, 16B, the 2-byte value for each R/G/B component is stored as big-endian
+ AV_PIX_FMT_RGB48LE, ///< packed RGB 16:16:16, 48bpp, 16R, 16G, 16B, the 2-byte value for each R/G/B component is stored as little-endian
+
+ AV_PIX_FMT_RGB565BE, ///< packed RGB 5:6:5, 16bpp, (msb) 5R 6G 5B(lsb), big-endian
+ AV_PIX_FMT_RGB565LE, ///< packed RGB 5:6:5, 16bpp, (msb) 5R 6G 5B(lsb), little-endian
+ AV_PIX_FMT_RGB555BE, ///< packed RGB 5:5:5, 16bpp, (msb)1A 5R 5G 5B(lsb), big-endian, most significant bit to 0
+ AV_PIX_FMT_RGB555LE, ///< packed RGB 5:5:5, 16bpp, (msb)1A 5R 5G 5B(lsb), little-endian, most significant bit to 0
+
+ AV_PIX_FMT_BGR565BE, ///< packed BGR 5:6:5, 16bpp, (msb) 5B 6G 5R(lsb), big-endian
+ AV_PIX_FMT_BGR565LE, ///< packed BGR 5:6:5, 16bpp, (msb) 5B 6G 5R(lsb), little-endian
+ AV_PIX_FMT_BGR555BE, ///< packed BGR 5:5:5, 16bpp, (msb)1A 5B 5G 5R(lsb), big-endian, most significant bit to 1
+ AV_PIX_FMT_BGR555LE, ///< packed BGR 5:5:5, 16bpp, (msb)1A 5B 5G 5R(lsb), little-endian, most significant bit to 1
+
+ AV_PIX_FMT_VAAPI_MOCO, ///< HW acceleration through VA API at motion compensation entry-point, Picture.data[3] contains a vaapi_render_state struct which contains macroblocks as well as various fields extracted from headers
+ AV_PIX_FMT_VAAPI_IDCT, ///< HW acceleration through VA API at IDCT entry-point, Picture.data[3] contains a vaapi_render_state struct which contains fields extracted from headers
+ AV_PIX_FMT_VAAPI_VLD, ///< HW decoding through VA API, Picture.data[3] contains a vaapi_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+
+ AV_PIX_FMT_YUV420P16LE, ///< planar YUV 4:2:0, 24bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian
+ AV_PIX_FMT_YUV420P16BE, ///< planar YUV 4:2:0, 24bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian
+ AV_PIX_FMT_YUV422P16LE, ///< planar YUV 4:2:2, 32bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ AV_PIX_FMT_YUV422P16BE, ///< planar YUV 4:2:2, 32bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+ AV_PIX_FMT_YUV444P16LE, ///< planar YUV 4:4:4, 48bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian
+ AV_PIX_FMT_YUV444P16BE, ///< planar YUV 4:4:4, 48bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian
+ AV_PIX_FMT_VDPAU_MPEG4, ///< MPEG4 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ AV_PIX_FMT_DXVA2_VLD, ///< HW decoding through DXVA2, Picture.data[3] contains a LPDIRECT3DSURFACE9 pointer
+
+ AV_PIX_FMT_RGB444LE, ///< packed RGB 4:4:4, 16bpp, (msb)4A 4R 4G 4B(lsb), little-endian, most significant bits to 0
+ AV_PIX_FMT_RGB444BE, ///< packed RGB 4:4:4, 16bpp, (msb)4A 4R 4G 4B(lsb), big-endian, most significant bits to 0
+ AV_PIX_FMT_BGR444LE, ///< packed BGR 4:4:4, 16bpp, (msb)4A 4B 4G 4R(lsb), little-endian, most significant bits to 1
+ AV_PIX_FMT_BGR444BE, ///< packed BGR 4:4:4, 16bpp, (msb)4A 4B 4G 4R(lsb), big-endian, most significant bits to 1
+ AV_PIX_FMT_Y400A, ///< 8bit gray, 8bit alpha
+ AV_PIX_FMT_BGR48BE, ///< packed RGB 16:16:16, 48bpp, 16B, 16G, 16R, the 2-byte value for each R/G/B component is stored as big-endian
+ AV_PIX_FMT_BGR48LE, ///< packed RGB 16:16:16, 48bpp, 16B, 16G, 16R, the 2-byte value for each R/G/B component is stored as little-endian
+ AV_PIX_FMT_YUV420P9BE, ///< planar YUV 4:2:0, 13.5bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian
+ AV_PIX_FMT_YUV420P9LE, ///< planar YUV 4:2:0, 13.5bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian
+ AV_PIX_FMT_YUV420P10BE,///< planar YUV 4:2:0, 15bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian
+ AV_PIX_FMT_YUV420P10LE,///< planar YUV 4:2:0, 15bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian
+ AV_PIX_FMT_YUV422P10BE,///< planar YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+ AV_PIX_FMT_YUV422P10LE,///< planar YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ AV_PIX_FMT_YUV444P9BE, ///< planar YUV 4:4:4, 27bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian
+ AV_PIX_FMT_YUV444P9LE, ///< planar YUV 4:4:4, 27bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian
+ AV_PIX_FMT_YUV444P10BE,///< planar YUV 4:4:4, 30bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian
+ AV_PIX_FMT_YUV444P10LE,///< planar YUV 4:4:4, 30bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian
+ AV_PIX_FMT_YUV422P9BE, ///< planar YUV 4:2:2, 18bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+ AV_PIX_FMT_YUV422P9LE, ///< planar YUV 4:2:2, 18bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ AV_PIX_FMT_VDA_VLD, ///< hardware decoding through VDA
+ AV_PIX_FMT_GBRP, ///< planar GBR 4:4:4 24bpp
+ AV_PIX_FMT_GBRP9BE, ///< planar GBR 4:4:4 27bpp, big-endian
+ AV_PIX_FMT_GBRP9LE, ///< planar GBR 4:4:4 27bpp, little-endian
+ AV_PIX_FMT_GBRP10BE, ///< planar GBR 4:4:4 30bpp, big-endian
+ AV_PIX_FMT_GBRP10LE, ///< planar GBR 4:4:4 30bpp, little-endian
+ AV_PIX_FMT_GBRP16BE, ///< planar GBR 4:4:4 48bpp, big-endian
+ AV_PIX_FMT_GBRP16LE, ///< planar GBR 4:4:4 48bpp, little-endian
+ AV_PIX_FMT_YUVA422P, ///< planar YUV 4:2:2 24bpp, (1 Cr & Cb sample per 2x1 Y & A samples)
+ AV_PIX_FMT_YUVA444P, ///< planar YUV 4:4:4 32bpp, (1 Cr & Cb sample per 1x1 Y & A samples)
+ AV_PIX_FMT_YUVA420P9BE, ///< planar YUV 4:2:0 22.5bpp, (1 Cr & Cb sample per 2x2 Y & A samples), big-endian
+ AV_PIX_FMT_YUVA420P9LE, ///< planar YUV 4:2:0 22.5bpp, (1 Cr & Cb sample per 2x2 Y & A samples), little-endian
+ AV_PIX_FMT_YUVA422P9BE, ///< planar YUV 4:2:2 27bpp, (1 Cr & Cb sample per 2x1 Y & A samples), big-endian
+ AV_PIX_FMT_YUVA422P9LE, ///< planar YUV 4:2:2 27bpp, (1 Cr & Cb sample per 2x1 Y & A samples), little-endian
+ AV_PIX_FMT_YUVA444P9BE, ///< planar YUV 4:4:4 36bpp, (1 Cr & Cb sample per 1x1 Y & A samples), big-endian
+ AV_PIX_FMT_YUVA444P9LE, ///< planar YUV 4:4:4 36bpp, (1 Cr & Cb sample per 1x1 Y & A samples), little-endian
+ AV_PIX_FMT_YUVA420P10BE, ///< planar YUV 4:2:0 25bpp, (1 Cr & Cb sample per 2x2 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA420P10LE, ///< planar YUV 4:2:0 25bpp, (1 Cr & Cb sample per 2x2 Y & A samples, little-endian)
+ AV_PIX_FMT_YUVA422P10BE, ///< planar YUV 4:2:2 30bpp, (1 Cr & Cb sample per 2x1 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA422P10LE, ///< planar YUV 4:2:2 30bpp, (1 Cr & Cb sample per 2x1 Y & A samples, little-endian)
+ AV_PIX_FMT_YUVA444P10BE, ///< planar YUV 4:4:4 40bpp, (1 Cr & Cb sample per 1x1 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA444P10LE, ///< planar YUV 4:4:4 40bpp, (1 Cr & Cb sample per 1x1 Y & A samples, little-endian)
+ AV_PIX_FMT_YUVA420P16BE, ///< planar YUV 4:2:0 40bpp, (1 Cr & Cb sample per 2x2 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA420P16LE, ///< planar YUV 4:2:0 40bpp, (1 Cr & Cb sample per 2x2 Y & A samples, little-endian)
+ AV_PIX_FMT_YUVA422P16BE, ///< planar YUV 4:2:2 48bpp, (1 Cr & Cb sample per 2x1 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA422P16LE, ///< planar YUV 4:2:2 48bpp, (1 Cr & Cb sample per 2x1 Y & A samples, little-endian)
+ AV_PIX_FMT_YUVA444P16BE, ///< planar YUV 4:4:4 64bpp, (1 Cr & Cb sample per 1x1 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA444P16LE, ///< planar YUV 4:4:4 64bpp, (1 Cr & Cb sample per 1x1 Y & A samples, little-endian)
+ AV_PIX_FMT_NB, ///< number of pixel formats, DO NOT USE THIS if you want to link with shared libav* because the number of formats might differ between versions
+
+#if FF_API_PIX_FMT
+#include "old_pix_fmts.h"
+#endif
+};
+
+#if AV_HAVE_BIGENDIAN
+# define AV_PIX_FMT_NE(be, le) AV_PIX_FMT_##be
+#else
+# define AV_PIX_FMT_NE(be, le) AV_PIX_FMT_##le
+#endif
+
+#define AV_PIX_FMT_RGB32 AV_PIX_FMT_NE(ARGB, BGRA)
+#define AV_PIX_FMT_RGB32_1 AV_PIX_FMT_NE(RGBA, ABGR)
+#define AV_PIX_FMT_BGR32 AV_PIX_FMT_NE(ABGR, RGBA)
+#define AV_PIX_FMT_BGR32_1 AV_PIX_FMT_NE(BGRA, ARGB)
+
+#define AV_PIX_FMT_GRAY16 AV_PIX_FMT_NE(GRAY16BE, GRAY16LE)
+#define AV_PIX_FMT_RGB48 AV_PIX_FMT_NE(RGB48BE, RGB48LE)
+#define AV_PIX_FMT_RGB565 AV_PIX_FMT_NE(RGB565BE, RGB565LE)
+#define AV_PIX_FMT_RGB555 AV_PIX_FMT_NE(RGB555BE, RGB555LE)
+#define AV_PIX_FMT_RGB444 AV_PIX_FMT_NE(RGB444BE, RGB444LE)
+#define AV_PIX_FMT_BGR48 AV_PIX_FMT_NE(BGR48BE, BGR48LE)
+#define AV_PIX_FMT_BGR565 AV_PIX_FMT_NE(BGR565BE, BGR565LE)
+#define AV_PIX_FMT_BGR555 AV_PIX_FMT_NE(BGR555BE, BGR555LE)
+#define AV_PIX_FMT_BGR444 AV_PIX_FMT_NE(BGR444BE, BGR444LE)
+
+#define AV_PIX_FMT_YUV420P9 AV_PIX_FMT_NE(YUV420P9BE , YUV420P9LE)
+#define AV_PIX_FMT_YUV422P9 AV_PIX_FMT_NE(YUV422P9BE , YUV422P9LE)
+#define AV_PIX_FMT_YUV444P9 AV_PIX_FMT_NE(YUV444P9BE , YUV444P9LE)
+#define AV_PIX_FMT_YUV420P10 AV_PIX_FMT_NE(YUV420P10BE, YUV420P10LE)
+#define AV_PIX_FMT_YUV422P10 AV_PIX_FMT_NE(YUV422P10BE, YUV422P10LE)
+#define AV_PIX_FMT_YUV444P10 AV_PIX_FMT_NE(YUV444P10BE, YUV444P10LE)
+#define AV_PIX_FMT_YUV420P16 AV_PIX_FMT_NE(YUV420P16BE, YUV420P16LE)
+#define AV_PIX_FMT_YUV422P16 AV_PIX_FMT_NE(YUV422P16BE, YUV422P16LE)
+#define AV_PIX_FMT_YUV444P16 AV_PIX_FMT_NE(YUV444P16BE, YUV444P16LE)
+
+#define AV_PIX_FMT_GBRP9 AV_PIX_FMT_NE(GBRP9BE , GBRP9LE)
+#define AV_PIX_FMT_GBRP10 AV_PIX_FMT_NE(GBRP10BE, GBRP10LE)
+#define AV_PIX_FMT_GBRP16 AV_PIX_FMT_NE(GBRP16BE, GBRP16LE)
+
+#define AV_PIX_FMT_YUVA420P9 AV_PIX_FMT_NE(YUVA420P9BE , YUVA420P9LE)
+#define AV_PIX_FMT_YUVA422P9 AV_PIX_FMT_NE(YUVA422P9BE , YUVA422P9LE)
+#define AV_PIX_FMT_YUVA444P9 AV_PIX_FMT_NE(YUVA444P9BE , YUVA444P9LE)
+#define AV_PIX_FMT_YUVA420P10 AV_PIX_FMT_NE(YUVA420P10BE, YUVA420P10LE)
+#define AV_PIX_FMT_YUVA422P10 AV_PIX_FMT_NE(YUVA422P10BE, YUVA422P10LE)
+#define AV_PIX_FMT_YUVA444P10 AV_PIX_FMT_NE(YUVA444P10BE, YUVA444P10LE)
+#define AV_PIX_FMT_YUVA420P16 AV_PIX_FMT_NE(YUVA420P16BE, YUVA420P16LE)
+#define AV_PIX_FMT_YUVA422P16 AV_PIX_FMT_NE(YUVA422P16BE, YUVA422P16LE)
+#define AV_PIX_FMT_YUVA444P16 AV_PIX_FMT_NE(YUVA444P16BE, YUVA444P16LE)
+
+#if FF_API_PIX_FMT
+#define PixelFormat AVPixelFormat
+
+#define PIX_FMT_NE(be, le) AV_PIX_FMT_NE(be, le)
+
+#define PIX_FMT_RGB32 AV_PIX_FMT_RGB32
+#define PIX_FMT_RGB32_1 AV_PIX_FMT_RGB32_1
+#define PIX_FMT_BGR32 AV_PIX_FMT_BGR32
+#define PIX_FMT_BGR32_1 AV_PIX_FMT_BGR32_1
+
+#define PIX_FMT_GRAY16 AV_PIX_FMT_GRAY16
+#define PIX_FMT_RGB48 AV_PIX_FMT_RGB48
+#define PIX_FMT_RGB565 AV_PIX_FMT_RGB565
+#define PIX_FMT_RGB555 AV_PIX_FMT_RGB555
+#define PIX_FMT_RGB444 AV_PIX_FMT_RGB444
+#define PIX_FMT_BGR48 AV_PIX_FMT_BGR48
+#define PIX_FMT_BGR565 AV_PIX_FMT_BGR565
+#define PIX_FMT_BGR555 AV_PIX_FMT_BGR555
+#define PIX_FMT_BGR444 AV_PIX_FMT_BGR444
+
+#define PIX_FMT_YUV420P9 AV_PIX_FMT_YUV420P9
+#define PIX_FMT_YUV422P9 AV_PIX_FMT_YUV422P9
+#define PIX_FMT_YUV444P9 AV_PIX_FMT_YUV444P9
+#define PIX_FMT_YUV420P10 AV_PIX_FMT_YUV420P10
+#define PIX_FMT_YUV422P10 AV_PIX_FMT_YUV422P10
+#define PIX_FMT_YUV444P10 AV_PIX_FMT_YUV444P10
+#define PIX_FMT_YUV420P16 AV_PIX_FMT_YUV420P16
+#define PIX_FMT_YUV422P16 AV_PIX_FMT_YUV422P16
+#define PIX_FMT_YUV444P16 AV_PIX_FMT_YUV444P16
+
+#define PIX_FMT_GBRP9 AV_PIX_FMT_GBRP9
+#define PIX_FMT_GBRP10 AV_PIX_FMT_GBRP10
+#define PIX_FMT_GBRP16 AV_PIX_FMT_GBRP16
+#endif
+
+#endif /* AVUTIL_PIXFMT_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/random_seed.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/random_seed.h
new file mode 100644
index 0000000000..b1fad13d07
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/random_seed.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2009 Baptiste Coudurier <baptiste.coudurier@gmail.com>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_RANDOM_SEED_H
+#define AVUTIL_RANDOM_SEED_H
+
+#include <stdint.h>
+/**
+ * @addtogroup lavu_crypto
+ * @{
+ */
+
+/**
+ * Get random data.
+ *
+ * This function can be called repeatedly to generate more random bits
+ * as needed. It is generally quite slow, and usually used to seed a
+ * PRNG. As it uses /dev/urandom and /dev/random, the quality of the
+ * returned random data depends on the platform.
+ */
+uint32_t av_get_random_seed(void);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_RANDOM_SEED_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/rational.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/rational.h
new file mode 100644
index 0000000000..5d7dab7fd0
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/rational.h
@@ -0,0 +1,155 @@
+/*
+ * rational numbers
+ * Copyright (c) 2003 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * rational numbers
+ * @author Michael Niedermayer <michaelni@gmx.at>
+ */
+
+#ifndef AVUTIL_RATIONAL_H
+#define AVUTIL_RATIONAL_H
+
+#include <stdint.h>
+#include <limits.h>
+#include "attributes.h"
+
+/**
+ * @addtogroup lavu_math
+ * @{
+ */
+
+/**
+ * rational number numerator/denominator
+ */
+typedef struct AVRational{
+ int num; ///< numerator
+ int den; ///< denominator
+} AVRational;
+
+/**
+ * Compare two rationals.
+ * @param a first rational
+ * @param b second rational
+ * @return 0 if a==b, 1 if a>b, -1 if a<b, and INT_MIN if one of the
+ * values is of the form 0/0
+ */
+static inline int av_cmp_q(AVRational a, AVRational b){
+ const int64_t tmp= a.num * (int64_t)b.den - b.num * (int64_t)a.den;
+
+ if(tmp) return ((tmp ^ a.den ^ b.den)>>63)|1;
+ else if(b.den && a.den) return 0;
+ else if(a.num && b.num) return (a.num>>31) - (b.num>>31);
+ else return INT_MIN;
+}
+
+/**
+ * Convert rational to double.
+ * @param a rational to convert
+ * @return (double) a
+ */
+static inline double av_q2d(AVRational a){
+ return a.num / (double) a.den;
+}
+
+/**
+ * Reduce a fraction.
+ * This is useful for framerate calculations.
+ * @param dst_num destination numerator
+ * @param dst_den destination denominator
+ * @param num source numerator
+ * @param den source denominator
+ * @param max the maximum allowed for dst_num & dst_den
+ * @return 1 if exact, 0 otherwise
+ */
+int av_reduce(int *dst_num, int *dst_den, int64_t num, int64_t den, int64_t max);
+
+/**
+ * Multiply two rationals.
+ * @param b first rational
+ * @param c second rational
+ * @return b*c
+ */
+AVRational av_mul_q(AVRational b, AVRational c) av_const;
+
+/**
+ * Divide one rational by another.
+ * @param b first rational
+ * @param c second rational
+ * @return b/c
+ */
+AVRational av_div_q(AVRational b, AVRational c) av_const;
+
+/**
+ * Add two rationals.
+ * @param b first rational
+ * @param c second rational
+ * @return b+c
+ */
+AVRational av_add_q(AVRational b, AVRational c) av_const;
+
+/**
+ * Subtract one rational from another.
+ * @param b first rational
+ * @param c second rational
+ * @return b-c
+ */
+AVRational av_sub_q(AVRational b, AVRational c) av_const;
+
+/**
+ * Invert a rational.
+ * @param q value
+ * @return 1 / q
+ */
+static av_always_inline AVRational av_inv_q(AVRational q)
+{
+ AVRational r = { q.den, q.num };
+ return r;
+}
+
+/**
+ * Convert a double precision floating point number to a rational.
+ * inf is expressed as {1,0} or {-1,0} depending on the sign.
+ *
+ * @param d double to convert
+ * @param max the maximum allowed numerator and denominator
+ * @return (AVRational) d
+ */
+AVRational av_d2q(double d, int max) av_const;
+
+/**
+ * @return 1 if q1 is nearer to q than q2, -1 if q2 is nearer
+ * than q1, 0 if they have the same distance.
+ */
+int av_nearer_q(AVRational q, AVRational q1, AVRational q2);
+
+/**
+ * Find the nearest value in q_list to q.
+ * @param q_list an array of rationals terminated by {0, 0}
+ * @return the index of the nearest value found in the array
+ */
+int av_find_nearest_q_idx(AVRational q, const AVRational* q_list);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_RATIONAL_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/samplefmt.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/samplefmt.h
new file mode 100644
index 0000000000..33cbdedf5f
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/samplefmt.h
@@ -0,0 +1,220 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_SAMPLEFMT_H
+#define AVUTIL_SAMPLEFMT_H
+
+#include <stdint.h>
+
+#include "avutil.h"
+#include "attributes.h"
+
+/**
+ * Audio Sample Formats
+ *
+ * @par
+ * The data described by the sample format is always in native-endian order.
+ * Sample values can be expressed by native C types, hence the lack of a signed
+ * 24-bit sample format even though it is a common raw audio data format.
+ *
+ * @par
+ * The floating-point formats are based on full volume being in the range
+ * [-1.0, 1.0]. Any values outside this range are beyond full volume level.
+ *
+ * @par
+ * The data layout as used in av_samples_fill_arrays() and elsewhere in Libav
+ * (such as AVFrame in libavcodec) is as follows:
+ *
+ * For planar sample formats, each audio channel is in a separate data plane,
+ * and linesize is the buffer size, in bytes, for a single plane. All data
+ * planes must be the same size. For packed sample formats, only the first data
+ * plane is used, and samples for each channel are interleaved. In this case,
+ * linesize is the buffer size, in bytes, for the 1 plane.
+ */
+enum AVSampleFormat {
+ AV_SAMPLE_FMT_NONE = -1,
+ AV_SAMPLE_FMT_U8, ///< unsigned 8 bits
+ AV_SAMPLE_FMT_S16, ///< signed 16 bits
+ AV_SAMPLE_FMT_S32, ///< signed 32 bits
+ AV_SAMPLE_FMT_FLT, ///< float
+ AV_SAMPLE_FMT_DBL, ///< double
+
+ AV_SAMPLE_FMT_U8P, ///< unsigned 8 bits, planar
+ AV_SAMPLE_FMT_S16P, ///< signed 16 bits, planar
+ AV_SAMPLE_FMT_S32P, ///< signed 32 bits, planar
+ AV_SAMPLE_FMT_FLTP, ///< float, planar
+ AV_SAMPLE_FMT_DBLP, ///< double, planar
+
+ AV_SAMPLE_FMT_NB ///< Number of sample formats. DO NOT USE if linking dynamically
+};
+
+/**
+ * Return the name of sample_fmt, or NULL if sample_fmt is not
+ * recognized.
+ */
+const char *av_get_sample_fmt_name(enum AVSampleFormat sample_fmt);
+
+/**
+ * Return a sample format corresponding to name, or AV_SAMPLE_FMT_NONE
+ * on error.
+ */
+enum AVSampleFormat av_get_sample_fmt(const char *name);
+
+/**
+ * Get the packed alternative form of the given sample format.
+ *
+ * If the passed sample_fmt is already in packed format, the format returned is
+ * the same as the input.
+ *
+ * @return the packed alternative form of the given sample format or
+ AV_SAMPLE_FMT_NONE on error.
+ */
+enum AVSampleFormat av_get_packed_sample_fmt(enum AVSampleFormat sample_fmt);
+
+/**
+ * Get the planar alternative form of the given sample format.
+ *
+ * If the passed sample_fmt is already in planar format, the format returned is
+ * the same as the input.
+ *
+ * @return the planar alternative form of the given sample format or
+ AV_SAMPLE_FMT_NONE on error.
+ */
+enum AVSampleFormat av_get_planar_sample_fmt(enum AVSampleFormat sample_fmt);
+
+/**
+ * Generate a string corresponding to the sample format with
+ * sample_fmt, or a header if sample_fmt is negative.
+ *
+ * @param buf the buffer where to write the string
+ * @param buf_size the size of buf
+ * @param sample_fmt the number of the sample format to print the
+ * corresponding info string, or a negative value to print the
+ * corresponding header.
+ * @return the pointer to the filled buffer or NULL if sample_fmt is
+ * unknown or in case of other errors
+ */
+char *av_get_sample_fmt_string(char *buf, int buf_size, enum AVSampleFormat sample_fmt);
+
+/**
+ * Return number of bytes per sample.
+ *
+ * @param sample_fmt the sample format
+ * @return number of bytes per sample or zero if unknown for the given
+ * sample format
+ */
+int av_get_bytes_per_sample(enum AVSampleFormat sample_fmt);
+
+/**
+ * Check if the sample format is planar.
+ *
+ * @param sample_fmt the sample format to inspect
+ * @return 1 if the sample format is planar, 0 if it is interleaved
+ */
+int av_sample_fmt_is_planar(enum AVSampleFormat sample_fmt);
+
+/**
+ * Get the required buffer size for the given audio parameters.
+ *
+ * @param[out] linesize calculated linesize, may be NULL
+ * @param nb_channels the number of channels
+ * @param nb_samples the number of samples in a single channel
+ * @param sample_fmt the sample format
+ * @param align buffer size alignment (0 = default, 1 = no alignment)
+ * @return required buffer size, or negative error code on failure
+ */
+int av_samples_get_buffer_size(int *linesize, int nb_channels, int nb_samples,
+ enum AVSampleFormat sample_fmt, int align);
+
+/**
+ * Fill channel data pointers and linesize for samples with sample
+ * format sample_fmt.
+ *
+ * The pointers array is filled with the pointers to the samples data:
+ * for planar, set the start point of each channel's data within the buffer,
+ * for packed, set the start point of the entire buffer only.
+ *
+ * The linesize array is filled with the aligned size of each channel's data
+ * buffer for planar layout, or the aligned size of the buffer for all channels
+ * for packed layout.
+ *
+ * @see enum AVSampleFormat
+ * The documentation for AVSampleFormat describes the data layout.
+ *
+ * @param[out] audio_data array to be filled with the pointer for each channel
+ * @param[out] linesize calculated linesize, may be NULL
+ * @param buf the pointer to a buffer containing the samples
+ * @param nb_channels the number of channels
+ * @param nb_samples the number of samples in a single channel
+ * @param sample_fmt the sample format
+ * @param align buffer size alignment (0 = default, 1 = no alignment)
+ * @return 0 on success or a negative error code on failure
+ */
+int av_samples_fill_arrays(uint8_t **audio_data, int *linesize,
+ const uint8_t *buf,
+ int nb_channels, int nb_samples,
+ enum AVSampleFormat sample_fmt, int align);
+
+/**
+ * Allocate a samples buffer for nb_samples samples, and fill data pointers and
+ * linesize accordingly.
+ * The allocated samples buffer can be freed by using av_freep(&audio_data[0])
+ * Allocated data will be initialized to silence.
+ *
+ * @see enum AVSampleFormat
+ * The documentation for AVSampleFormat describes the data layout.
+ *
+ * @param[out] audio_data array to be filled with the pointer for each channel
+ * @param[out] linesize aligned size for audio buffer(s), may be NULL
+ * @param nb_channels number of audio channels
+ * @param nb_samples number of samples per channel
+ * @param align buffer size alignment (0 = default, 1 = no alignment)
+ * @return 0 on success or a negative error code on failure
+ * @see av_samples_fill_arrays()
+ */
+int av_samples_alloc(uint8_t **audio_data, int *linesize, int nb_channels,
+ int nb_samples, enum AVSampleFormat sample_fmt, int align);
+
+/**
+ * Copy samples from src to dst.
+ *
+ * @param dst destination array of pointers to data planes
+ * @param src source array of pointers to data planes
+ * @param dst_offset offset in samples at which the data will be written to dst
+ * @param src_offset offset in samples at which the data will be read from src
+ * @param nb_samples number of samples to be copied
+ * @param nb_channels number of audio channels
+ * @param sample_fmt audio sample format
+ */
+int av_samples_copy(uint8_t **dst, uint8_t * const *src, int dst_offset,
+ int src_offset, int nb_samples, int nb_channels,
+ enum AVSampleFormat sample_fmt);
+
+/**
+ * Fill an audio buffer with silence.
+ *
+ * @param audio_data array of pointers to data planes
+ * @param offset offset in samples at which to start filling
+ * @param nb_samples number of samples to fill
+ * @param nb_channels number of audio channels
+ * @param sample_fmt audio sample format
+ */
+int av_samples_set_silence(uint8_t **audio_data, int offset, int nb_samples,
+ int nb_channels, enum AVSampleFormat sample_fmt);
+
+#endif /* AVUTIL_SAMPLEFMT_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/sha.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/sha.h
new file mode 100644
index 0000000000..4c9a0c9095
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/sha.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2007 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_SHA_H
+#define AVUTIL_SHA_H
+
+#include <stdint.h>
+
+#include "attributes.h"
+#include "version.h"
+
+/**
+ * @defgroup lavu_sha SHA
+ * @ingroup lavu_crypto
+ * @{
+ */
+
+#if FF_API_CONTEXT_SIZE
+extern attribute_deprecated const int av_sha_size;
+#endif
+
+struct AVSHA;
+
+/**
+ * Allocate an AVSHA context.
+ */
+struct AVSHA *av_sha_alloc(void);
+
+/**
+ * Initialize SHA-1 or SHA-2 hashing.
+ *
+ * @param context pointer to the function context (of size av_sha_size)
+ * @param bits number of bits in digest (SHA-1 - 160 bits, SHA-2 224 or 256 bits)
+ * @return zero if initialization succeeded, -1 otherwise
+ */
+int av_sha_init(struct AVSHA* context, int bits);
+
+/**
+ * Update hash value.
+ *
+ * @param context hash function context
+ * @param data input data to update hash with
+ * @param len input data length
+ */
+void av_sha_update(struct AVSHA* context, const uint8_t* data, unsigned int len);
+
+/**
+ * Finish hashing and output digest value.
+ *
+ * @param context hash function context
+ * @param digest buffer where output digest value is stored
+ */
+void av_sha_final(struct AVSHA* context, uint8_t *digest);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_SHA_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/time.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/time.h
new file mode 100644
index 0000000000..b01a97d770
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/time.h
@@ -0,0 +1,39 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_TIME_H
+#define AVUTIL_TIME_H
+
+#include <stdint.h>
+
+/**
+ * Get the current time in microseconds.
+ */
+int64_t av_gettime(void);
+
+/**
+ * Sleep for a period of time. Although the duration is expressed in
+ * microseconds, the actual delay may be rounded to the precision of the
+ * system timer.
+ *
+ * @param usec Number of microseconds to sleep.
+ * @return zero on success or (negative) error code.
+ */
+int av_usleep(unsigned usec);
+
+#endif /* AVUTIL_TIME_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/version.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/version.h
new file mode 100644
index 0000000000..1dbb11ca21
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/version.h
@@ -0,0 +1,87 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_VERSION_H
+#define AVUTIL_VERSION_H
+
+#include "avutil.h"
+
+/**
+ * @file
+ * @ingroup lavu
+ * Libavutil version macros
+ */
+
+/**
+ * @defgroup lavu_ver Version and Build diagnostics
+ *
+ * Macros and function useful to check at compiletime and at runtime
+ * which version of libavutil is in use.
+ *
+ * @{
+ */
+
+#define LIBAVUTIL_VERSION_MAJOR 52
+#define LIBAVUTIL_VERSION_MINOR 3
+#define LIBAVUTIL_VERSION_MICRO 0
+
+#define LIBAVUTIL_VERSION_INT AV_VERSION_INT(LIBAVUTIL_VERSION_MAJOR, \
+ LIBAVUTIL_VERSION_MINOR, \
+ LIBAVUTIL_VERSION_MICRO)
+#define LIBAVUTIL_VERSION AV_VERSION(LIBAVUTIL_VERSION_MAJOR, \
+ LIBAVUTIL_VERSION_MINOR, \
+ LIBAVUTIL_VERSION_MICRO)
+#define LIBAVUTIL_BUILD LIBAVUTIL_VERSION_INT
+
+#define LIBAVUTIL_IDENT "Lavu" AV_STRINGIFY(LIBAVUTIL_VERSION)
+
+/**
+ * @}
+ *
+ * @defgroup depr_guards Deprecation guards
+ * FF_API_* defines may be placed below to indicate public API that will be
+ * dropped at a future version bump. The defines themselves are not part of
+ * the public API and may change, break or disappear at any time.
+ *
+ * @{
+ */
+
+#ifndef FF_API_PIX_FMT
+#define FF_API_PIX_FMT (LIBAVUTIL_VERSION_MAJOR < 53)
+#endif
+#ifndef FF_API_CONTEXT_SIZE
+#define FF_API_CONTEXT_SIZE (LIBAVUTIL_VERSION_MAJOR < 53)
+#endif
+#ifndef FF_API_PIX_FMT_DESC
+#define FF_API_PIX_FMT_DESC (LIBAVUTIL_VERSION_MAJOR < 53)
+#endif
+#ifndef FF_API_AV_REVERSE
+#define FF_API_AV_REVERSE (LIBAVUTIL_VERSION_MAJOR < 53)
+#endif
+#ifndef FF_API_AUDIOCONVERT
+#define FF_API_AUDIOCONVERT (LIBAVUTIL_VERSION_MAJOR < 53)
+#endif
+#ifndef FF_API_CPU_FLAG_MMX2
+#define FF_API_CPU_FLAG_MMX2 (LIBAVUTIL_VERSION_MAJOR < 53)
+#endif
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_VERSION_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/include/libavutil/xtea.h b/dom/media/platforms/ffmpeg/libav54/include/libavutil/xtea.h
new file mode 100644
index 0000000000..7d2b07bbc7
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/include/libavutil/xtea.h
@@ -0,0 +1,61 @@
+/*
+ * A 32-bit implementation of the XTEA algorithm
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_XTEA_H
+#define AVUTIL_XTEA_H
+
+#include <stdint.h>
+
+/**
+ * @defgroup lavu_xtea XTEA
+ * @ingroup lavu_crypto
+ * @{
+ */
+
+typedef struct AVXTEA {
+ uint32_t key[16];
+} AVXTEA;
+
+/**
+ * Initialize an AVXTEA context.
+ *
+ * @param ctx an AVXTEA context
+ * @param key a key of 16 bytes used for encryption/decryption
+ */
+void av_xtea_init(struct AVXTEA *ctx, const uint8_t key[16]);
+
+/**
+ * Encrypt or decrypt a buffer using a previously initialized context.
+ *
+ * @param ctx an AVXTEA context
+ * @param dst destination array, can be equal to src
+ * @param src source array, can be equal to dst
+ * @param count number of 8 byte blocks
+ * @param iv initialization vector for CBC mode, if NULL then ECB will be used
+ * @param decrypt 0 for encryption, 1 for decryption
+ */
+void av_xtea_crypt(struct AVXTEA *ctx, uint8_t *dst, const uint8_t *src,
+ int count, uint8_t *iv, int decrypt);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_XTEA_H */
diff --git a/dom/media/platforms/ffmpeg/libav54/moz.build b/dom/media/platforms/ffmpeg/libav54/moz.build
new file mode 100644
index 0000000000..d956f077f3
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav54/moz.build
@@ -0,0 +1,23 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES += [
+ '../FFmpegAudioDecoder.cpp',
+ '../FFmpegDataDecoder.cpp',
+ '../FFmpegDecoderModule.cpp',
+ '../FFmpegVideoDecoder.cpp',
+]
+LOCAL_INCLUDES += [
+ '..',
+ 'include',
+]
+
+FINAL_LIBRARY = 'xul'
+
+if CONFIG['CC_TYPE'] == 'clang':
+ CXXFLAGS += [
+ '-Wno-unknown-attributes',
+ ]
diff --git a/dom/media/platforms/ffmpeg/libav55/include/COPYING.LGPLv2.1 b/dom/media/platforms/ffmpeg/libav55/include/COPYING.LGPLv2.1
new file mode 100644
index 0000000000..00b4fedfe7
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/COPYING.LGPLv2.1
@@ -0,0 +1,504 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the library's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+ <signature of Ty Coon>, 1 April 1990
+ Ty Coon, President of Vice
+
+That's all there is to it!
+
+
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavcodec/avcodec.h b/dom/media/platforms/ffmpeg/libav55/include/libavcodec/avcodec.h
new file mode 100644
index 0000000000..244f47ba10
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavcodec/avcodec.h
@@ -0,0 +1,4356 @@
+/*
+ * copyright (c) 2001 Fabrice Bellard
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_AVCODEC_H
+#define AVCODEC_AVCODEC_H
+
+/**
+ * @file
+ * @ingroup libavc
+ * Libavcodec external API header
+ */
+
+#include <errno.h>
+#include "libavutil/samplefmt.h"
+#include "libavutil/attributes.h"
+#include "libavutil/avutil.h"
+#include "libavutil/buffer.h"
+#include "libavutil/cpu.h"
+#include "libavutil/dict.h"
+#include "libavutil/frame.h"
+#include "libavutil/log.h"
+#include "libavutil/pixfmt.h"
+#include "libavutil/rational.h"
+
+#include "version.h"
+
+#if FF_API_FAST_MALLOC
+// to provide fast_*alloc
+#include "libavutil/mem.h"
+#endif
+
+/**
+ * @defgroup libavc Encoding/Decoding Library
+ * @{
+ *
+ * @defgroup lavc_decoding Decoding
+ * @{
+ * @}
+ *
+ * @defgroup lavc_encoding Encoding
+ * @{
+ * @}
+ *
+ * @defgroup lavc_codec Codecs
+ * @{
+ * @defgroup lavc_codec_native Native Codecs
+ * @{
+ * @}
+ * @defgroup lavc_codec_wrappers External library wrappers
+ * @{
+ * @}
+ * @defgroup lavc_codec_hwaccel Hardware Accelerators bridge
+ * @{
+ * @}
+ * @}
+ * @defgroup lavc_internal Internal
+ * @{
+ * @}
+ * @}
+ *
+ */
+
+/**
+ * @defgroup lavc_core Core functions/structures.
+ * @ingroup libavc
+ *
+ * Basic definitions, functions for querying libavcodec capabilities,
+ * allocating core structures, etc.
+ * @{
+ */
+
+
+/**
+ * Identify the syntax and semantics of the bitstream.
+ * The principle is roughly:
+ * Two decoders with the same ID can decode the same streams.
+ * Two encoders with the same ID can encode compatible streams.
+ * There may be slight deviations from the principle due to implementation
+ * details.
+ *
+ * If you add a codec ID to this list, add it so that
+ * 1. no value of a existing codec ID changes (that would break ABI),
+ * 2. it is as close as possible to similar codecs.
+ *
+ * After adding new codec IDs, do not forget to add an entry to the codec
+ * descriptor list and bump libavcodec minor version.
+ */
+enum AVCodecID {
+ AV_CODEC_ID_NONE,
+
+ /* video codecs */
+ AV_CODEC_ID_MPEG1VIDEO,
+ AV_CODEC_ID_MPEG2VIDEO, ///< preferred ID for MPEG-1/2 video decoding
+#if FF_API_XVMC
+ AV_CODEC_ID_MPEG2VIDEO_XVMC,
+#endif /* FF_API_XVMC */
+ AV_CODEC_ID_H261,
+ AV_CODEC_ID_H263,
+ AV_CODEC_ID_RV10,
+ AV_CODEC_ID_RV20,
+ AV_CODEC_ID_MJPEG,
+ AV_CODEC_ID_MJPEGB,
+ AV_CODEC_ID_LJPEG,
+ AV_CODEC_ID_SP5X,
+ AV_CODEC_ID_JPEGLS,
+ AV_CODEC_ID_MPEG4,
+ AV_CODEC_ID_RAWVIDEO,
+ AV_CODEC_ID_MSMPEG4V1,
+ AV_CODEC_ID_MSMPEG4V2,
+ AV_CODEC_ID_MSMPEG4V3,
+ AV_CODEC_ID_WMV1,
+ AV_CODEC_ID_WMV2,
+ AV_CODEC_ID_H263P,
+ AV_CODEC_ID_H263I,
+ AV_CODEC_ID_FLV1,
+ AV_CODEC_ID_SVQ1,
+ AV_CODEC_ID_SVQ3,
+ AV_CODEC_ID_DVVIDEO,
+ AV_CODEC_ID_HUFFYUV,
+ AV_CODEC_ID_CYUV,
+ AV_CODEC_ID_H264,
+ AV_CODEC_ID_INDEO3,
+ AV_CODEC_ID_VP3,
+ AV_CODEC_ID_THEORA,
+ AV_CODEC_ID_ASV1,
+ AV_CODEC_ID_ASV2,
+ AV_CODEC_ID_FFV1,
+ AV_CODEC_ID_4XM,
+ AV_CODEC_ID_VCR1,
+ AV_CODEC_ID_CLJR,
+ AV_CODEC_ID_MDEC,
+ AV_CODEC_ID_ROQ,
+ AV_CODEC_ID_INTERPLAY_VIDEO,
+ AV_CODEC_ID_XAN_WC3,
+ AV_CODEC_ID_XAN_WC4,
+ AV_CODEC_ID_RPZA,
+ AV_CODEC_ID_CINEPAK,
+ AV_CODEC_ID_WS_VQA,
+ AV_CODEC_ID_MSRLE,
+ AV_CODEC_ID_MSVIDEO1,
+ AV_CODEC_ID_IDCIN,
+ AV_CODEC_ID_8BPS,
+ AV_CODEC_ID_SMC,
+ AV_CODEC_ID_FLIC,
+ AV_CODEC_ID_TRUEMOTION1,
+ AV_CODEC_ID_VMDVIDEO,
+ AV_CODEC_ID_MSZH,
+ AV_CODEC_ID_ZLIB,
+ AV_CODEC_ID_QTRLE,
+ AV_CODEC_ID_TSCC,
+ AV_CODEC_ID_ULTI,
+ AV_CODEC_ID_QDRAW,
+ AV_CODEC_ID_VIXL,
+ AV_CODEC_ID_QPEG,
+ AV_CODEC_ID_PNG,
+ AV_CODEC_ID_PPM,
+ AV_CODEC_ID_PBM,
+ AV_CODEC_ID_PGM,
+ AV_CODEC_ID_PGMYUV,
+ AV_CODEC_ID_PAM,
+ AV_CODEC_ID_FFVHUFF,
+ AV_CODEC_ID_RV30,
+ AV_CODEC_ID_RV40,
+ AV_CODEC_ID_VC1,
+ AV_CODEC_ID_WMV3,
+ AV_CODEC_ID_LOCO,
+ AV_CODEC_ID_WNV1,
+ AV_CODEC_ID_AASC,
+ AV_CODEC_ID_INDEO2,
+ AV_CODEC_ID_FRAPS,
+ AV_CODEC_ID_TRUEMOTION2,
+ AV_CODEC_ID_BMP,
+ AV_CODEC_ID_CSCD,
+ AV_CODEC_ID_MMVIDEO,
+ AV_CODEC_ID_ZMBV,
+ AV_CODEC_ID_AVS,
+ AV_CODEC_ID_SMACKVIDEO,
+ AV_CODEC_ID_NUV,
+ AV_CODEC_ID_KMVC,
+ AV_CODEC_ID_FLASHSV,
+ AV_CODEC_ID_CAVS,
+ AV_CODEC_ID_JPEG2000,
+ AV_CODEC_ID_VMNC,
+ AV_CODEC_ID_VP5,
+ AV_CODEC_ID_VP6,
+ AV_CODEC_ID_VP6F,
+ AV_CODEC_ID_TARGA,
+ AV_CODEC_ID_DSICINVIDEO,
+ AV_CODEC_ID_TIERTEXSEQVIDEO,
+ AV_CODEC_ID_TIFF,
+ AV_CODEC_ID_GIF,
+ AV_CODEC_ID_DXA,
+ AV_CODEC_ID_DNXHD,
+ AV_CODEC_ID_THP,
+ AV_CODEC_ID_SGI,
+ AV_CODEC_ID_C93,
+ AV_CODEC_ID_BETHSOFTVID,
+ AV_CODEC_ID_PTX,
+ AV_CODEC_ID_TXD,
+ AV_CODEC_ID_VP6A,
+ AV_CODEC_ID_AMV,
+ AV_CODEC_ID_VB,
+ AV_CODEC_ID_PCX,
+ AV_CODEC_ID_SUNRAST,
+ AV_CODEC_ID_INDEO4,
+ AV_CODEC_ID_INDEO5,
+ AV_CODEC_ID_MIMIC,
+ AV_CODEC_ID_RL2,
+ AV_CODEC_ID_ESCAPE124,
+ AV_CODEC_ID_DIRAC,
+ AV_CODEC_ID_BFI,
+ AV_CODEC_ID_CMV,
+ AV_CODEC_ID_MOTIONPIXELS,
+ AV_CODEC_ID_TGV,
+ AV_CODEC_ID_TGQ,
+ AV_CODEC_ID_TQI,
+ AV_CODEC_ID_AURA,
+ AV_CODEC_ID_AURA2,
+ AV_CODEC_ID_V210X,
+ AV_CODEC_ID_TMV,
+ AV_CODEC_ID_V210,
+ AV_CODEC_ID_DPX,
+ AV_CODEC_ID_MAD,
+ AV_CODEC_ID_FRWU,
+ AV_CODEC_ID_FLASHSV2,
+ AV_CODEC_ID_CDGRAPHICS,
+ AV_CODEC_ID_R210,
+ AV_CODEC_ID_ANM,
+ AV_CODEC_ID_BINKVIDEO,
+ AV_CODEC_ID_IFF_ILBM,
+ AV_CODEC_ID_IFF_BYTERUN1,
+ AV_CODEC_ID_KGV1,
+ AV_CODEC_ID_YOP,
+ AV_CODEC_ID_VP8,
+ AV_CODEC_ID_PICTOR,
+ AV_CODEC_ID_ANSI,
+ AV_CODEC_ID_A64_MULTI,
+ AV_CODEC_ID_A64_MULTI5,
+ AV_CODEC_ID_R10K,
+ AV_CODEC_ID_MXPEG,
+ AV_CODEC_ID_LAGARITH,
+ AV_CODEC_ID_PRORES,
+ AV_CODEC_ID_JV,
+ AV_CODEC_ID_DFA,
+ AV_CODEC_ID_WMV3IMAGE,
+ AV_CODEC_ID_VC1IMAGE,
+ AV_CODEC_ID_UTVIDEO,
+ AV_CODEC_ID_BMV_VIDEO,
+ AV_CODEC_ID_VBLE,
+ AV_CODEC_ID_DXTORY,
+ AV_CODEC_ID_V410,
+ AV_CODEC_ID_XWD,
+ AV_CODEC_ID_CDXL,
+ AV_CODEC_ID_XBM,
+ AV_CODEC_ID_ZEROCODEC,
+ AV_CODEC_ID_MSS1,
+ AV_CODEC_ID_MSA1,
+ AV_CODEC_ID_TSCC2,
+ AV_CODEC_ID_MTS2,
+ AV_CODEC_ID_CLLC,
+ AV_CODEC_ID_MSS2,
+ AV_CODEC_ID_VP9,
+ AV_CODEC_ID_AIC,
+ AV_CODEC_ID_ESCAPE130,
+ AV_CODEC_ID_G2M,
+ AV_CODEC_ID_WEBP,
+ AV_CODEC_ID_HNM4_VIDEO,
+ AV_CODEC_ID_HEVC,
+ AV_CODEC_ID_FIC,
+
+ /* various PCM "codecs" */
+ AV_CODEC_ID_FIRST_AUDIO = 0x10000, ///< A dummy id pointing at the start of audio codecs
+ AV_CODEC_ID_PCM_S16LE = 0x10000,
+ AV_CODEC_ID_PCM_S16BE,
+ AV_CODEC_ID_PCM_U16LE,
+ AV_CODEC_ID_PCM_U16BE,
+ AV_CODEC_ID_PCM_S8,
+ AV_CODEC_ID_PCM_U8,
+ AV_CODEC_ID_PCM_MULAW,
+ AV_CODEC_ID_PCM_ALAW,
+ AV_CODEC_ID_PCM_S32LE,
+ AV_CODEC_ID_PCM_S32BE,
+ AV_CODEC_ID_PCM_U32LE,
+ AV_CODEC_ID_PCM_U32BE,
+ AV_CODEC_ID_PCM_S24LE,
+ AV_CODEC_ID_PCM_S24BE,
+ AV_CODEC_ID_PCM_U24LE,
+ AV_CODEC_ID_PCM_U24BE,
+ AV_CODEC_ID_PCM_S24DAUD,
+ AV_CODEC_ID_PCM_ZORK,
+ AV_CODEC_ID_PCM_S16LE_PLANAR,
+ AV_CODEC_ID_PCM_DVD,
+ AV_CODEC_ID_PCM_F32BE,
+ AV_CODEC_ID_PCM_F32LE,
+ AV_CODEC_ID_PCM_F64BE,
+ AV_CODEC_ID_PCM_F64LE,
+ AV_CODEC_ID_PCM_BLURAY,
+ AV_CODEC_ID_PCM_LXF,
+ AV_CODEC_ID_S302M,
+ AV_CODEC_ID_PCM_S8_PLANAR,
+ AV_CODEC_ID_PCM_S24LE_PLANAR,
+ AV_CODEC_ID_PCM_S32LE_PLANAR,
+
+ /* various ADPCM codecs */
+ AV_CODEC_ID_ADPCM_IMA_QT = 0x11000,
+ AV_CODEC_ID_ADPCM_IMA_WAV,
+ AV_CODEC_ID_ADPCM_IMA_DK3,
+ AV_CODEC_ID_ADPCM_IMA_DK4,
+ AV_CODEC_ID_ADPCM_IMA_WS,
+ AV_CODEC_ID_ADPCM_IMA_SMJPEG,
+ AV_CODEC_ID_ADPCM_MS,
+ AV_CODEC_ID_ADPCM_4XM,
+ AV_CODEC_ID_ADPCM_XA,
+ AV_CODEC_ID_ADPCM_ADX,
+ AV_CODEC_ID_ADPCM_EA,
+ AV_CODEC_ID_ADPCM_G726,
+ AV_CODEC_ID_ADPCM_CT,
+ AV_CODEC_ID_ADPCM_SWF,
+ AV_CODEC_ID_ADPCM_YAMAHA,
+ AV_CODEC_ID_ADPCM_SBPRO_4,
+ AV_CODEC_ID_ADPCM_SBPRO_3,
+ AV_CODEC_ID_ADPCM_SBPRO_2,
+ AV_CODEC_ID_ADPCM_THP,
+ AV_CODEC_ID_ADPCM_IMA_AMV,
+ AV_CODEC_ID_ADPCM_EA_R1,
+ AV_CODEC_ID_ADPCM_EA_R3,
+ AV_CODEC_ID_ADPCM_EA_R2,
+ AV_CODEC_ID_ADPCM_IMA_EA_SEAD,
+ AV_CODEC_ID_ADPCM_IMA_EA_EACS,
+ AV_CODEC_ID_ADPCM_EA_XAS,
+ AV_CODEC_ID_ADPCM_EA_MAXIS_XA,
+ AV_CODEC_ID_ADPCM_IMA_ISS,
+ AV_CODEC_ID_ADPCM_G722,
+ AV_CODEC_ID_ADPCM_IMA_APC,
+
+ /* AMR */
+ AV_CODEC_ID_AMR_NB = 0x12000,
+ AV_CODEC_ID_AMR_WB,
+
+ /* RealAudio codecs*/
+ AV_CODEC_ID_RA_144 = 0x13000,
+ AV_CODEC_ID_RA_288,
+
+ /* various DPCM codecs */
+ AV_CODEC_ID_ROQ_DPCM = 0x14000,
+ AV_CODEC_ID_INTERPLAY_DPCM,
+ AV_CODEC_ID_XAN_DPCM,
+ AV_CODEC_ID_SOL_DPCM,
+
+ /* audio codecs */
+ AV_CODEC_ID_MP2 = 0x15000,
+ AV_CODEC_ID_MP3, ///< preferred ID for decoding MPEG audio layer 1, 2 or 3
+ AV_CODEC_ID_AAC,
+ AV_CODEC_ID_AC3,
+ AV_CODEC_ID_DTS,
+ AV_CODEC_ID_VORBIS,
+ AV_CODEC_ID_DVAUDIO,
+ AV_CODEC_ID_WMAV1,
+ AV_CODEC_ID_WMAV2,
+ AV_CODEC_ID_MACE3,
+ AV_CODEC_ID_MACE6,
+ AV_CODEC_ID_VMDAUDIO,
+ AV_CODEC_ID_FLAC,
+ AV_CODEC_ID_MP3ADU,
+ AV_CODEC_ID_MP3ON4,
+ AV_CODEC_ID_SHORTEN,
+ AV_CODEC_ID_ALAC,
+ AV_CODEC_ID_WESTWOOD_SND1,
+ AV_CODEC_ID_GSM, ///< as in Berlin toast format
+ AV_CODEC_ID_QDM2,
+ AV_CODEC_ID_COOK,
+ AV_CODEC_ID_TRUESPEECH,
+ AV_CODEC_ID_TTA,
+ AV_CODEC_ID_SMACKAUDIO,
+ AV_CODEC_ID_QCELP,
+ AV_CODEC_ID_WAVPACK,
+ AV_CODEC_ID_DSICINAUDIO,
+ AV_CODEC_ID_IMC,
+ AV_CODEC_ID_MUSEPACK7,
+ AV_CODEC_ID_MLP,
+ AV_CODEC_ID_GSM_MS, /* as found in WAV */
+ AV_CODEC_ID_ATRAC3,
+#if FF_API_VOXWARE
+ AV_CODEC_ID_VOXWARE,
+#endif
+ AV_CODEC_ID_APE,
+ AV_CODEC_ID_NELLYMOSER,
+ AV_CODEC_ID_MUSEPACK8,
+ AV_CODEC_ID_SPEEX,
+ AV_CODEC_ID_WMAVOICE,
+ AV_CODEC_ID_WMAPRO,
+ AV_CODEC_ID_WMALOSSLESS,
+ AV_CODEC_ID_ATRAC3P,
+ AV_CODEC_ID_EAC3,
+ AV_CODEC_ID_SIPR,
+ AV_CODEC_ID_MP1,
+ AV_CODEC_ID_TWINVQ,
+ AV_CODEC_ID_TRUEHD,
+ AV_CODEC_ID_MP4ALS,
+ AV_CODEC_ID_ATRAC1,
+ AV_CODEC_ID_BINKAUDIO_RDFT,
+ AV_CODEC_ID_BINKAUDIO_DCT,
+ AV_CODEC_ID_AAC_LATM,
+ AV_CODEC_ID_QDMC,
+ AV_CODEC_ID_CELT,
+ AV_CODEC_ID_G723_1,
+ AV_CODEC_ID_G729,
+ AV_CODEC_ID_8SVX_EXP,
+ AV_CODEC_ID_8SVX_FIB,
+ AV_CODEC_ID_BMV_AUDIO,
+ AV_CODEC_ID_RALF,
+ AV_CODEC_ID_IAC,
+ AV_CODEC_ID_ILBC,
+ AV_CODEC_ID_OPUS,
+ AV_CODEC_ID_COMFORT_NOISE,
+ AV_CODEC_ID_TAK,
+ AV_CODEC_ID_METASOUND,
+
+ /* subtitle codecs */
+ AV_CODEC_ID_FIRST_SUBTITLE = 0x17000, ///< A dummy ID pointing at the start of subtitle codecs.
+ AV_CODEC_ID_DVD_SUBTITLE = 0x17000,
+ AV_CODEC_ID_DVB_SUBTITLE,
+ AV_CODEC_ID_TEXT, ///< raw UTF-8 text
+ AV_CODEC_ID_XSUB,
+ AV_CODEC_ID_SSA,
+ AV_CODEC_ID_MOV_TEXT,
+ AV_CODEC_ID_HDMV_PGS_SUBTITLE,
+ AV_CODEC_ID_DVB_TELETEXT,
+ AV_CODEC_ID_SRT,
+
+ /* other specific kind of codecs (generally used for attachments) */
+ AV_CODEC_ID_FIRST_UNKNOWN = 0x18000, ///< A dummy ID pointing at the start of various fake codecs.
+ AV_CODEC_ID_TTF = 0x18000,
+
+ AV_CODEC_ID_PROBE = 0x19000, ///< codec_id is not known (like AV_CODEC_ID_NONE) but lavf should attempt to identify it
+
+ AV_CODEC_ID_MPEG2TS = 0x20000, /**< _FAKE_ codec to indicate a raw MPEG-2 TS
+ * stream (only used by libavformat) */
+ AV_CODEC_ID_MPEG4SYSTEMS = 0x20001, /**< _FAKE_ codec to indicate a MPEG-4 Systems
+ * stream (only used by libavformat) */
+ AV_CODEC_ID_FFMETADATA = 0x21000, ///< Dummy codec for streams containing only metadata information.
+};
+
+/**
+ * This struct describes the properties of a single codec described by an
+ * AVCodecID.
+ * @see avcodec_get_descriptor()
+ */
+typedef struct AVCodecDescriptor {
+ enum AVCodecID id;
+ enum AVMediaType type;
+ /**
+ * Name of the codec described by this descriptor. It is non-empty and
+ * unique for each codec descriptor. It should contain alphanumeric
+ * characters and '_' only.
+ */
+ const char *name;
+ /**
+ * A more descriptive name for this codec. May be NULL.
+ */
+ const char *long_name;
+ /**
+ * Codec properties, a combination of AV_CODEC_PROP_* flags.
+ */
+ int props;
+} AVCodecDescriptor;
+
+/**
+ * Codec uses only intra compression.
+ * Video codecs only.
+ */
+#define AV_CODEC_PROP_INTRA_ONLY (1 << 0)
+/**
+ * Codec supports lossy compression. Audio and video codecs only.
+ * @note a codec may support both lossy and lossless
+ * compression modes
+ */
+#define AV_CODEC_PROP_LOSSY (1 << 1)
+/**
+ * Codec supports lossless compression. Audio and video codecs only.
+ */
+#define AV_CODEC_PROP_LOSSLESS (1 << 2)
+
+/**
+ * @ingroup lavc_decoding
+ * Required number of additionally allocated bytes at the end of the input bitstream for decoding.
+ * This is mainly needed because some optimized bitstream readers read
+ * 32 or 64 bit at once and could read over the end.<br>
+ * Note: If the first 23 bits of the additional bytes are not 0, then damaged
+ * MPEG bitstreams could cause overread and segfault.
+ */
+#define FF_INPUT_BUFFER_PADDING_SIZE 8
+
+/**
+ * @ingroup lavc_encoding
+ * minimum encoding buffer size
+ * Used to avoid some checks during header writing.
+ */
+#define FF_MIN_BUFFER_SIZE 16384
+
+
+/**
+ * @ingroup lavc_encoding
+ * motion estimation type.
+ */
+enum Motion_Est_ID {
+ ME_ZERO = 1, ///< no search, that is use 0,0 vector whenever one is needed
+ ME_FULL,
+ ME_LOG,
+ ME_PHODS,
+ ME_EPZS, ///< enhanced predictive zonal search
+ ME_X1, ///< reserved for experiments
+ ME_HEX, ///< hexagon based search
+ ME_UMH, ///< uneven multi-hexagon search
+ ME_TESA, ///< transformed exhaustive search algorithm
+};
+
+/**
+ * @ingroup lavc_decoding
+ */
+enum AVDiscard{
+ /* We leave some space between them for extensions (drop some
+ * keyframes for intra-only or drop just some bidir frames). */
+ AVDISCARD_NONE =-16, ///< discard nothing
+ AVDISCARD_DEFAULT = 0, ///< discard useless packets like 0 size packets in avi
+ AVDISCARD_NONREF = 8, ///< discard all non reference
+ AVDISCARD_BIDIR = 16, ///< discard all bidirectional frames
+ AVDISCARD_NONKEY = 32, ///< discard all frames except keyframes
+ AVDISCARD_ALL = 48, ///< discard all
+};
+
+enum AVColorPrimaries{
+ AVCOL_PRI_BT709 = 1, ///< also ITU-R BT1361 / IEC 61966-2-4 / SMPTE RP177 Annex B
+ AVCOL_PRI_UNSPECIFIED = 2,
+ AVCOL_PRI_BT470M = 4,
+ AVCOL_PRI_BT470BG = 5, ///< also ITU-R BT601-6 625 / ITU-R BT1358 625 / ITU-R BT1700 625 PAL & SECAM
+ AVCOL_PRI_SMPTE170M = 6, ///< also ITU-R BT601-6 525 / ITU-R BT1358 525 / ITU-R BT1700 NTSC
+ AVCOL_PRI_SMPTE240M = 7, ///< functionally identical to above
+ AVCOL_PRI_FILM = 8,
+ AVCOL_PRI_BT2020 = 9, ///< ITU-R BT2020
+ AVCOL_PRI_NB , ///< Not part of ABI
+};
+
+enum AVColorTransferCharacteristic{
+ AVCOL_TRC_BT709 = 1, ///< also ITU-R BT1361
+ AVCOL_TRC_UNSPECIFIED = 2,
+ AVCOL_TRC_GAMMA22 = 4, ///< also ITU-R BT470M / ITU-R BT1700 625 PAL & SECAM
+ AVCOL_TRC_GAMMA28 = 5, ///< also ITU-R BT470BG
+ AVCOL_TRC_SMPTE170M = 6, ///< also ITU-R BT601-6 525 or 625 / ITU-R BT1358 525 or 625 / ITU-R BT1700 NTSC
+ AVCOL_TRC_SMPTE240M = 7,
+ AVCOL_TRC_LINEAR = 8, ///< "Linear transfer characteristics"
+ AVCOL_TRC_LOG = 9, ///< "Logarithmic transfer characteristic (100:1 range)"
+ AVCOL_TRC_LOG_SQRT = 10, ///< "Logarithmic transfer characteristic (100 * Sqrt( 10 ) : 1 range)"
+ AVCOL_TRC_IEC61966_2_4 = 11, ///< IEC 61966-2-4
+ AVCOL_TRC_BT1361_ECG = 12, ///< ITU-R BT1361 Extended Colour Gamut
+ AVCOL_TRC_IEC61966_2_1 = 13, ///< IEC 61966-2-1 (sRGB or sYCC)
+ AVCOL_TRC_BT2020_10 = 14, ///< ITU-R BT2020 for 10 bit system
+ AVCOL_TRC_BT2020_12 = 15, ///< ITU-R BT2020 for 12 bit system
+ AVCOL_TRC_NB , ///< Not part of ABI
+};
+
+enum AVColorSpace{
+ AVCOL_SPC_RGB = 0,
+ AVCOL_SPC_BT709 = 1, ///< also ITU-R BT1361 / IEC 61966-2-4 xvYCC709 / SMPTE RP177 Annex B
+ AVCOL_SPC_UNSPECIFIED = 2,
+ AVCOL_SPC_FCC = 4,
+ AVCOL_SPC_BT470BG = 5, ///< also ITU-R BT601-6 625 / ITU-R BT1358 625 / ITU-R BT1700 625 PAL & SECAM / IEC 61966-2-4 xvYCC601
+ AVCOL_SPC_SMPTE170M = 6, ///< also ITU-R BT601-6 525 / ITU-R BT1358 525 / ITU-R BT1700 NTSC / functionally identical to above
+ AVCOL_SPC_SMPTE240M = 7,
+ AVCOL_SPC_YCOCG = 8, ///< Used by Dirac / VC-2 and H.264 FRext, see ITU-T SG16
+ AVCOL_SPC_BT2020_NCL = 9, ///< ITU-R BT2020 non-constant luminance system
+ AVCOL_SPC_BT2020_CL = 10, ///< ITU-R BT2020 constant luminance system
+ AVCOL_SPC_NB , ///< Not part of ABI
+};
+
+enum AVColorRange{
+ AVCOL_RANGE_UNSPECIFIED = 0,
+ AVCOL_RANGE_MPEG = 1, ///< the normal 219*2^(n-8) "MPEG" YUV ranges
+ AVCOL_RANGE_JPEG = 2, ///< the normal 2^n-1 "JPEG" YUV ranges
+ AVCOL_RANGE_NB , ///< Not part of ABI
+};
+
+/**
+ * X X 3 4 X X are luma samples,
+ * 1 2 1-6 are possible chroma positions
+ * X X 5 6 X 0 is undefined/unknown position
+ */
+enum AVChromaLocation{
+ AVCHROMA_LOC_UNSPECIFIED = 0,
+ AVCHROMA_LOC_LEFT = 1, ///< mpeg2/4, h264 default
+ AVCHROMA_LOC_CENTER = 2, ///< mpeg1, jpeg, h263
+ AVCHROMA_LOC_TOPLEFT = 3, ///< DV
+ AVCHROMA_LOC_TOP = 4,
+ AVCHROMA_LOC_BOTTOMLEFT = 5,
+ AVCHROMA_LOC_BOTTOM = 6,
+ AVCHROMA_LOC_NB , ///< Not part of ABI
+};
+
+enum AVAudioServiceType {
+ AV_AUDIO_SERVICE_TYPE_MAIN = 0,
+ AV_AUDIO_SERVICE_TYPE_EFFECTS = 1,
+ AV_AUDIO_SERVICE_TYPE_VISUALLY_IMPAIRED = 2,
+ AV_AUDIO_SERVICE_TYPE_HEARING_IMPAIRED = 3,
+ AV_AUDIO_SERVICE_TYPE_DIALOGUE = 4,
+ AV_AUDIO_SERVICE_TYPE_COMMENTARY = 5,
+ AV_AUDIO_SERVICE_TYPE_EMERGENCY = 6,
+ AV_AUDIO_SERVICE_TYPE_VOICE_OVER = 7,
+ AV_AUDIO_SERVICE_TYPE_KARAOKE = 8,
+ AV_AUDIO_SERVICE_TYPE_NB , ///< Not part of ABI
+};
+
+/**
+ * @ingroup lavc_encoding
+ */
+typedef struct RcOverride{
+ int start_frame;
+ int end_frame;
+ int qscale; // If this is 0 then quality_factor will be used instead.
+ float quality_factor;
+} RcOverride;
+
+#if FF_API_MAX_BFRAMES
+/**
+ * @deprecated there is no libavcodec-wide limit on the number of B-frames
+ */
+#define FF_MAX_B_FRAMES 16
+#endif
+
+/* encoding support
+ These flags can be passed in AVCodecContext.flags before initialization.
+ Note: Not everything is supported yet.
+*/
+
+/**
+ * Allow decoders to produce frames with data planes that are not aligned
+ * to CPU requirements (e.g. due to cropping).
+ */
+#define CODEC_FLAG_UNALIGNED 0x0001
+#define CODEC_FLAG_QSCALE 0x0002 ///< Use fixed qscale.
+#define CODEC_FLAG_4MV 0x0004 ///< 4 MV per MB allowed / advanced prediction for H.263.
+#define CODEC_FLAG_OUTPUT_CORRUPT 0x0008 ///< Output even those frames that might be corrupted
+#define CODEC_FLAG_QPEL 0x0010 ///< Use qpel MC.
+#define CODEC_FLAG_GMC 0x0020 ///< Use GMC.
+#define CODEC_FLAG_MV0 0x0040 ///< Always try a MB with MV=<0,0>.
+/**
+ * The parent program guarantees that the input for B-frames containing
+ * streams is not written to for at least s->max_b_frames+1 frames, if
+ * this is not set the input will be copied.
+ */
+#define CODEC_FLAG_INPUT_PRESERVED 0x0100
+#define CODEC_FLAG_PASS1 0x0200 ///< Use internal 2pass ratecontrol in first pass mode.
+#define CODEC_FLAG_PASS2 0x0400 ///< Use internal 2pass ratecontrol in second pass mode.
+#define CODEC_FLAG_GRAY 0x2000 ///< Only decode/encode grayscale.
+#if FF_API_EMU_EDGE
+/**
+ * @deprecated edges are not used/required anymore. I.e. this flag is now always
+ * set.
+ */
+#define CODEC_FLAG_EMU_EDGE 0x4000
+#endif
+#define CODEC_FLAG_PSNR 0x8000 ///< error[?] variables will be set during encoding.
+#define CODEC_FLAG_TRUNCATED 0x00010000 /** Input bitstream might be truncated at a random
+ location instead of only at frame boundaries. */
+#define CODEC_FLAG_NORMALIZE_AQP 0x00020000 ///< Normalize adaptive quantization.
+#define CODEC_FLAG_INTERLACED_DCT 0x00040000 ///< Use interlaced DCT.
+#define CODEC_FLAG_LOW_DELAY 0x00080000 ///< Force low delay.
+#define CODEC_FLAG_GLOBAL_HEADER 0x00400000 ///< Place global headers in extradata instead of every keyframe.
+#define CODEC_FLAG_BITEXACT 0x00800000 ///< Use only bitexact stuff (except (I)DCT).
+/* Fx : Flag for h263+ extra options */
+#define CODEC_FLAG_AC_PRED 0x01000000 ///< H.263 advanced intra coding / MPEG-4 AC prediction
+#define CODEC_FLAG_LOOP_FILTER 0x00000800 ///< loop filter
+#define CODEC_FLAG_INTERLACED_ME 0x20000000 ///< interlaced motion estimation
+#define CODEC_FLAG_CLOSED_GOP 0x80000000
+#define CODEC_FLAG2_FAST 0x00000001 ///< Allow non spec compliant speedup tricks.
+#define CODEC_FLAG2_NO_OUTPUT 0x00000004 ///< Skip bitstream encoding.
+#define CODEC_FLAG2_LOCAL_HEADER 0x00000008 ///< Place global headers at every keyframe instead of in extradata.
+#define CODEC_FLAG2_IGNORE_CROP 0x00010000 ///< Discard cropping information from SPS.
+
+#define CODEC_FLAG2_CHUNKS 0x00008000 ///< Input bitstream might be truncated at a packet boundaries instead of only at frame boundaries.
+
+/* Unsupported options :
+ * Syntax Arithmetic coding (SAC)
+ * Reference Picture Selection
+ * Independent Segment Decoding */
+/* /Fx */
+/* codec capabilities */
+
+#define CODEC_CAP_DRAW_HORIZ_BAND 0x0001 ///< Decoder can use draw_horiz_band callback.
+/**
+ * Codec uses get_buffer() for allocating buffers and supports custom allocators.
+ * If not set, it might not use get_buffer() at all or use operations that
+ * assume the buffer was allocated by avcodec_default_get_buffer.
+ */
+#define CODEC_CAP_DR1 0x0002
+#define CODEC_CAP_TRUNCATED 0x0008
+#if FF_API_XVMC
+/* Codec can export data for HW decoding (XvMC). */
+#define CODEC_CAP_HWACCEL 0x0010
+#endif /* FF_API_XVMC */
+/**
+ * Encoder or decoder requires flushing with NULL input at the end in order to
+ * give the complete and correct output.
+ *
+ * NOTE: If this flag is not set, the codec is guaranteed to never be fed with
+ * with NULL data. The user can still send NULL data to the public encode
+ * or decode function, but libavcodec will not pass it along to the codec
+ * unless this flag is set.
+ *
+ * Decoders:
+ * The decoder has a non-zero delay and needs to be fed with avpkt->data=NULL,
+ * avpkt->size=0 at the end to get the delayed data until the decoder no longer
+ * returns frames.
+ *
+ * Encoders:
+ * The encoder needs to be fed with NULL data at the end of encoding until the
+ * encoder no longer returns data.
+ *
+ * NOTE: For encoders implementing the AVCodec.encode2() function, setting this
+ * flag also means that the encoder must set the pts and duration for
+ * each output packet. If this flag is not set, the pts and duration will
+ * be determined by libavcodec from the input frame.
+ */
+#define CODEC_CAP_DELAY 0x0020
+/**
+ * Codec can be fed a final frame with a smaller size.
+ * This can be used to prevent truncation of the last audio samples.
+ */
+#define CODEC_CAP_SMALL_LAST_FRAME 0x0040
+#if FF_API_CAP_VDPAU
+/**
+ * Codec can export data for HW decoding (VDPAU).
+ */
+#define CODEC_CAP_HWACCEL_VDPAU 0x0080
+#endif
+/**
+ * Codec can output multiple frames per AVPacket
+ * Normally demuxers return one frame at a time, demuxers which do not do
+ * are connected to a parser to split what they return into proper frames.
+ * This flag is reserved to the very rare category of codecs which have a
+ * bitstream that cannot be split into frames without timeconsuming
+ * operations like full decoding. Demuxers carring such bitstreams thus
+ * may return multiple frames in a packet. This has many disadvantages like
+ * prohibiting stream copy in many cases thus it should only be considered
+ * as a last resort.
+ */
+#define CODEC_CAP_SUBFRAMES 0x0100
+/**
+ * Codec is experimental and is thus avoided in favor of non experimental
+ * encoders
+ */
+#define CODEC_CAP_EXPERIMENTAL 0x0200
+/**
+ * Codec should fill in channel configuration and samplerate instead of container
+ */
+#define CODEC_CAP_CHANNEL_CONF 0x0400
+#if FF_API_NEG_LINESIZES
+/**
+ * @deprecated no codecs use this capability
+ */
+#define CODEC_CAP_NEG_LINESIZES 0x0800
+#endif
+/**
+ * Codec supports frame-level multithreading.
+ */
+#define CODEC_CAP_FRAME_THREADS 0x1000
+/**
+ * Codec supports slice-based (or partition-based) multithreading.
+ */
+#define CODEC_CAP_SLICE_THREADS 0x2000
+/**
+ * Codec supports changed parameters at any point.
+ */
+#define CODEC_CAP_PARAM_CHANGE 0x4000
+/**
+ * Codec supports avctx->thread_count == 0 (auto).
+ */
+#define CODEC_CAP_AUTO_THREADS 0x8000
+/**
+ * Audio encoder supports receiving a different number of samples in each call.
+ */
+#define CODEC_CAP_VARIABLE_FRAME_SIZE 0x10000
+
+#if FF_API_MB_TYPE
+//The following defines may change, don't expect compatibility if you use them.
+#define MB_TYPE_INTRA4x4 0x0001
+#define MB_TYPE_INTRA16x16 0x0002 //FIXME H.264-specific
+#define MB_TYPE_INTRA_PCM 0x0004 //FIXME H.264-specific
+#define MB_TYPE_16x16 0x0008
+#define MB_TYPE_16x8 0x0010
+#define MB_TYPE_8x16 0x0020
+#define MB_TYPE_8x8 0x0040
+#define MB_TYPE_INTERLACED 0x0080
+#define MB_TYPE_DIRECT2 0x0100 //FIXME
+#define MB_TYPE_ACPRED 0x0200
+#define MB_TYPE_GMC 0x0400
+#define MB_TYPE_SKIP 0x0800
+#define MB_TYPE_P0L0 0x1000
+#define MB_TYPE_P1L0 0x2000
+#define MB_TYPE_P0L1 0x4000
+#define MB_TYPE_P1L1 0x8000
+#define MB_TYPE_L0 (MB_TYPE_P0L0 | MB_TYPE_P1L0)
+#define MB_TYPE_L1 (MB_TYPE_P0L1 | MB_TYPE_P1L1)
+#define MB_TYPE_L0L1 (MB_TYPE_L0 | MB_TYPE_L1)
+#define MB_TYPE_QUANT 0x00010000
+#define MB_TYPE_CBP 0x00020000
+//Note bits 24-31 are reserved for codec specific use (h264 ref0, mpeg1 0mv, ...)
+#endif
+
+/**
+ * Pan Scan area.
+ * This specifies the area which should be displayed.
+ * Note there may be multiple such areas for one frame.
+ */
+typedef struct AVPanScan{
+ /**
+ * id
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int id;
+
+ /**
+ * width and height in 1/16 pel
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int width;
+ int height;
+
+ /**
+ * position of the top left corner in 1/16 pel for up to 3 fields/frames
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int16_t position[3][2];
+}AVPanScan;
+
+#if FF_API_QSCALE_TYPE
+#define FF_QSCALE_TYPE_MPEG1 0
+#define FF_QSCALE_TYPE_MPEG2 1
+#define FF_QSCALE_TYPE_H264 2
+#define FF_QSCALE_TYPE_VP56 3
+#endif
+
+#if FF_API_GET_BUFFER
+#define FF_BUFFER_TYPE_INTERNAL 1
+#define FF_BUFFER_TYPE_USER 2 ///< direct rendering buffers (image is (de)allocated by user)
+#define FF_BUFFER_TYPE_SHARED 4 ///< Buffer from somewhere else; don't deallocate image (data/base), all other tables are not shared.
+#define FF_BUFFER_TYPE_COPY 8 ///< Just a (modified) copy of some other buffer, don't deallocate anything.
+
+#define FF_BUFFER_HINTS_VALID 0x01 // Buffer hints value is meaningful (if 0 ignore).
+#define FF_BUFFER_HINTS_READABLE 0x02 // Codec will read from buffer.
+#define FF_BUFFER_HINTS_PRESERVE 0x04 // User must not alter buffer content.
+#define FF_BUFFER_HINTS_REUSABLE 0x08 // Codec will reuse the buffer (update).
+#endif
+
+/**
+ * The decoder will keep a reference to the frame and may reuse it later.
+ */
+#define AV_GET_BUFFER_FLAG_REF (1 << 0)
+
+/**
+ * @defgroup lavc_packet AVPacket
+ *
+ * Types and functions for working with AVPacket.
+ * @{
+ */
+enum AVPacketSideDataType {
+ AV_PKT_DATA_PALETTE,
+ AV_PKT_DATA_NEW_EXTRADATA,
+
+ /**
+ * An AV_PKT_DATA_PARAM_CHANGE side data packet is laid out as follows:
+ * @code
+ * u32le param_flags
+ * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_COUNT)
+ * s32le channel_count
+ * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_LAYOUT)
+ * u64le channel_layout
+ * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_SAMPLE_RATE)
+ * s32le sample_rate
+ * if (param_flags & AV_SIDE_DATA_PARAM_CHANGE_DIMENSIONS)
+ * s32le width
+ * s32le height
+ * @endcode
+ */
+ AV_PKT_DATA_PARAM_CHANGE,
+
+ /**
+ * An AV_PKT_DATA_H263_MB_INFO side data packet contains a number of
+ * structures with info about macroblocks relevant to splitting the
+ * packet into smaller packets on macroblock edges (e.g. as for RFC 2190).
+ * That is, it does not necessarily contain info about all macroblocks,
+ * as long as the distance between macroblocks in the info is smaller
+ * than the target payload size.
+ * Each MB info structure is 12 bytes, and is laid out as follows:
+ * @code
+ * u32le bit offset from the start of the packet
+ * u8 current quantizer at the start of the macroblock
+ * u8 GOB number
+ * u16le macroblock address within the GOB
+ * u8 horizontal MV predictor
+ * u8 vertical MV predictor
+ * u8 horizontal MV predictor for block number 3
+ * u8 vertical MV predictor for block number 3
+ * @endcode
+ */
+ AV_PKT_DATA_H263_MB_INFO,
+};
+
+/**
+ * This structure stores compressed data. It is typically exported by demuxers
+ * and then passed as input to decoders, or received as output from encoders and
+ * then passed to muxers.
+ *
+ * For video, it should typically contain one compressed frame. For audio it may
+ * contain several compressed frames.
+ *
+ * AVPacket is one of the few structs in Libav, whose size is a part of public
+ * ABI. Thus it may be allocated on stack and no new fields can be added to it
+ * without libavcodec and libavformat major bump.
+ *
+ * The semantics of data ownership depends on the buf or destruct (deprecated)
+ * fields. If either is set, the packet data is dynamically allocated and is
+ * valid indefinitely until av_free_packet() is called (which in turn calls
+ * av_buffer_unref()/the destruct callback to free the data). If neither is set,
+ * the packet data is typically backed by some static buffer somewhere and is
+ * only valid for a limited time (e.g. until the next read call when demuxing).
+ *
+ * The side data is always allocated with av_malloc() and is freed in
+ * av_free_packet().
+ */
+typedef struct AVPacket {
+ /**
+ * A reference to the reference-counted buffer where the packet data is
+ * stored.
+ * May be NULL, then the packet data is not reference-counted.
+ */
+ AVBufferRef *buf;
+ /**
+ * Presentation timestamp in AVStream->time_base units; the time at which
+ * the decompressed packet will be presented to the user.
+ * Can be AV_NOPTS_VALUE if it is not stored in the file.
+ * pts MUST be larger or equal to dts as presentation cannot happen before
+ * decompression, unless one wants to view hex dumps. Some formats misuse
+ * the terms dts and pts/cts to mean something different. Such timestamps
+ * must be converted to true pts/dts before they are stored in AVPacket.
+ */
+ int64_t pts;
+ /**
+ * Decompression timestamp in AVStream->time_base units; the time at which
+ * the packet is decompressed.
+ * Can be AV_NOPTS_VALUE if it is not stored in the file.
+ */
+ int64_t dts;
+ uint8_t *data;
+ int size;
+ int stream_index;
+ /**
+ * A combination of AV_PKT_FLAG values
+ */
+ int flags;
+ /**
+ * Additional packet data that can be provided by the container.
+ * Packet can contain several types of side information.
+ */
+ struct {
+ uint8_t *data;
+ int size;
+ enum AVPacketSideDataType type;
+ } *side_data;
+ int side_data_elems;
+
+ /**
+ * Duration of this packet in AVStream->time_base units, 0 if unknown.
+ * Equals next_pts - this_pts in presentation order.
+ */
+ int duration;
+#if FF_API_DESTRUCT_PACKET
+ attribute_deprecated
+ void (*destruct)(struct AVPacket *);
+ attribute_deprecated
+ void *priv;
+#endif
+ int64_t pos; ///< byte position in stream, -1 if unknown
+
+ /**
+ * Time difference in AVStream->time_base units from the pts of this
+ * packet to the point at which the output from the decoder has converged
+ * independent from the availability of previous frames. That is, the
+ * frames are virtually identical no matter if decoding started from
+ * the very first frame or from this keyframe.
+ * Is AV_NOPTS_VALUE if unknown.
+ * This field is not the display duration of the current packet.
+ * This field has no meaning if the packet does not have AV_PKT_FLAG_KEY
+ * set.
+ *
+ * The purpose of this field is to allow seeking in streams that have no
+ * keyframes in the conventional sense. It corresponds to the
+ * recovery point SEI in H.264 and match_time_delta in NUT. It is also
+ * essential for some types of subtitle streams to ensure that all
+ * subtitles are correctly displayed after seeking.
+ */
+ int64_t convergence_duration;
+} AVPacket;
+#define AV_PKT_FLAG_KEY 0x0001 ///< The packet contains a keyframe
+#define AV_PKT_FLAG_CORRUPT 0x0002 ///< The packet content is corrupted
+
+enum AVSideDataParamChangeFlags {
+ AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_COUNT = 0x0001,
+ AV_SIDE_DATA_PARAM_CHANGE_CHANNEL_LAYOUT = 0x0002,
+ AV_SIDE_DATA_PARAM_CHANGE_SAMPLE_RATE = 0x0004,
+ AV_SIDE_DATA_PARAM_CHANGE_DIMENSIONS = 0x0008,
+};
+/**
+ * @}
+ */
+
+struct AVCodecInternal;
+
+enum AVFieldOrder {
+ AV_FIELD_UNKNOWN,
+ AV_FIELD_PROGRESSIVE,
+ AV_FIELD_TT, //< Top coded_first, top displayed first
+ AV_FIELD_BB, //< Bottom coded first, bottom displayed first
+ AV_FIELD_TB, //< Top coded first, bottom displayed first
+ AV_FIELD_BT, //< Bottom coded first, top displayed first
+};
+
+/**
+ * main external API structure.
+ * New fields can be added to the end with minor version bumps.
+ * Removal, reordering and changes to existing fields require a major
+ * version bump.
+ * sizeof(AVCodecContext) must not be used outside libav*.
+ */
+typedef struct AVCodecContext {
+ /**
+ * information on struct for av_log
+ * - set by avcodec_alloc_context3
+ */
+ const AVClass *av_class;
+ int log_level_offset;
+
+ enum AVMediaType codec_type; /* see AVMEDIA_TYPE_xxx */
+ const struct AVCodec *codec;
+ char codec_name[32];
+ enum AVCodecID codec_id; /* see AV_CODEC_ID_xxx */
+
+ /**
+ * fourcc (LSB first, so "ABCD" -> ('D'<<24) + ('C'<<16) + ('B'<<8) + 'A').
+ * This is used to work around some encoder bugs.
+ * A demuxer should set this to what is stored in the field used to identify the codec.
+ * If there are multiple such fields in a container then the demuxer should choose the one
+ * which maximizes the information about the used codec.
+ * If the codec tag field in a container is larger than 32 bits then the demuxer should
+ * remap the longer ID to 32 bits with a table or other structure. Alternatively a new
+ * extra_codec_tag + size could be added but for this a clear advantage must be demonstrated
+ * first.
+ * - encoding: Set by user, if not then the default based on codec_id will be used.
+ * - decoding: Set by user, will be converted to uppercase by libavcodec during init.
+ */
+ unsigned int codec_tag;
+
+ /**
+ * fourcc from the AVI stream header (LSB first, so "ABCD" -> ('D'<<24) + ('C'<<16) + ('B'<<8) + 'A').
+ * This is used to work around some encoder bugs.
+ * - encoding: unused
+ * - decoding: Set by user, will be converted to uppercase by libavcodec during init.
+ */
+ unsigned int stream_codec_tag;
+
+ void *priv_data;
+
+ /**
+ * Private context used for internal data.
+ *
+ * Unlike priv_data, this is not codec-specific. It is used in general
+ * libavcodec functions.
+ */
+ struct AVCodecInternal *internal;
+
+ /**
+ * Private data of the user, can be used to carry app specific stuff.
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ void *opaque;
+
+ /**
+ * the average bitrate
+ * - encoding: Set by user; unused for constant quantizer encoding.
+ * - decoding: Set by libavcodec. 0 or some bitrate if this info is available in the stream.
+ */
+ int bit_rate;
+
+ /**
+ * number of bits the bitstream is allowed to diverge from the reference.
+ * the reference can be CBR (for CBR pass1) or VBR (for pass2)
+ * - encoding: Set by user; unused for constant quantizer encoding.
+ * - decoding: unused
+ */
+ int bit_rate_tolerance;
+
+ /**
+ * Global quality for codecs which cannot change it per frame.
+ * This should be proportional to MPEG-1/2/4 qscale.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int global_quality;
+
+ /**
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int compression_level;
+#define FF_COMPRESSION_DEFAULT -1
+
+ /**
+ * CODEC_FLAG_*.
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int flags;
+
+ /**
+ * CODEC_FLAG2_*
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int flags2;
+
+ /**
+ * some codecs need / can use extradata like Huffman tables.
+ * mjpeg: Huffman tables
+ * rv10: additional flags
+ * mpeg4: global headers (they can be in the bitstream or here)
+ * The allocated memory should be FF_INPUT_BUFFER_PADDING_SIZE bytes larger
+ * than extradata_size to avoid prolems if it is read with the bitstream reader.
+ * The bytewise contents of extradata must not depend on the architecture or CPU endianness.
+ * - encoding: Set/allocated/freed by libavcodec.
+ * - decoding: Set/allocated/freed by user.
+ */
+ uint8_t *extradata;
+ int extradata_size;
+
+ /**
+ * This is the fundamental unit of time (in seconds) in terms
+ * of which frame timestamps are represented. For fixed-fps content,
+ * timebase should be 1/framerate and timestamp increments should be
+ * identically 1.
+ * - encoding: MUST be set by user.
+ * - decoding: Set by libavcodec.
+ */
+ AVRational time_base;
+
+ /**
+ * For some codecs, the time base is closer to the field rate than the frame rate.
+ * Most notably, H.264 and MPEG-2 specify time_base as half of frame duration
+ * if no telecine is used ...
+ *
+ * Set to time_base ticks per frame. Default 1, e.g., H.264/MPEG-2 set it to 2.
+ */
+ int ticks_per_frame;
+
+ /**
+ * Codec delay.
+ *
+ * Video:
+ * Number of frames the decoded output will be delayed relative to the
+ * encoded input.
+ *
+ * Audio:
+ * For encoding, this is the number of "priming" samples added to the
+ * beginning of the stream. The decoded output will be delayed by this
+ * many samples relative to the input to the encoder. Note that this
+ * field is purely informational and does not directly affect the pts
+ * output by the encoder, which should always be based on the actual
+ * presentation time, including any delay.
+ * For decoding, this is the number of samples the decoder needs to
+ * output before the decoder's output is valid. When seeking, you should
+ * start decoding this many samples prior to your desired seek point.
+ *
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by libavcodec.
+ */
+ int delay;
+
+
+ /* video only */
+ /**
+ * picture width / height.
+ * - encoding: MUST be set by user.
+ * - decoding: May be set by the user before opening the decoder if known e.g.
+ * from the container. Some decoders will require the dimensions
+ * to be set by the caller. During decoding, the decoder may
+ * overwrite those values as required.
+ */
+ int width, height;
+
+ /**
+ * Bitstream width / height, may be different from width/height e.g. when
+ * the decoded frame is cropped before being output.
+ * - encoding: unused
+ * - decoding: May be set by the user before opening the decoder if known
+ * e.g. from the container. During decoding, the decoder may
+ * overwrite those values as required.
+ */
+ int coded_width, coded_height;
+
+#if FF_API_ASPECT_EXTENDED
+#define FF_ASPECT_EXTENDED 15
+#endif
+
+ /**
+ * the number of pictures in a group of pictures, or 0 for intra_only
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int gop_size;
+
+ /**
+ * Pixel format, see AV_PIX_FMT_xxx.
+ * May be set by the demuxer if known from headers.
+ * May be overriden by the decoder if it knows better.
+ * - encoding: Set by user.
+ * - decoding: Set by user if known, overridden by libavcodec if known
+ */
+ enum AVPixelFormat pix_fmt;
+
+ /**
+ * Motion estimation algorithm used for video coding.
+ * 1 (zero), 2 (full), 3 (log), 4 (phods), 5 (epzs), 6 (x1), 7 (hex),
+ * 8 (umh), 10 (tesa) [7, 8, 10 are x264 specific]
+ * - encoding: MUST be set by user.
+ * - decoding: unused
+ */
+ int me_method;
+
+ /**
+ * If non NULL, 'draw_horiz_band' is called by the libavcodec
+ * decoder to draw a horizontal band. It improves cache usage. Not
+ * all codecs can do that. You must check the codec capabilities
+ * beforehand.
+ * When multithreading is used, it may be called from multiple threads
+ * at the same time; threads might draw different parts of the same AVFrame,
+ * or multiple AVFrames, and there is no guarantee that slices will be drawn
+ * in order.
+ * The function is also used by hardware acceleration APIs.
+ * It is called at least once during frame decoding to pass
+ * the data needed for hardware render.
+ * In that mode instead of pixel data, AVFrame points to
+ * a structure specific to the acceleration API. The application
+ * reads the structure and can change some fields to indicate progress
+ * or mark state.
+ * - encoding: unused
+ * - decoding: Set by user.
+ * @param height the height of the slice
+ * @param y the y position of the slice
+ * @param type 1->top field, 2->bottom field, 3->frame
+ * @param offset offset into the AVFrame.data from which the slice should be read
+ */
+ void (*draw_horiz_band)(struct AVCodecContext *s,
+ const AVFrame *src, int offset[AV_NUM_DATA_POINTERS],
+ int y, int type, int height);
+
+ /**
+ * callback to negotiate the pixelFormat
+ * @param fmt is the list of formats which are supported by the codec,
+ * it is terminated by -1 as 0 is a valid format, the formats are ordered by quality.
+ * The first is always the native one.
+ * @return the chosen format
+ * - encoding: unused
+ * - decoding: Set by user, if not set the native format will be chosen.
+ */
+ enum AVPixelFormat (*get_format)(struct AVCodecContext *s, const enum AVPixelFormat * fmt);
+
+ /**
+ * maximum number of B-frames between non-B-frames
+ * Note: The output will be delayed by max_b_frames+1 relative to the input.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int max_b_frames;
+
+ /**
+ * qscale factor between IP and B-frames
+ * If > 0 then the last P-frame quantizer will be used (q= lastp_q*factor+offset).
+ * If < 0 then normal ratecontrol will be done (q= -normal_q*factor+offset).
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float b_quant_factor;
+
+ /** obsolete FIXME remove */
+ int rc_strategy;
+#define FF_RC_STRATEGY_XVID 1
+
+ int b_frame_strategy;
+
+ /**
+ * qscale offset between IP and B-frames
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float b_quant_offset;
+
+ /**
+ * Size of the frame reordering buffer in the decoder.
+ * For MPEG-2 it is 1 IPB or 0 low delay IP.
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by libavcodec.
+ */
+ int has_b_frames;
+
+ /**
+ * 0-> h263 quant 1-> mpeg quant
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mpeg_quant;
+
+ /**
+ * qscale factor between P and I-frames
+ * If > 0 then the last p frame quantizer will be used (q= lastp_q*factor+offset).
+ * If < 0 then normal ratecontrol will be done (q= -normal_q*factor+offset).
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float i_quant_factor;
+
+ /**
+ * qscale offset between P and I-frames
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float i_quant_offset;
+
+ /**
+ * luminance masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float lumi_masking;
+
+ /**
+ * temporary complexity masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float temporal_cplx_masking;
+
+ /**
+ * spatial complexity masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float spatial_cplx_masking;
+
+ /**
+ * p block masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float p_masking;
+
+ /**
+ * darkness masking (0-> disabled)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float dark_masking;
+
+ /**
+ * slice count
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by user (or 0).
+ */
+ int slice_count;
+ /**
+ * prediction method (needed for huffyuv)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int prediction_method;
+#define FF_PRED_LEFT 0
+#define FF_PRED_PLANE 1
+#define FF_PRED_MEDIAN 2
+
+ /**
+ * slice offsets in the frame in bytes
+ * - encoding: Set/allocated by libavcodec.
+ * - decoding: Set/allocated by user (or NULL).
+ */
+ int *slice_offset;
+
+ /**
+ * sample aspect ratio (0 if unknown)
+ * That is the width of a pixel divided by the height of the pixel.
+ * Numerator and denominator must be relatively prime and smaller than 256 for some video standards.
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ AVRational sample_aspect_ratio;
+
+ /**
+ * motion estimation comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_cmp;
+ /**
+ * subpixel motion estimation comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_sub_cmp;
+ /**
+ * macroblock comparison function (not supported yet)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mb_cmp;
+ /**
+ * interlaced DCT comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int ildct_cmp;
+#define FF_CMP_SAD 0
+#define FF_CMP_SSE 1
+#define FF_CMP_SATD 2
+#define FF_CMP_DCT 3
+#define FF_CMP_PSNR 4
+#define FF_CMP_BIT 5
+#define FF_CMP_RD 6
+#define FF_CMP_ZERO 7
+#define FF_CMP_VSAD 8
+#define FF_CMP_VSSE 9
+#define FF_CMP_NSSE 10
+#define FF_CMP_DCTMAX 13
+#define FF_CMP_DCT264 14
+#define FF_CMP_CHROMA 256
+
+ /**
+ * ME diamond size & shape
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int dia_size;
+
+ /**
+ * amount of previous MV predictors (2a+1 x 2a+1 square)
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int last_predictor_count;
+
+ /**
+ * prepass for motion estimation
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int pre_me;
+
+ /**
+ * motion estimation prepass comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_pre_cmp;
+
+ /**
+ * ME prepass diamond size & shape
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int pre_dia_size;
+
+ /**
+ * subpel ME quality
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_subpel_quality;
+
+ /**
+ * DTG active format information (additional aspect ratio
+ * information only used in DVB MPEG-2 transport streams)
+ * 0 if not set.
+ *
+ * - encoding: unused
+ * - decoding: Set by decoder.
+ */
+ int dtg_active_format;
+#define FF_DTG_AFD_SAME 8
+#define FF_DTG_AFD_4_3 9
+#define FF_DTG_AFD_16_9 10
+#define FF_DTG_AFD_14_9 11
+#define FF_DTG_AFD_4_3_SP_14_9 13
+#define FF_DTG_AFD_16_9_SP_14_9 14
+#define FF_DTG_AFD_SP_4_3 15
+
+ /**
+ * maximum motion estimation search range in subpel units
+ * If 0 then no limit.
+ *
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_range;
+
+ /**
+ * intra quantizer bias
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int intra_quant_bias;
+#define FF_DEFAULT_QUANT_BIAS 999999
+
+ /**
+ * inter quantizer bias
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int inter_quant_bias;
+
+ /**
+ * slice flags
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int slice_flags;
+#define SLICE_FLAG_CODED_ORDER 0x0001 ///< draw_horiz_band() is called in coded order instead of display
+#define SLICE_FLAG_ALLOW_FIELD 0x0002 ///< allow draw_horiz_band() with field slices (MPEG2 field pics)
+#define SLICE_FLAG_ALLOW_PLANE 0x0004 ///< allow draw_horiz_band() with 1 component at a time (SVQ1)
+
+#if FF_API_XVMC
+ /**
+ * XVideo Motion Acceleration
+ * - encoding: forbidden
+ * - decoding: set by decoder
+ * @deprecated XvMC support is slated for removal.
+ */
+ attribute_deprecated int xvmc_acceleration;
+#endif /* FF_API_XVMC */
+
+ /**
+ * macroblock decision mode
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mb_decision;
+#define FF_MB_DECISION_SIMPLE 0 ///< uses mb_cmp
+#define FF_MB_DECISION_BITS 1 ///< chooses the one which needs the fewest bits
+#define FF_MB_DECISION_RD 2 ///< rate distortion
+
+ /**
+ * custom intra quantization matrix
+ * - encoding: Set by user, can be NULL.
+ * - decoding: Set by libavcodec.
+ */
+ uint16_t *intra_matrix;
+
+ /**
+ * custom inter quantization matrix
+ * - encoding: Set by user, can be NULL.
+ * - decoding: Set by libavcodec.
+ */
+ uint16_t *inter_matrix;
+
+ /**
+ * scene change detection threshold
+ * 0 is default, larger means fewer detected scene changes.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int scenechange_threshold;
+
+ /**
+ * noise reduction strength
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int noise_reduction;
+
+ /**
+ * Motion estimation threshold below which no motion estimation is
+ * performed, but instead the user specified motion vectors are used.
+ *
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_threshold;
+
+ /**
+ * Macroblock threshold below which the user specified macroblock types will be used.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mb_threshold;
+
+ /**
+ * precision of the intra DC coefficient - 8
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int intra_dc_precision;
+
+ /**
+ * Number of macroblock rows at the top which are skipped.
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int skip_top;
+
+ /**
+ * Number of macroblock rows at the bottom which are skipped.
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int skip_bottom;
+
+ /**
+ * Border processing masking, raises the quantizer for mbs on the borders
+ * of the picture.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float border_masking;
+
+ /**
+ * minimum MB lagrange multipler
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mb_lmin;
+
+ /**
+ * maximum MB lagrange multipler
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mb_lmax;
+
+ /**
+ *
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int me_penalty_compensation;
+
+ /**
+ *
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int bidir_refine;
+
+ /**
+ *
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int brd_scale;
+
+ /**
+ * minimum GOP size
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int keyint_min;
+
+ /**
+ * number of reference frames
+ * - encoding: Set by user.
+ * - decoding: Set by lavc.
+ */
+ int refs;
+
+ /**
+ * chroma qp offset from luma
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int chromaoffset;
+
+ /**
+ * Multiplied by qscale for each frame and added to scene_change_score.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int scenechange_factor;
+
+ /**
+ *
+ * Note: Value depends upon the compare function used for fullpel ME.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int mv0_threshold;
+
+ /**
+ * Adjust sensitivity of b_frame_strategy 1.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int b_sensitivity;
+
+ /**
+ * Chromaticity coordinates of the source primaries.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorPrimaries color_primaries;
+
+ /**
+ * Color Transfer Characteristic.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorTransferCharacteristic color_trc;
+
+ /**
+ * YUV colorspace type.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorSpace colorspace;
+
+ /**
+ * MPEG vs JPEG YUV range.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVColorRange color_range;
+
+ /**
+ * This defines the location of chroma samples.
+ * - encoding: Set by user
+ * - decoding: Set by libavcodec
+ */
+ enum AVChromaLocation chroma_sample_location;
+
+ /**
+ * Number of slices.
+ * Indicates number of picture subdivisions. Used for parallelized
+ * decoding.
+ * - encoding: Set by user
+ * - decoding: unused
+ */
+ int slices;
+
+ /** Field order
+ * - encoding: set by libavcodec
+ * - decoding: Set by libavcodec
+ */
+ enum AVFieldOrder field_order;
+
+ /* audio only */
+ int sample_rate; ///< samples per second
+ int channels; ///< number of audio channels
+
+ /**
+ * audio sample format
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ enum AVSampleFormat sample_fmt; ///< sample format
+
+ /* The following data should not be initialized. */
+ /**
+ * Number of samples per channel in an audio frame.
+ *
+ * - encoding: set by libavcodec in avcodec_open2(). Each submitted frame
+ * except the last must contain exactly frame_size samples per channel.
+ * May be 0 when the codec has CODEC_CAP_VARIABLE_FRAME_SIZE set, then the
+ * frame size is not restricted.
+ * - decoding: may be set by some decoders to indicate constant frame size
+ */
+ int frame_size;
+
+ /**
+ * Frame counter, set by libavcodec.
+ *
+ * - decoding: total number of frames returned from the decoder so far.
+ * - encoding: total number of frames passed to the encoder so far.
+ *
+ * @note the counter is not incremented if encoding/decoding resulted in
+ * an error.
+ */
+ int frame_number;
+
+ /**
+ * number of bytes per packet if constant and known or 0
+ * Used by some WAV based audio codecs.
+ */
+ int block_align;
+
+ /**
+ * Audio cutoff bandwidth (0 means "automatic")
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int cutoff;
+
+#if FF_API_REQUEST_CHANNELS
+ /**
+ * Decoder should decode to this many channels if it can (0 for default)
+ * - encoding: unused
+ * - decoding: Set by user.
+ * @deprecated Deprecated in favor of request_channel_layout.
+ */
+ attribute_deprecated int request_channels;
+#endif
+
+ /**
+ * Audio channel layout.
+ * - encoding: set by user.
+ * - decoding: set by libavcodec.
+ */
+ uint64_t channel_layout;
+
+ /**
+ * Request decoder to use this channel layout if it can (0 for default)
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ uint64_t request_channel_layout;
+
+ /**
+ * Type of service that the audio stream conveys.
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ enum AVAudioServiceType audio_service_type;
+
+ /**
+ * Used to request a sample format from the decoder.
+ * - encoding: unused.
+ * - decoding: Set by user.
+ */
+ enum AVSampleFormat request_sample_fmt;
+
+#if FF_API_GET_BUFFER
+ /**
+ * Called at the beginning of each frame to get a buffer for it.
+ *
+ * The function will set AVFrame.data[], AVFrame.linesize[].
+ * AVFrame.extended_data[] must also be set, but it should be the same as
+ * AVFrame.data[] except for planar audio with more channels than can fit
+ * in AVFrame.data[]. In that case, AVFrame.data[] shall still contain as
+ * many data pointers as it can hold.
+ *
+ * if CODEC_CAP_DR1 is not set then get_buffer() must call
+ * avcodec_default_get_buffer() instead of providing buffers allocated by
+ * some other means.
+ *
+ * AVFrame.data[] should be 32- or 16-byte-aligned unless the CPU doesn't
+ * need it. avcodec_default_get_buffer() aligns the output buffer properly,
+ * but if get_buffer() is overridden then alignment considerations should
+ * be taken into account.
+ *
+ * @see avcodec_default_get_buffer()
+ *
+ * Video:
+ *
+ * If pic.reference is set then the frame will be read later by libavcodec.
+ * avcodec_align_dimensions2() should be used to find the required width and
+ * height, as they normally need to be rounded up to the next multiple of 16.
+ *
+ * If frame multithreading is used and thread_safe_callbacks is set,
+ * it may be called from a different thread, but not from more than one at
+ * once. Does not need to be reentrant.
+ *
+ * @see release_buffer(), reget_buffer()
+ * @see avcodec_align_dimensions2()
+ *
+ * Audio:
+ *
+ * Decoders request a buffer of a particular size by setting
+ * AVFrame.nb_samples prior to calling get_buffer(). The decoder may,
+ * however, utilize only part of the buffer by setting AVFrame.nb_samples
+ * to a smaller value in the output frame.
+ *
+ * Decoders cannot use the buffer after returning from
+ * avcodec_decode_audio4(), so they will not call release_buffer(), as it
+ * is assumed to be released immediately upon return. In some rare cases,
+ * a decoder may need to call get_buffer() more than once in a single
+ * call to avcodec_decode_audio4(). In that case, when get_buffer() is
+ * called again after it has already been called once, the previously
+ * acquired buffer is assumed to be released at that time and may not be
+ * reused by the decoder.
+ *
+ * As a convenience, av_samples_get_buffer_size() and
+ * av_samples_fill_arrays() in libavutil may be used by custom get_buffer()
+ * functions to find the required data size and to fill data pointers and
+ * linesize. In AVFrame.linesize, only linesize[0] may be set for audio
+ * since all planes must be the same size.
+ *
+ * @see av_samples_get_buffer_size(), av_samples_fill_arrays()
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec, user can override.
+ *
+ * @deprecated use get_buffer2()
+ */
+ attribute_deprecated
+ int (*get_buffer)(struct AVCodecContext *c, AVFrame *pic);
+
+ /**
+ * Called to release buffers which were allocated with get_buffer.
+ * A released buffer can be reused in get_buffer().
+ * pic.data[*] must be set to NULL.
+ * May be called from a different thread if frame multithreading is used,
+ * but not by more than one thread at once, so does not need to be reentrant.
+ * - encoding: unused
+ * - decoding: Set by libavcodec, user can override.
+ *
+ * @deprecated custom freeing callbacks should be set from get_buffer2()
+ */
+ attribute_deprecated
+ void (*release_buffer)(struct AVCodecContext *c, AVFrame *pic);
+
+ /**
+ * Called at the beginning of a frame to get cr buffer for it.
+ * Buffer type (size, hints) must be the same. libavcodec won't check it.
+ * libavcodec will pass previous buffer in pic, function should return
+ * same buffer or new buffer with old frame "painted" into it.
+ * If pic.data[0] == NULL must behave like get_buffer().
+ * if CODEC_CAP_DR1 is not set then reget_buffer() must call
+ * avcodec_default_reget_buffer() instead of providing buffers allocated by
+ * some other means.
+ * - encoding: unused
+ * - decoding: Set by libavcodec, user can override.
+ */
+ attribute_deprecated
+ int (*reget_buffer)(struct AVCodecContext *c, AVFrame *pic);
+#endif
+
+ /**
+ * This callback is called at the beginning of each frame to get data
+ * buffer(s) for it. There may be one contiguous buffer for all the data or
+ * there may be a buffer per each data plane or anything in between. What
+ * this means is, you may set however many entries in buf[] you feel necessary.
+ * Each buffer must be reference-counted using the AVBuffer API (see description
+ * of buf[] below).
+ *
+ * The following fields will be set in the frame before this callback is
+ * called:
+ * - format
+ * - width, height (video only)
+ * - sample_rate, channel_layout, nb_samples (audio only)
+ * Their values may differ from the corresponding values in
+ * AVCodecContext. This callback must use the frame values, not the codec
+ * context values, to calculate the required buffer size.
+ *
+ * This callback must fill the following fields in the frame:
+ * - data[]
+ * - linesize[]
+ * - extended_data:
+ * * if the data is planar audio with more than 8 channels, then this
+ * callback must allocate and fill extended_data to contain all pointers
+ * to all data planes. data[] must hold as many pointers as it can.
+ * extended_data must be allocated with av_malloc() and will be freed in
+ * av_frame_unref().
+ * * otherwise exended_data must point to data
+ * - buf[] must contain one or more pointers to AVBufferRef structures. Each of
+ * the frame's data and extended_data pointers must be contained in these. That
+ * is, one AVBufferRef for each allocated chunk of memory, not necessarily one
+ * AVBufferRef per data[] entry. See: av_buffer_create(), av_buffer_alloc(),
+ * and av_buffer_ref().
+ * - extended_buf and nb_extended_buf must be allocated with av_malloc() by
+ * this callback and filled with the extra buffers if there are more
+ * buffers than buf[] can hold. extended_buf will be freed in
+ * av_frame_unref().
+ *
+ * If CODEC_CAP_DR1 is not set then get_buffer2() must call
+ * avcodec_default_get_buffer2() instead of providing buffers allocated by
+ * some other means.
+ *
+ * Each data plane must be aligned to the maximum required by the target
+ * CPU.
+ *
+ * @see avcodec_default_get_buffer2()
+ *
+ * Video:
+ *
+ * If AV_GET_BUFFER_FLAG_REF is set in flags then the frame may be reused
+ * (read and/or written to if it is writable) later by libavcodec.
+ *
+ * avcodec_align_dimensions2() should be used to find the required width and
+ * height, as they normally need to be rounded up to the next multiple of 16.
+ *
+ * If frame multithreading is used and thread_safe_callbacks is set,
+ * this callback may be called from a different thread, but not from more
+ * than one at once. Does not need to be reentrant.
+ *
+ * @see avcodec_align_dimensions2()
+ *
+ * Audio:
+ *
+ * Decoders request a buffer of a particular size by setting
+ * AVFrame.nb_samples prior to calling get_buffer2(). The decoder may,
+ * however, utilize only part of the buffer by setting AVFrame.nb_samples
+ * to a smaller value in the output frame.
+ *
+ * As a convenience, av_samples_get_buffer_size() and
+ * av_samples_fill_arrays() in libavutil may be used by custom get_buffer2()
+ * functions to find the required data size and to fill data pointers and
+ * linesize. In AVFrame.linesize, only linesize[0] may be set for audio
+ * since all planes must be the same size.
+ *
+ * @see av_samples_get_buffer_size(), av_samples_fill_arrays()
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec, user can override.
+ */
+ int (*get_buffer2)(struct AVCodecContext *s, AVFrame *frame, int flags);
+
+ /**
+ * If non-zero, the decoded audio and video frames returned from
+ * avcodec_decode_video2() and avcodec_decode_audio4() are reference-counted
+ * and are valid indefinitely. The caller must free them with
+ * av_frame_unref() when they are not needed anymore.
+ * Otherwise, the decoded frames must not be freed by the caller and are
+ * only valid until the next decode call.
+ *
+ * - encoding: unused
+ * - decoding: set by the caller before avcodec_open2().
+ */
+ int refcounted_frames;
+
+ /* - encoding parameters */
+ float qcompress; ///< amount of qscale change between easy & hard scenes (0.0-1.0)
+ float qblur; ///< amount of qscale smoothing over time (0.0-1.0)
+
+ /**
+ * minimum quantizer
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int qmin;
+
+ /**
+ * maximum quantizer
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int qmax;
+
+ /**
+ * maximum quantizer difference between frames
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int max_qdiff;
+
+ /**
+ * ratecontrol qmin qmax limiting method
+ * 0-> clipping, 1-> use a nice continuous function to limit qscale wthin qmin/qmax.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float rc_qsquish;
+
+ float rc_qmod_amp;
+ int rc_qmod_freq;
+
+ /**
+ * decoder bitstream buffer size
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int rc_buffer_size;
+
+ /**
+ * ratecontrol override, see RcOverride
+ * - encoding: Allocated/set/freed by user.
+ * - decoding: unused
+ */
+ int rc_override_count;
+ RcOverride *rc_override;
+
+ /**
+ * rate control equation
+ * - encoding: Set by user
+ * - decoding: unused
+ */
+ const char *rc_eq;
+
+ /**
+ * maximum bitrate
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int rc_max_rate;
+
+ /**
+ * minimum bitrate
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int rc_min_rate;
+
+ float rc_buffer_aggressivity;
+
+ /**
+ * initial complexity for pass1 ratecontrol
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ float rc_initial_cplx;
+
+ /**
+ * Ratecontrol attempt to use, at maximum, <value> of what can be used without an underflow.
+ * - encoding: Set by user.
+ * - decoding: unused.
+ */
+ float rc_max_available_vbv_use;
+
+ /**
+ * Ratecontrol attempt to use, at least, <value> times the amount needed to prevent a vbv overflow.
+ * - encoding: Set by user.
+ * - decoding: unused.
+ */
+ float rc_min_vbv_overflow_use;
+
+ /**
+ * Number of bits which should be loaded into the rc buffer before decoding starts.
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int rc_initial_buffer_occupancy;
+
+#define FF_CODER_TYPE_VLC 0
+#define FF_CODER_TYPE_AC 1
+#define FF_CODER_TYPE_RAW 2
+#define FF_CODER_TYPE_RLE 3
+#define FF_CODER_TYPE_DEFLATE 4
+ /**
+ * coder type
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int coder_type;
+
+ /**
+ * context model
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int context_model;
+
+ /**
+ * minimum Lagrange multipler
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int lmin;
+
+ /**
+ * maximum Lagrange multipler
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int lmax;
+
+ /**
+ * frame skip threshold
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int frame_skip_threshold;
+
+ /**
+ * frame skip factor
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int frame_skip_factor;
+
+ /**
+ * frame skip exponent
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int frame_skip_exp;
+
+ /**
+ * frame skip comparison function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int frame_skip_cmp;
+
+ /**
+ * trellis RD quantization
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int trellis;
+
+ /**
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int min_prediction_order;
+
+ /**
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int max_prediction_order;
+
+ /**
+ * GOP timecode frame start number, in non drop frame format
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int64_t timecode_frame_start;
+
+ /* The RTP callback: This function is called */
+ /* every time the encoder has a packet to send. */
+ /* It depends on the encoder if the data starts */
+ /* with a Start Code (it should). H.263 does. */
+ /* mb_nb contains the number of macroblocks */
+ /* encoded in the RTP payload. */
+ void (*rtp_callback)(struct AVCodecContext *avctx, void *data, int size, int mb_nb);
+
+ int rtp_payload_size; /* The size of the RTP payload: the coder will */
+ /* do its best to deliver a chunk with size */
+ /* below rtp_payload_size, the chunk will start */
+ /* with a start code on some codecs like H.263. */
+ /* This doesn't take account of any particular */
+ /* headers inside the transmitted RTP payload. */
+
+ /* statistics, used for 2-pass encoding */
+ int mv_bits;
+ int header_bits;
+ int i_tex_bits;
+ int p_tex_bits;
+ int i_count;
+ int p_count;
+ int skip_count;
+ int misc_bits;
+
+ /**
+ * number of bits used for the previously encoded frame
+ * - encoding: Set by libavcodec.
+ * - decoding: unused
+ */
+ int frame_bits;
+
+ /**
+ * pass1 encoding statistics output buffer
+ * - encoding: Set by libavcodec.
+ * - decoding: unused
+ */
+ char *stats_out;
+
+ /**
+ * pass2 encoding statistics input buffer
+ * Concatenated stuff from stats_out of pass1 should be placed here.
+ * - encoding: Allocated/set/freed by user.
+ * - decoding: unused
+ */
+ char *stats_in;
+
+ /**
+ * Work around bugs in encoders which sometimes cannot be detected automatically.
+ * - encoding: Set by user
+ * - decoding: Set by user
+ */
+ int workaround_bugs;
+#define FF_BUG_AUTODETECT 1 ///< autodetection
+#if FF_API_OLD_MSMPEG4
+#define FF_BUG_OLD_MSMPEG4 2
+#endif
+#define FF_BUG_XVID_ILACE 4
+#define FF_BUG_UMP4 8
+#define FF_BUG_NO_PADDING 16
+#define FF_BUG_AMV 32
+#if FF_API_AC_VLC
+#define FF_BUG_AC_VLC 0 ///< Will be removed, libavcodec can now handle these non-compliant files by default.
+#endif
+#define FF_BUG_QPEL_CHROMA 64
+#define FF_BUG_STD_QPEL 128
+#define FF_BUG_QPEL_CHROMA2 256
+#define FF_BUG_DIRECT_BLOCKSIZE 512
+#define FF_BUG_EDGE 1024
+#define FF_BUG_HPEL_CHROMA 2048
+#define FF_BUG_DC_CLIP 4096
+#define FF_BUG_MS 8192 ///< Work around various bugs in Microsoft's broken decoders.
+#define FF_BUG_TRUNCATED 16384
+
+ /**
+ * strictly follow the standard (MPEG4, ...).
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ * Setting this to STRICT or higher means the encoder and decoder will
+ * generally do stupid things, whereas setting it to unofficial or lower
+ * will mean the encoder might produce output that is not supported by all
+ * spec-compliant decoders. Decoders don't differentiate between normal,
+ * unofficial and experimental (that is, they always try to decode things
+ * when they can) unless they are explicitly asked to behave stupidly
+ * (=strictly conform to the specs)
+ */
+ int strict_std_compliance;
+#define FF_COMPLIANCE_VERY_STRICT 2 ///< Strictly conform to an older more strict version of the spec or reference software.
+#define FF_COMPLIANCE_STRICT 1 ///< Strictly conform to all the things in the spec no matter what consequences.
+#define FF_COMPLIANCE_NORMAL 0
+#define FF_COMPLIANCE_UNOFFICIAL -1 ///< Allow unofficial extensions
+#define FF_COMPLIANCE_EXPERIMENTAL -2 ///< Allow nonstandardized experimental things.
+
+ /**
+ * error concealment flags
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int error_concealment;
+#define FF_EC_GUESS_MVS 1
+#define FF_EC_DEBLOCK 2
+
+ /**
+ * debug
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int debug;
+#define FF_DEBUG_PICT_INFO 1
+#define FF_DEBUG_RC 2
+#define FF_DEBUG_BITSTREAM 4
+#define FF_DEBUG_MB_TYPE 8
+#define FF_DEBUG_QP 16
+#if FF_API_DEBUG_MV
+/**
+ * @deprecated this option does nothing
+ */
+#define FF_DEBUG_MV 32
+#endif
+#define FF_DEBUG_DCT_COEFF 0x00000040
+#define FF_DEBUG_SKIP 0x00000080
+#define FF_DEBUG_STARTCODE 0x00000100
+#define FF_DEBUG_PTS 0x00000200
+#define FF_DEBUG_ER 0x00000400
+#define FF_DEBUG_MMCO 0x00000800
+#define FF_DEBUG_BUGS 0x00001000
+#if FF_API_DEBUG_MV
+#define FF_DEBUG_VIS_QP 0x00002000
+#define FF_DEBUG_VIS_MB_TYPE 0x00004000
+#endif
+#define FF_DEBUG_BUFFERS 0x00008000
+#define FF_DEBUG_THREADS 0x00010000
+
+#if FF_API_DEBUG_MV
+ /**
+ * @deprecated this option does not have any effect
+ */
+ attribute_deprecated
+ int debug_mv;
+#define FF_DEBUG_VIS_MV_P_FOR 0x00000001 //visualize forward predicted MVs of P frames
+#define FF_DEBUG_VIS_MV_B_FOR 0x00000002 //visualize forward predicted MVs of B frames
+#define FF_DEBUG_VIS_MV_B_BACK 0x00000004 //visualize backward predicted MVs of B frames
+#endif
+
+ /**
+ * Error recognition; may misdetect some more or less valid parts as errors.
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int err_recognition;
+
+/**
+ * Verify checksums embedded in the bitstream (could be of either encoded or
+ * decoded data, depending on the codec) and print an error message on mismatch.
+ * If AV_EF_EXPLODE is also set, a mismatching checksum will result in the
+ * decoder returning an error.
+ */
+#define AV_EF_CRCCHECK (1<<0)
+#define AV_EF_BITSTREAM (1<<1)
+#define AV_EF_BUFFER (1<<2)
+#define AV_EF_EXPLODE (1<<3)
+
+ /**
+ * opaque 64bit number (generally a PTS) that will be reordered and
+ * output in AVFrame.reordered_opaque
+ * @deprecated in favor of pkt_pts
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ int64_t reordered_opaque;
+
+ /**
+ * Hardware accelerator in use
+ * - encoding: unused.
+ * - decoding: Set by libavcodec
+ */
+ struct AVHWAccel *hwaccel;
+
+ /**
+ * Hardware accelerator context.
+ * For some hardware accelerators, a global context needs to be
+ * provided by the user. In that case, this holds display-dependent
+ * data Libav cannot instantiate itself. Please refer to the
+ * Libav HW accelerator documentation to know how to fill this
+ * is. e.g. for VA API, this is a struct vaapi_context.
+ * - encoding: unused
+ * - decoding: Set by user
+ */
+ void *hwaccel_context;
+
+ /**
+ * error
+ * - encoding: Set by libavcodec if flags&CODEC_FLAG_PSNR.
+ * - decoding: unused
+ */
+ uint64_t error[AV_NUM_DATA_POINTERS];
+
+ /**
+ * DCT algorithm, see FF_DCT_* below
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int dct_algo;
+#define FF_DCT_AUTO 0
+#define FF_DCT_FASTINT 1
+#define FF_DCT_INT 2
+#define FF_DCT_MMX 3
+#define FF_DCT_ALTIVEC 5
+#define FF_DCT_FAAN 6
+
+ /**
+ * IDCT algorithm, see FF_IDCT_* below.
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int idct_algo;
+#define FF_IDCT_AUTO 0
+#define FF_IDCT_INT 1
+#define FF_IDCT_SIMPLE 2
+#define FF_IDCT_SIMPLEMMX 3
+#define FF_IDCT_ARM 7
+#define FF_IDCT_ALTIVEC 8
+#define FF_IDCT_SH4 9
+#define FF_IDCT_SIMPLEARM 10
+#define FF_IDCT_IPP 13
+#define FF_IDCT_XVIDMMX 14
+#define FF_IDCT_SIMPLEARMV5TE 16
+#define FF_IDCT_SIMPLEARMV6 17
+#define FF_IDCT_SIMPLEVIS 18
+#define FF_IDCT_FAAN 20
+#define FF_IDCT_SIMPLENEON 22
+#if FF_API_ARCH_ALPHA
+#define FF_IDCT_SIMPLEALPHA 23
+#endif
+
+ /**
+ * bits per sample/pixel from the demuxer (needed for huffyuv).
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by user.
+ */
+ int bits_per_coded_sample;
+
+ /**
+ * Bits per sample/pixel of internal libavcodec pixel/sample format.
+ * - encoding: set by user.
+ * - decoding: set by libavcodec.
+ */
+ int bits_per_raw_sample;
+
+#if FF_API_LOWRES
+ /**
+ * low resolution decoding, 1-> 1/2 size, 2->1/4 size
+ * - encoding: unused
+ * - decoding: Set by user.
+ *
+ * @deprecated use decoder private options instead
+ */
+ attribute_deprecated int lowres;
+#endif
+
+ /**
+ * the picture in the bitstream
+ * - encoding: Set by libavcodec.
+ * - decoding: unused
+ */
+ AVFrame *coded_frame;
+
+ /**
+ * thread count
+ * is used to decide how many independent tasks should be passed to execute()
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int thread_count;
+
+ /**
+ * Which multithreading methods to use.
+ * Use of FF_THREAD_FRAME will increase decoding delay by one frame per thread,
+ * so clients which cannot provide future frames should not use it.
+ *
+ * - encoding: Set by user, otherwise the default is used.
+ * - decoding: Set by user, otherwise the default is used.
+ */
+ int thread_type;
+#define FF_THREAD_FRAME 1 ///< Decode more than one frame at once
+#define FF_THREAD_SLICE 2 ///< Decode more than one part of a single frame at once
+
+ /**
+ * Which multithreading methods are in use by the codec.
+ * - encoding: Set by libavcodec.
+ * - decoding: Set by libavcodec.
+ */
+ int active_thread_type;
+
+ /**
+ * Set by the client if its custom get_buffer() callback can be called
+ * synchronously from another thread, which allows faster multithreaded decoding.
+ * draw_horiz_band() will be called from other threads regardless of this setting.
+ * Ignored if the default get_buffer() is used.
+ * - encoding: Set by user.
+ * - decoding: Set by user.
+ */
+ int thread_safe_callbacks;
+
+ /**
+ * The codec may call this to execute several independent things.
+ * It will return only after finishing all tasks.
+ * The user may replace this with some multithreaded implementation,
+ * the default implementation will execute the parts serially.
+ * @param count the number of things to execute
+ * - encoding: Set by libavcodec, user can override.
+ * - decoding: Set by libavcodec, user can override.
+ */
+ int (*execute)(struct AVCodecContext *c, int (*func)(struct AVCodecContext *c2, void *arg), void *arg2, int *ret, int count, int size);
+
+ /**
+ * The codec may call this to execute several independent things.
+ * It will return only after finishing all tasks.
+ * The user may replace this with some multithreaded implementation,
+ * the default implementation will execute the parts serially.
+ * Also see avcodec_thread_init and e.g. the --enable-pthread configure option.
+ * @param c context passed also to func
+ * @param count the number of things to execute
+ * @param arg2 argument passed unchanged to func
+ * @param ret return values of executed functions, must have space for "count" values. May be NULL.
+ * @param func function that will be called count times, with jobnr from 0 to count-1.
+ * threadnr will be in the range 0 to c->thread_count-1 < MAX_THREADS and so that no
+ * two instances of func executing at the same time will have the same threadnr.
+ * @return always 0 currently, but code should handle a future improvement where when any call to func
+ * returns < 0 no further calls to func may be done and < 0 is returned.
+ * - encoding: Set by libavcodec, user can override.
+ * - decoding: Set by libavcodec, user can override.
+ */
+ int (*execute2)(struct AVCodecContext *c, int (*func)(struct AVCodecContext *c2, void *arg, int jobnr, int threadnr), void *arg2, int *ret, int count);
+
+#if FF_API_THREAD_OPAQUE
+ /**
+ * @deprecated this field should not be used from outside of lavc
+ */
+ attribute_deprecated
+ void *thread_opaque;
+#endif
+
+ /**
+ * noise vs. sse weight for the nsse comparsion function
+ * - encoding: Set by user.
+ * - decoding: unused
+ */
+ int nsse_weight;
+
+ /**
+ * profile
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int profile;
+#define FF_PROFILE_UNKNOWN -99
+#define FF_PROFILE_RESERVED -100
+
+#define FF_PROFILE_AAC_MAIN 0
+#define FF_PROFILE_AAC_LOW 1
+#define FF_PROFILE_AAC_SSR 2
+#define FF_PROFILE_AAC_LTP 3
+#define FF_PROFILE_AAC_HE 4
+#define FF_PROFILE_AAC_HE_V2 28
+#define FF_PROFILE_AAC_LD 22
+#define FF_PROFILE_AAC_ELD 38
+#define FF_PROFILE_MPEG2_AAC_LOW 128
+#define FF_PROFILE_MPEG2_AAC_HE 131
+
+#define FF_PROFILE_DTS 20
+#define FF_PROFILE_DTS_ES 30
+#define FF_PROFILE_DTS_96_24 40
+#define FF_PROFILE_DTS_HD_HRA 50
+#define FF_PROFILE_DTS_HD_MA 60
+
+#define FF_PROFILE_MPEG2_422 0
+#define FF_PROFILE_MPEG2_HIGH 1
+#define FF_PROFILE_MPEG2_SS 2
+#define FF_PROFILE_MPEG2_SNR_SCALABLE 3
+#define FF_PROFILE_MPEG2_MAIN 4
+#define FF_PROFILE_MPEG2_SIMPLE 5
+
+#define FF_PROFILE_H264_CONSTRAINED (1<<9) // 8+1; constraint_set1_flag
+#define FF_PROFILE_H264_INTRA (1<<11) // 8+3; constraint_set3_flag
+
+#define FF_PROFILE_H264_BASELINE 66
+#define FF_PROFILE_H264_CONSTRAINED_BASELINE (66|FF_PROFILE_H264_CONSTRAINED)
+#define FF_PROFILE_H264_MAIN 77
+#define FF_PROFILE_H264_EXTENDED 88
+#define FF_PROFILE_H264_HIGH 100
+#define FF_PROFILE_H264_HIGH_10 110
+#define FF_PROFILE_H264_HIGH_10_INTRA (110|FF_PROFILE_H264_INTRA)
+#define FF_PROFILE_H264_HIGH_422 122
+#define FF_PROFILE_H264_HIGH_422_INTRA (122|FF_PROFILE_H264_INTRA)
+#define FF_PROFILE_H264_HIGH_444 144
+#define FF_PROFILE_H264_HIGH_444_PREDICTIVE 244
+#define FF_PROFILE_H264_HIGH_444_INTRA (244|FF_PROFILE_H264_INTRA)
+#define FF_PROFILE_H264_CAVLC_444 44
+
+#define FF_PROFILE_VC1_SIMPLE 0
+#define FF_PROFILE_VC1_MAIN 1
+#define FF_PROFILE_VC1_COMPLEX 2
+#define FF_PROFILE_VC1_ADVANCED 3
+
+#define FF_PROFILE_MPEG4_SIMPLE 0
+#define FF_PROFILE_MPEG4_SIMPLE_SCALABLE 1
+#define FF_PROFILE_MPEG4_CORE 2
+#define FF_PROFILE_MPEG4_MAIN 3
+#define FF_PROFILE_MPEG4_N_BIT 4
+#define FF_PROFILE_MPEG4_SCALABLE_TEXTURE 5
+#define FF_PROFILE_MPEG4_SIMPLE_FACE_ANIMATION 6
+#define FF_PROFILE_MPEG4_BASIC_ANIMATED_TEXTURE 7
+#define FF_PROFILE_MPEG4_HYBRID 8
+#define FF_PROFILE_MPEG4_ADVANCED_REAL_TIME 9
+#define FF_PROFILE_MPEG4_CORE_SCALABLE 10
+#define FF_PROFILE_MPEG4_ADVANCED_CODING 11
+#define FF_PROFILE_MPEG4_ADVANCED_CORE 12
+#define FF_PROFILE_MPEG4_ADVANCED_SCALABLE_TEXTURE 13
+#define FF_PROFILE_MPEG4_SIMPLE_STUDIO 14
+#define FF_PROFILE_MPEG4_ADVANCED_SIMPLE 15
+
+#define FF_PROFILE_JPEG2000_CSTREAM_RESTRICTION_0 0
+#define FF_PROFILE_JPEG2000_CSTREAM_RESTRICTION_1 1
+#define FF_PROFILE_JPEG2000_CSTREAM_NO_RESTRICTION 2
+#define FF_PROFILE_JPEG2000_DCINEMA_2K 3
+#define FF_PROFILE_JPEG2000_DCINEMA_4K 4
+
+
+#define FF_PROFILE_HEVC_MAIN 1
+#define FF_PROFILE_HEVC_MAIN_10 2
+#define FF_PROFILE_HEVC_MAIN_STILL_PICTURE 3
+
+ /**
+ * level
+ * - encoding: Set by user.
+ * - decoding: Set by libavcodec.
+ */
+ int level;
+#define FF_LEVEL_UNKNOWN -99
+
+ /**
+ *
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ enum AVDiscard skip_loop_filter;
+
+ /**
+ *
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ enum AVDiscard skip_idct;
+
+ /**
+ *
+ * - encoding: unused
+ * - decoding: Set by user.
+ */
+ enum AVDiscard skip_frame;
+
+ /**
+ * Header containing style information for text subtitles.
+ * For SUBTITLE_ASS subtitle type, it should contain the whole ASS
+ * [Script Info] and [V4+ Styles] section, plus the [Events] line and
+ * the Format line following. It shouldn't include any Dialogue line.
+ * - encoding: Set/allocated/freed by user (before avcodec_open2())
+ * - decoding: Set/allocated/freed by libavcodec (by avcodec_open2())
+ */
+ uint8_t *subtitle_header;
+ int subtitle_header_size;
+
+#if FF_API_ERROR_RATE
+ /**
+ * @deprecated use the 'error_rate' private AVOption of the mpegvideo
+ * encoders
+ */
+ attribute_deprecated
+ int error_rate;
+#endif
+
+#if FF_API_CODEC_PKT
+ /**
+ * @deprecated this field is not supposed to be accessed from outside lavc
+ */
+ attribute_deprecated
+ AVPacket *pkt;
+#endif
+
+ /**
+ * VBV delay coded in the last frame (in periods of a 27 MHz clock).
+ * Used for compliant TS muxing.
+ * - encoding: Set by libavcodec.
+ * - decoding: unused.
+ */
+ uint64_t vbv_delay;
+} AVCodecContext;
+
+/**
+ * AVProfile.
+ */
+typedef struct AVProfile {
+ int profile;
+ const char *name; ///< short name for the profile
+} AVProfile;
+
+typedef struct AVCodecDefault AVCodecDefault;
+
+struct AVSubtitle;
+
+/**
+ * AVCodec.
+ */
+typedef struct AVCodec {
+ /**
+ * Name of the codec implementation.
+ * The name is globally unique among encoders and among decoders (but an
+ * encoder and a decoder can share the same name).
+ * This is the primary way to find a codec from the user perspective.
+ */
+ const char *name;
+ /**
+ * Descriptive name for the codec, meant to be more human readable than name.
+ * You should use the NULL_IF_CONFIG_SMALL() macro to define it.
+ */
+ const char *long_name;
+ enum AVMediaType type;
+ enum AVCodecID id;
+ /**
+ * Codec capabilities.
+ * see CODEC_CAP_*
+ */
+ int capabilities;
+ const AVRational *supported_framerates; ///< array of supported framerates, or NULL if any, array is terminated by {0,0}
+ const enum AVPixelFormat *pix_fmts; ///< array of supported pixel formats, or NULL if unknown, array is terminated by -1
+ const int *supported_samplerates; ///< array of supported audio samplerates, or NULL if unknown, array is terminated by 0
+ const enum AVSampleFormat *sample_fmts; ///< array of supported sample formats, or NULL if unknown, array is terminated by -1
+ const uint64_t *channel_layouts; ///< array of support channel layouts, or NULL if unknown. array is terminated by 0
+#if FF_API_LOWRES
+ attribute_deprecated uint8_t max_lowres; ///< maximum value for lowres supported by the decoder
+#endif
+ const AVClass *priv_class; ///< AVClass for the private context
+ const AVProfile *profiles; ///< array of recognized profiles, or NULL if unknown, array is terminated by {FF_PROFILE_UNKNOWN}
+
+ /*****************************************************************
+ * No fields below this line are part of the public API. They
+ * may not be used outside of libavcodec and can be changed and
+ * removed at will.
+ * New public fields should be added right above.
+ *****************************************************************
+ */
+ int priv_data_size;
+ struct AVCodec *next;
+ /**
+ * @name Frame-level threading support functions
+ * @{
+ */
+ /**
+ * If defined, called on thread contexts when they are created.
+ * If the codec allocates writable tables in init(), re-allocate them here.
+ * priv_data will be set to a copy of the original.
+ */
+ int (*init_thread_copy)(AVCodecContext *);
+ /**
+ * Copy necessary context variables from a previous thread context to the current one.
+ * If not defined, the next thread will start automatically; otherwise, the codec
+ * must call ff_thread_finish_setup().
+ *
+ * dst and src will (rarely) point to the same context, in which case memcpy should be skipped.
+ */
+ int (*update_thread_context)(AVCodecContext *dst, const AVCodecContext *src);
+ /** @} */
+
+ /**
+ * Private codec-specific defaults.
+ */
+ const AVCodecDefault *defaults;
+
+ /**
+ * Initialize codec static data, called from avcodec_register().
+ */
+ void (*init_static_data)(struct AVCodec *codec);
+
+ int (*init)(AVCodecContext *);
+ int (*encode_sub)(AVCodecContext *, uint8_t *buf, int buf_size,
+ const struct AVSubtitle *sub);
+ /**
+ * Encode data to an AVPacket.
+ *
+ * @param avctx codec context
+ * @param avpkt output AVPacket (may contain a user-provided buffer)
+ * @param[in] frame AVFrame containing the raw data to be encoded
+ * @param[out] got_packet_ptr encoder sets to 0 or 1 to indicate that a
+ * non-empty packet was returned in avpkt.
+ * @return 0 on success, negative error code on failure
+ */
+ int (*encode2)(AVCodecContext *avctx, AVPacket *avpkt, const AVFrame *frame,
+ int *got_packet_ptr);
+ int (*decode)(AVCodecContext *, void *outdata, int *outdata_size, AVPacket *avpkt);
+ int (*close)(AVCodecContext *);
+ /**
+ * Flush buffers.
+ * Will be called when seeking
+ */
+ void (*flush)(AVCodecContext *);
+} AVCodec;
+
+/**
+ * AVHWAccel.
+ */
+typedef struct AVHWAccel {
+ /**
+ * Name of the hardware accelerated codec.
+ * The name is globally unique among encoders and among decoders (but an
+ * encoder and a decoder can share the same name).
+ */
+ const char *name;
+
+ /**
+ * Type of codec implemented by the hardware accelerator.
+ *
+ * See AVMEDIA_TYPE_xxx
+ */
+ enum AVMediaType type;
+
+ /**
+ * Codec implemented by the hardware accelerator.
+ *
+ * See AV_CODEC_ID_xxx
+ */
+ enum AVCodecID id;
+
+ /**
+ * Supported pixel format.
+ *
+ * Only hardware accelerated formats are supported here.
+ */
+ enum AVPixelFormat pix_fmt;
+
+ /**
+ * Hardware accelerated codec capabilities.
+ * see FF_HWACCEL_CODEC_CAP_*
+ */
+ int capabilities;
+
+ struct AVHWAccel *next;
+
+ /**
+ * Called at the beginning of each frame or field picture.
+ *
+ * Meaningful frame information (codec specific) is guaranteed to
+ * be parsed at this point. This function is mandatory.
+ *
+ * Note that buf can be NULL along with buf_size set to 0.
+ * Otherwise, this means the whole frame is available at this point.
+ *
+ * @param avctx the codec context
+ * @param buf the frame data buffer base
+ * @param buf_size the size of the frame in bytes
+ * @return zero if successful, a negative value otherwise
+ */
+ int (*start_frame)(AVCodecContext *avctx, const uint8_t *buf, uint32_t buf_size);
+
+ /**
+ * Callback for each slice.
+ *
+ * Meaningful slice information (codec specific) is guaranteed to
+ * be parsed at this point. This function is mandatory.
+ *
+ * @param avctx the codec context
+ * @param buf the slice data buffer base
+ * @param buf_size the size of the slice in bytes
+ * @return zero if successful, a negative value otherwise
+ */
+ int (*decode_slice)(AVCodecContext *avctx, const uint8_t *buf, uint32_t buf_size);
+
+ /**
+ * Called at the end of each frame or field picture.
+ *
+ * The whole picture is parsed at this point and can now be sent
+ * to the hardware accelerator. This function is mandatory.
+ *
+ * @param avctx the codec context
+ * @return zero if successful, a negative value otherwise
+ */
+ int (*end_frame)(AVCodecContext *avctx);
+
+ /**
+ * Size of HW accelerator private data.
+ *
+ * Private data is allocated with av_mallocz() before
+ * AVCodecContext.get_buffer() and deallocated after
+ * AVCodecContext.release_buffer().
+ */
+ int priv_data_size;
+} AVHWAccel;
+
+/**
+ * @defgroup lavc_picture AVPicture
+ *
+ * Functions for working with AVPicture
+ * @{
+ */
+
+/**
+ * four components are given, that's all.
+ * the last component is alpha
+ */
+typedef struct AVPicture {
+ uint8_t *data[AV_NUM_DATA_POINTERS];
+ int linesize[AV_NUM_DATA_POINTERS]; ///< number of bytes per line
+} AVPicture;
+
+/**
+ * @}
+ */
+
+#define AVPALETTE_SIZE 1024
+#define AVPALETTE_COUNT 256
+
+enum AVSubtitleType {
+ SUBTITLE_NONE,
+
+ SUBTITLE_BITMAP, ///< A bitmap, pict will be set
+
+ /**
+ * Plain text, the text field must be set by the decoder and is
+ * authoritative. ass and pict fields may contain approximations.
+ */
+ SUBTITLE_TEXT,
+
+ /**
+ * Formatted text, the ass field must be set by the decoder and is
+ * authoritative. pict and text fields may contain approximations.
+ */
+ SUBTITLE_ASS,
+};
+
+#define AV_SUBTITLE_FLAG_FORCED 0x00000001
+
+typedef struct AVSubtitleRect {
+ int x; ///< top left corner of pict, undefined when pict is not set
+ int y; ///< top left corner of pict, undefined when pict is not set
+ int w; ///< width of pict, undefined when pict is not set
+ int h; ///< height of pict, undefined when pict is not set
+ int nb_colors; ///< number of colors in pict, undefined when pict is not set
+
+ /**
+ * data+linesize for the bitmap of this subtitle.
+ * can be set for text/ass as well once they where rendered
+ */
+ AVPicture pict;
+ enum AVSubtitleType type;
+
+ char *text; ///< 0 terminated plain UTF-8 text
+
+ /**
+ * 0 terminated ASS/SSA compatible event line.
+ * The pressentation of this is unaffected by the other values in this
+ * struct.
+ */
+ char *ass;
+ int flags;
+} AVSubtitleRect;
+
+typedef struct AVSubtitle {
+ uint16_t format; /* 0 = graphics */
+ uint32_t start_display_time; /* relative to packet pts, in ms */
+ uint32_t end_display_time; /* relative to packet pts, in ms */
+ unsigned num_rects;
+ AVSubtitleRect **rects;
+ int64_t pts; ///< Same as packet pts, in AV_TIME_BASE
+} AVSubtitle;
+
+/**
+ * If c is NULL, returns the first registered codec,
+ * if c is non-NULL, returns the next registered codec after c,
+ * or NULL if c is the last one.
+ */
+AVCodec *av_codec_next(const AVCodec *c);
+
+/**
+ * Return the LIBAVCODEC_VERSION_INT constant.
+ */
+unsigned avcodec_version(void);
+
+/**
+ * Return the libavcodec build-time configuration.
+ */
+const char *avcodec_configuration(void);
+
+/**
+ * Return the libavcodec license.
+ */
+const char *avcodec_license(void);
+
+/**
+ * Register the codec codec and initialize libavcodec.
+ *
+ * @warning either this function or avcodec_register_all() must be called
+ * before any other libavcodec functions.
+ *
+ * @see avcodec_register_all()
+ */
+void avcodec_register(AVCodec *codec);
+
+/**
+ * Register all the codecs, parsers and bitstream filters which were enabled at
+ * configuration time. If you do not call this function you can select exactly
+ * which formats you want to support, by using the individual registration
+ * functions.
+ *
+ * @see avcodec_register
+ * @see av_register_codec_parser
+ * @see av_register_bitstream_filter
+ */
+void avcodec_register_all(void);
+
+/**
+ * Allocate an AVCodecContext and set its fields to default values. The
+ * resulting struct can be deallocated by calling avcodec_close() on it followed
+ * by av_free().
+ *
+ * @param codec if non-NULL, allocate private data and initialize defaults
+ * for the given codec. It is illegal to then call avcodec_open2()
+ * with a different codec.
+ * If NULL, then the codec-specific defaults won't be initialized,
+ * which may result in suboptimal default settings (this is
+ * important mainly for encoders, e.g. libx264).
+ *
+ * @return An AVCodecContext filled with default values or NULL on failure.
+ * @see avcodec_get_context_defaults
+ */
+AVCodecContext *avcodec_alloc_context3(const AVCodec *codec);
+
+/**
+ * Set the fields of the given AVCodecContext to default values corresponding
+ * to the given codec (defaults may be codec-dependent).
+ *
+ * Do not call this function if a non-NULL codec has been passed
+ * to avcodec_alloc_context3() that allocated this AVCodecContext.
+ * If codec is non-NULL, it is illegal to call avcodec_open2() with a
+ * different codec on this AVCodecContext.
+ */
+int avcodec_get_context_defaults3(AVCodecContext *s, const AVCodec *codec);
+
+/**
+ * Get the AVClass for AVCodecContext. It can be used in combination with
+ * AV_OPT_SEARCH_FAKE_OBJ for examining options.
+ *
+ * @see av_opt_find().
+ */
+const AVClass *avcodec_get_class(void);
+
+/**
+ * Copy the settings of the source AVCodecContext into the destination
+ * AVCodecContext. The resulting destination codec context will be
+ * unopened, i.e. you are required to call avcodec_open2() before you
+ * can use this AVCodecContext to decode/encode video/audio data.
+ *
+ * @param dest target codec context, should be initialized with
+ * avcodec_alloc_context3(), but otherwise uninitialized
+ * @param src source codec context
+ * @return AVERROR() on error (e.g. memory allocation error), 0 on success
+ */
+int avcodec_copy_context(AVCodecContext *dest, const AVCodecContext *src);
+
+#if FF_API_AVFRAME_LAVC
+/**
+ * @deprecated use av_frame_alloc()
+ */
+attribute_deprecated
+AVFrame *avcodec_alloc_frame(void);
+
+/**
+ * Set the fields of the given AVFrame to default values.
+ *
+ * @param frame The AVFrame of which the fields should be set to default values.
+ *
+ * @deprecated use av_frame_unref()
+ */
+attribute_deprecated
+void avcodec_get_frame_defaults(AVFrame *frame);
+
+/**
+ * Free the frame and any dynamically allocated objects in it,
+ * e.g. extended_data.
+ *
+ * @param frame frame to be freed. The pointer will be set to NULL.
+ *
+ * @warning this function does NOT free the data buffers themselves
+ * (it does not know how, since they might have been allocated with
+ * a custom get_buffer()).
+ *
+ * @deprecated use av_frame_free()
+ */
+attribute_deprecated
+void avcodec_free_frame(AVFrame **frame);
+#endif
+
+/**
+ * Initialize the AVCodecContext to use the given AVCodec. Prior to using this
+ * function the context has to be allocated with avcodec_alloc_context3().
+ *
+ * The functions avcodec_find_decoder_by_name(), avcodec_find_encoder_by_name(),
+ * avcodec_find_decoder() and avcodec_find_encoder() provide an easy way for
+ * retrieving a codec.
+ *
+ * @warning This function is not thread safe!
+ *
+ * @code
+ * avcodec_register_all();
+ * av_dict_set(&opts, "b", "2.5M", 0);
+ * codec = avcodec_find_decoder(AV_CODEC_ID_H264);
+ * if (!codec)
+ * exit(1);
+ *
+ * context = avcodec_alloc_context3(codec);
+ *
+ * if (avcodec_open2(context, codec, opts) < 0)
+ * exit(1);
+ * @endcode
+ *
+ * @param avctx The context to initialize.
+ * @param codec The codec to open this context for. If a non-NULL codec has been
+ * previously passed to avcodec_alloc_context3() or
+ * avcodec_get_context_defaults3() for this context, then this
+ * parameter MUST be either NULL or equal to the previously passed
+ * codec.
+ * @param options A dictionary filled with AVCodecContext and codec-private options.
+ * On return this object will be filled with options that were not found.
+ *
+ * @return zero on success, a negative value on error
+ * @see avcodec_alloc_context3(), avcodec_find_decoder(), avcodec_find_encoder(),
+ * av_dict_set(), av_opt_find().
+ */
+int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);
+
+/**
+ * Close a given AVCodecContext and free all the data associated with it
+ * (but not the AVCodecContext itself).
+ *
+ * Calling this function on an AVCodecContext that hasn't been opened will free
+ * the codec-specific data allocated in avcodec_alloc_context3() /
+ * avcodec_get_context_defaults3() with a non-NULL codec. Subsequent calls will
+ * do nothing.
+ */
+int avcodec_close(AVCodecContext *avctx);
+
+/**
+ * Free all allocated data in the given subtitle struct.
+ *
+ * @param sub AVSubtitle to free.
+ */
+void avsubtitle_free(AVSubtitle *sub);
+
+/**
+ * @}
+ */
+
+/**
+ * @addtogroup lavc_packet
+ * @{
+ */
+
+#if FF_API_DESTRUCT_PACKET
+/**
+ * Default packet destructor.
+ * @deprecated use the AVBuffer API instead
+ */
+attribute_deprecated
+void av_destruct_packet(AVPacket *pkt);
+#endif
+
+/**
+ * Initialize optional fields of a packet with default values.
+ *
+ * Note, this does not touch the data and size members, which have to be
+ * initialized separately.
+ *
+ * @param pkt packet
+ */
+void av_init_packet(AVPacket *pkt);
+
+/**
+ * Allocate the payload of a packet and initialize its fields with
+ * default values.
+ *
+ * @param pkt packet
+ * @param size wanted payload size
+ * @return 0 if OK, AVERROR_xxx otherwise
+ */
+int av_new_packet(AVPacket *pkt, int size);
+
+/**
+ * Reduce packet size, correctly zeroing padding
+ *
+ * @param pkt packet
+ * @param size new size
+ */
+void av_shrink_packet(AVPacket *pkt, int size);
+
+/**
+ * Increase packet size, correctly zeroing padding
+ *
+ * @param pkt packet
+ * @param grow_by number of bytes by which to increase the size of the packet
+ */
+int av_grow_packet(AVPacket *pkt, int grow_by);
+
+/**
+ * Initialize a reference-counted packet from av_malloc()ed data.
+ *
+ * @param pkt packet to be initialized. This function will set the data, size,
+ * buf and destruct fields, all others are left untouched.
+ * @param data Data allocated by av_malloc() to be used as packet data. If this
+ * function returns successfully, the data is owned by the underlying AVBuffer.
+ * The caller may not access the data through other means.
+ * @param size size of data in bytes, without the padding. I.e. the full buffer
+ * size is assumed to be size + FF_INPUT_BUFFER_PADDING_SIZE.
+ *
+ * @return 0 on success, a negative AVERROR on error
+ */
+int av_packet_from_data(AVPacket *pkt, uint8_t *data, int size);
+
+/**
+ * @warning This is a hack - the packet memory allocation stuff is broken. The
+ * packet is allocated if it was not really allocated.
+ */
+int av_dup_packet(AVPacket *pkt);
+
+/**
+ * Free a packet.
+ *
+ * @param pkt packet to free
+ */
+void av_free_packet(AVPacket *pkt);
+
+/**
+ * Allocate new information of a packet.
+ *
+ * @param pkt packet
+ * @param type side information type
+ * @param size side information size
+ * @return pointer to fresh allocated data or NULL otherwise
+ */
+uint8_t* av_packet_new_side_data(AVPacket *pkt, enum AVPacketSideDataType type,
+ int size);
+
+/**
+ * Shrink the already allocated side data buffer
+ *
+ * @param pkt packet
+ * @param type side information type
+ * @param size new side information size
+ * @return 0 on success, < 0 on failure
+ */
+int av_packet_shrink_side_data(AVPacket *pkt, enum AVPacketSideDataType type,
+ int size);
+
+/**
+ * Get side information from packet.
+ *
+ * @param pkt packet
+ * @param type desired side information type
+ * @param size pointer for side information size to store (optional)
+ * @return pointer to data if present or NULL otherwise
+ */
+uint8_t* av_packet_get_side_data(AVPacket *pkt, enum AVPacketSideDataType type,
+ int *size);
+
+/**
+ * Convenience function to free all the side data stored.
+ * All the other fields stay untouched.
+ *
+ * @param pkt packet
+ */
+void av_packet_free_side_data(AVPacket *pkt);
+
+/**
+ * Setup a new reference to the data described by a given packet
+ *
+ * If src is reference-counted, setup dst as a new reference to the
+ * buffer in src. Otherwise allocate a new buffer in dst and copy the
+ * data from src into it.
+ *
+ * All the other fields are copied from src.
+ *
+ * @see av_packet_unref
+ *
+ * @param dst Destination packet
+ * @param src Source packet
+ *
+ * @return 0 on success, a negative AVERROR on error.
+ */
+int av_packet_ref(AVPacket *dst, AVPacket *src);
+
+/**
+ * Wipe the packet.
+ *
+ * Unreference the buffer referenced by the packet and reset the
+ * remaining packet fields to their default values.
+ *
+ * @param pkt The packet to be unreferenced.
+ */
+void av_packet_unref(AVPacket *pkt);
+
+/**
+ * Move every field in src to dst and reset src.
+ *
+ * @see av_packet_unref
+ *
+ * @param src Source packet, will be reset
+ * @param dst Destination packet
+ */
+void av_packet_move_ref(AVPacket *dst, AVPacket *src);
+
+/**
+ * Copy only "properties" fields from src to dst.
+ *
+ * Properties for the purpose of this function are all the fields
+ * beside those related to the packet data (buf, data, size)
+ *
+ * @param dst Destination packet
+ * @param src Source packet
+ *
+ * @return 0 on success AVERROR on failure.
+ *
+ */
+int av_packet_copy_props(AVPacket *dst, const AVPacket *src);
+
+/**
+ * @}
+ */
+
+/**
+ * @addtogroup lavc_decoding
+ * @{
+ */
+
+/**
+ * Find a registered decoder with a matching codec ID.
+ *
+ * @param id AVCodecID of the requested decoder
+ * @return A decoder if one was found, NULL otherwise.
+ */
+AVCodec *avcodec_find_decoder(enum AVCodecID id);
+
+/**
+ * Find a registered decoder with the specified name.
+ *
+ * @param name name of the requested decoder
+ * @return A decoder if one was found, NULL otherwise.
+ */
+AVCodec *avcodec_find_decoder_by_name(const char *name);
+
+#if FF_API_GET_BUFFER
+attribute_deprecated int avcodec_default_get_buffer(AVCodecContext *s, AVFrame *pic);
+attribute_deprecated void avcodec_default_release_buffer(AVCodecContext *s, AVFrame *pic);
+attribute_deprecated int avcodec_default_reget_buffer(AVCodecContext *s, AVFrame *pic);
+#endif
+
+/**
+ * The default callback for AVCodecContext.get_buffer2(). It is made public so
+ * it can be called by custom get_buffer2() implementations for decoders without
+ * CODEC_CAP_DR1 set.
+ */
+int avcodec_default_get_buffer2(AVCodecContext *s, AVFrame *frame, int flags);
+
+#if FF_API_EMU_EDGE
+/**
+ * Return the amount of padding in pixels which the get_buffer callback must
+ * provide around the edge of the image for codecs which do not have the
+ * CODEC_FLAG_EMU_EDGE flag.
+ *
+ * @return Required padding in pixels.
+ *
+ * @deprecated CODEC_FLAG_EMU_EDGE is deprecated, so this function is no longer
+ * needed
+ */
+attribute_deprecated
+unsigned avcodec_get_edge_width(void);
+#endif
+
+/**
+ * Modify width and height values so that they will result in a memory
+ * buffer that is acceptable for the codec if you do not use any horizontal
+ * padding.
+ *
+ * May only be used if a codec with CODEC_CAP_DR1 has been opened.
+ */
+void avcodec_align_dimensions(AVCodecContext *s, int *width, int *height);
+
+/**
+ * Modify width and height values so that they will result in a memory
+ * buffer that is acceptable for the codec if you also ensure that all
+ * line sizes are a multiple of the respective linesize_align[i].
+ *
+ * May only be used if a codec with CODEC_CAP_DR1 has been opened.
+ */
+void avcodec_align_dimensions2(AVCodecContext *s, int *width, int *height,
+ int linesize_align[AV_NUM_DATA_POINTERS]);
+
+/**
+ * Decode the audio frame of size avpkt->size from avpkt->data into frame.
+ *
+ * Some decoders may support multiple frames in a single AVPacket. Such
+ * decoders would then just decode the first frame and the return value would be
+ * less than the packet size. In this case, avcodec_decode_audio4 has to be
+ * called again with an AVPacket containing the remaining data in order to
+ * decode the second frame, etc... Even if no frames are returned, the packet
+ * needs to be fed to the decoder with remaining data until it is completely
+ * consumed or an error occurs.
+ *
+ * Some decoders (those marked with CODEC_CAP_DELAY) have a delay between input
+ * and output. This means that for some packets they will not immediately
+ * produce decoded output and need to be flushed at the end of decoding to get
+ * all the decoded data. Flushing is done by calling this function with packets
+ * with avpkt->data set to NULL and avpkt->size set to 0 until it stops
+ * returning samples. It is safe to flush even those decoders that are not
+ * marked with CODEC_CAP_DELAY, then no samples will be returned.
+ *
+ * @warning The input buffer, avpkt->data must be FF_INPUT_BUFFER_PADDING_SIZE
+ * larger than the actual read bytes because some optimized bitstream
+ * readers read 32 or 64 bits at once and could read over the end.
+ *
+ * @param avctx the codec context
+ * @param[out] frame The AVFrame in which to store decoded audio samples.
+ * The decoder will allocate a buffer for the decoded frame by
+ * calling the AVCodecContext.get_buffer2() callback.
+ * When AVCodecContext.refcounted_frames is set to 1, the frame is
+ * reference counted and the returned reference belongs to the
+ * caller. The caller must release the frame using av_frame_unref()
+ * when the frame is no longer needed. The caller may safely write
+ * to the frame if av_frame_is_writable() returns 1.
+ * When AVCodecContext.refcounted_frames is set to 0, the returned
+ * reference belongs to the decoder and is valid only until the
+ * next call to this function or until closing or flushing the
+ * decoder. The caller may not write to it.
+ * @param[out] got_frame_ptr Zero if no frame could be decoded, otherwise it is
+ * non-zero. Note that this field being set to zero
+ * does not mean that an error has occurred. For
+ * decoders with CODEC_CAP_DELAY set, no given decode
+ * call is guaranteed to produce a frame.
+ * @param[in] avpkt The input AVPacket containing the input buffer.
+ * At least avpkt->data and avpkt->size should be set. Some
+ * decoders might also require additional fields to be set.
+ * @return A negative error code is returned if an error occurred during
+ * decoding, otherwise the number of bytes consumed from the input
+ * AVPacket is returned.
+ */
+int avcodec_decode_audio4(AVCodecContext *avctx, AVFrame *frame,
+ int *got_frame_ptr, AVPacket *avpkt);
+
+/**
+ * Decode the video frame of size avpkt->size from avpkt->data into picture.
+ * Some decoders may support multiple frames in a single AVPacket, such
+ * decoders would then just decode the first frame.
+ *
+ * @warning The input buffer must be FF_INPUT_BUFFER_PADDING_SIZE larger than
+ * the actual read bytes because some optimized bitstream readers read 32 or 64
+ * bits at once and could read over the end.
+ *
+ * @warning The end of the input buffer buf should be set to 0 to ensure that
+ * no overreading happens for damaged MPEG streams.
+ *
+ * @note Codecs which have the CODEC_CAP_DELAY capability set have a delay
+ * between input and output, these need to be fed with avpkt->data=NULL,
+ * avpkt->size=0 at the end to return the remaining frames.
+ *
+ * @param avctx the codec context
+ * @param[out] picture The AVFrame in which the decoded video frame will be stored.
+ * Use av_frame_alloc() to get an AVFrame. The codec will
+ * allocate memory for the actual bitmap by calling the
+ * AVCodecContext.get_buffer2() callback.
+ * When AVCodecContext.refcounted_frames is set to 1, the frame is
+ * reference counted and the returned reference belongs to the
+ * caller. The caller must release the frame using av_frame_unref()
+ * when the frame is no longer needed. The caller may safely write
+ * to the frame if av_frame_is_writable() returns 1.
+ * When AVCodecContext.refcounted_frames is set to 0, the returned
+ * reference belongs to the decoder and is valid only until the
+ * next call to this function or until closing or flushing the
+ * decoder. The caller may not write to it.
+ *
+ * @param[in] avpkt The input AVpacket containing the input buffer.
+ * You can create such packet with av_init_packet() and by then setting
+ * data and size, some decoders might in addition need other fields like
+ * flags&AV_PKT_FLAG_KEY. All decoders are designed to use the least
+ * fields possible.
+ * @param[in,out] got_picture_ptr Zero if no frame could be decompressed, otherwise, it is nonzero.
+ * @return On error a negative value is returned, otherwise the number of bytes
+ * used or zero if no frame could be decompressed.
+ */
+int avcodec_decode_video2(AVCodecContext *avctx, AVFrame *picture,
+ int *got_picture_ptr,
+ AVPacket *avpkt);
+
+/**
+ * Decode a subtitle message.
+ * Return a negative value on error, otherwise return the number of bytes used.
+ * If no subtitle could be decompressed, got_sub_ptr is zero.
+ * Otherwise, the subtitle is stored in *sub.
+ * Note that CODEC_CAP_DR1 is not available for subtitle codecs. This is for
+ * simplicity, because the performance difference is expect to be negligible
+ * and reusing a get_buffer written for video codecs would probably perform badly
+ * due to a potentially very different allocation pattern.
+ *
+ * @param avctx the codec context
+ * @param[out] sub The AVSubtitle in which the decoded subtitle will be stored, must be
+ freed with avsubtitle_free if *got_sub_ptr is set.
+ * @param[in,out] got_sub_ptr Zero if no subtitle could be decompressed, otherwise, it is nonzero.
+ * @param[in] avpkt The input AVPacket containing the input buffer.
+ */
+int avcodec_decode_subtitle2(AVCodecContext *avctx, AVSubtitle *sub,
+ int *got_sub_ptr,
+ AVPacket *avpkt);
+
+/**
+ * @defgroup lavc_parsing Frame parsing
+ * @{
+ */
+
+enum AVPictureStructure {
+ AV_PICTURE_STRUCTURE_UNKNOWN, //< unknown
+ AV_PICTURE_STRUCTURE_TOP_FIELD, //< coded as top field
+ AV_PICTURE_STRUCTURE_BOTTOM_FIELD, //< coded as bottom field
+ AV_PICTURE_STRUCTURE_FRAME, //< coded as frame
+};
+
+typedef struct AVCodecParserContext {
+ void *priv_data;
+ struct AVCodecParser *parser;
+ int64_t frame_offset; /* offset of the current frame */
+ int64_t cur_offset; /* current offset
+ (incremented by each av_parser_parse()) */
+ int64_t next_frame_offset; /* offset of the next frame */
+ /* video info */
+ int pict_type; /* XXX: Put it back in AVCodecContext. */
+ /**
+ * This field is used for proper frame duration computation in lavf.
+ * It signals, how much longer the frame duration of the current frame
+ * is compared to normal frame duration.
+ *
+ * frame_duration = (1 + repeat_pict) * time_base
+ *
+ * It is used by codecs like H.264 to display telecined material.
+ */
+ int repeat_pict; /* XXX: Put it back in AVCodecContext. */
+ int64_t pts; /* pts of the current frame */
+ int64_t dts; /* dts of the current frame */
+
+ /* private data */
+ int64_t last_pts;
+ int64_t last_dts;
+ int fetch_timestamp;
+
+#define AV_PARSER_PTS_NB 4
+ int cur_frame_start_index;
+ int64_t cur_frame_offset[AV_PARSER_PTS_NB];
+ int64_t cur_frame_pts[AV_PARSER_PTS_NB];
+ int64_t cur_frame_dts[AV_PARSER_PTS_NB];
+
+ int flags;
+#define PARSER_FLAG_COMPLETE_FRAMES 0x0001
+#define PARSER_FLAG_ONCE 0x0002
+/// Set if the parser has a valid file offset
+#define PARSER_FLAG_FETCHED_OFFSET 0x0004
+
+ int64_t offset; ///< byte offset from starting packet start
+ int64_t cur_frame_end[AV_PARSER_PTS_NB];
+
+ /**
+ * Set by parser to 1 for key frames and 0 for non-key frames.
+ * It is initialized to -1, so if the parser doesn't set this flag,
+ * old-style fallback using AV_PICTURE_TYPE_I picture type as key frames
+ * will be used.
+ */
+ int key_frame;
+
+ /**
+ * Time difference in stream time base units from the pts of this
+ * packet to the point at which the output from the decoder has converged
+ * independent from the availability of previous frames. That is, the
+ * frames are virtually identical no matter if decoding started from
+ * the very first frame or from this keyframe.
+ * Is AV_NOPTS_VALUE if unknown.
+ * This field is not the display duration of the current frame.
+ * This field has no meaning if the packet does not have AV_PKT_FLAG_KEY
+ * set.
+ *
+ * The purpose of this field is to allow seeking in streams that have no
+ * keyframes in the conventional sense. It corresponds to the
+ * recovery point SEI in H.264 and match_time_delta in NUT. It is also
+ * essential for some types of subtitle streams to ensure that all
+ * subtitles are correctly displayed after seeking.
+ */
+ int64_t convergence_duration;
+
+ // Timestamp generation support:
+ /**
+ * Synchronization point for start of timestamp generation.
+ *
+ * Set to >0 for sync point, 0 for no sync point and <0 for undefined
+ * (default).
+ *
+ * For example, this corresponds to presence of H.264 buffering period
+ * SEI message.
+ */
+ int dts_sync_point;
+
+ /**
+ * Offset of the current timestamp against last timestamp sync point in
+ * units of AVCodecContext.time_base.
+ *
+ * Set to INT_MIN when dts_sync_point unused. Otherwise, it must
+ * contain a valid timestamp offset.
+ *
+ * Note that the timestamp of sync point has usually a nonzero
+ * dts_ref_dts_delta, which refers to the previous sync point. Offset of
+ * the next frame after timestamp sync point will be usually 1.
+ *
+ * For example, this corresponds to H.264 cpb_removal_delay.
+ */
+ int dts_ref_dts_delta;
+
+ /**
+ * Presentation delay of current frame in units of AVCodecContext.time_base.
+ *
+ * Set to INT_MIN when dts_sync_point unused. Otherwise, it must
+ * contain valid non-negative timestamp delta (presentation time of a frame
+ * must not lie in the past).
+ *
+ * This delay represents the difference between decoding and presentation
+ * time of the frame.
+ *
+ * For example, this corresponds to H.264 dpb_output_delay.
+ */
+ int pts_dts_delta;
+
+ /**
+ * Position of the packet in file.
+ *
+ * Analogous to cur_frame_pts/dts
+ */
+ int64_t cur_frame_pos[AV_PARSER_PTS_NB];
+
+ /**
+ * Byte position of currently parsed frame in stream.
+ */
+ int64_t pos;
+
+ /**
+ * Previous frame byte position.
+ */
+ int64_t last_pos;
+
+ /**
+ * Duration of the current frame.
+ * For audio, this is in units of 1 / AVCodecContext.sample_rate.
+ * For all other types, this is in units of AVCodecContext.time_base.
+ */
+ int duration;
+
+ enum AVFieldOrder field_order;
+
+ /**
+ * Indicate whether a picture is coded as a frame, top field or bottom field.
+ *
+ * For example, H.264 field_pic_flag equal to 0 corresponds to
+ * AV_PICTURE_STRUCTURE_FRAME. An H.264 picture with field_pic_flag
+ * equal to 1 and bottom_field_flag equal to 0 corresponds to
+ * AV_PICTURE_STRUCTURE_TOP_FIELD.
+ */
+ enum AVPictureStructure picture_structure;
+
+ /**
+ * Picture number incremented in presentation or output order.
+ * This field may be reinitialized at the first picture of a new sequence.
+ *
+ * For example, this corresponds to H.264 PicOrderCnt.
+ */
+ int output_picture_number;
+} AVCodecParserContext;
+
+typedef struct AVCodecParser {
+ int codec_ids[5]; /* several codec IDs are permitted */
+ int priv_data_size;
+ int (*parser_init)(AVCodecParserContext *s);
+ int (*parser_parse)(AVCodecParserContext *s,
+ AVCodecContext *avctx,
+ const uint8_t **poutbuf, int *poutbuf_size,
+ const uint8_t *buf, int buf_size);
+ void (*parser_close)(AVCodecParserContext *s);
+ int (*split)(AVCodecContext *avctx, const uint8_t *buf, int buf_size);
+ struct AVCodecParser *next;
+} AVCodecParser;
+
+AVCodecParser *av_parser_next(AVCodecParser *c);
+
+void av_register_codec_parser(AVCodecParser *parser);
+AVCodecParserContext *av_parser_init(int codec_id);
+
+/**
+ * Parse a packet.
+ *
+ * @param s parser context.
+ * @param avctx codec context.
+ * @param poutbuf set to pointer to parsed buffer or NULL if not yet finished.
+ * @param poutbuf_size set to size of parsed buffer or zero if not yet finished.
+ * @param buf input buffer.
+ * @param buf_size input length, to signal EOF, this should be 0 (so that the last frame can be output).
+ * @param pts input presentation timestamp.
+ * @param dts input decoding timestamp.
+ * @param pos input byte position in stream.
+ * @return the number of bytes of the input bitstream used.
+ *
+ * Example:
+ * @code
+ * while(in_len){
+ * len = av_parser_parse2(myparser, AVCodecContext, &data, &size,
+ * in_data, in_len,
+ * pts, dts, pos);
+ * in_data += len;
+ * in_len -= len;
+ *
+ * if(size)
+ * decode_frame(data, size);
+ * }
+ * @endcode
+ */
+int av_parser_parse2(AVCodecParserContext *s,
+ AVCodecContext *avctx,
+ uint8_t **poutbuf, int *poutbuf_size,
+ const uint8_t *buf, int buf_size,
+ int64_t pts, int64_t dts,
+ int64_t pos);
+
+/**
+ * @return 0 if the output buffer is a subset of the input, 1 if it is allocated and must be freed
+ * @deprecated use AVBitstreamFilter
+ */
+int av_parser_change(AVCodecParserContext *s,
+ AVCodecContext *avctx,
+ uint8_t **poutbuf, int *poutbuf_size,
+ const uint8_t *buf, int buf_size, int keyframe);
+void av_parser_close(AVCodecParserContext *s);
+
+/**
+ * @}
+ * @}
+ */
+
+/**
+ * @addtogroup lavc_encoding
+ * @{
+ */
+
+/**
+ * Find a registered encoder with a matching codec ID.
+ *
+ * @param id AVCodecID of the requested encoder
+ * @return An encoder if one was found, NULL otherwise.
+ */
+AVCodec *avcodec_find_encoder(enum AVCodecID id);
+
+/**
+ * Find a registered encoder with the specified name.
+ *
+ * @param name name of the requested encoder
+ * @return An encoder if one was found, NULL otherwise.
+ */
+AVCodec *avcodec_find_encoder_by_name(const char *name);
+
+/**
+ * Encode a frame of audio.
+ *
+ * Takes input samples from frame and writes the next output packet, if
+ * available, to avpkt. The output packet does not necessarily contain data for
+ * the most recent frame, as encoders can delay, split, and combine input frames
+ * internally as needed.
+ *
+ * @param avctx codec context
+ * @param avpkt output AVPacket.
+ * The user can supply an output buffer by setting
+ * avpkt->data and avpkt->size prior to calling the
+ * function, but if the size of the user-provided data is not
+ * large enough, encoding will fail. All other AVPacket fields
+ * will be reset by the encoder using av_init_packet(). If
+ * avpkt->data is NULL, the encoder will allocate it.
+ * The encoder will set avpkt->size to the size of the
+ * output packet.
+ *
+ * If this function fails or produces no output, avpkt will be
+ * freed using av_free_packet() (i.e. avpkt->destruct will be
+ * called to free the user supplied buffer).
+ * @param[in] frame AVFrame containing the raw audio data to be encoded.
+ * May be NULL when flushing an encoder that has the
+ * CODEC_CAP_DELAY capability set.
+ * If CODEC_CAP_VARIABLE_FRAME_SIZE is set, then each frame
+ * can have any number of samples.
+ * If it is not set, frame->nb_samples must be equal to
+ * avctx->frame_size for all frames except the last.
+ * The final frame may be smaller than avctx->frame_size.
+ * @param[out] got_packet_ptr This field is set to 1 by libavcodec if the
+ * output packet is non-empty, and to 0 if it is
+ * empty. If the function returns an error, the
+ * packet can be assumed to be invalid, and the
+ * value of got_packet_ptr is undefined and should
+ * not be used.
+ * @return 0 on success, negative error code on failure
+ */
+int avcodec_encode_audio2(AVCodecContext *avctx, AVPacket *avpkt,
+ const AVFrame *frame, int *got_packet_ptr);
+
+/**
+ * Encode a frame of video.
+ *
+ * Takes input raw video data from frame and writes the next output packet, if
+ * available, to avpkt. The output packet does not necessarily contain data for
+ * the most recent frame, as encoders can delay and reorder input frames
+ * internally as needed.
+ *
+ * @param avctx codec context
+ * @param avpkt output AVPacket.
+ * The user can supply an output buffer by setting
+ * avpkt->data and avpkt->size prior to calling the
+ * function, but if the size of the user-provided data is not
+ * large enough, encoding will fail. All other AVPacket fields
+ * will be reset by the encoder using av_init_packet(). If
+ * avpkt->data is NULL, the encoder will allocate it.
+ * The encoder will set avpkt->size to the size of the
+ * output packet. The returned data (if any) belongs to the
+ * caller, he is responsible for freeing it.
+ *
+ * If this function fails or produces no output, avpkt will be
+ * freed using av_free_packet() (i.e. avpkt->destruct will be
+ * called to free the user supplied buffer).
+ * @param[in] frame AVFrame containing the raw video data to be encoded.
+ * May be NULL when flushing an encoder that has the
+ * CODEC_CAP_DELAY capability set.
+ * @param[out] got_packet_ptr This field is set to 1 by libavcodec if the
+ * output packet is non-empty, and to 0 if it is
+ * empty. If the function returns an error, the
+ * packet can be assumed to be invalid, and the
+ * value of got_packet_ptr is undefined and should
+ * not be used.
+ * @return 0 on success, negative error code on failure
+ */
+int avcodec_encode_video2(AVCodecContext *avctx, AVPacket *avpkt,
+ const AVFrame *frame, int *got_packet_ptr);
+
+int avcodec_encode_subtitle(AVCodecContext *avctx, uint8_t *buf, int buf_size,
+ const AVSubtitle *sub);
+
+
+/**
+ * @}
+ */
+
+/**
+ * @addtogroup lavc_picture
+ * @{
+ */
+
+/**
+ * Allocate memory for a picture. Call avpicture_free() to free it.
+ *
+ * @see avpicture_fill()
+ *
+ * @param picture the picture to be filled in
+ * @param pix_fmt the format of the picture
+ * @param width the width of the picture
+ * @param height the height of the picture
+ * @return zero if successful, a negative value if not
+ */
+int avpicture_alloc(AVPicture *picture, enum AVPixelFormat pix_fmt, int width, int height);
+
+/**
+ * Free a picture previously allocated by avpicture_alloc().
+ * The data buffer used by the AVPicture is freed, but the AVPicture structure
+ * itself is not.
+ *
+ * @param picture the AVPicture to be freed
+ */
+void avpicture_free(AVPicture *picture);
+
+/**
+ * Fill in the AVPicture fields.
+ * The fields of the given AVPicture are filled in by using the 'ptr' address
+ * which points to the image data buffer. Depending on the specified picture
+ * format, one or multiple image data pointers and line sizes will be set.
+ * If a planar format is specified, several pointers will be set pointing to
+ * the different picture planes and the line sizes of the different planes
+ * will be stored in the lines_sizes array.
+ * Call with ptr == NULL to get the required size for the ptr buffer.
+ *
+ * To allocate the buffer and fill in the AVPicture fields in one call,
+ * use avpicture_alloc().
+ *
+ * @param picture AVPicture whose fields are to be filled in
+ * @param ptr Buffer which will contain or contains the actual image data
+ * @param pix_fmt The format in which the picture data is stored.
+ * @param width the width of the image in pixels
+ * @param height the height of the image in pixels
+ * @return size of the image data in bytes
+ */
+int avpicture_fill(AVPicture *picture, uint8_t *ptr,
+ enum AVPixelFormat pix_fmt, int width, int height);
+
+/**
+ * Copy pixel data from an AVPicture into a buffer.
+ * The data is stored compactly, without any gaps for alignment or padding
+ * which may be applied by avpicture_fill().
+ *
+ * @see avpicture_get_size()
+ *
+ * @param[in] src AVPicture containing image data
+ * @param[in] pix_fmt The format in which the picture data is stored.
+ * @param[in] width the width of the image in pixels.
+ * @param[in] height the height of the image in pixels.
+ * @param[out] dest A buffer into which picture data will be copied.
+ * @param[in] dest_size The size of 'dest'.
+ * @return The number of bytes written to dest, or a negative value (error code) on error.
+ */
+int avpicture_layout(const AVPicture* src, enum AVPixelFormat pix_fmt,
+ int width, int height,
+ unsigned char *dest, int dest_size);
+
+/**
+ * Calculate the size in bytes that a picture of the given width and height
+ * would occupy if stored in the given picture format.
+ * Note that this returns the size of a compact representation as generated
+ * by avpicture_layout(), which can be smaller than the size required for e.g.
+ * avpicture_fill().
+ *
+ * @param pix_fmt the given picture format
+ * @param width the width of the image
+ * @param height the height of the image
+ * @return Image data size in bytes or -1 on error (e.g. too large dimensions).
+ */
+int avpicture_get_size(enum AVPixelFormat pix_fmt, int width, int height);
+
+#if FF_API_DEINTERLACE
+/**
+ * deinterlace - if not supported return -1
+ *
+ * @deprecated - use yadif (in libavfilter) instead
+ */
+attribute_deprecated
+int avpicture_deinterlace(AVPicture *dst, const AVPicture *src,
+ enum AVPixelFormat pix_fmt, int width, int height);
+#endif
+/**
+ * Copy image src to dst. Wraps av_picture_data_copy() above.
+ */
+void av_picture_copy(AVPicture *dst, const AVPicture *src,
+ enum AVPixelFormat pix_fmt, int width, int height);
+
+/**
+ * Crop image top and left side.
+ */
+int av_picture_crop(AVPicture *dst, const AVPicture *src,
+ enum AVPixelFormat pix_fmt, int top_band, int left_band);
+
+/**
+ * Pad image.
+ */
+int av_picture_pad(AVPicture *dst, const AVPicture *src, int height, int width, enum AVPixelFormat pix_fmt,
+ int padtop, int padbottom, int padleft, int padright, int *color);
+
+/**
+ * @}
+ */
+
+/**
+ * @defgroup lavc_misc Utility functions
+ * @ingroup libavc
+ *
+ * Miscellaneous utility functions related to both encoding and decoding
+ * (or neither).
+ * @{
+ */
+
+/**
+ * @defgroup lavc_misc_pixfmt Pixel formats
+ *
+ * Functions for working with pixel formats.
+ * @{
+ */
+
+/**
+ * @deprecated Use av_pix_fmt_get_chroma_sub_sample
+ */
+
+void attribute_deprecated avcodec_get_chroma_sub_sample(enum AVPixelFormat pix_fmt, int *h_shift, int *v_shift);
+
+/**
+ * Return a value representing the fourCC code associated to the
+ * pixel format pix_fmt, or 0 if no associated fourCC code can be
+ * found.
+ */
+unsigned int avcodec_pix_fmt_to_codec_tag(enum AVPixelFormat pix_fmt);
+
+#define FF_LOSS_RESOLUTION 0x0001 /**< loss due to resolution change */
+#define FF_LOSS_DEPTH 0x0002 /**< loss due to color depth change */
+#define FF_LOSS_COLORSPACE 0x0004 /**< loss due to color space conversion */
+#define FF_LOSS_ALPHA 0x0008 /**< loss of alpha bits */
+#define FF_LOSS_COLORQUANT 0x0010 /**< loss due to color quantization */
+#define FF_LOSS_CHROMA 0x0020 /**< loss of chroma (e.g. RGB to gray conversion) */
+
+/**
+ * Compute what kind of losses will occur when converting from one specific
+ * pixel format to another.
+ * When converting from one pixel format to another, information loss may occur.
+ * For example, when converting from RGB24 to GRAY, the color information will
+ * be lost. Similarly, other losses occur when converting from some formats to
+ * other formats. These losses can involve loss of chroma, but also loss of
+ * resolution, loss of color depth, loss due to the color space conversion, loss
+ * of the alpha bits or loss due to color quantization.
+ * avcodec_get_fix_fmt_loss() informs you about the various types of losses
+ * which will occur when converting from one pixel format to another.
+ *
+ * @param[in] dst_pix_fmt destination pixel format
+ * @param[in] src_pix_fmt source pixel format
+ * @param[in] has_alpha Whether the source pixel format alpha channel is used.
+ * @return Combination of flags informing you what kind of losses will occur.
+ */
+int avcodec_get_pix_fmt_loss(enum AVPixelFormat dst_pix_fmt, enum AVPixelFormat src_pix_fmt,
+ int has_alpha);
+
+/**
+ * Find the best pixel format to convert to given a certain source pixel
+ * format. When converting from one pixel format to another, information loss
+ * may occur. For example, when converting from RGB24 to GRAY, the color
+ * information will be lost. Similarly, other losses occur when converting from
+ * some formats to other formats. avcodec_find_best_pix_fmt2() searches which of
+ * the given pixel formats should be used to suffer the least amount of loss.
+ * The pixel formats from which it chooses one, are determined by the
+ * pix_fmt_list parameter.
+ *
+ *
+ * @param[in] pix_fmt_list AV_PIX_FMT_NONE terminated array of pixel formats to choose from
+ * @param[in] src_pix_fmt source pixel format
+ * @param[in] has_alpha Whether the source pixel format alpha channel is used.
+ * @param[out] loss_ptr Combination of flags informing you what kind of losses will occur.
+ * @return The best pixel format to convert to or -1 if none was found.
+ */
+enum AVPixelFormat avcodec_find_best_pix_fmt2(enum AVPixelFormat *pix_fmt_list,
+ enum AVPixelFormat src_pix_fmt,
+ int has_alpha, int *loss_ptr);
+
+enum AVPixelFormat avcodec_default_get_format(struct AVCodecContext *s, const enum AVPixelFormat * fmt);
+
+/**
+ * @}
+ */
+
+#if FF_API_SET_DIMENSIONS
+/**
+ * @deprecated this function is not supposed to be used from outside of lavc
+ */
+attribute_deprecated
+void avcodec_set_dimensions(AVCodecContext *s, int width, int height);
+#endif
+
+/**
+ * Put a string representing the codec tag codec_tag in buf.
+ *
+ * @param buf buffer to place codec tag in
+ * @param buf_size size in bytes of buf
+ * @param codec_tag codec tag to assign
+ * @return the length of the string that would have been generated if
+ * enough space had been available, excluding the trailing null
+ */
+size_t av_get_codec_tag_string(char *buf, size_t buf_size, unsigned int codec_tag);
+
+void avcodec_string(char *buf, int buf_size, AVCodecContext *enc, int encode);
+
+/**
+ * Return a name for the specified profile, if available.
+ *
+ * @param codec the codec that is searched for the given profile
+ * @param profile the profile value for which a name is requested
+ * @return A name for the profile if found, NULL otherwise.
+ */
+const char *av_get_profile_name(const AVCodec *codec, int profile);
+
+int avcodec_default_execute(AVCodecContext *c, int (*func)(AVCodecContext *c2, void *arg2),void *arg, int *ret, int count, int size);
+int avcodec_default_execute2(AVCodecContext *c, int (*func)(AVCodecContext *c2, void *arg2, int, int),void *arg, int *ret, int count);
+//FIXME func typedef
+
+/**
+ * Fill audio frame data and linesize.
+ * AVFrame extended_data channel pointers are allocated if necessary for
+ * planar audio.
+ *
+ * @param frame the AVFrame
+ * frame->nb_samples must be set prior to calling the
+ * function. This function fills in frame->data,
+ * frame->extended_data, frame->linesize[0].
+ * @param nb_channels channel count
+ * @param sample_fmt sample format
+ * @param buf buffer to use for frame data
+ * @param buf_size size of buffer
+ * @param align plane size sample alignment (0 = default)
+ * @return 0 on success, negative error code on failure
+ */
+int avcodec_fill_audio_frame(AVFrame *frame, int nb_channels,
+ enum AVSampleFormat sample_fmt, const uint8_t *buf,
+ int buf_size, int align);
+
+/**
+ * Reset the internal decoder state / flush internal buffers. Should be called
+ * e.g. when seeking or when switching to a different stream.
+ *
+ * @note when refcounted frames are not used (i.e. avctx->refcounted_frames is 0),
+ * this invalidates the frames previously returned from the decoder. When
+ * refcounted frames are used, the decoder just releases any references it might
+ * keep internally, but the caller's reference remains valid.
+ */
+void avcodec_flush_buffers(AVCodecContext *avctx);
+
+/**
+ * Return codec bits per sample.
+ *
+ * @param[in] codec_id the codec
+ * @return Number of bits per sample or zero if unknown for the given codec.
+ */
+int av_get_bits_per_sample(enum AVCodecID codec_id);
+
+/**
+ * Return codec bits per sample.
+ * Only return non-zero if the bits per sample is exactly correct, not an
+ * approximation.
+ *
+ * @param[in] codec_id the codec
+ * @return Number of bits per sample or zero if unknown for the given codec.
+ */
+int av_get_exact_bits_per_sample(enum AVCodecID codec_id);
+
+/**
+ * Return audio frame duration.
+ *
+ * @param avctx codec context
+ * @param frame_bytes size of the frame, or 0 if unknown
+ * @return frame duration, in samples, if known. 0 if not able to
+ * determine.
+ */
+int av_get_audio_frame_duration(AVCodecContext *avctx, int frame_bytes);
+
+
+typedef struct AVBitStreamFilterContext {
+ void *priv_data;
+ struct AVBitStreamFilter *filter;
+ AVCodecParserContext *parser;
+ struct AVBitStreamFilterContext *next;
+} AVBitStreamFilterContext;
+
+
+typedef struct AVBitStreamFilter {
+ const char *name;
+ int priv_data_size;
+ int (*filter)(AVBitStreamFilterContext *bsfc,
+ AVCodecContext *avctx, const char *args,
+ uint8_t **poutbuf, int *poutbuf_size,
+ const uint8_t *buf, int buf_size, int keyframe);
+ void (*close)(AVBitStreamFilterContext *bsfc);
+ struct AVBitStreamFilter *next;
+} AVBitStreamFilter;
+
+void av_register_bitstream_filter(AVBitStreamFilter *bsf);
+AVBitStreamFilterContext *av_bitstream_filter_init(const char *name);
+int av_bitstream_filter_filter(AVBitStreamFilterContext *bsfc,
+ AVCodecContext *avctx, const char *args,
+ uint8_t **poutbuf, int *poutbuf_size,
+ const uint8_t *buf, int buf_size, int keyframe);
+void av_bitstream_filter_close(AVBitStreamFilterContext *bsf);
+
+AVBitStreamFilter *av_bitstream_filter_next(AVBitStreamFilter *f);
+
+/* memory */
+
+/**
+ * Allocate a buffer with padding, reusing the given one if large enough.
+ *
+ * Same behaviour av_fast_malloc but the buffer has additional
+ * FF_INPUT_PADDING_SIZE at the end which will always memset to 0.
+ *
+ */
+void av_fast_padded_malloc(void *ptr, unsigned int *size, size_t min_size);
+
+/**
+ * Encode extradata length to a buffer. Used by xiph codecs.
+ *
+ * @param s buffer to write to; must be at least (v/255+1) bytes long
+ * @param v size of extradata in bytes
+ * @return number of bytes written to the buffer.
+ */
+unsigned int av_xiphlacing(unsigned char *s, unsigned int v);
+
+#if FF_API_MISSING_SAMPLE
+/**
+ * Log a generic warning message about a missing feature. This function is
+ * intended to be used internally by Libav (libavcodec, libavformat, etc.)
+ * only, and would normally not be used by applications.
+ * @param[in] avc a pointer to an arbitrary struct of which the first field is
+ * a pointer to an AVClass struct
+ * @param[in] feature string containing the name of the missing feature
+ * @param[in] want_sample indicates if samples are wanted which exhibit this feature.
+ * If want_sample is non-zero, additional verbage will be added to the log
+ * message which tells the user how to report samples to the development
+ * mailing list.
+ * @deprecated Use avpriv_report_missing_feature() instead.
+ */
+attribute_deprecated
+void av_log_missing_feature(void *avc, const char *feature, int want_sample);
+
+/**
+ * Log a generic warning message asking for a sample. This function is
+ * intended to be used internally by Libav (libavcodec, libavformat, etc.)
+ * only, and would normally not be used by applications.
+ * @param[in] avc a pointer to an arbitrary struct of which the first field is
+ * a pointer to an AVClass struct
+ * @param[in] msg string containing an optional message, or NULL if no message
+ * @deprecated Use avpriv_request_sample() instead.
+ */
+attribute_deprecated
+void av_log_ask_for_sample(void *avc, const char *msg, ...) av_printf_format(2, 3);
+#endif /* FF_API_MISSING_SAMPLE */
+
+/**
+ * Register the hardware accelerator hwaccel.
+ */
+void av_register_hwaccel(AVHWAccel *hwaccel);
+
+/**
+ * If hwaccel is NULL, returns the first registered hardware accelerator,
+ * if hwaccel is non-NULL, returns the next registered hardware accelerator
+ * after hwaccel, or NULL if hwaccel is the last one.
+ */
+AVHWAccel *av_hwaccel_next(AVHWAccel *hwaccel);
+
+
+/**
+ * Lock operation used by lockmgr
+ */
+enum AVLockOp {
+ AV_LOCK_CREATE, ///< Create a mutex
+ AV_LOCK_OBTAIN, ///< Lock the mutex
+ AV_LOCK_RELEASE, ///< Unlock the mutex
+ AV_LOCK_DESTROY, ///< Free mutex resources
+};
+
+/**
+ * Register a user provided lock manager supporting the operations
+ * specified by AVLockOp. mutex points to a (void *) where the
+ * lockmgr should store/get a pointer to a user allocated mutex. It's
+ * NULL upon AV_LOCK_CREATE and != NULL for all other ops.
+ *
+ * @param cb User defined callback. Note: Libav may invoke calls to this
+ * callback during the call to av_lockmgr_register().
+ * Thus, the application must be prepared to handle that.
+ * If cb is set to NULL the lockmgr will be unregistered.
+ * Also note that during unregistration the previously registered
+ * lockmgr callback may also be invoked.
+ */
+int av_lockmgr_register(int (*cb)(void **mutex, enum AVLockOp op));
+
+/**
+ * Get the type of the given codec.
+ */
+enum AVMediaType avcodec_get_type(enum AVCodecID codec_id);
+
+/**
+ * @return a positive value if s is open (i.e. avcodec_open2() was called on it
+ * with no corresponding avcodec_close()), 0 otherwise.
+ */
+int avcodec_is_open(AVCodecContext *s);
+
+/**
+ * @return a non-zero number if codec is an encoder, zero otherwise
+ */
+int av_codec_is_encoder(const AVCodec *codec);
+
+/**
+ * @return a non-zero number if codec is a decoder, zero otherwise
+ */
+int av_codec_is_decoder(const AVCodec *codec);
+
+/**
+ * @return descriptor for given codec ID or NULL if no descriptor exists.
+ */
+const AVCodecDescriptor *avcodec_descriptor_get(enum AVCodecID id);
+
+/**
+ * Iterate over all codec descriptors known to libavcodec.
+ *
+ * @param prev previous descriptor. NULL to get the first descriptor.
+ *
+ * @return next descriptor or NULL after the last descriptor
+ */
+const AVCodecDescriptor *avcodec_descriptor_next(const AVCodecDescriptor *prev);
+
+/**
+ * @return codec descriptor with the given name or NULL if no such descriptor
+ * exists.
+ */
+const AVCodecDescriptor *avcodec_descriptor_get_by_name(const char *name);
+
+/**
+ * @}
+ */
+
+#endif /* AVCODEC_AVCODEC_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavcodec/avfft.h b/dom/media/platforms/ffmpeg/libav55/include/libavcodec/avfft.h
new file mode 100644
index 0000000000..e2e727da9e
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavcodec/avfft.h
@@ -0,0 +1,118 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_AVFFT_H
+#define AVCODEC_AVFFT_H
+
+/**
+ * @file
+ * @ingroup lavc_fft
+ * FFT functions
+ */
+
+/**
+ * @defgroup lavc_fft FFT functions
+ * @ingroup lavc_misc
+ *
+ * @{
+ */
+
+typedef float FFTSample;
+
+typedef struct FFTComplex {
+ FFTSample re, im;
+} FFTComplex;
+
+typedef struct FFTContext FFTContext;
+
+/**
+ * Set up a complex FFT.
+ * @param nbits log2 of the length of the input array
+ * @param inverse if 0 perform the forward transform, if 1 perform the inverse
+ */
+FFTContext *av_fft_init(int nbits, int inverse);
+
+/**
+ * Do the permutation needed BEFORE calling ff_fft_calc().
+ */
+void av_fft_permute(FFTContext *s, FFTComplex *z);
+
+/**
+ * Do a complex FFT with the parameters defined in av_fft_init(). The
+ * input data must be permuted before. No 1.0/sqrt(n) normalization is done.
+ */
+void av_fft_calc(FFTContext *s, FFTComplex *z);
+
+void av_fft_end(FFTContext *s);
+
+FFTContext *av_mdct_init(int nbits, int inverse, double scale);
+void av_imdct_calc(FFTContext *s, FFTSample *output, const FFTSample *input);
+void av_imdct_half(FFTContext *s, FFTSample *output, const FFTSample *input);
+void av_mdct_calc(FFTContext *s, FFTSample *output, const FFTSample *input);
+void av_mdct_end(FFTContext *s);
+
+/* Real Discrete Fourier Transform */
+
+enum RDFTransformType {
+ DFT_R2C,
+ IDFT_C2R,
+ IDFT_R2C,
+ DFT_C2R,
+};
+
+typedef struct RDFTContext RDFTContext;
+
+/**
+ * Set up a real FFT.
+ * @param nbits log2 of the length of the input array
+ * @param trans the type of transform
+ */
+RDFTContext *av_rdft_init(int nbits, enum RDFTransformType trans);
+void av_rdft_calc(RDFTContext *s, FFTSample *data);
+void av_rdft_end(RDFTContext *s);
+
+/* Discrete Cosine Transform */
+
+typedef struct DCTContext DCTContext;
+
+enum DCTTransformType {
+ DCT_II = 0,
+ DCT_III,
+ DCT_I,
+ DST_I,
+};
+
+/**
+ * Set up DCT.
+ *
+ * @param nbits size of the input array:
+ * (1 << nbits) for DCT-II, DCT-III and DST-I
+ * (1 << nbits) + 1 for DCT-I
+ * @param type the type of transform
+ *
+ * @note the first element of the input of DST-I is ignored
+ */
+DCTContext *av_dct_init(int nbits, enum DCTTransformType type);
+void av_dct_calc(DCTContext *s, FFTSample *data);
+void av_dct_end (DCTContext *s);
+
+/**
+ * @}
+ */
+
+#endif /* AVCODEC_AVFFT_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavcodec/dxva2.h b/dom/media/platforms/ffmpeg/libav55/include/libavcodec/dxva2.h
new file mode 100644
index 0000000000..d161eb9f5e
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavcodec/dxva2.h
@@ -0,0 +1,88 @@
+/*
+ * DXVA2 HW acceleration
+ *
+ * copyright (c) 2009 Laurent Aimar
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_DXVA_H
+#define AVCODEC_DXVA_H
+
+/**
+ * @file
+ * @ingroup lavc_codec_hwaccel_dxva2
+ * Public libavcodec DXVA2 header.
+ */
+
+#define _WIN32_WINNT 0x0600
+#include <stdint.h>
+#include <d3d9.h>
+#include <dxva2api.h>
+
+/**
+ * @defgroup lavc_codec_hwaccel_dxva2 DXVA2
+ * @ingroup lavc_codec_hwaccel
+ *
+ * @{
+ */
+
+#define FF_DXVA2_WORKAROUND_SCALING_LIST_ZIGZAG 1 ///< Work around for DXVA2 and old UVD/UVD+ ATI video cards
+
+/**
+ * This structure is used to provides the necessary configurations and data
+ * to the DXVA2 Libav HWAccel implementation.
+ *
+ * The application must make it available as AVCodecContext.hwaccel_context.
+ */
+struct dxva_context {
+ /**
+ * DXVA2 decoder object
+ */
+ IDirectXVideoDecoder *decoder;
+
+ /**
+ * DXVA2 configuration used to create the decoder
+ */
+ const DXVA2_ConfigPictureDecode *cfg;
+
+ /**
+ * The number of surface in the surface array
+ */
+ unsigned surface_count;
+
+ /**
+ * The array of Direct3D surfaces used to create the decoder
+ */
+ LPDIRECT3DSURFACE9 *surface;
+
+ /**
+ * A bit field configuring the workarounds needed for using the decoder
+ */
+ uint64_t workaround;
+
+ /**
+ * Private to the Libav AVHWAccel implementation
+ */
+ unsigned report_id;
+};
+
+/**
+ * @}
+ */
+
+#endif /* AVCODEC_DXVA_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavcodec/vaapi.h b/dom/media/platforms/ffmpeg/libav55/include/libavcodec/vaapi.h
new file mode 100644
index 0000000000..39e88259d6
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavcodec/vaapi.h
@@ -0,0 +1,173 @@
+/*
+ * Video Acceleration API (shared data between Libav and the video player)
+ * HW decode acceleration for MPEG-2, MPEG-4, H.264 and VC-1
+ *
+ * Copyright (C) 2008-2009 Splitted-Desktop Systems
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_VAAPI_H
+#define AVCODEC_VAAPI_H
+
+/**
+ * @file
+ * @ingroup lavc_codec_hwaccel_vaapi
+ * Public libavcodec VA API header.
+ */
+
+#include <stdint.h>
+
+/**
+ * @defgroup lavc_codec_hwaccel_vaapi VA API Decoding
+ * @ingroup lavc_codec_hwaccel
+ * @{
+ */
+
+/**
+ * This structure is used to share data between the Libav library and
+ * the client video application.
+ * This shall be zero-allocated and available as
+ * AVCodecContext.hwaccel_context. All user members can be set once
+ * during initialization or through each AVCodecContext.get_buffer()
+ * function call. In any case, they must be valid prior to calling
+ * decoding functions.
+ */
+struct vaapi_context {
+ /**
+ * Window system dependent data
+ *
+ * - encoding: unused
+ * - decoding: Set by user
+ */
+ void *display;
+
+ /**
+ * Configuration ID
+ *
+ * - encoding: unused
+ * - decoding: Set by user
+ */
+ uint32_t config_id;
+
+ /**
+ * Context ID (video decode pipeline)
+ *
+ * - encoding: unused
+ * - decoding: Set by user
+ */
+ uint32_t context_id;
+
+ /**
+ * VAPictureParameterBuffer ID
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ uint32_t pic_param_buf_id;
+
+ /**
+ * VAIQMatrixBuffer ID
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ uint32_t iq_matrix_buf_id;
+
+ /**
+ * VABitPlaneBuffer ID (for VC-1 decoding)
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ uint32_t bitplane_buf_id;
+
+ /**
+ * Slice parameter/data buffer IDs
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ uint32_t *slice_buf_ids;
+
+ /**
+ * Number of effective slice buffer IDs to send to the HW
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ unsigned int n_slice_buf_ids;
+
+ /**
+ * Size of pre-allocated slice_buf_ids
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ unsigned int slice_buf_ids_alloc;
+
+ /**
+ * Pointer to VASliceParameterBuffers
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ void *slice_params;
+
+ /**
+ * Size of a VASliceParameterBuffer element
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ unsigned int slice_param_size;
+
+ /**
+ * Size of pre-allocated slice_params
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ unsigned int slice_params_alloc;
+
+ /**
+ * Number of slices currently filled in
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ unsigned int slice_count;
+
+ /**
+ * Pointer to slice data buffer base
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ const uint8_t *slice_data;
+
+ /**
+ * Current size of slice data
+ *
+ * - encoding: unused
+ * - decoding: Set by libavcodec
+ */
+ uint32_t slice_data_size;
+};
+
+/* @} */
+
+#endif /* AVCODEC_VAAPI_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavcodec/vda.h b/dom/media/platforms/ffmpeg/libav55/include/libavcodec/vda.h
new file mode 100644
index 0000000000..987b94f1fa
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavcodec/vda.h
@@ -0,0 +1,142 @@
+/*
+ * VDA HW acceleration
+ *
+ * copyright (c) 2011 Sebastien Zwickert
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_VDA_H
+#define AVCODEC_VDA_H
+
+/**
+ * @file
+ * @ingroup lavc_codec_hwaccel_vda
+ * Public libavcodec VDA header.
+ */
+
+#include "libavcodec/version.h"
+
+#include <stdint.h>
+
+// emmintrin.h is unable to compile with -std=c99 -Werror=missing-prototypes
+// http://openradar.appspot.com/8026390
+#undef __GNUC_STDC_INLINE__
+
+#define Picture QuickdrawPicture
+#include <VideoDecodeAcceleration/VDADecoder.h>
+#undef Picture
+
+/**
+ * @defgroup lavc_codec_hwaccel_vda VDA
+ * @ingroup lavc_codec_hwaccel
+ *
+ * @{
+ */
+
+/**
+ * This structure is used to provide the necessary configurations and data
+ * to the VDA Libav HWAccel implementation.
+ *
+ * The application must make it available as AVCodecContext.hwaccel_context.
+ */
+struct vda_context {
+ /**
+ * VDA decoder object.
+ *
+ * - encoding: unused
+ * - decoding: Set/Unset by libavcodec.
+ */
+ VDADecoder decoder;
+
+ /**
+ * The Core Video pixel buffer that contains the current image data.
+ *
+ * encoding: unused
+ * decoding: Set by libavcodec. Unset by user.
+ */
+ CVPixelBufferRef cv_buffer;
+
+ /**
+ * Use the hardware decoder in synchronous mode.
+ *
+ * encoding: unused
+ * decoding: Set by user.
+ */
+ int use_sync_decoding;
+
+ /**
+ * The frame width.
+ *
+ * - encoding: unused
+ * - decoding: Set/Unset by user.
+ */
+ int width;
+
+ /**
+ * The frame height.
+ *
+ * - encoding: unused
+ * - decoding: Set/Unset by user.
+ */
+ int height;
+
+ /**
+ * The frame format.
+ *
+ * - encoding: unused
+ * - decoding: Set/Unset by user.
+ */
+ int format;
+
+ /**
+ * The pixel format for output image buffers.
+ *
+ * - encoding: unused
+ * - decoding: Set/Unset by user.
+ */
+ OSType cv_pix_fmt_type;
+
+ /**
+ * The current bitstream buffer.
+ */
+ uint8_t *priv_bitstream;
+
+ /**
+ * The current size of the bitstream.
+ */
+ int priv_bitstream_size;
+
+ /**
+ * The reference size used for fast reallocation.
+ */
+ int priv_allocated_size;
+};
+
+/** Create the video decoder. */
+int ff_vda_create_decoder(struct vda_context *vda_ctx,
+ uint8_t *extradata,
+ int extradata_size);
+
+/** Destroy the video decoder. */
+int ff_vda_destroy_decoder(struct vda_context *vda_ctx);
+
+/**
+ * @}
+ */
+
+#endif /* AVCODEC_VDA_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavcodec/vdpau.h b/dom/media/platforms/ffmpeg/libav55/include/libavcodec/vdpau.h
new file mode 100644
index 0000000000..75cb1bf7a3
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavcodec/vdpau.h
@@ -0,0 +1,189 @@
+/*
+ * The Video Decode and Presentation API for UNIX (VDPAU) is used for
+ * hardware-accelerated decoding of MPEG-1/2, H.264 and VC-1.
+ *
+ * Copyright (C) 2008 NVIDIA
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_VDPAU_H
+#define AVCODEC_VDPAU_H
+
+/**
+ * @file
+ * @ingroup lavc_codec_hwaccel_vdpau
+ * Public libavcodec VDPAU header.
+ */
+
+
+/**
+ * @defgroup lavc_codec_hwaccel_vdpau VDPAU Decoder and Renderer
+ * @ingroup lavc_codec_hwaccel
+ *
+ * VDPAU hardware acceleration has two modules
+ * - VDPAU decoding
+ * - VDPAU presentation
+ *
+ * The VDPAU decoding module parses all headers using Libav
+ * parsing mechanisms and uses VDPAU for the actual decoding.
+ *
+ * As per the current implementation, the actual decoding
+ * and rendering (API calls) are done as part of the VDPAU
+ * presentation (vo_vdpau.c) module.
+ *
+ * @{
+ */
+
+#include <vdpau/vdpau.h>
+#include <vdpau/vdpau_x11.h>
+
+#include "libavutil/attributes.h"
+
+#include "avcodec.h"
+#include "version.h"
+
+#if FF_API_BUFS_VDPAU
+union AVVDPAUPictureInfo {
+ VdpPictureInfoH264 h264;
+ VdpPictureInfoMPEG1Or2 mpeg;
+ VdpPictureInfoVC1 vc1;
+ VdpPictureInfoMPEG4Part2 mpeg4;
+};
+#endif
+
+/**
+ * This structure is used to share data between the libavcodec library and
+ * the client video application.
+ * The user shall zero-allocate the structure and make it available as
+ * AVCodecContext.hwaccel_context. Members can be set by the user once
+ * during initialization or through each AVCodecContext.get_buffer()
+ * function call. In any case, they must be valid prior to calling
+ * decoding functions.
+ *
+ * The size of this structure is not a part of the public ABI and must not
+ * be used outside of libavcodec. Use av_vdpau_alloc_context() to allocate an
+ * AVVDPAUContext.
+ */
+typedef struct AVVDPAUContext {
+ /**
+ * VDPAU decoder handle
+ *
+ * Set by user.
+ */
+ VdpDecoder decoder;
+
+ /**
+ * VDPAU decoder render callback
+ *
+ * Set by the user.
+ */
+ VdpDecoderRender *render;
+
+#if FF_API_BUFS_VDPAU
+ /**
+ * VDPAU picture information
+ *
+ * Set by libavcodec.
+ */
+ attribute_deprecated
+ union AVVDPAUPictureInfo info;
+
+ /**
+ * Allocated size of the bitstream_buffers table.
+ *
+ * Set by libavcodec.
+ */
+ attribute_deprecated
+ int bitstream_buffers_allocated;
+
+ /**
+ * Useful bitstream buffers in the bitstream buffers table.
+ *
+ * Set by libavcodec.
+ */
+ attribute_deprecated
+ int bitstream_buffers_used;
+
+ /**
+ * Table of bitstream buffers.
+ * The user is responsible for freeing this buffer using av_freep().
+ *
+ * Set by libavcodec.
+ */
+ attribute_deprecated
+ VdpBitstreamBuffer *bitstream_buffers;
+#endif
+} AVVDPAUContext;
+
+/**
+ * Allocate an AVVDPAUContext.
+ *
+ * @return Newly-allocated AVVDPAUContext or NULL on failure.
+ */
+AVVDPAUContext *av_vdpau_alloc_context(void);
+
+/**
+ * Get a decoder profile that should be used for initializing a VDPAU decoder.
+ * Should be called from the AVCodecContext.get_format() callback.
+ *
+ * @param avctx the codec context being used for decoding the stream
+ * @param profile a pointer into which the result will be written on success.
+ * The contents of profile are undefined if this function returns
+ * an error.
+ *
+ * @return 0 on success (non-negative), a negative AVERROR on failure.
+ */
+int av_vdpau_get_profile(AVCodecContext *avctx, VdpDecoderProfile *profile);
+
+#if FF_API_CAP_VDPAU
+/** @brief The videoSurface is used for rendering. */
+#define FF_VDPAU_STATE_USED_FOR_RENDER 1
+
+/**
+ * @brief The videoSurface is needed for reference/prediction.
+ * The codec manipulates this.
+ */
+#define FF_VDPAU_STATE_USED_FOR_REFERENCE 2
+
+/**
+ * @brief This structure is used as a callback between the Libav
+ * decoder (vd_) and presentation (vo_) module.
+ * This is used for defining a video frame containing surface,
+ * picture parameter, bitstream information etc which are passed
+ * between the Libav decoder and its clients.
+ */
+struct vdpau_render_state {
+ VdpVideoSurface surface; ///< Used as rendered surface, never changed.
+
+ int state; ///< Holds FF_VDPAU_STATE_* values.
+
+ /** picture parameter information for all supported codecs */
+ union AVVDPAUPictureInfo info;
+
+ /** Describe size/location of the compressed video data.
+ Set to 0 when freeing bitstream_buffers. */
+ int bitstream_buffers_allocated;
+ int bitstream_buffers_used;
+ /** The user is responsible for freeing this buffer using av_freep(). */
+ VdpBitstreamBuffer *bitstream_buffers;
+};
+#endif
+
+/* @}*/
+
+#endif /* AVCODEC_VDPAU_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavcodec/version.h b/dom/media/platforms/ffmpeg/libav55/include/libavcodec/version.h
new file mode 100644
index 0000000000..cdd5a613d5
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavcodec/version.h
@@ -0,0 +1,127 @@
+/*
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_VERSION_H
+#define AVCODEC_VERSION_H
+
+/**
+ * @file
+ * @ingroup libavc
+ * Libavcodec version macros.
+ */
+
+#include "libavutil/version.h"
+
+#define LIBAVCODEC_VERSION_MAJOR 55
+#define LIBAVCODEC_VERSION_MINOR 34
+#define LIBAVCODEC_VERSION_MICRO 1
+
+#define LIBAVCODEC_VERSION_INT AV_VERSION_INT(LIBAVCODEC_VERSION_MAJOR, \
+ LIBAVCODEC_VERSION_MINOR, \
+ LIBAVCODEC_VERSION_MICRO)
+#define LIBAVCODEC_VERSION AV_VERSION(LIBAVCODEC_VERSION_MAJOR, \
+ LIBAVCODEC_VERSION_MINOR, \
+ LIBAVCODEC_VERSION_MICRO)
+#define LIBAVCODEC_BUILD LIBAVCODEC_VERSION_INT
+
+#define LIBAVCODEC_IDENT "Lavc" AV_STRINGIFY(LIBAVCODEC_VERSION)
+
+/**
+ * FF_API_* defines may be placed below to indicate public API that will be
+ * dropped at a future version bump. The defines themselves are not part of
+ * the public API and may change, break or disappear at any time.
+ */
+
+#ifndef FF_API_REQUEST_CHANNELS
+#define FF_API_REQUEST_CHANNELS (LIBAVCODEC_VERSION_MAJOR < 56)
+#endif
+#ifndef FF_API_DEINTERLACE
+#define FF_API_DEINTERLACE (LIBAVCODEC_VERSION_MAJOR < 56)
+#endif
+#ifndef FF_API_DESTRUCT_PACKET
+#define FF_API_DESTRUCT_PACKET (LIBAVCODEC_VERSION_MAJOR < 56)
+#endif
+#ifndef FF_API_GET_BUFFER
+#define FF_API_GET_BUFFER (LIBAVCODEC_VERSION_MAJOR < 56)
+#endif
+#ifndef FF_API_MISSING_SAMPLE
+#define FF_API_MISSING_SAMPLE (LIBAVCODEC_VERSION_MAJOR < 56)
+#endif
+#ifndef FF_API_LOWRES
+#define FF_API_LOWRES (LIBAVCODEC_VERSION_MAJOR < 56)
+#endif
+#ifndef FF_API_CAP_VDPAU
+#define FF_API_CAP_VDPAU (LIBAVCODEC_VERSION_MAJOR < 56)
+#endif
+#ifndef FF_API_BUFS_VDPAU
+#define FF_API_BUFS_VDPAU (LIBAVCODEC_VERSION_MAJOR < 56)
+#endif
+#ifndef FF_API_VOXWARE
+#define FF_API_VOXWARE (LIBAVCODEC_VERSION_MAJOR < 56)
+#endif
+#ifndef FF_API_SET_DIMENSIONS
+#define FF_API_SET_DIMENSIONS (LIBAVCODEC_VERSION_MAJOR < 56)
+#endif
+#ifndef FF_API_DEBUG_MV
+#define FF_API_DEBUG_MV (LIBAVCODEC_VERSION_MAJOR < 56)
+#endif
+#ifndef FF_API_AC_VLC
+#define FF_API_AC_VLC (LIBAVCODEC_VERSION_MAJOR < 56)
+#endif
+#ifndef FF_API_OLD_MSMPEG4
+#define FF_API_OLD_MSMPEG4 (LIBAVCODEC_VERSION_MAJOR < 56)
+#endif
+#ifndef FF_API_ASPECT_EXTENDED
+#define FF_API_ASPECT_EXTENDED (LIBAVCODEC_VERSION_MAJOR < 56)
+#endif
+#ifndef FF_API_THREAD_OPAQUE
+#define FF_API_THREAD_OPAQUE (LIBAVCODEC_VERSION_MAJOR < 56)
+#endif
+#ifndef FF_API_CODEC_PKT
+#define FF_API_CODEC_PKT (LIBAVCODEC_VERSION_MAJOR < 56)
+#endif
+#ifndef FF_API_ARCH_ALPHA
+#define FF_API_ARCH_ALPHA (LIBAVCODEC_VERSION_MAJOR < 56)
+#endif
+#ifndef FF_API_XVMC
+#define FF_API_XVMC (LIBAVCODEC_VERSION_MAJOR < 56)
+#endif
+#ifndef FF_API_ERROR_RATE
+#define FF_API_ERROR_RATE (LIBAVCODEC_VERSION_MAJOR < 56)
+#endif
+#ifndef FF_API_QSCALE_TYPE
+#define FF_API_QSCALE_TYPE (LIBAVCODEC_VERSION_MAJOR < 56)
+#endif
+#ifndef FF_API_MB_TYPE
+#define FF_API_MB_TYPE (LIBAVCODEC_VERSION_MAJOR < 56)
+#endif
+#ifndef FF_API_MAX_BFRAMES
+#define FF_API_MAX_BFRAMES (LIBAVCODEC_VERSION_MAJOR < 56)
+#endif
+#ifndef FF_API_FAST_MALLOC
+#define FF_API_FAST_MALLOC (LIBAVCODEC_VERSION_MAJOR < 56)
+#endif
+#ifndef FF_API_NEG_LINESIZES
+#define FF_API_NEG_LINESIZES (LIBAVCODEC_VERSION_MAJOR < 56)
+#endif
+#ifndef FF_API_EMU_EDGE
+#define FF_API_EMU_EDGE (LIBAVCODEC_VERSION_MAJOR < 56)
+#endif
+
+#endif /* AVCODEC_VERSION_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavcodec/xvmc.h b/dom/media/platforms/ffmpeg/libav55/include/libavcodec/xvmc.h
new file mode 100644
index 0000000000..950ed18276
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavcodec/xvmc.h
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2003 Ivan Kalvachev
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVCODEC_XVMC_H
+#define AVCODEC_XVMC_H
+
+/**
+ * @file
+ * @ingroup lavc_codec_hwaccel_xvmc
+ * Public libavcodec XvMC header.
+ */
+
+#include <X11/extensions/XvMC.h>
+
+#include "libavutil/attributes.h"
+#include "version.h"
+#include "avcodec.h"
+
+#if FF_API_XVMC
+
+/**
+ * @defgroup lavc_codec_hwaccel_xvmc XvMC
+ * @ingroup lavc_codec_hwaccel
+ *
+ * @{
+ */
+
+#define AV_XVMC_ID 0x1DC711C0 /**< special value to ensure that regular pixel routines haven't corrupted the struct
+ the number is 1337 speak for the letters IDCT MCo (motion compensation) */
+
+attribute_deprecated struct xvmc_pix_fmt {
+ /** The field contains the special constant value AV_XVMC_ID.
+ It is used as a test that the application correctly uses the API,
+ and that there is no corruption caused by pixel routines.
+ - application - set during initialization
+ - libavcodec - unchanged
+ */
+ int xvmc_id;
+
+ /** Pointer to the block array allocated by XvMCCreateBlocks().
+ The array has to be freed by XvMCDestroyBlocks().
+ Each group of 64 values represents one data block of differential
+ pixel information (in MoCo mode) or coefficients for IDCT.
+ - application - set the pointer during initialization
+ - libavcodec - fills coefficients/pixel data into the array
+ */
+ short* data_blocks;
+
+ /** Pointer to the macroblock description array allocated by
+ XvMCCreateMacroBlocks() and freed by XvMCDestroyMacroBlocks().
+ - application - set the pointer during initialization
+ - libavcodec - fills description data into the array
+ */
+ XvMCMacroBlock* mv_blocks;
+
+ /** Number of macroblock descriptions that can be stored in the mv_blocks
+ array.
+ - application - set during initialization
+ - libavcodec - unchanged
+ */
+ int allocated_mv_blocks;
+
+ /** Number of blocks that can be stored at once in the data_blocks array.
+ - application - set during initialization
+ - libavcodec - unchanged
+ */
+ int allocated_data_blocks;
+
+ /** Indicate that the hardware would interpret data_blocks as IDCT
+ coefficients and perform IDCT on them.
+ - application - set during initialization
+ - libavcodec - unchanged
+ */
+ int idct;
+
+ /** In MoCo mode it indicates that intra macroblocks are assumed to be in
+ unsigned format; same as the XVMC_INTRA_UNSIGNED flag.
+ - application - set during initialization
+ - libavcodec - unchanged
+ */
+ int unsigned_intra;
+
+ /** Pointer to the surface allocated by XvMCCreateSurface().
+ It has to be freed by XvMCDestroySurface() on application exit.
+ It identifies the frame and its state on the video hardware.
+ - application - set during initialization
+ - libavcodec - unchanged
+ */
+ XvMCSurface* p_surface;
+
+/** Set by the decoder before calling ff_draw_horiz_band(),
+ needed by the XvMCRenderSurface function. */
+//@{
+ /** Pointer to the surface used as past reference
+ - application - unchanged
+ - libavcodec - set
+ */
+ XvMCSurface* p_past_surface;
+
+ /** Pointer to the surface used as future reference
+ - application - unchanged
+ - libavcodec - set
+ */
+ XvMCSurface* p_future_surface;
+
+ /** top/bottom field or frame
+ - application - unchanged
+ - libavcodec - set
+ */
+ unsigned int picture_structure;
+
+ /** XVMC_SECOND_FIELD - 1st or 2nd field in the sequence
+ - application - unchanged
+ - libavcodec - set
+ */
+ unsigned int flags;
+//}@
+
+ /** Number of macroblock descriptions in the mv_blocks array
+ that have already been passed to the hardware.
+ - application - zeroes it on get_buffer().
+ A successful ff_draw_horiz_band() may increment it
+ with filled_mb_block_num or zero both.
+ - libavcodec - unchanged
+ */
+ int start_mv_blocks_num;
+
+ /** Number of new macroblock descriptions in the mv_blocks array (after
+ start_mv_blocks_num) that are filled by libavcodec and have to be
+ passed to the hardware.
+ - application - zeroes it on get_buffer() or after successful
+ ff_draw_horiz_band().
+ - libavcodec - increment with one of each stored MB
+ */
+ int filled_mv_blocks_num;
+
+ /** Number of the next free data block; one data block consists of
+ 64 short values in the data_blocks array.
+ All blocks before this one have already been claimed by placing their
+ position into the corresponding block description structure field,
+ that are part of the mv_blocks array.
+ - application - zeroes it on get_buffer().
+ A successful ff_draw_horiz_band() may zero it together
+ with start_mb_blocks_num.
+ - libavcodec - each decoded macroblock increases it by the number
+ of coded blocks it contains.
+ */
+ int next_free_data_block_num;
+};
+
+/**
+ * @}
+ */
+
+#endif /* FF_API_XVMC */
+
+#endif /* AVCODEC_XVMC_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/adler32.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/adler32.h
new file mode 100644
index 0000000000..a8ff6f9d41
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/adler32.h
@@ -0,0 +1,43 @@
+/*
+ * copyright (c) 2006 Mans Rullgard
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_ADLER32_H
+#define AVUTIL_ADLER32_H
+
+#include <stdint.h>
+#include "attributes.h"
+
+/**
+ * @ingroup lavu_crypto
+ * Calculate the Adler32 checksum of a buffer.
+ *
+ * Passing the return value to a subsequent av_adler32_update() call
+ * allows the checksum of multiple buffers to be calculated as though
+ * they were concatenated.
+ *
+ * @param adler initial checksum value
+ * @param buf pointer to input buffer
+ * @param len size of input buffer
+ * @return updated checksum
+ */
+unsigned long av_adler32_update(unsigned long adler, const uint8_t *buf,
+ unsigned int len) av_pure;
+
+#endif /* AVUTIL_ADLER32_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/aes.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/aes.h
new file mode 100644
index 0000000000..edff275b7a
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/aes.h
@@ -0,0 +1,67 @@
+/*
+ * copyright (c) 2007 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_AES_H
+#define AVUTIL_AES_H
+
+#include <stdint.h>
+
+#include "attributes.h"
+#include "version.h"
+
+/**
+ * @defgroup lavu_aes AES
+ * @ingroup lavu_crypto
+ * @{
+ */
+
+#if FF_API_CONTEXT_SIZE
+extern attribute_deprecated const int av_aes_size;
+#endif
+
+struct AVAES;
+
+/**
+ * Allocate an AVAES context.
+ */
+struct AVAES *av_aes_alloc(void);
+
+/**
+ * Initialize an AVAES context.
+ * @param key_bits 128, 192 or 256
+ * @param decrypt 0 for encryption, 1 for decryption
+ */
+int av_aes_init(struct AVAES *a, const uint8_t *key, int key_bits, int decrypt);
+
+/**
+ * Encrypt or decrypt a buffer using a previously initialized context.
+ * @param count number of 16 byte blocks
+ * @param dst destination array, can be equal to src
+ * @param src source array, can be equal to dst
+ * @param iv initialization vector for CBC mode, if NULL then ECB will be used
+ * @param decrypt 0 for encryption, 1 for decryption
+ */
+void av_aes_crypt(struct AVAES *a, uint8_t *dst, const uint8_t *src, int count, uint8_t *iv, int decrypt);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_AES_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/attributes.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/attributes.h
new file mode 100644
index 0000000000..d7f2bb5c6f
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/attributes.h
@@ -0,0 +1,126 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * Macro definitions for various function/variable attributes
+ */
+
+#ifndef AVUTIL_ATTRIBUTES_H
+#define AVUTIL_ATTRIBUTES_H
+
+#ifdef __GNUC__
+# define AV_GCC_VERSION_AT_LEAST(x,y) (__GNUC__ > x || __GNUC__ == x && __GNUC_MINOR__ >= y)
+#else
+# define AV_GCC_VERSION_AT_LEAST(x,y) 0
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3,1)
+# define av_always_inline __attribute__((always_inline)) inline
+#elif defined(_MSC_VER)
+# define av_always_inline __forceinline
+#else
+# define av_always_inline inline
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3,1)
+# define av_noinline __attribute__((noinline))
+#elif defined(_MSC_VER)
+# define av_noinline __declspec(noinline)
+#else
+# define av_noinline
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3,1)
+# define av_pure __attribute__((pure))
+#else
+# define av_pure
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(2,6)
+# define av_const __attribute__((const))
+#else
+# define av_const
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(4,3)
+# define av_cold __attribute__((cold))
+#else
+# define av_cold
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(4,1) && !defined(__llvm__)
+# define av_flatten __attribute__((flatten))
+#else
+# define av_flatten
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3,1)
+# define attribute_deprecated __attribute__((deprecated))
+#elif defined(_MSC_VER)
+# define attribute_deprecated __declspec(deprecated)
+#else
+# define attribute_deprecated
+#endif
+
+#if defined(__GNUC__)
+# define av_unused __attribute__((unused))
+#else
+# define av_unused
+#endif
+
+/**
+ * Mark a variable as used and prevent the compiler from optimizing it
+ * away. This is useful for variables accessed only from inline
+ * assembler without the compiler being aware.
+ */
+#if AV_GCC_VERSION_AT_LEAST(3,1)
+# define av_used __attribute__((used))
+#else
+# define av_used
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3,3)
+# define av_alias __attribute__((may_alias))
+#else
+# define av_alias
+#endif
+
+#if defined(__GNUC__) && !defined(__ICC)
+# define av_uninit(x) x=x
+#else
+# define av_uninit(x) x
+#endif
+
+#ifdef __GNUC__
+# define av_builtin_constant_p __builtin_constant_p
+# define av_printf_format(fmtpos, attrpos) __attribute__((__format__(__printf__, fmtpos, attrpos)))
+#else
+# define av_builtin_constant_p(x) 0
+# define av_printf_format(fmtpos, attrpos)
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(2,5)
+# define av_noreturn __attribute__((noreturn))
+#else
+# define av_noreturn
+#endif
+
+#endif /* AVUTIL_ATTRIBUTES_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/audio_fifo.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/audio_fifo.h
new file mode 100644
index 0000000000..8c76388255
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/audio_fifo.h
@@ -0,0 +1,146 @@
+/*
+ * Audio FIFO
+ * Copyright (c) 2012 Justin Ruggles <justin.ruggles@gmail.com>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * Audio FIFO Buffer
+ */
+
+#ifndef AVUTIL_AUDIO_FIFO_H
+#define AVUTIL_AUDIO_FIFO_H
+
+#include "avutil.h"
+#include "fifo.h"
+#include "samplefmt.h"
+
+/**
+ * @addtogroup lavu_audio
+ * @{
+ */
+
+/**
+ * Context for an Audio FIFO Buffer.
+ *
+ * - Operates at the sample level rather than the byte level.
+ * - Supports multiple channels with either planar or packed sample format.
+ * - Automatic reallocation when writing to a full buffer.
+ */
+typedef struct AVAudioFifo AVAudioFifo;
+
+/**
+ * Free an AVAudioFifo.
+ *
+ * @param af AVAudioFifo to free
+ */
+void av_audio_fifo_free(AVAudioFifo *af);
+
+/**
+ * Allocate an AVAudioFifo.
+ *
+ * @param sample_fmt sample format
+ * @param channels number of channels
+ * @param nb_samples initial allocation size, in samples
+ * @return newly allocated AVAudioFifo, or NULL on error
+ */
+AVAudioFifo *av_audio_fifo_alloc(enum AVSampleFormat sample_fmt, int channels,
+ int nb_samples);
+
+/**
+ * Reallocate an AVAudioFifo.
+ *
+ * @param af AVAudioFifo to reallocate
+ * @param nb_samples new allocation size, in samples
+ * @return 0 if OK, or negative AVERROR code on failure
+ */
+int av_audio_fifo_realloc(AVAudioFifo *af, int nb_samples);
+
+/**
+ * Write data to an AVAudioFifo.
+ *
+ * The AVAudioFifo will be reallocated automatically if the available space
+ * is less than nb_samples.
+ *
+ * @see enum AVSampleFormat
+ * The documentation for AVSampleFormat describes the data layout.
+ *
+ * @param af AVAudioFifo to write to
+ * @param data audio data plane pointers
+ * @param nb_samples number of samples to write
+ * @return number of samples actually written, or negative AVERROR
+ * code on failure.
+ */
+int av_audio_fifo_write(AVAudioFifo *af, void **data, int nb_samples);
+
+/**
+ * Read data from an AVAudioFifo.
+ *
+ * @see enum AVSampleFormat
+ * The documentation for AVSampleFormat describes the data layout.
+ *
+ * @param af AVAudioFifo to read from
+ * @param data audio data plane pointers
+ * @param nb_samples number of samples to read
+ * @return number of samples actually read, or negative AVERROR code
+ * on failure.
+ */
+int av_audio_fifo_read(AVAudioFifo *af, void **data, int nb_samples);
+
+/**
+ * Drain data from an AVAudioFifo.
+ *
+ * Removes the data without reading it.
+ *
+ * @param af AVAudioFifo to drain
+ * @param nb_samples number of samples to drain
+ * @return 0 if OK, or negative AVERROR code on failure
+ */
+int av_audio_fifo_drain(AVAudioFifo *af, int nb_samples);
+
+/**
+ * Reset the AVAudioFifo buffer.
+ *
+ * This empties all data in the buffer.
+ *
+ * @param af AVAudioFifo to reset
+ */
+void av_audio_fifo_reset(AVAudioFifo *af);
+
+/**
+ * Get the current number of samples in the AVAudioFifo available for reading.
+ *
+ * @param af the AVAudioFifo to query
+ * @return number of samples available for reading
+ */
+int av_audio_fifo_size(AVAudioFifo *af);
+
+/**
+ * Get the current number of samples in the AVAudioFifo available for writing.
+ *
+ * @param af the AVAudioFifo to query
+ * @return number of samples available for writing
+ */
+int av_audio_fifo_space(AVAudioFifo *af);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_AUDIO_FIFO_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/audioconvert.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/audioconvert.h
new file mode 100644
index 0000000000..300a67cd3d
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/audioconvert.h
@@ -0,0 +1,6 @@
+
+#include "version.h"
+
+#if FF_API_AUDIOCONVERT
+#include "channel_layout.h"
+#endif
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/avassert.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/avassert.h
new file mode 100644
index 0000000000..b223d26e8d
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/avassert.h
@@ -0,0 +1,66 @@
+/*
+ * copyright (c) 2010 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * simple assert() macros that are a bit more flexible than ISO C assert().
+ * @author Michael Niedermayer <michaelni@gmx.at>
+ */
+
+#ifndef AVUTIL_AVASSERT_H
+#define AVUTIL_AVASSERT_H
+
+#include <stdlib.h>
+#include "avutil.h"
+#include "log.h"
+
+/**
+ * assert() equivalent, that is always enabled.
+ */
+#define av_assert0(cond) do { \
+ if (!(cond)) { \
+ av_log(NULL, AV_LOG_FATAL, "Assertion %s failed at %s:%d\n", \
+ AV_STRINGIFY(cond), __FILE__, __LINE__); \
+ abort(); \
+ } \
+} while (0)
+
+
+/**
+ * assert() equivalent, that does not lie in speed critical code.
+ * These asserts() thus can be enabled without fearing speedloss.
+ */
+#if defined(ASSERT_LEVEL) && ASSERT_LEVEL > 0
+#define av_assert1(cond) av_assert0(cond)
+#else
+#define av_assert1(cond) ((void)0)
+#endif
+
+
+/**
+ * assert() equivalent, that does lie in speed critical code.
+ */
+#if defined(ASSERT_LEVEL) && ASSERT_LEVEL > 1
+#define av_assert2(cond) av_assert0(cond)
+#else
+#define av_assert2(cond) ((void)0)
+#endif
+
+#endif /* AVUTIL_AVASSERT_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/avconfig.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/avconfig.h
new file mode 100644
index 0000000000..f10aa6186b
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/avconfig.h
@@ -0,0 +1,6 @@
+/* Generated by ffconf */
+#ifndef AVUTIL_AVCONFIG_H
+#define AVUTIL_AVCONFIG_H
+#define AV_HAVE_BIGENDIAN 0
+#define AV_HAVE_FAST_UNALIGNED 1
+#endif /* AVUTIL_AVCONFIG_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/avstring.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/avstring.h
new file mode 100644
index 0000000000..b7d10983c3
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/avstring.h
@@ -0,0 +1,226 @@
+/*
+ * Copyright (c) 2007 Mans Rullgard
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_AVSTRING_H
+#define AVUTIL_AVSTRING_H
+
+#include <stddef.h>
+#include "attributes.h"
+
+/**
+ * @addtogroup lavu_string
+ * @{
+ */
+
+/**
+ * Return non-zero if pfx is a prefix of str. If it is, *ptr is set to
+ * the address of the first character in str after the prefix.
+ *
+ * @param str input string
+ * @param pfx prefix to test
+ * @param ptr updated if the prefix is matched inside str
+ * @return non-zero if the prefix matches, zero otherwise
+ */
+int av_strstart(const char *str, const char *pfx, const char **ptr);
+
+/**
+ * Return non-zero if pfx is a prefix of str independent of case. If
+ * it is, *ptr is set to the address of the first character in str
+ * after the prefix.
+ *
+ * @param str input string
+ * @param pfx prefix to test
+ * @param ptr updated if the prefix is matched inside str
+ * @return non-zero if the prefix matches, zero otherwise
+ */
+int av_stristart(const char *str, const char *pfx, const char **ptr);
+
+/**
+ * Locate the first case-independent occurrence in the string haystack
+ * of the string needle. A zero-length string needle is considered to
+ * match at the start of haystack.
+ *
+ * This function is a case-insensitive version of the standard strstr().
+ *
+ * @param haystack string to search in
+ * @param needle string to search for
+ * @return pointer to the located match within haystack
+ * or a null pointer if no match
+ */
+char *av_stristr(const char *haystack, const char *needle);
+
+/**
+ * Locate the first occurrence of the string needle in the string haystack
+ * where not more than hay_length characters are searched. A zero-length
+ * string needle is considered to match at the start of haystack.
+ *
+ * This function is a length-limited version of the standard strstr().
+ *
+ * @param haystack string to search in
+ * @param needle string to search for
+ * @param hay_length length of string to search in
+ * @return pointer to the located match within haystack
+ * or a null pointer if no match
+ */
+char *av_strnstr(const char *haystack, const char *needle, size_t hay_length);
+
+/**
+ * Copy the string src to dst, but no more than size - 1 bytes, and
+ * null-terminate dst.
+ *
+ * This function is the same as BSD strlcpy().
+ *
+ * @param dst destination buffer
+ * @param src source string
+ * @param size size of destination buffer
+ * @return the length of src
+ *
+ * @warning since the return value is the length of src, src absolutely
+ * _must_ be a properly 0-terminated string, otherwise this will read beyond
+ * the end of the buffer and possibly crash.
+ */
+size_t av_strlcpy(char *dst, const char *src, size_t size);
+
+/**
+ * Append the string src to the string dst, but to a total length of
+ * no more than size - 1 bytes, and null-terminate dst.
+ *
+ * This function is similar to BSD strlcat(), but differs when
+ * size <= strlen(dst).
+ *
+ * @param dst destination buffer
+ * @param src source string
+ * @param size size of destination buffer
+ * @return the total length of src and dst
+ *
+ * @warning since the return value use the length of src and dst, these
+ * absolutely _must_ be a properly 0-terminated strings, otherwise this
+ * will read beyond the end of the buffer and possibly crash.
+ */
+size_t av_strlcat(char *dst, const char *src, size_t size);
+
+/**
+ * Append output to a string, according to a format. Never write out of
+ * the destination buffer, and always put a terminating 0 within
+ * the buffer.
+ * @param dst destination buffer (string to which the output is
+ * appended)
+ * @param size total size of the destination buffer
+ * @param fmt printf-compatible format string, specifying how the
+ * following parameters are used
+ * @return the length of the string that would have been generated
+ * if enough space had been available
+ */
+size_t av_strlcatf(char *dst, size_t size, const char *fmt, ...) av_printf_format(3, 4);
+
+/**
+ * Convert a number to a av_malloced string.
+ */
+char *av_d2str(double d);
+
+/**
+ * Unescape the given string until a non escaped terminating char,
+ * and return the token corresponding to the unescaped string.
+ *
+ * The normal \ and ' escaping is supported. Leading and trailing
+ * whitespaces are removed, unless they are escaped with '\' or are
+ * enclosed between ''.
+ *
+ * @param buf the buffer to parse, buf will be updated to point to the
+ * terminating char
+ * @param term a 0-terminated list of terminating chars
+ * @return the malloced unescaped string, which must be av_freed by
+ * the user, NULL in case of allocation failure
+ */
+char *av_get_token(const char **buf, const char *term);
+
+/**
+ * Locale-independent conversion of ASCII isdigit.
+ */
+int av_isdigit(int c);
+
+/**
+ * Locale-independent conversion of ASCII isgraph.
+ */
+int av_isgraph(int c);
+
+/**
+ * Locale-independent conversion of ASCII isspace.
+ */
+int av_isspace(int c);
+
+/**
+ * Locale-independent conversion of ASCII characters to uppercase.
+ */
+static inline int av_toupper(int c)
+{
+ if (c >= 'a' && c <= 'z')
+ c ^= 0x20;
+ return c;
+}
+
+/**
+ * Locale-independent conversion of ASCII characters to lowercase.
+ */
+static inline int av_tolower(int c)
+{
+ if (c >= 'A' && c <= 'Z')
+ c ^= 0x20;
+ return c;
+}
+
+/**
+ * Locale-independent conversion of ASCII isxdigit.
+ */
+int av_isxdigit(int c);
+
+/*
+ * Locale-independent case-insensitive compare.
+ * @note This means only ASCII-range characters are case-insensitive
+ */
+int av_strcasecmp(const char *a, const char *b);
+
+/**
+ * Locale-independent case-insensitive compare.
+ * @note This means only ASCII-range characters are case-insensitive
+ */
+int av_strncasecmp(const char *a, const char *b, size_t n);
+
+
+/**
+ * Thread safe basename.
+ * @param path the path, on DOS both \ and / are considered separators.
+ * @return pointer to the basename substring.
+ */
+const char *av_basename(const char *path);
+
+/**
+ * Thread safe dirname.
+ * @param path the path, on DOS both \ and / are considered separators.
+ * @return the path with the separator replaced by the string terminator or ".".
+ * @note the function may change the input string.
+ */
+const char *av_dirname(char *path);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_AVSTRING_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/avutil.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/avutil.h
new file mode 100644
index 0000000000..a0d35d1627
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/avutil.h
@@ -0,0 +1,284 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_AVUTIL_H
+#define AVUTIL_AVUTIL_H
+
+/**
+ * @file
+ * external API header
+ */
+
+/**
+ * @mainpage
+ *
+ * @section libav_intro Introduction
+ *
+ * This document describes the usage of the different libraries
+ * provided by Libav.
+ *
+ * @li @ref libavc "libavcodec" encoding/decoding library
+ * @li @ref lavfi "libavfilter" graph-based frame editing library
+ * @li @ref libavf "libavformat" I/O and muxing/demuxing library
+ * @li @ref lavd "libavdevice" special devices muxing/demuxing library
+ * @li @ref lavu "libavutil" common utility library
+ * @li @ref lavr "libavresample" audio resampling, format conversion and mixing
+ * @li @ref libsws "libswscale" color conversion and scaling library
+ *
+ * @section libav_versioning Versioning and compatibility
+ *
+ * Each of the Libav libraries contains a version.h header, which defines a
+ * major, minor and micro version number with the
+ * <em>LIBRARYNAME_VERSION_{MAJOR,MINOR,MICRO}</em> macros. The major version
+ * number is incremented with backward incompatible changes - e.g. removing
+ * parts of the public API, reordering public struct members, etc. The minor
+ * version number is incremented for backward compatible API changes or major
+ * new features - e.g. adding a new public function or a new decoder. The micro
+ * version number is incremented for smaller changes that a calling program
+ * might still want to check for - e.g. changing behavior in a previously
+ * unspecified situation.
+ *
+ * Libav guarantees backward API and ABI compatibility for each library as long
+ * as its major version number is unchanged. This means that no public symbols
+ * will be removed or renamed. Types and names of the public struct members and
+ * values of public macros and enums will remain the same (unless they were
+ * explicitly declared as not part of the public API). Documented behavior will
+ * not change.
+ *
+ * In other words, any correct program that works with a given Libav snapshot
+ * should work just as well without any changes with any later snapshot with the
+ * same major versions. This applies to both rebuilding the program against new
+ * Libav versions or to replacing the dynamic Libav libraries that a program
+ * links against.
+ *
+ * However, new public symbols may be added and new members may be appended to
+ * public structs whose size is not part of public ABI (most public structs in
+ * Libav). New macros and enum values may be added. Behavior in undocumented
+ * situations may change slightly (and be documented). All those are accompanied
+ * by an entry in doc/APIchanges and incrementing either the minor or micro
+ * version number.
+ */
+
+/**
+ * @defgroup lavu Common utility functions
+ *
+ * @brief
+ * libavutil contains the code shared across all the other Libav
+ * libraries
+ *
+ * @note In order to use the functions provided by avutil you must include
+ * the specific header.
+ *
+ * @{
+ *
+ * @defgroup lavu_crypto Crypto and Hashing
+ *
+ * @{
+ * @}
+ *
+ * @defgroup lavu_math Maths
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_string String Manipulation
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_mem Memory Management
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_data Data Structures
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_audio Audio related
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_error Error Codes
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_log Logging Facility
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup lavu_misc Other
+ *
+ * @{
+ *
+ * @defgroup lavu_internal Internal
+ *
+ * Not exported functions, for internal usage only
+ *
+ * @{
+ *
+ * @}
+ *
+ * @defgroup preproc_misc Preprocessor String Macros
+ *
+ * @{
+ *
+ * @}
+ */
+
+
+/**
+ * @addtogroup lavu_ver
+ * @{
+ */
+
+/**
+ * Return the LIBAVUTIL_VERSION_INT constant.
+ */
+unsigned avutil_version(void);
+
+/**
+ * Return the libavutil build-time configuration.
+ */
+const char *avutil_configuration(void);
+
+/**
+ * Return the libavutil license.
+ */
+const char *avutil_license(void);
+
+/**
+ * @}
+ */
+
+/**
+ * @addtogroup lavu_media Media Type
+ * @brief Media Type
+ */
+
+enum AVMediaType {
+ AVMEDIA_TYPE_UNKNOWN = -1, ///< Usually treated as AVMEDIA_TYPE_DATA
+ AVMEDIA_TYPE_VIDEO,
+ AVMEDIA_TYPE_AUDIO,
+ AVMEDIA_TYPE_DATA, ///< Opaque data information usually continuous
+ AVMEDIA_TYPE_SUBTITLE,
+ AVMEDIA_TYPE_ATTACHMENT, ///< Opaque data information usually sparse
+ AVMEDIA_TYPE_NB
+};
+
+/**
+ * @defgroup lavu_const Constants
+ * @{
+ *
+ * @defgroup lavu_enc Encoding specific
+ *
+ * @note those definition should move to avcodec
+ * @{
+ */
+
+#define FF_LAMBDA_SHIFT 7
+#define FF_LAMBDA_SCALE (1<<FF_LAMBDA_SHIFT)
+#define FF_QP2LAMBDA 118 ///< factor to convert from H.263 QP to lambda
+#define FF_LAMBDA_MAX (256*128-1)
+
+#define FF_QUALITY_SCALE FF_LAMBDA_SCALE //FIXME maybe remove
+
+/**
+ * @}
+ * @defgroup lavu_time Timestamp specific
+ *
+ * Libav internal timebase and timestamp definitions
+ *
+ * @{
+ */
+
+/**
+ * @brief Undefined timestamp value
+ *
+ * Usually reported by demuxer that work on containers that do not provide
+ * either pts or dts.
+ */
+
+#define AV_NOPTS_VALUE INT64_C(0x8000000000000000)
+
+/**
+ * Internal time base represented as integer
+ */
+
+#define AV_TIME_BASE 1000000
+
+/**
+ * Internal time base represented as fractional value
+ */
+
+#define AV_TIME_BASE_Q (AVRational){1, AV_TIME_BASE}
+
+/**
+ * @}
+ * @}
+ * @defgroup lavu_picture Image related
+ *
+ * AVPicture types, pixel formats and basic image planes manipulation.
+ *
+ * @{
+ */
+
+enum AVPictureType {
+ AV_PICTURE_TYPE_I = 1, ///< Intra
+ AV_PICTURE_TYPE_P, ///< Predicted
+ AV_PICTURE_TYPE_B, ///< Bi-dir predicted
+ AV_PICTURE_TYPE_S, ///< S(GMC)-VOP MPEG4
+ AV_PICTURE_TYPE_SI, ///< Switching Intra
+ AV_PICTURE_TYPE_SP, ///< Switching Predicted
+ AV_PICTURE_TYPE_BI, ///< BI type
+};
+
+/**
+ * Return a single letter to describe the given picture type
+ * pict_type.
+ *
+ * @param[in] pict_type the picture type @return a single character
+ * representing the picture type, '?' if pict_type is unknown
+ */
+char av_get_picture_type_char(enum AVPictureType pict_type);
+
+/**
+ * @}
+ */
+
+#include "error.h"
+#include "version.h"
+#include "macros.h"
+
+/**
+ * @}
+ * @}
+ */
+
+#endif /* AVUTIL_AVUTIL_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/base64.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/base64.h
new file mode 100644
index 0000000000..4750cf5c72
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/base64.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright (c) 2006 Ryan Martell. (rdm4@martellventures.com)
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_BASE64_H
+#define AVUTIL_BASE64_H
+
+#include <stdint.h>
+
+/**
+ * @defgroup lavu_base64 Base64
+ * @ingroup lavu_crypto
+ * @{
+ */
+
+
+/**
+ * Decode a base64-encoded string.
+ *
+ * @param out buffer for decoded data
+ * @param in null-terminated input string
+ * @param out_size size in bytes of the out buffer, must be at
+ * least 3/4 of the length of in
+ * @return number of bytes written, or a negative value in case of
+ * invalid input
+ */
+int av_base64_decode(uint8_t *out, const char *in, int out_size);
+
+/**
+ * Encode data to base64 and null-terminate.
+ *
+ * @param out buffer for encoded data
+ * @param out_size size in bytes of the output buffer, must be at
+ * least AV_BASE64_SIZE(in_size)
+ * @param in_size size in bytes of the 'in' buffer
+ * @return 'out' or NULL in case of error
+ */
+char *av_base64_encode(char *out, int out_size, const uint8_t *in, int in_size);
+
+/**
+ * Calculate the output size needed to base64-encode x bytes.
+ */
+#define AV_BASE64_SIZE(x) (((x)+2) / 3 * 4 + 1)
+
+ /**
+ * @}
+ */
+
+#endif /* AVUTIL_BASE64_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/blowfish.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/blowfish.h
new file mode 100644
index 0000000000..8c29536cfe
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/blowfish.h
@@ -0,0 +1,76 @@
+/*
+ * Blowfish algorithm
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_BLOWFISH_H
+#define AVUTIL_BLOWFISH_H
+
+#include <stdint.h>
+
+/**
+ * @defgroup lavu_blowfish Blowfish
+ * @ingroup lavu_crypto
+ * @{
+ */
+
+#define AV_BF_ROUNDS 16
+
+typedef struct AVBlowfish {
+ uint32_t p[AV_BF_ROUNDS + 2];
+ uint32_t s[4][256];
+} AVBlowfish;
+
+/**
+ * Initialize an AVBlowfish context.
+ *
+ * @param ctx an AVBlowfish context
+ * @param key a key
+ * @param key_len length of the key
+ */
+void av_blowfish_init(struct AVBlowfish *ctx, const uint8_t *key, int key_len);
+
+/**
+ * Encrypt or decrypt a buffer using a previously initialized context.
+ *
+ * @param ctx an AVBlowfish context
+ * @param xl left four bytes halves of input to be encrypted
+ * @param xr right four bytes halves of input to be encrypted
+ * @param decrypt 0 for encryption, 1 for decryption
+ */
+void av_blowfish_crypt_ecb(struct AVBlowfish *ctx, uint32_t *xl, uint32_t *xr,
+ int decrypt);
+
+/**
+ * Encrypt or decrypt a buffer using a previously initialized context.
+ *
+ * @param ctx an AVBlowfish context
+ * @param dst destination array, can be equal to src
+ * @param src source array, can be equal to dst
+ * @param count number of 8 byte blocks
+ * @param iv initialization vector for CBC mode, if NULL ECB will be used
+ * @param decrypt 0 for encryption, 1 for decryption
+ */
+void av_blowfish_crypt(struct AVBlowfish *ctx, uint8_t *dst, const uint8_t *src,
+ int count, uint8_t *iv, int decrypt);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_BLOWFISH_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/bswap.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/bswap.h
new file mode 100644
index 0000000000..93a6016b8c
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/bswap.h
@@ -0,0 +1,111 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * byte swapping routines
+ */
+
+#ifndef AVUTIL_BSWAP_H
+#define AVUTIL_BSWAP_H
+
+#include <stdint.h>
+#include "libavutil/avconfig.h"
+#include "attributes.h"
+
+#ifdef HAVE_AV_CONFIG_H
+
+#include "config.h"
+
+#if ARCH_AARCH64
+# include "aarch64/bswap.h"
+#elif ARCH_ARM
+# include "arm/bswap.h"
+#elif ARCH_AVR32
+# include "avr32/bswap.h"
+#elif ARCH_BFIN
+# include "bfin/bswap.h"
+#elif ARCH_SH4
+# include "sh4/bswap.h"
+#elif ARCH_X86
+# include "x86/bswap.h"
+#endif
+
+#endif /* HAVE_AV_CONFIG_H */
+
+#define AV_BSWAP16C(x) (((x) << 8 & 0xff00) | ((x) >> 8 & 0x00ff))
+#define AV_BSWAP32C(x) (AV_BSWAP16C(x) << 16 | AV_BSWAP16C((x) >> 16))
+#define AV_BSWAP64C(x) (AV_BSWAP32C(x) << 32 | AV_BSWAP32C((x) >> 32))
+
+#define AV_BSWAPC(s, x) AV_BSWAP##s##C(x)
+
+#ifndef av_bswap16
+static av_always_inline av_const uint16_t av_bswap16(uint16_t x)
+{
+ x= (x>>8) | (x<<8);
+ return x;
+}
+#endif
+
+#ifndef av_bswap32
+static av_always_inline av_const uint32_t av_bswap32(uint32_t x)
+{
+ return AV_BSWAP32C(x);
+}
+#endif
+
+#ifndef av_bswap64
+static inline uint64_t av_const av_bswap64(uint64_t x)
+{
+ return (uint64_t)av_bswap32(x) << 32 | av_bswap32(x >> 32);
+}
+#endif
+
+// be2ne ... big-endian to native-endian
+// le2ne ... little-endian to native-endian
+
+#if AV_HAVE_BIGENDIAN
+#define av_be2ne16(x) (x)
+#define av_be2ne32(x) (x)
+#define av_be2ne64(x) (x)
+#define av_le2ne16(x) av_bswap16(x)
+#define av_le2ne32(x) av_bswap32(x)
+#define av_le2ne64(x) av_bswap64(x)
+#define AV_BE2NEC(s, x) (x)
+#define AV_LE2NEC(s, x) AV_BSWAPC(s, x)
+#else
+#define av_be2ne16(x) av_bswap16(x)
+#define av_be2ne32(x) av_bswap32(x)
+#define av_be2ne64(x) av_bswap64(x)
+#define av_le2ne16(x) (x)
+#define av_le2ne32(x) (x)
+#define av_le2ne64(x) (x)
+#define AV_BE2NEC(s, x) AV_BSWAPC(s, x)
+#define AV_LE2NEC(s, x) (x)
+#endif
+
+#define AV_BE2NE16C(x) AV_BE2NEC(16, x)
+#define AV_BE2NE32C(x) AV_BE2NEC(32, x)
+#define AV_BE2NE64C(x) AV_BE2NEC(64, x)
+#define AV_LE2NE16C(x) AV_LE2NEC(16, x)
+#define AV_LE2NE32C(x) AV_LE2NEC(32, x)
+#define AV_LE2NE64C(x) AV_LE2NEC(64, x)
+
+#endif /* AVUTIL_BSWAP_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/buffer.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/buffer.h
new file mode 100644
index 0000000000..56b4d020e5
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/buffer.h
@@ -0,0 +1,267 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * @ingroup lavu_buffer
+ * refcounted data buffer API
+ */
+
+#ifndef AVUTIL_BUFFER_H
+#define AVUTIL_BUFFER_H
+
+#include <stdint.h>
+
+/**
+ * @defgroup lavu_buffer AVBuffer
+ * @ingroup lavu_data
+ *
+ * @{
+ * AVBuffer is an API for reference-counted data buffers.
+ *
+ * There are two core objects in this API -- AVBuffer and AVBufferRef. AVBuffer
+ * represents the data buffer itself; it is opaque and not meant to be accessed
+ * by the caller directly, but only through AVBufferRef. However, the caller may
+ * e.g. compare two AVBuffer pointers to check whether two different references
+ * are describing the same data buffer. AVBufferRef represents a single
+ * reference to an AVBuffer and it is the object that may be manipulated by the
+ * caller directly.
+ *
+ * There are two functions provided for creating a new AVBuffer with a single
+ * reference -- av_buffer_alloc() to just allocate a new buffer, and
+ * av_buffer_create() to wrap an existing array in an AVBuffer. From an existing
+ * reference, additional references may be created with av_buffer_ref().
+ * Use av_buffer_unref() to free a reference (this will automatically free the
+ * data once all the references are freed).
+ *
+ * The convention throughout this API and the rest of Libav is such that the
+ * buffer is considered writable if there exists only one reference to it (and
+ * it has not been marked as read-only). The av_buffer_is_writable() function is
+ * provided to check whether this is true and av_buffer_make_writable() will
+ * automatically create a new writable buffer when necessary.
+ * Of course nothing prevents the calling code from violating this convention,
+ * however that is safe only when all the existing references are under its
+ * control.
+ *
+ * @note Referencing and unreferencing the buffers is thread-safe and thus
+ * may be done from multiple threads simultaneously without any need for
+ * additional locking.
+ *
+ * @note Two different references to the same buffer can point to different
+ * parts of the buffer (i.e. their AVBufferRef.data will not be equal).
+ */
+
+/**
+ * A reference counted buffer type. It is opaque and is meant to be used through
+ * references (AVBufferRef).
+ */
+typedef struct AVBuffer AVBuffer;
+
+/**
+ * A reference to a data buffer.
+ *
+ * The size of this struct is not a part of the public ABI and it is not meant
+ * to be allocated directly.
+ */
+typedef struct AVBufferRef {
+ AVBuffer *buffer;
+
+ /**
+ * The data buffer. It is considered writable if and only if
+ * this is the only reference to the buffer, in which case
+ * av_buffer_is_writable() returns 1.
+ */
+ uint8_t *data;
+ /**
+ * Size of data in bytes.
+ */
+ int size;
+} AVBufferRef;
+
+/**
+ * Allocate an AVBuffer of the given size using av_malloc().
+ *
+ * @return an AVBufferRef of given size or NULL when out of memory
+ */
+AVBufferRef *av_buffer_alloc(int size);
+
+/**
+ * Same as av_buffer_alloc(), except the returned buffer will be initialized
+ * to zero.
+ */
+AVBufferRef *av_buffer_allocz(int size);
+
+/**
+ * Always treat the buffer as read-only, even when it has only one
+ * reference.
+ */
+#define AV_BUFFER_FLAG_READONLY (1 << 0)
+
+/**
+ * Create an AVBuffer from an existing array.
+ *
+ * If this function is successful, data is owned by the AVBuffer. The caller may
+ * only access data through the returned AVBufferRef and references derived from
+ * it.
+ * If this function fails, data is left untouched.
+ * @param data data array
+ * @param size size of data in bytes
+ * @param free a callback for freeing this buffer's data
+ * @param opaque parameter to be passed to free
+ * @param flags a combination of AV_BUFFER_FLAG_*
+ *
+ * @return an AVBufferRef referring to data on success, NULL on failure.
+ */
+AVBufferRef *av_buffer_create(uint8_t *data, int size,
+ void (*free)(void *opaque, uint8_t *data),
+ void *opaque, int flags);
+
+/**
+ * Default free callback, which calls av_free() on the buffer data.
+ * This function is meant to be passed to av_buffer_create(), not called
+ * directly.
+ */
+void av_buffer_default_free(void *opaque, uint8_t *data);
+
+/**
+ * Create a new reference to an AVBuffer.
+ *
+ * @return a new AVBufferRef referring to the same AVBuffer as buf or NULL on
+ * failure.
+ */
+AVBufferRef *av_buffer_ref(AVBufferRef *buf);
+
+/**
+ * Free a given reference and automatically free the buffer if there are no more
+ * references to it.
+ *
+ * @param buf the reference to be freed. The pointer is set to NULL on return.
+ */
+void av_buffer_unref(AVBufferRef **buf);
+
+/**
+ * @return 1 if the caller may write to the data referred to by buf (which is
+ * true if and only if buf is the only reference to the underlying AVBuffer).
+ * Return 0 otherwise.
+ * A positive answer is valid until av_buffer_ref() is called on buf.
+ */
+int av_buffer_is_writable(const AVBufferRef *buf);
+
+/**
+ * Create a writable reference from a given buffer reference, avoiding data copy
+ * if possible.
+ *
+ * @param buf buffer reference to make writable. On success, buf is either left
+ * untouched, or it is unreferenced and a new writable AVBufferRef is
+ * written in its place. On failure, buf is left untouched.
+ * @return 0 on success, a negative AVERROR on failure.
+ */
+int av_buffer_make_writable(AVBufferRef **buf);
+
+/**
+ * Reallocate a given buffer.
+ *
+ * @param buf a buffer reference to reallocate. On success, buf will be
+ * unreferenced and a new reference with the required size will be
+ * written in its place. On failure buf will be left untouched. *buf
+ * may be NULL, then a new buffer is allocated.
+ * @param size required new buffer size.
+ * @return 0 on success, a negative AVERROR on failure.
+ *
+ * @note the buffer is actually reallocated with av_realloc() only if it was
+ * initially allocated through av_buffer_realloc(NULL) and there is only one
+ * reference to it (i.e. the one passed to this function). In all other cases
+ * a new buffer is allocated and the data is copied.
+ */
+int av_buffer_realloc(AVBufferRef **buf, int size);
+
+/**
+ * @}
+ */
+
+/**
+ * @defgroup lavu_bufferpool AVBufferPool
+ * @ingroup lavu_data
+ *
+ * @{
+ * AVBufferPool is an API for a lock-free thread-safe pool of AVBuffers.
+ *
+ * Frequently allocating and freeing large buffers may be slow. AVBufferPool is
+ * meant to solve this in cases when the caller needs a set of buffers of the
+ * same size (the most obvious use case being buffers for raw video or audio
+ * frames).
+ *
+ * At the beginning, the user must call av_buffer_pool_init() to create the
+ * buffer pool. Then whenever a buffer is needed, call av_buffer_pool_get() to
+ * get a reference to a new buffer, similar to av_buffer_alloc(). This new
+ * reference works in all aspects the same way as the one created by
+ * av_buffer_alloc(). However, when the last reference to this buffer is
+ * unreferenced, it is returned to the pool instead of being freed and will be
+ * reused for subsequent av_buffer_pool_get() calls.
+ *
+ * When the caller is done with the pool and no longer needs to allocate any new
+ * buffers, av_buffer_pool_uninit() must be called to mark the pool as freeable.
+ * Once all the buffers are released, it will automatically be freed.
+ *
+ * Allocating and releasing buffers with this API is thread-safe as long as
+ * either the default alloc callback is used, or the user-supplied one is
+ * thread-safe.
+ */
+
+/**
+ * The buffer pool. This structure is opaque and not meant to be accessed
+ * directly. It is allocated with av_buffer_pool_init() and freed with
+ * av_buffer_pool_uninit().
+ */
+typedef struct AVBufferPool AVBufferPool;
+
+/**
+ * Allocate and initialize a buffer pool.
+ *
+ * @param size size of each buffer in this pool
+ * @param alloc a function that will be used to allocate new buffers when the
+ * pool is empty. May be NULL, then the default allocator will be used
+ * (av_buffer_alloc()).
+ * @return newly created buffer pool on success, NULL on error.
+ */
+AVBufferPool *av_buffer_pool_init(int size, AVBufferRef* (*alloc)(int size));
+
+/**
+ * Mark the pool as being available for freeing. It will actually be freed only
+ * once all the allocated buffers associated with the pool are released. Thus it
+ * is safe to call this function while some of the allocated buffers are still
+ * in use.
+ *
+ * @param pool pointer to the pool to be freed. It will be set to NULL.
+ * @see av_buffer_pool_can_uninit()
+ */
+void av_buffer_pool_uninit(AVBufferPool **pool);
+
+/**
+ * Allocate a new AVBuffer, reusing an old buffer from the pool when available.
+ * This function may be called simultaneously from multiple threads.
+ *
+ * @return a reference to the new buffer on success, NULL on error.
+ */
+AVBufferRef *av_buffer_pool_get(AVBufferPool *pool);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_BUFFER_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/channel_layout.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/channel_layout.h
new file mode 100644
index 0000000000..6a1f83005a
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/channel_layout.h
@@ -0,0 +1,186 @@
+/*
+ * Copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ * Copyright (c) 2008 Peter Ross
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_CHANNEL_LAYOUT_H
+#define AVUTIL_CHANNEL_LAYOUT_H
+
+#include <stdint.h>
+
+/**
+ * @file
+ * audio channel layout utility functions
+ */
+
+/**
+ * @addtogroup lavu_audio
+ * @{
+ */
+
+/**
+ * @defgroup channel_masks Audio channel masks
+ * @{
+ */
+#define AV_CH_FRONT_LEFT 0x00000001
+#define AV_CH_FRONT_RIGHT 0x00000002
+#define AV_CH_FRONT_CENTER 0x00000004
+#define AV_CH_LOW_FREQUENCY 0x00000008
+#define AV_CH_BACK_LEFT 0x00000010
+#define AV_CH_BACK_RIGHT 0x00000020
+#define AV_CH_FRONT_LEFT_OF_CENTER 0x00000040
+#define AV_CH_FRONT_RIGHT_OF_CENTER 0x00000080
+#define AV_CH_BACK_CENTER 0x00000100
+#define AV_CH_SIDE_LEFT 0x00000200
+#define AV_CH_SIDE_RIGHT 0x00000400
+#define AV_CH_TOP_CENTER 0x00000800
+#define AV_CH_TOP_FRONT_LEFT 0x00001000
+#define AV_CH_TOP_FRONT_CENTER 0x00002000
+#define AV_CH_TOP_FRONT_RIGHT 0x00004000
+#define AV_CH_TOP_BACK_LEFT 0x00008000
+#define AV_CH_TOP_BACK_CENTER 0x00010000
+#define AV_CH_TOP_BACK_RIGHT 0x00020000
+#define AV_CH_STEREO_LEFT 0x20000000 ///< Stereo downmix.
+#define AV_CH_STEREO_RIGHT 0x40000000 ///< See AV_CH_STEREO_LEFT.
+#define AV_CH_WIDE_LEFT 0x0000000080000000ULL
+#define AV_CH_WIDE_RIGHT 0x0000000100000000ULL
+#define AV_CH_SURROUND_DIRECT_LEFT 0x0000000200000000ULL
+#define AV_CH_SURROUND_DIRECT_RIGHT 0x0000000400000000ULL
+#define AV_CH_LOW_FREQUENCY_2 0x0000000800000000ULL
+
+/** Channel mask value used for AVCodecContext.request_channel_layout
+ to indicate that the user requests the channel order of the decoder output
+ to be the native codec channel order. */
+#define AV_CH_LAYOUT_NATIVE 0x8000000000000000ULL
+
+/**
+ * @}
+ * @defgroup channel_mask_c Audio channel convenience macros
+ * @{
+ * */
+#define AV_CH_LAYOUT_MONO (AV_CH_FRONT_CENTER)
+#define AV_CH_LAYOUT_STEREO (AV_CH_FRONT_LEFT|AV_CH_FRONT_RIGHT)
+#define AV_CH_LAYOUT_2POINT1 (AV_CH_LAYOUT_STEREO|AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_2_1 (AV_CH_LAYOUT_STEREO|AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_SURROUND (AV_CH_LAYOUT_STEREO|AV_CH_FRONT_CENTER)
+#define AV_CH_LAYOUT_3POINT1 (AV_CH_LAYOUT_SURROUND|AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_4POINT0 (AV_CH_LAYOUT_SURROUND|AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_4POINT1 (AV_CH_LAYOUT_4POINT0|AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_2_2 (AV_CH_LAYOUT_STEREO|AV_CH_SIDE_LEFT|AV_CH_SIDE_RIGHT)
+#define AV_CH_LAYOUT_QUAD (AV_CH_LAYOUT_STEREO|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_5POINT0 (AV_CH_LAYOUT_SURROUND|AV_CH_SIDE_LEFT|AV_CH_SIDE_RIGHT)
+#define AV_CH_LAYOUT_5POINT1 (AV_CH_LAYOUT_5POINT0|AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_5POINT0_BACK (AV_CH_LAYOUT_SURROUND|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_5POINT1_BACK (AV_CH_LAYOUT_5POINT0_BACK|AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_6POINT0 (AV_CH_LAYOUT_5POINT0|AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_6POINT0_FRONT (AV_CH_LAYOUT_2_2|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER)
+#define AV_CH_LAYOUT_HEXAGONAL (AV_CH_LAYOUT_5POINT0_BACK|AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_6POINT1 (AV_CH_LAYOUT_5POINT1|AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_6POINT1_BACK (AV_CH_LAYOUT_5POINT1_BACK|AV_CH_BACK_CENTER)
+#define AV_CH_LAYOUT_6POINT1_FRONT (AV_CH_LAYOUT_6POINT0_FRONT|AV_CH_LOW_FREQUENCY)
+#define AV_CH_LAYOUT_7POINT0 (AV_CH_LAYOUT_5POINT0|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_7POINT0_FRONT (AV_CH_LAYOUT_5POINT0|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER)
+#define AV_CH_LAYOUT_7POINT1 (AV_CH_LAYOUT_5POINT1|AV_CH_BACK_LEFT|AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_7POINT1_WIDE (AV_CH_LAYOUT_5POINT1|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER)
+#define AV_CH_LAYOUT_7POINT1_WIDE_BACK (AV_CH_LAYOUT_5POINT1_BACK|AV_CH_FRONT_LEFT_OF_CENTER|AV_CH_FRONT_RIGHT_OF_CENTER)
+#define AV_CH_LAYOUT_OCTAGONAL (AV_CH_LAYOUT_5POINT0|AV_CH_BACK_LEFT|AV_CH_BACK_CENTER|AV_CH_BACK_RIGHT)
+#define AV_CH_LAYOUT_STEREO_DOWNMIX (AV_CH_STEREO_LEFT|AV_CH_STEREO_RIGHT)
+
+enum AVMatrixEncoding {
+ AV_MATRIX_ENCODING_NONE,
+ AV_MATRIX_ENCODING_DOLBY,
+ AV_MATRIX_ENCODING_DPLII,
+ AV_MATRIX_ENCODING_DPLIIX,
+ AV_MATRIX_ENCODING_DPLIIZ,
+ AV_MATRIX_ENCODING_DOLBYEX,
+ AV_MATRIX_ENCODING_DOLBYHEADPHONE,
+ AV_MATRIX_ENCODING_NB
+};
+
+/**
+ * @}
+ */
+
+/**
+ * Return a channel layout id that matches name, or 0 if no match is found.
+ *
+ * name can be one or several of the following notations,
+ * separated by '+' or '|':
+ * - the name of an usual channel layout (mono, stereo, 4.0, quad, 5.0,
+ * 5.0(side), 5.1, 5.1(side), 7.1, 7.1(wide), downmix);
+ * - the name of a single channel (FL, FR, FC, LFE, BL, BR, FLC, FRC, BC,
+ * SL, SR, TC, TFL, TFC, TFR, TBL, TBC, TBR, DL, DR);
+ * - a number of channels, in decimal, optionally followed by 'c', yielding
+ * the default channel layout for that number of channels (@see
+ * av_get_default_channel_layout);
+ * - a channel layout mask, in hexadecimal starting with "0x" (see the
+ * AV_CH_* macros).
+ *
+ * Example: "stereo+FC" = "2+FC" = "2c+1c" = "0x7"
+ */
+uint64_t av_get_channel_layout(const char *name);
+
+/**
+ * Return a description of a channel layout.
+ * If nb_channels is <= 0, it is guessed from the channel_layout.
+ *
+ * @param buf put here the string containing the channel layout
+ * @param buf_size size in bytes of the buffer
+ */
+void av_get_channel_layout_string(char *buf, int buf_size, int nb_channels, uint64_t channel_layout);
+
+/**
+ * Return the number of channels in the channel layout.
+ */
+int av_get_channel_layout_nb_channels(uint64_t channel_layout);
+
+/**
+ * Return default channel layout for a given number of channels.
+ */
+uint64_t av_get_default_channel_layout(int nb_channels);
+
+/**
+ * Get the index of a channel in channel_layout.
+ *
+ * @param channel a channel layout describing exactly one channel which must be
+ * present in channel_layout.
+ *
+ * @return index of channel in channel_layout on success, a negative AVERROR
+ * on error.
+ */
+int av_get_channel_layout_channel_index(uint64_t channel_layout,
+ uint64_t channel);
+
+/**
+ * Get the channel with the given index in channel_layout.
+ */
+uint64_t av_channel_layout_extract_channel(uint64_t channel_layout, int index);
+
+/**
+ * Get the name of a given channel.
+ *
+ * @return channel name on success, NULL on error.
+ */
+const char *av_get_channel_name(uint64_t channel);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_CHANNEL_LAYOUT_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/common.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/common.h
new file mode 100644
index 0000000000..eb40e12990
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/common.h
@@ -0,0 +1,406 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * common internal and external API header
+ */
+
+#ifndef AVUTIL_COMMON_H
+#define AVUTIL_COMMON_H
+
+#include <errno.h>
+#include <inttypes.h>
+#include <limits.h>
+#include <math.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "attributes.h"
+#include "version.h"
+#include "libavutil/avconfig.h"
+
+#if AV_HAVE_BIGENDIAN
+# define AV_NE(be, le) (be)
+#else
+# define AV_NE(be, le) (le)
+#endif
+
+//rounded division & shift
+#define RSHIFT(a,b) ((a) > 0 ? ((a) + ((1<<(b))>>1))>>(b) : ((a) + ((1<<(b))>>1)-1)>>(b))
+/* assume b>0 */
+#define ROUNDED_DIV(a,b) (((a)>0 ? (a) + ((b)>>1) : (a) - ((b)>>1))/(b))
+#define FFABS(a) ((a) >= 0 ? (a) : (-(a)))
+#define FFSIGN(a) ((a) > 0 ? 1 : -1)
+
+#define FFMAX(a,b) ((a) > (b) ? (a) : (b))
+#define FFMAX3(a,b,c) FFMAX(FFMAX(a,b),c)
+#define FFMIN(a,b) ((a) > (b) ? (b) : (a))
+#define FFMIN3(a,b,c) FFMIN(FFMIN(a,b),c)
+
+#define FFSWAP(type,a,b) do{type SWAP_tmp= b; b= a; a= SWAP_tmp;}while(0)
+#define FF_ARRAY_ELEMS(a) (sizeof(a) / sizeof((a)[0]))
+#define FFALIGN(x, a) (((x)+(a)-1)&~((a)-1))
+
+/* misc math functions */
+
+#if FF_API_AV_REVERSE
+extern attribute_deprecated const uint8_t av_reverse[256];
+#endif
+
+#ifdef HAVE_AV_CONFIG_H
+# include "config.h"
+# include "intmath.h"
+#endif
+
+/* Pull in unguarded fallback defines at the end of this file. */
+#include "common.h"
+
+#ifndef av_log2
+av_const int av_log2(unsigned v);
+#endif
+
+#ifndef av_log2_16bit
+av_const int av_log2_16bit(unsigned v);
+#endif
+
+/**
+ * Clip a signed integer value into the amin-amax range.
+ * @param a value to clip
+ * @param amin minimum value of the clip range
+ * @param amax maximum value of the clip range
+ * @return clipped value
+ */
+static av_always_inline av_const int av_clip_c(int a, int amin, int amax)
+{
+ if (a < amin) return amin;
+ else if (a > amax) return amax;
+ else return a;
+}
+
+/**
+ * Clip a signed integer value into the 0-255 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const uint8_t av_clip_uint8_c(int a)
+{
+ if (a&(~0xFF)) return (-a)>>31;
+ else return a;
+}
+
+/**
+ * Clip a signed integer value into the -128,127 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const int8_t av_clip_int8_c(int a)
+{
+ if ((a+0x80) & ~0xFF) return (a>>31) ^ 0x7F;
+ else return a;
+}
+
+/**
+ * Clip a signed integer value into the 0-65535 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const uint16_t av_clip_uint16_c(int a)
+{
+ if (a&(~0xFFFF)) return (-a)>>31;
+ else return a;
+}
+
+/**
+ * Clip a signed integer value into the -32768,32767 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const int16_t av_clip_int16_c(int a)
+{
+ if ((a+0x8000) & ~0xFFFF) return (a>>31) ^ 0x7FFF;
+ else return a;
+}
+
+/**
+ * Clip a signed 64-bit integer value into the -2147483648,2147483647 range.
+ * @param a value to clip
+ * @return clipped value
+ */
+static av_always_inline av_const int32_t av_clipl_int32_c(int64_t a)
+{
+ if ((a+0x80000000u) & ~UINT64_C(0xFFFFFFFF)) return (a>>63) ^ 0x7FFFFFFF;
+ else return a;
+}
+
+/**
+ * Clip a signed integer to an unsigned power of two range.
+ * @param a value to clip
+ * @param p bit position to clip at
+ * @return clipped value
+ */
+static av_always_inline av_const unsigned av_clip_uintp2_c(int a, int p)
+{
+ if (a & ~((1<<p) - 1)) return -a >> 31 & ((1<<p) - 1);
+ else return a;
+}
+
+/**
+ * Add two signed 32-bit values with saturation.
+ *
+ * @param a one value
+ * @param b another value
+ * @return sum with signed saturation
+ */
+static av_always_inline int av_sat_add32_c(int a, int b)
+{
+ return av_clipl_int32((int64_t)a + b);
+}
+
+/**
+ * Add a doubled value to another value with saturation at both stages.
+ *
+ * @param a first value
+ * @param b value doubled and added to a
+ * @return sum with signed saturation
+ */
+static av_always_inline int av_sat_dadd32_c(int a, int b)
+{
+ return av_sat_add32(a, av_sat_add32(b, b));
+}
+
+/**
+ * Clip a float value into the amin-amax range.
+ * @param a value to clip
+ * @param amin minimum value of the clip range
+ * @param amax maximum value of the clip range
+ * @return clipped value
+ */
+static av_always_inline av_const float av_clipf_c(float a, float amin, float amax)
+{
+ if (a < amin) return amin;
+ else if (a > amax) return amax;
+ else return a;
+}
+
+/** Compute ceil(log2(x)).
+ * @param x value used to compute ceil(log2(x))
+ * @return computed ceiling of log2(x)
+ */
+static av_always_inline av_const int av_ceil_log2_c(int x)
+{
+ return av_log2((x - 1) << 1);
+}
+
+/**
+ * Count number of bits set to one in x
+ * @param x value to count bits of
+ * @return the number of bits set to one in x
+ */
+static av_always_inline av_const int av_popcount_c(uint32_t x)
+{
+ x -= (x >> 1) & 0x55555555;
+ x = (x & 0x33333333) + ((x >> 2) & 0x33333333);
+ x = (x + (x >> 4)) & 0x0F0F0F0F;
+ x += x >> 8;
+ return (x + (x >> 16)) & 0x3F;
+}
+
+/**
+ * Count number of bits set to one in x
+ * @param x value to count bits of
+ * @return the number of bits set to one in x
+ */
+static av_always_inline av_const int av_popcount64_c(uint64_t x)
+{
+ return av_popcount(x) + av_popcount(x >> 32);
+}
+
+#define MKTAG(a,b,c,d) ((a) | ((b) << 8) | ((c) << 16) | ((unsigned)(d) << 24))
+#define MKBETAG(a,b,c,d) ((d) | ((c) << 8) | ((b) << 16) | ((unsigned)(a) << 24))
+
+/**
+ * Convert a UTF-8 character (up to 4 bytes) to its 32-bit UCS-4 encoded form.
+ *
+ * @param val Output value, must be an lvalue of type uint32_t.
+ * @param GET_BYTE Expression reading one byte from the input.
+ * Evaluated up to 7 times (4 for the currently
+ * assigned Unicode range). With a memory buffer
+ * input, this could be *ptr++.
+ * @param ERROR Expression to be evaluated on invalid input,
+ * typically a goto statement.
+ */
+#define GET_UTF8(val, GET_BYTE, ERROR)\
+ val= GET_BYTE;\
+ {\
+ uint32_t top = (val & 128) >> 1;\
+ if ((val & 0xc0) == 0x80)\
+ ERROR\
+ while (val & top) {\
+ int tmp= GET_BYTE - 128;\
+ if(tmp>>6)\
+ ERROR\
+ val= (val<<6) + tmp;\
+ top <<= 5;\
+ }\
+ val &= (top << 1) - 1;\
+ }
+
+/**
+ * Convert a UTF-16 character (2 or 4 bytes) to its 32-bit UCS-4 encoded form.
+ *
+ * @param val Output value, must be an lvalue of type uint32_t.
+ * @param GET_16BIT Expression returning two bytes of UTF-16 data converted
+ * to native byte order. Evaluated one or two times.
+ * @param ERROR Expression to be evaluated on invalid input,
+ * typically a goto statement.
+ */
+#define GET_UTF16(val, GET_16BIT, ERROR)\
+ val = GET_16BIT;\
+ {\
+ unsigned int hi = val - 0xD800;\
+ if (hi < 0x800) {\
+ val = GET_16BIT - 0xDC00;\
+ if (val > 0x3FFU || hi > 0x3FFU)\
+ ERROR\
+ val += (hi<<10) + 0x10000;\
+ }\
+ }\
+
+/**
+ * @def PUT_UTF8(val, tmp, PUT_BYTE)
+ * Convert a 32-bit Unicode character to its UTF-8 encoded form (up to 4 bytes long).
+ * @param val is an input-only argument and should be of type uint32_t. It holds
+ * a UCS-4 encoded Unicode character that is to be converted to UTF-8. If
+ * val is given as a function it is executed only once.
+ * @param tmp is a temporary variable and should be of type uint8_t. It
+ * represents an intermediate value during conversion that is to be
+ * output by PUT_BYTE.
+ * @param PUT_BYTE writes the converted UTF-8 bytes to any proper destination.
+ * It could be a function or a statement, and uses tmp as the input byte.
+ * For example, PUT_BYTE could be "*output++ = tmp;" PUT_BYTE will be
+ * executed up to 4 times for values in the valid UTF-8 range and up to
+ * 7 times in the general case, depending on the length of the converted
+ * Unicode character.
+ */
+#define PUT_UTF8(val, tmp, PUT_BYTE)\
+ {\
+ int bytes, shift;\
+ uint32_t in = val;\
+ if (in < 0x80) {\
+ tmp = in;\
+ PUT_BYTE\
+ } else {\
+ bytes = (av_log2(in) + 4) / 5;\
+ shift = (bytes - 1) * 6;\
+ tmp = (256 - (256 >> bytes)) | (in >> shift);\
+ PUT_BYTE\
+ while (shift >= 6) {\
+ shift -= 6;\
+ tmp = 0x80 | ((in >> shift) & 0x3f);\
+ PUT_BYTE\
+ }\
+ }\
+ }
+
+/**
+ * @def PUT_UTF16(val, tmp, PUT_16BIT)
+ * Convert a 32-bit Unicode character to its UTF-16 encoded form (2 or 4 bytes).
+ * @param val is an input-only argument and should be of type uint32_t. It holds
+ * a UCS-4 encoded Unicode character that is to be converted to UTF-16. If
+ * val is given as a function it is executed only once.
+ * @param tmp is a temporary variable and should be of type uint16_t. It
+ * represents an intermediate value during conversion that is to be
+ * output by PUT_16BIT.
+ * @param PUT_16BIT writes the converted UTF-16 data to any proper destination
+ * in desired endianness. It could be a function or a statement, and uses tmp
+ * as the input byte. For example, PUT_BYTE could be "*output++ = tmp;"
+ * PUT_BYTE will be executed 1 or 2 times depending on input character.
+ */
+#define PUT_UTF16(val, tmp, PUT_16BIT)\
+ {\
+ uint32_t in = val;\
+ if (in < 0x10000) {\
+ tmp = in;\
+ PUT_16BIT\
+ } else {\
+ tmp = 0xD800 | ((in - 0x10000) >> 10);\
+ PUT_16BIT\
+ tmp = 0xDC00 | ((in - 0x10000) & 0x3FF);\
+ PUT_16BIT\
+ }\
+ }\
+
+
+
+#include "mem.h"
+
+#ifdef HAVE_AV_CONFIG_H
+# include "internal.h"
+#endif /* HAVE_AV_CONFIG_H */
+
+#endif /* AVUTIL_COMMON_H */
+
+/*
+ * The following definitions are outside the multiple inclusion guard
+ * to ensure they are immediately available in intmath.h.
+ */
+
+#ifndef av_ceil_log2
+# define av_ceil_log2 av_ceil_log2_c
+#endif
+#ifndef av_clip
+# define av_clip av_clip_c
+#endif
+#ifndef av_clip_uint8
+# define av_clip_uint8 av_clip_uint8_c
+#endif
+#ifndef av_clip_int8
+# define av_clip_int8 av_clip_int8_c
+#endif
+#ifndef av_clip_uint16
+# define av_clip_uint16 av_clip_uint16_c
+#endif
+#ifndef av_clip_int16
+# define av_clip_int16 av_clip_int16_c
+#endif
+#ifndef av_clipl_int32
+# define av_clipl_int32 av_clipl_int32_c
+#endif
+#ifndef av_clip_uintp2
+# define av_clip_uintp2 av_clip_uintp2_c
+#endif
+#ifndef av_sat_add32
+# define av_sat_add32 av_sat_add32_c
+#endif
+#ifndef av_sat_dadd32
+# define av_sat_dadd32 av_sat_dadd32_c
+#endif
+#ifndef av_clipf
+# define av_clipf av_clipf_c
+#endif
+#ifndef av_popcount
+# define av_popcount av_popcount_c
+#endif
+#ifndef av_popcount64
+# define av_popcount64 av_popcount64_c
+#endif
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/cpu.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/cpu.h
new file mode 100644
index 0000000000..29036e3941
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/cpu.h
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2000, 2001, 2002 Fabrice Bellard
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_CPU_H
+#define AVUTIL_CPU_H
+
+#include "version.h"
+
+#define AV_CPU_FLAG_FORCE 0x80000000 /* force usage of selected flags (OR) */
+
+ /* lower 16 bits - CPU features */
+#define AV_CPU_FLAG_MMX 0x0001 ///< standard MMX
+#define AV_CPU_FLAG_MMXEXT 0x0002 ///< SSE integer functions or AMD MMX ext
+#if FF_API_CPU_FLAG_MMX2
+#define AV_CPU_FLAG_MMX2 0x0002 ///< SSE integer functions or AMD MMX ext
+#endif
+#define AV_CPU_FLAG_3DNOW 0x0004 ///< AMD 3DNOW
+#define AV_CPU_FLAG_SSE 0x0008 ///< SSE functions
+#define AV_CPU_FLAG_SSE2 0x0010 ///< PIV SSE2 functions
+#define AV_CPU_FLAG_SSE2SLOW 0x40000000 ///< SSE2 supported, but usually not faster
+ ///< than regular MMX/SSE (e.g. Core1)
+#define AV_CPU_FLAG_3DNOWEXT 0x0020 ///< AMD 3DNowExt
+#define AV_CPU_FLAG_SSE3 0x0040 ///< Prescott SSE3 functions
+#define AV_CPU_FLAG_SSE3SLOW 0x20000000 ///< SSE3 supported, but usually not faster
+ ///< than regular MMX/SSE (e.g. Core1)
+#define AV_CPU_FLAG_SSSE3 0x0080 ///< Conroe SSSE3 functions
+#define AV_CPU_FLAG_ATOM 0x10000000 ///< Atom processor, some SSSE3 instructions are slower
+#define AV_CPU_FLAG_SSE4 0x0100 ///< Penryn SSE4.1 functions
+#define AV_CPU_FLAG_SSE42 0x0200 ///< Nehalem SSE4.2 functions
+#define AV_CPU_FLAG_AVX 0x4000 ///< AVX functions: requires OS support even if YMM registers aren't used
+#define AV_CPU_FLAG_XOP 0x0400 ///< Bulldozer XOP functions
+#define AV_CPU_FLAG_FMA4 0x0800 ///< Bulldozer FMA4 functions
+#define AV_CPU_FLAG_CMOV 0x1000 ///< i686 cmov
+#define AV_CPU_FLAG_AVX2 0x8000 ///< AVX2 functions: requires OS support even if YMM registers aren't used
+
+#define AV_CPU_FLAG_ALTIVEC 0x0001 ///< standard
+
+#define AV_CPU_FLAG_ARMV5TE (1 << 0)
+#define AV_CPU_FLAG_ARMV6 (1 << 1)
+#define AV_CPU_FLAG_ARMV6T2 (1 << 2)
+#define AV_CPU_FLAG_VFP (1 << 3)
+#define AV_CPU_FLAG_VFPV3 (1 << 4)
+#define AV_CPU_FLAG_NEON (1 << 5)
+
+/**
+ * Return the flags which specify extensions supported by the CPU.
+ */
+int av_get_cpu_flags(void);
+
+/**
+ * Set a mask on flags returned by av_get_cpu_flags().
+ * This function is mainly useful for testing.
+ *
+ * @warning this function is not thread safe.
+ */
+void av_set_cpu_flags_mask(int mask);
+
+/**
+ * Parse CPU flags from a string.
+ *
+ * @return a combination of AV_CPU_* flags, negative on error.
+ */
+int av_parse_cpu_flags(const char *s);
+
+/**
+ * @return the number of logical CPU cores present.
+ */
+int av_cpu_count(void);
+
+#endif /* AVUTIL_CPU_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/crc.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/crc.h
new file mode 100644
index 0000000000..0540619d20
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/crc.h
@@ -0,0 +1,74 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_CRC_H
+#define AVUTIL_CRC_H
+
+#include <stdint.h>
+#include <stddef.h>
+#include "attributes.h"
+
+typedef uint32_t AVCRC;
+
+typedef enum {
+ AV_CRC_8_ATM,
+ AV_CRC_16_ANSI,
+ AV_CRC_16_CCITT,
+ AV_CRC_32_IEEE,
+ AV_CRC_32_IEEE_LE, /*< reversed bitorder version of AV_CRC_32_IEEE */
+ AV_CRC_MAX, /*< Not part of public API! Do not use outside libavutil. */
+}AVCRCId;
+
+/**
+ * Initialize a CRC table.
+ * @param ctx must be an array of size sizeof(AVCRC)*257 or sizeof(AVCRC)*1024
+ * @param le If 1, the lowest bit represents the coefficient for the highest
+ * exponent of the corresponding polynomial (both for poly and
+ * actual CRC).
+ * If 0, you must swap the CRC parameter and the result of av_crc
+ * if you need the standard representation (can be simplified in
+ * most cases to e.g. bswap16):
+ * av_bswap32(crc << (32-bits))
+ * @param bits number of bits for the CRC
+ * @param poly generator polynomial without the x**bits coefficient, in the
+ * representation as specified by le
+ * @param ctx_size size of ctx in bytes
+ * @return <0 on failure
+ */
+int av_crc_init(AVCRC *ctx, int le, int bits, uint32_t poly, int ctx_size);
+
+/**
+ * Get an initialized standard CRC table.
+ * @param crc_id ID of a standard CRC
+ * @return a pointer to the CRC table or NULL on failure
+ */
+const AVCRC *av_crc_get_table(AVCRCId crc_id);
+
+/**
+ * Calculate the CRC of a block.
+ * @param crc CRC of previous blocks if any or initial value for CRC
+ * @return CRC updated with the data from the given block
+ *
+ * @see av_crc_init() "le" parameter
+ */
+uint32_t av_crc(const AVCRC *ctx, uint32_t crc,
+ const uint8_t *buffer, size_t length) av_pure;
+
+#endif /* AVUTIL_CRC_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/dict.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/dict.h
new file mode 100644
index 0000000000..e0a91ae836
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/dict.h
@@ -0,0 +1,146 @@
+/*
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * Public dictionary API.
+ */
+
+#ifndef AVUTIL_DICT_H
+#define AVUTIL_DICT_H
+
+/**
+ * @addtogroup lavu_dict AVDictionary
+ * @ingroup lavu_data
+ *
+ * @brief Simple key:value store
+ *
+ * @{
+ * Dictionaries are used for storing key:value pairs. To create
+ * an AVDictionary, simply pass an address of a NULL pointer to
+ * av_dict_set(). NULL can be used as an empty dictionary wherever
+ * a pointer to an AVDictionary is required.
+ * Use av_dict_get() to retrieve an entry or iterate over all
+ * entries and finally av_dict_free() to free the dictionary
+ * and all its contents.
+ *
+ @code
+ AVDictionary *d = NULL; // "create" an empty dictionary
+ AVDictionaryEntry *t = NULL;
+
+ av_dict_set(&d, "foo", "bar", 0); // add an entry
+
+ char *k = av_strdup("key"); // if your strings are already allocated,
+ char *v = av_strdup("value"); // you can avoid copying them like this
+ av_dict_set(&d, k, v, AV_DICT_DONT_STRDUP_KEY | AV_DICT_DONT_STRDUP_VAL);
+
+ while (t = av_dict_get(d, "", t, AV_DICT_IGNORE_SUFFIX)) {
+ <....> // iterate over all entries in d
+ }
+ av_dict_free(&d);
+ @endcode
+ *
+ */
+
+#define AV_DICT_MATCH_CASE 1
+#define AV_DICT_IGNORE_SUFFIX 2
+#define AV_DICT_DONT_STRDUP_KEY 4 /**< Take ownership of a key that's been
+ allocated with av_malloc() and children. */
+#define AV_DICT_DONT_STRDUP_VAL 8 /**< Take ownership of a value that's been
+ allocated with av_malloc() and chilren. */
+#define AV_DICT_DONT_OVERWRITE 16 ///< Don't overwrite existing entries.
+#define AV_DICT_APPEND 32 /**< If the entry already exists, append to it. Note that no
+ delimiter is added, the strings are simply concatenated. */
+
+typedef struct AVDictionaryEntry {
+ char *key;
+ char *value;
+} AVDictionaryEntry;
+
+typedef struct AVDictionary AVDictionary;
+
+/**
+ * Get a dictionary entry with matching key.
+ *
+ * @param prev Set to the previous matching element to find the next.
+ * If set to NULL the first matching element is returned.
+ * @param flags Allows case as well as suffix-insensitive comparisons.
+ * @return Found entry or NULL, changing key or value leads to undefined behavior.
+ */
+AVDictionaryEntry *
+av_dict_get(AVDictionary *m, const char *key, const AVDictionaryEntry *prev, int flags);
+
+/**
+ * Get number of entries in dictionary.
+ *
+ * @param m dictionary
+ * @return number of entries in dictionary
+ */
+int av_dict_count(const AVDictionary *m);
+
+/**
+ * Set the given entry in *pm, overwriting an existing entry.
+ *
+ * @param pm pointer to a pointer to a dictionary struct. If *pm is NULL
+ * a dictionary struct is allocated and put in *pm.
+ * @param key entry key to add to *pm (will be av_strduped depending on flags)
+ * @param value entry value to add to *pm (will be av_strduped depending on flags).
+ * Passing a NULL value will cause an existing entry to be deleted.
+ * @return >= 0 on success otherwise an error code <0
+ */
+int av_dict_set(AVDictionary **pm, const char *key, const char *value, int flags);
+
+/**
+ * Parse the key/value pairs list and add to a dictionary.
+ *
+ * @param key_val_sep a 0-terminated list of characters used to separate
+ * key from value
+ * @param pairs_sep a 0-terminated list of characters used to separate
+ * two pairs from each other
+ * @param flags flags to use when adding to dictionary.
+ * AV_DICT_DONT_STRDUP_KEY and AV_DICT_DONT_STRDUP_VAL
+ * are ignored since the key/value tokens will always
+ * be duplicated.
+ * @return 0 on success, negative AVERROR code on failure
+ */
+int av_dict_parse_string(AVDictionary **pm, const char *str,
+ const char *key_val_sep, const char *pairs_sep,
+ int flags);
+
+/**
+ * Copy entries from one AVDictionary struct into another.
+ * @param dst pointer to a pointer to a AVDictionary struct. If *dst is NULL,
+ * this function will allocate a struct for you and put it in *dst
+ * @param src pointer to source AVDictionary struct
+ * @param flags flags to use when setting entries in *dst
+ * @note metadata is read using the AV_DICT_IGNORE_SUFFIX flag
+ */
+void av_dict_copy(AVDictionary **dst, AVDictionary *src, int flags);
+
+/**
+ * Free all the memory allocated for an AVDictionary struct
+ * and all keys and values.
+ */
+void av_dict_free(AVDictionary **m);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_DICT_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/downmix_info.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/downmix_info.h
new file mode 100644
index 0000000000..69969f6fbd
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/downmix_info.h
@@ -0,0 +1,114 @@
+/*
+ * Copyright (c) 2014 Tim Walker <tdskywalker@gmail.com>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_DOWNMIX_INFO_H
+#define AVUTIL_DOWNMIX_INFO_H
+
+#include "frame.h"
+
+/**
+ * @file
+ * audio downmix medatata
+ */
+
+/**
+ * @addtogroup lavu_audio
+ * @{
+ */
+
+/**
+ * @defgroup downmix_info Audio downmix metadata
+ * @{
+ */
+
+/**
+ * Possible downmix types.
+ */
+enum AVDownmixType {
+ AV_DOWNMIX_TYPE_UNKNOWN, /**< Not indicated. */
+ AV_DOWNMIX_TYPE_LORO, /**< Lo/Ro 2-channel downmix (Stereo). */
+ AV_DOWNMIX_TYPE_LTRT, /**< Lt/Rt 2-channel downmix, Dolby Surround compatible. */
+ AV_DOWNMIX_TYPE_DPLII, /**< Lt/Rt 2-channel downmix, Dolby Pro Logic II compatible. */
+ AV_DOWNMIX_TYPE_NB /**< Number of downmix types. Not part of ABI. */
+};
+
+/**
+ * This structure describes optional metadata relevant to a downmix procedure.
+ *
+ * All fields are set by the decoder to the value indicated in the audio
+ * bitstream (if present), or to a "sane" default otherwise.
+ */
+typedef struct AVDownmixInfo {
+ /**
+ * Type of downmix preferred by the mastering engineer.
+ */
+ enum AVDownmixType preferred_downmix_type;
+
+ /**
+ * Absolute scale factor representing the nominal level of the center
+ * channel during a regular downmix.
+ */
+ double center_mix_level;
+
+ /**
+ * Absolute scale factor representing the nominal level of the center
+ * channel during an Lt/Rt compatible downmix.
+ */
+ double center_mix_level_ltrt;
+
+ /**
+ * Absolute scale factor representing the nominal level of the surround
+ * channels during a regular downmix.
+ */
+ double surround_mix_level;
+
+ /**
+ * Absolute scale factor representing the nominal level of the surround
+ * channels during an Lt/Rt compatible downmix.
+ */
+ double surround_mix_level_ltrt;
+
+ /**
+ * Absolute scale factor representing the level at which the LFE data is
+ * mixed into L/R channels during downmixing.
+ */
+ double lfe_mix_level;
+} AVDownmixInfo;
+
+/**
+ * Get a frame's AV_FRAME_DATA_DOWNMIX_INFO side data for editing.
+ *
+ * The side data is created and added to the frame if it's absent.
+ *
+ * @param frame the frame for which the side data is to be obtained.
+ *
+ * @return the AVDownmixInfo structure to be edited by the caller.
+ */
+AVDownmixInfo *av_downmix_info_update_side_data(AVFrame *frame);
+
+/**
+ * @}
+ */
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_DOWNMIX_INFO_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/error.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/error.h
new file mode 100644
index 0000000000..268a0320a8
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/error.h
@@ -0,0 +1,82 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * error code definitions
+ */
+
+#ifndef AVUTIL_ERROR_H
+#define AVUTIL_ERROR_H
+
+#include <errno.h>
+#include <stddef.h>
+
+/**
+ * @addtogroup lavu_error
+ *
+ * @{
+ */
+
+
+/* error handling */
+#if EDOM > 0
+#define AVERROR(e) (-(e)) ///< Returns a negative error code from a POSIX error code, to return from library functions.
+#define AVUNERROR(e) (-(e)) ///< Returns a POSIX error code from a library function error return value.
+#else
+/* Some platforms have E* and errno already negated. */
+#define AVERROR(e) (e)
+#define AVUNERROR(e) (e)
+#endif
+
+#define AVERROR_BSF_NOT_FOUND (-0x39acbd08) ///< Bitstream filter not found
+#define AVERROR_DECODER_NOT_FOUND (-0x3cbabb08) ///< Decoder not found
+#define AVERROR_DEMUXER_NOT_FOUND (-0x32babb08) ///< Demuxer not found
+#define AVERROR_ENCODER_NOT_FOUND (-0x3cb1ba08) ///< Encoder not found
+#define AVERROR_EOF (-0x5fb9b0bb) ///< End of file
+#define AVERROR_EXIT (-0x2bb6a7bb) ///< Immediate exit was requested; the called function should not be restarted
+#define AVERROR_FILTER_NOT_FOUND (-0x33b6b908) ///< Filter not found
+#define AVERROR_INVALIDDATA (-0x3ebbb1b7) ///< Invalid data found when processing input
+#define AVERROR_MUXER_NOT_FOUND (-0x27aab208) ///< Muxer not found
+#define AVERROR_OPTION_NOT_FOUND (-0x2bafb008) ///< Option not found
+#define AVERROR_PATCHWELCOME (-0x3aa8beb0) ///< Not yet implemented in Libav, patches welcome
+#define AVERROR_PROTOCOL_NOT_FOUND (-0x30adaf08) ///< Protocol not found
+#define AVERROR_STREAM_NOT_FOUND (-0x2dabac08) ///< Stream not found
+#define AVERROR_BUG (-0x5fb8aabe) ///< Bug detected, please report the issue
+#define AVERROR_UNKNOWN (-0x31b4b1ab) ///< Unknown error, typically from an external library
+#define AVERROR_EXPERIMENTAL (-0x2bb2afa8) ///< Requested feature is flagged experimental. Set strict_std_compliance if you really want to use it.
+
+/**
+ * Put a description of the AVERROR code errnum in errbuf.
+ * In case of failure the global variable errno is set to indicate the
+ * error. Even in case of failure av_strerror() will print a generic
+ * error message indicating the errnum provided to errbuf.
+ *
+ * @param errnum error code to describe
+ * @param errbuf buffer to which description is written
+ * @param errbuf_size the size in bytes of errbuf
+ * @return 0 on success, a negative value if a description for errnum
+ * cannot be found
+ */
+int av_strerror(int errnum, char *errbuf, size_t errbuf_size);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_ERROR_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/eval.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/eval.h
new file mode 100644
index 0000000000..ccb29e7a33
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/eval.h
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2002 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * simple arithmetic expression evaluator
+ */
+
+#ifndef AVUTIL_EVAL_H
+#define AVUTIL_EVAL_H
+
+#include "avutil.h"
+
+typedef struct AVExpr AVExpr;
+
+/**
+ * Parse and evaluate an expression.
+ * Note, this is significantly slower than av_expr_eval().
+ *
+ * @param res a pointer to a double where is put the result value of
+ * the expression, or NAN in case of error
+ * @param s expression as a zero terminated string, for example "1+2^3+5*5+sin(2/3)"
+ * @param const_names NULL terminated array of zero terminated strings of constant identifiers, for example {"PI", "E", 0}
+ * @param const_values a zero terminated array of values for the identifiers from const_names
+ * @param func1_names NULL terminated array of zero terminated strings of funcs1 identifiers
+ * @param funcs1 NULL terminated array of function pointers for functions which take 1 argument
+ * @param func2_names NULL terminated array of zero terminated strings of funcs2 identifiers
+ * @param funcs2 NULL terminated array of function pointers for functions which take 2 arguments
+ * @param opaque a pointer which will be passed to all functions from funcs1 and funcs2
+ * @param log_ctx parent logging context
+ * @return 0 in case of success, a negative value corresponding to an
+ * AVERROR code otherwise
+ */
+int av_expr_parse_and_eval(double *res, const char *s,
+ const char * const *const_names, const double *const_values,
+ const char * const *func1_names, double (* const *funcs1)(void *, double),
+ const char * const *func2_names, double (* const *funcs2)(void *, double, double),
+ void *opaque, int log_offset, void *log_ctx);
+
+/**
+ * Parse an expression.
+ *
+ * @param expr a pointer where is put an AVExpr containing the parsed
+ * value in case of successful parsing, or NULL otherwise.
+ * The pointed to AVExpr must be freed with av_expr_free() by the user
+ * when it is not needed anymore.
+ * @param s expression as a zero terminated string, for example "1+2^3+5*5+sin(2/3)"
+ * @param const_names NULL terminated array of zero terminated strings of constant identifiers, for example {"PI", "E", 0}
+ * @param func1_names NULL terminated array of zero terminated strings of funcs1 identifiers
+ * @param funcs1 NULL terminated array of function pointers for functions which take 1 argument
+ * @param func2_names NULL terminated array of zero terminated strings of funcs2 identifiers
+ * @param funcs2 NULL terminated array of function pointers for functions which take 2 arguments
+ * @param log_ctx parent logging context
+ * @return 0 in case of success, a negative value corresponding to an
+ * AVERROR code otherwise
+ */
+int av_expr_parse(AVExpr **expr, const char *s,
+ const char * const *const_names,
+ const char * const *func1_names, double (* const *funcs1)(void *, double),
+ const char * const *func2_names, double (* const *funcs2)(void *, double, double),
+ int log_offset, void *log_ctx);
+
+/**
+ * Evaluate a previously parsed expression.
+ *
+ * @param const_values a zero terminated array of values for the identifiers from av_expr_parse() const_names
+ * @param opaque a pointer which will be passed to all functions from funcs1 and funcs2
+ * @return the value of the expression
+ */
+double av_expr_eval(AVExpr *e, const double *const_values, void *opaque);
+
+/**
+ * Free a parsed expression previously created with av_expr_parse().
+ */
+void av_expr_free(AVExpr *e);
+
+/**
+ * Parse the string in numstr and return its value as a double. If
+ * the string is empty, contains only whitespaces, or does not contain
+ * an initial substring that has the expected syntax for a
+ * floating-point number, no conversion is performed. In this case,
+ * returns a value of zero and the value returned in tail is the value
+ * of numstr.
+ *
+ * @param numstr a string representing a number, may contain one of
+ * the International System number postfixes, for example 'K', 'M',
+ * 'G'. If 'i' is appended after the postfix, powers of 2 are used
+ * instead of powers of 10. The 'B' postfix multiplies the value for
+ * 8, and can be appended after another postfix or used alone. This
+ * allows using for example 'KB', 'MiB', 'G' and 'B' as postfix.
+ * @param tail if non-NULL puts here the pointer to the char next
+ * after the last parsed character
+ */
+double av_strtod(const char *numstr, char **tail);
+
+#endif /* AVUTIL_EVAL_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/fifo.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/fifo.h
new file mode 100644
index 0000000000..ea30f5d2bd
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/fifo.h
@@ -0,0 +1,131 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * a very simple circular buffer FIFO implementation
+ */
+
+#ifndef AVUTIL_FIFO_H
+#define AVUTIL_FIFO_H
+
+#include <stdint.h>
+#include "avutil.h"
+#include "attributes.h"
+
+typedef struct AVFifoBuffer {
+ uint8_t *buffer;
+ uint8_t *rptr, *wptr, *end;
+ uint32_t rndx, wndx;
+} AVFifoBuffer;
+
+/**
+ * Initialize an AVFifoBuffer.
+ * @param size of FIFO
+ * @return AVFifoBuffer or NULL in case of memory allocation failure
+ */
+AVFifoBuffer *av_fifo_alloc(unsigned int size);
+
+/**
+ * Free an AVFifoBuffer.
+ * @param f AVFifoBuffer to free
+ */
+void av_fifo_free(AVFifoBuffer *f);
+
+/**
+ * Reset the AVFifoBuffer to the state right after av_fifo_alloc, in particular it is emptied.
+ * @param f AVFifoBuffer to reset
+ */
+void av_fifo_reset(AVFifoBuffer *f);
+
+/**
+ * Return the amount of data in bytes in the AVFifoBuffer, that is the
+ * amount of data you can read from it.
+ * @param f AVFifoBuffer to read from
+ * @return size
+ */
+int av_fifo_size(AVFifoBuffer *f);
+
+/**
+ * Return the amount of space in bytes in the AVFifoBuffer, that is the
+ * amount of data you can write into it.
+ * @param f AVFifoBuffer to write into
+ * @return size
+ */
+int av_fifo_space(AVFifoBuffer *f);
+
+/**
+ * Feed data from an AVFifoBuffer to a user-supplied callback.
+ * @param f AVFifoBuffer to read from
+ * @param buf_size number of bytes to read
+ * @param func generic read function
+ * @param dest data destination
+ */
+int av_fifo_generic_read(AVFifoBuffer *f, void *dest, int buf_size, void (*func)(void*, void*, int));
+
+/**
+ * Feed data from a user-supplied callback to an AVFifoBuffer.
+ * @param f AVFifoBuffer to write to
+ * @param src data source; non-const since it may be used as a
+ * modifiable context by the function defined in func
+ * @param size number of bytes to write
+ * @param func generic write function; the first parameter is src,
+ * the second is dest_buf, the third is dest_buf_size.
+ * func must return the number of bytes written to dest_buf, or <= 0 to
+ * indicate no more data available to write.
+ * If func is NULL, src is interpreted as a simple byte array for source data.
+ * @return the number of bytes written to the FIFO
+ */
+int av_fifo_generic_write(AVFifoBuffer *f, void *src, int size, int (*func)(void*, void*, int));
+
+/**
+ * Resize an AVFifoBuffer.
+ * @param f AVFifoBuffer to resize
+ * @param size new AVFifoBuffer size in bytes
+ * @return <0 for failure, >=0 otherwise
+ */
+int av_fifo_realloc2(AVFifoBuffer *f, unsigned int size);
+
+/**
+ * Read and discard the specified amount of data from an AVFifoBuffer.
+ * @param f AVFifoBuffer to read from
+ * @param size amount of data to read in bytes
+ */
+void av_fifo_drain(AVFifoBuffer *f, int size);
+
+/**
+ * Return a pointer to the data stored in a FIFO buffer at a certain offset.
+ * The FIFO buffer is not modified.
+ *
+ * @param f AVFifoBuffer to peek at, f must be non-NULL
+ * @param offs an offset in bytes, its absolute value must be less
+ * than the used buffer size or the returned pointer will
+ * point outside to the buffer data.
+ * The used buffer size can be checked with av_fifo_size().
+ */
+static inline uint8_t *av_fifo_peek2(const AVFifoBuffer *f, int offs)
+{
+ uint8_t *ptr = f->rptr + offs;
+ if (ptr >= f->end)
+ ptr = f->buffer + (ptr - f->end);
+ else if (ptr < f->buffer)
+ ptr = f->end - (f->buffer - ptr);
+ return ptr;
+}
+
+#endif /* AVUTIL_FIFO_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/file.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/file.h
new file mode 100644
index 0000000000..e3f02a8308
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/file.h
@@ -0,0 +1,54 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_FILE_H
+#define AVUTIL_FILE_H
+
+#include <stdint.h>
+
+#include "avutil.h"
+
+/**
+ * @file
+ * Misc file utilities.
+ */
+
+/**
+ * Read the file with name filename, and put its content in a newly
+ * allocated buffer or map it with mmap() when available.
+ * In case of success set *bufptr to the read or mmapped buffer, and
+ * *size to the size in bytes of the buffer in *bufptr.
+ * The returned buffer must be released with av_file_unmap().
+ *
+ * @param log_offset loglevel offset used for logging
+ * @param log_ctx context used for logging
+ * @return a non negative number in case of success, a negative value
+ * corresponding to an AVERROR error code in case of failure
+ */
+int av_file_map(const char *filename, uint8_t **bufptr, size_t *size,
+ int log_offset, void *log_ctx);
+
+/**
+ * Unmap or free the buffer bufptr created by av_file_map().
+ *
+ * @param size size in bytes of bufptr, must be the same as returned
+ * by av_file_map()
+ */
+void av_file_unmap(uint8_t *bufptr, size_t size);
+
+#endif /* AVUTIL_FILE_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/frame.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/frame.h
new file mode 100644
index 0000000000..63ed219f47
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/frame.h
@@ -0,0 +1,552 @@
+/*
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * @ingroup lavu_frame
+ * reference-counted frame API
+ */
+
+#ifndef AVUTIL_FRAME_H
+#define AVUTIL_FRAME_H
+
+#include <stdint.h>
+
+#include "avutil.h"
+#include "buffer.h"
+#include "dict.h"
+#include "rational.h"
+#include "samplefmt.h"
+#include "version.h"
+
+
+/**
+ * @defgroup lavu_frame AVFrame
+ * @ingroup lavu_data
+ *
+ * @{
+ * AVFrame is an abstraction for reference-counted raw multimedia data.
+ */
+
+enum AVFrameSideDataType {
+ /**
+ * The data is the AVPanScan struct defined in libavcodec.
+ */
+ AV_FRAME_DATA_PANSCAN,
+ /**
+ * ATSC A53 Part 4 Closed Captions.
+ * A53 CC bitstream is stored as uint8_t in AVFrameSideData.data.
+ * The number of bytes of CC data is AVFrameSideData.size.
+ */
+ AV_FRAME_DATA_A53_CC,
+ /**
+ * Stereoscopic 3d metadata.
+ * The data is the AVStereo3D struct defined in libavutil/stereo3d.h.
+ */
+ AV_FRAME_DATA_STEREO3D,
+ /**
+ * The data is the AVMatrixEncoding enum defined in libavutil/channel_layout.h.
+ */
+ AV_FRAME_DATA_MATRIXENCODING,
+ /**
+ * Metadata relevant to a downmix procedure.
+ * The data is the AVDownmixInfo struct defined in libavutil/downmix_info.h.
+ */
+ AV_FRAME_DATA_DOWNMIX_INFO,
+};
+
+typedef struct AVFrameSideData {
+ enum AVFrameSideDataType type;
+ uint8_t *data;
+ int size;
+ AVDictionary *metadata;
+} AVFrameSideData;
+
+/**
+ * This structure describes decoded (raw) audio or video data.
+ *
+ * AVFrame must be allocated using av_frame_alloc(). Note that this only
+ * allocates the AVFrame itself, the buffers for the data must be managed
+ * through other means (see below).
+ * AVFrame must be freed with av_frame_free().
+ *
+ * AVFrame is typically allocated once and then reused multiple times to hold
+ * different data (e.g. a single AVFrame to hold frames received from a
+ * decoder). In such a case, av_frame_unref() will free any references held by
+ * the frame and reset it to its original clean state before it
+ * is reused again.
+ *
+ * The data described by an AVFrame is usually reference counted through the
+ * AVBuffer API. The underlying buffer references are stored in AVFrame.buf /
+ * AVFrame.extended_buf. An AVFrame is considered to be reference counted if at
+ * least one reference is set, i.e. if AVFrame.buf[0] != NULL. In such a case,
+ * every single data plane must be contained in one of the buffers in
+ * AVFrame.buf or AVFrame.extended_buf.
+ * There may be a single buffer for all the data, or one separate buffer for
+ * each plane, or anything in between.
+ *
+ * sizeof(AVFrame) is not a part of the public ABI, so new fields may be added
+ * to the end with a minor bump.
+ */
+typedef struct AVFrame {
+#define AV_NUM_DATA_POINTERS 8
+ /**
+ * pointer to the picture/channel planes.
+ * This might be different from the first allocated byte
+ */
+ uint8_t *data[AV_NUM_DATA_POINTERS];
+
+ /**
+ * For video, size in bytes of each picture line.
+ * For audio, size in bytes of each plane.
+ *
+ * For audio, only linesize[0] may be set. For planar audio, each channel
+ * plane must be the same size.
+ *
+ * @note The linesize may be larger than the size of usable data -- there
+ * may be extra padding present for performance reasons.
+ */
+ int linesize[AV_NUM_DATA_POINTERS];
+
+ /**
+ * pointers to the data planes/channels.
+ *
+ * For video, this should simply point to data[].
+ *
+ * For planar audio, each channel has a separate data pointer, and
+ * linesize[0] contains the size of each channel buffer.
+ * For packed audio, there is just one data pointer, and linesize[0]
+ * contains the total size of the buffer for all channels.
+ *
+ * Note: Both data and extended_data should always be set in a valid frame,
+ * but for planar audio with more channels that can fit in data,
+ * extended_data must be used in order to access all channels.
+ */
+ uint8_t **extended_data;
+
+ /**
+ * width and height of the video frame
+ */
+ int width, height;
+
+ /**
+ * number of audio samples (per channel) described by this frame
+ */
+ int nb_samples;
+
+ /**
+ * format of the frame, -1 if unknown or unset
+ * Values correspond to enum AVPixelFormat for video frames,
+ * enum AVSampleFormat for audio)
+ */
+ int format;
+
+ /**
+ * 1 -> keyframe, 0-> not
+ */
+ int key_frame;
+
+ /**
+ * Picture type of the frame.
+ */
+ enum AVPictureType pict_type;
+
+#if FF_API_AVFRAME_LAVC
+ attribute_deprecated
+ uint8_t *base[AV_NUM_DATA_POINTERS];
+#endif
+
+ /**
+ * Sample aspect ratio for the video frame, 0/1 if unknown/unspecified.
+ */
+ AVRational sample_aspect_ratio;
+
+ /**
+ * Presentation timestamp in time_base units (time when frame should be shown to user).
+ */
+ int64_t pts;
+
+ /**
+ * PTS copied from the AVPacket that was decoded to produce this frame.
+ */
+ int64_t pkt_pts;
+
+ /**
+ * DTS copied from the AVPacket that triggered returning this frame.
+ */
+ int64_t pkt_dts;
+
+ /**
+ * picture number in bitstream order
+ */
+ int coded_picture_number;
+ /**
+ * picture number in display order
+ */
+ int display_picture_number;
+
+ /**
+ * quality (between 1 (good) and FF_LAMBDA_MAX (bad))
+ */
+ int quality;
+
+#if FF_API_AVFRAME_LAVC
+ attribute_deprecated
+ int reference;
+
+ /**
+ * QP table
+ */
+ attribute_deprecated
+ int8_t *qscale_table;
+ /**
+ * QP store stride
+ */
+ attribute_deprecated
+ int qstride;
+
+ attribute_deprecated
+ int qscale_type;
+
+ /**
+ * mbskip_table[mb]>=1 if MB didn't change
+ * stride= mb_width = (width+15)>>4
+ */
+ attribute_deprecated
+ uint8_t *mbskip_table;
+
+ /**
+ * motion vector table
+ * @code
+ * example:
+ * int mv_sample_log2= 4 - motion_subsample_log2;
+ * int mb_width= (width+15)>>4;
+ * int mv_stride= (mb_width << mv_sample_log2) + 1;
+ * motion_val[direction][x + y*mv_stride][0->mv_x, 1->mv_y];
+ * @endcode
+ */
+ attribute_deprecated
+ int16_t (*motion_val[2])[2];
+
+ /**
+ * macroblock type table
+ * mb_type_base + mb_width + 2
+ */
+ attribute_deprecated
+ uint32_t *mb_type;
+
+ /**
+ * DCT coefficients
+ */
+ attribute_deprecated
+ short *dct_coeff;
+
+ /**
+ * motion reference frame index
+ * the order in which these are stored can depend on the codec.
+ */
+ attribute_deprecated
+ int8_t *ref_index[2];
+#endif
+
+ /**
+ * for some private data of the user
+ */
+ void *opaque;
+
+ /**
+ * error
+ */
+ uint64_t error[AV_NUM_DATA_POINTERS];
+
+#if FF_API_AVFRAME_LAVC
+ attribute_deprecated
+ int type;
+#endif
+
+ /**
+ * When decoding, this signals how much the picture must be delayed.
+ * extra_delay = repeat_pict / (2*fps)
+ */
+ int repeat_pict;
+
+ /**
+ * The content of the picture is interlaced.
+ */
+ int interlaced_frame;
+
+ /**
+ * If the content is interlaced, is top field displayed first.
+ */
+ int top_field_first;
+
+ /**
+ * Tell user application that palette has changed from previous frame.
+ */
+ int palette_has_changed;
+
+#if FF_API_AVFRAME_LAVC
+ attribute_deprecated
+ int buffer_hints;
+
+ /**
+ * Pan scan.
+ */
+ attribute_deprecated
+ struct AVPanScan *pan_scan;
+#endif
+
+ /**
+ * reordered opaque 64bit (generally an integer or a double precision float
+ * PTS but can be anything).
+ * The user sets AVCodecContext.reordered_opaque to represent the input at
+ * that time,
+ * the decoder reorders values as needed and sets AVFrame.reordered_opaque
+ * to exactly one of the values provided by the user through AVCodecContext.reordered_opaque
+ * @deprecated in favor of pkt_pts
+ */
+ int64_t reordered_opaque;
+
+#if FF_API_AVFRAME_LAVC
+ /**
+ * @deprecated this field is unused
+ */
+ attribute_deprecated void *hwaccel_picture_private;
+
+ attribute_deprecated
+ struct AVCodecContext *owner;
+ attribute_deprecated
+ void *thread_opaque;
+
+ /**
+ * log2 of the size of the block which a single vector in motion_val represents:
+ * (4->16x16, 3->8x8, 2-> 4x4, 1-> 2x2)
+ */
+ attribute_deprecated
+ uint8_t motion_subsample_log2;
+#endif
+
+ /**
+ * Sample rate of the audio data.
+ */
+ int sample_rate;
+
+ /**
+ * Channel layout of the audio data.
+ */
+ uint64_t channel_layout;
+
+ /**
+ * AVBuffer references backing the data for this frame. If all elements of
+ * this array are NULL, then this frame is not reference counted.
+ *
+ * There may be at most one AVBuffer per data plane, so for video this array
+ * always contains all the references. For planar audio with more than
+ * AV_NUM_DATA_POINTERS channels, there may be more buffers than can fit in
+ * this array. Then the extra AVBufferRef pointers are stored in the
+ * extended_buf array.
+ */
+ AVBufferRef *buf[AV_NUM_DATA_POINTERS];
+
+ /**
+ * For planar audio which requires more than AV_NUM_DATA_POINTERS
+ * AVBufferRef pointers, this array will hold all the references which
+ * cannot fit into AVFrame.buf.
+ *
+ * Note that this is different from AVFrame.extended_data, which always
+ * contains all the pointers. This array only contains the extra pointers,
+ * which cannot fit into AVFrame.buf.
+ *
+ * This array is always allocated using av_malloc() by whoever constructs
+ * the frame. It is freed in av_frame_unref().
+ */
+ AVBufferRef **extended_buf;
+ /**
+ * Number of elements in extended_buf.
+ */
+ int nb_extended_buf;
+
+ AVFrameSideData **side_data;
+ int nb_side_data;
+
+/**
+ * @defgroup lavu_frame_flags AV_FRAME_FLAGS
+ * Flags describing additional frame properties.
+ *
+ * @{
+ */
+
+/**
+ * The frame data may be corrupted, e.g. due to decoding errors.
+ */
+#define AV_FRAME_FLAG_CORRUPT (1 << 0)
+/**
+ * @}
+ */
+
+ /**
+ * Frame flags, a combination of @ref lavu_frame_flags
+ */
+ int flags;
+} AVFrame;
+
+/**
+ * Allocate an AVFrame and set its fields to default values. The resulting
+ * struct must be freed using av_frame_free().
+ *
+ * @return An AVFrame filled with default values or NULL on failure.
+ *
+ * @note this only allocates the AVFrame itself, not the data buffers. Those
+ * must be allocated through other means, e.g. with av_frame_get_buffer() or
+ * manually.
+ */
+AVFrame *av_frame_alloc(void);
+
+/**
+ * Free the frame and any dynamically allocated objects in it,
+ * e.g. extended_data. If the frame is reference counted, it will be
+ * unreferenced first.
+ *
+ * @param frame frame to be freed. The pointer will be set to NULL.
+ */
+void av_frame_free(AVFrame **frame);
+
+/**
+ * Set up a new reference to the data described by the source frame.
+ *
+ * Copy frame properties from src to dst and create a new reference for each
+ * AVBufferRef from src.
+ *
+ * If src is not reference counted, new buffers are allocated and the data is
+ * copied.
+ *
+ * @return 0 on success, a negative AVERROR on error
+ */
+int av_frame_ref(AVFrame *dst, const AVFrame *src);
+
+/**
+ * Create a new frame that references the same data as src.
+ *
+ * This is a shortcut for av_frame_alloc()+av_frame_ref().
+ *
+ * @return newly created AVFrame on success, NULL on error.
+ */
+AVFrame *av_frame_clone(const AVFrame *src);
+
+/**
+ * Unreference all the buffers referenced by frame and reset the frame fields.
+ */
+void av_frame_unref(AVFrame *frame);
+
+/**
+ * Move everythnig contained in src to dst and reset src.
+ */
+void av_frame_move_ref(AVFrame *dst, AVFrame *src);
+
+/**
+ * Allocate new buffer(s) for audio or video data.
+ *
+ * The following fields must be set on frame before calling this function:
+ * - format (pixel format for video, sample format for audio)
+ * - width and height for video
+ * - nb_samples and channel_layout for audio
+ *
+ * This function will fill AVFrame.data and AVFrame.buf arrays and, if
+ * necessary, allocate and fill AVFrame.extended_data and AVFrame.extended_buf.
+ * For planar formats, one buffer will be allocated for each plane.
+ *
+ * @param frame frame in which to store the new buffers.
+ * @param align required buffer size alignment
+ *
+ * @return 0 on success, a negative AVERROR on error.
+ */
+int av_frame_get_buffer(AVFrame *frame, int align);
+
+/**
+ * Check if the frame data is writable.
+ *
+ * @return A positive value if the frame data is writable (which is true if and
+ * only if each of the underlying buffers has only one reference, namely the one
+ * stored in this frame). Return 0 otherwise.
+ *
+ * If 1 is returned the answer is valid until av_buffer_ref() is called on any
+ * of the underlying AVBufferRefs (e.g. through av_frame_ref() or directly).
+ *
+ * @see av_frame_make_writable(), av_buffer_is_writable()
+ */
+int av_frame_is_writable(AVFrame *frame);
+
+/**
+ * Ensure that the frame data is writable, avoiding data copy if possible.
+ *
+ * Do nothing if the frame is writable, allocate new buffers and copy the data
+ * if it is not.
+ *
+ * @return 0 on success, a negative AVERROR on error.
+ *
+ * @see av_frame_is_writable(), av_buffer_is_writable(),
+ * av_buffer_make_writable()
+ */
+int av_frame_make_writable(AVFrame *frame);
+
+/**
+ * Copy only "metadata" fields from src to dst.
+ *
+ * Metadata for the purpose of this function are those fields that do not affect
+ * the data layout in the buffers. E.g. pts, sample rate (for audio) or sample
+ * aspect ratio (for video), but not width/height or channel layout.
+ * Side data is also copied.
+ */
+int av_frame_copy_props(AVFrame *dst, const AVFrame *src);
+
+/**
+ * Get the buffer reference a given data plane is stored in.
+ *
+ * @param plane index of the data plane of interest in frame->extended_data.
+ *
+ * @return the buffer reference that contains the plane or NULL if the input
+ * frame is not valid.
+ */
+AVBufferRef *av_frame_get_plane_buffer(AVFrame *frame, int plane);
+
+/**
+ * Add a new side data to a frame.
+ *
+ * @param frame a frame to which the side data should be added
+ * @param type type of the added side data
+ * @param size size of the side data
+ *
+ * @return newly added side data on success, NULL on error
+ */
+AVFrameSideData *av_frame_new_side_data(AVFrame *frame,
+ enum AVFrameSideDataType type,
+ int size);
+
+/**
+ * @return a pointer to the side data of a given type on success, NULL if there
+ * is no side data with such type in this frame.
+ */
+AVFrameSideData *av_frame_get_side_data(const AVFrame *frame,
+ enum AVFrameSideDataType type);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_FRAME_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/hmac.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/hmac.h
new file mode 100644
index 0000000000..28c2062b1b
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/hmac.h
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2012 Martin Storsjo
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_HMAC_H
+#define AVUTIL_HMAC_H
+
+#include <stdint.h>
+
+/**
+ * @defgroup lavu_hmac HMAC
+ * @ingroup lavu_crypto
+ * @{
+ */
+
+enum AVHMACType {
+ AV_HMAC_MD5,
+ AV_HMAC_SHA1,
+};
+
+typedef struct AVHMAC AVHMAC;
+
+/**
+ * Allocate an AVHMAC context.
+ * @param type The hash function used for the HMAC.
+ */
+AVHMAC *av_hmac_alloc(enum AVHMACType type);
+
+/**
+ * Free an AVHMAC context.
+ * @param ctx The context to free, may be NULL
+ */
+void av_hmac_free(AVHMAC *ctx);
+
+/**
+ * Initialize an AVHMAC context with an authentication key.
+ * @param ctx The HMAC context
+ * @param key The authentication key
+ * @param keylen The length of the key, in bytes
+ */
+void av_hmac_init(AVHMAC *ctx, const uint8_t *key, unsigned int keylen);
+
+/**
+ * Hash data with the HMAC.
+ * @param ctx The HMAC context
+ * @param data The data to hash
+ * @param len The length of the data, in bytes
+ */
+void av_hmac_update(AVHMAC *ctx, const uint8_t *data, unsigned int len);
+
+/**
+ * Finish hashing and output the HMAC digest.
+ * @param ctx The HMAC context
+ * @param out The output buffer to write the digest into
+ * @param outlen The length of the out buffer, in bytes
+ * @return The number of bytes written to out, or a negative error code.
+ */
+int av_hmac_final(AVHMAC *ctx, uint8_t *out, unsigned int outlen);
+
+/**
+ * Hash an array of data with a key.
+ * @param ctx The HMAC context
+ * @param data The data to hash
+ * @param len The length of the data, in bytes
+ * @param key The authentication key
+ * @param keylen The length of the key, in bytes
+ * @param out The output buffer to write the digest into
+ * @param outlen The length of the out buffer, in bytes
+ * @return The number of bytes written to out, or a negative error code.
+ */
+int av_hmac_calc(AVHMAC *ctx, const uint8_t *data, unsigned int len,
+ const uint8_t *key, unsigned int keylen,
+ uint8_t *out, unsigned int outlen);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_HMAC_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/imgutils.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/imgutils.h
new file mode 100644
index 0000000000..71510132ae
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/imgutils.h
@@ -0,0 +1,138 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_IMGUTILS_H
+#define AVUTIL_IMGUTILS_H
+
+/**
+ * @file
+ * misc image utilities
+ *
+ * @addtogroup lavu_picture
+ * @{
+ */
+
+#include "avutil.h"
+#include "pixdesc.h"
+
+/**
+ * Compute the max pixel step for each plane of an image with a
+ * format described by pixdesc.
+ *
+ * The pixel step is the distance in bytes between the first byte of
+ * the group of bytes which describe a pixel component and the first
+ * byte of the successive group in the same plane for the same
+ * component.
+ *
+ * @param max_pixsteps an array which is filled with the max pixel step
+ * for each plane. Since a plane may contain different pixel
+ * components, the computed max_pixsteps[plane] is relative to the
+ * component in the plane with the max pixel step.
+ * @param max_pixstep_comps an array which is filled with the component
+ * for each plane which has the max pixel step. May be NULL.
+ */
+void av_image_fill_max_pixsteps(int max_pixsteps[4], int max_pixstep_comps[4],
+ const AVPixFmtDescriptor *pixdesc);
+
+/**
+ * Compute the size of an image line with format pix_fmt and width
+ * width for the plane plane.
+ *
+ * @return the computed size in bytes
+ */
+int av_image_get_linesize(enum AVPixelFormat pix_fmt, int width, int plane);
+
+/**
+ * Fill plane linesizes for an image with pixel format pix_fmt and
+ * width width.
+ *
+ * @param linesizes array to be filled with the linesize for each plane
+ * @return >= 0 in case of success, a negative error code otherwise
+ */
+int av_image_fill_linesizes(int linesizes[4], enum AVPixelFormat pix_fmt, int width);
+
+/**
+ * Fill plane data pointers for an image with pixel format pix_fmt and
+ * height height.
+ *
+ * @param data pointers array to be filled with the pointer for each image plane
+ * @param ptr the pointer to a buffer which will contain the image
+ * @param linesizes the array containing the linesize for each
+ * plane, should be filled by av_image_fill_linesizes()
+ * @return the size in bytes required for the image buffer, a negative
+ * error code in case of failure
+ */
+int av_image_fill_pointers(uint8_t *data[4], enum AVPixelFormat pix_fmt, int height,
+ uint8_t *ptr, const int linesizes[4]);
+
+/**
+ * Allocate an image with size w and h and pixel format pix_fmt, and
+ * fill pointers and linesizes accordingly.
+ * The allocated image buffer has to be freed by using
+ * av_freep(&pointers[0]).
+ *
+ * @param align the value to use for buffer size alignment
+ * @return the size in bytes required for the image buffer, a negative
+ * error code in case of failure
+ */
+int av_image_alloc(uint8_t *pointers[4], int linesizes[4],
+ int w, int h, enum AVPixelFormat pix_fmt, int align);
+
+/**
+ * Copy image plane from src to dst.
+ * That is, copy "height" number of lines of "bytewidth" bytes each.
+ * The first byte of each successive line is separated by *_linesize
+ * bytes.
+ *
+ * @param dst_linesize linesize for the image plane in dst
+ * @param src_linesize linesize for the image plane in src
+ */
+void av_image_copy_plane(uint8_t *dst, int dst_linesize,
+ const uint8_t *src, int src_linesize,
+ int bytewidth, int height);
+
+/**
+ * Copy image in src_data to dst_data.
+ *
+ * @param dst_linesizes linesizes for the image in dst_data
+ * @param src_linesizes linesizes for the image in src_data
+ */
+void av_image_copy(uint8_t *dst_data[4], int dst_linesizes[4],
+ const uint8_t *src_data[4], const int src_linesizes[4],
+ enum AVPixelFormat pix_fmt, int width, int height);
+
+/**
+ * Check if the given dimension of an image is valid, meaning that all
+ * bytes of the image can be addressed with a signed int.
+ *
+ * @param w the width of the picture
+ * @param h the height of the picture
+ * @param log_offset the offset to sum to the log level for logging with log_ctx
+ * @param log_ctx the parent logging context, it may be NULL
+ * @return >= 0 if valid, a negative error code otherwise
+ */
+int av_image_check_size(unsigned int w, unsigned int h, int log_offset, void *log_ctx);
+
+int avpriv_set_systematic_pal2(uint32_t pal[256], enum AVPixelFormat pix_fmt);
+
+/**
+ * @}
+ */
+
+
+#endif /* AVUTIL_IMGUTILS_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/intfloat.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/intfloat.h
new file mode 100644
index 0000000000..38d26ad87e
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/intfloat.h
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2011 Mans Rullgard
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_INTFLOAT_H
+#define AVUTIL_INTFLOAT_H
+
+#include <stdint.h>
+#include "attributes.h"
+
+union av_intfloat32 {
+ uint32_t i;
+ float f;
+};
+
+union av_intfloat64 {
+ uint64_t i;
+ double f;
+};
+
+/**
+ * Reinterpret a 32-bit integer as a float.
+ */
+static av_always_inline float av_int2float(uint32_t i)
+{
+ union av_intfloat32 v;
+ v.i = i;
+ return v.f;
+}
+
+/**
+ * Reinterpret a float as a 32-bit integer.
+ */
+static av_always_inline uint32_t av_float2int(float f)
+{
+ union av_intfloat32 v;
+ v.f = f;
+ return v.i;
+}
+
+/**
+ * Reinterpret a 64-bit integer as a double.
+ */
+static av_always_inline double av_int2double(uint64_t i)
+{
+ union av_intfloat64 v;
+ v.i = i;
+ return v.f;
+}
+
+/**
+ * Reinterpret a double as a 64-bit integer.
+ */
+static av_always_inline uint64_t av_double2int(double f)
+{
+ union av_intfloat64 v;
+ v.f = f;
+ return v.i;
+}
+
+#endif /* AVUTIL_INTFLOAT_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/intreadwrite.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/intreadwrite.h
new file mode 100644
index 0000000000..f77fd60f38
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/intreadwrite.h
@@ -0,0 +1,549 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_INTREADWRITE_H
+#define AVUTIL_INTREADWRITE_H
+
+#include <stdint.h>
+#include "libavutil/avconfig.h"
+#include "attributes.h"
+#include "bswap.h"
+
+typedef union {
+ uint64_t u64;
+ uint32_t u32[2];
+ uint16_t u16[4];
+ uint8_t u8 [8];
+ double f64;
+ float f32[2];
+} av_alias av_alias64;
+
+typedef union {
+ uint32_t u32;
+ uint16_t u16[2];
+ uint8_t u8 [4];
+ float f32;
+} av_alias av_alias32;
+
+typedef union {
+ uint16_t u16;
+ uint8_t u8 [2];
+} av_alias av_alias16;
+
+/*
+ * Arch-specific headers can provide any combination of
+ * AV_[RW][BLN](16|24|32|64) and AV_(COPY|SWAP|ZERO)(64|128) macros.
+ * Preprocessor symbols must be defined, even if these are implemented
+ * as inline functions.
+ */
+
+#ifdef HAVE_AV_CONFIG_H
+
+#include "config.h"
+
+#if ARCH_ARM
+# include "arm/intreadwrite.h"
+#elif ARCH_AVR32
+# include "avr32/intreadwrite.h"
+#elif ARCH_MIPS
+# include "mips/intreadwrite.h"
+#elif ARCH_PPC
+# include "ppc/intreadwrite.h"
+#elif ARCH_TOMI
+# include "tomi/intreadwrite.h"
+#elif ARCH_X86
+# include "x86/intreadwrite.h"
+#endif
+
+#endif /* HAVE_AV_CONFIG_H */
+
+/*
+ * Map AV_RNXX <-> AV_R[BL]XX for all variants provided by per-arch headers.
+ */
+
+#if AV_HAVE_BIGENDIAN
+
+# if defined(AV_RN16) && !defined(AV_RB16)
+# define AV_RB16(p) AV_RN16(p)
+# elif !defined(AV_RN16) && defined(AV_RB16)
+# define AV_RN16(p) AV_RB16(p)
+# endif
+
+# if defined(AV_WN16) && !defined(AV_WB16)
+# define AV_WB16(p, v) AV_WN16(p, v)
+# elif !defined(AV_WN16) && defined(AV_WB16)
+# define AV_WN16(p, v) AV_WB16(p, v)
+# endif
+
+# if defined(AV_RN24) && !defined(AV_RB24)
+# define AV_RB24(p) AV_RN24(p)
+# elif !defined(AV_RN24) && defined(AV_RB24)
+# define AV_RN24(p) AV_RB24(p)
+# endif
+
+# if defined(AV_WN24) && !defined(AV_WB24)
+# define AV_WB24(p, v) AV_WN24(p, v)
+# elif !defined(AV_WN24) && defined(AV_WB24)
+# define AV_WN24(p, v) AV_WB24(p, v)
+# endif
+
+# if defined(AV_RN32) && !defined(AV_RB32)
+# define AV_RB32(p) AV_RN32(p)
+# elif !defined(AV_RN32) && defined(AV_RB32)
+# define AV_RN32(p) AV_RB32(p)
+# endif
+
+# if defined(AV_WN32) && !defined(AV_WB32)
+# define AV_WB32(p, v) AV_WN32(p, v)
+# elif !defined(AV_WN32) && defined(AV_WB32)
+# define AV_WN32(p, v) AV_WB32(p, v)
+# endif
+
+# if defined(AV_RN64) && !defined(AV_RB64)
+# define AV_RB64(p) AV_RN64(p)
+# elif !defined(AV_RN64) && defined(AV_RB64)
+# define AV_RN64(p) AV_RB64(p)
+# endif
+
+# if defined(AV_WN64) && !defined(AV_WB64)
+# define AV_WB64(p, v) AV_WN64(p, v)
+# elif !defined(AV_WN64) && defined(AV_WB64)
+# define AV_WN64(p, v) AV_WB64(p, v)
+# endif
+
+#else /* AV_HAVE_BIGENDIAN */
+
+# if defined(AV_RN16) && !defined(AV_RL16)
+# define AV_RL16(p) AV_RN16(p)
+# elif !defined(AV_RN16) && defined(AV_RL16)
+# define AV_RN16(p) AV_RL16(p)
+# endif
+
+# if defined(AV_WN16) && !defined(AV_WL16)
+# define AV_WL16(p, v) AV_WN16(p, v)
+# elif !defined(AV_WN16) && defined(AV_WL16)
+# define AV_WN16(p, v) AV_WL16(p, v)
+# endif
+
+# if defined(AV_RN24) && !defined(AV_RL24)
+# define AV_RL24(p) AV_RN24(p)
+# elif !defined(AV_RN24) && defined(AV_RL24)
+# define AV_RN24(p) AV_RL24(p)
+# endif
+
+# if defined(AV_WN24) && !defined(AV_WL24)
+# define AV_WL24(p, v) AV_WN24(p, v)
+# elif !defined(AV_WN24) && defined(AV_WL24)
+# define AV_WN24(p, v) AV_WL24(p, v)
+# endif
+
+# if defined(AV_RN32) && !defined(AV_RL32)
+# define AV_RL32(p) AV_RN32(p)
+# elif !defined(AV_RN32) && defined(AV_RL32)
+# define AV_RN32(p) AV_RL32(p)
+# endif
+
+# if defined(AV_WN32) && !defined(AV_WL32)
+# define AV_WL32(p, v) AV_WN32(p, v)
+# elif !defined(AV_WN32) && defined(AV_WL32)
+# define AV_WN32(p, v) AV_WL32(p, v)
+# endif
+
+# if defined(AV_RN64) && !defined(AV_RL64)
+# define AV_RL64(p) AV_RN64(p)
+# elif !defined(AV_RN64) && defined(AV_RL64)
+# define AV_RN64(p) AV_RL64(p)
+# endif
+
+# if defined(AV_WN64) && !defined(AV_WL64)
+# define AV_WL64(p, v) AV_WN64(p, v)
+# elif !defined(AV_WN64) && defined(AV_WL64)
+# define AV_WN64(p, v) AV_WL64(p, v)
+# endif
+
+#endif /* !AV_HAVE_BIGENDIAN */
+
+/*
+ * Define AV_[RW]N helper macros to simplify definitions not provided
+ * by per-arch headers.
+ */
+
+#if defined(__GNUC__) && !defined(__TI_COMPILER_VERSION__)
+
+union unaligned_64 { uint64_t l; } __attribute__((packed)) av_alias;
+union unaligned_32 { uint32_t l; } __attribute__((packed)) av_alias;
+union unaligned_16 { uint16_t l; } __attribute__((packed)) av_alias;
+
+# define AV_RN(s, p) (((const union unaligned_##s *) (p))->l)
+# define AV_WN(s, p, v) ((((union unaligned_##s *) (p))->l) = (v))
+
+#elif defined(__DECC)
+
+# define AV_RN(s, p) (*((const __unaligned uint##s##_t*)(p)))
+# define AV_WN(s, p, v) (*((__unaligned uint##s##_t*)(p)) = (v))
+
+#elif AV_HAVE_FAST_UNALIGNED
+
+# define AV_RN(s, p) (((const av_alias##s*)(p))->u##s)
+# define AV_WN(s, p, v) (((av_alias##s*)(p))->u##s = (v))
+
+#else
+
+#ifndef AV_RB16
+# define AV_RB16(x) \
+ ((((const uint8_t*)(x))[0] << 8) | \
+ ((const uint8_t*)(x))[1])
+#endif
+#ifndef AV_WB16
+# define AV_WB16(p, d) do { \
+ ((uint8_t*)(p))[1] = (d); \
+ ((uint8_t*)(p))[0] = (d)>>8; \
+ } while(0)
+#endif
+
+#ifndef AV_RL16
+# define AV_RL16(x) \
+ ((((const uint8_t*)(x))[1] << 8) | \
+ ((const uint8_t*)(x))[0])
+#endif
+#ifndef AV_WL16
+# define AV_WL16(p, d) do { \
+ ((uint8_t*)(p))[0] = (d); \
+ ((uint8_t*)(p))[1] = (d)>>8; \
+ } while(0)
+#endif
+
+#ifndef AV_RB32
+# define AV_RB32(x) \
+ (((uint32_t)((const uint8_t*)(x))[0] << 24) | \
+ (((const uint8_t*)(x))[1] << 16) | \
+ (((const uint8_t*)(x))[2] << 8) | \
+ ((const uint8_t*)(x))[3])
+#endif
+#ifndef AV_WB32
+# define AV_WB32(p, d) do { \
+ ((uint8_t*)(p))[3] = (d); \
+ ((uint8_t*)(p))[2] = (d)>>8; \
+ ((uint8_t*)(p))[1] = (d)>>16; \
+ ((uint8_t*)(p))[0] = (d)>>24; \
+ } while(0)
+#endif
+
+#ifndef AV_RL32
+# define AV_RL32(x) \
+ (((uint32_t)((const uint8_t*)(x))[3] << 24) | \
+ (((const uint8_t*)(x))[2] << 16) | \
+ (((const uint8_t*)(x))[1] << 8) | \
+ ((const uint8_t*)(x))[0])
+#endif
+#ifndef AV_WL32
+# define AV_WL32(p, d) do { \
+ ((uint8_t*)(p))[0] = (d); \
+ ((uint8_t*)(p))[1] = (d)>>8; \
+ ((uint8_t*)(p))[2] = (d)>>16; \
+ ((uint8_t*)(p))[3] = (d)>>24; \
+ } while(0)
+#endif
+
+#ifndef AV_RB64
+# define AV_RB64(x) \
+ (((uint64_t)((const uint8_t*)(x))[0] << 56) | \
+ ((uint64_t)((const uint8_t*)(x))[1] << 48) | \
+ ((uint64_t)((const uint8_t*)(x))[2] << 40) | \
+ ((uint64_t)((const uint8_t*)(x))[3] << 32) | \
+ ((uint64_t)((const uint8_t*)(x))[4] << 24) | \
+ ((uint64_t)((const uint8_t*)(x))[5] << 16) | \
+ ((uint64_t)((const uint8_t*)(x))[6] << 8) | \
+ (uint64_t)((const uint8_t*)(x))[7])
+#endif
+#ifndef AV_WB64
+# define AV_WB64(p, d) do { \
+ ((uint8_t*)(p))[7] = (d); \
+ ((uint8_t*)(p))[6] = (d)>>8; \
+ ((uint8_t*)(p))[5] = (d)>>16; \
+ ((uint8_t*)(p))[4] = (d)>>24; \
+ ((uint8_t*)(p))[3] = (d)>>32; \
+ ((uint8_t*)(p))[2] = (d)>>40; \
+ ((uint8_t*)(p))[1] = (d)>>48; \
+ ((uint8_t*)(p))[0] = (d)>>56; \
+ } while(0)
+#endif
+
+#ifndef AV_RL64
+# define AV_RL64(x) \
+ (((uint64_t)((const uint8_t*)(x))[7] << 56) | \
+ ((uint64_t)((const uint8_t*)(x))[6] << 48) | \
+ ((uint64_t)((const uint8_t*)(x))[5] << 40) | \
+ ((uint64_t)((const uint8_t*)(x))[4] << 32) | \
+ ((uint64_t)((const uint8_t*)(x))[3] << 24) | \
+ ((uint64_t)((const uint8_t*)(x))[2] << 16) | \
+ ((uint64_t)((const uint8_t*)(x))[1] << 8) | \
+ (uint64_t)((const uint8_t*)(x))[0])
+#endif
+#ifndef AV_WL64
+# define AV_WL64(p, d) do { \
+ ((uint8_t*)(p))[0] = (d); \
+ ((uint8_t*)(p))[1] = (d)>>8; \
+ ((uint8_t*)(p))[2] = (d)>>16; \
+ ((uint8_t*)(p))[3] = (d)>>24; \
+ ((uint8_t*)(p))[4] = (d)>>32; \
+ ((uint8_t*)(p))[5] = (d)>>40; \
+ ((uint8_t*)(p))[6] = (d)>>48; \
+ ((uint8_t*)(p))[7] = (d)>>56; \
+ } while(0)
+#endif
+
+#if AV_HAVE_BIGENDIAN
+# define AV_RN(s, p) AV_RB##s(p)
+# define AV_WN(s, p, v) AV_WB##s(p, v)
+#else
+# define AV_RN(s, p) AV_RL##s(p)
+# define AV_WN(s, p, v) AV_WL##s(p, v)
+#endif
+
+#endif /* HAVE_FAST_UNALIGNED */
+
+#ifndef AV_RN16
+# define AV_RN16(p) AV_RN(16, p)
+#endif
+
+#ifndef AV_RN32
+# define AV_RN32(p) AV_RN(32, p)
+#endif
+
+#ifndef AV_RN64
+# define AV_RN64(p) AV_RN(64, p)
+#endif
+
+#ifndef AV_WN16
+# define AV_WN16(p, v) AV_WN(16, p, v)
+#endif
+
+#ifndef AV_WN32
+# define AV_WN32(p, v) AV_WN(32, p, v)
+#endif
+
+#ifndef AV_WN64
+# define AV_WN64(p, v) AV_WN(64, p, v)
+#endif
+
+#if AV_HAVE_BIGENDIAN
+# define AV_RB(s, p) AV_RN##s(p)
+# define AV_WB(s, p, v) AV_WN##s(p, v)
+# define AV_RL(s, p) av_bswap##s(AV_RN##s(p))
+# define AV_WL(s, p, v) AV_WN##s(p, av_bswap##s(v))
+#else
+# define AV_RB(s, p) av_bswap##s(AV_RN##s(p))
+# define AV_WB(s, p, v) AV_WN##s(p, av_bswap##s(v))
+# define AV_RL(s, p) AV_RN##s(p)
+# define AV_WL(s, p, v) AV_WN##s(p, v)
+#endif
+
+#define AV_RB8(x) (((const uint8_t*)(x))[0])
+#define AV_WB8(p, d) do { ((uint8_t*)(p))[0] = (d); } while(0)
+
+#define AV_RL8(x) AV_RB8(x)
+#define AV_WL8(p, d) AV_WB8(p, d)
+
+#ifndef AV_RB16
+# define AV_RB16(p) AV_RB(16, p)
+#endif
+#ifndef AV_WB16
+# define AV_WB16(p, v) AV_WB(16, p, v)
+#endif
+
+#ifndef AV_RL16
+# define AV_RL16(p) AV_RL(16, p)
+#endif
+#ifndef AV_WL16
+# define AV_WL16(p, v) AV_WL(16, p, v)
+#endif
+
+#ifndef AV_RB32
+# define AV_RB32(p) AV_RB(32, p)
+#endif
+#ifndef AV_WB32
+# define AV_WB32(p, v) AV_WB(32, p, v)
+#endif
+
+#ifndef AV_RL32
+# define AV_RL32(p) AV_RL(32, p)
+#endif
+#ifndef AV_WL32
+# define AV_WL32(p, v) AV_WL(32, p, v)
+#endif
+
+#ifndef AV_RB64
+# define AV_RB64(p) AV_RB(64, p)
+#endif
+#ifndef AV_WB64
+# define AV_WB64(p, v) AV_WB(64, p, v)
+#endif
+
+#ifndef AV_RL64
+# define AV_RL64(p) AV_RL(64, p)
+#endif
+#ifndef AV_WL64
+# define AV_WL64(p, v) AV_WL(64, p, v)
+#endif
+
+#ifndef AV_RB24
+# define AV_RB24(x) \
+ ((((const uint8_t*)(x))[0] << 16) | \
+ (((const uint8_t*)(x))[1] << 8) | \
+ ((const uint8_t*)(x))[2])
+#endif
+#ifndef AV_WB24
+# define AV_WB24(p, d) do { \
+ ((uint8_t*)(p))[2] = (d); \
+ ((uint8_t*)(p))[1] = (d)>>8; \
+ ((uint8_t*)(p))[0] = (d)>>16; \
+ } while(0)
+#endif
+
+#ifndef AV_RL24
+# define AV_RL24(x) \
+ ((((const uint8_t*)(x))[2] << 16) | \
+ (((const uint8_t*)(x))[1] << 8) | \
+ ((const uint8_t*)(x))[0])
+#endif
+#ifndef AV_WL24
+# define AV_WL24(p, d) do { \
+ ((uint8_t*)(p))[0] = (d); \
+ ((uint8_t*)(p))[1] = (d)>>8; \
+ ((uint8_t*)(p))[2] = (d)>>16; \
+ } while(0)
+#endif
+
+/*
+ * The AV_[RW]NA macros access naturally aligned data
+ * in a type-safe way.
+ */
+
+#define AV_RNA(s, p) (((const av_alias##s*)(p))->u##s)
+#define AV_WNA(s, p, v) (((av_alias##s*)(p))->u##s = (v))
+
+#ifndef AV_RN16A
+# define AV_RN16A(p) AV_RNA(16, p)
+#endif
+
+#ifndef AV_RN32A
+# define AV_RN32A(p) AV_RNA(32, p)
+#endif
+
+#ifndef AV_RN64A
+# define AV_RN64A(p) AV_RNA(64, p)
+#endif
+
+#ifndef AV_WN16A
+# define AV_WN16A(p, v) AV_WNA(16, p, v)
+#endif
+
+#ifndef AV_WN32A
+# define AV_WN32A(p, v) AV_WNA(32, p, v)
+#endif
+
+#ifndef AV_WN64A
+# define AV_WN64A(p, v) AV_WNA(64, p, v)
+#endif
+
+/*
+ * The AV_COPYxxU macros are suitable for copying data to/from unaligned
+ * memory locations.
+ */
+
+#define AV_COPYU(n, d, s) AV_WN##n(d, AV_RN##n(s));
+
+#ifndef AV_COPY16U
+# define AV_COPY16U(d, s) AV_COPYU(16, d, s)
+#endif
+
+#ifndef AV_COPY32U
+# define AV_COPY32U(d, s) AV_COPYU(32, d, s)
+#endif
+
+#ifndef AV_COPY64U
+# define AV_COPY64U(d, s) AV_COPYU(64, d, s)
+#endif
+
+#ifndef AV_COPY128U
+# define AV_COPY128U(d, s) \
+ do { \
+ AV_COPY64U(d, s); \
+ AV_COPY64U((char *)(d) + 8, (const char *)(s) + 8); \
+ } while(0)
+#endif
+
+/* Parameters for AV_COPY*, AV_SWAP*, AV_ZERO* must be
+ * naturally aligned. They may be implemented using MMX,
+ * so emms_c() must be called before using any float code
+ * afterwards.
+ */
+
+#define AV_COPY(n, d, s) \
+ (((av_alias##n*)(d))->u##n = ((const av_alias##n*)(s))->u##n)
+
+#ifndef AV_COPY16
+# define AV_COPY16(d, s) AV_COPY(16, d, s)
+#endif
+
+#ifndef AV_COPY32
+# define AV_COPY32(d, s) AV_COPY(32, d, s)
+#endif
+
+#ifndef AV_COPY64
+# define AV_COPY64(d, s) AV_COPY(64, d, s)
+#endif
+
+#ifndef AV_COPY128
+# define AV_COPY128(d, s) \
+ do { \
+ AV_COPY64(d, s); \
+ AV_COPY64((char*)(d)+8, (char*)(s)+8); \
+ } while(0)
+#endif
+
+#define AV_SWAP(n, a, b) FFSWAP(av_alias##n, *(av_alias##n*)(a), *(av_alias##n*)(b))
+
+#ifndef AV_SWAP64
+# define AV_SWAP64(a, b) AV_SWAP(64, a, b)
+#endif
+
+#define AV_ZERO(n, d) (((av_alias##n*)(d))->u##n = 0)
+
+#ifndef AV_ZERO16
+# define AV_ZERO16(d) AV_ZERO(16, d)
+#endif
+
+#ifndef AV_ZERO32
+# define AV_ZERO32(d) AV_ZERO(32, d)
+#endif
+
+#ifndef AV_ZERO64
+# define AV_ZERO64(d) AV_ZERO(64, d)
+#endif
+
+#ifndef AV_ZERO128
+# define AV_ZERO128(d) \
+ do { \
+ AV_ZERO64(d); \
+ AV_ZERO64((char*)(d)+8); \
+ } while(0)
+#endif
+
+#endif /* AVUTIL_INTREADWRITE_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/lfg.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/lfg.h
new file mode 100644
index 0000000000..5e526c1dae
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/lfg.h
@@ -0,0 +1,62 @@
+/*
+ * Lagged Fibonacci PRNG
+ * Copyright (c) 2008 Michael Niedermayer
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_LFG_H
+#define AVUTIL_LFG_H
+
+typedef struct AVLFG {
+ unsigned int state[64];
+ int index;
+} AVLFG;
+
+void av_lfg_init(AVLFG *c, unsigned int seed);
+
+/**
+ * Get the next random unsigned 32-bit number using an ALFG.
+ *
+ * Please also consider a simple LCG like state= state*1664525+1013904223,
+ * it may be good enough and faster for your specific use case.
+ */
+static inline unsigned int av_lfg_get(AVLFG *c){
+ c->state[c->index & 63] = c->state[(c->index-24) & 63] + c->state[(c->index-55) & 63];
+ return c->state[c->index++ & 63];
+}
+
+/**
+ * Get the next random unsigned 32-bit number using a MLFG.
+ *
+ * Please also consider av_lfg_get() above, it is faster.
+ */
+static inline unsigned int av_mlfg_get(AVLFG *c){
+ unsigned int a= c->state[(c->index-55) & 63];
+ unsigned int b= c->state[(c->index-24) & 63];
+ return c->state[c->index++ & 63] = 2*a*b+a+b;
+}
+
+/**
+ * Get the next two numbers generated by a Box-Muller Gaussian
+ * generator using the random numbers issued by lfg.
+ *
+ * @param out array where the two generated numbers are placed
+ */
+void av_bmg_get(AVLFG *lfg, double out[2]);
+
+#endif /* AVUTIL_LFG_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/log.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/log.h
new file mode 100644
index 0000000000..6d26b67db8
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/log.h
@@ -0,0 +1,262 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_LOG_H
+#define AVUTIL_LOG_H
+
+#include <stdarg.h>
+#include "avutil.h"
+#include "attributes.h"
+
+/**
+ * Describe the class of an AVClass context structure. That is an
+ * arbitrary struct of which the first field is a pointer to an
+ * AVClass struct (e.g. AVCodecContext, AVFormatContext etc.).
+ */
+typedef struct AVClass {
+ /**
+ * The name of the class; usually it is the same name as the
+ * context structure type to which the AVClass is associated.
+ */
+ const char* class_name;
+
+ /**
+ * A pointer to a function which returns the name of a context
+ * instance ctx associated with the class.
+ */
+ const char* (*item_name)(void* ctx);
+
+ /**
+ * a pointer to the first option specified in the class if any or NULL
+ *
+ * @see av_set_default_options()
+ */
+ const struct AVOption *option;
+
+ /**
+ * LIBAVUTIL_VERSION with which this structure was created.
+ * This is used to allow fields to be added without requiring major
+ * version bumps everywhere.
+ */
+
+ int version;
+
+ /**
+ * Offset in the structure where log_level_offset is stored.
+ * 0 means there is no such variable
+ */
+ int log_level_offset_offset;
+
+ /**
+ * Offset in the structure where a pointer to the parent context for
+ * logging is stored. For example a decoder could pass its AVCodecContext
+ * to eval as such a parent context, which an av_log() implementation
+ * could then leverage to display the parent context.
+ * The offset can be NULL.
+ */
+ int parent_log_context_offset;
+
+ /**
+ * Return next AVOptions-enabled child or NULL
+ */
+ void* (*child_next)(void *obj, void *prev);
+
+ /**
+ * Return an AVClass corresponding to the next potential
+ * AVOptions-enabled child.
+ *
+ * The difference between child_next and this is that
+ * child_next iterates over _already existing_ objects, while
+ * child_class_next iterates over _all possible_ children.
+ */
+ const struct AVClass* (*child_class_next)(const struct AVClass *prev);
+} AVClass;
+
+/**
+ * @addtogroup lavu_log
+ *
+ * @{
+ *
+ * @defgroup lavu_log_constants Logging Constants
+ *
+ * @{
+ */
+
+/**
+ * Print no output.
+ */
+#define AV_LOG_QUIET -8
+
+/**
+ * Something went really wrong and we will crash now.
+ */
+#define AV_LOG_PANIC 0
+
+/**
+ * Something went wrong and recovery is not possible.
+ * For example, no header was found for a format which depends
+ * on headers or an illegal combination of parameters is used.
+ */
+#define AV_LOG_FATAL 8
+
+/**
+ * Something went wrong and cannot losslessly be recovered.
+ * However, not all future data is affected.
+ */
+#define AV_LOG_ERROR 16
+
+/**
+ * Something somehow does not look correct. This may or may not
+ * lead to problems. An example would be the use of '-vstrict -2'.
+ */
+#define AV_LOG_WARNING 24
+
+/**
+ * Standard information.
+ */
+#define AV_LOG_INFO 32
+
+/**
+ * Detailed information.
+ */
+#define AV_LOG_VERBOSE 40
+
+/**
+ * Stuff which is only useful for libav* developers.
+ */
+#define AV_LOG_DEBUG 48
+
+/**
+ * @}
+ */
+
+/**
+ * Send the specified message to the log if the level is less than or equal
+ * to the current av_log_level. By default, all logging messages are sent to
+ * stderr. This behavior can be altered by setting a different logging callback
+ * function.
+ * @see av_log_set_callback
+ *
+ * @param avcl A pointer to an arbitrary struct of which the first field is a
+ * pointer to an AVClass struct.
+ * @param level The importance level of the message expressed using a @ref
+ * lavu_log_constants "Logging Constant".
+ * @param fmt The format string (printf-compatible) that specifies how
+ * subsequent arguments are converted to output.
+ */
+void av_log(void *avcl, int level, const char *fmt, ...) av_printf_format(3, 4);
+
+
+/**
+ * Send the specified message to the log if the level is less than or equal
+ * to the current av_log_level. By default, all logging messages are sent to
+ * stderr. This behavior can be altered by setting a different logging callback
+ * function.
+ * @see av_log_set_callback
+ *
+ * @param avcl A pointer to an arbitrary struct of which the first field is a
+ * pointer to an AVClass struct.
+ * @param level The importance level of the message expressed using a @ref
+ * lavu_log_constants "Logging Constant".
+ * @param fmt The format string (printf-compatible) that specifies how
+ * subsequent arguments are converted to output.
+ * @param vl The arguments referenced by the format string.
+ */
+void av_vlog(void *avcl, int level, const char *fmt, va_list vl);
+
+/**
+ * Get the current log level
+ *
+ * @see lavu_log_constants
+ *
+ * @return Current log level
+ */
+int av_log_get_level(void);
+
+/**
+ * Set the log level
+ *
+ * @see lavu_log_constants
+ *
+ * @param level Logging level
+ */
+void av_log_set_level(int level);
+
+/**
+ * Set the logging callback
+ *
+ * @see av_log_default_callback
+ *
+ * @param callback A logging function with a compatible signature.
+ */
+void av_log_set_callback(void (*callback)(void*, int, const char*, va_list));
+
+/**
+ * Default logging callback
+ *
+ * It prints the message to stderr, optionally colorizing it.
+ *
+ * @param avcl A pointer to an arbitrary struct of which the first field is a
+ * pointer to an AVClass struct.
+ * @param level The importance level of the message expressed using a @ref
+ * lavu_log_constants "Logging Constant".
+ * @param fmt The format string (printf-compatible) that specifies how
+ * subsequent arguments are converted to output.
+ * @param vl The arguments referenced by the format string.
+ */
+void av_log_default_callback(void *avcl, int level, const char *fmt,
+ va_list vl);
+
+/**
+ * Return the context name
+ *
+ * @param ctx The AVClass context
+ *
+ * @return The AVClass class_name
+ */
+const char* av_default_item_name(void* ctx);
+
+/**
+ * av_dlog macros
+ * Useful to print debug messages that shouldn't get compiled in normally.
+ */
+
+#ifdef DEBUG
+# define av_dlog(pctx, ...) av_log(pctx, AV_LOG_DEBUG, __VA_ARGS__)
+#else
+# define av_dlog(pctx, ...)
+#endif
+
+/**
+ * Skip repeated messages, this requires the user app to use av_log() instead of
+ * (f)printf as the 2 would otherwise interfere and lead to
+ * "Last message repeated x times" messages below (f)printf messages with some
+ * bad luck.
+ * Also to receive the last, "last repeated" line if any, the user app must
+ * call av_log(NULL, AV_LOG_QUIET, ""); at the end
+ */
+#define AV_LOG_SKIP_REPEATED 1
+void av_log_set_flags(int arg);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_LOG_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/lzo.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/lzo.h
new file mode 100644
index 0000000000..9d7e8f1dc1
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/lzo.h
@@ -0,0 +1,66 @@
+/*
+ * LZO 1x decompression
+ * copyright (c) 2006 Reimar Doeffinger
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_LZO_H
+#define AVUTIL_LZO_H
+
+/**
+ * @defgroup lavu_lzo LZO
+ * @ingroup lavu_crypto
+ *
+ * @{
+ */
+
+#include <stdint.h>
+
+/** @name Error flags returned by av_lzo1x_decode
+ * @{ */
+/// end of the input buffer reached before decoding finished
+#define AV_LZO_INPUT_DEPLETED 1
+/// decoded data did not fit into output buffer
+#define AV_LZO_OUTPUT_FULL 2
+/// a reference to previously decoded data was wrong
+#define AV_LZO_INVALID_BACKPTR 4
+/// a non-specific error in the compressed bitstream
+#define AV_LZO_ERROR 8
+/** @} */
+
+#define AV_LZO_INPUT_PADDING 8
+#define AV_LZO_OUTPUT_PADDING 12
+
+/**
+ * @brief Decodes LZO 1x compressed data.
+ * @param out output buffer
+ * @param outlen size of output buffer, number of bytes left are returned here
+ * @param in input buffer
+ * @param inlen size of input buffer, number of bytes left are returned here
+ * @return 0 on success, otherwise a combination of the error flags above
+ *
+ * Make sure all buffers are appropriately padded, in must provide
+ * AV_LZO_INPUT_PADDING, out must provide AV_LZO_OUTPUT_PADDING additional bytes.
+ */
+int av_lzo1x_decode(void *out, int *outlen, const void *in, int *inlen);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_LZO_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/macros.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/macros.h
new file mode 100644
index 0000000000..bf3eb9b9a4
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/macros.h
@@ -0,0 +1,48 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * @ingroup lavu
+ * Utility Preprocessor macros
+ */
+
+#ifndef AVUTIL_MACROS_H
+#define AVUTIL_MACROS_H
+
+/**
+ * @addtogroup preproc_misc Preprocessor String Macros
+ *
+ * String manipulation macros
+ *
+ * @{
+ */
+
+#define AV_STRINGIFY(s) AV_TOSTRING(s)
+#define AV_TOSTRING(s) #s
+
+#define AV_GLUE(a, b) a ## b
+#define AV_JOIN(a, b) AV_GLUE(a, b)
+
+/**
+ * @}
+ */
+
+#define AV_PRAGMA(s) _Pragma(#s)
+
+#endif /* AVUTIL_MACROS_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/mathematics.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/mathematics.h
new file mode 100644
index 0000000000..043dd0fafe
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/mathematics.h
@@ -0,0 +1,111 @@
+/*
+ * copyright (c) 2005 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_MATHEMATICS_H
+#define AVUTIL_MATHEMATICS_H
+
+#include <stdint.h>
+#include <math.h>
+#include "attributes.h"
+#include "rational.h"
+#include "intfloat.h"
+
+#ifndef M_LOG2_10
+#define M_LOG2_10 3.32192809488736234787 /* log_2 10 */
+#endif
+#ifndef M_PHI
+#define M_PHI 1.61803398874989484820 /* phi / golden ratio */
+#endif
+#ifndef NAN
+#define NAN av_int2float(0x7fc00000)
+#endif
+#ifndef INFINITY
+#define INFINITY av_int2float(0x7f800000)
+#endif
+
+/**
+ * @addtogroup lavu_math
+ * @{
+ */
+
+
+enum AVRounding {
+ AV_ROUND_ZERO = 0, ///< Round toward zero.
+ AV_ROUND_INF = 1, ///< Round away from zero.
+ AV_ROUND_DOWN = 2, ///< Round toward -infinity.
+ AV_ROUND_UP = 3, ///< Round toward +infinity.
+ AV_ROUND_NEAR_INF = 5, ///< Round to nearest and halfway cases away from zero.
+};
+
+/**
+ * Return the greatest common divisor of a and b.
+ * If both a and b are 0 or either or both are <0 then behavior is
+ * undefined.
+ */
+int64_t av_const av_gcd(int64_t a, int64_t b);
+
+/**
+ * Rescale a 64-bit integer with rounding to nearest.
+ * A simple a*b/c isn't possible as it can overflow.
+ */
+int64_t av_rescale(int64_t a, int64_t b, int64_t c) av_const;
+
+/**
+ * Rescale a 64-bit integer with specified rounding.
+ * A simple a*b/c isn't possible as it can overflow.
+ */
+int64_t av_rescale_rnd(int64_t a, int64_t b, int64_t c, enum AVRounding) av_const;
+
+/**
+ * Rescale a 64-bit integer by 2 rational numbers.
+ */
+int64_t av_rescale_q(int64_t a, AVRational bq, AVRational cq) av_const;
+
+/**
+ * Rescale a 64-bit integer by 2 rational numbers with specified rounding.
+ */
+int64_t av_rescale_q_rnd(int64_t a, AVRational bq, AVRational cq,
+ enum AVRounding) av_const;
+
+/**
+ * Compare 2 timestamps each in its own timebases.
+ * The result of the function is undefined if one of the timestamps
+ * is outside the int64_t range when represented in the others timebase.
+ * @return -1 if ts_a is before ts_b, 1 if ts_a is after ts_b or 0 if they represent the same position
+ */
+int av_compare_ts(int64_t ts_a, AVRational tb_a, int64_t ts_b, AVRational tb_b);
+
+/**
+ * Compare 2 integers modulo mod.
+ * That is we compare integers a and b for which only the least
+ * significant log2(mod) bits are known.
+ *
+ * @param mod must be a power of 2
+ * @return a negative value if a is smaller than b
+ * a positive value if a is greater than b
+ * 0 if a equals b
+ */
+int64_t av_compare_mod(uint64_t a, uint64_t b, uint64_t mod);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_MATHEMATICS_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/md5.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/md5.h
new file mode 100644
index 0000000000..29e4e7c2ba
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/md5.h
@@ -0,0 +1,51 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_MD5_H
+#define AVUTIL_MD5_H
+
+#include <stdint.h>
+
+#include "attributes.h"
+#include "version.h"
+
+/**
+ * @defgroup lavu_md5 MD5
+ * @ingroup lavu_crypto
+ * @{
+ */
+
+#if FF_API_CONTEXT_SIZE
+extern attribute_deprecated const int av_md5_size;
+#endif
+
+struct AVMD5;
+
+struct AVMD5 *av_md5_alloc(void);
+void av_md5_init(struct AVMD5 *ctx);
+void av_md5_update(struct AVMD5 *ctx, const uint8_t *src, const int len);
+void av_md5_final(struct AVMD5 *ctx, uint8_t *dst);
+void av_md5_sum(uint8_t *dst, const uint8_t *src, const int len);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_MD5_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/mem.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/mem.h
new file mode 100644
index 0000000000..4a5e362cec
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/mem.h
@@ -0,0 +1,265 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * memory handling functions
+ */
+
+#ifndef AVUTIL_MEM_H
+#define AVUTIL_MEM_H
+
+#include <limits.h>
+#include <stdint.h>
+
+#include "attributes.h"
+#include "avutil.h"
+
+/**
+ * @addtogroup lavu_mem
+ * @{
+ */
+
+
+#if defined(__ICC) && __ICC < 1200 || defined(__SUNPRO_C)
+ #define DECLARE_ALIGNED(n,t,v) t __attribute__ ((aligned (n))) v
+ #define DECLARE_ASM_CONST(n,t,v) const t __attribute__ ((aligned (n))) v
+#elif defined(__TI_COMPILER_VERSION__)
+ #define DECLARE_ALIGNED(n,t,v) \
+ AV_PRAGMA(DATA_ALIGN(v,n)) \
+ t __attribute__((aligned(n))) v
+ #define DECLARE_ASM_CONST(n,t,v) \
+ AV_PRAGMA(DATA_ALIGN(v,n)) \
+ static const t __attribute__((aligned(n))) v
+#elif defined(__GNUC__)
+ #define DECLARE_ALIGNED(n,t,v) t __attribute__ ((aligned (n))) v
+ #define DECLARE_ASM_CONST(n,t,v) static const t av_used __attribute__ ((aligned (n))) v
+#elif defined(_MSC_VER)
+ #define DECLARE_ALIGNED(n,t,v) __declspec(align(n)) t v
+ #define DECLARE_ASM_CONST(n,t,v) __declspec(align(n)) static const t v
+#else
+ #define DECLARE_ALIGNED(n,t,v) t v
+ #define DECLARE_ASM_CONST(n,t,v) static const t v
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(3,1)
+ #define av_malloc_attrib __attribute__((__malloc__))
+#else
+ #define av_malloc_attrib
+#endif
+
+#if AV_GCC_VERSION_AT_LEAST(4,3)
+ #define av_alloc_size(...) __attribute__((alloc_size(__VA_ARGS__)))
+#else
+ #define av_alloc_size(...)
+#endif
+
+/**
+ * Allocate a block of size bytes with alignment suitable for all
+ * memory accesses (including vectors if available on the CPU).
+ * @param size Size in bytes for the memory block to be allocated.
+ * @return Pointer to the allocated block, NULL if the block cannot
+ * be allocated.
+ * @see av_mallocz()
+ */
+void *av_malloc(size_t size) av_malloc_attrib av_alloc_size(1);
+
+/**
+ * Allocate a block of size * nmemb bytes with av_malloc().
+ * @param nmemb Number of elements
+ * @param size Size of the single element
+ * @return Pointer to the allocated block, NULL if the block cannot
+ * be allocated.
+ * @see av_malloc()
+ */
+av_alloc_size(1, 2) static inline void *av_malloc_array(size_t nmemb, size_t size)
+{
+ if (!size || nmemb >= INT_MAX / size)
+ return NULL;
+ return av_malloc(nmemb * size);
+}
+
+/**
+ * Allocate or reallocate a block of memory.
+ * If ptr is NULL and size > 0, allocate a new block. If
+ * size is zero, free the memory block pointed to by ptr.
+ * @param ptr Pointer to a memory block already allocated with
+ * av_realloc() or NULL.
+ * @param size Size in bytes of the memory block to be allocated or
+ * reallocated.
+ * @return Pointer to a newly-reallocated block or NULL if the block
+ * cannot be reallocated or the function is used to free the memory block.
+ * @warning Pointers originating from the av_malloc() family of functions must
+ * not be passed to av_realloc(). The former can be implemented using
+ * memalign() (or other functions), and there is no guarantee that
+ * pointers from such functions can be passed to realloc() at all.
+ * The situation is undefined according to POSIX and may crash with
+ * some libc implementations.
+ * @see av_fast_realloc()
+ */
+void *av_realloc(void *ptr, size_t size) av_alloc_size(2);
+
+/**
+ * Allocate or reallocate a block of memory.
+ * If *ptr is NULL and size > 0, allocate a new block. If
+ * size is zero, free the memory block pointed to by ptr.
+ * @param ptr Pointer to a pointer to a memory block already allocated
+ * with av_realloc(), or pointer to a pointer to NULL.
+ * The pointer is updated on success, or freed on failure.
+ * @param size Size in bytes for the memory block to be allocated or
+ * reallocated
+ * @return Zero on success, an AVERROR error code on failure.
+ * @warning Pointers originating from the av_malloc() family of functions must
+ * not be passed to av_reallocp(). The former can be implemented using
+ * memalign() (or other functions), and there is no guarantee that
+ * pointers from such functions can be passed to realloc() at all.
+ * The situation is undefined according to POSIX and may crash with
+ * some libc implementations.
+ */
+int av_reallocp(void *ptr, size_t size);
+
+/**
+ * Allocate or reallocate an array.
+ * If ptr is NULL and nmemb > 0, allocate a new block. If
+ * nmemb is zero, free the memory block pointed to by ptr.
+ * @param ptr Pointer to a memory block already allocated with
+ * av_realloc() or NULL.
+ * @param nmemb Number of elements
+ * @param size Size of the single element
+ * @return Pointer to a newly-reallocated block or NULL if the block
+ * cannot be reallocated or the function is used to free the memory block.
+ * @warning Pointers originating from the av_malloc() family of functions must
+ * not be passed to av_realloc(). The former can be implemented using
+ * memalign() (or other functions), and there is no guarantee that
+ * pointers from such functions can be passed to realloc() at all.
+ * The situation is undefined according to POSIX and may crash with
+ * some libc implementations.
+ */
+av_alloc_size(2, 3) void *av_realloc_array(void *ptr, size_t nmemb, size_t size);
+
+/**
+ * Allocate or reallocate an array through a pointer to a pointer.
+ * If *ptr is NULL and nmemb > 0, allocate a new block. If
+ * nmemb is zero, free the memory block pointed to by ptr.
+ * @param ptr Pointer to a pointer to a memory block already allocated
+ * with av_realloc(), or pointer to a pointer to NULL.
+ * The pointer is updated on success, or freed on failure.
+ * @param nmemb Number of elements
+ * @param size Size of the single element
+ * @return Zero on success, an AVERROR error code on failure.
+ * @warning Pointers originating from the av_malloc() family of functions must
+ * not be passed to av_realloc(). The former can be implemented using
+ * memalign() (or other functions), and there is no guarantee that
+ * pointers from such functions can be passed to realloc() at all.
+ * The situation is undefined according to POSIX and may crash with
+ * some libc implementations.
+ */
+av_alloc_size(2, 3) int av_reallocp_array(void *ptr, size_t nmemb, size_t size);
+
+/**
+ * Free a memory block which has been allocated with av_malloc(z)() or
+ * av_realloc().
+ * @param ptr Pointer to the memory block which should be freed.
+ * @note ptr = NULL is explicitly allowed.
+ * @note It is recommended that you use av_freep() instead.
+ * @see av_freep()
+ */
+void av_free(void *ptr);
+
+/**
+ * Allocate a block of size bytes with alignment suitable for all
+ * memory accesses (including vectors if available on the CPU) and
+ * zero all the bytes of the block.
+ * @param size Size in bytes for the memory block to be allocated.
+ * @return Pointer to the allocated block, NULL if it cannot be allocated.
+ * @see av_malloc()
+ */
+void *av_mallocz(size_t size) av_malloc_attrib av_alloc_size(1);
+
+/**
+ * Allocate a block of size * nmemb bytes with av_mallocz().
+ * @param nmemb Number of elements
+ * @param size Size of the single element
+ * @return Pointer to the allocated block, NULL if the block cannot
+ * be allocated.
+ * @see av_mallocz()
+ * @see av_malloc_array()
+ */
+av_alloc_size(1, 2) static inline void *av_mallocz_array(size_t nmemb, size_t size)
+{
+ if (!size || nmemb >= INT_MAX / size)
+ return NULL;
+ return av_mallocz(nmemb * size);
+}
+
+/**
+ * Duplicate the string s.
+ * @param s string to be duplicated
+ * @return Pointer to a newly-allocated string containing a
+ * copy of s or NULL if the string cannot be allocated.
+ */
+char *av_strdup(const char *s) av_malloc_attrib;
+
+/**
+ * Free a memory block which has been allocated with av_malloc(z)() or
+ * av_realloc() and set the pointer pointing to it to NULL.
+ * @param ptr Pointer to the pointer to the memory block which should
+ * be freed.
+ * @see av_free()
+ */
+void av_freep(void *ptr);
+
+/**
+ * deliberately overlapping memcpy implementation
+ * @param dst destination buffer
+ * @param back how many bytes back we start (the initial size of the overlapping window)
+ * @param cnt number of bytes to copy, must be >= 0
+ *
+ * cnt > back is valid, this will copy the bytes we just copied,
+ * thus creating a repeating pattern with a period length of back.
+ */
+void av_memcpy_backptr(uint8_t *dst, int back, int cnt);
+
+/**
+ * Reallocate the given block if it is not large enough, otherwise do nothing.
+ *
+ * @see av_realloc
+ */
+void *av_fast_realloc(void *ptr, unsigned int *size, size_t min_size);
+
+/**
+ * Allocate a buffer, reusing the given one if large enough.
+ *
+ * Contrary to av_fast_realloc the current buffer contents might not be
+ * preserved and on error the old buffer is freed, thus no special
+ * handling to avoid memleaks is necessary.
+ *
+ * @param ptr pointer to pointer to already allocated buffer, overwritten with pointer to new buffer
+ * @param size size of the buffer *ptr points to
+ * @param min_size minimum size of *ptr buffer after returning, *ptr will be NULL and
+ * *size 0 if an error occurred.
+ */
+void av_fast_malloc(void *ptr, unsigned int *size, size_t min_size);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_MEM_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/old_pix_fmts.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/old_pix_fmts.h
new file mode 100644
index 0000000000..d3e1e5b24d
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/old_pix_fmts.h
@@ -0,0 +1,134 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_OLD_PIX_FMTS_H
+#define AVUTIL_OLD_PIX_FMTS_H
+
+/*
+ * This header exists to prevent new pixel formats from being accidentally added
+ * to the deprecated list.
+ * Do not include it directly. It will be removed on next major bump
+ *
+ * Do not add new items to this list. Use the AVPixelFormat enum instead.
+ */
+ PIX_FMT_NONE = AV_PIX_FMT_NONE,
+ PIX_FMT_YUV420P, ///< planar YUV 4:2:0, 12bpp, (1 Cr & Cb sample per 2x2 Y samples)
+ PIX_FMT_YUYV422, ///< packed YUV 4:2:2, 16bpp, Y0 Cb Y1 Cr
+ PIX_FMT_RGB24, ///< packed RGB 8:8:8, 24bpp, RGBRGB...
+ PIX_FMT_BGR24, ///< packed RGB 8:8:8, 24bpp, BGRBGR...
+ PIX_FMT_YUV422P, ///< planar YUV 4:2:2, 16bpp, (1 Cr & Cb sample per 2x1 Y samples)
+ PIX_FMT_YUV444P, ///< planar YUV 4:4:4, 24bpp, (1 Cr & Cb sample per 1x1 Y samples)
+ PIX_FMT_YUV410P, ///< planar YUV 4:1:0, 9bpp, (1 Cr & Cb sample per 4x4 Y samples)
+ PIX_FMT_YUV411P, ///< planar YUV 4:1:1, 12bpp, (1 Cr & Cb sample per 4x1 Y samples)
+ PIX_FMT_GRAY8, ///< Y , 8bpp
+ PIX_FMT_MONOWHITE, ///< Y , 1bpp, 0 is white, 1 is black, in each byte pixels are ordered from the msb to the lsb
+ PIX_FMT_MONOBLACK, ///< Y , 1bpp, 0 is black, 1 is white, in each byte pixels are ordered from the msb to the lsb
+ PIX_FMT_PAL8, ///< 8 bit with PIX_FMT_RGB32 palette
+ PIX_FMT_YUVJ420P, ///< planar YUV 4:2:0, 12bpp, full scale (JPEG), deprecated in favor of PIX_FMT_YUV420P and setting color_range
+ PIX_FMT_YUVJ422P, ///< planar YUV 4:2:2, 16bpp, full scale (JPEG), deprecated in favor of PIX_FMT_YUV422P and setting color_range
+ PIX_FMT_YUVJ444P, ///< planar YUV 4:4:4, 24bpp, full scale (JPEG), deprecated in favor of PIX_FMT_YUV444P and setting color_range
+#if FF_API_XVMC
+ PIX_FMT_XVMC_MPEG2_MC,///< XVideo Motion Acceleration via common packet passing
+ PIX_FMT_XVMC_MPEG2_IDCT,
+#endif /* FF_API_XVMC */
+ PIX_FMT_UYVY422, ///< packed YUV 4:2:2, 16bpp, Cb Y0 Cr Y1
+ PIX_FMT_UYYVYY411, ///< packed YUV 4:1:1, 12bpp, Cb Y0 Y1 Cr Y2 Y3
+ PIX_FMT_BGR8, ///< packed RGB 3:3:2, 8bpp, (msb)2B 3G 3R(lsb)
+ PIX_FMT_BGR4, ///< packed RGB 1:2:1 bitstream, 4bpp, (msb)1B 2G 1R(lsb), a byte contains two pixels, the first pixel in the byte is the one composed by the 4 msb bits
+ PIX_FMT_BGR4_BYTE, ///< packed RGB 1:2:1, 8bpp, (msb)1B 2G 1R(lsb)
+ PIX_FMT_RGB8, ///< packed RGB 3:3:2, 8bpp, (msb)2R 3G 3B(lsb)
+ PIX_FMT_RGB4, ///< packed RGB 1:2:1 bitstream, 4bpp, (msb)1R 2G 1B(lsb), a byte contains two pixels, the first pixel in the byte is the one composed by the 4 msb bits
+ PIX_FMT_RGB4_BYTE, ///< packed RGB 1:2:1, 8bpp, (msb)1R 2G 1B(lsb)
+ PIX_FMT_NV12, ///< planar YUV 4:2:0, 12bpp, 1 plane for Y and 1 plane for the UV components, which are interleaved (first byte U and the following byte V)
+ PIX_FMT_NV21, ///< as above, but U and V bytes are swapped
+
+ PIX_FMT_ARGB, ///< packed ARGB 8:8:8:8, 32bpp, ARGBARGB...
+ PIX_FMT_RGBA, ///< packed RGBA 8:8:8:8, 32bpp, RGBARGBA...
+ PIX_FMT_ABGR, ///< packed ABGR 8:8:8:8, 32bpp, ABGRABGR...
+ PIX_FMT_BGRA, ///< packed BGRA 8:8:8:8, 32bpp, BGRABGRA...
+
+ PIX_FMT_GRAY16BE, ///< Y , 16bpp, big-endian
+ PIX_FMT_GRAY16LE, ///< Y , 16bpp, little-endian
+ PIX_FMT_YUV440P, ///< planar YUV 4:4:0 (1 Cr & Cb sample per 1x2 Y samples)
+ PIX_FMT_YUVJ440P, ///< planar YUV 4:4:0 full scale (JPEG), deprecated in favor of PIX_FMT_YUV440P and setting color_range
+ PIX_FMT_YUVA420P, ///< planar YUV 4:2:0, 20bpp, (1 Cr & Cb sample per 2x2 Y & A samples)
+#if FF_API_VDPAU
+ PIX_FMT_VDPAU_H264,///< H.264 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ PIX_FMT_VDPAU_MPEG1,///< MPEG-1 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ PIX_FMT_VDPAU_MPEG2,///< MPEG-2 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ PIX_FMT_VDPAU_WMV3,///< WMV3 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ PIX_FMT_VDPAU_VC1, ///< VC-1 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+#endif
+ PIX_FMT_RGB48BE, ///< packed RGB 16:16:16, 48bpp, 16R, 16G, 16B, the 2-byte value for each R/G/B component is stored as big-endian
+ PIX_FMT_RGB48LE, ///< packed RGB 16:16:16, 48bpp, 16R, 16G, 16B, the 2-byte value for each R/G/B component is stored as little-endian
+
+ PIX_FMT_RGB565BE, ///< packed RGB 5:6:5, 16bpp, (msb) 5R 6G 5B(lsb), big-endian
+ PIX_FMT_RGB565LE, ///< packed RGB 5:6:5, 16bpp, (msb) 5R 6G 5B(lsb), little-endian
+ PIX_FMT_RGB555BE, ///< packed RGB 5:5:5, 16bpp, (msb)1A 5R 5G 5B(lsb), big-endian, most significant bit to 0
+ PIX_FMT_RGB555LE, ///< packed RGB 5:5:5, 16bpp, (msb)1A 5R 5G 5B(lsb), little-endian, most significant bit to 0
+
+ PIX_FMT_BGR565BE, ///< packed BGR 5:6:5, 16bpp, (msb) 5B 6G 5R(lsb), big-endian
+ PIX_FMT_BGR565LE, ///< packed BGR 5:6:5, 16bpp, (msb) 5B 6G 5R(lsb), little-endian
+ PIX_FMT_BGR555BE, ///< packed BGR 5:5:5, 16bpp, (msb)1A 5B 5G 5R(lsb), big-endian, most significant bit to 1
+ PIX_FMT_BGR555LE, ///< packed BGR 5:5:5, 16bpp, (msb)1A 5B 5G 5R(lsb), little-endian, most significant bit to 1
+
+ PIX_FMT_VAAPI_MOCO, ///< HW acceleration through VA API at motion compensation entry-point, Picture.data[3] contains a vaapi_render_state struct which contains macroblocks as well as various fields extracted from headers
+ PIX_FMT_VAAPI_IDCT, ///< HW acceleration through VA API at IDCT entry-point, Picture.data[3] contains a vaapi_render_state struct which contains fields extracted from headers
+ PIX_FMT_VAAPI_VLD, ///< HW decoding through VA API, Picture.data[3] contains a vaapi_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+
+ PIX_FMT_YUV420P16LE, ///< planar YUV 4:2:0, 24bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian
+ PIX_FMT_YUV420P16BE, ///< planar YUV 4:2:0, 24bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian
+ PIX_FMT_YUV422P16LE, ///< planar YUV 4:2:2, 32bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ PIX_FMT_YUV422P16BE, ///< planar YUV 4:2:2, 32bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+ PIX_FMT_YUV444P16LE, ///< planar YUV 4:4:4, 48bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian
+ PIX_FMT_YUV444P16BE, ///< planar YUV 4:4:4, 48bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian
+#if FF_API_VDPAU
+ PIX_FMT_VDPAU_MPEG4, ///< MPEG4 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+#endif
+ PIX_FMT_DXVA2_VLD, ///< HW decoding through DXVA2, Picture.data[3] contains a LPDIRECT3DSURFACE9 pointer
+
+ PIX_FMT_RGB444LE, ///< packed RGB 4:4:4, 16bpp, (msb)4A 4R 4G 4B(lsb), little-endian, most significant bits to 0
+ PIX_FMT_RGB444BE, ///< packed RGB 4:4:4, 16bpp, (msb)4A 4R 4G 4B(lsb), big-endian, most significant bits to 0
+ PIX_FMT_BGR444LE, ///< packed BGR 4:4:4, 16bpp, (msb)4A 4B 4G 4R(lsb), little-endian, most significant bits to 1
+ PIX_FMT_BGR444BE, ///< packed BGR 4:4:4, 16bpp, (msb)4A 4B 4G 4R(lsb), big-endian, most significant bits to 1
+ PIX_FMT_Y400A, ///< 8bit gray, 8bit alpha
+ PIX_FMT_BGR48BE, ///< packed RGB 16:16:16, 48bpp, 16B, 16G, 16R, the 2-byte value for each R/G/B component is stored as big-endian
+ PIX_FMT_BGR48LE, ///< packed RGB 16:16:16, 48bpp, 16B, 16G, 16R, the 2-byte value for each R/G/B component is stored as little-endian
+ PIX_FMT_YUV420P9BE, ///< planar YUV 4:2:0, 13.5bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian
+ PIX_FMT_YUV420P9LE, ///< planar YUV 4:2:0, 13.5bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian
+ PIX_FMT_YUV420P10BE,///< planar YUV 4:2:0, 15bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian
+ PIX_FMT_YUV420P10LE,///< planar YUV 4:2:0, 15bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian
+ PIX_FMT_YUV422P10BE,///< planar YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+ PIX_FMT_YUV422P10LE,///< planar YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ PIX_FMT_YUV444P9BE, ///< planar YUV 4:4:4, 27bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian
+ PIX_FMT_YUV444P9LE, ///< planar YUV 4:4:4, 27bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian
+ PIX_FMT_YUV444P10BE,///< planar YUV 4:4:4, 30bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian
+ PIX_FMT_YUV444P10LE,///< planar YUV 4:4:4, 30bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian
+ PIX_FMT_YUV422P9BE, ///< planar YUV 4:2:2, 18bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+ PIX_FMT_YUV422P9LE, ///< planar YUV 4:2:2, 18bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ PIX_FMT_VDA_VLD, ///< hardware decoding through VDA
+ PIX_FMT_GBRP, ///< planar GBR 4:4:4 24bpp
+ PIX_FMT_GBRP9BE, ///< planar GBR 4:4:4 27bpp, big endian
+ PIX_FMT_GBRP9LE, ///< planar GBR 4:4:4 27bpp, little endian
+ PIX_FMT_GBRP10BE, ///< planar GBR 4:4:4 30bpp, big endian
+ PIX_FMT_GBRP10LE, ///< planar GBR 4:4:4 30bpp, little endian
+ PIX_FMT_GBRP16BE, ///< planar GBR 4:4:4 48bpp, big endian
+ PIX_FMT_GBRP16LE, ///< planar GBR 4:4:4 48bpp, little endian
+ PIX_FMT_NB, ///< number of pixel formats, DO NOT USE THIS if you want to link with shared libav* because the number of formats might differ between versions
+
+#endif /* AVUTIL_OLD_PIX_FMTS_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/opt.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/opt.h
new file mode 100644
index 0000000000..0181379b78
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/opt.h
@@ -0,0 +1,516 @@
+/*
+ * AVOptions
+ * copyright (c) 2005 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_OPT_H
+#define AVUTIL_OPT_H
+
+/**
+ * @file
+ * AVOptions
+ */
+
+#include "rational.h"
+#include "avutil.h"
+#include "dict.h"
+#include "log.h"
+
+/**
+ * @defgroup avoptions AVOptions
+ * @ingroup lavu_data
+ * @{
+ * AVOptions provide a generic system to declare options on arbitrary structs
+ * ("objects"). An option can have a help text, a type and a range of possible
+ * values. Options may then be enumerated, read and written to.
+ *
+ * @section avoptions_implement Implementing AVOptions
+ * This section describes how to add AVOptions capabilities to a struct.
+ *
+ * All AVOptions-related information is stored in an AVClass. Therefore
+ * the first member of the struct must be a pointer to an AVClass describing it.
+ * The option field of the AVClass must be set to a NULL-terminated static array
+ * of AVOptions. Each AVOption must have a non-empty name, a type, a default
+ * value and for number-type AVOptions also a range of allowed values. It must
+ * also declare an offset in bytes from the start of the struct, where the field
+ * associated with this AVOption is located. Other fields in the AVOption struct
+ * should also be set when applicable, but are not required.
+ *
+ * The following example illustrates an AVOptions-enabled struct:
+ * @code
+ * typedef struct test_struct {
+ * AVClass *class;
+ * int int_opt;
+ * char *str_opt;
+ * uint8_t *bin_opt;
+ * int bin_len;
+ * } test_struct;
+ *
+ * static const AVOption test_options[] = {
+ * { "test_int", "This is a test option of int type.", offsetof(test_struct, int_opt),
+ * AV_OPT_TYPE_INT, { .i64 = -1 }, INT_MIN, INT_MAX },
+ * { "test_str", "This is a test option of string type.", offsetof(test_struct, str_opt),
+ * AV_OPT_TYPE_STRING },
+ * { "test_bin", "This is a test option of binary type.", offsetof(test_struct, bin_opt),
+ * AV_OPT_TYPE_BINARY },
+ * { NULL },
+ * };
+ *
+ * static const AVClass test_class = {
+ * .class_name = "test class",
+ * .item_name = av_default_item_name,
+ * .option = test_options,
+ * .version = LIBAVUTIL_VERSION_INT,
+ * };
+ * @endcode
+ *
+ * Next, when allocating your struct, you must ensure that the AVClass pointer
+ * is set to the correct value. Then, av_opt_set_defaults() must be called to
+ * initialize defaults. After that the struct is ready to be used with the
+ * AVOptions API.
+ *
+ * When cleaning up, you may use the av_opt_free() function to automatically
+ * free all the allocated string and binary options.
+ *
+ * Continuing with the above example:
+ *
+ * @code
+ * test_struct *alloc_test_struct(void)
+ * {
+ * test_struct *ret = av_malloc(sizeof(*ret));
+ * ret->class = &test_class;
+ * av_opt_set_defaults(ret);
+ * return ret;
+ * }
+ * void free_test_struct(test_struct **foo)
+ * {
+ * av_opt_free(*foo);
+ * av_freep(foo);
+ * }
+ * @endcode
+ *
+ * @subsection avoptions_implement_nesting Nesting
+ * It may happen that an AVOptions-enabled struct contains another
+ * AVOptions-enabled struct as a member (e.g. AVCodecContext in
+ * libavcodec exports generic options, while its priv_data field exports
+ * codec-specific options). In such a case, it is possible to set up the
+ * parent struct to export a child's options. To do that, simply
+ * implement AVClass.child_next() and AVClass.child_class_next() in the
+ * parent struct's AVClass.
+ * Assuming that the test_struct from above now also contains a
+ * child_struct field:
+ *
+ * @code
+ * typedef struct child_struct {
+ * AVClass *class;
+ * int flags_opt;
+ * } child_struct;
+ * static const AVOption child_opts[] = {
+ * { "test_flags", "This is a test option of flags type.",
+ * offsetof(child_struct, flags_opt), AV_OPT_TYPE_FLAGS, { .i64 = 0 }, INT_MIN, INT_MAX },
+ * { NULL },
+ * };
+ * static const AVClass child_class = {
+ * .class_name = "child class",
+ * .item_name = av_default_item_name,
+ * .option = child_opts,
+ * .version = LIBAVUTIL_VERSION_INT,
+ * };
+ *
+ * void *child_next(void *obj, void *prev)
+ * {
+ * test_struct *t = obj;
+ * if (!prev && t->child_struct)
+ * return t->child_struct;
+ * return NULL
+ * }
+ * const AVClass child_class_next(const AVClass *prev)
+ * {
+ * return prev ? NULL : &child_class;
+ * }
+ * @endcode
+ * Putting child_next() and child_class_next() as defined above into
+ * test_class will now make child_struct's options accessible through
+ * test_struct (again, proper setup as described above needs to be done on
+ * child_struct right after it is created).
+ *
+ * From the above example it might not be clear why both child_next()
+ * and child_class_next() are needed. The distinction is that child_next()
+ * iterates over actually existing objects, while child_class_next()
+ * iterates over all possible child classes. E.g. if an AVCodecContext
+ * was initialized to use a codec which has private options, then its
+ * child_next() will return AVCodecContext.priv_data and finish
+ * iterating. OTOH child_class_next() on AVCodecContext.av_class will
+ * iterate over all available codecs with private options.
+ *
+ * @subsection avoptions_implement_named_constants Named constants
+ * It is possible to create named constants for options. Simply set the unit
+ * field of the option the constants should apply to to a string and
+ * create the constants themselves as options of type AV_OPT_TYPE_CONST
+ * with their unit field set to the same string.
+ * Their default_val field should contain the value of the named
+ * constant.
+ * For example, to add some named constants for the test_flags option
+ * above, put the following into the child_opts array:
+ * @code
+ * { "test_flags", "This is a test option of flags type.",
+ * offsetof(child_struct, flags_opt), AV_OPT_TYPE_FLAGS, { .i64 = 0 }, INT_MIN, INT_MAX, "test_unit" },
+ * { "flag1", "This is a flag with value 16", 0, AV_OPT_TYPE_CONST, { .i64 = 16 }, 0, 0, "test_unit" },
+ * @endcode
+ *
+ * @section avoptions_use Using AVOptions
+ * This section deals with accessing options in an AVOptions-enabled struct.
+ * Such structs in Libav are e.g. AVCodecContext in libavcodec or
+ * AVFormatContext in libavformat.
+ *
+ * @subsection avoptions_use_examine Examining AVOptions
+ * The basic functions for examining options are av_opt_next(), which iterates
+ * over all options defined for one object, and av_opt_find(), which searches
+ * for an option with the given name.
+ *
+ * The situation is more complicated with nesting. An AVOptions-enabled struct
+ * may have AVOptions-enabled children. Passing the AV_OPT_SEARCH_CHILDREN flag
+ * to av_opt_find() will make the function search children recursively.
+ *
+ * For enumerating there are basically two cases. The first is when you want to
+ * get all options that may potentially exist on the struct and its children
+ * (e.g. when constructing documentation). In that case you should call
+ * av_opt_child_class_next() recursively on the parent struct's AVClass. The
+ * second case is when you have an already initialized struct with all its
+ * children and you want to get all options that can be actually written or read
+ * from it. In that case you should call av_opt_child_next() recursively (and
+ * av_opt_next() on each result).
+ *
+ * @subsection avoptions_use_get_set Reading and writing AVOptions
+ * When setting options, you often have a string read directly from the
+ * user. In such a case, simply passing it to av_opt_set() is enough. For
+ * non-string type options, av_opt_set() will parse the string according to the
+ * option type.
+ *
+ * Similarly av_opt_get() will read any option type and convert it to a string
+ * which will be returned. Do not forget that the string is allocated, so you
+ * have to free it with av_free().
+ *
+ * In some cases it may be more convenient to put all options into an
+ * AVDictionary and call av_opt_set_dict() on it. A specific case of this
+ * are the format/codec open functions in lavf/lavc which take a dictionary
+ * filled with option as a parameter. This allows to set some options
+ * that cannot be set otherwise, since e.g. the input file format is not known
+ * before the file is actually opened.
+ */
+
+enum AVOptionType{
+ AV_OPT_TYPE_FLAGS,
+ AV_OPT_TYPE_INT,
+ AV_OPT_TYPE_INT64,
+ AV_OPT_TYPE_DOUBLE,
+ AV_OPT_TYPE_FLOAT,
+ AV_OPT_TYPE_STRING,
+ AV_OPT_TYPE_RATIONAL,
+ AV_OPT_TYPE_BINARY, ///< offset must point to a pointer immediately followed by an int for the length
+ AV_OPT_TYPE_CONST = 128,
+};
+
+/**
+ * AVOption
+ */
+typedef struct AVOption {
+ const char *name;
+
+ /**
+ * short English help text
+ * @todo What about other languages?
+ */
+ const char *help;
+
+ /**
+ * The offset relative to the context structure where the option
+ * value is stored. It should be 0 for named constants.
+ */
+ int offset;
+ enum AVOptionType type;
+
+ /**
+ * the default value for scalar options
+ */
+ union {
+ int64_t i64;
+ double dbl;
+ const char *str;
+ /* TODO those are unused now */
+ AVRational q;
+ } default_val;
+ double min; ///< minimum valid value for the option
+ double max; ///< maximum valid value for the option
+
+ int flags;
+#define AV_OPT_FLAG_ENCODING_PARAM 1 ///< a generic parameter which can be set by the user for muxing or encoding
+#define AV_OPT_FLAG_DECODING_PARAM 2 ///< a generic parameter which can be set by the user for demuxing or decoding
+#define AV_OPT_FLAG_METADATA 4 ///< some data extracted or inserted into the file like title, comment, ...
+#define AV_OPT_FLAG_AUDIO_PARAM 8
+#define AV_OPT_FLAG_VIDEO_PARAM 16
+#define AV_OPT_FLAG_SUBTITLE_PARAM 32
+//FIXME think about enc-audio, ... style flags
+
+ /**
+ * The logical unit to which the option belongs. Non-constant
+ * options and corresponding named constants share the same
+ * unit. May be NULL.
+ */
+ const char *unit;
+} AVOption;
+
+/**
+ * Show the obj options.
+ *
+ * @param req_flags requested flags for the options to show. Show only the
+ * options for which it is opt->flags & req_flags.
+ * @param rej_flags rejected flags for the options to show. Show only the
+ * options for which it is !(opt->flags & req_flags).
+ * @param av_log_obj log context to use for showing the options
+ */
+int av_opt_show2(void *obj, void *av_log_obj, int req_flags, int rej_flags);
+
+/**
+ * Set the values of all AVOption fields to their default values.
+ *
+ * @param s an AVOption-enabled struct (its first member must be a pointer to AVClass)
+ */
+void av_opt_set_defaults(void *s);
+
+/**
+ * Parse the key/value pairs list in opts. For each key/value pair
+ * found, stores the value in the field in ctx that is named like the
+ * key. ctx must be an AVClass context, storing is done using
+ * AVOptions.
+ *
+ * @param key_val_sep a 0-terminated list of characters used to
+ * separate key from value
+ * @param pairs_sep a 0-terminated list of characters used to separate
+ * two pairs from each other
+ * @return the number of successfully set key/value pairs, or a negative
+ * value corresponding to an AVERROR code in case of error:
+ * AVERROR(EINVAL) if opts cannot be parsed,
+ * the error code issued by av_set_string3() if a key/value pair
+ * cannot be set
+ */
+int av_set_options_string(void *ctx, const char *opts,
+ const char *key_val_sep, const char *pairs_sep);
+
+/**
+ * Free all string and binary options in obj.
+ */
+void av_opt_free(void *obj);
+
+/**
+ * Check whether a particular flag is set in a flags field.
+ *
+ * @param field_name the name of the flag field option
+ * @param flag_name the name of the flag to check
+ * @return non-zero if the flag is set, zero if the flag isn't set,
+ * isn't of the right type, or the flags field doesn't exist.
+ */
+int av_opt_flag_is_set(void *obj, const char *field_name, const char *flag_name);
+
+/*
+ * Set all the options from a given dictionary on an object.
+ *
+ * @param obj a struct whose first element is a pointer to AVClass
+ * @param options options to process. This dictionary will be freed and replaced
+ * by a new one containing all options not found in obj.
+ * Of course this new dictionary needs to be freed by caller
+ * with av_dict_free().
+ *
+ * @return 0 on success, a negative AVERROR if some option was found in obj,
+ * but could not be set.
+ *
+ * @see av_dict_copy()
+ */
+int av_opt_set_dict(void *obj, struct AVDictionary **options);
+
+/**
+ * @defgroup opt_eval_funcs Evaluating option strings
+ * @{
+ * This group of functions can be used to evaluate option strings
+ * and get numbers out of them. They do the same thing as av_opt_set(),
+ * except the result is written into the caller-supplied pointer.
+ *
+ * @param obj a struct whose first element is a pointer to AVClass.
+ * @param o an option for which the string is to be evaluated.
+ * @param val string to be evaluated.
+ * @param *_out value of the string will be written here.
+ *
+ * @return 0 on success, a negative number on failure.
+ */
+int av_opt_eval_flags (void *obj, const AVOption *o, const char *val, int *flags_out);
+int av_opt_eval_int (void *obj, const AVOption *o, const char *val, int *int_out);
+int av_opt_eval_int64 (void *obj, const AVOption *o, const char *val, int64_t *int64_out);
+int av_opt_eval_float (void *obj, const AVOption *o, const char *val, float *float_out);
+int av_opt_eval_double(void *obj, const AVOption *o, const char *val, double *double_out);
+int av_opt_eval_q (void *obj, const AVOption *o, const char *val, AVRational *q_out);
+/**
+ * @}
+ */
+
+#define AV_OPT_SEARCH_CHILDREN 0x0001 /**< Search in possible children of the
+ given object first. */
+/**
+ * The obj passed to av_opt_find() is fake -- only a double pointer to AVClass
+ * instead of a required pointer to a struct containing AVClass. This is
+ * useful for searching for options without needing to allocate the corresponding
+ * object.
+ */
+#define AV_OPT_SEARCH_FAKE_OBJ 0x0002
+
+/**
+ * Look for an option in an object. Consider only options which
+ * have all the specified flags set.
+ *
+ * @param[in] obj A pointer to a struct whose first element is a
+ * pointer to an AVClass.
+ * Alternatively a double pointer to an AVClass, if
+ * AV_OPT_SEARCH_FAKE_OBJ search flag is set.
+ * @param[in] name The name of the option to look for.
+ * @param[in] unit When searching for named constants, name of the unit
+ * it belongs to.
+ * @param opt_flags Find only options with all the specified flags set (AV_OPT_FLAG).
+ * @param search_flags A combination of AV_OPT_SEARCH_*.
+ *
+ * @return A pointer to the option found, or NULL if no option
+ * was found.
+ *
+ * @note Options found with AV_OPT_SEARCH_CHILDREN flag may not be settable
+ * directly with av_set_string3(). Use special calls which take an options
+ * AVDictionary (e.g. avformat_open_input()) to set options found with this
+ * flag.
+ */
+const AVOption *av_opt_find(void *obj, const char *name, const char *unit,
+ int opt_flags, int search_flags);
+
+/**
+ * Look for an option in an object. Consider only options which
+ * have all the specified flags set.
+ *
+ * @param[in] obj A pointer to a struct whose first element is a
+ * pointer to an AVClass.
+ * Alternatively a double pointer to an AVClass, if
+ * AV_OPT_SEARCH_FAKE_OBJ search flag is set.
+ * @param[in] name The name of the option to look for.
+ * @param[in] unit When searching for named constants, name of the unit
+ * it belongs to.
+ * @param opt_flags Find only options with all the specified flags set (AV_OPT_FLAG).
+ * @param search_flags A combination of AV_OPT_SEARCH_*.
+ * @param[out] target_obj if non-NULL, an object to which the option belongs will be
+ * written here. It may be different from obj if AV_OPT_SEARCH_CHILDREN is present
+ * in search_flags. This parameter is ignored if search_flags contain
+ * AV_OPT_SEARCH_FAKE_OBJ.
+ *
+ * @return A pointer to the option found, or NULL if no option
+ * was found.
+ */
+const AVOption *av_opt_find2(void *obj, const char *name, const char *unit,
+ int opt_flags, int search_flags, void **target_obj);
+
+/**
+ * Iterate over all AVOptions belonging to obj.
+ *
+ * @param obj an AVOptions-enabled struct or a double pointer to an
+ * AVClass describing it.
+ * @param prev result of the previous call to av_opt_next() on this object
+ * or NULL
+ * @return next AVOption or NULL
+ */
+const AVOption *av_opt_next(void *obj, const AVOption *prev);
+
+/**
+ * Iterate over AVOptions-enabled children of obj.
+ *
+ * @param prev result of a previous call to this function or NULL
+ * @return next AVOptions-enabled child or NULL
+ */
+void *av_opt_child_next(void *obj, void *prev);
+
+/**
+ * Iterate over potential AVOptions-enabled children of parent.
+ *
+ * @param prev result of a previous call to this function or NULL
+ * @return AVClass corresponding to next potential child or NULL
+ */
+const AVClass *av_opt_child_class_next(const AVClass *parent, const AVClass *prev);
+
+/**
+ * @defgroup opt_set_funcs Option setting functions
+ * @{
+ * Those functions set the field of obj with the given name to value.
+ *
+ * @param[in] obj A struct whose first element is a pointer to an AVClass.
+ * @param[in] name the name of the field to set
+ * @param[in] val The value to set. In case of av_opt_set() if the field is not
+ * of a string type, then the given string is parsed.
+ * SI postfixes and some named scalars are supported.
+ * If the field is of a numeric type, it has to be a numeric or named
+ * scalar. Behavior with more than one scalar and +- infix operators
+ * is undefined.
+ * If the field is of a flags type, it has to be a sequence of numeric
+ * scalars or named flags separated by '+' or '-'. Prefixing a flag
+ * with '+' causes it to be set without affecting the other flags;
+ * similarly, '-' unsets a flag.
+ * @param search_flags flags passed to av_opt_find2. I.e. if AV_OPT_SEARCH_CHILDREN
+ * is passed here, then the option may be set on a child of obj.
+ *
+ * @return 0 if the value has been set, or an AVERROR code in case of
+ * error:
+ * AVERROR_OPTION_NOT_FOUND if no matching option exists
+ * AVERROR(ERANGE) if the value is out of range
+ * AVERROR(EINVAL) if the value is not valid
+ */
+int av_opt_set (void *obj, const char *name, const char *val, int search_flags);
+int av_opt_set_int (void *obj, const char *name, int64_t val, int search_flags);
+int av_opt_set_double(void *obj, const char *name, double val, int search_flags);
+int av_opt_set_q (void *obj, const char *name, AVRational val, int search_flags);
+int av_opt_set_bin (void *obj, const char *name, const uint8_t *val, int size, int search_flags);
+/**
+ * @}
+ */
+
+/**
+ * @defgroup opt_get_funcs Option getting functions
+ * @{
+ * Those functions get a value of the option with the given name from an object.
+ *
+ * @param[in] obj a struct whose first element is a pointer to an AVClass.
+ * @param[in] name name of the option to get.
+ * @param[in] search_flags flags passed to av_opt_find2. I.e. if AV_OPT_SEARCH_CHILDREN
+ * is passed here, then the option may be found in a child of obj.
+ * @param[out] out_val value of the option will be written here
+ * @return 0 on success, a negative error code otherwise
+ */
+/**
+ * @note the returned string will av_malloc()ed and must be av_free()ed by the caller
+ */
+int av_opt_get (void *obj, const char *name, int search_flags, uint8_t **out_val);
+int av_opt_get_int (void *obj, const char *name, int search_flags, int64_t *out_val);
+int av_opt_get_double(void *obj, const char *name, int search_flags, double *out_val);
+int av_opt_get_q (void *obj, const char *name, int search_flags, AVRational *out_val);
+/**
+ * @}
+ * @}
+ */
+
+#endif /* AVUTIL_OPT_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/parseutils.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/parseutils.h
new file mode 100644
index 0000000000..0844abb2f0
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/parseutils.h
@@ -0,0 +1,124 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_PARSEUTILS_H
+#define AVUTIL_PARSEUTILS_H
+
+#include <time.h>
+
+#include "rational.h"
+
+/**
+ * @file
+ * misc parsing utilities
+ */
+
+/**
+ * Parse str and put in width_ptr and height_ptr the detected values.
+ *
+ * @param[in,out] width_ptr pointer to the variable which will contain the detected
+ * width value
+ * @param[in,out] height_ptr pointer to the variable which will contain the detected
+ * height value
+ * @param[in] str the string to parse: it has to be a string in the format
+ * width x height or a valid video size abbreviation.
+ * @return >= 0 on success, a negative error code otherwise
+ */
+int av_parse_video_size(int *width_ptr, int *height_ptr, const char *str);
+
+/**
+ * Parse str and store the detected values in *rate.
+ *
+ * @param[in,out] rate pointer to the AVRational which will contain the detected
+ * frame rate
+ * @param[in] str the string to parse: it has to be a string in the format
+ * rate_num / rate_den, a float number or a valid video rate abbreviation
+ * @return >= 0 on success, a negative error code otherwise
+ */
+int av_parse_video_rate(AVRational *rate, const char *str);
+
+/**
+ * Put the RGBA values that correspond to color_string in rgba_color.
+ *
+ * @param color_string a string specifying a color. It can be the name of
+ * a color (case insensitive match) or a [0x|#]RRGGBB[AA] sequence,
+ * possibly followed by "@" and a string representing the alpha
+ * component.
+ * The alpha component may be a string composed by "0x" followed by an
+ * hexadecimal number or a decimal number between 0.0 and 1.0, which
+ * represents the opacity value (0x00/0.0 means completely transparent,
+ * 0xff/1.0 completely opaque).
+ * If the alpha component is not specified then 0xff is assumed.
+ * The string "random" will result in a random color.
+ * @param slen length of the initial part of color_string containing the
+ * color. It can be set to -1 if color_string is a null terminated string
+ * containing nothing else than the color.
+ * @return >= 0 in case of success, a negative value in case of
+ * failure (for example if color_string cannot be parsed).
+ */
+int av_parse_color(uint8_t *rgba_color, const char *color_string, int slen,
+ void *log_ctx);
+
+/**
+ * Parse timestr and return in *time a corresponding number of
+ * microseconds.
+ *
+ * @param timeval puts here the number of microseconds corresponding
+ * to the string in timestr. If the string represents a duration, it
+ * is the number of microseconds contained in the time interval. If
+ * the string is a date, is the number of microseconds since 1st of
+ * January, 1970 up to the time of the parsed date. If timestr cannot
+ * be successfully parsed, set *time to INT64_MIN.
+
+ * @param timestr a string representing a date or a duration.
+ * - If a date the syntax is:
+ * @code
+ * [{YYYY-MM-DD|YYYYMMDD}[T|t| ]]{{HH[:MM[:SS[.m...]]]}|{HH[MM[SS[.m...]]]}}[Z]
+ * now
+ * @endcode
+ * If the value is "now" it takes the current time.
+ * Time is local time unless Z is appended, in which case it is
+ * interpreted as UTC.
+ * If the year-month-day part is not specified it takes the current
+ * year-month-day.
+ * - If a duration the syntax is:
+ * @code
+ * [-]HH[:MM[:SS[.m...]]]
+ * [-]S+[.m...]
+ * @endcode
+ * @param duration flag which tells how to interpret timestr, if not
+ * zero timestr is interpreted as a duration, otherwise as a date
+ * @return 0 in case of success, a negative value corresponding to an
+ * AVERROR code otherwise
+ */
+int av_parse_time(int64_t *timeval, const char *timestr, int duration);
+
+/**
+ * Attempt to find a specific tag in a URL.
+ *
+ * syntax: '?tag1=val1&tag2=val2...'. Little URL decoding is done.
+ * Return 1 if found.
+ */
+int av_find_info_tag(char *arg, int arg_size, const char *tag1, const char *info);
+
+/**
+ * Convert the decomposed UTC time in tm to a time_t value.
+ */
+time_t av_timegm(struct tm *tm);
+
+#endif /* AVUTIL_PARSEUTILS_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/pixdesc.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/pixdesc.h
new file mode 100644
index 0000000000..e5a16f418b
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/pixdesc.h
@@ -0,0 +1,276 @@
+/*
+ * pixel format descriptor
+ * Copyright (c) 2009 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_PIXDESC_H
+#define AVUTIL_PIXDESC_H
+
+#include <inttypes.h>
+
+#include "attributes.h"
+#include "pixfmt.h"
+
+typedef struct AVComponentDescriptor{
+ uint16_t plane :2; ///< which of the 4 planes contains the component
+
+ /**
+ * Number of elements between 2 horizontally consecutive pixels minus 1.
+ * Elements are bits for bitstream formats, bytes otherwise.
+ */
+ uint16_t step_minus1 :3;
+
+ /**
+ * Number of elements before the component of the first pixel plus 1.
+ * Elements are bits for bitstream formats, bytes otherwise.
+ */
+ uint16_t offset_plus1 :3;
+ uint16_t shift :3; ///< number of least significant bits that must be shifted away to get the value
+ uint16_t depth_minus1 :4; ///< number of bits in the component minus 1
+}AVComponentDescriptor;
+
+/**
+ * Descriptor that unambiguously describes how the bits of a pixel are
+ * stored in the up to 4 data planes of an image. It also stores the
+ * subsampling factors and number of components.
+ *
+ * @note This is separate of the colorspace (RGB, YCbCr, YPbPr, JPEG-style YUV
+ * and all the YUV variants) AVPixFmtDescriptor just stores how values
+ * are stored not what these values represent.
+ */
+typedef struct AVPixFmtDescriptor{
+ const char *name;
+ uint8_t nb_components; ///< The number of components each pixel has, (1-4)
+
+ /**
+ * Amount to shift the luma width right to find the chroma width.
+ * For YV12 this is 1 for example.
+ * chroma_width = -((-luma_width) >> log2_chroma_w)
+ * The note above is needed to ensure rounding up.
+ * This value only refers to the chroma components.
+ */
+ uint8_t log2_chroma_w; ///< chroma_width = -((-luma_width )>>log2_chroma_w)
+
+ /**
+ * Amount to shift the luma height right to find the chroma height.
+ * For YV12 this is 1 for example.
+ * chroma_height= -((-luma_height) >> log2_chroma_h)
+ * The note above is needed to ensure rounding up.
+ * This value only refers to the chroma components.
+ */
+ uint8_t log2_chroma_h;
+ uint8_t flags;
+
+ /**
+ * Parameters that describe how pixels are packed. If the format
+ * has chroma components, they must be stored in comp[1] and
+ * comp[2].
+ */
+ AVComponentDescriptor comp[4];
+}AVPixFmtDescriptor;
+
+/**
+ * Pixel format is big-endian.
+ */
+#define AV_PIX_FMT_FLAG_BE (1 << 0)
+/**
+ * Pixel format has a palette in data[1], values are indexes in this palette.
+ */
+#define AV_PIX_FMT_FLAG_PAL (1 << 1)
+/**
+ * All values of a component are bit-wise packed end to end.
+ */
+#define AV_PIX_FMT_FLAG_BITSTREAM (1 << 2)
+/**
+ * Pixel format is an HW accelerated format.
+ */
+#define AV_PIX_FMT_FLAG_HWACCEL (1 << 3)
+/**
+ * At least one pixel component is not in the first data plane.
+ */
+#define AV_PIX_FMT_FLAG_PLANAR (1 << 4)
+/**
+ * The pixel format contains RGB-like data (as opposed to YUV/grayscale).
+ */
+#define AV_PIX_FMT_FLAG_RGB (1 << 5)
+/**
+ * The pixel format is "pseudo-paletted". This means that Libav treats it as
+ * paletted internally, but the palette is generated by the decoder and is not
+ * stored in the file.
+ */
+#define AV_PIX_FMT_FLAG_PSEUDOPAL (1 << 6)
+/**
+ * The pixel format has an alpha channel.
+ */
+#define AV_PIX_FMT_FLAG_ALPHA (1 << 7)
+
+#if FF_API_PIX_FMT
+/**
+ * @deprecated use the AV_PIX_FMT_FLAG_* flags
+ */
+#define PIX_FMT_BE AV_PIX_FMT_FLAG_BE
+#define PIX_FMT_PAL AV_PIX_FMT_FLAG_PAL
+#define PIX_FMT_BITSTREAM AV_PIX_FMT_FLAG_BITSTREAM
+#define PIX_FMT_HWACCEL AV_PIX_FMT_FLAG_HWACCEL
+#define PIX_FMT_PLANAR AV_PIX_FMT_FLAG_PLANAR
+#define PIX_FMT_RGB AV_PIX_FMT_FLAG_RGB
+#define PIX_FMT_PSEUDOPAL AV_PIX_FMT_FLAG_PSEUDOPAL
+#define PIX_FMT_ALPHA AV_PIX_FMT_FLAG_ALPHA
+#endif
+
+#if FF_API_PIX_FMT_DESC
+/**
+ * The array of all the pixel format descriptors.
+ */
+extern attribute_deprecated const AVPixFmtDescriptor av_pix_fmt_descriptors[];
+#endif
+
+/**
+ * Read a line from an image, and write the values of the
+ * pixel format component c to dst.
+ *
+ * @param data the array containing the pointers to the planes of the image
+ * @param linesize the array containing the linesizes of the image
+ * @param desc the pixel format descriptor for the image
+ * @param x the horizontal coordinate of the first pixel to read
+ * @param y the vertical coordinate of the first pixel to read
+ * @param w the width of the line to read, that is the number of
+ * values to write to dst
+ * @param read_pal_component if not zero and the format is a paletted
+ * format writes the values corresponding to the palette
+ * component c in data[1] to dst, rather than the palette indexes in
+ * data[0]. The behavior is undefined if the format is not paletted.
+ */
+void av_read_image_line(uint16_t *dst, const uint8_t *data[4], const int linesize[4],
+ const AVPixFmtDescriptor *desc, int x, int y, int c, int w, int read_pal_component);
+
+/**
+ * Write the values from src to the pixel format component c of an
+ * image line.
+ *
+ * @param src array containing the values to write
+ * @param data the array containing the pointers to the planes of the
+ * image to write into. It is supposed to be zeroed.
+ * @param linesize the array containing the linesizes of the image
+ * @param desc the pixel format descriptor for the image
+ * @param x the horizontal coordinate of the first pixel to write
+ * @param y the vertical coordinate of the first pixel to write
+ * @param w the width of the line to write, that is the number of
+ * values to write to the image line
+ */
+void av_write_image_line(const uint16_t *src, uint8_t *data[4], const int linesize[4],
+ const AVPixFmtDescriptor *desc, int x, int y, int c, int w);
+
+/**
+ * Return the pixel format corresponding to name.
+ *
+ * If there is no pixel format with name name, then looks for a
+ * pixel format with the name corresponding to the native endian
+ * format of name.
+ * For example in a little-endian system, first looks for "gray16",
+ * then for "gray16le".
+ *
+ * Finally if no pixel format has been found, returns PIX_FMT_NONE.
+ */
+enum AVPixelFormat av_get_pix_fmt(const char *name);
+
+/**
+ * Return the short name for a pixel format, NULL in case pix_fmt is
+ * unknown.
+ *
+ * @see av_get_pix_fmt(), av_get_pix_fmt_string()
+ */
+const char *av_get_pix_fmt_name(enum AVPixelFormat pix_fmt);
+
+/**
+ * Print in buf the string corresponding to the pixel format with
+ * number pix_fmt, or an header if pix_fmt is negative.
+ *
+ * @param buf the buffer where to write the string
+ * @param buf_size the size of buf
+ * @param pix_fmt the number of the pixel format to print the
+ * corresponding info string, or a negative value to print the
+ * corresponding header.
+ */
+char *av_get_pix_fmt_string (char *buf, int buf_size, enum AVPixelFormat pix_fmt);
+
+/**
+ * Return the number of bits per pixel used by the pixel format
+ * described by pixdesc. Note that this is not the same as the number
+ * of bits per sample.
+ *
+ * The returned number of bits refers to the number of bits actually
+ * used for storing the pixel information, that is padding bits are
+ * not counted.
+ */
+int av_get_bits_per_pixel(const AVPixFmtDescriptor *pixdesc);
+
+/**
+ * @return a pixel format descriptor for provided pixel format or NULL if
+ * this pixel format is unknown.
+ */
+const AVPixFmtDescriptor *av_pix_fmt_desc_get(enum AVPixelFormat pix_fmt);
+
+/**
+ * Iterate over all pixel format descriptors known to libavutil.
+ *
+ * @param prev previous descriptor. NULL to get the first descriptor.
+ *
+ * @return next descriptor or NULL after the last descriptor
+ */
+const AVPixFmtDescriptor *av_pix_fmt_desc_next(const AVPixFmtDescriptor *prev);
+
+/**
+ * @return an AVPixelFormat id described by desc, or AV_PIX_FMT_NONE if desc
+ * is not a valid pointer to a pixel format descriptor.
+ */
+enum AVPixelFormat av_pix_fmt_desc_get_id(const AVPixFmtDescriptor *desc);
+
+/**
+ * Utility function to access log2_chroma_w log2_chroma_h from
+ * the pixel format AVPixFmtDescriptor.
+ *
+ * @param[in] pix_fmt the pixel format
+ * @param[out] h_shift store log2_chroma_h
+ * @param[out] v_shift store log2_chroma_w
+ *
+ * @return 0 on success, AVERROR(ENOSYS) on invalid or unknown pixel format
+ */
+int av_pix_fmt_get_chroma_sub_sample(enum AVPixelFormat pix_fmt,
+ int *h_shift, int *v_shift);
+
+/**
+ * @return number of planes in pix_fmt, a negative AVERROR if pix_fmt is not a
+ * valid pixel format.
+ */
+int av_pix_fmt_count_planes(enum AVPixelFormat pix_fmt);
+
+
+/**
+ * Utility function to swap the endianness of a pixel format.
+ *
+ * @param[in] pix_fmt the pixel format
+ *
+ * @return pixel format with swapped endianness if it exists,
+ * otherwise AV_PIX_FMT_NONE
+ */
+enum AVPixelFormat av_pix_fmt_swap_endianness(enum AVPixelFormat pix_fmt);
+
+
+#endif /* AVUTIL_PIXDESC_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/pixfmt.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/pixfmt.h
new file mode 100644
index 0000000000..0d6e0a3007
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/pixfmt.h
@@ -0,0 +1,283 @@
+/*
+ * copyright (c) 2006 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_PIXFMT_H
+#define AVUTIL_PIXFMT_H
+
+/**
+ * @file
+ * pixel format definitions
+ *
+ */
+
+#include "libavutil/avconfig.h"
+#include "version.h"
+
+/**
+ * Pixel format.
+ *
+ * @note
+ * PIX_FMT_RGB32 is handled in an endian-specific manner. An RGBA
+ * color is put together as:
+ * (A << 24) | (R << 16) | (G << 8) | B
+ * This is stored as BGRA on little-endian CPU architectures and ARGB on
+ * big-endian CPUs.
+ *
+ * @par
+ * When the pixel format is palettized RGB (PIX_FMT_PAL8), the palettized
+ * image data is stored in AVFrame.data[0]. The palette is transported in
+ * AVFrame.data[1], is 1024 bytes long (256 4-byte entries) and is
+ * formatted the same as in PIX_FMT_RGB32 described above (i.e., it is
+ * also endian-specific). Note also that the individual RGB palette
+ * components stored in AVFrame.data[1] should be in the range 0..255.
+ * This is important as many custom PAL8 video codecs that were designed
+ * to run on the IBM VGA graphics adapter use 6-bit palette components.
+ *
+ * @par
+ * For all the 8bit per pixel formats, an RGB32 palette is in data[1] like
+ * for pal8. This palette is filled in automatically by the function
+ * allocating the picture.
+ *
+ * @note
+ * Make sure that all newly added big-endian formats have pix_fmt & 1 == 1
+ * and that all newly added little-endian formats have pix_fmt & 1 == 0.
+ * This allows simpler detection of big vs little-endian.
+ */
+enum AVPixelFormat {
+ AV_PIX_FMT_NONE = -1,
+ AV_PIX_FMT_YUV420P, ///< planar YUV 4:2:0, 12bpp, (1 Cr & Cb sample per 2x2 Y samples)
+ AV_PIX_FMT_YUYV422, ///< packed YUV 4:2:2, 16bpp, Y0 Cb Y1 Cr
+ AV_PIX_FMT_RGB24, ///< packed RGB 8:8:8, 24bpp, RGBRGB...
+ AV_PIX_FMT_BGR24, ///< packed RGB 8:8:8, 24bpp, BGRBGR...
+ AV_PIX_FMT_YUV422P, ///< planar YUV 4:2:2, 16bpp, (1 Cr & Cb sample per 2x1 Y samples)
+ AV_PIX_FMT_YUV444P, ///< planar YUV 4:4:4, 24bpp, (1 Cr & Cb sample per 1x1 Y samples)
+ AV_PIX_FMT_YUV410P, ///< planar YUV 4:1:0, 9bpp, (1 Cr & Cb sample per 4x4 Y samples)
+ AV_PIX_FMT_YUV411P, ///< planar YUV 4:1:1, 12bpp, (1 Cr & Cb sample per 4x1 Y samples)
+ AV_PIX_FMT_GRAY8, ///< Y , 8bpp
+ AV_PIX_FMT_MONOWHITE, ///< Y , 1bpp, 0 is white, 1 is black, in each byte pixels are ordered from the msb to the lsb
+ AV_PIX_FMT_MONOBLACK, ///< Y , 1bpp, 0 is black, 1 is white, in each byte pixels are ordered from the msb to the lsb
+ AV_PIX_FMT_PAL8, ///< 8 bit with PIX_FMT_RGB32 palette
+ AV_PIX_FMT_YUVJ420P, ///< planar YUV 4:2:0, 12bpp, full scale (JPEG), deprecated in favor of PIX_FMT_YUV420P and setting color_range
+ AV_PIX_FMT_YUVJ422P, ///< planar YUV 4:2:2, 16bpp, full scale (JPEG), deprecated in favor of PIX_FMT_YUV422P and setting color_range
+ AV_PIX_FMT_YUVJ444P, ///< planar YUV 4:4:4, 24bpp, full scale (JPEG), deprecated in favor of PIX_FMT_YUV444P and setting color_range
+#if FF_API_XVMC
+ AV_PIX_FMT_XVMC_MPEG2_MC,///< XVideo Motion Acceleration via common packet passing
+ AV_PIX_FMT_XVMC_MPEG2_IDCT,
+#endif /* FF_API_XVMC */
+ AV_PIX_FMT_UYVY422, ///< packed YUV 4:2:2, 16bpp, Cb Y0 Cr Y1
+ AV_PIX_FMT_UYYVYY411, ///< packed YUV 4:1:1, 12bpp, Cb Y0 Y1 Cr Y2 Y3
+ AV_PIX_FMT_BGR8, ///< packed RGB 3:3:2, 8bpp, (msb)2B 3G 3R(lsb)
+ AV_PIX_FMT_BGR4, ///< packed RGB 1:2:1 bitstream, 4bpp, (msb)1B 2G 1R(lsb), a byte contains two pixels, the first pixel in the byte is the one composed by the 4 msb bits
+ AV_PIX_FMT_BGR4_BYTE, ///< packed RGB 1:2:1, 8bpp, (msb)1B 2G 1R(lsb)
+ AV_PIX_FMT_RGB8, ///< packed RGB 3:3:2, 8bpp, (msb)2R 3G 3B(lsb)
+ AV_PIX_FMT_RGB4, ///< packed RGB 1:2:1 bitstream, 4bpp, (msb)1R 2G 1B(lsb), a byte contains two pixels, the first pixel in the byte is the one composed by the 4 msb bits
+ AV_PIX_FMT_RGB4_BYTE, ///< packed RGB 1:2:1, 8bpp, (msb)1R 2G 1B(lsb)
+ AV_PIX_FMT_NV12, ///< planar YUV 4:2:0, 12bpp, 1 plane for Y and 1 plane for the UV components, which are interleaved (first byte U and the following byte V)
+ AV_PIX_FMT_NV21, ///< as above, but U and V bytes are swapped
+
+ AV_PIX_FMT_ARGB, ///< packed ARGB 8:8:8:8, 32bpp, ARGBARGB...
+ AV_PIX_FMT_RGBA, ///< packed RGBA 8:8:8:8, 32bpp, RGBARGBA...
+ AV_PIX_FMT_ABGR, ///< packed ABGR 8:8:8:8, 32bpp, ABGRABGR...
+ AV_PIX_FMT_BGRA, ///< packed BGRA 8:8:8:8, 32bpp, BGRABGRA...
+
+ AV_PIX_FMT_GRAY16BE, ///< Y , 16bpp, big-endian
+ AV_PIX_FMT_GRAY16LE, ///< Y , 16bpp, little-endian
+ AV_PIX_FMT_YUV440P, ///< planar YUV 4:4:0 (1 Cr & Cb sample per 1x2 Y samples)
+ AV_PIX_FMT_YUVJ440P, ///< planar YUV 4:4:0 full scale (JPEG), deprecated in favor of PIX_FMT_YUV440P and setting color_range
+ AV_PIX_FMT_YUVA420P, ///< planar YUV 4:2:0, 20bpp, (1 Cr & Cb sample per 2x2 Y & A samples)
+#if FF_API_VDPAU
+ AV_PIX_FMT_VDPAU_H264,///< H.264 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ AV_PIX_FMT_VDPAU_MPEG1,///< MPEG-1 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ AV_PIX_FMT_VDPAU_MPEG2,///< MPEG-2 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ AV_PIX_FMT_VDPAU_WMV3,///< WMV3 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+ AV_PIX_FMT_VDPAU_VC1, ///< VC-1 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+#endif
+ AV_PIX_FMT_RGB48BE, ///< packed RGB 16:16:16, 48bpp, 16R, 16G, 16B, the 2-byte value for each R/G/B component is stored as big-endian
+ AV_PIX_FMT_RGB48LE, ///< packed RGB 16:16:16, 48bpp, 16R, 16G, 16B, the 2-byte value for each R/G/B component is stored as little-endian
+
+ AV_PIX_FMT_RGB565BE, ///< packed RGB 5:6:5, 16bpp, (msb) 5R 6G 5B(lsb), big-endian
+ AV_PIX_FMT_RGB565LE, ///< packed RGB 5:6:5, 16bpp, (msb) 5R 6G 5B(lsb), little-endian
+ AV_PIX_FMT_RGB555BE, ///< packed RGB 5:5:5, 16bpp, (msb)1A 5R 5G 5B(lsb), big-endian, most significant bit to 0
+ AV_PIX_FMT_RGB555LE, ///< packed RGB 5:5:5, 16bpp, (msb)1A 5R 5G 5B(lsb), little-endian, most significant bit to 0
+
+ AV_PIX_FMT_BGR565BE, ///< packed BGR 5:6:5, 16bpp, (msb) 5B 6G 5R(lsb), big-endian
+ AV_PIX_FMT_BGR565LE, ///< packed BGR 5:6:5, 16bpp, (msb) 5B 6G 5R(lsb), little-endian
+ AV_PIX_FMT_BGR555BE, ///< packed BGR 5:5:5, 16bpp, (msb)1A 5B 5G 5R(lsb), big-endian, most significant bit to 1
+ AV_PIX_FMT_BGR555LE, ///< packed BGR 5:5:5, 16bpp, (msb)1A 5B 5G 5R(lsb), little-endian, most significant bit to 1
+
+ AV_PIX_FMT_VAAPI_MOCO, ///< HW acceleration through VA API at motion compensation entry-point, Picture.data[3] contains a vaapi_render_state struct which contains macroblocks as well as various fields extracted from headers
+ AV_PIX_FMT_VAAPI_IDCT, ///< HW acceleration through VA API at IDCT entry-point, Picture.data[3] contains a vaapi_render_state struct which contains fields extracted from headers
+ AV_PIX_FMT_VAAPI_VLD, ///< HW decoding through VA API, Picture.data[3] contains a vaapi_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+
+ AV_PIX_FMT_YUV420P16LE, ///< planar YUV 4:2:0, 24bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian
+ AV_PIX_FMT_YUV420P16BE, ///< planar YUV 4:2:0, 24bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian
+ AV_PIX_FMT_YUV422P16LE, ///< planar YUV 4:2:2, 32bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ AV_PIX_FMT_YUV422P16BE, ///< planar YUV 4:2:2, 32bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+ AV_PIX_FMT_YUV444P16LE, ///< planar YUV 4:4:4, 48bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian
+ AV_PIX_FMT_YUV444P16BE, ///< planar YUV 4:4:4, 48bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian
+#if FF_API_VDPAU
+ AV_PIX_FMT_VDPAU_MPEG4, ///< MPEG4 HW decoding with VDPAU, data[0] contains a vdpau_render_state struct which contains the bitstream of the slices as well as various fields extracted from headers
+#endif
+ AV_PIX_FMT_DXVA2_VLD, ///< HW decoding through DXVA2, Picture.data[3] contains a LPDIRECT3DSURFACE9 pointer
+
+ AV_PIX_FMT_RGB444LE, ///< packed RGB 4:4:4, 16bpp, (msb)4A 4R 4G 4B(lsb), little-endian, most significant bits to 0
+ AV_PIX_FMT_RGB444BE, ///< packed RGB 4:4:4, 16bpp, (msb)4A 4R 4G 4B(lsb), big-endian, most significant bits to 0
+ AV_PIX_FMT_BGR444LE, ///< packed BGR 4:4:4, 16bpp, (msb)4A 4B 4G 4R(lsb), little-endian, most significant bits to 1
+ AV_PIX_FMT_BGR444BE, ///< packed BGR 4:4:4, 16bpp, (msb)4A 4B 4G 4R(lsb), big-endian, most significant bits to 1
+ AV_PIX_FMT_Y400A, ///< 8bit gray, 8bit alpha
+ AV_PIX_FMT_BGR48BE, ///< packed RGB 16:16:16, 48bpp, 16B, 16G, 16R, the 2-byte value for each R/G/B component is stored as big-endian
+ AV_PIX_FMT_BGR48LE, ///< packed RGB 16:16:16, 48bpp, 16B, 16G, 16R, the 2-byte value for each R/G/B component is stored as little-endian
+ AV_PIX_FMT_YUV420P9BE, ///< planar YUV 4:2:0, 13.5bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian
+ AV_PIX_FMT_YUV420P9LE, ///< planar YUV 4:2:0, 13.5bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian
+ AV_PIX_FMT_YUV420P10BE,///< planar YUV 4:2:0, 15bpp, (1 Cr & Cb sample per 2x2 Y samples), big-endian
+ AV_PIX_FMT_YUV420P10LE,///< planar YUV 4:2:0, 15bpp, (1 Cr & Cb sample per 2x2 Y samples), little-endian
+ AV_PIX_FMT_YUV422P10BE,///< planar YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+ AV_PIX_FMT_YUV422P10LE,///< planar YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ AV_PIX_FMT_YUV444P9BE, ///< planar YUV 4:4:4, 27bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian
+ AV_PIX_FMT_YUV444P9LE, ///< planar YUV 4:4:4, 27bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian
+ AV_PIX_FMT_YUV444P10BE,///< planar YUV 4:4:4, 30bpp, (1 Cr & Cb sample per 1x1 Y samples), big-endian
+ AV_PIX_FMT_YUV444P10LE,///< planar YUV 4:4:4, 30bpp, (1 Cr & Cb sample per 1x1 Y samples), little-endian
+ AV_PIX_FMT_YUV422P9BE, ///< planar YUV 4:2:2, 18bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+ AV_PIX_FMT_YUV422P9LE, ///< planar YUV 4:2:2, 18bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ AV_PIX_FMT_VDA_VLD, ///< hardware decoding through VDA
+ AV_PIX_FMT_GBRP, ///< planar GBR 4:4:4 24bpp
+ AV_PIX_FMT_GBRP9BE, ///< planar GBR 4:4:4 27bpp, big-endian
+ AV_PIX_FMT_GBRP9LE, ///< planar GBR 4:4:4 27bpp, little-endian
+ AV_PIX_FMT_GBRP10BE, ///< planar GBR 4:4:4 30bpp, big-endian
+ AV_PIX_FMT_GBRP10LE, ///< planar GBR 4:4:4 30bpp, little-endian
+ AV_PIX_FMT_GBRP16BE, ///< planar GBR 4:4:4 48bpp, big-endian
+ AV_PIX_FMT_GBRP16LE, ///< planar GBR 4:4:4 48bpp, little-endian
+ AV_PIX_FMT_YUVA422P, ///< planar YUV 4:2:2 24bpp, (1 Cr & Cb sample per 2x1 Y & A samples)
+ AV_PIX_FMT_YUVA444P, ///< planar YUV 4:4:4 32bpp, (1 Cr & Cb sample per 1x1 Y & A samples)
+ AV_PIX_FMT_YUVA420P9BE, ///< planar YUV 4:2:0 22.5bpp, (1 Cr & Cb sample per 2x2 Y & A samples), big-endian
+ AV_PIX_FMT_YUVA420P9LE, ///< planar YUV 4:2:0 22.5bpp, (1 Cr & Cb sample per 2x2 Y & A samples), little-endian
+ AV_PIX_FMT_YUVA422P9BE, ///< planar YUV 4:2:2 27bpp, (1 Cr & Cb sample per 2x1 Y & A samples), big-endian
+ AV_PIX_FMT_YUVA422P9LE, ///< planar YUV 4:2:2 27bpp, (1 Cr & Cb sample per 2x1 Y & A samples), little-endian
+ AV_PIX_FMT_YUVA444P9BE, ///< planar YUV 4:4:4 36bpp, (1 Cr & Cb sample per 1x1 Y & A samples), big-endian
+ AV_PIX_FMT_YUVA444P9LE, ///< planar YUV 4:4:4 36bpp, (1 Cr & Cb sample per 1x1 Y & A samples), little-endian
+ AV_PIX_FMT_YUVA420P10BE, ///< planar YUV 4:2:0 25bpp, (1 Cr & Cb sample per 2x2 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA420P10LE, ///< planar YUV 4:2:0 25bpp, (1 Cr & Cb sample per 2x2 Y & A samples, little-endian)
+ AV_PIX_FMT_YUVA422P10BE, ///< planar YUV 4:2:2 30bpp, (1 Cr & Cb sample per 2x1 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA422P10LE, ///< planar YUV 4:2:2 30bpp, (1 Cr & Cb sample per 2x1 Y & A samples, little-endian)
+ AV_PIX_FMT_YUVA444P10BE, ///< planar YUV 4:4:4 40bpp, (1 Cr & Cb sample per 1x1 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA444P10LE, ///< planar YUV 4:4:4 40bpp, (1 Cr & Cb sample per 1x1 Y & A samples, little-endian)
+ AV_PIX_FMT_YUVA420P16BE, ///< planar YUV 4:2:0 40bpp, (1 Cr & Cb sample per 2x2 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA420P16LE, ///< planar YUV 4:2:0 40bpp, (1 Cr & Cb sample per 2x2 Y & A samples, little-endian)
+ AV_PIX_FMT_YUVA422P16BE, ///< planar YUV 4:2:2 48bpp, (1 Cr & Cb sample per 2x1 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA422P16LE, ///< planar YUV 4:2:2 48bpp, (1 Cr & Cb sample per 2x1 Y & A samples, little-endian)
+ AV_PIX_FMT_YUVA444P16BE, ///< planar YUV 4:4:4 64bpp, (1 Cr & Cb sample per 1x1 Y & A samples, big-endian)
+ AV_PIX_FMT_YUVA444P16LE, ///< planar YUV 4:4:4 64bpp, (1 Cr & Cb sample per 1x1 Y & A samples, little-endian)
+ AV_PIX_FMT_VDPAU, ///< HW acceleration through VDPAU, Picture.data[3] contains a VdpVideoSurface
+ AV_PIX_FMT_XYZ12LE, ///< packed XYZ 4:4:4, 36 bpp, (msb) 12X, 12Y, 12Z (lsb), the 2-byte value for each X/Y/Z is stored as little-endian, the 4 lower bits are set to 0
+ AV_PIX_FMT_XYZ12BE, ///< packed XYZ 4:4:4, 36 bpp, (msb) 12X, 12Y, 12Z (lsb), the 2-byte value for each X/Y/Z is stored as big-endian, the 4 lower bits are set to 0
+ AV_PIX_FMT_NV16, ///< interleaved chroma YUV 4:2:2, 16bpp, (1 Cr & Cb sample per 2x1 Y samples)
+ AV_PIX_FMT_NV20LE, ///< interleaved chroma YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), little-endian
+ AV_PIX_FMT_NV20BE, ///< interleaved chroma YUV 4:2:2, 20bpp, (1 Cr & Cb sample per 2x1 Y samples), big-endian
+ AV_PIX_FMT_NB, ///< number of pixel formats, DO NOT USE THIS if you want to link with shared libav* because the number of formats might differ between versions
+
+#if FF_API_PIX_FMT
+#include "old_pix_fmts.h"
+#endif
+};
+
+#if AV_HAVE_BIGENDIAN
+# define AV_PIX_FMT_NE(be, le) AV_PIX_FMT_##be
+#else
+# define AV_PIX_FMT_NE(be, le) AV_PIX_FMT_##le
+#endif
+
+#define AV_PIX_FMT_RGB32 AV_PIX_FMT_NE(ARGB, BGRA)
+#define AV_PIX_FMT_RGB32_1 AV_PIX_FMT_NE(RGBA, ABGR)
+#define AV_PIX_FMT_BGR32 AV_PIX_FMT_NE(ABGR, RGBA)
+#define AV_PIX_FMT_BGR32_1 AV_PIX_FMT_NE(BGRA, ARGB)
+
+#define AV_PIX_FMT_GRAY16 AV_PIX_FMT_NE(GRAY16BE, GRAY16LE)
+#define AV_PIX_FMT_RGB48 AV_PIX_FMT_NE(RGB48BE, RGB48LE)
+#define AV_PIX_FMT_RGB565 AV_PIX_FMT_NE(RGB565BE, RGB565LE)
+#define AV_PIX_FMT_RGB555 AV_PIX_FMT_NE(RGB555BE, RGB555LE)
+#define AV_PIX_FMT_RGB444 AV_PIX_FMT_NE(RGB444BE, RGB444LE)
+#define AV_PIX_FMT_BGR48 AV_PIX_FMT_NE(BGR48BE, BGR48LE)
+#define AV_PIX_FMT_BGR565 AV_PIX_FMT_NE(BGR565BE, BGR565LE)
+#define AV_PIX_FMT_BGR555 AV_PIX_FMT_NE(BGR555BE, BGR555LE)
+#define AV_PIX_FMT_BGR444 AV_PIX_FMT_NE(BGR444BE, BGR444LE)
+
+#define AV_PIX_FMT_YUV420P9 AV_PIX_FMT_NE(YUV420P9BE , YUV420P9LE)
+#define AV_PIX_FMT_YUV422P9 AV_PIX_FMT_NE(YUV422P9BE , YUV422P9LE)
+#define AV_PIX_FMT_YUV444P9 AV_PIX_FMT_NE(YUV444P9BE , YUV444P9LE)
+#define AV_PIX_FMT_YUV420P10 AV_PIX_FMT_NE(YUV420P10BE, YUV420P10LE)
+#define AV_PIX_FMT_YUV422P10 AV_PIX_FMT_NE(YUV422P10BE, YUV422P10LE)
+#define AV_PIX_FMT_YUV444P10 AV_PIX_FMT_NE(YUV444P10BE, YUV444P10LE)
+#define AV_PIX_FMT_YUV420P16 AV_PIX_FMT_NE(YUV420P16BE, YUV420P16LE)
+#define AV_PIX_FMT_YUV422P16 AV_PIX_FMT_NE(YUV422P16BE, YUV422P16LE)
+#define AV_PIX_FMT_YUV444P16 AV_PIX_FMT_NE(YUV444P16BE, YUV444P16LE)
+
+#define AV_PIX_FMT_GBRP9 AV_PIX_FMT_NE(GBRP9BE , GBRP9LE)
+#define AV_PIX_FMT_GBRP10 AV_PIX_FMT_NE(GBRP10BE, GBRP10LE)
+#define AV_PIX_FMT_GBRP16 AV_PIX_FMT_NE(GBRP16BE, GBRP16LE)
+
+#define AV_PIX_FMT_YUVA420P9 AV_PIX_FMT_NE(YUVA420P9BE , YUVA420P9LE)
+#define AV_PIX_FMT_YUVA422P9 AV_PIX_FMT_NE(YUVA422P9BE , YUVA422P9LE)
+#define AV_PIX_FMT_YUVA444P9 AV_PIX_FMT_NE(YUVA444P9BE , YUVA444P9LE)
+#define AV_PIX_FMT_YUVA420P10 AV_PIX_FMT_NE(YUVA420P10BE, YUVA420P10LE)
+#define AV_PIX_FMT_YUVA422P10 AV_PIX_FMT_NE(YUVA422P10BE, YUVA422P10LE)
+#define AV_PIX_FMT_YUVA444P10 AV_PIX_FMT_NE(YUVA444P10BE, YUVA444P10LE)
+#define AV_PIX_FMT_YUVA420P16 AV_PIX_FMT_NE(YUVA420P16BE, YUVA420P16LE)
+#define AV_PIX_FMT_YUVA422P16 AV_PIX_FMT_NE(YUVA422P16BE, YUVA422P16LE)
+#define AV_PIX_FMT_YUVA444P16 AV_PIX_FMT_NE(YUVA444P16BE, YUVA444P16LE)
+
+#define AV_PIX_FMT_XYZ12 AV_PIX_FMT_NE(XYZ12BE, XYZ12LE)
+#define AV_PIX_FMT_NV20 AV_PIX_FMT_NE(NV20BE, NV20LE)
+
+#if FF_API_PIX_FMT
+#define PixelFormat AVPixelFormat
+
+#define PIX_FMT_NE(be, le) AV_PIX_FMT_NE(be, le)
+
+#define PIX_FMT_RGB32 AV_PIX_FMT_RGB32
+#define PIX_FMT_RGB32_1 AV_PIX_FMT_RGB32_1
+#define PIX_FMT_BGR32 AV_PIX_FMT_BGR32
+#define PIX_FMT_BGR32_1 AV_PIX_FMT_BGR32_1
+
+#define PIX_FMT_GRAY16 AV_PIX_FMT_GRAY16
+#define PIX_FMT_RGB48 AV_PIX_FMT_RGB48
+#define PIX_FMT_RGB565 AV_PIX_FMT_RGB565
+#define PIX_FMT_RGB555 AV_PIX_FMT_RGB555
+#define PIX_FMT_RGB444 AV_PIX_FMT_RGB444
+#define PIX_FMT_BGR48 AV_PIX_FMT_BGR48
+#define PIX_FMT_BGR565 AV_PIX_FMT_BGR565
+#define PIX_FMT_BGR555 AV_PIX_FMT_BGR555
+#define PIX_FMT_BGR444 AV_PIX_FMT_BGR444
+
+#define PIX_FMT_YUV420P9 AV_PIX_FMT_YUV420P9
+#define PIX_FMT_YUV422P9 AV_PIX_FMT_YUV422P9
+#define PIX_FMT_YUV444P9 AV_PIX_FMT_YUV444P9
+#define PIX_FMT_YUV420P10 AV_PIX_FMT_YUV420P10
+#define PIX_FMT_YUV422P10 AV_PIX_FMT_YUV422P10
+#define PIX_FMT_YUV444P10 AV_PIX_FMT_YUV444P10
+#define PIX_FMT_YUV420P16 AV_PIX_FMT_YUV420P16
+#define PIX_FMT_YUV422P16 AV_PIX_FMT_YUV422P16
+#define PIX_FMT_YUV444P16 AV_PIX_FMT_YUV444P16
+
+#define PIX_FMT_GBRP9 AV_PIX_FMT_GBRP9
+#define PIX_FMT_GBRP10 AV_PIX_FMT_GBRP10
+#define PIX_FMT_GBRP16 AV_PIX_FMT_GBRP16
+#endif
+
+#endif /* AVUTIL_PIXFMT_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/random_seed.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/random_seed.h
new file mode 100644
index 0000000000..b1fad13d07
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/random_seed.h
@@ -0,0 +1,44 @@
+/*
+ * Copyright (c) 2009 Baptiste Coudurier <baptiste.coudurier@gmail.com>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_RANDOM_SEED_H
+#define AVUTIL_RANDOM_SEED_H
+
+#include <stdint.h>
+/**
+ * @addtogroup lavu_crypto
+ * @{
+ */
+
+/**
+ * Get random data.
+ *
+ * This function can be called repeatedly to generate more random bits
+ * as needed. It is generally quite slow, and usually used to seed a
+ * PRNG. As it uses /dev/urandom and /dev/random, the quality of the
+ * returned random data depends on the platform.
+ */
+uint32_t av_get_random_seed(void);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_RANDOM_SEED_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/rational.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/rational.h
new file mode 100644
index 0000000000..5d7dab7fd0
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/rational.h
@@ -0,0 +1,155 @@
+/*
+ * rational numbers
+ * Copyright (c) 2003 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+/**
+ * @file
+ * rational numbers
+ * @author Michael Niedermayer <michaelni@gmx.at>
+ */
+
+#ifndef AVUTIL_RATIONAL_H
+#define AVUTIL_RATIONAL_H
+
+#include <stdint.h>
+#include <limits.h>
+#include "attributes.h"
+
+/**
+ * @addtogroup lavu_math
+ * @{
+ */
+
+/**
+ * rational number numerator/denominator
+ */
+typedef struct AVRational{
+ int num; ///< numerator
+ int den; ///< denominator
+} AVRational;
+
+/**
+ * Compare two rationals.
+ * @param a first rational
+ * @param b second rational
+ * @return 0 if a==b, 1 if a>b, -1 if a<b, and INT_MIN if one of the
+ * values is of the form 0/0
+ */
+static inline int av_cmp_q(AVRational a, AVRational b){
+ const int64_t tmp= a.num * (int64_t)b.den - b.num * (int64_t)a.den;
+
+ if(tmp) return ((tmp ^ a.den ^ b.den)>>63)|1;
+ else if(b.den && a.den) return 0;
+ else if(a.num && b.num) return (a.num>>31) - (b.num>>31);
+ else return INT_MIN;
+}
+
+/**
+ * Convert rational to double.
+ * @param a rational to convert
+ * @return (double) a
+ */
+static inline double av_q2d(AVRational a){
+ return a.num / (double) a.den;
+}
+
+/**
+ * Reduce a fraction.
+ * This is useful for framerate calculations.
+ * @param dst_num destination numerator
+ * @param dst_den destination denominator
+ * @param num source numerator
+ * @param den source denominator
+ * @param max the maximum allowed for dst_num & dst_den
+ * @return 1 if exact, 0 otherwise
+ */
+int av_reduce(int *dst_num, int *dst_den, int64_t num, int64_t den, int64_t max);
+
+/**
+ * Multiply two rationals.
+ * @param b first rational
+ * @param c second rational
+ * @return b*c
+ */
+AVRational av_mul_q(AVRational b, AVRational c) av_const;
+
+/**
+ * Divide one rational by another.
+ * @param b first rational
+ * @param c second rational
+ * @return b/c
+ */
+AVRational av_div_q(AVRational b, AVRational c) av_const;
+
+/**
+ * Add two rationals.
+ * @param b first rational
+ * @param c second rational
+ * @return b+c
+ */
+AVRational av_add_q(AVRational b, AVRational c) av_const;
+
+/**
+ * Subtract one rational from another.
+ * @param b first rational
+ * @param c second rational
+ * @return b-c
+ */
+AVRational av_sub_q(AVRational b, AVRational c) av_const;
+
+/**
+ * Invert a rational.
+ * @param q value
+ * @return 1 / q
+ */
+static av_always_inline AVRational av_inv_q(AVRational q)
+{
+ AVRational r = { q.den, q.num };
+ return r;
+}
+
+/**
+ * Convert a double precision floating point number to a rational.
+ * inf is expressed as {1,0} or {-1,0} depending on the sign.
+ *
+ * @param d double to convert
+ * @param max the maximum allowed numerator and denominator
+ * @return (AVRational) d
+ */
+AVRational av_d2q(double d, int max) av_const;
+
+/**
+ * @return 1 if q1 is nearer to q than q2, -1 if q2 is nearer
+ * than q1, 0 if they have the same distance.
+ */
+int av_nearer_q(AVRational q, AVRational q1, AVRational q2);
+
+/**
+ * Find the nearest value in q_list to q.
+ * @param q_list an array of rationals terminated by {0, 0}
+ * @return the index of the nearest value found in the array
+ */
+int av_find_nearest_q_idx(AVRational q, const AVRational* q_list);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_RATIONAL_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/samplefmt.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/samplefmt.h
new file mode 100644
index 0000000000..33cbdedf5f
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/samplefmt.h
@@ -0,0 +1,220 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_SAMPLEFMT_H
+#define AVUTIL_SAMPLEFMT_H
+
+#include <stdint.h>
+
+#include "avutil.h"
+#include "attributes.h"
+
+/**
+ * Audio Sample Formats
+ *
+ * @par
+ * The data described by the sample format is always in native-endian order.
+ * Sample values can be expressed by native C types, hence the lack of a signed
+ * 24-bit sample format even though it is a common raw audio data format.
+ *
+ * @par
+ * The floating-point formats are based on full volume being in the range
+ * [-1.0, 1.0]. Any values outside this range are beyond full volume level.
+ *
+ * @par
+ * The data layout as used in av_samples_fill_arrays() and elsewhere in Libav
+ * (such as AVFrame in libavcodec) is as follows:
+ *
+ * For planar sample formats, each audio channel is in a separate data plane,
+ * and linesize is the buffer size, in bytes, for a single plane. All data
+ * planes must be the same size. For packed sample formats, only the first data
+ * plane is used, and samples for each channel are interleaved. In this case,
+ * linesize is the buffer size, in bytes, for the 1 plane.
+ */
+enum AVSampleFormat {
+ AV_SAMPLE_FMT_NONE = -1,
+ AV_SAMPLE_FMT_U8, ///< unsigned 8 bits
+ AV_SAMPLE_FMT_S16, ///< signed 16 bits
+ AV_SAMPLE_FMT_S32, ///< signed 32 bits
+ AV_SAMPLE_FMT_FLT, ///< float
+ AV_SAMPLE_FMT_DBL, ///< double
+
+ AV_SAMPLE_FMT_U8P, ///< unsigned 8 bits, planar
+ AV_SAMPLE_FMT_S16P, ///< signed 16 bits, planar
+ AV_SAMPLE_FMT_S32P, ///< signed 32 bits, planar
+ AV_SAMPLE_FMT_FLTP, ///< float, planar
+ AV_SAMPLE_FMT_DBLP, ///< double, planar
+
+ AV_SAMPLE_FMT_NB ///< Number of sample formats. DO NOT USE if linking dynamically
+};
+
+/**
+ * Return the name of sample_fmt, or NULL if sample_fmt is not
+ * recognized.
+ */
+const char *av_get_sample_fmt_name(enum AVSampleFormat sample_fmt);
+
+/**
+ * Return a sample format corresponding to name, or AV_SAMPLE_FMT_NONE
+ * on error.
+ */
+enum AVSampleFormat av_get_sample_fmt(const char *name);
+
+/**
+ * Get the packed alternative form of the given sample format.
+ *
+ * If the passed sample_fmt is already in packed format, the format returned is
+ * the same as the input.
+ *
+ * @return the packed alternative form of the given sample format or
+ AV_SAMPLE_FMT_NONE on error.
+ */
+enum AVSampleFormat av_get_packed_sample_fmt(enum AVSampleFormat sample_fmt);
+
+/**
+ * Get the planar alternative form of the given sample format.
+ *
+ * If the passed sample_fmt is already in planar format, the format returned is
+ * the same as the input.
+ *
+ * @return the planar alternative form of the given sample format or
+ AV_SAMPLE_FMT_NONE on error.
+ */
+enum AVSampleFormat av_get_planar_sample_fmt(enum AVSampleFormat sample_fmt);
+
+/**
+ * Generate a string corresponding to the sample format with
+ * sample_fmt, or a header if sample_fmt is negative.
+ *
+ * @param buf the buffer where to write the string
+ * @param buf_size the size of buf
+ * @param sample_fmt the number of the sample format to print the
+ * corresponding info string, or a negative value to print the
+ * corresponding header.
+ * @return the pointer to the filled buffer or NULL if sample_fmt is
+ * unknown or in case of other errors
+ */
+char *av_get_sample_fmt_string(char *buf, int buf_size, enum AVSampleFormat sample_fmt);
+
+/**
+ * Return number of bytes per sample.
+ *
+ * @param sample_fmt the sample format
+ * @return number of bytes per sample or zero if unknown for the given
+ * sample format
+ */
+int av_get_bytes_per_sample(enum AVSampleFormat sample_fmt);
+
+/**
+ * Check if the sample format is planar.
+ *
+ * @param sample_fmt the sample format to inspect
+ * @return 1 if the sample format is planar, 0 if it is interleaved
+ */
+int av_sample_fmt_is_planar(enum AVSampleFormat sample_fmt);
+
+/**
+ * Get the required buffer size for the given audio parameters.
+ *
+ * @param[out] linesize calculated linesize, may be NULL
+ * @param nb_channels the number of channels
+ * @param nb_samples the number of samples in a single channel
+ * @param sample_fmt the sample format
+ * @param align buffer size alignment (0 = default, 1 = no alignment)
+ * @return required buffer size, or negative error code on failure
+ */
+int av_samples_get_buffer_size(int *linesize, int nb_channels, int nb_samples,
+ enum AVSampleFormat sample_fmt, int align);
+
+/**
+ * Fill channel data pointers and linesize for samples with sample
+ * format sample_fmt.
+ *
+ * The pointers array is filled with the pointers to the samples data:
+ * for planar, set the start point of each channel's data within the buffer,
+ * for packed, set the start point of the entire buffer only.
+ *
+ * The linesize array is filled with the aligned size of each channel's data
+ * buffer for planar layout, or the aligned size of the buffer for all channels
+ * for packed layout.
+ *
+ * @see enum AVSampleFormat
+ * The documentation for AVSampleFormat describes the data layout.
+ *
+ * @param[out] audio_data array to be filled with the pointer for each channel
+ * @param[out] linesize calculated linesize, may be NULL
+ * @param buf the pointer to a buffer containing the samples
+ * @param nb_channels the number of channels
+ * @param nb_samples the number of samples in a single channel
+ * @param sample_fmt the sample format
+ * @param align buffer size alignment (0 = default, 1 = no alignment)
+ * @return 0 on success or a negative error code on failure
+ */
+int av_samples_fill_arrays(uint8_t **audio_data, int *linesize,
+ const uint8_t *buf,
+ int nb_channels, int nb_samples,
+ enum AVSampleFormat sample_fmt, int align);
+
+/**
+ * Allocate a samples buffer for nb_samples samples, and fill data pointers and
+ * linesize accordingly.
+ * The allocated samples buffer can be freed by using av_freep(&audio_data[0])
+ * Allocated data will be initialized to silence.
+ *
+ * @see enum AVSampleFormat
+ * The documentation for AVSampleFormat describes the data layout.
+ *
+ * @param[out] audio_data array to be filled with the pointer for each channel
+ * @param[out] linesize aligned size for audio buffer(s), may be NULL
+ * @param nb_channels number of audio channels
+ * @param nb_samples number of samples per channel
+ * @param align buffer size alignment (0 = default, 1 = no alignment)
+ * @return 0 on success or a negative error code on failure
+ * @see av_samples_fill_arrays()
+ */
+int av_samples_alloc(uint8_t **audio_data, int *linesize, int nb_channels,
+ int nb_samples, enum AVSampleFormat sample_fmt, int align);
+
+/**
+ * Copy samples from src to dst.
+ *
+ * @param dst destination array of pointers to data planes
+ * @param src source array of pointers to data planes
+ * @param dst_offset offset in samples at which the data will be written to dst
+ * @param src_offset offset in samples at which the data will be read from src
+ * @param nb_samples number of samples to be copied
+ * @param nb_channels number of audio channels
+ * @param sample_fmt audio sample format
+ */
+int av_samples_copy(uint8_t **dst, uint8_t * const *src, int dst_offset,
+ int src_offset, int nb_samples, int nb_channels,
+ enum AVSampleFormat sample_fmt);
+
+/**
+ * Fill an audio buffer with silence.
+ *
+ * @param audio_data array of pointers to data planes
+ * @param offset offset in samples at which to start filling
+ * @param nb_samples number of samples to fill
+ * @param nb_channels number of audio channels
+ * @param sample_fmt audio sample format
+ */
+int av_samples_set_silence(uint8_t **audio_data, int offset, int nb_samples,
+ int nb_channels, enum AVSampleFormat sample_fmt);
+
+#endif /* AVUTIL_SAMPLEFMT_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/sha.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/sha.h
new file mode 100644
index 0000000000..4c9a0c9095
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/sha.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2007 Michael Niedermayer <michaelni@gmx.at>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_SHA_H
+#define AVUTIL_SHA_H
+
+#include <stdint.h>
+
+#include "attributes.h"
+#include "version.h"
+
+/**
+ * @defgroup lavu_sha SHA
+ * @ingroup lavu_crypto
+ * @{
+ */
+
+#if FF_API_CONTEXT_SIZE
+extern attribute_deprecated const int av_sha_size;
+#endif
+
+struct AVSHA;
+
+/**
+ * Allocate an AVSHA context.
+ */
+struct AVSHA *av_sha_alloc(void);
+
+/**
+ * Initialize SHA-1 or SHA-2 hashing.
+ *
+ * @param context pointer to the function context (of size av_sha_size)
+ * @param bits number of bits in digest (SHA-1 - 160 bits, SHA-2 224 or 256 bits)
+ * @return zero if initialization succeeded, -1 otherwise
+ */
+int av_sha_init(struct AVSHA* context, int bits);
+
+/**
+ * Update hash value.
+ *
+ * @param context hash function context
+ * @param data input data to update hash with
+ * @param len input data length
+ */
+void av_sha_update(struct AVSHA* context, const uint8_t* data, unsigned int len);
+
+/**
+ * Finish hashing and output digest value.
+ *
+ * @param context hash function context
+ * @param digest buffer where output digest value is stored
+ */
+void av_sha_final(struct AVSHA* context, uint8_t *digest);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_SHA_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/stereo3d.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/stereo3d.h
new file mode 100644
index 0000000000..695d6f1bae
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/stereo3d.h
@@ -0,0 +1,147 @@
+/*
+ * Copyright (c) 2013 Vittorio Giovara <vittorio.giovara@gmail.com>
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#include <stdint.h>
+
+#include "frame.h"
+
+/**
+ * List of possible 3D Types
+ */
+enum AVStereo3DType {
+ /**
+ * Video is not stereoscopic (and metadata has to be there).
+ */
+ AV_STEREO3D_2D,
+
+ /**
+ * Views are next to each other.
+ *
+ * LLLLRRRR
+ * LLLLRRRR
+ * LLLLRRRR
+ * ...
+ */
+ AV_STEREO3D_SIDEBYSIDE,
+
+ /**
+ * Views are on top of each other.
+ *
+ * LLLLLLLL
+ * LLLLLLLL
+ * RRRRRRRR
+ * RRRRRRRR
+ */
+ AV_STEREO3D_TOPBOTTOM,
+
+ /**
+ * Views are alternated temporally.
+ *
+ * frame0 frame1 frame2 ...
+ * LLLLLLLL RRRRRRRR LLLLLLLL
+ * LLLLLLLL RRRRRRRR LLLLLLLL
+ * LLLLLLLL RRRRRRRR LLLLLLLL
+ * ... ... ...
+ */
+ AV_STEREO3D_FRAMESEQUENCE,
+
+ /**
+ * Views are packed in a checkerboard-like structure per pixel.
+ *
+ * LRLRLRLR
+ * RLRLRLRL
+ * LRLRLRLR
+ * ...
+ */
+ AV_STEREO3D_CHECKERBOARD,
+
+ /**
+ * Views are next to each other, but when upscaling
+ * apply a checkerboard pattern.
+ *
+ * LLLLRRRR L L L L R R R R
+ * LLLLRRRR => L L L L R R R R
+ * LLLLRRRR L L L L R R R R
+ * LLLLRRRR L L L L R R R R
+ */
+ AV_STEREO3D_SIDEBYSIDE_QUINCUNX,
+
+ /**
+ * Views are packed per line, as if interlaced.
+ *
+ * LLLLLLLL
+ * RRRRRRRR
+ * LLLLLLLL
+ * ...
+ */
+ AV_STEREO3D_LINES,
+
+ /**
+ * Views are packed per column.
+ *
+ * LRLRLRLR
+ * LRLRLRLR
+ * LRLRLRLR
+ * ...
+ */
+ AV_STEREO3D_COLUMNS,
+};
+
+
+/**
+ * Inverted views, Right/Bottom represents the left view.
+ */
+#define AV_STEREO3D_FLAG_INVERT (1 << 0)
+
+/**
+ * Stereo 3D type: this structure describes how two videos are packed
+ * within a single video surface, with additional information as needed.
+ *
+ * @note The struct must be allocated with av_stereo3d_alloc() and
+ * its size is not a part of the public ABI.
+ */
+typedef struct AVStereo3D {
+ /**
+ * How views are packed within the video.
+ */
+ enum AVStereo3DType type;
+
+ /**
+ * Additional information about the frame packing.
+ */
+ int flags;
+} AVStereo3D;
+
+/**
+ * Allocate an AVStereo3D structure and set its fields to default values.
+ * The resulting struct can be freed using av_freep().
+ *
+ * @return An AVStereo3D filled with default values or NULL on failure.
+ */
+AVStereo3D *av_stereo3d_alloc(void);
+
+/**
+ * Allocate a complete AVFrameSideData and add it to the frame.
+ *
+ * @param frame The frame which side data is added to.
+ *
+ * @return The AVStereo3D structure to be filled by caller.
+ */
+AVStereo3D *av_stereo3d_create_side_data(AVFrame *frame);
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/time.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/time.h
new file mode 100644
index 0000000000..b01a97d770
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/time.h
@@ -0,0 +1,39 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_TIME_H
+#define AVUTIL_TIME_H
+
+#include <stdint.h>
+
+/**
+ * Get the current time in microseconds.
+ */
+int64_t av_gettime(void);
+
+/**
+ * Sleep for a period of time. Although the duration is expressed in
+ * microseconds, the actual delay may be rounded to the precision of the
+ * system timer.
+ *
+ * @param usec Number of microseconds to sleep.
+ * @return zero on success or (negative) error code.
+ */
+int av_usleep(unsigned usec);
+
+#endif /* AVUTIL_TIME_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/version.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/version.h
new file mode 100644
index 0000000000..5196a674de
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/version.h
@@ -0,0 +1,116 @@
+/*
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_VERSION_H
+#define AVUTIL_VERSION_H
+
+#include "macros.h"
+
+/**
+ * @defgroup version_utils Library Version Macros
+ *
+ * Useful to check and match library version in order to maintain
+ * backward compatibility.
+ *
+ * @{
+ */
+
+#define AV_VERSION_INT(a, b, c) (a<<16 | b<<8 | c)
+#define AV_VERSION_DOT(a, b, c) a ##.## b ##.## c
+#define AV_VERSION(a, b, c) AV_VERSION_DOT(a, b, c)
+
+/**
+ * @}
+ */
+
+/**
+ * @file
+ * @ingroup lavu
+ * Libavutil version macros
+ */
+
+/**
+ * @defgroup lavu_ver Version and Build diagnostics
+ *
+ * Macros and function useful to check at compiletime and at runtime
+ * which version of libavutil is in use.
+ *
+ * @{
+ */
+
+#define LIBAVUTIL_VERSION_MAJOR 53
+#define LIBAVUTIL_VERSION_MINOR 3
+#define LIBAVUTIL_VERSION_MICRO 0
+
+#define LIBAVUTIL_VERSION_INT AV_VERSION_INT(LIBAVUTIL_VERSION_MAJOR, \
+ LIBAVUTIL_VERSION_MINOR, \
+ LIBAVUTIL_VERSION_MICRO)
+#define LIBAVUTIL_VERSION AV_VERSION(LIBAVUTIL_VERSION_MAJOR, \
+ LIBAVUTIL_VERSION_MINOR, \
+ LIBAVUTIL_VERSION_MICRO)
+#define LIBAVUTIL_BUILD LIBAVUTIL_VERSION_INT
+
+#define LIBAVUTIL_IDENT "Lavu" AV_STRINGIFY(LIBAVUTIL_VERSION)
+
+/**
+ * @}
+ *
+ * @defgroup depr_guards Deprecation guards
+ * FF_API_* defines may be placed below to indicate public API that will be
+ * dropped at a future version bump. The defines themselves are not part of
+ * the public API and may change, break or disappear at any time.
+ *
+ * @{
+ */
+
+#ifndef FF_API_PIX_FMT
+#define FF_API_PIX_FMT (LIBAVUTIL_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_CONTEXT_SIZE
+#define FF_API_CONTEXT_SIZE (LIBAVUTIL_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_PIX_FMT_DESC
+#define FF_API_PIX_FMT_DESC (LIBAVUTIL_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_AV_REVERSE
+#define FF_API_AV_REVERSE (LIBAVUTIL_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_AUDIOCONVERT
+#define FF_API_AUDIOCONVERT (LIBAVUTIL_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_CPU_FLAG_MMX2
+#define FF_API_CPU_FLAG_MMX2 (LIBAVUTIL_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_LLS_PRIVATE
+#define FF_API_LLS_PRIVATE (LIBAVUTIL_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_AVFRAME_LAVC
+#define FF_API_AVFRAME_LAVC (LIBAVUTIL_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_VDPAU
+#define FF_API_VDPAU (LIBAVUTIL_VERSION_MAJOR < 54)
+#endif
+#ifndef FF_API_XVMC
+#define FF_API_XVMC (LIBAVUTIL_VERSION_MAJOR < 54)
+#endif
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_VERSION_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/include/libavutil/xtea.h b/dom/media/platforms/ffmpeg/libav55/include/libavutil/xtea.h
new file mode 100644
index 0000000000..7d2b07bbc7
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/include/libavutil/xtea.h
@@ -0,0 +1,61 @@
+/*
+ * A 32-bit implementation of the XTEA algorithm
+ *
+ * This file is part of Libav.
+ *
+ * Libav is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * Libav is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with Libav; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ */
+
+#ifndef AVUTIL_XTEA_H
+#define AVUTIL_XTEA_H
+
+#include <stdint.h>
+
+/**
+ * @defgroup lavu_xtea XTEA
+ * @ingroup lavu_crypto
+ * @{
+ */
+
+typedef struct AVXTEA {
+ uint32_t key[16];
+} AVXTEA;
+
+/**
+ * Initialize an AVXTEA context.
+ *
+ * @param ctx an AVXTEA context
+ * @param key a key of 16 bytes used for encryption/decryption
+ */
+void av_xtea_init(struct AVXTEA *ctx, const uint8_t key[16]);
+
+/**
+ * Encrypt or decrypt a buffer using a previously initialized context.
+ *
+ * @param ctx an AVXTEA context
+ * @param dst destination array, can be equal to src
+ * @param src source array, can be equal to dst
+ * @param count number of 8 byte blocks
+ * @param iv initialization vector for CBC mode, if NULL then ECB will be used
+ * @param decrypt 0 for encryption, 1 for decryption
+ */
+void av_xtea_crypt(struct AVXTEA *ctx, uint8_t *dst, const uint8_t *src,
+ int count, uint8_t *iv, int decrypt);
+
+/**
+ * @}
+ */
+
+#endif /* AVUTIL_XTEA_H */
diff --git a/dom/media/platforms/ffmpeg/libav55/moz.build b/dom/media/platforms/ffmpeg/libav55/moz.build
new file mode 100644
index 0000000000..d892f2a8a5
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/libav55/moz.build
@@ -0,0 +1,29 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES += [
+ '../FFmpegAudioDecoder.cpp',
+ '../FFmpegDataDecoder.cpp',
+ '../FFmpegDecoderModule.cpp',
+ '../FFmpegVideoDecoder.cpp',
+]
+LOCAL_INCLUDES += [
+ '..',
+ 'include',
+]
+
+if CONFIG['CC_TYPE'] in ('clang', 'gcc'):
+ CXXFLAGS += [ '-Wno-deprecated-declarations' ]
+if CONFIG['CC_TYPE'] == 'clang':
+ CXXFLAGS += [
+ '-Wno-unknown-attributes',
+ ]
+if CONFIG['CC_TYPE'] == 'gcc':
+ CXXFLAGS += [
+ '-Wno-attributes',
+ ]
+
+FINAL_LIBRARY = 'xul'
diff --git a/dom/media/platforms/ffmpeg/moz.build b/dom/media/platforms/ffmpeg/moz.build
new file mode 100644
index 0000000000..f87dd00623
--- /dev/null
+++ b/dom/media/platforms/ffmpeg/moz.build
@@ -0,0 +1,28 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS += [
+ "FFmpegRuntimeLinker.h",
+]
+
+DIRS += [
+ "libav53",
+ "libav54",
+ "libav55",
+ "ffmpeg57",
+ "ffmpeg58",
+ "ffmpeg59",
+ "ffmpeg60",
+]
+
+UNIFIED_SOURCES += [
+ "FFmpegRuntimeLinker.cpp",
+]
+
+if CONFIG["MOZ_WAYLAND"]:
+ include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/platforms/moz.build b/dom/media/platforms/moz.build
new file mode 100644
index 0000000000..c71f22a22e
--- /dev/null
+++ b/dom/media/platforms/moz.build
@@ -0,0 +1,141 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS += [
+ "agnostic/AgnosticDecoderModule.h",
+ "agnostic/BlankDecoderModule.h",
+ "agnostic/DummyMediaDataDecoder.h",
+ "agnostic/OpusDecoder.h",
+ "agnostic/TheoraDecoder.h",
+ "agnostic/VorbisDecoder.h",
+ "agnostic/VPXDecoder.h",
+ "agnostic/WAVDecoder.h",
+ "AllocationPolicy.h",
+ "MediaCodecsSupport.h",
+ "MediaTelemetryConstants.h",
+ "PDMFactory.h",
+ "PEMFactory.h",
+ "PlatformDecoderModule.h",
+ "PlatformEncoderModule.h",
+ "ReorderQueue.h",
+ "SimpleMap.h",
+ "wrappers/AudioTrimmer.h",
+ "wrappers/MediaChangeMonitor.h",
+ "wrappers/MediaDataDecoderProxy.h",
+]
+
+UNIFIED_SOURCES += [
+ "agnostic/AgnosticDecoderModule.cpp",
+ "agnostic/BlankDecoderModule.cpp",
+ "agnostic/DummyMediaDataDecoder.cpp",
+ "agnostic/NullDecoderModule.cpp",
+ "agnostic/OpusDecoder.cpp",
+ "agnostic/TheoraDecoder.cpp",
+ "agnostic/VorbisDecoder.cpp",
+ "agnostic/VPXDecoder.cpp",
+ "agnostic/WAVDecoder.cpp",
+ "AllocationPolicy.cpp",
+ "MediaCodecsSupport.cpp",
+ "PDMFactory.cpp",
+ "PEMFactory.cpp",
+ "PlatformDecoderModule.cpp",
+ "wrappers/AudioTrimmer.cpp",
+ "wrappers/MediaChangeMonitor.cpp",
+ "wrappers/MediaDataDecoderProxy.cpp",
+]
+
+DIRS += ["agnostic/bytestreams", "agnostic/eme", "agnostic/gmp", "omx"]
+
+if CONFIG["MOZ_WMF"]:
+ DIRS += ["wmf"]
+
+if CONFIG["MOZ_FFVPX"] or CONFIG["MOZ_FFMPEG"]:
+ # common code to either FFmpeg or FFVPX
+ EXPORTS += [
+ "ffmpeg/FFmpegRDFTTypes.h",
+ ]
+ UNIFIED_SOURCES += [
+ "ffmpeg/FFmpegLibWrapper.cpp",
+ ]
+
+if CONFIG["MOZ_FFVPX"]:
+ DIRS += [
+ "ffmpeg/ffvpx",
+ ]
+
+if CONFIG["MOZ_FFMPEG"]:
+ DIRS += [
+ "ffmpeg",
+ ]
+
+if CONFIG["MOZ_AV1"]:
+ EXPORTS += [
+ "agnostic/AOMDecoder.h",
+ "agnostic/DAV1DDecoder.h",
+ ]
+ UNIFIED_SOURCES += [
+ "agnostic/AOMDecoder.cpp",
+ "agnostic/DAV1DDecoder.cpp",
+ ]
+
+if CONFIG["MOZ_OMX"]:
+ EXPORTS += [
+ "omx/OmxCoreLibLinker.h",
+ ]
+ UNIFIED_SOURCES += [
+ "omx/OmxCoreLibLinker.cpp",
+ ]
+
+if CONFIG["MOZ_APPLEMEDIA"]:
+ EXPORTS += [
+ "apple/AppleDecoderModule.h",
+ "apple/AppleEncoderModule.h",
+ ]
+ UNIFIED_SOURCES += [
+ "apple/AppleATDecoder.cpp",
+ "apple/AppleDecoderModule.cpp",
+ "apple/AppleEncoderModule.cpp",
+ "apple/AppleVTDecoder.cpp",
+ "apple/AppleVTEncoder.cpp",
+ ]
+ LOCAL_INCLUDES += [
+ "/media/libyuv/libyuv/include",
+ ]
+ OS_LIBS += [
+ "-framework AudioToolbox",
+ "-framework CoreMedia",
+ "-framework VideoToolbox",
+ # For some unknown reason, the documented method of using weak_import
+ # attribute doesn't work with VideoToolbox's functions.
+ # We want to lazily load _VTRegisterSupplementalVideoDecoderIfAvailable
+ # symbol as it's only available in macOS 11 and later.
+ "-Wl,-U,_VTRegisterSupplementalVideoDecoderIfAvailable",
+ # Same for VTIsHardwareDecodeSupported available from macOS 10.13.
+ "-Wl,-U,_VTIsHardwareDecodeSupported",
+ ]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "android":
+ EXPORTS += [
+ "android/AndroidDecoderModule.h",
+ "android/AndroidEncoderModule.h",
+ "android/JavaCallbacksSupport.h",
+ ]
+ UNIFIED_SOURCES += [
+ "android/AndroidDataEncoder.cpp",
+ "android/AndroidDecoderModule.cpp",
+ "android/AndroidEncoderModule.cpp",
+ "android/RemoteDataDecoder.cpp",
+ ]
+ LOCAL_INCLUDES += [
+ "/media/libyuv/libyuv/include",
+ ]
+
+FINAL_LIBRARY = "xul"
+
+# Add libFuzzer configuration directives
+include("/tools/fuzzing/libfuzzer-config.mozbuild")
diff --git a/dom/media/platforms/omx/OmxCoreLibLinker.cpp b/dom/media/platforms/omx/OmxCoreLibLinker.cpp
new file mode 100644
index 0000000000..a0ee61ec42
--- /dev/null
+++ b/dom/media/platforms/omx/OmxCoreLibLinker.cpp
@@ -0,0 +1,113 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "OmxCoreLibLinker.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/Preferences.h"
+#include "MainThreadUtils.h"
+#include "prlink.h"
+#include "PlatformDecoderModule.h"
+
+#ifdef LOG
+# undef LOG
+#endif
+
+#define LOG(arg, ...) \
+ MOZ_LOG(sPDMLog, mozilla::LogLevel::Debug, \
+ ("OmxCoreLibLinker::%s: " arg, __func__, ##__VA_ARGS__))
+
+namespace mozilla {
+
+OmxCoreLibLinker::LinkStatus OmxCoreLibLinker::sLinkStatus = LinkStatus_INIT;
+
+const char* OmxCoreLibLinker::sLibNames[] = {
+ "libopenmaxil.so", // Raspberry Pi
+ "libomxr_core.so", // Renesas R-Car, RZ/G
+ "libomxil-bellagio.so.0", // Bellagio: An OSS implementation of OpenMAX IL
+};
+
+PRLibrary* OmxCoreLibLinker::sLinkedLib = nullptr;
+const char* OmxCoreLibLinker::sLibName = nullptr;
+
+#define OMX_FUNC(func) void (*func)();
+#include "OmxFunctionList.h"
+#undef OMX_FUNC
+
+bool OmxCoreLibLinker::TryLinkingLibrary(const char* libName) {
+ PRLibSpec lspec;
+ lspec.type = PR_LibSpec_Pathname;
+ lspec.value.pathname = libName;
+ sLinkedLib = PR_LoadLibraryWithFlags(lspec, PR_LD_NOW | PR_LD_LOCAL);
+ if (sLinkedLib) {
+ if (Bind(libName)) {
+ sLibName = libName;
+ LOG("Succeeded to load %s", libName);
+ return true;
+ } else {
+ LOG("Failed to link %s", libName);
+ }
+ Unlink();
+ }
+ return false;
+}
+
+/* static */
+bool OmxCoreLibLinker::Link() {
+ LOG("");
+
+ if (sLinkStatus) {
+ return sLinkStatus == LinkStatus_SUCCEEDED;
+ }
+
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsAutoCString libPath;
+ nsresult rv = Preferences::GetCString("media.omx.core-lib-path", libPath);
+ if (NS_SUCCEEDED(rv) && !libPath.IsEmpty()) {
+ if (TryLinkingLibrary(libPath.Data())) {
+ sLinkStatus = LinkStatus_SUCCEEDED;
+ return true;
+ }
+ }
+
+ // try known paths
+ for (size_t i = 0; i < ArrayLength(sLibNames); i++) {
+ if (TryLinkingLibrary(sLibNames[i])) {
+ sLinkStatus = LinkStatus_SUCCEEDED;
+ return true;
+ }
+ }
+ sLinkStatus = LinkStatus_FAILED;
+ return false;
+}
+
+/* static */
+bool OmxCoreLibLinker::Bind(const char* aLibName) {
+#define OMX_FUNC(func) \
+ { \
+ if (!(func = (typeof(func))PR_FindSymbol(sLinkedLib, #func))) { \
+ LOG("Couldn't load function " #func " from %s.", aLibName); \
+ return false; \
+ } \
+ }
+#include "OmxFunctionList.h"
+#undef OMX_FUNC
+ return true;
+}
+
+/* static */
+void OmxCoreLibLinker::Unlink() {
+ LOG("");
+
+ if (sLinkedLib) {
+ PR_UnloadLibrary(sLinkedLib);
+ sLinkedLib = nullptr;
+ sLibName = nullptr;
+ sLinkStatus = LinkStatus_INIT;
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/omx/OmxCoreLibLinker.h b/dom/media/platforms/omx/OmxCoreLibLinker.h
new file mode 100644
index 0000000000..aaf1bf92de
--- /dev/null
+++ b/dom/media/platforms/omx/OmxCoreLibLinker.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef __OmxCoreLibLinker_h__
+#define __OmxCoreLibLinker_h__
+
+struct PRLibrary;
+
+namespace mozilla {
+
+class OmxCoreLibLinker {
+ public:
+ static bool Link();
+ static void Unlink();
+
+ private:
+ static PRLibrary* sLinkedLib;
+ static const char* sLibName;
+ static const char* sLibNames[];
+
+ static bool TryLinkingLibrary(const char* libName);
+ static bool Bind(const char* aLibName);
+
+ static enum LinkStatus {
+ LinkStatus_INIT = 0,
+ LinkStatus_FAILED,
+ LinkStatus_SUCCEEDED
+ } sLinkStatus;
+};
+
+} // namespace mozilla
+
+#endif // __OmxCoreLibLinker_h__
diff --git a/dom/media/platforms/omx/OmxDataDecoder.cpp b/dom/media/platforms/omx/OmxDataDecoder.cpp
new file mode 100644
index 0000000000..3224721878
--- /dev/null
+++ b/dom/media/platforms/omx/OmxDataDecoder.cpp
@@ -0,0 +1,1001 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "OmxDataDecoder.h"
+
+#include "OMX_Audio.h"
+#include "OMX_Component.h"
+#include "OMX_Types.h"
+#include "OmxPlatformLayer.h"
+#include "mozilla/IntegerPrintfMacros.h"
+
+#ifdef LOG
+# undef LOG
+# undef LOGL
+#endif
+
+#define LOG(arg, ...) \
+ DDMOZ_LOG(sPDMLog, mozilla::LogLevel::Debug, "::%s: " arg, __func__, \
+ ##__VA_ARGS__)
+
+#define LOGL(arg, ...) \
+ DDMOZ_LOGEX(self.get(), sPDMLog, mozilla::LogLevel::Debug, "::%s: " arg, \
+ __func__, ##__VA_ARGS__)
+
+#define CHECK_OMX_ERR(err) \
+ if (err != OMX_ErrorNone) { \
+ NotifyError(err, __func__); \
+ return; \
+ }
+
+namespace mozilla {
+
+using namespace gfx;
+
+static const char* StateTypeToStr(OMX_STATETYPE aType) {
+ MOZ_ASSERT(aType == OMX_StateLoaded || aType == OMX_StateIdle ||
+ aType == OMX_StateExecuting || aType == OMX_StatePause ||
+ aType == OMX_StateWaitForResources || aType == OMX_StateInvalid);
+
+ switch (aType) {
+ case OMX_StateLoaded:
+ return "OMX_StateLoaded";
+ case OMX_StateIdle:
+ return "OMX_StateIdle";
+ case OMX_StateExecuting:
+ return "OMX_StateExecuting";
+ case OMX_StatePause:
+ return "OMX_StatePause";
+ case OMX_StateWaitForResources:
+ return "OMX_StateWaitForResources";
+ case OMX_StateInvalid:
+ return "OMX_StateInvalid";
+ default:
+ return "Unknown";
+ }
+}
+
+// A helper class to retrieve AudioData or VideoData.
+class MediaDataHelper {
+ protected:
+ virtual ~MediaDataHelper() = default;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaDataHelper)
+
+ MediaDataHelper(const TrackInfo* aTrackInfo,
+ layers::ImageContainer* aImageContainer,
+ OmxPromiseLayer* aOmxLayer);
+
+ already_AddRefed<MediaData> GetMediaData(BufferData* aBufferData,
+ bool& aPlatformDepenentData);
+
+ protected:
+ already_AddRefed<AudioData> CreateAudioData(BufferData* aBufferData);
+
+ already_AddRefed<VideoData> CreateYUV420VideoData(BufferData* aBufferData);
+
+ const TrackInfo* mTrackInfo;
+
+ OMX_PARAM_PORTDEFINITIONTYPE mOutputPortDef;
+
+ // audio output
+ MediaQueue<AudioData> mAudioQueue;
+
+ AudioCompactor mAudioCompactor;
+
+ // video output
+ RefPtr<layers::ImageContainer> mImageContainer;
+};
+
+OmxDataDecoder::OmxDataDecoder(const TrackInfo& aTrackInfo,
+ layers::ImageContainer* aImageContainer,
+ Maybe<TrackingId> aTrackingId)
+ : mOmxTaskQueue(
+ CreateMediaDecodeTaskQueue("OmxDataDecoder::mOmxTaskQueue")),
+ mImageContainer(aImageContainer),
+ mWatchManager(this, mOmxTaskQueue),
+ mOmxState(OMX_STATETYPE::OMX_StateInvalid, "OmxDataDecoder::mOmxState"),
+ mTrackInfo(aTrackInfo.Clone()),
+ mFlushing(false),
+ mShuttingDown(false),
+ mCheckingInputExhausted(false),
+ mPortSettingsChanged(-1, "OmxDataDecoder::mPortSettingsChanged"),
+ mTrackingId(std::move(aTrackingId)) {
+ LOG("");
+ mOmxLayer = new OmxPromiseLayer(mOmxTaskQueue, this, aImageContainer);
+}
+
+OmxDataDecoder::~OmxDataDecoder() { LOG(""); }
+
+void OmxDataDecoder::InitializationTask() {
+ mWatchManager.Watch(mOmxState, &OmxDataDecoder::OmxStateRunner);
+ mWatchManager.Watch(mPortSettingsChanged,
+ &OmxDataDecoder::PortSettingsChanged);
+}
+
+void OmxDataDecoder::EndOfStream() {
+ LOG("");
+ MOZ_ASSERT(mOmxTaskQueue->IsCurrentThreadIn());
+
+ RefPtr<OmxDataDecoder> self = this;
+ mOmxLayer->SendCommand(OMX_CommandFlush, OMX_ALL, nullptr)
+ ->Then(mOmxTaskQueue, __func__,
+ [self, this](OmxCommandPromise::ResolveOrRejectValue&& aValue) {
+ mDrainPromise.ResolveIfExists(std::move(mDecodedData), __func__);
+ mDecodedData = DecodedData();
+ });
+}
+
+RefPtr<MediaDataDecoder::InitPromise> OmxDataDecoder::Init() {
+ LOG("");
+
+ mThread = GetCurrentSerialEventTarget();
+ RefPtr<OmxDataDecoder> self = this;
+ return InvokeAsync(mOmxTaskQueue, __func__, [self, this]() {
+ InitializationTask();
+
+ RefPtr<InitPromise> p = mInitPromise.Ensure(__func__);
+ mOmxLayer->Init(mTrackInfo.get())
+ ->Then(
+ mOmxTaskQueue, __func__,
+ [self, this]() {
+ // Omx state should be OMX_StateIdle.
+ mOmxState = mOmxLayer->GetState();
+ MOZ_ASSERT(mOmxState != OMX_StateIdle);
+ },
+ [self, this]() {
+ RejectInitPromise(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
+ });
+ return p;
+ });
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> OmxDataDecoder::Decode(
+ MediaRawData* aSample) {
+ LOG("sample %p", aSample);
+ MOZ_ASSERT(mThread->IsOnCurrentThread());
+ MOZ_ASSERT(mInitPromise.IsEmpty());
+
+ RefPtr<OmxDataDecoder> self = this;
+ RefPtr<MediaRawData> sample = aSample;
+ return InvokeAsync(mOmxTaskQueue, __func__, [self, this, sample]() {
+ RefPtr<DecodePromise> p = mDecodePromise.Ensure(__func__);
+
+ mTrackingId.apply([&](const auto& aId) {
+ MediaInfoFlag flag = MediaInfoFlag::None;
+ flag |= (sample->mKeyframe ? MediaInfoFlag::KeyFrame
+ : MediaInfoFlag::NonKeyFrame);
+
+ mPerformanceRecorder.Start(sample->mTimecode.ToMicroseconds(),
+ "OmxDataDecoder"_ns, aId, flag);
+ });
+ mMediaRawDatas.AppendElement(std::move(sample));
+
+ // Start to fill/empty buffers.
+ if (mOmxState == OMX_StateIdle || mOmxState == OMX_StateExecuting) {
+ FillAndEmptyBuffers();
+ }
+ return p;
+ });
+}
+
+RefPtr<MediaDataDecoder::FlushPromise> OmxDataDecoder::Flush() {
+ LOG("");
+ MOZ_ASSERT(mThread->IsOnCurrentThread());
+
+ mFlushing = true;
+
+ return InvokeAsync(mOmxTaskQueue, this, __func__, &OmxDataDecoder::DoFlush);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> OmxDataDecoder::Drain() {
+ LOG("");
+ MOZ_ASSERT(mThread->IsOnCurrentThread());
+
+ RefPtr<OmxDataDecoder> self = this;
+ return InvokeAsync(mOmxTaskQueue, __func__, [self]() {
+ RefPtr<DecodePromise> p = self->mDrainPromise.Ensure(__func__);
+ self->SendEosBuffer();
+ return p;
+ });
+}
+
+RefPtr<ShutdownPromise> OmxDataDecoder::Shutdown() {
+ LOG("");
+ // mThread may not be set if Init hasn't been called first.
+ MOZ_ASSERT(!mThread || mThread->IsOnCurrentThread());
+
+ mShuttingDown = true;
+
+ return InvokeAsync(mOmxTaskQueue, this, __func__,
+ &OmxDataDecoder::DoAsyncShutdown);
+}
+
+RefPtr<ShutdownPromise> OmxDataDecoder::DoAsyncShutdown() {
+ LOG("");
+ MOZ_ASSERT(mOmxTaskQueue->IsCurrentThreadIn());
+ MOZ_ASSERT(!mFlushing);
+
+ mWatchManager.Unwatch(mOmxState, &OmxDataDecoder::OmxStateRunner);
+ mWatchManager.Unwatch(mPortSettingsChanged,
+ &OmxDataDecoder::PortSettingsChanged);
+
+ // Flush to all ports, so all buffers can be returned from component.
+ RefPtr<OmxDataDecoder> self = this;
+ mOmxLayer->SendCommand(OMX_CommandFlush, OMX_ALL, nullptr)
+ ->Then(
+ mOmxTaskQueue, __func__,
+ [self]() -> RefPtr<OmxCommandPromise> {
+ LOGL("DoAsyncShutdown: flush complete");
+ return self->mOmxLayer->SendCommand(OMX_CommandStateSet,
+ OMX_StateIdle, nullptr);
+ },
+ [self](const OmxCommandFailureHolder& aError) {
+ self->mOmxLayer->Shutdown();
+ return OmxCommandPromise::CreateAndReject(aError, __func__);
+ })
+ ->Then(
+ mOmxTaskQueue, __func__,
+ [self]() -> RefPtr<OmxCommandPromise> {
+ RefPtr<OmxCommandPromise> p = self->mOmxLayer->SendCommand(
+ OMX_CommandStateSet, OMX_StateLoaded, nullptr);
+
+ // According to spec 3.1.1.2.2.1:
+ // OMX_StateLoaded needs to be sent before releasing buffers.
+ // And state transition from OMX_StateIdle to OMX_StateLoaded
+ // is completed when all of the buffers have been removed
+ // from the component.
+ // Here the buffer promises are not resolved due to displaying
+ // in layer, it needs to wait before the layer returns the
+ // buffers.
+ LOGL("DoAsyncShutdown: releasing buffers...");
+ self->ReleaseBuffers(OMX_DirInput);
+ self->ReleaseBuffers(OMX_DirOutput);
+
+ return p;
+ },
+ [self](const OmxCommandFailureHolder& aError) {
+ self->mOmxLayer->Shutdown();
+ return OmxCommandPromise::CreateAndReject(aError, __func__);
+ })
+ ->Then(
+ mOmxTaskQueue, __func__,
+ [self]() -> RefPtr<ShutdownPromise> {
+ LOGL(
+ "DoAsyncShutdown: OMX_StateLoaded, it is safe to shutdown omx");
+ self->mOmxLayer->Shutdown();
+ self->mWatchManager.Shutdown();
+ self->mOmxLayer = nullptr;
+ self->mMediaDataHelper = nullptr;
+ self->mShuttingDown = false;
+ return ShutdownPromise::CreateAndResolve(true, __func__);
+ },
+ [self]() -> RefPtr<ShutdownPromise> {
+ self->mOmxLayer->Shutdown();
+ self->mWatchManager.Shutdown();
+ self->mOmxLayer = nullptr;
+ self->mMediaDataHelper = nullptr;
+ return ShutdownPromise::CreateAndReject(false, __func__);
+ })
+ ->Then(
+ mThread, __func__,
+ [self]() {
+ self->mOmxTaskQueue->BeginShutdown();
+ self->mOmxTaskQueue->AwaitShutdownAndIdle();
+ self->mShutdownPromise.Resolve(true, __func__);
+ },
+ [self]() {
+ self->mOmxTaskQueue->BeginShutdown();
+ self->mOmxTaskQueue->AwaitShutdownAndIdle();
+ self->mShutdownPromise.Resolve(true, __func__);
+ });
+ return mShutdownPromise.Ensure(__func__);
+}
+
+void OmxDataDecoder::FillBufferDone(BufferData* aData) {
+ MOZ_ASSERT(!aData || aData->mStatus == BufferData::BufferStatus::OMX_CLIENT);
+
+ // Don't output sample when flush or shutting down, especially for video
+ // decoded frame. Because video decoded frame can have a promise in
+ // BufferData waiting for layer to resolve it via recycle callback, if other
+ // module doesn't send it to layer, it will cause a unresolved promise and
+ // waiting for resolve infinitely.
+ if (mFlushing || mShuttingDown) {
+ LOG("mFlush or mShuttingDown, drop data");
+ aData->mStatus = BufferData::BufferStatus::FREE;
+ return;
+ }
+
+ if (aData->mBuffer->nFlags & OMX_BUFFERFLAG_EOS) {
+ // Reach eos, it's an empty data so it doesn't need to output.
+ EndOfStream();
+ aData->mStatus = BufferData::BufferStatus::FREE;
+ } else {
+ Output(aData);
+ FillAndEmptyBuffers();
+ }
+}
+
+void OmxDataDecoder::Output(BufferData* aData) {
+ if (!mMediaDataHelper) {
+ mMediaDataHelper =
+ new MediaDataHelper(mTrackInfo.get(), mImageContainer, mOmxLayer);
+ }
+
+ bool isPlatformData = false;
+ RefPtr<MediaData> data =
+ mMediaDataHelper->GetMediaData(aData, isPlatformData);
+ if (!data) {
+ aData->mStatus = BufferData::BufferStatus::FREE;
+ return;
+ }
+
+ if (isPlatformData) {
+ // If the MediaData is platform dependnet data, it's mostly a kind of
+ // limited resource, so we use promise to notify when the resource is free.
+ aData->mStatus = BufferData::BufferStatus::OMX_CLIENT_OUTPUT;
+
+ MOZ_RELEASE_ASSERT(aData->mPromise.IsEmpty());
+ RefPtr<OmxBufferPromise> p = aData->mPromise.Ensure(__func__);
+
+ RefPtr<OmxDataDecoder> self = this;
+ RefPtr<BufferData> buffer = aData;
+ p->Then(
+ mOmxTaskQueue, __func__,
+ [self, buffer]() {
+ MOZ_RELEASE_ASSERT(buffer->mStatus ==
+ BufferData::BufferStatus::OMX_CLIENT_OUTPUT);
+ buffer->mStatus = BufferData::BufferStatus::FREE;
+ self->FillAndEmptyBuffers();
+ },
+ [buffer]() {
+ MOZ_RELEASE_ASSERT(buffer->mStatus ==
+ BufferData::BufferStatus::OMX_CLIENT_OUTPUT);
+ buffer->mStatus = BufferData::BufferStatus::FREE;
+ });
+ } else {
+ aData->mStatus = BufferData::BufferStatus::FREE;
+ }
+
+ if (mTrackInfo->IsVideo()) {
+ mPerformanceRecorder.Record(
+ aData->mRawData->mTimecode.ToMicroseconds(), [&](DecodeStage& aStage) {
+ const auto& image = data->As<VideoData>()->mImage;
+ aStage.SetResolution(image->GetSize().Width(),
+ image->GetSize().Height());
+ aStage.SetImageFormat(DecodeStage::YUV420P);
+ aStage.SetColorDepth(image->GetColorDepth());
+ });
+ }
+
+ mDecodedData.AppendElement(std::move(data));
+}
+
+void OmxDataDecoder::FillBufferFailure(OmxBufferFailureHolder aFailureHolder) {
+ NotifyError(aFailureHolder.mError, __func__);
+}
+
+void OmxDataDecoder::EmptyBufferDone(BufferData* aData) {
+ MOZ_ASSERT(mOmxTaskQueue->IsCurrentThreadIn());
+ MOZ_ASSERT(!aData || aData->mStatus == BufferData::BufferStatus::OMX_CLIENT);
+
+ // Nothing to do when status of input buffer is OMX_CLIENT.
+ aData->mStatus = BufferData::BufferStatus::FREE;
+ FillAndEmptyBuffers();
+
+ // There is no way to know if component gets enough raw samples to generate
+ // output, especially for video decoding. So here it needs to request raw
+ // samples aggressively.
+ if (!mCheckingInputExhausted && !mMediaRawDatas.Length()) {
+ mCheckingInputExhausted = true;
+
+ RefPtr<OmxDataDecoder> self = this;
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ "OmxDataDecoder::EmptyBufferDone", [self, this]() {
+ mCheckingInputExhausted = false;
+
+ if (mMediaRawDatas.Length()) {
+ return;
+ }
+
+ mDecodePromise.ResolveIfExists(std::move(mDecodedData), __func__);
+ mDecodedData = DecodedData();
+ });
+
+ nsresult rv = mOmxTaskQueue->Dispatch(r.forget());
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ }
+}
+
+void OmxDataDecoder::EmptyBufferFailure(OmxBufferFailureHolder aFailureHolder) {
+ NotifyError(aFailureHolder.mError, __func__);
+}
+
+void OmxDataDecoder::NotifyError(OMX_ERRORTYPE aOmxError, const char* aLine,
+ const MediaResult& aError) {
+ LOG("NotifyError %d (%s) at %s", static_cast<int>(aOmxError),
+ aError.ErrorName().get(), aLine);
+ mDecodedData = DecodedData();
+ mDecodePromise.RejectIfExists(aError, __func__);
+ mDrainPromise.RejectIfExists(aError, __func__);
+ mFlushPromise.RejectIfExists(aError, __func__);
+}
+
+void OmxDataDecoder::FillAndEmptyBuffers() {
+ MOZ_ASSERT(mOmxTaskQueue->IsCurrentThreadIn());
+ MOZ_ASSERT(mOmxState == OMX_StateExecuting);
+
+ // During the port setting changed, it is forbidden to do any buffer
+ // operation.
+ if (mPortSettingsChanged != -1 || mShuttingDown || mFlushing) {
+ return;
+ }
+
+ // Trigger input port.
+ while (!!mMediaRawDatas.Length()) {
+ // input buffer must be used by component if there is data available.
+ RefPtr<BufferData> inbuf = FindAvailableBuffer(OMX_DirInput);
+ if (!inbuf) {
+ LOG("no input buffer!");
+ break;
+ }
+
+ RefPtr<MediaRawData> data = mMediaRawDatas[0];
+ // Buffer size should large enough for raw data.
+ MOZ_RELEASE_ASSERT(inbuf->mBuffer->nAllocLen >= data->Size());
+
+ memcpy(inbuf->mBuffer->pBuffer, data->Data(), data->Size());
+ inbuf->mBuffer->nFilledLen = data->Size();
+ inbuf->mBuffer->nOffset = 0;
+ inbuf->mBuffer->nFlags = inbuf->mBuffer->nAllocLen > data->Size()
+ ? OMX_BUFFERFLAG_ENDOFFRAME
+ : 0;
+ inbuf->mBuffer->nTimeStamp = data->mTime.ToMicroseconds();
+ if (data->Size()) {
+ inbuf->mRawData = mMediaRawDatas[0];
+ } else {
+ LOG("send EOS buffer");
+ inbuf->mBuffer->nFlags |= OMX_BUFFERFLAG_EOS;
+ }
+
+ LOG("feed sample %p to omx component, len %ld, flag %lX", data.get(),
+ inbuf->mBuffer->nFilledLen, inbuf->mBuffer->nFlags);
+ mOmxLayer->EmptyBuffer(inbuf)->Then(mOmxTaskQueue, __func__, this,
+ &OmxDataDecoder::EmptyBufferDone,
+ &OmxDataDecoder::EmptyBufferFailure);
+ mMediaRawDatas.RemoveElementAt(0);
+ }
+
+ // Trigger output port.
+ while (true) {
+ RefPtr<BufferData> outbuf = FindAvailableBuffer(OMX_DirOutput);
+ if (!outbuf) {
+ break;
+ }
+
+ mOmxLayer->FillBuffer(outbuf)->Then(mOmxTaskQueue, __func__, this,
+ &OmxDataDecoder::FillBufferDone,
+ &OmxDataDecoder::FillBufferFailure);
+ }
+}
+
+OmxPromiseLayer::BufferData* OmxDataDecoder::FindAvailableBuffer(
+ OMX_DIRTYPE aType) {
+ BUFFERLIST* buffers = GetBuffers(aType);
+
+ for (uint32_t i = 0; i < buffers->Length(); i++) {
+ BufferData* buf = buffers->ElementAt(i);
+ if (buf->mStatus == BufferData::BufferStatus::FREE) {
+ return buf;
+ }
+ }
+
+ return nullptr;
+}
+
+nsresult OmxDataDecoder::AllocateBuffers(OMX_DIRTYPE aType) {
+ MOZ_ASSERT(mOmxTaskQueue->IsCurrentThreadIn());
+
+ return mOmxLayer->AllocateOmxBuffer(aType, GetBuffers(aType));
+}
+
+nsresult OmxDataDecoder::ReleaseBuffers(OMX_DIRTYPE aType) {
+ MOZ_ASSERT(mOmxTaskQueue->IsCurrentThreadIn());
+
+ return mOmxLayer->ReleaseOmxBuffer(aType, GetBuffers(aType));
+}
+
+nsTArray<RefPtr<OmxPromiseLayer::BufferData>>* OmxDataDecoder::GetBuffers(
+ OMX_DIRTYPE aType) {
+ MOZ_ASSERT(aType == OMX_DIRTYPE::OMX_DirInput ||
+ aType == OMX_DIRTYPE::OMX_DirOutput);
+
+ if (aType == OMX_DIRTYPE::OMX_DirInput) {
+ return &mInPortBuffers;
+ }
+ return &mOutPortBuffers;
+}
+
+void OmxDataDecoder::ResolveInitPromise(const char* aMethodName) {
+ MOZ_ASSERT(mOmxTaskQueue->IsCurrentThreadIn());
+ LOG("called from %s", aMethodName);
+ mInitPromise.ResolveIfExists(mTrackInfo->GetType(), aMethodName);
+}
+
+void OmxDataDecoder::RejectInitPromise(MediaResult aError,
+ const char* aMethodName) {
+ MOZ_ASSERT(mOmxTaskQueue->IsCurrentThreadIn());
+ mInitPromise.RejectIfExists(aError, aMethodName);
+}
+
+void OmxDataDecoder::OmxStateRunner() {
+ MOZ_ASSERT(mOmxTaskQueue->IsCurrentThreadIn());
+ LOG("OMX state: %s", StateTypeToStr(mOmxState));
+
+ // TODO: maybe it'd be better to use promise CompletionPromise() to replace
+ // this state machine.
+ if (mOmxState == OMX_StateLoaded) {
+ ConfigCodec();
+
+ // Send OpenMax state command to OMX_StateIdle.
+ RefPtr<OmxDataDecoder> self = this;
+ mOmxLayer->SendCommand(OMX_CommandStateSet, OMX_StateIdle, nullptr)
+ ->Then(
+ mOmxTaskQueue, __func__,
+ [self]() {
+ // Current state should be OMX_StateIdle.
+ self->mOmxState = self->mOmxLayer->GetState();
+ MOZ_ASSERT(self->mOmxState == OMX_StateIdle);
+ },
+ [self]() {
+ self->RejectInitPromise(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
+ });
+
+ // Allocate input and output buffers.
+ OMX_DIRTYPE types[] = {OMX_DIRTYPE::OMX_DirInput,
+ OMX_DIRTYPE::OMX_DirOutput};
+ for (const auto id : types) {
+ if (NS_FAILED(AllocateBuffers(id))) {
+ LOG("Failed to allocate buffer on port %d", id);
+ RejectInitPromise(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
+ break;
+ }
+ }
+ } else if (mOmxState == OMX_StateIdle) {
+ RefPtr<OmxDataDecoder> self = this;
+ mOmxLayer->SendCommand(OMX_CommandStateSet, OMX_StateExecuting, nullptr)
+ ->Then(
+ mOmxTaskQueue, __func__,
+ [self]() {
+ self->mOmxState = self->mOmxLayer->GetState();
+ MOZ_ASSERT(self->mOmxState == OMX_StateExecuting);
+
+ self->ResolveInitPromise(__func__);
+ },
+ [self]() {
+ self->RejectInitPromise(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
+ });
+ } else if (mOmxState == OMX_StateExecuting) {
+ // Configure codec once it gets OMX_StateExecuting state.
+ FillCodecConfigDataToOmx();
+ } else {
+ MOZ_ASSERT(0);
+ }
+}
+
+void OmxDataDecoder::ConfigCodec() {
+ OMX_ERRORTYPE err = mOmxLayer->Config();
+ CHECK_OMX_ERR(err);
+}
+
+void OmxDataDecoder::FillCodecConfigDataToOmx() {
+ // Codec configure data should be the first sample running on Omx TaskQueue.
+ MOZ_ASSERT(mOmxTaskQueue->IsCurrentThreadIn());
+ MOZ_ASSERT(!mMediaRawDatas.Length());
+ MOZ_ASSERT(mOmxState == OMX_StateIdle || mOmxState == OMX_StateExecuting);
+
+ RefPtr<BufferData> inbuf = FindAvailableBuffer(OMX_DirInput);
+ RefPtr<MediaByteBuffer> csc;
+ if (mTrackInfo->IsAudio()) {
+ // It would be nice to instead use more specific information here, but
+ // we force a byte buffer for now since this handles arbitrary codecs.
+ // TODO(bug 1768566): implement further type checking for codec data.
+ csc = ForceGetAudioCodecSpecificBlob(
+ mTrackInfo->GetAsAudioInfo()->mCodecSpecificConfig);
+ } else if (mTrackInfo->IsVideo()) {
+ csc = mTrackInfo->GetAsVideoInfo()->mExtraData;
+ }
+
+ MOZ_RELEASE_ASSERT(csc);
+
+ // Some codecs like h264, its codec specific data is at the first packet, not
+ // in container.
+ if (csc->Length()) {
+ // Buffer size should large enough for raw data.
+ MOZ_RELEASE_ASSERT(inbuf->mBuffer->nAllocLen >= csc->Length());
+
+ memcpy(inbuf->mBuffer->pBuffer, csc->Elements(), csc->Length());
+ inbuf->mBuffer->nFilledLen = csc->Length();
+ inbuf->mBuffer->nOffset = 0;
+ inbuf->mBuffer->nFlags =
+ (OMX_BUFFERFLAG_ENDOFFRAME | OMX_BUFFERFLAG_CODECCONFIG);
+
+ LOG("Feed codec configure data to OMX component");
+ mOmxLayer->EmptyBuffer(inbuf)->Then(mOmxTaskQueue, __func__, this,
+ &OmxDataDecoder::EmptyBufferDone,
+ &OmxDataDecoder::EmptyBufferFailure);
+ }
+}
+
+bool OmxDataDecoder::Event(OMX_EVENTTYPE aEvent, OMX_U32 aData1,
+ OMX_U32 aData2) {
+ MOZ_ASSERT(mOmxTaskQueue->IsCurrentThreadIn());
+
+ if (mOmxLayer->Event(aEvent, aData1, aData2)) {
+ return true;
+ }
+
+ switch (aEvent) {
+ case OMX_EventPortSettingsChanged: {
+ // Don't always disable port. See bug 1235340.
+ if (aData2 == 0 || aData2 == OMX_IndexParamPortDefinition) {
+ // According to spec: "To prevent the loss of any input data, the
+ // component issuing the OMX_EventPortSettingsChanged event on its input
+ // port should buffer all input port data that arrives between the
+ // emission of the OMX_EventPortSettingsChanged event and the arrival of
+ // the command to disable the input port."
+ //
+ // So client needs to disable port and reallocate buffers.
+ MOZ_ASSERT(mPortSettingsChanged == -1);
+ mPortSettingsChanged = aData1;
+ }
+ LOG("Got OMX_EventPortSettingsChanged event");
+ break;
+ }
+ default: {
+ // Got error during decoding, send msg to MFR skipping to next key frame.
+ if (aEvent == OMX_EventError && mOmxState == OMX_StateExecuting) {
+ NotifyError((OMX_ERRORTYPE)aData1, __func__,
+ MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR, __func__));
+ return true;
+ }
+ LOG("WARNING: got none handle event: %d, aData1: %ld, aData2: %ld",
+ aEvent, aData1, aData2);
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool OmxDataDecoder::BuffersCanBeReleased(OMX_DIRTYPE aType) {
+ BUFFERLIST* buffers = GetBuffers(aType);
+ uint32_t len = buffers->Length();
+ for (uint32_t i = 0; i < len; i++) {
+ BufferData::BufferStatus buf_status = buffers->ElementAt(i)->mStatus;
+ if (buf_status == BufferData::BufferStatus::OMX_COMPONENT ||
+ buf_status == BufferData::BufferStatus::OMX_CLIENT_OUTPUT) {
+ return false;
+ }
+ }
+ return true;
+}
+
+OMX_DIRTYPE
+OmxDataDecoder::GetPortDirection(uint32_t aPortIndex) {
+ OMX_PARAM_PORTDEFINITIONTYPE def;
+ InitOmxParameter(&def);
+ def.nPortIndex = mPortSettingsChanged;
+
+ OMX_ERRORTYPE err =
+ mOmxLayer->GetParameter(OMX_IndexParamPortDefinition, &def, sizeof(def));
+ if (err != OMX_ErrorNone) {
+ return OMX_DirMax;
+ }
+ return def.eDir;
+}
+
+RefPtr<OmxPromiseLayer::OmxBufferPromise::AllPromiseType>
+OmxDataDecoder::CollectBufferPromises(OMX_DIRTYPE aType) {
+ MOZ_ASSERT(mOmxTaskQueue->IsCurrentThreadIn());
+
+ nsTArray<RefPtr<OmxBufferPromise>> promises;
+ OMX_DIRTYPE types[] = {OMX_DIRTYPE::OMX_DirInput, OMX_DIRTYPE::OMX_DirOutput};
+ for (const auto type : types) {
+ if ((aType == type) || (aType == OMX_DirMax)) {
+ // find the buffer which has promise.
+ BUFFERLIST* buffers = GetBuffers(type);
+
+ for (uint32_t i = 0; i < buffers->Length(); i++) {
+ BufferData* buf = buffers->ElementAt(i);
+ if (!buf->mPromise.IsEmpty()) {
+ // OmxBufferPromise is not exclusive, it can be multiple "Then"s, so
+ // it is safe to call "Ensure" here.
+ promises.AppendElement(buf->mPromise.Ensure(__func__));
+ }
+ }
+ }
+ }
+
+ LOG("CollectBufferPromises: type %d, total %zu promiese", aType,
+ promises.Length());
+ if (promises.Length()) {
+ return OmxBufferPromise::All(mOmxTaskQueue, promises);
+ }
+
+ return OmxBufferPromise::AllPromiseType::CreateAndResolve(
+ nsTArray<BufferData*>(), __func__);
+}
+
+void OmxDataDecoder::PortSettingsChanged() {
+ MOZ_ASSERT(mOmxTaskQueue->IsCurrentThreadIn());
+
+ if (mPortSettingsChanged == -1 ||
+ mOmxState == OMX_STATETYPE::OMX_StateInvalid) {
+ return;
+ }
+
+ // The PortSettingsChanged algorithm:
+ //
+ // 1. disable port.
+ // 2. wait for port buffers return to client and then release these buffers.
+ // 3. enable port.
+ // 4. allocate port buffers.
+ //
+
+ // Disable port. Get port definition if the target port is enable.
+ OMX_PARAM_PORTDEFINITIONTYPE def;
+ InitOmxParameter(&def);
+ def.nPortIndex = mPortSettingsChanged;
+
+ OMX_ERRORTYPE err =
+ mOmxLayer->GetParameter(OMX_IndexParamPortDefinition, &def, sizeof(def));
+ CHECK_OMX_ERR(err);
+
+ RefPtr<OmxDataDecoder> self = this;
+ if (def.bEnabled) {
+ // 1. disable port.
+ LOG("PortSettingsChanged: disable port %lu", def.nPortIndex);
+ mOmxLayer
+ ->SendCommand(OMX_CommandPortDisable, mPortSettingsChanged, nullptr)
+ ->Then(
+ mOmxTaskQueue, __func__,
+ [self, def]() -> RefPtr<OmxCommandPromise> {
+ // 3. enable port.
+ // Send enable port command.
+ RefPtr<OmxCommandPromise> p = self->mOmxLayer->SendCommand(
+ OMX_CommandPortEnable, self->mPortSettingsChanged, nullptr);
+
+ // 4. allocate port buffers.
+ // Allocate new port buffers.
+ nsresult rv = self->AllocateBuffers(def.eDir);
+ if (NS_FAILED(rv)) {
+ self->NotifyError(OMX_ErrorUndefined, __func__);
+ }
+
+ return p;
+ },
+ [self](const OmxCommandFailureHolder& aError) {
+ self->NotifyError(OMX_ErrorUndefined, __func__);
+ return OmxCommandPromise::CreateAndReject(aError, __func__);
+ })
+ ->Then(
+ mOmxTaskQueue, __func__,
+ [self]() {
+ LOGL("PortSettingsChanged: port settings changed complete");
+ // finish port setting changed.
+ self->mPortSettingsChanged = -1;
+ self->FillAndEmptyBuffers();
+ },
+ [self]() { self->NotifyError(OMX_ErrorUndefined, __func__); });
+
+ // 2. wait for port buffers return to client and then release these buffers.
+ //
+ // Port buffers will be returned to client soon once OMX_CommandPortDisable
+ // command is sent. Then releasing these buffers.
+ CollectBufferPromises(def.eDir)->Then(
+ mOmxTaskQueue, __func__,
+ [self, def]() {
+ MOZ_ASSERT(self->BuffersCanBeReleased(def.eDir));
+ nsresult rv = self->ReleaseBuffers(def.eDir);
+ if (NS_FAILED(rv)) {
+ MOZ_RELEASE_ASSERT(0);
+ self->NotifyError(OMX_ErrorUndefined, __func__);
+ }
+ },
+ [self]() { self->NotifyError(OMX_ErrorUndefined, __func__); });
+ }
+}
+
+void OmxDataDecoder::SendEosBuffer() {
+ MOZ_ASSERT(mOmxTaskQueue->IsCurrentThreadIn());
+
+ // There is no 'Drain' API in OpenMax, so it needs to wait for output sample
+ // with EOS flag. However, MediaRawData doesn't provide EOS information,
+ // so here it generates an empty BufferData with eos OMX_BUFFERFLAG_EOS in
+ // queue. This behaviour should be compliant with spec, I think...
+ RefPtr<MediaRawData> eos_data = new MediaRawData();
+ mMediaRawDatas.AppendElement(eos_data);
+ FillAndEmptyBuffers();
+}
+
+RefPtr<MediaDataDecoder::FlushPromise> OmxDataDecoder::DoFlush() {
+ MOZ_ASSERT(mOmxTaskQueue->IsCurrentThreadIn());
+
+ mDecodedData = DecodedData();
+ mDecodePromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ mDrainPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ mPerformanceRecorder.Record(std::numeric_limits<int64_t>::max());
+
+ RefPtr<FlushPromise> p = mFlushPromise.Ensure(__func__);
+
+ // 1. Call OMX command OMX_CommandFlush in Omx TaskQueue.
+ // 2. Remove all elements in mMediaRawDatas when flush is completed.
+ mOmxLayer->SendCommand(OMX_CommandFlush, OMX_ALL, nullptr)
+ ->Then(mOmxTaskQueue, __func__, this, &OmxDataDecoder::FlushComplete,
+ &OmxDataDecoder::FlushFailure);
+
+ return p;
+}
+
+void OmxDataDecoder::FlushComplete(OMX_COMMANDTYPE aCommandType) {
+ mMediaRawDatas.Clear();
+ mFlushing = false;
+
+ LOG("Flush complete");
+ mFlushPromise.ResolveIfExists(true, __func__);
+}
+
+void OmxDataDecoder::FlushFailure(OmxCommandFailureHolder aFailureHolder) {
+ mFlushing = false;
+ mFlushPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
+}
+
+MediaDataHelper::MediaDataHelper(const TrackInfo* aTrackInfo,
+ layers::ImageContainer* aImageContainer,
+ OmxPromiseLayer* aOmxLayer)
+ : mTrackInfo(aTrackInfo),
+ mAudioCompactor(mAudioQueue),
+ mImageContainer(aImageContainer) {
+ InitOmxParameter(&mOutputPortDef);
+ mOutputPortDef.nPortIndex = aOmxLayer->OutputPortIndex();
+ aOmxLayer->GetParameter(OMX_IndexParamPortDefinition, &mOutputPortDef,
+ sizeof(mOutputPortDef));
+}
+
+already_AddRefed<MediaData> MediaDataHelper::GetMediaData(
+ BufferData* aBufferData, bool& aPlatformDepenentData) {
+ aPlatformDepenentData = false;
+ RefPtr<MediaData> data;
+
+ if (mTrackInfo->IsAudio()) {
+ if (!aBufferData->mBuffer->nFilledLen) {
+ return nullptr;
+ }
+ data = CreateAudioData(aBufferData);
+ } else if (mTrackInfo->IsVideo()) {
+ data = aBufferData->GetPlatformMediaData();
+ if (data) {
+ aPlatformDepenentData = true;
+ } else {
+ if (!aBufferData->mBuffer->nFilledLen) {
+ return nullptr;
+ }
+ // Get YUV VideoData, it uses more CPU, in most cases, on software codec.
+ data = CreateYUV420VideoData(aBufferData);
+ }
+
+ // Update video time code, duration... from the raw data.
+ VideoData* video(data->As<VideoData>());
+ if (aBufferData->mRawData) {
+ video->mTime = aBufferData->mRawData->mTime;
+ video->mTimecode = aBufferData->mRawData->mTimecode;
+ video->mOffset = aBufferData->mRawData->mOffset;
+ video->mDuration = aBufferData->mRawData->mDuration;
+ video->mKeyframe = aBufferData->mRawData->mKeyframe;
+ }
+ }
+
+ return data.forget();
+}
+
+already_AddRefed<AudioData> MediaDataHelper::CreateAudioData(
+ BufferData* aBufferData) {
+ RefPtr<AudioData> audio;
+ OMX_BUFFERHEADERTYPE* buf = aBufferData->mBuffer;
+ const AudioInfo* info = mTrackInfo->GetAsAudioInfo();
+ if (buf->nFilledLen) {
+ uint64_t offset = 0;
+ uint32_t frames = buf->nFilledLen / (2 * info->mChannels);
+ if (aBufferData->mRawData) {
+ offset = aBufferData->mRawData->mOffset;
+ }
+ typedef AudioCompactor::NativeCopy OmxCopy;
+ mAudioCompactor.Push(
+ offset, buf->nTimeStamp, info->mRate, frames, info->mChannels,
+ OmxCopy(buf->pBuffer + buf->nOffset, buf->nFilledLen, info->mChannels));
+ audio = mAudioQueue.PopFront();
+ }
+
+ return audio.forget();
+}
+
+already_AddRefed<VideoData> MediaDataHelper::CreateYUV420VideoData(
+ BufferData* aBufferData) {
+ uint8_t* yuv420p_buffer = (uint8_t*)aBufferData->mBuffer->pBuffer;
+ int32_t stride = mOutputPortDef.format.video.nStride;
+ int32_t slice_height = mOutputPortDef.format.video.nSliceHeight;
+ int32_t width = mTrackInfo->GetAsVideoInfo()->mImage.width;
+ int32_t height = mTrackInfo->GetAsVideoInfo()->mImage.height;
+
+ // TODO: convert other formats to YUV420.
+ if (mOutputPortDef.format.video.eColorFormat !=
+ OMX_COLOR_FormatYUV420Planar) {
+ return nullptr;
+ }
+
+ size_t yuv420p_y_size = stride * slice_height;
+ size_t yuv420p_u_size = ((stride + 1) / 2) * ((slice_height + 1) / 2);
+ uint8_t* yuv420p_y = yuv420p_buffer;
+ uint8_t* yuv420p_u = yuv420p_y + yuv420p_y_size;
+ uint8_t* yuv420p_v = yuv420p_u + yuv420p_u_size;
+
+ VideoData::YCbCrBuffer b;
+ b.mPlanes[0].mData = yuv420p_y;
+ b.mPlanes[0].mWidth = width;
+ b.mPlanes[0].mHeight = height;
+ b.mPlanes[0].mStride = stride;
+ b.mPlanes[0].mSkip = 0;
+
+ b.mPlanes[1].mData = yuv420p_u;
+ b.mPlanes[1].mWidth = (width + 1) / 2;
+ b.mPlanes[1].mHeight = (height + 1) / 2;
+ b.mPlanes[1].mStride = (stride + 1) / 2;
+ b.mPlanes[1].mSkip = 0;
+
+ b.mPlanes[2].mData = yuv420p_v;
+ b.mPlanes[2].mWidth = (width + 1) / 2;
+ b.mPlanes[2].mHeight = (height + 1) / 2;
+ b.mPlanes[2].mStride = (stride + 1) / 2;
+ b.mPlanes[2].mSkip = 0;
+
+ b.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT;
+
+ VideoInfo info(*mTrackInfo->GetAsVideoInfo());
+
+ auto maybeColorSpace = info.mColorSpace;
+ if (!maybeColorSpace) {
+ maybeColorSpace = Some(DefaultColorSpace({width, height}));
+ }
+ b.mYUVColorSpace = *maybeColorSpace;
+
+ auto maybeColorPrimaries = info.mColorPrimaries;
+ if (!maybeColorPrimaries) {
+ maybeColorPrimaries = Some(gfx::ColorSpace2::BT709);
+ }
+ b.mColorPrimaries = *maybeColorPrimaries;
+
+ RefPtr<VideoData> data = VideoData::CreateAndCopyData(
+ info, mImageContainer,
+ 0, // Filled later by caller.
+ media::TimeUnit::Zero(), // Filled later by caller.
+ media::TimeUnit::FromMicroseconds(1), // We don't know the duration.
+ b,
+ 0, // Filled later by caller.
+ media::TimeUnit::FromMicroseconds(-1), info.ImageRect(), nullptr);
+
+ MOZ_LOG(sPDMLog, mozilla::LogLevel::Debug,
+ ("YUV420 VideoData: disp width %d, height %d, pic width %d, height "
+ "%d, time %lld",
+ info.mDisplay.width, info.mDisplay.height, info.mImage.width,
+ info.mImage.height, aBufferData->mBuffer->nTimeStamp));
+
+ return data.forget();
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/omx/OmxDataDecoder.h b/dom/media/platforms/omx/OmxDataDecoder.h
new file mode 100644
index 0000000000..69c388ecee
--- /dev/null
+++ b/dom/media/platforms/omx/OmxDataDecoder.h
@@ -0,0 +1,224 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(OmxDataDecoder_h_)
+# define OmxDataDecoder_h_
+
+# include "AudioCompactor.h"
+# include "ImageContainer.h"
+# include "MediaInfo.h"
+# include "OMX_Component.h"
+# include "OmxPromiseLayer.h"
+# include "PerformanceRecorder.h"
+# include "PlatformDecoderModule.h"
+# include "mozilla/Monitor.h"
+# include "mozilla/StateWatching.h"
+
+namespace mozilla {
+
+class MediaDataHelper;
+
+typedef OmxPromiseLayer::OmxCommandPromise OmxCommandPromise;
+typedef OmxPromiseLayer::OmxBufferPromise OmxBufferPromise;
+typedef OmxPromiseLayer::OmxBufferFailureHolder OmxBufferFailureHolder;
+typedef OmxPromiseLayer::OmxCommandFailureHolder OmxCommandFailureHolder;
+typedef OmxPromiseLayer::BufferData BufferData;
+typedef OmxPromiseLayer::BUFFERLIST BUFFERLIST;
+
+DDLoggedTypeDeclNameAndBase(OmxDataDecoder, MediaDataDecoder);
+
+/* OmxDataDecoder is the major class which performs followings:
+ * 1. Translate PDM function into OMX commands.
+ * 2. Keeping the buffers between client and component.
+ * 3. Manage the OMX state.
+ *
+ * From the definition in OpenMax spec. "2.2.1", there are 3 major roles in
+ * OpenMax IL.
+ *
+ * IL client:
+ * "The IL client may be a layer below the GUI application, such as GStreamer,
+ * or may be several layers below the GUI layer."
+ *
+ * OmxDataDecoder acts as the IL client.
+ *
+ * OpenMAX IL component:
+ * "A component that is intended to wrap functionality that is required in the
+ * target system."
+ *
+ * OmxPromiseLayer acts as the OpenMAX IL component.
+ *
+ * OpenMAX IL core:
+ * "Platform-specific code that has the functionality necessary to locate and
+ * then load an OpenMAX IL component into main memory."
+ *
+ * OmxPlatformLayer acts as the OpenMAX IL core.
+ */
+class OmxDataDecoder final : public MediaDataDecoder,
+ public DecoderDoctorLifeLogger<OmxDataDecoder> {
+ protected:
+ virtual ~OmxDataDecoder();
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(OmxDataDecoder, final);
+
+ OmxDataDecoder(const TrackInfo& aTrackInfo,
+ layers::ImageContainer* aImageContainer,
+ Maybe<TrackingId> aTrackingId);
+
+ RefPtr<InitPromise> Init() override;
+ RefPtr<DecodePromise> Decode(MediaRawData* aSample) override;
+ RefPtr<DecodePromise> Drain() override;
+ RefPtr<FlushPromise> Flush() override;
+ RefPtr<ShutdownPromise> Shutdown() override;
+
+ nsCString GetDescriptionName() const override { return "omx decoder"_ns; }
+
+ nsCString GetCodecName() const override { return "unknown"_ns; }
+
+ ConversionRequired NeedsConversion() const override {
+ return ConversionRequired::kNeedAnnexB;
+ }
+
+ // Return true if event is handled.
+ bool Event(OMX_EVENTTYPE aEvent, OMX_U32 aData1, OMX_U32 aData2);
+
+ protected:
+ void InitializationTask();
+
+ void ResolveInitPromise(const char* aMethodName);
+
+ void RejectInitPromise(MediaResult aError, const char* aMethodName);
+
+ void OmxStateRunner();
+
+ void FillAndEmptyBuffers();
+
+ void FillBufferDone(BufferData* aData);
+
+ void FillBufferFailure(OmxBufferFailureHolder aFailureHolder);
+
+ void EmptyBufferDone(BufferData* aData);
+
+ void EmptyBufferFailure(OmxBufferFailureHolder aFailureHolder);
+
+ void NotifyError(
+ OMX_ERRORTYPE aOmxError, const char* aLine,
+ const MediaResult& aError = MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR));
+
+ // Configure audio/video codec.
+ // Some codec may just ignore this and rely on codec specific data in
+ // FillCodecConfigDataToOmx().
+ void ConfigCodec();
+
+ // Sending codec specific data to OMX component. OMX component could send a
+ // OMX_EventPortSettingsChanged back to client. And then client needs to
+ // disable port and reallocate buffer.
+ void FillCodecConfigDataToOmx();
+
+ void SendEosBuffer();
+
+ void EndOfStream();
+
+ // It could be called after codec specific data is sent and component found
+ // the port format is changed due to different codec specific.
+ void PortSettingsChanged();
+
+ void Output(BufferData* aData);
+
+ // Buffer can be released if its status is not OMX_COMPONENT or
+ // OMX_CLIENT_OUTPUT.
+ bool BuffersCanBeReleased(OMX_DIRTYPE aType);
+
+ OMX_DIRTYPE GetPortDirection(uint32_t aPortIndex);
+
+ RefPtr<ShutdownPromise> DoAsyncShutdown();
+
+ RefPtr<FlushPromise> DoFlush();
+
+ void FlushComplete(OMX_COMMANDTYPE aCommandType);
+
+ void FlushFailure(OmxCommandFailureHolder aFailureHolder);
+
+ BUFFERLIST* GetBuffers(OMX_DIRTYPE aType);
+
+ nsresult AllocateBuffers(OMX_DIRTYPE aType);
+
+ nsresult ReleaseBuffers(OMX_DIRTYPE aType);
+
+ BufferData* FindAvailableBuffer(OMX_DIRTYPE aType);
+
+ // aType could be OMX_DirMax for all types.
+ RefPtr<OmxPromiseLayer::OmxBufferPromise::AllPromiseType>
+ CollectBufferPromises(OMX_DIRTYPE aType);
+
+ // The Omx TaskQueue.
+ RefPtr<TaskQueue> mOmxTaskQueue;
+
+ nsCOMPtr<nsISerialEventTarget> mThread;
+ RefPtr<layers::ImageContainer> mImageContainer;
+
+ WatchManager<OmxDataDecoder> mWatchManager;
+
+ // It is accessed in omx TaskQueue.
+ Watchable<OMX_STATETYPE> mOmxState;
+
+ RefPtr<OmxPromiseLayer> mOmxLayer;
+
+ UniquePtr<TrackInfo> mTrackInfo;
+
+ // It is accessed in both omx and reader TaskQueue.
+ Atomic<bool> mFlushing;
+
+ // It is accessed in Omx/reader TaskQueue.
+ Atomic<bool> mShuttingDown;
+
+ // It is accessed in Omx TaskQeueu.
+ bool mCheckingInputExhausted;
+
+ // It is accessed in OMX TaskQueue.
+ MozPromiseHolder<InitPromise> mInitPromise;
+ MozPromiseHolder<DecodePromise> mDecodePromise;
+ MozPromiseHolder<DecodePromise> mDrainPromise;
+ MozPromiseHolder<FlushPromise> mFlushPromise;
+ MozPromiseHolder<ShutdownPromise> mShutdownPromise;
+ // Where decoded samples will be stored until the decode promise is resolved.
+ DecodedData mDecodedData;
+
+ void CompleteDrain();
+
+ // It is written in Omx TaskQueue. Read in Omx TaskQueue.
+ // It value means the port index which port settings is changed.
+ // -1 means no port setting changed.
+ //
+ // Note: when port setting changed, there should be no buffer operations
+ // via EmptyBuffer or FillBuffer.
+ Watchable<int32_t> mPortSettingsChanged;
+
+ // It is access in Omx TaskQueue.
+ nsTArray<RefPtr<MediaRawData>> mMediaRawDatas;
+
+ BUFFERLIST mInPortBuffers;
+
+ BUFFERLIST mOutPortBuffers;
+
+ RefPtr<MediaDataHelper> mMediaDataHelper;
+
+ const Maybe<TrackingId> mTrackingId;
+
+ // Accessed on Omx TaskQueue
+ PerformanceRecorderMulti<DecodeStage> mPerformanceRecorder;
+};
+
+template <class T>
+void InitOmxParameter(T* aParam) {
+ PodZero(aParam);
+ aParam->nSize = sizeof(T);
+ aParam->nVersion.s.nVersionMajor = 1;
+}
+
+} // namespace mozilla
+
+#endif /* OmxDataDecoder_h_ */
diff --git a/dom/media/platforms/omx/OmxDecoderModule.cpp b/dom/media/platforms/omx/OmxDecoderModule.cpp
new file mode 100644
index 0000000000..2267e66cc3
--- /dev/null
+++ b/dom/media/platforms/omx/OmxDecoderModule.cpp
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "OmxDecoderModule.h"
+
+#include "OmxDataDecoder.h"
+#include "OmxPlatformLayer.h"
+
+#ifdef MOZ_OMX
+# include "PureOmxPlatformLayer.h"
+#endif
+
+namespace mozilla {
+
+/* static */
+bool OmxDecoderModule::Init() {
+#ifdef MOZ_OMX
+ return PureOmxPlatformLayer::Init();
+#endif
+ return false;
+}
+
+OmxDecoderModule* OmxDecoderModule::Create() {
+#ifdef MOZ_OMX
+ if (Init()) {
+ return new OmxDecoderModule();
+ }
+#endif
+ return nullptr;
+}
+
+already_AddRefed<MediaDataDecoder> OmxDecoderModule::CreateVideoDecoder(
+ const CreateDecoderParams& aParams) {
+ RefPtr<OmxDataDecoder> decoder = new OmxDataDecoder(
+ aParams.mConfig, aParams.mImageContainer, aParams.mTrackingId);
+ return decoder.forget();
+}
+
+already_AddRefed<MediaDataDecoder> OmxDecoderModule::CreateAudioDecoder(
+ const CreateDecoderParams& aParams) {
+ RefPtr<OmxDataDecoder> decoder =
+ new OmxDataDecoder(aParams.mConfig, nullptr, aParams.mTrackingId);
+ return decoder.forget();
+}
+
+media::DecodeSupportSet OmxDecoderModule::SupportsMimeType(
+ const nsACString& aMimeType, DecoderDoctorDiagnostics* aDiagnostics) const {
+ if (OmxPlatformLayer::SupportsMimeType(aMimeType)) {
+ // TODO: Note that we do not yet distinguish between SW/HW decode support.
+ // Will be done in bug 1754239.
+ return media::DecodeSupport::SoftwareDecode;
+ }
+ return media::DecodeSupport::Unsupported;
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/omx/OmxDecoderModule.h b/dom/media/platforms/omx/OmxDecoderModule.h
new file mode 100644
index 0000000000..04fa809e54
--- /dev/null
+++ b/dom/media/platforms/omx/OmxDecoderModule.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(OmxDecoderModule_h_)
+# define OmxDecoderModule_h_
+
+# include "PlatformDecoderModule.h"
+
+namespace mozilla {
+
+class OmxDecoderModule : public PlatformDecoderModule {
+ public:
+ // Called on main thread.
+ static bool Init();
+ static OmxDecoderModule* Create();
+
+ already_AddRefed<MediaDataDecoder> CreateVideoDecoder(
+ const CreateDecoderParams& aParams) override;
+
+ already_AddRefed<MediaDataDecoder> CreateAudioDecoder(
+ const CreateDecoderParams& aParams) override;
+
+ media::DecodeSupportSet SupportsMimeType(
+ const nsACString& aMimeType,
+ DecoderDoctorDiagnostics* aDiagnostics) const override;
+};
+
+} // namespace mozilla
+
+#endif // OmxDecoderModule_h_
diff --git a/dom/media/platforms/omx/OmxFunctionList.h b/dom/media/platforms/omx/OmxFunctionList.h
new file mode 100644
index 0000000000..e1e92bfe65
--- /dev/null
+++ b/dom/media/platforms/omx/OmxFunctionList.h
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+OMX_FUNC(OMX_Init)
+OMX_FUNC(OMX_Deinit)
+OMX_FUNC(OMX_GetHandle)
+OMX_FUNC(OMX_FreeHandle)
+OMX_FUNC(OMX_ComponentNameEnum)
+OMX_FUNC(OMX_GetComponentsOfRole)
+OMX_FUNC(OMX_GetRolesOfComponent)
+OMX_FUNC(OMX_SetupTunnel)
+OMX_FUNC(OMX_GetContentPipe)
diff --git a/dom/media/platforms/omx/OmxPlatformLayer.cpp b/dom/media/platforms/omx/OmxPlatformLayer.cpp
new file mode 100644
index 0000000000..dc3ccc0979
--- /dev/null
+++ b/dom/media/platforms/omx/OmxPlatformLayer.cpp
@@ -0,0 +1,307 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "OmxPlatformLayer.h"
+
+#include "OmxDataDecoder.h"
+#include "OMX_Component.h"
+#include "OMX_VideoExt.h" // For VP8.
+
+#ifdef MOZ_OMX
+# include "PureOmxPlatformLayer.h"
+#endif
+
+#include "VPXDecoder.h"
+
+#ifdef LOG
+# undef LOG
+#endif
+
+#define LOG(arg, ...) \
+ MOZ_LOG(sPDMLog, mozilla::LogLevel::Debug, \
+ ("OmxPlatformLayer -- %s: " arg, __func__, ##__VA_ARGS__))
+
+#define RETURN_IF_ERR(err) \
+ if (err != OMX_ErrorNone) { \
+ LOG("error: 0x%08x", err); \
+ return err; \
+ }
+
+// Common OMX decoder configuration code.
+namespace mozilla {
+
+// This helper class encapsulates the details of component parameters setting
+// for different OMX audio & video codecs.
+template <typename ParamType>
+class OmxConfig {
+ public:
+ virtual ~OmxConfig() = default;
+ // Subclasses should implement this method to configure the codec.
+ virtual OMX_ERRORTYPE Apply(OmxPlatformLayer& aOmx,
+ const ParamType& aParam) = 0;
+};
+
+typedef OmxConfig<AudioInfo> OmxAudioConfig;
+typedef OmxConfig<VideoInfo> OmxVideoConfig;
+
+template <typename ConfigType>
+UniquePtr<ConfigType> ConfigForMime(const nsACString&);
+
+static OMX_ERRORTYPE ConfigAudioOutputPort(OmxPlatformLayer& aOmx,
+ const AudioInfo& aInfo) {
+ OMX_PARAM_PORTDEFINITIONTYPE def;
+ InitOmxParameter(&def);
+ def.nPortIndex = aOmx.OutputPortIndex();
+ OMX_ERRORTYPE err =
+ aOmx.GetParameter(OMX_IndexParamPortDefinition, &def, sizeof(def));
+ RETURN_IF_ERR(err);
+
+ def.format.audio.eEncoding = OMX_AUDIO_CodingPCM;
+ err = aOmx.SetParameter(OMX_IndexParamPortDefinition, &def, sizeof(def));
+ RETURN_IF_ERR(err);
+
+ OMX_AUDIO_PARAM_PCMMODETYPE pcmParams;
+ InitOmxParameter(&pcmParams);
+ pcmParams.nPortIndex = def.nPortIndex;
+ err =
+ aOmx.GetParameter(OMX_IndexParamAudioPcm, &pcmParams, sizeof(pcmParams));
+ RETURN_IF_ERR(err);
+
+ pcmParams.nChannels = aInfo.mChannels;
+ pcmParams.eNumData = OMX_NumericalDataSigned;
+ pcmParams.bInterleaved = OMX_TRUE;
+ pcmParams.nBitPerSample = 16;
+ pcmParams.nSamplingRate = aInfo.mRate;
+ pcmParams.ePCMMode = OMX_AUDIO_PCMModeLinear;
+ err =
+ aOmx.SetParameter(OMX_IndexParamAudioPcm, &pcmParams, sizeof(pcmParams));
+ RETURN_IF_ERR(err);
+
+ LOG("Config OMX_IndexParamAudioPcm, channel %lu, sample rate %lu",
+ pcmParams.nChannels, pcmParams.nSamplingRate);
+
+ return OMX_ErrorNone;
+}
+
+class OmxAacConfig : public OmxAudioConfig {
+ public:
+ OMX_ERRORTYPE Apply(OmxPlatformLayer& aOmx, const AudioInfo& aInfo) override {
+ OMX_AUDIO_PARAM_AACPROFILETYPE aacProfile;
+ InitOmxParameter(&aacProfile);
+ aacProfile.nPortIndex = aOmx.InputPortIndex();
+ OMX_ERRORTYPE err = aOmx.GetParameter(OMX_IndexParamAudioAac, &aacProfile,
+ sizeof(aacProfile));
+ RETURN_IF_ERR(err);
+
+ aacProfile.nChannels = aInfo.mChannels;
+ aacProfile.nSampleRate = aInfo.mRate;
+ aacProfile.eAACProfile =
+ static_cast<OMX_AUDIO_AACPROFILETYPE>(aInfo.mProfile);
+ err = aOmx.SetParameter(OMX_IndexParamAudioAac, &aacProfile,
+ sizeof(aacProfile));
+ RETURN_IF_ERR(err);
+
+ LOG("Config OMX_IndexParamAudioAac, channel %lu, sample rate %lu, profile "
+ "%d",
+ aacProfile.nChannels, aacProfile.nSampleRate, aacProfile.eAACProfile);
+
+ return ConfigAudioOutputPort(aOmx, aInfo);
+ }
+};
+
+class OmxMp3Config : public OmxAudioConfig {
+ public:
+ OMX_ERRORTYPE Apply(OmxPlatformLayer& aOmx, const AudioInfo& aInfo) override {
+ OMX_AUDIO_PARAM_MP3TYPE mp3Param;
+ InitOmxParameter(&mp3Param);
+ mp3Param.nPortIndex = aOmx.InputPortIndex();
+ OMX_ERRORTYPE err =
+ aOmx.GetParameter(OMX_IndexParamAudioMp3, &mp3Param, sizeof(mp3Param));
+ RETURN_IF_ERR(err);
+
+ mp3Param.nChannels = aInfo.mChannels;
+ mp3Param.nSampleRate = aInfo.mRate;
+ err =
+ aOmx.SetParameter(OMX_IndexParamAudioMp3, &mp3Param, sizeof(mp3Param));
+ RETURN_IF_ERR(err);
+
+ LOG("Config OMX_IndexParamAudioMp3, channel %lu, sample rate %lu",
+ mp3Param.nChannels, mp3Param.nSampleRate);
+
+ return ConfigAudioOutputPort(aOmx, aInfo);
+ }
+};
+
+enum OmxAmrSampleRate {
+ kNarrowBand = 8000,
+ kWideBand = 16000,
+};
+
+template <OmxAmrSampleRate R>
+class OmxAmrConfig : public OmxAudioConfig {
+ public:
+ OMX_ERRORTYPE Apply(OmxPlatformLayer& aOmx, const AudioInfo& aInfo) override {
+ OMX_AUDIO_PARAM_AMRTYPE def;
+ InitOmxParameter(&def);
+ def.nPortIndex = aOmx.InputPortIndex();
+ OMX_ERRORTYPE err =
+ aOmx.GetParameter(OMX_IndexParamAudioAmr, &def, sizeof(def));
+ RETURN_IF_ERR(err);
+
+ def.eAMRFrameFormat = OMX_AUDIO_AMRFrameFormatFSF;
+ err = aOmx.SetParameter(OMX_IndexParamAudioAmr, &def, sizeof(def));
+ RETURN_IF_ERR(err);
+
+ MOZ_ASSERT(aInfo.mChannels == 1);
+ MOZ_ASSERT(aInfo.mRate == R);
+
+ return ConfigAudioOutputPort(aOmx, aInfo);
+ }
+};
+
+template <>
+UniquePtr<OmxAudioConfig> ConfigForMime(const nsACString& aMimeType) {
+ UniquePtr<OmxAudioConfig> conf;
+
+ if (OmxPlatformLayer::SupportsMimeType(aMimeType)) {
+ if (aMimeType.EqualsLiteral("audio/mp4a-latm")) {
+ conf.reset(new OmxAacConfig());
+ } else if (aMimeType.EqualsLiteral("audio/mp3") ||
+ aMimeType.EqualsLiteral("audio/mpeg")) {
+ conf.reset(new OmxMp3Config());
+ } else if (aMimeType.EqualsLiteral("audio/3gpp")) {
+ conf.reset(new OmxAmrConfig<OmxAmrSampleRate::kNarrowBand>());
+ } else if (aMimeType.EqualsLiteral("audio/amr-wb")) {
+ conf.reset(new OmxAmrConfig<OmxAmrSampleRate::kWideBand>());
+ }
+ }
+ return conf;
+}
+
+// There should be a better way to calculate it.
+#define MIN_VIDEO_INPUT_BUFFER_SIZE 64 * 1024
+
+class OmxCommonVideoConfig : public OmxVideoConfig {
+ public:
+ explicit OmxCommonVideoConfig() : OmxVideoConfig() {}
+
+ OMX_ERRORTYPE Apply(OmxPlatformLayer& aOmx, const VideoInfo& aInfo) override {
+ OMX_ERRORTYPE err = OMX_ErrorNone;
+ OMX_PARAM_PORTDEFINITIONTYPE def;
+
+ // Set up in/out port definition.
+ nsTArray<uint32_t> ports;
+ aOmx.GetPortIndices(ports);
+ for (auto idx : ports) {
+ InitOmxParameter(&def);
+ def.nPortIndex = idx;
+ err = aOmx.GetParameter(OMX_IndexParamPortDefinition, &def, sizeof(def));
+ RETURN_IF_ERR(err);
+
+ def.format.video.nFrameWidth = aInfo.mDisplay.width;
+ def.format.video.nFrameHeight = aInfo.mDisplay.height;
+ def.format.video.nStride = aInfo.mImage.width;
+ def.format.video.nSliceHeight = aInfo.mImage.height;
+
+ if (def.eDir == OMX_DirInput) {
+ def.format.video.eCompressionFormat = aOmx.CompressionFormat();
+ def.format.video.eColorFormat = OMX_COLOR_FormatUnused;
+ if (def.nBufferSize < MIN_VIDEO_INPUT_BUFFER_SIZE) {
+ def.nBufferSize = aInfo.mImage.width * aInfo.mImage.height;
+ LOG("Change input buffer size to %lu", def.nBufferSize);
+ }
+ } else {
+ def.format.video.eCompressionFormat = OMX_VIDEO_CodingUnused;
+ }
+
+ err = aOmx.SetParameter(OMX_IndexParamPortDefinition, &def, sizeof(def));
+ }
+ return err;
+ }
+};
+
+template <>
+UniquePtr<OmxVideoConfig> ConfigForMime(const nsACString& aMimeType) {
+ UniquePtr<OmxVideoConfig> conf;
+
+ if (OmxPlatformLayer::SupportsMimeType(aMimeType)) {
+ conf.reset(new OmxCommonVideoConfig());
+ }
+ return conf;
+}
+
+OMX_ERRORTYPE
+OmxPlatformLayer::Config() {
+ MOZ_ASSERT(mInfo);
+
+ OMX_PORT_PARAM_TYPE portParam;
+ InitOmxParameter(&portParam);
+ if (mInfo->IsAudio()) {
+ GetParameter(OMX_IndexParamAudioInit, &portParam, sizeof(portParam));
+ mStartPortNumber = portParam.nStartPortNumber;
+ UniquePtr<OmxAudioConfig> conf(
+ ConfigForMime<OmxAudioConfig>(mInfo->mMimeType));
+ MOZ_RELEASE_ASSERT(conf.get());
+ return conf->Apply(*this, *(mInfo->GetAsAudioInfo()));
+ } else if (mInfo->IsVideo()) {
+ GetParameter(OMX_IndexParamVideoInit, &portParam, sizeof(portParam));
+ UniquePtr<OmxVideoConfig> conf(
+ ConfigForMime<OmxVideoConfig>(mInfo->mMimeType));
+ MOZ_RELEASE_ASSERT(conf.get());
+ return conf->Apply(*this, *(mInfo->GetAsVideoInfo()));
+ } else {
+ MOZ_ASSERT_UNREACHABLE("non-AV data (text?) is not supported.");
+ return OMX_ErrorNotImplemented;
+ }
+}
+
+OMX_VIDEO_CODINGTYPE
+OmxPlatformLayer::CompressionFormat() {
+ MOZ_ASSERT(mInfo);
+
+ if (mInfo->mMimeType.EqualsLiteral("video/avc")) {
+ return OMX_VIDEO_CodingAVC;
+ } else if (mInfo->mMimeType.EqualsLiteral("video/mp4v-es") ||
+ mInfo->mMimeType.EqualsLiteral("video/mp4")) {
+ return OMX_VIDEO_CodingMPEG4;
+ } else if (mInfo->mMimeType.EqualsLiteral("video/3gpp")) {
+ return OMX_VIDEO_CodingH263;
+ } else if (VPXDecoder::IsVP8(mInfo->mMimeType)) {
+ return static_cast<OMX_VIDEO_CODINGTYPE>(OMX_VIDEO_CodingVP8);
+ } else {
+ MOZ_ASSERT_UNREACHABLE("Unsupported compression format");
+ return OMX_VIDEO_CodingUnused;
+ }
+}
+
+// Implementations for different platforms will be defined in their own files.
+#if defined(MOZ_OMX)
+
+bool OmxPlatformLayer::SupportsMimeType(const nsACString& aMimeType) {
+ return PureOmxPlatformLayer::SupportsMimeType(aMimeType);
+}
+
+OmxPlatformLayer* OmxPlatformLayer::Create(
+ OmxDataDecoder* aDataDecoder, OmxPromiseLayer* aPromiseLayer,
+ TaskQueue* aTaskQueue, layers::ImageContainer* aImageContainer) {
+ return new PureOmxPlatformLayer(aDataDecoder, aPromiseLayer, aTaskQueue,
+ aImageContainer);
+}
+
+#else // For platforms without OMX IL support.
+
+bool OmxPlatformLayer::SupportsMimeType(const nsACString& aMimeType) {
+ return false;
+}
+
+OmxPlatformLayer* OmxPlatformLayer::Create(
+ OmxDataDecoder* aDataDecoder, OmxPromiseLayer* aPromiseLayer,
+ TaskQueue* aTaskQueue, layers::ImageContainer* aImageContainer) {
+ return nullptr;
+}
+
+#endif
+} // namespace mozilla
diff --git a/dom/media/platforms/omx/OmxPlatformLayer.h b/dom/media/platforms/omx/OmxPlatformLayer.h
new file mode 100644
index 0000000000..53f3dccbca
--- /dev/null
+++ b/dom/media/platforms/omx/OmxPlatformLayer.h
@@ -0,0 +1,103 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(OmxPlatformLayer_h_)
+# define OmxPlatformLayer_h_
+
+# include "OMX_Core.h"
+# include "OMX_Types.h"
+# include "OMX_Video.h"
+
+# include "nsStringFwd.h"
+# include "OmxPromiseLayer.h"
+
+namespace mozilla {
+
+class TaskQueue;
+class TrackInfo;
+
+/*
+ * This class the the abstract layer of the platform OpenMax IL implementation.
+ *
+ * For some platform like andoird, it exposures its OpenMax IL via IOMX which
+ * is definitions are different comparing to standard.
+ * For other platforms like Raspberry Pi, it will be easy to implement this
+ * layer with the standard OpenMax IL api.
+ */
+class OmxPlatformLayer {
+ public:
+ typedef OmxPromiseLayer::BUFFERLIST BUFFERLIST;
+ typedef OmxPromiseLayer::BufferData BufferData;
+
+ virtual OMX_ERRORTYPE InitOmxToStateLoaded(const TrackInfo* aInfo) = 0;
+
+ OMX_ERRORTYPE Config();
+
+ virtual OMX_ERRORTYPE EmptyThisBuffer(BufferData* aData) = 0;
+
+ virtual OMX_ERRORTYPE FillThisBuffer(BufferData* aData) = 0;
+
+ virtual OMX_ERRORTYPE SendCommand(OMX_COMMANDTYPE aCmd, OMX_U32 aParam1,
+ OMX_PTR aCmdData) = 0;
+
+ // Buffer could be platform dependent. Therefore, derived class needs to
+ // implement its owned buffer allocate/release API according to its platform
+ // type.
+ virtual nsresult AllocateOmxBuffer(OMX_DIRTYPE aType,
+ BUFFERLIST* aBufferList) = 0;
+
+ virtual nsresult ReleaseOmxBuffer(OMX_DIRTYPE aType,
+ BUFFERLIST* aBufferList) = 0;
+
+ virtual OMX_ERRORTYPE GetState(OMX_STATETYPE* aType) = 0;
+
+ virtual OMX_ERRORTYPE GetParameter(OMX_INDEXTYPE aParamIndex,
+ OMX_PTR aComponentParameterStructure,
+ OMX_U32 aComponentParameterSize) = 0;
+
+ virtual OMX_ERRORTYPE SetParameter(OMX_INDEXTYPE nIndex,
+ OMX_PTR aComponentParameterStructure,
+ OMX_U32 aComponentParameterSize) = 0;
+
+ virtual nsresult Shutdown() = 0;
+
+ virtual ~OmxPlatformLayer() = default;
+
+ // For decoders, input port index is start port number and output port is
+ // next. See OpenMAX IL spec v1.1.2 section 8.6.1 & 8.8.1.
+ OMX_U32 InputPortIndex() { return mStartPortNumber; }
+
+ OMX_U32 OutputPortIndex() { return mStartPortNumber + 1; }
+
+ void GetPortIndices(nsTArray<uint32_t>& aPortIndex) {
+ aPortIndex.AppendElement(InputPortIndex());
+ aPortIndex.AppendElement(OutputPortIndex());
+ }
+
+ virtual OMX_VIDEO_CODINGTYPE CompressionFormat();
+
+ // Check if the platform implementation supports given MIME type.
+ static bool SupportsMimeType(const nsACString& aMimeType);
+
+ // Hide the details of creating implementation objects for different
+ // platforms.
+ static OmxPlatformLayer* Create(OmxDataDecoder* aDataDecoder,
+ OmxPromiseLayer* aPromiseLayer,
+ TaskQueue* aTaskQueue,
+ layers::ImageContainer* aImageContainer);
+
+ protected:
+ OmxPlatformLayer() : mInfo(nullptr), mStartPortNumber(0) {}
+
+ // The pointee is held by |OmxDataDecoder::mTrackInfo| and will outlive this
+ // pointer.
+ const TrackInfo* mInfo;
+ OMX_U32 mStartPortNumber;
+};
+
+} // namespace mozilla
+
+#endif // OmxPlatformLayer_h_
diff --git a/dom/media/platforms/omx/OmxPromiseLayer.cpp b/dom/media/platforms/omx/OmxPromiseLayer.cpp
new file mode 100644
index 0000000000..74d1fe15f8
--- /dev/null
+++ b/dom/media/platforms/omx/OmxPromiseLayer.cpp
@@ -0,0 +1,355 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "OmxPromiseLayer.h"
+
+#include "ImageContainer.h"
+
+#include "OmxDataDecoder.h"
+#include "OmxPlatformLayer.h"
+
+#ifdef LOG
+# undef LOG
+#endif
+
+#define LOG(arg, ...) \
+ MOZ_LOG(sPDMLog, mozilla::LogLevel::Debug, \
+ ("OmxPromiseLayer(%p)::%s: " arg, this, __func__, ##__VA_ARGS__))
+
+namespace mozilla {
+
+OmxPromiseLayer::OmxPromiseLayer(TaskQueue* aTaskQueue,
+ OmxDataDecoder* aDataDecoder,
+ layers::ImageContainer* aImageContainer)
+ : mTaskQueue(aTaskQueue) {
+ mPlatformLayer.reset(OmxPlatformLayer::Create(aDataDecoder, this, aTaskQueue,
+ aImageContainer));
+ MOZ_ASSERT(!!mPlatformLayer);
+}
+
+RefPtr<OmxPromiseLayer::OmxCommandPromise> OmxPromiseLayer::Init(
+ const TrackInfo* aInfo) {
+ MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
+
+ OMX_ERRORTYPE err = mPlatformLayer->InitOmxToStateLoaded(aInfo);
+ if (err != OMX_ErrorNone) {
+ OmxCommandFailureHolder failure(OMX_ErrorUndefined, OMX_CommandStateSet);
+ return OmxCommandPromise::CreateAndReject(failure, __func__);
+ }
+
+ OMX_STATETYPE state = GetState();
+ if (state == OMX_StateLoaded) {
+ return OmxCommandPromise::CreateAndResolve(OMX_CommandStateSet, __func__);
+ }
+ if (state == OMX_StateIdle) {
+ return SendCommand(OMX_CommandStateSet, OMX_StateIdle, nullptr);
+ }
+
+ OmxCommandFailureHolder failure(OMX_ErrorUndefined, OMX_CommandStateSet);
+ return OmxCommandPromise::CreateAndReject(failure, __func__);
+}
+
+OMX_ERRORTYPE
+OmxPromiseLayer::Config() {
+ MOZ_ASSERT(GetState() == OMX_StateLoaded);
+
+ return mPlatformLayer->Config();
+}
+
+RefPtr<OmxPromiseLayer::OmxBufferPromise> OmxPromiseLayer::FillBuffer(
+ BufferData* aData) {
+ MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
+ LOG("buffer %p", aData->mBuffer);
+
+ RefPtr<OmxBufferPromise> p = aData->mPromise.Ensure(__func__);
+
+ OMX_ERRORTYPE err = mPlatformLayer->FillThisBuffer(aData);
+
+ if (err != OMX_ErrorNone) {
+ OmxBufferFailureHolder failure(err, aData);
+ aData->mPromise.Reject(std::move(failure), __func__);
+ } else {
+ aData->mStatus = BufferData::BufferStatus::OMX_COMPONENT;
+ GetBufferHolders(OMX_DirOutput)->AppendElement(aData);
+ }
+
+ return p;
+}
+
+RefPtr<OmxPromiseLayer::OmxBufferPromise> OmxPromiseLayer::EmptyBuffer(
+ BufferData* aData) {
+ MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
+ LOG("buffer %p, size %lu", aData->mBuffer, aData->mBuffer->nFilledLen);
+
+ RefPtr<OmxBufferPromise> p = aData->mPromise.Ensure(__func__);
+
+ OMX_ERRORTYPE err = mPlatformLayer->EmptyThisBuffer(aData);
+
+ if (err != OMX_ErrorNone) {
+ OmxBufferFailureHolder failure(err, aData);
+ aData->mPromise.Reject(std::move(failure), __func__);
+ } else {
+ if (aData->mRawData) {
+ mRawDatas.AppendElement(std::move(aData->mRawData));
+ }
+ aData->mStatus = BufferData::BufferStatus::OMX_COMPONENT;
+ GetBufferHolders(OMX_DirInput)->AppendElement(aData);
+ }
+
+ return p;
+}
+
+OmxPromiseLayer::BUFFERLIST* OmxPromiseLayer::GetBufferHolders(
+ OMX_DIRTYPE aType) {
+ MOZ_ASSERT(aType == OMX_DirInput || aType == OMX_DirOutput);
+
+ if (aType == OMX_DirInput) {
+ return &mInbufferHolders;
+ }
+
+ return &mOutbufferHolders;
+}
+
+already_AddRefed<MediaRawData> OmxPromiseLayer::FindAndRemoveRawData(
+ OMX_TICKS aTimecode) {
+ for (auto raw : mRawDatas) {
+ if (raw->mTime.ToMicroseconds() == aTimecode) {
+ mRawDatas.RemoveElement(raw);
+ return raw.forget();
+ }
+ }
+ return nullptr;
+}
+
+already_AddRefed<BufferData> OmxPromiseLayer::FindAndRemoveBufferHolder(
+ OMX_DIRTYPE aType, BufferData::BufferID aId) {
+ MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
+
+ RefPtr<BufferData> holder;
+ BUFFERLIST* holders = GetBufferHolders(aType);
+
+ for (uint32_t i = 0; i < holders->Length(); i++) {
+ if (holders->ElementAt(i)->ID() == aId) {
+ holder = holders->ElementAt(i);
+ holders->RemoveElementAt(i);
+ return holder.forget();
+ }
+ }
+
+ return nullptr;
+}
+
+already_AddRefed<BufferData> OmxPromiseLayer::FindBufferById(
+ OMX_DIRTYPE aType, BufferData::BufferID aId) {
+ MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
+
+ RefPtr<BufferData> holder;
+ BUFFERLIST* holders = GetBufferHolders(aType);
+
+ for (uint32_t i = 0; i < holders->Length(); i++) {
+ if (holders->ElementAt(i)->ID() == aId) {
+ holder = holders->ElementAt(i);
+ return holder.forget();
+ }
+ }
+
+ return nullptr;
+}
+
+void OmxPromiseLayer::EmptyFillBufferDone(OMX_DIRTYPE aType,
+ BufferData* aData) {
+ if (aData) {
+ LOG("type %d, buffer %p", aType, aData->mBuffer);
+ if (aType == OMX_DirOutput) {
+ aData->mRawData = nullptr;
+ aData->mRawData = FindAndRemoveRawData(aData->mBuffer->nTimeStamp);
+ }
+ aData->mStatus = BufferData::BufferStatus::OMX_CLIENT;
+ aData->mPromise.Resolve(aData, __func__);
+ } else {
+ LOG("type %d, no buffer", aType);
+ }
+}
+
+void OmxPromiseLayer::EmptyFillBufferDone(OMX_DIRTYPE aType,
+ BufferData::BufferID aID) {
+ RefPtr<BufferData> holder = FindAndRemoveBufferHolder(aType, aID);
+ EmptyFillBufferDone(aType, holder);
+}
+
+RefPtr<OmxPromiseLayer::OmxCommandPromise> OmxPromiseLayer::SendCommand(
+ OMX_COMMANDTYPE aCmd, OMX_U32 aParam1, OMX_PTR aCmdData) {
+ if (aCmd == OMX_CommandFlush) {
+ // It doesn't support another flush commands before previous one is
+ // completed.
+ MOZ_RELEASE_ASSERT(!mFlushCommands.Length());
+
+ // Some coomponents don't send event with OMX_ALL, they send flush complete
+ // event with input port and another event for output port.
+ // In prupose of better compatibility, we interpret the OMX_ALL to
+ // OMX_DirInput and OMX_DirOutput flush separately.
+ OMX_DIRTYPE types[] = {OMX_DIRTYPE::OMX_DirInput,
+ OMX_DIRTYPE::OMX_DirOutput};
+ for (const auto type : types) {
+ if ((aParam1 == type) || (aParam1 == OMX_ALL)) {
+ mFlushCommands.AppendElement(FlushCommand({type, aCmdData}));
+ }
+
+ if (type == OMX_DirInput) {
+ // Clear all buffered raw data.
+ mRawDatas.Clear();
+ }
+ }
+
+ // Don't overlay more than one flush command, some components can't overlay
+ // flush commands. So here we send another flush after receiving the
+ // previous flush completed event.
+ if (mFlushCommands.Length()) {
+ OMX_ERRORTYPE err = mPlatformLayer->SendCommand(
+ OMX_CommandFlush, mFlushCommands.ElementAt(0).type,
+ mFlushCommands.ElementAt(0).cmd);
+ if (err != OMX_ErrorNone) {
+ OmxCommandFailureHolder failure(OMX_ErrorNotReady, OMX_CommandFlush);
+ return OmxCommandPromise::CreateAndReject(failure, __func__);
+ }
+ } else {
+ LOG("OMX_CommandFlush parameter error");
+ OmxCommandFailureHolder failure(OMX_ErrorNotReady, OMX_CommandFlush);
+ return OmxCommandPromise::CreateAndReject(failure, __func__);
+ }
+ } else {
+ OMX_ERRORTYPE err = mPlatformLayer->SendCommand(aCmd, aParam1, aCmdData);
+ if (err != OMX_ErrorNone) {
+ OmxCommandFailureHolder failure(OMX_ErrorNotReady, aCmd);
+ return OmxCommandPromise::CreateAndReject(failure, __func__);
+ }
+ }
+
+ RefPtr<OmxCommandPromise> p;
+ if (aCmd == OMX_CommandStateSet) {
+ p = mCommandStatePromise.Ensure(__func__);
+ } else if (aCmd == OMX_CommandFlush) {
+ p = mFlushPromise.Ensure(__func__);
+ } else if (aCmd == OMX_CommandPortEnable) {
+ p = mPortEnablePromise.Ensure(__func__);
+ } else if (aCmd == OMX_CommandPortDisable) {
+ p = mPortDisablePromise.Ensure(__func__);
+ } else {
+ LOG("error unsupport command");
+ MOZ_ASSERT(0);
+ }
+
+ return p;
+}
+
+bool OmxPromiseLayer::Event(OMX_EVENTTYPE aEvent, OMX_U32 aData1,
+ OMX_U32 aData2) {
+ OMX_COMMANDTYPE cmd = (OMX_COMMANDTYPE)aData1;
+ switch (aEvent) {
+ case OMX_EventCmdComplete: {
+ if (cmd == OMX_CommandStateSet) {
+ mCommandStatePromise.Resolve(OMX_CommandStateSet, __func__);
+ } else if (cmd == OMX_CommandFlush) {
+ MOZ_RELEASE_ASSERT(mFlushCommands.ElementAt(0).type == aData2);
+ LOG("OMX_CommandFlush completed port type %lu", aData2);
+ mFlushCommands.RemoveElementAt(0);
+
+ // Sending next flush command.
+ if (mFlushCommands.Length()) {
+ OMX_ERRORTYPE err = mPlatformLayer->SendCommand(
+ OMX_CommandFlush, mFlushCommands.ElementAt(0).type,
+ mFlushCommands.ElementAt(0).cmd);
+ if (err != OMX_ErrorNone) {
+ OmxCommandFailureHolder failure(OMX_ErrorNotReady,
+ OMX_CommandFlush);
+ mFlushPromise.Reject(failure, __func__);
+ }
+ } else {
+ mFlushPromise.Resolve(OMX_CommandFlush, __func__);
+ }
+ } else if (cmd == OMX_CommandPortDisable) {
+ mPortDisablePromise.Resolve(OMX_CommandPortDisable, __func__);
+ } else if (cmd == OMX_CommandPortEnable) {
+ mPortEnablePromise.Resolve(OMX_CommandPortEnable, __func__);
+ }
+ break;
+ }
+ case OMX_EventError: {
+ if (cmd == OMX_CommandStateSet) {
+ OmxCommandFailureHolder failure(OMX_ErrorUndefined,
+ OMX_CommandStateSet);
+ mCommandStatePromise.Reject(failure, __func__);
+ } else if (cmd == OMX_CommandFlush) {
+ OmxCommandFailureHolder failure(OMX_ErrorUndefined, OMX_CommandFlush);
+ mFlushPromise.Reject(failure, __func__);
+ } else if (cmd == OMX_CommandPortDisable) {
+ OmxCommandFailureHolder failure(OMX_ErrorUndefined,
+ OMX_CommandPortDisable);
+ mPortDisablePromise.Reject(failure, __func__);
+ } else if (cmd == OMX_CommandPortEnable) {
+ OmxCommandFailureHolder failure(OMX_ErrorUndefined,
+ OMX_CommandPortEnable);
+ mPortEnablePromise.Reject(failure, __func__);
+ } else {
+ return false;
+ }
+ break;
+ }
+ default: {
+ return false;
+ }
+ }
+ return true;
+}
+
+nsresult OmxPromiseLayer::AllocateOmxBuffer(OMX_DIRTYPE aType,
+ BUFFERLIST* aBuffers) {
+ return mPlatformLayer->AllocateOmxBuffer(aType, aBuffers);
+}
+
+nsresult OmxPromiseLayer::ReleaseOmxBuffer(OMX_DIRTYPE aType,
+ BUFFERLIST* aBuffers) {
+ return mPlatformLayer->ReleaseOmxBuffer(aType, aBuffers);
+}
+
+OMX_STATETYPE
+OmxPromiseLayer::GetState() {
+ OMX_STATETYPE state;
+ OMX_ERRORTYPE err = mPlatformLayer->GetState(&state);
+ return err == OMX_ErrorNone ? state : OMX_StateInvalid;
+}
+
+OMX_ERRORTYPE
+OmxPromiseLayer::GetParameter(OMX_INDEXTYPE aParamIndex,
+ OMX_PTR aComponentParameterStructure,
+ OMX_U32 aComponentParameterSize) {
+ return mPlatformLayer->GetParameter(aParamIndex, aComponentParameterStructure,
+ aComponentParameterSize);
+}
+
+OMX_ERRORTYPE
+OmxPromiseLayer::SetParameter(OMX_INDEXTYPE aParamIndex,
+ OMX_PTR aComponentParameterStructure,
+ OMX_U32 aComponentParameterSize) {
+ return mPlatformLayer->SetParameter(aParamIndex, aComponentParameterStructure,
+ aComponentParameterSize);
+}
+
+OMX_U32
+OmxPromiseLayer::InputPortIndex() { return mPlatformLayer->InputPortIndex(); }
+
+OMX_U32
+OmxPromiseLayer::OutputPortIndex() { return mPlatformLayer->OutputPortIndex(); }
+
+nsresult OmxPromiseLayer::Shutdown() {
+ LOG("");
+ MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
+ MOZ_ASSERT(!GetBufferHolders(OMX_DirInput)->Length());
+ MOZ_ASSERT(!GetBufferHolders(OMX_DirOutput)->Length());
+ return mPlatformLayer->Shutdown();
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/omx/OmxPromiseLayer.h b/dom/media/platforms/omx/OmxPromiseLayer.h
new file mode 100644
index 0000000000..82a1b3c268
--- /dev/null
+++ b/dom/media/platforms/omx/OmxPromiseLayer.h
@@ -0,0 +1,243 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(OmxPromiseLayer_h_)
+# define OmxPromiseLayer_h_
+
+# include "mozilla/MozPromise.h"
+# include "mozilla/TaskQueue.h"
+
+# include "OMX_Core.h"
+# include "OMX_Types.h"
+
+namespace mozilla {
+
+namespace layers {
+class ImageContainer;
+}
+
+class MediaData;
+class MediaRawData;
+class OmxDataDecoder;
+class OmxPlatformLayer;
+class TrackInfo;
+
+/* This class acts as a middle layer between OmxDataDecoder and the underlying
+ * OmxPlatformLayer.
+ *
+ * This class has two purposes:
+ * 1. Using promise instead of OpenMax async callback function.
+ * For example, OmxCommandPromise is used for OpenMax IL SendCommand.
+ * 2. Manage the buffer exchanged between client and component.
+ * Because omx buffer works crossing threads, so each omx buffer has its own
+ * promise, it is defined in BufferData.
+ *
+ * All of functions and members in this class should be run in the same
+ * TaskQueue.
+ */
+class OmxPromiseLayer {
+ protected:
+ virtual ~OmxPromiseLayer() = default;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(OmxPromiseLayer)
+
+ OmxPromiseLayer(TaskQueue* aTaskQueue, OmxDataDecoder* aDataDecoder,
+ layers::ImageContainer* aImageContainer);
+
+ class BufferData;
+
+ typedef nsTArray<RefPtr<BufferData>> BUFFERLIST;
+
+ class OmxBufferFailureHolder {
+ public:
+ OmxBufferFailureHolder(OMX_ERRORTYPE aError, BufferData* aBuffer)
+ : mError(aError), mBuffer(aBuffer) {}
+
+ OMX_ERRORTYPE mError;
+ BufferData* mBuffer;
+ };
+
+ typedef MozPromise<BufferData*, OmxBufferFailureHolder,
+ /* IsExclusive = */ false>
+ OmxBufferPromise;
+
+ class OmxCommandFailureHolder {
+ public:
+ OmxCommandFailureHolder(OMX_ERRORTYPE aErrorType,
+ OMX_COMMANDTYPE aCommandType)
+ : mErrorType(aErrorType), mCommandType(aCommandType) {}
+
+ OMX_ERRORTYPE mErrorType;
+ OMX_COMMANDTYPE mCommandType;
+ };
+
+ typedef MozPromise<OMX_COMMANDTYPE, OmxCommandFailureHolder,
+ /* IsExclusive = */ true>
+ OmxCommandPromise;
+
+ typedef MozPromise<uint32_t, bool, /* IsExclusive = */ true>
+ OmxPortConfigPromise;
+
+ // TODO: maybe a generic promise is good enough for this case?
+ RefPtr<OmxCommandPromise> Init(const TrackInfo* aInfo);
+
+ OMX_ERRORTYPE Config();
+
+ RefPtr<OmxBufferPromise> FillBuffer(BufferData* aData);
+
+ RefPtr<OmxBufferPromise> EmptyBuffer(BufferData* aData);
+
+ RefPtr<OmxCommandPromise> SendCommand(OMX_COMMANDTYPE aCmd, OMX_U32 aParam1,
+ OMX_PTR aCmdData);
+
+ nsresult AllocateOmxBuffer(OMX_DIRTYPE aType, BUFFERLIST* aBuffers);
+
+ nsresult ReleaseOmxBuffer(OMX_DIRTYPE aType, BUFFERLIST* aBuffers);
+
+ OMX_STATETYPE GetState();
+
+ OMX_ERRORTYPE GetParameter(OMX_INDEXTYPE aParamIndex,
+ OMX_PTR aComponentParameterStructure,
+ OMX_U32 aComponentParameterSize);
+
+ OMX_ERRORTYPE SetParameter(OMX_INDEXTYPE nIndex,
+ OMX_PTR aComponentParameterStructure,
+ OMX_U32 aComponentParameterSize);
+
+ OMX_U32 InputPortIndex();
+
+ OMX_U32 OutputPortIndex();
+
+ nsresult Shutdown();
+
+ // BufferData maintains the status of OMX buffer (OMX_BUFFERHEADERTYPE).
+ // mStatus tracks the buffer owner.
+ // And a promise because OMX buffer working among different threads.
+ class BufferData {
+ protected:
+ virtual ~BufferData() = default;
+
+ public:
+ explicit BufferData(OMX_BUFFERHEADERTYPE* aBuffer)
+ : mEos(false), mStatus(BufferStatus::FREE), mBuffer(aBuffer) {}
+
+ typedef void* BufferID;
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(BufferData)
+
+ // In most cases, the ID of this buffer is the pointer address of mBuffer.
+ // However, on some platforms it may be another value.
+ virtual BufferID ID() { return mBuffer; }
+
+ // Return the platform dependent MediaData().
+ // For example, it returns the MediaData with Gralloc texture.
+ // If it returns nullptr, then caller uses the normal way to
+ // create MediaData().
+ virtual already_AddRefed<MediaData> GetPlatformMediaData() {
+ return nullptr;
+ }
+
+ // The buffer could be used by several objects. And only one object owns the
+ // buffer the same time.
+ // FREE:
+ // nobody uses it.
+ //
+ // OMX_COMPONENT:
+ // buffer is used by OMX component (OmxPlatformLayer).
+ //
+ // OMX_CLIENT:
+ // buffer is used by client which is wait for audio/video playing
+ // (OmxDataDecoder)
+ //
+ // OMX_CLIENT_OUTPUT:
+ // used by client to output decoded data (for example, Gecko layer in
+ // this case)
+ //
+ // For output port buffer, the status transition is:
+ // FREE -> OMX_COMPONENT -> OMX_CLIENT -> OMX_CLIENT_OUTPUT -> FREE
+ //
+ // For input port buffer, the status transition is:
+ // FREE -> OMX_COMPONENT -> OMX_CLIENT -> FREE
+ //
+ enum BufferStatus {
+ FREE,
+ OMX_COMPONENT,
+ OMX_CLIENT,
+ OMX_CLIENT_OUTPUT,
+ INVALID
+ };
+
+ bool mEos;
+
+ // The raw keeps in OmxPromiseLayer after EmptyBuffer and then passing to
+ // output decoded buffer in EmptyFillBufferDone. It is used to keep the
+ // records of the original data from demuxer, like duration, stream
+ // offset...etc.
+ RefPtr<MediaRawData> mRawData;
+
+ // Because OMX buffer works across threads, so it uses a promise
+ // for each buffer when the buffer is used by Omx component.
+ MozPromiseHolder<OmxBufferPromise> mPromise;
+ BufferStatus mStatus;
+ OMX_BUFFERHEADERTYPE* mBuffer;
+ };
+
+ void EmptyFillBufferDone(OMX_DIRTYPE aType, BufferData::BufferID aID);
+
+ void EmptyFillBufferDone(OMX_DIRTYPE aType, BufferData* aData);
+
+ already_AddRefed<BufferData> FindBufferById(OMX_DIRTYPE aType,
+ BufferData::BufferID aId);
+
+ already_AddRefed<BufferData> FindAndRemoveBufferHolder(
+ OMX_DIRTYPE aType, BufferData::BufferID aId);
+
+ // Return true if event is handled.
+ bool Event(OMX_EVENTTYPE aEvent, OMX_U32 aData1, OMX_U32 aData2);
+
+ protected:
+ struct FlushCommand {
+ OMX_DIRTYPE type;
+ OMX_PTR cmd;
+ };
+
+ BUFFERLIST* GetBufferHolders(OMX_DIRTYPE aType);
+
+ already_AddRefed<MediaRawData> FindAndRemoveRawData(OMX_TICKS aTimecode);
+
+ RefPtr<TaskQueue> mTaskQueue;
+
+ MozPromiseHolder<OmxCommandPromise> mCommandStatePromise;
+
+ MozPromiseHolder<OmxCommandPromise> mPortDisablePromise;
+
+ MozPromiseHolder<OmxCommandPromise> mPortEnablePromise;
+
+ MozPromiseHolder<OmxCommandPromise> mFlushPromise;
+
+ nsTArray<FlushCommand> mFlushCommands;
+
+ UniquePtr<OmxPlatformLayer> mPlatformLayer;
+
+ private:
+ // Elements are added to holders when FillBuffer() or FillBuffer(). And
+ // removing element when the promise is resolved. Buffers in these lists
+ // should NOT be used by other component; for example, output it to audio
+ // output. These lists should be empty when engine is about to shutdown.
+ //
+ // Note:
+ // There bufferlist should not be used by other class directly.
+ BUFFERLIST mInbufferHolders;
+
+ BUFFERLIST mOutbufferHolders;
+
+ nsTArray<RefPtr<MediaRawData>> mRawDatas;
+};
+
+} // namespace mozilla
+
+#endif /* OmxPromiseLayer_h_ */
diff --git a/dom/media/platforms/omx/PureOmxPlatformLayer.cpp b/dom/media/platforms/omx/PureOmxPlatformLayer.cpp
new file mode 100644
index 0000000000..dd6b259ac6
--- /dev/null
+++ b/dom/media/platforms/omx/PureOmxPlatformLayer.cpp
@@ -0,0 +1,405 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "OmxDataDecoder.h"
+#include "OmxPromiseLayer.h"
+#include "PureOmxPlatformLayer.h"
+#include "OmxCoreLibLinker.h"
+
+#ifdef LOG
+# undef LOG
+#endif
+
+#define LOG(arg, ...) \
+ MOZ_LOG( \
+ sPDMLog, mozilla::LogLevel::Debug, \
+ ("PureOmxPlatformLayer(%p)::%s: " arg, this, __func__, ##__VA_ARGS__))
+#define LOG_BUF(arg, ...) \
+ MOZ_LOG(sPDMLog, mozilla::LogLevel::Debug, \
+ ("PureOmxBufferData(%p)::%s: " arg, this, __func__, ##__VA_ARGS__))
+
+namespace mozilla {
+
+#define OMX_FUNC(func) extern typeof(func)* func;
+#include "OmxFunctionList.h"
+#undef OMX_FUNC
+
+PureOmxBufferData::PureOmxBufferData(
+ const PureOmxPlatformLayer& aPlatformLayer,
+ const OMX_PARAM_PORTDEFINITIONTYPE& aPortDef)
+ : BufferData(nullptr), mPlatformLayer(aPlatformLayer), mPortDef(aPortDef) {
+ LOG_BUF("");
+
+ if (ShouldUseEGLImage()) {
+ // TODO
+ LOG_BUF(
+ "OMX_UseEGLImage() seems available but using it isn't implemented "
+ "yet.");
+ }
+
+ OMX_ERRORTYPE err;
+ err = OMX_AllocateBuffer(mPlatformLayer.GetComponent(), &mBuffer,
+ mPortDef.nPortIndex, this, mPortDef.nBufferSize);
+ if (err != OMX_ErrorNone) {
+ LOG_BUF("Failed to allocate the buffer!: 0x%08x", err);
+ }
+}
+
+PureOmxBufferData::~PureOmxBufferData() {
+ LOG_BUF("");
+ ReleaseBuffer();
+}
+
+void PureOmxBufferData::ReleaseBuffer() {
+ LOG_BUF("");
+
+ if (mBuffer) {
+ OMX_ERRORTYPE err;
+ err = OMX_FreeBuffer(mPlatformLayer.GetComponent(), mPortDef.nPortIndex,
+ mBuffer);
+ if (err != OMX_ErrorNone) {
+ LOG_BUF("Failed to free the buffer!: 0x%08x", err);
+ }
+ mBuffer = nullptr;
+ }
+}
+
+bool PureOmxBufferData::ShouldUseEGLImage() {
+ OMX_ERRORTYPE err;
+ err = OMX_UseEGLImage(mPlatformLayer.GetComponent(), nullptr,
+ mPortDef.nPortIndex, nullptr, nullptr);
+ return (err != OMX_ErrorNotImplemented);
+}
+
+/* static */
+bool PureOmxPlatformLayer::Init(void) {
+ if (!OmxCoreLibLinker::Link()) {
+ return false;
+ }
+
+ OMX_ERRORTYPE err = OMX_Init();
+ if (err != OMX_ErrorNone) {
+ MOZ_LOG(sPDMLog, mozilla::LogLevel::Debug,
+ ("PureOmxPlatformLayer::%s: Failed to initialize OMXCore: 0x%08x",
+ __func__, err));
+ return false;
+ }
+
+ return true;
+}
+
+/* static */
+OMX_CALLBACKTYPE PureOmxPlatformLayer::sCallbacks = {
+ EventHandler, EmptyBufferDone, FillBufferDone};
+
+PureOmxPlatformLayer::PureOmxPlatformLayer(
+ OmxDataDecoder* aDataDecoder, OmxPromiseLayer* aPromiseLayer,
+ TaskQueue* aTaskQueue, layers::ImageContainer* aImageContainer)
+ : mComponent(nullptr),
+ mDataDecoder(aDataDecoder),
+ mPromiseLayer(aPromiseLayer),
+ mTaskQueue(aTaskQueue),
+ mImageContainer(aImageContainer) {
+ LOG("");
+}
+
+PureOmxPlatformLayer::~PureOmxPlatformLayer() { LOG(""); }
+
+OMX_ERRORTYPE
+PureOmxPlatformLayer::InitOmxToStateLoaded(const TrackInfo* aInfo) {
+ LOG("");
+
+ if (!aInfo) {
+ return OMX_ErrorUndefined;
+ }
+ mInfo = aInfo;
+
+ return CreateComponent();
+}
+
+OMX_ERRORTYPE
+PureOmxPlatformLayer::EmptyThisBuffer(BufferData* aData) {
+ LOG("");
+ return OMX_EmptyThisBuffer(mComponent, aData->mBuffer);
+}
+
+OMX_ERRORTYPE
+PureOmxPlatformLayer::FillThisBuffer(BufferData* aData) {
+ LOG("");
+ return OMX_FillThisBuffer(mComponent, aData->mBuffer);
+}
+
+OMX_ERRORTYPE
+PureOmxPlatformLayer::SendCommand(OMX_COMMANDTYPE aCmd, OMX_U32 aParam1,
+ OMX_PTR aCmdData) {
+ LOG("aCmd: 0x%08x", aCmd);
+ if (!mComponent) {
+ return OMX_ErrorUndefined;
+ }
+ return OMX_SendCommand(mComponent, aCmd, aParam1, aCmdData);
+}
+
+nsresult PureOmxPlatformLayer::FindPortDefinition(
+ OMX_DIRTYPE aType, OMX_PARAM_PORTDEFINITIONTYPE& portDef) {
+ nsTArray<uint32_t> portIndex;
+ GetPortIndices(portIndex);
+ for (auto idx : portIndex) {
+ InitOmxParameter(&portDef);
+ portDef.nPortIndex = idx;
+
+ OMX_ERRORTYPE err;
+ err = GetParameter(OMX_IndexParamPortDefinition, &portDef,
+ sizeof(OMX_PARAM_PORTDEFINITIONTYPE));
+ if (err != OMX_ErrorNone) {
+ return NS_ERROR_FAILURE;
+ } else if (portDef.eDir == aType) {
+ LOG("Found OMX_IndexParamPortDefinition: port: %d, type: %d",
+ portDef.nPortIndex, portDef.eDir);
+ return NS_OK;
+ }
+ }
+ return NS_ERROR_FAILURE;
+}
+
+nsresult PureOmxPlatformLayer::AllocateOmxBuffer(OMX_DIRTYPE aType,
+ BUFFERLIST* aBufferList) {
+ LOG("aType: %d", aType);
+
+ OMX_PARAM_PORTDEFINITIONTYPE portDef;
+ nsresult result = FindPortDefinition(aType, portDef);
+ if (result != NS_OK) {
+ return result;
+ }
+
+ LOG("nBufferCountActual: %d, nBufferSize: %d", portDef.nBufferCountActual,
+ portDef.nBufferSize);
+
+ for (OMX_U32 i = 0; i < portDef.nBufferCountActual; ++i) {
+ RefPtr<PureOmxBufferData> buffer = new PureOmxBufferData(*this, portDef);
+ aBufferList->AppendElement(buffer);
+ }
+
+ return NS_OK;
+}
+
+nsresult PureOmxPlatformLayer::ReleaseOmxBuffer(OMX_DIRTYPE aType,
+ BUFFERLIST* aBufferList) {
+ LOG("aType: 0x%08x", aType);
+
+ uint32_t len = aBufferList->Length();
+ for (uint32_t i = 0; i < len; i++) {
+ PureOmxBufferData* buffer =
+ static_cast<PureOmxBufferData*>(aBufferList->ElementAt(i).get());
+
+ // All raw OpenMAX buffers have to be released here to flush
+ // OMX_CommandStateSet for switching the state to OMX_StateLoaded.
+ // See OmxDataDecoder::DoAsyncShutdown() for more detail.
+ buffer->ReleaseBuffer();
+ }
+ aBufferList->Clear();
+
+ return NS_OK;
+}
+
+OMX_ERRORTYPE
+PureOmxPlatformLayer::GetState(OMX_STATETYPE* aType) {
+ LOG("");
+
+ if (mComponent) {
+ return OMX_GetState(mComponent, aType);
+ }
+
+ return OMX_ErrorUndefined;
+}
+
+OMX_ERRORTYPE
+PureOmxPlatformLayer::GetParameter(OMX_INDEXTYPE aParamIndex,
+ OMX_PTR aComponentParameterStructure,
+ OMX_U32 aComponentParameterSize) {
+ LOG("aParamIndex: 0x%08x", aParamIndex);
+
+ if (!mComponent) {
+ return OMX_ErrorUndefined;
+ }
+
+ return OMX_GetParameter(mComponent, aParamIndex,
+ aComponentParameterStructure);
+}
+
+OMX_ERRORTYPE
+PureOmxPlatformLayer::SetParameter(OMX_INDEXTYPE aParamIndex,
+ OMX_PTR aComponentParameterStructure,
+ OMX_U32 aComponentParameterSize) {
+ LOG("aParamIndex: 0x%08x", aParamIndex);
+
+ if (!mComponent) {
+ return OMX_ErrorUndefined;
+ }
+
+ return OMX_SetParameter(mComponent, aParamIndex,
+ aComponentParameterStructure);
+}
+
+nsresult PureOmxPlatformLayer::Shutdown() {
+ LOG("");
+ if (mComponent) {
+ OMX_FreeHandle(mComponent);
+ mComponent = nullptr;
+ }
+ mPromiseLayer = nullptr;
+ mDataDecoder = nullptr;
+ return NS_OK;
+}
+
+/* static */
+OMX_ERRORTYPE PureOmxPlatformLayer::EventHandler(OMX_HANDLETYPE hComponent,
+ OMX_PTR pAppData,
+ OMX_EVENTTYPE eEventType,
+ OMX_U32 nData1, OMX_U32 nData2,
+ OMX_PTR pEventData) {
+ PureOmxPlatformLayer* self = static_cast<PureOmxPlatformLayer*>(pAppData);
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ "mozilla::PureOmxPlatformLayer::EventHandler",
+ [self, eEventType, nData1, nData2, pEventData]() {
+ self->EventHandler(eEventType, nData1, nData2, pEventData);
+ });
+ nsresult rv = self->mTaskQueue->Dispatch(r.forget());
+ return NS_SUCCEEDED(rv) ? OMX_ErrorNone : OMX_ErrorUndefined;
+}
+
+/* static */
+OMX_ERRORTYPE PureOmxPlatformLayer::EmptyBufferDone(
+ OMX_HANDLETYPE hComponent, OMX_IN OMX_PTR pAppData,
+ OMX_IN OMX_BUFFERHEADERTYPE* pBuffer) {
+ PureOmxPlatformLayer* self = static_cast<PureOmxPlatformLayer*>(pAppData);
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ "mozilla::PureOmxPlatformLayer::EmptyBufferDone",
+ [self, pBuffer]() { self->EmptyBufferDone(pBuffer); });
+ nsresult rv = self->mTaskQueue->Dispatch(r.forget());
+ return NS_SUCCEEDED(rv) ? OMX_ErrorNone : OMX_ErrorUndefined;
+}
+
+/* static */
+OMX_ERRORTYPE PureOmxPlatformLayer::FillBufferDone(
+ OMX_OUT OMX_HANDLETYPE hComponent, OMX_OUT OMX_PTR pAppData,
+ OMX_OUT OMX_BUFFERHEADERTYPE* pBuffer) {
+ PureOmxPlatformLayer* self = static_cast<PureOmxPlatformLayer*>(pAppData);
+ nsCOMPtr<nsIRunnable> r = NS_NewRunnableFunction(
+ "mozilla::PureOmxPlatformLayer::FillBufferDone",
+ [self, pBuffer]() { self->FillBufferDone(pBuffer); });
+ nsresult rv = self->mTaskQueue->Dispatch(r.forget());
+ return NS_SUCCEEDED(rv) ? OMX_ErrorNone : OMX_ErrorUndefined;
+}
+
+OMX_ERRORTYPE
+PureOmxPlatformLayer::EventHandler(OMX_EVENTTYPE eEventType, OMX_U32 nData1,
+ OMX_U32 nData2, OMX_PTR pEventData) {
+ bool handled = mPromiseLayer->Event(eEventType, nData1, nData2);
+ LOG("eEventType: 0x%08x, handled: %d", eEventType, handled);
+ return OMX_ErrorNone;
+}
+
+OMX_ERRORTYPE
+PureOmxPlatformLayer::EmptyBufferDone(OMX_IN OMX_BUFFERHEADERTYPE* pBuffer) {
+ PureOmxBufferData* buffer =
+ static_cast<PureOmxBufferData*>(pBuffer->pAppPrivate);
+ OMX_DIRTYPE portDirection = buffer->GetPortDirection();
+ LOG("PortDirection: %d", portDirection);
+ mPromiseLayer->EmptyFillBufferDone(portDirection, buffer);
+ return OMX_ErrorNone;
+}
+
+OMX_ERRORTYPE
+PureOmxPlatformLayer::FillBufferDone(OMX_OUT OMX_BUFFERHEADERTYPE* pBuffer) {
+ PureOmxBufferData* buffer =
+ static_cast<PureOmxBufferData*>(pBuffer->pAppPrivate);
+ OMX_DIRTYPE portDirection = buffer->GetPortDirection();
+ LOG("PortDirection: %d", portDirection);
+ mPromiseLayer->EmptyFillBufferDone(portDirection, buffer);
+ return OMX_ErrorNone;
+}
+
+bool PureOmxPlatformLayer::SupportsMimeType(const nsACString& aMimeType) {
+ return FindStandardComponent(aMimeType, nullptr);
+}
+
+static bool GetStandardComponentRole(const nsACString& aMimeType,
+ nsACString& aRole) {
+ if (aMimeType.EqualsLiteral("video/avc") ||
+ aMimeType.EqualsLiteral("video/mp4") ||
+ aMimeType.EqualsLiteral("video/mp4v-es")) {
+ aRole.Assign("video_decoder.avc");
+ return true;
+ } else if (aMimeType.EqualsLiteral("audio/mp4a-latm") ||
+ aMimeType.EqualsLiteral("audio/mp4") ||
+ aMimeType.EqualsLiteral("audio/aac")) {
+ aRole.Assign("audio_decoder.aac");
+ return true;
+ }
+ return false;
+}
+
+/* static */
+bool PureOmxPlatformLayer::FindStandardComponent(const nsACString& aMimeType,
+ nsACString* aComponentName) {
+ nsAutoCString role;
+ if (!GetStandardComponentRole(aMimeType, role)) return false;
+
+ OMX_U32 nComponents = 0;
+ OMX_ERRORTYPE err;
+ err = OMX_GetComponentsOfRole(const_cast<OMX_STRING>(role.Data()),
+ &nComponents, nullptr);
+ if (err != OMX_ErrorNone || nComponents <= 0) {
+ return false;
+ }
+ if (!aComponentName) {
+ return true;
+ }
+
+ // TODO:
+ // Only the first component will be used.
+ // We should detect the most preferred component.
+ OMX_U8* componentNames[1];
+ UniquePtr<OMX_U8[]> componentName;
+ componentName = MakeUniqueFallible<OMX_U8[]>(OMX_MAX_STRINGNAME_SIZE);
+ componentNames[0] = componentName.get();
+ nComponents = 1;
+ err = OMX_GetComponentsOfRole(const_cast<OMX_STRING>(role.Data()),
+ &nComponents, componentNames);
+ if (err == OMX_ErrorNone) {
+ MOZ_LOG(sPDMLog, mozilla::LogLevel::Debug,
+ ("PureOmxPlatformLayer::%s: A component has been found for %s: %s",
+ __func__, aMimeType.Data(), componentNames[0]));
+ aComponentName->Assign(reinterpret_cast<char*>(componentNames[0]));
+ }
+
+ return err == OMX_ErrorNone;
+}
+
+OMX_ERRORTYPE
+PureOmxPlatformLayer::CreateComponent(const nsACString* aComponentName) {
+ nsAutoCString componentName;
+ if (aComponentName) {
+ componentName = *aComponentName;
+ } else if (!FindStandardComponent(mInfo->mMimeType, &componentName)) {
+ return OMX_ErrorComponentNotFound;
+ }
+
+ OMX_ERRORTYPE err;
+ err = OMX_GetHandle(&mComponent, const_cast<OMX_STRING>(componentName.Data()),
+ this, &sCallbacks);
+
+ const char* mime = mInfo->mMimeType.Data();
+ if (err == OMX_ErrorNone) {
+ LOG("Succeeded to create the component for %s", mime);
+ } else {
+ LOG("Failed to create the component for %s: 0x%08x", mime, err);
+ }
+
+ return err;
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/omx/PureOmxPlatformLayer.h b/dom/media/platforms/omx/PureOmxPlatformLayer.h
new file mode 100644
index 0000000000..95c95b8781
--- /dev/null
+++ b/dom/media/platforms/omx/PureOmxPlatformLayer.h
@@ -0,0 +1,110 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(PureOmxPlatformLayer_h_)
+# define PureOmxPlatformLayer_h_
+
+# include "OmxPlatformLayer.h"
+
+namespace mozilla {
+
+class PureOmxPlatformLayer;
+
+class PureOmxBufferData : public OmxPromiseLayer::BufferData {
+ protected:
+ virtual ~PureOmxBufferData();
+
+ public:
+ PureOmxBufferData(const PureOmxPlatformLayer& aPlatformLayer,
+ const OMX_PARAM_PORTDEFINITIONTYPE& aPortDef);
+
+ void ReleaseBuffer();
+ OMX_DIRTYPE GetPortDirection() const { return mPortDef.eDir; };
+
+ protected:
+ bool ShouldUseEGLImage();
+
+ const PureOmxPlatformLayer& mPlatformLayer;
+ const OMX_PARAM_PORTDEFINITIONTYPE mPortDef;
+};
+
+class PureOmxPlatformLayer : public OmxPlatformLayer {
+ public:
+ static bool Init(void);
+
+ static bool SupportsMimeType(const nsACString& aMimeType);
+
+ PureOmxPlatformLayer(OmxDataDecoder* aDataDecoder,
+ OmxPromiseLayer* aPromiseLayer, TaskQueue* aTaskQueue,
+ layers::ImageContainer* aImageContainer);
+
+ virtual ~PureOmxPlatformLayer();
+
+ OMX_ERRORTYPE InitOmxToStateLoaded(const TrackInfo* aInfo) override;
+
+ OMX_ERRORTYPE EmptyThisBuffer(BufferData* aData) override;
+
+ OMX_ERRORTYPE FillThisBuffer(BufferData* aData) override;
+
+ OMX_ERRORTYPE SendCommand(OMX_COMMANDTYPE aCmd, OMX_U32 aParam1,
+ OMX_PTR aCmdData) override;
+
+ nsresult AllocateOmxBuffer(OMX_DIRTYPE aType,
+ BUFFERLIST* aBufferList) override;
+
+ nsresult ReleaseOmxBuffer(OMX_DIRTYPE aType,
+ BUFFERLIST* aBufferList) override;
+
+ OMX_ERRORTYPE GetState(OMX_STATETYPE* aType) override;
+
+ OMX_ERRORTYPE GetParameter(OMX_INDEXTYPE aParamIndex,
+ OMX_PTR aComponentParameterStructure,
+ OMX_U32 aComponentParameterSize) override;
+
+ OMX_ERRORTYPE SetParameter(OMX_INDEXTYPE aParamIndex,
+ OMX_PTR aComponentParameterStructure,
+ OMX_U32 aComponentParameterSize) override;
+
+ nsresult Shutdown() override;
+
+ OMX_HANDLETYPE GetComponent() const { return mComponent; };
+
+ static OMX_ERRORTYPE EventHandler(OMX_HANDLETYPE hComponent, OMX_PTR pAppData,
+ OMX_EVENTTYPE eEventType, OMX_U32 nData1,
+ OMX_U32 nData2, OMX_PTR pEventData);
+ static OMX_ERRORTYPE EmptyBufferDone(OMX_HANDLETYPE hComponent,
+ OMX_IN OMX_PTR pAppData,
+ OMX_IN OMX_BUFFERHEADERTYPE* pBuffer);
+ static OMX_ERRORTYPE FillBufferDone(OMX_OUT OMX_HANDLETYPE hComponent,
+ OMX_OUT OMX_PTR pAppData,
+ OMX_OUT OMX_BUFFERHEADERTYPE* pBuffer);
+
+ protected:
+ static bool FindStandardComponent(const nsACString& aMimeType,
+ nsACString* aComponentName);
+
+ OMX_ERRORTYPE CreateComponent(const nsACString* aComponentName = nullptr);
+ nsresult FindPortDefinition(OMX_DIRTYPE aType,
+ OMX_PARAM_PORTDEFINITIONTYPE& portDef);
+
+ OMX_ERRORTYPE EventHandler(OMX_EVENTTYPE eEventType, OMX_U32 nData1,
+ OMX_U32 nData2, OMX_PTR pEventData);
+ OMX_ERRORTYPE EmptyBufferDone(OMX_IN OMX_BUFFERHEADERTYPE* pBuffer);
+ OMX_ERRORTYPE FillBufferDone(OMX_OUT OMX_BUFFERHEADERTYPE* pBuffer);
+
+ protected:
+ static OMX_CALLBACKTYPE sCallbacks;
+
+ OMX_HANDLETYPE mComponent;
+ RefPtr<OmxDataDecoder> mDataDecoder;
+ RefPtr<OmxPromiseLayer> mPromiseLayer;
+ RefPtr<TaskQueue> mTaskQueue;
+ RefPtr<layers::ImageContainer> mImageContainer;
+};
+
+} // namespace mozilla
+
+#endif // PureOmxPlatformLayer_h_
diff --git a/dom/media/platforms/omx/moz.build b/dom/media/platforms/omx/moz.build
new file mode 100644
index 0000000000..c8a79cabb4
--- /dev/null
+++ b/dom/media/platforms/omx/moz.build
@@ -0,0 +1,36 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS += [
+ "OmxDecoderModule.h",
+]
+
+UNIFIED_SOURCES += [
+ "OmxDataDecoder.cpp",
+ "OmxDecoderModule.cpp",
+ "OmxPlatformLayer.cpp",
+ "OmxPromiseLayer.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/media/openmax_il/il112",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+if CONFIG["MOZ_OMX"]:
+ UNIFIED_SOURCES += [
+ "PureOmxPlatformLayer.cpp",
+ ]
+
+FINAL_LIBRARY = "xul"
+
+# Avoid warnings from third-party code that we can not modify.
+if CONFIG["CC_TYPE"] == "clang-cl":
+ CXXFLAGS += ["-Wno-invalid-source-encoding"]
+
+# Add libFuzzer configuration directives
+include("/tools/fuzzing/libfuzzer-config.mozbuild")
diff --git a/dom/media/platforms/wmf/DXVA2Manager.cpp b/dom/media/platforms/wmf/DXVA2Manager.cpp
new file mode 100644
index 0000000000..f080a16779
--- /dev/null
+++ b/dom/media/platforms/wmf/DXVA2Manager.cpp
@@ -0,0 +1,1512 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifdef MOZ_AV1
+# include "AOMDecoder.h"
+#endif
+#include "DXVA2Manager.h"
+#include <d3d11.h>
+#include "D3D9SurfaceImage.h"
+#include "DriverCrashGuard.h"
+#include "GfxDriverInfo.h"
+#include "ImageContainer.h"
+#include "MFTDecoder.h"
+#include "MediaTelemetryConstants.h"
+#include "PerformanceRecorder.h"
+#include "VideoUtils.h"
+#include "VPXDecoder.h"
+#include "WMFUtils.h"
+#include "gfxCrashReporterUtils.h"
+#include "gfxWindowsPlatform.h"
+#include "mfapi.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/gfx/DeviceManagerDx.h"
+#include "mozilla/layers/D3D11ShareHandleImage.h"
+#include "mozilla/layers/D3D11TextureIMFSampleImage.h"
+#include "mozilla/layers/ImageBridgeChild.h"
+#include "mozilla/layers/TextureD3D11.h"
+#include "mozilla/layers/TextureForwarder.h"
+#include "mozilla/mscom/EnsureMTA.h"
+#include "nsPrintfCString.h"
+#include "nsThreadUtils.h"
+
+const GUID MF_XVP_PLAYBACK_MODE = {
+ 0x3c5d293f,
+ 0xad67,
+ 0x4e29,
+ {0xaf, 0x12, 0xcf, 0x3e, 0x23, 0x8a, 0xcc, 0xe9}};
+
+DEFINE_GUID(MF_LOW_LATENCY, 0x9c27891a, 0xed7a, 0x40e1, 0x88, 0xe8, 0xb2, 0x27,
+ 0x27, 0xa0, 0x24, 0xee);
+
+// R600, R700, Evergreen and Cayman AMD cards. These support DXVA via UVD3 or
+// earlier, and don't handle 1080p60 well.
+static const DWORD sAMDPreUVD4[] = {
+ // clang-format off
+ 0x9400, 0x9401, 0x9402, 0x9403, 0x9405, 0x940a, 0x940b, 0x940f, 0x94c0, 0x94c1, 0x94c3, 0x94c4, 0x94c5,
+ 0x94c6, 0x94c7, 0x94c8, 0x94c9, 0x94cb, 0x94cc, 0x94cd, 0x9580, 0x9581, 0x9583, 0x9586, 0x9587, 0x9588,
+ 0x9589, 0x958a, 0x958b, 0x958c, 0x958d, 0x958e, 0x958f, 0x9500, 0x9501, 0x9504, 0x9505, 0x9506, 0x9507,
+ 0x9508, 0x9509, 0x950f, 0x9511, 0x9515, 0x9517, 0x9519, 0x95c0, 0x95c2, 0x95c4, 0x95c5, 0x95c6, 0x95c7,
+ 0x95c9, 0x95cc, 0x95cd, 0x95ce, 0x95cf, 0x9590, 0x9591, 0x9593, 0x9595, 0x9596, 0x9597, 0x9598, 0x9599,
+ 0x959b, 0x9610, 0x9611, 0x9612, 0x9613, 0x9614, 0x9615, 0x9616, 0x9710, 0x9711, 0x9712, 0x9713, 0x9714,
+ 0x9715, 0x9440, 0x9441, 0x9442, 0x9443, 0x9444, 0x9446, 0x944a, 0x944b, 0x944c, 0x944e, 0x9450, 0x9452,
+ 0x9456, 0x945a, 0x945b, 0x945e, 0x9460, 0x9462, 0x946a, 0x946b, 0x947a, 0x947b, 0x9480, 0x9487, 0x9488,
+ 0x9489, 0x948a, 0x948f, 0x9490, 0x9491, 0x9495, 0x9498, 0x949c, 0x949e, 0x949f, 0x9540, 0x9541, 0x9542,
+ 0x954e, 0x954f, 0x9552, 0x9553, 0x9555, 0x9557, 0x955f, 0x94a0, 0x94a1, 0x94a3, 0x94b1, 0x94b3, 0x94b4,
+ 0x94b5, 0x94b9, 0x68e0, 0x68e1, 0x68e4, 0x68e5, 0x68e8, 0x68e9, 0x68f1, 0x68f2, 0x68f8, 0x68f9, 0x68fa,
+ 0x68fe, 0x68c0, 0x68c1, 0x68c7, 0x68c8, 0x68c9, 0x68d8, 0x68d9, 0x68da, 0x68de, 0x68a0, 0x68a1, 0x68a8,
+ 0x68a9, 0x68b0, 0x68b8, 0x68b9, 0x68ba, 0x68be, 0x68bf, 0x6880, 0x6888, 0x6889, 0x688a, 0x688c, 0x688d,
+ 0x6898, 0x6899, 0x689b, 0x689e, 0x689c, 0x689d, 0x9802, 0x9803, 0x9804, 0x9805, 0x9806, 0x9807, 0x9808,
+ 0x9809, 0x980a, 0x9640, 0x9641, 0x9647, 0x9648, 0x964a, 0x964b, 0x964c, 0x964e, 0x964f, 0x9642, 0x9643,
+ 0x9644, 0x9645, 0x9649, 0x6720, 0x6721, 0x6722, 0x6723, 0x6724, 0x6725, 0x6726, 0x6727, 0x6728, 0x6729,
+ 0x6738, 0x6739, 0x673e, 0x6740, 0x6741, 0x6742, 0x6743, 0x6744, 0x6745, 0x6746, 0x6747, 0x6748, 0x6749,
+ 0x674a, 0x6750, 0x6751, 0x6758, 0x6759, 0x675b, 0x675d, 0x675f, 0x6840, 0x6841, 0x6842, 0x6843, 0x6849,
+ 0x6850, 0x6858, 0x6859, 0x6760, 0x6761, 0x6762, 0x6763, 0x6764, 0x6765, 0x6766, 0x6767, 0x6768, 0x6770,
+ 0x6771, 0x6772, 0x6778, 0x6779, 0x677b, 0x6700, 0x6701, 0x6702, 0x6703, 0x6704, 0x6705, 0x6706, 0x6707,
+ 0x6708, 0x6709, 0x6718, 0x6719, 0x671c, 0x671d, 0x671f, 0x9900, 0x9901, 0x9903, 0x9904, 0x9905, 0x9906,
+ 0x9907, 0x9908, 0x9909, 0x990a, 0x990b, 0x990c, 0x990d, 0x990e, 0x990f, 0x9910, 0x9913, 0x9917, 0x9918,
+ 0x9919, 0x9990, 0x9991, 0x9992, 0x9993, 0x9994, 0x9995, 0x9996, 0x9997, 0x9998, 0x9999, 0x999a, 0x999b,
+ 0x999c, 0x999d, 0x99a0, 0x99a2, 0x99a4
+ // clang-format on
+};
+
+// List of NVidia Telsa GPU known to have broken NV12 rendering.
+static const DWORD sNVIDIABrokenNV12[] = {
+ // clang-format off
+ 0x0191, 0x0193, 0x0194, 0x0197, 0x019d, 0x019e, // G80
+ 0x0400, 0x0401, 0x0402, 0x0403, 0x0404, 0x0405, 0x0406, 0x0407, 0x0408, 0x0409, // G84
+ 0x040a, 0x040b, 0x040c, 0x040d, 0x040e, 0x040f,
+ 0x0420, 0x0421, 0x0422, 0x0423, 0x0424, 0x0425, 0x0426, 0x0427, 0x0428, 0x0429, // G86
+ 0x042a, 0x042b, 0x042c, 0x042d, 0x042e, 0x042f,
+ 0x0410, 0x0600, 0x0601, 0x0602, 0x0603, 0x0604, 0x0605, 0x0606, 0x0607, 0x0608, // G92
+ 0x0609, 0x060a, 0x060b, 0x060c, 0x060f, 0x0610, 0x0611, 0x0612, 0x0613, 0x0614,
+ 0x0615, 0x0617, 0x0618, 0x0619, 0x061a, 0x061b, 0x061c, 0x061d, 0x061e, 0x061f, // G94
+ 0x0621, 0x0622, 0x0623, 0x0625, 0x0626, 0x0627, 0x0628, 0x062a, 0x062b, 0x062c,
+ 0x062d, 0x062e, 0x0631, 0x0635, 0x0637, 0x0638, 0x063a,
+ 0x0640, 0x0641, 0x0643, 0x0644, 0x0645, 0x0646, 0x0647, 0x0648, 0x0649, 0x064a, // G96
+ 0x064b, 0x064c, 0x0651, 0x0652, 0x0653, 0x0654, 0x0655, 0x0656, 0x0658, 0x0659,
+ 0x065a, 0x065b, 0x065c, 0x065f,
+ 0x06e0, 0x06e1, 0x06e2, 0x06e3, 0x06e4, 0x06e6, 0x06e7, 0x06e8, 0x06e9, 0x06ea, // G98
+ 0x06eb, 0x06ec, 0x06ef, 0x06f1, 0x06f8, 0x06f9, 0x06fa, 0x06fb, 0x06fd, 0x06ff,
+ 0x05e0, 0x05e1, 0x05e2, 0x05e3, 0x05e6, 0x05e7, 0x05e9, 0x05ea, 0x05eb, 0x05ed, // G200
+ 0x05ee, 0x05ef,
+ 0x0840, 0x0844, 0x0845, 0x0846, 0x0847, 0x0848, 0x0849, 0x084a, 0x084b, 0x084c, // MCP77
+ 0x084d, 0x084f,
+ 0x0860, 0x0861, 0x0862, 0x0863, 0x0864, 0x0865, 0x0866, 0x0867, 0x0868, 0x0869, // MCP79
+ 0x086a, 0x086c, 0x086d, 0x086e, 0x086f, 0x0870, 0x0871, 0x0872, 0x0873, 0x0874,
+ 0x0876, 0x087a, 0x087d, 0x087e, 0x087f,
+ 0x0ca0, 0x0ca2, 0x0ca3, 0x0ca2, 0x0ca4, 0x0ca5, 0x0ca7, 0x0ca9, 0x0cac, 0x0caf, // GT215
+ 0x0cb0, 0x0cb1, 0x0cbc,
+ 0x0a20, 0x0a22, 0x0a23, 0x0a26, 0x0a27, 0x0a28, 0x0a29, 0x0a2a, 0x0a2b, 0x0a2c, // GT216
+ 0x0a2d, 0x0a32, 0x0a34, 0x0a35, 0x0a38, 0x0a3c,
+ 0x0a60, 0x0a62, 0x0a63, 0x0a64, 0x0a65, 0x0a66, 0x0a67, 0x0a68, 0x0a69, 0x0a6a, // GT218
+ 0x0a6c, 0x0a6e, 0x0a6f, 0x0a70, 0x0a71, 0x0a72, 0x0a73, 0x0a74, 0x0a75, 0x0a76,
+ 0x0a78, 0x0a7a, 0x0a7c, 0x10c0, 0x10c3, 0x10c5, 0x10d8
+ // clang-format on
+};
+
+// The size we use for our synchronization surface.
+// 16x16 is the size recommended by Microsoft (in the D3D9ExDXGISharedSurf
+// sample) that works best to avoid driver bugs.
+static const uint32_t kSyncSurfaceSize = 16;
+
+namespace mozilla {
+
+using layers::D3D11RecycleAllocator;
+using layers::D3D11ShareHandleImage;
+using layers::D3D9RecycleAllocator;
+using layers::D3D9SurfaceImage;
+using layers::Image;
+using layers::ImageContainer;
+using namespace layers;
+using namespace gfx;
+
+class D3D9DXVA2Manager : public DXVA2Manager {
+ public:
+ D3D9DXVA2Manager();
+ virtual ~D3D9DXVA2Manager();
+
+ HRESULT Init(layers::KnowsCompositor* aKnowsCompositor,
+ nsACString& aFailureReason);
+
+ IUnknown* GetDXVADeviceManager() override;
+
+ // Copies a region (aRegion) of the video frame stored in aVideoSample
+ // into an image which is returned by aOutImage.
+ HRESULT CopyToImage(IMFSample* aVideoSample, const gfx::IntRect& aRegion,
+ Image** aOutImage) override;
+
+ bool SupportsConfig(const VideoInfo& aInfo, IMFMediaType* aInputType,
+ IMFMediaType* aOutputType) override;
+
+ private:
+ bool CanCreateDecoder(const DXVA2_VideoDesc& aDesc) const;
+
+ already_AddRefed<IDirectXVideoDecoder> CreateDecoder(
+ const DXVA2_VideoDesc& aDesc) const;
+
+ RefPtr<IDirect3D9Ex> mD3D9;
+ RefPtr<IDirect3DDevice9Ex> mDevice;
+ RefPtr<IDirect3DDeviceManager9> mDeviceManager;
+ RefPtr<D3D9RecycleAllocator> mTextureClientAllocator;
+ RefPtr<IDirectXVideoDecoderService> mDecoderService;
+ RefPtr<IDirect3DSurface9> mSyncSurface;
+ RefPtr<IDirectXVideoDecoder> mDecoder;
+ GUID mDecoderGUID;
+ UINT32 mResetToken = 0;
+};
+
+void GetDXVA2ExtendedFormatFromMFMediaType(IMFMediaType* pType,
+ DXVA2_ExtendedFormat* pFormat) {
+ // Get the interlace mode.
+ MFVideoInterlaceMode interlace = MFVideoInterlaceMode(MFGetAttributeUINT32(
+ pType, MF_MT_INTERLACE_MODE, MFVideoInterlace_Unknown));
+
+ if (interlace == MFVideoInterlace_MixedInterlaceOrProgressive) {
+ pFormat->SampleFormat = DXVA2_SampleFieldInterleavedEvenFirst;
+ } else {
+ pFormat->SampleFormat = UINT(interlace);
+ }
+
+ pFormat->VideoChromaSubsampling = MFGetAttributeUINT32(
+ pType, MF_MT_VIDEO_CHROMA_SITING, MFVideoChromaSubsampling_Unknown);
+ pFormat->NominalRange = MFGetAttributeUINT32(pType, MF_MT_VIDEO_NOMINAL_RANGE,
+ MFNominalRange_Unknown);
+ pFormat->VideoTransferMatrix = MFGetAttributeUINT32(
+ pType, MF_MT_YUV_MATRIX, MFVideoTransferMatrix_Unknown);
+ pFormat->VideoLighting = MFGetAttributeUINT32(pType, MF_MT_VIDEO_LIGHTING,
+ MFVideoLighting_Unknown);
+ pFormat->VideoPrimaries = MFGetAttributeUINT32(pType, MF_MT_VIDEO_PRIMARIES,
+ MFVideoPrimaries_Unknown);
+ pFormat->VideoTransferFunction = MFGetAttributeUINT32(
+ pType, MF_MT_TRANSFER_FUNCTION, MFVideoTransFunc_Unknown);
+}
+
+HRESULT ConvertMFTypeToDXVAType(IMFMediaType* pType, DXVA2_VideoDesc* pDesc) {
+ ZeroMemory(pDesc, sizeof(*pDesc));
+
+ // The D3D format is the first DWORD of the subtype GUID.
+ GUID subtype = GUID_NULL;
+ HRESULT hr = pType->GetGUID(MF_MT_SUBTYPE, &subtype);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ pDesc->Format = (D3DFORMAT)subtype.Data1;
+
+ UINT32 width = 0;
+ UINT32 height = 0;
+ hr = MFGetAttributeSize(pType, MF_MT_FRAME_SIZE, &width, &height);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ NS_ENSURE_TRUE(width <= MAX_VIDEO_WIDTH, E_FAIL);
+ NS_ENSURE_TRUE(height <= MAX_VIDEO_HEIGHT, E_FAIL);
+ pDesc->SampleWidth = width;
+ pDesc->SampleHeight = height;
+
+ UINT32 fpsNumerator = 0;
+ UINT32 fpsDenominator = 0;
+ if (SUCCEEDED(MFGetAttributeRatio(pType, MF_MT_FRAME_RATE, &fpsNumerator,
+ &fpsDenominator))) {
+ pDesc->InputSampleFreq.Numerator = fpsNumerator;
+ pDesc->InputSampleFreq.Denominator = fpsDenominator;
+
+ GetDXVA2ExtendedFormatFromMFMediaType(pType, &pDesc->SampleFormat);
+ pDesc->OutputFrameFreq = pDesc->InputSampleFreq;
+ if ((pDesc->SampleFormat.SampleFormat ==
+ DXVA2_SampleFieldInterleavedEvenFirst) ||
+ (pDesc->SampleFormat.SampleFormat ==
+ DXVA2_SampleFieldInterleavedOddFirst)) {
+ pDesc->OutputFrameFreq.Numerator *= 2;
+ }
+ }
+
+ return S_OK;
+}
+
+// All GUIDs other than Intel ClearVideo can be found here:
+// https://docs.microsoft.com/en-us/windows/win32/medfound/direct3d-12-video-guids
+// VLD = Variable-length decoder, FGT = Film grain technology
+static const GUID DXVA2_ModeH264_VLD_NoFGT = {
+ 0x1b81be68,
+ 0xa0c7,
+ 0x11d3,
+ {0xb9, 0x84, 0x00, 0xc0, 0x4f, 0x2e, 0x73, 0xc5}};
+
+// Also known as DXVADDI_Intel_ModeH264_E here:
+// https://www.intel.com/content/dam/develop/external/us/en/documents/h264-avc-x4500-acceration-esardell-157713.pdf
+// Named based on the fact that this is only supported on older ClearVideo
+// Intel decoding hardware.
+static const GUID DXVA2_Intel_ClearVideo_ModeH264_VLD_NoFGT = {
+ 0x604F8E68,
+ 0x4951,
+ 0x4c54,
+ {0x88, 0xFE, 0xAB, 0xD2, 0x5C, 0x15, 0xB3, 0xD6}};
+
+// VP8 profiles
+static const GUID DXVA2_ModeVP8_VLD = {
+ 0x90b899ea,
+ 0x3a62,
+ 0x4705,
+ {0x88, 0xb3, 0x8d, 0xf0, 0x4b, 0x27, 0x44, 0xe7}};
+
+// VP9 profiles
+static const GUID DXVA2_ModeVP9_VLD_Profile0 = {
+ 0x463707f8,
+ 0xa1d0,
+ 0x4585,
+ {0x87, 0x6d, 0x83, 0xaa, 0x6d, 0x60, 0xb8, 0x9e}};
+
+static const GUID DXVA2_ModeVP9_VLD_10bit_Profile2 = {
+ 0xa4c749ef,
+ 0x6ecf,
+ 0x48aa,
+ {0x84, 0x48, 0x50, 0xa7, 0xa1, 0x16, 0x5f, 0xf7}};
+
+// AV1 profiles
+static const GUID DXVA2_ModeAV1_VLD_Profile0 = {
+ 0xb8be4ccb,
+ 0xcf53,
+ 0x46ba,
+ {0x8d, 0x59, 0xd6, 0xb8, 0xa6, 0xda, 0x5d, 0x2a}};
+
+static const GUID DXVA2_ModeAV1_VLD_Profile1 = {
+ 0x6936ff0f,
+ 0x45b1,
+ 0x4163,
+ {0x9c, 0xc1, 0x64, 0x6e, 0xf6, 0x94, 0x61, 0x08}};
+
+static const GUID DXVA2_ModeAV1_VLD_Profile2 = {
+ 0x0c5f2aa1,
+ 0xe541,
+ 0x4089,
+ {0xbb, 0x7b, 0x98, 0x11, 0x0a, 0x19, 0xd7, 0xc8}};
+
+static const GUID DXVA2_ModeAV1_VLD_12bit_Profile2 = {
+ 0x17127009,
+ 0xa00f,
+ 0x4ce1,
+ {0x99, 0x4e, 0xbf, 0x40, 0x81, 0xf6, 0xf3, 0xf0}};
+
+static const GUID DXVA2_ModeAV1_VLD_12bit_Profile2_420 = {
+ 0x2d80bed6,
+ 0x9cac,
+ 0x4835,
+ {0x9e, 0x91, 0x32, 0x7b, 0xbc, 0x4f, 0x9e, 0xe8}};
+
+// This tests if a DXVA video decoder can be created for the given media
+// type/resolution. It uses the same decoder device (DXVA2_ModeH264_E -
+// DXVA2_ModeH264_VLD_NoFGT) as the H264 decoder MFT provided by windows
+// (CLSID_CMSH264DecoderMFT) uses, so we can use it to determine if the MFT will
+// use software fallback or not.
+bool D3D9DXVA2Manager::SupportsConfig(const VideoInfo& aInfo,
+ IMFMediaType* aInputType,
+ IMFMediaType* aOutputType) {
+ GUID inputSubtype;
+ HRESULT hr = aInputType->GetGUID(MF_MT_SUBTYPE, &inputSubtype);
+ if (FAILED(hr) || inputSubtype != MFVideoFormat_H264) {
+ return false;
+ }
+
+ DXVA2_VideoDesc desc;
+ hr = ConvertMFTypeToDXVAType(aInputType, &desc);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+ return CanCreateDecoder(desc);
+}
+
+D3D9DXVA2Manager::D3D9DXVA2Manager() { MOZ_COUNT_CTOR(D3D9DXVA2Manager); }
+
+D3D9DXVA2Manager::~D3D9DXVA2Manager() { MOZ_COUNT_DTOR(D3D9DXVA2Manager); }
+
+IUnknown* D3D9DXVA2Manager::GetDXVADeviceManager() {
+ MutexAutoLock lock(mLock);
+ return mDeviceManager;
+}
+
+HRESULT
+D3D9DXVA2Manager::Init(layers::KnowsCompositor* aKnowsCompositor,
+ nsACString& aFailureReason) {
+ ScopedGfxFeatureReporter reporter("DXVA2D3D9");
+
+ // Create D3D9Ex.
+ HMODULE d3d9lib = LoadLibraryW(L"d3d9.dll");
+ NS_ENSURE_TRUE(d3d9lib, E_FAIL);
+ decltype(Direct3DCreate9Ex)* d3d9Create =
+ (decltype(Direct3DCreate9Ex)*)GetProcAddress(d3d9lib,
+ "Direct3DCreate9Ex");
+ if (!d3d9Create) {
+ NS_WARNING("Couldn't find Direct3DCreate9Ex symbol in d3d9.dll");
+ aFailureReason.AssignLiteral(
+ "Couldn't find Direct3DCreate9Ex symbol in d3d9.dll");
+ return E_FAIL;
+ }
+ RefPtr<IDirect3D9Ex> d3d9Ex;
+ HRESULT hr = d3d9Create(D3D_SDK_VERSION, getter_AddRefs(d3d9Ex));
+ if (!d3d9Ex) {
+ NS_WARNING("Direct3DCreate9 failed");
+ aFailureReason.AssignLiteral("Direct3DCreate9 failed");
+ return E_FAIL;
+ }
+
+ // Ensure we can do the YCbCr->RGB conversion in StretchRect.
+ // Fail if we can't.
+ hr = d3d9Ex->CheckDeviceFormatConversion(
+ D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL,
+ (D3DFORMAT)MAKEFOURCC('N', 'V', '1', '2'), D3DFMT_X8R8G8B8);
+ if (!SUCCEEDED(hr)) {
+ aFailureReason = nsPrintfCString(
+ "CheckDeviceFormatConversion failed with error %lX", hr);
+ return hr;
+ }
+
+ // Create D3D9DeviceEx. We pass null HWNDs here even though the documentation
+ // suggests that one of them should not be. At this point in time Chromium
+ // does the same thing for video acceleration.
+ D3DPRESENT_PARAMETERS params = {0};
+ params.BackBufferWidth = 1;
+ params.BackBufferHeight = 1;
+ params.BackBufferFormat = D3DFMT_A8R8G8B8;
+ params.BackBufferCount = 1;
+ params.SwapEffect = D3DSWAPEFFECT_DISCARD;
+ params.hDeviceWindow = nullptr;
+ params.Windowed = TRUE;
+ params.Flags = D3DPRESENTFLAG_VIDEO;
+
+ RefPtr<IDirect3DDevice9Ex> device;
+ hr = d3d9Ex->CreateDeviceEx(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, nullptr,
+ D3DCREATE_FPU_PRESERVE | D3DCREATE_MULTITHREADED |
+ D3DCREATE_MIXED_VERTEXPROCESSING,
+ &params, nullptr, getter_AddRefs(device));
+ if (!SUCCEEDED(hr)) {
+ aFailureReason =
+ nsPrintfCString("CreateDeviceEx failed with error %lX", hr);
+ return hr;
+ }
+
+ // Ensure we can create queries to synchronize operations between devices.
+ // Without this, when we make a copy of the frame in order to share it with
+ // another device, we can't be sure that the copy has finished before the
+ // other device starts using it.
+ RefPtr<IDirect3DQuery9> query;
+
+ hr = device->CreateQuery(D3DQUERYTYPE_EVENT, getter_AddRefs(query));
+ if (!SUCCEEDED(hr)) {
+ aFailureReason = nsPrintfCString("CreateQuery failed with error %lX", hr);
+ return hr;
+ }
+
+ // Create and initialize IDirect3DDeviceManager9.
+ UINT resetToken = 0;
+ RefPtr<IDirect3DDeviceManager9> deviceManager;
+
+ hr = wmf::DXVA2CreateDirect3DDeviceManager9(&resetToken,
+ getter_AddRefs(deviceManager));
+ if (!SUCCEEDED(hr)) {
+ aFailureReason = nsPrintfCString(
+ "DXVA2CreateDirect3DDeviceManager9 failed with error %lX", hr);
+ return hr;
+ }
+ hr = deviceManager->ResetDevice(device, resetToken);
+ if (!SUCCEEDED(hr)) {
+ aFailureReason = nsPrintfCString(
+ "IDirect3DDeviceManager9::ResetDevice failed with error %lX", hr);
+ return hr;
+ }
+
+ HANDLE deviceHandle;
+ RefPtr<IDirectXVideoDecoderService> decoderService;
+ hr = deviceManager->OpenDeviceHandle(&deviceHandle);
+ if (!SUCCEEDED(hr)) {
+ aFailureReason = nsPrintfCString(
+ "IDirect3DDeviceManager9::OpenDeviceHandle failed with error %lX", hr);
+ return hr;
+ }
+
+ hr = deviceManager->GetVideoService(
+ deviceHandle, IID_PPV_ARGS(decoderService.StartAssignment()));
+ deviceManager->CloseDeviceHandle(deviceHandle);
+ if (!SUCCEEDED(hr)) {
+ aFailureReason = nsPrintfCString(
+ "IDirectXVideoDecoderServer::GetVideoService failed with error %lX",
+ hr);
+ return hr;
+ }
+
+ UINT deviceCount;
+ GUID* decoderDevices = nullptr;
+ hr = decoderService->GetDecoderDeviceGuids(&deviceCount, &decoderDevices);
+ if (!SUCCEEDED(hr)) {
+ aFailureReason = nsPrintfCString(
+ "IDirectXVideoDecoderServer::GetDecoderDeviceGuids failed with error "
+ "%lX",
+ hr);
+ return hr;
+ }
+
+ bool found = false;
+ for (UINT i = 0; i < deviceCount; i++) {
+ if (decoderDevices[i] == DXVA2_ModeH264_VLD_NoFGT ||
+ decoderDevices[i] == DXVA2_Intel_ClearVideo_ModeH264_VLD_NoFGT) {
+ mDecoderGUID = decoderDevices[i];
+ found = true;
+ break;
+ }
+ }
+ CoTaskMemFree(decoderDevices);
+
+ if (!found) {
+ aFailureReason.AssignLiteral("Failed to find an appropriate decoder GUID");
+ return E_FAIL;
+ }
+
+ D3DADAPTER_IDENTIFIER9 adapter;
+ hr = d3d9Ex->GetAdapterIdentifier(D3DADAPTER_DEFAULT, 0, &adapter);
+ if (!SUCCEEDED(hr)) {
+ aFailureReason = nsPrintfCString(
+ "IDirect3D9Ex::GetAdapterIdentifier failed with error %lX", hr);
+ return hr;
+ }
+
+ if ((adapter.VendorId == 0x1022 || adapter.VendorId == 0x1002) &&
+ !StaticPrefs::media_wmf_skip_blacklist()) {
+ for (const auto& model : sAMDPreUVD4) {
+ if (adapter.DeviceId == model) {
+ mIsAMDPreUVD4 = true;
+ break;
+ }
+ }
+ if (StaticPrefs::media_wmf_dxva_d3d9_amd_pre_uvd4_disabled() &&
+ mIsAMDPreUVD4) {
+ aFailureReason.AssignLiteral(
+ "D3D9DXVA2Manager is disabled on AMDPreUVD4");
+ return E_FAIL;
+ }
+ }
+
+ RefPtr<IDirect3DSurface9> syncSurf;
+ hr = device->CreateRenderTarget(kSyncSurfaceSize, kSyncSurfaceSize,
+ D3DFMT_X8R8G8B8, D3DMULTISAMPLE_NONE, 0, TRUE,
+ getter_AddRefs(syncSurf), NULL);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ mDecoderService = decoderService;
+
+ mResetToken = resetToken;
+ mD3D9 = d3d9Ex;
+ mDevice = device;
+ mDeviceManager = deviceManager;
+ mSyncSurface = syncSurf;
+
+ if (layers::ImageBridgeChild::GetSingleton()) {
+ // There's no proper KnowsCompositor for ImageBridge currently (and it
+ // implements the interface), so just use that if it's available.
+ mTextureClientAllocator = new D3D9RecycleAllocator(
+ layers::ImageBridgeChild::GetSingleton().get(), mDevice);
+ } else {
+ mTextureClientAllocator =
+ new D3D9RecycleAllocator(aKnowsCompositor, mDevice);
+ }
+ mTextureClientAllocator->SetMaxPoolSize(5);
+
+ Telemetry::Accumulate(Telemetry::MEDIA_DECODER_BACKEND_USED,
+ uint32_t(media::MediaDecoderBackend::WMFDXVA2D3D9));
+
+ reporter.SetSuccessful();
+
+ return S_OK;
+}
+
+HRESULT
+D3D9DXVA2Manager::CopyToImage(IMFSample* aSample, const gfx::IntRect& aRegion,
+ Image** aOutImage) {
+ RefPtr<IMFMediaBuffer> buffer;
+ HRESULT hr = aSample->GetBufferByIndex(0, getter_AddRefs(buffer));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ RefPtr<IDirect3DSurface9> surface;
+ hr = wmf::MFGetService(buffer, MR_BUFFER_SERVICE, IID_IDirect3DSurface9,
+ getter_AddRefs(surface));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ RefPtr<D3D9SurfaceImage> image = new D3D9SurfaceImage();
+ hr = image->AllocateAndCopy(mTextureClientAllocator, surface, aRegion);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ RefPtr<IDirect3DSurface9> sourceSurf = image->GetD3D9Surface();
+
+ // Copy a small rect into our sync surface, and then map it
+ // to block until decoding/color conversion completes.
+ RECT copyRect = {0, 0, kSyncSurfaceSize, kSyncSurfaceSize};
+ hr = mDevice->StretchRect(sourceSurf, &copyRect, mSyncSurface, &copyRect,
+ D3DTEXF_NONE);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ D3DLOCKED_RECT lockedRect;
+ hr = mSyncSurface->LockRect(&lockedRect, NULL, D3DLOCK_READONLY);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = mSyncSurface->UnlockRect();
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ image.forget(aOutImage);
+ return S_OK;
+}
+
+// Count of the number of DXVAManager's we've created. This is also the
+// number of videos we're decoding with DXVA. Use on main thread only.
+static Atomic<uint32_t> sDXVAVideosCount(0);
+
+/* static */
+DXVA2Manager* DXVA2Manager::CreateD3D9DXVA(
+ layers::KnowsCompositor* aKnowsCompositor, nsACString& aFailureReason) {
+ HRESULT hr;
+
+ // DXVA processing takes up a lot of GPU resources, so limit the number of
+ // videos we use DXVA with at any one time.
+ uint32_t dxvaLimit = StaticPrefs::media_wmf_dxva_max_videos();
+
+ if (sDXVAVideosCount == dxvaLimit) {
+ aFailureReason.AssignLiteral("Too many DXVA videos playing");
+ return nullptr;
+ }
+
+ UniquePtr<D3D9DXVA2Manager> d3d9Manager(new D3D9DXVA2Manager());
+ hr = d3d9Manager->Init(aKnowsCompositor, aFailureReason);
+ if (SUCCEEDED(hr)) {
+ return d3d9Manager.release();
+ }
+
+ // No hardware accelerated video decoding. :(
+ return nullptr;
+}
+
+bool D3D9DXVA2Manager::CanCreateDecoder(const DXVA2_VideoDesc& aDesc) const {
+ float framerate = static_cast<float>(aDesc.OutputFrameFreq.Numerator) /
+ aDesc.OutputFrameFreq.Denominator;
+ if (IsUnsupportedResolution(aDesc.SampleWidth, aDesc.SampleHeight,
+ framerate)) {
+ return false;
+ }
+ RefPtr<IDirectXVideoDecoder> decoder = CreateDecoder(aDesc);
+ return decoder.get() != nullptr;
+}
+
+already_AddRefed<IDirectXVideoDecoder> D3D9DXVA2Manager::CreateDecoder(
+ const DXVA2_VideoDesc& aDesc) const {
+ UINT configCount;
+ DXVA2_ConfigPictureDecode* configs = nullptr;
+ HRESULT hr = mDecoderService->GetDecoderConfigurations(
+ mDecoderGUID, &aDesc, nullptr, &configCount, &configs);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ RefPtr<IDirect3DSurface9> surface;
+ hr = mDecoderService->CreateSurface(
+ aDesc.SampleWidth, aDesc.SampleHeight, 0,
+ (D3DFORMAT)MAKEFOURCC('N', 'V', '1', '2'), D3DPOOL_DEFAULT, 0,
+ DXVA2_VideoDecoderRenderTarget, surface.StartAssignment(), NULL);
+ if (!SUCCEEDED(hr)) {
+ CoTaskMemFree(configs);
+ return nullptr;
+ }
+
+ for (UINT i = 0; i < configCount; i++) {
+ RefPtr<IDirectXVideoDecoder> decoder;
+ IDirect3DSurface9* surfaces = surface;
+ hr = mDecoderService->CreateVideoDecoder(mDecoderGUID, &aDesc, &configs[i],
+ &surfaces, 1,
+ decoder.StartAssignment());
+ if (FAILED(hr)) {
+ continue;
+ }
+
+ CoTaskMemFree(configs);
+ return decoder.forget();
+ }
+
+ CoTaskMemFree(configs);
+ return nullptr;
+}
+
+class D3D11DXVA2Manager : public DXVA2Manager {
+ public:
+ D3D11DXVA2Manager();
+ virtual ~D3D11DXVA2Manager();
+
+ HRESULT Init(layers::KnowsCompositor* aKnowsCompositor,
+ nsACString& aFailureReason, ID3D11Device* aDevice);
+ HRESULT InitInternal(layers::KnowsCompositor* aKnowsCompositor,
+ nsACString& aFailureReason, ID3D11Device* aDevice);
+
+ IUnknown* GetDXVADeviceManager() override;
+
+ // Copies a region (aRegion) of the video frame stored in aVideoSample
+ // into an image which is returned by aOutImage.
+ HRESULT CopyToImage(IMFSample* aVideoSample, const gfx::IntRect& aRegion,
+ Image** aOutImage) override;
+
+ HRESULT WrapTextureWithImage(IMFSample* aVideoSample,
+ const gfx::IntRect& aRegion,
+ layers::Image** aOutImage) override;
+
+ HRESULT CopyToBGRATexture(ID3D11Texture2D* aInTexture, uint32_t aArrayIndex,
+ ID3D11Texture2D** aOutTexture) override;
+
+ HRESULT ConfigureForSize(IMFMediaType* aInputType,
+ gfx::YUVColorSpace aColorSpace,
+ gfx::ColorRange aColorRange, uint32_t aWidth,
+ uint32_t aHeight) override;
+
+ bool IsD3D11() override { return true; }
+
+ bool SupportsConfig(const VideoInfo& aInfo, IMFMediaType* aInputType,
+ IMFMediaType* aOutputType) override;
+
+ void BeforeShutdownVideoMFTDecoder() override;
+
+ bool SupportsZeroCopyNV12Texture() override {
+ if (mIMFSampleUsageInfo->SupportsZeroCopyNV12Texture() &&
+ (mDevice != DeviceManagerDx::Get()->GetCompositorDevice())) {
+ mIMFSampleUsageInfo->DisableZeroCopyNV12Texture();
+ }
+ return mIMFSampleUsageInfo->SupportsZeroCopyNV12Texture();
+ }
+
+ private:
+ HRESULT CreateOutputSample(RefPtr<IMFSample>& aSample,
+ ID3D11Texture2D* aTexture);
+
+ bool CanCreateDecoder(const D3D11_VIDEO_DECODER_DESC& aDesc) const;
+
+ already_AddRefed<ID3D11VideoDecoder> CreateDecoder(
+ const D3D11_VIDEO_DECODER_DESC& aDesc) const;
+ void RefreshIMFSampleWrappers();
+ void ReleaseAllIMFSamples();
+
+ RefPtr<ID3D11Device> mDevice;
+ RefPtr<ID3D11DeviceContext> mContext;
+ RefPtr<IMFDXGIDeviceManager> mDXGIDeviceManager;
+ RefPtr<MFTDecoder> mTransform;
+ RefPtr<D3D11RecycleAllocator> mTextureClientAllocator;
+ RefPtr<layers::KnowsCompositor> mKnowsCompositor;
+ RefPtr<ID3D11VideoDecoder> mDecoder;
+ RefPtr<layers::SyncObjectClient> mSyncObject;
+ uint32_t mWidth = 0;
+ uint32_t mHeight = 0;
+ UINT mDeviceManagerToken = 0;
+ RefPtr<IMFMediaType> mInputType;
+ GUID mInputSubType;
+ gfx::YUVColorSpace mYUVColorSpace;
+ gfx::ColorRange mColorRange = gfx::ColorRange::LIMITED;
+ std::list<ThreadSafeWeakPtr<layers::IMFSampleWrapper>> mIMFSampleWrappers;
+ RefPtr<layers::IMFSampleUsageInfo> mIMFSampleUsageInfo;
+};
+
+bool D3D11DXVA2Manager::SupportsConfig(const VideoInfo& aInfo,
+ IMFMediaType* aInputType,
+ IMFMediaType* aOutputType) {
+ D3D11_VIDEO_DECODER_DESC desc = {GUID_NULL, 0, 0, DXGI_FORMAT_UNKNOWN};
+
+ HRESULT hr = MFGetAttributeSize(aInputType, MF_MT_FRAME_SIZE,
+ &desc.SampleWidth, &desc.SampleHeight);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+ NS_ENSURE_TRUE(desc.SampleWidth <= MAX_VIDEO_WIDTH, false);
+ NS_ENSURE_TRUE(desc.SampleHeight <= MAX_VIDEO_HEIGHT, false);
+
+ GUID subtype;
+ hr = aInputType->GetGUID(MF_MT_SUBTYPE, &subtype);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+
+ if (subtype == MFVideoFormat_H264) {
+ // IsUnsupportedResolution is only used to work around an AMD H264 issue.
+ const float framerate = [&]() {
+ UINT32 numerator;
+ UINT32 denominator;
+ if (SUCCEEDED(MFGetAttributeRatio(aInputType, MF_MT_FRAME_RATE,
+ &numerator, &denominator))) {
+ return static_cast<float>(numerator) / denominator;
+ }
+ return 30.0f;
+ }();
+ NS_ENSURE_FALSE(
+ IsUnsupportedResolution(desc.SampleWidth, desc.SampleHeight, framerate),
+ false);
+ NS_ENSURE_TRUE(aInfo.mColorDepth == ColorDepth::COLOR_8, false);
+
+ RefPtr<ID3D11VideoDevice> videoDevice;
+ hr = mDevice->QueryInterface(
+ static_cast<ID3D11VideoDevice**>(getter_AddRefs(videoDevice)));
+
+ GUID guids[] = {DXVA2_ModeH264_VLD_NoFGT,
+ DXVA2_Intel_ClearVideo_ModeH264_VLD_NoFGT};
+ for (const GUID& guid : guids) {
+ BOOL supported = false;
+ hr = videoDevice->CheckVideoDecoderFormat(&guid, DXGI_FORMAT_NV12,
+ &supported);
+ if (SUCCEEDED(hr) && supported) {
+ desc.Guid = guid;
+ break;
+ }
+ }
+ } else if (subtype == MFVideoFormat_VP80) {
+ NS_ENSURE_TRUE(aInfo.mColorDepth == ColorDepth::COLOR_8, false);
+ desc.Guid = DXVA2_ModeVP8_VLD;
+ } else if (subtype == MFVideoFormat_VP90) {
+ NS_ENSURE_TRUE(aInfo.mColorDepth == ColorDepth::COLOR_8 ||
+ aInfo.mColorDepth == ColorDepth::COLOR_10,
+ false);
+ uint8_t profile;
+
+ if (aInfo.mExtraData && !aInfo.mExtraData->IsEmpty()) {
+ VPXDecoder::VPXStreamInfo vp9Info;
+ VPXDecoder::ReadVPCCBox(vp9Info, aInfo.mExtraData);
+ profile = vp9Info.mProfile;
+ } else {
+ // If no vpcC is present, we can't know the profile, which limits the
+ // subsampling mode, but 4:2:0 is most supported so default to profiles 0
+ // and 2:
+ // Profile 0 = 8bit, 4:2:0
+ // Profile 2 = 10/12bit, 4:2:0
+ profile = aInfo.mColorDepth == ColorDepth::COLOR_8 ? 0 : 2;
+ }
+
+ switch (profile) {
+ case 0:
+ desc.Guid = DXVA2_ModeVP9_VLD_Profile0;
+ break;
+ case 2:
+ desc.Guid = DXVA2_ModeVP9_VLD_10bit_Profile2;
+ break;
+ default:
+ break;
+ }
+ } else if (subtype == MFVideoFormat_AV1) {
+ uint8_t profile;
+ bool yuv420;
+
+ if (aInfo.mExtraData && !aInfo.mExtraData->IsEmpty()) {
+ AOMDecoder::AV1SequenceInfo av1Info;
+ bool hadSeqHdr;
+ AOMDecoder::ReadAV1CBox(aInfo.mExtraData, av1Info, hadSeqHdr);
+ profile = av1Info.mProfile;
+ yuv420 = av1Info.mSubsamplingX && av1Info.mSubsamplingY;
+ } else {
+ // If no av1C is present, we can't get profile or subsampling mode. 4:2:0
+ // subsampling is most likely to be supported in hardware, so set av1Info
+ // accordingly.
+ // 8bit/10bit = Main profile, 4:2:0
+ // 12bit = Professional, 4:2:0
+ profile = aInfo.mColorDepth == ColorDepth::COLOR_12 ? 2 : 0;
+ yuv420 = true;
+ }
+
+ switch (profile) {
+ case 0:
+ desc.Guid = DXVA2_ModeAV1_VLD_Profile0;
+ break;
+ case 1:
+ desc.Guid = DXVA2_ModeAV1_VLD_Profile1;
+ break;
+ case 2:
+ MOZ_ASSERT(aInfo.mColorDepth < ColorDepth::COLOR_16);
+ if (aInfo.mColorDepth == ColorDepth::COLOR_12) {
+ if (yuv420) {
+ desc.Guid = DXVA2_ModeAV1_VLD_12bit_Profile2_420;
+ } else {
+ desc.Guid = DXVA2_ModeAV1_VLD_12bit_Profile2;
+ }
+ } else {
+ desc.Guid = DXVA2_ModeAV1_VLD_Profile2;
+ }
+ break;
+ default:
+ break;
+ }
+ }
+
+ hr = aOutputType->GetGUID(MF_MT_SUBTYPE, &subtype);
+ if (SUCCEEDED(hr)) {
+ if (subtype == MFVideoFormat_NV12) {
+ desc.OutputFormat = DXGI_FORMAT_NV12;
+ } else if (subtype == MFVideoFormat_P010) {
+ desc.OutputFormat = DXGI_FORMAT_P010;
+ } else if (subtype == MFVideoFormat_P016) {
+ desc.OutputFormat = DXGI_FORMAT_P016;
+ }
+ }
+
+ if (desc.Guid == GUID_NULL || desc.OutputFormat == DXGI_FORMAT_UNKNOWN) {
+ return false;
+ }
+
+ return CanCreateDecoder(desc);
+}
+
+D3D11DXVA2Manager::D3D11DXVA2Manager()
+ : mIMFSampleUsageInfo(new layers::IMFSampleUsageInfo) {}
+
+D3D11DXVA2Manager::~D3D11DXVA2Manager() {}
+
+IUnknown* D3D11DXVA2Manager::GetDXVADeviceManager() {
+ MutexAutoLock lock(mLock);
+ return mDXGIDeviceManager;
+}
+HRESULT
+D3D11DXVA2Manager::Init(layers::KnowsCompositor* aKnowsCompositor,
+ nsACString& aFailureReason, ID3D11Device* aDevice) {
+ if (aDevice) {
+ return InitInternal(aKnowsCompositor, aFailureReason, aDevice);
+ }
+
+ HRESULT hr;
+ ScopedGfxFeatureReporter reporter("DXVA2D3D11");
+
+ hr = InitInternal(aKnowsCompositor, aFailureReason, aDevice);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ if (layers::ImageBridgeChild::GetSingleton() || !aKnowsCompositor) {
+ // There's no proper KnowsCompositor for ImageBridge currently (and it
+ // implements the interface), so just use that if it's available.
+ mTextureClientAllocator = new D3D11RecycleAllocator(
+ layers::ImageBridgeChild::GetSingleton().get(), mDevice,
+ gfx::SurfaceFormat::NV12);
+
+ if (ImageBridgeChild::GetSingleton() &&
+ StaticPrefs::media_wmf_use_sync_texture_AtStartup() &&
+ mDevice != DeviceManagerDx::Get()->GetCompositorDevice()) {
+ // We use a syncobject to avoid the cost of the mutex lock when
+ // compositing, and because it allows color conversion ocurring directly
+ // from this texture DXVA does not seem to accept IDXGIKeyedMutex textures
+ // as input.
+ mSyncObject = layers::SyncObjectClient::CreateSyncObjectClient(
+ layers::ImageBridgeChild::GetSingleton()
+ ->GetTextureFactoryIdentifier()
+ .mSyncHandle,
+ mDevice);
+ }
+ } else {
+ mTextureClientAllocator = new D3D11RecycleAllocator(
+ aKnowsCompositor, mDevice, gfx::SurfaceFormat::NV12);
+ mKnowsCompositor = aKnowsCompositor;
+ if (StaticPrefs::media_wmf_use_sync_texture_AtStartup()) {
+ // We use a syncobject to avoid the cost of the mutex lock when
+ // compositing, and because it allows color conversion ocurring directly
+ // from this texture DXVA does not seem to accept IDXGIKeyedMutex textures
+ // as input.
+ mSyncObject = layers::SyncObjectClient::CreateSyncObjectClient(
+ aKnowsCompositor->GetTextureFactoryIdentifier().mSyncHandle, mDevice);
+ }
+ }
+ mTextureClientAllocator->SetMaxPoolSize(5);
+
+ Telemetry::Accumulate(Telemetry::MEDIA_DECODER_BACKEND_USED,
+ uint32_t(media::MediaDecoderBackend::WMFDXVA2D3D11));
+
+ reporter.SetSuccessful();
+
+ return S_OK;
+}
+
+HRESULT
+D3D11DXVA2Manager::InitInternal(layers::KnowsCompositor* aKnowsCompositor,
+ nsACString& aFailureReason,
+ ID3D11Device* aDevice) {
+ HRESULT hr;
+
+ mDevice = aDevice;
+
+ if (!mDevice) {
+ bool useHardwareWebRender =
+ aKnowsCompositor && aKnowsCompositor->UsingHardwareWebRender();
+ mDevice =
+ gfx::DeviceManagerDx::Get()->CreateDecoderDevice(useHardwareWebRender);
+ if (!mDevice) {
+ aFailureReason.AssignLiteral("Failed to create D3D11 device for decoder");
+ return E_FAIL;
+ }
+ }
+
+ RefPtr<ID3D10Multithread> mt;
+ hr = mDevice->QueryInterface((ID3D10Multithread**)getter_AddRefs(mt));
+ NS_ENSURE_TRUE(SUCCEEDED(hr) && mt, hr);
+ mt->SetMultithreadProtected(TRUE);
+
+ mDevice->GetImmediateContext(getter_AddRefs(mContext));
+
+ hr = wmf::MFCreateDXGIDeviceManager(&mDeviceManagerToken,
+ getter_AddRefs(mDXGIDeviceManager));
+ if (!SUCCEEDED(hr)) {
+ aFailureReason =
+ nsPrintfCString("MFCreateDXGIDeviceManager failed with code %lX", hr);
+ return hr;
+ }
+
+ hr = mDXGIDeviceManager->ResetDevice(mDevice, mDeviceManagerToken);
+ if (!SUCCEEDED(hr)) {
+ aFailureReason = nsPrintfCString(
+ "IMFDXGIDeviceManager::ResetDevice failed with code %lX", hr);
+ return hr;
+ }
+
+ // The IMFTransform interface used by MFTDecoder is documented to require to
+ // run on an MTA thread.
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/ee892371(v=vs.85).aspx#components
+ // The main thread (where this function is called) is STA, not MTA.
+ RefPtr<MFTDecoder> mft;
+ mozilla::mscom::EnsureMTA([&]() -> void {
+ mft = new MFTDecoder();
+ hr = mft->Create(MFT_CATEGORY_VIDEO_PROCESSOR, MFVideoFormat_NV12,
+ MFVideoFormat_ARGB32);
+
+ if (!SUCCEEDED(hr)) {
+ aFailureReason = nsPrintfCString(
+ "MFTDecoder::Create of Video Processor MFT for color conversion "
+ "failed with code %lX",
+ hr);
+ return;
+ }
+
+ hr = mft->SendMFTMessage(MFT_MESSAGE_SET_D3D_MANAGER,
+ ULONG_PTR(mDXGIDeviceManager.get()));
+ if (!SUCCEEDED(hr)) {
+ aFailureReason = nsPrintfCString(
+ "MFTDecoder::SendMFTMessage(MFT_MESSAGE_"
+ "SET_D3D_MANAGER) failed with code %lX",
+ hr);
+ return;
+ }
+ });
+
+ if (!SUCCEEDED(hr)) {
+ return hr;
+ }
+ mTransform = mft;
+
+ RefPtr<IDXGIDevice> dxgiDevice;
+ hr = mDevice->QueryInterface(
+ static_cast<IDXGIDevice**>(getter_AddRefs(dxgiDevice)));
+ if (!SUCCEEDED(hr)) {
+ aFailureReason =
+ nsPrintfCString("QI to IDXGIDevice failed with code %lX", hr);
+ return hr;
+ }
+
+ RefPtr<IDXGIAdapter> adapter;
+ hr = dxgiDevice->GetAdapter(adapter.StartAssignment());
+ if (!SUCCEEDED(hr)) {
+ aFailureReason =
+ nsPrintfCString("IDXGIDevice::GetAdapter failed with code %lX", hr);
+ return hr;
+ }
+
+ DXGI_ADAPTER_DESC adapterDesc;
+ hr = adapter->GetDesc(&adapterDesc);
+ if (!SUCCEEDED(hr)) {
+ aFailureReason =
+ nsPrintfCString("IDXGIAdapter::GetDesc failed with code %lX", hr);
+ return hr;
+ }
+
+ if ((adapterDesc.VendorId == 0x1022 || adapterDesc.VendorId == 0x1002) &&
+ !StaticPrefs::media_wmf_skip_blacklist()) {
+ for (const auto& model : sAMDPreUVD4) {
+ if (adapterDesc.DeviceId == model) {
+ mIsAMDPreUVD4 = true;
+ break;
+ }
+ }
+ }
+
+ if (!IsD3D11() || !XRE_IsGPUProcess() ||
+ (mDevice != DeviceManagerDx::Get()->GetCompositorDevice())) {
+ mIMFSampleUsageInfo->DisableZeroCopyNV12Texture();
+ }
+
+ return S_OK;
+}
+
+HRESULT
+D3D11DXVA2Manager::CreateOutputSample(RefPtr<IMFSample>& aSample,
+ ID3D11Texture2D* aTexture) {
+ RefPtr<IMFSample> sample;
+ HRESULT hr = wmf::MFCreateSample(getter_AddRefs(sample));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ RefPtr<IMFMediaBuffer> buffer;
+ hr = wmf::MFCreateDXGISurfaceBuffer(__uuidof(ID3D11Texture2D), aTexture, 0,
+ FALSE, getter_AddRefs(buffer));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = sample->AddBuffer(buffer);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ aSample = sample;
+ return S_OK;
+}
+
+HRESULT
+D3D11DXVA2Manager::CopyToImage(IMFSample* aVideoSample,
+ const gfx::IntRect& aRegion, Image** aOutImage) {
+ NS_ENSURE_TRUE(aVideoSample, E_POINTER);
+ NS_ENSURE_TRUE(aOutImage, E_POINTER);
+ MOZ_ASSERT(mTextureClientAllocator);
+
+ RefPtr<D3D11ShareHandleImage> image =
+ new D3D11ShareHandleImage(gfx::IntSize(mWidth, mHeight), aRegion,
+ ToColorSpace2(mYUVColorSpace), mColorRange);
+
+ // Retrieve the DXGI_FORMAT for the current video sample.
+ RefPtr<IMFMediaBuffer> buffer;
+ HRESULT hr = aVideoSample->GetBufferByIndex(0, getter_AddRefs(buffer));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ RefPtr<IMFDXGIBuffer> dxgiBuf;
+ hr = buffer->QueryInterface((IMFDXGIBuffer**)getter_AddRefs(dxgiBuf));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ RefPtr<ID3D11Texture2D> tex;
+ hr = dxgiBuf->GetResource(__uuidof(ID3D11Texture2D), getter_AddRefs(tex));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ D3D11_TEXTURE2D_DESC inDesc;
+ tex->GetDesc(&inDesc);
+
+ bool ok = image->AllocateTexture(mTextureClientAllocator, mDevice);
+ NS_ENSURE_TRUE(ok, E_FAIL);
+
+ RefPtr<TextureClient> client =
+ image->GetTextureClient(ImageBridgeChild::GetSingleton().get());
+ NS_ENSURE_TRUE(client, E_FAIL);
+
+ RefPtr<ID3D11Texture2D> texture = image->GetTexture();
+ D3D11_TEXTURE2D_DESC outDesc;
+ texture->GetDesc(&outDesc);
+
+ RefPtr<IDXGIKeyedMutex> mutex;
+ texture->QueryInterface((IDXGIKeyedMutex**)getter_AddRefs(mutex));
+
+ {
+ AutoTextureLock(mutex, hr, 2000);
+ if (mutex && (FAILED(hr) || hr == WAIT_TIMEOUT || hr == WAIT_ABANDONED)) {
+ return hr;
+ }
+
+ if (!mutex && mDevice != DeviceManagerDx::Get()->GetCompositorDevice()) {
+ NS_ENSURE_TRUE(mSyncObject, E_FAIL);
+ }
+
+ UINT height = std::min(inDesc.Height, outDesc.Height);
+ PerformanceRecorder<PlaybackStage> perfRecorder(
+ MediaStage::CopyDecodedVideo, height);
+ // The D3D11TextureClientAllocator may return a different texture format
+ // than preferred. In which case the destination texture will be BGRA32.
+ if (outDesc.Format == inDesc.Format) {
+ // Our video frame is stored in a non-sharable ID3D11Texture2D. We need
+ // to create a copy of that frame as a sharable resource, save its share
+ // handle, and put that handle into the rendering pipeline.
+ UINT width = std::min(inDesc.Width, outDesc.Width);
+ D3D11_BOX srcBox = {0, 0, 0, width, height, 1};
+
+ UINT index;
+ dxgiBuf->GetSubresourceIndex(&index);
+ mContext->CopySubresourceRegion(texture, 0, 0, 0, 0, tex, index, &srcBox);
+ } else {
+ // Use MFT to do color conversion.
+ hr = E_FAIL;
+ mozilla::mscom::EnsureMTA(
+ [&]() -> void { hr = mTransform->Input(aVideoSample); });
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ RefPtr<IMFSample> sample;
+ hr = CreateOutputSample(sample, texture);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = E_FAIL;
+ mozilla::mscom::EnsureMTA(
+ [&]() -> void { hr = mTransform->Output(&sample); });
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ }
+ perfRecorder.Record();
+ }
+
+ if (!mutex && mDevice != DeviceManagerDx::Get()->GetCompositorDevice() &&
+ mSyncObject) {
+ static StaticMutex sMutex MOZ_UNANNOTATED;
+ // Ensure that we only ever attempt to synchronise via the sync object
+ // serially as when using the same D3D11 device for multiple video decoders
+ // it can lead to deadlocks.
+ StaticMutexAutoLock lock(sMutex);
+ // It appears some race-condition may allow us to arrive here even when
+ // mSyncObject is null. It's better to avoid that crash.
+ client->SyncWithObject(mSyncObject);
+ if (!mSyncObject->Synchronize(true)) {
+ return DXGI_ERROR_DEVICE_RESET;
+ }
+ }
+
+ image.forget(aOutImage);
+
+ return S_OK;
+}
+
+HRESULT D3D11DXVA2Manager::WrapTextureWithImage(IMFSample* aVideoSample,
+ const gfx::IntRect& aRegion,
+ layers::Image** aOutImage) {
+ NS_ENSURE_TRUE(aVideoSample, E_POINTER);
+ NS_ENSURE_TRUE(aOutImage, E_POINTER);
+
+ RefPtr<IMFMediaBuffer> buffer;
+ HRESULT hr = aVideoSample->GetBufferByIndex(0, getter_AddRefs(buffer));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ RefPtr<IMFDXGIBuffer> dxgiBuf;
+ hr = buffer->QueryInterface((IMFDXGIBuffer**)getter_AddRefs(dxgiBuf));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ RefPtr<ID3D11Texture2D> texture;
+ hr = dxgiBuf->GetResource(__uuidof(ID3D11Texture2D), getter_AddRefs(texture));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ D3D11_TEXTURE2D_DESC desc;
+ texture->GetDesc(&desc);
+
+ UINT arrayIndex;
+ dxgiBuf->GetSubresourceIndex(&arrayIndex);
+
+ RefreshIMFSampleWrappers();
+
+ RefPtr<D3D11TextureIMFSampleImage> image = new D3D11TextureIMFSampleImage(
+ aVideoSample, texture, arrayIndex, gfx::IntSize(mWidth, mHeight), aRegion,
+ ToColorSpace2(mYUVColorSpace), mColorRange);
+ image->AllocateTextureClient(mKnowsCompositor, mIMFSampleUsageInfo);
+
+ RefPtr<IMFSampleWrapper> wrapper = image->GetIMFSampleWrapper();
+ ThreadSafeWeakPtr<IMFSampleWrapper> weak(wrapper);
+ mIMFSampleWrappers.push_back(weak);
+
+ image.forget(aOutImage);
+
+ return S_OK;
+}
+
+void D3D11DXVA2Manager::RefreshIMFSampleWrappers() {
+ for (auto it = mIMFSampleWrappers.begin(); it != mIMFSampleWrappers.end();) {
+ auto wrapper = RefPtr<IMFSampleWrapper>(*it);
+ if (!wrapper) {
+ // wrapper is already destroyed.
+ it = mIMFSampleWrappers.erase(it);
+ continue;
+ }
+ it++;
+ }
+}
+
+void D3D11DXVA2Manager::ReleaseAllIMFSamples() {
+ for (auto it = mIMFSampleWrappers.begin(); it != mIMFSampleWrappers.end();
+ it++) {
+ RefPtr<IMFSampleWrapper> wrapper = RefPtr<IMFSampleWrapper>(*it);
+ if (wrapper) {
+ wrapper->ClearVideoSample();
+ }
+ }
+}
+
+void D3D11DXVA2Manager::BeforeShutdownVideoMFTDecoder() {
+ ReleaseAllIMFSamples();
+}
+
+HRESULT
+D3D11DXVA2Manager::CopyToBGRATexture(ID3D11Texture2D* aInTexture,
+ uint32_t aArrayIndex,
+ ID3D11Texture2D** aOutTexture) {
+ NS_ENSURE_TRUE(aInTexture, E_POINTER);
+ NS_ENSURE_TRUE(aOutTexture, E_POINTER);
+
+ HRESULT hr;
+ RefPtr<ID3D11Texture2D> texture, inTexture;
+
+ inTexture = aInTexture;
+
+ CD3D11_TEXTURE2D_DESC desc;
+ aInTexture->GetDesc(&desc);
+
+ if (!mInputType || desc.Width != mWidth || desc.Height != mHeight) {
+ RefPtr<IMFMediaType> inputType;
+ hr = wmf::MFCreateMediaType(getter_AddRefs(inputType));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = inputType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ const GUID subType = [&]() {
+ switch (desc.Format) {
+ case DXGI_FORMAT_NV12:
+ return MFVideoFormat_NV12;
+ case DXGI_FORMAT_P010:
+ return MFVideoFormat_P010;
+ case DXGI_FORMAT_P016:
+ return MFVideoFormat_P016;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unexpected texture type");
+ return MFVideoFormat_NV12;
+ }
+ }();
+
+ hr = inputType->SetGUID(MF_MT_SUBTYPE, subType);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = inputType->SetUINT32(MF_MT_INTERLACE_MODE,
+ MFVideoInterlace_Progressive);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = inputType->SetUINT32(MF_MT_ALL_SAMPLES_INDEPENDENT, TRUE);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = ConfigureForSize(inputType, mYUVColorSpace, mColorRange, desc.Width,
+ desc.Height);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ }
+
+ RefPtr<IDXGIKeyedMutex> mutex;
+ inTexture->QueryInterface((IDXGIKeyedMutex**)getter_AddRefs(mutex));
+ // The rest of this function will not work if inTexture implements
+ // IDXGIKeyedMutex! In that case case we would have to copy to a
+ // non-mutex using texture.
+
+ if (mutex) {
+ RefPtr<ID3D11Texture2D> newTexture;
+
+ desc.MiscFlags = 0;
+ hr = mDevice->CreateTexture2D(&desc, nullptr, getter_AddRefs(newTexture));
+ NS_ENSURE_TRUE(SUCCEEDED(hr) && newTexture, E_FAIL);
+
+ hr = mutex->AcquireSync(0, 2000);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ mContext->CopyResource(newTexture, inTexture);
+
+ mutex->ReleaseSync(0);
+ inTexture = newTexture;
+ }
+
+ desc.Format = DXGI_FORMAT_B8G8R8A8_UNORM;
+ desc.BindFlags = D3D11_BIND_RENDER_TARGET | D3D11_BIND_SHADER_RESOURCE;
+
+ hr = mDevice->CreateTexture2D(&desc, nullptr, getter_AddRefs(texture));
+ NS_ENSURE_TRUE(SUCCEEDED(hr) && texture, E_FAIL);
+
+ RefPtr<IMFSample> inputSample;
+ wmf::MFCreateSample(getter_AddRefs(inputSample));
+
+ // If these aren't set the decoder fails.
+ inputSample->SetSampleTime(10);
+ inputSample->SetSampleDuration(10000);
+
+ RefPtr<IMFMediaBuffer> inputBuffer;
+ hr = wmf::MFCreateDXGISurfaceBuffer(__uuidof(ID3D11Texture2D), inTexture,
+ aArrayIndex, FALSE,
+ getter_AddRefs(inputBuffer));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ inputSample->AddBuffer(inputBuffer);
+
+ hr = E_FAIL;
+ mozilla::mscom::EnsureMTA(
+ [&]() -> void { hr = mTransform->Input(inputSample); });
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ RefPtr<IMFSample> outputSample;
+ hr = CreateOutputSample(outputSample, texture);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = E_FAIL;
+ mozilla::mscom::EnsureMTA(
+ [&]() -> void { hr = mTransform->Output(&outputSample); });
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ texture.forget(aOutTexture);
+
+ return S_OK;
+}
+
+HRESULT
+D3D11DXVA2Manager::ConfigureForSize(IMFMediaType* aInputType,
+ gfx::YUVColorSpace aColorSpace,
+ gfx::ColorRange aColorRange,
+ uint32_t aWidth, uint32_t aHeight) {
+ GUID subType = {0};
+ HRESULT hr = aInputType->GetGUID(MF_MT_SUBTYPE, &subType);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ if (subType == mInputSubType && aWidth == mWidth && aHeight == mHeight &&
+ mYUVColorSpace == aColorSpace && mColorRange == aColorRange) {
+ // If the media type hasn't changed, don't reconfigure.
+ return S_OK;
+ }
+
+ // Create a copy of our input type.
+ RefPtr<IMFMediaType> inputType;
+ hr = wmf::MFCreateMediaType(getter_AddRefs(inputType));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ hr = aInputType->CopyAllItems(inputType);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = MFSetAttributeSize(inputType, MF_MT_FRAME_SIZE, aWidth, aHeight);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ RefPtr<IMFAttributes> attr;
+ mozilla::mscom::EnsureMTA(
+ [&]() -> void { attr = mTransform->GetAttributes(); });
+ NS_ENSURE_TRUE(attr != nullptr, E_FAIL);
+
+ hr = attr->SetUINT32(MF_XVP_PLAYBACK_MODE, TRUE);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = attr->SetUINT32(MF_LOW_LATENCY, FALSE);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ RefPtr<IMFMediaType> outputType;
+ hr = wmf::MFCreateMediaType(getter_AddRefs(outputType));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = outputType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = outputType->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_ARGB32);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = E_FAIL;
+ mozilla::mscom::EnsureMTA([&]() -> void {
+ hr = mTransform->SetMediaTypes(
+ inputType, outputType, [aWidth, aHeight](IMFMediaType* aOutput) {
+ HRESULT hr = aOutput->SetUINT32(MF_MT_INTERLACE_MODE,
+ MFVideoInterlace_Progressive);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ hr = aOutput->SetUINT32(MF_MT_ALL_SAMPLES_INDEPENDENT, TRUE);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ hr = MFSetAttributeSize(aOutput, MF_MT_FRAME_SIZE, aWidth, aHeight);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ return S_OK;
+ });
+ });
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ mWidth = aWidth;
+ mHeight = aHeight;
+ mInputType = inputType;
+ mInputSubType = subType;
+ mYUVColorSpace = aColorSpace;
+ mColorRange = aColorRange;
+ if (mTextureClientAllocator) {
+ gfx::SurfaceFormat format = [&]() {
+ if (subType == MFVideoFormat_NV12) {
+ return gfx::SurfaceFormat::NV12;
+ } else if (subType == MFVideoFormat_P010) {
+ return gfx::SurfaceFormat::P010;
+ } else if (subType == MFVideoFormat_P016) {
+ return gfx::SurfaceFormat::P016;
+ } else {
+ MOZ_ASSERT_UNREACHABLE("Unexpected texture type");
+ return gfx::SurfaceFormat::NV12;
+ }
+ }();
+ mTextureClientAllocator->SetPreferredSurfaceFormat(format);
+ }
+ return S_OK;
+}
+
+bool D3D11DXVA2Manager::CanCreateDecoder(
+ const D3D11_VIDEO_DECODER_DESC& aDesc) const {
+ RefPtr<ID3D11VideoDecoder> decoder = CreateDecoder(aDesc);
+ return decoder.get() != nullptr;
+}
+
+already_AddRefed<ID3D11VideoDecoder> D3D11DXVA2Manager::CreateDecoder(
+ const D3D11_VIDEO_DECODER_DESC& aDesc) const {
+ RefPtr<ID3D11VideoDevice> videoDevice;
+ HRESULT hr = mDevice->QueryInterface(
+ static_cast<ID3D11VideoDevice**>(getter_AddRefs(videoDevice)));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ UINT configCount = 0;
+ hr = videoDevice->GetVideoDecoderConfigCount(&aDesc, &configCount);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ for (UINT i = 0; i < configCount; i++) {
+ D3D11_VIDEO_DECODER_CONFIG config;
+ hr = videoDevice->GetVideoDecoderConfig(&aDesc, i, &config);
+ if (SUCCEEDED(hr)) {
+ RefPtr<ID3D11VideoDecoder> decoder;
+ hr = videoDevice->CreateVideoDecoder(&aDesc, &config,
+ decoder.StartAssignment());
+ return decoder.forget();
+ }
+ }
+ return nullptr;
+}
+
+/* static */
+DXVA2Manager* DXVA2Manager::CreateD3D11DXVA(
+ layers::KnowsCompositor* aKnowsCompositor, nsACString& aFailureReason,
+ ID3D11Device* aDevice) {
+ // DXVA processing takes up a lot of GPU resources, so limit the number of
+ // videos we use DXVA with at any one time.
+ uint32_t dxvaLimit = StaticPrefs::media_wmf_dxva_max_videos();
+
+ if (sDXVAVideosCount == dxvaLimit) {
+ aFailureReason.AssignLiteral("Too many DXVA videos playing");
+ return nullptr;
+ }
+
+ UniquePtr<D3D11DXVA2Manager> manager(new D3D11DXVA2Manager());
+ HRESULT hr = manager->Init(aKnowsCompositor, aFailureReason, aDevice);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ return manager.release();
+}
+
+DXVA2Manager::DXVA2Manager() : mLock("DXVA2Manager") { ++sDXVAVideosCount; }
+
+DXVA2Manager::~DXVA2Manager() { --sDXVAVideosCount; }
+
+bool DXVA2Manager::IsUnsupportedResolution(const uint32_t& aWidth,
+ const uint32_t& aHeight,
+ const float& aFramerate) const {
+ // AMD cards with UVD3 or earlier perform poorly trying to decode 1080p60 in
+ // hardware, so use software instead. Pick 45 as an arbitrary upper bound for
+ // the framerate we can handle.
+ return !StaticPrefs::media_wmf_amd_highres_enabled() && mIsAMDPreUVD4 &&
+ (aWidth >= 1920 || aHeight >= 1088) && aFramerate > 45;
+}
+
+/* static */
+bool DXVA2Manager::IsNV12Supported(uint32_t aVendorID, uint32_t aDeviceID,
+ const nsAString& aDriverVersionString) {
+ if (aVendorID == 0x1022 || aVendorID == 0x1002) {
+ // AMD
+ // Block old cards regardless of driver version.
+ for (const auto& model : sAMDPreUVD4) {
+ if (aDeviceID == model) {
+ return false;
+ }
+ }
+ // AMD driver earlier than 21.19.411.0 have bugs in their handling of NV12
+ // surfaces.
+ uint64_t driverVersion;
+ if (!widget::ParseDriverVersion(aDriverVersionString, &driverVersion) ||
+ driverVersion < widget::V(21, 19, 411, 0)) {
+ return false;
+ }
+ } else if (aVendorID == 0x10DE) {
+ // NVidia
+ for (const auto& model : sNVIDIABrokenNV12) {
+ if (aDeviceID == model) {
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/wmf/DXVA2Manager.h b/dom/media/platforms/wmf/DXVA2Manager.h
new file mode 100644
index 0000000000..1fb501b406
--- /dev/null
+++ b/dom/media/platforms/wmf/DXVA2Manager.h
@@ -0,0 +1,94 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#if !defined(DXVA2Manager_h_)
+# define DXVA2Manager_h_
+
+# include "MediaInfo.h"
+# include "WMF.h"
+# include "mozilla/Mutex.h"
+# include "mozilla/gfx/Rect.h"
+# include "d3d11.h"
+
+namespace mozilla {
+
+namespace layers {
+class Image;
+class ImageContainer;
+class KnowsCompositor;
+} // namespace layers
+
+class DXVA2Manager {
+ public:
+ // Creates and initializes a DXVA2Manager. We can use DXVA2 via either
+ // D3D9Ex or D3D11.
+ static DXVA2Manager* CreateD3D9DXVA(layers::KnowsCompositor* aKnowsCompositor,
+ nsACString& aFailureReason);
+ static DXVA2Manager* CreateD3D11DXVA(
+ layers::KnowsCompositor* aKnowsCompositor, nsACString& aFailureReason,
+ ID3D11Device* aDevice = nullptr);
+
+ // Returns a pointer to the D3D device manager responsible for managing the
+ // device we're using for hardware accelerated video decoding. If we're using
+ // D3D9Ex, this is an IDirect3DDeviceManager9. For D3D11 this is an
+ // IMFDXGIDeviceManager. It is safe to call this on any thread.
+ virtual IUnknown* GetDXVADeviceManager() = 0;
+
+ // Creates an Image for the video frame stored in aVideoSample.
+ virtual HRESULT CopyToImage(IMFSample* aVideoSample,
+ const gfx::IntRect& aRegion,
+ layers::Image** aOutImage) = 0;
+
+ virtual HRESULT WrapTextureWithImage(IMFSample* aVideoSample,
+ const gfx::IntRect& aRegion,
+ layers::Image** aOutImage) {
+ // Not implemented!
+ MOZ_CRASH("WrapTextureWithImage not implemented on this manager.");
+ return E_FAIL;
+ }
+
+ virtual HRESULT CopyToBGRATexture(ID3D11Texture2D* aInTexture,
+ uint32_t aArrayIndex,
+ ID3D11Texture2D** aOutTexture) {
+ // Not implemented!
+ MOZ_CRASH("CopyToBGRATexture not implemented on this manager.");
+ return E_FAIL;
+ }
+
+ virtual HRESULT ConfigureForSize(IMFMediaType* aInputType,
+ gfx::YUVColorSpace aColorSpace,
+ gfx::ColorRange aColorRange, uint32_t aWidth,
+ uint32_t aHeight) {
+ return S_OK;
+ }
+
+ virtual bool IsD3D11() { return false; }
+
+ virtual ~DXVA2Manager();
+
+ virtual bool SupportsConfig(const VideoInfo& aInfo, IMFMediaType* aInputType,
+ IMFMediaType* aOutputType) = 0;
+
+ // Called before shutdown video MFTDecoder.
+ virtual void BeforeShutdownVideoMFTDecoder() {}
+
+ virtual bool SupportsZeroCopyNV12Texture() { return false; }
+
+ static bool IsNV12Supported(uint32_t aVendorID, uint32_t aDeviceID,
+ const nsAString& aDriverVersionString);
+
+ protected:
+ Mutex mLock MOZ_UNANNOTATED;
+ DXVA2Manager();
+
+ bool IsUnsupportedResolution(const uint32_t& aWidth, const uint32_t& aHeight,
+ const float& aFramerate) const;
+
+ bool mIsAMDPreUVD4 = false;
+};
+
+} // namespace mozilla
+
+#endif // DXVA2Manager_h_
diff --git a/dom/media/platforms/wmf/MFCDMExtra.h b/dom/media/platforms/wmf/MFCDMExtra.h
new file mode 100644
index 0000000000..04e625d854
--- /dev/null
+++ b/dom/media/platforms/wmf/MFCDMExtra.h
@@ -0,0 +1,307 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_PLATFORM_WMF_MFCDMEXTRA_H
+#define DOM_MEDIA_PLATFORM_WMF_MFCDMEXTRA_H
+
+// Currently, we build with WINVER=0x601 (Win7), which means the declarations in
+// mfcontentdecryptionmodule.h will not be visible, which is only available on
+// Win10 (0x0A00). Also, we don't yet have the Fall Creators Update SDK
+// available on build machines, so even with updated WINVER, some of the
+// interfaces we need would not be present. To work around this, until the build
+// environment is updated, we include copies of the relevant classes/interfaces
+// we need.
+#if defined(WINVER) && WINVER >= 0x0A00
+# include <mfcontentdecryptionmodule.h>
+#else
+// For `IMFCdmSuspendNotify`
+# include "MFMediaEngineExtra.h"
+
+typedef enum MF_MEDIAKEYS_REQUIREMENT {
+ MF_MEDIAKEYS_REQUIREMENT_REQUIRED = 1,
+ MF_MEDIAKEYS_REQUIREMENT_OPTIONAL = 2,
+ MF_MEDIAKEYS_REQUIREMENT_NOT_ALLOWED = 3
+} MF_MEDIAKEYS_REQUIREMENT;
+
+EXTERN_C const DECLSPEC_SELECTANY PROPERTYKEY
+ MF_CONTENTDECRYPTIONMODULE_STOREPATH = {
+ {0x77d993b9,
+ 0xba61,
+ 0x4bb7,
+ {0x92, 0xc6, 0x18, 0xc8, 0x6a, 0x18, 0x9c, 0x06}},
+ 0x02};
+EXTERN_C const DECLSPEC_SELECTANY PROPERTYKEY MF_EME_INITDATATYPES = {
+ {0x497d231b,
+ 0x4eb9,
+ 0x4df0,
+ {0xb4, 0x74, 0xb9, 0xaf, 0xeb, 0x0a, 0xdf, 0x38}},
+ PID_FIRST_USABLE + 0x00000001};
+EXTERN_C const DECLSPEC_SELECTANY PROPERTYKEY MF_EME_DISTINCTIVEID = {
+ {0x7dc9c4a5,
+ 0x12be,
+ 0x497e,
+ {0x8b, 0xff, 0x9b, 0x60, 0xb2, 0xdc, 0x58, 0x45}},
+ PID_FIRST_USABLE + 0x00000002};
+EXTERN_C const DECLSPEC_SELECTANY PROPERTYKEY MF_EME_PERSISTEDSTATE = {
+ {0x5d4df6ae,
+ 0x9af1,
+ 0x4e3d,
+ {0x95, 0x5b, 0x0e, 0x4b, 0xd2, 0x2f, 0xed, 0xf0}},
+ PID_FIRST_USABLE + 0x00000003};
+EXTERN_C const DECLSPEC_SELECTANY PROPERTYKEY MF_EME_AUDIOCAPABILITIES = {
+ {0x980fbb84,
+ 0x297d,
+ 0x4ea7,
+ {0x89, 0x5f, 0xbc, 0xf2, 0x8a, 0x46, 0x28, 0x81}},
+ PID_FIRST_USABLE + 0x00000004};
+EXTERN_C const DECLSPEC_SELECTANY PROPERTYKEY MF_EME_VIDEOCAPABILITIES = {
+ {0xb172f83d,
+ 0x30dd,
+ 0x4c10,
+ {0x80, 0x06, 0xed, 0x53, 0xda, 0x4d, 0x3b, 0xdb}},
+ PID_FIRST_USABLE + 0x00000005};
+EXTERN_C const DECLSPEC_SELECTANY PROPERTYKEY MF_EME_ROBUSTNESS = {
+ {0x9d3d2b9e,
+ 0x7023,
+ 0x4944,
+ {0xa8, 0xf5, 0xec, 0xca, 0x52, 0xa4, 0x69, 0x90}},
+ PID_FIRST_USABLE + 0x00000001};
+
+typedef enum MF_MEDIAKEYSESSION_TYPE {
+ MF_MEDIAKEYSESSION_TYPE_TEMPORARY = 0,
+ MF_MEDIAKEYSESSION_TYPE_PERSISTENT_LICENSE =
+ (MF_MEDIAKEYSESSION_TYPE_TEMPORARY + 1),
+ MF_MEDIAKEYSESSION_TYPE_PERSISTENT_RELEASE_MESSAGE =
+ (MF_MEDIAKEYSESSION_TYPE_PERSISTENT_LICENSE + 1),
+ MF_MEDIAKEYSESSION_TYPE_PERSISTENT_USAGE_RECORD =
+ (MF_MEDIAKEYSESSION_TYPE_PERSISTENT_RELEASE_MESSAGE + 1)
+} MF_MEDIAKEYSESSION_TYPE;
+
+typedef enum MF_MEDIAKEYSESSION_MESSAGETYPE {
+ MF_MEDIAKEYSESSION_MESSAGETYPE_LICENSE_REQUEST = 0,
+ MF_MEDIAKEYSESSION_MESSAGETYPE_LICENSE_RENEWAL = 1,
+ MF_MEDIAKEYSESSION_MESSAGETYPE_LICENSE_RELEASE = 2,
+ MF_MEDIAKEYSESSION_MESSAGETYPE_INDIVIDUALIZATION_REQUEST = 3
+} MF_MEDIAKEYSESSION_MESSAGETYPE;
+
+typedef enum MF_MEDIAKEY_STATUS {
+ MF_MEDIAKEY_STATUS_USABLE = 0,
+ MF_MEDIAKEY_STATUS_EXPIRED = (MF_MEDIAKEY_STATUS_USABLE + 1),
+ MF_MEDIAKEY_STATUS_OUTPUT_DOWNSCALED = (MF_MEDIAKEY_STATUS_EXPIRED + 1),
+ MF_MEDIAKEY_STATUS_OUTPUT_NOT_ALLOWED =
+ (MF_MEDIAKEY_STATUS_OUTPUT_DOWNSCALED + 1),
+ MF_MEDIAKEY_STATUS_STATUS_PENDING =
+ (MF_MEDIAKEY_STATUS_OUTPUT_NOT_ALLOWED + 1),
+ MF_MEDIAKEY_STATUS_INTERNAL_ERROR = (MF_MEDIAKEY_STATUS_STATUS_PENDING + 1),
+ MF_MEDIAKEY_STATUS_RELEASED = (MF_MEDIAKEY_STATUS_INTERNAL_ERROR + 1),
+ MF_MEDIAKEY_STATUS_OUTPUT_RESTRICTED = (MF_MEDIAKEY_STATUS_RELEASED + 1)
+} MF_MEDIAKEY_STATUS;
+
+typedef struct MFMediaKeyStatus {
+ BYTE* pbKeyId;
+ UINT cbKeyId;
+ MF_MEDIAKEY_STATUS eMediaKeyStatus;
+} MFMediaKeyStatus;
+
+EXTERN_GUID(MF_CONTENTDECRYPTIONMODULE_SERVICE, 0x15320c45, 0xff80, 0x484a,
+ 0x9d, 0xcb, 0xd, 0xf8, 0x94, 0xe6, 0x9a, 0x1);
+EXTERN_GUID(GUID_ObjectStream, 0x3e73735c, 0xe6c0, 0x481d, 0x82, 0x60, 0xee,
+ 0x5d, 0xb1, 0x34, 0x3b, 0x5f);
+EXTERN_GUID(GUID_ClassName, 0x77631a31, 0xe5e7, 0x4785, 0xbf, 0x17, 0x20, 0xf5,
+ 0x7b, 0x22, 0x48, 0x02);
+EXTERN_GUID(CLSID_EMEStoreActivate, 0x2df7b51e, 0x797b, 0x4d06, 0xbe, 0x71,
+ 0xd1, 0x4a, 0x52, 0xcf, 0x84, 0x21);
+
+# ifndef __IMFContentDecryptionModuleSessionCallbacks_INTERFACE_DEFINED__
+# define __IMFContentDecryptionModuleSessionCallbacks_INTERFACE_DEFINED__
+
+/* interface IMFContentDecryptionModuleSessionCallbacks */
+/* [unique][uuid][object] */
+
+EXTERN_C const IID IID_IMFContentDecryptionModuleSessionCallbacks;
+
+MIDL_INTERFACE("3f96ee40-ad81-4096-8470-59a4b770f89a")
+IMFContentDecryptionModuleSessionCallbacks : public IUnknown {
+ public:
+ virtual HRESULT STDMETHODCALLTYPE KeyMessage(
+ /* [in] */ MF_MEDIAKEYSESSION_MESSAGETYPE messageType,
+ /* [size_is][in] */
+ __RPC__in_ecount_full(messageSize) const BYTE* message,
+ /* [in] */ DWORD messageSize,
+ /* [optional][in] */ __RPC__in LPCWSTR destinationURL) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE KeyStatusChanged(void) = 0;
+};
+
+# endif /* __IMFContentDecryptionModuleSessionCallbacks_INTERFACE_DEFINED__ \
+ */
+
+# ifndef __IMFContentDecryptionModuleSession_INTERFACE_DEFINED__
+# define __IMFContentDecryptionModuleSession_INTERFACE_DEFINED__
+
+/* interface IMFContentDecryptionModuleSession */
+/* [unique][uuid][object] */
+
+EXTERN_C const IID IID_IMFContentDecryptionModuleSession;
+
+MIDL_INTERFACE("4e233efd-1dd2-49e8-b577-d63eee4c0d33")
+IMFContentDecryptionModuleSession : public IUnknown {
+ public:
+ virtual HRESULT STDMETHODCALLTYPE GetSessionId(
+ /* [out] */ __RPC__deref_out_opt LPWSTR * sessionId) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE GetExpiration(
+ /* [out] */ __RPC__out double* expiration) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE GetKeyStatuses(
+ /* [size_is][size_is][out] */ __RPC__deref_out_ecount_full_opt(
+ *numKeyStatuses) MFMediaKeyStatus *
+ *keyStatuses,
+ /* [out] */ __RPC__out UINT * numKeyStatuses) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE Load(
+ /* [in] */ __RPC__in LPCWSTR sessionId,
+ /* [out] */ __RPC__out BOOL * loaded) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE GenerateRequest(
+ /* [in] */ __RPC__in LPCWSTR initDataType,
+ /* [size_is][in] */
+ __RPC__in_ecount_full(initDataSize) const BYTE* initData,
+ /* [in] */ DWORD initDataSize) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE Update(
+ /* [size_is][in] */ __RPC__in_ecount_full(responseSize)
+ const BYTE* response,
+ /* [in] */ DWORD responseSize) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE Close(void) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE Remove(void) = 0;
+};
+
+# endif /* __IMFContentDecryptionModuleSession_INTERFACE_DEFINED__ */
+
+# ifndef __IMFPMPHostApp_INTERFACE_DEFINED__
+# define __IMFPMPHostApp_INTERFACE_DEFINED__
+
+/* interface IMFPMPHostApp */
+/* [uuid][object] */
+
+EXTERN_C const IID IID_IMFPMPHostApp;
+
+MIDL_INTERFACE("84d2054a-3aa1-4728-a3b0-440a418cf49c")
+IMFPMPHostApp : public IUnknown {
+ public:
+ virtual HRESULT STDMETHODCALLTYPE LockProcess(void) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE UnlockProcess(void) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE ActivateClassById(
+ /* [in] */ __RPC__in LPCWSTR id,
+ /* [unique][in] */ __RPC__in_opt IStream * pStream,
+ /* [in] */ __RPC__in REFIID riid,
+ /* [iid_is][out] */ __RPC__deref_out_opt void** ppv) = 0;
+};
+
+# endif /* __IMFPMPHostApp_INTERFACE_DEFINED__ */
+
+# ifndef __IMFContentDecryptionModule_INTERFACE_DEFINED__
+# define __IMFContentDecryptionModule_INTERFACE_DEFINED__
+
+/* interface IMFContentDecryptionModule */
+/* [unique][uuid][object] */
+
+EXTERN_C const IID IID_IMFContentDecryptionModule;
+
+MIDL_INTERFACE("87be986c-10be-4943-bf48-4b54ce1983a2")
+IMFContentDecryptionModule : public IUnknown {
+ public:
+ virtual HRESULT STDMETHODCALLTYPE SetContentEnabler(
+ /* [in] */ __RPC__in_opt IMFContentEnabler * contentEnabler,
+ /* [in] */ __RPC__in_opt IMFAsyncResult * result) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE GetSuspendNotify(
+ /* [out] */ __RPC__deref_out_opt IMFCdmSuspendNotify * *notify) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE SetPMPHostApp(
+ /* [in] */ __RPC__in_opt IMFPMPHostApp * pmpHostApp) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE CreateSession(
+ /* [in] */ MF_MEDIAKEYSESSION_TYPE sessionType,
+ /* [in] */ __RPC__in_opt IMFContentDecryptionModuleSessionCallbacks *
+ callbacks,
+ /* [out] */ __RPC__deref_out_opt IMFContentDecryptionModuleSession *
+ *session) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE SetServerCertificate(
+ /* [size_is][in] */ __RPC__in_ecount_full(certificateSize)
+ const BYTE* certificate,
+ /* [in] */ DWORD certificateSize) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE CreateTrustedInput(
+ /* [size_is][in] */ __RPC__in_ecount_full(contentInitDataSize)
+ const BYTE* contentInitData,
+ /* [in] */ DWORD contentInitDataSize,
+ /* [out] */ __RPC__deref_out_opt IMFTrustedInput** trustedInput) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE GetProtectionSystemIds(
+ /* [size_is][size_is][out] */ __RPC__deref_out_ecount_full_opt(*count)
+ GUID *
+ *systemIds,
+ /* [out] */ __RPC__out DWORD * count) = 0;
+};
+
+# endif /* __IMFContentDecryptionModule_INTERFACE_DEFINED__ */
+
+# ifndef __IMFContentDecryptionModuleAccess_INTERFACE_DEFINED__
+# define __IMFContentDecryptionModuleAccess_INTERFACE_DEFINED__
+
+/* interface IMFContentDecryptionModuleAccess */
+/* [unique][uuid][object] */
+
+EXTERN_C const IID IID_IMFContentDecryptionModuleAccess;
+
+MIDL_INTERFACE("a853d1f4-e2a0-4303-9edc-f1a68ee43136")
+IMFContentDecryptionModuleAccess : public IUnknown {
+ public:
+ virtual HRESULT STDMETHODCALLTYPE CreateContentDecryptionModule(
+ /* [in] */ __RPC__in_opt IPropertyStore *
+ contentDecryptionModuleProperties,
+ /* [out] */ __RPC__deref_out_opt IMFContentDecryptionModule *
+ *contentDecryptionModule) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE GetConfiguration(
+ /* [out] */ __RPC__deref_out_opt IPropertyStore * *configuration) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE GetKeySystem(
+ /* [out] */ __RPC__deref_out_opt LPWSTR * keySystem) = 0;
+};
+# endif /* __IMFContentDecryptionModuleAccess_INTERFACE_DEFINED__ */
+
+# ifndef __IMFContentDecryptionModuleFactory_INTERFACE_DEFINED__
+# define __IMFContentDecryptionModuleFactory_INTERFACE_DEFINED__
+
+/* interface IMFContentDecryptionModuleFactory */
+/* [local][uuid][object] */
+
+EXTERN_C const IID IID_IMFContentDecryptionModuleFactory;
+MIDL_INTERFACE("7d5abf16-4cbb-4e08-b977-9ba59049943e")
+IMFContentDecryptionModuleFactory : public IUnknown {
+ public:
+ virtual BOOL STDMETHODCALLTYPE IsTypeSupported(
+ /* [in] */ LPCWSTR keySystem,
+ /* [optional][in] */ LPCWSTR contentType) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE CreateContentDecryptionModuleAccess(
+ /* [in] */ LPCWSTR keySystem,
+ /* [size_is][size_is][in] */ IPropertyStore * *configurations,
+ /* [in] */ DWORD numConfigurations,
+ /* [out] */ IMFContentDecryptionModuleAccess *
+ *contentDecryptionModuleAccess) = 0;
+};
+# endif /* __IMFContentDecryptionModuleFactory_INTERFACE_DEFINED__ */
+
+#endif // defined(WINVER) && WINVER >= 0x0A00
+
+#endif // DOM_MEDIA_PLATFORM_WMF_MFCDMEXTRA_H
diff --git a/dom/media/platforms/wmf/MFCDMProxy.cpp b/dom/media/platforms/wmf/MFCDMProxy.cpp
new file mode 100644
index 0000000000..f460f5fb02
--- /dev/null
+++ b/dom/media/platforms/wmf/MFCDMProxy.cpp
@@ -0,0 +1,74 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MFCDMProxy.h"
+
+#include "MFMediaEngineUtils.h"
+
+namespace mozilla {
+
+using Microsoft::WRL::ComPtr;
+
+#define LOG(msg, ...) \
+ MOZ_LOG(gMFMediaEngineLog, LogLevel::Debug, \
+ ("MFCDMProxy=%p, " msg, this, ##__VA_ARGS__))
+
+HRESULT MFCDMProxy::GetPMPServer(REFIID aRiid, LPVOID* aPMPServerOut) {
+ ComPtr<IMFGetService> cdmServices;
+ RETURN_IF_FAILED(mCDM.As(&cdmServices));
+ RETURN_IF_FAILED(cdmServices->GetService(MF_CONTENTDECRYPTIONMODULE_SERVICE,
+ aRiid, aPMPServerOut));
+ return S_OK;
+}
+
+HRESULT MFCDMProxy::GetInputTrustAuthority(uint32_t aStreamId,
+ const uint8_t* aContentInitData,
+ uint32_t aContentInitDataSize,
+ REFIID aRiid,
+ IUnknown** aInputTrustAuthorityOut) {
+ if (mInputTrustAuthorities.count(aStreamId)) {
+ RETURN_IF_FAILED(
+ mInputTrustAuthorities[aStreamId].CopyTo(aInputTrustAuthorityOut));
+ return S_OK;
+ }
+
+ if (!mTrustedInput) {
+ RETURN_IF_FAILED(mCDM->CreateTrustedInput(
+ aContentInitData, aContentInitDataSize, &mTrustedInput));
+ LOG("Created a trust input for stream %u", aStreamId);
+ }
+
+ // GetInputTrustAuthority takes IUnknown* as the output. Using other COM
+ // interface will have a v-table mismatch issue.
+ ComPtr<IUnknown> unknown;
+ RETURN_IF_FAILED(
+ mTrustedInput->GetInputTrustAuthority(aStreamId, aRiid, &unknown));
+
+ ComPtr<IMFInputTrustAuthority> inputTrustAuthority;
+ RETURN_IF_FAILED(unknown.As(&inputTrustAuthority));
+ RETURN_IF_FAILED(unknown.CopyTo(aInputTrustAuthorityOut));
+
+ mInputTrustAuthorities[aStreamId] = inputTrustAuthority;
+ return S_OK;
+}
+
+HRESULT MFCDMProxy::SetContentEnabler(IUnknown* aRequest,
+ IMFAsyncResult* aResult) {
+ LOG("SetContentEnabler");
+ ComPtr<IMFContentEnabler> contentEnabler;
+ RETURN_IF_FAILED(aRequest->QueryInterface(IID_PPV_ARGS(&contentEnabler)));
+ return mCDM->SetContentEnabler(contentEnabler.Get(), aResult);
+}
+
+void MFCDMProxy::OnHardwareContextReset() {
+ LOG("OnHardwareContextReset");
+ // Hardware context reset happens, all the crypto sessions are in invalid
+ // states. So drop everything here.
+ mTrustedInput.Reset();
+ mInputTrustAuthorities.clear();
+}
+
+#undef LOG
+
+} // namespace mozilla
diff --git a/dom/media/platforms/wmf/MFCDMProxy.h b/dom/media/platforms/wmf/MFCDMProxy.h
new file mode 100644
index 0000000000..605755705c
--- /dev/null
+++ b/dom/media/platforms/wmf/MFCDMProxy.h
@@ -0,0 +1,71 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_PLATFORM_WMF_MFCDMPROXY_H
+#define DOM_MEDIA_PLATFORM_WMF_MFCDMPROXY_H
+
+#include <map>
+#include <mfobjects.h>
+#include <unknwn.h>
+#include <windef.h>
+#include <wrl.h>
+
+#include "MFCDMExtra.h"
+#include "nsISupportsImpl.h"
+
+namespace mozilla {
+
+/**
+ * MFCDMProxy wraps a IMFContentDecryptionModule and provides some high level
+ * helper methods in order to allow caller to interact with the wrapped CDM.
+ */
+class MFCDMProxy {
+ NS_INLINE_DECL_REFCOUNTING(MFCDMProxy);
+
+ explicit MFCDMProxy(IMFContentDecryptionModule* aCDM) : mCDM(aCDM) {}
+
+ public:
+ // Return a IMediaProtectionPMPServer from the existing CDM.
+ HRESULT GetPMPServer(REFIID aRiid, LPVOID* aPMPServerOut);
+
+ // Return a IMFInputTrustAuthority for given stream id, the same stream ID
+ // always maps to the same IMFInputTrustAuthority. In addition,
+ // `aContentInitData` is optional initialization data as in
+ // https://www.w3.org/TR/encrypted-media/#initialization-data
+ HRESULT GetInputTrustAuthority(uint32_t aStreamId,
+ const uint8_t* aContentInitData,
+ uint32_t aContentInitDataSize, REFIID aRiid,
+ IUnknown** aInputTrustAuthorityOut);
+
+ // Set IMFContentEnabler to the existing CDM, `aRequest` should be a inherited
+ // class of `IMFContentEnabler`.
+ HRESULT SetContentEnabler(IUnknown* aRequest, IMFAsyncResult* aResult);
+
+ // Notify the CDM on DRM_E_TEE_INVALID_HWDRM_STATE (0x8004cd12), which happens
+ // in cases like OS Sleep. In this case, the CDM should close all sessions
+ // because they are in bad state.
+ void OnHardwareContextReset();
+
+ // TODO : set last key id in order to let CDM use the key IDs information to
+ // perform some optimization.
+
+ private:
+ ~MFCDMProxy() = default;
+
+ Microsoft::WRL::ComPtr<IMFContentDecryptionModule> mCDM;
+
+ // The same ITA is always mapping to the same stream Id.
+ std::map<uint32_t /* stream Id */,
+ Microsoft::WRL::ComPtr<IMFInputTrustAuthority>>
+ mInputTrustAuthorities;
+
+ Microsoft::WRL::ComPtr<IMFTrustedInput> mTrustedInput;
+
+ // TODO : need some events? (Eg. significant playback, error, hardware context
+ // reset)
+};
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_PLATFORM_WMF_MFCDMPROXY_H
diff --git a/dom/media/platforms/wmf/MFCDMSession.cpp b/dom/media/platforms/wmf/MFCDMSession.cpp
new file mode 100644
index 0000000000..d693c5a731
--- /dev/null
+++ b/dom/media/platforms/wmf/MFCDMSession.cpp
@@ -0,0 +1,314 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MFCDMSession.h"
+
+#include <limits>
+#include <vcruntime.h>
+#include <winerror.h>
+
+#include "MFMediaEngineUtils.h"
+#include "GMPUtils.h" // ToHexString
+#include "mozilla/EMEUtils.h"
+#include "mozilla/dom/MediaKeyMessageEventBinding.h"
+#include "mozilla/dom/MediaKeyStatusMapBinding.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+
+using Microsoft::WRL::ComPtr;
+using Microsoft::WRL::MakeAndInitialize;
+
+#define LOG(msg, ...) EME_LOG("MFCDMSession=%p, " msg, this, ##__VA_ARGS__)
+
+static inline MF_MEDIAKEYSESSION_TYPE ConvertSessionType(
+ KeySystemConfig::SessionType aType) {
+ switch (aType) {
+ case KeySystemConfig::SessionType::Temporary:
+ return MF_MEDIAKEYSESSION_TYPE_TEMPORARY;
+ case KeySystemConfig::SessionType::PersistentLicense:
+ return MF_MEDIAKEYSESSION_TYPE_PERSISTENT_LICENSE;
+ }
+}
+
+static inline LPCWSTR InitDataTypeToString(const nsAString& aInitDataType) {
+ // The strings are defined in https://www.w3.org/TR/eme-initdata-registry/
+ if (aInitDataType.EqualsLiteral("webm")) {
+ return L"webm";
+ } else if (aInitDataType.EqualsLiteral("cenc")) {
+ return L"cenc";
+ } else if (aInitDataType.EqualsLiteral("keyids")) {
+ return L"keyids";
+ } else {
+ return L"unknown";
+ }
+}
+
+// The callback interface which IMFContentDecryptionModuleSession uses for
+// communicating with the session.
+class MFCDMSession::SessionCallbacks final
+ : public Microsoft::WRL::RuntimeClass<
+ Microsoft::WRL::RuntimeClassFlags<Microsoft::WRL::ClassicCom>,
+ IMFContentDecryptionModuleSessionCallbacks> {
+ public:
+ SessionCallbacks() = default;
+ SessionCallbacks(const SessionCallbacks&) = delete;
+ SessionCallbacks& operator=(const SessionCallbacks&) = delete;
+ ~SessionCallbacks() = default;
+
+ HRESULT RuntimeClassInitialize() { return S_OK; }
+
+ // IMFContentDecryptionModuleSessionCallbacks
+ STDMETHODIMP KeyMessage(MF_MEDIAKEYSESSION_MESSAGETYPE aType,
+ const BYTE* aMessage, DWORD aMessageSize,
+ LPCWSTR aUrl) final {
+ CopyableTArray<uint8_t> msg{static_cast<const uint8_t*>(aMessage),
+ aMessageSize};
+ mKeyMessageEvent.Notify(aType, std::move(msg));
+ return S_OK;
+ }
+
+ STDMETHODIMP KeyStatusChanged() final {
+ mKeyChangeEvent.Notify();
+ return S_OK;
+ }
+
+ MediaEventSource<MF_MEDIAKEYSESSION_MESSAGETYPE, nsTArray<uint8_t>>&
+ KeyMessageEvent() {
+ return mKeyMessageEvent;
+ }
+ MediaEventSource<void>& KeyChangeEvent() { return mKeyChangeEvent; }
+
+ private:
+ MediaEventProducer<MF_MEDIAKEYSESSION_MESSAGETYPE, nsTArray<uint8_t>>
+ mKeyMessageEvent;
+ MediaEventProducer<void> mKeyChangeEvent;
+};
+
+/* static*/
+MFCDMSession* MFCDMSession::Create(KeySystemConfig::SessionType aSessionType,
+ IMFContentDecryptionModule* aCdm,
+ nsISerialEventTarget* aManagerThread) {
+ MOZ_ASSERT(aCdm);
+ MOZ_ASSERT(aManagerThread);
+ ComPtr<SessionCallbacks> callbacks;
+ RETURN_PARAM_IF_FAILED(MakeAndInitialize<SessionCallbacks>(&callbacks),
+ nullptr);
+
+ ComPtr<IMFContentDecryptionModuleSession> session;
+ RETURN_PARAM_IF_FAILED(aCdm->CreateSession(ConvertSessionType(aSessionType),
+ callbacks.Get(), &session),
+ nullptr);
+ return new MFCDMSession(session.Get(), callbacks.Get(), aManagerThread);
+}
+
+MFCDMSession::MFCDMSession(IMFContentDecryptionModuleSession* aSession,
+ SessionCallbacks* aCallback,
+ nsISerialEventTarget* aManagerThread)
+ : mSession(aSession),
+ mManagerThread(aManagerThread),
+ mExpiredTimeMilliSecondsSinceEpoch(
+ std::numeric_limits<double>::quiet_NaN()) {
+ MOZ_ASSERT(aSession);
+ MOZ_ASSERT(aCallback);
+ MOZ_ASSERT(aManagerThread);
+ mKeyMessageListener = aCallback->KeyMessageEvent().Connect(
+ mManagerThread, this, &MFCDMSession::OnSessionKeyMessage);
+ mKeyChangeListener = aCallback->KeyChangeEvent().Connect(
+ mManagerThread, this, &MFCDMSession::OnSessionKeysChange);
+}
+
+MFCDMSession::~MFCDMSession() {
+ // TODO : maybe disconnect them in `Close()`?
+ mKeyChangeListener.DisconnectIfExists();
+ mKeyMessageListener.DisconnectIfExists();
+}
+
+HRESULT MFCDMSession::GenerateRequest(const nsAString& aInitDataType,
+ const uint8_t* aInitData,
+ uint32_t aInitDataSize) {
+ AssertOnManagerThread();
+ LOG("GenerateRequest for %s (init sz=%u)",
+ NS_ConvertUTF16toUTF8(aInitDataType).get(), aInitDataSize);
+ RETURN_IF_FAILED(mSession->GenerateRequest(
+ InitDataTypeToString(aInitDataType), aInitData, aInitDataSize));
+ Unused << RetrieveSessionId();
+ return S_OK;
+}
+
+HRESULT MFCDMSession::Load(const nsAString& aSessionId) {
+ AssertOnManagerThread();
+ // TODO : do we need to implement this? Chromium doesn't implement this one.
+ // Also, how do we know is this given session ID is equal to the session Id
+ // asked from CDM session or not?
+ BOOL rv = FALSE;
+ mSession->Load(char16ptr_t(aSessionId.BeginReading()), &rv);
+ LOG("Load, id=%s, rv=%s", NS_ConvertUTF16toUTF8(aSessionId).get(),
+ rv ? "success" : "fail");
+ return rv ? S_OK : S_FALSE;
+}
+
+HRESULT MFCDMSession::Update(const nsTArray<uint8_t>& aMessage) {
+ AssertOnManagerThread();
+ LOG("Update");
+ RETURN_IF_FAILED(mSession->Update(
+ static_cast<const BYTE*>(aMessage.Elements()), aMessage.Length()));
+ RETURN_IF_FAILED(UpdateExpirationIfNeeded());
+ return S_OK;
+}
+
+HRESULT MFCDMSession::Close() {
+ AssertOnManagerThread();
+ LOG("Close");
+ RETURN_IF_FAILED(mSession->Close());
+ return S_OK;
+}
+
+HRESULT MFCDMSession::Remove() {
+ AssertOnManagerThread();
+ LOG("Remove");
+ RETURN_IF_FAILED(mSession->Remove());
+ RETURN_IF_FAILED(UpdateExpirationIfNeeded());
+ return S_OK;
+}
+
+bool MFCDMSession::RetrieveSessionId() {
+ AssertOnManagerThread();
+ if (mSessionId) {
+ return true;
+ }
+ ScopedCoMem<wchar_t> sessionId;
+ if (FAILED(mSession->GetSessionId(&sessionId)) || !sessionId) {
+ LOG("Can't get session id or empty session ID!");
+ return false;
+ }
+ LOG("Set session Id %ls", sessionId.Get());
+ mSessionId = Some(sessionId.Get());
+ return true;
+}
+
+void MFCDMSession::OnSessionKeysChange() {
+ AssertOnManagerThread();
+ LOG("OnSessionKeysChange");
+
+ if (!mSessionId) {
+ LOG("Unexpected session keys change ignored");
+ return;
+ }
+
+ ScopedCoMem<MFMediaKeyStatus> keyStatuses;
+ UINT count = 0;
+ RETURN_VOID_IF_FAILED(mSession->GetKeyStatuses(&keyStatuses, &count));
+
+ static auto ToMediaKeyStatus = [](MF_MEDIAKEY_STATUS aStatus) {
+ // https://learn.microsoft.com/en-us/windows/win32/api/mfidl/ne-mfidl-mf_mediakey_status
+ switch (aStatus) {
+ case MF_MEDIAKEY_STATUS_USABLE:
+ return dom::MediaKeyStatus::Usable;
+ case MF_MEDIAKEY_STATUS_EXPIRED:
+ return dom::MediaKeyStatus::Expired;
+ case MF_MEDIAKEY_STATUS_OUTPUT_DOWNSCALED:
+ return dom::MediaKeyStatus::Output_downscaled;
+ // This is for legacy use and should not happen in normal cases. Map it to
+ // internal error in case it happens.
+ case MF_MEDIAKEY_STATUS_OUTPUT_NOT_ALLOWED:
+ return dom::MediaKeyStatus::Internal_error;
+ case MF_MEDIAKEY_STATUS_STATUS_PENDING:
+ return dom::MediaKeyStatus::Status_pending;
+ case MF_MEDIAKEY_STATUS_INTERNAL_ERROR:
+ return dom::MediaKeyStatus::Internal_error;
+ case MF_MEDIAKEY_STATUS_RELEASED:
+ return dom::MediaKeyStatus::Released;
+ case MF_MEDIAKEY_STATUS_OUTPUT_RESTRICTED:
+ return dom::MediaKeyStatus::Output_restricted;
+ }
+ MOZ_ASSERT_UNREACHABLE("Invalid MF_MEDIAKEY_STATUS enum value");
+ return dom::MediaKeyStatus::Internal_error;
+ };
+
+ CopyableTArray<MFCDMKeyInformation> keyInfos;
+ for (uint32_t idx = 0; idx < count; idx++) {
+ const MFMediaKeyStatus& keyStatus = keyStatuses[idx];
+ if (keyStatus.cbKeyId != sizeof(GUID)) {
+ LOG("Key ID with unsupported size ignored");
+ continue;
+ }
+ CopyableTArray<uint8_t> keyId;
+ ByteArrayFromGUID(reinterpret_cast<REFGUID>(keyStatus.pbKeyId), keyId);
+
+ nsAutoCString keyIdString(ToHexString(keyId));
+ LOG("Append keyid-sz=%u, keyid=%s, status=%s", keyStatus.cbKeyId,
+ keyIdString.get(),
+ ToMediaKeyStatusStr(ToMediaKeyStatus(keyStatus.eMediaKeyStatus)));
+ keyInfos.AppendElement(MFCDMKeyInformation{
+ std::move(keyId), ToMediaKeyStatus(keyStatus.eMediaKeyStatus)});
+ }
+ LOG("Notify 'keychange' for %s", NS_ConvertUTF16toUTF8(*mSessionId).get());
+ mKeyChangeEvent.Notify(
+ MFCDMKeyStatusChange{*mSessionId, std::move(keyInfos)});
+
+ // ScopedCoMem<MFMediaKeyStatus> only releases memory for |keyStatuses|. We
+ // need to manually release memory for |pbKeyId| here.
+ for (size_t idx = 0; idx < count; idx++) {
+ if (const auto& keyStatus = keyStatuses[idx]; keyStatus.pbKeyId) {
+ CoTaskMemFree(keyStatus.pbKeyId);
+ }
+ }
+}
+
+HRESULT MFCDMSession::UpdateExpirationIfNeeded() {
+ AssertOnManagerThread();
+ MOZ_ASSERT(mSessionId);
+
+ // The msdn document doesn't mention the unit for the expiration time,
+ // follow chromium's implementation to treat them as millisecond.
+ double newExpiredEpochTimeMs = 0.0;
+ RETURN_IF_FAILED(mSession->GetExpiration(&newExpiredEpochTimeMs));
+
+ if (newExpiredEpochTimeMs == mExpiredTimeMilliSecondsSinceEpoch ||
+ (std::isnan(newExpiredEpochTimeMs) &&
+ std::isnan(mExpiredTimeMilliSecondsSinceEpoch))) {
+ return S_OK;
+ }
+
+ LOG("Session expiration change from %f to %f, notify 'expiration' for %s",
+ mExpiredTimeMilliSecondsSinceEpoch, newExpiredEpochTimeMs,
+ NS_ConvertUTF16toUTF8(*mSessionId).get());
+ mExpiredTimeMilliSecondsSinceEpoch = newExpiredEpochTimeMs;
+ mExpirationEvent.Notify(
+ MFCDMKeyExpiration{*mSessionId, mExpiredTimeMilliSecondsSinceEpoch});
+ return S_OK;
+}
+
+void MFCDMSession::OnSessionKeyMessage(
+ const MF_MEDIAKEYSESSION_MESSAGETYPE& aType,
+ const nsTArray<uint8_t>& aMessage) {
+ AssertOnManagerThread();
+ // Only send key message after the session Id is ready.
+ if (!RetrieveSessionId()) {
+ return;
+ }
+ static auto ToMediaKeyMessageType = [](MF_MEDIAKEYSESSION_MESSAGETYPE aType) {
+ switch (aType) {
+ case MF_MEDIAKEYSESSION_MESSAGETYPE_LICENSE_REQUEST:
+ return dom::MediaKeyMessageType::License_request;
+ case MF_MEDIAKEYSESSION_MESSAGETYPE_LICENSE_RENEWAL:
+ return dom::MediaKeyMessageType::License_renewal;
+ case MF_MEDIAKEYSESSION_MESSAGETYPE_LICENSE_RELEASE:
+ return dom::MediaKeyMessageType::License_release;
+ case MF_MEDIAKEYSESSION_MESSAGETYPE_INDIVIDUALIZATION_REQUEST:
+ return dom::MediaKeyMessageType::Individualization_request;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unknown session message type");
+ return dom::MediaKeyMessageType::EndGuard_;
+ }
+ };
+ LOG("Notify 'keymessage' for %s", NS_ConvertUTF16toUTF8(*mSessionId).get());
+ mKeyMessageEvent.Notify(MFCDMKeyMessage{
+ *mSessionId, ToMediaKeyMessageType(aType), std::move(aMessage)});
+}
+
+#undef LOG
+
+} // namespace mozilla
diff --git a/dom/media/platforms/wmf/MFCDMSession.h b/dom/media/platforms/wmf/MFCDMSession.h
new file mode 100644
index 0000000000..44b7c3b239
--- /dev/null
+++ b/dom/media/platforms/wmf/MFCDMSession.h
@@ -0,0 +1,93 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_PLATFORM_WMF_MFCDMSESSION_H
+#define DOM_MEDIA_PLATFORM_WMF_MFCDMSESSION_H
+
+#include <vector>
+#include <wrl.h>
+#include <wrl/client.h>
+
+#include "MFCDMExtra.h"
+#include "MediaEventSource.h"
+#include "mozilla/PMFCDM.h"
+#include "mozilla/KeySystemConfig.h"
+#include "nsAString.h"
+
+namespace mozilla {
+
+// MFCDMSession represents a key session defined by the EME spec, it operates
+// the IMFContentDecryptionModuleSession directly and forward events from
+// IMFContentDecryptionModuleSession to its caller. It's not thread-safe and
+// can only be used on the manager thread for now.
+class MFCDMSession final {
+ public:
+ ~MFCDMSession();
+
+ static MFCDMSession* Create(KeySystemConfig::SessionType aSessionType,
+ IMFContentDecryptionModule* aCdm,
+ nsISerialEventTarget* aManagerThread);
+
+ // APIs corresponding to EME APIs (MediaKeySession)
+ HRESULT GenerateRequest(const nsAString& aInitDataType,
+ const uint8_t* aInitData, uint32_t aInitDataSize);
+ HRESULT Load(const nsAString& aSessionId);
+ HRESULT Update(const nsTArray<uint8_t>& aMessage);
+ HRESULT Close();
+ HRESULT Remove();
+
+ // Session status related events
+ MediaEventSource<MFCDMKeyMessage>& KeyMessageEvent() {
+ return mKeyMessageEvent;
+ }
+ MediaEventSource<MFCDMKeyStatusChange>& KeyChangeEvent() {
+ return mKeyChangeEvent;
+ }
+ MediaEventSource<MFCDMKeyExpiration>& ExpirationEvent() {
+ return mExpirationEvent;
+ }
+
+ const Maybe<nsString>& SessionID() const { return mSessionId; }
+
+ private:
+ class SessionCallbacks;
+
+ MFCDMSession(IMFContentDecryptionModuleSession* aSession,
+ SessionCallbacks* aCallback,
+ nsISerialEventTarget* aManagerThread);
+ MFCDMSession(const MFCDMSession&) = delete;
+ MFCDMSession& operator=(const MFCDMSession&) = delete;
+
+ bool RetrieveSessionId();
+ void OnSessionKeysChange();
+ void OnSessionKeyMessage(const MF_MEDIAKEYSESSION_MESSAGETYPE& aType,
+ const nsTArray<uint8_t>& aMessage);
+
+ HRESULT UpdateExpirationIfNeeded();
+
+ void AssertOnManagerThread() const {
+ MOZ_ASSERT(mManagerThread->IsOnCurrentThread());
+ }
+
+ const Microsoft::WRL::ComPtr<IMFContentDecryptionModuleSession> mSession;
+ const nsCOMPtr<nsISerialEventTarget> mManagerThread;
+
+ MediaEventProducer<MFCDMKeyMessage> mKeyMessageEvent;
+ MediaEventProducer<MFCDMKeyStatusChange> mKeyChangeEvent;
+ MediaEventProducer<MFCDMKeyExpiration> mExpirationEvent;
+ MediaEventListener mKeyMessageListener;
+ MediaEventListener mKeyChangeListener;
+
+ // IMFContentDecryptionModuleSession's id might not be ready immediately after
+ // the session gets created.
+ Maybe<nsString> mSessionId;
+
+ // NaN when the CDM doesn't explicitly define the time or the time never
+ // expires.
+ double mExpiredTimeMilliSecondsSinceEpoch;
+};
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_PLATFORM_WMF_MFCDMSESSION_H
diff --git a/dom/media/platforms/wmf/MFContentProtectionManager.cpp b/dom/media/platforms/wmf/MFContentProtectionManager.cpp
new file mode 100644
index 0000000000..79189d59b3
--- /dev/null
+++ b/dom/media/platforms/wmf/MFContentProtectionManager.cpp
@@ -0,0 +1,164 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MFContentProtectionManager.h"
+
+#include <hstring.h>
+#include <winnt.h>
+
+#include "MFMediaEngineUtils.h"
+#include "WMF.h"
+
+namespace mozilla {
+
+using Microsoft::WRL::ComPtr;
+
+#define LOG(msg, ...) \
+ MOZ_LOG(gMFMediaEngineLog, LogLevel::Debug, \
+ ("MFContentProtectionManager=%p, " msg, this, ##__VA_ARGS__))
+
+class ScopedHString final {
+ public:
+ explicit ScopedHString(const WCHAR aCharArray[]) {
+ WindowsCreateString(aCharArray, wcslen(aCharArray), &mString);
+ }
+ ~ScopedHString() { WindowsDeleteString(mString); }
+ const HSTRING& Get() { return mString; }
+
+ private:
+ HSTRING mString;
+};
+
+HRESULT MFContentProtectionManager::RuntimeClassInitialize() {
+ ScopedHString propertyId(
+ RuntimeClass_Windows_Foundation_Collections_PropertySet);
+ RETURN_IF_FAILED(RoActivateInstance(propertyId.Get(), &mPMPServerSet));
+ return S_OK;
+}
+
+HRESULT MFContentProtectionManager::BeginEnableContent(
+ IMFActivate* aEnablerActivate, IMFTopology* aTopology,
+ IMFAsyncCallback* aCallback, IUnknown* aState) {
+ LOG("BeginEnableContent");
+ ComPtr<IUnknown> unknownObject;
+ ComPtr<IMFAsyncResult> asyncResult;
+ RETURN_IF_FAILED(
+ wmf::MFCreateAsyncResult(nullptr, aCallback, aState, &asyncResult));
+ RETURN_IF_FAILED(
+ aEnablerActivate->ActivateObject(IID_PPV_ARGS(&unknownObject)));
+
+ GUID enablerType = GUID_NULL;
+ ComPtr<IMFContentEnabler> contentEnabler;
+ if (SUCCEEDED(unknownObject.As(&contentEnabler))) {
+ RETURN_IF_FAILED(contentEnabler->GetEnableType(&enablerType));
+ } else {
+ ComPtr<ABI::Windows::Media::Protection::IMediaProtectionServiceRequest>
+ serviceRequest;
+ RETURN_IF_FAILED(unknownObject.As(&serviceRequest));
+ RETURN_IF_FAILED(serviceRequest->get_Type(&enablerType));
+ }
+
+ if (enablerType == MFENABLETYPE_MF_RebootRequired) {
+ LOG("Error - MFENABLETYPE_MF_RebootRequired");
+ return MF_E_REBOOT_REQUIRED;
+ } else if (enablerType == MFENABLETYPE_MF_UpdateRevocationInformation) {
+ LOG("Error - MFENABLETYPE_MF_UpdateRevocationInformation");
+ return MF_E_GRL_VERSION_TOO_LOW;
+ } else if (enablerType == MFENABLETYPE_MF_UpdateUntrustedComponent) {
+ LOG("Error - MFENABLETYPE_MF_UpdateUntrustedComponent");
+ return HRESULT_FROM_WIN32(ERROR_INVALID_IMAGE_HASH);
+ }
+
+ MOZ_ASSERT(mCDMProxy);
+ RETURN_IF_FAILED(
+ mCDMProxy->SetContentEnabler(unknownObject.Get(), asyncResult.Get()));
+
+ // TODO : maybe need to notify waiting for key status?
+ LOG("Finished BeginEnableContent");
+ return S_OK;
+}
+
+HRESULT MFContentProtectionManager::EndEnableContent(
+ IMFAsyncResult* aAsyncResult) {
+ HRESULT hr = aAsyncResult->GetStatus();
+ if (FAILED(hr)) {
+ // Follow Chromium to not to return failure, which avoid doing additional
+ // work here.
+ LOG("Content enabling failed. hr=%lx", hr);
+ } else {
+ LOG("Content enabling succeeded");
+ }
+ return S_OK;
+}
+
+HRESULT MFContentProtectionManager::add_ServiceRequested(
+ ABI::Windows::Media::Protection::IServiceRequestedEventHandler* aHandler,
+ EventRegistrationToken* aCookie) {
+ return E_NOTIMPL;
+}
+
+HRESULT MFContentProtectionManager::remove_ServiceRequested(
+ EventRegistrationToken aCookie) {
+ return E_NOTIMPL;
+}
+
+HRESULT MFContentProtectionManager::add_RebootNeeded(
+ ABI::Windows::Media::Protection::IRebootNeededEventHandler* aHandler,
+ EventRegistrationToken* aCookie) {
+ return E_NOTIMPL;
+}
+
+HRESULT MFContentProtectionManager::remove_RebootNeeded(
+ EventRegistrationToken aCookie) {
+ return E_NOTIMPL;
+}
+
+HRESULT MFContentProtectionManager::add_ComponentLoadFailed(
+ ABI::Windows::Media::Protection::IComponentLoadFailedEventHandler* aHandler,
+ EventRegistrationToken* aCookie) {
+ return E_NOTIMPL;
+}
+
+HRESULT MFContentProtectionManager::remove_ComponentLoadFailed(
+ EventRegistrationToken aCookie) {
+ return E_NOTIMPL;
+}
+
+HRESULT MFContentProtectionManager::get_Properties(
+ ABI::Windows::Foundation::Collections::IPropertySet** properties) {
+ if (!mPMPServerSet) {
+ return E_POINTER;
+ }
+ return mPMPServerSet.CopyTo(properties);
+}
+
+HRESULT MFContentProtectionManager::SetCDMProxy(MFCDMProxy* aCDMProxy) {
+ MOZ_ASSERT(aCDMProxy);
+ mCDMProxy = aCDMProxy;
+ ComPtr<ABI::Windows::Media::Protection::IMediaProtectionPMPServer> pmpServer;
+ RETURN_IF_FAILED(mCDMProxy->GetPMPServer(IID_PPV_ARGS(&pmpServer)));
+ RETURN_IF_FAILED(SetPMPServer(pmpServer.Get()));
+ return S_OK;
+}
+
+HRESULT MFContentProtectionManager::SetPMPServer(
+ ABI::Windows::Media::Protection::IMediaProtectionPMPServer* aPMPServer) {
+ MOZ_ASSERT(aPMPServer);
+
+ ComPtr<ABI::Windows::Foundation::Collections::IMap<HSTRING, IInspectable*>>
+ serverMap;
+ RETURN_IF_FAILED(mPMPServerSet.As(&serverMap));
+
+ // MFMediaEngine uses |serverKey| to get the Protected Media Path (PMP)
+ // server used for playing protected content. This is not currently documented
+ // in MSDN.
+ boolean replaced = false;
+ ScopedHString serverKey{L"Windows.Media.Protection.MediaProtectionPMPServer"};
+ RETURN_IF_FAILED(serverMap->Insert(serverKey.Get(), aPMPServer, &replaced));
+ return S_OK;
+}
+
+#undef LOG
+
+} // namespace mozilla
diff --git a/dom/media/platforms/wmf/MFContentProtectionManager.h b/dom/media/platforms/wmf/MFContentProtectionManager.h
new file mode 100644
index 0000000000..964c965c32
--- /dev/null
+++ b/dom/media/platforms/wmf/MFContentProtectionManager.h
@@ -0,0 +1,79 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_PLATFORM_WMF_MFCONTENTPROTECTIONMANAGER_H
+#define DOM_MEDIA_PLATFORM_WMF_MFCONTENTPROTECTIONMANAGER_H
+
+#include <mfapi.h>
+#include <mfidl.h>
+#include <windows.media.protection.h>
+#include <wrl.h>
+
+#include "MFCDMProxy.h"
+
+namespace mozilla {
+
+/**
+ * MFContentProtectionManager is used to enable the encrypted playback for the
+ * media engine.
+ * https://docs.microsoft.com/en-us/windows/win32/api/mfidl/nn-mfidl-imfcontentprotectionmanager
+ * https://docs.microsoft.com/en-us/uwp/api/windows.media.protection.mediaprotectionmanager
+ */
+class MFContentProtectionManager
+ : public Microsoft::WRL::RuntimeClass<
+ Microsoft::WRL::RuntimeClassFlags<
+ Microsoft::WRL::RuntimeClassType::WinRtClassicComMix |
+ Microsoft::WRL::RuntimeClassType::InhibitRoOriginateError>,
+ IMFContentProtectionManager,
+ ABI::Windows::Media::Protection::IMediaProtectionManager> {
+ public:
+ MFContentProtectionManager() = default;
+ ~MFContentProtectionManager() = default;
+
+ HRESULT RuntimeClassInitialize();
+
+ // IMFContentProtectionManager.
+ IFACEMETHODIMP BeginEnableContent(IMFActivate* aEnablerActivate,
+ IMFTopology* aTopology,
+ IMFAsyncCallback* aCallback,
+ IUnknown* aState) override;
+ IFACEMETHODIMP EndEnableContent(IMFAsyncResult* aAsyncResult) override;
+
+ // IMediaProtectionManager.
+ // MFMediaEngine can query this interface to invoke get_Properties().
+ IFACEMETHODIMP add_ServiceRequested(
+ ABI::Windows::Media::Protection::IServiceRequestedEventHandler* aHandler,
+ EventRegistrationToken* aCookie) override;
+ IFACEMETHODIMP remove_ServiceRequested(
+ EventRegistrationToken aCookie) override;
+ IFACEMETHODIMP add_RebootNeeded(
+ ABI::Windows::Media::Protection::IRebootNeededEventHandler* aHandler,
+ EventRegistrationToken* aCookie) override;
+ IFACEMETHODIMP remove_RebootNeeded(EventRegistrationToken aCookie) override;
+ IFACEMETHODIMP add_ComponentLoadFailed(
+ ABI::Windows::Media::Protection::IComponentLoadFailedEventHandler*
+ aHandler,
+ EventRegistrationToken* aCookie) override;
+ IFACEMETHODIMP remove_ComponentLoadFailed(
+ EventRegistrationToken aCookie) override;
+ IFACEMETHODIMP get_Properties(
+ ABI::Windows::Foundation::Collections::IPropertySet** aValue) override;
+
+ HRESULT SetCDMProxy(MFCDMProxy* aCDMProxy);
+
+ MFCDMProxy* GetCDMProxy() const { return mCDMProxy; }
+
+ private:
+ HRESULT SetPMPServer(
+ ABI::Windows::Media::Protection::IMediaProtectionPMPServer* aPMPServer);
+
+ RefPtr<MFCDMProxy> mCDMProxy;
+
+ Microsoft::WRL::ComPtr<ABI::Windows::Foundation::Collections::IPropertySet>
+ mPMPServerSet;
+};
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_PLATFORM_WMF_MFCONTENTPROTECTIONMANAGER_H
diff --git a/dom/media/platforms/wmf/MFMediaEngineAudioStream.cpp b/dom/media/platforms/wmf/MFMediaEngineAudioStream.cpp
new file mode 100644
index 0000000000..4acf26e041
--- /dev/null
+++ b/dom/media/platforms/wmf/MFMediaEngineAudioStream.cpp
@@ -0,0 +1,137 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MFMediaEngineAudioStream.h"
+
+#include <mferror.h>
+#include <mfapi.h>
+
+#include "MFMediaEngineUtils.h"
+#include "WMFUtils.h"
+#include "mozilla/StaticPrefs_media.h"
+
+namespace mozilla {
+
+#define LOG(msg, ...) \
+ MOZ_LOG(gMFMediaEngineLog, LogLevel::Debug, \
+ ("MFMediaStream=%p (%s), " msg, this, \
+ this->GetDescriptionName().get(), ##__VA_ARGS__))
+
+using Microsoft::WRL::ComPtr;
+using Microsoft::WRL::MakeAndInitialize;
+
+/* static */
+MFMediaEngineAudioStream* MFMediaEngineAudioStream::Create(
+ uint64_t aStreamId, const TrackInfo& aInfo, MFMediaSource* aParentSource) {
+ MOZ_ASSERT(aInfo.IsAudio());
+ MFMediaEngineAudioStream* stream;
+ if (FAILED(MakeAndInitialize<MFMediaEngineAudioStream>(
+ &stream, aStreamId, aInfo, aParentSource))) {
+ return nullptr;
+ }
+ return stream;
+}
+
+HRESULT MFMediaEngineAudioStream::CreateMediaType(const TrackInfo& aInfo,
+ IMFMediaType** aMediaType) {
+ const AudioInfo& info = *aInfo.GetAsAudioInfo();
+ mAudioInfo = info;
+ GUID subType = AudioMimeTypeToMediaFoundationSubtype(info.mMimeType);
+ NS_ENSURE_TRUE(subType != GUID_NULL, MF_E_TOPO_CODEC_NOT_FOUND);
+
+ // https://docs.microsoft.com/en-us/windows/win32/medfound/media-type-attributes
+ ComPtr<IMFMediaType> mediaType;
+ RETURN_IF_FAILED(wmf::MFCreateMediaType(&mediaType));
+ RETURN_IF_FAILED(mediaType->SetGUID(MF_MT_SUBTYPE, subType));
+ RETURN_IF_FAILED(mediaType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio));
+ RETURN_IF_FAILED(
+ mediaType->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, info.mChannels));
+ RETURN_IF_FAILED(
+ mediaType->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, info.mRate));
+ uint64_t bitDepth = info.mBitDepth != 0 ? info.mBitDepth : 16;
+ RETURN_IF_FAILED(mediaType->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, bitDepth));
+ if (subType == MFAudioFormat_AAC) {
+ if (mAACUserData.IsEmpty()) {
+ MOZ_ASSERT(info.mCodecSpecificConfig.is<AacCodecSpecificData>() ||
+ info.mCodecSpecificConfig.is<AudioCodecSpecificBinaryBlob>());
+ RefPtr<MediaByteBuffer> blob;
+ if (info.mCodecSpecificConfig.is<AacCodecSpecificData>()) {
+ blob = info.mCodecSpecificConfig.as<AacCodecSpecificData>()
+ .mDecoderConfigDescriptorBinaryBlob;
+ } else {
+ blob = info.mCodecSpecificConfig.as<AudioCodecSpecificBinaryBlob>()
+ .mBinaryBlob;
+ }
+ AACAudioSpecificConfigToUserData(info.mExtendedProfile, blob->Elements(),
+ blob->Length(), mAACUserData);
+ LOG("Generated AAC user data");
+ }
+ RETURN_IF_FAILED(
+ mediaType->SetUINT32(MF_MT_AAC_PAYLOAD_TYPE, 0x0)); // Raw AAC packet
+ RETURN_IF_FAILED(mediaType->SetBlob(
+ MF_MT_USER_DATA, mAACUserData.Elements(), mAACUserData.Length()));
+ }
+ LOG("Created audio type, subtype=%s, channel=%" PRIu32 ", rate=%" PRIu32
+ ", bitDepth=%" PRIu64 ", encrypted=%d",
+ GUIDToStr(subType), info.mChannels, info.mRate, bitDepth,
+ mAudioInfo.mCrypto.IsEncrypted());
+
+ if (IsEncrypted()) {
+ ComPtr<IMFMediaType> protectedMediaType;
+ RETURN_IF_FAILED(wmf::MFWrapMediaType(mediaType.Get(),
+ MFMediaType_Protected, subType,
+ protectedMediaType.GetAddressOf()));
+ LOG("Wrap MFMediaType_Audio into MFMediaType_Protected");
+ *aMediaType = protectedMediaType.Detach();
+ } else {
+ *aMediaType = mediaType.Detach();
+ }
+ return S_OK;
+}
+
+bool MFMediaEngineAudioStream::HasEnoughRawData() const {
+ // If more than this much raw audio is queued, we'll hold off request more
+ // audio.
+ return mRawDataQueueForFeedingEngine.Duration() >=
+ StaticPrefs::media_wmf_media_engine_raw_data_threshold_audio();
+}
+
+already_AddRefed<MediaData> MFMediaEngineAudioStream::OutputDataInternal() {
+ AssertOnTaskQueue();
+ if (mRawDataQueueForGeneratingOutput.GetSize() == 0) {
+ return nullptr;
+ }
+ // The media engine doesn't provide a way to allow us to access decoded audio
+ // frames, and the audio playback will be handled internally inside the media
+ // engine. So we simply return fake audio data.
+ RefPtr<MediaRawData> input = mRawDataQueueForGeneratingOutput.PopFront();
+ RefPtr<MediaData> output =
+ new AudioData(input->mOffset, input->mTime, AlignedAudioBuffer{},
+ mAudioInfo.mChannels, mAudioInfo.mRate);
+ return output.forget();
+}
+
+nsCString MFMediaEngineAudioStream::GetCodecName() const {
+ WMFStreamType type = GetStreamTypeFromMimeType(mAudioInfo.mMimeType);
+ switch (type) {
+ case WMFStreamType::MP3:
+ return "mp3"_ns;
+ case WMFStreamType::AAC:
+ return "aac"_ns;
+ case WMFStreamType::OPUS:
+ return "opus"_ns;
+ case WMFStreamType::VORBIS:
+ return "vorbis"_ns;
+ default:
+ return "unknown"_ns;
+ }
+}
+
+bool MFMediaEngineAudioStream::IsEncrypted() const {
+ return mAudioInfo.mCrypto.IsEncrypted();
+}
+
+#undef LOG
+
+} // namespace mozilla
diff --git a/dom/media/platforms/wmf/MFMediaEngineAudioStream.h b/dom/media/platforms/wmf/MFMediaEngineAudioStream.h
new file mode 100644
index 0000000000..14a72b9f63
--- /dev/null
+++ b/dom/media/platforms/wmf/MFMediaEngineAudioStream.h
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_PLATFORM_WMF_MFMEDIAENGINEAUDIOSTREAM_H
+#define DOM_MEDIA_PLATFORM_WMF_MFMEDIAENGINEAUDIOSTREAM_H
+
+#include "MFMediaEngineStream.h"
+
+namespace mozilla {
+
+class MFMediaSource;
+
+class MFMediaEngineAudioStream final : public MFMediaEngineStream {
+ public:
+ MFMediaEngineAudioStream() = default;
+
+ static MFMediaEngineAudioStream* Create(uint64_t aStreamId,
+ const TrackInfo& aInfo,
+ MFMediaSource* aParentSource);
+
+ nsCString GetDescriptionName() const override {
+ return "media engine audio stream"_ns;
+ }
+
+ nsCString GetCodecName() const override;
+
+ TrackInfo::TrackType TrackType() override {
+ return TrackInfo::TrackType::kAudioTrack;
+ }
+
+ bool IsEncrypted() const override;
+
+ private:
+ HRESULT CreateMediaType(const TrackInfo& aInfo,
+ IMFMediaType** aMediaType) override;
+
+ bool HasEnoughRawData() const override;
+
+ already_AddRefed<MediaData> OutputDataInternal() override;
+
+ // For MF_MT_USER_DATA. Currently only used for AAC.
+ nsTArray<BYTE> mAACUserData;
+
+ // Set when `CreateMediaType()` is called.
+ AudioInfo mAudioInfo;
+};
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_PLATFORM_WMF_MFMEDIAENGINEAUDIOSTREAM_H
diff --git a/dom/media/platforms/wmf/MFMediaEngineDecoderModule.cpp b/dom/media/platforms/wmf/MFMediaEngineDecoderModule.cpp
new file mode 100644
index 0000000000..13be162af5
--- /dev/null
+++ b/dom/media/platforms/wmf/MFMediaEngineDecoderModule.cpp
@@ -0,0 +1,174 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MFMediaEngineDecoderModule.h"
+
+#include "MFTDecoder.h"
+#include "VideoUtils.h"
+#include "mozilla/MFMediaEngineParent.h"
+#include "mozilla/MFMediaEngineUtils.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/WindowsVersion.h"
+#include "mozilla/mscom/EnsureMTA.h"
+
+namespace mozilla {
+
+#define LOG(msg, ...) \
+ MOZ_LOG(gMFMediaEngineLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
+
+/* static */
+void MFMediaEngineDecoderModule::Init() {
+ // TODO : Init any thing that media engine would need. Implement this when we
+ // start implementing media engine in following patches.
+}
+
+/* static */
+already_AddRefed<PlatformDecoderModule> MFMediaEngineDecoderModule::Create() {
+ RefPtr<MFMediaEngineDecoderModule> module = new MFMediaEngineDecoderModule();
+ return module.forget();
+}
+
+/* static */
+bool MFMediaEngineDecoderModule::SupportsConfig(const TrackInfo& aConfig) {
+ RefPtr<MFMediaEngineDecoderModule> module = new MFMediaEngineDecoderModule();
+ return module->SupportInternal(SupportDecoderParams(aConfig), nullptr) !=
+ media::DecodeSupport::Unsupported;
+}
+
+already_AddRefed<MediaDataDecoder>
+MFMediaEngineDecoderModule::CreateVideoDecoder(
+ const CreateDecoderParams& aParams) {
+ if (!aParams.mMediaEngineId ||
+ !StaticPrefs::media_wmf_media_engine_enabled()) {
+ return nullptr;
+ }
+ RefPtr<MFMediaEngineParent> mediaEngine =
+ MFMediaEngineParent::GetMediaEngineById(*aParams.mMediaEngineId);
+ if (!mediaEngine) {
+ LOG("Can't find media engine %" PRIu64 " for video decoder",
+ *aParams.mMediaEngineId);
+ return nullptr;
+ }
+ LOG("MFMediaEngineDecoderModule, CreateVideoDecoder");
+ RefPtr<MediaDataDecoder> decoder = mediaEngine->GetMediaEngineStream(
+ TrackInfo::TrackType::kVideoTrack, aParams);
+ return decoder.forget();
+}
+
+already_AddRefed<MediaDataDecoder>
+MFMediaEngineDecoderModule::CreateAudioDecoder(
+ const CreateDecoderParams& aParams) {
+ if (!aParams.mMediaEngineId ||
+ !StaticPrefs::media_wmf_media_engine_enabled()) {
+ return nullptr;
+ }
+ RefPtr<MFMediaEngineParent> mediaEngine =
+ MFMediaEngineParent::GetMediaEngineById(*aParams.mMediaEngineId);
+ if (!mediaEngine) {
+ LOG("Can't find media engine %" PRIu64 " for audio decoder",
+ *aParams.mMediaEngineId);
+ return nullptr;
+ }
+ LOG("MFMediaEngineDecoderModule, CreateAudioDecoder");
+ RefPtr<MediaDataDecoder> decoder = mediaEngine->GetMediaEngineStream(
+ TrackInfo::TrackType::kAudioTrack, aParams);
+ return decoder.forget();
+}
+
+media::DecodeSupportSet MFMediaEngineDecoderModule::SupportsMimeType(
+ const nsACString& aMimeType, DecoderDoctorDiagnostics* aDiagnostics) const {
+ UniquePtr<TrackInfo> trackInfo = CreateTrackInfoWithMIMEType(aMimeType);
+ if (!trackInfo) {
+ return media::DecodeSupport::Unsupported;
+ }
+ return SupportInternal(SupportDecoderParams(*trackInfo), aDiagnostics);
+}
+
+media::DecodeSupportSet MFMediaEngineDecoderModule::Supports(
+ const SupportDecoderParams& aParams,
+ DecoderDoctorDiagnostics* aDiagnostics) const {
+ if (!aParams.mMediaEngineId) {
+ return media::DecodeSupport::Unsupported;
+ }
+ return SupportInternal(aParams, aDiagnostics);
+}
+
+media::DecodeSupportSet MFMediaEngineDecoderModule::SupportInternal(
+ const SupportDecoderParams& aParams,
+ DecoderDoctorDiagnostics* aDiagnostics) const {
+ if (!StaticPrefs::media_wmf_media_engine_enabled()) {
+ return media::DecodeSupport::Unsupported;
+ }
+ bool supports = false;
+ WMFStreamType type = GetStreamTypeFromMimeType(aParams.MimeType());
+ if (type != WMFStreamType::Unknown) {
+ supports = CanCreateMFTDecoder(type);
+ }
+ MOZ_LOG(sPDMLog, LogLevel::Debug,
+ ("MFMediaEngine decoder %s requested type '%s'",
+ supports ? "supports" : "rejects", aParams.MimeType().get()));
+ return supports ? media::DecodeSupport::SoftwareDecode
+ : media::DecodeSupport::Unsupported;
+}
+
+static bool CreateMFTDecoderOnMTA(const WMFStreamType& aType) {
+ RefPtr<MFTDecoder> decoder = new MFTDecoder();
+ static std::unordered_map<WMFStreamType, bool> sResults;
+ if (auto rv = sResults.find(aType); rv != sResults.end()) {
+ return rv->second;
+ }
+
+ bool result = false;
+ switch (aType) {
+ case WMFStreamType::MP3:
+ result = SUCCEEDED(decoder->Create(CLSID_CMP3DecMediaObject));
+ break;
+ case WMFStreamType::AAC:
+ result = SUCCEEDED(decoder->Create(CLSID_CMSAACDecMFT));
+ break;
+ // Opus and vorbis are supported via extension.
+ // https://www.microsoft.com/en-us/p/web-media-extensions/9n5tdp8vcmhs
+ case WMFStreamType::OPUS:
+ result = SUCCEEDED(decoder->Create(CLSID_MSOpusDecoder));
+ break;
+ case WMFStreamType::VORBIS:
+ result = SUCCEEDED(decoder->Create(
+ MFT_CATEGORY_AUDIO_DECODER, MFAudioFormat_Vorbis, MFAudioFormat_PCM));
+ break;
+ case WMFStreamType::H264:
+ result = SUCCEEDED(decoder->Create(CLSID_CMSH264DecoderMFT));
+ break;
+ case WMFStreamType::VP8:
+ case WMFStreamType::VP9: {
+ static const uint32_t VPX_USABLE_BUILD = 16287;
+ if (IsWindowsBuildOrLater(VPX_USABLE_BUILD)) {
+ result = SUCCEEDED(decoder->Create(CLSID_CMSVPXDecMFT));
+ }
+ break;
+ }
+#ifdef MOZ_AV1
+ case WMFStreamType::AV1:
+ result = SUCCEEDED(decoder->Create(MFT_CATEGORY_VIDEO_DECODER,
+ MFVideoFormat_AV1, GUID_NULL));
+ break;
+#endif
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unexpected type");
+ }
+ sResults.insert({aType, result});
+ return result;
+}
+
+bool MFMediaEngineDecoderModule::CanCreateMFTDecoder(
+ const WMFStreamType& aType) const {
+ // TODO : caching the result to prevent performing on MTA thread everytime.
+ bool canCreateDecoder = false;
+ mozilla::mscom::EnsureMTA(
+ [&]() { canCreateDecoder = CreateMFTDecoderOnMTA(aType); });
+ return canCreateDecoder;
+}
+
+#undef LOG
+
+} // namespace mozilla
diff --git a/dom/media/platforms/wmf/MFMediaEngineDecoderModule.h b/dom/media/platforms/wmf/MFMediaEngineDecoderModule.h
new file mode 100644
index 0000000000..97a434fca8
--- /dev/null
+++ b/dom/media/platforms/wmf/MFMediaEngineDecoderModule.h
@@ -0,0 +1,45 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_PLATFORM_WMF_MFMEDIAENGINEDECODERMODULE_H
+#define DOM_MEDIA_PLATFORM_WMF_MFMEDIAENGINEDECODERMODULE_H
+
+#include "PlatformDecoderModule.h"
+#include "WMFUtils.h"
+
+namespace mozilla {
+
+class MFMediaEngineDecoderModule final : public PlatformDecoderModule {
+ public:
+ static void Init();
+
+ static already_AddRefed<PlatformDecoderModule> Create();
+
+ static bool SupportsConfig(const TrackInfo& aConfig);
+
+ already_AddRefed<MediaDataDecoder> CreateVideoDecoder(
+ const CreateDecoderParams& aParams) override;
+
+ already_AddRefed<MediaDataDecoder> CreateAudioDecoder(
+ const CreateDecoderParams& aParams) override;
+
+ media::DecodeSupportSet SupportsMimeType(
+ const nsACString& aMimeType,
+ DecoderDoctorDiagnostics* aDiagnostics) const override;
+ media::DecodeSupportSet Supports(
+ const SupportDecoderParams& aParams,
+ DecoderDoctorDiagnostics* aDiagnostics) const override;
+
+ private:
+ media::DecodeSupportSet SupportInternal(
+ const SupportDecoderParams& aParams,
+ DecoderDoctorDiagnostics* aDiagnostics) const;
+ bool CanCreateMFTDecoder(const WMFStreamType& aType) const;
+ MFMediaEngineDecoderModule() = default;
+ ~MFMediaEngineDecoderModule() = default;
+};
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_PLATFORM_WMF_MFMEDIAENGINEDECODERMODULE_H
diff --git a/dom/media/platforms/wmf/MFMediaEngineExtension.cpp b/dom/media/platforms/wmf/MFMediaEngineExtension.cpp
new file mode 100644
index 0000000000..eb761da364
--- /dev/null
+++ b/dom/media/platforms/wmf/MFMediaEngineExtension.cpp
@@ -0,0 +1,88 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MFMediaEngineExtension.h"
+
+#include <mfapi.h>
+#include <mferror.h>
+
+#include "MFMediaSource.h"
+#include "MFMediaEngineUtils.h"
+#include "WMF.h"
+
+namespace mozilla {
+
+#define LOG(msg, ...) \
+ MOZ_LOG(gMFMediaEngineLog, LogLevel::Debug, \
+ ("MFMediaEngineExtension=%p, " msg, this, ##__VA_ARGS__))
+
+using Microsoft::WRL::ComPtr;
+
+void MFMediaEngineExtension::SetMediaSource(IMFMediaSource* aMediaSource) {
+ LOG("SetMediaSource=%p", aMediaSource);
+ mMediaSource = aMediaSource;
+}
+
+// https://docs.microsoft.com/en-us/windows/win32/api/mfmediaengine/nf-mfmediaengine-imfmediaengineextension-begincreateobject
+IFACEMETHODIMP MFMediaEngineExtension::BeginCreateObject(
+ BSTR aUrl, IMFByteStream* aByteStream, MF_OBJECT_TYPE aType,
+ IUnknown** aCancelCookie, IMFAsyncCallback* aCallback, IUnknown* aState) {
+ if (aCancelCookie) {
+ // We don't support a cancel cookie.
+ *aCancelCookie = nullptr;
+ }
+
+ if (aType != MF_OBJECT_MEDIASOURCE) {
+ LOG("Only support media source type");
+ return MF_E_UNEXPECTED;
+ }
+
+ MOZ_ASSERT(mMediaSource);
+ ComPtr<IMFAsyncResult> result;
+ ComPtr<IUnknown> sourceUnknown = mMediaSource;
+ RETURN_IF_FAILED(wmf::MFCreateAsyncResult(sourceUnknown.Get(), aCallback,
+ aState, &result));
+ RETURN_IF_FAILED(result->SetStatus(S_OK));
+
+ LOG("Creating object");
+ mIsObjectCreating = true;
+
+ RETURN_IF_FAILED(aCallback->Invoke(result.Get()));
+ return S_OK;
+}
+
+IFACEMETHODIMP MFMediaEngineExtension::CancelObjectCreation(
+ IUnknown* aCancelCookie) {
+ return MF_E_UNEXPECTED;
+}
+
+IFACEMETHODIMP MFMediaEngineExtension::EndCreateObject(IMFAsyncResult* aResult,
+ IUnknown** aRetObj) {
+ *aRetObj = nullptr;
+ if (!mIsObjectCreating) {
+ LOG("No object is creating, not an expected call");
+ return MF_E_UNEXPECTED;
+ }
+
+ RETURN_IF_FAILED(aResult->GetStatus());
+ RETURN_IF_FAILED(aResult->GetObject(aRetObj));
+
+ LOG("End of creating object");
+ mIsObjectCreating = false;
+ return S_OK;
+}
+
+IFACEMETHODIMP MFMediaEngineExtension::CanPlayType(
+ BOOL aIsAudioOnly, BSTR aMimeType, MF_MEDIA_ENGINE_CANPLAY* aResult) {
+ // We use MF_MEDIA_ENGINE_EXTENSION to resolve as custom media source for
+ // MFMediaEngine, MIME types are not used.
+ *aResult = MF_MEDIA_ENGINE_CANPLAY_NOT_SUPPORTED;
+ return S_OK;
+}
+
+// TODO : break cycle of mMediaSource
+
+#undef LOG
+
+} // namespace mozilla
diff --git a/dom/media/platforms/wmf/MFMediaEngineExtension.h b/dom/media/platforms/wmf/MFMediaEngineExtension.h
new file mode 100644
index 0000000000..e6b9dde96d
--- /dev/null
+++ b/dom/media/platforms/wmf/MFMediaEngineExtension.h
@@ -0,0 +1,49 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_PLATFORM_WMF_MFMEDIAENGINEEXTENSION_H
+#define DOM_MEDIA_PLATFORM_WMF_MFMEDIAENGINEEXTENSION_H
+
+#include <wrl.h>
+
+#include "MFMediaEngineExtra.h"
+
+namespace mozilla {
+
+/**
+ * MFMediaEngineNotify is used to load media resources in the media engine.
+ * https://docs.microsoft.com/en-us/windows/win32/api/mfmediaengine/nn-mfmediaengine-imfmediaengineextension
+ */
+class MFMediaEngineExtension final
+ : public Microsoft::WRL::RuntimeClass<
+ Microsoft::WRL::RuntimeClassFlags<
+ Microsoft::WRL::RuntimeClassType::ClassicCom>,
+ IMFMediaEngineExtension> {
+ public:
+ MFMediaEngineExtension() = default;
+
+ HRESULT RuntimeClassInitialize() { return S_OK; }
+
+ void SetMediaSource(IMFMediaSource* aMediaSource);
+
+ // Method for MFMediaEngineExtension
+ IFACEMETHODIMP BeginCreateObject(BSTR aUrl, IMFByteStream* aByteStream,
+ MF_OBJECT_TYPE aType,
+ IUnknown** aCancelCookie,
+ IMFAsyncCallback* aCallback,
+ IUnknown* aState) override;
+ IFACEMETHODIMP CancelObjectCreation(IUnknown* aCancelCookie) override;
+ IFACEMETHODIMP EndCreateObject(IMFAsyncResult* aResult,
+ IUnknown** aRetObj) override;
+ IFACEMETHODIMP CanPlayType(BOOL aIsAudioOnly, BSTR aMimeType,
+ MF_MEDIA_ENGINE_CANPLAY* aResult) override;
+
+ private:
+ bool mIsObjectCreating = false;
+ Microsoft::WRL::ComPtr<IMFMediaSource> mMediaSource;
+};
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_PLATFORM_WMF_MFMEDIAENGINEEXTENSION_H
diff --git a/dom/media/platforms/wmf/MFMediaEngineExtra.h b/dom/media/platforms/wmf/MFMediaEngineExtra.h
new file mode 100644
index 0000000000..238db9e238
--- /dev/null
+++ b/dom/media/platforms/wmf/MFMediaEngineExtra.h
@@ -0,0 +1,715 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_PLATFORM_WMF_MFMEDIAENGINEEXTRA_H
+#define DOM_MEDIA_PLATFORM_WMF_MFMEDIAENGINEEXTRA_H
+
+#include <evr.h>
+#include <mfmediaengine.h>
+
+// Currently, we build with WINVER=0x601 (Win7), which means newer
+// declarations in mfmediaengine.h will not be visible. Also, we don't
+// yet have the Fall Creators Update SDK available on build machines,
+// so even with updated WINVER, some of the interfaces we need would
+// not be present.
+// To work around this, until the build environment is updated,
+// we include copies of the relevant classes/interfaces we need.
+#if !defined(WINVER) || WINVER < 0x0602
+
+# define WS_EX_NOREDIRECTIONBITMAP 0x00200000L
+
+EXTERN_GUID(MF_MEDIA_ENGINE_CALLBACK, 0xc60381b8, 0x83a4, 0x41f8, 0xa3, 0xd0,
+ 0xde, 0x05, 0x07, 0x68, 0x49, 0xa9);
+EXTERN_GUID(MF_MEDIA_ENGINE_DXGI_MANAGER, 0x065702da, 0x1094, 0x486d, 0x86,
+ 0x17, 0xee, 0x7c, 0xc4, 0xee, 0x46, 0x48);
+EXTERN_GUID(MF_MEDIA_ENGINE_EXTENSION, 0x3109fd46, 0x060d, 0x4b62, 0x8d, 0xcf,
+ 0xfa, 0xff, 0x81, 0x13, 0x18, 0xd2);
+EXTERN_GUID(MF_MEDIA_ENGINE_PLAYBACK_HWND, 0xd988879b, 0x67c9, 0x4d92, 0xba,
+ 0xa7, 0x6e, 0xad, 0xd4, 0x46, 0x03, 0x9d);
+EXTERN_GUID(MF_MEDIA_ENGINE_OPM_HWND, 0xa0be8ee7, 0x0572, 0x4f2c, 0xa8, 0x01,
+ 0x2a, 0x15, 0x1b, 0xd3, 0xe7, 0x26);
+EXTERN_GUID(MF_MEDIA_ENGINE_PLAYBACK_VISUAL, 0x6debd26f, 0x6ab9, 0x4d7e, 0xb0,
+ 0xee, 0xc6, 0x1a, 0x73, 0xff, 0xad, 0x15);
+EXTERN_GUID(MF_MEDIA_ENGINE_COREWINDOW, 0xfccae4dc, 0x0b7f, 0x41c2, 0x9f, 0x96,
+ 0x46, 0x59, 0x94, 0x8a, 0xcd, 0xdc);
+EXTERN_GUID(MF_MEDIA_ENGINE_VIDEO_OUTPUT_FORMAT, 0x5066893c, 0x8cf9, 0x42bc,
+ 0x8b, 0x8a, 0x47, 0x22, 0x12, 0xe5, 0x27, 0x26);
+EXTERN_GUID(MF_MEDIA_ENGINE_CONTENT_PROTECTION_FLAGS, 0xe0350223, 0x5aaf,
+ 0x4d76, 0xa7, 0xc3, 0x06, 0xde, 0x70, 0x89, 0x4d, 0xb4);
+EXTERN_GUID(MF_MEDIA_ENGINE_CONTENT_PROTECTION_MANAGER, 0xfdd6dfaa, 0xbd85,
+ 0x4af3, 0x9e, 0x0f, 0xa0, 0x1d, 0x53, 0x9d, 0x87, 0x6a);
+EXTERN_GUID(MF_MEDIA_ENGINE_AUDIO_ENDPOINT_ROLE, 0xd2cb93d1, 0x116a, 0x44f2,
+ 0x93, 0x85, 0xf7, 0xd0, 0xfd, 0xa2, 0xfb, 0x46);
+EXTERN_GUID(MF_MEDIA_ENGINE_AUDIO_CATEGORY, 0xc8d4c51d, 0x350e, 0x41f2, 0xba,
+ 0x46, 0xfa, 0xeb, 0xbb, 0x08, 0x57, 0xf6);
+EXTERN_GUID(MF_MEDIA_ENGINE_STREAM_CONTAINS_ALPHA_CHANNEL, 0x5cbfaf44, 0xd2b2,
+ 0x4cfb, 0x80, 0xa7, 0xd4, 0x29, 0xc7, 0x4c, 0x78, 0x9d);
+EXTERN_GUID(MF_MEDIA_ENGINE_BROWSER_COMPATIBILITY_MODE, 0x4e0212e2, 0xe18f,
+ 0x41e1, 0x95, 0xe5, 0xc0, 0xe7, 0xe9, 0x23, 0x5b, 0xc3);
+EXTERN_GUID(MF_MEDIA_ENGINE_BROWSER_COMPATIBILITY_MODE_IE9, 0x052c2d39, 0x40c0,
+ 0x4188, 0xab, 0x86, 0xf8, 0x28, 0x27, 0x3b, 0x75, 0x22);
+EXTERN_GUID(MF_MEDIA_ENGINE_BROWSER_COMPATIBILITY_MODE_IE10, 0x11a47afd, 0x6589,
+ 0x4124, 0xb3, 0x12, 0x61, 0x58, 0xec, 0x51, 0x7f, 0xc3);
+EXTERN_GUID(MF_MEDIA_ENGINE_BROWSER_COMPATIBILITY_MODE_IE11, 0x1cf1315f, 0xce3f,
+ 0x4035, 0x93, 0x91, 0x16, 0x14, 0x2f, 0x77, 0x51, 0x89);
+EXTERN_GUID(MF_MEDIA_ENGINE_BROWSER_COMPATIBILITY_MODE_IE_EDGE, 0xa6f3e465,
+ 0x3aca, 0x442c, 0xa3, 0xf0, 0xad, 0x6d, 0xda, 0xd8, 0x39, 0xae);
+EXTERN_GUID(MF_MEDIA_ENGINE_COMPATIBILITY_MODE, 0x3ef26ad4, 0xdc54, 0x45de,
+ 0xb9, 0xaf, 0x76, 0xc8, 0xc6, 0x6b, 0xfa, 0x8e);
+EXTERN_GUID(MF_MEDIA_ENGINE_COMPATIBILITY_MODE_WWA_EDGE, 0x15b29098, 0x9f01,
+ 0x4e4d, 0xb6, 0x5a, 0xc0, 0x6c, 0x6c, 0x89, 0xda, 0x2a);
+EXTERN_GUID(MF_MEDIA_ENGINE_COMPATIBILITY_MODE_WIN10, 0x5b25e089, 0x6ca7,
+ 0x4139, 0xa2, 0xcb, 0xfc, 0xaa, 0xb3, 0x95, 0x52, 0xa3);
+EXTERN_GUID(MF_MEDIA_ENGINE_SOURCE_RESOLVER_CONFIG_STORE, 0x0ac0c497, 0xb3c4,
+ 0x48c9, 0x9c, 0xde, 0xbb, 0x8c, 0xa2, 0x44, 0x2c, 0xa3);
+EXTERN_GUID(MF_MEDIA_ENGINE_TRACK_ID, 0x65bea312, 0x4043, 0x4815, 0x8e, 0xab,
+ 0x44, 0xdc, 0xe2, 0xef, 0x8f, 0x2a);
+EXTERN_GUID(MF_MEDIA_ENGINE_TELEMETRY_APPLICATION_ID, 0x1e7b273b, 0xa7e4,
+ 0x402a, 0x8f, 0x51, 0xc4, 0x8e, 0x88, 0xa2, 0xca, 0xbc);
+EXTERN_GUID(MF_MEDIA_ENGINE_SYNCHRONOUS_CLOSE, 0xc3c2e12f, 0x7e0e, 0x4e43, 0xb9,
+ 0x1c, 0xdc, 0x99, 0x2c, 0xcd, 0xfa, 0x5e);
+EXTERN_GUID(MF_MEDIA_ENGINE_MEDIA_PLAYER_MODE, 0x3ddd8d45, 0x5aa1, 0x4112, 0x82,
+ 0xe5, 0x36, 0xf6, 0xa2, 0x19, 0x7e, 0x6e);
+EXTERN_GUID(CLSID_MFMediaEngineClassFactory, 0xb44392da, 0x499b, 0x446b, 0xa4,
+ 0xcb, 0x0, 0x5f, 0xea, 0xd0, 0xe6, 0xd5);
+EXTERN_GUID(MF_MT_VIDEO_ROTATION, 0xc380465d, 0x2271, 0x428c, 0x9b, 0x83, 0xec,
+ 0xea, 0x3b, 0x4a, 0x85, 0xc1);
+
+typedef enum _MFVideoRotationFormat {
+ MFVideoRotationFormat_0 = 0,
+ MFVideoRotationFormat_90 = 90,
+ MFVideoRotationFormat_180 = 180,
+ MFVideoRotationFormat_270 = 270
+} MFVideoRotationFormat;
+
+typedef enum MF_MEDIA_ENGINE_EVENT {
+ MF_MEDIA_ENGINE_EVENT_LOADSTART = 1,
+ MF_MEDIA_ENGINE_EVENT_PROGRESS = 2,
+ MF_MEDIA_ENGINE_EVENT_SUSPEND = 3,
+ MF_MEDIA_ENGINE_EVENT_ABORT = 4,
+ MF_MEDIA_ENGINE_EVENT_ERROR = 5,
+ MF_MEDIA_ENGINE_EVENT_EMPTIED = 6,
+ MF_MEDIA_ENGINE_EVENT_STALLED = 7,
+ MF_MEDIA_ENGINE_EVENT_PLAY = 8,
+ MF_MEDIA_ENGINE_EVENT_PAUSE = 9,
+ MF_MEDIA_ENGINE_EVENT_LOADEDMETADATA = 10,
+ MF_MEDIA_ENGINE_EVENT_LOADEDDATA = 11,
+ MF_MEDIA_ENGINE_EVENT_WAITING = 12,
+ MF_MEDIA_ENGINE_EVENT_PLAYING = 13,
+ MF_MEDIA_ENGINE_EVENT_CANPLAY = 14,
+ MF_MEDIA_ENGINE_EVENT_CANPLAYTHROUGH = 15,
+ MF_MEDIA_ENGINE_EVENT_SEEKING = 16,
+ MF_MEDIA_ENGINE_EVENT_SEEKED = 17,
+ MF_MEDIA_ENGINE_EVENT_TIMEUPDATE = 18,
+ MF_MEDIA_ENGINE_EVENT_ENDED = 19,
+ MF_MEDIA_ENGINE_EVENT_RATECHANGE = 20,
+ MF_MEDIA_ENGINE_EVENT_DURATIONCHANGE = 21,
+ MF_MEDIA_ENGINE_EVENT_VOLUMECHANGE = 22,
+ MF_MEDIA_ENGINE_EVENT_FORMATCHANGE = 1000,
+ MF_MEDIA_ENGINE_EVENT_PURGEQUEUEDEVENTS = 1001,
+ MF_MEDIA_ENGINE_EVENT_TIMELINE_MARKER = 1002,
+ MF_MEDIA_ENGINE_EVENT_BALANCECHANGE = 1003,
+ MF_MEDIA_ENGINE_EVENT_DOWNLOADCOMPLETE = 1004,
+ MF_MEDIA_ENGINE_EVENT_BUFFERINGSTARTED = 1005,
+ MF_MEDIA_ENGINE_EVENT_BUFFERINGENDED = 1006,
+ MF_MEDIA_ENGINE_EVENT_FRAMESTEPCOMPLETED = 1007,
+ MF_MEDIA_ENGINE_EVENT_NOTIFYSTABLESTATE = 1008,
+ MF_MEDIA_ENGINE_EVENT_FIRSTFRAMEREADY = 1009,
+ MF_MEDIA_ENGINE_EVENT_TRACKSCHANGE = 1010,
+ MF_MEDIA_ENGINE_EVENT_OPMINFO = 1011,
+ MF_MEDIA_ENGINE_EVENT_RESOURCELOST = 1012,
+ MF_MEDIA_ENGINE_EVENT_DELAYLOADEVENT_CHANGED = 1013,
+ MF_MEDIA_ENGINE_EVENT_STREAMRENDERINGERROR = 1014,
+ MF_MEDIA_ENGINE_EVENT_SUPPORTEDRATES_CHANGED = 1015,
+ MF_MEDIA_ENGINE_EVENT_AUDIOENDPOINTCHANGE = 1016
+} MF_MEDIA_ENGINE_EVENT;
+
+typedef enum MF_MEDIA_ENGINE_PROTECTION_FLAGS {
+ MF_MEDIA_ENGINE_ENABLE_PROTECTED_CONTENT = 1,
+ MF_MEDIA_ENGINE_USE_PMP_FOR_ALL_CONTENT = 2,
+ MF_MEDIA_ENGINE_USE_UNPROTECTED_PMP = 4
+} MF_MEDIA_ENGINE_PROTECTION_FLAGS;
+
+typedef enum MF_MEDIA_ENGINE_CREATEFLAGS {
+ MF_MEDIA_ENGINE_AUDIOONLY = 0x1,
+ MF_MEDIA_ENGINE_WAITFORSTABLE_STATE = 0x2,
+ MF_MEDIA_ENGINE_FORCEMUTE = 0x4,
+ MF_MEDIA_ENGINE_REAL_TIME_MODE = 0x8,
+ MF_MEDIA_ENGINE_DISABLE_LOCAL_PLUGINS = 0x10,
+ MF_MEDIA_ENGINE_CREATEFLAGS_MASK = 0x1f
+} MF_MEDIA_ENGINE_CREATEFLAGS;
+
+typedef enum MF_MEDIA_ENGINE_S3D_PACKING_MODE {
+ MF_MEDIA_ENGINE_S3D_PACKING_MODE_NONE = 0,
+ MF_MEDIA_ENGINE_S3D_PACKING_MODE_SIDE_BY_SIDE = 1,
+ MF_MEDIA_ENGINE_S3D_PACKING_MODE_TOP_BOTTOM = 2
+} MF_MEDIA_ENGINE_S3D_PACKING_MODE;
+
+typedef enum MF_MEDIA_ENGINE_STATISTIC {
+ MF_MEDIA_ENGINE_STATISTIC_FRAMES_RENDERED = 0,
+ MF_MEDIA_ENGINE_STATISTIC_FRAMES_DROPPED = 1,
+ MF_MEDIA_ENGINE_STATISTIC_BYTES_DOWNLOADED = 2,
+ MF_MEDIA_ENGINE_STATISTIC_BUFFER_PROGRESS = 3,
+ MF_MEDIA_ENGINE_STATISTIC_FRAMES_PER_SECOND = 4,
+ MF_MEDIA_ENGINE_STATISTIC_PLAYBACK_JITTER = 5,
+ MF_MEDIA_ENGINE_STATISTIC_FRAMES_CORRUPTED = 6,
+ MF_MEDIA_ENGINE_STATISTIC_TOTAL_FRAME_DELAY = 7
+} MF_MEDIA_ENGINE_STATISTIC;
+
+typedef enum MF_MEDIA_ENGINE_SEEK_MODE {
+ MF_MEDIA_ENGINE_SEEK_MODE_NORMAL = 0,
+ MF_MEDIA_ENGINE_SEEK_MODE_APPROXIMATE = 1
+} MF_MEDIA_ENGINE_SEEK_MODE;
+
+typedef enum MF_MEDIA_ENGINE_ERR {
+ MF_MEDIA_ENGINE_ERR_NOERROR = 0,
+ MF_MEDIA_ENGINE_ERR_ABORTED = 1,
+ MF_MEDIA_ENGINE_ERR_NETWORK = 2,
+ MF_MEDIA_ENGINE_ERR_DECODE = 3,
+ MF_MEDIA_ENGINE_ERR_SRC_NOT_SUPPORTED = 4,
+ MF_MEDIA_ENGINE_ERR_ENCRYPTED = 5
+} MF_MEDIA_ENGINE_ERR;
+
+typedef enum MF_MEDIA_ENGINE_NETWORK {
+ MF_MEDIA_ENGINE_NETWORK_EMPTY = 0,
+ MF_MEDIA_ENGINE_NETWORK_IDLE = 1,
+ MF_MEDIA_ENGINE_NETWORK_LOADING = 2,
+ MF_MEDIA_ENGINE_NETWORK_NO_SOURCE = 3
+} MF_MEDIA_ENGINE_NETWORK;
+
+typedef enum MF_MEDIA_ENGINE_READY {
+ MF_MEDIA_ENGINE_READY_HAVE_NOTHING = 0,
+ MF_MEDIA_ENGINE_READY_HAVE_METADATA = 1,
+ MF_MEDIA_ENGINE_READY_HAVE_CURRENT_DATA = 2,
+ MF_MEDIA_ENGINE_READY_HAVE_FUTURE_DATA = 3,
+ MF_MEDIA_ENGINE_READY_HAVE_ENOUGH_DATA = 4
+} MF_MEDIA_ENGINE_READY;
+
+typedef enum MF_MEDIA_ENGINE_CANPLAY {
+ MF_MEDIA_ENGINE_CANPLAY_NOT_SUPPORTED = 0,
+ MF_MEDIA_ENGINE_CANPLAY_MAYBE = 1,
+ MF_MEDIA_ENGINE_CANPLAY_PROBABLY = 2
+} MF_MEDIA_ENGINE_CANPLAY;
+
+typedef enum MF_MEDIA_ENGINE_PRELOAD {
+ MF_MEDIA_ENGINE_PRELOAD_MISSING = 0,
+ MF_MEDIA_ENGINE_PRELOAD_EMPTY = 1,
+ MF_MEDIA_ENGINE_PRELOAD_NONE = 2,
+ MF_MEDIA_ENGINE_PRELOAD_METADATA = 3,
+ MF_MEDIA_ENGINE_PRELOAD_AUTOMATIC = 4
+} MF_MEDIA_ENGINE_PRELOAD;
+
+typedef enum _MF3DVideoOutputType {
+ MF3DVideoOutputType_BaseView = 0,
+ MF3DVideoOutputType_Stereo = 1
+} MF3DVideoOutputType;
+
+# ifndef __IMFMediaEngineNotify_INTERFACE_DEFINED__
+# define __IMFMediaEngineNotify_INTERFACE_DEFINED__
+
+/* interface IMFMediaEngineNotify */
+/* [local][unique][uuid][object] */
+
+EXTERN_C const IID IID_IMFMediaEngineNotify;
+MIDL_INTERFACE("fee7c112-e776-42b5-9bbf-0048524e2bd5")
+IMFMediaEngineNotify : public IUnknown {
+ public:
+ virtual HRESULT STDMETHODCALLTYPE EventNotify(
+ /* [annotation][in] */
+ _In_ DWORD event,
+ /* [annotation][in] */
+ _In_ DWORD_PTR param1,
+ /* [annotation][in] */
+ _In_ DWORD param2) = 0;
+};
+
+# endif /* __IMFMediaEngineNotify_INTERFACE_DEFINED__ */
+
+# ifndef __IMFMediaEngineExtension_INTERFACE_DEFINED__
+# define __IMFMediaEngineExtension_INTERFACE_DEFINED__
+
+/* interface IMFMediaEngineExtension */
+/* [local][unique][uuid][object] */
+EXTERN_C const IID IID_IMFMediaEngineExtension;
+MIDL_INTERFACE("2f69d622-20b5-41e9-afdf-89ced1dda04e")
+IMFMediaEngineExtension : public IUnknown {
+ public:
+ virtual HRESULT STDMETHODCALLTYPE CanPlayType(
+ /* [annotation][in] */
+ _In_ BOOL AudioOnly,
+ /* [annotation][in] */
+ _In_ BSTR MimeType,
+ /* [annotation][out] */
+ _Out_ MF_MEDIA_ENGINE_CANPLAY * pAnswer) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE BeginCreateObject(
+ /* [annotation][in] */
+ _In_ BSTR bstrURL,
+ /* [annotation][in] */
+ _In_opt_ IMFByteStream * pByteStream,
+ /* [annotation][in] */
+ _In_ MF_OBJECT_TYPE type,
+ /* [annotation][out] */
+ _Outptr_ IUnknown * *ppIUnknownCancelCookie,
+ /* [annotation][in] */
+ _In_ IMFAsyncCallback * pCallback,
+ /* [annotation][in] */
+ _In_opt_ IUnknown * punkState) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE CancelObjectCreation(
+ /* [annotation][in] */
+ _In_ IUnknown * pIUnknownCancelCookie) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE EndCreateObject(
+ /* [annotation][in] */
+ _In_ IMFAsyncResult * pResult,
+ /* [annotation][out] */
+ _Outptr_ IUnknown * *ppObject) = 0;
+};
+
+# endif /* __IMFMediaEngineExtension_INTERFACE_DEFINED__ */
+
+# ifndef __IMFMediaEngineClassFactory_INTERFACE_DEFINED__
+# define __IMFMediaEngineClassFactory_INTERFACE_DEFINED__
+
+/* interface IMFMediaEngineClassFactory */
+/* [local][unique][uuid][object] */
+
+EXTERN_C const IID IID_IMFMediaEngineClassFactory;
+
+MIDL_INTERFACE("4D645ACE-26AA-4688-9BE1-DF3516990B93")
+IMFMediaEngineClassFactory : public IUnknown {
+ public:
+ virtual HRESULT STDMETHODCALLTYPE CreateInstance(
+ /* [annotation][in] */
+ _In_ DWORD dwFlags,
+ /* [annotation][in] */
+ _In_ IMFAttributes * pAttr,
+ /* [annotation][out] */
+ _Outptr_ IMFMediaEngine * *ppPlayer) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE CreateTimeRange(
+ /* [annotation][out] */
+ _Outptr_ IMFMediaTimeRange * *ppTimeRange) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE CreateError(
+ /* [annotation][out] */
+ _Outptr_ IMFMediaError * *ppError) = 0;
+};
+
+# endif /* __IMFMediaEngineClassFactory_INTERFACE_DEFINED__ */
+
+# ifndef __IMFMediaEngineClassFactory4_INTERFACE_DEFINED__
+# define __IMFMediaEngineClassFactory4_INTERFACE_DEFINED__
+
+/* interface IMFMediaEngineClassFactory4 */
+/* [local][uuid][object] */
+
+EXTERN_C const IID IID_IMFMediaEngineClassFactory4;
+
+MIDL_INTERFACE("fbe256c1-43cf-4a9b-8cb8-ce8632a34186")
+IMFMediaEngineClassFactory4 : public IUnknown {
+ public:
+ virtual HRESULT STDMETHODCALLTYPE CreateContentDecryptionModuleFactory(
+ /* [annotation][in] */
+ _In_ LPCWSTR keySystem,
+ /* [annotation][in] */
+ _In_ REFIID riid,
+ /* [annotation][iid_is][out] */
+ _Outptr_ LPVOID * ppvObject) = 0;
+};
+# endif // __IMFMediaEngineClassFactory4_INTERFACE_DEFINED__
+
+# ifndef __IMFMediaEngine_INTERFACE_DEFINED__
+# define __IMFMediaEngine_INTERFACE_DEFINED__
+
+/* interface IMFMediaEngine */
+/* [local][unique][uuid][object] */
+
+EXTERN_C const IID IID_IMFMediaEngine;
+MIDL_INTERFACE("98a1b0bb-03eb-4935-ae7c-93c1fa0e1c93")
+IMFMediaEngine : public IUnknown {
+ public:
+ virtual HRESULT STDMETHODCALLTYPE GetError(
+ /* [annotation][out] */
+ _Outptr_ IMFMediaError * *ppError) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE SetErrorCode(
+ /* [annotation][in] */
+ _In_ MF_MEDIA_ENGINE_ERR error) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE SetSourceElements(
+ /* [annotation][in] */
+ _In_ IMFMediaEngineSrcElements * pSrcElements) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE SetSource(
+ /* [annotation][in] */
+ _In_ BSTR pUrl) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE GetCurrentSource(
+ /* [annotation][out] */
+ _Out_ BSTR * ppUrl) = 0;
+
+ virtual USHORT STDMETHODCALLTYPE GetNetworkState(void) = 0;
+
+ virtual MF_MEDIA_ENGINE_PRELOAD STDMETHODCALLTYPE GetPreload(void) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE SetPreload(
+ /* [annotation][in] */
+ _In_ MF_MEDIA_ENGINE_PRELOAD Preload) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE GetBuffered(
+ /* [annotation][out] */
+ _Outptr_ IMFMediaTimeRange * *ppBuffered) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE Load(void) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE CanPlayType(
+ /* [annotation][in] */
+ _In_ BSTR type,
+ /* [annotation][out] */
+ _Out_ MF_MEDIA_ENGINE_CANPLAY * pAnswer) = 0;
+
+ virtual USHORT STDMETHODCALLTYPE GetReadyState(void) = 0;
+
+ virtual BOOL STDMETHODCALLTYPE IsSeeking(void) = 0;
+
+ virtual double STDMETHODCALLTYPE GetCurrentTime(void) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE SetCurrentTime(
+ /* [annotation][in] */
+ _In_ double seekTime) = 0;
+
+ virtual double STDMETHODCALLTYPE GetStartTime(void) = 0;
+
+ virtual double STDMETHODCALLTYPE GetDuration(void) = 0;
+
+ virtual BOOL STDMETHODCALLTYPE IsPaused(void) = 0;
+
+ virtual double STDMETHODCALLTYPE GetDefaultPlaybackRate(void) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE SetDefaultPlaybackRate(
+ /* [annotation][in] */
+ _In_ double Rate) = 0;
+
+ virtual double STDMETHODCALLTYPE GetPlaybackRate(void) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE SetPlaybackRate(
+ /* [annotation][in] */
+ _In_ double Rate) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE GetPlayed(
+ /* [annotation][out] */
+ _Outptr_ IMFMediaTimeRange * *ppPlayed) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE GetSeekable(
+ /* [annotation][out] */
+ _Outptr_ IMFMediaTimeRange * *ppSeekable) = 0;
+
+ virtual BOOL STDMETHODCALLTYPE IsEnded(void) = 0;
+
+ virtual BOOL STDMETHODCALLTYPE GetAutoPlay(void) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE SetAutoPlay(
+ /* [annotation][in] */
+ _In_ BOOL AutoPlay) = 0;
+
+ virtual BOOL STDMETHODCALLTYPE GetLoop(void) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE SetLoop(
+ /* [annotation][in] */
+ _In_ BOOL Loop) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE Play(void) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE Pause(void) = 0;
+
+ virtual BOOL STDMETHODCALLTYPE GetMuted(void) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE SetMuted(
+ /* [annotation][in] */
+ _In_ BOOL Muted) = 0;
+
+ virtual double STDMETHODCALLTYPE GetVolume(void) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE SetVolume(
+ /* [annotation][in] */
+ _In_ double Volume) = 0;
+
+ virtual BOOL STDMETHODCALLTYPE HasVideo(void) = 0;
+
+ virtual BOOL STDMETHODCALLTYPE HasAudio(void) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE GetNativeVideoSize(
+ /* [annotation][out] */
+ _Out_opt_ DWORD * cx,
+ /* [annotation][out] */
+ _Out_opt_ DWORD * cy) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE GetVideoAspectRatio(
+ /* [annotation][out] */
+ _Out_opt_ DWORD * cx,
+ /* [annotation][out] */
+ _Out_opt_ DWORD * cy) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE Shutdown(void) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE TransferVideoFrame(
+ /* [annotation][in] */
+ _In_ IUnknown * pDstSurf,
+ /* [annotation][in] */
+ _In_opt_ const MFVideoNormalizedRect* pSrc,
+ /* [annotation][in] */
+ _In_ const RECT* pDst,
+ /* [annotation][in] */
+ _In_opt_ const MFARGB* pBorderClr) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE OnVideoStreamTick(
+ /* [annotation][out] */
+ _Out_ LONGLONG * pPts) = 0;
+};
+# endif /* __IMFMediaEngine_INTERFACE_DEFINED__ */
+
+# ifndef __IMFMediaEngineEx_INTERFACE_DEFINED__
+# define __IMFMediaEngineEx_INTERFACE_DEFINED__
+
+/* interface IMFMediaEngineEx */
+/* [local][unique][uuid][object] */
+
+EXTERN_C const IID IID_IMFMediaEngineEx;
+MIDL_INTERFACE("83015ead-b1e6-40d0-a98a-37145ffe1ad1")
+IMFMediaEngineEx : public IMFMediaEngine {
+ public:
+ virtual HRESULT STDMETHODCALLTYPE SetSourceFromByteStream(
+ /* [annotation][in] */
+ _In_ IMFByteStream * pByteStream,
+ /* [annotation][in] */
+ _In_ BSTR pURL) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE GetStatistics(
+ /* [annotation][in] */
+ _In_ MF_MEDIA_ENGINE_STATISTIC StatisticID,
+ /* [annotation][out] */
+ _Out_ PROPVARIANT * pStatistic) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE UpdateVideoStream(
+ /* [annotation][in] */
+ _In_opt_ const MFVideoNormalizedRect* pSrc,
+ /* [annotation][in] */
+ _In_opt_ const RECT* pDst,
+ /* [annotation][in] */
+ _In_opt_ const MFARGB* pBorderClr) = 0;
+
+ virtual double STDMETHODCALLTYPE GetBalance(void) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE SetBalance(
+ /* [annotation][in] */
+ _In_ double balance) = 0;
+
+ virtual BOOL STDMETHODCALLTYPE IsPlaybackRateSupported(
+ /* [annotation][in] */
+ _In_ double rate) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE FrameStep(
+ /* [annotation][in] */
+ _In_ BOOL Forward) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE GetResourceCharacteristics(
+ /* [annotation][out] */
+ _Out_ DWORD * pCharacteristics) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE GetPresentationAttribute(
+ /* [annotation][in] */
+ _In_ REFGUID guidMFAttribute,
+ /* [annotation][out] */
+ _Out_ PROPVARIANT * pvValue) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE GetNumberOfStreams(
+ /* [annotation][out] */
+ _Out_ DWORD * pdwStreamCount) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE GetStreamAttribute(
+ /* [annotation][in] */
+ _In_ DWORD dwStreamIndex,
+ /* [annotation][in] */
+ _In_ REFGUID guidMFAttribute,
+ /* [annotation][out] */
+ _Out_ PROPVARIANT * pvValue) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE GetStreamSelection(
+ /* [annotation][in] */
+ _In_ DWORD dwStreamIndex,
+ /* [annotation][out] */
+ _Out_ BOOL * pEnabled) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE SetStreamSelection(
+ /* [annotation][in] */
+ _In_ DWORD dwStreamIndex,
+ /* [annotation][in] */
+ _In_ BOOL Enabled) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE ApplyStreamSelections(void) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE IsProtected(
+ /* [annotation][out] */
+ _Out_ BOOL * pProtected) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE InsertVideoEffect(
+ /* [annotation][in] */
+ _In_ IUnknown * pEffect,
+ /* [annotation][in] */
+ _In_ BOOL fOptional) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE InsertAudioEffect(
+ /* [annotation][in] */
+ _In_ IUnknown * pEffect,
+ /* [annotation][in] */
+ _In_ BOOL fOptional) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE RemoveAllEffects(void) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE SetTimelineMarkerTimer(
+ /* [annotation][in] */
+ _In_ double timeToFire) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE GetTimelineMarkerTimer(
+ /* [annotation][out] */
+ _Out_ double* pTimeToFire) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE CancelTimelineMarkerTimer(void) = 0;
+
+ virtual BOOL STDMETHODCALLTYPE IsStereo3D(void) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE GetStereo3DFramePackingMode(
+ /* [annotation][out] */
+ _Out_ MF_MEDIA_ENGINE_S3D_PACKING_MODE * packMode) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE SetStereo3DFramePackingMode(
+ /* [annotation][in] */
+ _In_ MF_MEDIA_ENGINE_S3D_PACKING_MODE packMode) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE GetStereo3DRenderMode(
+ /* [annotation][out] */
+ _Out_ MF3DVideoOutputType * outputType) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE SetStereo3DRenderMode(
+ /* [annotation][in] */
+ _In_ MF3DVideoOutputType outputType) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE EnableWindowlessSwapchainMode(
+ /* [annotation][in] */
+ _In_ BOOL fEnable) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE GetVideoSwapchainHandle(
+ /* [annotation][out] */
+ _Out_ HANDLE * phSwapchain) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE EnableHorizontalMirrorMode(
+ /* [annotation][in] */
+ _In_ BOOL fEnable) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE GetAudioStreamCategory(
+ /* [annotation][out] */
+ _Out_ UINT32 * pCategory) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE SetAudioStreamCategory(
+ /* [annotation][in] */
+ _In_ UINT32 category) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE GetAudioEndpointRole(
+ /* [annotation][out] */
+ _Out_ UINT32 * pRole) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE SetAudioEndpointRole(
+ /* [annotation][in] */
+ _In_ UINT32 role) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE GetRealTimeMode(
+ /* [annotation][out] */
+ _Out_ BOOL * pfEnabled) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE SetRealTimeMode(
+ /* [annotation][in] */
+ _In_ BOOL fEnable) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE SetCurrentTimeEx(
+ /* [annotation][in] */
+ _In_ double seekTime,
+ /* [annotation][in] */
+ _In_ MF_MEDIA_ENGINE_SEEK_MODE seekMode) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE EnableTimeUpdateTimer(
+ /* [annotation][in] */
+ _In_ BOOL fEnableTimer) = 0;
+};
+# endif /* __IMFMediaEngineEx_INTERFACE_DEFINED__ */
+
+# ifndef __IMFCdmSuspendNotify_INTERFACE_DEFINED__
+# define __IMFCdmSuspendNotify_INTERFACE_DEFINED__
+
+/* interface IMFCdmSuspendNotify */
+/* [unique][uuid][object] */
+
+EXTERN_C const IID IID_IMFCdmSuspendNotify;
+
+MIDL_INTERFACE("7a5645d2-43bd-47fd-87b7-dcd24cc7d692")
+IMFCdmSuspendNotify : public IUnknown {
+ public:
+ virtual HRESULT STDMETHODCALLTYPE Begin(void) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE End(void) = 0;
+};
+
+# endif /* __IMFCdmSuspendNotify_INTERFACE_DEFINED__ */
+
+# ifndef __IMFMediaEngineProtectedContent_INTERFACE_DEFINED__
+# define __IMFMediaEngineProtectedContent_INTERFACE_DEFINED__
+
+/* interface IMFMediaEngineProtectedContent */
+/* [local][uuid][object] */
+
+EXTERN_C const IID IID_IMFMediaEngineProtectedContent;
+
+MIDL_INTERFACE("9f8021e8-9c8c-487e-bb5c-79aa4779938c")
+IMFMediaEngineProtectedContent : public IUnknown {
+ public:
+ virtual HRESULT STDMETHODCALLTYPE ShareResources(
+ /* [annotation] */
+ _In_ IUnknown * pUnkDeviceContext) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE GetRequiredProtections(
+ /* [annotation][out] */
+ _Out_ DWORD * pFrameProtectionFlags) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE SetOPMWindow(
+ /* [annotation][in] */
+ _In_ HWND hwnd) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE TransferVideoFrame(
+ /* [annotation][in] */
+ _In_ IUnknown * pDstSurf,
+ /* [annotation][in] */
+ _In_opt_ const MFVideoNormalizedRect* pSrc,
+ /* [annotation][in] */
+ _In_ const RECT* pDst,
+ /* [annotation][in] */
+ _In_opt_ const MFARGB* pBorderClr,
+ /* [annotation][out] */
+ _Out_ DWORD* pFrameProtectionFlags) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE SetContentProtectionManager(
+ /* [annotation][in] */
+ _In_opt_ IMFContentProtectionManager * pCPM) = 0;
+
+ virtual HRESULT STDMETHODCALLTYPE SetApplicationCertificate(
+ /* [annotation][in] */
+ _In_reads_bytes_(cbBlob) const BYTE* pbBlob,
+ /* [annotation][in] */
+ _In_ DWORD cbBlob) = 0;
+};
+
+# endif /* __IMFMediaEngineProtectedContent_INTERFACE_DEFINED__ */
+
+#endif // extra class copy from mfmediaengine.h
+#endif // DOM_MEDIA_PLATFORM_WMF_MFMEDIAENGINENOTIFY_H
diff --git a/dom/media/platforms/wmf/MFMediaEngineNotify.cpp b/dom/media/platforms/wmf/MFMediaEngineNotify.cpp
new file mode 100644
index 0000000000..a33757ac26
--- /dev/null
+++ b/dom/media/platforms/wmf/MFMediaEngineNotify.cpp
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MFMediaEngineNotify.h"
+
+#include "MFMediaEngineUtils.h"
+
+namespace mozilla {
+
+#define LOG(msg, ...) \
+ MOZ_LOG(gMFMediaEngineLog, LogLevel::Debug, \
+ ("MFMediaEngineNotify=%p, " msg, this, ##__VA_ARGS__))
+
+IFACEMETHODIMP MFMediaEngineNotify::EventNotify(DWORD aEvent, DWORD_PTR aParam1,
+ DWORD aParam2) {
+ auto event = static_cast<MF_MEDIA_ENGINE_EVENT>(aEvent);
+ LOG("Received media engine event %s", MediaEngineEventToStr(event));
+ MFMediaEngineEventWrapper engineEvent{event};
+ if (event == MF_MEDIA_ENGINE_EVENT_ERROR ||
+ event == MF_MEDIA_ENGINE_EVENT_FORMATCHANGE ||
+ event == MF_MEDIA_ENGINE_EVENT_NOTIFYSTABLESTATE) {
+ engineEvent.mParam1 = Some(aParam1);
+ engineEvent.mParam2 = Some(aParam2);
+ }
+ mEngineEvents.Notify(engineEvent);
+ return S_OK;
+}
+
+#undef LOG
+
+} // namespace mozilla
diff --git a/dom/media/platforms/wmf/MFMediaEngineNotify.h b/dom/media/platforms/wmf/MFMediaEngineNotify.h
new file mode 100644
index 0000000000..9e42e115c0
--- /dev/null
+++ b/dom/media/platforms/wmf/MFMediaEngineNotify.h
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_PLATFORM_WMF_MFMEDIAENGINENOTIFY_H
+#define DOM_MEDIA_PLATFORM_WMF_MFMEDIAENGINENOTIFY_H
+
+#include <wrl.h>
+
+#include "MediaEventSource.h"
+#include "MFMediaEngineExtra.h"
+#include "mozilla/Maybe.h"
+
+namespace mozilla {
+
+const char* MediaEngineEventToStr(MF_MEDIA_ENGINE_EVENT aEvent);
+
+// https://docs.microsoft.com/en-us/windows/win32/api/mfmediaengine/ne-mfmediaengine-mf_media_engine_event
+struct MFMediaEngineEventWrapper final {
+ explicit MFMediaEngineEventWrapper(MF_MEDIA_ENGINE_EVENT aEvent)
+ : mEvent(aEvent) {}
+ MF_MEDIA_ENGINE_EVENT mEvent;
+ Maybe<DWORD_PTR> mParam1;
+ Maybe<DWORD> mParam2;
+};
+
+/**
+ * MFMediaEngineNotify is used to handle the event sent from the media engine.
+ * https://docs.microsoft.com/en-us/windows/win32/api/mfmediaengine/nn-mfmediaengine-imfmediaenginenotify
+ */
+class MFMediaEngineNotify final
+ : public Microsoft::WRL::RuntimeClass<
+ Microsoft::WRL::RuntimeClassFlags<
+ Microsoft::WRL::RuntimeClassType::ClassicCom>,
+ IMFMediaEngineNotify> {
+ public:
+ MFMediaEngineNotify() = default;
+
+ HRESULT RuntimeClassInitialize() { return S_OK; }
+
+ // Method for IMFMediaEngineNotify
+ IFACEMETHODIMP EventNotify(DWORD aEvent, DWORD_PTR aParam1,
+ DWORD aParam2) override;
+
+ MediaEventSource<MFMediaEngineEventWrapper>& MediaEngineEvent() {
+ return mEngineEvents;
+ }
+
+ private:
+ MediaEventProducer<MFMediaEngineEventWrapper> mEngineEvents;
+};
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_PLATFORM_WMF_MFMEDIAENGINENOTIFY_H
diff --git a/dom/media/platforms/wmf/MFMediaEngineStream.cpp b/dom/media/platforms/wmf/MFMediaEngineStream.cpp
new file mode 100644
index 0000000000..6dce37ee35
--- /dev/null
+++ b/dom/media/platforms/wmf/MFMediaEngineStream.cpp
@@ -0,0 +1,596 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MFMediaEngineStream.h"
+#include <vcruntime.h>
+
+#include "AudioConverter.h"
+#include "MFMediaSource.h"
+#include "MFMediaEngineUtils.h"
+#include "TimeUnits.h"
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/ProfilerMarkerTypes.h"
+#include "WMF.h"
+#include "WMFUtils.h"
+
+namespace mozilla {
+
+// Don't use this log on the task queue, because it would be racy for `mStream`.
+#define WLOGV(msg, ...) \
+ MOZ_LOG(gMFMediaEngineLog, LogLevel::Verbose, \
+ ("MFMediaEngineStreamWrapper for stream %p (%s, id=%lu), " msg, \
+ mStream.Get(), mStream->GetDescriptionName().get(), \
+ mStream->DescriptorId(), ##__VA_ARGS__))
+
+#define SLOG(msg, ...) \
+ MOZ_LOG( \
+ gMFMediaEngineLog, LogLevel::Debug, \
+ ("MFMediaStream=%p (%s, id=%lu), " msg, this, \
+ this->GetDescriptionName().get(), this->DescriptorId(), ##__VA_ARGS__))
+
+#define SLOGV(msg, ...) \
+ MOZ_LOG( \
+ gMFMediaEngineLog, LogLevel::Verbose, \
+ ("MFMediaStream=%p (%s, id=%lu), " msg, this, \
+ this->GetDescriptionName().get(), this->DescriptorId(), ##__VA_ARGS__))
+
+using Microsoft::WRL::ComPtr;
+
+RefPtr<MediaDataDecoder::InitPromise> MFMediaEngineStreamWrapper::Init() {
+ MOZ_ASSERT(mStream->DescriptorId(), "Stream hasn't been initialized!");
+ WLOGV("Init");
+ return InitPromise::CreateAndResolve(mStream->TrackType(), __func__);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> MFMediaEngineStreamWrapper::Decode(
+ MediaRawData* aSample) {
+ WLOGV("Decode");
+ if (!mStream || mStream->IsShutdown()) {
+ return DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_FAILURE, "MFMediaEngineStreamWrapper is shutdown"),
+ __func__);
+ }
+ RefPtr<MediaRawData> sample = aSample;
+ return InvokeAsync(mTaskQueue, mStream.Get(), __func__,
+ &MFMediaEngineStream::OutputData, std::move(sample));
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> MFMediaEngineStreamWrapper::Drain() {
+ WLOGV("Drain");
+ if (!mStream || mStream->IsShutdown()) {
+ return DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_FAILURE, "MFMediaEngineStreamWrapper is shutdown"),
+ __func__);
+ }
+ return InvokeAsync(mTaskQueue, mStream.Get(), __func__,
+ &MFMediaEngineStream::Drain);
+}
+
+RefPtr<MediaDataDecoder::FlushPromise> MFMediaEngineStreamWrapper::Flush() {
+ WLOGV("Flush");
+ if (!mStream || mStream->IsShutdown()) {
+ return FlushPromise::CreateAndReject(
+ MediaResult(NS_ERROR_FAILURE, "MFMediaEngineStreamWrapper is shutdown"),
+ __func__);
+ }
+ return InvokeAsync(mTaskQueue, mStream.Get(), __func__,
+ &MFMediaEngineStream::Flush);
+}
+
+RefPtr<ShutdownPromise> MFMediaEngineStreamWrapper::Shutdown() {
+ // Stream shutdown is controlled by the media source, so we don't need to call
+ // its shutdown.
+ WLOGV("Disconnect wrapper");
+ if (!mStream) {
+ // This promise must only ever be resolved. See the definition of the
+ // original abstract function.
+ return ShutdownPromise::CreateAndResolve(false, __func__);
+ }
+ mStream = nullptr;
+ mTaskQueue = nullptr;
+ return ShutdownPromise::CreateAndResolve(true, __func__);
+}
+
+nsCString MFMediaEngineStreamWrapper::GetDescriptionName() const {
+ return mStream ? mStream->GetDescriptionName() : nsLiteralCString("none");
+}
+
+nsCString MFMediaEngineStreamWrapper::GetCodecName() const {
+ return mStream ? mStream->GetCodecName() : nsLiteralCString("none");
+}
+
+MediaDataDecoder::ConversionRequired
+MFMediaEngineStreamWrapper::NeedsConversion() const {
+ return mStream ? mStream->NeedsConversion()
+ : MediaDataDecoder::ConversionRequired::kNeedNone;
+}
+
+MFMediaEngineStream::MFMediaEngineStream()
+ : mIsShutdown(false), mIsSelected(false), mReceivedEOS(false) {
+ MOZ_COUNT_CTOR(MFMediaEngineStream);
+}
+
+MFMediaEngineStream::~MFMediaEngineStream() {
+ MOZ_ASSERT(IsShutdown());
+ MOZ_COUNT_DTOR(MFMediaEngineStream);
+}
+
+HRESULT MFMediaEngineStream::RuntimeClassInitialize(
+ uint64_t aStreamId, const TrackInfo& aInfo, MFMediaSource* aParentSource) {
+ mParentSource = aParentSource;
+ mTaskQueue = aParentSource->GetTaskQueue();
+ MOZ_ASSERT(mTaskQueue);
+ mStreamId = aStreamId;
+ RETURN_IF_FAILED(wmf::MFCreateEventQueue(&mMediaEventQueue));
+
+ ComPtr<IMFMediaType> mediaType;
+ // The inherited stream would return different type based on their media info.
+ RETURN_IF_FAILED(CreateMediaType(aInfo, mediaType.GetAddressOf()));
+ RETURN_IF_FAILED(GenerateStreamDescriptor(mediaType));
+ SLOG("Initialized %s (id=%" PRIu64 ", descriptorId=%lu)",
+ GetDescriptionName().get(), aStreamId, mStreamDescriptorId);
+ return S_OK;
+}
+
+HRESULT MFMediaEngineStream::GenerateStreamDescriptor(
+ ComPtr<IMFMediaType>& aMediaType) {
+ RETURN_IF_FAILED(wmf::MFCreateStreamDescriptor(
+ mStreamId, 1 /* stream amount */, aMediaType.GetAddressOf(),
+ &mStreamDescriptor));
+ RETURN_IF_FAILED(
+ mStreamDescriptor->GetStreamIdentifier(&mStreamDescriptorId));
+ if (IsEncrypted()) {
+ RETURN_IF_FAILED(mStreamDescriptor->SetUINT32(MF_SD_PROTECTED, 1));
+ }
+ return S_OK;
+}
+
+HRESULT MFMediaEngineStream::Start(const PROPVARIANT* aPosition) {
+ AssertOnMFThreadPool();
+ if (!IsSelected()) {
+ SLOG("No need to start non-selected stream");
+ return S_OK;
+ }
+ if (IsShutdown()) {
+ return MF_E_SHUTDOWN;
+ }
+ SLOG("Start");
+ const bool isFromCurrentPosition = aPosition->vt == VT_EMPTY;
+ RETURN_IF_FAILED(QueueEvent(MEStreamStarted, GUID_NULL, S_OK, aPosition));
+ MOZ_ASSERT(mTaskQueue);
+ Unused << mTaskQueue->Dispatch(NS_NewRunnableFunction(
+ "MFMediaEngineStream::Start",
+ [self = RefPtr{this}, isFromCurrentPosition, this]() {
+ if (!isFromCurrentPosition && IsEnded()) {
+ SLOG("Stream restarts again from a new position, reset EOS");
+ mReceivedEOS = false;
+ }
+ // Process pending requests (if any) which happened when the stream
+ // wasn't allowed to serve samples. Eg. stream is paused. Or resend the
+ // ended event if the stream is ended already.
+ ReplySampleRequestIfPossible();
+ }));
+ return S_OK;
+}
+
+HRESULT MFMediaEngineStream::Seek(const PROPVARIANT* aPosition) {
+ AssertOnMFThreadPool();
+ if (!IsSelected()) {
+ SLOG("No need to seek non-selected stream");
+ return S_OK;
+ }
+ SLOG("Seek");
+ RETURN_IF_FAILED(QueueEvent(MEStreamSeeked, GUID_NULL, S_OK, aPosition));
+ return S_OK;
+}
+
+HRESULT MFMediaEngineStream::Stop() {
+ AssertOnMFThreadPool();
+ if (!IsSelected()) {
+ SLOG("No need to stop non-selected stream");
+ return S_OK;
+ }
+ SLOG("Stop");
+ RETURN_IF_FAILED(QueueEvent(MEStreamStopped, GUID_NULL, S_OK, nullptr));
+ return S_OK;
+}
+
+HRESULT MFMediaEngineStream::Pause() {
+ AssertOnMFThreadPool();
+ if (!IsSelected()) {
+ SLOG("No need to pause non-selected stream");
+ return S_OK;
+ }
+ SLOG("Pause");
+ RETURN_IF_FAILED(QueueEvent(MEStreamPaused, GUID_NULL, S_OK, nullptr));
+ return S_OK;
+}
+
+void MFMediaEngineStream::Shutdown() {
+ AssertOnMFThreadPool();
+ if (IsShutdown()) {
+ return;
+ }
+ SLOG("Shutdown");
+ mIsShutdown = true;
+ // After this method is called, all IMFMediaEventQueue methods return
+ // MF_E_SHUTDOWN.
+ RETURN_VOID_IF_FAILED(mMediaEventQueue->Shutdown());
+ ComPtr<MFMediaEngineStream> self = this;
+ MOZ_ASSERT(mTaskQueue);
+ Unused << mTaskQueue->Dispatch(
+ NS_NewRunnableFunction("MFMediaEngineStream::Shutdown", [self]() {
+ self->mParentSource = nullptr;
+ self->mRawDataQueueForFeedingEngine.Reset();
+ self->mRawDataQueueForGeneratingOutput.Reset();
+ self->ShutdownCleanUpOnTaskQueue();
+ self->mTaskQueue = nullptr;
+ }));
+}
+
+IFACEMETHODIMP
+MFMediaEngineStream::GetMediaSource(IMFMediaSource** aMediaSource) {
+ AssertOnMFThreadPool();
+ if (IsShutdown()) {
+ return MF_E_SHUTDOWN;
+ }
+ RETURN_IF_FAILED(mParentSource.CopyTo(aMediaSource));
+ return S_OK;
+}
+
+IFACEMETHODIMP MFMediaEngineStream::GetStreamDescriptor(
+ IMFStreamDescriptor** aStreamDescriptor) {
+ AssertOnMFThreadPool();
+ if (IsShutdown()) {
+ return MF_E_SHUTDOWN;
+ }
+ if (!mStreamDescriptor) {
+ SLOG("Hasn't initialized stream descriptor");
+ return MF_E_NOT_INITIALIZED;
+ }
+ RETURN_IF_FAILED(mStreamDescriptor.CopyTo(aStreamDescriptor));
+ return S_OK;
+}
+
+IFACEMETHODIMP MFMediaEngineStream::RequestSample(IUnknown* aToken) {
+ AssertOnMFThreadPool();
+ if (IsShutdown()) {
+ return MF_E_SHUTDOWN;
+ }
+
+ ComPtr<IUnknown> token = aToken;
+ ComPtr<MFMediaEngineStream> self = this;
+ MOZ_ASSERT(mTaskQueue);
+ Unused << mTaskQueue->Dispatch(NS_NewRunnableFunction(
+ "MFMediaEngineStream::RequestSample", [token, self, this]() {
+ AssertOnTaskQueue();
+ mSampleRequestTokens.push(token);
+ SLOGV("RequestSample, token amount=%zu", mSampleRequestTokens.size());
+ ReplySampleRequestIfPossible();
+ if (!HasEnoughRawData() && mParentSource && !IsEnded()) {
+ SendRequestSampleEvent(false /* isEnough */);
+ }
+ }));
+ return S_OK;
+}
+
+void MFMediaEngineStream::ReplySampleRequestIfPossible() {
+ AssertOnTaskQueue();
+ if (IsEnded()) {
+ // We have no more sample to return, clean all pending requests.
+ while (!mSampleRequestTokens.empty()) {
+ mSampleRequestTokens.pop();
+ }
+
+ SLOG("Notify end events");
+ MOZ_ASSERT(mRawDataQueueForFeedingEngine.GetSize() == 0);
+ MOZ_ASSERT(mSampleRequestTokens.empty());
+ RETURN_VOID_IF_FAILED(mMediaEventQueue->QueueEventParamUnk(
+ MEEndOfStream, GUID_NULL, S_OK, nullptr));
+ mEndedEvent.Notify(TrackType());
+ PROFILER_MARKER_TEXT(
+ "MFMediaEngineStream:NotifyEnd", MEDIA_PLAYBACK, {},
+ nsPrintfCString("stream=%s, id=%" PRIu64, GetDescriptionName().get(),
+ mStreamId));
+ return;
+ }
+
+ if (mSampleRequestTokens.empty() ||
+ mRawDataQueueForFeedingEngine.GetSize() == 0) {
+ return;
+ }
+
+ if (!ShouldServeSamples()) {
+ SLOGV("Not deliver samples if the stream is not started");
+ return;
+ }
+
+ // Push data into the mf media event queue if the media engine is already
+ // waiting for data.
+ ComPtr<IMFSample> inputSample;
+ RETURN_VOID_IF_FAILED(CreateInputSample(inputSample.GetAddressOf()));
+ ComPtr<IUnknown> token = mSampleRequestTokens.front();
+ RETURN_VOID_IF_FAILED(
+ inputSample->SetUnknown(MFSampleExtension_Token, token.Get()));
+ mSampleRequestTokens.pop();
+ RETURN_VOID_IF_FAILED(mMediaEventQueue->QueueEventParamUnk(
+ MEMediaSample, GUID_NULL, S_OK, inputSample.Get()));
+}
+
+bool MFMediaEngineStream::ShouldServeSamples() const {
+ AssertOnTaskQueue();
+ return mParentSource &&
+ mParentSource->GetState() == MFMediaSource::State::Started &&
+ mIsSelected;
+}
+
+HRESULT MFMediaEngineStream::CreateInputSample(IMFSample** aSample) {
+ AssertOnTaskQueue();
+
+ ComPtr<IMFSample> sample;
+ RETURN_IF_FAILED(wmf::MFCreateSample(&sample));
+
+ MOZ_ASSERT(mRawDataQueueForFeedingEngine.GetSize() != 0);
+ RefPtr<MediaRawData> data = mRawDataQueueForFeedingEngine.PopFront();
+ SLOGV("CreateInputSample, pop data [%" PRId64 ", %" PRId64
+ "] (duration=%" PRId64 ", kf=%d), queue size=%zu",
+ data->mTime.ToMicroseconds(), data->GetEndTime().ToMicroseconds(),
+ data->mDuration.ToMicroseconds(), data->mKeyframe,
+ mRawDataQueueForFeedingEngine.GetSize());
+ PROFILER_MARKER(
+ nsPrintfCString(
+ "pop %s (stream=%" PRIu64 ")",
+ TrackType() == TrackInfo::TrackType::kVideoTrack ? "video" : "audio",
+ mStreamId),
+ MEDIA_PLAYBACK, {}, MediaSampleMarker, data->mTime.ToMicroseconds(),
+ data->GetEndTime().ToMicroseconds(),
+ mRawDataQueueForFeedingEngine.GetSize());
+
+ // Copy data into IMFMediaBuffer
+ ComPtr<IMFMediaBuffer> buffer;
+ BYTE* dst = nullptr;
+ DWORD maxLength = 0;
+ RETURN_IF_FAILED(
+ wmf::MFCreateMemoryBuffer(data->Size(), buffer.GetAddressOf()));
+ RETURN_IF_FAILED(buffer->Lock(&dst, &maxLength, 0));
+ memcpy(dst, data->Data(), data->Size());
+ RETURN_IF_FAILED(buffer->Unlock());
+ RETURN_IF_FAILED(buffer->SetCurrentLength(data->Size()));
+
+ // Setup sample attributes
+ RETURN_IF_FAILED(sample->AddBuffer(buffer.Get()));
+ RETURN_IF_FAILED(
+ sample->SetSampleTime(UsecsToHNs(data->mTime.ToMicroseconds())));
+ RETURN_IF_FAILED(
+ sample->SetSampleDuration(UsecsToHNs(data->mDuration.ToMicroseconds())));
+ if (data->mKeyframe) {
+ RETURN_IF_FAILED(sample->SetUINT32(MFSampleExtension_CleanPoint, 1));
+ }
+
+ // Setup encrypt attributes
+ if (data->mCrypto.IsEncrypted()) {
+ RETURN_IF_FAILED(AddEncryptAttributes(sample.Get(), data->mCrypto));
+ }
+
+ *aSample = sample.Detach();
+ return S_OK;
+}
+
+HRESULT MFMediaEngineStream::AddEncryptAttributes(
+ IMFSample* aSample, const CryptoSample& aCryptoConfig) {
+ // Scheme
+ MFSampleEncryptionProtectionScheme protectionScheme;
+ if (aCryptoConfig.mCryptoScheme == CryptoScheme::Cenc) {
+ protectionScheme = MFSampleEncryptionProtectionScheme::
+ MF_SAMPLE_ENCRYPTION_PROTECTION_SCHEME_AES_CTR;
+ } else if (aCryptoConfig.mCryptoScheme == CryptoScheme::Cbcs) {
+ protectionScheme = MFSampleEncryptionProtectionScheme::
+ MF_SAMPLE_ENCRYPTION_PROTECTION_SCHEME_AES_CBC;
+ } else {
+ SLOG("Unexpected encryption scheme");
+ return MF_E_UNEXPECTED;
+ }
+ RETURN_IF_FAILED(aSample->SetUINT32(
+ MFSampleExtension_Encryption_ProtectionScheme, protectionScheme));
+
+ // KID
+ if (aCryptoConfig.mKeyId.Length() != sizeof(GUID)) {
+ SLOG("Unsupported key ID size (%zu)", aCryptoConfig.mKeyId.Length());
+ return MF_E_UNEXPECTED;
+ }
+ GUID keyId;
+ GUIDFromByteArray(aCryptoConfig.mKeyId, keyId);
+ RETURN_IF_FAILED(aSample->SetGUID(MFSampleExtension_Content_KeyID, keyId));
+ // TODO : if we want to suspend/resume the media engine, then we can consider
+ // to store last key id and set it in CDM to refresh the decryptor.
+
+ // IV
+ RETURN_IF_FAILED(aSample->SetBlob(
+ MFSampleExtension_Encryption_SampleID,
+ reinterpret_cast<const uint8_t*>(aCryptoConfig.mIV.Elements()),
+ aCryptoConfig.mIVSize));
+
+ // Subsample entries.
+ MOZ_ASSERT(aCryptoConfig.mEncryptedSizes.Length() ==
+ aCryptoConfig.mPlainSizes.Length());
+ size_t numSubsamples = aCryptoConfig.mEncryptedSizes.Length();
+ if (numSubsamples != 0) {
+ std::vector<MediaFoundationSubsampleEntry> subsampleEntries;
+ for (size_t idx = 0; idx < numSubsamples; idx++) {
+ subsampleEntries.push_back(MediaFoundationSubsampleEntry{
+ aCryptoConfig.mPlainSizes[idx], aCryptoConfig.mEncryptedSizes[idx]});
+ }
+ const uint32_t entriesSize =
+ sizeof(MediaFoundationSubsampleEntry) * numSubsamples;
+ RETURN_IF_FAILED(aSample->SetBlob(
+ MFSampleExtension_Encryption_SubSample_Mapping,
+ reinterpret_cast<const uint8_t*>(subsampleEntries.data()),
+ entriesSize));
+ }
+
+ return S_OK;
+}
+
+IFACEMETHODIMP MFMediaEngineStream::GetEvent(DWORD aFlags,
+ IMFMediaEvent** aEvent) {
+ AssertOnMFThreadPool();
+ MOZ_ASSERT(mMediaEventQueue);
+ RETURN_IF_FAILED(mMediaEventQueue->GetEvent(aFlags, aEvent));
+ return S_OK;
+}
+
+IFACEMETHODIMP MFMediaEngineStream::BeginGetEvent(IMFAsyncCallback* aCallback,
+ IUnknown* aState) {
+ AssertOnMFThreadPool();
+ MOZ_ASSERT(mMediaEventQueue);
+ RETURN_IF_FAILED(mMediaEventQueue->BeginGetEvent(aCallback, aState));
+ return S_OK;
+}
+
+IFACEMETHODIMP MFMediaEngineStream::EndGetEvent(IMFAsyncResult* aResult,
+ IMFMediaEvent** aEvent) {
+ AssertOnMFThreadPool();
+ MOZ_ASSERT(mMediaEventQueue);
+ RETURN_IF_FAILED(mMediaEventQueue->EndGetEvent(aResult, aEvent));
+ return S_OK;
+}
+
+IFACEMETHODIMP MFMediaEngineStream::QueueEvent(MediaEventType aType,
+ REFGUID aExtendedType,
+ HRESULT aStatus,
+ const PROPVARIANT* aValue) {
+ AssertOnMFThreadPool();
+ MOZ_ASSERT(mMediaEventQueue);
+ RETURN_IF_FAILED(mMediaEventQueue->QueueEventParamVar(aType, aExtendedType,
+ aStatus, aValue));
+ SLOG("Queued event %s", MediaEventTypeToStr(aType));
+ return S_OK;
+}
+
+void MFMediaEngineStream::SetSelected(bool aSelected) {
+ AssertOnMFThreadPool();
+ SLOG("Select=%d", aSelected);
+ mIsSelected = aSelected;
+}
+
+void MFMediaEngineStream::NotifyNewData(MediaRawData* aSample) {
+ AssertOnTaskQueue();
+ if (IsShutdown()) {
+ return;
+ }
+ const bool wasEnough = HasEnoughRawData();
+ mRawDataQueueForFeedingEngine.Push(aSample);
+ mRawDataQueueForGeneratingOutput.Push(aSample);
+ SLOGV("NotifyNewData, push data [%" PRId64 ", %" PRId64
+ "], queue size=%zu, queue duration=%" PRId64,
+ aSample->mTime.ToMicroseconds(), aSample->GetEndTime().ToMicroseconds(),
+ mRawDataQueueForFeedingEngine.GetSize(),
+ mRawDataQueueForFeedingEngine.Duration());
+ if (mReceivedEOS) {
+ SLOG("Receive a new data, cancel old EOS flag");
+ mReceivedEOS = false;
+ }
+ ReplySampleRequestIfPossible();
+ if (!wasEnough && HasEnoughRawData()) {
+ SendRequestSampleEvent(true /* isEnough */);
+ }
+}
+
+void MFMediaEngineStream::SendRequestSampleEvent(bool aIsEnough) {
+ AssertOnTaskQueue();
+ SLOGV("data is %s, queue duration=%" PRId64,
+ aIsEnough ? "enough" : "not enough",
+ mRawDataQueueForFeedingEngine.Duration());
+ mParentSource->mRequestSampleEvent.Notify(
+ SampleRequest{TrackType(), aIsEnough});
+}
+
+void MFMediaEngineStream::NotifyEndOfStreamInternal() {
+ AssertOnTaskQueue();
+ if (mReceivedEOS) {
+ return;
+ }
+ SLOG("EOS");
+ mReceivedEOS = true;
+ ReplySampleRequestIfPossible();
+}
+
+bool MFMediaEngineStream::IsEnded() const {
+ AssertOnTaskQueue();
+ return mReceivedEOS && mRawDataQueueForFeedingEngine.GetSize() == 0;
+}
+
+RefPtr<MediaDataDecoder::FlushPromise> MFMediaEngineStream::Flush() {
+ if (IsShutdown()) {
+ return MediaDataDecoder::FlushPromise::CreateAndReject(
+ MediaResult(NS_ERROR_FAILURE,
+ RESULT_DETAIL("MFMediaEngineStream is shutdown")),
+ __func__);
+ }
+ AssertOnTaskQueue();
+ SLOG("Flush");
+ mRawDataQueueForFeedingEngine.Reset();
+ mRawDataQueueForGeneratingOutput.Reset();
+ mReceivedEOS = false;
+ return MediaDataDecoder::FlushPromise::CreateAndResolve(true, __func__);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> MFMediaEngineStream::OutputData(
+ RefPtr<MediaRawData> aSample) {
+ if (IsShutdown()) {
+ return MediaDataDecoder::DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_FAILURE,
+ RESULT_DETAIL("MFMediaEngineStream is shutdown")),
+ __func__);
+ }
+ AssertOnTaskQueue();
+ NotifyNewData(aSample);
+ MediaDataDecoder::DecodedData outputs;
+ if (RefPtr<MediaData> outputData = OutputDataInternal()) {
+ outputs.AppendElement(outputData);
+ SLOGV("Output data [%" PRId64 ",%" PRId64 "]",
+ outputData->mTime.ToMicroseconds(),
+ outputData->GetEndTime().ToMicroseconds());
+ }
+ return MediaDataDecoder::DecodePromise::CreateAndResolve(std::move(outputs),
+ __func__);
+};
+
+RefPtr<MediaDataDecoder::DecodePromise> MFMediaEngineStream::Drain() {
+ if (IsShutdown()) {
+ return MediaDataDecoder::DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_FAILURE,
+ RESULT_DETAIL("MFMediaEngineStream is shutdown")),
+ __func__);
+ }
+ AssertOnTaskQueue();
+ MediaDataDecoder::DecodedData outputs;
+ while (RefPtr<MediaData> outputData = OutputDataInternal()) {
+ outputs.AppendElement(outputData);
+ SLOGV("Output data [%" PRId64 ",%" PRId64 "]",
+ outputData->mTime.ToMicroseconds(),
+ outputData->GetEndTime().ToMicroseconds());
+ }
+ return MediaDataDecoder::DecodePromise::CreateAndResolve(std::move(outputs),
+ __func__);
+}
+
+void MFMediaEngineStream::AssertOnTaskQueue() const {
+ MOZ_ASSERT(mTaskQueue && mTaskQueue->IsCurrentThreadIn());
+}
+
+void MFMediaEngineStream::AssertOnMFThreadPool() const {
+ // We can't really assert the thread id from thread pool, because it would
+ // change any time. So we just assert this is not the task queue, and use the
+ // explicit function name to indicate what thread we should run on.
+ // TODO : this assertion is not precise, because the running thread could be
+ // the stream wrapper thread as well,
+ MOZ_ASSERT(!mTaskQueue || !mTaskQueue->IsCurrentThreadIn());
+}
+
+#undef WLOGV
+#undef SLOG
+#undef SLOGV
+
+} // namespace mozilla
diff --git a/dom/media/platforms/wmf/MFMediaEngineStream.h b/dom/media/platforms/wmf/MFMediaEngineStream.h
new file mode 100644
index 0000000000..aa3bf7e65d
--- /dev/null
+++ b/dom/media/platforms/wmf/MFMediaEngineStream.h
@@ -0,0 +1,228 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_PLATFORM_WMF_MFMEDIAENGINESTREAM_H
+#define DOM_MEDIA_PLATFORM_WMF_MFMEDIAENGINESTREAM_H
+
+#include <mfidl.h>
+#include <wrl.h>
+
+#include <queue>
+
+#include "BlankDecoderModule.h"
+#include "MediaQueue.h"
+#include "PlatformDecoderModule.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/SPSCQueue.h"
+
+namespace mozilla {
+
+class MFMediaEngineVideoStream;
+class MFMediaSource;
+
+/**
+ * MFMediaEngineStream represents a track which would be responsible to provide
+ * encoded data into the media engine. The media engine can access this stream
+ * by the presentation descriptor which was acquired from the custom media
+ * source.
+ */
+class MFMediaEngineStream
+ : public Microsoft::WRL::RuntimeClass<
+ Microsoft::WRL::RuntimeClassFlags<
+ Microsoft::WRL::RuntimeClassType::ClassicCom>,
+ IMFMediaStream> {
+ public:
+ MFMediaEngineStream();
+ ~MFMediaEngineStream();
+
+ virtual nsCString GetDescriptionName() const = 0;
+
+ virtual nsCString GetCodecName() const = 0;
+
+ HRESULT RuntimeClassInitialize(uint64_t aStreamId, const TrackInfo& aInfo,
+ MFMediaSource* aParentSource);
+
+ // Called by MFMediaSource.
+ HRESULT Start(const PROPVARIANT* aPosition);
+ HRESULT Seek(const PROPVARIANT* aPosition);
+ HRESULT Stop();
+ HRESULT Pause();
+ void Shutdown();
+
+ void SetSelected(bool aSelected);
+ bool IsSelected() const { return mIsSelected; }
+ DWORD DescriptorId() const { return mStreamDescriptorId; }
+
+ // Methods for IMFMediaStream
+ IFACEMETHODIMP GetMediaSource(IMFMediaSource** aMediaSource) override;
+ IFACEMETHODIMP GetStreamDescriptor(
+ IMFStreamDescriptor** aStreamDescriptor) override;
+ IFACEMETHODIMP RequestSample(IUnknown* aToken) override;
+
+ // Methods for IMFMediaEventGenerator, IMFMediaStream derives from
+ // IMFMediaEventGenerator.
+ IFACEMETHODIMP GetEvent(DWORD aFlags, IMFMediaEvent** aEvent) override;
+ IFACEMETHODIMP BeginGetEvent(IMFAsyncCallback* aCallback,
+ IUnknown* aState) override;
+ IFACEMETHODIMP EndGetEvent(IMFAsyncResult* aResult,
+ IMFMediaEvent** aEvent) override;
+ IFACEMETHODIMP QueueEvent(MediaEventType aType, REFGUID aExtendedType,
+ HRESULT aStatus,
+ const PROPVARIANT* aValue) override;
+
+ TaskQueue* GetTaskQueue() { return mTaskQueue; }
+
+ void NotifyEndOfStream() {
+ Microsoft::WRL::ComPtr<MFMediaEngineStream> self = this;
+ Unused << mTaskQueue->Dispatch(NS_NewRunnableFunction(
+ "MFMediaEngineStream::NotifyEndOfStream",
+ [self]() { self->NotifyEndOfStreamInternal(); }));
+ }
+
+ // Return the type of the track, the result should be either audio or video.
+ virtual TrackInfo::TrackType TrackType() = 0;
+
+ RefPtr<MediaDataDecoder::FlushPromise> Flush();
+
+ MediaEventProducer<TrackInfo::TrackType>& EndedEvent() { return mEndedEvent; }
+
+ // True if the stream has been shutdown, it's a thread safe method.
+ bool IsShutdown() const { return mIsShutdown; }
+
+ virtual MFMediaEngineVideoStream* AsVideoStream() { return nullptr; }
+
+ RefPtr<MediaDataDecoder::DecodePromise> OutputData(
+ RefPtr<MediaRawData> aSample);
+
+ virtual RefPtr<MediaDataDecoder::DecodePromise> Drain();
+
+ virtual MediaDataDecoder::ConversionRequired NeedsConversion() const {
+ return MediaDataDecoder::ConversionRequired::kNeedNone;
+ }
+
+ virtual bool IsEncrypted() const = 0;
+
+ protected:
+ HRESULT GenerateStreamDescriptor(
+ Microsoft::WRL::ComPtr<IMFMediaType>& aMediaType);
+
+ // Create a IMFMediaType which includes the details about the stream.
+ // https://docs.microsoft.com/en-us/windows/win32/medfound/media-type-attributes
+ virtual HRESULT CreateMediaType(const TrackInfo& aInfo,
+ IMFMediaType** aMediaType) = 0;
+
+ // True if the stream already has enough raw data.
+ virtual bool HasEnoughRawData() const = 0;
+
+ HRESULT CreateInputSample(IMFSample** aSample);
+ void ReplySampleRequestIfPossible();
+ bool ShouldServeSamples() const;
+
+ void NotifyNewData(MediaRawData* aSample);
+ void NotifyEndOfStreamInternal();
+
+ virtual bool IsEnded() const;
+
+ // Overwrite this method if inherited class needs to perform clean up on the
+ // task queue when the stream gets shutdowned.
+ virtual void ShutdownCleanUpOnTaskQueue(){};
+
+ // Inherited class must implement this method to return decoded data. it
+ // should uses `mRawDataQueueForGeneratingOutput` to generate output.
+ virtual already_AddRefed<MediaData> OutputDataInternal() = 0;
+
+ void SendRequestSampleEvent(bool aIsEnough);
+
+ HRESULT AddEncryptAttributes(IMFSample* aSample,
+ const CryptoSample& aCryptoConfig);
+
+ void AssertOnTaskQueue() const;
+ void AssertOnMFThreadPool() const;
+
+ // IMFMediaEventQueue is thread-safe.
+ Microsoft::WRL::ComPtr<IMFMediaEventQueue> mMediaEventQueue;
+ Microsoft::WRL::ComPtr<IMFStreamDescriptor> mStreamDescriptor;
+ Microsoft::WRL::ComPtr<MFMediaSource> mParentSource;
+
+ // This an unique ID retrieved from the IMFStreamDescriptor.
+ DWORD mStreamDescriptorId = 0;
+
+ // A unique ID assigned by MFMediaSource, which won't be changed after first
+ // assignment.
+ uint64_t mStreamId = 0;
+
+ RefPtr<TaskQueue> mTaskQueue;
+
+ // This class would be run on three threads, MF thread pool, the source's
+ // task queue and MediaPDecoder (wrapper thread). Following members would be
+ // used across both threads so they need to be thread-safe.
+
+ // Modify on the MF thread pool, access from any threads.
+ Atomic<bool> mIsShutdown;
+
+ // True if the stream is selected by the media source.
+ // Modify on MF thread pool, access from any threads.
+ Atomic<bool> mIsSelected;
+
+ // A thread-safe queue storing input samples, which provides samples to the
+ // media engine.
+ MediaQueue<MediaRawData> mRawDataQueueForFeedingEngine;
+
+ // A thread-safe queue storing input samples, which would be used to generate
+ // decoded data.
+ MediaQueue<MediaRawData> mRawDataQueueForGeneratingOutput;
+
+ // Thread-safe members END
+
+ // Store sample request token, one token should be related with one output
+ // data. It's used on the task queue only.
+ std::queue<Microsoft::WRL::ComPtr<IUnknown>> mSampleRequestTokens;
+
+ // Notify when playback reachs the end for this track.
+ MediaEventProducer<TrackInfo::TrackType> mEndedEvent;
+
+ // True if the stream has received the last data, but it could be reset if the
+ // stream starts delivering more data. Used on the task queue only.
+ bool mReceivedEOS;
+};
+
+/**
+ * This wrapper helps to dispatch task onto the stream's task queue. Its methods
+ * are not thread-safe and would only be called on the IPC decoder manager
+ * thread.
+ */
+class MFMediaEngineStreamWrapper final : public MediaDataDecoder {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MFMediaEngineStreamWrapper, final);
+
+ MFMediaEngineStreamWrapper(MFMediaEngineStream* aStream,
+ TaskQueue* aTaskQueue,
+ const CreateDecoderParams& aParams)
+ : mStream(aStream), mTaskQueue(aTaskQueue) {
+ MOZ_ASSERT(mStream);
+ MOZ_ASSERT(mTaskQueue);
+ }
+
+ // Methods for MediaDataDecoder, they are all called on the remote
+ // decoder manager thread.
+ RefPtr<InitPromise> Init() override;
+ RefPtr<DecodePromise> Decode(MediaRawData* aSample) override;
+ RefPtr<DecodePromise> Drain() override;
+ RefPtr<FlushPromise> Flush() override;
+ RefPtr<ShutdownPromise> Shutdown() override;
+ nsCString GetDescriptionName() const override;
+ nsCString GetCodecName() const override;
+ ConversionRequired NeedsConversion() const override;
+
+ private:
+ ~MFMediaEngineStreamWrapper() = default;
+
+ Microsoft::WRL::ComPtr<MFMediaEngineStream> mStream;
+ RefPtr<TaskQueue> mTaskQueue;
+};
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_PLATFORM_WMF_MFMEDIAENGINESTREAM_H
diff --git a/dom/media/platforms/wmf/MFMediaEngineVideoStream.cpp b/dom/media/platforms/wmf/MFMediaEngineVideoStream.cpp
new file mode 100644
index 0000000000..6ac716ea15
--- /dev/null
+++ b/dom/media/platforms/wmf/MFMediaEngineVideoStream.cpp
@@ -0,0 +1,372 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MFMediaEngineVideoStream.h"
+
+#include "mozilla/layers/DcompSurfaceImage.h"
+#include "MFMediaEngineUtils.h"
+#include "mozilla/StaticPrefs_media.h"
+
+namespace mozilla {
+
+#define LOG(msg, ...) \
+ MOZ_LOG(gMFMediaEngineLog, LogLevel::Debug, \
+ ("MFMediaStream=%p (%s), " msg, this, \
+ this->GetDescriptionName().get(), ##__VA_ARGS__))
+
+#define LOGV(msg, ...) \
+ MOZ_LOG(gMFMediaEngineLog, LogLevel::Verbose, \
+ ("MFMediaStream=%p (%s), " msg, this, \
+ this->GetDescriptionName().get(), ##__VA_ARGS__))
+
+using Microsoft::WRL::ComPtr;
+using Microsoft::WRL::MakeAndInitialize;
+
+/* static */
+MFMediaEngineVideoStream* MFMediaEngineVideoStream::Create(
+ uint64_t aStreamId, const TrackInfo& aInfo, MFMediaSource* aParentSource) {
+ MFMediaEngineVideoStream* stream;
+ MOZ_ASSERT(aInfo.IsVideo());
+ if (FAILED(MakeAndInitialize<MFMediaEngineVideoStream>(
+ &stream, aStreamId, aInfo, aParentSource))) {
+ return nullptr;
+ }
+ stream->mStreamType =
+ GetStreamTypeFromMimeType(aInfo.GetAsVideoInfo()->mMimeType);
+ MOZ_ASSERT(StreamTypeIsVideo(stream->mStreamType));
+ stream->mHasReceivedInitialCreateDecoderConfig = false;
+ stream->SetDCompSurfaceHandle(INVALID_HANDLE_VALUE, gfx::IntSize{});
+ return stream;
+}
+
+void MFMediaEngineVideoStream::SetKnowsCompositor(
+ layers::KnowsCompositor* aKnowsCompositor) {
+ ComPtr<MFMediaEngineVideoStream> self = this;
+ Unused << mTaskQueue->Dispatch(NS_NewRunnableFunction(
+ "MFMediaEngineStream::SetKnowsCompositor",
+ [self, knowCompositor = RefPtr<layers::KnowsCompositor>{aKnowsCompositor},
+ this]() {
+ mKnowsCompositor = knowCompositor;
+ LOG("Set SetKnowsCompositor=%p", mKnowsCompositor.get());
+ ResolvePendingDrainPromiseIfNeeded();
+ }));
+}
+
+void MFMediaEngineVideoStream::SetDCompSurfaceHandle(HANDLE aDCompSurfaceHandle,
+ gfx::IntSize aDisplay) {
+ ComPtr<MFMediaEngineVideoStream> self = this;
+ Unused << mTaskQueue->Dispatch(NS_NewRunnableFunction(
+ "MFMediaEngineStream::SetDCompSurfaceHandle",
+ [self, aDCompSurfaceHandle, aDisplay, this]() {
+ if (mDCompSurfaceHandle == aDCompSurfaceHandle) {
+ return;
+ }
+ mDCompSurfaceHandle = aDCompSurfaceHandle;
+ mNeedRecreateImage = true;
+ {
+ MutexAutoLock lock(mMutex);
+ if (aDCompSurfaceHandle != INVALID_HANDLE_VALUE &&
+ aDisplay != mDisplay) {
+ LOG("Update display [%dx%d] -> [%dx%d]", mDisplay.Width(),
+ mDisplay.Height(), aDisplay.Width(), aDisplay.Height());
+ mDisplay = aDisplay;
+ }
+ }
+ LOG("Set DCompSurfaceHandle, handle=%p", mDCompSurfaceHandle);
+ ResolvePendingDrainPromiseIfNeeded();
+ }));
+}
+
+HRESULT MFMediaEngineVideoStream::CreateMediaType(const TrackInfo& aInfo,
+ IMFMediaType** aMediaType) {
+ auto& videoInfo = *aInfo.GetAsVideoInfo();
+ mIsEncrypted = videoInfo.mCrypto.IsEncrypted();
+
+ GUID subType = VideoMimeTypeToMediaFoundationSubtype(videoInfo.mMimeType);
+ NS_ENSURE_TRUE(subType != GUID_NULL, MF_E_TOPO_CODEC_NOT_FOUND);
+
+ // https://docs.microsoft.com/en-us/windows/win32/medfound/media-type-attributes
+ ComPtr<IMFMediaType> mediaType;
+ RETURN_IF_FAILED(wmf::MFCreateMediaType(&mediaType));
+ RETURN_IF_FAILED(mediaType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video));
+ RETURN_IF_FAILED(mediaType->SetGUID(MF_MT_SUBTYPE, subType));
+
+ const auto& image = videoInfo.mImage;
+ UINT32 imageWidth = image.Width();
+ UINT32 imageHeight = image.Height();
+ RETURN_IF_FAILED(MFSetAttributeSize(mediaType.Get(), MF_MT_FRAME_SIZE,
+ imageWidth, imageHeight));
+
+ UINT32 displayWidth = videoInfo.mDisplay.Width();
+ UINT32 displayHeight = videoInfo.mDisplay.Height();
+ {
+ MutexAutoLock lock(mMutex);
+ mDisplay = videoInfo.mDisplay;
+ }
+ // PAR = DAR / SAR = (DW / DH) / (SW / SH) = (DW * SH) / (DH * SW)
+ RETURN_IF_FAILED(MFSetAttributeRatio(
+ mediaType.Get(), MF_MT_PIXEL_ASPECT_RATIO, displayWidth * imageHeight,
+ displayHeight * imageWidth));
+
+ // https://docs.microsoft.com/en-us/windows/win32/api/mfobjects/ns-mfobjects-mfoffset
+ // The value of the MFOffset number is value + (fract / 65536.0f).
+ static const auto ToMFOffset = [](float aValue) {
+ MFOffset offset;
+ offset.value = static_cast<short>(aValue);
+ offset.fract = static_cast<WORD>(65536 * (aValue - offset.value));
+ return offset;
+ };
+ MFVideoArea area;
+ area.OffsetX = ToMFOffset(videoInfo.ImageRect().x);
+ area.OffsetY = ToMFOffset(videoInfo.ImageRect().y);
+ area.Area = {(LONG)imageWidth, (LONG)imageHeight};
+ RETURN_IF_FAILED(mediaType->SetBlob(MF_MT_GEOMETRIC_APERTURE, (UINT8*)&area,
+ sizeof(area)));
+
+ // https://docs.microsoft.com/en-us/windows/win32/api/mfapi/ne-mfapi-mfvideorotationformat
+ static const auto ToMFVideoRotationFormat =
+ [](VideoInfo::Rotation aRotation) {
+ using Rotation = VideoInfo::Rotation;
+ switch (aRotation) {
+ case Rotation::kDegree_0:
+ return MFVideoRotationFormat_0;
+ case Rotation::kDegree_90:
+ return MFVideoRotationFormat_90;
+ case Rotation::kDegree_180:
+ return MFVideoRotationFormat_180;
+ default:
+ MOZ_ASSERT(aRotation == Rotation::kDegree_270);
+ return MFVideoRotationFormat_270;
+ }
+ };
+ const auto rotation = ToMFVideoRotationFormat(videoInfo.mRotation);
+ RETURN_IF_FAILED(mediaType->SetUINT32(MF_MT_VIDEO_ROTATION, rotation));
+
+ static const auto ToMFVideoTransFunc =
+ [](const Maybe<gfx::YUVColorSpace>& aColorSpace) {
+ using YUVColorSpace = gfx::YUVColorSpace;
+ if (!aColorSpace) {
+ return MFVideoTransFunc_Unknown;
+ }
+ // https://docs.microsoft.com/en-us/windows/win32/api/mfobjects/ne-mfobjects-mfvideotransferfunction
+ switch (*aColorSpace) {
+ case YUVColorSpace::BT601:
+ case YUVColorSpace::BT709:
+ return MFVideoTransFunc_709;
+ case YUVColorSpace::BT2020:
+ return MFVideoTransFunc_2020;
+ case YUVColorSpace::Identity:
+ return MFVideoTransFunc_sRGB;
+ default:
+ return MFVideoTransFunc_Unknown;
+ }
+ };
+ const auto transFunc = ToMFVideoTransFunc(videoInfo.mColorSpace);
+ RETURN_IF_FAILED(mediaType->SetUINT32(MF_MT_TRANSFER_FUNCTION, transFunc));
+
+ static const auto ToMFVideoPrimaries =
+ [](const Maybe<gfx::YUVColorSpace>& aColorSpace) {
+ using YUVColorSpace = gfx::YUVColorSpace;
+ if (!aColorSpace) {
+ return MFVideoPrimaries_Unknown;
+ }
+ // https://docs.microsoft.com/en-us/windows/win32/api/mfobjects/ne-mfobjects-mfvideoprimaries
+ switch (*aColorSpace) {
+ case YUVColorSpace::BT601:
+ return MFVideoPrimaries_Unknown;
+ case YUVColorSpace::BT709:
+ return MFVideoPrimaries_BT709;
+ case YUVColorSpace::BT2020:
+ return MFVideoPrimaries_BT2020;
+ case YUVColorSpace::Identity:
+ return MFVideoPrimaries_BT709;
+ default:
+ return MFVideoPrimaries_Unknown;
+ }
+ };
+ const auto videoPrimaries = ToMFVideoPrimaries(videoInfo.mColorSpace);
+ RETURN_IF_FAILED(mediaType->SetUINT32(MF_MT_VIDEO_PRIMARIES, videoPrimaries));
+
+ LOG("Created video type, subtype=%s, image=[%ux%u], display=[%ux%u], "
+ "rotation=%s, tranFuns=%s, primaries=%s, encrypted=%d",
+ GUIDToStr(subType), imageWidth, imageHeight, displayWidth, displayHeight,
+ MFVideoRotationFormatToStr(rotation),
+ MFVideoTransferFunctionToStr(transFunc),
+ MFVideoPrimariesToStr(videoPrimaries), mIsEncrypted);
+ if (IsEncrypted()) {
+ ComPtr<IMFMediaType> protectedMediaType;
+ RETURN_IF_FAILED(wmf::MFWrapMediaType(mediaType.Get(),
+ MFMediaType_Protected, subType,
+ protectedMediaType.GetAddressOf()));
+ LOG("Wrap MFMediaType_Video into MFMediaType_Protected");
+ *aMediaType = protectedMediaType.Detach();
+ } else {
+ *aMediaType = mediaType.Detach();
+ }
+ return S_OK;
+}
+
+bool MFMediaEngineVideoStream::HasEnoughRawData() const {
+ // If more than this much raw video is queued, we'll hold off request more
+ // video.
+ return mRawDataQueueForFeedingEngine.Duration() >=
+ StaticPrefs::media_wmf_media_engine_raw_data_threshold_video();
+}
+
+bool MFMediaEngineVideoStream::IsDCompImageReady() {
+ AssertOnTaskQueue();
+ if (!mDCompSurfaceHandle || mDCompSurfaceHandle == INVALID_HANDLE_VALUE) {
+ LOGV("Can't create image without a valid dcomp surface handle");
+ return false;
+ }
+
+ if (!mKnowsCompositor) {
+ LOGV("Can't create image without the knows compositor");
+ return false;
+ }
+
+ if (!mDcompSurfaceImage || mNeedRecreateImage) {
+ MutexAutoLock lock(mMutex);
+ // DirectComposition only supports RGBA. We use DXGI_FORMAT_B8G8R8A8_UNORM
+ // as a default because we can't know what format the dcomp surface is.
+ // https://docs.microsoft.com/en-us/windows/win32/api/dcomp/nf-dcomp-idcompositionsurfacefactory-createsurface
+ mDcompSurfaceImage = new layers::DcompSurfaceImage(
+ mDCompSurfaceHandle, mDisplay, gfx::SurfaceFormat::B8G8R8A8,
+ mKnowsCompositor);
+ mNeedRecreateImage = false;
+ LOG("Created dcomp surface image, handle=%p, size=[%u,%u]",
+ mDCompSurfaceHandle, mDisplay.Width(), mDisplay.Height());
+ }
+ return true;
+}
+
+already_AddRefed<MediaData> MFMediaEngineVideoStream::OutputDataInternal() {
+ AssertOnTaskQueue();
+ if (mRawDataQueueForGeneratingOutput.GetSize() == 0 || !IsDCompImageReady()) {
+ return nullptr;
+ }
+ RefPtr<MediaRawData> sample = mRawDataQueueForGeneratingOutput.PopFront();
+ RefPtr<VideoData> output;
+ {
+ MutexAutoLock lock(mMutex);
+ output = VideoData::CreateFromImage(
+ mDisplay, sample->mOffset, sample->mTime, sample->mDuration,
+ mDcompSurfaceImage, sample->mKeyframe, sample->mTimecode);
+ }
+ return output.forget();
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> MFMediaEngineVideoStream::Drain() {
+ AssertOnTaskQueue();
+ MediaDataDecoder::DecodedData outputs;
+ if (!IsDCompImageReady()) {
+ LOGV("Waiting for dcomp image for draining");
+ return mPendingDrainPromise.Ensure(__func__);
+ }
+ return MFMediaEngineStream::Drain();
+}
+
+void MFMediaEngineVideoStream::ResolvePendingDrainPromiseIfNeeded() {
+ AssertOnTaskQueue();
+ if (mPendingDrainPromise.IsEmpty()) {
+ return;
+ }
+ if (!IsDCompImageReady()) {
+ return;
+ }
+ MediaDataDecoder::DecodedData outputs;
+ while (RefPtr<MediaData> outputData = OutputDataInternal()) {
+ outputs.AppendElement(outputData);
+ LOGV("Output data [%" PRId64 ",%" PRId64 "]",
+ outputData->mTime.ToMicroseconds(),
+ outputData->GetEndTime().ToMicroseconds());
+ }
+ mPendingDrainPromise.Resolve(std::move(outputs), __func__);
+ LOG("Resolved pending drain promise");
+}
+
+MediaDataDecoder::ConversionRequired MFMediaEngineVideoStream::NeedsConversion()
+ const {
+ return mStreamType == WMFStreamType::H264
+ ? MediaDataDecoder::ConversionRequired::kNeedAnnexB
+ : MediaDataDecoder::ConversionRequired::kNeedNone;
+}
+
+void MFMediaEngineVideoStream::SetConfig(const TrackInfo& aConfig) {
+ MOZ_ASSERT(aConfig.IsVideo());
+ ComPtr<MFMediaEngineStream> self = this;
+ Unused << mTaskQueue->Dispatch(
+ NS_NewRunnableFunction("MFMediaEngineStream::SetConfig",
+ [self, info = *aConfig.GetAsVideoInfo(), this]() {
+ if (mHasReceivedInitialCreateDecoderConfig) {
+ // Here indicating a new config for video,
+ // which is triggered by the media change
+ // monitor, so we need to update the config.
+ UpdateConfig(info);
+ }
+ mHasReceivedInitialCreateDecoderConfig = true;
+ }));
+}
+
+void MFMediaEngineVideoStream::UpdateConfig(const VideoInfo& aInfo) {
+ AssertOnTaskQueue();
+ // Disable explicit format change event for H264 to allow switching to the
+ // new stream without a full re-create, which will be much faster. This is
+ // also due to the fact that the MFT decoder can handle some format changes
+ // without a format change event. For format changes that the MFT decoder
+ // cannot support (e.g. codec change), the playback will fail later with
+ // MF_E_INVALIDMEDIATYPE (0xC00D36B4).
+ if (mStreamType == WMFStreamType::H264) {
+ return;
+ }
+
+ LOG("Video config changed, will update stream descriptor");
+ PROFILER_MARKER_TEXT("VideoConfigChange", MEDIA_PLAYBACK, {},
+ nsPrintfCString("stream=%s, id=%" PRIu64,
+ GetDescriptionName().get(), mStreamId));
+ ComPtr<IMFMediaType> mediaType;
+ RETURN_VOID_IF_FAILED(CreateMediaType(aInfo, mediaType.GetAddressOf()));
+ RETURN_VOID_IF_FAILED(GenerateStreamDescriptor(mediaType));
+ RETURN_VOID_IF_FAILED(mMediaEventQueue->QueueEventParamUnk(
+ MEStreamFormatChanged, GUID_NULL, S_OK, mediaType.Get()));
+}
+
+void MFMediaEngineVideoStream::ShutdownCleanUpOnTaskQueue() {
+ AssertOnTaskQueue();
+ mPendingDrainPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+}
+
+bool MFMediaEngineVideoStream::IsEnded() const {
+ AssertOnTaskQueue();
+ // If a video only contains one frame, the media engine won't return a decoded
+ // frame before we tell it the track is already ended. However, due to the
+ // constraint of our media pipeline, the format reader won't notify EOS until
+ // the draining finishes, which causes a deadlock. Therefore, we would
+ // consider having pending drain promise as a sign of EOS as well, in order to
+ // get the decoded frame and revolve the drain promise.
+ return (mReceivedEOS || !mPendingDrainPromise.IsEmpty()) &&
+ mRawDataQueueForFeedingEngine.GetSize() == 0;
+}
+
+bool MFMediaEngineVideoStream::IsEncrypted() const { return mIsEncrypted; }
+
+nsCString MFMediaEngineVideoStream::GetCodecName() const {
+ switch (mStreamType) {
+ case WMFStreamType::H264:
+ return "h264"_ns;
+ case WMFStreamType::VP8:
+ return "vp8"_ns;
+ case WMFStreamType::VP9:
+ return "vp9"_ns;
+ case WMFStreamType::AV1:
+ return "av1"_ns;
+ default:
+ return "unknown"_ns;
+ };
+}
+
+#undef LOG
+#undef LOGV
+
+} // namespace mozilla
diff --git a/dom/media/platforms/wmf/MFMediaEngineVideoStream.h b/dom/media/platforms/wmf/MFMediaEngineVideoStream.h
new file mode 100644
index 0000000000..df17c264e4
--- /dev/null
+++ b/dom/media/platforms/wmf/MFMediaEngineVideoStream.h
@@ -0,0 +1,107 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_PLATFORM_WMF_MFMEDIAENGINEVIDEOSTREAM_H
+#define DOM_MEDIA_PLATFORM_WMF_MFMEDIAENGINEVIDEOSTREAM_H
+
+#include "MFMediaEngineStream.h"
+#include "WMFUtils.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/Mutex.h"
+
+namespace mozilla {
+namespace layers {
+
+class Image;
+class DcompSurfaceImage;
+
+} // namespace layers
+
+class MFMediaSource;
+
+class MFMediaEngineVideoStream final : public MFMediaEngineStream {
+ public:
+ MFMediaEngineVideoStream() = default;
+
+ static MFMediaEngineVideoStream* Create(uint64_t aStreamId,
+ const TrackInfo& aInfo,
+ MFMediaSource* aParentSource);
+ nsCString GetDescriptionName() const override {
+ return "media engine video stream"_ns;
+ }
+
+ nsCString GetCodecName() const override;
+
+ TrackInfo::TrackType TrackType() override {
+ return TrackInfo::TrackType::kVideoTrack;
+ }
+
+ void SetKnowsCompositor(layers::KnowsCompositor* aKnowsCompositor);
+
+ void SetDCompSurfaceHandle(HANDLE aDCompSurfaceHandle, gfx::IntSize aDisplay);
+
+ MFMediaEngineVideoStream* AsVideoStream() override { return this; }
+
+ MediaDataDecoder::ConversionRequired NeedsConversion() const override;
+
+ // Called by MFMediaEngineParent when we are creating a video decoder for
+ // the remote decoder. This is used to detect if the inband video config
+ // change happens during playback.
+ void SetConfig(const TrackInfo& aConfig);
+
+ RefPtr<MediaDataDecoder::DecodePromise> Drain() override;
+
+ bool IsEncrypted() const override;
+
+ private:
+ HRESULT
+ CreateMediaType(const TrackInfo& aInfo, IMFMediaType** aMediaType) override;
+
+ bool HasEnoughRawData() const override;
+
+ void UpdateConfig(const VideoInfo& aInfo);
+
+ already_AddRefed<MediaData> OutputDataInternal() override;
+
+ bool IsDCompImageReady();
+
+ void ResolvePendingDrainPromiseIfNeeded();
+
+ void ShutdownCleanUpOnTaskQueue() override;
+
+ bool IsEnded() const override;
+
+ // Task queue only members.
+ HANDLE mDCompSurfaceHandle;
+ bool mNeedRecreateImage;
+ RefPtr<layers::KnowsCompositor> mKnowsCompositor;
+
+ Mutex mMutex{"MFMediaEngineVideoStream"};
+ gfx::IntSize mDisplay MOZ_GUARDED_BY(mMutex);
+
+ // Set on the initialization, won't be changed after that.
+ WMFStreamType mStreamType;
+
+ // Created and accessed in the decoder thread.
+ RefPtr<layers::DcompSurfaceImage> mDcompSurfaceImage;
+
+ // This flag is used to check if the video config changes detected by the
+ // media config monitor. When the video decoder get created first, we will set
+ // this flag to true, then we know any config being set afterward indicating
+ // a new config change.
+ bool mHasReceivedInitialCreateDecoderConfig;
+
+ // When draining, the track should return all decoded data. However, if the
+ // dcomp image hasn't been ready yet, then we won't have any decoded data to
+ // return. This promise is used for that case, and will be resolved once we
+ // have dcomp image.
+ MozPromiseHolder<MediaDataDecoder::DecodePromise> mPendingDrainPromise;
+
+ // Set when `CreateMediaType()` is called.
+ bool mIsEncrypted = false;
+};
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_PLATFORM_WMF_MFMEDIAENGINEVIDEOSTREAM_H
diff --git a/dom/media/platforms/wmf/MFMediaSource.cpp b/dom/media/platforms/wmf/MFMediaSource.cpp
new file mode 100644
index 0000000000..ace3c7988c
--- /dev/null
+++ b/dom/media/platforms/wmf/MFMediaSource.cpp
@@ -0,0 +1,605 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MFMediaSource.h"
+
+#include <mfapi.h>
+#include <mfidl.h>
+#include <stdint.h>
+
+#include "MFCDMProxy.h"
+#include "MFMediaEngineAudioStream.h"
+#include "MFMediaEngineUtils.h"
+#include "MFMediaEngineVideoStream.h"
+#include "VideoUtils.h"
+#include "WMF.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/TaskQueue.h"
+
+namespace mozilla {
+
+#define LOG(msg, ...) \
+ MOZ_LOG(gMFMediaEngineLog, LogLevel::Debug, \
+ ("MFMediaSource=%p, " msg, this, ##__VA_ARGS__))
+
+using Microsoft::WRL::ComPtr;
+
+MFMediaSource::MFMediaSource()
+ : mPresentationEnded(false), mIsAudioEnded(false), mIsVideoEnded(false) {
+ MOZ_COUNT_CTOR(MFMediaSource);
+}
+
+MFMediaSource::~MFMediaSource() {
+ // TODO : notify cdm about the last key id?
+ MOZ_COUNT_DTOR(MFMediaSource);
+}
+
+HRESULT MFMediaSource::RuntimeClassInitialize(
+ const Maybe<AudioInfo>& aAudio, const Maybe<VideoInfo>& aVideo,
+ nsISerialEventTarget* aManagerThread) {
+ // On manager thread.
+ MutexAutoLock lock(mMutex);
+
+ static uint64_t streamId = 1;
+
+ mTaskQueue = TaskQueue::Create(
+ GetMediaThreadPool(MediaThreadType::PLATFORM_DECODER), "MFMediaSource");
+ mManagerThread = aManagerThread;
+ MOZ_ASSERT(mManagerThread, "manager thread shouldn't be nullptr!");
+
+ if (aAudio) {
+ mAudioStream.Attach(
+ MFMediaEngineAudioStream::Create(streamId++, *aAudio, this));
+ if (!mAudioStream) {
+ NS_WARNING("Failed to create audio stream");
+ return E_FAIL;
+ }
+ mAudioStreamEndedListener = mAudioStream->EndedEvent().Connect(
+ mManagerThread, this, &MFMediaSource::HandleStreamEnded);
+ } else {
+ mIsAudioEnded = true;
+ }
+
+ if (aVideo) {
+ mVideoStream.Attach(
+ MFMediaEngineVideoStream::Create(streamId++, *aVideo, this));
+ if (!mVideoStream) {
+ NS_WARNING("Failed to create video stream");
+ return E_FAIL;
+ }
+ mVideoStreamEndedListener = mVideoStream->EndedEvent().Connect(
+ mManagerThread, this, &MFMediaSource::HandleStreamEnded);
+ } else {
+ mIsVideoEnded = true;
+ }
+
+ RETURN_IF_FAILED(wmf::MFCreateEventQueue(&mMediaEventQueue));
+
+ LOG("Initialized a media source");
+ return S_OK;
+}
+
+IFACEMETHODIMP MFMediaSource::GetCharacteristics(DWORD* aCharacteristics) {
+ // This could be run on both mf thread pool and manager thread.
+ {
+ MutexAutoLock lock(mMutex);
+ if (mState == State::Shutdowned) {
+ return MF_E_SHUTDOWN;
+ }
+ }
+ // https://docs.microsoft.com/en-us/windows/win32/api/mfidl/ne-mfidl-mfmediasource_characteristics
+ *aCharacteristics = MFMEDIASOURCE_CAN_SEEK | MFMEDIASOURCE_CAN_PAUSE;
+ return S_OK;
+}
+
+IFACEMETHODIMP MFMediaSource::CreatePresentationDescriptor(
+ IMFPresentationDescriptor** aPresentationDescriptor) {
+ AssertOnMFThreadPool();
+ MutexAutoLock lock(mMutex);
+ if (mState == State::Shutdowned) {
+ return MF_E_SHUTDOWN;
+ }
+
+ LOG("CreatePresentationDescriptor");
+ // See steps of creating the presentation descriptor
+ // https://docs.microsoft.com/en-us/windows/win32/medfound/writing-a-custom-media-source#creating-the-presentation-descriptor
+ ComPtr<IMFPresentationDescriptor> presentationDescriptor;
+ nsTArray<ComPtr<IMFStreamDescriptor>> streamDescriptors;
+
+ DWORD audioDescriptorId = 0, videoDescriptorId = 0;
+ if (mAudioStream) {
+ ComPtr<IMFStreamDescriptor>* descriptor = streamDescriptors.AppendElement();
+ RETURN_IF_FAILED(
+ mAudioStream->GetStreamDescriptor(descriptor->GetAddressOf()));
+ audioDescriptorId = mAudioStream->DescriptorId();
+ }
+
+ if (mVideoStream) {
+ ComPtr<IMFStreamDescriptor>* descriptor = streamDescriptors.AppendElement();
+ RETURN_IF_FAILED(
+ mVideoStream->GetStreamDescriptor(descriptor->GetAddressOf()));
+ videoDescriptorId = mVideoStream->DescriptorId();
+ }
+
+ const DWORD descCount = static_cast<DWORD>(streamDescriptors.Length());
+ MOZ_ASSERT(descCount <= 2);
+ RETURN_IF_FAILED(wmf::MFCreatePresentationDescriptor(
+ descCount,
+ reinterpret_cast<IMFStreamDescriptor**>(streamDescriptors.Elements()),
+ &presentationDescriptor));
+
+ // Select default streams for the presentation descriptor.
+ for (DWORD idx = 0; idx < descCount; idx++) {
+ ComPtr<IMFStreamDescriptor> streamDescriptor;
+ BOOL selected;
+ RETURN_IF_FAILED(presentationDescriptor->GetStreamDescriptorByIndex(
+ idx, &selected, &streamDescriptor));
+ if (selected) {
+ continue;
+ }
+ RETURN_IF_FAILED(presentationDescriptor->SelectStream(idx));
+ DWORD streamId;
+ streamDescriptor->GetStreamIdentifier(&streamId);
+ LOG(" Select stream (id=%lu)", streamId);
+ }
+
+ LOG("Created a presentation descriptor (a=%lu,v=%lu)", audioDescriptorId,
+ videoDescriptorId);
+ *aPresentationDescriptor = presentationDescriptor.Detach();
+ return S_OK;
+}
+
+IFACEMETHODIMP MFMediaSource::Start(
+ IMFPresentationDescriptor* aPresentationDescriptor,
+ const GUID* aGuidTimeFormat, const PROPVARIANT* aStartPosition) {
+ AssertOnMFThreadPool();
+ MutexAutoLock lock(mMutex);
+ if (mState == State::Shutdowned) {
+ return MF_E_SHUTDOWN;
+ }
+
+ // See detailed steps in following documents.
+ // https://docs.microsoft.com/en-us/windows/win32/api/mfidl/nf-mfidl-imfmediasource-start
+ // https://docs.microsoft.com/en-us/windows/win32/medfound/writing-a-custom-media-source#starting-the-media-source
+
+ // A call to Start results in a seek if the previous state was started or
+ // paused, and the new starting position is not VT_EMPTY.
+ const bool isSeeking =
+ IsSeekable() && ((mState == State::Started || mState == State::Paused) &&
+ aStartPosition->vt != VT_EMPTY);
+ nsAutoCString startPosition;
+ if (aStartPosition->vt == VT_I8) {
+ startPosition.AppendInt(aStartPosition->hVal.QuadPart);
+ } else if (aStartPosition->vt == VT_EMPTY) {
+ startPosition.AppendLiteral("empty");
+ }
+ LOG("Start, start position=%s, isSeeking=%d", startPosition.get(), isSeeking);
+
+ // Ask IMFMediaStream to send stream events.
+ DWORD streamDescCount = 0;
+ RETURN_IF_FAILED(
+ aPresentationDescriptor->GetStreamDescriptorCount(&streamDescCount));
+
+ // TODO : should event orders be exactly same as msdn's order?
+ for (DWORD idx = 0; idx < streamDescCount; idx++) {
+ ComPtr<IMFStreamDescriptor> streamDescriptor;
+ BOOL selected;
+ RETURN_IF_FAILED(aPresentationDescriptor->GetStreamDescriptorByIndex(
+ idx, &selected, &streamDescriptor));
+
+ DWORD streamId;
+ RETURN_IF_FAILED(streamDescriptor->GetStreamIdentifier(&streamId));
+
+ ComPtr<MFMediaEngineStream> stream;
+ if (mAudioStream && mAudioStream->DescriptorId() == streamId) {
+ stream = mAudioStream;
+ } else if (mVideoStream && mVideoStream->DescriptorId() == streamId) {
+ stream = mVideoStream;
+ }
+ NS_ENSURE_TRUE(stream, MF_E_INVALIDREQUEST);
+
+ if (selected) {
+ RETURN_IF_FAILED(mMediaEventQueue->QueueEventParamUnk(
+ stream->IsSelected() ? MEUpdatedStream : MENewStream, GUID_NULL, S_OK,
+ stream.Get()));
+ // Need to select stream first before doing other operations.
+ stream->SetSelected(true);
+ if (isSeeking) {
+ RETURN_IF_FAILED(stream->Seek(aStartPosition));
+ } else {
+ RETURN_IF_FAILED(stream->Start(aStartPosition));
+ }
+ } else {
+ stream->SetSelected(false);
+ }
+ }
+
+ // Send source event.
+ RETURN_IF_FAILED(QueueEvent(isSeeking ? MESourceSeeked : MESourceStarted,
+ GUID_NULL, S_OK, aStartPosition));
+ mState = State::Started;
+ mPresentationEnded = false;
+ if (mAudioStream && mAudioStream->IsSelected()) {
+ mIsAudioEnded = false;
+ }
+ if (mVideoStream && mVideoStream->IsSelected()) {
+ mIsVideoEnded = false;
+ }
+ LOG("Started media source");
+ return S_OK;
+}
+
+IFACEMETHODIMP MFMediaSource::Stop() {
+ AssertOnMFThreadPool();
+ MutexAutoLock lock(mMutex);
+ if (mState == State::Shutdowned) {
+ return MF_E_SHUTDOWN;
+ }
+
+ LOG("Stop");
+ RETURN_IF_FAILED(QueueEvent(MESourceStopped, GUID_NULL, S_OK, nullptr));
+ if (mAudioStream) {
+ RETURN_IF_FAILED(mAudioStream->Stop());
+ }
+ if (mVideoStream) {
+ RETURN_IF_FAILED(mVideoStream->Stop());
+ }
+
+ mState = State::Stopped;
+ LOG("Stopped media source");
+ return S_OK;
+}
+
+IFACEMETHODIMP MFMediaSource::Pause() {
+ AssertOnMFThreadPool();
+ MutexAutoLock lock(mMutex);
+ if (mState == State::Shutdowned) {
+ return MF_E_SHUTDOWN;
+ }
+ if (mState != State::Started) {
+ return MF_E_INVALID_STATE_TRANSITION;
+ }
+
+ LOG("Pause");
+ RETURN_IF_FAILED(QueueEvent(MESourcePaused, GUID_NULL, S_OK, nullptr));
+ if (mAudioStream) {
+ RETURN_IF_FAILED(mAudioStream->Pause());
+ }
+ if (mVideoStream) {
+ RETURN_IF_FAILED(mVideoStream->Pause());
+ }
+
+ mState = State::Paused;
+ LOG("Paused media source");
+ return S_OK;
+}
+
+IFACEMETHODIMP MFMediaSource::Shutdown() {
+ // Could be called on either manager thread or MF thread pool.
+ MutexAutoLock lock(mMutex);
+ if (mState == State::Shutdowned) {
+ return MF_E_SHUTDOWN;
+ }
+
+ LOG("Shutdown");
+ // After this method is called, all IMFMediaEventQueue methods return
+ // MF_E_SHUTDOWN.
+ RETURN_IF_FAILED(mMediaEventQueue->Shutdown());
+ mState = State::Shutdowned;
+ LOG("Shutdowned media source");
+ return S_OK;
+}
+
+void MFMediaSource::ShutdownTaskQueue() {
+ AssertOnManagerThread();
+ LOG("ShutdownTaskQueue");
+ MutexAutoLock lock(mMutex);
+ if (mAudioStream) {
+ mAudioStream->Shutdown();
+ mAudioStream = nullptr;
+ mAudioStreamEndedListener.DisconnectIfExists();
+ }
+ if (mVideoStream) {
+ mVideoStream->Shutdown();
+ mVideoStream = nullptr;
+ mVideoStreamEndedListener.DisconnectIfExists();
+ }
+ Unused << mTaskQueue->BeginShutdown();
+ mTaskQueue = nullptr;
+}
+
+IFACEMETHODIMP MFMediaSource::GetEvent(DWORD aFlags, IMFMediaEvent** aEvent) {
+ MOZ_ASSERT(mMediaEventQueue);
+ return mMediaEventQueue->GetEvent(aFlags, aEvent);
+}
+
+IFACEMETHODIMP MFMediaSource::BeginGetEvent(IMFAsyncCallback* aCallback,
+ IUnknown* aState) {
+ MOZ_ASSERT(mMediaEventQueue);
+ return mMediaEventQueue->BeginGetEvent(aCallback, aState);
+}
+
+IFACEMETHODIMP MFMediaSource::EndGetEvent(IMFAsyncResult* aResult,
+ IMFMediaEvent** aEvent) {
+ MOZ_ASSERT(mMediaEventQueue);
+ return mMediaEventQueue->EndGetEvent(aResult, aEvent);
+}
+
+IFACEMETHODIMP MFMediaSource::QueueEvent(MediaEventType aType,
+ REFGUID aExtendedType, HRESULT aStatus,
+ const PROPVARIANT* aValue) {
+ MOZ_ASSERT(mMediaEventQueue);
+ RETURN_IF_FAILED(mMediaEventQueue->QueueEventParamVar(aType, aExtendedType,
+ aStatus, aValue));
+ LOG("Queued event %s", MediaEventTypeToStr(aType));
+ PROFILER_MARKER_TEXT("MFMediaSource::QueueEvent", MEDIA_PLAYBACK, {},
+ nsPrintfCString("%s", MediaEventTypeToStr(aType)));
+ return S_OK;
+}
+
+bool MFMediaSource::IsSeekable() const {
+ // TODO : check seekable from info.
+ return true;
+}
+
+void MFMediaSource::NotifyEndOfStream(TrackInfo::TrackType aType) {
+ AssertOnManagerThread();
+ MutexAutoLock lock(mMutex);
+ if (mState == State::Shutdowned) {
+ return;
+ }
+ if (aType == TrackInfo::TrackType::kAudioTrack) {
+ MOZ_ASSERT(mAudioStream);
+ mAudioStream->NotifyEndOfStream();
+ } else if (aType == TrackInfo::TrackType::kVideoTrack) {
+ MOZ_ASSERT(mVideoStream);
+ mVideoStream->NotifyEndOfStream();
+ }
+}
+
+void MFMediaSource::HandleStreamEnded(TrackInfo::TrackType aType) {
+ AssertOnManagerThread();
+ MutexAutoLock lock(mMutex);
+ if (mState == State::Shutdowned) {
+ return;
+ }
+ if (mPresentationEnded) {
+ LOG("Presentation is ended already");
+ RETURN_VOID_IF_FAILED(
+ QueueEvent(MEEndOfPresentation, GUID_NULL, S_OK, nullptr));
+ return;
+ }
+
+ LOG("Handle %s stream ended", TrackTypeToStr(aType));
+ if (aType == TrackInfo::TrackType::kAudioTrack) {
+ mIsAudioEnded = true;
+ } else if (aType == TrackInfo::TrackType::kVideoTrack) {
+ mIsVideoEnded = true;
+ } else {
+ MOZ_ASSERT_UNREACHABLE("Incorrect track type!");
+ }
+ mPresentationEnded = mIsAudioEnded && mIsVideoEnded;
+ LOG("PresentationEnded=%d, audioEnded=%d, videoEnded=%d",
+ !!mPresentationEnded, mIsAudioEnded, mIsVideoEnded);
+ PROFILER_MARKER_TEXT(
+ " MFMediaSource::HandleStreamEnded", MEDIA_PLAYBACK, {},
+ nsPrintfCString("PresentationEnded=%d, audioEnded=%d, videoEnded=%d",
+ !!mPresentationEnded, mIsAudioEnded, mIsVideoEnded));
+ if (mPresentationEnded) {
+ RETURN_VOID_IF_FAILED(
+ QueueEvent(MEEndOfPresentation, GUID_NULL, S_OK, nullptr));
+ }
+}
+
+void MFMediaSource::SetDCompSurfaceHandle(HANDLE aDCompSurfaceHandle,
+ gfx::IntSize aDisplay) {
+ AssertOnManagerThread();
+ MutexAutoLock lock(mMutex);
+ if (mVideoStream) {
+ mVideoStream->AsVideoStream()->SetDCompSurfaceHandle(aDCompSurfaceHandle,
+ aDisplay);
+ }
+}
+
+IFACEMETHODIMP MFMediaSource::GetService(REFGUID aGuidService, REFIID aRiid,
+ LPVOID* aResult) {
+ if (!IsEqualGUID(aGuidService, MF_RATE_CONTROL_SERVICE)) {
+ return MF_E_UNSUPPORTED_SERVICE;
+ }
+ return QueryInterface(aRiid, aResult);
+}
+
+IFACEMETHODIMP MFMediaSource::GetSlowestRate(MFRATE_DIRECTION aDirection,
+ BOOL aSupportsThinning,
+ float* aRate) {
+ AssertOnMFThreadPool();
+ MOZ_ASSERT(aRate);
+ *aRate = 0.0f;
+ {
+ MutexAutoLock lock(mMutex);
+ if (mState == State::Shutdowned) {
+ return MF_E_SHUTDOWN;
+ }
+ }
+ if (aDirection == MFRATE_REVERSE) {
+ return MF_E_REVERSE_UNSUPPORTED;
+ }
+ return S_OK;
+}
+
+IFACEMETHODIMP MFMediaSource::GetFastestRate(MFRATE_DIRECTION aDirection,
+ BOOL aSupportsThinning,
+ float* aRate) {
+ AssertOnMFThreadPool();
+ MOZ_ASSERT(aRate);
+ {
+ MutexAutoLock lock(mMutex);
+ if (mState == State::Shutdowned) {
+ *aRate = 0.0f;
+ return MF_E_SHUTDOWN;
+ }
+ }
+ if (aDirection == MFRATE_REVERSE) {
+ return MF_E_REVERSE_UNSUPPORTED;
+ }
+ *aRate = 16.0f;
+ return S_OK;
+}
+
+IFACEMETHODIMP MFMediaSource::IsRateSupported(BOOL aSupportsThinning,
+ float aNewRate,
+ float* aSupportedRate) {
+ AssertOnMFThreadPool();
+ {
+ MutexAutoLock lock(mMutex);
+ if (mState == State::Shutdowned) {
+ return MF_E_SHUTDOWN;
+ }
+ }
+
+ if (aSupportedRate) {
+ *aSupportedRate = 0.0f;
+ }
+
+ MFRATE_DIRECTION direction = aNewRate >= 0 ? MFRATE_FORWARD : MFRATE_REVERSE;
+ float fastestRate = 0.0f, slowestRate = 0.0f;
+ GetFastestRate(direction, aSupportsThinning, &fastestRate);
+ GetSlowestRate(direction, aSupportsThinning, &slowestRate);
+
+ if (aSupportsThinning) {
+ return MF_E_THINNING_UNSUPPORTED;
+ } else if (aNewRate < slowestRate) {
+ return MF_E_REVERSE_UNSUPPORTED;
+ } else if (aNewRate > fastestRate) {
+ return MF_E_UNSUPPORTED_RATE;
+ }
+
+ if (aSupportedRate) {
+ *aSupportedRate = aNewRate;
+ }
+ return S_OK;
+}
+
+IFACEMETHODIMP MFMediaSource::SetRate(BOOL aSupportsThinning, float aRate) {
+ AssertOnMFThreadPool();
+ {
+ MutexAutoLock lock(mMutex);
+ if (mState == State::Shutdowned) {
+ return MF_E_SHUTDOWN;
+ }
+ }
+
+ HRESULT hr = IsRateSupported(aSupportsThinning, aRate, &mPlaybackRate);
+ if (FAILED(hr)) {
+ LOG("Unsupported playback rate %f, error=%lX", aRate, hr);
+ return hr;
+ }
+
+ PROPVARIANT varRate;
+ varRate.vt = VT_R4;
+ varRate.fltVal = mPlaybackRate;
+ LOG("Set playback rate %f", mPlaybackRate);
+ return QueueEvent(MESourceRateChanged, GUID_NULL, S_OK, &varRate);
+}
+
+IFACEMETHODIMP MFMediaSource::GetRate(BOOL* aSupportsThinning, float* aRate) {
+ AssertOnMFThreadPool();
+ {
+ MutexAutoLock lock(mMutex);
+ if (mState == State::Shutdowned) {
+ return MF_E_SHUTDOWN;
+ }
+ }
+ *aSupportsThinning = FALSE;
+ *aRate = mPlaybackRate;
+ return S_OK;
+}
+
+HRESULT MFMediaSource::GetInputTrustAuthority(DWORD aStreamId, REFIID aRiid,
+ IUnknown** aITAOut) {
+ // TODO : add threading assertion, not sure what thread it would be running on
+ // now.
+ {
+ MutexAutoLock lock(mMutex);
+ if (mState == State::Shutdowned) {
+ return MF_E_SHUTDOWN;
+ }
+ }
+#ifdef MOZ_WMF_CDM
+ if (!mCDMProxy) {
+ return MF_E_NOT_PROTECTED;
+ }
+
+ // TODO : verify if this aStreamId is really matching our stream id or not.
+ ComPtr<MFMediaEngineStream> stream = GetStreamByIndentifier(aStreamId);
+ if (!stream) {
+ return E_INVALIDARG;
+ }
+
+ if (!stream->IsEncrypted()) {
+ return MF_E_NOT_PROTECTED;
+ }
+
+ RETURN_IF_FAILED(
+ mCDMProxy->GetInputTrustAuthority(aStreamId, nullptr, 0, aRiid, aITAOut));
+#endif
+ return S_OK;
+}
+
+MFMediaSource::State MFMediaSource::GetState() const {
+ MutexAutoLock lock(mMutex);
+ return mState;
+}
+
+MFMediaEngineStream* MFMediaSource::GetAudioStream() {
+ MutexAutoLock lock(mMutex);
+ return mAudioStream.Get();
+}
+MFMediaEngineStream* MFMediaSource::GetVideoStream() {
+ MutexAutoLock lock(mMutex);
+ return mVideoStream.Get();
+}
+
+MFMediaEngineStream* MFMediaSource::GetStreamByIndentifier(
+ DWORD aStreamId) const {
+ MutexAutoLock lock(mMutex);
+ if (mAudioStream && mAudioStream->DescriptorId() == aStreamId) {
+ return mAudioStream.Get();
+ }
+ if (mVideoStream && mVideoStream->DescriptorId() == aStreamId) {
+ return mVideoStream.Get();
+ }
+ return nullptr;
+}
+
+#ifdef MOZ_WMF_CDM
+void MFMediaSource::SetCDMProxy(MFCDMProxy* aCDMProxy) {
+ // TODO : add threading assertion, not sure what thread it would be running on
+ // now.
+ mCDMProxy = aCDMProxy;
+ // TODO : ask cdm proxy to refresh trusted input
+}
+#endif
+
+bool MFMediaSource::IsEncrypted() const {
+ MutexAutoLock lock(mMutex);
+ return (mAudioStream && mAudioStream->IsEncrypted()) ||
+ (mVideoStream && mVideoStream->IsEncrypted());
+}
+
+void MFMediaSource::AssertOnManagerThread() const {
+ MOZ_ASSERT(mManagerThread->IsOnCurrentThread());
+}
+
+void MFMediaSource::AssertOnMFThreadPool() const {
+ // We can't really assert the thread id from thread pool, because it would
+ // change any time. So we just assert this is not the manager thread, and use
+ // the explicit function name to indicate what thread we should run on.
+ MOZ_ASSERT(!mManagerThread->IsOnCurrentThread());
+}
+
+#undef LOG
+
+} // namespace mozilla
diff --git a/dom/media/platforms/wmf/MFMediaSource.h b/dom/media/platforms/wmf/MFMediaSource.h
new file mode 100644
index 0000000000..735d53579e
--- /dev/null
+++ b/dom/media/platforms/wmf/MFMediaSource.h
@@ -0,0 +1,188 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_PLATFORM_WMF_MFMEDIASOURCE_H
+#define DOM_MEDIA_PLATFORM_WMF_MFMEDIASOURCE_H
+
+#include <mfidl.h>
+#include <wrl.h>
+
+#include "MediaInfo.h"
+#include "MediaEventSource.h"
+#include "MFMediaEngineExtra.h"
+#include "MFMediaEngineStream.h"
+#include "mozilla/EnumSet.h"
+#include "mozilla/TaskQueue.h"
+
+namespace mozilla {
+
+class MFCDMProxy;
+
+// An event to indicate a need for a certain type of sample.
+struct SampleRequest {
+ SampleRequest(TrackInfo::TrackType aType, bool aIsEnough)
+ : mType(aType), mIsEnough(aIsEnough) {}
+ TrackInfo::TrackType mType;
+ bool mIsEnough;
+};
+
+/**
+ * MFMediaSource is a custom source for the media engine, the media engine would
+ * ask the source for the characteristics and the presentation descriptor to
+ * know how to react with the source. This source is also responsible to
+ * dispatch events to the media engine to notify the status changes.
+ *
+ * https://docs.microsoft.com/en-us/windows/win32/api/mfidl/nn-mfidl-imfmediasource
+ */
+class MFMediaSource : public Microsoft::WRL::RuntimeClass<
+ Microsoft::WRL::RuntimeClassFlags<
+ Microsoft::WRL::RuntimeClassType::ClassicCom>,
+ IMFMediaSource, IMFRateControl, IMFRateSupport,
+ IMFGetService, IMFTrustedInput> {
+ public:
+ MFMediaSource();
+ ~MFMediaSource();
+
+ HRESULT RuntimeClassInitialize(const Maybe<AudioInfo>& aAudio,
+ const Maybe<VideoInfo>& aVideo,
+ nsISerialEventTarget* aManagerThread);
+
+ // Methods for IMFMediaSource
+ IFACEMETHODIMP GetCharacteristics(DWORD* aCharacteristics) override;
+ IFACEMETHODIMP CreatePresentationDescriptor(
+ IMFPresentationDescriptor** aPresentationDescriptor) override;
+ IFACEMETHODIMP Start(IMFPresentationDescriptor* aPresentationDescriptor,
+ const GUID* aGuidTimeFormat,
+ const PROPVARIANT* aStartPosition) override;
+ IFACEMETHODIMP Stop() override;
+ IFACEMETHODIMP Pause() override;
+ IFACEMETHODIMP Shutdown() override;
+
+ // Methods for IMFMediaEventGenerator, IMFMediaSource derives from
+ // IMFMediaEventGenerator.
+ IFACEMETHODIMP GetEvent(DWORD aFlags, IMFMediaEvent** aEvent) override;
+ IFACEMETHODIMP BeginGetEvent(IMFAsyncCallback* aCallback,
+ IUnknown* aState) override;
+ IFACEMETHODIMP EndGetEvent(IMFAsyncResult* aResult,
+ IMFMediaEvent** aEvent) override;
+ IFACEMETHODIMP QueueEvent(MediaEventType aType, REFGUID aExtendedType,
+ HRESULT aStatus,
+ const PROPVARIANT* aValue) override;
+
+ // IMFGetService
+ IFACEMETHODIMP GetService(REFGUID aGuidService, REFIID aRiid,
+ LPVOID* aResult) override;
+
+ // IMFRateSupport
+ IFACEMETHODIMP GetSlowestRate(MFRATE_DIRECTION aDirection,
+ BOOL aSupportsThinning, float* aRate) override;
+ IFACEMETHODIMP GetFastestRate(MFRATE_DIRECTION aDirection,
+ BOOL aSupportsThinning, float* aRate) override;
+ IFACEMETHODIMP IsRateSupported(BOOL aSupportsThinning, float aNewRate,
+ float* aSupportedRate) override;
+
+ // IMFRateControl
+ IFACEMETHODIMP SetRate(BOOL aSupportsThinning, float aRate) override;
+ IFACEMETHODIMP GetRate(BOOL* aSupportsThinning, float* aRate) override;
+
+ // IMFTrustedInput
+ IFACEMETHODIMP GetInputTrustAuthority(DWORD aStreamId, REFIID aRiid,
+ IUnknown** aITAOut) override;
+
+ MFMediaEngineStream* GetAudioStream();
+ MFMediaEngineStream* GetVideoStream();
+
+ MFMediaEngineStream* GetStreamByIndentifier(DWORD aStreamId) const;
+
+#ifdef MOZ_WMF_CDM
+ void SetCDMProxy(MFCDMProxy* aCDMProxy);
+#endif
+
+ TaskQueue* GetTaskQueue() const { return mTaskQueue; }
+
+ MediaEventSource<SampleRequest>& RequestSampleEvent() {
+ return mRequestSampleEvent;
+ }
+
+ // Called from the content process to notify that no more encoded data in that
+ // type of track.
+ void NotifyEndOfStream(TrackInfo::TrackType aType);
+
+ // Called from the MF stream to indicate that the stream has provided last
+ // encoded sample to the media engine.
+ void HandleStreamEnded(TrackInfo::TrackType aType);
+
+ enum class State {
+ Initialized,
+ Started,
+ Stopped,
+ Paused,
+ Shutdowned,
+ };
+ State GetState() const;
+
+ void SetDCompSurfaceHandle(HANDLE aDCompSurfaceHandle, gfx::IntSize aDisplay);
+
+ void ShutdownTaskQueue();
+
+ bool IsEncrypted() const;
+
+ private:
+ void AssertOnManagerThread() const;
+ void AssertOnMFThreadPool() const;
+
+ void NotifyEndOfStreamInternal(TrackInfo::TrackType aType);
+
+ bool IsSeekable() const;
+
+ // A thread-safe event queue.
+ // https://docs.microsoft.com/en-us/windows/win32/medfound/media-event-generators#implementing-imfmediaeventgenerator
+ Microsoft::WRL::ComPtr<IMFMediaEventQueue> mMediaEventQueue;
+
+ // The thread used to run the engine streams' tasks.
+ RefPtr<TaskQueue> mTaskQueue;
+
+ // The thread used to run the media source's tasks.
+ RefPtr<nsISerialEventTarget> mManagerThread;
+
+ // MFMediaEngineStream will notify us when we need more sample.
+ friend class MFMediaEngineStream;
+ MediaEventProducer<SampleRequest> mRequestSampleEvent;
+
+ MediaEventListener mAudioStreamEndedListener;
+ MediaEventListener mVideoStreamEndedListener;
+
+ // This class would be run/accessed on two threads, MF thread pool and the
+ // manager thread. Following members could be used across threads so they need
+ // to be thread-safe.
+
+ mutable Mutex mMutex{"MFMediaEngineSource"};
+
+ // True if the playback is ended. Use and modify on both the manager thread
+ // and MF thread pool.
+ bool mPresentationEnded MOZ_GUARDED_BY(mMutex);
+ bool mIsAudioEnded MOZ_GUARDED_BY(mMutex);
+ bool mIsVideoEnded MOZ_GUARDED_BY(mMutex);
+
+ // Modify on MF thread pool and the manager thread, read on any threads.
+ State mState MOZ_GUARDED_BY(mMutex);
+
+ Microsoft::WRL::ComPtr<MFMediaEngineStream> mAudioStream
+ MOZ_GUARDED_BY(mMutex);
+ Microsoft::WRL::ComPtr<MFMediaEngineStream> mVideoStream
+ MOZ_GUARDED_BY(mMutex);
+
+ // Thread-safe members END
+
+ // Modify and access on MF thread pool.
+ float mPlaybackRate = 0.0f;
+
+#ifdef MOZ_WMF_CDM
+ RefPtr<MFCDMProxy> mCDMProxy;
+#endif
+};
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_PLATFORM_WMF_MFMEDIASOURCE_H
diff --git a/dom/media/platforms/wmf/MFPMPHostWrapper.cpp b/dom/media/platforms/wmf/MFPMPHostWrapper.cpp
new file mode 100644
index 0000000000..64266f4ad5
--- /dev/null
+++ b/dom/media/platforms/wmf/MFPMPHostWrapper.cpp
@@ -0,0 +1,66 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MFPMPHostWrapper.h"
+
+#include "MFMediaEngineUtils.h"
+#include "WMF.h"
+#include "mozilla/EMEUtils.h"
+
+namespace mozilla {
+
+using Microsoft::WRL::ComPtr;
+
+#define LOG(msg, ...) EME_LOG("MFPMPHostWrapper=%p, " msg, this, ##__VA_ARGS__)
+
+HRESULT MFPMPHostWrapper::RuntimeClassInitialize(
+ Microsoft::WRL::ComPtr<IMFPMPHost>& aHost) {
+ mPMPHost = aHost;
+ return S_OK;
+}
+
+STDMETHODIMP MFPMPHostWrapper::LockProcess() { return mPMPHost->LockProcess(); }
+
+STDMETHODIMP MFPMPHostWrapper::UnlockProcess() {
+ return mPMPHost->UnlockProcess();
+}
+
+STDMETHODIMP MFPMPHostWrapper::ActivateClassById(LPCWSTR aId, IStream* aStream,
+ REFIID aRiid,
+ void** aActivatedClass) {
+ LOG("ActivateClassById, id=%ls", aId);
+ ComPtr<IMFAttributes> creationAttributes;
+ RETURN_IF_FAILED(wmf::MFCreateAttributes(&creationAttributes, 2));
+ RETURN_IF_FAILED(creationAttributes->SetString(GUID_ClassName, aId));
+
+ if (aStream) {
+ STATSTG statstg;
+ RETURN_IF_FAILED(
+ aStream->Stat(&statstg, STATFLAG_NOOPEN | STATFLAG_NONAME));
+ nsTArray<uint8_t> streamBlob;
+ streamBlob.SetLength(statstg.cbSize.LowPart);
+ unsigned long readSize = 0;
+ RETURN_IF_FAILED(
+ aStream->Read(&streamBlob[0], streamBlob.Length(), &readSize));
+ RETURN_IF_FAILED(creationAttributes->SetBlob(GUID_ObjectStream,
+ &streamBlob[0], readSize));
+ }
+
+ ComPtr<IStream> outputStream;
+ RETURN_IF_FAILED(CreateStreamOnHGlobal(nullptr, TRUE, &outputStream));
+ RETURN_IF_FAILED(wmf::MFSerializeAttributesToStream(creationAttributes.Get(),
+ 0, outputStream.Get()));
+ RETURN_IF_FAILED(outputStream->Seek({}, STREAM_SEEK_SET, nullptr));
+
+ ComPtr<IMFActivate> activator;
+ RETURN_IF_FAILED(mPMPHost->CreateObjectByCLSID(
+ CLSID_EMEStoreActivate, outputStream.Get(), IID_PPV_ARGS(&activator)));
+ RETURN_IF_FAILED(activator->ActivateObject(aRiid, aActivatedClass));
+ LOG("Done ActivateClassById, id=%ls", aId);
+ return S_OK;
+}
+
+#undef LOG
+
+} // namespace mozilla
diff --git a/dom/media/platforms/wmf/MFPMPHostWrapper.h b/dom/media/platforms/wmf/MFPMPHostWrapper.h
new file mode 100644
index 0000000000..3b644283b7
--- /dev/null
+++ b/dom/media/platforms/wmf/MFPMPHostWrapper.h
@@ -0,0 +1,42 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_PLATFORM_WMF_MPMPHOSTWRAPPER_H
+#define DOM_MEDIA_PLATFORM_WMF_MPMPHOSTWRAPPER_H
+
+#include <wrl.h>
+#include <wrl/client.h>
+
+#include "MFCDMExtra.h"
+
+namespace mozilla {
+
+// This class is used to create and manage PMP sessions. For PlayReady CDM,
+// it needs to connect with IMFPMPHostApp first before generating any request.
+// That behavior is undocumented on the mdsn, see more details in
+// https://github.com/microsoft/media-foundation/issues/37#issuecomment-1197321484
+class MFPMPHostWrapper : public Microsoft::WRL::RuntimeClass<
+ Microsoft::WRL::RuntimeClassFlags<
+ Microsoft::WRL::RuntimeClassType::ClassicCom>,
+ IMFPMPHostApp> {
+ public:
+ MFPMPHostWrapper() = default;
+ ~MFPMPHostWrapper() = default;
+
+ HRESULT RuntimeClassInitialize(Microsoft::WRL::ComPtr<IMFPMPHost>& aHost);
+
+ STDMETHODIMP LockProcess() override;
+
+ STDMETHODIMP UnlockProcess() override;
+
+ STDMETHODIMP ActivateClassById(LPCWSTR aId, IStream* aStream, REFIID aRiid,
+ void** aActivatedClass) override;
+
+ private:
+ Microsoft::WRL::ComPtr<IMFPMPHost> mPMPHost;
+};
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_PLATFORM_WMF_MPMPHOSTWRAPPER_H
diff --git a/dom/media/platforms/wmf/MFTDecoder.cpp b/dom/media/platforms/wmf/MFTDecoder.cpp
new file mode 100644
index 0000000000..6b66a9e399
--- /dev/null
+++ b/dom/media/platforms/wmf/MFTDecoder.cpp
@@ -0,0 +1,430 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MFTDecoder.h"
+#include "WMFUtils.h"
+#include "mozilla/Logging.h"
+#include "nsThreadUtils.h"
+#include "mozilla/mscom/COMWrappers.h"
+#include "mozilla/mscom/Utils.h"
+#include "PlatformDecoderModule.h"
+
+#define LOG(...) MOZ_LOG(sPDMLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
+
+namespace mozilla {
+MFTDecoder::MFTDecoder() {
+ memset(&mInputStreamInfo, 0, sizeof(MFT_INPUT_STREAM_INFO));
+ memset(&mOutputStreamInfo, 0, sizeof(MFT_OUTPUT_STREAM_INFO));
+}
+
+MFTDecoder::~MFTDecoder() {
+ if (mActivate) {
+ // Releases all internal references to the created IMFTransform.
+ // https://docs.microsoft.com/en-us/windows/win32/api/mfobjects/nf-mfobjects-imfactivate-shutdownobject
+ mActivate->ShutdownObject();
+ }
+}
+
+HRESULT MFTDecoder::Create(const GUID& aCLSID) {
+ MOZ_ASSERT(mscom::IsCurrentThreadMTA());
+
+ HRESULT hr = mscom::wrapped::CoCreateInstance(
+ aCLSID, nullptr, CLSCTX_INPROC_SERVER,
+ IID_PPV_ARGS(static_cast<IMFTransform**>(getter_AddRefs(mDecoder))));
+ NS_WARNING_ASSERTION(SUCCEEDED(hr), "Failed to create MFT by CLSID");
+ return hr;
+}
+
+HRESULT
+MFTDecoder::Create(const GUID& aCategory, const GUID& aInSubtype,
+ const GUID& aOutSubtype) {
+ // Note: IMFTransform is documented to only be safe on MTA threads.
+ MOZ_ASSERT(mscom::IsCurrentThreadMTA());
+
+ // Use video by default, but select audio if necessary.
+ const GUID major = aCategory == MFT_CATEGORY_AUDIO_DECODER
+ ? MFMediaType_Audio
+ : MFMediaType_Video;
+
+ // Ignore null GUIDs to allow searching for all decoders supporting
+ // just one input or output type.
+ auto createInfo = [&major](const GUID& subtype) -> MFT_REGISTER_TYPE_INFO* {
+ if (IsEqualGUID(subtype, GUID_NULL)) {
+ return nullptr;
+ }
+
+ MFT_REGISTER_TYPE_INFO* info = new MFT_REGISTER_TYPE_INFO();
+ info->guidMajorType = major;
+ info->guidSubtype = subtype;
+ return info;
+ };
+ const MFT_REGISTER_TYPE_INFO* inInfo = createInfo(aInSubtype);
+ const MFT_REGISTER_TYPE_INFO* outInfo = createInfo(aOutSubtype);
+
+ // Request a decoder from the Windows API.
+ HRESULT hr;
+ IMFActivate** acts = nullptr;
+ UINT32 actsNum = 0;
+
+ hr = wmf::MFTEnumEx(aCategory, MFT_ENUM_FLAG_SORTANDFILTER, inInfo, outInfo,
+ &acts, &actsNum);
+ delete inInfo;
+ delete outInfo;
+ if (FAILED(hr)) {
+ NS_WARNING(nsPrintfCString("MFTEnumEx failed with code %lx", hr).get());
+ return hr;
+ }
+ if (actsNum == 0) {
+ NS_WARNING("MFTEnumEx returned no IMFActivate instances");
+ return WINCODEC_ERR_COMPONENTNOTFOUND;
+ }
+ auto guard = MakeScopeExit([&] {
+ // Start from index 1, acts[0] will be stored as a RefPtr to release later.
+ for (UINT32 i = 1; i < actsNum; i++) {
+ acts[i]->Release();
+ }
+ CoTaskMemFree(acts);
+ });
+
+ // Create the IMFTransform to do the decoding.
+ // Note: Ideally we would cache the IMFActivate and call
+ // IMFActivate::DetachObject, but doing so causes the MFTs to fail on
+ // MFT_MESSAGE_SET_D3D_MANAGER.
+ mActivate = RefPtr<IMFActivate>(acts[0]);
+ hr = mActivate->ActivateObject(
+ IID_PPV_ARGS(static_cast<IMFTransform**>(getter_AddRefs(mDecoder))));
+ NS_WARNING_ASSERTION(
+ SUCCEEDED(hr),
+ nsPrintfCString("IMFActivate::ActivateObject failed with code %lx", hr)
+ .get());
+ return hr;
+}
+
+HRESULT
+MFTDecoder::SetMediaTypes(IMFMediaType* aInputType, IMFMediaType* aOutputType,
+ std::function<HRESULT(IMFMediaType*)>&& aCallback) {
+ MOZ_ASSERT(mscom::IsCurrentThreadMTA());
+
+ // Set the input type to the one the caller gave us...
+ HRESULT hr = mDecoder->SetInputType(0, aInputType, 0);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ GUID currentSubtype = {0};
+ hr = aOutputType->GetGUID(MF_MT_SUBTYPE, &currentSubtype);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = SetDecoderOutputType(currentSubtype, aOutputType, std::move(aCallback));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = mDecoder->GetInputStreamInfo(0, &mInputStreamInfo);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = SendMFTMessage(MFT_MESSAGE_NOTIFY_BEGIN_STREAMING, 0);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = SendMFTMessage(MFT_MESSAGE_NOTIFY_START_OF_STREAM, 0);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ return S_OK;
+}
+
+already_AddRefed<IMFAttributes> MFTDecoder::GetAttributes() {
+ MOZ_ASSERT(mscom::IsCurrentThreadMTA());
+ RefPtr<IMFAttributes> attr;
+ HRESULT hr = mDecoder->GetAttributes(getter_AddRefs(attr));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+ return attr.forget();
+}
+
+already_AddRefed<IMFAttributes> MFTDecoder::GetOutputStreamAttributes() {
+ MOZ_ASSERT(mscom::IsCurrentThreadMTA());
+ RefPtr<IMFAttributes> attr;
+ HRESULT hr = mDecoder->GetOutputStreamAttributes(0, getter_AddRefs(attr));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+ return attr.forget();
+}
+
+HRESULT
+MFTDecoder::FindDecoderOutputType() {
+ MOZ_ASSERT(mscom::IsCurrentThreadMTA());
+ MOZ_ASSERT(mOutputType, "SetDecoderTypes must have been called once");
+
+ return FindDecoderOutputTypeWithSubtype(mOutputSubType);
+}
+
+HRESULT
+MFTDecoder::FindDecoderOutputTypeWithSubtype(const GUID& aSubType) {
+ return SetDecoderOutputType(aSubType, nullptr,
+ [](IMFMediaType*) { return S_OK; });
+}
+
+HRESULT
+MFTDecoder::SetDecoderOutputType(
+ const GUID& aSubType, IMFMediaType* aTypeToUse,
+ std::function<HRESULT(IMFMediaType*)>&& aCallback) {
+ MOZ_ASSERT(mscom::IsCurrentThreadMTA());
+ NS_ENSURE_TRUE(mDecoder != nullptr, E_POINTER);
+
+ if (!aTypeToUse) {
+ aTypeToUse = mOutputType;
+ }
+
+ // Iterate the enumerate the output types, until we find one compatible
+ // with what we need.
+ RefPtr<IMFMediaType> outputType;
+ UINT32 typeIndex = 0;
+ while (SUCCEEDED(mDecoder->GetOutputAvailableType(
+ 0, typeIndex++, getter_AddRefs(outputType)))) {
+ GUID outSubtype = {0};
+ HRESULT hr = outputType->GetGUID(MF_MT_SUBTYPE, &outSubtype);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ if (aSubType == outSubtype) {
+ hr = aCallback(outputType);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = mDecoder->SetOutputType(0, outputType, 0);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = mDecoder->GetOutputStreamInfo(0, &mOutputStreamInfo);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ mMFTProvidesOutputSamples = IsFlagSet(mOutputStreamInfo.dwFlags,
+ MFT_OUTPUT_STREAM_PROVIDES_SAMPLES);
+
+ mOutputType = outputType;
+ mOutputSubType = outSubtype;
+
+ return S_OK;
+ }
+ outputType = nullptr;
+ }
+ return E_FAIL;
+}
+
+HRESULT
+MFTDecoder::SendMFTMessage(MFT_MESSAGE_TYPE aMsg, ULONG_PTR aData) {
+ MOZ_ASSERT(mscom::IsCurrentThreadMTA());
+ NS_ENSURE_TRUE(mDecoder != nullptr, E_POINTER);
+ LOG("Send message '%s'", MFTMessageTypeToStr(aMsg));
+ HRESULT hr = mDecoder->ProcessMessage(aMsg, aData);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ return S_OK;
+}
+
+HRESULT
+MFTDecoder::CreateInputSample(const uint8_t* aData, uint32_t aDataSize,
+ int64_t aTimestamp, int64_t aDuration,
+ RefPtr<IMFSample>* aOutSample) {
+ MOZ_ASSERT(mscom::IsCurrentThreadMTA());
+ NS_ENSURE_TRUE(mDecoder != nullptr, E_POINTER);
+
+ HRESULT hr;
+ RefPtr<IMFSample> sample;
+ hr = wmf::MFCreateSample(getter_AddRefs(sample));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ RefPtr<IMFMediaBuffer> buffer;
+ int32_t bufferSize =
+ std::max<uint32_t>(uint32_t(mInputStreamInfo.cbSize), aDataSize);
+ UINT32 alignment =
+ (mInputStreamInfo.cbAlignment > 1) ? mInputStreamInfo.cbAlignment - 1 : 0;
+ hr = wmf::MFCreateAlignedMemoryBuffer(bufferSize, alignment,
+ getter_AddRefs(buffer));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ DWORD maxLength = 0;
+ DWORD currentLength = 0;
+ BYTE* dst = nullptr;
+ hr = buffer->Lock(&dst, &maxLength, &currentLength);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ // Copy data into sample's buffer.
+ memcpy(dst, aData, aDataSize);
+
+ hr = buffer->Unlock();
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = buffer->SetCurrentLength(aDataSize);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = sample->AddBuffer(buffer);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = sample->SetSampleTime(UsecsToHNs(aTimestamp));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ if (aDuration == 0) {
+ // If the sample duration is 0, the decoder will try and estimate the
+ // duration. In practice this can lead to some wildly incorrect durations,
+ // as in bug 1560440. The Microsoft docs seem conflicting here with
+ // `IMFSample::SetSampleDuration` stating 'The duration can also be zero.
+ // This might be valid for some types of data.' However,
+ // `IMFSample::GetSampleDuration method` states 'If the retrieved duration
+ // is zero, or if the method returns MF_E_NO_SAMPLE_DURATION, the duration
+ // is unknown. In that case, it might be possible to calculate the duration
+ // from the media type--for example, by using the video frame rate or the
+ // audio sampling rate.' The latter of those seems to be how the decoder
+ // handles 0 duration, hence why it estimates.
+ //
+ // Since our demuxing pipeline can create 0 duration samples, and since the
+ // decoder will override them to something positive anyway, setting them to
+ // have a trivial duration seems like the lesser of evils.
+ aDuration = 1;
+ }
+ hr = sample->SetSampleDuration(UsecsToHNs(aDuration));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ *aOutSample = sample.forget();
+
+ return S_OK;
+}
+
+HRESULT
+MFTDecoder::CreateOutputSample(RefPtr<IMFSample>* aOutSample) {
+ MOZ_ASSERT(mscom::IsCurrentThreadMTA());
+ NS_ENSURE_TRUE(mDecoder != nullptr, E_POINTER);
+
+ HRESULT hr;
+ RefPtr<IMFSample> sample;
+ hr = wmf::MFCreateSample(getter_AddRefs(sample));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ RefPtr<IMFMediaBuffer> buffer;
+ int32_t bufferSize = mOutputStreamInfo.cbSize;
+ UINT32 alignment = (mOutputStreamInfo.cbAlignment > 1)
+ ? mOutputStreamInfo.cbAlignment - 1
+ : 0;
+ hr = wmf::MFCreateAlignedMemoryBuffer(bufferSize, alignment,
+ getter_AddRefs(buffer));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = sample->AddBuffer(buffer);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ *aOutSample = sample.forget();
+
+ return S_OK;
+}
+
+HRESULT
+MFTDecoder::Output(RefPtr<IMFSample>* aOutput) {
+ MOZ_ASSERT(mscom::IsCurrentThreadMTA());
+ NS_ENSURE_TRUE(mDecoder != nullptr, E_POINTER);
+
+ HRESULT hr;
+
+ MFT_OUTPUT_DATA_BUFFER output = {0};
+
+ bool providedSample = false;
+ RefPtr<IMFSample> sample;
+ if (*aOutput) {
+ output.pSample = *aOutput;
+ providedSample = true;
+ } else if (!mMFTProvidesOutputSamples) {
+ hr = CreateOutputSample(&sample);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ output.pSample = sample;
+ }
+
+ DWORD status = 0;
+ hr = mDecoder->ProcessOutput(0, 1, &output, &status);
+ if (output.pEvents) {
+ // We must release this, as per the IMFTransform::ProcessOutput()
+ // MSDN documentation.
+ output.pEvents->Release();
+ output.pEvents = nullptr;
+ }
+
+ if (hr == MF_E_TRANSFORM_STREAM_CHANGE) {
+ return MF_E_TRANSFORM_STREAM_CHANGE;
+ }
+
+ if (hr == MF_E_TRANSFORM_NEED_MORE_INPUT) {
+ // Not enough input to produce output. This is an expected failure,
+ // so don't warn on encountering it.
+ return hr;
+ }
+ // Treat other errors as unexpected, and warn.
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ if (!output.pSample) {
+ return S_OK;
+ }
+
+ if (mDiscontinuity) {
+ output.pSample->SetUINT32(MFSampleExtension_Discontinuity, TRUE);
+ mDiscontinuity = false;
+ }
+
+ *aOutput = output.pSample; // AddRefs
+ if (mMFTProvidesOutputSamples && !providedSample) {
+ // If the MFT is providing samples, we must release the sample here.
+ // Typically only the H.264 MFT provides samples when using DXVA,
+ // and it always re-uses the same sample, so if we don't release it
+ // MFT::ProcessOutput() deadlocks waiting for the sample to be released.
+ output.pSample->Release();
+ output.pSample = nullptr;
+ }
+
+ return S_OK;
+}
+
+HRESULT
+MFTDecoder::Input(const uint8_t* aData, uint32_t aDataSize, int64_t aTimestamp,
+ int64_t aDuration) {
+ MOZ_ASSERT(mscom::IsCurrentThreadMTA());
+ NS_ENSURE_TRUE(mDecoder != nullptr, E_POINTER);
+
+ RefPtr<IMFSample> input;
+ HRESULT hr =
+ CreateInputSample(aData, aDataSize, aTimestamp, aDuration, &input);
+ NS_ENSURE_TRUE(SUCCEEDED(hr) && input != nullptr, hr);
+
+ return Input(input);
+}
+
+HRESULT
+MFTDecoder::Input(IMFSample* aSample) {
+ MOZ_ASSERT(mscom::IsCurrentThreadMTA());
+ HRESULT hr = mDecoder->ProcessInput(0, aSample, 0);
+ if (hr == MF_E_NOTACCEPTING) {
+ // MFT *already* has enough data to produce a sample. Retrieve it.
+ return MF_E_NOTACCEPTING;
+ }
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ return S_OK;
+}
+
+HRESULT
+MFTDecoder::Flush() {
+ MOZ_ASSERT(mscom::IsCurrentThreadMTA());
+ HRESULT hr = SendMFTMessage(MFT_MESSAGE_COMMAND_FLUSH, 0);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ mDiscontinuity = true;
+
+ return S_OK;
+}
+
+HRESULT
+MFTDecoder::GetInputMediaType(RefPtr<IMFMediaType>& aMediaType) {
+ MOZ_ASSERT(mscom::IsCurrentThreadMTA());
+ NS_ENSURE_TRUE(mDecoder, E_POINTER);
+ return mDecoder->GetInputCurrentType(0, getter_AddRefs(aMediaType));
+}
+
+HRESULT
+MFTDecoder::GetOutputMediaType(RefPtr<IMFMediaType>& aMediaType) {
+ MOZ_ASSERT(mscom::IsCurrentThreadMTA());
+ NS_ENSURE_TRUE(mDecoder, E_POINTER);
+ return mDecoder->GetOutputCurrentType(0, getter_AddRefs(aMediaType));
+}
+
+#undef LOG
+
+} // namespace mozilla
diff --git a/dom/media/platforms/wmf/MFTDecoder.h b/dom/media/platforms/wmf/MFTDecoder.h
new file mode 100644
index 0000000000..7af99e550d
--- /dev/null
+++ b/dom/media/platforms/wmf/MFTDecoder.h
@@ -0,0 +1,132 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(MFTDecoder_h_)
+# define MFTDecoder_h_
+
+# include "WMF.h"
+# include "mozilla/ReentrantMonitor.h"
+# include "mozilla/RefPtr.h"
+# include "nsIThread.h"
+
+namespace mozilla {
+
+class MFTDecoder final {
+ ~MFTDecoder();
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MFTDecoder)
+
+ MFTDecoder();
+
+ // Creates the MFT by COM class ID.
+ //
+ // Params:
+ // - aCLSID The COM class ID of the decoder.
+ HRESULT Create(const GUID& aCLSID);
+
+ // Creates the MFT by querying a category and media subtype.
+ // First thing to do as part of setup.
+ //
+ // Params:
+ // - aCategory the GUID of the MFT category to use.
+ // - aInSubType the GUID of the input MFT media type to use.
+ // GUID_NULL may be used as a wildcard.
+ // - aOutSubType the GUID of the output MFT media type to use.
+ // GUID_NULL may be used as a wildcard.
+ HRESULT Create(const GUID& aCategory, const GUID& aInSubtype,
+ const GUID& aOutSubtype);
+
+ // Sets the input and output media types. Call after Init().
+ //
+ // Params:
+ // - aInputType needs at least major and minor types set.
+ // - aOutputType needs at least major and minor types set.
+ // This is used to select the matching output type out
+ // of all the available output types of the MFT.
+ HRESULT SetMediaTypes(
+ IMFMediaType* aInputType, IMFMediaType* aOutputType,
+ std::function<HRESULT(IMFMediaType*)>&& aCallback =
+ [](IMFMediaType* aOutput) { return S_OK; });
+
+ // Returns the MFT's global IMFAttributes object.
+ already_AddRefed<IMFAttributes> GetAttributes();
+
+ // Returns the MFT's IMFAttributes object for an output stream.
+ already_AddRefed<IMFAttributes> GetOutputStreamAttributes();
+
+ // Retrieves the media type being input.
+ HRESULT GetInputMediaType(RefPtr<IMFMediaType>& aMediaType);
+
+ // Retrieves the media type being output. This may not be valid until
+ // the first sample is decoded.
+ HRESULT GetOutputMediaType(RefPtr<IMFMediaType>& aMediaType);
+ const GUID& GetOutputMediaSubType() const { return mOutputSubType; }
+
+ // Submits data into the MFT for processing.
+ //
+ // Returns:
+ // - MF_E_NOTACCEPTING if the decoder can't accept input. The data
+ // must be resubmitted after Output() stops producing output.
+ HRESULT Input(const uint8_t* aData, uint32_t aDataSize,
+ int64_t aTimestampUsecs, int64_t aDurationUsecs);
+ HRESULT Input(IMFSample* aSample);
+
+ HRESULT CreateInputSample(const uint8_t* aData, uint32_t aDataSize,
+ int64_t aTimestampUsecs, int64_t aDurationUsecs,
+ RefPtr<IMFSample>* aOutSample);
+
+ // Retrieves output from the MFT. Call this once Input() returns
+ // MF_E_NOTACCEPTING. Some MFTs with hardware acceleration (the H.264
+ // decoder MFT in particular) can't handle it if clients hold onto
+ // references to the output IMFSample, so don't do that.
+ //
+ // Returns:
+ // - MF_E_TRANSFORM_STREAM_CHANGE if the underlying stream output
+ // type changed. Retrieve the output media type and reconfig client,
+ // else you may misinterpret the MFT's output.
+ // - MF_E_TRANSFORM_NEED_MORE_INPUT if no output can be produced
+ // due to lack of input.
+ // - S_OK if an output frame is produced.
+ HRESULT Output(RefPtr<IMFSample>* aOutput);
+
+ // Sends a flush message to the MFT. This causes it to discard all
+ // input data. Use before seeking.
+ HRESULT Flush();
+
+ // Sends a message to the MFT.
+ HRESULT SendMFTMessage(MFT_MESSAGE_TYPE aMsg, ULONG_PTR aData);
+
+ HRESULT FindDecoderOutputTypeWithSubtype(const GUID& aSubType);
+ HRESULT FindDecoderOutputType();
+
+ private:
+ // Will search a suitable MediaType using aTypeToUse if set, if not will
+ // use the current mOutputType.
+ HRESULT SetDecoderOutputType(
+ const GUID& aSubType, IMFMediaType* aTypeToUse,
+ std::function<HRESULT(IMFMediaType*)>&& aCallback);
+ HRESULT CreateOutputSample(RefPtr<IMFSample>* aOutSample);
+
+ MFT_INPUT_STREAM_INFO mInputStreamInfo;
+ MFT_OUTPUT_STREAM_INFO mOutputStreamInfo;
+
+ RefPtr<IMFActivate> mActivate;
+ RefPtr<IMFTransform> mDecoder;
+
+ RefPtr<IMFMediaType> mOutputType;
+ GUID mOutputSubType;
+
+ // True if the IMFTransform allocates the samples that it returns.
+ bool mMFTProvidesOutputSamples = false;
+
+ // True if we need to mark the next sample as a discontinuity.
+ bool mDiscontinuity = true;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/platforms/wmf/MFTEncoder.cpp b/dom/media/platforms/wmf/MFTEncoder.cpp
new file mode 100644
index 0000000000..410da2733c
--- /dev/null
+++ b/dom/media/platforms/wmf/MFTEncoder.cpp
@@ -0,0 +1,754 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MFTEncoder.h"
+#include "mozilla/Logging.h"
+#include "mozilla/WindowsProcessMitigations.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/mscom/Utils.h"
+#include "WMFUtils.h"
+
+// Missing from MinGW.
+#ifndef CODECAPI_AVEncAdaptiveMode
+# define STATIC_CODECAPI_AVEncAdaptiveMode \
+ 0x4419b185, 0xda1f, 0x4f53, 0xbc, 0x76, 0x9, 0x7d, 0xc, 0x1e, 0xfb, 0x1e
+DEFINE_CODECAPI_GUID(AVEncAdaptiveMode, "4419b185-da1f-4f53-bc76-097d0c1efb1e",
+ 0x4419b185, 0xda1f, 0x4f53, 0xbc, 0x76, 0x9, 0x7d, 0xc,
+ 0x1e, 0xfb, 0x1e)
+# define CODECAPI_AVEncAdaptiveMode \
+ DEFINE_CODECAPI_GUIDNAMED(AVEncAdaptiveMode)
+#endif
+#ifndef MF_E_NO_EVENTS_AVAILABLE
+# define MF_E_NO_EVENTS_AVAILABLE _HRESULT_TYPEDEF_(0xC00D3E80L)
+#endif
+
+#define MFT_ENC_LOGD(arg, ...) \
+ MOZ_LOG(mozilla::sPEMLog, mozilla::LogLevel::Debug, \
+ ("MFTEncoder(0x%p)::%s: " arg, this, __func__, ##__VA_ARGS__))
+#define MFT_ENC_LOGE(arg, ...) \
+ MOZ_LOG(mozilla::sPEMLog, mozilla::LogLevel::Error, \
+ ("MFTEncoder(0x%p)::%s: " arg, this, __func__, ##__VA_ARGS__))
+#define MFT_ENC_SLOGD(arg, ...) \
+ MOZ_LOG(mozilla::sPEMLog, mozilla::LogLevel::Debug, \
+ ("MFTEncoder::%s: " arg, __func__, ##__VA_ARGS__))
+#define MFT_ENC_SLOGE(arg, ...) \
+ MOZ_LOG(mozilla::sPEMLog, mozilla::LogLevel::Error, \
+ ("MFTEncoder::%s: " arg, __func__, ##__VA_ARGS__))
+
+namespace mozilla {
+extern LazyLogModule sPEMLog;
+
+static const char* ErrorStr(HRESULT hr) {
+ switch (hr) {
+ case S_OK:
+ return "OK";
+ case MF_E_INVALIDMEDIATYPE:
+ return "INVALIDMEDIATYPE";
+ case MF_E_INVALIDSTREAMNUMBER:
+ return "INVALIDSTREAMNUMBER";
+ case MF_E_INVALIDTYPE:
+ return "INVALIDTYPE";
+ case MF_E_TRANSFORM_CANNOT_CHANGE_MEDIATYPE_WHILE_PROCESSING:
+ return "TRANSFORM_PROCESSING";
+ case MF_E_TRANSFORM_TYPE_NOT_SET:
+ return "TRANSFORM_TYPE_NO_SET";
+ case MF_E_UNSUPPORTED_D3D_TYPE:
+ return "UNSUPPORTED_D3D_TYPE";
+ case E_INVALIDARG:
+ return "INVALIDARG";
+ case MF_E_NO_SAMPLE_DURATION:
+ return "NO_SAMPLE_DURATION";
+ case MF_E_NO_SAMPLE_TIMESTAMP:
+ return "NO_SAMPLE_TIMESTAMP";
+ case MF_E_NOTACCEPTING:
+ return "NOTACCEPTING";
+ case MF_E_ATTRIBUTENOTFOUND:
+ return "NOTFOUND";
+ case MF_E_BUFFERTOOSMALL:
+ return "BUFFERTOOSMALL";
+ case E_NOTIMPL:
+ return "NOTIMPL";
+ default:
+ return "OTHER";
+ }
+}
+
+static const char* CodecStr(const GUID& aGUID) {
+ if (IsEqualGUID(aGUID, MFVideoFormat_H264)) {
+ return "H.264";
+ } else if (IsEqualGUID(aGUID, MFVideoFormat_VP80)) {
+ return "VP8";
+ } else if (IsEqualGUID(aGUID, MFVideoFormat_VP90)) {
+ return "VP9";
+ } else {
+ return "Unsupported codec";
+ }
+}
+
+static UINT32 EnumEncoders(const GUID& aSubtype, IMFActivate**& aActivates,
+ const bool aUseHW = true) {
+ UINT32 num = 0;
+ MFT_REGISTER_TYPE_INFO inType = {.guidMajorType = MFMediaType_Video,
+ .guidSubtype = MFVideoFormat_NV12};
+ MFT_REGISTER_TYPE_INFO outType = {.guidMajorType = MFMediaType_Video,
+ .guidSubtype = aSubtype};
+ HRESULT hr = S_OK;
+ if (aUseHW) {
+ if (IsWin32kLockedDown()) {
+ // Some HW encoders use DXGI API and crash when locked down.
+ // TODO: move HW encoding out of content process (bug 1754531).
+ MFT_ENC_SLOGD("Don't use HW encoder when win32k locked down.");
+ return 0;
+ }
+
+ hr = wmf::MFTEnumEx(MFT_CATEGORY_VIDEO_ENCODER,
+ MFT_ENUM_FLAG_HARDWARE | MFT_ENUM_FLAG_SORTANDFILTER,
+ &inType, &outType, &aActivates, &num);
+ if (FAILED(hr)) {
+ MFT_ENC_SLOGE("enumerate HW encoder for %s: error=%s", CodecStr(aSubtype),
+ ErrorStr(hr));
+ return 0;
+ }
+ if (num > 0) {
+ return num;
+ }
+ }
+
+ // Try software MFTs.
+ hr = wmf::MFTEnumEx(MFT_CATEGORY_VIDEO_ENCODER,
+ MFT_ENUM_FLAG_SYNCMFT | MFT_ENUM_FLAG_ASYNCMFT |
+ MFT_ENUM_FLAG_SORTANDFILTER,
+ &inType, &outType, &aActivates, &num);
+ if (FAILED(hr)) {
+ MFT_ENC_SLOGE("enumerate SW encoder for %s: error=%s", CodecStr(aSubtype),
+ ErrorStr(hr));
+ return 0;
+ }
+ if (num == 0) {
+ MFT_ENC_SLOGD("cannot find encoder for %s", CodecStr(aSubtype));
+ }
+ return num;
+}
+
+static HRESULT GetFriendlyName(IMFActivate* aAttributes, nsCString& aName) {
+ UINT32 len = 0;
+ HRESULT hr = aAttributes->GetStringLength(MFT_FRIENDLY_NAME_Attribute, &len);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ if (len > 0) {
+ ++len; // '\0'.
+ WCHAR name[len];
+ if (SUCCEEDED(aAttributes->GetString(MFT_FRIENDLY_NAME_Attribute, name, len,
+ nullptr))) {
+ aName.Append(NS_ConvertUTF16toUTF8(name));
+ }
+ }
+
+ if (aName.Length() == 0) {
+ aName.Append("Unknown MFT");
+ }
+
+ return S_OK;
+}
+
+static void PopulateEncoderInfo(const GUID& aSubtype,
+ nsTArray<MFTEncoder::Info>& aInfos) {
+ IMFActivate** activates = nullptr;
+ UINT32 num = EnumEncoders(aSubtype, activates);
+ for (UINT32 i = 0; i < num; ++i) {
+ MFTEncoder::Info info = {.mSubtype = aSubtype};
+ GetFriendlyName(activates[i], info.mName);
+ aInfos.AppendElement(info);
+ MFT_ENC_SLOGD("<ENC> [%s] %s\n", CodecStr(aSubtype), info.mName.Data());
+ activates[i]->Release();
+ activates[i] = nullptr;
+ }
+ CoTaskMemFree(activates);
+}
+
+Maybe<MFTEncoder::Info> MFTEncoder::GetInfo(const GUID& aSubtype) {
+ nsTArray<Info>& infos = Infos();
+
+ for (auto i : infos) {
+ if (IsEqualGUID(aSubtype, i.mSubtype)) {
+ return Some(i);
+ }
+ }
+ return Nothing();
+}
+
+nsCString MFTEncoder::GetFriendlyName(const GUID& aSubtype) {
+ Maybe<Info> info = GetInfo(aSubtype);
+
+ return info ? info.ref().mName : "???"_ns;
+}
+
+// Called only once by Infos().
+nsTArray<MFTEncoder::Info> MFTEncoder::Enumerate() {
+ nsTArray<Info> infos;
+
+ if (!wmf::MediaFoundationInitializer::HasInitialized()) {
+ MFT_ENC_SLOGE("cannot init Media Foundation");
+ return infos;
+ }
+
+ PopulateEncoderInfo(MFVideoFormat_H264, infos);
+ PopulateEncoderInfo(MFVideoFormat_VP90, infos);
+ PopulateEncoderInfo(MFVideoFormat_VP80, infos);
+
+ return infos;
+}
+
+nsTArray<MFTEncoder::Info>& MFTEncoder::Infos() {
+ static nsTArray<Info> infos = Enumerate();
+ return infos;
+}
+
+already_AddRefed<IMFActivate> MFTEncoder::CreateFactory(const GUID& aSubtype) {
+ IMFActivate** activates = nullptr;
+ UINT32 num = EnumEncoders(aSubtype, activates, !mHardwareNotAllowed);
+ if (num == 0) {
+ return nullptr;
+ }
+
+ // Keep the first and throw out others, if there is any.
+ RefPtr<IMFActivate> factory = activates[0];
+ activates[0] = nullptr;
+ for (UINT32 i = 1; i < num; ++i) {
+ activates[i]->Release();
+ activates[i] = nullptr;
+ }
+ CoTaskMemFree(activates);
+
+ return factory.forget();
+}
+
+HRESULT MFTEncoder::Create(const GUID& aSubtype) {
+ MOZ_ASSERT(mscom::IsCurrentThreadMTA());
+ MOZ_ASSERT(!mEncoder);
+
+ RefPtr<IMFActivate> factory = CreateFactory(aSubtype);
+ if (!factory) {
+ return E_FAIL;
+ }
+
+ // Create MFT via the activation object.
+ RefPtr<IMFTransform> encoder;
+ HRESULT hr = factory->ActivateObject(
+ IID_PPV_ARGS(static_cast<IMFTransform**>(getter_AddRefs(encoder))));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ RefPtr<ICodecAPI> config;
+ // Avoid IID_PPV_ARGS() here for MingGW fails to declare UUID for ICodecAPI.
+ hr = encoder->QueryInterface(IID_ICodecAPI, getter_AddRefs(config));
+ if (FAILED(hr)) {
+ encoder = nullptr;
+ factory->ShutdownObject();
+ return hr;
+ }
+
+ mFactory = std::move(factory);
+ mEncoder = std::move(encoder);
+ mConfig = std::move(config);
+ return S_OK;
+}
+
+HRESULT
+MFTEncoder::Destroy() {
+ if (!mEncoder) {
+ return S_OK;
+ }
+
+ mEncoder = nullptr;
+ mConfig = nullptr;
+ // Release MFT resources via activation object.
+ HRESULT hr = mFactory->ShutdownObject();
+ mFactory = nullptr;
+
+ return hr;
+}
+
+HRESULT
+MFTEncoder::SetMediaTypes(IMFMediaType* aInputType, IMFMediaType* aOutputType) {
+ MOZ_ASSERT(mscom::IsCurrentThreadMTA());
+ MOZ_ASSERT(aInputType && aOutputType);
+
+ AsyncMFTResult asyncMFT = AttemptEnableAsync();
+ NS_ENSURE_TRUE(asyncMFT.isOk(), asyncMFT.unwrapErr());
+
+ HRESULT hr = GetStreamIDs();
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ // Always set encoder output type before input.
+ hr = mEncoder->SetOutputType(mOutputStreamID, aOutputType, 0);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ NS_ENSURE_TRUE(MatchInputSubtype(aInputType) != GUID_NULL,
+ MF_E_INVALIDMEDIATYPE);
+
+ hr = mEncoder->SetInputType(mInputStreamID, aInputType, 0);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = mEncoder->GetInputStreamInfo(mInputStreamID, &mInputStreamInfo);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = mEncoder->GetOutputStreamInfo(mInputStreamID, &mOutputStreamInfo);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ mOutputStreamProvidesSample =
+ IsFlagSet(mOutputStreamInfo.dwFlags, MFT_OUTPUT_STREAM_PROVIDES_SAMPLES);
+
+ hr = SendMFTMessage(MFT_MESSAGE_NOTIFY_BEGIN_STREAMING, 0);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = SendMFTMessage(MFT_MESSAGE_NOTIFY_START_OF_STREAM, 0);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ if (asyncMFT.unwrap()) {
+ RefPtr<IMFMediaEventGenerator> source;
+ hr = mEncoder->QueryInterface(IID_PPV_ARGS(
+ static_cast<IMFMediaEventGenerator**>(getter_AddRefs(source))));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ mEventSource.SetAsyncEventGenerator(source.forget());
+ } else {
+ mEventSource.InitSyncMFTEventQueue();
+ }
+
+ mNumNeedInput = 0;
+ return S_OK;
+}
+
+// Async MFT won't work without unlocking. See
+// https://docs.microsoft.com/en-us/windows/win32/medfound/asynchronous-mfts#unlocking-asynchronous-mfts
+MFTEncoder::AsyncMFTResult MFTEncoder::AttemptEnableAsync() {
+ IMFAttributes* pAttributes = nullptr;
+ HRESULT hr = mEncoder->GetAttributes(&pAttributes);
+ if (FAILED(hr)) {
+ return AsyncMFTResult(hr);
+ }
+
+ bool async =
+ MFGetAttributeUINT32(pAttributes, MF_TRANSFORM_ASYNC, FALSE) == TRUE;
+ if (async) {
+ hr = pAttributes->SetUINT32(MF_TRANSFORM_ASYNC_UNLOCK, TRUE);
+ } else {
+ hr = S_OK;
+ }
+ pAttributes->Release();
+
+ return SUCCEEDED(hr) ? AsyncMFTResult(async) : AsyncMFTResult(hr);
+}
+
+HRESULT MFTEncoder::GetStreamIDs() {
+ DWORD numIns;
+ DWORD numOuts;
+ HRESULT hr = mEncoder->GetStreamCount(&numIns, &numOuts);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ if (numIns < 1 || numOuts < 1) {
+ MFT_ENC_LOGE("stream count error");
+ return MF_E_INVALIDSTREAMNUMBER;
+ }
+
+ DWORD inIDs[numIns];
+ DWORD outIDs[numOuts];
+ hr = mEncoder->GetStreamIDs(numIns, inIDs, numOuts, outIDs);
+ if (SUCCEEDED(hr)) {
+ mInputStreamID = inIDs[0];
+ mOutputStreamID = outIDs[0];
+ } else if (hr == E_NOTIMPL) {
+ mInputStreamID = 0;
+ mOutputStreamID = 0;
+ } else {
+ MFT_ENC_LOGE("failed to get stream IDs");
+ return hr;
+ }
+ return S_OK;
+}
+
+GUID MFTEncoder::MatchInputSubtype(IMFMediaType* aInputType) {
+ MOZ_ASSERT(mEncoder);
+ MOZ_ASSERT(aInputType);
+
+ GUID desired = GUID_NULL;
+ HRESULT hr = aInputType->GetGUID(MF_MT_SUBTYPE, &desired);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), GUID_NULL);
+ MOZ_ASSERT(desired != GUID_NULL);
+
+ DWORD i = 0;
+ IMFMediaType* inputType = nullptr;
+ GUID preferred = GUID_NULL;
+ while (true) {
+ hr = mEncoder->GetInputAvailableType(mInputStreamID, i, &inputType);
+ if (hr == MF_E_NO_MORE_TYPES) {
+ break;
+ }
+ NS_ENSURE_TRUE(SUCCEEDED(hr), GUID_NULL);
+
+ GUID sub = GUID_NULL;
+ hr = inputType->GetGUID(MF_MT_SUBTYPE, &sub);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), GUID_NULL);
+
+ if (IsEqualGUID(desired, sub)) {
+ preferred = desired;
+ break;
+ }
+ ++i;
+ }
+
+ return IsEqualGUID(preferred, desired) ? preferred : GUID_NULL;
+}
+
+HRESULT
+MFTEncoder::SendMFTMessage(MFT_MESSAGE_TYPE aMsg, ULONG_PTR aData) {
+ MOZ_ASSERT(mscom::IsCurrentThreadMTA());
+ MOZ_ASSERT(mEncoder);
+
+ return mEncoder->ProcessMessage(aMsg, aData);
+}
+
+HRESULT MFTEncoder::SetModes(UINT32 aBitsPerSec) {
+ MOZ_ASSERT(mscom::IsCurrentThreadMTA());
+ MOZ_ASSERT(mConfig);
+
+ VARIANT var;
+ var.vt = VT_UI4;
+ var.ulVal = eAVEncCommonRateControlMode_CBR;
+ HRESULT hr = mConfig->SetValue(&CODECAPI_AVEncCommonRateControlMode, &var);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ var.ulVal = aBitsPerSec;
+ hr = mConfig->SetValue(&CODECAPI_AVEncCommonMeanBitRate, &var);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ if (SUCCEEDED(mConfig->IsModifiable(&CODECAPI_AVEncAdaptiveMode))) {
+ var.ulVal = eAVEncAdaptiveMode_Resolution;
+ hr = mConfig->SetValue(&CODECAPI_AVEncAdaptiveMode, &var);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ }
+
+ if (SUCCEEDED(mConfig->IsModifiable(&CODECAPI_AVLowLatencyMode))) {
+ var.vt = VT_BOOL;
+ var.boolVal = VARIANT_TRUE;
+ hr = mConfig->SetValue(&CODECAPI_AVLowLatencyMode, &var);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ }
+
+ return S_OK;
+}
+
+HRESULT
+MFTEncoder::SetBitrate(UINT32 aBitsPerSec) {
+ MOZ_ASSERT(mscom::IsCurrentThreadMTA());
+ MOZ_ASSERT(mConfig);
+
+ VARIANT var = {.vt = VT_UI4, .ulVal = aBitsPerSec};
+ return mConfig->SetValue(&CODECAPI_AVEncCommonMeanBitRate, &var);
+}
+
+static HRESULT CreateSample(RefPtr<IMFSample>* aOutSample, DWORD aSize,
+ DWORD aAlignment) {
+ MOZ_ASSERT(mscom::IsCurrentThreadMTA());
+
+ HRESULT hr;
+ RefPtr<IMFSample> sample;
+ hr = wmf::MFCreateSample(getter_AddRefs(sample));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ RefPtr<IMFMediaBuffer> buffer;
+ hr = wmf::MFCreateAlignedMemoryBuffer(aSize, aAlignment,
+ getter_AddRefs(buffer));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = sample->AddBuffer(buffer);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ *aOutSample = sample.forget();
+
+ return S_OK;
+}
+
+HRESULT
+MFTEncoder::CreateInputSample(RefPtr<IMFSample>* aSample, size_t aSize) {
+ MOZ_ASSERT(mscom::IsCurrentThreadMTA());
+
+ return CreateSample(
+ aSample, aSize,
+ mInputStreamInfo.cbAlignment > 0 ? mInputStreamInfo.cbAlignment - 1 : 0);
+}
+
+HRESULT
+MFTEncoder::PushInput(RefPtr<IMFSample>&& aInput) {
+ MOZ_ASSERT(mscom::IsCurrentThreadMTA());
+ MOZ_ASSERT(mEncoder);
+ MOZ_ASSERT(aInput);
+
+ mPendingInputs.Push(aInput.forget());
+ if (mEventSource.IsSync() && mNumNeedInput == 0) {
+ // To step 2 in
+ // https://docs.microsoft.com/en-us/windows/win32/medfound/basic-mft-processing-model#process-data
+ mNumNeedInput++;
+ }
+
+ HRESULT hr = ProcessInput();
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ return ProcessEvents();
+}
+
+HRESULT MFTEncoder::ProcessInput() {
+ MOZ_ASSERT(mscom::IsCurrentThreadMTA());
+ MOZ_ASSERT(mEncoder);
+
+ if (mNumNeedInput == 0 || mPendingInputs.GetSize() == 0) {
+ return S_OK;
+ }
+
+ RefPtr<IMFSample> input = mPendingInputs.PopFront();
+ HRESULT hr = mEncoder->ProcessInput(mInputStreamID, input, 0);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ --mNumNeedInput;
+
+ if (!mEventSource.IsSync()) {
+ return S_OK;
+ }
+ // For sync MFT: Step 3 in
+ // https://docs.microsoft.com/en-us/windows/win32/medfound/basic-mft-processing-model#process-data
+ DWORD flags = 0;
+ hr = mEncoder->GetOutputStatus(&flags);
+ MediaEventType evType = MEUnknown;
+ switch (hr) {
+ case S_OK:
+ evType = flags == MFT_OUTPUT_STATUS_SAMPLE_READY
+ ? METransformHaveOutput // To step 4: ProcessOutput().
+ : METransformNeedInput; // To step 2: ProcessInput().
+ break;
+ case E_NOTIMPL:
+ evType = METransformHaveOutput; // To step 4: ProcessOutput().
+ break;
+ default:
+ MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE("undefined output status");
+ return hr;
+ }
+ return mEventSource.QueueSyncMFTEvent(evType);
+}
+
+HRESULT MFTEncoder::ProcessEvents() {
+ MOZ_ASSERT(mscom::IsCurrentThreadMTA());
+ MOZ_ASSERT(mEncoder);
+
+ HRESULT hr = E_FAIL;
+ while (true) {
+ Event event = mEventSource.GetEvent();
+ if (event.isErr()) {
+ hr = event.unwrapErr();
+ break;
+ }
+
+ MediaEventType evType = event.unwrap();
+ switch (evType) {
+ case METransformNeedInput:
+ ++mNumNeedInput;
+ hr = ProcessInput();
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ break;
+ case METransformHaveOutput:
+ hr = ProcessOutput();
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ break;
+ case METransformDrainComplete:
+ mDrainState = DrainState::DRAINED;
+ break;
+ default:
+ MFT_ENC_LOGE("unsupported event: %lx", evType);
+ }
+ }
+
+ switch (hr) {
+ case MF_E_NO_EVENTS_AVAILABLE:
+ return S_OK;
+ case MF_E_MULTIPLE_SUBSCRIBERS:
+ default:
+ MFT_ENC_LOGE("failed to get event: %s", ErrorStr(hr));
+ return hr;
+ }
+}
+
+HRESULT MFTEncoder::ProcessOutput() {
+ MOZ_ASSERT(mscom::IsCurrentThreadMTA());
+ MOZ_ASSERT(mEncoder);
+
+ MFT_OUTPUT_DATA_BUFFER output = {.dwStreamID = mOutputStreamID,
+ .pSample = nullptr,
+ .dwStatus = 0,
+ .pEvents = nullptr};
+ RefPtr<IMFSample> sample;
+ HRESULT hr = E_FAIL;
+ if (!mOutputStreamProvidesSample) {
+ hr = CreateSample(&sample, mOutputStreamInfo.cbSize,
+ mOutputStreamInfo.cbAlignment > 1
+ ? mOutputStreamInfo.cbAlignment - 1
+ : 0);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ output.pSample = sample;
+ }
+
+ DWORD status = 0;
+ hr = mEncoder->ProcessOutput(0, 1, &output, &status);
+ if (hr == MF_E_TRANSFORM_STREAM_CHANGE) {
+ MFT_ENC_LOGD("output stream change");
+ if (output.dwStatus & MFT_OUTPUT_DATA_BUFFER_FORMAT_CHANGE) {
+ // Follow the instructions in Microsoft doc:
+ // https://docs.microsoft.com/en-us/windows/win32/medfound/handling-stream-changes#output-type
+ IMFMediaType* outputType = nullptr;
+ hr = mEncoder->GetOutputAvailableType(mOutputStreamID, 0, &outputType);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ hr = mEncoder->SetOutputType(mOutputStreamID, outputType, 0);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ }
+ return MF_E_TRANSFORM_STREAM_CHANGE;
+ }
+
+ // Step 8 in
+ // https://docs.microsoft.com/en-us/windows/win32/medfound/basic-mft-processing-model#process-data
+ if (hr == MF_E_TRANSFORM_NEED_MORE_INPUT) {
+ MOZ_ASSERT(mEventSource.IsSync());
+ MOZ_ASSERT(mDrainState == DrainState::DRAINING);
+
+ mEventSource.QueueSyncMFTEvent(METransformDrainComplete);
+ return S_OK;
+ }
+
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ mOutputs.AppendElement(output.pSample);
+ if (mOutputStreamProvidesSample) {
+ // Release MFT provided sample.
+ output.pSample->Release();
+ output.pSample = nullptr;
+ }
+
+ return S_OK;
+}
+
+HRESULT MFTEncoder::TakeOutput(nsTArray<RefPtr<IMFSample>>& aOutput) {
+ MOZ_ASSERT(aOutput.Length() == 0);
+ aOutput.SwapElements(mOutputs);
+ return S_OK;
+}
+
+HRESULT MFTEncoder::Drain(nsTArray<RefPtr<IMFSample>>& aOutput) {
+ MOZ_ASSERT(mscom::IsCurrentThreadMTA());
+ MOZ_ASSERT(mEncoder);
+ MOZ_ASSERT(aOutput.Length() == 0);
+
+ switch (mDrainState) {
+ case DrainState::DRAINABLE:
+ // Exhaust pending inputs.
+ while (mPendingInputs.GetSize() > 0) {
+ if (mEventSource.IsSync()) {
+ // Step 5 in
+ // https://docs.microsoft.com/en-us/windows/win32/medfound/basic-mft-processing-model#process-data
+ mEventSource.QueueSyncMFTEvent(METransformNeedInput);
+ }
+ HRESULT hr = ProcessEvents();
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ }
+ SendMFTMessage(MFT_MESSAGE_COMMAND_DRAIN, 0);
+ mDrainState = DrainState::DRAINING;
+ [[fallthrough]]; // To collect and return outputs.
+ case DrainState::DRAINING:
+ // Collect remaining outputs.
+ while (mOutputs.Length() == 0 && mDrainState != DrainState::DRAINED) {
+ if (mEventSource.IsSync()) {
+ // Step 8 in
+ // https://docs.microsoft.com/en-us/windows/win32/medfound/basic-mft-processing-model#process-data
+ mEventSource.QueueSyncMFTEvent(METransformHaveOutput);
+ }
+ HRESULT hr = ProcessEvents();
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ }
+ [[fallthrough]]; // To return outputs.
+ case DrainState::DRAINED:
+ aOutput.SwapElements(mOutputs);
+ return S_OK;
+ }
+}
+
+HRESULT MFTEncoder::GetMPEGSequenceHeader(nsTArray<UINT8>& aHeader) {
+ MOZ_ASSERT(mscom::IsCurrentThreadMTA());
+ MOZ_ASSERT(mEncoder);
+ MOZ_ASSERT(aHeader.Length() == 0);
+
+ RefPtr<IMFMediaType> outputType;
+ HRESULT hr = mEncoder->GetOutputCurrentType(mOutputStreamID,
+ getter_AddRefs(outputType));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ UINT32 length = 0;
+ hr = outputType->GetBlobSize(MF_MT_MPEG_SEQUENCE_HEADER, &length);
+ if (hr == MF_E_ATTRIBUTENOTFOUND || length == 0) {
+ return S_OK;
+ }
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ aHeader.SetCapacity(length);
+ hr = outputType->GetBlob(MF_MT_MPEG_SEQUENCE_HEADER, aHeader.Elements(),
+ length, nullptr);
+ aHeader.SetLength(SUCCEEDED(hr) ? length : 0);
+
+ return hr;
+}
+
+MFTEncoder::Event MFTEncoder::EventSource::GetEvent() {
+ if (IsSync()) {
+ return GetSyncMFTEvent();
+ }
+
+ RefPtr<IMFMediaEvent> event;
+ HRESULT hr = mImpl.as<RefPtr<IMFMediaEventGenerator>>()->GetEvent(
+ MF_EVENT_FLAG_NO_WAIT, getter_AddRefs(event));
+ MediaEventType type = MEUnknown;
+ if (SUCCEEDED(hr)) {
+ hr = event->GetType(&type);
+ }
+ return SUCCEEDED(hr) ? Event{type} : Event{hr};
+}
+
+HRESULT MFTEncoder::EventSource::QueueSyncMFTEvent(MediaEventType aEventType) {
+ MOZ_ASSERT(IsSync());
+ MOZ_ASSERT(IsOnCurrentThread());
+
+ auto q = mImpl.as<UniquePtr<EventQueue>>().get();
+ q->push(aEventType);
+ return S_OK;
+}
+
+MFTEncoder::Event MFTEncoder::EventSource::GetSyncMFTEvent() {
+ MOZ_ASSERT(IsOnCurrentThread());
+
+ auto q = mImpl.as<UniquePtr<EventQueue>>().get();
+ if (q->empty()) {
+ return Event{MF_E_NO_EVENTS_AVAILABLE};
+ }
+
+ MediaEventType type = q->front();
+ q->pop();
+ return Event{type};
+}
+
+#ifdef DEBUG
+bool MFTEncoder::EventSource::IsOnCurrentThread() {
+ if (!mThread) {
+ mThread = GetCurrentSerialEventTarget();
+ }
+ return mThread->IsOnCurrentThread();
+}
+#endif
+
+} // namespace mozilla
+
+#undef MFT_ENC_SLOGE
+#undef MFT_ENC_SLOGD
+#undef MFT_ENC_LOGE
+#undef MFT_ENC_LOGD
diff --git a/dom/media/platforms/wmf/MFTEncoder.h b/dom/media/platforms/wmf/MFTEncoder.h
new file mode 100644
index 0000000000..e2eaec3476
--- /dev/null
+++ b/dom/media/platforms/wmf/MFTEncoder.h
@@ -0,0 +1,144 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(MFTEncoder_h_)
+# define MFTEncoder_h_
+
+# include <functional>
+# include <queue>
+# include "mozilla/RefPtr.h"
+# include "mozilla/ResultVariant.h"
+# include "nsISupportsImpl.h"
+# include "nsDeque.h"
+# include "nsTArray.h"
+# include "WMF.h"
+
+namespace mozilla {
+
+class MFTEncoder final {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MFTEncoder)
+
+ explicit MFTEncoder(const bool aHardwareNotAllowed)
+ : mHardwareNotAllowed(aHardwareNotAllowed) {}
+
+ HRESULT Create(const GUID& aSubtype);
+ HRESULT Destroy();
+ HRESULT SetMediaTypes(IMFMediaType* aInputType, IMFMediaType* aOutputType);
+ HRESULT SetModes(UINT32 aBitsPerSec);
+ HRESULT SetBitrate(UINT32 aBitsPerSec);
+
+ HRESULT CreateInputSample(RefPtr<IMFSample>* aSample, size_t aSize);
+ HRESULT PushInput(RefPtr<IMFSample>&& aInput);
+ HRESULT TakeOutput(nsTArray<RefPtr<IMFSample>>& aOutput);
+ HRESULT Drain(nsTArray<RefPtr<IMFSample>>& aOutput);
+
+ HRESULT GetMPEGSequenceHeader(nsTArray<UINT8>& aHeader);
+
+ static nsCString GetFriendlyName(const GUID& aSubtype);
+
+ struct Info final {
+ GUID mSubtype;
+ nsCString mName;
+ };
+
+ private:
+ // Abstractions to support sync MFTs using the same logic for async MFTs.
+ // When the MFT is async and a real event generator is available, simply
+ // forward the calls. For sync MFTs, use the synchronous processing model
+ // described in
+ // https://docs.microsoft.com/en-us/windows/win32/medfound/basic-mft-processing-model#process-data
+ // to generate events of the asynchronous processing model.
+ using Event = Result<MediaEventType, HRESULT>;
+ using EventQueue = std::queue<MediaEventType>;
+ class EventSource final {
+ public:
+ EventSource() : mImpl(Nothing{}) {}
+
+ void SetAsyncEventGenerator(
+ already_AddRefed<IMFMediaEventGenerator>&& aAsyncEventGenerator) {
+ MOZ_ASSERT(mImpl.is<Nothing>());
+ mImpl.emplace<RefPtr<IMFMediaEventGenerator>>(aAsyncEventGenerator);
+ }
+
+ void InitSyncMFTEventQueue() {
+ MOZ_ASSERT(mImpl.is<Nothing>());
+ mImpl.emplace<UniquePtr<EventQueue>>(MakeUnique<EventQueue>());
+ }
+
+ bool IsSync() const { return mImpl.is<UniquePtr<EventQueue>>(); }
+
+ Event GetEvent();
+ // Push an event when sync MFT is used.
+ HRESULT QueueSyncMFTEvent(MediaEventType aEventType);
+
+ private:
+ // Pop an event from the queue when sync MFT is used.
+ Event GetSyncMFTEvent();
+
+ Variant<
+ // Uninitialized.
+ Nothing,
+ // For async MFT events. See
+ // https://docs.microsoft.com/en-us/windows/win32/medfound/asynchronous-mfts#events
+ RefPtr<IMFMediaEventGenerator>,
+ // Event queue for a sync MFT. Storing EventQueue directly breaks the
+ // code so a pointer is introduced.
+ UniquePtr<EventQueue>>
+ mImpl;
+# ifdef DEBUG
+ bool IsOnCurrentThread();
+ nsCOMPtr<nsISerialEventTarget> mThread;
+# endif
+ };
+
+ ~MFTEncoder() { Destroy(); };
+
+ static nsTArray<Info>& Infos();
+ static nsTArray<Info> Enumerate();
+ static Maybe<Info> GetInfo(const GUID& aSubtype);
+
+ already_AddRefed<IMFActivate> CreateFactory(const GUID& aSubtype);
+ // Return true when successfully enabled, false for MFT that doesn't support
+ // async processing model, and error otherwise.
+ using AsyncMFTResult = Result<bool, HRESULT>;
+ AsyncMFTResult AttemptEnableAsync();
+ HRESULT GetStreamIDs();
+ GUID MatchInputSubtype(IMFMediaType* aInputType);
+ HRESULT SendMFTMessage(MFT_MESSAGE_TYPE aMsg, ULONG_PTR aData);
+
+ HRESULT ProcessEvents();
+ HRESULT ProcessInput();
+ HRESULT ProcessOutput();
+
+ const bool mHardwareNotAllowed;
+ RefPtr<IMFTransform> mEncoder;
+ // For MFT object creation. See
+ // https://docs.microsoft.com/en-us/windows/win32/medfound/activation-objects
+ RefPtr<IMFActivate> mFactory;
+ // For encoder configuration. See
+ // https://docs.microsoft.com/en-us/windows/win32/directshow/encoder-api
+ RefPtr<ICodecAPI> mConfig;
+
+ DWORD mInputStreamID;
+ DWORD mOutputStreamID;
+ MFT_INPUT_STREAM_INFO mInputStreamInfo;
+ MFT_OUTPUT_STREAM_INFO mOutputStreamInfo;
+ bool mOutputStreamProvidesSample;
+
+ size_t mNumNeedInput;
+ enum class DrainState { DRAINED, DRAINABLE, DRAINING };
+ DrainState mDrainState = DrainState::DRAINABLE;
+
+ nsRefPtrDeque<IMFSample> mPendingInputs;
+ nsTArray<RefPtr<IMFSample>> mOutputs;
+
+ EventSource mEventSource;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/platforms/wmf/WMF.h b/dom/media/platforms/wmf/WMF.h
new file mode 100644
index 0000000000..740442ceda
--- /dev/null
+++ b/dom/media/platforms/wmf/WMF.h
@@ -0,0 +1,198 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef WMF_H_
+#define WMF_H_
+
+#include <windows.h>
+#include <mfapi.h>
+#include <mfidl.h>
+#include <mfreadwrite.h>
+#include <mfobjects.h>
+#include <ks.h>
+#include <stdio.h>
+#include <mferror.h>
+#include <propvarutil.h>
+#include <wmcodecdsp.h>
+#include <d3d9.h>
+#include <dxva2api.h>
+#include <wmcodecdsp.h>
+#include <codecapi.h>
+
+#include "mozilla/Atomics.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/StaticMutex.h"
+#include "nsThreadUtils.h"
+
+// The Windows headers helpfully declare min and max macros, which don't
+// compile in the presence of std::min and std::max and unified builds.
+// So undef them here.
+#ifdef min
+# undef min
+#endif
+#ifdef max
+# undef max
+#endif
+
+// https://stackoverflow.com/questions/25759700/ms-format-tag-for-opus-codec
+#ifndef MFAudioFormat_Opus
+DEFINE_GUID(MFAudioFormat_Opus, WAVE_FORMAT_OPUS, 0x000, 0x0010, 0x80, 0x00,
+ 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71);
+#endif
+
+const inline GUID CLSID_CMSVPXDecMFT = {
+ 0xe3aaf548,
+ 0xc9a4,
+ 0x4c6e,
+ {0x23, 0x4d, 0x5a, 0xda, 0x37, 0x4b, 0x00, 0x00}};
+
+namespace mozilla::wmf {
+
+// A helper class for automatically starting and shuting down the Media
+// Foundation. Prior to using Media Foundation in a process, users should call
+// MediaFoundationInitializer::HasInitialized() to ensure Media Foundation is
+// initialized. Users should also check the result of this call, in case the
+// internal call to MFStartup fails. The first check to HasInitialized will
+// cause the helper to start up Media Foundation and set up a runnable to handle
+// Media Foundation shutdown at XPCOM shutdown. Calls after the first will not
+// cause any extra startups or shutdowns, so it's safe to check multiple times
+// in the same process. Users do not need to do any manual shutdown, the helper
+// will handle this internally.
+class MediaFoundationInitializer final {
+ public:
+ ~MediaFoundationInitializer() {
+ if (mHasInitialized) {
+ if (FAILED(MFShutdown())) {
+ NS_WARNING("MFShutdown failed");
+ }
+ }
+ }
+ static bool HasInitialized() {
+ if (sIsShutdown) {
+ return false;
+ }
+ return Get()->mHasInitialized;
+ }
+
+ private:
+ static MediaFoundationInitializer* Get() {
+ {
+ StaticMutexAutoLock lock(sCreateMutex);
+ if (!sInitializer) {
+ sInitializer.reset(new MediaFoundationInitializer());
+ GetMainThreadSerialEventTarget()->Dispatch(
+ NS_NewRunnableFunction("MediaFoundationInitializer::Get", [&] {
+ // Need to run this before MTA thread gets destroyed.
+ RunOnShutdown(
+ [&] {
+ sInitializer.reset();
+ sIsShutdown = true;
+ },
+ ShutdownPhase::XPCOMShutdown);
+ }));
+ }
+ }
+ return sInitializer.get();
+ }
+
+ MediaFoundationInitializer() : mHasInitialized(SUCCEEDED(MFStartup())) {
+ if (!mHasInitialized) {
+ NS_WARNING("MFStartup failed");
+ }
+ }
+
+ // If successful, loads all required WMF DLLs and calls the WMF MFStartup()
+ // function. This delegates the WMF MFStartup() call to the MTA thread if
+ // the current thread is not MTA. This is to ensure we always interact with
+ // WMF from threads with the same COM compartment model.
+ HRESULT MFStartup();
+
+ // Calls the WMF MFShutdown() function. Call this once for every time
+ // wmf::MFStartup() succeeds. Note: does not unload the WMF DLLs loaded by
+ // MFStartup(); leaves them in memory to save I/O at next MFStartup() call.
+ // This delegates the WMF MFShutdown() call to the MTA thread if the current
+ // thread is not MTA. This is to ensure we always interact with
+ // WMF from threads with the same COM compartment model.
+ HRESULT MFShutdown();
+
+ static inline UniquePtr<MediaFoundationInitializer> sInitializer;
+ static inline StaticMutex sCreateMutex;
+ static inline Atomic<bool> sIsShutdown{false};
+ const bool mHasInitialized;
+};
+
+// All functions below are wrappers around the corresponding WMF function,
+// and automatically locate and call the corresponding function in the WMF DLLs.
+
+HRESULT MFCreateMediaType(IMFMediaType** aOutMFType);
+
+HRESULT MFGetStrideForBitmapInfoHeader(DWORD aFormat, DWORD aWidth,
+ LONG* aOutStride);
+
+HRESULT MFGetService(IUnknown* punkObject, REFGUID guidService, REFIID riid,
+ LPVOID* ppvObject);
+
+HRESULT DXVA2CreateDirect3DDeviceManager9(
+ UINT* pResetToken, IDirect3DDeviceManager9** ppDXVAManager);
+
+HRESULT MFCreateDXGIDeviceManager(UINT* pResetToken,
+ IMFDXGIDeviceManager** ppDXVAManager);
+
+HRESULT MFCreateSample(IMFSample** ppIMFSample);
+
+HRESULT MFCreateAlignedMemoryBuffer(DWORD cbMaxLength, DWORD fAlignmentFlags,
+ IMFMediaBuffer** ppBuffer);
+
+HRESULT MFCreateDXGISurfaceBuffer(REFIID riid, IUnknown* punkSurface,
+ UINT uSubresourceIndex,
+ BOOL fButtomUpWhenLinear,
+ IMFMediaBuffer** ppBuffer);
+
+HRESULT MFTEnumEx(GUID guidCategory, UINT32 Flags,
+ const MFT_REGISTER_TYPE_INFO* pInputType,
+ const MFT_REGISTER_TYPE_INFO* pOutputType,
+ IMFActivate*** pppMFTActivate, UINT32* pnumMFTActivate);
+
+HRESULT MFTGetInfo(CLSID clsidMFT, LPWSTR* pszName,
+ MFT_REGISTER_TYPE_INFO** ppInputTypes, UINT32* pcInputTypes,
+ MFT_REGISTER_TYPE_INFO** ppOutputTypes,
+ UINT32* pcOutputTypes, IMFAttributes** ppAttributes);
+
+HRESULT MFCreateAttributes(IMFAttributes** ppMFAttributes, UINT32 cInitialSize);
+
+HRESULT MFCreateEventQueue(IMFMediaEventQueue** ppMediaEventQueue);
+
+HRESULT MFCreateStreamDescriptor(DWORD dwStreamIdentifier, DWORD cMediaTypes,
+ IMFMediaType** apMediaTypes,
+ IMFStreamDescriptor** ppDescriptor);
+
+HRESULT MFCreateAsyncResult(IUnknown* punkObject, IMFAsyncCallback* pCallback,
+ IUnknown* punkState,
+ IMFAsyncResult** ppAsyncResult);
+
+HRESULT MFCreatePresentationDescriptor(
+ DWORD cStreamDescriptors, IMFStreamDescriptor** apStreamDescriptors,
+ IMFPresentationDescriptor** ppPresentationDescriptor);
+
+HRESULT MFCreateMemoryBuffer(DWORD cbMaxLength, IMFMediaBuffer** ppBuffer);
+
+HRESULT MFLockDXGIDeviceManager(UINT* pResetToken,
+ IMFDXGIDeviceManager** ppManager);
+
+HRESULT MFUnlockDXGIDeviceManager();
+
+HRESULT MFPutWorkItem(DWORD dwQueue, IMFAsyncCallback* pCallback,
+ IUnknown* pState);
+
+HRESULT MFSerializeAttributesToStream(IMFAttributes* pAttr, DWORD dwOptions,
+ IStream* pStm);
+
+HRESULT MFWrapMediaType(IMFMediaType* pOrig, REFGUID MajorType, REFGUID SubType,
+ IMFMediaType** ppWrap);
+
+} // namespace mozilla::wmf
+
+#endif
diff --git a/dom/media/platforms/wmf/WMFAudioMFTManager.cpp b/dom/media/platforms/wmf/WMFAudioMFTManager.cpp
new file mode 100644
index 0000000000..6ebcf9a80a
--- /dev/null
+++ b/dom/media/platforms/wmf/WMFAudioMFTManager.cpp
@@ -0,0 +1,315 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WMFAudioMFTManager.h"
+#include "MediaInfo.h"
+#include "TimeUnits.h"
+#include "VideoUtils.h"
+#include "WMFUtils.h"
+#include "mozilla/AbstractThread.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Telemetry.h"
+#include "nsTArray.h"
+#include "BufferReader.h"
+#include "mozilla/ScopeExit.h"
+
+#define LOG(...) MOZ_LOG(sPDMLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
+
+namespace mozilla {
+
+using media::TimeUnit;
+
+WMFAudioMFTManager::WMFAudioMFTManager(const AudioInfo& aConfig)
+ : mAudioChannels(aConfig.mChannels),
+ mChannelsMap(AudioConfig::ChannelLayout::UNKNOWN_MAP),
+ mAudioRate(aConfig.mRate),
+ mStreamType(GetStreamTypeFromMimeType(aConfig.mMimeType)) {
+ MOZ_COUNT_CTOR(WMFAudioMFTManager);
+
+ if (mStreamType == WMFStreamType::AAC) {
+ const uint8_t* audioSpecConfig;
+ uint32_t configLength;
+ if (aConfig.mCodecSpecificConfig.is<AacCodecSpecificData>()) {
+ const AacCodecSpecificData& aacCodecSpecificData =
+ aConfig.mCodecSpecificConfig.as<AacCodecSpecificData>();
+ audioSpecConfig =
+ aacCodecSpecificData.mDecoderConfigDescriptorBinaryBlob->Elements();
+ configLength =
+ aacCodecSpecificData.mDecoderConfigDescriptorBinaryBlob->Length();
+
+ mRemainingEncoderDelay = mEncoderDelay =
+ aacCodecSpecificData.mEncoderDelayFrames;
+ mTotalMediaFrames = aacCodecSpecificData.mMediaFrameCount;
+ LOG("AudioMFT decoder: Found AAC decoder delay (%" PRIu32
+ "frames) and total media frames (%" PRIu64 " frames)\n",
+ mEncoderDelay, mTotalMediaFrames);
+ } else {
+ // Gracefully handle failure to cover all codec specific cases above. Once
+ // we're confident there is no fall through from these cases above, we
+ // should remove this code.
+ RefPtr<MediaByteBuffer> audioCodecSpecificBinaryBlob =
+ GetAudioCodecSpecificBlob(aConfig.mCodecSpecificConfig);
+ audioSpecConfig = audioCodecSpecificBinaryBlob->Elements();
+ configLength = audioCodecSpecificBinaryBlob->Length();
+ }
+ AACAudioSpecificConfigToUserData(aConfig.mExtendedProfile, audioSpecConfig,
+ configLength, mUserData);
+ }
+}
+
+WMFAudioMFTManager::~WMFAudioMFTManager() {
+ MOZ_COUNT_DTOR(WMFAudioMFTManager);
+}
+
+const GUID& WMFAudioMFTManager::GetMediaSubtypeGUID() {
+ MOZ_ASSERT(StreamTypeIsAudio(mStreamType));
+ switch (mStreamType) {
+ case WMFStreamType::AAC:
+ return MFAudioFormat_AAC;
+ case WMFStreamType::MP3:
+ return MFAudioFormat_MP3;
+ default:
+ return GUID_NULL;
+ };
+}
+
+bool WMFAudioMFTManager::Init() {
+ NS_ENSURE_TRUE(StreamTypeIsAudio(mStreamType), false);
+
+ RefPtr<MFTDecoder> decoder(new MFTDecoder());
+ // Note: MP3 MFT isn't registered as supporting Float output, but it works.
+ // Find PCM output MFTs as this is the common type.
+ HRESULT hr = WMFDecoderModule::CreateMFTDecoder(mStreamType, decoder);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+
+ // Setup input/output media types
+ RefPtr<IMFMediaType> inputType;
+
+ hr = wmf::MFCreateMediaType(getter_AddRefs(inputType));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+
+ hr = inputType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+
+ hr = inputType->SetGUID(MF_MT_SUBTYPE, GetMediaSubtypeGUID());
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+
+ hr = inputType->SetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, mAudioRate);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+
+ hr = inputType->SetUINT32(MF_MT_AUDIO_NUM_CHANNELS, mAudioChannels);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+
+ if (mStreamType == WMFStreamType::AAC) {
+ hr = inputType->SetUINT32(MF_MT_AAC_PAYLOAD_TYPE, 0x0); // Raw AAC packet
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+
+ hr = inputType->SetBlob(MF_MT_USER_DATA, mUserData.Elements(),
+ mUserData.Length());
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+ }
+
+ RefPtr<IMFMediaType> outputType;
+ hr = wmf::MFCreateMediaType(getter_AddRefs(outputType));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+
+ hr = outputType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Audio);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+
+ hr = outputType->SetGUID(MF_MT_SUBTYPE, MFAudioFormat_Float);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+
+ hr = outputType->SetUINT32(MF_MT_AUDIO_BITS_PER_SAMPLE, 32);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+
+ hr = decoder->SetMediaTypes(inputType, outputType);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), false);
+
+ mDecoder = decoder;
+
+ return true;
+}
+
+HRESULT
+WMFAudioMFTManager::Input(MediaRawData* aSample) {
+ mLastInputTime = aSample->mTime;
+ return mDecoder->Input(aSample->Data(), uint32_t(aSample->Size()),
+ aSample->mTime.ToMicroseconds(),
+ aSample->mDuration.ToMicroseconds());
+}
+
+nsCString WMFAudioMFTManager::GetCodecName() const {
+ if (mStreamType == WMFStreamType::AAC) {
+ return "aac"_ns;
+ } else if (mStreamType == WMFStreamType::MP3) {
+ return "mp3"_ns;
+ }
+ return "unknown"_ns;
+}
+
+HRESULT
+WMFAudioMFTManager::UpdateOutputType() {
+ HRESULT hr;
+
+ RefPtr<IMFMediaType> type;
+ hr = mDecoder->GetOutputMediaType(type);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = type->GetUINT32(MF_MT_AUDIO_SAMPLES_PER_SECOND, &mAudioRate);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = type->GetUINT32(MF_MT_AUDIO_NUM_CHANNELS, &mAudioChannels);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ uint32_t channelsMap;
+ hr = type->GetUINT32(MF_MT_AUDIO_CHANNEL_MASK, &channelsMap);
+ if (SUCCEEDED(hr)) {
+ mChannelsMap = channelsMap;
+ } else {
+ LOG("Unable to retrieve channel layout. Ignoring");
+ mChannelsMap = AudioConfig::ChannelLayout::UNKNOWN_MAP;
+ }
+
+ return S_OK;
+}
+
+HRESULT
+WMFAudioMFTManager::Output(int64_t aStreamOffset, RefPtr<MediaData>& aOutData) {
+ aOutData = nullptr;
+ RefPtr<IMFSample> sample;
+ HRESULT hr;
+ int typeChangeCount = 0;
+ const auto oldAudioRate = mAudioRate;
+ while (true) {
+ hr = mDecoder->Output(&sample);
+ if (hr == MF_E_TRANSFORM_NEED_MORE_INPUT) {
+ return hr;
+ }
+ if (hr == MF_E_TRANSFORM_STREAM_CHANGE) {
+ hr = mDecoder->FindDecoderOutputType();
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ hr = UpdateOutputType();
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ // Catch infinite loops, but some decoders perform at least 2 stream
+ // changes on consecutive calls, so be permissive.
+ // 100 is arbitrarily > 2.
+ NS_ENSURE_TRUE(typeChangeCount < 100, MF_E_TRANSFORM_STREAM_CHANGE);
+ ++typeChangeCount;
+ continue;
+ }
+ break;
+ }
+
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ if (!sample) {
+ LOG("Audio MFTDecoder returned success but null output.");
+ return E_FAIL;
+ }
+
+ UINT32 discontinuity = false;
+ sample->GetUINT32(MFSampleExtension_Discontinuity, &discontinuity);
+ if (mFirstFrame || discontinuity) {
+ // Update the output type, in case this segment has a different
+ // rate. This also triggers on the first sample, which can have a
+ // different rate than is advertised in the container, and sometimes we
+ // don't get a MF_E_TRANSFORM_STREAM_CHANGE when the rate changes.
+ hr = UpdateOutputType();
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ mFirstFrame = false;
+ }
+
+ LONGLONG hns;
+ hr = sample->GetSampleTime(&hns);
+ if (FAILED(hr)) {
+ return E_FAIL;
+ }
+ TimeUnit pts = TimeUnit::FromHns(hns, mAudioRate);
+ NS_ENSURE_TRUE(pts.IsValid(), E_FAIL);
+
+ RefPtr<IMFMediaBuffer> buffer;
+ hr = sample->ConvertToContiguousBuffer(getter_AddRefs(buffer));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ BYTE* data = nullptr; // Note: *data will be owned by the IMFMediaBuffer, we
+ // don't need to free it.
+ DWORD maxLength = 0, currentLength = 0;
+ hr = buffer->Lock(&data, &maxLength, &currentLength);
+ ScopeExit exit([buffer] { buffer->Unlock(); });
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ // Output is made of floats.
+ int32_t numSamples = currentLength / sizeof(float);
+ int32_t numFrames = numSamples / mAudioChannels;
+ MOZ_ASSERT(numFrames >= 0);
+ MOZ_ASSERT(numSamples >= 0);
+ if (numFrames == 0) {
+ // All data from this chunk stripped, loop back and try to output the next
+ // frame, if possible.
+ return S_OK;
+ }
+
+ if (oldAudioRate != mAudioRate) {
+ LOG("Audio rate changed from %" PRIu32 " to %" PRIu32, oldAudioRate,
+ mAudioRate);
+ }
+
+ AlignedAudioBuffer audioData(numSamples);
+ if (!audioData) {
+ return E_OUTOFMEMORY;
+ }
+
+ float* floatData = reinterpret_cast<float*>(data);
+ PodCopy(audioData.Data(), floatData, numSamples);
+
+ TimeUnit duration(numFrames, mAudioRate);
+ NS_ENSURE_TRUE(duration.IsValid(), E_FAIL);
+
+ const bool isAudioRateChangedToHigher = oldAudioRate < mAudioRate;
+ if (IsPartialOutput(duration, isAudioRateChangedToHigher)) {
+ LOG("Encounter a partial frame?! duration shrinks from %s to %s",
+ mLastOutputDuration.ToString().get(), duration.ToString().get());
+ return MF_E_TRANSFORM_NEED_MORE_INPUT;
+ }
+
+ aOutData = new AudioData(aStreamOffset, pts, std::move(audioData),
+ mAudioChannels, mAudioRate, mChannelsMap);
+ MOZ_DIAGNOSTIC_ASSERT(duration == aOutData->mDuration, "must be equal");
+ mLastOutputDuration = aOutData->mDuration;
+
+#ifdef LOG_SAMPLE_DECODE
+ LOG("Decoded audio sample! timestamp=%lld duration=%lld currentLength=%u",
+ pts.ToMicroseconds(), duration.ToMicroseconds(), currentLength);
+#endif
+
+ return S_OK;
+}
+
+bool WMFAudioMFTManager::IsPartialOutput(
+ const media::TimeUnit& aNewOutputDuration,
+ const bool aIsRateChangedToHigher) const {
+ // This issue was found in Windows11, where AAC MFT decoder would incorrectly
+ // output partial output samples to us, even if MS's documentation said it
+ // won't happen [1]. More details are described in bug 1731430 comment 26.
+ // If the audio rate isn't changed to higher, which would result in shorter
+ // duration, but the new output duration is still shorter than the last one,
+ // then new output is possible an incorrect partial output.
+ // [1]
+ // https://docs.microsoft.com/en-us/windows/win32/medfound/mft-message-command-drain
+ if (mStreamType != WMFStreamType::AAC) {
+ return false;
+ }
+ if (mLastOutputDuration > aNewOutputDuration && !aIsRateChangedToHigher) {
+ return true;
+ }
+ return false;
+}
+
+void WMFAudioMFTManager::Shutdown() { mDecoder = nullptr; }
+
+} // namespace mozilla
+
+#undef LOG
diff --git a/dom/media/platforms/wmf/WMFAudioMFTManager.h b/dom/media/platforms/wmf/WMFAudioMFTManager.h
new file mode 100644
index 0000000000..b5dc379396
--- /dev/null
+++ b/dom/media/platforms/wmf/WMFAudioMFTManager.h
@@ -0,0 +1,69 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(WMFAudioOutputSource_h_)
+# define WMFAudioOutputSource_h_
+
+# include "MFTDecoder.h"
+# include "WMF.h"
+# include "WMFDecoderModule.h"
+# include "WMFMediaDataDecoder.h"
+# include "mozilla/RefPtr.h"
+
+namespace mozilla {
+
+class WMFAudioMFTManager : public MFTManager {
+ public:
+ explicit WMFAudioMFTManager(const AudioInfo& aConfig);
+ ~WMFAudioMFTManager();
+
+ bool Init();
+
+ HRESULT Input(MediaRawData* aSample) override;
+
+ // Note WMF's AAC decoder sometimes output negatively timestamped samples,
+ // presumably they're the preroll samples, and we strip them. We may return
+ // a null aOutput in this case.
+ HRESULT Output(int64_t aStreamOffset, RefPtr<MediaData>& aOutput) override;
+
+ void Shutdown() override;
+
+ TrackInfo::TrackType GetType() override { return TrackInfo::kAudioTrack; }
+
+ nsCString GetDescriptionName() const override {
+ return "wmf audio decoder"_ns;
+ }
+
+ nsCString GetCodecName() const override;
+
+ private:
+ HRESULT UpdateOutputType();
+
+ bool IsPartialOutput(const media::TimeUnit& aNewOutputDuration,
+ const bool aIsRateChangedToHigher) const;
+
+ uint32_t mAudioChannels;
+ AudioConfig::ChannelLayout::ChannelMap mChannelsMap;
+ uint32_t mAudioRate;
+ nsTArray<BYTE> mUserData;
+
+ WMFStreamType mStreamType;
+
+ const GUID& GetMediaSubtypeGUID();
+
+ media::TimeUnit mLastInputTime = media::TimeUnit::Zero();
+ media::TimeUnit mLastOutputDuration = media::TimeUnit::Zero();
+
+ bool mFirstFrame = true;
+
+ uint64_t mTotalMediaFrames = 0;
+ uint32_t mEncoderDelay = 0;
+ uint32_t mRemainingEncoderDelay = 0;
+};
+
+} // namespace mozilla
+
+#endif // WMFAudioOutputSource_h_
diff --git a/dom/media/platforms/wmf/WMFDataEncoderUtils.h b/dom/media/platforms/wmf/WMFDataEncoderUtils.h
new file mode 100644
index 0000000000..49221f7ae3
--- /dev/null
+++ b/dom/media/platforms/wmf/WMFDataEncoderUtils.h
@@ -0,0 +1,165 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WMFMediaDataEncoder.h"
+
+#include "AnnexB.h"
+#include "H264.h"
+#include "libyuv.h"
+#include "mozilla/Logging.h"
+#include "mozilla/mscom/EnsureMTA.h"
+
+#define WMF_ENC_LOGD(arg, ...) \
+ MOZ_LOG( \
+ mozilla::sPEMLog, mozilla::LogLevel::Debug, \
+ ("WMFMediaDataEncoder(0x%p)::%s: " arg, this, __func__, ##__VA_ARGS__))
+#define WMF_ENC_LOGE(arg, ...) \
+ MOZ_LOG( \
+ mozilla::sPEMLog, mozilla::LogLevel::Error, \
+ ("WMFMediaDataEncoder(0x%p)::%s: " arg, this, __func__, ##__VA_ARGS__))
+
+namespace mozilla {
+
+extern LazyLogModule sPEMLog;
+
+static const GUID CodecToSubtype(MediaDataEncoder::CodecType aCodec) {
+ switch (aCodec) {
+ case MediaDataEncoder::CodecType::H264:
+ return MFVideoFormat_H264;
+ case MediaDataEncoder::CodecType::VP8:
+ return MFVideoFormat_VP80;
+ case MediaDataEncoder::CodecType::VP9:
+ return MFVideoFormat_VP90;
+ default:
+ MOZ_ASSERT(false, "Unsupported codec");
+ return GUID_NULL;
+ }
+}
+
+bool CanCreateWMFEncoder(MediaDataEncoder::CodecType aCodec) {
+ bool canCreate = false;
+ mscom::EnsureMTA([&]() {
+ if (!wmf::MediaFoundationInitializer::HasInitialized()) {
+ return;
+ }
+ // Try HW encoder first.
+ auto enc = MakeRefPtr<MFTEncoder>(false /* HW not allowed */);
+ canCreate = SUCCEEDED(enc->Create(CodecToSubtype(aCodec)));
+ if (!canCreate) {
+ // Try SW encoder.
+ enc = MakeRefPtr<MFTEncoder>(true /* HW not allowed */);
+ canCreate = SUCCEEDED(enc->Create(CodecToSubtype(aCodec)));
+ }
+ });
+ return canCreate;
+}
+
+static already_AddRefed<MediaByteBuffer> ParseH264Parameters(
+ nsTArray<uint8_t>& aHeader, const bool aAsAnnexB) {
+ size_t length = aHeader.Length();
+ auto annexB = MakeRefPtr<MediaByteBuffer>(length);
+ PodCopy(annexB->Elements(), aHeader.Elements(), length);
+ annexB->SetLength(length);
+ if (aAsAnnexB) {
+ return annexB.forget();
+ }
+
+ // Convert to avcC.
+ nsTArray<AnnexB::NALEntry> paramSets;
+ AnnexB::ParseNALEntries(
+ Span<const uint8_t>(annexB->Elements(), annexB->Length()), paramSets);
+
+ auto avcc = MakeRefPtr<MediaByteBuffer>();
+ AnnexB::NALEntry& sps = paramSets.ElementAt(0);
+ AnnexB::NALEntry& pps = paramSets.ElementAt(1);
+ const uint8_t* spsPtr = annexB->Elements() + sps.mOffset;
+ H264::WriteExtraData(
+ avcc, spsPtr[1], spsPtr[2], spsPtr[3],
+ Span<const uint8_t>(spsPtr, sps.mSize),
+ Span<const uint8_t>(annexB->Elements() + pps.mOffset, pps.mSize));
+ return avcc.forget();
+}
+
+static uint32_t GetProfile(
+ MediaDataEncoder::H264Specific::ProfileLevel aProfileLevel) {
+ switch (aProfileLevel) {
+ case MediaDataEncoder::H264Specific::ProfileLevel::BaselineAutoLevel:
+ return eAVEncH264VProfile_Base;
+ case MediaDataEncoder::H264Specific::ProfileLevel::MainAutoLevel:
+ return eAVEncH264VProfile_Main;
+ default:
+ return eAVEncH264VProfile_unknown;
+ }
+}
+
+template <typename Config>
+HRESULT SetMediaTypes(RefPtr<MFTEncoder>& aEncoder, Config& aConfig) {
+ RefPtr<IMFMediaType> inputType = CreateInputType(aConfig);
+ if (!inputType) {
+ return E_FAIL;
+ }
+
+ RefPtr<IMFMediaType> outputType = CreateOutputType(aConfig);
+ if (!outputType) {
+ return E_FAIL;
+ }
+
+ return aEncoder->SetMediaTypes(inputType, outputType);
+}
+
+template <typename Config>
+already_AddRefed<IMFMediaType> CreateInputType(Config& aConfig) {
+ RefPtr<IMFMediaType> type;
+ return SUCCEEDED(wmf::MFCreateMediaType(getter_AddRefs(type))) &&
+ SUCCEEDED(
+ type->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video)) &&
+ SUCCEEDED(type->SetGUID(MF_MT_SUBTYPE, MFVideoFormat_NV12)) &&
+ SUCCEEDED(type->SetUINT32(MF_MT_INTERLACE_MODE,
+ MFVideoInterlace_Progressive)) &&
+ SUCCEEDED(MFSetAttributeRatio(type, MF_MT_FRAME_RATE,
+ aConfig.mFramerate, 1)) &&
+ SUCCEEDED(MFSetAttributeSize(type, MF_MT_FRAME_SIZE,
+ aConfig.mSize.width,
+ aConfig.mSize.height))
+ ? type.forget()
+ : nullptr;
+}
+
+template <typename Config>
+already_AddRefed<IMFMediaType> CreateOutputType(Config& aConfig) {
+ RefPtr<IMFMediaType> type;
+ if (FAILED(wmf::MFCreateMediaType(getter_AddRefs(type))) ||
+ FAILED(type->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video)) ||
+ FAILED(
+ type->SetGUID(MF_MT_SUBTYPE, CodecToSubtype(aConfig.mCodecType))) ||
+ FAILED(type->SetUINT32(MF_MT_AVG_BITRATE, aConfig.mBitsPerSec)) ||
+ FAILED(type->SetUINT32(MF_MT_INTERLACE_MODE,
+ MFVideoInterlace_Progressive)) ||
+ FAILED(
+ MFSetAttributeRatio(type, MF_MT_FRAME_RATE, aConfig.mFramerate, 1)) ||
+ FAILED(MFSetAttributeSize(type, MF_MT_FRAME_SIZE, aConfig.mSize.width,
+ aConfig.mSize.height))) {
+ return nullptr;
+ }
+ if (aConfig.mCodecSpecific &&
+ FAILED(SetCodecSpecific(type, aConfig.mCodecSpecific.ref()))) {
+ return nullptr;
+ }
+
+ return type.forget();
+}
+
+template <typename T>
+HRESULT SetCodecSpecific(IMFMediaType* aOutputType, const T& aSpecific) {
+ return S_OK;
+}
+
+template <>
+HRESULT SetCodecSpecific(IMFMediaType* aOutputType,
+ const MediaDataEncoder::H264Specific& aSpecific) {
+ return aOutputType->SetUINT32(MF_MT_MPEG2_PROFILE,
+ GetProfile(aSpecific.mProfileLevel));
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/wmf/WMFDecoderModule.cpp b/dom/media/platforms/wmf/WMFDecoderModule.cpp
new file mode 100644
index 0000000000..5366071840
--- /dev/null
+++ b/dom/media/platforms/wmf/WMFDecoderModule.cpp
@@ -0,0 +1,454 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WMFDecoderModule.h"
+
+#include <algorithm>
+#include <vector>
+
+#include "DriverCrashGuard.h"
+#include "GfxDriverInfo.h"
+#include "MFTDecoder.h"
+#include "MP4Decoder.h"
+#include "MediaInfo.h"
+#include "PDMFactory.h"
+#include "VPXDecoder.h"
+#include "WMFAudioMFTManager.h"
+#include "WMFMediaDataDecoder.h"
+#include "WMFVideoMFTManager.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/SyncRunnable.h"
+#include "mozilla/WindowsVersion.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/mscom/EnsureMTA.h"
+#include "mozilla/ProfilerMarkers.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIXULRuntime.h"
+#include "nsIXULRuntime.h" // for BrowserTabsRemoteAutostart
+#include "nsServiceManagerUtils.h"
+#include "nsWindowsHelpers.h"
+#include "prsystem.h"
+
+#ifdef MOZ_AV1
+# include "AOMDecoder.h"
+#endif
+
+#define LOG(...) MOZ_LOG(sPDMLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
+
+namespace mozilla {
+
+// Helper function to add a profile marker and log at the same time.
+static void MOZ_FORMAT_PRINTF(2, 3)
+ WmfDecoderModuleMarkerAndLog(const ProfilerString8View& aMarkerTag,
+ const char* aFormat, ...) {
+ va_list ap;
+ va_start(ap, aFormat);
+ const nsVprintfCString markerString(aFormat, ap);
+ va_end(ap);
+ PROFILER_MARKER_TEXT(aMarkerTag, MEDIA_PLAYBACK, {}, markerString);
+ LOG("%s", markerString.get());
+}
+
+static const GUID CLSID_CMSAACDecMFT = {
+ 0x32D186A7,
+ 0x218F,
+ 0x4C75,
+ {0x88, 0x76, 0xDD, 0x77, 0x27, 0x3A, 0x89, 0x99}};
+
+static Atomic<bool> sDXVAEnabled(false);
+
+/* static */
+already_AddRefed<PlatformDecoderModule> WMFDecoderModule::Create() {
+ RefPtr<WMFDecoderModule> wmf = new WMFDecoderModule();
+ return wmf.forget();
+}
+
+static bool IsRemoteAcceleratedCompositor(
+ layers::KnowsCompositor* aKnowsCompositor) {
+ if (!aKnowsCompositor) {
+ return false;
+ }
+
+ if (aKnowsCompositor->UsingSoftwareWebRenderD3D11()) {
+ return true;
+ }
+
+ layers::TextureFactoryIdentifier ident =
+ aKnowsCompositor->GetTextureFactoryIdentifier();
+ return !aKnowsCompositor->UsingSoftwareWebRender() &&
+ ident.mParentProcessType == GeckoProcessType_GPU;
+}
+
+static Atomic<bool> sSupportedTypesInitialized(false);
+static EnumSet<WMFStreamType> sSupportedTypes;
+
+/* static */
+void WMFDecoderModule::Init() {
+ MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
+ if (XRE_IsContentProcess()) {
+ // If we're in the content process and the UseGPUDecoder pref is set, it
+ // means that we've given up on the GPU process (it's been crashing) so we
+ // should disable DXVA
+ sDXVAEnabled = !StaticPrefs::media_gpu_process_decoder();
+ } else if (XRE_IsGPUProcess()) {
+ // Always allow DXVA in the GPU process.
+ sDXVAEnabled = true;
+ } else if (XRE_IsRDDProcess()) {
+ // Hardware accelerated decoding is explicitly only done in the GPU process
+ // to avoid copying textures whenever possible. Previously, detecting
+ // whether the video bridge was set up could be done with the following:
+ // sDXVAEnabled = !!DeviceManagerDx::Get()->GetImageDevice();
+ // The video bridge was previously broken due to initialization order
+ // issues. For more information see Bug 1763880.
+ sDXVAEnabled = false;
+ } else {
+ // Only allow DXVA in the UI process if we aren't in e10s Firefox
+ sDXVAEnabled = !mozilla::BrowserTabsRemoteAutostart();
+ }
+
+ // We have heavy logging below to help diagnose issue around hardware
+ // decoding failures. Due to these failures often relating to driver level
+ // problems they're hard to nail down, so we want lots of info. We may be
+ // able to relax this in future if we're not seeing such problems (see bug
+ // 1673007 for references to the bugs motivating this).
+ bool hwVideo = gfx::gfxVars::GetCanUseHardwareVideoDecodingOrDefault();
+ WmfDecoderModuleMarkerAndLog(
+ "WMFInit DXVA Status",
+ "sDXVAEnabled: %s, CanUseHardwareVideoDecoding: %s",
+ sDXVAEnabled ? "true" : "false", hwVideo ? "true" : "false");
+ sDXVAEnabled = sDXVAEnabled && hwVideo;
+
+ mozilla::mscom::EnsureMTA([&]() {
+ // Store the supported MFT decoders.
+ sSupportedTypes.clear();
+ // i = 1 to skip Unknown.
+ for (uint32_t i = 1; i < static_cast<uint32_t>(WMFStreamType::SENTINEL);
+ i++) {
+ WMFStreamType type = static_cast<WMFStreamType>(i);
+ RefPtr<MFTDecoder> decoder = new MFTDecoder();
+ HRESULT hr = CreateMFTDecoder(type, decoder);
+ if (SUCCEEDED(hr)) {
+ sSupportedTypes += type;
+ WmfDecoderModuleMarkerAndLog("WMFInit Decoder Supported",
+ "%s is enabled", StreamTypeToString(type));
+ } else if (hr != E_FAIL) {
+ // E_FAIL should be logged by CreateMFTDecoder. Skipping those codes
+ // will help to keep the logs readable.
+ WmfDecoderModuleMarkerAndLog("WMFInit Decoder Failed",
+ "%s failed with code 0x%lx",
+ StreamTypeToString(type), hr);
+ }
+ }
+ });
+
+ sSupportedTypesInitialized = true;
+
+ WmfDecoderModuleMarkerAndLog("WMFInit Result",
+ "WMFDecoderModule::Init finishing");
+}
+
+/* static */
+int WMFDecoderModule::GetNumDecoderThreads() {
+ int32_t numCores = PR_GetNumberOfProcessors();
+
+ // If we have more than 4 cores, let the decoder decide how many threads.
+ // On an 8 core machine, WMF chooses 4 decoder threads.
+ static const int WMF_DECODER_DEFAULT = -1;
+ if (numCores > 4) {
+ return WMF_DECODER_DEFAULT;
+ }
+ return std::max(numCores - 1, 1);
+}
+
+/* static */
+HRESULT WMFDecoderModule::CreateMFTDecoder(const WMFStreamType& aType,
+ RefPtr<MFTDecoder>& aDecoder) {
+ // Do not expose any video decoder on utility process which is only for audio
+ // decoding.
+ if (XRE_IsUtilityProcess()) {
+ switch (aType) {
+ case WMFStreamType::H264:
+ case WMFStreamType::VP8:
+ case WMFStreamType::VP9:
+ case WMFStreamType::AV1:
+ return E_FAIL;
+ default:
+ break;
+ }
+ }
+
+ switch (aType) {
+ case WMFStreamType::H264:
+ return aDecoder->Create(CLSID_CMSH264DecoderMFT);
+ case WMFStreamType::VP8:
+ static const uint32_t VP8_USABLE_BUILD = 16287;
+ if (!IsWindowsBuildOrLater(VP8_USABLE_BUILD)) {
+ WmfDecoderModuleMarkerAndLog("CreateMFTDecoder, VP8 Failure",
+ "VP8 MFT requires Windows build %" PRId32
+ " or later",
+ VP8_USABLE_BUILD);
+ return E_FAIL;
+ }
+ if (!gfx::gfxVars::UseVP8HwDecode()) {
+ WmfDecoderModuleMarkerAndLog("CreateMFTDecoder, VP8 Failure",
+ "Gfx VP8 blocklist");
+ return E_FAIL;
+ }
+ [[fallthrough]];
+ case WMFStreamType::VP9:
+ if (!sDXVAEnabled) {
+ WmfDecoderModuleMarkerAndLog("CreateMFTDecoder, VPx Disabled",
+ "%s MFT requires DXVA",
+ StreamTypeToString(aType));
+ return E_FAIL;
+ }
+
+ {
+ gfx::WMFVPXVideoCrashGuard guard;
+ if (guard.Crashed()) {
+ WmfDecoderModuleMarkerAndLog(
+ "CreateMFTDecoder, VPx Failure",
+ "Will not use VPx MFT due to crash guard reporting a crash");
+ return E_FAIL;
+ }
+ return aDecoder->Create(CLSID_CMSVPXDecMFT);
+ }
+#ifdef MOZ_AV1
+ case WMFStreamType::AV1:
+ // If this process cannot use DXVA, the AV1 decoder will not be used.
+ // Also, upon startup, init will be called both before and after
+ // layers acceleration is setup. This prevents creating the AV1 decoder
+ // twice.
+ if (!sDXVAEnabled) {
+ WmfDecoderModuleMarkerAndLog("CreateMFTDecoder AV1 Disabled",
+ "AV1 MFT requires DXVA");
+ return E_FAIL;
+ }
+ // TODO: MFTEnumEx is slower than creating by CLSID, it may be worth
+ // investigating other ways to instantiate the AV1 decoder.
+ return aDecoder->Create(MFT_CATEGORY_VIDEO_DECODER, MFVideoFormat_AV1,
+ MFVideoFormat_NV12);
+#endif
+ case WMFStreamType::MP3:
+ return aDecoder->Create(CLSID_CMP3DecMediaObject);
+ case WMFStreamType::AAC:
+ return aDecoder->Create(CLSID_CMSAACDecMFT);
+ default:
+ return E_FAIL;
+ }
+}
+
+/* static */
+bool WMFDecoderModule::CanCreateMFTDecoder(const WMFStreamType& aType) {
+ MOZ_ASSERT(WMFStreamType::Unknown < aType && aType < WMFStreamType::SENTINEL);
+ if (!sSupportedTypesInitialized) {
+ if (NS_IsMainThread()) {
+ Init();
+ } else {
+ nsCOMPtr<nsIRunnable> runnable =
+ NS_NewRunnableFunction("WMFDecoderModule::Init", [&]() { Init(); });
+ SyncRunnable::DispatchToThread(GetMainThreadSerialEventTarget(),
+ runnable);
+ }
+ }
+
+ // Check prefs here rather than CreateMFTDecoder so that prefs aren't baked
+ // into sSupportedTypes
+ switch (aType) {
+ case WMFStreamType::VP8:
+ case WMFStreamType::VP9:
+ if (!StaticPrefs::media_wmf_vp9_enabled()) {
+ return false;
+ }
+ break;
+#ifdef MOZ_AV1
+ case WMFStreamType::AV1:
+ if (!StaticPrefs::media_av1_enabled() ||
+ !StaticPrefs::media_wmf_av1_enabled()) {
+ return false;
+ }
+ break;
+#endif
+ case WMFStreamType::MP3:
+ // Prefer ffvpx mp3 decoder over WMF.
+ if (StaticPrefs::media_ffvpx_mp3_enabled()) {
+ return false;
+ }
+ break;
+ default:
+ break;
+ }
+
+ // Do not expose any video decoder on utility process which is only for audio
+ // decoding.
+ if (XRE_IsUtilityProcess()) {
+ switch (aType) {
+ case WMFStreamType::H264:
+ case WMFStreamType::VP8:
+ case WMFStreamType::VP9:
+ case WMFStreamType::AV1:
+ return false;
+ default:
+ break;
+ }
+ }
+
+ return sSupportedTypes.contains(aType);
+}
+
+bool WMFDecoderModule::SupportsColorDepth(
+ gfx::ColorDepth aColorDepth, DecoderDoctorDiagnostics* aDiagnostics) const {
+ // Color depth support can be determined by creating DX decoders.
+ return true;
+}
+
+media::DecodeSupportSet WMFDecoderModule::Supports(
+ const SupportDecoderParams& aParams,
+ DecoderDoctorDiagnostics* aDiagnostics) const {
+ // This should only be supported by MFMediaEngineDecoderModule.
+ if (aParams.mMediaEngineId) {
+ return media::DecodeSupport::Unsupported;
+ }
+ // In GPU process, only support decoding if video. This only gives a hint of
+ // what the GPU decoder *may* support. The actual check will occur in
+ // CreateVideoDecoder.
+ const auto& trackInfo = aParams.mConfig;
+ if (XRE_IsGPUProcess() && !trackInfo.GetAsVideoInfo()) {
+ return media::DecodeSupport::Unsupported;
+ }
+
+ const auto* videoInfo = trackInfo.GetAsVideoInfo();
+ // Temporary - forces use of VPXDecoder when alpha is present.
+ // Bug 1263836 will handle alpha scenario once implemented. It will shift
+ // the check for alpha to PDMFactory but not itself remove the need for a
+ // check.
+ if (videoInfo && (!SupportsColorDepth(videoInfo->mColorDepth, aDiagnostics) ||
+ videoInfo->HasAlpha())) {
+ return media::DecodeSupport::Unsupported;
+ }
+
+ WMFStreamType type = GetStreamTypeFromMimeType(aParams.MimeType());
+ if (type == WMFStreamType::Unknown) {
+ return media::DecodeSupport::Unsupported;
+ }
+
+ if (CanCreateMFTDecoder(type)) {
+ if (StreamTypeIsVideo(type)) {
+ return sDXVAEnabled ? media::DecodeSupport::HardwareDecode
+ : media::DecodeSupport::SoftwareDecode;
+ } else {
+ // Audio only supports software decode
+ return media::DecodeSupport::SoftwareDecode;
+ }
+ }
+
+ return media::DecodeSupport::Unsupported;
+}
+
+nsresult WMFDecoderModule::Startup() {
+ return wmf::MediaFoundationInitializer::HasInitialized() ? NS_OK
+ : NS_ERROR_FAILURE;
+}
+
+already_AddRefed<MediaDataDecoder> WMFDecoderModule::CreateVideoDecoder(
+ const CreateDecoderParams& aParams) {
+ // In GPU process, only support decoding if an accelerated compositor is
+ // known.
+ if (XRE_IsGPUProcess() &&
+ !IsRemoteAcceleratedCompositor(aParams.mKnowsCompositor)) {
+ return nullptr;
+ }
+
+ UniquePtr<WMFVideoMFTManager> manager(new WMFVideoMFTManager(
+ aParams.VideoConfig(), aParams.mKnowsCompositor, aParams.mImageContainer,
+ aParams.mRate.mValue, aParams.mOptions, sDXVAEnabled,
+ aParams.mTrackingId));
+
+ MediaResult result = manager->Init();
+ if (NS_FAILED(result)) {
+ if (aParams.mError) {
+ *aParams.mError = result;
+ }
+ WmfDecoderModuleMarkerAndLog(
+ "WMFVDecoderCreation Failure",
+ "WMFDecoderModule::CreateVideoDecoder failed for manager with "
+ "description %s with result: %s",
+ manager->GetDescriptionName().get(), result.Description().get());
+ return nullptr;
+ }
+
+ nsAutoCString hwFailure;
+ if (!manager->IsHardwareAccelerated(hwFailure)) {
+ // The decoder description includes whether it is using software or
+ // hardware, but no information about how the hardware acceleration failed.
+ WmfDecoderModuleMarkerAndLog(
+ "WMFVDecoderCreation Success",
+ "WMFDecoderModule::CreateVideoDecoder success for manager with "
+ "description %s - DXVA failure: %s",
+ manager->GetDescriptionName().get(), hwFailure.get());
+ } else {
+ WmfDecoderModuleMarkerAndLog(
+ "WMFVDecoderCreation Success",
+ "WMFDecoderModule::CreateVideoDecoder success for manager with "
+ "description %s",
+ manager->GetDescriptionName().get());
+ }
+
+ RefPtr<MediaDataDecoder> decoder = new WMFMediaDataDecoder(manager.release());
+ return decoder.forget();
+}
+
+already_AddRefed<MediaDataDecoder> WMFDecoderModule::CreateAudioDecoder(
+ const CreateDecoderParams& aParams) {
+ if (XRE_IsGPUProcess()) {
+ // Only allow video in the GPU process.
+ return nullptr;
+ }
+
+ UniquePtr<WMFAudioMFTManager> manager(
+ new WMFAudioMFTManager(aParams.AudioConfig()));
+
+ if (!manager->Init()) {
+ WmfDecoderModuleMarkerAndLog(
+ "WMFADecoderCreation Failure",
+ "WMFDecoderModule::CreateAudioDecoder failed for manager with "
+ "description %s",
+ manager->GetDescriptionName().get());
+ return nullptr;
+ }
+
+ WmfDecoderModuleMarkerAndLog(
+ "WMFADecoderCreation Success",
+ "WMFDecoderModule::CreateAudioDecoder success for manager with "
+ "description %s",
+ manager->GetDescriptionName().get());
+
+ RefPtr<MediaDataDecoder> decoder = new WMFMediaDataDecoder(manager.release());
+ return decoder.forget();
+}
+
+media::DecodeSupportSet WMFDecoderModule::SupportsMimeType(
+ const nsACString& aMimeType, DecoderDoctorDiagnostics* aDiagnostics) const {
+ UniquePtr<TrackInfo> trackInfo = CreateTrackInfoWithMIMEType(aMimeType);
+ if (!trackInfo) {
+ return media::DecodeSupport::Unsupported;
+ }
+ auto supports = Supports(SupportDecoderParams(*trackInfo), aDiagnostics);
+ MOZ_LOG(
+ sPDMLog, LogLevel::Debug,
+ ("WMF decoder %s requested type '%s'",
+ supports != media::DecodeSupport::Unsupported ? "supports" : "rejects",
+ aMimeType.BeginReading()));
+ return supports;
+}
+
+} // namespace mozilla
+
+#undef WFM_DECODER_MODULE_STATUS_MARKER
+#undef LOG
diff --git a/dom/media/platforms/wmf/WMFDecoderModule.h b/dom/media/platforms/wmf/WMFDecoderModule.h
new file mode 100644
index 0000000000..3198860511
--- /dev/null
+++ b/dom/media/platforms/wmf/WMFDecoderModule.h
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(WMFPlatformDecoderModule_h_)
+# define WMFPlatformDecoderModule_h_
+
+# include "PlatformDecoderModule.h"
+# include "WMF.h"
+# include "WMFUtils.h"
+
+namespace mozilla {
+
+class MFTDecoder;
+
+class WMFDecoderModule : public PlatformDecoderModule {
+ public:
+ static already_AddRefed<PlatformDecoderModule> Create();
+
+ // Initializes the module, loads required dynamic libraries, etc.
+ nsresult Startup() override;
+
+ already_AddRefed<MediaDataDecoder> CreateVideoDecoder(
+ const CreateDecoderParams& aParams) override;
+
+ already_AddRefed<MediaDataDecoder> CreateAudioDecoder(
+ const CreateDecoderParams& aParams) override;
+
+ bool SupportsColorDepth(
+ gfx::ColorDepth aColorDepth,
+ DecoderDoctorDiagnostics* aDiagnostics) const override;
+ media::DecodeSupportSet SupportsMimeType(
+ const nsACString& aMimeType,
+ DecoderDoctorDiagnostics* aDiagnostics) const override;
+ media::DecodeSupportSet Supports(
+ const SupportDecoderParams& aParams,
+ DecoderDoctorDiagnostics* aDiagnostics) const override;
+
+ // Called on main thread.
+ static void Init();
+
+ // Called from any thread, must call init first
+ static int GetNumDecoderThreads();
+
+ static HRESULT CreateMFTDecoder(const WMFStreamType& aType,
+ RefPtr<MFTDecoder>& aDecoder);
+ static bool CanCreateMFTDecoder(const WMFStreamType& aType);
+
+ private:
+ WMFDecoderModule() = default;
+ virtual ~WMFDecoderModule() = default;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/platforms/wmf/WMFEncoderModule.cpp b/dom/media/platforms/wmf/WMFEncoderModule.cpp
new file mode 100644
index 0000000000..0f8e432390
--- /dev/null
+++ b/dom/media/platforms/wmf/WMFEncoderModule.cpp
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WMFEncoderModule.h"
+
+#include "WMFMediaDataEncoder.h"
+
+namespace mozilla {
+extern LazyLogModule sPEMLog;
+
+bool WMFEncoderModule::SupportsMimeType(const nsACString& aMimeType) const {
+ return CanCreateWMFEncoder(CreateEncoderParams::CodecTypeForMime(aMimeType));
+}
+
+already_AddRefed<MediaDataEncoder> WMFEncoderModule::CreateVideoEncoder(
+ const CreateEncoderParams& aParams, const bool aHardwareNotAllowed) const {
+ MediaDataEncoder::CodecType codec =
+ CreateEncoderParams::CodecTypeForMime(aParams.mConfig.mMimeType);
+ RefPtr<MediaDataEncoder> encoder;
+ switch (codec) {
+ case MediaDataEncoder::CodecType::H264:
+ encoder = new WMFMediaDataEncoder<MediaDataEncoder::H264Config>(
+ aParams.ToH264Config(), aParams.mTaskQueue, aHardwareNotAllowed);
+ break;
+ case MediaDataEncoder::CodecType::VP8:
+ encoder = new WMFMediaDataEncoder<MediaDataEncoder::VP8Config>(
+ aParams.ToVP8Config(), aParams.mTaskQueue, aHardwareNotAllowed);
+ break;
+ case MediaDataEncoder::CodecType::VP9:
+ encoder = new WMFMediaDataEncoder<MediaDataEncoder::VP9Config>(
+ aParams.ToVP9Config(), aParams.mTaskQueue, aHardwareNotAllowed);
+ break;
+ default:
+ // Do nothing.
+ break;
+ }
+ return encoder.forget();
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/wmf/WMFEncoderModule.h b/dom/media/platforms/wmf/WMFEncoderModule.h
new file mode 100644
index 0000000000..6d02a3af96
--- /dev/null
+++ b/dom/media/platforms/wmf/WMFEncoderModule.h
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef WMFEncoderModule_h_
+#define WMFEncoderModule_h_
+
+#include "PlatformEncoderModule.h"
+
+namespace mozilla {
+class WMFEncoderModule final : public PlatformEncoderModule {
+ public:
+ bool SupportsMimeType(const nsACString& aMimeType) const override;
+
+ already_AddRefed<MediaDataEncoder> CreateVideoEncoder(
+ const CreateEncoderParams& aParams,
+ const bool aHardwareNotAllowed) const override;
+};
+
+} // namespace mozilla
+
+#endif /* WMFEncoderModule_h_ */
diff --git a/dom/media/platforms/wmf/WMFMediaDataDecoder.cpp b/dom/media/platforms/wmf/WMFMediaDataDecoder.cpp
new file mode 100644
index 0000000000..73589d02c2
--- /dev/null
+++ b/dom/media/platforms/wmf/WMFMediaDataDecoder.cpp
@@ -0,0 +1,279 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WMFMediaDataDecoder.h"
+
+#include "VideoUtils.h"
+#include "WMFUtils.h"
+#include "mozilla/Logging.h"
+#include "mozilla/ProfilerMarkers.h"
+#include "mozilla/SyncRunnable.h"
+#include "mozilla/TaskQueue.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/WindowsVersion.h"
+#include "nsTArray.h"
+
+#define LOG(...) MOZ_LOG(sPDMLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
+
+namespace mozilla {
+
+WMFMediaDataDecoder::WMFMediaDataDecoder(MFTManager* aMFTManager)
+ : mTaskQueue(TaskQueue::Create(
+ GetMediaThreadPool(MediaThreadType::PLATFORM_DECODER),
+ "WMFMediaDataDecoder")),
+ mMFTManager(aMFTManager) {}
+
+WMFMediaDataDecoder::~WMFMediaDataDecoder() {}
+
+RefPtr<MediaDataDecoder::InitPromise> WMFMediaDataDecoder::Init() {
+ MOZ_ASSERT(!mIsShutDown);
+ return InitPromise::CreateAndResolve(mMFTManager->GetType(), __func__);
+}
+
+RefPtr<ShutdownPromise> WMFMediaDataDecoder::Shutdown() {
+ MOZ_DIAGNOSTIC_ASSERT(!mIsShutDown);
+ mIsShutDown = true;
+
+ return InvokeAsync(mTaskQueue, __func__, [self = RefPtr{this}, this] {
+ if (mMFTManager) {
+ mMFTManager->Shutdown();
+ mMFTManager = nullptr;
+ }
+ return mTaskQueue->BeginShutdown();
+ });
+}
+
+// Inserts data into the decoder's pipeline.
+RefPtr<MediaDataDecoder::DecodePromise> WMFMediaDataDecoder::Decode(
+ MediaRawData* aSample) {
+ MOZ_DIAGNOSTIC_ASSERT(!mIsShutDown);
+
+ return InvokeAsync<MediaRawData*>(
+ mTaskQueue, this, __func__, &WMFMediaDataDecoder::ProcessDecode, aSample);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> WMFMediaDataDecoder::ProcessError(
+ HRESULT aError, const char* aReason) {
+ MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
+
+ nsPrintfCString markerString(
+ "WMFMediaDataDecoder::ProcessError for decoder with description %s with "
+ "reason: %s",
+ GetDescriptionName().get(), aReason);
+ LOG("%s", markerString.get());
+ PROFILER_MARKER_TEXT("WMFDecoder Error", MEDIA_PLAYBACK, {}, markerString);
+
+ // TODO: For the error DXGI_ERROR_DEVICE_RESET, we could return
+ // NS_ERROR_DOM_MEDIA_NEED_NEW_DECODER to get the latest device. Maybe retry
+ // up to 3 times.
+ return DecodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_DECODE_ERR,
+ RESULT_DETAIL("%s:%lx", aReason, aError)),
+ __func__);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> WMFMediaDataDecoder::ProcessDecode(
+ MediaRawData* aSample) {
+ MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
+ DecodedData results;
+ LOG("ProcessDecode, type=%s, sample=%" PRId64,
+ TrackTypeToStr(mMFTManager->GetType()), aSample->mTime.ToMicroseconds());
+ HRESULT hr = mMFTManager->Input(aSample);
+ if (hr == MF_E_NOTACCEPTING) {
+ hr = ProcessOutput(results);
+ if (FAILED(hr) && hr != MF_E_TRANSFORM_NEED_MORE_INPUT) {
+ return ProcessError(hr, "MFTManager::Output(1)");
+ }
+ hr = mMFTManager->Input(aSample);
+ }
+
+ if (FAILED(hr)) {
+ NS_WARNING("MFTManager rejected sample");
+ return ProcessError(hr, "MFTManager::Input");
+ }
+
+ if (mOutputsCount == 0) {
+ mInputTimesSet.insert(aSample->mTime.ToMicroseconds());
+ }
+
+ if (!mLastTime || aSample->mTime > *mLastTime) {
+ mLastTime = Some(aSample->mTime);
+ mLastDuration = aSample->mDuration;
+ }
+
+ mSamplesCount++;
+ mDrainStatus = DrainStatus::DRAINABLE;
+ mLastStreamOffset = aSample->mOffset;
+
+ hr = ProcessOutput(results);
+ if (SUCCEEDED(hr) || hr == MF_E_TRANSFORM_NEED_MORE_INPUT) {
+ return DecodePromise::CreateAndResolve(std::move(results), __func__);
+ }
+ return ProcessError(hr, "MFTManager::Output(2)");
+}
+
+bool WMFMediaDataDecoder::ShouldGuardAgaintIncorrectFirstSample(
+ MediaData* aOutput) const {
+ // Incorrect first samples have only been observed in video tracks, so only
+ // guard video tracks.
+ if (mMFTManager->GetType() != TrackInfo::kVideoTrack) {
+ return false;
+ }
+
+ // By observation so far this issue only happens on Windows 10 so we don't
+ // need to enable this on other versions.
+ if (!IsWin10OrLater()) {
+ return false;
+ }
+
+ // This is not the first output sample so we don't need to guard it.
+ if (mOutputsCount != 0) {
+ return false;
+ }
+
+ // Output isn't in the map which contains the inputs we gave to the decoder.
+ // This is probably the invalid first sample. MFT decoder sometime will return
+ // incorrect first output to us, which always has 0 timestamp, even if the
+ // input we gave to MFT has timestamp that is way later than 0.
+ MOZ_ASSERT(!mInputTimesSet.empty());
+ return mInputTimesSet.find(aOutput->mTime.ToMicroseconds()) ==
+ mInputTimesSet.end() &&
+ aOutput->mTime.ToMicroseconds() == 0;
+}
+
+HRESULT
+WMFMediaDataDecoder::ProcessOutput(DecodedData& aResults) {
+ MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
+ RefPtr<MediaData> output;
+ HRESULT hr = S_OK;
+ while (SUCCEEDED(hr = mMFTManager->Output(mLastStreamOffset, output))) {
+ MOZ_ASSERT(output.get(), "Upon success, we must receive an output");
+ if (ShouldGuardAgaintIncorrectFirstSample(output)) {
+ LOG("Discarding sample with time %" PRId64
+ " because of ShouldGuardAgaintIncorrectFirstSample check",
+ output->mTime.ToMicroseconds());
+ continue;
+ }
+ if (++mOutputsCount == 1) {
+ // Got first valid sample, don't need to guard following sample anymore.
+ mInputTimesSet.clear();
+ }
+ aResults.AppendElement(std::move(output));
+ if (mDrainStatus == DrainStatus::DRAINING) {
+ break;
+ }
+ }
+ return hr;
+}
+
+RefPtr<MediaDataDecoder::FlushPromise> WMFMediaDataDecoder::ProcessFlush() {
+ MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
+ if (mMFTManager) {
+ mMFTManager->Flush();
+ }
+ LOG("ProcessFlush, type=%s", TrackTypeToStr(mMFTManager->GetType()));
+ mDrainStatus = DrainStatus::DRAINED;
+ mSamplesCount = 0;
+ mOutputsCount = 0;
+ mLastTime.reset();
+ mInputTimesSet.clear();
+ return FlushPromise::CreateAndResolve(true, __func__);
+}
+
+RefPtr<MediaDataDecoder::FlushPromise> WMFMediaDataDecoder::Flush() {
+ MOZ_DIAGNOSTIC_ASSERT(!mIsShutDown);
+
+ return InvokeAsync(mTaskQueue, this, __func__,
+ &WMFMediaDataDecoder::ProcessFlush);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> WMFMediaDataDecoder::ProcessDrain() {
+ MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
+ if (!mMFTManager || mDrainStatus == DrainStatus::DRAINED) {
+ return DecodePromise::CreateAndResolve(DecodedData(), __func__);
+ }
+
+ if (mDrainStatus != DrainStatus::DRAINING) {
+ // Order the decoder to drain...
+ mMFTManager->Drain();
+ mDrainStatus = DrainStatus::DRAINING;
+ }
+
+ // Then extract all available output.
+ DecodedData results;
+ HRESULT hr = ProcessOutput(results);
+ if (hr == MF_E_TRANSFORM_NEED_MORE_INPUT) {
+ mDrainStatus = DrainStatus::DRAINED;
+ }
+ if (SUCCEEDED(hr) || hr == MF_E_TRANSFORM_NEED_MORE_INPUT) {
+ if (results.Length() > 0 &&
+ results.LastElement()->mType == MediaData::Type::VIDEO_DATA) {
+ const RefPtr<MediaData>& data = results.LastElement();
+ if (mSamplesCount == 1 && data->mTime == media::TimeUnit::Zero()) {
+ // WMF is unable to calculate a duration if only a single sample
+ // was parsed. Additionally, the pts always comes out at 0 under those
+ // circumstances.
+ // Seeing that we've only fed the decoder a single frame, the pts
+ // and duration are known, it's of the last sample.
+ data->mTime = *mLastTime;
+ }
+ if (data->mTime == *mLastTime) {
+ // The WMF Video decoder is sometimes unable to provide a valid duration
+ // on the last sample even if it has been first set through
+ // SetSampleTime (appears to always happen on Windows 7). So we force
+ // set the duration of the last sample as it was input.
+ data->mDuration = mLastDuration;
+ }
+ } else if (results.Length() == 1 &&
+ results.LastElement()->mType == MediaData::Type::AUDIO_DATA) {
+ // When we drain the audio decoder and one frame was queued (such as with
+ // AAC) the MFT will re-calculate the starting time rather than use the
+ // value set on the IMF Sample.
+ // This is normally an okay thing to do; however when dealing with poorly
+ // muxed content that has incorrect start time, it could lead to broken
+ // A/V sync. So we ensure that we use the compressed sample's time
+ // instead. Additionally, this is what all other audio decoders are doing
+ // anyway.
+ MOZ_ASSERT(mLastTime,
+ "We must have attempted to decode at least one frame to get "
+ "one decoded output");
+ results.LastElement()->As<AudioData>()->SetOriginalStartTime(*mLastTime);
+ }
+ return DecodePromise::CreateAndResolve(std::move(results), __func__);
+ }
+ return ProcessError(hr, "MFTManager::Output");
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> WMFMediaDataDecoder::Drain() {
+ MOZ_DIAGNOSTIC_ASSERT(!mIsShutDown);
+
+ return InvokeAsync(mTaskQueue, this, __func__,
+ &WMFMediaDataDecoder::ProcessDrain);
+}
+
+bool WMFMediaDataDecoder::IsHardwareAccelerated(
+ nsACString& aFailureReason) const {
+ MOZ_ASSERT(!mIsShutDown);
+
+ return mMFTManager && mMFTManager->IsHardwareAccelerated(aFailureReason);
+}
+
+void WMFMediaDataDecoder::SetSeekThreshold(const media::TimeUnit& aTime) {
+ MOZ_DIAGNOSTIC_ASSERT(!mIsShutDown);
+
+ RefPtr<WMFMediaDataDecoder> self = this;
+ nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction(
+ "WMFMediaDataDecoder::SetSeekThreshold", [self, aTime]() {
+ MOZ_ASSERT(self->mTaskQueue->IsCurrentThreadIn());
+ media::TimeUnit threshold = aTime;
+ self->mMFTManager->SetSeekThreshold(threshold);
+ });
+ nsresult rv = mTaskQueue->Dispatch(runnable.forget());
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/wmf/WMFMediaDataDecoder.h b/dom/media/platforms/wmf/WMFMediaDataDecoder.h
new file mode 100644
index 0000000000..b344ba7b65
--- /dev/null
+++ b/dom/media/platforms/wmf/WMFMediaDataDecoder.h
@@ -0,0 +1,182 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(WMFMediaDataDecoder_h_)
+# define WMFMediaDataDecoder_h_
+
+# include <set>
+
+# include "MFTDecoder.h"
+# include "PlatformDecoderModule.h"
+# include "WMF.h"
+# include "mozilla/RefPtr.h"
+
+namespace mozilla {
+
+// Encapsulates the initialization of the MFTDecoder appropriate for decoding
+// a given stream, and the process of converting the IMFSample produced
+// by the MFT into a MediaData object.
+class MFTManager {
+ public:
+ virtual ~MFTManager() {}
+
+ // Submit a compressed sample for decoding.
+ // This should forward to the MFTDecoder after performing
+ // any required sample formatting.
+ virtual HRESULT Input(MediaRawData* aSample) = 0;
+
+ // Produces decoded output, if possible. Blocks until output can be produced,
+ // or until no more is able to be produced.
+ // Returns S_OK on success, or MF_E_TRANSFORM_NEED_MORE_INPUT if there's not
+ // enough data to produce more output. If this returns a failure code other
+ // than MF_E_TRANSFORM_NEED_MORE_INPUT, an error will be reported to the
+ // MP4Reader.
+ virtual HRESULT Output(int64_t aStreamOffset, RefPtr<MediaData>& aOutput) = 0;
+
+ virtual void Flush() {
+ mDecoder->Flush();
+ mSeekTargetThreshold.reset();
+ }
+
+ void Drain() {
+ if (FAILED(mDecoder->SendMFTMessage(MFT_MESSAGE_COMMAND_DRAIN, 0))) {
+ NS_WARNING("Failed to send DRAIN command to MFT");
+ }
+ }
+
+ // Destroys all resources.
+ virtual void Shutdown() = 0;
+
+ virtual bool IsHardwareAccelerated(nsACString& aFailureReason) const {
+ return false;
+ }
+
+ virtual TrackInfo::TrackType GetType() = 0;
+
+ virtual nsCString GetDescriptionName() const = 0;
+
+ virtual nsCString GetCodecName() const = 0;
+
+ virtual void SetSeekThreshold(const media::TimeUnit& aTime) {
+ if (aTime.IsValid()) {
+ mSeekTargetThreshold = Some(aTime);
+ } else {
+ mSeekTargetThreshold.reset();
+ }
+ }
+
+ virtual bool HasSeekThreshold() const {
+ return mSeekTargetThreshold.isSome();
+ }
+
+ virtual MediaDataDecoder::ConversionRequired NeedsConversion() const {
+ return MediaDataDecoder::ConversionRequired::kNeedNone;
+ }
+
+ protected:
+ // IMFTransform wrapper that performs the decoding.
+ RefPtr<MFTDecoder> mDecoder;
+
+ Maybe<media::TimeUnit> mSeekTargetThreshold;
+};
+
+DDLoggedTypeDeclNameAndBase(WMFMediaDataDecoder, MediaDataDecoder);
+
+// Decodes audio and video using Windows Media Foundation. Samples are decoded
+// using the MFTDecoder created by the MFTManager. This class implements
+// the higher-level logic that drives mapping the MFT to the async
+// MediaDataDecoder interface. The specifics of decoding the exact stream
+// type are handled by MFTManager and the MFTDecoder it creates.
+class WMFMediaDataDecoder final
+ : public MediaDataDecoder,
+ public DecoderDoctorLifeLogger<WMFMediaDataDecoder> {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WMFMediaDataDecoder, final);
+
+ explicit WMFMediaDataDecoder(MFTManager* aOutputSource);
+
+ RefPtr<MediaDataDecoder::InitPromise> Init() override;
+
+ RefPtr<DecodePromise> Decode(MediaRawData* aSample) override;
+
+ RefPtr<DecodePromise> Drain() override;
+
+ RefPtr<FlushPromise> Flush() override;
+
+ RefPtr<ShutdownPromise> Shutdown() override;
+
+ bool IsHardwareAccelerated(nsACString& aFailureReason) const override;
+
+ nsCString GetDescriptionName() const override {
+ return mMFTManager ? mMFTManager->GetDescriptionName() : "unknown"_ns;
+ }
+
+ nsCString GetCodecName() const override {
+ return mMFTManager ? mMFTManager->GetCodecName() : ""_ns;
+ }
+
+ ConversionRequired NeedsConversion() const override {
+ MOZ_ASSERT(mMFTManager);
+ return mMFTManager->NeedsConversion();
+ }
+
+ virtual void SetSeekThreshold(const media::TimeUnit& aTime) override;
+
+ private:
+ ~WMFMediaDataDecoder();
+
+ RefPtr<DecodePromise> ProcessError(HRESULT aError, const char* aReason);
+
+ // Called on the task queue. Inserts the sample into the decoder, and
+ // extracts output if available.
+ RefPtr<DecodePromise> ProcessDecode(MediaRawData* aSample);
+
+ // Called on the task queue. Extracts output if available, and delivers
+ // it to the reader. Called after ProcessDecode() and ProcessDrain().
+ HRESULT ProcessOutput(DecodedData& aResults);
+
+ // Called on the task queue. Orders the MFT to flush. There is no output to
+ // extract.
+ RefPtr<FlushPromise> ProcessFlush();
+
+ // Called on the task queue. Orders the MFT to drain, and then extracts
+ // all available output.
+ RefPtr<DecodePromise> ProcessDrain();
+
+ // Checks if `aOutput` should be discarded (guarded against) because its a
+ // potentially invalid output from the decoder. This is done because the
+ // Windows decoder appears to produce invalid outputs under certain
+ // conditions.
+ bool ShouldGuardAgaintIncorrectFirstSample(MediaData* aOutput) const;
+
+ const RefPtr<TaskQueue> mTaskQueue;
+
+ UniquePtr<MFTManager> mMFTManager;
+
+ // The last offset into the media resource that was passed into Input().
+ // This is used to approximate the decoder's position in the media resource.
+ int64_t mLastStreamOffset;
+ Maybe<media::TimeUnit> mLastTime;
+ media::TimeUnit mLastDuration;
+ // Before we get the first sample, this records the times of all samples we
+ // send to the decoder which is used to validate if the first sample is valid.
+ std::set<int64_t> mInputTimesSet;
+ int64_t mSamplesCount = 0;
+ int64_t mOutputsCount = 0;
+
+ bool mIsShutDown = false;
+
+ enum class DrainStatus {
+ DRAINED,
+ DRAINABLE,
+ DRAINING,
+ };
+ DrainStatus mDrainStatus = DrainStatus::DRAINED;
+};
+
+} // namespace mozilla
+
+#endif // WMFMediaDataDecoder_h_
diff --git a/dom/media/platforms/wmf/WMFMediaDataEncoder.h b/dom/media/platforms/wmf/WMFMediaDataEncoder.h
new file mode 100644
index 0000000000..a0cc1dd1a8
--- /dev/null
+++ b/dom/media/platforms/wmf/WMFMediaDataEncoder.h
@@ -0,0 +1,337 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef WMFMediaDataEncoder_h_
+#define WMFMediaDataEncoder_h_
+
+#include "ImageContainer.h"
+#include "MFTEncoder.h"
+#include "PlatformEncoderModule.h"
+#include "TimeUnits.h"
+#include "WMFDataEncoderUtils.h"
+#include "WMFUtils.h"
+
+namespace mozilla {
+
+template <typename ConfigType>
+class WMFMediaDataEncoder final : public MediaDataEncoder {
+ public:
+ WMFMediaDataEncoder(const ConfigType& aConfig, RefPtr<TaskQueue> aTaskQueue,
+ const bool aHardwareNotAllowed)
+ : mConfig(aConfig),
+ mTaskQueue(aTaskQueue),
+ mHardwareNotAllowed(aHardwareNotAllowed) {
+ MOZ_ASSERT(mTaskQueue);
+ }
+
+ RefPtr<InitPromise> Init() override {
+ return InvokeAsync(mTaskQueue, this, __func__,
+ &WMFMediaDataEncoder<ConfigType>::ProcessInit);
+ }
+ RefPtr<EncodePromise> Encode(const MediaData* aSample) override {
+ MOZ_ASSERT(aSample);
+
+ RefPtr<const VideoData> sample(aSample->As<const VideoData>());
+
+ return InvokeAsync<RefPtr<const VideoData>>(
+ mTaskQueue, this, __func__, &WMFMediaDataEncoder::ProcessEncode,
+ std::move(sample));
+ }
+ RefPtr<EncodePromise> Drain() override {
+ return InvokeAsync(
+ mTaskQueue, __func__, [self = RefPtr<WMFMediaDataEncoder>(this)]() {
+ nsTArray<RefPtr<IMFSample>> outputs;
+ return SUCCEEDED(self->mEncoder->Drain(outputs))
+ ? self->ProcessOutputSamples(outputs)
+ : EncodePromise::CreateAndReject(
+ NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
+ });
+ }
+ RefPtr<ShutdownPromise> Shutdown() override {
+ return InvokeAsync(
+ mTaskQueue, __func__, [self = RefPtr<WMFMediaDataEncoder>(this)]() {
+ if (self->mEncoder) {
+ self->mEncoder->Destroy();
+ self->mEncoder = nullptr;
+ }
+ return ShutdownPromise::CreateAndResolve(true, __func__);
+ });
+ }
+ RefPtr<GenericPromise> SetBitrate(Rate aBitsPerSec) override {
+ return InvokeAsync(
+ mTaskQueue, __func__,
+ [self = RefPtr<WMFMediaDataEncoder>(this), aBitsPerSec]() {
+ MOZ_ASSERT(self->mEncoder);
+ return SUCCEEDED(self->mEncoder->SetBitrate(aBitsPerSec))
+ ? GenericPromise::CreateAndResolve(true, __func__)
+ : GenericPromise::CreateAndReject(
+ NS_ERROR_DOM_MEDIA_NOT_SUPPORTED_ERR, __func__);
+ });
+ }
+
+ nsCString GetDescriptionName() const override {
+ return MFTEncoder::GetFriendlyName(CodecToSubtype(mConfig.mCodecType));
+ }
+
+ private:
+ // Automatically lock/unlock IMFMediaBuffer.
+ class LockBuffer final {
+ public:
+ explicit LockBuffer(RefPtr<IMFMediaBuffer>& aBuffer) : mBuffer(aBuffer) {
+ mResult = mBuffer->Lock(&mBytes, &mCapacity, &mLength);
+ }
+
+ ~LockBuffer() {
+ if (SUCCEEDED(mResult)) {
+ mBuffer->Unlock();
+ }
+ }
+
+ BYTE* Data() { return mBytes; }
+ DWORD Capacity() { return mCapacity; }
+ DWORD Length() { return mLength; }
+ HRESULT Result() { return mResult; }
+
+ private:
+ RefPtr<IMFMediaBuffer> mBuffer;
+ BYTE* mBytes;
+ DWORD mCapacity;
+ DWORD mLength;
+ HRESULT mResult;
+ };
+
+ RefPtr<InitPromise> ProcessInit() {
+ AssertOnTaskQueue();
+
+ MOZ_ASSERT(!mEncoder,
+ "Should not initialize encoder again without shutting down");
+
+ if (!wmf::MediaFoundationInitializer::HasInitialized()) {
+ return InitPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Can't create the MFT encoder.")),
+ __func__);
+ }
+
+ RefPtr<MFTEncoder> encoder = new MFTEncoder(mHardwareNotAllowed);
+ HRESULT hr;
+ mscom::EnsureMTA([&]() { hr = InitMFTEncoder(encoder); });
+
+ if (FAILED(hr)) {
+ WMF_ENC_LOGE("init MFTEncoder: error = 0x%lX", hr);
+ return InitPromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Can't create the MFT encoder.")),
+ __func__);
+ }
+
+ mEncoder = std::move(encoder);
+ FillConfigData();
+ return InitPromise::CreateAndResolve(TrackInfo::TrackType::kVideoTrack,
+ __func__);
+ }
+
+ HRESULT InitMFTEncoder(RefPtr<MFTEncoder>& aEncoder) {
+ HRESULT hr = aEncoder->Create(CodecToSubtype(mConfig.mCodecType));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = SetMediaTypes(aEncoder, mConfig);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = aEncoder->SetModes(mConfig.mBitsPerSec);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ return S_OK;
+ }
+
+ void FillConfigData() {
+ nsTArray<UINT8> header;
+ NS_ENSURE_TRUE_VOID(SUCCEEDED(mEncoder->GetMPEGSequenceHeader(header)));
+
+ mConfigData =
+ header.Length() > 0
+ ? ParseH264Parameters(header, mConfig.mUsage == Usage::Realtime)
+ : nullptr;
+ }
+
+ RefPtr<EncodePromise> ProcessEncode(RefPtr<const VideoData>&& aSample) {
+ AssertOnTaskQueue();
+ MOZ_ASSERT(mEncoder);
+ MOZ_ASSERT(aSample);
+
+ RefPtr<IMFSample> nv12 = ConvertToNV12InputSample(std::move(aSample));
+ if (!nv12 || FAILED(mEncoder->PushInput(std::move(nv12)))) {
+ WMF_ENC_LOGE("failed to process input sample");
+ return EncodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Failed to process input.")),
+ __func__);
+ }
+
+ nsTArray<RefPtr<IMFSample>> outputs;
+ HRESULT hr = mEncoder->TakeOutput(outputs);
+ if (hr == MF_E_TRANSFORM_STREAM_CHANGE) {
+ FillConfigData();
+ } else if (FAILED(hr)) {
+ WMF_ENC_LOGE("failed to process output");
+ return EncodePromise::CreateAndReject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Failed to process output.")),
+ __func__);
+ }
+
+ return ProcessOutputSamples(outputs);
+ }
+
+ already_AddRefed<IMFSample> ConvertToNV12InputSample(
+ RefPtr<const VideoData>&& aData) {
+ AssertOnTaskQueue();
+ MOZ_ASSERT(mEncoder);
+
+ const layers::PlanarYCbCrImage* image = aData->mImage->AsPlanarYCbCrImage();
+ MOZ_ASSERT(image);
+ const layers::PlanarYCbCrData* yuv = image->GetData();
+ auto ySize = yuv->YDataSize();
+ auto cbcrSize = yuv->CbCrDataSize();
+ size_t yLength = yuv->mYStride * ySize.height;
+ size_t length = yLength + (yuv->mCbCrStride * cbcrSize.height * 2);
+
+ RefPtr<IMFSample> input;
+ HRESULT hr = mEncoder->CreateInputSample(&input, length);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ RefPtr<IMFMediaBuffer> buffer;
+ hr = input->GetBufferByIndex(0, getter_AddRefs(buffer));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ hr = buffer->SetCurrentLength(length);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ LockBuffer lockBuffer(buffer);
+ NS_ENSURE_TRUE(SUCCEEDED(lockBuffer.Result()), nullptr);
+
+ bool ok = libyuv::I420ToNV12(
+ yuv->mYChannel, yuv->mYStride, yuv->mCbChannel,
+ yuv->mCbCrStride, yuv->mCrChannel, yuv->mCbCrStride,
+ lockBuffer.Data(), yuv->mYStride, lockBuffer.Data() + yLength,
+ yuv->mCbCrStride * 2, ySize.width, ySize.height) == 0;
+ NS_ENSURE_TRUE(ok, nullptr);
+
+ hr = input->SetSampleTime(UsecsToHNs(aData->mTime.ToMicroseconds()));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ hr =
+ input->SetSampleDuration(UsecsToHNs(aData->mDuration.ToMicroseconds()));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ return input.forget();
+ }
+
+ RefPtr<EncodePromise> ProcessOutputSamples(
+ nsTArray<RefPtr<IMFSample>>& aSamples) {
+ EncodedData frames;
+ for (auto sample : aSamples) {
+ RefPtr<MediaRawData> frame = IMFSampleToMediaData(sample);
+ if (frame) {
+ frames.AppendElement(std::move(frame));
+ } else {
+ WMF_ENC_LOGE("failed to convert output frame");
+ }
+ }
+ aSamples.Clear();
+ return EncodePromise::CreateAndResolve(std::move(frames), __func__);
+ }
+
+ already_AddRefed<MediaRawData> IMFSampleToMediaData(
+ RefPtr<IMFSample>& aSample) {
+ AssertOnTaskQueue();
+ MOZ_ASSERT(aSample);
+
+ RefPtr<IMFMediaBuffer> buffer;
+ HRESULT hr = aSample->GetBufferByIndex(0, getter_AddRefs(buffer));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ LockBuffer lockBuffer(buffer);
+ NS_ENSURE_TRUE(SUCCEEDED(lockBuffer.Result()), nullptr);
+
+ LONGLONG time = 0;
+ hr = aSample->GetSampleTime(&time);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ LONGLONG duration = 0;
+ hr = aSample->GetSampleDuration(&duration);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), nullptr);
+
+ bool isKeyframe =
+ MFGetAttributeUINT32(aSample, MFSampleExtension_CleanPoint, false);
+
+ auto frame = MakeRefPtr<MediaRawData>();
+ if (!WriteFrameData(frame, lockBuffer, isKeyframe)) {
+ return nullptr;
+ }
+
+ frame->mTime = media::TimeUnit::FromMicroseconds(HNsToUsecs(time));
+ frame->mDuration = media::TimeUnit::FromMicroseconds(HNsToUsecs(duration));
+ frame->mKeyframe = isKeyframe;
+
+ return frame.forget();
+ }
+
+ bool WriteFrameData(RefPtr<MediaRawData>& aDest, LockBuffer& aSrc,
+ bool aIsKeyframe) {
+ if (std::is_same_v<ConfigType, MediaDataEncoder::H264Config>) {
+ size_t prependLength = 0;
+ RefPtr<MediaByteBuffer> avccHeader;
+ if (aIsKeyframe && mConfigData) {
+ if (mConfig.mUsage == Usage::Realtime) {
+ prependLength = mConfigData->Length();
+ } else {
+ avccHeader = mConfigData;
+ }
+ }
+
+ UniquePtr<MediaRawDataWriter> writer(aDest->CreateWriter());
+ if (!writer->SetSize(prependLength + aSrc.Length())) {
+ WMF_ENC_LOGE("fail to allocate output buffer");
+ return false;
+ }
+
+ if (prependLength > 0) {
+ PodCopy(writer->Data(), mConfigData->Elements(), prependLength);
+ }
+ PodCopy(writer->Data() + prependLength, aSrc.Data(), aSrc.Length());
+
+ if (mConfig.mUsage != Usage::Realtime &&
+ !AnnexB::ConvertSampleToAVCC(aDest, avccHeader)) {
+ WMF_ENC_LOGE("fail to convert annex-b sample to AVCC");
+ return false;
+ }
+
+ return true;
+ }
+ UniquePtr<MediaRawDataWriter> writer(aDest->CreateWriter());
+ if (!writer->SetSize(aSrc.Length())) {
+ WMF_ENC_LOGE("fail to allocate output buffer");
+ return false;
+ }
+
+ PodCopy(writer->Data(), aSrc.Data(), aSrc.Length());
+ return true;
+ }
+
+ void AssertOnTaskQueue() { MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn()); }
+
+ const ConfigType mConfig;
+ const RefPtr<TaskQueue> mTaskQueue;
+ const bool mHardwareNotAllowed;
+ RefPtr<MFTEncoder> mEncoder;
+ // SPS/PPS NALUs for realtime usage, avcC otherwise.
+ RefPtr<MediaByteBuffer> mConfigData;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/platforms/wmf/WMFUtils.cpp b/dom/media/platforms/wmf/WMFUtils.cpp
new file mode 100644
index 0000000000..75888c12c3
--- /dev/null
+++ b/dom/media/platforms/wmf/WMFUtils.cpp
@@ -0,0 +1,632 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WMFUtils.h"
+
+#include <mfidl.h>
+#include <shlobj.h>
+#include <shlwapi.h>
+#include <initguid.h>
+#include <stdint.h>
+
+#ifdef MOZ_AV1
+# include "AOMDecoder.h"
+#endif
+#include "MP4Decoder.h"
+#include "OpusDecoder.h"
+#include "VideoUtils.h"
+#include "VorbisDecoder.h"
+#include "VPXDecoder.h"
+#include "mozilla/ArrayUtils.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/Logging.h"
+#include "mozilla/RefPtr.h"
+#include "nsTArray.h"
+#include "nsThreadUtils.h"
+#include "nsWindowsHelpers.h"
+#include "prenv.h"
+#include "mozilla/mscom/EnsureMTA.h"
+#include "mozilla/WindowsVersion.h"
+
+#ifndef WAVE_FORMAT_OPUS
+# define WAVE_FORMAT_OPUS 0x704F
+#endif
+DEFINE_GUID(MEDIASUBTYPE_OPUS, WAVE_FORMAT_OPUS, 0x000, 0x0010, 0x80, 0x00,
+ 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71);
+
+namespace mozilla {
+
+using media::TimeUnit;
+
+bool StreamTypeIsVideo(const WMFStreamType& aType) {
+ switch (aType) {
+ case WMFStreamType::H264:
+ case WMFStreamType::VP8:
+ case WMFStreamType::VP9:
+ case WMFStreamType::AV1:
+ return true;
+ default:
+ return false;
+ }
+}
+
+bool StreamTypeIsAudio(const WMFStreamType& aType) {
+ switch (aType) {
+ case WMFStreamType::MP3:
+ case WMFStreamType::AAC:
+ case WMFStreamType::OPUS:
+ case WMFStreamType::VORBIS:
+ return true;
+ default:
+ return false;
+ }
+}
+
+// Get a string representation of the stream type. Useful for logging.
+const char* StreamTypeToString(WMFStreamType aStreamType) {
+ switch (aStreamType) {
+ case WMFStreamType::H264:
+ return "H264";
+ case WMFStreamType::VP8:
+ return "VP8";
+ case WMFStreamType::VP9:
+ return "VP9";
+ case WMFStreamType::AV1:
+ return "AV1";
+ case WMFStreamType::MP3:
+ return "MP3";
+ case WMFStreamType::AAC:
+ return "AAC";
+ case WMFStreamType::OPUS:
+ return "OPUS";
+ case WMFStreamType::VORBIS:
+ return "VORBIS";
+ default:
+ MOZ_ASSERT(aStreamType == WMFStreamType::Unknown);
+ return "Unknown";
+ }
+}
+
+WMFStreamType GetStreamTypeFromMimeType(const nsCString& aMimeType) {
+ if (MP4Decoder::IsH264(aMimeType)) {
+ return WMFStreamType::H264;
+ }
+ if (VPXDecoder::IsVP8(aMimeType)) {
+ return WMFStreamType::VP8;
+ }
+ if (VPXDecoder::IsVP9(aMimeType)) {
+ return WMFStreamType::VP9;
+ }
+#ifdef MOZ_AV1
+ if (AOMDecoder::IsAV1(aMimeType)) {
+ return WMFStreamType::AV1;
+ }
+#endif
+ if (aMimeType.EqualsLiteral("audio/mp4a-latm") ||
+ aMimeType.EqualsLiteral("audio/mp4")) {
+ return WMFStreamType::AAC;
+ }
+ if (aMimeType.EqualsLiteral("audio/mpeg")) {
+ return WMFStreamType::MP3;
+ }
+ if (OpusDataDecoder::IsOpus(aMimeType)) {
+ return WMFStreamType::OPUS;
+ }
+ if (VorbisDataDecoder::IsVorbis(aMimeType)) {
+ return WMFStreamType::VORBIS;
+ }
+ return WMFStreamType::Unknown;
+}
+
+HRESULT
+HNsToFrames(int64_t aHNs, uint32_t aRate, int64_t* aOutFrames) {
+ MOZ_ASSERT(aOutFrames);
+ const int64_t HNS_PER_S = USECS_PER_S * 10;
+ CheckedInt<int64_t> i = aHNs;
+ i *= aRate;
+ i /= HNS_PER_S;
+ NS_ENSURE_TRUE(i.isValid(), E_FAIL);
+ *aOutFrames = i.value();
+ return S_OK;
+}
+
+HRESULT
+GetDefaultStride(IMFMediaType* aType, uint32_t aWidth, uint32_t* aOutStride) {
+ // Try to get the default stride from the media type.
+ HRESULT hr = aType->GetUINT32(MF_MT_DEFAULT_STRIDE, aOutStride);
+ if (SUCCEEDED(hr)) {
+ return S_OK;
+ }
+
+ // Stride attribute not set, calculate it.
+ GUID subtype = GUID_NULL;
+
+ hr = aType->GetGUID(MF_MT_SUBTYPE, &subtype);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = wmf::MFGetStrideForBitmapInfoHeader(subtype.Data1, aWidth,
+ (LONG*)(aOutStride));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ return hr;
+}
+
+Maybe<gfx::YUVColorSpace> GetYUVColorSpace(IMFMediaType* aType) {
+ UINT32 yuvColorMatrix;
+ HRESULT hr = aType->GetUINT32(MF_MT_YUV_MATRIX, &yuvColorMatrix);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), {});
+
+ switch (yuvColorMatrix) {
+ case MFVideoTransferMatrix_BT2020_10:
+ case MFVideoTransferMatrix_BT2020_12:
+ return Some(gfx::YUVColorSpace::BT2020);
+ case MFVideoTransferMatrix_BT709:
+ return Some(gfx::YUVColorSpace::BT709);
+ case MFVideoTransferMatrix_BT601:
+ return Some(gfx::YUVColorSpace::BT601);
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unhandled MFVideoTransferMatrix_?");
+ return {};
+ }
+}
+
+int32_t MFOffsetToInt32(const MFOffset& aOffset) {
+ return int32_t(aOffset.value + (aOffset.fract / 65536.0f));
+}
+
+TimeUnit GetSampleDuration(IMFSample* aSample) {
+ NS_ENSURE_TRUE(aSample, TimeUnit::Invalid());
+ int64_t duration = 0;
+ HRESULT hr = aSample->GetSampleDuration(&duration);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), TimeUnit::Invalid());
+ return TimeUnit::FromMicroseconds(HNsToUsecs(duration));
+}
+
+TimeUnit GetSampleTime(IMFSample* aSample) {
+ NS_ENSURE_TRUE(aSample, TimeUnit::Invalid());
+ LONGLONG timestampHns = 0;
+ HRESULT hr = aSample->GetSampleTime(&timestampHns);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), TimeUnit::Invalid());
+ return TimeUnit::FromMicroseconds(HNsToUsecs(timestampHns));
+}
+
+// Gets the sub-region of the video frame that should be displayed.
+// See:
+// http://msdn.microsoft.com/en-us/library/windows/desktop/bb530115(v=vs.85).aspx
+HRESULT
+GetPictureRegion(IMFMediaType* aMediaType, gfx::IntRect& aOutPictureRegion) {
+ // Determine if "pan and scan" is enabled for this media. If it is, we
+ // only display a region of the video frame, not the entire frame.
+ BOOL panScan =
+ MFGetAttributeUINT32(aMediaType, MF_MT_PAN_SCAN_ENABLED, FALSE);
+
+ // If pan and scan mode is enabled. Try to get the display region.
+ HRESULT hr = E_FAIL;
+ MFVideoArea videoArea;
+ memset(&videoArea, 0, sizeof(MFVideoArea));
+ if (panScan) {
+ hr = aMediaType->GetBlob(MF_MT_PAN_SCAN_APERTURE, (UINT8*)&videoArea,
+ sizeof(MFVideoArea), nullptr);
+ }
+
+ // If we're not in pan-and-scan mode, or the pan-and-scan region is not set,
+ // check for a minimimum display aperture.
+ if (!panScan || hr == MF_E_ATTRIBUTENOTFOUND) {
+ hr = aMediaType->GetBlob(MF_MT_MINIMUM_DISPLAY_APERTURE, (UINT8*)&videoArea,
+ sizeof(MFVideoArea), nullptr);
+ }
+
+ if (hr == MF_E_ATTRIBUTENOTFOUND) {
+ // Minimum display aperture is not set, for "backward compatibility with
+ // some components", check for a geometric aperture.
+ hr = aMediaType->GetBlob(MF_MT_GEOMETRIC_APERTURE, (UINT8*)&videoArea,
+ sizeof(MFVideoArea), nullptr);
+ }
+
+ if (SUCCEEDED(hr)) {
+ // The media specified a picture region, return it.
+ aOutPictureRegion = gfx::IntRect(MFOffsetToInt32(videoArea.OffsetX),
+ MFOffsetToInt32(videoArea.OffsetY),
+ videoArea.Area.cx, videoArea.Area.cy);
+ return S_OK;
+ }
+
+ // No picture region defined, fall back to using the entire video area.
+ UINT32 width = 0, height = 0;
+ hr = MFGetAttributeSize(aMediaType, MF_MT_FRAME_SIZE, &width, &height);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ NS_ENSURE_TRUE(width <= MAX_VIDEO_WIDTH, E_FAIL);
+ NS_ENSURE_TRUE(height <= MAX_VIDEO_HEIGHT, E_FAIL);
+
+ aOutPictureRegion = gfx::IntRect(0, 0, width, height);
+ return S_OK;
+}
+
+nsString GetProgramW6432Path() {
+ char* programPath = PR_GetEnvSecure("ProgramW6432");
+ if (!programPath) {
+ programPath = PR_GetEnvSecure("ProgramFiles");
+ }
+
+ if (!programPath) {
+ return u"C:\\Program Files"_ns;
+ }
+ return NS_ConvertUTF8toUTF16(programPath);
+}
+
+const char* MFTMessageTypeToStr(MFT_MESSAGE_TYPE aMsg) {
+ switch (aMsg) {
+ case MFT_MESSAGE_COMMAND_FLUSH:
+ return "MFT_MESSAGE_COMMAND_FLUSH";
+ case MFT_MESSAGE_COMMAND_DRAIN:
+ return "MFT_MESSAGE_COMMAND_DRAIN";
+ case MFT_MESSAGE_COMMAND_MARKER:
+ return "MFT_MESSAGE_COMMAND_MARKER";
+ case MFT_MESSAGE_SET_D3D_MANAGER:
+ return "MFT_MESSAGE_SET_D3D_MANAGER";
+ case MFT_MESSAGE_NOTIFY_BEGIN_STREAMING:
+ return "MFT_MESSAGE_NOTIFY_BEGIN_STREAMING";
+ case MFT_MESSAGE_NOTIFY_END_STREAMING:
+ return "MFT_MESSAGE_NOTIFY_END_STREAMING";
+ case MFT_MESSAGE_NOTIFY_END_OF_STREAM:
+ return "MFT_MESSAGE_NOTIFY_END_OF_STREAM";
+ case MFT_MESSAGE_NOTIFY_START_OF_STREAM:
+ return "MFT_MESSAGE_NOTIFY_START_OF_STREAM";
+ case MFT_MESSAGE_DROP_SAMPLES:
+ return "MFT_MESSAGE_DROP_SAMPLES";
+ case MFT_MESSAGE_COMMAND_TICK:
+ return "MFT_MESSAGE_COMMAND_TICK";
+ case MFT_MESSAGE_NOTIFY_RELEASE_RESOURCES:
+ return "MFT_MESSAGE_NOTIFY_RELEASE_RESOURCES";
+ case MFT_MESSAGE_NOTIFY_REACQUIRE_RESOURCES:
+ return "MFT_MESSAGE_NOTIFY_REACQUIRE_RESOURCES";
+ case MFT_MESSAGE_NOTIFY_EVENT:
+ return "MFT_MESSAGE_NOTIFY_EVENT";
+ case MFT_MESSAGE_COMMAND_SET_OUTPUT_STREAM_STATE:
+ return "MFT_MESSAGE_COMMAND_SET_OUTPUT_STREAM_STATE";
+ case MFT_MESSAGE_COMMAND_FLUSH_OUTPUT_STREAM:
+ return "MFT_MESSAGE_COMMAND_FLUSH_OUTPUT_STREAM";
+ default:
+ return "Invalid message?";
+ }
+}
+
+GUID AudioMimeTypeToMediaFoundationSubtype(const nsACString& aMimeType) {
+ if (aMimeType.EqualsLiteral("audio/mpeg")) {
+ return MFAudioFormat_MP3;
+ } else if (MP4Decoder::IsAAC(aMimeType)) {
+ return MFAudioFormat_AAC;
+ } else if (VorbisDataDecoder::IsVorbis(aMimeType)) {
+ return MFAudioFormat_Vorbis;
+ } else if (OpusDataDecoder::IsOpus(aMimeType)) {
+ return MFAudioFormat_Opus;
+ }
+ NS_WARNING("Unsupport audio mimetype");
+ return GUID_NULL;
+}
+
+GUID VideoMimeTypeToMediaFoundationSubtype(const nsACString& aMimeType) {
+ if (MP4Decoder::IsH264(aMimeType)) {
+ return MFVideoFormat_H264;
+ } else if (VPXDecoder::IsVP8(aMimeType)) {
+ return MFVideoFormat_VP80;
+ } else if (VPXDecoder::IsVP9(aMimeType)) {
+ return MFVideoFormat_VP90;
+ }
+#ifdef MOZ_AV1
+ else if (AOMDecoder::IsAV1(aMimeType)) {
+ return MFVideoFormat_AV1;
+ }
+#endif
+ NS_WARNING("Unsupport video mimetype");
+ return GUID_NULL;
+}
+
+void AACAudioSpecificConfigToUserData(uint8_t aAACProfileLevelIndication,
+ const uint8_t* aAudioSpecConfig,
+ uint32_t aConfigLength,
+ nsTArray<BYTE>& aOutUserData) {
+ MOZ_ASSERT(aOutUserData.IsEmpty());
+
+ // The MF_MT_USER_DATA for AAC is defined here:
+ // http://msdn.microsoft.com/en-us/library/windows/desktop/dd742784%28v=vs.85%29.aspx
+ //
+ // For MFAudioFormat_AAC, MF_MT_USER_DATA contains the portion of
+ // the HEAACWAVEINFO structure that appears after the WAVEFORMATEX
+ // structure (that is, after the wfx member). This is followed by
+ // the AudioSpecificConfig() data, as defined by ISO/IEC 14496-3.
+ // [...]
+ // The length of the AudioSpecificConfig() data is 2 bytes for AAC-LC
+ // or HE-AAC with implicit signaling of SBR/PS. It is more than 2 bytes
+ // for HE-AAC with explicit signaling of SBR/PS.
+ //
+ // The value of audioObjectType as defined in AudioSpecificConfig()
+ // must be 2, indicating AAC-LC. The value of extensionAudioObjectType
+ // must be 5 for SBR or 29 for PS.
+ //
+ // HEAACWAVEINFO structure:
+ // typedef struct heaacwaveinfo_tag {
+ // WAVEFORMATEX wfx;
+ // WORD wPayloadType;
+ // WORD wAudioProfileLevelIndication;
+ // WORD wStructType;
+ // WORD wReserved1;
+ // DWORD dwReserved2;
+ // }
+ const UINT32 heeInfoLen = 4 * sizeof(WORD) + sizeof(DWORD);
+
+ // The HEAACWAVEINFO must have payload and profile set,
+ // the rest can be all 0x00.
+ BYTE heeInfo[heeInfoLen] = {0};
+ WORD* w = (WORD*)heeInfo;
+ w[0] = 0x0; // Payload type raw AAC packet
+ w[1] = aAACProfileLevelIndication;
+
+ aOutUserData.AppendElements(heeInfo, heeInfoLen);
+
+ if (aAACProfileLevelIndication == 2 && aConfigLength > 2) {
+ // The AudioSpecificConfig is TTTTTFFF|FCCCCGGG
+ // (T=ObjectType, F=Frequency, C=Channel, G=GASpecificConfig)
+ // If frequency = 0xf, then the frequency is explicitly defined on 24 bits.
+ int8_t frequency =
+ (aAudioSpecConfig[0] & 0x7) << 1 | (aAudioSpecConfig[1] & 0x80) >> 7;
+ int8_t channels = (aAudioSpecConfig[1] & 0x78) >> 3;
+ int8_t gasc = aAudioSpecConfig[1] & 0x7;
+ if (frequency != 0xf && channels && !gasc) {
+ // We enter this condition if the AudioSpecificConfig should theorically
+ // be 2 bytes long but it's not.
+ // The WMF AAC decoder will error if unknown extensions are found,
+ // so remove them.
+ aConfigLength = 2;
+ }
+ }
+ aOutUserData.AppendElements(aAudioSpecConfig, aConfigLength);
+}
+
+namespace wmf {
+
+static const wchar_t* sDLLs[] = {
+ L"mfplat.dll",
+ L"mf.dll",
+ L"dxva2.dll",
+ L"evr.dll",
+};
+
+HRESULT
+LoadDLLs() {
+ static bool sDLLsLoaded = false;
+ static bool sFailedToLoadDlls = false;
+
+ if (sDLLsLoaded) {
+ return S_OK;
+ }
+ if (sFailedToLoadDlls) {
+ return E_FAIL;
+ }
+
+ // Try to load all the required DLLs. If we fail to load any dll,
+ // unload the dlls we succeeded in loading.
+ nsTArray<const wchar_t*> loadedDlls;
+ for (const wchar_t* dll : sDLLs) {
+ if (!LoadLibrarySystem32(dll)) {
+ NS_WARNING("Failed to load WMF DLLs");
+ for (const wchar_t* loadedDll : loadedDlls) {
+ FreeLibrary(GetModuleHandleW(loadedDll));
+ }
+ sFailedToLoadDlls = true;
+ return E_FAIL;
+ }
+ loadedDlls.AppendElement(dll);
+ }
+ sDLLsLoaded = true;
+
+ return S_OK;
+}
+
+#define ENSURE_FUNCTION_PTR_HELPER(FunctionType, FunctionName, DLL) \
+ static FunctionType FunctionName##Ptr = nullptr; \
+ if (!FunctionName##Ptr) { \
+ FunctionName##Ptr = (FunctionType)GetProcAddress( \
+ GetModuleHandleW(L## #DLL), #FunctionName); \
+ if (!FunctionName##Ptr) { \
+ NS_WARNING("Failed to get GetProcAddress of " #FunctionName \
+ " from " #DLL); \
+ return E_FAIL; \
+ } \
+ }
+
+#define ENSURE_FUNCTION_PTR(FunctionName, DLL) \
+ ENSURE_FUNCTION_PTR_HELPER(decltype(::FunctionName)*, FunctionName, DLL)
+
+#define ENSURE_FUNCTION_PTR_(FunctionName, DLL) \
+ ENSURE_FUNCTION_PTR_HELPER(FunctionName##Ptr_t, FunctionName, DLL)
+
+#define DECL_FUNCTION_PTR(FunctionName, ...) \
+ typedef HRESULT(STDMETHODCALLTYPE* FunctionName##Ptr_t)(__VA_ARGS__)
+
+HRESULT
+MediaFoundationInitializer::MFStartup() {
+ if (IsWin7AndPre2000Compatible()) {
+ /*
+ * Specific exclude the usage of WMF on Win 7 with compatibility mode
+ * prior to Win 2000 as we may crash while trying to startup WMF.
+ * Using GetVersionEx API which takes compatibility mode into account.
+ * See Bug 1279171.
+ */
+ return E_FAIL;
+ }
+
+ HRESULT hr = LoadDLLs();
+ if (FAILED(hr)) {
+ return hr;
+ }
+
+ const int MF_WIN7_VERSION = (0x0002 << 16 | MF_API_VERSION);
+
+ // decltype is unusable for functions having default parameters
+ DECL_FUNCTION_PTR(MFStartup, ULONG, DWORD);
+ ENSURE_FUNCTION_PTR_(MFStartup, Mfplat.dll)
+
+ hr = E_FAIL;
+ mozilla::mscom::EnsureMTA(
+ [&]() -> void { hr = MFStartupPtr(MF_WIN7_VERSION, MFSTARTUP_FULL); });
+ return hr;
+}
+
+HRESULT
+MediaFoundationInitializer::MFShutdown() {
+ ENSURE_FUNCTION_PTR(MFShutdown, Mfplat.dll)
+ HRESULT hr = E_FAIL;
+ mozilla::mscom::EnsureMTA([&]() -> void { hr = (MFShutdownPtr)(); });
+ return hr;
+}
+
+HRESULT
+MFCreateMediaType(IMFMediaType** aOutMFType) {
+ ENSURE_FUNCTION_PTR(MFCreateMediaType, Mfplat.dll)
+ return (MFCreateMediaTypePtr)(aOutMFType);
+}
+
+HRESULT
+MFGetStrideForBitmapInfoHeader(DWORD aFormat, DWORD aWidth, LONG* aOutStride) {
+ ENSURE_FUNCTION_PTR(MFGetStrideForBitmapInfoHeader, evr.dll)
+ return (MFGetStrideForBitmapInfoHeaderPtr)(aFormat, aWidth, aOutStride);
+}
+
+HRESULT MFGetService(IUnknown* punkObject, REFGUID guidService, REFIID riid,
+ LPVOID* ppvObject) {
+ ENSURE_FUNCTION_PTR(MFGetService, mf.dll)
+ return (MFGetServicePtr)(punkObject, guidService, riid, ppvObject);
+}
+
+HRESULT
+DXVA2CreateDirect3DDeviceManager9(UINT* pResetToken,
+ IDirect3DDeviceManager9** ppDXVAManager) {
+ ENSURE_FUNCTION_PTR(DXVA2CreateDirect3DDeviceManager9, dxva2.dll)
+ return (DXVA2CreateDirect3DDeviceManager9Ptr)(pResetToken, ppDXVAManager);
+}
+
+HRESULT
+MFCreateSample(IMFSample** ppIMFSample) {
+ ENSURE_FUNCTION_PTR(MFCreateSample, mfplat.dll)
+ return (MFCreateSamplePtr)(ppIMFSample);
+}
+
+HRESULT
+MFCreateAlignedMemoryBuffer(DWORD cbMaxLength, DWORD fAlignmentFlags,
+ IMFMediaBuffer** ppBuffer) {
+ ENSURE_FUNCTION_PTR(MFCreateAlignedMemoryBuffer, mfplat.dll)
+ return (MFCreateAlignedMemoryBufferPtr)(cbMaxLength, fAlignmentFlags,
+ ppBuffer);
+}
+
+HRESULT
+MFCreateDXGIDeviceManager(UINT* pResetToken,
+ IMFDXGIDeviceManager** ppDXVAManager) {
+ ENSURE_FUNCTION_PTR(MFCreateDXGIDeviceManager, mfplat.dll)
+ return (MFCreateDXGIDeviceManagerPtr)(pResetToken, ppDXVAManager);
+}
+
+HRESULT
+MFCreateDXGISurfaceBuffer(REFIID riid, IUnknown* punkSurface,
+ UINT uSubresourceIndex, BOOL fButtomUpWhenLinear,
+ IMFMediaBuffer** ppBuffer) {
+ ENSURE_FUNCTION_PTR(MFCreateDXGISurfaceBuffer, mfplat.dll)
+ return (MFCreateDXGISurfaceBufferPtr)(riid, punkSurface, uSubresourceIndex,
+ fButtomUpWhenLinear, ppBuffer);
+}
+
+HRESULT
+MFTEnumEx(GUID guidCategory, UINT32 Flags,
+ const MFT_REGISTER_TYPE_INFO* pInputType,
+ const MFT_REGISTER_TYPE_INFO* pOutputType,
+ IMFActivate*** pppMFTActivate, UINT32* pnumMFTActivate) {
+ ENSURE_FUNCTION_PTR(MFTEnumEx, mfplat.dll)
+ return (MFTEnumExPtr)(guidCategory, Flags, pInputType, pOutputType,
+ pppMFTActivate, pnumMFTActivate);
+}
+
+HRESULT MFTGetInfo(CLSID clsidMFT, LPWSTR* pszName,
+ MFT_REGISTER_TYPE_INFO** ppInputTypes, UINT32* pcInputTypes,
+ MFT_REGISTER_TYPE_INFO** ppOutputTypes,
+ UINT32* pcOutputTypes, IMFAttributes** ppAttributes) {
+ ENSURE_FUNCTION_PTR(MFTGetInfo, mfplat.dll)
+ return (MFTGetInfoPtr)(clsidMFT, pszName, ppInputTypes, pcInputTypes,
+ ppOutputTypes, pcOutputTypes, ppAttributes);
+}
+
+HRESULT
+MFCreateAttributes(IMFAttributes** ppMFAttributes, UINT32 cInitialSize) {
+ ENSURE_FUNCTION_PTR(MFCreateAttributes, mfplat.dll)
+ return (MFCreateAttributesPtr)(ppMFAttributes, cInitialSize);
+}
+
+HRESULT MFCreateEventQueue(IMFMediaEventQueue** ppMediaEventQueue) {
+ ENSURE_FUNCTION_PTR(MFCreateEventQueue, mfplat.dll)
+ return (MFCreateEventQueuePtr)(ppMediaEventQueue);
+}
+
+HRESULT MFCreateStreamDescriptor(DWORD dwStreamIdentifier, DWORD cMediaTypes,
+ IMFMediaType** apMediaTypes,
+ IMFStreamDescriptor** ppDescriptor) {
+ ENSURE_FUNCTION_PTR(MFCreateStreamDescriptor, mfplat.dll)
+ return (MFCreateStreamDescriptorPtr)(dwStreamIdentifier, cMediaTypes,
+ apMediaTypes, ppDescriptor);
+}
+
+HRESULT MFCreateAsyncResult(IUnknown* punkObject, IMFAsyncCallback* pCallback,
+ IUnknown* punkState,
+ IMFAsyncResult** ppAsyncResult) {
+ ENSURE_FUNCTION_PTR(MFCreateAsyncResult, mfplat.dll)
+ return (MFCreateAsyncResultPtr)(punkObject, pCallback, punkState,
+ ppAsyncResult);
+}
+
+HRESULT MFCreatePresentationDescriptor(
+ DWORD cStreamDescriptors, IMFStreamDescriptor** apStreamDescriptors,
+ IMFPresentationDescriptor** ppPresentationDescriptor) {
+ ENSURE_FUNCTION_PTR(MFCreatePresentationDescriptor, mfplat.dll)
+ return (MFCreatePresentationDescriptorPtr)(cStreamDescriptors,
+ apStreamDescriptors,
+ ppPresentationDescriptor);
+}
+
+HRESULT MFCreateMemoryBuffer(DWORD cbMaxLength, IMFMediaBuffer** ppBuffer) {
+ ENSURE_FUNCTION_PTR(MFCreateMemoryBuffer, mfplat.dll);
+ return (MFCreateMemoryBufferPtr)(cbMaxLength, ppBuffer);
+}
+
+HRESULT MFLockDXGIDeviceManager(UINT* pResetToken,
+ IMFDXGIDeviceManager** ppManager) {
+ ENSURE_FUNCTION_PTR(MFLockDXGIDeviceManager, mfplat.dll);
+ return (MFLockDXGIDeviceManagerPtr)(pResetToken, ppManager);
+}
+
+HRESULT MFUnlockDXGIDeviceManager() {
+ ENSURE_FUNCTION_PTR(MFUnlockDXGIDeviceManager, mfplat.dll);
+ return (MFUnlockDXGIDeviceManagerPtr)();
+}
+
+HRESULT MFPutWorkItem(DWORD dwQueue, IMFAsyncCallback* pCallback,
+ IUnknown* pState) {
+ ENSURE_FUNCTION_PTR(MFPutWorkItem, mfplat.dll);
+ return (MFPutWorkItemPtr)(dwQueue, pCallback, pState);
+}
+
+HRESULT MFSerializeAttributesToStream(IMFAttributes* pAttr, DWORD dwOptions,
+ IStream* pStm) {
+ ENSURE_FUNCTION_PTR(MFSerializeAttributesToStream, mfplat.dll);
+ return (MFSerializeAttributesToStreamPtr)(pAttr, dwOptions, pStm);
+}
+
+HRESULT MFWrapMediaType(IMFMediaType* pOrig, REFGUID MajorType, REFGUID SubType,
+ IMFMediaType** ppWrap) {
+ ENSURE_FUNCTION_PTR(MFWrapMediaType, mfplat.dll);
+ return (MFWrapMediaTypePtr)(pOrig, MajorType, SubType, ppWrap);
+}
+
+} // end namespace wmf
+} // end namespace mozilla
diff --git a/dom/media/platforms/wmf/WMFUtils.h b/dom/media/platforms/wmf/WMFUtils.h
new file mode 100644
index 0000000000..316c3dd78c
--- /dev/null
+++ b/dom/media/platforms/wmf/WMFUtils.h
@@ -0,0 +1,104 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef WMFUtils_h
+#define WMFUtils_h
+
+#include "ImageTypes.h"
+#include "TimeUnits.h"
+#include "VideoUtils.h"
+#include "WMF.h"
+#include "mozilla/gfx/Rect.h"
+#include "nsString.h"
+
+// Various utilities shared by WMF backend files.
+
+namespace mozilla {
+
+static const GUID CLSID_MSOpusDecoder = {
+ 0x63e17c10,
+ 0x2d43,
+ 0x4c42,
+ {0x8f, 0xe3, 0x8d, 0x8b, 0x63, 0xe4, 0x6a, 0x6a}};
+
+// Media types supported by Media Foundation.
+enum class WMFStreamType {
+ Unknown,
+ H264,
+ VP8,
+ VP9,
+ AV1,
+ MP3,
+ AAC,
+ OPUS,
+ VORBIS,
+ SENTINEL
+};
+
+bool StreamTypeIsVideo(const WMFStreamType& aType);
+
+bool StreamTypeIsAudio(const WMFStreamType& aType);
+
+// Get a string representation of the stream type. Useful for logging.
+const char* StreamTypeToString(WMFStreamType aStreamType);
+
+WMFStreamType GetStreamTypeFromMimeType(const nsCString& aMimeType);
+
+// Converts from microseconds to hundreds of nanoseconds.
+// We use microseconds for our timestamps, whereas WMF uses
+// hundreds of nanoseconds.
+inline int64_t UsecsToHNs(int64_t aUsecs) { return aUsecs * 10; }
+
+// Converts from hundreds of nanoseconds to microseconds.
+// We use microseconds for our timestamps, whereas WMF uses
+// hundreds of nanoseconds.
+inline int64_t HNsToUsecs(int64_t hNanoSecs) { return hNanoSecs / 10; }
+
+HRESULT HNsToFrames(int64_t aHNs, uint32_t aRate, int64_t* aOutFrames);
+
+HRESULT
+GetDefaultStride(IMFMediaType* aType, uint32_t aWidth, uint32_t* aOutStride);
+
+Maybe<gfx::YUVColorSpace> GetYUVColorSpace(IMFMediaType* aType);
+
+int32_t MFOffsetToInt32(const MFOffset& aOffset);
+
+// Gets the sub-region of the video frame that should be displayed.
+// See:
+// http://msdn.microsoft.com/en-us/library/windows/desktop/bb530115(v=vs.85).aspx
+HRESULT
+GetPictureRegion(IMFMediaType* aMediaType, gfx::IntRect& aOutPictureRegion);
+
+// Returns the duration of a IMFSample in TimeUnit.
+// Returns media::TimeUnit::Invalid() on failure.
+media::TimeUnit GetSampleDuration(IMFSample* aSample);
+
+// Returns the presentation time of a IMFSample in TimeUnit.
+// Returns media::TimeUnit::Invalid() on failure.
+media::TimeUnit GetSampleTime(IMFSample* aSample);
+
+inline bool IsFlagSet(DWORD flags, DWORD pattern) {
+ return (flags & pattern) == pattern;
+}
+
+// Will return %ProgramW6432% value as per:
+// https://msdn.microsoft.com/library/windows/desktop/aa384274.aspx
+nsString GetProgramW6432Path();
+
+const char* MFTMessageTypeToStr(MFT_MESSAGE_TYPE aMsg);
+
+GUID AudioMimeTypeToMediaFoundationSubtype(const nsACString& aMimeType);
+
+GUID VideoMimeTypeToMediaFoundationSubtype(const nsACString& aMimeType);
+
+void AACAudioSpecificConfigToUserData(uint8_t aAACProfileLevelIndication,
+ const uint8_t* aAudioSpecConfig,
+ uint32_t aConfigLength,
+ nsTArray<BYTE>& aOutUserData);
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/platforms/wmf/WMFVideoMFTManager.cpp b/dom/media/platforms/wmf/WMFVideoMFTManager.cpp
new file mode 100644
index 0000000000..79cfd1cc0b
--- /dev/null
+++ b/dom/media/platforms/wmf/WMFVideoMFTManager.cpp
@@ -0,0 +1,1096 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WMFVideoMFTManager.h"
+
+#include <psapi.h>
+#include <winsdkver.h>
+#include <algorithm>
+#include "DXVA2Manager.h"
+#include "GMPUtils.h" // For SplitAt. TODO: Move SplitAt to a central place.
+#include "IMFYCbCrImage.h"
+#include "ImageContainer.h"
+#include "MediaInfo.h"
+#include "MediaTelemetryConstants.h"
+#include "VideoUtils.h"
+#include "WMFDecoderModule.h"
+#include "WMFUtils.h"
+#include "gfx2DGlue.h"
+#include "gfxWindowsPlatform.h"
+#include "mozilla/AbstractThread.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Logging.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/StaticPrefs_gfx.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/SyncRunnable.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/WindowsVersion.h"
+#include "mozilla/gfx/DeviceManagerDx.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/layers/LayersTypes.h"
+#include "nsPrintfCString.h"
+#include "nsThreadUtils.h"
+#include "nsWindowsHelpers.h"
+
+#define LOG(...) MOZ_LOG(sPDMLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
+
+using mozilla::layers::Image;
+using mozilla::layers::IMFYCbCrImage;
+using mozilla::layers::LayerManager;
+using mozilla::layers::LayersBackend;
+using mozilla::media::TimeUnit;
+
+#if WINVER_MAXVER < 0x0A00
+// Windows 10+ SDK has VP80 and VP90 defines
+const GUID MFVideoFormat_VP80 = {
+ 0x30385056,
+ 0x0000,
+ 0x0010,
+ {0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}};
+
+const GUID MFVideoFormat_VP90 = {
+ 0x30395056,
+ 0x0000,
+ 0x0010,
+ {0x80, 0x00, 0x00, 0xaa, 0x00, 0x38, 0x9b, 0x71}};
+#endif
+
+#if !defined(__MINGW32__) && _WIN32_WINNT < _WIN32_WINNT_WIN8
+const GUID MF_SA_MINIMUM_OUTPUT_SAMPLE_COUNT = {
+ 0x851745d5,
+ 0xc3d6,
+ 0x476d,
+ {0x95, 0x27, 0x49, 0x8e, 0xf2, 0xd1, 0xd, 0x18}};
+const GUID MF_SA_MINIMUM_OUTPUT_SAMPLE_COUNT_PROGRESSIVE = {
+ 0xf5523a5,
+ 0x1cb2,
+ 0x47c5,
+ {0xa5, 0x50, 0x2e, 0xeb, 0x84, 0xb4, 0xd1, 0x4a}};
+const GUID MF_SA_D3D11_BINDFLAGS = {
+ 0xeacf97ad,
+ 0x065c,
+ 0x4408,
+ {0xbe, 0xe3, 0xfd, 0xcb, 0xfd, 0x12, 0x8b, 0xe2}};
+const GUID MF_SA_D3D11_SHARED_WITHOUT_MUTEX = {
+ 0x39dbd44d,
+ 0x2e44,
+ 0x4931,
+ {0xa4, 0xc8, 0x35, 0x2d, 0x3d, 0xc4, 0x21, 0x15}};
+#endif
+
+namespace mozilla {
+
+static bool IsWin7H264Decoder4KCapable() {
+ WCHAR systemPath[MAX_PATH + 1];
+ if (!ConstructSystem32Path(L"msmpeg2vdec.dll", systemPath, MAX_PATH + 1)) {
+ // Cannot build path -> Assume it's the old DLL or it's missing.
+ return false;
+ }
+
+ DWORD zero;
+ DWORD infoSize = GetFileVersionInfoSizeW(systemPath, &zero);
+ if (infoSize == 0) {
+ // Can't get file info -> Assume it's the old DLL or it's missing.
+ return false;
+ }
+ auto infoData = MakeUnique<unsigned char[]>(infoSize);
+ VS_FIXEDFILEINFO* vInfo;
+ UINT vInfoLen;
+ if (GetFileVersionInfoW(systemPath, 0, infoSize, infoData.get()) &&
+ VerQueryValueW(infoData.get(), L"\\", (LPVOID*)&vInfo, &vInfoLen)) {
+ uint64_t version = uint64_t(vInfo->dwFileVersionMS) << 32 |
+ uint64_t(vInfo->dwFileVersionLS);
+ // 12.0.9200.16426 & later allow for >1920x1088 resolutions.
+ const uint64_t minimum =
+ (uint64_t(12) << 48) | (uint64_t(9200) << 16) | uint64_t(16426);
+ return version >= minimum;
+ }
+ // Can't get file version -> Assume it's the old DLL.
+ return false;
+}
+
+LayersBackend GetCompositorBackendType(
+ layers::KnowsCompositor* aKnowsCompositor) {
+ if (aKnowsCompositor) {
+ return aKnowsCompositor->GetCompositorBackendType();
+ }
+ return LayersBackend::LAYERS_NONE;
+}
+
+WMFVideoMFTManager::WMFVideoMFTManager(
+ const VideoInfo& aConfig, layers::KnowsCompositor* aKnowsCompositor,
+ layers::ImageContainer* aImageContainer, float aFramerate,
+ const CreateDecoderParams::OptionSet& aOptions, bool aDXVAEnabled,
+ Maybe<TrackingId> aTrackingId)
+ : mVideoInfo(aConfig),
+ mImageSize(aConfig.mImage),
+ mStreamType(GetStreamTypeFromMimeType(aConfig.mMimeType)),
+ mSoftwareImageSize(aConfig.mImage),
+ mSoftwarePictureSize(aConfig.mImage),
+ mVideoStride(0),
+ mColorSpace(aConfig.mColorSpace),
+ mColorRange(aConfig.mColorRange),
+ mImageContainer(aImageContainer),
+ mKnowsCompositor(aKnowsCompositor),
+ mDXVAEnabled(aDXVAEnabled &&
+ !aOptions.contains(
+ CreateDecoderParams::Option::HardwareDecoderNotAllowed)),
+ mZeroCopyNV12Texture(false),
+ mFramerate(aFramerate),
+ mLowLatency(aOptions.contains(CreateDecoderParams::Option::LowLatency)),
+ mTrackingId(std::move(aTrackingId))
+// mVideoStride, mVideoWidth, mVideoHeight, mUseHwAccel are initialized in
+// Init().
+{
+ MOZ_COUNT_CTOR(WMFVideoMFTManager);
+
+ // The V and U planes are stored 16-row-aligned, so we need to add padding
+ // to the row heights to ensure the Y'CbCr planes are referenced properly.
+ // This value is only used with software decoder.
+ if (mSoftwareImageSize.height % 16 != 0) {
+ mSoftwareImageSize.height += 16 - (mSoftwareImageSize.height % 16);
+ }
+}
+
+WMFVideoMFTManager::~WMFVideoMFTManager() {
+ MOZ_COUNT_DTOR(WMFVideoMFTManager);
+}
+
+/* static */
+const GUID& WMFVideoMFTManager::GetMediaSubtypeGUID() {
+ MOZ_ASSERT(StreamTypeIsVideo(mStreamType));
+ switch (mStreamType) {
+ case WMFStreamType::H264:
+ return MFVideoFormat_H264;
+ case WMFStreamType::VP8:
+ return MFVideoFormat_VP80;
+ case WMFStreamType::VP9:
+ return MFVideoFormat_VP90;
+ case WMFStreamType::AV1:
+ return MFVideoFormat_AV1;
+ default:
+ return GUID_NULL;
+ };
+}
+
+bool WMFVideoMFTManager::InitializeDXVA() {
+ // If we use DXVA but aren't running with a D3D layer manager then the
+ // readback of decoded video frames from GPU to CPU memory grinds painting
+ // to a halt, and makes playback performance *worse*.
+ if (!mDXVAEnabled) {
+ mDXVAFailureReason.AssignLiteral(
+ "Hardware video decoding disabled or blacklisted");
+ return false;
+ }
+ MOZ_ASSERT(!mDXVA2Manager);
+ if (!mKnowsCompositor || !mKnowsCompositor->SupportsD3D11()) {
+ mDXVAFailureReason.AssignLiteral("Unsupported layers backend");
+ return false;
+ }
+
+ if (!XRE_IsRDDProcess() && !XRE_IsGPUProcess()) {
+ mDXVAFailureReason.AssignLiteral(
+ "DXVA only supported in RDD or GPU process");
+ return false;
+ }
+
+ bool d3d11 = true;
+ if (!StaticPrefs::media_wmf_dxva_d3d11_enabled()) {
+ mDXVAFailureReason = nsPrintfCString(
+ "D3D11: %s is false",
+ StaticPrefs::GetPrefName_media_wmf_dxva_d3d11_enabled());
+ d3d11 = false;
+ }
+ if (!IsWin8OrLater()) {
+ mDXVAFailureReason.AssignLiteral("D3D11: Requires Windows 8 or later");
+ d3d11 = false;
+ }
+
+ if (d3d11) {
+ mDXVAFailureReason.AppendLiteral("D3D11: ");
+ mDXVA2Manager.reset(
+ DXVA2Manager::CreateD3D11DXVA(mKnowsCompositor, mDXVAFailureReason));
+ if (mDXVA2Manager) {
+ return true;
+ }
+ }
+
+ // Try again with d3d9, but record the failure reason
+ // into a new var to avoid overwriting the d3d11 failure.
+ nsAutoCString d3d9Failure;
+ mDXVA2Manager.reset(
+ DXVA2Manager::CreateD3D9DXVA(mKnowsCompositor, d3d9Failure));
+ // Make sure we include the messages from both attempts (if applicable).
+ if (!d3d9Failure.IsEmpty()) {
+ mDXVAFailureReason.AppendLiteral("; D3D9: ");
+ mDXVAFailureReason.Append(d3d9Failure);
+ }
+
+ return mDXVA2Manager != nullptr;
+}
+
+MediaResult WMFVideoMFTManager::ValidateVideoInfo() {
+ NS_ENSURE_TRUE(StreamTypeIsVideo(mStreamType),
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Invalid stream type")));
+ switch (mStreamType) {
+ case WMFStreamType::H264:
+ if (!StaticPrefs::media_wmf_allow_unsupported_resolutions()) {
+ // The WMF H.264 decoder is documented to have a minimum resolution
+ // 48x48 pixels for resolution, but we won't enable hw decoding for the
+ // resolution < 132 pixels. It's assumed the software decoder doesn't
+ // have this limitation, but it still might have maximum resolution
+ // limitation.
+ // https://msdn.microsoft.com/en-us/library/windows/desktop/dd797815(v=vs.85).aspx
+ const bool Is4KCapable =
+ IsWin8OrLater() || IsWin7H264Decoder4KCapable();
+ static const int32_t MAX_H264_PIXEL_COUNT =
+ Is4KCapable ? 4096 * 2304 : 1920 * 1088;
+ const CheckedInt32 pixelCount =
+ CheckedInt32(mVideoInfo.mImage.width) * mVideoInfo.mImage.height;
+
+ if (!pixelCount.isValid() ||
+ pixelCount.value() > MAX_H264_PIXEL_COUNT) {
+ mIsValid = false;
+ return MediaResult(
+ NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Can't decode H.264 stream because its "
+ "resolution is out of the maximum limitation"));
+ }
+ }
+ break;
+ default:
+ break;
+ }
+
+ return NS_OK;
+}
+
+MediaResult WMFVideoMFTManager::Init() {
+ MediaResult result = ValidateVideoInfo();
+ if (NS_FAILED(result)) {
+ return result;
+ }
+
+ result = InitInternal();
+ if (NS_SUCCEEDED(result) && mDXVA2Manager) {
+ // If we had some failures but eventually made it work,
+ // make sure we preserve the messages.
+ if (mDXVA2Manager->IsD3D11()) {
+ mDXVAFailureReason.AppendLiteral("Using D3D11 API");
+ } else {
+ mDXVAFailureReason.AppendLiteral("Using D3D9 API");
+ }
+ }
+
+ return result;
+}
+
+MediaResult WMFVideoMFTManager::InitInternal() {
+ // The H264 SanityTest uses a 132x132 videos to determine if DXVA can be used.
+ // so we want to use the software decoder for videos with lower resolutions.
+ static const int MIN_H264_HW_WIDTH = 132;
+ static const int MIN_H264_HW_HEIGHT = 132;
+
+ mUseHwAccel = false; // default value; changed if D3D setup succeeds.
+ bool useDxva = true;
+
+ if (mStreamType == WMFStreamType::H264 &&
+ (mVideoInfo.ImageRect().width <= MIN_H264_HW_WIDTH ||
+ mVideoInfo.ImageRect().height <= MIN_H264_HW_HEIGHT)) {
+ useDxva = false;
+ mDXVAFailureReason = nsPrintfCString(
+ "H264 video resolution too low: %" PRIu32 "x%" PRIu32,
+ mVideoInfo.ImageRect().width, mVideoInfo.ImageRect().height);
+ }
+
+ if (useDxva) {
+ useDxva = InitializeDXVA();
+ }
+
+ RefPtr<MFTDecoder> decoder = new MFTDecoder();
+ HRESULT hr = WMFDecoderModule::CreateMFTDecoder(mStreamType, decoder);
+ NS_ENSURE_TRUE(SUCCEEDED(hr),
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Can't create the MFT decoder.")));
+
+ RefPtr<IMFAttributes> attr(decoder->GetAttributes());
+ UINT32 aware = 0;
+ if (attr) {
+ attr->GetUINT32(MF_SA_D3D_AWARE, &aware);
+ attr->SetUINT32(CODECAPI_AVDecNumWorkerThreads,
+ WMFDecoderModule::GetNumDecoderThreads());
+ bool lowLatency =
+ (StaticPrefs::media_wmf_low_latency_enabled() || IsWin10OrLater()) &&
+ !StaticPrefs::media_wmf_low_latency_force_disabled();
+ if (mLowLatency || lowLatency) {
+ hr = attr->SetUINT32(CODECAPI_AVLowLatencyMode, TRUE);
+ if (SUCCEEDED(hr)) {
+ LOG("Enabling Low Latency Mode");
+ } else {
+ LOG("Couldn't enable Low Latency Mode");
+ }
+ }
+
+ if (gfx::gfxVars::HwDecodedVideoZeroCopy() && mKnowsCompositor &&
+ mKnowsCompositor->UsingHardwareWebRender() && mDXVA2Manager &&
+ mDXVA2Manager->SupportsZeroCopyNV12Texture()) {
+ mZeroCopyNV12Texture = true;
+ const int kOutputBufferSize = 10;
+
+ // Each picture buffer can store a sample, plus one in
+ // pending_output_samples_. The decoder adds this number to the number of
+ // reference pictures it expects to need and uses that to determine the
+ // array size of the output texture.
+ const int kMaxOutputSamples = kOutputBufferSize + 1;
+ attr->SetUINT32(MF_SA_MINIMUM_OUTPUT_SAMPLE_COUNT_PROGRESSIVE,
+ kMaxOutputSamples);
+ attr->SetUINT32(MF_SA_MINIMUM_OUTPUT_SAMPLE_COUNT, kMaxOutputSamples);
+ }
+ }
+
+ if (useDxva) {
+ if (aware) {
+ // TODO: Test if I need this anywhere... Maybe on Vista?
+ // hr = attr->SetUINT32(CODECAPI_AVDecVideoAcceleration_H264, TRUE);
+ // NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ MOZ_ASSERT(mDXVA2Manager);
+ ULONG_PTR manager = ULONG_PTR(mDXVA2Manager->GetDXVADeviceManager());
+ hr = decoder->SendMFTMessage(MFT_MESSAGE_SET_D3D_MANAGER, manager);
+ if (SUCCEEDED(hr)) {
+ mUseHwAccel = true;
+ } else {
+ mDXVAFailureReason = nsPrintfCString(
+ "MFT_MESSAGE_SET_D3D_MANAGER failed with code %lX", hr);
+ }
+ } else {
+ mDXVAFailureReason.AssignLiteral(
+ "Decoder returned false for MF_SA_D3D_AWARE");
+ }
+ }
+
+ if (!mDXVAFailureReason.IsEmpty()) {
+ // DXVA failure reason being set can mean that D3D11 failed, or that DXVA is
+ // entirely disabled.
+ LOG("DXVA failure: %s", mDXVAFailureReason.get());
+ }
+
+ if (!mUseHwAccel) {
+ if (mDXVA2Manager) {
+ // Either mDXVAEnabled was set to false prior the second call to
+ // InitInternal() due to CanUseDXVA() returning false, or
+ // MFT_MESSAGE_SET_D3D_MANAGER failed
+ mDXVA2Manager.reset();
+ }
+ if (mStreamType == WMFStreamType::VP9 ||
+ mStreamType == WMFStreamType::VP8 ||
+ mStreamType == WMFStreamType::AV1) {
+ return MediaResult(
+ NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Use VP8/VP9/AV1 MFT only if HW acceleration "
+ "is available."));
+ }
+ Telemetry::Accumulate(Telemetry::MEDIA_DECODER_BACKEND_USED,
+ uint32_t(media::MediaDecoderBackend::WMFSoftware));
+ }
+
+ mDecoder = decoder;
+ hr = SetDecoderMediaTypes();
+ NS_ENSURE_TRUE(
+ SUCCEEDED(hr),
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Fail to set the decoder media types.")));
+
+ RefPtr<IMFMediaType> inputType;
+ hr = mDecoder->GetInputMediaType(inputType);
+ NS_ENSURE_TRUE(
+ SUCCEEDED(hr),
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Fail to get the input media type.")));
+
+ RefPtr<IMFMediaType> outputType;
+ hr = mDecoder->GetOutputMediaType(outputType);
+ NS_ENSURE_TRUE(
+ SUCCEEDED(hr),
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Fail to get the output media type.")));
+
+ if (mUseHwAccel && !CanUseDXVA(inputType, outputType)) {
+ LOG("DXVA manager determined that the input type was unsupported in "
+ "hardware, retrying init without DXVA.");
+ mDXVAEnabled = false;
+ // DXVA initialization with current decoder actually failed,
+ // re-do initialization.
+ return InitInternal();
+ }
+
+ LOG("Video Decoder initialized, Using DXVA: %s",
+ (mUseHwAccel ? "Yes" : "No"));
+
+ if (mUseHwAccel) {
+ hr = mDXVA2Manager->ConfigureForSize(
+ outputType,
+ mColorSpace.refOr(
+ DefaultColorSpace({mImageSize.width, mImageSize.height})),
+ mColorRange, mVideoInfo.ImageRect().width,
+ mVideoInfo.ImageRect().height);
+ NS_ENSURE_TRUE(SUCCEEDED(hr),
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Fail to configure image size for "
+ "DXVA2Manager.")));
+ } else {
+ GetDefaultStride(outputType, mVideoInfo.ImageRect().width, &mVideoStride);
+ }
+ LOG("WMFVideoMFTManager frame geometry stride=%u picture=(%d, %d, %d, %d) "
+ "display=(%d,%d)",
+ mVideoStride, mVideoInfo.ImageRect().x, mVideoInfo.ImageRect().y,
+ mVideoInfo.ImageRect().width, mVideoInfo.ImageRect().height,
+ mVideoInfo.mDisplay.width, mVideoInfo.mDisplay.height);
+
+ if (!mUseHwAccel) {
+ RefPtr<ID3D11Device> device = gfx::DeviceManagerDx::Get()->GetImageDevice();
+ if (device) {
+ mIMFUsable = true;
+ }
+ }
+ return MediaResult(NS_OK);
+}
+
+HRESULT
+WMFVideoMFTManager::SetDecoderMediaTypes() {
+ // Setup the input/output media types.
+ RefPtr<IMFMediaType> inputType;
+ HRESULT hr = wmf::MFCreateMediaType(getter_AddRefs(inputType));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = inputType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = inputType->SetGUID(MF_MT_SUBTYPE, GetMediaSubtypeGUID());
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = inputType->SetUINT32(MF_MT_INTERLACE_MODE,
+ MFVideoInterlace_MixedInterlaceOrProgressive);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = inputType->SetUINT32(MF_MT_INTERLACE_MODE, MFVideoInterlace_Progressive);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = MFSetAttributeSize(inputType, MF_MT_FRAME_SIZE,
+ mVideoInfo.ImageRect().width,
+ mVideoInfo.ImageRect().height);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ UINT32 fpsDenominator = 1000;
+ UINT32 fpsNumerator = static_cast<uint32_t>(mFramerate * fpsDenominator);
+ hr = MFSetAttributeRatio(inputType, MF_MT_FRAME_RATE, fpsNumerator,
+ fpsDenominator);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ RefPtr<IMFMediaType> outputType;
+ hr = wmf::MFCreateMediaType(getter_AddRefs(outputType));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = outputType->SetGUID(MF_MT_MAJOR_TYPE, MFMediaType_Video);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = MFSetAttributeSize(outputType, MF_MT_FRAME_SIZE,
+ mVideoInfo.ImageRect().width,
+ mVideoInfo.ImageRect().height);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = MFSetAttributeRatio(outputType, MF_MT_FRAME_RATE, fpsNumerator,
+ fpsDenominator);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ GUID outputSubType = [&]() {
+ switch (mVideoInfo.mColorDepth) {
+ case gfx::ColorDepth::COLOR_8:
+ return mUseHwAccel ? MFVideoFormat_NV12 : MFVideoFormat_YV12;
+ case gfx::ColorDepth::COLOR_10:
+ return MFVideoFormat_P010;
+ case gfx::ColorDepth::COLOR_12:
+ case gfx::ColorDepth::COLOR_16:
+ return MFVideoFormat_P016;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unexpected color depth");
+ }
+ }();
+ hr = outputType->SetGUID(MF_MT_SUBTYPE, outputSubType);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ if (mZeroCopyNV12Texture) {
+ RefPtr<IMFAttributes> attr(mDecoder->GetOutputStreamAttributes());
+ if (attr) {
+ hr = attr->SetUINT32(MF_SA_D3D11_SHARED_WITHOUT_MUTEX, TRUE);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ hr = attr->SetUINT32(MF_SA_D3D11_BINDFLAGS,
+ D3D11_BIND_SHADER_RESOURCE | D3D11_BIND_DECODER);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ }
+ }
+
+ return mDecoder->SetMediaTypes(inputType, outputType);
+}
+
+HRESULT
+WMFVideoMFTManager::Input(MediaRawData* aSample) {
+ if (!mIsValid) {
+ return E_FAIL;
+ }
+
+ if (!mDecoder) {
+ // This can happen during shutdown.
+ return E_FAIL;
+ }
+
+ mTrackingId.apply([&](const auto& aId) {
+ MediaInfoFlag flag = MediaInfoFlag::None;
+ flag |= (aSample->mKeyframe ? MediaInfoFlag::KeyFrame
+ : MediaInfoFlag::NonKeyFrame);
+ flag |= (mUseHwAccel ? MediaInfoFlag::HardwareDecoding
+ : MediaInfoFlag::SoftwareDecoding);
+ switch (mStreamType) {
+ case WMFStreamType::H264:
+ flag |= MediaInfoFlag::VIDEO_H264;
+ break;
+ case WMFStreamType::VP8:
+ flag |= MediaInfoFlag::VIDEO_VP8;
+ break;
+ case WMFStreamType::VP9:
+ flag |= MediaInfoFlag::VIDEO_VP9;
+ break;
+ case WMFStreamType::AV1:
+ flag |= MediaInfoFlag::VIDEO_AV1;
+ break;
+ default:
+ break;
+ };
+ mPerformanceRecorder.Start(aSample->mTime.ToMicroseconds(),
+ "WMFVideoDecoder"_ns, aId, flag);
+ });
+
+ RefPtr<IMFSample> inputSample;
+ HRESULT hr = mDecoder->CreateInputSample(
+ aSample->Data(), uint32_t(aSample->Size()),
+ aSample->mTime.ToMicroseconds(), aSample->mDuration.ToMicroseconds(),
+ &inputSample);
+ NS_ENSURE_TRUE(SUCCEEDED(hr) && inputSample != nullptr, hr);
+
+ if (!mColorSpace && aSample->mTrackInfo) {
+ // The colorspace definition is found in the H264 SPS NAL, available out of
+ // band, while for VP9 it's only available within the VP9 bytestream.
+ // The info would have been updated by the MediaChangeMonitor.
+ mColorSpace = aSample->mTrackInfo->GetAsVideoInfo()->mColorSpace;
+ mColorRange = aSample->mTrackInfo->GetAsVideoInfo()->mColorRange;
+ }
+ mLastDuration = aSample->mDuration;
+
+ // Forward sample data to the decoder.
+ return mDecoder->Input(inputSample);
+}
+
+// The MFTransforms we use for decoding H264 and AV1 video will silently fall
+// back to software decoding (even if we've negotiated DXVA) if the GPU
+// doesn't support decoding the given codec and resolution. It will then upload
+// the software decoded frames into d3d textures to preserve behaviour.
+//
+// Unfortunately this seems to cause corruption (see bug 1193547) and is
+// slow because the upload is done into a non-shareable texture and requires
+// us to copy it.
+//
+// This code tests if the given codec and resolution can be supported directly
+// on the GPU, and makes sure we only ask the MFT for DXVA if it can be
+// supported properly.
+//
+// Ideally we'd know the framerate during initialization and would also ensure
+// that new decoders are created if the resolution changes. Then we could move
+// this check into Init and consolidate the main thread blocking code.
+bool WMFVideoMFTManager::CanUseDXVA(IMFMediaType* aInputType,
+ IMFMediaType* aOutputType) {
+ MOZ_ASSERT(mDXVA2Manager);
+ // Check if we're able to use hardware decoding for the current codec config.
+ return mDXVA2Manager->SupportsConfig(mVideoInfo, aInputType, aOutputType);
+}
+
+TimeUnit WMFVideoMFTManager::GetSampleDurationOrLastKnownDuration(
+ IMFSample* aSample) const {
+ TimeUnit duration = GetSampleDuration(aSample);
+ if (!duration.IsValid()) {
+ // WMF returned a non-success code (likely duration unknown, but the API
+ // also allows for other, unspecified codes).
+ LOG("Got unknown sample duration -- bad return code. Using mLastDuration.");
+ } else if (duration == TimeUnit::Zero()) {
+ // Duration is zero. WMF uses this to indicate an unknown duration.
+ LOG("Got unknown sample duration -- zero duration returned. Using "
+ "mLastDuration.");
+ } else if (duration.IsNegative()) {
+ // A negative duration will cause issues up the stack. It's also unclear
+ // why this would happen, but the API allows for it by returning a signed
+ // int, so we handle it here.
+ LOG("Got negative sample duration: %f seconds. Using mLastDuration "
+ "instead.",
+ duration.ToSeconds());
+ } else {
+ // We got a duration without any problems.
+ return duration;
+ }
+
+ return mLastDuration;
+}
+
+HRESULT
+WMFVideoMFTManager::CreateBasicVideoFrame(IMFSample* aSample,
+ int64_t aStreamOffset,
+ VideoData** aOutVideoData) {
+ NS_ENSURE_TRUE(aSample, E_POINTER);
+ NS_ENSURE_TRUE(aOutVideoData, E_POINTER);
+
+ *aOutVideoData = nullptr;
+
+ HRESULT hr;
+ RefPtr<IMFMediaBuffer> buffer;
+
+ // Must convert to contiguous buffer to use IMD2DBuffer interface.
+ hr = aSample->ConvertToContiguousBuffer(getter_AddRefs(buffer));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ // Try and use the IMF2DBuffer interface if available, otherwise fallback
+ // to the IMFMediaBuffer interface. Apparently IMF2DBuffer is more efficient,
+ // but only some systems (Windows 8?) support it.
+ BYTE* data = nullptr;
+ LONG stride = 0;
+ RefPtr<IMF2DBuffer> twoDBuffer;
+ hr = buffer->QueryInterface(
+ static_cast<IMF2DBuffer**>(getter_AddRefs(twoDBuffer)));
+ if (SUCCEEDED(hr)) {
+ hr = twoDBuffer->Lock2D(&data, &stride);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ } else {
+ hr = buffer->Lock(&data, nullptr, nullptr);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ stride = mVideoStride;
+ }
+
+ const GUID& subType = mDecoder->GetOutputMediaSubType();
+ MOZ_DIAGNOSTIC_ASSERT(subType == MFVideoFormat_YV12 ||
+ subType == MFVideoFormat_P010 ||
+ subType == MFVideoFormat_P016);
+ const gfx::ColorDepth colorDepth = subType == MFVideoFormat_YV12
+ ? gfx::ColorDepth::COLOR_8
+ : gfx::ColorDepth::COLOR_16;
+
+ // YV12, planar format (3 planes): [YYYY....][VVVV....][UUUU....]
+ // i.e., Y, then V, then U.
+ // P010, P016 planar format (2 planes) [YYYY....][UVUV...]
+ // See
+ // https://docs.microsoft.com/en-us/windows/desktop/medfound/10-bit-and-16-bit-yuv-video-formats
+ VideoData::YCbCrBuffer b;
+
+ const uint32_t videoWidth = mSoftwareImageSize.width;
+ const uint32_t videoHeight = mSoftwareImageSize.height;
+
+ // Y (Y') plane
+ b.mPlanes[0].mData = data;
+ b.mPlanes[0].mStride = stride;
+ b.mPlanes[0].mHeight = videoHeight;
+ b.mPlanes[0].mWidth = videoWidth;
+ b.mPlanes[0].mSkip = 0;
+
+ MOZ_DIAGNOSTIC_ASSERT(mSoftwareImageSize.height % 16 == 0,
+ "decoded height must be 16 bytes aligned");
+ const uint32_t y_size = stride * mSoftwareImageSize.height;
+ const uint32_t v_size = stride * mSoftwareImageSize.height / 4;
+ const uint32_t halfStride = (stride + 1) / 2;
+ const uint32_t halfHeight = (videoHeight + 1) / 2;
+ const uint32_t halfWidth = (videoWidth + 1) / 2;
+
+ if (subType == MFVideoFormat_YV12) {
+ // U plane (Cb)
+ b.mPlanes[1].mData = data + y_size + v_size;
+ b.mPlanes[1].mStride = halfStride;
+ b.mPlanes[1].mHeight = halfHeight;
+ b.mPlanes[1].mWidth = halfWidth;
+ b.mPlanes[1].mSkip = 0;
+
+ // V plane (Cr)
+ b.mPlanes[2].mData = data + y_size;
+ b.mPlanes[2].mStride = halfStride;
+ b.mPlanes[2].mHeight = halfHeight;
+ b.mPlanes[2].mWidth = halfWidth;
+ b.mPlanes[2].mSkip = 0;
+ } else {
+ // U plane (Cb)
+ b.mPlanes[1].mData = data + y_size;
+ b.mPlanes[1].mStride = stride;
+ b.mPlanes[1].mHeight = halfHeight;
+ b.mPlanes[1].mWidth = halfWidth;
+ b.mPlanes[1].mSkip = 1;
+
+ // V plane (Cr)
+ b.mPlanes[2].mData = data + y_size + sizeof(short);
+ b.mPlanes[2].mStride = stride;
+ b.mPlanes[2].mHeight = halfHeight;
+ b.mPlanes[2].mWidth = halfWidth;
+ b.mPlanes[2].mSkip = 1;
+ }
+
+ b.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT;
+
+ // YuvColorSpace
+ b.mYUVColorSpace =
+ mColorSpace.refOr(DefaultColorSpace({videoWidth, videoHeight}));
+ b.mColorDepth = colorDepth;
+ b.mColorRange = mColorRange;
+
+ TimeUnit pts = GetSampleTime(aSample);
+ NS_ENSURE_TRUE(pts.IsValid(), E_FAIL);
+ TimeUnit duration = GetSampleDurationOrLastKnownDuration(aSample);
+ NS_ENSURE_TRUE(duration.IsValid(), E_FAIL);
+ gfx::IntRect pictureRegion = mVideoInfo.ScaledImageRect(
+ mSoftwarePictureSize.width, mSoftwarePictureSize.height);
+
+ if (colorDepth != gfx::ColorDepth::COLOR_8 || !mKnowsCompositor ||
+ !mKnowsCompositor->SupportsD3D11() || !mIMFUsable) {
+ RefPtr<VideoData> v = VideoData::CreateAndCopyData(
+ mVideoInfo, mImageContainer, aStreamOffset, pts, duration, b, false,
+ TimeUnit::FromMicroseconds(-1), pictureRegion, mKnowsCompositor);
+ if (twoDBuffer) {
+ twoDBuffer->Unlock2D();
+ } else {
+ buffer->Unlock();
+ }
+ v.forget(aOutVideoData);
+ return S_OK;
+ }
+
+ RefPtr<layers::PlanarYCbCrImage> image =
+ new IMFYCbCrImage(buffer, twoDBuffer, mKnowsCompositor, mImageContainer);
+
+ VideoData::SetVideoDataToImage(image, mVideoInfo, b, pictureRegion, false);
+
+ RefPtr<VideoData> v = VideoData::CreateFromImage(
+ mVideoInfo.mDisplay, aStreamOffset, pts, duration, image.forget(), false,
+ TimeUnit::FromMicroseconds(-1));
+
+ mPerformanceRecorder.Record(pts.ToMicroseconds(), [&](DecodeStage& aStage) {
+ aStage.SetColorDepth(b.mColorDepth);
+ aStage.SetColorRange(b.mColorRange);
+ aStage.SetYUVColorSpace(b.mYUVColorSpace);
+ if (subType == MFVideoFormat_NV12) {
+ aStage.SetImageFormat(DecodeStage::NV12);
+ } else if (subType == MFVideoFormat_YV12) {
+ aStage.SetImageFormat(DecodeStage::YV12);
+ } else if (subType == MFVideoFormat_P010) {
+ aStage.SetImageFormat(DecodeStage::P010);
+ } else if (subType == MFVideoFormat_P016) {
+ aStage.SetImageFormat(DecodeStage::P016);
+ }
+ aStage.SetResolution(videoWidth, videoHeight);
+ });
+
+ v.forget(aOutVideoData);
+ return S_OK;
+}
+
+HRESULT
+WMFVideoMFTManager::CreateD3DVideoFrame(IMFSample* aSample,
+ int64_t aStreamOffset,
+ VideoData** aOutVideoData) {
+ NS_ENSURE_TRUE(aSample, E_POINTER);
+ NS_ENSURE_TRUE(aOutVideoData, E_POINTER);
+ NS_ENSURE_TRUE(mDXVA2Manager, E_ABORT);
+ NS_ENSURE_TRUE(mUseHwAccel, E_ABORT);
+
+ *aOutVideoData = nullptr;
+ HRESULT hr;
+
+ gfx::IntRect pictureRegion =
+ mVideoInfo.ScaledImageRect(mImageSize.width, mImageSize.height);
+ RefPtr<Image> image;
+ if (mZeroCopyNV12Texture && mDXVA2Manager->SupportsZeroCopyNV12Texture()) {
+ hr = mDXVA2Manager->WrapTextureWithImage(aSample, pictureRegion,
+ getter_AddRefs(image));
+ } else {
+ hr = mDXVA2Manager->CopyToImage(aSample, pictureRegion,
+ getter_AddRefs(image));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ }
+ NS_ENSURE_TRUE(image, E_FAIL);
+
+ gfx::IntSize size = image->GetSize();
+
+ TimeUnit pts = GetSampleTime(aSample);
+ NS_ENSURE_TRUE(pts.IsValid(), E_FAIL);
+ TimeUnit duration = GetSampleDurationOrLastKnownDuration(aSample);
+ NS_ENSURE_TRUE(duration.IsValid(), E_FAIL);
+ RefPtr<VideoData> v = VideoData::CreateFromImage(
+ mVideoInfo.mDisplay, aStreamOffset, pts, duration, image.forget(), false,
+ TimeUnit::FromMicroseconds(-1));
+
+ NS_ENSURE_TRUE(v, E_FAIL);
+ v.forget(aOutVideoData);
+
+ mPerformanceRecorder.Record(pts.ToMicroseconds(), [&](DecodeStage& aStage) {
+ aStage.SetColorDepth(mVideoInfo.mColorDepth);
+ aStage.SetColorRange(mColorRange);
+ aStage.SetYUVColorSpace(mColorSpace.refOr(
+ DefaultColorSpace({mImageSize.width, mImageSize.height})));
+ const GUID& subType = mDecoder->GetOutputMediaSubType();
+ if (subType == MFVideoFormat_NV12) {
+ aStage.SetImageFormat(DecodeStage::NV12);
+ } else if (subType == MFVideoFormat_YV12) {
+ aStage.SetImageFormat(DecodeStage::YV12);
+ } else if (subType == MFVideoFormat_P010) {
+ aStage.SetImageFormat(DecodeStage::P010);
+ } else if (subType == MFVideoFormat_P016) {
+ aStage.SetImageFormat(DecodeStage::P016);
+ }
+ aStage.SetResolution(size.width, size.height);
+ });
+
+ return S_OK;
+}
+
+// Blocks until decoded sample is produced by the decoder.
+HRESULT
+WMFVideoMFTManager::Output(int64_t aStreamOffset, RefPtr<MediaData>& aOutData) {
+ RefPtr<IMFSample> sample;
+ HRESULT hr;
+ aOutData = nullptr;
+ int typeChangeCount = 0;
+
+ // Loop until we decode a sample, or an unexpected error that we can't
+ // handle occurs.
+ while (true) {
+ hr = mDecoder->Output(&sample);
+ if (hr == MF_E_TRANSFORM_NEED_MORE_INPUT) {
+ return MF_E_TRANSFORM_NEED_MORE_INPUT;
+ }
+
+ if (hr == MF_E_TRANSFORM_STREAM_CHANGE) {
+ MOZ_ASSERT(!sample);
+ // Video stream output type change, probably geometric aperture change or
+ // pixel type.
+ // We must reconfigure the decoder output type.
+
+ // Attempt to find an appropriate OutputType, trying in order:
+ // if HW accelerated: NV12, P010, P016
+ // if SW: YV12, P010, P016
+ if (FAILED(
+ (hr = (mDecoder->FindDecoderOutputTypeWithSubtype(
+ mUseHwAccel ? MFVideoFormat_NV12 : MFVideoFormat_YV12)))) &&
+ FAILED((hr = mDecoder->FindDecoderOutputTypeWithSubtype(
+ MFVideoFormat_P010))) &&
+ FAILED((hr = mDecoder->FindDecoderOutputTypeWithSubtype(
+ MFVideoFormat_P016)))) {
+ LOG("No suitable output format found");
+ return hr;
+ }
+
+ RefPtr<IMFMediaType> outputType;
+ hr = mDecoder->GetOutputMediaType(outputType);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ if (mUseHwAccel) {
+ hr = mDXVA2Manager->ConfigureForSize(
+ outputType,
+ mColorSpace.refOr(
+ DefaultColorSpace({mImageSize.width, mImageSize.height})),
+ mColorRange, mVideoInfo.ImageRect().width,
+ mVideoInfo.ImageRect().height);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ } else {
+ // The stride may have changed, recheck for it.
+ hr = GetDefaultStride(outputType, mVideoInfo.ImageRect().width,
+ &mVideoStride);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+
+ UINT32 width = 0, height = 0;
+ hr = MFGetAttributeSize(outputType, MF_MT_FRAME_SIZE, &width, &height);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ NS_ENSURE_TRUE(width <= MAX_VIDEO_WIDTH, E_FAIL);
+ NS_ENSURE_TRUE(height <= MAX_VIDEO_HEIGHT, E_FAIL);
+ mSoftwareImageSize = gfx::IntSize(width, height);
+
+ gfx::IntRect picture;
+ hr = GetPictureRegion(outputType, picture);
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ MOZ_ASSERT(picture.width != 0 && picture.height != 0);
+ mSoftwarePictureSize = gfx::IntSize(picture.width, picture.height);
+ LOG("Output stream change, image size=[%ux%u], picture=[%u,%u]",
+ mSoftwareImageSize.width, mSoftwareImageSize.height,
+ mSoftwarePictureSize.width, mSoftwarePictureSize.height);
+ }
+ // Catch infinite loops, but some decoders perform at least 2 stream
+ // changes on consecutive calls, so be permissive.
+ // 100 is arbitrarily > 2.
+ NS_ENSURE_TRUE(typeChangeCount < 100, MF_E_TRANSFORM_STREAM_CHANGE);
+ // Loop back and try decoding again...
+ ++typeChangeCount;
+ continue;
+ }
+
+ if (SUCCEEDED(hr)) {
+ if (!sample) {
+ LOG("Video MFTDecoder returned success but no output!");
+ // On some machines/input the MFT returns success but doesn't output
+ // a video frame. If we detect this, try again, but only up to a
+ // point; after 250 failures, give up. Note we count all failures
+ // over the life of the decoder, as we may end up exiting with a
+ // NEED_MORE_INPUT and coming back to hit the same error. So just
+ // counting with a local variable (like typeChangeCount does) may
+ // not work in this situation.
+ ++mNullOutputCount;
+ if (mNullOutputCount > 250) {
+ LOG("Excessive Video MFTDecoder returning success but no output; "
+ "giving up");
+ mGotExcessiveNullOutput = true;
+ return E_FAIL;
+ }
+ continue;
+ }
+ TimeUnit pts = GetSampleTime(sample);
+ TimeUnit duration = GetSampleDurationOrLastKnownDuration(sample);
+
+ // AV1 MFT fix: Sample duration after seeking is always equal to the
+ // sample time, for some reason. Set it to last duration instead.
+ if (mStreamType == WMFStreamType::AV1 && duration == pts) {
+ LOG("Video sample duration (%" PRId64 ") matched timestamp (%" PRId64
+ "), setting to previous sample duration (%" PRId64 ") instead.",
+ pts.ToMicroseconds(), duration.ToMicroseconds(),
+ mLastDuration.ToMicroseconds());
+ duration = mLastDuration;
+ sample->SetSampleDuration(UsecsToHNs(duration.ToMicroseconds()));
+ }
+
+ if (!pts.IsValid() || !duration.IsValid()) {
+ return E_FAIL;
+ }
+ if (mSeekTargetThreshold.isSome()) {
+ if ((pts + duration) < mSeekTargetThreshold.ref()) {
+ LOG("Dropping video frame which pts (%" PRId64 " + %" PRId64
+ ") is smaller than seek target (%" PRId64 ").",
+ pts.ToMicroseconds(), duration.ToMicroseconds(),
+ mSeekTargetThreshold->ToMicroseconds());
+ // It is necessary to clear the pointer to release the previous output
+ // buffer.
+ sample = nullptr;
+ continue;
+ }
+ mSeekTargetThreshold.reset();
+ }
+ break;
+ }
+ // Else unexpected error so bail.
+ NS_WARNING("WMFVideoMFTManager::Output() unexpected error");
+ return hr;
+ }
+
+ RefPtr<VideoData> frame;
+ if (mUseHwAccel) {
+ hr = CreateD3DVideoFrame(sample, aStreamOffset, getter_AddRefs(frame));
+ } else {
+ hr = CreateBasicVideoFrame(sample, aStreamOffset, getter_AddRefs(frame));
+ }
+ // Frame should be non null only when we succeeded.
+ MOZ_ASSERT((frame != nullptr) == SUCCEEDED(hr));
+ NS_ENSURE_TRUE(SUCCEEDED(hr), hr);
+ NS_ENSURE_TRUE(frame, E_FAIL);
+
+ aOutData = frame;
+
+ if (mNullOutputCount) {
+ mGotValidOutputAfterNullOutput = true;
+ }
+
+ return S_OK;
+}
+
+void WMFVideoMFTManager::Flush() {
+ MFTManager::Flush();
+ mPerformanceRecorder.Record(std::numeric_limits<int64_t>::max());
+}
+
+void WMFVideoMFTManager::Shutdown() {
+ if (mDXVA2Manager) {
+ mDXVA2Manager->BeforeShutdownVideoMFTDecoder();
+ }
+ mDecoder = nullptr;
+ mDXVA2Manager.reset();
+}
+
+bool WMFVideoMFTManager::IsHardwareAccelerated(
+ nsACString& aFailureReason) const {
+ aFailureReason = mDXVAFailureReason;
+ return mDecoder && mUseHwAccel;
+}
+
+nsCString WMFVideoMFTManager::GetDescriptionName() const {
+ nsCString failureReason;
+ bool hw = IsHardwareAccelerated(failureReason);
+
+ const char* formatName = [&]() {
+ if (!mDecoder) {
+ return "not initialized";
+ }
+ GUID format = mDecoder->GetOutputMediaSubType();
+ if (format == MFVideoFormat_NV12) {
+ if (!gfx::DeviceManagerDx::Get()->CanUseNV12()) {
+ return "nv12->argb32";
+ }
+ return "nv12";
+ }
+ if (format == MFVideoFormat_P010) {
+ if (!gfx::DeviceManagerDx::Get()->CanUseP010()) {
+ return "p010->argb32";
+ }
+ return "p010";
+ }
+ if (format == MFVideoFormat_P016) {
+ if (!gfx::DeviceManagerDx::Get()->CanUseP016()) {
+ return "p016->argb32";
+ }
+ return "p016";
+ }
+ if (format == MFVideoFormat_YV12) {
+ return "yv12";
+ }
+ return "unknown";
+ }();
+
+ const char* dxvaName = [&]() {
+ if (!mDXVA2Manager) {
+ return "no DXVA";
+ }
+ if (mDXVA2Manager->IsD3D11()) {
+ return "D3D11";
+ }
+ return "D3D9";
+ }();
+
+ return nsPrintfCString("wmf %s codec %s video decoder - %s, %s",
+ StreamTypeToString(mStreamType),
+ hw ? "hardware" : "software", dxvaName, formatName);
+}
+nsCString WMFVideoMFTManager::GetCodecName() const {
+ switch (mStreamType) {
+ case WMFStreamType::H264:
+ return "h264"_ns;
+ case WMFStreamType::VP8:
+ return "vp8"_ns;
+ case WMFStreamType::VP9:
+ return "vp9"_ns;
+ case WMFStreamType::AV1:
+ return "av1"_ns;
+ default:
+ return "unknown"_ns;
+ };
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/wmf/WMFVideoMFTManager.h b/dom/media/platforms/wmf/WMFVideoMFTManager.h
new file mode 100644
index 0000000000..4982acadab
--- /dev/null
+++ b/dom/media/platforms/wmf/WMFVideoMFTManager.h
@@ -0,0 +1,132 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(WMFVideoMFTManager_h_)
+# define WMFVideoMFTManager_h_
+
+# include "MFTDecoder.h"
+# include "MediaResult.h"
+# include "PerformanceRecorder.h"
+# include "WMF.h"
+# include "WMFDecoderModule.h"
+# include "WMFMediaDataDecoder.h"
+# include "mozilla/Atomics.h"
+# include "mozilla/RefPtr.h"
+# include "mozilla/gfx/Rect.h"
+
+namespace mozilla {
+
+class DXVA2Manager;
+
+class WMFVideoMFTManager : public MFTManager {
+ public:
+ WMFVideoMFTManager(const VideoInfo& aConfig,
+ layers::KnowsCompositor* aKnowsCompositor,
+ layers::ImageContainer* aImageContainer, float aFramerate,
+ const CreateDecoderParams::OptionSet& aOptions,
+ bool aDXVAEnabled, Maybe<TrackingId> aTrackingId);
+ ~WMFVideoMFTManager();
+
+ MediaResult Init();
+
+ HRESULT Input(MediaRawData* aSample) override;
+
+ HRESULT Output(int64_t aStreamOffset, RefPtr<MediaData>& aOutput) override;
+
+ void Flush() override;
+
+ void Shutdown() override;
+
+ bool IsHardwareAccelerated(nsACString& aFailureReason) const override;
+
+ TrackInfo::TrackType GetType() override { return TrackInfo::kVideoTrack; }
+
+ nsCString GetDescriptionName() const override;
+
+ nsCString GetCodecName() const override;
+
+ MediaDataDecoder::ConversionRequired NeedsConversion() const override {
+ return mStreamType == WMFStreamType::H264
+ ? MediaDataDecoder::ConversionRequired::kNeedAnnexB
+ : MediaDataDecoder::ConversionRequired::kNeedNone;
+ }
+
+ private:
+ MediaResult ValidateVideoInfo();
+
+ bool InitializeDXVA();
+
+ MediaResult InitInternal();
+
+ HRESULT CreateBasicVideoFrame(IMFSample* aSample, int64_t aStreamOffset,
+ VideoData** aOutVideoData);
+
+ HRESULT CreateD3DVideoFrame(IMFSample* aSample, int64_t aStreamOffset,
+ VideoData** aOutVideoData);
+
+ HRESULT SetDecoderMediaTypes();
+
+ bool CanUseDXVA(IMFMediaType* aInputType, IMFMediaType* aOutputType);
+
+ // Gets the duration from aSample, and if an unknown or invalid duration is
+ // returned from WMF, this instead returns the last known input duration.
+ // The sample duration is unknown per `IMFSample::GetSampleDuration` docs
+ // 'If the retrieved duration is zero, or if the method returns
+ // MF_E_NO_SAMPLE_DURATION, the duration is unknown'. The same API also
+ // suggests it may return other unspecified error codes, so we handle those
+ // too. It also returns a signed int, but since a negative duration doesn't
+ // make sense, we also handle that case.
+ media::TimeUnit GetSampleDurationOrLastKnownDuration(
+ IMFSample* aSample) const;
+
+ // Video frame geometry.
+ const VideoInfo mVideoInfo;
+ const gfx::IntSize mImageSize;
+ const WMFStreamType mStreamType;
+
+ // The size we update from the IMFMediaType which might include paddings when
+ // the stream format changes. This is only used for software decoding.
+ gfx::IntSize mSoftwareImageSize;
+
+ // The picture size we update from the IMFMediaType when the stream format
+ // changes. We assume it's equal to the image size by default (no cropping).
+ // This is only used for software decoding.
+ gfx::IntSize mSoftwarePictureSize;
+
+ uint32_t mVideoStride;
+ Maybe<gfx::YUVColorSpace> mColorSpace;
+ gfx::ColorRange mColorRange;
+
+ RefPtr<layers::ImageContainer> mImageContainer;
+ RefPtr<layers::KnowsCompositor> mKnowsCompositor;
+ UniquePtr<DXVA2Manager> mDXVA2Manager;
+
+ media::TimeUnit mLastDuration;
+
+ bool mDXVAEnabled;
+ bool mUseHwAccel;
+
+ bool mZeroCopyNV12Texture;
+
+ nsCString mDXVAFailureReason;
+
+ const GUID& GetMediaSubtypeGUID();
+
+ uint32_t mNullOutputCount = 0;
+ bool mGotValidOutputAfterNullOutput = false;
+ bool mGotExcessiveNullOutput = false;
+ bool mIsValid = true;
+ bool mIMFUsable = false;
+ const float mFramerate;
+ const bool mLowLatency;
+
+ PerformanceRecorderMulti<DecodeStage> mPerformanceRecorder;
+ const Maybe<TrackingId> mTrackingId;
+};
+
+} // namespace mozilla
+
+#endif // WMFVideoMFTManager_h_
diff --git a/dom/media/platforms/wmf/gtest/TestCanCreateMFTDecoder.cpp b/dom/media/platforms/wmf/gtest/TestCanCreateMFTDecoder.cpp
new file mode 100644
index 0000000000..fc11e89dfd
--- /dev/null
+++ b/dom/media/platforms/wmf/gtest/TestCanCreateMFTDecoder.cpp
@@ -0,0 +1,21 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <gtest/gtest.h>
+
+#include "WMFDecoderModule.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/Preferences.h"
+
+using namespace mozilla;
+
+TEST(CanCreateMFTDecoder, NoIPC)
+{
+ const auto ffvpxMP3Pref = StaticPrefs::GetPrefName_media_ffvpx_mp3_enabled();
+ const bool ffvpxMP3WasOn = Preferences::GetBool(ffvpxMP3Pref);
+ Preferences::SetBool(ffvpxMP3Pref, false);
+ EXPECT_TRUE(WMFDecoderModule::CanCreateMFTDecoder(WMFStreamType::MP3));
+ Preferences::SetBool(ffvpxMP3Pref, ffvpxMP3WasOn);
+}
diff --git a/dom/media/platforms/wmf/gtest/moz.build b/dom/media/platforms/wmf/gtest/moz.build
new file mode 100644
index 0000000000..ccd056ecf1
--- /dev/null
+++ b/dom/media/platforms/wmf/gtest/moz.build
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES += [
+ "TestCanCreateMFTDecoder.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/dom/media/platforms/wmf/gtest",
+]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/dom/media/platforms/wmf/moz.build b/dom/media/platforms/wmf/moz.build
new file mode 100644
index 0000000000..9e0f3aa94a
--- /dev/null
+++ b/dom/media/platforms/wmf/moz.build
@@ -0,0 +1,85 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS += [
+ "DXVA2Manager.h",
+ "MFTDecoder.h",
+ "WMF.h",
+ "WMFAudioMFTManager.h",
+ "WMFDataEncoderUtils.h",
+ "WMFDecoderModule.h",
+ "WMFEncoderModule.h",
+ "WMFMediaDataDecoder.h",
+ "WMFMediaDataEncoder.h",
+ "WMFUtils.h",
+ "WMFVideoMFTManager.h",
+]
+
+if CONFIG["MOZ_WMF_MEDIA_ENGINE"]:
+ EXPORTS += [
+ "MFMediaEngineAudioStream.h",
+ "MFMediaEngineDecoderModule.h",
+ "MFMediaEngineExtra.h",
+ "MFMediaEngineStream.h",
+ "MFMediaEngineVideoStream.h",
+ "MFMediaSource.h",
+ ]
+ UNIFIED_SOURCES += [
+ "MFMediaEngineAudioStream.cpp",
+ "MFMediaEngineDecoderModule.cpp",
+ "MFMediaEngineExtension.cpp",
+ "MFMediaEngineNotify.cpp",
+ "MFMediaEngineStream.cpp",
+ "MFMediaEngineVideoStream.cpp",
+ "MFMediaSource.cpp",
+ ]
+
+if CONFIG["MOZ_WMF_CDM"]:
+ EXPORTS += [
+ "MFCDMExtra.h",
+ "MFCDMProxy.h",
+ "MFCDMSession.h",
+ "MFContentProtectionManager.h",
+ "MFPMPHostWrapper.h",
+ ]
+ UNIFIED_SOURCES += [
+ "MFCDMProxy.cpp",
+ "MFCDMSession.cpp",
+ "MFContentProtectionManager.cpp",
+ "MFPMPHostWrapper.cpp",
+ ]
+
+UNIFIED_SOURCES += [
+ "DXVA2Manager.cpp",
+ "MFTDecoder.cpp",
+ "MFTEncoder.cpp",
+ "WMFAudioMFTManager.cpp",
+ "WMFDecoderModule.cpp",
+ "WMFEncoderModule.cpp",
+ "WMFMediaDataDecoder.cpp",
+ "WMFVideoMFTManager.cpp",
+]
+
+SOURCES += [
+ "WMFUtils.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "../../ipc/",
+ "/gfx/cairo/cairo/src",
+ "/media/libyuv/libyuv/include",
+]
+
+TEST_DIRS += [
+ "gtest",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+
+# Add libFuzzer configuration directives
+include("/tools/fuzzing/libfuzzer-config.mozbuild")
diff --git a/dom/media/platforms/wrappers/AudioTrimmer.cpp b/dom/media/platforms/wrappers/AudioTrimmer.cpp
new file mode 100644
index 0000000000..8ed466001b
--- /dev/null
+++ b/dom/media/platforms/wrappers/AudioTrimmer.cpp
@@ -0,0 +1,221 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioTrimmer.h"
+
+#define LOG(arg, ...) \
+ DDMOZ_LOG(sPDMLog, mozilla::LogLevel::Debug, "::%s: " arg, __func__, \
+ ##__VA_ARGS__)
+
+#define LOGV(arg, ...) \
+ DDMOZ_LOG(sPDMLog, mozilla::LogLevel::Verbose, "::%s: " arg, __func__, \
+ ##__VA_ARGS__)
+
+namespace mozilla {
+
+using media::TimeInterval;
+using media::TimeUnit;
+
+RefPtr<MediaDataDecoder::InitPromise> AudioTrimmer::Init() {
+ mThread = GetCurrentSerialEventTarget();
+ return mDecoder->Init();
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> AudioTrimmer::Decode(
+ MediaRawData* aSample) {
+ MOZ_ASSERT(mThread->IsOnCurrentThread(),
+ "We're not on the thread we were first initialized on");
+ RefPtr<MediaRawData> sample = aSample;
+ PrepareTrimmers(sample);
+ RefPtr<AudioTrimmer> self = this;
+ RefPtr<DecodePromise> p = mDecoder->Decode(sample)->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self, sample](DecodePromise::ResolveOrRejectValue&& aValue) {
+ return self->HandleDecodedResult(std::move(aValue), sample);
+ });
+ return p;
+}
+
+RefPtr<MediaDataDecoder::FlushPromise> AudioTrimmer::Flush() {
+ MOZ_ASSERT(mThread->IsOnCurrentThread(),
+ "We're not on the thread we were first initialized on");
+ RefPtr<FlushPromise> p = mDecoder->Flush();
+ mTrimmers.Clear();
+ return p;
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> AudioTrimmer::Drain() {
+ MOZ_ASSERT(mThread->IsOnCurrentThread(),
+ "We're not on the thread we were first initialized on");
+ LOG("Draining");
+ RefPtr<DecodePromise> p = mDecoder->Drain()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr{this}](DecodePromise::ResolveOrRejectValue&& aValue) {
+ return self->HandleDecodedResult(std::move(aValue), nullptr);
+ });
+ return p;
+}
+
+RefPtr<ShutdownPromise> AudioTrimmer::Shutdown() {
+ // mThread may not be set if Init hasn't been called first.
+ MOZ_ASSERT(!mThread || mThread->IsOnCurrentThread());
+ return mDecoder->Shutdown();
+}
+
+nsCString AudioTrimmer::GetDescriptionName() const {
+ return mDecoder->GetDescriptionName();
+}
+
+nsCString AudioTrimmer::GetProcessName() const {
+ return mDecoder->GetProcessName();
+}
+
+nsCString AudioTrimmer::GetCodecName() const {
+ return mDecoder->GetCodecName();
+}
+
+bool AudioTrimmer::IsHardwareAccelerated(nsACString& aFailureReason) const {
+ return mDecoder->IsHardwareAccelerated(aFailureReason);
+}
+
+void AudioTrimmer::SetSeekThreshold(const media::TimeUnit& aTime) {
+ mDecoder->SetSeekThreshold(aTime);
+}
+
+bool AudioTrimmer::SupportDecoderRecycling() const {
+ return mDecoder->SupportDecoderRecycling();
+}
+
+MediaDataDecoder::ConversionRequired AudioTrimmer::NeedsConversion() const {
+ return mDecoder->NeedsConversion();
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> AudioTrimmer::HandleDecodedResult(
+ DecodePromise::ResolveOrRejectValue&& aValue, MediaRawData* aRaw) {
+ MOZ_ASSERT(mThread->IsOnCurrentThread(),
+ "We're not on the thread we were first initialized on");
+ if (aValue.IsReject()) {
+ return DecodePromise::CreateAndReject(std::move(aValue.RejectValue()),
+ __func__);
+ }
+ TimeUnit rawStart = aRaw ? aRaw->mTime : TimeUnit::Zero();
+ TimeUnit rawEnd = aRaw ? aRaw->GetEndTime() : TimeUnit::Zero();
+ MediaDataDecoder::DecodedData results = std::move(aValue.ResolveValue());
+ if (results.IsEmpty()) {
+ // No samples returned, we assume this is due to the latency of the
+ // decoder and that the related decoded sample will be returned during
+ // the next call to Decode().
+ LOGV("No sample returned for sample[%s, %s]", rawStart.ToString().get(),
+ rawEnd.ToString().get());
+ }
+ for (uint32_t i = 0; i < results.Length();) {
+ const RefPtr<MediaData>& data = results[i];
+ MOZ_ASSERT(data->mType == MediaData::Type::AUDIO_DATA);
+ TimeInterval sampleInterval(data->mTime, data->GetEndTime());
+ if (mTrimmers.IsEmpty()) {
+ // mTrimmers being empty can only occurs if the decoder returned more
+ // frames than we pushed in. We can't handle this case, abort trimming.
+ LOG("sample[%s, %s] (decoded[%s, %s] no trimming information)",
+ rawStart.ToString().get(), rawEnd.ToString().get(),
+ sampleInterval.mStart.ToString().get(),
+ sampleInterval.mEnd.ToString().get());
+ i++;
+ continue;
+ }
+
+ Maybe<TimeInterval> trimmer = mTrimmers[0];
+ mTrimmers.RemoveElementAt(0);
+ if (!trimmer) {
+ // Those frames didn't need trimming.
+ LOGV("sample[%s, %s] (decoded[%s, %s] no trimming needed",
+ rawStart.ToString().get(), rawEnd.ToString().get(),
+ sampleInterval.mStart.ToString().get(),
+ sampleInterval.mEnd.ToString().get());
+ i++;
+ continue;
+ }
+ if (!trimmer->Intersects(sampleInterval)) {
+ LOGV(
+ "sample[%s, %s] (decoded[%s, %s] would be empty after trimming, "
+ "dropping it",
+ rawStart.ToString().get(), rawEnd.ToString().get(),
+ sampleInterval.mStart.ToString().get(),
+ sampleInterval.mEnd.ToString().get());
+ results.RemoveElementAt(i);
+ continue;
+ }
+ LOGV("Trimming sample[%s,%s] to [%s,%s] (raw was:[%s, %s])",
+ sampleInterval.mStart.ToString().get(),
+ sampleInterval.mEnd.ToString().get(), trimmer->mStart.ToString().get(),
+ trimmer->mEnd.ToString().get(), rawStart.ToString().get(),
+ rawEnd.ToString().get());
+
+ TimeInterval trim({std::max(trimmer->mStart, sampleInterval.mStart),
+ std::min(trimmer->mEnd, sampleInterval.mEnd)});
+ AudioData* sample = static_cast<AudioData*>(data.get());
+ bool ok = sample->SetTrimWindow(trim);
+ NS_ASSERTION(ok, "Trimming of audio sample failed");
+ Unused << ok;
+ if (sample->Frames() == 0) {
+ LOGV("sample[%s, %s] is empty after trimming, dropping it",
+ rawStart.ToString().get(), rawEnd.ToString().get());
+ results.RemoveElementAt(i);
+ continue;
+ }
+ i++;
+ }
+ return DecodePromise::CreateAndResolve(std::move(results), __func__);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> AudioTrimmer::DecodeBatch(
+ nsTArray<RefPtr<MediaRawData>>&& aSamples) {
+ MOZ_ASSERT(mThread->IsOnCurrentThread(),
+ "We're not on the thread we were first initialized on");
+ LOGV("DecodeBatch");
+
+ for (auto&& sample : aSamples) {
+ PrepareTrimmers(sample);
+ }
+ RefPtr<DecodePromise> p =
+ mDecoder->DecodeBatch(std::move(aSamples))
+ ->Then(GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr{this}](
+ DecodePromise::ResolveOrRejectValue&& aValue) {
+ // If the decoder returned less samples than what we fed it.
+ // We can assume that this is due to the decoder encoding
+ // delay and that all decoded frames have been shifted by n =
+ // compressedSamples.Length() - decodedSamples.Length() and
+ // that the first n compressed samples returned nothing.
+ return self->HandleDecodedResult(std::move(aValue), nullptr);
+ });
+ return p;
+}
+
+void AudioTrimmer::PrepareTrimmers(MediaRawData* aRaw) {
+ // A compress sample indicates that it needs to be trimmed after decoding by
+ // having its mOriginalPresentationWindow member set; in which case
+ // mOriginalPresentationWindow contains the original time and duration of
+ // the frame set by the demuxer and mTime and mDuration set to what it
+ // should be after trimming.
+ if (aRaw->mOriginalPresentationWindow) {
+ LOGV("sample[%s, %s] has trimming info ([%s, %s]",
+ aRaw->mOriginalPresentationWindow->mStart.ToString().get(),
+ aRaw->mOriginalPresentationWindow->mEnd.ToString().get(),
+ aRaw->mTime.ToString().get(), aRaw->GetEndTime().ToString().get());
+ mTrimmers.AppendElement(
+ Some(TimeInterval(aRaw->mTime, aRaw->GetEndTime())));
+ aRaw->mTime = aRaw->mOriginalPresentationWindow->mStart;
+ aRaw->mDuration = aRaw->mOriginalPresentationWindow->Length();
+ } else {
+ LOGV("sample[%s,%s] no trimming information", aRaw->mTime.ToString().get(),
+ aRaw->GetEndTime().ToString().get());
+ mTrimmers.AppendElement(Nothing());
+ }
+}
+
+} // namespace mozilla
+
+#undef LOG
diff --git a/dom/media/platforms/wrappers/AudioTrimmer.h b/dom/media/platforms/wrappers/AudioTrimmer.h
new file mode 100644
index 0000000000..e841a5919f
--- /dev/null
+++ b/dom/media/platforms/wrappers/AudioTrimmer.h
@@ -0,0 +1,56 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(AudioTrimmer_h_)
+# define AudioTrimmer_h_
+
+# include "PlatformDecoderModule.h"
+# include "mozilla/Mutex.h"
+
+namespace mozilla {
+
+DDLoggedTypeDeclNameAndBase(AudioTrimmer, MediaDataDecoder);
+
+class AudioTrimmer final : public MediaDataDecoder {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AudioTrimmer, final);
+
+ AudioTrimmer(already_AddRefed<MediaDataDecoder> aDecoder,
+ const CreateDecoderParams& aParams)
+ : mDecoder(aDecoder) {}
+
+ RefPtr<InitPromise> Init() override;
+ RefPtr<DecodePromise> Decode(MediaRawData* aSample) override;
+ bool CanDecodeBatch() const override { return mDecoder->CanDecodeBatch(); }
+ RefPtr<DecodePromise> DecodeBatch(
+ nsTArray<RefPtr<MediaRawData>>&& aSamples) override;
+ RefPtr<DecodePromise> Drain() override;
+ RefPtr<FlushPromise> Flush() override;
+ RefPtr<ShutdownPromise> Shutdown() override;
+ nsCString GetDescriptionName() const override;
+ nsCString GetProcessName() const override;
+ nsCString GetCodecName() const override;
+ bool IsHardwareAccelerated(nsACString& aFailureReason) const override;
+ void SetSeekThreshold(const media::TimeUnit& aTime) override;
+ bool SupportDecoderRecycling() const override;
+ ConversionRequired NeedsConversion() const override;
+
+ private:
+ ~AudioTrimmer() = default;
+
+ // Apply trimming information on decoded data. aRaw can be null as it's only
+ // used for logging purposes.
+ RefPtr<DecodePromise> HandleDecodedResult(
+ DecodePromise::ResolveOrRejectValue&& aValue, MediaRawData* aRaw);
+ void PrepareTrimmers(MediaRawData* aRaw);
+ const RefPtr<MediaDataDecoder> mDecoder;
+ nsCOMPtr<nsISerialEventTarget> mThread;
+ AutoTArray<Maybe<media::TimeInterval>, 2> mTrimmers;
+};
+
+} // namespace mozilla
+
+#endif // AudioTrimmer_h_
diff --git a/dom/media/platforms/wrappers/MediaChangeMonitor.cpp b/dom/media/platforms/wrappers/MediaChangeMonitor.cpp
new file mode 100644
index 0000000000..17387204ed
--- /dev/null
+++ b/dom/media/platforms/wrappers/MediaChangeMonitor.cpp
@@ -0,0 +1,951 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaChangeMonitor.h"
+
+#include "AnnexB.h"
+#include "H264.h"
+#include "GeckoProfiler.h"
+#include "ImageContainer.h"
+#include "MP4Decoder.h"
+#include "MediaInfo.h"
+#include "PDMFactory.h"
+#include "VPXDecoder.h"
+#ifdef MOZ_AV1
+# include "AOMDecoder.h"
+#endif
+#include "gfxUtils.h"
+#include "mozilla/ProfilerMarkers.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/TaskQueue.h"
+
+namespace mozilla {
+
+extern LazyLogModule gMediaDecoderLog;
+
+#define LOG(x, ...) \
+ MOZ_LOG(gMediaDecoderLog, LogLevel::Debug, (x, ##__VA_ARGS__))
+
+// H264ChangeMonitor is used to ensure that only AVCC or AnnexB is fed to the
+// underlying MediaDataDecoder. The H264ChangeMonitor allows playback of content
+// where the SPS NAL may not be provided in the init segment (e.g. AVC3 or Annex
+// B) H264ChangeMonitor will monitor the input data, and will delay creation of
+// the MediaDataDecoder until a SPS and PPS NALs have been extracted.
+
+class H264ChangeMonitor : public MediaChangeMonitor::CodecChangeMonitor {
+ public:
+ explicit H264ChangeMonitor(const VideoInfo& aInfo, bool aFullParsing)
+ : mCurrentConfig(aInfo), mFullParsing(aFullParsing) {
+ if (CanBeInstantiated()) {
+ UpdateConfigFromExtraData(aInfo.mExtraData);
+ }
+ }
+
+ bool CanBeInstantiated() const override {
+ return H264::HasSPS(mCurrentConfig.mExtraData);
+ }
+
+ MediaResult CheckForChange(MediaRawData* aSample) override {
+ // To be usable we need to convert the sample to 4 bytes NAL size AVCC.
+ if (!AnnexB::ConvertSampleToAVCC(aSample)) {
+ // We need AVCC content to be able to later parse the SPS.
+ // This is a no-op if the data is already AVCC.
+ return MediaResult(NS_ERROR_OUT_OF_MEMORY,
+ RESULT_DETAIL("ConvertSampleToAVCC"));
+ }
+
+ if (!AnnexB::IsAVCC(aSample)) {
+ return MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Invalid H264 content"));
+ }
+
+ RefPtr<MediaByteBuffer> extra_data =
+ aSample->mKeyframe || !mGotSPS || mFullParsing
+ ? H264::ExtractExtraData(aSample)
+ : nullptr;
+
+ if (!H264::HasSPS(extra_data) && !H264::HasSPS(mCurrentConfig.mExtraData)) {
+ // We don't have inband data and the original config didn't contain a SPS.
+ // We can't decode this content.
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ mGotSPS = true;
+
+ if (!H264::HasSPS(extra_data)) {
+ // This sample doesn't contain inband SPS/PPS
+ // We now check if the out of band one has changed.
+ // This scenario can currently only occur on Android with devices that can
+ // recycle a decoder.
+ bool hasOutOfBandExtraData = H264::HasSPS(aSample->mExtraData);
+ if (!hasOutOfBandExtraData || !mPreviousExtraData ||
+ H264::CompareExtraData(aSample->mExtraData, mPreviousExtraData)) {
+ if (hasOutOfBandExtraData && !mPreviousExtraData) {
+ // We are decoding the first sample, store the out of band sample's
+ // extradata so that we can check for future change.
+ mPreviousExtraData = aSample->mExtraData;
+ }
+ return NS_OK;
+ }
+ extra_data = aSample->mExtraData;
+ } else if (H264::CompareExtraData(extra_data, mCurrentConfig.mExtraData)) {
+ return NS_OK;
+ }
+
+ // Store the sample's extradata so we don't trigger a false positive
+ // with the out of band test on the next sample.
+ mPreviousExtraData = aSample->mExtraData;
+ UpdateConfigFromExtraData(extra_data);
+
+ PROFILER_MARKER_TEXT("H264 Stream Change", MEDIA_PLAYBACK, {},
+ "H264ChangeMonitor::CheckForChange has detected a "
+ "change in the stream and will request a new decoder");
+ return NS_ERROR_DOM_MEDIA_NEED_NEW_DECODER;
+ }
+
+ const TrackInfo& Config() const override { return mCurrentConfig; }
+
+ MediaResult PrepareSample(MediaDataDecoder::ConversionRequired aConversion,
+ MediaRawData* aSample,
+ bool aNeedKeyFrame) override {
+ MOZ_DIAGNOSTIC_ASSERT(
+ aConversion == MediaDataDecoder::ConversionRequired::kNeedAnnexB ||
+ aConversion == MediaDataDecoder::ConversionRequired::kNeedAVCC,
+ "Conversion must be either AVCC or AnnexB");
+
+ aSample->mExtraData = mCurrentConfig.mExtraData;
+ aSample->mTrackInfo = mTrackInfo;
+
+ if (aConversion == MediaDataDecoder::ConversionRequired::kNeedAnnexB) {
+ auto res = AnnexB::ConvertSampleToAnnexB(aSample, aNeedKeyFrame);
+ if (res.isErr()) {
+ return MediaResult(res.unwrapErr(),
+ RESULT_DETAIL("ConvertSampleToAnnexB"));
+ }
+ }
+
+ return NS_OK;
+ }
+
+ private:
+ void UpdateConfigFromExtraData(MediaByteBuffer* aExtraData) {
+ SPSData spsdata;
+ if (H264::DecodeSPSFromExtraData(aExtraData, spsdata) &&
+ spsdata.pic_width > 0 && spsdata.pic_height > 0) {
+ H264::EnsureSPSIsSane(spsdata);
+ mCurrentConfig.mImage.width = spsdata.pic_width;
+ mCurrentConfig.mImage.height = spsdata.pic_height;
+ mCurrentConfig.mDisplay.width = spsdata.display_width;
+ mCurrentConfig.mDisplay.height = spsdata.display_height;
+ mCurrentConfig.mColorDepth = spsdata.ColorDepth();
+ mCurrentConfig.mColorSpace = Some(spsdata.ColorSpace());
+ // spsdata.colour_primaries has the same values as
+ // gfx::CICP::ColourPrimaries.
+ mCurrentConfig.mColorPrimaries = gfxUtils::CicpToColorPrimaries(
+ static_cast<gfx::CICP::ColourPrimaries>(spsdata.colour_primaries),
+ gMediaDecoderLog);
+ // spsdata.transfer_characteristics has the same values as
+ // gfx::CICP::TransferCharacteristics.
+ mCurrentConfig.mTransferFunction = gfxUtils::CicpToTransferFunction(
+ static_cast<gfx::CICP::TransferCharacteristics>(
+ spsdata.transfer_characteristics));
+ mCurrentConfig.mColorRange = spsdata.video_full_range_flag
+ ? gfx::ColorRange::FULL
+ : gfx::ColorRange::LIMITED;
+ }
+ mCurrentConfig.mExtraData = aExtraData;
+ mTrackInfo = new TrackInfoSharedPtr(mCurrentConfig, mStreamID++);
+ }
+
+ VideoInfo mCurrentConfig;
+ uint32_t mStreamID = 0;
+ const bool mFullParsing;
+ bool mGotSPS = false;
+ RefPtr<TrackInfoSharedPtr> mTrackInfo;
+ RefPtr<MediaByteBuffer> mPreviousExtraData;
+};
+
+// Gets the pixel aspect ratio from the decoded video size and the rendered
+// size.
+inline double GetPixelAspectRatio(const gfx::IntSize& aImage,
+ const gfx::IntSize& aDisplay) {
+ return (static_cast<double>(aDisplay.Width()) / aImage.Width()) /
+ (static_cast<double>(aDisplay.Height()) / aImage.Height());
+}
+
+// Returns the render size based on the PAR and the new image size.
+inline gfx::IntSize ApplyPixelAspectRatio(double aPixelAspectRatio,
+ const gfx::IntSize& aImage) {
+ return gfx::IntSize(static_cast<int32_t>(aImage.Width() * aPixelAspectRatio),
+ aImage.Height());
+}
+
+class VPXChangeMonitor : public MediaChangeMonitor::CodecChangeMonitor {
+ public:
+ explicit VPXChangeMonitor(const VideoInfo& aInfo)
+ : mCurrentConfig(aInfo),
+ mCodec(VPXDecoder::IsVP8(aInfo.mMimeType) ? VPXDecoder::Codec::VP8
+ : VPXDecoder::Codec::VP9),
+ mPixelAspectRatio(GetPixelAspectRatio(aInfo.mImage, aInfo.mDisplay)) {
+ mTrackInfo = new TrackInfoSharedPtr(mCurrentConfig, mStreamID++);
+
+ if (mCurrentConfig.mExtraData && !mCurrentConfig.mExtraData->IsEmpty()) {
+ // If we're passed VP codec configuration, store it so that we can
+ // instantiate the decoder on init.
+ VPXDecoder::VPXStreamInfo vpxInfo;
+ vpxInfo.mImage = mCurrentConfig.mImage;
+ vpxInfo.mDisplay = mCurrentConfig.mDisplay;
+ VPXDecoder::ReadVPCCBox(vpxInfo, mCurrentConfig.mExtraData);
+ mInfo = Some(vpxInfo);
+ }
+ }
+
+ bool CanBeInstantiated() const override {
+ // We want to see at least one sample before we create a decoder so that we
+ // can create the vpcC content on mCurrentConfig.mExtraData.
+ return mCodec == VPXDecoder::Codec::VP8 || mInfo ||
+ mCurrentConfig.mCrypto.IsEncrypted();
+ }
+
+ MediaResult CheckForChange(MediaRawData* aSample) override {
+ // Don't look at encrypted content.
+ if (aSample->mCrypto.IsEncrypted()) {
+ return NS_OK;
+ }
+ auto dataSpan = Span<const uint8_t>(aSample->Data(), aSample->Size());
+
+ // We don't trust the keyframe flag as set on the MediaRawData.
+ VPXDecoder::VPXStreamInfo info;
+ if (!VPXDecoder::GetStreamInfo(dataSpan, info, mCodec)) {
+ return NS_ERROR_DOM_MEDIA_DECODE_ERR;
+ }
+ // For both VP8 and VP9, we only look for resolution changes
+ // on keyframes. Other resolution changes are invalid.
+ if (!info.mKeyFrame) {
+ return NS_OK;
+ }
+
+ nsresult rv = NS_OK;
+ if (mInfo) {
+ if (mInfo.ref().IsCompatible(info)) {
+ return rv;
+ }
+ // We can't properly determine the image rect once we've had a resolution
+ // change.
+ mCurrentConfig.ResetImageRect();
+ PROFILER_MARKER_TEXT(
+ "VPX Stream Change", MEDIA_PLAYBACK, {},
+ "VPXChangeMonitor::CheckForChange has detected a change in the "
+ "stream and will request a new decoder");
+ rv = NS_ERROR_DOM_MEDIA_NEED_NEW_DECODER;
+ } else if (mCurrentConfig.mImage != info.mImage ||
+ mCurrentConfig.mDisplay != info.mDisplay) {
+ // We can't properly determine the image rect if we're changing
+ // resolution based on sample information.
+ mCurrentConfig.ResetImageRect();
+ PROFILER_MARKER_TEXT("VPX Stream Init Discrepancy", MEDIA_PLAYBACK, {},
+ "VPXChangeMonitor::CheckForChange has detected a "
+ "discrepancy between initialization data and stream "
+ "content and will request a new decoder");
+ rv = NS_ERROR_DOM_MEDIA_NEED_NEW_DECODER;
+ }
+
+ LOG("Detect inband %s resolution changes, image (%" PRId32 ",%" PRId32
+ ")->(%" PRId32 ",%" PRId32 "), display (%" PRId32 ",%" PRId32
+ ")->(%" PRId32 ",%" PRId32 " %s)",
+ mCodec == VPXDecoder::Codec::VP9 ? "VP9" : "VP8",
+ mCurrentConfig.mImage.Width(), mCurrentConfig.mImage.Height(),
+ info.mImage.Width(), info.mImage.Height(),
+ mCurrentConfig.mDisplay.Width(), mCurrentConfig.mDisplay.Height(),
+ info.mDisplay.Width(), info.mDisplay.Height(),
+ info.mDisplayAndImageDifferent ? "specified" : "unspecified");
+
+ mInfo = Some(info);
+ mCurrentConfig.mImage = info.mImage;
+ if (info.mDisplayAndImageDifferent) {
+ // If the flag to change the display size is set in the sequence, we
+ // set our original values to begin rescaling according to the new values.
+ mCurrentConfig.mDisplay = info.mDisplay;
+ mPixelAspectRatio = GetPixelAspectRatio(info.mImage, info.mDisplay);
+ } else {
+ mCurrentConfig.mDisplay =
+ ApplyPixelAspectRatio(mPixelAspectRatio, info.mImage);
+ }
+
+ mCurrentConfig.mColorDepth = gfx::ColorDepthForBitDepth(info.mBitDepth);
+ mCurrentConfig.mColorSpace = Some(info.ColorSpace());
+ // VPX bitstream doesn't specify color primaries.
+
+ // We don't update the transfer function here, because VPX bitstream
+ // doesn't specify the transfer function. Instead, we keep the transfer
+ // function (if any) that was set in mCurrentConfig when we were created.
+ // If a video changes colorspaces away from BT2020, we won't clear
+ // mTransferFunction, in case the video changes back to BT2020 and we
+ // need the value again.
+
+ mCurrentConfig.mColorRange = info.ColorRange();
+ if (mCodec == VPXDecoder::Codec::VP9) {
+ mCurrentConfig.mExtraData->ClearAndRetainStorage();
+ VPXDecoder::GetVPCCBox(mCurrentConfig.mExtraData, info);
+ }
+ mTrackInfo = new TrackInfoSharedPtr(mCurrentConfig, mStreamID++);
+
+ return rv;
+ }
+
+ const TrackInfo& Config() const override { return mCurrentConfig; }
+
+ MediaResult PrepareSample(MediaDataDecoder::ConversionRequired aConversion,
+ MediaRawData* aSample,
+ bool aNeedKeyFrame) override {
+ aSample->mTrackInfo = mTrackInfo;
+
+ return NS_OK;
+ }
+
+ private:
+ VideoInfo mCurrentConfig;
+ const VPXDecoder::Codec mCodec;
+ Maybe<VPXDecoder::VPXStreamInfo> mInfo;
+ uint32_t mStreamID = 0;
+ RefPtr<TrackInfoSharedPtr> mTrackInfo;
+ double mPixelAspectRatio;
+};
+
+#ifdef MOZ_AV1
+class AV1ChangeMonitor : public MediaChangeMonitor::CodecChangeMonitor {
+ public:
+ explicit AV1ChangeMonitor(const VideoInfo& aInfo)
+ : mCurrentConfig(aInfo),
+ mPixelAspectRatio(GetPixelAspectRatio(aInfo.mImage, aInfo.mDisplay)) {
+ mTrackInfo = new TrackInfoSharedPtr(mCurrentConfig, mStreamID++);
+
+ if (mCurrentConfig.mExtraData && !mCurrentConfig.mExtraData->IsEmpty()) {
+ // If we're passed AV1 codec configuration, store it so that we can
+ // instantiate a decoder in MediaChangeMonitor::Create.
+ AOMDecoder::AV1SequenceInfo seqInfo;
+ MediaResult seqHdrResult;
+ AOMDecoder::TryReadAV1CBox(mCurrentConfig.mExtraData, seqInfo,
+ seqHdrResult);
+ // If the av1C box doesn't include a sequence header specifying image
+ // size, keep the one provided by VideoInfo.
+ if (seqHdrResult.Code() != NS_OK) {
+ seqInfo.mImage = mCurrentConfig.mImage;
+ }
+
+ UpdateConfig(seqInfo);
+ }
+ }
+
+ bool CanBeInstantiated() const override {
+ // We want to have enough codec configuration to determine whether hardware
+ // decoding can be used before creating a decoder. The av1C box or a
+ // sequence header from a sample will contain this information.
+ return mInfo || mCurrentConfig.mCrypto.IsEncrypted();
+ }
+
+ void UpdateConfig(const AOMDecoder::AV1SequenceInfo& aInfo) {
+ mInfo = Some(aInfo);
+ mCurrentConfig.mColorDepth = gfx::ColorDepthForBitDepth(aInfo.mBitDepth);
+ mCurrentConfig.mColorSpace = gfxUtils::CicpToColorSpace(
+ aInfo.mColorSpace.mMatrix, aInfo.mColorSpace.mPrimaries,
+ gMediaDecoderLog);
+ mCurrentConfig.mColorPrimaries = gfxUtils::CicpToColorPrimaries(
+ aInfo.mColorSpace.mPrimaries, gMediaDecoderLog);
+ mCurrentConfig.mTransferFunction =
+ gfxUtils::CicpToTransferFunction(aInfo.mColorSpace.mTransfer);
+ mCurrentConfig.mColorRange = aInfo.mColorSpace.mRange;
+
+ if (mCurrentConfig.mImage != mInfo->mImage) {
+ gfx::IntSize newDisplay =
+ ApplyPixelAspectRatio(mPixelAspectRatio, aInfo.mImage);
+ LOG("AV1ChangeMonitor detected a resolution change in-band, image "
+ "(%" PRIu32 ",%" PRIu32 ")->(%" PRIu32 ",%" PRIu32
+ "), display (%" PRIu32 ",%" PRIu32 ")->(%" PRIu32 ",%" PRIu32
+ " from PAR)",
+ mCurrentConfig.mImage.Width(), mCurrentConfig.mImage.Height(),
+ aInfo.mImage.Width(), aInfo.mImage.Height(),
+ mCurrentConfig.mDisplay.Width(), mCurrentConfig.mDisplay.Height(),
+ newDisplay.Width(), newDisplay.Height());
+ mCurrentConfig.mImage = aInfo.mImage;
+ mCurrentConfig.mDisplay = newDisplay;
+ mCurrentConfig.ResetImageRect();
+ }
+
+ bool wroteSequenceHeader = false;
+ // Our headers should all be around the same size.
+ mCurrentConfig.mExtraData->ClearAndRetainStorage();
+ AOMDecoder::WriteAV1CBox(aInfo, mCurrentConfig.mExtraData.get(),
+ wroteSequenceHeader);
+ // Header should always be written ReadSequenceHeaderInfo succeeds.
+ MOZ_ASSERT(wroteSequenceHeader);
+ }
+
+ MediaResult CheckForChange(MediaRawData* aSample) override {
+ // Don't look at encrypted content.
+ if (aSample->mCrypto.IsEncrypted()) {
+ return NS_OK;
+ }
+ auto dataSpan = Span<const uint8_t>(aSample->Data(), aSample->Size());
+
+ // We don't trust the keyframe flag as set on the MediaRawData.
+ AOMDecoder::AV1SequenceInfo info;
+ MediaResult seqHdrResult =
+ AOMDecoder::ReadSequenceHeaderInfo(dataSpan, info);
+ nsresult seqHdrCode = seqHdrResult.Code();
+ if (seqHdrCode == NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA) {
+ return NS_OK;
+ }
+ if (seqHdrCode != NS_OK) {
+ LOG("AV1ChangeMonitor::CheckForChange read a corrupted sample: %s",
+ seqHdrResult.Description().get());
+ return seqHdrResult;
+ }
+
+ nsresult rv = NS_OK;
+ if (mInfo.isSome() &&
+ (mInfo->mProfile != info.mProfile ||
+ mInfo->ColorDepth() != info.ColorDepth() ||
+ mInfo->mMonochrome != info.mMonochrome ||
+ mInfo->mSubsamplingX != info.mSubsamplingX ||
+ mInfo->mSubsamplingY != info.mSubsamplingY ||
+ mInfo->mChromaSamplePosition != info.mChromaSamplePosition ||
+ mInfo->mImage != info.mImage)) {
+ PROFILER_MARKER_TEXT(
+ "AV1 Stream Change", MEDIA_PLAYBACK, {},
+ "AV1ChangeMonitor::CheckForChange has detected a change in a "
+ "stream and will request a new decoder");
+ LOG("AV1ChangeMonitor detected a change and requests a new decoder");
+ rv = NS_ERROR_DOM_MEDIA_NEED_NEW_DECODER;
+ }
+
+ UpdateConfig(info);
+
+ if (rv == NS_ERROR_DOM_MEDIA_NEED_NEW_DECODER) {
+ mTrackInfo = new TrackInfoSharedPtr(mCurrentConfig, mStreamID++);
+ }
+ return rv;
+ }
+
+ const TrackInfo& Config() const override { return mCurrentConfig; }
+
+ MediaResult PrepareSample(MediaDataDecoder::ConversionRequired aConversion,
+ MediaRawData* aSample,
+ bool aNeedKeyFrame) override {
+ aSample->mTrackInfo = mTrackInfo;
+ return NS_OK;
+ }
+
+ private:
+ VideoInfo mCurrentConfig;
+ Maybe<AOMDecoder::AV1SequenceInfo> mInfo;
+ uint32_t mStreamID = 0;
+ RefPtr<TrackInfoSharedPtr> mTrackInfo;
+ double mPixelAspectRatio;
+};
+#endif
+
+MediaChangeMonitor::MediaChangeMonitor(
+ PDMFactory* aPDMFactory,
+ UniquePtr<CodecChangeMonitor>&& aCodecChangeMonitor,
+ MediaDataDecoder* aDecoder, const CreateDecoderParams& aParams)
+ : mChangeMonitor(std::move(aCodecChangeMonitor)),
+ mPDMFactory(aPDMFactory),
+ mCurrentConfig(aParams.VideoConfig()),
+ mDecoder(aDecoder),
+ mParams(aParams) {}
+
+/* static */
+RefPtr<PlatformDecoderModule::CreateDecoderPromise> MediaChangeMonitor::Create(
+ PDMFactory* aPDMFactory, const CreateDecoderParams& aParams) {
+ UniquePtr<CodecChangeMonitor> changeMonitor;
+ const VideoInfo& currentConfig = aParams.VideoConfig();
+ if (VPXDecoder::IsVPX(currentConfig.mMimeType)) {
+ changeMonitor = MakeUnique<VPXChangeMonitor>(currentConfig);
+#ifdef MOZ_AV1
+ } else if (AOMDecoder::IsAV1(currentConfig.mMimeType)) {
+ changeMonitor = MakeUnique<AV1ChangeMonitor>(currentConfig);
+#endif
+ } else {
+ MOZ_ASSERT(MP4Decoder::IsH264(currentConfig.mMimeType));
+ changeMonitor = MakeUnique<H264ChangeMonitor>(
+ currentConfig, aParams.mOptions.contains(
+ CreateDecoderParams::Option::FullH264Parsing));
+ }
+
+ // The change monitor may have an updated track config. E.g. the h264 monitor
+ // may update the config after parsing extra data in the VideoInfo. Create a
+ // new set of params with the updated track info from our monitor and the
+ // other params for aParams and use that going forward.
+ const CreateDecoderParams updatedParams{changeMonitor->Config(), aParams};
+
+ RefPtr<MediaChangeMonitor> instance = new MediaChangeMonitor(
+ aPDMFactory, std::move(changeMonitor), nullptr, updatedParams);
+
+ if (instance->mChangeMonitor->CanBeInstantiated()) {
+ RefPtr<PlatformDecoderModule::CreateDecoderPromise> p =
+ instance->CreateDecoder()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [instance = RefPtr{instance}] {
+ return PlatformDecoderModule::CreateDecoderPromise::
+ CreateAndResolve(instance, __func__);
+ },
+ [](const MediaResult& aError) {
+ return PlatformDecoderModule::CreateDecoderPromise::
+ CreateAndReject(aError, __func__);
+ });
+ return p;
+ }
+
+ return PlatformDecoderModule::CreateDecoderPromise::CreateAndResolve(
+ instance, __func__);
+}
+
+MediaChangeMonitor::~MediaChangeMonitor() = default;
+
+RefPtr<MediaDataDecoder::InitPromise> MediaChangeMonitor::Init() {
+ mThread = GetCurrentSerialEventTarget();
+ if (mDecoder) {
+ RefPtr<InitPromise> p = mInitPromise.Ensure(__func__);
+ RefPtr<MediaChangeMonitor> self = this;
+ mDecoder->Init()
+ ->Then(GetCurrentSerialEventTarget(), __func__,
+ [self, this](InitPromise::ResolveOrRejectValue&& aValue) {
+ mInitPromiseRequest.Complete();
+ if (aValue.IsResolve()) {
+ mDecoderInitialized = true;
+ mConversionRequired = Some(mDecoder->NeedsConversion());
+ mCanRecycleDecoder = Some(CanRecycleDecoder());
+ }
+ return mInitPromise.ResolveOrRejectIfExists(std::move(aValue),
+ __func__);
+ })
+ ->Track(mInitPromiseRequest);
+ return p;
+ }
+
+ // We haven't been able to initialize a decoder due to missing
+ // extradata.
+ return MediaDataDecoder::InitPromise::CreateAndResolve(TrackType::kVideoTrack,
+ __func__);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> MediaChangeMonitor::Decode(
+ MediaRawData* aSample) {
+ AssertOnThread();
+ MOZ_RELEASE_ASSERT(mFlushPromise.IsEmpty(),
+ "Flush operation didn't complete");
+
+ MOZ_RELEASE_ASSERT(
+ !mDecodePromiseRequest.Exists() && !mInitPromiseRequest.Exists(),
+ "Can't request a new decode until previous one completed");
+
+ MediaResult rv = CheckForChange(aSample);
+
+ if (rv == NS_ERROR_NOT_INITIALIZED) {
+ // We are missing the required init data to create the decoder.
+ if (mParams.mOptions.contains(
+ CreateDecoderParams::Option::ErrorIfNoInitializationData)) {
+ // This frame can't be decoded and should be treated as an error.
+ return DecodePromise::CreateAndReject(rv, __func__);
+ }
+ // Swallow the frame, and await delivery of init data.
+ return DecodePromise::CreateAndResolve(DecodedData(), __func__);
+ }
+ if (rv == NS_ERROR_DOM_MEDIA_INITIALIZING_DECODER) {
+ // The decoder is pending initialization.
+ RefPtr<DecodePromise> p = mDecodePromise.Ensure(__func__);
+ return p;
+ }
+
+ if (NS_FAILED(rv)) {
+ return DecodePromise::CreateAndReject(rv, __func__);
+ }
+
+ if (mNeedKeyframe && !aSample->mKeyframe) {
+ return DecodePromise::CreateAndResolve(DecodedData(), __func__);
+ }
+
+ rv = mChangeMonitor->PrepareSample(*mConversionRequired, aSample,
+ mNeedKeyframe);
+ if (NS_FAILED(rv)) {
+ return DecodePromise::CreateAndReject(rv, __func__);
+ }
+
+ mNeedKeyframe = false;
+
+ return mDecoder->Decode(aSample);
+}
+
+RefPtr<MediaDataDecoder::FlushPromise> MediaChangeMonitor::Flush() {
+ AssertOnThread();
+ mDecodePromiseRequest.DisconnectIfExists();
+ mDecodePromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ mNeedKeyframe = true;
+ mPendingFrames.Clear();
+
+ MOZ_RELEASE_ASSERT(mFlushPromise.IsEmpty(), "Previous flush didn't complete");
+
+ /*
+ When we detect a change of content in the byte stream, we first drain the
+ current decoder (1), flush (2), shut it down (3) create a new decoder (4)
+ and initialize it (5). It is possible for MediaChangeMonitor::Flush to be
+ called during any of those times. If during (1):
+ - mDrainRequest will not be empty.
+ - The old decoder can still be used, with the current extradata as
+ stored in mCurrentConfig.mExtraData.
+
+ If during (2):
+ - mFlushRequest will not be empty.
+ - The old decoder can still be used, with the current extradata as
+ stored in mCurrentConfig.mExtraData.
+
+ If during (3):
+ - mShutdownRequest won't be empty.
+ - mDecoder is empty.
+ - The old decoder is no longer referenced by the MediaChangeMonitor.
+
+ If during (4):
+ - mDecoderRequest won't be empty.
+ - mDecoder is not set. Steps will continue to (5) to set and initialize it
+
+ If during (5):
+ - mInitPromiseRequest won't be empty.
+ - mDecoder is set but not usable yet.
+ */
+
+ if (mDrainRequest.Exists() || mFlushRequest.Exists() ||
+ mShutdownRequest.Exists() || mDecoderRequest.Exists() ||
+ mInitPromiseRequest.Exists()) {
+ // We let the current decoder complete and will resume after.
+ RefPtr<FlushPromise> p = mFlushPromise.Ensure(__func__);
+ return p;
+ }
+ if (mDecoder && mDecoderInitialized) {
+ return mDecoder->Flush();
+ }
+ return FlushPromise::CreateAndResolve(true, __func__);
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> MediaChangeMonitor::Drain() {
+ AssertOnThread();
+ MOZ_RELEASE_ASSERT(!mDrainRequest.Exists());
+ mNeedKeyframe = true;
+ if (mDecoder) {
+ return mDecoder->Drain();
+ }
+ return DecodePromise::CreateAndResolve(DecodedData(), __func__);
+}
+
+RefPtr<ShutdownPromise> MediaChangeMonitor::Shutdown() {
+ AssertOnThread();
+ mInitPromiseRequest.DisconnectIfExists();
+ mInitPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ mDecodePromiseRequest.DisconnectIfExists();
+ mDecodePromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ mDrainRequest.DisconnectIfExists();
+ mFlushRequest.DisconnectIfExists();
+ mFlushPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+ mShutdownRequest.DisconnectIfExists();
+
+ if (mShutdownPromise) {
+ // We have a shutdown in progress, return that promise instead as we can't
+ // shutdown a decoder twice.
+ RefPtr<ShutdownPromise> p = std::move(mShutdownPromise);
+ return p;
+ }
+ return ShutdownDecoder();
+}
+
+RefPtr<ShutdownPromise> MediaChangeMonitor::ShutdownDecoder() {
+ AssertOnThread();
+ mConversionRequired.reset();
+ if (mDecoder) {
+ RefPtr<MediaDataDecoder> decoder = std::move(mDecoder);
+ return decoder->Shutdown();
+ }
+ return ShutdownPromise::CreateAndResolve(true, __func__);
+}
+
+bool MediaChangeMonitor::IsHardwareAccelerated(
+ nsACString& aFailureReason) const {
+ if (mDecoder) {
+ return mDecoder->IsHardwareAccelerated(aFailureReason);
+ }
+#ifdef MOZ_APPLEMEDIA
+ // On mac, we can assume H264 is hardware accelerated for now.
+ // This allows MediaCapabilities to report that playback will be smooth.
+ // Which will always be.
+ return true;
+#else
+ return MediaDataDecoder::IsHardwareAccelerated(aFailureReason);
+#endif
+}
+
+void MediaChangeMonitor::SetSeekThreshold(const media::TimeUnit& aTime) {
+ if (mDecoder) {
+ mDecoder->SetSeekThreshold(aTime);
+ } else {
+ MediaDataDecoder::SetSeekThreshold(aTime);
+ }
+}
+
+RefPtr<MediaChangeMonitor::CreateDecoderPromise>
+MediaChangeMonitor::CreateDecoder() {
+ mCurrentConfig = *mChangeMonitor->Config().GetAsVideoInfo();
+ RefPtr<CreateDecoderPromise> p =
+ mPDMFactory
+ ->CreateDecoder(
+ {mCurrentConfig, mParams, CreateDecoderParams::NoWrapper(true)})
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr{this}, this](RefPtr<MediaDataDecoder>&& aDecoder) {
+ mDecoder = std::move(aDecoder);
+ DDLINKCHILD("decoder", mDecoder.get());
+ return CreateDecoderPromise::CreateAndResolve(true, __func__);
+ },
+ [self = RefPtr{this}](const MediaResult& aError) {
+ return CreateDecoderPromise::CreateAndReject(aError, __func__);
+ });
+
+ mDecoderInitialized = false;
+ mNeedKeyframe = true;
+
+ return p;
+}
+
+MediaResult MediaChangeMonitor::CreateDecoderAndInit(MediaRawData* aSample) {
+ MOZ_ASSERT(mThread && mThread->IsOnCurrentThread());
+
+ MediaResult rv = mChangeMonitor->CheckForChange(aSample);
+ if (!NS_SUCCEEDED(rv) && rv != NS_ERROR_DOM_MEDIA_NEED_NEW_DECODER) {
+ return rv;
+ }
+
+ if (!mChangeMonitor->CanBeInstantiated()) {
+ // Nothing found yet, will try again later.
+ return NS_ERROR_NOT_INITIALIZED;
+ }
+
+ CreateDecoder()
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr{this}, this, sample = RefPtr{aSample}] {
+ mDecoderRequest.Complete();
+ mDecoder->Init()
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self, sample, this](const TrackType aTrackType) {
+ mInitPromiseRequest.Complete();
+ mDecoderInitialized = true;
+ mConversionRequired = Some(mDecoder->NeedsConversion());
+ mCanRecycleDecoder = Some(CanRecycleDecoder());
+
+ if (!mFlushPromise.IsEmpty()) {
+ // A Flush is pending, abort the current operation.
+ mFlushPromise.Resolve(true, __func__);
+ return;
+ }
+
+ DecodeFirstSample(sample);
+ },
+ [self, this](const MediaResult& aError) {
+ mInitPromiseRequest.Complete();
+
+ if (!mFlushPromise.IsEmpty()) {
+ // A Flush is pending, abort the current operation.
+ mFlushPromise.Reject(aError, __func__);
+ return;
+ }
+
+ mDecodePromise.Reject(
+ MediaResult(
+ aError.Code(),
+ RESULT_DETAIL("Unable to initialize decoder")),
+ __func__);
+ })
+ ->Track(mInitPromiseRequest);
+ },
+ [self = RefPtr{this}, this](const MediaResult& aError) {
+ mDecoderRequest.Complete();
+ if (!mFlushPromise.IsEmpty()) {
+ // A Flush is pending, abort the current operation.
+ mFlushPromise.Reject(aError, __func__);
+ return;
+ }
+ mDecodePromise.Reject(
+ MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Unable to create decoder")),
+ __func__);
+ })
+ ->Track(mDecoderRequest);
+ return NS_ERROR_DOM_MEDIA_INITIALIZING_DECODER;
+}
+
+bool MediaChangeMonitor::CanRecycleDecoder() const {
+ MOZ_ASSERT(mDecoder);
+ return StaticPrefs::media_decoder_recycle_enabled() &&
+ mDecoder->SupportDecoderRecycling();
+}
+
+void MediaChangeMonitor::DecodeFirstSample(MediaRawData* aSample) {
+ // We feed all the data to AnnexB decoder as a non-keyframe could contain
+ // the SPS/PPS when used with WebRTC and this data is needed by the decoder.
+ if (mNeedKeyframe && !aSample->mKeyframe &&
+ *mConversionRequired != ConversionRequired::kNeedAnnexB) {
+ mDecodePromise.Resolve(std::move(mPendingFrames), __func__);
+ mPendingFrames = DecodedData();
+ return;
+ }
+
+ MediaResult rv = mChangeMonitor->PrepareSample(*mConversionRequired, aSample,
+ mNeedKeyframe);
+
+ if (NS_FAILED(rv)) {
+ mDecodePromise.Reject(rv, __func__);
+ return;
+ }
+
+ if (aSample->mKeyframe) {
+ mNeedKeyframe = false;
+ }
+
+ RefPtr<MediaChangeMonitor> self = this;
+ mDecoder->Decode(aSample)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self, this](MediaDataDecoder::DecodedData&& aResults) {
+ mDecodePromiseRequest.Complete();
+ mPendingFrames.AppendElements(std::move(aResults));
+ mDecodePromise.Resolve(std::move(mPendingFrames), __func__);
+ mPendingFrames = DecodedData();
+ },
+ [self, this](const MediaResult& aError) {
+ mDecodePromiseRequest.Complete();
+ mDecodePromise.Reject(aError, __func__);
+ })
+ ->Track(mDecodePromiseRequest);
+}
+
+MediaResult MediaChangeMonitor::CheckForChange(MediaRawData* aSample) {
+ if (!mDecoder) {
+ return CreateDecoderAndInit(aSample);
+ }
+
+ MediaResult rv = mChangeMonitor->CheckForChange(aSample);
+
+ if (NS_SUCCEEDED(rv) || rv != NS_ERROR_DOM_MEDIA_NEED_NEW_DECODER) {
+ return rv;
+ }
+
+ if (*mCanRecycleDecoder) {
+ // Do not recreate the decoder, reuse it.
+ mNeedKeyframe = true;
+ return NS_OK;
+ }
+
+ // The content has changed, signal to drain the current decoder and once done
+ // create a new one.
+ DrainThenFlushDecoder(aSample);
+ return NS_ERROR_DOM_MEDIA_INITIALIZING_DECODER;
+}
+
+void MediaChangeMonitor::DrainThenFlushDecoder(MediaRawData* aPendingSample) {
+ AssertOnThread();
+ MOZ_ASSERT(mDecoderInitialized);
+ RefPtr<MediaRawData> sample = aPendingSample;
+ RefPtr<MediaChangeMonitor> self = this;
+ mDecoder->Drain()
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self, sample, this](MediaDataDecoder::DecodedData&& aResults) {
+ mDrainRequest.Complete();
+ if (!mFlushPromise.IsEmpty()) {
+ // A Flush is pending, abort the current operation.
+ mFlushPromise.Resolve(true, __func__);
+ return;
+ }
+ if (aResults.Length() > 0) {
+ mPendingFrames.AppendElements(std::move(aResults));
+ DrainThenFlushDecoder(sample);
+ return;
+ }
+ // We've completed the draining operation, we can now flush the
+ // decoder.
+ FlushThenShutdownDecoder(sample);
+ },
+ [self, this](const MediaResult& aError) {
+ mDrainRequest.Complete();
+ if (!mFlushPromise.IsEmpty()) {
+ // A Flush is pending, abort the current operation.
+ mFlushPromise.Reject(aError, __func__);
+ return;
+ }
+ mDecodePromise.Reject(aError, __func__);
+ })
+ ->Track(mDrainRequest);
+}
+
+void MediaChangeMonitor::FlushThenShutdownDecoder(
+ MediaRawData* aPendingSample) {
+ AssertOnThread();
+ MOZ_ASSERT(mDecoderInitialized);
+ RefPtr<MediaRawData> sample = aPendingSample;
+ RefPtr<MediaChangeMonitor> self = this;
+ mDecoder->Flush()
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self, sample, this]() {
+ mFlushRequest.Complete();
+
+ if (!mFlushPromise.IsEmpty()) {
+ // A Flush is pending, abort the current operation.
+ mFlushPromise.Resolve(true, __func__);
+ return;
+ }
+
+ mShutdownPromise = ShutdownDecoder();
+ mShutdownPromise
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self, sample, this]() {
+ mShutdownRequest.Complete();
+ mShutdownPromise = nullptr;
+
+ if (!mFlushPromise.IsEmpty()) {
+ // A Flush is pending, abort the current
+ // operation.
+ mFlushPromise.Resolve(true, __func__);
+ return;
+ }
+
+ MediaResult rv = CreateDecoderAndInit(sample);
+ if (rv == NS_ERROR_DOM_MEDIA_INITIALIZING_DECODER) {
+ // All good so far, will continue later.
+ return;
+ }
+ MOZ_ASSERT(NS_FAILED(rv));
+ mDecodePromise.Reject(rv, __func__);
+ return;
+ },
+ [] { MOZ_CRASH("Can't reach here'"); })
+ ->Track(mShutdownRequest);
+ },
+ [self, this](const MediaResult& aError) {
+ mFlushRequest.Complete();
+ if (!mFlushPromise.IsEmpty()) {
+ // A Flush is pending, abort the current operation.
+ mFlushPromise.Reject(aError, __func__);
+ return;
+ }
+ mDecodePromise.Reject(aError, __func__);
+ })
+ ->Track(mFlushRequest);
+}
+
+#undef LOG
+
+} // namespace mozilla
diff --git a/dom/media/platforms/wrappers/MediaChangeMonitor.h b/dom/media/platforms/wrappers/MediaChangeMonitor.h
new file mode 100644
index 0000000000..a85c23fcb2
--- /dev/null
+++ b/dom/media/platforms/wrappers/MediaChangeMonitor.h
@@ -0,0 +1,142 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_H264Converter_h
+#define mozilla_H264Converter_h
+
+#include "PDMFactory.h"
+#include "PlatformDecoderModule.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/UniquePtr.h"
+
+namespace mozilla {
+
+DDLoggedTypeDeclNameAndBase(MediaChangeMonitor, MediaDataDecoder);
+
+// MediaChangeMonitor is a MediaDataDecoder wrapper used to ensure that
+// only one type of content is fed to the underlying MediaDataDecoder.
+// The MediaChangeMonitor allows playback of content where some out of band
+// extra data (such as SPS NAL for H264 content) may not be provided in the
+// init segment (e.g. AVC3 or Annex B) MediaChangeMonitor will monitor the
+// input data, and will delay creation of the MediaDataDecoder until such out
+// of band have been extracted should the underlying decoder required it.
+
+class MediaChangeMonitor final
+ : public MediaDataDecoder,
+ public DecoderDoctorLifeLogger<MediaChangeMonitor> {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaChangeMonitor, final);
+
+ static RefPtr<PlatformDecoderModule::CreateDecoderPromise> Create(
+ PDMFactory* aPDMFactory, const CreateDecoderParams& aParams);
+
+ RefPtr<InitPromise> Init() override;
+ RefPtr<DecodePromise> Decode(MediaRawData* aSample) override;
+ RefPtr<DecodePromise> Drain() override;
+ RefPtr<FlushPromise> Flush() override;
+ RefPtr<ShutdownPromise> Shutdown() override;
+ bool IsHardwareAccelerated(nsACString& aFailureReason) const override;
+ nsCString GetDescriptionName() const override {
+ if (mDecoder) {
+ return mDecoder->GetDescriptionName();
+ }
+ return "MediaChangeMonitor decoder (pending)"_ns;
+ }
+ nsCString GetProcessName() const override {
+ if (mDecoder) {
+ return mDecoder->GetProcessName();
+ }
+ return "MediaChangeMonitor"_ns;
+ }
+ nsCString GetCodecName() const override {
+ if (mDecoder) {
+ return mDecoder->GetCodecName();
+ }
+ return "MediaChangeMonitor"_ns;
+ }
+ void SetSeekThreshold(const media::TimeUnit& aTime) override;
+ bool SupportDecoderRecycling() const override {
+ if (mDecoder) {
+ return mDecoder->SupportDecoderRecycling();
+ }
+ return false;
+ }
+
+ ConversionRequired NeedsConversion() const override {
+ if (mDecoder) {
+ return mDecoder->NeedsConversion();
+ }
+ // Default so no conversion is performed.
+ return ConversionRequired::kNeedNone;
+ }
+
+ class CodecChangeMonitor {
+ public:
+ virtual bool CanBeInstantiated() const = 0;
+ virtual MediaResult CheckForChange(MediaRawData* aSample) = 0;
+ virtual const TrackInfo& Config() const = 0;
+ virtual MediaResult PrepareSample(
+ MediaDataDecoder::ConversionRequired aConversion, MediaRawData* aSample,
+ bool aNeedKeyFrame) = 0;
+ virtual ~CodecChangeMonitor() = default;
+ };
+
+ private:
+ MediaChangeMonitor(PDMFactory* aPDMFactory,
+ UniquePtr<CodecChangeMonitor>&& aCodecChangeMonitor,
+ MediaDataDecoder* aDecoder,
+ const CreateDecoderParams& aParams);
+ virtual ~MediaChangeMonitor();
+
+ void AssertOnThread() const {
+ // mThread may not be set if Init hasn't been called first.
+ MOZ_ASSERT(!mThread || mThread->IsOnCurrentThread());
+ }
+
+ bool CanRecycleDecoder() const;
+
+ typedef MozPromise<bool, MediaResult, true /* exclusive */>
+ CreateDecoderPromise;
+ // Will create the required MediaDataDecoder if need AVCC and we have a SPS
+ // NAL. Returns NS_ERROR_FAILURE if error is permanent and can't be recovered
+ // and will set mError accordingly.
+ RefPtr<CreateDecoderPromise> CreateDecoder();
+ MediaResult CreateDecoderAndInit(MediaRawData* aSample);
+ MediaResult CheckForChange(MediaRawData* aSample);
+
+ void DecodeFirstSample(MediaRawData* aSample);
+ void DrainThenFlushDecoder(MediaRawData* aPendingSample);
+ void FlushThenShutdownDecoder(MediaRawData* aPendingSample);
+ RefPtr<ShutdownPromise> ShutdownDecoder();
+
+ UniquePtr<CodecChangeMonitor> mChangeMonitor;
+ RefPtr<PDMFactory> mPDMFactory;
+ VideoInfo mCurrentConfig;
+ nsCOMPtr<nsISerialEventTarget> mThread;
+ RefPtr<MediaDataDecoder> mDecoder;
+ MozPromiseRequestHolder<CreateDecoderPromise> mDecoderRequest;
+ MozPromiseRequestHolder<InitPromise> mInitPromiseRequest;
+ MozPromiseHolder<InitPromise> mInitPromise;
+ MozPromiseRequestHolder<DecodePromise> mDecodePromiseRequest;
+ MozPromiseHolder<DecodePromise> mDecodePromise;
+ MozPromiseRequestHolder<FlushPromise> mFlushRequest;
+ MediaDataDecoder::DecodedData mPendingFrames;
+ MozPromiseRequestHolder<DecodePromise> mDrainRequest;
+ MozPromiseRequestHolder<ShutdownPromise> mShutdownRequest;
+ RefPtr<ShutdownPromise> mShutdownPromise;
+ MozPromiseHolder<FlushPromise> mFlushPromise;
+
+ bool mNeedKeyframe = true;
+ Maybe<bool> mCanRecycleDecoder;
+ Maybe<MediaDataDecoder::ConversionRequired> mConversionRequired;
+ bool mDecoderInitialized = false;
+ const CreateDecoderParamsForAsync mParams;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_H264Converter_h
diff --git a/dom/media/platforms/wrappers/MediaDataDecoderProxy.cpp b/dom/media/platforms/wrappers/MediaDataDecoderProxy.cpp
new file mode 100644
index 0000000000..dcb31b0899
--- /dev/null
+++ b/dom/media/platforms/wrappers/MediaDataDecoderProxy.cpp
@@ -0,0 +1,149 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaDataDecoderProxy.h"
+
+namespace mozilla {
+
+RefPtr<MediaDataDecoder::InitPromise> MediaDataDecoderProxy::Init() {
+ MOZ_ASSERT(!mIsShutdown);
+
+ if (!mProxyThread || mProxyThread->IsOnCurrentThread()) {
+ return mProxyDecoder->Init();
+ }
+ return InvokeAsync(mProxyThread, __func__, [self = RefPtr{this}] {
+ return self->mProxyDecoder->Init();
+ });
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> MediaDataDecoderProxy::Decode(
+ MediaRawData* aSample) {
+ MOZ_ASSERT(!mIsShutdown);
+
+ if (!mProxyThread || mProxyThread->IsOnCurrentThread()) {
+ return mProxyDecoder->Decode(aSample);
+ }
+ RefPtr<MediaRawData> sample = aSample;
+ return InvokeAsync(mProxyThread, __func__, [self = RefPtr{this}, sample] {
+ return self->mProxyDecoder->Decode(sample);
+ });
+}
+
+bool MediaDataDecoderProxy::CanDecodeBatch() const {
+ return mProxyDecoder->CanDecodeBatch();
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> MediaDataDecoderProxy::DecodeBatch(
+ nsTArray<RefPtr<MediaRawData>>&& aSamples) {
+ MOZ_ASSERT(!mIsShutdown);
+ if (!mProxyThread || mProxyThread->IsOnCurrentThread()) {
+ return mProxyDecoder->DecodeBatch(std::move(aSamples));
+ }
+ return InvokeAsync(
+ mProxyThread, __func__,
+ [self = RefPtr{this}, samples = std::move(aSamples)]() mutable {
+ return self->mProxyDecoder->DecodeBatch(std::move(samples));
+ });
+}
+
+RefPtr<MediaDataDecoder::FlushPromise> MediaDataDecoderProxy::Flush() {
+ MOZ_ASSERT(!mIsShutdown);
+
+ if (!mProxyThread || mProxyThread->IsOnCurrentThread()) {
+ return mProxyDecoder->Flush();
+ }
+ return InvokeAsync(mProxyThread, __func__, [self = RefPtr{this}] {
+ return self->mProxyDecoder->Flush();
+ });
+}
+
+RefPtr<MediaDataDecoder::DecodePromise> MediaDataDecoderProxy::Drain() {
+ MOZ_ASSERT(!mIsShutdown);
+
+ if (!mProxyThread || mProxyThread->IsOnCurrentThread()) {
+ return mProxyDecoder->Drain();
+ }
+ return InvokeAsync(mProxyThread, __func__, [self = RefPtr{this}] {
+ return self->mProxyDecoder->Drain();
+ });
+}
+
+RefPtr<ShutdownPromise> MediaDataDecoderProxy::Shutdown() {
+ MOZ_ASSERT(!mIsShutdown);
+
+#if defined(DEBUG)
+ mIsShutdown = true;
+#endif
+
+ if (!mProxyThread || mProxyThread->IsOnCurrentThread()) {
+ return mProxyDecoder->Shutdown();
+ }
+ // We chain another promise to ensure that the proxied decoder gets destructed
+ // on the proxy thread.
+ return InvokeAsync(mProxyThread, __func__, [self = RefPtr{this}] {
+ RefPtr<ShutdownPromise> p = self->mProxyDecoder->Shutdown()->Then(
+ self->mProxyThread, __func__,
+ [self](const ShutdownPromise::ResolveOrRejectValue& aResult) {
+ self->mProxyDecoder = nullptr;
+ return ShutdownPromise::CreateAndResolveOrReject(aResult, __func__);
+ });
+ return p;
+ });
+}
+
+nsCString MediaDataDecoderProxy::GetDescriptionName() const {
+ MOZ_ASSERT(!mIsShutdown);
+
+ return mProxyDecoder->GetDescriptionName();
+}
+
+nsCString MediaDataDecoderProxy::GetProcessName() const {
+ MOZ_ASSERT(!mIsShutdown);
+
+ return mProxyDecoder->GetProcessName();
+}
+
+nsCString MediaDataDecoderProxy::GetCodecName() const {
+ MOZ_ASSERT(!mIsShutdown);
+
+ return mProxyDecoder->GetCodecName();
+}
+
+bool MediaDataDecoderProxy::IsHardwareAccelerated(
+ nsACString& aFailureReason) const {
+ MOZ_ASSERT(!mIsShutdown);
+
+ return mProxyDecoder->IsHardwareAccelerated(aFailureReason);
+}
+
+void MediaDataDecoderProxy::SetSeekThreshold(const media::TimeUnit& aTime) {
+ MOZ_ASSERT(!mIsShutdown);
+
+ if (!mProxyThread || mProxyThread->IsOnCurrentThread()) {
+ mProxyDecoder->SetSeekThreshold(aTime);
+ return;
+ }
+ media::TimeUnit time = aTime;
+ mProxyThread->Dispatch(NS_NewRunnableFunction(
+ "MediaDataDecoderProxy::SetSeekThreshold", [self = RefPtr{this}, time] {
+ self->mProxyDecoder->SetSeekThreshold(time);
+ }));
+}
+
+bool MediaDataDecoderProxy::SupportDecoderRecycling() const {
+ MOZ_ASSERT(!mIsShutdown);
+
+ return mProxyDecoder->SupportDecoderRecycling();
+}
+
+MediaDataDecoder::ConversionRequired MediaDataDecoderProxy::NeedsConversion()
+ const {
+ MOZ_ASSERT(!mIsShutdown);
+
+ return mProxyDecoder->NeedsConversion();
+}
+
+} // namespace mozilla
diff --git a/dom/media/platforms/wrappers/MediaDataDecoderProxy.h b/dom/media/platforms/wrappers/MediaDataDecoderProxy.h
new file mode 100644
index 0000000000..fe61b48411
--- /dev/null
+++ b/dom/media/platforms/wrappers/MediaDataDecoderProxy.h
@@ -0,0 +1,64 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(MediaDataDecoderProxy_h_)
+# define MediaDataDecoderProxy_h_
+
+# include "PlatformDecoderModule.h"
+# include "mozilla/Atomics.h"
+# include "mozilla/RefPtr.h"
+# include "nsThreadUtils.h"
+# include "nscore.h"
+
+namespace mozilla {
+
+DDLoggedTypeDeclNameAndBase(MediaDataDecoderProxy, MediaDataDecoder);
+
+class MediaDataDecoderProxy
+ : public MediaDataDecoder,
+ public DecoderDoctorLifeLogger<MediaDataDecoderProxy> {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaDataDecoderProxy, final);
+
+ explicit MediaDataDecoderProxy(
+ already_AddRefed<MediaDataDecoder> aProxyDecoder,
+ already_AddRefed<nsISerialEventTarget> aProxyThread = nullptr)
+ : mProxyDecoder(aProxyDecoder), mProxyThread(aProxyThread) {
+ DDLINKCHILD("proxy decoder", mProxyDecoder.get());
+ }
+
+ RefPtr<InitPromise> Init() override;
+ RefPtr<DecodePromise> Decode(MediaRawData* aSample) override;
+ bool CanDecodeBatch() const override;
+ RefPtr<DecodePromise> DecodeBatch(
+ nsTArray<RefPtr<MediaRawData>>&& aSamples) override;
+ RefPtr<DecodePromise> Drain() override;
+ RefPtr<FlushPromise> Flush() override;
+ RefPtr<ShutdownPromise> Shutdown() override;
+ bool IsHardwareAccelerated(nsACString& aFailureReason) const override;
+ nsCString GetDescriptionName() const override;
+ nsCString GetProcessName() const override;
+ nsCString GetCodecName() const override;
+ void SetSeekThreshold(const media::TimeUnit& aTime) override;
+ bool SupportDecoderRecycling() const override;
+ ConversionRequired NeedsConversion() const override;
+
+ protected:
+ ~MediaDataDecoderProxy() = default;
+
+ private:
+ // Set on construction and clear on the proxy thread if set.
+ RefPtr<MediaDataDecoder> mProxyDecoder;
+ const nsCOMPtr<nsISerialEventTarget> mProxyThread;
+
+# if defined(DEBUG)
+ Atomic<bool> mIsShutdown = Atomic<bool>(false);
+# endif
+};
+
+} // namespace mozilla
+
+#endif // MediaDataDecoderProxy_h_
diff --git a/dom/media/systemservices/CamerasChild.cpp b/dom/media/systemservices/CamerasChild.cpp
new file mode 100644
index 0000000000..3b9ce64e46
--- /dev/null
+++ b/dom/media/systemservices/CamerasChild.cpp
@@ -0,0 +1,535 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CamerasChild.h"
+
+#undef FF
+
+#include "mozilla/Assertions.h"
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+#include "mozilla/Logging.h"
+#include "mozilla/SyncRunnable.h"
+#include "mozilla/WeakPtr.h"
+#include "mozilla/Unused.h"
+#include "MediaUtils.h"
+#include "nsThreadUtils.h"
+
+#undef LOG
+#undef LOG_ENABLED
+mozilla::LazyLogModule gCamerasChildLog("CamerasChild");
+#define LOG(args) MOZ_LOG(gCamerasChildLog, mozilla::LogLevel::Debug, args)
+#define LOG_ENABLED() MOZ_LOG_TEST(gCamerasChildLog, mozilla::LogLevel::Debug)
+
+namespace mozilla::camera {
+
+CamerasSingleton::CamerasSingleton()
+ : mCamerasMutex("CamerasSingleton::mCamerasMutex"),
+ mCameras(nullptr),
+ mCamerasChildThread(nullptr) {
+ LOG(("CamerasSingleton: %p", this));
+}
+
+CamerasSingleton::~CamerasSingleton() { LOG(("~CamerasSingleton: %p", this)); }
+
+class InitializeIPCThread : public Runnable {
+ public:
+ InitializeIPCThread()
+ : Runnable("camera::InitializeIPCThread"), mCamerasChild(nullptr) {}
+
+ NS_IMETHOD Run() override {
+ // Try to get the PBackground handle
+ ipc::PBackgroundChild* existingBackgroundChild =
+ ipc::BackgroundChild::GetForCurrentThread();
+ // If it's not spun up yet, block until it is, and retry
+ if (!existingBackgroundChild) {
+ LOG(("No existingBackgroundChild"));
+ existingBackgroundChild =
+ ipc::BackgroundChild::GetOrCreateForCurrentThread();
+ LOG(("BackgroundChild: %p", existingBackgroundChild));
+ if (!existingBackgroundChild) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // Create CamerasChild
+ // We will be returning the resulting pointer (synchronously) to our caller.
+ mCamerasChild = static_cast<mozilla::camera::CamerasChild*>(
+ existingBackgroundChild->SendPCamerasConstructor());
+
+ return NS_OK;
+ }
+
+ CamerasChild* GetCamerasChild() { return mCamerasChild; }
+
+ private:
+ CamerasChild* mCamerasChild;
+};
+
+CamerasChild* GetCamerasChild() {
+ CamerasSingleton::Mutex().AssertCurrentThreadOwns();
+ if (!CamerasSingleton::Child()) {
+ MOZ_ASSERT(!NS_IsMainThread(), "Should not be on the main Thread");
+ MOZ_ASSERT(!CamerasSingleton::Thread());
+ LOG(("No sCameras, setting up IPC Thread"));
+ nsresult rv = NS_NewNamedThread("Cameras IPC",
+ getter_AddRefs(CamerasSingleton::Thread()));
+ if (NS_FAILED(rv)) {
+ LOG(("Error launching IPC Thread"));
+ return nullptr;
+ }
+
+ // At this point we are in the MediaManager thread, and the thread we are
+ // dispatching to is the specific Cameras IPC thread that was just made
+ // above, so now we will fire off a runnable to run
+ // BackgroundChild::GetOrCreateForCurrentThread there, while we
+ // block in this thread.
+ // We block until the following happens in the Cameras IPC thread:
+ // 1) Creation of PBackground finishes
+ // 2) Creation of PCameras finishes by sending a message to the parent
+ RefPtr<InitializeIPCThread> runnable = new InitializeIPCThread();
+ RefPtr<SyncRunnable> sr = new SyncRunnable(runnable);
+ sr->DispatchToThread(CamerasSingleton::Thread());
+ CamerasSingleton::Child() = runnable->GetCamerasChild();
+ }
+ if (!CamerasSingleton::Child()) {
+ LOG(("Failed to set up CamerasChild, are we in shutdown?"));
+ }
+ return CamerasSingleton::Child();
+}
+
+CamerasChild* GetCamerasChildIfExists() {
+ OffTheBooksMutexAutoLock lock(CamerasSingleton::Mutex());
+ return CamerasSingleton::Child();
+}
+
+mozilla::ipc::IPCResult CamerasChild::RecvReplyFailure(void) {
+ LOG(("%s", __PRETTY_FUNCTION__));
+ MonitorAutoLock monitor(mReplyMonitor);
+ mReceivedReply = true;
+ mReplySuccess = false;
+ monitor.Notify();
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CamerasChild::RecvReplySuccess(void) {
+ LOG(("%s", __PRETTY_FUNCTION__));
+ MonitorAutoLock monitor(mReplyMonitor);
+ mReceivedReply = true;
+ mReplySuccess = true;
+ monitor.Notify();
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CamerasChild::RecvReplyNumberOfCapabilities(
+ const int& capabilityCount) {
+ LOG(("%s", __PRETTY_FUNCTION__));
+ MonitorAutoLock monitor(mReplyMonitor);
+ mReceivedReply = true;
+ mReplySuccess = true;
+ mReplyInteger = capabilityCount;
+ monitor.Notify();
+ return IPC_OK();
+}
+
+// Helper function to dispatch calls to the IPC Thread and
+// CamerasChild object. Takes the needed locks and dispatches.
+// Takes a "failed" value and a reference to the output variable
+// as parameters, will return the right one depending on whether
+// dispatching succeeded.
+//
+// The LockAndDispatch object in the caller must stay alive until after any
+// reply data has been retreived (mReplyInteger, etc) so that the data is
+// protected by the ReplyMonitor/RequestMutex
+template <class T = int>
+class LockAndDispatch {
+ public:
+ LockAndDispatch(CamerasChild* aCamerasChild, const char* aRequestingFunc,
+ nsIRunnable* aRunnable, T aFailureValue,
+ const T& aSuccessValue)
+ : mCamerasChild(aCamerasChild),
+ mRequestingFunc(aRequestingFunc),
+ mRunnable(aRunnable),
+ mReplyLock(aCamerasChild->mReplyMonitor),
+ mRequestLock(aCamerasChild->mRequestMutex),
+ mSuccess(true),
+ mFailureValue(aFailureValue),
+ mSuccessValue(aSuccessValue) {
+ Dispatch();
+ }
+
+ T ReturnValue() const {
+ if (mSuccess) {
+ return mSuccessValue;
+ } else {
+ return mFailureValue;
+ }
+ }
+
+ const bool& Success() const { return mSuccess; }
+
+ private:
+ void Dispatch() {
+ if (!mCamerasChild->DispatchToParent(mRunnable, mReplyLock)) {
+ LOG(("Cameras dispatch for IPC failed in %s", mRequestingFunc));
+ mSuccess = false;
+ }
+ }
+
+ CamerasChild* mCamerasChild;
+ const char* mRequestingFunc;
+ nsIRunnable* mRunnable;
+ // Prevent concurrent use of the reply variables by holding
+ // the mReplyMonitor. Note that this is unlocked while waiting for
+ // the reply to be filled in, necessitating the additional mRequestLock/Mutex;
+ MonitorAutoLock mReplyLock;
+ MutexAutoLock mRequestLock;
+ bool mSuccess;
+ const T mFailureValue;
+ const T& mSuccessValue;
+};
+
+bool CamerasChild::DispatchToParent(nsIRunnable* aRunnable,
+ MonitorAutoLock& aMonitor) {
+ CamerasSingleton::Mutex().AssertCurrentThreadOwns();
+ mReplyMonitor.AssertCurrentThreadOwns();
+ CamerasSingleton::Thread()->Dispatch(aRunnable, NS_DISPATCH_NORMAL);
+ // Guard against spurious wakeups.
+ mReceivedReply = false;
+ // Wait for a reply
+ do {
+ // If the parent has been shut down, then we won't receive a reply.
+ if (!mIPCIsAlive) {
+ return false;
+ }
+ aMonitor.Wait();
+ } while (!mReceivedReply);
+ return mReplySuccess;
+}
+
+int CamerasChild::NumberOfCapabilities(CaptureEngine aCapEngine,
+ const char* deviceUniqueIdUTF8) {
+ LOG(("%s", __PRETTY_FUNCTION__));
+ LOG(("NumberOfCapabilities for %s", deviceUniqueIdUTF8));
+ nsCString unique_id(deviceUniqueIdUTF8);
+ nsCOMPtr<nsIRunnable> runnable =
+ mozilla::NewRunnableMethod<CaptureEngine, nsCString>(
+ "camera::PCamerasChild::SendNumberOfCapabilities", this,
+ &CamerasChild::SendNumberOfCapabilities, aCapEngine, unique_id);
+ LockAndDispatch<> dispatcher(this, __func__, runnable, 0, mReplyInteger);
+ LOG(("Capture capability count: %d", dispatcher.ReturnValue()));
+ return dispatcher.ReturnValue();
+}
+
+int CamerasChild::NumberOfCaptureDevices(CaptureEngine aCapEngine) {
+ LOG(("%s", __PRETTY_FUNCTION__));
+ nsCOMPtr<nsIRunnable> runnable = mozilla::NewRunnableMethod<CaptureEngine>(
+ "camera::PCamerasChild::SendNumberOfCaptureDevices", this,
+ &CamerasChild::SendNumberOfCaptureDevices, aCapEngine);
+ LockAndDispatch<> dispatcher(this, __func__, runnable, 0, mReplyInteger);
+ LOG(("Capture Devices: %d", dispatcher.ReturnValue()));
+ return dispatcher.ReturnValue();
+}
+
+mozilla::ipc::IPCResult CamerasChild::RecvReplyNumberOfCaptureDevices(
+ const int& aDeviceCount) {
+ LOG(("%s", __PRETTY_FUNCTION__));
+ MonitorAutoLock monitor(mReplyMonitor);
+ mReceivedReply = true;
+ mReplySuccess = true;
+ mReplyInteger = aDeviceCount;
+ monitor.Notify();
+ return IPC_OK();
+}
+
+int CamerasChild::EnsureInitialized(CaptureEngine aCapEngine) {
+ LOG(("%s", __PRETTY_FUNCTION__));
+ nsCOMPtr<nsIRunnable> runnable = mozilla::NewRunnableMethod<CaptureEngine>(
+ "camera::PCamerasChild::SendEnsureInitialized", this,
+ &CamerasChild::SendEnsureInitialized, aCapEngine);
+ LockAndDispatch<> dispatcher(this, __func__, runnable, 0, mReplyInteger);
+ LOG(("Capture Devices: %d", dispatcher.ReturnValue()));
+ return dispatcher.ReturnValue();
+}
+
+int CamerasChild::GetCaptureCapability(
+ CaptureEngine aCapEngine, const char* unique_idUTF8,
+ const unsigned int capability_number,
+ webrtc::VideoCaptureCapability* capability) {
+ LOG(("GetCaptureCapability: %s %d", unique_idUTF8, capability_number));
+ MOZ_ASSERT(capability);
+ nsCString unique_id(unique_idUTF8);
+ nsCOMPtr<nsIRunnable> runnable =
+ mozilla::NewRunnableMethod<CaptureEngine, nsCString, unsigned int>(
+ "camera::PCamerasChild::SendGetCaptureCapability", this,
+ &CamerasChild::SendGetCaptureCapability, aCapEngine, unique_id,
+ capability_number);
+ mReplyCapability = capability;
+ LockAndDispatch<> dispatcher(this, __func__, runnable, -1, mZero);
+ mReplyCapability = nullptr;
+ return dispatcher.ReturnValue();
+}
+
+mozilla::ipc::IPCResult CamerasChild::RecvReplyGetCaptureCapability(
+ const VideoCaptureCapability& ipcCapability) {
+ LOG(("%s", __PRETTY_FUNCTION__));
+ MonitorAutoLock monitor(mReplyMonitor);
+ mReceivedReply = true;
+ mReplySuccess = true;
+ mReplyCapability->width = ipcCapability.width();
+ mReplyCapability->height = ipcCapability.height();
+ mReplyCapability->maxFPS = ipcCapability.maxFPS();
+ mReplyCapability->videoType =
+ static_cast<webrtc::VideoType>(ipcCapability.videoType());
+ mReplyCapability->interlaced = ipcCapability.interlaced();
+ monitor.Notify();
+ return IPC_OK();
+}
+
+int CamerasChild::GetCaptureDevice(
+ CaptureEngine aCapEngine, unsigned int list_number, char* device_nameUTF8,
+ const unsigned int device_nameUTF8Length, char* unique_idUTF8,
+ const unsigned int unique_idUTF8Length, bool* scary) {
+ LOG(("%s", __PRETTY_FUNCTION__));
+ nsCOMPtr<nsIRunnable> runnable =
+ mozilla::NewRunnableMethod<CaptureEngine, unsigned int>(
+ "camera::PCamerasChild::SendGetCaptureDevice", this,
+ &CamerasChild::SendGetCaptureDevice, aCapEngine, list_number);
+ LockAndDispatch<> dispatcher(this, __func__, runnable, -1, mZero);
+ if (dispatcher.Success()) {
+ base::strlcpy(device_nameUTF8, mReplyDeviceName.get(),
+ device_nameUTF8Length);
+ base::strlcpy(unique_idUTF8, mReplyDeviceID.get(), unique_idUTF8Length);
+ if (scary) {
+ *scary = mReplyScary;
+ }
+ LOG(("Got %s name %s id", device_nameUTF8, unique_idUTF8));
+ }
+ return dispatcher.ReturnValue();
+}
+
+mozilla::ipc::IPCResult CamerasChild::RecvReplyGetCaptureDevice(
+ const nsACString& device_name, const nsACString& device_id,
+ const bool& scary) {
+ LOG(("%s", __PRETTY_FUNCTION__));
+ MonitorAutoLock monitor(mReplyMonitor);
+ mReceivedReply = true;
+ mReplySuccess = true;
+ mReplyDeviceName = device_name;
+ mReplyDeviceID = device_id;
+ mReplyScary = scary;
+ monitor.Notify();
+ return IPC_OK();
+}
+
+int CamerasChild::AllocateCapture(CaptureEngine aCapEngine,
+ const char* unique_idUTF8,
+ uint64_t aWindowID) {
+ LOG(("%s", __PRETTY_FUNCTION__));
+ nsCString unique_id(unique_idUTF8);
+ nsCOMPtr<nsIRunnable> runnable =
+ mozilla::NewRunnableMethod<CaptureEngine, nsCString, const uint64_t&>(
+ "camera::PCamerasChild::SendAllocateCapture", this,
+ &CamerasChild::SendAllocateCapture, aCapEngine, unique_id, aWindowID);
+ LockAndDispatch<> dispatcher(this, __func__, runnable, -1, mReplyInteger);
+ if (dispatcher.Success()) {
+ LOG(("Capture Device allocated: %d", mReplyInteger));
+ }
+ return dispatcher.ReturnValue();
+}
+
+mozilla::ipc::IPCResult CamerasChild::RecvReplyAllocateCapture(
+ const int& aCaptureId) {
+ LOG(("%s", __PRETTY_FUNCTION__));
+ MonitorAutoLock monitor(mReplyMonitor);
+ mReceivedReply = true;
+ mReplySuccess = true;
+ mReplyInteger = aCaptureId;
+ monitor.Notify();
+ return IPC_OK();
+}
+
+int CamerasChild::ReleaseCapture(CaptureEngine aCapEngine,
+ const int capture_id) {
+ LOG(("%s", __PRETTY_FUNCTION__));
+ nsCOMPtr<nsIRunnable> runnable =
+ mozilla::NewRunnableMethod<CaptureEngine, int>(
+ "camera::PCamerasChild::SendReleaseCapture", this,
+ &CamerasChild::SendReleaseCapture, aCapEngine, capture_id);
+ LockAndDispatch<> dispatcher(this, __func__, runnable, -1, mZero);
+ return dispatcher.ReturnValue();
+}
+
+void CamerasChild::AddCallback(const CaptureEngine aCapEngine,
+ const int capture_id, FrameRelay* render) {
+ MutexAutoLock lock(mCallbackMutex);
+ CapturerElement ce;
+ ce.engine = aCapEngine;
+ ce.id = capture_id;
+ ce.callback = render;
+ mCallbacks.AppendElement(ce);
+}
+
+void CamerasChild::RemoveCallback(const CaptureEngine aCapEngine,
+ const int capture_id) {
+ MutexAutoLock lock(mCallbackMutex);
+ for (unsigned int i = 0; i < mCallbacks.Length(); i++) {
+ CapturerElement ce = mCallbacks[i];
+ if (ce.engine == aCapEngine && ce.id == capture_id) {
+ mCallbacks.RemoveElementAt(i);
+ break;
+ }
+ }
+}
+
+int CamerasChild::StartCapture(CaptureEngine aCapEngine, const int capture_id,
+ const webrtc::VideoCaptureCapability& webrtcCaps,
+ FrameRelay* cb) {
+ LOG(("%s", __PRETTY_FUNCTION__));
+ AddCallback(aCapEngine, capture_id, cb);
+ VideoCaptureCapability capCap(
+ webrtcCaps.width, webrtcCaps.height, webrtcCaps.maxFPS,
+ static_cast<int>(webrtcCaps.videoType), webrtcCaps.interlaced);
+ nsCOMPtr<nsIRunnable> runnable =
+ mozilla::NewRunnableMethod<CaptureEngine, int, VideoCaptureCapability>(
+ "camera::PCamerasChild::SendStartCapture", this,
+ &CamerasChild::SendStartCapture, aCapEngine, capture_id, capCap);
+ LockAndDispatch<> dispatcher(this, __func__, runnable, -1, mZero);
+ return dispatcher.ReturnValue();
+}
+
+int CamerasChild::FocusOnSelectedSource(CaptureEngine aCapEngine,
+ const int aCaptureId) {
+ LOG(("%s", __PRETTY_FUNCTION__));
+ nsCOMPtr<nsIRunnable> runnable =
+ mozilla::NewRunnableMethod<CaptureEngine, int>(
+ "camera::PCamerasChild::SendFocusOnSelectedSource", this,
+ &CamerasChild::SendFocusOnSelectedSource, aCapEngine, aCaptureId);
+ LockAndDispatch<> dispatcher(this, __func__, runnable, -1, mZero);
+ return dispatcher.ReturnValue();
+}
+
+int CamerasChild::StopCapture(CaptureEngine aCapEngine, const int capture_id) {
+ LOG(("%s", __PRETTY_FUNCTION__));
+ nsCOMPtr<nsIRunnable> runnable =
+ mozilla::NewRunnableMethod<CaptureEngine, int>(
+ "camera::PCamerasChild::SendStopCapture", this,
+ &CamerasChild::SendStopCapture, aCapEngine, capture_id);
+ LockAndDispatch<> dispatcher(this, __func__, runnable, -1, mZero);
+ if (dispatcher.Success()) {
+ RemoveCallback(aCapEngine, capture_id);
+ }
+ return dispatcher.ReturnValue();
+}
+
+class ShutdownRunnable : public Runnable {
+ public:
+ explicit ShutdownRunnable(already_AddRefed<Runnable>&& aReplyEvent)
+ : Runnable("camera::ShutdownRunnable"), mReplyEvent(aReplyEvent){};
+
+ NS_IMETHOD Run() override {
+ LOG(("Closing BackgroundChild"));
+ // This will also destroy the CamerasChild.
+ ipc::BackgroundChild::CloseForCurrentThread();
+
+ NS_DispatchToMainThread(mReplyEvent.forget());
+
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<Runnable> mReplyEvent;
+};
+
+void Shutdown(void) {
+ // Called from both MediaEngineWebRTC::Shutdown() on the MediaManager thread
+ // and DeallocPCamerasChild() on the dedicated IPC thread.
+ OffTheBooksMutexAutoLock lock(CamerasSingleton::Mutex());
+
+ CamerasChild* child = CamerasSingleton::Child();
+ if (!child) {
+ // We don't want to cause everything to get fired up if we're
+ // really already shut down.
+ LOG(("Shutdown when already shut down"));
+ return;
+ }
+ if (CamerasSingleton::Thread()) {
+ LOG(("PBackground thread exists, dispatching close"));
+ // The IPC thread is shut down on the main thread after the
+ // BackgroundChild is closed.
+ RefPtr<ShutdownRunnable> runnable = new ShutdownRunnable(
+ NewRunnableMethod("nsIThread::Shutdown", CamerasSingleton::Thread(),
+ &nsIThread::Shutdown));
+ CamerasSingleton::Thread()->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL);
+ } else {
+ LOG(("Shutdown called without PBackground thread"));
+ }
+ LOG(("Erasing sCameras & thread refs (original thread)"));
+ CamerasSingleton::Child() = nullptr;
+ CamerasSingleton::Thread() = nullptr;
+}
+
+mozilla::ipc::IPCResult CamerasChild::RecvDeliverFrame(
+ const CaptureEngine& capEngine, const int& capId,
+ mozilla::ipc::Shmem&& shmem, const VideoFrameProperties& prop) {
+ MutexAutoLock lock(mCallbackMutex);
+ if (Callback(capEngine, capId)) {
+ unsigned char* image = shmem.get<unsigned char>();
+ Callback(capEngine, capId)->DeliverFrame(image, prop);
+ } else {
+ LOG(("DeliverFrame called with dead callback"));
+ }
+ SendReleaseFrame(std::move(shmem));
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult CamerasChild::RecvDeviceChange() {
+ mDeviceListChangeEvent.Notify();
+ return IPC_OK();
+}
+
+void CamerasChild::ActorDestroy(ActorDestroyReason aWhy) {
+ LOG(("ActorDestroy"));
+ MonitorAutoLock monitor(mReplyMonitor);
+ mIPCIsAlive = false;
+ // Hopefully prevent us from getting stuck
+ // on replies that'll never come.
+ monitor.NotifyAll();
+}
+
+CamerasChild::CamerasChild()
+ : mCallbackMutex("mozilla::cameras::CamerasChild::mCallbackMutex"),
+ mIPCIsAlive(true),
+ mRequestMutex("mozilla::cameras::CamerasChild::mRequestMutex"),
+ mReplyMonitor("mozilla::cameras::CamerasChild::mReplyMonitor"),
+ mReceivedReply(false),
+ mReplySuccess(false),
+ mZero(0),
+ mReplyInteger(0),
+ mReplyScary(false) {
+ LOG(("CamerasChild: %p", this));
+
+ MOZ_COUNT_CTOR(CamerasChild);
+}
+
+CamerasChild::~CamerasChild() {
+ LOG(("~CamerasChild: %p", this));
+ CamerasSingleton::AssertNoChild();
+ MOZ_COUNT_DTOR(CamerasChild);
+}
+
+FrameRelay* CamerasChild::Callback(CaptureEngine aCapEngine, int capture_id) {
+ for (unsigned int i = 0; i < mCallbacks.Length(); i++) {
+ CapturerElement ce = mCallbacks[i];
+ if (ce.engine == aCapEngine && ce.id == capture_id) {
+ return ce.callback;
+ }
+ }
+
+ return nullptr;
+}
+
+} // namespace mozilla::camera
diff --git a/dom/media/systemservices/CamerasChild.h b/dom/media/systemservices/CamerasChild.h
new file mode 100644
index 0000000000..18bdeec251
--- /dev/null
+++ b/dom/media/systemservices/CamerasChild.h
@@ -0,0 +1,262 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_CamerasChild_h
+#define mozilla_CamerasChild_h
+
+#include <utility>
+
+#include "MediaEventSource.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/camera/PCamerasChild.h"
+#include "mozilla/camera/PCamerasParent.h"
+#include "nsCOMPtr.h"
+
+// conflicts with #include of scoped_ptr.h
+#undef FF
+#include "modules/video_capture/video_capture_defines.h"
+
+namespace mozilla {
+
+namespace ipc {
+class BackgroundChildImpl;
+} // namespace ipc
+
+namespace camera {
+
+class FrameRelay {
+ public:
+ virtual int DeliverFrame(
+ uint8_t* buffer, const mozilla::camera::VideoFrameProperties& props) = 0;
+};
+
+struct CapturerElement {
+ CaptureEngine engine;
+ int id;
+ FrameRelay* callback;
+};
+
+// Forward declaration so we can work with pointers to it.
+class CamerasChild;
+// Helper class in impl that we friend.
+template <class T>
+class LockAndDispatch;
+
+// We emulate the sync webrtc.org API with the help of singleton
+// CamerasSingleton, which manages a pointer to an IPC object, a thread
+// where IPC operations should run on, and a mutex.
+// The static function Cameras() will use that Singleton to set up,
+// if needed, both the thread and the associated IPC objects and return
+// a pointer to the IPC object. Users can then do IPC calls on that object
+// after dispatching them to aforementioned thread.
+
+// 2 Threads are involved in this code:
+// - the MediaManager thread, which will call the (static, sync API) functions
+// through MediaEngineRemoteVideoSource
+// - the Cameras IPC thread, which will be doing our IPC to the parent process
+// via PBackground
+
+// Our main complication is that we emulate a sync API while (having to do)
+// async messaging. We dispatch the messages to another thread to send them
+// async and hold a Monitor to wait for the result to be asynchronously received
+// again. The requirement for async messaging originates on the parent side:
+// it's not reasonable to block all PBackground IPC there while waiting for
+// something like device enumeration to complete.
+
+class CamerasSingleton {
+ public:
+ static OffTheBooksMutex& Mutex() { return singleton().mCamerasMutex; }
+
+ static CamerasChild*& Child() {
+ Mutex().AssertCurrentThreadOwns();
+ return singleton().mCameras;
+ }
+
+ static nsCOMPtr<nsIThread>& Thread() {
+ Mutex().AssertCurrentThreadOwns();
+ return singleton().mCamerasChildThread;
+ }
+ // The mutex is not held because mCameras is known not to be modified
+ // concurrently when this is asserted.
+ static void AssertNoChild() { MOZ_ASSERT(!singleton().mCameras); }
+
+ private:
+ CamerasSingleton();
+ ~CamerasSingleton();
+
+ static CamerasSingleton& singleton() {
+ static CamerasSingleton camera;
+ return camera;
+ }
+
+ // Reinitializing CamerasChild will change the pointers below.
+ // We don't want this to happen in the middle of preparing IPC.
+ // We will be alive on destruction, so this needs to be off the books.
+ mozilla::OffTheBooksMutex mCamerasMutex;
+
+ // This is owned by the IPC code, and the same code controls the lifetime.
+ // It will set and clear this pointer as appropriate in setup/teardown.
+ // We'd normally make this a WeakPtr but unfortunately the IPC code already
+ // uses the WeakPtr mixin in a protected base class of CamerasChild, and in
+ // any case the object becomes unusable as soon as IPC is tearing down, which
+ // will be before actual destruction.
+ CamerasChild* mCameras;
+ nsCOMPtr<nsIThread> mCamerasChildThread;
+};
+
+// Get a pointer to a CamerasChild object we can use to do IPC with.
+// This does everything needed to set up, including starting the IPC
+// channel with PBackground, blocking until thats done, and starting the
+// thread to do IPC on. This will fail if we're in shutdown. On success
+// it will set up the CamerasSingleton.
+CamerasChild* GetCamerasChild();
+
+CamerasChild* GetCamerasChildIfExists();
+
+// Shut down the IPC channel and everything associated, like WebRTC.
+// This is a static call because the CamerasChild object may not even
+// be alive when we're called.
+void Shutdown(void);
+
+// Obtain the CamerasChild object (if possible, i.e. not shutting down),
+// and maintain a grip on the object for the duration of the call.
+template <class MEM_FUN, class... ARGS>
+int GetChildAndCall(MEM_FUN&& f, ARGS&&... args) {
+ OffTheBooksMutexAutoLock lock(CamerasSingleton::Mutex());
+ CamerasChild* child = GetCamerasChild();
+ if (child) {
+ return (child->*f)(std::forward<ARGS>(args)...);
+ } else {
+ return -1;
+ }
+}
+
+class CamerasChild final : public PCamerasChild {
+ friend class mozilla::ipc::BackgroundChildImpl;
+ template <class T>
+ friend class mozilla::camera::LockAndDispatch;
+
+ public:
+ // We are owned by the PBackground thread only. CamerasSingleton
+ // takes a non-owning reference.
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CamerasChild)
+
+ // IPC messages recevied, received on the PBackground thread
+ // these are the actual callbacks with data
+ mozilla::ipc::IPCResult RecvDeliverFrame(
+ const CaptureEngine&, const int&, mozilla::ipc::Shmem&&,
+ const VideoFrameProperties& prop) override;
+
+ mozilla::ipc::IPCResult RecvDeviceChange() override;
+
+ // these are response messages to our outgoing requests
+ mozilla::ipc::IPCResult RecvReplyNumberOfCaptureDevices(const int&) override;
+ mozilla::ipc::IPCResult RecvReplyNumberOfCapabilities(const int&) override;
+ mozilla::ipc::IPCResult RecvReplyAllocateCapture(const int&) override;
+ mozilla::ipc::IPCResult RecvReplyGetCaptureCapability(
+ const VideoCaptureCapability& capability) override;
+ mozilla::ipc::IPCResult RecvReplyGetCaptureDevice(
+ const nsACString& device_name, const nsACString& device_id,
+ const bool& scary) override;
+ mozilla::ipc::IPCResult RecvReplyFailure(void) override;
+ mozilla::ipc::IPCResult RecvReplySuccess(void) override;
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ // the webrtc.org ViECapture calls are mirrored here, but with access
+ // to a specific PCameras instance to communicate over. These also
+ // run on the MediaManager thread
+ int NumberOfCaptureDevices(CaptureEngine aCapEngine);
+ int NumberOfCapabilities(CaptureEngine aCapEngine,
+ const char* deviceUniqueIdUTF8);
+ int ReleaseCapture(CaptureEngine aCapEngine, const int capture_id);
+ int StartCapture(CaptureEngine aCapEngine, const int capture_id,
+ const webrtc::VideoCaptureCapability& capability,
+ FrameRelay* func);
+ int FocusOnSelectedSource(CaptureEngine aCapEngine, const int capture_id);
+ int StopCapture(CaptureEngine aCapEngine, const int capture_id);
+ // Returns a non-negative capture identifier or -1 on failure.
+ int AllocateCapture(CaptureEngine aCapEngine, const char* unique_idUTF8,
+ uint64_t aWindowID);
+ int GetCaptureCapability(CaptureEngine aCapEngine, const char* unique_idUTF8,
+ const unsigned int capability_number,
+ webrtc::VideoCaptureCapability* capability);
+ int GetCaptureDevice(CaptureEngine aCapEngine, unsigned int list_number,
+ char* device_nameUTF8,
+ const unsigned int device_nameUTF8Length,
+ char* unique_idUTF8,
+ const unsigned int unique_idUTF8Length,
+ bool* scary = nullptr);
+ int EnsureInitialized(CaptureEngine aCapEngine);
+
+ template <typename This>
+ int ConnectDeviceListChangeListener(MediaEventListener* aListener,
+ AbstractThread* aTarget, This* aThis,
+ void (This::*aMethod)()) {
+ // According to the spec, if the script sets
+ // navigator.mediaDevices.ondevicechange and the permission state is
+ // "always granted", the User Agent MUST fires a devicechange event when
+ // a new media input device is made available, even the script never
+ // call getusermedia or enumerateDevices.
+
+ // In order to detect the event, we need to init the camera engine.
+ // Currently EnsureInitialized(aCapEngine) is only called when one of
+ // CamerasParent api, e.g., RecvNumberOfCaptureDevices(), is called.
+
+ // So here we setup camera engine via EnsureInitialized(aCapEngine)
+
+ EnsureInitialized(CameraEngine);
+ *aListener = mDeviceListChangeEvent.Connect(aTarget, aThis, aMethod);
+ return IPC_OK();
+ }
+
+ FrameRelay* Callback(CaptureEngine aCapEngine, int capture_id);
+
+ private:
+ CamerasChild();
+ ~CamerasChild();
+ // Dispatch a Runnable to the PCamerasParent, by executing it on the
+ // decidecated Cameras IPC/PBackground thread.
+ bool DispatchToParent(nsIRunnable* aRunnable, MonitorAutoLock& aMonitor);
+ void AddCallback(const CaptureEngine aCapEngine, const int capture_id,
+ FrameRelay* render);
+ void RemoveCallback(const CaptureEngine aCapEngine, const int capture_id);
+
+ nsTArray<CapturerElement> mCallbacks;
+ // Protects the callback arrays
+ Mutex mCallbackMutex MOZ_UNANNOTATED;
+
+ bool mIPCIsAlive;
+
+ // Hold to prevent multiple outstanding requests. We don't use
+ // request IDs so we only support one at a time. Don't want try
+ // to use the webrtc.org API from multiple threads simultanously.
+ // The monitor below isn't sufficient for this, as it will drop
+ // the lock when Wait-ing for a response, allowing us to send a new
+ // request. The Notify on receiving the response will then unblock
+ // both waiters and one will be guaranteed to get the wrong result.
+ // Take this one before taking mReplyMonitor.
+ Mutex mRequestMutex MOZ_UNANNOTATED;
+ // Hold to wait for an async response to our calls *and* until the
+ // user of LockAndDispatch<> has read the data out. This is done by
+ // keeping the LockAndDispatch object alive.
+ Monitor mReplyMonitor MOZ_UNANNOTATED;
+ // Async response valid?
+ bool mReceivedReply;
+ // Async responses data contents;
+ bool mReplySuccess;
+ const int mZero;
+ int mReplyInteger;
+ webrtc::VideoCaptureCapability* mReplyCapability = nullptr;
+ nsCString mReplyDeviceName;
+ nsCString mReplyDeviceID;
+ bool mReplyScary;
+ MediaEventProducer<void> mDeviceListChangeEvent;
+};
+
+} // namespace camera
+} // namespace mozilla
+
+#endif // mozilla_CamerasChild_h
diff --git a/dom/media/systemservices/CamerasParent.cpp b/dom/media/systemservices/CamerasParent.cpp
new file mode 100644
index 0000000000..1c63f0cee1
--- /dev/null
+++ b/dom/media/systemservices/CamerasParent.cpp
@@ -0,0 +1,1220 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CamerasParent.h"
+
+#include <atomic>
+#include "MediaEngineSource.h"
+#include "PerformanceRecorder.h"
+#include "VideoFrameUtils.h"
+
+#include "mozilla/AppShutdown.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/ProfilerMarkers.h"
+#include "mozilla/Unused.h"
+#include "mozilla/Services.h"
+#include "mozilla/Logging.h"
+#include "mozilla/ipc/BackgroundParent.h"
+#include "mozilla/ipc/PBackgroundParent.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+#include "mozilla/dom/WindowGlobalParent.h"
+#include "mozilla/media/MediaUtils.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_permissions.h"
+#include "nsIPermissionManager.h"
+#include "nsIThread.h"
+#include "nsThreadUtils.h"
+#include "nsNetUtil.h"
+
+#include "api/video/video_frame_buffer.h"
+#include "common_video/libyuv/include/webrtc_libyuv.h"
+
+#if defined(_WIN32)
+# include <process.h>
+# define getpid() _getpid()
+#endif
+
+#undef LOG
+#undef LOG_VERBOSE
+#undef LOG_ENABLED
+mozilla::LazyLogModule gCamerasParentLog("CamerasParent");
+#define LOG(...) \
+ MOZ_LOG(gCamerasParentLog, mozilla::LogLevel::Debug, (__VA_ARGS__))
+#define LOG_FUNCTION() \
+ MOZ_LOG(gCamerasParentLog, mozilla::LogLevel::Debug, \
+ ("CamerasParent(%p)::%s", this, __func__))
+#define LOG_VERBOSE(...) \
+ MOZ_LOG(gCamerasParentLog, mozilla::LogLevel::Verbose, (__VA_ARGS__))
+#define LOG_ENABLED() MOZ_LOG_TEST(gCamerasParentLog, mozilla::LogLevel::Debug)
+
+namespace mozilla {
+using media::ShutdownBlockingTicket;
+namespace camera {
+
+std::map<uint32_t, const char*> sDeviceUniqueIDs;
+std::map<uint32_t, webrtc::VideoCaptureCapability> sAllRequestedCapabilities;
+
+uint32_t ResolutionFeasibilityDistance(int32_t candidate, int32_t requested) {
+ // The purpose of this function is to find a smallest resolution
+ // which is larger than all requested capabilities.
+ // Then we can use down-scaling to fulfill each request.
+
+ MOZ_DIAGNOSTIC_ASSERT(candidate >= 0, "Candidate unexpectedly negative");
+ MOZ_DIAGNOSTIC_ASSERT(requested >= 0, "Requested unexpectedly negative");
+
+ if (candidate == 0) {
+ // Treat width|height capability of 0 as "can do any".
+ // This allows for orthogonal capabilities that are not in discrete steps.
+ return 0;
+ }
+
+ uint32_t distance =
+ std::abs(candidate - requested) * 1000 / std::max(candidate, requested);
+ if (candidate >= requested) {
+ // This is a good case, the candidate covers the requested resolution.
+ return distance;
+ }
+
+ // This is a bad case, the candidate is lower than the requested resolution.
+ // This is penalized with an added weight of 10000.
+ return 10000 + distance;
+}
+
+uint32_t FeasibilityDistance(int32_t candidate, int32_t requested) {
+ MOZ_DIAGNOSTIC_ASSERT(candidate >= 0, "Candidate unexpectedly negative");
+ MOZ_DIAGNOSTIC_ASSERT(requested >= 0, "Requested unexpectedly negative");
+
+ if (candidate == 0) {
+ // Treat maxFPS capability of 0 as "can do any".
+ // This allows for orthogonal capabilities that are not in discrete steps.
+ return 0;
+ }
+
+ return std::abs(candidate - requested) * 1000 /
+ std::max(candidate, requested);
+}
+
+class CamerasParent::VideoEngineArray
+ : public media::Refcountable<nsTArray<RefPtr<VideoEngine>>> {};
+
+// Singleton video engines. The sEngines RefPtr is IPC background thread only
+// and outlives the CamerasParent instances. The array elements are video
+// capture thread only.
+using VideoEngineArray = CamerasParent::VideoEngineArray;
+static StaticRefPtr<VideoEngineArray> sEngines;
+// Number of CamerasParents instances in the current process for which
+// mVideoCaptureThread has been set. IPC background thread only.
+static int32_t sNumCamerasParents = 0;
+// Video processing thread - where webrtc.org capturer code runs. Outlives the
+// CamerasParent instances. IPC background thread only.
+static StaticRefPtr<nsIThread> sVideoCaptureThread;
+
+static already_AddRefed<nsISerialEventTarget>
+MakeAndAddRefVideoCaptureThreadAndSingletons() {
+ ipc::AssertIsOnBackgroundThread();
+
+ MOZ_ASSERT_IF(sVideoCaptureThread, sNumCamerasParents > 0);
+ MOZ_ASSERT_IF(!sVideoCaptureThread, sNumCamerasParents == 0);
+
+ if (!sVideoCaptureThread) {
+ LOG("Spinning up WebRTC Cameras Thread");
+ nsIThreadManager::ThreadCreationOptions options;
+#ifdef XP_WIN
+ // Windows desktop capture needs a UI thread
+ options.isUiThread = true;
+#endif
+ nsCOMPtr<nsIThread> videoCaptureThread;
+ if (NS_FAILED(NS_NewNamedThread("VideoCapture",
+ getter_AddRefs(videoCaptureThread), nullptr,
+ options))) {
+ return nullptr;
+ }
+ sVideoCaptureThread = videoCaptureThread.forget();
+
+ sEngines = MakeRefPtr<VideoEngineArray>();
+ sEngines->AppendElements(CaptureEngine::MaxEngine);
+ }
+
+ ++sNumCamerasParents;
+ return do_AddRef(sVideoCaptureThread);
+}
+
+static void ReleaseVideoCaptureThreadAndSingletons() {
+ ipc::AssertIsOnBackgroundThread();
+
+ if (--sNumCamerasParents > 0) {
+ // Other CamerasParent instances are using the singleton classes.
+ return;
+ }
+
+ MOZ_ASSERT(sNumCamerasParents == 0, "Double release!");
+
+ // No other CamerasParent instances alive. Clean up.
+ LOG("Shutting down VideoEngines and the VideoCapture thread");
+ MOZ_ALWAYS_SUCCEEDS(sVideoCaptureThread->Dispatch(
+ NS_NewRunnableFunction(__func__, [engines = RefPtr(sEngines.forget())] {
+ for (RefPtr<VideoEngine>& engine : *engines) {
+ if (engine) {
+ VideoEngine::Delete(engine);
+ engine = nullptr;
+ }
+ }
+ })));
+
+ MOZ_ALWAYS_SUCCEEDS(RefPtr(sVideoCaptureThread.forget())->AsyncShutdown());
+}
+
+// 3 threads are involved in this code:
+// - the main thread for some setups, and occassionally for video capture setup
+// calls that don't work correctly elsewhere.
+// - the IPC thread on which PBackground is running and which receives and
+// sends messages
+// - a thread which will execute the actual (possibly slow) camera access
+// called "VideoCapture". On Windows this is a thread with an event loop
+// suitable for UI access.
+
+void CamerasParent::OnDeviceChange() {
+ LOG_FUNCTION();
+
+ mPBackgroundEventTarget->Dispatch(
+ NS_NewRunnableFunction(__func__, [this, self = RefPtr(this)]() {
+ if (IsShuttingDown()) {
+ LOG("OnDeviceChanged failure: parent shutting down.");
+ return;
+ }
+ Unused << SendDeviceChange();
+ }));
+};
+
+class DeliverFrameRunnable : public mozilla::Runnable {
+ public:
+ DeliverFrameRunnable(CamerasParent* aParent, CaptureEngine aEngine,
+ uint32_t aStreamId, const TrackingId& aTrackingId,
+ const webrtc::VideoFrame& aFrame,
+ const VideoFrameProperties& aProperties)
+ : Runnable("camera::DeliverFrameRunnable"),
+ mParent(aParent),
+ mCapEngine(aEngine),
+ mStreamId(aStreamId),
+ mTrackingId(aTrackingId),
+ mProperties(aProperties),
+ mResult(0) {
+ // No ShmemBuffer (of the right size) was available, so make an
+ // extra buffer here. We have no idea when we are going to run and
+ // it will be potentially long after the webrtc frame callback has
+ // returned, so the copy needs to be no later than here.
+ // We will need to copy this back into a Shmem later on so we prefer
+ // using ShmemBuffers to avoid the extra copy.
+ PerformanceRecorder<CopyVideoStage> rec(
+ "CamerasParent::VideoFrameToAltBuffer"_ns, aTrackingId, aFrame.width(),
+ aFrame.height());
+ mAlternateBuffer.reset(new unsigned char[aProperties.bufferSize()]);
+ VideoFrameUtils::CopyVideoFrameBuffers(mAlternateBuffer.get(),
+ aProperties.bufferSize(), aFrame);
+ rec.Record();
+ }
+
+ DeliverFrameRunnable(CamerasParent* aParent, CaptureEngine aEngine,
+ uint32_t aStreamId, const TrackingId& aTrackingId,
+ ShmemBuffer aBuffer, VideoFrameProperties& aProperties)
+ : Runnable("camera::DeliverFrameRunnable"),
+ mParent(aParent),
+ mCapEngine(aEngine),
+ mStreamId(aStreamId),
+ mTrackingId(aTrackingId),
+ mBuffer(std::move(aBuffer)),
+ mProperties(aProperties),
+ mResult(0){};
+
+ NS_IMETHOD Run() override {
+ // runs on BackgroundEventTarget
+ MOZ_ASSERT(GetCurrentSerialEventTarget() ==
+ mParent->mPBackgroundEventTarget);
+ if (mParent->IsShuttingDown()) {
+ // Communication channel is being torn down
+ mResult = 0;
+ return NS_OK;
+ }
+ if (!mParent->DeliverFrameOverIPC(mCapEngine, mStreamId, mTrackingId,
+ std::move(mBuffer),
+ mAlternateBuffer.get(), mProperties)) {
+ mResult = -1;
+ } else {
+ mResult = 0;
+ }
+ return NS_OK;
+ }
+
+ int GetResult() { return mResult; }
+
+ private:
+ const RefPtr<CamerasParent> mParent;
+ const CaptureEngine mCapEngine;
+ const uint32_t mStreamId;
+ const TrackingId mTrackingId;
+ ShmemBuffer mBuffer;
+ UniquePtr<unsigned char[]> mAlternateBuffer;
+ const VideoFrameProperties mProperties;
+ int mResult;
+};
+
+int CamerasParent::DeliverFrameOverIPC(CaptureEngine aCapEngine,
+ uint32_t aStreamId,
+ const TrackingId& aTrackingId,
+ ShmemBuffer aBuffer,
+ unsigned char* aAltBuffer,
+ const VideoFrameProperties& aProps) {
+ // No ShmemBuffers were available, so construct one now of the right size
+ // and copy into it. That is an extra copy, but we expect this to be
+ // the exceptional case, because we just assured the next call *will* have a
+ // buffer of the right size.
+ if (aAltBuffer != nullptr) {
+ // Get a shared memory buffer from the pool, at least size big
+ ShmemBuffer shMemBuff = mShmemPool.Get(this, aProps.bufferSize());
+
+ if (!shMemBuff.Valid()) {
+ LOG("No usable Video shmem in DeliverFrame (out of buffers?)");
+ // We can skip this frame if we run out of buffers, it's not a real error.
+ return 0;
+ }
+
+ PerformanceRecorder<CopyVideoStage> rec(
+ "CamerasParent::AltBufferToShmem"_ns, aTrackingId, aProps.width(),
+ aProps.height());
+ // get() and Size() check for proper alignment of the segment
+ memcpy(shMemBuff.GetBytes(), aAltBuffer, aProps.bufferSize());
+ rec.Record();
+
+ if (!SendDeliverFrame(aCapEngine, aStreamId, std::move(shMemBuff.Get()),
+ aProps)) {
+ return -1;
+ }
+ } else {
+ MOZ_ASSERT(aBuffer.Valid());
+ // ShmemBuffer was available, we're all good. A single copy happened
+ // in the original webrtc callback.
+ if (!SendDeliverFrame(aCapEngine, aStreamId, std::move(aBuffer.Get()),
+ aProps)) {
+ return -1;
+ }
+ }
+
+ return 0;
+}
+
+ShmemBuffer CamerasParent::GetBuffer(size_t aSize) {
+ return mShmemPool.GetIfAvailable(aSize);
+}
+
+void CallbackHelper::OnFrame(const webrtc::VideoFrame& aVideoFrame) {
+ LOG_VERBOSE("CamerasParent(%p)::%s", mParent, __func__);
+ if (profiler_thread_is_being_profiled_for_markers()) {
+ PROFILER_MARKER_UNTYPED(
+ nsPrintfCString("CaptureVideoFrame %dx%d %s %s", aVideoFrame.width(),
+ aVideoFrame.height(),
+ webrtc::VideoFrameBufferTypeToString(
+ aVideoFrame.video_frame_buffer()->type()),
+ mTrackingId.ToString().get()),
+ MEDIA_RT);
+ }
+ RefPtr<DeliverFrameRunnable> runnable = nullptr;
+ // Get frame properties
+ camera::VideoFrameProperties properties;
+ VideoFrameUtils::InitFrameBufferProperties(aVideoFrame, properties);
+ // Get a shared memory buffer to copy the frame data into
+ ShmemBuffer shMemBuffer = mParent->GetBuffer(properties.bufferSize());
+ if (!shMemBuffer.Valid()) {
+ // Either we ran out of buffers or they're not the right size yet
+ LOG("Correctly sized Video shmem not available in DeliverFrame");
+ // We will do the copy into a(n extra) temporary buffer inside
+ // the DeliverFrameRunnable constructor.
+ } else {
+ // Shared memory buffers of the right size are available, do the copy here.
+ PerformanceRecorder<CopyVideoStage> rec(
+ "CamerasParent::VideoFrameToShmem"_ns, mTrackingId, aVideoFrame.width(),
+ aVideoFrame.height());
+ VideoFrameUtils::CopyVideoFrameBuffers(
+ shMemBuffer.GetBytes(), properties.bufferSize(), aVideoFrame);
+ rec.Record();
+ runnable =
+ new DeliverFrameRunnable(mParent, mCapEngine, mStreamId, mTrackingId,
+ std::move(shMemBuffer), properties);
+ }
+ if (!runnable) {
+ runnable = new DeliverFrameRunnable(mParent, mCapEngine, mStreamId,
+ mTrackingId, aVideoFrame, properties);
+ }
+ MOZ_ASSERT(mParent);
+ nsIEventTarget* target = mParent->GetBackgroundEventTarget();
+ MOZ_ASSERT(target != nullptr);
+ target->Dispatch(runnable, NS_DISPATCH_NORMAL);
+}
+
+ipc::IPCResult CamerasParent::RecvReleaseFrame(ipc::Shmem&& aShmem) {
+ MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread());
+
+ mShmemPool.Put(ShmemBuffer(aShmem));
+ return IPC_OK();
+}
+
+void CamerasParent::CloseEngines() {
+ MOZ_ASSERT(mVideoCaptureThread->IsOnCurrentThread());
+ LOG_FUNCTION();
+
+ // Stop the callers
+ while (!mCallbacks.IsEmpty()) {
+ auto capEngine = mCallbacks[0]->mCapEngine;
+ auto streamNum = mCallbacks[0]->mStreamId;
+ LOG("Forcing shutdown of engine %d, capturer %d", capEngine, streamNum);
+ StopCapture(capEngine, streamNum);
+ Unused << ReleaseCapture(capEngine, streamNum);
+ }
+
+ if (VideoEngine* engine = mEngines->ElementAt(CameraEngine); engine) {
+ auto device_info = engine->GetOrCreateVideoCaptureDeviceInfo();
+ MOZ_ASSERT(device_info);
+ if (device_info) {
+ device_info->DeRegisterVideoInputFeedBack(this);
+ }
+ }
+}
+
+VideoEngine* CamerasParent::EnsureInitialized(int aEngine) {
+ MOZ_ASSERT(mVideoCaptureThread->IsOnCurrentThread());
+ LOG_VERBOSE("CamerasParent(%p)::%s", this, __func__);
+ CaptureEngine capEngine = static_cast<CaptureEngine>(aEngine);
+
+ if (VideoEngine* engine = mEngines->ElementAt(capEngine); engine) {
+ return engine;
+ }
+
+ CaptureDeviceType captureDeviceType = CaptureDeviceType::Camera;
+ switch (capEngine) {
+ case ScreenEngine:
+ captureDeviceType = CaptureDeviceType::Screen;
+ break;
+ case BrowserEngine:
+ captureDeviceType = CaptureDeviceType::Browser;
+ break;
+ case WinEngine:
+ captureDeviceType = CaptureDeviceType::Window;
+ break;
+ case CameraEngine:
+ captureDeviceType = CaptureDeviceType::Camera;
+ break;
+ default:
+ LOG("Invalid webrtc Video engine");
+ return nullptr;
+ }
+
+ RefPtr<VideoEngine> engine = VideoEngine::Create(captureDeviceType);
+ if (!engine) {
+ LOG("VideoEngine::Create failed");
+ return nullptr;
+ }
+
+ if (capEngine == CameraEngine) {
+ auto device_info = engine->GetOrCreateVideoCaptureDeviceInfo();
+ MOZ_ASSERT(device_info);
+ if (device_info) {
+ device_info->RegisterVideoInputFeedBack(this);
+ }
+ }
+
+ return mEngines->ElementAt(capEngine) = std::move(engine);
+}
+
+// Dispatch the runnable to do the camera operation on the
+// specific Cameras thread, preventing us from blocking, and
+// chain a runnable to send back the result on the IPC thread.
+// It would be nice to get rid of the code duplication here,
+// perhaps via Promises.
+ipc::IPCResult CamerasParent::RecvNumberOfCaptureDevices(
+ const CaptureEngine& aCapEngine) {
+ MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread());
+ MOZ_ASSERT(!mDestroyed);
+
+ LOG_FUNCTION();
+ LOG("CaptureEngine=%d", aCapEngine);
+
+ using Promise = MozPromise<int, bool, true>;
+ InvokeAsync(
+ mVideoCaptureThread, __func__,
+ [this, self = RefPtr(this), aCapEngine] {
+ int num = -1;
+ if (auto* engine = EnsureInitialized(aCapEngine)) {
+ if (auto devInfo = engine->GetOrCreateVideoCaptureDeviceInfo()) {
+ num = static_cast<int>(devInfo->NumberOfDevices());
+ }
+ }
+ return Promise::CreateAndResolve(
+ num, "CamerasParent::RecvNumberOfCaptureDevices");
+ })
+ ->Then(
+ mPBackgroundEventTarget, __func__,
+ [this, self = RefPtr(this)](Promise::ResolveOrRejectValue&& aValue) {
+ int nrDevices = aValue.ResolveValue();
+
+ if (mDestroyed) {
+ LOG("RecvNumberOfCaptureDevices failure: child not alive");
+ return;
+ }
+
+ if (nrDevices < 0) {
+ LOG("RecvNumberOfCaptureDevices couldn't find devices");
+ Unused << SendReplyFailure();
+ return;
+ }
+
+ LOG("RecvNumberOfCaptureDevices: %d", nrDevices);
+ Unused << SendReplyNumberOfCaptureDevices(nrDevices);
+ });
+ return IPC_OK();
+}
+
+ipc::IPCResult CamerasParent::RecvEnsureInitialized(
+ const CaptureEngine& aCapEngine) {
+ MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread());
+ MOZ_ASSERT(!mDestroyed);
+
+ LOG_FUNCTION();
+
+ using Promise = MozPromise<bool, bool, true>;
+ InvokeAsync(mVideoCaptureThread, __func__,
+ [this, self = RefPtr(this), aCapEngine] {
+ return Promise::CreateAndResolve(
+ EnsureInitialized(aCapEngine),
+ "CamerasParent::RecvEnsureInitialized");
+ })
+ ->Then(
+ mPBackgroundEventTarget, __func__,
+ [this, self = RefPtr(this)](Promise::ResolveOrRejectValue&& aValue) {
+ bool result = aValue.ResolveValue();
+
+ if (mDestroyed) {
+ LOG("RecvEnsureInitialized: child not alive");
+ return;
+ }
+
+ if (!result) {
+ LOG("RecvEnsureInitialized failed");
+ Unused << SendReplyFailure();
+ return;
+ }
+
+ LOG("RecvEnsureInitialized succeeded");
+ Unused << SendReplySuccess();
+ });
+ return IPC_OK();
+}
+
+ipc::IPCResult CamerasParent::RecvNumberOfCapabilities(
+ const CaptureEngine& aCapEngine, const nsACString& aUniqueId) {
+ MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread());
+ MOZ_ASSERT(!mDestroyed);
+
+ LOG_FUNCTION();
+ LOG("Getting caps for %s", PromiseFlatCString(aUniqueId).get());
+
+ using Promise = MozPromise<int, bool, true>;
+ InvokeAsync(
+ mVideoCaptureThread, __func__,
+ [this, self = RefPtr(this), id = nsCString(aUniqueId), aCapEngine]() {
+ int num = -1;
+ if (auto* engine = EnsureInitialized(aCapEngine)) {
+ if (auto devInfo = engine->GetOrCreateVideoCaptureDeviceInfo()) {
+ num = devInfo->NumberOfCapabilities(id.get());
+ }
+ }
+ return Promise::CreateAndResolve(
+ num, "CamerasParent::RecvNumberOfCapabilities");
+ })
+ ->Then(
+ mPBackgroundEventTarget, __func__,
+ [this, self = RefPtr(this)](Promise::ResolveOrRejectValue&& aValue) {
+ int aNrCapabilities = aValue.ResolveValue();
+
+ if (mDestroyed) {
+ LOG("RecvNumberOfCapabilities: child not alive");
+ return;
+ }
+
+ if (aNrCapabilities < 0) {
+ LOG("RecvNumberOfCapabilities couldn't find capabilities");
+ Unused << SendReplyFailure();
+ return;
+ }
+
+ LOG("RecvNumberOfCapabilities: %d", aNrCapabilities);
+ Unused << SendReplyNumberOfCapabilities(aNrCapabilities);
+ });
+ return IPC_OK();
+}
+
+ipc::IPCResult CamerasParent::RecvGetCaptureCapability(
+ const CaptureEngine& aCapEngine, const nsACString& aUniqueId,
+ const int& aIndex) {
+ MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread());
+ MOZ_ASSERT(!mDestroyed);
+
+ LOG_FUNCTION();
+ LOG("RecvGetCaptureCapability: %s %d", PromiseFlatCString(aUniqueId).get(),
+ aIndex);
+
+ using Promise = MozPromise<webrtc::VideoCaptureCapability, int, true>;
+ InvokeAsync(
+ mVideoCaptureThread, __func__,
+ [this, self = RefPtr(this), id = nsCString(aUniqueId), aCapEngine,
+ aIndex] {
+ webrtc::VideoCaptureCapability webrtcCaps;
+ int error = -1;
+ if (auto* engine = EnsureInitialized(aCapEngine)) {
+ if (auto devInfo = engine->GetOrCreateVideoCaptureDeviceInfo()) {
+ error = devInfo->GetCapability(id.get(), aIndex, webrtcCaps);
+ }
+ }
+
+ if (!error && aCapEngine == CameraEngine) {
+ auto iter = mAllCandidateCapabilities.find(id);
+ if (iter == mAllCandidateCapabilities.end()) {
+ std::map<uint32_t, webrtc::VideoCaptureCapability>
+ candidateCapabilities;
+ candidateCapabilities.emplace(aIndex, webrtcCaps);
+ mAllCandidateCapabilities.emplace(id, candidateCapabilities);
+ } else {
+ (iter->second).emplace(aIndex, webrtcCaps);
+ }
+ }
+ if (error) {
+ return Promise::CreateAndReject(
+ error, "CamerasParent::RecvGetCaptureCapability");
+ }
+ return Promise::CreateAndResolve(
+ webrtcCaps, "CamerasParent::RecvGetCaptureCapability");
+ })
+ ->Then(
+ mPBackgroundEventTarget, __func__,
+ [this, self = RefPtr(this)](Promise::ResolveOrRejectValue&& aValue) {
+ if (mDestroyed) {
+ LOG("RecvGetCaptureCapability: child not alive");
+ return;
+ }
+
+ if (aValue.IsReject()) {
+ LOG("RecvGetCaptureCapability: reply failure");
+ Unused << SendReplyFailure();
+ return;
+ }
+
+ auto webrtcCaps = aValue.ResolveValue();
+ VideoCaptureCapability capCap(
+ webrtcCaps.width, webrtcCaps.height, webrtcCaps.maxFPS,
+ static_cast<int>(webrtcCaps.videoType), webrtcCaps.interlaced);
+ LOG("Capability: %u %u %u %d %d", webrtcCaps.width,
+ webrtcCaps.height, webrtcCaps.maxFPS,
+ static_cast<int>(webrtcCaps.videoType), webrtcCaps.interlaced);
+ Unused << SendReplyGetCaptureCapability(capCap);
+ });
+ return IPC_OK();
+}
+
+ipc::IPCResult CamerasParent::RecvGetCaptureDevice(
+ const CaptureEngine& aCapEngine, const int& aDeviceIndex) {
+ MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread());
+ MOZ_ASSERT(!mDestroyed);
+
+ LOG_FUNCTION();
+
+ using Data = std::tuple<nsCString, nsCString, pid_t, int>;
+ using Promise = MozPromise<Data, bool, true>;
+ InvokeAsync(
+ mVideoCaptureThread, __func__,
+ [this, self = RefPtr(this), aCapEngine, aDeviceIndex] {
+ char deviceName[MediaEngineSource::kMaxDeviceNameLength];
+ char deviceUniqueId[MediaEngineSource::kMaxUniqueIdLength];
+ nsCString name;
+ nsCString uniqueId;
+ pid_t devicePid = 0;
+ int error = -1;
+ if (auto* engine = EnsureInitialized(aCapEngine)) {
+ if (auto devInfo = engine->GetOrCreateVideoCaptureDeviceInfo()) {
+ error = devInfo->GetDeviceName(
+ aDeviceIndex, deviceName, sizeof(deviceName), deviceUniqueId,
+ sizeof(deviceUniqueId), nullptr, 0, &devicePid);
+ }
+ }
+ if (error == 0) {
+ name.Assign(deviceName);
+ uniqueId.Assign(deviceUniqueId);
+ }
+ return Promise::CreateAndResolve(
+ std::make_tuple(std::move(name), std::move(uniqueId), devicePid,
+ error),
+ "CamerasParent::RecvGetCaptureDevice");
+ })
+ ->Then(
+ mPBackgroundEventTarget, __func__,
+ [this, self = RefPtr(this)](Promise::ResolveOrRejectValue&& aValue) {
+ const auto& [name, uniqueId, devicePid, error] =
+ aValue.ResolveValue();
+ if (mDestroyed) {
+ return;
+ }
+ if (error != 0) {
+ LOG("GetCaptureDevice failed: %d", error);
+ Unused << SendReplyFailure();
+ return;
+ }
+ bool scary = (devicePid == getpid());
+
+ LOG("Returning %s name %s id (pid = %d)%s", name.get(),
+ uniqueId.get(), devicePid, (scary ? " (scary)" : ""));
+ Unused << SendReplyGetCaptureDevice(name, uniqueId, scary);
+ });
+ return IPC_OK();
+}
+
+// Find out whether the given window with id has permission to use the
+// camera. If the permission is not persistent, we'll make it a one-shot by
+// removing the (session) permission.
+static bool HasCameraPermission(const uint64_t& aWindowId) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<dom::WindowGlobalParent> window =
+ dom::WindowGlobalParent::GetByInnerWindowId(aWindowId);
+ if (!window) {
+ // Could not find window by id
+ return false;
+ }
+
+ // If we delegate permission from first party, we should use the top level
+ // window
+ if (StaticPrefs::permissions_delegation_enabled()) {
+ RefPtr<dom::BrowsingContext> topBC = window->BrowsingContext()->Top();
+ window = topBC->Canonical()->GetCurrentWindowGlobal();
+ }
+
+ // Return false if the window is not the currently-active window for its
+ // BrowsingContext.
+ if (!window || !window->IsCurrentGlobal()) {
+ return false;
+ }
+
+ nsIPrincipal* principal = window->DocumentPrincipal();
+ if (principal->GetIsNullPrincipal()) {
+ return false;
+ }
+
+ if (principal->IsSystemPrincipal()) {
+ return true;
+ }
+
+ MOZ_ASSERT(principal->GetIsContentPrincipal());
+
+ nsresult rv;
+ // Name used with nsIPermissionManager
+ static const nsLiteralCString cameraPermission = "MediaManagerVideo"_ns;
+ nsCOMPtr<nsIPermissionManager> mgr =
+ do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ uint32_t video = nsIPermissionManager::UNKNOWN_ACTION;
+ rv = mgr->TestExactPermissionFromPrincipal(principal, cameraPermission,
+ &video);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ bool allowed = (video == nsIPermissionManager::ALLOW_ACTION);
+
+ // Session permissions are removed after one use.
+ if (allowed) {
+ mgr->RemoveFromPrincipal(principal, cameraPermission);
+ }
+
+ return allowed;
+}
+
+ipc::IPCResult CamerasParent::RecvAllocateCapture(
+ const CaptureEngine& aCapEngine, const nsACString& aUniqueIdUTF8,
+ const uint64_t& aWindowID) {
+ MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread());
+ MOZ_ASSERT(!mDestroyed);
+
+ LOG("CamerasParent(%p)::%s: Verifying permissions", this, __func__);
+
+ using Promise1 = MozPromise<bool, bool, true>;
+ using Data = std::tuple<int, int>;
+ using Promise2 = MozPromise<Data, bool, true>;
+ InvokeAsync(GetMainThreadSerialEventTarget(), __func__,
+ [aWindowID] {
+ // Verify whether the claimed origin has received permission
+ // to use the camera, either persistently or this session (one
+ // shot).
+ bool allowed = HasCameraPermission(aWindowID);
+ if (!allowed) {
+ // Developer preference for turning off permission check.
+ if (Preferences::GetBool(
+ "media.navigator.permission.disabled", false)) {
+ allowed = true;
+ LOG("No permission but checks are disabled");
+ } else {
+ LOG("No camera permission for this origin");
+ }
+ }
+ return Promise1::CreateAndResolve(
+ allowed, "CamerasParent::RecvAllocateCapture");
+ })
+ ->Then(mVideoCaptureThread, __func__,
+ [this, self = RefPtr(this), aCapEngine,
+ unique_id = nsCString(aUniqueIdUTF8)](
+ Promise1::ResolveOrRejectValue&& aValue) {
+ bool allowed = aValue.ResolveValue();
+ int captureId = -1;
+ int error = -1;
+ if (allowed && EnsureInitialized(aCapEngine)) {
+ VideoEngine* engine = mEngines->ElementAt(aCapEngine);
+ captureId = engine->CreateVideoCapture(unique_id.get());
+ engine->WithEntry(captureId,
+ [&error](VideoEngine::CaptureEntry& cap) {
+ if (cap.VideoCapture()) {
+ error = 0;
+ }
+ });
+ }
+ return Promise2::CreateAndResolve(
+ std::make_tuple(captureId, error),
+ "CamerasParent::RecvAllocateCapture");
+ })
+ ->Then(
+ mPBackgroundEventTarget, __func__,
+ [this, self = RefPtr(this)](Promise2::ResolveOrRejectValue&& aValue) {
+ const auto [captureId, error] = aValue.ResolveValue();
+ if (mDestroyed) {
+ LOG("RecvAllocateCapture: child not alive");
+ return;
+ }
+
+ if (error != 0) {
+ Unused << SendReplyFailure();
+ LOG("RecvAllocateCapture: WithEntry error");
+ return;
+ }
+
+ LOG("Allocated device nr %d", captureId);
+ Unused << SendReplyAllocateCapture(captureId);
+ });
+ return IPC_OK();
+}
+
+int CamerasParent::ReleaseCapture(const CaptureEngine& aCapEngine,
+ int aCaptureId) {
+ MOZ_ASSERT(mVideoCaptureThread->IsOnCurrentThread());
+ int error = -1;
+ if (auto* engine = EnsureInitialized(aCapEngine)) {
+ error = engine->ReleaseVideoCapture(aCaptureId);
+ }
+ return error;
+}
+
+ipc::IPCResult CamerasParent::RecvReleaseCapture(
+ const CaptureEngine& aCapEngine, const int& aCaptureId) {
+ MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread());
+ MOZ_ASSERT(!mDestroyed);
+
+ LOG_FUNCTION();
+ LOG("RecvReleaseCamera device nr %d", aCaptureId);
+
+ using Promise = MozPromise<int, bool, true>;
+ InvokeAsync(mVideoCaptureThread, __func__,
+ [this, self = RefPtr(this), aCapEngine, aCaptureId] {
+ return Promise::CreateAndResolve(
+ ReleaseCapture(aCapEngine, aCaptureId),
+ "CamerasParent::RecvReleaseCapture");
+ })
+ ->Then(mPBackgroundEventTarget, __func__,
+ [this, self = RefPtr(this),
+ aCaptureId](Promise::ResolveOrRejectValue&& aValue) {
+ int error = aValue.ResolveValue();
+
+ if (mDestroyed) {
+ LOG("RecvReleaseCapture: child not alive");
+ return;
+ }
+
+ if (error != 0) {
+ Unused << SendReplyFailure();
+ LOG("RecvReleaseCapture: Failed to free device nr %d",
+ aCaptureId);
+ return;
+ }
+
+ Unused << SendReplySuccess();
+ LOG("Freed device nr %d", aCaptureId);
+ });
+ return IPC_OK();
+}
+
+ipc::IPCResult CamerasParent::RecvStartCapture(
+ const CaptureEngine& aCapEngine, const int& aCaptureId,
+ const VideoCaptureCapability& aIpcCaps) {
+ MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread());
+ MOZ_ASSERT(!mDestroyed);
+
+ LOG_FUNCTION();
+
+ using Promise = MozPromise<int, bool, true>;
+ InvokeAsync(
+ mVideoCaptureThread, __func__,
+ [this, self = RefPtr(this), aCapEngine, aCaptureId, aIpcCaps] {
+ LOG_FUNCTION();
+ CallbackHelper** cbh;
+ int error = -1;
+
+ if (!EnsureInitialized(aCapEngine)) {
+ return Promise::CreateAndResolve(error,
+ "CamerasParent::RecvStartCapture");
+ }
+
+ cbh = mCallbacks.AppendElement(new CallbackHelper(
+ static_cast<CaptureEngine>(aCapEngine), aCaptureId, this));
+
+ mEngines->ElementAt(aCapEngine)
+ ->WithEntry(aCaptureId, [&](VideoEngine::CaptureEntry& cap) {
+ webrtc::VideoCaptureCapability capability;
+ capability.width = aIpcCaps.width();
+ capability.height = aIpcCaps.height();
+ capability.maxFPS = aIpcCaps.maxFPS();
+ capability.videoType =
+ static_cast<webrtc::VideoType>(aIpcCaps.videoType());
+ capability.interlaced = aIpcCaps.interlaced();
+
+#ifndef FUZZING_SNAPSHOT
+ MOZ_DIAGNOSTIC_ASSERT(sDeviceUniqueIDs.find(aCaptureId) ==
+ sDeviceUniqueIDs.end());
+#endif
+ sDeviceUniqueIDs.emplace(aCaptureId,
+ cap.VideoCapture()->CurrentDeviceName());
+
+#ifndef FUZZING_SNAPSHOT
+ MOZ_DIAGNOSTIC_ASSERT(
+ sAllRequestedCapabilities.find(aCaptureId) ==
+ sAllRequestedCapabilities.end());
+#endif
+ sAllRequestedCapabilities.emplace(aCaptureId, capability);
+
+ if (aCapEngine == CameraEngine) {
+ for (const auto& it : sDeviceUniqueIDs) {
+ if (strcmp(it.second,
+ cap.VideoCapture()->CurrentDeviceName()) == 0) {
+ capability.width =
+ std::max(capability.width,
+ sAllRequestedCapabilities[it.first].width);
+ capability.height =
+ std::max(capability.height,
+ sAllRequestedCapabilities[it.first].height);
+ capability.maxFPS =
+ std::max(capability.maxFPS,
+ sAllRequestedCapabilities[it.first].maxFPS);
+ }
+ }
+
+ auto candidateCapabilities = mAllCandidateCapabilities.find(
+ nsCString(cap.VideoCapture()->CurrentDeviceName()));
+ if ((candidateCapabilities !=
+ mAllCandidateCapabilities.end()) &&
+ (!candidateCapabilities->second.empty())) {
+ int32_t minIdx = -1;
+ uint64_t minDistance = UINT64_MAX;
+
+ for (auto& candidateCapability :
+ candidateCapabilities->second) {
+ if (candidateCapability.second.videoType !=
+ capability.videoType) {
+ continue;
+ }
+ // The first priority is finding a suitable resolution.
+ // So here we raise the weight of width and height
+ uint64_t distance = uint64_t(ResolutionFeasibilityDistance(
+ candidateCapability.second.width,
+ capability.width)) +
+ uint64_t(ResolutionFeasibilityDistance(
+ candidateCapability.second.height,
+ capability.height)) +
+ uint64_t(FeasibilityDistance(
+ candidateCapability.second.maxFPS,
+ capability.maxFPS));
+ if (distance < minDistance) {
+ minIdx = static_cast<int32_t>(candidateCapability.first);
+ minDistance = distance;
+ }
+ }
+ MOZ_ASSERT(minIdx != -1);
+ capability = candidateCapabilities->second[minIdx];
+ }
+ } else if (aCapEngine == ScreenEngine ||
+ aCapEngine == BrowserEngine ||
+ aCapEngine == WinEngine) {
+ for (const auto& it : sDeviceUniqueIDs) {
+ if (strcmp(it.second,
+ cap.VideoCapture()->CurrentDeviceName()) == 0) {
+ capability.maxFPS =
+ std::max(capability.maxFPS,
+ sAllRequestedCapabilities[it.first].maxFPS);
+ }
+ }
+ }
+
+ cap.VideoCapture()->SetTrackingId(
+ (*cbh)->mTrackingId.mUniqueInProcId);
+ error = cap.VideoCapture()->StartCapture(capability);
+
+ if (!error) {
+ cap.VideoCapture()->RegisterCaptureDataCallback(
+ static_cast<rtc::VideoSinkInterface<webrtc::VideoFrame>*>(
+ *cbh));
+ } else {
+ sDeviceUniqueIDs.erase(aCaptureId);
+ sAllRequestedCapabilities.erase(aCaptureId);
+ }
+ });
+
+ return Promise::CreateAndResolve(error,
+ "CamerasParent::RecvStartCapture");
+ })
+ ->Then(
+ mPBackgroundEventTarget, __func__,
+ [this, self = RefPtr(this)](Promise::ResolveOrRejectValue&& aValue) {
+ int error = aValue.ResolveValue();
+
+ if (mDestroyed) {
+ LOG("RecvStartCapture failure: child is not alive");
+ return;
+ }
+
+ if (error != 0) {
+ LOG("RecvStartCapture failure: StartCapture failed");
+ Unused << SendReplyFailure();
+ return;
+ }
+
+ Unused << SendReplySuccess();
+ });
+ return IPC_OK();
+}
+
+ipc::IPCResult CamerasParent::RecvFocusOnSelectedSource(
+ const CaptureEngine& aCapEngine, const int& aCaptureId) {
+ MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread());
+ MOZ_ASSERT(!mDestroyed);
+
+ LOG_FUNCTION();
+
+ using Promise = MozPromise<bool, bool, true>;
+ InvokeAsync(mVideoCaptureThread, __func__,
+ [this, self = RefPtr(this), aCapEngine, aCaptureId] {
+ bool result = false;
+ if (auto* engine = EnsureInitialized(aCapEngine)) {
+ engine->WithEntry(
+ aCaptureId, [&](VideoEngine::CaptureEntry& cap) {
+ if (cap.VideoCapture()) {
+ result = cap.VideoCapture()->FocusOnSelectedSource();
+ }
+ });
+ }
+ return Promise::CreateAndResolve(
+ result, "CamerasParent::RecvFocusOnSelectedSource");
+ })
+ ->Then(
+ mPBackgroundEventTarget, __func__,
+ [this, self = RefPtr(this)](Promise::ResolveOrRejectValue&& aValue) {
+ bool result = aValue.ResolveValue();
+ if (mDestroyed) {
+ LOG("RecvFocusOnSelectedSource failure: child is not alive");
+ return;
+ }
+
+ if (!result) {
+ Unused << SendReplyFailure();
+ LOG("RecvFocusOnSelectedSource failure.");
+ return;
+ }
+
+ Unused << SendReplySuccess();
+ });
+ return IPC_OK();
+}
+
+void CamerasParent::StopCapture(const CaptureEngine& aCapEngine,
+ int aCaptureId) {
+ MOZ_ASSERT(mVideoCaptureThread->IsOnCurrentThread());
+ if (auto* engine = EnsureInitialized(aCapEngine)) {
+ // we're removing elements, iterate backwards
+ for (size_t i = mCallbacks.Length(); i > 0; i--) {
+ if (mCallbacks[i - 1]->mCapEngine == aCapEngine &&
+ mCallbacks[i - 1]->mStreamId == (uint32_t)aCaptureId) {
+ CallbackHelper* cbh = mCallbacks[i - 1];
+ engine->WithEntry(aCaptureId, [cbh, &aCaptureId](
+ VideoEngine::CaptureEntry& cap) {
+ if (cap.VideoCapture()) {
+ cap.VideoCapture()->DeRegisterCaptureDataCallback(
+ static_cast<rtc::VideoSinkInterface<webrtc::VideoFrame>*>(cbh));
+ cap.VideoCapture()->StopCaptureIfAllClientsClose();
+
+ sDeviceUniqueIDs.erase(aCaptureId);
+ sAllRequestedCapabilities.erase(aCaptureId);
+ }
+ });
+
+ delete mCallbacks[i - 1];
+ mCallbacks.RemoveElementAt(i - 1);
+ break;
+ }
+ }
+ }
+}
+
+ipc::IPCResult CamerasParent::RecvStopCapture(const CaptureEngine& aCapEngine,
+ const int& aCaptureId) {
+ MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread());
+ MOZ_ASSERT(!mDestroyed);
+
+ LOG_FUNCTION();
+
+ nsresult rv = mVideoCaptureThread->Dispatch(NS_NewRunnableFunction(
+ __func__, [this, self = RefPtr(this), aCapEngine, aCaptureId] {
+ StopCapture(aCapEngine, aCaptureId);
+ }));
+
+ if (mDestroyed) {
+ if (NS_FAILED(rv)) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+ } else {
+ if (NS_SUCCEEDED(rv)) {
+ if (!SendReplySuccess()) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+ } else {
+ if (!SendReplyFailure()) {
+ return IPC_FAIL_NO_REASON(this);
+ }
+ }
+ }
+ return IPC_OK();
+}
+
+void CamerasParent::ActorDestroy(ActorDestroyReason aWhy) {
+ MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread());
+ LOG_FUNCTION();
+
+ // Release shared memory now, it's our last chance
+ mShmemPool.Cleanup(this);
+ // We don't want to receive callbacks or anything if we can't
+ // forward them anymore anyway.
+ mDestroyed = true;
+ // We don't need to listen for shutdown any longer. Disconnect the request.
+ // This breaks the reference cycle between CamerasParent and the shutdown
+ // promise's Then handler.
+ mShutdownRequest.DisconnectIfExists();
+
+ if (mVideoCaptureThread) {
+ // Shut down the WebRTC stack, on the video capture thread.
+ MOZ_ALWAYS_SUCCEEDS(mVideoCaptureThread->Dispatch(
+ NewRunnableMethod(__func__, this, &CamerasParent::CloseEngines)));
+ }
+}
+
+void CamerasParent::OnShutdown() {
+ ipc::AssertIsOnBackgroundThread();
+ LOG("CamerasParent(%p) ShutdownEvent", this);
+ mShutdownRequest.Complete();
+ (void)Send__delete__(this);
+}
+
+CamerasParent::CamerasParent()
+ : mShutdownBlocker(ShutdownBlockingTicket::Create(
+ u"CamerasParent"_ns, NS_LITERAL_STRING_FROM_CSTRING(__FILE__),
+ __LINE__)),
+ mVideoCaptureThread(mShutdownBlocker
+ ? MakeAndAddRefVideoCaptureThreadAndSingletons()
+ : nullptr),
+ mEngines(sEngines),
+ mShmemPool(CaptureEngine::MaxEngine),
+ mPBackgroundEventTarget(GetCurrentSerialEventTarget()),
+ mDestroyed(false) {
+ MOZ_ASSERT(mPBackgroundEventTarget != nullptr,
+ "GetCurrentThreadEventTarget failed");
+ LOG("CamerasParent: %p", this);
+
+ // Don't dispatch from the constructor a runnable that may toggle the
+ // reference count, because the IPC thread does not get a reference until
+ // after the constructor returns.
+}
+
+// RecvPCamerasConstructor() is used because IPC messages, for
+// Send__delete__(), cannot be sent from AllocPCamerasParent().
+ipc::IPCResult CamerasParent::RecvPCamerasConstructor() {
+ MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread());
+
+ // AsyncShutdown barriers are available only for ShutdownPhases as late as
+ // XPCOMWillShutdown. The IPC background thread shuts down during
+ // XPCOMShutdownThreads, so actors may be created when AsyncShutdown barriers
+ // are no longer available. Should shutdown be past XPCOMWillShutdown we end
+ // up with a null mShutdownBlocker.
+
+ if (!mShutdownBlocker) {
+ LOG("CamerasParent(%p) Got no ShutdownBlockingTicket. We are already in "
+ "shutdown. Deleting.",
+ this);
+ return Send__delete__(this) ? IPC_OK() : IPC_FAIL(this, "Failed to send");
+ }
+
+ if (!mVideoCaptureThread) {
+ return Send__delete__(this) ? IPC_OK() : IPC_FAIL(this, "Failed to send");
+ }
+
+ MOZ_ASSERT(mEngines);
+
+ mShutdownBlocker->ShutdownPromise()
+ ->Then(mPBackgroundEventTarget, "CamerasParent OnShutdown",
+ [this, self = RefPtr(this)](
+ const ShutdownPromise::ResolveOrRejectValue& aValue) {
+ MOZ_ASSERT(aValue.IsResolve(),
+ "ShutdownBlockingTicket must have been destroyed "
+ "without us disconnecting the shutdown request");
+ OnShutdown();
+ })
+ ->Track(mShutdownRequest);
+
+ return IPC_OK();
+}
+
+CamerasParent::~CamerasParent() {
+ ipc::AssertIsOnBackgroundThread();
+ LOG_FUNCTION();
+
+ if (!mVideoCaptureThread) {
+ // No video engines or video capture thread to shutdown here.
+ return;
+ }
+
+ MOZ_ASSERT(mShutdownBlocker,
+ "A ShutdownBlocker is a prerequisite for mVideoCaptureThread");
+
+ ReleaseVideoCaptureThreadAndSingletons();
+}
+
+already_AddRefed<CamerasParent> CamerasParent::Create() {
+ ipc::AssertIsOnBackgroundThread();
+ return MakeAndAddRef<CamerasParent>();
+}
+
+} // namespace camera
+} // namespace mozilla
diff --git a/dom/media/systemservices/CamerasParent.h b/dom/media/systemservices/CamerasParent.h
new file mode 100644
index 0000000000..97e7ea0bd4
--- /dev/null
+++ b/dom/media/systemservices/CamerasParent.h
@@ -0,0 +1,157 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_CamerasParent_h
+#define mozilla_CamerasParent_h
+
+#include "CamerasChild.h"
+#include "VideoEngine.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/camera/PCamerasParent.h"
+#include "mozilla/ipc/Shmem.h"
+#include "mozilla/ShmemPool.h"
+#include "api/video/video_sink_interface.h"
+#include "modules/video_capture/video_capture.h"
+#include "modules/video_capture/video_capture_defines.h"
+#include "video/render/incoming_video_stream.h"
+
+class nsIThread;
+
+namespace mozilla::camera {
+
+class CamerasParent;
+
+class CallbackHelper : public rtc::VideoSinkInterface<webrtc::VideoFrame> {
+ public:
+ CallbackHelper(CaptureEngine aCapEng, uint32_t aStreamId,
+ CamerasParent* aParent)
+ : mCapEngine(aCapEng),
+ mStreamId(aStreamId),
+ mTrackingId(CaptureEngineToTrackingSourceStr(aCapEng), aStreamId),
+ mParent(aParent){};
+
+ // These callbacks end up running on the VideoCapture thread.
+ // From VideoCaptureCallback
+ void OnFrame(const webrtc::VideoFrame& aVideoFrame) override;
+
+ friend CamerasParent;
+
+ private:
+ const CaptureEngine mCapEngine;
+ const uint32_t mStreamId;
+ const TrackingId mTrackingId;
+ CamerasParent* const mParent;
+};
+
+class DeliverFrameRunnable;
+
+class CamerasParent final : public PCamerasParent,
+ private webrtc::VideoInputFeedBack {
+ using ShutdownMozPromise = media::ShutdownBlockingTicket::ShutdownMozPromise;
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING_WITH_DELETE_ON_EVENT_TARGET(
+ CamerasParent, mPBackgroundEventTarget)
+
+ public:
+ class VideoEngineArray;
+ friend DeliverFrameRunnable;
+
+ static already_AddRefed<CamerasParent> Create();
+
+ // Messages received from the child. These run on the IPC/PBackground thread.
+ mozilla::ipc::IPCResult RecvPCamerasConstructor();
+ mozilla::ipc::IPCResult RecvAllocateCapture(
+ const CaptureEngine& aCapEngine, const nsACString& aUniqueIdUTF8,
+ const uint64_t& aWindowID) override;
+ mozilla::ipc::IPCResult RecvReleaseCapture(const CaptureEngine& aCapEngine,
+ const int& aCaptureId) override;
+ mozilla::ipc::IPCResult RecvNumberOfCaptureDevices(
+ const CaptureEngine& aCapEngine) override;
+ mozilla::ipc::IPCResult RecvNumberOfCapabilities(
+ const CaptureEngine& aCapEngine, const nsACString& aUniqueId) override;
+ mozilla::ipc::IPCResult RecvGetCaptureCapability(
+ const CaptureEngine& aCapEngine, const nsACString& aUniqueId,
+ const int& aIndex) override;
+ mozilla::ipc::IPCResult RecvGetCaptureDevice(
+ const CaptureEngine& aCapEngine, const int& aDeviceIndex) override;
+ mozilla::ipc::IPCResult RecvStartCapture(
+ const CaptureEngine& aCapEngine, const int& aCaptureId,
+ const VideoCaptureCapability& aIpcCaps) override;
+ mozilla::ipc::IPCResult RecvFocusOnSelectedSource(
+ const CaptureEngine& aCapEngine, const int& aCaptureId) override;
+ mozilla::ipc::IPCResult RecvStopCapture(const CaptureEngine& aCapEngine,
+ const int& aCaptureId) override;
+ mozilla::ipc::IPCResult RecvReleaseFrame(
+ mozilla::ipc::Shmem&& aShmem) override;
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+ mozilla::ipc::IPCResult RecvEnsureInitialized(
+ const CaptureEngine& aCapEngine) override;
+
+ nsIEventTarget* GetBackgroundEventTarget() {
+ return mPBackgroundEventTarget;
+ };
+ bool IsShuttingDown() {
+ // the first 2 are pBackground only, the last is atomic
+ MOZ_ASSERT(mPBackgroundEventTarget->IsOnCurrentThread());
+ return mDestroyed;
+ };
+ ShmemBuffer GetBuffer(size_t aSize);
+
+ // helper to forward to the PBackground thread
+ int DeliverFrameOverIPC(CaptureEngine aCapEngine, uint32_t aStreamId,
+ const TrackingId& aTrackingId, ShmemBuffer aBuffer,
+ unsigned char* aAltBuffer,
+ const VideoFrameProperties& aProps);
+
+ CamerasParent();
+
+ private:
+ virtual ~CamerasParent();
+
+ // We use these helpers for shutdown and for the respective IPC commands.
+ void StopCapture(const CaptureEngine& aCapEngine, int aCaptureId);
+ int ReleaseCapture(const CaptureEngine& aCapEngine, int aCaptureId);
+
+ // VideoInputFeedBack
+ void OnDeviceChange() override;
+
+ VideoEngine* EnsureInitialized(int aEngine);
+
+ // Stops any ongoing capturing and releases resources. Called on
+ // mVideoCaptureThread. Idempotent.
+ void CloseEngines();
+
+ void OnShutdown();
+
+ nsTArray<CallbackHelper*> mCallbacks;
+ // If existent, blocks xpcom shutdown while alive.
+ // Note that this makes a reference cycle that gets broken in ActorDestroy().
+ const UniquePtr<media::ShutdownBlockingTicket> mShutdownBlocker;
+ // Tracks the mShutdownBlocker shutdown handler. mPBackgroundEventTarget only.
+ MozPromiseRequestHolder<ShutdownMozPromise> mShutdownRequest;
+
+ // Local copy of sVideoCaptureThread. Guaranteed alive if non-null.
+ const nsCOMPtr<nsISerialEventTarget> mVideoCaptureThread;
+
+ // Reference to same VideoEngineArray as sEngines. Video capture thread only.
+ const RefPtr<VideoEngineArray> mEngines;
+
+ // image buffers
+ ShmemPool mShmemPool;
+
+ // PBackgroundParent thread
+ const nsCOMPtr<nsISerialEventTarget> mPBackgroundEventTarget;
+
+ // Set to true in ActorDestroy. PBackground only.
+ bool mDestroyed;
+
+ std::map<nsCString, std::map<uint32_t, webrtc::VideoCaptureCapability>>
+ mAllCandidateCapabilities;
+};
+
+} // namespace mozilla::camera
+
+#endif // mozilla_CameraParent_h
diff --git a/dom/media/systemservices/CamerasTypes.cpp b/dom/media/systemservices/CamerasTypes.cpp
new file mode 100644
index 0000000000..7eda2f650b
--- /dev/null
+++ b/dom/media/systemservices/CamerasTypes.cpp
@@ -0,0 +1,26 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CamerasTypes.h"
+
+namespace mozilla::camera {
+
+TrackingId::Source CaptureEngineToTrackingSourceStr(
+ const CaptureEngine& aEngine) {
+ switch (aEngine) {
+ case ScreenEngine:
+ return TrackingId::Source::Screen;
+ case BrowserEngine:
+ return TrackingId::Source::Tab;
+ case WinEngine:
+ return TrackingId::Source::Window;
+ case CameraEngine:
+ return TrackingId::Source::Camera;
+ default:
+ return TrackingId::Source::Unimplemented;
+ }
+}
+} // namespace mozilla::camera
diff --git a/dom/media/systemservices/CamerasTypes.h b/dom/media/systemservices/CamerasTypes.h
new file mode 100644
index 0000000000..fed3941d9c
--- /dev/null
+++ b/dom/media/systemservices/CamerasTypes.h
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_CamerasTypes_h
+#define mozilla_CamerasTypes_h
+
+#include "ipc/EnumSerializer.h"
+#include "PerformanceRecorder.h"
+
+namespace mozilla::camera {
+
+enum CaptureEngine : int {
+ InvalidEngine = 0,
+ ScreenEngine,
+ BrowserEngine,
+ WinEngine,
+ CameraEngine,
+ MaxEngine
+};
+
+TrackingId::Source CaptureEngineToTrackingSourceStr(
+ const CaptureEngine& aEngine);
+
+} // namespace mozilla::camera
+
+namespace IPC {
+template <>
+struct ParamTraits<mozilla::camera::CaptureEngine>
+ : public ContiguousEnumSerializer<
+ mozilla::camera::CaptureEngine,
+ mozilla::camera::CaptureEngine::InvalidEngine,
+ mozilla::camera::CaptureEngine::MaxEngine> {};
+} // namespace IPC
+
+#endif // mozilla_CamerasTypes_h
diff --git a/dom/media/systemservices/MediaChild.cpp b/dom/media/systemservices/MediaChild.cpp
new file mode 100644
index 0000000000..34780ed74f
--- /dev/null
+++ b/dom/media/systemservices/MediaChild.cpp
@@ -0,0 +1,95 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaChild.h"
+#include "MediaParent.h"
+
+#include "nsGlobalWindow.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/MediaManager.h"
+#include "mozilla/Logging.h"
+#include "nsQueryObject.h"
+
+#undef LOG
+mozilla::LazyLogModule gMediaChildLog("MediaChild");
+#define LOG(args) MOZ_LOG(gMediaChildLog, mozilla::LogLevel::Debug, args)
+
+namespace mozilla::media {
+
+RefPtr<PrincipalKeyPromise> GetPrincipalKey(
+ const ipc::PrincipalInfo& aPrincipalInfo, bool aPersist) {
+ RefPtr<MediaManager> mgr = MediaManager::GetInstance();
+ MOZ_ASSERT(mgr);
+
+ if (XRE_GetProcessType() == GeckoProcessType_Default) {
+ auto p = MakeRefPtr<PrincipalKeyPromise::Private>(__func__);
+
+ mgr->GetNonE10sParent()->RecvGetPrincipalKey(
+ aPrincipalInfo, aPersist,
+ [p](const nsACString& aKey) { p->Resolve(aKey, __func__); });
+ return p;
+ }
+ return Child::Get()
+ ->SendGetPrincipalKey(aPrincipalInfo, aPersist)
+ ->Then(GetMainThreadSerialEventTarget(), __func__,
+ [](const Child::GetPrincipalKeyPromise::ResolveOrRejectValue&
+ aValue) {
+ if (aValue.IsReject() || aValue.ResolveValue().IsEmpty()) {
+ return PrincipalKeyPromise::CreateAndReject(NS_ERROR_FAILURE,
+ __func__);
+ }
+ return PrincipalKeyPromise::CreateAndResolve(
+ aValue.ResolveValue(), __func__);
+ });
+}
+
+void SanitizeOriginKeys(const uint64_t& aSinceWhen, bool aOnlyPrivateBrowsing) {
+ LOG(("SanitizeOriginKeys since %" PRIu64 " %s", aSinceWhen,
+ (aOnlyPrivateBrowsing ? "in Private Browsing." : ".")));
+
+ if (XRE_GetProcessType() == GeckoProcessType_Default) {
+ // Avoid opening MediaManager in this case, since this is called by
+ // sanitize.js when cookies are cleared, which can happen on startup.
+ RefPtr<Parent<NonE10s>> tmpParent = new Parent<NonE10s>();
+ tmpParent->RecvSanitizeOriginKeys(aSinceWhen, aOnlyPrivateBrowsing);
+ } else {
+ Child::Get()->SendSanitizeOriginKeys(aSinceWhen, aOnlyPrivateBrowsing);
+ }
+}
+
+static Child* sChild;
+
+Child* Child::Get() {
+ MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_Content);
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!sChild) {
+ sChild = static_cast<Child*>(
+ dom::ContentChild::GetSingleton()->SendPMediaConstructor());
+ }
+ return sChild;
+}
+
+Child::Child() : mActorDestroyed(false) {
+ LOG(("media::Child: %p", this));
+ MOZ_COUNT_CTOR(Child);
+}
+
+Child::~Child() {
+ LOG(("~media::Child: %p", this));
+ sChild = nullptr;
+ MOZ_COUNT_DTOR(Child);
+}
+
+void Child::ActorDestroy(ActorDestroyReason aWhy) { mActorDestroyed = true; }
+
+PMediaChild* AllocPMediaChild() { return new Child(); }
+
+bool DeallocPMediaChild(media::PMediaChild* aActor) {
+ delete static_cast<Child*>(aActor);
+ return true;
+}
+
+} // namespace mozilla::media
diff --git a/dom/media/systemservices/MediaChild.h b/dom/media/systemservices/MediaChild.h
new file mode 100644
index 0000000000..b0ae776cce
--- /dev/null
+++ b/dom/media/systemservices/MediaChild.h
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_MediaChild_h
+#define mozilla_MediaChild_h
+
+#include "mozilla/media/PMediaChild.h"
+#include "mozilla/media/PMediaParent.h"
+#include "MediaUtils.h"
+
+namespace mozilla {
+
+namespace ipc {
+class PrincipalInfo;
+}
+
+namespace media {
+
+typedef MozPromise<nsCString, nsresult, false> PrincipalKeyPromise;
+
+// media::Child implements proxying to the chrome process for some media-related
+// functions, for the moment just:
+//
+// GetPrincipalKey() - get a cookie-like persisted unique key for a given
+// principalInfo.
+//
+// SanitizeOriginKeys() - reset persisted unique keys.
+
+// GetPrincipalKey and SanitizeOriginKeys are asynchronous APIs that return
+// pledges (promise-like objects) with the future value. Use pledge.Then(func)
+// to access.
+
+RefPtr<PrincipalKeyPromise> GetPrincipalKey(
+ const mozilla::ipc::PrincipalInfo& aPrincipalInfo, bool aPersist);
+
+void SanitizeOriginKeys(const uint64_t& aSinceWhen, bool aOnlyPrivateBrowsing);
+
+class Child : public PMediaChild {
+ public:
+ static Child* Get();
+
+ Child();
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+ virtual ~Child();
+
+ private:
+ bool mActorDestroyed;
+};
+
+PMediaChild* AllocPMediaChild();
+bool DeallocPMediaChild(PMediaChild* aActor);
+
+} // namespace media
+} // namespace mozilla
+
+#endif // mozilla_MediaChild_h
diff --git a/dom/media/systemservices/MediaParent.cpp b/dom/media/systemservices/MediaParent.cpp
new file mode 100644
index 0000000000..d2fb06b8ae
--- /dev/null
+++ b/dom/media/systemservices/MediaParent.cpp
@@ -0,0 +1,536 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaParent.h"
+
+#include "mozilla/Base64.h"
+#include <mozilla/StaticMutex.h>
+
+#include "MediaUtils.h"
+#include "MediaEngine.h"
+#include "VideoUtils.h"
+#include "nsClassHashtable.h"
+#include "nsThreadUtils.h"
+#include "nsNetCID.h"
+#include "nsNetUtil.h"
+#include "nsIInputStream.h"
+#include "nsILineInputStream.h"
+#include "nsIOutputStream.h"
+#include "nsISafeOutputStream.h"
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsIFile.h"
+#include "nsISupportsImpl.h"
+#include "mozilla/Logging.h"
+
+#undef LOG
+mozilla::LazyLogModule gMediaParentLog("MediaParent");
+#define LOG(args) MOZ_LOG(gMediaParentLog, mozilla::LogLevel::Debug, args)
+
+// A file in the profile dir is used to persist mOriginKeys used to anonymize
+// deviceIds to be unique per origin, to avoid them being supercookies.
+
+#define ORIGINKEYS_FILE u"enumerate_devices.txt"
+#define ORIGINKEYS_VERSION "1"
+
+namespace mozilla::media {
+
+StaticMutex sOriginKeyStoreStsMutex;
+
+class OriginKeyStore {
+ NS_INLINE_DECL_REFCOUNTING(OriginKeyStore);
+ class OriginKey {
+ public:
+ static const size_t DecodedLength = 18;
+ static const size_t EncodedLength = DecodedLength * 4 / 3;
+
+ explicit OriginKey(const nsACString& aKey,
+ int64_t aSecondsStamp = 0) // 0 = temporal
+ : mKey(aKey), mSecondsStamp(aSecondsStamp) {}
+
+ nsCString mKey; // Base64 encoded.
+ int64_t mSecondsStamp;
+ };
+
+ class OriginKeysTable {
+ public:
+ OriginKeysTable() : mPersistCount(0) {}
+
+ nsresult GetPrincipalKey(const ipc::PrincipalInfo& aPrincipalInfo,
+ nsCString& aResult, bool aPersist = false) {
+ nsAutoCString principalString;
+ PrincipalInfoToString(aPrincipalInfo, principalString);
+
+ OriginKey* key;
+ if (!mKeys.Get(principalString, &key)) {
+ nsCString salt; // Make a new one
+ nsresult rv = GenerateRandomName(salt, OriginKey::EncodedLength);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ key = mKeys.InsertOrUpdate(principalString, MakeUnique<OriginKey>(salt))
+ .get();
+ }
+ if (aPersist && !key->mSecondsStamp) {
+ key->mSecondsStamp = PR_Now() / PR_USEC_PER_SEC;
+ mPersistCount++;
+ }
+ aResult = key->mKey;
+ return NS_OK;
+ }
+
+ void Clear(int64_t aSinceWhen) {
+ // Avoid int64_t* <-> void* casting offset
+ OriginKey since(nsCString(), aSinceWhen / PR_USEC_PER_SEC);
+ for (auto iter = mKeys.Iter(); !iter.Done(); iter.Next()) {
+ auto originKey = iter.UserData();
+ LOG((((originKey->mSecondsStamp >= since.mSecondsStamp)
+ ? "%s: REMOVE %" PRId64 " >= %" PRId64
+ : "%s: KEEP %" PRId64 " < %" PRId64),
+ __FUNCTION__, originKey->mSecondsStamp, since.mSecondsStamp));
+
+ if (originKey->mSecondsStamp >= since.mSecondsStamp) {
+ iter.Remove();
+ }
+ }
+ mPersistCount = 0;
+ }
+
+ private:
+ void PrincipalInfoToString(const ipc::PrincipalInfo& aPrincipalInfo,
+ nsACString& aString) {
+ switch (aPrincipalInfo.type()) {
+ case ipc::PrincipalInfo::TSystemPrincipalInfo:
+ aString.AssignLiteral("[System Principal]");
+ return;
+
+ case ipc::PrincipalInfo::TNullPrincipalInfo: {
+ const ipc::NullPrincipalInfo& info =
+ aPrincipalInfo.get_NullPrincipalInfo();
+ aString.Assign(info.spec());
+ return;
+ }
+
+ case ipc::PrincipalInfo::TContentPrincipalInfo: {
+ const ipc::ContentPrincipalInfo& info =
+ aPrincipalInfo.get_ContentPrincipalInfo();
+ aString.Assign(info.originNoSuffix());
+
+ nsAutoCString suffix;
+ info.attrs().CreateSuffix(suffix);
+ aString.Append(suffix);
+ return;
+ }
+
+ case ipc::PrincipalInfo::TExpandedPrincipalInfo: {
+ const ipc::ExpandedPrincipalInfo& info =
+ aPrincipalInfo.get_ExpandedPrincipalInfo();
+
+ aString.AssignLiteral("[Expanded Principal [");
+
+ for (uint32_t i = 0; i < info.allowlist().Length(); i++) {
+ nsAutoCString str;
+ PrincipalInfoToString(info.allowlist()[i], str);
+
+ if (i != 0) {
+ aString.AppendLiteral(", ");
+ }
+
+ aString.Append(str);
+ }
+
+ aString.AppendLiteral("]]");
+ return;
+ }
+
+ default:
+ MOZ_CRASH("Unknown PrincipalInfo type!");
+ }
+ }
+
+ protected:
+ nsClassHashtable<nsCStringHashKey, OriginKey> mKeys;
+ size_t mPersistCount;
+ };
+
+ class OriginKeysLoader : public OriginKeysTable {
+ public:
+ OriginKeysLoader() = default;
+
+ nsresult GetPrincipalKey(const ipc::PrincipalInfo& aPrincipalInfo,
+ nsCString& aResult, bool aPersist = false) {
+ auto before = mPersistCount;
+ nsresult rv =
+ OriginKeysTable::GetPrincipalKey(aPrincipalInfo, aResult, aPersist);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ if (mPersistCount != before) {
+ Save();
+ }
+ return NS_OK;
+ }
+
+ already_AddRefed<nsIFile> GetFile() {
+ MOZ_ASSERT(mProfileDir);
+ nsCOMPtr<nsIFile> file;
+ nsresult rv = mProfileDir->Clone(getter_AddRefs(file));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return nullptr;
+ }
+ file->Append(nsLiteralString(ORIGINKEYS_FILE));
+ return file.forget();
+ }
+
+ // Format of file is key secondsstamp origin (first line is version #):
+ //
+ // 1
+ // rOMAAbFujNwKyIpj4RJ3Wt5Q 1424733961 http://fiddle.jshell.net
+ // rOMAAbFujNwKyIpj4RJ3Wt5Q 1424734841 http://mozilla.github.io
+ // etc.
+
+ nsresult Read() {
+ nsCOMPtr<nsIFile> file = GetFile();
+ if (NS_WARN_IF(!file)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ bool exists;
+ nsresult rv = file->Exists(&exists);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ if (!exists) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIInputStream> stream;
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(stream), file);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ nsCOMPtr<nsILineInputStream> i = do_QueryInterface(stream);
+ MOZ_ASSERT(i);
+ MOZ_ASSERT(!mPersistCount);
+
+ nsCString line;
+ bool hasMoreLines;
+ rv = i->ReadLine(line, &hasMoreLines);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ if (!line.EqualsLiteral(ORIGINKEYS_VERSION)) {
+ // If version on disk is newer than we can understand then ignore it.
+ return NS_OK;
+ }
+
+ while (hasMoreLines) {
+ rv = i->ReadLine(line, &hasMoreLines);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ // Read key secondsstamp origin.
+ // Ignore any lines that don't fit format in the comment above exactly.
+ int32_t f = line.FindChar(' ');
+ if (f < 0) {
+ continue;
+ }
+ const nsACString& key = Substring(line, 0, f);
+ const nsACString& s = Substring(line, f + 1);
+ f = s.FindChar(' ');
+ if (f < 0) {
+ continue;
+ }
+ int64_t secondsstamp = Substring(s, 0, f).ToInteger64(&rv);
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+ const nsACString& origin = Substring(s, f + 1);
+
+ // Validate key
+ if (key.Length() != OriginKey::EncodedLength) {
+ continue;
+ }
+ nsCString dummy;
+ rv = Base64Decode(key, dummy);
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+ mKeys.InsertOrUpdate(origin, MakeUnique<OriginKey>(key, secondsstamp));
+ }
+ mPersistCount = mKeys.Count();
+ return NS_OK;
+ }
+
+ nsresult Write() {
+ nsCOMPtr<nsIFile> file = GetFile();
+ if (NS_WARN_IF(!file)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ nsCOMPtr<nsIOutputStream> stream;
+ nsresult rv =
+ NS_NewSafeLocalFileOutputStream(getter_AddRefs(stream), file);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsAutoCString versionBuffer;
+ versionBuffer.AppendLiteral(ORIGINKEYS_VERSION);
+ versionBuffer.Append('\n');
+
+ uint32_t count;
+ rv = stream->Write(versionBuffer.Data(), versionBuffer.Length(), &count);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ if (count != versionBuffer.Length()) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ for (const auto& entry : mKeys) {
+ const nsACString& origin = entry.GetKey();
+ OriginKey* originKey = entry.GetWeak();
+
+ if (!originKey->mSecondsStamp) {
+ continue; // don't write temporal ones
+ }
+
+ nsCString originBuffer;
+ originBuffer.Append(originKey->mKey);
+ originBuffer.Append(' ');
+ originBuffer.AppendInt(originKey->mSecondsStamp);
+ originBuffer.Append(' ');
+ originBuffer.Append(origin);
+ originBuffer.Append('\n');
+
+ rv = stream->Write(originBuffer.Data(), originBuffer.Length(), &count);
+ if (NS_WARN_IF(NS_FAILED(rv)) || count != originBuffer.Length()) {
+ break;
+ }
+ }
+
+ nsCOMPtr<nsISafeOutputStream> safeStream = do_QueryInterface(stream);
+ MOZ_ASSERT(safeStream);
+
+ rv = safeStream->Finish();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ return NS_OK;
+ }
+
+ nsresult Load() {
+ nsresult rv = Read();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ Delete();
+ }
+ return rv;
+ }
+
+ nsresult Save() {
+ nsresult rv = Write();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ NS_WARNING("Failed to write data for EnumerateDevices id-persistence.");
+ Delete();
+ }
+ return rv;
+ }
+
+ void Clear(int64_t aSinceWhen) {
+ OriginKeysTable::Clear(aSinceWhen);
+ Delete();
+ Save();
+ }
+
+ nsresult Delete() {
+ nsCOMPtr<nsIFile> file = GetFile();
+ if (NS_WARN_IF(!file)) {
+ return NS_ERROR_UNEXPECTED;
+ }
+ nsresult rv = file->Remove(false);
+ if (rv == NS_ERROR_FILE_NOT_FOUND) {
+ return NS_OK;
+ }
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ return NS_OK;
+ }
+
+ void SetProfileDir(nsIFile* aProfileDir) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ bool first = !mProfileDir;
+ mProfileDir = aProfileDir;
+ // Load from disk when we first get a profileDir, but not subsequently.
+ if (first) {
+ Load();
+ }
+ }
+
+ private:
+ nsCOMPtr<nsIFile> mProfileDir;
+ };
+
+ private:
+ static OriginKeyStore* sOriginKeyStore;
+
+ virtual ~OriginKeyStore() {
+ MOZ_ASSERT(NS_IsMainThread());
+ sOriginKeyStore = nullptr;
+ LOG(("%s", __FUNCTION__));
+ }
+
+ public:
+ static RefPtr<OriginKeyStore> Get() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!sOriginKeyStore) {
+ sOriginKeyStore = new OriginKeyStore();
+ }
+ return RefPtr(sOriginKeyStore);
+ }
+
+ // Only accessed on StreamTS threads
+ OriginKeysLoader mOriginKeys MOZ_GUARDED_BY(sOriginKeyStoreStsMutex);
+ OriginKeysTable mPrivateBrowsingOriginKeys
+ MOZ_GUARDED_BY(sOriginKeyStoreStsMutex);
+};
+OriginKeyStore* OriginKeyStore::sOriginKeyStore = nullptr;
+
+template <class Super>
+mozilla::ipc::IPCResult Parent<Super>::RecvGetPrincipalKey(
+ const ipc::PrincipalInfo& aPrincipalInfo, const bool& aPersist,
+ PMediaParent::GetPrincipalKeyResolver&& aResolve) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // First, get profile dir.
+
+ nsCOMPtr<nsIFile> profileDir;
+ nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(profileDir));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return IPCResult(this, false);
+ }
+
+ // Resolver has to be called in MainThread but the key is discovered
+ // in a different thread. We wrap the resolver around a MozPromise to make
+ // it more flexible and pass it to the new task. When this is done the
+ // resolver is resolved in MainThread.
+
+ // Then over to stream-transport thread (a thread pool) to do the actual
+ // file io. Stash a promise to hold the answer and get an id for this request.
+
+ nsCOMPtr<nsIEventTarget> sts =
+ do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
+ MOZ_ASSERT(sts);
+ auto taskQueue = TaskQueue::Create(sts.forget(), "RecvGetPrincipalKey");
+ RefPtr<Parent<Super>> that(this);
+
+ InvokeAsync(
+ taskQueue, __func__,
+ [this, that, profileDir, aPrincipalInfo, aPersist]() {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ StaticMutexAutoLock lock(sOriginKeyStoreStsMutex);
+ mOriginKeyStore->mOriginKeys.SetProfileDir(profileDir);
+
+ nsresult rv;
+ nsAutoCString result;
+ if (IsPrincipalInfoPrivate(aPrincipalInfo)) {
+ rv = mOriginKeyStore->mPrivateBrowsingOriginKeys.GetPrincipalKey(
+ aPrincipalInfo, result);
+ } else {
+ rv = mOriginKeyStore->mOriginKeys.GetPrincipalKey(aPrincipalInfo,
+ result, aPersist);
+ }
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return PrincipalKeyPromise::CreateAndReject(rv, __func__);
+ }
+ return PrincipalKeyPromise::CreateAndResolve(result, __func__);
+ })
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [aResolve](const PrincipalKeyPromise::ResolveOrRejectValue& aValue) {
+ if (aValue.IsReject()) {
+ aResolve(""_ns);
+ } else {
+ aResolve(aValue.ResolveValue());
+ }
+ });
+
+ return IPC_OK();
+}
+
+template <class Super>
+mozilla::ipc::IPCResult Parent<Super>::RecvSanitizeOriginKeys(
+ const uint64_t& aSinceWhen, const bool& aOnlyPrivateBrowsing) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCOMPtr<nsIFile> profileDir;
+ nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(profileDir));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return IPCResult(this, false);
+ }
+ // Over to stream-transport thread (a thread pool) to do the file io.
+
+ nsCOMPtr<nsIEventTarget> sts =
+ do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
+ MOZ_ASSERT(sts);
+ RefPtr<Parent<Super>> that(this);
+
+ rv = sts->Dispatch(
+ NewRunnableFrom(
+ [this, that, profileDir, aSinceWhen, aOnlyPrivateBrowsing]() {
+ MOZ_ASSERT(!NS_IsMainThread());
+ StaticMutexAutoLock lock(sOriginKeyStoreStsMutex);
+ mOriginKeyStore->mPrivateBrowsingOriginKeys.Clear(aSinceWhen);
+ if (!aOnlyPrivateBrowsing) {
+ mOriginKeyStore->mOriginKeys.SetProfileDir(profileDir);
+ mOriginKeyStore->mOriginKeys.Clear(aSinceWhen);
+ }
+ return NS_OK;
+ }),
+ NS_DISPATCH_NORMAL);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return IPCResult(this, false);
+ }
+ return IPC_OK();
+}
+
+template <class Super>
+void Parent<Super>::ActorDestroy(ActorDestroyReason aWhy) {
+ // No more IPC from here
+ mDestroyed = true;
+ LOG(("%s", __FUNCTION__));
+}
+
+template <class Super>
+Parent<Super>::Parent()
+ : mOriginKeyStore(OriginKeyStore::Get()), mDestroyed(false) {
+ LOG(("media::Parent: %p", this));
+}
+
+template <class Super>
+Parent<Super>::~Parent() {
+ NS_ReleaseOnMainThread("Parent<Super>::mOriginKeyStore",
+ mOriginKeyStore.forget());
+ LOG(("~media::Parent: %p", this));
+}
+
+PMediaParent* AllocPMediaParent() {
+ Parent<PMediaParent>* obj = new Parent<PMediaParent>();
+ obj->AddRef();
+ return obj;
+}
+
+bool DeallocPMediaParent(media::PMediaParent* aActor) {
+ static_cast<Parent<PMediaParent>*>(aActor)->Release();
+ return true;
+}
+
+} // namespace mozilla::media
+
+// Instantiate templates to satisfy linker
+template class mozilla::media::Parent<mozilla::media::NonE10s>;
diff --git a/dom/media/systemservices/MediaParent.h b/dom/media/systemservices/MediaParent.h
new file mode 100644
index 0000000000..77cba312f3
--- /dev/null
+++ b/dom/media/systemservices/MediaParent.h
@@ -0,0 +1,91 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_MediaParent_h
+#define mozilla_MediaParent_h
+
+#include "MediaChild.h"
+
+#include "mozilla/media/PMediaParent.h"
+
+namespace mozilla::media {
+
+// media::Parent implements the chrome-process side of ipc for media::Child APIs
+// A same-process version may also be created to service non-e10s calls.
+
+class OriginKeyStore;
+
+class NonE10s {
+ typedef mozilla::ipc::IProtocol::ActorDestroyReason ActorDestroyReason;
+
+ public:
+ virtual ~NonE10s() = default;
+
+ protected:
+ virtual mozilla::ipc::IPCResult RecvGetPrincipalKey(
+ const mozilla::ipc::PrincipalInfo& aPrincipalInfo, const bool& aPersist,
+ PMediaParent::GetPrincipalKeyResolver&& aResolve) = 0;
+ virtual mozilla::ipc::IPCResult RecvSanitizeOriginKeys(
+ const uint64_t& aSinceWhen, const bool& aOnlyPrivateBrowsing) = 0;
+ virtual void ActorDestroy(ActorDestroyReason aWhy) = 0;
+};
+
+/**
+ * Dummy class to avoid a templated class being passed to the refcounting macro
+ * (see Bug 1334421 for what happens then)
+ */
+class RefCountedParent {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RefCountedParent)
+
+ protected:
+ virtual ~RefCountedParent() = default;
+};
+
+// Super = PMediaParent or NonE10s
+
+template <class Super>
+class Parent : public RefCountedParent, public Super {
+ typedef mozilla::ipc::IProtocol::ActorDestroyReason ActorDestroyReason;
+
+ public:
+ virtual mozilla::ipc::IPCResult RecvGetPrincipalKey(
+ const mozilla::ipc::PrincipalInfo& aPrincipalInfo, const bool& aPersist,
+ PMediaParent::GetPrincipalKeyResolver&& aResolve) override;
+ virtual mozilla::ipc::IPCResult RecvSanitizeOriginKeys(
+ const uint64_t& aSinceWhen, const bool& aOnlyPrivateBrowsing) override;
+ virtual void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ Parent();
+
+ private:
+ virtual ~Parent();
+
+ RefPtr<OriginKeyStore> mOriginKeyStore;
+ bool mDestroyed;
+};
+
+template <class Parent>
+mozilla::ipc::IPCResult IPCResult(Parent* aSelf, bool aSuccess);
+
+template <>
+inline mozilla::ipc::IPCResult IPCResult(Parent<PMediaParent>* aSelf,
+ bool aSuccess) {
+ return aSuccess ? IPC_OK() : IPC_FAIL_NO_REASON(aSelf);
+}
+
+template <>
+inline mozilla::ipc::IPCResult IPCResult(Parent<NonE10s>* aSelf,
+ bool aSuccess) {
+ return IPC_OK();
+}
+
+PMediaParent* AllocPMediaParent();
+bool DeallocPMediaParent(PMediaParent* aActor);
+
+} // namespace mozilla::media
+
+#endif // mozilla_MediaParent_h
diff --git a/dom/media/systemservices/MediaSystemResourceClient.cpp b/dom/media/systemservices/MediaSystemResourceClient.cpp
new file mode 100644
index 0000000000..50695fc76c
--- /dev/null
+++ b/dom/media/systemservices/MediaSystemResourceClient.cpp
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/Monitor.h"
+#include "mozilla/ReentrantMonitor.h"
+
+#include "MediaSystemResourceClient.h"
+
+namespace mozilla {
+
+Atomic<uint32_t> MediaSystemResourceClient::sSerialCounter(0);
+
+MediaSystemResourceClient::MediaSystemResourceClient(
+ MediaSystemResourceType aReourceType)
+ : mResourceType(aReourceType),
+ mId(++sSerialCounter),
+ mListener(nullptr),
+ mResourceState(RESOURCE_STATE_START),
+ mIsSync(false),
+ mAcquireSyncWaitMonitor(nullptr),
+ mAcquireSyncWaitDone(nullptr) {
+ mManager = MediaSystemResourceManager::Get();
+ if (mManager) {
+ mManager->Register(this);
+ }
+}
+
+MediaSystemResourceClient::~MediaSystemResourceClient() {
+ ReleaseResource();
+ if (mManager) {
+ mManager->Unregister(this);
+ }
+}
+
+bool MediaSystemResourceClient::SetListener(
+ MediaSystemResourceReservationListener* aListener) {
+ if (!mManager) {
+ return false;
+ }
+ return mManager->SetListener(this, aListener);
+}
+
+void MediaSystemResourceClient::Acquire() {
+ if (!mManager) {
+ return;
+ }
+ mManager->Acquire(this);
+}
+
+bool MediaSystemResourceClient::AcquireSyncNoWait() {
+ if (!mManager) {
+ return false;
+ }
+ return mManager->AcquireSyncNoWait(this);
+}
+
+void MediaSystemResourceClient::ReleaseResource() {
+ if (!mManager) {
+ return;
+ }
+ mManager->ReleaseResource(this);
+}
+
+} // namespace mozilla
diff --git a/dom/media/systemservices/MediaSystemResourceClient.h b/dom/media/systemservices/MediaSystemResourceClient.h
new file mode 100644
index 0000000000..52cf5107e9
--- /dev/null
+++ b/dom/media/systemservices/MediaSystemResourceClient.h
@@ -0,0 +1,91 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(MediaSystemResourceClient_h_)
+# define MediaSystemResourceClient_h_
+
+# include "MediaSystemResourceManager.h"
+# include "MediaSystemResourceTypes.h"
+# include "mozilla/Atomics.h"
+# include "mozilla/media/MediaSystemResourceTypes.h"
+# include "mozilla/Monitor.h"
+# include "mozilla/RefPtr.h"
+
+namespace mozilla {
+
+class MediaSystemResourceManager;
+
+/**
+ * This is a base class for listener callbacks.
+ * This callback is invoked when the media system resource reservation state
+ * is changed.
+ */
+class MediaSystemResourceReservationListener {
+ public:
+ virtual void ResourceReserved() = 0;
+ virtual void ResourceReserveFailed() = 0;
+};
+
+/**
+ * MediaSystemResourceClient is used to reserve a media system resource
+ * like hw decoder. When system has a limitation of a media resource,
+ * use this class to mediate use rights of the resource.
+ */
+class MediaSystemResourceClient {
+ public:
+ // Enumeration for the valid decoding states
+ enum ResourceState {
+ RESOURCE_STATE_START,
+ RESOURCE_STATE_WAITING,
+ RESOURCE_STATE_ACQUIRED,
+ RESOURCE_STATE_NOT_ACQUIRED,
+ RESOURCE_STATE_END
+ };
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaSystemResourceClient)
+
+ explicit MediaSystemResourceClient(MediaSystemResourceType aReourceType);
+
+ bool SetListener(MediaSystemResourceReservationListener* aListener);
+
+ // Try to acquire media resource asynchronously.
+ // If the resource is used by others, wait until acquired.
+ void Acquire();
+
+ // Try to acquire media resource synchronously. If the resource is not
+ // immediately available, fail to acquire it. return false if resource is not
+ // acquired. return true if resource is acquired.
+ //
+ // This function should not be called on ImageBridge thread.
+ // It should be used only for compatibility with legacy code.
+ bool AcquireSyncNoWait();
+
+ void ReleaseResource();
+
+ private:
+ ~MediaSystemResourceClient();
+
+ RefPtr<MediaSystemResourceManager> mManager;
+ const MediaSystemResourceType mResourceType;
+ const uint32_t mId;
+
+ // Modified only by MediaSystemResourceManager.
+ // Accessed and modified with MediaSystemResourceManager::mReentrantMonitor
+ // held.
+ MediaSystemResourceReservationListener* mListener;
+ ResourceState mResourceState;
+ bool mIsSync;
+ ReentrantMonitor* mAcquireSyncWaitMonitor;
+ bool* mAcquireSyncWaitDone;
+
+ static mozilla::Atomic<uint32_t> sSerialCounter;
+
+ friend class MediaSystemResourceManager;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/systemservices/MediaSystemResourceManager.cpp b/dom/media/systemservices/MediaSystemResourceManager.cpp
new file mode 100644
index 0000000000..414ef8e81d
--- /dev/null
+++ b/dom/media/systemservices/MediaSystemResourceManager.cpp
@@ -0,0 +1,358 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/TaskQueue.h"
+
+#include "MediaSystemResourceManagerChild.h"
+#include "MediaSystemResourceClient.h"
+
+#include "mozilla/layers/ImageBridgeChild.h"
+
+#include "MediaSystemResourceManager.h"
+
+namespace mozilla {
+
+using namespace mozilla::ipc;
+using namespace mozilla::layers;
+
+/* static */
+StaticRefPtr<MediaSystemResourceManager> MediaSystemResourceManager::sSingleton;
+
+/* static */
+MediaSystemResourceManager* MediaSystemResourceManager::Get() {
+ if (sSingleton) {
+ return sSingleton;
+ }
+ MediaSystemResourceManager::Init();
+ return sSingleton;
+}
+
+/* static */
+void MediaSystemResourceManager::Shutdown() {
+ MOZ_ASSERT(InImageBridgeChildThread());
+ if (sSingleton) {
+ sSingleton->CloseIPC();
+ sSingleton = nullptr;
+ }
+}
+
+/* static */
+void MediaSystemResourceManager::Init() {
+ RefPtr<ImageBridgeChild> imageBridge = ImageBridgeChild::GetSingleton();
+ if (!imageBridge) {
+ NS_WARNING("ImageBridge does not exist");
+ return;
+ }
+
+ if (InImageBridgeChildThread()) {
+ if (!sSingleton) {
+#ifdef DEBUG
+ static int timesCreated = 0;
+ timesCreated++;
+ MOZ_ASSERT(timesCreated == 1);
+#endif
+ sSingleton = new MediaSystemResourceManager();
+ }
+ return;
+ }
+
+ ReentrantMonitor barrier MOZ_UNANNOTATED("MediaSystemResourceManager::Init");
+ ReentrantMonitorAutoEnter mainThreadAutoMon(barrier);
+ bool done = false;
+
+ RefPtr<Runnable> runnable =
+ NS_NewRunnableFunction("MediaSystemResourceManager::Init", [&]() {
+ if (!sSingleton) {
+ sSingleton = new MediaSystemResourceManager();
+ }
+ ReentrantMonitorAutoEnter childThreadAutoMon(barrier);
+ done = true;
+ barrier.NotifyAll();
+ });
+
+ imageBridge->GetThread()->Dispatch(runnable.forget());
+
+ // should stop the thread until done.
+ while (!done) {
+ barrier.Wait();
+ }
+}
+
+MediaSystemResourceManager::MediaSystemResourceManager()
+ : mReentrantMonitor("MediaSystemResourceManager.mReentrantMonitor"),
+ mShutDown(false),
+ mChild(nullptr) {
+ MOZ_ASSERT(InImageBridgeChildThread());
+ OpenIPC();
+}
+
+MediaSystemResourceManager::~MediaSystemResourceManager() {
+ MOZ_ASSERT(IsIpcClosed());
+}
+
+void MediaSystemResourceManager::OpenIPC() {
+ MOZ_ASSERT(InImageBridgeChildThread());
+ MOZ_ASSERT(!mChild);
+
+ media::PMediaSystemResourceManagerChild* child =
+ ImageBridgeChild::GetSingleton()
+ ->SendPMediaSystemResourceManagerConstructor();
+ mChild = static_cast<media::MediaSystemResourceManagerChild*>(child);
+ mChild->SetManager(this);
+}
+
+void MediaSystemResourceManager::CloseIPC() {
+ MOZ_ASSERT(InImageBridgeChildThread());
+
+ if (!mChild) {
+ return;
+ }
+ mChild->Destroy();
+ mChild = nullptr;
+ mShutDown = true;
+}
+
+void MediaSystemResourceManager::OnIpcClosed() { mChild = nullptr; }
+
+bool MediaSystemResourceManager::IsIpcClosed() { return mChild ? true : false; }
+
+void MediaSystemResourceManager::Register(MediaSystemResourceClient* aClient) {
+ ReentrantMonitorAutoEnter mon(mReentrantMonitor);
+ MOZ_ASSERT(aClient);
+ MOZ_ASSERT(!mResourceClients.Contains(aClient->mId));
+
+ mResourceClients.InsertOrUpdate(aClient->mId, aClient);
+}
+
+void MediaSystemResourceManager::Unregister(
+ MediaSystemResourceClient* aClient) {
+ ReentrantMonitorAutoEnter mon(mReentrantMonitor);
+ MOZ_ASSERT(aClient);
+ MOZ_ASSERT(mResourceClients.Contains(aClient->mId));
+ MOZ_ASSERT(mResourceClients.Get(aClient->mId) == aClient);
+
+ mResourceClients.Remove(aClient->mId);
+}
+
+bool MediaSystemResourceManager::SetListener(
+ MediaSystemResourceClient* aClient,
+ MediaSystemResourceReservationListener* aListener) {
+ ReentrantMonitorAutoEnter mon(mReentrantMonitor);
+ MOZ_ASSERT(aClient);
+
+ MediaSystemResourceClient* client = mResourceClients.Get(aClient->mId);
+ MOZ_ASSERT(client);
+
+ if (!client) {
+ return false;
+ }
+ // State Check
+ if (aClient->mResourceState !=
+ MediaSystemResourceClient::RESOURCE_STATE_START) {
+ return false;
+ }
+ aClient->mListener = aListener;
+ return true;
+}
+
+void MediaSystemResourceManager::Acquire(MediaSystemResourceClient* aClient) {
+ MOZ_ASSERT(aClient);
+ MOZ_ASSERT(!InImageBridgeChildThread());
+
+ ReentrantMonitorAutoEnter mon(mReentrantMonitor);
+ MediaSystemResourceClient* client = mResourceClients.Get(aClient->mId);
+ MOZ_ASSERT(client);
+ MOZ_ASSERT(client == aClient);
+
+ aClient->mIsSync = false; // async request
+
+ if (!client) {
+ HandleAcquireResult(aClient->mId, false);
+ return;
+ }
+ // State Check
+ if (aClient->mResourceState !=
+ MediaSystemResourceClient::RESOURCE_STATE_START) {
+ HandleAcquireResult(aClient->mId, false);
+ return;
+ }
+ aClient->mResourceState = MediaSystemResourceClient::RESOURCE_STATE_WAITING;
+ ImageBridgeChild::GetSingleton()->GetThread()->Dispatch(
+ NewRunnableMethod<uint32_t>("MediaSystemResourceManager::DoAcquire", this,
+ &MediaSystemResourceManager::DoAcquire,
+ aClient->mId));
+}
+
+bool MediaSystemResourceManager::AcquireSyncNoWait(
+ MediaSystemResourceClient* aClient) {
+ MOZ_ASSERT(aClient);
+ MOZ_ASSERT(!InImageBridgeChildThread());
+
+ ReentrantMonitor barrier MOZ_UNANNOTATED(
+ "MediaSystemResourceManager::AcquireSyncNoWait");
+ ReentrantMonitorAutoEnter autoMon(barrier);
+ bool done = false;
+ {
+ ReentrantMonitorAutoEnter mon(mReentrantMonitor);
+ MediaSystemResourceClient* client = mResourceClients.Get(aClient->mId);
+ MOZ_ASSERT(client);
+ MOZ_ASSERT(client == aClient);
+
+ aClient->mIsSync = true; // sync request
+
+ if (InImageBridgeChildThread()) {
+ HandleAcquireResult(aClient->mId, false);
+ return false;
+ }
+ if (!client || client != aClient) {
+ HandleAcquireResult(aClient->mId, false);
+ return false;
+ }
+ // State Check
+ if (aClient->mResourceState !=
+ MediaSystemResourceClient::RESOURCE_STATE_START) {
+ HandleAcquireResult(aClient->mId, false);
+ return false;
+ }
+ // Hold barrier Monitor until acquire task end.
+ aClient->mAcquireSyncWaitMonitor = &barrier;
+ aClient->mAcquireSyncWaitDone = &done;
+ aClient->mResourceState = MediaSystemResourceClient::RESOURCE_STATE_WAITING;
+ }
+
+ ImageBridgeChild::GetSingleton()->GetThread()->Dispatch(
+ NewRunnableMethod<uint32_t>("MediaSystemResourceManager::DoAcquire", this,
+ &MediaSystemResourceManager::DoAcquire,
+ aClient->mId));
+
+ // should stop the thread until done.
+ while (!done) {
+ barrier.Wait();
+ }
+
+ {
+ ReentrantMonitorAutoEnter mon(mReentrantMonitor);
+ if (aClient->mResourceState !=
+ MediaSystemResourceClient::RESOURCE_STATE_ACQUIRED) {
+ return false;
+ }
+ return true;
+ }
+}
+
+void MediaSystemResourceManager::DoAcquire(uint32_t aId) {
+ MOZ_ASSERT(InImageBridgeChildThread());
+ if (mShutDown || !mChild) {
+ HandleAcquireResult(aId, false);
+ return;
+ }
+ {
+ ReentrantMonitorAutoEnter mon(mReentrantMonitor);
+ MediaSystemResourceClient* client = mResourceClients.Get(aId);
+ MOZ_ASSERT(client);
+
+ if (!client || client->mResourceState !=
+ MediaSystemResourceClient::RESOURCE_STATE_WAITING) {
+ HandleAcquireResult(aId, false);
+ return;
+ }
+ MOZ_ASSERT(aId == client->mId);
+ bool willWait = !client->mAcquireSyncWaitMonitor ? true : false;
+ mChild->SendAcquire(client->mId, client->mResourceType, willWait);
+ }
+}
+
+void MediaSystemResourceManager::ReleaseResource(
+ MediaSystemResourceClient* aClient) {
+ MOZ_ASSERT(aClient);
+ {
+ ReentrantMonitorAutoEnter mon(mReentrantMonitor);
+ MediaSystemResourceClient* client = mResourceClients.Get(aClient->mId);
+ MOZ_ASSERT(client);
+ MOZ_ASSERT(client == aClient);
+
+ if (!client || client != aClient ||
+ aClient->mResourceState ==
+ MediaSystemResourceClient::RESOURCE_STATE_START ||
+ aClient->mResourceState ==
+ MediaSystemResourceClient::RESOURCE_STATE_END) {
+ aClient->mResourceState = MediaSystemResourceClient::RESOURCE_STATE_END;
+ return;
+ }
+
+ aClient->mResourceState = MediaSystemResourceClient::RESOURCE_STATE_END;
+
+ ImageBridgeChild::GetSingleton()->GetThread()->Dispatch(
+ NewRunnableMethod<uint32_t>(
+ "MediaSystemResourceManager::DoRelease", this,
+ &MediaSystemResourceManager::DoRelease, aClient->mId));
+ }
+}
+
+void MediaSystemResourceManager::DoRelease(uint32_t aId) {
+ MOZ_ASSERT(InImageBridgeChildThread());
+ if (mShutDown || !mChild) {
+ return;
+ }
+ mChild->SendRelease(aId);
+}
+
+void MediaSystemResourceManager::RecvResponse(uint32_t aId, bool aSuccess) {
+ HandleAcquireResult(aId, aSuccess);
+}
+
+void MediaSystemResourceManager::HandleAcquireResult(uint32_t aId,
+ bool aSuccess) {
+ if (!InImageBridgeChildThread()) {
+ ImageBridgeChild::GetSingleton()->GetThread()->Dispatch(
+ NewRunnableMethod<uint32_t, bool>(
+ "MediaSystemResourceManager::HandleAcquireResult", this,
+ &MediaSystemResourceManager::HandleAcquireResult, aId, aSuccess));
+ return;
+ }
+
+ ReentrantMonitorAutoEnter mon(mReentrantMonitor);
+ MediaSystemResourceClient* client = mResourceClients.Get(aId);
+ if (!client) {
+ // Client was already unregistered.
+ return;
+ }
+ if (client->mResourceState !=
+ MediaSystemResourceClient::RESOURCE_STATE_WAITING) {
+ return;
+ }
+
+ // Update state
+ if (aSuccess) {
+ client->mResourceState = MediaSystemResourceClient::RESOURCE_STATE_ACQUIRED;
+ } else {
+ client->mResourceState =
+ MediaSystemResourceClient::RESOURCE_STATE_NOT_ACQUIRED;
+ }
+
+ if (client->mIsSync) {
+ if (client->mAcquireSyncWaitMonitor) {
+ // Notify AcquireSync() complete
+ MOZ_ASSERT(client->mAcquireSyncWaitDone);
+ ReentrantMonitorAutoEnter autoMon(*client->mAcquireSyncWaitMonitor);
+ *client->mAcquireSyncWaitDone = true;
+ client->mAcquireSyncWaitMonitor->NotifyAll();
+ client->mAcquireSyncWaitMonitor = nullptr;
+ client->mAcquireSyncWaitDone = nullptr;
+ }
+ } else {
+ // Notify Acquire() result
+ if (client->mListener) {
+ if (aSuccess) {
+ client->mListener->ResourceReserved();
+ } else {
+ client->mListener->ResourceReserveFailed();
+ }
+ }
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/systemservices/MediaSystemResourceManager.h b/dom/media/systemservices/MediaSystemResourceManager.h
new file mode 100644
index 0000000000..293595ece0
--- /dev/null
+++ b/dom/media/systemservices/MediaSystemResourceManager.h
@@ -0,0 +1,81 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(MediaSystemResourceManager_h_)
+# define MediaSystemResourceManager_h_
+
+# include <queue>
+
+# include "MediaSystemResourceTypes.h"
+# include "mozilla/ReentrantMonitor.h"
+# include "mozilla/StaticPtr.h"
+# include "nsTHashMap.h"
+# include "nsISupportsImpl.h"
+
+namespace mozilla {
+
+namespace media {
+class MediaSystemResourceManagerChild;
+} // namespace media
+
+class MediaSystemResourceClient;
+class MediaSystemResourceReservationListener;
+class ReentrantMonitor;
+class TaskQueue;
+
+/**
+ * Manage media system resource allocation requests within a process.
+ */
+class MediaSystemResourceManager {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaSystemResourceManager)
+
+ static MediaSystemResourceManager* Get();
+ static void Init();
+ static void Shutdown();
+
+ void OnIpcClosed();
+
+ void Register(MediaSystemResourceClient* aClient);
+ void Unregister(MediaSystemResourceClient* aClient);
+
+ bool SetListener(MediaSystemResourceClient* aClient,
+ MediaSystemResourceReservationListener* aListener);
+
+ void Acquire(MediaSystemResourceClient* aClient);
+ bool AcquireSyncNoWait(MediaSystemResourceClient* aClient);
+ void ReleaseResource(MediaSystemResourceClient* aClient);
+
+ void RecvResponse(uint32_t aId, bool aSuccess);
+
+ private:
+ MediaSystemResourceManager();
+ virtual ~MediaSystemResourceManager();
+
+ void OpenIPC();
+ void CloseIPC();
+ bool IsIpcClosed();
+
+ void DoAcquire(uint32_t aId);
+
+ void DoRelease(uint32_t aId);
+
+ void HandleAcquireResult(uint32_t aId, bool aSuccess);
+
+ ReentrantMonitor mReentrantMonitor MOZ_UNANNOTATED;
+
+ bool mShutDown;
+
+ media::MediaSystemResourceManagerChild* mChild;
+
+ nsTHashMap<nsUint32HashKey, MediaSystemResourceClient*> mResourceClients;
+
+ static StaticRefPtr<MediaSystemResourceManager> sSingleton;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/systemservices/MediaSystemResourceManagerChild.cpp b/dom/media/systemservices/MediaSystemResourceManagerChild.cpp
new file mode 100644
index 0000000000..ff671fdf8d
--- /dev/null
+++ b/dom/media/systemservices/MediaSystemResourceManagerChild.cpp
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaSystemResourceManager.h"
+
+#include "MediaSystemResourceManagerChild.h"
+
+namespace mozilla::media {
+
+MediaSystemResourceManagerChild::MediaSystemResourceManagerChild()
+ : mDestroyed(false), mManager(nullptr) {}
+
+MediaSystemResourceManagerChild::~MediaSystemResourceManagerChild() = default;
+
+mozilla::ipc::IPCResult MediaSystemResourceManagerChild::RecvResponse(
+ const uint32_t& aId, const bool& aSuccess) {
+ if (mManager) {
+ mManager->RecvResponse(aId, aSuccess);
+ }
+ return IPC_OK();
+}
+
+void MediaSystemResourceManagerChild::ActorDestroy(
+ ActorDestroyReason aActorDestroyReason) {
+ MOZ_ASSERT(!mDestroyed);
+ if (mManager) {
+ mManager->OnIpcClosed();
+ }
+ mDestroyed = true;
+}
+
+void MediaSystemResourceManagerChild::Destroy() {
+ if (mDestroyed) {
+ return;
+ }
+ SendRemoveResourceManager();
+ // WARNING: |this| is dead, hands off
+}
+
+} // namespace mozilla::media
diff --git a/dom/media/systemservices/MediaSystemResourceManagerChild.h b/dom/media/systemservices/MediaSystemResourceManagerChild.h
new file mode 100644
index 0000000000..66bf76cdd3
--- /dev/null
+++ b/dom/media/systemservices/MediaSystemResourceManagerChild.h
@@ -0,0 +1,65 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(MediaSystemResourceManagerChild_h_)
+# define MediaSystemResourceManagerChild_h_
+
+# include "mozilla/media/PMediaSystemResourceManagerChild.h"
+# include "nsISupportsImpl.h"
+
+namespace mozilla {
+
+class MediaSystemResourceManager;
+
+namespace ipc {
+class BackgroundChildImpl;
+} // namespace ipc
+
+namespace media {
+
+/**
+ * Handle MediaSystemResourceManager's IPC
+ */
+class MediaSystemResourceManagerChild final
+ : public PMediaSystemResourceManagerChild {
+ friend class PMediaSystemResourceManagerChild;
+
+ public:
+ struct ResourceListener {
+ /* The resource is reserved and can be granted.
+ * The client can allocate the requested resource.
+ */
+ virtual void resourceReserved() = 0;
+ /* The resource is not reserved any more.
+ * The client should release the resource as soon as possible if the
+ * resource is still being held.
+ */
+ virtual void resourceCanceled() = 0;
+ };
+
+ MediaSystemResourceManagerChild();
+ virtual ~MediaSystemResourceManagerChild();
+
+ void Destroy();
+
+ void SetManager(MediaSystemResourceManager* aManager) { mManager = aManager; }
+
+ protected:
+ mozilla::ipc::IPCResult RecvResponse(const uint32_t& aId,
+ const bool& aSuccess);
+
+ private:
+ void ActorDestroy(ActorDestroyReason aActorDestroyReason) override;
+
+ bool mDestroyed;
+ MediaSystemResourceManager* mManager;
+
+ friend class mozilla::ipc::BackgroundChildImpl;
+};
+
+} // namespace media
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/systemservices/MediaSystemResourceManagerParent.cpp b/dom/media/systemservices/MediaSystemResourceManagerParent.cpp
new file mode 100644
index 0000000000..ec20079abc
--- /dev/null
+++ b/dom/media/systemservices/MediaSystemResourceManagerParent.cpp
@@ -0,0 +1,75 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/Unused.h"
+#include "mozilla/layers/PImageBridgeParent.h"
+
+#include "MediaSystemResourceManagerParent.h"
+
+namespace mozilla::media {
+
+using namespace ipc;
+
+MediaSystemResourceManagerParent::MediaSystemResourceManagerParent()
+ : mDestroyed(false) {
+ mMediaSystemResourceService = MediaSystemResourceService::Get();
+}
+
+MediaSystemResourceManagerParent::~MediaSystemResourceManagerParent() {
+ MOZ_ASSERT(mDestroyed);
+}
+
+mozilla::ipc::IPCResult MediaSystemResourceManagerParent::RecvAcquire(
+ const uint32_t& aId, const MediaSystemResourceType& aResourceType,
+ const bool& aWillWait) {
+ mResourceRequests.WithEntryHandle(aId, [&](auto&& request) {
+ MOZ_ASSERT(!request);
+ if (request) {
+ // Send fail response
+ mozilla::Unused << SendResponse(aId, false /* fail */);
+ return;
+ }
+
+ request.Insert(MakeUnique<MediaSystemResourceRequest>(aId, aResourceType));
+ mMediaSystemResourceService->Acquire(this, aId, aResourceType, aWillWait);
+ });
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult MediaSystemResourceManagerParent::RecvRelease(
+ const uint32_t& aId) {
+ MediaSystemResourceRequest* request = mResourceRequests.Get(aId);
+ if (!request) {
+ return IPC_OK();
+ }
+
+ mMediaSystemResourceService->ReleaseResource(this, aId,
+ request->mResourceType);
+ mResourceRequests.Remove(aId);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
+MediaSystemResourceManagerParent::RecvRemoveResourceManager() {
+ IProtocol* mgr = Manager();
+ if (!PMediaSystemResourceManagerParent::Send__delete__(this)) {
+ return IPC_FAIL_NO_REASON(mgr);
+ }
+ return IPC_OK();
+}
+
+void MediaSystemResourceManagerParent::ActorDestroy(
+ ActorDestroyReason aReason) {
+ MOZ_ASSERT(!mDestroyed);
+
+ // Release all resource requests of the MediaSystemResourceManagerParent.
+ // Clears all remaining pointers to this object.
+ mMediaSystemResourceService->ReleaseResource(this);
+
+ mDestroyed = true;
+}
+
+} // namespace mozilla::media
diff --git a/dom/media/systemservices/MediaSystemResourceManagerParent.h b/dom/media/systemservices/MediaSystemResourceManagerParent.h
new file mode 100644
index 0000000000..29ed219f2e
--- /dev/null
+++ b/dom/media/systemservices/MediaSystemResourceManagerParent.h
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(MediaSystemResourceManagerParent_h_)
+# define MediaSystemResourceManagerParent_h_
+
+# include "MediaSystemResourceManager.h"
+# include "MediaSystemResourceService.h"
+# include "MediaSystemResourceTypes.h"
+# include "mozilla/media/PMediaSystemResourceManagerParent.h"
+
+namespace mozilla::media {
+
+/**
+ * Handle MediaSystemResourceManager's IPC
+ */
+class MediaSystemResourceManagerParent final
+ : public PMediaSystemResourceManagerParent {
+ friend class PMediaSystemResourceManagerParent;
+
+ public:
+ MediaSystemResourceManagerParent();
+ virtual ~MediaSystemResourceManagerParent();
+
+ protected:
+ mozilla::ipc::IPCResult RecvAcquire(
+ const uint32_t& aId, const MediaSystemResourceType& aResourceType,
+ const bool& aWillWait);
+
+ mozilla::ipc::IPCResult RecvRelease(const uint32_t& aId);
+
+ mozilla::ipc::IPCResult RecvRemoveResourceManager();
+
+ private:
+ void ActorDestroy(ActorDestroyReason aActorDestroyReason) override;
+
+ struct MediaSystemResourceRequest {
+ MediaSystemResourceRequest()
+ : mId(-1), mResourceType(MediaSystemResourceType::INVALID_RESOURCE) {}
+ MediaSystemResourceRequest(uint32_t aId,
+ MediaSystemResourceType aResourceType)
+ : mId(aId), mResourceType(aResourceType) {}
+ int32_t mId;
+ MediaSystemResourceType mResourceType;
+ };
+
+ bool mDestroyed;
+
+ RefPtr<MediaSystemResourceService> mMediaSystemResourceService;
+
+ nsClassHashtable<nsUint32HashKey, MediaSystemResourceRequest>
+ mResourceRequests;
+};
+
+} // namespace mozilla::media
+
+#endif
diff --git a/dom/media/systemservices/MediaSystemResourceMessageUtils.h b/dom/media/systemservices/MediaSystemResourceMessageUtils.h
new file mode 100644
index 0000000000..f06da1467d
--- /dev/null
+++ b/dom/media/systemservices/MediaSystemResourceMessageUtils.h
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(MediaSystemResourceMessageUtils_h_)
+# define MediaSystemResourceMessageUtils_h_
+
+# include "ipc/EnumSerializer.h"
+# include "MediaSystemResourceTypes.h"
+
+namespace IPC {
+
+template <>
+struct ParamTraits<mozilla::MediaSystemResourceType>
+ : public ContiguousEnumSerializer<
+ mozilla::MediaSystemResourceType,
+ mozilla::MediaSystemResourceType::VIDEO_DECODER,
+ mozilla::MediaSystemResourceType::INVALID_RESOURCE> {};
+
+} // namespace IPC
+
+#endif
diff --git a/dom/media/systemservices/MediaSystemResourceService.cpp b/dom/media/systemservices/MediaSystemResourceService.cpp
new file mode 100644
index 0000000000..88c4566e76
--- /dev/null
+++ b/dom/media/systemservices/MediaSystemResourceService.cpp
@@ -0,0 +1,222 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaSystemResourceManagerParent.h"
+#include "mozilla/layers/CompositorThread.h"
+#include "mozilla/Unused.h"
+
+#include "MediaSystemResourceService.h"
+
+using namespace mozilla::layers;
+
+namespace mozilla {
+
+/* static */
+StaticRefPtr<MediaSystemResourceService> MediaSystemResourceService::sSingleton;
+
+/* static */
+MediaSystemResourceService* MediaSystemResourceService::Get() {
+ if (sSingleton) {
+ return sSingleton;
+ }
+ Init();
+ return sSingleton;
+}
+
+/* static */
+void MediaSystemResourceService::Init() {
+ if (!sSingleton) {
+ sSingleton = new MediaSystemResourceService();
+ }
+}
+
+/* static */
+void MediaSystemResourceService::Shutdown() {
+ if (sSingleton) {
+ sSingleton->Destroy();
+ sSingleton = nullptr;
+ }
+}
+
+MediaSystemResourceService::MediaSystemResourceService() : mDestroyed(false) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+}
+
+MediaSystemResourceService::~MediaSystemResourceService() = default;
+
+void MediaSystemResourceService::Destroy() { mDestroyed = true; }
+
+void MediaSystemResourceService::Acquire(
+ media::MediaSystemResourceManagerParent* aParent, uint32_t aId,
+ MediaSystemResourceType aResourceType, bool aWillWait) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ MOZ_ASSERT(aParent);
+
+ if (mDestroyed) {
+ return;
+ }
+
+ MediaSystemResource* resource =
+ mResources.Get(static_cast<uint32_t>(aResourceType));
+
+ if (!resource || resource->mResourceCount == 0) {
+ // Resource does not exit
+ // Send fail response
+ mozilla::Unused << aParent->SendResponse(aId, false /* fail */);
+ return;
+ }
+
+ // Try to acquire a resource
+ if (resource->mAcquiredRequests.size() < resource->mResourceCount) {
+ // Resource is available
+ resource->mAcquiredRequests.push_back(
+ MediaSystemResourceRequest(aParent, aId));
+ // Send success response
+ mozilla::Unused << aParent->SendResponse(aId, true /* success */);
+ return;
+ }
+
+ if (!aWillWait) {
+ // Resource is not available and do not wait.
+ // Send fail response
+ mozilla::Unused << aParent->SendResponse(aId, false /* fail */);
+ return;
+ }
+ // Wait until acquire.
+ resource->mWaitingRequests.push_back(
+ MediaSystemResourceRequest(aParent, aId));
+}
+
+void MediaSystemResourceService::ReleaseResource(
+ media::MediaSystemResourceManagerParent* aParent, uint32_t aId,
+ MediaSystemResourceType aResourceType) {
+ MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
+ MOZ_ASSERT(aParent);
+
+ if (mDestroyed) {
+ return;
+ }
+
+ MediaSystemResource* resource =
+ mResources.Get(static_cast<uint32_t>(aResourceType));
+
+ if (!resource || resource->mResourceCount == 0) {
+ // Resource does not exit
+ return;
+ }
+ RemoveRequest(aParent, aId, aResourceType);
+ UpdateRequests(aResourceType);
+}
+
+void MediaSystemResourceService::ReleaseResource(
+ media::MediaSystemResourceManagerParent* aParent) {
+ MOZ_ASSERT(aParent);
+
+ if (mDestroyed) {
+ return;
+ }
+
+ for (const uint32_t& key : mResources.Keys()) {
+ RemoveRequests(aParent, static_cast<MediaSystemResourceType>(key));
+ UpdateRequests(static_cast<MediaSystemResourceType>(key));
+ }
+}
+
+void MediaSystemResourceService::RemoveRequest(
+ media::MediaSystemResourceManagerParent* aParent, uint32_t aId,
+ MediaSystemResourceType aResourceType) {
+ MOZ_ASSERT(aParent);
+
+ MediaSystemResource* resource =
+ mResources.Get(static_cast<uint32_t>(aResourceType));
+ if (!resource) {
+ return;
+ }
+
+ std::deque<MediaSystemResourceRequest>::iterator it;
+ std::deque<MediaSystemResourceRequest>& acquiredRequests =
+ resource->mAcquiredRequests;
+ for (it = acquiredRequests.begin(); it != acquiredRequests.end(); it++) {
+ if (((*it).mParent == aParent) && ((*it).mId == aId)) {
+ acquiredRequests.erase(it);
+ return;
+ }
+ }
+
+ std::deque<MediaSystemResourceRequest>& waitingRequests =
+ resource->mWaitingRequests;
+ for (it = waitingRequests.begin(); it != waitingRequests.end(); it++) {
+ if (((*it).mParent == aParent) && ((*it).mId == aId)) {
+ waitingRequests.erase(it);
+ return;
+ }
+ }
+}
+
+void MediaSystemResourceService::RemoveRequests(
+ media::MediaSystemResourceManagerParent* aParent,
+ MediaSystemResourceType aResourceType) {
+ MOZ_ASSERT(aParent);
+
+ MediaSystemResource* resource =
+ mResources.Get(static_cast<uint32_t>(aResourceType));
+
+ if (!resource || resource->mResourceCount == 0) {
+ // Resource does not exit
+ return;
+ }
+
+ std::deque<MediaSystemResourceRequest>::iterator it;
+ std::deque<MediaSystemResourceRequest>& acquiredRequests =
+ resource->mAcquiredRequests;
+ for (it = acquiredRequests.begin(); it != acquiredRequests.end();) {
+ if ((*it).mParent == aParent) {
+ it = acquiredRequests.erase(it);
+ } else {
+ it++;
+ }
+ }
+
+ std::deque<MediaSystemResourceRequest>& waitingRequests =
+ resource->mWaitingRequests;
+ for (it = waitingRequests.begin(); it != waitingRequests.end();) {
+ if ((*it).mParent == aParent) {
+ it = waitingRequests.erase(it);
+ } else {
+ it++;
+ }
+ }
+}
+
+void MediaSystemResourceService::UpdateRequests(
+ MediaSystemResourceType aResourceType) {
+ MediaSystemResource* resource =
+ mResources.Get(static_cast<uint32_t>(aResourceType));
+
+ if (!resource || resource->mResourceCount == 0) {
+ // Resource does not exit
+ return;
+ }
+
+ std::deque<MediaSystemResourceRequest>& acquiredRequests =
+ resource->mAcquiredRequests;
+ std::deque<MediaSystemResourceRequest>& waitingRequests =
+ resource->mWaitingRequests;
+
+ while ((acquiredRequests.size() < resource->mResourceCount) &&
+ (!waitingRequests.empty())) {
+ MediaSystemResourceRequest& request = waitingRequests.front();
+ MOZ_ASSERT(request.mParent);
+ // Send response
+ mozilla::Unused << request.mParent->SendResponse(request.mId,
+ true /* success */);
+ // Move request to mAcquiredRequests
+ acquiredRequests.push_back(waitingRequests.front());
+ waitingRequests.pop_front();
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/systemservices/MediaSystemResourceService.h b/dom/media/systemservices/MediaSystemResourceService.h
new file mode 100644
index 0000000000..8a75a6cafd
--- /dev/null
+++ b/dom/media/systemservices/MediaSystemResourceService.h
@@ -0,0 +1,83 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(MediaSystemResourceService_h_)
+# define MediaSystemResourceService_h_
+
+# include <deque>
+
+# include "MediaSystemResourceTypes.h"
+# include "mozilla/StaticPtr.h"
+# include "nsClassHashtable.h"
+
+namespace mozilla {
+
+namespace media {
+class MediaSystemResourceManagerParent;
+} // namespace media
+
+/**
+ * Manage media system resource allocation requests within system.
+ */
+class MediaSystemResourceService {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaSystemResourceService)
+
+ static MediaSystemResourceService* Get();
+ static void Init();
+ static void Shutdown();
+
+ void Acquire(media::MediaSystemResourceManagerParent* aParent, uint32_t aId,
+ MediaSystemResourceType aResourceType, bool aWillWait);
+
+ void ReleaseResource(media::MediaSystemResourceManagerParent* aParent,
+ uint32_t aId, MediaSystemResourceType aResourceType);
+
+ void ReleaseResource(media::MediaSystemResourceManagerParent* aParent);
+
+ private:
+ MediaSystemResourceService();
+ ~MediaSystemResourceService();
+
+ struct MediaSystemResourceRequest {
+ MediaSystemResourceRequest() : mParent(nullptr), mId(-1) {}
+ MediaSystemResourceRequest(media::MediaSystemResourceManagerParent* aParent,
+ uint32_t aId)
+ : mParent(aParent), mId(aId) {}
+ media::MediaSystemResourceManagerParent* mParent;
+ uint32_t mId;
+ };
+
+ struct MediaSystemResource {
+ MediaSystemResource() : mResourceCount(0) {}
+ explicit MediaSystemResource(uint32_t aResourceCount)
+ : mResourceCount(aResourceCount) {}
+
+ std::deque<MediaSystemResourceRequest> mWaitingRequests;
+ std::deque<MediaSystemResourceRequest> mAcquiredRequests;
+ uint32_t mResourceCount;
+ };
+
+ void Destroy();
+
+ void RemoveRequest(media::MediaSystemResourceManagerParent* aParent,
+ uint32_t aId, MediaSystemResourceType aResourceType);
+
+ void RemoveRequests(media::MediaSystemResourceManagerParent* aParent,
+ MediaSystemResourceType aResourceType);
+
+ void UpdateRequests(MediaSystemResourceType aResourceType);
+
+ bool mDestroyed;
+
+ nsClassHashtable<nsUint32HashKey, MediaSystemResource> mResources;
+
+ static StaticRefPtr<MediaSystemResourceService> sSingleton;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/systemservices/MediaSystemResourceTypes.h b/dom/media/systemservices/MediaSystemResourceTypes.h
new file mode 100644
index 0000000000..d294c2b364
--- /dev/null
+++ b/dom/media/systemservices/MediaSystemResourceTypes.h
@@ -0,0 +1,23 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(MediaSystemResourceTypes_h_)
+# define MediaSystemResourceTypes_h_
+
+namespace mozilla {
+
+enum class MediaSystemResourceType : uint32_t {
+ VIDEO_DECODER = 0,
+ AUDIO_DECODER, // Not supported currently.
+ VIDEO_ENCODER,
+ AUDIO_ENCODER, // Not supported currently.
+ CAMERA, // Not supported currently.
+ INVALID_RESOURCE,
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/systemservices/MediaTaskUtils.h b/dom/media/systemservices/MediaTaskUtils.h
new file mode 100644
index 0000000000..cbe464e015
--- /dev/null
+++ b/dom/media/systemservices/MediaTaskUtils.h
@@ -0,0 +1,52 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_MediaTaskUtils_h
+#define mozilla_MediaTaskUtils_h
+
+#include "nsThreadUtils.h"
+
+// The main reason this file is separate from MediaUtils.h
+#include "base/task.h"
+
+namespace mozilla {
+namespace media {
+
+/* media::NewTaskFrom() - Create a Task from a lambda.
+ *
+ * Similar to media::NewRunnableFrom() - Create an nsRunnable from a lambda,
+ * but ignore the return value from the lambda.
+ *
+ * Prefer NS_NewRunnableFunction(), which provides a specific name, unless the
+ * lambda really must have a non-void return value that is to be ignored.
+ */
+
+template <typename OnRunType>
+class LambdaTask : public Runnable {
+ public:
+ explicit LambdaTask(OnRunType&& aOnRun)
+ : Runnable("media::LambdaTask"), mOnRun(std::move(aOnRun)) {}
+
+ private:
+ NS_IMETHOD
+ Run() override {
+ mOnRun();
+ return NS_OK;
+ }
+ OnRunType mOnRun;
+};
+
+template <typename OnRunType>
+already_AddRefed<LambdaTask<OnRunType>> NewTaskFrom(OnRunType&& aOnRun) {
+ typedef LambdaTask<OnRunType> LambdaType;
+ RefPtr<LambdaType> lambda = new LambdaType(std::forward<OnRunType>(aOnRun));
+ return lambda.forget();
+}
+
+} // namespace media
+} // namespace mozilla
+
+#endif // mozilla_MediaTaskUtils_h
diff --git a/dom/media/systemservices/MediaUtils.cpp b/dom/media/systemservices/MediaUtils.cpp
new file mode 100644
index 0000000000..fad2fd4e2c
--- /dev/null
+++ b/dom/media/systemservices/MediaUtils.cpp
@@ -0,0 +1,126 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaUtils.h"
+
+#include "mozilla/AppShutdown.h"
+#include "mozilla/Services.h"
+
+namespace mozilla::media {
+
+nsCOMPtr<nsIAsyncShutdownClient> GetShutdownBarrier() {
+ nsCOMPtr<nsIAsyncShutdownService> svc = services::GetAsyncShutdownService();
+ if (!svc) {
+ // We can fail to get the shutdown service if we're already shutting down.
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIAsyncShutdownClient> barrier;
+ nsresult rv = svc->GetProfileBeforeChange(getter_AddRefs(barrier));
+ if (!barrier) {
+ // We are probably in a content process. We need to do cleanup at
+ // XPCOM shutdown in leakchecking builds.
+ rv = svc->GetXpcomWillShutdown(getter_AddRefs(barrier));
+ }
+ MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
+ MOZ_RELEASE_ASSERT(barrier);
+ return barrier;
+}
+
+nsCOMPtr<nsIAsyncShutdownClient> MustGetShutdownBarrier() {
+ nsCOMPtr<nsIAsyncShutdownClient> barrier = GetShutdownBarrier();
+ MOZ_RELEASE_ASSERT(barrier);
+ return barrier;
+}
+
+NS_IMPL_ISUPPORTS(ShutdownBlocker, nsIAsyncShutdownBlocker)
+
+namespace {
+class TicketBlocker : public ShutdownBlocker {
+ using ShutdownMozPromise = ShutdownBlockingTicket::ShutdownMozPromise;
+
+ public:
+ explicit TicketBlocker(const nsAString& aName)
+ : ShutdownBlocker(aName), mPromise(mHolder.Ensure(__func__)) {}
+
+ NS_IMETHOD
+ BlockShutdown(nsIAsyncShutdownClient* aProfileBeforeChange) override {
+ mHolder.Resolve(true, __func__);
+ return NS_OK;
+ }
+
+ void RejectIfExists() { mHolder.RejectIfExists(false, __func__); }
+
+ ShutdownMozPromise* ShutdownPromise() { return mPromise; }
+
+ private:
+ ~TicketBlocker() = default;
+
+ MozPromiseHolder<ShutdownMozPromise> mHolder;
+ const RefPtr<ShutdownMozPromise> mPromise;
+};
+
+class ShutdownBlockingTicketImpl : public ShutdownBlockingTicket {
+ private:
+ RefPtr<TicketBlocker> mBlocker;
+
+ public:
+ explicit ShutdownBlockingTicketImpl(RefPtr<TicketBlocker> aBlocker)
+ : mBlocker(std::move(aBlocker)) {}
+
+ static UniquePtr<ShutdownBlockingTicket> Create(const nsAString& aName,
+ const nsAString& aFileName,
+ int32_t aLineNr) {
+ auto blocker = MakeRefPtr<TicketBlocker>(aName);
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "ShutdownBlockingTicketImpl::AddBlocker",
+ [blocker, file = nsString(aFileName), aLineNr] {
+ MustGetShutdownBarrier()->AddBlocker(blocker, file, aLineNr, u""_ns);
+ }));
+ if (AppShutdown::IsInOrBeyond(ShutdownPhase::AppShutdown)) {
+ // Adding a blocker is not guaranteed to succeed. Remove the blocker in
+ // case it succeeded anyway, and bail.
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "ShutdownBlockingTicketImpl::RemoveBlocker", [blocker] {
+ MustGetShutdownBarrier()->RemoveBlocker(blocker);
+ blocker->RejectIfExists();
+ }));
+ return nullptr;
+ }
+
+ // Adding a blocker is now guaranteed to succeed:
+ // - If AppShutdown::IsInOrBeyond(AppShutdown) returned false,
+ // - then the AddBlocker main thread task was queued before AppShutdown's
+ // sCurrentShutdownPhase is set to ShutdownPhase::AppShutdown,
+ // - which is before AppShutdown will drain the (main thread) event queue to
+ // run the AddBlocker task, if not already run,
+ // - which is before profile-before-change (the earliest barrier we'd add a
+ // blocker to, see GetShutdownBarrier()) is notified,
+ // - which is when AsyncShutdown prevents further conditions (blockers)
+ // being added to the profile-before-change barrier.
+ return MakeUnique<ShutdownBlockingTicketImpl>(std::move(blocker));
+ }
+
+ ~ShutdownBlockingTicketImpl() {
+ MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(
+ NS_NewRunnableFunction(__func__, [blocker = std::move(mBlocker)] {
+ GetShutdownBarrier()->RemoveBlocker(blocker);
+ blocker->RejectIfExists();
+ })));
+ }
+
+ ShutdownMozPromise* ShutdownPromise() override {
+ return mBlocker->ShutdownPromise();
+ }
+};
+} // namespace
+
+UniquePtr<ShutdownBlockingTicket> ShutdownBlockingTicket::Create(
+ const nsAString& aName, const nsAString& aFileName, int32_t aLineNr) {
+ return ShutdownBlockingTicketImpl::Create(aName, aFileName, aLineNr);
+}
+
+} // namespace mozilla::media
diff --git a/dom/media/systemservices/MediaUtils.h b/dom/media/systemservices/MediaUtils.h
new file mode 100644
index 0000000000..90e21ce9aa
--- /dev/null
+++ b/dom/media/systemservices/MediaUtils.h
@@ -0,0 +1,332 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_MediaUtils_h
+#define mozilla_MediaUtils_h
+
+#include <map>
+
+#include "mozilla/Assertions.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/SharedThreadPool.h"
+#include "mozilla/TaskQueue.h"
+#include "mozilla/UniquePtr.h"
+#include "MediaEventSource.h"
+#include "nsCOMPtr.h"
+#include "nsIAsyncShutdown.h"
+#include "nsISupportsImpl.h"
+#include "nsProxyRelease.h"
+#include "nsThreadUtils.h"
+
+class nsIEventTarget;
+
+namespace mozilla::media {
+
+/* media::NewRunnableFrom() - Create a Runnable from a lambda.
+ *
+ * Passing variables (closures) to an async function is clunky with Runnable:
+ *
+ * void Foo()
+ * {
+ * class FooRunnable : public Runnable
+ * {
+ * public:
+ * FooRunnable(const Bar &aBar) : mBar(aBar) {}
+ * NS_IMETHOD Run() override
+ * {
+ * // Use mBar
+ * }
+ * private:
+ * RefPtr<Bar> mBar;
+ * };
+ *
+ * RefPtr<Bar> bar = new Bar();
+ * NS_DispatchToMainThread(new FooRunnable(bar);
+ * }
+ *
+ * It's worse with more variables. Lambdas have a leg up with variable capture:
+ *
+ * void Foo()
+ * {
+ * RefPtr<Bar> bar = new Bar();
+ * NS_DispatchToMainThread(media::NewRunnableFrom([bar]() mutable {
+ * // use bar
+ * }));
+ * }
+ *
+ * Capture is by-copy by default, so the nsRefPtr 'bar' is safely copied for
+ * access on the other thread (threadsafe refcounting in bar is assumed).
+ *
+ * The 'mutable' keyword is only needed for non-const access to bar.
+ */
+
+template <typename OnRunType>
+class LambdaRunnable : public Runnable {
+ public:
+ explicit LambdaRunnable(OnRunType&& aOnRun)
+ : Runnable("media::LambdaRunnable"), mOnRun(std::move(aOnRun)) {}
+
+ private:
+ NS_IMETHODIMP
+ Run() override { return mOnRun(); }
+ OnRunType mOnRun;
+};
+
+template <typename OnRunType>
+already_AddRefed<LambdaRunnable<OnRunType>> NewRunnableFrom(
+ OnRunType&& aOnRun) {
+ typedef LambdaRunnable<OnRunType> LambdaType;
+ RefPtr<LambdaType> lambda = new LambdaType(std::forward<OnRunType>(aOnRun));
+ return lambda.forget();
+}
+
+/* media::Refcountable - Add threadsafe ref-counting to something that isn't.
+ *
+ * Often, reference counting is the most practical way to share an object with
+ * another thread without imposing lifetime restrictions, even if there's
+ * otherwise no concurrent access happening on the object. For instance, an
+ * algorithm on another thread may find it more expedient to modify a passed-in
+ * object, rather than pass expensive copies back and forth.
+ *
+ * Lists in particular often aren't ref-countable, yet are expensive to copy,
+ * e.g. nsTArray<RefPtr<Foo>>. Refcountable can be used to make such objects
+ * (or owning smart-pointers to such objects) refcountable.
+ *
+ * Technical limitation: A template specialization is needed for types that take
+ * a constructor. Please add below (UniquePtr covers a lot of ground though).
+ */
+
+class RefcountableBase {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RefcountableBase)
+ protected:
+ virtual ~RefcountableBase() = default;
+};
+
+template <typename T>
+class Refcountable : public T, public RefcountableBase {
+ public:
+ Refcountable& operator=(T&& aOther) {
+ T::operator=(std::move(aOther));
+ return *this;
+ }
+
+ Refcountable& operator=(T& aOther) {
+ T::operator=(aOther);
+ return *this;
+ }
+};
+
+template <typename T>
+class Refcountable<UniquePtr<T>> : public UniquePtr<T>,
+ public RefcountableBase {
+ public:
+ explicit Refcountable(T* aPtr) : UniquePtr<T>(aPtr) {}
+};
+
+template <>
+class Refcountable<bool> : public RefcountableBase {
+ public:
+ explicit Refcountable(bool aValue) : mValue(aValue) {}
+
+ Refcountable& operator=(bool aOther) {
+ mValue = aOther;
+ return *this;
+ }
+
+ Refcountable& operator=(const Refcountable& aOther) {
+ mValue = aOther.mValue;
+ return *this;
+ }
+
+ explicit operator bool() const { return mValue; }
+
+ private:
+ bool mValue;
+};
+
+/*
+ * Async shutdown helpers
+ */
+
+nsCOMPtr<nsIAsyncShutdownClient> GetShutdownBarrier();
+
+// Like GetShutdownBarrier but will release assert that the result is not null.
+nsCOMPtr<nsIAsyncShutdownClient> MustGetShutdownBarrier();
+
+class ShutdownBlocker : public nsIAsyncShutdownBlocker {
+ public:
+ ShutdownBlocker(const nsAString& aName) : mName(aName) {}
+
+ NS_IMETHOD
+ BlockShutdown(nsIAsyncShutdownClient* aProfileBeforeChange) override = 0;
+
+ NS_IMETHOD GetName(nsAString& aName) override {
+ aName = mName;
+ return NS_OK;
+ }
+
+ NS_IMETHOD GetState(nsIPropertyBag**) override { return NS_OK; }
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ protected:
+ virtual ~ShutdownBlocker() = default;
+
+ private:
+ const nsString mName;
+};
+
+/**
+ * A convenience class representing a "ticket" that keeps the process from
+ * shutting down until it is destructed. It does this by blocking
+ * xpcom-will-shutdown. Constructed and destroyed on any thread.
+ */
+class ShutdownBlockingTicket {
+ public:
+ using ShutdownMozPromise = MozPromise<bool, bool, false>;
+
+ /**
+ * Construct with an arbitrary name, __FILE__ and __LINE__.
+ * Note that __FILE__ needs to be made wide, typically through
+ * NS_LITERAL_STRING_FROM_CSTRING(__FILE__).
+ * Returns nullptr if we are too far in the shutdown sequence to add a
+ * blocker. Any thread.
+ */
+ static UniquePtr<ShutdownBlockingTicket> Create(const nsAString& aName,
+ const nsAString& aFileName,
+ int32_t aLineNr);
+
+ virtual ~ShutdownBlockingTicket() = default;
+
+ /**
+ * MozPromise that gets resolved upon xpcom-will-shutdown.
+ * Should the ticket get destroyed before the MozPromise has been resolved,
+ * the MozPromise will get rejected.
+ */
+ virtual ShutdownMozPromise* ShutdownPromise() = 0;
+};
+
+/**
+ * Await convenience methods to block until the promise has been resolved or
+ * rejected. The Resolve/Reject functions, while called on a different thread,
+ * would be running just as on the current thread thanks to the memory barrier
+ * provided by the monitor.
+ * For now Await can only be used with an exclusive MozPromise if passed a
+ * Resolve/Reject function.
+ * Await() can *NOT* be called from a task queue/nsISerialEventTarget used for
+ * resolving/rejecting aPromise, otherwise things will deadlock.
+ */
+template <typename ResolveValueType, typename RejectValueType,
+ typename ResolveFunction, typename RejectFunction>
+void Await(already_AddRefed<nsIEventTarget> aPool,
+ RefPtr<MozPromise<ResolveValueType, RejectValueType, true>> aPromise,
+ ResolveFunction&& aResolveFunction,
+ RejectFunction&& aRejectFunction) {
+ RefPtr<TaskQueue> taskQueue =
+ TaskQueue::Create(std::move(aPool), "MozPromiseAwait");
+ Monitor mon MOZ_UNANNOTATED(__func__);
+ bool done = false;
+
+ aPromise->Then(
+ taskQueue, __func__,
+ [&](ResolveValueType&& aResolveValue) {
+ MonitorAutoLock lock(mon);
+ aResolveFunction(std::forward<ResolveValueType>(aResolveValue));
+ done = true;
+ mon.Notify();
+ },
+ [&](RejectValueType&& aRejectValue) {
+ MonitorAutoLock lock(mon);
+ aRejectFunction(std::forward<RejectValueType>(aRejectValue));
+ done = true;
+ mon.Notify();
+ });
+
+ MonitorAutoLock lock(mon);
+ while (!done) {
+ mon.Wait();
+ }
+}
+
+template <typename ResolveValueType, typename RejectValueType, bool Excl>
+typename MozPromise<ResolveValueType, RejectValueType,
+ Excl>::ResolveOrRejectValue
+Await(already_AddRefed<nsIEventTarget> aPool,
+ RefPtr<MozPromise<ResolveValueType, RejectValueType, Excl>> aPromise) {
+ RefPtr<TaskQueue> taskQueue =
+ TaskQueue::Create(std::move(aPool), "MozPromiseAwait");
+ Monitor mon MOZ_UNANNOTATED(__func__);
+ bool done = false;
+
+ typename MozPromise<ResolveValueType, RejectValueType,
+ Excl>::ResolveOrRejectValue val;
+ aPromise->Then(
+ taskQueue, __func__,
+ [&](ResolveValueType aResolveValue) {
+ val.SetResolve(std::move(aResolveValue));
+ MonitorAutoLock lock(mon);
+ done = true;
+ mon.Notify();
+ },
+ [&](RejectValueType aRejectValue) {
+ val.SetReject(std::move(aRejectValue));
+ MonitorAutoLock lock(mon);
+ done = true;
+ mon.Notify();
+ });
+
+ MonitorAutoLock lock(mon);
+ while (!done) {
+ mon.Wait();
+ }
+
+ return val;
+}
+
+/**
+ * Similar to Await, takes an array of promises of the same type.
+ * MozPromise::All is used to handle the resolution/rejection of the promises.
+ */
+template <typename ResolveValueType, typename RejectValueType,
+ typename ResolveFunction, typename RejectFunction>
+void AwaitAll(
+ already_AddRefed<nsIEventTarget> aPool,
+ nsTArray<RefPtr<MozPromise<ResolveValueType, RejectValueType, true>>>&
+ aPromises,
+ ResolveFunction&& aResolveFunction, RejectFunction&& aRejectFunction) {
+ typedef MozPromise<ResolveValueType, RejectValueType, true> Promise;
+ RefPtr<nsIEventTarget> pool = aPool;
+ RefPtr<TaskQueue> taskQueue =
+ TaskQueue::Create(do_AddRef(pool), "MozPromiseAwaitAll");
+ RefPtr<typename Promise::AllPromiseType> p =
+ Promise::All(taskQueue, aPromises);
+ Await(pool.forget(), p, std::move(aResolveFunction),
+ std::move(aRejectFunction));
+}
+
+// Note: only works with exclusive MozPromise, as Promise::All would attempt
+// to perform copy of nsTArrays which are disallowed.
+template <typename ResolveValueType, typename RejectValueType>
+typename MozPromise<ResolveValueType, RejectValueType,
+ true>::AllPromiseType::ResolveOrRejectValue
+AwaitAll(already_AddRefed<nsIEventTarget> aPool,
+ nsTArray<RefPtr<MozPromise<ResolveValueType, RejectValueType, true>>>&
+ aPromises) {
+ typedef MozPromise<ResolveValueType, RejectValueType, true> Promise;
+ RefPtr<nsIEventTarget> pool = aPool;
+ RefPtr<TaskQueue> taskQueue =
+ TaskQueue::Create(do_AddRef(pool), "MozPromiseAwaitAll");
+ RefPtr<typename Promise::AllPromiseType> p =
+ Promise::All(taskQueue, aPromises);
+ return Await(pool.forget(), p);
+}
+
+} // namespace mozilla::media
+
+#endif // mozilla_MediaUtils_h
diff --git a/dom/media/systemservices/OSXRunLoopSingleton.cpp b/dom/media/systemservices/OSXRunLoopSingleton.cpp
new file mode 100644
index 0000000000..6dea084e27
--- /dev/null
+++ b/dom/media/systemservices/OSXRunLoopSingleton.cpp
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "OSXRunLoopSingleton.h"
+#include <mozilla/StaticMutex.h>
+
+#include <AudioUnit/AudioUnit.h>
+#include <CoreAudio/AudioHardware.h>
+#include <CoreAudio/HostTime.h>
+#include <CoreFoundation/CoreFoundation.h>
+
+static bool gRunLoopSet = false;
+static mozilla::StaticMutex gMutex MOZ_UNANNOTATED;
+
+void mozilla_set_coreaudio_notification_runloop_if_needed() {
+ mozilla::StaticMutexAutoLock lock(gMutex);
+ if (gRunLoopSet) {
+ return;
+ }
+
+ /* This is needed so that AudioUnit listeners get called on this thread, and
+ * not the main thread. If we don't do that, they are not called, or a crash
+ * occur, depending on the OSX version. */
+ AudioObjectPropertyAddress runloop_address = {
+ kAudioHardwarePropertyRunLoop, kAudioObjectPropertyScopeGlobal,
+ kAudioObjectPropertyElementMaster};
+
+ CFRunLoopRef run_loop = nullptr;
+
+ OSStatus r;
+ r = AudioObjectSetPropertyData(kAudioObjectSystemObject, &runloop_address, 0,
+ NULL, sizeof(CFRunLoopRef), &run_loop);
+ if (r != noErr) {
+ NS_WARNING(
+ "Could not make global CoreAudio notifications use their own thread.");
+ }
+
+ gRunLoopSet = true;
+}
diff --git a/dom/media/systemservices/OSXRunLoopSingleton.h b/dom/media/systemservices/OSXRunLoopSingleton.h
new file mode 100644
index 0000000000..10e7b0153f
--- /dev/null
+++ b/dom/media/systemservices/OSXRunLoopSingleton.h
@@ -0,0 +1,24 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef OSXRUNLOOPSINGLETON_H_
+#define OSXRUNLOOPSINGLETON_H_
+
+#include <mozilla/Types.h>
+
+#if defined(__cplusplus)
+extern "C" {
+#endif
+
+/* This function tells CoreAudio to use its own thread for device change
+ * notifications, and can be called from any thread without external
+ * synchronization. */
+void MOZ_EXPORT mozilla_set_coreaudio_notification_runloop_if_needed();
+
+#if defined(__cplusplus)
+}
+#endif
+
+#endif // OSXRUNLOOPSINGLETON_H_
diff --git a/dom/media/systemservices/PCameras.ipdl b/dom/media/systemservices/PCameras.ipdl
new file mode 100644
index 0000000000..535702cd68
--- /dev/null
+++ b/dom/media/systemservices/PCameras.ipdl
@@ -0,0 +1,93 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PContent;
+include protocol PBackground;
+
+include PBackgroundSharedTypes;
+
+using mozilla::camera::CaptureEngine from "mozilla/media/CamerasTypes.h";
+
+namespace mozilla {
+namespace camera {
+
+// IPC analog for webrtc::VideoCaptureCapability
+struct VideoCaptureCapability
+{
+ int width;
+ int height;
+ int maxFPS;
+ int videoType;
+ bool interlaced;
+};
+
+
+// IPC analog for webrtc::VideoFrame
+// the described buffer is transported seperately in a Shmem
+// See VideoFrameUtils.h
+struct VideoFrameProperties
+{
+ // Size of image data within the ShMem,
+ // the ShMem is at least this large
+ uint32_t bufferSize;
+ // From webrtc::VideoFrame
+ uint32_t timeStamp;
+ int64_t ntpTimeMs;
+ int64_t renderTimeMs;
+ // See webrtc/**/rotation.h
+ int rotation;
+ int yAllocatedSize;
+ int uAllocatedSize;
+ int vAllocatedSize;
+ // From webrtc::VideoFrameBuffer
+ int width;
+ int height;
+ int yStride;
+ int uStride;
+ int vStride;
+};
+
+[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual]
+async protocol PCameras
+{
+ manager PBackground;
+
+child:
+ // transfers ownership of |buffer| from parent to child
+ async DeliverFrame(CaptureEngine capEngine, int streamId,
+ Shmem buffer, VideoFrameProperties props);
+ async DeviceChange();
+ async ReplyNumberOfCaptureDevices(int deviceCount);
+ async ReplyNumberOfCapabilities(int capabilityCount);
+ async ReplyAllocateCapture(int captureId);
+ async ReplyGetCaptureCapability(VideoCaptureCapability cap);
+ async ReplyGetCaptureDevice(nsCString device_name, nsCString device_id, bool scary);
+ async ReplyFailure();
+ async ReplySuccess();
+ async __delete__();
+
+parent:
+ async NumberOfCaptureDevices(CaptureEngine engine);
+ async NumberOfCapabilities(CaptureEngine engine, nsCString deviceUniqueIdUTF8);
+
+ async GetCaptureCapability(CaptureEngine engine, nsCString unique_idUTF8,
+ int capability_number);
+ async GetCaptureDevice(CaptureEngine engine, int deviceIndex);
+
+ async AllocateCapture(CaptureEngine engine, nsCString unique_idUTF8,
+ uint64_t windowID);
+ async ReleaseCapture(CaptureEngine engine, int captureId);
+ async StartCapture(CaptureEngine engine, int captureId,
+ VideoCaptureCapability capability);
+ async FocusOnSelectedSource(CaptureEngine engine, int captureId);
+ async StopCapture(CaptureEngine engine, int captureId);
+ // transfers frame back
+ async ReleaseFrame(Shmem s);
+
+ // setup camera engine
+ async EnsureInitialized(CaptureEngine engine);
+};
+
+} // namespace camera
+} // namespace mozilla
diff --git a/dom/media/systemservices/PMedia.ipdl b/dom/media/systemservices/PMedia.ipdl
new file mode 100644
index 0000000000..c3d8476b5f
--- /dev/null
+++ b/dom/media/systemservices/PMedia.ipdl
@@ -0,0 +1,55 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PContent;
+
+include PBackgroundSharedTypes;
+
+include "mozilla/media/MediaChild.h";
+
+namespace mozilla {
+namespace media {
+
+[ManualDealloc, ChildImpl="Child", ParentImpl=virtual]
+protocol PMedia
+{
+ manager PContent;
+
+parent:
+ /**
+ * Requests a potentially persistent unique secret key for each principal.
+ * Has no expiry, but is cleared by age along with cookies.
+ * This is needed by mediaDevices.enumerateDevices() to produce persistent
+ * deviceIds that wont work cross-origin.
+ *
+ * If this OriginAttributes dictionary has the privateBrowsing flag set to
+ * false, a key for this origin is returned from a primary pool of temporal
+ * in-memory keys and persistent keys read from disk. If no key exists, a
+ * temporal one is created.
+ * If aPersist is true and key is temporal, the key is promoted to persistent.
+ * Once persistent, a key cannot become temporal again.
+ *
+ * If the OriginAttributes dictionary has the privateBrowsing flag set to
+ * true, a different key for this origin is returned from a secondary pool
+ * that is never persisted to disk, and aPersist is ignored.
+ */
+ async GetPrincipalKey(PrincipalInfo aPrincipal, bool aPersist) returns(nsCString aKey);
+
+ /**
+ * Clear per-orgin list of persistent deviceIds stored for enumerateDevices
+ * Fire and forget.
+ *
+ * aSinceTime - milliseconds since 1 January 1970 00:00:00 UTC. 0 = clear all
+ *
+ * aOnlyPrivateBrowsing - if true then only purge the separate in-memory
+ * per-origin list used in Private Browsing.
+ */
+ async SanitizeOriginKeys(uint64_t aSinceWhen, bool aOnlyPrivateBrowsing);
+
+child:
+ async __delete__();
+};
+
+} // namespace media
+} // namespace mozilla
diff --git a/dom/media/systemservices/PMediaSystemResourceManager.ipdl b/dom/media/systemservices/PMediaSystemResourceManager.ipdl
new file mode 100644
index 0000000000..a682de60bf
--- /dev/null
+++ b/dom/media/systemservices/PMediaSystemResourceManager.ipdl
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PImageBridge;
+include "mozilla/media/MediaSystemResourceMessageUtils.h";
+
+using mozilla::MediaSystemResourceType from "mozilla/media/MediaSystemResourceTypes.h";
+
+namespace mozilla {
+namespace media {
+
+/*
+ * The PMediaSystemResourceManager is a sub-protocol in PImageBridge
+ */
+[ManualDealloc]
+sync protocol PMediaSystemResourceManager
+{
+ manager PImageBridge;
+
+child:
+ async Response(uint32_t aId, bool aSuccess);
+ async __delete__();
+
+parent:
+ async Acquire(uint32_t aId, MediaSystemResourceType aResourceType, bool aWillWait);
+ async Release(uint32_t aId);
+
+ /**
+ * Asynchronously tell the parent side to remove the PMediaSystemResourceManager.
+ */
+ async RemoveResourceManager();
+};
+
+} // namespace media
+} // namespace mozilla
+
diff --git a/dom/media/systemservices/ShmemPool.cpp b/dom/media/systemservices/ShmemPool.cpp
new file mode 100644
index 0000000000..c39ed5e790
--- /dev/null
+++ b/dom/media/systemservices/ShmemPool.cpp
@@ -0,0 +1,99 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/ShmemPool.h"
+
+#include <utility>
+
+#include "mozilla/Assertions.h"
+#include "mozilla/Logging.h"
+
+mozilla::LazyLogModule sShmemPoolLog("ShmemPool");
+
+#define SHMEMPOOL_LOG_VERBOSE(args) \
+ MOZ_LOG(sShmemPoolLog, mozilla::LogLevel::Verbose, args)
+
+namespace mozilla {
+
+ShmemPool::ShmemPool(size_t aPoolSize, PoolType aPoolType)
+ : mPoolType(aPoolType),
+ mMutex("mozilla::ShmemPool"),
+ mPoolFree(aPoolSize),
+ mErrorLogged(false)
+#ifdef DEBUG
+ ,
+ mMaxPoolUse(0)
+#endif
+{
+ mShmemPool.SetLength(aPoolSize);
+}
+
+mozilla::ShmemBuffer ShmemPool::GetIfAvailable(size_t aSize) {
+ MutexAutoLock lock(mMutex);
+
+ // Pool is empty, don't block caller.
+ if (mPoolFree == 0) {
+ if (!mErrorLogged) {
+ // log "out of pool" once as error to avoid log spam
+ mErrorLogged = true;
+ SHMEMPOOL_LOG_ERROR(
+ ("ShmemPool is empty, future occurrences "
+ "will be logged as warnings"));
+ } else {
+ SHMEMPOOL_LOG_WARN(("ShmemPool is empty"));
+ }
+ // This isn't initialized, so will be understood as an error.
+ return ShmemBuffer();
+ }
+
+ ShmemBuffer& res = mShmemPool[mPoolFree - 1];
+
+ if (!res.mInitialized) {
+ SHMEMPOOL_LOG(("No free preallocated Shmem"));
+ return ShmemBuffer();
+ }
+
+ MOZ_ASSERT(res.mShmem.IsWritable(), "Pool in Shmem is not writable?");
+
+ if (res.mShmem.Size<uint8_t>() < aSize) {
+ SHMEMPOOL_LOG(("Free Shmem but not of the right size"));
+ return ShmemBuffer();
+ }
+
+ mPoolFree--;
+#ifdef DEBUG
+ size_t poolUse = mShmemPool.Length() - mPoolFree;
+ if (poolUse > mMaxPoolUse) {
+ mMaxPoolUse = poolUse;
+ SHMEMPOOL_LOG(
+ ("Maximum ShmemPool use increased: %zu buffers", mMaxPoolUse));
+ }
+#endif
+ return std::move(res);
+}
+
+void ShmemPool::Put(ShmemBuffer&& aShmem) {
+ MutexAutoLock lock(mMutex);
+ MOZ_ASSERT(mPoolFree < mShmemPool.Length());
+ mShmemPool[mPoolFree] = std::move(aShmem);
+ mPoolFree++;
+#ifdef DEBUG
+ size_t poolUse = mShmemPool.Length() - mPoolFree;
+ if (poolUse > 0) {
+ SHMEMPOOL_LOG_VERBOSE(("ShmemPool usage reduced to %zu buffers", poolUse));
+ }
+#endif
+}
+
+ShmemPool::~ShmemPool() {
+#ifdef DEBUG
+ for (size_t i = 0; i < mShmemPool.Length(); i++) {
+ MOZ_ASSERT(!mShmemPool[i].Valid());
+ }
+#endif
+}
+
+} // namespace mozilla
diff --git a/dom/media/systemservices/ShmemPool.h b/dom/media/systemservices/ShmemPool.h
new file mode 100644
index 0000000000..e62ccff24a
--- /dev/null
+++ b/dom/media/systemservices/ShmemPool.h
@@ -0,0 +1,181 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_ShmemPool_h
+#define mozilla_ShmemPool_h
+
+#include "mozilla/Mutex.h"
+#include "mozilla/ipc/Shmem.h"
+#include "nsTArray.h"
+
+extern mozilla::LazyLogModule sShmemPoolLog;
+#define SHMEMPOOL_LOG(args) \
+ MOZ_LOG(sShmemPoolLog, mozilla::LogLevel::Debug, args)
+#define SHMEMPOOL_LOG_WARN(args) \
+ MOZ_LOG(sShmemPoolLog, mozilla::LogLevel::Warning, args)
+#define SHMEMPOOL_LOG_ERROR(args) \
+ MOZ_LOG(sShmemPoolLog, mozilla::LogLevel::Error, args)
+
+namespace mozilla {
+
+class ShmemPool;
+
+class ShmemBuffer {
+ public:
+ ShmemBuffer() : mInitialized(false) {}
+ explicit ShmemBuffer(mozilla::ipc::Shmem aShmem) {
+ mInitialized = true;
+ mShmem = aShmem;
+ }
+
+ ShmemBuffer(ShmemBuffer&& rhs) {
+ mInitialized = rhs.mInitialized;
+ mShmem = std::move(rhs.mShmem);
+ }
+
+ ShmemBuffer& operator=(ShmemBuffer&& rhs) {
+ MOZ_ASSERT(&rhs != this, "self-moves are prohibited");
+ mInitialized = rhs.mInitialized;
+ mShmem = std::move(rhs.mShmem);
+ return *this;
+ }
+
+ // No copies allowed
+ ShmemBuffer(const ShmemBuffer&) = delete;
+ ShmemBuffer& operator=(const ShmemBuffer&) = delete;
+
+ bool Valid() { return mInitialized; }
+
+ uint8_t* GetBytes() { return mShmem.get<uint8_t>(); }
+
+ mozilla::ipc::Shmem& Get() { return mShmem; }
+
+ private:
+ friend class ShmemPool;
+
+ bool mInitialized;
+ mozilla::ipc::Shmem mShmem;
+};
+
+class ShmemPool final {
+ public:
+ enum class PoolType { StaticPool, DynamicPool };
+ explicit ShmemPool(size_t aPoolSize,
+ PoolType aPoolType = PoolType::StaticPool);
+ ~ShmemPool();
+ // Get/GetIfAvailable differ in what thread they can run on. GetIfAvailable
+ // can run anywhere but won't allocate if the right size isn't available.
+ ShmemBuffer GetIfAvailable(size_t aSize);
+ void Put(ShmemBuffer&& aShmem);
+
+ // We need to use the allocation/deallocation functions
+ // of a specific IPC child/parent instance.
+ template <class T>
+ void Cleanup(T* aInstance) {
+ MutexAutoLock lock(mMutex);
+ for (size_t i = 0; i < mShmemPool.Length(); i++) {
+ if (mShmemPool[i].mInitialized) {
+ aInstance->DeallocShmem(mShmemPool[i].Get());
+ mShmemPool[i].mInitialized = false;
+ }
+ }
+ }
+
+ enum class AllocationPolicy { Default, Unsafe };
+
+ template <class T>
+ ShmemBuffer Get(T* aInstance, size_t aSize,
+ AllocationPolicy aPolicy = AllocationPolicy::Default) {
+ MutexAutoLock lock(mMutex);
+
+ // Pool is empty, don't block caller.
+ if (mPoolFree == 0 && mPoolType == PoolType::StaticPool) {
+ if (!mErrorLogged) {
+ // log "out of pool" once as error to avoid log spam
+ mErrorLogged = true;
+ SHMEMPOOL_LOG_ERROR(
+ ("ShmemPool is empty, future occurrences "
+ "will be logged as warnings"));
+ } else {
+ SHMEMPOOL_LOG_WARN(("ShmemPool is empty"));
+ }
+ // This isn't initialized, so will be understood as an error.
+ return ShmemBuffer();
+ }
+ if (mPoolFree == 0) {
+ MOZ_ASSERT(mPoolType == PoolType::DynamicPool);
+ SHMEMPOOL_LOG(("Dynamic ShmemPool empty, allocating extra Shmem buffer"));
+ ShmemBuffer newBuffer;
+ mShmemPool.InsertElementAt(0, std::move(newBuffer));
+ mPoolFree++;
+ }
+
+ ShmemBuffer& res = mShmemPool[mPoolFree - 1];
+
+ if (!res.mInitialized) {
+ SHMEMPOOL_LOG(("Initializing new Shmem in pool"));
+ if (!AllocateShmem(aInstance, aSize, res, aPolicy)) {
+ SHMEMPOOL_LOG(("Failure allocating new Shmem buffer"));
+ return ShmemBuffer();
+ }
+ res.mInitialized = true;
+ }
+
+ MOZ_DIAGNOSTIC_ASSERT(res.mShmem.IsWritable(),
+ "Shmem in Pool is not writable?");
+
+ // Prepare buffer, increase size if needed (we never shrink as we don't
+ // maintain seperate sized pools and we don't want to keep reallocating)
+ if (res.mShmem.Size<char>() < aSize) {
+ SHMEMPOOL_LOG(("Size change/increase in Shmem Pool"));
+ aInstance->DeallocShmem(res.mShmem);
+ res.mInitialized = false;
+ // this may fail; always check return value
+ if (!AllocateShmem(aInstance, aSize, res, aPolicy)) {
+ SHMEMPOOL_LOG(("Failure allocating resized Shmem buffer"));
+ return ShmemBuffer();
+ } else {
+ res.mInitialized = true;
+ }
+ }
+
+ MOZ_ASSERT(res.mShmem.IsWritable(),
+ "Shmem in Pool is not writable post resize?");
+
+ mPoolFree--;
+#ifdef DEBUG
+ size_t poolUse = mShmemPool.Length() - mPoolFree;
+ if (poolUse > mMaxPoolUse) {
+ mMaxPoolUse = poolUse;
+ SHMEMPOOL_LOG(
+ ("Maximum ShmemPool use increased: %zu buffers", mMaxPoolUse));
+ }
+#endif
+ return std::move(res);
+ }
+
+ private:
+ template <class T>
+ bool AllocateShmem(T* aInstance, size_t aSize, ShmemBuffer& aRes,
+ AllocationPolicy aPolicy) {
+ return (aPolicy == AllocationPolicy::Default &&
+ aInstance->AllocShmem(aSize, &aRes.mShmem)) ||
+ (aPolicy == AllocationPolicy::Unsafe &&
+ aInstance->AllocUnsafeShmem(aSize, &aRes.mShmem));
+ }
+ const PoolType mPoolType;
+ Mutex mMutex MOZ_UNANNOTATED;
+ size_t mPoolFree;
+ bool mErrorLogged;
+#ifdef DEBUG
+ size_t mMaxPoolUse;
+#endif
+ nsTArray<ShmemBuffer> mShmemPool;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_ShmemPool_h
diff --git a/dom/media/systemservices/VideoEngine.cpp b/dom/media/systemservices/VideoEngine.cpp
new file mode 100644
index 0000000000..1d4bde6344
--- /dev/null
+++ b/dom/media/systemservices/VideoEngine.cpp
@@ -0,0 +1,245 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "VideoEngine.h"
+#include "libwebrtcglue/SystemTime.h"
+#include "video_engine/desktop_capture_impl.h"
+#include "system_wrappers/include/clock.h"
+#ifdef WEBRTC_ANDROID
+# include "modules/video_capture/video_capture.h"
+#endif
+
+#ifdef MOZ_WIDGET_ANDROID
+# include "mozilla/jni/Utils.h"
+#endif
+
+namespace mozilla::camera {
+
+#undef LOG
+#undef LOG_ENABLED
+mozilla::LazyLogModule gVideoEngineLog("VideoEngine");
+#define LOG(args) MOZ_LOG(gVideoEngineLog, mozilla::LogLevel::Debug, args)
+#define LOG_ENABLED() MOZ_LOG_TEST(gVideoEngineLog, mozilla::LogLevel::Debug)
+
+#if defined(ANDROID)
+int VideoEngine::SetAndroidObjects() {
+ LOG(("%s", __PRETTY_FUNCTION__));
+
+ JavaVM* const javaVM = mozilla::jni::GetVM();
+ if (!javaVM || webrtc::SetCaptureAndroidVM(javaVM) != 0) {
+ LOG(("Could not set capture Android VM"));
+ return -1;
+ }
+# ifdef WEBRTC_INCLUDE_INTERNAL_VIDEO_RENDER
+ if (webrtc::SetRenderAndroidVM(javaVM) != 0) {
+ LOG(("Could not set render Android VM"));
+ return -1;
+ }
+# endif
+ return 0;
+}
+#endif
+
+int32_t VideoEngine::CreateVideoCapture(const char* aDeviceUniqueIdUTF8) {
+ LOG(("%s", __PRETTY_FUNCTION__));
+ MOZ_ASSERT(aDeviceUniqueIdUTF8);
+
+ int32_t id = GenerateId();
+ LOG(("CaptureDeviceInfo.type=%s id=%d", mCaptureDevInfo.TypeName(), id));
+
+ for (auto& it : mCaps) {
+ if (it.second.VideoCapture() &&
+ it.second.VideoCapture()->CurrentDeviceName() &&
+ strcmp(it.second.VideoCapture()->CurrentDeviceName(),
+ aDeviceUniqueIdUTF8) == 0) {
+ mIdMap.emplace(id, it.first);
+ return id;
+ }
+ }
+
+ CaptureEntry entry = {-1, nullptr};
+
+ if (mCaptureDevInfo.type == CaptureDeviceType::Camera) {
+ entry = CaptureEntry(
+ id, webrtc::VideoCaptureFactory::Create(aDeviceUniqueIdUTF8));
+ if (entry.VideoCapture()) {
+ entry.VideoCapture()->SetApplyRotation(true);
+ }
+ } else {
+#ifndef WEBRTC_ANDROID
+ entry = CaptureEntry(
+ id, rtc::scoped_refptr<webrtc::VideoCaptureModule>(
+ webrtc::DesktopCaptureImpl::Create(id, aDeviceUniqueIdUTF8,
+ mCaptureDevInfo.type)));
+#else
+ MOZ_ASSERT("CreateVideoCapture NO DESKTOP CAPTURE IMPL ON ANDROID" ==
+ nullptr);
+#endif
+ }
+ mCaps.emplace(id, std::move(entry));
+ mIdMap.emplace(id, id);
+ return id;
+}
+
+int VideoEngine::ReleaseVideoCapture(const int32_t aId) {
+ bool found = false;
+
+#ifdef DEBUG
+ {
+ auto it = mIdMap.find(aId);
+ MOZ_ASSERT(it != mIdMap.end());
+ Unused << it;
+ }
+#endif
+
+ for (auto& it : mIdMap) {
+ if (it.first != aId && it.second == mIdMap[aId]) {
+ // There are other tracks still using this hardware.
+ found = true;
+ }
+ }
+
+ if (!found) {
+ WithEntry(aId, [&found](CaptureEntry& cap) {
+ cap.mVideoCaptureModule = nullptr;
+ found = true;
+ });
+ MOZ_ASSERT(found);
+ if (found) {
+ auto it = mCaps.find(mIdMap[aId]);
+ MOZ_ASSERT(it != mCaps.end());
+ mCaps.erase(it);
+ }
+ }
+
+ mIdMap.erase(aId);
+ return found ? 0 : (-1);
+}
+
+std::shared_ptr<webrtc::VideoCaptureModule::DeviceInfo>
+VideoEngine::GetOrCreateVideoCaptureDeviceInfo() {
+ LOG(("%s", __PRETTY_FUNCTION__));
+ webrtc::Timestamp currentTime = webrtc::Timestamp::Micros(0);
+
+ const char* capDevTypeName =
+ CaptureDeviceInfo(mCaptureDevInfo.type).TypeName();
+
+ if (mDeviceInfo) {
+ LOG(("Device cache available."));
+ // Camera cache is invalidated by HW change detection elsewhere
+ if (mCaptureDevInfo.type == CaptureDeviceType::Camera) {
+ LOG(("returning cached CaptureDeviceInfo of type %s", capDevTypeName));
+ return mDeviceInfo;
+ }
+ // Screen sharing cache is invalidated after the expiration time
+ currentTime = WebrtcSystemTime();
+ LOG(("Checking expiry, fetched current time of: %" PRId64,
+ currentTime.ms()));
+ LOG(("device cache expiration is %" PRId64, mExpiryTime.ms()));
+ if (currentTime <= mExpiryTime) {
+ LOG(("returning cached CaptureDeviceInfo of type %s", capDevTypeName));
+ return mDeviceInfo;
+ }
+ }
+
+ if (currentTime.IsZero()) {
+ currentTime = WebrtcSystemTime();
+ LOG(("Fetched current time of: %" PRId64, currentTime.ms()));
+ }
+ mExpiryTime = currentTime + webrtc::TimeDelta::Millis(kCacheExpiryPeriodMs);
+ LOG(("new device cache expiration is %" PRId64, mExpiryTime.ms()));
+ LOG(("creating a new VideoCaptureDeviceInfo of type %s", capDevTypeName));
+
+ switch (mCaptureDevInfo.type) {
+ case CaptureDeviceType::Camera: {
+#ifdef MOZ_WIDGET_ANDROID
+ if (SetAndroidObjects()) {
+ LOG(("VideoEngine::SetAndroidObjects Failed"));
+ break;
+ }
+#endif
+ mDeviceInfo.reset(webrtc::VideoCaptureFactory::CreateDeviceInfo());
+ LOG(("CaptureDeviceType::Camera: Finished creating new device."));
+ break;
+ }
+ // Window and Screen and Browser (tab) types are handled by DesktopCapture
+ case CaptureDeviceType::Browser:
+ case CaptureDeviceType::Window:
+ case CaptureDeviceType::Screen: {
+#if !defined(WEBRTC_ANDROID) && !defined(WEBRTC_IOS)
+ mDeviceInfo = webrtc::DesktopCaptureImpl::CreateDeviceInfo(
+ mId, mCaptureDevInfo.type);
+ LOG(("screen capture: Finished creating new device."));
+#else
+ MOZ_ASSERT(
+ "GetVideoCaptureDeviceInfo NO DESKTOP CAPTURE IMPL ON ANDROID" ==
+ nullptr);
+ mDeviceInfo.reset();
+#endif
+ break;
+ }
+ }
+ LOG(("EXIT %s", __PRETTY_FUNCTION__));
+ return mDeviceInfo;
+}
+
+already_AddRefed<VideoEngine> VideoEngine::Create(
+ const CaptureDeviceType& aCaptureDeviceType) {
+ LOG(("%s", __PRETTY_FUNCTION__));
+ return do_AddRef(new VideoEngine(aCaptureDeviceType));
+}
+
+VideoEngine::CaptureEntry::CaptureEntry(
+ int32_t aCapnum, rtc::scoped_refptr<webrtc::VideoCaptureModule> aCapture)
+ : mCapnum(aCapnum), mVideoCaptureModule(aCapture) {}
+
+rtc::scoped_refptr<webrtc::VideoCaptureModule>
+VideoEngine::CaptureEntry::VideoCapture() {
+ return mVideoCaptureModule;
+}
+
+int32_t VideoEngine::CaptureEntry::Capnum() const { return mCapnum; }
+
+bool VideoEngine::WithEntry(
+ const int32_t entryCapnum,
+ const std::function<void(CaptureEntry& entry)>&& fn) {
+#ifdef DEBUG
+ {
+ auto it = mIdMap.find(entryCapnum);
+ MOZ_ASSERT(it != mIdMap.end());
+ Unused << it;
+ }
+#endif
+
+ auto it = mCaps.find(mIdMap[entryCapnum]);
+ MOZ_ASSERT(it != mCaps.end());
+ if (it == mCaps.end()) {
+ return false;
+ }
+ fn(it->second);
+ return true;
+}
+
+int32_t VideoEngine::GenerateId() {
+ // XXX Something better than this (a map perhaps, or a simple boolean TArray,
+ // given the number in-use is O(1) normally!)
+ static int sId = 0;
+ return mId = sId++;
+}
+
+VideoEngine::VideoEngine(const CaptureDeviceType& aCaptureDeviceType)
+ : mId(0), mCaptureDevInfo(aCaptureDeviceType), mDeviceInfo(nullptr) {
+ LOG(("%s", __PRETTY_FUNCTION__));
+ LOG(("Creating new VideoEngine with CaptureDeviceType %s",
+ mCaptureDevInfo.TypeName()));
+}
+
+VideoEngine::~VideoEngine() {
+ MOZ_ASSERT(mCaps.empty());
+ MOZ_ASSERT(mIdMap.empty());
+}
+
+} // namespace mozilla::camera
diff --git a/dom/media/systemservices/VideoEngine.h b/dom/media/systemservices/VideoEngine.h
new file mode 100644
index 0000000000..e7a34b34e6
--- /dev/null
+++ b/dom/media/systemservices/VideoEngine.h
@@ -0,0 +1,117 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_VideoEngine_h
+#define mozilla_VideoEngine_h
+
+#include "MediaEngine.h"
+#include "VideoFrameUtils.h"
+#include "mozilla/media/MediaUtils.h"
+#include "modules/video_capture/video_capture_impl.h"
+#include "modules/video_capture/video_capture_defines.h"
+#include "modules/video_capture/video_capture_factory.h"
+#include <memory>
+#include <functional>
+
+namespace mozilla::camera {
+
+enum class CaptureDeviceType { Camera, Screen, Window, Browser };
+
+struct CaptureDeviceInfo {
+ CaptureDeviceType type;
+
+ CaptureDeviceInfo() : type(CaptureDeviceType::Camera) {}
+ explicit CaptureDeviceInfo(CaptureDeviceType t) : type(t) {}
+
+ const char* TypeName() const {
+ switch (type) {
+ case CaptureDeviceType::Camera: {
+ return "Camera";
+ }
+ case CaptureDeviceType::Screen: {
+ return "Screen";
+ }
+ case CaptureDeviceType::Window: {
+ return "Window";
+ }
+ case CaptureDeviceType::Browser: {
+ return "Browser";
+ }
+ }
+ assert(false);
+ return "UNKOWN-CaptureDeviceType!";
+ }
+};
+
+// Historically the video engine was part of webrtc
+// it was removed (and reimplemented in Talk)
+class VideoEngine {
+ private:
+ virtual ~VideoEngine();
+
+ // Base cache expiration period
+ // Note because cameras use HW plug event detection, this
+ // only applies to screen based modes.
+ static const int64_t kCacheExpiryPeriodMs = 2000;
+
+ public:
+ NS_INLINE_DECL_REFCOUNTING(VideoEngine)
+
+ static already_AddRefed<VideoEngine> Create(
+ const CaptureDeviceType& aCaptureDeviceType);
+#if defined(ANDROID)
+ static int SetAndroidObjects();
+#endif
+ // Returns a non-negative capture identifier or -1 on failure.
+ int32_t CreateVideoCapture(const char* aDeviceUniqueIdUTF8);
+
+ int ReleaseVideoCapture(const int32_t aId);
+
+ // VideoEngine is responsible for any cleanup in its modules
+ static void Delete(VideoEngine* aEngine) {}
+
+ /** Returns an existing or creates a new new DeviceInfo.
+ * Camera info is cached to prevent repeated lengthy polling for "realness"
+ * of the hardware devices. Other types of capture, e.g. screen share info,
+ * are cached for 1 second. This could be handled in a more elegant way in
+ * the future.
+ * @return on failure the shared_ptr will be null, otherwise it will contain
+ * a DeviceInfo.
+ * @see bug 1305212 https://bugzilla.mozilla.org/show_bug.cgi?id=1305212
+ */
+ std::shared_ptr<webrtc::VideoCaptureModule::DeviceInfo>
+ GetOrCreateVideoCaptureDeviceInfo();
+
+ class CaptureEntry {
+ public:
+ CaptureEntry(int32_t aCapnum,
+ rtc::scoped_refptr<webrtc::VideoCaptureModule> aCapture);
+ int32_t Capnum() const;
+ rtc::scoped_refptr<webrtc::VideoCaptureModule> VideoCapture();
+
+ private:
+ int32_t mCapnum;
+ rtc::scoped_refptr<webrtc::VideoCaptureModule> mVideoCaptureModule;
+ friend class VideoEngine;
+ };
+
+ // Returns true iff an entry for capnum exists
+ bool WithEntry(const int32_t entryCapnum,
+ const std::function<void(CaptureEntry& entry)>&& fn);
+
+ private:
+ explicit VideoEngine(const CaptureDeviceType& aCaptureDeviceType);
+ int32_t mId;
+ const CaptureDeviceInfo mCaptureDevInfo;
+ std::shared_ptr<webrtc::VideoCaptureModule::DeviceInfo> mDeviceInfo;
+ std::map<int32_t, CaptureEntry> mCaps;
+ std::map<int32_t, int32_t> mIdMap;
+ // The validity period for non-camera capture device infos`
+ webrtc::Timestamp mExpiryTime = webrtc::Timestamp::Micros(0);
+ int32_t GenerateId();
+};
+} // namespace mozilla::camera
+#endif
diff --git a/dom/media/systemservices/VideoFrameUtils.cpp b/dom/media/systemservices/VideoFrameUtils.cpp
new file mode 100644
index 0000000000..00ead56a7b
--- /dev/null
+++ b/dom/media/systemservices/VideoFrameUtils.cpp
@@ -0,0 +1,90 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "VideoFrameUtils.h"
+#include "api/video/video_frame.h"
+#include "mozilla/ShmemPool.h"
+
+namespace mozilla {
+
+uint32_t VideoFrameUtils::TotalRequiredBufferSize(
+ const webrtc::VideoFrame& aVideoFrame) {
+ auto i420 = aVideoFrame.video_frame_buffer()->ToI420();
+ auto height = i420->height();
+ size_t size = height * i420->StrideY() +
+ ((height + 1) / 2) * i420->StrideU() +
+ ((height + 1) / 2) * i420->StrideV();
+ MOZ_RELEASE_ASSERT(size < std::numeric_limits<uint32_t>::max());
+ return static_cast<uint32_t>(size);
+}
+
+void VideoFrameUtils::InitFrameBufferProperties(
+ const webrtc::VideoFrame& aVideoFrame,
+ camera::VideoFrameProperties& aDestProps) {
+ // The VideoFrameBuffer image data stored in the accompanying buffer
+ // the buffer is at least this size of larger.
+ aDestProps.bufferSize() = TotalRequiredBufferSize(aVideoFrame);
+
+ aDestProps.timeStamp() = aVideoFrame.timestamp();
+ aDestProps.ntpTimeMs() = aVideoFrame.ntp_time_ms();
+ aDestProps.renderTimeMs() = aVideoFrame.render_time_ms();
+
+ aDestProps.rotation() = aVideoFrame.rotation();
+
+ auto i420 = aVideoFrame.video_frame_buffer()->ToI420();
+ auto height = i420->height();
+ aDestProps.yAllocatedSize() = height * i420->StrideY();
+ aDestProps.uAllocatedSize() = ((height + 1) / 2) * i420->StrideU();
+ aDestProps.vAllocatedSize() = ((height + 1) / 2) * i420->StrideV();
+
+ aDestProps.width() = i420->width();
+ aDestProps.height() = height;
+
+ aDestProps.yStride() = i420->StrideY();
+ aDestProps.uStride() = i420->StrideU();
+ aDestProps.vStride() = i420->StrideV();
+}
+
+void VideoFrameUtils::CopyVideoFrameBuffers(uint8_t* aDestBuffer,
+ const size_t aDestBufferSize,
+ const webrtc::VideoFrame& aFrame) {
+ size_t aggregateSize = TotalRequiredBufferSize(aFrame);
+
+ MOZ_ASSERT(aDestBufferSize >= aggregateSize);
+ auto i420 = aFrame.video_frame_buffer()->ToI420();
+
+ // If planes are ordered YUV and contiguous then do a single copy
+ if ((i420->DataY() != nullptr) &&
+ // Check that the three planes are ordered
+ (i420->DataY() < i420->DataU()) && (i420->DataU() < i420->DataV()) &&
+ // Check that the last plane ends at firstPlane[totalsize]
+ (&i420->DataY()[aggregateSize] ==
+ &i420->DataV()[((i420->height() + 1) / 2) * i420->StrideV()])) {
+ memcpy(aDestBuffer, i420->DataY(), aggregateSize);
+ return;
+ }
+
+ // Copy each plane
+ size_t offset = 0;
+ size_t size;
+ auto height = i420->height();
+ size = height * i420->StrideY();
+ memcpy(&aDestBuffer[offset], i420->DataY(), size);
+ offset += size;
+ size = ((height + 1) / 2) * i420->StrideU();
+ memcpy(&aDestBuffer[offset], i420->DataU(), size);
+ offset += size;
+ size = ((height + 1) / 2) * i420->StrideV();
+ memcpy(&aDestBuffer[offset], i420->DataV(), size);
+}
+
+void VideoFrameUtils::CopyVideoFrameBuffers(
+ ShmemBuffer& aDestShmem, const webrtc::VideoFrame& aVideoFrame) {
+ CopyVideoFrameBuffers(aDestShmem.Get().get<uint8_t>(),
+ aDestShmem.Get().Size<uint8_t>(), aVideoFrame);
+}
+
+} // namespace mozilla
diff --git a/dom/media/systemservices/VideoFrameUtils.h b/dom/media/systemservices/VideoFrameUtils.h
new file mode 100644
index 0000000000..23ebf4f316
--- /dev/null
+++ b/dom/media/systemservices/VideoFrameUtils.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_VideoFrameUtil_h
+#define mozilla_VideoFrameUtil_h
+
+#include "mozilla/camera/PCameras.h"
+
+namespace webrtc {
+class VideoFrame;
+}
+
+namespace mozilla {
+class ShmemBuffer;
+
+// Util methods for working with webrtc::VideoFrame(s) and
+// the IPC classes that are used to deliver their contents to the
+// MediaEnginge
+
+class VideoFrameUtils {
+ public:
+ // Returns the total number of bytes necessary to copy a VideoFrame's buffer
+ // across all planes.
+ static uint32_t TotalRequiredBufferSize(const webrtc::VideoFrame& frame);
+
+ // Initializes a camera::VideoFrameProperties from a VideoFrameBuffer
+ static void InitFrameBufferProperties(
+ const webrtc::VideoFrame& aVideoFrame,
+ camera::VideoFrameProperties& aDestProperties);
+
+ // Copies the buffers out of a VideoFrameBuffer into a buffer.
+ // Attempts to make as few memcopies as possible.
+ static void CopyVideoFrameBuffers(uint8_t* aDestBuffer,
+ const size_t aDestBufferSize,
+ const webrtc::VideoFrame& aVideoFrame);
+
+ // Copies the buffers in a VideoFrameBuffer into a Shmem
+ // returns the eno from the underlying memcpy.
+ static void CopyVideoFrameBuffers(ShmemBuffer& aDestShmem,
+ const webrtc::VideoFrame& aVideoFrame);
+};
+
+} /* namespace mozilla */
+
+#endif
diff --git a/dom/media/systemservices/android_video_capture/device_info_android.cc b/dom/media/systemservices/android_video_capture/device_info_android.cc
new file mode 100644
index 0000000000..581040eb94
--- /dev/null
+++ b/dom/media/systemservices/android_video_capture/device_info_android.cc
@@ -0,0 +1,316 @@
+/*
+ * Copyright (c) 2012 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+#include "device_info_android.h"
+
+#include <algorithm>
+#include <string>
+#include <sstream>
+#include <vector>
+
+#include "rtc_base/logging.h"
+#include "modules/utility/include/helpers_android.h"
+
+#include "mozilla/jni/Utils.h"
+
+namespace webrtc {
+
+namespace videocapturemodule {
+
+// Helper for storing lists of pairs of ints. Used e.g. for resolutions & FPS
+// ranges.
+typedef std::pair<int, int> IntPair;
+typedef std::vector<IntPair> IntPairs;
+
+static std::string IntPairsToString(const IntPairs& pairs, char separator) {
+ std::stringstream stream;
+ for (size_t i = 0; i < pairs.size(); ++i) {
+ if (i > 0) {
+ stream << ", ";
+ }
+ stream << "(" << pairs[i].first << separator << pairs[i].second << ")";
+ }
+ return stream.str();
+}
+
+struct AndroidCameraInfo {
+ std::string name;
+ bool front_facing;
+ int orientation;
+ IntPairs resolutions; // Pairs are: (width,height).
+ // Pairs are (min,max) in units of FPS*1000 ("milli-frame-per-second").
+ IntPairs mfpsRanges;
+
+ std::string ToString() {
+ std::stringstream stream;
+ stream << "Name: [" << name << "], MFPS ranges: ["
+ << IntPairsToString(mfpsRanges, ':')
+ << "], front_facing: " << front_facing
+ << ", orientation: " << orientation << ", resolutions: ["
+ << IntPairsToString(resolutions, 'x') << "]";
+ return stream.str();
+ }
+};
+
+// Camera info; populated during DeviceInfoAndroid::Refresh()
+static std::vector<AndroidCameraInfo>* g_camera_info = NULL;
+
+static JavaVM* g_jvm_dev_info = NULL;
+
+// Set |*index| to the index of |name| in g_camera_info or return false if no
+// match found.
+static bool FindCameraIndexByName(const std::string& name, size_t* index) {
+ for (size_t i = 0; i < g_camera_info->size(); ++i) {
+ if (g_camera_info->at(i).name == name) {
+ *index = i;
+ return true;
+ }
+ }
+ return false;
+}
+
+// Returns a pointer to the named member of g_camera_info, or NULL if no match
+// is found.
+static AndroidCameraInfo* FindCameraInfoByName(const std::string& name) {
+ size_t index = 0;
+ if (FindCameraIndexByName(name, &index)) {
+ return &g_camera_info->at(index);
+ }
+ return NULL;
+}
+
+// static
+void DeviceInfoAndroid::Initialize(JavaVM* javaVM) {
+ // TODO(henrike): this "if" would make a lot more sense as an assert, but
+ // Java_org_webrtc_videoengineapp_ViEAndroidJavaAPI_GetVideoEngine() and
+ // Java_org_webrtc_videoengineapp_ViEAndroidJavaAPI_Terminate() conspire to
+ // prevent this. Once that code is made to only
+ // VideoEngine::SetAndroidObjects() once per process, this can turn into an
+ // assert.
+ if (g_camera_info) {
+ return;
+ }
+
+ g_jvm_dev_info = javaVM;
+ BuildDeviceList();
+}
+
+void DeviceInfoAndroid::BuildDeviceList() {
+ if (!g_jvm_dev_info) {
+ return;
+ }
+
+ AttachThreadScoped ats(g_jvm_dev_info);
+ JNIEnv* jni = ats.env();
+
+ g_camera_info = new std::vector<AndroidCameraInfo>();
+ jclass j_info_class = mozilla::jni::GetClassRef(
+ jni, "org/webrtc/videoengine/VideoCaptureDeviceInfoAndroid");
+ jclass j_cap_class = mozilla::jni::GetClassRef(
+ jni, "org/webrtc/videoengine/CaptureCapabilityAndroid");
+ assert(j_info_class);
+ jmethodID j_get_device_info = jni->GetStaticMethodID(
+ j_info_class, "getDeviceInfo",
+ "()[Lorg/webrtc/videoengine/CaptureCapabilityAndroid;");
+ jarray j_camera_caps = static_cast<jarray>(
+ jni->CallStaticObjectMethod(j_info_class, j_get_device_info));
+ if (jni->ExceptionCheck()) {
+ jni->ExceptionClear();
+ RTC_LOG(LS_INFO) << __FUNCTION__ << ": Failed to get camera capabilities.";
+ return;
+ }
+ if (j_camera_caps == nullptr) {
+ RTC_LOG(LS_INFO) << __FUNCTION__ << ": Failed to get camera capabilities.";
+ return;
+ }
+
+ const jsize capLength = jni->GetArrayLength(j_camera_caps);
+
+ jfieldID widthField = jni->GetFieldID(j_cap_class, "width", "[I");
+ jfieldID heightField = jni->GetFieldID(j_cap_class, "height", "[I");
+ jfieldID maxFpsField = jni->GetFieldID(j_cap_class, "maxMilliFPS", "I");
+ jfieldID minFpsField = jni->GetFieldID(j_cap_class, "minMilliFPS", "I");
+ jfieldID orientationField = jni->GetFieldID(j_cap_class, "orientation", "I");
+ jfieldID frontFacingField = jni->GetFieldID(j_cap_class, "frontFacing", "Z");
+ jfieldID nameField =
+ jni->GetFieldID(j_cap_class, "name", "Ljava/lang/String;");
+ if (widthField == NULL || heightField == NULL || maxFpsField == NULL ||
+ minFpsField == NULL || orientationField == NULL ||
+ frontFacingField == NULL || nameField == NULL) {
+ RTC_LOG(LS_INFO) << __FUNCTION__ << ": Failed to get field Id.";
+ return;
+ }
+
+ for (jsize i = 0; i < capLength; i++) {
+ jobject capabilityElement =
+ jni->GetObjectArrayElement((jobjectArray)j_camera_caps, i);
+
+ AndroidCameraInfo info;
+ jstring camName =
+ static_cast<jstring>(jni->GetObjectField(capabilityElement, nameField));
+ const char* camChars = jni->GetStringUTFChars(camName, nullptr);
+ info.name = std::string(camChars);
+ jni->ReleaseStringUTFChars(camName, camChars);
+
+ info.orientation = jni->GetIntField(capabilityElement, orientationField);
+ info.front_facing =
+ jni->GetBooleanField(capabilityElement, frontFacingField);
+ jint min_mfps = jni->GetIntField(capabilityElement, minFpsField);
+ jint max_mfps = jni->GetIntField(capabilityElement, maxFpsField);
+
+ jintArray widthResArray = static_cast<jintArray>(
+ jni->GetObjectField(capabilityElement, widthField));
+ jintArray heightResArray = static_cast<jintArray>(
+ jni->GetObjectField(capabilityElement, heightField));
+
+ const jsize numRes = jni->GetArrayLength(widthResArray);
+
+ jint* widths = jni->GetIntArrayElements(widthResArray, nullptr);
+ jint* heights = jni->GetIntArrayElements(heightResArray, nullptr);
+
+ for (jsize j = 0; j < numRes; ++j) {
+ info.resolutions.push_back(std::make_pair(widths[j], heights[j]));
+ }
+
+ info.mfpsRanges.push_back(std::make_pair(min_mfps, max_mfps));
+ g_camera_info->push_back(info);
+
+ jni->ReleaseIntArrayElements(widthResArray, widths, JNI_ABORT);
+ jni->ReleaseIntArrayElements(heightResArray, heights, JNI_ABORT);
+ }
+
+ jni->DeleteLocalRef(j_info_class);
+ jni->DeleteLocalRef(j_cap_class);
+}
+
+void DeviceInfoAndroid::DeInitialize() {
+ if (g_camera_info) {
+ delete g_camera_info;
+ g_camera_info = NULL;
+ }
+}
+
+int32_t DeviceInfoAndroid::Refresh() {
+ if (!g_camera_info || g_camera_info->size() == 0) {
+ DeviceInfoAndroid::BuildDeviceList();
+#ifdef DEBUG
+ int frontFacingIndex = -1;
+ for (uint32_t i = 0; i < g_camera_info->size(); i++) {
+ if (g_camera_info->at(i).front_facing) {
+ frontFacingIndex = i;
+ }
+ }
+ // Either there is a front-facing camera, and it's first in the list, or
+ // there is no front-facing camera.
+ MOZ_ASSERT(frontFacingIndex == 0 || frontFacingIndex == -1);
+#endif
+ }
+ return 0;
+}
+
+VideoCaptureModule::DeviceInfo* VideoCaptureImpl::CreateDeviceInfo() {
+ return new videocapturemodule::DeviceInfoAndroid();
+}
+
+DeviceInfoAndroid::DeviceInfoAndroid() : DeviceInfoImpl() {}
+
+DeviceInfoAndroid::~DeviceInfoAndroid() {}
+
+bool DeviceInfoAndroid::FindCameraIndex(const char* deviceUniqueIdUTF8,
+ size_t* index) {
+ return FindCameraIndexByName(deviceUniqueIdUTF8, index);
+}
+
+int32_t DeviceInfoAndroid::Init() { return 0; }
+
+uint32_t DeviceInfoAndroid::NumberOfDevices() {
+ Refresh();
+ return g_camera_info->size();
+}
+
+int32_t DeviceInfoAndroid::GetDeviceName(
+ uint32_t deviceNumber, char* deviceNameUTF8, uint32_t deviceNameLength,
+ char* deviceUniqueIdUTF8, uint32_t deviceUniqueIdUTF8Length,
+ char* /*productUniqueIdUTF8*/, uint32_t /*productUniqueIdUTF8Length*/,
+ pid_t* /*pid*/) {
+ if (deviceNumber >= g_camera_info->size()) {
+ return -1;
+ }
+ const AndroidCameraInfo& info = g_camera_info->at(deviceNumber);
+ if (info.name.length() + 1 > deviceNameLength ||
+ info.name.length() + 1 > deviceUniqueIdUTF8Length) {
+ return -1;
+ }
+ memcpy(deviceNameUTF8, info.name.c_str(), info.name.length() + 1);
+ memcpy(deviceUniqueIdUTF8, info.name.c_str(), info.name.length() + 1);
+ return 0;
+}
+
+int32_t DeviceInfoAndroid::CreateCapabilityMap(const char* deviceUniqueIdUTF8) {
+ _captureCapabilities.clear();
+ const AndroidCameraInfo* info = FindCameraInfoByName(deviceUniqueIdUTF8);
+ if (info == NULL) {
+ return -1;
+ }
+
+ for (size_t i = 0; i < info->resolutions.size(); ++i) {
+ for (size_t j = 0; j < info->mfpsRanges.size(); ++j) {
+ const IntPair& size = info->resolutions[i];
+ const IntPair& mfpsRange = info->mfpsRanges[j];
+ VideoCaptureCapability cap;
+ cap.width = size.first;
+ cap.height = size.second;
+ cap.maxFPS = mfpsRange.second / 1000;
+ cap.videoType = VideoType::kNV21;
+ _captureCapabilities.push_back(cap);
+ }
+ }
+ return _captureCapabilities.size();
+}
+
+int32_t DeviceInfoAndroid::GetOrientation(const char* deviceUniqueIdUTF8,
+ VideoRotation& orientation) {
+ const AndroidCameraInfo* info = FindCameraInfoByName(deviceUniqueIdUTF8);
+ if (info == NULL || VideoCaptureImpl::RotationFromDegrees(
+ info->orientation, &orientation) != 0) {
+ return -1;
+ }
+ return 0;
+}
+
+void DeviceInfoAndroid::GetMFpsRange(const char* deviceUniqueIdUTF8,
+ int max_fps_to_match, int* min_mfps,
+ int* max_mfps) {
+ const AndroidCameraInfo* info = FindCameraInfoByName(deviceUniqueIdUTF8);
+ if (info == NULL) {
+ return;
+ }
+ int desired_mfps = max_fps_to_match * 1000;
+ int best_diff_mfps = 0;
+ RTC_LOG(LS_INFO) << "Search for best target mfps " << desired_mfps;
+ // Search for best fps range with preference shifted to constant fps modes.
+ for (size_t i = 0; i < info->mfpsRanges.size(); ++i) {
+ int diff_mfps =
+ abs(info->mfpsRanges[i].first - desired_mfps) +
+ abs(info->mfpsRanges[i].second - desired_mfps) +
+ (info->mfpsRanges[i].second - info->mfpsRanges[i].first) / 2;
+ RTC_LOG(LS_INFO) << "Fps range " << info->mfpsRanges[i].first << ":"
+ << info->mfpsRanges[i].second
+ << ". Distance: " << diff_mfps;
+ if (i == 0 || diff_mfps < best_diff_mfps) {
+ best_diff_mfps = diff_mfps;
+ *min_mfps = info->mfpsRanges[i].first;
+ *max_mfps = info->mfpsRanges[i].second;
+ }
+ }
+}
+
+} // namespace videocapturemodule
+} // namespace webrtc
diff --git a/dom/media/systemservices/android_video_capture/device_info_android.h b/dom/media/systemservices/android_video_capture/device_info_android.h
new file mode 100644
index 0000000000..ac88b2b8ba
--- /dev/null
+++ b/dom/media/systemservices/android_video_capture/device_info_android.h
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2012 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+#ifndef WEBRTC_MODULES_VIDEO_CAPTURE_MAIN_SOURCE_ANDROID_DEVICE_INFO_ANDROID_H_
+#define WEBRTC_MODULES_VIDEO_CAPTURE_MAIN_SOURCE_ANDROID_DEVICE_INFO_ANDROID_H_
+
+#include <jni.h>
+
+#include "modules/video_capture/device_info_impl.h"
+#include "modules/video_capture/video_capture_impl.h"
+
+#define AndroidJavaCaptureDeviceInfoClass \
+ "org/webrtc/videoengine/VideoCaptureDeviceInfoAndroid"
+#define AndroidJavaCaptureCapabilityClass \
+ "org/webrtc/videoengine/CaptureCapabilityAndroid"
+
+namespace webrtc {
+namespace videocapturemodule {
+
+class DeviceInfoAndroid : public DeviceInfoImpl {
+ public:
+ static void Initialize(JavaVM* javaVM);
+ static void DeInitialize();
+
+ DeviceInfoAndroid();
+ virtual ~DeviceInfoAndroid();
+
+ // Set |*index| to the index of the camera matching |deviceUniqueIdUTF8|, or
+ // return false if no match.
+ bool FindCameraIndex(const char* deviceUniqueIdUTF8, size_t* index);
+
+ virtual int32_t Init();
+ virtual uint32_t NumberOfDevices();
+ virtual int32_t Refresh();
+ virtual int32_t GetDeviceName(uint32_t deviceNumber, char* deviceNameUTF8,
+ uint32_t deviceNameLength,
+ char* deviceUniqueIdUTF8,
+ uint32_t deviceUniqueIdUTF8Length,
+ char* productUniqueIdUTF8 = 0,
+ uint32_t productUniqueIdUTF8Length = 0,
+ pid_t* pid = 0);
+ virtual int32_t CreateCapabilityMap(const char* deviceUniqueIdUTF8)
+ RTC_EXCLUSIVE_LOCKS_REQUIRED(_apiLock);
+
+ virtual int32_t DisplayCaptureSettingsDialogBox(
+ const char* /*deviceUniqueIdUTF8*/, const char* /*dialogTitleUTF8*/,
+ void* /*parentWindow*/, uint32_t /*positionX*/, uint32_t /*positionY*/) {
+ return -1;
+ }
+ virtual int32_t GetOrientation(const char* deviceUniqueIdUTF8,
+ VideoRotation& orientation);
+
+ // Populate |min_mfps| and |max_mfps| with the closest supported range of the
+ // device to |max_fps_to_match|.
+ void GetMFpsRange(const char* deviceUniqueIdUTF8, int max_fps_to_match,
+ int* min_mfps, int* max_mfps);
+
+ private:
+ enum { kExpectedCaptureDelay = 190 };
+ static void BuildDeviceList();
+};
+
+} // namespace videocapturemodule
+} // namespace webrtc
+
+#endif // WEBRTC_MODULES_VIDEO_CAPTURE_MAIN_SOURCE_ANDROID_DEVICE_INFO_ANDROID_H_
diff --git a/dom/media/systemservices/android_video_capture/java/src/org/webrtc/videoengine/CaptureCapabilityAndroid.java b/dom/media/systemservices/android_video_capture/java/src/org/webrtc/videoengine/CaptureCapabilityAndroid.java
new file mode 100644
index 0000000000..305fc74804
--- /dev/null
+++ b/dom/media/systemservices/android_video_capture/java/src/org/webrtc/videoengine/CaptureCapabilityAndroid.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (c) 2012 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+package org.webrtc.videoengine;
+
+import org.mozilla.gecko.annotation.WebRTCJNITarget;
+
+@WebRTCJNITarget
+public class CaptureCapabilityAndroid {
+ public String name;
+ public int width[];
+ public int height[];
+ public int minMilliFPS;
+ public int maxMilliFPS;
+ public boolean frontFacing;
+ public boolean infrared;
+ public int orientation;
+}
diff --git a/dom/media/systemservices/android_video_capture/java/src/org/webrtc/videoengine/VideoCaptureAndroid.java b/dom/media/systemservices/android_video_capture/java/src/org/webrtc/videoengine/VideoCaptureAndroid.java
new file mode 100644
index 0000000000..cc54009a7b
--- /dev/null
+++ b/dom/media/systemservices/android_video_capture/java/src/org/webrtc/videoengine/VideoCaptureAndroid.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (c) 2012 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+package org.webrtc.videoengine;
+
+import java.io.IOException;
+import java.util.List;
+
+import android.content.Context;
+import android.util.Log;
+import android.view.Surface;
+import android.view.WindowManager;
+
+import java.util.concurrent.CountDownLatch;
+
+import org.mozilla.gecko.annotation.WebRTCJNITarget;
+
+import org.webrtc.CameraEnumerator;
+import org.webrtc.Camera1Enumerator;
+import org.webrtc.Camera2Enumerator;
+import org.webrtc.CameraVideoCapturer;
+import org.webrtc.CapturerObserver;
+import org.webrtc.EglBase;
+import org.webrtc.SurfaceTextureHelper;
+import org.webrtc.VideoFrame;
+import org.webrtc.VideoFrame.I420Buffer;
+
+public class VideoCaptureAndroid implements CameraVideoCapturer.CameraEventsHandler, CapturerObserver {
+ private final static String TAG = "WEBRTC-JC";
+
+ private final String deviceName;
+ private volatile long native_capturer; // |VideoCaptureAndroid*| in C++.
+ private Context context;
+ private CameraVideoCapturer cameraVideoCapturer;
+ private EglBase eglBase;
+ private SurfaceTextureHelper surfaceTextureHelper;
+
+ // This class is recreated everytime we start/stop capture, so we
+ // can safely create the CountDownLatches here.
+ private final CountDownLatch capturerStarted = new CountDownLatch(1);
+ private boolean capturerStartedSucceeded = false;
+ private final CountDownLatch capturerStopped = new CountDownLatch(1);
+
+ @WebRTCJNITarget
+ public VideoCaptureAndroid(String deviceName) {
+ // Remove the camera facing information from the name.
+ String[] parts = deviceName.split("Facing (front|back):");
+ if (parts.length == 2) {
+ this.deviceName = parts[1].replace(" (infrared)", "");
+ } else {
+ Log.e(TAG, "VideoCaptureAndroid: Expected facing mode as part of name: " + deviceName);
+ this.deviceName = deviceName;
+ }
+ this.context = GetContext();
+
+ CameraEnumerator enumerator;
+ if (Camera2Enumerator.isSupported(context)) {
+ enumerator = new Camera2Enumerator(context);
+ } else {
+ enumerator = new Camera1Enumerator();
+ }
+ try {
+ cameraVideoCapturer = enumerator.createCapturer(this.deviceName, this);
+ eglBase = EglBase.create();
+ surfaceTextureHelper = SurfaceTextureHelper.create("VideoCaptureAndroidSurfaceTextureHelper", eglBase.getEglBaseContext());
+ cameraVideoCapturer.initialize(surfaceTextureHelper, context, this);
+ } catch (java.lang.IllegalArgumentException e) {
+ Log.e(TAG, "VideoCaptureAndroid: Exception while creating capturer: " + e);
+ }
+ }
+
+ // Return the global application context.
+ @WebRTCJNITarget
+ private static native Context GetContext();
+
+ // Called by native code. Returns true if capturer is started.
+ //
+ // Note that this actually opens the camera, and Camera callbacks run on the
+ // thread that calls open(), so this is done on the CameraThread. Since ViE
+ // API needs a synchronous success return value we wait for the result.
+ @WebRTCJNITarget
+ private synchronized boolean startCapture(
+ final int width, final int height,
+ final int min_mfps, final int max_mfps,
+ long native_capturer) {
+ Log.d(TAG, "startCapture: " + width + "x" + height + "@" +
+ min_mfps + ":" + max_mfps);
+
+ if (cameraVideoCapturer == null) {
+ return false;
+ }
+
+ cameraVideoCapturer.startCapture(width, height, max_mfps);
+ try {
+ capturerStarted.await();
+ } catch (InterruptedException e) {
+ return false;
+ }
+ if (capturerStartedSucceeded) {
+ this.native_capturer = native_capturer;
+ }
+ return capturerStartedSucceeded;
+ }
+
+ // Called by native code. Returns true when camera is known to be stopped.
+ @WebRTCJNITarget
+ private synchronized boolean stopCapture() {
+ Log.d(TAG, "stopCapture");
+ if (cameraVideoCapturer == null) {
+ return false;
+ }
+
+ native_capturer = 0;
+ try {
+ cameraVideoCapturer.stopCapture();
+ capturerStopped.await();
+ } catch (InterruptedException e) {
+ return false;
+ }
+ Log.d(TAG, "stopCapture done");
+ return true;
+ }
+
+ @WebRTCJNITarget
+ private int getDeviceOrientation() {
+ int orientation = 0;
+ if (context != null) {
+ WindowManager wm = (WindowManager) context.getSystemService(
+ Context.WINDOW_SERVICE);
+ switch(wm.getDefaultDisplay().getRotation()) {
+ case Surface.ROTATION_90:
+ orientation = 90;
+ break;
+ case Surface.ROTATION_180:
+ orientation = 180;
+ break;
+ case Surface.ROTATION_270:
+ orientation = 270;
+ break;
+ case Surface.ROTATION_0:
+ default:
+ orientation = 0;
+ break;
+ }
+ }
+ return orientation;
+ }
+
+ @WebRTCJNITarget
+ private native void ProvideCameraFrame(
+ int width, int height,
+ java.nio.ByteBuffer dataY, int strideY,
+ java.nio.ByteBuffer dataU, int strideU,
+ java.nio.ByteBuffer dataV, int strideV,
+ int rotation, long timeStamp, long captureObject);
+
+ //
+ // CameraVideoCapturer.CameraEventsHandler interface
+ //
+
+ // Camera error handler - invoked when camera can not be opened
+ // or any camera exception happens on camera thread.
+ public void onCameraError(String errorDescription) {}
+
+ // Called when camera is disconnected.
+ public void onCameraDisconnected() {}
+
+ // Invoked when camera stops receiving frames.
+ public void onCameraFreezed(String errorDescription) {}
+
+ // Callback invoked when camera is opening.
+ public void onCameraOpening(String cameraName) {}
+
+ // Callback invoked when first camera frame is available after camera is started.
+ public void onFirstFrameAvailable() {}
+
+ // Callback invoked when camera is closed.
+ public void onCameraClosed() {}
+
+ //
+ // CapturerObserver interface
+ //
+
+ // Notify if the capturer have been started successfully or not.
+ public void onCapturerStarted(boolean success) {
+ capturerStartedSucceeded = success;
+ capturerStarted.countDown();
+ }
+
+ // Notify that the capturer has been stopped.
+ public void onCapturerStopped() {
+ capturerStopped.countDown();
+ }
+
+ // Delivers a captured frame.
+ public void onFrameCaptured(VideoFrame frame) {
+ if (native_capturer != 0) {
+ I420Buffer i420Buffer = frame.getBuffer().toI420();
+ ProvideCameraFrame(i420Buffer.getWidth(), i420Buffer.getHeight(),
+ i420Buffer.getDataY(), i420Buffer.getStrideY(),
+ i420Buffer.getDataU(), i420Buffer.getStrideU(),
+ i420Buffer.getDataV(), i420Buffer.getStrideV(),
+ frame.getRotation(),
+ frame.getTimestampNs() / 1000000, native_capturer);
+
+ i420Buffer.release();
+ }
+ }
+}
diff --git a/dom/media/systemservices/android_video_capture/java/src/org/webrtc/videoengine/VideoCaptureDeviceInfoAndroid.java b/dom/media/systemservices/android_video_capture/java/src/org/webrtc/videoengine/VideoCaptureDeviceInfoAndroid.java
new file mode 100644
index 0000000000..8ad8453955
--- /dev/null
+++ b/dom/media/systemservices/android_video_capture/java/src/org/webrtc/videoengine/VideoCaptureDeviceInfoAndroid.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (c) 2012 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+package org.webrtc.videoengine;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.Context;
+import android.util.Log;
+
+import org.mozilla.gecko.GeckoAppShell;
+import org.mozilla.gecko.annotation.WebRTCJNITarget;
+
+import org.webrtc.CameraEnumerator;
+import org.webrtc.CameraEnumerationAndroid.CaptureFormat;
+import org.webrtc.Camera1Enumerator;
+import org.webrtc.Camera2Enumerator;
+
+public class VideoCaptureDeviceInfoAndroid {
+ private final static String TAG = "WEBRTC-JC";
+
+ // Returns information about all cameras on the device.
+ // Since this reflects static information about the hardware present, there is
+ // no need to call this function more than once in a single process. It is
+ // marked "private" as it is only called by native code.
+ @WebRTCJNITarget
+ private static CaptureCapabilityAndroid[] getDeviceInfo() {
+ final Context context = GeckoAppShell.getApplicationContext();
+
+ if (Camera2Enumerator.isSupported(context)) {
+ return createDeviceList(new Camera2Enumerator(context));
+ } else {
+ return createDeviceList(new Camera1Enumerator());
+ }
+ }
+
+ private static CaptureCapabilityAndroid[] createDeviceList(CameraEnumerator enumerator) {
+
+ ArrayList<CaptureCapabilityAndroid> allDevices = new ArrayList<CaptureCapabilityAndroid>();
+ ArrayList<CaptureCapabilityAndroid> IRDevices = new ArrayList<CaptureCapabilityAndroid>();
+
+ for (String camera: enumerator.getDeviceNames()) {
+ List<CaptureFormat> formats = enumerator.getSupportedFormats(camera);
+ int numFormats = formats.size();
+ if (numFormats <= 0) {
+ continue;
+ }
+
+ CaptureCapabilityAndroid device = new CaptureCapabilityAndroid();
+
+ // The only way to plumb through whether the device is front facing
+ // or not is by the name, but the name we receive depends upon the
+ // camera API in use. For the Camera1 API, this information is
+ // already present, but that is not the case when using Camera2.
+ // Later on, we look up the camera by name, so we have to use a
+ // format this is easy to undo. Ideally, libwebrtc would expose
+ // camera facing in VideoCaptureCapability and none of this would be
+ // necessary.
+ device.name = "Facing " + (enumerator.isFrontFacing(camera) ? "front" : "back") + ":" + camera;
+
+
+ boolean ir = enumerator.isInfrared(camera);
+ device.infrared = ir;
+ if (ir) {
+ device.name += " (infrared)";
+ }
+
+ // This isn't part of the new API, but we don't call
+ // GetDeviceOrientation() anywhere, so this value is unused.
+ device.orientation = 0;
+
+ device.width = new int[numFormats];
+ device.height = new int[numFormats];
+ device.minMilliFPS = formats.get(0).framerate.min;
+ device.maxMilliFPS = formats.get(0).framerate.max;
+ int i = 0;
+ for (CaptureFormat format: formats) {
+ device.width[i] = format.width;
+ device.height[i] = format.height;
+ if (format.framerate.min < device.minMilliFPS) {
+ device.minMilliFPS = format.framerate.min;
+ }
+ if (format.framerate.max > device.maxMilliFPS) {
+ device.maxMilliFPS = format.framerate.max;
+ }
+ i++;
+ }
+ device.frontFacing = enumerator.isFrontFacing(camera);
+ // Infrared devices always last (but front facing ones before
+ // non-front-facing ones), front-facing non IR first, other in
+ // the middle.
+ if (!device.infrared) {
+ if (device.frontFacing) {
+ allDevices.add(0, device);
+ } else {
+ allDevices.add(device);
+ }
+ } else {
+ if (device.frontFacing) {
+ IRDevices.add(0, device);
+ } else {
+ IRDevices.add(device);
+ }
+ }
+ }
+
+ allDevices.addAll(IRDevices);
+
+ return allDevices.toArray(new CaptureCapabilityAndroid[0]);
+ }
+}
diff --git a/dom/media/systemservices/android_video_capture/video_capture_android.cc b/dom/media/systemservices/android_video_capture/video_capture_android.cc
new file mode 100644
index 0000000000..7c9cd72ccb
--- /dev/null
+++ b/dom/media/systemservices/android_video_capture/video_capture_android.cc
@@ -0,0 +1,270 @@
+/*
+ * Copyright (c) 2012 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+#include "video_capture_android.h"
+
+#include "device_info_android.h"
+#include "modules/utility/include/helpers_android.h"
+#include "rtc_base/logging.h"
+#include "rtc_base/ref_counted_object.h"
+#include "rtc_base/time_utils.h"
+
+#include "AndroidBridge.h"
+
+static JavaVM* g_jvm_capture = NULL;
+static jclass g_java_capturer_class = NULL; // VideoCaptureAndroid.class.
+static jobject g_context = NULL; // Owned android.content.Context.
+
+namespace webrtc {
+
+jobject JniCommon_allocateNativeByteBuffer(JNIEnv* env, jclass, jint size) {
+ void* new_data = ::operator new(size);
+ jobject byte_buffer = env->NewDirectByteBuffer(new_data, size);
+ return byte_buffer;
+}
+
+void JniCommon_freeNativeByteBuffer(JNIEnv* env, jclass, jobject byte_buffer) {
+ void* data = env->GetDirectBufferAddress(byte_buffer);
+ ::operator delete(data);
+}
+
+// Called by Java to get the global application context.
+jobject JNICALL GetContext(JNIEnv* env, jclass) {
+ assert(g_context);
+ return g_context;
+}
+
+// Called by Java when the camera has a new frame to deliver.
+void JNICALL ProvideCameraFrame(JNIEnv* env, jobject, jint width, jint height,
+ jobject javaDataY, jint strideY,
+ jobject javaDataU, jint strideU,
+ jobject javaDataV, jint strideV, jint rotation,
+ jlong timeStamp, jlong context) {
+ if (!context) {
+ return;
+ }
+
+ webrtc::videocapturemodule::VideoCaptureAndroid* captureModule =
+ reinterpret_cast<webrtc::videocapturemodule::VideoCaptureAndroid*>(
+ context);
+ uint8_t* dataY =
+ reinterpret_cast<uint8_t*>(env->GetDirectBufferAddress(javaDataY));
+ uint8_t* dataU =
+ reinterpret_cast<uint8_t*>(env->GetDirectBufferAddress(javaDataU));
+ uint8_t* dataV =
+ reinterpret_cast<uint8_t*>(env->GetDirectBufferAddress(javaDataV));
+
+ rtc::scoped_refptr<I420Buffer> i420Buffer = I420Buffer::Copy(
+ width, height, dataY, strideY, dataU, strideU, dataV, strideV);
+
+ captureModule->OnIncomingFrame(i420Buffer, rotation, timeStamp);
+}
+
+int32_t SetCaptureAndroidVM(JavaVM* javaVM) {
+ if (g_java_capturer_class) {
+ return 0;
+ }
+
+ if (javaVM) {
+ assert(!g_jvm_capture);
+ g_jvm_capture = javaVM;
+ AttachThreadScoped ats(g_jvm_capture);
+
+ g_context = mozilla::AndroidBridge::Bridge()->GetGlobalContextRef();
+
+ videocapturemodule::DeviceInfoAndroid::Initialize(g_jvm_capture);
+
+ {
+ jclass clsRef = mozilla::jni::GetClassRef(
+ ats.env(), "org/webrtc/videoengine/VideoCaptureAndroid");
+ g_java_capturer_class =
+ static_cast<jclass>(ats.env()->NewGlobalRef(clsRef));
+ ats.env()->DeleteLocalRef(clsRef);
+ assert(g_java_capturer_class);
+
+ JNINativeMethod native_methods[] = {
+ {"GetContext", "()Landroid/content/Context;",
+ reinterpret_cast<void*>(&GetContext)},
+ {"ProvideCameraFrame",
+ "(IILjava/nio/ByteBuffer;ILjava/nio/ByteBuffer;ILjava/nio/"
+ "ByteBuffer;IIJJ)V",
+ reinterpret_cast<void*>(&ProvideCameraFrame)}};
+ if (ats.env()->RegisterNatives(g_java_capturer_class, native_methods,
+ 2) != 0)
+ assert(false);
+ }
+
+ {
+ jclass clsRef =
+ mozilla::jni::GetClassRef(ats.env(), "org/webrtc/JniCommon");
+
+ JNINativeMethod native_methods[] = {
+ {"nativeAllocateByteBuffer", "(I)Ljava/nio/ByteBuffer;",
+ reinterpret_cast<void*>(&JniCommon_allocateNativeByteBuffer)},
+ {"nativeFreeByteBuffer", "(Ljava/nio/ByteBuffer;)V",
+ reinterpret_cast<void*>(&JniCommon_freeNativeByteBuffer)}};
+ if (ats.env()->RegisterNatives(clsRef, native_methods, 2) != 0)
+ assert(false);
+ }
+ } else {
+ if (g_jvm_capture) {
+ AttachThreadScoped ats(g_jvm_capture);
+ ats.env()->UnregisterNatives(g_java_capturer_class);
+ ats.env()->DeleteGlobalRef(g_java_capturer_class);
+ g_java_capturer_class = NULL;
+ g_context = NULL;
+ videocapturemodule::DeviceInfoAndroid::DeInitialize();
+ g_jvm_capture = NULL;
+ }
+ }
+
+ return 0;
+}
+
+namespace videocapturemodule {
+
+rtc::scoped_refptr<VideoCaptureModule> VideoCaptureImpl::Create(
+ const char* deviceUniqueIdUTF8) {
+ rtc::scoped_refptr<VideoCaptureAndroid> implementation(
+ new rtc::RefCountedObject<VideoCaptureAndroid>());
+ if (implementation->Init(deviceUniqueIdUTF8) != 0) {
+ implementation = nullptr;
+ }
+ return implementation;
+}
+
+void VideoCaptureAndroid::OnIncomingFrame(rtc::scoped_refptr<I420Buffer> buffer,
+ int32_t degrees,
+ int64_t captureTime) {
+ MutexLock lock(&api_lock_);
+
+ VideoRotation rotation =
+ (degrees <= 45 || degrees > 315) ? kVideoRotation_0
+ : (degrees > 45 && degrees <= 135) ? kVideoRotation_90
+ : (degrees > 135 && degrees <= 225) ? kVideoRotation_180
+ : (degrees > 225 && degrees <= 315) ? kVideoRotation_270
+ : kVideoRotation_0; // Impossible.
+
+ // Historically, we have ignored captureTime. Why?
+ VideoFrame captureFrame(I420Buffer::Rotate(*buffer, rotation), 0,
+ rtc::TimeMillis(), rotation);
+
+ DeliverCapturedFrame(captureFrame);
+}
+
+VideoCaptureAndroid::VideoCaptureAndroid()
+ : VideoCaptureImpl(),
+ _deviceInfo(),
+ _jCapturer(NULL),
+ _captureStarted(false) {}
+
+int32_t VideoCaptureAndroid::Init(const char* deviceUniqueIdUTF8) {
+ const int nameLength = strlen(deviceUniqueIdUTF8);
+ if (nameLength >= kVideoCaptureUniqueNameLength) return -1;
+
+ // Store the device name
+ RTC_LOG(LS_INFO) << "VideoCaptureAndroid::Init: " << deviceUniqueIdUTF8;
+ _deviceUniqueId = new char[nameLength + 1];
+ memcpy(_deviceUniqueId, deviceUniqueIdUTF8, nameLength + 1);
+
+ AttachThreadScoped ats(g_jvm_capture);
+ JNIEnv* env = ats.env();
+ jmethodID ctor = env->GetMethodID(g_java_capturer_class, "<init>",
+ "(Ljava/lang/String;)V");
+ assert(ctor);
+ jstring j_deviceName = env->NewStringUTF(_deviceUniqueId);
+ _jCapturer = env->NewGlobalRef(
+ env->NewObject(g_java_capturer_class, ctor, j_deviceName));
+ assert(_jCapturer);
+ return 0;
+}
+
+VideoCaptureAndroid::~VideoCaptureAndroid() {
+ // Ensure Java camera is released even if our caller didn't explicitly Stop.
+ if (_captureStarted) StopCapture();
+ AttachThreadScoped ats(g_jvm_capture);
+ JNIEnv* env = ats.env();
+ env->DeleteGlobalRef(_jCapturer);
+}
+
+int32_t VideoCaptureAndroid::StartCapture(
+ const VideoCaptureCapability& capability) {
+ AttachThreadScoped ats(g_jvm_capture);
+ JNIEnv* env = ats.env();
+ int width = 0;
+ int height = 0;
+ int min_mfps = 0;
+ int max_mfps = 0;
+ {
+ MutexLock lock(&api_lock_);
+
+ if (_deviceInfo.GetBestMatchedCapability(_deviceUniqueId, capability,
+ _captureCapability) < 0) {
+ RTC_LOG(LS_ERROR) << __FUNCTION__
+ << "s: GetBestMatchedCapability failed: "
+ << capability.width << "x" << capability.height;
+ return -1;
+ }
+
+ width = _captureCapability.width;
+ height = _captureCapability.height;
+ _deviceInfo.GetMFpsRange(_deviceUniqueId, _captureCapability.maxFPS,
+ &min_mfps, &max_mfps);
+
+ // Exit critical section to avoid blocking camera thread inside
+ // onIncomingFrame() call.
+ }
+
+ jmethodID j_start =
+ env->GetMethodID(g_java_capturer_class, "startCapture", "(IIIIJ)Z");
+ assert(j_start);
+ jlong j_this = reinterpret_cast<intptr_t>(this);
+ bool started = env->CallBooleanMethod(_jCapturer, j_start, width, height,
+ min_mfps, max_mfps, j_this);
+ if (started) {
+ MutexLock lock(&api_lock_);
+ _requestedCapability = capability;
+ _captureStarted = true;
+ }
+ return started ? 0 : -1;
+}
+
+int32_t VideoCaptureAndroid::StopCapture() {
+ AttachThreadScoped ats(g_jvm_capture);
+ JNIEnv* env = ats.env();
+ {
+ MutexLock lock(&api_lock_);
+
+ memset(&_requestedCapability, 0, sizeof(_requestedCapability));
+ memset(&_captureCapability, 0, sizeof(_captureCapability));
+ _captureStarted = false;
+ // Exit critical section to avoid blocking camera thread inside
+ // onIncomingFrame() call.
+ }
+
+ // try to stop the capturer.
+ jmethodID j_stop =
+ env->GetMethodID(g_java_capturer_class, "stopCapture", "()Z");
+ return env->CallBooleanMethod(_jCapturer, j_stop) ? 0 : -1;
+}
+
+bool VideoCaptureAndroid::CaptureStarted() {
+ MutexLock lock(&api_lock_);
+ return _captureStarted;
+}
+
+int32_t VideoCaptureAndroid::CaptureSettings(VideoCaptureCapability& settings) {
+ MutexLock lock(&api_lock_);
+ settings = _requestedCapability;
+ return 0;
+}
+
+} // namespace videocapturemodule
+} // namespace webrtc
diff --git a/dom/media/systemservices/android_video_capture/video_capture_android.h b/dom/media/systemservices/android_video_capture/video_capture_android.h
new file mode 100644
index 0000000000..720c28e70b
--- /dev/null
+++ b/dom/media/systemservices/android_video_capture/video_capture_android.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2012 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+#ifndef WEBRTC_MODULES_VIDEO_CAPTURE_MAIN_SOURCE_ANDROID_VIDEO_CAPTURE_ANDROID_H_
+#define WEBRTC_MODULES_VIDEO_CAPTURE_MAIN_SOURCE_ANDROID_VIDEO_CAPTURE_ANDROID_H_
+
+#include <jni.h>
+
+#include "device_info_android.h"
+#include "api/video/i420_buffer.h"
+#include "modules/video_capture/video_capture_impl.h"
+
+namespace webrtc {
+namespace videocapturemodule {
+
+class VideoCaptureAndroid : public VideoCaptureImpl {
+ public:
+ VideoCaptureAndroid();
+ virtual int32_t Init(const char* deviceUniqueIdUTF8);
+
+ virtual int32_t StartCapture(const VideoCaptureCapability& capability);
+ virtual int32_t StopCapture();
+ virtual bool CaptureStarted();
+ virtual int32_t CaptureSettings(VideoCaptureCapability& settings);
+
+ void OnIncomingFrame(rtc::scoped_refptr<I420Buffer> buffer, int32_t degrees,
+ int64_t captureTime = 0);
+
+ protected:
+ virtual ~VideoCaptureAndroid();
+
+ DeviceInfoAndroid _deviceInfo;
+ jobject _jCapturer; // Global ref to Java VideoCaptureAndroid object.
+ VideoCaptureCapability _captureCapability;
+ bool _captureStarted;
+};
+
+} // namespace videocapturemodule
+} // namespace webrtc
+#endif // WEBRTC_MODULES_VIDEO_CAPTURE_MAIN_SOURCE_ANDROID_VIDEO_CAPTURE_ANDROID_H_
diff --git a/dom/media/systemservices/moz.build b/dom/media/systemservices/moz.build
new file mode 100644
index 0000000000..09a20b9f15
--- /dev/null
+++ b/dom/media/systemservices/moz.build
@@ -0,0 +1,112 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+include("/dom/media/webrtc/third_party_build/webrtc.mozbuild")
+
+if CONFIG["MOZ_WEBRTC"]:
+ EXPORTS += [
+ "CamerasChild.h",
+ "CamerasParent.h",
+ "VideoEngine.h",
+ "VideoFrameUtils.h",
+ ]
+ UNIFIED_SOURCES += [
+ "CamerasChild.cpp",
+ "CamerasParent.cpp",
+ "VideoEngine.cpp",
+ "VideoFrameUtils.cpp",
+ ]
+ LOCAL_INCLUDES += [
+ "/dom/media/webrtc",
+ "/media/libyuv/libyuv/include",
+ "/mfbt",
+ "/third_party/libwebrtc",
+ "/third_party/libwebrtc/third_party/abseil-cpp",
+ "/tools/profiler/public",
+ ]
+
+ if CONFIG["OS_TARGET"] == "Android":
+ UNIFIED_SOURCES += [
+ "android_video_capture/device_info_android.cc",
+ "android_video_capture/video_capture_android.cc",
+ ]
+ elif CONFIG["OS_TARGET"] == "Darwin":
+ UNIFIED_SOURCES += [
+ "objc_video_capture/device_info.mm",
+ "objc_video_capture/device_info_avfoundation.mm",
+ "objc_video_capture/device_info_objc.mm",
+ "objc_video_capture/rtc_video_capture_objc.mm",
+ "objc_video_capture/video_capture.mm",
+ "objc_video_capture/video_capture_avfoundation.mm",
+ ]
+ LOCAL_INCLUDES += [
+ "/third_party/libwebrtc/sdk/objc",
+ "/third_party/libwebrtc/sdk/objc/base",
+ ]
+ CMMFLAGS += [
+ "-fobjc-arc",
+ ]
+
+ if CONFIG["OS_TARGET"] != "Android":
+ UNIFIED_SOURCES += [
+ "video_engine/desktop_capture_impl.cc",
+ "video_engine/desktop_device_info.cc",
+ "video_engine/tab_capturer.cc",
+ ]
+
+
+if CONFIG["OS_TARGET"] == "Android":
+ DEFINES["WEBRTC_ANDROID"] = True
+
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa":
+ UNIFIED_SOURCES += ["OSXRunLoopSingleton.cpp"]
+ EXPORTS += ["OSXRunLoopSingleton.h"]
+
+EXPORTS.mozilla += [
+ "ShmemPool.h",
+]
+
+EXPORTS.mozilla.media += [
+ "CamerasTypes.h",
+ "MediaChild.h",
+ "MediaParent.h",
+ "MediaSystemResourceClient.h",
+ "MediaSystemResourceManager.h",
+ "MediaSystemResourceManagerChild.h",
+ "MediaSystemResourceManagerParent.h",
+ "MediaSystemResourceMessageUtils.h",
+ "MediaSystemResourceService.h",
+ "MediaSystemResourceTypes.h",
+ "MediaTaskUtils.h",
+ "MediaUtils.h",
+]
+UNIFIED_SOURCES += [
+ "CamerasTypes.cpp",
+ "MediaChild.cpp",
+ "MediaParent.cpp",
+ "MediaSystemResourceClient.cpp",
+ "MediaSystemResourceManager.cpp",
+ "MediaSystemResourceManagerChild.cpp",
+ "MediaSystemResourceManagerParent.cpp",
+ "MediaSystemResourceService.cpp",
+ "MediaUtils.cpp",
+ "ShmemPool.cpp",
+]
+IPDL_SOURCES += [
+ "PCameras.ipdl",
+ "PMedia.ipdl",
+ "PMediaSystemResourceManager.ipdl",
+]
+# /dom/base needed for nsGlobalWindow.h in MediaChild.cpp
+LOCAL_INCLUDES += [
+ "/dom/base",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+
+with Files("android_video_capture/**"):
+ SCHEDULES.exclusive = ["android"]
diff --git a/dom/media/systemservices/objc_video_capture/device_info.h b/dom/media/systemservices/objc_video_capture/device_info.h
new file mode 100644
index 0000000000..d146cfcfda
--- /dev/null
+++ b/dom/media/systemservices/objc_video_capture/device_info.h
@@ -0,0 +1,56 @@
+/*
+ * Copyright (c) 2013 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+#ifndef MODULES_VIDEO_CAPTURE_OBJC_DEVICE_INFO_H_
+#define MODULES_VIDEO_CAPTURE_OBJC_DEVICE_INFO_H_
+
+#include "modules/video_capture/device_info_impl.h"
+
+#include <map>
+#include <string>
+
+@class DeviceInfoIosObjC;
+
+namespace webrtc::videocapturemodule {
+class DeviceInfoIos : public DeviceInfoImpl {
+ public:
+ DeviceInfoIos();
+ virtual ~DeviceInfoIos();
+
+ // Implementation of DeviceInfoImpl.
+ int32_t Init() override;
+ uint32_t NumberOfDevices() override;
+ int32_t GetDeviceName(uint32_t deviceNumber, char* deviceNameUTF8, uint32_t deviceNameLength,
+ char* deviceUniqueIdUTF8, uint32_t deviceUniqueIdUTF8Length,
+ char* productUniqueIdUTF8 = 0, uint32_t productUniqueIdUTF8Length = 0,
+ pid_t* pid = 0) override;
+
+ int32_t NumberOfCapabilities(const char* deviceUniqueIdUTF8) override;
+
+ int32_t GetCapability(const char* deviceUniqueIdUTF8, const uint32_t deviceCapabilityNumber,
+ VideoCaptureCapability& capability) override;
+
+ int32_t DisplayCaptureSettingsDialogBox(const char* deviceUniqueIdUTF8,
+ const char* dialogTitleUTF8, void* parentWindow,
+ uint32_t positionX, uint32_t positionY) override;
+
+ int32_t GetOrientation(const char* deviceUniqueIdUTF8, VideoRotation& orientation) override;
+
+ int32_t CreateCapabilityMap(const char* device_unique_id_utf8) override
+ RTC_EXCLUSIVE_LOCKS_REQUIRED(_apiLock);
+
+ private:
+ std::map<std::string, VideoCaptureCapabilities> _capabilitiesMap;
+ DeviceInfoIosObjC* _captureInfo;
+};
+
+} // namespace webrtc::videocapturemodule
+
+#endif // MODULES_VIDEO_CAPTURE_OBJC_DEVICE_INFO_H_
diff --git a/dom/media/systemservices/objc_video_capture/device_info.mm b/dom/media/systemservices/objc_video_capture/device_info.mm
new file mode 100644
index 0000000000..d0299a9ec9
--- /dev/null
+++ b/dom/media/systemservices/objc_video_capture/device_info.mm
@@ -0,0 +1,170 @@
+/*
+ * Copyright (c) 2013 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+# error "This file requires ARC support."
+#endif
+
+#include <AVFoundation/AVFoundation.h>
+
+#include <string>
+
+#include "device_info.h"
+#include "device_info_objc.h"
+#include "modules/video_capture/video_capture_impl.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "objc_video_capture/device_info_avfoundation.h"
+#include "rtc_base/logging.h"
+
+using namespace mozilla;
+using namespace webrtc;
+using namespace videocapturemodule;
+
+static NSArray* camera_presets = @[
+ AVCaptureSessionPreset352x288, AVCaptureSessionPreset640x480, AVCaptureSessionPreset1280x720
+];
+
+#define IOS_UNSUPPORTED() \
+ RTC_LOG(LS_ERROR) << __FUNCTION__ << " is not supported on the iOS platform."; \
+ return -1;
+
+VideoCaptureModule::DeviceInfo* VideoCaptureImpl::CreateDeviceInfo() {
+ if (StaticPrefs::media_getusermedia_camera_macavf_enabled_AtStartup()) {
+ return new DeviceInfoAvFoundation();
+ }
+ return new DeviceInfoIos();
+}
+
+DeviceInfoIos::DeviceInfoIos() { this->Init(); }
+
+DeviceInfoIos::~DeviceInfoIos() { [_captureInfo registerOwner:nil]; }
+
+int32_t DeviceInfoIos::Init() {
+ _captureInfo = [[DeviceInfoIosObjC alloc] init];
+ [_captureInfo registerOwner:this];
+
+ // Fill in all device capabilities.
+ int deviceCount = [DeviceInfoIosObjC captureDeviceCount];
+
+ for (int i = 0; i < deviceCount; i++) {
+ AVCaptureDevice* avDevice = [DeviceInfoIosObjC captureDeviceForIndex:i];
+ VideoCaptureCapabilities capabilityVector;
+
+ for (NSString* preset in camera_presets) {
+ BOOL support = [avDevice supportsAVCaptureSessionPreset:preset];
+ if (support) {
+ VideoCaptureCapability capability = [DeviceInfoIosObjC capabilityForPreset:preset];
+ capabilityVector.push_back(capability);
+ }
+ }
+
+ char deviceNameUTF8[256];
+ char deviceId[256];
+ int error = this->GetDeviceName(i, deviceNameUTF8, 256, deviceId, 256);
+ if (error) {
+ return error;
+ }
+ std::string deviceIdCopy(deviceId);
+ std::pair<std::string, VideoCaptureCapabilities> mapPair =
+ std::pair<std::string, VideoCaptureCapabilities>(deviceIdCopy, capabilityVector);
+ _capabilitiesMap.insert(mapPair);
+ }
+
+ return 0;
+}
+
+uint32_t DeviceInfoIos::NumberOfDevices() { return [DeviceInfoIosObjC captureDeviceCount]; }
+
+int32_t DeviceInfoIos::GetDeviceName(uint32_t deviceNumber, char* deviceNameUTF8,
+ uint32_t deviceNameUTF8Length, char* deviceUniqueIdUTF8,
+ uint32_t deviceUniqueIdUTF8Length, char* productUniqueIdUTF8,
+ uint32_t productUniqueIdUTF8Length, pid_t* pid) {
+ if (deviceNumber >= NumberOfDevices()) {
+ return -1;
+ }
+
+ NSString* deviceName = [DeviceInfoIosObjC deviceNameForIndex:deviceNumber];
+
+ NSString* deviceUniqueId = [DeviceInfoIosObjC deviceUniqueIdForIndex:deviceNumber];
+
+ strncpy(deviceNameUTF8, [deviceName UTF8String], deviceNameUTF8Length);
+ deviceNameUTF8[deviceNameUTF8Length - 1] = '\0';
+
+ strncpy(deviceUniqueIdUTF8, deviceUniqueId.UTF8String, deviceUniqueIdUTF8Length);
+ deviceUniqueIdUTF8[deviceUniqueIdUTF8Length - 1] = '\0';
+
+ if (productUniqueIdUTF8) {
+ productUniqueIdUTF8[0] = '\0';
+ }
+
+ return 0;
+}
+
+int32_t DeviceInfoIos::NumberOfCapabilities(const char* deviceUniqueIdUTF8) {
+ int32_t numberOfCapabilities = 0;
+ std::string deviceUniqueId(deviceUniqueIdUTF8);
+ std::map<std::string, VideoCaptureCapabilities>::iterator it =
+ _capabilitiesMap.find(deviceUniqueId);
+
+ if (it != _capabilitiesMap.end()) {
+ numberOfCapabilities = it->second.size();
+ }
+ return numberOfCapabilities;
+}
+
+int32_t DeviceInfoIos::GetCapability(const char* deviceUniqueIdUTF8,
+ const uint32_t deviceCapabilityNumber,
+ VideoCaptureCapability& capability) {
+ std::string deviceUniqueId(deviceUniqueIdUTF8);
+ std::map<std::string, VideoCaptureCapabilities>::iterator it =
+ _capabilitiesMap.find(deviceUniqueId);
+
+ if (it != _capabilitiesMap.end()) {
+ VideoCaptureCapabilities deviceCapabilities = it->second;
+
+ if (deviceCapabilityNumber < deviceCapabilities.size()) {
+ VideoCaptureCapability cap;
+ cap = deviceCapabilities[deviceCapabilityNumber];
+ capability = cap;
+ return 0;
+ }
+ }
+
+ return -1;
+}
+
+int32_t DeviceInfoIos::DisplayCaptureSettingsDialogBox(const char* deviceUniqueIdUTF8,
+ const char* dialogTitleUTF8,
+ void* parentWindow, uint32_t positionX,
+ uint32_t positionY) {
+ IOS_UNSUPPORTED();
+}
+
+int32_t DeviceInfoIos::GetOrientation(const char* deviceUniqueIdUTF8, VideoRotation& orientation) {
+ if (strcmp(deviceUniqueIdUTF8, "Front Camera") == 0) {
+ orientation = kVideoRotation_0;
+ } else {
+ orientation = kVideoRotation_90;
+ }
+ return orientation;
+}
+
+int32_t DeviceInfoIos::CreateCapabilityMap(const char* deviceUniqueIdUTF8) {
+ std::string deviceName(deviceUniqueIdUTF8);
+ std::map<std::string, std::vector<VideoCaptureCapability>>::iterator it =
+ _capabilitiesMap.find(deviceName);
+ VideoCaptureCapabilities deviceCapabilities;
+ if (it != _capabilitiesMap.end()) {
+ _captureCapabilities = it->second;
+ return 0;
+ }
+
+ return -1;
+}
diff --git a/dom/media/systemservices/objc_video_capture/device_info_avfoundation.h b/dom/media/systemservices/objc_video_capture/device_info_avfoundation.h
new file mode 100644
index 0000000000..9a698480fa
--- /dev/null
+++ b/dom/media/systemservices/objc_video_capture/device_info_avfoundation.h
@@ -0,0 +1,71 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_SYSTEMSERVICES_OBJC_VIDEO_CAPTURE_DEVICE_INFO_AVFOUNDATION_H_
+#define DOM_MEDIA_SYSTEMSERVICES_OBJC_VIDEO_CAPTURE_DEVICE_INFO_AVFOUNDATION_H_
+
+#include <map>
+#include <string>
+
+#include "api/sequence_checker.h"
+#include "device_info_objc.h"
+#include "modules/video_capture/device_info_impl.h"
+
+namespace webrtc::videocapturemodule {
+
+/**
+ * DeviceInfo implementation for the libwebrtc ios/mac sdk camera backend.
+ * Single threaded except for DeviceChange() that happens on a platform callback
+ * thread.
+ */
+class DeviceInfoAvFoundation : public DeviceInfoImpl {
+ public:
+ static int32_t ConvertAVFrameRateToCapabilityFPS(Float64 aRate);
+ static webrtc::VideoType ConvertFourCCToVideoType(FourCharCode aCode);
+
+ DeviceInfoAvFoundation();
+ virtual ~DeviceInfoAvFoundation();
+
+ // Implementation of DeviceInfoImpl.
+ int32_t Init() override { return 0; }
+ void DeviceChange() override;
+ uint32_t NumberOfDevices() override;
+ int32_t GetDeviceName(uint32_t aDeviceNumber, char* aDeviceNameUTF8,
+ uint32_t aDeviceNameLength, char* aDeviceUniqueIdUTF8,
+ uint32_t aDeviceUniqueIdUTF8Length,
+ char* aProductUniqueIdUTF8 = nullptr,
+ uint32_t aProductUniqueIdUTF8Length = 0,
+ pid_t* aPid = nullptr) override;
+ int32_t NumberOfCapabilities(const char* aDeviceUniqueIdUTF8) override;
+ int32_t GetCapability(const char* aDeviceUniqueIdUTF8,
+ const uint32_t aDeviceCapabilityNumber,
+ VideoCaptureCapability& aCapability) override;
+ int32_t DisplayCaptureSettingsDialogBox(const char* aDeviceUniqueIdUTF8,
+ const char* aDialogTitleUTF8,
+ void* aParentWindow,
+ uint32_t aPositionX,
+ uint32_t aPositionY) override {
+ return -1;
+ }
+ int32_t CreateCapabilityMap(const char* aDeviceUniqueIdUTF8) override
+ RTC_EXCLUSIVE_LOCKS_REQUIRED(_apiLock);
+
+ private:
+ const std::tuple<std::string, std::string, VideoCaptureCapabilities>*
+ FindDeviceAndCapabilities(const std::string& aDeviceUniqueId) const;
+ void EnsureCapabilitiesMap();
+
+ SequenceChecker mChecker;
+ std::atomic<bool> mInvalidateCapabilities;
+ // [{uniqueId, name, capabilities}]
+ std::vector<std::tuple<std::string, std::string, VideoCaptureCapabilities>>
+ mDevicesAndCapabilities RTC_GUARDED_BY(mChecker);
+ const DeviceInfoIosObjC* mDeviceChangeCaptureInfo RTC_GUARDED_BY(mChecker);
+};
+
+} // namespace webrtc::videocapturemodule
+
+#endif
diff --git a/dom/media/systemservices/objc_video_capture/device_info_avfoundation.mm b/dom/media/systemservices/objc_video_capture/device_info_avfoundation.mm
new file mode 100644
index 0000000000..fae65ff343
--- /dev/null
+++ b/dom/media/systemservices/objc_video_capture/device_info_avfoundation.mm
@@ -0,0 +1,213 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "device_info_avfoundation.h"
+#include <CoreVideo/CVPixelBuffer.h>
+
+#include <string>
+
+#include "components/capturer/RTCCameraVideoCapturer.h"
+#import "helpers/NSString+StdString.h"
+#include "media/base/video_common.h"
+#include "modules/video_capture/video_capture_defines.h"
+#include "rtc_base/logging.h"
+
+namespace webrtc::videocapturemodule {
+/* static */
+int32_t DeviceInfoAvFoundation::ConvertAVFrameRateToCapabilityFPS(Float64 aRate) {
+ return static_cast<int32_t>(aRate);
+}
+
+/* static */
+webrtc::VideoType DeviceInfoAvFoundation::ConvertFourCCToVideoType(FourCharCode aCode) {
+ switch (aCode) {
+ case kCVPixelFormatType_420YpCbCr8Planar:
+ case kCVPixelFormatType_420YpCbCr8PlanarFullRange:
+ return webrtc::VideoType::kI420;
+ case kCVPixelFormatType_24BGR:
+ return webrtc::VideoType::kRGB24;
+ case kCVPixelFormatType_32ABGR:
+ return webrtc::VideoType::kABGR;
+ case kCMPixelFormat_32ARGB:
+ return webrtc::VideoType::kBGRA;
+ case kCMPixelFormat_32BGRA:
+ return webrtc::VideoType::kARGB;
+ case kCMPixelFormat_16LE565:
+ return webrtc::VideoType::kRGB565;
+ case kCMPixelFormat_16LE555:
+ case kCMPixelFormat_16LE5551:
+ return webrtc::VideoType::kARGB1555;
+ case kCMPixelFormat_422YpCbCr8_yuvs:
+ return webrtc::VideoType::kYUY2;
+ case kCMPixelFormat_422YpCbCr8:
+ return webrtc::VideoType::kUYVY;
+ case kCMVideoCodecType_JPEG:
+ case kCMVideoCodecType_JPEG_OpenDML:
+ return webrtc::VideoType::kMJPEG;
+ case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
+ case kCVPixelFormatType_420YpCbCr8BiPlanarFullRange:
+ return webrtc::VideoType::kNV12;
+ default:
+ RTC_LOG(LS_WARNING) << "Unhandled FourCharCode" << aCode;
+ return webrtc::VideoType::kUnknown;
+ }
+}
+
+DeviceInfoAvFoundation::DeviceInfoAvFoundation()
+ : mInvalidateCapabilities(false), mDeviceChangeCaptureInfo([[DeviceInfoIosObjC alloc] init]) {
+ [mDeviceChangeCaptureInfo registerOwner:this];
+}
+
+DeviceInfoAvFoundation::~DeviceInfoAvFoundation() { [mDeviceChangeCaptureInfo registerOwner:nil]; }
+
+void DeviceInfoAvFoundation::DeviceChange() {
+ mInvalidateCapabilities = true;
+ DeviceInfo::DeviceChange();
+}
+
+uint32_t DeviceInfoAvFoundation::NumberOfDevices() {
+ RTC_DCHECK_RUN_ON(&mChecker);
+ EnsureCapabilitiesMap();
+ return mDevicesAndCapabilities.size();
+}
+
+int32_t DeviceInfoAvFoundation::GetDeviceName(uint32_t aDeviceNumber, char* aDeviceNameUTF8,
+ uint32_t aDeviceNameLength, char* aDeviceUniqueIdUTF8,
+ uint32_t aDeviceUniqueIdUTF8Length,
+ char* /* aProductUniqueIdUTF8 */,
+ uint32_t /* aProductUniqueIdUTF8Length */,
+ pid_t* /* aPid */) {
+ RTC_DCHECK_RUN_ON(&mChecker);
+ // Don't EnsureCapabilitiesMap() here, since:
+ // 1) That might invalidate the capabilities map
+ // 2) This function depends on the device index
+
+ if (aDeviceNumber >= mDevicesAndCapabilities.size()) {
+ return -1;
+ }
+
+ const auto& [uniqueId, name, _] = mDevicesAndCapabilities[aDeviceNumber];
+
+ strncpy(aDeviceUniqueIdUTF8, uniqueId.c_str(), aDeviceUniqueIdUTF8Length);
+ aDeviceUniqueIdUTF8[aDeviceUniqueIdUTF8Length - 1] = '\0';
+
+ strncpy(aDeviceNameUTF8, name.c_str(), aDeviceNameLength);
+ aDeviceNameUTF8[aDeviceNameLength - 1] = '\0';
+
+ return 0;
+}
+
+int32_t DeviceInfoAvFoundation::NumberOfCapabilities(const char* aDeviceUniqueIdUTF8) {
+ RTC_DCHECK_RUN_ON(&mChecker);
+
+ std::string deviceUniqueId(aDeviceUniqueIdUTF8);
+ const auto* tup = FindDeviceAndCapabilities(deviceUniqueId);
+ if (!tup) {
+ return 0;
+ }
+
+ const auto& [_, __, capabilities] = *tup;
+ return static_cast<int32_t>(capabilities.size());
+}
+
+int32_t DeviceInfoAvFoundation::GetCapability(const char* aDeviceUniqueIdUTF8,
+ const uint32_t aDeviceCapabilityNumber,
+ VideoCaptureCapability& aCapability) {
+ RTC_DCHECK_RUN_ON(&mChecker);
+
+ std::string deviceUniqueId(aDeviceUniqueIdUTF8);
+ const auto* tup = FindDeviceAndCapabilities(deviceUniqueId);
+ if (!tup) {
+ return -1;
+ }
+
+ const auto& [_, __, capabilities] = *tup;
+ if (aDeviceCapabilityNumber >= capabilities.size()) {
+ return -1;
+ }
+
+ aCapability = capabilities[aDeviceCapabilityNumber];
+ return 0;
+}
+
+int32_t DeviceInfoAvFoundation::CreateCapabilityMap(const char* aDeviceUniqueIdUTF8) {
+ RTC_DCHECK_RUN_ON(&mChecker);
+
+ const size_t deviceUniqueIdUTF8Length = strlen(aDeviceUniqueIdUTF8);
+ if (deviceUniqueIdUTF8Length > kVideoCaptureUniqueNameLength) {
+ RTC_LOG(LS_INFO) << "Device name too long";
+ return -1;
+ }
+ RTC_LOG(LS_INFO) << "CreateCapabilityMap called for device " << aDeviceUniqueIdUTF8;
+ std::string deviceUniqueId(aDeviceUniqueIdUTF8);
+ const auto* tup = FindDeviceAndCapabilities(deviceUniqueId);
+ if (!tup) {
+ RTC_LOG(LS_INFO) << "no matching device found";
+ return -1;
+ }
+
+ // Store the new used device name
+ _lastUsedDeviceNameLength = deviceUniqueIdUTF8Length;
+ _lastUsedDeviceName =
+ static_cast<char*>(realloc(_lastUsedDeviceName, _lastUsedDeviceNameLength + 1));
+ memcpy(_lastUsedDeviceName, aDeviceUniqueIdUTF8, _lastUsedDeviceNameLength + 1);
+
+ const auto& [_, __, capabilities] = *tup;
+ _captureCapabilities = capabilities;
+ return static_cast<int32_t>(_captureCapabilities.size());
+}
+
+auto DeviceInfoAvFoundation::FindDeviceAndCapabilities(const std::string& aDeviceUniqueId) const
+ -> const std::tuple<std::string, std::string, VideoCaptureCapabilities>* {
+ RTC_DCHECK_RUN_ON(&mChecker);
+ for (const auto& tup : mDevicesAndCapabilities) {
+ if (std::get<0>(tup) == aDeviceUniqueId) {
+ return &tup;
+ }
+ }
+ return nullptr;
+}
+
+void DeviceInfoAvFoundation::EnsureCapabilitiesMap() {
+ RTC_DCHECK_RUN_ON(&mChecker);
+
+ if (mInvalidateCapabilities.exchange(false)) {
+ mDevicesAndCapabilities.clear();
+ }
+
+ if (!mDevicesAndCapabilities.empty()) {
+ return;
+ }
+
+ for (AVCaptureDevice* device in [RTCCameraVideoCapturer captureDevices]) {
+ std::string uniqueId = [NSString stdStringForString:device.uniqueID];
+ std::string name = [NSString stdStringForString:device.localizedName];
+ auto& [_, __, capabilities] =
+ mDevicesAndCapabilities.emplace_back(uniqueId, name, VideoCaptureCapabilities());
+
+ for (AVCaptureDeviceFormat* format in
+ [RTCCameraVideoCapturer supportedFormatsForDevice:device]) {
+ VideoCaptureCapability cap;
+ FourCharCode fourcc = CMFormatDescriptionGetMediaSubType(format.formatDescription);
+ cap.videoType = ConvertFourCCToVideoType(fourcc);
+ CMVideoDimensions dimensions =
+ CMVideoFormatDescriptionGetDimensions(format.formatDescription);
+ cap.width = dimensions.width;
+ cap.height = dimensions.height;
+
+ for (AVFrameRateRange* range in format.videoSupportedFrameRateRanges) {
+ cap.maxFPS = ConvertAVFrameRateToCapabilityFPS(range.maxFrameRate);
+ capabilities.push_back(cap);
+ }
+
+ if (capabilities.empty()) {
+ cap.maxFPS = 30;
+ capabilities.push_back(cap);
+ }
+ }
+ }
+}
+} // namespace webrtc::videocapturemodule
diff --git a/dom/media/systemservices/objc_video_capture/device_info_objc.h b/dom/media/systemservices/objc_video_capture/device_info_objc.h
new file mode 100644
index 0000000000..1ddedb471e
--- /dev/null
+++ b/dom/media/systemservices/objc_video_capture/device_info_objc.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (c) 2013 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+#ifndef MODULES_VIDEO_CAPTURE_OBJC_DEVICE_INFO_OBJC_H_
+#define MODULES_VIDEO_CAPTURE_OBJC_DEVICE_INFO_OBJC_H_
+
+#import <AVFoundation/AVFoundation.h>
+
+#include "modules/video_capture/video_capture_defines.h"
+#include "device_info.h"
+
+@interface DeviceInfoIosObjC : NSObject {
+ NSArray* _observers;
+ NSLock* _lock;
+ webrtc::VideoCaptureModule::DeviceInfo* _owner;
+}
+
++ (int)captureDeviceCount;
++ (AVCaptureDevice*)captureDeviceForIndex:(int)index;
++ (AVCaptureDevice*)captureDeviceForUniqueId:(NSString*)uniqueId;
++ (NSString*)deviceNameForIndex:(int)index;
++ (NSString*)deviceUniqueIdForIndex:(int)index;
++ (NSString*)deviceNameForUniqueId:(NSString*)uniqueId;
++ (webrtc::VideoCaptureCapability)capabilityForPreset:(NSString*)preset;
+
+- (void)registerOwner:(webrtc::VideoCaptureModule::DeviceInfo*)owner;
+- (void)configureObservers;
+
+@end
+
+#endif // MODULES_VIDEO_CAPTURE_OBJC_DEVICE_INFO_OBJC_H_
diff --git a/dom/media/systemservices/objc_video_capture/device_info_objc.mm b/dom/media/systemservices/objc_video_capture/device_info_objc.mm
new file mode 100644
index 0000000000..6e9435daff
--- /dev/null
+++ b/dom/media/systemservices/objc_video_capture/device_info_objc.mm
@@ -0,0 +1,166 @@
+/*
+ * Copyright (c) 2013 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+# error "This file requires ARC support."
+#endif
+
+#import <AVFoundation/AVFoundation.h>
+
+#import "device_info_objc.h"
+
+@implementation DeviceInfoIosObjC
+
+- (id)init {
+ self = [super init];
+ if (nil != self) {
+ _lock = [[NSLock alloc] init];
+ }
+ return self;
+}
+
+- (void)dealloc {
+}
+
+- (void)registerOwner:(webrtc::VideoCaptureModule::DeviceInfo*)owner {
+ [_lock lock];
+ if (!_owner && owner) {
+ [self configureObservers];
+ } else if (_owner && !owner) {
+ NSNotificationCenter* notificationCenter = [NSNotificationCenter defaultCenter];
+ for (id observer in _observers) {
+ [notificationCenter removeObserver:observer];
+ }
+ _observers = nil;
+ }
+ _owner = owner;
+ [_lock unlock];
+}
+
++ (int)captureDeviceCount {
+ int cnt = 0;
+ @try {
+ for (AVCaptureDevice* device in [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]) {
+ if ([device isSuspended]) {
+ continue;
+ }
+ cnt++;
+ }
+ } @catch (NSException* exception) {
+ cnt = 0;
+ }
+ return cnt;
+}
+
++ (AVCaptureDevice*)captureDeviceForIndex:(int)index {
+ int cnt = 0;
+ @try {
+ for (AVCaptureDevice* device in [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]) {
+ if ([device isSuspended]) {
+ continue;
+ }
+ if (cnt == index) {
+ return device;
+ }
+ cnt++;
+ }
+ } @catch (NSException* exception) {
+ cnt = 0;
+ }
+
+ return nil;
+}
+
++ (AVCaptureDevice*)captureDeviceForUniqueId:(NSString*)uniqueId {
+ for (AVCaptureDevice* device in [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo]) {
+ if ([device isSuspended]) {
+ continue;
+ }
+ if ([uniqueId isEqual:device.uniqueID]) {
+ return device;
+ }
+ }
+
+ return nil;
+}
+
++ (NSString*)deviceNameForIndex:(int)index {
+ return [DeviceInfoIosObjC captureDeviceForIndex:index].localizedName;
+}
+
++ (NSString*)deviceUniqueIdForIndex:(int)index {
+ return [DeviceInfoIosObjC captureDeviceForIndex:index].uniqueID;
+}
+
++ (NSString*)deviceNameForUniqueId:(NSString*)uniqueId {
+ return [[AVCaptureDevice deviceWithUniqueID:uniqueId] localizedName];
+}
+
++ (webrtc::VideoCaptureCapability)capabilityForPreset:(NSString*)preset {
+ webrtc::VideoCaptureCapability capability;
+
+ // TODO(tkchin): Maybe query AVCaptureDevice for supported formats, and
+ // then get the dimensions / frame rate from each supported format
+ if ([preset isEqualToString:AVCaptureSessionPreset352x288]) {
+ capability.width = 352;
+ capability.height = 288;
+ capability.maxFPS = 30;
+ capability.videoType = webrtc::VideoType::kNV12;
+ capability.interlaced = false;
+ } else if ([preset isEqualToString:AVCaptureSessionPreset640x480]) {
+ capability.width = 640;
+ capability.height = 480;
+ capability.maxFPS = 30;
+ capability.videoType = webrtc::VideoType::kNV12;
+ capability.interlaced = false;
+ } else if ([preset isEqualToString:AVCaptureSessionPreset1280x720]) {
+ capability.width = 1280;
+ capability.height = 720;
+ capability.maxFPS = 30;
+ capability.videoType = webrtc::VideoType::kNV12;
+ capability.interlaced = false;
+ }
+
+ return capability;
+}
+
+- (void)configureObservers {
+ // register device connected / disconnected event
+ NSNotificationCenter* notificationCenter = [NSNotificationCenter defaultCenter];
+
+ id deviceWasConnectedObserver =
+ [notificationCenter addObserverForName:AVCaptureDeviceWasConnectedNotification
+ object:nil
+ queue:[NSOperationQueue mainQueue]
+ usingBlock:^(NSNotification* note) {
+ [_lock lock];
+ AVCaptureDevice* device = [note object];
+ BOOL isVideoDevice = [device hasMediaType:AVMediaTypeVideo];
+ if (isVideoDevice && _owner) _owner->DeviceChange();
+ [_lock unlock];
+ }];
+
+ id deviceWasDisconnectedObserver =
+ [notificationCenter addObserverForName:AVCaptureDeviceWasDisconnectedNotification
+ object:nil
+ queue:[NSOperationQueue mainQueue]
+ usingBlock:^(NSNotification* note) {
+ [_lock lock];
+ AVCaptureDevice* device = [note object];
+ BOOL isVideoDevice = [device hasMediaType:AVMediaTypeVideo];
+ if (isVideoDevice && _owner) _owner->DeviceChange();
+ [_lock unlock];
+ }];
+
+ _observers = [[NSArray alloc]
+ initWithObjects:deviceWasConnectedObserver, deviceWasDisconnectedObserver, nil];
+}
+
+@end
diff --git a/dom/media/systemservices/objc_video_capture/rtc_video_capture_objc.h b/dom/media/systemservices/objc_video_capture/rtc_video_capture_objc.h
new file mode 100644
index 0000000000..9c6604ffe5
--- /dev/null
+++ b/dom/media/systemservices/objc_video_capture/rtc_video_capture_objc.h
@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) 2013 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+#ifndef MODULES_VIDEO_CAPTURE_OBJC_RTC_VIDEO_CAPTURE_OBJC_H_
+#define MODULES_VIDEO_CAPTURE_OBJC_RTC_VIDEO_CAPTURE_OBJC_H_
+
+#import <AVFoundation/AVFoundation.h>
+#import <Foundation/Foundation.h>
+#ifdef WEBRTC_IOS
+# import <UIKit/UIKit.h>
+#endif
+
+#include "video_capture.h"
+
+// The following class listens to a notification with name:
+// 'StatusBarOrientationDidChange'.
+// This notification must be posted in order for the capturer to reflect the
+// orientation change in video w.r.t. the application orientation.
+@interface RTCVideoCaptureIosObjC : NSObject <AVCaptureVideoDataOutputSampleBufferDelegate>
+
+@property webrtc::VideoRotation frameRotation;
+
+// custom initializer. Instance of VideoCaptureIos is needed
+// for callback purposes.
+// default init methods have been overridden to return nil.
+- (id)initWithOwner:(webrtc::videocapturemodule::VideoCaptureIos*)owner;
+- (BOOL)setCaptureDeviceByUniqueId:(NSString*)uniqueId;
+- (BOOL)startCaptureWithCapability:(const webrtc::VideoCaptureCapability&)capability;
+- (BOOL)stopCapture;
+
+@end
+#endif // MODULES_VIDEO_CAPTURE_OBJC_RTC_VIDEO_CAPTURE_OBJC_H_
diff --git a/dom/media/systemservices/objc_video_capture/rtc_video_capture_objc.mm b/dom/media/systemservices/objc_video_capture/rtc_video_capture_objc.mm
new file mode 100644
index 0000000000..0a36768fa8
--- /dev/null
+++ b/dom/media/systemservices/objc_video_capture/rtc_video_capture_objc.mm
@@ -0,0 +1,355 @@
+/*
+ * Copyright (c) 2013 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+# error "This file requires ARC support."
+#endif
+
+#import <AVFoundation/AVFoundation.h>
+#ifdef WEBRTC_IOS
+# import <UIKit/UIKit.h>
+#endif
+
+#import "device_info_objc.h"
+#import "rtc_video_capture_objc.h"
+
+#include "rtc_base/logging.h"
+
+using namespace webrtc;
+using namespace webrtc::videocapturemodule;
+
+@interface RTCVideoCaptureIosObjC (hidden)
+- (int)changeCaptureInputWithName:(NSString*)captureDeviceName;
+@end
+
+@implementation RTCVideoCaptureIosObjC {
+ webrtc::videocapturemodule::VideoCaptureIos* _owner;
+ webrtc::VideoCaptureCapability _capability;
+ AVCaptureSession* _captureSession;
+ BOOL _orientationHasChanged;
+ AVCaptureConnection* _connection;
+ BOOL _captureChanging; // Guarded by _captureChangingCondition.
+ NSCondition* _captureChangingCondition;
+ dispatch_queue_t _frameQueue;
+}
+
+@synthesize frameRotation = _framRotation;
+
+- (id)initWithOwner:(VideoCaptureIos*)owner {
+ if (self == [super init]) {
+ _owner = owner;
+ _captureSession = [[AVCaptureSession alloc] init];
+#if defined(WEBRTC_IOS)
+ _captureSession.usesApplicationAudioSession = NO;
+#endif
+ _captureChanging = NO;
+ _captureChangingCondition = [[NSCondition alloc] init];
+
+ if (!_captureSession || !_captureChangingCondition) {
+ return nil;
+ }
+
+ // create and configure a new output (using callbacks)
+ AVCaptureVideoDataOutput* captureOutput = [[AVCaptureVideoDataOutput alloc] init];
+ NSString* key = (NSString*)kCVPixelBufferPixelFormatTypeKey;
+
+ NSNumber* val = [NSNumber numberWithUnsignedInt:kCVPixelFormatType_422YpCbCr8];
+ NSDictionary* videoSettings = [NSDictionary dictionaryWithObject:val forKey:key];
+ captureOutput.videoSettings = videoSettings;
+
+ // add new output
+ if ([_captureSession canAddOutput:captureOutput]) {
+ [_captureSession addOutput:captureOutput];
+ } else {
+ RTC_LOG(LS_ERROR) << __FUNCTION__ << ": Could not add output to AVCaptureSession";
+ }
+
+#ifdef WEBRTC_IOS
+ [[UIDevice currentDevice] beginGeneratingDeviceOrientationNotifications];
+
+ NSNotificationCenter* notify = [NSNotificationCenter defaultCenter];
+ [notify addObserver:self
+ selector:@selector(onVideoError:)
+ name:AVCaptureSessionRuntimeErrorNotification
+ object:_captureSession];
+ [notify addObserver:self
+ selector:@selector(deviceOrientationDidChange:)
+ name:UIDeviceOrientationDidChangeNotification
+ object:nil];
+#endif
+ }
+
+ // Create a serial queue on which video capture will run. By setting the target,
+ // blocks should still run on DISPATH_QUEUE_PRIORITY_DEFAULT rather than creating
+ // a new thread.
+ _frameQueue = dispatch_queue_create("org.webrtc.videocapture", DISPATCH_QUEUE_SERIAL);
+ dispatch_set_target_queue(_frameQueue,
+ dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0));
+
+ return self;
+}
+
+- (void)directOutputToSelf {
+ [[self currentOutput] setSampleBufferDelegate:self queue:_frameQueue];
+}
+
+- (void)directOutputToNil {
+ [[self currentOutput] setSampleBufferDelegate:nil queue:NULL];
+}
+
+- (void)deviceOrientationDidChange:(NSNotification*)notification {
+ _orientationHasChanged = YES;
+ [self setRelativeVideoOrientation];
+}
+
+- (void)dealloc {
+ [[NSNotificationCenter defaultCenter] removeObserver:self];
+}
+
+- (BOOL)setCaptureDeviceByUniqueId:(NSString*)uniqueId {
+ [self waitForCaptureChangeToFinish];
+ // check to see if the camera is already set
+ if (_captureSession) {
+ NSArray* currentInputs = [NSArray arrayWithArray:[_captureSession inputs]];
+ if ([currentInputs count] > 0) {
+ AVCaptureDeviceInput* currentInput = [currentInputs objectAtIndex:0];
+ if ([uniqueId isEqualToString:[currentInput.device localizedName]]) {
+ return YES;
+ }
+ }
+ }
+
+ return [self changeCaptureInputByUniqueId:uniqueId];
+}
+
+- (BOOL)startCaptureWithCapability:(const VideoCaptureCapability&)capability {
+ [self waitForCaptureChangeToFinish];
+ if (!_captureSession) {
+ return NO;
+ }
+
+ // check limits of the resolution
+ if (capability.maxFPS < 0 || capability.maxFPS > 60) {
+ return NO;
+ }
+
+ if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset1280x720]) {
+ if (capability.width > 1280 || capability.height > 720) {
+ return NO;
+ }
+ } else if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset640x480]) {
+ if (capability.width > 640 || capability.height > 480) {
+ return NO;
+ }
+ } else if ([_captureSession canSetSessionPreset:AVCaptureSessionPreset352x288]) {
+ if (capability.width > 352 || capability.height > 288) {
+ return NO;
+ }
+ } else if (capability.width < 0 || capability.height < 0) {
+ return NO;
+ }
+
+ _capability = capability;
+
+ AVCaptureVideoDataOutput* currentOutput = [self currentOutput];
+ if (!currentOutput) return NO;
+
+ [self directOutputToSelf];
+
+ _orientationHasChanged = NO;
+ _captureChanging = YES;
+ dispatch_async(_frameQueue, ^{
+ [self startCaptureInBackgroundWithOutput:currentOutput];
+ });
+ return YES;
+}
+
+- (AVCaptureVideoDataOutput*)currentOutput {
+ return [[_captureSession outputs] firstObject];
+}
+
+- (void)startCaptureInBackgroundWithOutput:(AVCaptureVideoDataOutput*)currentOutput {
+ NSString* captureQuality = [NSString stringWithString:AVCaptureSessionPresetLow];
+ if (_capability.width >= 1280 || _capability.height >= 720) {
+ captureQuality = [NSString stringWithString:AVCaptureSessionPreset1280x720];
+ } else if (_capability.width >= 640 || _capability.height >= 480) {
+ captureQuality = [NSString stringWithString:AVCaptureSessionPreset640x480];
+ } else if (_capability.width >= 352 || _capability.height >= 288) {
+ captureQuality = [NSString stringWithString:AVCaptureSessionPreset352x288];
+ }
+
+ // begin configuration for the AVCaptureSession
+ [_captureSession beginConfiguration];
+
+ // picture resolution
+ [_captureSession setSessionPreset:captureQuality];
+
+ _connection = [currentOutput connectionWithMediaType:AVMediaTypeVideo];
+ [self setRelativeVideoOrientation];
+
+ // finished configuring, commit settings to AVCaptureSession.
+ [_captureSession commitConfiguration];
+
+ [_captureSession startRunning];
+ [self signalCaptureChangeEnd];
+}
+
+- (void)setRelativeVideoOrientation {
+ if (!_connection.supportsVideoOrientation) {
+ return;
+ }
+#ifndef WEBRTC_IOS
+ _connection.videoOrientation = AVCaptureVideoOrientationLandscapeRight;
+ return;
+#else
+ switch ([UIDevice currentDevice].orientation) {
+ case UIDeviceOrientationPortrait:
+ _connection.videoOrientation = AVCaptureVideoOrientationPortrait;
+ break;
+ case UIDeviceOrientationPortraitUpsideDown:
+ _connection.videoOrientation = AVCaptureVideoOrientationPortraitUpsideDown;
+ break;
+ case UIDeviceOrientationLandscapeLeft:
+ _connection.videoOrientation = AVCaptureVideoOrientationLandscapeRight;
+ break;
+ case UIDeviceOrientationLandscapeRight:
+ _connection.videoOrientation = AVCaptureVideoOrientationLandscapeLeft;
+ break;
+ case UIDeviceOrientationFaceUp:
+ case UIDeviceOrientationFaceDown:
+ case UIDeviceOrientationUnknown:
+ if (!_orientationHasChanged) {
+ _connection.videoOrientation = AVCaptureVideoOrientationPortrait;
+ }
+ break;
+ }
+#endif
+}
+
+- (void)onVideoError:(NSNotification*)notification {
+ NSLog(@"onVideoError: %@", notification);
+ // TODO(sjlee): make the specific error handling with this notification.
+ RTC_LOG(LS_ERROR) << __FUNCTION__ << ": [AVCaptureSession startRunning] error.";
+}
+
+- (BOOL)stopCapture {
+#ifdef WEBRTC_IOS
+ [[UIDevice currentDevice] endGeneratingDeviceOrientationNotifications];
+#endif
+ _orientationHasChanged = NO;
+ [self waitForCaptureChangeToFinish];
+ [self directOutputToNil];
+
+ if (!_captureSession) {
+ return NO;
+ }
+
+ _captureChanging = YES;
+ [_captureSession stopRunning];
+
+ dispatch_sync(_frameQueue, ^{
+ [self signalCaptureChangeEnd];
+ });
+ return YES;
+}
+
+- (BOOL)changeCaptureInputByUniqueId:(NSString*)uniqueId {
+ [self waitForCaptureChangeToFinish];
+ NSArray* currentInputs = [_captureSession inputs];
+ // remove current input
+ if ([currentInputs count] > 0) {
+ AVCaptureInput* currentInput = (AVCaptureInput*)[currentInputs objectAtIndex:0];
+
+ [_captureSession removeInput:currentInput];
+ }
+
+ // Look for input device with the name requested (as our input param)
+ // get list of available capture devices
+ int captureDeviceCount = [DeviceInfoIosObjC captureDeviceCount];
+ if (captureDeviceCount <= 0) {
+ return NO;
+ }
+
+ AVCaptureDevice* captureDevice = [DeviceInfoIosObjC captureDeviceForUniqueId:uniqueId];
+
+ if (!captureDevice) {
+ return NO;
+ }
+
+ // now create capture session input out of AVCaptureDevice
+ NSError* deviceError = nil;
+ AVCaptureDeviceInput* newCaptureInput = [AVCaptureDeviceInput deviceInputWithDevice:captureDevice
+ error:&deviceError];
+
+ if (!newCaptureInput) {
+ const char* errorMessage = [[deviceError localizedDescription] UTF8String];
+
+ RTC_LOG(LS_ERROR) << __FUNCTION__ << ": deviceInputWithDevice error:" << errorMessage;
+
+ return NO;
+ }
+
+ // try to add our new capture device to the capture session
+ [_captureSession beginConfiguration];
+
+ BOOL addedCaptureInput = NO;
+ if ([_captureSession canAddInput:newCaptureInput]) {
+ [_captureSession addInput:newCaptureInput];
+ addedCaptureInput = YES;
+ } else {
+ addedCaptureInput = NO;
+ }
+
+ [_captureSession commitConfiguration];
+
+ return addedCaptureInput;
+}
+
+- (void)captureOutput:(AVCaptureOutput*)captureOutput
+ didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer
+ fromConnection:(AVCaptureConnection*)connection {
+ const int kFlags = 0;
+ CVImageBufferRef videoFrame = CMSampleBufferGetImageBuffer(sampleBuffer);
+
+ if (CVPixelBufferLockBaseAddress(videoFrame, kFlags) != kCVReturnSuccess) {
+ return;
+ }
+
+ uint8_t* baseAddress = (uint8_t*)CVPixelBufferGetBaseAddress(videoFrame);
+ const size_t width = CVPixelBufferGetWidth(videoFrame);
+ const size_t height = CVPixelBufferGetHeight(videoFrame);
+ const size_t frameSize = width * height * 2;
+
+ VideoCaptureCapability tempCaptureCapability;
+ tempCaptureCapability.width = width;
+ tempCaptureCapability.height = height;
+ tempCaptureCapability.maxFPS = _capability.maxFPS;
+ tempCaptureCapability.videoType = VideoType::kUYVY;
+
+ _owner->IncomingFrame(baseAddress, frameSize, tempCaptureCapability, 0);
+
+ CVPixelBufferUnlockBaseAddress(videoFrame, kFlags);
+}
+
+- (void)signalCaptureChangeEnd {
+ [_captureChangingCondition lock];
+ _captureChanging = NO;
+ [_captureChangingCondition signal];
+ [_captureChangingCondition unlock];
+}
+
+- (void)waitForCaptureChangeToFinish {
+ [_captureChangingCondition lock];
+ while (_captureChanging) {
+ [_captureChangingCondition wait];
+ }
+ [_captureChangingCondition unlock];
+}
+@end
diff --git a/dom/media/systemservices/objc_video_capture/video_capture.h b/dom/media/systemservices/objc_video_capture/video_capture.h
new file mode 100644
index 0000000000..b9f228f679
--- /dev/null
+++ b/dom/media/systemservices/objc_video_capture/video_capture.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2013 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+#ifndef MODULES_VIDEO_CAPTURE_OBJC_VIDEO_CAPTURE_H_
+#define MODULES_VIDEO_CAPTURE_OBJC_VIDEO_CAPTURE_H_
+
+#include "modules/video_capture/video_capture_impl.h"
+#include "api/scoped_refptr.h"
+
+@class RTCVideoCaptureIosObjC;
+
+namespace webrtc::videocapturemodule {
+class VideoCaptureIos : public VideoCaptureImpl {
+ public:
+ VideoCaptureIos();
+ virtual ~VideoCaptureIos();
+
+ static rtc::scoped_refptr<VideoCaptureModule> Create(const char* device_unique_id_utf8);
+
+ // Implementation of VideoCaptureImpl.
+ int32_t StartCapture(const VideoCaptureCapability& capability) override;
+ int32_t StopCapture() override;
+ bool CaptureStarted() override;
+ int32_t CaptureSettings(VideoCaptureCapability& settings) override;
+
+ private:
+ RTCVideoCaptureIosObjC* capture_device_;
+ bool is_capturing_;
+ VideoCaptureCapability capability_;
+};
+
+} // namespace webrtc::videocapturemodule
+
+#endif // MODULES_VIDEO_CAPTURE_OBJC_VIDEO_CAPTURE_H_
diff --git a/dom/media/systemservices/objc_video_capture/video_capture.mm b/dom/media/systemservices/objc_video_capture/video_capture.mm
new file mode 100644
index 0000000000..63aef3204c
--- /dev/null
+++ b/dom/media/systemservices/objc_video_capture/video_capture.mm
@@ -0,0 +1,102 @@
+/*
+ * Copyright (c) 2013 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+#if !defined(__has_feature) || !__has_feature(objc_arc)
+# error "This file requires ARC support."
+#endif
+
+#include "device_info_objc.h"
+#include "rtc_video_capture_objc.h"
+#include "rtc_base/ref_counted_object.h"
+#include "api/scoped_refptr.h"
+#include "video_capture_avfoundation.h"
+#include "mozilla/StaticPrefs_media.h"
+
+using namespace mozilla;
+using namespace webrtc;
+using namespace videocapturemodule;
+
+rtc::scoped_refptr<VideoCaptureModule> VideoCaptureImpl::Create(const char* deviceUniqueIdUTF8) {
+ if (StaticPrefs::media_getusermedia_camera_macavf_enabled_AtStartup()) {
+ return VideoCaptureAvFoundation::Create(deviceUniqueIdUTF8);
+ }
+ return VideoCaptureIos::Create(deviceUniqueIdUTF8);
+}
+
+VideoCaptureIos::VideoCaptureIos() : is_capturing_(false) {
+ capability_.width = kDefaultWidth;
+ capability_.height = kDefaultHeight;
+ capability_.maxFPS = kDefaultFrameRate;
+ capture_device_ = nil;
+}
+
+VideoCaptureIos::~VideoCaptureIos() {
+ if (is_capturing_) {
+ [capture_device_ stopCapture];
+ capture_device_ = nil;
+ }
+}
+
+rtc::scoped_refptr<VideoCaptureModule> VideoCaptureIos::Create(const char* deviceUniqueIdUTF8) {
+ if (!deviceUniqueIdUTF8[0]) {
+ return NULL;
+ }
+
+ rtc::scoped_refptr<VideoCaptureIos> capture_module(new rtc::RefCountedObject<VideoCaptureIos>());
+
+ const int32_t name_length = strlen(deviceUniqueIdUTF8);
+ if (name_length >= kVideoCaptureUniqueNameLength) return nullptr;
+
+ capture_module->_deviceUniqueId = new char[name_length + 1];
+ strncpy(capture_module->_deviceUniqueId, deviceUniqueIdUTF8, name_length + 1);
+ capture_module->_deviceUniqueId[name_length] = '\0';
+
+ capture_module->capture_device_ =
+ [[RTCVideoCaptureIosObjC alloc] initWithOwner:capture_module.get()];
+ if (!capture_module->capture_device_) {
+ return nullptr;
+ }
+
+ if (![capture_module->capture_device_
+ setCaptureDeviceByUniqueId:[[NSString alloc] initWithCString:deviceUniqueIdUTF8
+ encoding:NSUTF8StringEncoding]]) {
+ return nullptr;
+ }
+ return capture_module;
+}
+
+int32_t VideoCaptureIos::StartCapture(const VideoCaptureCapability& capability) {
+ capability_ = capability;
+
+ if (![capture_device_ startCaptureWithCapability:capability]) {
+ return -1;
+ }
+
+ is_capturing_ = true;
+
+ return 0;
+}
+
+int32_t VideoCaptureIos::StopCapture() {
+ if (![capture_device_ stopCapture]) {
+ return -1;
+ }
+
+ is_capturing_ = false;
+ return 0;
+}
+
+bool VideoCaptureIos::CaptureStarted() { return is_capturing_; }
+
+int32_t VideoCaptureIos::CaptureSettings(VideoCaptureCapability& settings) {
+ settings = capability_;
+ settings.videoType = VideoType::kNV12;
+ return 0;
+}
diff --git a/dom/media/systemservices/objc_video_capture/video_capture_avfoundation.h b/dom/media/systemservices/objc_video_capture/video_capture_avfoundation.h
new file mode 100644
index 0000000000..f5a45b4531
--- /dev/null
+++ b/dom/media/systemservices/objc_video_capture/video_capture_avfoundation.h
@@ -0,0 +1,79 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_SYSTEMSERVICES_OBJC_VIDEO_CAPTURE_VIDEO_CAPTURE2_H_
+#define DOM_MEDIA_SYSTEMSERVICES_OBJC_VIDEO_CAPTURE_VIDEO_CAPTURE2_H_
+
+#import "components/capturer/RTCCameraVideoCapturer.h"
+
+#include "api/scoped_refptr.h"
+#include "api/sequence_checker.h"
+#include "modules/video_capture/video_capture_impl.h"
+#include "mozilla/Maybe.h"
+#include "PerformanceRecorder.h"
+
+@class VideoCaptureAdapter;
+
+namespace webrtc::videocapturemodule {
+
+/**
+ * VideoCaptureImpl implementation of the libwebrtc ios/mac sdk camera backend.
+ * Single threaded except for OnFrame() that happens on a platform callback thread.
+ */
+class VideoCaptureAvFoundation : public VideoCaptureImpl {
+ public:
+ VideoCaptureAvFoundation(AVCaptureDevice* _Nonnull aDevice);
+ virtual ~VideoCaptureAvFoundation();
+
+ static rtc::scoped_refptr<VideoCaptureModule> Create(const char* _Nullable aDeviceUniqueIdUTF8);
+
+ // Implementation of VideoCaptureImpl. Single threaded.
+
+ // Starts capturing synchronously. Idempotent. If an existing capture is live and another
+ // capability is requested we'll restart the underlying backend with the new capability.
+ int32_t StartCapture(const VideoCaptureCapability& aCapability) MOZ_EXCLUDES(api_lock_) override;
+ // Stops capturing synchronously. Idempotent.
+ int32_t StopCapture() MOZ_EXCLUDES(api_lock_) override;
+ bool CaptureStarted() MOZ_EXCLUDES(api_lock_) override;
+ int32_t CaptureSettings(VideoCaptureCapability& aSettings) override;
+
+ // Callback. This can be called on any thread.
+ int32_t OnFrame(__strong RTCVideoFrame* _Nonnull aFrame) MOZ_EXCLUDES(api_lock_);
+
+ void SetTrackingId(uint32_t aTrackingIdProcId) MOZ_EXCLUDES(api_lock_) override;
+
+ // Registers the current thread with the profiler if not already registered.
+ void MaybeRegisterCallbackThread();
+
+ private:
+ // Control thread checker.
+ SequenceChecker mChecker;
+ AVCaptureDevice* _Nonnull const mDevice RTC_GUARDED_BY(mChecker);
+ VideoCaptureAdapter* _Nonnull const mAdapter RTC_GUARDED_BY(mChecker);
+ RTCCameraVideoCapturer* _Nonnull const mCapturer RTC_GUARDED_BY(mChecker);
+ // If capture has started, this is the capability it was started for. Written on the mChecker
+ // thread only.
+ mozilla::Maybe<VideoCaptureCapability> mCapability MOZ_GUARDED_BY(api_lock_);
+ // The image type that mCapability maps to. Set in lockstep with mCapability.
+ mozilla::Maybe<mozilla::CaptureStage::ImageType> mImageType MOZ_GUARDED_BY(api_lock_);
+ // Id string uniquely identifying this capture source. Written on the mChecker thread only.
+ mozilla::Maybe<mozilla::TrackingId> mTrackingId MOZ_GUARDED_BY(api_lock_);
+ // Adds frame specific markers to the profiler while mTrackingId is set. Callback thread only.
+ mozilla::PerformanceRecorderMulti<mozilla::CaptureStage> mCaptureRecorder;
+ mozilla::PerformanceRecorderMulti<mozilla::CopyVideoStage> mConversionRecorder;
+ std::atomic<ProfilerThreadId> mCallbackThreadId;
+};
+
+} // namespace webrtc::videocapturemodule
+
+@interface VideoCaptureAdapter : NSObject <RTCVideoCapturerDelegate> {
+ webrtc::Mutex _mutex;
+ webrtc::videocapturemodule::VideoCaptureAvFoundation* _Nullable _capturer RTC_GUARDED_BY(_mutex);
+}
+- (void)setCapturer:(webrtc::videocapturemodule::VideoCaptureAvFoundation* _Nullable)capturer;
+@end
+
+#endif
diff --git a/dom/media/systemservices/objc_video_capture/video_capture_avfoundation.mm b/dom/media/systemservices/objc_video_capture/video_capture_avfoundation.mm
new file mode 100644
index 0000000000..e5ca826fa4
--- /dev/null
+++ b/dom/media/systemservices/objc_video_capture/video_capture_avfoundation.mm
@@ -0,0 +1,306 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "video_capture_avfoundation.h"
+
+#import "api/video_frame_buffer/RTCNativeI420Buffer+Private.h"
+#import "base/RTCI420Buffer.h"
+#import "base/RTCVideoFrame.h"
+#import "base/RTCVideoFrameBuffer.h"
+#import "components/capturer/RTCCameraVideoCapturer.h"
+#import "helpers/NSString+StdString.h"
+
+#include "api/scoped_refptr.h"
+#include "api/video/video_rotation.h"
+#include "CallbackThreadRegistry.h"
+#include "device_info_avfoundation.h"
+#include "modules/video_capture/video_capture_defines.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/UniquePtr.h"
+#include "rtc_base/time_utils.h"
+
+using namespace mozilla;
+using namespace webrtc::videocapturemodule;
+
+namespace {
+webrtc::VideoRotation ToNativeRotation(RTCVideoRotation aRotation) {
+ switch (aRotation) {
+ case RTCVideoRotation_0:
+ return webrtc::kVideoRotation_0;
+ case RTCVideoRotation_90:
+ return webrtc::kVideoRotation_90;
+ case RTCVideoRotation_180:
+ return webrtc::kVideoRotation_180;
+ case RTCVideoRotation_270:
+ return webrtc::kVideoRotation_270;
+ default:
+ MOZ_CRASH_UNSAFE_PRINTF("Unexpected rotation %d", static_cast<int>(aRotation));
+ return webrtc::kVideoRotation_0;
+ }
+}
+
+AVCaptureDeviceFormat* _Nullable FindFormat(AVCaptureDevice* _Nonnull aDevice,
+ webrtc::VideoCaptureCapability aCapability) {
+ for (AVCaptureDeviceFormat* format in [aDevice formats]) {
+ CMVideoDimensions dimensions = CMVideoFormatDescriptionGetDimensions(format.formatDescription);
+ if (dimensions.width != aCapability.width) {
+ continue;
+ }
+ if (dimensions.height != aCapability.height) {
+ continue;
+ }
+ FourCharCode fourcc = CMFormatDescriptionGetMediaSubType(format.formatDescription);
+ if (aCapability.videoType != DeviceInfoAvFoundation::ConvertFourCCToVideoType(fourcc)) {
+ continue;
+ }
+ if ([format.videoSupportedFrameRateRanges
+ indexOfObjectPassingTest:^BOOL(AVFrameRateRange* _Nonnull obj, NSUInteger idx,
+ BOOL* _Nonnull stop) {
+ return static_cast<BOOL>(DeviceInfoAvFoundation::ConvertAVFrameRateToCapabilityFPS(
+ obj.maxFrameRate) == aCapability.maxFPS);
+ }] == NSNotFound) {
+ continue;
+ }
+
+ return format;
+ }
+ return nullptr;
+}
+} // namespace
+
+@implementation VideoCaptureAdapter
+- (void)setCapturer:(webrtc::videocapturemodule::VideoCaptureAvFoundation* _Nullable)capturer {
+ webrtc::MutexLock lock(&_mutex);
+ _capturer = capturer;
+}
+
+- (void)capturer:(RTCVideoCapturer* _Nonnull)capturer
+ didCaptureVideoFrame:(RTCVideoFrame* _Nonnull)frame {
+ rtc::scoped_refptr<webrtc::videocapturemodule::VideoCaptureAvFoundation> cap;
+ {
+ webrtc::MutexLock lock(&_mutex);
+ cap = rtc::scoped_refptr(_capturer);
+ }
+ if (!cap) return;
+ cap->OnFrame(frame);
+}
+@end
+
+namespace webrtc::videocapturemodule {
+VideoCaptureAvFoundation::VideoCaptureAvFoundation(AVCaptureDevice* _Nonnull aDevice)
+ : mDevice(aDevice),
+ mAdapter([[VideoCaptureAdapter alloc] init]),
+ mCapturer([[RTC_OBJC_TYPE(RTCCameraVideoCapturer) alloc] initWithDelegate:mAdapter]),
+ mCallbackThreadId() {
+ const char* uniqueId = [[aDevice uniqueID] UTF8String];
+ size_t len = strlen(uniqueId);
+ _deviceUniqueId = new (std::nothrow) char[len + 1];
+ if (_deviceUniqueId) {
+ memcpy(_deviceUniqueId, uniqueId, len + 1);
+ }
+}
+
+VideoCaptureAvFoundation::~VideoCaptureAvFoundation() {
+ // Must block until capture has fully stopped, including async operations.
+ StopCapture();
+}
+
+/* static */
+rtc::scoped_refptr<VideoCaptureModule> VideoCaptureAvFoundation::Create(
+ const char* _Nullable aDeviceUniqueIdUTF8) {
+ std::string uniqueId(aDeviceUniqueIdUTF8);
+ for (AVCaptureDevice* device in [RTCCameraVideoCapturer captureDevices]) {
+ if ([NSString stdStringForString:device.uniqueID] == uniqueId) {
+ rtc::scoped_refptr<VideoCaptureModule> module(
+ new rtc::RefCountedObject<VideoCaptureAvFoundation>(device));
+ return module;
+ }
+ }
+ return nullptr;
+}
+
+int32_t VideoCaptureAvFoundation::StartCapture(const VideoCaptureCapability& aCapability) {
+ RTC_DCHECK_RUN_ON(&mChecker);
+ AVCaptureDeviceFormat* format = FindFormat(mDevice, aCapability);
+ if (!format) {
+ return -1;
+ }
+
+ {
+ MutexLock lock(&api_lock_);
+ if (mCapability) {
+ if (mCapability->width == aCapability.width && mCapability->height == aCapability.height &&
+ mCapability->maxFPS == aCapability.maxFPS &&
+ mCapability->videoType == aCapability.videoType) {
+ return 0;
+ }
+
+ api_lock_.Unlock();
+ int32_t rv = StopCapture();
+ api_lock_.Lock();
+
+ if (rv != 0) {
+ return rv;
+ }
+ }
+ }
+
+ [mAdapter setCapturer:this];
+
+ {
+ Monitor monitor("VideoCaptureAVFoundation::StartCapture");
+ Monitor* copyableMonitor = &monitor;
+ MonitorAutoLock lock(monitor);
+ __block Maybe<int32_t> rv;
+
+ [mCapturer startCaptureWithDevice:mDevice
+ format:format
+ fps:aCapability.maxFPS
+ completionHandler:^(NSError* error) {
+ MonitorAutoLock lock2(*copyableMonitor);
+ MOZ_RELEASE_ASSERT(!rv);
+ rv = Some(error ? -1 : 0);
+ copyableMonitor->Notify();
+ }];
+
+ while (!rv) {
+ monitor.Wait();
+ }
+
+ if (*rv != 0) {
+ return *rv;
+ }
+ }
+
+ MutexLock lock(&api_lock_);
+ mCapability = Some(aCapability);
+ mImageType = Some([type = aCapability.videoType] {
+ switch (type) {
+ case webrtc::VideoType::kI420:
+ return CaptureStage::ImageType::I420;
+ case webrtc::VideoType::kYUY2:
+ return CaptureStage::ImageType::YUY2;
+ case webrtc::VideoType::kYV12:
+ case webrtc::VideoType::kIYUV:
+ return CaptureStage::ImageType::YV12;
+ case webrtc::VideoType::kUYVY:
+ return CaptureStage::ImageType::UYVY;
+ case webrtc::VideoType::kNV12:
+ return CaptureStage::ImageType::NV12;
+ case webrtc::VideoType::kNV21:
+ return CaptureStage::ImageType::NV21;
+ case webrtc::VideoType::kMJPEG:
+ return CaptureStage::ImageType::MJPEG;
+ case webrtc::VideoType::kRGB24:
+ case webrtc::VideoType::kBGR24:
+ case webrtc::VideoType::kABGR:
+ case webrtc::VideoType::kARGB:
+ case webrtc::VideoType::kARGB4444:
+ case webrtc::VideoType::kRGB565:
+ case webrtc::VideoType::kARGB1555:
+ case webrtc::VideoType::kBGRA:
+ case webrtc::VideoType::kUnknown:
+ // Unlikely, and not represented by CaptureStage::ImageType.
+ return CaptureStage::ImageType::Unknown;
+ }
+ return CaptureStage::ImageType::Unknown;
+ }());
+
+ return 0;
+}
+
+int32_t VideoCaptureAvFoundation::StopCapture() {
+ RTC_DCHECK_RUN_ON(&mChecker);
+ {
+ MutexLock lock(&api_lock_);
+ if (!mCapability) {
+ return 0;
+ }
+ mCapability = Nothing();
+ }
+
+ Monitor monitor("VideoCaptureAVFoundation::StopCapture");
+ Monitor* copyableMonitor = &monitor;
+ MonitorAutoLock lock(monitor);
+ __block bool done = false;
+
+ [mCapturer stopCaptureWithCompletionHandler:^(void) {
+ MonitorAutoLock lock2(*copyableMonitor);
+ MOZ_RELEASE_ASSERT(!done);
+ done = true;
+ copyableMonitor->Notify();
+ }];
+
+ while (!done) {
+ monitor.Wait();
+ }
+
+ [mAdapter setCapturer:nil];
+
+ return 0;
+}
+
+bool VideoCaptureAvFoundation::CaptureStarted() {
+ RTC_DCHECK_RUN_ON(&mChecker);
+ MutexLock lock(&api_lock_);
+ return mCapability.isSome();
+}
+
+int32_t VideoCaptureAvFoundation::CaptureSettings(VideoCaptureCapability& aSettings) {
+ MOZ_CRASH("Unexpected call");
+ return -1;
+}
+
+int32_t VideoCaptureAvFoundation::OnFrame(__strong RTCVideoFrame* _Nonnull aFrame) {
+ MaybeRegisterCallbackThread();
+ if (MutexLock lock(&api_lock_); MOZ_LIKELY(mTrackingId)) {
+ mCaptureRecorder.Start(0, "VideoCaptureAVFoundation"_ns, *mTrackingId, aFrame.width,
+ aFrame.height, mImageType.valueOr(CaptureStage::ImageType::Unknown));
+ if (mCapability && mCapability->videoType != webrtc::VideoType::kI420) {
+ mConversionRecorder.Start(0, "VideoCaptureAVFoundation"_ns, *mTrackingId, aFrame.width,
+ aFrame.height);
+ }
+ }
+
+ const int64_t timestamp_us = aFrame.timeStampNs / rtc::kNumNanosecsPerMicrosec;
+ RTCI420Buffer* buffer = [aFrame.buffer toI420];
+ mConversionRecorder.Record(0);
+ // Accessing the (intended-to-be-private) native buffer directly is hacky but lets us skip two
+ // copies
+ rtc::scoped_refptr<webrtc::I420BufferInterface> nativeBuffer = buffer.nativeI420Buffer;
+ auto frame = webrtc::VideoFrame::Builder()
+ .set_video_frame_buffer(nativeBuffer)
+ .set_rotation(ToNativeRotation(aFrame.rotation))
+ .set_timestamp_us(timestamp_us)
+ .build();
+
+ MutexLock lock(&api_lock_);
+ int32_t rv = DeliverCapturedFrame(frame);
+ mCaptureRecorder.Record(0);
+ return rv;
+}
+
+void VideoCaptureAvFoundation::SetTrackingId(uint32_t aTrackingIdProcId) {
+ RTC_DCHECK_RUN_ON(&mChecker);
+ MutexLock lock(&api_lock_);
+ if (NS_WARN_IF(mTrackingId.isSome())) {
+ // This capture instance must be shared across multiple camera requests. For now ignore other
+ // requests than the first.
+ return;
+ }
+ mTrackingId.emplace(TrackingId::Source::Camera, aTrackingIdProcId);
+}
+
+void VideoCaptureAvFoundation::MaybeRegisterCallbackThread() {
+ ProfilerThreadId id = profiler_current_thread_id();
+ if (MOZ_LIKELY(id == mCallbackThreadId)) {
+ return;
+ }
+ mCallbackThreadId = id;
+ CallbackThreadRegistry::Get()->Register(mCallbackThreadId, "VideoCaptureAVFoundationCallback");
+}
+} // namespace webrtc::videocapturemodule
diff --git a/dom/media/systemservices/video_engine/browser_capture_impl.h b/dom/media/systemservices/video_engine/browser_capture_impl.h
new file mode 100644
index 0000000000..aeaae62202
--- /dev/null
+++ b/dom/media/systemservices/video_engine/browser_capture_impl.h
@@ -0,0 +1,78 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef WEBRTC_MODULES_BROWSER_CAPTURE_MAIN_SOURCE_BROWSER_CAPTURE_IMPL_H_
+#define WEBRTC_MODULES_BROWSER_CAPTURE_MAIN_SOURCE_BROWSER_CAPTURE_IMPL_H_
+
+#include "webrtc/modules/video_capture/video_capture.h"
+
+using namespace webrtc::videocapturemodule;
+
+namespace webrtc {
+
+class BrowserDeviceInfoImpl : public VideoCaptureModule::DeviceInfo {
+ public:
+ virtual uint32_t NumberOfDevices() { return 1; }
+
+ virtual int32_t Refresh() { return 0; }
+
+ virtual int32_t GetDeviceName(uint32_t deviceNumber, char* deviceNameUTF8,
+ uint32_t deviceNameSize,
+ char* deviceUniqueIdUTF8,
+ uint32_t deviceUniqueIdUTF8Size,
+ char* productUniqueIdUTF8 = NULL,
+ uint32_t productUniqueIdUTF8Size = 0,
+ pid_t* pid = 0) {
+ deviceNameUTF8 = const_cast<char*>(kDeviceName);
+ deviceUniqueIdUTF8 = const_cast<char*>(kUniqueDeviceName);
+ productUniqueIdUTF8 = const_cast<char*>(kProductUniqueId);
+ return 1;
+ };
+
+ virtual int32_t NumberOfCapabilities(const char* deviceUniqueIdUTF8) {
+ return 0;
+ }
+
+ virtual int32_t GetCapability(const char* deviceUniqueIdUTF8,
+ const uint32_t deviceCapabilityNumber,
+ VideoCaptureCapability& capability) {
+ return 0;
+ };
+
+ virtual int32_t GetOrientation(const char* deviceUniqueIdUTF8,
+ VideoRotation& orientation) {
+ return 0;
+ }
+
+ virtual int32_t GetBestMatchedCapability(
+ const char* deviceUniqueIdUTF8, const VideoCaptureCapability& requested,
+ VideoCaptureCapability& resulting) {
+ return 0;
+ }
+
+ virtual int32_t DisplayCaptureSettingsDialogBox(
+ const char* deviceUniqueIdUTF8, const char* dialogTitleUTF8,
+ void* parentWindow, uint32_t positionX, uint32_t positionY) {
+ return 0;
+ }
+
+ BrowserDeviceInfoImpl()
+ : kDeviceName("browser"),
+ kUniqueDeviceName("browser"),
+ kProductUniqueId("browser") {}
+
+ static BrowserDeviceInfoImpl* CreateDeviceInfo() {
+ return new BrowserDeviceInfoImpl();
+ }
+ virtual ~BrowserDeviceInfoImpl() {}
+
+ private:
+ const char* kDeviceName;
+ const char* kUniqueDeviceName;
+ const char* kProductUniqueId;
+};
+} // namespace webrtc
+#endif
diff --git a/dom/media/systemservices/video_engine/desktop_capture_impl.cc b/dom/media/systemservices/video_engine/desktop_capture_impl.cc
new file mode 100644
index 0000000000..2274a21e8a
--- /dev/null
+++ b/dom/media/systemservices/video_engine/desktop_capture_impl.cc
@@ -0,0 +1,776 @@
+/*
+ * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+#include "video_engine/desktop_capture_impl.h"
+
+#include <cstdlib>
+#include <memory>
+#include <string>
+
+#include "CamerasTypes.h"
+#include "VideoEngine.h"
+#include "VideoUtils.h"
+#include "api/video/i420_buffer.h"
+#include "common_video/libyuv/include/webrtc_libyuv.h"
+#include "libyuv.h" // NOLINT
+#include "modules/include/module_common_types.h"
+#include "modules/video_capture/video_capture_config.h"
+#include "modules/video_capture/video_capture_impl.h"
+#include "system_wrappers/include/clock.h"
+#include "rtc_base/logging.h"
+#include "rtc_base/ref_counted_object.h"
+#include "rtc_base/time_utils.h"
+#include "rtc_base/trace_event.h"
+#include "modules/desktop_capture/desktop_and_cursor_composer.h"
+#include "modules/desktop_capture/desktop_frame.h"
+#include "modules/desktop_capture/desktop_capture_options.h"
+#include "modules/desktop_capture/desktop_capturer_differ_wrapper.h"
+#include "modules/video_capture/video_capture.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/SyncRunnable.h"
+#include "mozilla/TaskQueue.h"
+#include "mozilla/TimeStamp.h"
+#include "nsThreadUtils.h"
+#include "tab_capturer.h"
+
+using mozilla::NewRunnableMethod;
+using mozilla::TabCapturerWebrtc;
+using mozilla::TimeDuration;
+using mozilla::camera::CaptureDeviceType;
+using mozilla::camera::CaptureEngine;
+
+static void CaptureFrameOnThread(nsITimer* aTimer, void* aClosure) {
+ static_cast<webrtc::DesktopCaptureImpl*>(aClosure)->CaptureFrameOnThread();
+}
+
+namespace webrtc {
+
+int32_t ScreenDeviceInfoImpl::Init() {
+ mDesktopDeviceInfo =
+ std::unique_ptr<DesktopDeviceInfo>(DesktopDeviceInfo::Create());
+ return 0;
+}
+
+int32_t ScreenDeviceInfoImpl::Refresh() {
+ mDesktopDeviceInfo->Refresh();
+ return 0;
+}
+
+uint32_t ScreenDeviceInfoImpl::NumberOfDevices() {
+ return mDesktopDeviceInfo->getDisplayDeviceCount();
+}
+
+int32_t ScreenDeviceInfoImpl::GetDeviceName(
+ uint32_t aDeviceNumber, char* aDeviceNameUTF8, uint32_t aDeviceNameUTF8Size,
+ char* aDeviceUniqueIdUTF8, uint32_t aDeviceUniqueIdUTF8Size,
+ char* aProductUniqueIdUTF8, uint32_t aProductUniqueIdUTF8Size,
+ pid_t* aPid) {
+ DesktopDisplayDevice desktopDisplayDevice;
+
+ // always initialize output
+ if (aDeviceNameUTF8 && aDeviceNameUTF8Size > 0) {
+ memset(aDeviceNameUTF8, 0, aDeviceNameUTF8Size);
+ }
+
+ if (aDeviceUniqueIdUTF8 && aDeviceUniqueIdUTF8Size > 0) {
+ memset(aDeviceUniqueIdUTF8, 0, aDeviceUniqueIdUTF8Size);
+ }
+ if (aProductUniqueIdUTF8 && aProductUniqueIdUTF8Size > 0) {
+ memset(aProductUniqueIdUTF8, 0, aProductUniqueIdUTF8Size);
+ }
+
+ if (mDesktopDeviceInfo->getDesktopDisplayDeviceInfo(
+ aDeviceNumber, desktopDisplayDevice) == 0) {
+ size_t len;
+
+ const char* deviceName = desktopDisplayDevice.getDeviceName();
+ len = deviceName ? strlen(deviceName) : 0;
+ if (len && aDeviceNameUTF8 && len < aDeviceNameUTF8Size) {
+ memcpy(aDeviceNameUTF8, deviceName, len);
+ }
+
+ const char* deviceUniqueId = desktopDisplayDevice.getUniqueIdName();
+ len = deviceUniqueId ? strlen(deviceUniqueId) : 0;
+ if (len && aDeviceUniqueIdUTF8 && len < aDeviceUniqueIdUTF8Size) {
+ memcpy(aDeviceUniqueIdUTF8, deviceUniqueId, len);
+ }
+ }
+
+ return 0;
+}
+
+int32_t ScreenDeviceInfoImpl::DisplayCaptureSettingsDialogBox(
+ const char* aDeviceUniqueIdUTF8, const char* aDialogTitleUTF8,
+ void* aParentWindow, uint32_t aPositionX, uint32_t aPositionY) {
+ // no device properties to change
+ return 0;
+}
+
+int32_t ScreenDeviceInfoImpl::NumberOfCapabilities(
+ const char* aDeviceUniqueIdUTF8) {
+ return 0;
+}
+
+int32_t ScreenDeviceInfoImpl::GetCapability(
+ const char* aDeviceUniqueIdUTF8, uint32_t aDeviceCapabilityNumber,
+ VideoCaptureCapability& aCapability) {
+ return 0;
+}
+
+int32_t ScreenDeviceInfoImpl::GetBestMatchedCapability(
+ const char* aDeviceUniqueIdUTF8, const VideoCaptureCapability& aRequested,
+ VideoCaptureCapability& aResulting) {
+ return 0;
+}
+
+int32_t ScreenDeviceInfoImpl::GetOrientation(const char* aDeviceUniqueIdUTF8,
+ VideoRotation& aOrientation) {
+ return 0;
+}
+
+VideoCaptureModule* DesktopCaptureImpl::Create(const int32_t aModuleId,
+ const char* aUniqueId,
+ const CaptureDeviceType aType) {
+ return new rtc::RefCountedObject<DesktopCaptureImpl>(aModuleId, aUniqueId,
+ aType);
+}
+
+int32_t WindowDeviceInfoImpl::Init() {
+ mDesktopDeviceInfo =
+ std::unique_ptr<DesktopDeviceInfo>(DesktopDeviceInfo::Create());
+ return 0;
+}
+
+int32_t WindowDeviceInfoImpl::Refresh() {
+ mDesktopDeviceInfo->Refresh();
+ return 0;
+}
+
+uint32_t WindowDeviceInfoImpl::NumberOfDevices() {
+ return mDesktopDeviceInfo->getWindowCount();
+}
+
+int32_t WindowDeviceInfoImpl::GetDeviceName(
+ uint32_t aDeviceNumber, char* aDeviceNameUTF8, uint32_t aDeviceNameUTF8Size,
+ char* aDeviceUniqueIdUTF8, uint32_t aDeviceUniqueIdUTF8Size,
+ char* aProductUniqueIdUTF8, uint32_t aProductUniqueIdUTF8Size,
+ pid_t* aPid) {
+ DesktopDisplayDevice desktopDisplayDevice;
+
+ // always initialize output
+ if (aDeviceNameUTF8 && aDeviceNameUTF8Size > 0) {
+ memset(aDeviceNameUTF8, 0, aDeviceNameUTF8Size);
+ }
+ if (aDeviceUniqueIdUTF8 && aDeviceUniqueIdUTF8Size > 0) {
+ memset(aDeviceUniqueIdUTF8, 0, aDeviceUniqueIdUTF8Size);
+ }
+ if (aProductUniqueIdUTF8 && aProductUniqueIdUTF8Size > 0) {
+ memset(aProductUniqueIdUTF8, 0, aProductUniqueIdUTF8Size);
+ }
+
+ if (mDesktopDeviceInfo->getWindowInfo(aDeviceNumber, desktopDisplayDevice) ==
+ 0) {
+ size_t len;
+
+ const char* deviceName = desktopDisplayDevice.getDeviceName();
+ len = deviceName ? strlen(deviceName) : 0;
+ if (len && aDeviceNameUTF8 && len < aDeviceNameUTF8Size) {
+ memcpy(aDeviceNameUTF8, deviceName, len);
+ }
+
+ const char* deviceUniqueId = desktopDisplayDevice.getUniqueIdName();
+ len = deviceUniqueId ? strlen(deviceUniqueId) : 0;
+ if (len && aDeviceUniqueIdUTF8 && len < aDeviceUniqueIdUTF8Size) {
+ memcpy(aDeviceUniqueIdUTF8, deviceUniqueId, len);
+ }
+ if (aPid) {
+ *aPid = desktopDisplayDevice.getPid();
+ }
+ }
+
+ return 0;
+}
+
+int32_t WindowDeviceInfoImpl::DisplayCaptureSettingsDialogBox(
+ const char* aDeviceUniqueIdUTF8, const char* aDialogTitleUTF8,
+ void* aParentWindow, uint32_t aPositionX, uint32_t aPositionY) {
+ // no device properties to change
+ return 0;
+}
+
+int32_t WindowDeviceInfoImpl::NumberOfCapabilities(
+ const char* aDeviceUniqueIdUTF8) {
+ return 0;
+}
+
+int32_t WindowDeviceInfoImpl::GetCapability(
+ const char* aDeviceUniqueIdUTF8, uint32_t aDeviceCapabilityNumber,
+ VideoCaptureCapability& aCapability) {
+ return 0;
+}
+
+int32_t WindowDeviceInfoImpl::GetBestMatchedCapability(
+ const char* aDeviceUniqueIdUTF8, const VideoCaptureCapability& aRequested,
+ VideoCaptureCapability& aResulting) {
+ return 0;
+}
+
+int32_t WindowDeviceInfoImpl::GetOrientation(const char* aDeviceUniqueIdUTF8,
+ VideoRotation& aOrientation) {
+ return 0;
+}
+
+int32_t BrowserDeviceInfoImpl::Init() {
+ mDesktopDeviceInfo =
+ std::unique_ptr<DesktopDeviceInfo>(DesktopDeviceInfo::Create());
+ return 0;
+}
+
+int32_t BrowserDeviceInfoImpl::Refresh() {
+ mDesktopDeviceInfo->Refresh();
+ return 0;
+}
+
+uint32_t BrowserDeviceInfoImpl::NumberOfDevices() {
+ return mDesktopDeviceInfo->getTabCount();
+}
+
+int32_t BrowserDeviceInfoImpl::GetDeviceName(
+ uint32_t aDeviceNumber, char* aDeviceNameUTF8, uint32_t aDeviceNameUTF8Size,
+ char* aDeviceUniqueIdUTF8, uint32_t aDeviceUniqueIdUTF8Size,
+ char* aProductUniqueIdUTF8, uint32_t aProductUniqueIdUTF8Size,
+ pid_t* aPid) {
+ DesktopTab desktopTab;
+
+ // always initialize output
+ if (aDeviceNameUTF8 && aDeviceNameUTF8Size > 0) {
+ memset(aDeviceNameUTF8, 0, aDeviceNameUTF8Size);
+ }
+ if (aDeviceUniqueIdUTF8 && aDeviceUniqueIdUTF8Size > 0) {
+ memset(aDeviceUniqueIdUTF8, 0, aDeviceUniqueIdUTF8Size);
+ }
+ if (aProductUniqueIdUTF8 && aProductUniqueIdUTF8Size > 0) {
+ memset(aProductUniqueIdUTF8, 0, aProductUniqueIdUTF8Size);
+ }
+
+ if (mDesktopDeviceInfo->getTabInfo(aDeviceNumber, desktopTab) == 0) {
+ size_t len;
+
+ const char* deviceName = desktopTab.getTabName();
+ len = deviceName ? strlen(deviceName) : 0;
+ if (len && aDeviceNameUTF8 && len < aDeviceNameUTF8Size) {
+ memcpy(aDeviceNameUTF8, deviceName, len);
+ }
+
+ const char* deviceUniqueId = desktopTab.getUniqueIdName();
+ len = deviceUniqueId ? strlen(deviceUniqueId) : 0;
+ if (len && aDeviceUniqueIdUTF8 && len < aDeviceUniqueIdUTF8Size) {
+ memcpy(aDeviceUniqueIdUTF8, deviceUniqueId, len);
+ }
+ }
+
+ return 0;
+}
+
+int32_t BrowserDeviceInfoImpl::DisplayCaptureSettingsDialogBox(
+ const char* aDeviceUniqueIdUTF8, const char* aDialogTitleUTF8,
+ void* aParentWindow, uint32_t aPositionX, uint32_t aPositionY) {
+ // no device properties to change
+ return 0;
+}
+
+int32_t BrowserDeviceInfoImpl::NumberOfCapabilities(
+ const char* aDeviceUniqueIdUTF8) {
+ return 0;
+}
+
+int32_t BrowserDeviceInfoImpl::GetCapability(
+ const char* aDeviceUniqueIdUTF8, uint32_t aDeviceCapabilityNumber,
+ VideoCaptureCapability& aCapability) {
+ return 0;
+}
+
+int32_t BrowserDeviceInfoImpl::GetBestMatchedCapability(
+ const char* aDeviceUniqueIdUTF8, const VideoCaptureCapability& aRequested,
+ VideoCaptureCapability& aResulting) {
+ return 0;
+}
+
+int32_t BrowserDeviceInfoImpl::GetOrientation(const char* aDeviceUniqueIdUTF8,
+ VideoRotation& aOrientation) {
+ return 0;
+}
+
+std::shared_ptr<VideoCaptureModule::DeviceInfo>
+DesktopCaptureImpl::CreateDeviceInfo(const int32_t aId,
+ const CaptureDeviceType aType) {
+ if (aType == CaptureDeviceType::Screen) {
+ auto screenInfo = std::make_shared<ScreenDeviceInfoImpl>(aId);
+ if (!screenInfo || screenInfo->Init() != 0) {
+ return nullptr;
+ }
+ return screenInfo;
+ }
+ if (aType == CaptureDeviceType::Window) {
+ auto windowInfo = std::make_shared<WindowDeviceInfoImpl>(aId);
+ if (!windowInfo || windowInfo->Init() != 0) {
+ return nullptr;
+ }
+ return windowInfo;
+ }
+ if (aType == CaptureDeviceType::Browser) {
+ auto browserInfo = std::make_shared<BrowserDeviceInfoImpl>(aId);
+ if (!browserInfo || browserInfo->Init() != 0) {
+ return nullptr;
+ }
+ return browserInfo;
+ }
+ return nullptr;
+}
+
+const char* DesktopCaptureImpl::CurrentDeviceName() const {
+ return mDeviceUniqueId.c_str();
+}
+
+static DesktopCaptureOptions CreateDesktopCaptureOptions() {
+ DesktopCaptureOptions options;
+// Help avoid an X11 deadlock, see bug 1456101.
+#ifdef MOZ_X11
+ MOZ_ALWAYS_SUCCEEDS(mozilla::SyncRunnable::DispatchToThread(
+ mozilla::GetMainThreadSerialEventTarget(),
+ NS_NewRunnableFunction(__func__, [&] {
+ options = DesktopCaptureOptions::CreateDefault();
+ })));
+#else
+ options = DesktopCaptureOptions::CreateDefault();
+#endif
+
+ // Leave desktop effects enabled during WebRTC captures.
+ options.set_disable_effects(false);
+
+#if defined(WEBRTC_WIN)
+ if (mozilla::StaticPrefs::media_webrtc_capture_allow_directx()) {
+ options.set_allow_directx_capturer(true);
+ options.set_allow_use_magnification_api(false);
+ } else {
+ options.set_allow_use_magnification_api(true);
+ }
+ options.set_allow_cropping_window_capturer(true);
+# if defined(RTC_ENABLE_WIN_WGC)
+ if (mozilla::StaticPrefs::media_webrtc_capture_allow_wgc()) {
+ options.set_allow_wgc_capturer(true);
+ }
+# endif
+#endif
+
+#if defined(WEBRTC_MAC)
+ if (mozilla::StaticPrefs::media_webrtc_capture_allow_iosurface()) {
+ options.set_allow_iosurface(true);
+ }
+#endif
+
+#if defined(WEBRTC_USE_PIPEWIRE)
+ if (mozilla::StaticPrefs::media_webrtc_capture_allow_pipewire()) {
+ options.set_allow_pipewire(true);
+ }
+#endif
+
+ return options;
+}
+
+static std::unique_ptr<DesktopCapturer> CreateTabCapturer(
+ const DesktopCaptureOptions& options, DesktopCapturer::SourceId aSourceId,
+ nsCOMPtr<nsISerialEventTarget> aCaptureThread) {
+ std::unique_ptr<DesktopCapturer> capturer =
+ TabCapturerWebrtc::Create(aSourceId, std::move(aCaptureThread));
+ if (capturer && options.detect_updated_region()) {
+ capturer.reset(new DesktopCapturerDifferWrapper(std::move(capturer)));
+ }
+
+ return capturer;
+}
+
+static bool UsePipewire() {
+#if defined(WEBRTC_USE_PIPEWIRE)
+ return mozilla::StaticPrefs::media_webrtc_capture_allow_pipewire() &&
+ webrtc::DesktopCapturer::IsRunningUnderWayland();
+#else
+ return false;
+#endif
+}
+
+static std::unique_ptr<DesktopCapturer> CreateDesktopCapturerAndThread(
+ CaptureDeviceType aDeviceType, DesktopCapturer::SourceId aSourceId,
+ nsIThread** aOutThread) {
+ DesktopCaptureOptions options = CreateDesktopCaptureOptions();
+ std::unique_ptr<DesktopCapturer> capturer;
+
+ auto ensureThread = [&]() {
+ if (*aOutThread) {
+ return *aOutThread;
+ }
+
+ nsIThreadManager::ThreadCreationOptions threadOptions;
+#if defined(XP_WIN) || defined(XP_MACOSX)
+ // Windows desktop capture needs a UI thread.
+ // Mac screen capture needs a thread with a CFRunLoop.
+ threadOptions.isUiThread = true;
+#endif
+ NS_NewNamedThread("DesktopCapture", aOutThread, nullptr, threadOptions);
+ return *aOutThread;
+ };
+
+ if ((aDeviceType == CaptureDeviceType::Screen ||
+ aDeviceType == CaptureDeviceType::Window) &&
+ UsePipewire()) {
+ capturer = DesktopCapturer::CreateGenericCapturer(options);
+ if (!capturer) {
+ return capturer;
+ }
+
+ capturer = std::make_unique<DesktopAndCursorComposer>(std::move(capturer),
+ options);
+ } else if (aDeviceType == CaptureDeviceType::Screen) {
+ capturer = DesktopCapturer::CreateScreenCapturer(options);
+ if (!capturer) {
+ return capturer;
+ }
+
+ capturer->SelectSource(aSourceId);
+
+ capturer = std::make_unique<DesktopAndCursorComposer>(std::move(capturer),
+ options);
+ } else if (aDeviceType == CaptureDeviceType::Window) {
+#if defined(RTC_ENABLE_WIN_WGC)
+ options.set_allow_wgc_capturer_fallback(true);
+#endif
+ capturer = DesktopCapturer::CreateWindowCapturer(options);
+ if (!capturer) {
+ return capturer;
+ }
+
+ capturer->SelectSource(aSourceId);
+
+ capturer = std::make_unique<DesktopAndCursorComposer>(std::move(capturer),
+ options);
+ } else if (aDeviceType == CaptureDeviceType::Browser) {
+ // XXX We don't capture cursors, so avoid the extra indirection layer. We
+ // could also pass null for the pMouseCursorMonitor.
+ capturer = CreateTabCapturer(options, aSourceId, ensureThread());
+ } else {
+ MOZ_ASSERT(!capturer);
+ return capturer;
+ }
+
+ MOZ_ASSERT(capturer);
+ ensureThread();
+
+ return capturer;
+}
+
+DesktopCaptureImpl::DesktopCaptureImpl(const int32_t aId, const char* aUniqueId,
+ const CaptureDeviceType aType)
+ : mModuleId(aId),
+ mTrackingId(mozilla::TrackingId(CaptureEngineToTrackingSourceStr([&] {
+ switch (aType) {
+ case CaptureDeviceType::Screen:
+ return CaptureEngine::ScreenEngine;
+ case CaptureDeviceType::Window:
+ return CaptureEngine::WinEngine;
+ case CaptureDeviceType::Browser:
+ return CaptureEngine::BrowserEngine;
+ default:
+ return CaptureEngine::InvalidEngine;
+ }
+ }()),
+ aId)),
+ mDeviceUniqueId(aUniqueId),
+ mDeviceType(aType),
+ mControlThread(mozilla::GetCurrentSerialEventTarget()),
+ mNextFrameMinimumTime(Timestamp::Zero()),
+ mCallbacks("DesktopCaptureImpl::mCallbacks") {}
+
+DesktopCaptureImpl::~DesktopCaptureImpl() {
+ MOZ_ASSERT(!mCaptureThread);
+ MOZ_ASSERT(!mRequestedCapability);
+}
+
+void DesktopCaptureImpl::RegisterCaptureDataCallback(
+ rtc::VideoSinkInterface<VideoFrame>* aDataCallback) {
+ auto callbacks = mCallbacks.Lock();
+ callbacks->insert(aDataCallback);
+}
+
+void DesktopCaptureImpl::DeRegisterCaptureDataCallback(
+ rtc::VideoSinkInterface<VideoFrame>* aDataCallback) {
+ auto callbacks = mCallbacks.Lock();
+ auto it = callbacks->find(aDataCallback);
+ if (it != callbacks->end()) {
+ callbacks->erase(it);
+ }
+}
+
+int32_t DesktopCaptureImpl::StopCaptureIfAllClientsClose() {
+ {
+ auto callbacks = mCallbacks.Lock();
+ if (!callbacks->empty()) {
+ return 0;
+ }
+ }
+ return StopCapture();
+}
+
+int32_t DesktopCaptureImpl::SetCaptureRotation(VideoRotation aRotation) {
+ MOZ_ASSERT_UNREACHABLE("Unused");
+ return -1;
+}
+
+bool DesktopCaptureImpl::SetApplyRotation(bool aEnable) { return true; }
+
+int32_t DesktopCaptureImpl::StartCapture(
+ const VideoCaptureCapability& aCapability) {
+ RTC_DCHECK_RUN_ON(&mControlThreadChecker);
+
+ if (mRequestedCapability) {
+ // Already initialized
+ MOZ_ASSERT(*mRequestedCapability == aCapability);
+
+ return 0;
+ }
+
+ MOZ_ASSERT(!mCaptureThread);
+
+ DesktopCapturer::SourceId sourceId = std::stoi(mDeviceUniqueId);
+ std::unique_ptr capturer = CreateDesktopCapturerAndThread(
+ mDeviceType, sourceId, getter_AddRefs(mCaptureThread));
+
+ MOZ_ASSERT(!capturer == !mCaptureThread);
+ if (!capturer) {
+ return -1;
+ }
+
+ mRequestedCapability = mozilla::Some(aCapability);
+ mCaptureThreadChecker.Detach();
+
+ MOZ_ALWAYS_SUCCEEDS(mCaptureThread->Dispatch(NS_NewRunnableFunction(
+ "DesktopCaptureImpl::InitOnThread",
+ [this, self = RefPtr(this), capturer = std::move(capturer),
+ maxFps = std::max(aCapability.maxFPS, 1)]() mutable {
+ InitOnThread(std::move(capturer), maxFps);
+ })));
+
+ return 0;
+}
+
+bool DesktopCaptureImpl::FocusOnSelectedSource() {
+ RTC_DCHECK_RUN_ON(&mControlThreadChecker);
+ if (!mCaptureThread) {
+ MOZ_ASSERT_UNREACHABLE(
+ "FocusOnSelectedSource must be called after StartCapture");
+ return false;
+ }
+
+ bool success = false;
+ MOZ_ALWAYS_SUCCEEDS(mozilla::SyncRunnable::DispatchToThread(
+ mCaptureThread, NS_NewRunnableFunction(__func__, [&] {
+ RTC_DCHECK_RUN_ON(&mCaptureThreadChecker);
+ MOZ_ASSERT(mCapturer);
+ success = mCapturer && mCapturer->FocusOnSelectedSource();
+ })));
+ return success;
+}
+
+int32_t DesktopCaptureImpl::StopCapture() {
+ RTC_DCHECK_RUN_ON(&mControlThreadChecker);
+ if (mRequestedCapability) {
+ // Sync-cancel the capture timer so no CaptureFrame calls will come in after
+ // we return.
+ MOZ_ALWAYS_SUCCEEDS(mozilla::SyncRunnable::DispatchToThread(
+ mCaptureThread,
+ NewRunnableMethod(__func__, this,
+ &DesktopCaptureImpl::ShutdownOnThread)));
+
+ mRequestedCapability = mozilla::Nothing();
+ }
+
+ if (mCaptureThread) {
+ // CaptureThread shutdown.
+ mCaptureThread->AsyncShutdown();
+ mCaptureThread = nullptr;
+ }
+
+ return 0;
+}
+
+bool DesktopCaptureImpl::CaptureStarted() {
+ MOZ_ASSERT_UNREACHABLE("Unused");
+ return true;
+}
+
+int32_t DesktopCaptureImpl::CaptureSettings(VideoCaptureCapability& aSettings) {
+ MOZ_ASSERT_UNREACHABLE("Unused");
+ return -1;
+}
+
+void DesktopCaptureImpl::OnCaptureResult(DesktopCapturer::Result aResult,
+ std::unique_ptr<DesktopFrame> aFrame) {
+ RTC_DCHECK_RUN_ON(&mCaptureThreadChecker);
+ if (!aFrame) {
+ return;
+ }
+
+ const auto startProcessTime = Timestamp::Micros(rtc::TimeMicros());
+ auto frameTime = startProcessTime;
+ if (auto diff = startProcessTime - mNextFrameMinimumTime;
+ diff < TimeDelta::Zero()) {
+ if (diff > TimeDelta::Millis(-1)) {
+ // Two consecutive frames within a millisecond is OK. It could happen due
+ // to timing.
+ frameTime = mNextFrameMinimumTime;
+ } else {
+ // Three consecutive frames within two milliseconds seems too much, drop
+ // one.
+ MOZ_ASSERT(diff >= TimeDelta::Millis(-2));
+ RTC_LOG(LS_WARNING) << "DesktopCapture render time is getting too far "
+ "ahead. Framerate is unexpectedly high.";
+ return;
+ }
+ }
+
+ uint8_t* videoFrame = aFrame->data();
+ VideoCaptureCapability frameInfo;
+ frameInfo.width = aFrame->size().width();
+ frameInfo.height = aFrame->size().height();
+ frameInfo.videoType = VideoType::kARGB;
+
+ size_t videoFrameLength =
+ frameInfo.width * frameInfo.height * DesktopFrame::kBytesPerPixel;
+
+ const int32_t width = frameInfo.width;
+ const int32_t height = frameInfo.height;
+
+ // Not encoded, convert to I420.
+ if (frameInfo.videoType != VideoType::kMJPEG &&
+ CalcBufferSize(frameInfo.videoType, width, abs(height)) !=
+ videoFrameLength) {
+ RTC_LOG(LS_ERROR) << "Wrong incoming frame length.";
+ return;
+ }
+
+ int stride_y = width;
+ int stride_uv = (width + 1) / 2;
+
+ // Setting absolute height (in case it was negative).
+ // In Windows, the image starts bottom left, instead of top left.
+ // Setting a negative source height, inverts the image (within LibYuv).
+
+ mozilla::PerformanceRecorder<mozilla::CopyVideoStage> rec(
+ "DesktopCaptureImpl::ConvertToI420"_ns, mTrackingId, width, abs(height));
+ // TODO(nisse): Use a pool?
+ rtc::scoped_refptr<I420Buffer> buffer =
+ I420Buffer::Create(width, abs(height), stride_y, stride_uv, stride_uv);
+
+ const int conversionResult = libyuv::ConvertToI420(
+ videoFrame, videoFrameLength, buffer->MutableDataY(), buffer->StrideY(),
+ buffer->MutableDataU(), buffer->StrideU(), buffer->MutableDataV(),
+ buffer->StrideV(), 0, 0, // No Cropping
+ aFrame->stride() / DesktopFrame::kBytesPerPixel, height, width, height,
+ libyuv::kRotate0, ConvertVideoType(frameInfo.videoType));
+ if (conversionResult != 0) {
+ RTC_LOG(LS_ERROR) << "Failed to convert capture frame from type "
+ << static_cast<int>(frameInfo.videoType) << "to I420.";
+ return;
+ }
+ rec.Record();
+
+ NotifyOnFrame(VideoFrame::Builder()
+ .set_video_frame_buffer(buffer)
+ .set_timestamp_us(frameTime.us())
+ .build());
+
+ const TimeDelta processTime =
+ Timestamp::Micros(rtc::TimeMicros()) - startProcessTime;
+
+ if (processTime > TimeDelta::Millis(10)) {
+ RTC_LOG(LS_WARNING)
+ << "Too long processing time of incoming frame with dimensions "
+ << width << "x" << height << ": " << processTime.ms() << " ms";
+ }
+}
+
+void DesktopCaptureImpl::NotifyOnFrame(const VideoFrame& aFrame) {
+ RTC_DCHECK_RUN_ON(&mCaptureThreadChecker);
+ MOZ_ASSERT(Timestamp::Millis(aFrame.render_time_ms()) >
+ mNextFrameMinimumTime);
+ // Set the next frame's minimum time to ensure two consecutive frames don't
+ // have an identical render time (which is in milliseconds).
+ mNextFrameMinimumTime =
+ Timestamp::Millis(aFrame.render_time_ms()) + TimeDelta::Millis(1);
+ auto callbacks = mCallbacks.Lock();
+ for (auto* cb : *callbacks) {
+ cb->OnFrame(aFrame);
+ }
+}
+
+void DesktopCaptureImpl::InitOnThread(
+ std::unique_ptr<DesktopCapturer> aCapturer, int aFramerate) {
+ RTC_DCHECK_RUN_ON(&mCaptureThreadChecker);
+
+ mCapturer = std::move(aCapturer);
+
+ // We need to call Start on the same thread we call CaptureFrame on.
+ mCapturer->Start(this);
+
+ mCaptureTimer = NS_NewTimer();
+ mRequestedCaptureInterval = mozilla::Some(
+ TimeDuration::FromSeconds(1. / static_cast<double>(aFramerate)));
+
+ CaptureFrameOnThread();
+}
+
+void DesktopCaptureImpl::ShutdownOnThread() {
+ RTC_DCHECK_RUN_ON(&mCaptureThreadChecker);
+ if (mCaptureTimer) {
+ mCaptureTimer->Cancel();
+ mCaptureTimer = nullptr;
+ }
+
+ // DesktopCapturer dtor blocks until fully shut down. TabCapturerWebrtc needs
+ // the capture thread to be alive.
+ mCapturer = nullptr;
+
+ mRequestedCaptureInterval = mozilla::Nothing();
+}
+
+void DesktopCaptureImpl::CaptureFrameOnThread() {
+ RTC_DCHECK_RUN_ON(&mCaptureThreadChecker);
+
+ auto start = mozilla::TimeStamp::Now();
+ mCapturer->CaptureFrame();
+ auto end = mozilla::TimeStamp::Now();
+
+ // Calculate next capture time.
+ const auto duration = end - start;
+ const auto timeUntilRequestedCapture = *mRequestedCaptureInterval - duration;
+
+ // Use at most x% CPU or limit framerate
+ constexpr float sleepTimeFactor =
+ (100.0f / kMaxDesktopCaptureCpuUsage) - 1.0f;
+ static_assert(sleepTimeFactor >= 0.0);
+ static_assert(sleepTimeFactor < 100.0);
+ const auto sleepTime = duration.MultDouble(sleepTimeFactor);
+
+ mCaptureTimer->InitHighResolutionWithNamedFuncCallback(
+ &::CaptureFrameOnThread, this,
+ std::max(timeUntilRequestedCapture, sleepTime), nsITimer::TYPE_ONE_SHOT,
+ "DesktopCaptureImpl::mCaptureTimer");
+}
+
+} // namespace webrtc
diff --git a/dom/media/systemservices/video_engine/desktop_capture_impl.h b/dom/media/systemservices/video_engine/desktop_capture_impl.h
new file mode 100644
index 0000000000..64eadb1401
--- /dev/null
+++ b/dom/media/systemservices/video_engine/desktop_capture_impl.h
@@ -0,0 +1,246 @@
+/*
+ * Copyright (c) 2014 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+#ifndef WEBRTC_MODULES_DESKTOP_CAPTURE_MAIN_SOURCE_DESKTOP_CAPTURE_IMPL_H_
+#define WEBRTC_MODULES_DESKTOP_CAPTURE_MAIN_SOURCE_DESKTOP_CAPTURE_IMPL_H_
+
+/*
+ * video_capture_impl.h
+ */
+
+#include <memory>
+#include <set>
+#include <string>
+
+#include "api/sequence_checker.h"
+#include "api/video/video_frame.h"
+#include "api/video/video_sink_interface.h"
+#include "modules/desktop_capture/desktop_capturer.h"
+#include "modules/video_capture/video_capture.h"
+
+#include "desktop_device_info.h"
+#include "mozilla/DataMutex.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/TimeStamp.h"
+#include "nsCOMPtr.h"
+#include "PerformanceRecorder.h"
+
+class nsIThread;
+class nsITimer;
+
+namespace mozilla::camera {
+enum class CaptureDeviceType;
+}
+
+namespace webrtc {
+
+class VideoCaptureEncodeInterface;
+
+// simulate deviceInfo interface for video engine, bridge screen/application and
+// real screen/application device info
+
+class ScreenDeviceInfoImpl : public VideoCaptureModule::DeviceInfo {
+ public:
+ ScreenDeviceInfoImpl(int32_t aId) : mId(aId) {}
+ virtual ~ScreenDeviceInfoImpl() = default;
+
+ int32_t Init();
+ int32_t Refresh();
+
+ virtual uint32_t NumberOfDevices();
+ virtual int32_t GetDeviceName(uint32_t aDeviceNumber, char* aDeviceNameUTF8,
+ uint32_t aDeviceNameUTF8Size,
+ char* aDeviceUniqueIdUTF8,
+ uint32_t aDeviceUniqueIdUTF8Size,
+ char* aProductUniqueIdUTF8,
+ uint32_t aProductUniqueIdUTF8Size, pid_t* aPid);
+
+ virtual int32_t DisplayCaptureSettingsDialogBox(
+ const char* aDeviceUniqueIdUTF8, const char* aDialogTitleUTF8,
+ void* aParentWindow, uint32_t aPositionX, uint32_t aPositionY);
+ virtual int32_t NumberOfCapabilities(const char* aDeviceUniqueIdUTF8);
+ virtual int32_t GetCapability(const char* aDeviceUniqueIdUTF8,
+ uint32_t aDeviceCapabilityNumber,
+ VideoCaptureCapability& aCapability);
+
+ virtual int32_t GetBestMatchedCapability(
+ const char* aDeviceUniqueIdUTF8, const VideoCaptureCapability& aRequested,
+ VideoCaptureCapability& aResulting);
+ virtual int32_t GetOrientation(const char* aDeviceUniqueIdUTF8,
+ VideoRotation& aOrientation);
+
+ protected:
+ int32_t mId;
+ std::unique_ptr<DesktopDeviceInfo> mDesktopDeviceInfo;
+};
+
+class WindowDeviceInfoImpl : public VideoCaptureModule::DeviceInfo {
+ public:
+ WindowDeviceInfoImpl(int32_t aId) : mId(aId){};
+ virtual ~WindowDeviceInfoImpl() = default;
+
+ int32_t Init();
+ int32_t Refresh();
+
+ virtual uint32_t NumberOfDevices();
+ virtual int32_t GetDeviceName(uint32_t aDeviceNumber, char* aDeviceNameUTF8,
+ uint32_t aDeviceNameUTF8Size,
+ char* aDeviceUniqueIdUTF8,
+ uint32_t aDeviceUniqueIdUTF8Size,
+ char* aProductUniqueIdUTF8,
+ uint32_t aProductUniqueIdUTF8Size, pid_t* aPid);
+
+ virtual int32_t DisplayCaptureSettingsDialogBox(
+ const char* aDeviceUniqueIdUTF8, const char* aDialogTitleUTF8,
+ void* aParentWindow, uint32_t aPositionX, uint32_t aPositionY);
+ virtual int32_t NumberOfCapabilities(const char* aDeviceUniqueIdUTF8);
+ virtual int32_t GetCapability(const char* aDeviceUniqueIdUTF8,
+ uint32_t aDeviceCapabilityNumber,
+ VideoCaptureCapability& aCapability);
+
+ virtual int32_t GetBestMatchedCapability(
+ const char* aDeviceUniqueIdUTF8, const VideoCaptureCapability& aRequested,
+ VideoCaptureCapability& aResulting);
+ virtual int32_t GetOrientation(const char* aDeviceUniqueIdUTF8,
+ VideoRotation& aOrientation);
+
+ protected:
+ int32_t mId;
+ std::unique_ptr<DesktopDeviceInfo> mDesktopDeviceInfo;
+};
+
+class BrowserDeviceInfoImpl : public VideoCaptureModule::DeviceInfo {
+ public:
+ BrowserDeviceInfoImpl(int32_t aId) : mId(aId){};
+ virtual ~BrowserDeviceInfoImpl() = default;
+
+ int32_t Init();
+ int32_t Refresh();
+
+ virtual uint32_t NumberOfDevices();
+ virtual int32_t GetDeviceName(uint32_t aDeviceNumber, char* aDeviceNameUTF8,
+ uint32_t aDeviceNameUTF8Size,
+ char* aDeviceUniqueIdUTF8,
+ uint32_t aDeviceUniqueIdUTF8Size,
+ char* aProductUniqueIdUTF8,
+ uint32_t aProductUniqueIdUTF8Size, pid_t* aPid);
+
+ virtual int32_t DisplayCaptureSettingsDialogBox(
+ const char* aDeviceUniqueIdUTF8, const char* aDialogTitleUTF8,
+ void* aParentWindow, uint32_t aPositionX, uint32_t aPositionY);
+ virtual int32_t NumberOfCapabilities(const char* aDeviceUniqueIdUTF8);
+ virtual int32_t GetCapability(const char* aDeviceUniqueIdUTF8,
+ uint32_t aDeviceCapabilityNumber,
+ VideoCaptureCapability& aCapability);
+
+ virtual int32_t GetBestMatchedCapability(
+ const char* aDeviceUniqueIdUTF8, const VideoCaptureCapability& aRequested,
+ VideoCaptureCapability& aResulting);
+ virtual int32_t GetOrientation(const char* aDeviceUniqueIdUTF8,
+ VideoRotation& aOrientation);
+
+ protected:
+ int32_t mId;
+ std::unique_ptr<DesktopDeviceInfo> mDesktopDeviceInfo;
+};
+
+// Reuses the video engine pipeline for screen sharing.
+// As with video, DesktopCaptureImpl is a proxy for screen sharing
+// and follows the video pipeline design
+class DesktopCaptureImpl : public DesktopCapturer::Callback,
+ public VideoCaptureModule {
+ public:
+ /* Create a screen capture modules object
+ */
+ static VideoCaptureModule* Create(
+ const int32_t aModuleId, const char* aUniqueId,
+ const mozilla::camera::CaptureDeviceType aType);
+
+ [[nodiscard]] static std::shared_ptr<VideoCaptureModule::DeviceInfo>
+ CreateDeviceInfo(const int32_t aId,
+ const mozilla::camera::CaptureDeviceType aType);
+
+ // mControlThread only.
+ void RegisterCaptureDataCallback(
+ rtc::VideoSinkInterface<VideoFrame>* aCallback) override;
+ void RegisterCaptureDataCallback(
+ RawVideoSinkInterface* dataCallback) override {}
+ void DeRegisterCaptureDataCallback(
+ rtc::VideoSinkInterface<VideoFrame>* aCallback) override;
+ int32_t StopCaptureIfAllClientsClose() override;
+
+ int32_t SetCaptureRotation(VideoRotation aRotation) override;
+ bool SetApplyRotation(bool aEnable) override;
+ bool GetApplyRotation() override { return true; }
+
+ const char* CurrentDeviceName() const override;
+
+ int32_t StartCapture(const VideoCaptureCapability& aCapability) override;
+ virtual bool FocusOnSelectedSource() override;
+ int32_t StopCapture() override;
+ bool CaptureStarted() override;
+ int32_t CaptureSettings(VideoCaptureCapability& aSettings) override;
+
+ void CaptureFrameOnThread();
+
+ const int32_t mModuleId;
+ const mozilla::TrackingId mTrackingId;
+ const std::string mDeviceUniqueId;
+ const mozilla::camera::CaptureDeviceType mDeviceType;
+
+ protected:
+ DesktopCaptureImpl(const int32_t aId, const char* aUniqueId,
+ const mozilla::camera::CaptureDeviceType aType);
+ virtual ~DesktopCaptureImpl();
+
+ private:
+ // Maximum CPU usage in %.
+ static constexpr uint32_t kMaxDesktopCaptureCpuUsage = 50;
+ void InitOnThread(std::unique_ptr<DesktopCapturer> aCapturer, int aFramerate);
+ void ShutdownOnThread();
+ // DesktopCapturer::Callback interface.
+ void OnCaptureResult(DesktopCapturer::Result aResult,
+ std::unique_ptr<DesktopFrame> aFrame) override;
+
+ // Notifies all mCallbacks of OnFrame(). mCaptureThread only.
+ void NotifyOnFrame(const VideoFrame& aFrame);
+
+ // Control thread on which the public API is called.
+ const nsCOMPtr<nsISerialEventTarget> mControlThread;
+ // Set in StartCapture.
+ mozilla::Maybe<VideoCaptureCapability> mRequestedCapability
+ RTC_GUARDED_BY(mControlThreadChecker);
+ // The DesktopCapturer is created on mControlThread but assigned and accessed
+ // only on mCaptureThread.
+ std::unique_ptr<DesktopCapturer> mCapturer
+ RTC_GUARDED_BY(mCaptureThreadChecker);
+ // Dedicated thread that does the capturing.
+ nsCOMPtr<nsIThread> mCaptureThread RTC_GUARDED_BY(mControlThreadChecker);
+ // Checks that API methods are called on mControlThread.
+ webrtc::SequenceChecker mControlThreadChecker;
+ // Checks that frame delivery only happens on mCaptureThread.
+ webrtc::SequenceChecker mCaptureThreadChecker;
+ // Timer that triggers frame captures. Only used on mCaptureThread.
+ // TODO(Bug 1806646): Drive capture with vsync instead.
+ nsCOMPtr<nsITimer> mCaptureTimer RTC_GUARDED_BY(mCaptureThreadChecker);
+ // Interval between captured frames, based on the framerate in
+ // mRequestedCapability. mCaptureThread only.
+ mozilla::Maybe<mozilla::TimeDuration> mRequestedCaptureInterval
+ RTC_GUARDED_BY(mCaptureThreadChecker);
+ // Used to make sure incoming timestamp is increasing for every frame.
+ webrtc::Timestamp mNextFrameMinimumTime RTC_GUARDED_BY(mCaptureThreadChecker);
+ // Callbacks for captured frames. Mutated on mControlThread, callbacks happen
+ // on mCaptureThread.
+ mozilla::DataMutex<std::set<rtc::VideoSinkInterface<VideoFrame>*>> mCallbacks;
+};
+
+} // namespace webrtc
+
+#endif // WEBRTC_MODULES_DESKTOP_CAPTURE_MAIN_SOURCE_DESKTOP_CAPTURE_IMPL_H_
diff --git a/dom/media/systemservices/video_engine/desktop_device_info.cc b/dom/media/systemservices/video_engine/desktop_device_info.cc
new file mode 100644
index 0000000000..b3632a509f
--- /dev/null
+++ b/dom/media/systemservices/video_engine/desktop_device_info.cc
@@ -0,0 +1,488 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "desktop_device_info.h"
+#include "modules/desktop_capture/desktop_capture_options.h"
+#include "modules/desktop_capture/desktop_capturer.h"
+#include "mozilla/Sprintf.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/SyncRunnable.h"
+#include "mozilla/UniquePtr.h"
+#include "nsIBrowserWindowTracker.h"
+#include "nsImportModule.h"
+
+#include <cstddef>
+#include <cstdlib>
+#include <cstdio>
+#include <cstring>
+#include <memory>
+
+namespace webrtc {
+
+static inline void SetStringMember(char** aMember, const char* aValue) {
+ if (!aValue) {
+ return;
+ }
+
+ if (*aMember) {
+ delete[] * aMember;
+ *aMember = nullptr;
+ }
+
+ size_t nBufLen = strlen(aValue) + 1;
+ char* buffer = new char[nBufLen];
+ memcpy(buffer, aValue, nBufLen - 1);
+ buffer[nBufLen - 1] = '\0';
+ *aMember = buffer;
+}
+
+DesktopDisplayDevice::DesktopDisplayDevice() {
+ mScreenId = kInvalidScreenId;
+ mDeviceUniqueIdUTF8 = nullptr;
+ mDeviceNameUTF8 = nullptr;
+ mPid = 0;
+}
+
+DesktopDisplayDevice::~DesktopDisplayDevice() {
+ mScreenId = kInvalidScreenId;
+
+ delete[] mDeviceUniqueIdUTF8;
+ delete[] mDeviceNameUTF8;
+
+ mDeviceUniqueIdUTF8 = nullptr;
+ mDeviceNameUTF8 = nullptr;
+}
+
+void DesktopDisplayDevice::setScreenId(const ScreenId aScreenId) {
+ mScreenId = aScreenId;
+}
+
+void DesktopDisplayDevice::setDeviceName(const char* aDeviceNameUTF8) {
+ SetStringMember(&mDeviceNameUTF8, aDeviceNameUTF8);
+}
+
+void DesktopDisplayDevice::setUniqueIdName(const char* aDeviceUniqueIdUTF8) {
+ SetStringMember(&mDeviceUniqueIdUTF8, aDeviceUniqueIdUTF8);
+}
+
+void DesktopDisplayDevice::setPid(const int aPid) { mPid = aPid; }
+
+ScreenId DesktopDisplayDevice::getScreenId() { return mScreenId; }
+
+const char* DesktopDisplayDevice::getDeviceName() { return mDeviceNameUTF8; }
+
+const char* DesktopDisplayDevice::getUniqueIdName() {
+ return mDeviceUniqueIdUTF8;
+}
+
+pid_t DesktopDisplayDevice::getPid() { return mPid; }
+
+DesktopDisplayDevice& DesktopDisplayDevice::operator=(
+ DesktopDisplayDevice& aOther) {
+ if (&aOther == this) {
+ return *this;
+ }
+ mScreenId = aOther.getScreenId();
+ setUniqueIdName(aOther.getUniqueIdName());
+ setDeviceName(aOther.getDeviceName());
+ mPid = aOther.getPid();
+
+ return *this;
+}
+
+DesktopTab::DesktopTab() {
+ mTabBrowserId = 0;
+ mTabNameUTF8 = nullptr;
+ mTabUniqueIdUTF8 = nullptr;
+ mTabCount = 0;
+}
+
+DesktopTab::~DesktopTab() {
+ delete[] mTabNameUTF8;
+ delete[] mTabUniqueIdUTF8;
+
+ mTabNameUTF8 = nullptr;
+ mTabUniqueIdUTF8 = nullptr;
+}
+
+void DesktopTab::setTabBrowserId(uint64_t aTabBrowserId) {
+ mTabBrowserId = aTabBrowserId;
+}
+
+void DesktopTab::setUniqueIdName(const char* aTabUniqueIdUTF8) {
+ SetStringMember(&mTabUniqueIdUTF8, aTabUniqueIdUTF8);
+}
+
+void DesktopTab::setTabName(const char* aTabNameUTF8) {
+ SetStringMember(&mTabNameUTF8, aTabNameUTF8);
+}
+
+void DesktopTab::setTabCount(const uint32_t aCount) { mTabCount = aCount; }
+
+uint64_t DesktopTab::getTabBrowserId() { return mTabBrowserId; }
+
+const char* DesktopTab::getUniqueIdName() { return mTabUniqueIdUTF8; }
+
+const char* DesktopTab::getTabName() { return mTabNameUTF8; }
+
+uint32_t DesktopTab::getTabCount() { return mTabCount; }
+
+DesktopTab& DesktopTab::operator=(DesktopTab& aOther) {
+ mTabBrowserId = aOther.getTabBrowserId();
+ setUniqueIdName(aOther.getUniqueIdName());
+ setTabName(aOther.getTabName());
+
+ return *this;
+}
+
+class DesktopDeviceInfoImpl : public DesktopDeviceInfo {
+ public:
+ DesktopDeviceInfoImpl();
+ ~DesktopDeviceInfoImpl();
+
+ int32_t Init() override;
+ int32_t Refresh() override;
+ int32_t getDisplayDeviceCount() override;
+ int32_t getDesktopDisplayDeviceInfo(
+ uint32_t aIndex, DesktopDisplayDevice& aDesktopDisplayDevice) override;
+ int32_t getWindowCount() override;
+ int32_t getWindowInfo(uint32_t aIndex,
+ DesktopDisplayDevice& aWindowDevice) override;
+ uint32_t getTabCount() override;
+ int32_t getTabInfo(uint32_t aIndex, DesktopTab& aDesktopTab) override;
+
+ protected:
+ DesktopDisplayDeviceList mDesktopDisplayList;
+ DesktopDisplayDeviceList mDesktopWindowList;
+ DesktopTabList mDesktopTabList;
+
+ void CleanUp();
+ void CleanUpWindowList();
+ void CleanUpTabList();
+ void CleanUpScreenList();
+
+ void InitializeWindowList();
+ virtual void InitializeTabList();
+ void InitializeScreenList();
+
+ void RefreshWindowList();
+ void RefreshTabList();
+ void RefreshScreenList();
+
+ void DummyTabList(DesktopTabList& aList);
+};
+
+DesktopDeviceInfoImpl::DesktopDeviceInfoImpl() = default;
+
+DesktopDeviceInfoImpl::~DesktopDeviceInfoImpl() { CleanUp(); }
+
+int32_t DesktopDeviceInfoImpl::getDisplayDeviceCount() {
+ return static_cast<int32_t>(mDesktopDisplayList.size());
+}
+
+int32_t DesktopDeviceInfoImpl::getDesktopDisplayDeviceInfo(
+ uint32_t aIndex, DesktopDisplayDevice& aDesktopDisplayDevice) {
+ if (aIndex >= mDesktopDisplayList.size()) {
+ return -1;
+ }
+
+ std::map<intptr_t, DesktopDisplayDevice*>::iterator iter =
+ mDesktopDisplayList.begin();
+ std::advance(iter, aIndex);
+ DesktopDisplayDevice* desktopDisplayDevice = iter->second;
+ if (desktopDisplayDevice) {
+ aDesktopDisplayDevice = (*desktopDisplayDevice);
+ }
+
+ return 0;
+}
+
+int32_t DesktopDeviceInfoImpl::getWindowCount() {
+ return static_cast<int32_t>(mDesktopWindowList.size());
+}
+
+int32_t DesktopDeviceInfoImpl::getWindowInfo(
+ uint32_t aIndex, DesktopDisplayDevice& aWindowDevice) {
+ if (aIndex >= mDesktopWindowList.size()) {
+ return -1;
+ }
+
+ std::map<intptr_t, DesktopDisplayDevice*>::iterator itr =
+ mDesktopWindowList.begin();
+ std::advance(itr, aIndex);
+ DesktopDisplayDevice* window = itr->second;
+ if (!window) {
+ return -1;
+ }
+
+ aWindowDevice = (*window);
+ return 0;
+}
+
+uint32_t DesktopDeviceInfoImpl::getTabCount() { return mDesktopTabList.size(); }
+
+int32_t DesktopDeviceInfoImpl::getTabInfo(uint32_t aIndex,
+ DesktopTab& aDesktopTab) {
+ if (aIndex >= mDesktopTabList.size()) {
+ return -1;
+ }
+
+ std::map<intptr_t, DesktopTab*>::iterator iter = mDesktopTabList.begin();
+ std::advance(iter, aIndex);
+ DesktopTab* desktopTab = iter->second;
+ if (desktopTab) {
+ aDesktopTab = (*desktopTab);
+ }
+
+ return 0;
+}
+
+void DesktopDeviceInfoImpl::CleanUp() {
+ CleanUpScreenList();
+ CleanUpWindowList();
+ CleanUpTabList();
+}
+int32_t DesktopDeviceInfoImpl::Init() {
+ InitializeScreenList();
+ InitializeWindowList();
+ InitializeTabList();
+
+ return 0;
+}
+int32_t DesktopDeviceInfoImpl::Refresh() {
+ RefreshScreenList();
+ RefreshWindowList();
+ RefreshTabList();
+
+ return 0;
+}
+
+void DesktopDeviceInfoImpl::CleanUpWindowList() {
+ std::map<intptr_t, DesktopDisplayDevice*>::iterator iterWindow;
+ for (iterWindow = mDesktopWindowList.begin();
+ iterWindow != mDesktopWindowList.end(); iterWindow++) {
+ DesktopDisplayDevice* aWindow = iterWindow->second;
+ delete aWindow;
+ iterWindow->second = nullptr;
+ }
+ mDesktopWindowList.clear();
+}
+
+void DesktopDeviceInfoImpl::InitializeWindowList() {
+ DesktopCaptureOptions options;
+
+// Wayland is special and we will not get any information about windows
+// without going through xdg-desktop-portal. We will already have
+// a screen placeholder so there is no reason to build windows list.
+#if defined(WEBRTC_USE_PIPEWIRE)
+ if (mozilla::StaticPrefs::media_webrtc_capture_allow_pipewire() &&
+ webrtc::DesktopCapturer::IsRunningUnderWayland()) {
+ return;
+ }
+#endif
+
+// Help avoid an X11 deadlock, see bug 1456101.
+#ifdef MOZ_X11
+ MOZ_ALWAYS_SUCCEEDS(mozilla::SyncRunnable::DispatchToThread(
+ mozilla::GetMainThreadSerialEventTarget(),
+ NS_NewRunnableFunction(__func__, [&] {
+ options = DesktopCaptureOptions::CreateDefault();
+ })));
+#else
+ options = DesktopCaptureOptions::CreateDefault();
+#endif
+ std::unique_ptr<DesktopCapturer> winCap =
+ DesktopCapturer::CreateWindowCapturer(options);
+ DesktopCapturer::SourceList list;
+ if (winCap && winCap->GetSourceList(&list)) {
+ DesktopCapturer::SourceList::iterator itr;
+ for (itr = list.begin(); itr != list.end(); itr++) {
+ DesktopDisplayDevice* winDevice = new DesktopDisplayDevice;
+ if (!winDevice) {
+ continue;
+ }
+
+ winDevice->setScreenId(itr->id);
+ winDevice->setDeviceName(itr->title.c_str());
+ winDevice->setPid(itr->pid);
+
+ char idStr[BUFSIZ];
+#if WEBRTC_WIN
+ _snprintf_s(idStr, sizeof(idStr), sizeof(idStr) - 1, "%ld",
+ static_cast<long>(winDevice->getScreenId()));
+#else
+ SprintfLiteral(idStr, "%ld", static_cast<long>(winDevice->getScreenId()));
+#endif
+ winDevice->setUniqueIdName(idStr);
+ mDesktopWindowList[winDevice->getScreenId()] = winDevice;
+ }
+ }
+}
+
+void DesktopDeviceInfoImpl::RefreshWindowList() {
+ CleanUpWindowList();
+ InitializeWindowList();
+}
+
+void DesktopDeviceInfoImpl::CleanUpTabList() {
+ for (auto& iterTab : mDesktopTabList) {
+ DesktopTab* desktopTab = iterTab.second;
+ delete desktopTab;
+ iterTab.second = nullptr;
+ }
+ mDesktopTabList.clear();
+}
+
+void webrtc::DesktopDeviceInfoImpl::InitializeTabList() {
+ if (!mozilla::StaticPrefs::media_getusermedia_browser_enabled()) {
+ return;
+ }
+
+ // This is a sync dispatch to main thread, which is unfortunate. To
+ // call JavaScript we have to be on main thread, but the remaining
+ // DesktopCapturer very much wants to be off main thread. This might
+ // be solvable by calling this method earlier on while we're still on
+ // main thread and plumbing the information down to here.
+ nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction(__func__, [&] {
+ nsresult rv;
+ nsCOMPtr<nsIBrowserWindowTracker> bwt =
+ do_ImportModule("resource:///modules/BrowserWindowTracker.jsm",
+ "BrowserWindowTracker", &rv);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ nsTArray<RefPtr<nsIVisibleTab>> tabArray;
+ rv = bwt->GetAllVisibleTabs(tabArray);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ for (const auto& browserTab : tabArray) {
+ nsString contentTitle;
+ browserTab->GetContentTitle(contentTitle);
+ int64_t browserId;
+ browserTab->GetBrowserId(&browserId);
+
+ DesktopTab* desktopTab = new DesktopTab;
+ if (desktopTab) {
+ char* contentTitleUTF8 = ToNewUTF8String(contentTitle);
+ desktopTab->setTabBrowserId(browserId);
+ desktopTab->setTabName(contentTitleUTF8);
+ std::ostringstream uniqueId;
+ uniqueId << browserId;
+ desktopTab->setUniqueIdName(uniqueId.str().c_str());
+ mDesktopTabList[static_cast<intptr_t>(desktopTab->getTabBrowserId())] =
+ desktopTab;
+ free(contentTitleUTF8);
+ }
+ }
+ });
+ mozilla::SyncRunnable::DispatchToThread(
+ mozilla::GetMainThreadSerialEventTarget(), runnable);
+}
+
+void DesktopDeviceInfoImpl::RefreshTabList() {
+ CleanUpTabList();
+ InitializeTabList();
+}
+
+void DesktopDeviceInfoImpl::CleanUpScreenList() {
+ std::map<intptr_t, DesktopDisplayDevice*>::iterator iterDevice;
+ for (iterDevice = mDesktopDisplayList.begin();
+ iterDevice != mDesktopDisplayList.end(); iterDevice++) {
+ DesktopDisplayDevice* desktopDisplayDevice = iterDevice->second;
+ delete desktopDisplayDevice;
+ iterDevice->second = nullptr;
+ }
+ mDesktopDisplayList.clear();
+}
+
+// With PipeWire we can't select which system resource is shared so
+// we don't create a window/screen list. Instead we place these constants
+// as window name/id so frontend code can identify PipeWire backend
+// and does not try to create screen/window preview.
+
+#define PIPEWIRE_ID 0xaffffff
+#define PIPEWIRE_NAME "####_PIPEWIRE_PORTAL_####"
+
+void DesktopDeviceInfoImpl::InitializeScreenList() {
+ DesktopCaptureOptions options;
+
+// Wayland is special and we will not get any information about screens
+// without going through xdg-desktop-portal so we just need a screen
+// placeholder.
+#if defined(WEBRTC_USE_PIPEWIRE)
+ if (mozilla::StaticPrefs::media_webrtc_capture_allow_pipewire() &&
+ webrtc::DesktopCapturer::IsRunningUnderWayland()) {
+ DesktopDisplayDevice* screenDevice = new DesktopDisplayDevice;
+ if (!screenDevice) {
+ return;
+ }
+
+ screenDevice->setScreenId(PIPEWIRE_ID);
+ screenDevice->setDeviceName(PIPEWIRE_NAME);
+
+ char idStr[BUFSIZ];
+ SprintfLiteral(idStr, "%ld",
+ static_cast<long>(screenDevice->getScreenId()));
+ screenDevice->setUniqueIdName(idStr);
+ mDesktopDisplayList[screenDevice->getScreenId()] = screenDevice;
+ return;
+ }
+#endif
+
+// Help avoid an X11 deadlock, see bug 1456101.
+#ifdef MOZ_X11
+ MOZ_ALWAYS_SUCCEEDS(mozilla::SyncRunnable::DispatchToThread(
+ mozilla::GetMainThreadSerialEventTarget(),
+ NS_NewRunnableFunction(__func__, [&] {
+ options = DesktopCaptureOptions::CreateDefault();
+ })));
+#else
+ options = DesktopCaptureOptions::CreateDefault();
+#endif
+ std::unique_ptr<DesktopCapturer> screenCapturer =
+ DesktopCapturer::CreateScreenCapturer(options);
+ DesktopCapturer::SourceList list;
+ if (screenCapturer && screenCapturer->GetSourceList(&list)) {
+ DesktopCapturer::SourceList::iterator itr;
+ for (itr = list.begin(); itr != list.end(); itr++) {
+ DesktopDisplayDevice* screenDevice = new DesktopDisplayDevice;
+ screenDevice->setScreenId(itr->id);
+ if (list.size() == 1) {
+ screenDevice->setDeviceName("Primary Monitor");
+ } else {
+ screenDevice->setDeviceName(itr->title.c_str());
+ }
+ screenDevice->setPid(itr->pid);
+
+ char idStr[BUFSIZ];
+#if WEBRTC_WIN
+ _snprintf_s(idStr, sizeof(idStr), sizeof(idStr) - 1, "%ld",
+ static_cast<long>(screenDevice->getScreenId()));
+#else
+ SprintfLiteral(idStr, "%ld",
+ static_cast<long>(screenDevice->getScreenId()));
+#endif
+ screenDevice->setUniqueIdName(idStr);
+ mDesktopDisplayList[screenDevice->getScreenId()] = screenDevice;
+ }
+ }
+}
+
+void DesktopDeviceInfoImpl::RefreshScreenList() {
+ CleanUpScreenList();
+ InitializeScreenList();
+}
+
+/* static */
+DesktopDeviceInfo* DesktopDeviceInfo::Create() {
+ auto info = mozilla::MakeUnique<DesktopDeviceInfoImpl>();
+ if (info->Init() != 0) {
+ return nullptr;
+ }
+ return info.release();
+}
+} // namespace webrtc
diff --git a/dom/media/systemservices/video_engine/desktop_device_info.h b/dom/media/systemservices/video_engine/desktop_device_info.h
new file mode 100644
index 0000000000..824792b3c0
--- /dev/null
+++ b/dom/media/systemservices/video_engine/desktop_device_info.h
@@ -0,0 +1,84 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef WEBRTC_MODULES_DESKTOP_CAPTURE_DEVICE_INFO_H_
+#define WEBRTC_MODULES_DESKTOP_CAPTURE_DEVICE_INFO_H_
+
+#include <map>
+#include "modules/desktop_capture/desktop_capture_types.h"
+
+namespace webrtc {
+
+class DesktopDisplayDevice {
+ public:
+ DesktopDisplayDevice();
+ ~DesktopDisplayDevice();
+
+ void setScreenId(const ScreenId aScreenId);
+ void setDeviceName(const char* aDeviceNameUTF8);
+ void setUniqueIdName(const char* aDeviceUniqueIdUTF8);
+ void setPid(pid_t aPid);
+
+ ScreenId getScreenId();
+ const char* getDeviceName();
+ const char* getUniqueIdName();
+ pid_t getPid();
+
+ DesktopDisplayDevice& operator=(DesktopDisplayDevice& aOther);
+
+ protected:
+ ScreenId mScreenId;
+ char* mDeviceNameUTF8;
+ char* mDeviceUniqueIdUTF8;
+ pid_t mPid;
+};
+
+using DesktopDisplayDeviceList = std::map<intptr_t, DesktopDisplayDevice*>;
+
+class DesktopTab {
+ public:
+ DesktopTab();
+ ~DesktopTab();
+
+ void setTabBrowserId(uint64_t aTabBrowserId);
+ void setUniqueIdName(const char* aTabUniqueIdUTF8);
+ void setTabName(const char* aTabNameUTF8);
+ void setTabCount(const uint32_t aCount);
+
+ uint64_t getTabBrowserId();
+ const char* getUniqueIdName();
+ const char* getTabName();
+ uint32_t getTabCount();
+
+ DesktopTab& operator=(DesktopTab& aOther);
+
+ protected:
+ uint64_t mTabBrowserId;
+ char* mTabNameUTF8;
+ char* mTabUniqueIdUTF8;
+ uint32_t mTabCount;
+};
+
+using DesktopTabList = std::map<intptr_t, DesktopTab*>;
+
+class DesktopDeviceInfo {
+ public:
+ virtual ~DesktopDeviceInfo() = default;
+
+ virtual int32_t Init() = 0;
+ virtual int32_t Refresh() = 0;
+ virtual int32_t getDisplayDeviceCount() = 0;
+ virtual int32_t getDesktopDisplayDeviceInfo(
+ uint32_t aIndex, DesktopDisplayDevice& aDesktopDisplayDevice) = 0;
+ virtual int32_t getWindowCount() = 0;
+ virtual int32_t getWindowInfo(uint32_t aIndex,
+ DesktopDisplayDevice& aWindowDevice) = 0;
+ virtual uint32_t getTabCount() = 0;
+ virtual int32_t getTabInfo(uint32_t aIndex, DesktopTab& aDesktopTab) = 0;
+
+ static DesktopDeviceInfo* Create();
+};
+}; // namespace webrtc
+
+#endif
diff --git a/dom/media/systemservices/video_engine/platform_uithread.cc b/dom/media/systemservices/video_engine/platform_uithread.cc
new file mode 100644
index 0000000000..701a989a18
--- /dev/null
+++ b/dom/media/systemservices/video_engine/platform_uithread.cc
@@ -0,0 +1,198 @@
+/*
+ * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+#if defined(WEBRTC_WIN)
+
+# include "platform_uithread.h"
+
+namespace rtc {
+
+// timer id used in delayed callbacks
+static const UINT_PTR kTimerId = 1;
+static const wchar_t kThisProperty[] = L"ThreadWindowsUIPtr";
+static const wchar_t kThreadWindow[] = L"WebrtcWindowsUIThread";
+
+PlatformUIThread::~PlatformUIThread() {
+ CritScope scoped_lock(&cs_);
+ switch (state_) {
+ case State::STARTED: {
+ MOZ_DIAGNOSTIC_ASSERT(
+ false, "PlatformUIThread must be stopped before destruction");
+ break;
+ }
+ case State::STOPPED:
+ break;
+ case State::UNSTARTED:
+ break;
+ }
+}
+
+bool PlatformUIThread::InternalInit() {
+ // Create an event window for use in generating callbacks to capture
+ // objects.
+ CritScope scoped_lock(&cs_);
+ switch (state_) {
+ // We have already started there is nothing todo. Should this be assert?
+ case State::STARTED:
+ break;
+ // Stop() has already been called so there is likewise nothing to do.
+ case State::STOPPED:
+ break;
+ // Stop() has not been called yet, setup the UI thread, and set our
+ // state to STARTED.
+ case State::UNSTARTED: {
+ WNDCLASSW wc;
+ HMODULE hModule = GetModuleHandle(NULL);
+ if (!GetClassInfoW(hModule, kThreadWindow, &wc)) {
+ ZeroMemory(&wc, sizeof(WNDCLASSW));
+ wc.hInstance = hModule;
+ wc.lpfnWndProc = EventWindowProc;
+ wc.lpszClassName = kThreadWindow;
+ RegisterClassW(&wc);
+ }
+ hwnd_ = CreateWindowW(kThreadWindow, L"", 0, 0, 0, 0, 0, NULL, NULL,
+ hModule, NULL);
+ // Added in review of bug 1760843, follow up to remove 1767861
+ MOZ_RELEASE_ASSERT(hwnd_);
+ // Expected to always work but if it doesn't we should still fulfill the
+ // contract of always running the process loop at least a single
+ // iteration.
+ // This could be rexamined in the future.
+ if (hwnd_) {
+ SetPropW(hwnd_, kThisProperty, this);
+ // state_ needs to be STARTED before we request the initial timer
+ state_ = State::STARTED;
+ if (timeout_) {
+ // if someone set the timer before we started
+ RequestCallbackTimer(timeout_);
+ }
+ }
+ break;
+ }
+ };
+ return state_ == State::STARTED;
+}
+
+bool PlatformUIThread::RequestCallbackTimer(unsigned int milliseconds) {
+ CritScope scoped_lock(&cs_);
+
+ switch (state_) {
+ // InternalInit() has yet to run so we do not have a UI thread to use as a
+ // target of the timer. We should just remember what timer interval was
+ // requested and let InternalInit() call this function again when it is
+ // ready.
+ case State::UNSTARTED: {
+ timeout_ = milliseconds;
+ return false;
+ }
+ // We have already stopped, do not schedule a new timer.
+ case State::STOPPED:
+ return false;
+ case State::STARTED: {
+ if (timerid_) {
+ KillTimer(hwnd_, timerid_);
+ }
+ timeout_ = milliseconds;
+ timerid_ = SetTimer(hwnd_, kTimerId, milliseconds, NULL);
+ return !!timerid_;
+ }
+ }
+ // UNREACHABLE
+}
+
+void PlatformUIThread::Stop() {
+ {
+ RTC_DCHECK_RUN_ON(&thread_checker_);
+ CritScope scoped_lock(&cs_);
+ // Shut down the dispatch loop and let the background thread exit.
+ if (timerid_) {
+ MOZ_ASSERT(hwnd_);
+ KillTimer(hwnd_, timerid_);
+ timerid_ = 0;
+ }
+ switch (state_) {
+ // If we haven't started yet there is nothing to do, we will go into
+ // the STOPPED state at the end of the function and InternalInit()
+ // will not move us to STARTED.
+ case State::UNSTARTED:
+ break;
+ // If we have started, that means that InternalInit() has run and the
+ // message wait loop has or will run. We need to signal it to stop. wich
+ // will allow PlatformThread::Stop to join that thread.
+ case State::STARTED: {
+ MOZ_ASSERT(hwnd_);
+ PostMessage(hwnd_, WM_CLOSE, 0, 0);
+ break;
+ }
+ // We have already stopped. There is nothing to do.
+ case State::STOPPED:
+ break;
+ }
+ // Always set our state to STOPPED
+ state_ = State::STOPPED;
+ }
+ monitor_thread_.Finalize();
+}
+
+void PlatformUIThread::Run() {
+ // InternalInit() will return false when the thread is already in shutdown.
+ // otherwise we must run until we get a Windows WM_QUIT msg.
+ const bool runUntilQuitMsg = InternalInit();
+ // The interface contract of Start/Stop is that for a successful call to
+ // Start, there should be at least one call to the run function.
+ NativeEventCallback();
+ while (runUntilQuitMsg) {
+ // Alertable sleep to receive WM_QUIT (following a WM_CLOSE triggering a
+ // WM_DESTROY)
+ if (MsgWaitForMultipleObjectsEx(0, nullptr, INFINITE, QS_ALLINPUT,
+ MWMO_ALERTABLE | MWMO_INPUTAVAILABLE) ==
+ WAIT_OBJECT_0) {
+ MSG msg;
+ if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
+ if (msg.message == WM_QUIT) {
+ // THE ONLY WAY to exit the thread loop
+ break;
+ }
+ TranslateMessage(&msg);
+ DispatchMessage(&msg);
+ }
+ }
+ }
+}
+
+void PlatformUIThread::NativeEventCallback() { native_event_callback_(); }
+
+/* static */
+LRESULT CALLBACK PlatformUIThread::EventWindowProc(HWND hwnd, UINT uMsg,
+ WPARAM wParam,
+ LPARAM lParam) {
+ if (uMsg == WM_DESTROY) {
+ RemovePropW(hwnd, kThisProperty);
+ PostQuitMessage(0);
+ return 0;
+ }
+
+ PlatformUIThread* twui =
+ static_cast<PlatformUIThread*>(GetPropW(hwnd, kThisProperty));
+ if (!twui) {
+ return DefWindowProc(hwnd, uMsg, wParam, lParam);
+ }
+
+ if (uMsg == WM_TIMER && wParam == kTimerId) {
+ twui->NativeEventCallback();
+ return 0;
+ }
+
+ return DefWindowProc(hwnd, uMsg, wParam, lParam);
+}
+
+} // namespace rtc
+
+#endif
diff --git a/dom/media/systemservices/video_engine/platform_uithread.h b/dom/media/systemservices/video_engine/platform_uithread.h
new file mode 100644
index 0000000000..9c213ca933
--- /dev/null
+++ b/dom/media/systemservices/video_engine/platform_uithread.h
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2015 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+#ifndef RTC_BASE_PLATFORM_UITHREAD_H_
+#define RTC_BASE_PLATFORM_UITHREAD_H_
+
+#if defined(WEBRTC_WIN)
+# include "Assertions.h"
+# include "rtc_base/deprecated/recursive_critical_section.h"
+# include "rtc_base/platform_thread.h"
+# include "api/sequence_checker.h"
+# include "ThreadSafety.h"
+
+namespace rtc {
+/*
+ * Windows UI thread for screen capture
+ * Launches a thread which enters a message wait loop after calling the
+ * provided ThreadRunFunction once. A repeating timer event might be registered
+ * with a callback through the Win32 API. If so, that timer will cause WM_TIMER
+ * messages to appear in the threads message queue. This will wake the thread
+ * which will then first look to see if it received the WM_QUIT message, then
+ * it will pass any non WM_QUIT messages on to the registered message handlers
+ * (synchronously on the current thread). In the case oF WM_TIMER the
+ * registered handler calls the NativeEventCallback which is simply the
+ * ThreadRunFunction which was passed to the constructor.
+ *
+ * Shutdown of the message wait loop is triggered by sending a WM_CLOSE which
+ * will start tearing down the "window" which hosts the UI thread. This will
+ * cause a WM_DESTROY message to be received. Upon reception a WM_QUIT message
+ * is enqueued. When the message wait loop receives a WM_QUIT message it stops,
+ * thus allowing the thread to be joined.
+ *
+ * Note: that the only source of a WM_CLOSE should be PlatformUIThread::Stop.
+ * Note: because PlatformUIThread::Stop is called from a different thread than
+ * PlatformUIThread::Run, it is possible that Stop can race Run.
+ *
+ * After being stopped PlatformUIThread can not be started again.
+ *
+ */
+
+class PlatformUIThread {
+ public:
+ PlatformUIThread(std::function<void()> func, const char* name,
+ ThreadAttributes attributes)
+ : name_(name),
+ native_event_callback_(std::move(func)),
+ monitor_thread_(PlatformThread::SpawnJoinable([this]() { Run(); }, name,
+ attributes)) {}
+
+ virtual ~PlatformUIThread();
+
+ void Stop();
+
+ /**
+ * Request a recurring callback.
+ */
+ bool RequestCallbackTimer(unsigned int milliseconds);
+
+ protected:
+ void Run();
+
+ private:
+ static LRESULT CALLBACK EventWindowProc(HWND, UINT, WPARAM, LPARAM);
+ void NativeEventCallback();
+ // Initialize the UI thread that is servicing the timer events
+ bool InternalInit();
+
+ // Needs to be initialized before monitor_thread_ as it takes a string view to
+ // name_
+ std::string name_;
+ RecursiveCriticalSection cs_;
+ std::function<void()> native_event_callback_;
+ webrtc::SequenceChecker thread_checker_;
+ PlatformThread monitor_thread_;
+ HWND hwnd_ MOZ_GUARDED_BY(cs_) = nullptr;
+ UINT_PTR timerid_ MOZ_GUARDED_BY(cs_) = 0;
+ unsigned int timeout_ MOZ_GUARDED_BY(cs_) = 0;
+ enum class State {
+ UNSTARTED,
+ STARTED,
+ STOPPED,
+ };
+ State state_ MOZ_GUARDED_BY(cs_) = State::UNSTARTED;
+};
+
+} // namespace rtc
+
+#endif
+#endif // RTC_BASE_PLATFORM_UITHREAD_H_
diff --git a/dom/media/systemservices/video_engine/tab_capturer.cc b/dom/media/systemservices/video_engine/tab_capturer.cc
new file mode 100644
index 0000000000..793d965028
--- /dev/null
+++ b/dom/media/systemservices/video_engine/tab_capturer.cc
@@ -0,0 +1,331 @@
+/*
+ * Copyright (c) 2013 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+#include "tab_capturer.h"
+
+#include "desktop_device_info.h"
+#include "modules/desktop_capture/desktop_capture_options.h"
+#include "modules/desktop_capture/desktop_frame.h"
+#include "mozilla/Logging.h"
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/ImageBitmap.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/PromiseNativeHandler.h"
+#include "mozilla/dom/WindowGlobalParent.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/SpinEventLoopUntil.h"
+#include "mozilla/TaskQueue.h"
+#include "nsThreadUtils.h"
+#include "rtc_base/checks.h"
+#include "rtc_base/logging.h"
+
+mozilla::LazyLogModule gTabShareLog("TabShare");
+#define LOG_FUNC_IMPL(level) \
+ MOZ_LOG( \
+ gTabShareLog, level, \
+ ("TabCapturerWebrtc %p: %s id=%" PRIu64, this, __func__, mBrowserId))
+#define LOG_FUNC() LOG_FUNC_IMPL(LogLevel::Debug)
+#define LOG_FUNCV() LOG_FUNC_IMPL(LogLevel::Verbose)
+
+using namespace mozilla::dom;
+
+namespace mozilla {
+
+class CaptureFrameRequest {
+ using CapturePromise = TabCapturerWebrtc::CapturePromise;
+
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CaptureFrameRequest)
+
+ CaptureFrameRequest() : mCaptureTime(TimeStamp::Now()) {}
+
+ operator MozPromiseRequestHolder<CapturePromise>&() { return mRequest; }
+
+ void Complete() { mRequest.Complete(); }
+ void Disconnect() { mRequest.Disconnect(); }
+ bool Exists() { return mRequest.Exists(); }
+
+ protected:
+ virtual ~CaptureFrameRequest() { MOZ_RELEASE_ASSERT(!Exists()); }
+
+ public:
+ const TimeStamp mCaptureTime;
+
+ private:
+ MozPromiseRequestHolder<CapturePromise> mRequest;
+};
+
+TabCapturerWebrtc::TabCapturerWebrtc(
+ SourceId aSourceId, nsCOMPtr<nsISerialEventTarget> aCaptureThread)
+ : mBrowserId(aSourceId),
+ mMainThreadWorker(
+ TaskQueue::Create(do_AddRef(GetMainThreadSerialEventTarget()),
+ "TabCapturerWebrtc::mMainThreadWorker")),
+ mCallbackWorker(TaskQueue::Create(aCaptureThread.forget(),
+ "TabCapturerWebrtc::mCallbackWorker")) {
+ RTC_DCHECK_RUN_ON(&mControlChecker);
+ MOZ_ASSERT(aSourceId != 0);
+ mCallbackChecker.Detach();
+
+ LOG_FUNC();
+}
+
+// static
+std::unique_ptr<webrtc::DesktopCapturer> TabCapturerWebrtc::Create(
+ SourceId aSourceId, nsCOMPtr<nsISerialEventTarget> aCaptureThread) {
+ return std::unique_ptr<webrtc::DesktopCapturer>(
+ new TabCapturerWebrtc(aSourceId, std::move(aCaptureThread)));
+}
+
+TabCapturerWebrtc::~TabCapturerWebrtc() {
+ RTC_DCHECK_RUN_ON(&mCallbackChecker);
+ LOG_FUNC();
+
+ // mMainThreadWorker handles frame capture requests async. Since we're in the
+ // dtor, no more frame capture requests can be made through CaptureFrame(). It
+ // can be shut down now.
+ mMainThreadWorker->BeginShutdown();
+
+ // There may still be async frame capture requests in flight, waiting to be
+ // reported to mCallback on mCallbackWorker. Disconnect them (must be done on
+ // mCallbackWorker) and shut down mCallbackWorker to ensure nothing more can
+ // get queued to it.
+ MOZ_ALWAYS_SUCCEEDS(
+ mCallbackWorker->Dispatch(NS_NewRunnableFunction(__func__, [this] {
+ RTC_DCHECK_RUN_ON(&mCallbackChecker);
+ for (const auto& req : mRequests) {
+ DisconnectRequest(req);
+ }
+ mCallbackWorker->BeginShutdown();
+ })));
+
+ // Block until the workers have run all pending tasks. We must do this for two
+ // reasons:
+ // - All runnables dispatched to mMainThreadWorker and mCallbackWorker capture
+ // the raw pointer `this` as they rely on `this` outliving the worker
+ // TaskQueues.
+ // - mCallback is only guaranteed to outlive `this`. No calls can be made to
+ // it after the dtor is finished.
+
+ // Spin the underlying thread of mCallbackWorker, which we are currently on,
+ // until it is empty. We have no other way of waiting for mCallbackWorker to
+ // become empty while blocking the current call.
+ SpinEventLoopUntil<ProcessFailureBehavior::IgnoreAndContinue>(
+ "~TabCapturerWebrtc"_ns, [&] { return mCallbackWorker->IsEmpty(); });
+
+ // No need to await shutdown since it was shut down synchronously above.
+ mMainThreadWorker->AwaitIdle();
+}
+
+bool TabCapturerWebrtc::GetSourceList(
+ webrtc::DesktopCapturer::SourceList* aSources) {
+ MOZ_LOG(gTabShareLog, LogLevel::Debug,
+ ("TabShare: GetSourceList, result %zu", aSources->size()));
+ // XXX UI
+ return true;
+}
+
+bool TabCapturerWebrtc::SelectSource(webrtc::DesktopCapturer::SourceId) {
+ MOZ_ASSERT_UNREACHABLE("Source is passed through ctor for constness");
+ return true;
+}
+
+bool TabCapturerWebrtc::FocusOnSelectedSource() { return true; }
+
+void TabCapturerWebrtc::Start(webrtc::DesktopCapturer::Callback* aCallback) {
+ RTC_DCHECK_RUN_ON(&mCallbackChecker);
+ RTC_DCHECK(!mCallback);
+ RTC_DCHECK(aCallback);
+
+ LOG_FUNC();
+
+ mCallback = aCallback;
+}
+
+void TabCapturerWebrtc::CaptureFrame() {
+ RTC_DCHECK_RUN_ON(&mCallbackChecker);
+ LOG_FUNCV();
+ if (mRequests.GetSize() > 2) {
+ // Allow two async capture requests in flight
+ OnCaptureFrameFailure();
+ return;
+ }
+
+ auto request = MakeRefPtr<CaptureFrameRequest>();
+ InvokeAsync(mMainThreadWorker, __func__, [this] { return CaptureFrameNow(); })
+ ->Then(mCallbackWorker, __func__,
+ [this, request](CapturePromise::ResolveOrRejectValue&& aValue) {
+ if (!CompleteRequest(request)) {
+ // Request was disconnected or overrun. Failure has already
+ // been reported to the callback elsewhere.
+ return;
+ }
+
+ if (aValue.IsReject()) {
+ OnCaptureFrameFailure();
+ return;
+ }
+
+ OnCaptureFrameSuccess(std::move(aValue.ResolveValue()));
+ })
+ ->Track(*request);
+ mRequests.PushFront(request.forget());
+}
+
+void TabCapturerWebrtc::OnCaptureFrameSuccess(
+ UniquePtr<dom::ImageBitmapCloneData> aData) {
+ RTC_DCHECK_RUN_ON(&mCallbackChecker);
+ MOZ_DIAGNOSTIC_ASSERT(aData);
+ LOG_FUNCV();
+ webrtc::DesktopSize size(aData->mPictureRect.Width(),
+ aData->mPictureRect.Height());
+ webrtc::DesktopRect rect = webrtc::DesktopRect::MakeSize(size);
+ std::unique_ptr<webrtc::DesktopFrame> frame(
+ new webrtc::BasicDesktopFrame(size));
+
+ gfx::DataSourceSurface::ScopedMap map(aData->mSurface,
+ gfx::DataSourceSurface::READ);
+ if (!map.IsMapped()) {
+ OnCaptureFrameFailure();
+ return;
+ }
+ frame->CopyPixelsFrom(map.GetData(), map.GetStride(), rect);
+
+ mCallback->OnCaptureResult(webrtc::DesktopCapturer::Result::SUCCESS,
+ std::move(frame));
+}
+
+void TabCapturerWebrtc::OnCaptureFrameFailure() {
+ RTC_DCHECK_RUN_ON(&mCallbackChecker);
+ LOG_FUNC();
+ mCallback->OnCaptureResult(webrtc::DesktopCapturer::Result::ERROR_TEMPORARY,
+ nullptr);
+}
+
+bool TabCapturerWebrtc::IsOccluded(const webrtc::DesktopVector& aPos) {
+ return false;
+}
+
+class TabCapturedHandler final : public PromiseNativeHandler {
+ public:
+ NS_DECL_ISUPPORTS
+
+ using CapturePromise = TabCapturerWebrtc::CapturePromise;
+
+ static void Create(Promise* aPromise,
+ MozPromiseHolder<CapturePromise> aHolder) {
+ MOZ_ASSERT(aPromise);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<TabCapturedHandler> handler =
+ new TabCapturedHandler(std::move(aHolder));
+ aPromise->AppendNativeHandler(handler);
+ }
+
+ void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (NS_WARN_IF(!aValue.isObject())) {
+ mHolder.Reject(NS_ERROR_UNEXPECTED, __func__);
+ return;
+ }
+
+ RefPtr<ImageBitmap> bitmap;
+ if (NS_WARN_IF(NS_FAILED(
+ UNWRAP_OBJECT(ImageBitmap, &aValue.toObject(), bitmap)))) {
+ mHolder.Reject(NS_ERROR_UNEXPECTED, __func__);
+ return;
+ }
+
+ UniquePtr<ImageBitmapCloneData> data = bitmap->ToCloneData();
+ if (!data) {
+ mHolder.Reject(NS_ERROR_UNEXPECTED, __func__);
+ return;
+ }
+
+ mHolder.Resolve(std::move(data), __func__);
+ }
+
+ void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override {
+ MOZ_ASSERT(NS_IsMainThread());
+ mHolder.Reject(aRv.StealNSResult(), __func__);
+ }
+
+ private:
+ explicit TabCapturedHandler(MozPromiseHolder<CapturePromise> aHolder)
+ : mHolder(std::move(aHolder)) {}
+
+ ~TabCapturedHandler() = default;
+
+ MozPromiseHolder<CapturePromise> mHolder;
+};
+
+NS_IMPL_ISUPPORTS0(TabCapturedHandler)
+
+bool TabCapturerWebrtc::CompleteRequest(CaptureFrameRequest* aRequest) {
+ RTC_DCHECK_RUN_ON(&mCallbackChecker);
+ if (!aRequest->Exists()) {
+ // Request was disconnected or overrun. mCallback has already been notified.
+ return false;
+ }
+ while (CaptureFrameRequest* req = mRequests.Peek()) {
+ if (req->mCaptureTime > aRequest->mCaptureTime) {
+ break;
+ }
+ // Pop the request before calling the callback, in case it could mutate
+ // mRequests, now or in the future.
+ RefPtr<CaptureFrameRequest> dropMe = mRequests.Pop();
+ req->Complete();
+ if (req->mCaptureTime < aRequest->mCaptureTime) {
+ OnCaptureFrameFailure();
+ }
+ }
+ MOZ_DIAGNOSTIC_ASSERT(!aRequest->Exists());
+ return true;
+}
+
+void TabCapturerWebrtc::DisconnectRequest(CaptureFrameRequest* aRequest) {
+ RTC_DCHECK_RUN_ON(&mCallbackChecker);
+ LOG_FUNCV();
+ aRequest->Disconnect();
+ OnCaptureFrameFailure();
+}
+
+auto TabCapturerWebrtc::CaptureFrameNow() -> RefPtr<CapturePromise> {
+ MOZ_ASSERT(mMainThreadWorker->IsOnCurrentThread());
+ LOG_FUNCV();
+
+ WindowGlobalParent* wgp = nullptr;
+ RefPtr<BrowsingContext> context =
+ BrowsingContext::GetCurrentTopByBrowserId(mBrowserId);
+ if (context) {
+ wgp = context->Canonical()->GetCurrentWindowGlobal();
+ }
+ if (!wgp) {
+ // If we can't access the window, we just won't capture anything
+ return CapturePromise::CreateAndReject(NS_ERROR_NOT_AVAILABLE, __func__);
+ }
+
+ // XXX This would be more efficient if we used CrossProcessPaint directly and
+ // returned a surface.
+ RefPtr<Promise> promise =
+ wgp->DrawSnapshot(nullptr, 1.0, "white"_ns, false, IgnoreErrors());
+ if (!promise) {
+ return CapturePromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+
+ MozPromiseHolder<CapturePromise> holder;
+ RefPtr<CapturePromise> p = holder.Ensure(__func__);
+ TabCapturedHandler::Create(promise, std::move(holder));
+ return p;
+}
+
+} // namespace mozilla
diff --git a/dom/media/systemservices/video_engine/tab_capturer.h b/dom/media/systemservices/video_engine/tab_capturer.h
new file mode 100644
index 0000000000..92c4fa2ad1
--- /dev/null
+++ b/dom/media/systemservices/video_engine/tab_capturer.h
@@ -0,0 +1,88 @@
+/*
+ * Copyright 2018 The WebRTC project authors. All Rights Reserved.
+ *
+ * Use of this source code is governed by a BSD-style license
+ * that can be found in the LICENSE file in the root of the source
+ * tree. An additional intellectual property rights grant can be found
+ * in the file PATENTS. All contributing project authors may
+ * be found in the AUTHORS file in the root of the source tree.
+ */
+
+#ifndef MODULES_DESKTOP_CAPTURE_TAB_CAPTURER_H_
+#define MODULES_DESKTOP_CAPTURE_TAB_CAPTURER_H_
+
+#include "api/sequence_checker.h"
+#include "modules/desktop_capture/desktop_capturer.h"
+#include "mozilla/MozPromise.h"
+#include "nsDeque.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+namespace dom {
+struct ImageBitmapCloneData;
+} // namespace dom
+
+class CaptureFrameRequest;
+class TabCapturedHandler;
+class TaskQueue;
+
+class TabCapturerWebrtc : public webrtc::DesktopCapturer {
+ protected:
+ TabCapturerWebrtc(SourceId aSourceId,
+ nsCOMPtr<nsISerialEventTarget> aCaptureThread);
+ ~TabCapturerWebrtc();
+
+ public:
+ friend class CaptureFrameRequest;
+ friend class TabCapturedHandler;
+
+ static std::unique_ptr<webrtc::DesktopCapturer> Create(
+ SourceId aSourceId, nsCOMPtr<nsISerialEventTarget> aCaptureThread);
+
+ TabCapturerWebrtc(const TabCapturerWebrtc&) = delete;
+ TabCapturerWebrtc& operator=(const TabCapturerWebrtc&) = delete;
+
+ // DesktopCapturer interface.
+ void Start(Callback* aCallback) override;
+ void CaptureFrame() override;
+ bool GetSourceList(SourceList* aSources) override;
+ bool SelectSource(SourceId) override;
+ bool FocusOnSelectedSource() override;
+ bool IsOccluded(const webrtc::DesktopVector& aPos) override;
+
+ private:
+ // Capture code
+ using CapturePromise =
+ MozPromise<UniquePtr<dom::ImageBitmapCloneData>, nsresult, true>;
+ RefPtr<CapturePromise> CaptureFrameNow();
+
+ // Helper that checks for overrun requests. Returns true if aRequest had not
+ // been dropped due to disconnection or overrun.
+ // Note that if this returns true, the caller takes the responsibility to call
+ // mCallback with a capture result for aRequest.
+ bool CompleteRequest(CaptureFrameRequest* aRequest);
+
+ // Helper that disconnects the request, and notifies mCallback of a temporary
+ // failure.
+ void DisconnectRequest(CaptureFrameRequest* aRequest);
+
+ // Handle the result from the async callback from CaptureFrameNow.
+ void OnCaptureFrameSuccess(UniquePtr<dom::ImageBitmapCloneData> aData);
+ void OnCaptureFrameFailure();
+
+ const uint64_t mBrowserId;
+ const RefPtr<TaskQueue> mMainThreadWorker;
+ const RefPtr<TaskQueue> mCallbackWorker;
+ webrtc::SequenceChecker mControlChecker;
+ webrtc::SequenceChecker mCallbackChecker;
+ // Set in Start() and guaranteed by the owner of this class to outlive us.
+ webrtc::DesktopCapturer::Callback* mCallback
+ RTC_GUARDED_BY(mCallbackChecker) = nullptr;
+
+ // mCallbackWorker only
+ nsRefPtrDeque<CaptureFrameRequest> mRequests RTC_GUARDED_BY(mCallbackChecker);
+};
+
+} // namespace mozilla
+
+#endif // MODULES_DESKTOP_CAPTURE_TAB_CAPTURER_H_
diff --git a/dom/media/test/16bit_wave_extrametadata.wav b/dom/media/test/16bit_wave_extrametadata.wav
new file mode 100644
index 0000000000..443ec73a3d
--- /dev/null
+++ b/dom/media/test/16bit_wave_extrametadata.wav
Binary files differ
diff --git a/dom/media/test/16bit_wave_extrametadata.wav^headers^ b/dom/media/test/16bit_wave_extrametadata.wav^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/16bit_wave_extrametadata.wav^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/320x240.ogv b/dom/media/test/320x240.ogv
new file mode 100644
index 0000000000..093158432a
--- /dev/null
+++ b/dom/media/test/320x240.ogv
Binary files differ
diff --git a/dom/media/test/320x240.ogv^headers^ b/dom/media/test/320x240.ogv^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/320x240.ogv^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/448636.ogv b/dom/media/test/448636.ogv
new file mode 100644
index 0000000000..628df924f8
--- /dev/null
+++ b/dom/media/test/448636.ogv
Binary files differ
diff --git a/dom/media/test/448636.ogv^headers^ b/dom/media/test/448636.ogv^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/448636.ogv^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/A4.ogv b/dom/media/test/A4.ogv
new file mode 100644
index 0000000000..de99616ece
--- /dev/null
+++ b/dom/media/test/A4.ogv
Binary files differ
diff --git a/dom/media/test/A4.ogv^headers^ b/dom/media/test/A4.ogv^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/A4.ogv^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/TestPatternHDR.mp4 b/dom/media/test/TestPatternHDR.mp4
new file mode 100644
index 0000000000..2aeb5d7f05
--- /dev/null
+++ b/dom/media/test/TestPatternHDR.mp4
Binary files differ
diff --git a/dom/media/test/VID_0001.ogg b/dom/media/test/VID_0001.ogg
new file mode 100644
index 0000000000..0068b9af85
--- /dev/null
+++ b/dom/media/test/VID_0001.ogg
Binary files differ
diff --git a/dom/media/test/VID_0001.ogg^headers^ b/dom/media/test/VID_0001.ogg^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/VID_0001.ogg^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/adts.aac b/dom/media/test/adts.aac
new file mode 100644
index 0000000000..208515464a
--- /dev/null
+++ b/dom/media/test/adts.aac
Binary files differ
diff --git a/dom/media/test/adts.aac^headers^ b/dom/media/test/adts.aac^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/adts.aac^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/allowed.sjs b/dom/media/test/allowed.sjs
new file mode 100644
index 0000000000..4460cd05af
--- /dev/null
+++ b/dom/media/test/allowed.sjs
@@ -0,0 +1,61 @@
+function parseQuery(request, key) {
+ var params = request.queryString.split("&");
+ for (var j = 0; j < params.length; ++j) {
+ var p = params[j];
+ if (p == key) {
+ return true;
+ }
+ if (p.indexOf(key + "=") == 0) {
+ return p.substring(key.length + 1);
+ }
+ if (!p.includes("=") && key == "") {
+ return p;
+ }
+ }
+ return false;
+}
+
+var types = {
+ js: "text/javascript",
+ m4s: "video/mp4",
+ mp4: "video/mp4",
+ ogg: "video/ogg",
+ ogv: "video/ogg",
+ oga: "audio/ogg",
+ webm: "video/webm",
+ wav: "audio/x-wav",
+};
+
+// Return file with name as per the query string with access control
+// allow headers.
+function handleRequest(request, response) {
+ var resource = parseQuery(request, "");
+
+ var file = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
+ var fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ var bis = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+ Ci.nsIBinaryInputStream
+ );
+ var paths = "tests/dom/media/test/" + resource;
+ var split = paths.split("/");
+ for (var i = 0; i < split.length; ++i) {
+ file.append(split[i]);
+ }
+ fis.init(file, -1, -1, false);
+ dump("file=" + file + "\n");
+ bis.setInputStream(fis);
+ var bytes = bis.readBytes(bis.available());
+ response.setStatusLine(request.httpVersion, 206, "Partial Content");
+ response.setHeader(
+ "Content-Range",
+ "bytes 0-" + (bytes.length - 1) + "/" + bytes.length
+ );
+ response.setHeader("Content-Length", "" + bytes.length, false);
+ var ext = resource.substring(resource.lastIndexOf(".") + 1);
+ response.setHeader("Content-Type", types[ext], false);
+ response.setHeader("Access-Control-Allow-Origin", "*");
+ response.write(bytes, bytes.length);
+ bis.close();
+}
diff --git a/dom/media/test/ambisonics.mp4 b/dom/media/test/ambisonics.mp4
new file mode 100644
index 0000000000..4f5bcdfd26
--- /dev/null
+++ b/dom/media/test/ambisonics.mp4
Binary files differ
diff --git a/dom/media/test/ambisonics.mp4^headers^ b/dom/media/test/ambisonics.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/ambisonics.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/audio-gaps-short.ogg b/dom/media/test/audio-gaps-short.ogg
new file mode 100644
index 0000000000..e01a24bfda
--- /dev/null
+++ b/dom/media/test/audio-gaps-short.ogg
Binary files differ
diff --git a/dom/media/test/audio-gaps-short.ogg^headers^ b/dom/media/test/audio-gaps-short.ogg^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/audio-gaps-short.ogg^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/audio-gaps.ogg b/dom/media/test/audio-gaps.ogg
new file mode 100644
index 0000000000..ce96748ccd
--- /dev/null
+++ b/dom/media/test/audio-gaps.ogg
Binary files differ
diff --git a/dom/media/test/audio-gaps.ogg^headers^ b/dom/media/test/audio-gaps.ogg^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/audio-gaps.ogg^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/audio-overhang.ogg b/dom/media/test/audio-overhang.ogg
new file mode 100644
index 0000000000..c07986e7a1
--- /dev/null
+++ b/dom/media/test/audio-overhang.ogg
Binary files differ
diff --git a/dom/media/test/audio-overhang.ogg^headers^ b/dom/media/test/audio-overhang.ogg^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/audio-overhang.ogg^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/audio.wav b/dom/media/test/audio.wav
new file mode 100644
index 0000000000..c6fd5cb869
--- /dev/null
+++ b/dom/media/test/audio.wav
Binary files differ
diff --git a/dom/media/test/audio.wav^headers^ b/dom/media/test/audio.wav^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/audio.wav^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/av1.mp4 b/dom/media/test/av1.mp4
new file mode 100644
index 0000000000..28de929a29
--- /dev/null
+++ b/dom/media/test/av1.mp4
Binary files differ
diff --git a/dom/media/test/av1.mp4^headers^ b/dom/media/test/av1.mp4^headers^
new file mode 100644
index 0000000000..2567dc2fe5
--- /dev/null
+++ b/dom/media/test/av1.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store \ No newline at end of file
diff --git a/dom/media/test/background_video.js b/dom/media/test/background_video.js
new file mode 100644
index 0000000000..508f8fd89a
--- /dev/null
+++ b/dom/media/test/background_video.js
@@ -0,0 +1,224 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file expects manager to be defined in the global scope.
+/* global manager */
+/* import-globals-from manifest.js */
+
+"use strict";
+
+function startTest(test) {
+ info(test.desc);
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({ set: test.prefs }, () => {
+ manager.runTests(test.tests, test.runTest);
+ });
+}
+
+function nextVideoEnded(video) {
+ return nextEvent(video, "ended");
+}
+
+function nextVideoPlaying(video) {
+ return nextEvent(video, "playing");
+}
+
+function nextVideoResumes(video) {
+ return nextEvent(video, "mozexitvideosuspend");
+}
+
+function nextVideoSuspends(video) {
+ return nextEvent(video, "mozentervideosuspend");
+}
+
+/**
+ * @param {string} url video src.
+ * @returns {HTMLMediaElement} The created video element.
+ */
+function appendVideoToDoc(url, token, width, height) {
+ // Default size of (160, 120) is used by other media tests.
+ if (width === undefined) {
+ width = 160;
+ }
+ if (height === undefined) {
+ height = (3 * width) / 4;
+ }
+
+ let v = document.createElement("video");
+ v.token = token;
+ v.width = width;
+ v.height = height;
+ v.src = url;
+ document.body.appendChild(v);
+ return v;
+}
+
+function appendVideoToDocWithoutLoad(token, width, height) {
+ // Default size of (160, 120) is used by other media tests.
+ if (width === undefined) {
+ width = 160;
+ }
+ if (height === undefined) {
+ height = (3 * width) / 4;
+ }
+
+ let v = document.createElement("video");
+ v.token = token;
+ document.body.appendChild(v);
+ v.width = width;
+ v.height = height;
+ return v;
+}
+
+function loadAndWaitUntilLoadedmetadata(video, url, preloadType = "metadata") {
+ return new Promise((resolve, reject) => {
+ video.preload = preloadType;
+ video.addEventListener(
+ "loadedmetadata",
+ () => {
+ resolve();
+ },
+ true
+ );
+ video.src = url;
+ });
+}
+
+/**
+ * @param {HTMLMediaElement} video Video element with under test.
+ * @returns {Promise} Promise that is resolved when video 'visibilitychanged' event fires.
+ */
+function waitUntilVisible(video) {
+ let videoChrome = SpecialPowers.wrap(video);
+ if (videoChrome.isInViewPort) {
+ return Promise.resolve();
+ }
+
+ return new Promise(resolve => {
+ videoChrome.addEventListener("visibilitychanged", () => {
+ if (videoChrome.isInViewPort) {
+ ok(true, `${video.token} is visible.`);
+ videoChrome.removeEventListener("visibilitychanged", this);
+ resolve();
+ }
+ });
+ });
+}
+
+/**
+ * @param {HTMLMediaElement} video Video element under test.
+ * @returns {Promise} Promise that is resolved when video 'playing' event fires.
+ */
+function waitUntilPlaying(video) {
+ var p = once(video, "playing", () => {
+ ok(true, `${video.token} played.`);
+ });
+ Log(video.token, "Start playing");
+ video.play();
+ return p;
+}
+
+/**
+ * @param {HTMLMediaElement} video Video element under test.
+ * @returns {Promise} Promise which is resolved when video 'ended' event fires.
+ */
+function waitUntilEnded(video) {
+ Log(video.token, "Waiting for ended");
+ if (video.ended) {
+ ok(true, video.token + " already ended");
+ return Promise.resolve();
+ }
+
+ return once(video, "ended", () => {
+ ok(true, `${video.token} ended`);
+ });
+}
+
+/**
+ * @param {HTMLMediaElement} video Video element under test.
+ * @returns {Promise} Promise that is resolved when video decode starts
+ * suspend timer.
+ */
+function testSuspendTimerStartedWhenHidden(video) {
+ var p = once(video, "mozstartvideosuspendtimer").then(() => {
+ ok(true, `${video.token} suspend begins`);
+ });
+ Log(video.token, "Set Hidden");
+ video.setVisible(false);
+ return p;
+}
+
+/**
+ * @param {HTMLMediaElement} video Video element under test.
+ * @returns {Promise} Promise that is resolved when video decode suspends.
+ */
+function testVideoSuspendsWhenHidden(video) {
+ let p = once(video, "mozentervideosuspend").then(() => {
+ ok(true, `${video.token} suspends`);
+ });
+ Log(video.token, "Set hidden");
+ video.setVisible(false);
+ return p;
+}
+
+/**
+ * @param {HTMLMediaElement} video Video element under test.
+ * @returns {Promise} Promise that is resolved when video decode resumes.
+ */
+function testVideoResumesWhenShown(video) {
+ var p = once(video, "mozexitvideosuspend").then(() => {
+ ok(true, `${video.token} resumes`);
+ });
+ Log(video.token, "Set visible");
+ video.setVisible(true);
+ return p;
+}
+
+/**
+ * @param {HTMLMediaElement} video Video element under test.
+ * @returns {Promise} Promise that is resolved when video decode resumes.
+ */
+function testVideoOnlySeekCompletedWhenShown(video) {
+ var p = once(video, "mozvideoonlyseekcompleted").then(() => {
+ ok(true, `${video.token} resumes`);
+ });
+ Log(video.token, "Set visible");
+ video.setVisible(true);
+ return p;
+}
+
+/**
+ * @param {HTMLVideoElement} video Video element under test.
+ * @returns {Promise} Promise that is resolved if video ends and rejects if video suspends.
+ */
+function checkVideoDoesntSuspend(video) {
+ let p = Promise.race([
+ waitUntilEnded(video).then(() => {
+ ok(true, `${video.token} ended before decode was suspended`);
+ }),
+ once(video, "mozentervideosuspend", () => {
+ Promise.reject(new Error(`${video.token} suspended`));
+ }),
+ ]);
+ Log(video.token, "Set hidden.");
+ video.setVisible(false);
+ return p;
+}
+
+/**
+ * @param {HTMLMediaElement} video Video element under test.
+ * @param {number} time video current time to wait til.
+ * @returns {Promise} Promise that is resolved once currentTime passes time.
+ */
+function waitTil(video, time) {
+ Log(video.token, `Waiting for time to reach ${time}s`);
+ return new Promise(resolve => {
+ video.addEventListener("timeupdate", function timeUpdateEvent() {
+ if (video.currentTime > time) {
+ video.removeEventListener(name, timeUpdateEvent);
+ resolve();
+ }
+ });
+ });
+}
diff --git a/dom/media/test/badtags.ogg b/dom/media/test/badtags.ogg
new file mode 100644
index 0000000000..12d8358730
--- /dev/null
+++ b/dom/media/test/badtags.ogg
Binary files differ
diff --git a/dom/media/test/badtags.ogg^headers^ b/dom/media/test/badtags.ogg^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/badtags.ogg^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bear-640x360-a_frag-cenc-key_rotation.mp4 b/dom/media/test/bear-640x360-a_frag-cenc-key_rotation.mp4
new file mode 100644
index 0000000000..dc4f197ffa
--- /dev/null
+++ b/dom/media/test/bear-640x360-a_frag-cenc-key_rotation.mp4
Binary files differ
diff --git a/dom/media/test/bear-640x360-v_frag-cenc-key_rotation.mp4 b/dom/media/test/bear-640x360-v_frag-cenc-key_rotation.mp4
new file mode 100644
index 0000000000..916c64e9ee
--- /dev/null
+++ b/dom/media/test/bear-640x360-v_frag-cenc-key_rotation.mp4
Binary files differ
diff --git a/dom/media/test/beta-phrasebook.ogg b/dom/media/test/beta-phrasebook.ogg
new file mode 100644
index 0000000000..7e6ef77ec4
--- /dev/null
+++ b/dom/media/test/beta-phrasebook.ogg
Binary files differ
diff --git a/dom/media/test/beta-phrasebook.ogg^headers^ b/dom/media/test/beta-phrasebook.ogg^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/beta-phrasebook.ogg^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/big-buck-bunny-cenc-avc3-1.m4s b/dom/media/test/big-buck-bunny-cenc-avc3-1.m4s
new file mode 100644
index 0000000000..266ec4c100
--- /dev/null
+++ b/dom/media/test/big-buck-bunny-cenc-avc3-1.m4s
Binary files differ
diff --git a/dom/media/test/big-buck-bunny-cenc-avc3-1.m4s^headers^ b/dom/media/test/big-buck-bunny-cenc-avc3-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/big-buck-bunny-cenc-avc3-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/big-buck-bunny-cenc-avc3-init.mp4 b/dom/media/test/big-buck-bunny-cenc-avc3-init.mp4
new file mode 100644
index 0000000000..7aeb3eca8a
--- /dev/null
+++ b/dom/media/test/big-buck-bunny-cenc-avc3-init.mp4
Binary files differ
diff --git a/dom/media/test/big-buck-bunny-cenc-avc3-init.mp4^headers^ b/dom/media/test/big-buck-bunny-cenc-avc3-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/big-buck-bunny-cenc-avc3-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/big-short.wav b/dom/media/test/big-short.wav
new file mode 100644
index 0000000000..c850e5fd14
--- /dev/null
+++ b/dom/media/test/big-short.wav
Binary files differ
diff --git a/dom/media/test/big-short.wav^headers^ b/dom/media/test/big-short.wav^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/big-short.wav^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/big.wav b/dom/media/test/big.wav
new file mode 100644
index 0000000000..5f66bc1f02
--- /dev/null
+++ b/dom/media/test/big.wav
Binary files differ
diff --git a/dom/media/test/big.wav^headers^ b/dom/media/test/big.wav^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/big.wav^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop-cenc-audio-key1.xml b/dom/media/test/bipbop-cenc-audio-key1.xml
new file mode 100644
index 0000000000..a1672eecef
--- /dev/null
+++ b/dom/media/test/bipbop-cenc-audio-key1.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ This XML file describes the encryption applied to |bipbop_<res>-cenc*|. To
+ generate the encrypted files, run bipbop-cenc.sh
+-->
+
+<GPACDRM type="CENC AES-CTR">
+
+ <DRMInfo type="pssh" version="1">
+ <!--
+ SystemID specified in
+ https://dvcs.w3.org/hg/html-media/raw-file/tip/encrypted-media/cenc-format.html
+ -->
+ <BS ID128="1077efecc0b24d02ace33c1e52e2fb4b" />
+ <!-- Number of KeyIDs = 1 -->
+ <BS bits="32" value="1" />
+ <!-- KeyID -->
+ <BS ID128="0x7e571d047e571d047e571d047e571d21" />
+ </DRMInfo>
+
+ <CrypTrack trackID="2" isEncrypted="1" IV_size="16" saiSavedBox="senc"
+ first_IV="0x00000000000000000000000000000000">
+ <key KID="0x7e571d047e571d047e571d047e571d21"
+ value="0x7e5744447e5744447e5744447e574421" />
+ </CrypTrack>
+
+</GPACDRM>
diff --git a/dom/media/test/bipbop-cenc-audio-key2.xml b/dom/media/test/bipbop-cenc-audio-key2.xml
new file mode 100644
index 0000000000..b706609052
--- /dev/null
+++ b/dom/media/test/bipbop-cenc-audio-key2.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ This XML file describes the encryption applied to |bipbop_<res>-cenc*|. To
+ generate the encrypted files, run bipbop-cenc.sh
+-->
+
+<GPACDRM type="CENC AES-CTR">
+
+ <DRMInfo type="pssh" version="1">
+ <!--
+ SystemID specified in
+ https://dvcs.w3.org/hg/html-media/raw-file/tip/encrypted-media/cenc-format.html
+ -->
+ <BS ID128="1077efecc0b24d02ace33c1e52e2fb4b" />
+ <!-- Number of KeyIDs = 1 -->
+ <BS bits="32" value="1" />
+ <!-- KeyID -->
+ <BS ID128="0x7e571d047e571d047e571d047e571d22" />
+ </DRMInfo>
+
+ <CrypTrack trackID="2" isEncrypted="1" IV_size="16" saiSavedBox="senc"
+ first_IV="0x00000000000000000000000000000000">
+ <key KID="0x7e571d047e571d047e571d047e571d22"
+ value="0x7e5744447e5744447e5744447e574422" />
+ </CrypTrack>
+
+</GPACDRM>
diff --git a/dom/media/test/bipbop-cenc-audio1.m4s b/dom/media/test/bipbop-cenc-audio1.m4s
new file mode 100644
index 0000000000..63cfd66f7e
--- /dev/null
+++ b/dom/media/test/bipbop-cenc-audio1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop-cenc-audio1.m4s^headers^ b/dom/media/test/bipbop-cenc-audio1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop-cenc-audio1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop-cenc-audio2.m4s b/dom/media/test/bipbop-cenc-audio2.m4s
new file mode 100644
index 0000000000..04a6cb6ff9
--- /dev/null
+++ b/dom/media/test/bipbop-cenc-audio2.m4s
Binary files differ
diff --git a/dom/media/test/bipbop-cenc-audio2.m4s^headers^ b/dom/media/test/bipbop-cenc-audio2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop-cenc-audio2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop-cenc-audio3.m4s b/dom/media/test/bipbop-cenc-audio3.m4s
new file mode 100644
index 0000000000..ad0cd72f90
--- /dev/null
+++ b/dom/media/test/bipbop-cenc-audio3.m4s
Binary files differ
diff --git a/dom/media/test/bipbop-cenc-audio3.m4s^headers^ b/dom/media/test/bipbop-cenc-audio3.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop-cenc-audio3.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop-cenc-audioinit.mp4 b/dom/media/test/bipbop-cenc-audioinit.mp4
new file mode 100644
index 0000000000..b827aa49aa
--- /dev/null
+++ b/dom/media/test/bipbop-cenc-audioinit.mp4
Binary files differ
diff --git a/dom/media/test/bipbop-cenc-audioinit.mp4^headers^ b/dom/media/test/bipbop-cenc-audioinit.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop-cenc-audioinit.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop-cenc-video-10s.mp4 b/dom/media/test/bipbop-cenc-video-10s.mp4
new file mode 100644
index 0000000000..abbe4561fd
--- /dev/null
+++ b/dom/media/test/bipbop-cenc-video-10s.mp4
Binary files differ
diff --git a/dom/media/test/bipbop-cenc-video-10s.mp4^headers^ b/dom/media/test/bipbop-cenc-video-10s.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop-cenc-video-10s.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop-cenc-video-key1.xml b/dom/media/test/bipbop-cenc-video-key1.xml
new file mode 100644
index 0000000000..f0d9878fa2
--- /dev/null
+++ b/dom/media/test/bipbop-cenc-video-key1.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ This XML file describes the encryption applied to |bipbop_<res>-cenc*|. To
+ generate the encrypted files, run bipbop-cenc.sh
+-->
+
+<GPACDRM type="CENC AES-CTR">
+
+ <DRMInfo type="pssh" version="1">
+ <!--
+ SystemID specified in
+ https://dvcs.w3.org/hg/html-media/raw-file/tip/encrypted-media/cenc-format.html
+ -->
+ <BS ID128="1077efecc0b24d02ace33c1e52e2fb4b" />
+ <!-- Number of KeyIDs = 1 -->
+ <BS bits="32" value="1" />
+ <!-- KeyID -->
+ <BS ID128="0x7e571d037e571d037e571d037e571d11" />
+ </DRMInfo>
+
+ <CrypTrack trackID="1" isEncrypted="1" IV_size="16" saiSavedBox="senc"
+ first_IV="0x00000000000000000000000000000000">
+ <key KID="0x7e571d037e571d037e571d037e571d11"
+ value="0x7e5733337e5733337e5733337e573311" />
+ </CrypTrack>
+
+</GPACDRM>
diff --git a/dom/media/test/bipbop-cenc-video-key2.xml b/dom/media/test/bipbop-cenc-video-key2.xml
new file mode 100644
index 0000000000..1f320e6336
--- /dev/null
+++ b/dom/media/test/bipbop-cenc-video-key2.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ This XML file describes the encryption applied to |bipbop_<res>-cenc*|. To
+ generate the encrypted files, run bipbop-cenc.sh
+-->
+
+<GPACDRM type="CENC AES-CTR">
+
+ <DRMInfo type="pssh" version="1">
+ <!--
+ SystemID specified in
+ https://dvcs.w3.org/hg/html-media/raw-file/tip/encrypted-media/cenc-format.html
+ -->
+ <BS ID128="1077efecc0b24d02ace33c1e52e2fb4b" />
+ <!-- Number of KeyIDs = 1 -->
+ <BS bits="32" value="1" />
+ <!-- KeyID -->
+ <BS ID128="0x7e571d037e571d037e571d037e571d12" />
+ </DRMInfo>
+
+ <CrypTrack trackID="1" isEncrypted="1" IV_size="16" saiSavedBox="senc"
+ first_IV="0x00000000000000000000000000000000">
+ <key KID="0x7e571d037e571d037e571d037e571d12"
+ value="0x7e5733337e5733337e5733337e573312" />
+ </CrypTrack>
+
+</GPACDRM>
diff --git a/dom/media/test/bipbop-cenc-video1.m4s b/dom/media/test/bipbop-cenc-video1.m4s
new file mode 100644
index 0000000000..755013c11c
--- /dev/null
+++ b/dom/media/test/bipbop-cenc-video1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop-cenc-video1.m4s^headers^ b/dom/media/test/bipbop-cenc-video1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop-cenc-video1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop-cenc-video2.m4s b/dom/media/test/bipbop-cenc-video2.m4s
new file mode 100644
index 0000000000..c884bd95fc
--- /dev/null
+++ b/dom/media/test/bipbop-cenc-video2.m4s
Binary files differ
diff --git a/dom/media/test/bipbop-cenc-video2.m4s^headers^ b/dom/media/test/bipbop-cenc-video2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop-cenc-video2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop-cenc-videoinit.mp4 b/dom/media/test/bipbop-cenc-videoinit.mp4
new file mode 100644
index 0000000000..aa87d0bbe6
--- /dev/null
+++ b/dom/media/test/bipbop-cenc-videoinit.mp4
Binary files differ
diff --git a/dom/media/test/bipbop-cenc-videoinit.mp4^headers^ b/dom/media/test/bipbop-cenc-videoinit.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop-cenc-videoinit.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop-cenc.sh b/dom/media/test/bipbop-cenc.sh
new file mode 100644
index 0000000000..a00c38ae80
--- /dev/null
+++ b/dom/media/test/bipbop-cenc.sh
@@ -0,0 +1,29 @@
+mkdir work.tmp
+
+for r in 225w_175kbps 300_215kbps 300wp_227kbps 360w_253kbps 480_624kbps 480wp_663kbps 480_959kbps 480wp_1001kbps
+do
+ for k in 1 2
+ do
+ # Encrypt bipbop_<res>.mp4 with the keys specified in this file,
+ # and output to |bipbop_<res>-cenc-{video,audio}.mp4|
+ MP4Box -crypt bipbop-cenc-audio-key${k}.xml -rem 1 -out work.tmp/bipbop_${r}-cenc-audio-key${k}.mp4 bipbop_${r}.mp4
+ MP4Box -crypt bipbop-cenc-video-key${k}.xml -rem 2 -out work.tmp/bipbop_${r}-cenc-video-key${k}.mp4 bipbop_${r}.mp4
+
+ # Fragment |bipbop_<res>-cenc-*.mp4| into 500ms segments:
+ MP4Box -dash 500 -rap -segment-name work.tmp/bipbop_${r}-cenc-audio-key${k}- -subsegs-per-sidx 5 work.tmp/bipbop_${r}-cenc-audio-key${k}.mp4
+ MP4Box -dash 500 -rap -segment-name work.tmp/bipbop_${r}-cenc-video-key${k}- -subsegs-per-sidx 5 work.tmp/bipbop_${r}-cenc-video-key${k}.mp4
+
+ # The above command will generate a set of fragments |bipbop_<res>-cenc-{video,audio}-*.m4s
+ # and |bipbop_<res>-cenc-{video,audio}-init.mp4| containing just the init segment.
+
+ # Remove unneeded mpd files.
+ rm bipbop_${r}-cenc-{audio,video}-key${k}_dash.mpd
+ done
+done
+
+# Only keep the first 4 audio & 2 video segments:
+cp work.tmp/*-init[.]mp4 ./
+cp work.tmp/*audio*-[1234][.]m4s ./
+cp work.tmp/*video*-[12][.]m4s ./
+
+rm -Rf work.tmp
diff --git a/dom/media/test/bipbop-clearkey-keyrotation-clear-lead-audio.mp4 b/dom/media/test/bipbop-clearkey-keyrotation-clear-lead-audio.mp4
new file mode 100644
index 0000000000..5e5e30c255
--- /dev/null
+++ b/dom/media/test/bipbop-clearkey-keyrotation-clear-lead-audio.mp4
Binary files differ
diff --git a/dom/media/test/bipbop-clearkey-keyrotation-clear-lead-audio.mp4^headers^ b/dom/media/test/bipbop-clearkey-keyrotation-clear-lead-audio.mp4^headers^
new file mode 100644
index 0000000000..12a01c4a22
--- /dev/null
+++ b/dom/media/test/bipbop-clearkey-keyrotation-clear-lead-audio.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop-clearkey-keyrotation-clear-lead-video.mp4 b/dom/media/test/bipbop-clearkey-keyrotation-clear-lead-video.mp4
new file mode 100644
index 0000000000..447c657475
--- /dev/null
+++ b/dom/media/test/bipbop-clearkey-keyrotation-clear-lead-video.mp4
Binary files differ
diff --git a/dom/media/test/bipbop-clearkey-keyrotation-clear-lead-video.mp4^headers^ b/dom/media/test/bipbop-clearkey-keyrotation-clear-lead-video.mp4^headers^
new file mode 100644
index 0000000000..12a01c4a22
--- /dev/null
+++ b/dom/media/test/bipbop-clearkey-keyrotation-clear-lead-video.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop-frag-cenc.xml b/dom/media/test/bipbop-frag-cenc.xml
new file mode 100644
index 0000000000..6f6a4d90a9
--- /dev/null
+++ b/dom/media/test/bipbop-frag-cenc.xml
@@ -0,0 +1,57 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ This XML file describes the encryption applied to |bipbop-cenc*|. To
+ generate the bipbop-cenc files, run the following commands:
+
+ Encrypt bipbop-no-edts.mp4 with the keys specified in this file,
+ and output to |bipbop-cenc-{video,audio}.mp4|
+ MP4Box -crypt bipbop-frag-cenc.xml -rem 2 -out bipbop-cenc-video.mp4 bipbop-no-edts.mp4
+ MP4Box -crypt bipbop-frag-cenc.xml -rem 1 -out bipbop-cenc-audio.mp4 bipbop-no-edts.mp4
+
+ Fragment |bipbop-cenc-*.mp4| into 500ms segments:
+ MP4Box -dash 500 -rap -segment-name bipbop-cenc-video -subsegs-per-sidx 5 bipbop-cenc-video.mp4
+ MP4Box -dash 500 -rap -segment-name bipbop-cenc-audio -subsegs-per-sidx 5 bipbop-cenc-audio.mp4
+
+ The above command will generate a set of fragments in |bipbop-cenc-{video,audio}*.m4s
+ and |bipbop-cenc-{video,audio}init.mp4| containing just the init segment.
+
+ To cut down the duration, we throw out all but the first 3 audio & 2 video segments:
+ rm bipbop-cenc-audio{[^123],[123][^.]}.m4s
+ rm bipbop-cenc-video{[^12],[12][^.]}.m4s
+
+ MP4Box will also have generated some *.mpd files we don't need:
+ rm bipbop-cenc-*.mpd
+
+ Delete intermediate encrypted files:
+ rm bipbop-cenc-{audio,video}.mp4
+-->
+
+<GPACDRM type="CENC AES-CTR">
+
+ <DRMInfo type="pssh" version="1">
+ <!--
+ SystemID specified in
+ https://dvcs.w3.org/hg/html-media/raw-file/tip/encrypted-media/cenc-format.html
+ -->
+ <BS ID128="1077efecc0b24d02ace33c1e52e2fb4b" />
+ <!-- Number of KeyIDs = 2 -->
+ <BS bits="32" value="2" />
+ <!-- KeyID -->
+ <BS ID128="0x7e571d037e571d037e571d037e571d03" />
+ <BS ID128="0x7e571d047e571d047e571d047e571d04" />
+ </DRMInfo>
+
+ <CrypTrack trackID="1" isEncrypted="1" IV_size="16" saiSavedBox="senc"
+ first_IV="0x00000000000000000000000000000000">
+ <key KID="0x7e571d037e571d037e571d037e571d03"
+ value="0x7e5733337e5733337e5733337e573333" />
+ </CrypTrack>
+
+ <CrypTrack trackID="2" isEncrypted="1" IV_size="16" saiSavedBox="senc"
+ first_IV="0x00000000000000000000000000000000">
+ <key KID="0x7e571d047e571d047e571d047e571d04"
+ value="0x7e5744447e5744447e5744447e574444" />
+ </CrypTrack>
+
+</GPACDRM>
diff --git a/dom/media/test/bipbop-lateaudio.mp4 b/dom/media/test/bipbop-lateaudio.mp4
new file mode 100644
index 0000000000..5b4cc57095
--- /dev/null
+++ b/dom/media/test/bipbop-lateaudio.mp4
Binary files differ
diff --git a/dom/media/test/bipbop-lateaudio.mp4^headers^ b/dom/media/test/bipbop-lateaudio.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop-lateaudio.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop-no-edts.mp4 b/dom/media/test/bipbop-no-edts.mp4
new file mode 100644
index 0000000000..63435887df
--- /dev/null
+++ b/dom/media/test/bipbop-no-edts.mp4
Binary files differ
diff --git a/dom/media/test/bipbop.mp4 b/dom/media/test/bipbop.mp4
new file mode 100644
index 0000000000..017d658f31
--- /dev/null
+++ b/dom/media/test/bipbop.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-1.m4s b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-1.m4s
new file mode 100644
index 0000000000..e2bd754c7e
--- /dev/null
+++ b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-1.m4s^headers^ b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-2.m4s b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-2.m4s
new file mode 100644
index 0000000000..347835feee
--- /dev/null
+++ b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-2.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-2.m4s^headers^ b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-3.m4s b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-3.m4s
new file mode 100644
index 0000000000..64b0da69a0
--- /dev/null
+++ b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-3.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-3.m4s^headers^ b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-3.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-3.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-4.m4s b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-4.m4s
new file mode 100644
index 0000000000..864f4248af
--- /dev/null
+++ b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-4.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-4.m4s^headers^ b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-4.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-4.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-init.mp4 b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-init.mp4
new file mode 100644
index 0000000000..40c3a7bb98
--- /dev/null
+++ b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-init.mp4^headers^ b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key1-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-1.m4s b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-1.m4s
new file mode 100644
index 0000000000..a8896e069a
--- /dev/null
+++ b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-1.m4s^headers^ b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-2.m4s b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-2.m4s
new file mode 100644
index 0000000000..0f0a35ce79
--- /dev/null
+++ b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-2.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-2.m4s^headers^ b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-3.m4s b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-3.m4s
new file mode 100644
index 0000000000..fece52ff42
--- /dev/null
+++ b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-3.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-3.m4s^headers^ b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-3.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-3.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-4.m4s b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-4.m4s
new file mode 100644
index 0000000000..70e61e3d5f
--- /dev/null
+++ b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-4.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-4.m4s^headers^ b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-4.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-4.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-init.mp4 b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-init.mp4
new file mode 100644
index 0000000000..986e5fb186
--- /dev/null
+++ b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-init.mp4^headers^ b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_225w_175kbps-cenc-audio-key2-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_225w_175kbps-cenc-video-key1-1.m4s b/dom/media/test/bipbop_225w_175kbps-cenc-video-key1-1.m4s
new file mode 100644
index 0000000000..547950e516
--- /dev/null
+++ b/dom/media/test/bipbop_225w_175kbps-cenc-video-key1-1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_225w_175kbps-cenc-video-key1-1.m4s^headers^ b/dom/media/test/bipbop_225w_175kbps-cenc-video-key1-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_225w_175kbps-cenc-video-key1-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_225w_175kbps-cenc-video-key1-init.mp4 b/dom/media/test/bipbop_225w_175kbps-cenc-video-key1-init.mp4
new file mode 100644
index 0000000000..3214f131d4
--- /dev/null
+++ b/dom/media/test/bipbop_225w_175kbps-cenc-video-key1-init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_225w_175kbps-cenc-video-key1-init.mp4^headers^ b/dom/media/test/bipbop_225w_175kbps-cenc-video-key1-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_225w_175kbps-cenc-video-key1-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_225w_175kbps-cenc-video-key2-1.m4s b/dom/media/test/bipbop_225w_175kbps-cenc-video-key2-1.m4s
new file mode 100644
index 0000000000..08713078d9
--- /dev/null
+++ b/dom/media/test/bipbop_225w_175kbps-cenc-video-key2-1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_225w_175kbps-cenc-video-key2-1.m4s^headers^ b/dom/media/test/bipbop_225w_175kbps-cenc-video-key2-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_225w_175kbps-cenc-video-key2-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_225w_175kbps-cenc-video-key2-init.mp4 b/dom/media/test/bipbop_225w_175kbps-cenc-video-key2-init.mp4
new file mode 100644
index 0000000000..0b13fed5f0
--- /dev/null
+++ b/dom/media/test/bipbop_225w_175kbps-cenc-video-key2-init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_225w_175kbps-cenc-video-key2-init.mp4^headers^ b/dom/media/test/bipbop_225w_175kbps-cenc-video-key2-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_225w_175kbps-cenc-video-key2-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_225w_175kbps.mp4 b/dom/media/test/bipbop_225w_175kbps.mp4
new file mode 100644
index 0000000000..abe37b9f9d
--- /dev/null
+++ b/dom/media/test/bipbop_225w_175kbps.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_225w_175kbps.mp4^headers^ b/dom/media/test/bipbop_225w_175kbps.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_225w_175kbps.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300_215kbps-cenc-audio-key1-1.m4s b/dom/media/test/bipbop_300_215kbps-cenc-audio-key1-1.m4s
new file mode 100644
index 0000000000..e2bd754c7e
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps-cenc-audio-key1-1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_300_215kbps-cenc-audio-key1-1.m4s^headers^ b/dom/media/test/bipbop_300_215kbps-cenc-audio-key1-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps-cenc-audio-key1-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300_215kbps-cenc-audio-key1-2.m4s b/dom/media/test/bipbop_300_215kbps-cenc-audio-key1-2.m4s
new file mode 100644
index 0000000000..347835feee
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps-cenc-audio-key1-2.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_300_215kbps-cenc-audio-key1-2.m4s^headers^ b/dom/media/test/bipbop_300_215kbps-cenc-audio-key1-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps-cenc-audio-key1-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300_215kbps-cenc-audio-key1-3.m4s b/dom/media/test/bipbop_300_215kbps-cenc-audio-key1-3.m4s
new file mode 100644
index 0000000000..64b0da69a0
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps-cenc-audio-key1-3.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_300_215kbps-cenc-audio-key1-3.m4s^headers^ b/dom/media/test/bipbop_300_215kbps-cenc-audio-key1-3.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps-cenc-audio-key1-3.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300_215kbps-cenc-audio-key1-4.m4s b/dom/media/test/bipbop_300_215kbps-cenc-audio-key1-4.m4s
new file mode 100644
index 0000000000..864f4248af
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps-cenc-audio-key1-4.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_300_215kbps-cenc-audio-key1-4.m4s^headers^ b/dom/media/test/bipbop_300_215kbps-cenc-audio-key1-4.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps-cenc-audio-key1-4.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300_215kbps-cenc-audio-key1-init.mp4 b/dom/media/test/bipbop_300_215kbps-cenc-audio-key1-init.mp4
new file mode 100644
index 0000000000..21f3863274
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps-cenc-audio-key1-init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_300_215kbps-cenc-audio-key1-init.mp4^headers^ b/dom/media/test/bipbop_300_215kbps-cenc-audio-key1-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps-cenc-audio-key1-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300_215kbps-cenc-audio-key2-1.m4s b/dom/media/test/bipbop_300_215kbps-cenc-audio-key2-1.m4s
new file mode 100644
index 0000000000..a8896e069a
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps-cenc-audio-key2-1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_300_215kbps-cenc-audio-key2-1.m4s^headers^ b/dom/media/test/bipbop_300_215kbps-cenc-audio-key2-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps-cenc-audio-key2-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300_215kbps-cenc-audio-key2-2.m4s b/dom/media/test/bipbop_300_215kbps-cenc-audio-key2-2.m4s
new file mode 100644
index 0000000000..0f0a35ce79
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps-cenc-audio-key2-2.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_300_215kbps-cenc-audio-key2-2.m4s^headers^ b/dom/media/test/bipbop_300_215kbps-cenc-audio-key2-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps-cenc-audio-key2-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300_215kbps-cenc-audio-key2-3.m4s b/dom/media/test/bipbop_300_215kbps-cenc-audio-key2-3.m4s
new file mode 100644
index 0000000000..fece52ff42
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps-cenc-audio-key2-3.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_300_215kbps-cenc-audio-key2-3.m4s^headers^ b/dom/media/test/bipbop_300_215kbps-cenc-audio-key2-3.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps-cenc-audio-key2-3.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300_215kbps-cenc-audio-key2-4.m4s b/dom/media/test/bipbop_300_215kbps-cenc-audio-key2-4.m4s
new file mode 100644
index 0000000000..70e61e3d5f
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps-cenc-audio-key2-4.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_300_215kbps-cenc-audio-key2-4.m4s^headers^ b/dom/media/test/bipbop_300_215kbps-cenc-audio-key2-4.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps-cenc-audio-key2-4.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300_215kbps-cenc-audio-key2-init.mp4 b/dom/media/test/bipbop_300_215kbps-cenc-audio-key2-init.mp4
new file mode 100644
index 0000000000..bc741cdf86
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps-cenc-audio-key2-init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_300_215kbps-cenc-audio-key2-init.mp4^headers^ b/dom/media/test/bipbop_300_215kbps-cenc-audio-key2-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps-cenc-audio-key2-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300_215kbps-cenc-video-key1-1.m4s b/dom/media/test/bipbop_300_215kbps-cenc-video-key1-1.m4s
new file mode 100644
index 0000000000..9c6818d06f
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps-cenc-video-key1-1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_300_215kbps-cenc-video-key1-1.m4s^headers^ b/dom/media/test/bipbop_300_215kbps-cenc-video-key1-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps-cenc-video-key1-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300_215kbps-cenc-video-key1-2.m4s b/dom/media/test/bipbop_300_215kbps-cenc-video-key1-2.m4s
new file mode 100644
index 0000000000..f327aaa573
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps-cenc-video-key1-2.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_300_215kbps-cenc-video-key1-2.m4s^headers^ b/dom/media/test/bipbop_300_215kbps-cenc-video-key1-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps-cenc-video-key1-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300_215kbps-cenc-video-key1-init.mp4 b/dom/media/test/bipbop_300_215kbps-cenc-video-key1-init.mp4
new file mode 100644
index 0000000000..543f18c24b
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps-cenc-video-key1-init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_300_215kbps-cenc-video-key1-init.mp4^headers^ b/dom/media/test/bipbop_300_215kbps-cenc-video-key1-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps-cenc-video-key1-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300_215kbps-cenc-video-key2-1.m4s b/dom/media/test/bipbop_300_215kbps-cenc-video-key2-1.m4s
new file mode 100644
index 0000000000..f850ceaf0a
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps-cenc-video-key2-1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_300_215kbps-cenc-video-key2-1.m4s^headers^ b/dom/media/test/bipbop_300_215kbps-cenc-video-key2-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps-cenc-video-key2-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300_215kbps-cenc-video-key2-2.m4s b/dom/media/test/bipbop_300_215kbps-cenc-video-key2-2.m4s
new file mode 100644
index 0000000000..a28a106daf
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps-cenc-video-key2-2.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_300_215kbps-cenc-video-key2-2.m4s^headers^ b/dom/media/test/bipbop_300_215kbps-cenc-video-key2-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps-cenc-video-key2-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300_215kbps-cenc-video-key2-init.mp4 b/dom/media/test/bipbop_300_215kbps-cenc-video-key2-init.mp4
new file mode 100644
index 0000000000..a05a879970
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps-cenc-video-key2-init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_300_215kbps-cenc-video-key2-init.mp4^headers^ b/dom/media/test/bipbop_300_215kbps-cenc-video-key2-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps-cenc-video-key2-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300_215kbps.mp4 b/dom/media/test/bipbop_300_215kbps.mp4
new file mode 100644
index 0000000000..084d477430
--- /dev/null
+++ b/dom/media/test/bipbop_300_215kbps.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-1.m4s b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-1.m4s
new file mode 100644
index 0000000000..e2bd754c7e
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-1.m4s^headers^ b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-2.m4s b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-2.m4s
new file mode 100644
index 0000000000..347835feee
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-2.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-2.m4s^headers^ b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-3.m4s b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-3.m4s
new file mode 100644
index 0000000000..64b0da69a0
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-3.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-3.m4s^headers^ b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-3.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-3.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-4.m4s b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-4.m4s
new file mode 100644
index 0000000000..864f4248af
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-4.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-4.m4s^headers^ b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-4.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-4.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-init.mp4 b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-init.mp4
new file mode 100644
index 0000000000..40c3a7bb98
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-init.mp4^headers^ b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key1-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-1.m4s b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-1.m4s
new file mode 100644
index 0000000000..a8896e069a
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-1.m4s^headers^ b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-2.m4s b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-2.m4s
new file mode 100644
index 0000000000..0f0a35ce79
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-2.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-2.m4s^headers^ b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-3.m4s b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-3.m4s
new file mode 100644
index 0000000000..fece52ff42
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-3.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-3.m4s^headers^ b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-3.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-3.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-4.m4s b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-4.m4s
new file mode 100644
index 0000000000..70e61e3d5f
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-4.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-4.m4s^headers^ b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-4.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-4.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-init.mp4 b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-init.mp4
new file mode 100644
index 0000000000..986e5fb186
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-init.mp4^headers^ b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps-cenc-audio-key2-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300wp_227kbps-cenc-video-key1-1.m4s b/dom/media/test/bipbop_300wp_227kbps-cenc-video-key1-1.m4s
new file mode 100644
index 0000000000..9c6818d06f
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps-cenc-video-key1-1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_300wp_227kbps-cenc-video-key1-1.m4s^headers^ b/dom/media/test/bipbop_300wp_227kbps-cenc-video-key1-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps-cenc-video-key1-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300wp_227kbps-cenc-video-key1-2.m4s b/dom/media/test/bipbop_300wp_227kbps-cenc-video-key1-2.m4s
new file mode 100644
index 0000000000..ce2e64eb33
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps-cenc-video-key1-2.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_300wp_227kbps-cenc-video-key1-2.m4s^headers^ b/dom/media/test/bipbop_300wp_227kbps-cenc-video-key1-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps-cenc-video-key1-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300wp_227kbps-cenc-video-key1-init.mp4 b/dom/media/test/bipbop_300wp_227kbps-cenc-video-key1-init.mp4
new file mode 100644
index 0000000000..8592a5b0a3
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps-cenc-video-key1-init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_300wp_227kbps-cenc-video-key1-init.mp4^headers^ b/dom/media/test/bipbop_300wp_227kbps-cenc-video-key1-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps-cenc-video-key1-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300wp_227kbps-cenc-video-key2-1.m4s b/dom/media/test/bipbop_300wp_227kbps-cenc-video-key2-1.m4s
new file mode 100644
index 0000000000..f850ceaf0a
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps-cenc-video-key2-1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_300wp_227kbps-cenc-video-key2-1.m4s^headers^ b/dom/media/test/bipbop_300wp_227kbps-cenc-video-key2-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps-cenc-video-key2-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300wp_227kbps-cenc-video-key2-2.m4s b/dom/media/test/bipbop_300wp_227kbps-cenc-video-key2-2.m4s
new file mode 100644
index 0000000000..d07ce9753e
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps-cenc-video-key2-2.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_300wp_227kbps-cenc-video-key2-2.m4s^headers^ b/dom/media/test/bipbop_300wp_227kbps-cenc-video-key2-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps-cenc-video-key2-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300wp_227kbps-cenc-video-key2-init.mp4 b/dom/media/test/bipbop_300wp_227kbps-cenc-video-key2-init.mp4
new file mode 100644
index 0000000000..9d2fa23dd4
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps-cenc-video-key2-init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_300wp_227kbps-cenc-video-key2-init.mp4^headers^ b/dom/media/test/bipbop_300wp_227kbps-cenc-video-key2-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps-cenc-video-key2-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_300wp_227kbps.mp4 b/dom/media/test/bipbop_300wp_227kbps.mp4
new file mode 100644
index 0000000000..1499355313
--- /dev/null
+++ b/dom/media/test/bipbop_300wp_227kbps.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-1.m4s b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-1.m4s
new file mode 100644
index 0000000000..e2bd754c7e
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-1.m4s^headers^ b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-2.m4s b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-2.m4s
new file mode 100644
index 0000000000..347835feee
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-2.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-2.m4s^headers^ b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-3.m4s b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-3.m4s
new file mode 100644
index 0000000000..64b0da69a0
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-3.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-3.m4s^headers^ b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-3.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-3.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-4.m4s b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-4.m4s
new file mode 100644
index 0000000000..864f4248af
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-4.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-4.m4s^headers^ b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-4.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-4.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-init.mp4 b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-init.mp4
new file mode 100644
index 0000000000..40c3a7bb98
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-init.mp4^headers^ b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key1-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-1.m4s b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-1.m4s
new file mode 100644
index 0000000000..a8896e069a
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-1.m4s^headers^ b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-2.m4s b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-2.m4s
new file mode 100644
index 0000000000..0f0a35ce79
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-2.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-2.m4s^headers^ b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-3.m4s b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-3.m4s
new file mode 100644
index 0000000000..fece52ff42
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-3.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-3.m4s^headers^ b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-3.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-3.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-4.m4s b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-4.m4s
new file mode 100644
index 0000000000..70e61e3d5f
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-4.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-4.m4s^headers^ b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-4.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-4.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-init.mp4 b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-init.mp4
new file mode 100644
index 0000000000..986e5fb186
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-init.mp4^headers^ b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-cenc-audio-key2-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_360w_253kbps-cenc-video-key1-1.m4s b/dom/media/test/bipbop_360w_253kbps-cenc-video-key1-1.m4s
new file mode 100644
index 0000000000..a571d47cfb
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-cenc-video-key1-1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_360w_253kbps-cenc-video-key1-1.m4s^headers^ b/dom/media/test/bipbop_360w_253kbps-cenc-video-key1-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-cenc-video-key1-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_360w_253kbps-cenc-video-key1-init.mp4 b/dom/media/test/bipbop_360w_253kbps-cenc-video-key1-init.mp4
new file mode 100644
index 0000000000..42dbfec1ed
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-cenc-video-key1-init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_360w_253kbps-cenc-video-key1-init.mp4^headers^ b/dom/media/test/bipbop_360w_253kbps-cenc-video-key1-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-cenc-video-key1-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_360w_253kbps-cenc-video-key2-1.m4s b/dom/media/test/bipbop_360w_253kbps-cenc-video-key2-1.m4s
new file mode 100644
index 0000000000..9e4224cac8
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-cenc-video-key2-1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_360w_253kbps-cenc-video-key2-1.m4s^headers^ b/dom/media/test/bipbop_360w_253kbps-cenc-video-key2-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-cenc-video-key2-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_360w_253kbps-cenc-video-key2-init.mp4 b/dom/media/test/bipbop_360w_253kbps-cenc-video-key2-init.mp4
new file mode 100644
index 0000000000..21763ecbdd
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-cenc-video-key2-init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_360w_253kbps-cenc-video-key2-init.mp4^headers^ b/dom/media/test/bipbop_360w_253kbps-cenc-video-key2-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-cenc-video-key2-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_360w_253kbps-clearkey-audio.webm b/dom/media/test/bipbop_360w_253kbps-clearkey-audio.webm
new file mode 100644
index 0000000000..4be8f340c3
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-clearkey-audio.webm
Binary files differ
diff --git a/dom/media/test/bipbop_360w_253kbps-clearkey-audio.webm^headers^ b/dom/media/test/bipbop_360w_253kbps-clearkey-audio.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-clearkey-audio.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_360w_253kbps-clearkey-video-vp8.webm b/dom/media/test/bipbop_360w_253kbps-clearkey-video-vp8.webm
new file mode 100644
index 0000000000..56cf4c483c
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-clearkey-video-vp8.webm
Binary files differ
diff --git a/dom/media/test/bipbop_360w_253kbps-clearkey-video-vp8.webm^headers^ b/dom/media/test/bipbop_360w_253kbps-clearkey-video-vp8.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-clearkey-video-vp8.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_360w_253kbps-clearkey-video-vp9.webm b/dom/media/test/bipbop_360w_253kbps-clearkey-video-vp9.webm
new file mode 100644
index 0000000000..9f411d0e34
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-clearkey-video-vp9.webm
Binary files differ
diff --git a/dom/media/test/bipbop_360w_253kbps-clearkey-video-vp9.webm^headers^ b/dom/media/test/bipbop_360w_253kbps-clearkey-video-vp9.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps-clearkey-video-vp9.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_360w_253kbps.mp4 b/dom/media/test/bipbop_360w_253kbps.mp4
new file mode 100644
index 0000000000..6c796f4e1f
--- /dev/null
+++ b/dom/media/test/bipbop_360w_253kbps.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_480_624kbps-cenc-audio-key1-1.m4s b/dom/media/test/bipbop_480_624kbps-cenc-audio-key1-1.m4s
new file mode 100644
index 0000000000..e2bd754c7e
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps-cenc-audio-key1-1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480_624kbps-cenc-audio-key1-1.m4s^headers^ b/dom/media/test/bipbop_480_624kbps-cenc-audio-key1-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps-cenc-audio-key1-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480_624kbps-cenc-audio-key1-2.m4s b/dom/media/test/bipbop_480_624kbps-cenc-audio-key1-2.m4s
new file mode 100644
index 0000000000..347835feee
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps-cenc-audio-key1-2.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480_624kbps-cenc-audio-key1-2.m4s^headers^ b/dom/media/test/bipbop_480_624kbps-cenc-audio-key1-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps-cenc-audio-key1-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480_624kbps-cenc-audio-key1-3.m4s b/dom/media/test/bipbop_480_624kbps-cenc-audio-key1-3.m4s
new file mode 100644
index 0000000000..64b0da69a0
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps-cenc-audio-key1-3.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480_624kbps-cenc-audio-key1-3.m4s^headers^ b/dom/media/test/bipbop_480_624kbps-cenc-audio-key1-3.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps-cenc-audio-key1-3.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480_624kbps-cenc-audio-key1-4.m4s b/dom/media/test/bipbop_480_624kbps-cenc-audio-key1-4.m4s
new file mode 100644
index 0000000000..864f4248af
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps-cenc-audio-key1-4.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480_624kbps-cenc-audio-key1-4.m4s^headers^ b/dom/media/test/bipbop_480_624kbps-cenc-audio-key1-4.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps-cenc-audio-key1-4.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480_624kbps-cenc-audio-key1-init.mp4 b/dom/media/test/bipbop_480_624kbps-cenc-audio-key1-init.mp4
new file mode 100644
index 0000000000..e626fa4564
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps-cenc-audio-key1-init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_480_624kbps-cenc-audio-key1-init.mp4^headers^ b/dom/media/test/bipbop_480_624kbps-cenc-audio-key1-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps-cenc-audio-key1-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480_624kbps-cenc-audio-key2-1.m4s b/dom/media/test/bipbop_480_624kbps-cenc-audio-key2-1.m4s
new file mode 100644
index 0000000000..a8896e069a
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps-cenc-audio-key2-1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480_624kbps-cenc-audio-key2-1.m4s^headers^ b/dom/media/test/bipbop_480_624kbps-cenc-audio-key2-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps-cenc-audio-key2-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480_624kbps-cenc-audio-key2-2.m4s b/dom/media/test/bipbop_480_624kbps-cenc-audio-key2-2.m4s
new file mode 100644
index 0000000000..0f0a35ce79
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps-cenc-audio-key2-2.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480_624kbps-cenc-audio-key2-2.m4s^headers^ b/dom/media/test/bipbop_480_624kbps-cenc-audio-key2-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps-cenc-audio-key2-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480_624kbps-cenc-audio-key2-3.m4s b/dom/media/test/bipbop_480_624kbps-cenc-audio-key2-3.m4s
new file mode 100644
index 0000000000..fece52ff42
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps-cenc-audio-key2-3.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480_624kbps-cenc-audio-key2-3.m4s^headers^ b/dom/media/test/bipbop_480_624kbps-cenc-audio-key2-3.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps-cenc-audio-key2-3.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480_624kbps-cenc-audio-key2-4.m4s b/dom/media/test/bipbop_480_624kbps-cenc-audio-key2-4.m4s
new file mode 100644
index 0000000000..70e61e3d5f
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps-cenc-audio-key2-4.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480_624kbps-cenc-audio-key2-4.m4s^headers^ b/dom/media/test/bipbop_480_624kbps-cenc-audio-key2-4.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps-cenc-audio-key2-4.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480_624kbps-cenc-audio-key2-init.mp4 b/dom/media/test/bipbop_480_624kbps-cenc-audio-key2-init.mp4
new file mode 100644
index 0000000000..d7cbb2b6b0
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps-cenc-audio-key2-init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_480_624kbps-cenc-audio-key2-init.mp4^headers^ b/dom/media/test/bipbop_480_624kbps-cenc-audio-key2-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps-cenc-audio-key2-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480_624kbps-cenc-video-key1-1.m4s b/dom/media/test/bipbop_480_624kbps-cenc-video-key1-1.m4s
new file mode 100644
index 0000000000..805f4bbf3f
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps-cenc-video-key1-1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480_624kbps-cenc-video-key1-1.m4s^headers^ b/dom/media/test/bipbop_480_624kbps-cenc-video-key1-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps-cenc-video-key1-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480_624kbps-cenc-video-key1-2.m4s b/dom/media/test/bipbop_480_624kbps-cenc-video-key1-2.m4s
new file mode 100644
index 0000000000..5bf9994733
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps-cenc-video-key1-2.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480_624kbps-cenc-video-key1-2.m4s^headers^ b/dom/media/test/bipbop_480_624kbps-cenc-video-key1-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps-cenc-video-key1-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480_624kbps-cenc-video-key1-init.mp4 b/dom/media/test/bipbop_480_624kbps-cenc-video-key1-init.mp4
new file mode 100644
index 0000000000..77c7daba5a
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps-cenc-video-key1-init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_480_624kbps-cenc-video-key1-init.mp4^headers^ b/dom/media/test/bipbop_480_624kbps-cenc-video-key1-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps-cenc-video-key1-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480_624kbps-cenc-video-key2-1.m4s b/dom/media/test/bipbop_480_624kbps-cenc-video-key2-1.m4s
new file mode 100644
index 0000000000..c5127beec9
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps-cenc-video-key2-1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480_624kbps-cenc-video-key2-1.m4s^headers^ b/dom/media/test/bipbop_480_624kbps-cenc-video-key2-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps-cenc-video-key2-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480_624kbps-cenc-video-key2-2.m4s b/dom/media/test/bipbop_480_624kbps-cenc-video-key2-2.m4s
new file mode 100644
index 0000000000..b0ff51f74a
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps-cenc-video-key2-2.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480_624kbps-cenc-video-key2-2.m4s^headers^ b/dom/media/test/bipbop_480_624kbps-cenc-video-key2-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps-cenc-video-key2-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480_624kbps-cenc-video-key2-init.mp4 b/dom/media/test/bipbop_480_624kbps-cenc-video-key2-init.mp4
new file mode 100644
index 0000000000..cfa099c043
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps-cenc-video-key2-init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_480_624kbps-cenc-video-key2-init.mp4^headers^ b/dom/media/test/bipbop_480_624kbps-cenc-video-key2-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps-cenc-video-key2-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480_624kbps.mp4 b/dom/media/test/bipbop_480_624kbps.mp4
new file mode 100644
index 0000000000..27928b85f4
--- /dev/null
+++ b/dom/media/test/bipbop_480_624kbps.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_480_959kbps-cenc-audio-key1-1.m4s b/dom/media/test/bipbop_480_959kbps-cenc-audio-key1-1.m4s
new file mode 100644
index 0000000000..e2bd754c7e
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps-cenc-audio-key1-1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480_959kbps-cenc-audio-key1-1.m4s^headers^ b/dom/media/test/bipbop_480_959kbps-cenc-audio-key1-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps-cenc-audio-key1-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480_959kbps-cenc-audio-key1-2.m4s b/dom/media/test/bipbop_480_959kbps-cenc-audio-key1-2.m4s
new file mode 100644
index 0000000000..347835feee
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps-cenc-audio-key1-2.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480_959kbps-cenc-audio-key1-2.m4s^headers^ b/dom/media/test/bipbop_480_959kbps-cenc-audio-key1-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps-cenc-audio-key1-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480_959kbps-cenc-audio-key1-3.m4s b/dom/media/test/bipbop_480_959kbps-cenc-audio-key1-3.m4s
new file mode 100644
index 0000000000..64b0da69a0
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps-cenc-audio-key1-3.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480_959kbps-cenc-audio-key1-3.m4s^headers^ b/dom/media/test/bipbop_480_959kbps-cenc-audio-key1-3.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps-cenc-audio-key1-3.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480_959kbps-cenc-audio-key1-4.m4s b/dom/media/test/bipbop_480_959kbps-cenc-audio-key1-4.m4s
new file mode 100644
index 0000000000..864f4248af
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps-cenc-audio-key1-4.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480_959kbps-cenc-audio-key1-4.m4s^headers^ b/dom/media/test/bipbop_480_959kbps-cenc-audio-key1-4.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps-cenc-audio-key1-4.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480_959kbps-cenc-audio-key1-init.mp4 b/dom/media/test/bipbop_480_959kbps-cenc-audio-key1-init.mp4
new file mode 100644
index 0000000000..c9106aad99
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps-cenc-audio-key1-init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_480_959kbps-cenc-audio-key1-init.mp4^headers^ b/dom/media/test/bipbop_480_959kbps-cenc-audio-key1-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps-cenc-audio-key1-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480_959kbps-cenc-audio-key2-1.m4s b/dom/media/test/bipbop_480_959kbps-cenc-audio-key2-1.m4s
new file mode 100644
index 0000000000..a8896e069a
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps-cenc-audio-key2-1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480_959kbps-cenc-audio-key2-1.m4s^headers^ b/dom/media/test/bipbop_480_959kbps-cenc-audio-key2-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps-cenc-audio-key2-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480_959kbps-cenc-audio-key2-2.m4s b/dom/media/test/bipbop_480_959kbps-cenc-audio-key2-2.m4s
new file mode 100644
index 0000000000..0f0a35ce79
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps-cenc-audio-key2-2.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480_959kbps-cenc-audio-key2-2.m4s^headers^ b/dom/media/test/bipbop_480_959kbps-cenc-audio-key2-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps-cenc-audio-key2-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480_959kbps-cenc-audio-key2-3.m4s b/dom/media/test/bipbop_480_959kbps-cenc-audio-key2-3.m4s
new file mode 100644
index 0000000000..fece52ff42
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps-cenc-audio-key2-3.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480_959kbps-cenc-audio-key2-3.m4s^headers^ b/dom/media/test/bipbop_480_959kbps-cenc-audio-key2-3.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps-cenc-audio-key2-3.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480_959kbps-cenc-audio-key2-4.m4s b/dom/media/test/bipbop_480_959kbps-cenc-audio-key2-4.m4s
new file mode 100644
index 0000000000..70e61e3d5f
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps-cenc-audio-key2-4.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480_959kbps-cenc-audio-key2-4.m4s^headers^ b/dom/media/test/bipbop_480_959kbps-cenc-audio-key2-4.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps-cenc-audio-key2-4.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480_959kbps-cenc-audio-key2-init.mp4 b/dom/media/test/bipbop_480_959kbps-cenc-audio-key2-init.mp4
new file mode 100644
index 0000000000..888b20ab63
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps-cenc-audio-key2-init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_480_959kbps-cenc-audio-key2-init.mp4^headers^ b/dom/media/test/bipbop_480_959kbps-cenc-audio-key2-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps-cenc-audio-key2-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480_959kbps-cenc-video-key1-1.m4s b/dom/media/test/bipbop_480_959kbps-cenc-video-key1-1.m4s
new file mode 100644
index 0000000000..796ad13670
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps-cenc-video-key1-1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480_959kbps-cenc-video-key1-1.m4s^headers^ b/dom/media/test/bipbop_480_959kbps-cenc-video-key1-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps-cenc-video-key1-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480_959kbps-cenc-video-key1-2.m4s b/dom/media/test/bipbop_480_959kbps-cenc-video-key1-2.m4s
new file mode 100644
index 0000000000..d02be53198
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps-cenc-video-key1-2.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480_959kbps-cenc-video-key1-2.m4s^headers^ b/dom/media/test/bipbop_480_959kbps-cenc-video-key1-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps-cenc-video-key1-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480_959kbps-cenc-video-key1-init.mp4 b/dom/media/test/bipbop_480_959kbps-cenc-video-key1-init.mp4
new file mode 100644
index 0000000000..6e0c60f986
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps-cenc-video-key1-init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_480_959kbps-cenc-video-key1-init.mp4^headers^ b/dom/media/test/bipbop_480_959kbps-cenc-video-key1-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps-cenc-video-key1-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480_959kbps-cenc-video-key2-1.m4s b/dom/media/test/bipbop_480_959kbps-cenc-video-key2-1.m4s
new file mode 100644
index 0000000000..06778e6f2b
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps-cenc-video-key2-1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480_959kbps-cenc-video-key2-1.m4s^headers^ b/dom/media/test/bipbop_480_959kbps-cenc-video-key2-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps-cenc-video-key2-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480_959kbps-cenc-video-key2-2.m4s b/dom/media/test/bipbop_480_959kbps-cenc-video-key2-2.m4s
new file mode 100644
index 0000000000..4c1c603e8d
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps-cenc-video-key2-2.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480_959kbps-cenc-video-key2-2.m4s^headers^ b/dom/media/test/bipbop_480_959kbps-cenc-video-key2-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps-cenc-video-key2-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480_959kbps-cenc-video-key2-init.mp4 b/dom/media/test/bipbop_480_959kbps-cenc-video-key2-init.mp4
new file mode 100644
index 0000000000..f4a98eca97
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps-cenc-video-key2-init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_480_959kbps-cenc-video-key2-init.mp4^headers^ b/dom/media/test/bipbop_480_959kbps-cenc-video-key2-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps-cenc-video-key2-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480_959kbps.mp4 b/dom/media/test/bipbop_480_959kbps.mp4
new file mode 100644
index 0000000000..4a9f2ee823
--- /dev/null
+++ b/dom/media/test/bipbop_480_959kbps.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-1.m4s b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-1.m4s
new file mode 100644
index 0000000000..e2bd754c7e
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-1.m4s^headers^ b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-2.m4s b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-2.m4s
new file mode 100644
index 0000000000..347835feee
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-2.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-2.m4s^headers^ b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-3.m4s b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-3.m4s
new file mode 100644
index 0000000000..64b0da69a0
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-3.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-3.m4s^headers^ b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-3.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-3.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-4.m4s b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-4.m4s
new file mode 100644
index 0000000000..864f4248af
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-4.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-4.m4s^headers^ b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-4.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-4.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-init.mp4 b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-init.mp4
new file mode 100644
index 0000000000..416bc7a7ca
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-init.mp4^headers^ b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key1-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-1.m4s b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-1.m4s
new file mode 100644
index 0000000000..a8896e069a
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-1.m4s^headers^ b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-2.m4s b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-2.m4s
new file mode 100644
index 0000000000..0f0a35ce79
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-2.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-2.m4s^headers^ b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-3.m4s b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-3.m4s
new file mode 100644
index 0000000000..fece52ff42
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-3.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-3.m4s^headers^ b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-3.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-3.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-4.m4s b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-4.m4s
new file mode 100644
index 0000000000..70e61e3d5f
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-4.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-4.m4s^headers^ b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-4.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-4.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-init.mp4 b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-init.mp4
new file mode 100644
index 0000000000..73d542cfe0
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-init.mp4^headers^ b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps-cenc-audio-key2-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key1-1.m4s b/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key1-1.m4s
new file mode 100644
index 0000000000..796ad13670
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key1-1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key1-1.m4s^headers^ b/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key1-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key1-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key1-2.m4s b/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key1-2.m4s
new file mode 100644
index 0000000000..80824e9ffc
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key1-2.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key1-2.m4s^headers^ b/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key1-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key1-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key1-init.mp4 b/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key1-init.mp4
new file mode 100644
index 0000000000..5db21d091b
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key1-init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key1-init.mp4^headers^ b/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key1-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key1-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key2-1.m4s b/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key2-1.m4s
new file mode 100644
index 0000000000..06778e6f2b
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key2-1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key2-1.m4s^headers^ b/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key2-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key2-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key2-2.m4s b/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key2-2.m4s
new file mode 100644
index 0000000000..38a081187a
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key2-2.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key2-2.m4s^headers^ b/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key2-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key2-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key2-init.mp4 b/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key2-init.mp4
new file mode 100644
index 0000000000..bc8bddf505
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key2-init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key2-init.mp4^headers^ b/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key2-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps-cenc-video-key2-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480wp_1001kbps.mp4 b/dom/media/test/bipbop_480wp_1001kbps.mp4
new file mode 100644
index 0000000000..600376cf83
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_1001kbps.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-1.m4s b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-1.m4s
new file mode 100644
index 0000000000..e2bd754c7e
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-1.m4s^headers^ b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-2.m4s b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-2.m4s
new file mode 100644
index 0000000000..347835feee
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-2.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-2.m4s^headers^ b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-3.m4s b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-3.m4s
new file mode 100644
index 0000000000..64b0da69a0
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-3.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-3.m4s^headers^ b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-3.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-3.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-4.m4s b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-4.m4s
new file mode 100644
index 0000000000..864f4248af
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-4.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-4.m4s^headers^ b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-4.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-4.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-init.mp4 b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-init.mp4
new file mode 100644
index 0000000000..416bc7a7ca
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-init.mp4^headers^ b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key1-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-1.m4s b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-1.m4s
new file mode 100644
index 0000000000..a8896e069a
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-1.m4s^headers^ b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-2.m4s b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-2.m4s
new file mode 100644
index 0000000000..0f0a35ce79
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-2.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-2.m4s^headers^ b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-3.m4s b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-3.m4s
new file mode 100644
index 0000000000..fece52ff42
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-3.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-3.m4s^headers^ b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-3.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-3.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-4.m4s b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-4.m4s
new file mode 100644
index 0000000000..70e61e3d5f
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-4.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-4.m4s^headers^ b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-4.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-4.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-init.mp4 b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-init.mp4
new file mode 100644
index 0000000000..73d542cfe0
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-init.mp4^headers^ b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps-cenc-audio-key2-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480wp_663kbps-cenc-video-key1-1.m4s b/dom/media/test/bipbop_480wp_663kbps-cenc-video-key1-1.m4s
new file mode 100644
index 0000000000..805f4bbf3f
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps-cenc-video-key1-1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_663kbps-cenc-video-key1-1.m4s^headers^ b/dom/media/test/bipbop_480wp_663kbps-cenc-video-key1-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps-cenc-video-key1-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480wp_663kbps-cenc-video-key1-2.m4s b/dom/media/test/bipbop_480wp_663kbps-cenc-video-key1-2.m4s
new file mode 100644
index 0000000000..0a40d1cb73
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps-cenc-video-key1-2.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_663kbps-cenc-video-key1-2.m4s^headers^ b/dom/media/test/bipbop_480wp_663kbps-cenc-video-key1-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps-cenc-video-key1-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480wp_663kbps-cenc-video-key1-init.mp4 b/dom/media/test/bipbop_480wp_663kbps-cenc-video-key1-init.mp4
new file mode 100644
index 0000000000..5db21d091b
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps-cenc-video-key1-init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_663kbps-cenc-video-key1-init.mp4^headers^ b/dom/media/test/bipbop_480wp_663kbps-cenc-video-key1-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps-cenc-video-key1-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480wp_663kbps-cenc-video-key2-1.m4s b/dom/media/test/bipbop_480wp_663kbps-cenc-video-key2-1.m4s
new file mode 100644
index 0000000000..c5127beec9
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps-cenc-video-key2-1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_663kbps-cenc-video-key2-1.m4s^headers^ b/dom/media/test/bipbop_480wp_663kbps-cenc-video-key2-1.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps-cenc-video-key2-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480wp_663kbps-cenc-video-key2-2.m4s b/dom/media/test/bipbop_480wp_663kbps-cenc-video-key2-2.m4s
new file mode 100644
index 0000000000..3f344022a4
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps-cenc-video-key2-2.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_663kbps-cenc-video-key2-2.m4s^headers^ b/dom/media/test/bipbop_480wp_663kbps-cenc-video-key2-2.m4s^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps-cenc-video-key2-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480wp_663kbps-cenc-video-key2-init.mp4 b/dom/media/test/bipbop_480wp_663kbps-cenc-video-key2-init.mp4
new file mode 100644
index 0000000000..bc8bddf505
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps-cenc-video-key2-init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_480wp_663kbps-cenc-video-key2-init.mp4^headers^ b/dom/media/test/bipbop_480wp_663kbps-cenc-video-key2-init.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps-cenc-video-key2-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_480wp_663kbps.mp4 b/dom/media/test/bipbop_480wp_663kbps.mp4
new file mode 100644
index 0000000000..3cc1da69d2
--- /dev/null
+++ b/dom/media/test/bipbop_480wp_663kbps.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_audio_aac_22.05k.mp4 b/dom/media/test/bipbop_audio_aac_22.05k.mp4
new file mode 100644
index 0000000000..e89ba40a10
--- /dev/null
+++ b/dom/media/test/bipbop_audio_aac_22.05k.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_audio_aac_22.05k.mp4^headers^ b/dom/media/test/bipbop_audio_aac_22.05k.mp4^headers^
new file mode 100644
index 0000000000..12a01c4a22
--- /dev/null
+++ b/dom/media/test/bipbop_audio_aac_22.05k.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_audio_aac_44.1k.mp4 b/dom/media/test/bipbop_audio_aac_44.1k.mp4
new file mode 100644
index 0000000000..9a13333c12
--- /dev/null
+++ b/dom/media/test/bipbop_audio_aac_44.1k.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_audio_aac_44.1k.mp4^headers^ b/dom/media/test/bipbop_audio_aac_44.1k.mp4^headers^
new file mode 100644
index 0000000000..12a01c4a22
--- /dev/null
+++ b/dom/media/test/bipbop_audio_aac_44.1k.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_audio_aac_48k.mp4 b/dom/media/test/bipbop_audio_aac_48k.mp4
new file mode 100644
index 0000000000..0224350449
--- /dev/null
+++ b/dom/media/test/bipbop_audio_aac_48k.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_audio_aac_48k.mp4^headers^ b/dom/media/test/bipbop_audio_aac_48k.mp4^headers^
new file mode 100644
index 0000000000..12a01c4a22
--- /dev/null
+++ b/dom/media/test/bipbop_audio_aac_48k.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_audio_aac_88.2k.mp4 b/dom/media/test/bipbop_audio_aac_88.2k.mp4
new file mode 100644
index 0000000000..a653be973f
--- /dev/null
+++ b/dom/media/test/bipbop_audio_aac_88.2k.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_audio_aac_88.2k.mp4^headers^ b/dom/media/test/bipbop_audio_aac_88.2k.mp4^headers^
new file mode 100644
index 0000000000..12a01c4a22
--- /dev/null
+++ b/dom/media/test/bipbop_audio_aac_88.2k.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_audio_aac_8k.mp4 b/dom/media/test/bipbop_audio_aac_8k.mp4
new file mode 100644
index 0000000000..fb704090d6
--- /dev/null
+++ b/dom/media/test/bipbop_audio_aac_8k.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_audio_aac_8k.mp4^headers^ b/dom/media/test/bipbop_audio_aac_8k.mp4^headers^
new file mode 100644
index 0000000000..12a01c4a22
--- /dev/null
+++ b/dom/media/test/bipbop_audio_aac_8k.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_audio_aac_96k.mp4 b/dom/media/test/bipbop_audio_aac_96k.mp4
new file mode 100644
index 0000000000..30579f59a4
--- /dev/null
+++ b/dom/media/test/bipbop_audio_aac_96k.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_audio_aac_96k.mp4^headers^ b/dom/media/test/bipbop_audio_aac_96k.mp4^headers^
new file mode 100644
index 0000000000..12a01c4a22
--- /dev/null
+++ b/dom/media/test/bipbop_audio_aac_96k.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_cbcs_10_0_audio_1.m4s b/dom/media/test/bipbop_cbcs_10_0_audio_1.m4s
new file mode 100644
index 0000000000..98ff850161
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_10_0_audio_1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_cbcs_10_0_audio_1.m4s^headers^ b/dom/media/test/bipbop_cbcs_10_0_audio_1.m4s^headers^
new file mode 100644
index 0000000000..12a01c4a22
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_10_0_audio_1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_cbcs_10_0_audio_init.mp4 b/dom/media/test/bipbop_cbcs_10_0_audio_init.mp4
new file mode 100644
index 0000000000..ef462e0d66
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_10_0_audio_init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_cbcs_10_0_audio_init.mp4^headers^ b/dom/media/test/bipbop_cbcs_10_0_audio_init.mp4^headers^
new file mode 100644
index 0000000000..12a01c4a22
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_10_0_audio_init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_cbcs_10_0_video_1.m4s b/dom/media/test/bipbop_cbcs_10_0_video_1.m4s
new file mode 100644
index 0000000000..73a90b9a5b
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_10_0_video_1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_cbcs_10_0_video_1.m4s^headers^ b/dom/media/test/bipbop_cbcs_10_0_video_1.m4s^headers^
new file mode 100644
index 0000000000..12a01c4a22
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_10_0_video_1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_cbcs_10_0_video_init.mp4 b/dom/media/test/bipbop_cbcs_10_0_video_init.mp4
new file mode 100644
index 0000000000..266e846e9b
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_10_0_video_init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_cbcs_10_0_video_init.mp4^headers^ b/dom/media/test/bipbop_cbcs_10_0_video_init.mp4^headers^
new file mode 100644
index 0000000000..12a01c4a22
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_10_0_video_init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_cbcs_1_9_audio_1.m4s b/dom/media/test/bipbop_cbcs_1_9_audio_1.m4s
new file mode 100644
index 0000000000..98ff850161
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_1_9_audio_1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_cbcs_1_9_audio_1.m4s^headers^ b/dom/media/test/bipbop_cbcs_1_9_audio_1.m4s^headers^
new file mode 100644
index 0000000000..12a01c4a22
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_1_9_audio_1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_cbcs_1_9_audio_init.mp4 b/dom/media/test/bipbop_cbcs_1_9_audio_init.mp4
new file mode 100644
index 0000000000..8f5521eaa3
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_1_9_audio_init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_cbcs_1_9_audio_init.mp4^headers^ b/dom/media/test/bipbop_cbcs_1_9_audio_init.mp4^headers^
new file mode 100644
index 0000000000..12a01c4a22
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_1_9_audio_init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_cbcs_1_9_video_1.m4s b/dom/media/test/bipbop_cbcs_1_9_video_1.m4s
new file mode 100644
index 0000000000..7606c23199
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_1_9_video_1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_cbcs_1_9_video_1.m4s^headers^ b/dom/media/test/bipbop_cbcs_1_9_video_1.m4s^headers^
new file mode 100644
index 0000000000..12a01c4a22
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_1_9_video_1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_cbcs_1_9_video_init.mp4 b/dom/media/test/bipbop_cbcs_1_9_video_init.mp4
new file mode 100644
index 0000000000..f9f20f9aec
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_1_9_video_init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_cbcs_1_9_video_init.mp4^headers^ b/dom/media/test/bipbop_cbcs_1_9_video_init.mp4^headers^
new file mode 100644
index 0000000000..12a01c4a22
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_1_9_video_init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_cbcs_5_5_audio_1.m4s b/dom/media/test/bipbop_cbcs_5_5_audio_1.m4s
new file mode 100644
index 0000000000..98ff850161
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_5_5_audio_1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_cbcs_5_5_audio_1.m4s^headers^ b/dom/media/test/bipbop_cbcs_5_5_audio_1.m4s^headers^
new file mode 100644
index 0000000000..12a01c4a22
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_5_5_audio_1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_cbcs_5_5_audio_init.mp4 b/dom/media/test/bipbop_cbcs_5_5_audio_init.mp4
new file mode 100644
index 0000000000..3118a175f1
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_5_5_audio_init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_cbcs_5_5_audio_init.mp4^headers^ b/dom/media/test/bipbop_cbcs_5_5_audio_init.mp4^headers^
new file mode 100644
index 0000000000..12a01c4a22
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_5_5_audio_init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_cbcs_5_5_video_1.m4s b/dom/media/test/bipbop_cbcs_5_5_video_1.m4s
new file mode 100644
index 0000000000..8ed5b11aa0
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_5_5_video_1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_cbcs_5_5_video_1.m4s^headers^ b/dom/media/test/bipbop_cbcs_5_5_video_1.m4s^headers^
new file mode 100644
index 0000000000..12a01c4a22
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_5_5_video_1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_cbcs_5_5_video_init.mp4 b/dom/media/test/bipbop_cbcs_5_5_video_init.mp4
new file mode 100644
index 0000000000..be9731c4c6
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_5_5_video_init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_cbcs_5_5_video_init.mp4^headers^ b/dom/media/test/bipbop_cbcs_5_5_video_init.mp4^headers^
new file mode 100644
index 0000000000..12a01c4a22
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_5_5_video_init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_cbcs_7_7_audio_1.m4s b/dom/media/test/bipbop_cbcs_7_7_audio_1.m4s
new file mode 100644
index 0000000000..98ff850161
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_7_7_audio_1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_cbcs_7_7_audio_1.m4s^headers^ b/dom/media/test/bipbop_cbcs_7_7_audio_1.m4s^headers^
new file mode 100644
index 0000000000..12a01c4a22
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_7_7_audio_1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_cbcs_7_7_audio_init.mp4 b/dom/media/test/bipbop_cbcs_7_7_audio_init.mp4
new file mode 100644
index 0000000000..3017ff231e
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_7_7_audio_init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_cbcs_7_7_audio_init.mp4^headers^ b/dom/media/test/bipbop_cbcs_7_7_audio_init.mp4^headers^
new file mode 100644
index 0000000000..12a01c4a22
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_7_7_audio_init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_cbcs_7_7_video_1.m4s b/dom/media/test/bipbop_cbcs_7_7_video_1.m4s
new file mode 100644
index 0000000000..c3d41c16c6
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_7_7_video_1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_cbcs_7_7_video_1.m4s^headers^ b/dom/media/test/bipbop_cbcs_7_7_video_1.m4s^headers^
new file mode 100644
index 0000000000..12a01c4a22
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_7_7_video_1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_cbcs_7_7_video_init.mp4 b/dom/media/test/bipbop_cbcs_7_7_video_init.mp4
new file mode 100644
index 0000000000..994e319e57
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_7_7_video_init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_cbcs_7_7_video_init.mp4^headers^ b/dom/media/test/bipbop_cbcs_7_7_video_init.mp4^headers^
new file mode 100644
index 0000000000..12a01c4a22
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_7_7_video_init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_cbcs_9_8_audio_1.m4s b/dom/media/test/bipbop_cbcs_9_8_audio_1.m4s
new file mode 100644
index 0000000000..98ff850161
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_9_8_audio_1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_cbcs_9_8_audio_1.m4s^headers^ b/dom/media/test/bipbop_cbcs_9_8_audio_1.m4s^headers^
new file mode 100644
index 0000000000..12a01c4a22
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_9_8_audio_1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_cbcs_9_8_audio_init.mp4 b/dom/media/test/bipbop_cbcs_9_8_audio_init.mp4
new file mode 100644
index 0000000000..0f436af510
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_9_8_audio_init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_cbcs_9_8_audio_init.mp4^headers^ b/dom/media/test/bipbop_cbcs_9_8_audio_init.mp4^headers^
new file mode 100644
index 0000000000..12a01c4a22
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_9_8_audio_init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_cbcs_9_8_video_1.m4s b/dom/media/test/bipbop_cbcs_9_8_video_1.m4s
new file mode 100644
index 0000000000..1bb3dd493c
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_9_8_video_1.m4s
Binary files differ
diff --git a/dom/media/test/bipbop_cbcs_9_8_video_1.m4s^headers^ b/dom/media/test/bipbop_cbcs_9_8_video_1.m4s^headers^
new file mode 100644
index 0000000000..12a01c4a22
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_9_8_video_1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_cbcs_9_8_video_init.mp4 b/dom/media/test/bipbop_cbcs_9_8_video_init.mp4
new file mode 100644
index 0000000000..df5f9a13b8
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_9_8_video_init.mp4
Binary files differ
diff --git a/dom/media/test/bipbop_cbcs_9_8_video_init.mp4^headers^ b/dom/media/test/bipbop_cbcs_9_8_video_init.mp4^headers^
new file mode 100644
index 0000000000..12a01c4a22
--- /dev/null
+++ b/dom/media/test/bipbop_cbcs_9_8_video_init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_short_pixel_metadata_bigger_than_in_stream_vp8.webm b/dom/media/test/bipbop_short_pixel_metadata_bigger_than_in_stream_vp8.webm
new file mode 100644
index 0000000000..7d0935aead
--- /dev/null
+++ b/dom/media/test/bipbop_short_pixel_metadata_bigger_than_in_stream_vp8.webm
Binary files differ
diff --git a/dom/media/test/bipbop_short_pixel_metadata_bigger_than_in_stream_vp8.webm^headers^ b/dom/media/test/bipbop_short_pixel_metadata_bigger_than_in_stream_vp8.webm^headers^
new file mode 100644
index 0000000000..12a01c4a22
--- /dev/null
+++ b/dom/media/test/bipbop_short_pixel_metadata_bigger_than_in_stream_vp8.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_short_pixel_metadata_narrower_than_in_stream_vp8.webm b/dom/media/test/bipbop_short_pixel_metadata_narrower_than_in_stream_vp8.webm
new file mode 100644
index 0000000000..39d20e1e87
--- /dev/null
+++ b/dom/media/test/bipbop_short_pixel_metadata_narrower_than_in_stream_vp8.webm
Binary files differ
diff --git a/dom/media/test/bipbop_short_pixel_metadata_narrower_than_in_stream_vp8.webm^headers^ b/dom/media/test/bipbop_short_pixel_metadata_narrower_than_in_stream_vp8.webm^headers^
new file mode 100644
index 0000000000..12a01c4a22
--- /dev/null
+++ b/dom/media/test/bipbop_short_pixel_metadata_narrower_than_in_stream_vp8.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_short_pixel_metadata_smaller_than_in_stream_vp8.webm b/dom/media/test/bipbop_short_pixel_metadata_smaller_than_in_stream_vp8.webm
new file mode 100644
index 0000000000..4d6a68fbc6
--- /dev/null
+++ b/dom/media/test/bipbop_short_pixel_metadata_smaller_than_in_stream_vp8.webm
Binary files differ
diff --git a/dom/media/test/bipbop_short_pixel_metadata_smaller_than_in_stream_vp8.webm^headers^ b/dom/media/test/bipbop_short_pixel_metadata_smaller_than_in_stream_vp8.webm^headers^
new file mode 100644
index 0000000000..12a01c4a22
--- /dev/null
+++ b/dom/media/test/bipbop_short_pixel_metadata_smaller_than_in_stream_vp8.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bipbop_short_vp8.webm b/dom/media/test/bipbop_short_vp8.webm
new file mode 100644
index 0000000000..7c47b3d7e8
--- /dev/null
+++ b/dom/media/test/bipbop_short_vp8.webm
Binary files differ
diff --git a/dom/media/test/bipbop_short_vp8.webm^headers^ b/dom/media/test/bipbop_short_vp8.webm^headers^
new file mode 100644
index 0000000000..12a01c4a22
--- /dev/null
+++ b/dom/media/test/bipbop_short_vp8.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/black100x100-aspect3to2.ogv b/dom/media/test/black100x100-aspect3to2.ogv
new file mode 100644
index 0000000000..81fe51ffb3
--- /dev/null
+++ b/dom/media/test/black100x100-aspect3to2.ogv
Binary files differ
diff --git a/dom/media/test/black100x100-aspect3to2.ogv^headers^ b/dom/media/test/black100x100-aspect3to2.ogv^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/black100x100-aspect3to2.ogv^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bogus.duh b/dom/media/test/bogus.duh
new file mode 100644
index 0000000000..528ae275d0
--- /dev/null
+++ b/dom/media/test/bogus.duh
@@ -0,0 +1,45 @@
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
diff --git a/dom/media/test/bogus.ogv b/dom/media/test/bogus.ogv
new file mode 100644
index 0000000000..528ae275d0
--- /dev/null
+++ b/dom/media/test/bogus.ogv
@@ -0,0 +1,45 @@
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
diff --git a/dom/media/test/bogus.ogv^headers^ b/dom/media/test/bogus.ogv^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bogus.ogv^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bogus.wav b/dom/media/test/bogus.wav
new file mode 100644
index 0000000000..528ae275d0
--- /dev/null
+++ b/dom/media/test/bogus.wav
@@ -0,0 +1,45 @@
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
+bogus bogus bogus
diff --git a/dom/media/test/bogus.wav^headers^ b/dom/media/test/bogus.wav^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bogus.wav^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/browser/browser.ini b/dom/media/test/browser/browser.ini
new file mode 100644
index 0000000000..11ff9bad1c
--- /dev/null
+++ b/dom/media/test/browser/browser.ini
@@ -0,0 +1,28 @@
+[DEFAULT]
+subsuite = media-bc
+prefs =
+ gfx.font_loader.delay=0
+
+support-files =
+ file_empty_page.html
+ file_media.html
+ ../av1.mp4
+ ../bipbop_short_vp8.webm
+ ../bunny_hd_5s.mp4
+ ../eme_standalone.js
+ ../gizmo.mp4
+ ../gizmo.webm
+ ../sintel-short-clearkey-subsample-encrypted-video.webm
+ ../small-shot.flac
+ ../small-shot.m4a
+ ../small-shot.mp3
+ ../small-shot.ogg
+ ../TestPatternHDR.mp4
+
+[browser_encrypted_play_time_telemetry.js]
+skip-if =
+ apple_silicon # Disabled due to bleedover with other tests when run in regular suites; passes in "failures" jobs
+[browser_tab_visibility_and_play_time.js]
+skip-if =
+ os == "win" && os_version == "6.1" # Skip on Azure - frequent failure
+[browser_telemetry_video_hardware_decoding_support.js]
diff --git a/dom/media/test/browser/browser_encrypted_play_time_telemetry.js b/dom/media/test/browser/browser_encrypted_play_time_telemetry.js
new file mode 100644
index 0000000000..aebc386e6b
--- /dev/null
+++ b/dom/media/test/browser/browser_encrypted_play_time_telemetry.js
@@ -0,0 +1,266 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This test verifies that telemetry gathered around encrypted media playtime
+// is gathered as expected.
+
+"use strict";
+
+/* import-globals-from ../eme_standalone.js */
+
+// Clears any existing telemetry data that has been accumulated. Returns a
+// promise the will be resolved once the telemetry store is clear.
+async function clearTelemetry() {
+ // There's an arbitrary interval of 2 seconds in which the content
+ // processes sync their event data with the parent process, we wait
+ // this out to ensure that we clear everything that is left over from
+ // previous tests and don't receive random events in the middle of our tests.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 2000));
+
+ Services.telemetry.clearEvents();
+ return TestUtils.waitForCondition(() => {
+ let events = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_ALL_CHANNELS,
+ true
+ ).content;
+ return !events || !events.length;
+ });
+}
+
+// Opens a tab containing a blank page, returns a promise that will resolve
+// to that tab.
+async function openTab() {
+ const emptyPageUri =
+ "https://example.com/browser/dom/media/test/browser/file_empty_page.html";
+ return BrowserTestUtils.openNewForegroundTab(window.gBrowser, emptyPageUri);
+}
+
+// Creates and configures a video element for EME playback in `tab`. Does not
+// start playback for the element. Returns a promise that will resolve once
+// the element is setup and ready for playback.
+async function loadEmeVideo(tab) {
+ const emeHelperUri =
+ gTestPath.substr(0, gTestPath.lastIndexOf("/")) + "/eme_standalone.js";
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [emeHelperUri],
+ async _emeHelperUri => {
+ // Begin helper functions.
+ async function once(target, name) {
+ return new Promise(r =>
+ target.addEventListener(name, r, { once: true })
+ );
+ }
+
+ // Helper to clone data into content so the EME helper can use the data.
+ function cloneIntoContent(data) {
+ return Cu.cloneInto(data, content.wrappedJSObject);
+ }
+ // End helper functions.
+
+ // Load the EME helper into content.
+ Services.scriptloader.loadSubScript(_emeHelperUri, content);
+ // Setup EME with the helper.
+ let video = content.document.createElement("video");
+ video.id = "media";
+ content.document.body.appendChild(video);
+ let emeHelper = new content.wrappedJSObject.EmeHelper();
+ emeHelper.SetKeySystem(
+ content.wrappedJSObject.EmeHelper.GetClearkeyKeySystemString()
+ );
+ emeHelper.SetInitDataTypes(cloneIntoContent(["webm"]));
+ emeHelper.SetVideoCapabilities(
+ cloneIntoContent([{ contentType: 'video/webm; codecs="vp9"' }])
+ );
+ emeHelper.AddKeyIdAndKey(
+ "2cdb0ed6119853e7850671c3e9906c3c",
+ "808b9adac384de1e4f56140f4ad76194"
+ );
+ emeHelper.onerror = error => {
+ is(false, `Got unexpected error from EME helper: ${error}`);
+ };
+ await emeHelper.ConfigureEme(video);
+ // Done setting up EME.
+
+ // Setup MSE.
+ const ms = new content.wrappedJSObject.MediaSource();
+ video.src = content.wrappedJSObject.URL.createObjectURL(ms);
+ await once(ms, "sourceopen");
+ const sb = ms.addSourceBuffer("video/webm");
+ const videoFile = "sintel-short-clearkey-subsample-encrypted-video.webm";
+ let fetchResponse = await content.fetch(videoFile);
+ sb.appendBuffer(await fetchResponse.arrayBuffer());
+ await once(sb, "updateend");
+ ms.endOfStream();
+ await once(ms, "sourceended");
+ }
+ );
+}
+
+// Plays the media in `tab` until the 'ended' event is fire. Returns a promise
+// that resolves once that state has been reached.
+async function playMediaThrough(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ let video = content.document.getElementById("media");
+ await Promise.all([new Promise(r => (video.onended = r)), video.play()]);
+ });
+}
+
+// Plays the media in `tab` until the 'timeupdate' event is fire. Returns a
+// promise that resolves once that state has been reached.
+async function playMediaToTimeUpdate(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ let video = content.document.getElementById("media");
+ await Promise.all([
+ new Promise(r => (video.ontimeupdate = r)),
+ video.play(),
+ ]);
+ });
+}
+
+// Aborts existing loads and replaces the media on the media element with an
+// unencrypted file.
+async function replaceMediaWithUnencrypted(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ let video = content.document.getElementById("media");
+ video.src = "gizmo.mp4";
+ video.load();
+ });
+}
+
+// Clears/nulls the media keys on the media in `tab`.
+async function clearMediaKeys(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
+ let video = content.document.getElementById("media");
+ await video.setMediaKeys(null);
+ });
+}
+
+// Wait for telemetry information to be received from the content process
+// then get the relevant histograms for the tests and return the sums of
+// those histograms. If a histogram does not exist this will return a 0
+// sum. Returns a promise the resolves to an object with sums for
+// - VIDEO_PLAY_TIME_MS
+// - VIDEO_ENCRYPTED_PLAY_TIME_MS
+// - VIDEO_CLEARKEY_PLAY_TIME_MS
+// This function clears the histograms as it gets them.
+async function getTelemetrySums() {
+ // The telemetry was gathered in the content process, so we have to wait
+ // until is arrived in the parent to check it. At time of writing there's
+ // not a more elegant way of doing this than polling.
+ return TestUtils.waitForCondition(() => {
+ let histograms = Services.telemetry.getSnapshotForHistograms(
+ "main",
+ true
+ ).content;
+ // All the histogram data should come at the same time, so we just check
+ // for playtime here as we always expect it in these tests, but we'll
+ // grab other values if present.
+ if (histograms.VIDEO_PLAY_TIME_MS) {
+ // We only expect to have one value for each histogram, so returning the
+ // sums is a short hand for returning that one value.
+ return {
+ VIDEO_PLAY_TIME_MS: histograms.VIDEO_PLAY_TIME_MS.sum,
+ VIDEO_ENCRYPTED_PLAY_TIME_MS: histograms.VIDEO_ENCRYPTED_PLAY_TIME_MS
+ ? histograms.VIDEO_ENCRYPTED_PLAY_TIME_MS.sum
+ : 0,
+ VIDEO_CLEARKEY_PLAY_TIME_MS: histograms.VIDEO_CLEARKEY_PLAY_TIME_MS
+ ? histograms.VIDEO_CLEARKEY_PLAY_TIME_MS.sum
+ : 0,
+ };
+ }
+ return null;
+ }, "recorded telemetry from playing media");
+}
+
+// Clear telemetry before other tests. Internally the tests clear the telemetry
+// when they check it, so we shouldn't need to do this between tests.
+add_task(clearTelemetry);
+
+add_task(async function testEncryptedMediaPlayback() {
+ let testTab = await openTab();
+
+ await loadEmeVideo(testTab);
+ await playMediaThrough(testTab);
+
+ BrowserTestUtils.removeTab(testTab);
+
+ let telemetrySums = await getTelemetrySums();
+
+ ok(telemetrySums, "Should get play time telemetry");
+ is(
+ telemetrySums.VIDEO_PLAY_TIME_MS,
+ telemetrySums.VIDEO_ENCRYPTED_PLAY_TIME_MS,
+ "Play time should be the same as encrypted play time"
+ );
+ is(
+ telemetrySums.VIDEO_PLAY_TIME_MS,
+ telemetrySums.VIDEO_CLEARKEY_PLAY_TIME_MS,
+ "Play time should be the same as clearkey play time"
+ );
+ ok(
+ telemetrySums.VIDEO_PLAY_TIME_MS > 0,
+ "Should have a play time greater than zero"
+ );
+});
+
+add_task(async function testChangingFromEncryptedToUnencrypted() {
+ let testTab = await openTab();
+
+ await loadEmeVideo(testTab);
+ await replaceMediaWithUnencrypted(testTab);
+ await playMediaToTimeUpdate(testTab);
+
+ BrowserTestUtils.removeTab(testTab);
+
+ let telemetrySums = await getTelemetrySums();
+
+ ok(telemetrySums, "Should get play time telemetry");
+ is(
+ telemetrySums.VIDEO_ENCRYPTED_PLAY_TIME_MS,
+ 0,
+ "Encrypted play time should be 0"
+ );
+ is(
+ telemetrySums.VIDEO_PLAY_TIME_MS,
+ telemetrySums.VIDEO_CLEARKEY_PLAY_TIME_MS,
+ "Play time should be the same as clearkey play time because the media element still has a media keys attached"
+ );
+ ok(
+ telemetrySums.VIDEO_PLAY_TIME_MS > 0,
+ "Should have a play time greater than zero"
+ );
+});
+
+add_task(
+ async function testChangingFromEncryptedToUnencryptedAndClearingMediaKeys() {
+ let testTab = await openTab();
+
+ await loadEmeVideo(testTab);
+ await replaceMediaWithUnencrypted(testTab);
+ await clearMediaKeys(testTab);
+ await playMediaToTimeUpdate(testTab);
+
+ BrowserTestUtils.removeTab(testTab);
+
+ let telemetrySums = await getTelemetrySums();
+
+ ok(telemetrySums, "Should get play time telemetry");
+ is(
+ telemetrySums.VIDEO_ENCRYPTED_PLAY_TIME_MS,
+ 0,
+ "Encrypted play time should be 0"
+ );
+ is(
+ telemetrySums.VIDEO_CLEARKEY_PLAY_TIME_MS,
+ 0,
+ "Clearkey play time should be 0"
+ );
+ ok(
+ telemetrySums.VIDEO_PLAY_TIME_MS > 0,
+ "Should have a play time greater than zero"
+ );
+ }
+);
diff --git a/dom/media/test/browser/browser_tab_visibility_and_play_time.js b/dom/media/test/browser/browser_tab_visibility_and_play_time.js
new file mode 100644
index 0000000000..66fae40889
--- /dev/null
+++ b/dom/media/test/browser/browser_tab_visibility_and_play_time.js
@@ -0,0 +1,216 @@
+/**
+ * This test is used to ensure that invisible play time would be accumulated
+ * when tab is in background. It also checks the HDR video accumulation time.
+ * However, this test won't directly check the reported telemetry result,
+ * because we can't check the snapshot histogram in the content process.
+ * The actual probe checking happens in `test_accumulated_play_time.html`.
+ */
+"use strict";
+
+const PAGE_URL =
+ "https://example.com/browser/dom/media/test/browser/file_media.html";
+
+// This HDR tests will only pass on platforms that accurately report color
+// depth in their VideoInfo structures. Presently, that is only true for
+// macOS.
+
+const reportsColorDepthFromVideoData = AppConstants.platform == "macosx";
+
+add_task(async function testChangingTabVisibilityAffectsInvisiblePlayTime() {
+ const originalTab = gBrowser.selectedTab;
+ const mediaTab = await openMediaTab(PAGE_URL);
+
+ info(`measuring play time when tab is in foreground`);
+ await startMedia({
+ mediaTab,
+ shouldAccumulateTime: true,
+ shouldAccumulateInvisibleTime: false,
+ shouldAccumulateHDRTime: reportsColorDepthFromVideoData,
+ });
+ await pauseMedia(mediaTab);
+
+ info(`measuring play time when tab is in background`);
+ await BrowserTestUtils.switchTab(window.gBrowser, originalTab);
+ await startMedia({
+ mediaTab,
+ shouldAccumulateTime: true,
+ shouldAccumulateInvisibleTime: true,
+ shouldAccumulateHDRTime: reportsColorDepthFromVideoData,
+ });
+ await pauseMedia(mediaTab);
+
+ BrowserTestUtils.removeTab(mediaTab);
+});
+
+/**
+ * Following are helper functions.
+ */
+async function openMediaTab(url) {
+ info(`open tab for media playback`);
+ const tab = await BrowserTestUtils.openNewForegroundTab(window.gBrowser, url);
+ info(`add content helper functions and variables`);
+ await SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ content.waitForOnTimeUpdate = element => {
+ return new Promise(resolve => {
+ element.addEventListener(
+ "timeupdate",
+ e => {
+ resolve();
+ },
+ { once: true }
+ );
+ });
+ };
+
+ content.sleep = ms => {
+ return new Promise(resolve => content.setTimeout(resolve, ms));
+ };
+
+ content.assertAttributeDefined = (videoChrome, checkType) => {
+ ok(videoChrome[checkType] != undefined, `${checkType} exists`);
+ };
+ content.assertValueEqualTo = (videoChrome, checkType, expectedValue) => {
+ content.assertAttributeDefined(videoChrome, checkType);
+ is(
+ videoChrome[checkType],
+ expectedValue,
+ `${checkType} equals to ${expectedValue}`
+ );
+ };
+ content.assertValueConstantlyIncreases = async (videoChrome, checkType) => {
+ content.assertAttributeDefined(videoChrome, checkType);
+ const valueSnapshot = videoChrome[checkType];
+ await content.waitForOnTimeUpdate(videoChrome);
+ ok(
+ videoChrome[checkType] > valueSnapshot,
+ `${checkType} keeps increasing`
+ );
+ };
+ content.assertValueKeptUnchanged = async (videoChrome, checkType) => {
+ content.assertAttributeDefined(videoChrome, checkType);
+ const valueSnapshot = videoChrome[checkType];
+ await content.sleep(1000);
+ ok(
+ videoChrome[checkType] == valueSnapshot,
+ `${checkType} keeps unchanged`
+ );
+ };
+ });
+ return tab;
+}
+
+function startMedia({
+ mediaTab,
+ shouldAccumulateTime,
+ shouldAccumulateInvisibleTime,
+ shouldAccumulateHDRTime,
+}) {
+ return SpecialPowers.spawn(
+ mediaTab.linkedBrowser,
+ [
+ shouldAccumulateTime,
+ shouldAccumulateInvisibleTime,
+ shouldAccumulateHDRTime,
+ ],
+ async (accumulateTime, accumulateInvisibleTime, accumulateHDRTime) => {
+ const video = content.document.getElementById("video");
+ ok(
+ await video.play().then(
+ () => true,
+ () => false
+ ),
+ "video started playing"
+ );
+ const videoChrome = SpecialPowers.wrap(video);
+ if (accumulateTime) {
+ await content.assertValueConstantlyIncreases(
+ videoChrome,
+ "totalVideoPlayTime"
+ );
+ } else {
+ await content.assertValueKeptUnchanged(
+ videoChrome,
+ "totalVideoPlayTime"
+ );
+ }
+ if (accumulateInvisibleTime) {
+ await content.assertValueConstantlyIncreases(
+ videoChrome,
+ "invisiblePlayTime"
+ );
+ } else {
+ await content.assertValueKeptUnchanged(
+ videoChrome,
+ "invisiblePlayTime"
+ );
+ }
+
+ const videoHDR = content.document.getElementById("videoHDR");
+
+ // HDR test video might not decode on all platforms, so catch
+ // the play() command and exit early in such a case. Failure to
+ // decode might manifest as a timeout, so add a rejection race
+ // to catch that.
+ let didDecode = true;
+ const playPromise = videoHDR.play().then(
+ () => true,
+ () => false
+ );
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ const tooSlowPromise = new Promise(resolve =>
+ setTimeout(() => {
+ info("videoHDR timed out.");
+ didDecode = false;
+ resolve(false);
+ }, 1000)
+ );
+ /* eslint-enable mozilla/no-arbitrary-setTimeout */
+
+ let didPlay = await Promise.race(playPromise, tooSlowPromise).catch(
+ err => {
+ info("videoHDR failed to decode with error: " + err.message);
+ didDecode = false;
+ return false;
+ }
+ );
+
+ if (!didDecode) {
+ return;
+ }
+
+ ok(didPlay, "videoHDR started playing");
+ const videoHDRChrome = SpecialPowers.wrap(videoHDR);
+ if (accumulateHDRTime) {
+ await content.assertValueConstantlyIncreases(
+ videoHDRChrome,
+ "totalVideoHDRPlayTime"
+ );
+ } else {
+ await content.assertValueKeptUnchanged(
+ videoHDRChrome,
+ "totalVideoHDRPlayTime"
+ );
+ }
+ }
+ );
+}
+
+function pauseMedia(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async _ => {
+ const video = content.document.getElementById("video");
+ video.pause();
+ ok(true, "video paused");
+ const videoChrome = SpecialPowers.wrap(video);
+ await content.assertValueKeptUnchanged(videoChrome, "totalVideoPlayTime");
+ await content.assertValueKeptUnchanged(videoChrome, "invisiblePlayTime");
+
+ const videoHDR = content.document.getElementById("videoHDR");
+ videoHDR.pause();
+ ok(true, "videoHDR paused");
+ const videoHDRChrome = SpecialPowers.wrap(videoHDR);
+ await content.assertValueKeptUnchanged(
+ videoHDRChrome,
+ "totalVideoHDRPlayTime"
+ );
+ });
+}
diff --git a/dom/media/test/browser/browser_telemetry_video_hardware_decoding_support.js b/dom/media/test/browser/browser_telemetry_video_hardware_decoding_support.js
new file mode 100644
index 0000000000..3b1b41c03f
--- /dev/null
+++ b/dom/media/test/browser/browser_telemetry_video_hardware_decoding_support.js
@@ -0,0 +1,106 @@
+/**
+ * This test is used to ensure that the scalar which indicates whether hardware
+ * decoding is supported for a specific video codec type can be recorded
+ * correctly.
+ */
+"use strict";
+
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // In order to test av1 in the chrome process, see https://bit.ly/3oF0oan
+ ["media.rdd-process.enabled", false],
+ ],
+ });
+});
+
+const ALL_SCALAR = "media.video_hardware_decoding_support";
+const HD_SCALAR = "media.video_hd_hardware_decoding_support";
+
+add_task(async function testVideoCodecs() {
+ // There are still other video codecs, but we only care about these popular
+ // codec types.
+ const testFiles = [
+ { fileName: "gizmo.mp4", type: "video/avc" },
+ { fileName: "gizmo.webm", type: "video/vp9" },
+ { fileName: "bipbop_short_vp8.webm", type: "video/vp8" },
+ { fileName: "av1.mp4", type: "video/av1" },
+ { fileName: "bunny_hd_5s.mp4", type: "video/avc", hd: true },
+ ];
+
+ for (const file of testFiles) {
+ const { fileName, type, hd } = file;
+ let video = document.createElement("video");
+ video.src = GetTestWebBasedURL(fileName);
+ await video.play();
+ let snapshot = Services.telemetry.getSnapshotForKeyedScalars(
+ "main",
+ false
+ ).parent;
+ ok(
+ snapshot.hasOwnProperty(ALL_SCALAR),
+ `Found stored scalar '${ALL_SCALAR}'`
+ );
+ ok(
+ snapshot[ALL_SCALAR].hasOwnProperty(type),
+ `Found key '${type}' in '${ALL_SCALAR}'`
+ );
+ if (hd) {
+ ok(
+ snapshot.hasOwnProperty(HD_SCALAR),
+ `HD video '${fileName}' should record a scalar '${HD_SCALAR}'`
+ );
+ ok(
+ snapshot[HD_SCALAR].hasOwnProperty(type),
+ `Found key '${type}' in '${HD_SCALAR}'`
+ );
+ } else {
+ ok(
+ !snapshot.hasOwnProperty(HD_SCALAR),
+ `SD video won't store a scalar '${HD_SCALAR}'`
+ );
+ }
+ video.src = "";
+ Services.telemetry.clearScalars();
+ }
+});
+
+add_task(async function testAudioCodecs() {
+ const testFiles = [
+ "small-shot.ogg",
+ "small-shot.m4a",
+ "small-shot.mp3",
+ "small-shot.flac",
+ ];
+ for (const file of testFiles) {
+ let audio = document.createElement("audio");
+ info(GetTestWebBasedURL(file));
+ audio.src = GetTestWebBasedURL(file);
+ await audio.play();
+ let snapshot = Services.telemetry.getSnapshotForKeyedScalars(
+ "main",
+ false
+ ).parent;
+ ok(
+ !snapshot ||
+ (!snapshot.hasOwnProperty(ALL_SCALAR) &&
+ !snapshot.hasOwnProperty(HD_SCALAR)),
+ `Did not record scalar for ${file}`
+ );
+ audio.src = "";
+ }
+});
+
+/**
+ * Return a web-based URL for a given file based on the testing directory.
+ * @param {String} fileName
+ * file that caller wants its web-based url
+ */
+function GetTestWebBasedURL(fileName) {
+ return (
+ getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content",
+ "http://example.org"
+ ) + fileName
+ );
+}
diff --git a/dom/media/test/browser/file_empty_page.html b/dom/media/test/browser/file_empty_page.html
new file mode 100644
index 0000000000..cd1b7830be
--- /dev/null
+++ b/dom/media/test/browser/file_empty_page.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>An empty page</title>
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/media/test/browser/file_media.html b/dom/media/test/browser/file_media.html
new file mode 100644
index 0000000000..36dca8d01c
--- /dev/null
+++ b/dom/media/test/browser/file_media.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Non-Autoplay page</title>
+</head>
+<body>
+<video id="video" src="gizmo.mp4" loop></video>
+<video id="videoHDR" src="TestPatternHDR.mp4" loop></video>
+</body>
+</html>
diff --git a/dom/media/test/browser/wmfme/browser.ini b/dom/media/test/browser/wmfme/browser.ini
new file mode 100644
index 0000000000..9913bdb19e
--- /dev/null
+++ b/dom/media/test/browser/wmfme/browser.ini
@@ -0,0 +1,11 @@
+[DEFAULT]
+subsuite = media-bc
+tags = media-engine-compatible
+run-if = wmfme
+support-files =
+ head.js
+ file_video.html
+ ../../gizmo.mp4
+
+[browser_wmfme_crash.js]
+[browser_wmfme_max_crashes.js]
diff --git a/dom/media/test/browser/wmfme/browser_wmfme_crash.js b/dom/media/test/browser/wmfme/browser_wmfme_crash.js
new file mode 100644
index 0000000000..8ebe582595
--- /dev/null
+++ b/dom/media/test/browser/wmfme/browser_wmfme_crash.js
@@ -0,0 +1,52 @@
+"use strict";
+
+/**
+ * This test aims to ensure that the media engine playback will recover from a
+ * crash and keep playing without any problem.
+ */
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.wmf.media-engine.enabled", true],
+ ["media.wmf.media-engine.channel-decoder.enabled", true],
+ ],
+ });
+});
+
+const VIDEO_PAGE = GetTestWebBasedURL("file_video.html");
+
+add_task(async function testPlaybackRecoveryFromCrash() {
+ info(`Create a tab and load test page`);
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "about:blank"
+ );
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, VIDEO_PAGE);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ await playVideo(tab);
+
+ info("Ensure video is running via the media engine framework");
+ await assertRunningProcessAndDecoderName(tab, {
+ expectedProcess: "Utility MF Media Engine CDM",
+ expectedDecoder: "media engine video stream",
+ });
+
+ const pidBeforeCrash = await getMFCDMProcessId();
+ await crashUtilityProcess(pidBeforeCrash);
+
+ info("The CDM process should be recreated which makes media keep playing");
+ await assertRunningProcessAndDecoderName(tab, {
+ expectedProcess: "Utility MF Media Engine CDM",
+ expectedDecoder: "media engine video stream",
+ });
+
+ const pidAfterCrash = await getMFCDMProcessId();
+ isnot(
+ pidBeforeCrash,
+ pidAfterCrash,
+ `new process ${pidAfterCrash} is not previous crashed one ${pidBeforeCrash}`
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/dom/media/test/browser/wmfme/browser_wmfme_max_crashes.js b/dom/media/test/browser/wmfme/browser_wmfme_max_crashes.js
new file mode 100644
index 0000000000..4472814171
--- /dev/null
+++ b/dom/media/test/browser/wmfme/browser_wmfme_max_crashes.js
@@ -0,0 +1,69 @@
+"use strict";
+
+/**
+ * This test aims to ensure that the MFCDM process won't be recovered once the
+ * amount of crashes has exceeded the amount of value which we tolerate.
+ */
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.wmf.media-engine.enabled", true],
+ ["media.wmf.media-engine.channel-decoder.enabled", true],
+ ],
+ });
+});
+
+const VIDEO_PAGE = GetTestWebBasedURL("file_video.html");
+
+add_task(async function testPlaybackRecoveryFromCrash() {
+ const maxCrashes = Services.prefs.getIntPref(
+ "media.wmf.media-engine.max-crashes"
+ );
+ info(`The amount of tolerable crashes=${maxCrashes}`);
+
+ info(`Create a tab and load test page`);
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ window.gBrowser,
+ "about:blank"
+ );
+ BrowserTestUtils.loadURIString(tab.linkedBrowser, VIDEO_PAGE);
+ await BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ await playVideo(tab);
+
+ info("Ensure video is running via the media engine framework");
+ await assertRunningProcessAndDecoderName(tab, {
+ expectedProcess: "Utility MF Media Engine CDM",
+ expectedDecoder: "media engine video stream",
+ });
+
+ let pidBeforeCrash, pidAfterCrash;
+ for (let idx = 0; idx < maxCrashes; idx++) {
+ pidBeforeCrash = await getMFCDMProcessId();
+ await crashUtilityProcess(pidBeforeCrash);
+
+ info("The CDM process should be recreated which makes media keep playing");
+ await assertRunningProcessAndDecoderName(tab, {
+ expectedProcess: "Utility MF Media Engine CDM",
+ expectedDecoder: "media engine video stream",
+ });
+
+ pidAfterCrash = await getMFCDMProcessId();
+ isnot(
+ pidBeforeCrash,
+ pidAfterCrash,
+ `new process ${pidAfterCrash} is not previous crashed one ${pidBeforeCrash}`
+ );
+ }
+
+ info("This crash should result in not spawning MFCDM process again");
+ pidBeforeCrash = await getMFCDMProcessId();
+ await crashUtilityProcess(pidBeforeCrash);
+
+ await assertNotEqualRunningProcessAndDecoderName(tab, {
+ givenProcess: "Utility MF Media Engine CDM",
+ givenDecoder: "media engine video stream",
+ });
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/dom/media/test/browser/wmfme/file_video.html b/dom/media/test/browser/wmfme/file_video.html
new file mode 100644
index 0000000000..3c70268fbb
--- /dev/null
+++ b/dom/media/test/browser/wmfme/file_video.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>video</title>
+</head>
+<body>
+<video id="v" src="gizmo.mp4" controls loop></video>
+</body>
+</html>
diff --git a/dom/media/test/browser/wmfme/head.js b/dom/media/test/browser/wmfme/head.js
new file mode 100644
index 0000000000..2524287870
--- /dev/null
+++ b/dom/media/test/browser/wmfme/head.js
@@ -0,0 +1,200 @@
+"use strict";
+
+/**
+ * Return a web-based URL for a given file based on the testing directory.
+ * @param {String} fileName
+ * file that caller wants its web-based url
+ * @param {Boolean} cors [optional]
+ * if set, then return a url with different origin
+ */
+function GetTestWebBasedURL(fileName) {
+ const origin = "https://example.com";
+ return (
+ getRootDirectory(gTestPath).replace("chrome://mochitests/content", origin) +
+ fileName
+ );
+}
+
+/**
+ * Return current process Id for the Media Foundation CDM process.
+ */
+async function getMFCDMProcessId() {
+ const process = (await ChromeUtils.requestProcInfo()).children.find(
+ p =>
+ p.type === "utility" &&
+ p.utilityActors.find(a => a.actorName === "mfMediaEngineCDM")
+ );
+ return process.pid;
+}
+
+/**
+ * Make the utility process with given process id crash.
+ * @param {int} pid
+ * the process id for the process which is going to crash
+ */
+async function crashUtilityProcess(utilityPid) {
+ info(`Crashing process ${utilityPid}`);
+ SimpleTest.expectChildProcessCrash();
+
+ const crashMan = Services.crashmanager;
+ const utilityProcessGone = TestUtils.topicObserved(
+ "ipc:utility-shutdown",
+ (subject, data) => {
+ info(`ipc:utility-shutdown: data=${data} subject=${subject}`);
+ return parseInt(data, 10) === utilityPid;
+ }
+ );
+
+ info("Prune any previous crashes");
+ const future = new Date(Date.now() + 1000 * 60 * 60 * 24);
+ await crashMan.pruneOldCrashes(future);
+
+ info("Crash Utility Process");
+ const ProcessTools = Cc["@mozilla.org/processtools-service;1"].getService(
+ Ci.nsIProcessToolsService
+ );
+
+ info(`Crash Utility Process ${utilityPid}`);
+ ProcessTools.crash(utilityPid);
+
+ info(`Waiting for utility process ${utilityPid} to go away.`);
+ let [subject, data] = await utilityProcessGone;
+ ok(
+ parseInt(data, 10) === utilityPid,
+ `Should match the crashed PID ${utilityPid} with ${data}`
+ );
+ ok(
+ subject instanceof Ci.nsIPropertyBag2,
+ "Subject needs to be a nsIPropertyBag2 to clean up properly"
+ );
+
+ const dumpID = subject.getPropertyAsAString("dumpID");
+ ok(dumpID, "There should be a dumpID");
+
+ await crashMan.ensureCrashIsPresent(dumpID);
+ await crashMan.getCrashes().then(crashes => {
+ is(crashes.length, 1, "There should be only one record");
+ const crash = crashes[0];
+ ok(
+ crash.isOfType(
+ crashMan.processTypes[Ci.nsIXULRuntime.PROCESS_TYPE_UTILITY],
+ crashMan.CRASH_TYPE_CRASH
+ ),
+ "Record should be a utility process crash"
+ );
+ ok(crash.id === dumpID, "Record should have an ID");
+ });
+
+ let minidumpDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ minidumpDirectory.append("minidumps");
+
+ let dumpfile = minidumpDirectory.clone();
+ dumpfile.append(dumpID + ".dmp");
+ if (dumpfile.exists()) {
+ info(`Removal of ${dumpfile.path}`);
+ dumpfile.remove(false);
+ }
+
+ let extrafile = minidumpDirectory.clone();
+ extrafile.append(dumpID + ".extra");
+ info(`Removal of ${extrafile.path}`);
+ if (extrafile.exists()) {
+ extrafile.remove(false);
+ }
+}
+
+/**
+ * Make video in the tab play.
+ * @param {object} tab
+ * the tab contains at least one video element
+ */
+async function playVideo(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], async _ => {
+ const video = content.document.querySelector("video");
+ ok(
+ await video.play().then(
+ () => true,
+ () => false
+ ),
+ "video started playing"
+ );
+ });
+}
+
+/**
+ * Check whether the video playback is performed in the right process and right decoder.
+ * @param {object} tab
+ * the tab which has a playing video
+ * @param {string} expectedProcess
+ * the expected process name
+ * @param {string} expectedDecoder
+ * the expected decoder name
+ */
+async function assertRunningProcessAndDecoderName(
+ tab,
+ { expectedProcess, expectedDecoder } = {}
+) {
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [expectedProcess, expectedDecoder],
+ // eslint-disable-next-line no-shadow
+ async (expectedProcess, expectedDecoder) => {
+ const video = content.document.querySelector("video");
+ ok(!video.paused, "checking a playing video");
+
+ const debugInfo = await SpecialPowers.wrap(video).mozRequestDebugInfo();
+ const videoDecoderName = debugInfo.decoder.reader.videoDecoderName;
+
+ const isExpectedDecoder =
+ videoDecoderName.indexOf(`${expectedDecoder}`) == 0;
+ ok(
+ isExpectedDecoder,
+ `Playback running by decoder '${videoDecoderName}', expected '${expectedDecoder}'`
+ );
+
+ const isExpectedProcess =
+ videoDecoderName.indexOf(`(${expectedProcess} remote)`) > 0;
+ ok(
+ isExpectedProcess,
+ `Playback running in process '${videoDecoderName}', expected '${expectedProcess}'`
+ );
+ }
+ );
+}
+
+/**
+ * Check whether the video playback is not performed in the given process and given decoder.
+ * @param {object} tab
+ * the tab which has a playing video
+ * @param {string} givenProcess
+ * the process name on which the video playback should not be running
+ * @param {string} givenDecoder
+ * the decoder name with which the video playback should not be running
+ */
+async function assertNotEqualRunningProcessAndDecoderName(
+ tab,
+ { givenProcess, givenDecoder } = {}
+) {
+ return SpecialPowers.spawn(
+ tab.linkedBrowser,
+ [givenProcess, givenDecoder],
+ // eslint-disable-next-line no-shadow
+ async (givenProcess, givenDecoder) => {
+ const video = content.document.querySelector("video");
+ ok(!video.paused, "checking a playing video");
+
+ const debugInfo = await SpecialPowers.wrap(video).mozRequestDebugInfo();
+ const videoDecoderName = debugInfo.decoder.reader.videoDecoderName;
+ const pattern = /(.+?)\s+\((\S+)\s+remote\)/;
+ const match = videoDecoderName.match(pattern);
+ if (match) {
+ const decoder = match[1];
+ const process = match[2];
+ isnot(decoder, givenDecoder, `Decoder name is not equal`);
+ isnot(process, givenProcess, `Process name is not equal`);
+ } else {
+ ok(false, "failed to match decoder/process name?");
+ }
+ }
+ );
+}
diff --git a/dom/media/test/bug1066943.webm b/dom/media/test/bug1066943.webm
new file mode 100644
index 0000000000..64a24ec898
--- /dev/null
+++ b/dom/media/test/bug1066943.webm
Binary files differ
diff --git a/dom/media/test/bug1066943.webm^headers^ b/dom/media/test/bug1066943.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug1066943.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bug1301226-odd.wav b/dom/media/test/bug1301226-odd.wav
new file mode 100644
index 0000000000..dd2df4e4dd
--- /dev/null
+++ b/dom/media/test/bug1301226-odd.wav
Binary files differ
diff --git a/dom/media/test/bug1301226-odd.wav^headers^ b/dom/media/test/bug1301226-odd.wav^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug1301226-odd.wav^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bug1301226.wav b/dom/media/test/bug1301226.wav
new file mode 100644
index 0000000000..0128486f07
--- /dev/null
+++ b/dom/media/test/bug1301226.wav
Binary files differ
diff --git a/dom/media/test/bug1301226.wav^headers^ b/dom/media/test/bug1301226.wav^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug1301226.wav^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bug1377278.webm b/dom/media/test/bug1377278.webm
new file mode 100644
index 0000000000..802019f39f
--- /dev/null
+++ b/dom/media/test/bug1377278.webm
Binary files differ
diff --git a/dom/media/test/bug1377278.webm^headers^ b/dom/media/test/bug1377278.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug1377278.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bug1535980.webm b/dom/media/test/bug1535980.webm
new file mode 100644
index 0000000000..efc43959bf
--- /dev/null
+++ b/dom/media/test/bug1535980.webm
Binary files differ
diff --git a/dom/media/test/bug1535980.webm^headers^ b/dom/media/test/bug1535980.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug1535980.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bug1799787.webm b/dom/media/test/bug1799787.webm
new file mode 100644
index 0000000000..e6e613a59a
--- /dev/null
+++ b/dom/media/test/bug1799787.webm
Binary files differ
diff --git a/dom/media/test/bug1799787.webm^headers^ b/dom/media/test/bug1799787.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug1799787.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bug461281.ogg b/dom/media/test/bug461281.ogg
new file mode 100644
index 0000000000..d7f6a0ccf4
--- /dev/null
+++ b/dom/media/test/bug461281.ogg
Binary files differ
diff --git a/dom/media/test/bug461281.ogg^headers^ b/dom/media/test/bug461281.ogg^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug461281.ogg^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bug482461-theora.ogv b/dom/media/test/bug482461-theora.ogv
new file mode 100644
index 0000000000..941b8d8efd
--- /dev/null
+++ b/dom/media/test/bug482461-theora.ogv
Binary files differ
diff --git a/dom/media/test/bug482461-theora.ogv^headers^ b/dom/media/test/bug482461-theora.ogv^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug482461-theora.ogv^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bug482461.ogv b/dom/media/test/bug482461.ogv
new file mode 100644
index 0000000000..6cf6aed330
--- /dev/null
+++ b/dom/media/test/bug482461.ogv
Binary files differ
diff --git a/dom/media/test/bug482461.ogv^headers^ b/dom/media/test/bug482461.ogv^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug482461.ogv^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bug495129.ogv b/dom/media/test/bug495129.ogv
new file mode 100644
index 0000000000..44eb9296f5
--- /dev/null
+++ b/dom/media/test/bug495129.ogv
Binary files differ
diff --git a/dom/media/test/bug495129.ogv^headers^ b/dom/media/test/bug495129.ogv^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug495129.ogv^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bug495794.ogg b/dom/media/test/bug495794.ogg
new file mode 100644
index 0000000000..1c19a64061
--- /dev/null
+++ b/dom/media/test/bug495794.ogg
Binary files differ
diff --git a/dom/media/test/bug495794.ogg^headers^ b/dom/media/test/bug495794.ogg^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug495794.ogg^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bug498380.ogv b/dom/media/test/bug498380.ogv
new file mode 100644
index 0000000000..1179ecb70a
--- /dev/null
+++ b/dom/media/test/bug498380.ogv
Binary files differ
diff --git a/dom/media/test/bug498380.ogv^headers^ b/dom/media/test/bug498380.ogv^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug498380.ogv^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bug498855-1.ogv b/dom/media/test/bug498855-1.ogv
new file mode 100644
index 0000000000..95a524da4c
--- /dev/null
+++ b/dom/media/test/bug498855-1.ogv
Binary files differ
diff --git a/dom/media/test/bug498855-1.ogv^headers^ b/dom/media/test/bug498855-1.ogv^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug498855-1.ogv^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bug498855-2.ogv b/dom/media/test/bug498855-2.ogv
new file mode 100644
index 0000000000..795a308ae1
--- /dev/null
+++ b/dom/media/test/bug498855-2.ogv
Binary files differ
diff --git a/dom/media/test/bug498855-2.ogv^headers^ b/dom/media/test/bug498855-2.ogv^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug498855-2.ogv^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bug498855-3.ogv b/dom/media/test/bug498855-3.ogv
new file mode 100644
index 0000000000..714858dfed
--- /dev/null
+++ b/dom/media/test/bug498855-3.ogv
Binary files differ
diff --git a/dom/media/test/bug498855-3.ogv^headers^ b/dom/media/test/bug498855-3.ogv^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug498855-3.ogv^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bug499519.ogv b/dom/media/test/bug499519.ogv
new file mode 100644
index 0000000000..62c0922d36
--- /dev/null
+++ b/dom/media/test/bug499519.ogv
Binary files differ
diff --git a/dom/media/test/bug499519.ogv^headers^ b/dom/media/test/bug499519.ogv^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug499519.ogv^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bug500311.ogv b/dom/media/test/bug500311.ogv
new file mode 100644
index 0000000000..2cf27ef1ee
--- /dev/null
+++ b/dom/media/test/bug500311.ogv
Binary files differ
diff --git a/dom/media/test/bug500311.ogv^headers^ b/dom/media/test/bug500311.ogv^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug500311.ogv^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bug501279.ogg b/dom/media/test/bug501279.ogg
new file mode 100644
index 0000000000..e266f07ee8
--- /dev/null
+++ b/dom/media/test/bug501279.ogg
Binary files differ
diff --git a/dom/media/test/bug501279.ogg^headers^ b/dom/media/test/bug501279.ogg^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug501279.ogg^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bug504613.ogv b/dom/media/test/bug504613.ogv
new file mode 100644
index 0000000000..5c7fd015e9
--- /dev/null
+++ b/dom/media/test/bug504613.ogv
Binary files differ
diff --git a/dom/media/test/bug504613.ogv^headers^ b/dom/media/test/bug504613.ogv^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug504613.ogv^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bug504644.ogv b/dom/media/test/bug504644.ogv
new file mode 100644
index 0000000000..46fb4a876b
--- /dev/null
+++ b/dom/media/test/bug504644.ogv
Binary files differ
diff --git a/dom/media/test/bug504644.ogv^headers^ b/dom/media/test/bug504644.ogv^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug504644.ogv^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bug504843.ogv b/dom/media/test/bug504843.ogv
new file mode 100644
index 0000000000..94b4750865
--- /dev/null
+++ b/dom/media/test/bug504843.ogv
Binary files differ
diff --git a/dom/media/test/bug504843.ogv^headers^ b/dom/media/test/bug504843.ogv^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug504843.ogv^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bug506094.ogv b/dom/media/test/bug506094.ogv
new file mode 100644
index 0000000000..142b7b9ad1
--- /dev/null
+++ b/dom/media/test/bug506094.ogv
Binary files differ
diff --git a/dom/media/test/bug506094.ogv^headers^ b/dom/media/test/bug506094.ogv^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug506094.ogv^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bug516323.indexed.ogv b/dom/media/test/bug516323.indexed.ogv
new file mode 100644
index 0000000000..7bd76eeccc
--- /dev/null
+++ b/dom/media/test/bug516323.indexed.ogv
Binary files differ
diff --git a/dom/media/test/bug516323.indexed.ogv^headers^ b/dom/media/test/bug516323.indexed.ogv^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug516323.indexed.ogv^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bug516323.ogv b/dom/media/test/bug516323.ogv
new file mode 100644
index 0000000000..8f2f38b983
--- /dev/null
+++ b/dom/media/test/bug516323.ogv
Binary files differ
diff --git a/dom/media/test/bug516323.ogv^headers^ b/dom/media/test/bug516323.ogv^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug516323.ogv^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bug520493.ogg b/dom/media/test/bug520493.ogg
new file mode 100644
index 0000000000..6eb23198f4
--- /dev/null
+++ b/dom/media/test/bug520493.ogg
Binary files differ
diff --git a/dom/media/test/bug520493.ogg^headers^ b/dom/media/test/bug520493.ogg^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug520493.ogg^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bug520500.ogg b/dom/media/test/bug520500.ogg
new file mode 100644
index 0000000000..b91d3dd97d
--- /dev/null
+++ b/dom/media/test/bug520500.ogg
Binary files differ
diff --git a/dom/media/test/bug520500.ogg^headers^ b/dom/media/test/bug520500.ogg^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug520500.ogg^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bug520908.ogv b/dom/media/test/bug520908.ogv
new file mode 100644
index 0000000000..093158432a
--- /dev/null
+++ b/dom/media/test/bug520908.ogv
Binary files differ
diff --git a/dom/media/test/bug520908.ogv^headers^ b/dom/media/test/bug520908.ogv^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug520908.ogv^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bug523816.ogv b/dom/media/test/bug523816.ogv
new file mode 100644
index 0000000000..ca9a31b6da
--- /dev/null
+++ b/dom/media/test/bug523816.ogv
Binary files differ
diff --git a/dom/media/test/bug523816.ogv^headers^ b/dom/media/test/bug523816.ogv^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug523816.ogv^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bug533822.ogg b/dom/media/test/bug533822.ogg
new file mode 100644
index 0000000000..a8e506910e
--- /dev/null
+++ b/dom/media/test/bug533822.ogg
Binary files differ
diff --git a/dom/media/test/bug533822.ogg^headers^ b/dom/media/test/bug533822.ogg^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug533822.ogg^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bug556821.ogv b/dom/media/test/bug556821.ogv
new file mode 100644
index 0000000000..8d76fee45e
--- /dev/null
+++ b/dom/media/test/bug556821.ogv
Binary files differ
diff --git a/dom/media/test/bug556821.ogv^headers^ b/dom/media/test/bug556821.ogv^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug556821.ogv^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bug557094.ogv b/dom/media/test/bug557094.ogv
new file mode 100644
index 0000000000..b4fc0799a6
--- /dev/null
+++ b/dom/media/test/bug557094.ogv
Binary files differ
diff --git a/dom/media/test/bug557094.ogv^headers^ b/dom/media/test/bug557094.ogv^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug557094.ogv^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bug603918.webm b/dom/media/test/bug603918.webm
new file mode 100644
index 0000000000..c430e97f40
--- /dev/null
+++ b/dom/media/test/bug603918.webm
Binary files differ
diff --git a/dom/media/test/bug603918.webm^headers^ b/dom/media/test/bug603918.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug603918.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bug604067.webm b/dom/media/test/bug604067.webm
new file mode 100644
index 0000000000..86bdfdc91f
--- /dev/null
+++ b/dom/media/test/bug604067.webm
Binary files differ
diff --git a/dom/media/test/bug604067.webm^headers^ b/dom/media/test/bug604067.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/bug604067.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/bunny.webm b/dom/media/test/bunny.webm
new file mode 100644
index 0000000000..823439d1c5
--- /dev/null
+++ b/dom/media/test/bunny.webm
Binary files differ
diff --git a/dom/media/test/bunny_hd_5s.mp4 b/dom/media/test/bunny_hd_5s.mp4
new file mode 100644
index 0000000000..975a159574
--- /dev/null
+++ b/dom/media/test/bunny_hd_5s.mp4
Binary files differ
diff --git a/dom/media/test/can_play_type_dash.js b/dom/media/test/can_play_type_dash.js
new file mode 100644
index 0000000000..b4760545db
--- /dev/null
+++ b/dom/media/test/can_play_type_dash.js
@@ -0,0 +1,27 @@
+function check_dash(v, enabled) {
+ function check(type, expected) {
+ is(v.canPlayType(type), enabled ? expected : "", type);
+ }
+
+ // DASH types
+ check("application/dash+xml", "probably");
+
+ // Supported Webm codecs
+ check("application/dash+xml; codecs=vorbis", "probably");
+ check("application/dash+xml; codecs=vorbis", "probably");
+ check("application/dash+xml; codecs=vorbis,vp8", "probably");
+ check("application/dash+xml; codecs=vorbis,vp8.0", "probably");
+ check('application/dash+xml; codecs="vorbis,vp8"', "probably");
+ check('application/dash+xml; codecs="vorbis,vp8.0"', "probably");
+ check('application/dash+xml; codecs="vp8, vorbis"', "probably");
+ check('application/dash+xml; codecs="vp8.0, vorbis"', "probably");
+ check("application/dash+xml; codecs=vp8", "probably");
+ check("application/dash+xml; codecs=vp8.0", "probably");
+
+ // Unsupported codecs
+ check("application/dash+xml; codecs=xyz", "");
+ check("application/dash+xml; codecs=xyz,vorbis", "");
+ check("application/dash+xml; codecs=vorbis,xyz", "");
+ check("application/dash+xml; codecs=xyz,vp8.0", "");
+ check("application/dash+xml; codecs=vp8.0,xyz", "");
+}
diff --git a/dom/media/test/can_play_type_ogg.js b/dom/media/test/can_play_type_ogg.js
new file mode 100644
index 0000000000..79bf8a554c
--- /dev/null
+++ b/dom/media/test/can_play_type_ogg.js
@@ -0,0 +1,72 @@
+function check_ogg(v, enabled, finish) {
+ function check(type, expected) {
+ is(v.canPlayType(type), enabled ? expected : "", type);
+ }
+
+ function basic_test() {
+ return new Promise(function (resolve, reject) {
+ // Ogg types
+ check("video/ogg", "maybe");
+ check("audio/ogg", "maybe");
+ check("application/ogg", "maybe");
+
+ // Supported Ogg codecs
+ check("audio/ogg; codecs=vorbis", "probably");
+ check("video/ogg; codecs=vorbis", "probably");
+ check("video/ogg; codecs=vorbis,theora", "probably");
+ check('video/ogg; codecs="vorbis, theora"', "probably");
+ check("video/ogg; codecs=theora", "probably");
+
+ resolve();
+ });
+ }
+
+ // Verify Opus support
+ function verify_opus_support() {
+ return new Promise(function (resolve, reject) {
+ var OpusEnabled = SpecialPowers.getBoolPref(
+ "media.opus.enabled",
+ undefined
+ );
+ if (OpusEnabled != undefined) {
+ resolve();
+ } else {
+ console.log(
+ "media.opus.enabled pref not found; skipping Opus validation"
+ );
+ reject();
+ }
+ });
+ }
+
+ function opus_enable() {
+ return SpecialPowers.pushPrefEnv({
+ set: [["media.opus.enabled", true]],
+ }).then(function () {
+ check("audio/ogg; codecs=opus", "probably");
+ });
+ }
+
+ function opus_disable() {
+ return SpecialPowers.pushPrefEnv({
+ set: [["media.opus.enabled", false]],
+ }).then(function () {
+ check("audio/ogg; codecs=opus", "");
+ });
+ }
+
+ function unspported_ogg() {
+ // Unsupported Ogg codecs
+ check("video/ogg; codecs=xyz", "");
+ check("video/ogg; codecs=xyz,vorbis", "");
+ check("video/ogg; codecs=vorbis,xyz", "");
+
+ finish.call();
+ }
+
+ basic_test()
+ .then(verify_opus_support)
+ .then(opus_enable)
+ .then(opus_disable)
+ .then(unspported_ogg, unspported_ogg);
+}
diff --git a/dom/media/test/can_play_type_wave.js b/dom/media/test/can_play_type_wave.js
new file mode 100644
index 0000000000..a5e087aa40
--- /dev/null
+++ b/dom/media/test/can_play_type_wave.js
@@ -0,0 +1,30 @@
+function check_wave(v, enabled) {
+ function check(type, expected) {
+ is(v.canPlayType(type), enabled ? expected : "", type);
+ }
+
+ // Wave types
+ check("audio/wave", "maybe");
+ check("audio/wav", "maybe");
+ check("audio/x-wav", "maybe");
+ check("audio/x-pn-wav", "maybe");
+
+ // Supported Wave codecs
+ check("audio/wave; codecs=1", "probably");
+ check("audio/wave; codecs=3", "probably");
+ check("audio/wave; codecs=6", "probably");
+ check("audio/wave; codecs=7", "probably");
+ // "no codecs" should be supported, I guess
+ check("audio/wave; codecs=", "maybe");
+ check('audio/wave; codecs=""', "maybe");
+
+ // Unsupported Wave codecs
+ check("audio/wave; codecs=0", "");
+ check("audio/wave; codecs=2", "");
+ check("audio/wave; codecs=xyz,1", "");
+ check("audio/wave; codecs=1,xyz", "");
+ check('audio/wave; codecs="xyz, 1"', "");
+ // empty codec names
+ check("audio/wave; codecs=,", "");
+ check('audio/wave; codecs="0, 1,"', "");
+}
diff --git a/dom/media/test/can_play_type_webm.js b/dom/media/test/can_play_type_webm.js
new file mode 100644
index 0000000000..315a8ef3d9
--- /dev/null
+++ b/dom/media/test/can_play_type_webm.js
@@ -0,0 +1,39 @@
+async function check_webm(v, enabled) {
+ function check(type, expected) {
+ is(
+ v.canPlayType(type),
+ enabled ? expected : "",
+ type + "='" + expected + "'"
+ );
+ }
+
+ // WebM types
+ check("video/webm", "maybe");
+ check("audio/webm", "maybe");
+
+ var video = ["vp8", "vp8.0", "vp9", "vp9.0"];
+ var audio = ["vorbis", "opus"];
+
+ audio.forEach(function (acodec) {
+ check("audio/webm; codecs=" + acodec, "probably");
+ check("video/webm; codecs=" + acodec, "probably");
+ });
+ video.forEach(function (vcodec) {
+ check("video/webm; codecs=" + vcodec, "probably");
+ audio.forEach(function (acodec) {
+ check('video/webm; codecs="' + vcodec + ", " + acodec + '"', "probably");
+ check('video/webm; codecs="' + acodec + ", " + vcodec + '"', "probably");
+ });
+ });
+
+ // Unsupported WebM codecs
+ check("video/webm; codecs=xyz", "");
+ check("video/webm; codecs=xyz,vorbis", "");
+ check("video/webm; codecs=vorbis,xyz", "");
+
+ await SpecialPowers.pushPrefEnv({ set: [["media.av1.enabled", true]] });
+ check('video/webm; codecs="av1"', "probably");
+
+ await SpecialPowers.pushPrefEnv({ set: [["media.av1.enabled", false]] });
+ check('video/webm; codecs="av1"', "");
+}
diff --git a/dom/media/test/cancellable_request.sjs b/dom/media/test/cancellable_request.sjs
new file mode 100644
index 0000000000..ffeb0c7818
--- /dev/null
+++ b/dom/media/test/cancellable_request.sjs
@@ -0,0 +1,162 @@
+function parseQuery(request, key) {
+ var params = request.queryString.split("&");
+ for (var j = 0; j < params.length; ++j) {
+ var p = params[j];
+ if (p == key) {
+ return true;
+ }
+ if (p.indexOf(key + "=") == 0) {
+ return p.substring(key.length + 1);
+ }
+ if (!p.includes("=") && key == "") {
+ return p;
+ }
+ }
+ return false;
+}
+
+function push32BE(array, input) {
+ array.push(String.fromCharCode((input >> 24) & 0xff));
+ array.push(String.fromCharCode((input >> 16) & 0xff));
+ array.push(String.fromCharCode((input >> 8) & 0xff));
+ array.push(String.fromCharCode(input & 0xff));
+}
+
+function push32LE(array, input) {
+ array.push(String.fromCharCode(input & 0xff));
+ array.push(String.fromCharCode((input >> 8) & 0xff));
+ array.push(String.fromCharCode((input >> 16) & 0xff));
+ array.push(String.fromCharCode((input >> 24) & 0xff));
+}
+
+function push16LE(array, input) {
+ array.push(String.fromCharCode(input & 0xff));
+ array.push(String.fromCharCode((input >> 8) & 0xff));
+}
+
+function buildWave(samples, sample_rate) {
+ const RIFF_MAGIC = 0x52494646;
+ const WAVE_MAGIC = 0x57415645;
+ const FRMT_MAGIC = 0x666d7420;
+ const DATA_MAGIC = 0x64617461;
+ const RIFF_SIZE = 44;
+
+ var header = [];
+ push32BE(header, RIFF_MAGIC);
+ push32LE(header, RIFF_SIZE + samples.length * 2);
+ push32BE(header, WAVE_MAGIC);
+ push32BE(header, FRMT_MAGIC);
+ push32LE(header, 16);
+ push16LE(header, 1);
+ push16LE(header, 1);
+ push32LE(header, sample_rate);
+ push32LE(header, sample_rate);
+ push16LE(header, 2);
+ push16LE(header, 16);
+ push32BE(header, DATA_MAGIC);
+ push32LE(header, samples.length * 2);
+ for (var i = 0; i < samples.length; ++i) {
+ push16LE(header, samples[i], 2);
+ }
+ return header;
+}
+
+const CC = Components.Constructor;
+const Timer = CC("@mozilla.org/timer;1", "nsITimer", "initWithCallback");
+const BinaryOutputStream = CC(
+ "@mozilla.org/binaryoutputstream;1",
+ "nsIBinaryOutputStream",
+ "setOutputStream"
+);
+
+function poll(f) {
+ if (f()) {
+ return;
+ }
+ new Timer(
+ function () {
+ poll(f);
+ },
+ 100,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+}
+
+function handleRequest(request, response) {
+ var cancel = parseQuery(request, "cancelkey");
+ if (cancel) {
+ setState(cancel[1], "cancelled");
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write("Cancel approved!");
+ return;
+ }
+
+ var samples = [];
+ for (var i = 0; i < 1000000; ++i) {
+ samples.push(0);
+ }
+ var bytes = buildWave(samples, 44100).join("");
+
+ var key = parseQuery(request, "key");
+ response.setHeader("Content-Type", "audio/x-wav");
+ response.setHeader("Content-Length", "" + bytes.length, false);
+
+ var out = new BinaryOutputStream(response.bodyOutputStream);
+
+ var start = 0,
+ end = bytes.length - 1;
+ if (request.hasHeader("Range")) {
+ var rangeMatch = request.getHeader("Range").match(/^bytes=(\d+)?-(\d+)?$/);
+
+ if (rangeMatch[1] !== undefined) {
+ start = parseInt(rangeMatch[1], 10);
+ }
+
+ if (rangeMatch[2] !== undefined) {
+ end = parseInt(rangeMatch[2], 10);
+ }
+
+ // No start given, so the end is really the count of bytes from the
+ // end of the file.
+ if (start === undefined) {
+ start = Math.max(0, bytes.length - end);
+ end = bytes.length - 1;
+ }
+
+ // start and end are inclusive
+ if (end === undefined || end >= bytes.length) {
+ end = bytes.length - 1;
+ }
+
+ if (end < start) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ start = 0;
+ end = bytes.length - 1;
+ } else {
+ response.setStatusLine(request.httpVersion, 206, "Partial Content");
+ var contentRange = "bytes " + start + "-" + end + "/" + bytes.length;
+ response.setHeader("Content-Range", contentRange);
+ }
+ }
+
+ if (start > 0) {
+ // Send all requested data
+ out.write(bytes.slice(start, end + 1), end + 1 - start);
+ return;
+ }
+
+ // Write the first 1.2M of the Wave file. We know the cache size is set to
+ // 100K so this will fill the cache and and cause a "suspend" event on
+ // the loading element.
+ out.write(bytes, 1200000);
+
+ response.processAsync();
+ // Now wait for the message to cancel this response
+ poll(function () {
+ if (getState(key[1]) != "cancelled") {
+ return false;
+ }
+ response.finish();
+ return true;
+ });
+}
diff --git a/dom/media/test/chain.ogg b/dom/media/test/chain.ogg
new file mode 100644
index 0000000000..3535b280f4
--- /dev/null
+++ b/dom/media/test/chain.ogg
Binary files differ
diff --git a/dom/media/test/chain.ogg^headers^ b/dom/media/test/chain.ogg^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/chain.ogg^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/chain.ogv b/dom/media/test/chain.ogv
new file mode 100644
index 0000000000..3e684b64a5
--- /dev/null
+++ b/dom/media/test/chain.ogv
Binary files differ
diff --git a/dom/media/test/chain.ogv^headers^ b/dom/media/test/chain.ogv^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/chain.ogv^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/chain.opus b/dom/media/test/chain.opus
new file mode 100644
index 0000000000..9fa67f94c3
--- /dev/null
+++ b/dom/media/test/chain.opus
Binary files differ
diff --git a/dom/media/test/chain.opus^headers^ b/dom/media/test/chain.opus^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/chain.opus^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/chained-audio-video.ogg b/dom/media/test/chained-audio-video.ogg
new file mode 100644
index 0000000000..adda68bb47
--- /dev/null
+++ b/dom/media/test/chained-audio-video.ogg
Binary files differ
diff --git a/dom/media/test/chained-audio-video.ogg^headers^ b/dom/media/test/chained-audio-video.ogg^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/chained-audio-video.ogg^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/chained-video.ogv b/dom/media/test/chained-video.ogv
new file mode 100644
index 0000000000..a6288ef6c9
--- /dev/null
+++ b/dom/media/test/chained-video.ogv
Binary files differ
diff --git a/dom/media/test/chained-video.ogv^headers^ b/dom/media/test/chained-video.ogv^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/chained-video.ogv^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/chrome/chrome.ini b/dom/media/test/chrome/chrome.ini
new file mode 100644
index 0000000000..c6969d0bc5
--- /dev/null
+++ b/dom/media/test/chrome/chrome.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+subsuite = media
+support-files =
+ ../gizmo.mp4
+ ../gizmo-noaudio.mp4
+ ../TestPatternHDR.mp4
+ ../tone2s-silence4s-tone2s.opus
+
+[test_accumulated_play_time.html]
+[test_telemetry_source_buffer_type.html]
diff --git a/dom/media/test/chrome/test_accumulated_play_time.html b/dom/media/test/chrome/test_accumulated_play_time.html
new file mode 100644
index 0000000000..fd21e71a57
--- /dev/null
+++ b/dom/media/test/chrome/test_accumulated_play_time.html
@@ -0,0 +1,692 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test Video Play Time Related Permanent Telemetry Probes</title>
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script type="application/javascript">
+
+/**
+ * This test is used to ensure that we accumulate time for video playback
+ * correctly, and the results would be used in Telemetry probes.
+ * Currently this test covers following probes
+ * - VIDEO_PLAY_TIME_MS
+ * - VIDEO_HDR_PLAY_TIME_MS
+ * - VIDEO_HIDDEN_PLAY_TIME_MS
+ * - VIDEO_HIDDEN_PLAY_TIME_PERCENTAGE
+ * - VIDEO_INFERRED_DECODE_SUSPEND_PERCENTAGE
+ * - VIDEO_VISIBLE_PLAY_TIME_MS
+ * - MEDIA_PLAY_TIME_MS
+ * - MUTED_PLAY_TIME_PERCENT
+ * - AUDIBLE_PLAY_TIME_PERCENT
+ */
+const videoHistNames = [
+ "VIDEO_PLAY_TIME_MS",
+ "VIDEO_HIDDEN_PLAY_TIME_MS"
+];
+const videoHDRHistNames = [
+ "VIDEO_HDR_PLAY_TIME_MS"
+];
+const videoKeyedHistNames = [
+ "VIDEO_HIDDEN_PLAY_TIME_PERCENTAGE",
+ "VIDEO_INFERRED_DECODE_SUSPEND_PERCENTAGE",
+ "VIDEO_VISIBLE_PLAY_TIME_MS"
+];
+const audioKeyedHistNames = [
+ "MUTED_PLAY_TIME_PERCENT",
+ "AUDIBLE_PLAY_TIME_PERCENT"
+];
+
+add_task(async function setTestPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.testing-only-events", true],
+ ["media.test.video-suspend", true],
+ ["media.suspend-bkgnd-video.enabled", true],
+ ["media.suspend-bkgnd-video.delay-ms", 0],
+ ["dom.media.silence_duration_for_audibility", 0.1]
+ ]});
+});
+
+add_task(async function testTotalPlayTime() {
+ const video = document.createElement('video');
+ video.src = "gizmo.mp4";
+ document.body.appendChild(video);
+
+ info(`all accumulated time should be zero`);
+ const videoChrome = SpecialPowers.wrap(video);
+ await new Promise(r => video.onloadeddata = r);
+ assertValueEqualTo(videoChrome, "totalVideoPlayTime", 0);
+ assertValueEqualTo(videoChrome, "invisiblePlayTime", 0);
+
+ info(`start accumulating play time after media starts`);
+ video.autoplay = true;
+ await Promise.all([
+ once(video, "playing"),
+ once(video, "moztotalplaytimestarted"),
+ ]);
+ await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
+ assertValueKeptUnchanged(videoChrome, "invisiblePlayTime");
+ assertValueKeptUnchanged(videoChrome, "videoDecodeSuspendedTime");
+
+ info(`should not accumulate time for paused video`);
+ video.pause();
+ await once(video, "moztotalplaytimepaused");
+ assertValueKeptUnchanged(videoChrome, "totalVideoPlayTime");
+ assertValueEqualTo(videoChrome, "totalVideoPlayTime", 0);
+
+ info(`should start accumulating time again`);
+ let rv = await Promise.all([
+ onceWithTrueReturn(video, "moztotalplaytimestarted"),
+ video.play().then(_ => true, _ => false),
+ ]);
+ ok(returnTrueWhenAllValuesAreTrue(rv), "video started again");
+ await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
+ await cleanUpMediaAndCheckTelemetry(video);
+});
+
+// The testHDRPlayTime task will only pass on platforms that accurately report
+// color depth in their VideoInfo structures. Presently, that is only true for
+// macOS.
+const {AppConstants} = ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+const reportsColorDepthFromVideoData = (AppConstants.platform == "macosx");
+if (reportsColorDepthFromVideoData) {
+ add_task(async function testHDRPlayTime() {
+ // This task is different from the others because the HTMLMediaElement does
+ // not expose a chrome property for video hdr play time. But we do capture
+ // telemety for VIDEO_HDR_PLAY_TIME_MS. To ensure that this telemetry is
+ // generated, this task follows the same structure as the other tasks, but
+ // doesn't actually check the properties of the video player, other than to
+ // confirm that video has played for at least some time.
+ const video = document.createElement('video');
+ video.src = "TestPatternHDR.mp4"; // This is an HDR video with no audio.
+ document.body.appendChild(video);
+
+ info(`load the HDR video`);
+ const videoChrome = SpecialPowers.wrap(video);
+ await new Promise(r => video.onloadeddata = r);
+
+ info(`start accumulating play time after media starts`);
+ video.autoplay = true;
+ await Promise.all([
+ once(video, "playing"),
+ once(video, "moztotalplaytimestarted"),
+ ]);
+ // Check that we have at least some video play time, because the
+ // HDR play time telemetry is emitted by the same process.
+ await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
+ await cleanUpMediaAndCheckTelemetry(video, {hasVideo: true, hasAudio: false, hasVideoHDR: true});
+ });
+}
+
+add_task(async function testVisiblePlayTime() {
+ const video = document.createElement('video');
+ video.src = "gizmo.mp4";
+ document.body.appendChild(video);
+
+ info(`all accumulated time should be zero`);
+ const videoChrome = SpecialPowers.wrap(video);
+ await new Promise(r => video.onloadeddata = r);
+ assertValueEqualTo(videoChrome, "totalVideoPlayTime", 0);
+ assertValueEqualTo(videoChrome, "visiblePlayTime", 0);
+ assertValueEqualTo(videoChrome, "invisiblePlayTime", 0);
+
+ info(`start accumulating play time after media starts`);
+ video.autoplay = true;
+ await Promise.all([
+ once(video, "playing"),
+ once(video, "moztotalplaytimestarted"),
+ ]);
+ await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
+ await assertValueConstantlyIncreases(videoChrome, "visiblePlayTime");
+ assertValueKeptUnchanged(videoChrome, "invisiblePlayTime");
+
+ info(`make video invisible`);
+ video.style.display = "none";
+ await once(video, "mozinvisibleplaytimestarted");
+ await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
+ await assertValueConstantlyIncreases(videoChrome, "invisiblePlayTime");
+ assertValueKeptUnchanged(videoChrome, "visiblePlayTime");
+ await cleanUpMediaAndCheckTelemetry(video);
+});
+
+add_task(async function testAudibleAudioPlayTime() {
+ const audio = document.createElement('audio');
+ audio.src = "tone2s-silence4s-tone2s.opus";
+ audio.controls = true;
+ audio.loop = true;
+ document.body.appendChild(audio);
+
+ info(`all accumulated time should be zero`);
+ const audioChrome = SpecialPowers.wrap(audio);
+ await new Promise(r => audio.onloadeddata = r);
+ assertValueEqualTo(audioChrome, "totalVideoPlayTime", 0);
+ assertValueEqualTo(audioChrome, "totalAudioPlayTime", 0);
+ assertValueEqualTo(audioChrome, "mutedPlayTime", 0);
+ assertValueEqualTo(audioChrome, "audiblePlayTime", 0);
+
+ info(`start accumulating play time after media starts`);
+ await Promise.all([
+ audio.play(),
+ once(audio, "moztotalplaytimestarted"),
+ ]);
+ await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
+ await assertValueConstantlyIncreases(audioChrome, "audiblePlayTime");
+ assertValueKeptUnchanged(audioChrome, "mutedPlayTime");
+ assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
+
+ info(`audio becomes inaudible for 4s`);
+ await once(audio, "mozinaudibleaudioplaytimestarted");
+
+ await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
+ assertValueKeptUnchanged(audioChrome, "audiblePlayTime");
+ assertValueKeptUnchanged(audioChrome, "mutedPlayTime");
+ assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
+
+ info(`audio becomes audible after 4s`);
+ await once(audio, "mozinaudibleaudioplaytimepaused");
+
+ await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
+ await assertValueConstantlyIncreases(audioChrome, "audiblePlayTime");
+ assertValueKeptUnchanged(audioChrome, "mutedPlayTime");
+ assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
+
+ await cleanUpMediaAndCheckTelemetry(audio, {hasVideo: false});
+});
+
+add_task(async function testHiddenPlayTime() {
+ const invisibleReasons = ["notInTree", "notInConnectedTree", "invisibleInDisplay"];
+ for (let reason of invisibleReasons) {
+ const video = document.createElement('video');
+ video.src = "gizmo.mp4";
+ video.loop = true;
+ info(`invisible video due to '${reason}'`);
+
+ if (reason == "notInConnectedTree") {
+ let disconnected = document.createElement("div")
+ disconnected.appendChild(video);
+ } else if (reason == "invisibleInDisplay") {
+ document.body.appendChild(video);
+ video.style.display = "none";
+ } else if (reason == "notInTree") {
+ // video is already created in the `notInTree` situation.
+ } else {
+ ok(false, "undefined reason");
+ }
+
+ info(`start invisible video should start accumulating timers`);
+ const videoChrome = SpecialPowers.wrap(video);
+ let rv = await Promise.all([
+ onceWithTrueReturn(video, "mozinvisibleplaytimestarted"),
+ video.play().then(_ => true, _ => false),
+ ]);
+ ok(returnTrueWhenAllValuesAreTrue(rv), "video started playing");
+ await assertValueConstantlyIncreases(videoChrome, "invisiblePlayTime");
+
+ info(`should not accumulate time for paused video`);
+ video.pause();
+ await once(video, "mozinvisibleplaytimepaused");
+ assertValueKeptUnchanged(videoChrome, "invisiblePlayTime");
+
+ info(`should start accumulating time again`);
+ rv = await Promise.all([
+ onceWithTrueReturn(video, "mozinvisibleplaytimestarted"),
+ video.play().then(_ => true, _ => false),
+ ]);
+ ok(returnTrueWhenAllValuesAreTrue(rv), "video started again");
+ await assertValueConstantlyIncreases(videoChrome, "invisiblePlayTime");
+
+ info(`make video visible should stop accumulating invisible related time`);
+ if (reason == "notInTree" || reason == "notInConnectedTree") {
+ document.body.appendChild(video);
+ } else if (reason == "invisibleInDisplay") {
+ video.style.display = "block";
+ } else {
+ ok(false, "undefined reason");
+ }
+ await once(video, "mozinvisibleplaytimepaused");
+ assertValueKeptUnchanged(videoChrome, "invisiblePlayTime");
+ await cleanUpMediaAndCheckTelemetry(video);
+ }
+});
+
+add_task(async function testAudioProbesWithoutAudio() {
+ const video = document.createElement('video');
+ video.src = "gizmo-noaudio.mp4";
+ video.loop = true;
+ document.body.appendChild(video);
+
+ info(`all accumulated time should be zero`);
+ const videoChrome = SpecialPowers.wrap(video);
+ await new Promise(r => video.onloadeddata = r);
+ assertValueEqualTo(videoChrome, "totalVideoPlayTime", 0);
+ assertValueEqualTo(videoChrome, "totalAudioPlayTime", 0);
+ assertValueEqualTo(videoChrome, "mutedPlayTime", 0);
+ assertValueEqualTo(videoChrome, "audiblePlayTime", 0);
+
+ info(`start accumulating play time after media starts`);
+ await Promise.all([
+ video.play(),
+ once(video, "moztotalplaytimestarted"),
+ ]);
+
+ async function checkInvariants() {
+ await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
+ assertValueKeptUnchanged(videoChrome, "audiblePlayTime");
+ assertValueKeptUnchanged(videoChrome, "mutedPlayTime");
+ assertValueKeptUnchanged(videoChrome, "totalAudioPlayTime");
+ }
+
+ checkInvariants();
+
+ video.muted = true;
+
+ checkInvariants();
+
+ video.currentTime = 0.0;
+ await once(video, "seeked");
+
+ checkInvariants();
+
+ video.muted = false;
+
+ checkInvariants();
+
+ video.volume = 0.0;
+
+ checkInvariants();
+
+ video.volume = 1.0;
+
+ checkInvariants();
+
+ video.muted = true;
+
+ checkInvariants();
+
+ video.currentTime = 0.0;
+
+ checkInvariants();
+
+ await cleanUpMediaAndCheckTelemetry(video, {hasAudio: false});
+});
+
+add_task(async function testMutedAudioPlayTime() {
+ const audio = document.createElement('audio');
+ audio.src = "gizmo.mp4";
+ audio.controls = true;
+ audio.loop = true;
+ document.body.appendChild(audio);
+
+ info(`all accumulated time should be zero`);
+ const audioChrome = SpecialPowers.wrap(audio);
+ await new Promise(r => audio.onloadeddata = r);
+ assertValueEqualTo(audioChrome, "totalVideoPlayTime", 0);
+ assertValueEqualTo(audioChrome, "totalAudioPlayTime", 0);
+ assertValueEqualTo(audioChrome, "mutedPlayTime", 0);
+ assertValueEqualTo(audioChrome, "audiblePlayTime", 0);
+
+ info(`start accumulating play time after media starts`);
+ await Promise.all([
+ audio.play(),
+ once(audio, "moztotalplaytimestarted"),
+ ]);
+ await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
+ await assertValueConstantlyIncreases(audioChrome, "audiblePlayTime");
+ assertValueKeptUnchanged(audioChrome, "mutedPlayTime");
+ assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
+
+ audio.muted = true;
+ await once(audio, "mozmutedaudioplaytimestarted");
+
+ await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
+ await assertValueConstantlyIncreases(audioChrome, "mutedPlayTime");
+ await assertValueConstantlyIncreases(audioChrome, "audiblePlayTime");
+ assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
+
+ audio.currentTime = 0.0;
+ await once(audio, "seeked");
+
+ await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
+ await assertValueConstantlyIncreases(audioChrome, "mutedPlayTime");
+ await assertValueConstantlyIncreases(audioChrome, "audiblePlayTime");
+ assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
+
+ audio.muted = false;
+ await once(audio, "mozmutedeaudioplaytimepaused");
+
+ await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
+ await assertValueConstantlyIncreases(audioChrome, "audiblePlayTime");
+ assertValueKeptUnchanged(audioChrome, "mutedPlayTime");
+ assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
+
+ audio.volume = 0.0;
+ await once(audio, "mozmutedaudioplaytimestarted");
+
+ await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
+ await assertValueConstantlyIncreases(audioChrome, "mutedPlayTime");
+ await assertValueConstantlyIncreases(audioChrome, "audiblePlayTime");
+ assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
+
+ audio.volume = 1.0;
+ await once(audio, "mozmutedeaudioplaytimepaused");
+
+ await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
+ await assertValueConstantlyIncreases(audioChrome, "audiblePlayTime");
+ assertValueKeptUnchanged(audioChrome, "mutedPlayTime");
+ assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
+
+ audio.muted = true;
+ await once(audio, "mozmutedaudioplaytimestarted");
+
+ await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
+ await assertValueConstantlyIncreases(audioChrome, "mutedPlayTime");
+ await assertValueConstantlyIncreases(audioChrome, "audiblePlayTime");
+ assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
+
+ audio.currentTime = 0.0;
+
+ await assertValueConstantlyIncreases(audioChrome, "totalAudioPlayTime");
+ await assertValueConstantlyIncreases(audioChrome, "mutedPlayTime");
+ await assertValueConstantlyIncreases(audioChrome, "audiblePlayTime");
+ assertValueKeptUnchanged(audioChrome, "totalVideoPlayTime");
+
+ // The media has a video track, but it's being played back in an
+ // HTMLAudioElement, without video frame location.
+ await cleanUpMediaAndCheckTelemetry(audio, {hasVideo: false});
+});
+
+// Note that video suspended time is not always align with the invisible play
+// time even if `media.suspend-bkgnd-video.delay-ms` is `0`, because not all
+// invisible videos would be suspended under current strategy.
+add_task(async function testDecodeSuspendedTime() {
+ const video = document.createElement('video');
+ video.src = "gizmo.mp4";
+ video.loop = true;
+ document.body.appendChild(video);
+
+ info(`start video should start accumulating timers`);
+ const videoChrome = SpecialPowers.wrap(video);
+ let rv = await Promise.all([
+ onceWithTrueReturn(video, "moztotalplaytimestarted"),
+ video.play().then(_ => true, _ => false),
+ ]);
+ ok(returnTrueWhenAllValuesAreTrue(rv), "video started playing");
+ await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
+ assertValueKeptUnchanged(videoChrome, "invisiblePlayTime");
+ assertValueKeptUnchanged(videoChrome, "videoDecodeSuspendedTime");
+
+ info(`make it invisible and force to suspend decoding`);
+ video.setVisible(false);
+ await once(video, "mozvideodecodesuspendedstarted");
+ await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
+ await assertValueConstantlyIncreases(videoChrome, "invisiblePlayTime");
+ await assertValueConstantlyIncreases(videoChrome, "videoDecodeSuspendedTime");
+
+ info(`make it visible and resume decoding`);
+ video.setVisible(true);
+ await Promise.all([
+ once(video, "mozinvisibleplaytimepaused"),
+ once(video, "mozvideodecodesuspendedpaused"),
+ ]);
+ await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
+ assertValueKeptUnchanged(videoChrome, "invisiblePlayTime");
+ assertValueKeptUnchanged(videoChrome, "videoDecodeSuspendedTime");
+ await cleanUpMediaAndCheckTelemetry(video);
+});
+
+add_task(async function reuseSameElementForPlayback() {
+ const video = document.createElement('video');
+ video.src = "gizmo.mp4";
+ document.body.appendChild(video);
+
+ info(`start accumulating play time after media starts`);
+ const videoChrome = SpecialPowers.wrap(video);
+ let rv = await Promise.all([
+ onceWithTrueReturn(video, "moztotalplaytimestarted"),
+ video.play().then(_ => true, _ => false),
+ ]);
+ ok(returnTrueWhenAllValuesAreTrue(rv), "video started again");
+ await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
+
+ info(`reset its src and all accumulated value should be reset after then`);
+ // After setting its src to nothing, that would trigger a failed load and set
+ // the error. If the following step tries to set the new resource and `play()`
+ // , then they should be done after receving the `error` from that failed load
+ // first.
+ await Promise.all([
+ once(video, "error"),
+ cleanUpMediaAndCheckTelemetry(video),
+ ]);
+ // video doesn't have a decoder, so the return value would be -1 (error).
+ assertValueEqualTo(videoChrome, "totalVideoPlayTime", -1);
+ assertValueEqualTo(videoChrome, "invisiblePlayTime", -1);
+
+ info(`resue same element, make it visible and start playback again`);
+ video.src = "gizmo.mp4";
+ rv = await Promise.all([
+ onceWithTrueReturn(video, "moztotalplaytimestarted"),
+ video.play().then(_ => true, _ => false),
+ ]);
+ ok(returnTrueWhenAllValuesAreTrue(rv), "video started");
+ await assertValueConstantlyIncreases(videoChrome, "totalVideoPlayTime");
+ await cleanUpMediaAndCheckTelemetry(video);
+});
+
+add_task(async function testNoReportedTelemetryResult() {
+ info(`No result for empty video`);
+ const video = document.createElement('video');
+ assertAllProbeRelatedAttributesKeptUnchanged(video);
+ await assertNoReportedTelemetryResult(video);
+
+ info(`No result for video which hasn't started playing`);
+ video.src = "gizmo.mp4";
+ document.body.appendChild(video);
+ ok(await once(video, "loadeddata").then(_ => true), "video loaded data");
+ assertAllProbeRelatedAttributesKeptUnchanged(video);
+ await assertNoReportedTelemetryResult(video);
+
+ info(`No result for video with error`);
+ video.src = "filedoesnotexist.mp4";
+ ok(await video.play().then(_ => false, _ => true), "video failed to play");
+ ok(video.error != undefined, "video got error");
+ assertAllProbeRelatedAttributesKeptUnchanged(video);
+ await assertNoReportedTelemetryResult(video);
+});
+
+/**
+ * Following are helper functions
+ */
+async function cleanUpMediaAndCheckTelemetry(media, { reportExpected = true, hasVideo = true, hasAudio = true, hasVideoHDR = false } = {}) {
+ media.src = "";
+ await checkReportedTelemetry(media, reportExpected, hasVideo, hasAudio, hasVideoHDR);
+}
+
+async function assertNoReportedTelemetryResult(media) {
+ await checkReportedTelemetry(media, false, true, true);
+}
+
+async function checkReportedTelemetry(media, reportExpected, hasVideo, hasAudio, hasVideoHDR) {
+ const reportResultPromise = once(media, "mozreportedtelemetry");
+ info(`check telemetry result, reportExpected=${reportExpected}`);
+ if (reportExpected) {
+ await reportResultPromise;
+ }
+ for (const name of videoHistNames) {
+ try {
+ const hist = SpecialPowers.Services.telemetry.getHistogramById(name);
+ /**
+ * Histogram's snapshot looks like that
+ * {
+ * "bucket_count": X,
+ * "histogram_type": Y,
+ * "sum": Z,
+ * "range": [min, max],
+ * "values": { "value1" : "num1", "value2" : "num2", ...}
+ * }
+ */
+ const entriesNums = Object.entries(hist.snapshot().values).length;
+ if (reportExpected && hasVideo) {
+ ok(entriesNums > 0, `Reported result for ${name}`);
+ } else {
+ ok(entriesNums == 0, `Reported nothing for ${name}`);
+ }
+ hist.clear();
+ } catch (e) {
+ ok(false , `histogram '${name}' doesn't exist`);
+ }
+ }
+ // videoHDRHistNames are checked for total time, not for number of samples.
+ for (const name of videoHDRHistNames) {
+ try {
+ const hist = SpecialPowers.Services.telemetry.getHistogramById(name);
+ const totalTimeMS = hist.snapshot().sum;
+ if (reportExpected && hasVideoHDR) {
+ ok(totalTimeMS > 0, `Reported some time for ${name}`);
+ } else {
+ ok(totalTimeMS == 0, `Reported no time for ${name}`);
+ }
+ hist.clear();
+ } catch (e) {
+ ok(false , `histogram '${name}' doesn't exist`);
+ }
+ }
+ for (const name of videoKeyedHistNames) {
+ try {
+ const hist = SpecialPowers.Services.telemetry.getKeyedHistogramById(name);
+ /**
+ * Keyed Histogram's snapshot looks like that
+ * {
+ * "Key1" : {
+ * "bucket_count": X,
+ * "histogram_type": Y,
+ * "sum": Z,
+ * "range": [min, max],
+ * "values": { "value1" : "num1", "value2" : "num2", ...}
+ * },
+ * "Key2" : {...},
+ * }
+ */
+ const items = Object.entries(hist.snapshot());
+ if (items.length) {
+ for (const [key, value] of items) {
+ const entriesNums = Object.entries(value.values).length;
+ ok(reportExpected && entriesNums > 0, `Reported ${key} for ${name}`);
+ }
+ } else if (reportExpected) {
+ ok(!hasVideo, `No video telemetry reported but no video track in the media`);
+ } else {
+ ok(true, `No video telemetry expected, none reported`);
+ }
+ // Avoid to pollute next test task.
+ hist.clear();
+ } catch (e) {
+ ok(false , `keyed histogram '${name}' doesn't exist`);
+ }
+ }
+
+ // In any case, the combined probe MEDIA_PLAY_TIME_MS should be reported, if
+ // expected
+ {
+ const hist =
+ SpecialPowers.Services.telemetry.getKeyedHistogramById("MEDIA_PLAY_TIME_MS");
+ const items = Object.entries(hist.snapshot());
+ if (items.length) {
+ for (const item of items) {
+ ok(item[0].includes("V") != -1 || !hasVideo, "Video time is reported if video was present");
+ }
+ hist.clear();
+ } else {
+ ok(!reportExpected, "MEDIA_PLAY_TIME_MS should always be reported if a report is expected");
+ }
+ }
+
+ for (const name of audioKeyedHistNames) {
+ try {
+ const hist = SpecialPowers.Services.telemetry.getKeyedHistogramById(name);
+ const items = Object.entries(hist.snapshot());
+ if (items.length) {
+ for (const [key, value] of items) {
+ const entriesNums = Object.entries(value.values).length;
+ ok(reportExpected && entriesNums > 0, `Reported ${key} for ${name}`);
+ }
+ } else {
+ ok(!reportExpected || !hasAudio, `No audio telemetry expected, none reported`);
+ }
+ // Avoid to pollute next test task.
+ hist.clear();
+ } catch (e) {
+ ok(false , `keyed histogram '${name}' doesn't exist`);
+ }
+ }
+}
+
+function once(target, name) {
+ return new Promise(r => target.addEventListener(name, r, { once: true }));
+}
+
+function onceWithTrueReturn(target, name) {
+ return once(target, name).then(_ => true);
+}
+
+function returnTrueWhenAllValuesAreTrue(arr) {
+ for (let val of arr) {
+ if (!val) {
+ return false;
+ }
+ }
+ return true;
+}
+
+// Block the main thread for a number of milliseconds
+function blockMainThread(durationMS) {
+ const start = Date.now();
+ while (Date.now() - start < durationMS) { /* spin */ }
+}
+
+// Allows comparing two values from the system clocks that are not gathered
+// atomically. Allow up to 1ms of fuzzing when lhs and rhs are seconds.
+function timeFuzzyEquals(lhs, rhs, str) {
+ ok(Math.abs(lhs - rhs) < 1e-3, str);
+}
+
+function assertAttributeDefined(mediaChrome, checkType) {
+ ok(mediaChrome[checkType] != undefined, `${checkType} exists`);
+}
+
+function assertValueEqualTo(mediaChrome, checkType, expectedValue) {
+ assertAttributeDefined(mediaChrome, checkType);
+ is(mediaChrome[checkType], expectedValue, `${checkType} equals to ${expectedValue}`);
+}
+
+async function assertValueConstantlyIncreases(mediaChrome, checkType) {
+ assertAttributeDefined(mediaChrome, checkType);
+ const valueSnapshot = mediaChrome[checkType];
+ // 30ms is long enough to have a low-resolution system clock tick, but short
+ // enough to not slow the test down.
+ blockMainThread(30);
+ const current = mediaChrome[checkType];
+ ok(current > valueSnapshot, `${checkType} keeps increasing (${current} > ${valueSnapshot})`);
+}
+
+function assertValueKeptUnchanged(mediaChrome, checkType) {
+ assertAttributeDefined(mediaChrome, checkType);
+ const valueSnapshot = mediaChrome[checkType];
+ // 30ms is long enough to have a low-resolution system clock tick, but short
+ // enough to not slow the test down.
+ blockMainThread(30);
+ const newValue = mediaChrome[checkType];
+ timeFuzzyEquals(newValue, valueSnapshot, `${checkType} keeps unchanged (${newValue} vs. ${valueSnapshot})`);
+}
+
+function assertAllProbeRelatedAttributesKeptUnchanged(video) {
+ const videoChrome = SpecialPowers.wrap(video);
+ assertValueKeptUnchanged(videoChrome, "totalVideoPlayTime");
+ assertValueKeptUnchanged(videoChrome, "invisiblePlayTime");
+ assertValueKeptUnchanged(videoChrome, "videoDecodeSuspendedTime");
+}
+
+</script>
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/media/test/chrome/test_telemetry_source_buffer_type.html b/dom/media/test/chrome/test_telemetry_source_buffer_type.html
new file mode 100644
index 0000000000..695835a9b8
--- /dev/null
+++ b/dom/media/test/chrome/test_telemetry_source_buffer_type.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Telemetry : test permanent probe MSE_SOURCE_BUFFER_TYPE</title>
+<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script type="application/javascript">
+
+/**
+ * This test is used for the permanent Telemetry probe MSE_SOURCE_BUFFER_TYPE.
+ * which is used to measure the usage of mime type used in MSE.
+ */
+const VIDEO_WEBM = "video/webm";
+const AUDIO_WEBM = "audio/webm";
+const VIDEO_MP4 = "video/mp4";
+const AUDIO_MP4 = "audio/mp4";
+const VIDEO_MP2T = "video/mp2t";
+const AUDIO_MP2T = "audio/mp2t";
+const AUDIO_MP3 = "audio/mpeg";
+const AUDIO_AAC = "audio/aac";
+
+// The order follows `MSE_SOURCE_BUFFER_TYPE` in `Histogram.json`.
+const gLabelNames = [VIDEO_WEBM, AUDIO_WEBM, VIDEO_MP4, AUDIO_MP4,
+ VIDEO_MP2T, AUDIO_MP2T, AUDIO_MP3, AUDIO_AAC];
+
+add_task(async function setTestPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.testing-only-events", true]]});
+});
+
+add_task(async function testSourceBufferTypeProbe() {
+ for (let label of gLabelNames) {
+ info(`calling 'isTypeSupported()' should increase the probe value`);
+ MediaSource.isTypeSupported(label);
+ assertLabelValueEqualTo(label, 1);
+
+ info(`calling 'addSourceBuffer()' should increase the probe value`);
+ try {
+ const source = new MediaSource();
+ source.addSourceBuffer(label);
+ } catch (e) {
+ info(`ignore error for unsupported type ${label}`);
+ }
+ assertLabelValueEqualTo(label, 2);
+ }
+});
+
+/**
+ * Following are helper functions
+ */
+async function assertLabelValueEqualTo(labelName, value) {
+ const hist = SpecialPowers.Services.telemetry.getHistogramById("MSE_SOURCE_BUFFER_TYPE");
+ /**
+ * Histogram's snapshot looks like that
+ * {
+ * "bucket_count": X,
+ * "histogram_type": Y,
+ * "sum": Z,
+ * "range": [min, max],
+ * "values": { "value1" : "num1", "value2" : "num2", ...}
+ * }
+ */
+ if (!gLabelNames.includes(labelName)) {
+ ok(false, `undefined label name=${labelName}`);
+ return;
+ }
+ const labelIdx = gLabelNames.indexOf(labelName);
+ is(hist.snapshot().values[labelIdx], value, `${labelName} equal to ${value}`);
+}
+
+</script>
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/media/test/chromeHelper.js b/dom/media/test/chromeHelper.js
new file mode 100644
index 0000000000..a89d12d6dd
--- /dev/null
+++ b/dom/media/test/chromeHelper.js
@@ -0,0 +1,23 @@
+/* -*- Mode: javascript; indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint-env mozilla/chrome-script */
+
+"use strict";
+
+// eslint-disable-next-line mozilla/use-services
+const dirSvc = Cc["@mozilla.org/file/directory_service;1"].getService(
+ Ci.nsIProperties
+);
+
+addMessageListener("media-test:getcwd", () => {
+ let cwd;
+ try {
+ cwd = dirSvc.get("CurWorkD", Ci.nsIFile).path;
+ } finally {
+ sendAsyncMessage("media-test:cwd", cwd);
+ }
+});
diff --git a/dom/media/test/cloneElementVisually_helpers.js b/dom/media/test/cloneElementVisually_helpers.js
new file mode 100644
index 0000000000..fa9ae49a56
--- /dev/null
+++ b/dom/media/test/cloneElementVisually_helpers.js
@@ -0,0 +1,232 @@
+const TEST_VIDEO_1 =
+ "http://mochi.test:8888/tests/dom/media/test/bipbop_225w_175kbps.mp4";
+const TEST_VIDEO_2 =
+ "http://mochi.test:8888/tests/dom/media/test/pixel_aspect_ratio.mp4";
+const LONG_VIDEO = "http://mochi.test:8888/tests/dom/media/test/gizmo.mp4";
+
+/**
+ * Ensure that the original <video> is prepped and ready to play
+ * before starting any other tests.
+ */
+async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.test.video-suspend", true],
+ ["media.suspend-bkgnd-video.enabled", true],
+ ["media.suspend-bkgnd-video.delay-ms", 500],
+ ["media.dormant-on-pause-timeout-ms", 0],
+ ["media.cloneElementVisually.testing", true],
+ ],
+ });
+
+ let originalVideo = document.getElementById("original");
+ await setVideoSrc(originalVideo, TEST_VIDEO_1);
+}
+
+/**
+ * Given a canvas, as well as a width and height of something to be
+ * blitted onto that canvas, makes any necessary adjustments to the
+ * canvas to work with the current display resolution.
+ *
+ * @param canvas (<canvas> element)
+ * The canvas to be adjusted.
+ * @param width (int)
+ * The width of the image to be blitted.
+ * @param height (int)
+ * The height of the image to be blitted.
+ * @return CanvasRenderingContext2D (SpecialPowers wrapper)
+ */
+function getWrappedScaledCanvasContext(canvas, width, height) {
+ let devRatio = window.devicePixelRatio || 1;
+ let ctx = SpecialPowers.wrap(canvas.getContext("2d"));
+ let backingRatio = ctx.webkitBackingStorePixelRatio || 1;
+
+ let ratio = 1;
+ ratio = devRatio / backingRatio;
+ canvas.width = ratio * width;
+ canvas.height = ratio * height;
+ canvas.style.width = width + "px";
+ canvas.style.height = height + "px";
+ ctx.scale(ratio, ratio);
+
+ return ctx;
+}
+
+/**
+ * Given a <video> element in the DOM, figures out its location, and captures
+ * a snapshot of what it's currently presenting.
+ *
+ * @param video (<video> element)
+ * @return ImageData (SpecialPowers wrapper)
+ */
+function captureFrameImageData(video) {
+ // Flush layout first, just in case the decoder has recently
+ // updated the dimensions of the video.
+ let rect = video.getBoundingClientRect();
+
+ let width = video.videoWidth;
+ let height = video.videoHeight;
+
+ let canvas = document.createElement("canvas");
+ let ctx = getWrappedScaledCanvasContext(canvas, width, height);
+ ctx.drawWindow(window, rect.left, rect.top, width, height, "rgb(0,0,0)");
+
+ return ctx.getImageData(0, 0, width, height);
+}
+
+/**
+ * Given two <video> elements, captures snapshots of what they're currently
+ * displaying, and asserts that they're identical.
+ *
+ * @param video1 (<video> element)
+ * A video element to compare against.
+ * @param video2 (<video> element)
+ * A video to compare with video1.
+ * @return Promise
+ * @resolves
+ * Resolves as true if the two videos match.
+ */
+async function assertVideosMatch(video1, video2) {
+ let video1Frame = captureFrameImageData(video1);
+ let video2Frame = captureFrameImageData(video2);
+
+ let left = document.getElementById("left");
+ let leftCtx = getWrappedScaledCanvasContext(
+ left,
+ video1Frame.width,
+ video1Frame.height
+ );
+ leftCtx.putImageData(video1Frame, 0, 0);
+
+ let right = document.getElementById("right");
+ let rightCtx = getWrappedScaledCanvasContext(
+ right,
+ video2Frame.width,
+ video2Frame.height
+ );
+ rightCtx.putImageData(video2Frame, 0, 0);
+
+ if (video1Frame.data.length != video2Frame.data.length) {
+ return false;
+ }
+
+ let leftDataURL = left.toDataURL();
+ let rightDataURL = right.toDataURL();
+
+ if (leftDataURL != rightDataURL) {
+ dump("Left frame: " + leftDataURL + "\n\n");
+ dump("Right frame: " + rightDataURL + "\n\n");
+
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Testing helper function that constructs a node clone of a video,
+ * injects it into the DOM, and then runs an async testing function.
+ * This also does the work of removing the clone before resolving.
+ *
+ * @param video (<video> element)
+ * The video to clone the node from.
+ * @param asyncFn (async function)
+ * A test function that will be passed the new clone as its
+ * only argument.
+ * @return Promise
+ * @resolves
+ * When the asyncFn resolves and the clone has been removed
+ * from the DOM.
+ */
+async function withNewClone(video, asyncFn) {
+ let clone = video.cloneNode();
+ clone.id = "clone";
+ clone.src = "";
+ let content = document.getElementById("content");
+ content.appendChild(clone);
+
+ try {
+ await asyncFn(clone);
+ } finally {
+ clone.remove();
+ }
+}
+
+/**
+ * Sets the src on a video and waits until its ready.
+ *
+ * @param video (<video> element)
+ * The video to set the src on.
+ * @param src (string)
+ * The URL to set as the source on a video.
+ * @return Promise
+ * @resolves
+ * When the video fires the "canplay" event.
+ */
+async function setVideoSrc(video, src) {
+ let promiseReady = waitForEventOnce(video, "canplay");
+ video.src = src;
+ await promiseReady;
+}
+
+/**
+ * Returns a Promise once target emits a particular event
+ * once.
+ *
+ * @param target (DOM node)
+ * The target to monitor for the event.
+ * @param event (string)
+ * The name of the event to wait for.
+ * @return Promise
+ * @resolves
+ * When the event fires, and resolves to the event.
+ */
+function waitForEventOnce(target, event) {
+ return new Promise(resolve => {
+ target.addEventListener(event, resolve, { once: true });
+ });
+}
+
+/**
+ * Polls the video debug data as a hacky way of knowing when
+ * when the decoders have shut down.
+ *
+ * @param video (<video> element)
+ * @return Promise
+ * @resolves
+ * When the decoder has shut down.
+ */
+function waitForShutdownDecoder(video) {
+ return SimpleTest.promiseWaitForCondition(async () => {
+ let readerData = await SpecialPowers.wrap(video).mozRequestDebugInfo();
+ return readerData.decoder.reader.audioDecoderName == "shutdown";
+ }, "Video decoder should eventually shut down.");
+}
+
+/**
+ * Ensures that both hiding and pausing the video causes the
+ * video to suspend and make dormant its decoders, respectively.
+ *
+ * @param video (<video element)
+ */
+async function ensureVideoSuspendable(video) {
+ video = SpecialPowers.wrap(video);
+
+ ok(!video.hasSuspendTaint(), "Should be suspendable");
+
+ // First, we'll simulate putting the video in the background by
+ // making it invisible.
+ let suspendPromise = waitForEventOnce(video, "mozentervideosuspend");
+ video.setVisible(false);
+ await suspendPromise;
+ ok(true, "Suspended after the video was made invisible.");
+ video.setVisible(true);
+
+ ok(!video.hasSuspendTaint(), "Should still be suspendable.");
+
+ // Next, we'll pause the video.
+ await video.pause();
+ await waitForShutdownDecoder(video);
+ ok(true, "Shutdown decoder after the video was paused.");
+ await video.play();
+}
diff --git a/dom/media/test/contentType.sjs b/dom/media/test/contentType.sjs
new file mode 100644
index 0000000000..5a371564cf
--- /dev/null
+++ b/dom/media/test/contentType.sjs
@@ -0,0 +1,77 @@
+// Parse the query string, and give us the value for a certain key, or false if
+// it does not exist.
+function parseQuery(request, key) {
+ var params = request.queryString.split("?")[0].split("&");
+ for (var j = 0; j < params.length; ++j) {
+ var p = params[j];
+ if (p == key) {
+ return true;
+ }
+ if (p.indexOf(key + "=") == 0) {
+ return p.substring(key.length + 1);
+ }
+ if (!p.includes("=") && key == "") {
+ return p;
+ }
+ }
+ return false;
+}
+
+function handleRequest(request, response) {
+ try {
+ // Get the filename to send back.
+ var filename = parseQuery(request, "file");
+
+ var file = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
+ var fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ var bis = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+ Ci.nsIBinaryInputStream
+ );
+ var paths = "tests/dom/media/test/" + filename;
+ dump(paths + "\n");
+ var split = paths.split("/");
+ for (var i = 0; i < split.length; ++i) {
+ file.append(split[i]);
+ }
+ fis.init(file, -1, -1, false);
+
+ // handle range requests
+ var partialstart = 0,
+ partialend = file.fileSize - 1;
+ if (request.hasHeader("Range")) {
+ var range = request.getHeader("Range");
+ var parts = range.replace(/bytes=/, "").split("-");
+ partialstart = parts[0];
+ partialend = parts[1];
+ if (!partialend.length) {
+ partialend = file.fileSize - 1;
+ }
+ response.setStatusLine(request.httpVersion, 206, "Partial Content");
+ var contentRange =
+ "bytes " + partialstart + "-" + partialend + "/" + file.fileSize;
+ response.setHeader("Content-Range", contentRange);
+ }
+
+ fis.seek(Ci.nsISeekableStream.NS_SEEK_SET, partialstart);
+ bis.setInputStream(fis);
+
+ var sendContentType = parseQuery(request, "nomime");
+ if (!sendContentType) {
+ var contentType = parseQuery(request, "type");
+ if (!contentType) {
+ // This should not happen.
+ dump("No type specified without having 'nomime' in parameters.");
+ return;
+ }
+ response.setHeader("Content-Type", contentType, false);
+ }
+ response.setHeader("Content-Length", "" + bis.available(), false);
+
+ var bytes = bis.readBytes(bis.available());
+ response.write(bytes, bytes.length);
+ } catch (e) {
+ dump("ERROR : " + e + "\n");
+ }
+}
diff --git a/dom/media/test/crashtests/0-timescale.html b/dom/media/test/crashtests/0-timescale.html
new file mode 100644
index 0000000000..db845096dd
--- /dev/null
+++ b/dom/media/test/crashtests/0-timescale.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+</head>
+<body>
+<!-- This video file has a timescale of 0 in the mdhd atom -->
+<video src="0-timescale.mp4"
+ autoplay
+ onerror="document.documentElement.className=undefined"
+ onloadedmetadata="this.src=''; document.documentElement.className=undefined">
+<!-- Note we reset 'src' to release decoder resources and cubeb streams to prevent OOM or OpenCubeb() failures. -->
+</video>
+</body>
+</html>
diff --git a/dom/media/test/crashtests/0-timescale.mp4 b/dom/media/test/crashtests/0-timescale.mp4
new file mode 100644
index 0000000000..32df5dc5a5
--- /dev/null
+++ b/dom/media/test/crashtests/0-timescale.mp4
Binary files differ
diff --git a/dom/media/test/crashtests/1012609.html b/dom/media/test/crashtests/1012609.html
new file mode 100644
index 0000000000..1dad783a07
--- /dev/null
+++ b/dom/media/test/crashtests/1012609.html
@@ -0,0 +1,9 @@
+<script>
+try{var r0=new AudioContext();}catch(e){}
+try{var r32=r0.createOscillator();}catch(e){}
+try{var r58=r0.createPeriodicWave(new Float32Array(1997),new Float32Array(1997));}catch(e){}
+try{r32.start(0);}catch(e){}
+try{r32.setPeriodicWave(r58);}catch(e){}
+try{r32.frequency.value=-1;}catch(e){}
+</script>
+
diff --git a/dom/media/test/crashtests/1015662.html b/dom/media/test/crashtests/1015662.html
new file mode 100644
index 0000000000..20407c807d
--- /dev/null
+++ b/dom/media/test/crashtests/1015662.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<body>
+<video><track src="javascript:5"></track></video>
+</body>
diff --git a/dom/media/test/crashtests/1028458.html b/dom/media/test/crashtests/1028458.html
new file mode 100644
index 0000000000..bb01e3343a
--- /dev/null
+++ b/dom/media/test/crashtests/1028458.html
@@ -0,0 +1,23 @@
+<html class="reftest-wait">
+<audio id="testAudio" controls></audio>
+<script type="text/javascript">
+navigator.mozGetUserMedia({audio: true, fake: true}, function(stream) {
+ stream.getAudioTracks()[0].enabled = false;
+ var testAudio = document.getElementById('testAudio');
+ // Wait some time for good measure
+ var eventReceived = 0;
+ testAudio.addEventListener("timeupdate", function() {
+ if (++eventReceived == 3) {
+ document.querySelector("html").className = "";
+ }
+ })
+ testAudio.srcObject = stream;
+ testAudio.play();
+ }, function(err) {
+ // Don't go orange if we can't get an audio input stream,
+ // as this is not what we are trying to test and can happen on Windows.
+ document.querySelector("html").className = "";
+ });
+</script>
+
+</html>
diff --git a/dom/media/test/crashtests/1041466.html b/dom/media/test/crashtests/1041466.html
new file mode 100644
index 0000000000..0f064d186c
--- /dev/null
+++ b/dom/media/test/crashtests/1041466.html
@@ -0,0 +1,21 @@
+<html>
+<script>
+try{var Context1= new (window.webkitAudioContext || window.AudioContext)()}catch(e){}
+try{var Delay0=Context1.createDelay();}catch(e){}
+try{var ScriptProcessor0=Context1.createScriptProcessor(512,26,7);}catch(e){}
+try{var ChannelSplitter0=Context1.createChannelSplitter(91);}catch(e){}
+try{var Gain1=Context1.createGain();}catch(e){}
+try{var WaveShaper0=Context1.createWaveShaper();}catch(e){}
+try{var Analyser1=Context1.createAnalyser();}catch(e){}
+try{Gain1.connect(Delay0);}catch(e){}
+try{Analyser1.connect(BiquadFilter0);}catch(e){}
+try{Gain1.connect(Context1.destination);}catch(e){}
+try{WaveShaper0.connect(ScriptProcessor0);}catch(e){}
+try{ChannelSplitter0.connect(BiquadFilter1);}catch(e){}
+try{Delay0.connect(Gain1);}catch(e){}
+try{ScriptProcessor0.connect(Context1.destination);}catch(e){}
+try{WaveShaper0.connect(Gain1);}catch(e){}
+try{WaveShaper0.connect(ChannelSplitter0);}catch(e){}
+try{ScriptProcessor0.connect(WaveShaper0);}catch(e){}
+</script>
+</html>
diff --git a/dom/media/test/crashtests/1045650.html b/dom/media/test/crashtests/1045650.html
new file mode 100644
index 0000000000..32acd2ce75
--- /dev/null
+++ b/dom/media/test/crashtests/1045650.html
@@ -0,0 +1,18 @@
+<html><body><script>
+
+var r0=new AudioContext();
+
+var splitter=r0.createChannelSplitter();
+var delay=r0.createDelay();
+var scriptp=r0.createScriptProcessor();
+var cmerger=r0.createChannelMerger();
+var gain=r0.createGain();
+
+splitter.connect(delay,2);
+delay.connect(scriptp);
+scriptp.connect(cmerger);
+cmerger.connect(splitter);
+gain.connect(gain);
+gain.connect(cmerger);
+
+</script></body></html>
diff --git a/dom/media/test/crashtests/1080986.html b/dom/media/test/crashtests/1080986.html
new file mode 100644
index 0000000000..1de3075169
--- /dev/null
+++ b/dom/media/test/crashtests/1080986.html
@@ -0,0 +1,3 @@
+<html>
+<audio autoplay src="1080986.wav"></audio>
+</html>
diff --git a/dom/media/test/crashtests/1080986.wav b/dom/media/test/crashtests/1080986.wav
new file mode 100644
index 0000000000..b96c59b7ec
--- /dev/null
+++ b/dom/media/test/crashtests/1080986.wav
Binary files differ
diff --git a/dom/media/test/crashtests/1122218.html b/dom/media/test/crashtests/1122218.html
new file mode 100644
index 0000000000..984487dd21
--- /dev/null
+++ b/dom/media/test/crashtests/1122218.html
@@ -0,0 +1,24 @@
+<html>
+<head>
+<script>
+function boom() {
+ var r0=new AudioContext();
+
+ var cm=r0.createChannelMerger(20);
+
+ var o1=r0.createOscillator();
+ var o2=r0.createOscillator();
+
+ var pw=r0.createPeriodicWave(new Float32Array(4),new Float32Array(4));
+ o2.setPeriodicWave(pw);
+
+ o1.connect(cm);
+ cm.connect(o2.frequency);
+
+ o1.start();
+ o2.start(0.476);
+}
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/dom/media/test/crashtests/1127188.html b/dom/media/test/crashtests/1127188.html
new file mode 100644
index 0000000000..650f07dd47
--- /dev/null
+++ b/dom/media/test/crashtests/1127188.html
@@ -0,0 +1,3 @@
+<script>
+ (new AudioContext).close();
+</script>
diff --git a/dom/media/test/crashtests/1157994.html b/dom/media/test/crashtests/1157994.html
new file mode 100644
index 0000000000..2e3001302a
--- /dev/null
+++ b/dom/media/test/crashtests/1157994.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ var ac = new AudioContext();
+ // first "suspended" -> "running" transition
+ ac.onstatechange = function() {
+ ac.onstatechange = null;
+ ac.suspend();
+ ac.close();
+ }
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
+
diff --git a/dom/media/test/crashtests/1158427.html b/dom/media/test/crashtests/1158427.html
new file mode 100644
index 0000000000..b544a942a1
--- /dev/null
+++ b/dom/media/test/crashtests/1158427.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ dump("before capture\n");
+ document.createElement("audio").mozCaptureStreamUntilEnded();
+ dump("before context\n");
+ new window.AudioContext();
+ dump("before gc\n");
+ SpecialPowers.forceCC();
+ dump("after gc\n");
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
+
diff --git a/dom/media/test/crashtests/1180881.html b/dom/media/test/crashtests/1180881.html
new file mode 100644
index 0000000000..d5bf2f5642
--- /dev/null
+++ b/dom/media/test/crashtests/1180881.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+<body>
+<video src="1180881.webm" autoplay></video>
+</body>
+</html>
diff --git a/dom/media/test/crashtests/1180881.webm b/dom/media/test/crashtests/1180881.webm
new file mode 100644
index 0000000000..2fb2be7a7f
--- /dev/null
+++ b/dom/media/test/crashtests/1180881.webm
Binary files differ
diff --git a/dom/media/test/crashtests/1185176.html b/dom/media/test/crashtests/1185176.html
new file mode 100644
index 0000000000..d5e9b68a29
--- /dev/null
+++ b/dom/media/test/crashtests/1185176.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<script>
+
+function boom()
+{
+ var ac = new window.AudioContext();
+ var oscillator = ac.createOscillator();
+ oscillator.start(0);
+ oscillator.stop(0.1);
+ setTimeout(function() {
+ oscillator.channelCount = 1;
+ oscillator.channelCountMode = "explicit";
+ oscillator.channelInterpretation = "speakers";
+ document.documentElement.removeAttribute("class");
+ }, 1000);
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
+
diff --git a/dom/media/test/crashtests/1185191.html b/dom/media/test/crashtests/1185191.html
new file mode 100644
index 0000000000..4ed5ba2ee2
--- /dev/null
+++ b/dom/media/test/crashtests/1185191.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<script>
+
+function boom()
+{
+ var a = new AudioContext();
+ var b = new BroadcastChannel("x");
+ a.addEventListener("statechange", bye, false);
+}
+
+function bye()
+{
+ document.documentElement.removeAttribute("class");
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/dom/media/test/crashtests/1185192.html b/dom/media/test/crashtests/1185192.html
new file mode 100644
index 0000000000..46fa311aa2
--- /dev/null
+++ b/dom/media/test/crashtests/1185192.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ new Audio().mozCaptureStreamUntilEnded();
+ var ac = new window.AudioContext();
+ ac.resume();
+ ac.close();
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
+
diff --git a/dom/media/test/crashtests/1197935.html b/dom/media/test/crashtests/1197935.html
new file mode 100644
index 0000000000..dd8ad0382d
--- /dev/null
+++ b/dom/media/test/crashtests/1197935.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+<body>
+<video src="1197935.mp4" autoplay></video>
+</body>
+</html>
diff --git a/dom/media/test/crashtests/1197935.mp4 b/dom/media/test/crashtests/1197935.mp4
new file mode 100644
index 0000000000..f00de75627
--- /dev/null
+++ b/dom/media/test/crashtests/1197935.mp4
Binary files differ
diff --git a/dom/media/test/crashtests/1223670.html b/dom/media/test/crashtests/1223670.html
new file mode 100644
index 0000000000..94cad43e23
--- /dev/null
+++ b/dom/media/test/crashtests/1223670.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<script>
+
+function boom()
+{
+
+ var ac = new window.AudioContext("publicnotification");
+
+ setTimeout(function() {
+ document.documentElement.removeAttribute("class");
+ var htmlAudio = new Audio();
+ var stream = htmlAudio.mozCaptureStreamUntilEnded();
+ ac.createMediaStreamSource(stream);
+ }, 0);
+}
+
+</script>
+</head>
+<body onload="boom();">
+</body>
+</html>
diff --git a/dom/media/test/crashtests/1236639.html b/dom/media/test/crashtests/1236639.html
new file mode 100644
index 0000000000..5c4634a4d4
--- /dev/null
+++ b/dom/media/test/crashtests/1236639.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Bug 1236639: Crash on audio playback due to invalid XING headers</title>
+</head>
+<body>
+<audio autoplay src="1236639.mp3"></audio>
+</body>
+</html>
diff --git a/dom/media/test/crashtests/1236639.mp3 b/dom/media/test/crashtests/1236639.mp3
new file mode 100644
index 0000000000..8ef96e8cc7
--- /dev/null
+++ b/dom/media/test/crashtests/1236639.mp3
Binary files differ
diff --git a/dom/media/test/crashtests/1257700.html b/dom/media/test/crashtests/1257700.html
new file mode 100644
index 0000000000..5377b17da5
--- /dev/null
+++ b/dom/media/test/crashtests/1257700.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+<body>
+<video src="1257700.webm" autoplay></video>
+</body>
+</html>
diff --git a/dom/media/test/crashtests/1257700.webm b/dom/media/test/crashtests/1257700.webm
new file mode 100644
index 0000000000..63e53c8c0a
--- /dev/null
+++ b/dom/media/test/crashtests/1257700.webm
Binary files differ
diff --git a/dom/media/test/crashtests/1267263.html b/dom/media/test/crashtests/1267263.html
new file mode 100644
index 0000000000..a4d0e621cd
--- /dev/null
+++ b/dom/media/test/crashtests/1267263.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<script>
+
+function boom()
+{
+ vid.setMediaKeys(null);
+ vid.fastSeek(111);
+}
+
+</script>
+</head>
+
+<body onload="boom();">
+ <video id="vid" src="../../../../layout/reftests/webm-video/frames.webm"></video>
+</body>
+</html>
diff --git a/dom/media/test/crashtests/1270303.html b/dom/media/test/crashtests/1270303.html
new file mode 100644
index 0000000000..23608bb2b8
--- /dev/null
+++ b/dom/media/test/crashtests/1270303.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+<body>
+<video src="1270303.webm" autoplay></video>
+</body>
+</html>
diff --git a/dom/media/test/crashtests/1270303.webm b/dom/media/test/crashtests/1270303.webm
new file mode 100644
index 0000000000..c9b0003d6b
--- /dev/null
+++ b/dom/media/test/crashtests/1270303.webm
Binary files differ
diff --git a/dom/media/test/crashtests/1291702.html b/dom/media/test/crashtests/1291702.html
new file mode 100644
index 0000000000..54f17b45b8
--- /dev/null
+++ b/dom/media/test/crashtests/1291702.html
@@ -0,0 +1,72 @@
+<script>
+Logger={}; Logger.JSError=function(e){};
+try { o0 = new Audio("media/audio/mono-uncompressed-8bit-44100hz.wav") } catch(e) { Logger.JSError(e); }
+try { o1 = o0.mozCaptureStreamUntilEnded() } catch(e) { Logger.JSError(e); }
+try { o2 = new window.AudioContext(); } catch(e) { Logger.JSError(e); }
+try { o3 = o2.createBufferSource(); } catch(e) { Logger.JSError(e); }
+try { o5 = o2.createChannelMerger(3); } catch(e) { Logger.JSError(e); }
+try { o3.start(0) } catch(e) { Logger.JSError(e); }
+try { o2.listener.setPosition(0.0417049336344248, 0.9932504594310304, 32) } catch(e) { Logger.JSError(e); }
+try { o5.disconnect(0) } catch(e) { Logger.JSError(e); }
+try { o0.mozGetMetadata() } catch(e) { Logger.JSError(e); }
+try { o3.channelCount = 1; } catch(e) { Logger.JSError(e); }
+try { o3.buffer = function anonymous() {
+var buffer = o2.createBuffer(1, 512, o2.sampleRate);for(var c=0;c<1;c++) {var data = buffer.getChannelData(c);for(var i=0;i<512;i++) {data[i] = i % 512}}return buffer;
+}(); } catch(e) { Logger.JSError(e); }
+try { o3.loop = false; } catch(e) { Logger.JSError(e); }
+try { o0.preservesPitch = false; } catch(e) { Logger.JSError(e); }
+try { o2.destination.channelCount = 1; } catch(e) { Logger.JSError(e); }
+try { o3.loopStart = 8; } catch(e) { Logger.JSError(e); }
+try { o3.playbackRate.value = 0.46271130895770884; } catch(e) { Logger.JSError(e); }
+try { o2.listener.setVelocity(0.34781960219792546, 4, 2048) } catch(e) { Logger.JSError(e); }
+try { o5.connect(o5, 0, 0) } catch(e) { Logger.JSError(e); }
+try { o3.loopStart = -0.24696638021780326; } catch(e) { Logger.JSError(e); }
+try { o0.mozSetup(1, 44100) } catch(e) { Logger.JSError(e); }
+try { o3.buffer.copyToChannel(function anonymous() {
+var buffer=new Float32Array(256);for(var i=0;i<256;i++){buffer[i]=i / 256}return buffer;
+}(), 1, 2048, 64) } catch(e) { Logger.JSError(e); }
+try { o3.loop = false; } catch(e) { Logger.JSError(e); }
+try { setInterval(function anonymous() {
+try { o0.pause() } catch(e) { Logger.JSError(e); }
+}, 12.902067779658143) } catch(e) { Logger.JSError(e); }
+try { o2.listener.setPosition(256, 256, 16) } catch(e) { Logger.JSError(e); }
+try { o3.playbackRate.setValueCurveAtTime(function anonymous() {
+var buffer=new Float32Array(4);for(var i=0;i<4;i++){buffer[i]=i / 4}return buffer;
+}(), 2, 0.40792575814014437) } catch(e) { Logger.JSError(e); }
+try { o3.playbackRate.value = 0.4997270553139334; } catch(e) { Logger.JSError(e); }
+try { o0.loop = true; } catch(e) { Logger.JSError(e); }
+try { o3.loopStart = 4; } catch(e) { Logger.JSError(e); }
+try { setInterval(function anonymous() {
+try { o3.buffer = function anonymous() {
+var buffer = o2.createBuffer(1, 1, o2.sampleRate);for(var c=0;c<1;c++) {var data = buffer.getChannelData(c);for(var i=0;i<1;i++) {data[i] = Math.sin(Math.sin(i))}}return buffer;
+}() } catch(e) { Logger.JSError(e); }
+}, 54.32078602859342) } catch(e) { Logger.JSError(e); }
+try { o3.connect(o2.destination); } catch(e) { Logger.JSError(e); }
+try { o3.channelCountMode = 'max'; } catch(e) { Logger.JSError(e); }
+try { setInterval(function anonymous() {
+try { o3.channelCount = 1; } catch(e) { Logger.JSError(e); }
+}, 55.448587039802966) } catch(e) { Logger.JSError(e); }
+try { o3.playbackRate.cancelScheduledValues(0.7190983131805198) } catch(e) { Logger.JSError(e); }
+try { o3.playbackRate.cancelScheduledValues(16) } catch(e) { Logger.JSError(e); }
+try { o3.connect(o5, 0, 2) } catch(e) { Logger.JSError(e); }
+try { o3.loopEnd = 0.5864771678080962; } catch(e) { Logger.JSError(e); }
+try { o0.playbackRate = 0.2781783298771312; } catch(e) { Logger.JSError(e); }
+try { o0.loop = false; } catch(e) { Logger.JSError(e); }
+try { setInterval(function anonymous() {
+try { o5.disconnect(0) } catch(e) { Logger.JSError(e); }
+}, 29.75776777646425) } catch(e) { Logger.JSError(e); }
+try { o3.playbackRate.setValueCurveAtTime(function anonymous() {
+var buffer=new Float32Array(8);for(var i=0;i<8;i++){buffer[i]=8 % 8}return buffer;
+}(), 0.4972710112336257, 64) } catch(e) { Logger.JSError(e); }
+try { setInterval(function anonymous() {
+try { o0.controls = false; } catch(e) { Logger.JSError(e); }
+}, 22.550249570567694) } catch(e) { Logger.JSError(e); }
+try { o2.listener.setOrientation(0.6531494410366634, 64, 0.5120918081402992, -64, 0.32912433155093446, 256) } catch(e) { Logger.JSError(e); }
+try { o3.loop = true; } catch(e) { Logger.JSError(e); }
+try { o3.connect(o5, 0, 0) } catch(e) { Logger.JSError(e); }
+try { o3.buffer = function anonymous() {
+var buffer = o2.createBuffer(1, 2048, 48000);for(var c=0;c<1;c++) {var data = buffer.getChannelData(c);for(var i=0;i<2048;i++) {data[i] = Math.sin(Math.sin(2048 * 0.2519529190035427))}}return buffer;
+}(); } catch(e) { Logger.JSError(e); }
+try { o3.disconnect(0) } catch(e) { Logger.JSError(e); }
+</script>
+
diff --git a/dom/media/test/crashtests/1368490.html b/dom/media/test/crashtests/1368490.html
new file mode 100644
index 0000000000..8a2d9f9674
--- /dev/null
+++ b/dom/media/test/crashtests/1368490.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+ <title> Bug 1368490 : Crash if media recorder source stream reduces number of channels. </title>
+</head>
+<meta charset="utf-8">
+<script type="text/javascript">
+
+function boom() {
+ let audioContext = new window.AudioContext();
+ let oscillator = audioContext.createOscillator();
+ let dst = audioContext.createMediaStreamDestination();
+ oscillator.channelCount = 4;
+ dst.channelCount = 4;
+ oscillator.connect(dst, 0, 0);
+ oscillator.start();
+ mediaRec = new MediaRecorder(dst.stream);
+
+ mediaRec.start(100);
+ setTimeout(() => {
+ dst.channelCount = 1;
+ setTimeout(() => {
+ mediaRec.stop();
+ document.documentElement.removeAttribute("class");
+ }, 100);
+ }, 100);
+}
+</script>
+<body onload="boom();"></body>
+</html>
diff --git a/dom/media/test/crashtests/1378826.html b/dom/media/test/crashtests/1378826.html
new file mode 100644
index 0000000000..e1913cd0f5
--- /dev/null
+++ b/dom/media/test/crashtests/1378826.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<title>Bug 1378826 : Removing last video track from recorder stream crashes.</title>
+</head>
+<body>
+<canvas id="canvas"></canvas>
+<script type="text/javascript">
+
+function wait(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+function boom() {
+ let canvas = document.getElementById("canvas");
+ let ctx = canvas.getContext('2d');
+ ctx.fillRect(10, 10, 100, 100);
+ let stream = canvas.captureStream();
+ let rec = new MediaRecorder(stream);
+ // At the time of fixing this bug onstop would fire, but this may change in
+ // future. As such defensively listen for onerror too to prevent this test
+ // timing out.
+ let stoppedPromise = new Promise(y => (rec.onstop = y,
+ rec.onerror = e => y));
+ rec.onstart = () => {
+ // Remove the video track from the stream we're recording
+ stream.removeTrack(stream.getTracks()[0]);
+ // Recorder should stop or error in response to the above
+ return stoppedPromise
+ .then(() => {
+ // Little wait to help get bad writes if they're going to happen
+ wait(100)
+ .then(() => {
+ // Didn't crash, finish
+ document.documentElement.removeAttribute("class");
+ });
+ });
+ };
+ rec.start();
+}
+
+window.onload = boom;
+
+</script>
+</body>
+</html>
diff --git a/dom/media/test/crashtests/1384248.html b/dom/media/test/crashtests/1384248.html
new file mode 100644
index 0000000000..5d9c60edda
--- /dev/null
+++ b/dom/media/test/crashtests/1384248.html
@@ -0,0 +1,10 @@
+<html>
+ <head>
+ <script>
+ try { o1 = document.createElement('audio') } catch(e) { }
+ try { o2 = document.implementation.createDocument('', '', null).adoptNode(o1); } catch(e) { };
+ try { o3 = new AudioContext('alarm') } catch(e) { }
+ try { o3.createMediaElementSource(o1) } catch(e) { }
+ </script>
+ </head>
+</html>
diff --git a/dom/media/test/crashtests/1388372.html b/dom/media/test/crashtests/1388372.html
new file mode 100644
index 0000000000..977ebddf53
--- /dev/null
+++ b/dom/media/test/crashtests/1388372.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+navigator.mediaDevices.getUserMedia({audio: {
+ echoCancellation: false,
+ noiseSuppression: false,
+ autoGainControl: false
+ }, fake: true
+}).then((stream) => {
+ document.documentElement.removeAttribute("class");
+})
+</script>
+</html>
diff --git a/dom/media/test/crashtests/1389304.html b/dom/media/test/crashtests/1389304.html
new file mode 100644
index 0000000000..df419c51d7
--- /dev/null
+++ b/dom/media/test/crashtests/1389304.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: Negative duration.</title>
+</head>
+<body>
+
+<video id="v" controls src="1389304.mp4">
+</video>
+<p id="msg"></p>
+
+<script type="text/javascript">
+
+function log(x) {
+ msg.innerHTML = x + "<br>";
+}
+
+v.play();
+v.onended = function() {
+ log("endded!");
+ let seekable = v.seekable;
+ for (let i = 0; i < seekable.length; ++i) {
+ let start = seekable.start(i);
+ let end = seekable.end(i);
+ log(`[${i}]: start=${start} end=${end}`);
+ }
+}
+
+</script>
+
+</body>
+</html>
diff --git a/dom/media/test/crashtests/1389304.mp4 b/dom/media/test/crashtests/1389304.mp4
new file mode 100644
index 0000000000..25cd746972
--- /dev/null
+++ b/dom/media/test/crashtests/1389304.mp4
Binary files differ
diff --git a/dom/media/test/crashtests/1393272.webm b/dom/media/test/crashtests/1393272.webm
new file mode 100644
index 0000000000..1f1cade6dc
--- /dev/null
+++ b/dom/media/test/crashtests/1393272.webm
Binary files differ
diff --git a/dom/media/test/crashtests/1411322.html b/dom/media/test/crashtests/1411322.html
new file mode 100644
index 0000000000..772b68f0cc
--- /dev/null
+++ b/dom/media/test/crashtests/1411322.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1411322: Shutdown after getting memory reports from MediaRecorder</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<audio id="audio"></audio>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+let recorder = new MediaRecorder(audio.mozCaptureStream());
+recorder.start();
+SpecialPowers.getMemoryReports();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/crashtests/1450845.html b/dom/media/test/crashtests/1450845.html
new file mode 100644
index 0000000000..451d116e83
--- /dev/null
+++ b/dom/media/test/crashtests/1450845.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+ <title>Bug 1450845: Avoid seek to next frame when already seeking</title>
+ <script>
+ async function boom() {
+ let video = document.getElementById('video');
+
+ // Internally play causes a seek, make sure we don't crash during this
+ video.play();
+ try {
+ await document.getElementById('video').seekToNextFrame();
+ } catch (e) {
+ // We don't mind if the promise was rejected so long as we don't crash
+ }
+ // Didn't crash
+
+ // Stop playback and cause a seek to 0
+ video.pause();
+ video.currentTime = 0;
+ try {
+ await document.getElementById('video').seekToNextFrame();
+ } finally {
+ // Didn't crash
+ document.documentElement.removeAttribute("class");
+ }
+ }
+ window.addEventListener('load', boom)
+ </script>
+</head>
+<body>
+ <video id='video' src='data:video/webm;base64,GkXfowEAAAAAAAAfQoaBAUL3gQFC8oEEQvOBQoWBAhhTgGcBAAAAAAAB6BFNm3RALE27i1OrhBVJqWZTrIHfTbuMU6uEFlSua1OsggEwTbuMU6uEHFO7a1OsggHL7AEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAVSalmAQAAAAAAAEUqTYCNTGF2ZjU3LjI5LjEwMVdBjUxhdmY1Ny4yOS4xMDFzpJBAb17Yv2oNAF1ZEESuco33RImIQFCAAAAAAAAWVK5rAQAAAAAAADyuAQAAAAAAADPXgQFzxYEBnIEAIrWcg3VuZIaFVl9WUDmDgQEj44OEAfygVeABAAAAAAAAB7CCAUC6gfAfQ7Z1AQAAAAAAAEfngQCjqYEAAICCSYNCABPwDvYAOCQcGFQAAFBh9jAAABML7AAATEnjdRwIJ+gAo5eBACEAhgBAkpwATEAABCasAABekcXgAB'>
+</body>
+</html>
diff --git a/dom/media/test/crashtests/1489160.html b/dom/media/test/crashtests/1489160.html
new file mode 100644
index 0000000000..c4f643700c
--- /dev/null
+++ b/dom/media/test/crashtests/1489160.html
@@ -0,0 +1,10 @@
+<html>
+<head>
+<script>
+audio = new AudioContext()
+audio.close()
+audio.close()
+</script>
+</head>
+</html>
+
diff --git a/dom/media/test/crashtests/1494073.html b/dom/media/test/crashtests/1494073.html
new file mode 100644
index 0000000000..41e7a36554
--- /dev/null
+++ b/dom/media/test/crashtests/1494073.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+ <title>Bug 1494073: Setting playback rate too high</title>
+ <script>
+ function finish() {
+ document.documentElement.removeAttribute("class");
+ }
+ var src="data:audio/wav;base64,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"
+ var audio=new Audio(src)
+ audio.onended = finish;
+ audio.onerror = finish;
+ try{audio.play()}catch(e){}
+ try{audio.preload=1}catch(e){}
+ try{audio.muted=1}catch(e){}
+ try{audio.playbackRate=567312.2079031984}catch(e){}
+ </script>
+</head>
+</html>
diff --git a/dom/media/test/crashtests/1517199.html b/dom/media/test/crashtests/1517199.html
new file mode 100644
index 0000000000..10c994f845
--- /dev/null
+++ b/dom/media/test/crashtests/1517199.html
@@ -0,0 +1,17 @@
+<html class="reftest-wait">
+<head>
+<script>
+(async _ => {
+ const video = document.createElement("video");
+ video.mozCaptureStreamUntilEnded();
+ video.src = "test.mp4";
+ video.playbackRate = 2;
+ await new Promise(r => video.onloadedmetadata = r);
+ video.currentTime = video.duration - 1;
+ await video.play();
+ await new Promise(r => video.onended = r);
+ document.documentElement.removeAttribute("class");
+})();
+</script>
+</head>
+</html>
diff --git a/dom/media/test/crashtests/1526044.html b/dom/media/test/crashtests/1526044.html
new file mode 100644
index 0000000000..1914dfc2f5
--- /dev/null
+++ b/dom/media/test/crashtests/1526044.html
@@ -0,0 +1,19 @@
+<script>
+ function start () {
+ try { o1 = new AudioContext({}) } catch (e) { }
+ try { o2 = new DynamicsCompressorNode(o1, {}) } catch (e) { }
+ try { o3 = o2.attack } catch (e) { }
+ try { o4 = new XMLHttpRequest({mozAnon: true, mozSystem: true}) } catch (e) { }
+ try { o4.open('GET', '', false) } catch (e) { }
+ try { o4.send() } catch (e) { }
+ for (let i = 0; i < 20; i++) {
+ try { o5 = o1.createGain() } catch (e) { }
+ try { o1.suspend().then(function () { }) } catch (e) { }
+ try { o5.connect(o3) } catch (e) { }
+ }
+ setTimeout('location.reload()', 200)
+ }
+
+ window.addEventListener('load', start)
+</script>
+
diff --git a/dom/media/test/crashtests/1530897.webm b/dom/media/test/crashtests/1530897.webm
new file mode 100644
index 0000000000..ac2224157a
--- /dev/null
+++ b/dom/media/test/crashtests/1530897.webm
Binary files differ
diff --git a/dom/media/test/crashtests/1538727.html b/dom/media/test/crashtests/1538727.html
new file mode 100644
index 0000000000..6371d54383
--- /dev/null
+++ b/dom/media/test/crashtests/1538727.html
@@ -0,0 +1,14 @@
+<script>
+const canvas = document.createElement('canvas')
+const context = canvas.getContext('2d', {})
+const xhr = new XMLHttpRequest({})
+const stream = canvas.captureStream(new Float64Array([1593177632.1689904])[0])
+recorder = new MediaRecorder(stream)
+recorder.start(100)
+xhr.open('G', '', false)
+xhr.send()
+recorder.stop()
+tracks = stream.getVideoTracks()
+track = tracks[(1051736525 % tracks.length)]
+stream.removeTrack(track)
+</script>
diff --git a/dom/media/test/crashtests/1545133.html b/dom/media/test/crashtests/1545133.html
new file mode 100644
index 0000000000..fb7039aae3
--- /dev/null
+++ b/dom/media/test/crashtests/1545133.html
@@ -0,0 +1,34 @@
+<html class="reftest-wait">
+<head>
+<script>
+const xhr = new XMLHttpRequest()
+
+async function boom () {
+ await new Promise(r => setTimeout(r, 100))
+
+ SpecialPowers.forceCC()
+ SpecialPowers.forceCC()
+ SpecialPowers.forceCC()
+
+ document.documentElement.removeAttribute("class")
+}
+
+function start () {
+ const context = new AudioContext({})
+ const filter = new BiquadFilterNode(context, {})
+ const destination = context.createMediaStreamDestination()
+ const processor = context.createScriptProcessor(8192, 8, 8)
+ processor.connect(filter.Q)
+ processor.disconnect()
+ xhr.open('G', '', false)
+ xhr.send()
+ context.createMediaStreamSource(destination.stream)
+ processor.connect(filter.Q)
+ context.close()
+ context.addEventListener('statechange', boom, true)
+}
+
+document.addEventListener('DOMContentLoaded', start)
+</script>
+</head>
+</html>
diff --git a/dom/media/test/crashtests/1547784.html b/dom/media/test/crashtests/1547784.html
new file mode 100644
index 0000000000..ee270491f1
--- /dev/null
+++ b/dom/media/test/crashtests/1547784.html
@@ -0,0 +1,33 @@
+<html class="reftest-wait">
+<head>
+ <script>
+ const doc = new Document();
+ const video = document.createElementNS('http://www.w3.org/1999/xhtml', 'video');
+ const source = new MediaSource();
+
+ navigator.requestMediaKeySystemAccess('org.w3.clearkey', [{ '': [{ '': '' }] }])
+ .then(keySystemAccess => {
+ return keySystemAccess.createMediaKeys();
+ }).then(_ => {
+ video.src = URL.createObjectURL(source);
+ source.addEventListener('sourceopen', () => {
+ doc.adoptNode(video);
+ });
+ });
+
+ navigator.requestMediaKeySystemAccess('org.w3.clearkey', [{ '': [{ '': '' }] }])
+ .then(keySystemAccess => {
+ return keySystemAccess.createMediaKeys();
+ }).then(mediaKeys => {
+ return video.setMediaKeys(mediaKeys);
+ }).then(() => {
+ video.src = URL.createObjectURL(source);
+ document.documentElement.removeAttribute("class");
+ }).catch(e => {
+ // Catch JS errors caused by raciness in the test. So long as we're
+ // not crashing we're good.
+ document.documentElement.removeAttribute("class");
+ });
+ </script>
+</head>
+</html>
diff --git a/dom/media/test/crashtests/1547899.html b/dom/media/test/crashtests/1547899.html
new file mode 100644
index 0000000000..4ffd90c565
--- /dev/null
+++ b/dom/media/test/crashtests/1547899.html
@@ -0,0 +1,20 @@
+<html>
+<head>
+ <script>
+ function start () {
+ const video = document.getElementById('id_0')
+ const stream_1 = new MediaStream()
+ const stream_2 = video.mozCaptureStreamUntilEnded()
+ const track = stream_2.getTracks()[0]
+
+ video.srcObject = stream_1
+ stream_1.addTrack(track)
+ }
+
+ window.addEventListener('load', start)
+ </script>
+</head>
+<body>
+<video id="id_0" src="data:audio/mpeg;base64,ZQr/+1DEAAAAAAAAAAAAAAAAAAAAAABJbmZvAAAADwAAAAsAAAnKABcXFxcXFxcXFy4uLi4uLi4uLkVFRUVFRUVFRV1dXV1dXV1dXXR0dHR0dHR0dIuLi4uLi4uLi6KioqKioqKiorq6urq6urq6utHR0dHR0dHR0ejo6Ojo6Ojo6P///////////wAAADlMQU1FMy45OHIBpQAAAAAuHQAAFEAkBElCAABAAAAJyuGI2MQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA//tQxAAABmQjXHSRAAH6IfB3OVIDAADLlmjIxWKxWTyIABgmGydGjRox4f8Tg+D4OAgc/BwEDn+UBD+qwH/+8Tny7///KO4IYDgcDgcDgcDgcCgQAAAKKBUFc/nnP30a5O0zyYzMNMkzKlPva2GXgMIBbwDQEAYDQNXGLT2EIgEhoFnQNDb4BAPD9wTFYGJBWJw/8AUWgMBQgIEg4KmAKGP/wRG0csCIWDaQxcFhYvv/8WYMUpEIkXhvnRef//kmsuLPH5OvSoAFKxrJIm26DP/7UsQ">
+</body>
+</html>
diff --git a/dom/media/test/crashtests/1560215.html b/dom/media/test/crashtests/1560215.html
new file mode 100644
index 0000000000..1b76e218d7
--- /dev/null
+++ b/dom/media/test/crashtests/1560215.html
@@ -0,0 +1,20 @@
+<html class="reftest-wait">
+<head>
+ <script>
+ async function start () {
+ const canvas = document.createElement('canvas')
+ const context = canvas.getContext('2d')
+ context.fillStyle = "red"
+ context.fillRect(0, 0, 1, 1)
+ const recorder = new MediaRecorder(
+ canvas.captureStream(), { videoBitsPerSecond: 16 })
+ recorder.start(100)
+ await new Promise(r => recorder.onstart = r)
+ recorder.pause()
+ document.documentElement.removeAttribute("class")
+ }
+
+ window.addEventListener('load', start)
+ </script>
+</head>
+</html>
diff --git a/dom/media/test/crashtests/1569645.html b/dom/media/test/crashtests/1569645.html
new file mode 100644
index 0000000000..b1f1247f26
--- /dev/null
+++ b/dom/media/test/crashtests/1569645.html
@@ -0,0 +1,23 @@
+<html>
+<head>
+ <script>
+ function start () {
+ const canvas = document.getElementById("c")
+ canvas.getContext("2d")
+ const video = canvas.captureStream()
+ const ac = new AudioContext()
+ const dest = ac.createMediaStreamDestination()
+ const recorder = new MediaRecorder(
+ new MediaStream([...video.getTracks(), ...dest.stream.getTracks()]), {
+ 'mimeType': 'audio/ogg'
+ })
+ recorder.start()
+ }
+
+ window.addEventListener('load', start)
+ </script>
+</head>
+<body>
+<canvas id="c"></canvas>
+</body>
+</html>
diff --git a/dom/media/test/crashtests/1575271.html b/dom/media/test/crashtests/1575271.html
new file mode 100644
index 0000000000..ceda986553
--- /dev/null
+++ b/dom/media/test/crashtests/1575271.html
@@ -0,0 +1,25 @@
+<html class="reftest-wait">
+<head>
+<script>
+ async function start () {
+ const canvas = document.createElement("canvas")
+ const context = canvas.getContext("2d")
+ context.fillStyle = "blue"
+ context.fillRect(0, 0, canvas.width, canvas.height)
+ const stream = canvas.captureStream()
+ const track = stream.getTracks()[0]
+ const recorder = new MediaRecorder(stream)
+ recorder.start()
+ await new Promise(r => recorder.onstart = r)
+ recorder.pause()
+ stream.removeTrack(track)
+ recorder.resume()
+ await new Promise(r => recorder.onstop = r)
+ document.documentElement.removeAttribute("class")
+ }
+
+ window.addEventListener('load', start)
+</script>
+</head>
+</html>
+
diff --git a/dom/media/test/crashtests/1577184.html b/dom/media/test/crashtests/1577184.html
new file mode 100644
index 0000000000..a38c4a1265
--- /dev/null
+++ b/dom/media/test/crashtests/1577184.html
@@ -0,0 +1,15 @@
+<html>
+<head>
+<script>
+function start () {
+ const frame = document.createElementNS('http://www.w3.org/1999/xhtml', 'frame')
+ document.documentElement.appendChild(frame)
+ frame.contentWindow.eval('window.top.context=new AudioContext()')
+ document.documentElement.innerHTML = ''
+ context.createMediaElementSource(new Audio(''))
+}
+
+document.addEventListener('DOMContentLoaded', start)
+</script>
+</head>
+</html>
diff --git a/dom/media/test/crashtests/1587248.html b/dom/media/test/crashtests/1587248.html
new file mode 100644
index 0000000000..10c555e2b1
--- /dev/null
+++ b/dom/media/test/crashtests/1587248.html
@@ -0,0 +1,23 @@
+<html class="reftest-wait">
+<head>
+<script>
+function start () {
+ const audio = document.getElementById('id_4')
+ const doc = new Document()
+ const stream = new MediaStream()
+ const track = document.createElementNS('http://www.w3.org/1999/xhtml', 'track')
+ audio.srcObject = stream
+ track.textContent = '�'
+ setTimeout(() => {
+ track.replaceChild(audio, track.childNodes[0])
+ audio.play().then(function (arg4) { })
+ document.documentElement.removeAttribute("class")
+ }, 157)
+ doc.adoptNode(audio)
+}
+
+document.addEventListener('DOMContentLoaded', start)
+</script>
+</head>
+<audio class="" id="id_4" itemscope></audio>
+</html>
diff --git a/dom/media/test/crashtests/1594466.html b/dom/media/test/crashtests/1594466.html
new file mode 100644
index 0000000000..276c5fe3d1
--- /dev/null
+++ b/dom/media/test/crashtests/1594466.html
@@ -0,0 +1,22 @@
+<html>
+<head>
+<script>
+function start() {
+ const ac = new AudioContext();
+ const {stream: audioStream} = ac.createMediaStreamDestination();
+ const [audioTrack] = audioStream.getTracks();
+ const canvas = document.createElement("canvas");
+ const ctx = canvas.getContext("2d");
+ const [videoTrack] = canvas.captureStream().getTracks();
+
+ const rec = new MediaRecorder(new MediaStream([audioTrack, videoTrack]), {
+ mimeType: 'video/webm; codecs="vp8, opus"'
+ });
+ rec.start();
+}
+
+document.addEventListener('DOMContentLoaded', start)
+</script>
+</head>
+</html>
+
diff --git a/dom/media/test/crashtests/1601385.html b/dom/media/test/crashtests/1601385.html
new file mode 100644
index 0000000000..7192809076
--- /dev/null
+++ b/dom/media/test/crashtests/1601385.html
@@ -0,0 +1,12 @@
+<html>
+<head>
+<script>
+const video = document.createElement("video");
+video.srcObject = new MediaStream();
+video.mozCaptureStreamUntilEnded();
+video.src = "sound.ogg";
+video.srcObject = undefined;
+</script>
+</head>
+</html>
+
diff --git a/dom/media/test/crashtests/1601422.html b/dom/media/test/crashtests/1601422.html
new file mode 100644
index 0000000000..9ff3c1a07a
--- /dev/null
+++ b/dom/media/test/crashtests/1601422.html
@@ -0,0 +1,20 @@
+<html class="reftest-wait">
+<head>
+<script>
+(async _ => {
+ try {
+ const video = document.createElement('video')
+ video.preload = 'metadata'
+ video.src = 'sound.ogg'
+ await new Promise(r => video.onloadedmetadata = r)
+ const stream_1 = video.mozCaptureStreamUntilEnded()
+ video.src = ''
+ const stream_2 = video.mozCaptureStreamUntilEnded()
+ } finally {
+ document.documentElement.removeAttribute("class")
+ }
+})()
+</script>
+</head>
+</html>
+
diff --git a/dom/media/test/crashtests/1604941.html b/dom/media/test/crashtests/1604941.html
new file mode 100644
index 0000000000..5a9265ea1b
--- /dev/null
+++ b/dom/media/test/crashtests/1604941.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<script>
+
+async function boom()
+{
+ await SpecialPowers.pushPrefEnv({"set": [
+ ["media.cubeb.force_null_context", true],
+ ]});
+ new Audio().mozCaptureStreamUntilEnded();
+ var ac = new window.AudioContext();
+ ac.resume();
+ ac.close();
+ document.documentElement.removeAttribute("class");
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
+
diff --git a/dom/media/test/crashtests/1608286.html b/dom/media/test/crashtests/1608286.html
new file mode 100644
index 0000000000..9f52605be6
--- /dev/null
+++ b/dom/media/test/crashtests/1608286.html
@@ -0,0 +1,50 @@
+<html class="reftest-wait">
+<head>
+ <script>
+ function test() {
+ function checkResolve(value) {
+ // Let the test timeout and fail
+ throw new Error("This promise should not resolve");
+ }
+
+ function checkReject(reason) {
+ if (reason.message !== "Browsing context is no longer available") {
+ // Let the test timeout and fail
+ throw new Error("Unexpected rejected promise reason");
+ }
+ // Otherwise, successfully rejected a request not attached to a
+ // window without crashing
+ }
+
+ var i = document.querySelector("iframe");
+ var nav = i.contentWindow.navigator;
+ i.remove();
+
+ // First, check with valid args
+ nav.requestMediaKeySystemAccess(
+ "com.widevine.alpha",
+ [{
+ initDataTypes: ["webm"],
+ videoCapabilities: [{ contentType: 'video/webm; codecs="vp9"' }]
+ }]
+ ).then(
+ checkResolve,
+ (reason) => {
+ checkReject(reason);
+
+ // Then, check with invalid args
+ nav.requestMediaKeySystemAccess("", []).then(
+ checkResolve,
+ (reason) => {
+ checkReject(reason);
+ document.documentElement.removeAttribute("class");
+ }
+ );
+ });
+ }
+ </script>
+</head>
+<body onload="test()">
+ <iframe></iframe>
+</body>
+</html>
diff --git a/dom/media/test/crashtests/1673525.html b/dom/media/test/crashtests/1673525.html
new file mode 100644
index 0000000000..51de202999
--- /dev/null
+++ b/dom/media/test/crashtests/1673525.html
@@ -0,0 +1,15 @@
+<html>
+<head>
+ <script>
+ document.addEventListener("DOMContentLoaded", () => {
+ const audio = document.getElementById("audio");
+ audio.autoplay = true;
+ audio.mozCaptureStream();
+ })
+ </script>
+</head>
+<body>
+ <!-- The data URL is crafted from a fuzzed mp3 so the base64 can be decoded back to an mp3 as needed -->
+ <audio id="audio" src="data:audio/mpeg;base64,//tQxAAAAAAAAAAAAAAAAAAAAAAASW5mbwAAAA8AAAALAAAJygAXFxcXFxcXFxcuLi4uLi4uLi5FRUVFRUVFRUVdXV1dXV1dXV10dHR0dHR0dHSLi4uLi4uLi4uioqKioqKioqK6urq6urq6urrR0dHR0dHR0dHo6Ojo6Ojo6Oj///////////8AAAA5TEFNRTMuOThyAaUAAAAALh0AABRAJARJQgAAQAAACcrhiNjEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP/7UMQAAAZkI1x0kQAB+iHwdzlSAwAAy5ZoyMVisVk8iAAYJhsnRo0aMeH/E4Pg+DgIHPwcBA5/lAQ/qsB//vE58u///yjuCGA4HA4HA4HA4HAoEAAACigVBXP55z99GuTtM8mMzDTJMypT72thl4DCAW8A0BAGA0DVxi09hCIBIaBZ0DQ2+AQDw/cExWBiQVicP/AFFoDAUICBIOCpgChj/8ERtHLAiFg2kMXBYWL7//FmDFKRCJF4b50Xn//5JrLizx+Tr0qABSsaySJtugz/+1LEBIALgQmLvLUAMXkorjzSjuWd/+FbPOduga79Dhef0OFYNhp+FEPDPRyRvOEERl/Uwbv9F/UuQN5yt9WPM5xwqlB4+hzmfmoYmcceWPD8s3WA8FhYEPkhR0SgMHw="></audio>
+</body>
+</html>
diff --git a/dom/media/test/crashtests/1673526-1.html b/dom/media/test/crashtests/1673526-1.html
new file mode 100644
index 0000000000..925dbae6ab
--- /dev/null
+++ b/dom/media/test/crashtests/1673526-1.html
@@ -0,0 +1,20 @@
+<html>
+<head>
+<script>
+window.addEventListener('load', async () => {
+ const frame = document.createElement('frame')
+ document.documentElement.appendChild(frame)
+ const pc = new RTCPeerConnection({})
+ await pc.createOffer({ 'offerToReceiveAudio': true })
+ const [{receiver}] = pc.getTransceivers()
+ const track = pc.addTrack(receiver.track)
+ pc.removeTrack(track)
+ const [track2] = (await frame.contentWindow.navigator.mediaDevices.getUserMedia({
+ 'audio': {},
+ 'fake': true
+ })).getTracks()
+ pc.addTrack(track2)
+})
+</script>
+</head>
+</html>
diff --git a/dom/media/test/crashtests/1673526-2.html b/dom/media/test/crashtests/1673526-2.html
new file mode 100644
index 0000000000..ddae9520fc
--- /dev/null
+++ b/dom/media/test/crashtests/1673526-2.html
@@ -0,0 +1,20 @@
+<html>
+<head>
+<script>
+window.addEventListener('load', async () => {
+ const frame = document.createElement('frame')
+ document.documentElement.appendChild(frame)
+ const pc = new RTCPeerConnection({})
+ await pc.createOffer({ 'offerToReceiveAudio': true })
+ const [{sender, receiver}] = pc.getTransceivers()
+ await sender.replaceTrack(receiver.track)
+ await sender.replaceTrack(null)
+ const stream = await frame.contentWindow.navigator.mediaDevices.getUserMedia({
+ 'audio': {},
+ 'fake': true
+ })
+ await sender.replaceTrack(stream.getTracks()[0])
+})
+</script>
+</head>
+</html>
diff --git a/dom/media/test/crashtests/1693043.html b/dom/media/test/crashtests/1693043.html
new file mode 100644
index 0000000000..f73323bbd7
--- /dev/null
+++ b/dom/media/test/crashtests/1693043.html
@@ -0,0 +1,21 @@
+<html class="reftest-wait">
+<head>
+<script>
+async function boom() {
+ const audio = document.createElement("audio");
+ audio.preload = "metadata";
+ audio.src = "sound.ogg";
+ await new Promise(r => audio.onloadedmetadata = r);
+ const s = audio.mozCaptureStream();
+ const recorder = new MediaRecorder(
+ new MediaStream(s.getTracks()),
+ { audioBitsPerSecond: 3994678619 }
+ );
+ recorder.start();
+ document.documentElement.removeAttribute("class");
+}
+</script>
+</head>
+<body onload="boom()">
+</body>
+</html>
diff --git a/dom/media/test/crashtests/1696511.html b/dom/media/test/crashtests/1696511.html
new file mode 100644
index 0000000000..de66fa3a67
--- /dev/null
+++ b/dom/media/test/crashtests/1696511.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html class='reftest-wait'>
+<head>
+<script>
+document.addEventListener('DOMContentLoaded', async () => {
+ const canvas = document.createElement('canvas')
+ canvas.height = 27530
+ const context = canvas.getContext('2d')
+ context.strokeRect(5, 5, canvas.width - 5, canvas.height - 5)
+ const stream = canvas.captureStream()
+ const recorder = new MediaRecorder(stream)
+ recorder.start()
+ const stopped = new Promise(r => recorder.onstop = r)
+ await new Promise(r => setTimeout(r, 100))
+ stream.getTracks().forEach(t => t.stop())
+ await stopped
+ document.documentElement.removeAttribute("class")
+})
+</script>
+</head>
+</html>
+
diff --git a/dom/media/test/crashtests/1697521.html b/dom/media/test/crashtests/1697521.html
new file mode 100644
index 0000000000..b44c02e227
--- /dev/null
+++ b/dom/media/test/crashtests/1697521.html
@@ -0,0 +1,19 @@
+<html>
+<head>
+<script>
+async function boom() {
+ const canvas = document.createElement("canvas");
+ canvas.getContext("2d");
+ const recorder = new MediaRecorder(new MediaStream([
+ ...canvas.captureStream().getTracks(),
+ ...canvas.captureStream().getTracks(),
+ ]));
+ recorder.start();
+ recorder.requestData();
+}
+</script>
+</head>
+<body onload="boom()">
+</body>
+</html>
+
diff --git a/dom/media/test/crashtests/1708790.html b/dom/media/test/crashtests/1708790.html
new file mode 100644
index 0000000000..ee97a31c81
--- /dev/null
+++ b/dom/media/test/crashtests/1708790.html
@@ -0,0 +1,22 @@
+<html class="reftest-wait">
+<head>
+<script>
+(async _ => {
+ try {
+ const audio = document.createElement('audio');
+ audio.src = 'sound.ogg';
+ const ac = new AudioContext();
+ const src = ac.createMediaElementSource(audio);
+ src.connect(ac.destination);
+ await audio.play();
+ audio.pause();
+ audio.volume = 0.5;
+ audio.playbackRate = 0.5;
+ audio.preservesPitch = false;
+ } finally {
+ document.documentElement.removeAttribute("class")
+ }
+})()
+</script>
+</head>
+</html>
diff --git a/dom/media/test/crashtests/1709130.html b/dom/media/test/crashtests/1709130.html
new file mode 100644
index 0000000000..2c6d7dc690
--- /dev/null
+++ b/dom/media/test/crashtests/1709130.html
@@ -0,0 +1,19 @@
+<html class="reftest-wait">
+<head>
+<script>
+(async _ => {
+ const video = document.createElement("video");
+ video.mozCaptureStreamUntilEnded();
+ video.src = "test.mp4";
+ await video.play();
+ for (let i = 0; i < 23; i++) {
+ console.log("nop");
+ await video.seekToNextFrame();
+ video.removeAttribute("readonly");
+ }
+ setTimeout("self.close()", 100);
+ document.documentElement.removeAttribute("class");
+})();
+</script>
+</head>
+</html>
diff --git a/dom/media/test/crashtests/1734008.html b/dom/media/test/crashtests/1734008.html
new file mode 100644
index 0000000000..31c8d31dae
--- /dev/null
+++ b/dom/media/test/crashtests/1734008.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script>
+ window.addEventListener("load", async () => {
+ const object = document.createElementNS("http://www.w3.org/2000/svg", "foreignObject")
+ const video = document.createElementNS("http://www.w3.org/1999/xhtml", "video")
+ document.documentElement.appendChild(video)
+ video.setAttribute("src", "1734008.webm")
+ video.mozCaptureStream()
+ const xhr = new XMLHttpRequest()
+ xhr.open("POST", "FOOBAR", false)
+ xhr.send()
+ const document_0 = new Document()
+ const adopted = document_0.adoptNode(document.documentElement)
+ object.insertBefore(adopted, object.childNodes[(216679474 % object.childNodes.length)])
+ try { await video.play() } catch (e) {}
+ setTimeout("location.reload()", 500)
+ })
+ </script>
+</head>
+</html>
diff --git a/dom/media/test/crashtests/1734008.webm b/dom/media/test/crashtests/1734008.webm
new file mode 100644
index 0000000000..07d2052879
--- /dev/null
+++ b/dom/media/test/crashtests/1734008.webm
Binary files differ
diff --git a/dom/media/test/crashtests/1741677.html b/dom/media/test/crashtests/1741677.html
new file mode 100644
index 0000000000..0c442e830d
--- /dev/null
+++ b/dom/media/test/crashtests/1741677.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<html class="reftest-wait">
+<script>
+ const script = `
+ navigator.mediaCapabilities.decodingInfo({
+ type: 'file',
+ audio: { contentType: "audio/flac" }
+ }).then(postMessage("done"));
+ `;
+ let worker = new Worker(URL.createObjectURL(new Blob([script])));
+ // Stop test completion and crash from racing each other.
+ worker.onmessage = () => document.documentElement.removeAttribute("class");
+</script>
+</html>
diff --git a/dom/media/test/crashtests/1748272.html b/dom/media/test/crashtests/1748272.html
new file mode 100644
index 0000000000..53953e7c0e
--- /dev/null
+++ b/dom/media/test/crashtests/1748272.html
@@ -0,0 +1,12 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script>
+ window.addEventListener("load", () => {
+ const audio = new Audio();
+ const context = new AudioContext({ "sampleRate": 15772.7 })
+ context.createMediaElementSource(audio);
+ });
+ </script>
+</head>
+</html>
diff --git a/dom/media/test/crashtests/1752917.html b/dom/media/test/crashtests/1752917.html
new file mode 100644
index 0000000000..84752a174f
--- /dev/null
+++ b/dom/media/test/crashtests/1752917.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script>
+ window.addEventListener("load", () => {
+ const context = new AudioContext({})
+ const node = new DelayNode(context, {})
+ const abort = new AbortController()
+ let processor = context.createScriptProcessor(4096, 1, 26)
+ processor.addEventListener("audioprocess", () => {}, { "signal": abort.signal })
+ processor.connect(node.delayTime)
+ processor = undefined
+ SpecialPowers.forceGC()
+ abort.abort(undefined)
+ });
+ </script>
+</head>
+</html>
diff --git a/dom/media/test/crashtests/1762620.html b/dom/media/test/crashtests/1762620.html
new file mode 100644
index 0000000000..b4c705c84f
--- /dev/null
+++ b/dom/media/test/crashtests/1762620.html
@@ -0,0 +1,8 @@
+<audio id='fuzzed' controls>
+ <source src='fuzzed.wav'>
+</audio>
+<script>
+ var m = document.getElementById('fuzzed')
+ m.addEventListener('canplay', m.play, true)
+</script>
+
diff --git a/dom/media/test/crashtests/1765842.html b/dom/media/test/crashtests/1765842.html
new file mode 100644
index 0000000000..8b77ec998a
--- /dev/null
+++ b/dom/media/test/crashtests/1765842.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+<body>
+ <video src="1765842.webm" type="video/webm" autoplay="true" controls></video>
+</body>
+</html>
diff --git a/dom/media/test/crashtests/1765842.webm b/dom/media/test/crashtests/1765842.webm
new file mode 100644
index 0000000000..62eb5d779d
--- /dev/null
+++ b/dom/media/test/crashtests/1765842.webm
Binary files differ
diff --git a/dom/media/test/crashtests/1787281.html b/dom/media/test/crashtests/1787281.html
new file mode 100644
index 0000000000..e5586dbf2f
--- /dev/null
+++ b/dom/media/test/crashtests/1787281.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+</head>
+<body>
+ <video id=a src="1787281.mp4" autoplay="true" controls></video>
+ <script>
+ a.onended = function() {
+ document.documentElement.className = "";
+ }
+ </script>
+</body>
+</html>
diff --git a/dom/media/test/crashtests/1787281.mp4 b/dom/media/test/crashtests/1787281.mp4
new file mode 100644
index 0000000000..8481cf034c
--- /dev/null
+++ b/dom/media/test/crashtests/1787281.mp4
Binary files differ
diff --git a/dom/media/test/crashtests/1798778.html b/dom/media/test/crashtests/1798778.html
new file mode 100644
index 0000000000..300d84afa1
--- /dev/null
+++ b/dom/media/test/crashtests/1798778.html
@@ -0,0 +1,11 @@
+<html class="reftest-wait">
+<head>
+<audio id=a src="adts-truncated.aac" controls></audio>
+<script>
+ a.play();
+ a.onerror = function() {
+ document.documentElement.removeAttribute("class");
+ }
+</script>
+</head>
+</html>
diff --git a/dom/media/test/crashtests/1833894.mp4 b/dom/media/test/crashtests/1833894.mp4
new file mode 100644
index 0000000000..a5c20e4846
--- /dev/null
+++ b/dom/media/test/crashtests/1833894.mp4
Binary files differ
diff --git a/dom/media/test/crashtests/1833896.mp4 b/dom/media/test/crashtests/1833896.mp4
new file mode 100644
index 0000000000..6d40590c8a
--- /dev/null
+++ b/dom/media/test/crashtests/1833896.mp4
Binary files differ
diff --git a/dom/media/test/crashtests/1835164.html b/dom/media/test/crashtests/1835164.html
new file mode 100644
index 0000000000..702e082954
--- /dev/null
+++ b/dom/media/test/crashtests/1835164.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<audio id=a src=1835164.opus controls autoplay></audio>
+<script>
+a.onloadedmetadata = function() {
+ a.currentTime = 1e40;
+ a.play();
+}
+a.onplaying = function() {
+ document.documentElement.removeAttribute("class");
+}
+</script>
+</html>
diff --git a/dom/media/test/crashtests/1835164.opus b/dom/media/test/crashtests/1835164.opus
new file mode 100644
index 0000000000..124fdbfda1
--- /dev/null
+++ b/dom/media/test/crashtests/1835164.opus
Binary files differ
diff --git a/dom/media/test/crashtests/1840002.webm b/dom/media/test/crashtests/1840002.webm
new file mode 100644
index 0000000000..0ad6d513e7
--- /dev/null
+++ b/dom/media/test/crashtests/1840002.webm
Binary files differ
diff --git a/dom/media/test/crashtests/1845350.mp4 b/dom/media/test/crashtests/1845350.mp4
new file mode 100644
index 0000000000..e22af449c2
--- /dev/null
+++ b/dom/media/test/crashtests/1845350.mp4
Binary files differ
diff --git a/dom/media/test/crashtests/255ch.wav b/dom/media/test/crashtests/255ch.wav
new file mode 100644
index 0000000000..d9ee36d5bf
--- /dev/null
+++ b/dom/media/test/crashtests/255ch.wav
Binary files differ
diff --git a/dom/media/test/crashtests/459439-1.html b/dom/media/test/crashtests/459439-1.html
new file mode 100644
index 0000000000..7bb0131d51
--- /dev/null
+++ b/dom/media/test/crashtests/459439-1.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<script type="text/javascript">
+var i = 0;
+function boom()
+{
+ var div = document.getElementById("div");
+ var audio = document.getElementById("audio");
+
+ audio.onload = null;
+
+ div.textContent = "FAIL";
+ audio.src += "";
+ div.textContent = "PASS?";
+
+ ++i;
+
+ setTimeout(done, 1);
+}
+
+function done()
+{
+ // Note we reset 'src' to release decoder resources and cubeb streams to
+ // prevent OOM or OpenCubeb() failures.
+ var audio = document.getElementById("audio");
+ audio.src = "";
+ document.documentElement.removeAttribute("class");
+}
+</script>
+</head>
+<body>
+<audio id="audio" autoplay src="sound.ogg" oncanplaythrough="setTimeout(boom, 1);"></audio>
+<div id="div"></div>
+</body>
+</html>
diff --git a/dom/media/test/crashtests/466607-1.html b/dom/media/test/crashtests/466607-1.html
new file mode 100644
index 0000000000..e154223efe
--- /dev/null
+++ b/dom/media/test/crashtests/466607-1.html
@@ -0,0 +1,14 @@
+<html>
+<head>
+<script type="text/javascript">
+
+function boom()
+{
+ document.body.appendChild(document.createElementNS("bar", "audio"));
+ document.body.appendChild(document.createElementNS("bar", "video"));
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/dom/media/test/crashtests/466945-1.html b/dom/media/test/crashtests/466945-1.html
new file mode 100644
index 0000000000..ac1ba29e36
--- /dev/null
+++ b/dom/media/test/crashtests/466945-1.html
@@ -0,0 +1,25 @@
+<html class="reftest-wait">
+<head>
+<script type="text/javascript">
+
+var s;
+
+function boom()
+{
+ s = document.createElement("span");
+ s.innerHTML = "<video src='data:text/html,' autoplay='autoplay'><\/video>";
+ document.body.appendChild(document.createElement("iframe"));
+ setTimeout(boom2, 0);
+}
+
+function boom2()
+{
+ s.innerHTML = "";
+ document.documentElement.removeAttribute("class");
+}
+
+</script>
+</head>
+
+<body onload="setTimeout(boom, 0);"></body>
+</html>
diff --git a/dom/media/test/crashtests/468763-1.html b/dom/media/test/crashtests/468763-1.html
new file mode 100644
index 0000000000..d21e5bc388
--- /dev/null
+++ b/dom/media/test/crashtests/468763-1.html
@@ -0,0 +1 @@
+<html><head></head><body><video src="nosuchprotocol:"></video></body></html> \ No newline at end of file
diff --git a/dom/media/test/crashtests/474744-1.html b/dom/media/test/crashtests/474744-1.html
new file mode 100644
index 0000000000..8a7c70cf05
--- /dev/null
+++ b/dom/media/test/crashtests/474744-1.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script type="text/javascript">
+
+function boom()
+{
+ document.body.innerHTML += "<video src='aim:yaz'><\/video>";
+}
+
+</script>
+</head>
+<body onload="boom();">
+</body>
+</html>
diff --git a/dom/media/test/crashtests/481136-1.html b/dom/media/test/crashtests/481136-1.html
new file mode 100644
index 0000000000..bd264058d9
--- /dev/null
+++ b/dom/media/test/crashtests/481136-1.html
@@ -0,0 +1,3 @@
+<html>
+<div><object data='sound.ogg'></div>
+</html>
diff --git a/dom/media/test/crashtests/492286-1.xhtml b/dom/media/test/crashtests/492286-1.xhtml
new file mode 100644
index 0000000000..627ac38723
--- /dev/null
+++ b/dom/media/test/crashtests/492286-1.xhtml
@@ -0,0 +1 @@
+<source xmlns="http://www.w3.org/1999/xhtml"/> \ No newline at end of file
diff --git a/dom/media/test/crashtests/493915-1.html b/dom/media/test/crashtests/493915-1.html
new file mode 100644
index 0000000000..2a6ae9bd6c
--- /dev/null
+++ b/dom/media/test/crashtests/493915-1.html
@@ -0,0 +1,18 @@
+<html>
+<head>
+<script type="text/javascript">
+
+function boom()
+{
+ s = document.createElement("span");
+ a = document.createElement("audio");
+ a['src'] = "javascript:4";
+ a['loopend'] = 3;
+ s.appendChild(a);
+}
+
+</script>
+</head>
+
+<body onload="boom();"></body>
+</html>
diff --git a/dom/media/test/crashtests/495794-1.html b/dom/media/test/crashtests/495794-1.html
new file mode 100644
index 0000000000..2db69206a1
--- /dev/null
+++ b/dom/media/test/crashtests/495794-1.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+ <body>
+ <audio src="495794-1.ogg" autoplay onended="this.src=''; document.documentElement.className=undefined"></audio>
+ <!-- Note we reset 'src' to release decoder resources and cubeb streams to prevent OOM or OpenCubeb() failures. -->
+ </body>
+</html>
+
diff --git a/dom/media/test/crashtests/495794-1.ogg b/dom/media/test/crashtests/495794-1.ogg
new file mode 100644
index 0000000000..1c19a64061
--- /dev/null
+++ b/dom/media/test/crashtests/495794-1.ogg
Binary files differ
diff --git a/dom/media/test/crashtests/497734-1.xhtml b/dom/media/test/crashtests/497734-1.xhtml
new file mode 100644
index 0000000000..6df055da39
--- /dev/null
+++ b/dom/media/test/crashtests/497734-1.xhtml
@@ -0,0 +1,21 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<script type="text/javascript">
+
+function boom()
+{
+
+ div = document.createElementNS("http://www.w3.org/1999/xhtml", "div");
+ div.appendChild(document.getElementById("v"));
+ document.body.appendChild(div);
+}
+
+</script>
+</head>
+
+<body onload="boom();">
+
+<video id="v"><source></source></video>
+
+</body>
+</html>
diff --git a/dom/media/test/crashtests/497734-2.html b/dom/media/test/crashtests/497734-2.html
new file mode 100644
index 0000000000..990ac4af46
--- /dev/null
+++ b/dom/media/test/crashtests/497734-2.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<script>
+
+function boom()
+{
+ audio1 = document.createElement("audio");
+ (audio1).appendChild(document.createElement("source"));
+ (audio1).appendChild(document.createElement("source"));
+ setTimeout(function() {
+ audio2 = document.createElement("audio");
+ audio2.appendChild(audio1);
+ }, 100);
+}
+
+</script>
+<body onload="boom();"></body>
diff --git a/dom/media/test/crashtests/576612-1.html b/dom/media/test/crashtests/576612-1.html
new file mode 100644
index 0000000000..04f993e780
--- /dev/null
+++ b/dom/media/test/crashtests/576612-1.html
@@ -0,0 +1,15 @@
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+<script>
+function boom()
+{
+
+ var v = document.getElementById("v");
+ v.src = "data:text/plain,_";
+ document.documentElement.appendChild(v);
+
+}
+</script>
+</head>
+<body onload="boom();"><video id="v" src="data:video/ogg;codecs=&quot;theora,vorbis&quot;,1"></video></body>
+</html>
diff --git a/dom/media/test/crashtests/691096-1.html b/dom/media/test/crashtests/691096-1.html
new file mode 100644
index 0000000000..3c3ebfc12a
--- /dev/null
+++ b/dom/media/test/crashtests/691096-1.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<script>
+var ITERATIONS = 200;
+
+function stoptest (evt)
+{
+ if (evt) {
+ // Note we reset 'src' to release decoder resources and cubeb streams to
+ // prevent OOM or OpenCubeb() failures.
+ evt.target.src = "";
+ }
+ document.documentElement.removeAttribute("class");
+}
+
+function boom()
+{
+ for (var i = 0; i < ITERATIONS; ++i) {
+ a = document.createElementNS("http://www.w3.org/1999/xhtml", "audio");
+ a.addEventListener("loadedmetadata", stoptest);
+ a.setAttributeNS(null, "autoplay", "");
+ a.setAttributeNS(null, "src", "sound.ogg");
+ }
+ setTimeout(stoptest, 250);
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/dom/media/test/crashtests/752784-1.html b/dom/media/test/crashtests/752784-1.html
new file mode 100644
index 0000000000..4644eeb89a
--- /dev/null
+++ b/dom/media/test/crashtests/752784-1.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+function boom()
+{
+ document.getElementById("a").mozCaptureStream();
+}
+</script>
+</head>
+
+<body onload="boom();">
+<audio id="a" src="sound.ogg">
+</body>
+</html>
diff --git a/dom/media/test/crashtests/789075-1.html b/dom/media/test/crashtests/789075-1.html
new file mode 100644
index 0000000000..6cd673fb75
--- /dev/null
+++ b/dom/media/test/crashtests/789075-1.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+</head>
+<body>
+<video src="789075.webm" preload="metadata" id="v">
+</video>
+<!-- Note we reset 'src' to release decoder resources and cubeb streams to prevent OOM or OpenCubeb() failures. -->
+<script type="application/javascript">
+ var video = document.getElementById("v");
+ video.onloadeddata = function () {
+ video.play();
+ };
+ video.onended = function () {
+ video.src="";
+ document.documentElement.className = undefined;
+ };
+</script>
+</body>
+</html>
diff --git a/dom/media/test/crashtests/789075.webm b/dom/media/test/crashtests/789075.webm
new file mode 100644
index 0000000000..602e53fca2
--- /dev/null
+++ b/dom/media/test/crashtests/789075.webm
Binary files differ
diff --git a/dom/media/test/crashtests/795892-1.html b/dom/media/test/crashtests/795892-1.html
new file mode 100644
index 0000000000..d73cea7f2e
--- /dev/null
+++ b/dom/media/test/crashtests/795892-1.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<script>
+function boom()
+{
+ var a = document.getElementById("a");
+ a.play();
+ a.onplaying = function () {
+ a.onplaying = null;
+ // Note we reset 'src' to release decoder resources and cubeb streams to
+ // prevent OOM or OpenCubeb() failures.
+ a.src = "";
+ document.documentElement.className = "";
+ }
+}
+</script>
+</head>
+
+<body>
+<video id="a" src="cors.webm" crossorigin preload="metadata" onloadedmetadata="boom();">
+</body>
+</html>
diff --git a/dom/media/test/crashtests/844563.html b/dom/media/test/crashtests/844563.html
new file mode 100644
index 0000000000..a3cb91838d
--- /dev/null
+++ b/dom/media/test/crashtests/844563.html
@@ -0,0 +1,5 @@
+<script>
+var a = document.createElementNS("http://www.w3.org/1999/xhtml", "audio");
+a.preservesPitch = a;
+</script>
+
diff --git a/dom/media/test/crashtests/846612.html b/dom/media/test/crashtests/846612.html
new file mode 100644
index 0000000000..070c375381
--- /dev/null
+++ b/dom/media/test/crashtests/846612.html
@@ -0,0 +1,8 @@
+<script>
+document.addEventListener("DOMContentLoaded", function() {
+ var a = document.querySelector("audio");
+ a.playbackRate = 2;
+ a.play();
+});
+</script>
+<audio src="495794-1.ogg"></audio>
diff --git a/dom/media/test/crashtests/852838.html b/dom/media/test/crashtests/852838.html
new file mode 100644
index 0000000000..0bea29351b
--- /dev/null
+++ b/dom/media/test/crashtests/852838.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <script>
+ var o0 = new window.AudioContext();
+ var o1 = o0.createBuffer(536870912, 1, 8192);
+ </script>
+ </head>
+ <body>
+ </body>
+</html>
diff --git a/dom/media/test/crashtests/865004.html b/dom/media/test/crashtests/865004.html
new file mode 100644
index 0000000000..4da39071c4
--- /dev/null
+++ b/dom/media/test/crashtests/865004.html
@@ -0,0 +1,19 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<meta charset="UTF-8">
+<script>
+
+function boom()
+{
+ var ac = new AudioContext();
+ for (var j = 0; j < 200; ++j) {
+ ac.createScriptProcessor(undefined);
+ }
+ document.documentElement.removeAttribute("class");
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/dom/media/test/crashtests/865537-1.html b/dom/media/test/crashtests/865537-1.html
new file mode 100644
index 0000000000..4065eb3608
--- /dev/null
+++ b/dom/media/test/crashtests/865537-1.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<html>
+<body onload="doTest()">
+<base href="../unknown">
+<div id="test3"></div>
+<video id="test4"><source src="white.webm"></video>
+<script>
+function doTest() {
+ test3.appendChild(test4);
+}
+</script>
+</body>
+</html>
diff --git a/dom/media/test/crashtests/865550.html b/dom/media/test/crashtests/865550.html
new file mode 100644
index 0000000000..b8626e8d67
--- /dev/null
+++ b/dom/media/test/crashtests/865550.html
@@ -0,0 +1,22 @@
+<html class="reftest-wait">
+ <head>
+ <script>
+ var i = 0;
+ var interval;
+ function crash() {
+ var o0 = new AudioContext();
+ o1 = o0.createBufferSource();
+ ++i;
+ if (i == 2000) {
+ document.documentElement.removeAttribute("class");
+ clearInterval(interval);
+ }
+ }
+ function start() {
+ interval = setInterval("crash()", 0)
+ }
+ </script>
+ </head>
+ <body onload="start()">
+ </body>
+</html>
diff --git a/dom/media/test/crashtests/868504.html b/dom/media/test/crashtests/868504.html
new file mode 100644
index 0000000000..94090c1c09
--- /dev/null
+++ b/dom/media/test/crashtests/868504.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ new AudioContext().createBufferSource().playbackRate.linearRampToValueAtTime(0, -1);
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/dom/media/test/crashtests/874869.html b/dom/media/test/crashtests/874869.html
new file mode 100644
index 0000000000..1fe3dbd2fc
--- /dev/null
+++ b/dom/media/test/crashtests/874869.html
@@ -0,0 +1,15 @@
+<script>
+var Context0= new AudioContext()
+var Panner0=Context0.createPanner();
+var BufferSource3=Context0.createBufferSource();
+Panner0.channelCount=0;
+BufferSource3.connect(Panner0);
+BufferSource3.buffer=function(){
+ var length=1;
+ var Buffer=Context0.createBuffer(1,length,'44200');
+ var bufferData= Buffer.getChannelData(0);
+ for (var i = 0; i < length; ++i) { bufferData[i] = 255;};
+ return Buffer;
+}();
+
+</script> \ No newline at end of file
diff --git a/dom/media/test/crashtests/874915.html b/dom/media/test/crashtests/874915.html
new file mode 100644
index 0000000000..59218b3da3
--- /dev/null
+++ b/dom/media/test/crashtests/874915.html
@@ -0,0 +1,24 @@
+<script>
+var Context0= new AudioContext()
+var BufferSource6=Context0.createBufferSource();
+
+setInterval(function(){
+BufferSource6.buffer=function(){
+ var length=11283;
+ var Buffer=Context0.createBuffer(1,length,Context0.sampleRate);
+ var bufferData= Buffer.getChannelData(0);
+ for (var i = 0; i < length; ++i) { bufferData[i] = Math.sin(i*(624))};
+ return Buffer;
+}();
+},0)
+
+BufferSource6.start(0.15831333969254047,0.23571860056836158,0.529235512483865);
+
+BufferSource6.buffer=function(){
+ var length=48517;
+ var Buffer=Context0.createBuffer(1,length,Context0.sampleRate);
+ var bufferData= Buffer.getChannelData(0);
+ for (var i = 0; i < length; ++i) { bufferData[i] = Math.sin(i*(365))};
+ return Buffer;
+}();
+</script> \ No newline at end of file
diff --git a/dom/media/test/crashtests/874934.html b/dom/media/test/crashtests/874934.html
new file mode 100644
index 0000000000..350ce28101
--- /dev/null
+++ b/dom/media/test/crashtests/874934.html
@@ -0,0 +1,23 @@
+<script>
+var Context0= new AudioContext()
+var BufferSource0=Context0.createBufferSource();
+BufferSource0.start(0.01932738965842873,0.33345631847623736,0.3893404237460345);
+BufferSource0.buffer=function(){
+ var length=35887;
+ var Buffer=Context0.createBuffer(1,length,Context0.sampleRate);
+ var bufferData= Buffer.getChannelData(0);
+ for (var i = 0; i < length; ++i) { bufferData[i] = Math.sin(i*(0.39765272522345185))};
+ return Buffer;
+}();
+
+BufferSource0.buffer=function(){
+ var length=15952;
+ var Buffer=Context0.createBuffer(1,length,Context0.sampleRate);
+ var bufferData= Buffer.getChannelData(0);
+ for (var i = 0; i < length; ++i) { bufferData[i] = Math.sin(i*(85))};
+ return Buffer;
+}();
+
+BufferSource0.playbackRate.value=20.401213286832185;
+
+</script>
diff --git a/dom/media/test/crashtests/874952.html b/dom/media/test/crashtests/874952.html
new file mode 100644
index 0000000000..a9a398fdb2
--- /dev/null
+++ b/dom/media/test/crashtests/874952.html
@@ -0,0 +1,11 @@
+<script>
+var Context0= new AudioContext()
+var ChannelSplitter0=Context0.createChannelSplitter();
+var BiquadFilter0=Context0.createBiquadFilter();
+var WaveShaper0=Context0.createWaveShaper();
+
+ChannelSplitter0.connect(BiquadFilter0,3,0);
+ChannelSplitter0.connect(WaveShaper0);
+BiquadFilter0.disconnect();
+WaveShaper0.connect(ChannelSplitter0);
+</script>
diff --git a/dom/media/test/crashtests/875144.html b/dom/media/test/crashtests/875144.html
new file mode 100644
index 0000000000..bf5d0d0861
--- /dev/null
+++ b/dom/media/test/crashtests/875144.html
@@ -0,0 +1,81 @@
+<script>
+Logger = {}
+Logger.error = function(e) {}
+Logger.comment = function(e) {}
+
+try { o0 = document.createElement('audio'); } catch(e) { Logger.error(Logger.comment(e)); }
+try { (document.body || document.documentElement).appendChild(o0); } catch(e) { Logger.error(Logger.comment(e)); }
+try { o1 = new AudioContext(); } catch(e) { Logger.error(Logger.comment(e)); }
+try { o2 = o1.createGain(); } catch(e) { Logger.error(Logger.comment(e)); }
+try { o3 = o1.createBufferSource(); } catch(e) { Logger.error(Logger.comment(e)); }
+try { o3.buffer = function() { o4 = o1.createBuffer(1, 3, 52970);
+o5 = o4.getChannelData(0);
+for(var i=0; i<3; ++i) {
+o5[i] = Math.sin(i * 63);
+}
+return o4;
+}(); } catch(e) { Logger.error(Logger.comment(e)); }
+try { o3.buffer = function() { o6 = o1.createBuffer(1, 15, 41218);
+o7 = o6.getChannelData(0);
+for(var i=0; i<15; ++i) {
+o7[i] = Math.sin(i * 0);
+}
+return o6;
+}(); } catch(e) { Logger.error(Logger.comment(e)); }
+try { o3.buffer = function() { o8 = o1.createBuffer(1, 0, 49074);
+o9 = o8.getChannelData(0);
+for(var i=0; i<0; ++i) {
+o9[i] = Math.sin(i * 0);
+}
+return o8;
+}(); } catch(e) { Logger.error(Logger.comment(e)); }
+try { o3.buffer = function() { o10 = o1.createBuffer(1, 31, 86527);
+o11 = o10.getChannelData(0);
+for(var i=0; i<31; ++i) {
+o11[i] = Math.sin(i * 127);
+}
+return o10;
+}(); } catch(e) { Logger.error(Logger.comment(e)); }
+try { o3.stop(-1) } catch(e) { Logger.error(Logger.comment(e)); }
+/* [Exception... "An attempt was made to use an object that is not, or is no longer, usable" code: "11" nsresult: "0x8053000b (InvalidStateError)" location: "file:///Users/cdiehl/dev/projects/peach/Peach/Utilities/JS/undefined.js Line: 602"] */
+try { o3.channelCountMode = 'explicit'; } catch(e) { Logger.error(Logger.comment(e)); }
+try { o12 = o1.createBiquadFilter(); } catch(e) { Logger.error(Logger.comment(e)); }
+try { o3.buffer = function() { o13 = o1.createBuffer(1, 63, 28347);
+o14 = o13.getChannelData(0);
+for(var i=0; i<63; ++i) {
+o14[i] = Math.sin(i * 15);
+}
+return o13;
+}(); } catch(e) { Logger.error(Logger.comment(e)); }
+try { o12.channelCount = 1; } catch(e) { Logger.error(Logger.comment(e)); }
+try { o12.connect(GainNode, 65536, 0) } catch(e) { Logger.error(Logger.comment(e)); }
+/* TypeError: Value does not implement interface AudioNode. */
+try { o3.buffer = function() { o15 = o1.createBuffer(1, 1, 72540);
+o16 = o15.getChannelData(0);
+for(var i=0; i<1; ++i) {
+o16[i] = Math.sin(i * 7);
+}
+return o15;
+}(); } catch(e) { Logger.error(Logger.comment(e)); }
+try { o12.getFrequencyResponse(new Float32Array(7), new Float32Array(127), new Float32Array(7)) } catch(e) { Logger.error(Logger.comment(e)); }
+try { o12.getFrequencyResponse(new Float32Array(15), new Float32Array(127), new Float32Array(7)) } catch(e) { Logger.error(Logger.comment(e)); }
+try { o17 = document.createElement('audio'); } catch(e) { Logger.error(Logger.comment(e)); }
+try { (document.body || document.documentElement).appendChild(o0); } catch(e) { Logger.error(Logger.comment(e)); }
+try { o3.buffer = function() { o18 = o1.createBuffer(1, 7, 91261);
+o19 = o18.getChannelData(0);
+for(var i=0; i<7; ++i) {
+o19[i] = Math.sin(i * 7);
+}
+return o18;
+}(); } catch(e) { Logger.error(Logger.comment(e)); }
+try { o12.getFrequencyResponse(new Float32Array(31), new Float32Array(31), new Float32Array(127)) } catch(e) { Logger.error(Logger.comment(e)); }
+try { o20 = o1.createChannelSplitter(1, 2, 4, 16, 32); } catch(e) { Logger.error(Logger.comment(e)); }
+try { o12.channelCountMode = 'explicit'; } catch(e) { Logger.error(Logger.comment(e)); }
+try { o3.buffer = function() { o21 = o1.createBuffer(1, 0, 14451);
+o22 = o21.getChannelData(0);
+for(var i=0; i<0; ++i) {
+o22[i] = Math.sin(i * 63);
+}
+return o21;
+}(); } catch(e) { Logger.error(Logger.comment(e)); }
+</script>
diff --git a/dom/media/test/crashtests/875596.html b/dom/media/test/crashtests/875596.html
new file mode 100644
index 0000000000..7bac09dab7
--- /dev/null
+++ b/dom/media/test/crashtests/875596.html
@@ -0,0 +1,12 @@
+<script>
+try { o1 = new AudioContext(); } catch(e) { }
+try { o6 = o1.createGain(); } catch(e) { }
+try { o8 = o1.createBufferSource(); } catch(e) { }
+try { o6.gain.setValueCurveAtTime(new Float32Array(7), 0, 0) } catch(e) { }
+try { o8.connect(o6, 0, 0) } catch(e) { }
+try { o8.buffer = function() {
+ o19 = o1.createBuffer(1, 1, 76309);
+ for(var i=0; i<1; ++i) { }
+ return o19;
+}(); } catch(e) { }
+</script>
diff --git a/dom/media/test/crashtests/875911.html b/dom/media/test/crashtests/875911.html
new file mode 100644
index 0000000000..fbc52642b4
--- /dev/null
+++ b/dom/media/test/crashtests/875911.html
@@ -0,0 +1,3 @@
+<script>
+ new OfflineAudioContext(1, 10, 48000);
+</script>
diff --git a/dom/media/test/crashtests/876024-1.html b/dom/media/test/crashtests/876024-1.html
new file mode 100644
index 0000000000..5502d8e42d
--- /dev/null
+++ b/dom/media/test/crashtests/876024-1.html
@@ -0,0 +1,5 @@
+<script>
+o1 = new window.AudioContext(2, 8, 44100);
+o4 = o1.createBiquadFilter();
+o4.detune.setValueAtTime(0.0843, 1.7976931348623157e+308);
+</script>
diff --git a/dom/media/test/crashtests/876024-2.html b/dom/media/test/crashtests/876024-2.html
new file mode 100644
index 0000000000..4b9beb7453
--- /dev/null
+++ b/dom/media/test/crashtests/876024-2.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<script>
+
+function boom()
+{
+ var bufferSource = new AudioContext().createScriptProcessor().context.createBufferSource();
+ bufferSource.start(0, 0, 0);
+ bufferSource.stop(562949953421313);
+}
+
+</script></head>
+
+<body onload="boom();"></body>
+</html>
diff --git a/dom/media/test/crashtests/876118.html b/dom/media/test/crashtests/876118.html
new file mode 100644
index 0000000000..bc0630350a
--- /dev/null
+++ b/dom/media/test/crashtests/876118.html
@@ -0,0 +1,16 @@
+<script>
+var Context0= new AudioContext()
+var BufferSource7=Context0.createBufferSource();
+
+BufferSource7.connect(Context0.destination);
+BufferSource7.buffer=function(){
+ var length=7;
+ var Buffer=Context0.createBuffer(1,length,Context0.sampleRate);
+ var bufferData= Buffer.getChannelData(0);for (var i = 0; i < length; ++i) { bufferData[i] = Math.sin(i*(-1))};
+ return Buffer;
+}();
+
+
+Context0.destination.channelCountMode="explicit";
+Context0.destination.channelCount=519910189000000;
+</script>
diff --git a/dom/media/test/crashtests/876207.html b/dom/media/test/crashtests/876207.html
new file mode 100644
index 0000000000..7bfcb096b2
--- /dev/null
+++ b/dom/media/test/crashtests/876207.html
@@ -0,0 +1,30 @@
+<script>
+try { o1 = new window.OfflineAudioContext(1, 10, 44100); } catch(e) { }
+try { o2 = o1.createChannelSplitter(1); } catch(e) { }
+try { o3 = o1.createAnalyser(); } catch(e) { }
+try { o4 = o1.createWaveShaper(); } catch(e) { }
+try { o5 = o1.createChannelSplitter(6); } catch(e) { }
+try { o6 = o1.createAnalyser(); } catch(e) { }
+try { o7 = o1.createBufferSource(); } catch(e) { }
+try { o7.connect(o1.destination); } catch(e) { }
+try { o7.buffer = function() {
+o8 = o1.createBuffer(1, 3, o1.sampleRate);
+o9 = o8.getChannelData(0);
+for(var i = 0; i < 3; ++i) {
+o9[i] = Math.sin(i * 127);
+}
+return o8;
+}()
+; } catch(e) { }
+try { o7.playbackRate.cancelScheduledValues(0) } catch(e) { }
+try { o7.channelInterpretation = 'speakers'; } catch(e) { }
+try { o1.destination.channelInterpretation = 'speakers'; } catch(e) { }
+try { o5.connect(o1.destination, 1, 1) } catch(e) { }
+try { o1.listener.setOrientation(0, 1073741824, 1073741824, 4194304, 1, 0) } catch(e) { }
+try { o4.curve = new Float32Array(127); } catch(e) { }
+try { o6.getByteFrequencyData(new Uint8Array(12)) } catch(e) { }
+try { o6.disconnect() } catch(e) { }
+try { o1.destination.channelCount = 33554432; } catch(e) { }
+try { o7.playbackRate.setTargetAtTime(0, 8388608, 1) } catch(e) { }
+try { o6.getByteTimeDomainData(new Uint8Array(12)) } catch(e) { }
+</script>
diff --git a/dom/media/test/crashtests/876215.html b/dom/media/test/crashtests/876215.html
new file mode 100644
index 0000000000..07135e3628
--- /dev/null
+++ b/dom/media/test/crashtests/876215.html
@@ -0,0 +1,14 @@
+<script>
+try { o1 = new window.OfflineAudioContext(1, 10, 44100); } catch(e) { }
+try { o6 = o1.createScriptProcessor(0, 0, 2); } catch(e) { }
+try { o8 = o1.createBufferSource(); } catch(e) { }
+try { o8.connect(o1.destination); } catch(e) { }
+try { o8.connect(o6) } catch(e) { }
+try { o3.disconnect() } catch(e) { }
+try { o8.buffer = function() {
+o21 = o1.createBuffer(1, 3, o1.sampleRate);
+o22 = o21.getChannelData(0);
+return o21;
+}()
+; } catch(e) { }
+</script>
diff --git a/dom/media/test/crashtests/876249.html b/dom/media/test/crashtests/876249.html
new file mode 100644
index 0000000000..6f72b40bc1
--- /dev/null
+++ b/dom/media/test/crashtests/876249.html
@@ -0,0 +1,27 @@
+<script>
+var Context0= new AudioContext()
+var BufferSource0=Context0.createBufferSource();
+var BiquadFilter0=Context0.createBiquadFilter();
+BufferSource0.start(0,0.6167310480959713,0.7142638498917222);
+BiquadFilter0.connect(Context0.destination);
+BufferSource0.buffer=function(){
+ var length=86333;
+ var Buffer=Context0.createBuffer(1,length,Context0.sampleRate);
+ var bufferData= Buffer.getChannelData(0);
+ for (var i = 0; i < length; ++i) { bufferData[i] = Math.sin(i*(-1))};
+ return Buffer;
+}();
+
+BufferSource0.connect(BiquadFilter0);
+
+BufferSource0.buffer=function(){
+ var length=21989;
+ var Buffer=Context0.createBuffer(1,length,Context0.sampleRate);
+ var bufferData= Buffer.getChannelData(0);
+ for (var i = 0; i < length; ++i) { bufferData[i] = Math.sin(i*(0))};
+ return Buffer;
+}();
+
+BufferSource0.stop(0.04184641852043569);
+
+</script>
diff --git a/dom/media/test/crashtests/876252.html b/dom/media/test/crashtests/876252.html
new file mode 100644
index 0000000000..cb9cb63ed3
--- /dev/null
+++ b/dom/media/test/crashtests/876252.html
@@ -0,0 +1,23 @@
+<script>
+var Context0= new AudioContext()
+var BufferSource4=Context0.createBufferSource();
+BufferSource4.start(0.05386466556228697,0.397192713804543,0.48810303467325866);
+
+BufferSource4.buffer=function(){
+ var length=109076;
+ var Buffer=Context0.createBuffer(1,length,Context0.sampleRate);
+ var bufferData= Buffer.getChannelData(0);
+ for (var i = 0; i < length; ++i) { bufferData[i] = Math.sin(i*(370))};
+ return Buffer;
+}();
+
+BufferSource4.buffer=function(){
+ var length=19339;
+ var Buffer=Context0.createBuffer(1,length,53362);
+ var bufferData= Buffer.getChannelData(0);
+ for (var i = 0; i < length; ++i) { bufferData[i] = Math.sin(i*(-0.16235407581552863))};
+ return Buffer;
+}();
+
+BufferSource4.stop(0.46482366253621876);
+</script>
diff --git a/dom/media/test/crashtests/876834.html b/dom/media/test/crashtests/876834.html
new file mode 100644
index 0000000000..f4ca6ee558
--- /dev/null
+++ b/dom/media/test/crashtests/876834.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<script>
+new OfflineAudioContext(0, 0, 3229622);
+</script>
diff --git a/dom/media/test/crashtests/877527.html b/dom/media/test/crashtests/877527.html
new file mode 100644
index 0000000000..c639d501b7
--- /dev/null
+++ b/dom/media/test/crashtests/877527.html
@@ -0,0 +1,37 @@
+<script>
+try { o1 = new window.AudioContext(2, 5, 44100); } catch(e) { }
+try { o2 = o1.createChannelMerger(1); } catch(e) { }
+try { o3 = o1.createDelay(10); } catch(e) { }
+try { o4 = o1.createBuffer(2, 2048, 8000); } catch(e) { }
+try { o5 = o1.createPanner(); } catch(e) { }
+try { o6 = o1.createBufferSource(); } catch(e) { }
+try { o7 = (function() {
+var buf = o1.createBuffer(1, 50000, o1.sampleRate);
+for(var j=0; j<1; ++j) {
+for(var i=0; i<50000; ++i) { buf.getChannelData(j)[i] = Math.sin(i * (9.8));}
+}
+return buf;
+})(); } catch(e) { }
+try { o6.buffer = o7; } catch(e) { }
+try { o6.connect(o5); } catch(e) { }
+try { o5.connect(o1.destination); } catch(e) { }
+try { o1.listener.speedOfSound = 0.0000019073486328125; } catch(e) { }
+try { o6.loop = true; } catch(e) { }
+try { o8 = (function() {
+var buf = o1.createBuffer(2, 1000, o1.sampleRate);
+for(var j=0; j<2; ++j) {
+for(var i=0; i<1000; ++i) { buf.getChannelData(j)[i] = Math.sin(i * (1));}
+}
+return buf;
+})(); } catch(e) { }
+try { o6.buffer = o7; } catch(e) { }
+try { o6.connect(o5); } catch(e) { }
+try { o5.connect(o1.destination); } catch(e) { }
+try { o6.loopEnd = 1.4901161193847656e-8; } catch(e) { }
+try { o6.connect(o1.destination); } catch(e) { }
+try { o6.buffer = o8; } catch(e) { }
+try { o5.setPosition(0.36, o1.destination.context.destination.channelCountMode, o1.destination.context.destination.channelInterpretation) } catch(e) { }
+try { o2.channelCountMode = 'explicit'; } catch(e) { }
+try { o1.listener.speedOfSound = 4; } catch(e) { }
+try { o1.startRendering(); } catch(e) { }
+</script>
diff --git a/dom/media/test/crashtests/877820.html b/dom/media/test/crashtests/877820.html
new file mode 100644
index 0000000000..6d65c1e9d9
--- /dev/null
+++ b/dom/media/test/crashtests/877820.html
@@ -0,0 +1,4 @@
+<script>
+o1 = new window.OfflineAudioContext(2, 5, 0.39);
+window.location.reload();
+</script> \ No newline at end of file
diff --git a/dom/media/test/crashtests/878014.html b/dom/media/test/crashtests/878014.html
new file mode 100644
index 0000000000..4a0ca6dce2
--- /dev/null
+++ b/dom/media/test/crashtests/878014.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+try { o2 = new window.AudioContext(1, 15, 44100); } catch(e) { }
+try { o5 = o2.createDelay(0.1); } catch(e) { }
+try { o6 = o2.createPanner(); } catch(e) { }
+try { o8 = o2.createBufferSource(); } catch(e) { }
+try { o9 = (function() {
+var buf = o2.createBuffer(1, 12134, o2.sampleRate);
+for(var j=0; j<1; ++j) {
+for(var i=0; i<12134; ++i) { buf.getChannelData(j)[i] = Math.sin(i * (-127));}
+}
+return buf;
+})(); } catch(e) { }
+try { o8.buffer = o9; } catch(e) { }
+try { o8.connect(o6); } catch(e) { }
+try { o6.connect(o5, 0, 0) } catch(e) { }
+try { o8.buffer = (function() {
+var buf = o2.createBuffer(1, 5409, o2.sampleRate);
+for(var j=0; j<1; ++j) {
+for(var i=0; i<5409; ++i) { buf.getChannelData(j)[i] = Math.sin(i * (-1));}
+}
+return buf;
+})(); } catch(e) { }
+setTimeout(function() { try { o5.delayTime.setValueAtTime(0.78, 121560862.56366833); } catch(e) { } setTimeout(done, 0); },128)
+try { o5.delayTime.value = 0.4283; } catch(e) { }
+
+function done() {
+ document.documentElement.removeAttribute("class");
+}
+</script>
diff --git a/dom/media/test/crashtests/878328.html b/dom/media/test/crashtests/878328.html
new file mode 100644
index 0000000000..ec7b39871b
--- /dev/null
+++ b/dom/media/test/crashtests/878328.html
@@ -0,0 +1,5 @@
+<script>
+o1 = new window.AudioContext(2, 7, 44100);
+o5 = o1.createDelay(1);
+o5.delayTime.setValueCurveAtTime(new Float32Array(3), 1.7976931348623157e+308, 0.64);
+</script> \ No newline at end of file
diff --git a/dom/media/test/crashtests/878407.html b/dom/media/test/crashtests/878407.html
new file mode 100644
index 0000000000..ae2918dc85
--- /dev/null
+++ b/dom/media/test/crashtests/878407.html
@@ -0,0 +1,11 @@
+<script>
+o1 = new window.AudioContext();
+o4 = o1.createDelay(0.4468);
+o6 = o1.createPanner();
+o7 = (function() {var buf = o1.createBuffer(1, 1000, o1.sampleRate);for(var j=0; j<1; ++j) {for(var i=0; i<1000; ++i) {buf.getChannelData(j)[i] = Math.sin(i * (-15));}}return buf;})();
+o8 = o1.createBufferSource();
+o8.buffer = o7;
+o8.connect(o6);
+o6.connect(o4)
+o4.delayTime.setValueAtTime(0.6019893103749466289898, 0.26)
+</script>
diff --git a/dom/media/test/crashtests/878478.html b/dom/media/test/crashtests/878478.html
new file mode 100644
index 0000000000..89a47ddb55
--- /dev/null
+++ b/dom/media/test/crashtests/878478.html
@@ -0,0 +1,30 @@
+<script>
+var Context0= new AudioContext()
+var BufferSource0=Context0.createBufferSource();
+
+BufferSource0.loop=true;
+
+BufferSource0.buffer=function(){
+ var channels=3;
+ var length=97252;
+ var Buffer=Context0.createBuffer(channels,length,Context0.sampleRate);
+ for(var y=0;y<channels;y++){
+ var bufferData= Buffer.getChannelData(y);
+ for (var i = 0; i < length; ++i) { bufferData[i] = 1;}
+ };
+ return Buffer;
+}();
+
+BufferSource0.playbackRate.cancelScheduledValues(0.4);
+
+BufferSource0.loopEnd=5e-324;
+
+BufferSource0.buffer=function(){
+ var length=26590;
+ var Buffer=Context0.createBuffer(1,length,Context0.sampleRate);
+ var bufferData= Buffer.getChannelData(0);
+ for (var i = 0; i < length; ++i) { bufferData[i] = 1};
+ return Buffer;
+}();
+
+</script>
diff --git a/dom/media/test/crashtests/880129.html b/dom/media/test/crashtests/880129.html
new file mode 100644
index 0000000000..775e9d80ba
--- /dev/null
+++ b/dom/media/test/crashtests/880129.html
@@ -0,0 +1,9 @@
+<script>
+try { o1 = new window.AudioContext(3, 2, 44100); } catch(e) { }
+try { o6 = o1.createBufferSource(); } catch(e) { }
+try { o15 = o1.createAnalyser(); } catch(e) { }
+try { o15.fftSize = 32; } catch(e) { }
+try { o6.connect(o15,0,0) } catch(e) { }
+try { o27 = o1.createPanner(); } catch(e) { }
+try { o6.buffer = function(){var buffer = o1.createBuffer(2, 1148, o1.sampleRate);for(var c=0; c<2; c++) {for(var i=0; i<1148; i++) {buffer.getChannelData(c)[i] = 0;}}return buffer;}() } catch(e) { }
+</script> \ No newline at end of file
diff --git a/dom/media/test/crashtests/880202.html b/dom/media/test/crashtests/880202.html
new file mode 100644
index 0000000000..dc0fade9db
--- /dev/null
+++ b/dom/media/test/crashtests/880202.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+var Context0= new window.OfflineAudioContext(15,12119,44100)
+var BufferSource1=Context0.createBufferSource();
+var Gain0=Context0.createGain();
+var Panner0=Context0.createPanner();
+Context0.listener.setPosition(29,135,158);
+
+BufferSource1.connect(Gain0);
+
+BufferSource1.buffer=function(){
+ var channels=3;
+ var length=53325;
+ var Buffer=Context0.createBuffer(channels,length,Context0.sampleRate);
+ for(var y=0;y<channels;y++){
+ var bufferData= Buffer.getChannelData(y);
+ for (var i = 0; i < length; ++i) {
+ bufferData[i] = i*(270);
+ }
+ };
+ return Buffer;
+}();
+
+Gain0.connect(Panner0);
+Panner0.panningModel="equalpower";
+
+
+setTimeout(function(){
+document.documentElement.removeAttribute("class");
+},500)
+
+</script>
diff --git a/dom/media/test/crashtests/880342-1.html b/dom/media/test/crashtests/880342-1.html
new file mode 100644
index 0000000000..7d1aa0c3e3
--- /dev/null
+++ b/dom/media/test/crashtests/880342-1.html
@@ -0,0 +1,208 @@
+<script>
+try { o1 = new window.AudioContext(2, 10, 1019159); } catch(e) { }
+try { o2 = o1.createBufferSource(); } catch(e) { }
+try { o3 = o1.createConvolver(); } catch(e) { }
+try { o4 = o1.createChannelMerger(2); } catch(e) { }
+try { o5 = o1.createAnalyser(); } catch(e) { }
+try { o6 = o1.createPanner(); } catch(e) { }
+try { o7 = o1.createDynamicsCompressor(); } catch(e) { }
+try { o8 = o1.createWaveShaper(); } catch(e) { }
+try { o9 = o1.createChannelSplitter(6); } catch(e) { }
+try { o10 = o1.createScriptProcessor(8192, 2, 3); } catch(e) { }
+try { o1.listener.setPosition(-144115188075855870, 0, -5e-324) } catch(e) { }
+try { o3.buffer = function(){var buffer = o1.createBuffer(2, 741, o1.sampleRate);for(var c=0; c<2; c++) {for(var i=0; i<741; i++) {buffer.getChannelData(c)[i] = 1;}}return buffer;}(); } catch(e) { }
+try { o10.onaudioprocess = function(e) { /* onEvent */ }; } catch(e) { }
+try { o3.connect(o5,0,0) } catch(e) { }
+try { o9.channelCountMode = 'max'; } catch(e) { }
+try { o10.onaudioprocess = function(e) { /* onEvent */ }; } catch(e) { }
+try { o7.channelInterpretation = 'speakers'; } catch(e) { }
+try { o2.buffer = function(){var buffer = o1.createBuffer(2, 1887, 63346);for(var c=0; c<2; c++) {for(var i=0; i<1887; i++) {buffer.getChannelData(c)[i] = 1;}}return buffer;}() } catch(e) { }
+try { o2.connect(o1.destination); } catch(e) { }
+try { o2.playbackRate.exponentialRampToValueAtTime(3.819464020334534, 32) } catch(e) { }
+try { o5.connect(o9,2,0) } catch(e) { }
+try { o9.channelCountMode = 'explicit'; } catch(e) { }
+try { o1.listener.speedOfSound = 72057594037927940; } catch(e) { }
+try { o1.destination.channelCountMode = 'explicit'; } catch(e) { }
+try { o7.threshold.value = 0.0646435404346891590021684237399313133209944; } catch(e) { }
+try { o7.connect(o10,0,0) } catch(e) { }
+try { o1.destination.channelInterpretation = 'speakers'; } catch(e) { }
+try { o3.connect(o4,0,0) } catch(e) { }
+try { o7.release.value = 23.030355486273447; } catch(e) { }
+try { o9.connect(o9,4,0) } catch(e) { }
+try { o6.channelCountMode = 'max'; } catch(e) { }
+try { o7.threshold.value = 0.6395867363641939418172910336579661816358566284179687500; } catch(e) { }
+try { o2.connect(o9,3,0) } catch(e) { }
+try { o10.onaudioprocess = function(e) { /* onEvent */ }; } catch(e) { }
+try { o7.connect(o10,0,0) } catch(e) { }
+try { o5.connect(o5,0,0) } catch(e) { }
+try { o2.playbackRate.value = 5e-324; } catch(e) { }
+try { o4.channelInterpretation = 'discrete'; } catch(e) { }
+try { o2.playbackRate.value = 1.2211628339508704; } catch(e) { }
+try { o2.channelCountMode = 'clamped-max'; } catch(e) { }
+try { o1.listener.dopplerFactor = 32; } catch(e) { }
+try { o2.buffer = function(){var buffer = o1.createBuffer(22, 1811, 21177);for(var c=0; c<22; c++) {for(var i=0; i<1811; i++) {buffer.getChannelData(c)[i] = 1;}}return buffer;}() } catch(e) { }
+try { o2.connect(o1.destination); } catch(e) { }
+try { o5.channelCountMode = 'max'; } catch(e) { }
+try { o1.listener.speedOfSound = 1024; } catch(e) { }
+try { o2.playbackRate.value = 9.999999999999998e-91; } catch(e) { }
+try { o1.listener.speedOfSound = 16; } catch(e) { }
+try { o8.curve = new Float32Array(256); } catch(e) { }
+try { o1.listener.setVelocity(0, 128, 4) } catch(e) { }
+try { o2.buffer = function(){var buffer = o1.createBuffer(23, 483, o1.sampleRate);for(var c=0; c<23; c++) {for(var i=0; i<483; i++) {buffer.getChannelData(c)[i] = 0.026;}}return buffer;}() } catch(e) { }
+try { o2.connect(o1.destination); } catch(e) { }
+try { o2.buffer = function(){var buffer = o1.createBuffer(3, 945, 43803);for(var c=0; c<3; c++) {for(var i=0; i<945; i++) {buffer.getChannelData(c)[i] = -1;}}return buffer;}() } catch(e) { }
+try { o2.connect(o1.destination); } catch(e) { }
+try { o2.buffer = function(){var buffer = o1.createBuffer(19, 997, 23781);for(var c=0; c<19; c++) {for(var i=0; i<997; i++) {buffer.getChannelData(c)[i] = -1;}}return buffer;}() } catch(e) { }
+try { o2.connect(o1.destination); } catch(e) { }
+try { o1.listener.speedOfSound = -8; } catch(e) { }
+try { o3.channelCountMode = 'explicit'; } catch(e) { }
+try { o2.buffer = function(){var buffer = o1.createBuffer(3, 1740, o1.sampleRate);for(var c=0; c<3; c++) {for(var i=0; i<1740; i++) {buffer.getChannelData(c)[i] = 8;}}return buffer;}() } catch(e) { }
+try { o2.connect(o1.destination); } catch(e) { }
+try { o2.buffer = function(){var buffer = o1.createBuffer(1, 1057, 30602);for(var c=0; c<1; c++) {for(var i=0; i<1057; i++) {buffer.getChannelData(c)[i] = -1;}}return buffer;}() } catch(e) { }
+try { o2.connect(o1.destination); } catch(e) { }
+try { o2.buffer = function(){var buffer = o1.createBuffer(3, 78, o1.sampleRate);for(var c=0; c<3; c++) {for(var i=0; i<78; i++) {buffer.getChannelData(c)[i] = 10;}}return buffer;}() } catch(e) { }
+try { o2.connect(o1.destination); } catch(e) { }
+try { o1.listener.setPosition(9.999999999999995e-275, 0.0001, 2) } catch(e) { }
+try { o2.buffer = function(){var buffer = o1.createBuffer(1, 863, o1.sampleRate);for(var c=0; c<1; c++) {for(var i=0; i<863; i++) {buffer.getChannelData(c)[i] = 1;}}return buffer;}() } catch(e) { }
+try { o2.connect(o1.destination); } catch(e) { }
+try { o8.channelCount = 3; } catch(e) { }
+try { o2.buffer = function(){var buffer = o1.createBuffer(2, 1821, o1.sampleRate);for(var c=0; c<2; c++) {for(var i=0; i<1821; i++) {buffer.getChannelData(c)[i] = 0.3655;}}return buffer;}() } catch(e) { }
+try { o2.connect(o1.destination); } catch(e) { }
+try { o6.connect(o6,0,0) } catch(e) { }
+try { o1.listener.setOrientation(128, -0.479527496, 128, 1, 1, -16) } catch(e) { }
+try { o4.connect(o8,0,0) } catch(e) { }
+try { o7.reduction.exponentialRampToValueAtTime(1.7976931348623157e+308, 2048) } catch(e) { }
+try { o1.listener.dopplerFactor = 0.5754; } catch(e) { }
+try { o7.release.value = 0; } catch(e) { }
+try { o5.getFloatFrequencyData(new Float32Array(2048)) } catch(e) { }
+try { o2.buffer = function(){var buffer = o1.createBuffer(2, 120, 42419);for(var c=0; c<2; c++) {for(var i=0; i<120; i++) {buffer.getChannelData(c)[i] = -1;}}return buffer;}() } catch(e) { }
+try { o2.connect(o1.destination); } catch(e) { }
+try { o3.buffer = function(){var buffer = o1.createBuffer(1, 1620, o1.sampleRate);for(var c=0; c<1; c++) {for(var i=0; i<1620; i++) {buffer.getChannelData(c)[i] = -1;}}return buffer;}(); } catch(e) { }
+try { o2.buffer = function(){var buffer = o1.createBuffer(1, 823, 77239);for(var c=0; c<1; c++) {for(var i=0; i<823; i++) {buffer.getChannelData(c)[i] = -1;}}return buffer;}() } catch(e) { }
+try { o2.connect(o1.destination); } catch(e) { }
+try { o1.destination.channelInterpretation = 'speakers'; } catch(e) { }
+try { o1.listener.setPosition(2048, 0.000522899209, -4503599627370495) } catch(e) { }
+try { o2.buffer = function(){var buffer = o1.createBuffer(3, 84028, 75483);for(var c=0; c<3; c++) {for(var i=0; i<84028; i++) {buffer.getChannelData(c)[i] = -1;}}return buffer;}() } catch(e) { }
+try { o2.connect(o1.destination); } catch(e) { }
+try { o2.playbackRate.setTargetAtTime(5e-324, 9.999999999999998e-104, 0) } catch(e) { }
+try { o7.attack.value = 9.999999999999994e-236; } catch(e) { }
+try { o1.listener.dopplerFactor = 1024; } catch(e) { }
+try { o3.connect(o7,0,0) } catch(e) { }
+try { o2.playbackRate.linearRampToValueAtTime(0, 0) } catch(e) { }
+try { o4.connect(o10,0,0) } catch(e) { }
+try { o1.listener.setVelocity(0.682, 32, 1e-76) } catch(e) { }
+try { o2.buffer = function(){var buffer = o1.createBuffer(3, 201, o1.sampleRate);for(var c=0; c<3; c++) {for(var i=0; i<201; i++) {buffer.getChannelData(c)[i] = 4;}}return buffer;}() } catch(e) { }
+try { o2.connect(o1.destination); } catch(e) { }
+try { o2.connect(o6,0,0) } catch(e) { }
+try { o5.getByteFrequencyData(new Uint8Array(32)) } catch(e) { }
+try { o3.connect(o6,0,0) } catch(e) { }
+try { o7.channelCount = 1; } catch(e) { }
+try { o2.playbackRate.setValueCurveAtTime(new Float32Array(512), 8, 1) } catch(e) { }
+try { o3.connect(o4,0,1) } catch(e) { }
+try { o2.buffer = function(){var buffer = o1.createBuffer(3, 1996, o1.sampleRate);for(var c=0; c<3; c++) {for(var i=0; i<1996; i++) {buffer.getChannelData(c)[i] = -1;}}return buffer;}() } catch(e) { }
+try { o2.connect(o1.destination); } catch(e) { }
+try { o1.listener.dopplerFactor = 2; } catch(e) { }
+try { o2.buffer = function(){var buffer = o1.createBuffer(2, 983, 60517);for(var c=0; c<2; c++) {for(var i=0; i<983; i++) {buffer.getChannelData(c)[i] = 1;}}return buffer;}() } catch(e) { }
+try { o2.connect(o1.destination); } catch(e) { }
+try { o6.coneInnerAngle = 360; } catch(e) { }
+try { o8.curve = new Float32Array(512); } catch(e) { }
+try { o1.listener.setPosition(32, 16, 9.999999999999995e-280) } catch(e) { }
+try { o6.connect(o6,0,0) } catch(e) { }
+try { o1.listener.setPosition(2097151, 512, 5e-324) } catch(e) { }
+try { o7.channelCountMode = 'clamped-max'; } catch(e) { }
+try { o1.destination.channelCountMode = 'max'; } catch(e) { }
+try { o1.listener.setPosition(0, 1000000, 64) } catch(e) { }
+try { o4.connect(o5,0,0) } catch(e) { }
+try { o9.connect(o4,0,1) } catch(e) { }
+try { o7.attack.setValueCurveAtTime(new Float32Array(8), 256, 2048) } catch(e) { }
+try { o9.channelCountMode = 'explicit'; } catch(e) { }
+try { o1.listener.dopplerFactor = 5e-324; } catch(e) { }
+try { o4.connect(o10,0,0) } catch(e) { }
+try { o7.ratio.cancelScheduledValues(1) } catch(e) { }
+try { o2.buffer = function(){var buffer = o1.createBuffer(1, 1590, 9733);for(var c=0; c<1; c++) {for(var i=0; i<1590; i++) {buffer.getChannelData(c)[i] = 2;}}return buffer;}() } catch(e) { }
+try { o2.connect(o1.destination); } catch(e) { }
+try { o1.destination.channelCount = 2; } catch(e) { }
+try { o1.destination.channelInterpretation = 'speakers'; } catch(e) { }
+try { o5.channelCountMode = 'max'; } catch(e) { }
+try { o9.channelCountMode = 'clamped-max'; } catch(e) { }
+try { o7.attack.setTargetAtTime(0.11499421161482907549622467513472656719386577606201171875000, 1, 1.7976931348623157e+308) } catch(e) { }
+try { o2.playbackRate.value = 1; } catch(e) { }
+try { o3.normalize = false; } catch(e) { }
+try { o2.buffer = function(){var buffer = o1.createBuffer(2, 1866, o1.sampleRate);for(var c=0; c<2; c++) {for(var i=0; i<1866; i++) {buffer.getChannelData(c)[i] = 0.174908422905697580329587026426452212035655975341797;}}return buffer;}(); } catch(e) { }
+try { o2.playbackRate.value = 1.0; } catch(e) { }
+try { o7.threshold.value = 100; } catch(e) { }
+try { o1.listener.setVelocity(961.4441892370145, 9.999999999999999e-157, 1) } catch(e) { }
+try { o5.getByteFrequencyData(new Uint8Array(2)) } catch(e) { }
+try { o6.connect(o10,0,0) } catch(e) { }
+try { o2.buffer = function(){var buffer = o1.createBuffer(3, 1176, o1.sampleRate);for(var c=0; c<3; c++) {for(var i=0; i<1176; i++) {buffer.getChannelData(c)[i] = 9.753993292300242;}}return buffer;}() } catch(e) { }
+try { o2.connect(o1.destination); } catch(e) { }
+try { o2.playbackRate.linearRampToValueAtTime(0.8687, 32) } catch(e) { }
+try { o5.channelCountMode = 'explicit'; } catch(e) { }
+try { o2.buffer = function(){var buffer = o1.createBuffer(13, 703, 12723);for(var c=0; c<13; c++) {for(var i=0; i<703; i++) {buffer.getChannelData(c)[i] = -1;}}return buffer;}() } catch(e) { }
+try { o2.connect(o1.destination); } catch(e) { }
+try { o2.loopEnd = 1.7976931348623157e+308; } catch(e) { }
+try { o3.connect(o7,0,0) } catch(e) { }
+try { o1.destination.channelCountMode = 'clamped-max'; } catch(e) { }
+try { o1.destination.channelInterpretation = 'discrete'; } catch(e) { }
+try { o2.connect(o10,0,0) } catch(e) { }
+try { o3.normalize = false; } catch(e) { }
+try { /* switch-case did not return anything. */ } catch(e) { }
+try { o10.connect(o8,0,0) } catch(e) { }
+try { o2.buffer = function(){var buffer = o1.createBuffer(3, 132, 88507);for(var c=0; c<3; c++) {for(var i=0; i<132; i++) {buffer.getChannelData(c)[i] = 1.7976931348623157e+308;}}return buffer;}() } catch(e) { }
+try { o2.connect(o1.destination); } catch(e) { }
+try { o2.loopStart = 1.7976931348623157e+308; } catch(e) { }
+try { o1.listener.dopplerFactor = 1024; } catch(e) { }
+try { o2.buffer = function(){var buffer = o1.createBuffer(1, 40, o1.sampleRate);for(var c=0; c<1; c++) {for(var i=0; i<40; i++) {buffer.getChannelData(c)[i] = 1.3785770335346594;}}return buffer;}() } catch(e) { }
+try { o2.connect(o1.destination); } catch(e) { }
+try { o4.connect(o9,5,0) } catch(e) { }
+try { o8.curve = new Float32Array(16); } catch(e) { }
+try { o10.onaudioprocess = function(e) { /* onEvent */ }; } catch(e) { }
+try { o6.channelCountMode = 'explicit'; } catch(e) { }
+try { o2.playbackRate.value = 0; } catch(e) { }
+try { o4.channelCount = 3; } catch(e) { }
+try { o2.buffer = function(){var buffer = o1.createBuffer(3, 220, o1.sampleRate);for(var c=0; c<3; c++) {for(var i=0; i<220; i++) {buffer.getChannelData(c)[i] = -1;}}return buffer;}() } catch(e) { }
+try { o2.connect(o1.destination); } catch(e) { }
+try { o1.destination.channelInterpretation = 'discrete'; } catch(e) { }
+try { o1.listener.setPosition(70368744177664, 2048, 1.7976931348623157e+308) } catch(e) { }
+try { o2.playbackRate.setValueCurveAtTime(new Float32Array(128), 1.7976931348623157e+308, 0.75) } catch(e) { }
+try { o5.fftSize = 118; } catch(e) { }
+try { o5.minDecibels = 1; } catch(e) { }
+try { o7.release.value = 5e-324; } catch(e) { }
+try { o2.channelCount = 3; } catch(e) { }
+try { o2.channelCountMode = 'clamped-max'; } catch(e) { }
+try { o3.normalize = false; } catch(e) { }
+try { o1.listener.speedOfSound = 2; } catch(e) { }
+try { o2.loopStart = 32; } catch(e) { }
+try { o3.channelCountMode = 'max'; } catch(e) { }
+try { o1.listener.setPosition(0.37976588317653304, -274877906943, 64) } catch(e) { }
+try { o8.curve = new Float32Array(4); } catch(e) { }
+try { o1.listener.setVelocity(16, -1024, 0) } catch(e) { }
+try { o5.minDecibels = 10000; } catch(e) { }
+try { o2.playbackRate.value = 5e-324; } catch(e) { }
+try { o3.connect(o9,1,0) } catch(e) { }
+try { o10.onaudioprocess = function(e) { /* onEvent */ }; } catch(e) { }
+try { o5.connect(o8,0,0) } catch(e) { }
+try { o2.buffer = function(){var buffer = o1.createBuffer(3, 554, o1.sampleRate);for(var c=0; c<3; c++) {for(var i=0; i<554; i++) {buffer.getChannelData(c)[i] = 0.000001;}}return buffer;}() } catch(e) { }
+try { o2.connect(o1.destination); } catch(e) { }
+try { o7.release.value = 0; } catch(e) { }
+try { o2.buffer = function(){var buffer = o1.createBuffer(1, 1289, 29583);for(var c=0; c<1; c++) {for(var i=0; i<1289; i++) {buffer.getChannelData(c)[i] = 5e-324;}}return buffer;}() } catch(e) { }
+try { o2.connect(o1.destination); } catch(e) { }
+try { o1.listener.setOrientation(1024, 64, 16, 1024, 1, 68719476735) } catch(e) { }
+try { o1.listener.setOrientation(512, 10000000, 274877906944, 9.999999999999998e-113, -8, 3.6191134923595274) } catch(e) { }
+try { o2.buffer = function(){var buffer = o1.createBuffer(1, 1453, 15258);for(var c=0; c<1; c++) {for(var i=0; i<1453; i++) {buffer.getChannelData(c)[i] = -1;}}return buffer;}() } catch(e) { }
+try { o2.connect(o1.destination); } catch(e) { }
+try { o8.channelInterpretation = 'discrete'; } catch(e) { }
+try { o5.channelInterpretation = 'discrete'; } catch(e) { }
+try { o7.connect(o9,4,0) } catch(e) { }
+try { o8.connect(o10,0,0) } catch(e) { }
+try { o1.listener.setPosition(18014398509481984, 16, 0.505129302503804056279079759406158700585365295410156250000) } catch(e) { }
+try { o10.connect(o9,5,0) } catch(e) { }
+try { o2.loop = false; } catch(e) { }
+try { o2.buffer = function(){var buffer = o1.createBuffer(11, 1673, o1.sampleRate);for(var c=0; c<11; c++) {for(var i=0; i<1673; i++) {buffer.getChannelData(c)[i] = 0;}}return buffer;}() } catch(e) { }
+try { o2.connect(o1.destination); } catch(e) { }
+try { o2.buffer = function(){var buffer = o1.createBuffer(2, 577, o1.sampleRate);for(var c=0; c<2; c++) {for(var i=0; i<577; i++) {buffer.getChannelData(c)[i] = 0.5611;}}return buffer;}() } catch(e) { }
+try { o2.connect(o1.destination); } catch(e) { }
+try { o1.listener.setOrientation(64, 2049, 5e-324, 0.1777, 2, 7) } catch(e) { }
+try { o5.maxDecibels = 128; } catch(e) { }
+try { o2.start(0) } catch(e) { }
+</script> \ No newline at end of file
diff --git a/dom/media/test/crashtests/880342-2.html b/dom/media/test/crashtests/880342-2.html
new file mode 100644
index 0000000000..a8ef88ae70
--- /dev/null
+++ b/dom/media/test/crashtests/880342-2.html
@@ -0,0 +1,8 @@
+<script>
+try { o1 = new window.AudioContext(2, 10, 1019159); } catch(e) { }
+try { o2 = o1.createBufferSource(); } catch(e) { }
+try { o3 = o1.createConvolver(); } catch(e) { }
+try { o3.buffer = function(){var buffer = o1.createBuffer(2, 741, o1.sampleRate);for(var c=0; c<2; c++) {for(var i=0; i<741; i++) {buffer.getChannelData(c)[i] = 1;}}return buffer;}(); } catch(e) { }
+try { o2.buffer = function(){var buffer = o1.createBuffer(2, 120, 42419);for(var c=0; c<2; c++) {for(var i=0; i<120; i++) {buffer.getChannelData(c)[i] = -1;}}return buffer;}() } catch(e) { }
+try { o3.buffer = function(){var buffer = o1.createBuffer(1, 1620, o1.sampleRate);for(var c=0; c<1; c++) {for(var i=0; i<1620; i++) {buffer.getChannelData(c)[i] = -1;}}return buffer;}(); } catch(e) { }
+</script> \ No newline at end of file
diff --git a/dom/media/test/crashtests/880384.html b/dom/media/test/crashtests/880384.html
new file mode 100644
index 0000000000..798fbf133d
--- /dev/null
+++ b/dom/media/test/crashtests/880384.html
@@ -0,0 +1,8 @@
+<script>
+o1 = new window.AudioContext(3, 256, 44100);
+o2 = o1.createBufferSource();
+o3 = o1.createConvolver();
+o3.normalize = false;
+o3.buffer = function(){var buffer = o1.createBuffer(2, 1051, o1.sampleRate);for(var c=0; c<2; c++) {for(var i=0; i<1051; i++) {buffer.getChannelData(c)[i] = 1;}}return buffer;}();
+o3.buffer = function(){var buffer = o1.createBuffer(2, 1112, o1.sampleRate);for(var c=0; c<2; c++) {for(var i=0; i<1112; i++) {buffer.getChannelData(c)[i] = 1;}}return buffer;}();
+</script>
diff --git a/dom/media/test/crashtests/880404.html b/dom/media/test/crashtests/880404.html
new file mode 100644
index 0000000000..bf34932388
--- /dev/null
+++ b/dom/media/test/crashtests/880404.html
@@ -0,0 +1,6 @@
+<script>
+o1 = new window.OfflineAudioContext(2, 32, 44100);
+o12 = o1.createConvolver();
+o12.buffer = function(){var buffer = o1.createBuffer(1, 78, o1.sampleRate);for(var c=0; c<1; c++) {for(var i=0; i<78; i++) {buffer.getChannelData(c)[i] = 1;}}return buffer;}();
+o1.startRendering();
+</script> \ No newline at end of file
diff --git a/dom/media/test/crashtests/880724.html b/dom/media/test/crashtests/880724.html
new file mode 100644
index 0000000000..b57b5f9964
--- /dev/null
+++ b/dom/media/test/crashtests/880724.html
@@ -0,0 +1,13 @@
+<script>
+o1 = new window.AudioContext();
+o5 = o1.createConvolver();
+o5.buffer = function() {
+ var buffer = o1.createBuffer(2, 3, 33120);
+ for(var c=0; c<2; c++) {
+ for(var i=0; i<3; i++) {
+ buffer.getChannelData(c)[i] = -1;
+ }
+ }
+ return buffer;
+}();
+</script> \ No newline at end of file
diff --git a/dom/media/test/crashtests/881775.html b/dom/media/test/crashtests/881775.html
new file mode 100644
index 0000000000..d55c45d17e
--- /dev/null
+++ b/dom/media/test/crashtests/881775.html
@@ -0,0 +1,25 @@
+<script>
+o1 = new window.AudioContext(2, 16, 44100);
+o2 = o1.createBufferSource();
+o12 = o1.createBiquadFilter();
+o1.destination.channelCountMode = 'max';
+o2.buffer = function(){
+ var buffer = o1.createBuffer(2, 326, 77632);
+ for(var c=0; c<2; c++) {
+ for(var i=0; i<326; i++) {
+ buffer.getChannelData(c)[i] = -1;
+ }
+ }
+ return buffer;
+}();
+o2.connect(o1.destination);
+o2.buffer = function(){
+ var buffer = o1.createBuffer(3, 405, o1.sampleRate);
+ for(var c=0; c<3; c++) {
+ for(var i=0; i<405; i++) {
+ buffer.getChannelData(c)[i] = 1;
+ }
+ }
+ return buffer;
+}();
+</script>
diff --git a/dom/media/test/crashtests/882956.html b/dom/media/test/crashtests/882956.html
new file mode 100644
index 0000000000..ae7b441f99
--- /dev/null
+++ b/dom/media/test/crashtests/882956.html
@@ -0,0 +1,15 @@
+<script>
+o1 = new window.AudioContext(1, 2048, 44100);
+o2 = o1.createBufferSource();
+o1.destination.channelCountMode = 'max';
+o2.connect(o1.destination);
+o2.buffer = function(){
+ var buffer = o1.createBuffer(30, 442, 94933);
+ for(var c=0; c<30; c++) {
+ for(var i=0; i<442; i++) {
+ buffer.getChannelData(c)[i] = 1;
+ }
+ }
+ return buffer;
+}();
+</script>
diff --git a/dom/media/test/crashtests/884459.html b/dom/media/test/crashtests/884459.html
new file mode 100644
index 0000000000..e321d569f2
--- /dev/null
+++ b/dom/media/test/crashtests/884459.html
@@ -0,0 +1,12 @@
+<script>
+var Context0= new window.OfflineAudioContext(14,191531,44100)
+var BufferSource1=Context0.createBufferSource();
+
+setInterval(function(){
+BufferSource1.playbackRate.setTargetAtTime(0xC8F461D3EE6B2,(Context0.currentTime+0.0677539280615747),0.826130285160616);
+BufferSource1.playbackRate.setValueAtTime(35467.63924283907536607336193,0);
+},1)
+
+Context0.startRendering();
+
+</script>
diff --git a/dom/media/test/crashtests/889042.html b/dom/media/test/crashtests/889042.html
new file mode 100644
index 0000000000..9f74979c58
--- /dev/null
+++ b/dom/media/test/crashtests/889042.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<script>
+ (new AudioContext()).createConvolver().buffer = null;
+</script>
diff --git a/dom/media/test/crashtests/907986-1.html b/dom/media/test/crashtests/907986-1.html
new file mode 100644
index 0000000000..4dec6bc480
--- /dev/null
+++ b/dom/media/test/crashtests/907986-1.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+var context = new window.OfflineAudioContext(1, 100, 48000);
+context.oncomplete = function(e) {
+ document.documentElement.removeAttribute("class");
+};
+// zero front vector
+context.listener.setOrientation(0, 0, 0, 6.311749985202524e+307, 0, 0);
+var panner = context.createPanner();
+panner.setPosition(3.40282e+38, 4, 3.40282e+38);
+panner.connect(context.destination);
+var source = context.createOscillator();
+source.connect(panner);
+source.start(0);
+context.startRendering();
+</script>
diff --git a/dom/media/test/crashtests/907986-2.html b/dom/media/test/crashtests/907986-2.html
new file mode 100644
index 0000000000..e0626ba2c2
--- /dev/null
+++ b/dom/media/test/crashtests/907986-2.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+var context = new window.OfflineAudioContext(1, 100, 48000);
+context.oncomplete = function(e) {
+ document.documentElement.removeAttribute("class");
+};
+// zero up vector
+context.listener.setOrientation(0, 6.311749985202524e+307, 0, 0, 0, 0);
+var panner = context.createPanner();
+panner.setPosition(1, 2, 3);
+panner.connect(context.destination);
+var source = context.createOscillator();
+source.connect(panner);
+source.start(0);
+context.startRendering();
+</script>
diff --git a/dom/media/test/crashtests/907986-3.html b/dom/media/test/crashtests/907986-3.html
new file mode 100644
index 0000000000..75b756c363
--- /dev/null
+++ b/dom/media/test/crashtests/907986-3.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+var context = new window.OfflineAudioContext(1, 100, 48000);
+context.oncomplete = function(e) {
+ document.documentElement.removeAttribute("class");
+};
+// linearly dependent
+context.listener.setOrientation(0, 0, -6.311749985202524e+307, 0, 0, 6.311749985202524e+307);
+var panner = context.createPanner();
+panner.setPosition(1, 2, 3);
+panner.connect(context.destination);
+var source = context.createOscillator();
+source.connect(panner);
+source.start(0);
+context.startRendering();
+</script>
diff --git a/dom/media/test/crashtests/907986-4.html b/dom/media/test/crashtests/907986-4.html
new file mode 100644
index 0000000000..a73500efca
--- /dev/null
+++ b/dom/media/test/crashtests/907986-4.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+var context = new window.OfflineAudioContext(1, 100, 48000);
+context.oncomplete = function(e) {
+ document.documentElement.removeAttribute("class");
+};
+var panner = context.createPanner();
+panner.setPosition(0, 3, 0); // directly overhead
+panner.connect(context.destination);
+var source = context.createOscillator();
+source.connect(panner);
+source.start(0);
+context.startRendering();
+</script>
diff --git a/dom/media/test/crashtests/910171-1.html b/dom/media/test/crashtests/910171-1.html
new file mode 100644
index 0000000000..9f3ec831be
--- /dev/null
+++ b/dom/media/test/crashtests/910171-1.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+var context = new window.OfflineAudioContext(1, 4096, 48000);
+context.oncomplete = function(e) {
+ document.documentElement.removeAttribute("class");
+};
+var delay = context.createDelay();
+delay.connect(context.destination);
+delay.delayTime.value = 1.0;
+var buffer = context.createBuffer(1, 2048, context.sampleRate);
+var source = context.createBufferSource();
+source.buffer = buffer;
+source.connect(delay);
+source.start();
+context.startRendering();
+</script>
diff --git a/dom/media/test/crashtests/920987.html b/dom/media/test/crashtests/920987.html
new file mode 100644
index 0000000000..6e8b2992a6
--- /dev/null
+++ b/dom/media/test/crashtests/920987.html
@@ -0,0 +1,6 @@
+<script>
+var ctx = new AudioContext();
+var buffer = ctx.createBuffer(1, 1000, ctx.sampleRate);
+var array = new Float32Array(900);
+buffer.copyToChannel(array, 0, 0xfffffff8);
+</script>
diff --git a/dom/media/test/crashtests/925619-1.html b/dom/media/test/crashtests/925619-1.html
new file mode 100644
index 0000000000..146c531f9b
--- /dev/null
+++ b/dom/media/test/crashtests/925619-1.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+// 1024 > 89478.5 * 48000 - (1 << 32)
+var context = new window.OfflineAudioContext(1, 1024, 48000);
+context.oncomplete = function(e) {
+ document.documentElement.removeAttribute("class");
+};
+var buffer = context.createBuffer(1, 2048, context.sampleRate);
+var source = context.createBufferSource();
+source.buffer = buffer;
+source.start(89478.5); // 89478.5 is a little greater than 2^32 / 48000.
+context.startRendering();
+</script>
diff --git a/dom/media/test/crashtests/925619-2.html b/dom/media/test/crashtests/925619-2.html
new file mode 100644
index 0000000000..e734b8bcad
--- /dev/null
+++ b/dom/media/test/crashtests/925619-2.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+var context = new window.OfflineAudioContext(1, 2048, 48000);
+// 1024 > 89478.5 * 48000 - (1 << 32)
+var buffer = context.createBuffer(1, 1024, context.sampleRate);
+var source = context.createBufferSource();
+source.buffer = buffer;
+source.onended = function(e) {
+ document.documentElement.removeAttribute("class");
+};
+source.start(0);
+source.stop(89478.5); // 89478.5 is a little greater than 2^32 / 48000.
+context.startRendering();
+</script>
diff --git a/dom/media/test/crashtests/926619.html b/dom/media/test/crashtests/926619.html
new file mode 100644
index 0000000000..2ead02af4e
--- /dev/null
+++ b/dom/media/test/crashtests/926619.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<script>
+
+function boom()
+{
+ var ac = new window.AudioContext();
+
+ var _ChannelMergerNode = ac.createChannelMerger(4);
+
+ var _MediaStreamAudioDestinationNode = ac.createMediaStreamDestination();
+ var _MediaStream = _MediaStreamAudioDestinationNode.stream;
+ var _MediaStreamAudioSourceNode = ac.createMediaStreamSource(_MediaStream);
+
+ _ChannelMergerNode.connect(_MediaStreamAudioDestinationNode, 0, 0);
+ _MediaStreamAudioSourceNode.connect(_ChannelMergerNode, 0, 0);
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/dom/media/test/crashtests/933151.html b/dom/media/test/crashtests/933151.html
new file mode 100644
index 0000000000..3d45f7af38
--- /dev/null
+++ b/dom/media/test/crashtests/933151.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+function boom()
+{
+ var ac = new window.AudioContext();
+ var buffer = ac.createBuffer(1, 24313, 47250);
+ buffer.copyFromChannel(buffer.getChannelData(0), '');
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/dom/media/test/crashtests/933156.html b/dom/media/test/crashtests/933156.html
new file mode 100644
index 0000000000..b89445a43d
--- /dev/null
+++ b/dom/media/test/crashtests/933156.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+function boom()
+{
+ var ac = new window.AudioContext();
+
+ var panner = ac.createPanner();
+ panner.setPosition(8, 0.7643051305237005, 0.10292575673733972);
+
+ var oscillator = ac.createOscillator();
+ oscillator.connect(panner);
+ oscillator.start(0);
+
+ setTimeout(function() {
+ panner.panningModel = 'equalpower';
+ oscillator.stop(0);
+ document.documentElement.removeAttribute("class");
+ }, 0.5);
+}
+boom();
+</script>
+</html>
diff --git a/dom/media/test/crashtests/944851.html b/dom/media/test/crashtests/944851.html
new file mode 100644
index 0000000000..4f663accc4
--- /dev/null
+++ b/dom/media/test/crashtests/944851.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<script>
+
+var ac = new AudioContext(1, 1354, 44100);
+var shaper = ac.createWaveShaper();
+var biquad = ac.createBiquadFilter();
+shaper.connect(biquad.frequency);
+biquad.getFrequencyResponse(new Float32Array(55785),
+ new Float32Array(62876),
+ new Float32Array(45111));
+
+</script>
+</head>
+</html>
diff --git a/dom/media/test/crashtests/952756.html b/dom/media/test/crashtests/952756.html
new file mode 100644
index 0000000000..ffced2e400
--- /dev/null
+++ b/dom/media/test/crashtests/952756.html
@@ -0,0 +1,19 @@
+<script>
+var Context0= new AudioContext()
+var BufferSource0=Context0.createBufferSource();
+BufferSource0.buffer=function(){
+ var length=35887;
+ var Buffer=Context0.createBuffer(1,length,Context0.sampleRate);
+ var bufferData= Buffer.getChannelData(0);
+ for (var i = 0; i < length; ++i) { bufferData[i] = Math.sin(i*(0.39765272522345185))};
+ return Buffer;
+}();
+BufferSource0.start(0.01932738965842873,0.33345631847623736,0.3893404237460345);
+BufferSource0.buffer=function(){
+ var length=15952;
+ var Buffer=Context0.createBuffer(1,length,Context0.sampleRate);
+ var bufferData= Buffer.getChannelData(0);
+ for (var i = 0; i < length; ++i) { bufferData[i] = Math.sin(i*(85))};
+ return Buffer;
+}();
+</script>
diff --git a/dom/media/test/crashtests/986901.html b/dom/media/test/crashtests/986901.html
new file mode 100644
index 0000000000..343df2c0ed
--- /dev/null
+++ b/dom/media/test/crashtests/986901.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<script>
+ var ac = new window.AudioContext();
+ var delay1 = ac.createDelay(0.02);
+ var delay2 = ac.createDelay(0.002);
+ var source = ac.createOscillator();
+ source.start(0);
+ source.connect(delay1, 0, 0);
+ delay2.connect(delay1, 0, 0);
+ delay1.connect(delay2, 0, 0);
+</script>
+</head>
+</html>
diff --git a/dom/media/test/crashtests/990794.html b/dom/media/test/crashtests/990794.html
new file mode 100644
index 0000000000..8b40088b1a
--- /dev/null
+++ b/dom/media/test/crashtests/990794.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<script>
+var ctx = new AudioContext();
+var source = ctx.createOscillator();
+source.start(0);
+
+function appendMerger(src) {
+ const inputCount = 18;
+
+ var merger = ctx.createChannelMerger(32);
+
+ for (var i = 0; i < inputCount; ++i) {
+ src.connect(merger, 0, i);
+ }
+
+ return merger;
+}
+
+for (var i = 0; i < 6; ++i) {
+ source = appendMerger(source);
+}
+</script>
diff --git a/dom/media/test/crashtests/995289.html b/dom/media/test/crashtests/995289.html
new file mode 100644
index 0000000000..c988f41fa8
--- /dev/null
+++ b/dom/media/test/crashtests/995289.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<script>
+var r0=new AudioContext();
+var r5=r0.createOscillator();
+var r6=r0.createPeriodicWave(new Float32Array(1),new Float32Array(1));
+r5.frequency.value = 4294967295;
+r5.start(0);
+r5.setPeriodicWave(r6);
+</script>
diff --git a/dom/media/test/crashtests/adts-truncated.aac b/dom/media/test/crashtests/adts-truncated.aac
new file mode 100644
index 0000000000..670f696e1d
--- /dev/null
+++ b/dom/media/test/crashtests/adts-truncated.aac
Binary files differ
diff --git a/dom/media/test/crashtests/adts.aac b/dom/media/test/crashtests/adts.aac
new file mode 100644
index 0000000000..208515464a
--- /dev/null
+++ b/dom/media/test/crashtests/adts.aac
Binary files differ
diff --git a/dom/media/test/crashtests/analyser-channels-1.html b/dom/media/test/crashtests/analyser-channels-1.html
new file mode 100644
index 0000000000..2f3133cf13
--- /dev/null
+++ b/dom/media/test/crashtests/analyser-channels-1.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+ var context = new window.OfflineAudioContext(1, 256, 48000);
+ var analyser = context.createAnalyser();
+ analyser.channelCount = 2;
+ analyser.channelCountMode = "explicit";
+ analyser.fftSize = 32;
+ var source = context.createOscillator();
+ source.connect(analyser);
+ source.start(0);
+ context.startRendering().
+ then(function() {
+ document.documentElement.removeAttribute("class");
+ });
+</script>
diff --git a/dom/media/test/crashtests/audiocontext-after-unload-1.html b/dom/media/test/crashtests/audiocontext-after-unload-1.html
new file mode 100644
index 0000000000..9b4f1181d2
--- /dev/null
+++ b/dom/media/test/crashtests/audiocontext-after-unload-1.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<title>Test for bug 1646601</title>
+<script>
+document.addEventListener('DOMContentLoaded', async () => {
+ const frame = document.createElement('iframe');
+ document.body.appendChild(frame);
+ frame.srcdoc = '<html></html>';
+ await new Promise(resolve => frame.onload = resolve);
+ const subwin = frame.contentWindow;
+ const subcontext = subwin.AudioContext;
+ // Construct an AudioContext while the subdocument is fully active to start
+ // a MediaTrackGraph.
+ new subcontext();
+ // Unload the subdocument and wait for completion.
+ // This shuts down the MediaTrackGraph.
+ subwin.location.reload();
+ await new Promise(resolve => frame.onload = resolve);
+ // Test that a new AudioContext on the inactive subdocument does not attempt
+ // to use the shut-down MediaTrackGraph.
+ try { new subcontext() } catch {}
+ document.documentElement.removeAttribute('class');
+});
+</script>
+</head>
+</html>
diff --git a/dom/media/test/crashtests/audiocontext-after-xhr.html b/dom/media/test/crashtests/audiocontext-after-xhr.html
new file mode 100644
index 0000000000..a4dd9990ca
--- /dev/null
+++ b/dom/media/test/crashtests/audiocontext-after-xhr.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script>
+ document.addEventListener('DOMContentLoaded', () => {
+ const xhr = new XMLHttpRequest()
+ xhr.open('G', '', false)
+ xhr.send()
+ window.ac = new AudioContext()
+ })
+ </script>
+</head>
+</html>
diff --git a/dom/media/test/crashtests/audiocontext-double-suspend.html b/dom/media/test/crashtests/audiocontext-double-suspend.html
new file mode 100644
index 0000000000..98399549bd
--- /dev/null
+++ b/dom/media/test/crashtests/audiocontext-double-suspend.html
@@ -0,0 +1,5 @@
+<script>
+var ac = new AudioContext();
+ac.resume();
+ac.resume();
+</script>
diff --git a/dom/media/test/crashtests/audioworkletnode-after-unload-1.html b/dom/media/test/crashtests/audioworkletnode-after-unload-1.html
new file mode 100644
index 0000000000..7da8d1161a
--- /dev/null
+++ b/dom/media/test/crashtests/audioworkletnode-after-unload-1.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<title>Test for bug 1634200 and bug 1655544</title>
+<script>
+document.addEventListener('DOMContentLoaded', async () => {
+ const frame = document.createElement('iframe');
+ document.body.appendChild(frame);
+ frame.srcdoc = '<html></html>';
+ await new Promise(resolve => frame.onload = resolve);
+
+ const subwin = frame.contentWindow;
+ const ctx = new subwin.AudioContext();
+ const url = URL.createObjectURL(
+ new Blob([`registerProcessor("noop",
+ class extends AudioWorkletProcessor {})`]),
+ {type: "application/javascript"});
+ await ctx.audioWorklet.addModule(url);
+
+ frame.remove();
+ new subwin.AudioWorkletNode(ctx, 'noop')
+
+ document.documentElement.removeAttribute('class');
+});
+</script>
+</head>
+</html>
diff --git a/dom/media/test/crashtests/buffer-source-duration-1.html b/dom/media/test/crashtests/buffer-source-duration-1.html
new file mode 100644
index 0000000000..df8d7a37d5
--- /dev/null
+++ b/dom/media/test/crashtests/buffer-source-duration-1.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+const rate = 44100;
+var context = new window.OfflineAudioContext(1, 512, rate);
+var buffer = context.createBuffer(1, 128, rate);
+var source = context.createBufferSource();
+source.buffer = buffer;
+source.start(0, 0, 86400);
+context.startRendering().
+ then(function() {
+ document.documentElement.removeAttribute("class");
+ });
+</script>
diff --git a/dom/media/test/crashtests/buffer-source-ended-1.html b/dom/media/test/crashtests/buffer-source-ended-1.html
new file mode 100644
index 0000000000..de8546316c
--- /dev/null
+++ b/dom/media/test/crashtests/buffer-source-ended-1.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+var context = new AudioContext();
+
+var source = context.createBufferSource();
+source.buffer = context.createBuffer(1, 2.0 * context.sampleRate, context.sampleRate);
+source.onended = function(e) {
+ document.documentElement.removeAttribute("class");
+}
+source.start(0.0, 1.0);
+setTimeout(
+ function() {
+ source.buffer = context.createBuffer(1, 1, context.sampleRate);
+ }, 0);
+</script>
diff --git a/dom/media/test/crashtests/buffer-source-resampling-start-1.html b/dom/media/test/crashtests/buffer-source-resampling-start-1.html
new file mode 100644
index 0000000000..55db8591ed
--- /dev/null
+++ b/dom/media/test/crashtests/buffer-source-resampling-start-1.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+const rate = 44101; // not divisible by 2
+var context = new window.OfflineAudioContext(1, 512, rate);
+var buffer = context.createBuffer(1, 128, rate);
+buffer.getChannelData(0)[0] = 1.0;
+var source = context.createBufferSource();
+source.buffer = buffer;
+source.playbackRate.value = rate / (Math.pow(2, 30) * 1.0000001);
+source.start(512 / rate);
+context.startRendering().
+ then(function() {
+ document.documentElement.removeAttribute("class");
+ });
+</script>
diff --git a/dom/media/test/crashtests/buffer-source-slow-resampling-1.html b/dom/media/test/crashtests/buffer-source-slow-resampling-1.html
new file mode 100644
index 0000000000..5d8a50442b
--- /dev/null
+++ b/dom/media/test/crashtests/buffer-source-slow-resampling-1.html
@@ -0,0 +1,34 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+const blockSize = 128;
+// The sample rate is a prime number so that the resampler is not expected to
+// simplify in/out fractions.
+const rate = 44101;
+var context = new window.OfflineAudioContext(1, 3 * blockSize, rate);
+// Non-zero buffer, so it can't be optimized away.
+var buffer = context.createBuffer(1, 128, rate);
+buffer.getChannelData(0)[0] = 1.0;
+var source = context.createBufferSource();
+source.buffer = buffer;
+source.loop = true;
+// Initialize the resampler with a slow input rate.
+// With the current (Mar 2017) implementation, very slow rates give the
+// resampler a very large denominator.
+source.playbackRate.setValueAtTime(rate / 0x7fffffff, 0.0);
+// Change to a moderate input rate.
+// With the current implementation, skip_frac_num increases by den_rate for
+// each output sample and so one block before the change in playback rate is
+// enough for high skip_frac_num at the time of the change.
+const changeBlock = 1;
+const changeBlockSeconds = changeBlock * blockSize / rate;
+// With the current speex_resampler_set_rate_frac() implementation, the
+// moderate resampler denominator is still large enough to trigger overflow of
+// 32-bit unsigned integer arithmetic.
+source.playbackRate.setValueAtTime(rate / (rate + 1), changeBlockSeconds);
+source.start(0);
+context.startRendering().
+ then(function() {
+ document.documentElement.removeAttribute("class");
+ });
+</script>
diff --git a/dom/media/test/crashtests/channel-count-in-metadata-different-than-in-content.mp4 b/dom/media/test/crashtests/channel-count-in-metadata-different-than-in-content.mp4
new file mode 100644
index 0000000000..92bf3722f2
--- /dev/null
+++ b/dom/media/test/crashtests/channel-count-in-metadata-different-than-in-content.mp4
Binary files differ
diff --git a/dom/media/test/crashtests/convolver-memory-report-1.html b/dom/media/test/crashtests/convolver-memory-report-1.html
new file mode 100644
index 0000000000..a49a281d1c
--- /dev/null
+++ b/dom/media/test/crashtests/convolver-memory-report-1.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+ <title>Bug 1481745: Exercise ConvolverNode memory reporting</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script>
+let context = new AudioContext();
+let response = new AudioBuffer({length: 128,
+ sampleRate: context.sampleRate});
+response.getChannelData(0)[response.length - 1] = 1;
+let convolver = new ConvolverNode(context,
+ {disableNormalization: true,
+ buffer: response});
+convolver.connect(context.destination);
+let osc = new OscillatorNode(context);
+osc.connect(convolver);
+osc.start();
+osc.stop(128/context.sampleRate);
+osc.onended = (e) => {
+ SpecialPowers.getMemoryReports();
+ document.documentElement.removeAttribute("class");
+};
+ </script>
+</head>
+</html>
diff --git a/dom/media/test/crashtests/copyFromChannel-2.html b/dom/media/test/crashtests/copyFromChannel-2.html
new file mode 100644
index 0000000000..8d3d5a2124
--- /dev/null
+++ b/dom/media/test/crashtests/copyFromChannel-2.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Crashtest for bug 1548816</title>
+ <script>
+let cx = new OfflineAudioContext({numberOfChannels: 1,
+ length: 1, sampleRate: 44100});
+let buffer = new AudioBuffer({numberOfChannels: 13,
+ length: 22050, sampleRate: 44100});
+buffer.getChannelData(12)[0] = 1.0;
+let o2248 = new AudioBufferSourceNode(cx, {buffer: buffer});
+let array = new Float32Array(52428);
+buffer.copyFromChannel(array, 12);
+ </script>
+</head>
+</html>
diff --git a/dom/media/test/crashtests/cors.webm b/dom/media/test/crashtests/cors.webm
new file mode 100644
index 0000000000..72b0297233
--- /dev/null
+++ b/dom/media/test/crashtests/cors.webm
Binary files differ
diff --git a/dom/media/test/crashtests/cors.webm^headers^ b/dom/media/test/crashtests/cors.webm^headers^
new file mode 100644
index 0000000000..cb762eff80
--- /dev/null
+++ b/dom/media/test/crashtests/cors.webm^headers^
@@ -0,0 +1 @@
+Access-Control-Allow-Origin: *
diff --git a/dom/media/test/crashtests/crashtests.list b/dom/media/test/crashtests/crashtests.list
new file mode 100644
index 0000000000..0a420d9e1f
--- /dev/null
+++ b/dom/media/test/crashtests/crashtests.list
@@ -0,0 +1,169 @@
+skip-if(OSX) load 1185191.html # this needs to run near the beginning of the test suite
+load 0-timescale.html # bug 1229166
+skip-if(Android) pref(media.autoplay.default,0) load 459439-1.html # bug 888557
+load 466607-1.html
+load 466945-1.html
+load 468763-1.html
+load 474744-1.html
+HTTP load 481136-1.html # needs to be HTTP to recognize the ogg as an audio file?
+load 492286-1.xhtml
+load 493915-1.html
+pref(media.autoplay.default,0) load 495794-1.html
+load 497734-1.xhtml
+load 497734-2.html
+load 576612-1.html
+load 752784-1.html
+skip-if(Android) load 789075-1.html # bug 1374405 for android
+skip-if(Android&&AndroidVersion=='22') HTTP load 795892-1.html # bug 1358718
+load 844563.html
+load 846612.html
+load 852838.html
+load 865004.html
+load 865537-1.html
+load 865550.html
+load 868504.html
+load 874869.html
+load 874915.html
+load 874934.html
+load 874952.html
+load 875144.html
+load 875596.html
+load 875911.html
+load 876024-1.html
+load 876024-2.html
+load 876118.html
+load 876207.html
+load 876215.html
+load 876249.html
+load 876252.html
+load 876834.html
+load 877527.html
+load 877820.html
+load 878014.html
+load 878328.html
+load 878407.html
+load 878478.html
+load 880129.html
+load 880202.html
+load 880342-1.html
+load 880342-2.html
+load 880384.html
+load 880404.html
+load 880724.html
+load 881775.html
+load 882956.html
+load 884459.html
+load 889042.html
+load 907986-1.html
+load 907986-2.html
+load 907986-3.html
+load 907986-4.html
+load 910171-1.html
+load 920987.html
+load 925619-1.html
+load 925619-2.html
+load 926619.html
+load 933151.html
+load 933156.html
+load 944851.html
+load 952756.html
+load 986901.html
+load 990794.html
+load 995289.html
+load 1012609.html
+load 1015662.html
+skip-if(Android) test-pref(media.navigator.permission.disabled,true) load 1028458.html # bug 1048863
+load 1041466.html
+load 1045650.html
+load 1080986.html
+skip-if(Android&&AndroidVersion=='21') load 1180881.html # bug 1409365
+load 1197935.html
+load 1122218.html
+load 1127188.html
+load 1157994.html
+load 1158427.html
+load 1185176.html
+load 1185192.html
+skip-if(Android) load 1257700.html # bug 1575666
+load 1267263.html
+load 1270303.html
+load 1368490.html
+load 1291702.html
+load 1378826.html
+load 1384248.html
+load 1389304.html
+load 1393272.webm
+load 1411322.html
+load 1450845.html
+load 1489160.html
+load disconnect-wrong-destination.html
+load analyser-channels-1.html
+load audiocontext-after-unload-1.html
+load audiocontext-after-xhr.html
+load audiocontext-double-suspend.html
+skip-if(Android) load audioworkletnode-after-unload-1.html # Needs secure context
+load buffer-source-duration-1.html
+load buffer-source-ended-1.html
+load buffer-source-resampling-start-1.html
+load buffer-source-slow-resampling-1.html
+load convolver-memory-report-1.html
+load copyFromChannel-2.html
+load empty-buffer-source.html
+HTTP load media-element-source-seek-1.html
+load offline-buffer-source-ended-1.html
+load oscillator-ended-1.html
+load oscillator-ended-2.html
+skip-if(Android&&AndroidVersion=='22') load video-replay-after-audio-end.html # bug 1315125, bug 1358876
+# This needs to run at the end to avoid leaking busted state into other tests.
+skip-if(Android||ThreadSanitizer) load 691096-1.html # Bug 1365451
+load 1236639.html
+test-pref(media.navigator.permission.disabled,true) test-pref(media.devices.insecure.enabled,true) test-pref(media.getusermedia.insecure.enabled,true) load 1388372.html
+load 1494073.html
+skip-if(Android) load 1526044.html # Bug 1528391
+load 1530897.webm
+skip-if(Android&&AndroidVersion<21) load encrypted-track-with-bad-sample-description-index.mp4 # Bug 1533211, unkip after bug 1550912
+load encrypted-track-without-tenc.mp4 # Bug 1533215
+asserts-if(Android,0-1) load encrypted-track-with-sample-missing-cenc-aux.mp4 # Bug 1533625, bug 1588967
+load 1538727.html
+load empty-samples.webm # Bug 1540580
+test-pref(media.autoplay.block-webaudio,false) load 1545133.html
+load track-with-zero-dimensions.mp4 # Bug 1542539
+load 1560215.html
+skip-if(Android) load 1547784.html # Skip on Android as clearkey is not supported
+load 1547899.html
+load 1569645.html
+load 1575271.html
+load 1577184.html
+pref(media.autoplay.default,0) load 1587248.html
+load 1594466.html
+load 1601385.html
+load 1601422.html
+load 1604941.html
+pref(media.autoplay.default,0) load 1673525.html
+skip-if(!winWidget) load 1608286.html
+load channel-count-in-metadata-different-than-in-content.mp4 # Bug 1584959
+load mp4_box_emptyrange.mp4 # Bug 1667480
+load 1673526-1.html
+load 1673526-2.html
+load 1693043.html
+load 1696511.html
+load 1697521.html
+load 1708790.html
+skip-if(cocoaWidget) load 1709130.html # video failed decoding on MacOS, 1709684
+skip-if(appleSilicon) load 1517199.html
+load 1734008.html
+load 1741677.html
+load 1748272.html
+load 1752917.html
+load 1762620.html
+load 1765842.html
+load adts.aac # Bug 1770073
+load 1787281.html
+load 1798778.html
+load 1830206.html
+load 1830206.html
+load 1833896.mp4
+load 1833894.mp4
+load 1835164.html
+load 1840002.webm
+load 1845350.mp4
diff --git a/dom/media/test/crashtests/disconnect-wrong-destination.html b/dom/media/test/crashtests/disconnect-wrong-destination.html
new file mode 100644
index 0000000000..515ca3c877
--- /dev/null
+++ b/dom/media/test/crashtests/disconnect-wrong-destination.html
@@ -0,0 +1,13 @@
+<script>
+ var oc = new OfflineAudioContext(1, 1, 44100);
+ var splitter = oc.createChannelSplitter(2);
+ var merger0 = oc.createChannelMerger(2);
+ var merger1 = oc.createChannelMerger(2);
+ splitter.connect(merger0, 0);
+ splitter.connect(merger0, 1);
+ splitter.connect(merger1, 0);
+ splitter.connect(merger1, 1);
+
+ splitter.disconnect(merger0, 0);
+ splitter.disconnect(merger1, 1);
+</script>
diff --git a/dom/media/test/crashtests/doppler-1.html b/dom/media/test/crashtests/doppler-1.html
new file mode 100644
index 0000000000..2af3c8f460
--- /dev/null
+++ b/dom/media/test/crashtests/doppler-1.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+var context = new window.AudioContext();
+var source = context.createBufferSource();
+source.buffer = context.createBuffer(1, 1, context.sampleRate);
+source.onended =
+ function(e) {
+ setTimeout(
+ function() {
+ var panner = context.createPanner();
+ source.connect(panner);
+ panner.setVelocity(1.0, 0.0, 0.0);
+ setTimeout(
+ function() {
+ document.documentElement.removeAttribute("class");
+ },
+ 0);
+ },
+ 0);
+ };
+source.start(0);
+</script>
diff --git a/dom/media/test/crashtests/empty-buffer-source.html b/dom/media/test/crashtests/empty-buffer-source.html
new file mode 100644
index 0000000000..2ce48a9ec1
--- /dev/null
+++ b/dom/media/test/crashtests/empty-buffer-source.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+ <title>Bug 1636540: AudioBufferSourceNode with empty buffer</title>
+ <script>
+const offline = new OfflineAudioContext({length: 128, sampleRate: 16384});
+const buffer = new AudioBuffer({length: 1, sampleRate: 21725});
+const node = new AudioBufferSourceNode(offline, {buffer: buffer});
+node.start(5/offline.sampleRate);
+offline.startRendering().then(
+ () => document.documentElement.removeAttribute("class"));
+ </script>
+</head>
+</html>
diff --git a/dom/media/test/crashtests/empty-samples.webm b/dom/media/test/crashtests/empty-samples.webm
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/dom/media/test/crashtests/empty-samples.webm
diff --git a/dom/media/test/crashtests/encrypted-track-with-bad-sample-description-index.mp4 b/dom/media/test/crashtests/encrypted-track-with-bad-sample-description-index.mp4
new file mode 100644
index 0000000000..32303f0357
--- /dev/null
+++ b/dom/media/test/crashtests/encrypted-track-with-bad-sample-description-index.mp4
Binary files differ
diff --git a/dom/media/test/crashtests/encrypted-track-with-sample-missing-cenc-aux.mp4 b/dom/media/test/crashtests/encrypted-track-with-sample-missing-cenc-aux.mp4
new file mode 100644
index 0000000000..875c5dca76
--- /dev/null
+++ b/dom/media/test/crashtests/encrypted-track-with-sample-missing-cenc-aux.mp4
Binary files differ
diff --git a/dom/media/test/crashtests/encrypted-track-without-tenc.mp4 b/dom/media/test/crashtests/encrypted-track-without-tenc.mp4
new file mode 100644
index 0000000000..188faebf1b
--- /dev/null
+++ b/dom/media/test/crashtests/encrypted-track-without-tenc.mp4
Binary files differ
diff --git a/dom/media/test/crashtests/media-element-source-seek-1.html b/dom/media/test/crashtests/media-element-source-seek-1.html
new file mode 100644
index 0000000000..5c3aed5ae7
--- /dev/null
+++ b/dom/media/test/crashtests/media-element-source-seek-1.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+var audioElement = document.createElement("audio");
+audioElement.autoplay = true;
+audioElement.src = "sound.ogg";
+audioElement.onplaying =
+ function() {
+ audioElement.onplaying = null;
+ setTimeout(
+ function() {
+ audioElement.onseeked =
+ function() {
+ // Note we reset 'src' to release decoder resources and cubeb
+ // streams to prevent OOM or OpenCubeb() failures.
+ audioElement.src = "";
+ document.documentElement.removeAttribute("class");
+ };
+ audioElement.currentTime = 0;
+ }, 100);
+ };
+
+var context = new window.AudioContext();
+var source = context.createMediaElementSource(audioElement);
+source.connect(context.destination);
+</script>
+</html>
diff --git a/dom/media/test/crashtests/mp4_box_emptyrange.mp4 b/dom/media/test/crashtests/mp4_box_emptyrange.mp4
new file mode 100644
index 0000000000..83057533a0
--- /dev/null
+++ b/dom/media/test/crashtests/mp4_box_emptyrange.mp4
Binary files differ
diff --git a/dom/media/test/crashtests/offline-buffer-source-ended-1.html b/dom/media/test/crashtests/offline-buffer-source-ended-1.html
new file mode 100644
index 0000000000..0631021126
--- /dev/null
+++ b/dom/media/test/crashtests/offline-buffer-source-ended-1.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+var context = new window.OfflineAudioContext(1, 12001, 12000);
+
+var source = context.createBufferSource();
+source.buffer = context.createBuffer(1, 12000, context.sampleRate);
+source.onended = function(e) {
+ document.documentElement.removeAttribute("class");
+}
+source.connect(context.destination);
+source.start(0);
+
+context.startRendering();
+</script>
diff --git a/dom/media/test/crashtests/oscillator-ended-1.html b/dom/media/test/crashtests/oscillator-ended-1.html
new file mode 100644
index 0000000000..831111261c
--- /dev/null
+++ b/dom/media/test/crashtests/oscillator-ended-1.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+function createContext() {
+ var context = new window.AudioContext();
+ var source = context.createOscillator();
+ source.onended = function(e) {
+ document.documentElement.removeAttribute("class");
+ };
+ source.connect(context.destination);
+ source.start(0.49);
+ source.stop(0.5);
+}
+createContext();
+</script>
diff --git a/dom/media/test/crashtests/oscillator-ended-2.html b/dom/media/test/crashtests/oscillator-ended-2.html
new file mode 100644
index 0000000000..ee9b8cf300
--- /dev/null
+++ b/dom/media/test/crashtests/oscillator-ended-2.html
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<script>
+function createContext() {
+ var context = new window.AudioContext();
+ var source = context.createOscillator();
+ source.onended = function(e) {
+ document.documentElement.removeAttribute("class");
+ };
+ source.connect(context.destination);
+ source.start(60);
+ source.stop(0.5);
+}
+createContext();
+</script>
diff --git a/dom/media/test/crashtests/sound.ogg b/dom/media/test/crashtests/sound.ogg
new file mode 100644
index 0000000000..edda4e9128
--- /dev/null
+++ b/dom/media/test/crashtests/sound.ogg
Binary files differ
diff --git a/dom/media/test/crashtests/test.mp4 b/dom/media/test/crashtests/test.mp4
new file mode 100644
index 0000000000..44d7ee9cbc
--- /dev/null
+++ b/dom/media/test/crashtests/test.mp4
Binary files differ
diff --git a/dom/media/test/crashtests/track-with-zero-dimensions.mp4 b/dom/media/test/crashtests/track-with-zero-dimensions.mp4
new file mode 100644
index 0000000000..3f4a1317f3
--- /dev/null
+++ b/dom/media/test/crashtests/track-with-zero-dimensions.mp4
Binary files differ
diff --git a/dom/media/test/crashtests/video-crash.webm b/dom/media/test/crashtests/video-crash.webm
new file mode 100644
index 0000000000..9532113d87
--- /dev/null
+++ b/dom/media/test/crashtests/video-crash.webm
Binary files differ
diff --git a/dom/media/test/crashtests/video-replay-after-audio-end.html b/dom/media/test/crashtests/video-replay-after-audio-end.html
new file mode 100644
index 0000000000..9ffd6078de
--- /dev/null
+++ b/dom/media/test/crashtests/video-replay-after-audio-end.html
@@ -0,0 +1,43 @@
+<html class="reftest-wait">
+<head>
+ <title> Bug 1242774 : video crashed if pause and play again after audio track ends </title>
+</head>
+<body>
+<script type="text/javascript">
+function assert(value, msg) {
+ if (!value) {
+ dump("### Error : " + msg + "\n");
+ }
+}
+
+var AUDIO_END_TIME = 4.5;
+var video = document.createElement('video');
+video.src = "video-crash.webm";
+video.play();
+
+video.ontimeupdate = function () {
+ assert(AUDIO_END_TIME < video.duration,
+ "AUDIO_END_TIME should be smaller than the duration!");
+
+ if (video.currentTime > AUDIO_END_TIME) {
+ dump("### Pause video during silent part.\n");
+ video.ontimeupdate = null;
+ video.pause();
+ }
+
+ video.onpause = function () {
+ video.onpause = null;
+ setTimeout(function() {
+ dump("### Re-play after pausing during silent part.\n");
+ video.play();
+ video.onended = function () {
+ video.onended = null;
+ dump("### Video is ended.\n");
+ document.documentElement.removeAttribute("class");
+ }
+ }, 1000);
+ }
+}
+</script>
+</body>
+</html> \ No newline at end of file
diff --git a/dom/media/test/dash/dash-manifest-garbled-webm.mpd b/dom/media/test/dash/dash-manifest-garbled-webm.mpd
new file mode 100644
index 0000000000..aa78ded3ec
--- /dev/null
+++ b/dom/media/test/dash/dash-manifest-garbled-webm.mpd
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<MPD
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns="urn:mpeg:DASH:schema:MPD:2011"
+ xsi:schemaLocation="urn:mpeg:DASH:schema:MPD:2011"
+ type="static"
+ mediaPresentationDuration="PT3.958S"
+ minBufferTime="PT1S"
+ profiles="urn:webm:dash:profile:webm-on-demand:2012">
+ <BaseURL>./</BaseURL>
+ <Period id="0" start="PT0S" duration="PT3.958S" >
+ <AdaptationSet id="0" mimeType="video/webm" codecs="vp8" lang="eng" subsegmentAlignment="true" subsegmentStartsWithSAP="1" bitstreamSwitching="true">
+ <Representation id="0" bandwidth="54207" width="320" height="180">
+ <BaseURL>garbled.webm</BaseURL>
+ <SegmentBase indexRange="35090-35123">
+ <Initialization range="0-228" />
+ </SegmentBase>
+ </Representation>
+ <Representation id="1" bandwidth="78006" width="428" height="240">
+ <BaseURL>dash-webm-video-428x240.webm</BaseURL>
+ <SegmentBase indexRange="50173-50206">
+ <Initialization range="0-228" />
+ </SegmentBase>
+ </Representation>
+ </AdaptationSet>
+ <AdaptationSet id="1" mimeType="audio/webm" codecs="vorbis" lang="eng" audioSamplingRate="48000" subsegmentStartsWithSAP="1">
+ <Representation id="2" bandwidth="57264">
+ <BaseURL>dash-webm-audio-128k.webm</BaseURL>
+ <SegmentBase indexRange="41927-41946">
+ <Initialization range="0-4521" />
+ </SegmentBase>
+ </Representation>
+ </AdaptationSet>
+ </Period>
+</MPD>
diff --git a/dom/media/test/dash/dash-manifest-garbled.mpd b/dom/media/test/dash/dash-manifest-garbled.mpd
new file mode 100644
index 0000000000..ac8eadbddc
--- /dev/null
+++ b/dom/media/test/dash/dash-manifest-garbled.mpd
@@ -0,0 +1 @@
+PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxNUEQgbWVkaWFQcmVzZW50YXRpb25EdXJhdGlvbj0iUFQxOS41MVMiIG1pbkJ1ZmZlclRpbWU9IlBUMVMiIHByb2ZpbGVzPSJ1cm46d2VibTpkYXNoOnByb2ZpbGU6d2VibS1vbi1kZW1hbmQ6MjAxMiIgdHlwZT0ic3RhdGljIiB4bWxucz0idXJuOm1wZWc6REFTSDpzY2hlbWE6TVBEOjIwMTEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTpzY2hlbWFMb2NhdGlvbj0idXJuOm1wZWc6REFTSDpzY2hlbWE6TVBEOjIwMTEiPjxCYXNlVVJMPmh0dHA6Ly93d3cuZ29vZ2xlLmNvbTwvQmFzZVVSTD48UGVyaW9kIGR1cmF0aW9uPSJQVDE5LjUxUyIgaWQ9IjAiIHN0YXJ0PSJQVDBTIj48QWRhcHRhdGlvblNldCBhdWRpb1NhbXBsaW5nUmF0ZT0iNDgwMDAiIGNvZGVjcz0idm9yYmlzIiBpZD0iMSIgbGFuZz0iZW5nIiBtaW1lVHlwZT0iYXVkaW8vd2VibSIgc3Vic2VnbWVudFN0YXJ0c1dpdGhTQVA9IjEiPjxSZXByZXNlbnRhdGlvbiBiYW5kd2lkdGg9IjIwMTA5IiBpZD0iMiI+PEJhc2VVUkwvPjxTZWdtZW50QmFzZSBpbmRleFJhbmdlPSIzMTk3ODAtMzIwNjEyIj48SW5pdGlhbGl6YXRpb24gcmFuZ2U9IjAtMjA4NzAiLz48L1NlZ21lbnRCYXNlPjwvUmVwcmVzZW50YXRpb24+PC9BZGFwdGF0aW9uU2V0PjwvUGVyaW9kPjwvTVBEPg
diff --git a/dom/media/test/dash/dash-manifest-sjs.mpd b/dom/media/test/dash/dash-manifest-sjs.mpd
new file mode 100644
index 0000000000..c7ecba3c69
--- /dev/null
+++ b/dom/media/test/dash/dash-manifest-sjs.mpd
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<MPD
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns="urn:mpeg:DASH:schema:MPD:2011"
+ xsi:schemaLocation="urn:mpeg:DASH:schema:MPD:2011"
+ type="static"
+ mediaPresentationDuration="PT3.958S"
+ minBufferTime="PT1S"
+ profiles="urn:webm:dash:profile:webm-on-demand:2012">
+ <BaseURL>./dash_detect_stream_switch.sjs?name=</BaseURL>
+ <Period id="0" start="PT0S" duration="PT3.958S" >
+ <AdaptationSet id="0" mimeType="video/webm" codecs="vp8" lang="eng" subsegmentAlignment="true" subsegmentStartsWithSAP="1" bitstreamSwitching="true">
+ <Representation id="0" bandwidth="54207" width="320" height="180">
+ <BaseURL>dash-webm-video-320x180.webm</BaseURL>
+ <SegmentBase indexRange="35090-35123">
+ <Initialization range="0-228" />
+ </SegmentBase>
+ </Representation>
+ <Representation id="1" bandwidth="78006" width="428" height="240">
+ <BaseURL>dash-webm-video-428x240.webm</BaseURL>
+ <SegmentBase indexRange="50173-50206">
+ <Initialization range="0-228" />
+ </SegmentBase>
+ </Representation>
+ </AdaptationSet>
+ <AdaptationSet id="1" mimeType="audio/webm" codecs="vorbis" lang="eng" audioSamplingRate="48000" subsegmentStartsWithSAP="1">
+ <Representation id="2" bandwidth="57264">
+ <BaseURL>dash-webm-audio-128k.webm</BaseURL>
+ <SegmentBase indexRange="41927-41946">
+ <Initialization range="0-4521" />
+ </SegmentBase>
+ </Representation>
+ </AdaptationSet>
+ </Period>
+</MPD>
diff --git a/dom/media/test/dash/dash-manifest.mpd b/dom/media/test/dash/dash-manifest.mpd
new file mode 100644
index 0000000000..98c7a90480
--- /dev/null
+++ b/dom/media/test/dash/dash-manifest.mpd
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<MPD
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns="urn:mpeg:DASH:schema:MPD:2011"
+ xsi:schemaLocation="urn:mpeg:DASH:schema:MPD:2011"
+ type="static"
+ mediaPresentationDuration="PT3.958S"
+ minBufferTime="PT1S"
+ profiles="urn:webm:dash:profile:webm-on-demand:2012">
+ <BaseURL>./</BaseURL>
+ <Period id="0" start="PT0S" duration="PT3.958S" >
+ <AdaptationSet id="0" mimeType="video/webm" codecs="vp8" lang="eng" subsegmentAlignment="true" subsegmentStartsWithSAP="1" bitstreamSwitching="true">
+ <Representation id="0" bandwidth="54207" width="320" height="180">
+ <BaseURL>dash-webm-video-320x180.webm</BaseURL>
+ <SegmentBase indexRange="35090-35123">
+ <Initialization range="0-228" />
+ </SegmentBase>
+ </Representation>
+ <Representation id="1" bandwidth="78006" width="428" height="240">
+ <BaseURL>dash-webm-video-428x240.webm</BaseURL>
+ <SegmentBase indexRange="50173-50206">
+ <Initialization range="0-228" />
+ </SegmentBase>
+ </Representation>
+ </AdaptationSet>
+ <AdaptationSet id="1" mimeType="audio/webm" codecs="vorbis" lang="eng" audioSamplingRate="48000" subsegmentStartsWithSAP="1">
+ <Representation id="2" bandwidth="57264">
+ <BaseURL>dash-webm-audio-128k.webm</BaseURL>
+ <SegmentBase indexRange="41927-41946">
+ <Initialization range="0-4521" />
+ </SegmentBase>
+ </Representation>
+ </AdaptationSet>
+ </Period>
+</MPD>
diff --git a/dom/media/test/dash/dash-webm-audio-128k.webm b/dom/media/test/dash/dash-webm-audio-128k.webm
new file mode 100644
index 0000000000..f56c042053
--- /dev/null
+++ b/dom/media/test/dash/dash-webm-audio-128k.webm
Binary files differ
diff --git a/dom/media/test/dash/dash-webm-video-320x180.webm b/dom/media/test/dash/dash-webm-video-320x180.webm
new file mode 100644
index 0000000000..282e6a2cc3
--- /dev/null
+++ b/dom/media/test/dash/dash-webm-video-320x180.webm
Binary files differ
diff --git a/dom/media/test/dash/dash-webm-video-428x240.webm b/dom/media/test/dash/dash-webm-video-428x240.webm
new file mode 100644
index 0000000000..23f2c89616
--- /dev/null
+++ b/dom/media/test/dash/dash-webm-video-428x240.webm
Binary files differ
diff --git a/dom/media/test/dash/garbled.webm b/dom/media/test/dash/garbled.webm
new file mode 100644
index 0000000000..ac8eadbddc
--- /dev/null
+++ b/dom/media/test/dash/garbled.webm
@@ -0,0 +1 @@
+PD94bWwgdmVyc2lvbj0iMS4wIiA/PjxNUEQgbWVkaWFQcmVzZW50YXRpb25EdXJhdGlvbj0iUFQxOS41MVMiIG1pbkJ1ZmZlclRpbWU9IlBUMVMiIHByb2ZpbGVzPSJ1cm46d2VibTpkYXNoOnByb2ZpbGU6d2VibS1vbi1kZW1hbmQ6MjAxMiIgdHlwZT0ic3RhdGljIiB4bWxucz0idXJuOm1wZWc6REFTSDpzY2hlbWE6TVBEOjIwMTEiIHhtbG5zOnhzaT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEtaW5zdGFuY2UiIHhzaTpzY2hlbWFMb2NhdGlvbj0idXJuOm1wZWc6REFTSDpzY2hlbWE6TVBEOjIwMTEiPjxCYXNlVVJMPmh0dHA6Ly93d3cuZ29vZ2xlLmNvbTwvQmFzZVVSTD48UGVyaW9kIGR1cmF0aW9uPSJQVDE5LjUxUyIgaWQ9IjAiIHN0YXJ0PSJQVDBTIj48QWRhcHRhdGlvblNldCBhdWRpb1NhbXBsaW5nUmF0ZT0iNDgwMDAiIGNvZGVjcz0idm9yYmlzIiBpZD0iMSIgbGFuZz0iZW5nIiBtaW1lVHlwZT0iYXVkaW8vd2VibSIgc3Vic2VnbWVudFN0YXJ0c1dpdGhTQVA9IjEiPjxSZXByZXNlbnRhdGlvbiBiYW5kd2lkdGg9IjIwMTA5IiBpZD0iMiI+PEJhc2VVUkwvPjxTZWdtZW50QmFzZSBpbmRleFJhbmdlPSIzMTk3ODAtMzIwNjEyIj48SW5pdGlhbGl6YXRpb24gcmFuZ2U9IjAtMjA4NzAiLz48L1NlZ21lbnRCYXNlPjwvUmVwcmVzZW50YXRpb24+PC9BZGFwdGF0aW9uU2V0PjwvUGVyaW9kPjwvTVBEPg
diff --git a/dom/media/test/dash_detect_stream_switch.sjs b/dom/media/test/dash_detect_stream_switch.sjs
new file mode 100644
index 0000000000..b8480a1274
--- /dev/null
+++ b/dom/media/test/dash_detect_stream_switch.sjs
@@ -0,0 +1,143 @@
+/* -*- Mode: JavaScript; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* dash_detect_stream_switch.sjs
+ *
+ * Parses requests for DASH manifests and ensures stream switching takes place
+ * by verifying the subsegments downloaded and the streams they belong to.
+ * If unexpected subsegments (byte ranges) are requested, the script will
+ * will respond with a 404.
+ */
+
+var DEBUG = false;
+
+function parseQuery(request, key) {
+ var params = request.queryString.split("&");
+ if (DEBUG) {
+ dump('DASH-SJS: request params = "' + params + '"\n');
+ }
+ for (var j = 0; j < params.length; ++j) {
+ var p = params[j];
+ if (p == key) {
+ return true;
+ }
+ if (p.indexOf(key + "=") === 0) {
+ return p.substring(key.length + 1);
+ }
+ if (!p.includes("=") && key === "") {
+ return p;
+ }
+ }
+ return false;
+}
+
+function handleRequest(request, response) {
+ try {
+ var name = parseQuery(request, "name");
+ var range = request.hasHeader("Range")
+ ? request.getHeader("Range")
+ : undefined;
+
+ // Should not get request for 1st subsegment from 2nd stream, nor 2nd
+ // subsegment from 1st stream.
+ if (
+ (name == "dash-webm-video-320x180.webm" &&
+ range == "bytes=25514-32767") ||
+ (name == "dash-webm-video-428x240.webm" && range == "bytes=228-35852")
+ ) {
+ throw new Error(
+ "Should not request " + name + " with byte-range " + range
+ );
+ } else {
+ var rangeSplit = range.split("=");
+ if (rangeSplit.length != 2) {
+ throw new Error(
+ "DASH-SJS: ERROR: invalid number of tokens (" +
+ rangeSplit.length +
+ ") delimited by '=' in 'Range' header."
+ );
+ }
+ var offsets = rangeSplit[1].split("-");
+ if (offsets.length != 2) {
+ throw new Error(
+ "DASH-SJS: ERROR: invalid number of tokens (" +
+ offsets.length +
+ ") delimited by '-' in 'Range' header."
+ );
+ }
+ var startOffset = parseInt(offsets[0]);
+ var endOffset = parseInt(offsets[1]);
+ var file = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
+ var fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ var bis = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+ Ci.nsIBinaryInputStream
+ );
+
+ var paths = "tests/dom/media/test/" + name;
+ var split = paths.split("/");
+ for (var i = 0; i < split.length; ++i) {
+ file.append(split[i]);
+ }
+
+ fis.init(file, -1, -1, false);
+ // Exception: start offset should be within file bounds.
+ if (startOffset > file.fileSize) {
+ throw new Error(
+ "Starting offset [" +
+ startOffset +
+ "] is after end of file [" +
+ file.fileSize +
+ "]."
+ );
+ }
+ // End offset may be too large in the MPD. Real world HTTP servers just
+ // return what data they can; do the same here - reduce the end offset.
+ if (endOffset >= file.fileSize) {
+ if (DEBUG) {
+ dump(
+ "DASH-SJS: reducing endOffset [" +
+ endOffset +
+ "] to fileSize [" +
+ (file.fileSize - 1) +
+ "]\n"
+ );
+ }
+ endOffset = file.fileSize - 1;
+ }
+ fis.seek(Ci.nsISeekableStream.NS_SEEK_SET, startOffset);
+ bis.setInputStream(fis);
+
+ var byteLengthToRead = endOffset + 1 - startOffset;
+ var totalBytesExpected = byteLengthToRead + startOffset;
+ if (DEBUG) {
+ dump(
+ "DASH-SJS: byteLengthToRead = " +
+ byteLengthToRead +
+ " byteLengthToRead+startOffset = " +
+ totalBytesExpected +
+ " fileSize = " +
+ file.fileSize +
+ "\n"
+ );
+ }
+
+ var bytes = bis.readBytes(byteLengthToRead);
+ response.setStatusLine(request.httpVersion, 206, "Partial Content");
+ response.setHeader("Content-Length", "" + bytes.length, false);
+ response.setHeader("Content-Type", "application/dash+xml", false);
+ var contentRange =
+ "bytes " + startOffset + "-" + endOffset + "/" + file.fileSize;
+ response.setHeader("Content-Range", contentRange, false);
+ response.write(bytes, bytes.length);
+ bis.close();
+ }
+ } catch (e) {
+ dump("DASH-SJS-ERROR: " + e + "\n");
+ response.setStatusLine(request.httpVersion, 404, "Not found");
+ }
+}
diff --git a/dom/media/test/detodos-recorder-test.opus b/dom/media/test/detodos-recorder-test.opus
new file mode 100644
index 0000000000..88b2eab0f8
--- /dev/null
+++ b/dom/media/test/detodos-recorder-test.opus
Binary files differ
diff --git a/dom/media/test/detodos-recorder-test.opus^headers^ b/dom/media/test/detodos-recorder-test.opus^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/detodos-recorder-test.opus^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/detodos-short.opus b/dom/media/test/detodos-short.opus
new file mode 100644
index 0000000000..8bda283fc5
--- /dev/null
+++ b/dom/media/test/detodos-short.opus
Binary files differ
diff --git a/dom/media/test/detodos-short.opus^headers^ b/dom/media/test/detodos-short.opus^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/detodos-short.opus^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/detodos-short.webm b/dom/media/test/detodos-short.webm
new file mode 100644
index 0000000000..45af2675a6
--- /dev/null
+++ b/dom/media/test/detodos-short.webm
Binary files differ
diff --git a/dom/media/test/detodos-short.webm^headers^ b/dom/media/test/detodos-short.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/detodos-short.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/detodos.opus b/dom/media/test/detodos.opus
new file mode 100644
index 0000000000..6c7ba88a66
--- /dev/null
+++ b/dom/media/test/detodos.opus
Binary files differ
diff --git a/dom/media/test/detodos.opus^headers^ b/dom/media/test/detodos.opus^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/detodos.opus^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/detodos.webm b/dom/media/test/detodos.webm
new file mode 100644
index 0000000000..39cfa7f537
--- /dev/null
+++ b/dom/media/test/detodos.webm
Binary files differ
diff --git a/dom/media/test/detodos.webm^headers^ b/dom/media/test/detodos.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/detodos.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/dirac.ogg b/dom/media/test/dirac.ogg
new file mode 100644
index 0000000000..2986cf1e80
--- /dev/null
+++ b/dom/media/test/dirac.ogg
Binary files differ
diff --git a/dom/media/test/dirac.ogg^headers^ b/dom/media/test/dirac.ogg^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/dirac.ogg^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/dynamic_resource.sjs b/dom/media/test/dynamic_resource.sjs
new file mode 100644
index 0000000000..98c02e552d
--- /dev/null
+++ b/dom/media/test/dynamic_resource.sjs
@@ -0,0 +1,53 @@
+function parseQuery(request, key) {
+ var params = request.queryString.split("&");
+ for (var j = 0; j < params.length; ++j) {
+ var p = params[j];
+ if (p == key) {
+ return true;
+ }
+ if (p.indexOf(key + "=") == 0) {
+ return p.substring(key.length + 1);
+ }
+ if (!p.includes("=") && key == "") {
+ return p;
+ }
+ }
+ return false;
+}
+
+// Return resource1 file content for the first request with a given key.
+// All subsequent requests return resource2. Both must be video/ogg.
+function handleRequest(request, response) {
+ var key = parseQuery(request, "key");
+ var resource1 = parseQuery(request, "res1");
+ var resource2 = parseQuery(request, "res2");
+
+ var resource = getState(key) == "2" ? resource2 : resource1;
+ setState(key, "2");
+
+ var file = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
+ var fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ var bis = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+ Ci.nsIBinaryInputStream
+ );
+ var paths = "tests/dom/media/test/" + resource;
+ var split = paths.split("/");
+ for (var i = 0; i < split.length; ++i) {
+ file.append(split[i]);
+ }
+ fis.init(file, -1, -1, false);
+ dump("file=" + file + "\n");
+ bis.setInputStream(fis);
+ var bytes = bis.readBytes(bis.available());
+ response.setStatusLine(request.httpVersion, 206, "Partial Content");
+ response.setHeader(
+ "Content-Range",
+ "bytes 0-" + (bytes.length - 1) + "/" + bytes.length
+ );
+ response.setHeader("Content-Length", "" + bytes.length, false);
+ response.setHeader("Content-Type", "video/ogg", false);
+ response.write(bytes, bytes.length);
+ bis.close();
+}
diff --git a/dom/media/test/eme.js b/dom/media/test/eme.js
new file mode 100644
index 0000000000..927c99876a
--- /dev/null
+++ b/dom/media/test/eme.js
@@ -0,0 +1,479 @@
+/* import-globals-from manifest.js */
+
+const CLEARKEY_KEYSYSTEM = "org.w3.clearkey";
+
+const gCencMediaKeySystemConfig = [
+ {
+ initDataTypes: ["cenc"],
+ videoCapabilities: [{ contentType: "video/mp4" }],
+ audioCapabilities: [{ contentType: "audio/mp4" }],
+ },
+];
+
+function bail(message) {
+ return function (err) {
+ if (err) {
+ message += "; " + String(err);
+ }
+ ok(false, message);
+ if (err) {
+ info(String(err));
+ }
+ SimpleTest.finish();
+ };
+}
+
+function ArrayBufferToString(arr) {
+ var str = "";
+ var view = new Uint8Array(arr);
+ for (var i = 0; i < view.length; i++) {
+ str += String.fromCharCode(view[i]);
+ }
+ return str;
+}
+
+function StringToArrayBuffer(str) {
+ var arr = new ArrayBuffer(str.length);
+ var view = new Uint8Array(arr);
+ for (var i = 0; i < str.length; i++) {
+ view[i] = str.charCodeAt(i);
+ }
+ return arr;
+}
+
+function StringToHex(str) {
+ var res = "";
+ for (var i = 0; i < str.length; ++i) {
+ res += ("0" + str.charCodeAt(i).toString(16)).slice(-2);
+ }
+ return res;
+}
+
+function Base64ToHex(str) {
+ var bin = window.atob(str.replace(/-/g, "+").replace(/_/g, "/"));
+ var res = "";
+ for (var i = 0; i < bin.length; i++) {
+ res += ("0" + bin.charCodeAt(i).toString(16)).substr(-2);
+ }
+ return res;
+}
+
+function HexToBase64(hex) {
+ var bin = "";
+ for (var i = 0; i < hex.length; i += 2) {
+ bin += String.fromCharCode(parseInt(hex.substr(i, 2), 16));
+ }
+ return window
+ .btoa(bin)
+ .replace(/=/g, "")
+ .replace(/\+/g, "-")
+ .replace(/\//g, "_");
+}
+
+function TimeRangesToString(trs) {
+ var l = trs.length;
+ if (l === 0) {
+ return "-";
+ }
+ var s = "";
+ var i = 0;
+ for (;;) {
+ s += trs.start(i) + "-" + trs.end(i);
+ if (++i === l) {
+ return s;
+ }
+ s += ",";
+ }
+}
+
+function SourceBufferToString(sb) {
+ return (
+ "SourceBuffer{" +
+ "AppendMode=" +
+ (sb.AppendMode || "-") +
+ ", updating=" +
+ (sb.updating ? "true" : "false") +
+ ", buffered=" +
+ TimeRangesToString(sb.buffered) +
+ ", audioTracks=" +
+ (sb.audioTracks ? sb.audioTracks.length : "-") +
+ ", videoTracks=" +
+ (sb.videoTracks ? sb.videoTracks.length : "-") +
+ "}"
+ );
+}
+
+function SourceBufferListToString(sbl) {
+ return "SourceBufferList[" + sbl.map(SourceBufferToString).join(", ") + "]";
+}
+
+function GenerateClearKeyLicense(licenseRequest, keyStore) {
+ var msgStr = ArrayBufferToString(licenseRequest);
+ var msg = JSON.parse(msgStr);
+
+ var keys = [];
+ for (var i = 0; i < msg.kids.length; i++) {
+ var id64 = msg.kids[i];
+ var idHex = Base64ToHex(msg.kids[i]).toLowerCase();
+ var key = keyStore[idHex];
+
+ if (key) {
+ keys.push({
+ kty: "oct",
+ kid: id64,
+ k: HexToBase64(key),
+ });
+ }
+ }
+
+ return new TextEncoder().encode(
+ JSON.stringify({
+ keys,
+ type: msg.type || "temporary",
+ })
+ );
+}
+
+function UpdateSessionFunc(test, token, sessionType, resolve, reject) {
+ return function (ev) {
+ var license = GenerateClearKeyLicense(ev.message, test.keys);
+ Log(
+ token,
+ "sending update message to CDM: " + new TextDecoder().decode(license)
+ );
+ ev.target
+ .update(license)
+ .then(function () {
+ Log(token, "MediaKeySession update ok!");
+ resolve(ev.target);
+ })
+ .catch(function (reason) {
+ reject(`${token} MediaKeySession update failed: ${reason}`);
+ });
+ };
+}
+
+function MaybeCrossOriginURI(test, uri) {
+ if (test.crossOrigin) {
+ return "https://example.com:443/tests/dom/media/test/allowed.sjs?" + uri;
+ }
+ return uri;
+}
+
+function AppendTrack(test, ms, track, token) {
+ return new Promise(function (resolve, reject) {
+ var sb;
+ var curFragment = 0;
+ var fragments = track.fragments;
+ var fragmentFile;
+
+ function addNextFragment() {
+ if (curFragment >= fragments.length) {
+ Log(token, track.name + ": end of track");
+ resolve();
+ return;
+ }
+
+ fragmentFile = MaybeCrossOriginURI(test, fragments[curFragment++]);
+
+ var req = new XMLHttpRequest();
+ req.open("GET", fragmentFile);
+ req.responseType = "arraybuffer";
+
+ req.addEventListener("load", function () {
+ Log(
+ token,
+ track.name + ": fetch of " + fragmentFile + " complete, appending"
+ );
+ sb.appendBuffer(new Uint8Array(req.response));
+ });
+
+ req.addEventListener("error", function () {
+ reject(`${token} - ${track.name}: error fetching ${fragmentFile}`);
+ });
+ req.addEventListener("abort", function () {
+ reject(`${token} - ${track.name}: aborted fetching ${fragmentFile}`);
+ });
+
+ Log(
+ token,
+ track.name +
+ ": addNextFragment() fetching next fragment " +
+ fragmentFile
+ );
+ req.send(null);
+ }
+
+ Log(token, track.name + ": addSourceBuffer(" + track.type + ")");
+ sb = ms.addSourceBuffer(track.type);
+ sb.addEventListener("updateend", function () {
+ Log(
+ token,
+ track.name +
+ ": updateend for " +
+ fragmentFile +
+ ", " +
+ SourceBufferToString(sb)
+ );
+ addNextFragment();
+ });
+
+ addNextFragment();
+ });
+}
+
+//Returns a promise that is resolved when the media element is ready to have
+//its play() function called; when it's loaded MSE fragments.
+function LoadTest(test, elem, token, endOfStream = true) {
+ if (!test.tracks) {
+ ok(false, token + " test does not have a tracks list");
+ return Promise.reject();
+ }
+
+ var ms = new MediaSource();
+ elem.src = URL.createObjectURL(ms);
+ elem.crossOrigin = test.crossOrigin || false;
+
+ return new Promise(function (resolve, reject) {
+ ms.addEventListener(
+ "sourceopen",
+ function () {
+ Log(token, "sourceopen");
+ Promise.all(
+ test.tracks.map(function (track) {
+ return AppendTrack(test, ms, track, token);
+ })
+ )
+ .then(function () {
+ Log(token, "Tracks loaded, calling MediaSource.endOfStream()");
+ if (endOfStream) {
+ ms.endOfStream();
+ }
+ resolve();
+ })
+ .catch(reject);
+ },
+ { once: true }
+ );
+ });
+}
+
+function EMEPromise() {
+ var self = this;
+ self.promise = new Promise(function (resolve, reject) {
+ self.resolve = resolve;
+ self.reject = reject;
+ });
+}
+
+/*
+ * Create a new MediaKeys object.
+ * Return a promise which will be resolved with a new MediaKeys object,
+ * or will be rejected with a string that describes the failure.
+ */
+function CreateMediaKeys(v, test, token) {
+ let p = new EMEPromise();
+
+ function streamType(type) {
+ var x = test.tracks.find(o => o.name == type);
+ return x ? x.type : undefined;
+ }
+
+ function onencrypted(ev) {
+ var options = { initDataTypes: [ev.initDataType] };
+ if (streamType("video")) {
+ options.videoCapabilities = [{ contentType: streamType("video") }];
+ }
+ if (streamType("audio")) {
+ options.audioCapabilities = [{ contentType: streamType("audio") }];
+ }
+ navigator.requestMediaKeySystemAccess(CLEARKEY_KEYSYSTEM, [options]).then(
+ keySystemAccess => {
+ keySystemAccess
+ .createMediaKeys()
+ .then(p.resolve, () =>
+ p.reject(`${token} Failed to create MediaKeys object.`)
+ );
+ },
+ () => p.reject(`${token} Failed to request key system access.`)
+ );
+ }
+
+ v.addEventListener("encrypted", onencrypted, { once: true });
+ return p.promise;
+}
+
+/*
+ * Create a new MediaKeys object and provide it to the media element.
+ * Return a promise which will be resolved if succeeded, or will be rejected
+ * with a string that describes the failure.
+ */
+function CreateAndSetMediaKeys(v, test, token) {
+ let p = new EMEPromise();
+
+ CreateMediaKeys(v, test, token).then(mediaKeys => {
+ v.setMediaKeys(mediaKeys).then(p.resolve, () =>
+ p.reject(`${token} Failed to set MediaKeys on <video> element.`)
+ );
+ }, p.reject);
+
+ return p.promise;
+}
+
+/*
+ * Collect the init data from 'encrypted' events.
+ * Return a promise which will be resolved with the init data when collection
+ * is completed (specified by test.sessionCount).
+ */
+function LoadInitData(v, test, token) {
+ let p = new EMEPromise();
+ let initDataQueue = [];
+
+ // Call SimpleTest._originalSetTimeout() to bypass the flaky timeout checker.
+ let timer = SimpleTest._originalSetTimeout.call(
+ window,
+ () => {
+ p.reject(`${token} Timed out in waiting for the init data.`);
+ },
+ 60000
+ );
+
+ function onencrypted(ev) {
+ initDataQueue.push(ev);
+ Log(
+ token,
+ `got encrypted(${ev.initDataType}, ` +
+ `${StringToHex(ArrayBufferToString(ev.initData))}) event.`
+ );
+ if (test.sessionCount == initDataQueue.length) {
+ p.resolve(initDataQueue);
+ clearTimeout(timer);
+ }
+ }
+
+ v.addEventListener("encrypted", onencrypted);
+ return p.promise;
+}
+
+/*
+ * Generate a license request and update the session.
+ * Return a promsise which will be resolved with the updated session
+ * or rejected with a string that describes the failure.
+ */
+function MakeRequest(test, token, ev, session, sessionType) {
+ sessionType = sessionType || "temporary";
+ let p = new EMEPromise();
+ let str =
+ `session[${session.sessionId}].generateRequest(` +
+ `${ev.initDataType}, ${StringToHex(ArrayBufferToString(ev.initData))})`;
+
+ session.addEventListener(
+ "message",
+ UpdateSessionFunc(test, token, sessionType, p.resolve, p.reject)
+ );
+
+ Log(token, str);
+ session.generateRequest(ev.initDataType, ev.initData).catch(reason => {
+ // Reject the promise if generateRequest() failed.
+ // Otherwise it will be resolved in UpdateSessionFunc().
+ p.reject(`${token}: ${str} failed; ${reason}`);
+ });
+
+ return p.promise;
+}
+
+/*
+ * Process the init data by calling MakeRequest().
+ * Return a promise which will be resolved with the updated sessions
+ * when all init data are processed or rejected if any failure.
+ */
+function ProcessInitData(v, test, token, initData, sessionType) {
+ return Promise.all(
+ initData.map(ev => {
+ let session = v.mediaKeys.createSession(sessionType);
+ return MakeRequest(test, token, ev, session, sessionType);
+ })
+ );
+}
+
+/*
+ * Clean up the |v| element.
+ */
+function CleanUpMedia(v) {
+ v.setMediaKeys(null);
+ v.remove();
+ v.removeAttribute("src");
+ v.load();
+}
+
+/*
+ * Close all sessions and clean up the |v| element.
+ */
+function CloseSessions(v, sessions) {
+ return Promise.all(sessions.map(s => s.close())).then(CleanUpMedia(v));
+}
+
+/*
+ * Set up media keys and source buffers for the media element.
+ * Return a promise resolved when all key sessions are updated or rejected
+ * if any failure.
+ */
+function SetupEME(v, test, token) {
+ let p = new EMEPromise();
+
+ v.onerror = function () {
+ p.reject(`${token} got an error event.`);
+ };
+
+ Promise.all([
+ LoadInitData(v, test, token),
+ CreateAndSetMediaKeys(v, test, token),
+ LoadTest(test, v, token),
+ ])
+ .then(values => {
+ let initData = values[0];
+ return ProcessInitData(v, test, token, initData);
+ })
+ .then(p.resolve, p.reject);
+
+ return p.promise;
+}
+
+function fetchWithXHR(uri, onLoadFunction) {
+ var p = new Promise(function (resolve, reject) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", uri, true);
+ xhr.responseType = "arraybuffer";
+ xhr.addEventListener("load", function () {
+ is(
+ xhr.status,
+ 200,
+ "fetchWithXHR load uri='" + uri + "' status=" + xhr.status
+ );
+ resolve(xhr.response);
+ });
+ xhr.send();
+ });
+
+ if (onLoadFunction) {
+ p.then(onLoadFunction);
+ }
+
+ return p;
+}
+
+function once(target, name, cb) {
+ var p = new Promise(function (resolve, reject) {
+ target.addEventListener(
+ name,
+ function (arg) {
+ resolve(arg);
+ },
+ { once: true }
+ );
+ });
+ if (cb) {
+ p.then(cb);
+ }
+ return p;
+}
diff --git a/dom/media/test/eme_standalone.js b/dom/media/test/eme_standalone.js
new file mode 100644
index 0000000000..202259b7fa
--- /dev/null
+++ b/dom/media/test/eme_standalone.js
@@ -0,0 +1,286 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file offers standalone (no dependencies on other files) EME test
+// helpers. The intention is that this file can be used to provide helpers
+// while not coupling tests as tightly to `dom/media/test/manifest.js` or other
+// files. This allows these helpers to be used in different tests across the
+// codebase without imports becoming a mess.
+
+// A helper class to assist in setting up EME on media.
+//
+// Usage
+// 1. First configure the EME helper so it can have the information needed
+// to setup EME correctly. This is done by setting
+// - keySystem via `SetKeySystem`.
+// - initDataTypes via `SetInitDataTypes`.
+// - audioCapabilities and/or videoCapabilities via `SetAudioCapabilities`
+// and/or `SetVideoCapabilities`.
+// - keyIds and keys via `AddKeyIdAndKey`.
+// - onerror should be set to a function that will handle errors from the
+// helper. This function should take one argument, the error.
+// 2. Use the helper to configure a media element via `ConfigureEme`.
+// 3. One the promise from `ConfigureEme` has resolved the media element should
+// be configured and can be played. Errors that happen after this point are
+// reported via `onerror`.
+var EmeHelper = class EmeHelper {
+ // Members used to configure EME.
+ _keySystem;
+ _initDataTypes;
+ _audioCapabilities = [];
+ _videoCapabilities = [];
+
+ // Map of keyIds to keys.
+ _keyMap = new Map();
+
+ // Will be called if an error occurs during event handling. Users of the
+ // class should set a handler to be notified of errors.
+ onerror;
+
+ /**
+ * Get the clearkey key system string.
+ * @return The clearkey key system string.
+ */
+ static GetClearkeyKeySystemString() {
+ return "org.w3.clearkey";
+ }
+
+ // Begin conversion helpers.
+
+ /**
+ * Helper to convert Uint8Array into base64 using base64url alphabet, without
+ * padding.
+ * @param uint8Array An array of bytes to convert to base64.
+ * @return A base 64 encoded string
+ */
+ static Uint8ArrayToBase64(uint8Array) {
+ return new TextDecoder()
+ .decode(uint8Array)
+ .replace(/\+/g, "-") // Replace chars for base64url.
+ .replace(/\//g, "_")
+ .replace(/=*$/, ""); // Remove padding for base64url.
+ }
+
+ /**
+ * Helper to convert a hex string into base64 using base64url alphabet,
+ * without padding.
+ * @param hexString A string of hex characters.
+ * @return A base 64 encoded string
+ */
+ static HexToBase64(hexString) {
+ return btoa(
+ hexString
+ .match(/\w{2}/g) // Take chars two by two.
+ // Map to characters.
+ .map(hexByte => String.fromCharCode(parseInt(hexByte, 16)))
+ .join("")
+ )
+ .replace(/\+/g, "-") // Replace chars for base64url.
+ .replace(/\//g, "_")
+ .replace(/=*$/, ""); // Remove padding for base64url.
+ }
+
+ /**
+ * Helper to convert a base64 string (base64 or base64url) into a hex string.
+ * @param base64String A base64 encoded string. This can be base64url.
+ * @return A hex string (lower case);
+ */
+ static Base64ToHex(base64String) {
+ let binString = atob(base64String.replace(/-/g, "+").replace(/_/g, "/"));
+ let hexString = "";
+ for (let i = 0; i < binString.length; i++) {
+ // Covert to hex char. The "0" + and substr code are used to ensure we
+ // always get 2 chars, even for outputs the would normally be only one.
+ // E.g. for charcode 14 we'd get output 'e', and want to buffer that
+ // to '0e'.
+ hexString += ("0" + binString.charCodeAt(i).toString(16)).substr(-2);
+ }
+ // EMCA spec says that the num -> string conversion is lower case, so our
+ // hex string should already be lower case.
+ // https://tc39.es/ecma262/#sec-number.prototype.tostring
+ return hexString;
+ }
+
+ // End conversion helpers.
+
+ // Begin setters that setup the helper.
+ // These should be used to configure the helper prior to calling
+ // `ConfigureEme`.
+
+ /**
+ * Sets the key system that will be used by the EME helper.
+ * @param keySystem The key system to use. Probably "org.w3.clearkey", which
+ * can be fetched via `GetClearkeyKeySystemString`.
+ */
+ SetKeySystem(keySystem) {
+ this._keySystem = keySystem;
+ }
+
+ /**
+ * Sets the init data types that will be used by the EME helper. This is used
+ * when calling `navigator.requestMediaKeySystemAccess`.
+ * @param initDataTypes A list containing the init data types to be set by
+ * the helper. This will usually be ["cenc"] or ["webm"], see
+ * https://www.w3.org/TR/eme-initdata-registry/ for more info on what these
+ * mean.
+ */
+ SetInitDataTypes(initDataTypes) {
+ this._initDataTypes = initDataTypes;
+ }
+
+ /**
+ * Sets the audio capabilities that will be used by the EME helper. These are
+ * used when calling `navigator.requestMediaKeySystemAccess`.
+ * See https://developer.mozilla.org/en-US/docs/Web/API/Navigator/requestMediaKeySystemAccess
+ * for more info on these.
+ * @param audioCapabilities A list containing audio capabilities. E.g.
+ * [{ contentType: 'audio/webm; codecs="opus"' }].
+ */
+ SetAudioCapabilities(audioCapabilities) {
+ this._audioCapabilities = audioCapabilities;
+ }
+
+ /**
+ * Sets the video capabilities that will be used by the EME helper. These are
+ * used when calling `navigator.requestMediaKeySystemAccess`.
+ * See https://developer.mozilla.org/en-US/docs/Web/API/Navigator/requestMediaKeySystemAccess
+ * for more info on these.
+ * @param videoCapabilities A list containing video capabilities. E.g.
+ * [{ contentType: 'video/webm; codecs="vp9"' }]
+ */
+ SetVideoCapabilities(videoCapabilities) {
+ this._videoCapabilities = videoCapabilities;
+ }
+
+ /**
+ * Adds a key id and key pair to the key map. These should both be hex
+ * strings. E.g.
+ * emeHelper.AddKeyIdAndKey(
+ * "2cdb0ed6119853e7850671c3e9906c3c",
+ * "808b9adac384de1e4f56140f4ad76194"
+ * );
+ * This function will store the keyId and key in lower case to ensure
+ * consistency internally.
+ * @param keyId The key id used to lookup the following key.
+ * @param key The key associated with the earlier key id.
+ */
+ AddKeyIdAndKey(keyId, key) {
+ this._keyMap.set(keyId.toLowerCase(), key.toLowerCase());
+ }
+
+ /**
+ * Removes a key id and its associate key from the key map.
+ * @param keyId The key id to remove.
+ */
+ RemoveKeyIdAndKey(keyId) {
+ this._keyMap.delete(keyId);
+ }
+
+ // End setters that setup the helper.
+
+ /**
+ * Internal handler for `session.onmessage`. When calling this either do so
+ * from inside an arrow function or using `bind` to ensure `this` points to
+ * an EmeHelper instance (rather than a session).
+ * @param messageEvent The message event passed to `session.onmessage`.
+ */
+ _SessionMessageHandler(messageEvent) {
+ // This handles a session message and generates a clearkey license based
+ // on the information in this._keyMap. This is done by populating the
+ // appropriate keys on the session based on the keyIds surfaced in the
+ // session message (a license request).
+ let request = JSON.parse(new TextDecoder().decode(messageEvent.message));
+
+ let keys = [];
+ for (const keyId of request.kids) {
+ let id64 = keyId;
+ let idHex = EmeHelper.Base64ToHex(keyId);
+ let key = this._keyMap.get(idHex);
+
+ if (key) {
+ keys.push({
+ kty: "oct",
+ kid: id64,
+ k: EmeHelper.HexToBase64(key),
+ });
+ }
+ }
+
+ let license = new TextEncoder().encode(
+ JSON.stringify({
+ keys,
+ type: request.type || "temporary",
+ })
+ );
+
+ let session = messageEvent.target;
+ session.update(license).catch(error => {
+ if (this.onerror) {
+ this.onerror(error);
+ } else {
+ console.log(
+ `EmeHelper got an error, but no onerror handler was registered! Logging to console, error: ${error}`
+ );
+ }
+ });
+ }
+
+ /**
+ * Configures EME on a media element using the parameters already set on the
+ * instance of EmeHelper.
+ * @param htmlMediaElement - A media element to configure EME on.
+ * @return A promise that will be resolved once the media element is
+ * configured. This promise will be rejected with an error if configuration
+ * fails.
+ */
+ async ConfigureEme(htmlMediaElement) {
+ if (!this._keySystem) {
+ throw new Error("EmeHelper needs _keySystem to configure media");
+ }
+ if (!this._initDataTypes) {
+ throw new Error("EmeHelper needs _initDataTypes to configure media");
+ }
+ if (!this._audioCapabilities.length && !this._videoCapabilities.length) {
+ throw new Error(
+ "EmeHelper needs _audioCapabilities or _videoCapabilities to configure media"
+ );
+ }
+ const options = [
+ {
+ initDataTypes: this._initDataTypes,
+ audioCapabilities: this._audioCapabilities,
+ videoCapabilities: this._videoCapabilities,
+ },
+ ];
+ let access = await window.navigator.requestMediaKeySystemAccess(
+ this._keySystem,
+ options
+ );
+ let mediaKeys = await access.createMediaKeys();
+ await htmlMediaElement.setMediaKeys(mediaKeys);
+
+ htmlMediaElement.onencrypted = async encryptedEvent => {
+ let session = htmlMediaElement.mediaKeys.createSession();
+ // Use arrow notation so that `this` is the EmeHelper in the message
+ // handler. If we do `session.onmessage = this._SessionMessageHandler`
+ // then `this` will be the session in the callback.
+ session.onmessage = messageEvent =>
+ this._SessionMessageHandler(messageEvent);
+ try {
+ await session.generateRequest(
+ encryptedEvent.initDataType,
+ encryptedEvent.initData
+ );
+ } catch (error) {
+ if (this.onerror) {
+ this.onerror(error);
+ } else {
+ console.log(
+ `EmeHelper got an error, but no onerror handler was registered! Logging to console, error: ${error}`
+ );
+ }
+ }
+ };
+ }
+};
diff --git a/dom/media/test/empty_size.mp3 b/dom/media/test/empty_size.mp3
new file mode 100644
index 0000000000..0c208a2959
--- /dev/null
+++ b/dom/media/test/empty_size.mp3
Binary files differ
diff --git a/dom/media/test/file_access_controls.html b/dom/media/test/file_access_controls.html
new file mode 100644
index 0000000000..2f7bc360ed
--- /dev/null
+++ b/dom/media/test/file_access_controls.html
@@ -0,0 +1,160 @@
+<html>
+<head>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body onload="setTimeout(load, 0);">
+<script>
+
+// Page URL: http://example.org/tests/dom/media/test/file_access_controls.html
+
+var gResource = getPlayableVideo(gSmallTests).name;
+
+var gTests = [
+ {
+ // Test 0
+ url: "redirect.sjs?domain=example.com&file="+ gResource,
+ result: "error",
+ description: "Won't load when redirected to different domain",
+ },{
+ // Test 1
+ url: "redirect.sjs?domain=example.com&allowed&file=" + gResource,
+ result: "loadeddata",
+ description: "Can load when redirected to different domain with allow-origin",
+ },{
+ // Test 2
+ url: "redirect.sjs?domain=test1.example.org&file=" + gResource,
+ result: "error",
+ description: "Won't load when redirected to subdomain",
+ },{
+ // Test 3
+ url: "redirect.sjs?domain=test1.example.org&allowed&file=" + gResource,
+ result: "loadeddata",
+ description: "Can load when redirected to subdomain with allow-origin",
+ },{
+ // Test 4
+ url: "redirect.sjs?domain=example.org&file=" + gResource,
+ result: "loadeddata",
+ description: "Can load when redirected to same domain",
+ },{
+ // Test 5
+ url: "http://example.org/tests/dom/media/test/" + gResource,
+ result: "loadeddata",
+ description: "Can load from same domain"
+ },{
+ // Test 6
+ url: "http://example.org:8000/tests/dom/media/test/" + gResource,
+ result: "error",
+ description: "Won't load from different port on same domain"
+ },{
+ // Test 7
+ url: "http://example.org:8000/tests/dom/media/test/allowed.sjs?" + gResource,
+ result: "loadeddata",
+ description: "Can load from different port on same domain with allow-origin",
+ },{
+ // Test 8
+ url: "http://example.com/tests/dom/media/test/" + gResource,
+ result: "error",
+ description: "Won't load cross domain",
+ },{
+ // Test 9
+ url: "http://example.com/tests/dom/media/test/allowed.sjs?" + gResource,
+ result: "loadeddata",
+ description: "Can load cross domain with allow-origin",
+ },{
+ // Test 10
+ url: "http://test1.example.org/tests/dom/media/test/allowed.sjs?" + gResource,
+ result: "loadeddata",
+ description: "Can load from subdomain with allow-origin",
+ },{
+ // Test 11
+ url: "http://test1.example.org/tests/dom/media/test/" + gResource,
+ result: "error",
+ description: "Won't load from subdomain",
+ }
+];
+
+var gTestNum = 0;
+var gVideo = null;
+var gTestedRemoved = false;
+
+function eventHandler(event) {
+ //dump((gTestNum - 1) + ": " + event.type + "\n");
+ var video = event.target;
+ opener.postMessage({"result": (event.type == video.expectedResult),
+ "message": video.testDescription + (gTestedRemoved ? " (element not in document)" : " (element in document)")},
+ "http://mochi.test:8888");
+ // Make sure any extra events cause an error
+ video.expectedResult = "<none>";
+ nextTest();
+}
+
+function createVideo() {
+ var v = document.createElement('video');
+ v.addEventListener('loadeddata', eventHandler);
+ v.addEventListener('error', eventHandler);
+ v.crossOrigin = 'anonymous';
+ return v;
+}
+
+function load() {
+ opener.postMessage({"result": (window.location.href == "http://example.org/tests/dom/media/test/file_access_controls.html"),
+ "message": "We must be on a example.org:80"},
+ "http://mochi.test:8888");
+
+ nextTest();
+}
+
+function nextTest() {
+ //dump("nextTest() called, gTestNum="+gTestNum+" gTestedRemoved="+gTestedRemoved+"\n");
+ if (gTestNum == gTests.length) {
+ //dump("gTestNum == gTests.length\n");
+ if (!gTestedRemoved) {
+ // Repeat all tests with element removed from doc, should get same result.
+ gTestedRemoved = true;
+ gTestNum = 0;
+ } else {
+ //dump("Exiting...\n");
+ // We're done, exit the test.
+ done();
+ window.close();
+ return;
+ }
+ }
+
+ if (gVideo) {
+ gVideo.remove();
+ gVideo.removeAttribute("src");
+ gVideo.load();
+ }
+
+ gVideo = null;
+ SpecialPowers.forceGC();
+
+ gVideo = createVideo();
+ gVideo.expectedResult = gTests[gTestNum].result;
+ gVideo.testDescription = gTests[gTestNum].description;
+ // Uniquify the resource URL to ensure that the resources loaded by earlier or subsequent tests
+ // don't overlap with the resources we load here, which are loaded with non-default preferences set.
+ // We also want to make sure that an HTTP fetch actually happens for each testcase.
+ var url = gTests[gTestNum].url;
+ var random = Math.floor(Math.random()*1000000000);
+ url += (url.search(/\?/) < 0 ? "?" : "&") + "rand=" + random;
+ gVideo.src = url;
+ //dump("Starting test " + gTestNum + " at " + gVideo.src + " expecting:" + gVideo.expectedResult + "\n");
+ if (!gTestedRemoved) {
+ document.body.appendChild(gVideo);
+ // Will cause load() to be invoked.
+ } else {
+ gVideo.load();
+ }
+ gTestNum++;
+}
+
+function done() {
+ opener.postMessage({"done": "true"}, "http://mochi.test:8888");
+}
+
+</script>
+</body>
+</html>
+
diff --git a/dom/media/test/file_eme_createMediaKeys.html b/dom/media/test/file_eme_createMediaKeys.html
new file mode 100644
index 0000000000..3ff782b1bf
--- /dev/null
+++ b/dom/media/test/file_eme_createMediaKeys.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Eme createMediaKeys helper page</title>
+<script>
+// This script waits for a message then attempts to requestMediaKeySystemAccess
+// then createMediaKeys. On success posts 'successCreatingMediaKeys' to the
+// source of the message, on failure posts 'failureCreatingMediaKeys' and a
+// description of the failure to the source of the message.
+
+async function createMediaKeys() {
+ const clearKeyOptions = [
+ {
+ initDataTypes: ["webm"],
+ videoCapabilities: [{ contentType: 'video/webm; codecs="vp9"' }],
+ },
+ ];
+
+ let access = await navigator.requestMediaKeySystemAccess(
+ "org.w3.clearkey",
+ clearKeyOptions
+ );
+
+ return access.createMediaKeys();
+}
+function setupMessageListener() {
+ window.onmessage = async event => {
+ // We don't bother checking the message data since it should always be
+ // telling us to create media keys.
+ try {
+ let keys = await createMediaKeys();
+ if (!keys) {
+ event.source.postMessage("failureCreatingMediaKeys no keys", "*");
+ return;
+ }
+ event.source.postMessage("successCreatingMediaKeys", "*");
+ } catch (e) {
+ event.source.postMessage(`failureCreatingMediaKeys ${e}`, "*");
+ }
+ };
+}
+window.onload = setupMessageListener;
+</script>
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/media/test/file_playback_and_bfcache.html b/dom/media/test/file_playback_and_bfcache.html
new file mode 100644
index 0000000000..4798e1487e
--- /dev/null
+++ b/dom/media/test/file_playback_and_bfcache.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script>
+ function init() {
+ if (location.search == "") {
+ let bc1 = new BroadcastChannel("bc1");
+ bc1.onmessage = function(e) {
+ if (e.data == "loadNext") {
+ location.href = location.href + "?page2";
+ } else if (e.data == "forward") {
+ bc1.close();
+ history.forward();
+ }
+ };
+ window.onpageshow = function() {
+ bc1.postMessage("pageshow");
+ };
+ } else {
+ document.body.innerHTML = "<video controls src='owl.mp3' autoplay>";
+ let bc2 = new BroadcastChannel("bc2");
+ bc2.onmessage = function(e) {
+ if (e.data == "back") {
+ history.back();
+ } else if (e.data == "statistics") {
+ bc2.postMessage({ currentTime: document.body.firstChild.currentTime,
+ duration: document.body.firstChild.duration });
+ bc2.close();
+ window.close();
+ }
+ }
+ window.onpageshow = function(e) {
+ bc2.postMessage({ event: "pageshow", persisted: e.persisted});
+ if (!e.persisted) {
+ // The initial statistics is sent once we know the duration and
+ // have loaded all the data.
+ let mediaElement = document.body.firstChild;
+ mediaElement.onpause = function() {
+ mediaElement.onpause = null;
+ mediaElement.currentTime = 0;
+ mediaElement.onplay = function() {
+ setTimeout(function() {
+ bc2.postMessage({ currentTime: mediaElement.currentTime,
+ duration: mediaElement.duration });
+ }, 500);
+ }
+ mediaElement.play();
+ }
+ }
+ };
+ }
+ }
+ </script>
+</head>
+<body onload="init()">
+</body>
+</html>
diff --git a/dom/media/test/flac-noheader-s16.flac b/dom/media/test/flac-noheader-s16.flac
new file mode 100644
index 0000000000..01152142a9
--- /dev/null
+++ b/dom/media/test/flac-noheader-s16.flac
Binary files differ
diff --git a/dom/media/test/flac-noheader-s16.flac^headers^ b/dom/media/test/flac-noheader-s16.flac^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/flac-noheader-s16.flac^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/flac-s24.flac b/dom/media/test/flac-s24.flac
new file mode 100644
index 0000000000..1ba5e27a15
--- /dev/null
+++ b/dom/media/test/flac-s24.flac
Binary files differ
diff --git a/dom/media/test/flac-s24.flac^headers^ b/dom/media/test/flac-s24.flac^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/flac-s24.flac^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/flac-sample-cenc.mp4 b/dom/media/test/flac-sample-cenc.mp4
new file mode 100644
index 0000000000..c89190387a
--- /dev/null
+++ b/dom/media/test/flac-sample-cenc.mp4
Binary files differ
diff --git a/dom/media/test/flac-sample-cenc.mp4^headers^ b/dom/media/test/flac-sample-cenc.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/flac-sample-cenc.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/flac-sample.mp4 b/dom/media/test/flac-sample.mp4
new file mode 100644
index 0000000000..d39c94a7b2
--- /dev/null
+++ b/dom/media/test/flac-sample.mp4
Binary files differ
diff --git a/dom/media/test/flac-sample.mp4^headers^ b/dom/media/test/flac-sample.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/flac-sample.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/fragment_noplay.js b/dom/media/test/fragment_noplay.js
new file mode 100644
index 0000000000..247641763f
--- /dev/null
+++ b/dom/media/test/fragment_noplay.js
@@ -0,0 +1,19 @@
+function test_fragment_noplay(v, start, end, is, ok, finish) {
+ function onLoadedMetadata() {
+ var s = start == null ? 0 : start;
+ var e = end == null ? v.duration : end;
+ var a = s - 0.15;
+ var b = s + 0.15;
+ ok(
+ v.currentTime >= a && v.currentTime <= b,
+ "loadedmetadata currentTime is " + a + " < " + v.currentTime + " < " + b
+ );
+ ok(
+ v.mozFragmentEnd == e,
+ "mozFragmentEnd (" + v.mozFragmentEnd + ") == end Time (" + e + ")"
+ );
+ finish();
+ }
+
+ v.addEventListener("loadedmetadata", onLoadedMetadata);
+}
diff --git a/dom/media/test/fragment_play.js b/dom/media/test/fragment_play.js
new file mode 100644
index 0000000000..ec0fe7952a
--- /dev/null
+++ b/dom/media/test/fragment_play.js
@@ -0,0 +1,92 @@
+function test_fragment_play(v, start, end, is, ok, finish) {
+ var completed = false;
+ var loadedMetadataRaised = false;
+ var seekedRaised = false;
+ var pausedRaised = false;
+
+ function onLoadedMetadata() {
+ var s = start == null ? 0 : start;
+ var e = end == null ? v.duration : end;
+ ok(
+ v.currentTime == s,
+ "loadedmetadata currentTime is " + v.currentTime + " != " + s
+ );
+ ok(
+ v.mozFragmentEnd == e,
+ "mozFragmentEnd (" + v.mozFragmentEnd + ") == end Time (" + e + ")"
+ );
+ loadedMetadataRaised = true;
+ v.play();
+ }
+
+ function onSeeked() {
+ if (completed) {
+ return;
+ }
+
+ var s = start == null ? 0 : start;
+ ok(
+ v.currentTime - s < 0.1,
+ "seeked currentTime is " +
+ v.currentTime +
+ " != " +
+ s +
+ " (fuzzy compare +-0.1)"
+ );
+
+ seekedRaised = true;
+ }
+
+ function onTimeUpdate() {
+ if (completed) {
+ return;
+ }
+
+ v._lastTimeUpdate = v.currentTime;
+ }
+
+ function onPause() {
+ if (completed) {
+ return;
+ }
+
+ var e = end == null ? v.duration : end;
+ var a = e - 0.05;
+ var b = e + 0.05;
+ ok(
+ v.currentTime >= a && v.currentTime <= b,
+ "paused currentTime is " +
+ a +
+ " < " +
+ v.currentTime +
+ " < " +
+ b +
+ " ? " +
+ v._lastTimeUpdate
+ );
+ pausedRaised = true;
+ v.play();
+ }
+
+ function onEnded() {
+ if (completed) {
+ return;
+ }
+
+ completed = true;
+ ok(loadedMetadataRaised, "loadedmetadata event");
+ if (start) {
+ ok(seekedRaised, "seeked event");
+ }
+ if (end) {
+ ok(pausedRaised, "paused event: " + end + " " + v.duration);
+ }
+ finish();
+ }
+
+ v.addEventListener("ended", onEnded);
+ v.addEventListener("loadedmetadata", onLoadedMetadata);
+ v.addEventListener("seeked", onSeeked);
+ v.addEventListener("pause", onPause);
+ v.addEventListener("timeupdate", onTimeUpdate);
+}
diff --git a/dom/media/test/gUM_support.js b/dom/media/test/gUM_support.js
new file mode 100644
index 0000000000..80d218cae7
--- /dev/null
+++ b/dom/media/test/gUM_support.js
@@ -0,0 +1,103 @@
+// Support script for test that use getUserMedia. This allows explicit
+// configuration of prefs which affect gUM. See also
+// `testing/mochitest/runtests.py` for how the harness configures values.
+
+// Setup preconditions for tests using getUserMedia. This functions helps
+// manage different prefs that affect gUM calls in tests and makes explicit
+// the expected state before test runs.
+async function pushGetUserMediaTestPrefs({
+ fakeAudio = false,
+ fakeVideo = false,
+ loopbackAudio = false,
+ loopbackVideo = false,
+}) {
+ // Make sure we have sensical arguments
+ if (!fakeAudio && !loopbackAudio) {
+ throw new Error(
+ "pushGetUserMediaTestPrefs: Should have fake or loopback audio!"
+ );
+ } else if (fakeAudio && loopbackAudio) {
+ throw new Error(
+ "pushGetUserMediaTestPrefs: Should not have both fake and loopback audio!"
+ );
+ }
+ if (!fakeVideo && !loopbackVideo) {
+ throw new Error(
+ "pushGetUserMediaTestPrefs: Should have fake or loopback video!"
+ );
+ } else if (fakeVideo && loopbackVideo) {
+ throw new Error(
+ "pushGetUserMediaTestPrefs: Should not have both fake and loopback video!"
+ );
+ }
+
+ let testPrefs = [];
+ if (fakeAudio) {
+ // Unset the loopback device so it doesn't take precedence
+ testPrefs.push(["media.audio_loopback_dev", ""]);
+ // Setup fake streams pref
+ testPrefs.push(["media.navigator.streams.fake", true]);
+ }
+ if (loopbackAudio) {
+ // If audio loopback is requested we expect the test harness to have set
+ // the loopback device pref, make sure it's set
+ let audioLoopDev = SpecialPowers.getCharPref(
+ "media.audio_loopback_dev",
+ ""
+ );
+ if (!audioLoopDev) {
+ throw new Error(
+ "pushGetUserMediaTestPrefs: Loopback audio requested but " +
+ "media.audio_loopback_dev does not appear to be set!"
+ );
+ }
+ }
+ if (fakeVideo) {
+ // Unset the loopback device so it doesn't take precedence
+ testPrefs.push(["media.video_loopback_dev", ""]);
+ // Setup fake streams pref
+ testPrefs.push(["media.navigator.streams.fake", true]);
+ }
+ if (loopbackVideo) {
+ // If video loopback is requested we expect the test harness to have set
+ // the loopback device pref, make sure it's set
+ let videoLoopDev = SpecialPowers.getCharPref(
+ "media.video_loopback_dev",
+ ""
+ );
+ if (!videoLoopDev) {
+ throw new Error(
+ "pushGetUserMediaTestPrefs: Loopback video requested but " +
+ "media.video_loopback_dev does not appear to be set!"
+ );
+ }
+ }
+ // Prevent presentation of the gUM permission prompt.
+ testPrefs.push(["media.navigator.permission.disabled", true]);
+ return SpecialPowers.pushPrefEnv({ set: testPrefs });
+}
+
+// Setup preconditions for tests using getUserMedia. This function will
+// configure prefs to select loopback device(s) if it can find loopback device
+// names already set in the prefs. If no loopback device name can be found then
+// prefs are setup such that a fake device is used.
+async function setupGetUserMediaTestPrefs() {
+ let prefRequests = {};
+ let audioLoopDev = SpecialPowers.getCharPref("media.audio_loopback_dev", "");
+ if (audioLoopDev) {
+ prefRequests.fakeAudio = false;
+ prefRequests.loopbackAudio = true;
+ } else {
+ prefRequests.fakeAudio = true;
+ prefRequests.loopbackAudio = false;
+ }
+ let videoLoopDev = SpecialPowers.getCharPref("media.video_loopback_dev", "");
+ if (videoLoopDev) {
+ prefRequests.fakeVideo = false;
+ prefRequests.loopbackVideo = true;
+ } else {
+ prefRequests.fakeVideo = true;
+ prefRequests.loopbackVideo = false;
+ }
+ return pushGetUserMediaTestPrefs(prefRequests);
+}
diff --git a/dom/media/test/gizmo-frag.mp4 b/dom/media/test/gizmo-frag.mp4
new file mode 100644
index 0000000000..f6980663c2
--- /dev/null
+++ b/dom/media/test/gizmo-frag.mp4
Binary files differ
diff --git a/dom/media/test/gizmo-noaudio.mp4 b/dom/media/test/gizmo-noaudio.mp4
new file mode 100644
index 0000000000..24732a4064
--- /dev/null
+++ b/dom/media/test/gizmo-noaudio.mp4
Binary files differ
diff --git a/dom/media/test/gizmo-noaudio.mp4^headers^ b/dom/media/test/gizmo-noaudio.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/gizmo-noaudio.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/gizmo-noaudio.webm b/dom/media/test/gizmo-noaudio.webm
new file mode 100644
index 0000000000..9f412cb6e3
--- /dev/null
+++ b/dom/media/test/gizmo-noaudio.webm
Binary files differ
diff --git a/dom/media/test/gizmo-noaudio.webm^headers^ b/dom/media/test/gizmo-noaudio.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/gizmo-noaudio.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/gizmo-short.mp4 b/dom/media/test/gizmo-short.mp4
new file mode 100644
index 0000000000..f8caec741e
--- /dev/null
+++ b/dom/media/test/gizmo-short.mp4
Binary files differ
diff --git a/dom/media/test/gizmo-short.mp4^headers^ b/dom/media/test/gizmo-short.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/gizmo-short.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/gizmo.mp4 b/dom/media/test/gizmo.mp4
new file mode 100644
index 0000000000..87efad5ade
--- /dev/null
+++ b/dom/media/test/gizmo.mp4
Binary files differ
diff --git a/dom/media/test/gizmo.mp4^headers^ b/dom/media/test/gizmo.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/gizmo.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/gizmo.webm b/dom/media/test/gizmo.webm
new file mode 100644
index 0000000000..518531a93f
--- /dev/null
+++ b/dom/media/test/gizmo.webm
Binary files differ
diff --git a/dom/media/test/gizmo.webm^headers^ b/dom/media/test/gizmo.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/gizmo.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/gzipped_mp4.sjs b/dom/media/test/gzipped_mp4.sjs
new file mode 100644
index 0000000000..b08362d75e
--- /dev/null
+++ b/dom/media/test/gzipped_mp4.sjs
@@ -0,0 +1,25 @@
+function getGzippedFileBytes() {
+ var file;
+ getObjectState("SERVER_ROOT", function (serverRoot) {
+ file = serverRoot.getFile("tests/dom/media/test/short.mp4.gz");
+ });
+ var fileInputStream = Cc[
+ "@mozilla.org/network/file-input-stream;1"
+ ].createInstance(Ci.nsIFileInputStream);
+ var binaryInputStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+ Ci.nsIBinaryInputStream
+ );
+ fileInputStream.init(file, -1, -1, 0);
+ binaryInputStream.setInputStream(fileInputStream);
+ return binaryInputStream.readBytes(binaryInputStream.available());
+}
+
+function handleRequest(request, response) {
+ var bytes = getGzippedFileBytes();
+ response.setHeader("Content-Length", String(bytes.length), false);
+ response.setHeader("Content-Type", "video/mp4", false);
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ response.setHeader("Content-Encoding", "gzip", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.write(bytes, bytes.length);
+}
diff --git a/dom/media/test/hls/400x300_prog_index.m3u8 b/dom/media/test/hls/400x300_prog_index.m3u8
new file mode 100644
index 0000000000..3252eb178f
--- /dev/null
+++ b/dom/media/test/hls/400x300_prog_index.m3u8
@@ -0,0 +1,10 @@
+#EXTM3U
+#EXT-X-TARGETDURATION:10
+#EXT-X-VERSION:3
+#EXT-X-MEDIA-SEQUENCE:0
+#EXT-X-PLAYLIST-TYPE:VOD
+#EXTINF:9.97667,
+400x300_seg0.ts
+#EXTINF:9.97667,
+400x300_seg1.ts
+#EXT-X-ENDLIST
diff --git a/dom/media/test/hls/400x300_prog_index_5s.m3u8 b/dom/media/test/hls/400x300_prog_index_5s.m3u8
new file mode 100644
index 0000000000..8e9d19f764
--- /dev/null
+++ b/dom/media/test/hls/400x300_prog_index_5s.m3u8
@@ -0,0 +1,8 @@
+#EXTM3U
+#EXT-X-TARGETDURATION:4.00
+#EXT-X-VERSION:3
+#EXT-X-MEDIA-SEQUENCE:0
+#EXT-X-PLAYLIST-TYPE:VOD
+#EXTINF:4.00,
+400x300_seg0_5s.ts
+#EXT-X-ENDLIST
diff --git a/dom/media/test/hls/400x300_seg0.ts b/dom/media/test/hls/400x300_seg0.ts
new file mode 100644
index 0000000000..b17b0a88ff
--- /dev/null
+++ b/dom/media/test/hls/400x300_seg0.ts
Binary files differ
diff --git a/dom/media/test/hls/400x300_seg0_5s.ts b/dom/media/test/hls/400x300_seg0_5s.ts
new file mode 100644
index 0000000000..9504d5d889
--- /dev/null
+++ b/dom/media/test/hls/400x300_seg0_5s.ts
Binary files differ
diff --git a/dom/media/test/hls/400x300_seg1.ts b/dom/media/test/hls/400x300_seg1.ts
new file mode 100644
index 0000000000..d751091e68
--- /dev/null
+++ b/dom/media/test/hls/400x300_seg1.ts
Binary files differ
diff --git a/dom/media/test/hls/416x243_prog_index_5s.m3u8 b/dom/media/test/hls/416x243_prog_index_5s.m3u8
new file mode 100644
index 0000000000..ae518e7890
--- /dev/null
+++ b/dom/media/test/hls/416x243_prog_index_5s.m3u8
@@ -0,0 +1,8 @@
+#EXTM3U
+#EXT-X-TARGETDURATION:4.04
+#EXT-X-VERSION:4
+#EXT-X-MEDIA-SEQUENCE:0
+#EXT-X-PLAYLIST-TYPE:VOD
+#EXTINF:4.04,
+416x243_seg0_5s.ts
+#EXT-X-ENDLIST
diff --git a/dom/media/test/hls/416x243_seg0_5s.ts b/dom/media/test/hls/416x243_seg0_5s.ts
new file mode 100644
index 0000000000..48e5473276
--- /dev/null
+++ b/dom/media/test/hls/416x243_seg0_5s.ts
Binary files differ
diff --git a/dom/media/test/hls/640x480_prog_index.m3u8 b/dom/media/test/hls/640x480_prog_index.m3u8
new file mode 100644
index 0000000000..ac1212e2e7
--- /dev/null
+++ b/dom/media/test/hls/640x480_prog_index.m3u8
@@ -0,0 +1,10 @@
+#EXTM3U
+#EXT-X-TARGETDURATION:10
+#EXT-X-VERSION:3
+#EXT-X-MEDIA-SEQUENCE:0
+#EXT-X-PLAYLIST-TYPE:VOD
+#EXTINF:9.97667,
+640x480_seg0.ts
+#EXTINF:9.97667,
+640x480_seg1.ts
+#EXT-X-ENDLIST
diff --git a/dom/media/test/hls/640x480_seg0.ts b/dom/media/test/hls/640x480_seg0.ts
new file mode 100644
index 0000000000..9bf0f0454a
--- /dev/null
+++ b/dom/media/test/hls/640x480_seg0.ts
Binary files differ
diff --git a/dom/media/test/hls/640x480_seg1.ts b/dom/media/test/hls/640x480_seg1.ts
new file mode 100644
index 0000000000..c1ed938f44
--- /dev/null
+++ b/dom/media/test/hls/640x480_seg1.ts
Binary files differ
diff --git a/dom/media/test/hls/960x720_prog_index.m3u8 b/dom/media/test/hls/960x720_prog_index.m3u8
new file mode 100644
index 0000000000..8ff18b089c
--- /dev/null
+++ b/dom/media/test/hls/960x720_prog_index.m3u8
@@ -0,0 +1,10 @@
+#EXTM3U
+#EXT-X-TARGETDURATION:10
+#EXT-X-VERSION:3
+#EXT-X-MEDIA-SEQUENCE:0
+#EXT-X-PLAYLIST-TYPE:VOD
+#EXTINF:9.97667,
+960x720_seg0.ts
+#EXTINF:9.97667,
+960x720_seg1.ts
+#EXT-X-ENDLIST
diff --git a/dom/media/test/hls/960x720_seg0.ts b/dom/media/test/hls/960x720_seg0.ts
new file mode 100644
index 0000000000..031bfe30d6
--- /dev/null
+++ b/dom/media/test/hls/960x720_seg0.ts
Binary files differ
diff --git a/dom/media/test/hls/960x720_seg1.ts b/dom/media/test/hls/960x720_seg1.ts
new file mode 100644
index 0000000000..63f15cb0cb
--- /dev/null
+++ b/dom/media/test/hls/960x720_seg1.ts
Binary files differ
diff --git a/dom/media/test/hls/bipbop_16x9_single.m3u8 b/dom/media/test/hls/bipbop_16x9_single.m3u8
new file mode 100644
index 0000000000..dce6a76c7b
--- /dev/null
+++ b/dom/media/test/hls/bipbop_16x9_single.m3u8
@@ -0,0 +1,5 @@
+#EXTM3U
+
+#EXT-X-STREAM-INF:BANDWIDTH=263851,CODECS="mp4a.40.2, avc1.4d400d"
+416x243_prog_index_5s.m3u8
+
diff --git a/dom/media/test/hls/bipbop_4x3_single.m3u8 b/dom/media/test/hls/bipbop_4x3_single.m3u8
new file mode 100644
index 0000000000..8f354ff011
--- /dev/null
+++ b/dom/media/test/hls/bipbop_4x3_single.m3u8
@@ -0,0 +1,4 @@
+#EXTM3U
+
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=232370,CODECS="mp4a.40.2, avc1.4d4015"
+400x300_prog_index_5s.m3u8 \ No newline at end of file
diff --git a/dom/media/test/hls/bipbop_4x3_variant.m3u8 b/dom/media/test/hls/bipbop_4x3_variant.m3u8
new file mode 100644
index 0000000000..8a9a100dba
--- /dev/null
+++ b/dom/media/test/hls/bipbop_4x3_variant.m3u8
@@ -0,0 +1,10 @@
+#EXTM3U
+
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=232370,CODECS="mp4a.40.2, avc1.4d4015"
+400x300_prog_index.m3u8
+
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=649879,CODECS="mp4a.40.2, avc1.4d401e"
+640x480_prog_index.m3u8
+
+#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=649879,CODECS="mp4a.40.2, avc1.4d401e"
+960x720_prog_index.m3u8
diff --git a/dom/media/test/huge-id3.mp3 b/dom/media/test/huge-id3.mp3
new file mode 100644
index 0000000000..41cb93d805
--- /dev/null
+++ b/dom/media/test/huge-id3.mp3
Binary files differ
diff --git a/dom/media/test/huge-id3.mp3^headers^ b/dom/media/test/huge-id3.mp3^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/huge-id3.mp3^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/id3tags.mp3 b/dom/media/test/id3tags.mp3
new file mode 100644
index 0000000000..bad506cf18
--- /dev/null
+++ b/dom/media/test/id3tags.mp3
Binary files differ
diff --git a/dom/media/test/id3tags.mp3^headers^ b/dom/media/test/id3tags.mp3^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/id3tags.mp3^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/id3v1afterlongid3v2.mp3 b/dom/media/test/id3v1afterlongid3v2.mp3
new file mode 100644
index 0000000000..8de7bde0d3
--- /dev/null
+++ b/dom/media/test/id3v1afterlongid3v2.mp3
Binary files differ
diff --git a/dom/media/test/invalid-cmap-s0c0.opus b/dom/media/test/invalid-cmap-s0c0.opus
new file mode 100644
index 0000000000..0b99587865
--- /dev/null
+++ b/dom/media/test/invalid-cmap-s0c0.opus
Binary files differ
diff --git a/dom/media/test/invalid-cmap-s0c0.opus^headers^ b/dom/media/test/invalid-cmap-s0c0.opus^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/invalid-cmap-s0c0.opus^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/invalid-cmap-s0c2.opus b/dom/media/test/invalid-cmap-s0c2.opus
new file mode 100644
index 0000000000..a921894fee
--- /dev/null
+++ b/dom/media/test/invalid-cmap-s0c2.opus
Binary files differ
diff --git a/dom/media/test/invalid-cmap-s0c2.opus^headers^ b/dom/media/test/invalid-cmap-s0c2.opus^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/invalid-cmap-s0c2.opus^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/invalid-cmap-s1c2.opus b/dom/media/test/invalid-cmap-s1c2.opus
new file mode 100644
index 0000000000..95a84f523c
--- /dev/null
+++ b/dom/media/test/invalid-cmap-s1c2.opus
Binary files differ
diff --git a/dom/media/test/invalid-cmap-s1c2.opus^headers^ b/dom/media/test/invalid-cmap-s1c2.opus^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/invalid-cmap-s1c2.opus^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/invalid-cmap-short.opus b/dom/media/test/invalid-cmap-short.opus
new file mode 100644
index 0000000000..fcd7eb506a
--- /dev/null
+++ b/dom/media/test/invalid-cmap-short.opus
Binary files differ
diff --git a/dom/media/test/invalid-cmap-short.opus^headers^ b/dom/media/test/invalid-cmap-short.opus^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/invalid-cmap-short.opus^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/invalid-discard_on_multi_blocks.webm b/dom/media/test/invalid-discard_on_multi_blocks.webm
new file mode 100644
index 0000000000..f39ab5bcb8
--- /dev/null
+++ b/dom/media/test/invalid-discard_on_multi_blocks.webm
Binary files differ
diff --git a/dom/media/test/invalid-discard_on_multi_blocks.webm^headers^ b/dom/media/test/invalid-discard_on_multi_blocks.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/invalid-discard_on_multi_blocks.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/invalid-excess_discard.webm b/dom/media/test/invalid-excess_discard.webm
new file mode 100644
index 0000000000..5b34aca1a7
--- /dev/null
+++ b/dom/media/test/invalid-excess_discard.webm
Binary files differ
diff --git a/dom/media/test/invalid-excess_discard.webm^headers^ b/dom/media/test/invalid-excess_discard.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/invalid-excess_discard.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/invalid-excess_neg_discard.webm b/dom/media/test/invalid-excess_neg_discard.webm
new file mode 100644
index 0000000000..2bfad6ed21
--- /dev/null
+++ b/dom/media/test/invalid-excess_neg_discard.webm
Binary files differ
diff --git a/dom/media/test/invalid-excess_neg_discard.webm^headers^ b/dom/media/test/invalid-excess_neg_discard.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/invalid-excess_neg_discard.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/invalid-m0c0.opus b/dom/media/test/invalid-m0c0.opus
new file mode 100644
index 0000000000..86555f60eb
--- /dev/null
+++ b/dom/media/test/invalid-m0c0.opus
Binary files differ
diff --git a/dom/media/test/invalid-m0c0.opus^headers^ b/dom/media/test/invalid-m0c0.opus^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/invalid-m0c0.opus^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/invalid-m0c3.opus b/dom/media/test/invalid-m0c3.opus
new file mode 100644
index 0000000000..2c681a8c03
--- /dev/null
+++ b/dom/media/test/invalid-m0c3.opus
Binary files differ
diff --git a/dom/media/test/invalid-m0c3.opus^headers^ b/dom/media/test/invalid-m0c3.opus^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/invalid-m0c3.opus^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/invalid-m1c0.opus b/dom/media/test/invalid-m1c0.opus
new file mode 100644
index 0000000000..c7728f79ee
--- /dev/null
+++ b/dom/media/test/invalid-m1c0.opus
Binary files differ
diff --git a/dom/media/test/invalid-m1c0.opus^headers^ b/dom/media/test/invalid-m1c0.opus^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/invalid-m1c0.opus^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/invalid-m1c9.opus b/dom/media/test/invalid-m1c9.opus
new file mode 100644
index 0000000000..1ef6e9f9cf
--- /dev/null
+++ b/dom/media/test/invalid-m1c9.opus
Binary files differ
diff --git a/dom/media/test/invalid-m1c9.opus^headers^ b/dom/media/test/invalid-m1c9.opus^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/invalid-m1c9.opus^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/invalid-m2c0.opus b/dom/media/test/invalid-m2c0.opus
new file mode 100644
index 0000000000..5c3f97e2ab
--- /dev/null
+++ b/dom/media/test/invalid-m2c0.opus
Binary files differ
diff --git a/dom/media/test/invalid-m2c0.opus^headers^ b/dom/media/test/invalid-m2c0.opus^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/invalid-m2c0.opus^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/invalid-m2c1.opus b/dom/media/test/invalid-m2c1.opus
new file mode 100644
index 0000000000..5ecb95ee25
--- /dev/null
+++ b/dom/media/test/invalid-m2c1.opus
Binary files differ
diff --git a/dom/media/test/invalid-m2c1.opus^headers^ b/dom/media/test/invalid-m2c1.opus^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/invalid-m2c1.opus^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/invalid-neg_discard.webm b/dom/media/test/invalid-neg_discard.webm
new file mode 100644
index 0000000000..3f665c0b59
--- /dev/null
+++ b/dom/media/test/invalid-neg_discard.webm
Binary files differ
diff --git a/dom/media/test/invalid-neg_discard.webm^headers^ b/dom/media/test/invalid-neg_discard.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/invalid-neg_discard.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/invalid-preskip.webm b/dom/media/test/invalid-preskip.webm
new file mode 100644
index 0000000000..99b4f2ca71
--- /dev/null
+++ b/dom/media/test/invalid-preskip.webm
Binary files differ
diff --git a/dom/media/test/invalid-preskip.webm^headers^ b/dom/media/test/invalid-preskip.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/invalid-preskip.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/make-headers.sh b/dom/media/test/make-headers.sh
new file mode 100644
index 0000000000..35d9bd90f8
--- /dev/null
+++ b/dom/media/test/make-headers.sh
@@ -0,0 +1,18 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Script to generate ^header^ files for all media files we use.
+# This is to ensure that our media files are not cached by necko,
+# so that our detection as to whether the server supports byte range
+# requests is not interferred with by Necko's cache. See bug 977398
+# for details. Necko will fix this in bug 977314.
+
+FILES=(`ls *.ogg *.ogv *.webm *.mp3 *.opus *.mp4 *.m4s *.wav`)
+
+rm -f *.ogg^headers^ *.ogv^headers^ *.webm^headers^ *.mp3^headers^ *.opus^headers^ *.mp4^headers^ *.m4s^headers^ *.wav^headers^
+
+for i in "${FILES[@]}"
+do
+ echo "Cache-Control: no-store" >> $i^headers^
+done
diff --git a/dom/media/test/manifest.js b/dom/media/test/manifest.js
new file mode 100644
index 0000000000..be52d566ce
--- /dev/null
+++ b/dom/media/test/manifest.js
@@ -0,0 +1,2548 @@
+const { AppConstants } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+// In each list of tests below, test file types that are not supported should
+// be ignored. To make sure tests respect that, we include a file of type
+// "bogus/duh" in each list.
+
+// Make sure to not touch navigator in here, since we want to push prefs that
+// will affect the APIs it exposes, but the set of exposed APIs is determined
+// when Navigator.prototype is created. So if we touch navigator before pushing
+// the prefs, the APIs it exposes will not take those prefs into account. We
+// work around this by using a navigator object from a different global for our
+// UA string testing.
+var gManifestNavigatorSource = document.documentElement.appendChild(
+ document.createElement("iframe")
+);
+gManifestNavigatorSource.style.display = "none";
+function manifestNavigator() {
+ return gManifestNavigatorSource.contentWindow.navigator;
+}
+
+// Similarly, use a <video> element from a different global for canPlayType or
+// other feature testing. If we used one from our global and did so before our
+// prefs are pushed, then we'd instantiate HTMLMediaElement.prototype before the
+// prefs are pushed and APIs we expect to be on that object would not be there.
+function manifestVideo() {
+ return gManifestNavigatorSource.contentDocument.createElement("video");
+}
+
+// Need to get the server url composed with ip:port instead of mochi.test.
+// Since we will provide the url to Exoplayer which cannot recognize the domain
+// name "mochi.test".
+let serverUrl = SpecialPowers.Services.prefs.getCharPref(
+ "media.hls.server.url"
+);
+var gHLSTests = [
+ {
+ name: serverUrl + "/bipbop_4x3_variant.m3u8",
+ type: "audio/x-mpegurl",
+ duration: 20.0,
+ },
+];
+
+// These are small test files, good for just seeing if something loads. We
+// really only need one test file per backend here.
+var gSmallTests = [
+ { name: "small-shot.ogg", type: "audio/ogg", duration: 0.276 },
+ { name: "small-shot.m4a", type: "audio/mp4", duration: 0.29 },
+ { name: "small-shot.mp3", type: "audio/mpeg", duration: 0.27 },
+ { name: "small-shot-mp3.mp4", type: "audio/mp4; codecs=mp3", duration: 0.34 },
+ { name: "small-shot.flac", type: "audio/flac", duration: 0.197 },
+ { name: "r11025_s16_c1-short.wav", type: "audio/x-wav", duration: 0.37 },
+ {
+ name: "320x240.ogv",
+ type: "video/ogg",
+ width: 320,
+ height: 240,
+ duration: 0.266,
+ contentDuration: 0.133,
+ },
+ {
+ name: "seek-short.webm",
+ type: "video/webm",
+ width: 320,
+ height: 240,
+ duration: 0.23,
+ },
+ {
+ name: "vp9-short.webm",
+ type: "video/webm",
+ width: 320,
+ height: 240,
+ duration: 0.2,
+ },
+ {
+ name: "detodos-short.opus",
+ type: "audio/ogg; codecs=opus",
+ duration: 0.22,
+ },
+ {
+ name: "gizmo-short.mp4",
+ type: "video/mp4",
+ width: 560,
+ height: 320,
+ duration: 0.27,
+ },
+ { name: "flac-s24.flac", type: "audio/flac", duration: 4.04 },
+ { name: "bogus.duh", type: "bogus/duh" },
+];
+
+var gFrameCountTests = [
+ { name: "bipbop.mp4", type: "video/mp4", totalFrameCount: 297 },
+ { name: "gizmo.mp4", type: "video/mp4", totalFrameCount: 166 },
+ { name: "seek-short.webm", type: "video/webm", totalFrameCount: 8 },
+ { name: "seek.webm", type: "video/webm", totalFrameCount: 120 },
+ { name: "320x240.ogv", type: "video/ogg", totalFrameCount: 8 },
+ { name: "av1.mp4", type: "video/mp4", totalFrameCount: 24 },
+];
+
+gSmallTests = gSmallTests.concat([
+ { name: "sample.3gp", type: "video/3gpp", duration: 4.933 },
+ { name: "sample.3g2", type: "video/3gpp2", duration: 4.933 },
+]);
+
+// Used by test_bug654550.html, for videoStats preference
+var gVideoTests = [
+ {
+ name: "320x240.ogv",
+ type: "video/ogg",
+ width: 320,
+ height: 240,
+ duration: 0.266,
+ },
+ {
+ name: "seek-short.webm",
+ type: "video/webm",
+ width: 320,
+ height: 240,
+ duration: 0.23,
+ },
+ { name: "bogus.duh", type: "bogus/duh" },
+];
+
+// Temp hack for trackIDs and captureStream() -- bug 1215769
+var gLongerTests = [
+ {
+ name: "seek.webm",
+ type: "video/webm",
+ width: 320,
+ height: 240,
+ duration: 3.966,
+ },
+ {
+ name: "gizmo.mp4",
+ type: "video/mp4",
+ width: 560,
+ height: 320,
+ duration: 5.56,
+ },
+];
+
+// Used by test_progress to ensure we get the correct progress information
+// during resource download.
+var gProgressTests = [
+ { name: "r11025_u8_c1.wav", type: "audio/x-wav", duration: 1.0, size: 11069 },
+ { name: "big-short.wav", type: "audio/x-wav", duration: 1.11, size: 12366 },
+ { name: "seek-short.ogv", type: "video/ogg", duration: 1.03, size: 79921 },
+ {
+ name: "320x240.ogv",
+ type: "video/ogg",
+ width: 320,
+ height: 240,
+ duration: 0.266,
+ size: 28942,
+ },
+ { name: "seek-short.webm", type: "video/webm", duration: 0.23, size: 19267 },
+ { name: "gizmo-short.mp4", type: "video/mp4", duration: 0.27, size: 29905 },
+ { name: "bogus.duh", type: "bogus/duh" },
+];
+
+// Used by test_played.html
+var gPlayedTests = [
+ { name: "big-short.wav", type: "audio/x-wav", duration: 1.11 },
+ { name: "seek-short.ogv", type: "video/ogg", duration: 1.03 },
+ { name: "seek-short.webm", type: "video/webm", duration: 0.23 },
+ { name: "gizmo-short.mp4", type: "video/mp4", duration: 0.27 },
+ { name: "owl-short.mp3", type: "audio/mpeg", duration: 0.52 },
+ { name: "very-short.mp3", type: "audio/mpeg", duration: 0.07 },
+ // Disable vbr.mp3 to see if it reduces the error of AUDCLNT_E_CPUUSAGE_EXCEEDED.
+ // See bug 1110922 comment 26.
+ //{ name:"vbr.mp3", type:"audio/mpeg", duration:10.0 },
+ { name: "bug495794.ogg", type: "audio/ogg", duration: 0.3 },
+];
+
+if (
+ manifestNavigator().userAgent.includes("Windows") &&
+ manifestVideo().canPlayType('video/mp4; codecs="avc1.64000c"')
+) {
+ gPlayedTests = gPlayedTests.concat(
+ { name: "red-46x48.mp4", type: "video/mp4", duration: 1.0 },
+ { name: "red-48x46.mp4", type: "video/mp4", duration: 1.0 }
+ );
+}
+
+// Used by test_mozLoadFrom. Need one test file per decoder backend, plus
+// anything for testing clone-specific bugs.
+var cloneKey = Math.floor(Math.random() * 100000000);
+var gCloneTests = [
+ // short-video is more like 1s, so if you load this twice you'll get an unexpected duration
+ {
+ name:
+ "dynamic_resource.sjs?key=" +
+ cloneKey +
+ "&res1=320x240.ogv&res2=short-video.ogv",
+ type: "video/ogg",
+ duration: 0.266,
+ },
+];
+
+// Used by test_play_twice. Need one test file per decoder backend, plus
+// anything for testing bugs that occur when replying a played file.
+var gReplayTests = gSmallTests.concat([
+ { name: "bug533822.ogg", type: "audio/ogg" },
+]);
+
+// Used by test_paused_after_ended. Need one test file per decoder backend, plus
+// anything for testing bugs that occur when replying a played file.
+var gPausedAfterEndedTests = gSmallTests.concat([
+ { name: "r11025_u8_c1.wav", type: "audio/x-wav", duration: 1.0 },
+ { name: "small-shot.ogg", type: "video/ogg", duration: 0.276 },
+]);
+
+// Test the mozHasAudio property, and APIs that detect different kinds of
+// tracks
+var gTrackTests = [
+ {
+ name: "big-short.wav",
+ type: "audio/x-wav",
+ duration: 1.11,
+ size: 12366,
+ hasAudio: true,
+ hasVideo: false,
+ },
+ {
+ name: "320x240.ogv",
+ type: "video/ogg",
+ width: 320,
+ height: 240,
+ duration: 0.266,
+ size: 28942,
+ hasAudio: false,
+ hasVideo: true,
+ },
+ {
+ name: "short-video.ogv",
+ type: "video/ogg",
+ duration: 1.081,
+ hasAudio: true,
+ hasVideo: true,
+ },
+ {
+ name: "seek-short.webm",
+ type: "video/webm",
+ duration: 0.23,
+ size: 19267,
+ hasAudio: false,
+ hasVideo: true,
+ },
+ {
+ name: "flac-s24.flac",
+ type: "audio/flac",
+ duration: 4.04,
+ hasAudio: true,
+ hasVideo: false,
+ },
+ { name: "bogus.duh", type: "bogus/duh" },
+];
+
+var gClosingConnectionsTest = [
+ { name: "seek-short.ogv", type: "video/ogg", duration: 1.03 },
+];
+
+// Used by any media recorder test. Need one test file per decoder backend
+// currently supported by the media encoder.
+var gMediaRecorderTests = [
+ // Duration should be greater than 500ms because we will record 2
+ // time slices (250ms per slice)
+ {
+ name: "detodos-recorder-test.opus",
+ type: "audio/ogg; codecs=opus",
+ duration: 0.62,
+ },
+];
+
+// Used by video media recorder tests
+var gMediaRecorderVideoTests = [
+ {
+ name: "seek-short.webm",
+ type: "video/webm",
+ width: 320,
+ height: 240,
+ duration: 0.23,
+ },
+];
+
+// These are files that we want to make sure we can play through. We can
+// also check metadata. Put files of the same type together in this list so if
+// something crashes we have some idea of which backend is responsible.
+// Used by test_playback, which expects no error event and one ended event.
+var gPlayTests = [
+ // Test playback of a WebM file with vp9 video
+ { name: "vp9cake-short.webm", type: "video/webm", duration: 1.0 },
+ // 8-bit samples
+ { name: "r11025_u8_c1.wav", type: "audio/x-wav", duration: 1.0 },
+ // 8-bit samples, file is truncated
+ { name: "r11025_u8_c1_trunc.wav", type: "audio/x-wav", duration: 1.8 },
+ // file has trailing non-PCM data
+ { name: "r11025_s16_c1_trailing.wav", type: "audio/x-wav", duration: 1.0 },
+ // file with list chunk
+ { name: "r16000_u8_c1_list.wav", type: "audio/x-wav", duration: 4.2 },
+ // file with 2 extra bytes of metadata
+ {
+ name: "16bit_wave_extrametadata.wav",
+ type: "audio/x-wav",
+ duration: 1.108,
+ },
+ // IEEE float wave file
+ { name: "wavedata_float.wav", type: "audio/x-wav", duration: 1.0 },
+ // 24-bit samples
+ { name: "wavedata_s24.wav", type: "audio/x-wav", duration: 1.0 },
+ // aLaw compressed wave file
+ { name: "wavedata_alaw.wav", type: "audio/x-wav", duration: 1.0 },
+ // uLaw compressed wave file
+ { name: "wavedata_ulaw.wav", type: "audio/x-wav", duration: 1.0 },
+ // Data length 0xFFFFFFFF
+ { name: "bug1301226.wav", type: "audio/x-wav", duration: 0.003673 },
+ // Data length 0xFFFFFFFF and odd chunk lengths.
+ { name: "bug1301226-odd.wav", type: "audio/x-wav", duration: 0.003673 },
+
+ // Ogg stream without eof marker
+ { name: "bug461281.ogg", type: "application/ogg", duration: 2.208 },
+
+ // oggz-chop stream
+ { name: "bug482461.ogv", type: "video/ogg", duration: 4.34 },
+ // Theora only oggz-chop stream
+ { name: "bug482461-theora.ogv", type: "video/ogg", duration: 4.138 },
+ // With first frame a "duplicate" (empty) frame.
+ {
+ name: "bug500311.ogv",
+ type: "video/ogg",
+ duration: 1.96,
+ contentDuration: 1.958,
+ },
+ // Small audio file
+ { name: "small-shot.ogg", type: "audio/ogg", duration: 0.276 },
+ // More audio in file than video.
+ { name: "short-video.ogv", type: "video/ogg", duration: 1.081 },
+ // First Theora data packet is zero bytes.
+ { name: "bug504613.ogv", type: "video/ogg", duration: Number.NaN },
+ // Multiple audio streams.
+ { name: "bug516323.ogv", type: "video/ogg", duration: 4.208 },
+ // oggz-chop with non-keyframe as first frame
+ {
+ name: "bug556821.ogv",
+ type: "video/ogg",
+ duration: 2.936,
+ contentDuration: 2.903,
+ },
+
+ // Encoded with vorbis beta1, includes unusually sized codebooks
+ { name: "beta-phrasebook.ogg", type: "audio/ogg", duration: 4.01 },
+ // Small file, only 1 frame with audio only.
+ { name: "bug520493.ogg", type: "audio/ogg", duration: 0.458 },
+ // Small file with vorbis comments with 0 length values and names.
+ { name: "bug520500.ogg", type: "audio/ogg", duration: 0.123 },
+
+ // Various weirdly formed Ogg files
+ {
+ name: "bug499519.ogv",
+ type: "video/ogg",
+ duration: 0.24,
+ contentDuration: 0.22,
+ },
+ { name: "bug506094.ogv", type: "video/ogg", duration: 0 },
+ { name: "bug498855-1.ogv", type: "video/ogg", duration: 0.24 },
+ { name: "bug498855-2.ogv", type: "video/ogg", duration: 0.24 },
+ { name: "bug498855-3.ogv", type: "video/ogg", duration: 0.24 },
+ {
+ name: "bug504644.ogv",
+ type: "video/ogg",
+ duration: 1.6,
+ contentDuration: 1.52,
+ },
+ {
+ name: "chain.ogv",
+ type: "video/ogg",
+ duration: Number.NaN,
+ contentDuration: 0.266,
+ },
+ {
+ name: "bug523816.ogv",
+ type: "video/ogg",
+ duration: 0.766,
+ contentDuration: 0,
+ },
+ { name: "bug495129.ogv", type: "video/ogg", duration: 2.41 },
+ {
+ name: "bug498380.ogv",
+ type: "video/ogg",
+ duration: 0.7663,
+ contentDuration: 0,
+ },
+ { name: "bug495794.ogg", type: "audio/ogg", duration: 0.3 },
+ { name: "bug557094.ogv", type: "video/ogg", duration: 0.24 },
+ { name: "multiple-bos.ogg", type: "video/ogg", duration: 0.431 },
+ { name: "audio-overhang.ogg", type: "video/ogg", duration: 2.3 },
+ { name: "video-overhang.ogg", type: "video/ogg", duration: 3.966 },
+
+ // bug461281.ogg with the middle second chopped out.
+ { name: "audio-gaps.ogg", type: "audio/ogg", duration: 2.208 },
+
+ // Test playback/metadata work after a redirect
+ {
+ name: "redirect.sjs?domain=mochi.test:8888&file=320x240.ogv",
+ type: "video/ogg",
+ duration: 0.266,
+ },
+
+ // Test playback of a webm file
+ { name: "seek-short.webm", type: "video/webm", duration: 0.23 },
+
+ // Test playback of a webm file with 'matroska' doctype
+ { name: "bug1377278.webm", type: "video/webm", duration: 4.0 },
+
+ // Test playback of a WebM file with non-zero start time.
+ { name: "split.webm", type: "video/webm", duration: 1.967 },
+
+ // Test playback of a WebM file with resolution changes.
+ { name: "resolution-change.webm", type: "video/webm", duration: 6.533 },
+
+ // The following webm files test cases where the webm metadata dimensions do
+ // not match those in the stream. See bug 1695033 for more info.
+
+ // Reference file with correct dimensions (webm metadata matches stream
+ // resolution).
+ { name: "bipbop_short_vp8.webm", type: "video/webm", duration: 1.011 },
+
+ // The webm resolution is greater in both dimensions than the in stream
+ // resolution.
+ {
+ name: "bipbop_short_pixel_metadata_bigger_than_in_stream_vp8.webm",
+ type: "video/webm",
+ duration: 1.011,
+ },
+
+ // The webm resolution is correct for height, but is narrower than the stream
+ // resolution.
+ {
+ name: "bipbop_short_pixel_metadata_narrower_than_in_stream_vp8.webm",
+ type: "video/webm",
+ duration: 1.011,
+ },
+
+ // The webm resolution is smaller in both dimensions than the in stream
+ // resolution.
+ {
+ name: "bipbop_short_pixel_metadata_smaller_than_in_stream_vp8.webm",
+ type: "video/webm",
+ duration: 1.011,
+ },
+
+ // End of webm dimension clashing files.
+
+ // A really short, low sample rate, single channel file. This tests whether
+ // we can handle playing files when only push very little audio data to the
+ // hardware.
+ { name: "spacestorm-1000Hz-100ms.ogg", type: "audio/ogg", duration: 0.099 },
+
+ // Opus data in an ogg container
+ {
+ name: "detodos-short.opus",
+ type: "audio/ogg; codecs=opus",
+ duration: 0.22,
+ contentDuration: 0.2135,
+ },
+ // Opus data in a webm container
+ {
+ name: "detodos-short.webm",
+ type: "audio/webm; codecs=opus",
+ duration: 0.26,
+ contentDuration: 0.2535,
+ },
+ // Opus in webm channel mapping=2 sample file
+ {
+ name: "opus-mapping2.webm",
+ type: "audio/webm; codecs=opus",
+ duration: 10.01,
+ contentDuration: 9.99,
+ },
+ { name: "bug1066943.webm", type: "audio/webm; codecs=opus", duration: 1.383 },
+
+ // Multichannel Opus in an ogg container
+ { name: "test-1-mono.opus", type: "audio/ogg; codecs=opus", duration: 1.044 },
+ {
+ name: "test-2-stereo.opus",
+ type: "audio/ogg; codecs=opus",
+ duration: 2.925,
+ },
+ { name: "test-3-LCR.opus", type: "audio/ogg; codecs=opus", duration: 4.214 },
+ { name: "test-4-quad.opus", type: "audio/ogg; codecs=opus", duration: 6.234 },
+ { name: "test-5-5.0.opus", type: "audio/ogg; codecs=opus", duration: 7.558 },
+ { name: "test-6-5.1.opus", type: "audio/ogg; codecs=opus", duration: 10.333 },
+ { name: "test-7-6.1.opus", type: "audio/ogg; codecs=opus", duration: 11.69 },
+ { name: "test-8-7.1.opus", type: "audio/ogg; codecs=opus", duration: 13.478 },
+
+ {
+ name: "gizmo-short.mp4",
+ type: "video/mp4",
+ duration: 0.27,
+ contentDuration: 0.267,
+ },
+ // Test playback of a MP4 file with a non-zero start time (and audio starting
+ // a second later).
+ { name: "bipbop-lateaudio.mp4", type: "video/mp4" },
+ // Opus in MP4 channel mapping=0 sample file (content shorter due to preskip)
+ {
+ name: "opus-sample.mp4",
+ type: "audio/mp4; codecs=opus",
+ duration: 10.92,
+ contentDuration: 10.09,
+ },
+ // Opus in MP4 channel mapping=2 sample file
+ { name: "opus-mapping2.mp4", type: "audio/mp4; codecs=opus", duration: 10.0 },
+
+ { name: "small-shot.m4a", type: "audio/mp4", duration: 0.29 },
+ { name: "small-shot.mp3", type: "audio/mpeg", duration: 0.27 },
+ { name: "owl.mp3", type: "audio/mpeg", duration: 3.343 },
+ // owl.mp3 as above, but with something funny going on in the ID3v2 tag
+ // that caused DirectShow to fail.
+ { name: "owl-funny-id3.mp3", type: "audio/mpeg", duration: 3.343 },
+ // owl.mp3 as above, but with something even funnier going on in the ID3v2 tag
+ // that caused DirectShow to fail.
+ { name: "owl-funnier-id3.mp3", type: "audio/mpeg", duration: 3.343 },
+ // One second of silence with ~140KB of ID3 tags. Usually when the first MP3
+ // frame is at such a high offset into the file, MP3FrameParser will give up
+ // and report that the stream is not MP3. However, it does not count ID3 tags
+ // in that offset. This test case makes sure that ID3 exclusion holds.
+ { name: "huge-id3.mp3", type: "audio/mpeg", duration: 1.0 },
+ // Half a second file of a sine with a large ID3v2 tag, followed by an ID3v1
+ // tag. The ID3v1 tags should be at the end of the file, but software usually
+ // play it anyway.
+ { name: "id3v1afterlongid3v2.mp3", type: "audio/mpeg", duration: 0.5 },
+ // An VBR file with a padding value that is greater than an mp3 packet, and
+ // also subsequent packets after the theoretical EOF computed from metadata,
+ // to test padding trimming edge cases.
+ {
+ name: "padding-spanning-multiple-packets.mp3",
+ type: "audio/mpeg",
+ },
+ // A truncated VBR MP3 with just enough frames to keep most decoders happy.
+ // The Xing header reports the length of the file to be around 10 seconds, but
+ // there is really only one second worth of data. We want MP3FrameParser to
+ // trust the header, so this should be reported as 10 seconds.
+ {
+ name: "vbr-head.mp3",
+ type: "audio/mpeg",
+ duration: 10.0,
+ contentDuration: 1.019,
+ },
+
+ // A flac file where the STREAMINFO block was removed.
+ // It is necessary to parse the file to find an audio frame instead.
+ { name: "flac-noheader-s16.flac", type: "audio/flac", duration: 4.0 },
+ { name: "flac-s24.flac", type: "audio/flac", duration: 4.04 },
+ {
+ name: "flac-sample.mp4",
+ type: "audio/mp4; codecs=flac",
+ duration: 4.95,
+ contentDuration: 5.03,
+ },
+ // Ogg with theora video and flac audio.
+ {
+ name: "A4.ogv",
+ type: "video/ogg",
+ width: 320,
+ height: 240,
+ duration: 3.13,
+ },
+ // A file that has no codec delay at the container level, but has a delay at
+ // the codec level.
+ {
+ name: "no-container-codec-delay.webm",
+ type: "video/webm",
+ },
+ // A file that has a codec delay at a container level of 0, but as a delay at
+ // the codec level that is non-zero.
+ { name: "invalid-preskip.webm", type: "audio/webm; codecs=opus" },
+
+ // Invalid file
+ { name: "bogus.duh", type: "bogus/duh", duration: Number.NaN },
+];
+
+const win32 =
+ SpecialPowers.Services.appinfo.OS == "WINNT" &&
+ !SpecialPowers.Services.appinfo.is64Bit;
+if (!win32) {
+ gPlayTests.push({ name: "av1.mp4", type: "video/mp4", duration: 1.0 });
+}
+
+// AAC files with different sample rates. We add these here as some are added
+// conditionally.
+gPlayTests.push(
+ {
+ name: "bipbop_audio_aac_8k.mp4",
+ type: "audio/mp4",
+ duration: 1.06,
+ },
+ {
+ name: "bipbop_audio_aac_22.05k.mp4",
+ type: "audio/mp4",
+ duration: 1.06,
+ },
+ {
+ name: "bipbop_audio_aac_44.1k.mp4",
+ type: "audio/mp4",
+ duration: 1.06,
+ },
+ {
+ name: "bipbop_audio_aac_48k.mp4",
+ type: "audio/mp4",
+ duration: 1.06,
+ }
+);
+if (AppConstants.platform != "win") {
+ // Windows WMF decoder doesn't do >48K everywhere. See bug 1698639.
+ gPlayTests.push(
+ {
+ name: "bipbop_audio_aac_88.2k.mp4",
+ type: "audio/mp4",
+ duration: 1.06,
+ },
+ {
+ name: "bipbop_audio_aac_96k.mp4",
+ type: "audio/mp4",
+ duration: 1.06,
+ }
+ );
+}
+
+// ambisonics.mp4 causes intermittents, so we conditionally add it until we fix
+// the root cause.
+const skipAmbisonics =
+ // Bug 1484451 - skip on mac debug
+ (AppConstants.platform == "macosx" && AppConstants.DEBUG) ||
+ // Bug 1483259 - skip on linux64 opt
+ (AppConstants.platform == "linux" &&
+ !AppConstants.DEBUG &&
+ SpecialPowers.Services.appinfo.is64Bit);
+if (!skipAmbisonics) {
+ // Ambisonics AAC, requires AAC extradata to be set when creating decoder (see bug 1431169)
+ // Also test 4.0 decoding.
+ gPlayTests.push({
+ name: "ambisonics.mp4",
+ type: "audio/mp4",
+ duration: 16.48,
+ });
+}
+
+var gSeekToNextFrameTests = [
+ // Test playback of a WebM file with vp9 video
+ { name: "vp9-short.webm", type: "video/webm", duration: 0.2 },
+ { name: "vp9cake-short.webm", type: "video/webm", duration: 1.0 },
+ // oggz-chop stream
+ { name: "bug482461.ogv", type: "video/ogg", duration: 4.34 },
+ // Theora only oggz-chop stream
+ { name: "bug482461-theora.ogv", type: "video/ogg", duration: 4.138 },
+ // With first frame a "duplicate" (empty) frame.
+ { name: "bug500311.ogv", type: "video/ogg", duration: 1.96 },
+
+ // More audio in file than video.
+ { name: "short-video.ogv", type: "video/ogg", duration: 1.081 },
+ // First Theora data packet is zero bytes.
+ { name: "bug504613.ogv", type: "video/ogg", duration: Number.NaN },
+ // Multiple audio streams.
+ { name: "bug516323.ogv", type: "video/ogg", duration: 4.208 },
+ // oggz-chop with non-keyframe as first frame
+ { name: "bug556821.ogv", type: "video/ogg", duration: 2.936 },
+ // Various weirdly formed Ogg files
+ { name: "bug498855-1.ogv", type: "video/ogg", duration: 0.24 },
+ { name: "bug498855-2.ogv", type: "video/ogg", duration: 0.24 },
+ { name: "bug498855-3.ogv", type: "video/ogg", duration: 0.24 },
+ { name: "bug504644.ogv", type: "video/ogg", duration: 1.6 },
+
+ { name: "bug523816.ogv", type: "video/ogg", duration: 0.766 },
+
+ { name: "bug498380.ogv", type: "video/ogg", duration: 0.2 },
+ { name: "bug557094.ogv", type: "video/ogg", duration: 0.24 },
+ { name: "multiple-bos.ogg", type: "video/ogg", duration: 0.431 },
+ // Test playback/metadata work after a redirect
+ {
+ name: "redirect.sjs?domain=mochi.test:8888&file=320x240.ogv",
+ type: "video/ogg",
+ duration: 0.266,
+ },
+ // Test playback of a webm file
+ { name: "seek-short.webm", type: "video/webm", duration: 0.23 },
+ // Test playback of a WebM file with non-zero start time.
+ { name: "split.webm", type: "video/webm", duration: 1.967 },
+
+ { name: "gizmo-short.mp4", type: "video/mp4", duration: 0.27 },
+
+ // Test playback of a MP4 file with a non-zero start time (and audio starting
+ // a second later).
+ { name: "bipbop-lateaudio.mp4", type: "video/mp4" },
+];
+
+// A file for each type we can support.
+var gSnifferTests = [
+ { name: "big.wav", type: "audio/x-wav", duration: 9.278982, size: 102444 },
+ {
+ name: "320x240.ogv",
+ type: "video/ogg",
+ width: 320,
+ height: 240,
+ duration: 0.233,
+ size: 28942,
+ },
+ { name: "seek.webm", type: "video/webm", duration: 3.966, size: 215529 },
+ { name: "gizmo.mp4", type: "video/mp4", duration: 5.56, size: 383631 },
+ // A mp3 file with id3 tags.
+ { name: "id3tags.mp3", type: "audio/mpeg", duration: 0.28, size: 3530 },
+ { name: "bogus.duh", type: "bogus/duh" },
+];
+
+// Files that contain resolution changes
+var gResolutionChangeTests = [
+ { name: "resolution-change.webm", type: "video/webm", duration: 6.533 },
+];
+
+// Files we must reject as invalid.
+var gInvalidTests = [
+ { name: "invalid-m0c0.opus", type: "audio/ogg; codecs=opus" },
+ { name: "invalid-m0c3.opus", type: "audio/ogg; codecs=opus" },
+ { name: "invalid-m1c0.opus", type: "audio/ogg; codecs=opus" },
+ { name: "invalid-m1c9.opus", type: "audio/ogg; codecs=opus" },
+ { name: "invalid-m2c0.opus", type: "audio/ogg; codecs=opus" },
+ { name: "invalid-m2c1.opus", type: "audio/ogg; codecs=opus" },
+ { name: "invalid-cmap-short.opus", type: "audio/ogg; codecs=opus" },
+ { name: "invalid-cmap-s0c0.opus", type: "audio/ogg; codecs=opus" },
+ { name: "invalid-cmap-s0c2.opus", type: "audio/ogg; codecs=opus" },
+ { name: "invalid-cmap-s1c2.opus", type: "audio/ogg; codecs=opus" },
+];
+
+var gInvalidPlayTests = [
+ { name: "invalid-excess_discard.webm", type: "audio/webm; codecs=opus" },
+ { name: "invalid-excess_neg_discard.webm", type: "audio/webm; codecs=opus" },
+ { name: "invalid-neg_discard.webm", type: "audio/webm; codecs=opus" },
+ {
+ name: "invalid-discard_on_multi_blocks.webm",
+ type: "audio/webm; codecs=opus",
+ },
+];
+
+// Files to check different cases of ogg skeleton information.
+// sample-fisbone-skeleton4.ogv
+// - Skeleton v4, w/ Content-Type,Role,Name,Language,Title for both theora/vorbis
+// sample-fisbone-wrong-header.ogv
+// - Skeleton v4, wrong message field sequence for vorbis
+// multiple-bos-more-header-fields.ogg
+// - Skeleton v3, w/ Content-Type,Role,Name,Language,Title for both theora/vorbis
+// seek-short.ogv
+// - No skeleton, but theora
+// audio-gaps-short.ogg
+// - No skeleton, but vorbis
+var gMultitrackInfoOggPlayList = [
+ { name: "sample-fisbone-skeleton4.ogv", type: "video/ogg", duration: 1.0 },
+ { name: "sample-fisbone-wrong-header.ogv", type: "video/ogg", duration: 1.0 },
+ {
+ name: "multiple-bos-more-header-fileds.ogg",
+ type: "video/ogg",
+ duration: 0.431,
+ },
+ { name: "seek-short.ogv", type: "video/ogg", duration: 1.03 },
+ { name: "audio-gaps-short.ogg", type: "audio/ogg", duration: 0.5 },
+];
+// Pre-parsed results of gMultitrackInfoOggPlayList.
+var gOggTrackInfoResults = {
+ "sample-fisbone-skeleton4.ogv": {
+ audio_id: " audio_1",
+ audio_kind: "main",
+ audio_language: " en-US",
+ audio_label: " Audio track for test",
+ video_id: " video_1",
+ video_kind: "main",
+ video_language: " fr",
+ video_label: " Video track for test",
+ },
+ "sample-fisbone-wrong-header.ogv": {
+ audio_id: "1",
+ audio_kind: "main",
+ audio_language: "",
+ audio_label: "",
+ video_id: " video_1",
+ video_kind: "main",
+ video_language: " fr",
+ video_label: " Video track for test",
+ },
+ "multiple-bos-more-header-fileds.ogg": {
+ audio_id: "1",
+ audio_kind: "main",
+ audio_language: "",
+ audio_label: "",
+ video_id: "2",
+ video_kind: "main",
+ video_language: "",
+ video_label: "",
+ },
+ "seek-short.ogv": {
+ video_id: "2",
+ video_kind: "main",
+ video_language: "",
+ video_label: "",
+ },
+ "audio-gaps-short.ogg": {
+ audio_id: "1",
+ audio_kind: "main",
+ audio_language: "",
+ audio_label: "",
+ },
+};
+
+// Returns a promise that resolves to a function that converts
+// relative paths to absolute, to test loading files from file: URIs.
+// Optionally checks whether the file actually exists on disk at the location
+// we've specified.
+function makeAbsolutePathConverter() {
+ const url = SimpleTest.getTestFileURL("chromeHelper.js");
+ const script = SpecialPowers.loadChromeScript(url);
+ return new Promise((resolve, reject) => {
+ script.addMessageListener("media-test:cwd", cwd => {
+ if (!cwd) {
+ ok(false, "Failed to find path to test files");
+ }
+
+ resolve((path, mustExist) => {
+ // android mochitest doesn't support file://
+ if (manifestNavigator().appVersion.includes("Android")) {
+ return path;
+ }
+
+ const { Ci, Cc } = SpecialPowers;
+ var f = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ f.initWithPath(cwd);
+ var split = path.split("/");
+ for (var i = 0; i < split.length; ++i) {
+ f.append(split[i]);
+ }
+ if (mustExist && !f.exists()) {
+ ok(false, "We expected '" + path + "' to exist, but it doesn't!");
+ }
+ return f.path;
+ });
+ });
+ script.sendAsyncMessage("media-test:getcwd");
+ });
+}
+
+// Returns true if two TimeRanges are equal, false otherwise
+function range_equals(r1, r2) {
+ if (r1.length != r2.length) {
+ return false;
+ }
+ for (var i = 0; i < r1.length; i++) {
+ if (r1.start(i) != r2.start(i) || r1.end(i) != r2.end(i)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+// These are URIs to files that we use to check that we don't leak any state
+// or other information such that script can determine stuff about a user's
+// environment. Used by test_info_leak.
+function makeInfoLeakTests() {
+ return makeAbsolutePathConverter().then(fileUriToSrc => [
+ {
+ type: "video/ogg",
+ src: fileUriToSrc("tests/dom/media/test/320x240.ogv", true),
+ },
+ {
+ type: "video/ogg",
+ src: fileUriToSrc("tests/dom/media/test/404.ogv", false),
+ },
+ {
+ type: "audio/x-wav",
+ src: fileUriToSrc("tests/dom/media/test/r11025_s16_c1.wav", true),
+ },
+ {
+ type: "audio/x-wav",
+ src: fileUriToSrc("tests/dom/media/test/404.wav", false),
+ },
+ {
+ type: "audio/ogg",
+ src: fileUriToSrc("tests/dom/media/test/bug461281.ogg", true),
+ },
+ {
+ type: "audio/ogg",
+ src: fileUriToSrc("tests/dom/media/test/404.ogg", false),
+ },
+ {
+ type: "video/webm",
+ src: fileUriToSrc("tests/dom/media/test/seek.webm", true),
+ },
+ {
+ type: "video/webm",
+ src: fileUriToSrc("tests/dom/media/test/404.webm", false),
+ },
+ {
+ type: "video/ogg",
+ src: "http://localhost/404.ogv",
+ },
+ {
+ type: "audio/x-wav",
+ src: "http://localhost/404.wav",
+ },
+ {
+ type: "video/webm",
+ src: "http://localhost/404.webm",
+ },
+ {
+ type: "video/ogg",
+ src: "http://example.com/tests/dom/media/test/test_info_leak.html",
+ },
+ {
+ type: "audio/ogg",
+ src: "http://example.com/tests/dom/media/test/test_info_leak.html",
+ },
+ ]);
+}
+
+// These are files that must fire an error during load or playback, and do not
+// cause a crash. Used by test_playback_errors, which expects one error event
+// and no ended event. Put files of the same type together in this list so if
+// something crashes we have some idea of which backend is responsible.
+var gErrorTests = [
+ { name: "bogus.wav", type: "audio/x-wav" },
+ { name: "bogus.ogv", type: "video/ogg" },
+ { name: "448636.ogv", type: "video/ogg" },
+ { name: "bug504843.ogv", type: "video/ogg" },
+ { name: "bug501279.ogg", type: "audio/ogg" },
+ { name: "bug603918.webm", type: "video/webm" },
+ { name: "bug604067.webm", type: "video/webm" },
+ { name: "bug1535980.webm", type: "video/webm" },
+ { name: "bug1799787.webm", type: "video/webm" },
+ { name: "bogus.duh", type: "bogus/duh" },
+];
+
+// These files would get error after receiving "loadedmetadata", we would like
+// to check duration in "onerror" and make sure the duration is still available.
+var gDurationTests = [
+ { name: "bug603918.webm", duration: 6.076 },
+ { name: "bug604067.webm", duration: 6.076 },
+];
+
+// These are files that have nontrivial duration and are useful for seeking within.
+var gSeekTests = [
+ { name: "r11025_s16_c1.wav", type: "audio/x-wav", duration: 1.0 },
+ { name: "audio.wav", type: "audio/x-wav", duration: 0.031247 },
+ { name: "seek.ogv", type: "video/ogg", duration: 3.966 },
+ { name: "320x240.ogv", type: "video/ogg", duration: 0.266 },
+ { name: "seek.webm", type: "video/webm", duration: 3.966 },
+ { name: "sine.webm", type: "audio/webm", duration: 4.001 },
+ { name: "bug516323.indexed.ogv", type: "video/ogg", duration: 4.208333 },
+ { name: "split.webm", type: "video/webm", duration: 1.967 },
+ { name: "detodos.opus", type: "audio/ogg; codecs=opus", duration: 2.9135 },
+ { name: "gizmo.mp4", type: "video/mp4", duration: 5.56 },
+ { name: "owl.mp3", type: "audio/mpeg", duration: 3.343 },
+ { name: "bogus.duh", type: "bogus/duh", duration: 123 },
+
+ // Bug 1242338: hit a numerical problem while seeking to the duration.
+ { name: "bug482461-theora.ogv", type: "video/ogg", duration: 4.138 },
+];
+
+var gFastSeekTests = [
+ {
+ name: "gizmo.mp4",
+ type: "video/mp4",
+ keyframes: [0, 1.0, 2.0, 3.0, 4.0, 5.0],
+ },
+ // Note: Not all keyframes in the file are actually referenced in the Cues in this file.
+ { name: "seek.webm", type: "video/webm", keyframes: [0, 0.8, 1.6, 2.4, 3.2] },
+ // Note: the sync points are the points on both the audio and video streams
+ // before the keyframes. You can't just assume that the keyframes are the sync
+ // points, as the audio required for that sync point may be before the keyframe.
+ {
+ name: "bug516323.indexed.ogv",
+ type: "video/ogg",
+ keyframes: [0, 0.46, 3.06],
+ },
+];
+
+// These files are WebMs without cues. They're seekable within their buffered
+// ranges. If work renders WebMs fully seekable these files should be moved
+// into gSeekTests
+var gCuelessWebMTests = [
+ { name: "no-cues.webm", type: "video/webm", duration: 3.967 },
+];
+
+// These are files that are non seekable, due to problems with the media,
+// for example broken or missing indexes.
+var gUnseekableTests = [{ name: "bogus.duh", type: "bogus/duh" }];
+
+var androidVersion = -1; // non-Android platforms
+if (
+ manifestNavigator().userAgent.includes("Mobile") ||
+ manifestNavigator().userAgent.includes("Tablet")
+) {
+ androidVersion = SpecialPowers.Services.sysinfo.getProperty("version");
+}
+
+function getAndroidVersion() {
+ return androidVersion;
+}
+
+// These are files suitable for using with a "new Audio" constructor.
+var gAudioTests = [
+ { name: "adts.aac", type: "audio/mp4", duration: 1.37 },
+ { name: "r11025_s16_c1.wav", type: "audio/x-wav", duration: 1.0 },
+ { name: "sound.ogg", type: "audio/ogg" },
+ { name: "owl.mp3", type: "audio/mpeg", duration: 3.343 },
+ { name: "small-shot.m4a", type: "audio/mp4", duration: 0.29 },
+ { name: "bogus.duh", type: "bogus/duh", duration: 123 },
+ { name: "empty_size.mp3", type: "audio/mpeg", duration: 2.235 },
+];
+
+// These files ensure our handling of 404 errors is consistent across the
+// various backends.
+var g404Tests = [
+ { name: "404.wav", type: "audio/x-wav" },
+ { name: "404.ogv", type: "video/ogg" },
+ { name: "404.oga", type: "audio/ogg" },
+ { name: "404.webm", type: "video/webm" },
+ { name: "bogus.duh", type: "bogus/duh" },
+];
+
+// These are files suitable for testing various decoder failures that are
+// expected to fire MEDIA_ERR_DECODE. Used by test_decode_error, which expects
+// an error and emptied event, and no loadedmetadata or ended event.
+var gDecodeErrorTests = [
+ // Valid files with unsupported codecs
+ { name: "r11025_msadpcm_c1.wav", type: "audio/x-wav" },
+ { name: "dirac.ogg", type: "video/ogg" },
+ // Invalid files
+ { name: "bogus.wav", type: "audio/x-wav" },
+ { name: "bogus.ogv", type: "video/ogg" },
+
+ { name: "bogus.duh", type: "bogus/duh" },
+];
+
+// These are files that are used for media fragments tests
+var gFragmentTests = [
+ { name: "big.wav", type: "audio/x-wav", duration: 9.278982, size: 102444 },
+];
+
+// Used by test_chaining.html. The |links| attributes is the number of links in
+// this file that we should be able to play.
+var gChainingTests = [
+ // Vorbis and Opus chained file. They have user comments |index=n| where `n`
+ // is the index of this segment in the file, 0 indexed.
+ { name: "chain.ogg", type: "audio/ogg", links: 4 },
+ { name: "chain.opus", type: "audio/ogg; codec=opus", links: 4 },
+ // Those files are chained files with a different number of channels in each
+ // part. This is not supported and should stop playing after the first part.
+ { name: "variable-channel.ogg", type: "audio/ogg", links: 1 },
+ { name: "variable-channel.opus", type: "audio/ogg; codec=opus", links: 1 },
+ // Those files are chained files with a different sample rate in each
+ // part. This is not supported and should stop playing after the first part.
+ { name: "variable-samplerate.ogg", type: "audio/ogg", links: 1 },
+ // Opus decoding in Firefox outputs 48 kHz PCM despite having a different
+ // original sample rate, so we can safely play Opus chained media that have
+ // different samplerate accross links.
+ { name: "variable-samplerate.opus", type: "audio/ogg; codec=opus", links: 2 },
+ // A chained video file. We don't support those, so only one link should be
+ // reported.
+ { name: "chained-video.ogv", type: "video/ogg", links: 1 },
+ // A file that consist in 4 links of audio, then another link that has video.
+ // We should stop right after the 4 audio links.
+ { name: "chained-audio-video.ogg", type: "video/ogg", links: 4 },
+ // An opus file that has two links, with a different preskip value for each
+ // link. We should be able to play both links.
+ { name: "variable-preskip.opus", type: "audio/ogg; codec=opus", links: 2 },
+ { name: "bogus.duh", type: "bogus/duh" },
+];
+
+// Videos with an aspect ratio. Used for testing that displaying frames
+// on a canvas works correctly in the case of non-standard aspect ratios.
+// See bug 874897 for an example.
+var gAspectRatioTests = [
+ { name: "VID_0001.ogg", type: "video/ogg", duration: 19.966 },
+];
+
+// These are files with non-trivial tag sets.
+// Used by test_metadata.html.
+var gMetadataTests = [
+ // Ogg Vorbis files
+ {
+ name: "short-video.ogv",
+ tags: {
+ TITLE: "Lepidoptera",
+ ARTIST: "Epoq",
+ ALBUM: "Kahvi Collective",
+ DATE: "2002",
+ COMMENT: "http://www.kahvi.org",
+ },
+ },
+ {
+ name: "bug516323.ogv",
+ tags: {
+ GENRE: "Open Movie",
+ ENCODER: "Audacity",
+ TITLE: "Elephants Dream",
+ ARTIST: "Silvia Pfeiffer",
+ COMMENTS: "Audio Description",
+ },
+ },
+ {
+ name: "bug516323.indexed.ogv",
+ tags: {
+ GENRE: "Open Movie",
+ ENCODER: "Audacity",
+ TITLE: "Elephants Dream",
+ ARTIST: "Silvia Pfeiffer",
+ COMMENTS: "Audio Description",
+ },
+ },
+ {
+ name: "detodos.opus",
+ tags: {
+ title: "De todos. Para todos.",
+ artist: "Mozilla.org",
+ },
+ },
+ { name: "sound.ogg", tags: {} },
+ {
+ name: "small-shot.ogg",
+ tags: {
+ title: "Pew SFX",
+ },
+ },
+ {
+ name: "badtags.ogg",
+ tags: {
+ // We list only the valid tags here, and verify
+ // the invalid ones are filtered out.
+ title: "Invalid comments test file",
+ empty: "",
+ "": "empty",
+ "{- [(`!@\"#$%^&')] -}": "valid tag name, surprisingly",
+ // The file also includes the following invalid tags.
+ // "A description with no separator is a common problem.",
+ // "雨":"Likely, but an invalid key (non-ascii).",
+ // "not\nval\x1fid":"invalid tag name",
+ // "not~valid":"this isn't a valid name either",
+ // "not-utf-8":"invalid sequences: \xff\xfe\xfa\xfb\0eol"
+ },
+ },
+ {
+ name: "wave_metadata.wav",
+ tags: {
+ name: "Track Title",
+ artist: "Artist Name",
+ comments: "Comments",
+ },
+ },
+ {
+ name: "wave_metadata_utf8.wav",
+ tags: {
+ name: "歌曲名稱",
+ artist: "作曲者",
+ comments: "註解",
+ },
+ },
+ {
+ name: "wave_metadata_unknown_tag.wav",
+ tags: {
+ name: "Track Title",
+ comments: "Comments",
+ },
+ },
+ {
+ name: "wave_metadata_bad_len.wav",
+ tags: {
+ name: "Track Title",
+ artist: "Artist Name",
+ comments: "Comments",
+ },
+ },
+ {
+ name: "wave_metadata_bad_no_null.wav",
+ tags: {
+ name: "Track Title",
+ artist: "Artist Name",
+ comments: "Comments!!",
+ },
+ },
+ {
+ name: "wave_metadata_bad_utf8.wav",
+ tags: {
+ name: "歌曲名稱",
+ comments: "註解",
+ },
+ },
+ { name: "wavedata_u8.wav", tags: {} },
+];
+
+// Now Fennec doesn't support flac, so only test it on non-android platforms.
+if (getAndroidVersion() < 0) {
+ gMetadataTests = gMetadataTests.concat([
+ {
+ name: "flac-s24.flac",
+ tags: {
+ ALBUM: "Seascapes",
+ TITLE: "(La Mer) - II. Jeux de vagues. Allegro",
+ COMPOSER: "Debussy, Claude",
+ TRACKNUMBER: "2/9",
+ DISCNUMBER: "1/1",
+ encoder: "Lavf57.41.100",
+ },
+ },
+ ]);
+}
+
+// Test files for Encrypted Media Extensions
+var gEMETests = [
+ {
+ name: "vp9 in mp4",
+ tracks: [
+ {
+ name: "video",
+ type: 'video/mp4; codecs="vp9.0"',
+ fragments: ["short-vp9-encrypted-video.mp4"],
+ },
+ {
+ name: "audio",
+ type: 'audio/mp4; codecs="mp4a.40.2"',
+ fragments: ["short-aac-encrypted-audio.mp4"],
+ },
+ ],
+ keys: {
+ "2cdb0ed6119853e7850671c3e9906c3c": "808B9ADAC384DE1E4F56140F4AD76194",
+ },
+ sessionType: "temporary",
+ sessionCount: 2,
+ duration: 0.47,
+ },
+ {
+ name: "video-only with 2 keys",
+ tracks: [
+ {
+ name: "video",
+ type: 'video/mp4; codecs="avc1.4d4015"',
+ fragments: [
+ "bipbop-cenc-videoinit.mp4",
+ "bipbop-cenc-video1.m4s",
+ "bipbop-cenc-video2.m4s",
+ ],
+ },
+ ],
+ keys: {
+ // "keyid" : "key"
+ "7e571d037e571d037e571d037e571d03": "7e5733337e5733337e5733337e573333",
+ "7e571d047e571d047e571d047e571d04": "7e5744447e5744447e5744447e574444",
+ },
+ sessionType: "temporary",
+ sessionCount: 1,
+ duration: 1.6,
+ },
+ {
+ name: "video-only with 2 keys, CORS",
+ tracks: [
+ {
+ name: "video",
+ type: 'video/mp4; codecs="avc1.4d4015"',
+ fragments: [
+ "bipbop-cenc-videoinit.mp4",
+ "bipbop-cenc-video1.m4s",
+ "bipbop-cenc-video2.m4s",
+ ],
+ },
+ ],
+ keys: {
+ // "keyid" : "key"
+ "7e571d037e571d037e571d037e571d03": "7e5733337e5733337e5733337e573333",
+ "7e571d047e571d047e571d047e571d04": "7e5744447e5744447e5744447e574444",
+ },
+ sessionType: "temporary",
+ sessionCount: 1,
+ crossOrigin: true,
+ duration: 1.6,
+ },
+ {
+ name: "audio&video tracks, both with all keys",
+ tracks: [
+ {
+ name: "audio",
+ type: 'audio/mp4; codecs="mp4a.40.2"',
+ fragments: [
+ "bipbop-cenc-audioinit.mp4",
+ "bipbop-cenc-audio1.m4s",
+ "bipbop-cenc-audio2.m4s",
+ "bipbop-cenc-audio3.m4s",
+ ],
+ },
+ {
+ name: "video",
+ type: 'video/mp4; codecs="avc1.4d4015"',
+ fragments: [
+ "bipbop-cenc-videoinit.mp4",
+ "bipbop-cenc-video1.m4s",
+ "bipbop-cenc-video2.m4s",
+ ],
+ },
+ ],
+ keys: {
+ // "keyid" : "key"
+ "7e571d037e571d037e571d037e571d03": "7e5733337e5733337e5733337e573333",
+ "7e571d047e571d047e571d047e571d04": "7e5744447e5744447e5744447e574444",
+ },
+ sessionType: "temporary",
+ sessionCount: 2,
+ duration: 1.6,
+ },
+ {
+ name: "audio&video tracks, both with all keys, CORS",
+ tracks: [
+ {
+ name: "audio",
+ type: 'audio/mp4; codecs="mp4a.40.2"',
+ fragments: [
+ "bipbop-cenc-audioinit.mp4",
+ "bipbop-cenc-audio1.m4s",
+ "bipbop-cenc-audio2.m4s",
+ "bipbop-cenc-audio3.m4s",
+ ],
+ },
+ {
+ name: "video",
+ type: 'video/mp4; codecs="avc1.4d4015"',
+ fragments: [
+ "bipbop-cenc-videoinit.mp4",
+ "bipbop-cenc-video1.m4s",
+ "bipbop-cenc-video2.m4s",
+ ],
+ },
+ ],
+ keys: {
+ // "keyid" : "key"
+ "7e571d037e571d037e571d037e571d03": "7e5733337e5733337e5733337e573333",
+ "7e571d047e571d047e571d047e571d04": "7e5744447e5744447e5744447e574444",
+ },
+ sessionType: "temporary",
+ sessionCount: 2,
+ crossOrigin: true,
+ duration: 1.6,
+ },
+ {
+ name: "400x300 audio&video tracks, each with its key",
+ tracks: [
+ {
+ name: "audio",
+ type: 'audio/mp4; codecs="mp4a.40.2"',
+ fragments: [
+ "bipbop_300_215kbps-cenc-audio-key1-init.mp4",
+ "bipbop_300_215kbps-cenc-audio-key1-1.m4s",
+ "bipbop_300_215kbps-cenc-audio-key1-2.m4s",
+ "bipbop_300_215kbps-cenc-audio-key1-3.m4s",
+ "bipbop_300_215kbps-cenc-audio-key1-4.m4s",
+ ],
+ },
+ {
+ name: "video",
+ type: 'video/mp4; codecs="avc1.4d4015"',
+ fragments: [
+ "bipbop_300_215kbps-cenc-video-key1-init.mp4",
+ "bipbop_300_215kbps-cenc-video-key1-1.m4s",
+ "bipbop_300_215kbps-cenc-video-key1-2.m4s",
+ ],
+ },
+ ],
+ keys: {
+ // "keyid" : "key"
+ "7e571d037e571d037e571d037e571d11": "7e5733337e5733337e5733337e573311",
+ "7e571d047e571d047e571d047e571d21": "7e5744447e5744447e5744447e574421",
+ },
+ sessionType: "temporary",
+ sessionCount: 2,
+ duration: 1.6,
+ },
+ {
+ name: "640x480@624kbps audio&video tracks, each with its key",
+ tracks: [
+ {
+ name: "audio",
+ type: 'audio/mp4; codecs="mp4a.40.2"',
+ fragments: [
+ "bipbop_480_624kbps-cenc-audio-key1-init.mp4",
+ "bipbop_480_624kbps-cenc-audio-key1-1.m4s",
+ "bipbop_480_624kbps-cenc-audio-key1-2.m4s",
+ "bipbop_480_624kbps-cenc-audio-key1-3.m4s",
+ "bipbop_480_624kbps-cenc-audio-key1-4.m4s",
+ ],
+ },
+ {
+ name: "video",
+ type: 'video/mp4; codecs="avc1.4d401e"',
+ fragments: [
+ "bipbop_480_624kbps-cenc-video-key1-init.mp4",
+ "bipbop_480_624kbps-cenc-video-key1-1.m4s",
+ "bipbop_480_624kbps-cenc-video-key1-2.m4s",
+ ],
+ },
+ ],
+ keys: {
+ // "keyid" : "key"
+ "7e571d037e571d037e571d037e571d11": "7e5733337e5733337e5733337e573311",
+ "7e571d047e571d047e571d047e571d21": "7e5744447e5744447e5744447e574421",
+ },
+ sessionType: "temporary",
+ sessionCount: 2,
+ duration: 1.6,
+ },
+ {
+ name: "640x480@959kbps audio&video tracks, each with its key",
+ tracks: [
+ {
+ name: "audio",
+ type: 'audio/mp4; codecs="mp4a.40.2"',
+ fragments: [
+ "bipbop_480_959kbps-cenc-audio-key1-init.mp4",
+ "bipbop_480_959kbps-cenc-audio-key1-1.m4s",
+ "bipbop_480_959kbps-cenc-audio-key1-2.m4s",
+ "bipbop_480_959kbps-cenc-audio-key1-3.m4s",
+ "bipbop_480_959kbps-cenc-audio-key1-4.m4s",
+ ],
+ },
+ {
+ name: "video",
+ type: 'video/mp4; codecs="avc1.4d401e"',
+ fragments: [
+ "bipbop_480_959kbps-cenc-video-key1-init.mp4",
+ "bipbop_480_959kbps-cenc-video-key1-1.m4s",
+ "bipbop_480_959kbps-cenc-video-key1-2.m4s",
+ ],
+ },
+ ],
+ keys: {
+ // "keyid" : "key"
+ "7e571d037e571d037e571d037e571d11": "7e5733337e5733337e5733337e573311",
+ "7e571d047e571d047e571d047e571d21": "7e5744447e5744447e5744447e574421",
+ },
+ sessionType: "temporary",
+ sessionCount: 2,
+ duration: 1.6,
+ },
+ {
+ name: "640x480 then 400x300, same key (1st) per track",
+ tracks: [
+ {
+ name: "audio",
+ type: 'audio/mp4; codecs="mp4a.40.2"',
+ fragments: [
+ "bipbop_480_624kbps-cenc-audio-key1-init.mp4",
+ "bipbop_480_624kbps-cenc-audio-key1-1.m4s",
+ "bipbop_480_624kbps-cenc-audio-key1-2.m4s",
+ "bipbop_480_624kbps-cenc-audio-key1-3.m4s",
+ "bipbop_480_624kbps-cenc-audio-key1-4.m4s",
+ ],
+ },
+ {
+ name: "video",
+ type: 'video/mp4; codecs="avc1.4d401e,avc1.4d4015"',
+ fragments: [
+ "bipbop_480_624kbps-cenc-video-key1-init.mp4",
+ "bipbop_480_624kbps-cenc-video-key1-1.m4s",
+ "bipbop_300_215kbps-cenc-video-key1-init.mp4",
+ "bipbop_300_215kbps-cenc-video-key1-2.m4s",
+ ],
+ },
+ ],
+ keys: {
+ // "keyid" : "key"
+ "7e571d037e571d037e571d037e571d11": "7e5733337e5733337e5733337e573311",
+ "7e571d047e571d047e571d047e571d21": "7e5744447e5744447e5744447e574421",
+ },
+ sessionType: "temporary",
+ sessionCount: 3,
+ duration: 1.6,
+ },
+ {
+ name: "640x480 then 400x300, same key (2nd) per track",
+ tracks: [
+ {
+ name: "audio",
+ type: 'audio/mp4; codecs="mp4a.40.2"',
+ fragments: [
+ "bipbop_480_624kbps-cenc-audio-key2-init.mp4",
+ "bipbop_480_624kbps-cenc-audio-key2-1.m4s",
+ "bipbop_480_624kbps-cenc-audio-key2-2.m4s",
+ "bipbop_480_624kbps-cenc-audio-key2-3.m4s",
+ "bipbop_480_624kbps-cenc-audio-key2-4.m4s",
+ ],
+ },
+ {
+ name: "video",
+ type: 'video/mp4; codecs="avc1.4d401e,avc1.4d4015"',
+ fragments: [
+ "bipbop_480_624kbps-cenc-video-key2-init.mp4",
+ "bipbop_480_624kbps-cenc-video-key2-1.m4s",
+ "bipbop_300_215kbps-cenc-video-key2-init.mp4",
+ "bipbop_300_215kbps-cenc-video-key2-2.m4s",
+ ],
+ },
+ ],
+ keys: {
+ // "keyid" : "key"
+ "7e571d037e571d037e571d037e571d12": "7e5733337e5733337e5733337e573312",
+ "7e571d047e571d047e571d047e571d22": "7e5744447e5744447e5744447e574422",
+ },
+ sessionType: "temporary",
+ sessionCount: 3,
+ duration: 1.6,
+ },
+ {
+ name: "640x480 with 1st keys then 400x300 with 2nd keys",
+ tracks: [
+ {
+ name: "audio",
+ type: 'audio/mp4; codecs="mp4a.40.2"',
+ fragments: [
+ "bipbop_480_624kbps-cenc-audio-key1-init.mp4",
+ "bipbop_480_624kbps-cenc-audio-key1-1.m4s",
+ "bipbop_480_624kbps-cenc-audio-key1-2.m4s",
+ "bipbop_480_624kbps-cenc-audio-key1-3.m4s",
+ "bipbop_480_624kbps-cenc-audio-key1-4.m4s",
+ ],
+ },
+ {
+ name: "video",
+ type: 'video/mp4; codecs="avc1.4d401e,avc1.4d4015"',
+ fragments: [
+ "bipbop_480_624kbps-cenc-video-key1-init.mp4",
+ "bipbop_480_624kbps-cenc-video-key1-1.m4s",
+ "bipbop_300_215kbps-cenc-video-key2-init.mp4",
+ "bipbop_300_215kbps-cenc-video-key2-2.m4s",
+ ],
+ },
+ ],
+ keys: {
+ // "keyid" : "key"
+ "7e571d037e571d037e571d037e571d11": "7e5733337e5733337e5733337e573311",
+ "7e571d037e571d037e571d037e571d12": "7e5733337e5733337e5733337e573312",
+ "7e571d047e571d047e571d047e571d21": "7e5744447e5744447e5744447e574421",
+ },
+ sessionType: "temporary",
+ sessionCount: 3,
+ duration: 1.6,
+ },
+ {
+ name: "400x300 with 1st keys then 640x480 with 2nd keys",
+ tracks: [
+ {
+ name: "audio",
+ type: 'audio/mp4; codecs="mp4a.40.2"',
+ fragments: [
+ "bipbop_300_215kbps-cenc-audio-key1-init.mp4",
+ "bipbop_300_215kbps-cenc-audio-key1-1.m4s",
+ "bipbop_300_215kbps-cenc-audio-key1-2.m4s",
+ "bipbop_300_215kbps-cenc-audio-key1-3.m4s",
+ "bipbop_300_215kbps-cenc-audio-key1-4.m4s",
+ ],
+ },
+ {
+ name: "video",
+ type: 'video/mp4; codecs="avc1.4d4015,avc1.4d401e"',
+ fragments: [
+ "bipbop_300_215kbps-cenc-video-key1-init.mp4",
+ "bipbop_300_215kbps-cenc-video-key1-1.m4s",
+ "bipbop_480_624kbps-cenc-video-key2-init.mp4",
+ "bipbop_480_624kbps-cenc-video-key2-2.m4s",
+ ],
+ },
+ ],
+ keys: {
+ // "keyid" : "key"
+ "7e571d037e571d037e571d037e571d11": "7e5733337e5733337e5733337e573311",
+ "7e571d037e571d037e571d037e571d12": "7e5733337e5733337e5733337e573312",
+ "7e571d047e571d047e571d047e571d21": "7e5744447e5744447e5744447e574421",
+ },
+ sessionType: "temporary",
+ sessionCount: 3,
+ duration: 1.6,
+ },
+ {
+ name: "640x480@959kbps with 1st keys then 640x480@624kbps with 2nd keys",
+ tracks: [
+ {
+ name: "audio",
+ type: 'audio/mp4; codecs="mp4a.40.2"',
+ fragments: [
+ "bipbop_480_959kbps-cenc-audio-key1-init.mp4",
+ "bipbop_480_959kbps-cenc-audio-key1-1.m4s",
+ "bipbop_480_959kbps-cenc-audio-key1-2.m4s",
+ "bipbop_480_959kbps-cenc-audio-key1-3.m4s",
+ "bipbop_480_959kbps-cenc-audio-key1-4.m4s",
+ ],
+ },
+ {
+ name: "video",
+ type: 'video/mp4; codecs="avc1.4d401e"',
+ fragments: [
+ "bipbop_480_959kbps-cenc-video-key1-init.mp4",
+ "bipbop_480_959kbps-cenc-video-key1-1.m4s",
+ "bipbop_480_624kbps-cenc-video-key2-init.mp4",
+ "bipbop_480_624kbps-cenc-video-key2-2.m4s",
+ ],
+ },
+ ],
+ keys: {
+ // "keyid" : "key"
+ "7e571d037e571d037e571d037e571d11": "7e5733337e5733337e5733337e573311",
+ "7e571d037e571d037e571d037e571d12": "7e5733337e5733337e5733337e573312",
+ "7e571d047e571d047e571d047e571d21": "7e5744447e5744447e5744447e574421",
+ },
+ sessionType: "temporary",
+ sessionCount: 3,
+ duration: 1.6,
+ },
+ {
+ name: "640x480@624kbps with 1st keys then 640x480@959kbps with 2nd keys",
+ tracks: [
+ {
+ name: "audio",
+ type: 'audio/mp4; codecs="mp4a.40.2"',
+ fragments: [
+ "bipbop_480_624kbps-cenc-audio-key1-init.mp4",
+ "bipbop_480_624kbps-cenc-audio-key1-1.m4s",
+ "bipbop_480_624kbps-cenc-audio-key1-2.m4s",
+ "bipbop_480_624kbps-cenc-audio-key1-3.m4s",
+ "bipbop_480_624kbps-cenc-audio-key1-4.m4s",
+ ],
+ },
+ {
+ name: "video",
+ type: 'video/mp4; codecs="avc1.4d401e"',
+ fragments: [
+ "bipbop_480_624kbps-cenc-video-key1-init.mp4",
+ "bipbop_480_624kbps-cenc-video-key1-1.m4s",
+ "bipbop_480_959kbps-cenc-video-key2-init.mp4",
+ "bipbop_480_959kbps-cenc-video-key2-2.m4s",
+ ],
+ },
+ ],
+ keys: {
+ // "keyid" : "key"
+ "7e571d037e571d037e571d037e571d11": "7e5733337e5733337e5733337e573311",
+ "7e571d037e571d037e571d037e571d12": "7e5733337e5733337e5733337e573312",
+ "7e571d047e571d047e571d047e571d21": "7e5744447e5744447e5744447e574421",
+ },
+ sessionType: "temporary",
+ sessionCount: 3,
+ duration: 1.6,
+ },
+ {
+ name: "400x300 with presentation size 533x300",
+ tracks: [
+ {
+ name: "audio",
+ type: 'audio/mp4; codecs="mp4a.40.2"',
+ fragments: [
+ "bipbop_300wp_227kbps-cenc-audio-key1-init.mp4",
+ "bipbop_300wp_227kbps-cenc-audio-key1-1.m4s",
+ "bipbop_300wp_227kbps-cenc-audio-key1-2.m4s",
+ "bipbop_300wp_227kbps-cenc-audio-key1-3.m4s",
+ "bipbop_300wp_227kbps-cenc-audio-key1-4.m4s",
+ ],
+ },
+ {
+ name: "video",
+ type: 'video/mp4; codecs="avc1.4d4015"',
+ fragments: [
+ "bipbop_300wp_227kbps-cenc-video-key1-init.mp4",
+ "bipbop_300wp_227kbps-cenc-video-key1-1.m4s",
+ "bipbop_300wp_227kbps-cenc-video-key1-2.m4s",
+ ],
+ },
+ ],
+ keys: {
+ // "keyid" : "key"
+ "7e571d037e571d037e571d037e571d11": "7e5733337e5733337e5733337e573311",
+ "7e571d047e571d047e571d047e571d21": "7e5744447e5744447e5744447e574421",
+ },
+ sessionType: "temporary",
+ sessionCount: 2,
+ duration: 1.6,
+ },
+ {
+ name: "400x300 as-is then 400x300 presented as 533x300",
+ tracks: [
+ {
+ name: "audio",
+ type: 'audio/mp4; codecs="mp4a.40.2"',
+ fragments: [
+ "bipbop_300_215kbps-cenc-audio-key1-init.mp4",
+ "bipbop_300_215kbps-cenc-audio-key1-1.m4s",
+ "bipbop_300_215kbps-cenc-audio-key1-2.m4s",
+ "bipbop_300_215kbps-cenc-audio-key1-3.m4s",
+ "bipbop_300_215kbps-cenc-audio-key1-4.m4s",
+ ],
+ },
+ {
+ name: "video",
+ type: 'video/mp4; codecs="avc1.4d4015"',
+ fragments: [
+ "bipbop_300_215kbps-cenc-video-key1-init.mp4",
+ "bipbop_300_215kbps-cenc-video-key1-1.m4s",
+ "bipbop_300wp_227kbps-cenc-video-key1-init.mp4",
+ "bipbop_300wp_227kbps-cenc-video-key1-2.m4s",
+ ],
+ },
+ ],
+ keys: {
+ // "keyid" : "key"
+ "7e571d037e571d037e571d037e571d11": "7e5733337e5733337e5733337e573311",
+ "7e571d047e571d047e571d047e571d21": "7e5744447e5744447e5744447e574421",
+ },
+ sessionType: "temporary",
+ sessionCount: 3,
+ duration: 1.6,
+ },
+ {
+ name: "400x225",
+ tracks: [
+ {
+ name: "audio",
+ type: 'audio/mp4; codecs="mp4a.40.2"',
+ fragments: [
+ "bipbop_225w_175kbps-cenc-audio-key1-init.mp4",
+ "bipbop_225w_175kbps-cenc-audio-key1-1.m4s",
+ "bipbop_225w_175kbps-cenc-audio-key1-2.m4s",
+ "bipbop_225w_175kbps-cenc-audio-key1-3.m4s",
+ "bipbop_225w_175kbps-cenc-audio-key1-4.m4s",
+ ],
+ },
+ {
+ name: "video",
+ type: 'video/mp4; codecs="avc1.64000d"',
+ fragments: [
+ "bipbop_225w_175kbps-cenc-video-key1-init.mp4",
+ "bipbop_225w_175kbps-cenc-video-key1-1.m4s",
+ ],
+ },
+ ],
+ keys: {
+ // "keyid" : "key"
+ "7e571d037e571d037e571d037e571d11": "7e5733337e5733337e5733337e573311",
+ "7e571d047e571d047e571d047e571d21": "7e5744447e5744447e5744447e574421",
+ },
+ sessionType: "temporary",
+ sessionCount: 2,
+ duration: 1.6,
+ },
+ {
+ name: "640x360",
+ tracks: [
+ {
+ name: "audio",
+ type: 'audio/mp4; codecs="mp4a.40.2"',
+ fragments: [
+ "bipbop_360w_253kbps-cenc-audio-key1-init.mp4",
+ "bipbop_360w_253kbps-cenc-audio-key1-1.m4s",
+ "bipbop_360w_253kbps-cenc-audio-key1-2.m4s",
+ "bipbop_360w_253kbps-cenc-audio-key1-3.m4s",
+ "bipbop_360w_253kbps-cenc-audio-key1-4.m4s",
+ ],
+ },
+ {
+ name: "video",
+ type: 'video/mp4; codecs="avc1.64001e"',
+ fragments: [
+ "bipbop_360w_253kbps-cenc-video-key1-init.mp4",
+ "bipbop_360w_253kbps-cenc-video-key1-1.m4s",
+ ],
+ },
+ ],
+ keys: {
+ // "keyid" : "key"
+ "7e571d037e571d037e571d037e571d11": "7e5733337e5733337e5733337e573311",
+ "7e571d047e571d047e571d047e571d21": "7e5744447e5744447e5744447e574421",
+ },
+ sessionType: "temporary",
+ sessionCount: 2,
+ duration: 1.6,
+ },
+ {
+ name: "400x225 then 640x360",
+ tracks: [
+ {
+ name: "audio",
+ type: 'audio/mp4; codecs="mp4a.40.2"',
+ fragments: [
+ "bipbop_225w_175kbps-cenc-audio-key1-init.mp4",
+ "bipbop_225w_175kbps-cenc-audio-key1-1.m4s",
+ "bipbop_225w_175kbps-cenc-audio-key1-2.m4s",
+ "bipbop_225w_175kbps-cenc-audio-key1-3.m4s",
+ "bipbop_225w_175kbps-cenc-audio-key1-4.m4s",
+ ],
+ },
+ {
+ name: "video",
+ type: 'video/mp4; codecs="avc1.64000d,avc1.64001e"',
+ fragments: [
+ "bipbop_225w_175kbps-cenc-video-key1-init.mp4",
+ "bipbop_225w_175kbps-cenc-video-key1-1.m4s",
+ "bipbop_360w_253kbps-cenc-video-key2-init.mp4",
+ "bipbop_360w_253kbps-cenc-video-key2-1.m4s",
+ ],
+ },
+ ],
+ keys: {
+ // "keyid" : "key"
+ "7e571d037e571d037e571d037e571d11": "7e5733337e5733337e5733337e573311",
+ "7e571d047e571d047e571d047e571d21": "7e5744447e5744447e5744447e574421",
+ "7e571d037e571d037e571d037e571d12": "7e5733337e5733337e5733337e573312",
+ },
+ sessionType: "temporary",
+ sessionCount: 3,
+ duration: 1.6,
+ },
+ {
+ name: "640x360 then 640x480",
+ tracks: [
+ {
+ name: "audio",
+ type: 'audio/mp4; codecs="mp4a.40.2"',
+ fragments: [
+ "bipbop_360w_253kbps-cenc-audio-key1-init.mp4",
+ "bipbop_360w_253kbps-cenc-audio-key1-1.m4s",
+ "bipbop_360w_253kbps-cenc-audio-key1-2.m4s",
+ "bipbop_360w_253kbps-cenc-audio-key1-3.m4s",
+ "bipbop_360w_253kbps-cenc-audio-key1-4.m4s",
+ ],
+ },
+ {
+ name: "video",
+ type: 'video/mp4; codecs="avc1.64001e,avc1.4d401e"',
+ fragments: [
+ "bipbop_360w_253kbps-cenc-video-key1-init.mp4",
+ "bipbop_360w_253kbps-cenc-video-key1-1.m4s",
+ "bipbop_480_624kbps-cenc-video-key2-init.mp4",
+ "bipbop_480_624kbps-cenc-video-key2-2.m4s",
+ ],
+ },
+ ],
+ keys: {
+ // "keyid" : "key"
+ "7e571d037e571d037e571d037e571d11": "7e5733337e5733337e5733337e573311",
+ "7e571d047e571d047e571d047e571d21": "7e5744447e5744447e5744447e574421",
+ "7e571d037e571d037e571d037e571d12": "7e5733337e5733337e5733337e573312",
+ },
+ sessionType: "temporary",
+ sessionCount: 3,
+ duration: 1.6,
+ },
+ {
+ // File generated with shaka packager:
+ // packager-osx --enable_raw_key_encryption --keys label=:key_id=7e571d047e571d047e571d047e571d21:key=7e5744447e5744447e5744447e574421 --segment_duration 1 --clear_lead 0 in=test-flac.mp4,stream=audio,output=flac-sample-cenc.mp4
+ name: "flac in mp4 clearkey",
+ tracks: [
+ {
+ name: "audio",
+ type: 'audio/mp4; codecs="flac"',
+ fragments: ["flac-sample-cenc.mp4"],
+ },
+ ],
+ keys: {
+ // "keyid" : "key"
+ "7e571d047e571d047e571d047e571d21": "7e5744447e5744447e5744447e574421",
+ },
+ sessionType: "temporary",
+ sessionCount: 1,
+ duration: 2.05,
+ },
+ {
+ // File generated with shaka packager:
+ // packager-osx --enable_raw_key_encryption --keys label=:key_id=7e571d047e571d047e571d047e571d21:key=7e5744447e5744447e5744447e574421 --segment_duration 1 --clear_lead 0 in=test-opus.mp4,stream=audio,output=opus-sample-cenc.mp4
+ name: "opus in mp4 clearkey",
+ tracks: [
+ {
+ name: "audio",
+ type: 'audio/mp4; codecs="opus"',
+ fragments: ["opus-sample-cenc.mp4"],
+ },
+ ],
+ keys: {
+ // "keyid" : "key"
+ "7e571d047e571d047e571d047e571d21": "7e5744447e5744447e5744447e574421",
+ },
+ sessionType: "temporary",
+ sessionCount: 1,
+ duration: 1.98,
+ },
+ {
+ name: "WebM vorbis audio & vp8 video clearkey",
+ tracks: [
+ {
+ name: "audio",
+ type: 'audio/webm; codecs="vorbis"',
+ fragments: ["bipbop_360w_253kbps-clearkey-audio.webm"],
+ },
+ {
+ name: "video",
+ type: 'video/webm; codecs="vp8"',
+ fragments: ["bipbop_360w_253kbps-clearkey-video-vp8.webm"],
+ },
+ ],
+ keys: {
+ // "keyid" : "key"
+ f1f3ee1790527e9de47217d43835f76a: "97b9ddc459c8d5ff23c1f2754c95abe8",
+ "8b5df745ad84145b5617c33116e35a67": "bddfd35dd9be033ee73bc18bc1885056",
+ },
+ sessionType: "temporary",
+ sessionCount: 2,
+ duration: 1.6,
+ },
+ {
+ name: "WebM vorbis audio & vp9 video clearkey",
+ tracks: [
+ {
+ name: "audio",
+ type: 'audio/webm; codecs="vorbis"',
+ fragments: ["bipbop_360w_253kbps-clearkey-audio.webm"],
+ },
+ {
+ name: "video",
+ type: 'video/webm; codecs="vp9"',
+ fragments: ["bipbop_360w_253kbps-clearkey-video-vp9.webm"],
+ },
+ ],
+ keys: {
+ // "keyid" : "key"
+ f1f3ee1790527e9de47217d43835f76a: "97b9ddc459c8d5ff23c1f2754c95abe8",
+ eedf63a94fa7c398ee094f123a4ee709: "973b679a746c82f3acdb856b30e9378e",
+ },
+ sessionType: "temporary",
+ sessionCount: 2,
+ duration: 1.6,
+ },
+ {
+ name: "WebM vorbis audio & vp9 video clearkey with subsample encryption",
+ tracks: [
+ {
+ name: "audio",
+ type: 'audio/webm; codecs="vorbis"',
+ fragments: ["sintel-short-clearkey-subsample-encrypted-audio.webm"],
+ },
+ {
+ name: "video",
+ type: 'video/webm; codecs="vp9"',
+ fragments: ["sintel-short-clearkey-subsample-encrypted-video.webm"],
+ },
+ ],
+ keys: {
+ // "keyid" : "key"
+ "2cdb0ed6119853e7850671c3e9906c3c": "808B9ADAC384DE1E4F56140F4AD76194",
+ },
+ sessionType: "temporary",
+ sessionCount: 2,
+ duration: 2.0,
+ },
+ {
+ // Files adapted from testcase for bug 1560092. See bug 1630381 for a
+ // detailed explanation on how they were adapted.
+ name: "avc3 h264 video in mp4 using clearkey cenc encryption",
+ tracks: [
+ {
+ name: "video",
+ type: 'video/mp4; codecs="avc3.640015"',
+ fragments: [
+ "big-buck-bunny-cenc-avc3-init.mp4",
+ "big-buck-bunny-cenc-avc3-1.m4s",
+ ],
+ },
+ ],
+ keys: {
+ // "keyid" : "key"
+ "10000000100010001000100000000001": "3A2A1B68DD2BD9B2EEB25E84C4776668",
+ },
+ sessionType: "temporary",
+ sessionCount: 1,
+ duration: 2.08,
+ },
+ // The following cbcs files are created using shaka-packager using commands like
+ // ./packager-win.exe 'in=bipbop_2s.mp4,stream=audio,init_segment=bipbop_cbcs_1_9_audio_init.mp4,segment_template=bipbop_cbcs_1_9_audio_$Number$.m4s' \
+ // 'in=bipbop_2s.mp4,stream=video,init_segment=bipbop_cbcs_1_9_video_init.mp4,segment_template=bipbop_cbcs_1_9_video_$Number$.m4s' --protection_scheme cbcs \
+ // --enable_raw_key_encryption --keys label=:key_id=7e571d047e571d047e571d047e571d21:key=7e5744447e5744447e5744447e574421 --iv 11223344556677889900112233445566 \
+ // --clear_lead 0 --crypt_byte_block 1 --skip_byte_block 9
+ // See bug 1726202 for more details on their creation.
+ {
+ name: "mp4 h264 + aac clearkey cbcs 1:9 pattern",
+ tracks: [
+ {
+ name: "video",
+ type: 'video/mp4; codecs="avc1.4d4015"',
+ fragments: [
+ "bipbop_cbcs_1_9_video_init.mp4",
+ "bipbop_cbcs_1_9_video_1.m4s",
+ ],
+ },
+ {
+ name: "audio",
+ type: 'audio/mp4; codecs="mp4a.40.2"',
+ fragments: [
+ "bipbop_cbcs_1_9_audio_init.mp4",
+ "bipbop_cbcs_1_9_audio_1.m4s",
+ ],
+ },
+ ],
+ keys: {
+ // "keyid" : "key"
+ "7e571d047e571d047e571d047e571d21": "7e5744447e5744447e5744447e574421",
+ },
+ sessionType: "temporary",
+ sessionCount: 2,
+ duration: 2.04,
+ },
+ {
+ name: "mp4 h264 + aac clearkey cbcs 5:5 pattern",
+ tracks: [
+ {
+ name: "video",
+ type: 'video/mp4; codecs="avc1.4d4015"',
+ fragments: [
+ "bipbop_cbcs_5_5_video_init.mp4",
+ "bipbop_cbcs_5_5_video_1.m4s",
+ ],
+ },
+ {
+ name: "audio",
+ type: 'audio/mp4; codecs="mp4a.40.2"',
+ fragments: [
+ "bipbop_cbcs_5_5_audio_init.mp4",
+ "bipbop_cbcs_5_5_audio_1.m4s",
+ ],
+ },
+ ],
+ keys: {
+ // "keyid" : "key"
+ "7e571d047e571d047e571d047e571d21": "7e5744447e5744447e5744447e574421",
+ },
+ sessionType: "temporary",
+ sessionCount: 2,
+ duration: 2.04,
+ },
+ {
+ name: "mp4 h264 + aac clearkey cbcs 10:0 pattern",
+ tracks: [
+ {
+ name: "video",
+ type: 'video/mp4; codecs="avc1.4d4015"',
+ fragments: [
+ "bipbop_cbcs_10_0_video_init.mp4",
+ "bipbop_cbcs_10_0_video_1.m4s",
+ ],
+ },
+ {
+ name: "audio",
+ type: 'audio/mp4; codecs="mp4a.40.2"',
+ fragments: [
+ "bipbop_cbcs_10_0_audio_init.mp4",
+ "bipbop_cbcs_10_0_audio_1.m4s",
+ ],
+ },
+ ],
+ keys: {
+ // "keyid" : "key"
+ "7e571d047e571d047e571d047e571d21": "7e5744447e5744447e5744447e574421",
+ },
+ sessionType: "temporary",
+ sessionCount: 2,
+ duration: 2.04,
+ },
+ {
+ name: "mp4 h264 + aac clearkey cbcs 7:7 pattern",
+ tracks: [
+ {
+ name: "video",
+ type: 'video/mp4; codecs="avc1.4d4015"',
+ fragments: [
+ "bipbop_cbcs_7_7_video_init.mp4",
+ "bipbop_cbcs_7_7_video_1.m4s",
+ ],
+ },
+ {
+ name: "audio",
+ type: 'audio/mp4; codecs="mp4a.40.2"',
+ fragments: [
+ "bipbop_cbcs_7_7_audio_init.mp4",
+ "bipbop_cbcs_7_7_audio_1.m4s",
+ ],
+ },
+ ],
+ keys: {
+ // "keyid" : "key"
+ "7e571d047e571d047e571d047e571d21": "7e5744447e5744447e5744447e574421",
+ },
+ sessionType: "temporary",
+ sessionCount: 2,
+ duration: 2.04,
+ },
+ {
+ name: "mp4 h264 + aac clearkey cbcs 9:8 pattern",
+ tracks: [
+ {
+ name: "video",
+ type: 'video/mp4; codecs="avc1.4d4015"',
+ fragments: [
+ "bipbop_cbcs_9_8_video_init.mp4",
+ "bipbop_cbcs_9_8_video_1.m4s",
+ ],
+ },
+ {
+ name: "audio",
+ type: 'audio/mp4; codecs="mp4a.40.2"',
+ fragments: [
+ "bipbop_cbcs_9_8_audio_init.mp4",
+ "bipbop_cbcs_9_8_audio_1.m4s",
+ ],
+ },
+ ],
+ keys: {
+ // "keyid" : "key"
+ "7e571d047e571d047e571d047e571d21": "7e5744447e5744447e5744447e574421",
+ },
+ sessionType: "temporary",
+ sessionCount: 2,
+ duration: 2.04,
+ },
+];
+
+var gEMENonMSEFailTests = [
+ {
+ name: "short-cenc.mp4",
+ audioType: 'audio/mp4; codecs="mp4a.40.2"',
+ videoType: 'video/mp4; codecs="avc1.64000d"',
+ duration: 0.47,
+ },
+];
+
+// Test files that are supposed to loop seamlessly when played back.
+var gSeamlessLoopingTests = [
+ // MP4 files dont't loop seamlessly yet, the seeking logic seeks to 0, not the
+ // actual first packet, resulting in incorrect decoding.
+ // See bug 1817989
+ // { name: "sin-441-1s-44100-fdk_aac.mp4", type: "audio/mp4" },
+ // { name: "sin-441-1s-44100-afconvert.mp4", type: "audio/mp4" },
+ // { name: "sin-441-1s-44100.ogg", type: "audio/vorbis" },
+ // { name: "sin-441-1s-44100.opus", type: "audio/opus" },
+ { name: "sin-441-1s-44100-lame.mp3", type: "audio/mpeg" },
+ { name: "sin-441-1s-44100.flac", type: "audio/flac" },
+];
+
+// These are files that are used for video decode suspend in
+// background tabs tests.
+var gDecodeSuspendTests = [
+ { name: "gizmo.mp4", type: "video/mp4", duration: 5.56 },
+ { name: "gizmo-noaudio.mp4", type: "video/mp4", duration: 5.56 },
+ { name: "gizmo.webm", type: 'video/webm; codecs="vp9,opus"', duration: 5.56 },
+ {
+ name: "gizmo-noaudio.webm",
+ type: 'video/webm; codecs="vp9"',
+ duration: 5.56,
+ },
+];
+
+// These are video files with hardware-decodable formats and longer
+// durations that are looped while we check telemetry for macOS video
+// low power mode.
+var gVideoLowPowerTests = [
+ { name: "seek.ogv", type: "video/ogg", duration: 3.966 },
+ { name: "gizmo.mp4", type: "video/mp4", duration: 5.56 },
+];
+
+function checkMetadata(msg, e, test) {
+ if (test.width) {
+ is(e.videoWidth, test.width, msg + " video width");
+ }
+ if (test.height) {
+ is(e.videoHeight, test.height, msg + " video height");
+ }
+ if (test.duration) {
+ ok(
+ Math.abs(e.duration - test.duration) < 0.1,
+ msg + " duration (" + e.duration + ") should be around " + test.duration
+ );
+ }
+ is(
+ !!test.keys,
+ SpecialPowers.do_lookupGetter(e, "isEncrypted").apply(e),
+ msg + " isEncrypted should be true if we have decryption keys"
+ );
+}
+
+// Returns the first test from candidates array which we can play with the
+// installed video backends.
+function getPlayableVideo(candidates) {
+ var resources = getPlayableVideos(candidates);
+ if (resources.length) {
+ return resources[0];
+ }
+ return null;
+}
+
+function getPlayableVideos(candidates) {
+ var v = manifestVideo();
+ return candidates.filter(function (x) {
+ return /^video/.test(x.type) && v.canPlayType(x.type);
+ });
+}
+
+function getPlayableAudio(candidates) {
+ var v = manifestVideo();
+ var resources = candidates.filter(function (x) {
+ return /^audio/.test(x.type) && v.canPlayType(x.type);
+ });
+ if (resources.length) {
+ return resources[0];
+ }
+ return null;
+}
+
+// Returns the type of element that should be created for the given mimetype.
+function getMajorMimeType(mimetype) {
+ if (/^video/.test(mimetype)) {
+ return "video";
+ }
+ return "audio";
+}
+
+// Force releasing decoder to avoid timeout in waiting for decoding resource.
+function removeNodeAndSource(n) {
+ n.remove();
+ // reset |srcObject| first since it takes precedence over |src|.
+ n.srcObject = null;
+ n.removeAttribute("src");
+ n.load();
+ while (n.firstChild) {
+ n.firstChild.remove();
+ }
+}
+
+function once(target, name, cb) {
+ var p = new Promise(function (resolve, reject) {
+ target.addEventListener(
+ name,
+ function () {
+ resolve();
+ },
+ { once: true }
+ );
+ });
+ if (cb) {
+ p.then(cb);
+ }
+ return p;
+}
+
+/**
+ * @param {HTMLMediaElement} video target of interest.
+ * @param {string} eventName the event to wait on.
+ * @returns {Promise} A promise that is resolved when event happens.
+ */
+function nextEvent(video, eventName) {
+ return new Promise(function (resolve, reject) {
+ let f = function (event) {
+ video.removeEventListener(eventName, f);
+ resolve(event);
+ };
+ video.addEventListener(eventName, f);
+ });
+}
+
+function TimeStamp(token) {
+ function pad(x) {
+ return x < 10 ? "0" + x : x;
+ }
+ var now = new Date();
+ var ms = now.getMilliseconds();
+ var time =
+ "[" +
+ pad(now.getHours()) +
+ ":" +
+ pad(now.getMinutes()) +
+ ":" +
+ pad(now.getSeconds()) +
+ "." +
+ ms +
+ "]" +
+ // eslint-disable-next-line no-nested-ternary
+ (ms < 10 ? " " : ms < 100 ? " " : "");
+ return token ? time + " " + token : time;
+}
+
+function Log(token, msg) {
+ info(TimeStamp(token) + " " + msg);
+}
+
+// Number of tests to run in parallel.
+var PARALLEL_TESTS = 2;
+
+// Prefs to set before running tests. Use this to improve coverage of
+// conditions that might not otherwise be encountered on the test data.
+var gTestPrefs = [
+ ["media.recorder.max_memory", 1024],
+ ["media.audio-max-decode-error", 0],
+ ["media.video-max-decode-error", 0],
+];
+
+// When true, we'll loop forever on whatever test we run. Use this to debug
+// intermittent test failures.
+const DEBUG_TEST_LOOP_FOREVER = false;
+
+// Manages a run of media tests. Runs them in chunks in order to limit
+// the number of media elements/threads running in parallel. This limits peak
+// memory use, particularly on Linux x86 where thread stacks use 10MB of
+// virtual address space.
+// Usage:
+// 1. Create a new MediaTestManager object.
+// 2. Create a test startTest function. This takes a test object and a token,
+// and performs anything necessary to start the test. The test object is an
+// element in one of the g*Tests above. Your startTest function must call
+// MediaTestManager.start(token) if it starts a test. The test object is
+// guaranteed to be playable by our supported decoders; you don't need to
+// check canPlayType.
+// 3. When your tests finishes, call MediaTestManager.finished(), passing
+// the token back to the manager. The manager may either start the next run
+// or end the mochitest if all the tests are done.
+function MediaTestManager() {
+ // Set a very large timeout to prevent Mochitest timeout.
+ // Instead MediaTestManager will manage timeout of each test.
+ SimpleTest.requestLongerTimeout(1000);
+
+ // Return how many seconds elapsed since |begin|.
+ function elapsedTime(begin) {
+ var end = new Date();
+ return (end.getTime() - begin.getTime()) / 1000;
+ }
+ // Sets up a MediaTestManager to runs through the 'tests' array, which needs
+ // to be one of, or have the same fields as, the g*Test arrays of tests. Uses
+ // the user supplied 'startTest' function to initialize the test. This
+ // function must accept two arguments, the test entry from the 'tests' array,
+ // and a token. Call MediaTestManager.started(token) if you start the test,
+ // and MediaTestManager.finished(token) when the test finishes. You don't have
+ // to start every test, but if you call started() you *must* call finish()
+ // else you'll timeout.
+ this.runTests = function (tests, startTest) {
+ this.startTime = new Date();
+ SimpleTest.info(
+ "Started " +
+ this.startTime +
+ " (" +
+ this.startTime.getTime() / 1000 +
+ "s)"
+ );
+ this.testNum = 0;
+ this.tests = tests;
+ this.startTest = startTest;
+ this.tokens = [];
+ this.isShutdown = false;
+ this.numTestsRunning = 0;
+ this.handlers = {};
+ this.timers = {};
+
+ // Always wait for explicit finish.
+ SimpleTest.waitForExplicitFinish();
+ SpecialPowers.pushPrefEnv({ set: gTestPrefs }, () => {
+ this.nextTest();
+ });
+
+ SimpleTest.registerCleanupFunction(() => {
+ if (this.tokens.length) {
+ info("Test timed out. Remaining tests=" + this.tokens);
+ }
+ for (var token of this.tokens) {
+ var handler = this.handlers[token];
+ if (handler && handler.ontimeout) {
+ handler.ontimeout();
+ }
+ }
+ });
+ };
+
+ // Registers that the test corresponding to 'token' has been started.
+ // Don't call more than once per token.
+ this.started = function (token, handler) {
+ this.tokens.push(token);
+ this.numTestsRunning++;
+ this.handlers[token] = handler;
+
+ var onTimeout = async () => {
+ ok(false, "Test timed out!");
+ info(`${token} timed out!`);
+ await dumpDebugInfoForToken(token);
+ this.finished(token);
+ };
+ // Default timeout to 180s for each test.
+ // Call SimpleTest._originalSetTimeout() to bypass the flaky timeout checker.
+ this.timers[token] = SimpleTest._originalSetTimeout.call(
+ window,
+ onTimeout,
+ 180000
+ );
+
+ is(
+ this.numTestsRunning,
+ this.tokens.length,
+ "[started " +
+ token +
+ " t=" +
+ elapsedTime(this.startTime) +
+ "] Length of array should match number of running tests"
+ );
+ };
+
+ // Registers that the test corresponding to 'token' has finished. Call when
+ // you've finished your test. If all tests are complete this will finish the
+ // run, otherwise it may start up the next run. It's ok to call multiple times
+ // per token.
+ this.finished = function (token) {
+ var i = this.tokens.indexOf(token);
+ if (i != -1) {
+ // Remove the element from the list of running tests.
+ this.tokens.splice(i, 1);
+ }
+
+ if (this.timers[token]) {
+ // Cancel the timer when the test finishes.
+ clearTimeout(this.timers[token]);
+ this.timers[token] = null;
+ }
+
+ info("[finished " + token + "] remaining= " + this.tokens);
+ this.numTestsRunning--;
+ is(
+ this.numTestsRunning,
+ this.tokens.length,
+ "[finished " +
+ token +
+ " t=" +
+ elapsedTime(this.startTime) +
+ "] Length of array should match number of running tests"
+ );
+ if (this.tokens.length < PARALLEL_TESTS) {
+ this.nextTest();
+ }
+ };
+
+ // Starts the next batch of tests, or finishes if they're all done.
+ // Don't call this directly, call finished(token) when you're done.
+ this.nextTest = function () {
+ while (
+ this.testNum < this.tests.length &&
+ this.tokens.length < PARALLEL_TESTS
+ ) {
+ var test = this.tests[this.testNum];
+ var token = (test.name ? test.name + "-" : "") + this.testNum;
+ this.testNum++;
+
+ if (DEBUG_TEST_LOOP_FOREVER && this.testNum == this.tests.length) {
+ this.testNum = 0;
+ }
+
+ // Ensure we can play the resource type.
+ if (
+ test.type &&
+ !document.createElement("video").canPlayType(test.type)
+ ) {
+ continue;
+ }
+
+ // Do the init. This should start the test.
+ this.startTest(test, token);
+ }
+
+ if (
+ this.testNum == this.tests.length &&
+ !DEBUG_TEST_LOOP_FOREVER &&
+ !this.tokens.length &&
+ !this.isShutdown
+ ) {
+ this.isShutdown = true;
+ if (this.onFinished) {
+ this.onFinished();
+ }
+ var onCleanup = () => {
+ var end = new Date();
+ SimpleTest.info(
+ "Finished at " + end + " (" + end.getTime() / 1000 + "s)"
+ );
+ SimpleTest.info("Running time: " + elapsedTime(this.startTime) + "s");
+ SimpleTest.finish();
+ };
+ mediaTestCleanup(onCleanup);
+ }
+ };
+}
+
+// Ensures we've got no active video or audio elements in the document, and
+// forces a GC to release the address space reserved by the decoders' threads'
+// stacks.
+function mediaTestCleanup(callback) {
+ var V = document.getElementsByTagName("video");
+ for (let i = 0; i < V.length; i++) {
+ removeNodeAndSource(V[i]);
+ V[i] = null;
+ }
+ var A = document.getElementsByTagName("audio");
+ for (let i = 0; i < A.length; i++) {
+ removeNodeAndSource(A[i]);
+ A[i] = null;
+ }
+ SpecialPowers.exactGC(callback);
+}
+
+async function dumpDebugInfoForToken(token) {
+ for (let v of document.getElementsByTagName("video")) {
+ if (token === v.token) {
+ info(JSON.stringify(await SpecialPowers.wrap(v).mozRequestDebugInfo()));
+ return;
+ }
+ }
+ for (let a of document.getElementsByTagName("audio")) {
+ if (token === a.token) {
+ info(JSON.stringify(await SpecialPowers.wrap(a).mozRequestDebugInfo()));
+ return;
+ }
+ }
+}
+
+// Could be undefined in a page opened by the parent test page
+// like file_access_controls.html.
+if ("SimpleTest" in window) {
+ SimpleTest.requestFlakyTimeout("untriaged");
+
+ // Register timeout function to dump debugging logs.
+ SimpleTest.registerTimeoutFunction(async function () {
+ for (const v of document.getElementsByTagName("video")) {
+ SimpleTest.info(
+ JSON.stringify(await SpecialPowers.wrap(v).mozRequestDebugInfo())
+ );
+ }
+ for (const a of document.getElementsByTagName("audio")) {
+ SimpleTest.info(
+ JSON.stringify(await SpecialPowers.wrap(a).mozRequestDebugInfo())
+ );
+ }
+ });
+}
diff --git a/dom/media/test/midflight-redirect.sjs b/dom/media/test/midflight-redirect.sjs
new file mode 100644
index 0000000000..9a2101251c
--- /dev/null
+++ b/dom/media/test/midflight-redirect.sjs
@@ -0,0 +1,87 @@
+function parseQuery(query, key) {
+ for (let p of query.split("&")) {
+ if (p == key) {
+ return true;
+ }
+ if (p.startsWith(key + "=")) {
+ return p.substring(key.length + 1);
+ }
+ }
+ return false;
+}
+
+// Return the first few bytes in a short byte range response. When Firefox
+// requests subsequent bytes in a second range request, respond with a
+// redirect. Requests after the first redirected are serviced as expected.
+function handleRequest(request, response) {
+ var query = request.queryString;
+ var resource = parseQuery(query, "resource");
+ var type = parseQuery(query, "type") || "application/octet-stream";
+ var redirected = parseQuery(query, "redirected") || false;
+ var useCors = parseQuery(query, "cors") || false;
+
+ var file = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
+ var fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ var bis = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+ Ci.nsIBinaryInputStream
+ );
+ var paths = "tests/dom/media/test/" + resource;
+ var split = paths.split("/");
+ for (var i = 0; i < split.length; ++i) {
+ file.append(split[i]);
+ }
+ fis.init(file, -1, -1, false);
+
+ bis.setInputStream(fis);
+ var bytes = bis.readBytes(bis.available());
+ let [from, to] = request
+ .getHeader("range")
+ .split("=")[1]
+ .split("-")
+ .map(s => parseInt(s));
+
+ if (!redirected && from > 0) {
+ var origin =
+ request.host == "mochi.test" ? "example.org" : "mochi.test:8888";
+ response.setStatusLine(request.httpVersion, 303, "See Other");
+ let url =
+ "http://" +
+ origin +
+ "/tests/dom/media/test/midflight-redirect.sjs?redirected&" +
+ query;
+ response.setHeader("Location", url);
+ response.setHeader("Content-Type", "text/html");
+ return;
+ }
+
+ if (isNaN(to)) {
+ to = bytes.length - 1;
+ }
+
+ if (from == 0 && !redirected) {
+ to =
+ parseInt(parseQuery(query, "redirectAt")) || Math.floor(bytes.length / 4);
+ }
+ to = Math.min(to, bytes.length - 1);
+
+ // Note: 'to' is the first index *excluded*, so we need (to + 1)
+ // in the substring end here.
+ let byterange = bytes.substring(from, to + 1);
+
+ let contentRange = "bytes " + from + "-" + to + "/" + bytes.length;
+ let contentLength = byterange.length.toString();
+
+ response.setStatusLine(request.httpVersion, 206, "Partial Content");
+ response.setHeader("Content-Range", contentRange);
+ response.setHeader("Content-Length", contentLength, false);
+ response.setHeader("Content-Type", type, false);
+ response.setHeader("Accept-Ranges", "bytes", false);
+ response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
+ if (redirected && useCors) {
+ response.setHeader("Access-Control-Allow-Origin", "*");
+ }
+ response.write(byterange, byterange.length);
+ bis.close();
+}
diff --git a/dom/media/test/mochitest.ini b/dom/media/test/mochitest.ini
new file mode 100644
index 0000000000..6de12cf7f0
--- /dev/null
+++ b/dom/media/test/mochitest.ini
@@ -0,0 +1,885 @@
+# Media tests should be backend independent, i.e., not conditioned on ogg,
+# wave etc. (The only exception is the can_play_type tests, which
+# necessarily depend on the backend(s) configured.) As far as possible, each
+# test should work with any resource type. This makes it easy to add new
+# backends and reduces the amount of test duplication.
+
+# For each supported backend, resources that can be played by that backend
+# should be added to the lists in manifest.js. Media tests that aren't
+# testing for a bug in handling a specific resource type should pick one of
+# the lists in manifest.js and run the test for each resource in the list
+# that is supported in the current build (the canPlayType API is useful for
+# this).
+
+# To test whether a valid resource can simply be played through correctly,
+# and optionally that its metadata is read correctly, just add it to
+# gPlayTests in manifest.js. To test whether an invalid resource correctly
+# throws an error (and does not cause a crash or hang), just add it to
+# gErrorTests in manifest.js.
+
+# To test for a specific bug in handling a specific resource type, make the
+# test first check canPlayType for the type, and if it's not supported, just
+# do ok(true, "Type not supported") and stop the test.
+
+[DEFAULT]
+subsuite = media
+skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1536604
+prefs =
+ gfx.core-animation.low-power-telemetry-frames=60 # only for test_video_low_power_telemetry.html
+support-files =
+ 16bit_wave_extrametadata.wav
+ 16bit_wave_extrametadata.wav^headers^
+ 320x240.ogv
+ 320x240.ogv^headers^
+ 448636.ogv
+ 448636.ogv^headers^
+ A4.ogv
+ A4.ogv^headers^
+ VID_0001.ogg
+ VID_0001.ogg^headers^
+ allowed.sjs
+ ambisonics.mp4
+ ambisonics.mp4^headers^
+ audio-gaps.ogg
+ audio-gaps.ogg^headers^
+ audio-gaps-short.ogg
+ audio-gaps-short.ogg^headers^
+ audio-overhang.ogg
+ audio-overhang.ogg^headers^
+ audio.wav
+ audio.wav^headers^
+ av1.mp4
+ av1.mp4^headers^
+ background_video.js
+ badtags.ogg
+ badtags.ogg^headers^
+ bear-640x360-v_frag-cenc-key_rotation.mp4
+ bear-640x360-a_frag-cenc-key_rotation.mp4
+ beta-phrasebook.ogg
+ beta-phrasebook.ogg^headers^
+ big.wav
+ big.wav^headers^
+ big-buck-bunny-cenc-avc3-1.m4s
+ big-buck-bunny-cenc-avc3-1.m4s^headers^
+ big-buck-bunny-cenc-avc3-init.mp4
+ big-buck-bunny-cenc-avc3-init.mp4^headers^
+ big-short.wav
+ big-short.wav^headers^
+ bipbop.mp4
+ bipbop-cenc-audio1.m4s
+ bipbop-cenc-audio1.m4s^headers^
+ bipbop-cenc-audio2.m4s
+ bipbop-cenc-audio2.m4s^headers^
+ bipbop-cenc-audio3.m4s
+ bipbop-cenc-audio3.m4s^headers^
+ bipbop-cenc-audioinit.mp4
+ bipbop-cenc-audioinit.mp4^headers^
+ bipbop-cenc-video1.m4s
+ bipbop-cenc-video1.m4s^headers^
+ bipbop-cenc-video2.m4s
+ bipbop-cenc-video2.m4s^headers^
+ bipbop-cenc-videoinit.mp4
+ bipbop-cenc-videoinit.mp4^headers^
+ bipbop-cenc-video-10s.mp4
+ bipbop-cenc-video-10s.mp4^headers^
+ bipbop-clearkey-keyrotation-clear-lead-audio.mp4
+ bipbop-clearkey-keyrotation-clear-lead-audio.mp4^headers^
+ bipbop-clearkey-keyrotation-clear-lead-video.mp4
+ bipbop-clearkey-keyrotation-clear-lead-video.mp4^headers^
+ bipbop_225w_175kbps.mp4
+ bipbop_225w_175kbps.mp4^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-1.m4s
+ bipbop_225w_175kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-2.m4s
+ bipbop_225w_175kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-3.m4s
+ bipbop_225w_175kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-4.m4s
+ bipbop_225w_175kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-init.mp4
+ bipbop_225w_175kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-1.m4s
+ bipbop_225w_175kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-2.m4s
+ bipbop_225w_175kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-3.m4s
+ bipbop_225w_175kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-4.m4s
+ bipbop_225w_175kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-init.mp4
+ bipbop_225w_175kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_225w_175kbps-cenc-video-key1-1.m4s
+ bipbop_225w_175kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_225w_175kbps-cenc-video-key1-init.mp4
+ bipbop_225w_175kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_225w_175kbps-cenc-video-key2-1.m4s
+ bipbop_225w_175kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_225w_175kbps-cenc-video-key2-init.mp4
+ bipbop_225w_175kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_300_215kbps-cenc-audio-key1-1.m4s
+ bipbop_300_215kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key1-2.m4s
+ bipbop_300_215kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key1-3.m4s
+ bipbop_300_215kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key1-4.m4s
+ bipbop_300_215kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key1-init.mp4
+ bipbop_300_215kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_300_215kbps-cenc-audio-key2-1.m4s
+ bipbop_300_215kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key2-2.m4s
+ bipbop_300_215kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key2-3.m4s
+ bipbop_300_215kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key2-4.m4s
+ bipbop_300_215kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key2-init.mp4
+ bipbop_300_215kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_300_215kbps-cenc-video-key1-1.m4s
+ bipbop_300_215kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_300_215kbps-cenc-video-key1-2.m4s
+ bipbop_300_215kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_300_215kbps-cenc-video-key1-init.mp4
+ bipbop_300_215kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_300_215kbps-cenc-video-key2-1.m4s
+ bipbop_300_215kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_300_215kbps-cenc-video-key2-2.m4s
+ bipbop_300_215kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_300_215kbps-cenc-video-key2-init.mp4
+ bipbop_300_215kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-1.m4s
+ bipbop_300wp_227kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-2.m4s
+ bipbop_300wp_227kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-3.m4s
+ bipbop_300wp_227kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-4.m4s
+ bipbop_300wp_227kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-init.mp4
+ bipbop_300wp_227kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-1.m4s
+ bipbop_300wp_227kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-2.m4s
+ bipbop_300wp_227kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-3.m4s
+ bipbop_300wp_227kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-4.m4s
+ bipbop_300wp_227kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-init.mp4
+ bipbop_300wp_227kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_300wp_227kbps-cenc-video-key1-1.m4s
+ bipbop_300wp_227kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_300wp_227kbps-cenc-video-key1-2.m4s
+ bipbop_300wp_227kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_300wp_227kbps-cenc-video-key1-init.mp4
+ bipbop_300wp_227kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_300wp_227kbps-cenc-video-key2-1.m4s
+ bipbop_300wp_227kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_300wp_227kbps-cenc-video-key2-2.m4s
+ bipbop_300wp_227kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_300wp_227kbps-cenc-video-key2-init.mp4
+ bipbop_300wp_227kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-1.m4s
+ bipbop_360w_253kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-2.m4s
+ bipbop_360w_253kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-3.m4s
+ bipbop_360w_253kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-4.m4s
+ bipbop_360w_253kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-init.mp4
+ bipbop_360w_253kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-1.m4s
+ bipbop_360w_253kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-2.m4s
+ bipbop_360w_253kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-3.m4s
+ bipbop_360w_253kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-4.m4s
+ bipbop_360w_253kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-init.mp4
+ bipbop_360w_253kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_360w_253kbps-cenc-video-key1-1.m4s
+ bipbop_360w_253kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_360w_253kbps-cenc-video-key1-init.mp4
+ bipbop_360w_253kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_360w_253kbps-cenc-video-key2-1.m4s
+ bipbop_360w_253kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_360w_253kbps-cenc-video-key2-init.mp4
+ bipbop_360w_253kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_360w_253kbps-clearkey-audio.webm
+ bipbop_360w_253kbps-clearkey-audio.webm^headers^
+ bipbop_360w_253kbps-clearkey-video-vp8.webm
+ bipbop_360w_253kbps-clearkey-video-vp8.webm^headers^
+ bipbop_360w_253kbps-clearkey-video-vp9.webm
+ bipbop_360w_253kbps-clearkey-video-vp9.webm^headers^
+ bipbop_480_624kbps-cenc-audio-key1-1.m4s
+ bipbop_480_624kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key1-2.m4s
+ bipbop_480_624kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key1-3.m4s
+ bipbop_480_624kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key1-4.m4s
+ bipbop_480_624kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key1-init.mp4
+ bipbop_480_624kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_480_624kbps-cenc-audio-key2-1.m4s
+ bipbop_480_624kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key2-2.m4s
+ bipbop_480_624kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key2-3.m4s
+ bipbop_480_624kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key2-4.m4s
+ bipbop_480_624kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key2-init.mp4
+ bipbop_480_624kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_480_624kbps-cenc-video-key1-1.m4s
+ bipbop_480_624kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_480_624kbps-cenc-video-key1-2.m4s
+ bipbop_480_624kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_480_624kbps-cenc-video-key1-init.mp4
+ bipbop_480_624kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_480_624kbps-cenc-video-key2-1.m4s
+ bipbop_480_624kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_480_624kbps-cenc-video-key2-2.m4s
+ bipbop_480_624kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_480_624kbps-cenc-video-key2-init.mp4
+ bipbop_480_624kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_480_959kbps-cenc-audio-key1-1.m4s
+ bipbop_480_959kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key1-2.m4s
+ bipbop_480_959kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key1-3.m4s
+ bipbop_480_959kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key1-4.m4s
+ bipbop_480_959kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key1-init.mp4
+ bipbop_480_959kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_480_959kbps-cenc-audio-key2-1.m4s
+ bipbop_480_959kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key2-2.m4s
+ bipbop_480_959kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key2-3.m4s
+ bipbop_480_959kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key2-4.m4s
+ bipbop_480_959kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key2-init.mp4
+ bipbop_480_959kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_480_959kbps-cenc-video-key1-1.m4s
+ bipbop_480_959kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_480_959kbps-cenc-video-key1-2.m4s
+ bipbop_480_959kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_480_959kbps-cenc-video-key1-init.mp4
+ bipbop_480_959kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_480_959kbps-cenc-video-key2-1.m4s
+ bipbop_480_959kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_480_959kbps-cenc-video-key2-2.m4s
+ bipbop_480_959kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_480_959kbps-cenc-video-key2-init.mp4
+ bipbop_480_959kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-1.m4s
+ bipbop_480wp_663kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-2.m4s
+ bipbop_480wp_663kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-3.m4s
+ bipbop_480wp_663kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-4.m4s
+ bipbop_480wp_663kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-init.mp4
+ bipbop_480wp_663kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-1.m4s
+ bipbop_480wp_663kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-2.m4s
+ bipbop_480wp_663kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-3.m4s
+ bipbop_480wp_663kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-4.m4s
+ bipbop_480wp_663kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-init.mp4
+ bipbop_480wp_663kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_480wp_663kbps-cenc-video-key1-1.m4s
+ bipbop_480wp_663kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_480wp_663kbps-cenc-video-key1-2.m4s
+ bipbop_480wp_663kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_480wp_663kbps-cenc-video-key1-init.mp4
+ bipbop_480wp_663kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_480wp_663kbps-cenc-video-key2-1.m4s
+ bipbop_480wp_663kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_480wp_663kbps-cenc-video-key2-2.m4s
+ bipbop_480wp_663kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_480wp_663kbps-cenc-video-key2-init.mp4
+ bipbop_480wp_663kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-1.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-2.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-3.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-4.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-init.mp4
+ bipbop_480wp_1001kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-1.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-2.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-3.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-4.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-init.mp4
+ bipbop_480wp_1001kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_480wp_1001kbps-cenc-video-key1-1.m4s
+ bipbop_480wp_1001kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-video-key1-2.m4s
+ bipbop_480wp_1001kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-video-key1-init.mp4
+ bipbop_480wp_1001kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_480wp_1001kbps-cenc-video-key2-1.m4s
+ bipbop_480wp_1001kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-video-key2-2.m4s
+ bipbop_480wp_1001kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-video-key2-init.mp4
+ bipbop_480wp_1001kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_audio_aac_8k.mp4
+ bipbop_audio_aac_8k.mp4^headers^
+ bipbop_audio_aac_22.05k.mp4
+ bipbop_audio_aac_22.05k.mp4^headers^
+ bipbop_audio_aac_44.1k.mp4
+ bipbop_audio_aac_44.1k.mp4^headers^
+ bipbop_audio_aac_48k.mp4
+ bipbop_audio_aac_48k.mp4^headers^
+ bipbop_audio_aac_88.2k.mp4
+ bipbop_audio_aac_88.2k.mp4^headers^
+ bipbop_audio_aac_96k.mp4
+ bipbop_audio_aac_96k.mp4^headers^
+ bipbop_cbcs_1_9_audio_1.m4s
+ bipbop_cbcs_1_9_audio_1.m4s^headers^
+ bipbop_cbcs_1_9_audio_init.mp4
+ bipbop_cbcs_1_9_audio_init.mp4^headers^
+ bipbop_cbcs_1_9_video_1.m4s
+ bipbop_cbcs_1_9_video_1.m4s^headers^
+ bipbop_cbcs_1_9_video_init.mp4
+ bipbop_cbcs_1_9_video_init.mp4^headers^
+ bipbop_cbcs_5_5_audio_1.m4s
+ bipbop_cbcs_5_5_audio_1.m4s^headers^
+ bipbop_cbcs_5_5_audio_init.mp4
+ bipbop_cbcs_5_5_audio_init.mp4^headers^
+ bipbop_cbcs_5_5_video_1.m4s
+ bipbop_cbcs_5_5_video_1.m4s^headers^
+ bipbop_cbcs_5_5_video_init.mp4
+ bipbop_cbcs_5_5_video_init.mp4^headers^
+ bipbop_cbcs_7_7_audio_1.m4s
+ bipbop_cbcs_7_7_audio_1.m4s^headers^
+ bipbop_cbcs_7_7_audio_init.mp4
+ bipbop_cbcs_7_7_audio_init.mp4^headers^
+ bipbop_cbcs_7_7_video_1.m4s
+ bipbop_cbcs_7_7_video_1.m4s^headers^
+ bipbop_cbcs_7_7_video_init.mp4
+ bipbop_cbcs_7_7_video_init.mp4^headers^
+ bipbop_cbcs_9_8_audio_1.m4s
+ bipbop_cbcs_9_8_audio_1.m4s^headers^
+ bipbop_cbcs_9_8_audio_init.mp4
+ bipbop_cbcs_9_8_audio_init.mp4^headers^
+ bipbop_cbcs_9_8_video_1.m4s
+ bipbop_cbcs_9_8_video_1.m4s^headers^
+ bipbop_cbcs_9_8_video_init.mp4
+ bipbop_cbcs_9_8_video_init.mp4^headers^
+ bipbop_cbcs_10_0_audio_1.m4s
+ bipbop_cbcs_10_0_audio_1.m4s^headers^
+ bipbop_cbcs_10_0_audio_init.mp4
+ bipbop_cbcs_10_0_audio_init.mp4^headers^
+ bipbop_cbcs_10_0_video_1.m4s
+ bipbop_cbcs_10_0_video_1.m4s^headers^
+ bipbop_cbcs_10_0_video_init.mp4
+ bipbop_cbcs_10_0_video_init.mp4^headers^
+ bipbop_short_pixel_metadata_bigger_than_in_stream_vp8.webm
+ bipbop_short_pixel_metadata_bigger_than_in_stream_vp8.webm^headers^
+ bipbop_short_pixel_metadata_narrower_than_in_stream_vp8.webm
+ bipbop_short_pixel_metadata_narrower_than_in_stream_vp8.webm^headers^
+ bipbop_short_pixel_metadata_smaller_than_in_stream_vp8.webm^headers^
+ bipbop_short_pixel_metadata_smaller_than_in_stream_vp8.webm
+ bipbop_short_vp8.webm
+ bipbop_short_vp8.webm^headers^
+ bipbop-lateaudio.mp4
+ bipbop-lateaudio.mp4^headers^
+ black100x100-aspect3to2.ogv
+ black100x100-aspect3to2.ogv^headers^
+ bogus.duh
+ bogus.ogv
+ bogus.ogv^headers^
+ bogus.wav
+ bogus.wav^headers^
+ bug461281.ogg
+ bug461281.ogg^headers^
+ bug482461-theora.ogv
+ bug482461-theora.ogv^headers^
+ bug482461.ogv
+ bug482461.ogv^headers^
+ bug495129.ogv
+ bug495129.ogv^headers^
+ bug495794.ogg
+ bug495794.ogg^headers^
+ bug498380.ogv
+ bug498380.ogv^headers^
+ bug498855-1.ogv
+ bug498855-1.ogv^headers^
+ bug498855-2.ogv
+ bug498855-2.ogv^headers^
+ bug498855-3.ogv
+ bug498855-3.ogv^headers^
+ bug499519.ogv
+ bug499519.ogv^headers^
+ bug500311.ogv
+ bug500311.ogv^headers^
+ bug501279.ogg
+ bug501279.ogg^headers^
+ bug504613.ogv
+ bug504613.ogv^headers^
+ bug504644.ogv
+ bug504644.ogv^headers^
+ bug504843.ogv
+ bug504843.ogv^headers^
+ bug506094.ogv
+ bug506094.ogv^headers^
+ bug516323.indexed.ogv
+ bug516323.indexed.ogv^headers^
+ bug516323.ogv
+ bug516323.ogv^headers^
+ bug520493.ogg
+ bug520493.ogg^headers^
+ bug520500.ogg
+ bug520500.ogg^headers^
+ bug520908.ogv
+ bug520908.ogv^headers^
+ bug523816.ogv
+ bug523816.ogv^headers^
+ bug533822.ogg
+ bug533822.ogg^headers^
+ bug556821.ogv
+ bug556821.ogv^headers^
+ bug557094.ogv
+ bug557094.ogv^headers^
+ bug603918.webm
+ bug603918.webm^headers^
+ bug604067.webm
+ bug604067.webm^headers^
+ bug1066943.webm
+ bug1066943.webm^headers^
+ bug1301226.wav
+ bug1301226.wav^headers^
+ bug1301226-odd.wav
+ bug1301226-odd.wav^headers^
+ bug1377278.webm
+ bug1377278.webm^headers^
+ bug1535980.webm
+ bug1535980.webm^headers^
+ bunny.webm
+ can_play_type_dash.js
+ can_play_type_ogg.js
+ can_play_type_wave.js
+ can_play_type_webm.js
+ cancellable_request.sjs
+ chain.ogg
+ chain.ogg^headers^
+ chain.ogv
+ chain.ogv^headers^
+ chain.opus
+ chain.opus^headers^
+ chained-audio-video.ogg
+ chained-audio-video.ogg^headers^
+ chained-video.ogv
+ chained-video.ogv^headers^
+ chromeHelper.js
+ cloneElementVisually_helpers.js
+ contentType.sjs
+ detodos.opus
+ detodos.opus^headers^
+ detodos.webm
+ detodos.webm^headers^
+ detodos-short.webm
+ detodos-short.webm^headers^
+ detodos-recorder-test.opus
+ detodos-recorder-test.opus^headers^
+ detodos-short.opus
+ detodos-short.opus^headers^
+ dirac.ogg
+ dirac.ogg^headers^
+ dynamic_resource.sjs
+ eme_standalone.js
+ eme.js
+ empty_size.mp3
+ file_access_controls.html
+ file_eme_createMediaKeys.html
+ flac-s24.flac
+ flac-s24.flac^headers^
+ flac-noheader-s16.flac
+ flac-noheader-s16.flac^headers^
+ flac-sample.mp4
+ flac-sample.mp4^headers^
+ flac-sample-cenc.mp4
+ flac-sample-cenc.mp4^headers^
+ fragment_noplay.js
+ fragment_play.js
+ gizmo.mp4
+ gizmo.mp4^headers^
+ gizmo-noaudio.mp4
+ gizmo-noaudio.mp4^headers^
+ gizmo-short.mp4
+ gizmo-short.mp4^headers^
+ gizmo.webm
+ gizmo.webm^headers^
+ gizmo-noaudio.webm
+ gizmo-noaudio.webm^headers^
+ gUM_support.js
+ gzipped_mp4.sjs
+ huge-id3.mp3
+ huge-id3.mp3^headers^
+ id3tags.mp3
+ id3tags.mp3^headers^
+ id3v1afterlongid3v2.mp3
+ invalid-cmap-s0c0.opus
+ invalid-cmap-s0c0.opus^headers^
+ invalid-cmap-s0c2.opus
+ invalid-cmap-s0c2.opus^headers^
+ invalid-cmap-s1c2.opus
+ invalid-cmap-s1c2.opus^headers^
+ invalid-cmap-short.opus
+ invalid-cmap-short.opus^headers^
+ invalid-discard_on_multi_blocks.webm
+ invalid-discard_on_multi_blocks.webm^headers^
+ invalid-excess_discard.webm
+ invalid-excess_discard.webm^headers^
+ invalid-excess_neg_discard.webm
+ invalid-excess_neg_discard.webm^headers^
+ invalid-m0c0.opus
+ invalid-m0c0.opus^headers^
+ invalid-m0c3.opus
+ invalid-m0c3.opus^headers^
+ invalid-m1c0.opus
+ invalid-m1c0.opus^headers^
+ invalid-m1c9.opus
+ invalid-m1c9.opus^headers^
+ invalid-m2c0.opus
+ invalid-m2c0.opus^headers^
+ invalid-m2c1.opus
+ invalid-m2c1.opus^headers^
+ invalid-neg_discard.webm
+ invalid-neg_discard.webm^headers^
+ invalid-preskip.webm
+ invalid-preskip.webm^headers^
+ manifest.js
+ midflight-redirect.sjs
+ multiple-bos.ogg
+ multiple-bos.ogg^headers^
+ multiple-bos-more-header-fileds.ogg
+ multiple-bos-more-header-fileds.ogg^headers^
+ multi_id3v2.mp3
+ no-container-codec-delay.webm
+ no-cues.webm
+ no-cues.webm^headers^
+ notags.mp3
+ notags.mp3^headers^
+ opus-mapping2.mp4
+ opus-mapping2.mp4^headers^
+ opus-mapping2.webm
+ opus-mapping2.webm^headers^
+ opus-sample.mp4
+ opus-sample.mp4^headers^
+ opus-sample-cenc.mp4
+ opus-sample-cenc.mp4^headers^
+ owl-funnier-id3.mp3
+ owl-funnier-id3.mp3^headers^
+ owl-funny-id3.mp3
+ owl-funny-id3.mp3^headers^
+ owl.mp3
+ owl.mp3^headers^
+ owl-short.mp3
+ owl-short.mp3^headers^
+ pixel_aspect_ratio.mp4
+ play_promise.js
+ poster-test.jpg
+ r11025_msadpcm_c1.wav
+ r11025_msadpcm_c1.wav^headers^
+ r11025_s16_c1.wav
+ r11025_s16_c1.wav^headers^
+ r11025_s16_c1_trailing.wav
+ r11025_s16_c1_trailing.wav^headers^
+ r11025_s16_c1-short.wav
+ r11025_s16_c1-short.wav^headers^
+ r11025_u8_c1.wav
+ r11025_u8_c1.wav^headers^
+ r11025_u8_c1_trunc.wav
+ r11025_u8_c1_trunc.wav^headers^
+ r16000_u8_c1_list.wav
+ r16000_u8_c1_list.wav^headers^
+ reactivate_helper.html
+ red-46x48.mp4
+ red-46x48.mp4^headers^
+ red-48x46.mp4
+ red-48x46.mp4^headers^
+ redirect.sjs
+ referer.sjs
+ resolution-change.webm
+ resolution-change.webm^headers^
+ sample.3gp
+ sample.3g2
+ sample-encrypted-sgpdstbl-sbgptraf.mp4
+ sample-encrypted-sgpdstbl-sbgptraf.mp4^headers^
+ sample-fisbone-skeleton4.ogv
+ sample-fisbone-skeleton4.ogv^headers^
+ sample-fisbone-wrong-header.ogv
+ sample-fisbone-wrong-header.ogv^headers^
+ seek.ogv
+ seek.ogv^headers^
+ seek-short.ogv
+ seek-short.ogv^headers^
+ seek.webm
+ seek.webm^headers^
+ seek-short.webm
+ seek-short.webm^headers^
+ seek_support.js
+ seekLies.sjs
+ seek_with_sound.ogg^headers^
+ short-aac-encrypted-audio.mp4
+ short-aac-encrypted-audio.mp4^headers^
+ short-audio-fragmented-cenc-without-pssh.mp4
+ short-audio-fragmented-cenc-without-pssh.mp4^headers^
+ short-cenc.mp4
+ short-video.ogv
+ short.mp4
+ short.mp4.gz
+ short.mp4^headers^
+ # source file generated with:
+ # > sox -V -r 44100 -n -b 16 -c 1 sin-441-1s-44100.wav synth 1 sin 441 vol -5dB
+ # then encoded:
+ # ffmpeg -i sin-441-1s-44100.wav sin-441-1s-44100-libfdk_aac.mp4
+ sin-441-1s-44100-fdk_aac.mp4
+ # afconvert -s 3 -f mp4f -d aac sin-4411-1s-441100.wav
+ sin-441-1s-44100-afconvert.mp4
+ # ffmpeg -i sin-441-1s-44100.wav sin-441-1s-44100-libfdk_lame.mp3
+ sin-441-1s-44100-lame.mp3
+ # ffmpeg -i sin-441-1s-44100.wav sin-441-1s-44100.ogg
+ sin-441-1s-44100.ogg
+ # ffmpeg -i sin-441-1s-44100.wav sin-441-1s-44100.opus
+ sin-441-1s-44100.opus
+ # ffmpeg -i sin-441-1s-44100.wav sin-441-1s-44100.flac
+ sin-441-1s-44100.flac
+ sine.webm
+ sine.webm^headers^
+ sintel-short-clearkey-subsample-encrypted-audio.webm
+ sintel-short-clearkey-subsample-encrypted-audio.webm^headers^
+ sintel-short-clearkey-subsample-encrypted-video.webm
+ sintel-short-clearkey-subsample-encrypted-video.webm^headers^
+ short-video.ogv^headers^
+ short-vp9-encrypted-video.mp4
+ short-vp9-encrypted-video.mp4^headers^
+ small-shot-mp3.mp4
+ small-shot-mp3.mp4^headers^
+ small-shot.m4a
+ small-shot.mp3
+ small-shot.mp3^headers^
+ small-shot.ogg
+ small-shot.ogg^headers^
+ small-shot.flac
+ sound.ogg
+ sound.ogg^headers^
+ spacestorm-1000Hz-100ms.ogg
+ spacestorm-1000Hz-100ms.ogg^headers^
+ split.webm
+ split.webm^headers^
+ street.mp4
+ street.mp4^headers^
+ test-1-mono.opus
+ test-1-mono.opus^headers^
+ test-2-stereo.opus
+ test-2-stereo.opus^headers^
+ test-3-LCR.opus
+ test-3-LCR.opus^headers^
+ test-4-quad.opus
+ test-4-quad.opus^headers^
+ test-5-5.0.opus
+ test-5-5.0.opus^headers^
+ test-6-5.1.opus
+ test-6-5.1.opus^headers^
+ test-7-6.1.opus
+ test-7-6.1.opus^headers^
+ test-8-7.1.opus
+ test-8-7.1.opus^headers^
+ test-stereo-phase-inversion-180.opus
+ test-stereo-phase-inversion-180.opus^headers^
+ variable-channel.ogg
+ variable-channel.ogg^headers^
+ variable-channel.opus
+ variable-channel.opus^headers^
+ variable-preskip.opus
+ variable-preskip.opus^headers^
+ variable-samplerate.ogg
+ variable-samplerate.ogg^headers^
+ variable-samplerate.opus
+ variable-samplerate.opus^headers^
+ vbr-head.mp3
+ vbr-head.mp3^headers^
+ vbr.mp3
+ vbr.mp3^headers^
+ very-short.mp3
+ video-overhang.ogg
+ video-overhang.ogg^headers^
+ vp9-superframes.webm
+ vp9-superframes.webm^headers^
+ vp9.webm
+ vp9.webm^headers^
+ vp9-short.webm
+ vp9-short.webm^headers^
+ vp9cake.webm
+ vp9cake.webm^headers^
+ vp9cake-short.webm
+ vp9cake-short.webm^headers^
+ wave_metadata.wav
+ wave_metadata.wav^headers^
+ wave_metadata_bad_len.wav
+ wave_metadata_bad_len.wav^headers^
+ wave_metadata_bad_no_null.wav
+ wave_metadata_bad_no_null.wav^headers^
+ wave_metadata_bad_utf8.wav
+ wave_metadata_bad_utf8.wav^headers^
+ wave_metadata_unknown_tag.wav
+ wave_metadata_unknown_tag.wav^headers^
+ wave_metadata_utf8.wav
+ wave_metadata_utf8.wav^headers^
+ wavedata_alaw.wav
+ wavedata_alaw.wav^headers^
+ wavedata_float.wav
+ wavedata_float.wav^headers^
+ wavedata_s24.wav
+ wavedata_s24.wav^headers^
+ wavedata_s16.wav
+ wavedata_s16.wav^headers^
+ wavedata_u8.wav
+ wavedata_u8.wav^headers^
+ wavedata_ulaw.wav
+ wavedata_ulaw.wav^headers^
+ white-short.webm
+ !/dom/canvas/test/captureStream_common.js
+ !/dom/html/test/reflect.js
+ !/dom/media/webrtc/tests/mochitests/head.js
+ hls/bipbop_16x9_single.m3u8
+ hls/bipbop_4x3_single.m3u8
+ hls/bipbop_4x3_variant.m3u8
+ hls/400x300_prog_index.m3u8
+ hls/400x300_prog_index_5s.m3u8
+ hls/416x243_prog_index_5s.m3u8
+ hls/640x480_prog_index.m3u8
+ hls/960x720_prog_index.m3u8
+ hls/400x300_seg0.ts
+ hls/400x300_seg0_5s.ts
+ hls/400x300_seg1.ts
+ hls/416x243_seg0_5s.ts
+ hls/640x480_seg0.ts
+ hls/640x480_seg1.ts
+ hls/960x720_seg0.ts
+ hls/960x720_seg1.ts
+ sync.webm
+ two-xing-header-no-content-length.mp3
+ two-xing-header-no-content-length.mp3^headers^
+ single-xing-header-no-content-length.mp3
+ single-xing-header-no-content-length.mp3^headers^
+ padding-spanning-multiple-packets.mp3
+
+[test_capture_stream_av_sync.html]
+skip-if =
+ toolkit == 'android' # 1712598 (canvas error)
+ (os == 'mac') # 1517199 (timeout-on-osx)
+ (os == "linux") # 1713397, 1719881 (high intermittent failure on linux tsan), 1776937
+ apple_silicon # Disabled due to bleedover with other tests when run in regular suites; passes in "failures" jobs
+[test_chaining.html]
+[test_clone_media_element.html]
+skip-if = toolkit == 'android' # bug 1108558, android(bug 1232305)
+[test_fastSeek.html]
+[test_fastSeek-forwards.html]
+[test_imagecapture.html]
+scheme=https
+[test_invalid_reject_play.html]
+[test_media_selection.html]
+[test_media_sniffer.html]
+[test_mediastream_as_eventarget.html]
+[test_mediatrack_consuming_mediastream.html]
+scheme=https
+tags=mtg
+[test_mediatrack_replay_from_end.html]
+[test_midflight_redirect_blocked.html]
+[test_mixed_principals.html]
+skip-if =
+ toolkit == 'android' # bug 1309814, android(bug 1232305)
+ apple_silicon # bug 1707737
+[test_multiple_mediastreamtracks.html]
+scheme=https
+[test_play_events_2.html]
+[test_playback.html]
+skip-if = toolkit == 'android' # bug 1316177
+[test_playback_and_bfcache.html]
+support-files = file_playback_and_bfcache.html
+[test_playback_rate.html]
+[test_played.html]
+skip-if =
+ toolkit == 'android' && is_emulator # Times out on android-em, Bug 1613946
+[test_replay_metadata.html]
+[test_reset_events_async.html]
+[test_video_dimensions.html]
+[test_resolution_change.html]
+tags=capturestream
+[test_resume.html]
+skip-if = true # bug 1021673
+[test_seamless_looping.html]
+[test_seamless_looping_duration.html]
+[test_seamless_looping_cancel_looping_future_frames.html]
+[test_seamless_looping_media_element_state.html]
+[test_seamless_looping_resume_video_decoding.html]
+[test_seamless_looping_seek_current_time.html]
+[test_seamless_looping_video.html]
+skip-if = toolkit == 'android' # Android has black frames issue.
+[test_video_to_canvas.html]
+skip-if = toolkit == 'android' # android(bug 1232305), bugs 1320418,1347953,1347954,1348140,1348386
+[test_video_low_power_telemetry.html]
+skip-if = (os != 'mac') # This telemetry is macOS-specific.
+[test_vp9_superframes.html]
+[test_temporary_file_blob_video_plays.html]
+skip-if = toolkit == 'android'
+ (os == 'win' && processor == 'aarch64') # bug 1533534 # android(bug 1232305)
+[test_videoPlaybackQuality_totalFrames.html]
+skip-if = os == 'win'
+ (os == 'mac' && os_version == '10.14') # bug 1374189, mac due to bug 1544938
+
+[test_playback_hls.html]
+# HLS is only supported on Fennec with API level >= 16
+# TODO: This test is similar to test_playback.html, will remove the
+# redundant code once test_playback.html is enabled on Fennec.
+skip-if = toolkit != 'android'
+tags = hls
+
+[test_hls_player_independency.html]
+# There's a limit for creating decoder when API lever < 18(Bug 1278574)
+# We could skip the test in that case as we cannot play 2 video at a time.
+skip-if = toolkit != 'android'
+ android_version < '18'
+tags = hls
+
+[test_cloneElementVisually_paused.html]
+tags = cloneelementvisually
+[test_cloneElementVisually_mediastream.html]
+skip-if = (os == 'win' && os_version == '6.1') # Frequent failures on Win7
+tags = cloneelementvisually
+[test_cloneElementVisually_mediastream_multitrack.html]
+tags = cloneelementvisually
+[test_cloneElementVisually_resource_change.html]
+skip-if =
+ os == "linux" && !(debug || asan || tsan) # Bug 1559308 - lower frequency intermittent
+tags = cloneelementvisually
+[test_cloneElementVisually_no_suspend.html]
+tags = cloneelementvisually
+[test_cloneElementVisually_poster.html]
+tags = cloneelementvisually
+[test_cloneElementVisually_ended_video.html]
+tags = cloneelementvisually
+[test_mp3_broadcast.html]
diff --git a/dom/media/test/mochitest_background_video.ini b/dom/media/test/mochitest_background_video.ini
new file mode 100644
index 0000000000..bc65f63726
--- /dev/null
+++ b/dom/media/test/mochitest_background_video.ini
@@ -0,0 +1,787 @@
+# Media tests should be backend independent, i.e., not conditioned on ogg,
+# wave etc. (The only exception is the can_play_type tests, which
+# necessarily depend on the backend(s) configured.) As far as possible, each
+# test should work with any resource type. This makes it easy to add new
+# backends and reduces the amount of test duplication.
+
+# For each supported backend, resources that can be played by that backend
+# should be added to the lists in manifest.js. Media tests that aren't
+# testing for a bug in handling a specific resource type should pick one of
+# the lists in manifest.js and run the test for each resource in the list
+# that is supported in the current build (the canPlayType API is useful for
+# this).
+
+# To test whether a valid resource can simply be played through correctly,
+# and optionally that its metadata is read correctly, just add it to
+# gPlayTests in manifest.js. To test whether an invalid resource correctly
+# throws an error (and does not cause a crash or hang), just add it to
+# gErrorTests in manifest.js.
+
+# To test for a specific bug in handling a specific resource type, make the
+# test first check canPlayType for the type, and if it's not supported, just
+# do ok(true, "Type not supported") and stop the test.
+
+[DEFAULT]
+subsuite = media
+tags = suspend
+skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1536604
+support-files =
+ 16bit_wave_extrametadata.wav
+ 16bit_wave_extrametadata.wav^headers^
+ 320x240.ogv
+ 320x240.ogv^headers^
+ 448636.ogv
+ 448636.ogv^headers^
+ A4.ogv
+ A4.ogv^headers^
+ VID_0001.ogg
+ VID_0001.ogg^headers^
+ allowed.sjs
+ ambisonics.mp4
+ ambisonics.mp4^headers^
+ audio-gaps.ogg
+ audio-gaps.ogg^headers^
+ audio-gaps-short.ogg
+ audio-gaps-short.ogg^headers^
+ audio-overhang.ogg
+ audio-overhang.ogg^headers^
+ audio.wav
+ audio.wav^headers^
+ av1.mp4
+ av1.mp4^headers^
+ background_video.js
+ badtags.ogg
+ badtags.ogg^headers^
+ bear-640x360-v_frag-cenc-key_rotation.mp4
+ bear-640x360-a_frag-cenc-key_rotation.mp4
+ beta-phrasebook.ogg
+ beta-phrasebook.ogg^headers^
+ big.wav
+ big.wav^headers^
+ big-buck-bunny-cenc-avc3-1.m4s
+ big-buck-bunny-cenc-avc3-1.m4s^headers^
+ big-buck-bunny-cenc-avc3-init.mp4
+ big-buck-bunny-cenc-avc3-init.mp4^headers^
+ big-short.wav
+ big-short.wav^headers^
+ bipbop.mp4
+ bipbop-cenc-audio1.m4s
+ bipbop-cenc-audio1.m4s^headers^
+ bipbop-cenc-audio2.m4s
+ bipbop-cenc-audio2.m4s^headers^
+ bipbop-cenc-audio3.m4s
+ bipbop-cenc-audio3.m4s^headers^
+ bipbop-cenc-audioinit.mp4
+ bipbop-cenc-audioinit.mp4^headers^
+ bipbop-cenc-video1.m4s
+ bipbop-cenc-video1.m4s^headers^
+ bipbop-cenc-video2.m4s
+ bipbop-cenc-video2.m4s^headers^
+ bipbop-cenc-videoinit.mp4
+ bipbop-cenc-videoinit.mp4^headers^
+ bipbop-cenc-video-10s.mp4
+ bipbop-cenc-video-10s.mp4^headers^
+ bipbop-clearkey-keyrotation-clear-lead-audio.mp4
+ bipbop-clearkey-keyrotation-clear-lead-audio.mp4^headers^
+ bipbop-clearkey-keyrotation-clear-lead-video.mp4
+ bipbop-clearkey-keyrotation-clear-lead-video.mp4^headers^
+ bipbop_225w_175kbps.mp4
+ bipbop_225w_175kbps.mp4^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-1.m4s
+ bipbop_225w_175kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-2.m4s
+ bipbop_225w_175kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-3.m4s
+ bipbop_225w_175kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-4.m4s
+ bipbop_225w_175kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-init.mp4
+ bipbop_225w_175kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-1.m4s
+ bipbop_225w_175kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-2.m4s
+ bipbop_225w_175kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-3.m4s
+ bipbop_225w_175kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-4.m4s
+ bipbop_225w_175kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-init.mp4
+ bipbop_225w_175kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_225w_175kbps-cenc-video-key1-1.m4s
+ bipbop_225w_175kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_225w_175kbps-cenc-video-key1-init.mp4
+ bipbop_225w_175kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_225w_175kbps-cenc-video-key2-1.m4s
+ bipbop_225w_175kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_225w_175kbps-cenc-video-key2-init.mp4
+ bipbop_225w_175kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_300_215kbps-cenc-audio-key1-1.m4s
+ bipbop_300_215kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key1-2.m4s
+ bipbop_300_215kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key1-3.m4s
+ bipbop_300_215kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key1-4.m4s
+ bipbop_300_215kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key1-init.mp4
+ bipbop_300_215kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_300_215kbps-cenc-audio-key2-1.m4s
+ bipbop_300_215kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key2-2.m4s
+ bipbop_300_215kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key2-3.m4s
+ bipbop_300_215kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key2-4.m4s
+ bipbop_300_215kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key2-init.mp4
+ bipbop_300_215kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_300_215kbps-cenc-video-key1-1.m4s
+ bipbop_300_215kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_300_215kbps-cenc-video-key1-2.m4s
+ bipbop_300_215kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_300_215kbps-cenc-video-key1-init.mp4
+ bipbop_300_215kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_300_215kbps-cenc-video-key2-1.m4s
+ bipbop_300_215kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_300_215kbps-cenc-video-key2-2.m4s
+ bipbop_300_215kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_300_215kbps-cenc-video-key2-init.mp4
+ bipbop_300_215kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-1.m4s
+ bipbop_300wp_227kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-2.m4s
+ bipbop_300wp_227kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-3.m4s
+ bipbop_300wp_227kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-4.m4s
+ bipbop_300wp_227kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-init.mp4
+ bipbop_300wp_227kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-1.m4s
+ bipbop_300wp_227kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-2.m4s
+ bipbop_300wp_227kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-3.m4s
+ bipbop_300wp_227kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-4.m4s
+ bipbop_300wp_227kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-init.mp4
+ bipbop_300wp_227kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_300wp_227kbps-cenc-video-key1-1.m4s
+ bipbop_300wp_227kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_300wp_227kbps-cenc-video-key1-2.m4s
+ bipbop_300wp_227kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_300wp_227kbps-cenc-video-key1-init.mp4
+ bipbop_300wp_227kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_300wp_227kbps-cenc-video-key2-1.m4s
+ bipbop_300wp_227kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_300wp_227kbps-cenc-video-key2-2.m4s
+ bipbop_300wp_227kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_300wp_227kbps-cenc-video-key2-init.mp4
+ bipbop_300wp_227kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-1.m4s
+ bipbop_360w_253kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-2.m4s
+ bipbop_360w_253kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-3.m4s
+ bipbop_360w_253kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-4.m4s
+ bipbop_360w_253kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-init.mp4
+ bipbop_360w_253kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-1.m4s
+ bipbop_360w_253kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-2.m4s
+ bipbop_360w_253kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-3.m4s
+ bipbop_360w_253kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-4.m4s
+ bipbop_360w_253kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-init.mp4
+ bipbop_360w_253kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_360w_253kbps-cenc-video-key1-1.m4s
+ bipbop_360w_253kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_360w_253kbps-cenc-video-key1-init.mp4
+ bipbop_360w_253kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_360w_253kbps-cenc-video-key2-1.m4s
+ bipbop_360w_253kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_360w_253kbps-cenc-video-key2-init.mp4
+ bipbop_360w_253kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_360w_253kbps-clearkey-audio.webm
+ bipbop_360w_253kbps-clearkey-audio.webm^headers^
+ bipbop_360w_253kbps-clearkey-video-vp8.webm
+ bipbop_360w_253kbps-clearkey-video-vp8.webm^headers^
+ bipbop_360w_253kbps-clearkey-video-vp9.webm
+ bipbop_360w_253kbps-clearkey-video-vp9.webm^headers^
+ bipbop_480_624kbps-cenc-audio-key1-1.m4s
+ bipbop_480_624kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key1-2.m4s
+ bipbop_480_624kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key1-3.m4s
+ bipbop_480_624kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key1-4.m4s
+ bipbop_480_624kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key1-init.mp4
+ bipbop_480_624kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_480_624kbps-cenc-audio-key2-1.m4s
+ bipbop_480_624kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key2-2.m4s
+ bipbop_480_624kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key2-3.m4s
+ bipbop_480_624kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key2-4.m4s
+ bipbop_480_624kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key2-init.mp4
+ bipbop_480_624kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_480_624kbps-cenc-video-key1-1.m4s
+ bipbop_480_624kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_480_624kbps-cenc-video-key1-2.m4s
+ bipbop_480_624kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_480_624kbps-cenc-video-key1-init.mp4
+ bipbop_480_624kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_480_624kbps-cenc-video-key2-1.m4s
+ bipbop_480_624kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_480_624kbps-cenc-video-key2-2.m4s
+ bipbop_480_624kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_480_624kbps-cenc-video-key2-init.mp4
+ bipbop_480_624kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_480_959kbps-cenc-audio-key1-1.m4s
+ bipbop_480_959kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key1-2.m4s
+ bipbop_480_959kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key1-3.m4s
+ bipbop_480_959kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key1-4.m4s
+ bipbop_480_959kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key1-init.mp4
+ bipbop_480_959kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_480_959kbps-cenc-audio-key2-1.m4s
+ bipbop_480_959kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key2-2.m4s
+ bipbop_480_959kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key2-3.m4s
+ bipbop_480_959kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key2-4.m4s
+ bipbop_480_959kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key2-init.mp4
+ bipbop_480_959kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_480_959kbps-cenc-video-key1-1.m4s
+ bipbop_480_959kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_480_959kbps-cenc-video-key1-2.m4s
+ bipbop_480_959kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_480_959kbps-cenc-video-key1-init.mp4
+ bipbop_480_959kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_480_959kbps-cenc-video-key2-1.m4s
+ bipbop_480_959kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_480_959kbps-cenc-video-key2-2.m4s
+ bipbop_480_959kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_480_959kbps-cenc-video-key2-init.mp4
+ bipbop_480_959kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-1.m4s
+ bipbop_480wp_663kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-2.m4s
+ bipbop_480wp_663kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-3.m4s
+ bipbop_480wp_663kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-4.m4s
+ bipbop_480wp_663kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-init.mp4
+ bipbop_480wp_663kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-1.m4s
+ bipbop_480wp_663kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-2.m4s
+ bipbop_480wp_663kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-3.m4s
+ bipbop_480wp_663kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-4.m4s
+ bipbop_480wp_663kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-init.mp4
+ bipbop_480wp_663kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_480wp_663kbps-cenc-video-key1-1.m4s
+ bipbop_480wp_663kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_480wp_663kbps-cenc-video-key1-2.m4s
+ bipbop_480wp_663kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_480wp_663kbps-cenc-video-key1-init.mp4
+ bipbop_480wp_663kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_480wp_663kbps-cenc-video-key2-1.m4s
+ bipbop_480wp_663kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_480wp_663kbps-cenc-video-key2-2.m4s
+ bipbop_480wp_663kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_480wp_663kbps-cenc-video-key2-init.mp4
+ bipbop_480wp_663kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-1.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-2.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-3.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-4.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-init.mp4
+ bipbop_480wp_1001kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-1.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-2.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-3.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-4.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-init.mp4
+ bipbop_480wp_1001kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_480wp_1001kbps-cenc-video-key1-1.m4s
+ bipbop_480wp_1001kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-video-key1-2.m4s
+ bipbop_480wp_1001kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-video-key1-init.mp4
+ bipbop_480wp_1001kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_480wp_1001kbps-cenc-video-key2-1.m4s
+ bipbop_480wp_1001kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-video-key2-2.m4s
+ bipbop_480wp_1001kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-video-key2-init.mp4
+ bipbop_480wp_1001kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_audio_aac_8k.mp4
+ bipbop_audio_aac_8k.mp4^headers^
+ bipbop_audio_aac_22.05k.mp4
+ bipbop_audio_aac_22.05k.mp4^headers^
+ bipbop_audio_aac_44.1k.mp4
+ bipbop_audio_aac_44.1k.mp4^headers^
+ bipbop_audio_aac_48k.mp4
+ bipbop_audio_aac_48k.mp4^headers^
+ bipbop_audio_aac_88.2k.mp4
+ bipbop_audio_aac_88.2k.mp4^headers^
+ bipbop_audio_aac_96k.mp4
+ bipbop_audio_aac_96k.mp4^headers^
+ bipbop_cbcs_1_9_audio_1.m4s
+ bipbop_cbcs_1_9_audio_1.m4s^headers^
+ bipbop_cbcs_1_9_audio_init.mp4
+ bipbop_cbcs_1_9_audio_init.mp4^headers^
+ bipbop_cbcs_1_9_video_1.m4s
+ bipbop_cbcs_1_9_video_1.m4s^headers^
+ bipbop_cbcs_1_9_video_init.mp4
+ bipbop_cbcs_1_9_video_init.mp4^headers^
+ bipbop_cbcs_5_5_audio_1.m4s
+ bipbop_cbcs_5_5_audio_1.m4s^headers^
+ bipbop_cbcs_5_5_audio_init.mp4
+ bipbop_cbcs_5_5_audio_init.mp4^headers^
+ bipbop_cbcs_5_5_video_1.m4s
+ bipbop_cbcs_5_5_video_1.m4s^headers^
+ bipbop_cbcs_5_5_video_init.mp4
+ bipbop_cbcs_5_5_video_init.mp4^headers^
+ bipbop_cbcs_7_7_audio_1.m4s
+ bipbop_cbcs_7_7_audio_1.m4s^headers^
+ bipbop_cbcs_7_7_audio_init.mp4
+ bipbop_cbcs_7_7_audio_init.mp4^headers^
+ bipbop_cbcs_7_7_video_1.m4s
+ bipbop_cbcs_7_7_video_1.m4s^headers^
+ bipbop_cbcs_7_7_video_init.mp4
+ bipbop_cbcs_7_7_video_init.mp4^headers^
+ bipbop_cbcs_9_8_audio_1.m4s
+ bipbop_cbcs_9_8_audio_1.m4s^headers^
+ bipbop_cbcs_9_8_audio_init.mp4
+ bipbop_cbcs_9_8_audio_init.mp4^headers^
+ bipbop_cbcs_9_8_video_1.m4s
+ bipbop_cbcs_9_8_video_1.m4s^headers^
+ bipbop_cbcs_9_8_video_init.mp4
+ bipbop_cbcs_9_8_video_init.mp4^headers^
+ bipbop_cbcs_10_0_audio_1.m4s
+ bipbop_cbcs_10_0_audio_1.m4s^headers^
+ bipbop_cbcs_10_0_audio_init.mp4
+ bipbop_cbcs_10_0_audio_init.mp4^headers^
+ bipbop_cbcs_10_0_video_1.m4s
+ bipbop_cbcs_10_0_video_1.m4s^headers^
+ bipbop_cbcs_10_0_video_init.mp4
+ bipbop_cbcs_10_0_video_init.mp4^headers^
+ bipbop_short_pixel_metadata_bigger_than_in_stream_vp8.webm
+ bipbop_short_pixel_metadata_bigger_than_in_stream_vp8.webm^headers^
+ bipbop_short_pixel_metadata_narrower_than_in_stream_vp8.webm
+ bipbop_short_pixel_metadata_narrower_than_in_stream_vp8.webm^headers^
+ bipbop_short_pixel_metadata_smaller_than_in_stream_vp8.webm^headers^
+ bipbop_short_pixel_metadata_smaller_than_in_stream_vp8.webm
+ bipbop_short_vp8.webm
+ bipbop_short_vp8.webm^headers^
+ bipbop-lateaudio.mp4
+ bipbop-lateaudio.mp4^headers^
+ black100x100-aspect3to2.ogv
+ black100x100-aspect3to2.ogv^headers^
+ bogus.duh
+ bogus.ogv
+ bogus.ogv^headers^
+ bogus.wav
+ bogus.wav^headers^
+ bug461281.ogg
+ bug461281.ogg^headers^
+ bug482461-theora.ogv
+ bug482461-theora.ogv^headers^
+ bug482461.ogv
+ bug482461.ogv^headers^
+ bug495129.ogv
+ bug495129.ogv^headers^
+ bug495794.ogg
+ bug495794.ogg^headers^
+ bug498380.ogv
+ bug498380.ogv^headers^
+ bug498855-1.ogv
+ bug498855-1.ogv^headers^
+ bug498855-2.ogv
+ bug498855-2.ogv^headers^
+ bug498855-3.ogv
+ bug498855-3.ogv^headers^
+ bug499519.ogv
+ bug499519.ogv^headers^
+ bug500311.ogv
+ bug500311.ogv^headers^
+ bug501279.ogg
+ bug501279.ogg^headers^
+ bug504613.ogv
+ bug504613.ogv^headers^
+ bug504644.ogv
+ bug504644.ogv^headers^
+ bug504843.ogv
+ bug504843.ogv^headers^
+ bug506094.ogv
+ bug506094.ogv^headers^
+ bug516323.indexed.ogv
+ bug516323.indexed.ogv^headers^
+ bug516323.ogv
+ bug516323.ogv^headers^
+ bug520493.ogg
+ bug520493.ogg^headers^
+ bug520500.ogg
+ bug520500.ogg^headers^
+ bug520908.ogv
+ bug520908.ogv^headers^
+ bug523816.ogv
+ bug523816.ogv^headers^
+ bug533822.ogg
+ bug533822.ogg^headers^
+ bug556821.ogv
+ bug556821.ogv^headers^
+ bug557094.ogv
+ bug557094.ogv^headers^
+ bug603918.webm
+ bug603918.webm^headers^
+ bug604067.webm
+ bug604067.webm^headers^
+ bug1066943.webm
+ bug1066943.webm^headers^
+ bug1301226.wav
+ bug1301226.wav^headers^
+ bug1301226-odd.wav
+ bug1301226-odd.wav^headers^
+ bug1377278.webm
+ bug1377278.webm^headers^
+ bunny.webm
+ can_play_type_dash.js
+ can_play_type_ogg.js
+ can_play_type_wave.js
+ can_play_type_webm.js
+ cancellable_request.sjs
+ chain.ogg
+ chain.ogg^headers^
+ chain.ogv
+ chain.ogv^headers^
+ chain.opus
+ chain.opus^headers^
+ chained-audio-video.ogg
+ chained-audio-video.ogg^headers^
+ chained-video.ogv
+ chained-video.ogv^headers^
+ chromeHelper.js
+ cloneElementVisually_helpers.js
+ contentType.sjs
+ detodos.opus
+ detodos.opus^headers^
+ detodos.webm
+ detodos.webm^headers^
+ detodos-short.webm
+ detodos-short.webm^headers^
+ detodos-recorder-test.opus
+ detodos-recorder-test.opus^headers^
+ detodos-short.opus
+ detodos-short.opus^headers^
+ dirac.ogg
+ dirac.ogg^headers^
+ dynamic_resource.sjs
+ eme_standalone.js
+ eme.js
+ empty_size.mp3
+ file_access_controls.html
+ file_eme_createMediaKeys.html
+ flac-s24.flac
+ flac-s24.flac^headers^
+ flac-noheader-s16.flac
+ flac-noheader-s16.flac^headers^
+ flac-sample.mp4
+ flac-sample.mp4^headers^
+ flac-sample-cenc.mp4
+ flac-sample-cenc.mp4^headers^
+ fragment_noplay.js
+ fragment_play.js
+ gizmo.mp4
+ gizmo.mp4^headers^
+ gizmo-noaudio.mp4
+ gizmo-noaudio.mp4^headers^
+ gizmo-short.mp4
+ gizmo-short.mp4^headers^
+ gizmo.webm
+ gizmo.webm^headers^
+ gizmo-noaudio.webm
+ gizmo-noaudio.webm^headers^
+ gUM_support.js
+ gzipped_mp4.sjs
+ huge-id3.mp3
+ huge-id3.mp3^headers^
+ id3tags.mp3
+ id3tags.mp3^headers^
+ invalid-cmap-s0c0.opus
+ invalid-cmap-s0c0.opus^headers^
+ invalid-cmap-s0c2.opus
+ invalid-cmap-s0c2.opus^headers^
+ invalid-cmap-s1c2.opus
+ invalid-cmap-s1c2.opus^headers^
+ invalid-cmap-short.opus
+ invalid-cmap-short.opus^headers^
+ invalid-discard_on_multi_blocks.webm
+ invalid-discard_on_multi_blocks.webm^headers^
+ invalid-excess_discard.webm
+ invalid-excess_discard.webm^headers^
+ invalid-excess_neg_discard.webm
+ invalid-excess_neg_discard.webm^headers^
+ invalid-m0c0.opus
+ invalid-m0c0.opus^headers^
+ invalid-m0c3.opus
+ invalid-m0c3.opus^headers^
+ invalid-m1c0.opus
+ invalid-m1c0.opus^headers^
+ invalid-m1c9.opus
+ invalid-m1c9.opus^headers^
+ invalid-m2c0.opus
+ invalid-m2c0.opus^headers^
+ invalid-m2c1.opus
+ invalid-m2c1.opus^headers^
+ invalid-neg_discard.webm
+ invalid-neg_discard.webm^headers^
+ invalid-preskip.webm
+ invalid-preskip.webm^headers^
+ manifest.js
+ midflight-redirect.sjs
+ multiple-bos.ogg
+ multiple-bos.ogg^headers^
+ multiple-bos-more-header-fileds.ogg
+ multiple-bos-more-header-fileds.ogg^headers^
+ multi_id3v2.mp3
+ no-container-codec-delay.webm
+ no-cues.webm
+ no-cues.webm^headers^
+ notags.mp3
+ notags.mp3^headers^
+ opus-mapping2.mp4
+ opus-mapping2.mp4^headers^
+ opus-mapping2.webm
+ opus-mapping2.webm^headers^
+ opus-sample.mp4
+ opus-sample.mp4^headers^
+ opus-sample-cenc.mp4
+ opus-sample-cenc.mp4^headers^
+ owl-funnier-id3.mp3
+ owl-funnier-id3.mp3^headers^
+ owl-funny-id3.mp3
+ owl-funny-id3.mp3^headers^
+ owl.mp3
+ owl.mp3^headers^
+ owl-short.mp3
+ owl-short.mp3^headers^
+ pixel_aspect_ratio.mp4
+ play_promise.js
+ poster-test.jpg
+ r11025_msadpcm_c1.wav
+ r11025_msadpcm_c1.wav^headers^
+ r11025_s16_c1.wav
+ r11025_s16_c1.wav^headers^
+ r11025_s16_c1_trailing.wav
+ r11025_s16_c1_trailing.wav^headers^
+ r11025_s16_c1-short.wav
+ r11025_s16_c1-short.wav^headers^
+ r11025_u8_c1.wav
+ r11025_u8_c1.wav^headers^
+ r11025_u8_c1_trunc.wav
+ r11025_u8_c1_trunc.wav^headers^
+ r16000_u8_c1_list.wav
+ r16000_u8_c1_list.wav^headers^
+ reactivate_helper.html
+ red-46x48.mp4
+ red-46x48.mp4^headers^
+ red-48x46.mp4
+ red-48x46.mp4^headers^
+ redirect.sjs
+ referer.sjs
+ resolution-change.webm
+ resolution-change.webm^headers^
+ sample.3gp
+ sample.3g2
+ sample-encrypted-sgpdstbl-sbgptraf.mp4
+ sample-encrypted-sgpdstbl-sbgptraf.mp4^headers^
+ sample-fisbone-skeleton4.ogv
+ sample-fisbone-skeleton4.ogv^headers^
+ sample-fisbone-wrong-header.ogv
+ sample-fisbone-wrong-header.ogv^headers^
+ seek.ogv
+ seek.ogv^headers^
+ seek-short.ogv
+ seek-short.ogv^headers^
+ seek.webm
+ seek.webm^headers^
+ seek-short.webm
+ seek-short.webm^headers^
+ seek_support.js
+ seekLies.sjs
+ seek_with_sound.ogg^headers^
+ short-cenc.mp4
+ sine.webm
+ sine.webm^headers^
+ sintel-short-clearkey-subsample-encrypted-audio.webm
+ sintel-short-clearkey-subsample-encrypted-audio.webm^headers^
+ sintel-short-clearkey-subsample-encrypted-video.webm
+ sintel-short-clearkey-subsample-encrypted-video.webm^headers^
+ short.mp4
+ short.mp4.gz
+ short.mp4^headers^
+ short-aac-encrypted-audio.mp4
+ short-aac-encrypted-audio.mp4^headers^
+ short-audio-fragmented-cenc-without-pssh.mp4
+ short-audio-fragmented-cenc-without-pssh.mp4^headers^
+ short-video.ogv
+ short-video.ogv^headers^
+ short-vp9-encrypted-video.mp4
+ short-vp9-encrypted-video.mp4^headers^
+ small-shot-mp3.mp4
+ small-shot-mp3.mp4^headers^
+ small-shot.m4a
+ small-shot.mp3
+ small-shot.mp3^headers^
+ small-shot.ogg
+ small-shot.ogg^headers^
+ small-shot.flac
+ sound.ogg
+ sound.ogg^headers^
+ spacestorm-1000Hz-100ms.ogg
+ spacestorm-1000Hz-100ms.ogg^headers^
+ split.webm
+ split.webm^headers^
+ street.mp4
+ street.mp4^headers^
+ test-1-mono.opus
+ test-1-mono.opus^headers^
+ test-2-stereo.opus
+ test-2-stereo.opus^headers^
+ test-3-LCR.opus
+ test-3-LCR.opus^headers^
+ test-4-quad.opus
+ test-4-quad.opus^headers^
+ test-5-5.0.opus
+ test-5-5.0.opus^headers^
+ test-6-5.1.opus
+ test-6-5.1.opus^headers^
+ test-7-6.1.opus
+ test-7-6.1.opus^headers^
+ test-8-7.1.opus
+ test-8-7.1.opus^headers^
+ test-stereo-phase-inversion-180.opus
+ test-stereo-phase-inversion-180.opus^headers^
+ variable-channel.ogg
+ variable-channel.ogg^headers^
+ variable-channel.opus
+ variable-channel.opus^headers^
+ variable-preskip.opus
+ variable-preskip.opus^headers^
+ variable-samplerate.ogg
+ variable-samplerate.ogg^headers^
+ variable-samplerate.opus
+ variable-samplerate.opus^headers^
+ vbr-head.mp3
+ vbr-head.mp3^headers^
+ vbr.mp3
+ vbr.mp3^headers^
+ very-short.mp3
+ video-overhang.ogg
+ video-overhang.ogg^headers^
+ vp9-superframes.webm
+ vp9-superframes.webm^headers^
+ vp9.webm
+ vp9.webm^headers^
+ vp9-short.webm
+ vp9-short.webm^headers^
+ vp9cake.webm
+ vp9cake.webm^headers^
+ vp9cake-short.webm
+ vp9cake-short.webm^headers^
+ wave_metadata.wav
+ wave_metadata.wav^headers^
+ wave_metadata_bad_len.wav
+ wave_metadata_bad_len.wav^headers^
+ wave_metadata_bad_no_null.wav
+ wave_metadata_bad_no_null.wav^headers^
+ wave_metadata_bad_utf8.wav
+ wave_metadata_bad_utf8.wav^headers^
+ wave_metadata_unknown_tag.wav
+ wave_metadata_unknown_tag.wav^headers^
+ wave_metadata_utf8.wav
+ wave_metadata_utf8.wav^headers^
+ wavedata_alaw.wav
+ wavedata_alaw.wav^headers^
+ wavedata_float.wav
+ wavedata_float.wav^headers^
+ wavedata_s24.wav
+ wavedata_s24.wav^headers^
+ wavedata_s16.wav
+ wavedata_s16.wav^headers^
+ wavedata_u8.wav
+ wavedata_u8.wav^headers^
+ wavedata_ulaw.wav
+ wavedata_ulaw.wav^headers^
+ !/dom/canvas/test/captureStream_common.js
+ !/dom/html/test/reflect.js
+ !/dom/media/webrtc/tests/mochitests/head.js
+ hls/bipbop_16x9_single.m3u8
+ hls/bipbop_4x3_single.m3u8
+ hls/bipbop_4x3_variant.m3u8
+ hls/400x300_prog_index.m3u8
+ hls/400x300_prog_index_5s.m3u8
+ hls/416x243_prog_index_5s.m3u8
+ hls/640x480_prog_index.m3u8
+ hls/960x720_prog_index.m3u8
+ hls/400x300_seg0.ts
+ hls/400x300_seg0_5s.ts
+ hls/400x300_seg1.ts
+ hls/416x243_seg0_5s.ts
+ hls/640x480_seg0.ts
+ hls/640x480_seg1.ts
+ hls/960x720_seg0.ts
+ hls/960x720_seg1.ts
+ sync.webm
+
+[test_background_video_cancel_suspend_taint.html]
+skip-if =
+ toolkit == 'android' # bug 1346705
+ os == "mac" && debug # Bug 1670131 - after splitting manifest perma fail
+[test_background_video_cancel_suspend_visible.html]
+[test_background_video_no_suspend_disabled.html]
+[test_background_video_no_suspend_short_vid.html]
+[test_background_video_no_suspend_not_in_tree.html]
+[test_background_video_resume_after_end_show_last_frame.html]
+skip-if = toolkit == 'android' # bug 1346705
+[test_background_video_resume_looping_video_without_audio.html]
+[test_background_video_suspend.html]
+skip-if = os == 'android' #Bug 1304480
+[test_background_video_suspend_ends.html]
+[test_background_video_suspend_ready_state.html]
+[test_background_video_tainted_by_capturestream.html]
+[test_background_video_tainted_by_createimagebitmap.html]
+[test_background_video_tainted_by_drawimage.html]
+skip-if = toolkit == 'android' # bug 1346705
+[test_background_video_drawimage_with_suspended_video.html]
+skip-if = toolkit == 'android' # bug 1346705
+[test_background_video_ended_event.html]
+skip-if = toolkit == 'android' # bug 1346705
diff --git a/dom/media/test/mochitest_bugs.ini b/dom/media/test/mochitest_bugs.ini
new file mode 100644
index 0000000000..662c2deb84
--- /dev/null
+++ b/dom/media/test/mochitest_bugs.ini
@@ -0,0 +1,795 @@
+# Media tests should be backend independent, i.e., not conditioned on ogg,
+# wave etc. (The only exception is the can_play_type tests, which
+# necessarily depend on the backend(s) configured.) As far as possible, each
+# test should work with any resource type. This makes it easy to add new
+# backends and reduces the amount of test duplication.
+
+# For each supported backend, resources that can be played by that backend
+# should be added to the lists in manifest.js. Media tests that aren't
+# testing for a bug in handling a specific resource type should pick one of
+# the lists in manifest.js and run the test for each resource in the list
+# that is supported in the current build (the canPlayType API is useful for
+# this).
+
+# To test whether a valid resource can simply be played through correctly,
+# and optionally that its metadata is read correctly, just add it to
+# gPlayTests in manifest.js. To test whether an invalid resource correctly
+# throws an error (and does not cause a crash or hang), just add it to
+# gErrorTests in manifest.js.
+
+# To test for a specific bug in handling a specific resource type, make the
+# test first check canPlayType for the type, and if it's not supported, just
+# do ok(true, "Type not supported") and stop the test.
+
+[DEFAULT]
+subsuite = media
+skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1536604
+support-files =
+ 16bit_wave_extrametadata.wav
+ 16bit_wave_extrametadata.wav^headers^
+ 320x240.ogv
+ 320x240.ogv^headers^
+ 448636.ogv
+ 448636.ogv^headers^
+ A4.ogv
+ A4.ogv^headers^
+ VID_0001.ogg
+ VID_0001.ogg^headers^
+ allowed.sjs
+ ambisonics.mp4
+ ambisonics.mp4^headers^
+ audio-gaps.ogg
+ audio-gaps.ogg^headers^
+ audio-gaps-short.ogg
+ audio-gaps-short.ogg^headers^
+ audio-overhang.ogg
+ audio-overhang.ogg^headers^
+ audio.wav
+ audio.wav^headers^
+ av1.mp4
+ av1.mp4^headers^
+ background_video.js
+ badtags.ogg
+ badtags.ogg^headers^
+ bear-640x360-v_frag-cenc-key_rotation.mp4
+ bear-640x360-a_frag-cenc-key_rotation.mp4
+ beta-phrasebook.ogg
+ beta-phrasebook.ogg^headers^
+ big.wav
+ big.wav^headers^
+ big-buck-bunny-cenc-avc3-1.m4s
+ big-buck-bunny-cenc-avc3-1.m4s^headers^
+ big-buck-bunny-cenc-avc3-init.mp4
+ big-buck-bunny-cenc-avc3-init.mp4^headers^
+ big-short.wav
+ big-short.wav^headers^
+ bipbop.mp4
+ bipbop-cenc-audio1.m4s
+ bipbop-cenc-audio1.m4s^headers^
+ bipbop-cenc-audio2.m4s
+ bipbop-cenc-audio2.m4s^headers^
+ bipbop-cenc-audio3.m4s
+ bipbop-cenc-audio3.m4s^headers^
+ bipbop-cenc-audioinit.mp4
+ bipbop-cenc-audioinit.mp4^headers^
+ bipbop-cenc-video1.m4s
+ bipbop-cenc-video1.m4s^headers^
+ bipbop-cenc-video2.m4s
+ bipbop-cenc-video2.m4s^headers^
+ bipbop-cenc-videoinit.mp4
+ bipbop-cenc-videoinit.mp4^headers^
+ bipbop-cenc-video-10s.mp4
+ bipbop-cenc-video-10s.mp4^headers^
+ bipbop-clearkey-keyrotation-clear-lead-audio.mp4
+ bipbop-clearkey-keyrotation-clear-lead-audio.mp4^headers^
+ bipbop-clearkey-keyrotation-clear-lead-video.mp4
+ bipbop-clearkey-keyrotation-clear-lead-video.mp4^headers^
+ bipbop_225w_175kbps.mp4
+ bipbop_225w_175kbps.mp4^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-1.m4s
+ bipbop_225w_175kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-2.m4s
+ bipbop_225w_175kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-3.m4s
+ bipbop_225w_175kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-4.m4s
+ bipbop_225w_175kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-init.mp4
+ bipbop_225w_175kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-1.m4s
+ bipbop_225w_175kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-2.m4s
+ bipbop_225w_175kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-3.m4s
+ bipbop_225w_175kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-4.m4s
+ bipbop_225w_175kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-init.mp4
+ bipbop_225w_175kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_225w_175kbps-cenc-video-key1-1.m4s
+ bipbop_225w_175kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_225w_175kbps-cenc-video-key1-init.mp4
+ bipbop_225w_175kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_225w_175kbps-cenc-video-key2-1.m4s
+ bipbop_225w_175kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_225w_175kbps-cenc-video-key2-init.mp4
+ bipbop_225w_175kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_300_215kbps-cenc-audio-key1-1.m4s
+ bipbop_300_215kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key1-2.m4s
+ bipbop_300_215kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key1-3.m4s
+ bipbop_300_215kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key1-4.m4s
+ bipbop_300_215kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key1-init.mp4
+ bipbop_300_215kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_300_215kbps-cenc-audio-key2-1.m4s
+ bipbop_300_215kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key2-2.m4s
+ bipbop_300_215kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key2-3.m4s
+ bipbop_300_215kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key2-4.m4s
+ bipbop_300_215kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key2-init.mp4
+ bipbop_300_215kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_300_215kbps-cenc-video-key1-1.m4s
+ bipbop_300_215kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_300_215kbps-cenc-video-key1-2.m4s
+ bipbop_300_215kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_300_215kbps-cenc-video-key1-init.mp4
+ bipbop_300_215kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_300_215kbps-cenc-video-key2-1.m4s
+ bipbop_300_215kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_300_215kbps-cenc-video-key2-2.m4s
+ bipbop_300_215kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_300_215kbps-cenc-video-key2-init.mp4
+ bipbop_300_215kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-1.m4s
+ bipbop_300wp_227kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-2.m4s
+ bipbop_300wp_227kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-3.m4s
+ bipbop_300wp_227kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-4.m4s
+ bipbop_300wp_227kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-init.mp4
+ bipbop_300wp_227kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-1.m4s
+ bipbop_300wp_227kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-2.m4s
+ bipbop_300wp_227kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-3.m4s
+ bipbop_300wp_227kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-4.m4s
+ bipbop_300wp_227kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-init.mp4
+ bipbop_300wp_227kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_300wp_227kbps-cenc-video-key1-1.m4s
+ bipbop_300wp_227kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_300wp_227kbps-cenc-video-key1-2.m4s
+ bipbop_300wp_227kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_300wp_227kbps-cenc-video-key1-init.mp4
+ bipbop_300wp_227kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_300wp_227kbps-cenc-video-key2-1.m4s
+ bipbop_300wp_227kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_300wp_227kbps-cenc-video-key2-2.m4s
+ bipbop_300wp_227kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_300wp_227kbps-cenc-video-key2-init.mp4
+ bipbop_300wp_227kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-1.m4s
+ bipbop_360w_253kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-2.m4s
+ bipbop_360w_253kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-3.m4s
+ bipbop_360w_253kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-4.m4s
+ bipbop_360w_253kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-init.mp4
+ bipbop_360w_253kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-1.m4s
+ bipbop_360w_253kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-2.m4s
+ bipbop_360w_253kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-3.m4s
+ bipbop_360w_253kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-4.m4s
+ bipbop_360w_253kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-init.mp4
+ bipbop_360w_253kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_360w_253kbps-cenc-video-key1-1.m4s
+ bipbop_360w_253kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_360w_253kbps-cenc-video-key1-init.mp4
+ bipbop_360w_253kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_360w_253kbps-cenc-video-key2-1.m4s
+ bipbop_360w_253kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_360w_253kbps-cenc-video-key2-init.mp4
+ bipbop_360w_253kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_360w_253kbps-clearkey-audio.webm
+ bipbop_360w_253kbps-clearkey-audio.webm^headers^
+ bipbop_360w_253kbps-clearkey-video-vp8.webm
+ bipbop_360w_253kbps-clearkey-video-vp8.webm^headers^
+ bipbop_360w_253kbps-clearkey-video-vp9.webm
+ bipbop_360w_253kbps-clearkey-video-vp9.webm^headers^
+ bipbop_480_624kbps-cenc-audio-key1-1.m4s
+ bipbop_480_624kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key1-2.m4s
+ bipbop_480_624kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key1-3.m4s
+ bipbop_480_624kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key1-4.m4s
+ bipbop_480_624kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key1-init.mp4
+ bipbop_480_624kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_480_624kbps-cenc-audio-key2-1.m4s
+ bipbop_480_624kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key2-2.m4s
+ bipbop_480_624kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key2-3.m4s
+ bipbop_480_624kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key2-4.m4s
+ bipbop_480_624kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key2-init.mp4
+ bipbop_480_624kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_480_624kbps-cenc-video-key1-1.m4s
+ bipbop_480_624kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_480_624kbps-cenc-video-key1-2.m4s
+ bipbop_480_624kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_480_624kbps-cenc-video-key1-init.mp4
+ bipbop_480_624kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_480_624kbps-cenc-video-key2-1.m4s
+ bipbop_480_624kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_480_624kbps-cenc-video-key2-2.m4s
+ bipbop_480_624kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_480_624kbps-cenc-video-key2-init.mp4
+ bipbop_480_624kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_480_959kbps-cenc-audio-key1-1.m4s
+ bipbop_480_959kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key1-2.m4s
+ bipbop_480_959kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key1-3.m4s
+ bipbop_480_959kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key1-4.m4s
+ bipbop_480_959kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key1-init.mp4
+ bipbop_480_959kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_480_959kbps-cenc-audio-key2-1.m4s
+ bipbop_480_959kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key2-2.m4s
+ bipbop_480_959kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key2-3.m4s
+ bipbop_480_959kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key2-4.m4s
+ bipbop_480_959kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key2-init.mp4
+ bipbop_480_959kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_480_959kbps-cenc-video-key1-1.m4s
+ bipbop_480_959kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_480_959kbps-cenc-video-key1-2.m4s
+ bipbop_480_959kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_480_959kbps-cenc-video-key1-init.mp4
+ bipbop_480_959kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_480_959kbps-cenc-video-key2-1.m4s
+ bipbop_480_959kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_480_959kbps-cenc-video-key2-2.m4s
+ bipbop_480_959kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_480_959kbps-cenc-video-key2-init.mp4
+ bipbop_480_959kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-1.m4s
+ bipbop_480wp_663kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-2.m4s
+ bipbop_480wp_663kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-3.m4s
+ bipbop_480wp_663kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-4.m4s
+ bipbop_480wp_663kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-init.mp4
+ bipbop_480wp_663kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-1.m4s
+ bipbop_480wp_663kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-2.m4s
+ bipbop_480wp_663kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-3.m4s
+ bipbop_480wp_663kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-4.m4s
+ bipbop_480wp_663kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-init.mp4
+ bipbop_480wp_663kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_480wp_663kbps-cenc-video-key1-1.m4s
+ bipbop_480wp_663kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_480wp_663kbps-cenc-video-key1-2.m4s
+ bipbop_480wp_663kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_480wp_663kbps-cenc-video-key1-init.mp4
+ bipbop_480wp_663kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_480wp_663kbps-cenc-video-key2-1.m4s
+ bipbop_480wp_663kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_480wp_663kbps-cenc-video-key2-2.m4s
+ bipbop_480wp_663kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_480wp_663kbps-cenc-video-key2-init.mp4
+ bipbop_480wp_663kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-1.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-2.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-3.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-4.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-init.mp4
+ bipbop_480wp_1001kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-1.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-2.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-3.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-4.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-init.mp4
+ bipbop_480wp_1001kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_480wp_1001kbps-cenc-video-key1-1.m4s
+ bipbop_480wp_1001kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-video-key1-2.m4s
+ bipbop_480wp_1001kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-video-key1-init.mp4
+ bipbop_480wp_1001kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_480wp_1001kbps-cenc-video-key2-1.m4s
+ bipbop_480wp_1001kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-video-key2-2.m4s
+ bipbop_480wp_1001kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-video-key2-init.mp4
+ bipbop_480wp_1001kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_audio_aac_8k.mp4
+ bipbop_audio_aac_8k.mp4^headers^
+ bipbop_audio_aac_22.05k.mp4
+ bipbop_audio_aac_22.05k.mp4^headers^
+ bipbop_audio_aac_44.1k.mp4
+ bipbop_audio_aac_44.1k.mp4^headers^
+ bipbop_audio_aac_48k.mp4
+ bipbop_audio_aac_48k.mp4^headers^
+ bipbop_audio_aac_88.2k.mp4
+ bipbop_audio_aac_88.2k.mp4^headers^
+ bipbop_audio_aac_96k.mp4
+ bipbop_audio_aac_96k.mp4^headers^
+ bipbop_cbcs_1_9_audio_1.m4s
+ bipbop_cbcs_1_9_audio_1.m4s^headers^
+ bipbop_cbcs_1_9_audio_init.mp4
+ bipbop_cbcs_1_9_audio_init.mp4^headers^
+ bipbop_cbcs_1_9_video_1.m4s
+ bipbop_cbcs_1_9_video_1.m4s^headers^
+ bipbop_cbcs_1_9_video_init.mp4
+ bipbop_cbcs_1_9_video_init.mp4^headers^
+ bipbop_cbcs_5_5_audio_1.m4s
+ bipbop_cbcs_5_5_audio_1.m4s^headers^
+ bipbop_cbcs_5_5_audio_init.mp4
+ bipbop_cbcs_5_5_audio_init.mp4^headers^
+ bipbop_cbcs_5_5_video_1.m4s
+ bipbop_cbcs_5_5_video_1.m4s^headers^
+ bipbop_cbcs_5_5_video_init.mp4
+ bipbop_cbcs_5_5_video_init.mp4^headers^
+ bipbop_cbcs_7_7_audio_1.m4s
+ bipbop_cbcs_7_7_audio_1.m4s^headers^
+ bipbop_cbcs_7_7_audio_init.mp4
+ bipbop_cbcs_7_7_audio_init.mp4^headers^
+ bipbop_cbcs_7_7_video_1.m4s
+ bipbop_cbcs_7_7_video_1.m4s^headers^
+ bipbop_cbcs_7_7_video_init.mp4
+ bipbop_cbcs_7_7_video_init.mp4^headers^
+ bipbop_cbcs_9_8_audio_1.m4s
+ bipbop_cbcs_9_8_audio_1.m4s^headers^
+ bipbop_cbcs_9_8_audio_init.mp4
+ bipbop_cbcs_9_8_audio_init.mp4^headers^
+ bipbop_cbcs_9_8_video_1.m4s
+ bipbop_cbcs_9_8_video_1.m4s^headers^
+ bipbop_cbcs_9_8_video_init.mp4
+ bipbop_cbcs_9_8_video_init.mp4^headers^
+ bipbop_cbcs_10_0_audio_1.m4s
+ bipbop_cbcs_10_0_audio_1.m4s^headers^
+ bipbop_cbcs_10_0_audio_init.mp4
+ bipbop_cbcs_10_0_audio_init.mp4^headers^
+ bipbop_cbcs_10_0_video_1.m4s
+ bipbop_cbcs_10_0_video_1.m4s^headers^
+ bipbop_cbcs_10_0_video_init.mp4
+ bipbop_cbcs_10_0_video_init.mp4^headers^
+ bipbop_short_pixel_metadata_bigger_than_in_stream_vp8.webm
+ bipbop_short_pixel_metadata_bigger_than_in_stream_vp8.webm^headers^
+ bipbop_short_pixel_metadata_narrower_than_in_stream_vp8.webm
+ bipbop_short_pixel_metadata_narrower_than_in_stream_vp8.webm^headers^
+ bipbop_short_pixel_metadata_smaller_than_in_stream_vp8.webm^headers^
+ bipbop_short_pixel_metadata_smaller_than_in_stream_vp8.webm
+ bipbop_short_vp8.webm
+ bipbop_short_vp8.webm^headers^
+ bipbop-lateaudio.mp4
+ bipbop-lateaudio.mp4^headers^
+ black100x100-aspect3to2.ogv
+ black100x100-aspect3to2.ogv^headers^
+ bogus.duh
+ bogus.ogv
+ bogus.ogv^headers^
+ bogus.wav
+ bogus.wav^headers^
+ bug461281.ogg
+ bug461281.ogg^headers^
+ bug482461-theora.ogv
+ bug482461-theora.ogv^headers^
+ bug482461.ogv
+ bug482461.ogv^headers^
+ bug495129.ogv
+ bug495129.ogv^headers^
+ bug495794.ogg
+ bug495794.ogg^headers^
+ bug498380.ogv
+ bug498380.ogv^headers^
+ bug498855-1.ogv
+ bug498855-1.ogv^headers^
+ bug498855-2.ogv
+ bug498855-2.ogv^headers^
+ bug498855-3.ogv
+ bug498855-3.ogv^headers^
+ bug499519.ogv
+ bug499519.ogv^headers^
+ bug500311.ogv
+ bug500311.ogv^headers^
+ bug501279.ogg
+ bug501279.ogg^headers^
+ bug504613.ogv
+ bug504613.ogv^headers^
+ bug504644.ogv
+ bug504644.ogv^headers^
+ bug504843.ogv
+ bug504843.ogv^headers^
+ bug506094.ogv
+ bug506094.ogv^headers^
+ bug516323.indexed.ogv
+ bug516323.indexed.ogv^headers^
+ bug516323.ogv
+ bug516323.ogv^headers^
+ bug520493.ogg
+ bug520493.ogg^headers^
+ bug520500.ogg
+ bug520500.ogg^headers^
+ bug520908.ogv
+ bug520908.ogv^headers^
+ bug523816.ogv
+ bug523816.ogv^headers^
+ bug533822.ogg
+ bug533822.ogg^headers^
+ bug556821.ogv
+ bug556821.ogv^headers^
+ bug557094.ogv
+ bug557094.ogv^headers^
+ bug603918.webm
+ bug603918.webm^headers^
+ bug604067.webm
+ bug604067.webm^headers^
+ bug1066943.webm
+ bug1066943.webm^headers^
+ bug1301226.wav
+ bug1301226.wav^headers^
+ bug1301226-odd.wav
+ bug1301226-odd.wav^headers^
+ bug1377278.webm
+ bug1377278.webm^headers^
+ bug1799787.webm
+ bug1799787.webm^headers^
+ bunny.webm
+ can_play_type_dash.js
+ can_play_type_ogg.js
+ can_play_type_wave.js
+ can_play_type_webm.js
+ cancellable_request.sjs
+ chain.ogg
+ chain.ogg^headers^
+ chain.ogv
+ chain.ogv^headers^
+ chain.opus
+ chain.opus^headers^
+ chained-audio-video.ogg
+ chained-audio-video.ogg^headers^
+ chained-video.ogv
+ chained-video.ogv^headers^
+ chromeHelper.js
+ cloneElementVisually_helpers.js
+ contentType.sjs
+ detodos.opus
+ detodos.opus^headers^
+ detodos.webm
+ detodos.webm^headers^
+ detodos-short.webm
+ detodos-short.webm^headers^
+ detodos-recorder-test.opus
+ detodos-recorder-test.opus^headers^
+ detodos-short.opus
+ detodos-short.opus^headers^
+ dirac.ogg
+ dirac.ogg^headers^
+ dynamic_resource.sjs
+ eme_standalone.js
+ eme.js
+ empty_size.mp3
+ file_access_controls.html
+ file_eme_createMediaKeys.html
+ flac-s24.flac
+ flac-s24.flac^headers^
+ flac-noheader-s16.flac
+ flac-noheader-s16.flac^headers^
+ flac-sample.mp4
+ flac-sample.mp4^headers^
+ flac-sample-cenc.mp4
+ flac-sample-cenc.mp4^headers^
+ fragment_noplay.js
+ fragment_play.js
+ gizmo.mp4
+ gizmo.mp4^headers^
+ gizmo-noaudio.mp4
+ gizmo-noaudio.mp4^headers^
+ gizmo-short.mp4
+ gizmo-short.mp4^headers^
+ gizmo.webm
+ gizmo.webm^headers^
+ gizmo-noaudio.webm
+ gizmo-noaudio.webm^headers^
+ gUM_support.js
+ gzipped_mp4.sjs
+ huge-id3.mp3
+ huge-id3.mp3^headers^
+ id3tags.mp3
+ id3tags.mp3^headers^
+ invalid-cmap-s0c0.opus
+ invalid-cmap-s0c0.opus^headers^
+ invalid-cmap-s0c2.opus
+ invalid-cmap-s0c2.opus^headers^
+ invalid-cmap-s1c2.opus
+ invalid-cmap-s1c2.opus^headers^
+ invalid-cmap-short.opus
+ invalid-cmap-short.opus^headers^
+ invalid-discard_on_multi_blocks.webm
+ invalid-discard_on_multi_blocks.webm^headers^
+ invalid-excess_discard.webm
+ invalid-excess_discard.webm^headers^
+ invalid-excess_neg_discard.webm
+ invalid-excess_neg_discard.webm^headers^
+ invalid-m0c0.opus
+ invalid-m0c0.opus^headers^
+ invalid-m0c3.opus
+ invalid-m0c3.opus^headers^
+ invalid-m1c0.opus
+ invalid-m1c0.opus^headers^
+ invalid-m1c9.opus
+ invalid-m1c9.opus^headers^
+ invalid-m2c0.opus
+ invalid-m2c0.opus^headers^
+ invalid-m2c1.opus
+ invalid-m2c1.opus^headers^
+ invalid-neg_discard.webm
+ invalid-neg_discard.webm^headers^
+ invalid-preskip.webm
+ invalid-preskip.webm^headers^
+ manifest.js
+ midflight-redirect.sjs
+ multiple-bos.ogg
+ multiple-bos.ogg^headers^
+ multiple-bos-more-header-fileds.ogg
+ multiple-bos-more-header-fileds.ogg^headers^
+ multi_id3v2.mp3
+ no-container-codec-delay.webm
+ no-cues.webm
+ no-cues.webm^headers^
+ notags.mp3
+ notags.mp3^headers^
+ opus-mapping2.mp4
+ opus-mapping2.mp4^headers^
+ opus-mapping2.webm
+ opus-mapping2.webm^headers^
+ opus-sample.mp4
+ opus-sample.mp4^headers^
+ opus-sample-cenc.mp4
+ opus-sample-cenc.mp4^headers^
+ owl-funnier-id3.mp3
+ owl-funnier-id3.mp3^headers^
+ owl-funny-id3.mp3
+ owl-funny-id3.mp3^headers^
+ owl.mp3
+ owl.mp3^headers^
+ owl-short.mp3
+ owl-short.mp3^headers^
+ pixel_aspect_ratio.mp4
+ play_promise.js
+ poster-test.jpg
+ r11025_msadpcm_c1.wav
+ r11025_msadpcm_c1.wav^headers^
+ r11025_s16_c1.wav
+ r11025_s16_c1.wav^headers^
+ r11025_s16_c1_trailing.wav
+ r11025_s16_c1_trailing.wav^headers^
+ r11025_s16_c1-short.wav
+ r11025_s16_c1-short.wav^headers^
+ r11025_u8_c1.wav
+ r11025_u8_c1.wav^headers^
+ r11025_u8_c1_trunc.wav
+ r11025_u8_c1_trunc.wav^headers^
+ r16000_u8_c1_list.wav
+ r16000_u8_c1_list.wav^headers^
+ reactivate_helper.html
+ red-46x48.mp4
+ red-46x48.mp4^headers^
+ red-48x46.mp4
+ red-48x46.mp4^headers^
+ redirect.sjs
+ referer.sjs
+ resolution-change.webm
+ resolution-change.webm^headers^
+ sample.3gp
+ sample.3g2
+ sample-encrypted-sgpdstbl-sbgptraf.mp4
+ sample-encrypted-sgpdstbl-sbgptraf.mp4^headers^
+ sample-fisbone-skeleton4.ogv
+ sample-fisbone-skeleton4.ogv^headers^
+ sample-fisbone-wrong-header.ogv
+ sample-fisbone-wrong-header.ogv^headers^
+ seek.ogv
+ seek.ogv^headers^
+ seek-short.ogv
+ seek-short.ogv^headers^
+ seek.webm
+ seek.webm^headers^
+ seek-short.webm
+ seek-short.webm^headers^
+ seek_support.js
+ seekLies.sjs
+ seek_with_sound.ogg^headers^
+ short-cenc.mp4
+ sine.webm
+ sine.webm^headers^
+ sintel-short-clearkey-subsample-encrypted-audio.webm
+ sintel-short-clearkey-subsample-encrypted-audio.webm^headers^
+ sintel-short-clearkey-subsample-encrypted-video.webm
+ sintel-short-clearkey-subsample-encrypted-video.webm^headers^
+ short.mp4
+ short.mp4.gz
+ short.mp4^headers^
+ short-aac-encrypted-audio.mp4
+ short-aac-encrypted-audio.mp4^headers^
+ short-audio-fragmented-cenc-without-pssh.mp4
+ short-audio-fragmented-cenc-without-pssh.mp4^headers^
+ short-video.ogv
+ short-video.ogv^headers^
+ short-vp9-encrypted-video.mp4
+ short-vp9-encrypted-video.mp4^headers^
+ small-shot-mp3.mp4
+ small-shot-mp3.mp4^headers^
+ small-shot.m4a
+ small-shot.mp3
+ small-shot.mp3^headers^
+ small-shot.ogg
+ small-shot.ogg^headers^
+ small-shot.flac
+ sound.ogg
+ sound.ogg^headers^
+ spacestorm-1000Hz-100ms.ogg
+ spacestorm-1000Hz-100ms.ogg^headers^
+ split.webm
+ split.webm^headers^
+ street.mp4
+ street.mp4^headers^
+ test-1-mono.opus
+ test-1-mono.opus^headers^
+ test-2-stereo.opus
+ test-2-stereo.opus^headers^
+ test-3-LCR.opus
+ test-3-LCR.opus^headers^
+ test-4-quad.opus
+ test-4-quad.opus^headers^
+ test-5-5.0.opus
+ test-5-5.0.opus^headers^
+ test-6-5.1.opus
+ test-6-5.1.opus^headers^
+ test-7-6.1.opus
+ test-7-6.1.opus^headers^
+ test-8-7.1.opus
+ test-8-7.1.opus^headers^
+ test-stereo-phase-inversion-180.opus
+ test-stereo-phase-inversion-180.opus^headers^
+ variable-channel.ogg
+ variable-channel.ogg^headers^
+ variable-channel.opus
+ variable-channel.opus^headers^
+ variable-preskip.opus
+ variable-preskip.opus^headers^
+ variable-samplerate.ogg
+ variable-samplerate.ogg^headers^
+ variable-samplerate.opus
+ variable-samplerate.opus^headers^
+ vbr-head.mp3
+ vbr-head.mp3^headers^
+ vbr.mp3
+ vbr.mp3^headers^
+ very-short.mp3
+ video-overhang.ogg
+ video-overhang.ogg^headers^
+ vp9-superframes.webm
+ vp9-superframes.webm^headers^
+ vp9.webm
+ vp9.webm^headers^
+ vp9-short.webm
+ vp9-short.webm^headers^
+ vp9cake.webm
+ vp9cake.webm^headers^
+ vp9cake-short.webm
+ vp9cake-short.webm^headers^
+ wave_metadata.wav
+ wave_metadata.wav^headers^
+ wave_metadata_bad_len.wav
+ wave_metadata_bad_len.wav^headers^
+ wave_metadata_bad_no_null.wav
+ wave_metadata_bad_no_null.wav^headers^
+ wave_metadata_bad_utf8.wav
+ wave_metadata_bad_utf8.wav^headers^
+ wave_metadata_unknown_tag.wav
+ wave_metadata_unknown_tag.wav^headers^
+ wave_metadata_utf8.wav
+ wave_metadata_utf8.wav^headers^
+ wavedata_alaw.wav
+ wavedata_alaw.wav^headers^
+ wavedata_float.wav
+ wavedata_float.wav^headers^
+ wavedata_s24.wav
+ wavedata_s24.wav^headers^
+ wavedata_s16.wav
+ wavedata_s16.wav^headers^
+ wavedata_u8.wav
+ wavedata_u8.wav^headers^
+ wavedata_ulaw.wav
+ wavedata_ulaw.wav^headers^
+ !/dom/canvas/test/captureStream_common.js
+ !/dom/html/test/reflect.js
+ !/dom/media/webrtc/tests/mochitests/head.js
+ hls/bipbop_16x9_single.m3u8
+ hls/bipbop_4x3_single.m3u8
+ hls/bipbop_4x3_variant.m3u8
+ hls/400x300_prog_index.m3u8
+ hls/400x300_prog_index_5s.m3u8
+ hls/416x243_prog_index_5s.m3u8
+ hls/640x480_prog_index.m3u8
+ hls/960x720_prog_index.m3u8
+ hls/400x300_seg0.ts
+ hls/400x300_seg0_5s.ts
+ hls/400x300_seg1.ts
+ hls/416x243_seg0_5s.ts
+ hls/640x480_seg0.ts
+ hls/640x480_seg1.ts
+ hls/960x720_seg0.ts
+ hls/960x720_seg1.ts
+ sync.webm
+
+[test_bug448534.html]
+[test_bug463162.xhtml]
+[test_bug465498.html]
+[test_bug495145.html]
+skip-if = os == "win" #Bug 1404373
+[test_bug495300.html]
+[test_bug654550.html]
+[test_bug686942.html]
+[test_bug726904.html]
+[test_bug874897.html]
+[test_bug879717.html]
+skip-if = toolkit == 'android' # bug 1285441, android(bug 1232305)
+tags=capturestream
+[test_bug895305.html]
+skip-if = (android_version == '25' && debug) # android(bug 1232305)
+[test_bug919265.html]
+skip-if = (android_version == '25' && debug) # android(bug 1232305)
+[test_bug1113600.html]
+skip-if =
+ os == 'win' && os_version == '10.0' && debug # Bug 1713410
+ os == 'mac' # Bug 1198168
+[test_bug1120222.html]
+tags=capturestream
+[test_bug1242338.html]
+[test_bug1248229.html]
+tags=capturestream
+[test_bug1512958.html]
+tags=mtg capturestream
+[test_bug1553262.html]
+tags=mtg capturestream
diff --git a/dom/media/test/mochitest_compat.ini b/dom/media/test/mochitest_compat.ini
new file mode 100644
index 0000000000..14257aae32
--- /dev/null
+++ b/dom/media/test/mochitest_compat.ini
@@ -0,0 +1,910 @@
+# This file lists tests which are compatible for testing Windows Media
+# Foundation media engine playback. These tests would still be testing on
+# different platforms, but we will use these tests to do additional testing on
+# Windows to test the media engine playback.
+
+# --------------------------------------------------------------------------
+
+# Media tests should be backend independent, i.e., not conditioned on ogg,
+# wave etc. (The only exception is the can_play_type tests, which
+# necessarily depend on the backend(s) configured.) As far as possible, each
+# test should work with any resource type. This makes it easy to add new
+# backends and reduces the amount of test duplication.
+
+# For each supported backend, resources that can be played by that backend
+# should be added to the lists in manifest.js. Media tests that aren't
+# testing for a bug in handling a specific resource type should pick one of
+# the lists in manifest.js and run the test for each resource in the list
+# that is supported in the current build (the canPlayType API is useful for
+# this).
+
+# To test whether a valid resource can simply be played through correctly,
+# and optionally that its metadata is read correctly, just add it to
+# gPlayTests in manifest.js. To test whether an invalid resource correctly
+# throws an error (and does not cause a crash or hang), just add it to
+# gErrorTests in manifest.js.
+
+# To test for a specific bug in handling a specific resource type, make the
+# test first check canPlayType for the type, and if it's not supported, just
+# do ok(true, "Type not supported") and stop the test.
+
+[DEFAULT]
+subsuite = media
+tags = media-engine-compatible
+support-files =
+ 16bit_wave_extrametadata.wav
+ 16bit_wave_extrametadata.wav^headers^
+ 320x240.ogv
+ 320x240.ogv^headers^
+ 448636.ogv
+ 448636.ogv^headers^
+ A4.ogv
+ A4.ogv^headers^
+ VID_0001.ogg
+ VID_0001.ogg^headers^
+ adts.aac
+ adts.aac^headers^
+ allowed.sjs
+ ambisonics.mp4
+ ambisonics.mp4^headers^
+ audio-gaps.ogg
+ audio-gaps.ogg^headers^
+ audio-gaps-short.ogg
+ audio-gaps-short.ogg^headers^
+ audio-overhang.ogg
+ audio-overhang.ogg^headers^
+ audio.wav
+ audio.wav^headers^
+ av1.mp4
+ av1.mp4^headers^
+ background_video.js
+ badtags.ogg
+ badtags.ogg^headers^
+ bear-640x360-v_frag-cenc-key_rotation.mp4
+ bear-640x360-a_frag-cenc-key_rotation.mp4
+ beta-phrasebook.ogg
+ beta-phrasebook.ogg^headers^
+ big.wav
+ big.wav^headers^
+ big-buck-bunny-cenc-avc3-1.m4s
+ big-buck-bunny-cenc-avc3-1.m4s^headers^
+ big-buck-bunny-cenc-avc3-init.mp4
+ big-buck-bunny-cenc-avc3-init.mp4^headers^
+ big-short.wav
+ big-short.wav^headers^
+ bipbop.mp4
+ bipbop-cenc-audio1.m4s
+ bipbop-cenc-audio1.m4s^headers^
+ bipbop-cenc-audio2.m4s
+ bipbop-cenc-audio2.m4s^headers^
+ bipbop-cenc-audio3.m4s
+ bipbop-cenc-audio3.m4s^headers^
+ bipbop-cenc-audioinit.mp4
+ bipbop-cenc-audioinit.mp4^headers^
+ bipbop-cenc-video1.m4s
+ bipbop-cenc-video1.m4s^headers^
+ bipbop-cenc-video2.m4s
+ bipbop-cenc-video2.m4s^headers^
+ bipbop-cenc-videoinit.mp4
+ bipbop-cenc-videoinit.mp4^headers^
+ bipbop-cenc-video-10s.mp4
+ bipbop-cenc-video-10s.mp4^headers^
+ bipbop-clearkey-keyrotation-clear-lead-audio.mp4
+ bipbop-clearkey-keyrotation-clear-lead-audio.mp4^headers^
+ bipbop-clearkey-keyrotation-clear-lead-video.mp4
+ bipbop-clearkey-keyrotation-clear-lead-video.mp4^headers^
+ bipbop_225w_175kbps.mp4
+ bipbop_225w_175kbps.mp4^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-1.m4s
+ bipbop_225w_175kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-2.m4s
+ bipbop_225w_175kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-3.m4s
+ bipbop_225w_175kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-4.m4s
+ bipbop_225w_175kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-init.mp4
+ bipbop_225w_175kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-1.m4s
+ bipbop_225w_175kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-2.m4s
+ bipbop_225w_175kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-3.m4s
+ bipbop_225w_175kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-4.m4s
+ bipbop_225w_175kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-init.mp4
+ bipbop_225w_175kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_225w_175kbps-cenc-video-key1-1.m4s
+ bipbop_225w_175kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_225w_175kbps-cenc-video-key1-init.mp4
+ bipbop_225w_175kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_225w_175kbps-cenc-video-key2-1.m4s
+ bipbop_225w_175kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_225w_175kbps-cenc-video-key2-init.mp4
+ bipbop_225w_175kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_300_215kbps-cenc-audio-key1-1.m4s
+ bipbop_300_215kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key1-2.m4s
+ bipbop_300_215kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key1-3.m4s
+ bipbop_300_215kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key1-4.m4s
+ bipbop_300_215kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key1-init.mp4
+ bipbop_300_215kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_300_215kbps-cenc-audio-key2-1.m4s
+ bipbop_300_215kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key2-2.m4s
+ bipbop_300_215kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key2-3.m4s
+ bipbop_300_215kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key2-4.m4s
+ bipbop_300_215kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key2-init.mp4
+ bipbop_300_215kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_300_215kbps-cenc-video-key1-1.m4s
+ bipbop_300_215kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_300_215kbps-cenc-video-key1-2.m4s
+ bipbop_300_215kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_300_215kbps-cenc-video-key1-init.mp4
+ bipbop_300_215kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_300_215kbps-cenc-video-key2-1.m4s
+ bipbop_300_215kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_300_215kbps-cenc-video-key2-2.m4s
+ bipbop_300_215kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_300_215kbps-cenc-video-key2-init.mp4
+ bipbop_300_215kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-1.m4s
+ bipbop_300wp_227kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-2.m4s
+ bipbop_300wp_227kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-3.m4s
+ bipbop_300wp_227kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-4.m4s
+ bipbop_300wp_227kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-init.mp4
+ bipbop_300wp_227kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-1.m4s
+ bipbop_300wp_227kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-2.m4s
+ bipbop_300wp_227kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-3.m4s
+ bipbop_300wp_227kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-4.m4s
+ bipbop_300wp_227kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-init.mp4
+ bipbop_300wp_227kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_300wp_227kbps-cenc-video-key1-1.m4s
+ bipbop_300wp_227kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_300wp_227kbps-cenc-video-key1-2.m4s
+ bipbop_300wp_227kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_300wp_227kbps-cenc-video-key1-init.mp4
+ bipbop_300wp_227kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_300wp_227kbps-cenc-video-key2-1.m4s
+ bipbop_300wp_227kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_300wp_227kbps-cenc-video-key2-2.m4s
+ bipbop_300wp_227kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_300wp_227kbps-cenc-video-key2-init.mp4
+ bipbop_300wp_227kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-1.m4s
+ bipbop_360w_253kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-2.m4s
+ bipbop_360w_253kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-3.m4s
+ bipbop_360w_253kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-4.m4s
+ bipbop_360w_253kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-init.mp4
+ bipbop_360w_253kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-1.m4s
+ bipbop_360w_253kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-2.m4s
+ bipbop_360w_253kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-3.m4s
+ bipbop_360w_253kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-4.m4s
+ bipbop_360w_253kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-init.mp4
+ bipbop_360w_253kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_360w_253kbps-cenc-video-key1-1.m4s
+ bipbop_360w_253kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_360w_253kbps-cenc-video-key1-init.mp4
+ bipbop_360w_253kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_360w_253kbps-cenc-video-key2-1.m4s
+ bipbop_360w_253kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_360w_253kbps-cenc-video-key2-init.mp4
+ bipbop_360w_253kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_360w_253kbps-clearkey-audio.webm
+ bipbop_360w_253kbps-clearkey-audio.webm^headers^
+ bipbop_360w_253kbps-clearkey-video-vp8.webm
+ bipbop_360w_253kbps-clearkey-video-vp8.webm^headers^
+ bipbop_360w_253kbps-clearkey-video-vp9.webm
+ bipbop_360w_253kbps-clearkey-video-vp9.webm^headers^
+ bipbop_480_624kbps-cenc-audio-key1-1.m4s
+ bipbop_480_624kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key1-2.m4s
+ bipbop_480_624kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key1-3.m4s
+ bipbop_480_624kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key1-4.m4s
+ bipbop_480_624kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key1-init.mp4
+ bipbop_480_624kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_480_624kbps-cenc-audio-key2-1.m4s
+ bipbop_480_624kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key2-2.m4s
+ bipbop_480_624kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key2-3.m4s
+ bipbop_480_624kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key2-4.m4s
+ bipbop_480_624kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key2-init.mp4
+ bipbop_480_624kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_480_624kbps-cenc-video-key1-1.m4s
+ bipbop_480_624kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_480_624kbps-cenc-video-key1-2.m4s
+ bipbop_480_624kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_480_624kbps-cenc-video-key1-init.mp4
+ bipbop_480_624kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_480_624kbps-cenc-video-key2-1.m4s
+ bipbop_480_624kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_480_624kbps-cenc-video-key2-2.m4s
+ bipbop_480_624kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_480_624kbps-cenc-video-key2-init.mp4
+ bipbop_480_624kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_480_959kbps-cenc-audio-key1-1.m4s
+ bipbop_480_959kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key1-2.m4s
+ bipbop_480_959kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key1-3.m4s
+ bipbop_480_959kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key1-4.m4s
+ bipbop_480_959kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key1-init.mp4
+ bipbop_480_959kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_480_959kbps-cenc-audio-key2-1.m4s
+ bipbop_480_959kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key2-2.m4s
+ bipbop_480_959kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key2-3.m4s
+ bipbop_480_959kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key2-4.m4s
+ bipbop_480_959kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key2-init.mp4
+ bipbop_480_959kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_480_959kbps-cenc-video-key1-1.m4s
+ bipbop_480_959kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_480_959kbps-cenc-video-key1-2.m4s
+ bipbop_480_959kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_480_959kbps-cenc-video-key1-init.mp4
+ bipbop_480_959kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_480_959kbps-cenc-video-key2-1.m4s
+ bipbop_480_959kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_480_959kbps-cenc-video-key2-2.m4s
+ bipbop_480_959kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_480_959kbps-cenc-video-key2-init.mp4
+ bipbop_480_959kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-1.m4s
+ bipbop_480wp_663kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-2.m4s
+ bipbop_480wp_663kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-3.m4s
+ bipbop_480wp_663kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-4.m4s
+ bipbop_480wp_663kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-init.mp4
+ bipbop_480wp_663kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-1.m4s
+ bipbop_480wp_663kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-2.m4s
+ bipbop_480wp_663kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-3.m4s
+ bipbop_480wp_663kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-4.m4s
+ bipbop_480wp_663kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-init.mp4
+ bipbop_480wp_663kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_480wp_663kbps-cenc-video-key1-1.m4s
+ bipbop_480wp_663kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_480wp_663kbps-cenc-video-key1-2.m4s
+ bipbop_480wp_663kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_480wp_663kbps-cenc-video-key1-init.mp4
+ bipbop_480wp_663kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_480wp_663kbps-cenc-video-key2-1.m4s
+ bipbop_480wp_663kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_480wp_663kbps-cenc-video-key2-2.m4s
+ bipbop_480wp_663kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_480wp_663kbps-cenc-video-key2-init.mp4
+ bipbop_480wp_663kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-1.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-2.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-3.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-4.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-init.mp4
+ bipbop_480wp_1001kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-1.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-2.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-3.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-4.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-init.mp4
+ bipbop_480wp_1001kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_480wp_1001kbps-cenc-video-key1-1.m4s
+ bipbop_480wp_1001kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-video-key1-2.m4s
+ bipbop_480wp_1001kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-video-key1-init.mp4
+ bipbop_480wp_1001kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_480wp_1001kbps-cenc-video-key2-1.m4s
+ bipbop_480wp_1001kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-video-key2-2.m4s
+ bipbop_480wp_1001kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-video-key2-init.mp4
+ bipbop_480wp_1001kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_audio_aac_8k.mp4
+ bipbop_audio_aac_8k.mp4^headers^
+ bipbop_audio_aac_22.05k.mp4
+ bipbop_audio_aac_22.05k.mp4^headers^
+ bipbop_audio_aac_44.1k.mp4
+ bipbop_audio_aac_44.1k.mp4^headers^
+ bipbop_audio_aac_48k.mp4
+ bipbop_audio_aac_48k.mp4^headers^
+ bipbop_audio_aac_88.2k.mp4
+ bipbop_audio_aac_88.2k.mp4^headers^
+ bipbop_audio_aac_96k.mp4
+ bipbop_audio_aac_96k.mp4^headers^
+ bipbop_cbcs_1_9_audio_1.m4s
+ bipbop_cbcs_1_9_audio_1.m4s^headers^
+ bipbop_cbcs_1_9_audio_init.mp4
+ bipbop_cbcs_1_9_audio_init.mp4^headers^
+ bipbop_cbcs_1_9_video_1.m4s
+ bipbop_cbcs_1_9_video_1.m4s^headers^
+ bipbop_cbcs_1_9_video_init.mp4
+ bipbop_cbcs_1_9_video_init.mp4^headers^
+ bipbop_cbcs_5_5_audio_1.m4s
+ bipbop_cbcs_5_5_audio_1.m4s^headers^
+ bipbop_cbcs_5_5_audio_init.mp4
+ bipbop_cbcs_5_5_audio_init.mp4^headers^
+ bipbop_cbcs_5_5_video_1.m4s
+ bipbop_cbcs_5_5_video_1.m4s^headers^
+ bipbop_cbcs_5_5_video_init.mp4
+ bipbop_cbcs_5_5_video_init.mp4^headers^
+ bipbop_cbcs_7_7_audio_1.m4s
+ bipbop_cbcs_7_7_audio_1.m4s^headers^
+ bipbop_cbcs_7_7_audio_init.mp4
+ bipbop_cbcs_7_7_audio_init.mp4^headers^
+ bipbop_cbcs_7_7_video_1.m4s
+ bipbop_cbcs_7_7_video_1.m4s^headers^
+ bipbop_cbcs_7_7_video_init.mp4
+ bipbop_cbcs_7_7_video_init.mp4^headers^
+ bipbop_cbcs_9_8_audio_1.m4s
+ bipbop_cbcs_9_8_audio_1.m4s^headers^
+ bipbop_cbcs_9_8_audio_init.mp4
+ bipbop_cbcs_9_8_audio_init.mp4^headers^
+ bipbop_cbcs_9_8_video_1.m4s
+ bipbop_cbcs_9_8_video_1.m4s^headers^
+ bipbop_cbcs_9_8_video_init.mp4
+ bipbop_cbcs_9_8_video_init.mp4^headers^
+ bipbop_cbcs_10_0_audio_1.m4s
+ bipbop_cbcs_10_0_audio_1.m4s^headers^
+ bipbop_cbcs_10_0_audio_init.mp4
+ bipbop_cbcs_10_0_audio_init.mp4^headers^
+ bipbop_cbcs_10_0_video_1.m4s
+ bipbop_cbcs_10_0_video_1.m4s^headers^
+ bipbop_cbcs_10_0_video_init.mp4
+ bipbop_cbcs_10_0_video_init.mp4^headers^
+ bipbop_short_pixel_metadata_bigger_than_in_stream_vp8.webm
+ bipbop_short_pixel_metadata_bigger_than_in_stream_vp8.webm^headers^
+ bipbop_short_pixel_metadata_narrower_than_in_stream_vp8.webm
+ bipbop_short_pixel_metadata_narrower_than_in_stream_vp8.webm^headers^
+ bipbop_short_pixel_metadata_smaller_than_in_stream_vp8.webm^headers^
+ bipbop_short_pixel_metadata_smaller_than_in_stream_vp8.webm
+ bipbop_short_vp8.webm
+ bipbop_short_vp8.webm^headers^
+ bipbop-lateaudio.mp4
+ bipbop-lateaudio.mp4^headers^
+ black100x100-aspect3to2.ogv
+ black100x100-aspect3to2.ogv^headers^
+ bogus.duh
+ bogus.ogv
+ bogus.ogv^headers^
+ bogus.wav
+ bogus.wav^headers^
+ bug461281.ogg
+ bug461281.ogg^headers^
+ bug482461-theora.ogv
+ bug482461-theora.ogv^headers^
+ bug482461.ogv
+ bug482461.ogv^headers^
+ bug495129.ogv
+ bug495129.ogv^headers^
+ bug495794.ogg
+ bug495794.ogg^headers^
+ bug498380.ogv
+ bug498380.ogv^headers^
+ bug498855-1.ogv
+ bug498855-1.ogv^headers^
+ bug498855-2.ogv
+ bug498855-2.ogv^headers^
+ bug498855-3.ogv
+ bug498855-3.ogv^headers^
+ bug499519.ogv
+ bug499519.ogv^headers^
+ bug500311.ogv
+ bug500311.ogv^headers^
+ bug501279.ogg
+ bug501279.ogg^headers^
+ bug504613.ogv
+ bug504613.ogv^headers^
+ bug504644.ogv
+ bug504644.ogv^headers^
+ bug504843.ogv
+ bug504843.ogv^headers^
+ bug506094.ogv
+ bug506094.ogv^headers^
+ bug516323.indexed.ogv
+ bug516323.indexed.ogv^headers^
+ bug516323.ogv
+ bug516323.ogv^headers^
+ bug520493.ogg
+ bug520493.ogg^headers^
+ bug520500.ogg
+ bug520500.ogg^headers^
+ bug520908.ogv
+ bug520908.ogv^headers^
+ bug523816.ogv
+ bug523816.ogv^headers^
+ bug533822.ogg
+ bug533822.ogg^headers^
+ bug556821.ogv
+ bug556821.ogv^headers^
+ bug557094.ogv
+ bug557094.ogv^headers^
+ bug603918.webm
+ bug603918.webm^headers^
+ bug604067.webm
+ bug604067.webm^headers^
+ bug1066943.webm
+ bug1066943.webm^headers^
+ bug1301226.wav
+ bug1301226.wav^headers^
+ bug1301226-odd.wav
+ bug1301226-odd.wav^headers^
+ bug1377278.webm
+ bug1377278.webm^headers^
+ bunny.webm
+ can_play_type_dash.js
+ can_play_type_ogg.js
+ can_play_type_wave.js
+ can_play_type_webm.js
+ cancellable_request.sjs
+ chain.ogg
+ chain.ogg^headers^
+ chain.ogv
+ chain.ogv^headers^
+ chain.opus
+ chain.opus^headers^
+ chained-audio-video.ogg
+ chained-audio-video.ogg^headers^
+ chained-video.ogv
+ chained-video.ogv^headers^
+ chromeHelper.js
+ cloneElementVisually_helpers.js
+ contentType.sjs
+ detodos.opus
+ detodos.opus^headers^
+ detodos.webm
+ detodos.webm^headers^
+ detodos-short.webm
+ detodos-short.webm^headers^
+ detodos-recorder-test.opus
+ detodos-recorder-test.opus^headers^
+ detodos-short.opus
+ detodos-short.opus^headers^
+ dirac.ogg
+ dirac.ogg^headers^
+ dynamic_resource.sjs
+ eme_standalone.js
+ eme.js
+ empty_size.mp3
+ file_access_controls.html
+ file_eme_createMediaKeys.html
+ flac-s24.flac
+ flac-s24.flac^headers^
+ flac-noheader-s16.flac
+ flac-noheader-s16.flac^headers^
+ flac-sample.mp4
+ flac-sample.mp4^headers^
+ flac-sample-cenc.mp4
+ flac-sample-cenc.mp4^headers^
+ fragment_noplay.js
+ fragment_play.js
+ gizmo.mp4
+ gizmo.mp4^headers^
+ gizmo-noaudio.mp4
+ gizmo-noaudio.mp4^headers^
+ gizmo-short.mp4
+ gizmo-short.mp4^headers^
+ gizmo.webm
+ gizmo.webm^headers^
+ gizmo-noaudio.webm
+ gizmo-noaudio.webm^headers^
+ gUM_support.js
+ gzipped_mp4.sjs
+ huge-id3.mp3
+ huge-id3.mp3^headers^
+ id3tags.mp3
+ id3tags.mp3^headers^
+ invalid-cmap-s0c0.opus
+ invalid-cmap-s0c0.opus^headers^
+ invalid-cmap-s0c2.opus
+ invalid-cmap-s0c2.opus^headers^
+ invalid-cmap-s1c2.opus
+ invalid-cmap-s1c2.opus^headers^
+ invalid-cmap-short.opus
+ invalid-cmap-short.opus^headers^
+ invalid-discard_on_multi_blocks.webm
+ invalid-discard_on_multi_blocks.webm^headers^
+ invalid-excess_discard.webm
+ invalid-excess_discard.webm^headers^
+ invalid-excess_neg_discard.webm
+ invalid-excess_neg_discard.webm^headers^
+ invalid-m0c0.opus
+ invalid-m0c0.opus^headers^
+ invalid-m0c3.opus
+ invalid-m0c3.opus^headers^
+ invalid-m1c0.opus
+ invalid-m1c0.opus^headers^
+ invalid-m1c9.opus
+ invalid-m1c9.opus^headers^
+ invalid-m2c0.opus
+ invalid-m2c0.opus^headers^
+ invalid-m2c1.opus
+ invalid-m2c1.opus^headers^
+ invalid-neg_discard.webm
+ invalid-neg_discard.webm^headers^
+ invalid-preskip.webm
+ invalid-preskip.webm^headers^
+ manifest.js
+ midflight-redirect.sjs
+ multiple-bos.ogg
+ multiple-bos.ogg^headers^
+ multiple-bos-more-header-fileds.ogg
+ multiple-bos-more-header-fileds.ogg^headers^
+ multi_id3v2.mp3
+ no-container-codec-delay.webm
+ no-cues.webm
+ no-cues.webm^headers^
+ notags.mp3
+ notags.mp3^headers^
+ opus-mapping2.mp4
+ opus-mapping2.mp4^headers^
+ opus-mapping2.webm
+ opus-mapping2.webm^headers^
+ opus-sample.mp4
+ opus-sample.mp4^headers^
+ opus-sample-cenc.mp4
+ opus-sample-cenc.mp4^headers^
+ owl-funnier-id3.mp3
+ owl-funnier-id3.mp3^headers^
+ owl-funny-id3.mp3
+ owl-funny-id3.mp3^headers^
+ owl.mp3
+ owl.mp3^headers^
+ owl-short.mp3
+ owl-short.mp3^headers^
+ pixel_aspect_ratio.mp4
+ play_promise.js
+ poster-test.jpg
+ r11025_msadpcm_c1.wav
+ r11025_msadpcm_c1.wav^headers^
+ r11025_s16_c1.wav
+ r11025_s16_c1.wav^headers^
+ r11025_s16_c1_trailing.wav
+ r11025_s16_c1_trailing.wav^headers^
+ r11025_s16_c1-short.wav
+ r11025_s16_c1-short.wav^headers^
+ r11025_u8_c1.wav
+ r11025_u8_c1.wav^headers^
+ r11025_u8_c1_trunc.wav
+ r11025_u8_c1_trunc.wav^headers^
+ r16000_u8_c1_list.wav
+ r16000_u8_c1_list.wav^headers^
+ reactivate_helper.html
+ red-46x48.mp4
+ red-46x48.mp4^headers^
+ red-48x46.mp4
+ red-48x46.mp4^headers^
+ redirect.sjs
+ referer.sjs
+ resolution-change.webm
+ resolution-change.webm^headers^
+ sample.3gp
+ sample.3g2
+ sample-encrypted-sgpdstbl-sbgptraf.mp4
+ sample-encrypted-sgpdstbl-sbgptraf.mp4^headers^
+ sample-fisbone-skeleton4.ogv
+ sample-fisbone-skeleton4.ogv^headers^
+ sample-fisbone-wrong-header.ogv
+ sample-fisbone-wrong-header.ogv^headers^
+ seek.ogv
+ seek.ogv^headers^
+ seek-short.ogv
+ seek-short.ogv^headers^
+ seek.webm
+ seek.webm^headers^
+ seek-short.webm
+ seek-short.webm^headers^
+ seek_support.js
+ seekLies.sjs
+ seek_with_sound.ogg^headers^
+ short-cenc.mp4
+ sine.webm
+ sine.webm^headers^
+ sintel-short-clearkey-subsample-encrypted-audio.webm
+ sintel-short-clearkey-subsample-encrypted-audio.webm^headers^
+ sintel-short-clearkey-subsample-encrypted-video.webm
+ sintel-short-clearkey-subsample-encrypted-video.webm^headers^
+ short.mp4
+ short.mp4.gz
+ short.mp4^headers^
+ short-aac-encrypted-audio.mp4
+ short-aac-encrypted-audio.mp4^headers^
+ short-audio-fragmented-cenc-without-pssh.mp4
+ short-audio-fragmented-cenc-without-pssh.mp4^headers^
+ short-video.ogv
+ short-video.ogv^headers^
+ short-vp9-encrypted-video.mp4
+ short-vp9-encrypted-video.mp4^headers^
+ small-shot-mp3.mp4
+ small-shot-mp3.mp4^headers^
+ small-shot.m4a
+ small-shot.mp3
+ small-shot.mp3^headers^
+ small-shot.ogg
+ small-shot.ogg^headers^
+ small-shot.flac
+ sound.ogg
+ sound.ogg^headers^
+ spacestorm-1000Hz-100ms.ogg
+ spacestorm-1000Hz-100ms.ogg^headers^
+ split.webm
+ split.webm^headers^
+ street.mp4
+ street.mp4^headers^
+ test-1-mono.opus
+ test-1-mono.opus^headers^
+ test-2-stereo.opus
+ test-2-stereo.opus^headers^
+ test-3-LCR.opus
+ test-3-LCR.opus^headers^
+ test-4-quad.opus
+ test-4-quad.opus^headers^
+ test-5-5.0.opus
+ test-5-5.0.opus^headers^
+ test-6-5.1.opus
+ test-6-5.1.opus^headers^
+ test-7-6.1.opus
+ test-7-6.1.opus^headers^
+ test-8-7.1.opus
+ test-8-7.1.opus^headers^
+ test-stereo-phase-inversion-180.opus
+ test-stereo-phase-inversion-180.opus^headers^
+ variable-channel.ogg
+ variable-channel.ogg^headers^
+ variable-channel.opus
+ variable-channel.opus^headers^
+ variable-preskip.opus
+ variable-preskip.opus^headers^
+ variable-samplerate.ogg
+ variable-samplerate.ogg^headers^
+ variable-samplerate.opus
+ variable-samplerate.opus^headers^
+ vbr-head.mp3
+ vbr-head.mp3^headers^
+ vbr.mp3
+ vbr.mp3^headers^
+ very-short.mp3
+ video-overhang.ogg
+ video-overhang.ogg^headers^
+ vp9-superframes.webm
+ vp9-superframes.webm^headers^
+ vp9.webm
+ vp9.webm^headers^
+ vp9-short.webm
+ vp9-short.webm^headers^
+ vp9cake.webm
+ vp9cake.webm^headers^
+ vp9cake-short.webm
+ vp9cake-short.webm^headers^
+ wave_metadata.wav
+ wave_metadata.wav^headers^
+ wave_metadata_bad_len.wav
+ wave_metadata_bad_len.wav^headers^
+ wave_metadata_bad_no_null.wav
+ wave_metadata_bad_no_null.wav^headers^
+ wave_metadata_bad_utf8.wav
+ wave_metadata_bad_utf8.wav^headers^
+ wave_metadata_unknown_tag.wav
+ wave_metadata_unknown_tag.wav^headers^
+ wave_metadata_utf8.wav
+ wave_metadata_utf8.wav^headers^
+ wavedata_alaw.wav
+ wavedata_alaw.wav^headers^
+ wavedata_float.wav
+ wavedata_float.wav^headers^
+ wavedata_s24.wav
+ wavedata_s24.wav^headers^
+ wavedata_s16.wav
+ wavedata_s16.wav^headers^
+ wavedata_u8.wav
+ wavedata_u8.wav^headers^
+ wavedata_ulaw.wav
+ wavedata_ulaw.wav^headers^
+ !/dom/canvas/test/captureStream_common.js
+ !/dom/html/test/reflect.js
+ !/dom/media/webrtc/tests/mochitests/head.js
+ hls/bipbop_16x9_single.m3u8
+ hls/bipbop_4x3_single.m3u8
+ hls/bipbop_4x3_variant.m3u8
+ hls/400x300_prog_index.m3u8
+ hls/400x300_prog_index_5s.m3u8
+ hls/416x243_prog_index_5s.m3u8
+ hls/640x480_prog_index.m3u8
+ hls/960x720_prog_index.m3u8
+ hls/400x300_seg0.ts
+ hls/400x300_seg0_5s.ts
+ hls/400x300_seg1.ts
+ hls/416x243_seg0_5s.ts
+ hls/640x480_seg0.ts
+ hls/640x480_seg1.ts
+ hls/960x720_seg0.ts
+ hls/960x720_seg1.ts
+ sync.webm
+
+[test_aspectratio_mp4.html]
+[test_access_control.html]
+[test_arraybuffer.html]
+[test_audio1.html]
+[test_audio2.html]
+[test_audioDocumentTitle.html]
+skip-if = true # bug 475110 - disabled since we don't play Wave files standalone
+[test_buffered.html]
+[test_can_play_type.html]
+skip-if = (android_version == '25' && debug) # android(bug 1232305)
+[test_can_play_type_mpeg.html]
+[test_can_play_type_no_ogg.html]
+skip-if = (android_version == '25' && debug) # android(bug 1232305)
+[test_can_play_type_ogg.html]
+skip-if = (android_version == '25' && debug) # android(bug 1232305)
+[test_closing_connections.html]
+[test_constants.html]
+[test_controls.html]
+[test_cueless_webm_seek-1.html]
+[test_cueless_webm_seek-2.html]
+[test_cueless_webm_seek-3.html]
+[test_currentTime.html]
+[test_debug_data_helpers.html]
+[test_decode_error.html]
+[test_decode_error_crossorigin.html]
+[test_decoder_disable.html]
+[test_defaultMuted.html]
+[test_delay_load.html]
+[test_duration_after_error.html]
+[test_empty_resource.html]
+[test_error_in_video_document.html]
+[test_error_on_404.html]
+[test_info_leak.html]
+[test_invalid_reject.html]
+[test_invalid_seek.html]
+[test_load.html]
+[test_load_candidates.html]
+[test_load_same_resource.html]
+[test_load_source.html]
+[test_load_source_empty_type.html]
+[test_loop.html]
+skip-if =
+ wmfme # Bug 1781539
+[test_looping_eventsOrder.html]
+[test_mediatrack_consuming_mediaresource.html]
+[test_mediatrack_events.html]
+scheme=https
+[test_mediatrack_parsing_ogg.html]
+[test_metadata.html]
+[test_mozHasAudio.html]
+[test_mp3_with_multiple_ID3v2.html]
+[test_networkState.html]
+[test_new_audio.html]
+[test_no_load_event.html]
+[test_not_reset_playbackRate_when_removing_nonloaded_media_from_document.html]
+[test_paused.html]
+[test_paused_after_ended.html]
+[test_periodic_timeupdate.html]
+[test_play_events.html]
+[test_play_promise_1.html]
+tags=promise-play
+[test_play_promise_2.html]
+tags=promise-play
+[test_play_promise_3.html]
+tags=promise-play
+[test_play_promise_4.html]
+tags=promise-play
+[test_play_promise_5.html]
+tags=promise-play
+[test_play_promise_6.html]
+tags=promise-play
+[test_play_promise_7.html]
+tags=promise-play
+[test_play_promise_8.html]
+tags=promise-play
+[test_play_promise_9.html]
+tags=promise-play
+[test_play_promise_10.html]
+tags=promise-play
+[test_play_promise_11.html]
+tags=promise-play
+[test_play_promise_12.html]
+tags=promise-play
+[test_play_promise_13.html]
+tags=promise-play
+[test_play_promise_14.html]
+tags=promise-play
+[test_play_promise_15.html]
+tags=promise-play
+[test_play_promise_16.html]
+tags=promise-play
+[test_play_promise_17.html]
+tags=promise-play
+[test_play_promise_18.html]
+tags=promise-play
+[test_play_twice.html]
+skip-if = appname == "seamonkey" # Seamonkey: Bug 598252, bug 1307337, bug 1143695
+# If encountering intermittents in test_playback.html please consider disabling
+# the individual faulting file via `manifest.js` as disabling the whole test on
+# a platform removes a lot of coverage.
+[test_playback_errors.html]
+[test_playback_rate_playpause.html]
+[test_playback_reactivate.html]
+[test_preload_actions.html]
+[test_preload_attribute.html]
+[test_preload_suspend.html]
+[test_preserve_playbackrate_after_ui_play.html]
+[test_progress.html]
+[test_reactivate.html]
+skip-if = true # see bug 1319725
+[test_readyState.html]
+[test_referer.html]
+skip-if = android_version == '25' && debug # android(bug 1232305)
+[test_reset_src.html]
+skip-if = (verify && debug && os == 'win')
+[test_source.html]
+[test_source_null.html]
+[test_source_write.html]
+[test_standalone.html]
+[test_suspend_media_by_inactive_docshell.html]
+[test_timeupdate_small_files.html]
+[test_unseekable.html]
+[test_video_gzip_encoding.html]
+[test_video_in_audio_element.html]
+[test_video_stats_resistfingerprinting.html]
+tags = resistfingerprinting
+[test_videoDocumentTitle.html]
+[test_VideoPlaybackQuality.html]
+[test_VideoPlaybackQuality_disabled.html]
+[test_volume.html]
+# The tests below contain backend-specific tests. Write backend independent
+# tests rather than adding to this list.
+[test_can_play_type_webm.html]
+[test_can_play_type_wave.html]
+[test_fragment_noplay.html]
+[test_fragment_play.html]
+[test_bug1431810_opus_downmix_to_mono.html]
+
diff --git a/dom/media/test/mochitest_eme.ini b/dom/media/test/mochitest_eme.ini
new file mode 100644
index 0000000000..f17644799e
--- /dev/null
+++ b/dom/media/test/mochitest_eme.ini
@@ -0,0 +1,874 @@
+# Media tests should be backend independent, i.e., not conditioned on ogg,
+# wave etc. (The only exception is the can_play_type tests, which
+# necessarily depend on the backend(s) configured.) As far as possible, each
+# test should work with any resource type. This makes it easy to add new
+# backends and reduces the amount of test duplication.
+
+# For each supported backend, resources that can be played by that backend
+# should be added to the lists in manifest.js. Media tests that aren't
+# testing for a bug in handling a specific resource type should pick one of
+# the lists in manifest.js and run the test for each resource in the list
+# that is supported in the current build (the canPlayType API is useful for
+# this).
+
+# To test whether a valid resource can simply be played through correctly,
+# and optionally that its metadata is read correctly, just add it to
+# gPlayTests in manifest.js. To test whether an invalid resource correctly
+# throws an error (and does not cause a crash or hang), just add it to
+# gErrorTests in manifest.js.
+
+# To test for a specific bug in handling a specific resource type, make the
+# test first check canPlayType for the type, and if it's not supported, just
+# do ok(true, "Type not supported") and stop the test.
+
+[DEFAULT]
+subsuite = media
+skip-if =
+ (os == "win" && processor == "aarch64") # aarch64 due to 1536604
+ os == "linux" && (asan || debug) # Bug 1476870: common fatal error (shutdown hang) on asan/debug
+support-files =
+ 16bit_wave_extrametadata.wav
+ 16bit_wave_extrametadata.wav^headers^
+ 320x240.ogv
+ 320x240.ogv^headers^
+ 448636.ogv
+ 448636.ogv^headers^
+ A4.ogv
+ A4.ogv^headers^
+ VID_0001.ogg
+ VID_0001.ogg^headers^
+ allowed.sjs
+ ambisonics.mp4
+ ambisonics.mp4^headers^
+ audio-gaps.ogg
+ audio-gaps.ogg^headers^
+ audio-gaps-short.ogg
+ audio-gaps-short.ogg^headers^
+ audio-overhang.ogg
+ audio-overhang.ogg^headers^
+ audio.wav
+ audio.wav^headers^
+ av1.mp4
+ av1.mp4^headers^
+ background_video.js
+ badtags.ogg
+ badtags.ogg^headers^
+ bear-640x360-v_frag-cenc-key_rotation.mp4
+ bear-640x360-a_frag-cenc-key_rotation.mp4
+ beta-phrasebook.ogg
+ beta-phrasebook.ogg^headers^
+ big.wav
+ big.wav^headers^
+ big-buck-bunny-cenc-avc3-1.m4s
+ big-buck-bunny-cenc-avc3-1.m4s^headers^
+ big-buck-bunny-cenc-avc3-init.mp4
+ big-buck-bunny-cenc-avc3-init.mp4^headers^
+ big-short.wav
+ big-short.wav^headers^
+ bipbop.mp4
+ bipbop-cenc-audio1.m4s
+ bipbop-cenc-audio1.m4s^headers^
+ bipbop-cenc-audio2.m4s
+ bipbop-cenc-audio2.m4s^headers^
+ bipbop-cenc-audio3.m4s
+ bipbop-cenc-audio3.m4s^headers^
+ bipbop-cenc-audioinit.mp4
+ bipbop-cenc-audioinit.mp4^headers^
+ bipbop-cenc-video1.m4s
+ bipbop-cenc-video1.m4s^headers^
+ bipbop-cenc-video2.m4s
+ bipbop-cenc-video2.m4s^headers^
+ bipbop-cenc-videoinit.mp4
+ bipbop-cenc-videoinit.mp4^headers^
+ bipbop-cenc-video-10s.mp4
+ bipbop-cenc-video-10s.mp4^headers^
+ bipbop-clearkey-keyrotation-clear-lead-audio.mp4
+ bipbop-clearkey-keyrotation-clear-lead-audio.mp4^headers^
+ bipbop-clearkey-keyrotation-clear-lead-video.mp4
+ bipbop-clearkey-keyrotation-clear-lead-video.mp4^headers^
+ bipbop_225w_175kbps.mp4
+ bipbop_225w_175kbps.mp4^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-1.m4s
+ bipbop_225w_175kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-2.m4s
+ bipbop_225w_175kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-3.m4s
+ bipbop_225w_175kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-4.m4s
+ bipbop_225w_175kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-init.mp4
+ bipbop_225w_175kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-1.m4s
+ bipbop_225w_175kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-2.m4s
+ bipbop_225w_175kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-3.m4s
+ bipbop_225w_175kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-4.m4s
+ bipbop_225w_175kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-init.mp4
+ bipbop_225w_175kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_225w_175kbps-cenc-video-key1-1.m4s
+ bipbop_225w_175kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_225w_175kbps-cenc-video-key1-init.mp4
+ bipbop_225w_175kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_225w_175kbps-cenc-video-key2-1.m4s
+ bipbop_225w_175kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_225w_175kbps-cenc-video-key2-init.mp4
+ bipbop_225w_175kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_300_215kbps-cenc-audio-key1-1.m4s
+ bipbop_300_215kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key1-2.m4s
+ bipbop_300_215kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key1-3.m4s
+ bipbop_300_215kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key1-4.m4s
+ bipbop_300_215kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key1-init.mp4
+ bipbop_300_215kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_300_215kbps-cenc-audio-key2-1.m4s
+ bipbop_300_215kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key2-2.m4s
+ bipbop_300_215kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key2-3.m4s
+ bipbop_300_215kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key2-4.m4s
+ bipbop_300_215kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key2-init.mp4
+ bipbop_300_215kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_300_215kbps-cenc-video-key1-1.m4s
+ bipbop_300_215kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_300_215kbps-cenc-video-key1-2.m4s
+ bipbop_300_215kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_300_215kbps-cenc-video-key1-init.mp4
+ bipbop_300_215kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_300_215kbps-cenc-video-key2-1.m4s
+ bipbop_300_215kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_300_215kbps-cenc-video-key2-2.m4s
+ bipbop_300_215kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_300_215kbps-cenc-video-key2-init.mp4
+ bipbop_300_215kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-1.m4s
+ bipbop_300wp_227kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-2.m4s
+ bipbop_300wp_227kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-3.m4s
+ bipbop_300wp_227kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-4.m4s
+ bipbop_300wp_227kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-init.mp4
+ bipbop_300wp_227kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-1.m4s
+ bipbop_300wp_227kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-2.m4s
+ bipbop_300wp_227kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-3.m4s
+ bipbop_300wp_227kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-4.m4s
+ bipbop_300wp_227kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-init.mp4
+ bipbop_300wp_227kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_300wp_227kbps-cenc-video-key1-1.m4s
+ bipbop_300wp_227kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_300wp_227kbps-cenc-video-key1-2.m4s
+ bipbop_300wp_227kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_300wp_227kbps-cenc-video-key1-init.mp4
+ bipbop_300wp_227kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_300wp_227kbps-cenc-video-key2-1.m4s
+ bipbop_300wp_227kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_300wp_227kbps-cenc-video-key2-2.m4s
+ bipbop_300wp_227kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_300wp_227kbps-cenc-video-key2-init.mp4
+ bipbop_300wp_227kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-1.m4s
+ bipbop_360w_253kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-2.m4s
+ bipbop_360w_253kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-3.m4s
+ bipbop_360w_253kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-4.m4s
+ bipbop_360w_253kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-init.mp4
+ bipbop_360w_253kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-1.m4s
+ bipbop_360w_253kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-2.m4s
+ bipbop_360w_253kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-3.m4s
+ bipbop_360w_253kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-4.m4s
+ bipbop_360w_253kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-init.mp4
+ bipbop_360w_253kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_360w_253kbps-cenc-video-key1-1.m4s
+ bipbop_360w_253kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_360w_253kbps-cenc-video-key1-init.mp4
+ bipbop_360w_253kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_360w_253kbps-cenc-video-key2-1.m4s
+ bipbop_360w_253kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_360w_253kbps-cenc-video-key2-init.mp4
+ bipbop_360w_253kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_360w_253kbps-clearkey-audio.webm
+ bipbop_360w_253kbps-clearkey-audio.webm^headers^
+ bipbop_360w_253kbps-clearkey-video-vp8.webm
+ bipbop_360w_253kbps-clearkey-video-vp8.webm^headers^
+ bipbop_360w_253kbps-clearkey-video-vp9.webm
+ bipbop_360w_253kbps-clearkey-video-vp9.webm^headers^
+ bipbop_480_624kbps-cenc-audio-key1-1.m4s
+ bipbop_480_624kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key1-2.m4s
+ bipbop_480_624kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key1-3.m4s
+ bipbop_480_624kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key1-4.m4s
+ bipbop_480_624kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key1-init.mp4
+ bipbop_480_624kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_480_624kbps-cenc-audio-key2-1.m4s
+ bipbop_480_624kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key2-2.m4s
+ bipbop_480_624kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key2-3.m4s
+ bipbop_480_624kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key2-4.m4s
+ bipbop_480_624kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key2-init.mp4
+ bipbop_480_624kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_480_624kbps-cenc-video-key1-1.m4s
+ bipbop_480_624kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_480_624kbps-cenc-video-key1-2.m4s
+ bipbop_480_624kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_480_624kbps-cenc-video-key1-init.mp4
+ bipbop_480_624kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_480_624kbps-cenc-video-key2-1.m4s
+ bipbop_480_624kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_480_624kbps-cenc-video-key2-2.m4s
+ bipbop_480_624kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_480_624kbps-cenc-video-key2-init.mp4
+ bipbop_480_624kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_480_959kbps-cenc-audio-key1-1.m4s
+ bipbop_480_959kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key1-2.m4s
+ bipbop_480_959kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key1-3.m4s
+ bipbop_480_959kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key1-4.m4s
+ bipbop_480_959kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key1-init.mp4
+ bipbop_480_959kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_480_959kbps-cenc-audio-key2-1.m4s
+ bipbop_480_959kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key2-2.m4s
+ bipbop_480_959kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key2-3.m4s
+ bipbop_480_959kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key2-4.m4s
+ bipbop_480_959kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key2-init.mp4
+ bipbop_480_959kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_480_959kbps-cenc-video-key1-1.m4s
+ bipbop_480_959kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_480_959kbps-cenc-video-key1-2.m4s
+ bipbop_480_959kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_480_959kbps-cenc-video-key1-init.mp4
+ bipbop_480_959kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_480_959kbps-cenc-video-key2-1.m4s
+ bipbop_480_959kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_480_959kbps-cenc-video-key2-2.m4s
+ bipbop_480_959kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_480_959kbps-cenc-video-key2-init.mp4
+ bipbop_480_959kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-1.m4s
+ bipbop_480wp_663kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-2.m4s
+ bipbop_480wp_663kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-3.m4s
+ bipbop_480wp_663kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-4.m4s
+ bipbop_480wp_663kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-init.mp4
+ bipbop_480wp_663kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-1.m4s
+ bipbop_480wp_663kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-2.m4s
+ bipbop_480wp_663kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-3.m4s
+ bipbop_480wp_663kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-4.m4s
+ bipbop_480wp_663kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-init.mp4
+ bipbop_480wp_663kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_480wp_663kbps-cenc-video-key1-1.m4s
+ bipbop_480wp_663kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_480wp_663kbps-cenc-video-key1-2.m4s
+ bipbop_480wp_663kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_480wp_663kbps-cenc-video-key1-init.mp4
+ bipbop_480wp_663kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_480wp_663kbps-cenc-video-key2-1.m4s
+ bipbop_480wp_663kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_480wp_663kbps-cenc-video-key2-2.m4s
+ bipbop_480wp_663kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_480wp_663kbps-cenc-video-key2-init.mp4
+ bipbop_480wp_663kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-1.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-2.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-3.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-4.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-init.mp4
+ bipbop_480wp_1001kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-1.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-2.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-3.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-4.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-init.mp4
+ bipbop_480wp_1001kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_480wp_1001kbps-cenc-video-key1-1.m4s
+ bipbop_480wp_1001kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-video-key1-2.m4s
+ bipbop_480wp_1001kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-video-key1-init.mp4
+ bipbop_480wp_1001kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_480wp_1001kbps-cenc-video-key2-1.m4s
+ bipbop_480wp_1001kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-video-key2-2.m4s
+ bipbop_480wp_1001kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-video-key2-init.mp4
+ bipbop_480wp_1001kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_audio_aac_8k.mp4
+ bipbop_audio_aac_8k.mp4^headers^
+ bipbop_audio_aac_22.05k.mp4
+ bipbop_audio_aac_22.05k.mp4^headers^
+ bipbop_audio_aac_44.1k.mp4
+ bipbop_audio_aac_44.1k.mp4^headers^
+ bipbop_audio_aac_48k.mp4
+ bipbop_audio_aac_48k.mp4^headers^
+ bipbop_audio_aac_88.2k.mp4
+ bipbop_audio_aac_88.2k.mp4^headers^
+ bipbop_audio_aac_96k.mp4
+ bipbop_audio_aac_96k.mp4^headers^
+ bipbop_cbcs_1_9_audio_1.m4s
+ bipbop_cbcs_1_9_audio_1.m4s^headers^
+ bipbop_cbcs_1_9_audio_init.mp4
+ bipbop_cbcs_1_9_audio_init.mp4^headers^
+ bipbop_cbcs_1_9_video_1.m4s
+ bipbop_cbcs_1_9_video_1.m4s^headers^
+ bipbop_cbcs_1_9_video_init.mp4
+ bipbop_cbcs_1_9_video_init.mp4^headers^
+ bipbop_cbcs_5_5_audio_1.m4s
+ bipbop_cbcs_5_5_audio_1.m4s^headers^
+ bipbop_cbcs_5_5_audio_init.mp4
+ bipbop_cbcs_5_5_audio_init.mp4^headers^
+ bipbop_cbcs_5_5_video_1.m4s
+ bipbop_cbcs_5_5_video_1.m4s^headers^
+ bipbop_cbcs_5_5_video_init.mp4
+ bipbop_cbcs_5_5_video_init.mp4^headers^
+ bipbop_cbcs_7_7_audio_1.m4s
+ bipbop_cbcs_7_7_audio_1.m4s^headers^
+ bipbop_cbcs_7_7_audio_init.mp4
+ bipbop_cbcs_7_7_audio_init.mp4^headers^
+ bipbop_cbcs_7_7_video_1.m4s
+ bipbop_cbcs_7_7_video_1.m4s^headers^
+ bipbop_cbcs_7_7_video_init.mp4
+ bipbop_cbcs_7_7_video_init.mp4^headers^
+ bipbop_cbcs_9_8_audio_1.m4s
+ bipbop_cbcs_9_8_audio_1.m4s^headers^
+ bipbop_cbcs_9_8_audio_init.mp4
+ bipbop_cbcs_9_8_audio_init.mp4^headers^
+ bipbop_cbcs_9_8_video_1.m4s
+ bipbop_cbcs_9_8_video_1.m4s^headers^
+ bipbop_cbcs_9_8_video_init.mp4
+ bipbop_cbcs_9_8_video_init.mp4^headers^
+ bipbop_cbcs_10_0_audio_1.m4s
+ bipbop_cbcs_10_0_audio_1.m4s^headers^
+ bipbop_cbcs_10_0_audio_init.mp4
+ bipbop_cbcs_10_0_audio_init.mp4^headers^
+ bipbop_cbcs_10_0_video_1.m4s
+ bipbop_cbcs_10_0_video_1.m4s^headers^
+ bipbop_cbcs_10_0_video_init.mp4
+ bipbop_cbcs_10_0_video_init.mp4^headers^
+ bipbop_short_pixel_metadata_bigger_than_in_stream_vp8.webm
+ bipbop_short_pixel_metadata_bigger_than_in_stream_vp8.webm^headers^
+ bipbop_short_pixel_metadata_narrower_than_in_stream_vp8.webm
+ bipbop_short_pixel_metadata_narrower_than_in_stream_vp8.webm^headers^
+ bipbop_short_pixel_metadata_smaller_than_in_stream_vp8.webm^headers^
+ bipbop_short_pixel_metadata_smaller_than_in_stream_vp8.webm
+ bipbop_short_vp8.webm
+ bipbop_short_vp8.webm^headers^
+ bipbop-lateaudio.mp4
+ bipbop-lateaudio.mp4^headers^
+ black100x100-aspect3to2.ogv
+ black100x100-aspect3to2.ogv^headers^
+ bogus.duh
+ bogus.ogv
+ bogus.ogv^headers^
+ bogus.wav
+ bogus.wav^headers^
+ bug461281.ogg
+ bug461281.ogg^headers^
+ bug482461-theora.ogv
+ bug482461-theora.ogv^headers^
+ bug482461.ogv
+ bug482461.ogv^headers^
+ bug495129.ogv
+ bug495129.ogv^headers^
+ bug495794.ogg
+ bug495794.ogg^headers^
+ bug498380.ogv
+ bug498380.ogv^headers^
+ bug498855-1.ogv
+ bug498855-1.ogv^headers^
+ bug498855-2.ogv
+ bug498855-2.ogv^headers^
+ bug498855-3.ogv
+ bug498855-3.ogv^headers^
+ bug499519.ogv
+ bug499519.ogv^headers^
+ bug500311.ogv
+ bug500311.ogv^headers^
+ bug501279.ogg
+ bug501279.ogg^headers^
+ bug504613.ogv
+ bug504613.ogv^headers^
+ bug504644.ogv
+ bug504644.ogv^headers^
+ bug504843.ogv
+ bug504843.ogv^headers^
+ bug506094.ogv
+ bug506094.ogv^headers^
+ bug516323.indexed.ogv
+ bug516323.indexed.ogv^headers^
+ bug516323.ogv
+ bug516323.ogv^headers^
+ bug520493.ogg
+ bug520493.ogg^headers^
+ bug520500.ogg
+ bug520500.ogg^headers^
+ bug520908.ogv
+ bug520908.ogv^headers^
+ bug523816.ogv
+ bug523816.ogv^headers^
+ bug533822.ogg
+ bug533822.ogg^headers^
+ bug556821.ogv
+ bug556821.ogv^headers^
+ bug557094.ogv
+ bug557094.ogv^headers^
+ bug603918.webm
+ bug603918.webm^headers^
+ bug604067.webm
+ bug604067.webm^headers^
+ bug1066943.webm
+ bug1066943.webm^headers^
+ bug1301226.wav
+ bug1301226.wav^headers^
+ bug1301226-odd.wav
+ bug1301226-odd.wav^headers^
+ bug1377278.webm
+ bug1377278.webm^headers^
+ bunny.webm
+ can_play_type_dash.js
+ can_play_type_ogg.js
+ can_play_type_wave.js
+ can_play_type_webm.js
+ cancellable_request.sjs
+ chain.ogg
+ chain.ogg^headers^
+ chain.ogv
+ chain.ogv^headers^
+ chain.opus
+ chain.opus^headers^
+ chained-audio-video.ogg
+ chained-audio-video.ogg^headers^
+ chained-video.ogv
+ chained-video.ogv^headers^
+ chromeHelper.js
+ cloneElementVisually_helpers.js
+ contentType.sjs
+ detodos.opus
+ detodos.opus^headers^
+ detodos.webm
+ detodos.webm^headers^
+ detodos-short.webm
+ detodos-short.webm^headers^
+ detodos-recorder-test.opus
+ detodos-recorder-test.opus^headers^
+ detodos-short.opus
+ detodos-short.opus^headers^
+ dirac.ogg
+ dirac.ogg^headers^
+ dynamic_resource.sjs
+ eme_standalone.js
+ eme.js
+ empty_size.mp3
+ file_access_controls.html
+ file_eme_createMediaKeys.html
+ flac-s24.flac
+ flac-s24.flac^headers^
+ flac-noheader-s16.flac
+ flac-noheader-s16.flac^headers^
+ flac-sample.mp4
+ flac-sample.mp4^headers^
+ flac-sample-cenc.mp4
+ flac-sample-cenc.mp4^headers^
+ fragment_noplay.js
+ fragment_play.js
+ gizmo.mp4
+ gizmo.mp4^headers^
+ gizmo-noaudio.mp4
+ gizmo-noaudio.mp4^headers^
+ gizmo-short.mp4
+ gizmo-short.mp4^headers^
+ gizmo.webm
+ gizmo.webm^headers^
+ gizmo-noaudio.webm
+ gizmo-noaudio.webm^headers^
+ gUM_support.js
+ gzipped_mp4.sjs
+ huge-id3.mp3
+ huge-id3.mp3^headers^
+ id3tags.mp3
+ id3tags.mp3^headers^
+ invalid-cmap-s0c0.opus
+ invalid-cmap-s0c0.opus^headers^
+ invalid-cmap-s0c2.opus
+ invalid-cmap-s0c2.opus^headers^
+ invalid-cmap-s1c2.opus
+ invalid-cmap-s1c2.opus^headers^
+ invalid-cmap-short.opus
+ invalid-cmap-short.opus^headers^
+ invalid-discard_on_multi_blocks.webm
+ invalid-discard_on_multi_blocks.webm^headers^
+ invalid-excess_discard.webm
+ invalid-excess_discard.webm^headers^
+ invalid-excess_neg_discard.webm
+ invalid-excess_neg_discard.webm^headers^
+ invalid-m0c0.opus
+ invalid-m0c0.opus^headers^
+ invalid-m0c3.opus
+ invalid-m0c3.opus^headers^
+ invalid-m1c0.opus
+ invalid-m1c0.opus^headers^
+ invalid-m1c9.opus
+ invalid-m1c9.opus^headers^
+ invalid-m2c0.opus
+ invalid-m2c0.opus^headers^
+ invalid-m2c1.opus
+ invalid-m2c1.opus^headers^
+ invalid-neg_discard.webm
+ invalid-neg_discard.webm^headers^
+ invalid-preskip.webm
+ invalid-preskip.webm^headers^
+ manifest.js
+ midflight-redirect.sjs
+ multiple-bos.ogg
+ multiple-bos.ogg^headers^
+ multiple-bos-more-header-fileds.ogg
+ multiple-bos-more-header-fileds.ogg^headers^
+ multi_id3v2.mp3
+ no-container-codec-delay.webm
+ no-cues.webm
+ no-cues.webm^headers^
+ notags.mp3
+ notags.mp3^headers^
+ opus-mapping2.mp4
+ opus-mapping2.mp4^headers^
+ opus-mapping2.webm
+ opus-mapping2.webm^headers^
+ opus-sample.mp4
+ opus-sample.mp4^headers^
+ opus-sample-cenc.mp4
+ opus-sample-cenc.mp4^headers^
+ owl-funnier-id3.mp3
+ owl-funnier-id3.mp3^headers^
+ owl-funny-id3.mp3
+ owl-funny-id3.mp3^headers^
+ owl.mp3
+ owl.mp3^headers^
+ owl-short.mp3
+ owl-short.mp3^headers^
+ pixel_aspect_ratio.mp4
+ play_promise.js
+ poster-test.jpg
+ r11025_msadpcm_c1.wav
+ r11025_msadpcm_c1.wav^headers^
+ r11025_s16_c1.wav
+ r11025_s16_c1.wav^headers^
+ r11025_s16_c1_trailing.wav
+ r11025_s16_c1_trailing.wav^headers^
+ r11025_s16_c1-short.wav
+ r11025_s16_c1-short.wav^headers^
+ r11025_u8_c1.wav
+ r11025_u8_c1.wav^headers^
+ r11025_u8_c1_trunc.wav
+ r11025_u8_c1_trunc.wav^headers^
+ r16000_u8_c1_list.wav
+ r16000_u8_c1_list.wav^headers^
+ reactivate_helper.html
+ red-46x48.mp4
+ red-46x48.mp4^headers^
+ red-48x46.mp4
+ red-48x46.mp4^headers^
+ redirect.sjs
+ referer.sjs
+ resolution-change.webm
+ resolution-change.webm^headers^
+ sample.3gp
+ sample.3g2
+ sample-encrypted-sgpdstbl-sbgptraf.mp4
+ sample-encrypted-sgpdstbl-sbgptraf.mp4^headers^
+ sample-fisbone-skeleton4.ogv
+ sample-fisbone-skeleton4.ogv^headers^
+ sample-fisbone-wrong-header.ogv
+ sample-fisbone-wrong-header.ogv^headers^
+ seek.ogv
+ seek.ogv^headers^
+ seek-short.ogv
+ seek-short.ogv^headers^
+ seek.webm
+ seek.webm^headers^
+ seek-short.webm
+ seek-short.webm^headers^
+ seek_support.js
+ seekLies.sjs
+ seek_with_sound.ogg^headers^
+ short-cenc.mp4
+ sine.webm
+ sine.webm^headers^
+ sintel-short-clearkey-subsample-encrypted-audio.webm
+ sintel-short-clearkey-subsample-encrypted-audio.webm^headers^
+ sintel-short-clearkey-subsample-encrypted-video.webm
+ sintel-short-clearkey-subsample-encrypted-video.webm^headers^
+ short.mp4
+ short.mp4.gz
+ short.mp4^headers^
+ short-aac-encrypted-audio.mp4
+ short-aac-encrypted-audio.mp4^headers^
+ short-audio-fragmented-cenc-without-pssh.mp4
+ short-audio-fragmented-cenc-without-pssh.mp4^headers^
+ short-video.ogv
+ short-video.ogv^headers^
+ short-vp9-encrypted-video.mp4
+ short-vp9-encrypted-video.mp4^headers^
+ small-shot-mp3.mp4
+ small-shot-mp3.mp4^headers^
+ small-shot.m4a
+ small-shot.mp3
+ small-shot.mp3^headers^
+ small-shot.ogg
+ small-shot.ogg^headers^
+ small-shot.flac
+ sound.ogg
+ sound.ogg^headers^
+ spacestorm-1000Hz-100ms.ogg
+ spacestorm-1000Hz-100ms.ogg^headers^
+ split.webm
+ split.webm^headers^
+ street.mp4
+ street.mp4^headers^
+ test-1-mono.opus
+ test-1-mono.opus^headers^
+ test-2-stereo.opus
+ test-2-stereo.opus^headers^
+ test-3-LCR.opus
+ test-3-LCR.opus^headers^
+ test-4-quad.opus
+ test-4-quad.opus^headers^
+ test-5-5.0.opus
+ test-5-5.0.opus^headers^
+ test-6-5.1.opus
+ test-6-5.1.opus^headers^
+ test-7-6.1.opus
+ test-7-6.1.opus^headers^
+ test-8-7.1.opus
+ test-8-7.1.opus^headers^
+ test-stereo-phase-inversion-180.opus
+ test-stereo-phase-inversion-180.opus^headers^
+ variable-channel.ogg
+ variable-channel.ogg^headers^
+ variable-channel.opus
+ variable-channel.opus^headers^
+ variable-preskip.opus
+ variable-preskip.opus^headers^
+ variable-samplerate.ogg
+ variable-samplerate.ogg^headers^
+ variable-samplerate.opus
+ variable-samplerate.opus^headers^
+ vbr-head.mp3
+ vbr-head.mp3^headers^
+ vbr.mp3
+ vbr.mp3^headers^
+ very-short.mp3
+ video-overhang.ogg
+ video-overhang.ogg^headers^
+ vp9-superframes.webm
+ vp9-superframes.webm^headers^
+ vp9.webm
+ vp9.webm^headers^
+ vp9-short.webm
+ vp9-short.webm^headers^
+ vp9cake.webm
+ vp9cake.webm^headers^
+ vp9cake-short.webm
+ vp9cake-short.webm^headers^
+ wave_metadata.wav
+ wave_metadata.wav^headers^
+ wave_metadata_bad_len.wav
+ wave_metadata_bad_len.wav^headers^
+ wave_metadata_bad_no_null.wav
+ wave_metadata_bad_no_null.wav^headers^
+ wave_metadata_bad_utf8.wav
+ wave_metadata_bad_utf8.wav^headers^
+ wave_metadata_unknown_tag.wav
+ wave_metadata_unknown_tag.wav^headers^
+ wave_metadata_utf8.wav
+ wave_metadata_utf8.wav^headers^
+ wavedata_alaw.wav
+ wavedata_alaw.wav^headers^
+ wavedata_float.wav
+ wavedata_float.wav^headers^
+ wavedata_s24.wav
+ wavedata_s24.wav^headers^
+ wavedata_s16.wav
+ wavedata_s16.wav^headers^
+ wavedata_u8.wav
+ wavedata_u8.wav^headers^
+ wavedata_ulaw.wav
+ wavedata_ulaw.wav^headers^
+ !/dom/canvas/test/captureStream_common.js
+ !/dom/html/test/reflect.js
+ !/dom/media/webrtc/tests/mochitests/head.js
+ hls/bipbop_16x9_single.m3u8
+ hls/bipbop_4x3_single.m3u8
+ hls/bipbop_4x3_variant.m3u8
+ hls/400x300_prog_index.m3u8
+ hls/400x300_prog_index_5s.m3u8
+ hls/416x243_prog_index_5s.m3u8
+ hls/640x480_prog_index.m3u8
+ hls/960x720_prog_index.m3u8
+ hls/400x300_seg0.ts
+ hls/400x300_seg0_5s.ts
+ hls/400x300_seg1.ts
+ hls/416x243_seg0_5s.ts
+ hls/640x480_seg0.ts
+ hls/640x480_seg1.ts
+ hls/960x720_seg0.ts
+ hls/960x720_seg1.ts
+ sync.webm
+
+[test_eme_autoplay.html]
+skip-if =
+ toolkit == 'android' # bug 1149374
+ win10_2004 && asan # Bug 1718297
+scheme=https
+[test_eme_canvas_blocked.html]
+skip-if =
+ toolkit == 'android' # bug 1149374
+ apple_silicon # bug 1707737
+ win10_2004 && asan # Bug 1718297
+scheme=https
+[test_eme_createMediaKeys_iframes.html]
+skip-if =
+ toolkit == 'android' # bug 1149374
+ win10_2004 && asan # Bug 1718297
+scheme=https
+[test_eme_detach_media_keys.html]
+skip-if =
+ toolkit == 'android'
+ win10_2004 && asan # Bug 1718297
+scheme=https
+[test_eme_detach_reattach_same_mediakeys_during_playback.html]
+skip-if =
+ toolkit == 'android' # bug 1149374
+ win10_2004 && asan # Bug 1718297
+scheme=https
+[test_eme_getstatusforpolicy.html]
+skip-if =
+ toolkit == 'android' # bug 1149374
+ win10_2004 && asan # Bug 1718297
+scheme=https
+[test_eme_initDataTypes.html]
+skip-if =
+ toolkit == 'android'
+ win10_2004 && asan # Bug 1718297
+scheme=https
+[test_eme_missing_pssh.html]
+skip-if = toolkit == 'android'
+scheme=https
+[test_eme_non_mse_fails.html]
+skip-if =
+ toolkit == 'android' # bug 1149374
+ win10_2004 && asan # Bug 1718297
+scheme=https
+[test_eme_playback.html]
+skip-if =
+ toolkit == 'android' # bug 1149374
+ apple_silicon # bug 1707737
+ win10_2004 && asan # Bug 1718297
+[test_eme_protection_query.html]
+skip-if = toolkit == 'android' # bug 1149374
+scheme=https
+[test_eme_pssh_in_moof.html]
+skip-if =
+ toolkit == 'android' # bug 1149374
+ win10_2004 && asan # Bug 1718297
+scheme=https
+[test_eme_requestKeySystemAccess.html]
+skip-if = toolkit == 'android' # bug 1149374
+scheme=https
+[test_eme_requestMediaKeySystemAccess_with_app_approval.html]
+skip-if = toolkit == 'android' # bug 1149374
+scheme=https
+[test_eme_request_notifications.html]
+skip-if =
+ toolkit == 'android'
+ win10_2004 && asan # Bug 1718297
+scheme=https
+[test_eme_sample_groups_playback.html]
+skip-if = toolkit == 'android' # bug 1149374
+scheme=https
+[test_eme_session_callable_value.html]
+scheme=https
+[test_eme_setMediaKeys_before_attach_MediaSource.html]
+skip-if = toolkit == 'android' # bug 1149374
+scheme=https
+[test_eme_special_key_system.html]
+skip-if = toolkit == 'android' # bug 1149374
+scheme=https
+[test_eme_stream_capture_blocked_case1.html]
+tags=mtg capturestream
+skip-if = toolkit == 'android' # bug 1149374
+scheme=https
+[test_eme_stream_capture_blocked_case2.html]
+tags=mtg capturestream
+skip-if =
+ toolkit == 'android' # bug 1149374
+ apple_silicon # bug 1707737
+scheme=https
+[test_eme_stream_capture_blocked_case3.html]
+tags=mtg capturestream
+skip-if =
+ toolkit == 'android' # bug 1149374
+ apple_silicon # bug 1707737
+scheme=https
+[test_eme_unsetMediaKeys_then_capture.html]
+skip-if =
+ xorigin
+ toolkit == 'android' # bug 1149374
+ apple_silicon # bug 1707737
+scheme=https
+[test_eme_waitingforkey.html]
+skip-if =
+ xorigin
+ toolkit == 'android' # bug 1149374
+ apple_silicon # bug 1707737
+scheme=https
+[test_eme_wv_privacy.html]
+scheme=https
diff --git a/dom/media/test/mochitest_media_recorder.ini b/dom/media/test/mochitest_media_recorder.ini
new file mode 100644
index 0000000000..066f7ba980
--- /dev/null
+++ b/dom/media/test/mochitest_media_recorder.ini
@@ -0,0 +1,826 @@
+# Media tests should be backend independent, i.e., not conditioned on ogg,
+# wave etc. (The only exception is the can_play_type tests, which
+# necessarily depend on the backend(s) configured.) As far as possible, each
+# test should work with any resource type. This makes it easy to add new
+# backends and reduces the amount of test duplication.
+
+# For each supported backend, resources that can be played by that backend
+# should be added to the lists in manifest.js. Media tests that aren't
+# testing for a bug in handling a specific resource type should pick one of
+# the lists in manifest.js and run the test for each resource in the list
+# that is supported in the current build (the canPlayType API is useful for
+# this).
+
+# To test whether a valid resource can simply be played through correctly,
+# and optionally that its metadata is read correctly, just add it to
+# gPlayTests in manifest.js. To test whether an invalid resource correctly
+# throws an error (and does not cause a crash or hang), just add it to
+# gErrorTests in manifest.js.
+
+# To test for a specific bug in handling a specific resource type, make the
+# test first check canPlayType for the type, and if it's not supported, just
+# do ok(true, "Type not supported") and stop the test.
+
+[DEFAULT]
+subsuite = media
+tags=mtg
+skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1536604
+support-files =
+ 16bit_wave_extrametadata.wav
+ 16bit_wave_extrametadata.wav^headers^
+ 320x240.ogv
+ 320x240.ogv^headers^
+ 448636.ogv
+ 448636.ogv^headers^
+ A4.ogv
+ A4.ogv^headers^
+ VID_0001.ogg
+ VID_0001.ogg^headers^
+ allowed.sjs
+ ambisonics.mp4
+ ambisonics.mp4^headers^
+ audio-gaps.ogg
+ audio-gaps.ogg^headers^
+ audio-gaps-short.ogg
+ audio-gaps-short.ogg^headers^
+ audio-overhang.ogg
+ audio-overhang.ogg^headers^
+ audio.wav
+ audio.wav^headers^
+ av1.mp4
+ av1.mp4^headers^
+ background_video.js
+ badtags.ogg
+ badtags.ogg^headers^
+ bear-640x360-v_frag-cenc-key_rotation.mp4
+ bear-640x360-a_frag-cenc-key_rotation.mp4
+ beta-phrasebook.ogg
+ beta-phrasebook.ogg^headers^
+ big.wav
+ big.wav^headers^
+ big-buck-bunny-cenc-avc3-1.m4s
+ big-buck-bunny-cenc-avc3-1.m4s^headers^
+ big-buck-bunny-cenc-avc3-init.mp4
+ big-buck-bunny-cenc-avc3-init.mp4^headers^
+ big-short.wav
+ big-short.wav^headers^
+ bipbop.mp4
+ bipbop-cenc-audio1.m4s
+ bipbop-cenc-audio1.m4s^headers^
+ bipbop-cenc-audio2.m4s
+ bipbop-cenc-audio2.m4s^headers^
+ bipbop-cenc-audio3.m4s
+ bipbop-cenc-audio3.m4s^headers^
+ bipbop-cenc-audioinit.mp4
+ bipbop-cenc-audioinit.mp4^headers^
+ bipbop-cenc-video1.m4s
+ bipbop-cenc-video1.m4s^headers^
+ bipbop-cenc-video2.m4s
+ bipbop-cenc-video2.m4s^headers^
+ bipbop-cenc-videoinit.mp4
+ bipbop-cenc-videoinit.mp4^headers^
+ bipbop-cenc-video-10s.mp4
+ bipbop-cenc-video-10s.mp4^headers^
+ bipbop-clearkey-keyrotation-clear-lead-audio.mp4
+ bipbop-clearkey-keyrotation-clear-lead-audio.mp4^headers^
+ bipbop-clearkey-keyrotation-clear-lead-video.mp4
+ bipbop-clearkey-keyrotation-clear-lead-video.mp4^headers^
+ bipbop_225w_175kbps.mp4
+ bipbop_225w_175kbps.mp4^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-1.m4s
+ bipbop_225w_175kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-2.m4s
+ bipbop_225w_175kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-3.m4s
+ bipbop_225w_175kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-4.m4s
+ bipbop_225w_175kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-init.mp4
+ bipbop_225w_175kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-1.m4s
+ bipbop_225w_175kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-2.m4s
+ bipbop_225w_175kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-3.m4s
+ bipbop_225w_175kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-4.m4s
+ bipbop_225w_175kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-init.mp4
+ bipbop_225w_175kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_225w_175kbps-cenc-video-key1-1.m4s
+ bipbop_225w_175kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_225w_175kbps-cenc-video-key1-init.mp4
+ bipbop_225w_175kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_225w_175kbps-cenc-video-key2-1.m4s
+ bipbop_225w_175kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_225w_175kbps-cenc-video-key2-init.mp4
+ bipbop_225w_175kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_300_215kbps-cenc-audio-key1-1.m4s
+ bipbop_300_215kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key1-2.m4s
+ bipbop_300_215kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key1-3.m4s
+ bipbop_300_215kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key1-4.m4s
+ bipbop_300_215kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key1-init.mp4
+ bipbop_300_215kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_300_215kbps-cenc-audio-key2-1.m4s
+ bipbop_300_215kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key2-2.m4s
+ bipbop_300_215kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key2-3.m4s
+ bipbop_300_215kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key2-4.m4s
+ bipbop_300_215kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key2-init.mp4
+ bipbop_300_215kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_300_215kbps-cenc-video-key1-1.m4s
+ bipbop_300_215kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_300_215kbps-cenc-video-key1-2.m4s
+ bipbop_300_215kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_300_215kbps-cenc-video-key1-init.mp4
+ bipbop_300_215kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_300_215kbps-cenc-video-key2-1.m4s
+ bipbop_300_215kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_300_215kbps-cenc-video-key2-2.m4s
+ bipbop_300_215kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_300_215kbps-cenc-video-key2-init.mp4
+ bipbop_300_215kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-1.m4s
+ bipbop_300wp_227kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-2.m4s
+ bipbop_300wp_227kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-3.m4s
+ bipbop_300wp_227kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-4.m4s
+ bipbop_300wp_227kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-init.mp4
+ bipbop_300wp_227kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-1.m4s
+ bipbop_300wp_227kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-2.m4s
+ bipbop_300wp_227kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-3.m4s
+ bipbop_300wp_227kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-4.m4s
+ bipbop_300wp_227kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-init.mp4
+ bipbop_300wp_227kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_300wp_227kbps-cenc-video-key1-1.m4s
+ bipbop_300wp_227kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_300wp_227kbps-cenc-video-key1-2.m4s
+ bipbop_300wp_227kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_300wp_227kbps-cenc-video-key1-init.mp4
+ bipbop_300wp_227kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_300wp_227kbps-cenc-video-key2-1.m4s
+ bipbop_300wp_227kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_300wp_227kbps-cenc-video-key2-2.m4s
+ bipbop_300wp_227kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_300wp_227kbps-cenc-video-key2-init.mp4
+ bipbop_300wp_227kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-1.m4s
+ bipbop_360w_253kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-2.m4s
+ bipbop_360w_253kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-3.m4s
+ bipbop_360w_253kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-4.m4s
+ bipbop_360w_253kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-init.mp4
+ bipbop_360w_253kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-1.m4s
+ bipbop_360w_253kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-2.m4s
+ bipbop_360w_253kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-3.m4s
+ bipbop_360w_253kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-4.m4s
+ bipbop_360w_253kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-init.mp4
+ bipbop_360w_253kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_360w_253kbps-cenc-video-key1-1.m4s
+ bipbop_360w_253kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_360w_253kbps-cenc-video-key1-init.mp4
+ bipbop_360w_253kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_360w_253kbps-cenc-video-key2-1.m4s
+ bipbop_360w_253kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_360w_253kbps-cenc-video-key2-init.mp4
+ bipbop_360w_253kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_360w_253kbps-clearkey-audio.webm
+ bipbop_360w_253kbps-clearkey-audio.webm^headers^
+ bipbop_360w_253kbps-clearkey-video-vp8.webm
+ bipbop_360w_253kbps-clearkey-video-vp8.webm^headers^
+ bipbop_360w_253kbps-clearkey-video-vp9.webm
+ bipbop_360w_253kbps-clearkey-video-vp9.webm^headers^
+ bipbop_480_624kbps-cenc-audio-key1-1.m4s
+ bipbop_480_624kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key1-2.m4s
+ bipbop_480_624kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key1-3.m4s
+ bipbop_480_624kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key1-4.m4s
+ bipbop_480_624kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key1-init.mp4
+ bipbop_480_624kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_480_624kbps-cenc-audio-key2-1.m4s
+ bipbop_480_624kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key2-2.m4s
+ bipbop_480_624kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key2-3.m4s
+ bipbop_480_624kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key2-4.m4s
+ bipbop_480_624kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key2-init.mp4
+ bipbop_480_624kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_480_624kbps-cenc-video-key1-1.m4s
+ bipbop_480_624kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_480_624kbps-cenc-video-key1-2.m4s
+ bipbop_480_624kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_480_624kbps-cenc-video-key1-init.mp4
+ bipbop_480_624kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_480_624kbps-cenc-video-key2-1.m4s
+ bipbop_480_624kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_480_624kbps-cenc-video-key2-2.m4s
+ bipbop_480_624kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_480_624kbps-cenc-video-key2-init.mp4
+ bipbop_480_624kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_480_959kbps-cenc-audio-key1-1.m4s
+ bipbop_480_959kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key1-2.m4s
+ bipbop_480_959kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key1-3.m4s
+ bipbop_480_959kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key1-4.m4s
+ bipbop_480_959kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key1-init.mp4
+ bipbop_480_959kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_480_959kbps-cenc-audio-key2-1.m4s
+ bipbop_480_959kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key2-2.m4s
+ bipbop_480_959kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key2-3.m4s
+ bipbop_480_959kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key2-4.m4s
+ bipbop_480_959kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key2-init.mp4
+ bipbop_480_959kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_480_959kbps-cenc-video-key1-1.m4s
+ bipbop_480_959kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_480_959kbps-cenc-video-key1-2.m4s
+ bipbop_480_959kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_480_959kbps-cenc-video-key1-init.mp4
+ bipbop_480_959kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_480_959kbps-cenc-video-key2-1.m4s
+ bipbop_480_959kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_480_959kbps-cenc-video-key2-2.m4s
+ bipbop_480_959kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_480_959kbps-cenc-video-key2-init.mp4
+ bipbop_480_959kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-1.m4s
+ bipbop_480wp_663kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-2.m4s
+ bipbop_480wp_663kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-3.m4s
+ bipbop_480wp_663kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-4.m4s
+ bipbop_480wp_663kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-init.mp4
+ bipbop_480wp_663kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-1.m4s
+ bipbop_480wp_663kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-2.m4s
+ bipbop_480wp_663kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-3.m4s
+ bipbop_480wp_663kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-4.m4s
+ bipbop_480wp_663kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-init.mp4
+ bipbop_480wp_663kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_480wp_663kbps-cenc-video-key1-1.m4s
+ bipbop_480wp_663kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_480wp_663kbps-cenc-video-key1-2.m4s
+ bipbop_480wp_663kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_480wp_663kbps-cenc-video-key1-init.mp4
+ bipbop_480wp_663kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_480wp_663kbps-cenc-video-key2-1.m4s
+ bipbop_480wp_663kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_480wp_663kbps-cenc-video-key2-2.m4s
+ bipbop_480wp_663kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_480wp_663kbps-cenc-video-key2-init.mp4
+ bipbop_480wp_663kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-1.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-2.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-3.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-4.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-init.mp4
+ bipbop_480wp_1001kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-1.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-2.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-3.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-4.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-init.mp4
+ bipbop_480wp_1001kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_480wp_1001kbps-cenc-video-key1-1.m4s
+ bipbop_480wp_1001kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-video-key1-2.m4s
+ bipbop_480wp_1001kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-video-key1-init.mp4
+ bipbop_480wp_1001kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_480wp_1001kbps-cenc-video-key2-1.m4s
+ bipbop_480wp_1001kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-video-key2-2.m4s
+ bipbop_480wp_1001kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-video-key2-init.mp4
+ bipbop_480wp_1001kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_audio_aac_8k.mp4
+ bipbop_audio_aac_8k.mp4^headers^
+ bipbop_audio_aac_22.05k.mp4
+ bipbop_audio_aac_22.05k.mp4^headers^
+ bipbop_audio_aac_44.1k.mp4
+ bipbop_audio_aac_44.1k.mp4^headers^
+ bipbop_audio_aac_48k.mp4
+ bipbop_audio_aac_48k.mp4^headers^
+ bipbop_audio_aac_88.2k.mp4
+ bipbop_audio_aac_88.2k.mp4^headers^
+ bipbop_audio_aac_96k.mp4
+ bipbop_audio_aac_96k.mp4^headers^
+ bipbop_cbcs_1_9_audio_1.m4s
+ bipbop_cbcs_1_9_audio_1.m4s^headers^
+ bipbop_cbcs_1_9_audio_init.mp4
+ bipbop_cbcs_1_9_audio_init.mp4^headers^
+ bipbop_cbcs_1_9_video_1.m4s
+ bipbop_cbcs_1_9_video_1.m4s^headers^
+ bipbop_cbcs_1_9_video_init.mp4
+ bipbop_cbcs_1_9_video_init.mp4^headers^
+ bipbop_cbcs_5_5_audio_1.m4s
+ bipbop_cbcs_5_5_audio_1.m4s^headers^
+ bipbop_cbcs_5_5_audio_init.mp4
+ bipbop_cbcs_5_5_audio_init.mp4^headers^
+ bipbop_cbcs_5_5_video_1.m4s
+ bipbop_cbcs_5_5_video_1.m4s^headers^
+ bipbop_cbcs_5_5_video_init.mp4
+ bipbop_cbcs_5_5_video_init.mp4^headers^
+ bipbop_cbcs_7_7_audio_1.m4s
+ bipbop_cbcs_7_7_audio_1.m4s^headers^
+ bipbop_cbcs_7_7_audio_init.mp4
+ bipbop_cbcs_7_7_audio_init.mp4^headers^
+ bipbop_cbcs_7_7_video_1.m4s
+ bipbop_cbcs_7_7_video_1.m4s^headers^
+ bipbop_cbcs_7_7_video_init.mp4
+ bipbop_cbcs_7_7_video_init.mp4^headers^
+ bipbop_cbcs_9_8_audio_1.m4s
+ bipbop_cbcs_9_8_audio_1.m4s^headers^
+ bipbop_cbcs_9_8_audio_init.mp4
+ bipbop_cbcs_9_8_audio_init.mp4^headers^
+ bipbop_cbcs_9_8_video_1.m4s
+ bipbop_cbcs_9_8_video_1.m4s^headers^
+ bipbop_cbcs_9_8_video_init.mp4
+ bipbop_cbcs_9_8_video_init.mp4^headers^
+ bipbop_cbcs_10_0_audio_1.m4s
+ bipbop_cbcs_10_0_audio_1.m4s^headers^
+ bipbop_cbcs_10_0_audio_init.mp4
+ bipbop_cbcs_10_0_audio_init.mp4^headers^
+ bipbop_cbcs_10_0_video_1.m4s
+ bipbop_cbcs_10_0_video_1.m4s^headers^
+ bipbop_cbcs_10_0_video_init.mp4
+ bipbop_cbcs_10_0_video_init.mp4^headers^
+ bipbop_short_pixel_metadata_bigger_than_in_stream_vp8.webm
+ bipbop_short_pixel_metadata_bigger_than_in_stream_vp8.webm^headers^
+ bipbop_short_pixel_metadata_narrower_than_in_stream_vp8.webm
+ bipbop_short_pixel_metadata_narrower_than_in_stream_vp8.webm^headers^
+ bipbop_short_pixel_metadata_smaller_than_in_stream_vp8.webm^headers^
+ bipbop_short_pixel_metadata_smaller_than_in_stream_vp8.webm
+ bipbop_short_vp8.webm
+ bipbop_short_vp8.webm^headers^
+ bipbop-lateaudio.mp4
+ bipbop-lateaudio.mp4^headers^
+ black100x100-aspect3to2.ogv
+ black100x100-aspect3to2.ogv^headers^
+ bogus.duh
+ bogus.ogv
+ bogus.ogv^headers^
+ bogus.wav
+ bogus.wav^headers^
+ bug461281.ogg
+ bug461281.ogg^headers^
+ bug482461-theora.ogv
+ bug482461-theora.ogv^headers^
+ bug482461.ogv
+ bug482461.ogv^headers^
+ bug495129.ogv
+ bug495129.ogv^headers^
+ bug495794.ogg
+ bug495794.ogg^headers^
+ bug498380.ogv
+ bug498380.ogv^headers^
+ bug498855-1.ogv
+ bug498855-1.ogv^headers^
+ bug498855-2.ogv
+ bug498855-2.ogv^headers^
+ bug498855-3.ogv
+ bug498855-3.ogv^headers^
+ bug499519.ogv
+ bug499519.ogv^headers^
+ bug500311.ogv
+ bug500311.ogv^headers^
+ bug501279.ogg
+ bug501279.ogg^headers^
+ bug504613.ogv
+ bug504613.ogv^headers^
+ bug504644.ogv
+ bug504644.ogv^headers^
+ bug504843.ogv
+ bug504843.ogv^headers^
+ bug506094.ogv
+ bug506094.ogv^headers^
+ bug516323.indexed.ogv
+ bug516323.indexed.ogv^headers^
+ bug516323.ogv
+ bug516323.ogv^headers^
+ bug520493.ogg
+ bug520493.ogg^headers^
+ bug520500.ogg
+ bug520500.ogg^headers^
+ bug520908.ogv
+ bug520908.ogv^headers^
+ bug523816.ogv
+ bug523816.ogv^headers^
+ bug533822.ogg
+ bug533822.ogg^headers^
+ bug556821.ogv
+ bug556821.ogv^headers^
+ bug557094.ogv
+ bug557094.ogv^headers^
+ bug603918.webm
+ bug603918.webm^headers^
+ bug604067.webm
+ bug604067.webm^headers^
+ bug1066943.webm
+ bug1066943.webm^headers^
+ bug1301226.wav
+ bug1301226.wav^headers^
+ bug1301226-odd.wav
+ bug1301226-odd.wav^headers^
+ bug1377278.webm
+ bug1377278.webm^headers^
+ bunny.webm
+ can_play_type_dash.js
+ can_play_type_ogg.js
+ can_play_type_wave.js
+ can_play_type_webm.js
+ cancellable_request.sjs
+ chain.ogg
+ chain.ogg^headers^
+ chain.ogv
+ chain.ogv^headers^
+ chain.opus
+ chain.opus^headers^
+ chained-audio-video.ogg
+ chained-audio-video.ogg^headers^
+ chained-video.ogv
+ chained-video.ogv^headers^
+ chromeHelper.js
+ cloneElementVisually_helpers.js
+ contentType.sjs
+ detodos.opus
+ detodos.opus^headers^
+ detodos.webm
+ detodos.webm^headers^
+ detodos-short.webm
+ detodos-short.webm^headers^
+ detodos-recorder-test.opus
+ detodos-recorder-test.opus^headers^
+ detodos-short.opus
+ detodos-short.opus^headers^
+ dirac.ogg
+ dirac.ogg^headers^
+ dynamic_resource.sjs
+ eme_standalone.js
+ eme.js
+ empty_size.mp3
+ file_access_controls.html
+ file_eme_createMediaKeys.html
+ flac-s24.flac
+ flac-s24.flac^headers^
+ flac-noheader-s16.flac
+ flac-noheader-s16.flac^headers^
+ flac-sample.mp4
+ flac-sample.mp4^headers^
+ flac-sample-cenc.mp4
+ flac-sample-cenc.mp4^headers^
+ fragment_noplay.js
+ fragment_play.js
+ gizmo.mp4
+ gizmo.mp4^headers^
+ gizmo-noaudio.mp4
+ gizmo-noaudio.mp4^headers^
+ gizmo-short.mp4
+ gizmo-short.mp4^headers^
+ gizmo.webm
+ gizmo.webm^headers^
+ gizmo-noaudio.webm
+ gizmo-noaudio.webm^headers^
+ gUM_support.js
+ gzipped_mp4.sjs
+ huge-id3.mp3
+ huge-id3.mp3^headers^
+ id3tags.mp3
+ id3tags.mp3^headers^
+ invalid-cmap-s0c0.opus
+ invalid-cmap-s0c0.opus^headers^
+ invalid-cmap-s0c2.opus
+ invalid-cmap-s0c2.opus^headers^
+ invalid-cmap-s1c2.opus
+ invalid-cmap-s1c2.opus^headers^
+ invalid-cmap-short.opus
+ invalid-cmap-short.opus^headers^
+ invalid-discard_on_multi_blocks.webm
+ invalid-discard_on_multi_blocks.webm^headers^
+ invalid-excess_discard.webm
+ invalid-excess_discard.webm^headers^
+ invalid-excess_neg_discard.webm
+ invalid-excess_neg_discard.webm^headers^
+ invalid-m0c0.opus
+ invalid-m0c0.opus^headers^
+ invalid-m0c3.opus
+ invalid-m0c3.opus^headers^
+ invalid-m1c0.opus
+ invalid-m1c0.opus^headers^
+ invalid-m1c9.opus
+ invalid-m1c9.opus^headers^
+ invalid-m2c0.opus
+ invalid-m2c0.opus^headers^
+ invalid-m2c1.opus
+ invalid-m2c1.opus^headers^
+ invalid-neg_discard.webm
+ invalid-neg_discard.webm^headers^
+ invalid-preskip.webm
+ invalid-preskip.webm^headers^
+ manifest.js
+ midflight-redirect.sjs
+ multiple-bos.ogg
+ multiple-bos.ogg^headers^
+ multiple-bos-more-header-fileds.ogg
+ multiple-bos-more-header-fileds.ogg^headers^
+ multi_id3v2.mp3
+ no-container-codec-delay.webm
+ no-cues.webm
+ no-cues.webm^headers^
+ notags.mp3
+ notags.mp3^headers^
+ opus-mapping2.mp4
+ opus-mapping2.mp4^headers^
+ opus-mapping2.webm
+ opus-mapping2.webm^headers^
+ opus-sample.mp4
+ opus-sample.mp4^headers^
+ opus-sample-cenc.mp4
+ opus-sample-cenc.mp4^headers^
+ owl-funnier-id3.mp3
+ owl-funnier-id3.mp3^headers^
+ owl-funny-id3.mp3
+ owl-funny-id3.mp3^headers^
+ owl.mp3
+ owl.mp3^headers^
+ owl-short.mp3
+ owl-short.mp3^headers^
+ pixel_aspect_ratio.mp4
+ play_promise.js
+ poster-test.jpg
+ r11025_msadpcm_c1.wav
+ r11025_msadpcm_c1.wav^headers^
+ r11025_s16_c1.wav
+ r11025_s16_c1.wav^headers^
+ r11025_s16_c1_trailing.wav
+ r11025_s16_c1_trailing.wav^headers^
+ r11025_s16_c1-short.wav
+ r11025_s16_c1-short.wav^headers^
+ r11025_u8_c1.wav
+ r11025_u8_c1.wav^headers^
+ r11025_u8_c1_trunc.wav
+ r11025_u8_c1_trunc.wav^headers^
+ r16000_u8_c1_list.wav
+ r16000_u8_c1_list.wav^headers^
+ reactivate_helper.html
+ red-46x48.mp4
+ red-46x48.mp4^headers^
+ red-48x46.mp4
+ red-48x46.mp4^headers^
+ redirect.sjs
+ referer.sjs
+ resolution-change.webm
+ resolution-change.webm^headers^
+ sample.3gp
+ sample.3g2
+ sample-encrypted-sgpdstbl-sbgptraf.mp4
+ sample-encrypted-sgpdstbl-sbgptraf.mp4^headers^
+ sample-fisbone-skeleton4.ogv
+ sample-fisbone-skeleton4.ogv^headers^
+ sample-fisbone-wrong-header.ogv
+ sample-fisbone-wrong-header.ogv^headers^
+ seek.ogv
+ seek.ogv^headers^
+ seek-short.ogv
+ seek-short.ogv^headers^
+ seek.webm
+ seek.webm^headers^
+ seek-short.webm
+ seek-short.webm^headers^
+ seek_support.js
+ seekLies.sjs
+ seek_with_sound.ogg^headers^
+ short-cenc.mp4
+ sine.webm
+ sine.webm^headers^
+ sintel-short-clearkey-subsample-encrypted-audio.webm
+ sintel-short-clearkey-subsample-encrypted-audio.webm^headers^
+ sintel-short-clearkey-subsample-encrypted-video.webm
+ sintel-short-clearkey-subsample-encrypted-video.webm^headers^
+ short.mp4
+ short.mp4.gz
+ short.mp4^headers^
+ short-aac-encrypted-audio.mp4
+ short-aac-encrypted-audio.mp4^headers^
+ short-audio-fragmented-cenc-without-pssh.mp4
+ short-audio-fragmented-cenc-without-pssh.mp4^headers^
+ short-video.ogv
+ short-video.ogv^headers^
+ short-vp9-encrypted-video.mp4
+ short-vp9-encrypted-video.mp4^headers^
+ small-shot-mp3.mp4
+ small-shot-mp3.mp4^headers^
+ small-shot.m4a
+ small-shot.mp3
+ small-shot.mp3^headers^
+ small-shot.ogg
+ small-shot.ogg^headers^
+ small-shot.flac
+ sound.ogg
+ sound.ogg^headers^
+ spacestorm-1000Hz-100ms.ogg
+ spacestorm-1000Hz-100ms.ogg^headers^
+ split.webm
+ split.webm^headers^
+ street.mp4
+ street.mp4^headers^
+ test-1-mono.opus
+ test-1-mono.opus^headers^
+ test-2-stereo.opus
+ test-2-stereo.opus^headers^
+ test-3-LCR.opus
+ test-3-LCR.opus^headers^
+ test-4-quad.opus
+ test-4-quad.opus^headers^
+ test-5-5.0.opus
+ test-5-5.0.opus^headers^
+ test-6-5.1.opus
+ test-6-5.1.opus^headers^
+ test-7-6.1.opus
+ test-7-6.1.opus^headers^
+ test-8-7.1.opus
+ test-8-7.1.opus^headers^
+ test-stereo-phase-inversion-180.opus
+ test-stereo-phase-inversion-180.opus^headers^
+ variable-channel.ogg
+ variable-channel.ogg^headers^
+ variable-channel.opus
+ variable-channel.opus^headers^
+ variable-preskip.opus
+ variable-preskip.opus^headers^
+ variable-samplerate.ogg
+ variable-samplerate.ogg^headers^
+ variable-samplerate.opus
+ variable-samplerate.opus^headers^
+ vbr-head.mp3
+ vbr-head.mp3^headers^
+ vbr.mp3
+ vbr.mp3^headers^
+ very-short.mp3
+ video-overhang.ogg
+ video-overhang.ogg^headers^
+ vp9-superframes.webm
+ vp9-superframes.webm^headers^
+ vp9.webm
+ vp9.webm^headers^
+ vp9-short.webm
+ vp9-short.webm^headers^
+ vp9cake.webm
+ vp9cake.webm^headers^
+ vp9cake-short.webm
+ vp9cake-short.webm^headers^
+ wave_metadata.wav
+ wave_metadata.wav^headers^
+ wave_metadata_bad_len.wav
+ wave_metadata_bad_len.wav^headers^
+ wave_metadata_bad_no_null.wav
+ wave_metadata_bad_no_null.wav^headers^
+ wave_metadata_bad_utf8.wav
+ wave_metadata_bad_utf8.wav^headers^
+ wave_metadata_unknown_tag.wav
+ wave_metadata_unknown_tag.wav^headers^
+ wave_metadata_utf8.wav
+ wave_metadata_utf8.wav^headers^
+ wavedata_alaw.wav
+ wavedata_alaw.wav^headers^
+ wavedata_float.wav
+ wavedata_float.wav^headers^
+ wavedata_s24.wav
+ wavedata_s24.wav^headers^
+ wavedata_s16.wav
+ wavedata_s16.wav^headers^
+ wavedata_u8.wav
+ wavedata_u8.wav^headers^
+ wavedata_ulaw.wav
+ wavedata_ulaw.wav^headers^
+ !/dom/canvas/test/captureStream_common.js
+ !/dom/html/test/reflect.js
+ !/dom/media/webrtc/tests/mochitests/head.js
+ hls/bipbop_16x9_single.m3u8
+ hls/bipbop_4x3_single.m3u8
+ hls/bipbop_4x3_variant.m3u8
+ hls/400x300_prog_index.m3u8
+ hls/400x300_prog_index_5s.m3u8
+ hls/416x243_prog_index_5s.m3u8
+ hls/640x480_prog_index.m3u8
+ hls/960x720_prog_index.m3u8
+ hls/400x300_seg0.ts
+ hls/400x300_seg0_5s.ts
+ hls/400x300_seg1.ts
+ hls/416x243_seg0_5s.ts
+ hls/640x480_seg0.ts
+ hls/640x480_seg1.ts
+ hls/960x720_seg0.ts
+ hls/960x720_seg1.ts
+ sync.webm
+prefs =
+ media.recorder.video.frame_drops=false
+
+[test_mediacapabilities_resistfingerprinting.html]
+[test_mediarecorder_avoid_recursion.html]
+skip-if = os == 'win' && !debug
+scheme=https
+[test_mediarecorder_bitrate.html]
+skip-if = toolkit == 'android' # bug 1297432, android(bug 1232305)
+[test_mediarecorder_creation.html]
+tags=mtg capturestream
+[test_mediarecorder_creation_fail.html]
+[test_mediarecorder_fires_start_event_once_when_erroring.html]
+[test_mediarecorder_multipletracks.html]
+[test_mediarecorder_onerror_pause.html]
+scheme=https
+[test_mediarecorder_pause_resume_video.html]
+skip-if = toolkit == 'android' # android(bug 1232305)
+[test_mediarecorder_playback_can_repeat.html]
+[test_mediarecorder_principals.html]
+skip-if =
+ os == 'win' && os_version == '10.0' # Bug 1453375
+ os == 'android' # Bug 1694645
+[test_mediarecorder_record_4ch_audiocontext.html]
+skip-if = os == "linux" && bits == 64 #Bug 1598101
+[test_mediarecorder_record_addtracked_stream.html]
+skip-if = toolkit == 'android' # Bug 1408241
+tags=mtg capturestream
+[test_mediarecorder_record_audiocontext.html]
+[test_mediarecorder_record_audiocontext_mlk.html]
+[test_mediarecorder_record_audionode.html]
+[test_mediarecorder_record_canvas_captureStream.html]
+skip-if = toolkit == 'android' # android(bug 1232305)
+[test_mediarecorder_record_changing_video_resolution.html]
+skip-if =
+ toolkit == 'android' # android(bug 1232305)
+ (os == "linux" && (tsan || asan || debug)) # 1770504 (high intermittent failure on Linux tsan)
+[test_mediarecorder_record_upsize_resolution.html]
+skip-if = toolkit == 'android' # android(bug 1232305)
+[test_mediarecorder_record_downsize_resolution.html]
+skip-if = toolkit == 'android' # android(bug 1232305)
+[test_mediarecorder_record_gum_video_timeslice.html]
+scheme=https
+[test_mediarecorder_record_gum_video_timeslice_mixed.html]
+scheme=https
+[test_mediarecorder_record_immediate_stop.html]
+tags=mtg capturestream
+[test_mediarecorder_record_no_timeslice.html]
+tags=mtg capturestream
+[test_mediarecorder_record_session.html]
+tags=mtg capturestream
+[test_mediarecorder_record_startstopstart.html]
+[test_mediarecorder_record_timeslice.html]
+tags=mtg capturestream
+[test_mediarecorder_reload_crash.html]
+tags=mtg capturestream
+[test_mediarecorder_state_transition.html]
+tags=mtg capturestream
+[test_mediarecorder_state_event_order.html]
+tags=mtg capturestream
+[test_mediarecorder_webm_support.html]
+[test_mediarecorder_record_getdata_afterstart.html]
+tags=mtg capturestream
diff --git a/dom/media/test/mochitest_seek.ini b/dom/media/test/mochitest_seek.ini
new file mode 100644
index 0000000000..7ee0501e96
--- /dev/null
+++ b/dom/media/test/mochitest_seek.ini
@@ -0,0 +1,805 @@
+# Media tests should be backend independent, i.e., not conditioned on ogg,
+# wave etc. (The only exception is the can_play_type tests, which
+# necessarily depend on the backend(s) configured.) As far as possible, each
+# test should work with any resource type. This makes it easy to add new
+# backends and reduces the amount of test duplication.
+
+# For each supported backend, resources that can be played by that backend
+# should be added to the lists in manifest.js. Media tests that aren't
+# testing for a bug in handling a specific resource type should pick one of
+# the lists in manifest.js and run the test for each resource in the list
+# that is supported in the current build (the canPlayType API is useful for
+# this).
+
+# To test whether a valid resource can simply be played through correctly,
+# and optionally that its metadata is read correctly, just add it to
+# gPlayTests in manifest.js. To test whether an invalid resource correctly
+# throws an error (and does not cause a crash or hang), just add it to
+# gErrorTests in manifest.js.
+
+# To test for a specific bug in handling a specific resource type, make the
+# test first check canPlayType for the type, and if it's not supported, just
+# do ok(true, "Type not supported") and stop the test.
+
+[DEFAULT]
+subsuite = media
+skip-if =
+ (os == "win" && processor == "aarch64") # aarch64 due to 1536604
+support-files =
+ 16bit_wave_extrametadata.wav
+ 16bit_wave_extrametadata.wav^headers^
+ 320x240.ogv
+ 320x240.ogv^headers^
+ 448636.ogv
+ 448636.ogv^headers^
+ A4.ogv
+ A4.ogv^headers^
+ VID_0001.ogg
+ VID_0001.ogg^headers^
+ allowed.sjs
+ ambisonics.mp4
+ ambisonics.mp4^headers^
+ audio-gaps.ogg
+ audio-gaps.ogg^headers^
+ audio-gaps-short.ogg
+ audio-gaps-short.ogg^headers^
+ audio-overhang.ogg
+ audio-overhang.ogg^headers^
+ audio.wav
+ audio.wav^headers^
+ av1.mp4
+ av1.mp4^headers^
+ background_video.js
+ badtags.ogg
+ badtags.ogg^headers^
+ bear-640x360-v_frag-cenc-key_rotation.mp4
+ bear-640x360-a_frag-cenc-key_rotation.mp4
+ beta-phrasebook.ogg
+ beta-phrasebook.ogg^headers^
+ big.wav
+ big.wav^headers^
+ big-buck-bunny-cenc-avc3-1.m4s
+ big-buck-bunny-cenc-avc3-1.m4s^headers^
+ big-buck-bunny-cenc-avc3-init.mp4
+ big-buck-bunny-cenc-avc3-init.mp4^headers^
+ big-short.wav
+ big-short.wav^headers^
+ bipbop.mp4
+ bipbop-cenc-audio1.m4s
+ bipbop-cenc-audio1.m4s^headers^
+ bipbop-cenc-audio2.m4s
+ bipbop-cenc-audio2.m4s^headers^
+ bipbop-cenc-audio3.m4s
+ bipbop-cenc-audio3.m4s^headers^
+ bipbop-cenc-audioinit.mp4
+ bipbop-cenc-audioinit.mp4^headers^
+ bipbop-cenc-video1.m4s
+ bipbop-cenc-video1.m4s^headers^
+ bipbop-cenc-video2.m4s
+ bipbop-cenc-video2.m4s^headers^
+ bipbop-cenc-videoinit.mp4
+ bipbop-cenc-videoinit.mp4^headers^
+ bipbop-cenc-video-10s.mp4
+ bipbop-cenc-video-10s.mp4^headers^
+ bipbop-clearkey-keyrotation-clear-lead-audio.mp4
+ bipbop-clearkey-keyrotation-clear-lead-audio.mp4^headers^
+ bipbop-clearkey-keyrotation-clear-lead-video.mp4
+ bipbop-clearkey-keyrotation-clear-lead-video.mp4^headers^
+ bipbop_225w_175kbps.mp4
+ bipbop_225w_175kbps.mp4^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-1.m4s
+ bipbop_225w_175kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-2.m4s
+ bipbop_225w_175kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-3.m4s
+ bipbop_225w_175kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-4.m4s
+ bipbop_225w_175kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-init.mp4
+ bipbop_225w_175kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-1.m4s
+ bipbop_225w_175kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-2.m4s
+ bipbop_225w_175kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-3.m4s
+ bipbop_225w_175kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-4.m4s
+ bipbop_225w_175kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-init.mp4
+ bipbop_225w_175kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_225w_175kbps-cenc-video-key1-1.m4s
+ bipbop_225w_175kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_225w_175kbps-cenc-video-key1-init.mp4
+ bipbop_225w_175kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_225w_175kbps-cenc-video-key2-1.m4s
+ bipbop_225w_175kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_225w_175kbps-cenc-video-key2-init.mp4
+ bipbop_225w_175kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_300_215kbps-cenc-audio-key1-1.m4s
+ bipbop_300_215kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key1-2.m4s
+ bipbop_300_215kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key1-3.m4s
+ bipbop_300_215kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key1-4.m4s
+ bipbop_300_215kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key1-init.mp4
+ bipbop_300_215kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_300_215kbps-cenc-audio-key2-1.m4s
+ bipbop_300_215kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key2-2.m4s
+ bipbop_300_215kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key2-3.m4s
+ bipbop_300_215kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key2-4.m4s
+ bipbop_300_215kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key2-init.mp4
+ bipbop_300_215kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_300_215kbps-cenc-video-key1-1.m4s
+ bipbop_300_215kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_300_215kbps-cenc-video-key1-2.m4s
+ bipbop_300_215kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_300_215kbps-cenc-video-key1-init.mp4
+ bipbop_300_215kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_300_215kbps-cenc-video-key2-1.m4s
+ bipbop_300_215kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_300_215kbps-cenc-video-key2-2.m4s
+ bipbop_300_215kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_300_215kbps-cenc-video-key2-init.mp4
+ bipbop_300_215kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-1.m4s
+ bipbop_300wp_227kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-2.m4s
+ bipbop_300wp_227kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-3.m4s
+ bipbop_300wp_227kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-4.m4s
+ bipbop_300wp_227kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-init.mp4
+ bipbop_300wp_227kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-1.m4s
+ bipbop_300wp_227kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-2.m4s
+ bipbop_300wp_227kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-3.m4s
+ bipbop_300wp_227kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-4.m4s
+ bipbop_300wp_227kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-init.mp4
+ bipbop_300wp_227kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_300wp_227kbps-cenc-video-key1-1.m4s
+ bipbop_300wp_227kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_300wp_227kbps-cenc-video-key1-2.m4s
+ bipbop_300wp_227kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_300wp_227kbps-cenc-video-key1-init.mp4
+ bipbop_300wp_227kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_300wp_227kbps-cenc-video-key2-1.m4s
+ bipbop_300wp_227kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_300wp_227kbps-cenc-video-key2-2.m4s
+ bipbop_300wp_227kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_300wp_227kbps-cenc-video-key2-init.mp4
+ bipbop_300wp_227kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-1.m4s
+ bipbop_360w_253kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-2.m4s
+ bipbop_360w_253kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-3.m4s
+ bipbop_360w_253kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-4.m4s
+ bipbop_360w_253kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-init.mp4
+ bipbop_360w_253kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-1.m4s
+ bipbop_360w_253kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-2.m4s
+ bipbop_360w_253kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-3.m4s
+ bipbop_360w_253kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-4.m4s
+ bipbop_360w_253kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-init.mp4
+ bipbop_360w_253kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_360w_253kbps-cenc-video-key1-1.m4s
+ bipbop_360w_253kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_360w_253kbps-cenc-video-key1-init.mp4
+ bipbop_360w_253kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_360w_253kbps-cenc-video-key2-1.m4s
+ bipbop_360w_253kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_360w_253kbps-cenc-video-key2-init.mp4
+ bipbop_360w_253kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_360w_253kbps-clearkey-audio.webm
+ bipbop_360w_253kbps-clearkey-audio.webm^headers^
+ bipbop_360w_253kbps-clearkey-video-vp8.webm
+ bipbop_360w_253kbps-clearkey-video-vp8.webm^headers^
+ bipbop_360w_253kbps-clearkey-video-vp9.webm
+ bipbop_360w_253kbps-clearkey-video-vp9.webm^headers^
+ bipbop_480_624kbps-cenc-audio-key1-1.m4s
+ bipbop_480_624kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key1-2.m4s
+ bipbop_480_624kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key1-3.m4s
+ bipbop_480_624kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key1-4.m4s
+ bipbop_480_624kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key1-init.mp4
+ bipbop_480_624kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_480_624kbps-cenc-audio-key2-1.m4s
+ bipbop_480_624kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key2-2.m4s
+ bipbop_480_624kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key2-3.m4s
+ bipbop_480_624kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key2-4.m4s
+ bipbop_480_624kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key2-init.mp4
+ bipbop_480_624kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_480_624kbps-cenc-video-key1-1.m4s
+ bipbop_480_624kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_480_624kbps-cenc-video-key1-2.m4s
+ bipbop_480_624kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_480_624kbps-cenc-video-key1-init.mp4
+ bipbop_480_624kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_480_624kbps-cenc-video-key2-1.m4s
+ bipbop_480_624kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_480_624kbps-cenc-video-key2-2.m4s
+ bipbop_480_624kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_480_624kbps-cenc-video-key2-init.mp4
+ bipbop_480_624kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_480_959kbps-cenc-audio-key1-1.m4s
+ bipbop_480_959kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key1-2.m4s
+ bipbop_480_959kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key1-3.m4s
+ bipbop_480_959kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key1-4.m4s
+ bipbop_480_959kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key1-init.mp4
+ bipbop_480_959kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_480_959kbps-cenc-audio-key2-1.m4s
+ bipbop_480_959kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key2-2.m4s
+ bipbop_480_959kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key2-3.m4s
+ bipbop_480_959kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key2-4.m4s
+ bipbop_480_959kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key2-init.mp4
+ bipbop_480_959kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_480_959kbps-cenc-video-key1-1.m4s
+ bipbop_480_959kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_480_959kbps-cenc-video-key1-2.m4s
+ bipbop_480_959kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_480_959kbps-cenc-video-key1-init.mp4
+ bipbop_480_959kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_480_959kbps-cenc-video-key2-1.m4s
+ bipbop_480_959kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_480_959kbps-cenc-video-key2-2.m4s
+ bipbop_480_959kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_480_959kbps-cenc-video-key2-init.mp4
+ bipbop_480_959kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-1.m4s
+ bipbop_480wp_663kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-2.m4s
+ bipbop_480wp_663kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-3.m4s
+ bipbop_480wp_663kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-4.m4s
+ bipbop_480wp_663kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-init.mp4
+ bipbop_480wp_663kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-1.m4s
+ bipbop_480wp_663kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-2.m4s
+ bipbop_480wp_663kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-3.m4s
+ bipbop_480wp_663kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-4.m4s
+ bipbop_480wp_663kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-init.mp4
+ bipbop_480wp_663kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_480wp_663kbps-cenc-video-key1-1.m4s
+ bipbop_480wp_663kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_480wp_663kbps-cenc-video-key1-2.m4s
+ bipbop_480wp_663kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_480wp_663kbps-cenc-video-key1-init.mp4
+ bipbop_480wp_663kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_480wp_663kbps-cenc-video-key2-1.m4s
+ bipbop_480wp_663kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_480wp_663kbps-cenc-video-key2-2.m4s
+ bipbop_480wp_663kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_480wp_663kbps-cenc-video-key2-init.mp4
+ bipbop_480wp_663kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-1.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-2.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-3.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-4.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-init.mp4
+ bipbop_480wp_1001kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-1.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-2.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-3.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-4.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-init.mp4
+ bipbop_480wp_1001kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_480wp_1001kbps-cenc-video-key1-1.m4s
+ bipbop_480wp_1001kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-video-key1-2.m4s
+ bipbop_480wp_1001kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-video-key1-init.mp4
+ bipbop_480wp_1001kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_480wp_1001kbps-cenc-video-key2-1.m4s
+ bipbop_480wp_1001kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-video-key2-2.m4s
+ bipbop_480wp_1001kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-video-key2-init.mp4
+ bipbop_480wp_1001kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_audio_aac_8k.mp4
+ bipbop_audio_aac_8k.mp4^headers^
+ bipbop_audio_aac_22.05k.mp4
+ bipbop_audio_aac_22.05k.mp4^headers^
+ bipbop_audio_aac_44.1k.mp4
+ bipbop_audio_aac_44.1k.mp4^headers^
+ bipbop_audio_aac_48k.mp4
+ bipbop_audio_aac_48k.mp4^headers^
+ bipbop_audio_aac_88.2k.mp4
+ bipbop_audio_aac_88.2k.mp4^headers^
+ bipbop_audio_aac_96k.mp4
+ bipbop_audio_aac_96k.mp4^headers^
+ bipbop_cbcs_1_9_audio_1.m4s
+ bipbop_cbcs_1_9_audio_1.m4s^headers^
+ bipbop_cbcs_1_9_audio_init.mp4
+ bipbop_cbcs_1_9_audio_init.mp4^headers^
+ bipbop_cbcs_1_9_video_1.m4s
+ bipbop_cbcs_1_9_video_1.m4s^headers^
+ bipbop_cbcs_1_9_video_init.mp4
+ bipbop_cbcs_1_9_video_init.mp4^headers^
+ bipbop_cbcs_5_5_audio_1.m4s
+ bipbop_cbcs_5_5_audio_1.m4s^headers^
+ bipbop_cbcs_5_5_audio_init.mp4
+ bipbop_cbcs_5_5_audio_init.mp4^headers^
+ bipbop_cbcs_5_5_video_1.m4s
+ bipbop_cbcs_5_5_video_1.m4s^headers^
+ bipbop_cbcs_5_5_video_init.mp4
+ bipbop_cbcs_5_5_video_init.mp4^headers^
+ bipbop_cbcs_7_7_audio_1.m4s
+ bipbop_cbcs_7_7_audio_1.m4s^headers^
+ bipbop_cbcs_7_7_audio_init.mp4
+ bipbop_cbcs_7_7_audio_init.mp4^headers^
+ bipbop_cbcs_7_7_video_1.m4s
+ bipbop_cbcs_7_7_video_1.m4s^headers^
+ bipbop_cbcs_7_7_video_init.mp4
+ bipbop_cbcs_7_7_video_init.mp4^headers^
+ bipbop_cbcs_9_8_audio_1.m4s
+ bipbop_cbcs_9_8_audio_1.m4s^headers^
+ bipbop_cbcs_9_8_audio_init.mp4
+ bipbop_cbcs_9_8_audio_init.mp4^headers^
+ bipbop_cbcs_9_8_video_1.m4s
+ bipbop_cbcs_9_8_video_1.m4s^headers^
+ bipbop_cbcs_9_8_video_init.mp4
+ bipbop_cbcs_9_8_video_init.mp4^headers^
+ bipbop_cbcs_10_0_audio_1.m4s
+ bipbop_cbcs_10_0_audio_1.m4s^headers^
+ bipbop_cbcs_10_0_audio_init.mp4
+ bipbop_cbcs_10_0_audio_init.mp4^headers^
+ bipbop_cbcs_10_0_video_1.m4s
+ bipbop_cbcs_10_0_video_1.m4s^headers^
+ bipbop_cbcs_10_0_video_init.mp4
+ bipbop_cbcs_10_0_video_init.mp4^headers^
+ bipbop_short_pixel_metadata_bigger_than_in_stream_vp8.webm
+ bipbop_short_pixel_metadata_bigger_than_in_stream_vp8.webm^headers^
+ bipbop_short_pixel_metadata_narrower_than_in_stream_vp8.webm
+ bipbop_short_pixel_metadata_narrower_than_in_stream_vp8.webm^headers^
+ bipbop_short_pixel_metadata_smaller_than_in_stream_vp8.webm^headers^
+ bipbop_short_pixel_metadata_smaller_than_in_stream_vp8.webm
+ bipbop_short_vp8.webm
+ bipbop_short_vp8.webm^headers^
+ bipbop-lateaudio.mp4
+ bipbop-lateaudio.mp4^headers^
+ black100x100-aspect3to2.ogv
+ black100x100-aspect3to2.ogv^headers^
+ bogus.duh
+ bogus.ogv
+ bogus.ogv^headers^
+ bogus.wav
+ bogus.wav^headers^
+ bug461281.ogg
+ bug461281.ogg^headers^
+ bug482461-theora.ogv
+ bug482461-theora.ogv^headers^
+ bug482461.ogv
+ bug482461.ogv^headers^
+ bug495129.ogv
+ bug495129.ogv^headers^
+ bug495794.ogg
+ bug495794.ogg^headers^
+ bug498380.ogv
+ bug498380.ogv^headers^
+ bug498855-1.ogv
+ bug498855-1.ogv^headers^
+ bug498855-2.ogv
+ bug498855-2.ogv^headers^
+ bug498855-3.ogv
+ bug498855-3.ogv^headers^
+ bug499519.ogv
+ bug499519.ogv^headers^
+ bug500311.ogv
+ bug500311.ogv^headers^
+ bug501279.ogg
+ bug501279.ogg^headers^
+ bug504613.ogv
+ bug504613.ogv^headers^
+ bug504644.ogv
+ bug504644.ogv^headers^
+ bug504843.ogv
+ bug504843.ogv^headers^
+ bug506094.ogv
+ bug506094.ogv^headers^
+ bug516323.indexed.ogv
+ bug516323.indexed.ogv^headers^
+ bug516323.ogv
+ bug516323.ogv^headers^
+ bug520493.ogg
+ bug520493.ogg^headers^
+ bug520500.ogg
+ bug520500.ogg^headers^
+ bug520908.ogv
+ bug520908.ogv^headers^
+ bug523816.ogv
+ bug523816.ogv^headers^
+ bug533822.ogg
+ bug533822.ogg^headers^
+ bug556821.ogv
+ bug556821.ogv^headers^
+ bug557094.ogv
+ bug557094.ogv^headers^
+ bug603918.webm
+ bug603918.webm^headers^
+ bug604067.webm
+ bug604067.webm^headers^
+ bug1066943.webm
+ bug1066943.webm^headers^
+ bug1301226.wav
+ bug1301226.wav^headers^
+ bug1301226-odd.wav
+ bug1301226-odd.wav^headers^
+ bug1377278.webm
+ bug1377278.webm^headers^
+ bunny.webm
+ can_play_type_dash.js
+ can_play_type_ogg.js
+ can_play_type_wave.js
+ can_play_type_webm.js
+ cancellable_request.sjs
+ chain.ogg
+ chain.ogg^headers^
+ chain.ogv
+ chain.ogv^headers^
+ chain.opus
+ chain.opus^headers^
+ chained-audio-video.ogg
+ chained-audio-video.ogg^headers^
+ chained-video.ogv
+ chained-video.ogv^headers^
+ chromeHelper.js
+ cloneElementVisually_helpers.js
+ contentType.sjs
+ detodos.opus
+ detodos.opus^headers^
+ detodos.webm
+ detodos.webm^headers^
+ detodos-short.webm
+ detodos-short.webm^headers^
+ detodos-recorder-test.opus
+ detodos-recorder-test.opus^headers^
+ detodos-short.opus
+ detodos-short.opus^headers^
+ dirac.ogg
+ dirac.ogg^headers^
+ dynamic_resource.sjs
+ eme_standalone.js
+ eme.js
+ empty_size.mp3
+ file_access_controls.html
+ file_eme_createMediaKeys.html
+ flac-s24.flac
+ flac-s24.flac^headers^
+ flac-noheader-s16.flac
+ flac-noheader-s16.flac^headers^
+ flac-sample.mp4
+ flac-sample.mp4^headers^
+ flac-sample-cenc.mp4
+ flac-sample-cenc.mp4^headers^
+ fragment_noplay.js
+ fragment_play.js
+ gizmo.mp4
+ gizmo.mp4^headers^
+ gizmo-noaudio.mp4
+ gizmo-noaudio.mp4^headers^
+ gizmo-short.mp4
+ gizmo-short.mp4^headers^
+ gizmo.webm
+ gizmo.webm^headers^
+ gizmo-noaudio.webm
+ gizmo-noaudio.webm^headers^
+ gUM_support.js
+ gzipped_mp4.sjs
+ huge-id3.mp3
+ huge-id3.mp3^headers^
+ id3tags.mp3
+ id3tags.mp3^headers^
+ invalid-cmap-s0c0.opus
+ invalid-cmap-s0c0.opus^headers^
+ invalid-cmap-s0c2.opus
+ invalid-cmap-s0c2.opus^headers^
+ invalid-cmap-s1c2.opus
+ invalid-cmap-s1c2.opus^headers^
+ invalid-cmap-short.opus
+ invalid-cmap-short.opus^headers^
+ invalid-discard_on_multi_blocks.webm
+ invalid-discard_on_multi_blocks.webm^headers^
+ invalid-excess_discard.webm
+ invalid-excess_discard.webm^headers^
+ invalid-excess_neg_discard.webm
+ invalid-excess_neg_discard.webm^headers^
+ invalid-m0c0.opus
+ invalid-m0c0.opus^headers^
+ invalid-m0c3.opus
+ invalid-m0c3.opus^headers^
+ invalid-m1c0.opus
+ invalid-m1c0.opus^headers^
+ invalid-m1c9.opus
+ invalid-m1c9.opus^headers^
+ invalid-m2c0.opus
+ invalid-m2c0.opus^headers^
+ invalid-m2c1.opus
+ invalid-m2c1.opus^headers^
+ invalid-neg_discard.webm
+ invalid-neg_discard.webm^headers^
+ invalid-preskip.webm
+ invalid-preskip.webm^headers^
+ manifest.js
+ midflight-redirect.sjs
+ multiple-bos.ogg
+ multiple-bos.ogg^headers^
+ multiple-bos-more-header-fileds.ogg
+ multiple-bos-more-header-fileds.ogg^headers^
+ multi_id3v2.mp3
+ no-container-codec-delay.webm
+ no-cues.webm
+ no-cues.webm^headers^
+ notags.mp3
+ notags.mp3^headers^
+ opus-mapping2.mp4
+ opus-mapping2.mp4^headers^
+ opus-mapping2.webm
+ opus-mapping2.webm^headers^
+ opus-sample.mp4
+ opus-sample.mp4^headers^
+ opus-sample-cenc.mp4
+ opus-sample-cenc.mp4^headers^
+ owl-funnier-id3.mp3
+ owl-funnier-id3.mp3^headers^
+ owl-funny-id3.mp3
+ owl-funny-id3.mp3^headers^
+ owl.mp3
+ owl.mp3^headers^
+ owl-short.mp3
+ owl-short.mp3^headers^
+ pixel_aspect_ratio.mp4
+ play_promise.js
+ poster-test.jpg
+ r11025_msadpcm_c1.wav
+ r11025_msadpcm_c1.wav^headers^
+ r11025_s16_c1.wav
+ r11025_s16_c1.wav^headers^
+ r11025_s16_c1_trailing.wav
+ r11025_s16_c1_trailing.wav^headers^
+ r11025_s16_c1-short.wav
+ r11025_s16_c1-short.wav^headers^
+ r11025_u8_c1.wav
+ r11025_u8_c1.wav^headers^
+ r11025_u8_c1_trunc.wav
+ r11025_u8_c1_trunc.wav^headers^
+ r16000_u8_c1_list.wav
+ r16000_u8_c1_list.wav^headers^
+ reactivate_helper.html
+ red-46x48.mp4
+ red-46x48.mp4^headers^
+ red-48x46.mp4
+ red-48x46.mp4^headers^
+ redirect.sjs
+ referer.sjs
+ resolution-change.webm
+ resolution-change.webm^headers^
+ sample.3gp
+ sample.3g2
+ sample-encrypted-sgpdstbl-sbgptraf.mp4
+ sample-encrypted-sgpdstbl-sbgptraf.mp4^headers^
+ sample-fisbone-skeleton4.ogv
+ sample-fisbone-skeleton4.ogv^headers^
+ sample-fisbone-wrong-header.ogv
+ sample-fisbone-wrong-header.ogv^headers^
+ seek.ogv
+ seek.ogv^headers^
+ seek-short.ogv
+ seek-short.ogv^headers^
+ seek.webm
+ seek.webm^headers^
+ seek-short.webm
+ seek-short.webm^headers^
+ seek_support.js
+ seekLies.sjs
+ seek_with_sound.ogg^headers^
+ short-cenc.mp4
+ sine.webm
+ sine.webm^headers^
+ sintel-short-clearkey-subsample-encrypted-audio.webm
+ sintel-short-clearkey-subsample-encrypted-audio.webm^headers^
+ sintel-short-clearkey-subsample-encrypted-video.webm
+ sintel-short-clearkey-subsample-encrypted-video.webm^headers^
+ short.mp4
+ short.mp4.gz
+ short.mp4^headers^
+ short-aac-encrypted-audio.mp4
+ short-aac-encrypted-audio.mp4^headers^
+ short-audio-fragmented-cenc-without-pssh.mp4
+ short-audio-fragmented-cenc-without-pssh.mp4^headers^
+ short-video.ogv
+ short-video.ogv^headers^
+ short-vp9-encrypted-video.mp4
+ short-vp9-encrypted-video.mp4^headers^
+ small-shot-mp3.mp4
+ small-shot-mp3.mp4^headers^
+ small-shot.m4a
+ small-shot.mp3
+ small-shot.mp3^headers^
+ small-shot.ogg
+ small-shot.ogg^headers^
+ small-shot.flac
+ sound.ogg
+ sound.ogg^headers^
+ spacestorm-1000Hz-100ms.ogg
+ spacestorm-1000Hz-100ms.ogg^headers^
+ split.webm
+ split.webm^headers^
+ street.mp4
+ street.mp4^headers^
+ test-1-mono.opus
+ test-1-mono.opus^headers^
+ test-2-stereo.opus
+ test-2-stereo.opus^headers^
+ test-3-LCR.opus
+ test-3-LCR.opus^headers^
+ test-4-quad.opus
+ test-4-quad.opus^headers^
+ test-5-5.0.opus
+ test-5-5.0.opus^headers^
+ test-6-5.1.opus
+ test-6-5.1.opus^headers^
+ test-7-6.1.opus
+ test-7-6.1.opus^headers^
+ test-8-7.1.opus
+ test-8-7.1.opus^headers^
+ test-stereo-phase-inversion-180.opus
+ test-stereo-phase-inversion-180.opus^headers^
+ variable-channel.ogg
+ variable-channel.ogg^headers^
+ variable-channel.opus
+ variable-channel.opus^headers^
+ variable-preskip.opus
+ variable-preskip.opus^headers^
+ variable-samplerate.ogg
+ variable-samplerate.ogg^headers^
+ variable-samplerate.opus
+ variable-samplerate.opus^headers^
+ vbr-head.mp3
+ vbr-head.mp3^headers^
+ vbr.mp3
+ vbr.mp3^headers^
+ very-short.mp3
+ video-overhang.ogg
+ video-overhang.ogg^headers^
+ vp9-superframes.webm
+ vp9-superframes.webm^headers^
+ vp9.webm
+ vp9.webm^headers^
+ vp9-short.webm
+ vp9-short.webm^headers^
+ vp9cake.webm
+ vp9cake.webm^headers^
+ vp9cake-short.webm
+ vp9cake-short.webm^headers^
+ wave_metadata.wav
+ wave_metadata.wav^headers^
+ wave_metadata_bad_len.wav
+ wave_metadata_bad_len.wav^headers^
+ wave_metadata_bad_no_null.wav
+ wave_metadata_bad_no_null.wav^headers^
+ wave_metadata_bad_utf8.wav
+ wave_metadata_bad_utf8.wav^headers^
+ wave_metadata_unknown_tag.wav
+ wave_metadata_unknown_tag.wav^headers^
+ wave_metadata_utf8.wav
+ wave_metadata_utf8.wav^headers^
+ wavedata_alaw.wav
+ wavedata_alaw.wav^headers^
+ wavedata_float.wav
+ wavedata_float.wav^headers^
+ wavedata_s24.wav
+ wavedata_s24.wav^headers^
+ wavedata_s16.wav
+ wavedata_s16.wav^headers^
+ wavedata_u8.wav
+ wavedata_u8.wav^headers^
+ wavedata_ulaw.wav
+ wavedata_ulaw.wav^headers^
+ !/dom/canvas/test/captureStream_common.js
+ !/dom/html/test/reflect.js
+ !/dom/media/webrtc/tests/mochitests/head.js
+ hls/bipbop_16x9_single.m3u8
+ hls/bipbop_4x3_single.m3u8
+ hls/bipbop_4x3_variant.m3u8
+ hls/400x300_prog_index.m3u8
+ hls/400x300_prog_index_5s.m3u8
+ hls/416x243_prog_index_5s.m3u8
+ hls/640x480_prog_index.m3u8
+ hls/960x720_prog_index.m3u8
+ hls/400x300_seg0.ts
+ hls/400x300_seg0_5s.ts
+ hls/400x300_seg1.ts
+ hls/416x243_seg0_5s.ts
+ hls/640x480_seg0.ts
+ hls/640x480_seg1.ts
+ hls/960x720_seg0.ts
+ hls/960x720_seg1.ts
+ sync.webm
+
+[test_seek_negative.html]
+[test_seek_nosrc.html]
+[test_seek_out_of_range.html]
+skip-if = toolkit == 'android' # bug 1299382, android(bug 1232305)
+[test_seek_promise_bug1344357.html]
+skip-if = toolkit == 'android' # bug 1299382, android(bug 1232305)
+[test_seek-1.html]
+skip-if = toolkit == 'android' # bug 1322806, android(bug 1232305)
+[test_seek-2.html]
+skip-if = toolkit == 'android' # bug 1309778, android(bug 1232305)
+[test_seek-3.html]
+skip-if = toolkit == 'android' # bug 1321082, android(bug 1232305)
+[test_seek-4.html]
+skip-if = toolkit == 'android' # android(bug 1232305)
+[test_seek-5.html]
+skip-if = toolkit == 'android' # android(bug 1232305)
+[test_seek-6.html]
+skip-if = toolkit == 'android' # bug 1336629, bug 1324482, android(bug 1232305)
+[test_seek-7.html]
+skip-if = toolkit == 'android' # android(bug 1232305)
+[test_seek-8.html]
+skip-if = toolkit == 'android' # bug 1310584, android(bug 1232305)
+[test_seek-9.html]
+skip-if = toolkit == 'android' # bug 1332019, android(bug 1232305)
+[test_seek-10.html]
+skip-if = toolkit == 'android' # android(bug 1232305)
+[test_seek-11.html]
+skip-if = toolkit == 'android' # bug 1323133, android(bug 1232305)
+[test_seek-12.html]
+skip-if = toolkit == 'android' # bug 1321081, android(bug 1232305)
+[test_seek-13.html]
+skip-if = toolkit == 'android' # bug 1299174, android(bug 1232305)
+[test_seek-14.html]
+skip-if = toolkit == 'android' # android(bug 1232305)
+[test_seekable1.html]
+skip-if = toolkit == 'android' # android(bug 1232305)
+[test_seekLies.html]
+[test_seekToNextFrame.html]
+skip-if = toolkit == 'android' # bug 1329391, android(bug 1232305)
+tags=seektonextframe
+[test_seek_duration.html]
diff --git a/dom/media/test/mochitest_stream.ini b/dom/media/test/mochitest_stream.ini
new file mode 100644
index 0000000000..f224e72417
--- /dev/null
+++ b/dom/media/test/mochitest_stream.ini
@@ -0,0 +1,784 @@
+# Media tests should be backend independent, i.e., not conditioned on ogg,
+# wave etc. (The only exception is the can_play_type tests, which
+# necessarily depend on the backend(s) configured.) As far as possible, each
+# test should work with any resource type. This makes it easy to add new
+# backends and reduces the amount of test duplication.
+
+# For each supported backend, resources that can be played by that backend
+# should be added to the lists in manifest.js. Media tests that aren't
+# testing for a bug in handling a specific resource type should pick one of
+# the lists in manifest.js and run the test for each resource in the list
+# that is supported in the current build (the canPlayType API is useful for
+# this).
+
+# To test whether a valid resource can simply be played through correctly,
+# and optionally that its metadata is read correctly, just add it to
+# gPlayTests in manifest.js. To test whether an invalid resource correctly
+# throws an error (and does not cause a crash or hang), just add it to
+# gErrorTests in manifest.js.
+
+# To test for a specific bug in handling a specific resource type, make the
+# test first check canPlayType for the type, and if it's not supported, just
+# do ok(true, "Type not supported") and stop the test.
+
+[DEFAULT]
+subsuite = media
+tags=mtg capturestream
+skip-if =
+ (os == "win" && processor == "aarch64") # aarch64 due to 1536604
+support-files =
+ 16bit_wave_extrametadata.wav
+ 16bit_wave_extrametadata.wav^headers^
+ 320x240.ogv
+ 320x240.ogv^headers^
+ 448636.ogv
+ 448636.ogv^headers^
+ A4.ogv
+ A4.ogv^headers^
+ VID_0001.ogg
+ VID_0001.ogg^headers^
+ allowed.sjs
+ ambisonics.mp4
+ ambisonics.mp4^headers^
+ audio-gaps.ogg
+ audio-gaps.ogg^headers^
+ audio-gaps-short.ogg
+ audio-gaps-short.ogg^headers^
+ audio-overhang.ogg
+ audio-overhang.ogg^headers^
+ audio.wav
+ audio.wav^headers^
+ av1.mp4
+ av1.mp4^headers^
+ background_video.js
+ badtags.ogg
+ badtags.ogg^headers^
+ bear-640x360-v_frag-cenc-key_rotation.mp4
+ bear-640x360-a_frag-cenc-key_rotation.mp4
+ beta-phrasebook.ogg
+ beta-phrasebook.ogg^headers^
+ big.wav
+ big.wav^headers^
+ big-buck-bunny-cenc-avc3-1.m4s
+ big-buck-bunny-cenc-avc3-1.m4s^headers^
+ big-buck-bunny-cenc-avc3-init.mp4
+ big-buck-bunny-cenc-avc3-init.mp4^headers^
+ big-short.wav
+ big-short.wav^headers^
+ bipbop.mp4
+ bipbop-cenc-audio1.m4s
+ bipbop-cenc-audio1.m4s^headers^
+ bipbop-cenc-audio2.m4s
+ bipbop-cenc-audio2.m4s^headers^
+ bipbop-cenc-audio3.m4s
+ bipbop-cenc-audio3.m4s^headers^
+ bipbop-cenc-audioinit.mp4
+ bipbop-cenc-audioinit.mp4^headers^
+ bipbop-cenc-video1.m4s
+ bipbop-cenc-video1.m4s^headers^
+ bipbop-cenc-video2.m4s
+ bipbop-cenc-video2.m4s^headers^
+ bipbop-cenc-videoinit.mp4
+ bipbop-cenc-videoinit.mp4^headers^
+ bipbop-cenc-video-10s.mp4
+ bipbop-cenc-video-10s.mp4^headers^
+ bipbop-clearkey-keyrotation-clear-lead-audio.mp4
+ bipbop-clearkey-keyrotation-clear-lead-audio.mp4^headers^
+ bipbop-clearkey-keyrotation-clear-lead-video.mp4
+ bipbop-clearkey-keyrotation-clear-lead-video.mp4^headers^
+ bipbop_225w_175kbps.mp4
+ bipbop_225w_175kbps.mp4^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-1.m4s
+ bipbop_225w_175kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-2.m4s
+ bipbop_225w_175kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-3.m4s
+ bipbop_225w_175kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-4.m4s
+ bipbop_225w_175kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key1-init.mp4
+ bipbop_225w_175kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-1.m4s
+ bipbop_225w_175kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-2.m4s
+ bipbop_225w_175kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-3.m4s
+ bipbop_225w_175kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-4.m4s
+ bipbop_225w_175kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_225w_175kbps-cenc-audio-key2-init.mp4
+ bipbop_225w_175kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_225w_175kbps-cenc-video-key1-1.m4s
+ bipbop_225w_175kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_225w_175kbps-cenc-video-key1-init.mp4
+ bipbop_225w_175kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_225w_175kbps-cenc-video-key2-1.m4s
+ bipbop_225w_175kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_225w_175kbps-cenc-video-key2-init.mp4
+ bipbop_225w_175kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_300_215kbps-cenc-audio-key1-1.m4s
+ bipbop_300_215kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key1-2.m4s
+ bipbop_300_215kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key1-3.m4s
+ bipbop_300_215kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key1-4.m4s
+ bipbop_300_215kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key1-init.mp4
+ bipbop_300_215kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_300_215kbps-cenc-audio-key2-1.m4s
+ bipbop_300_215kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key2-2.m4s
+ bipbop_300_215kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key2-3.m4s
+ bipbop_300_215kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key2-4.m4s
+ bipbop_300_215kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_300_215kbps-cenc-audio-key2-init.mp4
+ bipbop_300_215kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_300_215kbps-cenc-video-key1-1.m4s
+ bipbop_300_215kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_300_215kbps-cenc-video-key1-2.m4s
+ bipbop_300_215kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_300_215kbps-cenc-video-key1-init.mp4
+ bipbop_300_215kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_300_215kbps-cenc-video-key2-1.m4s
+ bipbop_300_215kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_300_215kbps-cenc-video-key2-2.m4s
+ bipbop_300_215kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_300_215kbps-cenc-video-key2-init.mp4
+ bipbop_300_215kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-1.m4s
+ bipbop_300wp_227kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-2.m4s
+ bipbop_300wp_227kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-3.m4s
+ bipbop_300wp_227kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-4.m4s
+ bipbop_300wp_227kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key1-init.mp4
+ bipbop_300wp_227kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-1.m4s
+ bipbop_300wp_227kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-2.m4s
+ bipbop_300wp_227kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-3.m4s
+ bipbop_300wp_227kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-4.m4s
+ bipbop_300wp_227kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_300wp_227kbps-cenc-audio-key2-init.mp4
+ bipbop_300wp_227kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_300wp_227kbps-cenc-video-key1-1.m4s
+ bipbop_300wp_227kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_300wp_227kbps-cenc-video-key1-2.m4s
+ bipbop_300wp_227kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_300wp_227kbps-cenc-video-key1-init.mp4
+ bipbop_300wp_227kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_300wp_227kbps-cenc-video-key2-1.m4s
+ bipbop_300wp_227kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_300wp_227kbps-cenc-video-key2-2.m4s
+ bipbop_300wp_227kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_300wp_227kbps-cenc-video-key2-init.mp4
+ bipbop_300wp_227kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-1.m4s
+ bipbop_360w_253kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-2.m4s
+ bipbop_360w_253kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-3.m4s
+ bipbop_360w_253kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-4.m4s
+ bipbop_360w_253kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key1-init.mp4
+ bipbop_360w_253kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-1.m4s
+ bipbop_360w_253kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-2.m4s
+ bipbop_360w_253kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-3.m4s
+ bipbop_360w_253kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-4.m4s
+ bipbop_360w_253kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_360w_253kbps-cenc-audio-key2-init.mp4
+ bipbop_360w_253kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_360w_253kbps-cenc-video-key1-1.m4s
+ bipbop_360w_253kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_360w_253kbps-cenc-video-key1-init.mp4
+ bipbop_360w_253kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_360w_253kbps-cenc-video-key2-1.m4s
+ bipbop_360w_253kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_360w_253kbps-cenc-video-key2-init.mp4
+ bipbop_360w_253kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_360w_253kbps-clearkey-audio.webm
+ bipbop_360w_253kbps-clearkey-audio.webm^headers^
+ bipbop_360w_253kbps-clearkey-video-vp8.webm
+ bipbop_360w_253kbps-clearkey-video-vp8.webm^headers^
+ bipbop_360w_253kbps-clearkey-video-vp9.webm
+ bipbop_360w_253kbps-clearkey-video-vp9.webm^headers^
+ bipbop_480_624kbps-cenc-audio-key1-1.m4s
+ bipbop_480_624kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key1-2.m4s
+ bipbop_480_624kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key1-3.m4s
+ bipbop_480_624kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key1-4.m4s
+ bipbop_480_624kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key1-init.mp4
+ bipbop_480_624kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_480_624kbps-cenc-audio-key2-1.m4s
+ bipbop_480_624kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key2-2.m4s
+ bipbop_480_624kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key2-3.m4s
+ bipbop_480_624kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key2-4.m4s
+ bipbop_480_624kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_480_624kbps-cenc-audio-key2-init.mp4
+ bipbop_480_624kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_480_624kbps-cenc-video-key1-1.m4s
+ bipbop_480_624kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_480_624kbps-cenc-video-key1-2.m4s
+ bipbop_480_624kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_480_624kbps-cenc-video-key1-init.mp4
+ bipbop_480_624kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_480_624kbps-cenc-video-key2-1.m4s
+ bipbop_480_624kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_480_624kbps-cenc-video-key2-2.m4s
+ bipbop_480_624kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_480_624kbps-cenc-video-key2-init.mp4
+ bipbop_480_624kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_480_959kbps-cenc-audio-key1-1.m4s
+ bipbop_480_959kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key1-2.m4s
+ bipbop_480_959kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key1-3.m4s
+ bipbop_480_959kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key1-4.m4s
+ bipbop_480_959kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key1-init.mp4
+ bipbop_480_959kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_480_959kbps-cenc-audio-key2-1.m4s
+ bipbop_480_959kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key2-2.m4s
+ bipbop_480_959kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key2-3.m4s
+ bipbop_480_959kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key2-4.m4s
+ bipbop_480_959kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_480_959kbps-cenc-audio-key2-init.mp4
+ bipbop_480_959kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_480_959kbps-cenc-video-key1-1.m4s
+ bipbop_480_959kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_480_959kbps-cenc-video-key1-2.m4s
+ bipbop_480_959kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_480_959kbps-cenc-video-key1-init.mp4
+ bipbop_480_959kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_480_959kbps-cenc-video-key2-1.m4s
+ bipbop_480_959kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_480_959kbps-cenc-video-key2-2.m4s
+ bipbop_480_959kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_480_959kbps-cenc-video-key2-init.mp4
+ bipbop_480_959kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-1.m4s
+ bipbop_480wp_663kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-2.m4s
+ bipbop_480wp_663kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-3.m4s
+ bipbop_480wp_663kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-4.m4s
+ bipbop_480wp_663kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key1-init.mp4
+ bipbop_480wp_663kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-1.m4s
+ bipbop_480wp_663kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-2.m4s
+ bipbop_480wp_663kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-3.m4s
+ bipbop_480wp_663kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-4.m4s
+ bipbop_480wp_663kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_480wp_663kbps-cenc-audio-key2-init.mp4
+ bipbop_480wp_663kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_480wp_663kbps-cenc-video-key1-1.m4s
+ bipbop_480wp_663kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_480wp_663kbps-cenc-video-key1-2.m4s
+ bipbop_480wp_663kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_480wp_663kbps-cenc-video-key1-init.mp4
+ bipbop_480wp_663kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_480wp_663kbps-cenc-video-key2-1.m4s
+ bipbop_480wp_663kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_480wp_663kbps-cenc-video-key2-2.m4s
+ bipbop_480wp_663kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_480wp_663kbps-cenc-video-key2-init.mp4
+ bipbop_480wp_663kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-1.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key1-1.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-2.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key1-2.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-3.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key1-3.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-4.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key1-4.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key1-init.mp4
+ bipbop_480wp_1001kbps-cenc-audio-key1-init.mp4^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-1.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key2-1.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-2.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key2-2.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-3.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key2-3.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-4.m4s
+ bipbop_480wp_1001kbps-cenc-audio-key2-4.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-audio-key2-init.mp4
+ bipbop_480wp_1001kbps-cenc-audio-key2-init.mp4^headers^
+ bipbop_480wp_1001kbps-cenc-video-key1-1.m4s
+ bipbop_480wp_1001kbps-cenc-video-key1-1.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-video-key1-2.m4s
+ bipbop_480wp_1001kbps-cenc-video-key1-2.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-video-key1-init.mp4
+ bipbop_480wp_1001kbps-cenc-video-key1-init.mp4^headers^
+ bipbop_480wp_1001kbps-cenc-video-key2-1.m4s
+ bipbop_480wp_1001kbps-cenc-video-key2-1.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-video-key2-2.m4s
+ bipbop_480wp_1001kbps-cenc-video-key2-2.m4s^headers^
+ bipbop_480wp_1001kbps-cenc-video-key2-init.mp4
+ bipbop_480wp_1001kbps-cenc-video-key2-init.mp4^headers^
+ bipbop_audio_aac_8k.mp4
+ bipbop_audio_aac_8k.mp4^headers^
+ bipbop_audio_aac_22.05k.mp4
+ bipbop_audio_aac_22.05k.mp4^headers^
+ bipbop_audio_aac_44.1k.mp4
+ bipbop_audio_aac_44.1k.mp4^headers^
+ bipbop_audio_aac_48k.mp4
+ bipbop_audio_aac_48k.mp4^headers^
+ bipbop_audio_aac_88.2k.mp4
+ bipbop_audio_aac_88.2k.mp4^headers^
+ bipbop_audio_aac_96k.mp4
+ bipbop_audio_aac_96k.mp4^headers^
+ bipbop_cbcs_1_9_audio_1.m4s
+ bipbop_cbcs_1_9_audio_1.m4s^headers^
+ bipbop_cbcs_1_9_audio_init.mp4
+ bipbop_cbcs_1_9_audio_init.mp4^headers^
+ bipbop_cbcs_1_9_video_1.m4s
+ bipbop_cbcs_1_9_video_1.m4s^headers^
+ bipbop_cbcs_1_9_video_init.mp4
+ bipbop_cbcs_1_9_video_init.mp4^headers^
+ bipbop_cbcs_5_5_audio_1.m4s
+ bipbop_cbcs_5_5_audio_1.m4s^headers^
+ bipbop_cbcs_5_5_audio_init.mp4
+ bipbop_cbcs_5_5_audio_init.mp4^headers^
+ bipbop_cbcs_5_5_video_1.m4s
+ bipbop_cbcs_5_5_video_1.m4s^headers^
+ bipbop_cbcs_5_5_video_init.mp4
+ bipbop_cbcs_5_5_video_init.mp4^headers^
+ bipbop_cbcs_7_7_audio_1.m4s
+ bipbop_cbcs_7_7_audio_1.m4s^headers^
+ bipbop_cbcs_7_7_audio_init.mp4
+ bipbop_cbcs_7_7_audio_init.mp4^headers^
+ bipbop_cbcs_7_7_video_1.m4s
+ bipbop_cbcs_7_7_video_1.m4s^headers^
+ bipbop_cbcs_7_7_video_init.mp4
+ bipbop_cbcs_7_7_video_init.mp4^headers^
+ bipbop_cbcs_9_8_audio_1.m4s
+ bipbop_cbcs_9_8_audio_1.m4s^headers^
+ bipbop_cbcs_9_8_audio_init.mp4
+ bipbop_cbcs_9_8_audio_init.mp4^headers^
+ bipbop_cbcs_9_8_video_1.m4s
+ bipbop_cbcs_9_8_video_1.m4s^headers^
+ bipbop_cbcs_9_8_video_init.mp4
+ bipbop_cbcs_9_8_video_init.mp4^headers^
+ bipbop_cbcs_10_0_audio_1.m4s
+ bipbop_cbcs_10_0_audio_1.m4s^headers^
+ bipbop_cbcs_10_0_audio_init.mp4
+ bipbop_cbcs_10_0_audio_init.mp4^headers^
+ bipbop_cbcs_10_0_video_1.m4s
+ bipbop_cbcs_10_0_video_1.m4s^headers^
+ bipbop_cbcs_10_0_video_init.mp4
+ bipbop_cbcs_10_0_video_init.mp4^headers^
+ bipbop_short_pixel_metadata_bigger_than_in_stream_vp8.webm
+ bipbop_short_pixel_metadata_bigger_than_in_stream_vp8.webm^headers^
+ bipbop_short_pixel_metadata_narrower_than_in_stream_vp8.webm
+ bipbop_short_pixel_metadata_narrower_than_in_stream_vp8.webm^headers^
+ bipbop_short_pixel_metadata_smaller_than_in_stream_vp8.webm^headers^
+ bipbop_short_pixel_metadata_smaller_than_in_stream_vp8.webm
+ bipbop_short_vp8.webm
+ bipbop_short_vp8.webm^headers^
+ bipbop-lateaudio.mp4
+ bipbop-lateaudio.mp4^headers^
+ black100x100-aspect3to2.ogv
+ black100x100-aspect3to2.ogv^headers^
+ bogus.duh
+ bogus.ogv
+ bogus.ogv^headers^
+ bogus.wav
+ bogus.wav^headers^
+ bug461281.ogg
+ bug461281.ogg^headers^
+ bug482461-theora.ogv
+ bug482461-theora.ogv^headers^
+ bug482461.ogv
+ bug482461.ogv^headers^
+ bug495129.ogv
+ bug495129.ogv^headers^
+ bug495794.ogg
+ bug495794.ogg^headers^
+ bug498380.ogv
+ bug498380.ogv^headers^
+ bug498855-1.ogv
+ bug498855-1.ogv^headers^
+ bug498855-2.ogv
+ bug498855-2.ogv^headers^
+ bug498855-3.ogv
+ bug498855-3.ogv^headers^
+ bug499519.ogv
+ bug499519.ogv^headers^
+ bug500311.ogv
+ bug500311.ogv^headers^
+ bug501279.ogg
+ bug501279.ogg^headers^
+ bug504613.ogv
+ bug504613.ogv^headers^
+ bug504644.ogv
+ bug504644.ogv^headers^
+ bug504843.ogv
+ bug504843.ogv^headers^
+ bug506094.ogv
+ bug506094.ogv^headers^
+ bug516323.indexed.ogv
+ bug516323.indexed.ogv^headers^
+ bug516323.ogv
+ bug516323.ogv^headers^
+ bug520493.ogg
+ bug520493.ogg^headers^
+ bug520500.ogg
+ bug520500.ogg^headers^
+ bug520908.ogv
+ bug520908.ogv^headers^
+ bug523816.ogv
+ bug523816.ogv^headers^
+ bug533822.ogg
+ bug533822.ogg^headers^
+ bug556821.ogv
+ bug556821.ogv^headers^
+ bug557094.ogv
+ bug557094.ogv^headers^
+ bug603918.webm
+ bug603918.webm^headers^
+ bug604067.webm
+ bug604067.webm^headers^
+ bug1066943.webm
+ bug1066943.webm^headers^
+ bug1301226.wav
+ bug1301226.wav^headers^
+ bug1301226-odd.wav
+ bug1301226-odd.wav^headers^
+ bug1377278.webm
+ bug1377278.webm^headers^
+ bunny.webm
+ can_play_type_dash.js
+ can_play_type_ogg.js
+ can_play_type_wave.js
+ can_play_type_webm.js
+ cancellable_request.sjs
+ chain.ogg
+ chain.ogg^headers^
+ chain.ogv
+ chain.ogv^headers^
+ chain.opus
+ chain.opus^headers^
+ chained-audio-video.ogg
+ chained-audio-video.ogg^headers^
+ chained-video.ogv
+ chained-video.ogv^headers^
+ chromeHelper.js
+ cloneElementVisually_helpers.js
+ contentType.sjs
+ detodos.opus
+ detodos.opus^headers^
+ detodos.webm
+ detodos.webm^headers^
+ detodos-short.webm
+ detodos-short.webm^headers^
+ detodos-recorder-test.opus
+ detodos-recorder-test.opus^headers^
+ detodos-short.opus
+ detodos-short.opus^headers^
+ dirac.ogg
+ dirac.ogg^headers^
+ dynamic_resource.sjs
+ eme_standalone.js
+ eme.js
+ empty_size.mp3
+ file_access_controls.html
+ file_eme_createMediaKeys.html
+ flac-s24.flac
+ flac-s24.flac^headers^
+ flac-noheader-s16.flac
+ flac-noheader-s16.flac^headers^
+ flac-sample.mp4
+ flac-sample.mp4^headers^
+ flac-sample-cenc.mp4
+ flac-sample-cenc.mp4^headers^
+ fragment_noplay.js
+ fragment_play.js
+ gizmo.mp4
+ gizmo.mp4^headers^
+ gizmo-noaudio.mp4
+ gizmo-noaudio.mp4^headers^
+ gizmo-short.mp4
+ gizmo-short.mp4^headers^
+ gizmo.webm
+ gizmo.webm^headers^
+ gizmo-noaudio.webm
+ gizmo-noaudio.webm^headers^
+ gUM_support.js
+ gzipped_mp4.sjs
+ huge-id3.mp3
+ huge-id3.mp3^headers^
+ id3tags.mp3
+ id3tags.mp3^headers^
+ invalid-cmap-s0c0.opus
+ invalid-cmap-s0c0.opus^headers^
+ invalid-cmap-s0c2.opus
+ invalid-cmap-s0c2.opus^headers^
+ invalid-cmap-s1c2.opus
+ invalid-cmap-s1c2.opus^headers^
+ invalid-cmap-short.opus
+ invalid-cmap-short.opus^headers^
+ invalid-discard_on_multi_blocks.webm
+ invalid-discard_on_multi_blocks.webm^headers^
+ invalid-excess_discard.webm
+ invalid-excess_discard.webm^headers^
+ invalid-excess_neg_discard.webm
+ invalid-excess_neg_discard.webm^headers^
+ invalid-m0c0.opus
+ invalid-m0c0.opus^headers^
+ invalid-m0c3.opus
+ invalid-m0c3.opus^headers^
+ invalid-m1c0.opus
+ invalid-m1c0.opus^headers^
+ invalid-m1c9.opus
+ invalid-m1c9.opus^headers^
+ invalid-m2c0.opus
+ invalid-m2c0.opus^headers^
+ invalid-m2c1.opus
+ invalid-m2c1.opus^headers^
+ invalid-neg_discard.webm
+ invalid-neg_discard.webm^headers^
+ invalid-preskip.webm
+ invalid-preskip.webm^headers^
+ manifest.js
+ midflight-redirect.sjs
+ multiple-bos.ogg
+ multiple-bos.ogg^headers^
+ multiple-bos-more-header-fileds.ogg
+ multiple-bos-more-header-fileds.ogg^headers^
+ multi_id3v2.mp3
+ no-container-codec-delay.webm
+ no-cues.webm
+ no-cues.webm^headers^
+ notags.mp3
+ notags.mp3^headers^
+ opus-mapping2.mp4
+ opus-mapping2.mp4^headers^
+ opus-mapping2.webm
+ opus-mapping2.webm^headers^
+ opus-sample.mp4
+ opus-sample.mp4^headers^
+ opus-sample-cenc.mp4
+ opus-sample-cenc.mp4^headers^
+ owl-funnier-id3.mp3
+ owl-funnier-id3.mp3^headers^
+ owl-funny-id3.mp3
+ owl-funny-id3.mp3^headers^
+ owl.mp3
+ owl.mp3^headers^
+ owl-short.mp3
+ owl-short.mp3^headers^
+ pixel_aspect_ratio.mp4
+ play_promise.js
+ poster-test.jpg
+ r11025_msadpcm_c1.wav
+ r11025_msadpcm_c1.wav^headers^
+ r11025_s16_c1.wav
+ r11025_s16_c1.wav^headers^
+ r11025_s16_c1_trailing.wav
+ r11025_s16_c1_trailing.wav^headers^
+ r11025_s16_c1-short.wav
+ r11025_s16_c1-short.wav^headers^
+ r11025_u8_c1.wav
+ r11025_u8_c1.wav^headers^
+ r11025_u8_c1_trunc.wav
+ r11025_u8_c1_trunc.wav^headers^
+ r16000_u8_c1_list.wav
+ r16000_u8_c1_list.wav^headers^
+ reactivate_helper.html
+ red-46x48.mp4
+ red-46x48.mp4^headers^
+ red-48x46.mp4
+ red-48x46.mp4^headers^
+ redirect.sjs
+ referer.sjs
+ resolution-change.webm
+ resolution-change.webm^headers^
+ sample.3gp
+ sample.3g2
+ sample-encrypted-sgpdstbl-sbgptraf.mp4
+ sample-encrypted-sgpdstbl-sbgptraf.mp4^headers^
+ sample-fisbone-skeleton4.ogv
+ sample-fisbone-skeleton4.ogv^headers^
+ sample-fisbone-wrong-header.ogv
+ sample-fisbone-wrong-header.ogv^headers^
+ seek.ogv
+ seek.ogv^headers^
+ seek-short.ogv
+ seek-short.ogv^headers^
+ seek.webm
+ seek.webm^headers^
+ seek-short.webm
+ seek-short.webm^headers^
+ seek_support.js
+ seekLies.sjs
+ seek_with_sound.ogg^headers^
+ short-cenc.mp4
+ sine.webm
+ sine.webm^headers^
+ sintel-short-clearkey-subsample-encrypted-audio.webm
+ sintel-short-clearkey-subsample-encrypted-audio.webm^headers^
+ sintel-short-clearkey-subsample-encrypted-video.webm
+ sintel-short-clearkey-subsample-encrypted-video.webm^headers^
+ short.mp4
+ short.mp4.gz
+ short.mp4^headers^
+ short-aac-encrypted-audio.mp4
+ short-aac-encrypted-audio.mp4^headers^
+ short-audio-fragmented-cenc-without-pssh.mp4
+ short-audio-fragmented-cenc-without-pssh.mp4^headers^
+ short-video.ogv
+ short-video.ogv^headers^
+ short-vp9-encrypted-video.mp4
+ short-vp9-encrypted-video.mp4^headers^
+ small-shot-mp3.mp4
+ small-shot-mp3.mp4^headers^
+ small-shot.m4a
+ small-shot.mp3
+ small-shot.mp3^headers^
+ small-shot.ogg
+ small-shot.ogg^headers^
+ small-shot.flac
+ sound.ogg
+ sound.ogg^headers^
+ spacestorm-1000Hz-100ms.ogg
+ spacestorm-1000Hz-100ms.ogg^headers^
+ split.webm
+ split.webm^headers^
+ street.mp4
+ street.mp4^headers^
+ test-1-mono.opus
+ test-1-mono.opus^headers^
+ test-2-stereo.opus
+ test-2-stereo.opus^headers^
+ test-3-LCR.opus
+ test-3-LCR.opus^headers^
+ test-4-quad.opus
+ test-4-quad.opus^headers^
+ test-5-5.0.opus
+ test-5-5.0.opus^headers^
+ test-6-5.1.opus
+ test-6-5.1.opus^headers^
+ test-7-6.1.opus
+ test-7-6.1.opus^headers^
+ test-8-7.1.opus
+ test-8-7.1.opus^headers^
+ test-stereo-phase-inversion-180.opus
+ test-stereo-phase-inversion-180.opus^headers^
+ variable-channel.ogg
+ variable-channel.ogg^headers^
+ variable-channel.opus
+ variable-channel.opus^headers^
+ variable-preskip.opus
+ variable-preskip.opus^headers^
+ variable-samplerate.ogg
+ variable-samplerate.ogg^headers^
+ variable-samplerate.opus
+ variable-samplerate.opus^headers^
+ vbr-head.mp3
+ vbr-head.mp3^headers^
+ vbr.mp3
+ vbr.mp3^headers^
+ very-short.mp3
+ video-overhang.ogg
+ video-overhang.ogg^headers^
+ vp9-superframes.webm
+ vp9-superframes.webm^headers^
+ vp9.webm
+ vp9.webm^headers^
+ vp9-short.webm
+ vp9-short.webm^headers^
+ vp9cake.webm
+ vp9cake.webm^headers^
+ vp9cake-short.webm
+ vp9cake-short.webm^headers^
+ wave_metadata.wav
+ wave_metadata.wav^headers^
+ wave_metadata_bad_len.wav
+ wave_metadata_bad_len.wav^headers^
+ wave_metadata_bad_no_null.wav
+ wave_metadata_bad_no_null.wav^headers^
+ wave_metadata_bad_utf8.wav
+ wave_metadata_bad_utf8.wav^headers^
+ wave_metadata_unknown_tag.wav
+ wave_metadata_unknown_tag.wav^headers^
+ wave_metadata_utf8.wav
+ wave_metadata_utf8.wav^headers^
+ wavedata_alaw.wav
+ wavedata_alaw.wav^headers^
+ wavedata_float.wav
+ wavedata_float.wav^headers^
+ wavedata_s24.wav
+ wavedata_s24.wav^headers^
+ wavedata_s16.wav
+ wavedata_s16.wav^headers^
+ wavedata_u8.wav
+ wavedata_u8.wav^headers^
+ wavedata_ulaw.wav
+ wavedata_ulaw.wav^headers^
+ !/dom/canvas/test/captureStream_common.js
+ !/dom/html/test/reflect.js
+ !/dom/media/webrtc/tests/mochitests/head.js
+ hls/bipbop_16x9_single.m3u8
+ hls/bipbop_4x3_single.m3u8
+ hls/bipbop_4x3_variant.m3u8
+ hls/400x300_prog_index.m3u8
+ hls/400x300_prog_index_5s.m3u8
+ hls/416x243_prog_index_5s.m3u8
+ hls/640x480_prog_index.m3u8
+ hls/960x720_prog_index.m3u8
+ hls/400x300_seg0.ts
+ hls/400x300_seg0_5s.ts
+ hls/400x300_seg1.ts
+ hls/416x243_seg0_5s.ts
+ hls/640x480_seg0.ts
+ hls/640x480_seg1.ts
+ hls/960x720_seg0.ts
+ hls/960x720_seg1.ts
+ sync.webm
+
+[test_streams_capture_origin.html]
+[test_streams_element_capture.html]
+skip-if = true # bug 1372457 # bug 1557901 # bug 1554808
+[test_streams_element_capture_mediatrack.html]
+[test_streams_element_capture_playback.html]
+[test_streams_element_capture_reset.html]
+[test_streams_element_capture_twice.html]
+[test_streams_firstframe.html]
+[test_streams_gc.html]
+[test_streams_individual_pause.html]
+skip-if =
+ os == "mac" && debug # Bug 1756880 - temp due to low frequency shutdown hang
+scheme=https
+tags=mtg
+[test_streams_srcObject.html]
+skip-if = toolkit == 'android' # bug 1300443, android(bug 1232305)
+ os == "mac" && debug # Bug 1756880 - temp due to high frequency shutdown hang
+[test_streams_tracks.html]
+skip-if = toolkit == 'android' # android(bug 1232305)
diff --git a/dom/media/test/multi_id3v2.mp3 b/dom/media/test/multi_id3v2.mp3
new file mode 100644
index 0000000000..253f19a9b6
--- /dev/null
+++ b/dom/media/test/multi_id3v2.mp3
Binary files differ
diff --git a/dom/media/test/multiple-bos-more-header-fileds.ogg b/dom/media/test/multiple-bos-more-header-fileds.ogg
new file mode 100644
index 0000000000..c9721cb98e
--- /dev/null
+++ b/dom/media/test/multiple-bos-more-header-fileds.ogg
Binary files differ
diff --git a/dom/media/test/multiple-bos-more-header-fileds.ogg^headers^ b/dom/media/test/multiple-bos-more-header-fileds.ogg^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/multiple-bos-more-header-fileds.ogg^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/multiple-bos.ogg b/dom/media/test/multiple-bos.ogg
new file mode 100644
index 0000000000..193200868e
--- /dev/null
+++ b/dom/media/test/multiple-bos.ogg
Binary files differ
diff --git a/dom/media/test/multiple-bos.ogg^headers^ b/dom/media/test/multiple-bos.ogg^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/multiple-bos.ogg^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/no-container-codec-delay.webm b/dom/media/test/no-container-codec-delay.webm
new file mode 100644
index 0000000000..9c1ef8e6d7
--- /dev/null
+++ b/dom/media/test/no-container-codec-delay.webm
Binary files differ
diff --git a/dom/media/test/no-cues.webm b/dom/media/test/no-cues.webm
new file mode 100644
index 0000000000..8ed761099e
--- /dev/null
+++ b/dom/media/test/no-cues.webm
Binary files differ
diff --git a/dom/media/test/no-cues.webm^headers^ b/dom/media/test/no-cues.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/no-cues.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/notags.mp3 b/dom/media/test/notags.mp3
new file mode 100644
index 0000000000..7f298131aa
--- /dev/null
+++ b/dom/media/test/notags.mp3
Binary files differ
diff --git a/dom/media/test/notags.mp3^headers^ b/dom/media/test/notags.mp3^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/notags.mp3^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/opus-mapping2.mp4 b/dom/media/test/opus-mapping2.mp4
new file mode 100644
index 0000000000..72401a9c0b
--- /dev/null
+++ b/dom/media/test/opus-mapping2.mp4
Binary files differ
diff --git a/dom/media/test/opus-mapping2.mp4^headers^ b/dom/media/test/opus-mapping2.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/opus-mapping2.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/opus-mapping2.webm b/dom/media/test/opus-mapping2.webm
new file mode 100644
index 0000000000..4379f2534a
--- /dev/null
+++ b/dom/media/test/opus-mapping2.webm
Binary files differ
diff --git a/dom/media/test/opus-mapping2.webm^headers^ b/dom/media/test/opus-mapping2.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/opus-mapping2.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/opus-sample-cenc.mp4 b/dom/media/test/opus-sample-cenc.mp4
new file mode 100644
index 0000000000..22bb787540
--- /dev/null
+++ b/dom/media/test/opus-sample-cenc.mp4
Binary files differ
diff --git a/dom/media/test/opus-sample-cenc.mp4^headers^ b/dom/media/test/opus-sample-cenc.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/opus-sample-cenc.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/opus-sample.mp4 b/dom/media/test/opus-sample.mp4
new file mode 100644
index 0000000000..80329ce14b
--- /dev/null
+++ b/dom/media/test/opus-sample.mp4
Binary files differ
diff --git a/dom/media/test/opus-sample.mp4^headers^ b/dom/media/test/opus-sample.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/opus-sample.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/owl-funnier-id3.mp3 b/dom/media/test/owl-funnier-id3.mp3
new file mode 100644
index 0000000000..05ec507530
--- /dev/null
+++ b/dom/media/test/owl-funnier-id3.mp3
Binary files differ
diff --git a/dom/media/test/owl-funnier-id3.mp3^headers^ b/dom/media/test/owl-funnier-id3.mp3^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/owl-funnier-id3.mp3^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/owl-funny-id3.mp3 b/dom/media/test/owl-funny-id3.mp3
new file mode 100644
index 0000000000..6533755a32
--- /dev/null
+++ b/dom/media/test/owl-funny-id3.mp3
Binary files differ
diff --git a/dom/media/test/owl-funny-id3.mp3^headers^ b/dom/media/test/owl-funny-id3.mp3^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/owl-funny-id3.mp3^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/owl-short.mp3 b/dom/media/test/owl-short.mp3
new file mode 100644
index 0000000000..9b31531f22
--- /dev/null
+++ b/dom/media/test/owl-short.mp3
Binary files differ
diff --git a/dom/media/test/owl-short.mp3^headers^ b/dom/media/test/owl-short.mp3^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/owl-short.mp3^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/owl.mp3 b/dom/media/test/owl.mp3
new file mode 100644
index 0000000000..9fafa32f93
--- /dev/null
+++ b/dom/media/test/owl.mp3
Binary files differ
diff --git a/dom/media/test/owl.mp3^headers^ b/dom/media/test/owl.mp3^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/owl.mp3^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/padding-spanning-multiple-packets.mp3 b/dom/media/test/padding-spanning-multiple-packets.mp3
new file mode 100644
index 0000000000..d7ab9dc3f5
--- /dev/null
+++ b/dom/media/test/padding-spanning-multiple-packets.mp3
Binary files differ
diff --git a/dom/media/test/pixel_aspect_ratio.mp4 b/dom/media/test/pixel_aspect_ratio.mp4
new file mode 100644
index 0000000000..fce12cc03e
--- /dev/null
+++ b/dom/media/test/pixel_aspect_ratio.mp4
Binary files differ
diff --git a/dom/media/test/play_promise.js b/dom/media/test/play_promise.js
new file mode 100644
index 0000000000..7051fedc19
--- /dev/null
+++ b/dom/media/test/play_promise.js
@@ -0,0 +1,3 @@
+function getNotSupportedFile(name) {
+ return name + ".bad";
+}
diff --git a/dom/media/test/poster-test.jpg b/dom/media/test/poster-test.jpg
new file mode 100644
index 0000000000..595a5315f8
--- /dev/null
+++ b/dom/media/test/poster-test.jpg
Binary files differ
diff --git a/dom/media/test/r11025_msadpcm_c1.wav b/dom/media/test/r11025_msadpcm_c1.wav
new file mode 100644
index 0000000000..2e883ba5ed
--- /dev/null
+++ b/dom/media/test/r11025_msadpcm_c1.wav
Binary files differ
diff --git a/dom/media/test/r11025_msadpcm_c1.wav^headers^ b/dom/media/test/r11025_msadpcm_c1.wav^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/r11025_msadpcm_c1.wav^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/r11025_s16_c1-short.wav b/dom/media/test/r11025_s16_c1-short.wav
new file mode 100644
index 0000000000..e08d5bbdc0
--- /dev/null
+++ b/dom/media/test/r11025_s16_c1-short.wav
Binary files differ
diff --git a/dom/media/test/r11025_s16_c1-short.wav^headers^ b/dom/media/test/r11025_s16_c1-short.wav^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/r11025_s16_c1-short.wav^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/r11025_s16_c1.wav b/dom/media/test/r11025_s16_c1.wav
new file mode 100644
index 0000000000..ab2e08befb
--- /dev/null
+++ b/dom/media/test/r11025_s16_c1.wav
Binary files differ
diff --git a/dom/media/test/r11025_s16_c1.wav^headers^ b/dom/media/test/r11025_s16_c1.wav^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/r11025_s16_c1.wav^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/r11025_s16_c1_trailing.wav b/dom/media/test/r11025_s16_c1_trailing.wav
new file mode 100644
index 0000000000..af53beaf25
--- /dev/null
+++ b/dom/media/test/r11025_s16_c1_trailing.wav
Binary files differ
diff --git a/dom/media/test/r11025_s16_c1_trailing.wav^headers^ b/dom/media/test/r11025_s16_c1_trailing.wav^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/r11025_s16_c1_trailing.wav^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/r11025_u8_c1.wav b/dom/media/test/r11025_u8_c1.wav
new file mode 100644
index 0000000000..97dc453b9e
--- /dev/null
+++ b/dom/media/test/r11025_u8_c1.wav
Binary files differ
diff --git a/dom/media/test/r11025_u8_c1.wav^headers^ b/dom/media/test/r11025_u8_c1.wav^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/r11025_u8_c1.wav^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/r11025_u8_c1_trunc.wav b/dom/media/test/r11025_u8_c1_trunc.wav
new file mode 100644
index 0000000000..4d2db39777
--- /dev/null
+++ b/dom/media/test/r11025_u8_c1_trunc.wav
Binary files differ
diff --git a/dom/media/test/r11025_u8_c1_trunc.wav^headers^ b/dom/media/test/r11025_u8_c1_trunc.wav^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/r11025_u8_c1_trunc.wav^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/r16000_u8_c1_list.wav b/dom/media/test/r16000_u8_c1_list.wav
new file mode 100644
index 0000000000..afde32e9a3
--- /dev/null
+++ b/dom/media/test/r16000_u8_c1_list.wav
Binary files differ
diff --git a/dom/media/test/r16000_u8_c1_list.wav^headers^ b/dom/media/test/r16000_u8_c1_list.wav^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/r16000_u8_c1_list.wav^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/rdd_process_xpcom/RddProcessTest.cpp b/dom/media/test/rdd_process_xpcom/RddProcessTest.cpp
new file mode 100644
index 0000000000..fad7d6ee2e
--- /dev/null
+++ b/dom/media/test/rdd_process_xpcom/RddProcessTest.cpp
@@ -0,0 +1,69 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if defined(ENABLE_TESTS)
+# include "mozilla/RDDProcessManager.h"
+# include "mozilla/RDDChild.h"
+# include "mozilla/RddProcessTest.h"
+# include "mozilla/dom/Promise.h"
+
+namespace mozilla {
+
+NS_IMETHODIMP
+RddProcessTest::TestTelemetryProbes(JSContext* aCx,
+ mozilla::dom::Promise** aOutPromise) {
+ NS_ENSURE_ARG(aOutPromise);
+ *aOutPromise = nullptr;
+ nsIGlobalObject* global = xpc::CurrentNativeGlobal(aCx);
+ if (NS_WARN_IF(!global)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ErrorResult erv;
+ RefPtr<dom::Promise> promise = dom::Promise::Create(global, erv);
+ if (NS_WARN_IF(erv.Failed())) {
+ return erv.StealNSResult();
+ }
+
+ RDDProcessManager* rddProc = RDDProcessManager::Get();
+ MOZ_ASSERT(rddProc, "No RddProcessManager?");
+
+ rddProc->LaunchRDDProcess()->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [promise, rddProc]() {
+ RDDChild* child = rddProc->GetRDDChild();
+ if (!rddProc) {
+ promise->MaybeReject(NS_ERROR_NOT_AVAILABLE);
+ }
+ MOZ_ASSERT(rddProc, "No RDD Proc?");
+
+ if (!child) {
+ promise->MaybeReject(NS_ERROR_NOT_AVAILABLE);
+ }
+ MOZ_ASSERT(child, "No RDD Child?");
+
+ Unused << child->SendTestTelemetryProbes();
+ promise->MaybeResolve((int32_t)rddProc->RDDProcessPid());
+ },
+ [promise](nsresult aError) {
+ MOZ_ASSERT_UNREACHABLE("RddProcessTest; failure to get RDD child");
+ promise->MaybeReject(aError);
+ });
+
+ promise.forget(aOutPromise);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+RddProcessTest::StopProcess() {
+ RDDProcessManager::RDDProcessShutdown();
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(RddProcessTest, nsIRddProcessTest)
+
+} // namespace mozilla
+#endif // defined(ENABLE_TESTS)
diff --git a/dom/media/test/rdd_process_xpcom/RddProcessTest.h b/dom/media/test/rdd_process_xpcom/RddProcessTest.h
new file mode 100644
index 0000000000..b0281df45b
--- /dev/null
+++ b/dom/media/test/rdd_process_xpcom/RddProcessTest.h
@@ -0,0 +1,28 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef _include_dom_media_RddProcessTest_h_
+#define _include_dom_media_RddProcessTest_h_
+
+#if defined(ENABLE_TESTS)
+# include "nsIRddProcessTest.h"
+
+namespace mozilla {
+
+class RddProcessTest final : public nsIRddProcessTest {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIRDDPROCESSTEST
+
+ RddProcessTest() = default;
+
+ private:
+ ~RddProcessTest() = default;
+};
+
+} // namespace mozilla
+#endif // defined(ENABLE_TESTS)
+
+#endif // _include_dom_media_RddProcessTest_h_
diff --git a/dom/media/test/rdd_process_xpcom/components.conf b/dom/media/test/rdd_process_xpcom/components.conf
new file mode 100644
index 0000000000..507559851b
--- /dev/null
+++ b/dom/media/test/rdd_process_xpcom/components.conf
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+ {
+ 'cid': '{12f7d302-5368-412d-bdc9-26d151518e6c}',
+ 'contract_ids': ['@mozilla.org/rdd-process-test;1'],
+ 'type': 'mozilla::RddProcessTest',
+ 'headers': ['mozilla/RddProcessTest.h'],
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+]
diff --git a/dom/media/test/rdd_process_xpcom/moz.build b/dom/media/test/rdd_process_xpcom/moz.build
new file mode 100644
index 0000000000..87dbb39bc1
--- /dev/null
+++ b/dom/media/test/rdd_process_xpcom/moz.build
@@ -0,0 +1,21 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS.mozilla += ["RddProcessTest.h"]
+
+UNIFIED_SOURCES += ["RddProcessTest.cpp"]
+
+XPCOM_MANIFESTS += ["components.conf"]
+
+XPIDL_MODULE = "rdd_process_xpcom_test"
+
+XPIDL_SOURCES += [
+ "nsIRddProcessTest.idl",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/test/rdd_process_xpcom/nsIRddProcessTest.idl b/dom/media/test/rdd_process_xpcom/nsIRddProcessTest.idl
new file mode 100644
index 0000000000..a10fdbd249
--- /dev/null
+++ b/dom/media/test/rdd_process_xpcom/nsIRddProcessTest.idl
@@ -0,0 +1,25 @@
+/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+[scriptable, uuid(12f7d302-5368-412d-bdc9-26d151518e6c)]
+interface nsIRddProcessTest : nsISupports
+{
+ /**
+ * ** Test-only Method **
+ *
+ * Sending Telemetry probes
+ */
+ [implicit_jscontext]
+ Promise testTelemetryProbes();
+
+ /**
+ * ** Test-only Method **
+ *
+ * Stop existing RDD process
+ */
+ void stopProcess();
+};
diff --git a/dom/media/test/reactivate_helper.html b/dom/media/test/reactivate_helper.html
new file mode 100644
index 0000000000..1834131559
--- /dev/null
+++ b/dom/media/test/reactivate_helper.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+<script>
+var loadsWaiting = 0;
+var elements = [];
+
+function checkAllLoaded() {
+ --loadsWaiting;
+ if (loadsWaiting == 0) {
+ parent.loadedAll(elements);
+ }
+}
+
+function loadedData(event) {
+ var e = event.target;
+ parent.ok(!elements.includes(e), "Element already loaded: " + e._name);
+ parent.info("Loaded " + e._name);
+ elements.push(e);
+ // Reset "onerror" handler to avoid triggering another error in removeNodeAndSource().
+ e.onerror = null;
+ checkAllLoaded();
+
+}
+
+function error(event) {
+ var e = event.target;
+ parent.info("Error " + e._name);
+ // Don't wait for the element encounting errors.
+ checkAllLoaded();
+}
+
+for (var i = 0; i < parent.gSmallTests.length; ++i) {
+ var test = parent.gSmallTests[i];
+ var elemType = /^audio/.test(test.type) ? "audio" : "video";
+ // Associate these elements with the subframe's document
+ var e = document.createElement(elemType);
+ e.preload = "metadata";
+ if (e.canPlayType(test.type)) {
+ e.src = test.name;
+ e._name = test.name;
+ e.onloadeddata = loadedData;
+ e.onerror = error;
+ e.load();
+ ++loadsWaiting;
+ parent.info("Loading " + e._name);
+ }
+}
+
+if (loadsWaiting == 0) {
+ parent.todo(false, "Can't play anything");
+} else {
+ parent.SimpleTest.waitForExplicitFinish();
+}
+</script>
+</body>
+</html>
diff --git a/dom/media/test/red-46x48.mp4 b/dom/media/test/red-46x48.mp4
new file mode 100644
index 0000000000..0760cc1c16
--- /dev/null
+++ b/dom/media/test/red-46x48.mp4
Binary files differ
diff --git a/dom/media/test/red-46x48.mp4^headers^ b/dom/media/test/red-46x48.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/red-46x48.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/red-48x46.mp4 b/dom/media/test/red-48x46.mp4
new file mode 100644
index 0000000000..d83de4027d
--- /dev/null
+++ b/dom/media/test/red-48x46.mp4
Binary files differ
diff --git a/dom/media/test/red-48x46.mp4^headers^ b/dom/media/test/red-48x46.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/red-48x46.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/redirect.sjs b/dom/media/test/redirect.sjs
new file mode 100644
index 0000000000..6873fd52f3
--- /dev/null
+++ b/dom/media/test/redirect.sjs
@@ -0,0 +1,35 @@
+function parseQuery(request, key) {
+ var params = request.queryString.split("&");
+ for (var j = 0; j < params.length; ++j) {
+ var p = params[j];
+ if (p == key) {
+ return true;
+ }
+ if (p.indexOf(key + "=") == 0) {
+ return p.substring(key.length + 1);
+ }
+ if (!p.includes("=") && key == "") {
+ return p;
+ }
+ }
+ return false;
+}
+
+// Return file content for the first request with a given key.
+// All subsequent requests return a redirect to a different-origin resource.
+function handleRequest(request, response) {
+ var domain = parseQuery(request, "domain");
+ var file = parseQuery(request, "file");
+ var allowed = parseQuery(request, "allowed");
+
+ response.setStatusLine(request.httpVersion, 303, "See Other");
+ response.setHeader(
+ "Location",
+ "http://" +
+ domain +
+ "/tests/dom/media/test/" +
+ (allowed ? "allowed.sjs?" : "") +
+ file
+ );
+ response.setHeader("Content-Type", "text/html");
+}
diff --git a/dom/media/test/referer.sjs b/dom/media/test/referer.sjs
new file mode 100644
index 0000000000..0c699c4ae1
--- /dev/null
+++ b/dom/media/test/referer.sjs
@@ -0,0 +1,49 @@
+function parseQuery(request, key) {
+ var params = request.queryString.split("&");
+ for (var j = 0; j < params.length; ++j) {
+ var p = params[j];
+ if (p == key) {
+ return true;
+ }
+ if (p.indexOf(key + "=") == 0) {
+ return p.substring(key.length + 1);
+ }
+ if (!p.includes("=") && key == "") {
+ return p;
+ }
+ }
+ return false;
+}
+
+function handleRequest(request, response) {
+ var referer = request.hasHeader("Referer")
+ ? request.getHeader("Referer")
+ : undefined;
+ if (
+ referer == "http://mochi.test:8888/tests/dom/media/test/test_referer.html"
+ ) {
+ var name = parseQuery(request, "name");
+ var type = parseQuery(request, "type");
+ var file = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
+ var fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ var bis = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+ Ci.nsIBinaryInputStream
+ );
+ var paths = "tests/dom/media/test/" + name;
+ var split = paths.split("/");
+ for (var i = 0; i < split.length; ++i) {
+ file.append(split[i]);
+ }
+ fis.init(file, -1, -1, false);
+ bis.setInputStream(fis);
+ var bytes = bis.readBytes(bis.available());
+ response.setHeader("Content-Length", "" + bytes.length, false);
+ response.setHeader("Content-Type", type, false);
+ response.write(bytes, bytes.length);
+ bis.close();
+ } else {
+ response.setStatusLine(request.httpVersion, 404, "Not found");
+ }
+}
diff --git a/dom/media/test/reftest/av1hdr2020.mp4 b/dom/media/test/reftest/av1hdr2020.mp4
new file mode 100644
index 0000000000..295bec8a3c
--- /dev/null
+++ b/dom/media/test/reftest/av1hdr2020.mp4
Binary files differ
diff --git a/dom/media/test/reftest/av1hdr2020.png b/dom/media/test/reftest/av1hdr2020.png
new file mode 100644
index 0000000000..c5d3344a80
--- /dev/null
+++ b/dom/media/test/reftest/av1hdr2020.png
Binary files differ
diff --git a/dom/media/test/reftest/bipbop_300_215kbps.mp4.lastframe-ref.html b/dom/media/test/reftest/bipbop_300_215kbps.mp4.lastframe-ref.html
new file mode 100644
index 0000000000..575acb107d
--- /dev/null
+++ b/dom/media/test/reftest/bipbop_300_215kbps.mp4.lastframe-ref.html
@@ -0,0 +1,4 @@
+<!DOCTYPE HTML>
+<img style="position:absolute; left:0; top:0"
+src="data:image/png;base64,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"
+>
diff --git a/dom/media/test/reftest/bipbop_300_215kbps.mp4.lastframe.html b/dom/media/test/reftest/bipbop_300_215kbps.mp4.lastframe.html
new file mode 100644
index 0000000000..600b04a4f0
--- /dev/null
+++ b/dom/media/test/reftest/bipbop_300_215kbps.mp4.lastframe.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+<script type="text/javascript">
+function doTest() {
+ var video = document.getElementById("v1");
+ video.src = "../bipbop_300_215kbps.mp4";
+ video.play();
+ video.addEventListener("ended", function() {
+ document.documentElement.removeAttribute('class');
+ }, {once: true});
+}
+window.addEventListener("MozReftestInvalidate", doTest);
+</script>
+</head>
+<body>
+<video id="v1" style="position:absolute; left:0; top:0"></video>
+</body>
+</html>
diff --git a/dom/media/test/reftest/color_quads/720p.png b/dom/media/test/reftest/color_quads/720p.png
new file mode 100644
index 0000000000..cf3f2408c1
--- /dev/null
+++ b/dom/media/test/reftest/color_quads/720p.png
Binary files differ
diff --git a/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.gbrp.av1.mp4 b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.gbrp.av1.mp4
new file mode 100644
index 0000000000..fa1801013a
--- /dev/null
+++ b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.gbrp.av1.mp4
Binary files differ
diff --git a/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.gbrp.av1.webm b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.gbrp.av1.webm
new file mode 100644
index 0000000000..96fd46f848
--- /dev/null
+++ b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.gbrp.av1.webm
Binary files differ
diff --git a/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.gbrp.h264.mp4 b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.gbrp.h264.mp4
new file mode 100644
index 0000000000..c5f08a56f2
--- /dev/null
+++ b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.gbrp.h264.mp4
Binary files differ
diff --git a/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.gbrp.vp9.mp4 b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.gbrp.vp9.mp4
new file mode 100644
index 0000000000..c64f7eb8d2
--- /dev/null
+++ b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.gbrp.vp9.mp4
Binary files differ
diff --git a/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.gbrp.vp9.webm b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.gbrp.vp9.webm
new file mode 100644
index 0000000000..d4341d22ed
--- /dev/null
+++ b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.gbrp.vp9.webm
Binary files differ
diff --git a/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p.av1.mp4 b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p.av1.mp4
new file mode 100644
index 0000000000..ac89827620
--- /dev/null
+++ b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p.av1.mp4
Binary files differ
diff --git a/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p.av1.webm b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p.av1.webm
new file mode 100644
index 0000000000..1544ef996e
--- /dev/null
+++ b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p.av1.webm
Binary files differ
diff --git a/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p.h264.mp4 b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p.h264.mp4
new file mode 100644
index 0000000000..041d7b4477
--- /dev/null
+++ b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p.h264.mp4
Binary files differ
diff --git a/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p.vp9.mp4 b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p.vp9.mp4
new file mode 100644
index 0000000000..e9cdbb5f51
--- /dev/null
+++ b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p.vp9.mp4
Binary files differ
diff --git a/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p.vp9.webm b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p.vp9.webm
new file mode 100644
index 0000000000..6d0b53f002
--- /dev/null
+++ b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p.vp9.webm
Binary files differ
diff --git a/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p10.av1.mp4 b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p10.av1.mp4
new file mode 100644
index 0000000000..f30c731bf6
--- /dev/null
+++ b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p10.av1.mp4
Binary files differ
diff --git a/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p10.av1.webm b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p10.av1.webm
new file mode 100644
index 0000000000..0e733057d8
--- /dev/null
+++ b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p10.av1.webm
Binary files differ
diff --git a/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p10.h264.mp4 b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p10.h264.mp4
new file mode 100644
index 0000000000..9a4783f2db
--- /dev/null
+++ b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p10.h264.mp4
Binary files differ
diff --git a/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p10.vp9.mp4 b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p10.vp9.mp4
new file mode 100644
index 0000000000..00565d66d3
--- /dev/null
+++ b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p10.vp9.mp4
Binary files differ
diff --git a/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p10.vp9.webm b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p10.vp9.webm
new file mode 100644
index 0000000000..08159b5233
--- /dev/null
+++ b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.pc.yuv420p10.vp9.webm
Binary files differ
diff --git a/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.gbrp.av1.mp4 b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.gbrp.av1.mp4
new file mode 100644
index 0000000000..3a50d11691
--- /dev/null
+++ b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.gbrp.av1.mp4
Binary files differ
diff --git a/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.gbrp.av1.webm b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.gbrp.av1.webm
new file mode 100644
index 0000000000..c18de4bf4f
--- /dev/null
+++ b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.gbrp.av1.webm
Binary files differ
diff --git a/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.gbrp.h264.mp4 b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.gbrp.h264.mp4
new file mode 100644
index 0000000000..9d735cf9ed
--- /dev/null
+++ b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.gbrp.h264.mp4
Binary files differ
diff --git a/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.gbrp.vp9.mp4 b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.gbrp.vp9.mp4
new file mode 100644
index 0000000000..79dcb0d72d
--- /dev/null
+++ b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.gbrp.vp9.mp4
Binary files differ
diff --git a/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.gbrp.vp9.webm b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.gbrp.vp9.webm
new file mode 100644
index 0000000000..31af47cba1
--- /dev/null
+++ b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.gbrp.vp9.webm
Binary files differ
diff --git a/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p.av1.mp4 b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p.av1.mp4
new file mode 100644
index 0000000000..dfcac969b9
--- /dev/null
+++ b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p.av1.mp4
Binary files differ
diff --git a/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p.av1.webm b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p.av1.webm
new file mode 100644
index 0000000000..00f46b0597
--- /dev/null
+++ b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p.av1.webm
Binary files differ
diff --git a/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p.h264.mp4 b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p.h264.mp4
new file mode 100644
index 0000000000..ca8b8a1572
--- /dev/null
+++ b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p.h264.mp4
Binary files differ
diff --git a/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p.vp9.mp4 b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p.vp9.mp4
new file mode 100644
index 0000000000..19ff1b4265
--- /dev/null
+++ b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p.vp9.mp4
Binary files differ
diff --git a/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p.vp9.webm b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p.vp9.webm
new file mode 100644
index 0000000000..6fd0aa6756
--- /dev/null
+++ b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p.vp9.webm
Binary files differ
diff --git a/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p10.av1.mp4 b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p10.av1.mp4
new file mode 100644
index 0000000000..e95fd5c9fb
--- /dev/null
+++ b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p10.av1.mp4
Binary files differ
diff --git a/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p10.av1.webm b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p10.av1.webm
new file mode 100644
index 0000000000..f28e65f034
--- /dev/null
+++ b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p10.av1.webm
Binary files differ
diff --git a/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p10.h264.mp4 b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p10.h264.mp4
new file mode 100644
index 0000000000..1fe6824cb4
--- /dev/null
+++ b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p10.h264.mp4
Binary files differ
diff --git a/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p10.vp9.mp4 b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p10.vp9.mp4
new file mode 100644
index 0000000000..532ae19359
--- /dev/null
+++ b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p10.vp9.mp4
Binary files differ
diff --git a/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p10.vp9.webm b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p10.vp9.webm
new file mode 100644
index 0000000000..e4aad890f5
--- /dev/null
+++ b/dom/media/test/reftest/color_quads/720p.png.bt709.bt709.tv.yuv420p10.vp9.webm
Binary files differ
diff --git a/dom/media/test/reftest/color_quads/reftest.list b/dom/media/test/reftest/color_quads/reftest.list
new file mode 100644
index 0000000000..a5ad475b87
--- /dev/null
+++ b/dom/media/test/reftest/color_quads/reftest.list
@@ -0,0 +1,69 @@
+# Reference image generated via https://jdashg.github.io/misc/colors/color-quads-16-127-235.html
+# Test videos encoded via ../gen_combos.py --write color_quads/720p.png
+
+# We're sort of testing two things here:
+# 1. Does a av1.webm video into the actual values we expect?
+# 2. Do other similar videos decode the same was as av1.webm?
+# We have this split because while each platform/compositor has its own inaccuracies,
+# each platform/compositor will have the *same* inaccuracies regardless of video.
+# So, we just need to first check if e.g. av1.webm decodes to what we expect,
+# and then we have generally trivially compare other codecs/containers to that.
+
+defaults pref(media.av1.enabled,true)
+
+# -
+# yuv420p
+
+fuzzy(16-51,5234-5622) fuzzy-if(swgl,32-38,1600-91746) fuzzy-if(useDrawSnapshot,16-16,11600-11600) fuzzy-if(OSX,16-73,5212-5622) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.yuv420p.av1.webm ../reftest_img.html?src=color_quads/720p.png
+fuzzy-if(winWidget&&swgl,0-20,0-5620) fuzzy-if(winWidget&&!swgl,0-1,0-78) fuzzy-if(Android,254-255,273680-273807) fuzzy-if(OSX,0-35,0-1947) fuzzy-if(OSX&&swgl,0-67,0-5451) fuzzy-if(appleSilicon,30-48,1760-187409) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.yuv420p.vp9.webm ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.yuv420p.av1.webm
+fuzzy-if(winWidget,0-1,0-78) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.yuv420p.av1.mp4 ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.yuv420p.av1.webm
+skip-if(winWidget&&isCoverageBuild) fuzzy(0-16,75-1941) fuzzy-if(Android,254-255,273680-273807) fuzzy-if(OSX,30-32,187326-187407) fuzzy-if(appleSilicon,30-48,1835-187409) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.yuv420p.h264.mp4 ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.yuv420p.av1.webm
+fuzzy-if(winWidget&&swgl,0-20,0-5620) fuzzy-if(winWidget&&!swgl,0-1,0-78) fuzzy-if(Android,254-255,273680-273807) fuzzy-if(OSX,0-35,0-1947) fuzzy-if(OSX&&swgl,0-67,0-5451) fuzzy-if(appleSilicon,30-48,1760-187409) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.yuv420p.vp9.mp4 ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.yuv420p.av1.webm
+
+skip-if(Android) fuzzy(16-48,8107-8818) fuzzy-if(winWidget&&swgl,31-38,8240-184080) fuzzy-if(appleSilicon,33-38,8819-11705) fuzzy-if(useDrawSnapshot,20-20,187200-187200) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.pc.yuv420p.av1.webm ../reftest_img.html?src=color_quads/720p.png
+skip-if(Android) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.pc.yuv420p.av1.mp4 ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.pc.yuv420p.av1.webm
+# On Windows & sw render, we noticed that the comparison image captured from AV1 is not equal to its displayed video frame, so we would need to compare other codecs directly to PNG file. That should be fixed in bug 1748540.
+skip-if(Android) skip-if(winWidget&&swgl) fuzzy-if(Android,255-255,273726-273726) fuzzy-if(OSX,0-16,0-1718) fuzzy-if(OSX&&swgl,0-20,0-2423) fuzzy-if(appleSilicon,0-16,0-1874) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.pc.yuv420p.vp9.webm ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.pc.yuv420p.av1.webm
+skip-if(Android) skip-if(winWidget&&swgl) fuzzy-if(Android,255-255,273726-273726) fuzzy-if(OSX,2-36,184281-187407) fuzzy-if(winWidget,0-21,0-360000) fuzzy-if(appleSilicon,36-49,187329-187407) fuzzy-if(useDrawSnapshot,0-1,0-10) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.pc.yuv420p.h264.mp4 ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.pc.yuv420p.av1.webm
+skip-if(Android) skip-if(winWidget&&swgl) fuzzy-if(Android,255-255,273726-273726) fuzzy-if(OSX,0-16,0-1718) fuzzy-if(OSX&&swgl,0-20,0-2423) fuzzy-if(appleSilicon,0-16,0-1874) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.pc.yuv420p.vp9.mp4 ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.pc.yuv420p.av1.webm
+skip-if(Android) skip-if(!(winWidget&&swgl)) fuzzy(0-35,0-8506) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.pc.yuv420p.vp9.webm ../reftest_img.html?src=color_quads/720p.png
+skip-if(Android) skip-if(!(winWidget&&swgl)) fuzzy(0-35,0-8506) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.pc.yuv420p.vp9.mp4 ../reftest_img.html?src=color_quads/720p.png
+
+# -
+# yuv420p10
+
+skip-if(Android) fuzzy(33-49,1870-2579) fuzzy-if(swgl,34-52,180421-270528) fuzzy-if(useDrawSnapshot,16-16,183840-183840) fuzzy-if(OSX,60-74,270329-271024) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.yuv420p10.av1.webm ../reftest_img.html?src=color_quads/720p.png
+skip-if(Android) fuzzy-if(OSX,0-12,0-187770) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.yuv420p10.vp9.webm ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.yuv420p10.av1.webm
+skip-if(Android) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.yuv420p10.av1.mp4 ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.yuv420p10.av1.webm
+#[2] skip-if(Android) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.yuv420p10.h264.mp4 ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.yuv420p10.av1.webm
+skip-if(Android) fuzzy-if(OSX,0-12,0-187770) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.yuv420p10.vp9.mp4 ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.yuv420p10.av1.webm
+
+skip-if(Android) fuzzy(33-49,174620-270059) fuzzy-if(swgl&&!winWidget,36-52,11553-11555) fuzzy-if(swgl&&winWidget,36-52,11554-187200) fuzzy-if(swgl&&OSX,34-50,11465-270059) fuzzy-if(useDrawSnapshot,20-20,186800-186800) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.pc.yuv420p10.av1.webm ../reftest_img.html?src=color_quads/720p.png
+skip-if(Android) fuzzy-if(OSX,0-12,0-274122) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.pc.yuv420p10.vp9.webm ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.pc.yuv420p10.av1.webm
+skip-if(Android) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.pc.yuv420p10.av1.mp4 ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.pc.yuv420p10.av1.webm
+#[2] skip-if(Android) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.pc.yuv420p10.h264.mp4 ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.pc.yuv420p10.av1.webm
+skip-if(Android) fuzzy-if(OSX,0-12,0-274122) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.pc.yuv420p10.vp9.mp4 ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.pc.yuv420p10.av1.webm
+
+# Android is really broken in a variety of ways for p10.
+#[2]: yuv420p10 broken in h264.mp4: https://bugzilla.mozilla.org/show_bug.cgi?id=1711812
+
+
+# -
+# gbrp
+# Note: tv-gbrp doesn't really make sense, and we should consider dropping it.
+# Specifically, we should probably do (gbrp, ...(tv,pc)x(yuv,yuv10)) instead of (tv,pc)x(gbrp,yuv,yuv10)
+# That said, we should probably test a couple combos, at least. (But then again, why not all!)
+
+skip-if(winWidget&&swgl) fuzzy(0-1,0-3600) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.gbrp.av1.webm ../reftest_img.html?src=color_quads/720p.png
+skip-if(winWidget&&swgl) fuzzy(0-1,0-7200) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.pc.gbrp.av1.webm ../reftest_img.html?src=color_quads/720p.png
+
+== ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.gbrp.av1.mp4 ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.gbrp.av1.webm
+== ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.pc.gbrp.av1.mp4 ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.pc.gbrp.av1.webm
+
+# Our h264.mp4 doesn't handle gbrp, but *also* doesn't error properly.
+== ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.gbrp.h264.mp4 ../reftest_video.html?src=timeout
+== ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.pc.gbrp.h264.mp4 ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.gbrp.h264.mp4
+
+fuzzy(0-1,0-3600) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.pc.gbrp.vp9.webm ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.gbrp.vp9.webm
+== ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.gbrp.vp9.mp4 ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.gbrp.vp9.webm
+fuzzy(0-1,0-3600) == ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.pc.gbrp.vp9.mp4 ../reftest_video.html?src=color_quads/720p.png.bt709.bt709.tv.gbrp.vp9.webm
diff --git a/dom/media/test/reftest/frame_order.mp4 b/dom/media/test/reftest/frame_order.mp4
new file mode 100644
index 0000000000..87b65f4386
--- /dev/null
+++ b/dom/media/test/reftest/frame_order.mp4
Binary files differ
diff --git a/dom/media/test/reftest/frame_order_mp4-ref.html b/dom/media/test/reftest/frame_order_mp4-ref.html
new file mode 100644
index 0000000000..0c59debcfa
--- /dev/null
+++ b/dom/media/test/reftest/frame_order_mp4-ref.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<head>
+</head>
+<body>
+<!--
+ This ref image is generated by capturing video frame on 0.3s from
+ 'frame_order.mp4'
+-->
+<image style="position:absolute; left:0; top:0" src="data:image/png;base64,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QQ0AAIGgBgCAQFADAEAgqAEAIBDUAAAQCGoAAAgENQAABIIaAAACQQ0AAIGgBgCAQFADAEAgqAEAIBDUAAAQCGoAAAgENQAABIIaAAACQQ0AAIGgBgCAQFADAEAgqAEAIBDUAAAQCGoAAAgENQAABIIaAAACQQ0AAIGgBgCAQFADAEAgqAEAIBDUAAAQCGoAAAgENQAABIIaAAACQQ0AAIGgBgCAQFADAEAgqAEAIBDUAAAQCGoAAAgENQAABIIaAAACQQ0AAIGgBgCAQFADAEAgqAEAIBDUAAAQCGoAAAgENQAABIIaAAACQQ0AAIGgBgCAQFADAEAgqAEAIBDUAAAQCGoAAAgENQAABIIaAAACQQ0AAIGgBgCAQFADAEAgqAEAIBDUAAAQCGoAAAgENQAABIIaAAACQQ0AAIGgBgCAQFADAEAgqAEAIBDUAAAQCGoAAAgENQAABIIaAAACQQ0AAIGgBgCAQFADAEAgqAEAIBDUAAAQCGoAAAgENQAABIIaAAACQQ0AAIGgBgCAQFADAEAgqAEAIBDUAAAQCGoAAAgENQAABIIaAAACQQ0AAIGgBgCAQFADAEAgqAEAIBDUAAAQCGoAAAgENQAABIIaAAACQQ0AAIGgBgCAQFADAEAgqAEAIBDUAAAQCGoAAAgENQAABIIaAAACQQ0AAIGgBgCAQFADAEAgqAEAIBDUAAAQCGoAAAgENQAABIIaAAACQQ0AAIGgBgCAQFADAEAgqAEAIBDUAAAQCGoAAAgENQAABIIaAAACQQ0AAIGgBgCAQFADAEAgqAEAIBDUAAAQCGoAAAgENQAABIIaAAACQQ0AAIGgBgCAQFADAEAgqAEAIBDUAAAQCGoAAAgENQAABIIaAAACQQ0AAIGgBgCAQFADAEDwLzt4JGSwax6ZAAAAAElFTkSuQmCC"></video>
+<script type="text/javascript">
+</script>
+</body>
+</html>
diff --git a/dom/media/test/reftest/frame_order_mp4.html b/dom/media/test/reftest/frame_order_mp4.html
new file mode 100644
index 0000000000..e77cf05e9e
--- /dev/null
+++ b/dom/media/test/reftest/frame_order_mp4.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+</head>
+<body>
+<video id="v" style="position:absolute; left:0; top:0"></video>
+<canvas id="canvas" style="position:absolute; left:0; top:0"></video>
+<script type="text/javascript">
+/**
+ * Do seek multiple times and check the video frame on 0.3s.
+ */
+async function testFrameOrderAfterSeeking() {
+ const video = document.getElementById("v");
+ video.src = "frame_order.mp4";
+ await new Promise(r => video.oncanplay = r);
+ // The issue won't happen on the first seek, because the decoder hasn't been
+ // created yet.
+ video.currentTime = 0.1;
+ await new Promise(r => video.onseeked = r);
+ video.currentTime = 0.3;
+ await new Promise(r => video.onseeked = r);
+ // Since our media pipeline sends the frame to imageBridge, then fires
+ // a seeked event, the target frame may not be shown on the screen.
+ // So using canvas to access the target frame in the imageContainer in
+ // videoElement.
+ const canvas = document.getElementById("canvas");
+ canvas.width = video.videoWidth;
+ canvas.height = video.videoHeight;
+ const ctx = canvas.getContext("2d");
+ ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
+ document.documentElement.removeAttribute('class');
+};
+
+window.addEventListener("MozReftestInvalidate", testFrameOrderAfterSeeking);
+</script>
+</body>
+</html>
diff --git a/dom/media/test/reftest/gen_combos.py b/dom/media/test/reftest/gen_combos.py
new file mode 100644
index 0000000000..f7e7d50fe1
--- /dev/null
+++ b/dom/media/test/reftest/gen_combos.py
@@ -0,0 +1,257 @@
+#!/usr/bin/env python3
+
+# E.g. `./gen_combos.py [--write] color_quads/720p.png`
+
+import concurrent.futures
+import pathlib
+import subprocess
+import sys
+
+ARGS = sys.argv
+SRC_PATH = pathlib.Path(ARGS.pop())
+assert SRC_PATH.exists(), "gen_combos.py [--flags] <src file path>"
+DIR = SRC_PATH.parent
+
+
+# crossCombine([{a:false},{a:5}], [{},{b:5}])
+# [{a:false}, {a:true}, {a:false,b:5}, {a:true,b:5}]
+def cross_combine(*args):
+ args = list(args)
+
+ def cross_combine2(listA, listB):
+ listC = []
+ for a in listA:
+ for b in listB:
+ c = dict()
+ c.update(a)
+ c.update(b)
+ listC.append(c)
+ return listC
+
+ res = [dict()]
+ while True:
+ try:
+ next = args.pop(0)
+ except IndexError:
+ break
+ res = cross_combine2(res, next)
+ return res
+
+
+def keyed_combiner(key, vals):
+ res = []
+ for v in vals:
+ d = dict()
+ d[key] = v
+ res.append(d)
+ return res
+
+
+# -
+
+
+def eprint(*args, **kwargs):
+ print(*args, file=sys.stderr, **kwargs)
+
+
+# -
+
+OGG = []
+WEBM_CODECS = ["av1", "vp9"]
+
+if "--all" in ARGS:
+ OGG = cross_combine(
+ [{"ext": "ogg"}], keyed_combiner("vcodec", ["theora", "vp8", "vp9"])
+ )
+ WEBM_CODECS += ["vp8"]
+
+MP4 = cross_combine([{"ext": "mp4"}], keyed_combiner("vcodec", ["av1", "h264", "vp9"]))
+
+WEBM = cross_combine([{"ext": "webm"}], keyed_combiner("vcodec", WEBM_CODECS))
+
+# -
+
+FORMAT_LIST = set(
+ [
+ "yuv420p",
+ "yuv420p10",
+ # 'yuv420p12',
+ # 'yuv420p16be',
+ # 'yuv420p16le',
+ "gbrp",
+ ]
+)
+
+if "--all" in ARGS:
+ FORMAT_LIST |= set(
+ [
+ "yuv420p",
+ "yuv420p10",
+ "yuv420p12",
+ "yuv420p16be",
+ "yuv420p16le",
+ "yuv422p",
+ "yuv422p10",
+ "yuv422p12",
+ "yuv422p16be",
+ "yuv422p16le",
+ "yuv444p",
+ "yuv444p10",
+ "yuv444p12",
+ "yuv444p16be",
+ "yuv444p16le",
+ "yuv411p",
+ "yuv410p",
+ "yuyv422",
+ "uyvy422",
+ "rgb24",
+ "bgr24",
+ "rgb8",
+ "bgr8",
+ "rgb444be",
+ "rgb444le",
+ "bgr444be",
+ "bgr444le",
+ # 'nv12', # Encoding not different than yuv420p?
+ # 'nv21', # Encoding not different than yuv420p?
+ "gbrp",
+ "gbrp9be",
+ "gbrp9le",
+ "gbrp10be",
+ "gbrp10le",
+ "gbrp12be",
+ "gbrp12le",
+ "gbrp14be",
+ "gbrp14le",
+ "gbrp16be",
+ "gbrp16le",
+ ]
+ )
+
+FORMATS = keyed_combiner("format", list(FORMAT_LIST))
+
+RANGE = keyed_combiner("range", ["tv", "pc"])
+
+CSPACE_LIST = set(
+ [
+ "bt709",
+ # 'bt2020',
+ ]
+)
+
+if "--all" in ARGS:
+ CSPACE_LIST |= set(
+ [
+ "bt709",
+ "bt2020",
+ "bt601-6-525", # aka smpte170m NTSC
+ "bt601-6-625", # aka bt470bg PAL
+ ]
+ )
+CSPACE_LIST = list(CSPACE_LIST)
+
+# -
+
+COMBOS = cross_combine(
+ WEBM + MP4 + OGG,
+ FORMATS,
+ RANGE,
+ keyed_combiner("src_cspace", CSPACE_LIST),
+ keyed_combiner("dst_cspace", CSPACE_LIST),
+)
+
+# -
+
+print(f"{len(COMBOS)} combinations...")
+
+todo = []
+for c in COMBOS:
+ dst_name = ".".join(
+ [
+ SRC_PATH.name,
+ c["src_cspace"],
+ c["dst_cspace"],
+ c["range"],
+ c["format"],
+ c["vcodec"],
+ c["ext"],
+ ]
+ )
+
+ src_cspace = c["src_cspace"]
+
+ vf = f"scale=out_range={c['range']}"
+ vf += f",colorspace=all={c['dst_cspace']}"
+ vf += f":iall={src_cspace}"
+ args = [
+ "ffmpeg",
+ "-y",
+ # For input:
+ "-color_primaries",
+ src_cspace,
+ "-color_trc",
+ src_cspace,
+ "-colorspace",
+ src_cspace,
+ "-i",
+ SRC_PATH.as_posix(),
+ # For output:
+ "-bitexact", # E.g. don't use true random uuids
+ "-vf",
+ vf,
+ "-pix_fmt",
+ c["format"],
+ "-vcodec",
+ c["vcodec"],
+ "-crf",
+ "1", # Not-quite-lossless
+ (DIR / dst_name).as_posix(),
+ ]
+ if "-v" in ARGS or "-vv" in ARGS:
+ print("$ " + " ".join(args))
+ else:
+ print(" " + args[-1])
+
+ todo.append(args)
+
+# -
+
+with open(DIR / "reftest.list", "r") as f:
+ reftest_list_text = f.read()
+
+for args in todo:
+ vid_name = pathlib.Path(args[-1]).name
+ if vid_name not in reftest_list_text:
+ print(f"WARNING: Not in reftest.list: {vid_name}")
+
+# -
+
+if "--write" not in ARGS:
+ print("Use --write to write. Exiting...")
+ exit(0)
+
+# -
+
+
+def run_cmd(args):
+ dest = None
+ if "-vv" not in ARGS:
+ dest = subprocess.DEVNULL
+ try:
+ subprocess.run(args, stderr=dest)
+ except FileNotFoundError:
+ print("FileNotFoundError, is ffmpeg not in your PATH?")
+ raise
+
+
+with concurrent.futures.ThreadPoolExecutor() as pool:
+ fs = []
+ for cur_args in todo:
+ f = pool.submit(run_cmd, cur_args)
+ fs.append(f)
+
+ done = 0
+ for f in concurrent.futures.as_completed(fs):
+ f.result() # Raise if it raised
+ done += 1
+ sys.stdout.write(f"\rEncoded {done}/{len(todo)}")
diff --git a/dom/media/test/reftest/generateREF.html b/dom/media/test/reftest/generateREF.html
new file mode 100644
index 0000000000..4e26066973
--- /dev/null
+++ b/dom/media/test/reftest/generateREF.html
@@ -0,0 +1,104 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<script type="application/javascript">
+</script>
+</head>
+<body>
+<p id="out"></p>
+<video id="v1" style="position:absolute; left:0; top:0"></video>
+<canvas id="canvas"></canvas>
+<script type="application/javascript">
+// READ ME FIRST.
+// The script is trying to make a reftest sample for reftest.
+
+// STEP1. Uncomment the method below that you want to use. If you want to dump
+// Nth frame, modify the parameter to the number of frame you want to dump.
+//window.onload = function() { setTimeout(dumpFirstFrame, 0); };
+//window.onload = function() { setTimeout(dumpLastFrame, 0); };
+window.onload = function() { setTimeout(function(){dumpNthFrame(15);}, 0); };
+
+// STEP2. Set the source of video that you want to capture
+const videoSrc = '';
+
+// STEP3. Ensure the pref `media.seekToNextFrame.enabled` is on
+// STEP4. In a terminal, navigate to the containing folder and start a server with "python -m SimpleHTTPServer 8000"
+// STEP5. Open "http://localhost:8000/generateREF.html" in the browser
+// STEP6. Copy the base64 image url to your xxx-ref.html
+
+function drawVideoToInnerHTML(v) {
+ // This allows to dump content via canvas when the source is cross-origin.
+ v.crossorigin = "anonymous";
+ var canvas = document.getElementById("canvas");
+ canvas.width = v.videoWidth;
+ canvas.height = v.videoHeight;
+ var ctx = canvas.getContext("2d");
+ ctx.drawImage(v, 0, 0, v.videoWidth, v.videoHeight);
+ var dataURL = canvas.toDataURL();
+ document.getElementById("out").innerHTML=dataURL;
+}
+
+function dumpFirstFrame() {
+ var video = document.getElementById("v1");
+ video.src = videoSrc;
+ video.preload = "metadata";
+
+ video.addEventListener("loadeddata", function() {
+ drawVideoToInnerHTML(video);
+ });
+}
+
+function dumpNthFrame(n) {
+ var video = document.getElementById("v1");
+ video.src = videoSrc;
+ video.preload = "metadata";
+ const totalFrames = n;
+
+ function checkNthFrame() {
+ console.log((totalFrames-n+1)+"th Frame time is " + video.currentTime);
+ n--;
+ if (n == 0) {
+ drawVideoToInnerHTML(video);
+ } else {
+ video.seekToNextFrame();
+ }
+ }
+ video.addEventListener("loadeddata", checkNthFrame);
+ video.addEventListener("seeked", checkNthFrame);
+}
+
+function dumpLastFrame() {
+ var video = document.getElementById("v1");
+ video.src = videoSrc;
+ video.preload = "metadata";
+ video.seenEnded = false;
+
+ // Seek to the end
+ video.addEventListener("loadeddata", function() {
+ video.currentTime = video.duration;
+ video.onseeked = () => {
+ video.onseeked = null;
+ callSeekToNextFrame();
+ };
+ });
+
+ function callSeekToNextFrame() {
+ video.seekToNextFrame().then(
+ () => {
+ if (!video.seenEnded)
+ callSeekToNextFrame();
+ },
+ () => {
+ // Reach the end, do nothing.
+ }
+ );
+ }
+
+ video.addEventListener("ended", function() {
+ video.seenEnded = true;
+ drawVideoToInnerHTML(video);
+ });
+}
+</script>
+</body>
+</html>
diff --git a/dom/media/test/reftest/gizmo.mp4.55thframe-ref.html b/dom/media/test/reftest/gizmo.mp4.55thframe-ref.html
new file mode 100644
index 0000000000..28a93cc268
--- /dev/null
+++ b/dom/media/test/reftest/gizmo.mp4.55thframe-ref.html
@@ -0,0 +1,7 @@
+<!DOCTYPE HTML>
+<img style="position:absolute; left:0; top:0"
+src="
+
+data:image/png;base64,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
+"
+>
diff --git a/dom/media/test/reftest/gizmo.mp4.seek.html b/dom/media/test/reftest/gizmo.mp4.seek.html
new file mode 100644
index 0000000000..e4c1fe9515
--- /dev/null
+++ b/dom/media/test/reftest/gizmo.mp4.seek.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<!--This testing should match the 55th frame of gizmo.mp4. The
+55th frame's time is 1.8s, so seek to a time which is a little
+greater than 1.8s, the display frame should be the 55th frame.
+-->
+<head>
+<script type="text/javascript">
+function doTest() {
+ var video = document.getElementById("v1");
+ video.src = "../gizmo.mp4";
+ video.preload = "metadata";
+
+ video.currentTime = 1.801;
+
+ video.addEventListener("seeked", function() {
+ // Since the our media pipeline send the frame to imageBridge, then fire
+ // seeked event, the target frame may not be shown on the screen.
+ // So using canvas to access the target frame in the imageContainer in
+ // videoElement.
+ var canvas = document.getElementById("canvas");
+ canvas.width = video.videoWidth;
+ canvas.height = video.videoHeight;
+ var ctx = canvas.getContext("2d");
+ ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
+ document.documentElement.removeAttribute('class');
+ });
+}
+window.addEventListener("MozReftestInvalidate", doTest);
+</script>
+</head>
+<body>
+<video id="v1" style="position:absolute; left:0; top:0"></video>
+<canvas id="canvas" style="position:absolute; left:0; top:0"></video>
+</body>
+</html>
diff --git a/dom/media/test/reftest/image-10bits-rendering-720-90-ref.html b/dom/media/test/reftest/image-10bits-rendering-720-90-ref.html
new file mode 100644
index 0000000000..5e9a8e9b4c
--- /dev/null
+++ b/dom/media/test/reftest/image-10bits-rendering-720-90-ref.html
@@ -0,0 +1,4 @@
+<!DOCTYPE HTML>
+<img style="position:absolute; left:0; top:0; filter:hue-rotate(90deg);"
+ src="vp9hdr2020.png"
+>
diff --git a/dom/media/test/reftest/image-10bits-rendering-720-90-video.html b/dom/media/test/reftest/image-10bits-rendering-720-90-video.html
new file mode 100644
index 0000000000..a9ba8a9f2e
--- /dev/null
+++ b/dom/media/test/reftest/image-10bits-rendering-720-90-video.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+<script type="text/javascript">
+function doTest() {
+ var video = document.getElementById("v1");
+ video.src = "vp9hdr2020.webm";
+ video.preload = "metadata";
+ video.addEventListener("loadeddata", function() {
+ document.documentElement.removeAttribute('class');
+ });
+}
+window.addEventListener("MozReftestInvalidate", doTest);
+</script>
+</head>
+<body>
+<video id="v1" style="position:absolute; left:0; top:0; filter:hue-rotate(90deg);"></video>
+<script>
+//doTest();
+</script>
+</body>
+</html>
diff --git a/dom/media/test/reftest/image-10bits-rendering-720-ref.html b/dom/media/test/reftest/image-10bits-rendering-720-ref.html
new file mode 100644
index 0000000000..1ae393031a
--- /dev/null
+++ b/dom/media/test/reftest/image-10bits-rendering-720-ref.html
@@ -0,0 +1,4 @@
+<!DOCTYPE HTML>
+<img style="position:absolute; left:0; top:0"
+ src="vp9hdr2020.png"
+>
diff --git a/dom/media/test/reftest/image-10bits-rendering-720-video.html b/dom/media/test/reftest/image-10bits-rendering-720-video.html
new file mode 100644
index 0000000000..93d2651ffc
--- /dev/null
+++ b/dom/media/test/reftest/image-10bits-rendering-720-video.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+<script type="text/javascript">
+function doTest() {
+ var video = document.getElementById("v1");
+ video.src = "vp9hdr2020.webm";
+ video.preload = "metadata";
+ video.addEventListener("loadeddata", function() {
+ document.documentElement.removeAttribute('class');
+ });
+}
+window.addEventListener("MozReftestInvalidate", doTest);
+</script>
+</head>
+<body>
+<video id="v1" style="position:absolute; left:0; top:0"></video>
+</body>
+</html>
diff --git a/dom/media/test/reftest/image-10bits-rendering-720.video.html b/dom/media/test/reftest/image-10bits-rendering-720.video.html
new file mode 100644
index 0000000000..93d2651ffc
--- /dev/null
+++ b/dom/media/test/reftest/image-10bits-rendering-720.video.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+<script type="text/javascript">
+function doTest() {
+ var video = document.getElementById("v1");
+ video.src = "vp9hdr2020.webm";
+ video.preload = "metadata";
+ video.addEventListener("loadeddata", function() {
+ document.documentElement.removeAttribute('class');
+ });
+}
+window.addEventListener("MozReftestInvalidate", doTest);
+</script>
+</head>
+<body>
+<video id="v1" style="position:absolute; left:0; top:0"></video>
+</body>
+</html>
diff --git a/dom/media/test/reftest/image-10bits-rendering-90-ref.html b/dom/media/test/reftest/image-10bits-rendering-90-ref.html
new file mode 100644
index 0000000000..38f032f0fd
--- /dev/null
+++ b/dom/media/test/reftest/image-10bits-rendering-90-ref.html
@@ -0,0 +1,4 @@
+<!DOCTYPE HTML>
+<img style="position:absolute; left:0; top:0; filter:hue-rotate(90deg);"
+ src="av1hdr2020.png"
+>
diff --git a/dom/media/test/reftest/image-10bits-rendering-90-video.html b/dom/media/test/reftest/image-10bits-rendering-90-video.html
new file mode 100644
index 0000000000..94edcaefd8
--- /dev/null
+++ b/dom/media/test/reftest/image-10bits-rendering-90-video.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+<script type="text/javascript">
+function doTest() {
+ var video = document.getElementById("v1");
+ video.src = "av1hdr2020.mp4";
+ video.preload = "metadata";
+ video.addEventListener("loadeddata", function() {
+ document.documentElement.removeAttribute('class');
+ });
+}
+window.addEventListener("MozReftestInvalidate", doTest);
+</script>
+</head>
+<body>
+<video id="v1" style="position:absolute; left:0; top:0; filter:hue-rotate(90deg);"></video>
+<script>
+//doTest();
+</script>
+</body>
+</html>
diff --git a/dom/media/test/reftest/image-10bits-rendering-ref.html b/dom/media/test/reftest/image-10bits-rendering-ref.html
new file mode 100644
index 0000000000..de7fbbfc95
--- /dev/null
+++ b/dom/media/test/reftest/image-10bits-rendering-ref.html
@@ -0,0 +1,4 @@
+<!DOCTYPE HTML>
+<img style="position:absolute; left:0; top:0"
+ src="av1hdr2020.png"
+>
diff --git a/dom/media/test/reftest/image-10bits-rendering-video.html b/dom/media/test/reftest/image-10bits-rendering-video.html
new file mode 100644
index 0000000000..faaf9c29ef
--- /dev/null
+++ b/dom/media/test/reftest/image-10bits-rendering-video.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+<script type="text/javascript">
+function doTest() {
+ var video = document.getElementById("v1");
+ video.src = "av1hdr2020.mp4";
+ video.preload = "metadata";
+ video.addEventListener("loadeddata", function() {
+ document.documentElement.removeAttribute('class');
+ });
+}
+//window.addEventListener("MozReftestInvalidate", doTest);
+</script>
+</head>
+<body>
+<video id="v1" style="position:absolute; left:0; top:0"></video>
+<script>
+doTest();
+</script>
+</body>
+</html>
diff --git a/dom/media/test/reftest/incorrect_display_in_bytestream_vp8-ref.html b/dom/media/test/reftest/incorrect_display_in_bytestream_vp8-ref.html
new file mode 100644
index 0000000000..b08a8d365b
--- /dev/null
+++ b/dom/media/test/reftest/incorrect_display_in_bytestream_vp8-ref.html
@@ -0,0 +1,13 @@
+<!DOCTYPE HTML>
+<head>
+</head>
+<body>
+<!--
+ This ref image is generated by the first frame of the testing video in webm_aspect_ratio_vp8.html
+-->
+<image style="position:absolute; left:0; top:0" src="data:image/png;base64,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
+"></video>
+<script type="text/javascript">
+</script>
+</body>
+</html>
diff --git a/dom/media/test/reftest/incorrect_display_in_bytestream_vp8.html b/dom/media/test/reftest/incorrect_display_in_bytestream_vp8.html
new file mode 100644
index 0000000000..271343cfe2
--- /dev/null
+++ b/dom/media/test/reftest/incorrect_display_in_bytestream_vp8.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+</head>
+<body>
+<video id="v" style="position:absolute; left:0; top:0"></video>
+<canvas id="canvas" style="position:absolute; left:0; top:0"></video>
+<script type="text/javascript">
+/**
+ * The display information in the VP8 byte stream is different from the display
+ * information in the container, and it's the incorrect one, which doesn't honer
+ * the original DAR. This test is used to check whether we will display the
+ * video frame in correct DAR.
+ */
+async function testDisplayRatioForVP8() {
+ const video = document.getElementById("v");
+ video.src = "incorrect_display_in_bytestream_vp8.webm";
+ await new Promise(r => video.oncanplay = r);
+ // Since our media pipeline sends the frame to imageBridge, the target frame
+ // may not be shown on the screen yet. So using canvas to access the target
+ // frame in the imageContainer in videoElement.
+ const canvas = document.getElementById("canvas");
+ canvas.width = video.videoWidth;
+ canvas.height = video.videoHeight;
+ const ctx = canvas.getContext("2d");
+ ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
+ document.documentElement.removeAttribute('class');
+};
+
+window.addEventListener("MozReftestInvalidate", testDisplayRatioForVP8);
+</script>
+</body>
+</html>
diff --git a/dom/media/test/reftest/incorrect_display_in_bytestream_vp8.webm b/dom/media/test/reftest/incorrect_display_in_bytestream_vp8.webm
new file mode 100644
index 0000000000..e4143b7f38
--- /dev/null
+++ b/dom/media/test/reftest/incorrect_display_in_bytestream_vp8.webm
Binary files differ
diff --git a/dom/media/test/reftest/incorrect_display_in_bytestream_vp9-ref.html b/dom/media/test/reftest/incorrect_display_in_bytestream_vp9-ref.html
new file mode 100644
index 0000000000..6517c77f63
--- /dev/null
+++ b/dom/media/test/reftest/incorrect_display_in_bytestream_vp9-ref.html
@@ -0,0 +1,12 @@
+<!DOCTYPE HTML>
+<head>
+</head>
+<body>
+<!--
+ This ref image is generated by the first frame of the testing video in webm_aspect_ratio_vp8.html
+-->
+<image style="position:absolute; left:0; top:0" src="data:image/png;base64,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"></video>
+<script type="text/javascript">
+</script>
+</body>
+</html>
diff --git a/dom/media/test/reftest/incorrect_display_in_bytestream_vp9.html b/dom/media/test/reftest/incorrect_display_in_bytestream_vp9.html
new file mode 100644
index 0000000000..7d0e9404d5
--- /dev/null
+++ b/dom/media/test/reftest/incorrect_display_in_bytestream_vp9.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+</head>
+<body>
+<video id="v" style="position:absolute; left:0; top:0"></video>
+<canvas id="canvas" style="position:absolute; left:0; top:0"></video>
+<script type="text/javascript">
+/**
+ * The display information in the VP9 byte stream is different from the display
+ * information in the container, and it's the incorrect one, which doesn't honer
+ * the original DAR. This test is used to check whether we will display the
+ * video frame in correct DAR.
+ */
+async function testDisplayRatioForVP9() {
+ const video = document.getElementById("v");
+ video.src = "incorrect_display_in_bytestream_vp9.webm";
+ await new Promise(r => video.oncanplay = r);
+ // Since our media pipeline sends the frame to imageBridge, the target frame
+ // may not be shown on the screen yet. So using canvas to access the target
+ // frame in the imageContainer in videoElement.
+ const canvas = document.getElementById("canvas");
+ canvas.width = video.videoWidth;
+ canvas.height = video.videoHeight;
+ const ctx = canvas.getContext("2d");
+ ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
+ document.documentElement.removeAttribute('class');
+};
+
+window.addEventListener("MozReftestInvalidate", testDisplayRatioForVP9);
+</script>
+</body>
+</html>
diff --git a/dom/media/test/reftest/incorrect_display_in_bytestream_vp9.webm b/dom/media/test/reftest/incorrect_display_in_bytestream_vp9.webm
new file mode 100644
index 0000000000..499e3acd94
--- /dev/null
+++ b/dom/media/test/reftest/incorrect_display_in_bytestream_vp9.webm
Binary files differ
diff --git a/dom/media/test/reftest/reftest.list b/dom/media/test/reftest/reftest.list
new file mode 100644
index 0000000000..2a2f89742c
--- /dev/null
+++ b/dom/media/test/reftest/reftest.list
@@ -0,0 +1,15 @@
+skip-if(Android) fuzzy-if(OSX,0-80,0-76800) fuzzy-if(appleSilicon,0-80,0-76800) fuzzy-if(winWidget,0-62,0-76799) fuzzy-if(swgl&&winWidget,0-63,0-1969) fuzzy-if(gtkWidget&&layersGPUAccelerated,0-70,0-2032) HTTP(..) == short.mp4.firstframe.html short.mp4.firstframe-ref.html
+skip-if(Android) fuzzy-if(OSX,0-87,0-76797) fuzzy-if(appleSilicon,0-87,0-76797) fuzzy-if(winWidget,0-60,0-76797) fuzzy-if(gtkWidget&&layersGPUAccelerated,0-60,0-6070) HTTP(..) == short.mp4.lastframe.html short.mp4.lastframe-ref.html
+skip-if(Android) skip-if(cocoaWidget) skip-if(winWidget) fuzzy-if(gtkWidget&&layersGPUAccelerated,0-57,0-4282) fuzzy-if(OSX,55-80,4173-4417) HTTP(..) == bipbop_300_215kbps.mp4.lastframe.html bipbop_300_215kbps.mp4.lastframe-ref.html
+skip-if(Android) fuzzy-if(OSX,0-25,0-175921) fuzzy-if(appleSilicon,34-34,40100-40100) fuzzy-if(winWidget,0-71,0-179198) fuzzy-if(/^Windows\x20NT\x2010\.0/.test(http.oscpu)&&/^aarch64-msvc/.test(xulRuntime.XPCOMABI),0-255,0-179500) HTTP(..) == gizmo.mp4.seek.html gizmo.mp4.55thframe-ref.html
+# Bug 1758718
+skip-if(Android) skip-if(MinGW) skip-if(/^Windows\x20NT\x2010\.0/.test(http.oscpu)&&/^aarch64-msvc/.test(xulRuntime.XPCOMABI)) skip-if(OSX) fuzzy(0-10,0-778236) == image-10bits-rendering-video.html image-10bits-rendering-ref.html
+skip-if(Android) skip-if(MinGW) skip-if(/^Windows\x20NT\x2010\.0/.test(http.oscpu)&&/^aarch64-msvc/.test(xulRuntime.XPCOMABI)) fuzzy(0-10,0-778536) fuzzy-if(appleSilicon,0-37,0-699614) == image-10bits-rendering-90-video.html image-10bits-rendering-90-ref.html
+# Bug 1758718
+skip-if(Android) fuzzy(0-27,0-573106) skip-if(OSX) == image-10bits-rendering-720-video.html image-10bits-rendering-720-ref.html
+skip-if(Android) fuzzy(0-31,0-573249) fuzzy-if(appleSilicon,0-37,0-543189) == image-10bits-rendering-720-90-video.html image-10bits-rendering-720-90-ref.html
+skip-if(Android) fuzzy(0-84,0-771156) fails-if(useDrawSnapshot) skip-if(/^Windows\x20NT\x206\.1/.test(http.oscpu)) == uneven_frame_duration_video.html uneven_frame_duration_video-ref.html # Skip on Windows 7 as the resolution of the video is too high for test machines and will fail in the decoder.
+# Set media.dormant-on-pause-timeout-ms to avoid decoders becoming dormant and busting test, skip on android as test is too noisy and unstable
+skip-if(Android) pref(media.dormant-on-pause-timeout-ms,-1) fuzzy(0-20,0-500) == frame_order_mp4.html frame_order_mp4-ref.html
+skip-if(Android) fuzzy(0-30,0-270000) == incorrect_display_in_bytestream_vp8.html incorrect_display_in_bytestream_vp8-ref.html
+skip-if(Android) fuzzy(0-22,0-377335) == incorrect_display_in_bytestream_vp9.html incorrect_display_in_bytestream_vp9-ref.html
diff --git a/dom/media/test/reftest/reftest_img.html b/dom/media/test/reftest/reftest_img.html
new file mode 100644
index 0000000000..3e55c264ec
--- /dev/null
+++ b/dom/media/test/reftest/reftest_img.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+<meta charset='utf-8'>
+</head>
+<body>
+<img id="e_img" style="position:absolute; left:0; top:0; max-width:100%">
+<script>
+(async () => {
+ const params = new URLSearchParams(window.location.search);
+ const src = params.get('src');
+ src.defined;
+
+ e_img.src = src;
+ await e_img.decode()
+ document.documentElement.removeAttribute('class');
+})();
+</script>
+</body>
+</html>
diff --git a/dom/media/test/reftest/reftest_video.html b/dom/media/test/reftest/reftest_video.html
new file mode 100644
index 0000000000..cf9223e7ae
--- /dev/null
+++ b/dom/media/test/reftest/reftest_video.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+<meta charset='utf-8'>
+</head>
+<body>
+<video id="e_video" style="position:absolute; left:0; top:0; max-width:100%">
+<script>
+
+const TIMEOUT_MS = 2000;
+
+// -
+
+function sleepPromise(ms) {
+ return new Promise(go => {
+ setTimeout(go, ms);
+ });
+}
+
+(async () => {
+ await sleepPromise(TIMEOUT_MS);
+ if (!document.documentElement.hasAttribute('class')) return;
+
+ const div = document.body.appendChild(document.createElement('div'));
+ div.textContent = `Timed out after ${TIMEOUT_MS}ms`;
+ console.log(div.textContent);
+
+ document.documentElement.removeAttribute('class');
+})();
+
+// -
+// Test
+
+(async () => {
+ const params = new URLSearchParams(window.location.search);
+ const src = params.get('src');
+ src.defined;
+ if (src == 'none') {
+ console.log('Show blank.');
+ document.documentElement.removeAttribute('class');
+ return;
+ }
+ if (src == 'timeout') {
+ console.log('Deliberate timeout.');
+ return;
+ }
+
+ e_video.src = src;
+ e_video.muted = true;
+ const p = e_video.play();
+ p.defined;
+ try {
+ await p;
+ console.log('e_video.play() accepted');
+ } catch (e) {
+ const div = document.body.appendChild(document.createElement('div'));
+ div.textContent = `Error: ${JSON.stringify(e)}`;
+ console.log(div.textContent);
+ }
+ document.documentElement.removeAttribute('class');
+})();
+</script>
+</body>
+</html>
diff --git a/dom/media/test/reftest/short.mp4.firstframe-ref.html b/dom/media/test/reftest/short.mp4.firstframe-ref.html
new file mode 100644
index 0000000000..d80f2f985f
--- /dev/null
+++ b/dom/media/test/reftest/short.mp4.firstframe-ref.html
@@ -0,0 +1,4 @@
+<!DOCTYPE HTML>
+<img style="position:absolute; left:0; top:0"
+ src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAUAAAADwCAYAAABxLb1rAAAHv0lEQVR4nO3dQWyfdR3H8R8d7XBjMDYcYzqCQY0SkWhQAomJJ2/GgyeNiTExHk3URE9e1INGEz3NA5CYETwQFi8YEvAgGg6LogFDIojbEBTGpsyNrWNt9/HQso0Btus2vu2+r1/yypN/+/x/+fbyzpP/0z4dGSMAHY3qAQCqCCDQlgACbQkg0JYAAm0JINCWAAJtCSDQlgACbQkg0JYAAm0JINCWAAJtCSDQlgACbQkg0JYAAm0JINCWAAJtCSDQlgACbQkg0JYAAm0JINCWAAJtCSDQlgACbQkg0JYAAm0JINCWAAJtCSDQlgACbQkg0JYAAm0JINCWAAJtCSDQlgACbQkg0JYAAm0JINCWAAJtCSDQlgACbQkg0JYAAm0JINCWAAJtCSDQlgACbQkg0JYAAm0JINCWAAJtCSDQlgACbQkg0JYAAm0JINCWAAJtCSDQlgACbQkg0JYAAm0JINCWAAJtCSDQlgACbQkg0JYAAm0JINCWAAJtCSDQlgACbQkg0JYAAm0JINCWAAJtCSCl/jNGsmFi/vUVk5mZHDk2Rl4ZI5kaOTo25MC4/NT5h8b894+PTTk+NpXPz+omgJSaGyOZGDm48PrfY2R6YmRmzMvk9cm67afOPzpGpsfIsbExx8bG8vlZ3QSQFeFPYyQ3fyzJwSRHkiQ5+lqS55P9u5P163P0De+ZWlA/O6uXALICrEk+cEvy9W8nOZTk1Zxe/5yP4JVX5vgb3jOVjCtWwOysZgJIqSNjazJ5c+75zleTnMib12tJppOJLTnziu/IGHl1BczP6iaAlNoz1iSbbl+48nvlLQJ4/IwATp56nwByIQggpXbc+enk4d+edcV3IslskrnTX558YwDhQhBAaj3yaPLyoTMCOLsQwDkB5KITQEo8dP2G5N4dycwZkft/a921mRlryufm0iKA1Lh3R7L/2aXFTwC5SASQd9TOmzYnD9y99PC9vq7aeNbvAcL5E0DeWQ/cnTz3hACyIgggF8XJhePcmMihMfLzz9yW/PHhcw+fAHIRCSAX3sTEqeOJMZKP3568/Nfk8D4BZEURQC6eDRvy+599N8nR5YfvVACvyrHqn4dLjgBycWzfnnz/B8nJg0mOCSArkgByQcyNtTk6Ru6/447krrvy+hNdTi5atpnMPwFmkbXh7IchwPkTQM7fmg2ZHVPJ+29LnnoqOXDgHC7tZhYIIO88AWRZXr/L++jnPpvHv/TF5F9/yann+C2y5k4eTQ6eTHb/Lfd97fPJkX1LCOC6+RsqcAEJIMv30VuTx36XHNif0w8xWOJ6+kD+8OVv5Cs3XZ0c3iOAlBBAluWRa65Knt+7pNYdS5LZhU8D9+1JfrUrGZuSqffmnmu3Jc/sE0BKCCDLci4BPHPNfuub+cV1W5KxKRkb5wP47D8EkBICyLLs2rIteekcbnbs+HVy5xfetM9Da7cmTzwngJQQQJZlyQGcnsnuH/4key+/KU+PG07vMTF/fGjt1uRJV4DUEECW5b5t25KDbxHAuePzHns8+d6PkhuvSybffp+dm69N/u4mCDUEkGV52wBOH85/d96TJ2+9PQ9es3XRfQSQSgLIsjy45erkxb2Z/yuOY8lju5Mf/zT54KeSsXn+H54vwc7Nm5I9S3gwqgByEQggy3IqgIefyZO7duTPn7wzj37oI5kdIxlXJxNrl7SPAFJJAFmWE2Mkt3w42bg+WbP8fQSQSgLIssyMkVxx+XnvI4BUEkBKLTmA69ct+XNFWCoBpNQvr3t3sm/v4gGcmhJALjgBpNT929+TvPjC4gFcAbNy6RFASu163w3J/hcFkBICSCkBpJIAUkoAqSSAlBJAKgkgpQSQSgJIKQGkkgBSSgCpJICUEkAqCSClBJBKAkgpAaSSAFJKAKkkgJQSQCoJIKUEkEoCSCkBpJIAUkoAqSSAlBJAKgkgpQSQSgJIKQGkkgBSSgCpJICUEkAqCSClBJBKAkgpAaSSAFJKAKkkgJQSQCoJIKUEkEoCSCkBpJIAUkoAqSSAlBJAKgkgpQSQSgJIKQGkkgBSSgCpJICUEkAqCSClBJBKAkgpAaSSAFJKAKkkgJQSQCoJIKUEkEoCSKkHbtyevPySAFJCACklgFQSQEr9ZvMnkj2L92924rLMXFY/L5cWAaTUI5tuS/YtUr+T0zk4RrJ2snxeLi0CyIpy8qzX02MqM2NdXn3TuVML6mdm9RJAVrQT412ZfsvQrV1QPyOrlwCyqswuqJ6DS4MAsqoIIBeSAAJtCSDQlgACbQkg0JYAAm0JINCWAAJtCSDQlgACbQkg0JYAAm0JINCWAAJtCSDQlgACbQkg0JYAAm0JINCWAAJtCSDQlgACbQkg0JYAAm0JINCWAAJtCSDQlgACbQkg0JYAAm0JINCWAAJtCSDQlgACbQkg0JYAAm0JINCWAAJtCSDQlgACbQkg0JYAAm0JINCWAAJtCSDQlgACbQkg0JYAAm0JINCWAAJtCSDQlgACbQkg0JYAAm0JINCWAAJtCSDQlgACbQkg0JYAAm0JINCWAAJtCSDQlgACbQkg0JYAAm0JINCWAAJtCSDQlgACbQkg0JYAAm0JINCWAAJtCSDQlgACbQkg0JYAAm0JINCWAAJtCSDQlgACbQkg0Nb/AJRYBp4E6qsBAAAAAElFTkSuQmCC"
+>
diff --git a/dom/media/test/reftest/short.mp4.firstframe.html b/dom/media/test/reftest/short.mp4.firstframe.html
new file mode 100644
index 0000000000..759bdc5ede
--- /dev/null
+++ b/dom/media/test/reftest/short.mp4.firstframe.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+<script type="text/javascript">
+function doTest() {
+ var video = document.getElementById("v1");
+ video.src = "../short.mp4";
+ video.preload = "metadata";
+ video.addEventListener("loadeddata", function() {
+ document.documentElement.removeAttribute('class');
+ });
+}
+window.addEventListener("MozReftestInvalidate", doTest);
+</script>
+</head>
+<body>
+<video id="v1" style="position:absolute; left:0; top:0"></video>
+</body>
+</html>
diff --git a/dom/media/test/reftest/short.mp4.lastframe-ref.html b/dom/media/test/reftest/short.mp4.lastframe-ref.html
new file mode 100644
index 0000000000..7474b9039e
--- /dev/null
+++ b/dom/media/test/reftest/short.mp4.lastframe-ref.html
@@ -0,0 +1,4 @@
+<!DOCTYPE HTML>
+<img style="position:absolute; left:0; top:0"
+src="data:image/png;base64,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"
+>
diff --git a/dom/media/test/reftest/short.mp4.lastframe.html b/dom/media/test/reftest/short.mp4.lastframe.html
new file mode 100644
index 0000000000..abd27c5c8e
--- /dev/null
+++ b/dom/media/test/reftest/short.mp4.lastframe.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+<script type="text/javascript">
+function doTest() {
+ var video = document.getElementById("v1");
+ video.src = "../short.mp4";
+ video.preload = "metadata";
+ video.seenEnded = false;
+ // Seek to the end
+ video.addEventListener("loadeddata", function() {
+ video.currentTime = video.duration;
+ video.onseeked = () => {
+ video.onseeked = null;
+ callSeekToNextFrame();
+ };
+ });
+
+ function callSeekToNextFrame() {
+ video.seekToNextFrame().then(
+ () => {
+ if (!video.seenEnded)
+ callSeekToNextFrame();
+ },
+ () => {
+ // Reach the end, do nothing.
+ }
+ );
+ }
+
+ video.addEventListener("ended", function() {
+ video.seenEnded = true;
+ document.documentElement.removeAttribute('class');
+ });
+}
+window.addEventListener("MozReftestInvalidate", doTest);
+</script>
+</head>
+<body>
+<video id="v1" style="position:absolute; left:0; top:0"></video>
+</body>
+</html>
diff --git a/dom/media/test/reftest/uneven_frame_duration_video-ref.html b/dom/media/test/reftest/uneven_frame_duration_video-ref.html
new file mode 100644
index 0000000000..6f1d73cc71
--- /dev/null
+++ b/dom/media/test/reftest/uneven_frame_duration_video-ref.html
@@ -0,0 +1,7 @@
+<!DOCTYPE HTML>
+<img style="position:absolute; left:0; top:0"
+src="uneven_frame_durations_3.8s_frame.png"
+>
+<!-- Test frame created via
+`ffmpeg.exe -i uneven_frame_durations.mp4 -ss 3.8 -frames 1 uneven_frame_durations_3.8s_frame.png`
+-->
diff --git a/dom/media/test/reftest/uneven_frame_duration_video.html b/dom/media/test/reftest/uneven_frame_duration_video.html
new file mode 100644
index 0000000000..73ca34b8bd
--- /dev/null
+++ b/dom/media/test/reftest/uneven_frame_duration_video.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<!--Tests the fix for bug 1576990. We want to make sure the frames do not
+ become stuck due to decoders (in particular the Windows decoder) estimating
+ frame durations incorrectly.
+-->
+<head>
+<script type="text/javascript">
+function doTest() {
+ var video = document.getElementById("v1");
+ // Video from bug 1576990.
+ video.src = "uneven_frame_durations.mp4";
+ video.preload = "metadata";
+
+ // This frame differs depending on if the Windows' decoder is estimating
+ // frame duration or if we use Gecko durations.
+ video.currentTime = 3.8;
+
+ video.addEventListener("seeked", function() {
+ // Since our media pipeline sends the frame to imageBridge, then fires
+ // a seeked event, the target frame may not be shown on the screen.
+ // So using canvas to access the target frame in the imageContainer in
+ // videoElement.
+ var canvas = document.getElementById("canvas");
+ canvas.width = video.videoWidth;
+ canvas.height = video.videoHeight;
+ var ctx = canvas.getContext("2d");
+ ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight);
+ document.documentElement.removeAttribute("class");
+ });
+}
+window.addEventListener("MozReftestInvalidate", doTest);
+</script>
+</head>
+<body>
+<video id="v1" style="position:absolute; left:0; top:0"></video>
+<canvas id="canvas" style="position:absolute; left:0; top:0"></video>
+</body>
+</html>
diff --git a/dom/media/test/reftest/uneven_frame_durations.mp4 b/dom/media/test/reftest/uneven_frame_durations.mp4
new file mode 100644
index 0000000000..a5a0c16a0f
--- /dev/null
+++ b/dom/media/test/reftest/uneven_frame_durations.mp4
Binary files differ
diff --git a/dom/media/test/reftest/uneven_frame_durations_3.8s_frame.png b/dom/media/test/reftest/uneven_frame_durations_3.8s_frame.png
new file mode 100644
index 0000000000..889174e115
--- /dev/null
+++ b/dom/media/test/reftest/uneven_frame_durations_3.8s_frame.png
Binary files differ
diff --git a/dom/media/test/reftest/vp9hdr2020.png b/dom/media/test/reftest/vp9hdr2020.png
new file mode 100644
index 0000000000..afb68d9e0a
--- /dev/null
+++ b/dom/media/test/reftest/vp9hdr2020.png
Binary files differ
diff --git a/dom/media/test/reftest/vp9hdr2020.webm b/dom/media/test/reftest/vp9hdr2020.webm
new file mode 100644
index 0000000000..516f62093a
--- /dev/null
+++ b/dom/media/test/reftest/vp9hdr2020.webm
Binary files differ
diff --git a/dom/media/test/resolution-change.webm b/dom/media/test/resolution-change.webm
new file mode 100644
index 0000000000..29aad93b96
--- /dev/null
+++ b/dom/media/test/resolution-change.webm
Binary files differ
diff --git a/dom/media/test/resolution-change.webm^headers^ b/dom/media/test/resolution-change.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/resolution-change.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/sample-encrypted-sgpdstbl-sbgptraf.mp4 b/dom/media/test/sample-encrypted-sgpdstbl-sbgptraf.mp4
new file mode 100644
index 0000000000..720339bdc2
--- /dev/null
+++ b/dom/media/test/sample-encrypted-sgpdstbl-sbgptraf.mp4
Binary files differ
diff --git a/dom/media/test/sample-encrypted-sgpdstbl-sbgptraf.mp4^headers^ b/dom/media/test/sample-encrypted-sgpdstbl-sbgptraf.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/sample-encrypted-sgpdstbl-sbgptraf.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/sample-fisbone-skeleton4.ogv b/dom/media/test/sample-fisbone-skeleton4.ogv
new file mode 100644
index 0000000000..8afe0be7a4
--- /dev/null
+++ b/dom/media/test/sample-fisbone-skeleton4.ogv
Binary files differ
diff --git a/dom/media/test/sample-fisbone-skeleton4.ogv^headers^ b/dom/media/test/sample-fisbone-skeleton4.ogv^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/sample-fisbone-skeleton4.ogv^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/sample-fisbone-wrong-header.ogv b/dom/media/test/sample-fisbone-wrong-header.ogv
new file mode 100644
index 0000000000..46c3933da5
--- /dev/null
+++ b/dom/media/test/sample-fisbone-wrong-header.ogv
Binary files differ
diff --git a/dom/media/test/sample-fisbone-wrong-header.ogv^headers^ b/dom/media/test/sample-fisbone-wrong-header.ogv^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/sample-fisbone-wrong-header.ogv^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/sample.3g2 b/dom/media/test/sample.3g2
new file mode 100644
index 0000000000..769cb01dbd
--- /dev/null
+++ b/dom/media/test/sample.3g2
Binary files differ
diff --git a/dom/media/test/sample.3gp b/dom/media/test/sample.3gp
new file mode 100644
index 0000000000..4a3d8ea66f
--- /dev/null
+++ b/dom/media/test/sample.3gp
Binary files differ
diff --git a/dom/media/test/seek-short.ogv b/dom/media/test/seek-short.ogv
new file mode 100644
index 0000000000..a5ca6951d0
--- /dev/null
+++ b/dom/media/test/seek-short.ogv
Binary files differ
diff --git a/dom/media/test/seek-short.ogv^headers^ b/dom/media/test/seek-short.ogv^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/seek-short.ogv^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/seek-short.webm b/dom/media/test/seek-short.webm
new file mode 100644
index 0000000000..36abd1570e
--- /dev/null
+++ b/dom/media/test/seek-short.webm
Binary files differ
diff --git a/dom/media/test/seek-short.webm^headers^ b/dom/media/test/seek-short.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/seek-short.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/seek.ogv b/dom/media/test/seek.ogv
new file mode 100644
index 0000000000..ac7ece3519
--- /dev/null
+++ b/dom/media/test/seek.ogv
Binary files differ
diff --git a/dom/media/test/seek.ogv^headers^ b/dom/media/test/seek.ogv^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/seek.ogv^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/seek.webm b/dom/media/test/seek.webm
new file mode 100644
index 0000000000..72b0297233
--- /dev/null
+++ b/dom/media/test/seek.webm
Binary files differ
diff --git a/dom/media/test/seek.webm^headers^ b/dom/media/test/seek.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/seek.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/seekLies.sjs b/dom/media/test/seekLies.sjs
new file mode 100644
index 0000000000..4fc528a0a5
--- /dev/null
+++ b/dom/media/test/seekLies.sjs
@@ -0,0 +1,22 @@
+function handleRequest(request, response) {
+ var file = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
+ var fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ var bis = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+ Ci.nsIBinaryInputStream
+ );
+ var paths = "tests/dom/media/test/seek.ogv";
+ var split = paths.split("/");
+ for (var i = 0; i < split.length; ++i) {
+ file.append(split[i]);
+ }
+ fis.init(file, -1, -1, false);
+ bis.setInputStream(fis);
+ var bytes = bis.readBytes(bis.available());
+ response.setHeader("Content-Length", "" + bytes.length, false);
+ response.setHeader("Content-Type", "video/ogg", false);
+ response.setHeader("Accept-Ranges", "bytes", false);
+ response.write(bytes, bytes.length);
+ bis.close();
+}
diff --git a/dom/media/test/seek_support.js b/dom/media/test/seek_support.js
new file mode 100644
index 0000000000..ca6d563f1a
--- /dev/null
+++ b/dom/media/test/seek_support.js
@@ -0,0 +1,61 @@
+// This file expects manifest.js to be included in the same scope.
+/* import-globals-from manifest.js */
+// This file expects SEEK_TEST_NUMBER to be defined by the test.
+/* global SEEK_TEST_NUMBER */
+var manager = new MediaTestManager();
+
+function createTestArray() {
+ var tests = [];
+ var tmpVid = document.createElement("video");
+
+ for (var testNum = 0; testNum < gSeekTests.length; testNum++) {
+ var test = gSeekTests[testNum];
+ if (!tmpVid.canPlayType(test.type)) {
+ continue;
+ }
+
+ var t = {};
+ t.name = test.name;
+ t.type = test.type;
+ t.duration = test.duration;
+ t.number = SEEK_TEST_NUMBER;
+ tests.push(t);
+ }
+ return tests;
+}
+
+function startTest(test, token) {
+ var video = document.createElement("video");
+ video.token = token += "-seek" + test.number + ".js";
+ manager.started(video.token);
+ video.src = test.name;
+ video.preload = "metadata";
+ document.body.appendChild(video);
+ var name = test.name + " seek test " + test.number;
+ var localIs = (function (n) {
+ return function (a, b, msg) {
+ is(a, b, n + ": " + msg);
+ };
+ })(name);
+ var localOk = (function (n) {
+ return function (a, msg) {
+ ok(a, n + ": " + msg);
+ };
+ })(name);
+ var localFinish = (function (v, m) {
+ return function () {
+ v.onerror = null;
+ removeNodeAndSource(v);
+ dump("SEEK-TEST: Finished " + name + " token: " + v.token + "\n");
+ m.finished(v.token);
+ };
+ })(video, manager);
+ dump("SEEK-TEST: Started " + name + "\n");
+ window["test_seek" + test.number](
+ video,
+ test.duration / 2,
+ localIs,
+ localOk,
+ localFinish
+ );
+}
diff --git a/dom/media/test/seek_with_sound.ogg b/dom/media/test/seek_with_sound.ogg
new file mode 100644
index 0000000000..c86d9946bd
--- /dev/null
+++ b/dom/media/test/seek_with_sound.ogg
Binary files differ
diff --git a/dom/media/test/seek_with_sound.ogg^headers^ b/dom/media/test/seek_with_sound.ogg^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/seek_with_sound.ogg^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/short-aac-encrypted-audio.mp4 b/dom/media/test/short-aac-encrypted-audio.mp4
new file mode 100644
index 0000000000..c1ba8d37e4
--- /dev/null
+++ b/dom/media/test/short-aac-encrypted-audio.mp4
Binary files differ
diff --git a/dom/media/test/short-aac-encrypted-audio.mp4^headers^ b/dom/media/test/short-aac-encrypted-audio.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/short-aac-encrypted-audio.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/short-audio-fragmented-cenc-without-pssh.mp4 b/dom/media/test/short-audio-fragmented-cenc-without-pssh.mp4
new file mode 100644
index 0000000000..bc58623999
--- /dev/null
+++ b/dom/media/test/short-audio-fragmented-cenc-without-pssh.mp4
Binary files differ
diff --git a/dom/media/test/short-audio-fragmented-cenc-without-pssh.mp4^headers^ b/dom/media/test/short-audio-fragmented-cenc-without-pssh.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/short-audio-fragmented-cenc-without-pssh.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/short-cenc-pssh-in-moof.mp4 b/dom/media/test/short-cenc-pssh-in-moof.mp4
new file mode 100644
index 0000000000..aa44c3b9a1
--- /dev/null
+++ b/dom/media/test/short-cenc-pssh-in-moof.mp4
Binary files differ
diff --git a/dom/media/test/short-cenc.mp4 b/dom/media/test/short-cenc.mp4
new file mode 100644
index 0000000000..aa44c3b9a1
--- /dev/null
+++ b/dom/media/test/short-cenc.mp4
Binary files differ
diff --git a/dom/media/test/short-cenc.xml b/dom/media/test/short-cenc.xml
new file mode 100644
index 0000000000..9658c3e32f
--- /dev/null
+++ b/dom/media/test/short-cenc.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<!--
+ This XML file describes the encryption applied to short-cenc.mp4. To generate
+ short-cenc, run the following command:
+
+ MP4Box -crypt short-cenc.xml -out short-cenc.mp4 short.mp4
+-->
+
+<GPACDRM type="CENC AES-CTR">
+
+ <DRMInfo type="pssh" version="1">
+ <!--
+ SystemID specified in
+ https://dvcs.w3.org/hg/html-media/raw-file/tip/encrypted-media/cenc-format.html
+ -->
+ <BS ID128="1077efecc0b24d02ace33c1e52e2fb4b" />
+ <!-- Number of KeyIDs = 2 -->
+ <BS bits="32" value="2" />
+ <!-- KeyID -->
+ <BS ID128="0x7e571d017e571d017e571d017e571d01" />
+ <BS ID128="0x7e571d027e571d027e571d027e571d02" />
+ </DRMInfo>
+
+ <CrypTrack trackID="1" isEncrypted="1" IV_size="16" saiSavedBox="senc"
+ first_IV="0x00000000000000000000000000000000">
+ <key KID="0x7e571d017e571d017e571d017e571d01"
+ value="0x7e5711117e5711117e5711117e571111" />
+ </CrypTrack>
+
+ <CrypTrack trackID="2" isEncrypted="1" IV_size="16" saiSavedBox="senc"
+ first_IV="0x00000000000000000000000000000000">
+ <key KID="0x7e571d027e571d027e571d027e571d02"
+ value="0x7e5722227e5722227e5722227e572222" />
+ </CrypTrack>
+
+</GPACDRM>
diff --git a/dom/media/test/short-video.ogv b/dom/media/test/short-video.ogv
new file mode 100644
index 0000000000..68dee3cf2b
--- /dev/null
+++ b/dom/media/test/short-video.ogv
Binary files differ
diff --git a/dom/media/test/short-video.ogv^headers^ b/dom/media/test/short-video.ogv^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/short-video.ogv^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/short-vp9-encrypted-video.mp4 b/dom/media/test/short-vp9-encrypted-video.mp4
new file mode 100644
index 0000000000..23d1083d18
--- /dev/null
+++ b/dom/media/test/short-vp9-encrypted-video.mp4
Binary files differ
diff --git a/dom/media/test/short-vp9-encrypted-video.mp4^headers^ b/dom/media/test/short-vp9-encrypted-video.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/short-vp9-encrypted-video.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/short.mp4 b/dom/media/test/short.mp4
new file mode 100644
index 0000000000..802da047bc
--- /dev/null
+++ b/dom/media/test/short.mp4
Binary files differ
diff --git a/dom/media/test/short.mp4.gz b/dom/media/test/short.mp4.gz
new file mode 100644
index 0000000000..efb95e38e3
--- /dev/null
+++ b/dom/media/test/short.mp4.gz
Binary files differ
diff --git a/dom/media/test/short.mp4^headers^ b/dom/media/test/short.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/short.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/sin-441-1s-44100-afconvert.mp4 b/dom/media/test/sin-441-1s-44100-afconvert.mp4
new file mode 100644
index 0000000000..2e2763823d
--- /dev/null
+++ b/dom/media/test/sin-441-1s-44100-afconvert.mp4
Binary files differ
diff --git a/dom/media/test/sin-441-1s-44100-fdk_aac.mp4 b/dom/media/test/sin-441-1s-44100-fdk_aac.mp4
new file mode 100644
index 0000000000..34b54a1849
--- /dev/null
+++ b/dom/media/test/sin-441-1s-44100-fdk_aac.mp4
Binary files differ
diff --git a/dom/media/test/sin-441-1s-44100-lame.mp3 b/dom/media/test/sin-441-1s-44100-lame.mp3
new file mode 100644
index 0000000000..f0c0164c0a
--- /dev/null
+++ b/dom/media/test/sin-441-1s-44100-lame.mp3
Binary files differ
diff --git a/dom/media/test/sin-441-1s-44100.flac b/dom/media/test/sin-441-1s-44100.flac
new file mode 100644
index 0000000000..960044638c
--- /dev/null
+++ b/dom/media/test/sin-441-1s-44100.flac
Binary files differ
diff --git a/dom/media/test/sin-441-1s-44100.ogg b/dom/media/test/sin-441-1s-44100.ogg
new file mode 100644
index 0000000000..16c197b633
--- /dev/null
+++ b/dom/media/test/sin-441-1s-44100.ogg
Binary files differ
diff --git a/dom/media/test/sin-441-1s-44100.opus b/dom/media/test/sin-441-1s-44100.opus
new file mode 100644
index 0000000000..795034521d
--- /dev/null
+++ b/dom/media/test/sin-441-1s-44100.opus
Binary files differ
diff --git a/dom/media/test/sine.webm b/dom/media/test/sine.webm
new file mode 100644
index 0000000000..3913ffa874
--- /dev/null
+++ b/dom/media/test/sine.webm
Binary files differ
diff --git a/dom/media/test/sine.webm^headers^ b/dom/media/test/sine.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/sine.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/single-xing-header-no-content-length.mp3 b/dom/media/test/single-xing-header-no-content-length.mp3
new file mode 100644
index 0000000000..aef54e220a
--- /dev/null
+++ b/dom/media/test/single-xing-header-no-content-length.mp3
Binary files differ
diff --git a/dom/media/test/single-xing-header-no-content-length.mp3^headers^ b/dom/media/test/single-xing-header-no-content-length.mp3^headers^
new file mode 100644
index 0000000000..abfeb4ce28
--- /dev/null
+++ b/dom/media/test/single-xing-header-no-content-length.mp3^headers^
@@ -0,0 +1,3 @@
+HTTP 200 OK
+Content-Length: invalid
+Cache-Control: no-store
diff --git a/dom/media/test/sintel-short-clearkey-subsample-encrypted-audio.webm b/dom/media/test/sintel-short-clearkey-subsample-encrypted-audio.webm
new file mode 100644
index 0000000000..7497096ba5
--- /dev/null
+++ b/dom/media/test/sintel-short-clearkey-subsample-encrypted-audio.webm
Binary files differ
diff --git a/dom/media/test/sintel-short-clearkey-subsample-encrypted-audio.webm^headers^ b/dom/media/test/sintel-short-clearkey-subsample-encrypted-audio.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/sintel-short-clearkey-subsample-encrypted-audio.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/sintel-short-clearkey-subsample-encrypted-video.webm b/dom/media/test/sintel-short-clearkey-subsample-encrypted-video.webm
new file mode 100644
index 0000000000..0f7609439d
--- /dev/null
+++ b/dom/media/test/sintel-short-clearkey-subsample-encrypted-video.webm
Binary files differ
diff --git a/dom/media/test/sintel-short-clearkey-subsample-encrypted-video.webm^headers^ b/dom/media/test/sintel-short-clearkey-subsample-encrypted-video.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/sintel-short-clearkey-subsample-encrypted-video.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/small-shot-mp3.mp4 b/dom/media/test/small-shot-mp3.mp4
new file mode 100644
index 0000000000..61fe0ac719
--- /dev/null
+++ b/dom/media/test/small-shot-mp3.mp4
Binary files differ
diff --git a/dom/media/test/small-shot-mp3.mp4^headers^ b/dom/media/test/small-shot-mp3.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/small-shot-mp3.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/small-shot.flac b/dom/media/test/small-shot.flac
new file mode 100644
index 0000000000..0da7c9044e
--- /dev/null
+++ b/dom/media/test/small-shot.flac
Binary files differ
diff --git a/dom/media/test/small-shot.m4a b/dom/media/test/small-shot.m4a
new file mode 100644
index 0000000000..51a23c5b49
--- /dev/null
+++ b/dom/media/test/small-shot.m4a
Binary files differ
diff --git a/dom/media/test/small-shot.mp3 b/dom/media/test/small-shot.mp3
new file mode 100644
index 0000000000..f9397a5106
--- /dev/null
+++ b/dom/media/test/small-shot.mp3
Binary files differ
diff --git a/dom/media/test/small-shot.mp3^headers^ b/dom/media/test/small-shot.mp3^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/small-shot.mp3^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/small-shot.ogg b/dom/media/test/small-shot.ogg
new file mode 100644
index 0000000000..1a41623f81
--- /dev/null
+++ b/dom/media/test/small-shot.ogg
Binary files differ
diff --git a/dom/media/test/small-shot.ogg^headers^ b/dom/media/test/small-shot.ogg^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/small-shot.ogg^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/sound.ogg b/dom/media/test/sound.ogg
new file mode 100644
index 0000000000..edda4e9128
--- /dev/null
+++ b/dom/media/test/sound.ogg
Binary files differ
diff --git a/dom/media/test/sound.ogg^headers^ b/dom/media/test/sound.ogg^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/sound.ogg^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/spacestorm-1000Hz-100ms.ogg b/dom/media/test/spacestorm-1000Hz-100ms.ogg
new file mode 100644
index 0000000000..994041e1b0
--- /dev/null
+++ b/dom/media/test/spacestorm-1000Hz-100ms.ogg
Binary files differ
diff --git a/dom/media/test/spacestorm-1000Hz-100ms.ogg^headers^ b/dom/media/test/spacestorm-1000Hz-100ms.ogg^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/spacestorm-1000Hz-100ms.ogg^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/split.webm b/dom/media/test/split.webm
new file mode 100644
index 0000000000..9207017fb6
--- /dev/null
+++ b/dom/media/test/split.webm
Binary files differ
diff --git a/dom/media/test/split.webm^headers^ b/dom/media/test/split.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/split.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/street.mp4 b/dom/media/test/street.mp4
new file mode 100644
index 0000000000..837d23b383
--- /dev/null
+++ b/dom/media/test/street.mp4
Binary files differ
diff --git a/dom/media/test/street.mp4^headers^ b/dom/media/test/street.mp4^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/street.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/sync.webm b/dom/media/test/sync.webm
new file mode 100644
index 0000000000..91ce6320b5
--- /dev/null
+++ b/dom/media/test/sync.webm
Binary files differ
diff --git a/dom/media/test/test-1-mono.opus b/dom/media/test/test-1-mono.opus
new file mode 100644
index 0000000000..d5198e9ceb
--- /dev/null
+++ b/dom/media/test/test-1-mono.opus
Binary files differ
diff --git a/dom/media/test/test-1-mono.opus^headers^ b/dom/media/test/test-1-mono.opus^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/test-1-mono.opus^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/test-2-stereo.opus b/dom/media/test/test-2-stereo.opus
new file mode 100644
index 0000000000..7115cac243
--- /dev/null
+++ b/dom/media/test/test-2-stereo.opus
Binary files differ
diff --git a/dom/media/test/test-2-stereo.opus^headers^ b/dom/media/test/test-2-stereo.opus^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/test-2-stereo.opus^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/test-3-LCR.opus b/dom/media/test/test-3-LCR.opus
new file mode 100644
index 0000000000..145536f3e7
--- /dev/null
+++ b/dom/media/test/test-3-LCR.opus
Binary files differ
diff --git a/dom/media/test/test-3-LCR.opus^headers^ b/dom/media/test/test-3-LCR.opus^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/test-3-LCR.opus^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/test-4-quad.opus b/dom/media/test/test-4-quad.opus
new file mode 100644
index 0000000000..731b867b2e
--- /dev/null
+++ b/dom/media/test/test-4-quad.opus
Binary files differ
diff --git a/dom/media/test/test-4-quad.opus^headers^ b/dom/media/test/test-4-quad.opus^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/test-4-quad.opus^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/test-5-5.0.opus b/dom/media/test/test-5-5.0.opus
new file mode 100644
index 0000000000..7eb2faa812
--- /dev/null
+++ b/dom/media/test/test-5-5.0.opus
Binary files differ
diff --git a/dom/media/test/test-5-5.0.opus^headers^ b/dom/media/test/test-5-5.0.opus^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/test-5-5.0.opus^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/test-6-5.1.opus b/dom/media/test/test-6-5.1.opus
new file mode 100644
index 0000000000..526515eb7b
--- /dev/null
+++ b/dom/media/test/test-6-5.1.opus
Binary files differ
diff --git a/dom/media/test/test-6-5.1.opus^headers^ b/dom/media/test/test-6-5.1.opus^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/test-6-5.1.opus^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/test-7-6.1.opus b/dom/media/test/test-7-6.1.opus
new file mode 100644
index 0000000000..8b6a7ce329
--- /dev/null
+++ b/dom/media/test/test-7-6.1.opus
Binary files differ
diff --git a/dom/media/test/test-7-6.1.opus^headers^ b/dom/media/test/test-7-6.1.opus^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/test-7-6.1.opus^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/test-8-7.1.opus b/dom/media/test/test-8-7.1.opus
new file mode 100644
index 0000000000..e56176a30f
--- /dev/null
+++ b/dom/media/test/test-8-7.1.opus
Binary files differ
diff --git a/dom/media/test/test-8-7.1.opus^headers^ b/dom/media/test/test-8-7.1.opus^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/test-8-7.1.opus^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/test-stereo-phase-inversion-180.opus b/dom/media/test/test-stereo-phase-inversion-180.opus
new file mode 100644
index 0000000000..ce70290002
--- /dev/null
+++ b/dom/media/test/test-stereo-phase-inversion-180.opus
Binary files differ
diff --git a/dom/media/test/test-stereo-phase-inversion-180.opus^headers^ b/dom/media/test/test-stereo-phase-inversion-180.opus^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/test-stereo-phase-inversion-180.opus^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/test_VideoPlaybackQuality.html b/dom/media/test/test_VideoPlaybackQuality.html
new file mode 100644
index 0000000000..67636f9ea6
--- /dev/null
+++ b/dom/media/test/test_VideoPlaybackQuality.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test basic functionality of VideoPlaybackQuality</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+function test() {
+ var video = document.createElement("video");
+ ok(video.getVideoPlaybackQuality, "getVideoPlaybackQuality should be exposed with pref set");
+
+ var vpq = video.getVideoPlaybackQuality();
+ ok(vpq, "getVideoPlaybackQuality should return an object");
+ ok(vpq.creationTime <= performance.now(), "creationTime should be in the past");
+ is(vpq.totalVideoFrames, 0, "totalVideoFrames should be 0");
+ is(vpq.droppedVideoFrames, 0, "droppedVideoFrames should be 0");
+
+ var vpq2 = video.getVideoPlaybackQuality();
+ ok(vpq !== vpq2, "getVideoPlaybackQuality should return a new object");
+ ok(vpq.creationTime <= vpq2.creationTime, "VideoPlaybackQuality objects should have increasing creationTime");
+
+ var audio = document.createElement("audio");
+ ok(!audio.getVideoPlaybackQuality, "getVideoPlaybackQuality should not be available on Audio elements");
+
+ video.src = "seek.webm";
+ video.play();
+ video.addEventListener("ended", function () {
+ vpq = video.getVideoPlaybackQuality();
+ ok(vpq.creationTime <= performance.now(), "creationTime should be in the past");
+ ok(vpq.totalVideoFrames > 0, "totalVideoFrames should be > 0");
+ ok(vpq.droppedVideoFrames >= 0, "droppedVideoFrames should be >= 0");
+ ok(vpq.droppedVideoFrames <= vpq.totalVideoFrames, "droppedVideoFrames should be <= totalVideoFrames");
+
+ SpecialPowers.pushPrefEnv({"set": [["media.video_stats.enabled", false]]}, function () {
+ vpq = video.getVideoPlaybackQuality();
+ is(vpq.creationTime, 0, "creationTime should be 0");
+ is(vpq.totalVideoFrames, 0, "totalVideoFrames should be 0");
+ is(vpq.droppedVideoFrames, 0, "droppedVideoFrames should be 0");
+
+ SimpleTest.finish();
+ });
+ });
+}
+
+addLoadEvent(function() {
+ SpecialPowers.pushPrefEnv({"set":
+ [
+ ["media.mediasource.enabled", true],
+ ]
+ }, test);
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_VideoPlaybackQuality_disabled.html b/dom/media/test/test_VideoPlaybackQuality_disabled.html
new file mode 100644
index 0000000000..1c41b79d8b
--- /dev/null
+++ b/dom/media/test/test_VideoPlaybackQuality_disabled.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test basic functionality of VideoPlaybackQuality</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+function test() {
+ var video = document.createElement("video");
+ ok(!video.getVideoPlaybackQuality, "getVideoPlaybackQuality should be hidden behind a pref");
+ var accessThrows = false;
+ try {
+ video.getVideoPlaybackQuality();
+ } catch (e) {
+ accessThrows = true;
+ }
+ ok(accessThrows, "getVideoPlaybackQuality should be hidden behind a pref");
+ SimpleTest.finish();
+}
+
+addLoadEvent(function() {
+ SpecialPowers.pushPrefEnv({"set":
+ [
+ ["media.mediasource.enabled", false],
+ ]
+ }, test);
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_access_control.html b/dom/media/test/test_access_control.html
new file mode 100644
index 0000000000..bad43dff22
--- /dev/null
+++ b/dom/media/test/test_access_control.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=451958
+-->
+<head>
+ <title>Test for Bug 451958</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=451958">Mozilla Bug 451958</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 451958 **/
+
+function openWindow() {
+ window.open("http://example.org:80/tests/dom/media/test/file_access_controls.html", "", "width=500,height=500");
+}
+
+function run() {
+ SpecialPowers.pushPrefEnv({
+ "set": [
+ ["dom.security.https_first", false],
+ ],
+ }, openWindow);
+}
+
+function done() {
+ mediaTestCleanup();
+ SimpleTest.finish();
+}
+
+addLoadEvent(run);
+SimpleTest.waitForExplicitFinish();
+
+
+window.addEventListener("message", receiveMessage);
+
+function receiveMessage(event)
+{
+ if (event.origin !== "http://example.org") {
+ ok(false, "Received message from wrong domain");
+ return;
+ }
+
+ if (event.data.done == "true") {
+ done();
+ return;
+ }
+
+ ok(event.data.result, event.data.message);
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_arraybuffer.html b/dom/media/test/test_arraybuffer.html
new file mode 100644
index 0000000000..9ef84c53dc
--- /dev/null
+++ b/dom/media/test/test_arraybuffer.html
@@ -0,0 +1,83 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1462967
+-->
+<head>
+ <title>Test for Bug 1457661</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1457661">Mozilla Bug 1457661</a>
+
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+// Test for Bug 1457661; Ensure that readyState properly move to 4 when using arrayBuffer obtained from XMLHttpRequest
+
+var manager = new MediaTestManager;
+
+function getBlob(url, callback) {
+ const req = new XMLHttpRequest();
+ req.addEventListener("load", function() {
+ callback(req.response);
+ });
+ req.open("GET", url, true);
+ req.responseType = "arraybuffer";
+ req.send();
+}
+
+function startTest(test, token) {
+ manager.started(token);
+ // Fetch the media resource using XHR so we can be sure the entire
+ // resource is loaded before we test buffered ranges. This ensures
+ // we have deterministic behaviour.
+ getBlob(test.name, function(arr) {
+ const v = document.createElement("video");
+ const events = [ "suspend", "play", "canplay", "canplaythrough", "loadstart", "loadedmetadata",
+ "loadeddata", "playing", "ended", "error", "stalled", "emptied", "abort",
+ "waiting", "pause", "durationchange", "seeking", "seeked" ];
+ function logEvent(e) {
+ info(test.name + ": got " + e.type + " event");
+ }
+ events.forEach(function(e) {
+ v.addEventListener(e, logEvent);
+ });
+ once(v, "stalled", function(e) {
+ // Resource fetch algorithm in local mode should never fire stalled event.
+ // https://html.spec.whatwg.org/multipage/media.html#concept-media-load-resource
+ ok(false, test.name + ": got stalled");
+ removeNodeAndSource(v);
+ manager.finished(token);
+ });
+ once(v, "canplaythrough", function(e) {
+ ok(true, test.name + ": got canplaythrough");
+ is(v.readyState, v.HAVE_ENOUGH_DATA, test.name + ": readyState is HAVE_ENOUGH_DATA");
+ removeNodeAndSource(v);
+ manager.finished(token);
+ });
+ const blob = new Blob([arr], {type:test.type});
+ const blobUrl = URL.createObjectURL(blob);
+
+ document.body.appendChild(v);
+ v.preload = "auto";
+ v.src = blobUrl;
+ v.load();
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+// Note: No need to set media test prefs, since we're using XHR to fetch
+// media data.
+manager.runTests(gSmallTests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_aspectratio_mp4.html b/dom/media/test/test_aspectratio_mp4.html
new file mode 100644
index 0000000000..5e01875439
--- /dev/null
+++ b/dom/media/test/test_aspectratio_mp4.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=975978
+-->
+
+<head>
+ <title>Media test: default video size</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=975978">Mozilla Bug 975978</a>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// MP4 video with display size is difference to decode frame size.
+// The display size is recorded in TrackHeaderBox 'tkhd' of this mp4 video.
+var resource =
+ { name:"pixel_aspect_ratio.mp4", type:"video/mp4", width:525, height:288 };
+
+var v = document.createElement("video");
+v.onloadedmetadata = function() {
+ is(v.videoWidth, resource.width, "Intrinsic width should match video width");
+ is(v.videoHeight, resource.height, "Intrinsic height should match video height");
+ SimpleTest.finish();
+}
+v.addEventListener("error", function(ev) {
+ if (v.readyState < v.HAVE_METADATA) {
+ info("Video element returns with readyState " + v.readyState + " error.code " + v.error.code);
+ todo(false, "This platform doesn't support to retrieve MP4 metadata.");
+ SimpleTest.finish();
+ }
+});
+
+v.src = resource.name;
+v.preload = "auto";
+
+document.body.appendChild(v);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_audio1.html b/dom/media/test/test_audio1.html
new file mode 100644
index 0000000000..a5d9f12ab9
--- /dev/null
+++ b/dom/media/test/test_audio1.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: Audio Constructor Test 1</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+function startTest(test, token) {
+ var a1 = new Audio();
+ if (!a1.canPlayType(test.type))
+ return;
+ manager.started(token);
+
+ is(a1.getAttribute("preload"), "auto", "preload:auto automatically set");
+ is(a1.src, "", "Src set?");
+ a1 = null;
+ manager.finished(token);
+}
+
+manager.runTests(gAudioTests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_audio2.html b/dom/media/test/test_audio2.html
new file mode 100644
index 0000000000..fde9adc791
--- /dev/null
+++ b/dom/media/test/test_audio2.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: Audio Constructor Test 2</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var tmpAudio = new Audio();
+
+var manager = new MediaTestManager;
+
+function startTest(test, token) {
+ if (!tmpAudio.canPlayType(test.type))
+ return;
+ manager.started(token);
+ var a1 = new Audio(test.name);
+ is(a1.getAttribute("preload"), "auto", "Preload automatically set to auto");
+ ok(a1.src.endsWith("/" + test.name), "src OK");
+ manager.finished(token);
+}
+
+manager.runTests(gAudioTests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_audioDocumentTitle.html b/dom/media/test/test_audioDocumentTitle.html
new file mode 100644
index 0000000000..2913debb39
--- /dev/null
+++ b/dom/media/test/test_audioDocumentTitle.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=463830
+-->
+<head>
+ <title>Test for Bug 463830</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=463830">Mozilla Bug 463830</a>
+<p id="display"></p>
+<iframe id="i"></iframe>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 463830 **/
+
+var gTests = [
+ { file: "r11025_s16_c1.wav", title: "r11025_s16_c1.wav" }
+];
+
+var gTestNum = 0;
+
+addLoadEvent(runTest);
+
+var title;
+var i = document.getElementById("i");
+
+function runTest() {
+ if (gTestNum == gTests.length) {
+ SimpleTest.finish();
+ return;
+ }
+ if (gTestNum == 0) {
+ i.addEventListener("load", function() {
+ is(i.contentDocument.title, title, "Doc title incorrect");
+ setTimeout(runTest, 0);
+ });
+ }
+
+ title = gTests[gTestNum].title;
+ i.src = gTests[gTestNum].file;
+ gTestNum++;
+}
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_background_video_cancel_suspend_taint.html b/dom/media/test/test_background_video_cancel_suspend_taint.html
new file mode 100644
index 0000000000..26691f3f43
--- /dev/null
+++ b/dom/media/test/test_background_video_cancel_suspend_taint.html
@@ -0,0 +1,70 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test Background Video Suspend Cancels (Element Taint)</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="manifest.js"></script>
+<script src="background_video.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<script type="text/javascript">
+"use strict";
+
+var manager = new MediaTestManager;
+
+/**
+ * @param {HTMLMediaElement} video Video element under test.
+ * @returns {Promise} Promise that is resolved when video decode resumes.
+ */
+function testSuspendTimerCanceledWhenTainted(video) {
+ function ended() {
+ video.removeEventListener("mozcancelvideosuspendtimer", canceled);
+ ok(false, `${video.token} ended before suspend cancels`);
+ this.ended_resolve();
+ }
+
+ function canceled() {
+ video.removeEventListener("ended", ended);
+ ok(true, `${video.token} suspend cancels`);
+ this.canceled_resolve();
+ }
+
+ let p = Promise.race([
+ new Promise((resolve) => {
+ video.ended_resolve = resolve;
+ video.addEventListener('ended', ended, { 'once': true });
+ }),
+ new Promise((resolve) => {
+ video.canceled_resolve = resolve;
+ video.addEventListener('mozcancelvideosuspendtimer', canceled, { 'once': true });
+ })
+ ]);
+
+ Log(video.token, "Mark tainted");
+
+ let c = document.createElement('canvas');
+ let g = c.getContext('2d');
+ g.drawImage(video, 0, 0, c.width, c.height);
+ ok(video.hasSuspendTaint(), 'video used with drawImage is tainted.');
+
+ return p;
+}
+
+startTest({
+ desc: 'Test Background Video Suspend Cancels (Element Taint)',
+ prefs: [
+ [ "media.test.video-suspend", true ],
+ [ "media.suspend-bkgnd-video.enabled", true ],
+ [ "media.suspend-bkgnd-video.delay-ms", 10000 ]
+ ],
+ tests: gDecodeSuspendTests,
+ runTest: (test, token) => {
+ let v = appendVideoToDoc(test.name, token);
+ manager.started(token);
+
+ waitUntilVisible(v)
+ .then(() => waitUntilPlaying(v))
+ .then(() => testSuspendTimerStartedWhenHidden(v))
+ .then(() => testSuspendTimerCanceledWhenTainted(v))
+ .then(() => { manager.finished(token); });
+ }
+});
+</script>
diff --git a/dom/media/test/test_background_video_cancel_suspend_visible.html b/dom/media/test/test_background_video_cancel_suspend_visible.html
new file mode 100644
index 0000000000..e947b27734
--- /dev/null
+++ b/dom/media/test/test_background_video_cancel_suspend_visible.html
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test Background Video Suspend Cancels (Visibility)</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="manifest.js"></script>
+<script src="background_video.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<script type="text/javascript">
+"use strict";
+
+var manager = new MediaTestManager;
+
+/**
+ * Check that making the element visible before suspend timer delay causes the
+ * the suspend timer to be canceled.
+ * @param {HTMLMediaElement} video Video element under test.
+ * @returns {Promise} Promise that is resolved when video decode resumes.
+ */
+function testSuspendTimerCanceledWhenShown(video) {
+ function ended() {
+ video.removeEventListener("mozcancelvideosuspendtimer", canceled);
+ ok(false, `${video.token} ended before suspend cancels`);
+ this.ended_resolve();
+ }
+
+ function canceled() {
+ video.removeEventListener("ended", ended);
+ ok(true, `${video.token} suspend cancels`);
+ this.canceled_resolve();
+ }
+
+ let p = Promise.race([
+ new Promise((resolve) => {
+ video.ended_resolve = resolve;
+ video.addEventListener('ended', ended, { 'once': true });
+ }),
+ new Promise((resolve) => {
+ video.canceled_resolve = resolve;
+ video.addEventListener('mozcancelvideosuspendtimer', canceled, { 'once': true });
+ })
+ ]);
+
+ Log(video.token, "Set visible");
+ video.setVisible(true);
+
+ return p;
+}
+
+startTest({
+ desc: 'Test Background Video Suspend Cancels (Visibility)',
+ prefs: [
+ [ "media.test.video-suspend", true ],
+ [ "media.suspend-bkgnd-video.enabled", true ],
+ [ "media.suspend-bkgnd-video.delay-ms", 10000 ]
+ ],
+ tests: gDecodeSuspendTests,
+ runTest: (test, token) => {
+ let v = appendVideoToDoc(test.name, token);
+ manager.started(token);
+
+ waitUntilPlaying(v)
+ .then(() => testSuspendTimerStartedWhenHidden(v))
+ .then(() => testSuspendTimerCanceledWhenShown(v))
+ .then(() => {
+ ok(true, `${v.token} finished`);
+ manager.finished(token);
+ });
+ }
+});
diff --git a/dom/media/test/test_background_video_drawimage_with_suspended_video.html b/dom/media/test/test_background_video_drawimage_with_suspended_video.html
new file mode 100644
index 0000000000..32bda51dbf
--- /dev/null
+++ b/dom/media/test/test_background_video_drawimage_with_suspended_video.html
@@ -0,0 +1,76 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test Background Video Displays Video Frame via drawImage When Suspended</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="manifest.js"></script>
+<script src="background_video.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<style>
+video, canvas {
+ border: 1px solid black;
+}
+</style>
+<script type="text/javascript">
+"use strict";
+
+var manager = new MediaTestManager;
+
+function drawVideoToCanvas(v) {
+ console.log('drawVideoToCanvas');
+ let c = document.createElement('canvas');
+ c.width = 4;
+ c.height = 4;
+ c.style.width = 64;
+ c.style.height = 64;
+ document.body.appendChild(c);
+
+ let gfx = c.getContext('2d');
+ if (!gfx) {
+ throw Error("Unable to obtain context '2d' from canvas");
+ }
+
+ gfx.drawImage(v, 0, 0, 4, 4);
+ let imageData = gfx.getImageData(0, 0, 4, 4);
+ let pixels = imageData.data;
+
+ // Check that pixels aren't all the same colour.
+ // Implements by checking against rgb of the first pixel.
+ let rr = pixels[0],
+ gg = pixels[1],
+ bb = pixels[2],
+ allSame = true;
+
+ for (let i = 0; i < 4*4; i++) {
+ let r = pixels[4*i+0];
+ let g = pixels[4*i+1];
+ let b = pixels[4*i+2];
+ if (r != rr || g != gg || b != bb) {
+ allSame = false;
+ break;
+ }
+ }
+
+ ok(!allSame, "Pixels aren't all the same color");
+}
+
+startTest({
+ desc: 'Test Background Video Displays Video Frame via drawImage When Suspended',
+ prefs: [
+ [ "media.test.video-suspend", true ],
+ [ "media.suspend-bkgnd-video.enabled", true ],
+ [ "media.suspend-bkgnd-video.delay-ms", 500 ]
+ ],
+ tests: gDecodeSuspendTests,
+ runTest: (test, token) => {
+ let v = appendVideoToDoc(test.name, token);
+ manager.started(token);
+
+ waitUntilPlaying(v)
+ .then(() => testVideoSuspendsWhenHidden(v))
+ .then(() => {
+ drawVideoToCanvas(v);
+ manager.finished(token);
+ });
+ }
+});
+</script>
diff --git a/dom/media/test/test_background_video_ended_event.html b/dom/media/test/test_background_video_ended_event.html
new file mode 100644
index 0000000000..99e2dd2f18
--- /dev/null
+++ b/dom/media/test/test_background_video_ended_event.html
@@ -0,0 +1,48 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test Background Video Suspends</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="manifest.js"></script>
+<script src="background_video.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<script type="text/javascript">
+"use strict";
+
+var manager = new MediaTestManager;
+
+startTest({
+ desc: "Test background video doesn't fire the 'ended' event twice.",
+ prefs: [
+ [ "media.test.video-suspend", true ],
+ [ "media.suspend-bkgnd-video.enabled", true ],
+ [ "media.suspend-bkgnd-video.delay-ms", 100 ]
+ ],
+ tests: gDecodeSuspendTests,
+ runTest: (test, token) => {
+ let v = appendVideoToDoc(test.name, token);
+ manager.started(token);
+
+ let count = 0;
+ v.addEventListener("ended", function() {
+ is(++count, 1, `${token} should get only one 'ended' event.`);
+ });
+
+ /*
+ * This test checks that, after a video element had finished its playback,
+ * resuming video decoder doesn't dispatch 2nd ended event.
+ * This issue was found in bug 1349097.
+ */
+ waitUntilPlaying(v)
+ .then(() => testVideoSuspendsWhenHidden(v))
+ .then(() => waitUntilEnded(v))
+ .then(() => testVideoResumesWhenShown(v))
+ .then(() => {
+ // Wait for a while and finish the test. We should get only one 'ended' event.
+ setTimeout(function() {
+ removeNodeAndSource(v);
+ manager.finished(token);
+ }, 3000);
+ });
+ }
+});
+</script>
diff --git a/dom/media/test/test_background_video_no_suspend_disabled.html b/dom/media/test/test_background_video_no_suspend_disabled.html
new file mode 100644
index 0000000000..f93977f2df
--- /dev/null
+++ b/dom/media/test/test_background_video_no_suspend_disabled.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test Background Video Doesn't Suspend When Feature Disabled</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="manifest.js"></script>
+<script src="background_video.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<script>
+"use strict";
+
+var manager = new MediaTestManager;
+
+startTest({
+ desc: "Test Background Video Doesn't Suspend When Feature Disabled.",
+ prefs: [
+ [ 'media.test.video-suspend', true ],
+ [ 'media.suspend-bkgnd-video.enabled', false ],
+ [ 'media.suspend-bkgnd-video.delay-ms', 0 ]
+ ],
+ tests: gDecodeSuspendTests,
+ runTest: (test, token) => {
+ let v = appendVideoToDoc(test.name, token);
+ manager.started(token);
+
+ /* This test checks that suspend doesn't occur when the feature is disabled */
+ waitUntilPlaying(v)
+ .then(() => checkVideoDoesntSuspend(v))
+ .then(() => {
+ ok(true, 'Video ended before decode was suspended');
+ manager.finished(token); })
+ .catch((e) => {
+ ok(false, 'Test Failed: ' + e.toString());
+ manager.finished(token); });
+ }
+});
+</script> \ No newline at end of file
diff --git a/dom/media/test/test_background_video_no_suspend_not_in_tree.html b/dom/media/test/test_background_video_no_suspend_not_in_tree.html
new file mode 100644
index 0000000000..f65d363bd6
--- /dev/null
+++ b/dom/media/test/test_background_video_no_suspend_not_in_tree.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test Background Video Doesn't Suspend When Timeout Is Longer Than Video</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="manifest.js"></script>
+<script src="background_video.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<script>
+"use strict";
+
+var manager = new MediaTestManager;
+
+var MIN_DELAY = 100;
+
+/**
+ * @param {string} url video src.
+ * @returns {HTMLMediaElement} The created video element.
+ */
+function createVideoNotAppendToDoc(url, token, width, height) {
+ // Default size of (160, 120) is used by other media tests.
+ if (width === undefined) { width = 160; }
+ if (height === undefined) { height = 3*width/4; }
+
+ let v = document.createElement('video');
+ v.token = token;
+ v.width = width;
+ v.height = height;
+ v.src = url;
+ return v;
+}
+
+startTest({
+ desc: "Test Background Video Doesn't Suspend When If The Video Is Not In Tree.",
+ prefs: [
+ [ 'media.test.video-suspend', true ],
+ [ 'media.suspend-bkgnd-video.enabled', true ],
+ [ 'media.suspend-bkgnd-video.delay-ms', MIN_DELAY ]
+ ],
+ tests: gDecodeSuspendTests,
+ runTest: (test, token) => {
+ let v = createVideoNotAppendToDoc(test.name, token);
+ manager.started(token);
+
+ /* This test checks that suspend doesn't occur if a video element is not
+ append to tree. */
+ waitUntilPlaying(v)
+ .then(() => checkVideoDoesntSuspend(v))
+ .then(() => {
+ ok(true, 'Video ended before decode was suspended');
+ manager.finished(token); })
+ .catch((e) => {
+ ok(false, 'Test Failed: ' + e.toString());
+ manager.finished(token); });
+ }
+});
+</script> \ No newline at end of file
diff --git a/dom/media/test/test_background_video_no_suspend_short_vid.html b/dom/media/test/test_background_video_no_suspend_short_vid.html
new file mode 100644
index 0000000000..35597f6b96
--- /dev/null
+++ b/dom/media/test/test_background_video_no_suspend_short_vid.html
@@ -0,0 +1,38 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test Background Video Doesn't Suspend When Timeout Is Longer Than Video</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="manifest.js"></script>
+<script src="background_video.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<script>
+"use strict";
+
+var manager = new MediaTestManager;
+
+startTest({
+ desc: "Test Background Video Doesn't Suspend When Timeout Is Longer Than Video.",
+ prefs: [
+ [ 'media.test.video-suspend', true ],
+ [ 'media.suspend-bkgnd-video.enabled', true ],
+ // Gizmo.mp4 is about 5.6s
+ [ 'media.suspend-bkgnd-video.delay-ms', 10000 ]
+ ],
+ tests: gDecodeSuspendTests,
+ runTest: (test, token) => {
+ let v = appendVideoToDoc(test.name, token);
+ manager.started(token);
+
+ /* This test checks that suspend doesn't occur when the delay is longer
+ than the duration of the video that's playing */
+ waitUntilPlaying(v)
+ .then(() => checkVideoDoesntSuspend(v))
+ .then(() => {
+ ok(true, 'Video ended before decode was suspended');
+ manager.finished(token); })
+ .catch((e) => {
+ ok(false, 'Test Failed: ' + e.toString());
+ manager.finished(token); });
+ }
+});
+</script> \ No newline at end of file
diff --git a/dom/media/test/test_background_video_resume_after_end_show_last_frame.html b/dom/media/test/test_background_video_resume_after_end_show_last_frame.html
new file mode 100644
index 0000000000..767567e116
--- /dev/null
+++ b/dom/media/test/test_background_video_resume_after_end_show_last_frame.html
@@ -0,0 +1,131 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test Background Video Suspends</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="manifest.js"></script>
+<script src="background_video.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<script type="text/javascript">
+"use strict";
+
+var manager = new MediaTestManager;
+
+function testSameContent(video1, video2) {
+ if (video1.videoWidth != video2.videoWidth ||
+ video1.videoHeight != video2.videoHeight) {
+ ok(false, `${video1.token} video1 and video2 have different dimensions.`);
+ return;
+ }
+
+ let w = video1.videoWidth;
+ let h = video1.videoHeight;
+ let c1 = document.createElement('canvas');
+ let c2 = document.createElement('canvas');
+ c1.width = w;
+ c1.height = h;
+ c2.width = w;
+ c2.height = h;
+
+ let gfx1 = c1.getContext('2d');
+ let gfx2 = c2.getContext('2d');
+ if (!gfx1 || !gfx2) {
+ ok(false, "Unable to obtain context '2d' from canvas");
+ return;
+ }
+
+ gfx1.drawImage(video1, 0, 0, w, h);
+ gfx2.drawImage(video2, 0, 0, w, h);
+
+ // Get content out.
+ let contentWidth = 4;
+ let contentHeight = 4;
+ let imageData1 = gfx1.getImageData(0, 0, contentWidth, contentHeight);
+ let imageData2 = gfx2.getImageData(0, 0, contentWidth, contentHeight);
+ let pixels1 = imageData1.data;
+ let pixels2 = imageData2.data;
+
+ // Check that the content of two video are identical.
+ for (let i = 0; i < contentWidth*contentHeight; i++) {
+ let pixelCount = 4 * i;
+ if (pixels1[pixelCount+0] != pixels2[pixelCount+0] ||
+ pixels1[pixelCount+1] != pixels2[pixelCount+1] ||
+ pixels1[pixelCount+2] != pixels2[pixelCount+2] ||
+ pixels1[pixelCount+3] != pixels2[pixelCount+3]) {
+ ok(false, `${video1.token} video1 and video2 have different content.`);
+ return;
+ }
+ }
+
+ ok(true, `${video1.token} video1 and video2 have identical content.`);
+}
+
+function waitUntilSeekToLastFrame(video) {
+ Log(video.token, "Waiting for seeking to the last frame");
+ function callSeekToNextFrame() {
+ video.seekToNextFrame().then(
+ () => {
+ if (!video.seenEnded) {
+ callSeekToNextFrame();
+ }
+ },
+ () => {
+ // When seek reaches the end, the promise is resolved before 'ended'
+ // is fired. The resolver calls callSeekToNextFrame() to schedule
+ // another seek and then the 'ended' handler calls finish() to shut
+ // down the MediaDecoder which will reject the seek promise. So we don't
+ // raise an error in this case.
+ ok(video.seenEnded, "seekToNextFrame() failed.");
+ }
+ );
+ }
+
+ return new Promise(function(resolve, reject) {
+ video.seenEnded = false;
+ video.addEventListener("ended", () => {
+ video.seenEnded = true;
+ resolve();
+ }, true);
+ callSeekToNextFrame(video);
+ });
+}
+
+startTest({
+ desc: "Test resume an ended video shows the last frame.",
+ prefs: [
+ [ "media.test.video-suspend", true ],
+ [ "media.suspend-bkgnd-video.enabled", true ],
+ [ "media.suspend-bkgnd-video.delay-ms", 100 ],
+ [ "media.dormant-on-pause-timeout-ms", -1],
+ [ "media.decoder.skip-to-next-key-frame.enabled", false]
+ ],
+ tests: gDecodeSuspendTests,
+
+ runTest: (test, token) => {
+ let v = appendVideoToDocWithoutLoad(token);
+ let vReference = appendVideoToDocWithoutLoad(token+"-ref");
+ manager.started(token);
+
+ /*
+ * This test checks that, after a video element had finished its playback,
+ * resuming video decoder should seek to the last frame.
+ * This issue was found in bug 1358057.
+ */
+ waitUntilVisible(v)
+ .then(() => {
+ return Promise.all([loadAndWaitUntilLoadedmetadata(v, test.name), loadAndWaitUntilLoadedmetadata(vReference, test.name, "auto")]);
+ })
+ .then(() => waitUntilPlaying(v))
+ .then(() => testVideoSuspendsWhenHidden(v))
+ .then(() => {
+ return Promise.all([waitUntilEnded(v), waitUntilSeekToLastFrame(vReference)]);
+ })
+ .then(() => testVideoOnlySeekCompletedWhenShown(v))
+ .then(() => {
+ testSameContent(v, vReference);
+ removeNodeAndSource(v);
+ removeNodeAndSource(vReference);
+ manager.finished(token);
+ });
+ }
+});
+</script>
diff --git a/dom/media/test/test_background_video_resume_looping_video_without_audio.html b/dom/media/test/test_background_video_resume_looping_video_without_audio.html
new file mode 100644
index 0000000000..53b380ce15
--- /dev/null
+++ b/dom/media/test/test_background_video_resume_looping_video_without_audio.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Resume suspended looping video which doesn't contain audio track</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="manifest.js"></script>
+ <script src="background_video.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+/**
+ * This test is used to ensure that the looping video (without audio track) which
+ * has been suspended can continute to playback correctly after we resume video
+ * decoding.
+ */
+async function startTest() {
+ const video = await createVisibleVideo();
+ await startVideo(video);
+ await suspendVideoDecoding(video);
+ await resumeVideoDecoding(video);
+ await checkIfVideoIsStillPlaying(video);
+ endTestAndClearVideo(video);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({ 'set': [
+ ["media.test.video-suspend", true],
+ ["media.suspend-bkgnd-video.enabled", true],
+ ["media.suspend-bkgnd-video.delay-ms", 0],
+ ]}, () => {
+ startTest();
+});
+
+/**
+ * The following are test helper functions.
+ */
+async function createVisibleVideo() {
+ let video = document.createElement("video");
+ video.src = "gizmo-noaudio.webm";
+ video.controls = true;
+ video.loop = true;
+ document.body.appendChild(video);
+ info(`ensure video becomes visible`);
+ await waitUntilVisible(video);
+ return video;
+}
+
+async function startVideo(video) {
+ info(`start playing video`);
+ const played = video && await video.play().then(() => true, () => false);
+ ok(played, "video has started playing");
+}
+
+async function suspendVideoDecoding(video) {
+ info(`suspend video decoding`);
+ video.setVisible(false);
+ await nextVideoSuspends(video);
+ info(`suspended video decoding`);
+}
+
+async function resumeVideoDecoding(video) {
+ info(`resume video decoding.`);
+ video.setVisible(true);
+ await nextVideoResumes(video);
+ info(`resumed video decoding`);
+}
+
+async function checkIfVideoIsStillPlaying(video) {
+ await once(video, "timeupdate");
+ ok(!video.paused, "video is still playing after resuming video decoding.")
+}
+
+function endTestAndClearVideo(video) {
+ removeNodeAndSource(video);
+ SimpleTest.finish();
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/media/test/test_background_video_suspend.html b/dom/media/test/test_background_video_suspend.html
new file mode 100644
index 0000000000..a6a720ad13
--- /dev/null
+++ b/dom/media/test/test_background_video_suspend.html
@@ -0,0 +1,81 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test Background Video Suspends</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="manifest.js"></script>
+<script src="background_video.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+<script type="text/javascript">
+ "use strict";
+
+ var manager = new MediaTestManager;
+
+ var MIN_DELAY = 100;
+
+ function testDelay(v, start, min) {
+ let end = performance.now();
+ let delay = end - start;
+ ok(delay > min, `${v.token} suspended with a delay of ${delay} ms`);
+ }
+
+ async function runTest(test, token) {
+ let video = appendVideoToDocWithoutLoad(token);
+ manager.started(token);
+
+ let visible = waitUntilVisible(video);
+ let ended = nextVideoEnded(video);
+ let playing = nextVideoPlaying(video);
+ let resumes = nextVideoResumes(video);
+ let suspends = nextVideoSuspends(video);
+
+ Log(token, "Waiting until video becomes visible");
+ await visible;
+
+ Log(token, "Waiting for metadata loaded");
+ await loadAndWaitUntilLoadedmetadata(video, test.name);
+
+ Log(token, "Start playing");
+ video.play();
+
+ Log(token, "Waiting for video playing");
+ await playing;
+
+ let start = performance.now();
+
+ Log(token, "Set hidden");
+ video.setVisible(false);
+
+ Log(token, "Waiting for video suspend");
+ await suspends;
+
+ testDelay(video, start, MIN_DELAY);
+
+ Log(token, "Set visible");
+ video.setVisible(true);
+
+ Log(token, "Waiting for video resume");
+ await resumes;
+
+ Log(token, "Waiting for ended");
+ await ended;
+
+ ok(video.currentTime >= video.duration, 'current time approximates duration.');
+
+ removeNodeAndSource(video);
+ manager.finished(token);
+ }
+
+ startTest({
+ desc: 'Test Background Video Suspends',
+ prefs: [
+ ["media.test.video-suspend", true],
+ ["media.suspend-bkgnd-video.enabled", true],
+ // Use a short delay to ensure video decode suspend happens before end
+ // of video.
+ ["media.suspend-bkgnd-video.delay-ms", MIN_DELAY],
+ ["privacy.reduceTimerPrecision", false]
+ ],
+ tests: gDecodeSuspendTests,
+ runTest
+ });
+</script> \ No newline at end of file
diff --git a/dom/media/test/test_background_video_suspend_ends.html b/dom/media/test/test_background_video_suspend_ends.html
new file mode 100644
index 0000000000..c16ffff290
--- /dev/null
+++ b/dom/media/test/test_background_video_suspend_ends.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test Background Suspended Video Fires 'ended' Event</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="manifest.js"></script>
+<script src="background_video.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css" />
+<script type="text/javascript">
+ "use strict";
+
+ PARALLEL_TESTS = 1;
+
+ var manager = new MediaTestManager;
+
+ async function runTest(test, token) {
+ let video = appendVideoToDoc(test.name, token);
+ manager.started(token);
+
+ // This test checks that 'ended' event is received for videos with
+ // suspended video decoding. This is important for looping video logic
+ // handling in HTMLMediaElement.
+
+ let ended = nextVideoEnded(video);
+ let suspends = nextVideoSuspends(video);
+
+ Log(token, "Start playing");
+ video.play();
+
+ Log(token, "Set hidden");
+ video.setVisible(false);
+
+ Log(token, "Waiting for video suspend");
+ await suspends;
+
+ Log(token, "Waiting for ended");
+ await ended;
+
+ ok(video.currentTime >= video.duration, 'current time approximates duration.');
+
+ manager.finished(token);
+ }
+
+ startTest({
+ desc: "Test Background Suspended Video Fires 'ended' Event",
+ prefs: [
+ ["media.test.video-suspend", true],
+ ["media.suspend-bkgnd-video.enabled", true],
+ // User a short delay to ensure video decode suspend happens before end
+ // of video.
+ ["media.suspend-bkgnd-video.delay-ms", 1000]
+ ],
+ tests: gDecodeSuspendTests,
+ runTest
+ });
+</script> \ No newline at end of file
diff --git a/dom/media/test/test_background_video_suspend_ready_state.html b/dom/media/test/test_background_video_suspend_ready_state.html
new file mode 100644
index 0000000000..af5a4811ef
--- /dev/null
+++ b/dom/media/test/test_background_video_suspend_ready_state.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Check element's ready state while suspending video decoding</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="manifest.js"></script>
+<script src="background_video.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script type="application/javascript">
+
+/**
+ * This test is used to ensure that the media element's ready state won't be
+ * incorrectly changed to `HAVE_METADATA` or lower when seeking happens on a
+ * null decoder that is caused by suspending video decoding.
+ */
+add_task(async function setupTestingPref() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.test.video-suspend", true],
+ ["media.suspend-bkgnd-video.enabled", true],
+ ["media.suspend-bkgnd-video.delay-ms", 0],
+ ],
+ });
+});
+
+add_task(async function testReadyStateWhenSuspendingVideoDecoding() {
+ const video = await createPlayingVideo();
+ assertVideoReadyStateIsSufficientForPlaying(video);
+ await suspendVideoDecoding(video);
+ await seekVideo(video);
+ assertVideoReadyStateIsSufficientForPlaying(video);
+});
+
+async function createPlayingVideo() {
+ let video = document.createElement("video");
+ video.src = "gizmo.mp4";
+ video.controls = true;
+ video.loop = true;
+ document.body.appendChild(video);
+ ok(await video.play().then(_=>true, _=>false), "video started playing");
+ return video;
+}
+
+function assertVideoReadyStateIsSufficientForPlaying(video) {
+ ok(video.readyState > HTMLMediaElement.HAVE_METADATA,
+ `Ready state ${video.readyState} is suffient for playing`);
+}
+
+async function suspendVideoDecoding(video) {
+ video.setVisible(false);
+ nextVideoSuspends(video);
+ info(`suspended video decoding`);
+ // Because the suspend event we received can only indicate MDSM finishes the
+ // setup of suspending decoding, but the actual setting the null decoder
+ // happens on MediaFormatReader where we are not able to observe the behavior
+ // via event. Instread of implementing something in MFR to propagate the event
+ // or changing the timing of current suspend event, which might increase the
+ // maintenance effect, Here choose to use a simple workaround to wait for a
+ // short while to ensure the decoding has been switched to the null decoder.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 2000));
+}
+
+async function seekVideo(video) {
+ video.currentTime = 0;
+ ok(await new Promise(r => video.onseeked = r).then(_=>true, _=>false),
+ `seeking completed`);
+}
+
+</script>
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/media/test/test_background_video_tainted_by_capturestream.html b/dom/media/test/test_background_video_tainted_by_capturestream.html
new file mode 100644
index 0000000000..5cbb40f3f7
--- /dev/null
+++ b/dom/media/test/test_background_video_tainted_by_capturestream.html
@@ -0,0 +1,46 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test Background Video Is Tainted By captureStream</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="manifest.js"></script>
+<script src="background_video.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<script type="text/javascript">
+"use strict";
+
+var manager = new MediaTestManager;
+
+function captureVideoAsStream(v) {
+ v.mozCaptureStream();
+}
+
+startTest({
+ desc: 'Test Background Video Is Tainted By captureStream',
+ prefs: [
+ [ "media.test.video-suspend", true ],
+ [ "media.suspend-bkgnd-video.enabled", true ],
+ [ "media.suspend-bkgnd-video.delay-ms", 1000 ]
+ ],
+ tests: gDecodeSuspendTests,
+ runTest: (test, token) => {
+ ok(true, `${test.name}`);
+ let v = appendVideoToDoc(test.name, token);
+ manager.started(token);
+
+ waitUntilPlaying(v)
+ .then(() => {
+ captureVideoAsStream(v);
+ ok(v.hasSuspendTaint(), "Video is tainted after captured");
+ return checkVideoDoesntSuspend(v);
+ })
+ .then(() => {
+ ok(true, 'Video ended before decode was suspended');
+ manager.finished(token);
+ })
+ .catch((e) => {
+ ok(false, 'Test failed: ' + e.toString());
+ manager.finished(token);
+ });
+ }
+});
+</script>
diff --git a/dom/media/test/test_background_video_tainted_by_createimagebitmap.html b/dom/media/test/test_background_video_tainted_by_createimagebitmap.html
new file mode 100644
index 0000000000..cd884f9144
--- /dev/null
+++ b/dom/media/test/test_background_video_tainted_by_createimagebitmap.html
@@ -0,0 +1,42 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test Background Video Is Tainted By createImageBitmap</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="manifest.js"></script>
+<script src="background_video.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<script type="text/javascript">
+"use strict";
+
+var manager = new MediaTestManager;
+
+startTest({
+ desc: 'Test Background Video Is Tainted By createImageBitmap',
+ prefs: [
+ [ "media.test.video-suspend", true ],
+ [ "media.suspend-bkgnd-video.enabled", true ],
+ [ "media.suspend-bkgnd-video.delay-ms", 1000 ]
+ ],
+ tests: gDecodeSuspendTests,
+ runTest: (test, token) => {
+ ok(true, `${test.name}`);
+ let v = appendVideoToDoc(test.name, token);
+ manager.started(token);
+
+ waitUntilPlaying(v)
+ .then(() => createImageBitmap(v))
+ .then(() => {
+ ok(v.hasSuspendTaint(), "Video is tainted after drawing to canvas");
+ return checkVideoDoesntSuspend(v);
+ })
+ .then(() => {
+ ok(true, 'Video ended before decode was suspended');
+ manager.finished(token);
+ })
+ .catch((e) => {
+ ok(false, 'Test failed: ' + e.toString());
+ manager.finished(token);
+ });
+ }
+});
+</script> \ No newline at end of file
diff --git a/dom/media/test/test_background_video_tainted_by_drawimage.html b/dom/media/test/test_background_video_tainted_by_drawimage.html
new file mode 100644
index 0000000000..d5b8c75f3d
--- /dev/null
+++ b/dom/media/test/test_background_video_tainted_by_drawimage.html
@@ -0,0 +1,58 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>Test Background Video Is Tainted By drawImage</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script src="manifest.js"></script>
+<script src="background_video.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+<script type="text/javascript">
+"use strict";
+
+var manager = new MediaTestManager;
+
+function drawVideoToCanvas(v) {
+ let w = v.width,
+ h = v.height,
+ c = document.createElement('canvas');
+ c.width = w;
+ c.height = h;
+ document.body.appendChild(c);
+
+ let gfx = c.getContext('2d');
+ if (!gfx) {
+ throw Error("Unable to obtain context '2d' from canvas");
+ }
+
+ gfx.drawImage(v, 0, 0, w, h);
+}
+
+startTest({
+ desc: 'Test Background Video Is Tainted By drawImage',
+ prefs: [
+ [ "media.test.video-suspend", true ],
+ [ "media.suspend-bkgnd-video.enabled", true ],
+ [ "media.suspend-bkgnd-video.delay-ms", 1000 ]
+ ],
+ tests: gDecodeSuspendTests,
+ runTest: (test, token) => {
+ ok(true, `${test.name}`);
+ let v = appendVideoToDoc(test.name, token);
+ manager.started(token);
+
+ waitUntilPlaying(v)
+ .then(() => {
+ drawVideoToCanvas(v);
+ ok(v.hasSuspendTaint(), "Video is tainted after drawing to canvas");
+ return checkVideoDoesntSuspend(v);
+ })
+ .then(() => {
+ ok(true, 'Video ended before decode was suspended');
+ manager.finished(token);
+ })
+ .catch((e) => {
+ ok(false, 'Test failed: ' + e.toString());
+ manager.finished(token);
+ });
+ }
+});
+</script> \ No newline at end of file
diff --git a/dom/media/test/test_buffered.html b/dom/media/test/test_buffered.html
new file mode 100644
index 0000000000..86d8eec28a
--- /dev/null
+++ b/dom/media/test/test_buffered.html
@@ -0,0 +1,117 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=462957
+-->
+<head>
+ <title>Test for Bug 462957</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=462957">Mozilla Bug 462957</a>
+
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+// Test for Bug 462957; HTMLMediaElement.buffered.
+
+var manager = new MediaTestManager;
+
+function testBuffered(e) {
+ var v = e.target;
+
+ // The whole media should be buffered...
+ var b = v.buffered;
+ is(b.length, 1, v._name + ": Should be buffered in one range");
+ is(b.start(0), 0, v._name + ": First range start should be media start");
+ ok(Math.abs(b.end(0) - v.duration) < 0.1, v._name + ": First range end should be media end");
+
+ // Ensure INDEX_SIZE_ERR is thrown when we access outside the range
+ var caught = false;
+ try {
+ b.start(-1);
+ } catch (ex) {
+ caught = ex.name == "IndexSizeError" && ex.code == DOMException.INDEX_SIZE_ERR;
+ }
+ is(caught, true, v._name + ": Should throw INDEX_SIZE_ERR on under start bounds range");
+
+ caught = false;
+ try {
+ b.end(-1);
+ } catch (ex) {
+ caught = ex.name == "IndexSizeError" && ex.code == DOMException.INDEX_SIZE_ERR;
+ }
+ is(caught, true, v._name + ": Should throw INDEX_SIZE_ERR on under end bounds range");
+
+ caught = false;
+ try {
+ b.start(b.length);
+ } catch (ex) {
+ caught = ex.name == "IndexSizeError" && ex.code == DOMException.INDEX_SIZE_ERR;
+ }
+ is(caught, true, v._name + ": Should throw INDEX_SIZE_ERR on over start bounds range");
+
+ caught = false;
+ try {
+ b.end(b.length);
+ } catch (ex) {
+ caught = ex.name == "IndexSizeError" && ex.code == DOMException.INDEX_SIZE_ERR;
+ }
+ is(caught, true, v._name + ": Should throw INDEX_SIZE_ERR on over end bounds range");
+
+ removeNodeAndSource(v);
+ manager.finished(v._token);
+}
+
+function fetch(url, fetched_callback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", url, true);
+ xhr.responseType = "blob";
+
+ var loaded = function (event) {
+ if (xhr.status == 200 || xhr.status == 206) {
+ ok(true, `${url}: Fetch succeeded, status=${xhr.status}`);
+ // Request fulfilled. Note sometimes we get 206... Presumably because either
+ // httpd.js or Necko cached the result.
+ fetched_callback(window.URL.createObjectURL(xhr.response));
+ } else {
+ ok(false, `${url}: Fetch failed, headers=${xhr.getAllResponseHeaders()}`);
+ }
+ };
+
+ xhr.addEventListener("load", loaded);
+ xhr.send();
+}
+
+function startTest(test, token) {
+ // Fetch the media resource using XHR so we can be sure the entire
+ // resource is loaded before we test buffered ranges. This ensures
+ // we have deterministic behaviour.
+ var onfetched = function(uri) {
+ var v = document.createElement('video');
+ v._token = token;
+ v.src = uri;
+ v._name = test.name;
+ v._test = test;
+ v.addEventListener("loadeddata", testBuffered, {once: true});
+ document.body.appendChild(v);
+ };
+
+ manager.started(token);
+ fetch(test.name, onfetched);
+}
+
+// Note: No need to set media test prefs, since we're using XHR to fetch
+// media data.
+manager.runTests(gSeekTests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_bug1113600.html b/dom/media/test/test_bug1113600.html
new file mode 100644
index 0000000000..37a9cb0adb
--- /dev/null
+++ b/dom/media/test/test_bug1113600.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test that a video element captured to a stream mid-playback can be played to the end</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+PARALLEL_TESTS = 1;
+SimpleTest.requestCompleteLog();
+var manager = new MediaTestManager;
+
+function startTest(test, token) {
+ var v = document.createElement('video');
+ v.style = "background-color:#aca;";
+ v.width = 160;
+ v.height = 120;
+
+ manager.started(token);
+
+ v.src = test.name;
+
+ v.ontimeupdate = function() {
+ if (v.currentTime < test.duration / 4) {
+ // Allow some time to pass before starting the capture.
+ return;
+ }
+ v.ontimeupdate = null;
+ v.mozCaptureStreamUntilEnded();
+ info(test.name + " capture started at " + v.currentTime + ". Duration=" + test.duration);
+ };
+
+ v.onended = function() {
+ ok(true, test.name + " ended");
+ removeNodeAndSource(v);
+ manager.finished(token);
+ };
+
+ document.body.appendChild(v);
+ v.play();
+}
+
+manager.runTests(gSmallTests, startTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_bug1120222.html b/dom/media/test/test_bug1120222.html
new file mode 100644
index 0000000000..69027bc2e3
--- /dev/null
+++ b/dom/media/test/test_bug1120222.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1120222
+-->
+<head>
+ <title>Test for Bug 1120222</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var manager = new MediaTestManager;
+
+let count = 0;
+
+function startTest(test, token) {
+ manager.started(token);
+ var elemType = /^audio/.test(test.type) ? "audio" : "video";
+ var v = document.createElement(elemType);
+ v.token = token;
+ v.src = test.name;
+
+ v.addEventListener("play", (e) => {
+ document.title = `Test Title ${count++}`;
+ ok(true, "changed title while media was playing")
+ removeNodeAndSource(e.target);
+ manager.finished(e.target.token);
+ });
+
+ document.body.appendChild(v);
+ v.play();
+}
+
+manager.runTests(gSmallTests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_bug1242338.html b/dom/media/test/test_bug1242338.html
new file mode 100644
index 0000000000..7b72153a6e
--- /dev/null
+++ b/dom/media/test/test_bug1242338.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test Bug 1242338</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+function startTest(test, token) {
+ var video = document.createElement('video');
+ video.preload = "metadata";
+ video.token = token;
+
+ var handler = {
+ "ontimeout": function() {
+ Log(token, "timed out");
+ }
+ };
+ manager.started(token, handler);
+
+ video.src = test.name;
+ video.name = test.name;
+
+ function finish() {
+ video.finished = true;
+ video.removeEventListener("loadedmetadata", onLoadedmetadata);
+ video.removeEventListener("ended", onEnded);
+ removeNodeAndSource(video);
+ manager.finished(video.token);
+ }
+
+ function onLoadedmetadata() {
+ // seek to the media's duration
+ var duration = video.duration;
+ console.log("onloadedmetadata(), duration = " + duration);
+ video.currentTime = duration;
+ }
+
+ function onEnded() {
+ ok(video.ended, test.name + " checking playback has ended");
+ ok(!video.finished, test.name + " shouldn't be finished");
+ ok(!video.seenEnded, test.name + " shouldn't be ended");
+ video.seenEnded = true;
+
+ ok(true, "Seeking to the duration triggers ended event");
+ finish();
+ }
+
+ video.addEventListener("loadedmetadata", onLoadedmetadata);
+ video.addEventListener("ended", onEnded);
+
+ document.body.appendChild(video);
+}
+
+manager.runTests(gSeekTests, startTest);
+
+</script>
+</pre>
+</body>
+</html> \ No newline at end of file
diff --git a/dom/media/test/test_bug1248229.html b/dom/media/test/test_bug1248229.html
new file mode 100644
index 0000000000..3165795622
--- /dev/null
+++ b/dom/media/test/test_bug1248229.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test garbage collection of captured stream (bug 1248229)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body onload="doTest()">
+<video id="v" src="black100x100-aspect3to2.ogv"></video>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+function doTest() {
+ /* global v */
+ window.oak = v.mozCaptureStreamUntilEnded();
+ v.mozCaptureStreamUntilEnded();
+ v.play();
+
+ v.onended = function() {
+ info("Got ended.");
+ v.onended = null;
+ SpecialPowers.exactGC(function() {
+ info("GC completed.");
+ v.play();
+ SimpleTest.finish();
+ });
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_bug1431810_opus_downmix_to_mono.html b/dom/media/test/test_bug1431810_opus_downmix_to_mono.html
new file mode 100644
index 0000000000..647ddf0489
--- /dev/null
+++ b/dom/media/test/test_bug1431810_opus_downmix_to_mono.html
@@ -0,0 +1,139 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: disable phase inversion in opus decoder</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<audio preload=none id="a" controls></audio>
+<audio preload=none id="b" controls></audio>
+<script class="testbody" type="text/javascript">
+/*
+ This test makes use of an (stereo) opus file with phase inversion of 180 degrees (right = -left => right + left = 0).
+ Firstly, the phase inversion is verified on a normal stereo playback.
+ Secondly, mono playback is forced which results in the phase inversion being disabled (Bug 1431810).
+*/
+SimpleTest.waitForExplicitFinish();
+
+/* global a, b */
+
+function areChannelsInverted(b1, b2) {
+ for (var i = 0; i < b1.length; i++) {
+ if (Math.abs(b1[i] + b2[i]) > 9e-2) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function areChannelsEqual(b1, b2) {
+ for (var i = 0; i < b1.length; i++) {
+ if (Math.abs(b1[i] - b2[i]) > 9e-3) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function isSilent(b) {
+ for (var i = 0; i < b.length; i++) {
+ if (b[i] != 0.0) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function mediaElementWithPhaseInversion(audioContext, mediaElement, success) {
+ let audio_source = audioContext.createMediaElementSource(mediaElement);
+ let script_processor = audioContext.createScriptProcessor();
+ audio_source.connect(script_processor);
+
+ mediaElement.onplay = () => {
+ script_processor.onaudioprocess = (e) => {
+ let right = e.inputBuffer.getChannelData(0);
+ let left = e.inputBuffer.getChannelData(1);
+
+ // This is leading or trailing silence
+ // produced by ScriptProcessor.
+ if (isSilent(right) && isSilent(left)) {
+ return;
+ }
+
+ ok(areChannelsInverted(right, left), "Channels must be inverted");
+ }
+ }
+
+ mediaElement.onended = () => {
+ ok(true, "End of file.");
+ mediaElement.onended = null;
+ script_processor.onaudioprocess = null;
+ success();
+ }
+
+ mediaElement.src = "test-stereo-phase-inversion-180.opus";
+ // Normal playback channels will by inverted
+ mediaElement.play();
+}
+
+function mediaElementWithPhaseInversionDisabled(audioContext, mediaElement, success) {
+ let audio_source = audioContext.createMediaElementSource(mediaElement);
+ let script_processor = audioContext.createScriptProcessor();
+ audio_source.connect(script_processor);
+
+ mediaElement.onplay = () => {
+ script_processor.onaudioprocess = (e) => {
+ let right = e.inputBuffer.getChannelData(0);
+ let left = e.inputBuffer.getChannelData(1);
+
+ // This is leading or trailing silence
+ // produced by ScriptProcessor.
+ if (isSilent(right) && isSilent(left)) {
+ return;
+ }
+
+ ok(!areChannelsInverted(right, left), "Channels must not be inverted");
+ ok(areChannelsEqual(right, left), "Channels must be equal");
+ }
+ }
+
+ mediaElement.onended = () => {
+ ok(true, "End of file.");
+ mediaElement.onended = null;
+ script_processor.onaudioprocess = null;
+ success();
+ }
+
+ mediaElement.src = "test-stereo-phase-inversion-180.opus";
+
+ // Downmix to mono will force to disable opus phase inversion
+ SpecialPowers.pushPrefEnv({"set": [["accessibility.monoaudio.enable", true]]})
+ .then(() => {
+ mediaElement.play();
+ });
+}
+
+let ac = new AudioContext();
+
+function testPhaseInversion(mediaElement) {
+ return new Promise((accept, reject) => {
+ mediaElementWithPhaseInversion(ac, a, accept);
+ });
+}
+
+function testPhaseInversionDisabled(mediaElement) {
+ return new Promise((accept, reject) => {
+ mediaElementWithPhaseInversionDisabled(ac, b, accept);
+ });
+}
+
+// Start testing
+testPhaseInversion(a)
+.then( () => testPhaseInversionDisabled(b) )
+.then( () => SimpleTest.finish() )
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_bug1512958.html b/dom/media/test/test_bug1512958.html
new file mode 100644
index 0000000000..2513515542
--- /dev/null
+++ b/dom/media/test/test_bug1512958.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test that pausing and resuming a captured media element with audio doesn't stall</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<audio id="a"></audio>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+function dumpEvent({target, type}) {
+ info(`${target.name} GOT EVENT ${type} currentTime=${target.currentTime} ` +
+ `paused=${target.paused} ended=${target.ended} ` +
+ `readyState=${target.readyState}`);
+}
+
+function wait(ms) {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+const a = document.getElementById('a');
+
+const events = ["timeupdate", "seeking", "seeked", "ended", "playing", "pause"];
+for (let ev of events) {
+ a.addEventListener(ev, dumpEvent);
+}
+
+(async () => {
+ try {
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.requestFlakyTimeout("Timeouts for shortcutting test-timeout");
+
+ const test = getPlayableAudio(gTrackTests.filter(t => t.duration > 2));
+ if (!test) {
+ todo(false, "No playable audio");
+ return;
+ }
+
+ // Start playing and capture
+ a.src = test.name;
+ a.name = test.name;
+ const ac = new AudioContext();
+ ac.createMediaElementSource(a);
+ a.play();
+ do {
+ await new Promise(r => a.ontimeupdate = r);
+ } while(a.currentTime == 0)
+
+ // Pause to trigger recreating tracks in DecodedStream
+ a.pause();
+ await new Promise(r => a.onpause = r);
+
+ // Resuming should now work. Bug 1512958 would cause a stall because the
+ // original track wasn't ended and we'd block on it.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1512958#c5
+ a.play();
+ await new Promise(r => a.onplaying = r);
+ a.currentTime = test.duration - 1;
+ await Promise.race([
+ new Promise(res => a.onended = res),
+ wait(30000).then(() => Promise.reject(new Error("Timeout"))),
+ ]);
+ } catch(e) {
+ ok(false, e);
+ } finally {
+ SimpleTest.finish();
+ }
+})();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_bug1553262.html b/dom/media/test/test_bug1553262.html
new file mode 100644
index 0000000000..19b937f7fe
--- /dev/null
+++ b/dom/media/test/test_bug1553262.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+<title>Bug 1553262</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<script>
+ const canvas = document.createElement('canvas')
+ const context = canvas.getContext('2d')
+
+ const video = document.createElement('video')
+ const source = new AudioContext().createMediaElementSource(video)
+ const stream = canvas.captureStream()
+
+ const xhr = new XMLHttpRequest()
+
+ context.rect(256, -32768, 16, 16)
+ video.srcObject = stream
+ for (let i = 0; i < 16; i++) {
+ xhr.open('P', '', false)
+ xhr.send()
+ }
+
+ video.srcObject = stream
+ ok(true, "success")
+</script>
+</body>
+</html>
diff --git a/dom/media/test/test_bug448534.html b/dom/media/test/test_bug448534.html
new file mode 100644
index 0000000000..ed1135d85a
--- /dev/null
+++ b/dom/media/test/test_bug448534.html
@@ -0,0 +1,71 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=448534
+-->
+
+<head>
+ <title>Test for Bug 448534</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=448535">Mozilla Bug 448534</a>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+function loaded(event) {
+ var v = event.target;
+ info(v.token + ": event=" + event.type);
+ if (v._finished)
+ return;
+ v.play();
+}
+
+function started(event) {
+ var v = event.target;
+ info(v.token + ": event=" + event.type);
+ // For a short file, it could reach the end before 'play' received. We will
+ // skip the test for 'paused' would be true when ended.
+ if (v._finished || v.ended)
+ return;
+ ok(!v.paused, v.token + ": Video should not be paused while playing");
+ v.remove();
+ v._played = true;
+}
+
+function stopped(event) {
+ var v = event.target;
+ info(v.token + ": event=" + event.type);
+ if (v._finished)
+ return;
+ v._finished = true;
+ ok(v.paused, v.token + ": Video should be paused after removing from the Document");
+ removeNodeAndSource(v);
+ manager.finished(v.token);
+}
+
+
+function startTest(test, token) {
+ var v = document.createElement('video');
+ v.preload = "metadata";
+ v.token = token;
+ manager.started(token);
+ v.src = test.name;
+ v._played = false;
+ v._finished = false;
+ v.addEventListener("loadedmetadata", loaded);
+ v.addEventListener("play", started);
+ v.addEventListener("pause", stopped);
+ document.body.appendChild(v);
+}
+
+manager.runTests(gSmallTests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_bug463162.xhtml b/dom/media/test/test_bug463162.xhtml
new file mode 100644
index 0000000000..a84b3488d1
--- /dev/null
+++ b/dom/media/test/test_bug463162.xhtml
@@ -0,0 +1,78 @@
+<html xmlns="http://www.w3.org/1999/xhtml"
+ xmlns:html="http://www.w3.org/1999/xhtml"
+ xmlns:svg="http://www.w3.org/2000/svg">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=463162
+-->
+<head>
+ <title>Test for Bug 463162</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=463162">Mozilla Bug 463162</a>
+
+<script class="testbody" type="text/javascript">
+<![CDATA[
+
+var gExpectedResult = {
+ 'a1' : 'error',
+ 'a2' : 'loaded',
+ 'a3' : 'loaded',
+ 'a4' : 'error',
+};
+
+var gResultCount = 0;
+
+function onError(event, id) {
+ is('error', gExpectedResult[id], 'unexpected error loading ' + id);
+ gResultCount++;
+ dump('error('+id+') expected ' + gExpectedResult[id] + ' gResultCount=' + gResultCount + '\n');
+ if (gResultCount == 4)
+ SimpleTest.finish();
+}
+
+function onMetaData(id) {
+ is('loaded', gExpectedResult[id], 'unexpected loadedmetadata loading ' + id);
+ gResultCount++;
+ dump('onMetaData('+id+') expected ' + gExpectedResult[id] + ' gResultCount=' + gResultCount + '\n');
+ if (gResultCount == 4)
+ SimpleTest.finish();
+}
+
+]]>
+</script>
+
+<video id="a1" preload="metadata" onloadedmetadata="onMetaData('a1');"><sauce/><source type="bad" src="404" onerror="onError(event, 'a1');"/></video>
+<video id="a2" preload="metadata" onloadedmetadata="onMetaData('a2');"><source onerror="onError(event, 'a2');"/></video>
+<video id="a3" preload="metadata" onloadedmetadata="onMetaData('a3');"><html:source onerror="onError(event, 'a3');"/></video>
+<video id="a4" preload="metadata" onloadedmetadata="onMetaData('a4');"><svg:source/><source onerror="onError(event, 'a4');" type="bad" src="404"/></video>
+
+<script class="testbody" type="text/javascript">
+<![CDATA[
+
+function setSource(id, res) {
+ var v = document.getElementById(id);
+ v.firstChild.src = res.name;
+ v.firstChild.type = res.type;
+}
+
+var t = getPlayableVideo(gSmallTests);
+
+setSource('a1', t);
+setSource('a2', t);
+setSource('a3', t);
+setSource('a4', t);
+
+SimpleTest.waitForExplicitFinish();
+
+]]>
+</script>
+
+<pre id="test">
+
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_bug465498.html b/dom/media/test/test_bug465498.html
new file mode 100644
index 0000000000..157a972e77
--- /dev/null
+++ b/dom/media/test/test_bug465498.html
@@ -0,0 +1,83 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: Bug 465498 - Seeking after playback ended</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=465498">Mozilla Bug 465498</a>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+function startTest(e) {
+ var v = e.target;
+ info(v._name + " loadedmetadata");
+ e.target.play();
+}
+
+function playbackEnded(e) {
+ var v = e.target;
+ info(v._name + " ended");
+ if (v._finished)
+ return;
+ ok(v.currentTime >= v.duration - 0.1 && v.currentTime <= v.duration + 0.1,
+ "Checking currentTime at end: " + v.currentTime + " for " + v._name);
+ ok(v.ended, "Checking playback has ended for " + v._name);
+ v.pause();
+ v.currentTime = 0;
+ ok(!v.ended, "Checking ended is no longer true for " + v._name);
+ v._seeked = true;
+}
+
+function seekEnded(e) {
+ var v = e.target;
+ info(v._name + " seeked");
+ if (v._finished)
+ return;
+ ok(v.currentTime == 0, "Checking currentTime after seek: " +
+ v.currentTime + " for " + v._name);
+ ok(!v.ended, "Checking ended is false for " + v._name);
+ v._finished = true;
+ removeNodeAndSource(v);
+ manager.finished(v.token);
+}
+
+function seeking(e) {
+ var v = e.target;
+ info(v._name + " seeking");
+}
+
+function initTest(test, token) {
+ var type = getMajorMimeType(test.type);
+ var v = document.createElement(type);
+ if (!v.canPlayType(test.type))
+ return;
+ v.preload = "metadata";
+ v.token = token;
+ manager.started(token);
+ v._name = test.name;
+
+ var s = document.createElement("source");
+ s.type = test.type;
+ s.src = test.name;
+ v.appendChild(s);
+
+ v._seeked = false;
+ v._finished = false;
+ v.addEventListener("loadedmetadata", startTest);
+ v.addEventListener("ended", playbackEnded);
+ v.addEventListener("seeked", seekEnded);
+ v.addEventListener("seeking", seeking);
+ document.body.appendChild(v);
+}
+
+manager.runTests(gSmallTests, initTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_bug495145.html b/dom/media/test/test_bug495145.html
new file mode 100644
index 0000000000..3686e10270
--- /dev/null
+++ b/dom/media/test/test_bug495145.html
@@ -0,0 +1,95 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=495145
+-->
+
+<head>
+ <title>Bug 495145 - pausing while ended shouldn't cause problems</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=495145">Mozilla Bug 495145</a>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+function start(e) {
+ e.target.play();
+}
+
+function ended1(e) {
+ var v = e.target;
+ if (v._finished)
+ return;
+
+ ++v._endCount;
+ if (v._endCount == 2) {
+ ok(true, "Playing after pause while ended works for " + v._name);
+ v._finished = true;
+ v.removeEventListener("loadedmetadata", start);
+ v.removeEventListener("ended", ended1);
+ removeNodeAndSource(v);
+ manager.finished(v.token);
+ return;
+ }
+
+ v.pause();
+ v.play();
+}
+
+function ended2(e) {
+ var v = e.target;
+ if (v._finished)
+ return;
+
+ v.pause();
+ v.currentTime = 0;
+}
+
+function seeked2(e) {
+ var v = e.target;
+ if (v._finished)
+ return;
+
+ ok(v.paused, "Paused after seek after pause while ended for " + v._name);
+ v._finished = true;
+ v.removeEventListener("loadedmetadata", start);
+ v.removeEventListener("ended", ended2);
+ v.removeEventListener("seeked", seeked2);
+ removeNodeAndSource(v);
+ manager.finished(v.token);
+}
+
+function createVideo(test, x, token) {
+ var v = document.createElement('video');
+ v.preload = "metadata";
+ v.token = token;
+ manager.started(token);
+ v.src = test.name;
+ v._name = test.name + "#" + x;
+ v._endCount = 0;
+ v._finished = false;
+ v.addEventListener("loadedmetadata", start);
+ v.addEventListener("ended", x == 1 ? ended1 : ended2);
+ if (x == 2)
+ v.addEventListener("seeked", seeked2);
+ document.body.appendChild(v);
+}
+
+function startTest(test, token) {
+ createVideo(test, 1, token + "a");
+ createVideo(test, 2, token + "b");
+}
+
+manager.runTests(gSmallTests, startTest);
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_bug495300.html b/dom/media/test/test_bug495300.html
new file mode 100644
index 0000000000..bf3b921402
--- /dev/null
+++ b/dom/media/test/test_bug495300.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=495300
+-->
+
+<head>
+ <title>Bug 495300 - seeking to the end should behave as "ended"</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=495300">Mozilla Bug 495300</a>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+function filename(uri) {
+ return uri.substr(uri.lastIndexOf("/")+1);
+}
+
+function mediaEnded(event) {
+ ok(true, "Got expected 'ended' event: " + filename(event.target.currentSrc));
+
+ if (event.target._expectedDuration)
+ ok(Math.abs(event.target.currentTime - event.target._expectedDuration) < 0.1,
+ "currentTime equals duration: " + filename(event.target.currentSrc));
+
+ event.target.removeEventListener("ended", mediaEnded);
+ manager.finished(event.target.token);
+ removeNodeAndSource(event.target);
+}
+
+function mediaLoadedmetadata(event) {
+ event.target.currentTime = event.target.duration;
+ event.target.removeEventListener("loadedmetadata", mediaLoadedmetadata);
+}
+
+function startTest(test, token) {
+ var elemType = /^audio/.test(test.type) ? "audio" : "video";
+ var v1 = document.createElement(elemType);
+ v1.preload = "auto";
+
+ v1.src = test.name;
+ if (test.duration) {
+ v1._expectedDuration = test.duration;
+ }
+ v1.addEventListener("loadedmetadata", mediaLoadedmetadata);
+ v1.addEventListener("ended", mediaEnded);
+ v1.load();
+
+ v1.token = token;
+ manager.started(token);
+}
+
+manager.runTests(gSeekTests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_bug654550.html b/dom/media/test/test_bug654550.html
new file mode 100644
index 0000000000..f57afae2b4
--- /dev/null
+++ b/dom/media/test/test_bug654550.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=654550
+-->
+
+<head>
+ <title>Test for Bug 654550</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=654550">Mozilla Bug 654550</a>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+ /* Test for Bug 654550 */
+
+ // Parallel test must be disabled for media.video_stats.enabled is a global setting
+ // to prevent the setting from changing unexpectedly in the middle of the test.
+ PARALLEL_TESTS = 1;
+ SimpleTest.waitForExplicitFinish();
+ var manager = new MediaTestManager;
+
+ function checkStats(v, aShouldBeEnabled) {
+ if (aShouldBeEnabled) {
+ ok(v.mozParsedFrames != 0,
+ "At least one value should be different from 0 if stats are enabled");
+ } else {
+ ok(!v.mozParsedFrames,
+ "mozParsedFrames should be 0 if stats are disabled");
+ ok(!v.mozDecodedFrames,
+ "mozDecodedFrames should be 0 if stats are disabled");
+ ok(!v.mozPresentedFrames,
+ "mozPresentedFrames should be 0 if stats are disabled");
+ ok(!v.mozPaintedFrames,
+ "mozPaintedFrames should be 0 if stats are disabled");
+ }
+
+ }
+
+ function ontimeupdate_statsEnabled(event) {
+ var v = event.target;
+ v.removeEventListener('timeupdate', ontimeupdate_statsEnabled);
+ checkStats(v, true);
+ SpecialPowers.popPrefEnv(
+ function() {
+ v.addEventListener("timeupdate", ontimeupdate_statsDisabled);
+ });
+ }
+
+ function ontimeupdate_statsDisabled(event) {
+ var v = event.target;
+ v.removeEventListener('timeupdate', ontimeupdate_statsDisabled);
+ checkStats(v, false);
+ removeNodeAndSource(v);
+ manager.finished(v.token);
+ }
+
+ function startTest(test, token) {
+ var v = document.createElement('video');
+ v.token = token;
+ v.src = test.name;
+ // playback may reach the end before pref is changed for the duration is short
+ // set 'loop' to true to keep playing so that we won't miss 'timeupdate' events
+ v.loop = true;
+ manager.started(token);
+ SpecialPowers.pushPrefEnv({"set": [["media.video_stats.enabled", true]]},
+ function() {
+ v.play();
+ v.addEventListener("timeupdate", ontimeupdate_statsEnabled);
+ });
+ }
+
+ SpecialPowers.pushPrefEnv({"set": [["media.video_stats.enabled", false]]},
+ function() {
+ manager.runTests(gVideoTests, startTest);
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_bug686942.html b/dom/media/test/test_bug686942.html
new file mode 100644
index 0000000000..66b48d69f0
--- /dev/null
+++ b/dom/media/test/test_bug686942.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=686942
+-->
+
+<head>
+ <title>Test for Bug 448534</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=686942">Mozilla Bug 686942</a>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+function onloaded(event) {
+ var v = event.target;
+ v.removeEventListener("loadedmetadata", onloaded);
+ v.currentTime = v.duration;
+
+}
+
+function checkNotPlaying(v) {
+ ok(v.currentTime == 0, "Should not be playing after seek to end and back to beginning");
+ v._finished = true;
+ manager.finished(v.token);
+ removeNodeAndSource(v);
+}
+
+function onseeked(event) {
+ var v = event.target;
+ v.removeEventListener("seeked", onseeked);
+ setTimeout(function() { checkNotPlaying(v); }, 500);
+}
+
+function onended(event) {
+ var v = event.target;
+ v.removeEventListener("ended", onended);
+ if (v._finished)
+ return;
+ v.addEventListener("seeked", onseeked);
+ v.currentTime = 0;
+}
+
+function startTest(test, token) {
+ var v = document.createElement('video');
+ v.preload = "auto";
+ v.token = token;
+ manager.started(token);
+ v.src = test.name;
+ v._played = false;
+ v._finished = false;
+ v.addEventListener("loadedmetadata", onloaded);
+ v.addEventListener("ended", onended);
+
+ document.body.appendChild(v);
+}
+
+manager.runTests(gSmallTests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_bug726904.html b/dom/media/test/test_bug726904.html
new file mode 100644
index 0000000000..bf8e2536ca
--- /dev/null
+++ b/dom/media/test/test_bug726904.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=726904
+-->
+
+<head>
+ <title>Media test: default video size</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body onload="bodyLoaded();">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=726904">Mozilla Bug 726904</a>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+var v1 = document.createElement("video"),
+ v2 = document.createElement("video"),
+ poster = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAGQCAAAAACl1GkQAAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAAALJJREFUeNrtwQENAAAAwqD3T20ON6AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHg0cq4AATRk8BYAAAAASUVORK5CYII",
+ resource = getPlayableVideo(gSmallTests);
+
+function bodyLoaded(){
+ // Note: For DASH, width and height would vary once the video started playing, so
+ // the values would not correlate with those in manifest.js. Since this test has
+ // no playing, this should not affect the result.
+ is(v1.videoWidth, resource.width, "Intrinsic width should match video width");
+ is(v1.videoHeight, resource.height, "Intrinsic height should match video height");
+ is(v2.clientWidth, 400, "clientWidth should be 400");
+ is(v2.clientHeight, 400, "clientHeight should be 400");
+ SimpleTest.finish();
+}
+
+if (resource) {
+ v1.poster = v2.poster = poster;
+
+ v1.src = v2.src = "http://mochi.test:8888/tests/dom/media/test/" + resource.name;
+
+ v1.preload = "auto";
+ v2.preload = "none";
+
+ v1.muted = v2.muted = true;
+
+ document.body.appendChild(v1);
+ document.body.appendChild(v2);
+} else {
+ todo(false, "No types supported");
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_bug874897.html b/dom/media/test/test_bug874897.html
new file mode 100644
index 0000000000..7801487fe3
--- /dev/null
+++ b/dom/media/test/test_bug874897.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=874897
+-->
+
+<head>
+ <title>Test for Bug 874897</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+function loadeddata(e) {
+ var v = e.target;
+ ok(v.readyState >= v.HAVE_CURRENT_DATA,
+ "readyState must be >= HAVE_CURRENT_DATA for " + v._name);
+
+ var canvas = document.createElement("canvas");
+ canvas.width = 210;
+ canvas.height = 120;
+ document.body.appendChild(canvas);
+ var ctx = canvas.getContext("2d");
+ try {
+ ctx.drawImage(v, 0, 0, v.videoWidth, v.videoHeight, 0, 0, canvas.width, canvas.height);
+ ok(true, "Shouldn't throw exception while drawing to canvas from video for " + v._name);
+ } catch (ex) {
+ ok(false, "Shouldn't throw exception while drawing to canvas from video for " + v._name);
+ }
+
+ v._finished = true;
+ v.remove();
+ manager.finished(v.token);
+}
+
+function startTest(test, token) {
+ var type = getMajorMimeType(test.type);
+ if (type != "video")
+ return;
+
+ var v = document.createElement('video');
+ v.token = token;
+ manager.started(token);
+ v.src = test.name;
+ v._name = test.name;
+ v._finished = false;
+ v.autoplay = true;
+ v.style.display = "none";
+ v.addEventListener("loadeddata", loadeddata);
+ document.body.appendChild(v);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}, beginTest);
+function beginTest() {
+ manager.runTests(gAspectRatioTests, startTest);
+}
+
+</script>
+</pre>
+
+</body>
+</html>
diff --git a/dom/media/test/test_bug879717.html b/dom/media/test/test_bug879717.html
new file mode 100644
index 0000000000..c669773d3f
--- /dev/null
+++ b/dom/media/test/test_bug879717.html
@@ -0,0 +1,130 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for bug 879717, check that a video element can be drawn into a canvas at various states of playback</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+var canvas = document.createElement('canvas');
+document.body.appendChild(canvas);
+
+var checkDrawImage = function(eventName, videoElement) {
+ var exception = null;
+ var exceptionName = "nothing";
+ try {
+ var ctx = canvas.getContext('2d');
+ ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
+ } catch (e) {
+ exception = e;
+ exceptionName = e.name;
+ }
+ ok(exception === null,
+ "drawImage shouldn't throw an exception on " + eventName +
+ " of " + videoElement.testName + ", got " + exceptionName);
+};
+
+var checkDrawImageEventHandler = function(ev) {
+ checkDrawImage(ev.type, ev.target);
+};
+var startTest = function(media, token) {
+ manager.started(token);
+
+ // File playback
+ var v1 = document.createElement("video");
+ v1.autoplay = true;
+
+ // Captured file playback
+ var v2 = document.createElement("video");
+
+ // Stream playback
+ var v3 = document.createElement("video");
+ v3.autoplay = true;
+
+ v1.gotLoadeddata = false;
+ v2.gotLoadeddata = false;
+ v3.gotLoadeddata = false;
+
+ v1.testName = "v1 (" + media.name + ")";
+ v2.testName = "v2 (Captured " + media.name + ")";
+ v3.testName = "v3 (Stream of " + media.name + ")";
+
+ checkDrawImage("beforeplay", v1);
+ checkDrawImage("beforeplay", v2);
+ checkDrawImage("beforeplay", v3);
+
+ v1.onloadedmetadata = checkDrawImageEventHandler;
+ v2.onloadedmetadata = checkDrawImageEventHandler;
+ v3.onloadedmetadata = checkDrawImageEventHandler;
+
+ v1.onplay = checkDrawImageEventHandler;
+ v2.onplay = checkDrawImageEventHandler;
+ v3.onplay = checkDrawImageEventHandler;
+
+ function onplaying(ev) {
+ if (!ev.target.gotPlaying) {
+ ev.target.gotPlaying = true;
+ checkDrawImageEventHandler(ev);
+ }
+ }
+ v1.onplaying = onplaying;
+ v2.onplaying = onplaying;
+ v3.onplaying = onplaying;
+
+ var onloadeddata = function(ev) {
+ ev.target.gotLoadeddata = true;
+ checkDrawImageEventHandler(ev);
+ };
+
+ v1.onloadeddata = onloadeddata;
+ v2.onloadeddata = onloadeddata;
+ v3.onloadeddata = onloadeddata;
+
+ var checkFinished = function() {
+ if (!v1.testFinished || !v2.testFinished || !v3.testFinished) {
+ return;
+ }
+
+ ok(v1.gotLoadeddata, v1.testName + " should have gotten the 'loadeddata' event callback");
+ ok(v2.gotLoadeddata, v2.testName + " should have gotten the 'loadeddata' event callback");
+ ok(v3.gotLoadeddata, v3.testName + " should have gotten the 'loadeddata' event callback");
+
+ manager.finished(token);
+ };
+
+ var onended = function(ev) {
+ checkDrawImageEventHandler(ev);
+ removeNodeAndSource(ev.target);
+ ev.target.testFinished = true;
+ checkFinished();
+ };
+
+ v1.onended = onended;
+ v2.onended = onended;
+ v3.onended = onended;
+
+ document.body.appendChild(v1);
+ document.body.appendChild(v2);
+ document.body.appendChild(v3);
+
+ v1.src = media.name;
+ v2.src = media.name;
+ v2.preload = 'metadata';
+
+ v2.addEventListener('loadedmetadata', function () {
+ v3.srcObject = v2.mozCaptureStreamUntilEnded();
+ v2.play();
+ });
+}
+
+manager.runTests(getPlayableVideos(gSmallTests), startTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_bug895305.html b/dom/media/test/test_bug895305.html
new file mode 100644
index 0000000000..56ed3c775c
--- /dev/null
+++ b/dom/media/test/test_bug895305.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=895305
+https://bugzilla.mozilla.org/show_bug.cgi?id=905320
+-->
+<head>
+ <meta charset='utf-8'>
+ <title>Regression test for bug 895305 and 905320 - TextTrack* leaks</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+var audio = document.createElement("audio");
+
+// Check leaking on TextTrackList objects.
+/* global ttl, ttc */
+window.ttl = audio.textTracks;
+ttl.addEventListener("click", function(){});
+
+// Check leaking on VTTCue objects.
+window.ttc = new VTTCue(3, 4, "Test.");
+ttc.addEventListener("click", function() {});
+
+// Check leaking on TextTrack objects.
+audio.addTextTrack("subtitles", "label", "en-CA");
+ttl[0].addEventListener("click", function() {});
+
+ok(true); // Need to have at least one assertion for Mochitest to be happy.
+SimpleTest.finish();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_bug919265.html b/dom/media/test/test_bug919265.html
new file mode 100644
index 0000000000..40eff135e4
--- /dev/null
+++ b/dom/media/test/test_bug919265.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=919265
+-->
+<head>
+ <meta charset='utf-8'>
+ <title>Regression test for bug 919265 - Leak on VTTCue::GetCueAsHTML()</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+// We shouldn't leak upon shutdown.
+(new VTTCue(0, 0, "")).getCueAsHTML();
+
+// We need to assert something for Mochitest to be happy.
+ok(true);
+SimpleTest.finish();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_can_play_type.html b/dom/media/test/test_can_play_type.html
new file mode 100644
index 0000000000..a28d137783
--- /dev/null
+++ b/dom/media/test/test_can_play_type.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=469247
+-->
+<head>
+ <title>Test for Bug 469247</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=469247">Mozill
+a Bug 469247</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<video id="v"></video>
+
+<pre id="test">
+<script type="application/javascript">
+
+var v = document.getElementById('v');
+
+function check(type, expected) {
+ is(v.canPlayType(type), expected, type);
+}
+
+// Invalid types
+check("foo/bar", "");
+check("", "");
+check("!!!", "");
+
+mediaTestCleanup();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_can_play_type_mpeg.html b/dom/media/test/test_can_play_type_mpeg.html
new file mode 100644
index 0000000000..a4b87272c0
--- /dev/null
+++ b/dom/media/test/test_can_play_type_mpeg.html
@@ -0,0 +1,168 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=799315
+-->
+<head>
+ <title>Test for MP4 and MP3 support</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<video id="v"></video>
+
+<pre id="test">
+<script>
+
+function check_mp4(v, enabled) {
+ function check(type, expected) {
+ var ex = enabled ? expected : "";
+ is(v.canPlayType(type), ex, type + "='" + ex + "'");
+ }
+
+ check("video/mp4", "maybe");
+ check("video/x-m4v", "maybe");
+ check("audio/mp4", "maybe");
+ check("audio/x-m4a", "maybe");
+
+ // Not the MIME type that other browsers respond to, so we won't either.
+ check("audio/m4a", "");
+ check("video/m4v", "");
+
+ check("audio/aac", "maybe");
+ check("audio/aacp", "maybe");
+
+ // H.264 Constrained Baseline Profile Level 3.0, AAC-LC
+ check("video/mp4; codecs=\"avc1.42E01E, mp4a.40.2\"", "probably");
+
+ // H.264 Constrained Baseline Profile Level 3.0, mp3
+ check("video/mp4; codecs=\"avc1.42E01E, mp3\"", "probably");
+
+ check("video/mp4; codecs=\"avc1.42001E, mp4a.40.2\"", "probably");
+ check("video/mp4; codecs=\"avc1.58A01E, mp4a.40.2\"", "probably");
+
+ // H.264 Main Profile Level 3.0, AAC-LC
+ check("video/mp4; codecs=\"avc1.4D401E, mp4a.40.2\"", "probably");
+ // H.264 Main Profile Level 3.1, AAC-LC
+ check("video/mp4; codecs=\"avc1.4D401F, mp4a.40.2\"", "probably");
+ // H.264 Main Profile Level 4.0, AAC-LC
+ check("video/mp4; codecs=\"avc1.4D4028, mp4a.40.2\"", "probably");
+ // H.264 High Profile Level 3.0, AAC-LC
+ check("video/mp4; codecs=\"avc1.64001E, mp4a.40.2\"", "probably");
+ // H.264 High Profile Level 3.1, AAC-LC
+ check("video/mp4; codecs=\"avc1.64001F, mp4a.40.2\"", "probably");
+
+ check("video/mp4; codecs=\"avc1.42E01E\"", "probably");
+ check("video/mp4; codecs=\"avc1.42001E\"", "probably");
+ check("video/mp4; codecs=\"avc1.58A01E\"", "probably");
+ check("video/mp4; codecs=\"avc1.4D401E\"", "probably");
+ check("video/mp4; codecs=\"avc1.64001F\"", "probably");
+
+ // AAC-LC
+ check("audio/mp4; codecs=\"mp4a.40.2\"", "probably");
+ check("audio/mp4; codecs=mp4a.40.2", "probably");
+ check("audio/x-m4a; codecs=\"mp4a.40.2\"", "probably");
+ check("audio/x-m4a; codecs=mp4a.40.2", "probably");
+
+ check("audio/mp4; codecs=\"mp4a.40.2,\"", ""); // Invalid codecs string
+
+ // HE-AAC v1
+ check("audio/mp4; codecs=\"mp4a.40.5\"", "probably");
+ check("audio/mp4; codecs=mp4a.40.5", "probably");
+ check("audio/x-m4a; codecs=\"mp4a.40.5\"", "probably");
+ check("audio/x-m4a; codecs=mp4a.40.5", "probably");
+ // HE-AAC v2
+ check("audio/mp4; codecs=\"mp4a.40.29\"", "probably");
+
+ // Opus
+ check("audio/mp4; codecs=\"opus\"", "probably");
+ check("audio/mp4; codecs=opus", "probably");
+
+ // Flac.
+ var haveFlac = getPref("media.flac.enabled");
+ check("audio/mp4; codecs=\"flac\"", haveFlac ? "probably" : "");
+ check("audio/mp4; codecs=flac", haveFlac ? "probably" : "");
+
+ // VP9.
+ [ "video/mp4; codecs=vp9",
+ "video/mp4; codecs=\"vp9\"",
+ "video/mp4; codecs=\"vp9.0\""
+ ].forEach((codec) => {
+ // canPlayType should support VP9 in MP4...
+ check(codec, "probably");
+ if (!IsSupportedAndroid()) {
+ // VP9 codec is disabled on Android devices with no HW decoder. So skip it
+ // on Android for now.
+ ok(MediaSource.isTypeSupported(codec), "VP9 in MP4 should be supported in MSE");
+ }
+ });
+
+ var haveAV1 = getPref("media.av1.enabled");
+ check("video/mp4; codecs=\"av1\"", haveAV1 ? "probably" : "");
+}
+
+function check_mp3(v, enabled) {
+ function check(type, expected) {
+ var ex = enabled ? expected : "";
+ is(v.canPlayType(type), ex, type + "='" + ex + "'");
+ }
+
+ check("audio/mpeg", "maybe");
+ check("audio/mp3", "maybe");
+
+ check("audio/mpeg; codecs=\"mp3\"", "probably");
+ check("audio/mpeg; codecs=mp3", "probably");
+
+ check("audio/mp3; codecs=\"mp3\"", "probably");
+ check("audio/mp3; codecs=mp3", "probably");
+}
+
+function IsMacOS() {
+ return navigator.userAgent.includes("Mac OS");
+}
+
+function IsLinux() {
+ return navigator.userAgent.includes("Linux");
+}
+
+function getPref(name) {
+ return SpecialPowers.getBoolPref(name, false);
+}
+
+function IsSupportedAndroid() {
+ return getAndroidVersion() >= 14;
+}
+
+function IsJellyBeanOrLater() {
+ return getAndroidVersion() >= 16;
+}
+
+var haveMp4 =
+ getPref("media.wmf.enabled") ||
+ IsMacOS() ||
+ (IsSupportedAndroid() &&
+ (IsJellyBeanOrLater() || getPref("media.plugins.enabled"))) ||
+ (IsLinux() && getPref("media.ffmpeg.enabled"));
+
+check_mp4(document.getElementById('v'), haveMp4);
+
+var haveMp3 =
+ getPref("media.wmf.enabled") ||
+ (IsLinux() && getPref("media.ffmpeg.enabled")) ||
+ (IsSupportedAndroid() &&
+ ((IsJellyBeanOrLater() && getPref("media.android-media-codec.enabled")) ||
+ getPref("media.plugins.enabled"))) ||
+ IsMacOS();
+
+check_mp3(document.getElementById('v'), haveMp3);
+
+mediaTestCleanup();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_can_play_type_no_ogg.html b/dom/media/test/test_can_play_type_no_ogg.html
new file mode 100644
index 0000000000..2e63f191d2
--- /dev/null
+++ b/dom/media/test/test_can_play_type_no_ogg.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=469247
+-->
+<head>
+ <title>Test for Bug 469247: Ogg backend disabled</title>
+ <script type="application/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=469247">Mozill
+a Bug 469247</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<video id="v"></video>
+
+<pre id="test">
+<script src="can_play_type_ogg.js"></script>
+<script>
+
+SimpleTest.waitForExplicitFinish();
+
+function finish() {
+ mediaTestCleanup();
+ SimpleTest.finish();
+}
+
+SpecialPowers.pushPrefEnv({"set": [["media.ogg.enabled", false]]},
+ function() {
+ check_ogg(document.getElementById('v'), false, finish);
+ }
+);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_can_play_type_ogg.html b/dom/media/test/test_can_play_type_ogg.html
new file mode 100644
index 0000000000..3e44f8ba8c
--- /dev/null
+++ b/dom/media/test/test_can_play_type_ogg.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=469247
+-->
+<head>
+ <title>Test for Bug 469247: Ogg backend</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=469247">Mozill
+a Bug 469247</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<video id="v"></video>
+
+<pre id="test">
+<script src="can_play_type_ogg.js"></script>
+<script>
+
+SimpleTest.waitForExplicitFinish();
+
+function finish() {
+ mediaTestCleanup();
+ SimpleTest.finish();
+}
+
+check_ogg(document.getElementById('v'), true, finish);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_can_play_type_wave.html b/dom/media/test/test_can_play_type_wave.html
new file mode 100644
index 0000000000..e6d4e29d86
--- /dev/null
+++ b/dom/media/test/test_can_play_type_wave.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=469247
+-->
+<head>
+ <title>Test for Bug 469247: WAVE backend</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=469247">Mozill
+a Bug 469247</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<video id="v"></video>
+
+<pre id="test">
+<script src="can_play_type_wave.js"></script>
+<script>
+check_wave(document.getElementById('v'), true);
+
+mediaTestCleanup();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_can_play_type_webm.html b/dom/media/test/test_can_play_type_webm.html
new file mode 100644
index 0000000000..6b0e2adfad
--- /dev/null
+++ b/dom/media/test/test_can_play_type_webm.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=566245
+-->
+<head>
+ <title>Test for Bug 566245: WebM backend</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=566245">Mozill
+a Bug 566245</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+
+<video id="v"></video>
+
+<pre id="test">
+<script src="can_play_type_webm.js"></script>
+<script>
+ async function runTest() {
+ try {
+ await check_webm(document.getElementById('v'), true);
+ mediaTestCleanup();
+ } catch (e) {
+ info("Exception " + e.message);
+ ok(false, "Threw exception " + e.message);
+ }
+ SimpleTest.finish();
+ }
+ SimpleTest.waitForExplicitFinish();
+ addLoadEvent(runTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_capture_stream_av_sync.html b/dom/media/test/test_capture_stream_av_sync.html
new file mode 100644
index 0000000000..b5754c683f
--- /dev/null
+++ b/dom/media/test/test_capture_stream_av_sync.html
@@ -0,0 +1,276 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>A/V sync test for stream capturing</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<p>Following canvas will capture and show the video frame when the video becomes audible</p><br>
+<canvas id="canvas" width="640" height="480"></canvas>
+<script type="application/javascript">
+
+/**
+ * This test will capture stream before the video starts playing, and check if
+ * A/V sync will keep sync during playing.
+ */
+add_task(async function testAVSyncForStreamCapturing() {
+ createVideo();
+ captureStreamFromVideo();
+ await playMedia();
+ await testAVSync();
+ destroyVideo();
+});
+
+/**
+ * This test will check if A/V is still on sync after we switch the media sink
+ * from playback-based sink to mediatrack-based sink.
+ */
+add_task(async function testAVSyncWhenSwitchingMediaSink() {
+ createVideo();
+ await playMedia({resolveAfterReceivingTimeupdate : 5});
+ captureStreamFromVideo();
+ await testAVSync();
+ destroyVideo();
+});
+
+/**
+ * This test will check if A/V is still on sync after we change the playback
+ * rate on the captured stream.
+ */
+add_task(async function testAVSyncWhenChangingPlaybackRate() {
+ createVideo();
+ captureStreamFromVideo();
+ await playMedia();
+ const playbackRates = [0.25, 0.5, 1.0, 1.5, 2.0];
+ for (let rate of playbackRates) {
+ setPlaybackRate(rate);
+ // TODO : when playback rate set to 1.5+x, the A/V will become less stable
+ // in testing so we raise the fuzzy frames for that, but also increase the
+ // test times. As at that speed, precise A/V becomes trivial because we
+ // can't really tell the difference. But it would be good for us to
+ // investigate if we could make A/V sync work better at that extreme high
+ // rate.
+ if (rate >= 1.5) {
+ await testAVSync({ expectedAVSyncTestTimes : 4, fuzzyFrames : 10});
+ } else {
+ await testAVSync({ expectedAVSyncTestTimes : 2 });
+ }
+ }
+ destroyVideo();
+});
+
+/**
+ * Following are helper functions
+ */
+const DEBUG = false;
+function info_debug(msg) {
+ if (DEBUG) {
+ info(msg);
+ }
+}
+
+function createVideo() {
+ const video = document.createElement("video");
+ // This video is special for testing A/V sync, it only produce audible sound
+ // once per second, and when the sound comes out, you can check the position
+ // of the square to know if the A/V keeps sync.
+ video.src = "sync.webm";
+ video.loop = true;
+ video.controls = true;
+ video.width = 640;
+ video.height = 480;
+ video.id = "video";
+ document.body.appendChild(video);
+}
+
+function destroyVideo() {
+ const video = document.getElementById("video");
+ video.src = null;
+ video.remove();
+}
+
+async function playMedia({ resolveAfterReceivingTimeupdate } = {}) {
+ const video = document.getElementById("video");
+ ok(await video.play().then(_=>true,_=>false), "video started playing");
+ if (resolveAfterReceivingTimeupdate > 0) {
+ // Play it for a while to ensure the clock growing on the normal audio sink.
+ for (let idx = 0; idx < resolveAfterReceivingTimeupdate; idx++) {
+ await new Promise(r => video.ontimeupdate = r);
+ }
+ }
+}
+
+async function captureStreamFromVideo() {
+ const video = document.getElementById("video");
+ let ac = new AudioContext();
+ let analyser = ac.createAnalyser();
+ analyser.frequencyBuf = new Float32Array(analyser.frequencyBinCount);
+ analyser.smoothingTimeConstant = 0;
+ analyser.fftSize = 2048; // 1024 bins
+ let sourceNode = ac.createMediaElementSource(video);
+ sourceNode.connect(analyser);
+ analyser.connect(ac.destination);
+ video.analyser = analyser;
+}
+
+// This method will capture the stream from the video element, and check if A/V
+// keeps sync during capturing. `callback` parameter will be executed after
+// finishing capturing.
+// @param [optional] expectedAVSyncTestTimes
+// The amount of times that A/V sync test performs.
+// @param [optional] fuzzyFrames
+// This will fuzz the result from +0 (perfect sync) to -X to +X frames.
+async function testAVSync({ expectedAVSyncTestTimes = 5, fuzzyFrames = 5} = {}) {
+ return new Promise(r => {
+ const analyser = document.getElementById("video").analyser;
+ let testIdx = 0;
+ let hasDetectedAudibleFrame = false;
+ // As we only want to detect the audible frame at the first moment when
+ // sound becomes audible, so we always skip the first audible frame because
+ // it might not be the start, but the tail part (where audio is being
+ // decaying to silence) when we start detecting.
+ let hasSkippedFirstFrame = false;
+ analyser.notifyAnalysis = () => {
+ let {frequencyBuf} = analyser;
+ analyser.getFloatFrequencyData(frequencyBuf);
+ if (checkIfBufferIsSilent(frequencyBuf)) {
+ info_debug("no need to paint the silent frame");
+ hasDetectedAudibleFrame = false;
+ requestAnimationFrame(analyser.notifyAnalysis);
+ return;
+ }
+ if (hasDetectedAudibleFrame) {
+ info_debug("detected audible frame already");
+ requestAnimationFrame(analyser.notifyAnalysis);
+ return;
+ }
+ hasDetectedAudibleFrame = true;
+ if (!hasSkippedFirstFrame) {
+ info("skip the first audible frame");
+ hasSkippedFirstFrame = true;
+ requestAnimationFrame(analyser.notifyAnalysis);
+ return;
+ }
+ const video = document.getElementById("video");
+ info(`paint audible frame`);
+ const cvs = document.getElementById("canvas");
+ let context = cvs.getContext('2d');
+ context.drawImage(video, 0, 0, 640, 480);
+ if (checkIfAVIsOnSyncFuzzy(context, fuzzyFrames)) {
+ ok(true, `test ${testIdx++} times, a/v is in sync!`);
+ } else {
+ ok(false, `test ${testIdx++} times, a/v is out of sync!`);
+ }
+ if (testIdx == expectedAVSyncTestTimes) {
+ r();
+ return;
+ }
+ requestAnimationFrame(analyser.notifyAnalysis);
+ }
+ analyser.notifyAnalysis();
+ });
+}
+
+function checkIfBufferIsSilent(buffer) {
+ for (let data of buffer) {
+ // when sound is audible, its values are around -200 and the silence values
+ // are around -800.
+ if (data > -200) {
+ return false;
+ }
+ }
+ return true;
+}
+
+// This function will check the pixel data from the `context` to see if the
+// square appears in the right place. As we can't control the exact timing
+// of rendering video frames in the compositor, so the result would be fuzzy.
+function checkIfAVIsOnSyncFuzzy(context, fuzzyFrames) {
+ const squareLength = 48;
+ // Canvas is 640*480, so perfect sync is the left-top corner when the square
+ // shows up in the middle.
+ const perfectSync =
+ { x: 320 - squareLength/2.0 ,
+ y: 240 - squareLength/2.0 };
+ let isAVSyncFuzzy = false;
+ // Get the whole partial section of image and detect where the square is.
+ let imageData = context.getImageData(0, perfectSync.y, 640, squareLength);
+ for (let i = 0; i < imageData.data.length; i += 4) {
+ // If the pixel's color is red, then this position will be the left-top
+ // corner of the square.
+ if (isPixelColorRed(imageData.data[i], imageData.data[i+1],
+ imageData.data[i+2])) {
+ const pos = ImageIdxToRelativeCoordinate(imageData, i);
+ let diff = calculateFrameDiffenceInXAxis(pos.x, perfectSync.x);
+ info(`find the square in diff=${diff}`);
+ // Maybe we check A/V sync too early or too late, try to adjust the diff
+ // to guess the previous correct position where the square should be.
+ if (diff > fuzzyFrames) {
+ diff = adjustFrameDiffBasedOnMediaTime(diff);
+ const video = document.getElementById("video");
+ info(`adjusted diff to ${diff} (time=${video.currentTime})`);
+ }
+ if (diff <= fuzzyFrames) {
+ isAVSyncFuzzy = true;
+ }
+ context.putImageData(imageData, 0, 0);
+ break;
+ }
+ }
+ if (!isAVSyncFuzzy) {
+ const ctx = document.getElementById('canvas');
+ info(ctx.toDataURL());
+ }
+ return isAVSyncFuzzy;
+}
+
+// Input an imageData and its idx, then return a relative coordinate on the
+// range of given imageData.
+function ImageIdxToRelativeCoordinate(imageData, idx) {
+ const offset = idx / 4; // RGBA
+ return { x: offset % imageData.width, y: offset / imageData.width };
+}
+
+function calculateFrameDiffenceInXAxis(squareX, targetX) {
+ const offsetX = Math.abs(targetX - squareX);
+ const xSpeedPerFrame = 640 / 60; // video is 60fps
+ return offsetX / xSpeedPerFrame;
+}
+
+function isPixelColorRed(r, g, b) {
+ // As the rendering color would vary in the screen on different platforms, so
+ // we won't strict the R should be 255, just check if it's larger than a
+ // certain threshold.
+ return r > 200 && g < 10 && b < 10;
+}
+
+function setPlaybackRate(rate) {
+ const video = document.getElementById("video");
+ info(`change playback rate from ${video.playbackRate} to ${rate}`);
+ document.getElementById("video").playbackRate = rate;
+}
+
+function adjustFrameDiffBasedOnMediaTime(currentDiff) {
+ // The audio wave can be simply regarded as being composed by "start", "peak"
+ // and "tail". The "start" part is the sound gradually becoming louder and the
+ // "tail" is gradually becoming silent. We want to check the "peak" part which
+ // should happn on evert second regularly (1s, 2s, 3s ...) However, this check
+ // is triggered by `requestAnimationFrame()` and we can't guarantee that
+ // we're checking the peak part while the function is being called. Therefore,
+ // we have to do an adjustment by the video time, to know if we're checking
+ // the audio wave too early or too late in order to get a consistent result.
+ const video = document.getElementById("video");
+ const videoCurrentTimeFloatPortion = video.currentTime % 1;
+ const timeOffset =
+ videoCurrentTimeFloatPortion > 0.5 ?
+ 1 - videoCurrentTimeFloatPortion : // too early
+ videoCurrentTimeFloatPortion; // too late
+ const frameOffset = timeOffset / 0.016; // 60fps, 1 frame=0.016s
+ info(`timeOffset=${timeOffset}, frameOffset=${frameOffset}`);
+ return Math.abs(currentDiff - frameOffset);
+}
+
+</script>
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/media/test/test_chaining.html b/dom/media/test/test_chaining.html
new file mode 100644
index 0000000000..6ebf94c88d
--- /dev/null
+++ b/dom/media/test/test_chaining.html
@@ -0,0 +1,92 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: chained ogg files.</title>
+ <meta charset='utf-8'>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var manager = new MediaTestManager;
+
+function finish_test(element) {
+ removeNodeAndSource(element);
+ manager.finished(element.token);
+}
+
+function onended(e) {
+ var t = e.target;
+ is(t._metadataCount, t._links, "We should have received "+ t._links +
+ " metadataloaded event. " + t.src);
+
+ // If we encounter a file that has links with a different numbers of channel,
+ // we stop the decoding at the end of the first link. Hence, we report normal
+ // |seekable| and |buffered| values.
+ if (t._links != 1) {
+ is(t.seekable.length, 0, "No seekable ranges should be reported." + t.src);
+ is(t.buffered.length, 0, "No buffered region should be reported." + t.src);
+ }
+
+ is(t.played.length, 1, "The played region should be continuous." + t.src);
+
+ if (t._links != 1) {
+ var oldCurrentTime = t.currentTime;
+ t.currentTime = 0.0;
+ is(t.currentTime, oldCurrentTime,
+ "Seeking should be disabled when playing chained media." + t.src);
+ }
+
+ finish_test(t);
+}
+
+function onmetadataloaded(e) {
+ var t = e.target;
+ if (! t._metadataCount) {
+ t._metadataCount = 0;
+ }
+
+ if (t._metadataCount > 1 && t._links === 1) {
+ ok(false, "We should receive only one \"loadedmetadata\" event for a chained file we don't support.")
+ }
+
+ // We should be able to assert equality here, but somehow it fails (rarely but
+ // still) on try. Instead, we give it a little slack and assert that the index
+ // increases monotonically.
+ ok(t.mozGetMetadata().index >= t._metadataCount || t._links === 1,
+ "The metadata index value should increase." + t.src);
+
+ ok(t.currentTime >= t._prevCurrentTime,
+ "The currenttime should be increased correctly in new chained part.");
+ t._prevCurrentTime = t.currentTime;
+
+ // The files have all a user comment name 'index' that increments at each link
+ // in the chained media.
+ t._metadataCount++;
+ if (!t.playing && !t.ended) {
+ t.play();
+ }
+}
+
+function startTest(test, token) {
+ var elemType = /^audio/.test(test.type) ? "audio" : "video";
+ var element = document.createElement(elemType);
+ document.body.appendChild(element);
+ manager.started(token);
+ element._links= test.links;
+ element.src = test.name;
+ element.token = token;
+ element.controls = true;
+ element.addEventListener("loadedmetadata", onmetadataloaded);
+ element.addEventListener("ended", onended);
+ element.preload = "metadata";
+ element._prevCurrentTime = 0;
+}
+
+manager.runTests(gChainingTests, startTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_cloneElementVisually_ended_video.html b/dom/media/test/test_cloneElementVisually_ended_video.html
new file mode 100644
index 0000000000..331dda2db9
--- /dev/null
+++ b/dom/media/test/test_cloneElementVisually_ended_video.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test cloneElementVisually</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="https://example.com:443/tests/dom/media/test/cloneElementVisually_helpers.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="content">
+ <h1>Original</h1>
+ <video id="original"></video>
+ <h1>Clone</h1>
+</div>
+<div id="results">
+ <h1>Results</h1>
+ <canvas id="left"></canvas>
+ <canvas id="right"></canvas>
+</div>
+
+<script type="application/javascript">
+
+/* import-globals-from cloneElementVisually_helpers.js */
+
+/**
+ * Test that when we start cloning a video that has already ended, the
+ * clone displays the last frame from the video.
+ */
+add_task(async () => {
+ await setup();
+
+ let originalVideo = document.getElementById("original");
+ let ended = waitForEventOnce(originalVideo, "ended");
+ await originalVideo.play();
+ await ended;
+
+ await withNewClone(originalVideo, async clone => {
+ await SpecialPowers.wrap(originalVideo).cloneElementVisually(clone);
+ ok(await assertVideosMatch(originalVideo, clone),
+ "Visual clone should display final frame.");
+ });
+});
+
+</script>
+
+</body>
+</html>
diff --git a/dom/media/test/test_cloneElementVisually_mediastream.html b/dom/media/test/test_cloneElementVisually_mediastream.html
new file mode 100644
index 0000000000..5bfcf7673f
--- /dev/null
+++ b/dom/media/test/test_cloneElementVisually_mediastream.html
@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test cloneElementVisually</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="https://example.com:443/tests/dom/media/test/cloneElementVisually_helpers.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="content">
+ <h1>Original</h1>
+ <video id="original"></video>
+ <h1>MediaStream</h1>
+ <video id="streamTarget"></video>
+ <h1>Clone</h1>
+</div>
+<div id="results">
+ <h1>Results</h1>
+ <canvas id="left"></canvas>
+ <canvas id="right"></canvas>
+</div>
+
+<script type="application/javascript">
+
+/* import-globals-from cloneElementVisually_helpers.js */
+
+/**
+ * Test that we can clone a video that is playing a MediaStream.
+ */
+add_task(async () => {
+ await setup();
+
+ let originalVideo = document.getElementById("original");
+ let stream = originalVideo.mozCaptureStream();
+ let streamTarget = document.getElementById("streamTarget");
+ originalVideo.setAttribute("loop", true);
+ let playingPromise = waitForEventOnce(originalVideo, "playing");
+ await originalVideo.play();
+ await playingPromise;
+
+ streamTarget.srcObject = stream;
+ playingPromise = waitForEventOnce(streamTarget, "playing");
+ await streamTarget.play();
+ await playingPromise
+
+ await withNewClone(originalVideo, async clone => {
+ await SpecialPowers.wrap(streamTarget).cloneElementVisually(clone);
+
+ originalVideo.loop = false;
+ originalVideo.currentTime = originalVideo.duration - 0.1;
+ await waitForEventOnce(streamTarget, "ended");
+
+ ok(await assertVideosMatch(originalVideo, streamTarget),
+ "Should match Original video");
+ ok(await assertVideosMatch(streamTarget, clone),
+ "Should match MediaStream");
+ });
+
+ // Capturing a stream from a video "taints" it which prevents testing
+ // shutdown decoder behaviour. To avoid interfering with future tests,
+ // we replace the video.
+ let newVideo = originalVideo.cloneNode();
+ originalVideo.parentNode.replaceChild(newVideo, originalVideo);
+});
+
+</script>
+
+</body>
+</html>
diff --git a/dom/media/test/test_cloneElementVisually_mediastream_multitrack.html b/dom/media/test/test_cloneElementVisually_mediastream_multitrack.html
new file mode 100644
index 0000000000..04d1ed484c
--- /dev/null
+++ b/dom/media/test/test_cloneElementVisually_mediastream_multitrack.html
@@ -0,0 +1,88 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test cloneElementVisually</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="https://example.com:443/tests/dom/media/test/cloneElementVisually_helpers.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="content">
+ <h1>Originals</h1>
+ <div id="originalContainer"></div>
+ <canvas id="canvas"></canvas>
+ <h1>MediaStream</h1>
+ <div id="streamTargetContainer"></div>
+ <h1>Clone</h1>
+</div>
+<div id="results">
+ <h1>Results</h1>
+ <canvas id="left"></canvas>
+ <canvas id="right"></canvas>
+</div>
+
+<script type="application/javascript">
+
+/* import-globals-from cloneElementVisually_helpers.js */
+
+/**
+ * Test that a clone of a video that is playing a MediaStream properly tracks
+ * the selected video track.
+ */
+add_task(async () => {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.track.enabled", true],
+ ],
+ });
+
+ let originalVideo = document.createElement("video");
+ originalVideo.id = "original";
+ document.getElementById("originalContainer").appendChild(originalVideo);
+
+ let streamTarget = document.createElement("video");
+ document.getElementById("streamTargetContainer").appendChild(streamTarget);
+
+ await setup();
+
+ let [track1] = originalVideo.mozCaptureStream().getTracks();
+ let playingPromise = waitForEventOnce(originalVideo, "playing");
+ await originalVideo.play();
+ await playingPromise;
+
+ let canvas = document.getElementById("canvas");
+ canvas.width = originalVideo.videoWidth / 2;
+ canvas.height = originalVideo.videoHeight / 2;
+ let ctx = canvas.getContext("2d");
+ let [track2] = canvas.captureStream().getTracks();
+ ctx.fillStyle = "green";
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ streamTarget.srcObject = new MediaStream([track1, track2]);
+ playingPromise = waitForEventOnce(streamTarget, "playing");
+ await streamTarget.play();
+ await playingPromise;
+
+ await withNewClone(originalVideo, async clone => {
+ SpecialPowers.wrap(streamTarget).cloneElementVisually(clone);
+
+ let selectedTrackIdx = streamTarget.videoTracks.selectedIndex;
+ streamTarget.videoTracks[++selectedTrackIdx % 2].selected = true;
+ await waitForEventOnce(streamTarget, "resize");
+
+ ok(await assertVideosMatch(streamTarget, clone),
+ "Should match MediaStream");
+ });
+
+ // Capturing a stream from a video "taints" it which prevents testing
+ // shutdown decoder behaviour. To avoid interfering with future tests,
+ // we replace the video.
+ let newVideo = originalVideo.cloneNode();
+ originalVideo.parentNode.replaceChild(newVideo, originalVideo);
+});
+
+</script>
+
+</body>
+</html>
diff --git a/dom/media/test/test_cloneElementVisually_no_suspend.html b/dom/media/test/test_cloneElementVisually_no_suspend.html
new file mode 100644
index 0000000000..d576bf8ab3
--- /dev/null
+++ b/dom/media/test/test_cloneElementVisually_no_suspend.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test cloneElementVisually</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="https://example.com:443/tests/dom/media/test/cloneElementVisually_helpers.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="content">
+ <h1>Original</h1>
+ <video id="original"></video>
+ <h1>MediaStream</h1>
+ <video id="streamTarget"></video>
+ <h1>Clone</h1>
+</div>
+<div id="results">
+ <h1>Results</h1>
+ <canvas id="left"></canvas>
+ <canvas id="right"></canvas>
+</div>
+
+<script type="application/javascript">
+
+/* import-globals-from cloneElementVisually_helpers.js */
+
+/**
+ * Tests that cloning a video prevents the decoder from being suspended
+ * if the original video stops being visible.
+ */
+add_task(async () => {
+ await setup();
+
+ let originalVideo = document.getElementById("original");
+ await setVideoSrc(originalVideo, LONG_VIDEO);
+
+ await originalVideo.play();
+
+ // Ensure that hiding and pausing this video will cause us to
+ // try suspending it.
+ await ensureVideoSuspendable(originalVideo);
+
+ await withNewClone(originalVideo, async clone => {
+ SpecialPowers.wrap(originalVideo).cloneElementVisually(clone);
+
+ // Go back to the beginning of the video to give us enough time to
+ // fail to suspend the video when it's being cloned before the
+ // video ends.
+ originalVideo.removeAttribute("loop");
+ originalVideo.currentTime = 0;
+ await waitForEventOnce(originalVideo, "seeked");
+
+ let suspendTimerFired = false;
+
+ let listener = () => {
+ suspendTimerFired = true;
+ }
+ originalVideo.addEventListener("mozstartvideosuspendtimer", listener);
+
+ // Have to do this to access normally-preffed off binding methods for some
+ // reason.
+ // See bug 1544257.
+ SpecialPowers.wrap(originalVideo).setVisible(false);
+
+ await waitForEventOnce(originalVideo, "ended");
+
+ originalVideo.removeEventListener("mozstartvideosuspendtimer", listener);
+
+ ok(!suspendTimerFired,
+ "mozstartvideosuspendtimer should not have fired.");
+
+ // Have to do this to access normally-preffed off binding methods for some
+ // reason.
+ // See bug 1544257.
+ SpecialPowers.wrap(originalVideo).setVisible(true);
+ });
+
+ await originalVideo.play();
+
+ // With the clone gone, the original video should be able to suspend now.
+ await ensureVideoSuspendable(originalVideo);
+
+ await setVideoSrc(originalVideo, TEST_VIDEO_1);
+});
+
+</script>
+
+</body>
+</html>
diff --git a/dom/media/test/test_cloneElementVisually_paused.html b/dom/media/test/test_cloneElementVisually_paused.html
new file mode 100644
index 0000000000..1812becdd8
--- /dev/null
+++ b/dom/media/test/test_cloneElementVisually_paused.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test cloneElementVisually</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="https://example.com:443/tests/dom/media/test/cloneElementVisually_helpers.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="content">
+ <h1>Original</h1>
+ <video id="original"></video>
+ <h1>Clone</h1>
+</div>
+<div id="results">
+ <h1>Results</h1>
+ <canvas id="left"></canvas>
+ <canvas id="right"></canvas>
+</div>
+
+<script type="application/javascript">
+
+/* import-globals-from cloneElementVisually_helpers.js */
+
+/**
+ * Test that when we start cloning a paused video, the clone displays
+ * the first paused frame.
+ */
+add_task(async () => {
+ await setup();
+
+ let originalVideo = document.getElementById("original");
+ await withNewClone(originalVideo, async clone => {
+ await SpecialPowers.wrap(originalVideo).cloneElementVisually(clone);
+
+ ok(await assertVideosMatch(originalVideo, clone),
+ "Initial paused frame should match.");
+ });
+});
+
+</script>
+
+</body>
+</html>
diff --git a/dom/media/test/test_cloneElementVisually_poster.html b/dom/media/test/test_cloneElementVisually_poster.html
new file mode 100644
index 0000000000..40f60e4961
--- /dev/null
+++ b/dom/media/test/test_cloneElementVisually_poster.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test cloneElementVisually with poster</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="https://example.com:443/tests/dom/media/test/cloneElementVisually_helpers.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="content">
+ <h1>Original</h1>
+ <video id="original"></video>
+ <h1>Clone</h1>
+</div>
+<div id="results">
+ <h1>Results</h1>
+ <canvas id="left"></canvas>
+ <canvas id="right"></canvas>
+</div>
+
+<script type="application/javascript">
+
+/* import-globals-from cloneElementVisually_helpers.js */
+
+/**
+ * Test that when we clone a video with a poster, the poster does not prevent
+ * the cloned video from displaying properly (as in bug 1532692).
+ */
+add_task(async () => {
+ await setup();
+
+ let originalVideo = document.getElementById("original");
+ const POSTER_URL = "https://example.com:443/tests/dom/media/test/poster-test.jpg";
+ originalVideo.setAttribute("poster", POSTER_URL);
+
+ await withNewClone(originalVideo, async clone => {
+ await SpecialPowers.wrap(originalVideo).cloneElementVisually(clone);
+ originalVideo.loop = false;
+ originalVideo.currentTime = originalVideo.duration - 0.1;
+ let endedPromise = waitForEventOnce(originalVideo, "ended");
+ await originalVideo.play();
+ await endedPromise;
+
+ ok(await assertVideosMatch(originalVideo, clone),
+ "Video with a poster should clone properly.");
+ });
+});
+
+</script>
+
+</body>
+</html>
diff --git a/dom/media/test/test_cloneElementVisually_resource_change.html b/dom/media/test/test_cloneElementVisually_resource_change.html
new file mode 100644
index 0000000000..3a66906ea2
--- /dev/null
+++ b/dom/media/test/test_cloneElementVisually_resource_change.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test cloneElementVisually</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="https://example.com:443/tests/dom/media/test/cloneElementVisually_helpers.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css">
+</head>
+<body>
+<div id="content">
+ <h1>Original</h1>
+ <video id="original"></video>
+ <h1>MediaStream</h1>
+ <video id="streamTarget"></video>
+ <h1>Clone</h1>
+</div>
+<div id="results">
+ <h1>Results</h1>
+ <canvas id="left"></canvas>
+ <canvas id="right"></canvas>
+</div>
+
+<script type="application/javascript">
+
+/* import-globals-from cloneElementVisually_helpers.js */
+
+/**
+ * Tests that cloning survives changes to the underlying video resource.
+ */
+add_task(async () => {
+ await setup();
+
+ let originalVideo = document.getElementById("original");
+ originalVideo.setAttribute("loop", true);
+ await originalVideo.play();
+
+ await withNewClone(originalVideo, async clone => {
+ SpecialPowers.wrap(originalVideo).cloneElementVisually(clone);
+
+ await waitForEventOnce(originalVideo, "timeupdate");
+
+ originalVideo.pause();
+ await waitForEventOnce(originalVideo, "pause");
+
+ ok(await assertVideosMatch(originalVideo, clone),
+ "Initial video should match.");
+
+ await setVideoSrc(originalVideo, TEST_VIDEO_2);
+
+ await originalVideo.play();
+ await waitForEventOnce(originalVideo, "timeupdate");
+
+ originalVideo.pause();
+ await waitForEventOnce(originalVideo, "pause");
+
+ ok(await assertVideosMatch(originalVideo, clone),
+ "New video should match.");
+ });
+
+ await setVideoSrc(originalVideo, TEST_VIDEO_1);
+});
+
+</script>
+
+</body>
+</html>
diff --git a/dom/media/test/test_clone_media_element.html b/dom/media/test/test_clone_media_element.html
new file mode 100644
index 0000000000..35e5cd69d0
--- /dev/null
+++ b/dom/media/test/test_clone_media_element.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test: cloned media element should continue to play to the end even after the source of the original element is cleared</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// tests must run in sequence otherwise concurrent running test will also
+// update media cache which will hide the fact media cache not updated
+// after changes in media cache streams.
+PARALLEL_TESTS = 1;
+
+function startTest(test, token) {
+ manager.started(token);
+ info("Trying to load " + token);
+ var v = document.createElement('video');
+ v.preload = "metadata";
+ v.token = token;
+ v.src = test.name;
+
+ v.onloadedmetadata = function(evt) {
+ info(evt.target.token + " metadata loaded.");
+ evt.target.onloadedmetadata = null;
+ var clone = evt.target.cloneNode(false);
+ clone.token = evt.target.token;
+ clone.play();
+
+ clone.onloadstart = function(event) {
+ info("cloned " + event.target.token + " start loading.");
+ event.target.onloadstart = null;
+ removeNodeAndSource(v);
+ }
+
+ clone.onended = function(event) {
+ ok(true, "cloned " + event.target.token + " ended.");
+ event.target.onended = null;
+ removeNodeAndSource(event.target);
+ manager.finished(event.target.token);
+ }
+ }
+}
+
+var manager = new MediaTestManager;
+manager.runTests(gSmallTests.concat(gPlayedTests), startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_closing_connections.html b/dom/media/test/test_closing_connections.html
new file mode 100644
index 0000000000..c5eb565447
--- /dev/null
+++ b/dom/media/test/test_closing_connections.html
@@ -0,0 +1,58 @@
+hg diff<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=479863
+-->
+<head>
+ <title>Test for Bug 479863 --- loading many connections</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="use_large_cache.js"></script>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=479863">Mozilla Bug 479863</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+
+<script type="application/javascript">
+window.onload = function() {
+ ok(true, "loaded metadata for all videos");
+ mediaTestCleanup();
+ SimpleTest.finish();
+}
+
+/* With normal per-domain connection limits and a naive implementation, we
+ won't ever be able to load all these videos because the first 15 (or whatever)
+ will each take up one HTTP connection (which will be suspended) and then
+ the others will be blocked by the per-domain HTTP connection limit. We
+ pass this test by closing the connection for non-buffered videos after
+ we've got the first frame.
+*/
+
+var resource = getPlayableVideo(gClosingConnectionsTest);
+
+SimpleTest.waitForExplicitFinish();
+function beginTest() {
+ if (resource != null) {
+ for (var i=0; i<20; ++i) {
+ var v = document.createElement("video");
+ v.src = resource.name;
+ v.preload = "metadata";
+ document.body.appendChild(v);
+ }
+ } else {
+ todo(false, "No types supported");
+ }
+}
+beginTest();
+</script>
+
+<pre id="test">
+<script type="application/javascript">
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_constants.html b/dom/media/test/test_constants.html
new file mode 100644
index 0000000000..1d4a8da250
--- /dev/null
+++ b/dom/media/test/test_constants.html
@@ -0,0 +1,228 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+ Adapted from:
+ http://simon.html5.org/test/html/dom/interfaces/htmlelement/htmlmediaelement/const-unsigned-short/001.htm
+-->
+<head>
+ <title>Media test: constants</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<video><source></video><audio><source></audio>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+is(HTMLElement.NETWORK_EMPTY, undefined);
+is(HTMLElement.NETWORK_IDLE, undefined);
+is(HTMLElement.NETWORK_LOADING, undefined);
+is(HTMLElement.NETWORK_NO_SOURCE, undefined);
+is(HTMLElement.HAVE_NOTHING, undefined);
+is(HTMLElement.HAVE_METADATA, undefined);
+is(HTMLElement.HAVE_CURRENT_DATA, undefined);
+is(HTMLElement.HAVE_FUTURE_DATA, undefined);
+is(HTMLElement.HAVE_ENOUGH_DATA, undefined);
+is(HTMLElement.MEDIA_ERR_ABORTED, undefined);
+is(HTMLElement.MEDIA_ERR_NETWORK, undefined);
+is(HTMLElement.MEDIA_ERR_DECODE, undefined);
+is(HTMLElement.MEDIA_ERR_SRC_NOT_SUPPORTED, undefined);
+is(HTMLMediaElement.NETWORK_EMPTY, 0);
+is(HTMLMediaElement.NETWORK_IDLE, 1);
+is(HTMLMediaElement.NETWORK_LOADING, 2);
+is(HTMLMediaElement.NETWORK_NO_SOURCE, 3);
+is(HTMLMediaElement.HAVE_NOTHING, 0);
+is(HTMLMediaElement.HAVE_METADATA, 1);
+is(HTMLMediaElement.HAVE_CURRENT_DATA, 2);
+is(HTMLMediaElement.HAVE_FUTURE_DATA, 3);
+is(HTMLMediaElement.HAVE_ENOUGH_DATA, 4);
+is(HTMLMediaElement.MEDIA_ERR_ABORTED, undefined);
+is(HTMLMediaElement.MEDIA_ERR_NETWORK, undefined);
+is(HTMLMediaElement.MEDIA_ERR_DECODE, undefined);
+is(HTMLMediaElement.MEDIA_ERR_SRC_NOT_SUPPORTED, undefined);
+is(HTMLVideoElement.NETWORK_EMPTY, 0);
+is(HTMLVideoElement.NETWORK_IDLE, 1);
+is(HTMLVideoElement.NETWORK_LOADING, 2);
+is(HTMLVideoElement.NETWORK_NO_SOURCE, 3);
+is(HTMLVideoElement.HAVE_NOTHING, 0);
+is(HTMLVideoElement.HAVE_METADATA, 1);
+is(HTMLVideoElement.HAVE_CURRENT_DATA, 2);
+is(HTMLVideoElement.HAVE_FUTURE_DATA, 3);
+is(HTMLVideoElement.HAVE_ENOUGH_DATA, 4);
+is(HTMLVideoElement.MEDIA_ERR_ABORTED, undefined);
+is(HTMLVideoElement.MEDIA_ERR_NETWORK, undefined);
+is(HTMLVideoElement.MEDIA_ERR_DECODE, undefined);
+is(HTMLVideoElement.MEDIA_ERR_SRC_NOT_SUPPORTED, undefined);
+is(HTMLAudioElement.NETWORK_EMPTY, 0);
+is(HTMLAudioElement.NETWORK_IDLE, 1);
+is(HTMLAudioElement.NETWORK_LOADING, 2);
+is(HTMLAudioElement.NETWORK_NO_SOURCE, 3);
+is(HTMLAudioElement.HAVE_NOTHING, 0);
+is(HTMLAudioElement.HAVE_METADATA, 1);
+is(HTMLAudioElement.HAVE_CURRENT_DATA, 2);
+is(HTMLAudioElement.HAVE_FUTURE_DATA, 3);
+is(HTMLAudioElement.HAVE_ENOUGH_DATA, 4);
+is(HTMLAudioElement.MEDIA_ERR_ABORTED, undefined);
+is(HTMLAudioElement.MEDIA_ERR_NETWORK, undefined);
+is(HTMLAudioElement.MEDIA_ERR_DECODE, undefined);
+is(HTMLAudioElement.MEDIA_ERR_SRC_NOT_SUPPORTED, undefined);
+is(HTMLSourceElement.NETWORK_EMPTY, undefined);
+is(HTMLSourceElement.NETWORK_IDLE, undefined);
+is(HTMLSourceElement.NETWORK_LOADING, undefined);
+is(HTMLSourceElement.NETWORK_NO_SOURCE, undefined);
+is(HTMLSourceElement.HAVE_NOTHING, undefined);
+is(HTMLSourceElement.HAVE_METADATA, undefined);
+is(HTMLSourceElement.HAVE_CURRENT_DATA, undefined);
+is(HTMLSourceElement.HAVE_FUTURE_DATA, undefined);
+is(HTMLSourceElement.HAVE_ENOUGH_DATA, undefined);
+is(HTMLSourceElement.MEDIA_ERR_ABORTED, undefined);
+is(HTMLSourceElement.MEDIA_ERR_NETWORK, undefined);
+is(HTMLSourceElement.MEDIA_ERR_DECODE, undefined);
+is(HTMLSourceElement.MEDIA_ERR_SRC_NOT_SUPPORTED, undefined);
+is(MediaError.NETWORK_EMPTY, undefined);
+is(MediaError.NETWORK_IDLE, undefined);
+is(MediaError.NETWORK_LOADING, undefined);
+is(MediaError.NETWORK_NO_SOURCE, undefined);
+is(MediaError.HAVE_NOTHING, undefined);
+is(MediaError.HAVE_METADATA, undefined);
+is(MediaError.HAVE_CURRENT_DATA, undefined);
+is(MediaError.HAVE_FUTURE_DATA, undefined);
+is(MediaError.HAVE_ENOUGH_DATA, undefined);
+is(MediaError.MEDIA_ERR_ABORTED, 1);
+is(MediaError.MEDIA_ERR_NETWORK, 2);
+is(MediaError.MEDIA_ERR_DECODE, 3);
+is(MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED, 4);
+is(document.body.NETWORK_EMPTY, undefined);
+is(document.body.NETWORK_IDLE, undefined);
+is(document.body.NETWORK_LOADING, undefined);
+is(document.body.NETWORK_NO_SOURCE, undefined);
+is(document.body.HAVE_NOTHING, undefined);
+is(document.body.HAVE_METADATA, undefined);
+is(document.body.HAVE_CURRENT_DATA, undefined);
+is(document.body.HAVE_FUTURE_DATA, undefined);
+is(document.body.HAVE_ENOUGH_DATA, undefined);
+is(document.body.MEDIA_ERR_ABORTED, undefined);
+is(document.body.MEDIA_ERR_NETWORK, undefined);
+is(document.body.MEDIA_ERR_DECODE, undefined);
+is(document.body.MEDIA_ERR_SRC_NOT_SUPPORTED, undefined);
+is(document.getElementsByTagName("video")[0].NETWORK_EMPTY, 0);
+is(document.getElementsByTagName("video")[0].NETWORK_IDLE, 1);
+is(document.getElementsByTagName("video")[0].NETWORK_LOADING, 2);
+is(document.getElementsByTagName("video")[0].NETWORK_NO_SOURCE, 3);
+is(document.getElementsByTagName("video")[0].HAVE_NOTHING, 0);
+is(document.getElementsByTagName("video")[0].HAVE_METADATA, 1);
+is(document.getElementsByTagName("video")[0].HAVE_CURRENT_DATA, 2);
+is(document.getElementsByTagName("video")[0].HAVE_FUTURE_DATA, 3);
+is(document.getElementsByTagName("video")[0].HAVE_ENOUGH_DATA, 4);
+is(document.getElementsByTagName("video")[0].MEDIA_ERR_ABORTED, undefined);
+is(document.getElementsByTagName("video")[0].MEDIA_ERR_NETWORK, undefined);
+is(document.getElementsByTagName("video")[0].MEDIA_ERR_DECODE, undefined);
+is(document.getElementsByTagName("video")[0].MEDIA_ERR_SRC_NOT_SUPPORTED, undefined);
+is(document.getElementsByTagName("audio")[0].NETWORK_EMPTY, 0);
+is(document.getElementsByTagName("audio")[0].NETWORK_IDLE, 1);
+is(document.getElementsByTagName("audio")[0].NETWORK_LOADING, 2);
+is(document.getElementsByTagName("audio")[0].NETWORK_NO_SOURCE, 3);
+is(document.getElementsByTagName("audio")[0].HAVE_NOTHING, 0);
+is(document.getElementsByTagName("audio")[0].HAVE_METADATA, 1);
+is(document.getElementsByTagName("audio")[0].HAVE_CURRENT_DATA, 2);
+is(document.getElementsByTagName("audio")[0].HAVE_FUTURE_DATA, 3);
+is(document.getElementsByTagName("audio")[0].HAVE_ENOUGH_DATA, 4);
+is(document.getElementsByTagName("audio")[0].MEDIA_ERR_ABORTED, undefined);
+is(document.getElementsByTagName("audio")[0].MEDIA_ERR_NETWORK, undefined);
+is(document.getElementsByTagName("audio")[0].MEDIA_ERR_DECODE, undefined);
+is(document.getElementsByTagName("audio")[0].MEDIA_ERR_SRC_NOT_SUPPORTED, undefined);
+is(document.getElementsByTagName("source")[0].NETWORK_EMPTY, undefined);
+is(document.getElementsByTagName("source")[0].NETWORK_IDLE, undefined);
+is(document.getElementsByTagName("source")[0].NETWORK_LOADING, undefined);
+is(document.getElementsByTagName("source")[0].NETWORK_NO_SOURCE, undefined);
+is(document.getElementsByTagName("source")[0].HAVE_NOTHING, undefined);
+is(document.getElementsByTagName("source")[0].HAVE_METADATA, undefined);
+is(document.getElementsByTagName("source")[0].HAVE_CURRENT_DATA, undefined);
+is(document.getElementsByTagName("source")[0].HAVE_FUTURE_DATA, undefined);
+is(document.getElementsByTagName("source")[0].HAVE_ENOUGH_DATA, undefined);
+is(document.getElementsByTagName("source")[0].MEDIA_ERR_ABORTED, undefined);
+is(document.getElementsByTagName("source")[0].MEDIA_ERR_NETWORK, undefined);
+is(document.getElementsByTagName("source")[0].MEDIA_ERR_DECODE, undefined);
+is(document.getElementsByTagName("source")[0].MEDIA_ERR_SRC_NOT_SUPPORTED, undefined);
+is(HTMLElement.prototype.NETWORK_EMPTY, undefined);
+is(HTMLElement.prototype.NETWORK_IDLE, undefined);
+is(HTMLElement.prototype.NETWORK_LOADING, undefined);
+is(HTMLElement.prototype.NETWORK_NO_SOURCE, undefined);
+is(HTMLElement.prototype.HAVE_NOTHING, undefined);
+is(HTMLElement.prototype.HAVE_METADATA, undefined);
+is(HTMLElement.prototype.HAVE_CURRENT_DATA, undefined);
+is(HTMLElement.prototype.HAVE_FUTURE_DATA, undefined);
+is(HTMLElement.prototype.HAVE_ENOUGH_DATA, undefined);
+is(HTMLElement.prototype.MEDIA_ERR_ABORTED, undefined);
+is(HTMLElement.prototype.MEDIA_ERR_NETWORK, undefined);
+is(HTMLElement.prototype.MEDIA_ERR_DECODE, undefined);
+is(HTMLElement.prototype.MEDIA_ERR_SRC_NOT_SUPPORTED, undefined);
+is(HTMLMediaElement.prototype.NETWORK_EMPTY, 0, "HTMLMediaElement.prototype.NETWORK_EMPTY");
+is(HTMLMediaElement.prototype.NETWORK_IDLE, 1, "HTMLMediaElement.prototype.NETWORK_IDLE");
+is(HTMLMediaElement.prototype.NETWORK_LOADING, 2, "HTMLMediaElement.prototype.NETWORK_LOADING");
+is(HTMLMediaElement.prototype.NETWORK_NO_SOURCE, 3, "HTMLMediaElement.prototype.NETWORK_NO_SOURCE");
+is(HTMLMediaElement.prototype.HAVE_NOTHING, 0, "HTMLMediaElement.prototype.HAVE_NOTHING");
+is(HTMLMediaElement.prototype.HAVE_METADATA, 1, "HTMLMediaElement.prototype.HAVE_METADATA");
+is(HTMLMediaElement.prototype.HAVE_CURRENT_DATA, 2, "HTMLMediaElement.prototype.HAVE_CURRENT_DATA");
+is(HTMLMediaElement.prototype.HAVE_FUTURE_DATA, 3, "HTMLMediaElement.prototype.HAVE_FUTURE_DATA");
+is(HTMLMediaElement.prototype.HAVE_ENOUGH_DATA, 4, "HTMLMediaElement.prototype.HAVE_ENOUGH_DATA");
+is(HTMLMediaElement.prototype.MEDIA_ERR_ABORTED, undefined, "HTMLMediaElement.prototype.MEDIA_ERR_ABORTED");
+is(HTMLMediaElement.prototype.MEDIA_ERR_NETWORK, undefined, "HTMLMediaElement.prototype.MEDIA_ERR_NETWORK");
+is(HTMLMediaElement.prototype.MEDIA_ERR_DECODE, undefined, "HTMLMediaElement.prototype.MEDIA_ERR_DECODE");
+is(HTMLMediaElement.prototype.MEDIA_ERR_SRC_NOT_SUPPORTED, undefined, "HTMLMediaElement.prototype.MEDIA_ERR_SRC_NOT_SUPPORTED");
+is(HTMLVideoElement.prototype.NETWORK_EMPTY, 0);
+is(HTMLVideoElement.prototype.NETWORK_IDLE, 1);
+is(HTMLVideoElement.prototype.NETWORK_LOADING, 2);
+is(HTMLVideoElement.prototype.NETWORK_NO_SOURCE, 3);
+is(HTMLVideoElement.prototype.HAVE_NOTHING, 0);
+is(HTMLVideoElement.prototype.HAVE_METADATA, 1);
+is(HTMLVideoElement.prototype.HAVE_CURRENT_DATA, 2);
+is(HTMLVideoElement.prototype.HAVE_FUTURE_DATA, 3);
+is(HTMLVideoElement.prototype.HAVE_ENOUGH_DATA, 4);
+is(HTMLVideoElement.prototype.MEDIA_ERR_ABORTED, undefined);
+is(HTMLVideoElement.prototype.MEDIA_ERR_NETWORK, undefined);
+is(HTMLVideoElement.prototype.MEDIA_ERR_DECODE, undefined);
+is(HTMLVideoElement.prototype.MEDIA_ERR_SRC_NOT_SUPPORTED, undefined);
+is(HTMLAudioElement.prototype.NETWORK_EMPTY, 0);
+is(HTMLAudioElement.prototype.NETWORK_IDLE, 1);
+is(HTMLAudioElement.prototype.NETWORK_LOADING, 2);
+is(HTMLAudioElement.prototype.NETWORK_NO_SOURCE, 3);
+is(HTMLAudioElement.prototype.HAVE_NOTHING, 0);
+is(HTMLAudioElement.prototype.HAVE_METADATA, 1);
+is(HTMLAudioElement.prototype.HAVE_CURRENT_DATA, 2);
+is(HTMLAudioElement.prototype.HAVE_FUTURE_DATA, 3);
+is(HTMLAudioElement.prototype.HAVE_ENOUGH_DATA, 4);
+is(HTMLAudioElement.prototype.MEDIA_ERR_ABORTED, undefined);
+is(HTMLAudioElement.prototype.MEDIA_ERR_NETWORK, undefined);
+is(HTMLAudioElement.prototype.MEDIA_ERR_DECODE, undefined);
+is(HTMLAudioElement.prototype.MEDIA_ERR_SRC_NOT_SUPPORTED, undefined);
+is(HTMLSourceElement.prototype.NETWORK_EMPTY, undefined);
+is(HTMLSourceElement.prototype.NETWORK_IDLE, undefined);
+is(HTMLSourceElement.prototype.NETWORK_LOADING, undefined);
+is(HTMLSourceElement.prototype.NETWORK_NO_SOURCE, undefined);
+is(HTMLSourceElement.prototype.HAVE_NOTHING, undefined);
+is(HTMLSourceElement.prototype.HAVE_METADATA, undefined);
+is(HTMLSourceElement.prototype.HAVE_CURRENT_DATA, undefined);
+is(HTMLSourceElement.prototype.HAVE_FUTURE_DATA, undefined);
+is(HTMLSourceElement.prototype.HAVE_ENOUGH_DATA, undefined);
+is(HTMLSourceElement.prototype.MEDIA_ERR_ABORTED, undefined);
+is(HTMLSourceElement.prototype.MEDIA_ERR_NETWORK, undefined);
+is(HTMLSourceElement.prototype.MEDIA_ERR_DECODE, undefined);
+is(HTMLSourceElement.prototype.MEDIA_ERR_SRC_NOT_SUPPORTED, undefined);
+is(MediaError.prototype.NETWORK_EMPTY, undefined);
+is(MediaError.prototype.NETWORK_IDLE, undefined);
+is(MediaError.prototype.NETWORK_LOADING, undefined);
+is(MediaError.prototype.NETWORK_NO_SOURCE, undefined);
+is(MediaError.prototype.HAVE_NOTHING, undefined);
+is(MediaError.prototype.HAVE_METADATA, undefined);
+is(MediaError.prototype.HAVE_CURRENT_DATA, undefined);
+is(MediaError.prototype.HAVE_FUTURE_DATA, undefined);
+is(MediaError.prototype.HAVE_ENOUGH_DATA, undefined);
+is(MediaError.prototype.MEDIA_ERR_ABORTED, 1);
+is(MediaError.prototype.MEDIA_ERR_NETWORK, 2);
+is(MediaError.prototype.MEDIA_ERR_DECODE, 3);
+is(MediaError.prototype.MEDIA_ERR_SRC_NOT_SUPPORTED, 4);
+ok(document.getElementsByTagName("video")[0].buffered instanceof TimeRanges, "video.buffered must be TimeRanges object");
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_controls.html b/dom/media/test/test_controls.html
new file mode 100644
index 0000000000..5c9015fdf2
--- /dev/null
+++ b/dom/media/test/test_controls.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: controls</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<video id='v1'></video><audio id='a1'></audio>
+<video id='v2' controls></video><audio id='a2' controls></audio>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var v1 = document.getElementById('v1');
+var a1 = document.getElementById('a1');
+var v2 = document.getElementById('v2');
+var a2 = document.getElementById('a2');
+ok(!v1.controls, "v1.controls should be false by default");
+ok(!a1.controls, "v1.controls should be false by default");
+ok(v2.controls, "v2.controls should be true");
+ok(a2.controls, "v2.controls should be true");
+v2.controls=false;
+a2.controls=false;
+ok(!v2.controls, "v2.controls should be false");
+ok(!a2.controls, "a2.controls should be false");
+v2.controls=true;
+a2.controls=true;
+ok(v2.controls, "v2.controls should be true");
+ok(a2.controls, "a2.controls should be true");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_cueless_webm_seek-1.html b/dom/media/test/test_cueless_webm_seek-1.html
new file mode 100644
index 0000000000..db58a89665
--- /dev/null
+++ b/dom/media/test/test_cueless_webm_seek-1.html
@@ -0,0 +1,136 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=657791
+-->
+<head>
+ <title>Test for Bug 657791</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=657791">Mozilla Bug 657791</a>
+
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+// Subset of seek tests for cueless WebMs. When random seeking (rather than just
+// in buffered ranges) is implemented for WebM, these tests can be removed and
+// the cueless WebM(s) references can be moved to the general test_seek test
+// array.
+// Test array is defined in manifest.js
+
+var manager = new MediaTestManager;
+
+// Exercise functionality as in test_seek-1
+function testWebM1(e) {
+ var v = e.target;
+ v.removeEventListener('loadeddata', testWebM1);
+
+ var startPassed = false;
+ var endPassed = false;
+ var seekFlagStart = false;
+ var seekFlagEnd = false;
+ var readonly = true;
+ var completed = false;
+
+ ok(v.buffered.length >= 1, "Should have a buffered range");
+ var halfBuffered = v.buffered.end(0) / 2;
+
+ function start() {
+ is(v.seekable.start(0), v.buffered.start(0), "Seekable start should be buffered start");
+ is(v.seekable.end(0), v.buffered.end(0), "Seekable end should be buffered end");
+ ok(!completed, "Should not be completed yet");
+ ok(!v.seeking, "seeking should default to false");
+ try {
+ v.seeking = true;
+ readonly = v.seeking === false;
+ }
+ catch(ex) {
+ readonly = "threw exception: " + ex;
+ }
+ is(readonly, true, "seeking should be readonly");
+
+ v.currentTime = halfBuffered;
+ seekFlagStart = v.seeking;
+ }
+
+ function seekStarted() {
+ ok(!completed, "should not be completed yet");
+ startPassed = true;
+ }
+
+ function seekEnded() {
+ ok(!completed, "should not be completed yet");
+ ok(Math.abs(v.currentTime - halfBuffered) < 0.1,
+ "Video currentTime should be around " + halfBuffered + ": " + v.currentTime + " (seeked)");
+ endPassed = true;
+ seekFlagEnd = v.seeking;
+ v.play();
+ }
+
+ function playbackEnded() {
+ ok(!completed, "should not be completed yet");
+
+ completed = true;
+ ok(startPassed, "seeking event");
+ ok(endPassed, "seeked event");
+ ok(seekFlagStart, "seeking flag on start should be true");
+ ok(!seekFlagEnd, "seeking flag on end should be false");
+ removeNodeAndSource(v);
+ manager.finished(v._token);
+ }
+
+ once(v, "ended", playbackEnded);
+ once(v, "seeking", seekStarted);
+ once(v, "seeked", seekEnded);
+
+ start();
+}
+
+// Fetch the media resource using XHR so we can be sure the entire
+// resource is loaded before we test buffered ranges. This ensures
+// we have deterministic behaviour.
+function fetch(url, fetched_callback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", url, true);
+ xhr.responseType = "blob";
+
+ var loaded = function (event) {
+ if (xhr.status == 200 || xhr.status == 206) {
+ // Request fulfilled. Note sometimes we get 206... Presumably because either
+ // httpd.js or Necko cached the result.
+ fetched_callback(window.URL.createObjectURL(xhr.response));
+ } else {
+ ok(false, "Fetch failed headers=" + xhr.getAllResponseHeaders());
+ }
+ };
+
+ xhr.addEventListener("load", loaded);
+ xhr.send();
+}
+
+function startTest(test, token) {
+ var onfetched = function(uri) {
+ var v = document.createElement('video');
+ v._token = token;
+ v.src = uri;
+ v.addEventListener("loadeddata", testWebM1);
+ document.body.appendChild(v);
+ }
+ manager.started(token);
+ fetch(test.name, onfetched);
+}
+
+SimpleTest.waitForExplicitFinish();
+manager.runTests(gCuelessWebMTests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_cueless_webm_seek-2.html b/dom/media/test/test_cueless_webm_seek-2.html
new file mode 100644
index 0000000000..720cc18399
--- /dev/null
+++ b/dom/media/test/test_cueless_webm_seek-2.html
@@ -0,0 +1,126 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=657791
+-->
+<head>
+ <title>Test for Bug 657791</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=657791">Mozilla Bug 657791</a>
+
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+// Subset of seek tests for cueless WebMs. When random seeking (rather than just
+// in buffered ranges) is implemented for WebM, these tests can be removed and
+// the cueless WebM(s) references can be moved to the general test_seek test
+// array.
+// Test array is defined in manifest.js
+
+var manager = new MediaTestManager;
+
+// Exercise functionality as in test_seek-2
+function testWebM2(e) {
+ var v = e.target;
+ v.removeEventListener('loadeddata', testWebM2);
+
+ var startPassed = false;
+ var endPassed = false;
+ var completed = false;
+
+ ok(v.buffered.length >= 1, "Should have a buffered range");
+ var halfBuffered = v.buffered.end(0) / 2;
+
+ function start() {
+ if (completed)
+ return;
+
+ is(v.seekable.start(0), v.buffered.start(0), "Seekable start should be buffered start");
+ is(v.seekable.end(0), v.buffered.end(0), "Seekable end should be buffered end");
+ v.currentTime=halfBuffered;
+ v.play();
+ }
+
+ function seekStarted() {
+ if (completed)
+ return;
+
+ startPassed = true;
+ }
+
+ function seekEnded() {
+ if (completed)
+ return;
+
+ endPassed = true;
+ }
+
+ function playbackEnded() {
+ if (completed)
+ return;
+
+ completed = true;
+ ok(startPassed, "send seeking event");
+ ok(endPassed, "send seeked event");
+ ok(v.ended, "Checking playback has ended");
+ ok(Math.abs(v.currentTime - v.duration) <= 0.1, "Checking currentTime at end: " + v.currentTime);
+ removeNodeAndSource(v);
+ manager.finished(v._token);
+ }
+
+ v.addEventListener("ended", playbackEnded);
+ v.addEventListener("seeking", seekStarted);
+ v.addEventListener("seeked", seekEnded);
+
+ start();
+}
+
+// Fetch the media resource using XHR so we can be sure the entire
+// resource is loaded before we test buffered ranges. This ensures
+// we have deterministic behaviour.
+function fetch(url, fetched_callback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", url, true);
+ xhr.responseType = "blob";
+
+ var loaded = function (event) {
+ if (xhr.status == 200 || xhr.status == 206) {
+ // Request fulfilled. Note sometimes we get 206... Presumably because either
+ // httpd.js or Necko cached the result.
+ fetched_callback(window.URL.createObjectURL(xhr.response));
+ } else {
+ ok(false, "Fetch failed headers=" + xhr.getAllResponseHeaders());
+ }
+ };
+
+ xhr.addEventListener("load", loaded);
+ xhr.send();
+}
+
+function startTest(test, token) {
+ var onfetched = function(uri) {
+ var v = document.createElement('video');
+ v._token = token;
+ v.src = uri;
+ v.addEventListener("loadeddata", testWebM2);
+ document.body.appendChild(v);
+ }
+ manager.started(token);
+ fetch(test.name, onfetched);
+}
+
+SimpleTest.waitForExplicitFinish();
+manager.runTests(gCuelessWebMTests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_cueless_webm_seek-3.html b/dom/media/test/test_cueless_webm_seek-3.html
new file mode 100644
index 0000000000..d6e3e50d7d
--- /dev/null
+++ b/dom/media/test/test_cueless_webm_seek-3.html
@@ -0,0 +1,120 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=657791
+-->
+<head>
+ <title>Test for Bug 657791</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=657791">Mozilla Bug 657791</a>
+
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+// Subset of seek tests for cueless WebMs. When random seeking (rather than just
+// in buffered ranges) is implemented for WebM, these tests can be removed and
+// the cueless WebM(s) references can be moved to the general test_seek test
+// array.
+// Test array is defined in manifest.js
+
+var manager = new MediaTestManager;
+
+// Exercise functionality as in test_seek-3
+function testWebM3(e) {
+ var v = e.target;
+ v.removeEventListener('loadeddata', testWebM3);
+
+ var completed = false;
+ var gotTimeupdate = false;
+
+ ok(v.buffered.length >= 1, "Should have a buffered range");
+ var halfBuffered = v.buffered.end(0) / 2;
+
+ function start() {
+ if (completed)
+ return;
+
+ is(v.seekable.start(0), v.buffered.start(0), "Seekable start should be buffered start");
+ is(v.seekable.end(0), v.buffered.end(0), "Seekable end should be buffered end");
+ v.currentTime=halfBuffered;
+ }
+
+ function timeupdate() {
+ gotTimeupdate = true;
+ v.removeEventListener("timeupdate", timeupdate);
+ }
+
+ function seekStarted() {
+ if (completed)
+ return;
+
+ v.addEventListener("timeupdate", timeupdate);
+ }
+
+ function seekEnded() {
+ if (completed)
+ return;
+
+ var t = v.currentTime;
+ ok(Math.abs(t - halfBuffered) <= 0.1, "Video currentTime should be around " + halfBuffered + ": " + t);
+ ok(gotTimeupdate, "Should have got timeupdate between seeking and seekended");
+ completed = true;
+ removeNodeAndSource(v);
+ manager.finished(v._token);
+ }
+
+ v.addEventListener("seeking", seekStarted);
+ v.addEventListener("seeked", seekEnded);
+
+ start()
+}
+
+// Fetch the media resource using XHR so we can be sure the entire
+// resource is loaded before we test buffered ranges. This ensures
+// we have deterministic behaviour.
+function fetch(url, fetched_callback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", url, true);
+ xhr.responseType = "blob";
+
+ var loaded = function (event) {
+ if (xhr.status == 200 || xhr.status == 206) {
+ // Request fulfilled. Note sometimes we get 206... Presumably because either
+ // httpd.js or Necko cached the result.
+ fetched_callback(window.URL.createObjectURL(xhr.response));
+ } else {
+ ok(false, "Fetch failed headers=" + xhr.getAllResponseHeaders());
+ }
+ };
+
+ xhr.addEventListener("load", loaded);
+ xhr.send();
+}
+
+function startTest(test, token) {
+ var onfetched = function(uri) {
+ var v = document.createElement('video');
+ v._token = token;
+ v.src = uri;
+ v.addEventListener("loadeddata", testWebM3);
+ document.body.appendChild(v);
+ }
+ manager.started(token);
+ fetch(test.name, onfetched);
+}
+
+SimpleTest.waitForExplicitFinish();
+manager.runTests(gCuelessWebMTests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_currentTime.html b/dom/media/test/test_currentTime.html
new file mode 100644
index 0000000000..b38c8c2c53
--- /dev/null
+++ b/dom/media/test/test_currentTime.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: currentTime</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<video id='v1'></video><audio id='a1'></audio>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var v1 = document.getElementById('v1');
+var a1 = document.getElementById('a1');
+is(v1.currentTime, 0.0);
+is(a1.currentTime, 0.0);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_debug_data_helpers.html b/dom/media/test/test_debug_data_helpers.html
new file mode 100644
index 0000000000..f96aafc09b
--- /dev/null
+++ b/dom/media/test/test_debug_data_helpers.html
@@ -0,0 +1,74 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Test the special debug APIs give expected data</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+<script type="text/javascript" src="manifest.js"></script>
+<script class="testbody" type="text/javascript">
+add_task(async function testMozRequestDebugInfo() {
+ let video = document.createElement("video");
+ video.src = "gizmo.mp4";
+ document.body.appendChild(video);
+ await video.play();
+ let debugData = await SpecialPowers.wrap(video).mozRequestDebugInfo();
+ // Verify various members are present and as expected.
+ ok(debugData, "Should get some debug data");
+ ok(debugData.decoder.hasAudio, "Should have audio");
+ ok(debugData.decoder.hasVideo, "Should have video");
+ is(
+ debugData.decoder.reader.videoWidth,
+ 560,
+ "Video should have expected width"
+ );
+ is(
+ debugData.decoder.reader.videoHeight,
+ 320,
+ "Video should have expected height"
+ );
+ ok(
+ debugData.decoder.stateMachine.mediaTime >= 0,
+ "Media time should be positive"
+ );
+ removeNodeAndSource(video);
+});
+
+add_task(async function testMozDebugReaderData() {
+ let video = document.createElement("video");
+ let mediaSource = new MediaSource();
+ video.src = URL.createObjectURL(mediaSource);
+ await once(mediaSource, "sourceopen");
+ const sourceBuffer = mediaSource.addSourceBuffer("video/webm");
+ let fetchResponse = await fetch("bipbop_short_vp8.webm");
+ sourceBuffer.appendBuffer(await fetchResponse.arrayBuffer());
+ await once(sourceBuffer, "updateend");
+ mediaSource.endOfStream();
+ await once(mediaSource, "sourceended");
+ document.body.appendChild(video);
+ await video.play();
+ let debugData = await SpecialPowers.wrap(mediaSource).mozDebugReaderData();
+ // Verify various members are present and as expected.
+ ok(debugData, "Should get some debug data");
+ is(debugData.reader.videoWidth, 400, "Video should have expected width");
+ is(debugData.reader.videoHeight, 300, "Video should have expected height");
+ ok(
+ debugData.demuxer.audioTrack.numSamples > 0,
+ "Audio track should have demuxed some samples"
+ );
+ ok(
+ debugData.demuxer.audioTrack.ranges.length,
+ "Audio track should have some buffered range"
+ );
+ ok(
+ debugData.demuxer.videoTrack.numSamples > 0,
+ "Video track should have demuxed some samples"
+ );
+ ok(
+ debugData.demuxer.videoTrack.ranges.length,
+ "Video track should have some buffered range"
+ );
+ removeNodeAndSource(video);
+});
+</script>
+</head>
+</html>
diff --git a/dom/media/test/test_decode_error.html b/dom/media/test/test_decode_error.html
new file mode 100644
index 0000000000..27b83cd3f9
--- /dev/null
+++ b/dom/media/test/test_decode_error.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: unknown/invalid formats raise decode error</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var manager = new MediaTestManager;
+
+function startTest(test, token) {
+ var ok = function (condition, name) {
+ SimpleTest.ok(condition, test.name + ": " + name);
+ }
+ var is = function (a, b, name) {
+ SimpleTest.is(a, b, test.name + ": " + name);
+ }
+
+ var v = document.createElement("video");
+ manager.started(token);
+ v.addEventListener("error", function (event) {
+ var el = event.currentTarget;
+ is(event.type, "error", "Expected event of type 'error'");
+ ok(el.error, "Element 'error' attr expected to have a value");
+ ok(el.error instanceof MediaError, "Element 'error' attr expected to be MediaError");
+ if (v.readyState == v.HAVE_NOTHING) {
+ is(el.error.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED, "Expected media not supported error");
+ } else {
+ is(el.error.code, MediaError.MEDIA_ERR_DECODE, "Expected a decode error");
+ }
+ ok(typeof el.error.message === 'string' || el.error.message instanceof String, "Element 'message' attr expected to be a string");
+ ok(el.error.message.length, "Element 'message' attr has content");
+ el._sawError = true;
+ manager.finished(token);
+ });
+
+ v.addEventListener("loadeddata", function () {
+ ok(false, "Unexpected loadeddata event");
+ manager.finished(token);
+ });
+
+ v.autoplay = true;
+ v.addEventListener("ended", function () {
+ ok(false, "Unexpected ended event");
+ manager.finished(token);
+ });
+
+ v.src = test.name; // implicitly starts a load.
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({
+ "set": [
+ ["media.cache_size", 40000],
+ ]
+}, beginTest);
+function beginTest() {
+ manager.runTests(gDecodeErrorTests, startTest);
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_decode_error_crossorigin.html b/dom/media/test/test_decode_error_crossorigin.html
new file mode 100644
index 0000000000..24c1430a5b
--- /dev/null
+++ b/dom/media/test/test_decode_error_crossorigin.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Invalid formats raise decode errors with default messages for CORS cross-origin media</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+const manager = new MediaTestManager;
+let gotErrSrcNotSupported = false;
+let gotErrDecode = false;
+
+function startTest(test, token) {
+ const is = function(a, b, name) {
+ SimpleTest.is(a, b, `${test.name}: ${name}`);
+ };
+ const v = document.createElement("video");
+ manager.started(token);
+ v.addEventListener("error", event => {
+ if (v.readyState == v.HAVE_NOTHING) {
+ is(v.error.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED,
+ "Expected code for a load error");
+ is(v.error.message, "Failed to open media",
+ "Expected message for a load error");
+ gotErrSrcNotSupported = true;
+ } else {
+ is(v.error.code, MediaError.MEDIA_ERR_DECODE,
+ "Expected code for a decode error");
+ is(v.error.message, "Failed to decode media",
+ "Expected message for a decode error");
+ gotErrDecode = true;
+ }
+ manager.finished(token);
+ });
+
+ v.autoplay = true;
+
+ // CORS-cross-origin URL.
+ v.src = `http://example.com/tests/dom/media/test/${test.name}`;
+}
+
+gTestPrefs.push(["media.cache_size", 40000]);
+manager.onFinished = () => {
+ ok(gotErrSrcNotSupported, "At least one test led to src-not-supported");
+ ok(gotErrDecode, "At least one test led to a decode error");
+};
+manager.runTests(gErrorTests, startTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_decoder_disable.html b/dom/media/test/test_decoder_disable.html
new file mode 100644
index 0000000000..dd0d2cc51b
--- /dev/null
+++ b/dom/media/test/test_decoder_disable.html
@@ -0,0 +1,78 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=448600
+-->
+<head>
+ <title>Test for Bug 448600</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=448600">Mozilla Bug 448600</a>
+<p id="display"></p>
+
+
+<pre id="test">
+<script type="application/javascript">
+
+function filename(uri) {
+ return uri.substr(uri.lastIndexOf("/")+1);
+}
+
+function e(id) {
+ return document.getElementById(id);
+}
+
+var gLoadError = {};
+
+gLoadError.video1 = 0;
+gLoadError.video2 = 0;
+gLoadError.video3 = 0;
+
+var gErrorCount = 0;
+
+SimpleTest.waitForExplicitFinish();
+
+function finishTest() {
+ is(e('video1').currentSrc,
+ "",
+ 'video1 currentSrc should be empty when there\'s no playable typed source children');
+ is(filename(e('video2').currentSrc),
+ filename(e('video2').src),
+ 'video2 currentSrc should match src');
+ is(filename(e('video3').currentSrc),
+ filename(e('video3').src),
+ 'video3 currentSrc should match src');
+
+ is(gLoadError.video1, 2, "Expect one error per invalid source child on video1");
+ is(gLoadError.video2, 1, "Expect one error on video2");
+ is(gLoadError.video3, 1, "Expect one error on video3");
+
+ SimpleTest.finish();
+}
+
+function videoError(event, id) {
+ gLoadError[id]++;
+ gErrorCount++;
+ if (gErrorCount >= 4) {
+ finishTest();
+ }
+}
+
+</script>
+<!-- We make the resource URIs unique to ensure that they are (re)loaded with the new disable-decoder prefs. -->
+<div id="content">
+</div>
+<script>
+function makeVideos() {
+ document.getElementById('content').innerHTML = '<video id="video1" preload="metadata"><source type="video/ogg" src="320x240.ogv?decoder_disabled=1" onerror="videoError(event, \'video1\');"/><source type="audio/wave" src="r11025_u8_c1.wav?decoder_disabled=1" id=\'s2\' onerror="videoError(event, \'video1\');"/></video><video id="video2" preload="metadata" src="320x240.ogv?decoder_disabled=2" onerror="videoError(event, \'video2\');"></video><video id="video3" preload="metadata" src="r11025_u8_c1.wav?decoder_disabled=2" onerror="videoError(event, \'video3\');"></video>';
+}
+
+SpecialPowers.pushPrefEnv({"set": [["media.ogg.enabled", false], ["media.wave.enabled", false]]}, makeVideos);
+</script>
+
+</pre>
+
+</body>
+</html>
diff --git a/dom/media/test/test_defaultMuted.html b/dom/media/test/test_defaultMuted.html
new file mode 100644
index 0000000000..77bfa3d29a
--- /dev/null
+++ b/dom/media/test/test_defaultMuted.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: defaultMuted</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="../../../dom/html/test/reflect.js"></script>
+</head>
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=706731">Mozilla Bug 706731</a>
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <video id='v1'></video><audio id='a1'></audio>
+ <video id='v2' muted></video><audio id='a2' muted></audio>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+ reflectBoolean({
+ element: document.createElement("video"),
+ attribute: { content: "muted", idl: "defaultMuted" },
+ });
+
+ reflectBoolean({
+ element: document.createElement("audio"),
+ attribute: { content: "muted", idl: "defaultMuted" },
+ });
+
+ var v1 = document.getElementById('v1');
+ var a1 = document.getElementById('a1');
+ var v2 = document.getElementById('v2');
+ var a2 = document.getElementById('a2');
+
+ // Check that muted state correspond to the default value.
+ is(v1.muted, false, "v1.muted should be false by default");
+ is(a1.muted, false, "a1.muted should be false by default");
+ is(v2.muted, true, "v2.muted should be true by default");
+ is(a2.muted, true, "a2.muted should be true by default");
+
+ // Changing defaultMuted value should not change current muted state.
+ v1.defaultMuted = true;
+ a1.defaultMuted = true;
+ v2.defaultMuted = false;
+ a2.defaultMuted = false;
+
+ is(v1.muted, false, "v1.muted should not have changed");
+ is(a1.muted, false, "a1.muted should not have changed");
+ is(v2.muted, true, "v2.muted should not have changed");
+ is(a2.muted, true, "a2.muted should not have changed");
+
+ mediaTestCleanup();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_delay_load.html b/dom/media/test/test_delay_load.html
new file mode 100644
index 0000000000..05877aa911
--- /dev/null
+++ b/dom/media/test/test_delay_load.html
@@ -0,0 +1,108 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=479711
+-->
+<head>
+ <title>Test for Bug 479711</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+ <script>
+
+ var gRegisteredElements = [];
+ var testWindows = [];
+
+ function register(v) {
+ gRegisteredElements.push(v);
+ }
+
+ function loaded() {
+ info("onload fired!");
+
+ for (var i = 0; i < gRegisteredElements.length; ++i) {
+ var v = gRegisteredElements[i];
+ ok(v.readyState >= v.HAVE_CURRENT_DATA,
+ v._name + ":" + v.id + " is not ready before onload fired (" + v.readyState + ")");
+ }
+
+ for (i=0; i<testWindows.length; ++i) {
+ testWindows[i].close();
+ }
+
+ mediaTestCleanup();
+
+ SimpleTest.finish();
+ }
+
+ addLoadEvent(loaded);
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=479711">Mozilla Bug 479711</a>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 479711 **/
+
+function createVideo(name, type, id) {
+ var v = document.createElement("video");
+ v.preload = "metadata";
+ // Make sure each video is a unique resource
+ v.src = name + "?" + id;
+ v._name = name;
+ v.id = id;
+ register(v);
+ return v;
+}
+
+var test = getPlayableVideo(gSmallTests);
+
+// Straightforward add, causing a load.
+var v = createVideo(test.name, test.type, "1");
+document.body.appendChild(v);
+
+// Load, add, then remove.
+v = createVideo(test.name, test.type, "1");
+v.load();
+document.body.appendChild(v);
+v.remove();
+
+// Load and add.
+v = createVideo(test.name, test.type, "2");
+v.load();
+document.body.appendChild(v);
+
+// Load outside of doc.
+v = createVideo(test.name, test.type, "3");
+v.load();
+
+// Open a new window for the following test. We open it here instead of in
+// the event handler to ensure that our document load event doesn't fire while
+// window.open is spinning the event loop.
+var w = window.open("", "testWindow", "width=400,height=400");
+testWindows.push(w);
+
+v = createVideo(test.name, test.type, "4");
+v.onloadstart = function(e) {
+ // Using a new window to do this is a bit annoying, but if we use an iframe here,
+ // delaying of the iframe's load event might interfere with the firing of our load event
+ // in some confusing way. So it's simpler just to use another window.
+ w.document.body.appendChild(v);
+};
+v.load(); // load started while in this document, this doc's load will block until
+ // the video's finished loading (in the other document).
+
+if (gRegisteredElements.length) {
+ SimpleTest.waitForExplicitFinish();
+} else {
+ todo(false, "No types supported");
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_duration_after_error.html b/dom/media/test/test_duration_after_error.html
new file mode 100644
index 0000000000..ad6bbe414f
--- /dev/null
+++ b/dom/media/test/test_duration_after_error.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test playback of media files that should have errors</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+function checkDuration(name, e, test) {
+ if (test.duration) {
+ ok(Math.abs(e.duration - test.duration) < 0.1,
+ name + " duration (" + e.duration + ") should be around " + test.duration);
+ } else {
+ ok(false, "Test doesn't include the duration field!")
+ }
+}
+
+function startTest(test, token) {
+ manager.started(token);
+
+ let v = document.createElement('video');
+ v._loadedMetadata = false;
+ let name = test.name;
+
+ v.onloadedmetadata = function() {
+ v.onloadedmetadata = null;
+ v._loadedMetadata = true;
+ ok(v._loadedMetadata , name + " has loaded metadata.");
+ }
+
+ v.onerror = function() {
+ v.onerror = null;
+ ok(v._loadedMetadata , name + " should load metadata before getting error.");
+ checkDuration(name, v, test);
+ manager.finished(token);
+ }
+
+ v.src = name;
+ document.body.appendChild(v);
+ v.play();
+}
+
+manager.runTests(gDurationTests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_eme_autoplay.html b/dom/media/test/test_eme_autoplay.html
new file mode 100644
index 0000000000..c8947f63e0
--- /dev/null
+++ b/dom/media/test/test_eme_autoplay.html
@@ -0,0 +1,115 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test Encrypted Media Extensions</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="https://example.com:443/tests/dom/media/test/eme.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+/* import-globals-from eme.js */
+var manager = new MediaTestManager;
+
+var EMEmanifest = [
+ {
+ name:"bipbop 10s",
+ tracks: [
+ {
+ name:"video",
+ type:"video/mp4; codecs=\"avc1.4d4015\"",
+ fragments:[ "bipbop-cenc-video-10s.mp4",
+ ]
+ }
+ ],
+ keys: {
+ "7e571d037e571d037e571d037e571d11" : "7e5733337e5733337e5733337e573311",
+ },
+ sessionType:"temporary",
+ sessionCount:1,
+ duration:10.01
+ },
+];
+
+function startTest(test, token)
+{
+ manager.started(token);
+
+ let v = document.createElement("video");
+ v.controls = true;
+ v.autoplay = true;
+
+ document.body.appendChild(v);
+
+ var eventCounts = { play: 0, playing: 0};
+ function ForbiddenEvents(e) {
+ var video = e.target;
+ ok(video.readyState >= video.HAVE_FUTURE_DATA, "Must not have received event too early");
+ is(eventCounts[e.type], 0, "event should have only be fired once");
+ eventCounts[e.type]++;
+ }
+ // Log events for debugging.
+ var events = ["suspend", "play", "canplay", "canplaythrough", "loadstart", "loadedmetadata",
+ "loadeddata", "playing", "ended", "error", "stalled", "emptied", "abort",
+ "waiting", "pause", "durationchange", "seeking", "seeked"];
+ function logEvent(e) {
+ info("got " + e.type + " event");
+ }
+ events.forEach(function(e) {
+ v.addEventListener(e, logEvent);
+ });
+ v.addEventListener("play", ForbiddenEvents);
+ v.addEventListener("playing", ForbiddenEvents);
+
+ var gotWaitingForKey = 0;
+
+ let waitForKey = new EMEPromise;
+ v.addEventListener("waitingforkey", function() {
+ gotWaitingForKey += 1;
+ waitForKey.resolve();
+ });
+
+ v.addEventListener("loadedmetadata", function() {
+ ok(SpecialPowers.do_lookupGetter(v, "isEncrypted").apply(v),
+ TimeStamp(token) + " isEncrypted should be true");
+ is(v.isEncrypted, undefined, "isEncrypted should not be accessible from content");
+ });
+
+ let finish = new EMEPromise;
+ v.addEventListener("playing", function() {
+ ok(true, TimeStamp(token) + " got playing event");
+ // We expect only one waitingForKey as we delay until all sessions are ready.
+ // I.e. one waitingForKey should be fired, after which, we process all sessions,
+ // so it should not be possible to be blocked by a key after that point.
+ ok(gotWaitingForKey == 1, "Expected number 1 wait, got: " + gotWaitingForKey);
+
+ finish.resolve();
+ });
+
+ Promise.all([
+ LoadInitData(v, test, token),
+ CreateAndSetMediaKeys(v, test, token),
+ LoadTest(test, v, token, false /* do not call endOfStream */),
+ waitForKey.promise])
+ .then(values => {
+ let initData = values[0];
+ return ProcessInitData(v, test, token, initData);
+ })
+ .then(sessions => {
+ Log(token, "Updated all sessions, loading complete");
+ finish.promise.then(() => CloseSessions(v, sessions));
+ return finish.promise;
+ })
+ .catch(reason => ok(false, reason))
+ .then(() => manager.finished(token));
+}
+
+SimpleTest.waitForExplicitFinish();
+manager.runTests(EMEmanifest, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_eme_canvas_blocked.html b/dom/media/test/test_eme_canvas_blocked.html
new file mode 100644
index 0000000000..22986493c4
--- /dev/null
+++ b/dom/media/test/test_eme_canvas_blocked.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test Encrypted Media Extensions</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="eme.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var manager = new MediaTestManager();
+
+function startTest(test, token) {
+ manager.started(token);
+
+ let v = document.createElement("video");
+ v.preload = "auto"; // Required due to "canplay" not firing for MSE unless we do this.
+
+ var p1 = new EMEPromise();
+ v.addEventListener("loadeddata", function(ev) {
+ var video = ev.target;
+ var canvas = document.createElement("canvas");
+ canvas.width = video.videoWidth;
+ canvas.height = video.videoHeight;
+ document.body.appendChild(canvas);
+ var ctx = canvas.getContext("2d");
+ ctx.drawImage(video, 0, 0);
+ if (canvas.width || canvas.height) {
+ let pixels = ctx.getImageData(0, 0, canvas.width, canvas.height).data;
+ for (let byte of pixels) {
+ if (byte != 0) {
+ ok(false, "Should not draw EME video to canvas");
+ // We don't need a log for each pixel, break after 1st failure.
+ break;
+ }
+ }
+ }
+ p1.resolve();
+ });
+
+ let p2 = SetupEME(v, test, token);
+
+ Promise.all([p1.promise, p2])
+ .catch(reason => ok(false, reason))
+ .then(() => {
+ CleanUpMedia(v);
+ manager.finished(token);
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+manager.runTests(gEMETests, startTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_eme_createMediaKeys_iframes.html b/dom/media/test/test_eme_createMediaKeys_iframes.html
new file mode 100644
index 0000000000..6fe3dc9809
--- /dev/null
+++ b/dom/media/test/test_eme_createMediaKeys_iframes.html
@@ -0,0 +1,192 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test creation of MediaKeys in iframes</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="eme.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody">
+// Helper functions.
+
+// We take navigator explicitly as an argument to avoid ambiguity in fetching
+// it. This is to avoid issues with the following
+// ```
+// iframe.contentWindow.createMediaKeys = createMediaKeys;
+// await iframe.contentWindow.createMediaKeys();
+// ```
+// If we don't pass a navigator, and just use `navigator` in the function, this
+// ends up being equivalent to
+// ```
+// iframe.contentWindow.createMediaKeys = createMediaKeys;
+// await iframe.contentWindow.createMediaKeys(window.navigator);
+// ```
+// i.e. the function will use the navigator from the global window for the top
+// browsing context, not the iframe's. This would result in the tests not
+// correctly testing within the iframe.
+async function createMediaKeys(aNavigator) {
+ const clearKeyOptions = [
+ {
+ initDataTypes: ["webm"],
+ videoCapabilities: [{ contentType: 'video/webm; codecs="vp9"' }],
+ },
+ ];
+
+ let access = await aNavigator.requestMediaKeySystemAccess(
+ "org.w3.clearkey",
+ clearKeyOptions
+ );
+
+ return access.createMediaKeys();
+}
+// End helper functions.
+
+// These tests check that the following work using different iframe combinations
+// - navigator.requestMediaKeySystem(...) successfully grants access.
+// - the resulting MediaKeySystemAccess object's createMediaKeys() creates
+// MediaKeys as expected.
+
+// Same origin iframe, using src attribute, wait for onload.
+add_task(async () => {
+ info(
+ "Starting same origin iframe, using src attribute, wait for onload test"
+ );
+ let iframe = document.createElement("iframe");
+ let iframeLoadPromise = new Promise(r => {
+ iframe.onload = r;
+ });
+ iframe.src = "file_eme_createMediaKeys.html";
+ document.body.appendChild(iframe);
+ await iframeLoadPromise;
+ info("iframe loaded");
+
+ // Setup our handler for when the iframe messages to tell us if it
+ // created MediaKeys or not.
+ let iframeMessagePromise = new Promise(r => {
+ window.onmessage = message => {
+ is(
+ message.data,
+ "successCreatingMediaKeys",
+ "iframe should have posted us a message saying keys were successfully created"
+ );
+ r();
+ };
+ });
+ // Post a message to the iframe to ask it to try and create media keys.
+ iframe.contentWindow.postMessage("", "*");
+ // Wait until we've got a message back from our iframe.
+ await iframeMessagePromise;
+});
+
+// Same origin iframe, call via JS, wait for onload.
+add_task(async () => {
+ info("Starting same origin iframe, call via JS, wait for onload test");
+ let iframe = document.createElement("iframe");
+ let iframeLoadPromise = new Promise(r => {
+ iframe.onload = r;
+ });
+ iframe.src = ""; // No src iframes are same origin.
+ document.body.appendChild(iframe);
+ await iframeLoadPromise;
+ info("iframe loaded");
+
+ try {
+ iframe.contentWindow.createMediaKeys = createMediaKeys;
+ let mediaKeys = await iframe.contentWindow.createMediaKeys(
+ iframe.contentWindow.navigator
+ );
+ ok(mediaKeys, "Should get media keys");
+ } catch (e) {
+ ok(
+ false,
+ `Should not get any errors while trying to get media keys, got ${e}`
+ );
+ }
+});
+
+// Same origin iframe, call via JS, *do not* wait for onload.
+//
+// Note, sites shouldn't do this, because
+// https://bugzilla.mozilla.org/show_bug.cgi?id=543435
+// means not waiting for onload results in weird behavior, however
+// https://bugzilla.mozilla.org/show_bug.cgi?id=1675360
+// shows sites doing this in the wild because historically this worked in
+// Firefox.
+//
+// Breaking this test case isn't necessarily against any specifications
+// I'm (bryce) aware of, but it will probably break site compat, so be really
+// sure you want to before doing so.
+add_task(async () => {
+ info(
+ "Starting same origin iframe, call via JS, *do not* wait for onload test"
+ );
+ let iframe = document.createElement("iframe");
+ let iframeLoadPromise = new Promise(r => {
+ iframe.onload = r;
+ });
+ iframe.src = ""; // No src iframes are same origin.
+ document.body.appendChild(iframe);
+ info("iframe appended (we're not waiting for load)");
+
+ try {
+ iframe.contentWindow.createMediaKeys = createMediaKeys;
+ let mediaKeys = await iframe.contentWindow.createMediaKeys(
+ iframe.contentWindow.navigator
+ );
+ ok(mediaKeys, "Should get media keys");
+
+ // We await the load to see if they keys persist through the load.
+ // This could fail if gecko internally associates the keys with the
+ // about:blank page that is replaced by the load.
+ await iframeLoadPromise;
+ ok(mediaKeys, "Media keys should still exist after the load");
+ } catch (e) {
+ ok(
+ false,
+ `Should not get any errors while trying to get media keys, got ${e}`
+ );
+ }
+});
+
+// Different origin iframe, using src attribute, wait for onload
+add_task(async () => {
+ info(
+ "Starting different origin iframe, using src attribute, wait for onload test"
+ );
+ let iframe = document.createElement("iframe");
+ let iframeLoadPromise = new Promise(r => {
+ iframe.onload = r;
+ });
+ // Make our iframe cross origin (see build/pgo/server-locations.txt for more
+ // info the url used).
+ iframe.src =
+ "https://w3c-test.org:443/tests/dom/media/test/file_eme_createMediaKeys.html";
+ iframe.allow = "encrypted-media";
+ document.body.appendChild(iframe);
+ await iframeLoadPromise;
+ info("iframe loaded");
+
+ // Setup our handler for when the iframe messages to tell us if it
+ // created MediaKeys or not.
+ let iframeMessagePromise = new Promise(r => {
+ window.onmessage = message => {
+ is(
+ message.data,
+ "successCreatingMediaKeys",
+ "iframe should have posted us a message saying keys were successfully created"
+ );
+ r();
+ };
+ });
+ // Post a message to the iframe to ask it to try and create media keys.
+ iframe.contentWindow.postMessage("", "*");
+ // Wait until we've got a message back from our iframe.
+ await iframeMessagePromise;
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_eme_detach_media_keys.html b/dom/media/test/test_eme_detach_media_keys.html
new file mode 100644
index 0000000000..69b812032a
--- /dev/null
+++ b/dom/media/test/test_eme_detach_media_keys.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test Encrypted Media Extensions</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="eme.js"></script>
+</head>
+<body>
+<pre id="test">
+<video id="v" controls></video>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+function createAndSet() {
+ return new Promise(function(resolve, reject) {
+ var m;
+ navigator.requestMediaKeySystemAccess(CLEARKEY_KEYSYSTEM, gCencMediaKeySystemConfig)
+ .then(function (access) {
+ return access.createMediaKeys();
+ })
+ .then(function (mediaKeys) {
+ m = mediaKeys;
+ return document.getElementById("v").setMediaKeys(mediaKeys);
+ })
+ .then(function() {
+ resolve(m);
+ });
+ }
+)}
+
+var m1,m2;
+
+// Test that if we create and set two MediaKeys on one video element,
+// that if the first MediaKeys we set on the media elemnt is still usable
+// after the second MediaKeys has been set on the media element.
+
+createAndSet().then((m) => {
+ m1 = m; // Stash MediaKeys.
+ return createAndSet();
+})
+.then((m) => {
+ m2 = m;
+ is(document.getElementById("v").mediaKeys, m2, "Should have set MediaKeys on media element");
+ ok(document.getElementById("v").mediaKeys != m1, "First MediaKeys should no longer be set on media element");
+ var s = m1.createSession("temporary");
+ return s.generateRequest("webm", StringToArrayBuffer(atob('YAYeAX5Hfod+V9ANHtANHg==')));
+})
+.then(() => {
+ ok(true, "Was able to generateRequest using second CDM");
+ SimpleTest.finish();
+}, () => {
+ ok(false, "Was *NOT* able to generateRequest using second CDM");
+ SimpleTest.finish();
+});
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_eme_detach_reattach_same_mediakeys_during_playback.html b/dom/media/test/test_eme_detach_reattach_same_mediakeys_during_playback.html
new file mode 100644
index 0000000000..e47fae5891
--- /dev/null
+++ b/dom/media/test/test_eme_detach_reattach_same_mediakeys_during_playback.html
@@ -0,0 +1,141 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test Encrypted Media Extensions</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="https://example.com:443/tests/dom/media/test/eme.js"></script>
+</head>
+<body>
+<pre id="test">
+<video id="v" controls></video>
+<script class="testbody" type="text/javascript">
+/* import-globals-from eme.js */
+var manager = new MediaTestManager;
+
+var EMEmanifest = [
+ {
+ name:"bipbop 10s",
+ tracks: [
+ {
+ name:"video",
+ type:"video/mp4; codecs=\"avc1.4d4015\"",
+ fragments:[ "bipbop-cenc-video-10s.mp4",
+ ]
+ }
+ ],
+ keys: {
+ "7e571d037e571d037e571d037e571d11" : "7e5733337e5733337e5733337e573311",
+ },
+ sessionType:"temporary",
+ sessionCount:1,
+ duration:10.01
+ },
+];
+
+function sleep(time) {
+ return new Promise((resolve) => setTimeout(resolve, time));
+}
+
+// To check if playback can be blocked and resumed correctly after
+// detaching original mediakeys and reattach it back.
+function startTest(test, token)
+{
+ manager.started(token);
+
+ var mk_ori;
+ let finish = new EMEPromise;
+
+ let v = document.getElementById("v");
+ let sessions = [];
+ function onSessionCreated(session) {
+ sessions.push(session);
+ }
+
+ function closeSessions() {
+ let p = new EMEPromise;
+ Promise.all(sessions.map(s => s.close()))
+ .then(p.resolve, p.reject);
+ return p.promise;
+ }
+
+ function setMediaKeysToElement(mk, solve, reject) {
+ v.setMediaKeys(mk).then(solve, reject);
+ }
+
+ function ReattachOriMediaKeys() {
+ function onOriMediaKeysSetOK() {
+ ok(true, TimeStamp(token) + " (ENCRYPTED) Set original MediaKeys back OK!");
+ }
+ function onOriMediaKeysSetFailed() {
+ ok(false, " Failed to set original mediakeys back.");
+ }
+
+ function onCanPlayAgain(ev) {
+ Promise.all([closeSessions()])
+ .then(() => {
+ ok(true, " (ENCRYPTED) Playback can be resumed.");
+ manager.finished(token);
+ }, () => {
+ ok(false, TimeStamp(token) + " Sessions are closed incorrectly.");
+ manager.finished(token);
+ });
+ }
+
+ once(v, "canplay", onCanPlayAgain);
+ setMediaKeysToElement(mk_ori, onOriMediaKeysSetOK, onOriMediaKeysSetFailed)
+ }
+
+ function triggerSeek() {
+ v.currentTime = v.duration / 2;
+ }
+
+ function onCanPlay(ev) {
+ function onSetMediaKeysToNullOK() {
+ ok(true, TimeStamp(token) + " Set MediaKeys to null. OK!");
+
+ triggerSeek();
+
+ SimpleTest.requestFlakyTimeout("To reattach mediakeys back again in 5s.");
+ sleep(5000).then(ReattachOriMediaKeys);
+ }
+ function onSetMediaKeysToNullFailed() {
+ ok(false, TimeStamp(token) + " Set MediaKeys to null. FAILED!");
+ }
+
+ SimpleTest.requestFlakyTimeout("To detach mediakeys after receiving 'canplay' event in 2s");
+ sleep(2000).then(() => {
+ setMediaKeysToElement(null, onSetMediaKeysToNullOK, onSetMediaKeysToNullFailed);
+ });
+ }
+
+ once(v, "canplay", onCanPlay);
+
+ var p1 = LoadInitData(v, test, token);
+ var p2 = CreateAndSetMediaKeys(v, test, token);
+ var p3 = LoadTest(test, v, token);
+ Promise.all([p1, p2, p3])
+ .then(values => {
+ let initData = values[0];
+ // stash the mediakeys
+ mk_ori = v.mediaKeys;
+ initData.map(ev => {
+ let session = v.mediaKeys.createSession();
+ onSessionCreated(session);
+ MakeRequest(test, token, ev, session);
+ });
+ })
+ .then(() => {
+ return finish.promise;
+ })
+ .catch(reason => ok(false, reason))
+}
+
+SimpleTest.waitForExplicitFinish();
+manager.runTests(EMEmanifest, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_eme_getstatusforpolicy.html b/dom/media/test/test_eme_getstatusforpolicy.html
new file mode 100644
index 0000000000..2993dac7a6
--- /dev/null
+++ b/dom/media/test/test_eme_getstatusforpolicy.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test Encrypted Media Extensions</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="eme.js"></script>
+</head>
+<body>
+<pre id="test">
+<video id="v" controls></video>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+function createMediaKeysAndSet() {
+ return navigator.requestMediaKeySystemAccess(CLEARKEY_KEYSYSTEM, gCencMediaKeySystemConfig)
+ .then(function (access) {
+ return access.createMediaKeys();
+ })
+ .then(function (mediaKeys) {
+ document.getElementById("v").setMediaKeys(mediaKeys);
+ return mediaKeys;
+ });
+}
+
+function test() {
+ createMediaKeysAndSet()
+ .then((m) => {
+ let video = document.getElementById("v");
+ is(video.mediaKeys, m, "Should have set MediaKeys on media element");
+ // getStatusForPolicy() is not suppored by ClearKey key system.
+ // The promise will always be rejected with NotSupportedError.
+ return video.mediaKeys.getStatusForPolicy({minHdcpVersion: "hdcp-2.0"});
+ })
+ .then((mediaKeyStatus) => {
+ ok(false, "Promise of getStatusForPolicy should not be resolved with clearkey key system");
+ })
+ // Promise rejected with NotSupportedError as expected.
+ .catch(reason => is("NotSupportedError", reason.name,
+ "Promise should be rejected with NotSupportedError."))
+ .then(() => SimpleTest.finish());
+}
+
+SpecialPowers.pushPrefEnv({"set":
+ [
+ ["media.eme.hdcp-policy-check.enabled", true],
+ ]
+ }, test);
+
+</script>
+</pre>
+</body>
+</html> \ No newline at end of file
diff --git a/dom/media/test/test_eme_initDataTypes.html b/dom/media/test/test_eme_initDataTypes.html
new file mode 100644
index 0000000000..587e6fc161
--- /dev/null
+++ b/dom/media/test/test_eme_initDataTypes.html
@@ -0,0 +1,130 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test Encrypted Media Extensions</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="eme.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var tests = [
+ {
+ name: "One keyId",
+ initDataType: 'keyids',
+ initData: '{"kids":["LwVHf8JLtPrv2GUXFW2v_A"]}',
+ expectedRequest: '{"kids":["LwVHf8JLtPrv2GUXFW2v_A"],"type":"temporary"}',
+ sessionType: 'temporary',
+ expectPass: true,
+ },
+ {
+ name: "Two keyIds",
+ initDataType: 'keyids',
+ initData: '{"kids":["LwVHf8JLtPrv2GUXFW2v_A", "0DdtU9od-Bh5L3xbv0Xf_A"]}',
+ expectedRequest: '{"kids":["LwVHf8JLtPrv2GUXFW2v_A","0DdtU9od-Bh5L3xbv0Xf_A"],"type":"temporary"}',
+ sessionType: 'temporary',
+ expectPass: true,
+ },
+ {
+ name: "Two keyIds, temporary session",
+ initDataType: 'keyids',
+ initData: '{"type":"temporary", "kids":["LwVHf8JLtPrv2GUXFW2v_A", "0DdtU9od-Bh5L3xbv0Xf_A"]}',
+ expectedRequest: '{"kids":["LwVHf8JLtPrv2GUXFW2v_A","0DdtU9od-Bh5L3xbv0Xf_A"],"type":"temporary"}',
+ sessionType: 'temporary',
+ expectPass: true,
+ },
+ {
+ name: "Two keyIds, persistent session, type before kids",
+ initDataType: 'keyids',
+ initData: '{"type":"persistent-license", "kids":["LwVHf8JLtPrv2GUXFW2v_A", "0DdtU9od-Bh5L3xbv0Xf_A"]}',
+ expectedRequest: '{"kids":["LwVHf8JLtPrv2GUXFW2v_A","0DdtU9od-Bh5L3xbv0Xf_A"],"type":"persistent-license"}',
+ sessionType: 'persistent-license',
+ expectPass: false,
+ },
+ {
+ name: "Invalid keyId",
+ initDataType: 'keyids',
+ initData: '{"kids":["0"]}',
+ sessionType: 'temporary',
+ expectPass: false,
+ },
+ {
+ name: "Empty keyId",
+ initDataType: 'keyids',
+ initData: '{"kids":[""]}',
+ sessionType: 'temporary',
+ expectPass: false,
+ },
+ {
+ name: "Invalid initData",
+ initDataType: 'keyids',
+ initData: 'invalid initData',
+ sessionType: 'temporary',
+ expectPass: false,
+ },
+ {
+ name: "'webm' initDataType",
+ initDataType: 'webm',
+ initData: 'YAYeAX5Hfod+V9ANHtANHg==',
+ expectedRequest: '{"kids":["YAYeAX5Hfod-V9ANHtANHg"],"type":"temporary"}',
+ sessionType: 'temporary',
+ expectPass: true,
+ },
+ {
+ name: "'webm' initDataType with non 16 byte keyid",
+ initDataType: 'webm',
+ initData: 'YAYeAX5Hfod',
+ expectedRequest: '{\"kids\":[\"YAYeAX5Hfoc\"],\"type\":\"temporary\"}',
+ sessionType: 'temporary',
+ expectPass: true,
+ },
+];
+
+function PrepareInitData(initDataType, initData)
+{
+ if (initDataType == "keyids") {
+ return new TextEncoder().encode(initData);
+ } else if (initDataType == "webm") {
+ return StringToArrayBuffer(atob(initData));
+ }
+}
+
+function Test(test) {
+ return new Promise(function(resolve, reject) {
+ var configs = [{
+ initDataTypes: [test.initDataType],
+ videoCapabilities: [{contentType: 'video/mp4' }],
+ }];
+ navigator.requestMediaKeySystemAccess('org.w3.clearkey', configs)
+ .then((access) => access.createMediaKeys())
+ .then((mediaKeys) => {
+ var session = mediaKeys.createSession(test.sessionType);
+ session.addEventListener("message", function(event) {
+ is(event.messageType, "license-request", "'" + test.name + "' MediaKeyMessage type should be license-request.");
+ var text = new TextDecoder().decode(event.message);
+ is(text, test.expectedRequest, "'" + test.name + "' got expected response.");
+ is(text == test.expectedRequest, test.expectPass,
+ "'" + test.name + "' expected to " + (test.expectPass ? "pass" : "fail"));
+ resolve();
+ });
+ var initData = PrepareInitData(test.initDataType, test.initData);
+ return session.generateRequest(test.initDataType, initData);
+ }
+ ).catch((x) => {
+ ok(!test.expectPass, "'" + test.name + "' expected to fail.");
+ resolve();
+ });
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+Promise.all(tests.map(Test)).then(function() {
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_eme_missing_pssh.html b/dom/media/test/test_eme_missing_pssh.html
new file mode 100644
index 0000000000..29f77d021a
--- /dev/null
+++ b/dom/media/test/test_eme_missing_pssh.html
@@ -0,0 +1,92 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Test Encrypted Media Extensions</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="eme.js"></script>
+ </head>
+ <body>
+ <audio controls id="audio"></audio>
+ <pre id="test">
+ <script class="testbody" type="text/javascript">
+
+ // Tests that a fragmented MP4 file without a PSSH, but with valid encrypted
+ // tracks with valid TENC boxes, is able to load with EME.
+ // We setup MSE before starting up EME, so that we exercise the "waiting for
+ // cdm" step in the MediaDecoderStateMachine.
+
+ SimpleTest.waitForExplicitFinish();
+
+ var pssh = [
+ 0x00, 0x00, 0x00, 0x00,
+ 0x70, 0x73, 0x73, 0x68, // BMFF box header (76 bytes, 'pssh')
+ 0x01, 0x00, 0x00, 0x00, // Full box header (version = 1, flags = 0)
+ 0x10, 0x77, 0xef, 0xec, 0xc0, 0xb2, 0x4d, 0x02, // SystemID
+ 0xac, 0xe3, 0x3c, 0x1e, 0x52, 0xe2, 0xfb, 0x4b,
+ 0x00, 0x00, 0x00, 0x01, // KID_count (1)
+ 0x2f, 0xef, 0x8a, 0xd8, 0x12, 0xdf, 0x42, 0x97,
+ 0x83, 0xe9, 0xbf, 0x6e, 0x5e, 0x49, 0x3e, 0x53,
+ 0x00, 0x00, 0x00, 0x00 // Size of Data (0)
+ ];
+
+ var audio = document.getElementById("audio");
+
+ function LoadEME() {
+ var options = [{
+ initDataType: 'cenc',
+ audioType: 'audio/mp4; codecs="mp4a.40.2"',
+ }];
+ navigator.requestMediaKeySystemAccess("org.w3.clearkey", options)
+ .then((keySystemAccess) => {
+ return keySystemAccess.createMediaKeys();
+ }, bail("Failed to request key system access."))
+
+ .then((mediaKeys) => {
+ audio.setMediaKeys(mediaKeys);
+ var session = mediaKeys.createSession();
+ once(session, "message", (message) => {
+ is(message.messageType, 'license-request', "Expected a license-request");
+ var license = new TextEncoder().encode(JSON.stringify({
+ 'keys': [{
+ 'kty':'oct',
+ 'kid':'L--K2BLfQpeD6b9uXkk-Uw',
+ 'k':HexToBase64('7f412f0575f44f718259beef56ec7771')
+ }],
+ 'type': 'temporary'
+ }));
+ session.update(license);
+ });
+ session.generateRequest('cenc', new Uint8Array(pssh));
+ });
+ }
+
+ function DownloadMedia(url, type, mediaSource) {
+ return new Promise(function(resolve, reject) {
+ var sourceBuffer = mediaSource.addSourceBuffer(type);
+ fetchWithXHR(url, (response) => {
+ once(sourceBuffer, "updateend", resolve);
+ sourceBuffer.appendBuffer(new Uint8Array(response));
+ });
+ });
+ }
+
+ function LoadMSE() {
+ var ms = new MediaSource();
+ audio.src = URL.createObjectURL(ms);
+
+ once(ms, "sourceopen", ()=>{
+ DownloadMedia('short-audio-fragmented-cenc-without-pssh.mp4', 'audio/mp4; codecs="mp4a.40.2"', ms)
+ .then(() => { ms.endOfStream(); LoadEME();});
+ });
+
+ audio.addEventListener("loadeddata", SimpleTest.finish);
+ }
+
+ LoadMSE();
+
+ </script>
+ </pre>
+ </body>
+</html>
diff --git a/dom/media/test/test_eme_non_mse_fails.html b/dom/media/test/test_eme_non_mse_fails.html
new file mode 100644
index 0000000000..6ff17d59ff
--- /dev/null
+++ b/dom/media/test/test_eme_non_mse_fails.html
@@ -0,0 +1,95 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1131392 - Test that EME does not work for non-MSE media</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="eme.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+/* import-globals-from eme.js */
+var manager = new MediaTestManager;
+
+function DoSetMediaKeys(v, test, token)
+{
+ var options = [{
+ initDataTypes: ["cenc"],
+ audioCapabilities: [{contentType: test.audioType}],
+ videoCapabilities: [{contentType: test.videoType}],
+ }];
+
+ return navigator.requestMediaKeySystemAccess(CLEARKEY_KEYSYSTEM, options)
+
+ .then(function(keySystemAccess) {
+ return keySystemAccess.createMediaKeys();
+ })
+
+ .catch(function() {
+ ok(false, token + " was not expecting failure (yet)");
+ })
+
+ .then(function(mediaKeys) {
+ return v.setMediaKeys(mediaKeys);
+ });
+}
+
+function TestSetMediaKeys(test, token)
+{
+ manager.started(token);
+
+ var v = document.createElement("video");
+
+ v.addEventListener("encrypted", function() {
+ ok(false, token + " should not fire encrypted event");
+ });
+
+ var loadedMetadata = false;
+ v.addEventListener("loadedmetadata", function() {
+ loadedMetadata = true;
+ });
+
+ v.addEventListener("error", function() {
+ ok(true, token + " expected error event");
+ ok(loadedMetadata, token + " expected loadedmetadata to have fired");
+ manager.finished(token);
+ });
+
+ v.src = test.name;
+}
+
+function TestSetSrc(test, token)
+{
+ manager.started(token);
+
+ var v = document.createElement("video");
+ v.addEventListener("error", function(err) {
+ ok(true, token + " got error setting src on video element, as expected");
+ manager.finished(token);
+ });
+
+ DoSetMediaKeys(v, test, token)
+
+ .then(function() {
+ v.src = test.name;
+ })
+
+ .catch(function() {
+ ok(false, token + " got error setting media keys");
+ });
+}
+
+function startTest(test, token)
+{
+ TestSetMediaKeys(test, token + "_setMediaKeys");
+ TestSetSrc(test, token + "_setSrc");
+}
+
+SimpleTest.waitForExplicitFinish();
+manager.runTests(gEMENonMSEFailTests, startTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_eme_playback.html b/dom/media/test/test_eme_playback.html
new file mode 100644
index 0000000000..bcfa058e34
--- /dev/null
+++ b/dom/media/test/test_eme_playback.html
@@ -0,0 +1,188 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test Encrypted Media Extensions</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="https://example.com:443/tests/dom/media/test/eme.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+/* import-globals-from eme.js */
+var manager = new MediaTestManager;
+
+function ArrayBuffersEqual(a, b) {
+ if (a.byteLength != b.byteLength) {
+ return false;
+ }
+ var ua = new Uint8Array(a);
+ var ub = new Uint8Array(b);
+ for (var i = 0; i < ua.length; i++) {
+ if (ua[i] != ub[i]) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function KeysChangeFunc(session, keys, token) {
+ session.keyIdsReceived = [];
+ for (var keyid in keys) {
+ Log(token, "Set " + keyid + " to false in session[" + session.sessionId + "].keyIdsReceived");
+ session.keyIdsReceived[keyid] = false;
+ }
+ return function(ev) {
+ var s = ev.target;
+ s.gotKeysChanged = true;
+
+ var keyList = [];
+ var valueList = [];
+ var map = s.keyStatuses;
+
+ // Test that accessing keys not known to the CDM has expected behaviour.
+ var absentKey = new Uint8Array([0xeb, 0xdd, 0x62, 0xf1, 0x68, 0x14, 0xd2, 0x7b,
+ 0x68, 0xef, 0x12, 0x2a, 0xfc, 0xe4, 0xae, 0x3c]);
+ is(map.has(absentKey), false, "Shouldn't have a key that's not in the media");
+ is(map.get(absentKey), undefined, "Unknown keys should undefined status");
+
+ // Verify known keys have expected status.
+ for (let [key, val] of map.entries()) {
+ is(key.constructor, ArrayBuffer, "keyId should be ArrayBuffer");
+ ok(map.has(key), "MediaKeyStatusMap.has() should work.");
+ is(map.get(key), val, "MediaKeyStatusMap.get() should work.");
+ keyList.push(key);
+ valueList.push(val);
+ is(val, "usable", token + ": key status should be usable");
+ var kid = Base64ToHex(window.btoa(ArrayBufferToString(key)));
+ ok(kid in s.keyIdsReceived, TimeStamp(token) + " session[" + s.sessionId + "].keyIdsReceived contained " + kid + " as expected.");
+ s.keyIdsReceived[kid] = true;
+ }
+
+ var index = 0;
+ for (var keyId of map.keys()) {
+ ok(ArrayBuffersEqual(keyId, keyList[index]), "MediaKeyStatusMap.keys() should correspond to entries");
+ index++;
+ }
+ index = 0;
+ for (let val of map.values()) {
+ is(val, valueList[index], "MediaKeyStatusMap.values() should correspond to entries");
+ index++;
+ }
+ }
+}
+
+function startTest(test, token)
+{
+ manager.started(token);
+
+ var sessions = [];
+
+ function onSessionCreated(session) {
+ sessions.push(session);
+ session.addEventListener("keystatuseschange", KeysChangeFunc(session, test.keys, token));
+
+ session.numKeystatuseschangeEvents = 0;
+ session.numOnkeystatuseschangeEvents = 0;
+
+ session.addEventListener("keystatuseschange", function() {
+ session.numKeystatuseschangeEvents += 1;
+ });
+ session.onkeystatuseschange = function() {
+ session.numOnkeystatuseschangeEvents += 1;
+ };
+
+ session.numMessageEvents = 0;
+ session.numOnMessageEvents = 0;
+ session.addEventListener("message", function() {
+ session.numMessageEvents += 1;
+ });
+ session.onmessage = function() {
+ session.numOnMessageEvents += 1;
+ };
+ }
+
+ let v = document.createElement("video");
+ document.body.appendChild(v);
+
+ var gotEncrypted = 0;
+ let finish = new EMEPromise;
+
+ v.addEventListener("encrypted", function(ev) {
+ gotEncrypted += 1;
+ });
+
+ v.addEventListener("loadedmetadata", function() {
+ ok(SpecialPowers.do_lookupGetter(v, "isEncrypted").apply(v),
+ TimeStamp(token) + " isEncrypted should be true");
+ is(v.isEncrypted, undefined, "isEncrypted should not be accessible from content");
+ });
+
+ v.addEventListener("ended", function(ev) {
+ ok(true, TimeStamp(token) + " got ended event");
+
+ is(gotEncrypted, test.sessionCount,
+ TimeStamp(token) + " encrypted events expected: " + test.sessionCount
+ + ", actual: " + gotEncrypted);
+
+ ok(Math.abs(test.duration - v.duration) < 0.1,
+ TimeStamp(token) + " Duration of video should be corrrect");
+ ok(Math.abs(test.duration - v.currentTime) < 0.1,
+ TimeStamp(token) + " Current time should be same as duration");
+
+ // Verify all sessions had all keys went sent to the CDM usable, and thus
+ // that we received keystatuseschange event(s).
+ is(sessions.length, test.sessionCount, TimeStamp(token) + " should have "
+ + test.sessionCount
+ + " session" + (test.sessionCount === 1 ? "" : "s"));
+ var keyIdsReceived = [];
+ for (var keyid in test.keys) { keyIdsReceived[keyid] = false; }
+ for (var i = 0; i < sessions.length; i++) {
+ var session = sessions[i];
+ ok(session.gotKeysChanged,
+ TimeStamp(token) + " session[" + session.sessionId
+ + "] should have received at least one keychange event");
+ for (let kid in session.keyIdsReceived) {
+ Log(token, "session[" + session.sessionId + "] key " + kid + " = " + (session.keyIdsReceived[kid] ? "true" : "false"));
+ if (session.keyIdsReceived[kid]) { keyIdsReceived[kid] = true; }
+ }
+ ok(session.numKeystatuseschangeEvents > 0, TimeStamp(token) + " should get key status changes");
+ is(session.numKeystatuseschangeEvents, session.numOnkeystatuseschangeEvents,
+ TimeStamp(token) + " should have as many keystatuseschange event listener calls as event handler calls.");
+
+ ok(session.numMessageEvents > 0, TimeStamp(token) + " should get message events");
+ is(session.numMessageEvents, session.numOnMessageEvents,
+ TimeStamp(token) + " should have as many message event listener calls as event handler calls.");
+ }
+ for (let kid in keyIdsReceived) {
+ ok(keyIdsReceived[kid], TimeStamp(token) + " key with id " + kid + " was usable as expected");
+ }
+
+ CloseSessions(v, sessions).then(finish.resolve, finish.reject);
+ });
+
+ Promise.all([
+ LoadInitData(v, test, token),
+ CreateAndSetMediaKeys(v, test, token),
+ LoadTest(test, v, token)])
+ .then(values => {
+ v.play();
+ let initData = values[0];
+ initData.map(ev => {
+ let session = v.mediaKeys.createSession();
+ onSessionCreated(session);
+ MakeRequest(test, token, ev, session);
+ });
+ return finish.promise;
+ })
+ .catch(reason => ok(false, reason))
+ .then(() => manager.finished(token));
+}
+
+SimpleTest.waitForExplicitFinish();
+manager.runTests(gEMETests, startTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_eme_protection_query.html b/dom/media/test/test_eme_protection_query.html
new file mode 100644
index 0000000000..8bf97d8100
--- /dev/null
+++ b/dom/media/test/test_eme_protection_query.html
@@ -0,0 +1,250 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test Encrypted Media Extensions - Protection Query</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="eme.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+// Tests in this file check that output protection queries are performed and
+// handled correctly. This is done by using a special clear key key system that
+// emits key status to track protection status.
+
+// Special key system used for these tests.
+const kClearKeyWithProtectionQuery =
+ "org.mozilla.clearkey_with_protection_query";
+const kTestFile = "bipbop-cenc-video-10s.mp4";
+const kTestMimeType = 'video/mp4; codecs="avc1.4d4015"';
+const kTestKeyId = "7e571d037e571d037e571d037e571d11"; // Hex representation
+const kTestKey = "7e5733337e5733337e5733337e573311";
+
+// This is the special key-id used by the mozilla clearkey CDM to signal
+// protection query status. As hex it is "6f75747075742d70726f74656374696f6e",
+// the hex translates to ascii "output-protection".
+const kProtectionQueryKeyIdString = "output-protection";
+
+// Options for requestMediaKeySystemAccess
+const kKeySystemOptions = [
+ {
+ initDataTypes: ["cenc"],
+ videoCapabilities: [{ contentType: kTestMimeType }],
+ },
+];
+
+// Helper to setup EME on `video`.
+// @param video the HTMLMediaElement to configure EME on.
+// @returns a media key session for the video. Callers can use this to
+// configure the `onkeystatuseschange` event handler for each test. Callers
+// *should not* configure other aspects of the session as this helper already
+// does so.
+async function setupEme(video) {
+ // Start setting up EME.
+ let access = await navigator.requestMediaKeySystemAccess(
+ kClearKeyWithProtectionQuery,
+ kKeySystemOptions
+ );
+ let mediaKeys = await access.createMediaKeys();
+ await video.setMediaKeys(mediaKeys);
+
+ let session = video.mediaKeys.createSession();
+
+ video.onencrypted = async encryptedEvent => {
+ session.onmessage = messageEvent => {
+ // Handle license messages. Hard code the license because we always test
+ // with the same file and we know what the license should be.
+ const license = {
+ keys: [
+ {
+ kty: "oct",
+ kid: HexToBase64(kTestKeyId),
+ k: HexToBase64(kTestKey),
+ },
+ ],
+ type: "temporary",
+ };
+
+ const encodedLicense = new TextEncoder().encode(JSON.stringify(license));
+
+ session.update(encodedLicense);
+ };
+
+ session.generateRequest(
+ encryptedEvent.initDataType,
+ encryptedEvent.initData
+ );
+ };
+
+ return session;
+}
+
+// Helper to setup MSE media on `video`.
+// @param video the HTMLMediaElement to configure MSE on.
+async function setupMse(video) {
+ const mediaSource = new MediaSource();
+ video.src = URL.createObjectURL(mediaSource);
+ await once(mediaSource, "sourceopen");
+ const sourceBuffer = mediaSource.addSourceBuffer("video/mp4");
+ let fetchResponse = await fetch(kTestFile);
+ sourceBuffer.appendBuffer(await fetchResponse.arrayBuffer());
+ await once(sourceBuffer, "updateend");
+ mediaSource.endOfStream();
+ await once(mediaSource, "sourceended");
+}
+
+// Helper to create a video element and append it to the page.
+function createAndAppendVideo() {
+ const video = document.createElement("video");
+ video.id = "video";
+ // Loop in case tests run slowly, we want video to keep playing until we
+ // get expected events.
+ video.loop = true;
+ document.body.appendChild(video);
+ return video;
+}
+
+// Helper to remove a video from the page.
+function removeVideo() {
+ let video = document.getElementById("video");
+ CleanUpMedia(video);
+}
+
+// Helper to get the status for the kProtectionQueryKeyIdString key id. A
+// session can (and will) have other keys with their own status, but we want
+// to check this special key to find the protection query status.
+function getKeyStatusForProtectionKeyId(session) {
+ for (let [keyId, status] of session.keyStatuses) {
+ if (ArrayBufferToString(keyId) == kProtectionQueryKeyIdString) {
+ return status;
+ }
+ }
+ return null;
+}
+
+async function getDisplayMedia() {
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ return navigator.mediaDevices.getDisplayMedia();
+}
+
+// Tests playing encrypted media, starting a screen capture during playback,
+// then stopping the capture while playback continues.
+async function testProtectionQueryWithCaptureDuringVideo() {
+ let video = createAndAppendVideo();
+
+ // Configure the video and start it playing. KeyId should be usable (not restricted).
+ let session = await setupEme(video);
+ let keyStatusChangedPromise1 = new Promise(
+ resolve =>
+ (session.onkeystatuseschange = () => {
+ // We may get status changes prior to kProtectionQueryKeyIdString changing,
+ // ensure we wait for the first kProtectionQueryKeyIdString change.
+ if (getKeyStatusForProtectionKeyId(session)) {
+ resolve();
+ }
+ })
+ );
+ await setupMse(video);
+ await Promise.all([video.play(), keyStatusChangedPromise1]);
+ is(
+ getKeyStatusForProtectionKeyId(session),
+ "usable",
+ "Should be usable as capture hasn't started"
+ );
+
+ let keyStatusChangedPromise2 = new Promise(
+ resolve => (session.onkeystatuseschange = resolve)
+ );
+ let [displayMediaStream] = await Promise.all([
+ // Start a screen capture, this should restrict output.
+ getDisplayMedia(),
+ keyStatusChangedPromise2,
+ ]);
+ is(
+ getKeyStatusForProtectionKeyId(session),
+ "output-restricted",
+ "Should be output-restricted as capture is happening"
+ );
+
+ // Stop the screen capture, output should be usable again.
+ let keyStatusChangedPromise3 = new Promise(
+ resolve => (session.onkeystatuseschange = resolve)
+ );
+ displayMediaStream.getTracks().forEach(track => track.stop());
+ displayMediaStream = null;
+ await keyStatusChangedPromise3;
+ is(
+ getKeyStatusForProtectionKeyId(session),
+ "usable",
+ "Should be usable as capture has stopped"
+ );
+
+ removeVideo();
+}
+
+// Tests starting a screen capture, then starting encrypted playback, then
+// stopping the screen capture while encrypted playback continues.
+async function testProtectionQueryWithCaptureStartingBeforeVideo() {
+ // Start capture before setting up video.
+ let displayMediaStream = await getDisplayMedia();
+
+ let video = createAndAppendVideo();
+
+ // Configure the video and start it playing. KeyId should be restricted already.
+ let session = await setupEme(video);
+ let keyStatusChangedPromise1 = new Promise(
+ resolve =>
+ (session.onkeystatuseschange = () => {
+ // We may get status changes prior to kProtectionQueryKeyIdString changing,
+ // ensure we wait for the first kProtectionQueryKeyIdString change. In
+ // rare cases the first protection status can be "usable" due to racing
+ // between playback and the machinery that detects WebRTC capture. To
+ // avoid this, wait for the first 'output-restricted' notification,
+ // which will either be the first event, or will quickly follow 'usable'.
+ if (getKeyStatusForProtectionKeyId(session) == "output-restricted") {
+ resolve();
+ }
+ })
+ );
+ await setupMse(video);
+ await Promise.all([video.play(), keyStatusChangedPromise1]);
+ is(
+ getKeyStatusForProtectionKeyId(session),
+ "output-restricted",
+ "Should be restricted as capture is happening"
+ );
+
+ // Stop the screen capture, output should be usable again.
+ let keyStatusChangedPromise2 = new Promise(
+ resolve => (session.onkeystatuseschange = resolve)
+ );
+ displayMediaStream.getTracks().forEach(track => track.stop());
+ displayMediaStream = null;
+ await keyStatusChangedPromise2;
+ is(
+ getKeyStatusForProtectionKeyId(session),
+ "usable",
+ "Should be usable as capture has stopped"
+ );
+
+ removeVideo();
+}
+
+add_task(async function setupEnvironment() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // Need test key systems for test key system.
+ ["media.clearkey.test-key-systems.enabled", true],
+ // Need relaxed navigator permissions for getDisplayMedia.
+ ["media.navigator.permission.disabled", true],
+ ],
+ });
+});
+add_task(testProtectionQueryWithCaptureDuringVideo);
+add_task(testProtectionQueryWithCaptureStartingBeforeVideo);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_eme_pssh_in_moof.html b/dom/media/test/test_eme_pssh_in_moof.html
new file mode 100644
index 0000000000..d1965be844
--- /dev/null
+++ b/dom/media/test/test_eme_pssh_in_moof.html
@@ -0,0 +1,141 @@
+<!DOCTYPE HTML>
+<html>
+
+ <head>
+ <title>Test Encrypted Media Extensions</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="eme.js"></script>
+ </head>
+
+ <body>
+ <script class="testbody" type="text/javascript">
+ let manager = new MediaTestManager;
+
+ let psshTests = [
+ { name: "bear-640x360-cenc-key-rotation",
+ tracks : [
+ {
+ name: "video",
+ type: "video/mp4; codecs=\"avc1.64000d\"",
+ fragments: ["bear-640x360-v_frag-cenc-key_rotation.mp4"],
+ initDatas: 6
+ },
+ {
+ name: "audio",
+ type: "audio/mp4; codecs=\"mp4a.40.2\"",
+ fragments: ["bear-640x360-a_frag-cenc-key_rotation.mp4"],
+ initDatas: 7
+ }
+ ],
+ keys : {
+ "30313233343536373839303132333435" :
+ "ebdd62f16814d27b68ef122afce4ae3c"
+ }
+ },
+ { name: "bipbop-clearkey-keyrotation-clear-lead",
+ tracks : [
+ {
+ name: "video",
+ type: "video/mp4; codecs=\"avc1.4d4015\"",
+ fragments: ["bipbop-clearkey-keyrotation-clear-lead-video.mp4"],
+ initDatas: 3
+ },
+ {
+ name: "audio",
+ type: "audio/mp4; codecs=\"mp4a.40.2\"",
+ fragments: ["bipbop-clearkey-keyrotation-clear-lead-audio.mp4"],
+ initDatas: 3
+ }
+ ],
+ keys : {
+ "00112233445566778899aabbccddeeff" :
+ "00112233445566778899aabbccddeeff",
+ "112233445566778899aabbccddeeff00" :
+ "112233445566778899aabbccddeeff00"
+ }
+ }
+ ];
+
+ // Specialized create media keys function, since the one in eme.js relies
+ // on listening for encrypted events, and we want to manage those
+ // ourselves within this test.
+ async function createAndSetMediaKeys(video, test, token) {
+ function streamType(type) {
+ var x = test.tracks.find(o => o.name == type);
+ return x ? x.type : undefined;
+ }
+
+ var configuration = { initDataTypes: ["cenc"] };
+ if (streamType("video")) {
+ configuration.videoCapabilities = [{contentType: streamType("video")}];
+ }
+ if (streamType("audio")) {
+ configuration.audioCapabilities = [{contentType: streamType("audio")}];
+ }
+ let keySystemAccess = await navigator.requestMediaKeySystemAccess(
+ "org.w3.clearkey", [configuration]);
+ let mediaKeys = await keySystemAccess.createMediaKeys();
+ return video.setMediaKeys(mediaKeys);
+ }
+
+ function startTest(test, token) {
+ manager.started(token);
+ let video = document.createElement("video");
+ document.body.appendChild(video);
+
+ let encryptedEventCount = 0;
+
+ let initDatas = new Map();
+
+ function handleEncrypted(event) {
+ // We get one 'encrypted' event for every run of contiguous PSSH boxes
+ // in each stream. Note that some of the PSSH boxes contained in the
+ // "bear" streams aren't in the Common Open PSSH box format, so our
+ // ClearKey CDM will reject those license requests. Some of the init
+ // data is also repeated.
+ encryptedEventCount++;
+
+ let hexStr = StringToHex(new TextDecoder().decode(event.initData));
+ if (initDatas.has(hexStr)) {
+ // Already have a session for this.
+ return;
+ }
+
+ let initData = new Uint8Array(event.initData);
+ is(event.initDataType, "cenc", "'encrypted' event should have 'cenc' " +
+ "initDataType");
+ Log(token, "encrypted event => len=" + initData.length + " " + hexStr);
+ let session = event.target.mediaKeys.createSession();
+ initDatas.set(hexStr, session);
+ session.addEventListener("message", e => {
+ e.target.update(
+ GenerateClearKeyLicense(e.message, test.keys));
+ });
+
+ session.generateRequest(event.initDataType, event.initData);
+ }
+
+ video.addEventListener("encrypted", handleEncrypted);
+
+ video.addEventListener("ended", () => {
+ let expectedEncryptedEvents =
+ test.tracks.reduce((sum, track) => sum += track.initDatas, 0);
+ is(encryptedEventCount, expectedEncryptedEvents,
+ "Should get one 'encrypted' event per run of contiguous PSSH " +
+ "boxes in media.");
+ manager.finished(token);
+ });
+
+ video.autoplay = true;
+
+ createAndSetMediaKeys(video, test, token)
+ .then(() => LoadTest(test, video, token))
+ }
+
+ manager.runTests(psshTests, startTest);
+
+ </script>
+ </body>
+</html>
diff --git a/dom/media/test/test_eme_requestKeySystemAccess.html b/dom/media/test/test_eme_requestKeySystemAccess.html
new file mode 100644
index 0000000000..b044fe8c84
--- /dev/null
+++ b/dom/media/test/test_eme_requestKeySystemAccess.html
@@ -0,0 +1,477 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test Encrypted Media Extensions</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="eme.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+const SUPPORTED_LABEL = "pass label";
+
+function ValidateConfig(name, expected, observed) {
+ info("ValidateConfig " + name);
+ info("expected cfg=" + JSON.stringify(expected));
+ info("observed cfg=" + JSON.stringify(observed));
+
+ is(observed.label, expected.label, name + " label should match");
+ if (expected.initDataTypes) {
+ ok(expected.initDataTypes.every((element, index, array) => observed.initDataTypes.includes(element)), name + " initDataTypes should match.");
+ }
+
+ if (expected.audioCapabilities) {
+ ok(expected.audioCapabilities.length == 1, "Test function can only handle one capability.");
+ ok(observed.audioCapabilities.length == 1, "Test function can only handle one capability.");
+ is(observed.audioCapabilities[0].contentType, expected.audioCapabilities[0].contentType, name + " audioCapabilities should match.");
+ }
+ if (typeof expected.videoCapabilities !== 'undefined') {
+ info("expected.videoCapabilities=" + expected.videoCapabilities);
+ dump("expected.videoCapabilities=" + expected.videoCapabilities + "\n");
+ ok(expected.videoCapabilities.length == 1, "Test function can only handle one capability.");
+ ok(observed.videoCapabilities.length == 1, "Test function can only handle one capability.");
+ is(observed.videoCapabilities[0].contentType, expected.videoCapabilities[0].contentType, name + " videoCapabilities should match.");
+ }
+ if (expected.sessionTypes) {
+ is(expected.sessionTypes.length, observed.sessionTypes.length, "Should have expected number of sessionTypes");
+ for (var i = 0; i < expected.sessionTypes.length; i++) {
+ is(expected[i], observed[i], "Session type " + i + " should match");
+ }
+ }
+}
+
+function Test(test) {
+ var name = "'" + test.name + "'";
+ return new Promise(function(resolve, reject) {
+ var p;
+ if (test.options) {
+ var keySystem = (test.keySystem !== undefined) ? test.keySystem : CLEARKEY_KEYSYSTEM;
+ p = navigator.requestMediaKeySystemAccess(keySystem, test.options);
+ } else {
+ p = navigator.requestMediaKeySystemAccess(keySystem);
+ }
+ p.then(
+ function(keySystemAccess) {
+ ok(test.shouldPass, name + " passed and was expected to " + (test.shouldPass ? "pass" : "fail"));
+ is(keySystemAccess.keySystem, CLEARKEY_KEYSYSTEM, "CDM keySystem should be in MediaKeySystemAccess.keySystem");
+ ValidateConfig(name, test.expectedConfig, keySystemAccess.getConfiguration());
+ resolve();
+ },
+ function(ex) {
+ if (test.shouldPass) {
+ info(name + " failed: " + ex);
+ }
+ ok(!test.shouldPass, name + " failed and was expected to " + (test.shouldPass ? "pass" : "fail"));
+ resolve();
+ });
+ });
+}
+
+var tests = [
+ {
+ name: 'Empty keySystem string',
+ keySystem: '',
+ options: [
+ {
+ initDataTypes: ['cenc'],
+ videoCapabilities: [{contentType: 'video/mp4'}],
+ }
+ ],
+ shouldPass: false,
+ },
+ {
+ name: 'Empty options specified',
+ options: [ ],
+ shouldPass: false,
+ },
+ {
+ name: 'Undefined options',
+ shouldPass: false,
+ },
+ {
+ name: 'Basic MP4 cenc',
+ options: [
+ {
+ label: SUPPORTED_LABEL,
+ initDataTypes: ['cenc'],
+ audioCapabilities: [{contentType: 'audio/mp4'}],
+ videoCapabilities: [{contentType: 'video/mp4'}],
+ }
+ ],
+ expectedConfig: {
+ label: SUPPORTED_LABEL,
+ initDataTypes: ['cenc'],
+ audioCapabilities: [{contentType: 'audio/mp4'}],
+ videoCapabilities: [{contentType: 'video/mp4'}],
+ },
+ shouldPass: true,
+ },
+ {
+ name: 'Invalid keysystem failure',
+ keySystem: 'bogusKeySystem',
+ options: [
+ {
+ initDataTypes: ['cenc'],
+ videoCapabilities: [{contentType: 'video/mp4'}],
+ }
+ ],
+ shouldPass: false,
+ },
+ {
+ name: 'Invalid initDataType',
+ options: [
+ {
+ initDataTypes: ['bogus'],
+ audioCapabilities: [{contentType: 'audio/mp4'}],
+ }
+ ],
+ shouldPass: false,
+ },
+ {
+ name: 'Valid initDataType after invalid',
+ options: [
+ {
+ label: SUPPORTED_LABEL,
+ initDataTypes: ['bogus', 'invalid', 'cenc'],
+ audioCapabilities: [{contentType: 'audio/mp4'}],
+ }
+ ],
+ expectedConfig: {
+ label: SUPPORTED_LABEL,
+ initDataTypes: ['cenc'],
+ audioCapabilities: [{contentType: 'audio/mp4'}],
+ },
+ shouldPass: true,
+ },
+ {
+ name: 'Invalid videoType',
+ options: [
+ {
+ initDataTypes: ['cenc'],
+ videoCapabilities: [{contentType: 'video/bogus'}],
+ }
+ ],
+ shouldPass: false,
+ },
+ {
+ name: 'Invalid distinctiveIdentifier fails',
+ options: [
+ {
+ initDataTypes: ['cenc'],
+ videoCapabilities: [{contentType: 'video/mp4'}],
+ distinctiveIdentifier: 'bogus',
+ persistentState: 'bogus',
+ }
+ ],
+ shouldPass: false,
+ },
+ {
+ name: 'distinctiveIdentifier is prohibited for ClearKey',
+ options: [
+ {
+ initDataTypes: ['cenc'],
+ videoCapabilities: [{contentType: 'video/mp4'}],
+ distinctiveIdentifier: 'required',
+ }
+ ],
+ shouldPass: false,
+ },
+ {
+ name: 'Invalid persistentState fails',
+ options: [
+ {
+ initDataTypes: ['cenc'],
+ videoCapabilities: [{contentType: 'video/mp4'}],
+ persistentState: 'bogus',
+ }
+ ],
+ shouldPass: false,
+ },
+ {
+ name: 'Invalid robustness unsupported',
+ options: [
+ {
+ initDataTypes: ['cenc'],
+ videoCapabilities: [{contentType: 'video/mp4', robustness: 'very much so'}],
+ }
+ ],
+ shouldPass: false,
+ },
+ {
+ name: 'Unexpected config entry should be ignored',
+ options: [
+ {
+ label: SUPPORTED_LABEL,
+ initDataTypes: ['cenc'],
+ videoCapabilities: [{contentType: 'video/mp4'}],
+ unexpectedEntry: 'this should be ignored',
+ }
+ ],
+ expectedConfig: {
+ label: SUPPORTED_LABEL,
+ initDataTypes: ['cenc'],
+ videoCapabilities: [{contentType: 'video/mp4'}],
+ },
+ shouldPass: true,
+ },
+ {
+ name: 'Invalid option followed by valid',
+ options: [
+ {
+ label: "this config should not be supported",
+ initDataTypes: ['bogus'],
+ },
+ {
+ label: SUPPORTED_LABEL,
+ initDataTypes: ['cenc'],
+ videoCapabilities: [{contentType: 'video/mp4'}],
+ }
+ ],
+ expectedConfig: {
+ label: SUPPORTED_LABEL,
+ initDataTypes: ['cenc'],
+ videoCapabilities: [{contentType: 'video/mp4'}],
+ },
+ shouldPass: true,
+ },
+ {
+ name: 'Persistent-license should not be supported by ClearKey',
+ options: [
+ {
+ initDataTypes: ['cenc'],
+ videoCapabilities: [{contentType: 'video/mp4'}],
+ sessionTypes: ['persistent-license'],
+ persistentState: 'optional',
+ }
+ ],
+ shouldPass: false,
+ },
+ {
+ name: 'Persistent-usage-record should not be supported by ClearKey',
+ options: [
+ {
+ initDataTypes: ['cenc'],
+ videoCapabilities: [{contentType: 'video/mp4'}],
+ sessionTypes: ['persistent-usage-record'],
+ persistentState: 'optional',
+ }
+ ],
+ shouldPass: false,
+ },
+ {
+ name: 'MP4 audio container',
+ options: [
+ {
+ label: SUPPORTED_LABEL,
+ initDataTypes: ['cenc'],
+ audioCapabilities: [{contentType: 'audio/mp4'}],
+ }
+ ],
+ expectedConfig: {
+ label: SUPPORTED_LABEL,
+ initDataTypes: ['cenc'],
+ audioCapabilities: [{contentType: 'audio/mp4'}],
+ },
+ shouldPass: true,
+ },
+ {
+ name: 'MP4 audio container with AAC-LC',
+ options: [
+ {
+ label: SUPPORTED_LABEL,
+ initDataTypes: ['cenc'],
+ audioCapabilities: [{contentType: 'audio/mp4; codecs="mp4a.40.2"'}],
+ }
+ ],
+ expectedConfig: {
+ label: SUPPORTED_LABEL,
+ initDataTypes: ['cenc'],
+ audioCapabilities: [{contentType: 'audio/mp4; codecs="mp4a.40.2"'}],
+ },
+ shouldPass: true,
+ },
+ {
+ name: 'MP4 audio container with invalid codecs',
+ options: [
+ {
+ initDataTypes: ['cenc'],
+ audioCapabilities: [{contentType: 'audio/mp4; codecs="bogus"'}],
+ }
+ ],
+ shouldPass: false,
+ },
+ {
+ name: 'MP4 audio container with mp3 is unsupported',
+ options: [
+ {
+ initDataTypes: ['cenc'],
+ audioCapabilities: [{contentType: 'audio/mp4; codecs="mp3"'}],
+ }
+ ],
+ shouldPass: false,
+ },
+ {
+ name: 'MP4 video container type with an mp3 codec is unsupported',
+ options: [
+ {
+ initDataTypes: ['cenc'],
+ videoCapabilities: [{contentType: 'video/mp4; codecs="mp3"'}],
+ }
+ ],
+ shouldPass: false,
+ },
+ {
+ name: 'MP4 audio container type with a video codec is unsupported',
+ options: [
+ {
+ initDataTypes: ['cenc'],
+ audioCapabilities: [{contentType: 'audio/mp4; codecs="avc1.42E01E"'}],
+ }
+ ],
+ shouldPass: false,
+ },
+ {
+ name: 'MP4 video container with constrained baseline h.264',
+ options: [
+ {
+ label: SUPPORTED_LABEL,
+ initDataTypes: ['cenc'],
+ videoCapabilities: [{contentType: 'video/mp4; codecs="avc1.42E01E"'}],
+ }
+ ],
+ expectedConfig: {
+ label: SUPPORTED_LABEL,
+ initDataTypes: ['cenc'],
+ videoCapabilities: [{contentType: 'video/mp4; codecs="avc1.42E01E"'}],
+ },
+ shouldPass: true,
+ },
+ {
+ name: 'MP4 video container with invalid codecs',
+ options: [
+ {
+ initDataTypes: ['cenc'],
+ videoCapabilities: [{contentType: 'video/mp4; codecs="bogus"'}],
+ }
+ ],
+ shouldPass: false,
+ },
+ {
+ name: 'MP4 video container with both audio and video codec type in videoType',
+ options: [
+ {
+ initDataTypes: ['cenc'],
+ videoCapabilities: [{contentType: 'video/mp4; codecs="avc1.42E01E,mp4a.40.2"'}],
+ }
+ ],
+ shouldPass: false,
+ },
+ {
+ name: 'MP4 audio and video type both specified',
+ options: [
+ {
+ label: SUPPORTED_LABEL,
+ initDataTypes: ['cenc'],
+ videoCapabilities: [{contentType: 'video/mp4; codecs="avc1.42E01E"'}],
+ audioCapabilities: [{contentType: 'audio/mp4; codecs="mp4a.40.2"'}],
+ }
+ ],
+ expectedConfig: {
+ label: SUPPORTED_LABEL,
+ initDataTypes: ['cenc'],
+ videoCapabilities: [{contentType: 'video/mp4; codecs="avc1.42E01E"'}],
+ audioCapabilities: [{contentType: 'audio/mp4; codecs="mp4a.40.2"'}],
+ },
+ shouldPass: true,
+ },
+ {
+ name: 'Basic WebM video',
+ options: [
+ {
+ label: SUPPORTED_LABEL,
+ initDataTypes: ['webm'],
+ videoCapabilities: [{contentType: 'video/webm'}],
+ }
+ ],
+ expectedConfig: {
+ label: SUPPORTED_LABEL,
+ initDataTypes: ['webm'],
+ videoCapabilities: [{contentType: 'video/webm'}],
+ },
+ shouldPass: true,
+ },
+ {
+ name: 'Basic WebM audio',
+ options: [
+ {
+ label: SUPPORTED_LABEL,
+ initDataTypes: ['webm'],
+ audioCapabilities: [{contentType: 'audio/webm'}],
+ }
+ ],
+ expectedConfig: {
+ label: SUPPORTED_LABEL,
+ initDataTypes: ['webm'],
+ audioCapabilities: [{contentType: 'audio/webm'}],
+ },
+ shouldPass: true,
+ },
+ {
+ name: 'Webm with Vorbis audio and VP8 video.',
+ options: [
+ {
+ label: SUPPORTED_LABEL,
+ initDataTypes: ['webm'],
+ videoCapabilities: [{contentType: 'video/webm;codecs="vp8"'}],
+ audioCapabilities: [{contentType: 'audio/webm;codecs="vorbis"'}],
+ }
+ ],
+ expectedConfig: {
+ label: SUPPORTED_LABEL,
+ initDataTypes: ['webm'],
+ videoCapabilities: [{contentType: 'video/webm;codecs="vp8"'}],
+ audioCapabilities: [{contentType: 'audio/webm;codecs="vorbis"'}],
+ },
+ shouldPass: true,
+ },
+ {
+ name: 'Webm with Vorbis audio and VP9 video.',
+ options: [
+ {
+ label: SUPPORTED_LABEL,
+ initDataTypes: ['webm'],
+ videoCapabilities: [{contentType: 'video/webm;codecs="vp9"'}],
+ audioCapabilities: [{contentType: 'audio/webm;codecs="vorbis"'}],
+ }
+ ],
+ expectedConfig: {
+ label: SUPPORTED_LABEL,
+ initDataTypes: ['webm'],
+ videoCapabilities: [{contentType: 'video/webm;codecs="vp9"'}],
+ audioCapabilities: [{contentType: 'audio/webm;codecs="vorbis"'}],
+ },
+ shouldPass: true,
+ },
+ {
+ name: 'Webm with bogus video.',
+ options: [
+ {
+ initDataTypes: ['webm'],
+ videoCapabilities: [{contentType: 'video/webm;codecs="bogus"'}],
+ }
+ ],
+ shouldPass: false,
+ },
+];
+
+SimpleTest.waitForExplicitFinish();
+Promise.all(tests.map(Test)).then(function() {
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_eme_requestMediaKeySystemAccess_with_app_approval.html b/dom/media/test/test_eme_requestMediaKeySystemAccess_with_app_approval.html
new file mode 100644
index 0000000000..ceba459dae
--- /dev/null
+++ b/dom/media/test/test_eme_requestMediaKeySystemAccess_with_app_approval.html
@@ -0,0 +1,202 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test Encrypted Media Extensions access can be gated by application</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="eme.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// These test cases should be used to make a request to
+// requestMediaKeySystemAccess and have the following members:
+// name: a name describing the test.
+// askAppApproval: used to set prefs such so that Gecko will ask for app
+// approval for EME if true, or not if false.
+// appApproves: used to set prefs to simulate app approval of permission
+// request, true if the app approves the request, false if not.
+// expectedKeySystemAccess: true if we expect to be granted key system access,
+// false if not.
+const testCases = [
+ {
+ name: "Don't check for app approval",
+ askAppApproval: false,
+ expectedKeySystemAccess: true,
+ },
+ {
+ name: "Check for app approval and app denies request",
+ askAppApproval: true,
+ appApproves: false,
+ expectedKeySystemAccess: false,
+ },
+ {
+ name: "Check for app approval and app allows request",
+ askAppApproval: true,
+ appApproves: true,
+ expectedKeySystemAccess: true,
+ },
+];
+
+// Options for requestMediaKeySystemAccess that are expected to work.
+const options = [{
+ initDataTypes: ['webm'],
+ audioCapabilities: [
+ { contentType: 'audio/webm; codecs="opus"' },
+ ],
+ videoCapabilities: [
+ { contentType: 'video/webm; codecs="vp8"' }
+ ]
+}];
+
+async function setTestPrefs({askAppApproval, appApproves}) {
+ if (!askAppApproval) {
+ // Test doesn't want app approval, set pref so we don't ask and unset prefs
+ // used to determine permission response as we don't need them.
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.eme.require-app-approval", false]],
+ clear: [
+ ["media.eme.require-app-approval.prompt.testing"],
+ ["media.eme.require-app-approval.prompt.testing.allow"],
+ ]
+ });
+ return;
+ }
+
+ // Test wants app approval, and will approve deny requests per appApproces
+ // value, set prefs accordingly.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.eme.require-app-approval", true],
+ ["media.eme.require-app-approval.prompt.testing", true],
+ ["media.eme.require-app-approval.prompt.testing.allow", appApproves],
+ ],
+ });
+}
+
+// Run a test case that makes a single requestMediaKeySystemAccess call. The
+// outcome of such a test run should depend on the test case's setting of
+// preferences controlling the eme app approval.
+async function testSingleRequest(testCase) {
+ await setTestPrefs(testCase);
+
+ try {
+ await navigator.requestMediaKeySystemAccess(CLEARKEY_KEYSYSTEM, options);
+ ok(testCase.expectedKeySystemAccess,
+ `testSingleRequest ${testCase.name}: allowed media key system access.`);
+ } catch(e) {
+ is(e.name,
+ "NotSupportedError",
+ "Should get NotSupportedError when request is blocked.");
+ is(e.message,
+ "The application embedding this user agent has blocked MediaKeySystemAccess",
+ "Should get blocked error message.");
+ ok(!testCase.expectedKeySystemAccess,
+ `testSingleRequest ${testCase.name}: denied media key system access.`);
+ }
+}
+
+// Run a test case that, but using invalid arguments for
+// requestMediaKeySystemAccess. Because we expect the args to be checked
+// before requesting app approval, this test ensures that we always fail when
+// using bad args, regardless of the app approval prefs set.
+async function testRequestWithInvalidArgs(testCase) {
+ const badOptions = [{
+ initDataTypes: ['badType'],
+ audioCapabilities: [
+ { contentType: 'audio/webm; codecs="notACodec"' },
+ ],
+ videoCapabilities: [
+ { contentType: 'video/webm; codecs="notACodec"' }
+ ]
+ }];
+
+ await setTestPrefs(testCase);
+
+ // Check that calls with a bad key system fail.
+ try {
+ await navigator.requestMediaKeySystemAccess("BadKeySystem", options);
+ ok(false,
+ `testRequestWithInvalidArgs ${testCase.name}: should not get access when using bad key system.`);
+ } catch(e) {
+ is(e.name,
+ "NotSupportedError",
+ "Should get NotSupportedError using bad key system.");
+ is(e.message,
+ "Key system is unsupported",
+ "Should get not supported key system error message.");
+ }
+
+ // Check that calls with the bad options fail.
+ try {
+ await navigator.requestMediaKeySystemAccess(CLEARKEY_KEYSYSTEM, badOptions);
+ ok(false,
+ `testRequestWithInvalidArgs ${testCase.name}: should not get access when using bad options.`);
+ } catch(e) {
+ is(e.name,
+ "NotSupportedError",
+ "Should get NotSupportedError using bad options.");
+ is(e.message,
+ "Key system configuration is not supported",
+ "Should get not supported config error message.");
+ }
+}
+
+// Run a test case and make multiple requests with the same case. Check that
+// all requests are resolved with the expected outcome.
+async function testMultipleRequests(testCase) {
+ // Number of requests to concurrently make.
+ const NUM_REQUESTS = 5;
+
+ await setTestPrefs(testCase);
+
+ let resolveHandler = () => {
+ ok(testCase.expectedKeySystemAccess,
+ `testMultipleRequests ${testCase.name}: allowed media key system access.`);
+ }
+
+ let rejectHandler = e => {
+ is(e.name,
+ "NotSupportedError",
+ "Should get NotSupportedError when request is blocked.");
+ is(e.message,
+ "The application embedding this user agent has blocked MediaKeySystemAccess",
+ "Should get blocked error message.");
+ ok(!testCase.expectedKeySystemAccess,
+ `testMultipleRequests ${testCase.name}: denied media key system access.`);
+ }
+
+ let accessPromises = [];
+ for(let i = 0; i < NUM_REQUESTS; i++) {
+ // Request access then chain to our resolve and reject handlers. The
+ // handlers assert test state then resolve the promise chain. Ensuring the
+ // chain is always resolved allows us to correctly await all outstanding
+ // requests -- otherwise rejects short circuit the Promise.all call below.
+ let accessPromise = navigator.requestMediaKeySystemAccess(CLEARKEY_KEYSYSTEM, options)
+ .then(resolveHandler, rejectHandler);
+ accessPromises.push(accessPromise);
+ }
+ // Wait for all promises to be resolved. If not, we'll time out. Because
+ // our reject handler chains back into a resolved promise, this should wait
+ // for all requests to be serviced, even when requestMediaKeySystemAccess's
+ // promise is rejected.
+ await Promise.all(accessPromises);
+}
+
+// The tests rely on prefs being set, so run them in sequence. If we run in
+// parallel the tests break each other by overriding prefs.
+for (const testCase of testCases) {
+ add_task(() => testSingleRequest(testCase));
+}
+for (const testCase of testCases) {
+ add_task(() => testRequestWithInvalidArgs(testCase));
+}
+for (const testCase of testCases) {
+ add_task(() => testMultipleRequests(testCase));
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_eme_request_notifications.html b/dom/media/test/test_eme_request_notifications.html
new file mode 100644
index 0000000000..6c44f892a6
--- /dev/null
+++ b/dom/media/test/test_eme_request_notifications.html
@@ -0,0 +1,82 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test Encrypted Media Extensions</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="eme.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var manager = new MediaTestManager;
+
+function SetPrefs(prefs) {
+ return SpecialPowers.pushPrefEnv({"set": prefs});
+}
+
+function observe() {
+ return new Promise(function(resolve, reject) {
+ var observer = function(subject, topic, data) {
+ SpecialPowers.Services.obs.removeObserver(observer, "mediakeys-request");
+ resolve(JSON.parse(data).status);
+ };
+ SpecialPowers.Services.obs.addObserver(observer, "mediakeys-request");
+ });
+}
+
+function Test(test) {
+ var p = test.prefs ? SetPrefs(test.prefs) : Promise.resolve();
+ var name = "'" + test.keySystem + "'";
+
+ var res = observe().then((status) => {
+ is(status, test.expectedStatus, name + " expected status");
+ });
+
+ p.then(() => navigator.requestMediaKeySystemAccess(test.keySystem, gCencMediaKeySystemConfig))
+ .then((keySystemAccess) => keySystemAccess.createMediaKeys());
+
+ return res;
+}
+
+var tests = [
+ {
+ keySystem: CLEARKEY_KEYSYSTEM,
+ expectedStatus: 'cdm-created',
+ prefs: [["media.eme.enabled", false]]
+ },
+ {
+ keySystem: "com.widevine.alpha",
+ expectedStatus: 'api-disabled',
+ prefs: [["media.eme.enabled", false]]
+ },
+ {
+ keySystem: "com.widevine.alpha",
+ expectedStatus: 'cdm-disabled',
+ prefs: [["media.eme.enabled", true], ["media.gmp-widevinecdm.enabled", false]]
+ },
+ {
+ keySystem: "com.widevine.alpha",
+ expectedStatus: 'cdm-not-installed',
+ prefs: [["media.eme.enabled", true], ["media.gmp-widevinecdm.enabled", true]]
+ },
+ {
+ keySystem: CLEARKEY_KEYSYSTEM,
+ expectedStatus: 'cdm-created',
+ prefs: [["media.eme.enabled", true]]
+ }
+];
+
+SimpleTest.waitForExplicitFinish();
+tests
+ .reduce(function(p, c, i, array) {
+ return p.then(function() {
+ return Test(c);
+ });
+ }, Promise.resolve())
+ .then(SimpleTest.finish);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_eme_sample_groups_playback.html b/dom/media/test/test_eme_sample_groups_playback.html
new file mode 100644
index 0000000000..cef1e26b33
--- /dev/null
+++ b/dom/media/test/test_eme_sample_groups_playback.html
@@ -0,0 +1,130 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <title>Test Encrypted Media Extensions</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="eme.js"></script>
+</head>
+
+<body>
+ <video controls id="video"></video>
+ <pre id="test">
+ <script class="testbody" type="text/javascript">
+
+ // Tests that files with a default key and a seperate sample keyids in the
+ // sgpd box play correctly (if the keyid from the sgpd box is not parsed
+ // or assigned to the sample we will wait indefinitely for the default
+ // key).
+
+ SimpleTest.waitForExplicitFinish();
+
+ // Test files for samples encrypted with different media keys.
+ var gEMESampleGoupTests = [
+ {
+ name:"video with 4 keys in sgpd (sbgp in traf sgpd in stbl)",
+ track: {
+ name:"video",
+ type:"video/mp4; codecs=\"avc1.64000d\"",
+ fragments:[ "sample-encrypted-sgpdstbl-sbgptraf.mp4"
+ ]
+ },
+ keys: {
+ // "keyid" : "key"
+ "279926496a7f5d25da69f2b3b2799a7f": "5544694d47473326622665665a396b36",
+ "597669572e55547e656b56586e2f6f68": "7959493a764556786527517849756635",
+ "205b2b293a342f3d3268293e6f6f4e29": "3a4f3674376d6c48675a273464447b40",
+ "32783e367c2e4d4d6b46467b3e6b5478": "3e213f6d45584f51713d534f4b417855",
+ },
+ sessionType:"temporary",
+ sessionCount:1,
+ duration:2,
+ },
+ ],
+ test = gEMESampleGoupTests[0];
+
+ var video = document.getElementById("video");
+ video.addEventListener("encrypted", () => {
+ Log(test.name, "Recieved encrypted event");
+ });
+
+ video.addEventListener("waitingforkey", () => {
+ Log(test.name, "waitingforkey");
+ ok(false, test.name + " Video is waitingforkey, indicating that the samples are not being assigned the correct id from the sgpd box!");
+ SimpleTest.finish();
+ });
+
+ function LoadEME() {
+ var options = [{
+ initDataType: "cenc",
+ videoType: test.track.type,
+ }];
+
+ return navigator.requestMediaKeySystemAccess("org.w3.clearkey", options)
+ .then((keySystemAccess) => {
+ return keySystemAccess.createMediaKeys();
+ }, bail("Failed to request key system access."))
+
+ .then((mediaKeys) => {
+ video.setMediaKeys(mediaKeys);
+
+ var session = mediaKeys.createSession();
+ once(session, "message", (ev) => {
+ is(ev.messageType, "license-request", "Expected a license-request");
+ session.update(GenerateClearKeyLicense(ev.message, test.keys));
+ });
+
+ var json = JSON.stringify({
+ "kids":Object.keys(test.keys).map(HexToBase64)
+ });
+ var request = new TextEncoder().encode(json);
+ session.generateRequest("keyids", request)
+ .then(e => {
+ Log(test.name, "Request license success");
+ }, reason => {
+ Log("Request license failed! " + reason);
+ });
+ });
+ }
+
+ function DownloadMedia(url, type, mediaSource) {
+ return new Promise((resolve, reject) => {
+ var sourceBuffer = mediaSource.addSourceBuffer(type);
+ fetchWithXHR(url, (response) => {
+ once(sourceBuffer, "updateend", resolve);
+ sourceBuffer.appendBuffer(new Uint8Array(response));
+ });
+ });
+ }
+
+ function LoadMSE() {
+ // Only set the source of the video and download the tracks after we
+ // have set the license keys, so we don't hit the waitingforkey event
+ // unless samples are being incorrectly assigned the default key
+ // (and we can safely fail).
+ LoadEME()
+ .then(() => {
+ var ms = new MediaSource();
+ video.src = URL.createObjectURL(ms);
+
+ once(ms, "sourceopen", () => {
+ Promise.all(test.track.fragments.map(fragment => DownloadMedia(fragment, test.track.type, ms)))
+ .then(() => {
+ ms.endOfStream();
+ video.play();
+ });
+ });
+
+ once(video, "ended", SimpleTest.finish);
+ });
+ }
+
+ LoadMSE();
+
+ </script>
+ </pre>
+</body>
+
+</html> \ No newline at end of file
diff --git a/dom/media/test/test_eme_session_callable_value.html b/dom/media/test/test_eme_session_callable_value.html
new file mode 100644
index 0000000000..3139c36157
--- /dev/null
+++ b/dom/media/test/test_eme_session_callable_value.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test Encrypted Media Extensions</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="eme.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+function Test() {
+ navigator.requestMediaKeySystemAccess(CLEARKEY_KEYSYSTEM, gCencMediaKeySystemConfig)
+ .then(access => access.createMediaKeys())
+ .then(mediaKeys => {
+ var initData = (new TextEncoder()).encode( 'this is an invalid license, and that is ok');
+ var s = mediaKeys.createSession("temporary");
+ s.generateRequest("cenc", initData); // ignore result.
+ // "update()" call should fail, because MediaKeySession is "not callable"
+ // yet, since CDM won't have had a chance to set the sessionId on MediaKeySession.
+ return s.update(initData);
+ })
+ .then(()=>{ok(false, "An exception should be thrown; MediaKeySession should be not callable."); SimpleTest.finish();},
+ ()=>{ok(true, "We expect this to fail; MediaKeySession should be not callable."); SimpleTest.finish();});
+}
+
+SimpleTest.waitForExplicitFinish();
+Test();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_eme_setMediaKeys_before_attach_MediaSource.html b/dom/media/test/test_eme_setMediaKeys_before_attach_MediaSource.html
new file mode 100644
index 0000000000..1493cfdcb8
--- /dev/null
+++ b/dom/media/test/test_eme_setMediaKeys_before_attach_MediaSource.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test Encrypted Media Extensions</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="eme.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+function beginTest() {
+ var video = document.createElement("video");
+
+ navigator.requestMediaKeySystemAccess(CLEARKEY_KEYSYSTEM, gCencMediaKeySystemConfig)
+ .then(function(keySystemAccess) {
+ return keySystemAccess.createMediaKeys();
+ })
+ .then(mediaKeys => {
+ return video.setMediaKeys(mediaKeys);
+ })
+ .then(() => {
+ var ms = new MediaSource();
+ ms.addEventListener("sourceopen", ()=>{ok(true, "MediaSource should open"); SimpleTest.finish();});
+ video.addEventListener("error", ()=>{ok(false, "Shouldn't error."); SimpleTest.finish();});
+ video.src = URL.createObjectURL(ms);
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+beginTest();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_eme_special_key_system.html b/dom/media/test/test_eme_special_key_system.html
new file mode 100644
index 0000000000..0addc06ebc
--- /dev/null
+++ b/dom/media/test/test_eme_special_key_system.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test Encrypted Media Extensions - Special key system</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+function requestProtectionQueryKeySystemAccess() {
+ const kKeySystemOptions = [
+ {
+ initDataTypes: ["cenc"],
+ videoCapabilities: [{ contentType: 'video/mp4; codecs="avc1.4d4015"' }],
+ },
+ ];
+ const kClearKeyWithProtectionQuery =
+ "org.mozilla.clearkey_with_protection_query";
+ return navigator.requestMediaKeySystemAccess(
+ kClearKeyWithProtectionQuery,
+ kKeySystemOptions
+ );
+}
+// Tests that org.mozilla.clearkey_with_protection_query cannot be accessed from JS if not preffed on.
+add_task(async function protectionQueryKeySystemShouldBeHidden() {
+ try {
+ // Should throw since special key systems are not enabled.
+ await requestProtectionQueryKeySystemAccess();
+ ok(
+ false,
+ "Test should have thrown by default because media.clearkey.test-key-systems.enabled should not be set"
+ );
+ } catch (e) {
+ is(e.name, "NotSupportedError", "Should get not supported error");
+ is(
+ e.message,
+ "Key system is unsupported",
+ "Should get message that key system is unsupported"
+ );
+ }
+});
+
+// Tests that org.mozilla.clearkey_with_protection_query can be accessed from JS if preffed on.
+add_task(async function protectionQueryKeySystemShouldBeExposed() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.clearkey.test-key-systems.enabled", true]],
+ });
+ try {
+ // Should not throw since special key systems are enabled.
+ let access = await requestProtectionQueryKeySystemAccess();
+ ok(
+ access,
+ "Should get access to protection query key system when preffed on"
+ );
+ } catch (e) {
+ ok(false, "Test should not have thrown and key system should be available");
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_eme_stream_capture_blocked_case1.html b/dom/media/test/test_eme_stream_capture_blocked_case1.html
new file mode 100644
index 0000000000..0fc8d28364
--- /dev/null
+++ b/dom/media/test/test_eme_stream_capture_blocked_case1.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test Encrypted Media Extensions</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="eme.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var manager = new MediaTestManager;
+
+function startTest(test, token)
+{
+ // Case 1. setting MediaKeys on an element captured by MediaElementSource should fail.
+ var case1token = token + "_case1";
+ let v1 = document.createElement("video");
+
+ function setMediaKeys() {
+ let p = new EMEPromise;
+ CreateMediaKeys(v1, test, case1token)
+ .then(mediaKeys => {
+ v1.setMediaKeys(mediaKeys)
+ .then(() => {
+ p.reject(`${case1token} setMediaKeys shouldn't succeed.`);
+ }, () => {
+ ok(true, TimeStamp(case1token) + " setMediaKeys failed as expected.");
+ p.resolve();
+ })
+ }, p.reject);
+ return p.promise;
+ }
+
+ var context = new AudioContext();
+ context.createMediaElementSource(v1);
+ v1.addEventListener("loadeddata", function(ev) {
+ ok(false, TimeStamp(case1token) + " should never reach loadeddata, as setMediaKeys should fail");
+ });
+
+ manager.started(case1token);
+
+ Promise.all([
+ LoadTest(test, v1, case1token),
+ setMediaKeys()])
+ .catch(reason => ok(false, reason))
+ .then(() => {
+ CleanUpMedia(v1);
+ manager.finished(case1token);
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+manager.runTests(gEMETests, startTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_eme_stream_capture_blocked_case2.html b/dom/media/test/test_eme_stream_capture_blocked_case2.html
new file mode 100644
index 0000000000..b60538caf0
--- /dev/null
+++ b/dom/media/test/test_eme_stream_capture_blocked_case2.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test Encrypted Media Extensions</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="eme.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var manager = new MediaTestManager;
+
+function startTest(test, token)
+{
+ // Case 2. creating a MediaElementSource on a media element should always succeed
+ // (no matter whether it's restricted content or not), and
+ var p1 = new EMEPromise;
+ var case2token = token + "_case2";
+ let v2 = document.createElement("video");
+
+ v2.addEventListener("loadeddata", function(ev) {
+ ok(true, case2token + " should reach loadeddata");
+ var threw = false;
+ try {
+ var context = new AudioContext();
+ context.createMediaElementSource(v2);
+ } catch (e) {
+ threw = true;
+ }
+ ok(!threw, "Should always work when creating a MediaElementSource.");
+ p1.resolve();
+ });
+
+ manager.started(case2token);
+ let p2 = SetupEME(v2, test, case2token);
+
+ Promise.all([p1.promise, p2])
+ .catch(reason => ok(false, reason))
+ .then(() => {
+ CleanUpMedia(v2);
+ manager.finished(case2token);
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+manager.runTests(gEMETests, startTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_eme_stream_capture_blocked_case3.html b/dom/media/test/test_eme_stream_capture_blocked_case3.html
new file mode 100644
index 0000000000..e25a900956
--- /dev/null
+++ b/dom/media/test/test_eme_stream_capture_blocked_case3.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test Encrypted Media Extensions</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="eme.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var manager = new MediaTestManager;
+
+function startTest(test, token)
+{
+ // Case 3. capturing a media element with mozCaptureStream that has a MediaKeys should fail.
+ var p1 = new EMEPromise;
+ var case3token = token + "_case3";
+ let v3 = document.createElement("video");
+
+ v3.addEventListener("loadeddata", function(ev) {
+ ok(true, TimeStamp(case3token) + " should reach loadeddata");
+ var threw = false;
+ try {
+ v3.mozCaptureStreamUntilEnded();
+ } catch (e) {
+ threw = true;
+ }
+ ok(threw, TimeStamp(case3token) + " Should throw an error calling mozCaptureStreamUntilEnded an EME video.");
+ p1.resolve();
+ });
+
+ manager.started(case3token);
+ let p2 = SetupEME(v3, test, case3token);
+
+ Promise.all([p1.promise, p2])
+ .catch(reason => ok(false, reason))
+ .then(() => {
+ CleanUpMedia(v3);
+ manager.finished(case3token);
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+manager.runTests(gEMETests, startTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_eme_unsetMediaKeys_then_capture.html b/dom/media/test/test_eme_unsetMediaKeys_then_capture.html
new file mode 100644
index 0000000000..3ecdc79dbf
--- /dev/null
+++ b/dom/media/test/test_eme_unsetMediaKeys_then_capture.html
@@ -0,0 +1,108 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test Encrypted Media Extensions</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="https://example.com:443/tests/dom/media/test/eme.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+/* import-globals-from eme.js */
+var manager = new MediaTestManager;
+
+// Test that if we can capture a video frame while playing clear content after
+// removing the MediaKeys object which was used for a previous encrypted content
+// playback on the same video element
+function startTest(test, token)
+{
+ manager.started(token);
+ var sessions = [];
+ function onSessionCreated(session) {
+ sessions.push(session);
+ }
+
+ function closeSessions() {
+ let p = new EMEPromise;
+ Promise.all(sessions.map(s => s.close()))
+ .then(p.resolve, p.reject);
+ return p.promise;
+ }
+
+ let v = document.createElement("video");
+ document.body.appendChild(v);
+
+ let finish = new EMEPromise;
+
+ function onVideoEnded(ev) {
+ ok(true, TimeStamp(token) + " (ENCRYPTED) content playback ended.");
+
+ function playClearVideo() {
+ var p1 = once(v, 'loadeddata', (e) => {
+ ok(true, TimeStamp(token) + " Receiving event 'loadeddata' for (CLEAR) content.");
+ let canvasElem = document.createElement('canvas');
+ document.body.appendChild(canvasElem);
+ let ctx2d = canvasElem.getContext('2d');
+
+ var gotTypeError = false;
+ try {
+ ctx2d.drawImage(v, 0, 0);
+ } catch (ex) {
+ if (ex instanceof TypeError) {
+ gotTypeError = true;
+ }
+ }
+ ok(!gotTypeError, TimeStamp(token) + " Canvas2D context drawImage succeed.")
+ });
+ v.src = 'bipbop_225w_175kbps.mp4';
+ v.play();
+ Promise.all([p1]).then(() => {
+ manager.finished(token);
+ }, () => {
+ ok(false, TimeStamp(token) + " Something wrong.");
+ manager.finished(token);
+ });
+ }
+
+ closeSessions()
+ .then(() => {
+ v.setMediaKeys(null)
+ .then(() => {
+ ok(true, TimeStamp(token) + " Setting MediaKeys to null.");
+ playClearVideo();
+ }, () => {
+ ok(false, TimeStamp(token) + " Setting MediaKeys to null.");
+ });;
+ });
+ }
+
+ once(v, "ended", onVideoEnded);
+
+ // Create a MediaKeys object and set to HTMLMediaElement then start the playback.
+ Promise.all([
+ LoadInitData(v, test, token),
+ CreateAndSetMediaKeys(v, test, token),
+ LoadTest(test, v, token)])
+ .then(values => {
+ let initData = values[0];
+ v.play();
+ initData.map(ev => {
+ let session = v.mediaKeys.createSession();
+ onSessionCreated(session);
+ MakeRequest(test, token, ev, session);
+ });
+ })
+ .then(() => {
+ return finish.promise;
+ })
+ .catch(reason => ok(false, reason))
+}
+
+SimpleTest.waitForExplicitFinish();
+manager.runTests(gEMETests, startTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_eme_waitingforkey.html b/dom/media/test/test_eme_waitingforkey.html
new file mode 100644
index 0000000000..c12a3b228f
--- /dev/null
+++ b/dom/media/test/test_eme_waitingforkey.html
@@ -0,0 +1,78 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test Encrypted Media Extensions</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="https://example.com:443/tests/dom/media/test/eme.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+/* import-globals-from eme.js */
+var manager = new MediaTestManager;
+
+function startTest(test, token)
+{
+ manager.started(token);
+
+ let v = document.createElement("video");
+ document.body.appendChild(v);
+
+ var gotWaitingForKey = 0;
+ var gotOnwaitingforkey = 0;
+
+ let waitForKey = new EMEPromise;
+ v.addEventListener("waitingforkey", function() {
+ gotWaitingForKey += 1;
+ waitForKey.resolve();
+ });
+
+ v.onwaitingforkey = function() {
+ gotOnwaitingforkey += 1;
+ };
+
+ v.addEventListener("loadedmetadata", function() {
+ ok(SpecialPowers.do_lookupGetter(v, "isEncrypted").apply(v),
+ TimeStamp(token) + " isEncrypted should be true");
+ is(v.isEncrypted, undefined, "isEncrypted should not be accessible from content");
+ });
+
+ let finish = new EMEPromise;
+ v.addEventListener("ended", function() {
+ ok(true, TimeStamp(token) + " got ended event");
+ // We expect only one waitingForKey as we delay until all sessions are ready.
+ // I.e. one waitingForKey should be fired, after which, we process all sessions,
+ // so it should not be possible to be blocked by a key after that point.
+ ok(gotWaitingForKey == 1, "Expected number 1 wait, got: " + gotWaitingForKey);
+ ok(gotOnwaitingforkey == gotWaitingForKey, "Should have as many event listener calls as event handler calls, got: " + gotOnwaitingforkey);
+
+ finish.resolve();
+ });
+
+ Promise.all([
+ LoadInitData(v, test, token),
+ CreateAndSetMediaKeys(v, test, token),
+ LoadTest(test, v, token),
+ waitForKey.promise])
+ .then(values => {
+ let initData = values[0];
+ return ProcessInitData(v, test, token, initData);
+ })
+ .then(sessions => {
+ Log(token, "Updated all sessions, loading complete -> Play");
+ v.play();
+ finish.promise.then(() => CloseSessions(v, sessions));
+ return finish.promise;
+ })
+ .catch(reason => ok(false, reason))
+ .then(() => manager.finished(token));
+}
+
+SimpleTest.waitForExplicitFinish();
+manager.runTests(gEMETests, startTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_eme_wv_privacy.html b/dom/media/test/test_eme_wv_privacy.html
new file mode 100644
index 0000000000..a050d2a528
--- /dev/null
+++ b/dom/media/test/test_eme_wv_privacy.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test generating encrypted request in Widevine privacy mode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="eme.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+function Test() {
+ return new Promise(function(resolve, reject) {
+ var configs = [{
+ initDataTypes: ['keyids'],
+ videoCapabilities: [{contentType: 'video/mp4' }],
+ }];
+ navigator.requestMediaKeySystemAccess('com.widevine.alpha', configs)
+ .then((access) => access.createMediaKeys(),
+ (e) => { throw e; })
+ .then((mediaKeys) => {
+ var session = mediaKeys.createSession('temporary');
+ session.addEventListener('message', event => {
+ is(event.messageType, 'license-request',
+ 'MediaKeyMessage type should be license-request.');
+ const request = new Uint8Array(event.message);
+ info('request generated[' + request.length + ']: [' + request + ']');
+ ok(request.length == 2 && request[0] == 0x08 && request[1] == 0x04,
+ 'Widevine license request should be [0x08, 0x04] in privacy mode');
+ session.close().then(() => { resolve(); });
+ });
+ return session.generateRequest('keyids',
+ new TextEncoder().encode('{"kids":["MDAwMDAwMDAwMDAwMDAwMQ=="]}'));
+ },
+ (e) => { throw e; })
+ .catch((e) => {
+ is(e.name, 'NotSupportedError',
+ 'Should only be rejected when "not supported"');
+ resolve();
+ });
+ });
+}
+
+SimpleTest.waitForExplicitFinish();
+Test().then(function() {
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_empty_resource.html b/dom/media/test/test_empty_resource.html
new file mode 100644
index 0000000000..8f65bb503d
--- /dev/null
+++ b/dom/media/test/test_empty_resource.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1094549
+-->
+<head>
+ <title>Test for Bug 1094549</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1094549">Mozilla Bug 1094549</a>
+
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+// Shorter timeout for this test should finish soon.
+SimpleTest.requestLongerTimeout(0.3);
+
+function finish(v) {
+ isnot(v.error, null, "should've got an error event");
+ SimpleTest.finish();
+}
+
+function onload() {
+ info("iframe loaded");
+ var v = SpecialPowers.wrap(document.body.getElementsByTagName("iframe")[0])
+ .contentDocument.body.getElementsByTagName("video")[0];
+
+ // Got 'error' as expected, finish the test.
+ if (v.error) {
+ finish(v);
+ return;
+ }
+
+ // Otherwise, wait for it.
+ v.onerror = function() {
+ finish(v);
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+var f = document.createElement("iframe");
+// Assign a resource file with zero length and expect the error event from
+// the video element since decoding metadata will fail.
+f.src = "data:video/webm,";
+f.addEventListener("load", onload);
+document.body.appendChild(f);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_error_in_video_document.html b/dom/media/test/test_error_in_video_document.html
new file mode 100644
index 0000000000..e376ea95e3
--- /dev/null
+++ b/dom/media/test/test_error_in_video_document.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=604067
+-->
+<head>
+ <title>Test for Bug 604067</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=604067">Mozilla Bug 604067</a>
+
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 604067 **/
+
+function documentVideo() {
+ return document.body.getElementsByTagName("iframe")[0]
+ .contentDocument.body.getElementsByTagName("video")[0];
+}
+
+function check() {
+ var v = documentVideo();
+
+ // Debug info for Bug 608634
+ ok(true, "iframe src=" + document.body.getElementsByTagName("iframe")[0].src);
+ is(v.readyState, v.HAVE_NOTHING, "Ready state");
+
+ isnot(v.error, null, "Error object");
+ is(v.networkState, v.NETWORK_NO_SOURCE, "Network state");
+ is(v.error.code, MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED, "Expected media not supported error");
+ SimpleTest.finish();
+}
+
+// Find an error test that we'd think we should be able to play (if it
+// wasn't already known to fail).
+var t = getPlayableVideo(gErrorTests);
+if (!t) {
+ todo(false, "No types supported");
+} else {
+ SimpleTest.waitForExplicitFinish();
+
+ var f = document.createElement("iframe");
+ f.src = t.name;
+ f.addEventListener("load", check);
+ document.body.appendChild(f);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_error_on_404.html b/dom/media/test/test_error_on_404.html
new file mode 100644
index 0000000000..8f82b8a07e
--- /dev/null
+++ b/dom/media/test/test_error_on_404.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=476731
+-->
+<head>
+ <title>Test for Bug 476731</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=476731">Mozilla Bug </a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+/** Test for Bug 476731 **/
+
+var videos = [];
+
+function FinishedLoads() {
+ if (!videos.length)
+ return false;
+ for (var i=0; i<videos.length; ++i) {
+ if (videos[i]._loadedData) {
+ // A video loadeddata, it should have 404'd. Signal the end of the test
+ // so the error will be reported.
+ return true;
+ }
+ if (!videos[i]._loadError) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function loadError(evt) {
+ var v = evt.target;
+ is(v._loadError, false, "Shouldn't receive multiple error events for " + v.src);
+ v._loadError = true;
+ is(v._loadStart, true, "Receive loadstart for " + v.src);
+ is(v._loadedData, false, "Shouldn't receive loadeddata for " + v.src);
+ if (FinishedLoads(videos))
+ SimpleTest.finish();
+}
+
+function loadedData(evt) {
+ evt.target._loadedData = true;
+}
+
+function loadStart(evt) {
+ evt.target._loadStart = true;
+}
+
+// Create all video objects.
+for (var i=0; i<g404Tests.length; ++i) {
+ var v = document.createElement("video");
+ v.preload = "metadata";
+ var test = g404Tests[i];
+ if (!v.canPlayType(test.type)) {
+ continue;
+ }
+ v.src = test.name;
+ v._loadedData = false;
+ v._loadStart = false;
+ v._loadError = false;
+ v.addEventListener("error", loadError);
+ v.addEventListener("loadstart", loadStart);
+ v.addEventListener("loadeddata", loadedData);
+ document.body.appendChild(v); // Will start load.
+ videos.push(v);
+}
+
+if (!videos.length) {
+ todo(false, "No types supported");
+} else {
+ SimpleTest.waitForExplicitFinish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_fastSeek-forwards.html b/dom/media/test/test_fastSeek-forwards.html
new file mode 100644
index 0000000000..1e50021b13
--- /dev/null
+++ b/dom/media/test/test_fastSeek-forwards.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1022913
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1022913</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1022913">Mozilla Bug 1022913</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+ <script type="application/javascript">
+
+ // Test that if we're doing a fastSeek() forwards that we don't end up
+ // seeking backwards. This can happen if the keyframe before the seek
+ // target is before the current playback position. We'd prefer to seek to
+ // the keyframe after the seek target in this case, but we don't implement
+ // this yet (bug 1026330).
+ var manager = new MediaTestManager;
+
+ var onSecondSeekComplete = function(event) {
+ var v = event.target;
+ v.removeEventListener("seeked", onSecondSeekComplete);
+ ok(v.currentTime >= v.firstSeekTarget, v.name + " seek never go backwards. time=" + v.currentTime + " firstSeekTarget=" + v.firstSeekTarget + " secondSeekTarget=" + v.secondSeekTarget);
+ manager.finished(v.token);
+ removeNodeAndSource(v);
+ };
+
+ var onFirstSeekComplete = function(event) {
+ var v = event.target;
+ v.removeEventListener("seeked", onFirstSeekComplete);
+ // Seek to 75% of the way between the start and the first keyframe
+ // using fastSeek. We then test that the currentTime doesn't drop back
+ // to the previous keyframe, currentTime should go forwards.
+ v.addEventListener("seeked", onSecondSeekComplete);
+ v.secondSeekTarget = v.keyframes[1] * 0.75;
+ v.fastSeek(v.secondSeekTarget);
+ }
+
+ var onLoadedMetadata = function(event) {
+ // Seek to the mid-point between the start and the first keyframe.
+ var v = event.target;
+ v.removeEventListener("loadedmetadata", onLoadedMetadata);
+ v.addEventListener("seeked", onFirstSeekComplete);
+ v.firstSeekTarget = v.keyframes[1] * 0.5;
+ v.currentTime = v.firstSeekTarget;
+ }
+
+ function startTest(test, token) {
+ manager.started(token);
+ let v = document.createElement("video");
+ v.src = test.name;
+ v.name = test.name;
+ v.preload = "metadata";
+ v.token = token;
+ v.target = 0;
+ v.keyframes = test.keyframes;
+ v.keyframeIndex = 0;
+ ok(v.keyframes.length >= 2, v.name + " - video should have at least two sync points");
+ v.addEventListener("loadedmetadata", onLoadedMetadata);
+ document.body.appendChild(v);
+ }
+
+ manager.runTests(gFastSeekTests, startTest);
+
+ </script>
+</body>
+</html>
diff --git a/dom/media/test/test_fastSeek.html b/dom/media/test/test_fastSeek.html
new file mode 100644
index 0000000000..2245bcc9bf
--- /dev/null
+++ b/dom/media/test/test_fastSeek.html
@@ -0,0 +1,88 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=778077
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 778077</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=778077">Mozilla Bug 778077</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+ <script type="application/javascript">
+
+ /** Test for Bug 778077 - HTMLMediaElement.fastSeek() **/
+ // Iterate through a list of keyframe timestamps, and seek to
+ // halfway between the keyframe and the keyframe after it.
+ var manager = new MediaTestManager;
+
+ function doSeek(v) {
+ // fastSeek to half way between this keyframe and the next, or if this is the last
+ // keyframe seek to halfway between this keyframe and the end of media.
+ var nextKeyFrame = (v.keyframeIndex + 1) < v.keyframes.length ? v.keyframes[v.keyframeIndex + 1] : v.duration;
+ v.target = (v.keyframes[v.keyframeIndex] + nextKeyFrame) / 2;
+ v.fastSeek(v.target);
+ ok(Math.abs(v.currentTime - v.target) < 0.01,
+ v.name + " seekTo=" + v.target + " currentTime (" + v.currentTime + ") should be close to seek target initially");
+ }
+
+ function onloadedmetadata(event) {
+ var v = event.target;
+ doSeek(v);
+ }
+
+ function onseeked(event) {
+ var v = event.target;
+ var keyframe = v.keyframes[v.keyframeIndex];
+
+ // Check that the current time ended up roughly after the keyframe.
+ // We must be a bit fuzzy here, as the decoder backend may actually
+ // seek to the audio sample prior to the keyframe.
+ ok(v.currentTime >= keyframe - 0.05,
+ v.name + " seekTo=" + v.target + " currentTime (" + v.currentTime +
+ ") should be end up roughly after keyframe (" + keyframe + ") after fastSeek");
+
+ ok(v.currentTime <= v.target,
+ v.name + " seekTo=" + v.target + " currentTime (" + v.currentTime +
+ ") should be end up less than target after fastSeek");
+
+ v.keyframeIndex++
+ if (v.keyframeIndex == v.keyframes.length) {
+ v.src = "";
+ v.remove();
+ manager.finished(v.token);
+ } else {
+ doSeek(v);
+ }
+ }
+
+ function startTest(test, token) {
+ manager.started(token);
+ let v = document.createElement("video");
+ v.src = test.name;
+ v.name = test.name;
+ v.preload = "metadata";
+ v.token = token;
+ v.target = 0;
+ v.keyframes = test.keyframes;
+ v.keyframeIndex = 0;
+ ok(v.keyframes.length, v.name + " - video should have at least one sync point");
+ v.addEventListener("loadedmetadata", onloadedmetadata);
+ v.addEventListener("seeked", onseeked);
+ document.body.appendChild(v);
+ }
+
+ manager.runTests(gFastSeekTests, startTest);
+
+ </script>
+</body>
+</html>
diff --git a/dom/media/test/test_fragment_noplay.html b/dom/media/test/test_fragment_noplay.html
new file mode 100644
index 0000000000..6a1119f342
--- /dev/null
+++ b/dom/media/test/test_fragment_noplay.html
@@ -0,0 +1,127 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: fragment tests</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="fragment_noplay.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+// Fragment parameters to try
+var gFragmentParams = [
+ // W3C Media fragment tests
+ // http://www.w3.org/2008/WebVideo/Fragments/TC/ua-test-cases
+ { fragment: "#t=banana", start: null, end: null }, // TC0027-UA
+ { fragment: "#t=3,banana", start: null, end: null }, // TC0028-UA
+ { fragment: "#t=banana,7", start: null, end: null }, // TC0029-UA
+ { fragment: "#t='3'", start: null, end: null }, // TC0030-UA
+ { fragment: "#t=3-7", start: null, end: null }, // TC0031-UA
+ { fragment: "#t=3:7", start: null, end: null }, // TC0032-UA
+ { fragment: "#t=3,7,9", start: null, end: null }, // TC0033-UA
+ { fragment: "#t%3D3", start: null, end: null }, // TC0034-UA
+ { fragment: "#%74=3", start: 3, end: null }, // TC0035-UA
+ { fragment: "#t=%33", start: 3, end: null }, // TC0036-UA
+ { fragment: "#t=3%2C7", start: 3, end: 7 }, // TC0037-UA
+ { fragment: "#t=%6Ept:3", start: 3, end: null }, // TC0038-UA
+ { fragment: "#t=npt%3A3", start: 3, end: null }, // TC0039-UA
+ { fragment: "#t=-1,3", start: null, end: null }, // TC0044-UA
+ { fragment: "#t=3&", start: 3, end: null }, // TC0051-UA
+ { fragment: "#u=12&t=3", start: 3, end: null }, // TC0052-UA
+ { fragment: "#t=foo:7&t=npt:3", start: 3, end: null }, // TC0053-UA
+ { fragment: "#&&=&=tom&jerry=&t=3&t=meow:0#", start: 3, end: null }, // TC0054-UA
+ { fragment: "#t=7&t=3", start: 3, end: null }, // TC0055-UA
+ { fragment: "#T=3,7", start: null, end: null }, // TC0058-UA
+ { fragment: "#t=", start: null, end: null }, // TC0061-UA
+ { fragment: "#t=.", start: null, end: null }, // TC0062-UA
+ { fragment: "#t=.0", start: null, end: null }, // TC0063-UA
+ { fragment: "#t=0s", start: null, end: null }, // TC0064-UA
+ { fragment: "#t=,0s", start: null, end: null }, // TC0065-UA
+ { fragment: "#t=0s,0s", start: null, end: null }, // TC0066-UA
+ { fragment: "#t=00:00:00s", start: null, end: null }, // TC0067-UA
+ { fragment: "#t=s", start: null, end: null }, // TC0068-UA
+ { fragment: "#t=npt:", start: null, end: null }, // TC0069-UA
+ { fragment: "#t=1e-1:", start: null, end: null }, // TC0070-UA
+ { fragment: "#t=00:00:01.1e-1", start: null, end: null }, // TC0071-UA
+ { fragment: "#t=3.", start: 3, end: null }, // TC0072-UA
+ { fragment: "#t=0:0:0", start: null, end: null }, // TC0073-UA
+ { fragment: "#t=0:00:60", start: null, end: null }, // TC0074-UA
+ { fragment: "#t=0:01:60", start: null, end: null }, // TC0075-UA
+ { fragment: "#t=0:60:00", start: null, end: null }, // TC0076-UA
+ { fragment: "#t=0:000:000", start: null, end: null }, // TC0077-UA
+ { fragment: "#t=00:00:03,00:00:07", start: 3, end: 7 }, // TC0078-UA
+ { fragment: "#t=3,00:00:07", start: 3, end: 7 }, // TC0079-UA
+ { fragment: "#t=00:00.", start: null, end: null }, // TC0080-UA
+ { fragment: "#t=0:00:00.", start: null, end: null }, // TC0081-UA
+ { fragment: "#t=0:00:10e-1", start: null, end: null }, // TC0082-UA
+ { fragment: "#t=0:00:60.000", start: null, end: null }, // TC0083-UA
+ { fragment: "#t=0:60:00.000", start: null, end: null }, // TC0084-UA
+ { fragment: "#t=3,7&t=foo", start: 3, end: 7 }, // TC0085-UA
+ { fragment: "#foo&t=3,7", start: 3, end: 7 }, // TC0086-UA
+ { fragment: "#t=3,7&foo", start: 3, end: 7 }, // TC0087-UA
+ { fragment: "#t=3,7&&", start: 3, end: 7 }, // TC0088-UA
+ { fragment: "#&t=3,7", start: 3, end: 7 }, // TC0089-UA
+ { fragment: "#&&t=3,7", start: 3, end: 7 }, // TC0090-UA
+ { fragment: "#&t=3,7&", start: 3, end: 7 }, // TC0091-UA
+ { fragment: "#t%3d10", start: null, end: null }, // TC0092-UA
+ { fragment: "#t=10%26", start: null, end: null }, // TC0093-UA
+ { fragment: "#&t=3,7,", start: null, end: null } // TC0094-UA
+];
+
+function createTestArray() {
+ var tests = [];
+ var tmpVid = document.createElement("video");
+
+ for (var testNum=0; testNum<gFragmentTests.length; testNum++) {
+ var test = gFragmentTests[testNum];
+ if (!tmpVid.canPlayType(test.type)) {
+ continue;
+ }
+
+ for (var fragNum=0; fragNum<gFragmentParams.length; fragNum++) {
+ var p = gFragmentParams[fragNum];
+ var t = {};
+ t.name = test.name + p.fragment;
+ t.type = test.type;
+ t.duration = test.duration;
+ t.start = p.start;
+ t.end = p.end;
+ tests.push(t);
+ }
+ }
+ return tests;
+}
+
+function startTest(test, token) {
+ var video = document.createElement('video');
+ manager.started(token);
+ video.preload = "metadata";
+ video.src = test.name;
+ video.token = token;
+ video.controls = true;
+ document.body.appendChild(video);
+ var name = test.name + " fragment test";
+ var localIs = function(n) { return function(a, b, msg) {
+ is(a, b, n + ": " + msg);
+ }}(name);
+ var localOk = function(n) { return function(a, msg) {
+ ok(a, n + ": " + msg);
+ }}(name);
+ var localFinish = function(v, m) { return function() {
+ removeNodeAndSource(v);
+ m.finished(v.token);
+ }}(video, manager);
+ window.test_fragment_noplay(video, test.start, test.end, localIs, localOk, localFinish);
+}
+
+manager.runTests(createTestArray(), startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_fragment_play.html b/dom/media/test/test_fragment_play.html
new file mode 100644
index 0000000000..eab422bab1
--- /dev/null
+++ b/dom/media/test/test_fragment_play.html
@@ -0,0 +1,91 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: seek tests</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="fragment_play.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+PARALLEL_TESTS = 1;
+var manager = new MediaTestManager;
+
+// Fragment parameters to try. These tests
+// try playing the video. Tests for other fragment
+// formats are in test_fragment_noplay.html.
+var gFragmentParams = [
+ { fragment: "", start: null, end: null },
+ { fragment: "#t=,", start: null, end: null },
+ { fragment: "#t=3,3", start: null, end: null },
+ { fragment: "#t=7,3", start: null, end: null },
+ { fragment: "#t=7,15", start: 7, end: null },
+ { fragment: "#t=15,20", start: 9.287982, end: null },
+ { fragment: "#t=5", start: 5, end: null },
+ { fragment: "#t=5.5", start: 5.5, end: null },
+ { fragment: "#t=5,", start: null, end: null },
+ { fragment: "#t=,5", start: 0, end: 5, todo: "See bugs 682141 and 720248" },
+ { fragment: "#t=2.5,5.5", start: 2.5, end: 5.5, todo: "See bugs 682141 and 720248" },
+ { fragment: "#t=1,2.5", start: 1, end: 2.5, todo: "See bugs 682141 and 720248" },
+ { fragment: "#t=,15", start: 0, end: null }
+];
+
+function createTestArray() {
+ var tests = [];
+ var tmpVid = document.createElement("video");
+
+ for (var testNum=0; testNum<gFragmentTests.length; testNum++) {
+ var test = gFragmentTests[testNum];
+ if (!tmpVid.canPlayType(test.type)) {
+ continue;
+ }
+
+ for (var fragNum=0; fragNum<gFragmentParams.length; fragNum++) {
+ var p = gFragmentParams[fragNum];
+ var t = {};
+ t.name = test.name + p.fragment;
+ t.type = test.type;
+ t.duration = test.duration;
+ t.start = p.start;
+ t.end = p.end;
+ t.todo = p.todo;
+ tests.push(t);
+ }
+ }
+ return tests;
+}
+
+function startTest(test, token) {
+ if (test.todo) {
+ todo(false, test.todo);
+ return;
+ }
+ var video = document.createElement('video');
+ manager.started(token);
+ video.preload = "metadata";
+ video.src = test.name;
+ video.token = token;
+ video.controls = true;
+ document.body.appendChild(video);
+ var name = test.name + " fragment test";
+ var localIs = function(n) { return function(a, b, msg) {
+ is(a, b, n + ": " + msg);
+ }}(name);
+ var localOk = function(n) { return function(a, msg) {
+ ok(a, n + ": " + msg);
+ }}(name);
+ var localFinish = function(v, m) { return function() {
+ removeNodeAndSource(v);
+ m.finished(v.token);
+ }}(video, manager);
+ window.test_fragment_play(video, test.start, test.end, localIs, localOk, localFinish);
+}
+
+manager.runTests(createTestArray(), startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_hls_player_independency.html b/dom/media/test/test_hls_player_independency.html
new file mode 100644
index 0000000000..cea5c140ed
--- /dev/null
+++ b/dom/media/test/test_hls_player_independency.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test playback of 2 HLS video at the same page </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+ <div id='player1'>
+ <video id='player4x3' controls autoplay>
+ </video>
+ </div>
+ <p> 4x3 basic stream<span>
+ <span>
+ <div height = 10>
+ <span>
+ <div id='player2'>
+ <video id='player16x9' controls autoplay>
+ </video>
+ </div>
+ <p> 16x9 basic stream<span>
+
+<script class="testbody" type="text/javascript">
+
+function startTest() {
+ var v4x3 = document.getElementById('player4x3');
+ var v16x9 = document.getElementById('player16x9');
+
+ var p1 = once(v4x3, 'ended', function onended(e) {
+ is(v4x3.videoWidth, 400, "4x3 content, the width should be 400.");
+ is(v4x3.videoHeight, 300, "4x3 content, the height should be 300.");
+ });
+
+ var p2 = once(v16x9, 'ended', function onended(e) {
+ is(v16x9.videoWidth, 416, "16x9 content, the width should be 416.");
+ is(v16x9.videoHeight, 234, "16x9 content, the height should be 234.");
+ });
+
+ v4x3.src = serverUrl + "/bipbop_4x3_single.m3u8";
+ v16x9.src = serverUrl + "/bipbop_16x9_single.m3u8";
+ Promise.all([p1, p2]).then(() => {
+ SimpleTest.finish();
+ });
+}
+
+startTest();
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_imagecapture.html b/dom/media/test/test_imagecapture.html
new file mode 100644
index 0000000000..d61dd358bb
--- /dev/null
+++ b/dom/media/test/test_imagecapture.html
@@ -0,0 +1,128 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1041393
+-->
+<head>
+ <meta charset="utf-8">
+ <title>ImageCapture tests</title>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="gUM_support.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1041393">ImageCapture tests</a>
+<script type="application/javascript">
+
+// Check if the callback returns even no JS reference on it.
+async function gcTest(track) {
+ const repeat = 100;
+ const promises = [];
+ for (let i = 0; i < repeat; ++i) {
+ const imageCapture = new ImageCapture(track);
+ promises.push(new Promise((resolve, reject) => {
+ imageCapture.onphoto = resolve;
+ imageCapture.onerror = () =>
+ reject(new Error(`takePhoto gcTest failure for capture ${i}`));
+ }));
+ imageCapture.takePhoto();
+ }
+ info("Call gc ");
+ SpecialPowers.gc();
+ await Promise.all(promises);
+}
+
+// Continue calling takePhoto() in rapid succession.
+async function rapidTest(track) {
+ const repeat = 100;
+ const imageCapture = new ImageCapture(track);
+ // "error" can fire synchronously in `takePhoto`
+ const errorPromise = new Promise(r => imageCapture.onerror = r);
+ for (let i = 0; i < repeat; ++i) {
+ imageCapture.takePhoto();
+ }
+ for (let i = 0; i < repeat; ++i) {
+ await Promise.race([
+ new Promise(r => imageCapture.onphoto = r),
+ errorPromise.then(() => Promise.reject(new Error("Capture failed"))),
+ ]);
+ }
+}
+
+// Check if the blob is decodable.
+async function blobTest(track) {
+ const imageCapture = new ImageCapture(track);
+ // "error" can fire synchronously in `takePhoto`
+ const errorPromise = new Promise(r => imageCapture.onerror = r);
+ imageCapture.takePhoto();
+ const blob = await Promise.race([
+ new Promise(r => imageCapture.onphoto = r),
+ errorPromise.then(() => Promise.reject(new Error("Capture failed"))),
+ ]);
+
+ const img = new Image();
+ img.src = URL.createObjectURL(blob.data);
+ await new Promise((resolve, reject) => {
+ img.onload = resolve;
+ img.onerror = () => reject(new Error("Decode failed"));
+ });
+}
+
+// It should return an error event after disabling video track.
+async function trackTest(track) {
+ const imageCapture = new ImageCapture(track);
+ track.enabled = false;
+ try {
+ const errorPromise = new Promise(r => imageCapture.onerror = r);
+ imageCapture.takePhoto();
+ const error = await Promise.race([
+ errorPromise,
+ new Promise(r => imageCapture.onphoto = r).then(
+ () => Promise.reject(new Error("Disabled video track should fail"))),
+ ]);
+ is(error.imageCaptureError.code, error.imageCaptureError.PHOTO_ERROR,
+ "Expected error code")
+ } finally {
+ track.enabled = true;
+ }
+}
+
+async function init() {
+ // Use loopback camera if available, otherwise fake.
+ // MediaTrackGraph will be the backend of ImageCapture.
+ await setupGetUserMediaTestPrefs();
+ let stream = await navigator.mediaDevices.getUserMedia({video: true});
+ return stream.getVideoTracks()[0];
+}
+
+async function start() {
+ try {
+ const track = await init();
+ info("ImageCapture track disable test.");
+ await trackTest(track);
+ info("ImageCapture blob test.");
+ await blobTest(track);
+ info("ImageCapture rapid takePhoto() test.");
+ await rapidTest(track);
+ info("ImageCapture multiple instances test.");
+ await gcTest(track);
+ } catch (e) {
+ ok(false, "Unexpected error during test: " + e);
+ } finally {
+ SimpleTest.finish();
+ }
+}
+
+SimpleTest.requestCompleteLog();
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv({
+ "set": [
+ ["dom.imagecapture.enabled", true],
+ ["media.navigator.permission.disabled", true],
+ ],
+}, start);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_info_leak.html b/dom/media/test/test_info_leak.html
new file mode 100644
index 0000000000..41b01c74c1
--- /dev/null
+++ b/dom/media/test/test_info_leak.html
@@ -0,0 +1,175 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=478957
+-->
+<head>
+ <title>Test for Bug 478957</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=478957">Mozilla Bug 478957</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+
+<div id="log" style="font-size: small;"></div>
+
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 478957 **/
+
+// Tests whether we leak events and state change info when loading stuff from local files from a webserver.
+
+var manager = new MediaTestManager;
+
+var gEventTypes = [ 'loadstart', 'progress', 'suspend', 'abort', 'error', 'emptied', 'stalled', 'play', 'pause', 'loadedmetadata', 'loadeddata', 'waiting', 'playing', 'canplay', 'canplaythrough', 'seeking', 'seeked', 'timeupdate', 'ended', 'ratechange', 'durationchange', 'volumechange' ];
+
+var gExpectedEvents = ['loadstart', 'error'];
+
+function createTestArray() {
+ var tests = [];
+ var tmpVid = document.createElement("video");
+
+ return makeInfoLeakTests().then(infoLeakTests => {
+ for (var testNum=0; testNum < infoLeakTests.length; testNum++) {
+ var test = infoLeakTests[testNum];
+ if (!tmpVid.canPlayType(test.type)) {
+ continue;
+ }
+
+ var t = {};
+ t.name = test.src;
+ t.type = test.type;
+
+ tests.push(t);
+ }
+ return tests;
+ })
+}
+
+function log(msg) {
+ info(msg);
+ var l = document.getElementById('log');
+ // eslint-disable-next-line no-unsanitized/property
+ l.innerHTML += msg + "<br>";
+}
+
+function finish(v) {
+ log("finish: " + v.name);
+ clearInterval(v.checkStateInterval);
+
+ for (var i=0; i<gEventTypes.length; i++) {
+ v.removeEventListener(gEventTypes[i], listener);
+ }
+ removeNodeAndSource(v);
+
+ manager.finished(v.token);
+ v = null;
+}
+
+function listener(evt) {
+ var v = evt.target;
+ log(filename(v.name) + ': got ' + evt.type);
+
+ // On slow machines like B2G emulator, progress timer could time out before
+ // receiving any HTTP notification. We will ignore the 'stalled' event to
+ // pass the tests.
+ if (evt.type == 'stalled') {
+ return;
+ }
+
+ ok(v.eventNum < gExpectedEvents.length, filename(v.name) + " Too many events received");
+ var expected = (v.eventNum < gExpectedEvents.length) ? gExpectedEvents[v.eventNum] : "NoEvent";
+ is(evt.type, expected, filename(v.name) + " Events received in wrong order");
+ v.eventNum++;
+ if (v.eventNum == gExpectedEvents.length) {
+ // In one second, move onto the next test. This give a chance for any
+ // other events to come in. Note: we don't expect any events to come
+ // in, unless we've leaked some info, and 1 second should be enough time
+ // for the leak to show up.
+ setTimeout(function() {finish(v);}, 1000);
+ }
+}
+
+function createMedia(type, src, token) {
+ var tag = getMajorMimeType(type);
+ var v = document.createElement(tag);
+ for (var i=0; i<gEventTypes.length; i++) {
+ v.addEventListener(gEventTypes[i], listener);
+ }
+ v.preload = "metadata";
+ v.src = src;
+ v.name = src;
+ document.body.appendChild(v);
+ v.eventNum = 0;
+ v.token = token;
+ setTimeout(
+ function() {
+ v.checkStateInterval = setInterval(function(){checkState(v);},1);
+ }, 0);
+}
+
+// Define our own ok() and is() functions. The mochitest ones take ages constructing the log
+// of all the passes, so only report failures.
+function test_ok(b, msg) {
+ if (!b) {
+ log("FAILED test_ok: " + msg);
+ ok(b, msg);
+ }
+}
+
+function test_is(a, b, msg) {
+ if (a != b) {
+ log("FAILED test_is: " + msg);
+ is(a,b,msg);
+ }
+}
+
+function filename(uri) {
+ return uri.substr(uri.lastIndexOf("/")+1);
+}
+
+function checkState(v) {
+ test_ok(v.networkState <= HTMLMediaElement.NETWORK_LOADING ||
+ v.networkState == HTMLMediaElement.NETWORK_NO_SOURCE,
+ "NetworkState of " + v.networkState + " was leaked.");
+ test_ok(v.readyState == HTMLMediaElement.HAVE_NOTHING,
+ "Ready state of " + v.readyState + " was leaked");
+ test_is(v.seeking, false, "Seeking leaked");
+ test_is(v.currentTime, 0, "Leaked currentTime");
+ test_ok(isNaN(v.duration), "Leaked duration");
+ test_is(v.paused, true, "Paused leaked");
+ test_is(v.ended, false, "Ended leaked");
+ test_is(v.autoplay, false, "Autoplay leaked");
+ test_is(v.controls, false, "Controls leaked");
+ test_is(v.muted, false, "muted leaked");
+ test_ok(v.error==null || v.error.code==MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED,
+ "Error code should not exist or be SRC_NOT_SUPPORTED. v.error=" +
+ (v.error ? v.error.code : "null"));
+ test_ok(filename(v.currentSrc) == filename(v.name) ||
+ v.networkState == HTMLMediaElement.NETWORK_NO_SOURCE,
+ "currentSrc should match candidate uri, if we've got a valid source");
+}
+
+
+function startTest(test, token) {
+ manager.started(token);
+ log("Testing: " + test.type + " @ " + test.name);
+ createMedia(test.type, test.name, token);
+}
+
+SimpleTest.waitForExplicitFinish();
+createTestArray().then(testArray => {
+ manager.runTests(testArray, startTest);
+});
+
+</script>
+</pre>
+
+</body>
+</html>
diff --git a/dom/media/test/test_invalid_reject.html b/dom/media/test/test_invalid_reject.html
new file mode 100644
index 0000000000..583847fe12
--- /dev/null
+++ b/dom/media/test/test_invalid_reject.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="UTF-8" />
+ <title>Test rejection of invalid files</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+function startTest(test, token) {
+ var v = document.createElement('video');
+ manager.started(token);
+
+ // Set up event handlers. Seeing any of these is a failure.
+ function badEvent(type) { return function(e) {
+ ok(false, test.name + " should not fire '" + type + "' event");
+ }};
+ var events = [
+ 'loadeddata', 'load',
+ 'canplay', 'canplaythrough',
+ 'playing'
+ ];
+ events.forEach( function(e) {
+ v.addEventListener(e, badEvent(e));
+ });
+
+ // Seeing a decoder error is a success.
+ v.addEventListener("error", function onerror(e) {
+ if (v.readyState == v.HAVE_NOTHING) {
+ is(v.error.code, v.error.MEDIA_ERR_SRC_NOT_SUPPORTED,
+ "decoder should reject " + test.name);
+ } else {
+ is(v.error.code, v.error.MEDIA_ERR_DECODE,
+ "decoder should reject " + test.name);
+ }
+ v.removeEventListener('error', onerror);
+ manager.finished(token);
+ });
+
+ // Now try to load and play the file, which should result in the
+ // error event handler above being called, terminating the test.
+ document.body.appendChild(v);
+ v.src = test.name;
+ v.play();
+}
+
+manager.runTests(gInvalidTests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_invalid_reject_play.html b/dom/media/test/test_invalid_reject_play.html
new file mode 100644
index 0000000000..3e658f94b8
--- /dev/null
+++ b/dom/media/test/test_invalid_reject_play.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="UTF-8" />
+ <title>Test rejection of invalid files during playback</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+function startTest(test, token) {
+ var v = document.createElement('video');
+ manager.started(token);
+
+ // Seeing a decoder error is a success.
+ v.addEventListener("error", function onerror(e) {
+ is(v.error.code, v.error.MEDIA_ERR_DECODE,
+ "decoder should reject " + test.name);
+ v.removeEventListener("error", onerror);
+ manager.finished(token);
+ });
+
+ v.addEventListener("ended", function onended(e) {
+ ok(false, "decoder should have rejected file before playback ended");
+ v.removeEventListener("ended", onended);
+ manager.finished(token);
+ });
+
+ document.body.appendChild(v);
+ v.src = test.name;
+ v.play();
+}
+
+manager.runTests(gInvalidPlayTests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_invalid_seek.html b/dom/media/test/test_invalid_seek.html
new file mode 100644
index 0000000000..8aa514b977
--- /dev/null
+++ b/dom/media/test/test_invalid_seek.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: invalide seek test</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<video id='v'></video>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+// http://www.whatwg.org/specs/web-apps/current-work/#dom-media-seek
+// If the media element's readyState is HAVE_NOTHING, then the user agent
+// must raise an InvalidStateError exception.
+var v = document.getElementById('v');
+var passed = false;
+
+ok(v.readyState == HTMLMediaElement.HAVE_NOTHING,
+ "Invalid ready state in media element");
+
+try {
+ v.seek(1);
+}
+catch(e) {
+ passed = true;
+}
+
+ok(passed, "Video did not raise error during invalid seek");
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_load.html b/dom/media/test/test_load.html
new file mode 100644
index 0000000000..de8fd63948
--- /dev/null
+++ b/dom/media/test/test_load.html
@@ -0,0 +1,217 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=479859
+-->
+<head>
+ <title>Test for Bug 479859</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=479859">Mozilla Bug 479859</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="text/javascript">
+
+function log(msg) {
+ //document.getElementById('log').innerHTML += "<p>" + msg + "</p>";
+}
+
+// We don't track: progress, canplay, canplaythrough and stalled events,
+// as these can be delivered out of order, and/or multiple times.
+var gEventTypes = [ 'loadstart', 'abort', 'error', 'emptied', 'play',
+ 'pause', 'loadedmetadata', 'loadeddata', 'waiting', 'playing', 'seeking',
+ 'seeked', 'timeupdate', 'ended', 'ratechange', 'durationchange', 'volumechange' ];
+
+var gEventNum = 0;
+var gTestNum = 0;
+var gTestFileNum = 0;
+var gExpectedEvents = null;
+var gTest = null;
+var gTestName = "?";
+
+function listener(evt) {
+ log('event ' + evt.type);
+ ok(gEventNum < gExpectedEvents.length, gTestName+" - corrent number of events received");
+ var expected = (gEventNum < gExpectedEvents.length) ? gExpectedEvents[gEventNum] : "NoEvent";
+ is(evt.type, expected, gTestName+" - events received in order");
+ gEventNum++;
+ if (gEventNum == gExpectedEvents.length) {
+ setTimeout(nextTest, 0);
+ }
+}
+
+function source_error(evt) {
+ log('event source_error');
+ ok(evt.type == "error", "Should only get error events here");
+ ok(gEventNum < gExpectedEvents.length, gTestName+" - corrent number of events received");
+ var expected = (gEventNum < gExpectedEvents.length) ? gExpectedEvents[gEventNum] : "NoEvent";
+ is("source_error", expected, gTestName+" - events received in order");
+ gEventNum++;
+ if (gEventNum == gExpectedEvents.length) {
+ setTimeout(nextTest, 0);
+ }
+}
+
+var gMedia = null;
+
+function createMedia(tag) {
+ gMedia = document.createElement(tag);
+ gMedia.preload = "metadata";
+ for (var i=0; i<gEventTypes.length; i++) {
+ gMedia.addEventListener(gEventTypes[i], listener);
+ }
+}
+
+function addSource(src, type) {
+ var s = document.createElement("source");
+ s.addEventListener("error", source_error);
+ s.src = src;
+ s.type = type;
+ gMedia.appendChild(s);
+ return s;
+}
+
+function prependSource(src, type) {
+ var s = document.createElement("source");
+ s.addEventListener("error", source_error);
+ s.src = src;
+ s.type = type;
+ gMedia.insertBefore(s, gMedia.firstChild);
+ return s;
+}
+
+var gTests = [
+ {
+ // Test 0: adding video to doc, then setting src should load implicitly.
+ create(src, type) {
+ document.body.appendChild(gMedia);
+ gMedia.src = src;
+ },
+ expectedEvents: ['loadstart', 'durationchange', 'loadedmetadata', 'loadeddata']
+ }, {
+ // Test 1: adding video to doc, then adding source.
+ create(src, type) {
+ document.body.appendChild(gMedia);
+ addSource(src, type);
+ },
+ expectedEvents: ['loadstart', 'durationchange', 'loadedmetadata', 'loadeddata']
+ },{
+ // Test 2: video with multiple source, the first of which are bad, we should load the last,
+ // and receive error events for failed loads on the source children.
+ create(src, type) {
+ document.body.appendChild(gMedia);
+ addSource("404a", type);
+ addSource("404b", type);
+ addSource(src, type);
+ },
+ expectedEvents: ['loadstart', 'source_error', 'source_error', 'durationchange', 'loadedmetadata', 'loadeddata']
+ }, {
+ // Test 3: video with bad src, good <source>, ensure that <source> aren't used.
+ create(src, type) {
+ gMedia.src = "404a";
+ addSource(src, type);
+ document.body.appendChild(gMedia);
+ },
+ expectedEvents: ['loadstart', 'error']
+ }, {
+ // Test 4: video with only bad source, loading, then adding a good source
+ // - should resume load.
+ create(src, type) {
+ addSource("404a", type);
+ var s2 = addSource("404b", type);
+ s2.addEventListener("error",
+ function(e) {
+ // Should awaken waiting load, causing successful load.
+ addSource(src, type);
+ });
+ document.body.appendChild(gMedia);
+ },
+ expectedEvents: ['loadstart', 'source_error', 'source_error', 'durationchange', 'loadedmetadata', 'loadeddata']
+ }, {
+ // Test 5: video with only 1 bad source, let it fail to load, then prepend
+ // a good <source> to the video, it shouldn't be selected, because the
+ // "pointer" should be after the last child - the bad source.
+ prepended: false,
+ create(src, type) {
+ var prepended = false;
+ addSource("404a", type);
+ var s2 = addSource("404b", type);
+ s2.addEventListener("error",
+ function(e) {
+ // Should awaken waiting load, causing successful load.
+ if (!prepended) {
+ prependSource(src, type);
+ prepended = true;
+ }
+ });
+ document.body.appendChild(gMedia);
+ },
+ expectedEvents: ['loadstart', 'source_error', 'source_error']
+ }, {
+ // Test 6: (Bug 1165203) preload="none" then followed by an explicit
+ // call to load() should load metadata
+ create(src, type) {
+ gMedia.preload = "none";
+ gMedia.src = src;
+ document.body.appendChild(gMedia);
+ addSource(src, type);
+ gMedia.load();
+ },
+ expectedEvents: ['emptied', 'loadstart', 'durationchange', 'loadedmetadata', 'loadeddata']
+ }
+];
+
+function nextTest() {
+ if (gMedia) {
+ for (var i=0; i<gEventTypes.length; i++) {
+ gMedia.removeEventListener(gEventTypes[i], listener);
+ }
+ removeNodeAndSource(gMedia);
+ gMedia = null;
+ }
+ gEventNum = 0;
+
+ if (gTestNum == gTests.length) {
+ gTestNum = 0;
+ ++gTestFileNum;
+ if (gTestFileNum == gSmallTests.length) {
+ SimpleTest.finish();
+ return;
+ }
+ }
+
+ var src = gSmallTests[gTestFileNum].name;
+ var type = gSmallTests[gTestFileNum].type;
+
+ var t = gTests[gTestNum];
+ gTestNum++;
+
+ createMedia(type.match(/^audio\//) ? "audio" : "video");
+ if (!gMedia.canPlayType(type)) {
+ // Unsupported type, skip to next test
+ nextTest();
+ return;
+ }
+
+ gTestName = "Test " + src + " " + (gTestNum - 1);
+ log("Starting " + gTestName);
+ gExpectedEvents = t.expectedEvents;
+
+ t.create(src, type);
+}
+
+addLoadEvent(nextTest);
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+
+<div id="log" style="font-size: small"></div>
+</body>
+</html>
diff --git a/dom/media/test/test_load_candidates.html b/dom/media/test/test_load_candidates.html
new file mode 100644
index 0000000000..2bc1eb531b
--- /dev/null
+++ b/dom/media/test/test_load_candidates.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=465458
+-->
+<head>
+ <title>Test for Bug 465458</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=465458">Mozilla Bug 465458</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 465458 **/
+
+var manager = new MediaTestManager;
+
+function finish(evt) {
+ var v = evt.target;
+ is(v._error, 2, "Should have received 2 error events before loaded");
+ v._finished = true;
+ // remove error event handler, because this would otherwise
+ // cause a failure on Windows 7, see bug 1024535
+ v.onerror = null;
+ v.remove();
+ manager.finished(v.token);
+}
+
+function errorHandler(evt) {
+ evt.target.parentNode._error++;
+}
+
+var extension = {
+ "audio/wav" : "wav",
+ "audio/x-wav": "wav",
+ "video/ogg" : "ogv",
+ "audio/ogg" : "oga",
+ "video/webm" : "webm"
+};
+
+function startTest(test, token) {
+ var v = document.createElement('video');
+ v.preload = "auto";
+ v.onerror = function(){ok(false,"Error events on source children should not bubble");}
+ v.token = token;
+ manager.started(token);
+ v._error = 0;
+ v._finished = false;
+ v._name = test.name;
+
+ var s1 = document.createElement("source");
+ s1.type = test.type;
+ s1.src = "404." + extension[test.type];
+ s1.addEventListener("error", errorHandler);
+ v.appendChild(s1);
+
+ var s2 = document.createElement("source");
+ s2.type = test.type;
+ s2.src = "test_load_candidates.html"; // definitely an invalid media file, regardless of its actual mime type...
+ s2.addEventListener("error", errorHandler);
+ v.appendChild(s2);
+
+ var s3 = document.createElement("source");
+ s3.type = test.type;
+ s3.src = test.name;
+ v.appendChild(s3);
+
+ v.addEventListener("loadeddata", finish);
+ document.body.appendChild(v);
+}
+
+manager.runTests(gSmallTests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_load_same_resource.html b/dom/media/test/test_load_same_resource.html
new file mode 100644
index 0000000000..f3e6992e8c
--- /dev/null
+++ b/dom/media/test/test_load_same_resource.html
@@ -0,0 +1,106 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test loading of the same resource in multiple elements</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.requestCompleteLog();
+var manager = new MediaTestManager;
+
+function checkDuration(actual, expected, name) {
+ ok(Math.abs(actual - expected) < 0.1,
+ `${name} duration: ${actual} expected: ${expected}`);
+}
+
+function cloneLoaded(event) {
+ var e = event.target;
+ ok(true, `${e.token} loaded OK`);
+ checkDuration(e.duration, e._expectedDuration, e.token);
+ removeNodeAndSource(e);
+ manager.finished(e.token);
+}
+
+function tryClone(e) {
+ var clone = e.cloneNode(false);
+ clone.token = `${e.token}(cloned)`;
+ manager.started(clone.token);
+ manager.finished(e.token);
+
+ // Log events for debugging.
+ var events = ["suspend", "play", "canplay", "canplaythrough", "loadstart", "loadedmetadata",
+ "loadeddata", "playing", "ended", "error", "stalled", "emptied", "abort",
+ "waiting", "pause"];
+ function logEvent(evt) {
+ var event = evt.target;
+ info(`${event.token} got ${evt.type}`);
+ }
+ events.forEach(function(event) {
+ clone.addEventListener(event, logEvent);
+ });
+
+
+ checkDuration(e.duration, e._expectedDuration, e.token);
+ clone._expectedDuration = e._expectedDuration;
+
+ clone.addEventListener("loadeddata", cloneLoaded, {once: true});
+ clone.addEventListener("loadstart", function(evt) {
+ info(`${evt.target.token} starts loading.`);
+ // Since there is only one H264 decoder instance, we have to delete the
+ // decoder of the original element for the cloned element to load. However,
+ // we can't delete the decoder too early otherwise cloning decoder will
+ // fail to kick in. We wait for 'loadstart' event of the cloned element to
+ // know when the decoder is already cloned and we can delete the decoder of
+ // the original element.
+ removeNodeAndSource(e);
+ }, {once: true});
+}
+
+// This test checks that loading the same URI twice in different elements at the same time
+// uses the same resource without doing another network fetch. One of the gCloneTests
+// uses dynamic_resource.sjs to return one resource on the first fetch and a different resource
+// on the second fetch. These resources have different lengths, so if the cloned element
+// does a network fetch it will get a resource with the wrong length and we get a test
+// failure.
+
+async function initTest(test, token) {
+ var e = document.createElement("video");
+ e.preload = "auto";
+ e.src = test.name;
+ e._expectedDuration = test.duration;
+ ok(true, `Trying to load ${test.name}, duration=${test.duration}`);
+ e.token = token;
+ manager.started(token);
+
+ // Since 320x240.ogv is less than 32KB, we need to wait for the
+ // 'suspend' event to ensure the partial block is flushed to the cache
+ // otherwise the cloned resource will create a new channel when it
+ // has no data to read at position 0. The new channel will download
+ // a different file than the original resource and fail the duration
+ // test.
+ let p1 = once(e, "loadeddata");
+ let p2 = once(e, "suspend");
+ await p1;
+ await p2;
+ tryClone(e);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv(
+ {"set": [
+ ["logging.MediaDecoder", "Debug"],
+ ["logging.HTMLMediaElement", "Debug"],
+ ["logging.MediaCache", "Debug"],
+ ]},
+ manager.runTests.bind(manager, gCloneTests, initTest));
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_load_source.html b/dom/media/test/test_load_source.html
new file mode 100644
index 0000000000..95a925b61f
--- /dev/null
+++ b/dom/media/test/test_load_source.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=534571
+-->
+<head>
+ <title>Test for Bug 534571</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=534571">Mozilla Bug 534571</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 534571 **/
+
+// Test that when we load a video from a source child and then change the
+// source's src attribute and load again, that the subsequent loads work.
+
+var v = null;
+var s = null;
+
+function finish(event) {
+ ok(true, "Should have played both videos");
+ SimpleTest.finish();
+}
+
+var first = null;
+var second = null;
+
+function ended(event) {
+ s.type = second.type;
+ s.src = second.name;
+ v.removeEventListener("ended", ended);
+ v.addEventListener("ended", finish);
+ v.load();
+}
+
+// Find 2 videos we can play.
+v = document.createElement('video');
+for (var i=0; i<gPlayTests.length; i++) {
+ if (!v.canPlayType(gPlayTests[i].type))
+ continue;
+ if (!first) {
+ first = gPlayTests[i];
+ } else if (!second) {
+ second = gPlayTests[i];
+ break;
+ }
+}
+
+if (first && second) {
+ s = document.createElement('source');
+ s.type = first.type;
+ s.src = first.name;
+ v.appendChild(s);
+ v.autoplay = true;
+ v.addEventListener("ended", ended);
+ document.body.appendChild(v);
+ SimpleTest.waitForExplicitFinish();
+} else {
+ todo(false, "Need at least two media of supported types for this test!");
+}
+
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_load_source_empty_type.html b/dom/media/test/test_load_source_empty_type.html
new file mode 100644
index 0000000000..9ceee9af46
--- /dev/null
+++ b/dom/media/test/test_load_source_empty_type.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Load resource from source element with empty type</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="manifest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<video id="type"><source src="gizmo.mp4" type></video>
+<video id="type-and-equal"><source src="gizmo.mp4" type=></video>
+<video id="type-and-equal-quotation-marks"><source src="gizmo.mp4" type=""></video>
+<script class="testbody" type="text/javascript">
+/**
+ * This test is used to ensure that media element can start loading when its
+ * child source element with empty type property. We would test following forms,
+ * `type`, `type=` and `type=""`.
+ */
+SimpleTest.waitForExplicitFinish();
+
+const videos = document.getElementsByTagName("video");
+const videoNum = videos.length;
+let loadedElementsNum = 0;
+
+for (let video of videos) {
+ video.addEventListener("loadeddata",
+ () => {
+ ok(true, `loaded element '${video.id}'`);
+ if (++loadedElementsNum == videoNum) {
+ SimpleTest.finish();
+ }
+ }, { once: true});
+}
+</script>
+</body>
+</html>
diff --git a/dom/media/test/test_loop.html b/dom/media/test/test_loop.html
new file mode 100644
index 0000000000..943059edad
--- /dev/null
+++ b/dom/media/test/test_loop.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test looping support</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+function startTest(test, token) {
+ manager.started(token);
+ var v = document.createElement('video');
+ v.token = token;
+ v.src = test.name;
+ v.name = test.name;
+ v.playCount = 0;
+ v.seekingCount = 0;
+ v.seekedCount = 0;
+ v.loop = true;
+
+ v.addEventListener("play", function (e) {
+ e.target.playCount += 1;
+ ok(e.target.playCount == 1, "Should get exactly one play event.");
+ });
+
+ v.addEventListener("seeking", function (e) {
+ e.target.seekingCount += 1;
+ });
+
+ v.addEventListener("seeked", function (e) {
+ e.target.seekedCount += 1;
+ if (e.target.seekedCount == 2) {
+ ok(e.target.seekingCount == 2, "Expect matched pairs of seeking/seeked events.");
+ e.target.loop = false;
+ }
+ });
+
+ v.addEventListener("ended", function (e) {
+ ok(!e.target.loop, "Shouldn't get ended event while looping.");
+ removeNodeAndSource(e.target);
+ manager.finished(e.target.token);
+ });
+
+ document.body.appendChild(v);
+ v.play();
+}
+
+manager.runTests(gSmallTests, startTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_looping_eventsOrder.html b/dom/media/test/test_looping_eventsOrder.html
new file mode 100644
index 0000000000..7d070de72f
--- /dev/null
+++ b/dom/media/test/test_looping_eventsOrder.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Looping events order</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<script type="text/javascript">
+/**
+ * This test is used to ensure the events order when media is looping back to
+ * the start position. We should see events in following order.
+ * 'seeking' -> 'timeupdate' -> 'seeked'.
+ */
+SimpleTest.waitForExplicitFinish();
+
+var tests = [
+ { name:"small-shot.ogg", type:"audio/ogg" },
+ { name:"seek-short.webm", type:"video/webm" }
+];
+
+async function testTimeupdateChanged({name, type}) {
+ info(`- start testPlay for name=${name} -`);
+ const element = document.createElement(getMajorMimeType(type));
+ element.src = name;
+ element.loop = true;
+
+ await once(element, "canplay");
+ ok(await element.play().then(() => true, () => false), `start playing ${name}`);
+
+ let gotTimeUpdated = false;
+ await once(element, "seeking");
+ element.addEventListener("timeupdate", function() {
+ gotTimeUpdated = true;
+ }, {once: true});
+ await once(element, "seeked");
+ ok(gotTimeUpdated, "Got timeupdate between seeking and seeked.");
+
+ removeNodeAndSource(element);
+}
+
+(async function startTest() {
+ for (let test of tests) {
+ await testTimeupdateChanged(test);
+ }
+ SimpleTest.finish();
+})();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_media_selection.html b/dom/media/test/test_media_selection.html
new file mode 100644
index 0000000000..42f5a9bd43
--- /dev/null
+++ b/dom/media/test/test_media_selection.html
@@ -0,0 +1,142 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: media selection</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="application/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+function maketest(attach_media, name, type, check_metadata) {
+ return function (token) {
+ var e = document.createElement('video');
+ e.preload = "metadata";
+ token = name + "-" + token;
+ manager.started(token);
+ var errorRun = false;
+ if (check_metadata) {
+ e.addEventListener('loadedmetadata', function () {
+ ok(e.readyState >= HTMLMediaElement.HAVE_METADATA,
+ 'test ' + token + ' readyState ' + e.readyState + ' expected >= ' + HTMLMediaElement.HAVE_METADATA);
+ is(e.currentSrc.substring(e.currentSrc.length - name.length), name, 'test ' + token);
+ // The load can go idle due to cache size limits
+ ok(e.networkState >= HTMLMediaElement.NETWORK_IDLE,
+ 'test ' + token + ' networkState = ' + e.networkState + ' expected >= ' + HTMLMediaElement.NETWORK_IDLE);
+ check_metadata(e);
+ removeNodeAndSource(e);
+ manager.finished(token);
+ });
+ } else {
+ e.addEventListener('error', function onerror(event) {
+ is(errorRun, false, "error handler should run once only!");
+ errorRun = true;
+ is(e.readyState, HTMLMediaElement.HAVE_NOTHING,
+ 'test ' + token + ' readyState should be HAVE_NOTHING when load fails.');
+ e.removeEventListener('error', onerror, true);
+ removeNodeAndSource(e);
+ manager.finished(token);
+ }, true);
+ }
+ attach_media(e, name, type);
+ }
+}
+
+function set_src(element, name, type) {
+ element.src = name;
+ document.body.appendChild(element);
+}
+
+function add_source(element, name, type) {
+ do_add_source(element, name, type);
+ document.body.appendChild(element);
+}
+
+function do_add_source(element, name, type) {
+ var source = document.createElement('source');
+ if (type) {
+ source.type = type;
+ }
+ source.src = name;
+ element.appendChild(source);
+}
+
+function add_sources_last(element, name, type) {
+ do_add_source(element, name, 'unsupported/type');
+ do_add_source(element, name, type);
+ document.body.appendChild(element);
+}
+
+function add_sources_first(element, name, type) {
+ do_add_source(element, name, type);
+ do_add_source(element, name, 'unsupported/type');
+ document.body.appendChild(element);
+}
+
+function late_add_sources_last(element, name, type) {
+ document.body.appendChild(element);
+ do_add_source(element, name, 'unsupported/type');
+ do_add_source(element, name, type);
+}
+
+function late_add_sources_first(element, name, type) {
+ document.body.appendChild(element);
+ do_add_source(element, name, type);
+ do_add_source(element, name, 'unsupported/type');
+}
+
+var nextTest = 0;
+var subtests = [
+ maketest(add_source, 'unknown.raw', 'bogus/type', null)
+];
+
+var tmpVid = document.createElement('video');
+
+for (var i = 0; i < gSmallTests.length; ++i) {
+ var test = gSmallTests[i];
+ var src = test.name;
+ var type = test.type;
+
+ if (!tmpVid.canPlayType(type))
+ continue;
+
+ // The following nested function hack is to ensure that 'test' is correctly
+ // captured in the closure and we don't end up getting the value 'test'
+ // had in the last iteration of the loop. I blame Brendan.
+ var check = function(t) { return function (e) {
+ checkMetadata(t.name, e, t);
+ }}(test);
+
+ var otherType = type.match(/^video\//) ? "audio/x-wav" : "video/ogg";
+ subtests.push(maketest(set_src, src, null, check),
+ maketest(add_source, src, null, check),
+ maketest(add_source, src, type, check),
+ maketest(add_sources_last, src, null, check),
+ maketest(add_sources_first, src, type, check),
+
+ // type hint matches a decoder, actual type matches different decoder
+ maketest(add_source, src, otherType, check),
+ maketest(add_source, 'unknown.raw', type, null),
+
+ // should not start loading, type excludes it from media candiate list
+ maketest(add_source, src, 'bogus/type', null),
+
+ // element doesn't notice source children attached later, needs bug 462455 fixed
+ maketest(late_add_sources_last, src, type, check),
+ maketest(late_add_sources_first, src, type, check));
+}
+
+function startTest(t, token) {
+ t(token);
+}
+
+manager.runTests(subtests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_media_sniffer.html b/dom/media/test/test_media_sniffer.html
new file mode 100644
index 0000000000..68e2bba8af
--- /dev/null
+++ b/dom/media/test/test_media_sniffer.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: sniffing</title>
+ <meta charset='utf-8'>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var manager = new MediaTestManager;
+
+function finish_test(element) {
+ element.removeEventListener("error", onerror);
+ removeNodeAndSource(element);
+ manager.finished(element.token);
+}
+
+function onmetadataloaded(e) {
+ var t = e.target;
+ ++t.srcIndex;
+ ok(true, "The media loads when loaded via " + t.src);
+ if (t.srcIndex < t.srcList.length) {
+ t.src = t.srcList[t.srcIndex];
+ } else {
+ finish_test(t);
+ }
+}
+
+function onerror(e) {
+ var t = e.target;
+ t.removeEventListener('error', onerror);
+ ok(false, "The media could not be loaded." + t.src + "\n");
+ finish_test(t);
+}
+
+function startTest(test, token) {
+ var elemType = /^audio/.test(test.type) ? "audio" : "video";
+ var element = document.createElement(elemType);
+ // This .sjs file serve the media file without Content-Type header, or with a
+ // specific Content-Type header.
+ var baseSrc = 'contentType.sjs?file=' + test.name;
+ element.srcList = [
+ baseSrc + "&nomime",
+ baseSrc + "&type=application/octet-stream",
+ baseSrc + "&type=audio/wav",
+ baseSrc + "&type=text/html",
+ baseSrc + "&type=absolute_nonsense"
+ ];
+ element.srcIndex = 0;
+ element.src = element.srcList[element.srcIndex];
+ element.token = token;
+ element.controls = true;
+ element.preload = "metadata";
+ document.body.appendChild(element);
+ manager.started(token);
+ element.addEventListener("loadedmetadata", onmetadataloaded);
+ element.addEventListener("error", onerror);
+}
+
+manager.runTests(gSnifferTests, startTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_mediacapabilities_resistfingerprinting.html b/dom/media/test/test_mediacapabilities_resistfingerprinting.html
new file mode 100644
index 0000000000..1f07d1c707
--- /dev/null
+++ b/dom/media/test/test_mediacapabilities_resistfingerprinting.html
@@ -0,0 +1,69 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1369309</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="manifest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1461454">Mozilla Bug 1461454</a>
+<a target="_blank" href="https://gitlab.torproject.org/tpo/applications/tor-browser/-/issues/13543">Tor Issue 13543</a>
+
+<script type="application/javascript">
+async function testWhetherSpoofed(resistFingerprinting) {
+
+ var unsupportedVideoConfiguration = {
+ contentType: 'video/bogus',
+ width: 800,
+ height: 600,
+ bitrate: 3000,
+ framerate: 24,
+ };
+ var supportedVideoConfiguration = {
+ contentType: 'video/webm; codecs="vp09.00.10.08"',
+ width: 800,
+ height: 600,
+ bitrate: 3000,
+ framerate: 24,
+ };
+
+ await SpecialPowers.pushPrefEnv({
+ "set": [
+ ["privacy.resistFingerprinting", resistFingerprinting]
+ ],
+ });
+
+ var result;
+
+ result = await navigator.mediaCapabilities.decodingInfo({
+ type: 'file',
+ video: unsupportedVideoConfiguration
+ });
+ is(result.supported, false, "video/bogus should be unsupported.");
+ is(result.smooth, false, "smooth is false when unsupported.");
+ is(result.powerEfficient, false, "powerEfficient is false when unsupported.");
+
+ result = await navigator.mediaCapabilities.decodingInfo({
+ type: 'file',
+ video: supportedVideoConfiguration
+ });
+ is(result.supported, true, "'video/webm; codecs=\"vp09.00.10.08\"' should be supported.");
+ if (resistFingerprinting) {
+ is(result.smooth, true, "smooth should be spoofed to true in RFP mode.");
+ is(result.powerEfficient, false, "powerEfficient should be spoofed to false in RFP mode.");
+ }
+}
+
+async function start() {
+ await testWhetherSpoofed(true);
+ await testWhetherSpoofed(false);
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+start();
+</script>
+</body>
+</html>
diff --git a/dom/media/test/test_mediarecorder_avoid_recursion.html b/dom/media/test/test_mediarecorder_avoid_recursion.html
new file mode 100644
index 0000000000..0c733b9fc2
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_avoid_recursion.html
@@ -0,0 +1,61 @@
+<html>
+<head>
+ <title>MediaRecorder infinite recursion with requestData() calls in "dataavailable" event</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="gUM_support.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=897776">Mozill
+a Bug 897776</a>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+async function startTest() {
+ try {
+ await setupGetUserMediaTestPrefs();
+ let stream = await navigator.mediaDevices.getUserMedia({audio: true});
+ let mediaRecorder = new MediaRecorder(stream);
+ let count = 0;
+ mediaRecorder.onerror = function () {
+ ok(false, 'Unexpected onerror callback fired');
+ SimpleTest.finish();
+ };
+ mediaRecorder.onwarning = function () {
+ ok(false, 'Unexpected onwarning callback fired');
+ };
+ mediaRecorder.ondataavailable = function (e) {
+ ++count;
+ info("got ondataavailable data size = " + e.data.size);
+ // no more requestData() to prevent busy main thread from starving
+ // the encoding thread
+ if (count == 30) {
+ info("track.stop");
+ stream.getTracks()[0].stop();
+ } else if (count < 30 && mediaRecorder.state == 'recording') {
+ info("requestData again");
+ mediaRecorder.requestData();
+ }
+ };
+ mediaRecorder.onstop = function () {
+ ok(true, "requestData within ondataavailable successfully avoided infinite recursion");
+ SimpleTest.finish();
+ };
+ mediaRecorder.start();
+ info("mediaRecorder start");
+ mediaRecorder.requestData();
+ info("mediaRecorder requestData");
+ } catch (e) {
+ ok(false, 'Unexpected error fired with: ' + e);
+ SimpleTest.finish();
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+startTest();
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/media/test/test_mediarecorder_bitrate.html b/dom/media/test/test_mediarecorder_bitrate.html
new file mode 100644
index 0000000000..693b27e3bf
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_bitrate.html
@@ -0,0 +1,127 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test MediaRecorder Bitrate</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var manager = new MediaTestManager;
+var results = [];
+
+/**
+ * Starts a test on every media recorder file included to check that
+ * the bitrate control works
+ */
+function startTest(test, token) {
+ manager.started(token);
+ runTest(test, token, 1000000);
+ runTest(test, token, 100000);
+}
+
+function runTest(test, token, bitrate) {
+ var element = document.createElement('video');
+
+ element.token = token;
+
+ element.src = test.name;
+ element.preload = "metadata";
+ element.onloadedmetadata = function () {
+ info("loadedmetadata");
+ const stream = element.mozCaptureStreamUntilEnded();
+ element.onloadedmetadata = null;
+ element.play();
+
+ const mediaRecorder = new MediaRecorder(stream, {videoBitsPerSecond: bitrate});
+ mediaRecorder.start();
+ is(mediaRecorder.state, 'recording', 'Media recorder should be recording');
+ is(mediaRecorder.stream, stream,
+ 'Media recorder stream = element stream at the start of recording');
+
+ var onStopFired = false;
+ var onDataAvailableFired = false;
+ var encoded_size = 0;
+
+ mediaRecorder.onerror = function () {
+ ok(false, 'Unexpected onerror callback fired');
+ };
+
+ mediaRecorder.onwarning = function () {
+ ok(false, 'Unexpected onwarning callback fired');
+ };
+
+ // This handler verifies that only a single onstop event handler is fired.
+ mediaRecorder.onstop = function () {
+ if (onStopFired) {
+ ok(false, 'onstop unexpectedly fired more than once');
+ } else {
+ onStopFired = true;
+
+ // ondataavailable should always fire before onstop
+ if (onDataAvailableFired) {
+ ok(true, 'onstop fired after ondataavailable');
+ info("test " + test.name + " encoded@" + bitrate + "=" + encoded_size);
+ if (results[test.name]) {
+ var big, small, temp;
+ big = {};
+ big.bitrate = bitrate;
+ big.size = encoded_size;
+ small = results[test.name];
+ // Don't assume the order that these will finish in
+ if (results[test.name].bitrate > bitrate) {
+ temp = big;
+ big = small;
+ small = temp;
+ }
+ // Ensure there is a big enough difference in the encoded
+ // sizes
+ ok(small.size*1.25 < big.size,
+ test.name + ' encoded@' + big.bitrate + '=' + big.size +
+ ' > encoded@' + small.bitrate + '=' + small.size);
+ manager.finished(token);
+ } else {
+ results[test.name] = {};
+ results[test.name].bitrate = bitrate;
+ results[test.name].size = encoded_size;
+ }
+ } else {
+ ok(false, 'onstop fired without an ondataavailable event first');
+ }
+ }
+ };
+
+ // This handler verifies that only a single ondataavailable event handler
+ // is fired with the blob generated having greater than zero size
+ // and a correct mime type.
+ mediaRecorder.ondataavailable = function (evt) {
+ if (onDataAvailableFired) {
+ ok(false, 'ondataavailable unexpectedly fired more than once');
+ } else {
+ onDataAvailableFired = true;
+
+ ok(evt instanceof BlobEvent,
+ 'Events fired from ondataavailable should be BlobEvent');
+ is(evt.type, 'dataavailable',
+ 'Event type should dataavailable');
+ ok(evt.data.size > 0,
+ 'Blob data received should be greater than zero');
+ encoded_size = evt.data.size;
+
+ // onstop should not have fired before ondataavailable
+ if (onStopFired) {
+ ok(false, 'ondataavailable unexpectedly fired later than onstop');
+ manager.finished(token);
+ }
+ }
+ };
+ };
+}
+
+manager.runTests(gMediaRecorderVideoTests, startTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_mediarecorder_creation.html b/dom/media/test/test_mediarecorder_creation.html
new file mode 100644
index 0000000000..cd23cb7a96
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_creation.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test MediaRecorder Creation</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var manager = new MediaTestManager;
+
+/**
+ * Starts a test on every media recorder file included to check that
+ * a media recorder object created with a stream derived from a media
+ * element with that file produces the correct starting attribute values.
+ */
+function startTest(test, token) {
+ var element = document.createElement('audio');
+
+ element.token = token;
+ manager.started(token);
+
+ element.src = test.name;
+ element.test = test;
+ element.stream = element.mozCaptureStreamUntilEnded();
+
+ var mediaRecorder = new MediaRecorder(element.stream);
+
+ is(mediaRecorder.stream, element.stream,
+ 'Stream should be provided stream on creation');
+ is(mediaRecorder.mimeType, '',
+ 'mimeType should be an empty string on creation');
+ is(mediaRecorder.state, 'inactive',
+ 'state should be inactive on creation');
+
+ manager.finished(token);
+}
+
+manager.runTests(gMediaRecorderTests, startTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_mediarecorder_creation_fail.html b/dom/media/test/test_mediarecorder_creation_fail.html
new file mode 100644
index 0000000000..2630f069b0
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_creation_fail.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test MediaRecorder Record with media.ogg.enabled = false</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+function testThrows(stream, options, errorName, message, testName) {
+ try {
+ new MediaRecorder(stream, options);
+ } catch(e) {
+ is(e.name, errorName, testName);
+ is(e.message, message, testName);
+ return;
+ }
+ ok(!errorName, testName);
+}
+(async () => {
+ SimpleTest.waitForExplicitFinish();
+ await SpecialPowers.pushPrefEnv({set: [
+ ["media.ogg.enabled", false],
+ ["media.encoder.webm.enabled", false],
+ ]});
+ const stream = new AudioContext().createMediaStreamDestination().stream;
+ testThrows(
+ stream, {mimeType: "audio/ogg"}, "NotSupportedError",
+ "MediaRecorder constructor: audio/ogg indicates an unsupported container",
+ "Creating an ogg recorder without ogg support throws");
+ testThrows(
+ stream, {mimeType: "audio/webm"}, "NotSupportedError",
+ "MediaRecorder constructor: audio/webm indicates an unsupported container",
+ "Creating a webm recorder without webm support throws");
+ testThrows(
+ stream, {mimeType: "video/webm"}, "NotSupportedError",
+ "MediaRecorder constructor: video/webm indicates an unsupported container",
+ "Creating a webm recorder without webm support throws");
+ testThrows(
+ stream, {mimeType: "apa/bepa"}, "NotSupportedError",
+ "MediaRecorder constructor: apa/bepa is not a valid media type",
+ "Creating a recorder for a bogus mime type throws");
+ testThrows(
+ stream, {}, null, null,
+ "Creating a default recorder never throws, even without container support");
+ const recorder = new MediaRecorder(stream);
+ try {
+ recorder.start();
+ ok(false, "Starting a recorder without container support should throw");
+ } catch(e) {
+ is(e.name, "NotSupportedError",
+ "Starting a recorder without container support throws");
+ }
+ SimpleTest.finish();
+})();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_mediarecorder_fires_start_event_once_when_erroring.html b/dom/media/test/test_mediarecorder_fires_start_event_once_when_erroring.html
new file mode 100644
index 0000000000..537e1dbb47
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_fires_start_event_once_when_erroring.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1395022 - MediaRecorder fires start event when erroring.</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1395022">Mozilla Bug 1395022</a>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+function startTest() {
+ let audioContext = new AudioContext();
+ let destination1 = audioContext.createMediaStreamDestination();
+ let oscilator = audioContext.createOscillator();
+ oscilator.connect(destination1);
+ oscilator.start();
+
+ let destination2 = audioContext.createMediaStreamDestination();
+
+ let rec = new MediaRecorder(destination1.stream);
+
+ let numStartEvents = 0;
+
+ rec.onstart = () => {
+ numStartEvents += 1;
+ is(numStartEvents, 1, "One start event should be fired by the recorder");
+ // Trigger an error in the recorder
+ destination1.stream.addTrack(destination2.stream.getTracks()[0]);
+ };
+
+ rec.onerror = () => {
+ is(numStartEvents, 1, "One start event should have been fired by the recorder");
+ SimpleTest.finish();
+ };
+
+ rec.start();
+}
+
+SimpleTest.waitForExplicitFinish();
+startTest();
+
+</script>
+</head>
+</html> \ No newline at end of file
diff --git a/dom/media/test/test_mediarecorder_multipletracks.html b/dom/media/test/test_mediarecorder_multipletracks.html
new file mode 100644
index 0000000000..40fbfb92ce
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_multipletracks.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test that MediaRecorder fires an error event when started with >1 track of a kind</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/dom/media/webrtc/tests/mochitests/head.js"></script>
+</head>
+<body>
+<script class="testbody">
+"use strict";
+
+(async () => {
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.requestFlakyTimeout("Failure timeouts");
+
+ try {
+ const stream = new MediaStream;
+ const recorder = new MediaRecorder(stream);
+
+ {
+ info("Testing with two audio tracks");
+ const ac = new AudioContext;
+ const track = ac.createMediaStreamDestination().stream.getTracks()[0];
+ stream.addTrack(track);
+ stream.addTrack(track.clone());
+ mustThrowWith("Starting with multiple audio tracks",
+ "NotSupportedError",
+ () => recorder.start());
+ await Promise.all([
+ haveNoEvent(recorder, "start"),
+ haveNoEvent(recorder, "error"),
+ haveNoEvent(recorder, "stop"),
+ ]);
+ for (let t of stream.getTracks()) {
+ stream.removeTrack(t);
+ t.stop();
+ }
+ }
+
+ {
+ info("Testing with two video tracks");
+ const canvas = document.createElement("canvas");
+ canvas.getContext("2d");
+ const track = canvas.captureStream().getTracks()[0];
+ stream.addTrack(track);
+ stream.addTrack(track.clone());
+ mustThrowWith("Starting with multiple video tracks",
+ "NotSupportedError",
+ () => recorder.start());
+ await Promise.all([
+ haveNoEvent(recorder, "start"),
+ haveNoEvent(recorder, "error"),
+ haveNoEvent(recorder, "stop"),
+ ]);
+ for (let t of stream.getTracks()) {
+ stream.removeTrack(t);
+ t.stop();
+ }
+ }
+ } catch(e) {
+ ok(false, `${e}\n${e.stack}`);
+ } finally {
+ SimpleTest.finish();
+ }
+})();
+</script>
+</body>
+</html>
diff --git a/dom/media/test/test_mediarecorder_onerror_pause.html b/dom/media/test/test_mediarecorder_onerror_pause.html
new file mode 100644
index 0000000000..55ee5fb535
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_onerror_pause.html
@@ -0,0 +1,107 @@
+<html>
+<head>
+ <title>Bug 957439 - Media Recording - Assertion fail at Pause if unsupported input stream.</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="gUM_support.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=957439">Mozilla Bug 957439</a>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+function unexpected(e) {
+ ok(false, `Got unexpected ${e.type} event`);
+}
+
+
+async function startTest() {
+ // also do general checks on mimetype support for audio-only
+ ok(MediaRecorder.isTypeSupported("audio/ogg"),
+ 'Should support audio/ogg');
+ ok(MediaRecorder.isTypeSupported('audio/ogg; codecs=opus'),
+ 'Should support audio/ogg+opus');
+ ok(!MediaRecorder.isTypeSupported('audio/ogg; codecs=foobar'),
+ 'Should not support audio/ogg + unknown_codec');
+ ok(MediaRecorder.isTypeSupported("video/webm"),
+ 'Should support video/webm');
+ ok(!MediaRecorder.isTypeSupported("video/mp4"),
+ 'Should not support video/mp4');
+
+ try {
+ await setupGetUserMediaTestPrefs();
+ const expectedMimeType = 'video/webm; codecs="vp8, opus"';
+ const stream = await navigator.mediaDevices.getUserMedia({audio: true, video: true});
+ const [audioTrack] = stream.getAudioTracks();
+
+ // Expected event sequence should be:
+ // 1. start
+ // 2. error (from removed track)
+ // 3. dataavailable
+ // 4. stop
+ const mediaRecorder = new MediaRecorder(stream);
+ is(mediaRecorder.stream, stream, 'Stream should be provided on creation');
+
+ mediaRecorder.onstart = unexpected;
+ mediaRecorder.onerror = unexpected;
+ mediaRecorder.ondataavailable = unexpected;
+ mediaRecorder.onstop = unexpected;
+
+ mediaRecorder.start();
+ is(mediaRecorder.state, 'recording', 'state should be recording');
+ is(mediaRecorder.mimeType, '', 'mimetype should be unset');
+
+ await new Promise(r => mediaRecorder.onstart = r);
+ mediaRecorder.onstart = unexpected;
+ ok(true, 'start event fired');
+ is(mediaRecorder.mimeType, expectedMimeType, 'mimetype should be set');
+
+ // Trigger an error
+ stream.removeTrack(audioTrack);
+
+ const err = await new Promise(r => mediaRecorder.onerror = r);
+ mediaRecorder.onerror = unexpected;
+ ok(true, 'error event fired');
+ is(err.error.name, 'InvalidModificationError',
+ 'Error name should be InvalidModificationError.');
+ ok(err.error.stack.includes('test_mediarecorder_onerror_pause.html'),
+ 'Events fired from onerror should include an error with a stack trace indicating ' +
+ 'an error in this test');
+ is(mediaRecorder.mimeType, '', 'mimetype should be unset');
+ is(mediaRecorder.state, 'inactive', 'state is inactive');
+
+ try {
+ mediaRecorder.pause();
+ ok(false, 'pause should fire an exception if called on an inactive recorder');
+ } catch(e) {
+ ok(e instanceof DOMException, 'pause should fire an exception ' +
+ 'if called on an inactive recorder');
+ is(e.name, 'InvalidStateError', 'Exception name should be InvalidStateError');
+ }
+
+ const evt = await new Promise(r => mediaRecorder.ondataavailable = r);
+ mediaRecorder.ondataavailable = unexpected;
+ ok(true, 'dataavailable event fired');
+ isnot(evt.data.size, 0, 'data size should not be zero');
+ ok(evt instanceof BlobEvent,
+ 'Events fired from ondataavailable should be BlobEvent');
+ is(evt.data.type, expectedMimeType, 'blob mimeType is set');
+
+ await new Promise(r => mediaRecorder.onstop = r);
+ mediaRecorder.onstop = unexpected;
+ ok(true, 'onstop event fired');
+ is(mediaRecorder.state, 'inactive', 'state should be inactive');
+ } catch (err) {
+ ok(false, `Unexpected error fired with: ${err}`);
+ } finally {
+ SimpleTest.finish();
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+window.onload = startTest;
+
+</script>
+</head>
+</html>
diff --git a/dom/media/test/test_mediarecorder_pause_resume_video.html b/dom/media/test/test_mediarecorder_pause_resume_video.html
new file mode 100644
index 0000000000..a9b3d6a034
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_pause_resume_video.html
@@ -0,0 +1,130 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test MediaRecorder Recording doesn't record during pause</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<div id="content">
+ <canvas id="video-src-canvas"></canvas>
+ <video id="recorded-video"></video>
+</div>
+<script class="testbody" type="text/javascript">
+
+function startTest() {
+ // Setup canvas and take a stream from it
+ let canvas = document.getElementById("video-src-canvas");
+
+ let canvas_size = 100;
+ let new_canvas_size = 50;
+
+ canvas.width = canvas.height = canvas_size;
+
+ let helper = new CaptureStreamTestHelper2D(100, 100);
+ helper.drawColor(canvas, helper.red);
+
+ let canvasStream = canvas.captureStream();
+ // Canvas set up
+
+ // Check values for events
+ let numDataAvailabledRaised = 0;
+ // Recorded data that will be playback.
+ let blob;
+
+ let mediaRecorder = new MediaRecorder(canvasStream);
+ is(mediaRecorder.stream, canvasStream,
+ "Media recorder stream = canvas stream at the start of recording");
+
+ mediaRecorder.onwarning = () => ok(false, "warning unexpectedly fired");
+
+ mediaRecorder.onerror = () => ok(false, "Recording failed");
+
+ mediaRecorder.ondataavailable = ev => {
+ info("Got 'dataavailable' event");
+ ++numDataAvailabledRaised;
+ // Save recorded data for playback
+ blob = ev.data;
+ };
+
+ mediaRecorder.onstart = () => {
+ info("Got 'start' event");
+ // We just want one frame encoded before we pause
+ mediaRecorder.pause();
+ // We may rewrite this once we settle Bug 1363915, could listen for pause event instead
+ is(mediaRecorder.state, 'paused', 'Media recorder should be paused');
+
+ // Wait a while, then while paused draw blue at another size, then again
+ // green at the original size and resume.
+ let numberOfPaintsSincePause = 0;
+ window.requestAnimationFrame(function draw() {
+ numberOfPaintsSincePause++;
+ if (numberOfPaintsSincePause == 8) {
+ canvas.width = canvas.height = new_canvas_size;
+ helper.drawColor(canvas, helper.blue);
+ } else if (numberOfPaintsSincePause == 60) {
+ canvas.width = canvas.height = canvas_size;
+ helper.drawColor(canvas, helper.green);
+ } else if (numberOfPaintsSincePause == 68) {
+ // Waited 8 draws since changing canvas to green, should be safe to resume
+ mediaRecorder.resume();
+ } else if (numberOfPaintsSincePause > 120) {
+ mediaRecorder.stop();
+ return; // Early return, we don't want to request any more animation frames
+ }
+ window.requestAnimationFrame(draw);
+ });
+ };
+
+ mediaRecorder.onstop = () => {
+ info("Got 'stop' event");
+ is(mediaRecorder.state, 'inactive', 'Media recorder should be incative after stop');
+ is(numDataAvailabledRaised, 1, "Expected 1 dataavailable event");
+
+ ok(blob, "Should have gotten a data blob");
+ let video = document.getElementById("recorded-video");
+ video.id = "recorded-video";
+ video.src = URL.createObjectURL(blob);
+ // Setup a check to make sure we don't play back any blue
+ let checkVideoHasNoBlue = () => {
+ if(helper.isPixel(helper.getPixel(video), helper.blue, 128)) {
+ ok(false, "Video should have no blue frames");
+ // Remove handler so we don't spam the log
+ video.ontimeupdate = null;
+ }
+ };
+ video.ontimeupdate = checkVideoHasNoBlue;
+ video.onerror = () => {
+ ok(false, "Should be able to play the recording. Got error. code=" + video.error.code);
+ SimpleTest.finish();
+ };
+ video.onended = () => {
+ ok(helper.isPixel(helper.getPixel(video), helper.green, 128), "Last frame should be green");
+ SimpleTest.finish();
+ };
+ // The video will resize once it loads its metadata, only listen for resizes after that
+ video.onloadedmetadata = () => {
+ ok(video.videoWidth === canvas_size && video.videoHeight === canvas_size,
+ "video element should be same size as canvas once metadata is loaded");
+ // We shouldn't have any resize events once the video is loaded
+ video.onresize = () => {
+ ok(false, "Should not have any resize events!");
+ };
+ };
+
+ video.play();
+ };
+
+ mediaRecorder.start();
+ is(mediaRecorder.state, "recording", "Media recorder should be recording");
+}
+
+SimpleTest.waitForExplicitFinish();
+startTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_mediarecorder_playback_can_repeat.html b/dom/media/test/test_mediarecorder_playback_can_repeat.html
new file mode 100644
index 0000000000..4cf2735088
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_playback_can_repeat.html
@@ -0,0 +1,87 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test MediaRecorder Recording creates videos that can playback more than once</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<div id="content">
+ <canvas id="video-src-canvas"></canvas>
+ <video id="recorded-video" loop></video>
+</div>
+<script class="testbody" type="text/javascript">
+
+(async function() {
+ try {
+ SimpleTest.waitForExplicitFinish();
+ await startTest();
+ } catch(e) {
+ ok(false, `Got error msg '${e}'`);
+ } finally {
+ SimpleTest.finish();
+ }
+})();
+
+async function startTest() {
+ let canvas = document.getElementById("video-src-canvas");
+
+ let canvas_size = 100;
+ canvas.width = canvas.height = canvas_size;
+ let helper = new CaptureStreamTestHelper2D(canvas_size, canvas_size);
+ helper.drawColor(canvas, helper.red);
+
+ let stream = canvas.captureStream();
+ let mediaRecorder = new MediaRecorder(stream);
+ is(mediaRecorder.stream, stream,
+ "Media recorder stream = canvas stream at the beginning of recording");
+ new Promise(r => mediaRecorder.onerror = err => r(err)).then(
+ err => Promise.reject(`MediaRecorder: error unexpectedly fired. Code ${err.name}`));
+
+ mediaRecorder.start();
+ await new Promise(r => mediaRecorder.onstart = r);
+ info("Media recorder started");
+ // Feed some more data into the recorder so the output has a non trivial duration.
+ // This avoids the case where we have only 1 frame in the output, which breaks
+ // looping (this is expected as a WebM with 1 video frame has no duration).
+ let counter = 0;
+ let draw = () => {
+ counter++;
+ helper.drawColor(canvas, helper.blue);
+ if(counter > 2) {
+ mediaRecorder.stop();
+ return;
+ }
+ requestAnimationFrame(draw);
+ };
+ requestAnimationFrame(draw);
+
+ // When recorder is stopped get recorded data.
+ const data = await new Promise(r => mediaRecorder.ondataavailable = ev => r(ev));
+ info(`Media recorder get availiable data`);
+ const blob = data.data;
+
+ await new Promise(r => mediaRecorder.onstop = r);
+ info("Media recorder stopped");
+ ok(blob, "Should have gotten a data blob");
+ const video = document.getElementById("recorded-video");
+ new Promise(r => video.onerror = err => r(err)).then(
+ err => Promise.reject(`Video: error unexpectedly fired. Code ${err.code}`));
+ video.src = URL.createObjectURL(blob);
+ video.play();
+ await new Promise(r => video.onplaying = r);
+ for (let idx = 0; idx < 2; idx++) {
+ await new Promise(r => video.onseeking = r);
+ ok(true, `waiting until video finishes seeking`);
+ await new Promise(r => video.onseeked = r);
+ ok(true, "video finished seeked");
+ }
+ video.pause();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_mediarecorder_principals.html b/dom/media/test/test_mediarecorder_principals.html
new file mode 100644
index 0000000000..00dcaa5a5b
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_principals.html
@@ -0,0 +1,132 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=489415
+-->
+<head>
+ <title>Test for MediaRecorder Reaction to Principal Change</title>
+ <script type="application/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<div>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1018299">Test for MediaRecorder Principal Handling</a>
+</div>
+
+<pre id="test">
+<script type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+let throwOutside = e => setTimeout(() => { throw e; });
+
+// Loading data from a resource that changes origins while streaming should
+// be detected by the media cache and result in a null principal so that the
+// MediaRecorder usages below fail.
+
+// This test relies on midflight-redirect.sjs returning the the first quarter of
+// the resource as a byte range response, and then hanging up, and when Firefox
+// requests the remainder midflight-redirect.sjs serves a redirect to another origin.
+
+async function testPrincipals(resource) {
+ if (!resource) {
+ todo(false, "No types supported");
+ return;
+ }
+ await testPrincipals1(resource);
+ await testPrincipals2(resource);
+}
+
+function makeVideo() {
+ let video = document.createElement("video");
+ video.preload = "metadata";
+ video.controls = true;
+ document.body.appendChild(video);
+ return video;
+}
+
+// First test: Load file from same-origin first, then get redirected to
+// another origin before attempting to record stream.
+async function testPrincipals1(resource) {
+ let video = makeVideo();
+ video.src =
+ "http://mochi.test:8888/tests/dom/media/test/midflight-redirect.sjs" +
+ "?resource=" + resource.name + "&type=" + resource.type;
+
+ let errorBarrier = once(video, "error");
+ // Wait for the video to load to metadata. We can then start capturing.
+ // Must also handle the download bursting and hitting the error before we
+ // reach loadedmetadata. Normally we reach loadedmetadata first, but
+ // rarely we hit the redirect first.
+ await Promise.race([once(video, "loadedmetadata"), errorBarrier]);
+
+ let rec = new MediaRecorder(video.mozCaptureStreamUntilEnded());
+ video.play();
+
+ // Wait until we hit a playback error. This means our download has hit the redirect.
+ await errorBarrier;
+
+ // Try to record, it should be blocked with a security error.
+ try {
+ rec.start();
+ ok(false, "mediaRecorder.start() must throw SecurityError, but didn't throw at all");
+ } catch (ex) {
+ is(ex.name, "SecurityError", "mediaRecorder.start() must throw SecurityError");
+ }
+ removeNodeAndSource(video);
+}
+
+// Second test: Load file from same-origin first, but record ASAP, before
+// getting redirected to another origin.
+async function testPrincipals2(resource) {
+ let video = makeVideo();
+ video.src =
+ "http://mochi.test:8888/tests/dom/media/test/midflight-redirect.sjs" +
+ "?resource=" + resource.name + "&type=" + resource.type;
+
+ // Wait for the video to load to metadata. We can then start capturing.
+ // Must also handle the download bursting and hitting the error before we
+ // reach loadedmetadata. Normally we reach loadedmetadata first, but
+ // rarely we hit the redirect first.
+ await Promise.race([once(video, "loadedmetadata"), once(video, "error")]);
+
+ let ended = false;
+ once(video, "ended", () => ended = true);
+
+ // Start capturing. It should work.
+ let rec;
+ let errorBarrier;
+ try {
+ rec = new MediaRecorder(video.mozCaptureStreamUntilEnded());
+ errorBarrier = nextEvent(rec, "error");
+ rec.start();
+ ok(true, "mediaRecorder.start() should not throw here, and didn't");
+ } catch (ex) {
+ ok(false, "mediaRecorder.start() unexpectedly threw " + ex.name + " (" + ex.message + ")");
+ }
+
+ // Play the video, this should result in a SecurityError on the recorder.
+ let hasStopped = once(rec, "stop");
+ video.play();
+ let error = (await errorBarrier).error;
+ is(error.name, "SecurityError", "mediaRecorder.onerror must fire SecurityError");
+ ok(error.stack.includes('test_mediarecorder_principals.html'),
+ 'Events fired from onerror should include an error with a stack trace indicating ' +
+ 'an error in this test');
+ is(ended, false, "Playback should not have reached end");
+ await hasStopped;
+ is(ended, false, "Playback should definitely not have reached end");
+
+ removeNodeAndSource(video);
+}
+
+testPrincipals({ name:"pixel_aspect_ratio.mp4", type:"video/mp4", duration:28 })
+ .catch(e => throwOutside(e))
+ .then(() => SimpleTest.finish());
+
+</script>
+</pre>
+
+</body>
+</html>
diff --git a/dom/media/test/test_mediarecorder_record_4ch_audiocontext.html b/dom/media/test/test_mediarecorder_record_4ch_audiocontext.html
new file mode 100644
index 0000000000..99f57f181e
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_record_4ch_audiocontext.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test MediaRecorder Record AudioContext with four channels</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+
+<script class="testbody" type="text/javascript">
+
+function startTest() {
+ var context = new AudioContext();
+ var buffer = context.createBuffer(4, 80920, context.sampleRate);
+ for (var i = 0; i < 80920; ++i) {
+ for(var j = 0; j < 4; ++j) {
+ buffer.getChannelData(j)[i] = Math.sin(1000 * 2 * Math.PI * i / context.sampleRate);
+ }
+ }
+
+ var source = context.createBufferSource();
+ source.buffer = buffer;
+ source.loop = true;
+ var dest = context.createMediaStreamDestination();
+ var stopTriggered = false;
+ var onstopTriggered = false;
+ dest.channelCount = 4;
+ var expectedMimeType = 'audio/ogg; codecs=opus';
+ source.channelCountMode = 'explicit';
+ source.connect(dest);
+ var elem = document.createElement('audio');
+ elem.srcObject = dest.stream;
+ source.start(0);
+ elem.play();
+ let mMediaRecorder = new MediaRecorder(dest.stream);
+ mMediaRecorder.onwarning = function() {
+ ok(false, 'onwarning unexpectedly fired');
+ };
+
+ mMediaRecorder.onerror = function() {
+ ok(false, 'onerror unexpectedly fired');
+ };
+
+ mMediaRecorder.onstop = function() {
+ ok(true, 'onstop fired successfully');
+ is(mMediaRecorder.state, 'inactive', 'check recording status is inactive');
+ onstopTriggered = true;
+ SimpleTest.finish();
+ };
+ mMediaRecorder.ondataavailable = function (e) {
+ ok(e.data.size > 0, 'check blob has data');
+ is(e.data.type, expectedMimeType, 'blob should have expected mimetype');
+ if (!stopTriggered) {
+ is(mMediaRecorder.mimeType, expectedMimeType, 'recorder should have expected mimetype');
+ mMediaRecorder.stop();
+ is(mMediaRecorder.mimeType, '', 'recorder should have reset its mimetype');
+ stopTriggered = true;
+ } else if (onstopTriggered) {
+ ok(false, 'ondataavailable should come before onstop event');
+ }
+ };
+ try {
+ mMediaRecorder.start(1000);
+ is('recording', mMediaRecorder.state, "check record state recording");
+ } catch (e) {
+ ok(false, 'Can t record audio context');
+ }
+}
+
+startTest();
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_mediarecorder_record_addtracked_stream.html b/dom/media/test/test_mediarecorder_record_addtracked_stream.html
new file mode 100644
index 0000000000..639531f7f2
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_record_addtracked_stream.html
@@ -0,0 +1,182 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test MediaRecorder recording a constructed MediaStream</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <script src="/tests/dom/media/webrtc/tests/mochitests/head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<div id="content">
+</div>
+<script>
+SimpleTest.waitForExplicitFinish();
+runTestWhenReady(async () => {
+ const canvas = document.createElement("canvas");
+ canvas.width = canvas.height = 100;
+ document.getElementById("content").appendChild(canvas);
+
+ const helper = new CaptureStreamTestHelper2D(100, 100);
+ helper.drawColor(canvas, helper.red);
+
+ const audioCtx = new AudioContext();
+ const osc = audioCtx.createOscillator();
+ osc.frequency.value = 1000;
+ osc.start();
+ const dest = audioCtx.createMediaStreamDestination();
+ osc.connect(dest);
+
+ const stream = dest.stream;
+
+ // Timeouts are experienced intermittently in Linux due to no sound in the
+ // destination. As a workaround wait for the source sound to arrive.
+ const sourceAnalyser = new AudioStreamAnalyser(audioCtx, stream);
+ const sourceAudioReady = sourceAnalyser.waitForAnalysisSuccess(array => {
+ const freq = osc.frequency.value;
+ const lowerFreq = freq / 2;
+ const upperFreq = freq + 1000;
+ const lowerAmp = array[sourceAnalyser.binIndexForFrequency(lowerFreq)];
+ const freqAmp = array[sourceAnalyser.binIndexForFrequency(freq)];
+ const upperAmp = array[sourceAnalyser.binIndexForFrequency(upperFreq)];
+ info("Analysing source audio. "
+ + lowerFreq + ": " + lowerAmp + ", "
+ + freq + ": " + freqAmp + ", "
+ + upperFreq + ": " + upperAmp);
+ return lowerAmp < 50 && freqAmp > 200 && upperAmp < 50;
+ });
+ await sourceAudioReady;
+ info("Source Audio content ok");
+
+ canvas.captureStream(0).getVideoTracks().forEach(t => stream.addTrack(t));
+
+ const blobs = [];
+
+ let mediaRecorder = new MediaRecorder(stream);
+ is(mediaRecorder.stream, stream,
+ "Media recorder stream = constructed stream at the start of recording");
+
+
+ mediaRecorder.ondataavailable = evt => {
+ info("ondataavailable fired");
+
+ is(mediaRecorder.state, "inactive", "Blob received after stopping");
+ is(blobs.length, 0, "This is the first and only blob");
+ ok(evt instanceof BlobEvent,
+ "Events fired from ondataavailable should be BlobEvent");
+ is(evt.type, "dataavailable",
+ "Event type should dataavailable");
+ ok(evt.data.size >= 0,
+ "Blob data size received is greater than or equal to zero");
+
+ blobs.push(evt.data);
+ };
+
+ const stopped = haveEvent(mediaRecorder, "stop", wait(5000, new Error("Timeout")));
+ const stoppedNoErrors = Promise.all([
+ stopped,
+ haveNoEvent(mediaRecorder, "warning", stopped),
+ haveNoEvent(mediaRecorder, "error", stopped)
+ ]);
+
+ mediaRecorder.start();
+ is(mediaRecorder.state, "recording", "Media recorder should be recording");
+
+ await haveEvent(mediaRecorder, "start", wait(5000, new Error("Timeout")));
+ info("onstart fired");
+
+ // The recording can be too short to cause any checks with
+ // waitForAnalysisSuccess(). Waiting a bit here solves this.
+ await wait(500);
+
+ is(mediaRecorder.state, "recording",
+ "Media recorder is recording before being stopped");
+ mediaRecorder.stop();
+ is(mediaRecorder.state, "inactive",
+ "Media recorder is inactive after being stopped");
+ is(mediaRecorder.stream, stream,
+ "Media recorder stream = constructed stream post recording");
+
+ await stoppedNoErrors;
+ info("Got 'stop' event");
+
+ ok(blobs.length == 1, "Should have gotten one data blob");
+
+ // Clean up recording sources
+ osc.stop();
+ stream.getTracks().forEach(t => t.stop());
+
+ // Sanity check the recording
+ const video = document.createElement("video");
+ document.getElementById("content").appendChild(video);
+ video.id = "recorded-video";
+
+ const blob = new Blob(blobs);
+ ok(blob.size > 0, "Recorded blob should contain data");
+
+ video.src = URL.createObjectURL(blob);
+ video.preload = "metadata";
+
+ info("Waiting for metadata to be preloaded");
+
+ await haveEvent(video, "loadedmetadata", wait(5000, new Error("Timeout")));
+ info("Playback of recording loaded metadata");
+
+ const recordingStream = video.mozCaptureStream();
+ is(recordingStream.getVideoTracks().length, 1,
+ "Recording should have one video track");
+ is(recordingStream.getAudioTracks().length, 1,
+ "Recording should have one audio track");
+
+ const ended = haveEvent(video, "ended", wait(5000, new Error("Timeout")));
+ const endedNoError = Promise.all([
+ ended,
+ haveNoEvent(video, "error", ended),
+ ]);
+
+ const analyser = new AudioStreamAnalyser(audioCtx, recordingStream);
+ const audioReady = analyser.waitForAnalysisSuccess(array => {
+ const freq = osc.frequency.value;
+ const lowerFreq = freq / 2;
+ const upperFreq = freq + 1000;
+ const lowerAmp = array[analyser.binIndexForFrequency(lowerFreq)];
+ const freqAmp = array[analyser.binIndexForFrequency(freq)];
+ const upperAmp = array[analyser.binIndexForFrequency(upperFreq)];
+ info("Analysing audio. "
+ + lowerFreq + ": " + lowerAmp + ", "
+ + freq + ": " + freqAmp + ", "
+ + upperFreq + ": " + upperAmp);
+ return lowerAmp < 50 && freqAmp > 200 && upperAmp < 50;
+ }, endedNoError.then(() => new Error("Audio check failed")));
+
+ const videoReady = helper.pixelMustBecome(
+ video, helper.red, {
+ threshold: 128,
+ infoString: "Should become red",
+ cancelPromise: endedNoError.then(() => new Error("Video check failed")),
+ });
+
+ video.play();
+
+ try {
+ await endedNoError;
+ } finally {
+ analyser.disconnect();
+ let url = video.src;
+ video.src = "";
+ URL.revokeObjectURL(url);
+ }
+
+ info("Playback of recording ended without error");
+
+ await audioReady;
+ info("Audio content ok");
+
+ await videoReady;
+ info("Video content ok");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_mediarecorder_record_audiocontext.html b/dom/media/test/test_mediarecorder_record_audiocontext.html
new file mode 100644
index 0000000000..686faaeb6b
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_record_audiocontext.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test MediaRecorder Record AudioContext</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+
+<script class="testbody" type="text/javascript">
+
+function startTest() {
+ var context = new AudioContext();
+ var buffer = context.createBuffer(1, 80920, context.sampleRate);
+ for (var i = 0; i < 80920; ++i) {
+ buffer.getChannelData(0)[i] = Math.sin(1000 * 2 * Math.PI * i / context.sampleRate);
+ }
+
+ var source = context.createBufferSource();
+ source.buffer = buffer;
+ source.loop = true;
+
+ var dest = context.createMediaStreamDestination();
+ source.connect(dest);
+ var elem = document.createElement('audio');
+ elem.srcObject = dest.stream;
+ source.start(0);
+ elem.play();
+ let mMediaRecorder = new MediaRecorder(dest.stream);
+ mMediaRecorder.onwarning = function() {
+ ok(false, 'onwarning unexpectedly fired');
+ };
+
+ mMediaRecorder.onerror = function() {
+ ok(false, 'onerror unexpectedly fired');
+ };
+
+ mMediaRecorder.onstop = function() {
+ ok(true, 'onstop fired successfully');
+ is(mMediaRecorder.state, 'inactive', 'check recording status is inactive');
+ SimpleTest.finish();
+ };
+ mMediaRecorder.ondataavailable = function (e) {
+ if (mMediaRecorder.state == 'recording') {
+ is(mMediaRecorder.mimeType, 'audio/ogg; codecs=opus', 'Expected MediaRecorder mimetype');
+ is(e.data.type, 'audio/ogg; codecs=opus', 'Expected Blob mimetype');
+ ok(e.data.size > 0, 'check blob has data');
+ mMediaRecorder.stop();
+ }
+ };
+ try {
+ mMediaRecorder.start(1000);
+ is('recording', mMediaRecorder.state, "check record state recording");
+ } catch (e) {
+ ok(false, 'Can t record audio context');
+ }
+}
+
+startTest();
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_mediarecorder_record_audiocontext_mlk.html b/dom/media/test/test_mediarecorder_record_audiocontext_mlk.html
new file mode 100644
index 0000000000..4128702aef
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_record_audiocontext_mlk.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>capture for possible memory leak when record AudioContext</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=973765">Mozill
+a Bug 973765</a>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+ // This test case want to capture the memory leak if exit the browser after running those script.
+ var ac = new window.AudioContext();
+ var destStream = ac.createMediaStreamDestination().stream;
+ var recorder = new MediaRecorder(destStream);
+ recorder.start(1000);
+ is(recorder.state, 'recording', 'Media recorder should be recording');
+ is(recorder.stream, destStream,
+ 'Media recorder stream = element stream at the start of recording');
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_mediarecorder_record_audionode.html b/dom/media/test/test_mediarecorder_record_audionode.html
new file mode 100644
index 0000000000..8a57437b81
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_record_audionode.html
@@ -0,0 +1,135 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test MediaRecorder Record AudioContext Node</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=968109">Mozilla Bug 968109</a>
+
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+function setUpSource(contextType, nodeType) {
+ // Use contextType to choose offline or real-time context.
+ const context = contextType == "offline"?
+ new OfflineAudioContext(2 , 80920, 44100) : new AudioContext();
+ const buffer = context.createBuffer(2, 80920, context.sampleRate);
+ for (let i = 0; i < 80920; ++i) {
+ buffer.getChannelData(0)[i] = Math.sin(1000 * 2 * Math.PI * i / context.sampleRate);
+ buffer.getChannelData(1)[i] = Math.sin(1000 * 2 * Math.PI * i / context.sampleRate);
+ }
+
+ const source = context.createBufferSource();
+ source.buffer = buffer;
+ source.loop = true;
+
+ source.start(0);
+
+ // nodeType decides which node in graph should be the source of recording.
+ let node;
+ if (nodeType == "source") {
+ node = source;
+ } else if (nodeType == "splitter") {
+ const splitter = context.createChannelSplitter();
+ source.connect(splitter);
+ node = splitter;
+ } else if (nodeType == "destination") {
+ source.connect(context.destination);
+ node = context.destination;
+ }
+ // Explicitly start offline context.
+ if (contextType == "offline") {
+ context.startRendering();
+ }
+
+ return node;
+}
+
+async function testRecord(source, mimeType) {
+ const isOffline = source.context instanceof OfflineAudioContext;
+ const recorder = new MediaRecorder(source, 0, {mimeType});
+ is(recorder.mimeType, mimeType, "Mime type is set");
+ const extendedMimeType = `${mimeType || "audio/ogg"}; codecs=opus`;
+
+ recorder.onwarning = () => ok(false, "should not fire onwarning");
+ recorder.onerror = () => ok(false, "should not fire onerror");
+ if (!isOffline) {
+ recorder.onstop = () => ok(false, "should not fire stop yet");
+ }
+
+ recorder.start(1000);
+ is("recording", recorder.state, "state should become recording after calling start()");
+ is(recorder.mimeType, mimeType, "Mime type is not changed by start()");
+
+ await new Promise(r => recorder.onstart = r);
+ is(recorder.mimeType, extendedMimeType, "Mime type is fully defined");
+
+ const chunks = [];
+ let {data} = await new Promise(r => recorder.ondataavailable = r);
+ if (!isOffline) {
+ is(recorder.state, "recording", "Expected to still be recording");
+ }
+ is(data.type, extendedMimeType, "Blob has fully defined mimetype");
+ isnot(data.size, 0, "should get data and its length should be > 0");
+ chunks.push(data);
+
+ if (isOffline) {
+ await new Promise(r => recorder.onstop = r);
+ is(recorder.state, "inactive", "Offline context should end by itself");
+ } else {
+ is(recorder.state, "recording", "Expected to still be recording");
+ recorder.stop();
+ ({data} = await new Promise(r => recorder.ondataavailable = r));
+ is(recorder.state, "inactive", "Expected to be inactive after last blob");
+ isnot(data.size, 0, "should get data and its length should be > 0");
+ chunks.push(data);
+
+ await new Promise(r => recorder.onstop = r);
+ is(recorder.state, "inactive", "state should remain inactive after stop event");
+ }
+ return new Blob(chunks, {type: chunks[0].type});
+}
+
+addLoadEvent(async () => {
+ const src = setUpSource();
+ let didThrow = false;
+ try {
+ new MediaRecorder(src);
+ } catch (e) {
+ didThrow = true;
+ }
+ ok(didThrow, "MediaRecorder(AudioNode) should be hidden behind a pref");
+
+ await SpecialPowers.pushPrefEnv({set: [
+ ["media.recorder.audio_node.enabled", true],
+ ]});
+
+ // Test with various context and source node types.
+ for (const mimeType of [
+ "audio/ogg",
+ "audio/webm",
+ "video/webm",
+ "",
+ ]) {
+ for (const {context, node} of [
+ {context: "", node: "source"},
+ {context: "", node: "splitter"},
+ {context: "offline", node: "destination"},
+ ]) {
+ info(`Testing recording ${context || "regular"} context and ${node} ` +
+ `node with mimeType '${mimeType}'`);
+ await testRecord(setUpSource(context, node), mimeType);
+ }
+ }
+
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_mediarecorder_record_canvas_captureStream.html b/dom/media/test/test_mediarecorder_record_canvas_captureStream.html
new file mode 100644
index 0000000000..0b6cd6dbb5
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_record_canvas_captureStream.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test MediaRecorder Recording canvas stream</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<div id="content">
+</div>
+<script class="testbody" type="text/javascript">
+
+function startTest() {
+ var canvas = document.createElement("canvas");
+ canvas.width = canvas.height = 100;
+ document.getElementById("content").appendChild(canvas);
+
+ var helper = new CaptureStreamTestHelper2D(100, 100);
+ helper.drawColor(canvas, helper.red);
+
+ var stream = canvas.captureStream(0);
+
+ var blob;
+
+ let mediaRecorder = new MediaRecorder(stream);
+ is(mediaRecorder.stream, stream,
+ "Media recorder stream = canvas stream at the start of recording");
+
+ mediaRecorder.onwarning = () => ok(false, "warning unexpectedly fired");
+
+ mediaRecorder.onerror = () => ok(false, "Recording failed");
+
+ mediaRecorder.ondataavailable = ev => {
+ is(blob, undefined, "Should only get one dataavailable event");
+ blob = ev.data;
+ };
+
+ mediaRecorder.onstart = () => {
+ info("Got 'start' event");
+ // We just want one frame encoded, to see that the recorder produces something readable.
+ mediaRecorder.stop();
+ };
+
+ mediaRecorder.onstop = () => {
+ info("Got 'stop' event");
+ ok(blob, "Should have gotten a data blob");
+
+ var video = document.createElement("video");
+ video.id = "recorded-video";
+ video.src = URL.createObjectURL(blob);
+ video.play();
+ video.onerror = err => {
+ ok(false, "Should be able to play the recording. Got error. code=" + video.error.code);
+ SimpleTest.finish();
+ };
+ document.getElementById("content").appendChild(video);
+ helper.pixelMustBecome(video, helper.red, {
+ threshold: 128,
+ infoString: "Should become red"
+ }).then(SimpleTest.finish);
+ };
+
+ mediaRecorder.start();
+ is(mediaRecorder.state, "recording", "Media recorder should be recording");
+}
+
+SimpleTest.waitForExplicitFinish();
+startTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_mediarecorder_record_changing_video_resolution.html b/dom/media/test/test_mediarecorder_record_changing_video_resolution.html
new file mode 100644
index 0000000000..d6354ee5a1
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_record_changing_video_resolution.html
@@ -0,0 +1,174 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test MediaRecorder Recording canvas stream that dynamically changes resolution</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<div id="content">
+</div>
+<script class="testbody" type="text/javascript">
+
+function startTest() {
+ let canvas = document.createElement("canvas");
+ const resolution_change = [
+ {width: 100, height: 100, color: "red"},
+ {width: 150, height: 150, color: "blue"},
+ {width: 100, height: 100, color: "red"},
+ ];
+ canvas.width = resolution_change[0].width;
+ canvas.height = resolution_change[0].height;
+
+ let ctx = canvas.getContext("2d");
+ ctx.fillStyle = resolution_change[0].color;
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+
+ // The recorded stream coming from canvas.
+ let stream = canvas.captureStream();
+
+ // Check values for events
+ let numDataAvailabledRaised = 0;
+ let numResizeRaised = 0;
+ // Recorded data that will be playback.
+ let blob;
+
+ // Media recorder for VP8 and canvas stream.
+ let mediaRecorder = new MediaRecorder(stream);
+ is(mediaRecorder.stream, stream,
+ "Media recorder stream = canvas stream at the start of recording");
+
+ // Not expected events.
+ mediaRecorder.onwarning = () => ok(false, "MediaRecorder: onwarning unexpectedly fired");
+ mediaRecorder.onerror = err => {
+ ok(false, "MediaRecorder: onerror unexpectedly fired. Code " + err.name);
+ SimpleTest.finish();
+ };
+
+ // When recorder is stopped get recorded data.
+ mediaRecorder.ondataavailable = ev => {
+ info("Got 'dataavailable' event");
+ ++numDataAvailabledRaised;
+ is(blob, undefined, "Should only get one dataavailable event");
+ // Save recorded data for playback
+ blob = ev.data;
+ };
+
+ mediaRecorder.onstart = () => {
+ info('onstart fired successfully');
+ };
+
+ mediaRecorder.onstop = () => {
+ info("Got 'stop' event");
+ is(numDataAvailabledRaised, 1, "Should have gotten 1 dataavailable event");
+ // Playback stream and verify resolution changes.
+ ok(blob, "Should have gotten a data blob");
+
+ let video = document.createElement("video");
+ video.id = "recorded-video";
+ video.src = URL.createObjectURL(blob);
+ video.preload = "metadata";
+
+ video.onerror = err => {
+ ok(false, "Should be able to play the recording. Got error. code=" + video.error.code);
+ SimpleTest.finish();
+ };
+
+ // Check that the encoded frames have the correct sizes.
+ video.onresize = function() {
+ if (numResizeRaised < resolution_change.length) {
+ is(video.videoWidth, resolution_change[numResizeRaised].width,
+ "onresize width should be as expected");
+ is(video.videoHeight, resolution_change[numResizeRaised].height,
+ "onresize height should be as expected");
+ } else {
+ ok(false, "Got more resize events than expected");
+ }
+ ++numResizeRaised;
+ };
+
+ video.onloadedmetadata = function() {
+ info("loadedmetadata");
+ seekThroughFrames();
+ };
+
+ video.onended = function() {
+ is(numResizeRaised, resolution_change.length, "Expected number of resize events");
+ SimpleTest.finish();
+ // This shouldn't be needed, however video.ended may not be set after
+ // seeking to the final frame. This can result in seekToNextFrame being
+ // called again by seekThroughFrames and onended being invoked again,
+ // resulting in multiple finish() calls.
+ // FIXME: https://bugzilla.mozilla.org/show_bug.cgi?id=1386489
+ video.onended = null;
+ };
+
+ document.getElementById("content").appendChild(video);
+
+ function seekThroughFrames() {
+ info("Seeking to next frame");
+ video.seekToNextFrame()
+ .then(() => {
+ info("Seeking to next frame finished. width=" + video.videoWidth
+ + ", height=" + video.videoHeight);
+
+ if (video.ended) {
+ return;
+ }
+
+ // After seeking finished we queue the next seek task on the event
+ // loop so it gets in the same queue as the "resize" events.
+ setTimeout(seekThroughFrames, 0);
+ })
+ .catch(error => {
+ ok(false, "seekToNextFrame rejected: " + error);
+ });
+ }
+ };
+
+ // Start here by stream recorder.
+ mediaRecorder.start();
+ is(mediaRecorder.state, "recording", "Media recorder should be recording");
+ requestAnimationFrame(draw);
+
+ // Change resolution in every frame
+ // Stop recorder on last frame
+ let countFrames = 0;
+ let previous_time = performance.now();
+ function draw(timestamp) {
+ if (timestamp - previous_time < 100) {
+ requestAnimationFrame(draw);
+ return;
+ }
+ previous_time = timestamp;
+
+ if (countFrames == resolution_change.length) {
+ mediaRecorder.stop();
+ return;
+ }
+
+ canvas.width = resolution_change[countFrames].width;
+ canvas.height = resolution_change[countFrames].height;
+ ctx.fillStyle = resolution_change[countFrames].color;
+ // Resize and draw canvas
+ ctx.fillRect(0, 0, canvas.width, canvas.height);
+ // Register draw to be called on next rendering
+ requestAnimationFrame(draw);
+ countFrames++;
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv(
+ {
+ "set": [
+ ["media.seekToNextFrame.enabled", true ],
+ ["media.video-queue.send-to-compositor-size", 1]
+ ]
+ }, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_mediarecorder_record_downsize_resolution.html b/dom/media/test/test_mediarecorder_record_downsize_resolution.html
new file mode 100644
index 0000000000..f9422a3897
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_record_downsize_resolution.html
@@ -0,0 +1,148 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test MediaRecorder Recording canvas dynamically changes to greater resolution</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<div id="content">
+</div>
+<script class="testbody" type="text/javascript">
+
+function startTest() {
+ var canvas = document.createElement("canvas");
+ var canvas_size = 100;
+ var new_canvas_size = 50;
+ canvas.width = canvas.height = canvas_size;
+
+ var helper = new CaptureStreamTestHelper2D(canvas_size, canvas_size);
+ helper.drawColor(canvas, helper.red);
+
+ // The recorded stream coming from canvas.
+ var stream = canvas.captureStream();
+
+ // Check values for events
+ var numDataAvailabledRaised = 0;
+ var numResizeRaised = 0;
+ // Recorded data that will be playback.
+ var blob;
+
+ // Media recorder for VP8 and canvas stream.
+ var mediaRecorder = new MediaRecorder(stream);
+ is(mediaRecorder.stream, stream,
+ "Media recorder stream = canvas stream at the beginning of recording");
+
+ // Not expected events.
+ mediaRecorder.onwarning = () => ok(false, "MediaRecorder: onwarning unexpectedly fired");
+ mediaRecorder.onerror = err => {
+ ok(false, "MediaRecorder: onerror unexpectedly fired. Code " + err.name);
+ SimpleTest.finish();
+ }
+
+ // When recorder is stopped get recorded data.
+ mediaRecorder.ondataavailable = ev => {
+ info("Got 'dataavailable' event");
+ ++numDataAvailabledRaised;
+ is(blob, undefined, "On dataavailable event blob is undefined");
+ // Save recorded data for playback
+ blob = ev.data;
+ };
+
+ mediaRecorder.onstart = () => {
+ info('onstart fired successfully');
+ };
+
+ mediaRecorder.onstop = () => {
+ info("Got 'stop' event");
+ is(numDataAvailabledRaised, 1, "Expected 1 dataavailable event");
+
+ // Playback stream and verify resolution changes.
+ ok(blob, "Should have gotten a data blob");
+ var video = document.createElement("video");
+ video.id = "recorded-video";
+ video.src = URL.createObjectURL(blob);
+ video.onerror = err => {
+ ok(false, "Should be able to play the recording. Got error. code=" + video.error.code);
+ SimpleTest.finish();
+ };
+
+ // Check here that resize is correct in the playback stream.
+ video.onresize = function() {
+ ++numResizeRaised;
+ if (numResizeRaised == 1) {
+ is(this.videoWidth, canvas_size, "1st resize event original width");
+ is(this.videoHeight, canvas_size, "1st resize event original height ");
+ } else if (numResizeRaised == 2) {
+ is(this.videoWidth, new_canvas_size, "2nd resize event new width");
+ is(this.videoHeight, new_canvas_size, "2nd resize event new height");
+ } else {
+ ok(false, "Only 2 numResizeRaised events are expected");
+ }
+ };
+
+ video.onended = () => {
+ is(numResizeRaised, 2, "Expected 2 resize event");
+ };
+ document.getElementById("content").appendChild(video);
+ video.play();
+
+ // Check last color
+ helper.pixelMustBecome(video, helper.red, {
+ threshold: 128,
+ infoString: "Should become red",
+ }).then(() => {
+ video.onresize = {};
+ video.onended = {};
+ SimpleTest.finish();
+ });
+ };
+
+ // Start here by stream recorder.
+ mediaRecorder.start();
+ is(mediaRecorder.state, "recording", "Media recorder started");
+ requestAnimationFrame(draw);
+
+ // Change resolution every 100ms
+ var countFrames=0;
+ var previous_time = performance.now();
+ function draw(timestamp) {
+ if (timestamp - previous_time < 100) {
+ requestAnimationFrame(draw);
+ return;
+ }
+ previous_time = timestamp;
+
+ var size = 0;
+ var color = "";
+ if (countFrames < 1) {
+ // Initial size
+ size = canvas_size;
+ color = helper.blue;
+ } else if (countFrames < 2) {
+ // upsize
+ size = new_canvas_size;
+ color = helper.red;
+ } else {
+ // Stop recoredr on last frame
+ mediaRecorder.stop();
+ return;
+ }
+ // Resize and draw canvas
+ canvas.width = canvas.height = size;
+ helper.drawColor(canvas, color);
+ // Register next draw on every call
+ requestAnimationFrame(draw);
+ countFrames++;
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+startTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_mediarecorder_record_getdata_afterstart.html b/dom/media/test/test_mediarecorder_record_getdata_afterstart.html
new file mode 100644
index 0000000000..3b181ed8db
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_record_getdata_afterstart.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 951008 Test MediaRecorder Record has start event</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+function startTest(test, token) {
+
+ var element = document.createElement('audio');
+ var hasonstart = false;
+ var hasondataavailable = false;
+ var mMediaRecorder;
+
+ element.token = token;
+ manager.started(token);
+ element.src = test.name;
+ element.test = test;
+ element.stream = element.mozCaptureStream();
+
+ mMediaRecorder = new MediaRecorder(element.stream);
+ is(mMediaRecorder.mimeType, '', 'Expected MediaRecorder mimetype');
+ mMediaRecorder.onwarning = function() {
+ ok(false, 'onwarning unexpectedly fired');
+ };
+
+ mMediaRecorder.onerror = function() {
+ ok(false, 'onerror unexpectedly fired');
+ };
+
+ mMediaRecorder.onstart = function() {
+ info('onstart fired successfully');
+ hasonstart = true;
+ is(mMediaRecorder.mimeType, 'audio/ogg; codecs=opus',
+ "MediaRecorder mimetype as expected");
+ mMediaRecorder.requestData();
+ };
+
+ mMediaRecorder.onstop = function() {
+ info('onstop fired successfully');
+ ok(hasondataavailable, "should have ondataavailable before onstop");
+ is(mMediaRecorder.state, 'inactive', 'check recording status is inactive');
+ SimpleTest.finish();
+ };
+
+ mMediaRecorder.ondataavailable = function (e) {
+ info('ondataavailable fired successfully');
+ if (mMediaRecorder.state == 'recording') {
+ hasondataavailable = true;
+ ok(hasonstart, "should have had start event first");
+ is(e.data.type, mMediaRecorder.mimeType,
+ "blob's mimeType matches the recorder's");
+ mMediaRecorder.stop();
+ }
+ };
+
+ // Start recording once metadata are parsed.
+ element.onloadedmetadata = function() {
+ element.oncanplaythrough = null;
+ mMediaRecorder.start(0);
+ is(mMediaRecorder.state, 'recording', 'Media recorder should be recording');
+ is(mMediaRecorder.stream, element.stream,
+ 'Media recorder stream = element stream at the start of recording');
+ };
+
+ element.play();
+}
+
+manager.runTests(gMediaRecorderTests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_mediarecorder_record_gum_video_timeslice.html b/dom/media/test/test_mediarecorder_record_gum_video_timeslice.html
new file mode 100644
index 0000000000..961a9644b2
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_record_gum_video_timeslice.html
@@ -0,0 +1,94 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test MediaRecorder Record gUM video with Timeslice</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="gUM_support.js"></script>
+</head>
+<body>
+<pre id="test">
+<div id="content" style="display: none">
+</div>
+<script class="testbody" type="text/javascript">
+
+async function startTest() {
+ try {
+ await setupGetUserMediaTestPrefs();
+ let stream = await navigator.mediaDevices.getUserMedia({audio: true, video: true});
+ let dataAvailableCount = 0;
+ let onDataAvailableFirst = false;
+ const expectedMimeType = 'video/webm; codecs="vp8, opus"';
+
+ let mediaRecorder = new MediaRecorder(stream);
+ is(mediaRecorder.stream, stream,
+ 'Media recorder stream = element stream at the start of recording');
+ mediaRecorder.onwarning = function() {
+ ok(false, 'onwarning unexpectedly fired');
+ };
+
+ mediaRecorder.onerror = function() {
+ ok(false, 'onerror unexpectedly fired');
+ };
+
+ mediaRecorder.onstop = function() {
+ ok(false, 'Unexpected onstop callback fired');
+ };
+
+ mediaRecorder.onstart = function() {
+ is(mediaRecorder.mimeType, expectedMimeType, 'Expected mime type');
+ };
+
+ mediaRecorder.ondataavailable = function (evt) {
+ info('ondataavailable fired');
+ dataAvailableCount++;
+
+ ok(evt instanceof BlobEvent,
+ 'Events fired from ondataavailable should be BlobEvent');
+ is(evt.type, 'dataavailable',
+ 'Event type should dataavailable');
+ ok(evt.data.size >= 0,
+ 'Blob data size ' + evt.data.size + ' received is greater than or equal to zero');
+ is(evt.data.type, expectedMimeType, 'Expected blob mime type');
+
+ // We'll stop recording upon the 1st blob being received
+ if (dataAvailableCount === 1) {
+ mediaRecorder.onstop = function (event) {
+ info('onstop fired');
+
+ if (!onDataAvailableFirst) {
+ ok(false, 'onstop unexpectedly fired before ondataavailable');
+ }
+
+ ok(true, 'onstop fired successfully');
+ is(mediaRecorder.state, 'inactive',
+ 'check recording status is inactive');
+ SimpleTest.finish();
+ };
+
+ mediaRecorder.stop();
+ is(mediaRecorder.state, 'inactive',
+ 'Media recorder is inactive after being stopped');
+
+ } else if (dataAvailableCount === 2) {
+ // Ensure we've received at least two ondataavailable events before
+ // onstop
+ onDataAvailableFirst = true;
+ }
+ };
+
+ mediaRecorder.start(250);
+ is(mediaRecorder.state, 'recording', 'Media recorder should be recording');
+ is(mediaRecorder.mimeType, '', 'Expected mime type');
+ } catch (err) {
+ ok(false, 'Unexpected error fired with: ' + err);
+ SimpleTest.finish();
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+startTest();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_mediarecorder_record_gum_video_timeslice_mixed.html b/dom/media/test/test_mediarecorder_record_gum_video_timeslice_mixed.html
new file mode 100644
index 0000000000..5e9e4d3023
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_record_gum_video_timeslice_mixed.html
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test MediaRecorder Record gUM video with Timeslice, and playback of mixed memory and file blobs</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="gUM_support.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="text/javascript">
+function unexpected({type}) {
+ ok(false, `${type} unexpectedly fired`);
+}
+
+(async () => {
+ SimpleTest.waitForExplicitFinish();
+ let blobUrl = null;
+ let stream = null;
+ try {
+ // This is the memory limit per blob. If a blob is larger than this,
+ // MediaRecorder will put it in a file. For this test we need to get at
+ // least one blob under, and one blob over the limit.
+ const memoryLimit = 50000;
+ await SpecialPowers.pushPrefEnv({set: [
+ ["media.recorder.max_memory", memoryLimit],
+ ]});
+ // We always use fake devices since the loopback ones don't make enough
+ // pixels change per frame to make the encoded frames large enough.
+ await pushGetUserMediaTestPrefs({fakeAudio: true, fakeVideo: true});
+ stream = await navigator.mediaDevices.getUserMedia(
+ {audio: true, video: true});
+ const blobs = [];
+
+ let mediaRecorder = new MediaRecorder(stream);
+ is(mediaRecorder.stream, stream,
+ "Media recorder stream = element stream at the start of recording");
+ mediaRecorder.start();
+ mediaRecorder.addEventListener("warning", unexpected);
+ mediaRecorder.addEventListener("error", unexpected);
+ mediaRecorder.addEventListener("stop", unexpected);
+ await new Promise(r => mediaRecorder.onstart = r);
+
+ for (let hasMemory = false; !hasMemory;) {
+ mediaRecorder.requestData();
+ const {data} = await new Promise(r => mediaRecorder.ondataavailable = r);
+ blobs.push(data);
+ ok(data.size < memoryLimit, "Blob should be small enough at start");
+ hasMemory = data.size > 0 && data.size < memoryLimit;
+ info(`Blob is ${data.size} bytes.${hasMemory ? " In memory." : ""}`);
+ }
+ info("Got a memory blob");
+
+ SimpleTest.requestFlakyTimeout("Wait for file blob");
+ for (let hasFile = false, waitTimeMs = 500; !hasFile; waitTimeMs *= 4) {
+ info(`Waiting ${waitTimeMs/1000} seconds for file blob`);
+ await new Promise(r => setTimeout(r, waitTimeMs));
+ mediaRecorder.requestData();
+ const {data} = await new Promise(r => mediaRecorder.ondataavailable = r);
+ blobs.push(data);
+ hasFile = data.size > memoryLimit;
+ info(`Blob is ${data.size} bytes. In ${hasFile ? "file" : "memory"}.`);
+ }
+ info("Got a file blob");
+
+ mediaRecorder.stop();
+ const {data} = await new Promise(r => mediaRecorder.ondataavailable = r);
+ blobs.push(data);
+ mediaRecorder.removeEventListener("stop", unexpected);
+ await new Promise(r => mediaRecorder.onstop = r);
+
+ const video = document.createElement("video");
+ const blob = new Blob(blobs);
+ blobUrl = URL.createObjectURL(blob);
+ video.src = blobUrl;
+ info(`Starting playback. Blob-size=${blob.size}`);
+ video.play();
+
+ await Promise.race([
+ new Promise(res => video.onended = res),
+ new Promise((res, rej) => video.onerror = () => rej(video.error.message)),
+ ]);
+ } catch (e) {
+ ok(false, e);
+ } finally {
+ if (stream) {
+ for (const t of stream.getTracks()) {
+ t.stop();
+ }
+ }
+ if (blobUrl) {
+ URL.revokeObjectURL(blobUrl);
+ }
+ SimpleTest.finish();
+ }
+})();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_mediarecorder_record_immediate_stop.html b/dom/media/test/test_mediarecorder_record_immediate_stop.html
new file mode 100644
index 0000000000..8ed7c321a0
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_record_immediate_stop.html
@@ -0,0 +1,115 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test MediaRecorder Immediate Stop</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var manager = new MediaTestManager;
+
+/**
+ * Stops the media recorder immediately after starting the recorder. This test
+ * verifies whether the media recorder can handle this scenario nicely. The
+ * return blob size should be greater than zero, but its duration would be zero
+ * length when play.
+ */
+function startTest(test, token) {
+ var element = document.createElement('audio');
+ var expectedMimeType = test.type;
+
+ element.token = token;
+ manager.started(token);
+
+ element.src = test.name;
+ element.test = test;
+ element.stream = element.mozCaptureStreamUntilEnded();
+
+ var mediaRecorder =
+ new MediaRecorder(element.stream, {mimeType: expectedMimeType});
+ var onStopFired = false;
+ var onDataAvailableFired = false;
+
+ mediaRecorder.onerror = function () {
+ ok(false, 'Unexpected onerror callback fired');
+ };
+
+ mediaRecorder.onwarning = function () {
+ ok(false, 'Unexpected onwarning callback fired');
+ };
+
+ // This handler verifies that only a single onstop event handler is fired.
+ mediaRecorder.onstop = function () {
+ if (onStopFired) {
+ ok(false, 'onstop unexpectedly fired more than once');
+ } else {
+ onStopFired = true;
+
+ // ondataavailable should always fire before onstop
+ if (onDataAvailableFired) {
+ manager.finished(token);
+ } else {
+ ok(false, 'onstop fired without an ondataavailable event first');
+ }
+ }
+ };
+
+ // This handler verifies that only a single ondataavailable event handler
+ // is fired with the blob generated having greater than zero size
+ // and a correct mime type.
+ mediaRecorder.ondataavailable = function (evt) {
+ if (onDataAvailableFired) {
+ ok(false, 'ondataavailable unexpectedly fired more than once');
+ } else {
+ onDataAvailableFired = true;
+
+ ok(evt instanceof BlobEvent,
+ 'Events fired from ondataavailable should be BlobEvent');
+ is(evt.type, 'dataavailable',
+ 'Event type should dataavailable');
+
+ // The initialization of encoder can be cancelled.
+ // On some platforms, the stop method may run after media stream track
+ // available, so the blob can contain the header data.
+ is(evt.data.type, expectedMimeType,
+ 'Blob data received and should have mime type');
+ is(mediaRecorder.mimeType, expectedMimeType,
+ 'Media Recorder mime type in ondataavailable = ' + expectedMimeType);
+ ok(evt.data.size >= 0, 'Blob size can not be negative');
+
+ // onstop should not have fired before ondataavailable
+ if (onStopFired) {
+ ok(false, 'ondataavailable unexpectedly fired later than onstop');
+ manager.finished(token);
+ }
+ }
+ };
+
+ // This handler completes a start and stop of recording and verifies
+ // respective media recorder state.
+ element.onloadedmetadata = function () {
+ element.onloadedmetadata = null;
+ element.play();
+ mediaRecorder.start();
+ is(mediaRecorder.state, 'recording', 'Media recorder should be recording');
+ is(mediaRecorder.stream, element.stream,
+ 'Media recorder stream = element stream at the start of recording');
+
+ mediaRecorder.stop();
+ is(mediaRecorder.state, 'inactive',
+ 'Media recorder is inactive after being stopped');
+ is(mediaRecorder.stream, element.stream,
+ 'Media recorder stream = element stream post recording');
+ };
+
+ element.preload = "metadata";
+}
+
+manager.runTests(gMediaRecorderTests, startTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_mediarecorder_record_no_timeslice.html b/dom/media/test/test_mediarecorder_record_no_timeslice.html
new file mode 100644
index 0000000000..6ed148a108
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_record_no_timeslice.html
@@ -0,0 +1,106 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test MediaRecorder Record No Timeslice</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var manager = new MediaTestManager;
+
+/**
+ * Starts a test on every media recorder file included to check that a
+ * stream derived from the file can be recorded with no time slice provided.
+ */
+function startTest(test, token) {
+ var element = document.createElement('audio');
+ var expectedMimeType = test.type;
+
+ element.token = token;
+ manager.started(token);
+
+ element.src = test.name;
+ element.test = test;
+ element.stream = element.mozCaptureStreamUntilEnded();
+
+ var mediaRecorder =
+ new MediaRecorder(element.stream, {mimeType: expectedMimeType});
+ var onStopFired = false;
+ var onDataAvailableFired = false;
+
+ mediaRecorder.onerror = function () {
+ ok(false, 'Unexpected onerror callback fired');
+ };
+
+ mediaRecorder.onwarning = function () {
+ ok(false, 'Unexpected onwarning callback fired');
+ };
+
+ // This handler verifies that only a single onstop event handler is fired.
+ mediaRecorder.onstop = function () {
+ if (onStopFired) {
+ ok(false, 'onstop unexpectedly fired more than once');
+ } else {
+ onStopFired = true;
+
+ // ondataavailable should always fire before onstop
+ if (onDataAvailableFired) {
+ ok(true, 'onstop fired after ondataavailable');
+ manager.finished(token);
+ } else {
+ ok(false, 'onstop fired without an ondataavailable event first');
+ }
+ }
+ };
+
+ // This handler verifies that only a single ondataavailable event handler
+ // is fired with the blob generated having greater than zero size
+ // and a correct mime type.
+ mediaRecorder.ondataavailable = function (evt) {
+ if (onDataAvailableFired) {
+ ok(false, 'ondataavailable unexpectedly fired more than once');
+ } else {
+ onDataAvailableFired = true;
+
+ ok(evt instanceof BlobEvent,
+ 'Events fired from ondataavailable should be BlobEvent');
+ is(evt.type, 'dataavailable',
+ 'Event type should dataavailable');
+ ok(evt.data.size > 0,
+ 'Blob data received should be greater than zero');
+ is(evt.data.type, expectedMimeType,
+ 'Blob data received should have type = ' + expectedMimeType);
+
+ is(mediaRecorder.mimeType, expectedMimeType,
+ 'Mime type in ondataavailable = ' + expectedMimeType);
+
+ // onstop should not have fired before ondataavailable
+ if (onStopFired) {
+ ok(false, 'ondataavailable unexpectedly fired later than onstop');
+ manager.finished(token);
+ }
+ }
+ };
+
+ element.preload = "metadata";
+
+ element.onloadedmetadata = function () {
+ element.onloadedmetadata = null;
+ mediaRecorder.start();
+ is(mediaRecorder.state, 'recording',
+ 'Media recorder should be recording');
+ is(mediaRecorder.stream, element.stream,
+ 'Media recorder stream = element stream at the start of recording');
+
+ element.play();
+ }
+}
+
+manager.runTests(gMediaRecorderTests, startTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_mediarecorder_record_session.html b/dom/media/test/test_mediarecorder_record_session.html
new file mode 100644
index 0000000000..88795d82d0
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_record_session.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=909670
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Media Recoder recording session</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var manager = new MediaTestManager;
+
+function startTest(test, token) {
+ var element = document.createElement('audio');
+
+ element.token = token;
+ manager.started(token);
+
+ element.src = test.name;
+ element.test = test;
+ element.stream = element.mozCaptureStream();
+
+ var mStopCount = 0;
+ // Start and stop recording session three times continuously.
+ var mExpectStopCount = 3;
+ var mediaRecorder = new MediaRecorder(element.stream);
+
+ // Stop callback.
+ // Suppose to receive mExpectStopCount
+ mediaRecorder.onstop = function stopCallback() {
+ mStopCount++;
+
+ info("MediaRecorder.onstop callback: (" + mStopCount + ")");
+
+ if (mExpectStopCount === mStopCount)
+ {
+ manager.finished(token);
+ }
+ }
+
+ // data avaliable.
+ mediaRecorder.ondataavailable = function(evt) {}
+
+ mediaRecorder.onerror = function(err) {
+ ok(false, 'Unexpected error fired with:' + err);
+ }
+
+ mediaRecorder.onwarning = function() {
+ ok(false, 'Unexpected warning fired');
+ }
+
+ element.preload = "metadata";
+
+ element.onloadedmetadata = function () {
+ element.onloadedmetadata = null;
+ element.play();
+ for (var i = 0; i < mExpectStopCount; i++) {
+ mediaRecorder.start(1000);
+ mediaRecorder.stop();
+ }
+ }
+
+}
+
+manager.runTests(gMediaRecorderTests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_mediarecorder_record_startstopstart.html b/dom/media/test/test_mediarecorder_record_startstopstart.html
new file mode 100644
index 0000000000..b4cc62c709
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_record_startstopstart.html
@@ -0,0 +1,75 @@
+ <!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test MediaRecorder crash on sequence start stop start method</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<div id="content" style="display: none">
+</div>
+<script class="testbody" type="text/javascript">
+
+function startTest() {
+ var ac = new window.AudioContext();
+ var dest = ac.createMediaStreamDestination();
+ var recorder = new MediaRecorder(dest.stream);
+ var stopCount = 0;
+ var dataavailable = 0;
+ var expectedMimeType = 'audio/ogg; codecs=opus';
+ recorder.onstop = function (e) {
+ info('onstop fired');
+ is(recorder.stream, dest.stream,
+ 'Media recorder stream = element stream post recording');
+ stopCount++;
+ if (stopCount == 2) {
+ if (dataavailable >= 2) {
+ SimpleTest.finish();
+ } else {
+ ok(false, 'Should have at least two dataavailable events');
+ }
+ }
+ }
+ recorder.ondataavailable = function (evt) {
+ info('ondataavailable fired');
+ ok(evt instanceof BlobEvent,
+ 'Events fired from ondataavailable should be BlobEvent');
+ is(evt.type, 'dataavailable',
+ 'Event type should dataavailable');
+ // If script runs slower, it may generate header data in blob from encoder
+ if (evt.data.size > 0) {
+ info('blob size = ' + evt.data.size);
+ is(evt.data.type, expectedMimeType,
+ 'Blob data received should have type = ' + expectedMimeType);
+ }
+ dataavailable++;
+ }
+ recorder.onerror = function (e) {
+ ok(false, 'it should execute normally without exception');
+ }
+ recorder.onwarning = function() {
+ ok(false, 'onwarning unexpectedly fired');
+ };
+
+ recorder.start(2000);
+ is(recorder.state, 'recording', 'Media recorder should be recording');
+ recorder.stop();
+ is(recorder.state, 'inactive', 'check recording status is inactive');
+ recorder.start(10000); // This bug would crash on this line without this fix.
+ is(recorder.state, 'recording', 'check recording status is recording');
+ // Simulate delay stop, only delay stop no no stop can trigger crash.
+ setTimeout(function() {
+ recorder.stop();
+ is(recorder.state, 'inactive','check recording status is recording');
+ }, 1000);
+}
+
+SimpleTest.requestFlakyTimeout("untriaged");
+startTest();
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/media/test/test_mediarecorder_record_timeslice.html b/dom/media/test/test_mediarecorder_record_timeslice.html
new file mode 100644
index 0000000000..3e547e77b4
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_record_timeslice.html
@@ -0,0 +1,105 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test MediaRecorder Record Timeslice</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var manager = new MediaTestManager;
+
+/**
+ * Starts a test on every media recorder file included to check that a stream
+ * derived from the file can be recorded with a timeslice provided
+ */
+function startTest(test, token) {
+ var element = document.createElement('audio');
+ var expectedMimeType = test.type;
+
+ element.token = token;
+ manager.started(token);
+
+ element.src = test.name;
+ element.test = test;
+ element.preload = "auto";
+
+ // Set up MediaRecorder once loadedmetadata fires and tracks are available.
+ element.onloadedmetadata = function() {
+ element.onloadedmetadata = null;
+
+ const stream = element.mozCaptureStream();
+ const mediaRecorder =
+ new MediaRecorder(stream, {mimeType: expectedMimeType});
+
+ mediaRecorder.onerror = function () {
+ ok(false, 'Unexpected onerror callback fired');
+ };
+
+ mediaRecorder.onwarning = function () {
+ ok(false, 'Unexpected onwarning callback fired');
+ };
+
+ mediaRecorder.onstop = function () {
+ ok(false, 'Unexpected onstop callback fired');
+ };
+
+ var dataAvailableCount = 0;
+ var onDataAvailableFirst = false;
+
+ // This handler fires every 250ms to generate a blob.
+ mediaRecorder.ondataavailable = function (evt) {
+ info('ondataavailable fired');
+ dataAvailableCount++;
+
+ ok(evt instanceof BlobEvent,
+ 'Events fired from ondataavailable should be BlobEvent');
+ is(evt.type, 'dataavailable',
+ 'Event type should dataavailable');
+ ok(evt.data.size >= 0,
+ 'Blob data size received is greater than or equal to zero');
+
+ is(evt.data.type, expectedMimeType,
+ 'Blob data received should have type = ' + expectedMimeType);
+ is(mediaRecorder.mimeType, expectedMimeType,
+ 'Mime type in ondataavailable = ' + mediaRecorder.mimeType);
+
+ // We'll stop recording upon the 1st blob being received
+ if (dataAvailableCount === 1) {
+ mediaRecorder.onstop = function (event) {
+ info('onstop fired');
+
+ if (!onDataAvailableFirst) {
+ ok(false, 'onstop unexpectedly fired before ondataavailable');
+ }
+ element.pause();
+ manager.finished(token);
+ };
+
+ mediaRecorder.stop();
+ is(mediaRecorder.state, 'inactive',
+ 'Media recorder is inactive after being stopped');
+ is(mediaRecorder.stream, stream,
+ 'Media recorder stream = element stream post recording');
+
+ } else if (dataAvailableCount === 2) {
+ // Ensure we've received at least two ondataavailable events before onstop
+ onDataAvailableFirst = true;
+ }
+ };
+
+ mediaRecorder.start(1);
+ element.play();
+ is(mediaRecorder.state, 'recording', 'Media recorder should be recording');
+ is(mediaRecorder.stream, stream,
+ 'Media recorder stream = element stream at the start of recording');
+ };
+}
+
+manager.runTests(gMediaRecorderTests, startTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_mediarecorder_record_upsize_resolution.html b/dom/media/test/test_mediarecorder_record_upsize_resolution.html
new file mode 100644
index 0000000000..d02fd08e44
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_record_upsize_resolution.html
@@ -0,0 +1,148 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test MediaRecorder Recording canvas dynamically changes to greater resolution</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<div id="content">
+</div>
+<script class="testbody" type="text/javascript">
+
+function startTest() {
+ var canvas = document.createElement("canvas");
+ var canvas_size = 100;
+ var new_canvas_size = 150;
+ canvas.width = canvas.height = canvas_size;
+
+ var helper = new CaptureStreamTestHelper2D(canvas_size, canvas_size);
+ helper.drawColor(canvas, helper.red);
+
+ // The recorded stream coming from canvas.
+ var stream = canvas.captureStream();
+
+ // Check values for events
+ var numDataAvailabledRaised = 0;
+ var numResizeRaised = 0;
+ // Recorded data that will be playback.
+ var blob;
+
+ // Media recorder for VP8 and canvas stream.
+ var mediaRecorder = new MediaRecorder(stream);
+ is(mediaRecorder.stream, stream,
+ "Media recorder stream = canvas stream at the beginning of recording");
+
+ // Not expected events.
+ mediaRecorder.onwarning = () => ok(false, "MediaRecorder: onwarning unexpectedly fired");
+ mediaRecorder.onerror = err => {
+ ok(false, "MediaRecorder: onerror unexpectedly fired. Code " + err.name);
+ SimpleTest.finish();
+ }
+
+ // When recorder is stopped get recorded data.
+ mediaRecorder.ondataavailable = ev => {
+ info("Got 'dataavailable' event");
+ ++numDataAvailabledRaised;
+ is(blob, undefined, "On dataavailable event blob is undefined");
+ // Save recorded data for playback
+ blob = ev.data;
+ };
+
+ mediaRecorder.onstart = () => {
+ info('onstart fired successfully');
+ };
+
+ mediaRecorder.onstop = () => {
+ info("Got 'stop' event");
+ is(numDataAvailabledRaised, 1, "Expected 1 dataavailable event");
+
+ // Playback stream and verify resolution changes.
+ ok(blob, "Should have gotten a data blob");
+ var video = document.createElement("video");
+ video.id = "recorded-video";
+ video.src = URL.createObjectURL(blob);
+ video.onerror = err => {
+ ok(false, "Should be able to play the recording. Got error. code=" + video.error.code);
+ SimpleTest.finish();
+ };
+
+ // Check here that resize is correct in the playback stream.
+ video.onresize = function() {
+ ++numResizeRaised;
+ if (numResizeRaised == 1) {
+ is(this.videoWidth, canvas_size, "1st resize event original width");
+ is(this.videoHeight, canvas_size, "1st resize event original height ");
+ } else if (numResizeRaised == 2) {
+ is(this.videoWidth, new_canvas_size, "2nd resize event new width");
+ is(this.videoHeight, new_canvas_size, "2nd resize event new height");
+ } else {
+ ok(false, "Only 2 numResizeRaised events are expected");
+ }
+ };
+
+ video.onended = () => {
+ is(numResizeRaised, 2, "Expected 2 resize event");
+ };
+ document.getElementById("content").appendChild(video);
+ video.play();
+
+ // Check last color
+ helper.pixelMustBecome(video, helper.red, {
+ threshold: 128,
+ infoString: "Should become red",
+ }).then(() => {
+ video.onresize = {};
+ video.onended = {};
+ SimpleTest.finish();
+ });
+ };
+
+ // Start here by stream recorder.
+ mediaRecorder.start();
+ is(mediaRecorder.state, "recording", "Media recorder started");
+ requestAnimationFrame(draw);
+
+ // Change resolution every 100 ms
+ var countFrames=0;
+ var previous_time = performance.now();
+ function draw(timestamp) {
+ if (timestamp - previous_time < 100) {
+ requestAnimationFrame(draw);
+ return;
+ }
+ previous_time = timestamp;
+
+ var size = 0;
+ var color = "";
+ if (countFrames < 1) {
+ // Initial size
+ size = canvas_size;
+ color = helper.blue;
+ } else if (countFrames < 2) {
+ // upsize
+ size = new_canvas_size;
+ color = helper.red;
+ }else {
+ // Stop recoredr on last frame
+ mediaRecorder.stop();
+ return;
+ }
+ // Resize and draw canvas
+ canvas.width = canvas.height = size;
+ helper.drawColor(canvas, color);
+ // Register next draw on every call
+ requestAnimationFrame(draw);
+ countFrames++;
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+startTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_mediarecorder_reload_crash.html b/dom/media/test/test_mediarecorder_reload_crash.html
new file mode 100644
index 0000000000..f6d008261f
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_reload_crash.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test that reloading media recorder object</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=894348">Mozill
+a Bug 894348</a>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+ for (let i = 0; i< 5; i++) {
+ /* eslint-disable no-undef */
+ try { o0 = document.createElement('audio') } catch(e) { }
+ try { o0.src = "sound.ogg" } catch(e) { }
+ try { (document.body || document.documentElement).appendChild(o0) } catch(e) { }
+ try { o1 = o0.mozCaptureStreamUntilEnded(); } catch(e) { }
+ try { o2 = new MediaRecorder(o1) } catch(e) { }
+ try { o2.start(0) } catch(e) { }
+ /* eslint-enable no-undef */
+ SpecialPowers.gc();
+ }
+ ok(true, "pass the crash test");
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_mediarecorder_state_event_order.html b/dom/media/test/test_mediarecorder_state_event_order.html
new file mode 100644
index 0000000000..feba055f4d
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_state_event_order.html
@@ -0,0 +1,83 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test MediaRecorder fires an event after changing state</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var manager = new MediaTestManager;
+
+/**
+ * The flow of test is start()=>pause()=>resume()=>stop(). In each steps,
+ * this test verifies whether each MediaRecorder methods properly change
+ * its state before firing an event. Checking the state is done in the
+ * corresponding event handlers.
+ */
+function startTest(test, token) {
+ var element = document.createElement('audio');
+
+ element.token = token;
+ manager.started(token);
+
+ element.src = test.name;
+ element.test = test;
+ element.stream = element.mozCaptureStream();
+
+ var mediaRecorder = new MediaRecorder(element.stream);
+
+ mediaRecorder.onwarning = function() {
+ ok(false, 'onwarning unexpectedly fired');
+ };
+
+ mediaRecorder.onerror = function() {
+ ok(false, 'onerror unexpectedly fired');
+ };
+
+ mediaRecorder.onstart = function() {
+ info('onstart fired successfully');
+ is(mediaRecorder.state, 'recording',
+ 'Media Recorder changes state to recording before firing a start event');
+ mediaRecorder.pause();
+ };
+
+ mediaRecorder.onpause = function() {
+ info('onpause fired successfully');
+ is(mediaRecorder.state, 'paused',
+ 'Media Recorder changes state to paused before firing a pause event');
+ mediaRecorder.resume();
+ };
+
+ mediaRecorder.onresume = function() {
+ info('onresume fired successfully');
+ is(mediaRecorder.state, 'recording',
+ 'Media Recorder changes state to recording before firing a resume event');
+ mediaRecorder.stop();
+ };
+
+ mediaRecorder.onstop = function() {
+ info('onstop fired successfully');
+ is(mediaRecorder.state, 'inactive',
+ 'Media Recorder changes state to inactive before firing a stop event');
+ SimpleTest.finish();
+ };
+
+ // Call start() once metadata are parsed.
+ element.onloadedmetadata = function() {
+ element.play();
+ mediaRecorder.start();
+ is(mediaRecorder.state, 'recording', 'Media recorder should be recording');
+ is(mediaRecorder.stream, element.stream,
+ 'Media recorder stream = element stream at the start of recording');
+ };
+}
+
+manager.runTests(gMediaRecorderTests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_mediarecorder_state_transition.html b/dom/media/test/test_mediarecorder_state_transition.html
new file mode 100644
index 0000000000..5d6483b7de
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_state_transition.html
@@ -0,0 +1,280 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test MediaRecorder State Transition</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var manager = new MediaTestManager;
+
+// List of operation tests for media recorder objects to verify if running
+// these operations should result in an exception or not
+var operationTests = [
+ {
+ operations: ['stop'],
+ isValid: true
+ },
+ {
+ operations: ['requestData'],
+ isValid: false
+ },
+ {
+ operations: ['pause'],
+ isValid: false
+ },
+ {
+ operations: ['resume'],
+ isValid: false
+ },
+ {
+ operations: ['start'],
+ isValid: true
+ },
+ {
+ operations: ['start'],
+ isValid: true,
+ timeSlice: 200
+ },
+ {
+ operations: ['start', 'pause'],
+ isValid: true
+ },
+ {
+ operations: ['start', 'pause'],
+ isValid: true,
+ timeSlice: 200
+ },
+ {
+ operations: ['start', 'start'],
+ isValid: false
+ },
+ {
+ operations: ['start', 'resume'],
+ isValid: true
+ },
+ {
+ operations: ['pause', 'start'],
+ isValid: false
+ },
+ {
+ operations: ['resume', 'start'],
+ isValid: false
+ },
+ {
+ operations: ['requestData', 'start'],
+ isValid: false
+ },
+ {
+ operations: ['stop', 'start'],
+ isValid: true
+ },
+ {
+ operations: ['start', 'stop'],
+ isValid: true
+ },
+ {
+ operations: ['start', 'stop'],
+ isValid: true,
+ timeSlice: 200
+ },
+ {
+ operations: ['start', 'requestData'],
+ isValid: true
+ },
+ {
+ operations: ['start', 'requestData'],
+ isValid: true,
+ timeSlice: 200
+ },
+ {
+ operations: ['start', 'pause', 'stop'],
+ isValid: true
+ },
+ {
+ operations: ['start', 'pause', 'start'],
+ isValid: false
+ },
+ {
+ operations: ['start', 'pause', 'pause'],
+ isValid: true
+ },
+ {
+ operations: ['start', 'pause', 'requestData'],
+ isValid: true
+ },
+ {
+ operations: ['start', 'pause', 'resume'],
+ isValid: true
+ },
+ {
+ operations: ['start', 'pause', 'resume'],
+ isValid: true,
+ timeSlice: 200
+ },
+ {
+ operations: ['start', 'resume', 'resume'],
+ isValid: true
+ },
+ {
+ operations: ['start', 'resume', 'resume'],
+ isValid: true,
+ timeSlice: 200
+ },
+ {
+ operations: ['start', 'resume', 'stop'],
+ isValid: true
+ },
+ {
+ operations: ['start', 'resume', 'stop'],
+ isValid: true,
+ timeSlice: 200
+ },
+ {
+ operations: ['start', 'stop', 'start'],
+ isValid: true
+ },
+ {
+ operations: ['start', 'stop', 'start'],
+ isValid: true,
+ timeSlice: 200
+ },
+ {
+ operations: ['start', 'stop', 'pause'],
+ isValid: false
+ },
+ {
+ operations: ['start', 'stop', 'resume'],
+ isValid: false
+ },
+ {
+ operations: ['start', 'stop', 'requestData'],
+ isValid: false
+ },
+ {
+ operations: ['start', 'stop', 'stop'],
+ isValid: true
+ },
+ {
+ operations: ['start', 'pause', 'resume', 'resume'],
+ isValid: true
+ },
+ {
+ operations: ['start', 'pause', 'resume', 'resume'],
+ isValid: true,
+ timeSlice: 200
+ },
+ {
+ operations: ['start', 'pause', 'pause', 'resume'],
+ isValid: true
+ },
+ {
+ operations: ['start', 'pause', 'pause', 'resume'],
+ isValid: true,
+ timeSlice: 200
+ },
+ {
+ operations: ['start', 'stop', 'start', 'stop'],
+ isValid: true
+ },
+ {
+ operations: ['start', 'stop', 'start', 'stop'],
+ isValid: true,
+ timeSlice: 200
+ },
+ {
+ operations: ['start', 'stop', 'start', 'pause'],
+ isValid: true
+ },
+ {
+ operations: ['start', 'stop', 'start', 'pause'],
+ isValid: true,
+ timeSlice: 200
+ },
+ {
+ operations: ['start', 'stop', 'start', 'resume'],
+ isValid: true
+ },
+ {
+ operations: ['start', 'stop', 'start', 'resume'],
+ isValid: true,
+ timeSlice: 200
+ },
+ {
+ operations: ['start', 'stop', 'start', 'requestData'],
+ isValid: true
+ },
+ {
+ operations: ['start', 'stop', 'start', 'requestData'],
+ isValid: true,
+ timeSlice: 200
+ },
+];
+
+/**
+ * Runs through each available state transition test by running all
+ * available operations on a media recorder object. Then, we report
+ * back if the test was expected through an exception or not.
+ *
+ * @param {MediaStream} testStream the media stream used for media recorder
+ * operation tests
+ */
+function runStateTransitionTests(testStream) {
+ for (const operationTest of operationTests) {
+ var mediaRecorder = new MediaRecorder(testStream);
+ var operationsString = operationTest.operations.toString();
+
+ try {
+ for (const operation of operationTest.operations) {
+ if (operationTest.timeSlice && operation === 'start') {
+ operationsString += ' with timeslice ' + operationTest.timeSlice;
+ mediaRecorder[operation](operationTest.timeSlice);
+ } else {
+ mediaRecorder[operation]();
+ }
+ }
+
+ ok(operationTest.isValid, `${operationsString} should succeed`);
+ } catch (err) {
+ if (operationTest.isValid) {
+ ok(false, `${operationsString} failed unexpectedly with ${err.name}`);
+ } else {
+ is(err.name, "InvalidStateError",
+ `${operationsString} expected to fail with InvalidStateError`);
+ }
+ }
+ }
+}
+
+/**
+ * Starts a test on every media recorder file included to check that various
+ * state transition flows that can happen in the media recorder object throw
+ * exceptions when they are expected to and vice versa.
+ */
+function startTest(test, token) {
+ var element = document.createElement('audio');
+
+ element.token = token;
+ manager.started(token);
+
+ element.src = test.name;
+ element.test = test;
+ element.stream = element.mozCaptureStream();
+
+ element.oncanplaythrough = function () {
+ element.oncanplaythrough = null;
+ runStateTransitionTests(element.stream);
+ manager.finished(token);
+ };
+
+ element.play();
+}
+
+manager.runTests(gMediaRecorderTests, startTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_mediarecorder_webm_support.html b/dom/media/test/test_mediarecorder_webm_support.html
new file mode 100644
index 0000000000..6b115ee33a
--- /dev/null
+++ b/dom/media/test/test_mediarecorder_webm_support.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media Recording - test WebM MIME support</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+ok(MediaRecorder.isTypeSupported('audio/webm'),
+ 'Should support audio/webm');
+ok(MediaRecorder.isTypeSupported('AUDIO/WEBM'),
+ 'Should support audio/webm, upper case');
+ok(MediaRecorder.isTypeSupported('AuDiO/wEbM'),
+ 'Should support audio/webm, mixed case');
+
+ok(MediaRecorder.isTypeSupported('audio/webm;codecs=opus'),
+ 'Should support audio/webm;codecs=opus');
+ok(MediaRecorder.isTypeSupported('AUDIO/WEBM;CODECS=opus'),
+ 'Should support audio/webm;codecs=opus, upper case');
+ok(MediaRecorder.isTypeSupported('AuDiO/wEbM;cOdEcS=opus'),
+ 'Should support audio/webm;codecs=opus, mixed case');
+
+ok(MediaRecorder.isTypeSupported('video/webm'),
+ 'Should support video/webm');
+ok(MediaRecorder.isTypeSupported('VIDEO/WEBM'),
+ 'Should support video/webm, upper case');
+ok(MediaRecorder.isTypeSupported('vIdEo/WeBm'),
+ 'Should support video/webm, mixed case');
+
+ok(MediaRecorder.isTypeSupported('video/webm; codecs="vp8"'),
+ 'Should support video/webm; codecs="vp8"');
+ok(MediaRecorder.isTypeSupported('VIDEO/WEBM; CODECS="vp8"'),
+ 'Should support video/webm; codecs="vp8", upper case');
+ok(MediaRecorder.isTypeSupported('vIdEo/WeBm; CoDeCs="vp8"'),
+ 'Should support video/webm; codecs="vp8", mixed case');
+
+ok(MediaRecorder.isTypeSupported('video/webm; codecs="vp8.0"'),
+ 'Should support video/webm; codecs="vp8.0"');
+ok(MediaRecorder.isTypeSupported('VIDEO/WEBM; CODECS="vp8.0"'),
+ 'Should support video/webm; codecs="vp8.0", upper case');
+ok(MediaRecorder.isTypeSupported('vIdEo/WeBm; CoDeCs="vp8.0"'),
+ 'Should support video/webm; codecs="vp8.0", mixed case');
+
+ok(!MediaRecorder.isTypeSupported('video/webm; codecs="vp8, vorbis"'),
+ 'Should not support video/webm + vp8/vorbis');
+ok(!MediaRecorder.isTypeSupported('video/webm; codecs="vp9, vorbis"'),
+ 'Should not support video/webm + vp9/vorbis');
+ok(MediaRecorder.isTypeSupported('video/webm; codecs="vp8, opus"'),
+ 'Should support video/webm + vp8/opus');
+ok(!MediaRecorder.isTypeSupported('video/webm; codecs="vp9, opus"'),
+ 'Should not support video/webm + vp9/opus');
+</script>
+</head>
+</html>
diff --git a/dom/media/test/test_mediastream_as_eventarget.html b/dom/media/test/test_mediastream_as_eventarget.html
new file mode 100644
index 0000000000..f09c277583
--- /dev/null
+++ b/dom/media/test/test_mediastream_as_eventarget.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test MediaStream as EventTarget</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ var ms = new MediaStream();
+ var listenerForFooWasCalled = false;
+ ms.addEventListener("foo", () => { listenerForFooWasCalled = true; });
+ var listenerForAddTrackWasCalled = false;
+ ms.addEventListener("addtrack", () => { listenerForAddTrackWasCalled = true; });
+ var handlerForAddTrackWasCalled = false;
+ ms.onaddtrack = () => { handlerForAddTrackWasCalled = true; };
+
+ ms.dispatchEvent(new Event("foo"));
+ ms.dispatchEvent(new Event("addtrack"));
+
+ ok(listenerForFooWasCalled,
+ "Should have called the event listener for 'foo'");
+ ok(listenerForAddTrackWasCalled,
+ "Should have called the event listener for 'addtrack'");
+ ok(handlerForAddTrackWasCalled,
+ "Should have called the event handler for 'addtrack'")
+ </script>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/dom/media/test/test_mediatrack_consuming_mediaresource.html b/dom/media/test/test_mediatrack_consuming_mediaresource.html
new file mode 100644
index 0000000000..515df5c053
--- /dev/null
+++ b/dom/media/test/test_mediatrack_consuming_mediaresource.html
@@ -0,0 +1,198 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test track interfaces when consuming media resources</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+const manager = new MediaTestManager;
+
+function startTest(test, token) {
+ const elemType = getMajorMimeType(test.type);
+ const element = document.createElement(elemType);
+
+ let audioOnchange = 0;
+ let audioOnaddtrack = 0;
+ let audioOnremovetrack = 0;
+ let videoOnchange = 0;
+ let videoOnaddtrack = 0;
+ let videoOnremovetrack = 0;
+ let isPlaying = false;
+
+ isnot(element.audioTracks, undefined,
+ 'HTMLMediaElement::AudioTracks() property should be available.');
+ isnot(element.videoTracks, undefined,
+ 'HTMLMediaElement::VideoTracks() property should be available.');
+
+ element.audioTracks.onaddtrack = function(e) {
+ audioOnaddtrack++;
+ }
+
+ element.audioTracks.onremovetrack = function(e) {
+ audioOnremovetrack++;
+ }
+
+ element.audioTracks.onchange = function(e) {
+ audioOnchange++;
+ }
+
+ element.videoTracks.onaddtrack = function(e) {
+ videoOnaddtrack++;
+ }
+
+ element.videoTracks.onremovetrack = function(e) {
+ videoOnremovetrack++;
+ }
+
+ element.videoTracks.onchange = function(e) {
+ videoOnchange++;
+ }
+
+ function checkTrackNotRemoved() {
+ is(audioOnremovetrack, 0, 'Should have no calls of onremovetrack on audioTracks.');
+ is(videoOnremovetrack, 0, 'Should have no calls of onremovetrack on videoTracks.');
+ if (isPlaying) {
+ is(element.audioTracks.length, test.hasAudio ? 1 : 0,
+ 'Expected length of audioTracks.');
+ is(element.videoTracks.length, test.hasVideo ? 1 : 0,
+ 'Expected length of videoTracks.');
+ }
+ }
+
+ function checkTrackRemoved() {
+ is(element.audioTracks.length, 0, 'The length of audioTracks should be 0.');
+ is(element.videoTracks.length, 0, 'The length of videoTracks should be 0.');
+ if (isPlaying) {
+ is(audioOnremovetrack, test.hasAudio ? 1 : 0,
+ 'Expected calls of onremovetrack on audioTracks.');
+ is(videoOnremovetrack, test.hasVideo ? 1 : 0,
+ 'Expected calls of onremovetrack on videoTracks.');
+ }
+ }
+
+ function onended() {
+ ok(true, 'Event ended is expected to be fired on element.');
+ checkTrackNotRemoved();
+ element.onended = null;
+ element.onplaying = null;
+ element.onpause = null;
+ element.src = "";
+ is(element.audioTracks.length, 0, 'audioTracks have been forgotten');
+ is(element.videoTracks.length, 0, 'videoTracks have been forgotten');
+ is(audioOnremovetrack, 0, 'No audio removetrack events yet');
+ is(videoOnremovetrack, 0, 'No video removetrack events yet');
+ setTimeout(() => {
+ checkTrackRemoved();
+ manager.finished(element.token);
+ }, 100);
+ }
+
+ function checkTrackAdded() {
+ isPlaying = true;
+ if (test.hasAudio) {
+ is(audioOnaddtrack, 1, 'Calls of onaddtrack on audioTracks should be 1.');
+ is(element.audioTracks.length, 1, 'The length of audioTracks should be 1.');
+ ok(element.audioTracks[0].enabled, 'Audio track should be enabled as default.');
+ }
+ if (test.hasVideo) {
+ is(videoOnaddtrack, 1, 'Calls of onaddtrack on videoTracks should be 1.');
+ is(element.videoTracks.length, 1, 'The length of videoTracks should be 1.');
+ is(element.videoTracks.selectedIndex, 0,
+ 'The first video track is set selected as default.');
+ }
+ }
+
+ function setTrackEnabled(enabled) {
+ if (test.hasAudio) {
+ element.audioTracks[0].enabled = enabled;
+ }
+ if (test.hasVideo) {
+ element.videoTracks[0].selected = enabled;
+ }
+ }
+
+ function checkTrackChanged(calls, enabled) {
+ if (test.hasAudio) {
+ is(audioOnchange, calls, 'Calls of onchange on audioTracks should be '+calls);
+ is(element.audioTracks[0].enabled, enabled,
+ 'Enabled value of the audio track should be ' +enabled);
+ }
+ if (test.hasVideo) {
+ is(videoOnchange, calls, 'Calls of onchange on videoTracks should be '+calls);
+ is(element.videoTracks[0].selected, enabled,
+ 'Selected value of the video track should be ' +enabled);
+ var index = enabled ? 0 : -1;
+ is(element.videoTracks.selectedIndex, index,
+ 'SelectedIndex of video tracks should be ' +index);
+ }
+ }
+
+ function onpause() {
+ element.onpause = null;
+ if (element.ended) {
+ return;
+ }
+ if (steps == 1) {
+ setTrackEnabled(false);
+ element.onplaying = onplaying;
+ element.play();
+ steps++;
+ } else if (steps == 2) {
+ setTrackEnabled(true);
+ element.onplaying = onplaying;
+ element.play();
+ steps++;
+ }
+ }
+
+ function onplaying() {
+ element.onplaying = null;
+ if (element.ended) {
+ return;
+ }
+ if (steps == 1) {
+ element.onpause = onpause;
+ element.pause();
+ checkTrackAdded();
+ } else if (steps == 2) {
+ element.onpause = onpause;
+ element.pause();
+ checkTrackChanged(1, false);
+ } else if (steps == 3) {
+ checkTrackChanged(2, true);
+ }
+ }
+
+ var steps = 0;
+
+ element.token = token;
+ manager.started(token);
+
+ element.src = test.name;
+ element.test = test;
+ element.onplaying = onplaying;
+ element.onended = onended;
+ element.play();
+ steps++;
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv(
+ {
+ "set": [
+ ["media.track.enabled", true]
+ ]
+ },
+ function() {
+ manager.runTests(gTrackTests, startTest);
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_mediatrack_consuming_mediastream.html b/dom/media/test/test_mediatrack_consuming_mediastream.html
new file mode 100644
index 0000000000..b930ca4fdc
--- /dev/null
+++ b/dom/media/test/test_mediatrack_consuming_mediastream.html
@@ -0,0 +1,146 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test track interfaces when consuming a MediaStream from gUM</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="gUM_support.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+async function startTest() {
+ let steps = 0;
+ let audioOnchange = 0;
+ let audioOnaddtrack = 0;
+ let videoOnchange = 0;
+ let videoOnaddtrack = 0;
+ let isPlaying = false;
+ let element = document.createElement("video");
+ let stream;
+ try {
+ await setupGetUserMediaTestPrefs();
+ stream = await navigator.mediaDevices.getUserMedia({audio: true, video: true});
+ } catch (err) {
+ ok(false, 'Unexpected error fired with: ' + err);
+ SimpleTest.finish();
+ return;
+ }
+
+ element.audioTracks.onaddtrack = function(e) {
+ audioOnaddtrack++;
+ };
+
+ element.audioTracks.onchange = function(e) {
+ audioOnchange++;
+ };
+
+ element.videoTracks.onaddtrack = function(e) {
+ videoOnaddtrack++;
+ };
+
+ element.videoTracks.onchange = function(e) {
+ videoOnchange++;
+ };
+
+ function checkTrackRemoved() {
+ if (isPlaying) {
+ is(element.audioTracks.length, 0, 'The length of audioTracks should be 0.');
+ is(element.videoTracks.length, 0, 'The length of videoTracks should be 0.');
+ }
+ }
+
+ element.onended = function() {
+ ok(true, 'Event ended is expected to be fired on element.');
+ checkTrackRemoved();
+ element.onended = null;
+ element.onplaying = null;
+ element.onpause = null;
+ SimpleTest.finish();
+ }
+
+ function checkTrackAdded() {
+ isPlaying = true;
+ is(audioOnaddtrack, 1, 'Calls of onaddtrack on audioTracks should be 1.');
+ is(element.audioTracks.length, 1, 'The length of audioTracks should be 1.');
+ ok(element.audioTracks[0].enabled, 'Audio track should be enabled as default.');
+ is(videoOnaddtrack, 1, 'Calls of onaddtrack on videoTracks should be 1.');
+ is(element.videoTracks.length, 1, 'The length of videoTracks should be 1.');
+ is(element.videoTracks.selectedIndex, 0,
+ 'The first video track is set selected as default.');
+ }
+
+ function setTrackEnabled(enabled) {
+ element.audioTracks[0].enabled = enabled;
+ element.videoTracks[0].selected = enabled;
+ }
+
+ function checkTrackChanged(calls, enabled) {
+ is(audioOnchange, calls, 'Calls of onchange on audioTracks should be '+calls);
+ is(element.audioTracks[0].enabled, enabled,
+ 'Enabled value of the audio track should be ' +enabled);
+ is(videoOnchange, calls, 'Calls of onchange on videoTracks should be '+calls);
+ is(element.videoTracks[0].selected, enabled,
+ 'Selected value of the video track should be ' +enabled);
+ var index = enabled ? 0 : -1;
+ is(element.videoTracks.selectedIndex, index,
+ 'SelectedIndex of video tracks should be ' +index);
+ }
+
+ function onpause() {
+ element.onpause = null;
+ if (element.ended) {
+ return;
+ }
+ if (steps == 1) {
+ setTrackEnabled(false);
+ element.onplaying = onplaying;
+ element.play();
+ steps++;
+ } else if (steps == 2) {
+ setTrackEnabled(true);
+ element.onplaying = onplaying;
+ element.play();
+ steps++;
+ }
+ }
+
+ function onplaying() {
+ element.onplaying = null;
+ if (element.ended) {
+ return;
+ }
+ if (steps == 1) {
+ element.onpause = onpause;
+ element.pause();
+ checkTrackAdded();
+ } else if (steps == 2) {
+ element.onpause = onpause;
+ element.pause();
+ checkTrackChanged(1, false);
+ } else if (steps == 3) {
+ checkTrackChanged(2, true);
+ stream.getTracks().forEach(t => t.stop());
+ }
+ }
+
+ element.onplaying = onplaying;
+ element.srcObject = stream;
+
+ steps++;
+ await element.play();
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv(
+ {
+ "set": [
+ ["media.track.enabled", true]
+ ]
+ }, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_mediatrack_events.html b/dom/media/test/test_mediatrack_events.html
new file mode 100644
index 0000000000..5eae94f804
--- /dev/null
+++ b/dom/media/test/test_mediatrack_events.html
@@ -0,0 +1,135 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test events of media track interfaces</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="gUM_support.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+async function startTest() {
+ let steps = 0;
+ let element = document.createElement("video");
+ let stream;
+ try {
+ await setupGetUserMediaTestPrefs();
+ stream = await navigator.mediaDevices.getUserMedia({audio: true, video: true});
+ } catch (err) {
+ ok(false, 'Unexpected error fired with: ' + err);
+ SimpleTest.finish();
+ return;
+ }
+
+ function verifyEvent(e, type) {
+ is(e.type, type, "Event type should be " + type);
+ ok(e.isTrusted, "Event should be trusted.");
+ ok(!e.bubbles, "Event shouldn't bubble.");
+ ok(!e.cancelable, "Event shouldn't be cancelable.");
+ }
+
+ element.audioTracks.onaddtrack = function(e) {
+ ok(e instanceof TrackEvent, "Event fired from onaddtrack should be a TrackEvent");
+ ok(true, 'onaddtrack is expected to be called from audioTracks.');
+ verifyEvent(e, "addtrack");
+ };
+
+ element.audioTracks.onremovetrack = function(e) {
+ ok(e instanceof TrackEvent, "Event fired from onremovetrack should be a TrackEvent");
+ ok(true, 'onremovetrack is expected to be called from audioTracks.');
+ verifyEvent(e, "removetrack");
+ };
+
+ element.audioTracks.onchange = function(e) {
+ ok(e instanceof window.Event, "Event fired from onchange should be a simple event.");
+ ok(true, 'onchange is expected to be called from audioTracks.');
+ verifyEvent(e, "change");
+ };
+
+ element.videoTracks.onaddtrack = function(e) {
+ ok(e instanceof TrackEvent, "Event fired from onaddtrack should be a TrackEvent");
+ ok(true, 'onaddtrack is expected to be called from videoTracks.');
+ verifyEvent(e, "addtrack");
+ };
+
+ element.videoTracks.onremovetrack = function(e) {
+ ok(e instanceof TrackEvent, "Event fired from onremovetrack should be a TrackEvent");
+ ok(true, 'onremovetrack is expected to be called from videoTracks.');
+ verifyEvent(e, "removetrack");
+ };
+
+ element.videoTracks.onchange = function(e) {
+ ok(e instanceof window.Event, "Event fired from onchange should be a simple event.");
+ ok(true, 'onchange is expected to be called from videoTracks.');
+ verifyEvent(e, "change");
+ };
+
+ element.onended = function() {
+ ok(true, 'Event ended is expected to be fired on element.');
+ element.onended = null;
+ element.onplaying = null;
+ element.onpause = null;
+ //This helps to prevent these events from firing after SimpleTest.finish()
+ //on B2G ICS Emulator, but not sure they have been run at all, then
+ element.audioTracks.onremovetrack = null;
+ element.audioTracks.onaddtrack = null;
+ element.audioTracks.onchange = null;
+ element.videoTracks.onremovetrack = null;
+ element.videoTracks.onaddtrack = null;
+ element.videoTracks.onchange = null;
+ SimpleTest.finish();
+ }
+
+ function onpause() {
+ element.onpause = null;
+ if (element.ended) {
+ return;
+ }
+ if (steps == 1) {
+ element.audioTracks[0].enabled = false;
+ element.videoTracks[0].selected = false;
+ element.onplaying = onplaying;
+ element.play();
+ steps++;
+ }
+ }
+
+ function onplaying() {
+ element.onplaying = null;
+ if (element.ended) {
+ return;
+ }
+ if (steps == 1) {
+ element.onpause = onpause;
+ element.pause();
+ } else if (steps == 2) {
+ stream.getTracks().forEach(t => t.stop());
+ }
+ }
+
+ element.onplaying = onplaying;
+ element.srcObject = stream;
+
+ isnot(element.audioTracks, undefined,
+ 'HTMLMediaElement::AudioTracks() property should be available.');
+ isnot(element.videoTracks, undefined,
+ 'HTMLMediaElement::VideoTracks() property should be available.');
+
+ steps++;
+ await element.play();
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv(
+ {
+ "set": [
+ ["media.track.enabled", true]
+ ]
+ }, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_mediatrack_parsing_ogg.html b/dom/media/test/test_mediatrack_parsing_ogg.html
new file mode 100644
index 0000000000..aabd40e2a3
--- /dev/null
+++ b/dom/media/test/test_mediatrack_parsing_ogg.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test events of media track interfaces</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+function localCheckMetadata(msg, e) {
+ ok(msg in gOggTrackInfoResults, "File: " + msg + " is in pre-parsed gOggTrackInfoResults list");
+ var r = gOggTrackInfoResults[msg];
+
+ var hasExpectedAudio = r && r.hasOwnProperty("audio_id");
+ var hasExpectedVideo = r && r.hasOwnProperty("video_id");
+
+ var hasParsedAudio = e.audioTracks.length >= 1;
+ var hasParsedVideo = e.videoTracks.length >= 1;
+
+ ok(!(hasExpectedAudio ^ hasParsedAudio), "Check availability of expected/parsed audio");
+ ok(!(hasExpectedVideo ^ hasParsedVideo), "Check availability of expected/parsed video");
+ if (hasParsedAudio) {
+ is(e.audioTracks.length, 1, "The length of audio track should be 1");
+ is(e.audioTracks[0].id, r.audio_id, "File: " + msg + ", Audio track id");
+ is(e.audioTracks[0].kind, r.audio_kind, "File: " + msg + ", Audio track kind");
+ is(e.audioTracks[0].language, r.audio_language, "File: " + msg + ", Audio track language");
+ is(e.audioTracks[0].label, r.audio_label, "File: " + msg + ", Audio track label");
+ }
+ if (hasParsedVideo) {
+ is(e.videoTracks.length, 1, "The length of video track should be 1");
+ is(e.videoTracks[0].id, r.video_id, "File: " + msg + ", Video track id");
+ is(e.videoTracks[0].kind, r.video_kind, "File: " + msg + ", Video track kind");
+ is(e.videoTracks[0].language, r.video_language, "File: " + msg + ", Video track language");
+ is(e.videoTracks[0].label, r.video_label, "File: " + msg + ", Video track label");
+ }
+}
+
+function startTest(test, token) {
+ var v = document.createElement('video');
+ v.preload = "metadata";
+ v.token = token;
+ manager.started(token);
+
+ v.src = test.name;
+ v.name = test.name;
+
+ v.onloadedmetadata = function(evt) {
+ localCheckMetadata(evt.target.name, evt.target);
+ evt.target.finished = true;
+ evt.target.onloadedmetadata = null;
+ removeNodeAndSource(evt.target);
+ manager.finished(evt.target.token);
+ };
+
+ document.body.appendChild(v);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({"set": [["media.track.enabled", true]]},
+ function() {
+ manager.runTests(gMultitrackInfoOggPlayList, startTest);
+ }
+);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_mediatrack_replay_from_end.html b/dom/media/test/test_mediatrack_replay_from_end.html
new file mode 100644
index 0000000000..16b0cbeb97
--- /dev/null
+++ b/dom/media/test/test_mediatrack_replay_from_end.html
@@ -0,0 +1,160 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test media tracks if replay after playback has ended</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+const manager = new MediaTestManager;
+
+function startTest(test, token) {
+ // Scenario to test:
+ // 1. Audio tracks and video tracks should be added to the track list when
+ // metadata has loaded, and all tracks should remain even after we seek to
+ // the end.
+ // 2. No tracks should be added back to the list if we replay from the end,
+ // and no tracks should be removed from the list after we seek to the end.
+ // 3. After seek to the middle from end of playback, all tracks should remain
+ // in the list if we play from here, and no tracks should be removed from
+ // the list after we seek to the end.
+ // 4. Unsetting the media element's src attribute should remove all tracks.
+
+ const elemType = getMajorMimeType(test.type);
+ const element = document.createElement(elemType);
+
+ let audioOnaddtrack = 0;
+ let audioOnremovetrack = 0;
+ let videoOnaddtrack = 0;
+ let videoOnremovetrack = 0;
+ let isPlaying = false;
+ let steps = 0;
+
+ element.audioTracks.onaddtrack = function(e) {
+ audioOnaddtrack++;
+ }
+
+ element.audioTracks.onremovetrack = function(e) {
+ audioOnremovetrack++;
+ }
+
+ element.videoTracks.onaddtrack = function(e) {
+ videoOnaddtrack++;
+ }
+
+ element.videoTracks.onremovetrack = function(e) {
+ videoOnremovetrack++;
+ }
+
+ function testExpectedAddtrack(expectedCalls) {
+ if (test.hasAudio) {
+ is(audioOnaddtrack, expectedCalls,
+ 'Calls of onaddtrack on audioTracks should be '+expectedCalls+' times.');
+ }
+ if (test.hasVideo) {
+ is(videoOnaddtrack, expectedCalls,
+ 'Calls of onaddtrack on videoTracks should be '+expectedCalls+' times.');
+ }
+ }
+
+ function testExpectedRemovetrack(expectedCalls) {
+ if (test.hasAudio) {
+ is(audioOnremovetrack, expectedCalls,
+ 'Calls of onremovetrack on audioTracks should be '+expectedCalls+' times.');
+ }
+ if (test.hasVideo) {
+ is(videoOnremovetrack, expectedCalls,
+ 'Calls of onremovetrack on videoTracks should be '+expectedCalls+' times.');
+ }
+ }
+
+ function finishTesting() {
+ element.onpause = null;
+ element.onseeked = null;
+ element.onplaying = null;
+ element.onended = null;
+ manager.finished(element.token);
+ }
+
+ function onended() {
+ if (isPlaying) {
+ switch(steps) {
+ case 1:
+ testExpectedAddtrack(1);
+ testExpectedRemovetrack(0);
+ element.onplaying = onplaying;
+ element.play();
+ steps++;
+ break;
+ case 2:
+ testExpectedAddtrack(1);
+ testExpectedRemovetrack(0);
+ element.currentTime = element.duration * 0.5;
+ element.onplaying = onplaying;
+ element.play();
+ steps++;
+ break;
+ case 3:
+ testExpectedAddtrack(1);
+ testExpectedRemovetrack(0);
+ element.src = "";
+ setTimeout(() => {
+ testExpectedAddtrack(1);
+ testExpectedRemovetrack(1);
+ finishTesting();
+ }, 0);
+ break;
+ }
+ } else {
+ ok(true, 'Finish the test anyway if ended is fired before other events.');
+ finishTesting();
+ }
+ }
+
+ function seekToEnd() {
+ element.onpause = null;
+ element.currentTime = element.duration * 1.1;
+ }
+
+ function onseeked() {
+ element.onseeked = null;
+ element.onpause = seekToEnd;
+ element.pause();
+ }
+
+ function onplaying() {
+ isPlaying = true;
+ element.onplaying = null;
+ element.onseeked = onseeked;
+ }
+
+ element.token = token;
+ manager.started(token);
+
+ element.src = test.name;
+ element.test = test;
+ element.onplaying = onplaying;
+ element.onended = onended;
+ element.play();
+ steps++;
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv(
+ {
+ "set": [
+ ["media.track.enabled", true]
+ ]
+ },
+ function() {
+ manager.runTests(gTrackTests, startTest);
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_metadata.html b/dom/media/test/test_metadata.html
new file mode 100644
index 0000000000..08f28f5f47
--- /dev/null
+++ b/dom/media/test/test_metadata.html
@@ -0,0 +1,82 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test returning metadata from media files with mozGetMetadata()</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<div id="output"></div>
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+function startTest(test, token) {
+ var a = document.createElement('audio');
+ a.preload = "metadata";
+ a.token = token;
+ manager.started(token);
+
+ a.src = test.name;
+ a.name = test.name;
+
+ // Tags should not be available immediately.
+ var exception_fired = false;
+ try {
+ a.mozGetMetadata();
+ } catch (e) {
+ is(e.name, 'InvalidStateError',
+ "early mozGetMetadata() should throw InvalidStateError");
+ exception_fired = true;
+ }
+ ok(exception_fired,
+ "mozGetMetadata() should throw an exception before HAVE_METADATA");
+
+ // Wait until metadata has loaded.
+ a.addEventListener('loadedmetadata', function() {
+ // read decoded tags
+ let tags = a.mozGetMetadata();
+ ok(tags, "mozGetMetadata() should return a truthy value");
+ // Dump them out.
+ var d = document.getElementById('output');
+ var html = '<table>\n';
+ html += '<caption><p>Called getMozMetadata()'
+ html += ' on '+test.name+'</p></caption>\n';
+ html += '<tr><th>tag</th>';
+ html += '<th>decoded value</th><th>expected value</th></tr>\n';
+ for (let tag in tags) {
+ html += '<tr><td>'+tag+'</td>';
+ html += '<td>'+tags[tag]+'</td>';
+ html += '<td>'+test.tags[tag]+'</td>';
+ html += '</tr>\n';
+ }
+ if (!Object.keys(tags).length) {
+ html += '<tr><td colspan=3 align=center><em>no tags</em></td></tr>\n';
+ }
+ html += '</table>\n';
+ var div = document.createElement('div');
+ // eslint-disable-next-line no-unsanitized/property
+ div.innerHTML = html;
+ d.appendChild(div);
+ // Verify decoded tag values.
+ for (let tag in tags) {
+ is(tags[tag], test.tags[tag], "Tag '"+tag+"' should match");
+ }
+ // Verify expected tag values
+ for (let tag in test.tags) {
+ is(tags[tag], test.tags[tag], "Tag '"+tag+"' should match");
+ }
+ removeNodeAndSource(a);
+ manager.finished(token);
+ });
+}
+
+manager.runTests(gMetadataTests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_midflight_redirect_blocked.html b/dom/media/test/test_midflight_redirect_blocked.html
new file mode 100644
index 0000000000..ea85673b45
--- /dev/null
+++ b/dom/media/test/test_midflight_redirect_blocked.html
@@ -0,0 +1,87 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Test mid-flight cross site redirects are blocked</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ </head>
+ <body>
+ <pre id='test'>
+ <script class="testbody" type='application/javascript'>
+
+ function testIfLoadsToMetadata(test, useCors) {
+ return new Promise(function(resolve, reject) {
+ var elemType = getMajorMimeType(test.type);
+ var element = document.createElement(elemType);
+
+ if (useCors) {
+ element.crossOrigin = "anonymous";
+ }
+
+ // Log events for debugging.
+ [
+ "suspend", "play", "canplay", "canplaythrough", "loadstart",
+ "loadedmetadata", "loadeddata", "playing", "ended", "error",
+ "stalled", "emptied", "abort", "waiting", "pause"
+ ].forEach((eventName) => {
+ element.addEventListener(eventName, (event)=> {
+ info(test.name + " " + event.type);
+ });
+ });
+
+ element.addEventListener("loadedmetadata", ()=>{
+ resolve(true);
+ removeNodeAndSource(element);
+ });
+
+ element.addEventListener("error", ()=>{
+ resolve(false);
+ removeNodeAndSource(element);
+ });
+
+ // Note: request redirect before the end of metadata, otherwise we won't
+ // error before metadata has loaded if mixed origins are allowed.
+ element.src = "midflight-redirect.sjs?resource=" + test.name
+ + (useCors ? "&cors" : "")
+ + "&type=" + test.type
+ + "&redirectAt=200";
+ element.preload = "metadata";
+ document.body.appendChild(element);
+ element.load()
+ });
+ }
+
+ let v = document.createElement("video");
+ const testCases = gSmallTests.filter(t => v.canPlayType(t.type));
+
+ async function testMediaLoad(message, {useCors}) {
+ for (let test of testCases) {
+ let loaded = await testIfLoadsToMetadata(test, useCors);
+ is(loaded, useCors, test.name + " " + message);
+ }
+ }
+
+ async function runTest() {
+ try {
+ SimpleTest.info("Test that all media do not play without CORS...");
+ await testMediaLoad("expected to be blocked", {useCors: false});
+
+ SimpleTest.info("Test that all media play if CORS used...");
+ await testMediaLoad("expected to play with CORS", {useCors: true});
+ } catch (e) {
+ info("Exception " + e.message);
+ ok(false, "Threw exception " + e.message);
+ }
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+
+ addLoadEvent(runTest);
+
+ </script>
+ </pre>
+ </body>
+</html>
diff --git a/dom/media/test/test_mixed_principals.html b/dom/media/test/test_mixed_principals.html
new file mode 100644
index 0000000000..c00d65b983
--- /dev/null
+++ b/dom/media/test/test_mixed_principals.html
@@ -0,0 +1,83 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=489415
+-->
+<head>
+ <title>Test for Bug 489415</title>
+ <script type="application/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <style>
+ video {
+ width: 40%;
+ border: solid black 1px;
+ }
+ </style>
+</head>
+
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=489415">Mozilla Bug 489415</a>
+ <p id="display"></p>
+ <pre id="test">
+ <script type="text/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ var pushPrefs = (...p) => SpecialPowers.pushPrefEnv({ set: p });
+ var count = 0;
+
+ function canReadBack(video) {
+ var c = document.createElement("canvas");
+ var ctx = c.getContext("2d");
+ ctx.drawImage(video, 0, 0);
+ try {
+ c.toDataURL();
+ return true;
+ } catch (ex) {
+ return false;
+ }
+ }
+
+ function runTest(origin, shouldReadBackOnLoad) {
+ return new Promise(function (resolve, reject) {
+ // Load will redirect mid-flight, which will be detected and should error,
+ // and we should no longer be able to readback.
+ var video = document.createElement("video");
+ video.preload = "metadata";
+ video.controls = true;
+ var url = "http://" + origin + "/tests/dom/media/test/midflight-redirect.sjs"
+ + "?resource=pixel_aspect_ratio.mp4&type=video/mp4";
+ SimpleTest.info("Loading from " + url);
+ video.src = url;
+ document.body.appendChild(video);
+
+ once(video, "loadeddata", () => {
+ is(canReadBack(video), shouldReadBackOnLoad, "Should be able to readback");
+ video.play();
+ });
+
+ once(video, "error", () => {
+ is(canReadBack(video), false, "Should not be able to readback after error");
+ removeNodeAndSource(video);
+ resolve();
+ });
+
+ once(video, "ended", () => {
+ ok(false, "Should not be able to playback to end, we should have errored!");
+ removeNodeAndSource(video);
+ resolve();
+ });
+
+ });
+ }
+
+ Promise.all([
+ runTest("mochi.test:8888", true),
+ runTest("example.org", false),
+ ]).then(SimpleTest.finish);
+
+ </script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_mozHasAudio.html b/dom/media/test/test_mozHasAudio.html
new file mode 100644
index 0000000000..c25873c786
--- /dev/null
+++ b/dom/media/test/test_mozHasAudio.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test playback of media files that should play OK</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+function onloadedmetadata(e) {
+ var t = e.target;
+ is(t.mozHasAudio, t.hasAudio, "The element reports the wrong audio property." + t.token);
+ manager.finished(t.token);
+}
+
+function startTest(test, token) {
+ var elemType = /^audio/.test(test.type) ? "audio" : "video";
+ var element = document.createElement(elemType);
+ element.preload = "auto";
+
+ element.token = token;
+ manager.started(token);
+
+ element.src = test.name;
+ element.name = test.name;
+ element.hasAudio = elemType == "video" ? test.hasAudio : undefined;
+ element.addEventListener("loadedmetadata", onloadedmetadata);
+
+ element.load();
+}
+
+manager.runTests(gTrackTests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_mp3_broadcast.html b/dom/media/test/test_mp3_broadcast.html
new file mode 100644
index 0000000000..7922d8b4b6
--- /dev/null
+++ b/dom/media/test/test_mp3_broadcast.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+
+<head>
+ <title>Test playback of broadcast-like streams</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+
+<body>
+ <audio controls id=a style="width: 100%;"></audio>
+ <script type="module">
+ SimpleTest.waitForExplicitFinish();
+ const streams = [
+ // An mp3 bytestream consisting of a complete mp3 file with a XING
+ // header, that has duration information, but served without a
+ // `Content-Length`. It is followed by a second mp3 bytestream that
+ // also has a XING header. While some software are able to play the
+ // entire file, Web browser don't.
+ { src: "two-xing-header-no-content-length.mp3", duration: 1 },
+ // An mp3 bytestream consisting of a complete mp3 file with a XING
+ // header, that has duration information, but served without a
+ // `Content-Length` header. It is followed by a second mp3 bytestream that
+ // doesn't have a XING header.
+ // This scenario is typical in radio broadcast scenario, when the
+ // live-stream has a pre-recorded prelude. The reported duration,
+ // after "ended" has been received, is the duration of playback.
+ { src: "single-xing-header-no-content-length.mp3", duration: 11.050839},
+ ];
+ var audio = window.a;
+ // Prevent ESLint error about top-level await
+ (async function () {
+ for (let i of streams) {
+ audio.src = i.src;
+ audio.load();
+ audio.play();
+ audio.onerror = (e) => {
+ ok(false, `${i}: error: ${e.message}}`);
+ };
+ await once(audio, "ended");
+ ok(true, `${i}: playback through the end`);
+ is(audio.duration, i.duration, "Duration at end is correct");
+ is(audio.currentTime, i.duration, "Current time at end is correct");
+ }
+ SimpleTest.finish();
+ })()
+ </script>
+</body>
+
+</html>
+
diff --git a/dom/media/test/test_mp3_with_multiple_ID3v2.html b/dom/media/test/test_mp3_with_multiple_ID3v2.html
new file mode 100644
index 0000000000..1f2f946520
--- /dev/null
+++ b/dom/media/test/test_mp3_with_multiple_ID3v2.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Play mp3 file with multiple ID3v2 tags</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script type="application/javascript">
+
+add_task(async function testPlayMP3WithMultipleID3Tags() {
+ info(`adjust cache size`);
+ await SpecialPowers.pushPrefEnv(
+ // The second ID3v2 header is huge (4622361 bytes) so the first audio samle
+ // in this file is in the position 4945370, so we have to extend the size of
+ // the cache.
+ {"set": [["media.cache_size", 5000000]]}
+ );
+
+ info(`create audio and wait its loading`);
+ let audio = document.createElement('audio');
+ audio.src = "multi_id3v2.mp3";
+ document.body.appendChild(audio);
+ await new Promise(r => audio.onloadeddata = r);
+ ok(true, `finishing loading data.`)
+});
+
+</script>
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/media/test/test_multiple_mediastreamtracks.html b/dom/media/test/test_multiple_mediastreamtracks.html
new file mode 100644
index 0000000000..fb28d8652e
--- /dev/null
+++ b/dom/media/test/test_multiple_mediastreamtracks.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test the ability of MediaStream with multiple MediaStreamTracks</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="gUM_support.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+async function startTest() {
+ try {
+ await setupGetUserMediaTestPrefs();
+ let orgStream = await navigator.mediaDevices.getUserMedia({audio: true, video: true});
+ let a = orgStream.getAudioTracks()[0];
+ let v = orgStream.getVideoTracks()[0];
+ let stream = new MediaStream([a, a, a, a, v, v, v].map(track => track.clone()));
+ let element = document.createElement("video");
+
+ element.onloadedmetadata = function() {
+ is(stream.getAudioTracks().length, 4, 'Length of audio tracks should be 4.');
+ is(stream.getVideoTracks().length, 3, 'Length of vudio tracks should be 3.');
+ SimpleTest.finish();
+ };
+
+ element.srcObject = stream;
+ element.play();
+ } catch (err) {
+ ok(false, 'Unexpected error fired with: ' + err);
+ SimpleTest.finish();
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv(
+ {
+ "set": [
+ ["media.track.enabled", true]
+ ]
+ }, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_networkState.html b/dom/media/test/test_networkState.html
new file mode 100644
index 0000000000..8001ca514c
--- /dev/null
+++ b/dom/media/test/test_networkState.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: networkState</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body onunload="mediaTestCleanup();">
+<video id='v1'></video><audio id='a1'></audio>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+"use strict";
+var v1 = document.getElementById('v1');
+var a1 = document.getElementById('a1');
+var passed = "truthy";
+
+try {
+ v1.networkState = 0;
+} catch (e) {
+ passed = !passed;
+}
+try {
+ a1.networkState = 0;
+} catch (e) {
+ passed = !passed;
+}
+ok(passed === true,
+ "Setting networkState throws in strict mode (readonly attribute)");
+</script>
+
+<script class="testbody" type="text/javascript">
+var v1 = document.getElementById('v1');
+var a1 = document.getElementById('a1');
+var passed = false;
+
+var oldv1ns = v1.networkState, olda1ns = a1.networkState;
+try {
+ v1.networkState = 0;
+ a1.networkState = 0;
+ passed = v1.networkState === oldv1ns && a1.networkState === olda1ns;
+} catch (e) { }
+ok(passed, "Should not be able to modify networkState (readonly attribute)");
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_new_audio.html b/dom/media/test/test_new_audio.html
new file mode 100644
index 0000000000..e1f8964f73
--- /dev/null
+++ b/dom/media/test/test_new_audio.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=528566
+-->
+<head>
+ <title>Test for Bug 528566</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=528566">Mozilla Bug 528566</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 528566 **/
+
+var manager = new MediaTestManager;
+
+var player = new Audio();
+
+function startTest(test, token) {
+ if (!player.canPlayType(test.type)) {
+ return;
+ }
+ manager.started(token);
+ var a = new Audio(test.name);
+ a.autoplay = true;
+ document.body.appendChild(a);
+ a.addEventListener("ended",
+ function(e){
+ ok(true, "[" + a.src + "]We should get to the end. Oh look we did.");
+ a.remove();
+ manager.finished(token);
+ });
+}
+
+manager.runTests(gAudioTests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_no_load_event.html b/dom/media/test/test_no_load_event.html
new file mode 100644
index 0000000000..1e2717d983
--- /dev/null
+++ b/dom/media/test/test_no_load_event.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=715469
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 715469</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body onload="start();">
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=715469">Mozilla Bug 715469</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+
+<script type="application/javascript">
+
+/** Test for Bug 715469 **/
+
+var gotLoadEvent = false;
+
+function start() {
+ var resource = getPlayableVideo(gSmallTests);
+ if (resource == null) {
+ todo(false, "No types supported");
+ } else {
+ SimpleTest.waitForExplicitFinish();
+ var v = document.createElement("video");
+ v.src = resource.name;
+
+ v.addEventListener("load", function() {
+ gotLoadEvent = true;
+ });
+
+ v.addEventListener("ended", finished);
+ document.body.appendChild(v);
+ v.play();
+ }
+}
+
+function finished() {
+ is(gotLoadEvent, false, "Should not receive a load on the video element");
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_not_reset_playbackRate_when_removing_nonloaded_media_from_document.html b/dom/media/test/test_not_reset_playbackRate_when_removing_nonloaded_media_from_document.html
new file mode 100644
index 0000000000..f1b8cc8b15
--- /dev/null
+++ b/dom/media/test/test_not_reset_playbackRate_when_removing_nonloaded_media_from_document.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Do not reset playback rate when removing non-loaded media from a document</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="manifest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+/**
+ * When removing media from a document, it should only trigger internal pause,
+ * instead of the pause method, which would trigger loading process and reset
+ * the media's playback rate for non-loaded media.
+ */
+async function startTest() {
+ info(`create a media and append it to a document`);
+ const audio = document.createElement("audio");
+ document.body.appendChild(audio);
+
+ info(`change audio's playbackRate and remove it from a document`);
+ const expectedRate = 0.1;
+ audio.playbackRate = expectedRate;
+ await once(audio, "ratechange");
+ is(audio.playbackRate, expectedRate,
+ `${audio.playbackRate} is equal to ${expectedRate}`);
+ audio.remove();
+
+ info(`queue a macrotask to check if the playback rate is still unchanged`);
+ setTimeout(() => {
+ // If we unexpectedly reset the playback rate, it would happen in a
+ // microtask when removing media from a document [1] (Await a stable state),
+ // which would always be run before the macrotask.
+ // [1] https://html.spec.whatwg.org/#playing-the-media-resource:remove-an-element-from-a-document
+ is(audio.playbackRate, expectedRate,
+ `${audio.playbackRate} is equal to ${expectedRate}`);
+ SimpleTest.finish();
+ }, 0);
+}
+
+SimpleTest.waitForExplicitFinish();
+onload = startTest;
+
+</script>
+</body>
+</html>
diff --git a/dom/media/test/test_paused.html b/dom/media/test/test_paused.html
new file mode 100644
index 0000000000..17af4d3898
--- /dev/null
+++ b/dom/media/test/test_paused.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: paused</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<video id='v1'></video><audio id='a1'></audio>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var v1 = document.getElementById('v1');
+var a1 = document.getElementById('a1');
+ok(v1.paused, "v1.paused must initially be true");
+ok(a1.paused, "a1.paused must initially be true");
+mediaTestCleanup();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_paused_after_ended.html b/dom/media/test/test_paused_after_ended.html
new file mode 100644
index 0000000000..83f27a921f
--- /dev/null
+++ b/dom/media/test/test_paused_after_ended.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: paused</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+function ended(evt) {
+ var v = evt.target;
+ v.removeEventListener("ended", ended);
+ is(v.gotPause, true, "We should have received a \"pause\" event.")
+ is(v.paused, true, v._name + " must be paused after end");
+ manager.finished(v.token);
+ removeNodeAndSource(v);
+}
+
+function pause(evt) {
+ var v = evt.target;
+ v.removeEventListener("pause", pause);
+ v.gotPause = true;
+}
+
+function startTest(test, token) {
+ var v = document.createElement('video');
+ document.body.appendChild(v);
+ v.token = token;
+ manager.started(v.token);
+ v.src = test.name;
+ v._name = test.name;
+ v._finished = false;
+ v.load();
+ is(v.paused, true, v._name + " must be paused at start");
+
+ v.play();
+ is(v.paused, false, v._name + " must not be paused after play");
+
+ v.addEventListener("pause", pause);
+ v.addEventListener("ended", ended);
+}
+
+manager.runTests(gPausedAfterEndedTests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_periodic_timeupdate.html b/dom/media/test/test_periodic_timeupdate.html
new file mode 100644
index 0000000000..5d40c7e38a
--- /dev/null
+++ b/dom/media/test/test_periodic_timeupdate.html
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Periodic timeupdate test</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script type="application/javascript">
+
+/**
+ * To ensure that when dispatching periodic `timeupdate` event, we would only
+ * dispatch that once within 250ms according to the spec. This test would end
+ * after receiving certain number of timeupdate` event.
+ */
+const sNumPeriodicTimeupdatesToEndTestAfter = 5;
+const sTimeThreshold = 250;
+
+add_task(async function testPeriodicTimeupdateShouldOnlyBeDispatchedOnceWithin250Ms() {
+ const video = document.createElement('video');
+ video.src = "gizmo.mp4";
+ video.loop = true;
+ video._timeupdateCount = 0;
+ document.body.appendChild(video);
+ ok(await video.play().then(_=>true,_=>false), "video started playing");
+ const culprit = createCulpritToMakeMainThreadBusy();
+ await new Promise(r => {
+ function endTest() {
+ video.removeEventListener("timeupdate", checkTimeupdate);
+ culprit.shutdown();
+ r();
+ }
+ video.onseeking = () => {
+ info(`seeking starts (looping back to the head)`);
+ video._ignoreEvents = true;
+ }
+ video.onseeked = () => {
+ info(`seeking ends`);
+ video._ignoreEvents = false;
+ }
+ function checkTimeupdate(event) {
+ // When reaching to the end, video would perform a seek to the start
+ // position where one mandatory `timeupdate` would be dispatched.
+ if (video._ignoreEvents) {
+ info(`ignore non-periodic timeupdate because that is allowed to be dispatched within ${sTimeThreshold}ms`);
+ return;
+ }
+
+ const now = performance.now();
+ if (video._prevTime === undefined) {
+ info(`recevied the first 'timeupdate'`);
+ video._prevTime = now;
+ return;
+ }
+
+ const timeDiff = now - video._prevTime;
+ if (timeDiff < sTimeThreshold) {
+ ok(false, `Time diff ${timeDiff} is smaller than ${sTimeThreshold}ms!`);
+ endTest();
+ return;
+ }
+
+ ok(true, `Time diff ${timeDiff} since last time received 'timeupdate'`);
+ video._prevTime = now;
+ info(`check timeupdate ${++video._timeupdateCount} time`);
+ if (video._timeupdateCount == sNumPeriodicTimeupdatesToEndTestAfter) {
+ endTest();
+ }
+ };
+ video.addEventListener("timeupdate", checkTimeupdate);
+ });
+});
+
+window.onmessage = _ => blockMainThreadForMilliseconds(1);
+
+/**
+ * Following are helper functions
+ */
+function blockMainThreadForMilliseconds(ms) {
+ const lastTime = performance.now();
+ while (lastTime + ms > performance.now());
+}
+
+function createCulpritToMakeMainThreadBusy() {
+ let culprit = {};
+ culprit._id = setInterval(_ => {
+ blockMainThreadForMilliseconds(1000);
+ }, 0);
+ culprit.shutdown = _ => {
+ clearInterval(culprit._id);
+ }
+ for (let i = 0; i < 5000; ++i) {
+ window.postMessage("foo", "*");
+ }
+ return culprit;
+}
+
+</script>
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/media/test/test_play_events.html b/dom/media/test/test_play_events.html
new file mode 100644
index 0000000000..44b819b488
--- /dev/null
+++ b/dom/media/test/test_play_events.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: play() method</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script>
+
+var manager = new MediaTestManager;
+
+var tokens = {
+ 0: ["play"],
+ "play": ["canplay"],
+ "canplay": ["playing"],
+ "playing": ["canplay", "canplaythrough"],
+ "canplaythrough": ["canplay", "canplaythrough"]
+};
+
+function gotPlayEvent(event) {
+ var v = event.target;
+ ok(tokens[v._state].includes(event.type),
+ "Check expected event got " + event.type + " at " + v._state + " for " + v.src +
+ " tokens["+v._state+"]=" + tokens[v._state] +
+ " tokens["+v._state+"].indexOf(event.type)=" + tokens[v._state].indexOf(event.type));
+ v._state = event.type;
+}
+
+function ended(event) {
+ var v = event.target;
+ removeNodeAndSource(v);
+ manager.finished(v.token);
+}
+
+function initTest(test, token) {
+ var v = document.createElement('video');
+ v.token = token;
+ manager.started(token);
+ v._state = 0;
+
+ ["play", "canplay", "playing", "canplaythrough"].forEach(function (e) {
+ v.addEventListener(e, gotPlayEvent);
+ });
+
+ v.addEventListener("ended", ended);
+
+ v.src = test.name;
+ document.body.appendChild(v); // Causes load.
+ v.play();
+}
+
+manager.runTests(gSmallTests, initTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_play_events_2.html b/dom/media/test/test_play_events_2.html
new file mode 100644
index 0000000000..022bb5373a
--- /dev/null
+++ b/dom/media/test/test_play_events_2.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: play() method via DOM 0 handlers</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script>
+var manager = new MediaTestManager;
+
+var tokens = {
+ 0: ["play"],
+ "play": ["canplay"],
+ "canplay": ["playing"],
+ "playing": ["canplay", "canplaythrough"],
+ "canplaythrough": ["canplay", "canplaythrough"]
+};
+
+function gotPlayEvent(event) {
+ var v = event.target;
+ ok(tokens[v._state].includes(event.type),
+ "Check expected event got " + event.type + " at " + v._state + " for " + v.src);
+ v._state = event.type;
+}
+
+function ended(event) {
+ var v = event.target;
+ v._finished = true;
+ removeNodeAndSource(v);
+ manager.finished(v.token);
+}
+
+function startTest(test, token) {
+ var v = document.createElement('video');
+ v.token = token;
+ manager.started(token);
+ v._state = 0;
+ v._finished = false;
+
+ ["play", "canplay", "playing", "canplaythrough"].forEach(function (e) {
+ v["on" + e] = gotPlayEvent;
+ });
+
+ v.onended = ended;
+
+ v.src = test.name;
+ document.body.appendChild(v); // Causes load.
+ v.play();
+}
+
+manager.runTests(gSmallTests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_play_promise_1.html b/dom/media/test/test_play_promise_1.html
new file mode 100644
index 0000000000..ce4e287f34
--- /dev/null
+++ b/dom/media/test/test_play_promise_1.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: promise-based play() method</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script>
+// Name: playBeforeCanPlay
+// Case: invoke play() on an element that doesn't have enough data
+// Expected result: resolve the promise
+
+let manager = new MediaTestManager;
+
+function initTest(test, token) {
+ manager.started(token);
+
+ let element = document.createElement(getMajorMimeType(test.type));
+ element.src = test.name;
+ ok(element.readyState == HTMLMediaElement.HAVE_NOTHING);
+
+ element.play().then(
+ (result) => {
+ if (result == undefined) {
+ ok(true, `${token} is resolved with ${result}.`);
+ } else {
+ ok(false, `${token} is resolved with ${result}.`);
+ }
+ },
+ (error) => {
+ ok(false, `${token} is rejected with ${error.name}.`);
+ }
+ ).then( () => { manager.finished(token); } );
+}
+
+manager.runTests(gSmallTests, initTest);
+
+</script> \ No newline at end of file
diff --git a/dom/media/test/test_play_promise_10.html b/dom/media/test/test_play_promise_10.html
new file mode 100644
index 0000000000..1c0096986c
--- /dev/null
+++ b/dom/media/test/test_play_promise_10.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: promise-based play() method</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script>
+// Name: loadRejectsPendingPromises
+// Case: invoke load() on an element with pending promises.
+// Expected result: reject all the pending promises with AbortError DOM exception.
+
+let manager = new MediaTestManager;
+
+function initTest(test, token) {
+ manager.started(token);
+
+ let element = document.createElement(getMajorMimeType(test.type));
+ element.play().then(
+ (result) => {
+ ok(false, `${token} is resolved with ${result}.`);
+ },
+ (error) => {
+ if (error.name == "AbortError") {
+ ok(true, `${token} is rejected with ${error.name}.`);
+ } else {
+ ok(false, `${token} is rejected with ${error.name}.`);
+ }
+ }
+ ).then( () => { manager.finished(token); } );
+ element.load();
+}
+
+manager.runTests(gSmallTests, initTest);
+
+</script> \ No newline at end of file
diff --git a/dom/media/test/test_play_promise_11.html b/dom/media/test/test_play_promise_11.html
new file mode 100644
index 0000000000..a59baf5657
--- /dev/null
+++ b/dom/media/test/test_play_promise_11.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: promise-based play() method</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script>
+// Name: newSrcRejectPendingPromises
+// Case: change src of an element with pending promises.
+// Expected result: reject all the pending promises.
+
+let manager = new MediaTestManager;
+
+function initTest(test, token) {
+ manager.started(token);
+
+ let element = document.createElement(getMajorMimeType(test.type));
+ element.play().then(
+ (result) => {
+ ok(false, `${token} is resolved with ${result}.`);
+ },
+ (error) => {
+ if (error.name == "AbortError") {
+ ok(true, `${token} is rejected with ${error.name}.`);
+ } else {
+ ok(false, `${token} is rejected with ${error.name}.`);
+ }
+ }
+ ).then( () => { manager.finished(token); } );
+ element.src = test.name;
+}
+
+manager.runTests(gSmallTests, initTest);
+
+</script> \ No newline at end of file
diff --git a/dom/media/test/test_play_promise_12.html b/dom/media/test/test_play_promise_12.html
new file mode 100644
index 0000000000..50972885eb
--- /dev/null
+++ b/dom/media/test/test_play_promise_12.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: promise-based play() method</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script>
+// Name: pausePlayAfterPlaybackStarted
+// Case: invoke pause() and then play() on an element that is already playing.
+// Expected result: resolve the promise with undefined.
+
+let manager = new MediaTestManager;
+
+function initTest(test, token) {
+ manager.started(token);
+
+ let element = document.createElement(getMajorMimeType(test.type));
+ element.preload = "auto";
+ element.src = test.name;
+ element.play();
+ once(element, "playing").then(() => {
+ element.pause();
+ element.play().then(
+ (result) => {
+ if (result == undefined) {
+ ok(true, `${token} is resolved with ${result}.`);
+ } else {
+ ok(false, `${token} is resolved with ${result}.`);
+ }
+ },
+ (error) => {
+ ok(false, `${token} is rejected with ${error.name}.`);
+ }
+ ).then( () => { manager.finished(token); } );
+ });
+}
+
+manager.runTests(gSmallTests, initTest);
+
+</script> \ No newline at end of file
diff --git a/dom/media/test/test_play_promise_13.html b/dom/media/test/test_play_promise_13.html
new file mode 100644
index 0000000000..aacab88895
--- /dev/null
+++ b/dom/media/test/test_play_promise_13.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: promise-based play() method</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script>
+// Name: loadAlgorithmDoesNotCancelTasks
+// Case: re-invoke the load() on an element which had dispatched a task to resolve a promise.
+// Expected result: the already dispatched promise should still be resolved with undefined.
+
+let manager = new MediaTestManager;
+
+function initTest(test, token) {
+ manager.started(token);
+
+ let element = document.createElement(getMajorMimeType(test.type));
+ element.preload = "auto";
+ element.src = test.name;
+
+ // We must wait for "canplay" event; otherwise, invoke play() will lead to a
+ // pending promise and will then be rejected by the following load().
+ once(element, "canplay").then(() => {
+ // The play() promise will be queued to be resolved immediately, which means
+ // the promise is not in the pending list.
+ element.play().then(
+ (result) => {
+ if (result == undefined) {
+ ok(true, `${token} is resolved with ${result}.`);
+ } else {
+ ok(false, `${token} is resolved with ${result}.`);
+ }
+ },
+ (error) => {
+ ok(false, `${token} is rejected with ${error.name}.`);
+ }
+ ).then( () => { manager.finished(token); } );
+ element.src = test.name; // Re-invoke the load algorithm again.
+ });
+}
+
+manager.runTests(gSmallTests, initTest);
+
+</script> \ No newline at end of file
diff --git a/dom/media/test/test_play_promise_14.html b/dom/media/test/test_play_promise_14.html
new file mode 100644
index 0000000000..066138ecad
--- /dev/null
+++ b/dom/media/test/test_play_promise_14.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: promise-based play() method</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script>
+// Name: loadAlgorithmKeepPromisesPendingWhenNotPausing
+// Case: step1: create an element with its paused member to be fause and networkState to be NETWORK_EMPTY.
+// stpe2: invoke load() on the element and the load() leaves the promise pending.
+// Expected result: the pending promise should finally be resolved.
+
+let manager = new MediaTestManager;
+
+function initTest(test, token) {
+ manager.started(token);
+
+ let element = document.createElement(getMajorMimeType(test.type));
+ // Invoke play() -> (1) the promise will be left pending.
+ // (2) invoke load() -> (1) set the networkState to be NETWORK_NO_SOURCE.
+ // (2) queue a task to run resouce selection algorithm.
+ element.play().then(
+ (result) => {
+ if (result == undefined) {
+ ok(true, `${token} is resolved with ${result}.`);
+ } else {
+ ok(false, `${token} is resolved with ${result}.`);
+ }
+ },
+ (error) => {
+ ok(false, `${token} is rejected with ${error.name}.`);
+ }
+ ).then( () => { manager.finished(token); } );
+
+ once(element, "play").then(() => {
+ // The resouce selection algorithm has been done.
+ // -> set the networkState to be NETWORK_EMPTY because there is no valid resource to load.
+ ok(element.networkState == HTMLMediaElement.NETWORK_EMPTY);
+
+ // Invoke load() again and since the networkState is NETWORK_EMPTY, the load() does not reject the pending promise.
+ // The load() will queue a task to run resouce selection algorithm which will change the readyState and finally resolve the pending promise.
+ element.src = test.name;
+
+ // Since the networkState is NETWORK_EMPTY, the load() does not set paused to be true.
+ ok(!element.paused, `loadAlgorithmKeepPromisesPendingWhenNotPausing(${token}).paused should be false.`);
+ });
+}
+
+manager.runTests(gSmallTests, initTest);
+
+</script> \ No newline at end of file
diff --git a/dom/media/test/test_play_promise_15.html b/dom/media/test/test_play_promise_15.html
new file mode 100644
index 0000000000..b8bce48651
--- /dev/null
+++ b/dom/media/test/test_play_promise_15.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: promise-based play() method</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script>
+// Name: loadAlgorithmRejectPromisesWhenPausing
+// Case: step1: create an element with its paused member to be fause and networkState to be NETWORK_NO_SOURCE.
+// stpe2: invoke load() on the element and the load() rejects the pending promise.
+// Expected result: reject the pending promise with AbortError DOM exception.
+
+let manager = new MediaTestManager;
+
+function initTest(test, token) {
+ manager.started(token);
+
+ let element = document.createElement(getMajorMimeType(test.type));
+ // Invoke play() -> (1) the promise will be left pending.
+ // (2) invoke load() -> (1) set the networkState to be NETWORK_NO_SOURCE.
+ // (2) queue a task to run resouce selection algorithm.
+ element.play().then(
+ (result) => {
+ ok(false, `${token} is resolved with ${result}.`);
+ },
+ (error) => {
+ if (error.name == "AbortError") {
+ ok(true, `${token} is rejected with ${error.name}.`);
+ } else {
+ ok(false, `${token} is rejected with ${error.name}.`);
+ }
+ }
+ ).then( () => { manager.finished(token); } );
+
+ ok(element.networkState == HTMLMediaElement.NETWORK_NO_SOURCE);
+
+ // Invoke load() again and since the networkState is NETWORK_NO_SOURCE, the load() rejects the pending promise.
+ element.src = test.name;
+
+ // Since the networkState is not NETWORK_EMPTY, the load() sets paused to be true.
+ ok(element.paused, `loadAlgorithmRejectPromisesWhenPausing(${token}).paused should be true.`);
+}
+
+manager.runTests(gSmallTests, initTest);
+
+</script> \ No newline at end of file
diff --git a/dom/media/test/test_play_promise_16.html b/dom/media/test/test_play_promise_16.html
new file mode 100644
index 0000000000..a643e7fff8
--- /dev/null
+++ b/dom/media/test/test_play_promise_16.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: promise-based play() method</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script>
+// Name: loadAlgorithmResolveOrdering
+// Case: invoke load() on an element should resolve pending promises in order.
+// Expected result: the pending promises are resolved in order.
+
+let manager = new MediaTestManager;
+
+function initTest(test, token) {
+ manager.started(token);
+
+ let element = document.createElement(getMajorMimeType(test.type));
+ element.preload = "auto";
+ element.src = test.name;
+ once(element, "canplay").then(() => {
+ let firstPromiseResolved = false;
+
+ // play
+ element.play().then(
+ () => { firstPromiseResolved = true; },
+ () => { ok(false, `loadAlgorithmResolveOrdering(${token}) should not be rejected.`); }
+ );
+
+ // play again
+ element.play().then(
+ () => { ok(firstPromiseResolved, `loadAlgorithmResolveOrdering(${token}), the first play should already be resolved.`); },
+ () => { ok(false, `loadAlgorithmResolveOrdering(${token}) should not be rejected.`); }
+ ).then( () => { manager.finished(token); } );
+
+ // triger load
+ element.src = test.name;
+ });
+}
+
+manager.runTests(gSmallTests, initTest);
+
+</script> \ No newline at end of file
diff --git a/dom/media/test/test_play_promise_17.html b/dom/media/test/test_play_promise_17.html
new file mode 100644
index 0000000000..5e5eb71fd5
--- /dev/null
+++ b/dom/media/test/test_play_promise_17.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: promise-based play() method</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script>
+// Name: loadAlgorithmRejectOrdering
+// Case: invoke load() on an element should reject pending promises in order.
+// Expected result: the pending promises are rejected in order.
+
+let manager = new MediaTestManager;
+
+function initTest(test, token) {
+ manager.started(token);
+
+ let element = document.createElement(getMajorMimeType(test.type));
+ let firstPromiseRejected = false;
+
+ // play
+ element.play().then(
+ () => { ok(false, `loadAlgorithmRejectOrdering(${token}) should not be resolved.`); },
+ () => { firstPromiseRejected = true; }
+ );
+
+ // play again
+ element.play().then(
+ () => { ok(false, `loadAlgorithmRejectOrdering(${token}) should not be resolved.`); },
+ () => { ok(firstPromiseRejected, `loadAlgorithmRejectOrdering(${token}), the first play should already be rejected.`); }
+ ).then( () => { manager.finished(token); } );
+
+ // triger load
+ element.src = test.name;
+}
+
+manager.runTests(gSmallTests, initTest);
+
+</script> \ No newline at end of file
diff --git a/dom/media/test/test_play_promise_18.html b/dom/media/test/test_play_promise_18.html
new file mode 100644
index 0000000000..e0c0837fdf
--- /dev/null
+++ b/dom/media/test/test_play_promise_18.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: promise-based play() method</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script>
+// Name: 'playing' event should come before promise resolving
+// Case: invoke play() on an element which has valis source
+// Expected result: receive a 'playing' event before the promise is resolved
+
+let manager = new MediaTestManager;
+
+function initTest(test, token) {
+ manager.started(token);
+
+ let element = document.createElement(getMajorMimeType(test.type));
+ element.src = test.name;
+ element.receivedPlayingEvent = false;
+
+ element.addEventListener("playing", () => {
+ element.receivedPlayingEvent = true;
+ });
+
+ element.play().then(
+ (result) => {
+ if (result == undefined && element.receivedPlayingEvent) {
+ ok(true, `${token} is resolved with ${result}.`);
+ } else {
+ ok(false, `${token} is resolved with ${result} and receivedPlayingEvent = ${element.receivedPlayingEvent}`);
+ }
+ },
+ (error) => {
+ ok(false, `${token} is rejected with ${error.name}.`);
+ }
+ ).then( () => { manager.finished(token); } );
+}
+
+manager.runTests(gSmallTests, initTest);
+
+</script> \ No newline at end of file
diff --git a/dom/media/test/test_play_promise_2.html b/dom/media/test/test_play_promise_2.html
new file mode 100644
index 0000000000..c4befdbe91
--- /dev/null
+++ b/dom/media/test/test_play_promise_2.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: promise-based play() method</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script>
+// Name: playWhenCanPlay
+// Case: invoke play() on an element that has enough data
+// Expected result: resolve the promise
+
+let manager = new MediaTestManager;
+
+function initTest(test, token) {
+ manager.started(token);
+
+ let element = document.createElement(getMajorMimeType(test.type));
+ element.preload = "auto";
+ element.src = test.name;
+ once(element, "canplay").then(() => {
+ element.play().then(
+ (result) => {
+ if (result == undefined) {
+ ok(true, `${token} is resolved with ${result}.`);
+ } else {
+ ok(false, `${token} is resolved with ${result}.`);
+ }
+ },
+ (error) => {
+ ok(false, `${token} is rejected with ${error.name}.`);
+ }
+ ).then( () => { manager.finished(token); } );
+ });
+}
+
+manager.runTests(gSmallTests, initTest);
+
+</script> \ No newline at end of file
diff --git a/dom/media/test/test_play_promise_3.html b/dom/media/test/test_play_promise_3.html
new file mode 100644
index 0000000000..8ffacdae03
--- /dev/null
+++ b/dom/media/test/test_play_promise_3.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: promise-based play() method</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script>
+// Name: playAfterPlaybackStarted
+// Case: invoke play() on an element that is already playing
+// Expected result: resolve the promise
+
+let manager = new MediaTestManager;
+
+function initTest(test, token) {
+ manager.started(token);
+
+ let element = document.createElement(getMajorMimeType(test.type));
+ element.preload = "auto";
+ element.src = test.name;
+ once(element, "playing").then(() => {
+ ok(element.readyState >= HTMLMediaElement.HAVE_FUTURE_DATA, `playAfterPlaybackStarted(${token})`);
+ ok(!element.paused, `playAfterPlaybackStarted(${token})`);
+ element.play().then(
+ (result) => {
+ if (result == undefined) {
+ ok(true, `${token} is resolved with ${result}.`);
+ } else {
+ ok(false, `${token} is resolved with ${result}.`);
+ }
+ },
+ (error) => {
+ ok(false, `${token} is rejected with ${error.name}.`);
+ }
+ ).then( () => { manager.finished(token); } );
+ });
+
+ element.play();
+}
+
+manager.runTests(gSmallTests, initTest);
+
+</script> \ No newline at end of file
diff --git a/dom/media/test/test_play_promise_4.html b/dom/media/test/test_play_promise_4.html
new file mode 100644
index 0000000000..dfcd96e0b9
--- /dev/null
+++ b/dom/media/test/test_play_promise_4.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: promise-based play() method</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="play_promise.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script>
+// Name: playNotSupportedContent
+// Case: invoke play() on an element with an unsupported content
+// Expected result: reject the promise with NotSupportedError DOM exception
+
+let manager = new MediaTestManager;
+
+function initTest(test, token) {
+ manager.started(token);
+
+ let element = document.createElement(getMajorMimeType(test.type));
+ element.src = getNotSupportedFile(test.name);
+ element.play().then(
+ (result) => {
+ ok(false, `${token} is resolved with ${result}.`);
+ },
+ (error) => {
+ if (error.name == "NotSupportedError") {
+ ok(true, `${token} is rejected with ${error.name}.`);
+ } else {
+ ok(false, `${token} is rejected with ${error.name}.`);
+ }
+ }
+ ).then( () => { manager.finished(token); } );
+}
+
+manager.runTests(gSmallTests, initTest);
+
+</script> \ No newline at end of file
diff --git a/dom/media/test/test_play_promise_5.html b/dom/media/test/test_play_promise_5.html
new file mode 100644
index 0000000000..a4c3eeb936
--- /dev/null
+++ b/dom/media/test/test_play_promise_5.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: promise-based play() method</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="play_promise.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script>
+// Name: playWithErrorAlreadySet
+// Case: invoke play() on an element with MEDIA_ERR_SRC_NOT_SUPPORTED has been set
+// Expected result: reject the promise with NotSupportedError DOM exception
+
+let manager = new MediaTestManager;
+
+function initTest(test, token) {
+ manager.started(token);
+
+ let element = document.createElement(getMajorMimeType(test.type));
+ element.src = getNotSupportedFile(test.name);
+ once(element, "error").then(() => {
+ ok(element.error.code == MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED);
+ element.play().then(
+ (result) => {
+ ok(false, `${token} is resolved with ${result}.`);
+ },
+ (error) => {
+ if (error.name == "NotSupportedError") {
+ ok(true, `${token} is rejected with ${error.name}.`);
+ } else {
+ ok(false, `${token} is rejected with ${error.name}.`);
+ }
+ }
+ ).then( () => { manager.finished(token); } );
+ });
+}
+
+manager.runTests(gSmallTests, initTest);
+
+</script> \ No newline at end of file
diff --git a/dom/media/test/test_play_promise_6.html b/dom/media/test/test_play_promise_6.html
new file mode 100644
index 0000000000..dcbea3f108
--- /dev/null
+++ b/dom/media/test/test_play_promise_6.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: promise-based play() method</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="play_promise.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script>
+// Name: playSwitchToValidSrcAfterError
+// Case: invoke play() on an element which had its source changed (to a valid source) after suffering from an error
+// Expected result: resolve the promise with undefined
+
+let manager = new MediaTestManager;
+
+function initTest(test, token) {
+ manager.started(token);
+
+ let element = document.createElement(getMajorMimeType(test.type));
+ element.src = getNotSupportedFile(test.name);
+ once(element, "error").then(() => {
+ ok(element.error.code == MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED);
+ element.src = test.name;
+ element.play().then(
+ (result) => {
+ if (result == undefined) {
+ ok(true, `${token} is resolved with ${result}.`);
+ } else {
+ ok(false, `${token} is resolved with ${result}.`);
+ }
+ },
+ (error) => {
+ ok(false, `${token} is rejected with ${error.name}.`);
+ }
+ ).then( () => { manager.finished(token); } );
+ });
+}
+
+manager.runTests(gSmallTests, initTest);
+
+</script> \ No newline at end of file
diff --git a/dom/media/test/test_play_promise_7.html b/dom/media/test/test_play_promise_7.html
new file mode 100644
index 0000000000..1abbe42f33
--- /dev/null
+++ b/dom/media/test/test_play_promise_7.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: promise-based play() method</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="play_promise.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script>
+// Name: playSwitchToInvalidSrcAfterError
+// Case: invoke play() on an element which had its source changed (to a invalid source) after suffering from an error
+// Expected result: reject the promise with NotSupportedError DOM exception
+
+let manager = new MediaTestManager;
+
+function initTest(test, token) {
+ manager.started(token);
+
+ let element = document.createElement(getMajorMimeType(test.type));
+ element.src = getNotSupportedFile(test.name);
+ once(element, "error").then(() => {
+ ok(element.error.code == MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED);
+ element.src = getNotSupportedFile(test.name);
+ ok(element.error == null);
+ ok(element.readyState == HTMLMediaElement.HAVE_NOTHING);
+ element.play().then(
+ (result) => {
+ ok(false, `${token} is resolved with ${result}.`);
+ },
+ (error) => {
+ if (error.name == "NotSupportedError") {
+ ok(true, `${token} is rejected with ${error.name}.`);
+ } else {
+ ok(false, `${token} is rejected with ${error.name}.`);
+ }
+ }
+ ).then( () => { manager.finished(token); } );
+ });
+}
+
+manager.runTests(gSmallTests, initTest);
+
+</script> \ No newline at end of file
diff --git a/dom/media/test/test_play_promise_8.html b/dom/media/test/test_play_promise_8.html
new file mode 100644
index 0000000000..d61e12c9fe
--- /dev/null
+++ b/dom/media/test/test_play_promise_8.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: promise-based play() method</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script>
+// Name: playAndPauseWhenCanplay
+// Case: invlke play() and then pause() on an element that already has enough data to play
+// Expected result: resolve the promise
+// Note: the pause() doesn't cancel the play() because it was alredy been queued to be resolved.
+
+let manager = new MediaTestManager;
+
+function initTest(test, token) {
+ manager.started(token);
+
+ let element = document.createElement(getMajorMimeType(test.type));
+ element.preload = "auto";
+ element.src = test.name;
+ once(element, "canplay").then(() => {
+ element.play().then(
+ (result) => {
+ if (result == undefined) {
+ ok(true, `${token} is resolved with ${result}.`);
+ } else {
+ ok(false, `${token} is resolved with ${result}.`);
+ }
+ },
+ (error) => {
+ ok(false, `${token} is rejected with ${error.name}.`);
+ }
+ ).then( () => { manager.finished(token); } );
+ ok(!element.paused, `playAndPauseWhenCanplay(${token}) element should not be paused.`);
+ element.pause();
+ ok(element.paused, `playAndPauseWhenCanplay(${token}) element should be paused.`);
+ });
+}
+
+manager.runTests(gSmallTests, initTest);
+
+</script> \ No newline at end of file
diff --git a/dom/media/test/test_play_promise_9.html b/dom/media/test/test_play_promise_9.html
new file mode 100644
index 0000000000..c3a3856515
--- /dev/null
+++ b/dom/media/test/test_play_promise_9.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: promise-based play() method</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script>
+// Name: playAndPauseBeforeCanPlay
+// Case: invlke play() and then pause() on an element that deoen't have enough data to play.
+// Expected result: reject the promise with AbortError DOM exception.
+// Note: the pause() cancels the play() because the promise is still pending.
+
+let manager = new MediaTestManager;
+
+function initTest(test, token) {
+ manager.started(token);
+
+ let element = document.createElement(getMajorMimeType(test.type));
+ ok(element.readyState == HTMLMediaElement.HAVE_NOTHING);
+ element.play().then(
+ (result) => {
+ ok(false, `${token} is resolved with ${result}.`);
+ },
+ (error) => {
+ if (error.name == "AbortError") {
+ ok(true, `${token} is rejected with ${error.name}.`);
+ } else {
+ ok(false, `${token} is rejected with ${error.name}.`);
+ }
+ }
+ ).then( () => { manager.finished(token); } );
+ ok(!element.paused, `playAndPauseBeforeCanPlay(${token}) element should not be paused.`);
+ element.pause();
+ ok(element.paused, `playAndPauseBeforeCanPlay(${token}) element should be paused.`);
+}
+
+manager.runTests(gSmallTests, initTest);
+
+</script> \ No newline at end of file
diff --git a/dom/media/test/test_play_twice.html b/dom/media/test/test_play_twice.html
new file mode 100644
index 0000000000..e94f28a031
--- /dev/null
+++ b/dom/media/test/test_play_twice.html
@@ -0,0 +1,95 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test playback of media files that should play OK</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+function startTest(test, token) {
+ var video = document.createElement('video');
+ video.token = token;
+ manager.started(token);
+ video.src = test.name;
+ video.name = test.name;
+ video.playingCount = 0;
+ video._playedOnce = false;
+
+ var check = function(t, v) { return function() {
+ checkMetadata(t.name, v, test);
+ }}(test, video);
+
+ var noLoad = function(t, v) { return function() {
+ ok(false, t.name + " should not fire 'load' event");
+ }}(test, video);
+
+ function finish(v) {
+ removeNodeAndSource(v);
+ manager.finished(v.token);
+ }
+
+ function mayFinish(v) {
+ if (v.seenEnded && v.seenSuspend) {
+ finish(v);
+ }
+ }
+
+ var checkEnded = function(t, v) { return function() {
+ if (t.duration) {
+ ok(Math.abs(v.currentTime - t.duration) < 0.1,
+ t.name + " current time at end: " + v.currentTime);
+ }
+
+ is(v.readyState, v.HAVE_CURRENT_DATA, t.name + " checking readyState");
+ ok(v.ended, t.name + " checking playback has ended");
+ ok(v.playingCount > 0, "Expect at least one playing event");
+ v.playingCount = 0;
+
+ if (v._playedOnce) {
+ v.seenEnded = true;
+ mayFinish(v);
+ } else {
+ v._playedOnce = true;
+ v.play();
+ }
+ }}(test, video);
+
+ var checkSuspended = function(t, v) { return function() {
+ if (v.seenSuspend) {
+ return;
+ }
+
+ v.seenSuspend = true;
+ ok(true, t.name + " got suspend");
+ mayFinish(v);
+ }}(test, video);
+
+ var checkPlaying = function(t, v) { return function() {
+ is(t.name, v.name, "Should be testing file we think we're testing...");
+ v.playingCount++;
+ }}(test, video);
+
+ video.addEventListener("load", noLoad);
+ video.addEventListener("loadedmetadata", check);
+ video.addEventListener("playing", checkPlaying);
+
+ // We should get "ended" and "suspend" events for every resource
+ video.addEventListener("ended", checkEnded);
+ video.addEventListener("suspend", checkSuspended);
+
+ document.body.appendChild(video);
+ video.play();
+}
+
+manager.runTests(gReplayTests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_playback.html b/dom/media/test/test_playback.html
new file mode 100644
index 0000000000..5e28861e93
--- /dev/null
+++ b/dom/media/test/test_playback.html
@@ -0,0 +1,108 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test playback of media files that should play OK</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+function startTest(test, token) {
+ var video = document.createElement('video');
+ video.preload = "metadata";
+ video.token = token;
+ video.prevTime = 0;
+ video.seenEnded = false;
+ video.seenSuspend = false;
+
+ var handler = {
+ "ontimeout": function() {
+ Log(token, "timed out: ended=" + video.seenEnded + ", suspend=" + video.seenSuspend);
+ }
+ };
+ manager.started(token, handler);
+
+ video.src = test.name;
+ video.name = test.name;
+
+ var check = function(t, v) { return function() {
+ is(t.name, v.name, t.name + ": Name should match #1");
+ checkMetadata(t.name, v, t);
+ }}(test, video);
+
+ var noLoad = function(t, v) { return function() {
+ ok(false, t.name + " should not fire 'load' event");
+ }}(test, video);
+
+ var noError = function(t, v) { return function() {
+ ok(false, t.name + " should not fire 'error' event " + v.error.message);
+ }}(test, video);
+
+ var finish = function() {
+ video.finished = true;
+ video.removeEventListener("timeupdate", timeUpdate);
+ removeNodeAndSource(video);
+ manager.finished(video.token);
+ }
+
+ // We should get "ended" and "suspend" events to finish the test.
+ var mayFinish = function() {
+ if (video.seenEnded && video.seenSuspend) {
+ finish();
+ }
+ }
+
+ var checkEnded = function(t, v) { return function() {
+ is(t.name, v.name, t.name + ": Name should match #2");
+ checkMetadata(t.name, v, test);
+ is(v.readyState, v.HAVE_CURRENT_DATA, t.name + " checking readyState");
+ ok(v.ended, t.name + " checking playback has ended");
+ ok(!v.finished, t.name + " shouldn't be finished");
+ ok(!v.seenEnded, t.name + " shouldn't be ended");
+
+ v.seenEnded = true;
+ mayFinish();
+ }}(test, video);
+
+ var checkSuspended = function(t, v) { return function() {
+ if (v.seenSuspend) {
+ return;
+ }
+ is(t.name, v.name, t.name + ": Name should match #3");
+
+ v.seenSuspend = true;
+ mayFinish();
+ }}(test, video);
+
+ var timeUpdate = function(t, v) { return function() {
+ if (v.prevTime > v.currentTime) {
+ ok(false, t.name + " time should run forwards: p=" +
+ v.prevTime + " c=" + v.currentTime);
+ }
+ v.prevTime = v.currentTime;
+ }}(test, video);
+
+ video.addEventListener("load", noLoad);
+ video.addEventListener("error", noError);
+ video.addEventListener("loadedmetadata", check);
+ video.addEventListener("timeupdate", timeUpdate);
+
+ // We should get "ended" and "suspend" events for every resource
+ video.addEventListener("ended", checkEnded);
+ video.addEventListener("suspend", checkSuspended);
+
+ document.body.appendChild(video);
+ video.play();
+}
+
+manager.runTests(gPlayTests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_playback_and_bfcache.html b/dom/media/test/test_playback_and_bfcache.html
new file mode 100644
index 0000000000..3b1236c530
--- /dev/null
+++ b/dom/media/test/test_playback_and_bfcache.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test media playback and bfcache</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+ <script>
+ SimpleTest.requestFlakyTimeout("Need some timer to wait for the audio to play");
+ SimpleTest.waitForExplicitFinish();
+ var duration = 0;
+
+ // The test opens a page and another page with a media element is loaded.
+ // The media element plays an audio file and starts again and sends
+ // statistics about it and then history.back() is called. The test waits
+ // for 1s + duration of the audio file and goes forward. The audio playback
+ // shouldn't have progressed while the page was in the bfcache.
+ function test() {
+ let bc1 = new BroadcastChannel("bc1");
+ let pageshow1Count = 0;
+ bc1.onmessage = function(e) {
+ if (e.data == "pageshow") {
+ ++pageshow1Count;
+ info("Page 1 pageshow " + pageshow1Count);
+ if (pageshow1Count == 1) {
+ bc1.postMessage("loadNext");
+ } else if (pageshow1Count == 2) {
+ setTimeout(function() {
+ bc1.postMessage("forward");
+ bc1.close();
+ }, (Math.round(duration) + 1) * 1000);
+ }
+ }
+ };
+
+ let bc2 = new BroadcastChannel("bc2");
+ let pageshow2Count = 0;
+ let statisticsCount = 0;
+ bc2.onmessage = function(e) {
+ if (e.data.event == "pageshow") {
+ ++pageshow2Count;
+ info("Page 2 pageshow " + pageshow2Count);
+ if (pageshow2Count == 2) {
+ ok(e.data.persisted, "Should have persisted the page.");
+ bc2.postMessage("statistics");
+ }
+ } else {
+ ++statisticsCount;
+ if (statisticsCount == 1) {
+ duration = e.data.duration;
+ bc2.postMessage("back");
+ } else {
+ is(statisticsCount, 2, "Should got two play events.");
+ ok(e.data.currentTime < e.data.duration,
+ "Should have stopped the playback while the page was in bfcache." +
+ "currentTime: " + e.data.currentTime + " duration: " + e.data.duration);
+ bc2.close();
+ SimpleTest.finish();
+ }
+ }
+ };
+
+ window.open("file_playback_and_bfcache.html", "", "noopener");
+ }
+ </script>
+</head>
+<body onload="test()">
+<p id="display"></p>
+<div id="content" style="display: none"></div>
+<pre id="test"></pre>
+</body>
+</html>
diff --git a/dom/media/test/test_playback_errors.html b/dom/media/test/test_playback_errors.html
new file mode 100644
index 0000000000..7b3f046099
--- /dev/null
+++ b/dom/media/test/test_playback_errors.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test playback of media files that should have errors</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+function startTest(test, token) {
+ var video = document.createElement('video');
+ manager.started(token);
+ video._errorCount = 0;
+ video._ignore = false;
+ function endedTest(v) {
+ if (v._ignore)
+ return;
+ v._ignore = true;
+ v.remove();
+ manager.finished(token);
+ }
+ var checkError = function(t, v) { return function(evt) {
+ v._errorCount++;
+ is(v._errorCount, 1, t.name + " only one error fired");
+ endedTest(v);
+ }}(test, video);
+ var checkEnded = function(t, v) { return function() {
+ ok(false, t.name + " successfully played");
+ endedTest(v);
+ }}(test, video);
+ video.addEventListener("error", checkError);
+ video.addEventListener("ended", checkEnded);
+ video.src = test.name;
+ document.body.appendChild(video);
+ video.play();
+}
+
+manager.runTests(gErrorTests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_playback_hls.html b/dom/media/test/test_playback_hls.html
new file mode 100644
index 0000000000..7d5c777fc1
--- /dev/null
+++ b/dom/media/test/test_playback_hls.html
@@ -0,0 +1,91 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test playback of HLS with simple m3u8 that should play OK</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+function startTest(test, token) {
+ var video = document.createElement('video');
+ video.preload = "metadata";
+ video.token = token;
+ video.prevTime = 0;
+ video.seenEnded = false;
+
+ var handler = {
+ "ontimeout": function() {
+ Log(token, "timed out: ended=" + video.seenEnded);
+ }
+ };
+ manager.started(token, handler);
+
+ video.src = test.name;
+ video.name = test.name;
+
+ var check = function(t, v) { return function() {
+ is(t.name, v.name, t.name + ": Name should match #1");
+ checkMetadata(t.name, v, t);
+ }}(test, video);
+
+ var noLoad = function(t, v) { return function() {
+ ok(false, t.name + " should not fire 'load' event");
+ }}(test, video);
+
+ var finish = function() {
+ video.finished = true;
+ video.removeEventListener("timeupdate", timeUpdate);
+ removeNodeAndSource(video);
+ manager.finished(video.token);
+ }
+
+ // We should get "ended" events to finish the test.
+ var mayFinish = function() {
+ if (video.seenEnded) {
+ finish();
+ }
+ }
+
+ var checkEnded = function(t, v) { return function() {
+ is(t.name, v.name, t.name + ": Name should match #2");
+ checkMetadata(t.name, v, test);
+ is(v.readyState, v.HAVE_CURRENT_DATA, t.name + " checking readyState");
+ ok(v.ended, t.name + " checking playback has ended");
+ ok(!v.finished, t.name + " shouldn't be finished");
+ ok(!v.seenEnded, t.name + " shouldn't be ended");
+
+ v.seenEnded = true;
+ mayFinish();
+ }}(test, video);
+
+ var timeUpdate = function(t, v) { return function() {
+ if (v.prevTime > v.currentTime) {
+ ok(false, t.name + " time should run forwards: p=" +
+ v.prevTime + " c=" + v.currentTime);
+ }
+ v.prevTime = v.currentTime;
+ }}(test, video);
+
+ video.addEventListener("load", noLoad);
+ video.addEventListener("loadedmetadata", check);
+ video.addEventListener("timeupdate", timeUpdate);
+
+ // We should get "ended" events for the hls resource
+ video.addEventListener("ended", checkEnded);
+
+ document.body.appendChild(video);
+ video.play();
+}
+
+manager.runTests(gHLSTests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_playback_rate.html b/dom/media/test/test_playback_rate.html
new file mode 100644
index 0000000000..b44ebe27ee
--- /dev/null
+++ b/dom/media/test/test_playback_rate.html
@@ -0,0 +1,175 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for the playbackRate property </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type='application/javascript'>
+
+let manager = new MediaTestManager;
+
+function rangeCheck(lhs, rhs, threshold) {
+ var diff = Math.abs(lhs - rhs);
+ if (diff < threshold) {
+ return true;
+ }
+ return false;
+}
+
+function checkPlaybackRate(wallclock, media, expected, threshold) {
+ if (rangeCheck(media / wallclock, expected, threshold)) {
+ return true;
+ }
+ return false;
+}
+
+// Those value are expected to match those at the top of HTMLMediaElement.cpp.
+let VERY_SLOW_RATE = 1 / 32,
+ SLOW_RATE = 1 / 16,
+ FAST_RATE = 16,
+ VERY_FAST_RATE = 20,
+ NULL_RATE = 0.0;
+
+function ontimeupdate(e) {
+ var t = e.target;
+ // Skip short files for SoundTouch doesn't work well on small number of samples.
+ if (t.gotEnded || t.duration < 2) {
+ return;
+ }
+ t.testedForSlowdown = true;
+ if (t.currentTime > t.duration / 2) {
+ t.oldCurrentTime = t.currentTime;
+ t.timestamp = Date.now();
+ var delta = t.oldCurrentTime,
+ delta_wallclock = (t.timestamp - t.startTimestamp - t.bufferingTime) / 1000;
+
+ t.preservesPitch = false;
+ is(t.preservesPitch, false, t.name + ": If we disable the pitch preservation, it should appear as such.");
+
+ t.bufferingTime = 0;
+
+ is(t.playbackRate, SLOW_RATE, t.name + ": The playback rate shoud be "+SLOW_RATE+".");
+ ok(checkPlaybackRate(delta_wallclock, delta, SLOW_RATE, 0.25), t.name + ": We are effectively slowing down playback. (" + delta_wallclock + ", " + delta + ")");
+ t.removeEventListener("timeupdate", ontimeupdate);
+ t.addEventListener("pause", onpaused);
+ t.playbackRate = NULL_RATE;
+ t.oldCurrentTime = t.currentTime;
+ setTimeout(function() {
+ afterNullPlaybackRate(e);
+ }, 100);
+ }
+}
+
+function onpaused(e) {
+ var t = e.target;
+ t.pausedReceived = true;
+}
+
+function afterNullPlaybackRate(e) {
+ var t = e.target;
+
+ // skip if we have received 'ended' event or 'ended' event is pending.
+ if (t.gotEnded || t.ended) {
+ return;
+ }
+
+ t.testedForNull = true;
+
+ ok(t.currentTime == t.oldCurrentTime, t.name + ": Current time should not change when playbackRate is null (" + t.currentTime + " " + t.oldCurrentTime + ").");
+ ok(!t.paused, t.name + ": The element should not be in paused state.");
+ t.removeEventListener("paused", onpaused);
+ is(t.pausedReceived, undefined, t.name + ": Paused event should not have been received.");
+ t.timestamp = Date.now();
+ t.oldCurrentTime = t.currentTime;
+ t.playbackRate = VERY_FAST_RATE;
+ is(t.playbackRate, VERY_FAST_RATE, t.name + ": Playback rate should be clamped to " + VERY_FAST_RATE + ".");
+}
+
+function onended(e) {
+ var t = e.target;
+ t.gotEnded = true;
+
+ t.bufferingTime = 0;
+ // If we got "ended" too early, skip these tests.
+ if (t.testedForSlowdown && t.testedForNull) {
+ is(t.playbackRate, FAST_RATE, t.name + ": The playback rate should still be "+FAST_RATE+".");
+ ok(!t.muted, t.name + ": The audio should be muted when playing at high speed, but should not appear as such.");
+ is(t.currentTime, t.duration, t.name + ": Current time should be equal to the duration (not change by playback rate).");
+ }
+ finish_test(t);
+}
+
+function onratechange(e) {
+ if (!e.target.ratechangecount) {
+ e.target.ratechangecount = 0;
+ }
+ e.target.ratechangecount++;
+}
+
+function finish_test(element) {
+ removeNodeAndSource(element);
+ manager.finished(element.token);
+}
+
+// These two functions handle the case when the playback pauses for buffering. It
+// adjusts the timestamps to be accurate. Despite the fact that the web servers
+// is supposed to be on the same machine, buffering pauses can occur (rarely,
+// but still).
+function onplaying(e) {
+ var t = e.target;
+ if (t.bufferingTimestamp != undefined) {
+ t.bufferingTime += (Date.now() - t.bufferingTimestamp);
+ t.bufferingTimestamp = undefined;
+ }
+}
+
+function onwaiting(e) {
+ var t = e.target;
+ t.bufferingTimestamp = Date.now();
+}
+
+function onvolumechange(e) {
+ ok(false, e.target.name + ": We should not receive a volumechange event when changing the playback rate.");
+}
+
+function startTest(test, token) {
+ let elemType = /^audio/.test(test.type) ? "audio" : "video";
+ let element = document.createElement(elemType);
+ element.src = test.name;
+ element.name = test.name;
+ element.preload = "metadata";
+ element.token = token;
+ element.controls = true;
+ element.bufferingTime = 0;
+ document.body.appendChild(element);
+ element.addEventListener("ratechange", onratechange);
+ element.addEventListener("timeupdate", ontimeupdate);
+ element.addEventListener("ended", onended);
+ element.addEventListener("waiting", onwaiting);
+ element.addEventListener("playing", onplaying);
+ element.addEventListener("volumechange", onvolumechange);
+ manager.started(token);
+ element.startTimestamp = Date.now();
+ is(element.preservesPitch, true, test.name + ": Pitch preservation should be enabled by default.");
+ element.addEventListener("loadedmetadata", function() {
+ is(element.playbackRate, 1.0, test.name + ": playbackRate should be initially 1.0");
+ is(element.defaultPlaybackRate, 1.0, test.name + ": defaultPlaybackRate should be initially 1.0");
+ element.playbackRate = VERY_SLOW_RATE;
+ is(element.playbackRate, VERY_SLOW_RATE, test.name + ": PlaybackRate should be " + VERY_SLOW_RATE + ".");
+ element.play();
+ element.playbackRate = SLOW_RATE;
+ });
+}
+
+manager.runTests(gPlayedTests, startTest);
+
+</script>
+</pre>
+<div id="elements">
+</div>
+</body>
+</html>
diff --git a/dom/media/test/test_playback_rate_playpause.html b/dom/media/test/test_playback_rate_playpause.html
new file mode 100644
index 0000000000..fb57e50942
--- /dev/null
+++ b/dom/media/test/test_playback_rate_playpause.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test that the playbackRate property is not reset when resuming the playback</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type='application/javascript'>
+
+let manager = new MediaTestManager;
+
+function ontimeupdate(e) {
+ var t = e.target;
+ if (t.currentTime != 0.0) {
+ info(t.token + " t.currentTime=" + t.currentTime +"\n");
+ t.removeEventListener("timeupdate", ontimeupdate);
+ t.pause();
+ is(t.playbackRate, 0.5, "PlaybackRate should not have changed after pause.");
+ } else {
+ info(t.token + " t.currentTime == 0.0.\n");
+ }
+}
+
+function onpaused(e) {
+ var t = e.target;
+ dump(t.token + " onpaused.\n");
+ t.play();
+ is(t.playbackRate, 0.5, "PlaybackRate should not have changed after resuming playback.");
+ finish_test(t);
+}
+
+function finish_test(element) {
+ dump(element.token + " finish_test.\n");
+ removeNodeAndSource(element);
+ manager.finished(element.token);
+}
+
+function startTest(test, token) {
+ let elemType = /^audio/.test(test.type) ? "audio" : "video";
+ let element = document.createElement(elemType);
+ element.src = test.name;
+ element.token = token;
+ element.controls = true;
+ element.playbackRate = 0.5;
+ element.preload = "metadata";
+ element.addEventListener("timeupdate", ontimeupdate);
+ element.addEventListener("pause", onpaused);
+ element.addEventListener("loadedmetadata", function() {
+ dump(element.token + " loadedmetadata\n");
+ element.play();
+ });
+ document.body.appendChild(element);
+ manager.started(token);
+}
+
+manager.runTests(gPlayedTests, startTest);
+
+</script>
+</pre>
+<div id="elements">
+</div>
+</body>
+</html>
diff --git a/dom/media/test/test_playback_reactivate.html b/dom/media/test/test_playback_reactivate.html
new file mode 100644
index 0000000000..dec5330ce5
--- /dev/null
+++ b/dom/media/test/test_playback_reactivate.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test playback with dormant of media files that should play OK</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/* This testcase wants to test a video element's playback is not break
+ by dormant.
+ When the metadata is loaded, we remove the video element to trigger dormant.
+ Then set a timer to append the video element back and play it.
+ Test pass if the video plays to the end.
+*/
+
+var manager = new MediaTestManager;
+
+function startTest(test, token) {
+ var video = document.createElement('video');
+ video.preload = "metadata";
+ video.token = token;
+
+ var handler = {
+ "ontimeout": function() {
+ Log(token, "timed out: ended=" + video.seenEnded + ", suspend=" + video.seenSuspend);
+ }
+ };
+ manager.started(token, handler);
+
+ video.src = test.name;
+ video.name = test.name;
+
+ var check = function(t, v) { return function() {
+ is(t.name, v.name, t.name + ": Name should match #1");
+ Log(v.token, "removeChild: " + v.name);
+ document.body.removeChild(v);
+ var appendAndPlayElement = function() {
+ Log(v.token, "appendChild: " + v.name);
+ document.body.appendChild(v);
+ Log(v.token, "Element play: " + v.name);
+ v.play();
+ }
+ setTimeout(appendAndPlayElement, 2000);
+ }}(test, video);
+
+ var finish = function() {
+ video.finished = true;
+ removeNodeAndSource(video);
+ manager.finished(video.token);
+ }
+
+ var checkEnded = function(t, v) { return function() {
+ is(t.name, v.name, t.name + ": Name should match #2");
+ checkMetadata(t.name, v, t);
+ is(v.readyState, v.HAVE_CURRENT_DATA, t.name + " checking readyState");
+ ok(v.ended, t.name + " checking playback has ended");
+
+ finish();
+ }}(test, video);
+
+
+ video.addEventListener("loadedmetadata", check);
+ video.addEventListener("ended", checkEnded);
+
+ document.body.appendChild(video);
+}
+
+manager.runTests(gSmallTests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_played.html b/dom/media/test/test_played.html
new file mode 100644
index 0000000000..f6c05f7070
--- /dev/null
+++ b/dom/media/test/test_played.html
@@ -0,0 +1,288 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test played member for media elements</title>
+<script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+<script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id='test'>
+<script class="testbody" type='application/javascript'>
+
+let manager = new MediaTestManager;
+
+function finish_test(element) {
+ removeNodeAndSource(element);
+ manager.finished(element.token);
+}
+
+// Check that a file has been played in its entirety.
+function check_full_file_played(element) {
+ element.addEventListener('ended', (function(e) {
+ let interval_count = e.target.played.length;
+ is(interval_count, 1, element.token + ": played.length must be 1");
+ is(element.played.start(0), 0, element.token + ": start time shall be 0");
+ is(element.played.end(0), e.target.duration, element.token + ": end time shall be duration");
+ finish_test(e.target);
+ }));
+}
+
+var tests = [
+// Without playing, check that player.played.length == 0.
+{
+ setup(element) {
+ element.addEventListener("loadedmetadata", function() {
+ is(element.played.length, 0, element.token + ": initial played.length equals zero");
+ finish_test(element);
+ });
+ },
+ name: "test1"
+},
+// Play the file, test the range we have.
+{
+ setup(element) {
+ check_full_file_played(element);
+ element.play();
+ },
+ name: "test2"
+},
+
+// Play the second half of the file, pause, play
+// an check we have only one range.
+{
+ setup (element) {
+ element.onended = function (e) {
+ var t = e.target;
+ t.onended = null;
+ check_full_file_played(t);
+ t.pause();
+ t.currentTime = 0;
+ t.play();
+ };
+ element.addEventListener("loadedmetadata", function() {
+ element.currentTime = element.duration / 2;
+ element.play();
+ });
+ },
+ name: "test3"
+},
+
+// Play the first half of the file, seek back, while
+// continuing to play. We shall have only one range.
+{
+ setup (element) {
+ let onTimeUpdate = function() {
+ if (element.currentTime > element.duration / 2) {
+ info(element.token + ": currentTime=" + element.currentTime + ", duration=" + element.duration);
+ element.removeEventListener("timeupdate", onTimeUpdate);
+ element.pause();
+ var oldEndRange = element.played.end(0);
+ element.currentTime = element.duration / 4;
+ is(element.played.end(0), oldEndRange,
+ element.token + ": When seeking back, |played| should not be changed");
+ element.play();
+ }
+ }
+ element.addEventListener("timeupdate", onTimeUpdate);
+ check_full_file_played(element);
+ element.play();
+ },
+ name: "test4"
+},
+
+// Play and seek to have two ranges, and check that, as well a
+// boundaries.
+{
+ setup (element) {
+ let seekTarget = 0;
+ let onTimeUpdate = function() {
+ if (element.currentTime > element.duration / 2) {
+ info(element.token + ": currentTime=" + element.currentTime + ", duration=" + element.duration);
+ element.removeEventListener("timeupdate", onTimeUpdate);
+ element.pause();
+ // Remember seek target for later comparison since duration may change
+ // during playback.
+ seekTarget = element.currentTime = element.duration / 10;
+ element.currentTime = seekTarget;
+ element.play();
+ }
+ }
+
+ element.addEventListener("loadedmetadata", function() {
+ element.addEventListener("timeupdate", onTimeUpdate);
+ });
+
+
+ element.addEventListener("ended", (function() {
+ if(element.played.length > 1) {
+ is(element.played.length, 2, element.token + ": element.played.length == 2");
+ is(element.played.start(1), seekTarget, element.token + ": we should have seeked forward by one tenth of the duration");
+ is(element.played.end(1), element.duration, element.token + ": end of second range shall be the total duration");
+ }
+ is(element.played.start(0), 0, element.token + ": start of first range shall be 0");
+ finish_test(element);
+ }));
+
+ element.play();
+ },
+ name: "test5"
+},
+
+// Play to create two ranges, in the reverse order. check that they are sorted.
+{
+ setup (element) {
+ function end() {
+ element.pause();
+ let p = element.played;
+ ok(p.length >= 1, element.token + ": There should be at least one range=" + p.length);
+ is(p.start(0), seekTarget, element.token + ": Start of first range should be the sixth of the duration");
+ ok(p.end(p.length - 1) > 5 * element.duration / 6, element.token + ": End of last range should be greater that five times the sixth of the duration");
+ finish_test(element);
+ }
+
+ let seekTarget = 0;
+ function pauseseekrestart() {
+ element.pause();
+ // Remember seek target for later comparison since duration may change
+ // during playback.
+ seekTarget = element.duration / 6;
+ element.currentTime = seekTarget;
+ element.play();
+ }
+
+ function onTimeUpdate_pauseseekrestart() {
+ if (element.currentTime > 5 * element.duration / 6) {
+ element.removeEventListener("timeupdate", onTimeUpdate_pauseseekrestart);
+ pauseseekrestart();
+ element.addEventListener("timeupdate", onTimeUpdate_end);
+ }
+ }
+
+ function onTimeUpdate_end() {
+ if (element.currentTime > 3 * element.duration / 6) {
+ element.removeEventListener("timeupdate", onTimeUpdate_end);
+ end();
+ }
+ }
+
+ element.addEventListener("timeupdate", onTimeUpdate_pauseseekrestart);
+
+ element.addEventListener('loadedmetadata', function() {
+ element.currentTime = 4 * element.duration / 6;
+ element.play();
+ });
+ },
+ name: "test6"
+},
+// Seek repeatedly without playing. No range should appear.
+{
+ setup(element) {
+ let index = 1;
+
+ element.addEventListener('seeked', function() {
+ index++;
+ element.currentTime = index * element.duration / 5;
+ is(element.played.length, 0, element.token + ": played.length should be 0");
+ if (index == 5) {
+ finish_test(element);
+ }
+ });
+
+ element.addEventListener('loadedmetadata', function() {
+ element.currentTime = element.duration / 5;
+ });
+ },
+ name: "test7"
+},
+// Play for a bit, then seek to the end: two ranges when "ended" is received.
+// Load another resource (invalid): no range
+// Load another resource (valid): still no range
+{
+ setup(element) {
+ let index = 1;
+ let seeked = false;
+
+ element.addEventListener('timeupdate', function() {
+ index++;
+ if (index == 2) {
+ is(element.played.length, 1);
+ element.currentTime = element.currentTime + element.duration / 100;
+ }
+ if (seeked) {
+ is(element.played.length, 2, "After seeking, played.length should be 2");
+ }
+ });
+ element.addEventListener('seeked', function() {
+ seeked = true;
+ });
+ element.addEventListener('ended', function() {
+ // For very short media file, timing is unreliable, it can be that the
+ // file has finished playing, before the seek, and so there is only one
+ // range in `played`.
+ if (seeked) {
+ is(element.played.length, 2, element.token + "After ended, played.length should be 2");
+ } else {
+ is(element.played.length, 1, element.token + "After ended, a short media file played.length should be 1 if it has ended before the seek");
+ }
+ let src_bck = element.src;
+ element.src = "bogus.duh";
+ is(element.played.length, 0, "After subsequent invalid load, played.length should be 0");
+ element.src = src_bck;
+ is(element.played.length, 0, "After subsequent valid load, played.length should be 0");
+ finish_test(element);
+ });
+ element.addEventListener('loadedmetadata', function() {
+ element.play();
+ });
+ },
+ name: "test8"
+}
+];
+
+function createTestArray() {
+ var A = [];
+ for (var i=0; i<tests.length; i++) {
+ for (var k=0; k<gPlayedTests.length; k++) {
+ var t = {};
+ t.setup = tests[i].setup;
+ t.name = tests[i].name + "-" + gPlayedTests[k].name;
+ t.type = gPlayedTests[k].type;
+ t.src = gPlayedTests[k].name;
+ A.push(t);
+ }
+ }
+ return A;
+}
+
+function startTest(test, token) {
+ var elemType = getMajorMimeType(test.type);
+ var element = document.createElement(elemType);
+ element.src = test.src;
+ element.token = token;
+ element.preload = "metadata";
+ test.setup(element);
+ manager.started(token);
+
+ // Log events for debugging.
+ var events = ["suspend", "play", "canplay", "canplaythrough", "loadstart", "loadedmetadata",
+ "loadeddata", "playing", "ended", "error", "stalled", "emptied", "abort",
+ "waiting", "pause"];
+ function logEvent(e) {
+ var v = e.target;
+ info(v.token + ": got " + e.type);
+ }
+ events.forEach(function(e) {
+ element.addEventListener(e, logEvent);
+ });
+
+}
+
+
+manager.runTests(createTestArray(), startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_preload_actions.html b/dom/media/test/test_preload_actions.html
new file mode 100644
index 0000000000..a9de54988c
--- /dev/null
+++ b/dom/media/test/test_preload_actions.html
@@ -0,0 +1,582 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=548523
+-->
+<head>
+ <title>Test for Bug 548523</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=548523">Mozilla Bug 548523</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<!-- <button onClick="SimpleTest.finish();">Finish</button> -->
+<div>Tests complete: <span id="log" style="font-size: small;"></span></div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 548523 **/
+
+SimpleTest.requestCompleteLog();
+var manager = new MediaTestManager;
+
+manager.onFinished = function() {
+ is(gotLoadEvent, true, "Should not have delayed the load event indefinitely");
+};
+
+var test = getPlayableVideo(gSeekTests);
+var baseName = test.name;
+var gTest = test;
+var bogusSrc = "bogus.duh";
+var bogusType = "video/bogus";
+var gotLoadEvent = false;
+var finished = false;
+
+addLoadEvent(function() {gotLoadEvent=true;});
+
+function log(m) {
+ var l = document.getElementById("log");
+ // eslint-disable-next-line no-unsanitized/property
+ l.innerHTML += m;
+}
+
+function maybeFinish(v, n) {
+ if (v._finished) {
+ return;
+ }
+ v._finished = true;
+ log(n + ",");
+ removeNodeAndSource(v);
+ manager.finished(v.token);
+}
+
+function filename(uri) {
+ return uri.substr(uri.lastIndexOf("/")+1);
+}
+
+// Every test must have a setup(v) function, and must call maybeFinish() when test is complete.
+var tests = [
+ {
+ // 1. Add preload:none video with src to document. Load should halt at NETWORK_IDLE and HAVE_NOTHING,
+ // after receiving a suspend event. Should not receive loaded events until after we call load().
+ // Note the suspend event is explictly sent by our "stop the load" code, but other tests can't rely
+ // on it for the preload:metadata case, as there can be multiple suspend events when loading metadata.
+ suspend(e) {
+ var v = e.target;
+ is(v._gotLoadStart, true, "(1) Must get loadstart.");
+ is(v._gotLoadedMetaData, false, "(1) Must not get loadedmetadata.");
+ is(v.readyState, v.HAVE_NOTHING, "(1) ReadyState must be HAVE_NOTHING");
+ // bug 962949
+ // is(v.networkState, v.NETWORK_IDLE, "(1) NetworkState must be NETWORK_IDLE");
+ maybeFinish(v, 1);
+ },
+
+ setup(v) {
+ v._gotLoadStart = false;
+ v._gotLoadedMetaData = false;
+ v.preload = "none";
+ v.addEventListener("loadedmetadata", function(e){v._gotLoadedMetaData = true;});
+ v.addEventListener("loadstart", function(e){v._gotLoadStart = true;});
+ v.addEventListener("suspend", this.suspend);
+ v.src = test.name;
+ document.body.appendChild(v); // Causes implicit load, which will be halted due to preload:none.
+ },
+
+ name: "test1",
+ },
+ {
+ // 2. Add preload:metadata video with src to document. Should halt with NETWORK_IDLE, HAVE_CURRENT_DATA
+ // after suspend event and after loadedmetadata.
+ loadeddata(e) {
+ var v = e.target;
+ is(v._gotLoadStart, true, "(2) Must get loadstart.");
+ is(v._gotLoadedMetaData, true, "(2) Must get loadedmetadata.");
+ ok(v.readyState >= v.HAVE_CURRENT_DATA, "(2) ReadyState must be >= HAVE_CURRENT_DATA");
+ // bug 962949
+ // is(v.networkState, v.NETWORK_IDLE, "(2) NetworkState must be NETWORK_IDLE");
+ maybeFinish(v, 2);
+ },
+
+ setup(v) {
+ v._gotLoadStart = false;
+ v._gotLoadedMetaData = false;
+ v.preload = "metadata";
+ v.addEventListener("loadstart", function(e){v._gotLoadStart = true;});
+ v.addEventListener("loadedmetadata", function(e){v._gotLoadedMetaData = true;});
+ v.addEventListener("loadeddata", this.loadeddata);
+ v.src = test.name;
+ document.body.appendChild(v); // Causes implicit load, which will be halted after
+ // metadata due to preload:metadata.
+ },
+
+ name: "test2",
+ },
+ {
+ // 3. Add preload:auto to document. Should receive canplaythrough eventually.
+ canplaythrough(e) {
+ var v = e.target;
+ is(v._gotLoadStart, true, "(3) Must get loadstart.");
+ is(v._gotLoadedMetaData, true, "(3) Must get loadedmetadata.");
+ maybeFinish(v, 3);
+ },
+
+ setup(v) {
+ v._gotLoadStart = false;
+ v._gotLoadedMetaData = false;
+ v.preload = "auto";
+ v.addEventListener("loadstart", function(e){v._gotLoadStart = true;});
+ v.addEventListener("loadedmetadata", function(e){v._gotLoadedMetaData = true;});
+ v.addEventListener("canplaythrough", this.canplaythrough);
+ v.src = test.name; // Causes implicit load.
+ document.body.appendChild(v);
+ },
+
+ name: "test3",
+ },
+ {
+ // 4. Add preload:none video to document. Call play(), should load then play through.
+ suspend(e) {
+ var v = e.target;
+ if (v._gotSuspend) {
+ return; // We can receive multiple suspend events, like the one after download completes.
+ }
+ v._gotSuspend = true;
+ is(v._gotLoadStart, true, "(4) Must get loadstart.");
+ is(v._gotLoadedMetaData, false, "(4) Must not get loadedmetadata.");
+ is(v.readyState, v.HAVE_NOTHING, "(4) ReadyState must be HAVE_NOTHING");
+ // bug 962949
+ // is(v.networkState, v.NETWORK_IDLE, "(4) NetworkState must be NETWORK_IDLE");
+ v.play(); // Should load and play through.
+ },
+
+ ended(e) {
+ ok(true, "(4) Got playback ended");
+ maybeFinish(e.target, 4);
+ },
+
+ setup(v) {
+ v._gotLoadStart = false;
+ v._gotLoadedMetaData = false;
+ v._gotSuspend = false;
+ v.preload = "none";
+ v.addEventListener("loadedmetadata", function(e){v._gotLoadedMetaData = true;});
+ v.addEventListener("loadstart", function(e){v._gotLoadStart = true;});
+ v.addEventListener("suspend", this.suspend);
+ v.addEventListener("ended", this.ended);
+ v.src = test.name;
+ document.body.appendChild(v);
+ },
+
+ name: "test4",
+ },
+ {
+ // 5. preload:none video without resource, add to document, will implicitly start a
+ // preload:none load. Add a src, it shouldn't load.
+ suspend(e) {
+ var v = e.target;
+ is(v._gotLoadStart, true, "(5) Must get loadstart.");
+ is(v._gotLoadedMetaData, false, "(5) Must not get loadedmetadata.");
+ is(v.readyState, v.HAVE_NOTHING, "(5) ReadyState must be HAVE_NOTHING");
+ // bug 962949
+ // is(v.networkState, v.NETWORK_IDLE, "(5) NetworkState must be NETWORK_IDLE");
+ maybeFinish(v, 5);
+ },
+
+ setup(v) {
+ v._gotLoadStart = false;
+ v._gotLoadedMetaData = false;
+ v.preload = "none";
+ v.addEventListener("loadedmetadata", function(e){v._gotLoadedMetaData = true;});
+ v.addEventListener("loadstart", function(e){v._gotLoadStart = true;});
+ v.addEventListener("suspend", this.suspend);
+ document.body.appendChild(v); // Causes implicit load, which will be halted due to no resource.
+ v.src = test.name; // Load should start, and halt at preload:none.
+ },
+
+ name: "test5",
+ },
+ {
+ // 6. preload:none video without resource, add to document, will implicitly start a
+ // preload:none load. Add a source, it shouldn't load.
+ suspend(e) {
+ var v = e.target;
+ is(v._gotLoadStart, true, "(6) Must get loadstart.");
+ is(v._gotLoadedMetaData, false, "(6) Must not get loadedmetadata.");
+ is(v.readyState, v.HAVE_NOTHING, "(6) ReadyState must be HAVE_NOTHING");
+ // bug 962949
+ // is(v.networkState, v.NETWORK_IDLE, "(6) NetworkState must be NETWORK_IDLE");
+ maybeFinish(v, 6);
+ },
+
+ setup(v) {
+ v._gotLoadStart = false;
+ v._gotLoadedMetaData = false;
+ v.preload = "none";
+ v.addEventListener("loadedmetadata", function(e){v._gotLoadedMetaData = true;});
+ v.addEventListener("loadstart", function(e){v._gotLoadStart = true;});
+ v.addEventListener("suspend", this.suspend);
+ document.body.appendChild(v); // Causes implicit load, which will be halted due to no resource.
+ var s = document.createElement("source");
+ s.src = test.name;
+ s.type = test.type;
+ v.appendChild(s); // Load should start, and halt at preload:none.
+ },
+
+ name: "test6",
+ },
+ {
+ // 7. create a preload:none document with multiple sources, the first of which is invalid.
+ // Add to document, then play. It should load and play through the second source.
+ suspend(e) {
+ var v = e.target;
+ if (v._gotSuspend)
+ return; // We can receive multiple suspend events, like the one after download completes.
+ v._gotSuspend = true;
+ is(v._gotLoadStart, true, "(7) Must get loadstart.");
+ is(v._gotLoadedMetaData, false, "(7) Must not get loadedmetadata.");
+ is(v.readyState, v.HAVE_NOTHING, "(7) ReadyState must be HAVE_NOTHING");
+ // bug 962949
+ // is(v.networkState, v.NETWORK_IDLE, "(7) NetworkState must be NETWORK_IDLE");
+ v.play(); // Should load and play through.
+ },
+
+ ended(e) {
+ ok(true, "(7) Got playback ended");
+ var v = e.target;
+ is(v._gotErrorEvent, true, "(7) Should get error event from first source load failure");
+ maybeFinish(v, 7);
+ },
+
+ setup(v) {
+ v._gotLoadStart = false;
+ v._gotLoadedMetaData = false;
+ v.preload = "none";
+ v._gotErrorEvent = false;
+ v.addEventListener("loadedmetadata", function(e){v._gotLoadedMetaData = true;});
+ v.addEventListener("loadstart", function(e){v._gotLoadStart = true;});
+ v.addEventListener("suspend", this.suspend);
+ v.addEventListener("ended", this.ended);
+ var s1 = document.createElement("source");
+ s1.src = "not-a-real-file.404"
+ s1.type = test.type;
+ s1.addEventListener("error", function(e){v._gotErrorEvent = true;});
+ v.appendChild(s1);
+ var s2 = document.createElement("source");
+ s2.src = test.name;
+ s2.type = test.type;
+ v.appendChild(s2);
+ document.body.appendChild(v); // Causes implicit load, which will be halt at preload:none on the second resource.
+ },
+
+ name: "test7",
+ },
+ {
+ // 8. Change preload value from none to metadata should cause metadata to be loaded.
+ loadeddata(e) {
+ var v = e.target;
+ is(v._gotLoadedMetaData, true, "(8) Must get loadedmetadata.");
+ ok(v.readyState >= v.HAVE_CURRENT_DATA, "(8) ReadyState must be >= HAVE_CURRENT_DATA on suspend.");
+ // bug 962949
+ // is(v.networkState, v.NETWORK_IDLE, "(8) NetworkState must be NETWORK_IDLE when load is halted");
+ maybeFinish(v, 8);
+ },
+
+ setup(v) {
+ v._gotLoadedMetaData = false;
+ v.preload = "none";
+ v.addEventListener("loadstart", function(e){v.preload = "metadata";});
+ v.addEventListener("loadedmetadata", function(e){v._gotLoadedMetaData = true;});
+ v.addEventListener("loadeddata", this.loadeddata);
+ v.src = test.name; // Causes implicit load.
+ document.body.appendChild(v);
+ },
+
+ name: "test8",
+ },
+ /*{
+ // 9. Change preload value from metadata to auto should cause entire media to be loaded.
+ // For some reason we don't always receive the canplaythrough event, particuarly on this test.
+ // We've disabled this test until bug 568402 is fixed.
+ canplaythrough:
+ function(e) {
+ var v = e.target;
+ is(v._gotLoadStart, true, "(9) Must get loadstart.");
+ is(v._gotLoadedMetaData, true, "(9) Must get loadedmetadata.");
+ maybeFinish(v, 9);
+ },
+
+ setup:
+ function(v) {
+ v._gotLoadStart = false;
+ v._gotLoadedMetaData = false;
+ v.preload = "metadata";
+ v.addEventListener("loadstart", function(e){v._gotLoadStart = true;}, false);
+ v.addEventListener("loadedmetadata", function(e){v._gotLoadedMetaData = true;}, false);
+ v.addEventListener("loadeddata", function(){v.preload = "auto"}, false);
+ v.addEventListener("canplaythrough", this.canplaythrough, false);
+ v.src = test.name; // Causes implicit load.
+ document.body.appendChild(v);
+ },
+ },*/
+ {
+ // 10. Change preload value from none to auto should cause entire media to be loaded.
+ canplaythrough(e) {
+ var v = e.target;
+ is(v._gotLoadedMetaData, true, "(10) Must get loadedmetadata.");
+ maybeFinish(v, 10);
+ },
+
+ setup(v) {
+ v._gotLoadedMetaData = false;
+ v.preload = "none";
+ v.addEventListener("loadstart", function(e){v.preload = "auto";});
+ v.addEventListener("loadedmetadata", function(e){v._gotLoadedMetaData = true;});
+ v.addEventListener("canplaythrough", this.canplaythrough);
+ v.src = test.name; // Causes implicit load.
+ document.body.appendChild(v);
+ },
+
+ name: "test10",
+ },
+ {
+ // 11. Change preload value from none to metadata should cause metadata to load.
+ loadeddata(e) {
+ var v = e.target;
+ is(v._gotLoadedMetaData, true, "(11) Must get loadedmetadata.");
+ ok(v.readyState >= v.HAVE_CURRENT_DATA, "(11) ReadyState must be >= HAVE_CURRENT_DATA.");
+ // bug 962949
+ // is(v.networkState, v.NETWORK_IDLE, "(11) NetworkState must be NETWORK_IDLE.");
+ maybeFinish(v, 11);
+ },
+
+ setup(v) {
+ v._gotLoadedMetaData = false;
+ v.preload = "none";
+ v.addEventListener("loadstart", function(e){v.preload = "metadata";});
+ v.addEventListener("loadedmetadata", function(e){v._gotLoadedMetaData = true;});
+ v.addEventListener("loadeddata", this.loadeddata);
+ v.src = test.name; // Causes implicit load.
+ document.body.appendChild(v);
+ },
+
+ name: "test11",
+ },
+ {
+ // 13. Change preload value from auto to none after specifying a src
+ // should load according to preload none, no buffering should have taken place
+ suspend(e) {
+ var v = e.target;
+ is(v._gotLoadStart, true, "(13) Must get loadstart.");
+ is(v._gotLoadedMetaData, false, "(13) Must not get loadedmetadata.");
+ is(v.readyState, v.HAVE_NOTHING, "(13) ReadyState must be HAVE_NOTHING");
+ // bug 962949
+ // is(v.networkState, v.NETWORK_IDLE, "(13) NetworkState must be NETWORK_IDLE");
+ maybeFinish(v, 13);
+ },
+
+ setup(v) {
+ v._gotLoadStart = false;
+ v._gotLoadedMetaData = false;
+ v.preload = "auto";
+ v.src = test.name;
+ v.preload = "none";
+ v.addEventListener("loadedmetadata", function(e){v._gotLoadedMetaData = true;});
+ v.addEventListener("loadstart", function(e){v._gotLoadStart = true;});
+ v.addEventListener("suspend", this.suspend);
+ document.body.appendChild(v); // Causes implicit load, should load according to preload none
+ document.createElement("source");
+ },
+
+ name: "test13",
+ },
+ {
+ // 14. Add preload:metadata video with src to document. Play(), should play through.
+ loadeddata(e) {
+ var v = e.target;
+ is(v._gotLoadStart, true, "(14) Must get loadstart.");
+ is(v._gotLoadedMetaData, true, "(14) Must get loadedmetadata.");
+ ok(v.readyState >= v.HAVE_CURRENT_DATA, "(14) ReadyState must be >= HAVE_CURRENT_DATA");
+ // bug 962949
+ // is(v.networkState, v.NETWORK_IDLE, "(14) NetworkState must be NETWORK_IDLE");
+ v.play();
+ },
+
+ ended(e) {
+ ok(true, "(14) Got playback ended");
+ var v = e.target;
+ maybeFinish(v, 14);
+ },
+
+ setup(v) {
+ v._gotLoadStart = false;
+ v._gotLoadedMetaData = false;
+ v.preload = "metadata";
+ v.addEventListener("loadstart", function(e){v._gotLoadStart = true;});
+ v.addEventListener("loadedmetadata", function(e){v._gotLoadedMetaData = true;});
+ v.addEventListener("ended", this.ended);
+ v.addEventListener("loadeddata", this.loadeddata);
+ v.src = test.name;
+ document.body.appendChild(v); // Causes implicit load, which will be halted after
+ // metadata due to preload:metadata.
+ },
+
+ name: "test14",
+ },
+ {
+ // 15. Autoplay should override preload:none.
+ ended(e) {
+ ok(true, "(15) Got playback ended.");
+ var v = e.target;
+ maybeFinish(v, 15);
+ },
+
+ setup(v) {
+ v._gotLoadStart = false;
+ v._gotLoadedMetaData = false;
+ v.preload = "none";
+ v.autoplay = true;
+ v.addEventListener("loadstart", function(e){v._gotLoadStart = true;});
+ v.addEventListener("loadedmetadata", function(e){v._gotLoadedMetaData = true;});
+ v.addEventListener("ended", this.ended);
+ v.src = test.name; // Causes implicit load.
+ document.body.appendChild(v);
+
+ // Log events for debugging.
+ var events = ["suspend", "play", "canplay", "canplaythrough", "loadstart", "loadedmetadata",
+ "loadeddata", "playing", "ended", "error", "stalled", "emptied", "abort",
+ "waiting", "pause"];
+ function logEvent(e) {
+ info(e.target.token + ": got " + e.type);
+ }
+ events.forEach(function(e) {
+ v.addEventListener(e, logEvent);
+ });
+ },
+
+ name: "test15",
+ },
+ {
+ // 16. Autoplay should override preload:metadata.
+ ended(e) {
+ ok(true, "(16) Got playback ended.");
+ var v = e.target;
+ maybeFinish(v, 16);
+ },
+
+ setup(v) {
+ v.preload = "metadata";
+ v.autoplay = true;
+ v.addEventListener("ended", this.ended);
+ v.src = test.name; // Causes implicit load.
+ document.body.appendChild(v);
+ },
+
+ name: "test16",
+ },
+ {
+ // 17. On a preload:none video, adding autoplay should disable preload none, i.e. don't break autoplay!
+ ended(e) {
+ ok(true, "(17) Got playback ended.");
+ var v = e.target;
+ maybeFinish(v, 17);
+ },
+
+ setup(v) {
+ v.addEventListener("ended", this.ended);
+ v.preload = "none";
+ document.body.appendChild(v); // Causes implicit load, which will be halted due to preload:none.
+ v.autoplay = true;
+ v.src = test.name;
+ },
+
+ name: "test17",
+ },
+ {
+ // 18. On a preload='none' video, call play() before load algorithms's sync
+ // has run, the play() call should override preload='none'.
+ ended(e) {
+ ok(true, "(18) Got playback ended.");
+ var v = e.target;
+ maybeFinish(v, 18);
+ },
+
+ setup(v) {
+ v.addEventListener("ended", this.ended);
+ v.preload = "none";
+ v.src = test.name; // Schedules async section to continue load algorithm.
+ document.body.appendChild(v);
+ v.play(); // Should cause preload:none to be overridden.
+ },
+
+ name: "test18",
+ },
+ {
+ // 19. Set preload='auto' on first video source then switching preload='none' and swapping the video source to another.
+ // The second video should not start playing as it's preload state has been changed to 'none' from 'auto'
+ setup(v) {
+ v.preload = "auto";
+ v.src = test.name;
+ // add a listener for when the video has loaded, so we know preload auto has worked
+ v.onloadedmetadata = function() {
+ is(v.preload, "auto", "(19) preload is initially auto");
+ // set preload state to none and switch video sources
+ v.preload="none";
+ v.src = test.name + "?asdf";
+
+ v.onloadedmetadata = function() {
+ ok(false, "(19) 'loadedmetadata' shouldn't fire when preload is none");
+ }
+
+ var ontimeout = function() {
+ v.removeEventListener("suspend", onsuspend);
+ ok(false, "(19) 'suspend' should've fired");
+ maybeFinish(v, 19);
+ }
+ var cancel = setTimeout(ontimeout, 10000);
+
+ var onsuspend = function() {
+ v.removeEventListener("suspend", onsuspend);
+ clearTimeout(cancel);
+ is(v.readyState, 0, "(19) no buffering has taken place");
+ maybeFinish(v, 19);
+ }
+ v.addEventListener("suspend", onsuspend);
+ }
+ document.body.appendChild(v);
+ },
+
+ name: "test19",
+ }
+];
+
+var iterationCount = 0;
+function startTest(t, token) {
+ if (t == tests[0]) {
+ ++iterationCount;
+ info("iterationCount=" + iterationCount);
+ }
+ if (iterationCount == 2) {
+ // Do this series of tests on logically different resources
+ t.name = baseName + "?" + Math.floor(Math.random()*100000);
+ }
+ var v = document.createElement("video");
+ v.token = token;
+ t.setup(v);
+ manager.started(token);
+}
+
+var twiceTests = tests.concat(tests);
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}, beginTest);
+function beginTest() {
+ manager.runTests(twiceTests, startTest);
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_preload_attribute.html b/dom/media/test/test_preload_attribute.html
new file mode 100644
index 0000000000..1e415035c5
--- /dev/null
+++ b/dom/media/test/test_preload_attribute.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=479863
+-->
+<head>
+ <title>Test for Bug 479863</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=479863">Mozilla Bug 479863</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+
+<video id='v1'></video><audio id='a1'></audio>
+<video id='v2' preload="auto"></video><audio id='a2' preload="auto"></audio>
+
+<pre id="test">
+<script type="application/javascript">
+var v1 = document.getElementById('v1');
+var a1 = document.getElementById('a1');
+var v2 = document.getElementById('v2');
+var a2 = document.getElementById('a2');
+is(v1.getAttribute("preload"), null, "video preload via getAttribute should be null by default");
+is(a1.getAttribute("preload"), null, "video preload via getAttribute should be null by default");
+is(v1.preload, "", "v1.preload should be empty by default");
+is(a1.preload, "", "a1.preload should be empty by default");
+is(v2.preload, "auto", "v2.preload should be auto");
+is(a2.preload, "auto", "a2.preload should be auto");
+
+v1.preload = "auto";
+a1.preload = "auto";
+is(v1.preload, "auto", "video.preload should be auto");
+is(a1.preload, "auto", "audio.preload should be auto");
+is(v1.getAttribute("preload"), "auto", "video preload attribute should be auto");
+is(a1.getAttribute("preload"), "auto", "video preload attribute should be auto");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_preload_suspend.html b/dom/media/test/test_preload_suspend.html
new file mode 100644
index 0000000000..7f1146360f
--- /dev/null
+++ b/dom/media/test/test_preload_suspend.html
@@ -0,0 +1,112 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Bug 479863</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var manager = new MediaTestManager;
+
+function checkSuspendCount(evt) {
+ var v = evt.target;
+ ++v.suspendCount;
+ is(v.networkState, v.NETWORK_IDLE, v.name + " got suspended, count=" + v.suspendCount);
+ if (v.suspendCount == v.expectedSuspendCount) {
+ removeNodeAndSource(v);
+ manager.finished(v.name);
+ }
+ if (v.suspendCount > v.expectedSuspendCount) {
+ ok(false, v.name + " got too many suspend events");
+ }
+}
+
+var tests = [
+ {
+ name: 'v1',
+ preload: 'none',
+ expectedSuspendCount: 2,
+ onsuspend(evt) {
+ checkSuspendCount(evt);
+ if (evt.target.suspendCount == 1) {
+ evt.target.preload = 'auto';
+ }
+ }
+ },
+ {
+ name: 'v2',
+ preload: 'auto',
+ expectedSuspendCount: 1,
+ onsuspend: checkSuspendCount
+ },
+ {
+ name: 'v3',
+ preload: 'none',
+ autoplay: true,
+ expectedSuspendCount: 1,
+ onsuspend: checkSuspendCount
+ },
+ {
+ name: 'v4',
+ preload: 'none',
+ expectedSuspendCount: 2,
+ onsuspend(evt) {
+ checkSuspendCount(evt);
+ if (evt.target.suspendCount == 1) {
+ evt.target.play();
+ }
+ }
+ },
+ // disable v5 since media element doesn't support 'load' event anymore.
+ /*{
+ name: 'v5',
+ preload: 'none',
+ expectedSuspendCount: 2,
+ onsuspend: function(evt) {
+ checkSuspendCount(evt);
+ if (evt.target.suspendCount == 1) {
+ evt.target.currentTime = 0.1;
+ }
+ }
+ },*/
+ {
+ name: 'v6',
+ preload: 'none',
+ expectedSuspendCount: 2,
+ onsuspend(evt) {
+ checkSuspendCount(evt);
+ if (evt.target.suspendCount == 1) {
+ evt.target.autoplay = true;
+ }
+ }
+ }
+];
+
+function startTest(test, token) {
+ var v = document.createElement("video");
+ v.name = test.name;
+ var key = Math.random();
+ v.src = "seek.ogv?key=" + key + "&id=" + v.name;
+ v.preload = test.preload;
+ v.suspendCount = 0;
+ v.expectedSuspendCount = test.expectedSuspendCount;
+ if (test.autoplay) {
+ v.autoplay = true;
+ }
+ v.onsuspend = test.onsuspend;
+ document.body.appendChild(v);
+ manager.started(v.name);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}, function() {
+ manager.runTests(tests, startTest);
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_preserve_playbackrate_after_ui_play.html b/dom/media/test/test_preserve_playbackrate_after_ui_play.html
new file mode 100644
index 0000000000..98bdd66675
--- /dev/null
+++ b/dom/media/test/test_preserve_playbackrate_after_ui_play.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title> Bug 1013933 - preserve playbackRate after clicking play button </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/SimpleTest/EventUtils.js"></script>
+ <script type="application/javascript" src="browserElementTestHelpers.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+<div id="content">
+ <video width="320" height="240" id="video" controls mozNoDynamicControls preload="auto"></video>
+</div>
+
+<script type="text/javascript">
+/*
+ * Positions of the UI elements, relative to the upper-left corner of the
+ * <video> box.
+ */
+const videoHeight = 240;
+const playButtonWidth = 28;
+const playButtonHeight = 28;
+const playButtonCenterX = 0 + Math.round(playButtonWidth / 2);
+const playButtonCenterY = videoHeight - Math.round(playButtonHeight / 2);
+
+var expectedPlaybackRate = 0.5
+
+function runTest() {
+ var video = document.getElementById("video");
+ video.src = "audio.wav";
+ video.loop = true;
+ video.playbackRate = expectedPlaybackRate;
+
+ video.oncanplaythrough = function() {
+ video.oncanplaythrough = null;
+ is(video.paused, true, "video is not playing yet.");
+ is(video.playbackRate, expectedPlaybackRate,
+ "playbackRate is correct before clicking play button.");
+
+ // Click the play button
+ synthesizeMouse(video, playButtonCenterX, playButtonCenterY, { });
+ };
+
+ video.onplay = function() {
+ video.onplay = null;
+ is(video.paused, false, "video starts playing.");
+ is(video.playbackRate, expectedPlaybackRate,
+ "playbackRate is correct after clicking play button.");
+ video.pause();
+ SimpleTest.finish();
+ };
+}
+
+window.addEventListener("load", runTest);
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_progress.html b/dom/media/test/test_progress.html
new file mode 100644
index 0000000000..b958b2b335
--- /dev/null
+++ b/dom/media/test/test_progress.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: progress events</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+function do_progress(e) {
+ var v = e.target;
+ ok(!v._finished, "Check no progress events after completed for " + v._name);
+}
+
+function do_ended(e) {
+ var v = e.target;
+ ok(!v._finished, "Only one ended event for " + v._name);
+ v._finished = true;
+ v.removeEventListener("ended", do_ended);
+ v.removeEventListener("progress", do_progress);
+ removeNodeAndSource(v);
+ manager.finished(v.token);
+}
+
+function startTest(test, token) {
+ var type = /^video/.test(test.type) ? "video" : "audio";
+ var v = document.createElement(type);
+ v.token = token;
+ manager.started(token);
+ v.src = test.name;
+ v.autoplay = true;
+ v._name = test.name;
+ v._finished = false;
+ v.addEventListener("ended", do_ended);
+ v.addEventListener("progress", do_progress);
+ document.body.appendChild(v);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}, beginTest);
+function beginTest() {
+ manager.runTests(gProgressTests, startTest);
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_reactivate.html b/dom/media/test/test_reactivate.html
new file mode 100644
index 0000000000..43675f7771
--- /dev/null
+++ b/dom/media/test/test_reactivate.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test reactivation of a media element from a dead document</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+
+<iframe id="frame" src="reactivate_helper.html"></iframe>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var elements;
+
+function playElement(e) {
+ // All elements played out, finish the test case.
+ if (!e) {
+ SimpleTest.finish();
+ return;
+ }
+
+ e.play();
+ info("Element play: " + e._name);
+ var reviveElement = function() {
+ document.body.appendChild(e);
+ e.onended = function() {
+ info("Element ended: " + e._name);
+ removeNodeAndSource(e);
+ // Play next element.
+ playElement(elements.pop());
+ }
+ }
+ setTimeout(reviveElement, 2000);
+}
+
+function loadedAll(elementList) {
+ elements = elementList;
+
+ // Log events for debugging.
+ var events = ["suspend", "play", "canplay", "canplaythrough", "loadstart", "loadedmetadata",
+ "loadeddata", "playing", "ended", "error", "stalled", "emptied", "abort",
+ "waiting", "pause"];
+ function logEvent(e) {
+ info(e.target._name + ": got " + e.type);
+ }
+ elementList.forEach(function(element) {
+ events.forEach(function(evt) {
+ element.addEventListener(evt, logEvent);
+ });
+ });
+
+ // Blow away the subframe
+ document.body.removeChild(document.getElementById("frame"));
+
+ // Play elements one by one to avoid hitting bug 847903 on MacOSX.
+ playElement(elements.pop());
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_readyState.html b/dom/media/test/test_readyState.html
new file mode 100644
index 0000000000..17c363c503
--- /dev/null
+++ b/dom/media/test/test_readyState.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: readyState</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<video id='v1'></video><audio id='a1'></audio>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+"use strict";
+var v1 = document.getElementById('v1');
+var a1 = document.getElementById('a1');
+var passed = "truthy";
+
+is(v1.readyState, 0);
+is(a1.readyState, 0);
+
+try {
+ v1.readyState = 0;
+} catch (e) {
+ passed = !passed;
+}
+try {
+ a1.readyState = 0;
+} catch (e) {
+ passed = !passed;
+}
+ok(passed === true,
+ "Setting readyState throws in strict mode (readonly attribute)");
+</script>
+
+<script class="testbody" type="text/javascript">
+var v1 = document.getElementById('v1');
+var a1 = document.getElementById('a1');
+var passed = false;
+
+is(v1.readyState, 0);
+is(a1.readyState, 0);
+
+try {
+ v1.readyState = 1;
+ a1.readyState = 1;
+ passed = v1.readyState === 0 && a1.readyState === 0;
+} catch(e) { }
+ok(passed, "Should not be able to set readyState (readonly attribute)");
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_referer.html b/dom/media/test/test_referer.html
new file mode 100644
index 0000000000..4ce78d5fb3
--- /dev/null
+++ b/dom/media/test/test_referer.html
@@ -0,0 +1,88 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=584480
+-->
+<head>
+ <title>Test for Bug 584480</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=584480">Mozilla Bug </a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+var media = [];
+
+function checkComplete() {
+ for (var i=0; i<media.length; ++i) {
+ if (!media[i]._complete) {
+ return;
+ }
+ }
+
+ SimpleTest.finish();
+}
+
+function removeNode(v) {
+ v.removeEventListener("error", loadError);
+ v.removeEventListener("loadedmetadata", loadedMetadata);
+ v.remove();
+ v.src = "";
+}
+
+function loadError(evt) {
+ // If no referer is sent then the sjs returns an error
+ ok(false, "check referer is sent with media request");
+ evt.target._complete = true;
+ checkComplete();
+ removeNode(evt.target);
+}
+
+function loadedMetadata(evt) {
+ // If a referer is sent then the sjs returns a valid media
+ ok(true, "check referer is sent with media request");
+ evt.target._complete = true;
+ checkComplete();
+ removeNode(evt.target);
+}
+
+// Create all media objects.
+for (var i=0; i<gSmallTests.length; ++i) {
+ var test = gSmallTests[i];
+ var type;
+ if (/^video/.test(test.type)) {
+ type = "video"
+ } else {
+ type = "audio";
+ }
+ var v = document.createElement(type);
+ if (!v.canPlayType(test.type)) {
+ continue;
+ }
+ // ensure metadata is loaded for default preload is none on b2g
+ v.preload = "metadata";
+ v.autoplay = "true";
+ v._complete = false;
+ v.addEventListener("error", loadError);
+ v.addEventListener("loadedmetadata", loadedMetadata);
+ v.src = 'referer.sjs?name=' + test.name + '&type=' + test.type;
+ document.body.appendChild(v); // Will start load.
+ media.push(v);
+}
+
+if (!media.length) {
+ todo(false, "No types supported");
+} else {
+ SimpleTest.waitForExplicitFinish();
+}
+</script>
+</pre>
+
+</body>
+</html>
diff --git a/dom/media/test/test_replay_metadata.html b/dom/media/test/test_replay_metadata.html
new file mode 100644
index 0000000000..dab022885f
--- /dev/null
+++ b/dom/media/test/test_replay_metadata.html
@@ -0,0 +1,120 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=467972
+-->
+<head>
+ <title>Test for Bug 467972</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=467972">Mozilla Bug 467972</a>
+
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+// Test for Bug 467972. Tests that when we play to end, seek to 0, and play again, that we don't
+// send/receive multiple loadeddata and loadedmetadata events.
+
+var manager = new MediaTestManager;
+
+function seekStarted(evt) {
+ var v = evt.target;
+ v._gotSeekStarted = true;
+}
+
+function seekEnded(evt) {
+ var v = evt.target;
+ v._gotSeekEnded = true;
+ v.play();
+}
+
+function loadedData(evt) {
+ var v = evt.target;
+ v._loadedDataCount++;
+ ok(v._loadedDataCount <= 1, "No more than 1 onloadeddata event for " + v._name);
+}
+
+function loadedMetaData(evt) {
+ var v = evt.target;
+ v._loadedMetaDataCount++;
+ ok(v._loadedMetaDataCount <= 1, "No more than 1 onloadedmetadata event for " + v._name);
+ checkMetadata(v._name, v, v._test);
+ v.play();
+}
+
+function playing(evt) {
+ evt.target._playingCount++;
+}
+
+function removeNodeAndListener(n) {
+ n.removeEventListener("loadedmetadata", loadedMetaData);
+ n.removeEventListener("ended", playbackEnded);
+ n.removeEventListener("playing", playing);
+ n.removeEventListener("loadeddata", loadedData);
+ n.removeEventListener("seeking", seekStarted);
+ n.removeEventListener("seeked", seekEnded);
+ removeNodeAndSource(n);
+}
+
+function playbackEnded(evt) {
+ var v = evt.target;
+ v._endCount++;
+ ok(v.currentTime >= v.duration-0.1 && v.currentTime <= v.duration + 0.1,
+ "CurrentTime (" + v.currentTime + ") should be around " + v.duration
+ + " for " + v._name);
+ if (!v._playedOnce) {
+ v.currentTime = 0;
+ ok(v.seeking, "seeking should be true for " + v._name);
+ ok(!v.ended, "ended shouldn't be true once seeking has begun for " + v._name);
+ v._playedOnce = true;
+ } else {
+ ok(v._gotSeekEnded, "Should have received seekended for " + v._name);
+ ok(v._gotSeekStarted, "Should have received seekstarted for " + v._name);
+ is(v._loadedDataCount, 1, "Should have 1 onloadeddata event for " + v._name);
+ is(v._loadedMetaDataCount, 1, "Should have 1 onloadedmetadata event for " + v._name);
+ is(v._endCount, 2, "Should have received two ended events for " + v._name);
+ ok(v._playingCount > 0, "Should have at least one playing event for " + v._name);
+ v._finished = true;
+ removeNodeAndListener(v);
+ manager.finished(v.token);
+ }
+}
+
+function startTest(test, token) {
+ var v = document.createElement('video');
+ v.preload = "auto";
+ v.token = token;
+ manager.started(token);
+ v.src = test.name;
+ v._name = test.name;
+ v._playedOnce = false;
+ v._gotSeekEnded = false;
+ v._gotSeekStarted = false;
+ v._loadedDataCount = 0;
+ v._loadedMetaDataCount = 0;
+ v._playingCount = 0;
+ v._endCount = 0;
+ v._test = test;
+ v._finished = false;
+ v.addEventListener("loadedmetadata", loadedMetaData);
+ v.addEventListener("ended", playbackEnded);
+ v.addEventListener("playing", playing);
+ v.addEventListener("loadeddata", loadedData);
+ v.addEventListener("seeking", seekStarted);
+ v.addEventListener("seeked", seekEnded);
+ document.body.appendChild(v);
+}
+
+manager.runTests(gSmallTests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_reset_events_async.html b/dom/media/test/test_reset_events_async.html
new file mode 100644
index 0000000000..482ec55986
--- /dev/null
+++ b/dom/media/test/test_reset_events_async.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=975270
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="manifest.js"></script>
+ <script type="application/javascript">
+
+ /** Test for Bug 975270 **/
+ // Test that 'emptied' and 'abort' events are fired asynchronously when re-starting
+ // media load.
+ SimpleTest.waitForExplicitFinish();
+
+ var a = document.createElement("audio");
+ a._abort = 0;
+ a._emptied = 0;
+ a.preload = "metadata"; // On B2G we default to preload:none.
+
+ is(a.networkState, HTMLMediaElement.NETWORK_EMPTY, "Shouldn't be loading");
+
+ a.addEventListener("abort", function(e) { a._abort++; });
+ a.addEventListener("emptied", function(e) { a._emptied++; });
+ a.addEventListener("loadedmetadata",
+ function(e) {
+ is(a._abort, 0, "Should not have received 'abort' before 'loadedmetadata");
+ is(a._emptied, 0, "Should not have received 'emptied' before 'loadedmetadata");
+
+ a.addEventListener("loadstart",
+ function() {
+ is(a._abort, 1, "Should have received 'abort' before 'loadstart");
+ is(a._emptied, 1, "Should have received 'emptied' before 'loadstart");
+ SimpleTest.finish();
+ });
+
+ a.src = "";
+ is(a._abort, 0, "Should not have received 'abort' during setting a.src=''");
+ is(a._emptied, 0, "Should not have received 'emptied' during setting a.src=''");
+ });
+
+ a.src = getPlayableAudio(gSmallTests).name;
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=">Mozilla Bug </a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_reset_src.html b/dom/media/test/test_reset_src.html
new file mode 100644
index 0000000000..a30f9cc3ef
--- /dev/null
+++ b/dom/media/test/test_reset_src.html
@@ -0,0 +1,98 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=804875
+-->
+
+<head>
+ <title>Test for bug 804875</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="manifest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank"
+href="https://bugzilla.mozilla.org/show_bug.cgi?id=804875">Mozilla Bug 804875</a>
+
+<canvas></canvas>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+function finish(v) {
+ removeNodeAndSource(v);
+ manager.finished(v.token);
+}
+
+function onLoadedData_Audio(e) {
+ var t = e.target;
+ is(t.videoHeight, 0, t.name + ": videoHeight should be zero when there is no video.");
+ is(t.videoWidth, 0, t.name + ": videoWidth should be zero when there is no video.");
+ is(t.mozPaintedFrames, 0, t.name + ": mozPaintedFrames should be zero when there is no video.");
+ is(t.mozFrameDelay, 0, t.name + ": mozFrameDelay should be zero when there is no video.");
+ var c = document.getElementsByTagName("canvas")[0].getContext("2d");
+ var threw = false;
+ try {
+ c.drawImage(t, 0, 0, t.videoHeight, t.videoWidth);
+ } catch (ex) {
+ threw = true;
+ }
+ ok(!threw, t.name + ": Even though we don't succeed to draw a video frame on the canvas, we shouldn't throw.");
+ finish(t);
+}
+
+function onTimeUpdate_Video(e) {
+ var t = e.target;
+ if (t.currentTime < t.duration / 4) {
+ return;
+ }
+ t.removeEventListener("timeupdate", onTimeUpdate_Video);
+ // There's no guarantee that a video frame composite notification reaches
+ // us before timeupdate fires.
+ ok(t.mozPaintedFrames >= 0, t.name + ": mozPaintedFrames should be positive or zero, is " + t.mozPaintedFrames + ".");
+ ok(t.mozFrameDelay >= 0, t.name + ": mozFrameDelay should be positive or zero, is " + t.mozFrameDelay + ".");
+
+ if (t._firstTime) {
+ // eslint-disable-next-line no-self-assign
+ t.src = t.src;
+ t._firstTime = false;
+ } else {
+ var source = getPlayableAudio(gPlayTests);
+ if (!source) {
+ todo("No audio file available.")
+ finish(t);
+ } else {
+ t.removeEventListener("loadeddata", onLoadedData_Video);
+ t.addEventListener("loadeddata", onLoadedData_Audio);
+ t.src = source.name;
+ }
+ }
+}
+
+function onLoadedData_Video(e) {
+ var t = e.target;
+ isnot(t.videoHeight, 0, t.name + ": We should have a videoHeight.");
+ isnot(t.videoWidth, 0, t.name + ": We should have a videoWidth.");
+ t.addEventListener("timeupdate", onTimeUpdate_Video);
+ t.play();
+}
+
+function startTest(test, token) {
+ var v = document.createElement('video');
+ document.body.appendChild(v);
+ v._firstTime = true;
+ v.addEventListener("loadeddata", onLoadedData_Video);
+ v.src = test.name;
+ v.token = token;
+ v.name = test.name;
+ v.play();
+ manager.started(token);
+}
+
+manager.runTests(getPlayableVideos(gSmallTests.concat(gSeekTests)), startTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_resolution_change.html b/dom/media/test/test_resolution_change.html
new file mode 100644
index 0000000000..403f9b505d
--- /dev/null
+++ b/dom/media/test/test_resolution_change.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test playback of files with resolution changes</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+function loadedData(e) {
+ var v = e.target;
+ v.addEventListener("resize", resize);
+ v.play();
+}
+
+function resize(e) {
+ var v = e.target;
+ v.seenResolutionChange = true;
+}
+
+function ended(e) {
+ var v = e.target;
+ ok(v.seenResolutionChange, v.token + ": A resolution change should have ocurred by the end of playback");
+ removeNodeAndSource(v);
+ manager.finished(v.token);
+}
+
+function startTest(test, token) {
+ var v = document.createElement('video');
+ v.preload = "metadata";
+ v.token = token;
+ v.src = test.name;
+ v.seenResolutionChange = false;
+
+ v.addEventListener("loadeddata", loadedData)
+ v.addEventListener("ended", ended);
+
+ manager.started(token);
+ document.body.appendChild(v);
+}
+
+manager.runTests(gResolutionChangeTests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_resume.html b/dom/media/test/test_resume.html
new file mode 100644
index 0000000000..0e22894be1
--- /dev/null
+++ b/dom/media/test/test_resume.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: Test resume of server-dropped connections</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<audio preload="auto" id="a"></audio>
+<iframe id="f"></iframe>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var key = Math.round(Math.random()*1000000000);
+var a = document.getElementById("a");
+var f = document.getElementById("f");
+
+function didEnd() {
+ ok(a.currentTime > 2.26, "Reached correct end time (got " + a.currentTime + ", expected > 2.26");
+ SimpleTest.finish();
+}
+
+function didSendCancel() {
+ a.addEventListener("ended", didEnd);
+ a.play();
+}
+
+function didSuspend() {
+ a.removeEventListener("suspend", didSuspend);
+
+ // Cache must have filled up, or something. Tell the Web server to drop
+ // our connection.
+ f.addEventListener("load", didSendCancel);
+ f.src = "cancellable_request.sjs?cancelkey=" + key;
+}
+
+if (!a.canPlayType("audio/wave")) {
+ todo(false, "Test requires support for audio/wave");
+} else {
+ a.addEventListener("suspend", didSuspend);
+ a.src = "cancellable_request.sjs?key=" + key;
+ SimpleTest.waitForExplicitFinish();
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_seamless_looping.html b/dom/media/test/test_seamless_looping.html
new file mode 100644
index 0000000000..94d020c834
--- /dev/null
+++ b/dom/media/test/test_seamless_looping.html
@@ -0,0 +1,199 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Test for seamless loop of HTMLAudioElements</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<canvas id="canvas" width="300" height="300"></canvas>
+<script type="application/javascript">
+/**
+ * This test is used to ensure every time we loop audio, the audio can loop
+ * seamlessly which means there won't have any silenece or noise between the
+ * end and the start.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+// Set DEBUG to true to add a canvas with a little drawing of what is going
+// on, and actually outputs the audio to the speakers.
+var DEBUG = true;
+var LOOPING_COUNT = 0;
+var MAX_LOOPING_COUNT = 10;
+// Test files are at 44100Hz, files are one second long, and contain therefore
+// 100 periods
+var TONE_FREQUENCY = 441;
+
+(async function testSeamlesslooping() {
+ let wavFileURL = {
+ name: URL.createObjectURL(new Blob([createSrcBuffer()], { type: 'audio/wav'
+ })),
+ type: "audio/wav"
+ };
+
+ let testURLs = gSeamlessLoopingTests.splice(0)
+ testURLs.push(wavFileURL);
+ for (let testFile of testURLs) {
+ LOOPING_COUNT = 0;
+ info(`- create looping audio element ${testFile.name}`);
+ let audio = createAudioElement(testFile.name);
+
+ info(`- start audio and analyze audio wave data to ensure looping audio without any silence or noise -`);
+ await playAudioAndStartAnalyzingWaveData(audio);
+
+ info(`- test seamless looping multiples times -`);
+ for (LOOPING_COUNT = 0; LOOPING_COUNT < MAX_LOOPING_COUNT; LOOPING_COUNT++) {
+ await once(audio, "seeked");
+ info(`- the round ${LOOPING_COUNT} of the seamless looping succeeds -`);
+ }
+ window.audio.remove();
+ window.ac.close();
+ }
+
+ info(`- end of seamless looping test -`);
+ SimpleTest.finish();
+})();
+
+/**
+ * Test utility functions
+ */
+function createSrcBuffer() {
+ // Generate the sine in floats, then convert, for simplicity.
+ let channels = 1;
+ let sampleRate = 44100;
+ let buffer = new Float32Array(sampleRate * channels);
+ let phase = 0;
+ const TAU = 2 * Math.PI;
+ for (let i = 0; i < buffer.length; i++) {
+ // Adjust the gain a little so we're sure it's not going to clip. This is
+ // important because we're converting to 16bit integer right after, and
+ // clipping will clearly introduce a discontinuity that will be
+ // mischaracterized as a looping click.
+ buffer[i] = Math.sin(phase) * 0.99;
+ phase += TAU * TONE_FREQUENCY / 44100;
+ if (phase > 2 * TAU) {
+ phase -= TAU;
+ }
+ }
+
+ // Make a RIFF header, it's 23 bytes
+ let buf = new Int16Array(buffer.length + 23);
+ buf[0] = 0x4952;
+ buf[1] = 0x4646;
+ buf[2] = (2 * buffer.length + 15) & 0x0000ffff;
+ buf[3] = ((2 * buffer.length + 15) & 0xffff0000) >> 16;
+ buf[4] = 0x4157;
+ buf[5] = 0x4556;
+ buf[6] = 0x6d66;
+ buf[7] = 0x2074;
+ buf[8] = 0x0012;
+ buf[9] = 0x0000;
+ buf[10] = 0x0001;
+ buf[11] = 1;
+ buf[12] = 44100 & 0x0000ffff;
+ buf[13] = (44100 & 0xffff0000) >> 16;
+ buf[14] = (2 * channels * sampleRate) & 0x0000ffff;
+ buf[15] = ((2 * channels * sampleRate) & 0xffff0000) >> 16;
+ buf[16] = 0x0004;
+ buf[17] = 0x0010;
+ buf[18] = 0x0000;
+ buf[19] = 0x6164;
+ buf[20] = 0x6174;
+ buf[21] = (2 * buffer.length) & 0x0000ffff;
+ buf[22] = ((2 * buffer.length) & 0xffff0000) >> 16;
+
+ // convert to int16 and copy.
+ for (let i = 0; i < buffer.length; i++) {
+ buf[i + 23] = Math.round(buffer[i] * (1 << 15));
+ }
+ return buf;
+}
+
+function createAudioElement(url) {
+ /* global audio */
+ window.audio = document.createElement("audio");
+ audio.src = url;
+ audio.controls = true;
+ audio.loop = true;
+ document.body.appendChild(audio);
+ return audio;
+}
+
+async function playAudioAndStartAnalyzingWaveData(audio) {
+ createAudioWaveAnalyser(audio);
+ ok(await once(audio, "canplay").then(() => true, () => false),
+ `audio can start playing.`)
+ ok(await audio.play().then(() => true, () => false),
+ `audio started playing successfully.`);
+}
+
+function createAudioWaveAnalyser(source) {
+ /* global ac, analyser */
+ window.ac = new AudioContext();
+ window.analyser = ac.createAnalyser();
+ analyser.frequencyBuf = new Float32Array(analyser.frequencyBinCount);
+ analyser.smoothingTimeConstant = 0;
+ analyser.fftSize = 2048; // 1024 bins
+
+ let sourceNode = ac.createMediaElementSource(source);
+ sourceNode.connect(analyser);
+
+ if (DEBUG) {
+ analyser.connect(ac.destination);
+ analyser.timeDomainBuf = new Float32Array(analyser.frequencyBinCount);
+ let cvs = document.querySelector("canvas");
+ analyser.c = cvs.getContext("2d");
+ analyser.w = cvs.width;
+ analyser.h = cvs.height;
+ }
+
+ analyser.notifyAnalysis = () => {
+ if (LOOPING_COUNT >= MAX_LOOPING_COUNT) {
+ return;
+ }
+ let {frequencyBuf} = analyser;
+ analyser.getFloatFrequencyData(frequencyBuf);
+ // Let things stabilize at the beginning. See bug 1441509.
+ if (LOOPING_COUNT > 1) {
+ analyser.doAnalysis(frequencyBuf, ac.sampleRate);
+ }
+
+ if (DEBUG) {
+ let {c, w, h, timeDomainBuf} = analyser;
+ c.clearRect(0, 0, w, h);
+ analyser.getFloatTimeDomainData(timeDomainBuf);
+ for (let i = 0; i < frequencyBuf.length; i++) {
+ c.fillRect(i, h, 1, -frequencyBuf[i] + analyser.minDecibels);
+ }
+
+ for (let i = 0; i < timeDomainBuf.length; i++) {
+ c.fillRect(i, h / 2, 1, -timeDomainBuf[i] * h / 2);
+ }
+ }
+
+ requestAnimationFrame(analyser.notifyAnalysis);
+ }
+
+ analyser.doAnalysis = (buf, ctxSampleRate) => {
+ // The size of an FFT is twice the number of bins in its output.
+ let fftSize = 2 * buf.length;
+ // first find a peak where we expect one.
+ let binIndexTone = 1 + Math.round(TONE_FREQUENCY * fftSize / ctxSampleRate);
+ ok(buf[binIndexTone] > -35,
+ `Could not find a peak: ${buf[binIndexTone]} db at ${TONE_FREQUENCY}Hz
+ (${source.src})`);
+
+ // check that the energy some octaves higher is very low.
+ let binIndexOutsidePeak = 1 + Math.round(TONE_FREQUENCY * 4 * buf.length / ctxSampleRate);
+ ok(buf[binIndexOutsidePeak] < -84,
+ `Found unexpected high frequency content: ${buf[binIndexOutsidePeak]}db
+ at ${TONE_FREQUENCY * 4}Hz (${source.src})`);
+ }
+
+ analyser.notifyAnalysis();
+}
+</script>
+</body>
+</html>
diff --git a/dom/media/test/test_seamless_looping_cancel_looping_future_frames.html b/dom/media/test/test_seamless_looping_cancel_looping_future_frames.html
new file mode 100644
index 0000000000..28a56af7ff
--- /dev/null
+++ b/dom/media/test/test_seamless_looping_cancel_looping_future_frames.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Seamless looping test cancel looping - no future frame</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script type="text/javascript" src="manifest.js"></script>
+</head>
+<script type="application/javascript">
+
+/**
+ * This test aims to check whether the future frames, which are the frames
+ * beyond EOS and will be played in the next round of looping, would be discared
+ * correctly if we cancel looping in the first looping round.
+ */
+add_task(async function testSeamlessLoopingVideoCancelLoopingDiscardFutureFrames() {
+ info(`create video and play it`);
+ let video = document.createElement('video');
+ video.loop = true;
+ video.src = "gizmo.mp4";
+ document.body.appendChild(video);
+ // speed up the test.
+ video.playbackRate = 2;
+ await video.play();
+
+ info(`wait until the video reaches to near end`);
+ let checkPoint1, checkPoint2;
+ await new Promise(r => {
+ video.ontimeupdate = _ => {
+ // When media almost reaches to the end, the audio track should already
+ // reach to the end before and started storing some future frames for next
+ // round of looping.
+ if (video.currentTime > video.duration - 1.5) {
+ info(`cancel looping`);
+ video.loop = false;
+ video.ontimeupdate = null;
+ checkPoint1 = new Date();
+ r();
+ }
+ }
+ });
+
+ // Because we've canceled looping, playback should reach to the end soon
+ // (less than 0.5 seconds, but we use 1 second as a threshold to avoid
+ // intermittent failure) But if we didn't discard future frames correctly,
+ // that would cause that the video plays more audio frames and requires more
+ // time to reach the end. Note, in the error case I encountered, the playback
+ // didn't discard future frames and kept decoding since then, which causes
+ // playing extra a whole audio track again.
+ info(`wait until video reaches to the end within correct remaining time`);
+ await once(video, "ended");
+ checkPoint2 = new Date();
+ const diffInSeconds = Math.abs(checkPoint1 - checkPoint2) / 1000;
+ ok(diffInSeconds < 2,
+ `finished playing within ${diffInSeconds} without incorrectly playing any future frame`);
+});
+
+</script>
+<body>
+</body>
+</html>
diff --git a/dom/media/test/test_seamless_looping_duration.html b/dom/media/test/test_seamless_looping_duration.html
new file mode 100644
index 0000000000..4a64a81759
--- /dev/null
+++ b/dom/media/test/test_seamless_looping_duration.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Seamless looping test duration</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script type="text/javascript" src="manifest.js"></script>
+</head>
+<script type="application/javascript">
+
+/**
+ * This test aims to check that the media duration shouldn't be changed during
+ * seamless looping.
+ */
+add_task(async function testSeamlessLoopingDuration() {
+ info(`create video and play it`);
+ let video = document.createElement('video');
+ video.loop = true;
+ video.src = "gizmo-short.mp4";
+ document.body.appendChild(video);
+ await video.play();
+
+ video.ondurationchange =
+ _ => ok(false, "shouldn't change duration during looping!");
+
+ info(`test seamless looping multiples times`);
+ let MAX_LOOPING_COUNT = 10;
+ for (let count = 0; count < MAX_LOOPING_COUNT; count++) {
+ await once(video, "seeking");
+ await once(video, "seeked");
+ ok(true, `the round ${count} of the seamless looping succeeds`);
+ }
+});
+
+// This one tests the situation where both tracks reached EOS before entering
+// looping state.
+add_task(async function testSeamlessLoopingDuration2() {
+ info(`create video and play it to the end`);
+ let video = document.createElement('video');
+ video.src = "gizmo-short.mp4";
+ document.body.appendChild(video);
+ await video.play();
+ await once(video, "ended");
+
+ info(`play video again`);
+ video.ondurationchange =
+ _ => ok(false, "shouldn't change duration during looping!");
+ video.loop = true;
+ await video.play();
+
+ info(`test seamless looping multiples times`);
+ let MAX_LOOPING_COUNT = 10;
+ for (let count = 0; count < MAX_LOOPING_COUNT; count++) {
+ await once(video, "seeking");
+ await once(video, "seeked");
+ ok(true, `the round ${count} of the seamless looping succeeds`);
+ }
+});
+
+</script>
+<body>
+</body>
+</html>
diff --git a/dom/media/test/test_seamless_looping_media_element_state.html b/dom/media/test/test_seamless_looping_media_element_state.html
new file mode 100644
index 0000000000..c94de04912
--- /dev/null
+++ b/dom/media/test/test_seamless_looping_media_element_state.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Seamless looping media element state</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script type="text/javascript" src="manifest.js"></script>
+</head>
+<script type="application/javascript">
+
+/**
+ * This test aims to check if the media element would change its ready state
+ * under `HAVE_CURRENT_DATA` when the seamless looping is performed. Because
+ * during seamless looping, we should always have current frame.
+ */
+add_task(async function testSeamlessLoopingMediaElementState() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.seamless-looping-video", true],
+ ],
+ });
+
+ info(`create video`);
+ let video = document.createElement('video');
+ video.loop = true;
+ video.src = "gizmo-short.mp4";
+ document.body.appendChild(video);
+ await video.play();
+
+ info(`test seamless looping multiples times`);
+ let MAX_LOOPING_COUNT = 10;
+ for (let count = 0; count < MAX_LOOPING_COUNT; count++) {
+ await once(video, "seeking");
+ // If the video looping is not seamless, when playback reaches to the end,
+ // MDSM would trigger a seek in order to get the new frame from the start
+ // position. That would notify the media element that the status of the next
+ // frame is not available now due to seeking (NEXT_FRAME_UNAVAILABLE_SEEKING)
+ // and causes the media element dispatching `waiting` event.
+ video.onwaiting = () => {
+ ok(false, "should not get waiting event");
+ }
+ await once(video, "seeked");
+ video.onwaiting = null;
+ ok(true, `the round ${count} of the seamless looping succeeds`);
+ }
+});
+
+</script>
+<body>
+</body>
+</html>
diff --git a/dom/media/test/test_seamless_looping_resume_video_decoding.html b/dom/media/test/test_seamless_looping_resume_video_decoding.html
new file mode 100644
index 0000000000..e9e4778d72
--- /dev/null
+++ b/dom/media/test/test_seamless_looping_resume_video_decoding.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Seamless looping test with resuming video decoding</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script type="text/javascript" src="manifest.js"></script>
+<script src="background_video.js"></script>
+</head>
+<script type="application/javascript">
+
+/**
+ * This test aims to check if seamless looping can work well with the mechanism
+ * of resuming video decoding. When resuming decoding, MDSM will leave the
+ * looping state and go to video only seek state. We want to ensure that the
+ * timestamp of new video data that state requests will also be adjusted in
+ * order to keep video playback ongoing without video being frozen because
+ * frames get discarded.
+ */
+add_task(async function testSeamlessLoopingResumeVideoDecoding() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.test.video-suspend", true],
+ ["media.suspend-bkgnd-video.enabled", true],
+ ["media.suspend-bkgnd-video.delay-ms", 0],
+ ],
+ });
+
+ info(`create video and play it`);
+ let video = document.createElement('video');
+ video.loop = true;
+ video.src = "gizmo.mp4";
+ document.body.appendChild(video);
+ // speed up the test.
+ video.playbackRate = 2;
+ await video.play();
+
+ info(`test seamless looping once`);
+ await once(video, "seeked");
+ ok(true, `loop back happened`);
+
+ info(`suspend video decoding`);
+ video.setVisible(false);
+ await nextVideoSuspends(video);
+ info(`suspended video decoding`);
+
+ info(`resume video decoding (enter video-only seek state)`);
+ video.setVisible(true);
+ await testVideoOnlySeekCompletedWhenShown(video);
+ info(`resumed video decoding and finished video-only seeking`);
+
+ const lastPaintedFramesAmount = video.mozPaintedFrames;
+ info(`end test after looping one more time`);
+ await once(video, "seeked");
+
+ const currentPaintedFrameAmount = video.mozPaintedFrames;
+ ok(lastPaintedFramesAmount < currentPaintedFrameAmount,
+ `painted frames keeps growing from ${lastPaintedFramesAmount} to ${currentPaintedFrameAmount}`);
+});
+
+</script>
+<body>
+</body>
+</html>
diff --git a/dom/media/test/test_seamless_looping_seek_current_time.html b/dom/media/test/test_seamless_looping_seek_current_time.html
new file mode 100644
index 0000000000..888e5437c4
--- /dev/null
+++ b/dom/media/test/test_seamless_looping_seek_current_time.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Seamless looping current time after seek</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script type="text/javascript" src="manifest.js"></script>
+</head>
+<script type="application/javascript">
+
+/**
+ * This test is used to ensure that the playback position won't be reset to ZERO
+ * incorrectly when performing seek on a looping media.
+ */
+add_task(async function testSeekVideoOnlyPlayback() {
+ info(`create and play a media which contains only video track`);
+ let video = createLoopingMedia("video", "gizmo-noaudio.webm");
+ await video.play();
+ await assertSeekingForwardShouldIncreaseCurrentTime(video);
+ removeNodeAndSource(video);
+});
+
+add_task(async function testSeekAudioOnlyPlayback() {
+ info(`create and play a media which contains only audio track`);
+ let audio = createLoopingMedia("audio", "flac-s24.flac");
+ await audio.play();
+ await assertSeekingForwardShouldIncreaseCurrentTime(audio);
+ removeNodeAndSource(audio);
+});
+
+add_task(async function testSeekBothTracksPlayback() {
+ info(`create and play a media which contains both tracks`);
+ let video = createLoopingMedia("video", "gizmo.mp4");
+ await video.play();
+ await assertSeekingForwardShouldIncreaseCurrentTime(video);
+ removeNodeAndSource(video);
+});
+
+// Following are helper functions
+function createLoopingMedia(type, src) {
+ let media = document.createElement(type);
+ media.loop = true;
+ media.src = src;
+ document.body.appendChild(media);
+ return media;
+}
+
+async function assertSeekingForwardShouldIncreaseCurrentTime(media) {
+ const currentTimeBeforeSeek = media.currentTime;
+ const target = media.duration / 2;
+ media.currentTime = target;
+ await once(media, "seeked");
+ const currentTimeAfterSeek = media.currentTime;
+ ok(currentTimeAfterSeek > currentTimeBeforeSeek,
+ `media current time should keep going forward (target=${target}, ` +
+ `currentTime=${currentTimeBeforeSeek} -> ${currentTimeAfterSeek})`);
+}
+
+</script>
+<body>
+</body>
+</html>
diff --git a/dom/media/test/test_seamless_looping_video.html b/dom/media/test/test_seamless_looping_video.html
new file mode 100644
index 0000000000..3bf99b0a62
--- /dev/null
+++ b/dom/media/test/test_seamless_looping_video.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Seamless looping video canvas test</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+<script type="text/javascript" src="manifest.js"></script>
+</head>
+<canvas id="canvas"></canvas>
+<video id="v"></video>
+<script type="application/javascript">
+
+/**
+ * This test aims to check if the video is seamless looping by capturing the
+ * image when loop happens. We use a video contains only white frames, so the
+ * captured frame should always be a white frame. If looping is not seamless,
+ * the captured frame would become a black frame.
+ */
+const WIDTH = 10, HEIGHT = 10;
+
+add_task(async function testSeamlessLoopingVideoCanvas() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.seamless-looping-video", true],
+ ],
+ });
+
+ info(`load video which only contains white frames`);
+ let video = document.getElementById("v");
+ video.loop = true;
+ video.src = "white-short.webm";
+ video.width = WIDTH;
+ video.height = HEIGHT;
+ await video.play();
+
+ info(`setup canvas`);
+ const cvs = document.getElementById("canvas");
+ cvs.width = WIDTH;
+ cvs.height = HEIGHT;
+
+ info(`test seamless looping multiples times`);
+ let MAX_LOOPING_COUNT = 10;
+ for (let count = 0; count < MAX_LOOPING_COUNT; count++) {
+ await once(video, "seeking");
+ assertPaintedFrameIsWhiteFrame();
+ await once(video, "seeked");
+ ok(true, `the round ${count} of the seamless looping succeeds`);
+ }
+});
+
+function assertPaintedFrameIsWhiteFrame() {
+ info(`catpure image from video`);
+ const cvs = document.getElementById("canvas");
+ let context = cvs.getContext('2d');
+ if (!context) {
+ ok(false, "can't get 2d context");
+ }
+
+ context.drawImage(document.getElementById("v"), 0, 0, WIDTH, HEIGHT);
+ let imageData = context.getImageData(0, 0, WIDTH, HEIGHT);
+ for (let idx = 0; idx < WIDTH * HEIGHT; idx++) {
+ let pixelCount = 4 * idx; // RGBA
+ let data = imageData.data;
+ // White frame's RGB value should be [255,255,255]
+ is(data[pixelCount + 0], 255, `R should be 255`);
+ is(data[pixelCount + 1], 255, `G should be 255`);
+ is(data[pixelCount + 2], 255, `B should be 255`);
+ }
+}
+</script>
+<body>
+</body>
+</html>
diff --git a/dom/media/test/test_seek-1.html b/dom/media/test/test_seek-1.html
new file mode 100644
index 0000000000..8690a475b6
--- /dev/null
+++ b/dom/media/test/test_seek-1.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: seek tests</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="seek_support.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// The data being used in these tests is specified in manifest.js.
+// The functions to build the test array and to run a specific test are in
+// seek_support.js.
+
+const SEEK_TEST_NUMBER = 1;
+
+function test_seek1(v, seekTime, is, ok, finish) {
+
+var startPassed = false;
+var endPassed = false;
+var seekFlagStart = false;
+var seekFlagEnd = false;
+var readonly = true;
+var completed = false;
+
+function startTest() {
+ ok(!completed, "Should not be completed yet");
+ ok(!v.seeking, "seeking should default to false");
+ try {
+ v.seeking = true;
+ readonly = v.seeking === false;
+ }
+ catch(e) {
+ readonly = "threw exception: " + e;
+ }
+ is(readonly, true, "seeking should be readonly");
+
+ v.currentTime = seekTime;
+ seekFlagStart = v.seeking;
+}
+
+function seekStarted() {
+ ok(!completed, "should not be completed yet");
+ ok(Math.abs(v.currentTime - seekTime) < 0.1,
+ "Video currentTime should be around " + seekTime + ": " + v.currentTime + " (seeking)");
+ startPassed = true;
+}
+
+function seekEnded() {
+ ok(!completed, "shuld not be completed yet");
+ ok(Math.abs(v.currentTime - seekTime) < 0.1,
+ "Video currentTime should be around " + seekTime + ": " + v.currentTime + " (seeked)");
+ endPassed = true;
+ seekFlagEnd = v.seeking;
+ v.play();
+}
+
+function playbackEnded() {
+ ok(!completed, "should not be completed yet");
+
+ completed = true;
+ ok(startPassed, "seeking event");
+ ok(endPassed, "seeked event");
+ ok(seekFlagStart, "seeking flag on start should be true");
+ ok(!seekFlagEnd, "seeking flag on end should be false");
+ finish();
+}
+
+once(v, "ended", playbackEnded);
+once(v, "loadedmetadata", startTest);
+once(v, "seeking", seekStarted);
+once(v, "seeked", seekEnded);
+
+}
+
+manager.runTests(createTestArray(), startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_seek-10.html b/dom/media/test/test_seek-10.html
new file mode 100644
index 0000000000..6c02384ed4
--- /dev/null
+++ b/dom/media/test/test_seek-10.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: seek tests</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="seek_support.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// The data being used in these tests is specified in manifest.js.
+// The functions to build the test array and to run a specific test are in
+// seek_support.js.
+
+const SEEK_TEST_NUMBER = 10;
+
+function test_seek10(v, seekTime, is, ok, finish) {
+
+// Test bug 523335 - ensure that if we close a stream while seeking, we
+// don't hang during shutdown. This test won't "fail" per se if it's regressed,
+// it will instead start to cause random hangs in the mochitest harness on
+// shutdown.
+
+function startTest() {
+ // Must be duration*0.9 rather than seekTime, else we don't hit that problem.
+ // This is probably due to the seek bisection finishing too quickly, before
+ // we can close the stream.
+ v.currentTime = v.duration * 0.9;
+}
+
+function done(evt) {
+ ok(true, "We don't acutally test anything...");
+ finish();
+}
+
+function seeking() {
+ ok(v.currentTime >= seekTime - 0.1, "Video currentTime should be around " + seekTime + ": " + v.currentTime);
+ v.onerror = done;
+ v.src = "not a valid video file.";
+ v.load(); // Cause the existing stream to close.
+}
+
+v.addEventListener("loadeddata", startTest);
+v.addEventListener("seeking", seeking);
+
+}
+
+manager.runTests(createTestArray(), startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_seek-11.html b/dom/media/test/test_seek-11.html
new file mode 100644
index 0000000000..2207f684d7
--- /dev/null
+++ b/dom/media/test/test_seek-11.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: seek tests</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="seek_support.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// The data being used in these tests is specified in manifest.js.
+// The functions to build the test array and to run a specific test are in
+// seek_support.js.
+
+PARALLEL_TESTS = 1;
+const SEEK_TEST_NUMBER = 11;
+
+function test_seek11(v, seekTime, is, ok, finish) {
+
+// Test for bug 476973, multiple seeks to the same position shouldn't cause problems.
+
+var seekedNonZero = false;
+var completed = false;
+var target = 0;
+
+function startTest() {
+ if (completed)
+ return;
+ target = v.duration / 2;
+ v.currentTime = target;
+ v.currentTime = target;
+ v._seekTarget = target;
+}
+
+function startSeeking() {
+ ok(v.currentTime >= v._seekTarget - 0.1,
+ "Video currentTime should be around " + v._seekTarget + ": " + v.currentTime);
+ if (!seekedNonZero) {
+ v.currentTime = target;
+ v._seekTarget = target;
+ seekedNonZero = true;
+ }
+}
+
+function seekEnded() {
+ if (completed)
+ return;
+
+ if (v.currentTime > 0) {
+ ok(v.currentTime > target - 0.1 && v.currentTime < target + 0.1,
+ "Seek to wrong destination " + v.currentTime);
+ v.currentTime = 0.0;
+ v._seekTarget = 0.0;
+ } else {
+ ok(seekedNonZero, "Successfully seeked to nonzero");
+ ok(true, "Seek back to zero was successful");
+ completed = true;
+ finish();
+ }
+}
+
+v.addEventListener("loadedmetadata", startTest);
+v.addEventListener("seeking", startSeeking);
+v.addEventListener("seeked", seekEnded);
+
+}
+
+manager.runTests(createTestArray(), startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_seek-12.html b/dom/media/test/test_seek-12.html
new file mode 100644
index 0000000000..28decabadc
--- /dev/null
+++ b/dom/media/test/test_seek-12.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: seek tests</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="seek_support.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// The data being used in these tests is specified in manifest.js.
+// The functions to build the test array and to run a specific test are in
+// seek_support.js.
+
+const SEEK_TEST_NUMBER = 12;
+
+function test_seek12(v, seekTime, is, ok, finish) {
+var completed = false;
+
+function startTest() {
+ if (completed)
+ return;
+ ok(!v.seeking, "seeking should default to false");
+ v.currentTime = seekTime;
+ is(v.currentTime, seekTime, "currentTime must report seek target immediately");
+ is(v.seeking, true, "seeking flag on start should be true");
+}
+
+function seekStarted() {
+ if (completed)
+ return;
+ //is(v.currentTime, seekTime, "seeking: currentTime must be seekTime");
+ ok(Math.abs(v.currentTime - seekTime) < 0.01, "seeking: currentTime must be seekTime");
+}
+
+function seekEnded() {
+ if (completed)
+ return;
+ completed = true;
+ //is(v.currentTime, seekTime, "seeked: currentTime must be seekTime");
+ ok(Math.abs(v.currentTime - seekTime) < 0.01, "seeked: currentTime must be seekTime");
+ is(v.seeking, false, "seeking flag on end should be false");
+ finish();
+}
+
+v.addEventListener("loadedmetadata", startTest);
+v.addEventListener("seeking", seekStarted);
+v.addEventListener("seeked", seekEnded);
+}
+
+manager.runTests(createTestArray(), startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_seek-13.html b/dom/media/test/test_seek-13.html
new file mode 100644
index 0000000000..81eda9b658
--- /dev/null
+++ b/dom/media/test/test_seek-13.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: seek tests</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="seek_support.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// The data being used in these tests is specified in manifest.js.
+// The functions to build the test array and to run a specific test are in
+// seek_support.js.
+
+const SEEK_TEST_NUMBER = 13;
+
+function test_seek13(v, seekTime, is, ok, finish) {
+var completed = false;
+
+function startTest() {
+ if (completed)
+ return;
+ ok(!v.seeking, "seeking should default to false");
+ v.currentTime = v.duration;
+ is(v.currentTime, v.duration, "currentTime must report seek target immediately");
+ is(v.seeking, true, "seeking flag on start should be true");
+}
+
+function seekStarted() {
+ if (completed)
+ return;
+ //is(v.currentTime, v.duration, "seeking: currentTime must be duration");
+ ok(Math.abs(v.currentTime - v.duration) < 0.01,
+ "seeking: currentTime (" + v.currentTime + ") must be duration (" + v.duration + ")");
+}
+
+function seekEnded() {
+ if (completed)
+ return;
+ //is(v.currentTime, v.duration, "seeked: currentTime must be duration");
+ ok(Math.abs(v.currentTime - v.duration) < 0.01,
+ "seeked: currentTime (" + v.currentTime + ") must be duration (" + v.duration + ")");
+ is(v.seeking, false, "seeking flag on end should be false");
+}
+
+function playbackEnded() {
+ if (completed)
+ return;
+ completed = true;
+ //is(v.currentTime, v.duration, "ended: currentTime must be duration");
+ ok(Math.abs(v.currentTime - v.duration) < 0.01,
+ "ended: currentTime (" + v.currentTime + ") must be duration (" + v.duration + ")");
+ is(v.seeking, false, "seeking flag on end should be false");
+ is(v.ended, true, "ended must be true");
+ finish();
+}
+
+v.addEventListener("loadedmetadata", startTest);
+v.addEventListener("seeking", seekStarted);
+v.addEventListener("seeked", seekEnded);
+v.addEventListener("ended", playbackEnded);
+}
+
+manager.runTests(createTestArray(), startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_seek-14.html b/dom/media/test/test_seek-14.html
new file mode 100644
index 0000000000..7e19fe3bd3
--- /dev/null
+++ b/dom/media/test/test_seek-14.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: seek tests</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="seek_support.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+const SEEK_TEST_NUMBER = 14;
+
+function test_seek14(v, seekTime, is, ok, finish) {
+ var completed = false;
+
+ function startTest() {
+ v.play();
+ v.currentTime = v.duration;
+ }
+
+ function playbackEnded() {
+ if (completed) {
+ ok(false, "'ended' should only fire once.");
+ return;
+ }
+ completed = true;
+ // Finish the test after 700ms. We should receive only one 'ended' event.
+ setTimeout(finish, 700);
+ }
+
+ v.addEventListener("loadedmetadata", startTest);
+ v.addEventListener("ended", playbackEnded);
+}
+
+manager.runTests(createTestArray(), startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_seek-2.html b/dom/media/test/test_seek-2.html
new file mode 100644
index 0000000000..7b666df961
--- /dev/null
+++ b/dom/media/test/test_seek-2.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: seek tests</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="seek_support.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// The data being used in these tests is specified in manifest.js.
+// The functions to build the test array and to run a specific test are in
+// seek_support.js.
+
+PARALLEL_TESTS = 1;
+const SEEK_TEST_NUMBER = 2;
+
+function test_seek2(v, seekTime, is, ok, finish) {
+
+// Test seeking works if current time is set before video is
+// playing.
+var startPassed = false;
+var endPassed = false;
+var completed = false;
+
+function startTest() {
+ if (completed)
+ return;
+
+ v.currentTime=seekTime;
+ v.play();
+}
+
+function seekStarted() {
+ if (completed)
+ return;
+
+ ok(v.currentTime >= seekTime - 0.1, "Video currentTime should be around " + seekTime + ": " + v.currentTime);
+ startPassed = true;
+}
+
+function seekEnded() {
+ if (completed)
+ return;
+
+ endPassed = true;
+}
+
+function playbackEnded() {
+ if (completed)
+ return;
+
+ completed = true;
+ ok(startPassed, "send seeking event");
+ ok(endPassed, "send seeked event");
+ ok(v.ended, "Checking playback has ended");
+ ok(Math.abs(v.currentTime - v.duration) <= 0.1, "Checking currentTime at end: " + v.currentTime);
+ finish();
+}
+
+v.addEventListener("ended", playbackEnded);
+v.addEventListener("loadedmetadata", startTest);
+v.addEventListener("seeking", seekStarted);
+v.addEventListener("seeked", seekEnded);
+
+}
+
+manager.runTests(createTestArray(), startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_seek-3.html b/dom/media/test/test_seek-3.html
new file mode 100644
index 0000000000..c030f03d20
--- /dev/null
+++ b/dom/media/test/test_seek-3.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: seek tests</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="seek_support.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// The data being used in these tests is specified in manifest.js.
+// The functions to build the test array and to run a specific test are in
+// seek_support.js.
+
+const SEEK_TEST_NUMBER = 3;
+
+function test_seek3(v, seekTime, is, ok, finish) {
+
+// Test seeking works if current time is set but video is not played.
+var completed = false;
+var gotTimeupdate = false;
+
+function startTest() {
+ if (completed)
+ return;
+
+ v.currentTime=seekTime;
+}
+
+function timeupdate() {
+ gotTimeupdate = true;
+ v.removeEventListener("timeupdate", timeupdate);
+}
+
+function seekStarted() {
+ if (completed)
+ return;
+
+ ok(v.currentTime >= seekTime - 0.1, "Video currentTime should be around " + seekTime + ": " + v.currentTime);
+ v.addEventListener("timeupdate", timeupdate);
+}
+
+function seekEnded() {
+ if (completed)
+ return;
+
+ var t = v.currentTime;
+ ok(Math.abs(t - seekTime) <= 0.1, "Video currentTime should be around " + seekTime + ": " + t);
+ ok(gotTimeupdate, "Should have got timeupdate between seeking and seekended");
+ completed = true;
+ finish();
+}
+
+v.addEventListener("loadedmetadata", startTest);
+v.addEventListener("seeking", seekStarted);
+v.addEventListener("seeked", seekEnded);
+
+}
+
+manager.runTests(createTestArray(), startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_seek-4.html b/dom/media/test/test_seek-4.html
new file mode 100644
index 0000000000..4e5c1fee59
--- /dev/null
+++ b/dom/media/test/test_seek-4.html
@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: seek tests</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="seek_support.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// The data being used in these tests is specified in manifest.js.
+// The functions to build the test array and to run a specific test are in
+// seek_support.js.
+
+const SEEK_TEST_NUMBER = 4;
+
+function test_seek4(v, seekTime, is, ok, finish) {
+
+// Test for a seek, followed by another seek before the first is complete.
+var seekCount = 0;
+var completed = false;
+
+function startTest() {
+ if (completed)
+ return;
+
+ v.currentTime=seekTime;
+ v._seekTarget=seekTime;
+}
+
+function seekStarted() {
+ if (completed)
+ return;
+
+ seekCount += 1;
+
+ ok(v.currentTime >= v._seekTarget - 0.1,
+ "Video currentTime should be around " + v._seekTarget + ": " + v.currentTime);
+ if (seekCount == 1) {
+ v.currentTime=seekTime/2;
+ v._seekTarget=seekTime/2;
+ }
+}
+
+function seekEnded() {
+ if (completed)
+ return;
+
+ if (seekCount == 2) {
+ ok(Math.abs(v.currentTime - seekTime/2) <= 0.1, "seek on target: " + v.currentTime);
+ completed = true;
+ finish();
+ }
+}
+
+v.addEventListener("loadedmetadata", startTest);
+v.addEventListener("seeking", seekStarted);
+v.addEventListener("seeked", seekEnded);
+
+}
+
+manager.runTests(createTestArray(), startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_seek-5.html b/dom/media/test/test_seek-5.html
new file mode 100644
index 0000000000..a86477f77c
--- /dev/null
+++ b/dom/media/test/test_seek-5.html
@@ -0,0 +1,69 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: seek tests</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="seek_support.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// The data being used in these tests is specified in manifest.js.
+// The functions to build the test array and to run a specific test are in
+// seek_support.js.
+
+const SEEK_TEST_NUMBER = 5;
+
+function test_seek5(v, seekTime, is, ok, finish) {
+
+// Test for a seek, followed by a play before the seek completes, ensure we play at the end of the seek.
+var startPassed = false;
+var endPassed = false;
+var completed = false;
+
+function startTest() {
+ if (completed)
+ return;
+
+ v.currentTime=seekTime;
+}
+
+function seekStarted() {
+ if (completed)
+ return;
+ ok(v.currentTime >= seekTime - 0.1, "Video currentTime should be around " + seekTime + ": " + v.currentTime);
+ startPassed = true;
+ v.play();
+}
+
+function seekEnded() {
+ if (completed)
+ return;
+ endPassed = true;
+}
+
+function playbackEnded() {
+ if (completed)
+ return;
+ ok(startPassed, "Got seeking event");
+ ok(endPassed, "Got seeked event");
+ completed = true;
+ finish();
+}
+
+v.addEventListener("ended", playbackEnded);
+v.addEventListener("loadedmetadata", startTest);
+v.addEventListener("seeking", seekStarted);
+v.addEventListener("seeked", seekEnded);
+
+}
+
+manager.runTests(createTestArray(), startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_seek-6.html b/dom/media/test/test_seek-6.html
new file mode 100644
index 0000000000..32554c4c2f
--- /dev/null
+++ b/dom/media/test/test_seek-6.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: seek tests</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="seek_support.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// The data being used in these tests is specified in manifest.js.
+// The functions to build the test array and to run a specific test are in
+// seek_support.js.
+
+const SEEK_TEST_NUMBER = 6;
+
+function test_seek6(v, seekTime, is, ok, finish) {
+
+// Test for bug identified by Chris Pearce in comment 40 on
+// bug 449159.
+var seekCount = 0;
+var completed = false;
+var interval;
+
+function poll() {
+ v.currentTime;
+}
+
+function startTest() {
+ if (completed)
+ return;
+ interval = setInterval(poll, 10);
+ v.currentTime = Math.random() * v.duration;
+}
+
+function seekEnded() {
+ if (completed)
+ return;
+
+ seekCount++;
+ ok(true, "Seek " + seekCount);
+ if (seekCount == 3) {
+ clearInterval(interval);
+ completed = true;
+ finish();
+ } else {
+ v.currentTime = Math.random() * v.duration;
+ }
+}
+
+v.addEventListener("loadedmetadata", startTest);
+v.addEventListener("seeked", seekEnded);
+
+}
+
+manager.runTests(createTestArray(), startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_seek-7.html b/dom/media/test/test_seek-7.html
new file mode 100644
index 0000000000..96139d6f83
--- /dev/null
+++ b/dom/media/test/test_seek-7.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: seek tests</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="seek_support.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// The data being used in these tests is specified in manifest.js.
+// The functions to build the test array and to run a specific test are in
+// seek_support.js.
+
+const SEEK_TEST_NUMBER = 7;
+
+function test_seek7(v, seekTime, is, ok, finish) {
+
+// If a NaN is passed to currentTime, make sure this is caught
+// otherwise an infinite loop in the Ogg backend occurs.
+var completed = false;
+var thrown1 = false;
+var thrown3 = false;
+
+function startTest() {
+ if (completed)
+ return;
+
+ try {
+ v.currentTime = NaN;
+ } catch(e) {
+ thrown1 = true;
+ }
+
+ try {
+ v.currentTime = Math.random;
+ } catch(e) {
+ thrown3 = true;
+ }
+
+ completed = true;
+ ok(thrown1, "Setting currentTime to invalid value of NaN");
+ ok(thrown3, "Setting currentTime to invalid value of a function");
+ finish();
+}
+
+v.addEventListener("loadedmetadata", startTest);
+
+}
+
+manager.runTests(createTestArray(), startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_seek-8.html b/dom/media/test/test_seek-8.html
new file mode 100644
index 0000000000..2f6e390eef
--- /dev/null
+++ b/dom/media/test/test_seek-8.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: seek tests</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="seek_support.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// The data being used in these tests is specified in manifest.js.
+// The functions to build the test array and to run a specific test are in
+// seek_support.js.
+
+const SEEK_TEST_NUMBER = 8;
+
+function test_seek8(v, seekTime, is, ok, finish) {
+
+function startTest() {
+ v.currentTime = 1000;
+}
+
+function seekEnded() {
+ ok(Math.abs(v.currentTime - v.duration) < 0.2,
+ "currentTime " + v.currentTime + " close to " + v.duration);
+ finish();
+}
+
+v.addEventListener("loadedmetadata", startTest);
+v.addEventListener("seeked", seekEnded);
+
+}
+
+manager.runTests(createTestArray(), startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_seek-9.html b/dom/media/test/test_seek-9.html
new file mode 100644
index 0000000000..c03f6a75e3
--- /dev/null
+++ b/dom/media/test/test_seek-9.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: seek tests</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="seek_support.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// The data being used in these tests is specified in manifest.js.
+// The functions to build the test array and to run a specific test are in
+// seek_support.js.
+
+const SEEK_TEST_NUMBER = 9;
+
+function test_seek9(v, seekTime, is, ok, finish) {
+
+function startTest() {
+ v.currentTime = -1000;
+}
+
+function seekEnded() {
+ is(v.currentTime, 0, "currentTime clamped to 0");
+ finish();
+}
+
+v.addEventListener("loadedmetadata", startTest);
+v.addEventListener("seeked", seekEnded);
+
+}
+
+manager.runTests(createTestArray(), startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_seekLies.html b/dom/media/test/test_seekLies.html
new file mode 100644
index 0000000000..f3141eaabd
--- /dev/null
+++ b/dom/media/test/test_seekLies.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: server lies about range requests</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="manifest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body onunload="mediaTestCleanup();">
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+function on_metadataloaded() {
+ var v = document.getElementById('v');
+ var d = Math.round(v.duration*1000);
+ ok(d == 4000, "Checking duration: " + d);
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+<video id='v'
+ preload="metadata"
+ src='seekLies.sjs'
+ onloadedmetadata='on_metadataloaded();'></video>
+</body>
+</html>
diff --git a/dom/media/test/test_seekToNextFrame.html b/dom/media/test/test_seekToNextFrame.html
new file mode 100644
index 0000000000..755d06e622
--- /dev/null
+++ b/dom/media/test/test_seekToNextFrame.html
@@ -0,0 +1,95 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test seekToNextFrame of media files that should play OK</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+function startTest(test, token) {
+ var video = document.createElement('video');
+ video.preload = "metadata";
+ video.token = token;
+ video.seenSeeking = false;
+ video.seenEnded = false;
+
+ var handler = {
+ "ontimeout": function() {
+ Log(token, "timed out: ended=" + video.seenEnded);
+ }
+ };
+ manager.started(token, handler);
+
+ video.src = test.name;
+ video.name = test.name;
+
+ function callSeekToNextFrame() {
+ video.seekToNextFrame().then(
+ () => {
+ if (!video.seenSeeking) {
+ ok(false, video.token + ": Should have already received seeking event.");
+ }
+ video.seenSeeking = false;
+ if (!video.ended) {
+ callSeekToNextFrame();
+ }
+ },
+ () => {
+ ok(false, video.token + ": seekToNextFrame() failed.");
+ }
+ );
+ }
+
+ var onLoadedmetadata = function(t, v) { return function() {
+ callSeekToNextFrame();
+ }}(test, video);
+
+ var finish = function() {
+ video.finished = true;
+ video.removeEventListener("loadedmetadata", onLoadedmetadata);
+ video.removeEventListener("seeking", onSeeking);
+ removeNodeAndSource(video);
+ manager.finished(video.token);
+ }
+
+ var onEnded = function(t, v) { return function() {
+ v.seenEnded = true;
+ finish();
+ }}(test, video);
+
+ var onSeeking = function(t, v) { return function() {
+ if (v.seenSeeking) {
+ ok(false, v.token + ": Should yet receive seeking event.");
+ }
+ v.seenSeeking = true;
+ }}(test, video);
+
+ video.addEventListener("loadedmetadata", onLoadedmetadata);
+ video.addEventListener("seeking", onSeeking);
+ video.addEventListener("ended", onEnded);
+
+ document.body.appendChild(video);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv(
+ {
+ "set": [
+ ["media.seekToNextFrame.enabled", true ],
+ ["media.dormant-on-pause-timeout-ms", -1]
+ ]
+ },
+ function() {
+ manager.runTests(gSeekToNextFrameTests, startTest);
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_seek_duration.html b/dom/media/test/test_seek_duration.html
new file mode 100644
index 0000000000..11ee4cb534
--- /dev/null
+++ b/dom/media/test/test_seek_duration.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: seek tests</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="seek_support.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/**
+ * This test is used to make sure video's duration won't be changed when it
+ * reachs to the end after seeking to position where the time is very close to
+ * video's end time.
+ */
+
+SimpleTest.waitForExplicitFinish();
+
+(async function startTest()
+{
+ const video = document.createElement('video');
+ video.src = "bunny.webm";
+ document.body.appendChild(video);
+
+ const loadedMetadata = once(video, "loadedmetadata");
+ const canplay = once(video, "canplay");
+ const end = once(video, "ended");
+
+ info(`- wait for video loading metadata -`);
+ await loadedMetadata;
+ const originalDuration = video.duration;
+
+ info(`- seek video to the position which is close to end time -`);
+ // video's duration is 2.1 and the last key frame is in 2.0, we want to seek
+ // to that keyframe.
+ video.currentTime = originalDuration - 0.1;
+
+ info(`- play video until it ends -`);
+ await canplay;
+ await video.play();
+ await end;
+
+ ok(video.duration === originalDuration, `Duration shouldn't change`);
+ removeNodeAndSource(video);
+
+ SimpleTest.finish();
+})();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_seek_negative.html b/dom/media/test/test_seek_negative.html
new file mode 100644
index 0000000000..98f0b9c910
--- /dev/null
+++ b/dom/media/test/test_seek_negative.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: seeking to a negative time with readyState HAVE_NOTHING</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+function startTest(test, token) {
+ var type = getMajorMimeType(test.type);
+ var v = document.createElement(type);
+ v.token = token;
+ manager.started(token);
+
+ // Seek to negative start time.
+ v.currentTime = -123;
+ is(v.readyState, v.HAVE_NOTHING, "readyState is HAVE_NOTHING");
+ ok(!v.seeking, "can't be seeking prior src defined");
+ is(v.currentTime, -123, "currentTime is original seek time");
+
+ v.src = test.name;
+
+ // Initialize running variables.
+ v._name = test.name;
+ v._seekStarted = false;
+ v._seekCompleted = false;
+ v._metadata = false;
+
+ var events = [ "suspend", "play", "canplay", "canplaythrough", "loadstart",
+ "loadedmetadata", "loadeddata", "playing", "ended", "error",
+ "stalled", "emptied", "abort", "waiting", "pause" ];
+ function logEvent(e) {
+ var video = e.target;
+ Log(e.target.token, "got " + e.type + " with currentTime = " + video.currentTime);
+ }
+ events.forEach(function(e) {
+ v.addEventListener(e, logEvent);
+ });
+
+ once(v, "seeking", function() {
+ v._seekStarted = true;
+ ok(v.currentTime >= 0, "currentTime should be positive");
+ });
+ once(v, "seeked", function() {
+ v._seekCompleted = true;
+ ok(v.currentTime >= 0, "currentTime should be positive");
+ });
+ once(v, "loadedmetadata", function() {
+ v._metadata = true;
+ ok(v.seeking, "element is seeking once readyState is HAVE_METADATA");
+ ok(v.currentTime >= 0, "currentTime should be positive");
+ });
+ once(v, "ended", function() {
+ ok(v._seekStarted, "seek should have started");
+ ok(v._seekCompleted, "seek should have completed");
+ ok(v._metadata, "loadedmetadata fired");
+ ok(v.currentTime >= 0, "currentTime should be positive");
+ removeNodeAndSource(v);
+ manager.finished(v.token);
+ });
+
+ v.play();
+}
+
+
+manager.runTests(gSmallTests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_seek_nosrc.html b/dom/media/test/test_seek_nosrc.html
new file mode 100644
index 0000000000..56461007bd
--- /dev/null
+++ b/dom/media/test/test_seek_nosrc.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: seek tests</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+var SEEK_TIME = 3.5;
+var seekStarted = false;
+var seekCompleted = false;
+var metadata = false;
+
+var v = document.createElement('video');
+document.body.appendChild(v);
+SimpleTest.registerCleanupFunction(function () {
+ v.remove();
+});
+
+try {
+ v.currentTime = SEEK_TIME;
+} catch (e) {
+ ok(false, "should not fire '" + e + "' event");
+}
+is(v.readyState, v.HAVE_NOTHING, "readyState is HAVE_NOTHING");
+ok(!v.seeking, "can't be seeking prior src defined");
+is(v.currentTime, SEEK_TIME, "currentTime is default playback start position");
+once(v, "seeking", function() {
+ seekStarted = true;
+});
+once(v, "seeked", function() {
+ seekCompleted = true;
+});
+once(v, "loadedmetadata", function() {
+ metadata = true;
+ ok(v.seeking, "element is seeking once readyState is HAVE_METADATA");
+});
+once(v, "ended", function() {
+ ok(seekStarted, "seek should have started");
+ ok(seekCompleted, "seek should have completed");
+ ok(metadata, "loadedmetadata fired");
+ ok(v.currentTime >= SEEK_TIME, "currentTime should be after seek time");
+ SimpleTest.finish();
+});
+
+v.src = "seek.webm";
+v.play();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_seek_out_of_range.html b/dom/media/test/test_seek_out_of_range.html
new file mode 100644
index 0000000000..a477ebdd19
--- /dev/null
+++ b/dom/media/test/test_seek_out_of_range.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: seeking off the end of a file</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+// Test if the ended event works correctly.
+
+async function initTest(test, token) {
+ var type = getMajorMimeType(test.type);
+ var v = document.createElement(type);
+ v.preload = "auto";
+ v.token = token;
+ manager.started(token);
+ v.src = test.name;
+ v._name = test.name;
+ document.body.appendChild(v);
+
+ await once(v, "loadedmetadata");
+ info(`${v._name}: seeking to the end of the media.`);
+ v.currentTime = 3.0 * v.duration;
+ // Wait for 'seeked' and 'ended' to be fired.
+ await Promise.all([once(v, "seeked"), once(v, "ended")]);
+ // Check currentTime is near the end of the media.
+ ok(Math.abs(v.duration - v.currentTime) < 0.1,
+ "Should be at end of media for " + v._name + " t=" + v.currentTime + " d=" + v.duration);
+ // Call play() to start playback from the beginning.
+ v.play();
+ await once(v, "ended");
+ ok(v.ended, "Checking ended set after seeking to EOF and playing for " + v._name);
+ removeNodeAndSource(v);
+ manager.finished(v.token);
+}
+
+manager.runTests(gSmallTests, initTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_seek_promise_bug1344357.html b/dom/media/test/test_seek_promise_bug1344357.html
new file mode 100644
index 0000000000..6c441bc1cf
--- /dev/null
+++ b/dom/media/test/test_seek_promise_bug1344357.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: bug 1344357</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script>
+
+// This test always succeeds at runtime but should not cause any leaks at the end of mochitests.
+
+let manager = new MediaTestManager;
+
+function initTest(test, token) {
+ manager.started(token);
+
+ var win = window.open();
+ var video = win.document.createElement("video");
+ video.autoplay = true;
+ video.src = "http://example.com/tests/dom/media/test/" + test.name;
+ win.document.body.appendChild(video);
+ video.currentTime = test.duration / 2;
+ video.addEventListener("seeking", () => {
+ win.close();
+ manager.finished(token);
+ }, true);
+}
+
+manager.runTests(gSmallTests, initTest);
+
+</script> \ No newline at end of file
diff --git a/dom/media/test/test_seekable1.html b/dom/media/test/test_seekable1.html
new file mode 100644
index 0000000000..45c84b35df
--- /dev/null
+++ b/dom/media/test/test_seekable1.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test seekable member for media elements</title>
+<script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+<script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id='test'>
+<script class="testbody" type='application/javascript'>
+
+let manager = new MediaTestManager;
+
+function finish_test(element) {
+ if (element.parentNode)
+ element.remove();
+ element.src="";
+ manager.finished(element.token);
+}
+
+var tests = [
+// Test using a finite media stream, and a server supporting range requests
+{
+setup(element) {
+ is(element.seekable.length, 0, "seekable.length should be initialy 0.");
+ element.addEventListener("loadedmetadata", function() {
+ is(element.seekable.length, 1, "seekable.length should be 1 for a server supporting range requests.");
+
+ is(element.seekable.start(0), 0.0, "The start of the first range should be the initialTime.");
+ is(element.seekable.end(0), element.duration, "The end of the first range should be the duration.")
+ finish_test(element);
+ });
+ }
+}
+];
+
+function createTestArray() {
+ var A = [];
+ for (var k=0; k < gProgressTests.length; k++) {
+ var t = {};
+ t.setup = tests[0].setup;
+ t.name = gProgressTests[k].name;
+ t.type = gProgressTests[k].type;
+ A.push(t);
+ }
+ return A;
+}
+
+function startTest(test, token) {
+ var elemType = getMajorMimeType(test.type);
+ var element = document.createElement(elemType);
+ element.preload = "auto";
+ element.src = test.name;
+ element.token = token;
+ test.setup(element);
+ manager.started(token);
+}
+
+manager.runTests(createTestArray(), startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_source.html b/dom/media/test/test_source.html
new file mode 100644
index 0000000000..944e95b1df
--- /dev/null
+++ b/dom/media/test/test_source.html
@@ -0,0 +1,91 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: append source child</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<video id="v1"></video>
+<audio id="a1"></audio>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var v1 = document.getElementById("v1");
+var a1 = document.getElementById("a1");
+v1.preload = "auto";
+a1.preload = "auto";
+
+is(v1.src, "", "src should be null");
+is(a1.src, "", "src should be null");
+is(v1.currentSrc, "", "currentSrc should be null");
+is(a1.currentSrc, "", "currentSrc should be null");
+is(v1.childNodes.length, 0, "should have no children");
+is(a1.childNodes.length, 0, "should have no children");
+
+function newSource(filter) {
+ var candidates = gSmallTests.filter(function(x){return filter.test(x.type);});
+ if (candidates.length) {
+ var e = document.createElement("source");
+ e.type = candidates[0].type;
+ e.src = candidates[0].name;
+ return e;
+ }
+ return null
+
+}
+
+var audioLoaded = false;
+var videoLoaded = false;
+
+function loaded(e) {
+ var media = e.target;
+ ok(media.networkState > 0, "networkState should be > 0");
+ is(media.childNodes.length, 1, "should have 1 child");
+ var sourceFile = media.currentSrc.substring(media.currentSrc.lastIndexOf('/')+1);
+ var resource = media.firstChild.src.substring(media.firstChild.src.lastIndexOf('/')+1);
+ is(sourceFile, resource, "loaded wrong resource!");
+ if (media == a1)
+ audioLoaded = true;
+ else if (media == v1)
+ videoLoaded = true;
+ if (audioLoaded && videoLoaded) {
+ SimpleTest.finish();
+ }
+}
+
+v1.addEventListener('loadeddata', loaded);
+a1.addEventListener('loadeddata', loaded);
+
+var videoSource = newSource(/^video/);
+if (videoSource) {
+ v1.appendChild(videoSource);
+ v1.load();
+} else {
+ // No video backends? Don't test anything.
+ videoLoaded = true;
+}
+
+var audioSource = newSource(/^audio/);
+if (audioSource) {
+ a1.appendChild(audioSource);
+ a1.load();
+} else {
+ audioLoaded = true;
+}
+
+if (!audioLoaded && !videoLoaded) {
+ SimpleTest.waitForExplicitFinish();
+} else {
+ if (audioLoaded) {
+ todo(false, "No audio types supported");
+ }
+ if (videoLoaded) {
+ todo(false, "No video types supported");
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_source_null.html b/dom/media/test/test_source_null.html
new file mode 100644
index 0000000000..485edafd2d
--- /dev/null
+++ b/dom/media/test/test_source_null.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=752087
+-->
+<head>
+ <title>Test for Bug 752087</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=752087">Mozilla Bug 752087</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<video id="v">
+<script>
+var v = document.getElementById('v');
+v.src = null; // crashes on NULL access if not handled
+
+ok(true, "setting video.src to null didn't crash!");
+var srcPath = v.src.split('/');
+ok(srcPath.length >= 3, "src should be within dom/media/test");
+is(srcPath[srcPath.length - 1], "null", "Setting src to null is handled like 'null' string");
+</script>
+</video>
+</body>
+</html>
diff --git a/dom/media/test/test_source_write.html b/dom/media/test/test_source_write.html
new file mode 100644
index 0000000000..29fffebd10
--- /dev/null
+++ b/dom/media/test/test_source_write.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=462455
+-->
+<head>
+ <title>Test for Bug 462455</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=462455">Mozilla Bug 462455</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+
+<video id="v">
+<script>
+var loadStarted = false;
+document.write('Pause parsing!');
+var v = document.getElementById('v');
+
+var resource = getPlayableVideo(gSmallTests);
+if (resource != null) {
+ var s = document.createElement("source");
+ s.src = resource.name;
+ s.type = resource.type;
+ v.appendChild(s);
+}
+
+ok(v.networkState == HTMLMediaElement.NETWORK_NO_SOURCE,
+ "We shouldn't start network load until the current task returns.");
+</script>
+</video>
+</body>
+</html>
diff --git a/dom/media/test/test_standalone.html b/dom/media/test/test_standalone.html
new file mode 100644
index 0000000000..620878a394
--- /dev/null
+++ b/dom/media/test/test_standalone.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: standalone video documents</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body onload="doTest()">
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var iframes = [];
+
+for (let i=0; i<gSmallTests.length; ++i) {
+ var test = gSmallTests[i];
+
+ // We can't play WAV files in stand alone documents, so just don't
+ // run the test on non-video content types.
+ var tag = getMajorMimeType(test.type);
+ if (tag != "video" || !document.createElement("video").canPlayType(test.type))
+ continue;
+
+ let f = document.createElement("iframe");
+ f.src = test.name;
+ f._test = test;
+ f.id = "frame" + i;
+ iframes.push(f);
+ document.body.appendChild(f);
+}
+
+
+function filename(uri) {
+ return uri.substr(uri.lastIndexOf("/")+1);
+}
+
+function doTest()
+{
+ for (let i=0; i<iframes.length; ++i) {
+ let f = document.getElementById(iframes[i].id);
+ var v = f.contentDocument.body.firstChild;
+ is(v.tagName.toLowerCase(), "video", "Is video element");
+ var src = filename(v.currentSrc);
+ is(src, iframes[i]._test.name, "Name ("+src+") should match ("+iframes[i]._test.name+")");
+ is(v.controls, true, "Controls set (" + src + ")");
+ is(v.autoplay, true, "Autoplay set (" + src + ")");
+ }
+ SimpleTest.finish();
+}
+
+if (!iframes.length) {
+ todo(false, "No types supported");
+} else {
+ SimpleTest.waitForExplicitFinish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_streams_capture_origin.html b/dom/media/test/test_streams_capture_origin.html
new file mode 100644
index 0000000000..13d5589c0d
--- /dev/null
+++ b/dom/media/test/test_streams_capture_origin.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Bug 1189506</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1189506">Mozilla Bug 1189506</a>
+<p id="display"></p>
+<video id="vin"></video>
+<video id="vout"></video>
+<video id="vout_cors" crossorigin></video>
+<canvas id="cin" width="40" height="30"></canvas>
+<canvas id="cout" width="40" height="30"></canvas>
+<canvas id="cout_cors" width="40" height="30"></canvas>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script type="application/javascript">
+/* global vin, vout, vout_cors, cin, cout, cout_cors */
+
+/** Test for Bug 1189506 **/
+
+SimpleTest.waitForExplicitFinish();
+
+async function start() {
+ const resource = getPlayableVideo(gSmallTests).name;
+ vin.src = "http://example.org:8000/tests/dom/media/test/" + resource;
+ vin.preload = "metadata";
+
+ await new Promise(r => vin.onloadedmetadata = r);
+ vout.srcObject = vin.mozCaptureStreamUntilEnded();
+ vout_cors.srcObject = vin.mozCaptureStreamUntilEnded();
+ vin.play();
+ vout.play();
+ vout_cors.play();
+
+ await new Promise(r => vout.onended = r);
+ is(vin.ended, vout.ended, "Source media element ends first");
+
+ const ctxin = SpecialPowers.wrap(cin.getContext("2d"));
+ ctxin.drawImage(vin, 0, 0);
+
+ {
+ info("Testing that the last frame is rendered");
+ const powerCtx = SpecialPowers.wrap(cout.getContext("2d"));
+ powerCtx.drawImage(vout, 0, 0);
+ const datain = ctxin.getImageData(0, 0, cin.width, cin.height);
+ const dataout = powerCtx.getImageData(0, 0, cout.width, cout.height);
+ for (let i = 0; i < datain.data.length; i += 4) {
+ const pixelin = datain.data.slice(i, i + 4).join(',');
+ const pixelout = dataout.data.slice(i, i + 4).join(',');
+ if (pixelin != pixelout) {
+ is(pixelout, pixelin, `Pixel #${i/4} is rendered as expected`);
+ break;
+ }
+ }
+ is(datain.data.length / 4, cin.width * cin.height,
+ "Checked expected number of pixels");
+ }
+
+ {
+ info("Testing that the principal is set");
+ const ctx = cout.getContext("2d");
+ ctx.drawImage(vout, 0, 0);
+ SimpleTest.doesThrow(() => ctx.getImageData(0, 0, cout.width, cout.height),
+ "SecurityError");
+ }
+
+ {
+ info("Testing that the crossorigin attribute is ignored for MediaStreams");
+ const ctx = cout_cors.getContext("2d");
+ ctx.drawImage(vout_cors, 0, 0);
+ SimpleTest.doesThrow(
+ () => ctx.getImageData(0, 0, cout_cors.width, cout_cors.height),
+ "SecurityError");
+ }
+}
+
+(async () => {
+ try { await start(); }
+ catch(e) { ok(false, `Rejected with ${e}`); }
+ finally { SimpleTest.finish(); }
+})();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_streams_element_capture.html b/dom/media/test/test_streams_element_capture.html
new file mode 100644
index 0000000000..93f7fce53e
--- /dev/null
+++ b/dom/media/test/test_streams_element_capture.html
@@ -0,0 +1,120 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test that a MediaStream captured from one element plays back in another</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+const manager = new MediaTestManager();
+
+function checkDrawImage(vout, msg) {
+ const canvas = document.createElement("canvas");
+ const ctx = canvas.getContext("2d");
+ ctx.drawImage(vout, 0, 0);
+ const imgData = ctx.getImageData(0, 0, 1, 1);
+ is(imgData.data[3], 255, msg);
+}
+
+function isGreaterThanOrEqualEps(a, b, msg) {
+ ok(a >= b, `Got ${a}, expected at least ${b}; ${msg}`);
+}
+
+async function startTest(test, token) {
+ manager.started(token);
+ const v = document.createElement('video');
+ const vout = document.createElement('video');
+
+ v.token = token;
+ v.id = "MediaDecoder";
+ vout.id = "MediaStream";
+
+ document.body.appendChild(vout);
+
+ // Log events for debugging.
+ const events = ["suspend", "play", "canplay", "canplaythrough", "loadstart", "loadedmetadata",
+ "loadeddata", "playing", "ended", "error", "stalled", "emptied", "abort",
+ "waiting", "pause"];
+ function logEvent(e) {
+ Log(token, `${e.target.id} got ${e.type}`);
+ }
+ for (const e of events) {
+ v.addEventListener(e, logEvent);
+ vout.addEventListener(e, logEvent);
+ };
+
+ v.src = test.name;
+ v.preload = 'metadata';
+ await new Promise(r => v.onloadedmetadata = r);
+
+ const stream = v.mozCaptureStreamUntilEnded();
+ vout.srcObject = stream;
+ is(vout.srcObject, stream,
+ `${token} set output element .srcObject correctly`);
+ // Wait for the resource fetch algorithm to have run, so that the media
+ // element is hooked up to the MediaStream and ready to go. If we don't do
+ // this, we're not guaranteed to render the very first video frame, which
+ // can make this test fail the drawImage test when a video resource only
+ // contains one frame.
+ await new Promise(r => vout.onloadstart = r);
+ v.play();
+ vout.play();
+
+ await Promise.race([
+ Promise.all([
+ new Promise(r => vout.onended = r),
+ new Promise(r => v.onended = r),
+ ]),
+ new Promise((res, rej) => vout.onerror = () => rej(new Error(vout.error.message))),
+ new Promise((res, rej) => v.onerror = () => rej(new Error(v.error.message))),
+ ]);
+
+ let duration = test.duration;
+ if (typeof(test.contentDuration) == "number") {
+ duration = test.contentDuration;
+ }
+ if (duration) {
+ isGreaterThanOrEqualEps(vout.currentTime, duration,
+ `${token} current time at end`);
+ }
+ is(vout.readyState, vout.HAVE_CURRENT_DATA,
+ `${token} checking readyState`);
+ ok(vout.ended, `${token} checking playback has ended`);
+ isnot(stream.getTracks().length, 0, `${token} results in some tracks`);
+ if (stream.getVideoTracks().length) {
+ ok(test.type.match(/^video/), `${token} is a video resource`);
+ checkDrawImage(vout, `${token} checking video frame pixel has been drawn`);
+ }
+ vout.remove();
+ removeNodeAndSource(v);
+}
+
+(async () => {
+ SimpleTest.requestCompleteLog();
+ SimpleTest.waitForExplicitFinish();
+ await SpecialPowers.pushPrefEnv(
+ { "set": [
+ ["privacy.reduceTimerPrecision", false],
+ // This test exhibits bug 1543980 with RDD enabled.
+ ["media.rdd-process.enabled", false],
+ ]});
+ let tests = gPlayTests;
+ // Filter out bug1377278.webm due to bug 1541401.
+ tests = tests.filter(t => !t.name.includes("1377278"));
+
+ manager.runTests(tests, async (test, token) => {
+ try {
+ await startTest(test, token);
+ } catch(e) {
+ ok(false, `Caught exception for ${token}: ${e}`);
+ }
+ manager.finished(token);
+ });
+})();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_streams_element_capture_mediatrack.html b/dom/media/test/test_streams_element_capture_mediatrack.html
new file mode 100644
index 0000000000..6b4332d8af
--- /dev/null
+++ b/dom/media/test/test_streams_element_capture_mediatrack.html
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test that a media element captureStream works when disabling MediaTracks</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+const manager = new MediaTestManager();
+
+async function startTest(test, token) {
+ manager.started(token);
+ const v = document.createElement('video');
+
+ document.body.appendChild(v);
+ v.token = token;
+ v.id = "MediaDecoder";
+
+ // Log events for debugging.
+ const events = ["suspend", "play", "canplay", "canplaythrough", "loadstart", "loadedmetadata",
+ "loadeddata", "playing", "ended", "error", "stalled", "emptied", "abort",
+ "waiting", "pause"];
+ function logEvent(e) {
+ Log(token, `${token}: ${e.target.id} got ${e.type}`);
+ }
+ for (const e of events) {
+ v.addEventListener(e, logEvent);
+ };
+
+ v.src = test.name;
+ v.preload = 'metadata';
+ await new Promise(r => v.onloadedmetadata = r);
+
+ const stream = v.mozCaptureStream();
+
+ is(stream.getAudioTracks().length, Math.min(1, v.audioTracks.length),
+ `${token}: Expected number of audio tracks`);
+ is(stream.getVideoTracks().length, Math.min(1, v.videoTracks.length),
+ `${token}: Expected number of video tracks`);
+
+ if (v.audioTracks.length) {
+ v.audioTracks[0].enabled = false;
+ const track = stream.getAudioTracks()[0];
+ await new Promise(r => track.onended = r);
+ is(track.readyState, "ended", `${token}: Audio track has ended on removal`);
+ await new Promise(r => stream.onremovetrack = r);
+ is(stream.getAudioTracks().length, 0,
+ `${token}: Audio track was removed on removetrack`);
+ }
+
+ if (v.videoTracks.length) {
+ v.videoTracks[0].selected = false;
+ const track = stream.getVideoTracks()[0];
+ await new Promise(r => track.onended = r);
+ is(track.readyState, "ended", `${token}: Video track has ended on removal`);
+ await new Promise(r => stream.onremovetrack = r);
+ is(stream.getVideoTracks().length, 0,
+ `${token}: Video track was removed on removetrack`);
+ }
+
+ stream.onaddtrack = () => ok(false, "Unexpected addtrack event");
+
+ v.play();
+
+ await Promise.race([
+ new Promise(r => v.ontimeupdate = r),
+ new Promise((res, rej) => v.onerror = () => rej(new Error(v.error.message))),
+ ]);
+
+ is(stream.getTracks().length, 0, `${token}: no tracks appeared during playback`);
+ removeNodeAndSource(v);
+}
+
+(async () => {
+ SimpleTest.requestCompleteLog();
+ SimpleTest.waitForExplicitFinish();
+ await SpecialPowers.pushPrefEnv(
+ { "set": [
+ ["media.track.enabled", true],
+ ]});
+ let tests = gPlayTests;
+ // Filter out bug1377278.webm due to bug 1541401.
+ tests = tests.filter(t => !t.name.includes("1377278"));
+
+ manager.runTests(tests, async (test, token) => {
+ try {
+ await startTest(test, token);
+ } catch(e) {
+ ok(false, `Caught exception for ${token}: ${e}`);
+ }
+ manager.finished(token);
+ });
+})();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_streams_element_capture_playback.html b/dom/media/test/test_streams_element_capture_playback.html
new file mode 100644
index 0000000000..ce083069ef
--- /dev/null
+++ b/dom/media/test/test_streams_element_capture_playback.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test that capturing a stream doesn't stop the underlying element from firing events</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<audio id="a"></audio>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+var a = document.getElementById('a');
+var validTimeUpdate = false;
+
+function startTest() {
+ a.src = "big.wav";
+ var context = new AudioContext();
+ var node = context.createMediaElementSource(a);
+ node.connect(context.destination);
+ a.addEventListener("timeupdate", function() {
+ if (a.currentTime > 0.0 && a.currentTime < 5.0 && !validTimeUpdate) {
+ validTimeUpdate = true;
+ ok(true, "Received reasonable currentTime in a timeupdate");
+ SimpleTest.finish();
+ }
+ });
+ a.addEventListener("ended", function() {
+ if (!validTimeUpdate) {
+ ok(false, "Received reasonable currentTime in a timeupdate");
+ SimpleTest.finish();
+ }
+ });
+ a.play();
+}
+
+if (a.canPlayType("audio/wave")) {
+ startTest();
+} else {
+ todo(false, "No playable audio");
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_streams_element_capture_reset.html b/dom/media/test/test_streams_element_capture_reset.html
new file mode 100644
index 0000000000..625b0fe23f
--- /dev/null
+++ b/dom/media/test/test_streams_element_capture_reset.html
@@ -0,0 +1,174 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test that reloading, pausing and seeking in a media element that's being captured behaves as expected</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script src="manifest.js"></script>
+</head>
+<body>
+<video id="v"></video>
+<video id="vout"></video>
+<video id="vout_untilended"></video>
+<pre id="test">
+<script>
+const v = document.getElementById('v');
+const vout = document.getElementById('vout');
+const vout_untilended = document.getElementById('vout_untilended');
+
+function dumpEvent(event) {
+ const video = event.target;
+ info(
+ `${video.name}:${video.id} GOT EVENT ${event.type} ` +
+ `currentTime=${video.currentTime} paused=${video.paused} ` +
+ `ended=${video.ended} readyState=${video.readyState}`
+ );
+}
+
+function unexpected(event) {
+ ok(false, `${event.type} event received on ${event.target.id} unexpectedly`);
+};
+
+const events = ["timeupdate", "seeking", "seeked", "ended", "playing", "pause"];
+for (const e of events) {
+ v.addEventListener(e, dumpEvent);
+ vout.addEventListener(e, dumpEvent);
+ vout_untilended.addEventListener(e, dumpEvent);
+}
+
+function isWithinEps(a, b, msg) {
+ ok(Math.abs(a - b) < 0.01,
+ "Got " + a + ", expected " + b + "; " + msg);
+}
+
+function isGreaterThanOrEqualEps(a, b, msg) {
+ ok(a >= b - 0.01,
+ "Got " + a + ", expected at least " + b + "; " + msg);
+}
+
+async function startTest(test) {
+ const seekTime = test.duration/2;
+ const contentDuration = test.contentDuration ?? test.duration;
+
+ v.src = test.name;
+ v.name = test.name;
+ vout.name = test.name;
+ vout_untilended.name = test.name;
+ v.preload = "metadata";
+ await new Promise(r => v.onloadedmetadata = r);
+
+ vout.srcObject = v.mozCaptureStream();
+ vout.play();
+
+ vout_untilended.srcObject = v.mozCaptureStreamUntilEnded();
+ vout_untilended.play();
+
+ for (const track of [
+ ...vout.srcObject.getTracks(),
+ ...vout_untilended.srcObject.getTracks(),
+ ]) {
+ ok(track.muted, `${track.kind} track ${track.id} should be muted`);
+ }
+
+ v.play();
+
+ await Promise.all([
+ ...vout.srcObject.getTracks(),
+ ...vout_untilended.srcObject.getTracks()
+ ].map(t => new Promise(r => t.onunmute = r)));
+
+ await new Promise(r => v.onended = r);
+ isGreaterThanOrEqualEps(v.currentTime, test.duration,
+ "checking v.currentTime at first 'ended' event");
+
+ await Promise.all([
+ new Promise(r => vout.onended = r),
+ new Promise(r => vout_untilended.onended = r),
+ ]);
+
+ isGreaterThanOrEqualEps(vout.currentTime, contentDuration,
+ "checking vout.currentTime at first 'ended' event");
+ ok(vout.ended, "checking vout has actually ended");
+ ok(vout_untilended.ended, "checking vout_untilended has actually ended");
+
+ vout_untilended.srcObject.onaddtrack = unexpected;
+ vout_untilended.onplaying = unexpected;
+ vout_untilended.onended = unexpected;
+
+ const voutPreSeekCurrentTime = vout.currentTime;
+ v.currentTime = seekTime;
+ await new Promise(r => v.onseeked = r);
+
+ is(v.currentTime, seekTime, "Finished seeking");
+ is(vout.currentTime, voutPreSeekCurrentTime,
+ "checking vout.currentTime has not changed after seeking");
+
+ v.play();
+ vout.play();
+
+ await new Promise(r => v.onended = r);
+ isGreaterThanOrEqualEps(v.currentTime, test.duration,
+ "checking v.currentTime at second 'ended' event");
+
+ await new Promise(r => vout.onended = r);
+ isGreaterThanOrEqualEps(vout.currentTime,
+ (test.duration - seekTime) + contentDuration,
+ "checking vout.currentTime after seeking and playing through again");
+
+ v.src = test.name + "?1";
+ vout.play();
+ await v.play();
+
+ isnot(vout.srcObject.getTracks().length, 0, "There are some output tracks");
+
+ vout.onended = unexpected;
+ vout.srcObject.onremovetrack = unexpected;
+
+ v.pause();
+ await Promise.all(
+ vout.srcObject.getTracks().map(t => new Promise(r => t.onmute = r))
+ );
+
+ for (const track of vout.srcObject.getTracks()) {
+ track.onunmute = unexpected;
+ }
+
+ v.currentTime = 0;
+ await new Promise(r => v.onseeked = r);
+
+ v.play();
+ await Promise.all(
+ vout.srcObject.getTracks().map(t => new Promise(r => t.onunmute = r))
+ );
+
+ vout.srcObject.onremovetrack = null;
+
+ await new Promise(r => v.onended = r);
+ isGreaterThanOrEqualEps(v.currentTime, test.duration,
+ "checking v.currentTime at third 'ended' event");
+
+ await new Promise(r => vout.onended = r);
+ isGreaterThanOrEqualEps(vout.currentTime,
+ (test.duration - seekTime) + contentDuration*2,
+ "checking vout.currentTime after seeking, playing through and reloading");
+}
+
+(async () => {
+ SimpleTest.waitForExplicitFinish();
+ try {
+ const testVideo = getPlayableVideo(gSmallTests);
+ if (testVideo) {
+ await startTest(testVideo);
+ } else {
+ todo(false, "No playable video");
+ }
+ } catch(e) {
+ ok(false, `Error: ${e}`);
+ } finally {
+ SimpleTest.finish();
+ }
+})();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_streams_element_capture_twice.html b/dom/media/test/test_streams_element_capture_twice.html
new file mode 100644
index 0000000000..0e30be1801
--- /dev/null
+++ b/dom/media/test/test_streams_element_capture_twice.html
@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test that capturing a media element, then reloading and capturing again, works</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<video id="v"></video>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+const v = document.getElementById('v');
+
+function dumpEvent(event) {
+ const video = event.target;
+ info(video.name + " GOT EVENT " + event.type +
+ " currentTime=" + video.currentTime +
+ " paused=" + video.paused +
+ " ended=" + video.ended +
+ " readyState=" + video.readyState);
+}
+
+const events = ["timeupdate", "seeking", "seeked", "ended", "playing", "pause"];
+for (let i = 0; i < events.length; ++i) {
+ v.addEventListener(events[i], dumpEvent);
+}
+
+async function startTest(src) {
+ v.preload = "metadata";
+ v.src = src;
+ await new Promise(r => v.onloadedmetadata = r);
+ const s1 = v.mozCaptureStream();
+ const tracks = s1.getTracks();
+ is(tracks.length, 2, "Expected total tracks, s1, capture 1");
+ is(s1.getAudioTracks().length, 1, "Expected audio tracks, s1, capture 1");
+ is(s1.getVideoTracks().length, 1, "Expected video tracks, s1, capture 1");
+ is(s1.getAudioTracks()[0].readyState, "live", "Live audio, s1, capture 1");
+ is(s1.getVideoTracks()[0].readyState, "live", "Live video, s1, capture 1");
+
+ v.src = null;
+ for (let i = 0; i < tracks.length; ++i) {
+ await Promise.race(tracks.map(t => new Promise(r => t.onended = r)));
+ await new Promise(r => s1.onremovetrack = r);
+ }
+ is(s1.getTracks().length, 0, "Expected total tracks, s1, metadata 2");
+
+ v.src = src;
+ await new Promise(r => v.onloadedmetadata = r);
+ is(s1.getTracks().length, 2, "Expected total tracks, s1, metadata 2");
+ is(s1.getAudioTracks().length, 1, "Expected audio tracks, s1, metadata 2");
+ is(s1.getVideoTracks().length, 1, "Expected video tracks, s1, metadata 2");
+ is(s1.getAudioTracks()[0].readyState, "live", "Live audio, s1, metadata 2");
+ is(s1.getVideoTracks()[0].readyState, "live", "Live video, s1, metadata 2");
+
+ const s2 = v.mozCaptureStream();
+ is(s1.getTracks().length, 2, "Expected total tracks remains, s1, capture 2");
+ is(s2.getTracks().length, 2, "Expected total tracks, s2, capture 2");
+ is(s2.getAudioTracks().length, 1, "Expected audio tracks, s2, capture 2");
+ is(s2.getVideoTracks().length, 1, "Expected video tracks, s2, capture 2");
+ is(s2.getAudioTracks()[0].readyState, "live", "Live audio, s2, capture 2");
+ is(s2.getVideoTracks()[0].readyState, "live", "Live video, s2, capture 2");
+}
+
+(async function() {
+ try {
+ await startTest("short-video.ogv");
+ } catch(e) {
+ ok(false, `Caught error: ${e}${e.stack ? '\n' + e.stack : ''}`);
+ } finally {
+ SimpleTest.finish();
+ }
+})();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_streams_firstframe.html b/dom/media/test/test_streams_firstframe.html
new file mode 100644
index 0000000000..ea1dbd35c1
--- /dev/null
+++ b/dom/media/test/test_streams_firstframe.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test that a non-autoplaying, non-playing element with a MediaStream source triggers canplay and shows a first frame</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="manifest.js"></script>
+ <script src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+async function runTest() {
+ const canvas = document.createElement("canvas");
+ canvas.getContext("2d");
+ const helper = new CaptureStreamTestHelper2D(100, 100);
+
+ const video = document.createElement("video");
+ document.body.appendChild(video);
+
+ video.srcObject = canvas.captureStream();
+ helper.drawColor(canvas, helper.red);
+
+ await Promise.race([
+ new Promise(r => video.oncanplay = r),
+ new Promise(r => setTimeout(r, 30000))
+ .then(() => Promise.reject(new Error("Canplay timeout"))),
+ ]);
+
+ ok(true, "Got \"canplay\"");
+ is(video.readyState, video.HAVE_ENOUGH_DATA, "Expected readyState");
+ ok(helper.isPixel(helper.getPixel(video), helper.red),
+ "First frame is rendered before playing");
+
+ helper.drawColor(canvas, helper.green);
+ await helper.pixelMustNotBecome(video, helper.green, {
+ time: 1000,
+ infoString: "Rendered first frame doesn't change on new frame from source"
+ });
+ ok(helper.isPixel(helper.getPixel(video), helper.red),
+ "First frame is still rendered");
+
+ video.play();
+ helper.drawColor(canvas, helper.blue);
+ await helper.pixelMustBecome(video, helper.blue, {
+ infoString: "New frame gets rendered when playing"
+ });
+
+ video.srcObject.getTracks().forEach(t => t.stop());
+}
+
+(async function() {
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.requestFlakyTimeout("Explicit timeout reasons");
+ try {
+ await runTest();
+ } catch(e) {
+ ok(false, e);
+ }
+ SimpleTest.finish();
+})();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_streams_gc.html b/dom/media/test/test_streams_gc.html
new file mode 100644
index 0000000000..d2ba23042b
--- /dev/null
+++ b/dom/media/test/test_streams_gc.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test garbage collection of captured stream (bug 806754)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body onload="doTest()">
+<audio id="a" preload="metadata"></audio>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+var a = document.getElementById('a');
+a.src = getPlayableAudio(gSmallTests).name;
+
+function forceGC() {
+ SpecialPowers.gc();
+ SpecialPowers.forceGC();
+ SpecialPowers.forceCC();
+}
+
+function doTest() {
+ a.mozCaptureStreamUntilEnded();
+
+ a.addEventListener("seeked", function() {
+ a.play();
+
+ a.addEventListener("play", function() {
+ a.addEventListener("ended", function() {
+ ok(true, "GC completed OK");
+ SimpleTest.finish();
+ });
+ });
+ });
+
+ a.currentTime = a.duration;
+ setTimeout(forceGC, 0);
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_streams_individual_pause.html b/dom/media/test/test_streams_individual_pause.html
new file mode 100644
index 0000000000..c49b563d76
--- /dev/null
+++ b/dom/media/test/test_streams_individual_pause.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for bug 1073406. Pausing a video element should not pause another playing the same stream.</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="text/javascript" src="gUM_support.js"></script>
+</head>
+<body>
+<video id="video1" autoplay></video>
+<video id="video2" autoplay></video>
+<script class="testbody" type="text/javascript">
+function getVideoImagePixelData(v) {
+ const canvas = document.createElement("canvas");
+ const ctx = canvas.getContext("2d");
+ ctx.drawImage(v, 0, 0);
+ const imgData = ctx.getImageData(canvas.width/2, canvas.height/2, 1, 1).data;
+ return "r" + imgData[0] +
+ "g" + imgData[1] +
+ "b" + imgData[2] +
+ "a" + imgData[3];
+}
+
+async function startTest() {
+ // This test expects fake devices so that the video color will change
+ // over time, explicitly request fakes.
+ await pushGetUserMediaTestPrefs({fakeAudio: true, fakeVideo: true});
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ const video1 = document.getElementById('video1');
+ const video2 = document.getElementById('video2');
+
+ video1.srcObject = stream;
+ video2.srcObject = stream;
+
+ await new Promise(r => video1.onplaying = r);
+ video1.pause();
+ await new Promise(r => video1.onpause = r);
+
+ const v1PausedImageData = getVideoImagePixelData(video1);
+ const v2PausedImageData = getVideoImagePixelData(video2);
+
+ while (getVideoImagePixelData(video2) == v2PausedImageData) {
+ info("video2 has not progressed. Waiting.");
+ await new Promise(r => video2.ontimeupdate = r);
+ }
+
+ // Wait for a while to be sure video1 would have gotten a frame
+ // if it is playing.
+ for (let i = 3; i != 0; i--) {
+ await new Promise(r => video2.ontimeupdate = r);
+ }
+ info("video2 progressed OK");
+
+ isnot(video1.currentTime, video2.currentTime,
+ "v1 and v2 should not be at the same currentTime");
+ is(getVideoImagePixelData(video1), v1PausedImageData,
+ "video1 video frame should not have updated since video1 paused");
+
+ for (const t of stream.getTracks()) {
+ t.stop();
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+(async function() {
+ try {
+ await startTest();
+ } catch(error) {
+ ok(false, "getUserMedia should not fail, got " + error.name);
+ } finally {
+ SimpleTest.finish();
+ }
+})();
+</script>
+</body>
+</html>
diff --git a/dom/media/test/test_streams_srcObject.html b/dom/media/test/test_streams_srcObject.html
new file mode 100644
index 0000000000..8ea5797d14
--- /dev/null
+++ b/dom/media/test/test_streams_srcObject.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test interactions of src and srcObject</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body onload="doTests()">
+<audio id="a1"></audio>
+<audio id="a2"></audio>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+var doTest = srcObject => new Promise(resolve => {
+ var a = document.getElementById('a1');
+ a.src = getPlayableAudio(gSmallTests).name;
+ var b = new Audio();
+
+ var newSrc = a.src + "?2";
+ b.src = newSrc;
+ is(b[srcObject], null, "Initial srcObject is null");
+ var stream = a.mozCaptureStream();
+ b[srcObject] = stream;
+ is(b[srcObject], stream, "Stream set correctly");
+ try {
+ b[srcObject] = "invalid";
+ ok(false, "Setting srcObject to an invalid value should throw.");
+ } catch (e) {
+ ok(e instanceof TypeError, "Exception should be a TypeError");
+ }
+ is(b[srcObject], stream, "Stream not set to invalid value");
+ is(b.src, newSrc, "src attribute not affected by setting srcObject");
+ var step = 0;
+ b.addEventListener("loadedmetadata", function() {
+ if (step == 0) {
+ is(b.currentSrc, "", "currentSrc set to empty string while playing srcObject");
+ b[srcObject] = null;
+ is(b[srcObject], null, "Stream set to null");
+ // The resource selection algorithm will run again and choose b.src
+ } else if (step == 1) {
+ is(b.currentSrc, b.src, "currentSrc set to src now that srcObject is null");
+ resolve();
+ }
+ ++step;
+ });
+ a.play();
+ b.play();
+});
+
+var doTests = () => doTest("srcObject")
+ .catch(e => ok(false, "Unexpected error: " + e))
+ .then(() => SimpleTest.finish())
+ .catch(e => ok(false, "Coding error: " + e));
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_streams_tracks.html b/dom/media/test/test_streams_tracks.html
new file mode 100644
index 0000000000..845bc36ca3
--- /dev/null
+++ b/dom/media/test/test_streams_tracks.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test MediaStreamTrack interfaces</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+const manager = new MediaTestManager;
+
+function testTracks(tracks, hasTrack, kind, src) {
+ is(tracks.length, hasTrack ? 1 : 0, `Correct ${kind} track count for ${src}`);
+ for (const track of tracks) {
+ is(track.readyState, "live", `Track ${track.id} should still be live`);
+ is(track.kind, kind, `Correct track kind for track ${track.id} of ${src}`);
+ ok(/\{........-....-....-....-............\}/.test(track.id),
+ `id ${track.id} for ${track.kind} track of ${src} has correct form`);
+ }
+}
+
+async function startTest(test, token) {
+ try {
+ info(`Starting test of ${test.name}`);
+ const element = document.createElement("video");
+
+ element.token = token;
+ manager.started(token);
+
+ element.src = test.name;
+ element.test = test;
+ const stream = element.mozCaptureStreamUntilEnded();
+
+ element.play();
+
+ await new Promise(r => element.onloadedmetadata = r);
+
+ testTracks(stream.getAudioTracks(), test.hasAudio, "audio", test.name);
+ testTracks(stream.getVideoTracks(), test.hasVideo, "video", test.name);
+ const tracks = stream.getTracks();
+
+ await new Promise(r => element.onended = r);
+
+ for (let i = 0; i < tracks.length; ++i) {
+ await Promise.race(
+ tracks.map(t => new Promise(r => t.onended = r))
+ );
+ await new Promise(r => stream.onremovetrack = r);
+ }
+
+ testTracks(stream.getAudioTracks(), false, "audio", test.name);
+ testTracks(stream.getVideoTracks(), false, "video", test.name);
+ } catch(e) {
+ ok(false, `Caught error: ${e}`);
+ } finally {
+ manager.finished(token);
+ }
+}
+
+manager.runTests(gTrackTests, startTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_suspend_media_by_inactive_docshell.html b/dom/media/test/test_suspend_media_by_inactive_docshell.html
new file mode 100644
index 0000000000..7f819ca33b
--- /dev/null
+++ b/dom/media/test/test_suspend_media_by_inactive_docshell.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test suspending media by inactive docShell</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="manifest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<video id="testVideo" src="gizmo.mp4" loop></video>
+<script class="testbody" type="text/javascript">
+/**
+ * When calling `browser.suspendMediaWhenInactive`, it can set the docShell's
+ * corresponding flag that is used to suspend media when the docShell is
+ * inactive. This test is used to check if we can suspend/resume the media
+ * correctly when changing docShell's active state.
+ */
+async function startTest() {
+ const video = document.getElementById("testVideo");
+
+ info(`start video`);
+ await video.play();
+
+ info(`set docShell inactive which would suspend media`);
+ await setDocShellActive(false);
+
+ info(`set docShell active which would resume media`);
+ await setDocShellActive(true);
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv(
+ {"set": [["media.testing-only-events", true]]}, startTest);
+
+/**
+ * The following are test helper functions.
+ */
+function mediaSuspendedStateShouldEqualTo(expected) {
+ const video = document.getElementById("testVideo");
+ const result = SpecialPowers.wrap(video).isSuspendedByInactiveDocOrDocShell;
+ is(result, expected, `media's suspended state is correct`);
+}
+
+function setDocShellActive(isActive) {
+ const win = SpecialPowers.wrap(window);
+ const docShell = win.docShell;
+ const browsingContext = win.browsingContext;
+ // This flag is used to prevent media from playing when docShell is inactive.
+ browsingContext.top.suspendMediaWhenInactive = true;
+ browsingContext.isActive = isActive;
+ // After updating `docshell.isActive`, it would suspend/resume media and we
+ // wait suspending/resuming finishing by listening to `MozMediaSuspendChanged`
+ return new Promise(r => {
+ docShell.chromeEventHandler.addEventListener("MozMediaSuspendChanged",
+ () => {
+ mediaSuspendedStateShouldEqualTo(!isActive);
+ r();
+ }, {once : true}
+ );
+ });
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/media/test/test_temporary_file_blob_video_plays.html b/dom/media/test/test_temporary_file_blob_video_plays.html
new file mode 100644
index 0000000000..87f6b3c4e6
--- /dev/null
+++ b/dom/media/test/test_temporary_file_blob_video_plays.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test MediaRecorder Recording canvas stream</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<div id="content">
+</div>
+<script class="testbody" type="text/javascript">
+
+function startTest() {
+ var canvas = document.createElement("canvas");
+ canvas.width = canvas.height = 100;
+ document.getElementById("content").appendChild(canvas);
+
+ var helper = new CaptureStreamTestHelper2D(100, 100);
+ helper.drawColor(canvas, helper.red);
+
+ var stream = canvas.captureStream(0);
+
+ var blob;
+
+ let mediaRecorder = new MediaRecorder(stream);
+ is(mediaRecorder.stream, stream,
+ "Media recorder stream = canvas stream at the start of recording");
+
+ mediaRecorder.onwarning = () => ok(false, "warning unexpectedly fired");
+
+ mediaRecorder.onerror = () => ok(false, "Recording failed");
+
+ mediaRecorder.ondataavailable = ev => {
+ is(blob, undefined, "Should only get one dataavailable event");
+ blob = ev.data;
+ };
+
+ mediaRecorder.onstart = () => {
+ info("Got 'start' event");
+ // We just want one frame encoded, to see that the recorder produces something readable.
+ mediaRecorder.stop();
+ };
+
+ mediaRecorder.onstop = () => {
+ info("Got 'stop' event");
+ ok(blob, "Should have gotten a data blob");
+
+ var video = document.createElement("video");
+ video.id = "recorded-video";
+ video.src = URL.createObjectURL(blob);
+ video.play();
+ video.onerror = err => {
+ ok(false, "Should be able to play the recording. Got error. code=" + video.error.code);
+ SimpleTest.finish();
+ };
+ document.getElementById("content").appendChild(video);
+ helper.pixelMustBecome(video, helper.red, {
+ threshold: 128,
+ infoString: "Should become red",
+ }).then(SimpleTest.finish);
+ };
+
+ mediaRecorder.start();
+ is(mediaRecorder.state, "recording", "Media recorder should be recording");
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({set:[["media.recorder.max_memory", 1]]}, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_timeupdate_small_files.html b/dom/media/test/test_timeupdate_small_files.html
new file mode 100644
index 0000000000..fca35e5b71
--- /dev/null
+++ b/dom/media/test/test_timeupdate_small_files.html
@@ -0,0 +1,88 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=495319
+-->
+
+<head>
+ <title>Bug 495319 - playing back small audio files should fire timeupdate</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=495319">Mozilla Bug 495319</a>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+function ended(e) {
+ var v = e.target;
+ ++v.counter.ended;
+ is(v.counter.ended, 1, v._name + " should see ended only once");
+ ok(v.counter.timeupdate > 0, v._name + " should see at least one timeupdate: " + v.currentTime);
+
+ // Rest event counters for we don't allow events after ended.
+ eventsToLog.forEach(function(event) {
+ v.counter[event] = 0;
+ });
+
+ // Finish the test after 500ms. We shouldn't receive any timeupdate events
+ // after the ended event, so this gives time for any pending timeupdate events
+ // to fire so we can ensure we don't regress behaviour.
+ setTimeout(
+ function() {
+ // Remove the event listeners before removing the video from the document.
+ // We should receive a timeupdate and pause event when we remove the element
+ // from the document (as the element is specified to behave as if pause() was
+ // invoked when it's removed from a document), and we don't want those
+ // confusing the test results.
+ v.removeEventListener("ended", ended);
+ eventsToLog.forEach(function(event) {
+ v.removeEventListener(event, logEvent);
+ });
+ removeNodeAndSource(v);
+ manager.finished(v.token);
+ },
+ 500);
+}
+
+var eventsToLog = ["play", "canplay", "canplaythrough", "loadstart", "loadedmetadata",
+ "loadeddata", "playing", "timeupdate", "error", "stalled", "emptied", "abort",
+ "waiting", "pause"];
+
+function logEvent(event) {
+ var v = event.target;
+ ++v.counter[event.type];
+ if (v.counter.ended > 0) {
+ is(v.counter[event.type], 0, v._name + " got unexpected " + event.type + " after ended");
+ }
+}
+
+function startTest(test, token) {
+ var type = getMajorMimeType(test.type);
+ var v = document.createElement(type);
+ v.token = token;
+ manager.started(token);
+ v.src = test.name;
+ v._name = test.name;
+
+ // Keep how many events received for each event type.
+ v.counter = {};
+ eventsToLog.forEach(function(e) {
+ v.addEventListener(e, logEvent);
+ v.counter[e] = 0;
+ });
+ v.addEventListener("ended", ended);
+ v.counter.ended = 0;
+ document.body.appendChild(v);
+ v.play();
+}
+
+manager.runTests(gSmallTests, startTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_unseekable.html b/dom/media/test/test_unseekable.html
new file mode 100644
index 0000000000..52c7eccc50
--- /dev/null
+++ b/dom/media/test/test_unseekable.html
@@ -0,0 +1,101 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: unseekable</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/*
+
+Test that unseekable media can't be seeked. We load a media that shouldn't
+be seekable, and play through once. While playing through we repeatedly try
+to seek and check that nothing happens when we do. We also verify that the
+seekable ranges are empty.
+
+*/
+
+var manager = new MediaTestManager;
+
+var onseeking = function(event) {
+ var v = event.target;
+ v.actuallySeeked = true;
+};
+
+var onseeked = function(event) {
+ var v = event.target;
+ v.actuallySeeked = true;
+};
+
+var ontimeupdate = function(event) {
+ var v = event.target;
+
+ // Check that when we seek nothing happens.
+ var t = v.currentTime;
+ v.currentTime = v.currentTime /= 2;
+ ok(Math.abs(t - v.currentTime) < 0.01, "Current time shouldn't change when seeking in unseekable media: " + v.name);
+
+ // Check that the seekable ranges are empty.
+ is(v.seekable.length, 0, "Should have no seekable ranges in unseekable media: " + v.name);
+};
+
+var onended = function(event) {
+ var v = event.target;
+
+ // Remove the event listeners so that they can't run if there are any pending
+ // events.
+ v.removeEventListener("seeking", onseeking);
+ v.removeEventListener("seeked", onseeked);
+ v.removeEventListener("timeupdate", ontimeupdate);
+ v.removeEventListener("ended", onended);
+
+ v.src = "";
+ if (v.parentNode) {
+ v.remove();
+ }
+
+ // Verify that none of the seeks we did in timeupdate actually seeked.
+ ok(!v.actuallySeeked, "Should not be able to seek in unseekable media: " + v.name);
+
+ manager.finished(v.token);
+}
+
+function startTest(test, token) {
+ var v = document.createElement('video');
+ manager.started(token);
+ v.name = test.name;
+ v.src = test.name;
+ v.token = token;
+ v.autoplay = "true";
+
+ v.actuallySeeked = false;
+
+ v.addEventListener("seeking", onseeking);
+ v.addEventListener("seeked", onseeked);
+ v.addEventListener("timeupdate", ontimeupdate);
+ v.addEventListener("ended", onended);
+
+ document.body.appendChild(v);
+}
+
+function canPlay(candidates) {
+ var v = document.createElement("video");
+ var resources = candidates.filter(function(x){return v.canPlayType(x.type);});
+ return (resources.length);
+}
+
+if (canPlay(gUnseekableTests)) {
+ manager.runTests(gUnseekableTests, startTest);
+} else {
+ todo(false, "No files of supported format to test");
+}
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_videoDocumentTitle.html b/dom/media/test/test_videoDocumentTitle.html
new file mode 100644
index 0000000000..dd52dba26c
--- /dev/null
+++ b/dom/media/test/test_videoDocumentTitle.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=463830
+-->
+<head>
+ <title>Test for Bug 463830</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=463830">Mozilla Bug 463830</a>
+<p id="display"></p>
+<iframe id="i"></iframe>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 463830 **/
+
+var gTests = [
+ { file: "320x240.ogv", title: "320x240.ogv" },
+ { file: "bug461281.ogg", title: "bug461281.ogg" },
+];
+
+var gTestNum = 0;
+
+addLoadEvent(runTest);
+
+var title;
+var i = document.getElementById("i");
+
+function runTest() {
+ if (gTestNum == gTests.length) {
+ SimpleTest.finish();
+ return;
+ }
+ if (gTestNum == 0) {
+ i.addEventListener("load", function() {
+ is(i.contentDocument.title, title, "Doc title incorrect");
+ setTimeout(runTest, 0);
+ });
+ }
+
+ title = gTests[gTestNum].title;
+ i.src = gTests[gTestNum].file;
+ gTestNum++;
+}
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_videoPlaybackQuality_totalFrames.html b/dom/media/test/test_videoPlaybackQuality_totalFrames.html
new file mode 100644
index 0000000000..1b69a3b64f
--- /dev/null
+++ b/dom/media/test/test_videoPlaybackQuality_totalFrames.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Count the tatol frames of a video</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var manager = new MediaTestManager;
+
+var startTest = function(test, token) {
+ manager.started(token);
+ var v = document.createElement('video');
+ v.token = token;
+ document.body.appendChild(v);
+ v.src = test.name;
+
+ function ended(event) {
+ var video = event.target;
+ is(video.getVideoPlaybackQuality().totalVideoFrames, test.totalFrameCount,test.name+ " totalFrames should match!");
+ removeNodeAndSource(video);
+ manager.finished(video.token);
+ }
+ v.addEventListener("ended", ended);
+ v.play();
+};
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv(
+ {
+ "set": [
+ ["media.decoder.skip-to-next-key-frame.enabled", false],
+ ["media.av1.use-dav1d", true]
+ ]
+ },
+ function() {
+ manager.runTests(getPlayableVideos(gFrameCountTests), startTest);
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_video_dimensions.html b/dom/media/test/test_video_dimensions.html
new file mode 100644
index 0000000000..4d9c2a185f
--- /dev/null
+++ b/dom/media/test/test_video_dimensions.html
@@ -0,0 +1,96 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test that a video element has set video dimensions on loadedmetadata</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var manager = new MediaTestManager;
+
+var startTest = function(test, token) {
+ manager.started(token);
+ var v1 = document.createElement('video');
+ var v2 = document.createElement('video');
+ var vout = document.createElement('video');
+
+ // Avoid a race for hardware resources between v1 and v2 on platforms with
+ // a hardware decoder, like B2G.
+ v1.preload = 'none';
+ v2.preload = 'none';
+
+ var numVideoElementsFinished = 0;
+
+ var ondurationchange = function(ev) {
+ var v = ev.target;
+ info(v.testName + " got durationchange");
+ v.durationchange = true;
+ };
+ var onresize = function(ev) {
+ var v = ev.target;
+ info(v.testName + " got resize");
+ ok(!v.resize, v.testName + " should only fire resize once for same size");
+ v.resize = true;
+ ok(v.durationchange, v.testName +
+ " durationchange event should have been emitted before resize");
+ is(v.videoWidth, test.width, v.testName + " width should be set on resize");
+ is(v.videoHeight, test.height, v.testName + " height should be set on resize");
+ };
+ var onloadedmetadata = function(ev) {
+ var v = ev.target;
+ info(v.testName + " got loadedmetadata");
+ ok(!v.loadedmetadata, v.testName + " should only fire loadedmetadata once");
+ v.loadedmetadata = true;
+ ok(v.resize, v.testName +
+ " resize event should have been emitted before loadedmetadata");
+
+ numVideoElementsFinished += 1;
+ if (v === v1) {
+ removeNodeAndSource(v1);
+ v2.load();
+ }
+
+ if (v === v2) {
+ vout.srcObject = v2.mozCaptureStreamUntilEnded();
+ v2.play();
+ vout.play();
+ }
+
+ if (numVideoElementsFinished === 3) {
+ removeNodeAndSource(v2);
+ removeNodeAndSource(vout);
+ manager.finished(token);
+ }
+ };
+ var setupElement = function(v, id) {
+ v.durationchange = false;
+ v.ondurationchange = ondurationchange;
+ v.resize = false;
+ v.onresize = onresize;
+ v.loadedmetadata = false;
+ v.onloadedmetadata = onloadedmetadata;
+ document.body.appendChild(v);
+ };
+
+ v1.testName = test.name;
+ v2.testName = test.name + " (Captured)";
+ vout.testName = test.name + " (Stream)";
+
+ v1.src = test.name;
+ v2.src = test.name;
+
+ setupElement(v1, "v1");
+ setupElement(v2, "v2");
+ setupElement(vout, "vout");
+
+ v1.play();
+};
+
+manager.runTests(getPlayableVideos(gSmallTests), startTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_video_gzip_encoding.html b/dom/media/test/test_video_gzip_encoding.html
new file mode 100644
index 0000000000..355a245713
--- /dev/null
+++ b/dom/media/test/test_video_gzip_encoding.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Bug 1370177 gzipped mp4 with Content-Length</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+ </head>
+ <body>
+ <!--
+ Tests that an MP4 file served over a "Content-Encoding: gzip"
+ HTTP channel with a "Content-Length" header set to the length
+ of the compressed file works.
+ -->
+ <video id='v' src="http://mochi.test:8888/tests/dom/media/test/gzipped_mp4.sjs" controls autoplay></video>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ var v = document.getElementById('v');
+ v.addEventListener("ended", ()=>{
+ SimpleTest.finish();
+ mediaTestCleanup();
+ });
+ </script>
+ </body>
+</html>
diff --git a/dom/media/test/test_video_in_audio_element.html b/dom/media/test/test_video_in_audio_element.html
new file mode 100644
index 0000000000..a53adf9414
--- /dev/null
+++ b/dom/media/test/test_video_in_audio_element.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1060896
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1060896</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+ <script type="application/javascript">
+
+ /**
+ * Test for Bug 1060896; tests that loading a video inside an audio element works.
+ **/
+
+ var manager = new MediaTestManager;
+
+ function error(event) {
+ var a = event.target;
+ ok(!a.mozHasAudio, "Media must've had no active tracks to play");
+ a.removeEventListener("error", error);
+ a.removeEventListener("ended", ended);
+ removeNodeAndSource(a);
+ manager.finished(a.token);
+ }
+
+ function ended(event) {
+ var a = event.target;
+ a.removeEventListener("error", error);
+ a.removeEventListener("ended", ended);
+ removeNodeAndSource(a);
+ manager.finished(a.token);
+ }
+
+ function initTest(test, token) {
+ var a = document.createElement('audio');
+ a.token = token;
+ manager.started(token);
+ a.autoplay = true;
+
+ a.addEventListener("error", error);
+ a.addEventListener("ended", ended);
+
+ a.src = test.name;
+ }
+
+ var videos = getPlayableVideos(gSmallTests);
+ // Bug 1216012, skip the test on emulator-kk.
+ if (getAndroidVersion() == 19) {
+ todo(false, "Test disabled on emulator-kk.");
+ } else {
+ manager.runTests(videos, initTest);
+ }
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1060896">Mozilla Bug 1060896</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_video_low_power_telemetry.html b/dom/media/test/test_video_low_power_telemetry.html
new file mode 100644
index 0000000000..be609f7ceb
--- /dev/null
+++ b/dom/media/test/test_video_low_power_telemetry.html
@@ -0,0 +1,205 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1737682
+
+This is a test of the macOS video low power telemetry, defined as GFX_MACOS_VIDEO_LOW_POWER
+enums in Histograms.json. The test will load some video media files, play them for some
+number of frames, then check that appropriate telemetry has been recorded. Since the
+telemetry fires based on the number of frames painted, the test is structured around
+the `media.video_stats.enabled` pref, which allows us to monitor the number of frames
+that have been shown. The telemetry fires every 600 frames. To make sure we have enough
+painted frames to trigger telemetry, we run the media for many frames and then check
+that the expected telemetry value has been recorded at least once.
+
+The tests are run via the MediaTestManager, which loads and plays the video sequentially.
+Since we want to test different telemetry outcomes, we have some special setup and teardown
+steps we run before each video. We keep track of which test we are running with a simple
+1-based index, `testIndex`. So our test structure is:
+
+1) Set some initial preferences that need to be set for the whole test series.
+2) Start MediaTestManager with a set of media to play.
+3) When a video arrives, increment testIndex and call preTest.
+4) Run the video for PAINTED_FRAMES frames, then call postTest, which checks telemetry
+ and cleans up.
+5) Tell the MediaTestManager we've finished with that video, which triggers the next.
+-->
+
+<head>
+ <title>Test of macOS video low power telemetry</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1737682">Mozilla Bug 1737682</a>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+ // Parallel test must be disabled for media.video_stats.enabled is a global setting
+ // to prevent the setting from changing unexpectedly in the middle of the test.
+ PARALLEL_TESTS = 1;
+ SimpleTest.waitForExplicitFinish();
+
+ // How many frames do we run? Must be more than the value set by the
+ // gfx.core-animation.low-power-telemetry-frames pref, because that's how
+ // many frames must be shown before telemetry is emitted. We present for
+ // longer than that to ensure that the telemetry is emitted at least once.
+ // This also has to be enough frames to overcome the animated "<video> is
+ // now fullscreen." popup that appears over the video. While that modal
+ // is visible and animating away, it will register as an overlay and
+ // prevent low power mode. As an estimate, we need at least 200 frames to
+ // ensure that animation has finished and there has been time to fire the
+ // telemetry at least once after it has finished.
+ const TELEMETRY_FRAMES = SpecialPowers.getIntPref("gfx.core-animation.low-power-telemetry-frames");
+ const PAINTED_FRAMES = Math.max(Math.floor(TELEMETRY_FRAMES * 1.1), 200 + TELEMETRY_FRAMES);
+ info(`Running each video for ${PAINTED_FRAMES} frames.`);
+
+ // Taken from TelemetryHistogramEnums.h
+ const GFX_MACOS_VIDEO_LOW_POWER_LowPower = 1;
+ const GFX_MACOS_VIDEO_LOW_POWER_FailWindowed = 3;
+
+ var manager = new MediaTestManager;
+
+ // Define some state variables that we'll use to manage multiple setup,
+ // check, and teardown steps using the same preTest and postTest functions.
+ var testIndex = 0;
+ var testsComplete = false;
+
+ async function retrieveSnapshotAndClearTelemetry() {
+ return SpecialPowers.spawnChrome([], () => {
+ let hist = Services.telemetry.getHistogramById("GFX_MACOS_VIDEO_LOW_POWER");
+ let snap = hist.snapshot();
+ hist.clear();
+ return snap;
+ });
+ }
+
+ function checkSnapshot(snap, category) {
+ if (!snap) {
+ return;
+ }
+
+ // For debugging purposes, build a string of the snapshot values hashmap.
+ let valuesString = '';
+ let keys = Object.keys(snap.values);
+ keys.forEach(k => {
+ valuesString += `[${k}] = ${snap.values[k]}, `;
+ });
+ info(`Test ${testIndex} telemetry values are ${valuesString}`);
+
+ // Since we've run the media for more frames than the telemetry needs to
+ // fire, we should have at least 1 of the expected value.
+ let val = snap.values[category];
+ if (!val) {
+ val = 0;
+ }
+ ok(val > 0, `Test ${testIndex} enum ${category} should have at least 1 occurrence; found ${val}.`);
+ }
+
+ async function doPreTest(v) {
+ if (testsComplete) {
+ manager.finished(v.token);
+ return;
+ }
+
+ switch (testIndex) {
+ case 1: {
+ // Test 1 (FailWindowed): No special setup.
+ break;
+ }
+
+ case 2: {
+ // Test 2 (LowPower): Enter fullscreen.
+
+ // Wait one frame to ensure the old video is no longer in the layer tree, which
+ // causes problems with the telemetry.
+ await new Promise(resolve => requestAnimationFrame(resolve));
+
+ info("Attempting to enter fullscreen.");
+ ok(document.fullscreenEnabled, "Document should permit fullscreen-ing of elements.");
+ await SpecialPowers.wrap(v).requestFullscreen();
+ ok(document.fullscreenElement, "Document should have one element in fullscreen.");
+ break;
+ }
+ }
+ }
+
+ async function doPostTest(v) {
+ info(`Test ${testIndex} attempting to retrieve telemetry.`);
+ let snap = await retrieveSnapshotAndClearTelemetry();
+ ok(snap, `Test ${testIndex} should have telemetry.`);
+
+ switch (testIndex) {
+ case 1: {
+ // Test 1 (FailWindowed): Just check.
+ checkSnapshot(snap, GFX_MACOS_VIDEO_LOW_POWER_FailWindowed);
+ break;
+ }
+
+ case 2: {
+ // Test 2 (LowPower): Check, then exit fullscreen.
+ checkSnapshot(snap, GFX_MACOS_VIDEO_LOW_POWER_LowPower);
+
+ info("Attempting to exit fullscreen.");
+ await SpecialPowers.wrap(document).exitFullscreen();
+ ok(!document.fullscreenElement, "Document should be out of fullscreen.");
+
+ // This is the last test.
+ testsComplete = true;
+ break;
+ }
+ }
+ }
+
+ function ontimeupdate(event) {
+ let v = event.target;
+ // Count painted frames to see when we should hit some telemetry threshholds.
+ if (v.mozPaintedFrames >= PAINTED_FRAMES) {
+ v.pause();
+ v.removeEventListener("timeupdate", ontimeupdate);
+
+ doPostTest(v).then(() => {
+ let token = v.token;
+ removeNodeAndSource(v);
+ manager.finished(token);
+ });
+ }
+ }
+
+ function startTest(test, token) {
+ manager.started(token);
+
+ testIndex++;
+ info(`Starting test ${testIndex} video ${test.name}.`);
+
+ let v = document.createElement('video');
+ v.addEventListener("timeupdate", ontimeupdate);
+ v.token = token;
+ v.src = test.name;
+ v.loop = true;
+ document.body.appendChild(v);
+
+ doPreTest(v).then(() => {
+ info(`Playing test ${testIndex}.`);
+ v.play();
+ });
+ }
+
+ SpecialPowers.pushPrefEnv({"set": [
+ ["media.video_stats.enabled", true],
+ ["gfx.core-animation.specialize-video", true],
+ ]},
+ async function() {
+ // Clear out existing telemetry in case previous tests were displaying
+ // video.
+ info("Clearing initial telemetry.");
+ await retrieveSnapshotAndClearTelemetry();
+
+ ok(gVideoLowPowerTests.length >= 2, "Should have enough videos to test.");
+ manager.runTests(gVideoLowPowerTests, startTest);
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_video_stats_resistfingerprinting.html b/dom/media/test/test_video_stats_resistfingerprinting.html
new file mode 100644
index 0000000000..2bc239b367
--- /dev/null
+++ b/dom/media/test/test_video_stats_resistfingerprinting.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+Mozilla Bug:
+https://bugzilla.mozilla.org/show_bug.cgi?id=1369309
+Tor Ticket:
+https://trac.torproject.org/projects/tor/ticket/15757
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1369309</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="manifest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=682299">Mozilla Bug 1369309</a>
+<a target="_blank" href="https://trac.torproject.org/projects/tor/ticket/15757">Tor Ticket 15757</a>
+
+<!-- The main testing script -->
+<script class="testbody" type="text/javascript">
+ var manager = new MediaTestManager;
+ const SPOOFED_FRAMES_PER_SECOND = 30;
+ const SPOOFED_DROPPED_RATIO = 0.05;
+ const MS_PER_TIME_ATOM = 100; // Not the default anymore, but what we test here.
+ // Push the setting of 'privacy.resistFingerprinting' into gTestPrefs, which
+ // will be set during MediaTestManager.runTests().
+ gTestPrefs.push(
+ ["privacy.resistFingerprinting", true],
+ ["privacy.resistFingerprinting.reduceTimerPrecision.microseconds", MS_PER_TIME_ATOM * 1000],
+ ["privacy.resistFingerprinting.reduceTimerPrecision.jitter", false],
+ // We use 240p as the target resolution since 480p is greater than every video
+ // source in our test suite, so we need to use 240p here for allowing us to
+ // test dropped rate here.
+ ["privacy.resistFingerprinting.target_video_res", 240]
+ );
+ var testCases = [
+ { name:"320x240.ogv", type:"video/ogg", width:320, height:240, duration:0.266, drop: false },
+ { name:"seek.webm", type:"video/webm", width:320, height:240, duration:3.966, drop: false },
+ { name:"gizmo.mp4", type:"video/mp4", width:560, height:320, duration:5.56, drop: true }
+ ];
+
+ function checkStats(v, shouldDrop) {
+ // Rounding the current time to 100ms.
+ const secondsPerAtom = MS_PER_TIME_ATOM / 1000;
+ const currentTimeAtoms = Math.floor(v.currentTime / secondsPerAtom);
+ const currentTime = currentTimeAtoms * secondsPerAtom;
+ let dropRate = 0;
+
+ if (shouldDrop) {
+ dropRate = SPOOFED_DROPPED_RATIO;
+ }
+
+ is(v.mozParsedFrames, parseInt(currentTime * SPOOFED_FRAMES_PER_SECOND, 10),
+ "mozParsedFrames should be spoofed if fingerprinting resistance is enabled");
+ is(v.mozDecodedFrames, parseInt(currentTime * SPOOFED_FRAMES_PER_SECOND, 10),
+ "mozDecodedFrames should be spoofed if fingerprinting resistance is enabled");
+ is(v.mozPresentedFrames,
+ parseInt(currentTime * SPOOFED_FRAMES_PER_SECOND * (1 - dropRate), 10),
+ "mozPresentedFrames should be spoofed if fingerprinting resistance is enabled");
+ is(v.mozPaintedFrames,
+ parseInt(currentTime * SPOOFED_FRAMES_PER_SECOND * (1 - dropRate), 10),
+ "mozPaintedFrames should be spoofed if fingerprinting resistance is enabled");
+ is(v.mozFrameDelay, 0.0,
+ "mozFrameDelay should be 0.0 if fingerprinting resistance is enabled");
+ let playbackQuality = v.getVideoPlaybackQuality();
+ is(playbackQuality.totalVideoFrames, parseInt(currentTime * SPOOFED_FRAMES_PER_SECOND, 10),
+ "VideoPlaybackQuality.totalVideoFrames should be spoofed if fingerprinting resistance is enabled");
+ is(playbackQuality.droppedVideoFrames, parseInt(currentTime * SPOOFED_FRAMES_PER_SECOND * dropRate, 10),
+ "VideoPlaybackQuality.droppedVideoFrames should be spoofed if fingerprinting resistance is enabled");
+ }
+
+ function startTest(test, token) {
+ let v = document.createElement("video");
+ v.token = token;
+ v.src = test.name;
+ manager.started(token);
+ once(v, "ended", () => {
+ checkStats(v, test.drop);
+ removeNodeAndSource(v);
+ manager.finished(v.token);
+ });
+ v.play();
+ }
+
+ manager.runTests(testCases, startTest);
+
+</script>
+</body>
+</html>
diff --git a/dom/media/test/test_video_to_canvas.html b/dom/media/test/test_video_to_canvas.html
new file mode 100644
index 0000000000..3267dc63f5
--- /dev/null
+++ b/dom/media/test/test_video_to_canvas.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=486646
+-->
+
+<head>
+ <title>Test for Bug 486646</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+
+var manager = new MediaTestManager;
+
+function loaded(e) {
+ var v = e.target;
+ ok(v.readyState >= v.HAVE_CURRENT_DATA,
+ "readyState must be >= HAVE_CURRENT_DATA for " + v._name);
+
+ var canvas = document.createElement("canvas");
+ canvas.width = v.videoWidth;
+ canvas.height = v.videoHeight;
+ document.body.appendChild(canvas);
+ var ctx = canvas.getContext("2d");
+ try {
+ ctx.drawImage(v, 0, 0);
+ ok(true, "Shouldn't throw exception while drawing to canvas from video for " + v._name);
+ } catch (ex) {
+ ok(false, "Shouldn't throw exception while drawing to canvas from video for " + v._name);
+ }
+
+ v._finished = true;
+ v.remove();
+ manager.finished(v.token);
+}
+
+function startTest(test, token) {
+ var type = getMajorMimeType(test.type);
+ if (type != "video")
+ return;
+
+ var v = document.createElement('video');
+ v.token = token;
+ manager.started(token);
+ v.src = test.name;
+ v._name = test.name;
+ v._finished = false;
+ v.autoplay = true;
+ v.style.display = "none";
+ v.addEventListener("ended", loaded);
+ document.body.appendChild(v);
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({"set": [["media.cache_size", 40000]]}, beginTest);
+function beginTest() {
+ manager.runTests(gSmallTests, startTest);
+}
+
+</script>
+</pre>
+
+</body>
+</html>
diff --git a/dom/media/test/test_volume.html b/dom/media/test/test_volume.html
new file mode 100644
index 0000000000..3d9e5bd91a
--- /dev/null
+++ b/dom/media/test/test_volume.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Media test: volume attribute set</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+
+<video id='v1'></video><audio id='a1'></audio>
+
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+function test(element, value, shouldThrow) {
+ var threw = null;
+ try {
+ element.volume = value;
+ } catch (ex) {
+ threw = ex.name;
+ }
+ is(shouldThrow, threw, "Case: " +element.id+ " setVolume=" + value);
+}
+
+
+var ids = [document.getElementById('v1'), document.getElementById('a1')];
+
+for (let i=0; i<ids.length; i++) {
+ var element = ids[i];
+ test(element, 0.0, null);
+ test(element, 1.0, null);
+ test(element, -0.1, "IndexSizeError");
+ test(element, 1.1, "IndexSizeError");
+ test(element, undefined, "TypeError");
+ test(element, NaN, "TypeError");
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_vp9_superframes.html b/dom/media/test/test_vp9_superframes.html
new file mode 100644
index 0000000000..c570561b31
--- /dev/null
+++ b/dom/media/test/test_vp9_superframes.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test that all VP9 frames are decoded (contains superframes)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+function test() {
+ var video = document.createElement("video");
+ video.src = "vp9-superframes.webm";
+ video.play();
+ video.addEventListener("ended", function () {
+ let vpq = video.getVideoPlaybackQuality();
+ is(vpq.totalVideoFrames, 120, "totalVideoFrames must contains 120 frames");
+ SimpleTest.finish();
+ });
+}
+
+addLoadEvent(function() {
+ test();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/test/test_wav_ended1.html b/dom/media/test/test_wav_ended1.html
new file mode 100644
index 0000000000..99f6e38243
--- /dev/null
+++ b/dom/media/test/test_wav_ended1.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Wave Media test: ended</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+// Test if the ended event works correctly.
+var endPassed = false;
+var completed = false;
+
+function startTest() {
+ if (completed)
+ return;
+ var v = document.getElementById('v');
+ v.play();
+}
+
+function playbackEnded() {
+ if (completed)
+ return;
+
+ var v = document.getElementById('v');
+ completed = true;
+ ok(v.currentTime >= 0.9 && v.currentTime <= 1.1,
+ "Checking currentTime at end: " + v.currentTime);
+ ok(v.ended, "Checking playback has ended");
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+<audio id='v'
+ onloadedmetadata='return startTest();'
+ onended='return playbackEnded();'>
+ <source type='audio/x-wav' src='r11025_s16_c1.wav'>
+</audio>
+</body>
+</html>
diff --git a/dom/media/test/test_wav_ended2.html b/dom/media/test/test_wav_ended2.html
new file mode 100644
index 0000000000..0c172139f0
--- /dev/null
+++ b/dom/media/test/test_wav_ended2.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Wave Media test: ended and replaying</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<!-- try with autoplay and no v.play in starttest, also with both -->
+<pre id="test">
+<script class="testbody" type="text/javascript">
+// Test if audio can be replayed after ended.
+var completed = false;
+var playingCount = 0;
+var endCount = 0;
+
+function startTest() {
+ if (completed)
+ return;
+
+ var v = document.getElementById('v');
+ v.play();
+}
+
+function playbackStarted() {
+ if (completed)
+ return;
+
+ playingCount++;
+}
+
+function playbackEnded() {
+ if (completed)
+ return;
+
+ endCount++;
+ var v = document.getElementById('v');
+ ok(v.currentTime >= 0.9 && v.currentTime <= 1.1,
+ "Checking currentTime at end: " + v.currentTime);
+ ok(v.ended, "Checking playback has ended");
+ ok(playingCount > 0, "Expect at least one playing event");
+ playingCount = 0;
+ if (endCount < 2) {
+ v.play();
+ } else {
+ ok(endCount == 2, "Check playback after ended event");
+ completed = true;
+ SimpleTest.finish();
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+<audio id='v'
+ onloadedmetadata='return startTest();'
+ onplaying='return playbackStarted();'
+ onended='return playbackEnded();'>
+ <source type='audio/x-wav' src='r11025_s16_c1.wav'>
+</audio>
+</body>
+</html>
diff --git a/dom/media/test/tone2s-silence4s-tone2s.opus b/dom/media/test/tone2s-silence4s-tone2s.opus
new file mode 100644
index 0000000000..e7d754926e
--- /dev/null
+++ b/dom/media/test/tone2s-silence4s-tone2s.opus
Binary files differ
diff --git a/dom/media/test/two-xing-header-no-content-length.mp3 b/dom/media/test/two-xing-header-no-content-length.mp3
new file mode 100644
index 0000000000..4d139e2886
--- /dev/null
+++ b/dom/media/test/two-xing-header-no-content-length.mp3
Binary files differ
diff --git a/dom/media/test/two-xing-header-no-content-length.mp3^headers^ b/dom/media/test/two-xing-header-no-content-length.mp3^headers^
new file mode 100644
index 0000000000..abfeb4ce28
--- /dev/null
+++ b/dom/media/test/two-xing-header-no-content-length.mp3^headers^
@@ -0,0 +1,3 @@
+HTTP 200 OK
+Content-Length: invalid
+Cache-Control: no-store
diff --git a/dom/media/test/variable-channel.ogg b/dom/media/test/variable-channel.ogg
new file mode 100644
index 0000000000..77e116889c
--- /dev/null
+++ b/dom/media/test/variable-channel.ogg
Binary files differ
diff --git a/dom/media/test/variable-channel.ogg^headers^ b/dom/media/test/variable-channel.ogg^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/variable-channel.ogg^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/variable-channel.opus b/dom/media/test/variable-channel.opus
new file mode 100644
index 0000000000..76b8991167
--- /dev/null
+++ b/dom/media/test/variable-channel.opus
Binary files differ
diff --git a/dom/media/test/variable-channel.opus^headers^ b/dom/media/test/variable-channel.opus^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/variable-channel.opus^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/variable-preskip.opus b/dom/media/test/variable-preskip.opus
new file mode 100644
index 0000000000..a82e831ce6
--- /dev/null
+++ b/dom/media/test/variable-preskip.opus
Binary files differ
diff --git a/dom/media/test/variable-preskip.opus^headers^ b/dom/media/test/variable-preskip.opus^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/variable-preskip.opus^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/variable-samplerate.ogg b/dom/media/test/variable-samplerate.ogg
new file mode 100644
index 0000000000..0c2726e835
--- /dev/null
+++ b/dom/media/test/variable-samplerate.ogg
Binary files differ
diff --git a/dom/media/test/variable-samplerate.ogg^headers^ b/dom/media/test/variable-samplerate.ogg^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/variable-samplerate.ogg^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/variable-samplerate.opus b/dom/media/test/variable-samplerate.opus
new file mode 100644
index 0000000000..1d15170798
--- /dev/null
+++ b/dom/media/test/variable-samplerate.opus
Binary files differ
diff --git a/dom/media/test/variable-samplerate.opus^headers^ b/dom/media/test/variable-samplerate.opus^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/variable-samplerate.opus^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/vbr-head.mp3 b/dom/media/test/vbr-head.mp3
new file mode 100644
index 0000000000..35f4105491
--- /dev/null
+++ b/dom/media/test/vbr-head.mp3
Binary files differ
diff --git a/dom/media/test/vbr-head.mp3^headers^ b/dom/media/test/vbr-head.mp3^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/vbr-head.mp3^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/vbr.mp3 b/dom/media/test/vbr.mp3
new file mode 100644
index 0000000000..38eb376a97
--- /dev/null
+++ b/dom/media/test/vbr.mp3
Binary files differ
diff --git a/dom/media/test/vbr.mp3^headers^ b/dom/media/test/vbr.mp3^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/vbr.mp3^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/very-short.mp3 b/dom/media/test/very-short.mp3
new file mode 100644
index 0000000000..7dbdb20c3a
--- /dev/null
+++ b/dom/media/test/very-short.mp3
Binary files differ
diff --git a/dom/media/test/video-overhang.ogg b/dom/media/test/video-overhang.ogg
new file mode 100644
index 0000000000..e11b28fb5b
--- /dev/null
+++ b/dom/media/test/video-overhang.ogg
Binary files differ
diff --git a/dom/media/test/video-overhang.ogg^headers^ b/dom/media/test/video-overhang.ogg^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/video-overhang.ogg^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/vp9-short.webm b/dom/media/test/vp9-short.webm
new file mode 100644
index 0000000000..16d32abee3
--- /dev/null
+++ b/dom/media/test/vp9-short.webm
Binary files differ
diff --git a/dom/media/test/vp9-short.webm^headers^ b/dom/media/test/vp9-short.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/vp9-short.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/vp9-superframes.webm b/dom/media/test/vp9-superframes.webm
new file mode 100644
index 0000000000..d695e42357
--- /dev/null
+++ b/dom/media/test/vp9-superframes.webm
Binary files differ
diff --git a/dom/media/test/vp9-superframes.webm^headers^ b/dom/media/test/vp9-superframes.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/vp9-superframes.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/vp9.webm b/dom/media/test/vp9.webm
new file mode 100644
index 0000000000..221877e303
--- /dev/null
+++ b/dom/media/test/vp9.webm
Binary files differ
diff --git a/dom/media/test/vp9.webm^headers^ b/dom/media/test/vp9.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/vp9.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/vp9cake-short.webm b/dom/media/test/vp9cake-short.webm
new file mode 100644
index 0000000000..2d353d98a7
--- /dev/null
+++ b/dom/media/test/vp9cake-short.webm
Binary files differ
diff --git a/dom/media/test/vp9cake-short.webm^headers^ b/dom/media/test/vp9cake-short.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/vp9cake-short.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/vp9cake.webm b/dom/media/test/vp9cake.webm
new file mode 100644
index 0000000000..4ea70ed302
--- /dev/null
+++ b/dom/media/test/vp9cake.webm
Binary files differ
diff --git a/dom/media/test/vp9cake.webm^headers^ b/dom/media/test/vp9cake.webm^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/vp9cake.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/wave_metadata.wav b/dom/media/test/wave_metadata.wav
new file mode 100644
index 0000000000..5e17547c30
--- /dev/null
+++ b/dom/media/test/wave_metadata.wav
Binary files differ
diff --git a/dom/media/test/wave_metadata.wav^headers^ b/dom/media/test/wave_metadata.wav^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/wave_metadata.wav^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/wave_metadata_bad_len.wav b/dom/media/test/wave_metadata_bad_len.wav
new file mode 100644
index 0000000000..b89c4818be
--- /dev/null
+++ b/dom/media/test/wave_metadata_bad_len.wav
Binary files differ
diff --git a/dom/media/test/wave_metadata_bad_len.wav^headers^ b/dom/media/test/wave_metadata_bad_len.wav^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/wave_metadata_bad_len.wav^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/wave_metadata_bad_no_null.wav b/dom/media/test/wave_metadata_bad_no_null.wav
new file mode 100644
index 0000000000..18063048c3
--- /dev/null
+++ b/dom/media/test/wave_metadata_bad_no_null.wav
Binary files differ
diff --git a/dom/media/test/wave_metadata_bad_no_null.wav^headers^ b/dom/media/test/wave_metadata_bad_no_null.wav^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/wave_metadata_bad_no_null.wav^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/wave_metadata_bad_utf8.wav b/dom/media/test/wave_metadata_bad_utf8.wav
new file mode 100644
index 0000000000..b6f2a675b2
--- /dev/null
+++ b/dom/media/test/wave_metadata_bad_utf8.wav
Binary files differ
diff --git a/dom/media/test/wave_metadata_bad_utf8.wav^headers^ b/dom/media/test/wave_metadata_bad_utf8.wav^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/wave_metadata_bad_utf8.wav^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/wave_metadata_unknown_tag.wav b/dom/media/test/wave_metadata_unknown_tag.wav
new file mode 100644
index 0000000000..b19fb5170f
--- /dev/null
+++ b/dom/media/test/wave_metadata_unknown_tag.wav
Binary files differ
diff --git a/dom/media/test/wave_metadata_unknown_tag.wav^headers^ b/dom/media/test/wave_metadata_unknown_tag.wav^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/wave_metadata_unknown_tag.wav^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/wave_metadata_utf8.wav b/dom/media/test/wave_metadata_utf8.wav
new file mode 100644
index 0000000000..352db285bb
--- /dev/null
+++ b/dom/media/test/wave_metadata_utf8.wav
Binary files differ
diff --git a/dom/media/test/wave_metadata_utf8.wav^headers^ b/dom/media/test/wave_metadata_utf8.wav^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/wave_metadata_utf8.wav^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/wavedata_alaw.wav b/dom/media/test/wavedata_alaw.wav
new file mode 100644
index 0000000000..ef090d16e0
--- /dev/null
+++ b/dom/media/test/wavedata_alaw.wav
Binary files differ
diff --git a/dom/media/test/wavedata_alaw.wav^headers^ b/dom/media/test/wavedata_alaw.wav^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/wavedata_alaw.wav^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/wavedata_float.wav b/dom/media/test/wavedata_float.wav
new file mode 100644
index 0000000000..155f891d51
--- /dev/null
+++ b/dom/media/test/wavedata_float.wav
Binary files differ
diff --git a/dom/media/test/wavedata_float.wav^headers^ b/dom/media/test/wavedata_float.wav^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/wavedata_float.wav^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/wavedata_s16.wav b/dom/media/test/wavedata_s16.wav
new file mode 100644
index 0000000000..6a69cd78f6
--- /dev/null
+++ b/dom/media/test/wavedata_s16.wav
Binary files differ
diff --git a/dom/media/test/wavedata_s16.wav^headers^ b/dom/media/test/wavedata_s16.wav^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/wavedata_s16.wav^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/wavedata_s24.wav b/dom/media/test/wavedata_s24.wav
new file mode 100644
index 0000000000..dbdb6aac1e
--- /dev/null
+++ b/dom/media/test/wavedata_s24.wav
Binary files differ
diff --git a/dom/media/test/wavedata_s24.wav^headers^ b/dom/media/test/wavedata_s24.wav^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/wavedata_s24.wav^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/wavedata_u8.wav b/dom/media/test/wavedata_u8.wav
new file mode 100644
index 0000000000..1d895c2ce0
--- /dev/null
+++ b/dom/media/test/wavedata_u8.wav
Binary files differ
diff --git a/dom/media/test/wavedata_u8.wav^headers^ b/dom/media/test/wavedata_u8.wav^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/wavedata_u8.wav^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/wavedata_ulaw.wav b/dom/media/test/wavedata_ulaw.wav
new file mode 100644
index 0000000000..0874face21
--- /dev/null
+++ b/dom/media/test/wavedata_ulaw.wav
Binary files differ
diff --git a/dom/media/test/wavedata_ulaw.wav^headers^ b/dom/media/test/wavedata_ulaw.wav^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/test/wavedata_ulaw.wav^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/test/white-short.webm b/dom/media/test/white-short.webm
new file mode 100644
index 0000000000..0b718cdf20
--- /dev/null
+++ b/dom/media/test/white-short.webm
Binary files differ
diff --git a/dom/media/tests/crashtests/1281695.html b/dom/media/tests/crashtests/1281695.html
new file mode 100644
index 0000000000..9865c3a509
--- /dev/null
+++ b/dom/media/tests/crashtests/1281695.html
@@ -0,0 +1,24 @@
+
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<script>
+
+function zombieWindow()
+{
+ var frame = document.createElement("iframe");
+ document.body.appendChild(frame);
+ var frameWin = frame.contentWindow;
+ frame.remove();
+ return frameWin;
+}
+
+function boom() {
+ zombieWindow().navigator.mozGetUserMedia({ "fake": true, "audio": true }, function(stream) {}, function(e) {});
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/dom/media/tests/crashtests/1306476.html b/dom/media/tests/crashtests/1306476.html
new file mode 100644
index 0000000000..d15cbe57df
--- /dev/null
+++ b/dom/media/tests/crashtests/1306476.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+ <meta charset=utf-8>
+ <title>Bug 1348381: Integer overflow playing vorbis file</title>
+</head>
+<body>
+ <audio id=player controls
+ src="data:audio/ogg;base64,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">
+ </audio>
+ <div id=result>
+ </div>
+
+ <script type="application/javascript">
+ function done() {
+ // Remove reftest-wait to terminate the test.
+ document.documentElement.removeAttribute("class");
+ }
+
+ function log(msg) {
+ // Append a status result.
+ let p = document.createElement("p");
+ p.textContent = msg;
+ document.getElementById("result").appendChild(p);
+ }
+
+ // Confirm playback completes.
+ let a = document.getElementById("player");
+ a.addEventListener("ended", () => {
+ log("Playback completed.");
+ done();
+ });
+ a.addEventListener("error", () => {
+ log("Playback error.");
+ });
+
+ log("Starting playback...");
+ a.play();
+
+ // Set a short timeout in case we hang.
+ setTimeout(() => {
+ log("Timed out!");
+ done();
+ }, 100);
+ </script>
+</body>
+</html>
diff --git a/dom/media/tests/crashtests/1348381.html b/dom/media/tests/crashtests/1348381.html
new file mode 100644
index 0000000000..b872e58794
--- /dev/null
+++ b/dom/media/tests/crashtests/1348381.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+ <title>Bug 1348381: Crash when recording extremely large canvas' captureStream</title>
+</head>
+</body>
+<canvas id="c" height="0.6"></canvas>
+<img id="img" src="data:image/gif;base64,R0lGODlhAQABAAAAACwAAAAAAQABAAA="></img>
+<script type="application/javascript">
+const c = document.getElementById("c");
+const ctx = c.getContext('2d');
+const s = c.captureStream(0);
+const mr = new MediaRecorder(s);
+const t = s.getVideoTracks()[0];
+mr.start();
+const img = document.getElementById('img');
+t.enabled = false;
+ctx.drawImage(img, 16, 18014398509481984);
+setTimeout(() => document.documentElement.removeAttribute("class"), 100);
+</script>
+</body>
+</html>
diff --git a/dom/media/tests/crashtests/1367930_1.html b/dom/media/tests/crashtests/1367930_1.html
new file mode 100644
index 0000000000..a4039a61a7
--- /dev/null
+++ b/dom/media/tests/crashtests/1367930_1.html
@@ -0,0 +1,39 @@
+<html>
+<body>
+ <script>
+var offer= "v=0\r\no=- 6276735615230473072 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE video\r\na=msid-semantic:WMS *\r\na=ice-ufrag:XoEUdw==\r\na=ice-pwd:hfNjAs9TU4NNuumB50mGfJwkqIEN8A==\r\na=ice-lite\r\na=setup:actpass\r\na=fingerprint:sha-256 0D:54:F2:D9:F3:10:00:2D:CD:14:C6:AC:CB:5D:E7:34:5E:6F:A8:BF:25:96:48:40:0B:C7:F9:18:6A:A6:73:3F\r\nm=video 9 UDP/TLS/RTP/SAVPF 100\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=mid:video\r\na=recvonly\r\na=rtcp-rsize\r\na=rtcp-mux\r\na=rtpmap:100 VP8/90000\r\na=rtcp-fb:100 ccm fir\r\na=rtcp-fb:100 nack\r\na=rtcp-fb:100 nack pli\r\na=rtcp-fb:100 goog-remb\r\na=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\na=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:4 urn:3gpp:video-orientation\r\na=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\na=rid:hi recv\r\na=rid:mid recv\r\na=rid:lo recv\r\na=simulcast: recv rid=hi,mid,lo\r\na=candidate:1796272311 1 UDP 2130706431 192.168.10.146 55984 typ host\r\na=end-of-candidates\r\n"
+
+var data = {};
+data.sdp = offer;
+data.type = "offer";
+
+console.log('OFFER', JSON.stringify(data.sdp));
+
+var pc = new RTCPeerConnection();
+
+navigator.mediaDevices.getUserMedia({video: true, fake: true})
+ .then((stream) => {
+ return pc.addStream(stream);
+ })
+ .then(() => {
+ return pc.setRemoteDescription(new RTCSessionDescription(data));
+ })
+ .then(function() {
+ var sender = pc.getSenders()[0];
+ console.log('setting parameters');
+ return sender.setParameters({encodings: [
+ {rid: "hi", maxBitrate: 800000},
+ {rid: "mid", maxBitrate: 400000, scaleDownResolutionBy: 2},
+ {rid: "lo", maxBitrate: 150000, scaleDownResolutionBy: 4}
+ ]});
+ })
+ .then(function() {
+ return pc.createAnswer();
+ })
+ .then(function(answer) {
+ console.log('answer', JSON.stringify(answer.sdp));
+ return pc.setLocalDescription(answer);
+ });
+ </script>
+</body>
+</html>
diff --git a/dom/media/tests/crashtests/1367930_2.html b/dom/media/tests/crashtests/1367930_2.html
new file mode 100644
index 0000000000..e2ee6720c6
--- /dev/null
+++ b/dom/media/tests/crashtests/1367930_2.html
@@ -0,0 +1,25 @@
+<html>
+ <body>
+ <script>
+const sdp = {"type":"offer","sdp":"v=0\r\no=- 6276735615230473072 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE video\r\na=msid-semantic:WMS *\r\na=ice-ufrag:XpXW4g==\r\na=ice-pwd:hjagMNwFF/kMOmWoULEFptwuQXkMVQ==\r\na=ice-lite\r\na=setup:actpass\r\na=fingerprint:sha-256 DC:FC:25:56:2B:88:77:2F:E4:FA:97:4E:2E:F1:D6:34:A6:A0:11:E2:E4:38:B3:98:08:D2:F7:9D:F5:E2:C1:15\r\nm=video 9 UDP/TLS/RTP/SAVPF 100\r\nc=IN IP4 0.0.0.0\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=mid:video\r\na=recvonly\r\na=rtcp-rsize\r\na=rtcp-mux\r\na=rtpmap:100 VP8/90000\r\na=rtcp-fb:100 ccm fir\r\na=rtcp-fb:100 nack\r\na=rtcp-fb:100 nack pli\r\na=rtcp-fb:100 goog-remb\r\na=extmap:2 urn:ietf:params:rtp-hdrext:toffset\r\na=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:4 urn:3gpp:video-orientation\r\na=extmap:5 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\na=rid:hi recv\r\na=rid:mid recv\r\na=rid:lo recv\r\na=simulcast: recv rid=hi;mid;lo\r\na=candidate:1796272311 1 UDP 2130706431 127.0.0.1 33584 typ host\r\na=end-of-candidates\r\n"};
+
+var pc = new RTCPeerConnection();
+
+navigator.mediaDevices.getUserMedia({video: true, fake: true})
+.then(stream => pc.addStream(stream))
+.then(() => {
+ console.log('hum');
+ pc.setRemoteDescription(sdp)
+ .then(function() {
+ return pc.createAnswer();
+ })
+ .then(function(answer) {
+ console.log(answer.sdp);
+ return pc.setLocalDescription(answer);
+ })
+ .then(console.log('yay'))
+ .catch(e => console.error(e));
+})
+ </script>
+ </body>
+</html>
diff --git a/dom/media/tests/crashtests/1429507_1.html b/dom/media/tests/crashtests/1429507_1.html
new file mode 100644
index 0000000000..894abf911a
--- /dev/null
+++ b/dom/media/tests/crashtests/1429507_1.html
@@ -0,0 +1,13 @@
+<html>
+ <body>
+ <script>
+ try { o1 = window.open("") } catch (e) {}
+ try { o1.location.reload() } catch (e) {}
+ try { o2 = new RTCPeerConnection({iceServers: [{"url": "stun:d"}]}) } catch (e) {}
+ try { o1.navigator.mediaDevices.getUserMedia({video: true, fake: true}).catch((error) => {}) } catch (e) {}
+ try { o2.createOffer({offerToReceiveVideo: true}).then((offer) => {
+ try { o1.navigator.mediaDevices.getUserMedia({video: true}) } catch (e) {};
+ }) } catch (e) {}
+ </script>
+ </body>
+</html>
diff --git a/dom/media/tests/crashtests/1429507_2.html b/dom/media/tests/crashtests/1429507_2.html
new file mode 100644
index 0000000000..d5f15b648e
--- /dev/null
+++ b/dom/media/tests/crashtests/1429507_2.html
@@ -0,0 +1,15 @@
+<html>
+ <body>
+ <script>
+ try { o1 = window.open("") } catch(e) { }
+ try { o1.location.reload() } catch(e) { }
+ try { o2 = o1.navigator } catch(e) { }
+ try { o3 = o2.mediaDevices } catch(e) { }
+ try { o4 = new XMLHttpRequest() } catch(e) { }
+ try { o3.getUserMedia({video: true, fake: true }).then((stream) => {}).catch((error) => {}) } catch (e) {}
+ try { o4.open("T", "aa", false) } catch(e) { }
+ try { o4.send() } catch(e) { }
+ try { o3.getUserMedia({video: true}).then((stream) => {}).catch((error) => {}) } catch (e) {}
+ </script>
+ </body>
+</html>
diff --git a/dom/media/tests/crashtests/1443212.html b/dom/media/tests/crashtests/1443212.html
new file mode 100644
index 0000000000..3f0eedac90
--- /dev/null
+++ b/dom/media/tests/crashtests/1443212.html
@@ -0,0 +1,13 @@
+<script>
+window.addEventListener("load", () => {
+ const c = document.createElement("canvas");
+ c.getContext("2d");
+ const stream = c.captureStream();
+ const [track] = stream.getTracks();
+ stream.removeTrack(track);
+ track.stop();
+ stream.addTrack(track);
+ stream.removeTrack(track);
+ stream.addTrack(track);
+});
+</script>
diff --git a/dom/media/tests/crashtests/1453030.html b/dom/media/tests/crashtests/1453030.html
new file mode 100644
index 0000000000..9452ec1205
--- /dev/null
+++ b/dom/media/tests/crashtests/1453030.html
@@ -0,0 +1,19 @@
+<html class="reftest-wait">
+ <head>
+ <script>
+ function getDTMF () {
+ try { o4 = o2.dtmf } catch(e) { }
+ try { o4.insertDTMF("1BC1D55", new Uint32Array([3538134876])[0], new Uint32Array([666182017])[0]) } catch (e) { }
+ document.documentElement.removeAttribute("class");
+ }
+
+ o1 = new RTCPeerConnection({ }, null)
+ window.navigator.mediaDevices.getUserMedia({video: true, fake: true}).then((stream) => {
+ o2 = o1.addTrack(stream.getVideoTracks()[0], stream)
+ }).catch((error) => {})
+ o3 = window.open("datr=0")
+ setTimeout(getDTMF, 400)
+ setTimeout(window.location.reload.bind(window.location), 500);
+ </script>
+ </head>
+</html>
diff --git a/dom/media/tests/crashtests/1468451.html b/dom/media/tests/crashtests/1468451.html
new file mode 100644
index 0000000000..40f2efac0d
--- /dev/null
+++ b/dom/media/tests/crashtests/1468451.html
@@ -0,0 +1,10 @@
+<script>
+alice = new RTCPeerConnection({ iceServers: [{ 'url': 'stun:2' }], }, null)
+bob = new RTCPeerConnection({ }, null)
+alice.createOffer({ offerToReceiveAudio: true })
+track = alice.getReceivers()[0].track
+alice.createOffer({ }).then((offer) => {
+ bob.addEventListener('signalingstatechange', (x) => x.target.addTransceiver(track, {}), { })
+ bob.close()
+})
+</script>
diff --git a/dom/media/tests/crashtests/1490700.html b/dom/media/tests/crashtests/1490700.html
new file mode 100644
index 0000000000..ea881c3096
--- /dev/null
+++ b/dom/media/tests/crashtests/1490700.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+ <title>Bug 1490700 - Divide-by-zero for screen-capture with max-dimension 0</title>
+</head>
+</body>
+<script type="application/javascript">
+async function test() {
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ await window.navigator.mediaDevices.getUserMedia({
+ video: {
+ mediaSource: 'screen',
+ height: {max: 0},
+ },
+ });
+ await window.navigator.mediaDevices.getUserMedia({
+ video: {
+ mediaSource: 'screen',
+ advanced: [{height: 0}],
+ },
+ });
+ document.documentElement.removeAttribute("class");
+}
+
+test();
+</script>
+</body>
+</html>
diff --git a/dom/media/tests/crashtests/1505957.html b/dom/media/tests/crashtests/1505957.html
new file mode 100644
index 0000000000..7b0780a9cf
--- /dev/null
+++ b/dom/media/tests/crashtests/1505957.html
@@ -0,0 +1,23 @@
+<html id='a'>
+<script>
+window.onload=function() {
+ b.src=document.getElementById('c').innerHTML;
+ b.setAttribute('src', 'video-crash.webm');
+ document.documentElement.style.display='none';
+ window.top.open('');
+ var o = window.frames[0].document.body.childNodes[0];
+ document.getElementById('d').appendChild(o.parentNode.removeChild(o));
+ o = document.getElementById('a');
+ var p = o.parentNode;
+ o.setAttribute('id', 0)
+ p.removeChild(o);
+ p.appendChild(o);
+ o.setAttribute('style', 0)
+ p.removeChild(o);
+ p.appendChild(o);
+}
+</script>
+<iframe id='b'></iframe>
+<object id='c'>
+<ruby id='d'>
+</html>
diff --git a/dom/media/tests/crashtests/1509442-1.html b/dom/media/tests/crashtests/1509442-1.html
new file mode 100644
index 0000000000..eef877ecf8
--- /dev/null
+++ b/dom/media/tests/crashtests/1509442-1.html
@@ -0,0 +1,14 @@
+<!DOCTYPE HTML>
+<meta charset="UTF-8">
+<script>
+ function start() {
+ o5=new AudioContext();
+ o291=new Float32Array(0);
+ o388=document.write("");
+ o389=o5.createBuffer(2,22050,44100);
+ o420=new AudioBufferSourceNode(o5, {buffer:o389,detune:27});
+ o389.copyToChannel(o291,1,0);
+ document.close();
+ }
+</script>
+<body onload="start()"></body>
diff --git a/dom/media/tests/crashtests/1509442.html b/dom/media/tests/crashtests/1509442.html
new file mode 100644
index 0000000000..ddce401933
--- /dev/null
+++ b/dom/media/tests/crashtests/1509442.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<meta charset="UTF-8">
+<body>
+<iframe src="1509442-1.html" onload="document.documentElement.removeAttribute('class')"></iframe>
+</body>
+</html>
diff --git a/dom/media/tests/crashtests/1510848.html b/dom/media/tests/crashtests/1510848.html
new file mode 100644
index 0000000000..05e2c2d852
--- /dev/null
+++ b/dom/media/tests/crashtests/1510848.html
@@ -0,0 +1,4 @@
+<a>
+<dd>
+<video>
+<a>
diff --git a/dom/media/tests/crashtests/1511130.html b/dom/media/tests/crashtests/1511130.html
new file mode 100644
index 0000000000..6bdebe8de1
--- /dev/null
+++ b/dom/media/tests/crashtests/1511130.html
@@ -0,0 +1,12 @@
+<script>
+document.addEventListener("DOMContentLoaded", function(){
+ var o = window.frames[0].document.body.childNodes[0];
+ a.appendChild(o.parentNode.removeChild(o));
+ o = document.body;
+ o.parentNode.appendChild(o);
+ o.parentNode.appendChild(o);
+})
+</script>
+<body text=''>
+<hgroup id='a'>
+<iframe src="video-crash.webm"></iframe>
diff --git a/dom/media/tests/crashtests/1516292.html b/dom/media/tests/crashtests/1516292.html
new file mode 100644
index 0000000000..93c844c939
--- /dev/null
+++ b/dom/media/tests/crashtests/1516292.html
@@ -0,0 +1,16 @@
+<script>
+function go() {
+ a.children[0].appendChild(b)
+}
+</script>
+<body onload=go()>
+<svg id="a">
+<foreignObject>
+<li>
+<video controls="" autoplay="">
+<li id="a" contenteditable="true">
+</li>
+</svg>
+<canvas id="b">
+<svg>
+<use xlink:href="#a">
diff --git a/dom/media/tests/crashtests/1573536.html b/dom/media/tests/crashtests/1573536.html
new file mode 100644
index 0000000000..1283183254
--- /dev/null
+++ b/dom/media/tests/crashtests/1573536.html
@@ -0,0 +1,18 @@
+<html class="reftest-wait">
+<head>
+ <script>
+ function start () {
+ window.navigator.mediaDevices.getUserMedia({
+ 'audio': {
+ 'mediaSource': 'audioCapture'
+ },
+ 'video': true,
+ 'fake': true
+ }).catch(() => Promise.resolve())
+ .then(() => document.documentElement.removeAttribute("class"))
+ }
+
+ window.addEventListener('load', start)
+ </script>
+</head>
+</html>
diff --git a/dom/media/tests/crashtests/1576938.html b/dom/media/tests/crashtests/1576938.html
new file mode 100644
index 0000000000..71a785e299
--- /dev/null
+++ b/dom/media/tests/crashtests/1576938.html
@@ -0,0 +1,16 @@
+<html class="reftest-wait">
+<head>
+<script>
+async function start () {
+ const peer = new RTCPeerConnection()
+ await peer.createOffer({'offerToReceiveVideo': true})
+ const receiver = peer.getReceivers()[0]
+ peer.close()
+ receiver.track.clone()
+ document.documentElement.removeAttribute("class")
+}
+
+document.addEventListener('DOMContentLoaded', start)
+</script>
+</head>
+</html>
diff --git a/dom/media/tests/crashtests/1594136.html b/dom/media/tests/crashtests/1594136.html
new file mode 100644
index 0000000000..7f3189257e
--- /dev/null
+++ b/dom/media/tests/crashtests/1594136.html
@@ -0,0 +1,15 @@
+<html>
+<head>
+<script>
+function start () {
+ const context = new AudioContext();
+ const {stream} = new MediaStreamAudioDestinationNode(context);
+ const [track] = stream.getTracks();
+ track.stop();
+ context.createMediaStreamTrackSource(track);
+}
+
+document.addEventListener('DOMContentLoaded', start);
+</script>
+</head>
+</html>
diff --git a/dom/media/tests/crashtests/1749308.html b/dom/media/tests/crashtests/1749308.html
new file mode 100644
index 0000000000..1c17c6e35c
--- /dev/null
+++ b/dom/media/tests/crashtests/1749308.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <script>
+ window.addEventListener("load", () => {
+ const context = new AudioContext({})
+ const controller = new AbortController()
+ let processor = context.createScriptProcessor(512, 27, 2)
+ processor.addEventListener("audioprocess", async () => {}, { signal: controller.signal })
+ processor = null;
+ SpecialPowers.forceGC()
+ for (let i = 0; i < 7; i++) {
+ controller.abort();
+ }
+ })
+ </script>
+</head>
+</html>
diff --git a/dom/media/tests/crashtests/1764915.html b/dom/media/tests/crashtests/1764915.html
new file mode 100644
index 0000000000..82534caac7
--- /dev/null
+++ b/dom/media/tests/crashtests/1764915.html
@@ -0,0 +1,14 @@
+<html>
+<head>
+<script>
+window.addEventListener('load', async () => {
+ const frame = document.createElement('frame')
+ document.documentElement.appendChild(frame)
+ const peer = new RTCPeerConnection()
+ await peer.setRemoteDescription({}, () => {}, async () => await peer.getIdentityAssertion())
+ peer.close()
+})
+</script>
+</head>
+</html>
+
diff --git a/dom/media/tests/crashtests/1764933.html b/dom/media/tests/crashtests/1764933.html
new file mode 100644
index 0000000000..55f6fa2db9
--- /dev/null
+++ b/dom/media/tests/crashtests/1764933.html
@@ -0,0 +1,15 @@
+<html>
+<head>
+ <script>
+ window.addEventListener('load', async () => {
+ const peer = new RTCPeerConnection({})
+ await peer.createOffer({ 'offerToReceiveVideo': true })
+ const receivers = peer.getReceivers()
+ const sender = peer.addTrack(receivers[0].track)
+ peer.close()
+ await sender.getStats()
+ await sender.replaceTrack(receivers[0].track)
+ })
+ </script>
+</head>
+</html>
diff --git a/dom/media/tests/crashtests/1764940.html b/dom/media/tests/crashtests/1764940.html
new file mode 100644
index 0000000000..67bce74b18
--- /dev/null
+++ b/dom/media/tests/crashtests/1764940.html
@@ -0,0 +1,14 @@
+<html>
+<head>
+ <script>
+ document.addEventListener('DOMContentLoaded', async () => {
+ const peer = new RTCPeerConnection({ 'iceServers': [{ 'urls': 'stun:23.21.150.121' }] })
+ const offer = await peer.createOffer({ 'offerToReceiveVideo': true })
+ await peer.setRemoteDescription(offer)
+ const senders = peer.getSenders()
+ setTimeout(async () => await senders[1].setParameters({}), 334)
+ await peer.setRemoteDescription(offer)
+ })
+ </script>
+</head>
+</html>
diff --git a/dom/media/tests/crashtests/1766668.html b/dom/media/tests/crashtests/1766668.html
new file mode 100644
index 0000000000..0ea142e563
--- /dev/null
+++ b/dom/media/tests/crashtests/1766668.html
@@ -0,0 +1,11 @@
+<!DOCTYPE html>
+<script>
+ document.addEventListener('DOMContentLoaded', async () => {
+ await navigator.mediaDevices.getUserMedia({ 'audio': {} })
+ const peer = new RTCPeerConnection({ 'iceServers': [{ 'urls': 'stun:23.21.150.121' }], 'peerIdentity': 'B' }, {})
+ const stream = await navigator.mediaDevices.getUserMedia({ 'audio': {} })
+ stream.getTracks().forEach((track) => peer.addTrack(track, stream))
+ try { offer = await peer.createOffer({}) } catch (e) { }
+ await peer.setRemoteDescription(offer)
+ })
+</script>
diff --git a/dom/media/tests/crashtests/1783765.html b/dom/media/tests/crashtests/1783765.html
new file mode 100644
index 0000000000..333ad8d444
--- /dev/null
+++ b/dom/media/tests/crashtests/1783765.html
@@ -0,0 +1,22 @@
+<script>
+var timeout = async function (cmd) {
+ const timer = new Promise((resolve, reject) => {
+ const id = setTimeout(() => {
+ clearTimeout(id)
+ reject(new Error('Promise timed out!'))
+ }, 750)
+ })
+ return Promise.race([cmd, timer])
+}
+window.addEventListener('load', async () => {
+ let a = new RTCPeerConnection({
+ 'iceServers': [{'urls': 'stun:23.21.150.121'}],
+ 'peerIdentity': 'x',
+ })
+ let b = await timeout(a.createOffer({ }))
+ a.setLocalDescription(b)
+ try { a.setRemoteDescription(b) } catch (e) {}
+ a.setLocalDescription(b)
+})
+</script>
+
diff --git a/dom/media/tests/crashtests/780790.html b/dom/media/tests/crashtests/780790.html
new file mode 100644
index 0000000000..27eb2a961f
--- /dev/null
+++ b/dom/media/tests/crashtests/780790.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=780790
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Simple gUM test - null success callback</title>
+ <script type="application/javascript">
+ navigator.mozGetUserMedia({video: true, fake: true}, null, null);
+ </script>
+</head>
+
+<body>
+</body>
+</html>
diff --git a/dom/media/tests/crashtests/791270.html b/dom/media/tests/crashtests/791270.html
new file mode 100644
index 0000000000..627552fb84
--- /dev/null
+++ b/dom/media/tests/crashtests/791270.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=791270
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Simple PeerConnection Video Test</title>
+ <script type="application/javascript">
+ var pc = new RTCPeerConnection();
+ pc.addStream(undefined);
+ </script>
+</head>
+
+<body>
+</body>
+</html>
diff --git a/dom/media/tests/crashtests/791278.html b/dom/media/tests/crashtests/791278.html
new file mode 100644
index 0000000000..fd538f6788
--- /dev/null
+++ b/dom/media/tests/crashtests/791278.html
@@ -0,0 +1,20 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=791278
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Simple PeerConnection Video Test - Invalid callback</title>
+ <script type="application/javascript">
+ var pc1 = new RTCPeerConnection();
+ pc1.setLocalDescription(function() {});
+
+ var pc2 = new RTCPeerConnection();
+ pc2.setRemoteDescription(function() {});
+ </script>
+</head>
+
+<body>
+</body>
+</html>
diff --git a/dom/media/tests/crashtests/791330.html b/dom/media/tests/crashtests/791330.html
new file mode 100644
index 0000000000..fea2059a29
--- /dev/null
+++ b/dom/media/tests/crashtests/791330.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=791330
+-->
+<head>
+ <meta charset="utf-8">
+ <title>PeerConnection test - operate on closed connection</title>
+ <script type="application/javascript">
+ function finish() {
+ document.documentElement.removeAttribute("class");
+ }
+
+ function runTest() {
+ var pc = new RTCPeerConnection();
+ pc.close();
+
+ navigator.mozGetUserMedia({audio: true, fake: true}, function (stream) {
+ try {
+ pc.addStream(stream);
+ pc.createOffer(function (offer) {});
+ }
+ finally {
+ stream.getTracks().forEach(t => t.stop());
+
+ finish();
+ }
+ }, function () {});
+ }
+ </script>
+</head>
+
+<body onload="setTimeout(runTest, 100)">
+</body>
+</html>
diff --git a/dom/media/tests/crashtests/799419.html b/dom/media/tests/crashtests/799419.html
new file mode 100644
index 0000000000..7f815e73c8
--- /dev/null
+++ b/dom/media/tests/crashtests/799419.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=799419
+-->
+<head>
+ <meta charset="utf-8">
+ <title>2 Peer Connections Create and Close + Fake Video</title>
+ <script type="application/javascript">
+ function finish() {
+ document.documentElement.removeAttribute("class");
+ }
+
+ function boom() {
+ var v0 = new RTCPeerConnection();
+ var v1 = new RTCPeerConnection();
+ var v2 = document.getElementById("pc1video");
+ var v3 = document.getElementById("pc2video");
+ navigator.mozGetUserMedia({video:true, fake: true},
+ function(stream) {}, function() {});
+ v0.close();
+ v1.close();
+
+ finish();
+ }
+ </script>
+</head>
+<body onload="setTimeout(boom, 100)">
+<video id="pc1video" width="100" height="100" controls></video>
+<video id="pc2video" width="100" height="100" controls></video>
+</body>
+</html>
diff --git a/dom/media/tests/crashtests/801227.html b/dom/media/tests/crashtests/801227.html
new file mode 100644
index 0000000000..2dded54510
--- /dev/null
+++ b/dom/media/tests/crashtests/801227.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=801227
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Abort due to page reload</title>
+ <script type="application/javascript">
+ var pc = new RTCPeerConnection();
+
+ var index = localStorage.index || 0;
+ if (index < 3) {
+ localStorage.index = index + 1;
+ window.location.reload();
+ }
+
+ function finish() {
+ delete localStorage["index"];
+
+ pc.close();
+ document.documentElement.removeAttribute("class");
+ }
+
+ navigator.mozGetUserMedia({ audio: true, fake: true }, function (aStream) {
+ pc.addStream(aStream);
+ finish();
+ }, finish);
+ </script>
+</head>
+
+<body>
+</body>
+</html>
+
diff --git a/dom/media/tests/crashtests/802982.html b/dom/media/tests/crashtests/802982.html
new file mode 100644
index 0000000000..f034aff557
--- /dev/null
+++ b/dom/media/tests/crashtests/802982.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=802982
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Excessive getUserMedia calls</title>
+ <script type="application/javascript">
+
+ function finish() {
+ document.documentElement.removeAttribute("class");
+ }
+
+ function boom()
+ {
+ for (var j = 0; j < 100; ++j) {
+ navigator.mozGetUserMedia({video:true}, function(){}, function(){});
+ }
+ finish(); // we're not waiting for success/error callbacks here
+ }
+
+ </script>
+</head>
+
+<body onload="setTimeout(boom, 100)">
+</html>
diff --git a/dom/media/tests/crashtests/812785.html b/dom/media/tests/crashtests/812785.html
new file mode 100644
index 0000000000..f26168e49b
--- /dev/null
+++ b/dom/media/tests/crashtests/812785.html
@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=812785
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Bug 812785 - WebRTC use-after-free crash</title>
+ <script type="application/javascript">
+ var pc1, pc2, pc1_offer, pc2_answer, localAudio, remoteAudio;
+
+ function onFailure(code) {
+ stop();
+ }
+
+ function stop() {
+ pc1.close(); pc1 = null;
+ pc2.close(); pc2 = null;
+
+ var index = localStorage.index || 0;
+ if (index < 5) {
+ localStorage.index = index + 1;
+ window.location.reload();
+ }
+ else {
+ finish();
+ }
+ }
+
+ function start() {
+ localAudio = document.getElementById("local");
+ remoteAudio = document.getElementById("remote");
+
+ var stream = localAudio.mozCaptureStreamUntilEnded();
+
+ pc1 = new RTCPeerConnection();
+ pc2 = new RTCPeerConnection();
+
+ pc1.addStream(stream);
+ pc1.createOffer(function (offer) {
+ pc1_offer = offer;
+ pc1.setLocalDescription(pc1_offer, function () {
+ pc2.setRemoteDescription(pc1_offer, function () {
+ pc2.createAnswer(function (answer) {
+ pc2_answer = answer;
+ pc2.setLocalDescription(pc2_answer, function () {
+ pc1.setRemoteDescription(pc2_answer, function step6() {
+ stop();
+ }, onFailure);
+ }, onFailure);
+ }, onFailure);
+ }, onFailure);
+ }, onFailure);
+ }, onFailure);
+ }
+
+ function finish() {
+ delete localStorage["index"];
+
+ document.documentElement.removeAttribute("class");
+ }
+ </script>
+</head>
+
+<body onload="setTimeout(start, 100)">
+ <audio id="local" controls autoplay><source type="audio/wav" src="" /></audio>
+ <audio id="remote" controls></audio>
+</body>
+</html>
+
diff --git a/dom/media/tests/crashtests/822197.html b/dom/media/tests/crashtests/822197.html
new file mode 100644
index 0000000000..97abc44229
--- /dev/null
+++ b/dom/media/tests/crashtests/822197.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=822197
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Bug 822197 - Many PeerConnections with CreateOffer</title>
+ <script type="application/javascript">
+ var pcArray = [];
+
+ function finish() {
+ document.documentElement.removeAttribute("class");
+ }
+
+ function start() {
+ for(var i = 0; i < 70; i++) {
+ var pc = new RTCPeerConnection();
+ pc.createOffer(function() {}, function() {});
+ pcArray.push(pc);
+ }
+ finish();
+ }
+ </script>
+</head>
+
+<body onload="setTimeout(start, 100)">
+</html>
diff --git a/dom/media/tests/crashtests/834100.html b/dom/media/tests/crashtests/834100.html
new file mode 100644
index 0000000000..0042581564
--- /dev/null
+++ b/dom/media/tests/crashtests/834100.html
@@ -0,0 +1,25 @@
+<html class="reftest-wait">
+ <head>
+ <script language="javascript">
+
+function start() {
+ remotePC = new RTCPeerConnection();
+ var cand = new RTCIceCandidate(
+ {candidate: "1 1 UDP 1 127.0.0.1 34567 type host",
+ sdpMid: "helloworld",
+ sdbMid: "helloworld", // Mis-spelt attribute for bug 833948 compatibility.
+ sdpMLineIndex: 1
+ });
+ try {remotePC.addIceCandidate(cand);} catch(e) {} // bug 842075 - remove try when fixed
+ try {remotePC.addIceCandidate(cand, function(sdp){}, finish);} catch(e) {} // bug 842075 - remove try when fixed
+ finish();
+}
+
+function finish(arg) {
+ document.documentElement.removeAttribute("class");
+}
+ </script>
+ </head>
+ <body onload="setTimeout(start, 100)">
+ </body>
+</html>
diff --git a/dom/media/tests/crashtests/836349.html b/dom/media/tests/crashtests/836349.html
new file mode 100644
index 0000000000..651fde46ae
--- /dev/null
+++ b/dom/media/tests/crashtests/836349.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=836349
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Bug 836349</title>
+ <script type="application/javascript">
+ function crash()
+ {
+ var pc = new RTCPeerConnection();
+ var ld = pc.localDescription;
+ dump(ld.sdp);
+ }
+ </script>
+</head>
+
+<body onload="crash();">
+</html>
diff --git a/dom/media/tests/crashtests/837324.html b/dom/media/tests/crashtests/837324.html
new file mode 100644
index 0000000000..63162a336b
--- /dev/null
+++ b/dom/media/tests/crashtests/837324.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=837324
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Bug 837324</title>
+ <script type="application/javascript">
+ function finish() {
+ document.documentElement.removeAttribute("class");
+ }
+
+ function start() {
+ var o0 = new RTCPeerConnection();
+ var o1 = new RTCIceCandidate({"candidate":"0 -65535 IP 0 stun.sipgate.net 3227326073 type ::ffff:192.0.2.128 host 2302600701","sdpMid":"video 3907077665 RTP/SAVPF 5000","sdpMLineIndex":7});
+ try {o0.addIceCandidate(o1);} catch(e) {} // bug 842075 - remove try when fixed
+
+ finish();
+ }
+ </script>
+</head>
+
+<body onload="setTimeout(start, 100)">
+</html>
diff --git a/dom/media/tests/crashtests/855796.html b/dom/media/tests/crashtests/855796.html
new file mode 100644
index 0000000000..da46276c53
--- /dev/null
+++ b/dom/media/tests/crashtests/855796.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=855796
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Bug 855796</title>
+ <script type="application/javascript">
+ var pc1, pc2, pc1_offer, pc2_answer;
+
+ function onFailure(code) {
+ stop();
+ }
+
+ function stop() {
+ pc1.close();
+ pc1 = null;
+ pc2.close();
+ pc2 = null;
+ document.documentElement.removeAttribute("class");
+ }
+
+ function step1(offer) {
+ pc1_offer = offer;
+ pc1_offer.sdp = "v=0\r\no=Mozilla-SIPUA 2208 0 IN IP4 0.0.0.0\r\ns=SIP Call\r\nt=0 0\r\na=ice-pwd:4450d5a4a5f097855c16fa079893be18\r\na=fingerprint:sha-256 23:9A:2E:43:94:42:CF:46:68:FC:62:F9:F4:48:61:DB:2F:8C:C9:FF:6B:25:54:9D:41:09:EF:83:A8:19:FC:B6\r\nm=audio 56187 RTP/SAVPF 109 0 8 101\r\nc=IN IP4 77.9.79.167\r\na=rtpmap:109 opus/48000/2\r\na=ptime:20\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:101 telephone-event/8000\r\na=fmtp:101 0-15\r\na=sendrecv\r\na=candidate:0 1 UDP 2113601791 192.168.178.20 56187 typ host\r\na=candidate:1 1 UDP 1694236671 77.9.79.167 56187 typ srflx raddr 192.168.178.20 rport 56187\r\na=candidate:0 2 UDP 2113601790 192.168.178.20 52955 typ host\r\na=candidate:1 2 UDP 1694236670 77.9.79.167 52955 typ srflx raddr 192.168.178.20 rport 52955\r\nm=video 49929 RTP/SAVPF 120\r\nc=IN IP4 77.9.79.167\r\na=rtpmap:120 VP8/90000\r\na=recvonly\r\na=candidate:0 1 UDP 2113601791 192.168.178.20 49929 typ host\r\na=candidate:1 1 UDP 1694236671 77.9.79.167 49929 typ srflx raddr 192.168.178.20 rport 49929\r\na=candidate:0 2 UDP 2113601790 192.168.178.20 50769 typ host\r\na=candidate:1 2 UDP 1694236670 77.9.79.167 50769 typ srflx raddr 192.168.178.20 rport 50769\r\nm=application 54054 SCTP/DTLS 5000 \r\nc=IN IP4 77.9.79.167\r\na=fmtp:HuRUu]Dtcl\\zM,7(OmEU%O$gU]x/z\tD protocol=webrtc-datachannel;streams=16\r\na=sendrecv\r\n";
+ pc1.setLocalDescription(pc1_offer, step2, onFailure);
+ }
+
+ function step2() {
+ pc2.setRemoteDescription(pc1_offer, step3, onFailure);
+ }
+
+ function step3() {
+ pc2.createAnswer(step4, onFailure);
+ }
+
+ function step4(answer) {
+ pc2_answer = answer;
+ pc2.setLocalDescription(pc2_answer, step5, onFailure);
+ }
+
+ function step5() {
+ pc1.setRemoteDescription(pc2_answer, step6, onFailure);
+ }
+
+ function step6() {
+ stop();
+ }
+
+ function start() {
+ pc1 = new RTCPeerConnection();
+ pc2 = new RTCPeerConnection();
+ navigator.mozGetUserMedia({audio:true, video:true, fake:true}, function(s) {
+ pc1.addStream(s);
+ navigator.mozGetUserMedia({audio:true, video:true, fake:true}, function(s) {
+ pc2.addStream(s);
+ pc1.createOffer(step1, onFailure);
+ }, onFailure);
+ }, onFailure);
+ }
+ </script>
+</head>
+
+<body onload="setTimeout(start, 100)">
+</html>
diff --git a/dom/media/tests/crashtests/860143.html b/dom/media/tests/crashtests/860143.html
new file mode 100644
index 0000000000..2d17a8dace
--- /dev/null
+++ b/dom/media/tests/crashtests/860143.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=860143
+-->
+<head>
+ <meta charset="utf-8">
+ <title>bug 860143</title>
+ <script type="application/javascript">
+ function start() {
+ var o0 = new window.RTCPeerConnection({
+ iceServers: [
+ {
+ url: "turn:AAAAAAAAAAAAAAAAA AAAAAAAAAAA AAAAAAAAAAAAA AA AAAAA AAAAAAAAAAA AAAAAAAAAAAAAAAA AAAAA AAAAAAAAAA, AAAAAAAAAAAAAAAA AAA AAAAAAAAAAAAAAA"
+ }
+ ]
+ });
+ o0.close();
+ }
+ </script>
+</head>
+
+<body onload="start()">
+</body>
+</html>
diff --git a/dom/media/tests/crashtests/861958.html b/dom/media/tests/crashtests/861958.html
new file mode 100644
index 0000000000..d6ea7e2e84
--- /dev/null
+++ b/dom/media/tests/crashtests/861958.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=861958
+-->
+<head>
+ <meta charset="utf-8">
+ <title>bug 861958</title>
+ <script type="application/javascript">
+ function start() {
+ var o0 = new window.RTCPeerConnection();
+ var o1 = o0.createDataChannel("foo", {
+ "protocol": "text/char",
+ "preset": false,
+ "stream": 512
+ });
+ o0.close();
+ }
+ </script>
+</head>
+
+<body onload="start()">
+</body>
+</html>
diff --git a/dom/media/tests/crashtests/863929.html b/dom/media/tests/crashtests/863929.html
new file mode 100644
index 0000000000..dc534e264f
--- /dev/null
+++ b/dom/media/tests/crashtests/863929.html
@@ -0,0 +1,66 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=863929
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Bug 863929</title>
+ <script type="application/javascript">
+ var pc1, pc2, pc1_offer, pc2_answer;
+
+ function onFailure(code) {
+ stop();
+ }
+
+ function stop() {
+ pc1.close();
+ pc1 = null;
+ pc2.close();
+ pc2 = null;
+ document.documentElement.removeAttribute("class");
+ }
+
+ function step1(offer) {
+ pc1_offer = offer;
+ pc1_offer.sdp = 'v=0\r\no=Mozilla-SIPUA 2208 0 IN IP4 0.0.0.0\r\ns=SIP Call\r\nt=0 0\r\na=ice-ufrag:96e36277\r\na=ice-pwd:4450d5a4a5f097855c16fa079893be18\r\na=fingerprint:sha-256 23:9A:2E:43:94:42:CF:46:68:FC:62:F9:F4:48:61:DB:2F:8C:C9:FF:6B:25:54:9D:41:09:EF:83:A8:19:FC:B6\r\nm=audio 56187 RTP/SAVPF 109 0 8 101\r\nc=IN IP4 77.9.79.167\r\na=rtpmap:109 opus/48000/2\r\na=ptime:20\r\na=rtpmap:0 PCMU/8000\r\na=rtpmap:8 PCMA/8000\r\na=rtpmap:101 telephone-event/8000\r\na=fmtp:101 0-15\r\na=sendrecv\r\na=candidate:0 1 UDP 2113601791 192.168.178.20 56187 typ host\r\na=candidate:1 1 UDP 1694236671 77.9.79.167 56187 typ srflx raddr 192.168.178.20 rport 56187\r\na=candidate:0 2 UDP 2113601790 192.168.178.20 52955 typ host\r\na=candidate:1 2 UDP 1694236670 77.9.79.167 52955 typ srflx raddr 192.168.178.20 rport 52955\r\nm=video 49929 RTP/SAVPF 120\r\nc=IN IP4 77.9.79.167\r\na=rtpmap:120 telephone-event/90000\r\na=recvonly\r\na=candidate:0 1 UDP 2113601791 192.168.178.20 49929 typ host\r\na=candidate:1 1 UDP 1694236671 77.9.79.167 49929 typ srflx raddr 192.168.178.20 rport 49929\r\na=candidate:0 2 UDP 2113601790 192.168.178.20 50769 typ host\r\na=candidate:1 2 UDP 1694236670 77.9.79.167 50769 typ srflx raddr 192.168.178.20 rport 50769\r\nm=application 54054 SCTP/DTLS 5000 \r\nc=IN IP4 77.9.79.167\r\na=fmtp:5000 protocol=webrtc-datachannel;streams=16\r\na=sendrecv\r\n';
+ pc1.setLocalDescription(pc1_offer, step2, onFailure);
+ }
+
+ function step2() {
+ pc2.setRemoteDescription(pc1_offer, step3, onFailure);
+ }
+
+ function step3() {
+ pc2.createAnswer(step4, onFailure);
+ }
+
+ function step4(answer) {
+ pc2_answer = answer;
+ pc2.setLocalDescription(pc2_answer, step5, onFailure);
+ }
+
+ function step5() {
+ pc1.setRemoteDescription(pc2_answer, step6, onFailure);
+ }
+
+ function step6() {
+ stop();
+ }
+
+ function start() {
+ pc1 = new RTCPeerConnection();
+ pc2 = new RTCPeerConnection();
+ navigator.mozGetUserMedia({audio:true, video:true, fake:true}, function(s) {
+ pc1.addStream(s);
+ navigator.mozGetUserMedia({audio:true, video:true, fake:true}, function(s) {
+ pc2.addStream(s);
+ pc1.createOffer(step1, onFailure);
+ }, onFailure);
+ }, onFailure);
+ }
+ </script>
+</head>
+
+<body onload="start()">
+</html>
diff --git a/dom/media/tests/crashtests/crashtests.list b/dom/media/tests/crashtests/crashtests.list
new file mode 100644
index 0000000000..c18d288aa1
--- /dev/null
+++ b/dom/media/tests/crashtests/crashtests.list
@@ -0,0 +1,42 @@
+defaults pref(media.peerconnection.enabled,true) pref(media.navigator.permission.disabled,true) pref(dom.disable_open_during_load,false) pref(media.devices.insecure.enabled,true) pref(media.getusermedia.insecure.enabled,true)
+
+load 780790.html
+load 791270.html
+load 791278.html
+load 791330.html
+load 799419.html
+load 802982.html
+skip-if(isDebugBuild) load 812785.html
+load 834100.html
+load 836349.html
+load 837324.html
+load 855796.html
+load 860143.html
+load 861958.html
+load 863929.html
+load 1281695.html
+load 1306476.html
+load 1348381.html
+load 1367930_1.html
+load 1367930_2.html
+skip-if(/^Windows\x20NT\x206\.1/.test(http.oscpu)) asserts-if(Android,0-1) pref(browser.link.open_newwindow,2) load 1429507_1.html # window.open() in tab doesn't work for crashtest in e10s, this opens a new window instead
+skip-if(/^Windows\x20NT\x206\.1/.test(http.oscpu)) asserts-if(Android,0-1) pref(browser.link.open_newwindow,2) load 1429507_2.html # window.open() in tab doesn't work for crashtest in e10s, this opens a new window instead
+load 1443212.html
+skip-if(/^Windows\x20NT\x206\.1/.test(http.oscpu)) asserts-if(Android,0-2) load 1453030.html
+load 1468451.html
+skip-if(Android|/^Windows\x20NT\x206\.1/.test(http.oscpu)) load 1490700.html # No screenshare on Android
+skip-if(/^Windows\x20NT\x206\.1/.test(http.oscpu)) load 1505957.html
+load 1509442.html
+load 1511130.html
+load 1510848.html
+load 1516292.html
+load 1576938.html
+skip-if(Android) pref(media.getusermedia.audiocapture.enabled,true) load 1573536.html
+skip-if(!Android) pref(media.getusermedia.audiocapture.enabled,true) pref(media.navigator.permission.device,false) load 1573536.html # media.navigator.permission.device is mobile-only, so other platforms fail to set it (Bug 1350948)
+load 1594136.html
+load 1749308.html
+load 1764915.html
+load 1764933.html
+load 1764940.html
+load 1766668.html
+load 1783765.html
diff --git a/dom/media/tests/crashtests/datachannel-oom.html b/dom/media/tests/crashtests/datachannel-oom.html
new file mode 100644
index 0000000000..e55ec651be
--- /dev/null
+++ b/dom/media/tests/crashtests/datachannel-oom.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title></title>
+ <script type="application/javascript">
+ function start() {
+ var size = 2147483638;
+ new RTCPeerConnection().createDataChannel('a').send(new Uint8Array(size));
+ var a = "a";
+ var count = Math.floor(Math.log2(size)) - 1;
+ for (var i = 0; i < count; i++) {
+ a += a;
+ }
+ new RTCPeerConnection().createDataChannel('a').send(a)
+ }
+ </script>
+</head>
+
+<body onload="start()">
+</body>
+</html>
diff --git a/dom/media/tools/generateGmpJson.py b/dom/media/tools/generateGmpJson.py
new file mode 100644
index 0000000000..0cd1d03cfe
--- /dev/null
+++ b/dom/media/tools/generateGmpJson.py
@@ -0,0 +1,170 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import argparse
+import hashlib
+import json
+import logging
+
+import requests
+
+
+def fetch_data_for_cdms(cdms, urlParams):
+ for cdm in cdms:
+ if "fileName" in cdm:
+ cdm["fileUrl"] = cdm["fileName"].format_map(urlParams)
+ response = requests.get(cdm["fileUrl"])
+ response.raise_for_status()
+ cdm["hashValue"] = hashlib.sha512(response.content).hexdigest()
+ cdm["filesize"] = len(response.content)
+
+
+def generate_json_for_cdms(cdms):
+ cdm_json = ""
+ for cdm in cdms:
+ if "alias" in cdm:
+ cdm_json += (
+ ' "{target}": {{\n'
+ + ' "alias": "{alias}"\n'
+ + " }},\n"
+ ).format_map(cdm)
+ else:
+ cdm_json += (
+ ' "{target}": {{\n'
+ + ' "fileUrl": "{fileUrl}",\n'
+ + ' "filesize": {filesize},\n'
+ + ' "hashValue": "{hashValue}"\n'
+ + " }},\n"
+ ).format_map(cdm)
+ return cdm_json[:-2] + "\n"
+
+
+def calculate_gmpopenh264_json(version: str, version_hash: str, url_base: str) -> str:
+ # fmt: off
+ cdms = [
+ {"target": "Darwin_aarch64-gcc3", "fileName": "{url_base}/openh264-macosx64-aarch64-{version}.zip"},
+ {"target": "Darwin_x86_64-gcc3", "fileName": "{url_base}/openh264-macosx64-{version}.zip"},
+ {"target": "Linux_x86-gcc3", "fileName": "{url_base}/openh264-linux32-{version}.zip"},
+ {"target": "Linux_x86_64-gcc3", "fileName": "{url_base}/openh264-linux64-{version}.zip"},
+ {"target": "Linux_x86_64-gcc3-asan", "alias": "Linux_x86_64-gcc3"},
+ {"target": "Linux_aarch64-gcc3", "fileName": "{url_base}/openh264-linux64-aarch64-{version}.zip"},
+ {"target": "WINNT_aarch64-msvc-aarch64", "fileName": "{url_base}/openh264-win64-aarch64-{version}.zip"},
+ {"target": "WINNT_x86-msvc", "fileName": "{url_base}/openh264-win32-{version}.zip"},
+ {"target": "WINNT_x86-msvc-x64", "alias": "WINNT_x86-msvc"},
+ {"target": "WINNT_x86-msvc-x86", "alias": "WINNT_x86-msvc"},
+ {"target": "WINNT_x86_64-msvc", "fileName": "{url_base}/openh264-win64-{version}.zip"},
+ {"target": "WINNT_x86_64-msvc-x64", "alias": "WINNT_x86_64-msvc"},
+ {"target": "WINNT_x86_64-msvc-x64-asan", "alias": "WINNT_x86_64-msvc"},
+ ]
+ # fmt: on
+ try:
+ fetch_data_for_cdms(cdms, {"url_base": url_base, "version": version_hash})
+ except Exception as e:
+ logging.error("calculate_gmpopenh264_json: could not create JSON due to: %s", e)
+ return ""
+ else:
+ return (
+ "{\n"
+ + ' "hashFunction": "sha512",\n'
+ + ' "name": "OpenH264-{}",\n'.format(version)
+ + ' "schema_version": 1000,\n'
+ + ' "vendors": {\n'
+ + ' "gmp-gmpopenh264": {\n'
+ + ' "platforms": {\n'
+ + generate_json_for_cdms(cdms)
+ + " },\n"
+ + ' "version": "{}"\n'.format(version)
+ + " }\n"
+ + " }\n"
+ + "}"
+ )
+
+
+def calculate_widevinecdm_json(version: str, url_base: str) -> str:
+ # fmt: off
+ cdms = [
+ {"target": "Darwin_aarch64-gcc3", "fileName": "{url_base}/{version}-mac-arm64.zip"},
+ {"target": "Darwin_x86_64-gcc3", "alias": "Darwin_x86_64-gcc3-u-i386-x86_64"},
+ {"target": "Darwin_x86_64-gcc3-u-i386-x86_64", "fileName": "{url_base}/{version}-mac-x64.zip"},
+ {"target": "Linux_x86_64-gcc3", "fileName": "{url_base}/{version}-linux-x64.zip"},
+ {"target": "Linux_x86_64-gcc3-asan", "alias": "Linux_x86_64-gcc3"},
+ {"target": "WINNT_aarch64-msvc-aarch64", "fileName": "{url_base}/{version}-win-arm64.zip"},
+ {"target": "WINNT_x86-msvc", "fileName": "{url_base}/{version}-win-x86.zip"},
+ {"target": "WINNT_x86-msvc-x64", "alias": "WINNT_x86-msvc"},
+ {"target": "WINNT_x86-msvc-x86", "alias": "WINNT_x86-msvc"},
+ {"target": "WINNT_x86_64-msvc", "fileName": "{url_base}/{version}-win-x64.zip"},
+ {"target": "WINNT_x86_64-msvc-x64", "alias": "WINNT_x86_64-msvc"},
+ {"target": "WINNT_x86_64-msvc-x64-asan", "alias": "WINNT_x86_64-msvc"},
+ ]
+ # fmt: on
+ try:
+ fetch_data_for_cdms(cdms, {"url_base": url_base, "version": version})
+ except Exception as e:
+ logging.error("calculate_widevinecdm_json: could not create JSON due to: %s", e)
+ return ""
+ else:
+ return (
+ "{\n"
+ + ' "hashFunction": "sha512",\n'
+ + ' "name": "Widevine-{}",\n'.format(version)
+ + ' "schema_version": 1000,\n'
+ + ' "vendors": {\n'
+ + ' "gmp-widevinecdm": {\n'
+ + ' "platforms": {\n'
+ + generate_json_for_cdms(cdms)
+ + " },\n"
+ + ' "version": "{}"\n'.format(version)
+ + " }\n"
+ + " }\n"
+ + "}"
+ )
+
+
+def main():
+ examples = """examples:
+ python dom/media/tools/generateGmpJson.py widevine 4.10.2557.0 >toolkit/content/gmp-sources/widevinecdm.json
+ python dom/media/tools/generateGmpJson.py --url http://localhost:8080 openh264 2.3.1 0a48f4d2e9be2abb4fb01b4c3be83cf44ce91a6e"""
+
+ parser = argparse.ArgumentParser(
+ description="Generate JSON for GMP plugin updates",
+ epilog=examples,
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+ parser.add_argument("plugin", help="which plugin: openh264, widevine")
+ parser.add_argument("version", help="version of plugin")
+ parser.add_argument("revision", help="revision hash of plugin", nargs="?")
+ parser.add_argument("--url", help="override base URL from which to fetch plugins")
+ args = parser.parse_args()
+
+ if args.plugin == "openh264":
+ url_base = "http://ciscobinary.openh264.org"
+ if args.revision is None:
+ parser.error("openh264 requires revision")
+ elif args.plugin == "widevine":
+ url_base = "https://redirector.gvt1.com/edgedl/widevine-cdm"
+ if args.revision is not None:
+ parser.error("widevine cannot use revision")
+ else:
+ parser.error("plugin not recognized")
+
+ if args.url is not None:
+ url_base = args.url
+
+ if url_base[-1] == "/":
+ url_base = url_base[:-1]
+
+ if args.plugin == "openh264":
+ json_result = calculate_gmpopenh264_json(args.version, args.revision, url_base)
+ elif args.plugin == "widevine":
+ json_result = calculate_widevinecdm_json(args.version, url_base)
+
+ try:
+ json.loads(json_result)
+ except json.JSONDecodeError as e:
+ logging.error("invalid JSON produced: %s", e)
+ else:
+ print(json_result)
+
+
+main()
diff --git a/dom/media/utils/MediaElementEventRunners.cpp b/dom/media/utils/MediaElementEventRunners.cpp
new file mode 100644
index 0000000000..57be04528c
--- /dev/null
+++ b/dom/media/utils/MediaElementEventRunners.cpp
@@ -0,0 +1,140 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaElementEventRunners.h"
+
+#include "mozilla/dom/HTMLMediaElement.h"
+
+extern mozilla::LazyLogModule gMediaElementEventsLog;
+#define LOG_EVENT(type, msg) MOZ_LOG(gMediaElementEventsLog, type, msg)
+
+namespace mozilla::dom {
+
+nsMediaEventRunner::nsMediaEventRunner(const nsAString& aName,
+ HTMLMediaElement* aElement,
+ const nsAString& aEventName)
+ : mElement(aElement),
+ mName(aName),
+ mEventName(aEventName),
+ mLoadID(mElement->GetCurrentLoadID()) {}
+
+bool nsMediaEventRunner::IsCancelled() {
+ return !mElement || mElement->GetCurrentLoadID() != mLoadID;
+}
+
+nsresult nsMediaEventRunner::DispatchEvent(const nsAString& aName) {
+ return mElement ? mElement->DispatchEvent(aName) : NS_OK;
+}
+
+NS_IMPL_CYCLE_COLLECTION(nsMediaEventRunner, mElement)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(nsMediaEventRunner)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(nsMediaEventRunner)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsMediaEventRunner)
+ NS_INTERFACE_MAP_ENTRY(nsINamed)
+ NS_INTERFACE_MAP_ENTRY(nsIRunnable)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIRunnable)
+NS_INTERFACE_MAP_END
+
+NS_IMETHODIMP nsAsyncEventRunner::Run() {
+ // Silently cancel if our load has been cancelled or element has been CCed.
+ return IsCancelled() ? NS_OK : DispatchEvent(mEventName);
+}
+
+nsResolveOrRejectPendingPlayPromisesRunner::
+ nsResolveOrRejectPendingPlayPromisesRunner(
+ HTMLMediaElement* aElement, nsTArray<RefPtr<PlayPromise>>&& aPromises,
+ nsresult aError)
+ : nsMediaEventRunner(u"nsResolveOrRejectPendingPlayPromisesRunner"_ns,
+ aElement),
+ mPromises(std::move(aPromises)),
+ mError(aError) {
+ mElement->mPendingPlayPromisesRunners.AppendElement(this);
+}
+
+void nsResolveOrRejectPendingPlayPromisesRunner::ResolveOrReject() {
+ if (NS_SUCCEEDED(mError)) {
+ PlayPromise::ResolvePromisesWithUndefined(mPromises);
+ } else {
+ PlayPromise::RejectPromises(mPromises, mError);
+ }
+}
+
+NS_IMETHODIMP nsResolveOrRejectPendingPlayPromisesRunner::Run() {
+ if (!IsCancelled()) {
+ ResolveOrReject();
+ }
+
+ mElement->mPendingPlayPromisesRunners.RemoveElement(this);
+ return NS_OK;
+}
+
+NS_IMETHODIMP nsNotifyAboutPlayingRunner::Run() {
+ if (!IsCancelled()) {
+ DispatchEvent(u"playing"_ns);
+ }
+ return nsResolveOrRejectPendingPlayPromisesRunner::Run();
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(nsResolveOrRejectPendingPlayPromisesRunner,
+ nsMediaEventRunner, mPromises)
+NS_IMPL_ADDREF_INHERITED(nsResolveOrRejectPendingPlayPromisesRunner,
+ nsMediaEventRunner)
+NS_IMPL_RELEASE_INHERITED(nsResolveOrRejectPendingPlayPromisesRunner,
+ nsMediaEventRunner)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(
+ nsResolveOrRejectPendingPlayPromisesRunner)
+NS_INTERFACE_MAP_END_INHERITING(nsMediaEventRunner)
+
+NS_IMETHODIMP nsSourceErrorEventRunner::Run() {
+ // Silently cancel if our load has been cancelled.
+ if (IsCancelled()) {
+ return NS_OK;
+ }
+ LOG_EVENT(LogLevel::Debug,
+ ("%p Dispatching simple event source error", mElement.get()));
+ return nsContentUtils::DispatchTrustedEvent(mElement->OwnerDoc(), mSource,
+ u"error"_ns, CanBubble::eNo,
+ Cancelable::eNo);
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(nsSourceErrorEventRunner, nsMediaEventRunner,
+ mSource)
+NS_IMPL_ADDREF_INHERITED(nsSourceErrorEventRunner, nsMediaEventRunner)
+NS_IMPL_RELEASE_INHERITED(nsSourceErrorEventRunner, nsMediaEventRunner)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsSourceErrorEventRunner)
+NS_INTERFACE_MAP_END_INHERITING(nsMediaEventRunner)
+
+NS_IMETHODIMP nsTimeupdateRunner::Run() {
+ if (IsCancelled() || !ShouldDispatchTimeupdate()) {
+ return NS_OK;
+ }
+ // After dispatching `timeupdate`, if the timeupdate event listener takes lots
+ // of time then we end up spending all time handling just timeupdate events.
+ // The spec is vague in this situation, so we choose to update time after we
+ // dispatch the event in order to solve that issue.
+ nsresult rv = DispatchEvent(mEventName);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ LOG_EVENT(LogLevel::Debug,
+ ("%p Failed to dispatch 'timeupdate'", mElement.get()));
+ } else {
+ mElement->UpdateLastTimeupdateDispatchTime();
+ }
+ return rv;
+}
+
+bool nsTimeupdateRunner::ShouldDispatchTimeupdate() const {
+ if (mIsMandatory) {
+ return true;
+ }
+
+ // If the main thread is busy, tasks may be delayed and dispatched at
+ // unexpected times. Ensure we don't dispatch `timeupdate` more often
+ // than once per `TIMEUPDATE_MS`.
+ const TimeStamp& lastTime = mElement->LastTimeupdateDispatchTime();
+ return lastTime.IsNull() || TimeStamp::Now() - lastTime >
+ TimeDuration::FromMilliseconds(TIMEUPDATE_MS);
+}
+
+#undef LOG_EVENT
+} // namespace mozilla::dom
diff --git a/dom/media/utils/MediaElementEventRunners.h b/dom/media/utils/MediaElementEventRunners.h
new file mode 100644
index 0000000000..3f13494493
--- /dev/null
+++ b/dom/media/utils/MediaElementEventRunners.h
@@ -0,0 +1,190 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_media_mediaelementeventrunners_h
+#define mozilla_media_mediaelementeventrunners_h
+
+#include "mozilla/dom/PlayPromise.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIContent.h"
+#include "nsINamed.h"
+#include "nsIRunnable.h"
+#include "nsString.h"
+#include "nsISupportsImpl.h"
+#include "nsTString.h"
+
+namespace mozilla::dom {
+
+class HTMLMediaElement;
+
+// Under certain conditions there may be no-one holding references to
+// a media element from script, DOM parent, etc, but the element may still
+// fire meaningful events in the future so we can't destroy it yet:
+// 1) If the element is delaying the load event (or would be, if it were
+// in a document), then events up to loadeddata or error could be fired,
+// so we need to stay alive.
+// 2) If the element is not paused and playback has not ended, then
+// we will (or might) play, sending timeupdate and ended events and possibly
+// audio output, so we need to stay alive.
+// 3) if the element is seeking then we will fire seeking events and possibly
+// start playing afterward, so we need to stay alive.
+// 4) If autoplay could start playback in this element (if we got enough data),
+// then we need to stay alive.
+// 5) if the element is currently loading, not suspended, and its source is
+// not a MediaSource, then script might be waiting for progress events or a
+// 'stalled' or 'suspend' event, so we need to stay alive.
+// If we're already suspended then (all other conditions being met),
+// it's OK to just disappear without firing any more events,
+// since we have the freedom to remain suspended indefinitely. Note
+// that we could use this 'suspended' loophole to garbage-collect a suspended
+// element in case 4 even if it had 'autoplay' set, but we choose not to.
+// If someone throws away all references to a loading 'autoplay' element
+// sound should still eventually play.
+// 6) If the source is a MediaSource, most loading events will not fire unless
+// appendBuffer() is called on a SourceBuffer, in which case something is
+// already referencing the SourceBuffer, which keeps the associated media
+// element alive. Further, a MediaSource will never time out the resource
+// fetch, and so should not keep the media element alive if it is
+// unreferenced. A pending 'stalled' event keeps the media element alive.
+//
+// Media elements owned by inactive documents (i.e. documents not contained in
+// any document viewer) should never hold a self-reference because none of the
+// above conditions are allowed: the element will stop loading and playing
+// and never resume loading or playing unless its owner document changes to
+// an active document (which can only happen if there is an external reference
+// to the element).
+// Media elements with no owner doc should be able to hold a self-reference.
+// Something native must have created the element and may expect it to
+// stay alive to play.
+
+// It's very important that any change in state which could change the value of
+// needSelfReference in AddRemoveSelfReference be followed by a call to
+// AddRemoveSelfReference before this element could die!
+// It's especially important if needSelfReference would change to 'true',
+// since if we neglect to add a self-reference, this element might be
+// garbage collected while there are still event listeners that should
+// receive events. If we neglect to remove the self-reference then the element
+// just lives longer than it needs to.
+
+class nsMediaEventRunner : public nsIRunnable, public nsINamed {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(nsMediaEventRunner, nsIRunnable)
+
+ explicit nsMediaEventRunner(const nsAString& aName,
+ HTMLMediaElement* aElement,
+ const nsAString& aEventName = u"unknown"_ns);
+
+ void Cancel() { mElement = nullptr; }
+ NS_IMETHODIMP GetName(nsACString& aName) override {
+ aName = NS_ConvertUTF16toUTF8(mName).get();
+ return NS_OK;
+ }
+ nsString Name() const { return mName; }
+ nsString EventName() const { return mEventName; }
+
+ protected:
+ virtual ~nsMediaEventRunner() = default;
+ bool IsCancelled();
+ nsresult DispatchEvent(const nsAString& aName);
+
+ RefPtr<HTMLMediaElement> mElement;
+ nsString mName;
+ nsString mEventName;
+ uint32_t mLoadID;
+};
+
+/**
+ * This runner is used to dispatch async event on media element.
+ */
+class nsAsyncEventRunner : public nsMediaEventRunner {
+ public:
+ nsAsyncEventRunner(const nsAString& aEventName, HTMLMediaElement* aElement)
+ : nsMediaEventRunner(u"nsAsyncEventRunner"_ns, aElement, aEventName) {}
+ NS_IMETHOD Run() override;
+};
+
+/**
+ * These runners are used to handle `playing` event and address play promise.
+ *
+ * If no error is passed while constructing an instance, the instance will
+ * resolve the passed promises with undefined; otherwise, the instance will
+ * reject the passed promises with the passed error.
+ *
+ * The constructor appends the constructed instance into the passed media
+ * element's mPendingPlayPromisesRunners member and once the the runner is run
+ * (whether fulfilled or canceled), it removes itself from
+ * mPendingPlayPromisesRunners.
+ */
+class nsResolveOrRejectPendingPlayPromisesRunner : public nsMediaEventRunner {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(
+ nsResolveOrRejectPendingPlayPromisesRunner, nsMediaEventRunner)
+
+ nsResolveOrRejectPendingPlayPromisesRunner(
+ HTMLMediaElement* aElement, nsTArray<RefPtr<PlayPromise>>&& aPromises,
+ nsresult aError = NS_OK);
+ void ResolveOrReject();
+ NS_IMETHOD Run() override;
+
+ protected:
+ virtual ~nsResolveOrRejectPendingPlayPromisesRunner() = default;
+
+ private:
+ nsTArray<RefPtr<PlayPromise>> mPromises;
+ nsresult mError;
+};
+
+class nsNotifyAboutPlayingRunner
+ : public nsResolveOrRejectPendingPlayPromisesRunner {
+ public:
+ nsNotifyAboutPlayingRunner(
+ HTMLMediaElement* aElement,
+ nsTArray<RefPtr<PlayPromise>>&& aPendingPlayPromises)
+ : nsResolveOrRejectPendingPlayPromisesRunner(
+ aElement, std::move(aPendingPlayPromises)) {}
+ NS_IMETHOD Run() override;
+};
+
+/**
+ * This runner is used to dispatch a source error event, which would happen when
+ * loading resource failed.
+ */
+class nsSourceErrorEventRunner : public nsMediaEventRunner {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(nsSourceErrorEventRunner,
+ nsMediaEventRunner)
+ nsSourceErrorEventRunner(HTMLMediaElement* aElement, nsIContent* aSource)
+ : nsMediaEventRunner(u"nsSourceErrorEventRunner"_ns, aElement),
+ mSource(aSource) {}
+ NS_IMETHOD Run() override;
+
+ private:
+ virtual ~nsSourceErrorEventRunner() = default;
+ nsCOMPtr<nsIContent> mSource;
+};
+
+/**
+ * This runner is used to dispatch `timeupdate` event and ensure we don't
+ * dispatch `timeupdate` more often than once per `TIMEUPDATE_MS` if that is not
+ * a mandatory event.
+ */
+class nsTimeupdateRunner : public nsMediaEventRunner {
+ public:
+ nsTimeupdateRunner(HTMLMediaElement* aElement, bool aIsMandatory)
+ : nsMediaEventRunner(u"nsTimeupdateRunner"_ns, aElement,
+ u"timeupdate"_ns),
+ mIsMandatory(aIsMandatory) {}
+ NS_IMETHOD Run() override;
+
+ private:
+ bool ShouldDispatchTimeupdate() const;
+ bool mIsMandatory;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_media_mediaelementeventrunners_h
diff --git a/dom/media/utils/PerformanceRecorder.cpp b/dom/media/utils/PerformanceRecorder.cpp
new file mode 100644
index 0000000000..d6124e8cf6
--- /dev/null
+++ b/dom/media/utils/PerformanceRecorder.cpp
@@ -0,0 +1,308 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "PerformanceRecorder.h"
+
+#include "base/process_util.h"
+#include "mozilla/Logging.h"
+#include "mozilla/gfx/Types.h"
+#include "nsPrintfCString.h"
+
+namespace mozilla {
+
+static const char* SourceToStr(TrackingId::Source aSource) {
+ switch (aSource) {
+ case TrackingId::Source::Unimplemented:
+ MOZ_ASSERT_UNREACHABLE("Unimplemented TrackingId Source");
+ return "Unimplemented";
+ case TrackingId::Source::AudioDestinationNode:
+ return "AudioDestinationNode";
+ case TrackingId::Source::Camera:
+ return "CameraCapture";
+ case TrackingId::Source::Canvas:
+ return "CanvasCapture";
+ case TrackingId::Source::ChannelDecoder:
+ return "ChannelDecoder";
+ case TrackingId::Source::HLSDecoder:
+ return "HLSDecoder";
+ case TrackingId::Source::MediaCapabilities:
+ return "MediaCapabilities";
+ case TrackingId::Source::MediaElementDecoder:
+ return "MediaElementDecoderCapture";
+ case TrackingId::Source::MediaElementStream:
+ return "MediaElementStreamCapture";
+ case TrackingId::Source::MSEDecoder:
+ return "MSEDecoder";
+ case TrackingId::Source::RTCRtpReceiver:
+ return "RTCRtpReceiver";
+ case TrackingId::Source::Screen:
+ return "ScreenCapture";
+ case TrackingId::Source::Tab:
+ return "TabCapture";
+ case TrackingId::Source::Window:
+ return "WindowCapture";
+ case TrackingId::Source::LAST:
+ MOZ_ASSERT_UNREACHABLE("Invalid TrackingId Source");
+ return "Invalid";
+ }
+ MOZ_ASSERT_UNREACHABLE("Unexpected TrackingId Source");
+ return "Unexpected";
+}
+
+TrackingId::TrackingId() : mSource(Source::Unimplemented), mUniqueInProcId(0) {}
+
+TrackingId::TrackingId(
+ Source aSource, uint32_t aUniqueInProcId,
+ TrackAcrossProcesses aTrack /* = TrackAcrossProcesses::NO */)
+ : mSource(aSource),
+ mUniqueInProcId(aUniqueInProcId),
+ mProcId(aTrack == TrackAcrossProcesses::Yes
+ ? Some(base::GetCurrentProcId())
+ : Nothing()) {}
+
+nsCString TrackingId::ToString() const {
+ if (mProcId) {
+ return nsPrintfCString("%s-%u-%u", SourceToStr(mSource), *mProcId,
+ mUniqueInProcId);
+ }
+ return nsPrintfCString("%s-%u", SourceToStr(mSource), mUniqueInProcId);
+}
+
+static const char* StageToStr(MediaStage aStage) {
+ switch (aStage) {
+ case MediaStage::RequestData:
+ return "RequestData";
+ case MediaStage::RequestDemux:
+ return "RequestDemux";
+ case MediaStage::CopyDemuxedData:
+ return "CopyDemuxedData";
+ case MediaStage::RequestDecode:
+ return "RequestDecode";
+ case MediaStage::CopyDecodedVideo:
+ return "CopyDecodedVideo";
+ default:
+ return "InvalidStage";
+ }
+}
+
+static void AppendMediaInfoFlagToName(nsCString& aName, MediaInfoFlag aFlag) {
+ if (aFlag & MediaInfoFlag::KeyFrame) {
+ aName.Append("kf,");
+ }
+ // Decoding
+ if (aFlag & MediaInfoFlag::SoftwareDecoding) {
+ aName.Append("sw,");
+ } else if (aFlag & MediaInfoFlag::HardwareDecoding) {
+ aName.Append("hw,");
+ }
+ // Codec type
+ if (aFlag & MediaInfoFlag::VIDEO_AV1) {
+ aName.Append("av1,");
+ } else if (aFlag & MediaInfoFlag::VIDEO_H264) {
+ aName.Append("h264,");
+ } else if (aFlag & MediaInfoFlag::VIDEO_VP8) {
+ aName.Append("vp8,");
+ } else if (aFlag & MediaInfoFlag::VIDEO_VP9) {
+ aName.Append("vp9,");
+ } else if (aFlag & MediaInfoFlag::VIDEO_THEORA) {
+ aName.Append("theora,");
+ }
+}
+
+static void AppendImageFormatToName(nsCString& aName,
+ DecodeStage::ImageFormat aFormat) {
+ aName.Append([&] {
+ switch (aFormat) {
+ case DecodeStage::YUV420P:
+ return "yuv420p,";
+ case DecodeStage::YUV422P:
+ return "yuv422p,";
+ case DecodeStage::YUV444P:
+ return "yuv444p,";
+ case DecodeStage::NV12:
+ return "nv12,";
+ case DecodeStage::YV12:
+ return "yv12,";
+ case DecodeStage::NV21:
+ return "nv21,";
+ case DecodeStage::P010:
+ return "p010,";
+ case DecodeStage::P016:
+ return "p016,";
+ case DecodeStage::RGBA32:
+ return "rgba32,";
+ case DecodeStage::RGB24:
+ return "rgb24,";
+ case DecodeStage::GBRP:
+ return "gbrp,";
+ case DecodeStage::ANDROID_SURFACE:
+ return "android.Surface,";
+ }
+ MOZ_ASSERT_UNREACHABLE("Unhandled DecodeStage::ImageFormat");
+ return "";
+ }());
+}
+
+static void AppendYUVColorSpaceToName(nsCString& aName,
+ gfx::YUVColorSpace aSpace) {
+ aName.Append([&] {
+ switch (aSpace) {
+ case gfx::YUVColorSpace::BT601:
+ return "space=BT.601,";
+ case gfx::YUVColorSpace::BT709:
+ return "space=BT.709,";
+ case gfx::YUVColorSpace::BT2020:
+ return "space=BT.2020,";
+ case gfx::YUVColorSpace::Identity:
+ return "space=Identity,";
+ }
+ MOZ_ASSERT_UNREACHABLE("Unhandled gfx::YUVColorSpace");
+ return "";
+ }());
+}
+
+static void AppendColorRangeToName(nsCString& aName, gfx::ColorRange aRange) {
+ aName.Append([&] {
+ switch (aRange) {
+ case gfx::ColorRange::LIMITED:
+ return "range=Limited,";
+ case gfx::ColorRange::FULL:
+ return "range=Full,";
+ }
+ MOZ_ASSERT_UNREACHABLE("Unhandled gfx::ColorRange");
+ return "";
+ }());
+}
+
+static void AppendColorDepthToName(nsCString& aName, gfx::ColorDepth aDepth) {
+ aName.Append([&] {
+ switch (aDepth) {
+ case gfx::ColorDepth::COLOR_8:
+ return "depth=8,";
+ case gfx::ColorDepth::COLOR_10:
+ return "depth=10,";
+ case gfx::ColorDepth::COLOR_12:
+ return "depth=12,";
+ case gfx::ColorDepth::COLOR_16:
+ return "depth=16,";
+ }
+ MOZ_ASSERT_UNREACHABLE("Unhandled gfx::ColorDepth");
+ return "";
+ }());
+}
+
+/* static */
+const char* FindMediaResolution(int32_t aHeight) {
+ static const struct {
+ const int32_t mH;
+ const nsCString mRes;
+ } sResolutions[] = {{0, "A:0"_ns}, // other followings are for video
+ {240, "V:0<h<=240"_ns},
+ {480, "V:240<h<=480"_ns},
+ {576, "V:480<h<=576"_ns},
+ {720, "V:576<h<=720"_ns},
+ {1080, "V:720<h<=1080"_ns},
+ {1440, "V:1080<h<=1440"_ns},
+ {2160, "V:1440<h<=2160"_ns},
+ {INT_MAX, "V:h>2160"_ns}};
+ const char* resolution = sResolutions[0].mRes.get();
+ for (auto&& res : sResolutions) {
+ if (aHeight <= res.mH) {
+ resolution = res.mRes.get();
+ break;
+ }
+ }
+ return resolution;
+}
+
+/* static */
+bool PerformanceRecorderBase::IsMeasurementEnabled() {
+ return profiler_thread_is_being_profiled_for_markers() ||
+ PerformanceRecorderBase::sEnableMeasurementForTesting;
+}
+
+/* static */
+TimeStamp PerformanceRecorderBase::GetCurrentTimeForMeasurement() {
+ // The system call to get the clock is rather expensive on Windows. As we
+ // only report the measurement report via markers, if the marker isn't enabled
+ // then we won't do any measurement in order to save CPU time.
+ return IsMeasurementEnabled() ? TimeStamp::Now() : TimeStamp();
+}
+
+ProfilerString8View PlaybackStage::Name() const {
+ if (!mName) {
+ mName.emplace(StageToStr(mStage));
+ mName->Append(":");
+ mName->Append(FindMediaResolution(mHeight));
+ mName->Append(":");
+ AppendMediaInfoFlagToName(*mName, mFlag);
+ }
+ return *mName;
+}
+
+ProfilerString8View CaptureStage::Name() const {
+ if (!mName) {
+ auto imageTypeToStr = [](ImageType aType) -> const char* {
+ switch (aType) {
+ case ImageType::I420:
+ return "I420";
+ case ImageType::YUY2:
+ return "YUY2";
+ case ImageType::YV12:
+ return "YV12";
+ case ImageType::UYVY:
+ return "UYVY";
+ case ImageType::NV12:
+ return "NV12";
+ case ImageType::NV21:
+ return "NV21";
+ case ImageType::MJPEG:
+ return "MJPEG";
+ case ImageType::Unknown:
+ return "(unknown image type)";
+ default:
+ return "(unimplemented image type)";
+ };
+ };
+ mName = Some(nsPrintfCString(
+ "CaptureVideoFrame %s %dx%d %s %s", mSource.Data(), mWidth, mHeight,
+ imageTypeToStr(mImageType), mTrackingId.ToString().get()));
+ }
+ return *mName;
+}
+
+ProfilerString8View CopyVideoStage::Name() const {
+ if (!mName) {
+ mName =
+ Some(nsPrintfCString("CopyVideoFrame %s %dx%d %s", mSource.Data(),
+ mWidth, mHeight, mTrackingId.ToString().get()));
+ }
+ return *mName;
+}
+
+ProfilerString8View DecodeStage::Name() const {
+ if (!mName) {
+ nsCString extras;
+ AppendMediaInfoFlagToName(extras, mFlag);
+ mImageFormat.apply(
+ [&](ImageFormat aFormat) { AppendImageFormatToName(extras, aFormat); });
+ mColorDepth.apply([&](gfx::ColorDepth aDepth) {
+ AppendColorDepthToName(extras, aDepth);
+ });
+ mColorRange.apply([&](gfx::ColorRange aRange) {
+ AppendColorRangeToName(extras, aRange);
+ });
+ mYUVColorSpace.apply([&](gfx::YUVColorSpace aColorSpace) {
+ AppendYUVColorSpaceToName(extras, aColorSpace);
+ });
+ mName = Some(nsPrintfCString("DecodeFrame %s %dx%d %s %s", mSource.Data(),
+ mWidth.valueOr(-1), mHeight.valueOr(-1),
+ extras.get(), mTrackingId.ToString().get()));
+ }
+ return *mName;
+}
+
+} // namespace mozilla
diff --git a/dom/media/utils/PerformanceRecorder.h b/dom/media/utils/PerformanceRecorder.h
new file mode 100644
index 0000000000..582d56e5e3
--- /dev/null
+++ b/dom/media/utils/PerformanceRecorder.h
@@ -0,0 +1,407 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_PerformanceRecorder_h
+#define mozilla_PerformanceRecorder_h
+
+#include <type_traits>
+
+#include "mozilla/Attributes.h"
+#include "mozilla/BaseProfilerMarkersPrerequisites.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/TypedEnumBits.h"
+#include "nsStringFwd.h"
+#include "nsTPriorityQueue.h"
+#include "mozilla/ProfilerMarkers.h"
+
+namespace mozilla {
+namespace gfx {
+enum class YUVColorSpace : uint8_t;
+enum class ColorDepth : uint8_t;
+enum class ColorRange : uint8_t;
+} // namespace gfx
+
+struct TrackingId {
+ enum class Source : uint8_t {
+ Unimplemented,
+ AudioDestinationNode,
+ Camera,
+ Canvas,
+ ChannelDecoder,
+ HLSDecoder,
+ MediaCapabilities,
+ MediaElementDecoder,
+ MediaElementStream,
+ MSEDecoder,
+ RTCRtpReceiver,
+ Screen,
+ Tab,
+ Window,
+ LAST
+ };
+ enum class TrackAcrossProcesses : uint8_t {
+ Yes,
+ No,
+ };
+ TrackingId();
+ TrackingId(Source aSource, uint32_t aUniqueInProcId,
+ TrackAcrossProcesses aTrack = TrackAcrossProcesses::No);
+
+ nsCString ToString() const;
+
+ Source mSource;
+ uint32_t mUniqueInProcId;
+ Maybe<uint32_t> mProcId;
+};
+
+enum class MediaInfoFlag : uint16_t {
+ None = (0 << 0),
+ NonKeyFrame = (1 << 0),
+ KeyFrame = (1 << 1),
+ SoftwareDecoding = (1 << 2),
+ HardwareDecoding = (1 << 3),
+ VIDEO_AV1 = (1 << 4),
+ VIDEO_H264 = (1 << 5),
+ VIDEO_VP8 = (1 << 6),
+ VIDEO_VP9 = (1 << 7),
+ VIDEO_THEORA = (1 << 8),
+};
+MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(MediaInfoFlag)
+
+/**
+ * This represents the different stages that a media data will go through
+ * within the playback journey.
+ *
+ * |---| |---| |------|
+ * Copy Demuxed Copy Demuxed Copy Decoded
+ * Data Data Video
+ * |------------- | |-----------------------------------|
+ * Request Demux Request Decode
+ * |-----------------------------------------------------------|
+ * Request Data
+ *
+ * RequestData : Record the time where MediaDecoderStateMachine(MDSM) starts
+ * asking for a decoded data to MDSM receives a decoded data.
+ *
+ * RequestDemux : Record the time where MediaFormatReader(MFR) starts asking
+ * a demuxed sample to MFR received a demuxed sample. This stage is a sub-
+ * stage of RequestData.
+ *
+ * CopyDemuxedData : On some situations, we will need to copy the demuxed
+ * data, which is still not decoded yet so its size is still small. This
+ * records the time which we spend on copying data. This stage could happen
+ * multiple times, either being a sub-stage of RequestDemux (in MSE case),
+ * or being a sub-stage of RequestDecode (when sending data via IPC).
+ *
+ * RequestDecode : Record the time where MFR starts asking decoder to return
+ * a decoded data to MFR receives a decoded data. As the decoder might be
+ * remote, this stage might include the time spending on IPC trips. This
+ * stage is a sub-stage of RequestData.
+ *
+ * CopyDecodedVideo : If we can't reuse same decoder texture to the
+ * compositor, then we have to copy video data to to another sharable
+ * texture. This records the time which we spend on copying data. This stage
+ * is a sub- stage of RequestDecode.
+ */
+enum class MediaStage : uint8_t {
+ Invalid,
+ RequestData,
+ RequestDemux,
+ CopyDemuxedData,
+ RequestDecode,
+ CopyDecodedVideo,
+};
+
+class PlaybackStage {
+ public:
+ explicit PlaybackStage(MediaStage aStage, int32_t aHeight = 0,
+ MediaInfoFlag aFlag = MediaInfoFlag::None)
+ : mStage(aStage), mHeight(aHeight), mFlag(aFlag) {
+ MOZ_ASSERT(aStage != MediaStage::Invalid);
+ }
+
+ ProfilerString8View Name() const;
+ const MarkerCategory& Category() const {
+ return baseprofiler::category::MEDIA_PLAYBACK;
+ }
+
+ MediaStage mStage;
+ int32_t mHeight;
+ MediaInfoFlag mFlag;
+
+ private:
+ mutable Maybe<nsCString> mName;
+};
+
+class CaptureStage {
+ public:
+ enum class ImageType : uint8_t {
+ Unknown,
+ I420,
+ YUY2,
+ YV12,
+ UYVY,
+ NV12,
+ NV21,
+ MJPEG,
+ };
+
+ CaptureStage(nsCString aSource, TrackingId aTrackingId, int32_t aWidth,
+ int32_t aHeight, ImageType aImageType)
+ : mSource(std::move(aSource)),
+ mTrackingId(std::move(aTrackingId)),
+ mWidth(aWidth),
+ mHeight(aHeight),
+ mImageType(aImageType) {}
+
+ ProfilerString8View Name() const;
+ const MarkerCategory& Category() const {
+ return baseprofiler::category::MEDIA_RT;
+ }
+
+ nsCString mSource;
+ TrackingId mTrackingId;
+ int32_t mWidth;
+ int32_t mHeight;
+ ImageType mImageType;
+
+ private:
+ mutable Maybe<nsCString> mName;
+};
+
+class CopyVideoStage {
+ public:
+ CopyVideoStage(nsCString aSource, TrackingId aTrackingId, int32_t aWidth,
+ int32_t aHeight)
+ : mSource(std::move(aSource)),
+ mTrackingId(std::move(aTrackingId)),
+ mWidth(aWidth),
+ mHeight(aHeight) {}
+
+ ProfilerString8View Name() const;
+ const MarkerCategory& Category() const {
+ return baseprofiler::category::MEDIA_RT;
+ }
+
+ // The name of the source that performs this stage.
+ nsCString mSource;
+ // A unique id identifying the source of the video frame this stage is
+ // performed for.
+ TrackingId mTrackingId;
+ int32_t mWidth;
+ int32_t mHeight;
+
+ private:
+ mutable Maybe<nsCString> mName;
+};
+
+class DecodeStage {
+ public:
+ enum ImageFormat : uint8_t {
+ YUV420P,
+ YUV422P,
+ YUV444P,
+ NV12,
+ YV12,
+ NV21,
+ P010,
+ P016,
+ RGBA32,
+ RGB24,
+ GBRP,
+ ANDROID_SURFACE,
+ };
+
+ DecodeStage(nsCString aSource, TrackingId aTrackingId, MediaInfoFlag aFlag)
+ : mSource(std::move(aSource)),
+ mTrackingId(std::move(aTrackingId)),
+ mFlag(aFlag) {}
+ ProfilerString8View Name() const;
+ const MarkerCategory& Category() const {
+ return baseprofiler::category::MEDIA_PLAYBACK;
+ }
+
+ void SetResolution(int aWidth, int aHeight) {
+ mWidth = Some(aWidth);
+ mHeight = Some(aHeight);
+ }
+ void SetImageFormat(ImageFormat aFormat) { mImageFormat = Some(aFormat); }
+ void SetYUVColorSpace(gfx::YUVColorSpace aColorSpace) {
+ mYUVColorSpace = Some(aColorSpace);
+ }
+ void SetColorRange(gfx::ColorRange aColorRange) {
+ mColorRange = Some(aColorRange);
+ }
+ void SetColorDepth(gfx::ColorDepth aColorDepth) {
+ mColorDepth = Some(aColorDepth);
+ }
+
+ // The name of the source that performs this stage.
+ nsCString mSource;
+ // A unique id identifying the source of the video frame this stage is
+ // performed for.
+ TrackingId mTrackingId;
+ MediaInfoFlag mFlag;
+ Maybe<int> mWidth;
+ Maybe<int> mHeight;
+ Maybe<ImageFormat> mImageFormat;
+ Maybe<gfx::YUVColorSpace> mYUVColorSpace;
+ Maybe<gfx::ColorRange> mColorRange;
+ Maybe<gfx::ColorDepth> mColorDepth;
+ mutable Maybe<nsCString> mName;
+};
+
+class PerformanceRecorderBase {
+ public:
+ static bool IsMeasurementEnabled();
+ static TimeStamp GetCurrentTimeForMeasurement();
+
+ // Return the resolution range for the given height. Eg. V:1080<h<=1440.
+ static const char* FindMediaResolution(int32_t aHeight);
+
+ protected:
+ // We would enable the measurement on testing.
+ static inline bool sEnableMeasurementForTesting = false;
+};
+
+template <typename StageType>
+class PerformanceRecorderImpl : public PerformanceRecorderBase {
+ public:
+ ~PerformanceRecorderImpl() = default;
+
+ PerformanceRecorderImpl(PerformanceRecorderImpl&& aRhs) noexcept
+ : mStages(std::move(aRhs.mStages)) {}
+ PerformanceRecorderImpl& operator=(PerformanceRecorderImpl&&) = delete;
+ PerformanceRecorderImpl(const PerformanceRecorderImpl&) = delete;
+ PerformanceRecorderImpl& operator=(const PerformanceRecorderImpl&) = delete;
+
+ protected:
+ PerformanceRecorderImpl() = default;
+
+ // Stores the stage with the current time as its start time, associated with
+ // aId.
+ template <typename... Args>
+ void Start(int64_t aId, Args... aArgs) {
+ if (IsMeasurementEnabled()) {
+ MutexAutoLock lock(mMutex);
+ mStages.Push(std::make_tuple(aId, GetCurrentTimeForMeasurement(),
+ StageType(std::move(aArgs)...)));
+ }
+ }
+
+ // Return the passed time since creation of the aId stage in microseconds if
+ // it has not yet been recorded. Other stages with lower ids will be
+ // discarded. Otherwise, return 0.
+ template <typename F>
+ float Record(int64_t aId, F&& aStageMutator) {
+ Maybe<Entry> entry;
+ {
+ MutexAutoLock lock(mMutex);
+ while (!mStages.IsEmpty() && std::get<0>(mStages.Top()) < aId) {
+ mStages.Pop();
+ }
+ if (mStages.IsEmpty()) {
+ return 0.0;
+ }
+ if (std::get<0>(mStages.Top()) != aId) {
+ return 0.0;
+ }
+ entry = Some(mStages.Pop());
+ }
+ const auto& startTime = std::get<1>(*entry);
+ auto& stage = std::get<2>(*entry);
+ MOZ_ASSERT(std::get<0>(*entry) == aId);
+ double elapsedTimeUs = 0.0;
+ if (!startTime.IsNull() && IsMeasurementEnabled()) {
+ const auto now = TimeStamp::Now();
+ elapsedTimeUs = (now - startTime).ToMicroseconds();
+ MOZ_ASSERT(elapsedTimeUs >= 0, "Elapsed time can't be less than 0!");
+ aStageMutator(stage);
+ AUTO_PROFILER_STATS(PROFILER_MARKER_UNTYPED);
+ ::profiler_add_marker(
+ stage.Name(), stage.Category(),
+ MarkerOptions(MarkerTiming::Interval(startTime, now)));
+ }
+ return static_cast<float>(elapsedTimeUs);
+ }
+ float Record(int64_t aId) {
+ return Record(aId, [](auto&) {});
+ }
+
+ protected:
+ using Entry = std::tuple<int64_t, TimeStamp, StageType>;
+
+ struct IdComparator {
+ bool LessThan(const Entry& aTupleA, const Entry& aTupleB) {
+ return std::get<0>(aTupleA) < std::get<0>(aTupleB);
+ }
+ };
+
+ Mutex mMutex{"PerformanceRecorder::mMutex"};
+ nsTPriorityQueue<Entry, IdComparator> mStages MOZ_GUARDED_BY(mMutex);
+};
+
+/**
+ * This class is used to record the time spent on different stages in the media
+ * pipeline. `Record()` needs to be called explicitly to record a profiler
+ * marker registering the time passed since creation. A stage may be mutated in
+ * `Record()` in case data has become available since the recorder started.
+ *
+ * This variant is intended to be created on the stack when a stage starts, then
+ * recorded with `Record()` when the stage is finished.
+ */
+template <typename StageType>
+class PerformanceRecorder : public PerformanceRecorderImpl<StageType> {
+ using Super = PerformanceRecorderImpl<StageType>;
+
+ public:
+ template <typename... Args>
+ explicit PerformanceRecorder(Args... aArgs) {
+ Start(std::move(aArgs)...);
+ };
+
+ private:
+ template <typename... Args>
+ void Start(Args... aArgs) {
+ Super::Start(0, std::move(aArgs)...);
+ }
+
+ public:
+ template <typename F>
+ float Record(F&& aStageMutator) {
+ return Super::Record(0, std::forward<F>(aStageMutator));
+ }
+ float Record() { return Super::Record(0); }
+};
+
+/**
+ * This class is used to record the time spent on different stages in the media
+ * pipeline. `Start()` and `Record()` needs to be called explicitly to record a
+ * profiler marker registering the time passed since creation. A stage may be
+ * mutated in `Record()` in case data has become available since the recorder
+ * started.
+ *
+ * This variant is intended to be kept as a member in a class and supports async
+ * stages. The async stages may overlap each other. To distinguish different
+ * stages from each other, an int64_t is used as identifier. This is often a
+ * timestamp in microseconds, see TimeUnit::ToMicroseconds.
+ */
+template <typename StageType>
+class PerformanceRecorderMulti : public PerformanceRecorderImpl<StageType> {
+ using Super = PerformanceRecorderImpl<StageType>;
+
+ public:
+ PerformanceRecorderMulti() = default;
+
+ using Super::Record;
+ using Super::Start;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_PerformanceRecorder_h
diff --git a/dom/media/utils/TelemetryProbesReporter.cpp b/dom/media/utils/TelemetryProbesReporter.cpp
new file mode 100644
index 0000000000..dfc4e82241
--- /dev/null
+++ b/dom/media/utils/TelemetryProbesReporter.cpp
@@ -0,0 +1,673 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TelemetryProbesReporter.h"
+
+#include <cmath>
+
+#include "FrameStatistics.h"
+#include "VideoUtils.h"
+#include "mozilla/EMEUtils.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+
+LazyLogModule gTelemetryProbesReporterLog("TelemetryProbesReporter");
+#define LOG(msg, ...) \
+ MOZ_LOG(gTelemetryProbesReporterLog, LogLevel::Debug, \
+ ("TelemetryProbesReporter=%p, " msg, this, ##__VA_ARGS__))
+
+static const char* ToVisibilityStr(
+ TelemetryProbesReporter::Visibility aVisibility) {
+ switch (aVisibility) {
+ case TelemetryProbesReporter::Visibility::eVisible:
+ return "visible";
+ case TelemetryProbesReporter::Visibility::eInvisible:
+ return "invisible";
+ case TelemetryProbesReporter::Visibility::eInitial:
+ return "initial";
+ default:
+ MOZ_ASSERT_UNREACHABLE("invalid visibility");
+ return "unknown";
+ }
+}
+static const char* ToAudibilityStr(
+ TelemetryProbesReporter::AudibleState aAudibleState) {
+ switch (aAudibleState) {
+ case TelemetryProbesReporter::AudibleState::eAudible:
+ return "audible";
+ case TelemetryProbesReporter::AudibleState::eNotAudible:
+ return "inaudible";
+ default:
+ MOZ_ASSERT_UNREACHABLE("invalid audibility");
+ return "unknown";
+ }
+}
+
+static const char* ToMutedStr(bool aMuted) {
+ return aMuted ? "muted" : "unmuted";
+}
+
+MediaContent TelemetryProbesReporter::MediaInfoToMediaContent(
+ const MediaInfo& aInfo) {
+ MediaContent content = MediaContent::MEDIA_HAS_NOTHING;
+ if (aInfo.HasAudio()) {
+ content |= MediaContent::MEDIA_HAS_AUDIO;
+ }
+ if (aInfo.HasVideo()) {
+ content |= MediaContent::MEDIA_HAS_VIDEO;
+ if (aInfo.mVideo.GetAsVideoInfo()->mColorDepth > gfx::ColorDepth::COLOR_8) {
+ content |= MediaContent::MEDIA_HAS_COLOR_DEPTH_ABOVE_8;
+ }
+ }
+ return content;
+}
+
+TelemetryProbesReporter::TelemetryProbesReporter(
+ TelemetryProbesReporterOwner* aOwner)
+ : mOwner(aOwner) {
+ MOZ_ASSERT(mOwner);
+}
+
+void TelemetryProbesReporter::OnPlay(Visibility aVisibility,
+ MediaContent aMediaContent,
+ bool aIsMuted) {
+ LOG("Start time accumulation for total play time");
+
+ AssertOnMainThreadAndNotShutdown();
+ MOZ_ASSERT_IF(mMediaContent & MediaContent::MEDIA_HAS_VIDEO,
+ !mTotalVideoPlayTime.IsStarted());
+ MOZ_ASSERT_IF(mMediaContent & MediaContent::MEDIA_HAS_AUDIO,
+ !mTotalAudioPlayTime.IsStarted());
+
+ if (aMediaContent & MediaContent::MEDIA_HAS_VIDEO) {
+ mTotalVideoPlayTime.Start();
+
+ MOZ_ASSERT_IF(mMediaContent & MediaContent::MEDIA_HAS_COLOR_DEPTH_ABOVE_8,
+ !mTotalVideoHDRPlayTime.IsStarted());
+ if (aMediaContent & MediaContent::MEDIA_HAS_COLOR_DEPTH_ABOVE_8) {
+ mTotalVideoHDRPlayTime.Start();
+ }
+ }
+ if (aMediaContent & MediaContent::MEDIA_HAS_AUDIO) {
+ mTotalAudioPlayTime.Start();
+ }
+
+ OnMediaContentChanged(aMediaContent);
+ OnVisibilityChanged(aVisibility);
+ OnMutedChanged(aIsMuted);
+
+ mOwner->DispatchAsyncTestingEvent(u"moztotalplaytimestarted"_ns);
+
+ mIsPlaying = true;
+}
+
+void TelemetryProbesReporter::OnPause(Visibility aVisibility) {
+ if (!mIsPlaying) {
+ // Not started
+ LOG("TelemetryProbesReporter::OnPause: not started, early return");
+ return;
+ }
+
+ LOG("Pause time accumulation for total play time");
+
+ AssertOnMainThreadAndNotShutdown();
+ MOZ_ASSERT_IF(mMediaContent & MediaContent::MEDIA_HAS_VIDEO,
+ mTotalVideoPlayTime.IsStarted());
+ MOZ_ASSERT_IF(mMediaContent & MediaContent::MEDIA_HAS_AUDIO,
+ mTotalAudioPlayTime.IsStarted());
+
+ if (mMediaContent & MediaContent::MEDIA_HAS_VIDEO) {
+ MOZ_ASSERT_IF(mMediaContent & MediaContent::MEDIA_HAS_COLOR_DEPTH_ABOVE_8,
+ mTotalVideoHDRPlayTime.IsStarted());
+
+ LOG("Pause video time accumulation for total play time");
+ if (mInvisibleVideoPlayTime.IsStarted()) {
+ LOG("Pause invisible video time accumulation for total play time");
+ PauseInvisibleVideoTimeAccumulator();
+ }
+ mTotalVideoPlayTime.Pause();
+ mTotalVideoHDRPlayTime.Pause();
+ }
+ if (mMediaContent & MediaContent::MEDIA_HAS_AUDIO) {
+ LOG("Pause audio time accumulation for total play time");
+ if (mInaudibleAudioPlayTime.IsStarted()) {
+ LOG("Pause audible audio time accumulation for total play time");
+ PauseInaudibleAudioTimeAccumulator();
+ }
+ if (mMutedAudioPlayTime.IsStarted()) {
+ LOG("Pause muted audio time accumulation for total play time");
+ PauseMutedAudioTimeAccumulator();
+ }
+ mTotalAudioPlayTime.Pause();
+ }
+
+ mOwner->DispatchAsyncTestingEvent(u"moztotalplaytimepaused"_ns);
+ ReportTelemetry();
+
+ mIsPlaying = false;
+}
+
+void TelemetryProbesReporter::OnVisibilityChanged(Visibility aVisibility) {
+ AssertOnMainThreadAndNotShutdown();
+ LOG("Corresponding media element visibility change=%s -> %s",
+ ToVisibilityStr(mMediaElementVisibility), ToVisibilityStr(aVisibility));
+ if (aVisibility == Visibility::eInvisible) {
+ StartInvisibleVideoTimeAccumulator();
+ } else {
+ if (aVisibility != Visibility::eInitial) {
+ PauseInvisibleVideoTimeAccumulator();
+ } else {
+ LOG("Visibility was initial, not pausing.");
+ }
+ }
+ mMediaElementVisibility = aVisibility;
+}
+
+void TelemetryProbesReporter::OnAudibleChanged(AudibleState aAudibleState) {
+ AssertOnMainThreadAndNotShutdown();
+ LOG("Audibility changed, now %s", ToAudibilityStr(aAudibleState));
+ if (aAudibleState == AudibleState::eNotAudible) {
+ if (!mInaudibleAudioPlayTime.IsStarted()) {
+ StartInaudibleAudioTimeAccumulator();
+ }
+ } else {
+ // This happens when starting playback, no need to pause, because it hasn't
+ // been started yet.
+ if (mInaudibleAudioPlayTime.IsStarted()) {
+ PauseInaudibleAudioTimeAccumulator();
+ }
+ }
+}
+
+void TelemetryProbesReporter::OnMutedChanged(bool aMuted) {
+ // There are multiple ways to mute an element:
+ // - volume = 0
+ // - muted = true
+ // - set the enabled property of the playing AudioTrack to false
+ // Muted -> Muted "transisition" can therefore happen, and we can't add
+ // asserts here.
+ AssertOnMainThreadAndNotShutdown();
+ if (!(mMediaContent & MediaContent::MEDIA_HAS_AUDIO)) {
+ return;
+ }
+ LOG("Muted changed, was %s now %s", ToMutedStr(mIsMuted), ToMutedStr(aMuted));
+ if (aMuted) {
+ if (!mMutedAudioPlayTime.IsStarted()) {
+ StartMutedAudioTimeAccumulator();
+ }
+ } else {
+ // This happens when starting playback, no need to pause, because it hasn't
+ // been started yet.
+ if (mMutedAudioPlayTime.IsStarted()) {
+ PauseMutedAudioTimeAccumulator();
+ }
+ }
+ mIsMuted = aMuted;
+}
+
+void TelemetryProbesReporter::OnMediaContentChanged(MediaContent aContent) {
+ AssertOnMainThreadAndNotShutdown();
+ if (aContent == mMediaContent) {
+ return;
+ }
+ if (mMediaContent & MediaContent::MEDIA_HAS_VIDEO &&
+ !(aContent & MediaContent::MEDIA_HAS_VIDEO)) {
+ LOG("Video track removed from media.");
+ if (mInvisibleVideoPlayTime.IsStarted()) {
+ PauseInvisibleVideoTimeAccumulator();
+ }
+ if (mTotalVideoPlayTime.IsStarted()) {
+ mTotalVideoPlayTime.Pause();
+ mTotalVideoHDRPlayTime.Pause();
+ }
+ }
+ if (mMediaContent & MediaContent::MEDIA_HAS_AUDIO &&
+ !(aContent & MediaContent::MEDIA_HAS_AUDIO)) {
+ LOG("Audio track removed from media.");
+ if (mTotalAudioPlayTime.IsStarted()) {
+ mTotalAudioPlayTime.Pause();
+ }
+ if (mInaudibleAudioPlayTime.IsStarted()) {
+ mInaudibleAudioPlayTime.Pause();
+ }
+ if (mMutedAudioPlayTime.IsStarted()) {
+ mMutedAudioPlayTime.Pause();
+ }
+ }
+ if (!(mMediaContent & MediaContent::MEDIA_HAS_VIDEO) &&
+ aContent & MediaContent::MEDIA_HAS_VIDEO) {
+ LOG("Video track added to media.");
+ if (mIsPlaying) {
+ mTotalVideoPlayTime.Start();
+ if (mMediaElementVisibility == Visibility::eInvisible) {
+ StartInvisibleVideoTimeAccumulator();
+ }
+ }
+ }
+ if (!(mMediaContent & MediaContent::MEDIA_HAS_COLOR_DEPTH_ABOVE_8) &&
+ aContent & MediaContent::MEDIA_HAS_COLOR_DEPTH_ABOVE_8) {
+ if (mIsPlaying) {
+ mTotalVideoHDRPlayTime.Start();
+ }
+ }
+ if (!(mMediaContent & MediaContent::MEDIA_HAS_AUDIO) &&
+ aContent & MediaContent::MEDIA_HAS_AUDIO) {
+ LOG("Audio track added to media.");
+ if (mIsPlaying) {
+ mTotalAudioPlayTime.Start();
+ if (mIsMuted) {
+ StartMutedAudioTimeAccumulator();
+ }
+ }
+ }
+
+ mMediaContent = aContent;
+}
+
+void TelemetryProbesReporter::OnDecodeSuspended() {
+ AssertOnMainThreadAndNotShutdown();
+ // Suspended time should only be counted after starting accumulating invisible
+ // time.
+ if (!mInvisibleVideoPlayTime.IsStarted()) {
+ return;
+ }
+ LOG("Start time accumulation for video decoding suspension");
+ mVideoDecodeSuspendedTime.Start();
+ mOwner->DispatchAsyncTestingEvent(u"mozvideodecodesuspendedstarted"_ns);
+}
+
+void TelemetryProbesReporter::OnDecodeResumed() {
+ AssertOnMainThreadAndNotShutdown();
+ if (!mVideoDecodeSuspendedTime.IsStarted()) {
+ return;
+ }
+ LOG("Pause time accumulation for video decoding suspension");
+ mVideoDecodeSuspendedTime.Pause();
+ mOwner->DispatchAsyncTestingEvent(u"mozvideodecodesuspendedpaused"_ns);
+}
+
+void TelemetryProbesReporter::OnShutdown() {
+ AssertOnMainThreadAndNotShutdown();
+ LOG("Shutdown");
+ OnPause(Visibility::eInvisible);
+ mOwner = nullptr;
+}
+
+void TelemetryProbesReporter::StartInvisibleVideoTimeAccumulator() {
+ AssertOnMainThreadAndNotShutdown();
+ if (!mTotalVideoPlayTime.IsStarted() || mInvisibleVideoPlayTime.IsStarted() ||
+ !HasOwnerHadValidVideo()) {
+ return;
+ }
+ LOG("Start time accumulation for invisible video");
+ mInvisibleVideoPlayTime.Start();
+ mOwner->DispatchAsyncTestingEvent(u"mozinvisibleplaytimestarted"_ns);
+}
+
+void TelemetryProbesReporter::PauseInvisibleVideoTimeAccumulator() {
+ AssertOnMainThreadAndNotShutdown();
+ if (!mInvisibleVideoPlayTime.IsStarted()) {
+ return;
+ }
+ OnDecodeResumed();
+ LOG("Pause time accumulation for invisible video");
+ mInvisibleVideoPlayTime.Pause();
+ mOwner->DispatchAsyncTestingEvent(u"mozinvisibleplaytimepaused"_ns);
+}
+
+void TelemetryProbesReporter::StartInaudibleAudioTimeAccumulator() {
+ AssertOnMainThreadAndNotShutdown();
+ MOZ_ASSERT(!mInaudibleAudioPlayTime.IsStarted());
+ mInaudibleAudioPlayTime.Start();
+ mOwner->DispatchAsyncTestingEvent(u"mozinaudibleaudioplaytimestarted"_ns);
+}
+
+void TelemetryProbesReporter::PauseInaudibleAudioTimeAccumulator() {
+ AssertOnMainThreadAndNotShutdown();
+ MOZ_ASSERT(mInaudibleAudioPlayTime.IsStarted());
+ mInaudibleAudioPlayTime.Pause();
+ mOwner->DispatchAsyncTestingEvent(u"mozinaudibleaudioplaytimepaused"_ns);
+}
+
+void TelemetryProbesReporter::StartMutedAudioTimeAccumulator() {
+ AssertOnMainThreadAndNotShutdown();
+ MOZ_ASSERT(!mMutedAudioPlayTime.IsStarted());
+ mMutedAudioPlayTime.Start();
+ mOwner->DispatchAsyncTestingEvent(u"mozmutedaudioplaytimestarted"_ns);
+}
+
+void TelemetryProbesReporter::PauseMutedAudioTimeAccumulator() {
+ AssertOnMainThreadAndNotShutdown();
+ MOZ_ASSERT(mMutedAudioPlayTime.IsStarted());
+ mMutedAudioPlayTime.Pause();
+ mOwner->DispatchAsyncTestingEvent(u"mozmutedeaudioplaytimepaused"_ns);
+}
+
+bool TelemetryProbesReporter::HasOwnerHadValidVideo() const {
+ // Checking both image and display dimensions helps address cases such as
+ // suspending, where we use a null decoder. In that case a null decoder
+ // produces 0x0 video frames, which might cause layout to resize the display
+ // size, but the image dimensions would be still non-null.
+ const VideoInfo info = mOwner->GetMediaInfo().mVideo;
+ return (info.mDisplay.height > 0 && info.mDisplay.width > 0) ||
+ (info.mImage.height > 0 && info.mImage.width > 0);
+}
+
+bool TelemetryProbesReporter::HasOwnerHadValidMedia() const {
+ return mMediaContent != MediaContent::MEDIA_HAS_NOTHING;
+}
+
+void TelemetryProbesReporter::AssertOnMainThreadAndNotShutdown() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mOwner, "Already shutdown?");
+}
+
+void TelemetryProbesReporter::ReportTelemetry() {
+ AssertOnMainThreadAndNotShutdown();
+ // ReportResultForAudio needs to be called first, because it can use the video
+ // play time, that is reset in ReportResultForVideo.
+ ReportResultForAudio();
+ ReportResultForVideo();
+ mOwner->DispatchAsyncTestingEvent(u"mozreportedtelemetry"_ns);
+}
+
+void TelemetryProbesReporter::ReportResultForVideo() {
+ // We don't want to know the result for video without valid video frames.
+ if (!HasOwnerHadValidVideo()) {
+ return;
+ }
+
+ const double totalVideoPlayTimeS = mTotalVideoPlayTime.GetAndClearTotal();
+ const double invisiblePlayTimeS = mInvisibleVideoPlayTime.GetAndClearTotal();
+ const double videoDecodeSuspendTimeS =
+ mVideoDecodeSuspendedTime.GetAndClearTotal();
+ const double totalVideoHDRPlayTimeS =
+ mTotalVideoHDRPlayTime.GetAndClearTotal();
+
+ // No need to report result for video that didn't start playing.
+ if (totalVideoPlayTimeS == 0.0) {
+ return;
+ }
+ MOZ_ASSERT(totalVideoPlayTimeS >= invisiblePlayTimeS);
+
+ LOG("VIDEO_PLAY_TIME_S = %f", totalVideoPlayTimeS);
+ Telemetry::Accumulate(Telemetry::VIDEO_PLAY_TIME_MS,
+ SECONDS_TO_MS(totalVideoPlayTimeS));
+
+ LOG("VIDEO_HIDDEN_PLAY_TIME_S = %f", invisiblePlayTimeS);
+ Telemetry::Accumulate(Telemetry::VIDEO_HIDDEN_PLAY_TIME_MS,
+ SECONDS_TO_MS(invisiblePlayTimeS));
+
+ // We only want to accumulate non-zero samples for HDR playback.
+ // This is different from the other timings tracked here, but
+ // we don't need 0-length play times to do our calculations.
+ if (totalVideoHDRPlayTimeS > 0.0) {
+ LOG("VIDEO_HDR_PLAY_TIME_S = %f", totalVideoHDRPlayTimeS);
+ Telemetry::Accumulate(Telemetry::VIDEO_HDR_PLAY_TIME_MS,
+ SECONDS_TO_MS(totalVideoHDRPlayTimeS));
+ }
+
+ if (mOwner->IsEncrypted()) {
+ LOG("VIDEO_ENCRYPTED_PLAY_TIME_S = %f", totalVideoPlayTimeS);
+ Telemetry::Accumulate(Telemetry::VIDEO_ENCRYPTED_PLAY_TIME_MS,
+ SECONDS_TO_MS(totalVideoPlayTimeS));
+ }
+
+ // Report result for video using CDM
+ auto keySystem = mOwner->GetKeySystem();
+ if (keySystem) {
+ if (IsClearkeyKeySystem(*keySystem)) {
+ LOG("VIDEO_CLEARKEY_PLAY_TIME_S = %f", totalVideoPlayTimeS);
+ Telemetry::Accumulate(Telemetry::VIDEO_CLEARKEY_PLAY_TIME_MS,
+ SECONDS_TO_MS(totalVideoPlayTimeS));
+
+ } else if (IsWidevineKeySystem(*keySystem)) {
+ LOG("VIDEO_WIDEVINE_PLAY_TIME_S = %f", totalVideoPlayTimeS);
+ Telemetry::Accumulate(Telemetry::VIDEO_WIDEVINE_PLAY_TIME_MS,
+ SECONDS_TO_MS(totalVideoPlayTimeS));
+ }
+ }
+
+ // Keyed by audio+video or video alone, and by a resolution range.
+ const MediaInfo& info = mOwner->GetMediaInfo();
+ nsCString key(info.HasAudio() ? "AV," : "V,");
+ static const struct {
+ int32_t mH;
+ const char* mRes;
+ } sResolutions[] = {{240, "0<h<=240"}, {480, "240<h<=480"},
+ {576, "480<h<=576"}, {720, "576<h<=720"},
+ {1080, "720<h<=1080"}, {2160, "1080<h<=2160"}};
+ const char* resolution = "h>2160";
+ int32_t height = info.mVideo.mImage.height;
+ for (const auto& res : sResolutions) {
+ if (height <= res.mH) {
+ resolution = res.mRes;
+ break;
+ }
+ }
+ key.AppendASCII(resolution);
+
+ auto visiblePlayTimeS = totalVideoPlayTimeS - invisiblePlayTimeS;
+ LOG("VIDEO_VISIBLE_PLAY_TIME = %f, keys: '%s' and 'All'", visiblePlayTimeS,
+ key.get());
+ Telemetry::Accumulate(Telemetry::VIDEO_VISIBLE_PLAY_TIME_MS, key,
+ SECONDS_TO_MS(visiblePlayTimeS));
+ // Also accumulate result in an "All" key.
+ Telemetry::Accumulate(Telemetry::VIDEO_VISIBLE_PLAY_TIME_MS, "All"_ns,
+ SECONDS_TO_MS(visiblePlayTimeS));
+
+ const uint32_t hiddenPercentage =
+ lround(invisiblePlayTimeS / totalVideoPlayTimeS * 100.0);
+ Telemetry::Accumulate(Telemetry::VIDEO_HIDDEN_PLAY_TIME_PERCENTAGE, key,
+ hiddenPercentage);
+ // Also accumulate all percentages in an "All" key.
+ Telemetry::Accumulate(Telemetry::VIDEO_HIDDEN_PLAY_TIME_PERCENTAGE, "All"_ns,
+ hiddenPercentage);
+ LOG("VIDEO_HIDDEN_PLAY_TIME_PERCENTAGE = %u, keys: '%s' and 'All'",
+ hiddenPercentage, key.get());
+
+ const uint32_t videoDecodeSuspendPercentage =
+ lround(videoDecodeSuspendTimeS / totalVideoPlayTimeS * 100.0);
+ Telemetry::Accumulate(Telemetry::VIDEO_INFERRED_DECODE_SUSPEND_PERCENTAGE,
+ key, videoDecodeSuspendPercentage);
+ Telemetry::Accumulate(Telemetry::VIDEO_INFERRED_DECODE_SUSPEND_PERCENTAGE,
+ "All"_ns, videoDecodeSuspendPercentage);
+ LOG("VIDEO_INFERRED_DECODE_SUSPEND_PERCENTAGE = %u, keys: '%s' and 'All'",
+ videoDecodeSuspendPercentage, key.get());
+
+ ReportResultForVideoFrameStatistics(totalVideoPlayTimeS, key);
+}
+
+void TelemetryProbesReporter::ReportResultForAudio() {
+ // Don't record telemetry for a media that didn't have a valid audio or video
+ // to play, or hasn't played.
+ if (!HasOwnerHadValidMedia() || (mTotalAudioPlayTime.PeekTotal() == 0.0 &&
+ mTotalVideoPlayTime.PeekTotal() == 0.0)) {
+ return;
+ }
+
+ nsCString key;
+ nsCString avKey;
+ const double totalAudioPlayTimeS = mTotalAudioPlayTime.GetAndClearTotal();
+ const double inaudiblePlayTimeS = mInaudibleAudioPlayTime.GetAndClearTotal();
+ const double mutedPlayTimeS = mMutedAudioPlayTime.GetAndClearTotal();
+ const double audiblePlayTimeS = totalAudioPlayTimeS - inaudiblePlayTimeS;
+ const double unmutedPlayTimeS = totalAudioPlayTimeS - mutedPlayTimeS;
+ const uint32_t audiblePercentage =
+ lround(audiblePlayTimeS / totalAudioPlayTimeS * 100.0);
+ const uint32_t unmutedPercentage =
+ lround(unmutedPlayTimeS / totalAudioPlayTimeS * 100.0);
+ const double totalVideoPlayTimeS = mTotalVideoPlayTime.PeekTotal();
+
+ // Key semantics:
+ // - AV: Audible audio + video
+ // - IV: Inaudible audio + video
+ // - MV: Muted audio + video
+ // - A: Audible audio-only
+ // - I: Inaudible audio-only
+ // - M: Muted audio-only
+ // - V: Video-only
+ if (mMediaContent & MediaContent::MEDIA_HAS_AUDIO) {
+ if (audiblePercentage == 0) {
+ // Media element had an audio track, but it was inaudible throughout
+ key.AppendASCII("I");
+ } else if (unmutedPercentage == 0) {
+ // Media element had an audio track, but it was muted throughout
+ key.AppendASCII("M");
+ } else {
+ // Media element had an audible audio track
+ key.AppendASCII("A");
+ }
+ avKey.AppendASCII("A");
+ }
+ if (mMediaContent & MediaContent::MEDIA_HAS_VIDEO) {
+ key.AppendASCII("V");
+ avKey.AppendASCII("V");
+ }
+
+ LOG("Key: %s", key.get());
+
+ if (mMediaContent & MediaContent::MEDIA_HAS_AUDIO) {
+ LOG("Audio:\ntotal: %lf\naudible: %lf\ninaudible: %lf\nmuted: "
+ "%lf\npercentage audible: "
+ "%u\npercentage unmuted: %u\n",
+ totalAudioPlayTimeS, audiblePlayTimeS, inaudiblePlayTimeS,
+ mutedPlayTimeS, audiblePercentage, unmutedPercentage);
+ Telemetry::Accumulate(Telemetry::MEDIA_PLAY_TIME_MS, key,
+ SECONDS_TO_MS(totalAudioPlayTimeS));
+ Telemetry::Accumulate(Telemetry::MUTED_PLAY_TIME_PERCENT, avKey,
+ 100 - unmutedPercentage);
+ Telemetry::Accumulate(Telemetry::AUDIBLE_PLAY_TIME_PERCENT, avKey,
+ audiblePercentage);
+ } else {
+ MOZ_ASSERT(mMediaContent & MediaContent::MEDIA_HAS_VIDEO);
+ Telemetry::Accumulate(Telemetry::MEDIA_PLAY_TIME_MS, key,
+ SECONDS_TO_MS(totalVideoPlayTimeS));
+ }
+}
+
+void TelemetryProbesReporter::ReportResultForVideoFrameStatistics(
+ double aTotalPlayTimeS, const nsCString& key) {
+ FrameStatistics* stats = mOwner->GetFrameStatistics();
+ if (!stats) {
+ return;
+ }
+
+ FrameStatisticsData data = stats->GetFrameStatisticsData();
+ if (data.mInterKeyframeCount != 0) {
+ const uint32_t average_ms = uint32_t(
+ std::min<uint64_t>(lround(double(data.mInterKeyframeSum_us) /
+ double(data.mInterKeyframeCount) / 1000.0),
+ UINT32_MAX));
+ Telemetry::Accumulate(Telemetry::VIDEO_INTER_KEYFRAME_AVERAGE_MS, key,
+ average_ms);
+ Telemetry::Accumulate(Telemetry::VIDEO_INTER_KEYFRAME_AVERAGE_MS, "All"_ns,
+ average_ms);
+ LOG("VIDEO_INTER_KEYFRAME_AVERAGE_MS = %u, keys: '%s' and 'All'",
+ average_ms, key.get());
+
+ const uint32_t max_ms = uint32_t(std::min<uint64_t>(
+ (data.mInterKeyFrameMax_us + 500) / 1000, UINT32_MAX));
+ Telemetry::Accumulate(Telemetry::VIDEO_INTER_KEYFRAME_MAX_MS, key, max_ms);
+ Telemetry::Accumulate(Telemetry::VIDEO_INTER_KEYFRAME_MAX_MS, "All"_ns,
+ max_ms);
+ LOG("VIDEO_INTER_KEYFRAME_MAX_MS = %u, keys: '%s' and 'All'", max_ms,
+ key.get());
+ } else {
+ // Here, we have played *some* of the video, but didn't get more than 1
+ // keyframe. Report '0' if we have played for longer than the video-
+ // decode-suspend delay (showing recovery would be difficult).
+ const uint32_t suspendDelay_ms =
+ StaticPrefs::media_suspend_bkgnd_video_delay_ms();
+ if (uint32_t(aTotalPlayTimeS * 1000.0) > suspendDelay_ms) {
+ Telemetry::Accumulate(Telemetry::VIDEO_INTER_KEYFRAME_MAX_MS, key, 0);
+ Telemetry::Accumulate(Telemetry::VIDEO_INTER_KEYFRAME_MAX_MS, "All"_ns,
+ 0);
+ LOG("VIDEO_INTER_KEYFRAME_MAX_MS = 0 (only 1 keyframe), keys: '%s' and "
+ "'All'",
+ key.get());
+ }
+ }
+
+ const uint64_t parsedFrames = stats->GetParsedFrames();
+ if (parsedFrames) {
+ const uint64_t droppedFrames = stats->GetDroppedFrames();
+ MOZ_ASSERT(droppedFrames <= parsedFrames);
+ // Dropped frames <= total frames, so 'percentage' cannot be higher than
+ // 100 and therefore can fit in a uint32_t (that Telemetry takes).
+ const uint32_t percentage = 100 * droppedFrames / parsedFrames;
+ LOG("DROPPED_FRAMES_IN_VIDEO_PLAYBACK = %u", percentage);
+ Telemetry::Accumulate(Telemetry::VIDEO_DROPPED_FRAMES_PROPORTION,
+ percentage);
+ const uint32_t proportion = 10000 * droppedFrames / parsedFrames;
+ Telemetry::Accumulate(
+ Telemetry::VIDEO_DROPPED_FRAMES_PROPORTION_EXPONENTIAL, proportion);
+
+ {
+ const uint64_t droppedFrames = stats->GetDroppedDecodedFrames();
+ const uint32_t proportion = 10000 * droppedFrames / parsedFrames;
+ Telemetry::Accumulate(
+ Telemetry::VIDEO_DROPPED_DECODED_FRAMES_PROPORTION_EXPONENTIAL,
+ proportion);
+ }
+ {
+ const uint64_t droppedFrames = stats->GetDroppedSinkFrames();
+ const uint32_t proportion = 10000 * droppedFrames / parsedFrames;
+ Telemetry::Accumulate(
+ Telemetry::VIDEO_DROPPED_SINK_FRAMES_PROPORTION_EXPONENTIAL,
+ proportion);
+ }
+ {
+ const uint64_t droppedFrames = stats->GetDroppedCompositorFrames();
+ const uint32_t proportion = 10000 * droppedFrames / parsedFrames;
+ Telemetry::Accumulate(
+ Telemetry::VIDEO_DROPPED_COMPOSITOR_FRAMES_PROPORTION_EXPONENTIAL,
+ proportion);
+ }
+ }
+}
+
+double TelemetryProbesReporter::GetTotalVideoPlayTimeInSeconds() const {
+ return mTotalVideoPlayTime.PeekTotal();
+}
+
+double TelemetryProbesReporter::GetTotalVideoHDRPlayTimeInSeconds() const {
+ return mTotalVideoHDRPlayTime.PeekTotal();
+}
+
+double TelemetryProbesReporter::GetVisibleVideoPlayTimeInSeconds() const {
+ return GetTotalVideoPlayTimeInSeconds() -
+ GetInvisibleVideoPlayTimeInSeconds();
+}
+
+double TelemetryProbesReporter::GetInvisibleVideoPlayTimeInSeconds() const {
+ return mInvisibleVideoPlayTime.PeekTotal();
+}
+
+double TelemetryProbesReporter::GetVideoDecodeSuspendedTimeInSeconds() const {
+ return mVideoDecodeSuspendedTime.PeekTotal();
+}
+
+double TelemetryProbesReporter::GetTotalAudioPlayTimeInSeconds() const {
+ return mTotalAudioPlayTime.PeekTotal();
+}
+
+double TelemetryProbesReporter::GetInaudiblePlayTimeInSeconds() const {
+ return mInaudibleAudioPlayTime.PeekTotal();
+}
+
+double TelemetryProbesReporter::GetMutedPlayTimeInSeconds() const {
+ return mMutedAudioPlayTime.PeekTotal();
+}
+
+double TelemetryProbesReporter::GetAudiblePlayTimeInSeconds() const {
+ return GetTotalAudioPlayTimeInSeconds() - GetInaudiblePlayTimeInSeconds();
+}
+
+#undef LOG
+} // namespace mozilla
diff --git a/dom/media/utils/TelemetryProbesReporter.h b/dom/media/utils/TelemetryProbesReporter.h
new file mode 100644
index 0000000000..73a73f0403
--- /dev/null
+++ b/dom/media/utils/TelemetryProbesReporter.h
@@ -0,0 +1,172 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_TelemetryProbesReporter_H_
+#define DOM_TelemetryProbesReporter_H_
+
+#include "MediaInfo.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/AwakeTimeStamp.h"
+#include "AudioChannelService.h"
+#include "nsISupportsImpl.h"
+
+namespace mozilla {
+class FrameStatistics;
+
+class TelemetryProbesReporterOwner {
+ public:
+ virtual Maybe<nsAutoString> GetKeySystem() const = 0;
+ virtual MediaInfo GetMediaInfo() const = 0;
+ virtual FrameStatistics* GetFrameStatistics() const = 0;
+ virtual bool IsEncrypted() const = 0;
+ virtual void DispatchAsyncTestingEvent(const nsAString& aName) = 0;
+};
+
+enum class MediaContent : uint8_t {
+ MEDIA_HAS_NOTHING = (0 << 0),
+ MEDIA_HAS_VIDEO = (1 << 0),
+ MEDIA_HAS_AUDIO = (1 << 1),
+ MEDIA_HAS_COLOR_DEPTH_ABOVE_8 = (1 << 2),
+};
+
+MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(MediaContent)
+
+/**
+ * This class is used for collecting and reporting telemetry probes for
+ * its owner which should inherit from TelemetryProbesReporterOwner. We use it
+ * for HTMLMediaElement, and each element has one corresponding reporter.
+ */
+class TelemetryProbesReporter final {
+ public:
+ explicit TelemetryProbesReporter(TelemetryProbesReporterOwner* aOwner);
+ ~TelemetryProbesReporter() = default;
+
+ enum class Visibility {
+ eInitial,
+ eVisible,
+ eInvisible,
+ };
+
+ static MediaContent MediaInfoToMediaContent(const MediaInfo& aInfo);
+
+ using AudibleState = dom::AudioChannelService::AudibleState;
+
+ // State transitions
+ void OnPlay(Visibility aVisibility, MediaContent aContent, bool aIsMuted);
+ void OnPause(Visibility aVisibility);
+ void OnShutdown();
+
+ void OnVisibilityChanged(Visibility aVisibility);
+ void OnAudibleChanged(AudibleState aAudible);
+ void OnMediaContentChanged(MediaContent aContent);
+ void OnMutedChanged(bool aMuted);
+ void OnDecodeSuspended();
+ void OnDecodeResumed();
+
+ double GetTotalVideoPlayTimeInSeconds() const;
+ double GetTotalVideoHDRPlayTimeInSeconds() const;
+ double GetVisibleVideoPlayTimeInSeconds() const;
+ double GetInvisibleVideoPlayTimeInSeconds() const;
+ double GetVideoDecodeSuspendedTimeInSeconds() const;
+
+ double GetTotalAudioPlayTimeInSeconds() const;
+ double GetInaudiblePlayTimeInSeconds() const;
+ double GetAudiblePlayTimeInSeconds() const;
+ double GetMutedPlayTimeInSeconds() const;
+
+ private:
+ void StartInvisibleVideoTimeAccumulator();
+ void PauseInvisibleVideoTimeAccumulator();
+ void StartInaudibleAudioTimeAccumulator();
+ void PauseInaudibleAudioTimeAccumulator();
+ void StartMutedAudioTimeAccumulator();
+ void PauseMutedAudioTimeAccumulator();
+ bool HasOwnerHadValidVideo() const;
+ bool HasOwnerHadValidMedia() const;
+ void AssertOnMainThreadAndNotShutdown() const;
+
+ void ReportTelemetry();
+ void ReportResultForVideo();
+ void ReportResultForAudio();
+ void ReportResultForVideoFrameStatistics(double aTotalPlayTimeS,
+ const nsCString& key);
+
+ // Helper class to measure times for playback telemetry stats
+ class TimeDurationAccumulator {
+ public:
+ TimeDurationAccumulator() = default;
+ void Start() {
+ if (IsStarted()) {
+ return;
+ }
+ mStartTime = Some(AwakeTimeStamp::NowLoRes());
+ }
+ void Pause() {
+ if (!IsStarted()) {
+ return;
+ }
+ mSum = (AwakeTimeStamp::NowLoRes() - mStartTime.value());
+ mStartTime = Nothing();
+ }
+ bool IsStarted() const { return mStartTime.isSome(); }
+
+ double GetAndClearTotal() {
+ MOZ_ASSERT(!IsStarted(), "only call this when accumulator is paused");
+ double total = mSum.ToSeconds();
+ mStartTime = Nothing();
+ mSum = AwakeTimeDuration();
+ return total;
+ }
+
+ double PeekTotal() const {
+ if (!IsStarted()) {
+ return mSum.ToSeconds();
+ }
+ return (AwakeTimeStamp::NowLoRes() - mStartTime.value()).ToSeconds();
+ }
+
+ private:
+ Maybe<AwakeTimeStamp> mStartTime;
+ AwakeTimeDuration mSum;
+ };
+
+ // The owner is HTMLMediaElement that is guaranteed being always alive during
+ // our whole life cycle.
+ TelemetryProbesReporterOwner* mOwner;
+
+ // Total time an element has spent on playing video.
+ TimeDurationAccumulator mTotalVideoPlayTime;
+
+ // Total time an element has spent on playing video that has a color depth
+ // greater than 8, which is likely HDR video.
+ TimeDurationAccumulator mTotalVideoHDRPlayTime;
+
+ // Total time an element has spent on playing audio
+ TimeDurationAccumulator mTotalAudioPlayTime;
+
+ // Total time a VIDEO element has spent playing while the corresponding media
+ // element is invisible.
+ TimeDurationAccumulator mInvisibleVideoPlayTime;
+
+ // Total time an element has spent playing audio that was not audible
+ TimeDurationAccumulator mInaudibleAudioPlayTime;
+
+ // Total time an element with an audio track has spent muted
+ TimeDurationAccumulator mMutedAudioPlayTime;
+
+ // Total time a VIDEO has spent in video-decode-suspend mode.
+ TimeDurationAccumulator mVideoDecodeSuspendedTime;
+
+ Visibility mMediaElementVisibility = Visibility::eInitial;
+
+ MediaContent mMediaContent = MediaContent::MEDIA_HAS_NOTHING;
+
+ bool mIsPlaying = false;
+
+ bool mIsMuted = false;
+};
+
+} // namespace mozilla
+
+#endif // DOM_TelemetryProbesReporter_H_
diff --git a/dom/media/utils/gtest/TestPerformanceRecorder.cpp b/dom/media/utils/gtest/TestPerformanceRecorder.cpp
new file mode 100644
index 0000000000..ae5d22a916
--- /dev/null
+++ b/dom/media/utils/gtest/TestPerformanceRecorder.cpp
@@ -0,0 +1,110 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include <chrono>
+#include <thread>
+
+#include "PerformanceRecorder.h"
+#include "gtest/gtest.h"
+#include "nsString.h"
+
+using namespace mozilla;
+
+class PerformanceRecorderWrapper : public PerformanceRecorder<PlaybackStage> {
+ public:
+ PerformanceRecorderWrapper(MediaStage aStage, int32_t aHeight)
+ : PerformanceRecorder(aStage, aHeight) {}
+
+ static void EnableMeasurementOnNonMarkerSituation() {
+ sEnableMeasurementForTesting = true;
+ }
+};
+
+TEST(PerformanceRecorder, TestResolution)
+{
+ PerformanceRecorderWrapper::EnableMeasurementOnNonMarkerSituation();
+
+ static const struct {
+ const int32_t mH;
+ const char* mRes;
+ } resolutions[] = {{0, "A:0"},
+ {240, "V:0<h<=240"},
+ {480, "V:240<h<=480"},
+ {576, "V:480<h<=576"},
+ {720, "V:576<h<=720"},
+ {1080, "V:720<h<=1080"},
+ {1440, "V:1080<h<=1440"},
+ {2160, "V:1440<h<=2160"},
+ {4320, "V:h>2160"}};
+
+ const MediaStage stage = MediaStage::RequestDecode;
+ for (auto&& res : resolutions) {
+ PerformanceRecorderWrapper w(stage, res.mH);
+ nsCString name;
+ w.Record([&](auto& aStage) { name = nsCString(aStage.Name()); });
+ ASSERT_NE(name.Find(res.mRes), kNotFound);
+ }
+}
+
+TEST(PerformanceRecorder, TestMoveOperation)
+{
+ PerformanceRecorderWrapper::EnableMeasurementOnNonMarkerSituation();
+
+ const MediaStage stage = MediaStage::RequestDecode;
+ const uint32_t resolution = 1080;
+ PerformanceRecorderWrapper w1(stage, resolution);
+ std::this_thread::sleep_for(std::chrono::milliseconds(1));
+
+ // w1 has been moved which won't continue measuring data.
+ PerformanceRecorderWrapper w2(std::move(w1));
+ ASSERT_DOUBLE_EQ(w1.Record(), 0.0);
+ ASSERT_TRUE(w2.Record() > 0.0);
+}
+
+TEST(PerformanceRecorder, TestRecordInvalidation)
+{
+ PerformanceRecorderWrapper::EnableMeasurementOnNonMarkerSituation();
+
+ const MediaStage stage = MediaStage::RequestDecode;
+ const uint32_t resolution = 1080;
+ PerformanceRecorderWrapper w(stage, resolution);
+ std::this_thread::sleep_for(std::chrono::milliseconds(1));
+
+ ASSERT_TRUE(w.Record() > 0.0);
+
+ w.Record();
+ // w has been recorded and won't continue measuring data.
+ ASSERT_DOUBLE_EQ(w.Record(), 0.0);
+}
+
+TEST(PerformanceRecorder, TestMultipleRecords)
+{
+ PerformanceRecorderWrapper::EnableMeasurementOnNonMarkerSituation();
+
+ const MediaStage stage = MediaStage::RequestDecode;
+ PerformanceRecorderMulti<PlaybackStage> r;
+
+ r.Start(1, stage, 1);
+ r.Start(2, stage, 2);
+ r.Start(3, stage, 3);
+
+ std::this_thread::sleep_for(std::chrono::milliseconds(1));
+
+ // id 0 wasn't started
+ EXPECT_DOUBLE_EQ(r.Record(0), 0.0);
+
+ // id 1 gets recorded normally
+ EXPECT_TRUE(r.Record(1) > 0.0);
+
+ // id 1 was already recorded
+ EXPECT_DOUBLE_EQ(r.Record(1), 0.0);
+
+ // id 2 gets recorded normally
+ EXPECT_TRUE(r.Record(2) > 0.0);
+
+ // id 4 wasn't started
+ EXPECT_DOUBLE_EQ(r.Record(4), 0.0);
+
+ // All lower ids got discarded
+ EXPECT_DOUBLE_EQ(r.Record(3), 0.0);
+}
diff --git a/dom/media/utils/gtest/moz.build b/dom/media/utils/gtest/moz.build
new file mode 100644
index 0000000000..b046869f40
--- /dev/null
+++ b/dom/media/utils/gtest/moz.build
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES += [
+ "TestPerformanceRecorder.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "/dom/media/utils",
+]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/dom/media/utils/moz.build b/dom/media/utils/moz.build
new file mode 100644
index 0000000000..e503c29949
--- /dev/null
+++ b/dom/media/utils/moz.build
@@ -0,0 +1,26 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("test/**"):
+ BUG_COMPONENT = ("Core", "Audio/Video: Playback")
+
+EXPORTS += [
+ "MediaElementEventRunners.h",
+ "PerformanceRecorder.h",
+ "TelemetryProbesReporter.h",
+]
+
+UNIFIED_SOURCES += [
+ "MediaElementEventRunners.cpp",
+ "PerformanceRecorder.cpp",
+ "TelemetryProbesReporter.cpp",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+if CONFIG["ENABLE_TESTS"]:
+ DIRS += ["gtest"]
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/wave/WaveDecoder.cpp b/dom/media/wave/WaveDecoder.cpp
new file mode 100644
index 0000000000..2aea6b4178
--- /dev/null
+++ b/dom/media/wave/WaveDecoder.cpp
@@ -0,0 +1,56 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WaveDecoder.h"
+#include "MediaContainerType.h"
+#include "MediaDecoder.h"
+
+namespace mozilla {
+
+/* static */
+bool WaveDecoder::IsSupportedType(const MediaContainerType& aContainerType) {
+ if (!MediaDecoder::IsWaveEnabled()) {
+ return false;
+ }
+ if (aContainerType.Type() == MEDIAMIMETYPE("audio/wave") ||
+ aContainerType.Type() == MEDIAMIMETYPE("audio/x-wav") ||
+ aContainerType.Type() == MEDIAMIMETYPE("audio/wav") ||
+ aContainerType.Type() == MEDIAMIMETYPE("audio/x-pn-wav")) {
+ return (aContainerType.ExtendedType().Codecs().IsEmpty() ||
+ aContainerType.ExtendedType().Codecs() == "1" ||
+ aContainerType.ExtendedType().Codecs() == "3" ||
+ aContainerType.ExtendedType().Codecs() == "6" ||
+ aContainerType.ExtendedType().Codecs() == "7");
+ }
+
+ return false;
+}
+
+/* static */
+nsTArray<UniquePtr<TrackInfo>> WaveDecoder::GetTracksInfo(
+ const MediaContainerType& aType) {
+ nsTArray<UniquePtr<TrackInfo>> tracks;
+ if (!IsSupportedType(aType)) {
+ return tracks;
+ }
+
+ const MediaCodecs& codecs = aType.ExtendedType().Codecs();
+ if (codecs.IsEmpty()) {
+ tracks.AppendElement(
+ CreateTrackInfoWithMIMETypeAndContainerTypeExtraParameters(
+ "audio/x-wav"_ns, aType));
+ return tracks;
+ }
+
+ for (const auto& codec : codecs.Range()) {
+ tracks.AppendElement(
+ CreateTrackInfoWithMIMETypeAndContainerTypeExtraParameters(
+ "audio/wave; codecs="_ns + NS_ConvertUTF16toUTF8(codec), aType));
+ }
+ return tracks;
+}
+
+} // namespace mozilla
diff --git a/dom/media/wave/WaveDecoder.h b/dom/media/wave/WaveDecoder.h
new file mode 100644
index 0000000000..448602de9a
--- /dev/null
+++ b/dom/media/wave/WaveDecoder.h
@@ -0,0 +1,28 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#if !defined(WaveDecoder_h_)
+# define WaveDecoder_h_
+
+# include "mozilla/UniquePtr.h"
+# include "nsTArray.h"
+
+namespace mozilla {
+
+class MediaContainerType;
+class TrackInfo;
+
+class WaveDecoder {
+ public:
+ // Returns true if the Wave backend is pref'ed on, and we're running on a
+ // platform that is likely to have decoders for the format.
+ static bool IsSupportedType(const MediaContainerType& aContainerType);
+ static nsTArray<UniquePtr<TrackInfo>> GetTracksInfo(
+ const MediaContainerType& aType);
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/wave/WaveDemuxer.cpp b/dom/media/wave/WaveDemuxer.cpp
new file mode 100644
index 0000000000..814883dc54
--- /dev/null
+++ b/dom/media/wave/WaveDemuxer.cpp
@@ -0,0 +1,739 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WaveDemuxer.h"
+
+#include <inttypes.h>
+#include <algorithm>
+
+#include "mozilla/Assertions.h"
+#include "mozilla/EndianUtils.h"
+#include "mozilla/Utf8.h"
+#include "BufferReader.h"
+#include "VideoUtils.h"
+#include "TimeUnits.h"
+
+using mozilla::media::TimeIntervals;
+using mozilla::media::TimeUnit;
+
+namespace mozilla {
+
+// WAVDemuxer
+
+WAVDemuxer::WAVDemuxer(MediaResource* aSource) : mSource(aSource) {
+ DDLINKCHILD("source", aSource);
+}
+
+bool WAVDemuxer::InitInternal() {
+ if (!mTrackDemuxer) {
+ mTrackDemuxer = new WAVTrackDemuxer(mSource.GetResource());
+ DDLINKCHILD("track demuxer", mTrackDemuxer.get());
+ }
+ return mTrackDemuxer->Init();
+}
+
+RefPtr<WAVDemuxer::InitPromise> WAVDemuxer::Init() {
+ if (!InitInternal()) {
+ return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_METADATA_ERR,
+ __func__);
+ }
+ return InitPromise::CreateAndResolve(NS_OK, __func__);
+}
+
+uint32_t WAVDemuxer::GetNumberTracks(TrackInfo::TrackType aType) const {
+ return aType == TrackInfo::kAudioTrack ? 1u : 0u;
+}
+
+already_AddRefed<MediaTrackDemuxer> WAVDemuxer::GetTrackDemuxer(
+ TrackInfo::TrackType aType, uint32_t aTrackNumber) {
+ if (!mTrackDemuxer) {
+ return nullptr;
+ }
+ return RefPtr<WAVTrackDemuxer>(mTrackDemuxer).forget();
+}
+
+bool WAVDemuxer::IsSeekable() const { return true; }
+
+// WAVTrackDemuxer
+
+WAVTrackDemuxer::WAVTrackDemuxer(MediaResource* aSource)
+ : mSource(aSource),
+ mOffset(0),
+ mFirstChunkOffset(0),
+ mNumParsedChunks(0),
+ mChunkIndex(0),
+ mDataLength(0),
+ mTotalChunkLen(0),
+ mSamplesPerChunk(0),
+ mSamplesPerSecond(0),
+ mChannels(0),
+ mSampleFormat(0) {
+ DDLINKCHILD("source", aSource);
+ Reset();
+}
+
+bool WAVTrackDemuxer::Init() {
+ Reset();
+ FastSeek(TimeUnit());
+
+ if (!mInfo) {
+ mInfo = MakeUnique<AudioInfo>();
+ mInfo->mCodecSpecificConfig =
+ AudioCodecSpecificVariant{WaveCodecSpecificData{}};
+ }
+
+ if (!RIFFParserInit()) {
+ return false;
+ }
+
+ while (true) {
+ if (!HeaderParserInit()) {
+ return false;
+ }
+
+ uint32_t aChunkName = mHeaderParser.GiveHeader().ChunkName();
+ uint32_t aChunkSize = mHeaderParser.GiveHeader().ChunkSize();
+
+ if (aChunkName == FRMT_CODE) {
+ if (!FmtChunkParserInit()) {
+ return false;
+ }
+ } else if (aChunkName == LIST_CODE) {
+ mHeaderParser.Reset();
+ uint64_t endOfListChunk = static_cast<uint64_t>(mOffset) + aChunkSize;
+ if (endOfListChunk > UINT32_MAX) {
+ return false;
+ }
+ if (!ListChunkParserInit(aChunkSize)) {
+ mOffset = endOfListChunk;
+ }
+ } else if (aChunkName == DATA_CODE) {
+ mDataLength = aChunkSize;
+ if (mFirstChunkOffset != mOffset) {
+ mFirstChunkOffset = mOffset;
+ }
+ break;
+ } else {
+ mOffset += aChunkSize; // Skip other irrelevant chunks.
+ }
+ if (mOffset & 1) {
+ // Wave files are 2-byte aligned so we need to round up
+ mOffset += 1;
+ }
+ mHeaderParser.Reset();
+ }
+
+ int64_t streamLength = StreamLength();
+ // If the chunk length and the resource length are not equal, use the
+ // resource length as the "real" data chunk length, if it's longer than the
+ // chunk size.
+ if (streamLength != -1) {
+ uint64_t streamLengthPositive = static_cast<uint64_t>(streamLength);
+ if (streamLengthPositive > mFirstChunkOffset &&
+ mDataLength > streamLengthPositive - mFirstChunkOffset) {
+ mDataLength = streamLengthPositive - mFirstChunkOffset;
+ }
+ }
+
+ mSamplesPerSecond = mFmtParser.FmtChunk().SampleRate();
+ mChannels = mFmtParser.FmtChunk().Channels();
+ mSampleFormat = mFmtParser.FmtChunk().SampleFormat();
+ if (!mSamplesPerSecond || !mChannels || !mSampleFormat) {
+ return false;
+ }
+ mSamplesPerChunk = DATA_CHUNK_SIZE * 8 / mChannels / mSampleFormat;
+
+ mInfo->mRate = mSamplesPerSecond;
+ mInfo->mChannels = mChannels;
+ mInfo->mBitDepth = mSampleFormat;
+ mInfo->mProfile = mFmtParser.FmtChunk().WaveFormat() & 0x00FF;
+ mInfo->mExtendedProfile = (mFmtParser.FmtChunk().WaveFormat() & 0xFF00) >> 8;
+ mInfo->mMimeType = "audio/wave; codecs=";
+ mInfo->mMimeType.AppendInt(mFmtParser.FmtChunk().WaveFormat());
+ mInfo->mDuration = Duration();
+
+ return mInfo->mDuration.IsPositive();
+}
+
+bool WAVTrackDemuxer::RIFFParserInit() {
+ RefPtr<MediaRawData> riffHeader = GetFileHeader(FindRIFFHeader());
+ if (!riffHeader) {
+ return false;
+ }
+ BufferReader RIFFReader(riffHeader->Data(), 12);
+ Unused << mRIFFParser.Parse(RIFFReader);
+ return mRIFFParser.RiffHeader().IsValid(11);
+}
+
+bool WAVTrackDemuxer::HeaderParserInit() {
+ RefPtr<MediaRawData> header = GetFileHeader(FindChunkHeader());
+ if (!header) {
+ return false;
+ }
+ BufferReader HeaderReader(header->Data(), 8);
+ Unused << mHeaderParser.Parse(HeaderReader);
+ return true;
+}
+
+bool WAVTrackDemuxer::FmtChunkParserInit() {
+ RefPtr<MediaRawData> fmtChunk = GetFileHeader(FindFmtChunk());
+ if (!fmtChunk) {
+ return false;
+ }
+ BufferReader fmtReader(fmtChunk->Data(),
+ mHeaderParser.GiveHeader().ChunkSize());
+ Unused << mFmtParser.Parse(fmtReader);
+ return true;
+}
+
+bool WAVTrackDemuxer::ListChunkParserInit(uint32_t aChunkSize) {
+ uint32_t bytesRead = 0;
+
+ RefPtr<MediaRawData> infoTag = GetFileHeader(FindInfoTag());
+ if (!infoTag) {
+ return false;
+ }
+
+ BufferReader infoTagReader(infoTag->Data(), 4);
+ auto res = infoTagReader.ReadU32();
+ if (res.isErr() || (res.isOk() && res.unwrap() != INFO_CODE)) {
+ return false;
+ }
+
+ bytesRead += 4;
+
+ while (bytesRead < aChunkSize) {
+ if (!HeaderParserInit()) {
+ return false;
+ }
+
+ bytesRead += 8;
+
+ uint32_t id = mHeaderParser.GiveHeader().ChunkName();
+ uint32_t length = mHeaderParser.GiveHeader().ChunkSize();
+
+ // SubChunk Length Cannot Exceed List Chunk length.
+ if (length > aChunkSize - bytesRead) {
+ length = aChunkSize - bytesRead;
+ }
+
+ MediaByteRange mRange = {mOffset, mOffset + length};
+ RefPtr<MediaRawData> mChunkData = GetFileHeader(mRange);
+ if (!mChunkData) {
+ return false;
+ }
+
+ const char* rawData = reinterpret_cast<const char*>(mChunkData->Data());
+
+ nsCString val(rawData, length);
+ if (length > 0 && val[length - 1] == '\0') {
+ val.SetLength(length - 1);
+ }
+
+ if (length % 2) {
+ mOffset += 1;
+ length += length % 2;
+ }
+
+ bytesRead += length;
+
+ if (!IsUtf8(val)) {
+ mHeaderParser.Reset();
+ continue;
+ }
+
+ switch (id) {
+ case 0x49415254: // IART
+ mInfo->mTags.AppendElement(MetadataTag("artist"_ns, val));
+ break;
+ case 0x49434d54: // ICMT
+ mInfo->mTags.AppendElement(MetadataTag("comments"_ns, val));
+ break;
+ case 0x49474e52: // IGNR
+ mInfo->mTags.AppendElement(MetadataTag("genre"_ns, val));
+ break;
+ case 0x494e414d: // INAM
+ mInfo->mTags.AppendElement(MetadataTag("name"_ns, val));
+ break;
+ }
+
+ mHeaderParser.Reset();
+ }
+ return true;
+}
+
+media::TimeUnit WAVTrackDemuxer::SeekPosition() const {
+ TimeUnit pos = Duration(mChunkIndex);
+ if (Duration() > TimeUnit()) {
+ pos = std::min(Duration(), pos);
+ }
+ return pos;
+}
+
+RefPtr<MediaRawData> WAVTrackDemuxer::DemuxSample() {
+ return GetNextChunk(FindNextChunk());
+}
+
+UniquePtr<TrackInfo> WAVTrackDemuxer::GetInfo() const { return mInfo->Clone(); }
+
+RefPtr<WAVTrackDemuxer::SeekPromise> WAVTrackDemuxer::Seek(
+ const TimeUnit& aTime) {
+ FastSeek(aTime);
+ const TimeUnit seekTime = ScanUntil(aTime);
+ return SeekPromise::CreateAndResolve(seekTime, __func__);
+}
+
+TimeUnit WAVTrackDemuxer::FastSeek(const TimeUnit& aTime) {
+ if (aTime.ToMicroseconds()) {
+ mChunkIndex = ChunkIndexFromTime(aTime);
+ } else {
+ mChunkIndex = 0;
+ }
+
+ mOffset = OffsetFromChunkIndex(mChunkIndex);
+
+ if (mOffset > mFirstChunkOffset && StreamLength() > 0) {
+ mOffset = std::min(static_cast<uint64_t>(StreamLength() - 1), mOffset);
+ }
+
+ return Duration(mChunkIndex);
+}
+
+TimeUnit WAVTrackDemuxer::ScanUntil(const TimeUnit& aTime) {
+ if (!aTime.ToMicroseconds()) {
+ return FastSeek(aTime);
+ }
+
+ if (Duration(mChunkIndex) > aTime) {
+ FastSeek(aTime);
+ }
+
+ return SeekPosition();
+}
+
+RefPtr<WAVTrackDemuxer::SamplesPromise> WAVTrackDemuxer::GetSamples(
+ int32_t aNumSamples) {
+ MOZ_ASSERT(aNumSamples);
+
+ RefPtr<SamplesHolder> datachunks = new SamplesHolder();
+
+ while (aNumSamples--) {
+ RefPtr<MediaRawData> datachunk = GetNextChunk(FindNextChunk());
+ if (!datachunk) {
+ break;
+ }
+ if (!datachunk->HasValidTime()) {
+ return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_DEMUXER_ERR,
+ __func__);
+ }
+ datachunks->AppendSample(datachunk);
+ }
+
+ if (datachunks->GetSamples().IsEmpty()) {
+ return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_END_OF_STREAM,
+ __func__);
+ }
+
+ return SamplesPromise::CreateAndResolve(datachunks, __func__);
+}
+
+void WAVTrackDemuxer::Reset() {
+ FastSeek(TimeUnit());
+ mParser.Reset();
+ mHeaderParser.Reset();
+ mRIFFParser.Reset();
+ mFmtParser.Reset();
+}
+
+RefPtr<WAVTrackDemuxer::SkipAccessPointPromise>
+WAVTrackDemuxer::SkipToNextRandomAccessPoint(const TimeUnit& aTimeThreshold) {
+ return SkipAccessPointPromise::CreateAndReject(
+ SkipFailureHolder(NS_ERROR_DOM_MEDIA_DEMUXER_ERR, 0), __func__);
+}
+
+int64_t WAVTrackDemuxer::GetResourceOffset() const { return mOffset; }
+
+TimeIntervals WAVTrackDemuxer::GetBuffered() {
+ TimeUnit duration = Duration();
+
+ if (duration <= TimeUnit()) {
+ return TimeIntervals();
+ }
+
+ AutoPinned<MediaResource> stream(mSource.GetResource());
+ return GetEstimatedBufferedTimeRanges(stream, duration.ToMicroseconds());
+}
+
+int64_t WAVTrackDemuxer::StreamLength() const { return mSource.GetLength(); }
+
+TimeUnit WAVTrackDemuxer::Duration() const {
+ if (!mDataLength || !mChannels || !mSampleFormat) {
+ return TimeUnit();
+ }
+
+ int64_t numSamples =
+ static_cast<int64_t>(mDataLength) * 8 / mChannels / mSampleFormat;
+
+ int64_t numUSeconds = USECS_PER_S * numSamples / mSamplesPerSecond;
+
+ if (USECS_PER_S * numSamples % mSamplesPerSecond > mSamplesPerSecond / 2) {
+ numUSeconds++;
+ }
+
+ return TimeUnit::FromMicroseconds(numUSeconds);
+}
+
+TimeUnit WAVTrackDemuxer::Duration(int64_t aNumDataChunks) const {
+ if (!mSamplesPerSecond || !mSamplesPerChunk) {
+ return TimeUnit();
+ }
+ const double usPerDataChunk =
+ USECS_PER_S * static_cast<double>(mSamplesPerChunk) / mSamplesPerSecond;
+ return TimeUnit::FromMicroseconds(aNumDataChunks * usPerDataChunk);
+}
+
+TimeUnit WAVTrackDemuxer::DurationFromBytes(uint32_t aNumBytes) const {
+ if (!mSamplesPerSecond || !mChannels || !mSampleFormat) {
+ return TimeUnit();
+ }
+
+ uint64_t numSamples = aNumBytes * 8 / mChannels / mSampleFormat;
+
+ uint64_t numUSeconds = USECS_PER_S * numSamples / mSamplesPerSecond;
+
+ if (USECS_PER_S * numSamples % mSamplesPerSecond > mSamplesPerSecond / 2) {
+ numUSeconds++;
+ }
+
+ return TimeUnit::FromMicroseconds(numUSeconds);
+}
+
+MediaByteRange WAVTrackDemuxer::FindNextChunk() {
+ if (mOffset + DATA_CHUNK_SIZE < mFirstChunkOffset + mDataLength) {
+ return {mOffset, mOffset + DATA_CHUNK_SIZE};
+ } else {
+ return {mOffset, mFirstChunkOffset + mDataLength};
+ }
+}
+
+MediaByteRange WAVTrackDemuxer::FindChunkHeader() {
+ return {mOffset, mOffset + CHUNK_HEAD_SIZE};
+}
+
+MediaByteRange WAVTrackDemuxer::FindRIFFHeader() {
+ return {mOffset, mOffset + RIFF_CHUNK_SIZE};
+}
+
+MediaByteRange WAVTrackDemuxer::FindFmtChunk() {
+ return {mOffset, mOffset + mHeaderParser.GiveHeader().ChunkSize()};
+}
+
+MediaByteRange WAVTrackDemuxer::FindListChunk() {
+ return {mOffset, mOffset + mHeaderParser.GiveHeader().ChunkSize()};
+}
+
+MediaByteRange WAVTrackDemuxer::FindInfoTag() { return {mOffset, mOffset + 4}; }
+
+bool WAVTrackDemuxer::SkipNextChunk(const MediaByteRange& aRange) {
+ UpdateState(aRange);
+ return true;
+}
+
+already_AddRefed<MediaRawData> WAVTrackDemuxer::GetNextChunk(
+ const MediaByteRange& aRange) {
+ if (!aRange.Length()) {
+ return nullptr;
+ }
+
+ RefPtr<MediaRawData> datachunk = new MediaRawData();
+ datachunk->mOffset = aRange.mStart;
+
+ UniquePtr<MediaRawDataWriter> chunkWriter(datachunk->CreateWriter());
+ if (!chunkWriter->SetSize(static_cast<uint32_t>(aRange.Length()))) {
+ return nullptr;
+ }
+
+ const uint32_t read =
+ Read(chunkWriter->Data(), datachunk->mOffset, datachunk->Size());
+
+ if (read != aRange.Length()) {
+ return nullptr;
+ }
+
+ UpdateState(aRange);
+ ++mNumParsedChunks;
+ ++mChunkIndex;
+
+ datachunk->mTime = Duration(mChunkIndex - 1);
+
+ if (static_cast<uint32_t>(mChunkIndex) * DATA_CHUNK_SIZE < mDataLength) {
+ datachunk->mDuration = Duration(1);
+ } else {
+ uint32_t mBytesRemaining = mDataLength - mChunkIndex * DATA_CHUNK_SIZE;
+ datachunk->mDuration = DurationFromBytes(mBytesRemaining);
+ }
+ datachunk->mTimecode = datachunk->mTime;
+ datachunk->mKeyframe = true;
+
+ MOZ_ASSERT(!datachunk->mTime.IsNegative());
+ MOZ_ASSERT(!datachunk->mDuration.IsNegative());
+
+ return datachunk.forget();
+}
+
+already_AddRefed<MediaRawData> WAVTrackDemuxer::GetFileHeader(
+ const MediaByteRange& aRange) {
+ if (!aRange.Length()) {
+ return nullptr;
+ }
+
+ RefPtr<MediaRawData> fileHeader = new MediaRawData();
+ fileHeader->mOffset = aRange.mStart;
+
+ UniquePtr<MediaRawDataWriter> headerWriter(fileHeader->CreateWriter());
+ if (!headerWriter->SetSize(static_cast<uint32_t>(aRange.Length()))) {
+ return nullptr;
+ }
+
+ const uint32_t read =
+ Read(headerWriter->Data(), fileHeader->mOffset, fileHeader->Size());
+
+ if (read != aRange.Length()) {
+ return nullptr;
+ }
+
+ UpdateState(aRange);
+
+ return fileHeader.forget();
+}
+
+uint64_t WAVTrackDemuxer::OffsetFromChunkIndex(uint32_t aChunkIndex) const {
+ return mFirstChunkOffset + aChunkIndex * DATA_CHUNK_SIZE;
+}
+
+uint64_t WAVTrackDemuxer::ChunkIndexFromTime(
+ const media::TimeUnit& aTime) const {
+ if (!mSamplesPerChunk || !mSamplesPerSecond) {
+ return 0;
+ }
+ uint64_t chunkIndex =
+ (aTime.ToSeconds() * mSamplesPerSecond / mSamplesPerChunk) - 1;
+ return chunkIndex;
+}
+
+void WAVTrackDemuxer::UpdateState(const MediaByteRange& aRange) {
+ // Full chunk parsed, move offset to its end.
+ mOffset = static_cast<uint32_t>(aRange.mEnd);
+ mTotalChunkLen += static_cast<uint64_t>(aRange.Length());
+}
+
+uint32_t WAVTrackDemuxer::Read(uint8_t* aBuffer, int64_t aOffset,
+ int32_t aSize) {
+ const int64_t streamLen = StreamLength();
+ if (mInfo && streamLen > 0) {
+ int64_t max = streamLen > aOffset ? streamLen - aOffset : 0;
+ aSize = std::min<int64_t>(aSize, max);
+ }
+ uint32_t read = 0;
+ const nsresult rv = mSource.ReadAt(aOffset, reinterpret_cast<char*>(aBuffer),
+ static_cast<uint32_t>(aSize), &read);
+ NS_ENSURE_SUCCESS(rv, 0);
+ return read;
+}
+
+// RIFFParser
+
+Result<uint32_t, nsresult> RIFFParser::Parse(BufferReader& aReader) {
+ for (auto res = aReader.ReadU8();
+ res.isOk() && !mRiffHeader.ParseNext(res.unwrap());
+ res = aReader.ReadU8()) {
+ }
+
+ if (mRiffHeader.IsValid()) {
+ return RIFF_CHUNK_SIZE;
+ }
+
+ return 0;
+}
+
+void RIFFParser::Reset() { mRiffHeader.Reset(); }
+
+const RIFFParser::RIFFHeader& RIFFParser::RiffHeader() const {
+ return mRiffHeader;
+}
+
+// RIFFParser::RIFFHeader
+
+RIFFParser::RIFFHeader::RIFFHeader() { Reset(); }
+
+void RIFFParser::RIFFHeader::Reset() {
+ memset(mRaw, 0, sizeof(mRaw));
+ mPos = 0;
+}
+
+bool RIFFParser::RIFFHeader::ParseNext(uint8_t c) {
+ if (!Update(c)) {
+ Reset();
+ if (!Update(c)) {
+ Reset();
+ }
+ }
+ return IsValid();
+}
+
+bool RIFFParser::RIFFHeader::IsValid(int aPos) const {
+ if (aPos > -1 && aPos < 4) {
+ return RIFF[aPos] == mRaw[aPos];
+ } else if (aPos > 7 && aPos < 12) {
+ return WAVE[aPos - 8] == mRaw[aPos];
+ } else {
+ return true;
+ }
+}
+
+bool RIFFParser::RIFFHeader::IsValid() const { return mPos >= RIFF_CHUNK_SIZE; }
+
+bool RIFFParser::RIFFHeader::Update(uint8_t c) {
+ if (mPos < RIFF_CHUNK_SIZE) {
+ mRaw[mPos] = c;
+ }
+ return IsValid(mPos++);
+}
+
+// HeaderParser
+
+Result<uint32_t, nsresult> HeaderParser::Parse(BufferReader& aReader) {
+ for (auto res = aReader.ReadU8();
+ res.isOk() && !mHeader.ParseNext(res.unwrap()); res = aReader.ReadU8()) {
+ }
+
+ if (mHeader.IsValid()) {
+ return CHUNK_HEAD_SIZE;
+ }
+
+ return 0;
+}
+
+void HeaderParser::Reset() { mHeader.Reset(); }
+
+const HeaderParser::ChunkHeader& HeaderParser::GiveHeader() const {
+ return mHeader;
+}
+
+// HeaderParser::ChunkHeader
+
+HeaderParser::ChunkHeader::ChunkHeader() { Reset(); }
+
+void HeaderParser::ChunkHeader::Reset() {
+ memset(mRaw, 0, sizeof(mRaw));
+ mPos = 0;
+}
+
+bool HeaderParser::ChunkHeader::ParseNext(uint8_t c) {
+ Update(c);
+ return IsValid();
+}
+
+bool HeaderParser::ChunkHeader::IsValid() const {
+ return mPos >= CHUNK_HEAD_SIZE;
+}
+
+uint32_t HeaderParser::ChunkHeader::ChunkName() const {
+ return static_cast<uint32_t>(
+ ((mRaw[0] << 24) | (mRaw[1] << 16) | (mRaw[2] << 8) | (mRaw[3])));
+}
+
+uint32_t HeaderParser::ChunkHeader::ChunkSize() const {
+ return static_cast<uint32_t>(
+ ((mRaw[7] << 24) | (mRaw[6] << 16) | (mRaw[5] << 8) | (mRaw[4])));
+}
+
+void HeaderParser::ChunkHeader::Update(uint8_t c) {
+ if (mPos < CHUNK_HEAD_SIZE) {
+ mRaw[mPos++] = c;
+ }
+}
+
+// FormatParser
+
+Result<uint32_t, nsresult> FormatParser::Parse(BufferReader& aReader) {
+ for (auto res = aReader.ReadU8();
+ res.isOk() && !mFmtChunk.ParseNext(res.unwrap());
+ res = aReader.ReadU8()) {
+ }
+
+ if (mFmtChunk.IsValid()) {
+ return FMT_CHUNK_MIN_SIZE;
+ }
+
+ return 0;
+}
+
+void FormatParser::Reset() { mFmtChunk.Reset(); }
+
+const FormatParser::FormatChunk& FormatParser::FmtChunk() const {
+ return mFmtChunk;
+}
+
+// FormatParser::FormatChunk
+
+FormatParser::FormatChunk::FormatChunk() { Reset(); }
+
+void FormatParser::FormatChunk::Reset() {
+ memset(mRaw, 0, sizeof(mRaw));
+ mPos = 0;
+}
+
+uint16_t FormatParser::FormatChunk::WaveFormat() const {
+ return (mRaw[1] << 8) | (mRaw[0]);
+}
+
+uint16_t FormatParser::FormatChunk::Channels() const {
+ return (mRaw[3] << 8) | (mRaw[2]);
+}
+
+uint32_t FormatParser::FormatChunk::SampleRate() const {
+ return static_cast<uint32_t>((mRaw[7] << 24) | (mRaw[6] << 16) |
+ (mRaw[5] << 8) | (mRaw[4]));
+}
+
+uint16_t FormatParser::FormatChunk::FrameSize() const {
+ return (mRaw[13] << 8) | (mRaw[12]);
+}
+
+uint16_t FormatParser::FormatChunk::SampleFormat() const {
+ return (mRaw[15] << 8) | (mRaw[14]);
+}
+
+bool FormatParser::FormatChunk::ParseNext(uint8_t c) {
+ Update(c);
+ return IsValid();
+}
+
+bool FormatParser::FormatChunk::IsValid() const {
+ return (FrameSize() == SampleRate() * Channels() / 8) &&
+ (mPos >= FMT_CHUNK_MIN_SIZE);
+}
+
+void FormatParser::FormatChunk::Update(uint8_t c) {
+ if (mPos < FMT_CHUNK_MIN_SIZE) {
+ mRaw[mPos++] = c;
+ }
+}
+
+// DataParser
+
+DataParser::DataParser() = default;
+
+void DataParser::Reset() { mChunk.Reset(); }
+
+const DataParser::DataChunk& DataParser::CurrentChunk() const { return mChunk; }
+
+// DataParser::DataChunk
+
+void DataParser::DataChunk::Reset() { mPos = 0; }
+
+} // namespace mozilla
diff --git a/dom/media/wave/WaveDemuxer.h b/dom/media/wave/WaveDemuxer.h
new file mode 100644
index 0000000000..a7ab65c002
--- /dev/null
+++ b/dom/media/wave/WaveDemuxer.h
@@ -0,0 +1,266 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * Licence, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef WAV_DEMUXER_H_
+#define WAV_DEMUXER_H_
+
+#include "MediaDataDemuxer.h"
+#include "MediaResource.h"
+
+namespace mozilla {
+class BufferReader;
+
+static const uint32_t FRMT_CODE = 0x666d7420;
+static const uint32_t DATA_CODE = 0x64617461;
+static const uint32_t LIST_CODE = 0x4c495354;
+static const uint32_t INFO_CODE = 0x494e464f;
+
+static const uint8_t RIFF[4] = {'R', 'I', 'F', 'F'};
+static const uint8_t WAVE[4] = {'W', 'A', 'V', 'E'};
+
+static const uint16_t RIFF_CHUNK_SIZE = 12;
+static const uint16_t CHUNK_HEAD_SIZE = 8;
+static const uint16_t FMT_CHUNK_MIN_SIZE = 16;
+static const uint16_t DATA_CHUNK_SIZE = 768;
+
+class WAVTrackDemuxer;
+
+DDLoggedTypeDeclNameAndBase(WAVDemuxer, MediaDataDemuxer);
+DDLoggedTypeNameAndBase(WAVTrackDemuxer, MediaTrackDemuxer);
+
+class WAVDemuxer : public MediaDataDemuxer,
+ public DecoderDoctorLifeLogger<WAVDemuxer> {
+ public:
+ // MediaDataDemuxer interface.
+ explicit WAVDemuxer(MediaResource* aSource);
+ RefPtr<InitPromise> Init() override;
+ uint32_t GetNumberTracks(TrackInfo::TrackType aType) const override;
+ already_AddRefed<MediaTrackDemuxer> GetTrackDemuxer(
+ TrackInfo::TrackType aType, uint32_t aTrackNumber) override;
+ bool IsSeekable() const override;
+
+ private:
+ // Synchronous Initialization.
+ bool InitInternal();
+
+ MediaResourceIndex mSource;
+ RefPtr<WAVTrackDemuxer> mTrackDemuxer;
+};
+
+class RIFFParser {
+ private:
+ class RIFFHeader;
+
+ public:
+ const RIFFHeader& RiffHeader() const;
+
+ Result<uint32_t, nsresult> Parse(BufferReader& aReader);
+
+ void Reset();
+
+ private:
+ class RIFFHeader {
+ public:
+ RIFFHeader();
+ void Reset();
+
+ bool IsValid() const;
+ bool IsValid(int aPos) const;
+
+ bool ParseNext(uint8_t c);
+
+ private:
+ bool Update(uint8_t c);
+
+ uint8_t mRaw[RIFF_CHUNK_SIZE];
+
+ int mPos;
+ };
+
+ RIFFHeader mRiffHeader;
+};
+
+class HeaderParser {
+ private:
+ class ChunkHeader;
+
+ public:
+ const ChunkHeader& GiveHeader() const;
+
+ Result<uint32_t, nsresult> Parse(BufferReader& aReader);
+
+ void Reset();
+
+ private:
+ class ChunkHeader {
+ public:
+ ChunkHeader();
+ void Reset();
+
+ bool IsValid() const;
+
+ uint32_t ChunkName() const;
+ uint32_t ChunkSize() const;
+
+ bool ParseNext(uint8_t c);
+
+ private:
+ void Update(uint8_t c);
+
+ uint8_t mRaw[CHUNK_HEAD_SIZE];
+
+ int mPos;
+ };
+
+ ChunkHeader mHeader;
+};
+
+class FormatParser {
+ private:
+ class FormatChunk;
+
+ public:
+ const FormatChunk& FmtChunk() const;
+
+ Result<uint32_t, nsresult> Parse(BufferReader& aReader);
+
+ void Reset();
+
+ private:
+ class FormatChunk {
+ public:
+ FormatChunk();
+ void Reset();
+
+ uint16_t WaveFormat() const;
+ uint16_t Channels() const;
+ uint32_t SampleRate() const;
+ uint16_t FrameSize() const;
+ uint16_t SampleFormat() const;
+
+ bool IsValid() const;
+ bool ParseNext(uint8_t c);
+
+ private:
+ void Update(uint8_t c);
+
+ uint8_t mRaw[FMT_CHUNK_MIN_SIZE];
+
+ int mPos;
+ };
+
+ FormatChunk mFmtChunk;
+};
+
+class DataParser {
+ private:
+ class DataChunk;
+
+ public:
+ DataParser();
+
+ const DataChunk& CurrentChunk() const;
+
+ void Reset();
+
+ private:
+ class DataChunk {
+ public:
+ void Reset();
+
+ private:
+ int mPos; // To Check Alignment
+ };
+
+ DataChunk mChunk;
+};
+
+class WAVTrackDemuxer : public MediaTrackDemuxer,
+ public DecoderDoctorLifeLogger<WAVTrackDemuxer> {
+ public:
+ explicit WAVTrackDemuxer(MediaResource* aSource);
+
+ bool Init();
+
+ int64_t StreamLength() const;
+
+ media::TimeUnit Duration() const;
+ media::TimeUnit Duration(int64_t aNumDataChunks) const;
+ media::TimeUnit DurationFromBytes(uint32_t aNumBytes) const;
+
+ media::TimeUnit SeekPosition() const;
+
+ RefPtr<MediaRawData> DemuxSample();
+
+ // MediaTrackDemuxer interface.
+ UniquePtr<TrackInfo> GetInfo() const override;
+ RefPtr<SeekPromise> Seek(const media::TimeUnit& aTime) override;
+ RefPtr<SamplesPromise> GetSamples(int32_t aNumSamples) override;
+ void Reset() override;
+ RefPtr<SkipAccessPointPromise> SkipToNextRandomAccessPoint(
+ const media::TimeUnit& aTimeThreshold) override;
+ int64_t GetResourceOffset() const override;
+ media::TimeIntervals GetBuffered() override;
+
+ private:
+ ~WAVTrackDemuxer() = default;
+
+ media::TimeUnit FastSeek(const media::TimeUnit& aTime);
+ media::TimeUnit ScanUntil(const media::TimeUnit& aTime);
+
+ MediaByteRange FindNextChunk();
+
+ MediaByteRange FindChunkHeader();
+ MediaByteRange FindRIFFHeader();
+ MediaByteRange FindFmtChunk();
+ MediaByteRange FindListChunk();
+ MediaByteRange FindInfoTag();
+
+ bool RIFFParserInit();
+ bool HeaderParserInit();
+ bool FmtChunkParserInit();
+ bool ListChunkParserInit(uint32_t aChunkSize);
+
+ bool SkipNextChunk(const MediaByteRange& aRange);
+
+ already_AddRefed<MediaRawData> GetNextChunk(const MediaByteRange& aRange);
+ already_AddRefed<MediaRawData> GetFileHeader(const MediaByteRange& aRange);
+
+ void UpdateState(const MediaByteRange& aRange);
+
+ uint64_t OffsetFromChunkIndex(uint32_t aChunkIndex) const;
+ uint64_t ChunkIndexFromTime(const media::TimeUnit& aTime) const;
+
+ uint32_t Read(uint8_t* aBuffer, int64_t aOffset, int32_t aSize);
+
+ MediaResourceIndex mSource;
+
+ DataParser mParser;
+ RIFFParser mRIFFParser;
+ HeaderParser mHeaderParser;
+
+ FormatParser mFmtParser;
+ // ListChunkParser mListChunkParser;
+
+ uint64_t mOffset;
+ uint64_t mFirstChunkOffset;
+
+ uint32_t mNumParsedChunks;
+ uint32_t mChunkIndex;
+
+ uint32_t mDataLength;
+ uint64_t mTotalChunkLen;
+
+ uint32_t mSamplesPerChunk;
+ uint32_t mSamplesPerSecond;
+
+ uint32_t mChannels;
+ uint32_t mSampleFormat;
+
+ UniquePtr<AudioInfo> mInfo;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/wave/moz.build b/dom/media/wave/moz.build
new file mode 100644
index 0000000000..04eeba1b77
--- /dev/null
+++ b/dom/media/wave/moz.build
@@ -0,0 +1,20 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS += [
+ "WaveDecoder.h",
+ "WaveDemuxer.h",
+]
+
+UNIFIED_SOURCES += [
+ "WaveDecoder.cpp",
+ "WaveDemuxer.cpp",
+]
+
+FINAL_LIBRARY = "xul"
+
+# Add libFuzzer configuration directives
+include("/tools/fuzzing/libfuzzer-config.mozbuild")
diff --git a/dom/media/webaudio/AlignedTArray.h b/dom/media/webaudio/AlignedTArray.h
new file mode 100644
index 0000000000..28c0f094ef
--- /dev/null
+++ b/dom/media/webaudio/AlignedTArray.h
@@ -0,0 +1,115 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef AlignedTArray_h__
+#define AlignedTArray_h__
+
+#include "mozilla/Alignment.h"
+#include "nsTArray.h"
+
+/**
+ * E: element type, must be a POD type.
+ * N: N bytes alignment for the first element, defaults to 32
+ * S: S bytes of inline storage
+ */
+template <typename E, int S, int N = 32>
+class AlignedAutoTArray : private AutoTArray<E, S + N> {
+ static_assert((N & (N - 1)) == 0, "N must be power of 2");
+ typedef AutoTArray<E, S + N> base_type;
+
+ public:
+ typedef E value_type;
+ typedef typename base_type::size_type size_type;
+ typedef typename base_type::index_type index_type;
+
+ AlignedAutoTArray() = default;
+ explicit AlignedAutoTArray(size_type capacity)
+ : base_type(capacity + sExtra) {}
+ value_type* Elements() { return getAligned(base_type::Elements()); }
+ const value_type* Elements() const {
+ return getAligned(base_type::Elements());
+ }
+ value_type& operator[](index_type i) { return Elements()[i]; }
+ const value_type& operator[](index_type i) const { return Elements()[i]; }
+
+ void SetLength(size_type newLen) { base_type::SetLength(newLen + sExtra); }
+
+ [[nodiscard]] bool SetLength(size_type newLen, const mozilla::fallible_t&) {
+ return base_type::SetLength(newLen + sExtra, mozilla::fallible);
+ }
+
+ size_type Length() const {
+ return base_type::Length() <= sExtra ? 0 : base_type::Length() - sExtra;
+ }
+
+ using base_type::ShallowSizeOfExcludingThis;
+ using base_type::ShallowSizeOfIncludingThis;
+
+ private:
+ AlignedAutoTArray(const AlignedAutoTArray& other) = delete;
+ void operator=(const AlignedAutoTArray& other) = delete;
+
+ static const size_type sPadding =
+ N <= MOZ_ALIGNOF(E) ? 0 : N - MOZ_ALIGNOF(E);
+ static const size_type sExtra = (sPadding + sizeof(E) - 1) / sizeof(E);
+
+ template <typename U>
+ static U* getAligned(U* p) {
+ return reinterpret_cast<U*>(((uintptr_t)p + N - 1) & ~(N - 1));
+ }
+};
+
+/**
+ * E: element type, must be a POD type.
+ * N: N bytes alignment for the first element, defaults to 32
+ */
+template <typename E, int N = 32>
+class AlignedTArray : private nsTArray_Impl<E, nsTArrayInfallibleAllocator> {
+ static_assert((N & (N - 1)) == 0, "N must be power of 2");
+ typedef nsTArray_Impl<E, nsTArrayInfallibleAllocator> base_type;
+
+ public:
+ typedef E value_type;
+ typedef typename base_type::size_type size_type;
+ typedef typename base_type::index_type index_type;
+
+ AlignedTArray() = default;
+ explicit AlignedTArray(size_type capacity) : base_type(capacity + sExtra) {}
+ value_type* Elements() { return getAligned(base_type::Elements()); }
+ const value_type* Elements() const {
+ return getAligned(base_type::Elements());
+ }
+ value_type& operator[](index_type i) { return Elements()[i]; }
+ const value_type& operator[](index_type i) const { return Elements()[i]; }
+
+ void SetLength(size_type newLen) { base_type::SetLength(newLen + sExtra); }
+
+ [[nodiscard]] bool SetLength(size_type newLen, const mozilla::fallible_t&) {
+ return base_type::SetLength(newLen + sExtra, mozilla::fallible);
+ }
+
+ size_type Length() const {
+ return base_type::Length() <= sExtra ? 0 : base_type::Length() - sExtra;
+ }
+
+ using base_type::ShallowSizeOfExcludingThis;
+ using base_type::ShallowSizeOfIncludingThis;
+
+ private:
+ AlignedTArray(const AlignedTArray& other) = delete;
+ void operator=(const AlignedTArray& other) = delete;
+
+ static const size_type sPadding =
+ N <= MOZ_ALIGNOF(E) ? 0 : N - MOZ_ALIGNOF(E);
+ static const size_type sExtra = (sPadding + sizeof(E) - 1) / sizeof(E);
+
+ template <typename U>
+ static U* getAligned(U* p) {
+ return reinterpret_cast<U*>(((uintptr_t)p + N - 1) & ~(N - 1));
+ }
+};
+
+#endif // AlignedTArray_h__
diff --git a/dom/media/webaudio/AlignmentUtils.h b/dom/media/webaudio/AlignmentUtils.h
new file mode 100644
index 0000000000..c22dd4a58b
--- /dev/null
+++ b/dom/media/webaudio/AlignmentUtils.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef AlignmentUtils_h__
+#define AlignmentUtils_h__
+
+#define IS_ALIGNED16(ptr) ((((uintptr_t)ptr + 15) & ~0x0F) == (uintptr_t)ptr)
+
+#ifdef DEBUG
+# define ASSERT_ALIGNED16(ptr) \
+ MOZ_ASSERT(IS_ALIGNED16(ptr), \
+ #ptr " has to be aligned to a 16 byte boundary");
+#else
+# define ASSERT_ALIGNED16(ptr)
+#endif
+
+#ifdef DEBUG
+# define ASSERT_MULTIPLE16(v) \
+ MOZ_ASSERT(v % 16 == 0, #v " has to be a a multiple of 16");
+#else
+# define ASSERT_MULTIPLE16(v)
+#endif
+
+#define ALIGNED16(ptr) (float*)(((uintptr_t)ptr + 15) & ~0x0F);
+
+#endif
diff --git a/dom/media/webaudio/AnalyserNode.cpp b/dom/media/webaudio/AnalyserNode.cpp
new file mode 100644
index 0000000000..a3b0508a97
--- /dev/null
+++ b/dom/media/webaudio/AnalyserNode.cpp
@@ -0,0 +1,389 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/AnalyserNode.h"
+#include "mozilla/dom/AnalyserNodeBinding.h"
+#include "AudioNodeEngine.h"
+#include "AudioNodeTrack.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/PodOperations.h"
+#include "nsMathUtils.h"
+#include "Tracing.h"
+
+namespace mozilla {
+
+static const uint32_t MAX_FFT_SIZE = 32768;
+static const size_t CHUNK_COUNT = MAX_FFT_SIZE >> WEBAUDIO_BLOCK_SIZE_BITS;
+static_assert(MAX_FFT_SIZE == CHUNK_COUNT * WEBAUDIO_BLOCK_SIZE,
+ "MAX_FFT_SIZE must be a multiple of WEBAUDIO_BLOCK_SIZE");
+static_assert((CHUNK_COUNT & (CHUNK_COUNT - 1)) == 0,
+ "CHUNK_COUNT must be power of 2 for remainder behavior");
+
+namespace dom {
+
+class AnalyserNodeEngine final : public AudioNodeEngine {
+ class TransferBuffer final : public Runnable {
+ public:
+ TransferBuffer(AudioNodeTrack* aTrack, const AudioChunk& aChunk)
+ : Runnable("dom::AnalyserNodeEngine::TransferBuffer"),
+ mTrack(aTrack),
+ mChunk(aChunk) {}
+
+ NS_IMETHOD Run() override {
+ RefPtr<AnalyserNode> node =
+ static_cast<AnalyserNode*>(mTrack->Engine()->NodeMainThread());
+ if (node) {
+ node->AppendChunk(mChunk);
+ }
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<AudioNodeTrack> mTrack;
+ AudioChunk mChunk;
+ };
+
+ public:
+ explicit AnalyserNodeEngine(AnalyserNode* aNode) : AudioNodeEngine(aNode) {
+ MOZ_ASSERT(NS_IsMainThread());
+ }
+
+ virtual void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom,
+ const AudioBlock& aInput, AudioBlock* aOutput,
+ bool* aFinished) override {
+ TRACE("AnalyserNodeEngine::ProcessBlock");
+ *aOutput = aInput;
+
+ if (aInput.IsNull()) {
+ // If AnalyserNode::mChunks has only null chunks, then there is no need
+ // to send further null chunks.
+ if (mChunksToProcess == 0) {
+ return;
+ }
+
+ --mChunksToProcess;
+ if (mChunksToProcess == 0) {
+ aTrack->ScheduleCheckForInactive();
+ }
+
+ } else {
+ // This many null chunks will be required to empty AnalyserNode::mChunks.
+ mChunksToProcess = CHUNK_COUNT;
+ }
+
+ RefPtr<TransferBuffer> transfer =
+ new TransferBuffer(aTrack, aInput.AsAudioChunk());
+ mAbstractMainThread->Dispatch(transfer.forget());
+ }
+
+ virtual bool IsActive() const override { return mChunksToProcess != 0; }
+
+ virtual size_t SizeOfIncludingThis(
+ MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ uint32_t mChunksToProcess = 0;
+};
+
+/* static */
+already_AddRefed<AnalyserNode> AnalyserNode::Create(
+ AudioContext& aAudioContext, const AnalyserOptions& aOptions,
+ ErrorResult& aRv) {
+ RefPtr<AnalyserNode> analyserNode = new AnalyserNode(&aAudioContext);
+
+ analyserNode->Initialize(aOptions, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ analyserNode->SetFftSize(aOptions.mFftSize, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ analyserNode->SetMinAndMaxDecibels(aOptions.mMinDecibels,
+ aOptions.mMaxDecibels, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ analyserNode->SetSmoothingTimeConstant(aOptions.mSmoothingTimeConstant, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ return analyserNode.forget();
+}
+
+AnalyserNode::AnalyserNode(AudioContext* aContext)
+ : AudioNode(aContext, 2, ChannelCountMode::Max,
+ ChannelInterpretation::Speakers),
+ mAnalysisBlock(2048),
+ mMinDecibels(-100.),
+ mMaxDecibels(-30.),
+ mSmoothingTimeConstant(.8) {
+ mTrack =
+ AudioNodeTrack::Create(aContext, new AnalyserNodeEngine(this),
+ AudioNodeTrack::NO_TRACK_FLAGS, aContext->Graph());
+
+ // Enough chunks must be recorded to handle the case of fftSize being
+ // increased to maximum immediately before getFloatTimeDomainData() is
+ // called, for example.
+ Unused << mChunks.SetLength(CHUNK_COUNT, fallible);
+
+ AllocateBuffer();
+}
+
+size_t AnalyserNode::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf);
+ amount += mAnalysisBlock.SizeOfExcludingThis(aMallocSizeOf);
+ amount += mChunks.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ amount += mOutputBuffer.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ return amount;
+}
+
+size_t AnalyserNode::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
+
+JSObject* AnalyserNode::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return AnalyserNode_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void AnalyserNode::SetFftSize(uint32_t aValue, ErrorResult& aRv) {
+ // Disallow values that are not a power of 2 and outside the [32,32768] range
+ if (aValue < 32 || aValue > MAX_FFT_SIZE || (aValue & (aValue - 1)) != 0) {
+ aRv.ThrowIndexSizeError(nsPrintfCString(
+ "FFT size %u is not a power of two in the range 32 to 32768", aValue));
+ return;
+ }
+ if (FftSize() != aValue) {
+ mAnalysisBlock.SetFFTSize(aValue);
+ AllocateBuffer();
+ }
+}
+
+void AnalyserNode::SetMinDecibels(double aValue, ErrorResult& aRv) {
+ if (aValue >= mMaxDecibels) {
+ aRv.ThrowIndexSizeError(nsPrintfCString(
+ "%g is not strictly smaller than current maxDecibels (%g)", aValue,
+ mMaxDecibels));
+ return;
+ }
+ mMinDecibels = aValue;
+}
+
+void AnalyserNode::SetMaxDecibels(double aValue, ErrorResult& aRv) {
+ if (aValue <= mMinDecibels) {
+ aRv.ThrowIndexSizeError(nsPrintfCString(
+ "%g is not strictly larger than current minDecibels (%g)", aValue,
+ mMinDecibels));
+ return;
+ }
+ mMaxDecibels = aValue;
+}
+
+void AnalyserNode::SetMinAndMaxDecibels(double aMinValue, double aMaxValue,
+ ErrorResult& aRv) {
+ if (aMinValue >= aMaxValue) {
+ aRv.ThrowIndexSizeError(nsPrintfCString(
+ "minDecibels value (%g) must be smaller than maxDecibels value (%g)",
+ aMinValue, aMaxValue));
+ return;
+ }
+ mMinDecibels = aMinValue;
+ mMaxDecibels = aMaxValue;
+}
+
+void AnalyserNode::SetSmoothingTimeConstant(double aValue, ErrorResult& aRv) {
+ if (aValue < 0 || aValue > 1) {
+ aRv.ThrowIndexSizeError(
+ nsPrintfCString("%g is not in the range [0, 1]", aValue));
+ return;
+ }
+ mSmoothingTimeConstant = aValue;
+}
+
+void AnalyserNode::GetFloatFrequencyData(const Float32Array& aArray) {
+ if (!FFTAnalysis()) {
+ // Might fail to allocate memory
+ return;
+ }
+
+ aArray.ComputeState();
+
+ float* buffer = aArray.Data();
+ size_t length = std::min(size_t(aArray.Length()), mOutputBuffer.Length());
+
+ for (size_t i = 0; i < length; ++i) {
+ buffer[i] = WebAudioUtils::ConvertLinearToDecibels(
+ mOutputBuffer[i], -std::numeric_limits<float>::infinity());
+ }
+}
+
+void AnalyserNode::GetByteFrequencyData(const Uint8Array& aArray) {
+ if (!FFTAnalysis()) {
+ // Might fail to allocate memory
+ return;
+ }
+
+ const double rangeScaleFactor = 1.0 / (mMaxDecibels - mMinDecibels);
+
+ aArray.ComputeState();
+
+ unsigned char* buffer = aArray.Data();
+ size_t length = std::min(size_t(aArray.Length()), mOutputBuffer.Length());
+
+ for (size_t i = 0; i < length; ++i) {
+ const double decibels =
+ WebAudioUtils::ConvertLinearToDecibels(mOutputBuffer[i], mMinDecibels);
+ // scale down the value to the range of [0, UCHAR_MAX]
+ const double scaled = std::max(
+ 0.0, std::min(double(UCHAR_MAX), UCHAR_MAX * (decibels - mMinDecibels) *
+ rangeScaleFactor));
+ buffer[i] = static_cast<unsigned char>(scaled);
+ }
+}
+
+void AnalyserNode::GetFloatTimeDomainData(const Float32Array& aArray) {
+ aArray.ComputeState();
+
+ float* buffer = aArray.Data();
+ size_t length = std::min(aArray.Length(), FftSize());
+
+ GetTimeDomainData(buffer, length);
+}
+
+void AnalyserNode::GetByteTimeDomainData(const Uint8Array& aArray) {
+ aArray.ComputeState();
+
+ size_t length = std::min(aArray.Length(), FftSize());
+
+ AlignedTArray<float> tmpBuffer;
+ if (!tmpBuffer.SetLength(length, fallible)) {
+ return;
+ }
+
+ GetTimeDomainData(tmpBuffer.Elements(), length);
+
+ unsigned char* buffer = aArray.Data();
+ for (size_t i = 0; i < length; ++i) {
+ const float value = tmpBuffer[i];
+ // scale the value to the range of [0, UCHAR_MAX]
+ const float scaled =
+ std::max(0.0f, std::min(float(UCHAR_MAX), 128.0f * (value + 1.0f)));
+ buffer[i] = static_cast<unsigned char>(scaled);
+ }
+}
+
+bool AnalyserNode::FFTAnalysis() {
+ AlignedTArray<float> tmpBuffer;
+ size_t fftSize = FftSize();
+ if (!tmpBuffer.SetLength(fftSize, fallible)) {
+ return false;
+ }
+
+ float* inputBuffer = tmpBuffer.Elements();
+ GetTimeDomainData(inputBuffer, fftSize);
+ ApplyBlackmanWindow(inputBuffer, fftSize);
+ mAnalysisBlock.PerformFFT(inputBuffer);
+
+ // Normalize so than an input sine wave at 0dBfs registers as 0dBfs (undo FFT
+ // scaling factor).
+ const double magnitudeScale = 1.0 / fftSize;
+
+ for (uint32_t i = 0; i < mOutputBuffer.Length(); ++i) {
+ double scalarMagnitude =
+ NS_hypot(mAnalysisBlock.RealData(i), mAnalysisBlock.ImagData(i)) *
+ magnitudeScale;
+ mOutputBuffer[i] = mSmoothingTimeConstant * mOutputBuffer[i] +
+ (1.0 - mSmoothingTimeConstant) * scalarMagnitude;
+ }
+
+ return true;
+}
+
+void AnalyserNode::ApplyBlackmanWindow(float* aBuffer, uint32_t aSize) {
+ double alpha = 0.16;
+ double a0 = 0.5 * (1.0 - alpha);
+ double a1 = 0.5;
+ double a2 = 0.5 * alpha;
+
+ for (uint32_t i = 0; i < aSize; ++i) {
+ double x = double(i) / aSize;
+ double window = a0 - a1 * cos(2 * M_PI * x) + a2 * cos(4 * M_PI * x);
+ aBuffer[i] *= window;
+ }
+}
+
+bool AnalyserNode::AllocateBuffer() {
+ bool result = true;
+ if (mOutputBuffer.Length() != FrequencyBinCount()) {
+ if (!mOutputBuffer.SetLength(FrequencyBinCount(), fallible)) {
+ return false;
+ }
+ memset(mOutputBuffer.Elements(), 0, sizeof(float) * FrequencyBinCount());
+ }
+ return result;
+}
+
+void AnalyserNode::AppendChunk(const AudioChunk& aChunk) {
+ if (mChunks.Length() == 0) {
+ return;
+ }
+
+ ++mCurrentChunk;
+ mChunks[mCurrentChunk & (CHUNK_COUNT - 1)] = aChunk;
+}
+
+// Reads into aData the oldest aLength samples of the fftSize most recent
+// samples.
+void AnalyserNode::GetTimeDomainData(float* aData, size_t aLength) {
+ size_t fftSize = FftSize();
+ MOZ_ASSERT(aLength <= fftSize);
+
+ if (mChunks.Length() == 0) {
+ PodZero(aData, aLength);
+ return;
+ }
+
+ size_t readChunk =
+ mCurrentChunk - ((fftSize - 1) >> WEBAUDIO_BLOCK_SIZE_BITS);
+ size_t readIndex = (0 - fftSize) & (WEBAUDIO_BLOCK_SIZE - 1);
+ MOZ_ASSERT(readIndex == 0 || readIndex + fftSize == WEBAUDIO_BLOCK_SIZE);
+
+ for (size_t writeIndex = 0; writeIndex < aLength;) {
+ const AudioChunk& chunk = mChunks[readChunk & (CHUNK_COUNT - 1)];
+ const size_t channelCount = chunk.ChannelCount();
+ size_t copyLength =
+ std::min<size_t>(aLength - writeIndex, WEBAUDIO_BLOCK_SIZE);
+ float* dataOut = &aData[writeIndex];
+
+ if (channelCount == 0) {
+ PodZero(dataOut, copyLength);
+ } else {
+ float scale = chunk.mVolume / channelCount;
+ { // channel 0
+ auto channelData =
+ static_cast<const float*>(chunk.mChannelData[0]) + readIndex;
+ AudioBufferCopyWithScale(channelData, scale, dataOut, copyLength);
+ }
+ for (uint32_t i = 1; i < channelCount; ++i) {
+ auto channelData =
+ static_cast<const float*>(chunk.mChannelData[i]) + readIndex;
+ AudioBufferAddWithScale(channelData, scale, dataOut, copyLength);
+ }
+ }
+
+ readChunk++;
+ writeIndex += copyLength;
+ }
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/media/webaudio/AnalyserNode.h b/dom/media/webaudio/AnalyserNode.h
new file mode 100644
index 0000000000..bdec13f6b9
--- /dev/null
+++ b/dom/media/webaudio/AnalyserNode.h
@@ -0,0 +1,82 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef AnalyserNode_h_
+#define AnalyserNode_h_
+
+#include "AudioNode.h"
+#include "FFTBlock.h"
+#include "AlignedTArray.h"
+
+namespace mozilla::dom {
+
+class AudioContext;
+struct AnalyserOptions;
+
+class AnalyserNode final : public AudioNode {
+ public:
+ static already_AddRefed<AnalyserNode> Create(AudioContext& aAudioContext,
+ const AnalyserOptions& aOptions,
+ ErrorResult& aRv);
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(AnalyserNode, AudioNode)
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ static already_AddRefed<AnalyserNode> Constructor(
+ const GlobalObject& aGlobal, AudioContext& aAudioContext,
+ const AnalyserOptions& aOptions, ErrorResult& aRv) {
+ return Create(aAudioContext, aOptions, aRv);
+ }
+
+ void GetFloatFrequencyData(const Float32Array& aArray);
+ void GetByteFrequencyData(const Uint8Array& aArray);
+ void GetFloatTimeDomainData(const Float32Array& aArray);
+ void GetByteTimeDomainData(const Uint8Array& aArray);
+ uint32_t FftSize() const { return mAnalysisBlock.FFTSize(); }
+ void SetFftSize(uint32_t aValue, ErrorResult& aRv);
+ uint32_t FrequencyBinCount() const { return FftSize() / 2; }
+ double MinDecibels() const { return mMinDecibels; }
+ void SetMinDecibels(double aValue, ErrorResult& aRv);
+ double MaxDecibels() const { return mMaxDecibels; }
+ void SetMaxDecibels(double aValue, ErrorResult& aRv);
+ double SmoothingTimeConstant() const { return mSmoothingTimeConstant; }
+ void SetSmoothingTimeConstant(double aValue, ErrorResult& aRv);
+
+ virtual const char* NodeType() const override { return "AnalyserNode"; }
+
+ virtual size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override;
+ virtual size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override;
+
+ void SetMinAndMaxDecibels(double aMinValue, double aMaxValue,
+ ErrorResult& aRv);
+
+ private:
+ ~AnalyserNode() = default;
+
+ friend class AnalyserNodeEngine;
+ void AppendChunk(const AudioChunk& aChunk);
+ bool AllocateBuffer();
+ bool FFTAnalysis();
+ void ApplyBlackmanWindow(float* aBuffer, uint32_t aSize);
+ void GetTimeDomainData(float* aData, size_t aLength);
+
+ private:
+ explicit AnalyserNode(AudioContext* aContext);
+
+ FFTBlock mAnalysisBlock;
+ nsTArray<AudioChunk> mChunks;
+ double mMinDecibels;
+ double mMaxDecibels;
+ double mSmoothingTimeConstant;
+ size_t mCurrentChunk = 0;
+ AlignedTArray<float> mOutputBuffer;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webaudio/AudioBlock.cpp b/dom/media/webaudio/AudioBlock.cpp
new file mode 100644
index 0000000000..09bbd7f70a
--- /dev/null
+++ b/dom/media/webaudio/AudioBlock.cpp
@@ -0,0 +1,166 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioBlock.h"
+#include "AlignmentUtils.h"
+
+namespace mozilla {
+
+/**
+ * Heap-allocated buffer of channels of 128-sample float arrays, with
+ * threadsafe refcounting. Typically you would allocate one of these, fill it
+ * in, and then treat it as immutable while it's shared.
+ *
+ * Downstream references are accounted specially so that the creator of the
+ * buffer can reuse and modify its contents next iteration if other references
+ * are all downstream temporary references held by AudioBlock.
+ *
+ * We guarantee 16 byte alignment of the channel data.
+ */
+class AudioBlockBuffer final : public ThreadSharedObject {
+ public:
+ virtual AudioBlockBuffer* AsAudioBlockBuffer() override { return this; };
+
+ uint32_t ChannelsAllocated() const { return mChannelsAllocated; }
+ float* ChannelData(uint32_t aChannel) const {
+ float* base =
+ reinterpret_cast<float*>(((uintptr_t)(this + 1) + 15) & ~0x0F);
+ ASSERT_ALIGNED16(base);
+ return base + aChannel * WEBAUDIO_BLOCK_SIZE;
+ }
+
+ static already_AddRefed<AudioBlockBuffer> Create(uint32_t aChannelCount) {
+ CheckedInt<size_t> size = WEBAUDIO_BLOCK_SIZE;
+ size *= aChannelCount;
+ size *= sizeof(float);
+ size += sizeof(AudioBlockBuffer);
+ size += 15; // padding for alignment
+ if (!size.isValid()) {
+ MOZ_CRASH();
+ }
+
+ void* m = operator new(size.value());
+ RefPtr<AudioBlockBuffer> p = new (m) AudioBlockBuffer(aChannelCount);
+ NS_ASSERTION((reinterpret_cast<char*>(p.get() + 1) -
+ reinterpret_cast<char*>(p.get())) %
+ 4 ==
+ 0,
+ "AudioBlockBuffers should be at least 4-byte aligned");
+ return p.forget();
+ }
+
+ // Graph thread only.
+ void DownstreamRefAdded() { ++mDownstreamRefCount; }
+ void DownstreamRefRemoved() {
+ MOZ_ASSERT(mDownstreamRefCount > 0);
+ --mDownstreamRefCount;
+ }
+ // Whether this is shared by any owners that are not downstream.
+ // Called only from owners with a reference that is not a downstream
+ // reference. Graph thread only.
+ bool HasLastingShares() const {
+ // mRefCnt is atomic and so reading its value is defined even when
+ // modifications may happen on other threads. mDownstreamRefCount is
+ // not modified on any other thread.
+ //
+ // If all other references are downstream references (managed on this, the
+ // graph thread), then other threads are not using this buffer and cannot
+ // add further references. This method can safely return false. The
+ // buffer contents can be modified.
+ //
+ // If there are other references that are not downstream references, then
+ // this method will return true. The buffer will be assumed to be still
+ // in use and so will not be reused.
+ nsrefcnt count = mRefCnt;
+ // This test is strictly less than because the caller has a reference
+ // that is not a downstream reference.
+ MOZ_ASSERT(mDownstreamRefCount < count);
+ return count != mDownstreamRefCount + 1;
+ }
+
+ virtual size_t SizeOfIncludingThis(
+ MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ private:
+ explicit AudioBlockBuffer(uint32_t aChannelsAllocated)
+ : mChannelsAllocated(aChannelsAllocated) {}
+ ~AudioBlockBuffer() override { MOZ_ASSERT(mDownstreamRefCount == 0); }
+
+ nsAutoRefCnt mDownstreamRefCount;
+ const uint32_t mChannelsAllocated;
+};
+
+AudioBlock::~AudioBlock() { ClearDownstreamMark(); }
+
+void AudioBlock::SetBuffer(ThreadSharedObject* aNewBuffer) {
+ if (aNewBuffer == mBuffer) {
+ return;
+ }
+
+ ClearDownstreamMark();
+
+ mBuffer = aNewBuffer;
+
+ if (!aNewBuffer) {
+ return;
+ }
+
+ AudioBlockBuffer* buffer = aNewBuffer->AsAudioBlockBuffer();
+ if (buffer) {
+ buffer->DownstreamRefAdded();
+ mBufferIsDownstreamRef = true;
+ }
+}
+
+void AudioBlock::ClearDownstreamMark() {
+ if (mBufferIsDownstreamRef) {
+ mBuffer->AsAudioBlockBuffer()->DownstreamRefRemoved();
+ mBufferIsDownstreamRef = false;
+ }
+}
+
+bool AudioBlock::CanWrite() {
+ // If mBufferIsDownstreamRef is set then the buffer is not ours to use.
+ // It may be in use by another node which is not downstream.
+ return !mBufferIsDownstreamRef &&
+ !mBuffer->AsAudioBlockBuffer()->HasLastingShares();
+}
+
+void AudioBlock::AllocateChannels(uint32_t aChannelCount) {
+ MOZ_ASSERT(mDuration == WEBAUDIO_BLOCK_SIZE);
+
+ if (mBufferIsDownstreamRef) {
+ // This is not our buffer to re-use.
+ ClearDownstreamMark();
+ } else if (mBuffer) {
+ AudioBlockBuffer* buffer = mBuffer->AsAudioBlockBuffer();
+ if (buffer && !buffer->HasLastingShares() &&
+ buffer->ChannelsAllocated() >= aChannelCount) {
+ MOZ_ASSERT(mBufferFormat == AUDIO_FORMAT_FLOAT32);
+ // No need to allocate again.
+ uint32_t previousChannelCount = ChannelCount();
+ mChannelData.SetLength(aChannelCount);
+ for (uint32_t i = previousChannelCount; i < aChannelCount; ++i) {
+ mChannelData[i] = buffer->ChannelData(i);
+ }
+ mVolume = 1.0f;
+ return;
+ }
+ }
+
+ RefPtr<AudioBlockBuffer> buffer = AudioBlockBuffer::Create(aChannelCount);
+ mChannelData.SetLength(aChannelCount);
+ for (uint32_t i = 0; i < aChannelCount; ++i) {
+ mChannelData[i] = buffer->ChannelData(i);
+ }
+ mBuffer = std::move(buffer);
+ mVolume = 1.0f;
+ mBufferFormat = AUDIO_FORMAT_FLOAT32;
+}
+
+} // namespace mozilla
diff --git a/dom/media/webaudio/AudioBlock.h b/dom/media/webaudio/AudioBlock.h
new file mode 100644
index 0000000000..b5178c9d8a
--- /dev/null
+++ b/dom/media/webaudio/AudioBlock.h
@@ -0,0 +1,134 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef MOZILLA_AUDIOBLOCK_H_
+#define MOZILLA_AUDIOBLOCK_H_
+
+#include "AudioSegment.h"
+
+namespace mozilla {
+
+/**
+ * An AudioChunk whose buffer contents need to be valid only for one
+ * processing block iteration, after which contents can be overwritten if the
+ * buffer has not been passed to longer term storage or to another thread,
+ * which may happen though AsAudioChunk() or AsMutableChunk().
+ *
+ * Use on graph thread only.
+ */
+class AudioBlock : private AudioChunk {
+ public:
+ AudioBlock() {
+ mDuration = WEBAUDIO_BLOCK_SIZE;
+ mBufferFormat = AUDIO_FORMAT_SILENCE;
+ }
+ // No effort is made in constructors to ensure that mBufferIsDownstreamRef
+ // is set because the block is expected to be a temporary and so the
+ // reference will be released before the next iteration.
+ // The custom copy constructor is required so as not to set
+ // mBufferIsDownstreamRef without notifying AudioBlockBuffer.
+ AudioBlock(const AudioBlock& aBlock) : AudioChunk(aBlock.AsAudioChunk()) {}
+ explicit AudioBlock(const AudioChunk& aChunk) : AudioChunk(aChunk) {
+ MOZ_ASSERT(aChunk.mDuration == WEBAUDIO_BLOCK_SIZE);
+ }
+ ~AudioBlock();
+
+ using AudioChunk::ChannelCount;
+ using AudioChunk::ChannelData;
+ using AudioChunk::GetDuration;
+ using AudioChunk::IsNull;
+ using AudioChunk::SizeOfExcludingThis;
+ using AudioChunk::SizeOfExcludingThisIfUnshared;
+ // mDuration is not exposed. Use GetDuration().
+ // mBuffer is not exposed. Use Get/SetBuffer().
+ using AudioChunk::mBufferFormat;
+ using AudioChunk::mChannelData;
+ using AudioChunk::mVolume;
+
+ const AudioChunk& AsAudioChunk() const { return *this; }
+ AudioChunk* AsMutableChunk() {
+ ClearDownstreamMark();
+ return this;
+ }
+
+ /**
+ * Allocates, if necessary, aChannelCount buffers of WEBAUDIO_BLOCK_SIZE float
+ * samples for writing.
+ */
+ void AllocateChannels(uint32_t aChannelCount);
+
+ /**
+ * ChannelFloatsForWrite() should only be used when the buffers have been
+ * created with AllocateChannels().
+ */
+ float* ChannelFloatsForWrite(size_t aChannel) {
+ MOZ_ASSERT(mBufferFormat == AUDIO_FORMAT_FLOAT32);
+ MOZ_ASSERT(CanWrite());
+ return static_cast<float*>(const_cast<void*>(mChannelData[aChannel]));
+ }
+
+ ThreadSharedObject* GetBuffer() const { return mBuffer; }
+ void SetBuffer(ThreadSharedObject* aNewBuffer);
+ void SetNull(TrackTime aDuration) {
+ MOZ_ASSERT(aDuration == WEBAUDIO_BLOCK_SIZE);
+ SetBuffer(nullptr);
+ mChannelData.Clear();
+ mVolume = 1.0f;
+ mBufferFormat = AUDIO_FORMAT_SILENCE;
+ }
+
+ AudioBlock& operator=(const AudioBlock& aBlock) {
+ // Instead of just copying, mBufferIsDownstreamRef must be first cleared
+ // if set. It is set again for the new mBuffer if possible. This happens
+ // in SetBuffer().
+ return *this = aBlock.AsAudioChunk();
+ }
+ AudioBlock& operator=(const AudioChunk& aChunk) {
+ MOZ_ASSERT(aChunk.mDuration == WEBAUDIO_BLOCK_SIZE);
+ SetBuffer(aChunk.mBuffer);
+ mChannelData = aChunk.mChannelData;
+ mVolume = aChunk.mVolume;
+ mBufferFormat = aChunk.mBufferFormat;
+ return *this;
+ }
+
+ bool IsMuted() const { return mVolume == 0.0f; }
+
+ bool IsSilentOrSubnormal() const {
+ if (!mBuffer) {
+ return true;
+ }
+
+ for (uint32_t i = 0, length = mChannelData.Length(); i < length; ++i) {
+ const float* channel = static_cast<const float*>(mChannelData[i]);
+ for (TrackTime frame = 0; frame < mDuration; ++frame) {
+ if (fabs(channel[frame]) >= FLT_MIN) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+ }
+
+ private:
+ void ClearDownstreamMark();
+ bool CanWrite();
+
+ // mBufferIsDownstreamRef is set only when mBuffer references an
+ // AudioBlockBuffer created in a different AudioBlock. That can happen when
+ // this AudioBlock is on a node downstream from the node which created the
+ // buffer. When this is set, the AudioBlockBuffer is notified that this
+ // reference does not prevent the upstream node from re-using the buffer next
+ // iteration and modifying its contents. The AudioBlockBuffer is also
+ // notified when mBuffer releases this reference.
+ bool mBufferIsDownstreamRef = false;
+};
+
+} // namespace mozilla
+
+MOZ_DECLARE_RELOCATE_USING_MOVE_CONSTRUCTOR(mozilla::AudioBlock)
+
+#endif // MOZILLA_AUDIOBLOCK_H_
diff --git a/dom/media/webaudio/AudioBuffer.cpp b/dom/media/webaudio/AudioBuffer.cpp
new file mode 100644
index 0000000000..03e70567b9
--- /dev/null
+++ b/dom/media/webaudio/AudioBuffer.cpp
@@ -0,0 +1,480 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioBuffer.h"
+#include "mozilla/dom/AudioBufferBinding.h"
+#include "jsfriendapi.h"
+#include "js/ArrayBuffer.h" // JS::StealArrayBufferContents
+#include "js/experimental/TypedData.h" // JS_NewFloat32Array, JS_GetFloat32ArrayData, JS_GetTypedArrayLength, JS_GetArrayBufferViewBuffer
+#include "mozilla/ErrorResult.h"
+#include "AudioSegment.h"
+#include "AudioChannelFormat.h"
+#include "mozilla/PodOperations.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/HoldDropJSObjects.h"
+#include "mozilla/MemoryReporting.h"
+#include "AudioNodeEngine.h"
+#include "nsPrintfCString.h"
+#include "nsTHashSet.h"
+#include <numeric>
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(AudioBuffer)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AudioBuffer)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mJSChannels)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+ tmp->ClearJSChannels();
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AudioBuffer)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(AudioBuffer)
+ NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
+ for (uint32_t i = 0; i < tmp->mJSChannels.Length(); ++i) {
+ NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mJSChannels[i])
+ }
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+/**
+ * AudioBuffers can be shared between AudioContexts, so we need a separate
+ * mechanism to track their memory usage. This thread-safe class keeps track of
+ * all the AudioBuffers, and gets called back by the memory reporting system
+ * when a memory report is needed, reporting how much memory is used by the
+ * buffers backing AudioBuffer objects. */
+class AudioBufferMemoryTracker : public nsIMemoryReporter {
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIMEMORYREPORTER
+
+ private:
+ AudioBufferMemoryTracker();
+ virtual ~AudioBufferMemoryTracker();
+
+ public:
+ /* Those methods can be called on any thread. */
+ static void RegisterAudioBuffer(const AudioBuffer* aAudioBuffer);
+ static void UnregisterAudioBuffer(const AudioBuffer* aAudioBuffer);
+
+ private:
+ static AudioBufferMemoryTracker* GetInstance();
+ /* Those methods must be called with the lock held. */
+ void RegisterAudioBufferInternal(const AudioBuffer* aAudioBuffer);
+ /* Returns the number of buffers still present in the hash table. */
+ uint32_t UnregisterAudioBufferInternal(const AudioBuffer* aAudioBuffer);
+ void Init();
+
+ /* This protects all members of this class. */
+ static StaticMutex sMutex MOZ_UNANNOTATED;
+ static StaticRefPtr<AudioBufferMemoryTracker> sSingleton;
+ nsTHashSet<const AudioBuffer*> mBuffers;
+};
+
+StaticRefPtr<AudioBufferMemoryTracker> AudioBufferMemoryTracker::sSingleton;
+StaticMutex AudioBufferMemoryTracker::sMutex;
+
+NS_IMPL_ISUPPORTS(AudioBufferMemoryTracker, nsIMemoryReporter);
+
+AudioBufferMemoryTracker* AudioBufferMemoryTracker::GetInstance() {
+ sMutex.AssertCurrentThreadOwns();
+ if (!sSingleton) {
+ sSingleton = new AudioBufferMemoryTracker();
+ sSingleton->Init();
+ }
+ return sSingleton;
+}
+
+AudioBufferMemoryTracker::AudioBufferMemoryTracker() = default;
+
+void AudioBufferMemoryTracker::Init() { RegisterWeakMemoryReporter(this); }
+
+AudioBufferMemoryTracker::~AudioBufferMemoryTracker() {
+ UnregisterWeakMemoryReporter(this);
+}
+
+void AudioBufferMemoryTracker::RegisterAudioBuffer(
+ const AudioBuffer* aAudioBuffer) {
+ StaticMutexAutoLock lock(sMutex);
+ AudioBufferMemoryTracker* tracker = AudioBufferMemoryTracker::GetInstance();
+ tracker->RegisterAudioBufferInternal(aAudioBuffer);
+}
+
+void AudioBufferMemoryTracker::UnregisterAudioBuffer(
+ const AudioBuffer* aAudioBuffer) {
+ StaticMutexAutoLock lock(sMutex);
+ AudioBufferMemoryTracker* tracker = AudioBufferMemoryTracker::GetInstance();
+ uint32_t count;
+ count = tracker->UnregisterAudioBufferInternal(aAudioBuffer);
+ if (count == 0) {
+ sSingleton = nullptr;
+ }
+}
+
+void AudioBufferMemoryTracker::RegisterAudioBufferInternal(
+ const AudioBuffer* aAudioBuffer) {
+ sMutex.AssertCurrentThreadOwns();
+ mBuffers.Insert(aAudioBuffer);
+}
+
+uint32_t AudioBufferMemoryTracker::UnregisterAudioBufferInternal(
+ const AudioBuffer* aAudioBuffer) {
+ sMutex.AssertCurrentThreadOwns();
+ mBuffers.Remove(aAudioBuffer);
+ return mBuffers.Count();
+}
+
+MOZ_DEFINE_MALLOC_SIZE_OF(AudioBufferMemoryTrackerMallocSizeOf)
+
+NS_IMETHODIMP
+AudioBufferMemoryTracker::CollectReports(nsIHandleReportCallback* aHandleReport,
+ nsISupports* aData, bool) {
+ StaticMutexAutoLock lock(sMutex);
+ const size_t amount =
+ std::accumulate(mBuffers.cbegin(), mBuffers.cend(), size_t(0),
+ [](size_t val, const AudioBuffer* buffer) {
+ return val + buffer->SizeOfIncludingThis(
+ AudioBufferMemoryTrackerMallocSizeOf);
+ });
+
+ MOZ_COLLECT_REPORT("explicit/webaudio/audiobuffer", KIND_HEAP, UNITS_BYTES,
+ amount, "Memory used by AudioBuffer objects (Web Audio).");
+
+ return NS_OK;
+}
+
+AudioBuffer::AudioBuffer(nsPIDOMWindowInner* aWindow,
+ uint32_t aNumberOfChannels, uint32_t aLength,
+ float aSampleRate, ErrorResult& aRv)
+ : mOwnerWindow(do_GetWeakReference(aWindow)), mSampleRate(aSampleRate) {
+ // Note that a buffer with zero channels is permitted here for the sake of
+ // AudioProcessingEvent, where channel counts must match parameters passed
+ // to createScriptProcessor(), one of which may be zero.
+ if (aSampleRate < WebAudioUtils::MinSampleRate ||
+ aSampleRate > WebAudioUtils::MaxSampleRate) {
+ aRv.ThrowNotSupportedError(
+ nsPrintfCString("Sample rate (%g) is out of range", aSampleRate));
+ return;
+ }
+
+ if (aNumberOfChannels > WebAudioUtils::MaxChannelCount) {
+ aRv.ThrowNotSupportedError(nsPrintfCString(
+ "Number of channels (%u) is out of range", aNumberOfChannels));
+ return;
+ }
+
+ if (!aLength || aLength > INT32_MAX) {
+ aRv.ThrowNotSupportedError(
+ nsPrintfCString("Length (%u) is out of range", aLength));
+ return;
+ }
+
+ mSharedChannels.mDuration = aLength;
+ mJSChannels.SetLength(aNumberOfChannels);
+ mozilla::HoldJSObjects(this);
+ AudioBufferMemoryTracker::RegisterAudioBuffer(this);
+}
+
+AudioBuffer::~AudioBuffer() {
+ AudioBufferMemoryTracker::UnregisterAudioBuffer(this);
+ ClearJSChannels();
+ mozilla::DropJSObjects(this);
+}
+
+/* static */
+already_AddRefed<AudioBuffer> AudioBuffer::Constructor(
+ const GlobalObject& aGlobal, const AudioBufferOptions& aOptions,
+ ErrorResult& aRv) {
+ if (!aOptions.mNumberOfChannels) {
+ aRv.ThrowNotSupportedError("Must have nonzero number of channels");
+ return nullptr;
+ }
+
+ nsCOMPtr<nsPIDOMWindowInner> window =
+ do_QueryInterface(aGlobal.GetAsSupports());
+
+ return Create(window, aOptions.mNumberOfChannels, aOptions.mLength,
+ aOptions.mSampleRate, aRv);
+}
+
+void AudioBuffer::ClearJSChannels() { mJSChannels.Clear(); }
+
+void AudioBuffer::SetSharedChannels(
+ already_AddRefed<ThreadSharedFloatArrayBufferList> aBuffer) {
+ RefPtr<ThreadSharedFloatArrayBufferList> buffer = aBuffer;
+ uint32_t channelCount = buffer->GetChannels();
+ mSharedChannels.mChannelData.SetLength(channelCount);
+ for (uint32_t i = 0; i < channelCount; ++i) {
+ mSharedChannels.mChannelData[i] = buffer->GetData(i);
+ }
+ mSharedChannels.mBuffer = std::move(buffer);
+ mSharedChannels.mBufferFormat = AUDIO_FORMAT_FLOAT32;
+}
+
+/* static */
+already_AddRefed<AudioBuffer> AudioBuffer::Create(
+ nsPIDOMWindowInner* aWindow, uint32_t aNumberOfChannels, uint32_t aLength,
+ float aSampleRate,
+ already_AddRefed<ThreadSharedFloatArrayBufferList> aInitialContents,
+ ErrorResult& aRv) {
+ RefPtr<ThreadSharedFloatArrayBufferList> initialContents = aInitialContents;
+ RefPtr<AudioBuffer> buffer =
+ new AudioBuffer(aWindow, aNumberOfChannels, aLength, aSampleRate, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ if (initialContents) {
+ MOZ_ASSERT(initialContents->GetChannels() == aNumberOfChannels);
+ buffer->SetSharedChannels(initialContents.forget());
+ }
+
+ return buffer.forget();
+}
+
+/* static */
+already_AddRefed<AudioBuffer> AudioBuffer::Create(
+ nsPIDOMWindowInner* aWindow, float aSampleRate,
+ AudioChunk&& aInitialContents) {
+ AudioChunk initialContents = aInitialContents;
+ ErrorResult rv;
+ RefPtr<AudioBuffer> buffer =
+ new AudioBuffer(aWindow, initialContents.ChannelCount(),
+ initialContents.mDuration, aSampleRate, rv);
+ if (rv.Failed()) {
+ return nullptr;
+ }
+ buffer->mSharedChannels = std::move(aInitialContents);
+
+ return buffer.forget();
+}
+
+JSObject* AudioBuffer::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return AudioBuffer_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+static void CopyChannelDataToFloat(const AudioChunk& aChunk, uint32_t aChannel,
+ uint32_t aSrcOffset, float* aOutput,
+ uint32_t aLength) {
+ MOZ_ASSERT(aChunk.mVolume == 1.0f);
+ if (aChunk.mBufferFormat == AUDIO_FORMAT_FLOAT32) {
+ mozilla::PodCopy(
+ aOutput, aChunk.ChannelData<float>()[aChannel] + aSrcOffset, aLength);
+ } else {
+ MOZ_ASSERT(aChunk.mBufferFormat == AUDIO_FORMAT_S16);
+ ConvertAudioSamples(aChunk.ChannelData<int16_t>()[aChannel] + aSrcOffset,
+ aOutput, aLength);
+ }
+}
+
+bool AudioBuffer::RestoreJSChannelData(JSContext* aJSContext) {
+ nsPIDOMWindowInner* global = GetParentObject();
+ if (!global || !global->AsGlobal()->HasJSGlobal()) {
+ return false;
+ }
+
+ JSAutoRealm ar(aJSContext, global->AsGlobal()->GetGlobalJSObject());
+
+ for (uint32_t i = 0; i < mJSChannels.Length(); ++i) {
+ if (mJSChannels[i]) {
+ // Already have data in JS array.
+ continue;
+ }
+
+ // The following code first zeroes the array and then copies our data
+ // into it. We could avoid this with additional JS APIs to construct
+ // an array (or ArrayBuffer) containing initial data.
+ JS::Rooted<JSObject*> array(aJSContext,
+ JS_NewFloat32Array(aJSContext, Length()));
+ if (!array) {
+ return false;
+ }
+ if (!mSharedChannels.IsNull()) {
+ // "4. Attach ArrayBuffers containing copies of the data to the
+ // AudioBuffer, to be returned by the next call to getChannelData."
+ JS::AutoCheckCannotGC nogc;
+ bool isShared;
+ float* jsData = JS_GetFloat32ArrayData(array, &isShared, nogc);
+ MOZ_ASSERT(!isShared); // Was created as unshared above
+ CopyChannelDataToFloat(mSharedChannels, i, 0, jsData, Length());
+ }
+ mJSChannels[i] = array;
+ }
+
+ mSharedChannels.SetNull(Length());
+
+ return true;
+}
+
+void AudioBuffer::CopyFromChannel(const Float32Array& aDestination,
+ uint32_t aChannelNumber,
+ uint32_t aBufferOffset, ErrorResult& aRv) {
+ if (aChannelNumber >= NumberOfChannels()) {
+ aRv.ThrowIndexSizeError(
+ nsPrintfCString("Channel number (%u) is out of range", aChannelNumber));
+ return;
+ }
+ uint32_t length = Length();
+ if (aBufferOffset >= length) {
+ return;
+ }
+ JS::AutoCheckCannotGC nogc;
+ aDestination.ComputeState();
+ uint32_t count = std::min(length - aBufferOffset, aDestination.Length());
+
+ JSObject* channelArray = mJSChannels[aChannelNumber];
+ if (channelArray) {
+ if (JS_GetTypedArrayLength(channelArray) != length) {
+ // The array's buffer was detached.
+ return;
+ }
+ bool isShared = false;
+ const float* sourceData =
+ JS_GetFloat32ArrayData(channelArray, &isShared, nogc);
+ // The sourceData arrays should all have originated in
+ // RestoreJSChannelData, where they are created unshared.
+ MOZ_ASSERT(!isShared);
+ PodMove(aDestination.Data(), sourceData + aBufferOffset, count);
+ return;
+ }
+
+ if (!mSharedChannels.IsNull()) {
+ CopyChannelDataToFloat(mSharedChannels, aChannelNumber, aBufferOffset,
+ aDestination.Data(), count);
+ return;
+ }
+
+ PodZero(aDestination.Data(), count);
+}
+
+void AudioBuffer::CopyToChannel(JSContext* aJSContext,
+ const Float32Array& aSource,
+ uint32_t aChannelNumber, uint32_t aBufferOffset,
+ ErrorResult& aRv) {
+ if (aChannelNumber >= NumberOfChannels()) {
+ aRv.ThrowIndexSizeError(
+ nsPrintfCString("Channel number (%u) is out of range", aChannelNumber));
+ return;
+ }
+
+ if (!RestoreJSChannelData(aJSContext)) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ JS::AutoCheckCannotGC nogc;
+ JSObject* channelArray = mJSChannels[aChannelNumber];
+ // This may differ from Length() if the buffer has been detached.
+ uint32_t length = JS_GetTypedArrayLength(channelArray);
+ if (aBufferOffset >= length) {
+ return;
+ }
+
+ aSource.ComputeState();
+ uint32_t count = std::min(length - aBufferOffset, aSource.Length());
+ bool isShared = false;
+ float* channelData = JS_GetFloat32ArrayData(channelArray, &isShared, nogc);
+ // The channelData arrays should all have originated in
+ // RestoreJSChannelData, where they are created unshared.
+ MOZ_ASSERT(!isShared);
+ PodMove(channelData + aBufferOffset, aSource.Data(), count);
+}
+
+void AudioBuffer::GetChannelData(JSContext* aJSContext, uint32_t aChannel,
+ JS::MutableHandle<JSObject*> aRetval,
+ ErrorResult& aRv) {
+ if (aChannel >= NumberOfChannels()) {
+ aRv.ThrowIndexSizeError(
+ nsPrintfCString("Channel number (%u) is out of range", aChannel));
+ return;
+ }
+
+ if (!RestoreJSChannelData(aJSContext)) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ aRetval.set(mJSChannels[aChannel]);
+}
+
+already_AddRefed<ThreadSharedFloatArrayBufferList>
+AudioBuffer::StealJSArrayDataIntoSharedChannels(JSContext* aJSContext) {
+ nsPIDOMWindowInner* global = GetParentObject();
+ if (!global || !global->AsGlobal()->HasJSGlobal()) {
+ return nullptr;
+ }
+
+ JSAutoRealm ar(aJSContext, global->AsGlobal()->GetGlobalJSObject());
+
+ // "1. If any of the AudioBuffer's ArrayBuffer have been detached, abort
+ // these steps, and return a zero-length channel data buffers to the
+ // invoker."
+ for (uint32_t i = 0; i < mJSChannels.Length(); ++i) {
+ JSObject* channelArray = mJSChannels[i];
+ if (!channelArray || Length() != JS_GetTypedArrayLength(channelArray)) {
+ // Either empty buffer or one of the arrays' buffers was detached.
+ return nullptr;
+ }
+ }
+
+ // "2. Detach all ArrayBuffers for arrays previously returned by
+ // getChannelData on this AudioBuffer."
+ // "3. Retain the underlying data buffers from those ArrayBuffers and return
+ // references to them to the invoker."
+ RefPtr<ThreadSharedFloatArrayBufferList> result =
+ new ThreadSharedFloatArrayBufferList(mJSChannels.Length());
+ for (uint32_t i = 0; i < mJSChannels.Length(); ++i) {
+ JS::Rooted<JSObject*> arrayBufferView(aJSContext, mJSChannels[i]);
+ bool isSharedMemory;
+ JS::Rooted<JSObject*> arrayBuffer(
+ aJSContext, JS_GetArrayBufferViewBuffer(aJSContext, arrayBufferView,
+ &isSharedMemory));
+ // The channel data arrays should all have originated in
+ // RestoreJSChannelData, where they are created unshared.
+ MOZ_ASSERT(!isSharedMemory);
+ auto stolenData = arrayBuffer
+ ? static_cast<float*>(JS::StealArrayBufferContents(
+ aJSContext, arrayBuffer))
+ : nullptr;
+ if (stolenData) {
+ result->SetData(i, stolenData, js_free, stolenData);
+ } else {
+ NS_ASSERTION(i == 0, "some channels lost when contents not acquired");
+ return nullptr;
+ }
+ }
+
+ for (uint32_t i = 0; i < mJSChannels.Length(); ++i) {
+ mJSChannels[i] = nullptr;
+ }
+
+ return result.forget();
+}
+
+const AudioChunk& AudioBuffer::GetThreadSharedChannelsForRate(
+ JSContext* aJSContext) {
+ if (mSharedChannels.IsNull()) {
+ // mDuration is set in constructor
+ RefPtr<ThreadSharedFloatArrayBufferList> buffer =
+ StealJSArrayDataIntoSharedChannels(aJSContext);
+
+ if (buffer) {
+ SetSharedChannels(buffer.forget());
+ }
+ }
+
+ return mSharedChannels;
+}
+
+size_t AudioBuffer::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t amount = aMallocSizeOf(this);
+ amount += mJSChannels.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ amount += mSharedChannels.SizeOfExcludingThis(aMallocSizeOf, false);
+ return amount;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/AudioBuffer.h b/dom/media/webaudio/AudioBuffer.h
new file mode 100644
index 0000000000..e313c5c944
--- /dev/null
+++ b/dom/media/webaudio/AudioBuffer.h
@@ -0,0 +1,138 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef AudioBuffer_h_
+#define AudioBuffer_h_
+
+#include "AudioSegment.h"
+#include "nsWrapperCache.h"
+#include "nsCycleCollectionParticipant.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/StaticMutex.h"
+#include "nsTArray.h"
+#include "js/TypeDecls.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/dom/TypedArray.h"
+#include "nsPIDOMWindow.h"
+#include "nsIWeakReferenceUtils.h"
+
+namespace mozilla {
+
+class ErrorResult;
+class ThreadSharedFloatArrayBufferList;
+
+namespace dom {
+
+struct AudioBufferOptions;
+
+/**
+ * An AudioBuffer keeps its data either in the mJSChannels objects, which
+ * are Float32Arrays, or in mSharedChannels if the mJSChannels objects' buffers
+ * are detached.
+ */
+class AudioBuffer final : public nsWrapperCache {
+ public:
+ // If non-null, aInitialContents must have number of channels equal to
+ // aNumberOfChannels and their lengths must be at least aLength.
+ static already_AddRefed<AudioBuffer> Create(
+ nsPIDOMWindowInner* aWindow, uint32_t aNumberOfChannels, uint32_t aLength,
+ float aSampleRate,
+ already_AddRefed<ThreadSharedFloatArrayBufferList> aInitialContents,
+ ErrorResult& aRv);
+
+ static already_AddRefed<AudioBuffer> Create(nsPIDOMWindowInner* aWindow,
+ uint32_t aNumberOfChannels,
+ uint32_t aLength,
+ float aSampleRate,
+ ErrorResult& aRv) {
+ return Create(aWindow, aNumberOfChannels, aLength, aSampleRate, nullptr,
+ aRv);
+ }
+
+ // Non-unit AudioChunk::mVolume is not supported
+ static already_AddRefed<AudioBuffer> Create(nsPIDOMWindowInner* aWindow,
+ float aSampleRate,
+ AudioChunk&& aInitialContents);
+
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(AudioBuffer)
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_NATIVE_CLASS(AudioBuffer)
+
+ static already_AddRefed<AudioBuffer> Constructor(
+ const GlobalObject& aGlobal, const AudioBufferOptions& aOptions,
+ ErrorResult& aRv);
+
+ nsPIDOMWindowInner* GetParentObject() const {
+ nsCOMPtr<nsPIDOMWindowInner> parentObject = do_QueryReferent(mOwnerWindow);
+ return parentObject;
+ }
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ float SampleRate() const { return mSampleRate; }
+
+ uint32_t Length() const { return mSharedChannels.mDuration; }
+
+ double Duration() const {
+ return Length() / static_cast<double>(mSampleRate);
+ }
+
+ uint32_t NumberOfChannels() const { return mJSChannels.Length(); }
+
+ /**
+ * If mSharedChannels is non-null, copies its contents to
+ * new Float32Arrays in mJSChannels. Returns a Float32Array.
+ */
+ void GetChannelData(JSContext* aJSContext, uint32_t aChannel,
+ JS::MutableHandle<JSObject*> aRetval, ErrorResult& aRv);
+
+ void CopyFromChannel(const Float32Array& aDestination,
+ uint32_t aChannelNumber, uint32_t aBufferOffset,
+ ErrorResult& aRv);
+ void CopyToChannel(JSContext* aJSContext, const Float32Array& aSource,
+ uint32_t aChannelNumber, uint32_t aBufferOffset,
+ ErrorResult& aRv);
+
+ /**
+ * Returns a reference to an AudioChunk containing the sample data.
+ * The AudioChunk can have a null buffer if there is no data.
+ */
+ const AudioChunk& GetThreadSharedChannelsForRate(JSContext* aContext);
+
+ protected:
+ AudioBuffer(nsPIDOMWindowInner* aWindow, uint32_t aNumberOfChannels,
+ uint32_t aLength, float aSampleRate, ErrorResult& aRv);
+ ~AudioBuffer();
+
+ void SetSharedChannels(
+ already_AddRefed<ThreadSharedFloatArrayBufferList> aBuffer);
+
+ bool RestoreJSChannelData(JSContext* aJSContext);
+
+ already_AddRefed<ThreadSharedFloatArrayBufferList>
+ StealJSArrayDataIntoSharedChannels(JSContext* aJSContext);
+
+ void ClearJSChannels();
+
+ // Float32Arrays
+ AutoTArray<JS::Heap<JSObject*>, 2> mJSChannels;
+ // mSharedChannels aggregates the data from mJSChannels. This is non-null
+ // if and only if the mJSChannels' buffers are detached, but its mDuration
+ // member keeps the buffer length regardless of whether the buffer is
+ // provided by mJSChannels or mSharedChannels.
+ AudioChunk mSharedChannels;
+
+ nsWeakPtr mOwnerWindow;
+ float mSampleRate;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webaudio/AudioBufferSourceNode.cpp b/dom/media/webaudio/AudioBufferSourceNode.cpp
new file mode 100644
index 0000000000..38e2ebfa96
--- /dev/null
+++ b/dom/media/webaudio/AudioBufferSourceNode.cpp
@@ -0,0 +1,845 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioBufferSourceNode.h"
+#include "nsDebug.h"
+#include "mozilla/dom/AudioBufferSourceNodeBinding.h"
+#include "mozilla/dom/AudioParam.h"
+#include "mozilla/FloatingPoint.h"
+#include "nsContentUtils.h"
+#include "nsMathUtils.h"
+#include "AlignmentUtils.h"
+#include "AudioNodeEngine.h"
+#include "AudioNodeTrack.h"
+#include "AudioDestinationNode.h"
+#include "AudioParamTimeline.h"
+#include <limits>
+#include <algorithm>
+#include "Tracing.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(AudioBufferSourceNode,
+ AudioScheduledSourceNode, mBuffer,
+ mPlaybackRate, mDetune)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AudioBufferSourceNode)
+NS_INTERFACE_MAP_END_INHERITING(AudioScheduledSourceNode)
+
+NS_IMPL_ADDREF_INHERITED(AudioBufferSourceNode, AudioScheduledSourceNode)
+NS_IMPL_RELEASE_INHERITED(AudioBufferSourceNode, AudioScheduledSourceNode)
+
+/**
+ * Media-thread playback engine for AudioBufferSourceNode.
+ * Nothing is played until a non-null buffer has been set (via
+ * AudioNodeTrack::SetBuffer) and a non-zero mBufferSampleRate has been set
+ * (via AudioNodeTrack::SetInt32Parameter)
+ */
+class AudioBufferSourceNodeEngine final : public AudioNodeEngine {
+ public:
+ AudioBufferSourceNodeEngine(AudioNode* aNode,
+ AudioDestinationNode* aDestination)
+ : AudioNodeEngine(aNode),
+ mStart(0.0),
+ mBeginProcessing(0),
+ mStop(TRACK_TIME_MAX),
+ mResampler(nullptr),
+ mRemainingResamplerTail(0),
+ mRemainingFrames(TRACK_TICKS_MAX),
+ mLoopStart(0),
+ mLoopEnd(0),
+ mBufferPosition(0),
+ mBufferSampleRate(0),
+ // mResamplerOutRate is initialized in UpdateResampler().
+ mChannels(0),
+ mDestination(aDestination->Track()),
+ mPlaybackRateTimeline(1.0f),
+ mDetuneTimeline(0.0f),
+ mLoop(false) {}
+
+ ~AudioBufferSourceNodeEngine() {
+ if (mResampler) {
+ speex_resampler_destroy(mResampler);
+ }
+ }
+
+ void SetSourceTrack(AudioNodeTrack* aSource) { mSource = aSource; }
+
+ void RecvTimelineEvent(uint32_t aIndex,
+ dom::AudioTimelineEvent& aEvent) override {
+ MOZ_ASSERT(mDestination);
+ WebAudioUtils::ConvertAudioTimelineEventToTicks(aEvent, mDestination);
+
+ switch (aIndex) {
+ case AudioBufferSourceNode::PLAYBACKRATE:
+ mPlaybackRateTimeline.InsertEvent<int64_t>(aEvent);
+ break;
+ case AudioBufferSourceNode::DETUNE:
+ mDetuneTimeline.InsertEvent<int64_t>(aEvent);
+ break;
+ default:
+ NS_ERROR("Bad AudioBufferSourceNodeEngine TimelineParameter");
+ }
+ }
+ void SetTrackTimeParameter(uint32_t aIndex, TrackTime aParam) override {
+ switch (aIndex) {
+ case AudioBufferSourceNode::STOP:
+ mStop = aParam;
+ break;
+ default:
+ NS_ERROR("Bad AudioBufferSourceNodeEngine TrackTimeParameter");
+ }
+ }
+ void SetDoubleParameter(uint32_t aIndex, double aParam) override {
+ switch (aIndex) {
+ case AudioBufferSourceNode::START:
+ MOZ_ASSERT(!mStart, "Another START?");
+ mStart = aParam * mDestination->mSampleRate;
+ // Round to nearest
+ mBeginProcessing = llround(mStart);
+ break;
+ case AudioBufferSourceNode::DURATION:
+ MOZ_ASSERT(aParam >= 0);
+ mRemainingFrames = llround(aParam * mBufferSampleRate);
+ break;
+ default:
+ NS_ERROR("Bad AudioBufferSourceNodeEngine double parameter.");
+ };
+ }
+ void SetInt32Parameter(uint32_t aIndex, int32_t aParam) override {
+ switch (aIndex) {
+ case AudioBufferSourceNode::SAMPLE_RATE:
+ MOZ_ASSERT(aParam > 0);
+ mBufferSampleRate = aParam;
+ mSource->SetActive();
+ break;
+ case AudioBufferSourceNode::BUFFERSTART:
+ MOZ_ASSERT(aParam >= 0);
+ if (mBufferPosition == 0) {
+ mBufferPosition = aParam;
+ }
+ break;
+ case AudioBufferSourceNode::LOOP:
+ mLoop = !!aParam;
+ break;
+ case AudioBufferSourceNode::LOOPSTART:
+ MOZ_ASSERT(aParam >= 0);
+ mLoopStart = aParam;
+ break;
+ case AudioBufferSourceNode::LOOPEND:
+ MOZ_ASSERT(aParam >= 0);
+ mLoopEnd = aParam;
+ break;
+ default:
+ NS_ERROR("Bad AudioBufferSourceNodeEngine Int32Parameter");
+ }
+ }
+ void SetBuffer(AudioChunk&& aBuffer) override { mBuffer = aBuffer; }
+
+ bool BegunResampling() { return mBeginProcessing == -TRACK_TIME_MAX; }
+
+ void UpdateResampler(int32_t aOutRate, uint32_t aChannels) {
+ if (mResampler &&
+ (aChannels != mChannels ||
+ // If the resampler has begun, then it will have moved
+ // mBufferPosition to after the samples it has read, but it hasn't
+ // output its buffered samples. Keep using the resampler, even if
+ // the rates now match, so that this latent segment is output.
+ (aOutRate == mBufferSampleRate && !BegunResampling()))) {
+ speex_resampler_destroy(mResampler);
+ mResampler = nullptr;
+ mRemainingResamplerTail = 0;
+ mBeginProcessing = llround(mStart);
+ }
+
+ if (aChannels == 0 || (aOutRate == mBufferSampleRate && !mResampler)) {
+ mResamplerOutRate = aOutRate;
+ return;
+ }
+
+ if (!mResampler) {
+ mChannels = aChannels;
+ mResampler = speex_resampler_init(mChannels, mBufferSampleRate, aOutRate,
+ SPEEX_RESAMPLER_QUALITY_MIN, nullptr);
+ } else {
+ if (mResamplerOutRate == aOutRate) {
+ return;
+ }
+ if (speex_resampler_set_rate(mResampler, mBufferSampleRate, aOutRate) !=
+ RESAMPLER_ERR_SUCCESS) {
+ NS_ASSERTION(false, "speex_resampler_set_rate failed");
+ return;
+ }
+ }
+
+ mResamplerOutRate = aOutRate;
+
+ if (!BegunResampling()) {
+ // Low pass filter effects from the resampler mean that samples before
+ // the start time are influenced by resampling the buffer. The input
+ // latency indicates half the filter width.
+ int64_t inputLatency = speex_resampler_get_input_latency(mResampler);
+ uint32_t ratioNum, ratioDen;
+ speex_resampler_get_ratio(mResampler, &ratioNum, &ratioDen);
+ // The output subsample resolution supported in aligning the resampler
+ // is ratioNum. First round the start time to the nearest subsample.
+ int64_t subsample = llround(mStart * ratioNum);
+ // Now include the leading effects of the filter, and round *up* to the
+ // next whole tick, because there is no effect on samples outside the
+ // filter width.
+ mBeginProcessing =
+ (subsample - inputLatency * ratioDen + ratioNum - 1) / ratioNum;
+ }
+ }
+
+ // Borrow a full buffer of size WEBAUDIO_BLOCK_SIZE from the source buffer
+ // at offset aSourceOffset. This avoids copying memory.
+ void BorrowFromInputBuffer(AudioBlock* aOutput, uint32_t aChannels) {
+ aOutput->SetBuffer(mBuffer.mBuffer);
+ aOutput->mChannelData.SetLength(aChannels);
+ for (uint32_t i = 0; i < aChannels; ++i) {
+ aOutput->mChannelData[i] =
+ mBuffer.ChannelData<float>()[i] + mBufferPosition;
+ }
+ aOutput->mVolume = mBuffer.mVolume;
+ aOutput->mBufferFormat = AUDIO_FORMAT_FLOAT32;
+ }
+
+ // Copy aNumberOfFrames frames from the source buffer at offset aSourceOffset
+ // and put it at offset aBufferOffset in the destination buffer.
+ template <typename T>
+ void CopyFromInputBuffer(AudioBlock* aOutput, uint32_t aChannels,
+ uintptr_t aOffsetWithinBlock,
+ uint32_t aNumberOfFrames) {
+ MOZ_ASSERT(mBuffer.mVolume == 1.0f);
+ for (uint32_t i = 0; i < aChannels; ++i) {
+ float* baseChannelData = aOutput->ChannelFloatsForWrite(i);
+ ConvertAudioSamples(mBuffer.ChannelData<T>()[i] + mBufferPosition,
+ baseChannelData + aOffsetWithinBlock,
+ aNumberOfFrames);
+ }
+ }
+
+ // Resamples input data to an output buffer, according to |mBufferSampleRate|
+ // and the playbackRate/detune. The number of frames consumed/produced depends
+ // on the amount of space remaining in both the input and output buffer, and
+ // the playback rate (that is, the ratio between the output samplerate and the
+ // input samplerate).
+ void CopyFromInputBufferWithResampling(AudioBlock* aOutput,
+ uint32_t aChannels,
+ uint32_t* aOffsetWithinBlock,
+ uint32_t aAvailableInOutput,
+ TrackTime* aCurrentPosition,
+ uint32_t aBufferMax) {
+ if (*aOffsetWithinBlock == 0) {
+ aOutput->AllocateChannels(aChannels);
+ }
+ SpeexResamplerState* resampler = mResampler;
+ MOZ_ASSERT(aChannels > 0);
+
+ if (mBufferPosition < aBufferMax) {
+ uint32_t availableInInputBuffer = aBufferMax - mBufferPosition;
+ uint32_t ratioNum, ratioDen;
+ speex_resampler_get_ratio(resampler, &ratioNum, &ratioDen);
+ // Limit the number of input samples copied and possibly
+ // format-converted for resampling by estimating how many will be used.
+ // This may be a little small if still filling the resampler with
+ // initial data, but we'll get called again and it will work out.
+ uint32_t inputLimit = aAvailableInOutput * ratioNum / ratioDen + 10;
+ if (!BegunResampling()) {
+ // First time the resampler is used.
+ uint32_t inputLatency = speex_resampler_get_input_latency(resampler);
+ inputLimit += inputLatency;
+ // If starting after mStart, then play from the beginning of the
+ // buffer, but correct for input latency. If starting before mStart,
+ // then align the resampler so that the time corresponding to the
+ // first input sample is mStart.
+ int64_t skipFracNum = static_cast<int64_t>(inputLatency) * ratioDen;
+ double leadTicks = mStart - *aCurrentPosition;
+ if (leadTicks > 0.0) {
+ // Round to nearest output subsample supported by the resampler at
+ // these rates.
+ int64_t leadSubsamples = llround(leadTicks * ratioNum);
+ MOZ_ASSERT(leadSubsamples <= skipFracNum,
+ "mBeginProcessing is wrong?");
+ skipFracNum -= leadSubsamples;
+ }
+ speex_resampler_set_skip_frac_num(
+ resampler, std::min<int64_t>(skipFracNum, UINT32_MAX));
+
+ mBeginProcessing = -TRACK_TIME_MAX;
+ }
+ inputLimit = std::min(inputLimit, availableInInputBuffer);
+
+ MOZ_ASSERT(mBuffer.mVolume == 1.0f);
+ for (uint32_t i = 0; true;) {
+ uint32_t inSamples = inputLimit;
+
+ uint32_t outSamples = aAvailableInOutput;
+ float* outputData =
+ aOutput->ChannelFloatsForWrite(i) + *aOffsetWithinBlock;
+
+ if (mBuffer.mBufferFormat == AUDIO_FORMAT_FLOAT32) {
+ const float* inputData =
+ mBuffer.ChannelData<float>()[i] + mBufferPosition;
+ WebAudioUtils::SpeexResamplerProcess(
+ resampler, i, inputData, &inSamples, outputData, &outSamples);
+ } else {
+ MOZ_ASSERT(mBuffer.mBufferFormat == AUDIO_FORMAT_S16);
+ const int16_t* inputData =
+ mBuffer.ChannelData<int16_t>()[i] + mBufferPosition;
+ WebAudioUtils::SpeexResamplerProcess(
+ resampler, i, inputData, &inSamples, outputData, &outSamples);
+ }
+ if (++i == aChannels) {
+ mBufferPosition += inSamples;
+ mRemainingFrames -= inSamples;
+ MOZ_ASSERT(mBufferPosition <= mBuffer.GetDuration());
+ MOZ_ASSERT(mRemainingFrames >= 0);
+ *aOffsetWithinBlock += outSamples;
+ *aCurrentPosition += outSamples;
+ if ((!mLoop && inSamples == availableInInputBuffer) ||
+ mRemainingFrames == 0) {
+ // We'll feed in enough zeros to empty out the resampler's memory.
+ // This handles the output latency as well as capturing the low
+ // pass effects of the resample filter.
+ mRemainingResamplerTail =
+ 2 * speex_resampler_get_input_latency(resampler) - 1;
+ }
+ return;
+ }
+ }
+ } else {
+ for (uint32_t i = 0; true;) {
+ uint32_t inSamples = mRemainingResamplerTail;
+ uint32_t outSamples = aAvailableInOutput;
+ float* outputData =
+ aOutput->ChannelFloatsForWrite(i) + *aOffsetWithinBlock;
+
+ // AudioDataValue* for aIn selects the function that does not try to
+ // copy and format-convert input data.
+ WebAudioUtils::SpeexResamplerProcess(
+ resampler, i, static_cast<AudioDataValue*>(nullptr), &inSamples,
+ outputData, &outSamples);
+ if (++i == aChannels) {
+ MOZ_ASSERT(inSamples <= mRemainingResamplerTail);
+ mRemainingResamplerTail -= inSamples;
+ *aOffsetWithinBlock += outSamples;
+ *aCurrentPosition += outSamples;
+ break;
+ }
+ }
+ }
+ }
+
+ /**
+ * Fill aOutput with as many zero frames as we can, and advance
+ * aOffsetWithinBlock and aCurrentPosition based on how many frames we write.
+ * This will never advance aOffsetWithinBlock past WEBAUDIO_BLOCK_SIZE or
+ * aCurrentPosition past aMaxPos. This function knows when it needs to
+ * allocate the output buffer, and also optimizes the case where it can avoid
+ * memory allocations.
+ */
+ void FillWithZeroes(AudioBlock* aOutput, uint32_t aChannels,
+ uint32_t* aOffsetWithinBlock, TrackTime* aCurrentPosition,
+ TrackTime aMaxPos) {
+ MOZ_ASSERT(*aCurrentPosition < aMaxPos);
+ uint32_t numFrames = std::min<TrackTime>(
+ WEBAUDIO_BLOCK_SIZE - *aOffsetWithinBlock, aMaxPos - *aCurrentPosition);
+ if (numFrames == WEBAUDIO_BLOCK_SIZE || !aChannels) {
+ aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
+ } else {
+ if (*aOffsetWithinBlock == 0) {
+ aOutput->AllocateChannels(aChannels);
+ }
+ WriteZeroesToAudioBlock(aOutput, *aOffsetWithinBlock, numFrames);
+ }
+ *aOffsetWithinBlock += numFrames;
+ *aCurrentPosition += numFrames;
+ }
+
+ /**
+ * Copy as many frames as possible from the source buffer to aOutput, and
+ * advance aOffsetWithinBlock and aCurrentPosition based on how many frames
+ * we write. This will never advance aOffsetWithinBlock past
+ * WEBAUDIO_BLOCK_SIZE, or aCurrentPosition past mStop. It takes data from
+ * the buffer at aBufferOffset, and never takes more data than aBufferMax.
+ * This function knows when it needs to allocate the output buffer, and also
+ * optimizes the case where it can avoid memory allocations.
+ */
+ void CopyFromBuffer(AudioBlock* aOutput, uint32_t aChannels,
+ uint32_t* aOffsetWithinBlock, TrackTime* aCurrentPosition,
+ uint32_t aBufferMax) {
+ MOZ_ASSERT(*aCurrentPosition < mStop);
+ uint32_t availableInOutput = std::min<TrackTime>(
+ WEBAUDIO_BLOCK_SIZE - *aOffsetWithinBlock, mStop - *aCurrentPosition);
+ if (mResampler) {
+ CopyFromInputBufferWithResampling(aOutput, aChannels, aOffsetWithinBlock,
+ availableInOutput, aCurrentPosition,
+ aBufferMax);
+ return;
+ }
+
+ if (aChannels == 0) {
+ aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
+ // There is no attempt here to limit advance so that mBufferPosition is
+ // limited to aBufferMax. The only observable affect of skipping the
+ // check would be in the precise timing of the ended event if the loop
+ // attribute is reset after playback has looped.
+ *aOffsetWithinBlock += availableInOutput;
+ *aCurrentPosition += availableInOutput;
+ // Rounding at the start and end of the period means that fractional
+ // increments essentially accumulate if outRate remains constant. If
+ // outRate is varying, then accumulation happens on average but not
+ // precisely.
+ TrackTicks start =
+ *aCurrentPosition * mBufferSampleRate / mResamplerOutRate;
+ TrackTicks end = (*aCurrentPosition + availableInOutput) *
+ mBufferSampleRate / mResamplerOutRate;
+ mBufferPosition += end - start;
+ return;
+ }
+
+ uint32_t numFrames =
+ std::min(aBufferMax - mBufferPosition, availableInOutput);
+
+ bool shouldBorrow = false;
+ if (numFrames == WEBAUDIO_BLOCK_SIZE &&
+ mBuffer.mBufferFormat == AUDIO_FORMAT_FLOAT32) {
+ shouldBorrow = true;
+ for (uint32_t i = 0; i < aChannels; ++i) {
+ if (!IS_ALIGNED16(mBuffer.ChannelData<float>()[i] + mBufferPosition)) {
+ shouldBorrow = false;
+ break;
+ }
+ }
+ }
+ MOZ_ASSERT(mBufferPosition < aBufferMax);
+ if (shouldBorrow) {
+ BorrowFromInputBuffer(aOutput, aChannels);
+ } else {
+ if (*aOffsetWithinBlock == 0) {
+ aOutput->AllocateChannels(aChannels);
+ }
+ if (mBuffer.mBufferFormat == AUDIO_FORMAT_FLOAT32) {
+ CopyFromInputBuffer<float>(aOutput, aChannels, *aOffsetWithinBlock,
+ numFrames);
+ } else {
+ MOZ_ASSERT(mBuffer.mBufferFormat == AUDIO_FORMAT_S16);
+ CopyFromInputBuffer<int16_t>(aOutput, aChannels, *aOffsetWithinBlock,
+ numFrames);
+ }
+ }
+ *aOffsetWithinBlock += numFrames;
+ *aCurrentPosition += numFrames;
+ mBufferPosition += numFrames;
+ mRemainingFrames -= numFrames;
+ }
+
+ int32_t ComputeFinalOutSampleRate(float aPlaybackRate, float aDetune) {
+ float computedPlaybackRate = aPlaybackRate * exp2(aDetune / 1200.f);
+ // Make sure the playback rate is something our resampler can work with.
+ int32_t rate = WebAudioUtils::TruncateFloatToInt<int32_t>(
+ mSource->mSampleRate / computedPlaybackRate);
+ return rate ? rate : mBufferSampleRate;
+ }
+
+ void UpdateSampleRateIfNeeded(uint32_t aChannels, TrackTime aTrackPosition) {
+ float playbackRate;
+ float detune;
+
+ if (mPlaybackRateTimeline.HasSimpleValue()) {
+ playbackRate = mPlaybackRateTimeline.GetValue();
+ } else {
+ playbackRate = mPlaybackRateTimeline.GetValueAtTime(aTrackPosition);
+ }
+ if (mDetuneTimeline.HasSimpleValue()) {
+ detune = mDetuneTimeline.GetValue();
+ } else {
+ detune = mDetuneTimeline.GetValueAtTime(aTrackPosition);
+ }
+ if (playbackRate <= 0 || std::isnan(playbackRate)) {
+ playbackRate = 1.0f;
+ }
+
+ detune = std::min(std::max(-1200.f, detune), 1200.f);
+
+ int32_t outRate = ComputeFinalOutSampleRate(playbackRate, detune);
+ UpdateResampler(outRate, aChannels);
+ }
+
+ void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom,
+ const AudioBlock& aInput, AudioBlock* aOutput,
+ bool* aFinished) override {
+ TRACE("AudioBufferSourceNodeEngine::ProcessBlock");
+ if (mBufferSampleRate == 0) {
+ // start() has not yet been called or no buffer has yet been set
+ aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
+ return;
+ }
+
+ TrackTime streamPosition = mDestination->GraphTimeToTrackTime(aFrom);
+ uint32_t channels = mBuffer.ChannelCount();
+
+ UpdateSampleRateIfNeeded(channels, streamPosition);
+
+ uint32_t written = 0;
+ while (true) {
+ if ((mStop != TRACK_TIME_MAX && streamPosition >= mStop) ||
+ (!mRemainingResamplerTail &&
+ ((mBufferPosition >= mBuffer.GetDuration() && !mLoop) ||
+ mRemainingFrames <= 0))) {
+ if (written != WEBAUDIO_BLOCK_SIZE) {
+ FillWithZeroes(aOutput, channels, &written, &streamPosition,
+ TRACK_TIME_MAX);
+ }
+ *aFinished = true;
+ break;
+ }
+ if (written == WEBAUDIO_BLOCK_SIZE) {
+ break;
+ }
+ if (streamPosition < mBeginProcessing) {
+ FillWithZeroes(aOutput, channels, &written, &streamPosition,
+ mBeginProcessing);
+ continue;
+ }
+
+ TrackTicks bufferLeft;
+ if (mLoop) {
+ // mLoopEnd can become less than mBufferPosition when a LOOPEND engine
+ // parameter is received after "loopend" is changed on the node or a
+ // new buffer with lower samplerate is set.
+ if (mBufferPosition >= mLoopEnd) {
+ mBufferPosition = mLoopStart;
+ }
+ bufferLeft =
+ std::min<TrackTicks>(mRemainingFrames, mLoopEnd - mBufferPosition);
+ } else {
+ bufferLeft =
+ std::min(mRemainingFrames, mBuffer.GetDuration() - mBufferPosition);
+ }
+
+ CopyFromBuffer(aOutput, channels, &written, &streamPosition,
+ bufferLeft + mBufferPosition);
+ }
+ }
+
+ bool IsActive() const override {
+ // Whether buffer has been set and start() has been called.
+ return mBufferSampleRate != 0;
+ }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override {
+ // Not owned:
+ // - mBuffer - shared w/ AudioNode
+ // - mPlaybackRateTimeline - shared w/ AudioNode
+ // - mDetuneTimeline - shared w/ AudioNode
+
+ size_t amount = AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf);
+
+ // NB: We need to modify speex if we want the full memory picture, internal
+ // fields that need measuring noted below.
+ // - mResampler->mem
+ // - mResampler->sinc_table
+ // - mResampler->last_sample
+ // - mResampler->magic_samples
+ // - mResampler->samp_frac_num
+ amount += aMallocSizeOf(mResampler);
+
+ return amount;
+ }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ double mStart; // including the fractional position between ticks
+ // Low pass filter effects from the resampler mean that samples before the
+ // start time are influenced by resampling the buffer. mBeginProcessing
+ // includes the extent of this filter. The special value of -TRACK_TIME_MAX
+ // indicates that the resampler has begun processing.
+ TrackTime mBeginProcessing;
+ TrackTime mStop;
+ AudioChunk mBuffer;
+ SpeexResamplerState* mResampler;
+ // mRemainingResamplerTail, like mBufferPosition
+ // is measured in input buffer samples.
+ uint32_t mRemainingResamplerTail;
+ TrackTicks mRemainingFrames;
+ uint32_t mLoopStart;
+ uint32_t mLoopEnd;
+ uint32_t mBufferPosition;
+ int32_t mBufferSampleRate;
+ int32_t mResamplerOutRate;
+ uint32_t mChannels;
+ RefPtr<AudioNodeTrack> mDestination;
+
+ // mSource deletes the engine in its destructor.
+ AudioNodeTrack* MOZ_NON_OWNING_REF mSource;
+ AudioParamTimeline mPlaybackRateTimeline;
+ AudioParamTimeline mDetuneTimeline;
+ bool mLoop;
+};
+
+AudioBufferSourceNode::AudioBufferSourceNode(AudioContext* aContext)
+ : AudioScheduledSourceNode(aContext, 2, ChannelCountMode::Max,
+ ChannelInterpretation::Speakers),
+ mLoopStart(0.0),
+ mLoopEnd(0.0),
+ // mOffset and mDuration are initialized in Start().
+ mLoop(false),
+ mStartCalled(false),
+ mBufferSet(false) {
+ mPlaybackRate = CreateAudioParam(PLAYBACKRATE, u"playbackRate"_ns, 1.0f);
+ mDetune = CreateAudioParam(DETUNE, u"detune"_ns, 0.0f);
+ AudioBufferSourceNodeEngine* engine =
+ new AudioBufferSourceNodeEngine(this, aContext->Destination());
+ mTrack = AudioNodeTrack::Create(aContext, engine,
+ AudioNodeTrack::NEED_MAIN_THREAD_ENDED,
+ aContext->Graph());
+ engine->SetSourceTrack(mTrack);
+ mTrack->AddMainThreadListener(this);
+}
+
+/* static */
+already_AddRefed<AudioBufferSourceNode> AudioBufferSourceNode::Create(
+ JSContext* aCx, AudioContext& aAudioContext,
+ const AudioBufferSourceOptions& aOptions) {
+ RefPtr<AudioBufferSourceNode> audioNode =
+ new AudioBufferSourceNode(&aAudioContext);
+
+ if (aOptions.mBuffer.WasPassed()) {
+ ErrorResult ignored;
+ MOZ_ASSERT(aCx);
+ audioNode->SetBuffer(aCx, aOptions.mBuffer.Value(), ignored);
+ }
+
+ audioNode->Detune()->SetInitialValue(aOptions.mDetune);
+ audioNode->SetLoop(aOptions.mLoop);
+ audioNode->SetLoopEnd(aOptions.mLoopEnd);
+ audioNode->SetLoopStart(aOptions.mLoopStart);
+ audioNode->PlaybackRate()->SetInitialValue(aOptions.mPlaybackRate);
+
+ return audioNode.forget();
+}
+void AudioBufferSourceNode::DestroyMediaTrack() {
+ bool hadTrack = mTrack;
+ if (hadTrack) {
+ mTrack->RemoveMainThreadListener(this);
+ }
+ AudioNode::DestroyMediaTrack();
+}
+
+size_t AudioBufferSourceNode::SizeOfExcludingThis(
+ MallocSizeOf aMallocSizeOf) const {
+ size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf);
+
+ /* mBuffer can be shared and is accounted for separately. */
+
+ amount += mPlaybackRate->SizeOfIncludingThis(aMallocSizeOf);
+ amount += mDetune->SizeOfIncludingThis(aMallocSizeOf);
+ return amount;
+}
+
+size_t AudioBufferSourceNode::SizeOfIncludingThis(
+ MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
+
+JSObject* AudioBufferSourceNode::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return AudioBufferSourceNode_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void AudioBufferSourceNode::Start(double aWhen, double aOffset,
+ const Optional<double>& aDuration,
+ ErrorResult& aRv) {
+ if (!WebAudioUtils::IsTimeValid(aWhen)) {
+ aRv.ThrowRangeError<MSG_VALUE_OUT_OF_RANGE>("start time");
+ return;
+ }
+ if (aOffset < 0) {
+ aRv.ThrowRangeError<MSG_VALUE_OUT_OF_RANGE>("offset");
+ return;
+ }
+ if (aDuration.WasPassed() && !WebAudioUtils::IsTimeValid(aDuration.Value())) {
+ aRv.ThrowRangeError<MSG_VALUE_OUT_OF_RANGE>("duration");
+ return;
+ }
+
+ if (mStartCalled) {
+ aRv.ThrowInvalidStateError(
+ "Start has already been called on this AudioBufferSourceNode.");
+ return;
+ }
+ mStartCalled = true;
+
+ AudioNodeTrack* ns = mTrack;
+ if (!ns) {
+ // Nothing to play, or we're already dead for some reason
+ return;
+ }
+
+ // Remember our arguments so that we can use them when we get a new buffer.
+ mOffset = aOffset;
+ mDuration = aDuration.WasPassed() ? aDuration.Value()
+ : std::numeric_limits<double>::min();
+
+ WEB_AUDIO_API_LOG("%f: %s %u Start(%f, %g, %g)", Context()->CurrentTime(),
+ NodeType(), Id(), aWhen, aOffset, mDuration);
+
+ // We can't send these parameters without a buffer because we don't know the
+ // buffer's sample rate or length.
+ if (mBuffer) {
+ SendOffsetAndDurationParametersToTrack(ns);
+ }
+
+ // Don't set parameter unnecessarily
+ if (aWhen > 0.0) {
+ ns->SetDoubleParameter(START, aWhen);
+ }
+
+ Context()->StartBlockedAudioContextIfAllowed();
+}
+
+void AudioBufferSourceNode::Start(double aWhen, ErrorResult& aRv) {
+ Start(aWhen, 0 /* offset */, Optional<double>(), aRv);
+}
+
+void AudioBufferSourceNode::SendBufferParameterToTrack(JSContext* aCx) {
+ AudioNodeTrack* ns = mTrack;
+ if (!ns) {
+ return;
+ }
+
+ if (mBuffer) {
+ AudioChunk data = mBuffer->GetThreadSharedChannelsForRate(aCx);
+ ns->SetBuffer(std::move(data));
+
+ if (mStartCalled) {
+ SendOffsetAndDurationParametersToTrack(ns);
+ }
+ } else {
+ ns->SetBuffer(AudioChunk());
+
+ MarkInactive();
+ }
+}
+
+void AudioBufferSourceNode::SendOffsetAndDurationParametersToTrack(
+ AudioNodeTrack* aTrack) {
+ NS_ASSERTION(
+ mBuffer && mStartCalled,
+ "Only call this when we have a buffer and start() has been called");
+
+ float rate = mBuffer->SampleRate();
+ aTrack->SetInt32Parameter(SAMPLE_RATE, rate);
+
+ int32_t offsetSamples = std::max(0, NS_lround(mOffset * rate));
+
+ // Don't set parameter unnecessarily
+ if (offsetSamples > 0) {
+ aTrack->SetInt32Parameter(BUFFERSTART, offsetSamples);
+ }
+
+ if (mDuration != std::numeric_limits<double>::min()) {
+ MOZ_ASSERT(mDuration >= 0.0); // provided by Start()
+ MOZ_ASSERT(rate >= 0.0f); // provided by AudioBuffer::Create()
+ aTrack->SetDoubleParameter(DURATION, mDuration);
+ }
+ MarkActive();
+}
+
+void AudioBufferSourceNode::Stop(double aWhen, ErrorResult& aRv) {
+ if (!WebAudioUtils::IsTimeValid(aWhen)) {
+ aRv.ThrowRangeError<MSG_VALUE_OUT_OF_RANGE>("stop time");
+ return;
+ }
+
+ if (!mStartCalled) {
+ aRv.ThrowInvalidStateError(
+ "Start has not been called on this AudioBufferSourceNode.");
+ return;
+ }
+
+ WEB_AUDIO_API_LOG("%f: %s %u Stop(%f)", Context()->CurrentTime(), NodeType(),
+ Id(), aWhen);
+
+ AudioNodeTrack* ns = mTrack;
+ if (!ns || !Context()) {
+ // We've already stopped and had our track shut down
+ return;
+ }
+
+ ns->SetTrackTimeParameter(STOP, Context(), std::max(0.0, aWhen));
+}
+
+void AudioBufferSourceNode::NotifyMainThreadTrackEnded() {
+ MOZ_ASSERT(mTrack->IsEnded());
+
+ class EndedEventDispatcher final : public Runnable {
+ public:
+ explicit EndedEventDispatcher(AudioBufferSourceNode* aNode)
+ : mozilla::Runnable("EndedEventDispatcher"), mNode(aNode) {}
+ NS_IMETHOD Run() override {
+ // If it's not safe to run scripts right now, schedule this to run later
+ if (!nsContentUtils::IsSafeToRunScript()) {
+ nsContentUtils::AddScriptRunner(this);
+ return NS_OK;
+ }
+
+ mNode->DispatchTrustedEvent(u"ended"_ns);
+ // Release track resources.
+ mNode->DestroyMediaTrack();
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<AudioBufferSourceNode> mNode;
+ };
+
+ Context()->Dispatch(do_AddRef(new EndedEventDispatcher(this)));
+
+ // Drop the playing reference
+ // Warning: The below line might delete this.
+ MarkInactive();
+}
+
+void AudioBufferSourceNode::SendLoopParametersToTrack() {
+ if (!mTrack) {
+ return;
+ }
+ // Don't compute and set the loop parameters unnecessarily
+ if (mLoop && mBuffer) {
+ float rate = mBuffer->SampleRate();
+ double length = (double(mBuffer->Length()) / mBuffer->SampleRate());
+ double actualLoopStart, actualLoopEnd;
+ if (mLoopStart >= 0.0 && mLoopEnd > 0.0 && mLoopStart < mLoopEnd) {
+ MOZ_ASSERT(mLoopStart != 0.0 || mLoopEnd != 0.0);
+ actualLoopStart = (mLoopStart > length) ? 0.0 : mLoopStart;
+ actualLoopEnd = std::min(mLoopEnd, length);
+ } else {
+ actualLoopStart = 0.0;
+ actualLoopEnd = length;
+ }
+ int32_t loopStartTicks = NS_lround(actualLoopStart * rate);
+ int32_t loopEndTicks = NS_lround(actualLoopEnd * rate);
+ if (loopStartTicks < loopEndTicks) {
+ SendInt32ParameterToTrack(LOOPSTART, loopStartTicks);
+ SendInt32ParameterToTrack(LOOPEND, loopEndTicks);
+ SendInt32ParameterToTrack(LOOP, 1);
+ } else {
+ // Be explicit about looping not happening if the offsets make
+ // looping impossible.
+ SendInt32ParameterToTrack(LOOP, 0);
+ }
+ } else {
+ SendInt32ParameterToTrack(LOOP, 0);
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/AudioBufferSourceNode.h b/dom/media/webaudio/AudioBufferSourceNode.h
new file mode 100644
index 0000000000..9a3861555c
--- /dev/null
+++ b/dom/media/webaudio/AudioBufferSourceNode.h
@@ -0,0 +1,129 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef AudioBufferSourceNode_h_
+#define AudioBufferSourceNode_h_
+
+#include "AudioScheduledSourceNode.h"
+#include "AudioBuffer.h"
+
+namespace mozilla::dom {
+
+struct AudioBufferSourceOptions;
+class AudioParam;
+
+class AudioBufferSourceNode final : public AudioScheduledSourceNode,
+ public MainThreadMediaTrackListener {
+ public:
+ static already_AddRefed<AudioBufferSourceNode> Create(
+ JSContext* aCx, AudioContext& aAudioContext,
+ const AudioBufferSourceOptions& aOptions);
+
+ void DestroyMediaTrack() override;
+
+ uint16_t NumberOfInputs() const final { return 0; }
+ AudioBufferSourceNode* AsAudioBufferSourceNode() override { return this; }
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(AudioBufferSourceNode,
+ AudioScheduledSourceNode)
+
+ static already_AddRefed<AudioBufferSourceNode> Constructor(
+ const GlobalObject& aGlobal, AudioContext& aAudioContext,
+ const AudioBufferSourceOptions& aOptions) {
+ return Create(aGlobal.Context(), aAudioContext, aOptions);
+ }
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ void Start(double aWhen, double aOffset, const Optional<double>& aDuration,
+ ErrorResult& aRv);
+
+ void Start(double aWhen, ErrorResult& aRv) override;
+ void Stop(double aWhen, ErrorResult& aRv) override;
+
+ AudioBuffer* GetBuffer(JSContext* aCx) const { return mBuffer; }
+ void SetBuffer(JSContext* aCx, AudioBuffer* aBuffer, ErrorResult& aRv) {
+ if (aBuffer && mBufferSet) {
+ aRv.ThrowInvalidStateError(
+ "Cannot set the buffer attribute of an AudioBufferSourceNode "
+ "with an AudioBuffer more than once");
+ return;
+ }
+ if (aBuffer) {
+ mBufferSet = true;
+ }
+ mBuffer = aBuffer;
+ SendBufferParameterToTrack(aCx);
+ SendLoopParametersToTrack();
+ }
+ AudioParam* PlaybackRate() const { return mPlaybackRate; }
+ AudioParam* Detune() const { return mDetune; }
+ bool Loop() const { return mLoop; }
+ void SetLoop(bool aLoop) {
+ mLoop = aLoop;
+ SendLoopParametersToTrack();
+ }
+ double LoopStart() const { return mLoopStart; }
+ void SetLoopStart(double aStart) {
+ mLoopStart = aStart;
+ SendLoopParametersToTrack();
+ }
+ double LoopEnd() const { return mLoopEnd; }
+ void SetLoopEnd(double aEnd) {
+ mLoopEnd = aEnd;
+ SendLoopParametersToTrack();
+ }
+ void NotifyMainThreadTrackEnded() override;
+
+ const char* NodeType() const override { return "AudioBufferSourceNode"; }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override;
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override;
+
+ private:
+ explicit AudioBufferSourceNode(AudioContext* aContext);
+ ~AudioBufferSourceNode() = default;
+
+ friend class AudioBufferSourceNodeEngine;
+ // START is sent during Start().
+ // STOP is sent during Stop().
+ // BUFFERSTART and DURATION are sent when SetBuffer() and Start() have
+ // been called (along with sending the buffer).
+ enum EngineParameters {
+ SAMPLE_RATE,
+ START,
+ STOP,
+ // BUFFERSTART is the "offset" passed to start(), multiplied by
+ // buffer.sampleRate.
+ BUFFERSTART,
+ DURATION,
+ LOOP,
+ LOOPSTART,
+ LOOPEND,
+ PLAYBACKRATE,
+ DETUNE
+ };
+
+ void SendLoopParametersToTrack();
+ void SendBufferParameterToTrack(JSContext* aCx);
+ void SendOffsetAndDurationParametersToTrack(AudioNodeTrack* aTrack);
+
+ double mLoopStart;
+ double mLoopEnd;
+ double mOffset;
+ double mDuration;
+ RefPtr<AudioBuffer> mBuffer;
+ RefPtr<AudioParam> mPlaybackRate;
+ RefPtr<AudioParam> mDetune;
+ bool mLoop;
+ bool mStartCalled;
+ bool mBufferSet;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webaudio/AudioContext.cpp b/dom/media/webaudio/AudioContext.cpp
new file mode 100644
index 0000000000..80df77e9a6
--- /dev/null
+++ b/dom/media/webaudio/AudioContext.cpp
@@ -0,0 +1,1409 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioContext.h"
+
+#include "blink/PeriodicWave.h"
+
+#include "mozilla/ErrorResult.h"
+#include "mozilla/NotNull.h"
+#include "mozilla/OwningNonNull.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_media.h"
+
+#include "mozilla/dom/AnalyserNode.h"
+#include "mozilla/dom/AnalyserNodeBinding.h"
+#include "mozilla/dom/AudioBufferSourceNodeBinding.h"
+#include "mozilla/dom/AudioContextBinding.h"
+#include "mozilla/dom/BaseAudioContextBinding.h"
+#include "mozilla/dom/BiquadFilterNodeBinding.h"
+#include "mozilla/dom/BrowsingContext.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+#include "mozilla/dom/ChannelMergerNodeBinding.h"
+#include "mozilla/dom/ChannelSplitterNodeBinding.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/ConvolverNodeBinding.h"
+#include "mozilla/dom/DelayNodeBinding.h"
+#include "mozilla/dom/DynamicsCompressorNodeBinding.h"
+#include "mozilla/dom/GainNodeBinding.h"
+#include "mozilla/dom/IIRFilterNodeBinding.h"
+#include "mozilla/dom/HTMLMediaElement.h"
+#include "mozilla/dom/MediaElementAudioSourceNodeBinding.h"
+#include "mozilla/dom/MediaStreamAudioSourceNodeBinding.h"
+#include "mozilla/dom/MediaStreamTrackAudioSourceNodeBinding.h"
+#include "mozilla/dom/OfflineAudioContextBinding.h"
+#include "mozilla/dom/OscillatorNodeBinding.h"
+#include "mozilla/dom/PannerNodeBinding.h"
+#include "mozilla/dom/PeriodicWaveBinding.h"
+#include "mozilla/dom/Performance.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/StereoPannerNodeBinding.h"
+#include "mozilla/dom/WaveShaperNodeBinding.h"
+#include "mozilla/dom/Worklet.h"
+
+#include "AudioBuffer.h"
+#include "AudioBufferSourceNode.h"
+#include "AudioChannelService.h"
+#include "AudioDestinationNode.h"
+#include "AudioListener.h"
+#include "AudioNodeTrack.h"
+#include "AudioStream.h"
+#include "AudioWorkletImpl.h"
+#include "AutoplayPolicy.h"
+#include "BiquadFilterNode.h"
+#include "ChannelMergerNode.h"
+#include "ChannelSplitterNode.h"
+#include "ConstantSourceNode.h"
+#include "ConvolverNode.h"
+#include "DelayNode.h"
+#include "DynamicsCompressorNode.h"
+#include "GainNode.h"
+#include "IIRFilterNode.h"
+#include "js/ArrayBuffer.h" // JS::StealArrayBufferContents
+#include "MediaElementAudioSourceNode.h"
+#include "MediaStreamAudioDestinationNode.h"
+#include "MediaStreamAudioSourceNode.h"
+#include "MediaTrackGraph.h"
+#include "MediaStreamTrackAudioSourceNode.h"
+#include "nsContentUtils.h"
+#include "nsIScriptError.h"
+#include "nsNetCID.h"
+#include "nsNetUtil.h"
+#include "nsPIDOMWindow.h"
+#include "nsPrintfCString.h"
+#include "nsRFPService.h"
+#include "OscillatorNode.h"
+#include "PannerNode.h"
+#include "PeriodicWave.h"
+#include "ScriptProcessorNode.h"
+#include "StereoPannerNode.h"
+#include "WaveShaperNode.h"
+#include "Tracing.h"
+
+extern mozilla::LazyLogModule gAutoplayPermissionLog;
+
+#define AUTOPLAY_LOG(msg, ...) \
+ MOZ_LOG(gAutoplayPermissionLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
+
+namespace mozilla::dom {
+
+// 0 is a special value that MediaTracks use to denote they are not part of a
+// AudioContext.
+static dom::AudioContext::AudioContextId gAudioContextId = 1;
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(AudioContext)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AudioContext)
+ // The destination node and AudioContext form a cycle and so the destination
+ // track will be destroyed. mWorklet must be shut down before the track
+ // is destroyed. Do this before clearing mWorklet.
+ tmp->ShutdownWorklet();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mDestination)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mListener)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mWorklet)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mPromiseGripArray)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mPendingResumePromises)
+ if (tmp->mTracksAreSuspended || !tmp->mIsStarted) {
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mActiveNodes)
+ }
+ // mDecodeJobs owns the WebAudioDecodeJob objects whose lifetime is managed
+ // explicitly. mAllNodes is an array of weak pointers, ignore it here.
+ // mBasicWaveFormCache cannot participate in cycles, ignore it here.
+
+ // Remove weak reference on the global window as the context is not usable
+ // without mDestination.
+ tmp->DisconnectFromWindow();
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END_INHERITED(DOMEventTargetHelper)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(AudioContext,
+ DOMEventTargetHelper)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDestination)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mListener)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWorklet)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPromiseGripArray)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPendingResumePromises)
+ if (tmp->mTracksAreSuspended || !tmp->mIsStarted) {
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mActiveNodes)
+ }
+ // mDecodeJobs owns the WebAudioDecodeJob objects whose lifetime is managed
+ // explicitly. mAllNodes is an array of weak pointers, ignore it here.
+ // mBasicWaveFormCache cannot participate in cycles, ignore it here.
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_ADDREF_INHERITED(AudioContext, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(AudioContext, DOMEventTargetHelper)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AudioContext)
+ NS_INTERFACE_MAP_ENTRY(nsIMemoryReporter)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+static float GetSampleRateForAudioContext(bool aIsOffline, float aSampleRate,
+ bool aShouldResistFingerprinting) {
+ if (aIsOffline || aSampleRate != 0.0) {
+ return aSampleRate;
+ } else {
+ return static_cast<float>(
+ CubebUtils::PreferredSampleRate(aShouldResistFingerprinting));
+ }
+}
+
+AudioContext::AudioContext(nsPIDOMWindowInner* aWindow, bool aIsOffline,
+ uint32_t aNumberOfChannels, uint32_t aLength,
+ float aSampleRate)
+ : DOMEventTargetHelper(aWindow),
+ mId(gAudioContextId++),
+ mSampleRate(GetSampleRateForAudioContext(
+ aIsOffline, aSampleRate,
+ aWindow->AsGlobal()->ShouldResistFingerprinting(RFPTarget::Unknown))),
+ mAudioContextState(AudioContextState::Suspended),
+ mNumberOfChannels(aNumberOfChannels),
+ mRTPCallerType(aWindow->AsGlobal()->GetRTPCallerType()),
+ mShouldResistFingerprinting(
+ aWindow->AsGlobal()->ShouldResistFingerprinting(RFPTarget::Unknown)),
+ mIsOffline(aIsOffline),
+ mIsStarted(!aIsOffline),
+ mIsShutDown(false),
+ mIsDisconnecting(false),
+ mCloseCalled(false),
+ // Realtime contexts start with suspended tracks until an
+ // AudioCallbackDriver is running.
+ mTracksAreSuspended(!aIsOffline),
+ mWasAllowedToStart(true),
+ mSuspendedByContent(false),
+ mSuspendedByChrome(aWindow->IsSuspended()),
+ mWasEverAllowedToStart(false),
+ mWasEverBlockedToStart(false),
+ mWouldBeAllowedToStart(true) {
+ bool mute = aWindow->AddAudioContext(this);
+
+ // Note: AudioDestinationNode needs an AudioContext that must already be
+ // bound to the window.
+ const bool allowedToStart = media::AutoplayPolicy::IsAllowedToPlay(*this);
+ mDestination =
+ new AudioDestinationNode(this, aIsOffline, aNumberOfChannels, aLength);
+ mDestination->Init();
+ // If an AudioContext is not allowed to start, we would postpone its state
+ // transition from `suspended` to `running` until sites explicitly call
+ // AudioContext.resume() or AudioScheduledSourceNode.start().
+ if (!allowedToStart) {
+ MOZ_ASSERT(!mIsOffline);
+ AUTOPLAY_LOG("AudioContext %p is not allowed to start", this);
+ ReportBlocked();
+ } else if (!mIsOffline) {
+ ResumeInternal();
+ }
+
+ // The context can't be muted until it has a destination.
+ if (mute) {
+ Mute();
+ }
+
+ UpdateAutoplayAssumptionStatus();
+
+ FFTBlock::MainThreadInit();
+}
+
+void AudioContext::StartBlockedAudioContextIfAllowed() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MaybeUpdateAutoplayTelemetry();
+ // Only try to start AudioContext when AudioContext was not allowed to start.
+ if (mWasAllowedToStart) {
+ return;
+ }
+
+ const bool isAllowedToPlay = media::AutoplayPolicy::IsAllowedToPlay(*this);
+ AUTOPLAY_LOG("Trying to start AudioContext %p, IsAllowedToPlay=%d", this,
+ isAllowedToPlay);
+
+ // Only start the AudioContext if this resume() call was initiated by content,
+ // not if it was a result of the AudioContext starting after having been
+ // blocked because of the auto-play policy.
+ if (isAllowedToPlay && !mSuspendedByContent) {
+ ResumeInternal();
+ } else {
+ ReportBlocked();
+ }
+}
+
+void AudioContext::DisconnectFromWindow() {
+ MaybeClearPageAwakeRequest();
+ nsPIDOMWindowInner* window = GetOwner();
+ if (window) {
+ window->RemoveAudioContext(this);
+ }
+}
+
+AudioContext::~AudioContext() {
+ DisconnectFromWindow();
+ UnregisterWeakMemoryReporter(this);
+ MOZ_ASSERT(!mSetPageAwakeRequest, "forgot to revoke for page awake?");
+}
+
+JSObject* AudioContext::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ if (mIsOffline) {
+ return OfflineAudioContext_Binding::Wrap(aCx, this, aGivenProto);
+ } else {
+ return AudioContext_Binding::Wrap(aCx, this, aGivenProto);
+ }
+}
+
+static bool CheckFullyActive(nsPIDOMWindowInner* aWindow, ErrorResult& aRv) {
+ if (!aWindow->IsFullyActive()) {
+ aRv.ThrowInvalidStateError("The document is not fully active.");
+ return false;
+ }
+ return true;
+}
+
+/* static */
+already_AddRefed<AudioContext> AudioContext::Constructor(
+ const GlobalObject& aGlobal, const AudioContextOptions& aOptions,
+ ErrorResult& aRv) {
+ nsCOMPtr<nsPIDOMWindowInner> window =
+ do_QueryInterface(aGlobal.GetAsSupports());
+ if (!window) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+ /**
+ * If the current settings object’s responsible document is NOT fully
+ * active, throw an InvalidStateError and abort these steps.
+ */
+ if (!CheckFullyActive(window, aRv)) {
+ return nullptr;
+ }
+
+ if (aOptions.mSampleRate.WasPassed() &&
+ (aOptions.mSampleRate.Value() < WebAudioUtils::MinSampleRate ||
+ aOptions.mSampleRate.Value() > WebAudioUtils::MaxSampleRate)) {
+ aRv.ThrowNotSupportedError(nsPrintfCString(
+ "Sample rate %g is not in the range [%u, %u]",
+ aOptions.mSampleRate.Value(), WebAudioUtils::MinSampleRate,
+ WebAudioUtils::MaxSampleRate));
+ return nullptr;
+ }
+ float sampleRate = aOptions.mSampleRate.WasPassed()
+ ? aOptions.mSampleRate.Value()
+ : MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE;
+
+ RefPtr<AudioContext> object =
+ new AudioContext(window, false, 2, 0, sampleRate);
+
+ RegisterWeakMemoryReporter(object);
+
+ return object.forget();
+}
+
+/* static */
+already_AddRefed<AudioContext> AudioContext::Constructor(
+ const GlobalObject& aGlobal, const OfflineAudioContextOptions& aOptions,
+ ErrorResult& aRv) {
+ return Constructor(aGlobal, aOptions.mNumberOfChannels, aOptions.mLength,
+ aOptions.mSampleRate, aRv);
+}
+
+/* static */
+already_AddRefed<AudioContext> AudioContext::Constructor(
+ const GlobalObject& aGlobal, uint32_t aNumberOfChannels, uint32_t aLength,
+ float aSampleRate, ErrorResult& aRv) {
+ nsCOMPtr<nsPIDOMWindowInner> window =
+ do_QueryInterface(aGlobal.GetAsSupports());
+ if (!window) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+ /**
+ * If the current settings object’s responsible document is NOT fully
+ * active, throw an InvalidStateError and abort these steps.
+ */
+ if (!CheckFullyActive(window, aRv)) {
+ return nullptr;
+ }
+
+ if (aNumberOfChannels == 0 ||
+ aNumberOfChannels > WebAudioUtils::MaxChannelCount) {
+ aRv.ThrowNotSupportedError(
+ nsPrintfCString("%u is not a valid channel count", aNumberOfChannels));
+ return nullptr;
+ }
+
+ if (aLength == 0) {
+ aRv.ThrowNotSupportedError("Length must be nonzero");
+ return nullptr;
+ }
+
+ if (aSampleRate < WebAudioUtils::MinSampleRate ||
+ aSampleRate > WebAudioUtils::MaxSampleRate) {
+ // The DOM binding protects us against infinity and NaN
+ aRv.ThrowNotSupportedError(nsPrintfCString(
+ "Sample rate %g is not in the range [%u, %u]", aSampleRate,
+ WebAudioUtils::MinSampleRate, WebAudioUtils::MaxSampleRate));
+ return nullptr;
+ }
+
+ RefPtr<AudioContext> object =
+ new AudioContext(window, true, aNumberOfChannels, aLength, aSampleRate);
+
+ RegisterWeakMemoryReporter(object);
+
+ return object.forget();
+}
+
+already_AddRefed<AudioBufferSourceNode> AudioContext::CreateBufferSource() {
+ return AudioBufferSourceNode::Create(nullptr, *this,
+ AudioBufferSourceOptions());
+}
+
+already_AddRefed<ConstantSourceNode> AudioContext::CreateConstantSource() {
+ RefPtr<ConstantSourceNode> constantSourceNode = new ConstantSourceNode(this);
+ return constantSourceNode.forget();
+}
+
+already_AddRefed<AudioBuffer> AudioContext::CreateBuffer(
+ uint32_t aNumberOfChannels, uint32_t aLength, float aSampleRate,
+ ErrorResult& aRv) {
+ if (!aNumberOfChannels) {
+ aRv.ThrowNotSupportedError("Number of channels must be nonzero");
+ return nullptr;
+ }
+
+ return AudioBuffer::Create(GetOwner(), aNumberOfChannels, aLength,
+ aSampleRate, aRv);
+}
+
+namespace {
+
+bool IsValidBufferSize(uint32_t aBufferSize) {
+ switch (aBufferSize) {
+ case 0: // let the implementation choose the buffer size
+ case 256:
+ case 512:
+ case 1024:
+ case 2048:
+ case 4096:
+ case 8192:
+ case 16384:
+ return true;
+ default:
+ return false;
+ }
+}
+
+} // namespace
+
+already_AddRefed<MediaStreamAudioDestinationNode>
+AudioContext::CreateMediaStreamDestination(ErrorResult& aRv) {
+ return MediaStreamAudioDestinationNode::Create(*this, AudioNodeOptions(),
+ aRv);
+}
+
+already_AddRefed<ScriptProcessorNode> AudioContext::CreateScriptProcessor(
+ uint32_t aBufferSize, uint32_t aNumberOfInputChannels,
+ uint32_t aNumberOfOutputChannels, ErrorResult& aRv) {
+ if (aNumberOfInputChannels == 0 && aNumberOfOutputChannels == 0) {
+ aRv.ThrowIndexSizeError(
+ "At least one of numberOfInputChannels and numberOfOutputChannels must "
+ "be nonzero");
+ return nullptr;
+ }
+
+ if (aNumberOfInputChannels > WebAudioUtils::MaxChannelCount) {
+ aRv.ThrowIndexSizeError(nsPrintfCString(
+ "%u is not a valid number of input channels", aNumberOfInputChannels));
+ return nullptr;
+ }
+
+ if (aNumberOfOutputChannels > WebAudioUtils::MaxChannelCount) {
+ aRv.ThrowIndexSizeError(
+ nsPrintfCString("%u is not a valid number of output channels",
+ aNumberOfOutputChannels));
+ return nullptr;
+ }
+
+ if (!IsValidBufferSize(aBufferSize)) {
+ aRv.ThrowIndexSizeError(
+ nsPrintfCString("%u is not a valid bufferSize", aBufferSize));
+ return nullptr;
+ }
+
+ RefPtr<ScriptProcessorNode> scriptProcessor = new ScriptProcessorNode(
+ this, aBufferSize, aNumberOfInputChannels, aNumberOfOutputChannels);
+ return scriptProcessor.forget();
+}
+
+already_AddRefed<AnalyserNode> AudioContext::CreateAnalyser(ErrorResult& aRv) {
+ return AnalyserNode::Create(*this, AnalyserOptions(), aRv);
+}
+
+already_AddRefed<StereoPannerNode> AudioContext::CreateStereoPanner(
+ ErrorResult& aRv) {
+ return StereoPannerNode::Create(*this, StereoPannerOptions(), aRv);
+}
+
+already_AddRefed<MediaElementAudioSourceNode>
+AudioContext::CreateMediaElementSource(HTMLMediaElement& aMediaElement,
+ ErrorResult& aRv) {
+ MediaElementAudioSourceOptions options;
+ options.mMediaElement = aMediaElement;
+
+ return MediaElementAudioSourceNode::Create(*this, options, aRv);
+}
+
+already_AddRefed<MediaStreamAudioSourceNode>
+AudioContext::CreateMediaStreamSource(DOMMediaStream& aMediaStream,
+ ErrorResult& aRv) {
+ MediaStreamAudioSourceOptions options;
+ options.mMediaStream = aMediaStream;
+
+ return MediaStreamAudioSourceNode::Create(*this, options, aRv);
+}
+
+already_AddRefed<MediaStreamTrackAudioSourceNode>
+AudioContext::CreateMediaStreamTrackSource(MediaStreamTrack& aMediaStreamTrack,
+ ErrorResult& aRv) {
+ MediaStreamTrackAudioSourceOptions options;
+ options.mMediaStreamTrack = aMediaStreamTrack;
+
+ return MediaStreamTrackAudioSourceNode::Create(*this, options, aRv);
+}
+
+already_AddRefed<GainNode> AudioContext::CreateGain(ErrorResult& aRv) {
+ return GainNode::Create(*this, GainOptions(), aRv);
+}
+
+already_AddRefed<WaveShaperNode> AudioContext::CreateWaveShaper(
+ ErrorResult& aRv) {
+ return WaveShaperNode::Create(*this, WaveShaperOptions(), aRv);
+}
+
+already_AddRefed<DelayNode> AudioContext::CreateDelay(double aMaxDelayTime,
+ ErrorResult& aRv) {
+ DelayOptions options;
+ options.mMaxDelayTime = aMaxDelayTime;
+ return DelayNode::Create(*this, options, aRv);
+}
+
+already_AddRefed<PannerNode> AudioContext::CreatePanner(ErrorResult& aRv) {
+ return PannerNode::Create(*this, PannerOptions(), aRv);
+}
+
+already_AddRefed<ConvolverNode> AudioContext::CreateConvolver(
+ ErrorResult& aRv) {
+ return ConvolverNode::Create(nullptr, *this, ConvolverOptions(), aRv);
+}
+
+already_AddRefed<ChannelSplitterNode> AudioContext::CreateChannelSplitter(
+ uint32_t aNumberOfOutputs, ErrorResult& aRv) {
+ ChannelSplitterOptions options;
+ options.mNumberOfOutputs = aNumberOfOutputs;
+ return ChannelSplitterNode::Create(*this, options, aRv);
+}
+
+already_AddRefed<ChannelMergerNode> AudioContext::CreateChannelMerger(
+ uint32_t aNumberOfInputs, ErrorResult& aRv) {
+ ChannelMergerOptions options;
+ options.mNumberOfInputs = aNumberOfInputs;
+ return ChannelMergerNode::Create(*this, options, aRv);
+}
+
+already_AddRefed<DynamicsCompressorNode> AudioContext::CreateDynamicsCompressor(
+ ErrorResult& aRv) {
+ return DynamicsCompressorNode::Create(*this, DynamicsCompressorOptions(),
+ aRv);
+}
+
+already_AddRefed<BiquadFilterNode> AudioContext::CreateBiquadFilter(
+ ErrorResult& aRv) {
+ return BiquadFilterNode::Create(*this, BiquadFilterOptions(), aRv);
+}
+
+already_AddRefed<IIRFilterNode> AudioContext::CreateIIRFilter(
+ const Sequence<double>& aFeedforward, const Sequence<double>& aFeedback,
+ mozilla::ErrorResult& aRv) {
+ IIRFilterOptions options;
+ options.mFeedforward = aFeedforward;
+ options.mFeedback = aFeedback;
+ return IIRFilterNode::Create(*this, options, aRv);
+}
+
+already_AddRefed<OscillatorNode> AudioContext::CreateOscillator(
+ ErrorResult& aRv) {
+ return OscillatorNode::Create(*this, OscillatorOptions(), aRv);
+}
+
+already_AddRefed<PeriodicWave> AudioContext::CreatePeriodicWave(
+ const Sequence<float>& aRealData, const Sequence<float>& aImagData,
+ const PeriodicWaveConstraints& aConstraints, ErrorResult& aRv) {
+ RefPtr<PeriodicWave> periodicWave = new PeriodicWave(
+ this, aRealData.Elements(), aRealData.Length(), aImagData.Elements(),
+ aImagData.Length(), aConstraints.mDisableNormalization, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+ return periodicWave.forget();
+}
+
+AudioListener* AudioContext::Listener() {
+ if (!mListener) {
+ mListener = new AudioListener(this);
+ }
+ return mListener;
+}
+
+double AudioContext::OutputLatency() {
+ if (mIsShutDown) {
+ return 0.0;
+ }
+ // When reduceFingerprinting is enabled, return a latency figure that is
+ // fixed, but plausible for the platform.
+ double latency_s = 0.0;
+ if (mShouldResistFingerprinting) {
+#ifdef XP_MACOSX
+ latency_s = 512. / mSampleRate;
+#elif MOZ_WIDGET_ANDROID
+ latency_s = 0.020;
+#elif XP_WIN
+ latency_s = 0.04;
+#else // Catchall for other OSes, including Linux.
+ latency_s = 0.025;
+#endif
+ } else {
+ return Graph()->AudioOutputLatency();
+ }
+ return latency_s;
+}
+
+void AudioContext::GetOutputTimestamp(AudioTimestamp& aTimeStamp) {
+ if (!Destination()) {
+ aTimeStamp.mContextTime.Construct(0.0);
+ aTimeStamp.mPerformanceTime.Construct(0.0);
+ return;
+ }
+
+ // The currentTime currently being output is the currentTime minus the audio
+ // output latency. The resolution of CurrentTime() is already reduced.
+ aTimeStamp.mContextTime.Construct(
+ std::max(0.0, CurrentTime() - OutputLatency()));
+ nsPIDOMWindowInner* parent = GetParentObject();
+ Performance* perf = parent ? parent->GetPerformance() : nullptr;
+ if (perf) {
+ // perf->Now() already has reduced resolution here, no need to do it again.
+ aTimeStamp.mPerformanceTime.Construct(
+ std::max(0., perf->Now() - (OutputLatency() * 1000.)));
+ } else {
+ aTimeStamp.mPerformanceTime.Construct(0.0);
+ }
+}
+
+Worklet* AudioContext::GetAudioWorklet(ErrorResult& aRv) {
+ if (!mWorklet) {
+ mWorklet = AudioWorkletImpl::CreateWorklet(this, aRv);
+ }
+
+ return mWorklet;
+}
+bool AudioContext::IsRunning() const {
+ return mAudioContextState == AudioContextState::Running;
+}
+
+already_AddRefed<Promise> AudioContext::CreatePromise(ErrorResult& aRv) {
+ // Get the relevant global for the promise from the wrapper cache because
+ // DOMEventTargetHelper::GetOwner() returns null if the document is unloaded.
+ // We know the wrapper exists because it is being used for |this| from JS.
+ // See https://github.com/heycam/webidl/issues/932 for why the relevant
+ // global is used instead of the current global.
+ nsCOMPtr<nsIGlobalObject> global = xpc::NativeGlobal(GetWrapper());
+ RefPtr<Promise> promise = Promise::Create(global, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+ /**
+ * If this's relevant global object's associated Document is not fully
+ * active then return a promise rejected with "InvalidStateError"
+ * DOMException.
+ */
+ nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(global);
+ if (!window->IsFullyActive()) {
+ promise->MaybeRejectWithInvalidStateError(
+ "The document is not fully active.");
+ }
+ return promise.forget();
+}
+
+already_AddRefed<Promise> AudioContext::DecodeAudioData(
+ const ArrayBuffer& aBuffer,
+ const Optional<OwningNonNull<DecodeSuccessCallback>>& aSuccessCallback,
+ const Optional<OwningNonNull<DecodeErrorCallback>>& aFailureCallback,
+ ErrorResult& aRv) {
+ AutoJSAPI jsapi;
+ jsapi.Init();
+ JSContext* cx = jsapi.cx();
+
+ // CheckedUnwrapStatic is OK, since we know we have an ArrayBuffer.
+ JS::Rooted<JSObject*> obj(cx, js::CheckedUnwrapStatic(aBuffer.Obj()));
+ if (!obj) {
+ aRv.ThrowSecurityError("Can't get audio data from cross-origin object");
+ return nullptr;
+ }
+
+ RefPtr<Promise> promise = CreatePromise(aRv);
+ if (aRv.Failed() || promise->State() == Promise::PromiseState::Rejected) {
+ return promise.forget();
+ }
+
+ JSAutoRealm ar(cx, obj);
+ aBuffer.ComputeState();
+
+ if (!aBuffer.Data()) {
+ // Throw if the buffer is detached
+ aRv.ThrowTypeError("Buffer argument can't be a detached buffer");
+ return nullptr;
+ }
+
+ // Detach the array buffer
+ size_t length = aBuffer.Length();
+
+ uint8_t* data = static_cast<uint8_t*>(JS::StealArrayBufferContents(cx, obj));
+
+ // Sniff the content of the media.
+ // Failed type sniffing will be handled by AsyncDecodeWebAudio.
+ nsAutoCString contentType;
+ NS_SniffContent(NS_DATA_SNIFFER_CATEGORY, nullptr, data, length, contentType);
+
+ RefPtr<DecodeErrorCallback> failureCallback;
+ RefPtr<DecodeSuccessCallback> successCallback;
+ if (aFailureCallback.WasPassed()) {
+ failureCallback = &aFailureCallback.Value();
+ }
+ if (aSuccessCallback.WasPassed()) {
+ successCallback = &aSuccessCallback.Value();
+ }
+ UniquePtr<WebAudioDecodeJob> job(
+ new WebAudioDecodeJob(this, promise, successCallback, failureCallback));
+ AsyncDecodeWebAudio(contentType.get(), data, length, *job);
+ // Transfer the ownership to mDecodeJobs
+ mDecodeJobs.AppendElement(std::move(job));
+
+ return promise.forget();
+}
+
+void AudioContext::RemoveFromDecodeQueue(WebAudioDecodeJob* aDecodeJob) {
+ // Since UniquePtr doesn't provide an operator== which allows you to compare
+ // against raw pointers, we need to iterate manually.
+ for (uint32_t i = 0; i < mDecodeJobs.Length(); ++i) {
+ if (mDecodeJobs[i].get() == aDecodeJob) {
+ mDecodeJobs.RemoveElementAt(i);
+ break;
+ }
+ }
+}
+
+void AudioContext::RegisterActiveNode(AudioNode* aNode) {
+ if (!mCloseCalled) {
+ mActiveNodes.Insert(aNode);
+ }
+}
+
+void AudioContext::UnregisterActiveNode(AudioNode* aNode) {
+ mActiveNodes.Remove(aNode);
+}
+
+uint32_t AudioContext::MaxChannelCount() const {
+ if (mShouldResistFingerprinting) {
+ return 2;
+ }
+ return std::min<uint32_t>(
+ WebAudioUtils::MaxChannelCount,
+ mIsOffline ? mNumberOfChannels : CubebUtils::MaxNumberOfChannels());
+}
+
+uint32_t AudioContext::ActiveNodeCount() const { return mActiveNodes.Count(); }
+
+MediaTrackGraph* AudioContext::Graph() const {
+ return Destination()->Track()->Graph();
+}
+
+AudioNodeTrack* AudioContext::DestinationTrack() const {
+ if (Destination()) {
+ return Destination()->Track();
+ }
+ return nullptr;
+}
+
+void AudioContext::ShutdownWorklet() {
+ if (mWorklet) {
+ mWorklet->Impl()->NotifyWorkletFinished();
+ }
+}
+
+double AudioContext::CurrentTime() {
+ mozilla::MediaTrack* track = Destination()->Track();
+
+ double rawTime = track->TrackTimeToSeconds(track->GetCurrentTime());
+
+ // CurrentTime increments in intervals of 128/sampleRate. If the Timer
+ // Precision Reduction is smaller than this interval, the jittered time
+ // can always be reversed to the raw step of the interval. In that case
+ // we can simply return the un-reduced time; and avoid breaking tests.
+ // We have to convert each variable into a common magnitude, we choose ms.
+ if ((128 / mSampleRate) * 1000.0 >
+ nsRFPService::TimerResolution(mRTPCallerType) / 1000.0) {
+ return rawTime;
+ }
+
+ // The value of a MediaTrack's CurrentTime will always advance forward; it
+ // will never reset (even if one rewinds a video.) Therefore we can use a
+ // single Random Seed initialized at the same time as the object.
+ return nsRFPService::ReduceTimePrecisionAsSecs(
+ rawTime, GetRandomTimelineSeed(), mRTPCallerType);
+}
+
+nsISerialEventTarget* AudioContext::GetMainThread() const {
+ if (nsPIDOMWindowInner* window = GetParentObject()) {
+ return window->AsGlobal()->EventTargetFor(TaskCategory::Other);
+ }
+
+ return GetCurrentSerialEventTarget();
+}
+
+void AudioContext::DisconnectFromOwner() {
+ mIsDisconnecting = true;
+ MaybeClearPageAwakeRequest();
+ OnWindowDestroy();
+ DOMEventTargetHelper::DisconnectFromOwner();
+}
+
+void AudioContext::OnWindowDestroy() {
+ // Avoid resend the Telemetry data.
+ if (!mIsShutDown) {
+ MaybeUpdateAutoplayTelemetryWhenShutdown();
+ }
+ mIsShutDown = true;
+
+ CloseInternal(nullptr, AudioContextOperationFlags::None);
+
+ // We don't want to touch promises if the global is going away soon.
+ if (!mIsDisconnecting) {
+ for (auto p : mPromiseGripArray) {
+ p->MaybeRejectWithInvalidStateError("Navigated away from page");
+ }
+
+ mPromiseGripArray.Clear();
+
+ for (const auto& p : mPendingResumePromises) {
+ p->MaybeRejectWithInvalidStateError("Navigated away from page");
+ }
+ mPendingResumePromises.Clear();
+ }
+
+ // On process shutdown, the MTG thread shuts down before the destination
+ // track is destroyed, but AudioWorklet needs to release objects on the MTG
+ // thread. AudioContext::Shutdown() is invoked on processing the
+ // PBrowser::Destroy() message before xpcom shutdown begins.
+ ShutdownWorklet();
+
+ if (mDestination) {
+ // We can destroy the MediaTrackGraph at this point.
+ // Although there may be other clients using the graph, this graph is used
+ // only for clients in the same window and this window is going away.
+ // This will also interrupt any worklet script still running on the graph
+ // thread.
+ Graph()->ForceShutDown();
+ // AudioDestinationNodes on rendering offline contexts have a
+ // self-reference which needs removal.
+ if (mIsOffline) {
+ mDestination->OfflineShutdown();
+ }
+ }
+}
+
+/* This runnable allows to fire the "statechange" event */
+class OnStateChangeTask final : public Runnable {
+ public:
+ explicit OnStateChangeTask(AudioContext* aAudioContext)
+ : Runnable("dom::OnStateChangeTask"), mAudioContext(aAudioContext) {}
+
+ NS_IMETHODIMP
+ Run() override {
+ nsPIDOMWindowInner* parent = mAudioContext->GetParentObject();
+ if (!parent) {
+ return NS_ERROR_FAILURE;
+ }
+
+ Document* doc = parent->GetExtantDoc();
+ if (!doc) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return nsContentUtils::DispatchTrustedEvent(
+ doc, static_cast<EventTarget*>(mAudioContext), u"statechange"_ns,
+ CanBubble::eNo, Cancelable::eNo);
+ }
+
+ private:
+ RefPtr<AudioContext> mAudioContext;
+};
+
+void AudioContext::Dispatch(already_AddRefed<nsIRunnable>&& aRunnable) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsCOMPtr<nsIGlobalObject> parentObject = do_QueryInterface(GetParentObject());
+ // It can happen that this runnable took a long time to reach the main thread,
+ // and the global is not valid anymore.
+ if (parentObject) {
+ parentObject->AbstractMainThreadFor(TaskCategory::Other)
+ ->Dispatch(std::move(aRunnable));
+ } else {
+ RefPtr<nsIRunnable> runnable(aRunnable);
+ runnable = nullptr;
+ }
+}
+
+void AudioContext::OnStateChanged(void* aPromise, AudioContextState aNewState) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mAudioContextState == AudioContextState::Closed) {
+ fprintf(stderr,
+ "Invalid transition: mAudioContextState: %d -> aNewState %d\n",
+ static_cast<int>(mAudioContextState), static_cast<int>(aNewState));
+ MOZ_ASSERT(false);
+ }
+
+ if (aPromise) {
+ Promise* promise = reinterpret_cast<Promise*>(aPromise);
+ // It is possible for the promise to have been removed from
+ // mPromiseGripArray if the cycle collector has severed our connections. DO
+ // NOT dereference the promise pointer in that case since it may point to
+ // already freed memory.
+ if (mPromiseGripArray.Contains(promise)) {
+ promise->MaybeResolveWithUndefined();
+ DebugOnly<bool> rv = mPromiseGripArray.RemoveElement(promise);
+ MOZ_ASSERT(rv, "Promise wasn't in the grip array?");
+ }
+ }
+
+ // Resolve all pending promises once the audio context has been allowed to
+ // start.
+ if (aNewState == AudioContextState::Running) {
+ for (const auto& p : mPendingResumePromises) {
+ p->MaybeResolveWithUndefined();
+ }
+ mPendingResumePromises.Clear();
+ }
+
+ if (mAudioContextState != aNewState) {
+ RefPtr<OnStateChangeTask> task = new OnStateChangeTask(this);
+ Dispatch(task.forget());
+ }
+
+ mAudioContextState = aNewState;
+ Destination()->NotifyAudioContextStateChanged();
+ MaybeUpdatePageAwakeRequest();
+}
+
+BrowsingContext* AudioContext::GetTopLevelBrowsingContext() {
+ nsCOMPtr<nsPIDOMWindowInner> window = GetParentObject();
+ if (!window) {
+ return nullptr;
+ }
+ BrowsingContext* bc = window->GetBrowsingContext();
+ if (!bc || bc->IsDiscarded()) {
+ return nullptr;
+ }
+ return bc->Top();
+}
+
+void AudioContext::MaybeUpdatePageAwakeRequest() {
+ // No need to keep page awake for offline context.
+ if (IsOffline()) {
+ return;
+ }
+
+ if (mIsShutDown) {
+ return;
+ }
+
+ if (IsRunning() && !mSetPageAwakeRequest) {
+ SetPageAwakeRequest(true);
+ } else if (!IsRunning() && mSetPageAwakeRequest) {
+ SetPageAwakeRequest(false);
+ }
+}
+
+void AudioContext::SetPageAwakeRequest(bool aShouldSet) {
+ mSetPageAwakeRequest = aShouldSet;
+ BrowsingContext* bc = GetTopLevelBrowsingContext();
+ if (!bc) {
+ return;
+ }
+ if (XRE_IsContentProcess()) {
+ ContentChild* contentChild = ContentChild::GetSingleton();
+ Unused << contentChild->SendAddOrRemovePageAwakeRequest(bc, aShouldSet);
+ return;
+ }
+ if (aShouldSet) {
+ bc->Canonical()->AddPageAwakeRequest();
+ } else {
+ bc->Canonical()->RemovePageAwakeRequest();
+ }
+}
+
+void AudioContext::MaybeClearPageAwakeRequest() {
+ if (mSetPageAwakeRequest) {
+ SetPageAwakeRequest(false);
+ }
+}
+
+nsTArray<RefPtr<mozilla::MediaTrack>> AudioContext::GetAllTracks() const {
+ nsTArray<RefPtr<mozilla::MediaTrack>> tracks;
+ for (AudioNode* node : mAllNodes) {
+ mozilla::MediaTrack* t = node->GetTrack();
+ if (t) {
+ tracks.AppendElement(t);
+ }
+ // Add the tracks of AudioParam.
+ const nsTArray<RefPtr<AudioParam>>& audioParams = node->GetAudioParams();
+ if (!audioParams.IsEmpty()) {
+ for (auto& param : audioParams) {
+ t = param->GetTrack();
+ if (t && !tracks.Contains(t)) {
+ tracks.AppendElement(t);
+ }
+ }
+ }
+ }
+ return tracks;
+}
+
+already_AddRefed<Promise> AudioContext::Suspend(ErrorResult& aRv) {
+ TRACE("AudioContext::Suspend");
+ RefPtr<Promise> promise = CreatePromise(aRv);
+ if (aRv.Failed() || promise->State() == Promise::PromiseState::Rejected) {
+ return promise.forget();
+ }
+ if (mIsOffline) {
+ // XXXbz This is not reachable, since we don't implement this
+ // method on OfflineAudioContext at all!
+ promise->MaybeRejectWithNotSupportedError(
+ "Can't suspend OfflineAudioContext yet");
+ return promise.forget();
+ }
+
+ if (mCloseCalled) {
+ promise->MaybeRejectWithInvalidStateError(
+ "Can't suspend if the control thread state is \"closed\"");
+ return promise.forget();
+ }
+
+ mSuspendedByContent = true;
+ mPromiseGripArray.AppendElement(promise);
+ SuspendInternal(promise, AudioContextOperationFlags::SendStateChange);
+ return promise.forget();
+}
+
+void AudioContext::SuspendFromChrome() {
+ if (mIsOffline || mIsShutDown) {
+ return;
+ }
+ MOZ_ASSERT(!mSuspendedByChrome);
+ mSuspendedByChrome = true;
+ SuspendInternal(nullptr, Preferences::GetBool("dom.audiocontext.testing")
+ ? AudioContextOperationFlags::SendStateChange
+ : AudioContextOperationFlags::None);
+}
+
+void AudioContext::SuspendInternal(void* aPromise,
+ AudioContextOperationFlags aFlags) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!mIsOffline);
+ Destination()->Suspend();
+
+ nsTArray<RefPtr<mozilla::MediaTrack>> tracks;
+ // If mTracksAreSuspended is true then we already suspended all our tracks,
+ // so don't suspend them again (since suspend(); suspend(); resume(); should
+ // cancel both suspends). But we still need to do ApplyAudioContextOperation
+ // to ensure our new promise is resolved.
+ if (!mTracksAreSuspended) {
+ mTracksAreSuspended = true;
+ tracks = GetAllTracks();
+ }
+ auto promise = Graph()->ApplyAudioContextOperation(
+ DestinationTrack(), std::move(tracks), AudioContextOperation::Suspend);
+ if ((aFlags & AudioContextOperationFlags::SendStateChange)) {
+ promise->Then(
+ GetMainThread(), "AudioContext::OnStateChanged",
+ [self = RefPtr<AudioContext>(this),
+ aPromise](AudioContextState aNewState) {
+ self->OnStateChanged(aPromise, aNewState);
+ },
+ [] { MOZ_CRASH("Unexpected rejection"); });
+ }
+}
+
+void AudioContext::ResumeFromChrome() {
+ if (mIsOffline || mIsShutDown) {
+ return;
+ }
+ MOZ_ASSERT(mSuspendedByChrome);
+ mSuspendedByChrome = false;
+ if (!mWasAllowedToStart) {
+ return;
+ }
+ ResumeInternal();
+}
+
+already_AddRefed<Promise> AudioContext::Resume(ErrorResult& aRv) {
+ TRACE("AudioContext::Resume");
+ RefPtr<Promise> promise = CreatePromise(aRv);
+ if (aRv.Failed() || promise->State() == Promise::PromiseState::Rejected) {
+ return promise.forget();
+ }
+
+ if (mIsOffline) {
+ promise->MaybeRejectWithNotSupportedError(
+ "Can't resume OfflineAudioContext");
+ return promise.forget();
+ }
+
+ if (mCloseCalled) {
+ promise->MaybeRejectWithInvalidStateError(
+ "Can't resume if the control thread state is \"closed\"");
+ return promise.forget();
+ }
+
+ mSuspendedByContent = false;
+ mPendingResumePromises.AppendElement(promise);
+
+ const bool isAllowedToPlay = media::AutoplayPolicy::IsAllowedToPlay(*this);
+ AUTOPLAY_LOG("Trying to resume AudioContext %p, IsAllowedToPlay=%d", this,
+ isAllowedToPlay);
+ if (isAllowedToPlay) {
+ ResumeInternal();
+ } else {
+ ReportBlocked();
+ }
+
+ MaybeUpdateAutoplayTelemetry();
+
+ return promise.forget();
+}
+
+void AudioContext::ResumeInternal() {
+ MOZ_ASSERT(!mIsOffline);
+ AUTOPLAY_LOG("Allow to resume AudioContext %p", this);
+ mWasAllowedToStart = true;
+
+ if (mSuspendedByChrome || mSuspendedByContent || mCloseCalled) {
+ MOZ_ASSERT(mTracksAreSuspended);
+ return;
+ }
+
+ Destination()->Resume();
+
+ nsTArray<RefPtr<mozilla::MediaTrack>> tracks;
+ // If mTracksAreSuspended is false then we already resumed all our tracks,
+ // so don't resume them again (since suspend(); resume(); resume(); should
+ // be OK). But we still need to do ApplyAudioContextOperation
+ // to ensure our new promise is resolved.
+ if (mTracksAreSuspended) {
+ mTracksAreSuspended = false;
+ tracks = GetAllTracks();
+ }
+ // Check for statechange even when resumed from chrome because content may
+ // have called Resume() before chrome resumed the window.
+ Graph()
+ ->ApplyAudioContextOperation(DestinationTrack(), std::move(tracks),
+ AudioContextOperation::Resume)
+ ->Then(
+ GetMainThread(), "AudioContext::OnStateChanged",
+ [self = RefPtr<AudioContext>(this)](AudioContextState aNewState) {
+ self->OnStateChanged(nullptr, aNewState);
+ },
+ [] {}); // Promise may be rejected after graph shutdown.
+}
+
+void AudioContext::UpdateAutoplayAssumptionStatus() {
+ if (media::AutoplayPolicyTelemetryUtils::
+ WouldBeAllowedToPlayIfAutoplayDisabled(*this)) {
+ mWasEverAllowedToStart |= true;
+ mWouldBeAllowedToStart = true;
+ } else {
+ mWasEverBlockedToStart |= true;
+ mWouldBeAllowedToStart = false;
+ }
+}
+
+void AudioContext::MaybeUpdateAutoplayTelemetry() {
+ // Exclude offline AudioContext because it's always allowed to start.
+ if (mIsOffline) {
+ return;
+ }
+
+ if (media::AutoplayPolicyTelemetryUtils::
+ WouldBeAllowedToPlayIfAutoplayDisabled(*this) &&
+ !mWouldBeAllowedToStart) {
+ AccumulateCategorical(
+ mozilla::Telemetry::LABELS_WEB_AUDIO_AUTOPLAY::AllowedAfterBlocked);
+ }
+ UpdateAutoplayAssumptionStatus();
+}
+
+void AudioContext::MaybeUpdateAutoplayTelemetryWhenShutdown() {
+ // Exclude offline AudioContext because it's always allowed to start.
+ if (mIsOffline) {
+ return;
+ }
+
+ if (mWasEverAllowedToStart && !mWasEverBlockedToStart) {
+ AccumulateCategorical(
+ mozilla::Telemetry::LABELS_WEB_AUDIO_AUTOPLAY::NeverBlocked);
+ } else if (!mWasEverAllowedToStart && mWasEverBlockedToStart) {
+ AccumulateCategorical(
+ mozilla::Telemetry::LABELS_WEB_AUDIO_AUTOPLAY::NeverAllowed);
+ }
+}
+
+void AudioContext::ReportBlocked() {
+ ReportToConsole(nsIScriptError::warningFlag,
+ "BlockAutoplayWebAudioStartError");
+ mWasAllowedToStart = false;
+
+ if (!StaticPrefs::media_autoplay_block_event_enabled()) {
+ return;
+ }
+
+ RefPtr<AudioContext> self = this;
+ RefPtr<nsIRunnable> r =
+ NS_NewRunnableFunction("AudioContext::AutoplayBlocked", [self]() {
+ nsPIDOMWindowInner* parent = self->GetParentObject();
+ if (!parent) {
+ return;
+ }
+
+ Document* doc = parent->GetExtantDoc();
+ if (!doc) {
+ return;
+ }
+
+ AUTOPLAY_LOG("Dispatch `blocked` event for AudioContext %p",
+ self.get());
+ nsContentUtils::DispatchTrustedEvent(
+ doc, static_cast<EventTarget*>(self), u"blocked"_ns,
+ CanBubble::eNo, Cancelable::eNo);
+ });
+ Dispatch(r.forget());
+}
+
+already_AddRefed<Promise> AudioContext::Close(ErrorResult& aRv) {
+ TRACE("AudioContext::Close");
+ RefPtr<Promise> promise = CreatePromise(aRv);
+ if (aRv.Failed() || promise->State() == Promise::PromiseState::Rejected) {
+ return promise.forget();
+ }
+
+ if (mIsOffline) {
+ // XXXbz This is not reachable, since we don't implement this
+ // method on OfflineAudioContext at all!
+ promise->MaybeRejectWithNotSupportedError(
+ "Can't close OfflineAudioContext yet");
+ return promise.forget();
+ }
+
+ if (mCloseCalled) {
+ promise->MaybeRejectWithInvalidStateError(
+ "Can't close an AudioContext twice");
+ return promise.forget();
+ }
+
+ mPromiseGripArray.AppendElement(promise);
+
+ CloseInternal(promise, AudioContextOperationFlags::SendStateChange);
+
+ return promise.forget();
+}
+
+void AudioContext::OfflineClose() {
+ CloseInternal(nullptr, AudioContextOperationFlags::None);
+}
+
+void AudioContext::CloseInternal(void* aPromise,
+ AudioContextOperationFlags aFlags) {
+ // This can be called when freeing a document, and the tracks are dead at
+ // this point, so we need extra null-checks.
+ AudioNodeTrack* ds = DestinationTrack();
+ if (ds && !mIsOffline) {
+ Destination()->Close();
+
+ nsTArray<RefPtr<mozilla::MediaTrack>> tracks;
+ // If mTracksAreSuspended or mCloseCalled are true then we already suspended
+ // all our tracks, so don't suspend them again. But we still need to do
+ // ApplyAudioContextOperation to ensure our new promise is resolved.
+ if (!mTracksAreSuspended && !mCloseCalled) {
+ tracks = GetAllTracks();
+ }
+ auto promise = Graph()->ApplyAudioContextOperation(
+ ds, std::move(tracks), AudioContextOperation::Close);
+ if ((aFlags & AudioContextOperationFlags::SendStateChange)) {
+ promise->Then(
+ GetMainThread(), "AudioContext::OnStateChanged",
+ [self = RefPtr<AudioContext>(this),
+ aPromise](AudioContextState aNewState) {
+ self->OnStateChanged(aPromise, aNewState);
+ },
+ [] {}); // Promise may be rejected after graph shutdown.
+ }
+ }
+ mCloseCalled = true;
+ // Release references to active nodes.
+ // Active AudioNodes don't unregister in destructors, at which point the
+ // Node is already unregistered.
+ mActiveNodes.Clear();
+}
+
+void AudioContext::RegisterNode(AudioNode* aNode) {
+ MOZ_ASSERT(!mAllNodes.Contains(aNode));
+ mAllNodes.Insert(aNode);
+}
+
+void AudioContext::UnregisterNode(AudioNode* aNode) {
+ MOZ_ASSERT(mAllNodes.Contains(aNode));
+ mAllNodes.Remove(aNode);
+}
+
+already_AddRefed<Promise> AudioContext::StartRendering(ErrorResult& aRv) {
+ MOZ_ASSERT(mIsOffline, "This should only be called on OfflineAudioContext");
+ RefPtr<Promise> promise = CreatePromise(aRv);
+ if (aRv.Failed() || promise->State() == Promise::PromiseState::Rejected) {
+ return promise.forget();
+ }
+ if (mIsStarted) {
+ aRv.ThrowInvalidStateError("Rendering already started");
+ return nullptr;
+ }
+
+ mIsStarted = true;
+ mDestination->StartRendering(promise);
+
+ OnStateChanged(nullptr, AudioContextState::Running);
+
+ return promise.forget();
+}
+
+unsigned long AudioContext::Length() {
+ MOZ_ASSERT(mIsOffline);
+ return mDestination->Length();
+}
+
+void AudioContext::Mute() const {
+ MOZ_ASSERT(!mIsOffline);
+ if (mDestination) {
+ mDestination->Mute();
+ }
+}
+
+void AudioContext::Unmute() const {
+ MOZ_ASSERT(!mIsOffline);
+ if (mDestination) {
+ mDestination->Unmute();
+ }
+}
+
+void AudioContext::SetParamMapForWorkletName(
+ const nsAString& aName, AudioParamDescriptorMap* aParamMap) {
+ MOZ_ASSERT(!mWorkletParamDescriptors.Contains(aName));
+ Unused << mWorkletParamDescriptors.InsertOrUpdate(
+ aName, std::move(*aParamMap), fallible);
+}
+
+size_t AudioContext::SizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ // AudioNodes are tracked separately because we do not want the AudioContext
+ // to track all of the AudioNodes it creates, so we wouldn't be able to
+ // traverse them from here.
+
+ size_t amount = aMallocSizeOf(this);
+ if (mListener) {
+ amount += mListener->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ amount += mDecodeJobs.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (uint32_t i = 0; i < mDecodeJobs.Length(); ++i) {
+ amount += mDecodeJobs[i]->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ amount += mActiveNodes.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ return amount;
+}
+
+NS_IMETHODIMP
+AudioContext::CollectReports(nsIHandleReportCallback* aHandleReport,
+ nsISupports* aData, bool aAnonymize) {
+ const nsLiteralCString nodeDescription(
+ "Memory used by AudioNode DOM objects (Web Audio).");
+ for (AudioNode* node : mAllNodes) {
+ int64_t amount = node->SizeOfIncludingThis(MallocSizeOf);
+ nsPrintfCString domNodePath("explicit/webaudio/audio-node/%s/dom-nodes",
+ node->NodeType());
+ aHandleReport->Callback(""_ns, domNodePath, KIND_HEAP, UNITS_BYTES, amount,
+ nodeDescription, aData);
+ }
+
+ int64_t amount = SizeOfIncludingThis(MallocSizeOf);
+ MOZ_COLLECT_REPORT("explicit/webaudio/audiocontext", KIND_HEAP, UNITS_BYTES,
+ amount,
+ "Memory used by AudioContext objects (Web Audio).");
+
+ return NS_OK;
+}
+
+BasicWaveFormCache* AudioContext::GetBasicWaveFormCache() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!mBasicWaveFormCache) {
+ mBasicWaveFormCache = new BasicWaveFormCache(SampleRate());
+ }
+ return mBasicWaveFormCache;
+}
+
+void AudioContext::ReportToConsole(uint32_t aErrorFlags,
+ const char* aMsg) const {
+ MOZ_ASSERT(aMsg);
+ Document* doc =
+ GetParentObject() ? GetParentObject()->GetExtantDoc() : nullptr;
+ nsContentUtils::ReportToConsole(aErrorFlags, "Media"_ns, doc,
+ nsContentUtils::eDOM_PROPERTIES, aMsg);
+}
+
+BasicWaveFormCache::BasicWaveFormCache(uint32_t aSampleRate)
+ : mSampleRate(aSampleRate) {
+ MOZ_ASSERT(NS_IsMainThread());
+}
+BasicWaveFormCache::~BasicWaveFormCache() = default;
+
+WebCore::PeriodicWave* BasicWaveFormCache::GetBasicWaveForm(
+ OscillatorType aType) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ if (aType == OscillatorType::Sawtooth) {
+ if (!mSawtooth) {
+ mSawtooth = WebCore::PeriodicWave::createSawtooth(mSampleRate);
+ }
+ return mSawtooth;
+ }
+ if (aType == OscillatorType::Square) {
+ if (!mSquare) {
+ mSquare = WebCore::PeriodicWave::createSquare(mSampleRate);
+ }
+ return mSquare;
+ }
+ if (aType == OscillatorType::Triangle) {
+ if (!mTriangle) {
+ mTriangle = WebCore::PeriodicWave::createTriangle(mSampleRate);
+ }
+ return mTriangle;
+ }
+ MOZ_ASSERT(false, "Not reached");
+ return nullptr;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/AudioContext.h b/dom/media/webaudio/AudioContext.h
new file mode 100644
index 0000000000..81a64a18bf
--- /dev/null
+++ b/dom/media/webaudio/AudioContext.h
@@ -0,0 +1,479 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef AudioContext_h_
+#define AudioContext_h_
+
+#include "X11UndefineNone.h"
+#include "AudioParamDescriptorMap.h"
+#include "mozilla/dom/OfflineAudioContextBinding.h"
+#include "mozilla/dom/AudioContextBinding.h"
+#include "MediaBufferDecoder.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/dom/TypedArray.h"
+#include "mozilla/RelativeTimeline.h"
+#include "mozilla/TypedEnumBits.h"
+#include "mozilla/UniquePtr.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsTHashMap.h"
+#include "nsHashKeys.h"
+#include "nsTHashSet.h"
+#include "js/TypeDecls.h"
+#include "nsIMemoryReporter.h"
+
+namespace WebCore {
+class PeriodicWave;
+} // namespace WebCore
+
+class nsPIDOMWindowInner;
+
+namespace mozilla {
+
+class DOMMediaStream;
+class ErrorResult;
+class MediaTrack;
+class MediaTrackGraph;
+class AudioNodeTrack;
+
+namespace dom {
+
+enum class AudioContextState : uint8_t;
+class AnalyserNode;
+class AudioBuffer;
+class AudioBufferSourceNode;
+class AudioDestinationNode;
+class AudioListener;
+class AudioNode;
+class BiquadFilterNode;
+class BrowsingContext;
+class ChannelMergerNode;
+class ChannelSplitterNode;
+class ConstantSourceNode;
+class ConvolverNode;
+class DelayNode;
+class DynamicsCompressorNode;
+class GainNode;
+class GlobalObject;
+class HTMLMediaElement;
+class IIRFilterNode;
+class MediaElementAudioSourceNode;
+class MediaStreamAudioDestinationNode;
+class MediaStreamAudioSourceNode;
+class MediaStreamTrack;
+class MediaStreamTrackAudioSourceNode;
+class OscillatorNode;
+class PannerNode;
+class ScriptProcessorNode;
+class StereoPannerNode;
+class WaveShaperNode;
+class Worklet;
+class PeriodicWave;
+struct PeriodicWaveConstraints;
+class Promise;
+enum class OscillatorType : uint8_t;
+
+// This is addrefed by the OscillatorNodeEngine on the main thread
+// and then used from the MTG thread.
+// It can be released either from the graph thread or the main thread.
+class BasicWaveFormCache {
+ public:
+ explicit BasicWaveFormCache(uint32_t aSampleRate);
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(BasicWaveFormCache)
+ WebCore::PeriodicWave* GetBasicWaveForm(OscillatorType aType);
+
+ private:
+ ~BasicWaveFormCache();
+ RefPtr<WebCore::PeriodicWave> mSawtooth;
+ RefPtr<WebCore::PeriodicWave> mSquare;
+ RefPtr<WebCore::PeriodicWave> mTriangle;
+ uint32_t mSampleRate;
+};
+
+/* This runnable allows the MTG to notify the main thread when audio is actually
+ * flowing */
+class StateChangeTask final : public Runnable {
+ public:
+ /* This constructor should be used when this event is sent from the main
+ * thread. */
+ StateChangeTask(AudioContext* aAudioContext, void* aPromise,
+ AudioContextState aNewState);
+
+ /* This constructor should be used when this event is sent from the audio
+ * thread. */
+ StateChangeTask(AudioNodeTrack* aTrack, void* aPromise,
+ AudioContextState aNewState);
+
+ NS_IMETHOD Run() override;
+
+ private:
+ RefPtr<AudioContext> mAudioContext;
+ void* mPromise;
+ RefPtr<AudioNodeTrack> mAudioNodeTrack;
+ AudioContextState mNewState;
+};
+
+enum class AudioContextOperation : uint8_t { Suspend, Resume, Close };
+static const char* const kAudioContextOptionsStrings[] = {"Suspend", "Resume",
+ "Close"};
+// When suspending or resuming an AudioContext, some operations have to notify
+// the main thread, so that the Promise is resolved, the state is modified, and
+// the statechanged event is sent. Some other operations don't go back to the
+// main thread, for example when the AudioContext is paused by something that is
+// not caused by the page itself: opening a debugger, breaking on a breakpoint,
+// reloading a document.
+enum class AudioContextOperationFlags { None, SendStateChange };
+MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(AudioContextOperationFlags);
+
+struct AudioContextOptions;
+
+class AudioContext final : public DOMEventTargetHelper,
+ public nsIMemoryReporter,
+ public RelativeTimeline {
+ AudioContext(nsPIDOMWindowInner* aParentWindow, bool aIsOffline,
+ uint32_t aNumberOfChannels = 0, uint32_t aLength = 0,
+ float aSampleRate = 0.0f);
+ ~AudioContext();
+
+ public:
+ typedef uint64_t AudioContextId;
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(AudioContext, DOMEventTargetHelper)
+ MOZ_DEFINE_MALLOC_SIZE_OF(MallocSizeOf)
+
+ nsPIDOMWindowInner* GetParentObject() const { return GetOwner(); }
+
+ nsISerialEventTarget* GetMainThread() const;
+
+ virtual void DisconnectFromOwner() override;
+
+ void OnWindowDestroy(); // idempotent
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ using DOMEventTargetHelper::DispatchTrustedEvent;
+
+ // Constructor for regular AudioContext
+ static already_AddRefed<AudioContext> Constructor(
+ const GlobalObject& aGlobal, const AudioContextOptions& aOptions,
+ ErrorResult& aRv);
+
+ // Constructor for offline AudioContext with options object
+ static already_AddRefed<AudioContext> Constructor(
+ const GlobalObject& aGlobal, const OfflineAudioContextOptions& aOptions,
+ ErrorResult& aRv);
+
+ // Constructor for offline AudioContext
+ static already_AddRefed<AudioContext> Constructor(const GlobalObject& aGlobal,
+ uint32_t aNumberOfChannels,
+ uint32_t aLength,
+ float aSampleRate,
+ ErrorResult& aRv);
+
+ // AudioContext methods
+
+ AudioDestinationNode* Destination() const { return mDestination; }
+
+ float SampleRate() const { return mSampleRate; }
+
+ bool ShouldSuspendNewTrack() const {
+ return mTracksAreSuspended || mCloseCalled;
+ }
+ double CurrentTime();
+
+ AudioListener* Listener();
+
+ AudioContextState State() const { return mAudioContextState; }
+
+ double BaseLatency() const {
+ // Gecko does not do any buffering between rendering the audio and sending
+ // it to the audio subsystem.
+ return 0.0;
+ }
+
+ double OutputLatency();
+
+ void GetOutputTimestamp(AudioTimestamp& aTimeStamp);
+
+ Worklet* GetAudioWorklet(ErrorResult& aRv);
+
+ bool IsRunning() const;
+
+ // Called when an AudioScheduledSourceNode started or the source node starts,
+ // this method might resume the AudioContext if it was not allowed to start.
+ void StartBlockedAudioContextIfAllowed();
+
+ // Those three methods return a promise to content, that is resolved when an
+ // (possibly long) operation is completed on the MTG (and possibly other)
+ // thread(s). To avoid having to match the calls and asychronous result when
+ // the operation is completed, we keep a reference to the promises on the main
+ // thread, and then send the promises pointers down the MTG thread, as a void*
+ // (to make it very clear that the pointer is to merely be treated as an ID).
+ // When back on the main thread, we can resolve or reject the promise, by
+ // casting it back to a `Promise*` while asserting we're back on the main
+ // thread and removing the reference we added.
+ already_AddRefed<Promise> Suspend(ErrorResult& aRv);
+ already_AddRefed<Promise> Resume(ErrorResult& aRv);
+ already_AddRefed<Promise> Close(ErrorResult& aRv);
+ IMPL_EVENT_HANDLER(statechange)
+
+ // These two functions are similar with Suspend() and Resume(), the difference
+ // is they are designed for calling from chrome side, not content side. eg.
+ // calling from inner window, so we won't need to return promise for caller.
+ void SuspendFromChrome();
+ void ResumeFromChrome();
+ // Called on completion of offline rendering:
+ void OfflineClose();
+
+ already_AddRefed<AudioBufferSourceNode> CreateBufferSource();
+
+ already_AddRefed<ConstantSourceNode> CreateConstantSource();
+
+ already_AddRefed<AudioBuffer> CreateBuffer(uint32_t aNumberOfChannels,
+ uint32_t aLength,
+ float aSampleRate,
+ ErrorResult& aRv);
+
+ already_AddRefed<MediaStreamAudioDestinationNode>
+ CreateMediaStreamDestination(ErrorResult& aRv);
+
+ already_AddRefed<ScriptProcessorNode> CreateScriptProcessor(
+ uint32_t aBufferSize, uint32_t aNumberOfInputChannels,
+ uint32_t aNumberOfOutputChannels, ErrorResult& aRv);
+
+ already_AddRefed<StereoPannerNode> CreateStereoPanner(ErrorResult& aRv);
+
+ already_AddRefed<AnalyserNode> CreateAnalyser(ErrorResult& aRv);
+
+ already_AddRefed<GainNode> CreateGain(ErrorResult& aRv);
+
+ already_AddRefed<WaveShaperNode> CreateWaveShaper(ErrorResult& aRv);
+
+ already_AddRefed<MediaElementAudioSourceNode> CreateMediaElementSource(
+ HTMLMediaElement& aMediaElement, ErrorResult& aRv);
+ already_AddRefed<MediaStreamAudioSourceNode> CreateMediaStreamSource(
+ DOMMediaStream& aMediaStream, ErrorResult& aRv);
+ already_AddRefed<MediaStreamTrackAudioSourceNode>
+ CreateMediaStreamTrackSource(MediaStreamTrack& aMediaStreamTrack,
+ ErrorResult& aRv);
+
+ already_AddRefed<DelayNode> CreateDelay(double aMaxDelayTime,
+ ErrorResult& aRv);
+
+ already_AddRefed<PannerNode> CreatePanner(ErrorResult& aRv);
+
+ already_AddRefed<ConvolverNode> CreateConvolver(ErrorResult& aRv);
+
+ already_AddRefed<ChannelSplitterNode> CreateChannelSplitter(
+ uint32_t aNumberOfOutputs, ErrorResult& aRv);
+
+ already_AddRefed<ChannelMergerNode> CreateChannelMerger(
+ uint32_t aNumberOfInputs, ErrorResult& aRv);
+
+ already_AddRefed<DynamicsCompressorNode> CreateDynamicsCompressor(
+ ErrorResult& aRv);
+
+ already_AddRefed<BiquadFilterNode> CreateBiquadFilter(ErrorResult& aRv);
+
+ already_AddRefed<IIRFilterNode> CreateIIRFilter(
+ const Sequence<double>& aFeedforward, const Sequence<double>& aFeedback,
+ mozilla::ErrorResult& aRv);
+
+ already_AddRefed<OscillatorNode> CreateOscillator(ErrorResult& aRv);
+
+ already_AddRefed<PeriodicWave> CreatePeriodicWave(
+ const Sequence<float>& aRealData, const Sequence<float>& aImagData,
+ const PeriodicWaveConstraints& aConstraints, ErrorResult& aRv);
+
+ already_AddRefed<Promise> DecodeAudioData(
+ const ArrayBuffer& aBuffer,
+ const Optional<OwningNonNull<DecodeSuccessCallback>>& aSuccessCallback,
+ const Optional<OwningNonNull<DecodeErrorCallback>>& aFailureCallback,
+ ErrorResult& aRv);
+
+ // OfflineAudioContext methods
+ already_AddRefed<Promise> StartRendering(ErrorResult& aRv);
+ IMPL_EVENT_HANDLER(complete)
+ unsigned long Length();
+
+ bool IsOffline() const { return mIsOffline; }
+
+ bool ShouldResistFingerprinting() const {
+ return mShouldResistFingerprinting;
+ }
+
+ MediaTrackGraph* Graph() const;
+ AudioNodeTrack* DestinationTrack() const;
+
+ // Nodes register here if they will produce sound even if they have silent
+ // or no input connections. The AudioContext will keep registered nodes
+ // alive until the context is collected. This takes care of "playing"
+ // references and "tail-time" references.
+ void RegisterActiveNode(AudioNode* aNode);
+ // Nodes unregister when they have finished producing sound for the
+ // foreseeable future.
+ // Do NOT call UnregisterActiveNode from an AudioNode destructor.
+ // If the destructor is called, then the Node has already been unregistered.
+ // The destructor may be called during hashtable enumeration, during which
+ // unregistering would not be safe.
+ void UnregisterActiveNode(AudioNode* aNode);
+
+ uint32_t MaxChannelCount() const;
+
+ uint32_t ActiveNodeCount() const;
+
+ void Mute() const;
+ void Unmute() const;
+
+ void RegisterNode(AudioNode* aNode);
+ void UnregisterNode(AudioNode* aNode);
+
+ void OnStateChanged(void* aPromise, AudioContextState aNewState);
+
+ BasicWaveFormCache* GetBasicWaveFormCache();
+
+ void ShutdownWorklet();
+ // Steals from |aParamMap|
+ void SetParamMapForWorkletName(const nsAString& aName,
+ AudioParamDescriptorMap* aParamMap);
+ const AudioParamDescriptorMap* GetParamMapForWorkletName(
+ const nsAString& aName) {
+ return mWorkletParamDescriptors.Lookup(aName).DataPtrOrNull();
+ }
+
+ void Dispatch(already_AddRefed<nsIRunnable>&& aRunnable);
+
+ private:
+ void DisconnectFromWindow();
+ already_AddRefed<Promise> CreatePromise(ErrorResult& aRv);
+ void RemoveFromDecodeQueue(WebAudioDecodeJob* aDecodeJob);
+ void ShutdownDecoder();
+
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+ NS_DECL_NSIMEMORYREPORTER
+
+ friend struct ::mozilla::WebAudioDecodeJob;
+
+ nsTArray<RefPtr<mozilla::MediaTrack>> GetAllTracks() const;
+
+ void ResumeInternal();
+ void SuspendInternal(void* aPromise, AudioContextOperationFlags aFlags);
+ void CloseInternal(void* aPromise, AudioContextOperationFlags aFlags);
+
+ // Will report error message to console and dispatch testing event if needed
+ // when AudioContext is blocked by autoplay policy.
+ void ReportBlocked();
+
+ void ReportToConsole(uint32_t aErrorFlags, const char* aMsg) const;
+
+ // This function should be called everytime we decide whether allow to start
+ // audio context, it's used to update Telemetry related variables.
+ void UpdateAutoplayAssumptionStatus();
+
+ // These functions are used for updating Telemetry.
+ // - MaybeUpdateAutoplayTelemetry: update category 'AllowedAfterBlocked'
+ // - MaybeUpdateAutoplayTelemetryWhenShutdown: update category 'NeverBlocked'
+ // and 'NeverAllowed', so we need to call it when shutdown AudioContext
+ void MaybeUpdateAutoplayTelemetry();
+ void MaybeUpdateAutoplayTelemetryWhenShutdown();
+
+ // If the pref `dom.suspend_inactive.enabled` is enabled, the dom window will
+ // be suspended when the window becomes inactive. In order to keep audio
+ // context running still, we will ask pages to keep awake in that situation.
+ void MaybeUpdatePageAwakeRequest();
+ void MaybeClearPageAwakeRequest();
+ void SetPageAwakeRequest(bool aShouldSet);
+
+ BrowsingContext* GetTopLevelBrowsingContext();
+
+ private:
+ // Each AudioContext has an id, that is passed down the MediaTracks that
+ // back the AudioNodes, so we can easily compute the set of all the
+ // MediaTracks for a given context, on the MediasTrackGraph side.
+ const AudioContextId mId;
+ // Note that it's important for mSampleRate to be initialized before
+ // mDestination, as mDestination's constructor needs to access it!
+ const float mSampleRate;
+ AudioContextState mAudioContextState;
+ RefPtr<AudioDestinationNode> mDestination;
+ RefPtr<AudioListener> mListener;
+ RefPtr<Worklet> mWorklet;
+ nsTArray<UniquePtr<WebAudioDecodeJob>> mDecodeJobs;
+ // This array is used to keep the suspend/close promises alive until
+ // they are resolved, so we can safely pass them accross threads.
+ nsTArray<RefPtr<Promise>> mPromiseGripArray;
+ // This array is used to onlly keep the resume promises alive until they are
+ // resolved, so we can safely pass them accross threads. If the audio context
+ // is not allowed to play, the promise would be pending in this array and be
+ // resolved until audio context has been allowed and user call resume() again.
+ nsTArray<RefPtr<Promise>> mPendingResumePromises;
+ // See RegisterActiveNode. These will keep the AudioContext alive while it
+ // is rendering and the window remains alive.
+ nsTHashSet<RefPtr<AudioNode>> mActiveNodes;
+ // Raw (non-owning) references to all AudioNodes for this AudioContext.
+ nsTHashSet<AudioNode*> mAllNodes;
+ nsTHashMap<nsStringHashKey, AudioParamDescriptorMap> mWorkletParamDescriptors;
+ // Cache to avoid recomputing basic waveforms all the time.
+ RefPtr<BasicWaveFormCache> mBasicWaveFormCache;
+ // Number of channels passed in the OfflineAudioContext ctor.
+ uint32_t mNumberOfChannels;
+ const RTPCallerType mRTPCallerType;
+ const bool mShouldResistFingerprinting;
+ const bool mIsOffline;
+ // true iff realtime or startRendering() has been called.
+ bool mIsStarted;
+ bool mIsShutDown;
+ bool mIsDisconnecting;
+ // Close has been called; reject suspend and resume calls.
+ bool mCloseCalled;
+ // Whether the MediaTracks are suspended, due to one or more of
+ // !mWasAllowedToStart, mSuspendedByContent, or mSuspendedByChrome.
+ // false if offline.
+ bool mTracksAreSuspended;
+ // This flag stores the value of previous status of `allowed-to-start`.
+ // true if offline.
+ bool mWasAllowedToStart;
+ // Whether this AudioContext is suspended because the page called suspend().
+ // Unused if offline.
+ bool mSuspendedByContent;
+ // Whether this AudioContext is suspended because the Window is suspended.
+ // Unused if offline.
+ bool mSuspendedByChrome;
+
+ // These variables are used for telemetry, they're not reflect the actual
+ // status of AudioContext, they are based on the "assumption" of enabling
+ // blocking web audio. Because we want to record Telemetry no matter user
+ // enable blocking autoplay or not.
+ // - 'mWasEverAllowedToStart' would be true when AudioContext had ever been
+ // allowed to start if we enable blocking web audio.
+ // - 'mWasEverBlockedToStart' would be true when AudioContext had ever been
+ // blocked to start if we enable blocking web audio.
+ // - 'mWouldBeAllowedToStart' stores the value of previous status of
+ // `allowed-to-start` if we enable blocking web audio.
+ bool mWasEverAllowedToStart;
+ bool mWasEverBlockedToStart;
+ bool mWouldBeAllowedToStart;
+
+ // Whether we have set the page awake reqeust when non-offline audio context
+ // is running. That will keep the audio context being able to continue running
+ // even if the window is inactive.
+ bool mSetPageAwakeRequest = false;
+};
+
+static const dom::AudioContext::AudioContextId NO_AUDIO_CONTEXT = 0;
+
+} // namespace dom
+} // namespace mozilla
+
+inline nsISupports* ToSupports(mozilla::dom::AudioContext* p) {
+ return NS_CYCLE_COLLECTION_CLASSNAME(mozilla::dom::AudioContext)::Upcast(p);
+}
+
+#endif
diff --git a/dom/media/webaudio/AudioDestinationNode.cpp b/dom/media/webaudio/AudioDestinationNode.cpp
new file mode 100644
index 0000000000..a40e3b5eea
--- /dev/null
+++ b/dom/media/webaudio/AudioDestinationNode.cpp
@@ -0,0 +1,675 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioDestinationNode.h"
+
+#include "AlignmentUtils.h"
+#include "AudibilityMonitor.h"
+#include "AudioChannelService.h"
+#include "AudioContext.h"
+#include "AudioNodeEngine.h"
+#include "AudioNodeTrack.h"
+#include "CubebUtils.h"
+#include "MediaTrackGraph.h"
+#include "mozilla/StaticPrefs_dom.h"
+#include "mozilla/dom/AudioDestinationNodeBinding.h"
+#include "mozilla/dom/BaseAudioContextBinding.h"
+#include "mozilla/dom/OfflineAudioCompletionEvent.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/dom/WakeLock.h"
+#include "mozilla/dom/power/PowerManagerService.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/TelemetryHistogramEnums.h"
+#include "nsContentUtils.h"
+#include "nsIInterfaceRequestorUtils.h"
+#include "nsIScriptObjectPrincipal.h"
+#include "nsServiceManagerUtils.h"
+#include "Tracing.h"
+
+extern mozilla::LazyLogModule gAudioChannelLog;
+
+#define AUDIO_CHANNEL_LOG(msg, ...) \
+ MOZ_LOG(gAudioChannelLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
+
+namespace mozilla::dom {
+
+namespace {
+class OnCompleteTask final : public Runnable {
+ public:
+ OnCompleteTask(AudioContext* aAudioContext, AudioBuffer* aRenderedBuffer)
+ : Runnable("dom::OfflineDestinationNodeEngine::OnCompleteTask"),
+ mAudioContext(aAudioContext),
+ mRenderedBuffer(aRenderedBuffer) {}
+
+ NS_IMETHOD Run() override {
+ OfflineAudioCompletionEventInit param;
+ param.mRenderedBuffer = mRenderedBuffer;
+
+ RefPtr<OfflineAudioCompletionEvent> event =
+ OfflineAudioCompletionEvent::Constructor(mAudioContext, u"complete"_ns,
+ param);
+ mAudioContext->DispatchTrustedEvent(event);
+
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<AudioContext> mAudioContext;
+ RefPtr<AudioBuffer> mRenderedBuffer;
+};
+} // anonymous namespace
+
+class OfflineDestinationNodeEngine final : public AudioNodeEngine {
+ public:
+ explicit OfflineDestinationNodeEngine(AudioDestinationNode* aNode)
+ : AudioNodeEngine(aNode),
+ mWriteIndex(0),
+ mNumberOfChannels(aNode->ChannelCount()),
+ mLength(aNode->Length()),
+ mSampleRate(aNode->Context()->SampleRate()),
+ mBufferAllocated(false) {}
+
+ void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom,
+ const AudioBlock& aInput, AudioBlock* aOutput,
+ bool* aFinished) override {
+ TRACE("OfflineDestinationNodeEngine::ProcessBlock");
+ // Do this just for the sake of political correctness; this output
+ // will not go anywhere.
+ *aOutput = aInput;
+
+ // The output buffer is allocated lazily, on the rendering thread, when
+ // non-null input is received.
+ if (!mBufferAllocated && !aInput.IsNull()) {
+ // These allocations might fail if content provides a huge number of
+ // channels or size, but it's OK since we'll deal with the failure
+ // gracefully.
+ mBuffer = ThreadSharedFloatArrayBufferList::Create(mNumberOfChannels,
+ mLength, fallible);
+ if (mBuffer && mWriteIndex) {
+ // Zero leading for any null chunks that were skipped.
+ for (uint32_t i = 0; i < mNumberOfChannels; ++i) {
+ float* channelData = mBuffer->GetDataForWrite(i);
+ PodZero(channelData, mWriteIndex);
+ }
+ }
+
+ mBufferAllocated = true;
+ }
+
+ // Skip copying if there is no buffer.
+ uint32_t outputChannelCount = mBuffer ? mNumberOfChannels : 0;
+
+ // Record our input buffer
+ MOZ_ASSERT(mWriteIndex < mLength, "How did this happen?");
+ const uint32_t duration =
+ std::min(WEBAUDIO_BLOCK_SIZE, mLength - mWriteIndex);
+ const uint32_t inputChannelCount = aInput.ChannelCount();
+ for (uint32_t i = 0; i < outputChannelCount; ++i) {
+ float* outputData = mBuffer->GetDataForWrite(i) + mWriteIndex;
+ if (aInput.IsNull() || i >= inputChannelCount) {
+ PodZero(outputData, duration);
+ } else {
+ const float* inputBuffer =
+ static_cast<const float*>(aInput.mChannelData[i]);
+ if (duration == WEBAUDIO_BLOCK_SIZE && IS_ALIGNED16(inputBuffer)) {
+ // Use the optimized version of the copy with scale operation
+ AudioBlockCopyChannelWithScale(inputBuffer, aInput.mVolume,
+ outputData);
+ } else {
+ if (aInput.mVolume == 1.0f) {
+ PodCopy(outputData, inputBuffer, duration);
+ } else {
+ for (uint32_t j = 0; j < duration; ++j) {
+ outputData[j] = aInput.mVolume * inputBuffer[j];
+ }
+ }
+ }
+ }
+ }
+ mWriteIndex += duration;
+
+ if (mWriteIndex >= mLength) {
+ NS_ASSERTION(mWriteIndex == mLength, "Overshot length");
+ // Go to finished state. When the graph's current time eventually reaches
+ // the end of the track, then the main thread will be notified and we'll
+ // shut down the AudioContext.
+ *aFinished = true;
+ }
+ }
+
+ bool IsActive() const override {
+ // Keep processing to track track time, which is used for all timelines
+ // associated with the same AudioContext.
+ return true;
+ }
+
+ already_AddRefed<AudioBuffer> CreateAudioBuffer(AudioContext* aContext) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // Create the input buffer
+ ErrorResult rv;
+ RefPtr<AudioBuffer> renderedBuffer =
+ AudioBuffer::Create(aContext->GetOwner(), mNumberOfChannels, mLength,
+ mSampleRate, mBuffer.forget(), rv);
+ if (rv.Failed()) {
+ rv.SuppressException();
+ return nullptr;
+ }
+
+ return renderedBuffer.forget();
+ }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override {
+ size_t amount = AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf);
+ if (mBuffer) {
+ amount += mBuffer->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ return amount;
+ }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ private:
+ // The input to the destination node is recorded in mBuffer.
+ // When this buffer fills up with mLength frames, the buffered input is sent
+ // to the main thread in order to dispatch OfflineAudioCompletionEvent.
+ RefPtr<ThreadSharedFloatArrayBufferList> mBuffer;
+ // An index representing the next offset in mBuffer to be written to.
+ uint32_t mWriteIndex;
+ uint32_t mNumberOfChannels;
+ // How many frames the OfflineAudioContext intends to produce.
+ uint32_t mLength;
+ float mSampleRate;
+ bool mBufferAllocated;
+};
+
+class DestinationNodeEngine final : public AudioNodeEngine {
+ public:
+ explicit DestinationNodeEngine(AudioDestinationNode* aNode)
+ : AudioNodeEngine(aNode),
+ mSampleRate(CubebUtils::PreferredSampleRate(
+ aNode->Context()->ShouldResistFingerprinting())),
+ mVolume(1.0f),
+ mAudibilityMonitor(
+ mSampleRate,
+ StaticPrefs::dom_media_silence_duration_for_audibility()),
+ mSuspended(false),
+ mIsAudible(false) {
+ MOZ_ASSERT(aNode);
+ }
+
+ void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom,
+ const AudioBlock& aInput, AudioBlock* aOutput,
+ bool* aFinished) override {
+ TRACE("DestinationNodeEngine::ProcessBlock");
+ *aOutput = aInput;
+ aOutput->mVolume *= mVolume;
+
+ if (mSuspended) {
+ return;
+ }
+
+ mAudibilityMonitor.Process(aInput);
+ bool isAudible =
+ mAudibilityMonitor.RecentlyAudible() && aOutput->mVolume > 0.0;
+ if (isAudible != mIsAudible) {
+ mIsAudible = isAudible;
+ RefPtr<AudioNodeTrack> track = aTrack;
+ auto r = [track, isAudible]() -> void {
+ MOZ_ASSERT(NS_IsMainThread());
+ RefPtr<AudioNode> node = track->Engine()->NodeMainThread();
+ if (node) {
+ RefPtr<AudioDestinationNode> destinationNode =
+ static_cast<AudioDestinationNode*>(node.get());
+ destinationNode->NotifyDataAudibleStateChanged(isAudible);
+ }
+ };
+
+ aTrack->Graph()->DispatchToMainThreadStableState(NS_NewRunnableFunction(
+ "dom::WebAudioAudibleStateChangedRunnable", r));
+ }
+ }
+
+ bool IsActive() const override {
+ // Keep processing to track track time, which is used for all timelines
+ // associated with the same AudioContext. If there are no other engines
+ // for the AudioContext, then this could return false to suspend the
+ // track, but the track is blocked anyway through
+ // AudioDestinationNode::SetIsOnlyNodeForContext().
+ return true;
+ }
+
+ void SetDoubleParameter(uint32_t aIndex, double aParam) override {
+ if (aIndex == VOLUME) {
+ mVolume = static_cast<float>(aParam);
+ }
+ }
+
+ void SetInt32Parameter(uint32_t aIndex, int32_t aParam) override {
+ if (aIndex == SUSPENDED) {
+ mSuspended = !!aParam;
+ if (mSuspended) {
+ mIsAudible = false;
+ }
+ }
+ }
+
+ enum Parameters {
+ VOLUME,
+ SUSPENDED,
+ };
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ private:
+ int mSampleRate;
+ float mVolume;
+ AudibilityMonitor mAudibilityMonitor;
+ bool mSuspended;
+ bool mIsAudible;
+};
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(AudioDestinationNode, AudioNode,
+ mAudioChannelAgent, mOfflineRenderingPromise)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AudioDestinationNode)
+ NS_INTERFACE_MAP_ENTRY(nsIAudioChannelAgentCallback)
+NS_INTERFACE_MAP_END_INHERITING(AudioNode)
+
+NS_IMPL_ADDREF_INHERITED(AudioDestinationNode, AudioNode)
+NS_IMPL_RELEASE_INHERITED(AudioDestinationNode, AudioNode)
+
+const AudioNodeTrack::Flags kTrackFlags =
+ AudioNodeTrack::NEED_MAIN_THREAD_CURRENT_TIME |
+ AudioNodeTrack::NEED_MAIN_THREAD_ENDED | AudioNodeTrack::EXTERNAL_OUTPUT;
+
+AudioDestinationNode::AudioDestinationNode(AudioContext* aContext,
+ bool aIsOffline,
+ uint32_t aNumberOfChannels,
+ uint32_t aLength)
+ : AudioNode(aContext, aNumberOfChannels, ChannelCountMode::Explicit,
+ ChannelInterpretation::Speakers),
+ mFramesToProduce(aLength),
+ mIsOffline(aIsOffline),
+ mCreatedTime(TimeStamp::Now()) {
+ if (aIsOffline) {
+ // The track is created on demand to avoid creating a graph thread that
+ // may not be used.
+ return;
+ }
+
+ // GetParentObject can return nullptr here. This will end up creating another
+ // MediaTrackGraph
+ MediaTrackGraph* graph = MediaTrackGraph::GetInstance(
+ MediaTrackGraph::AUDIO_THREAD_DRIVER, aContext->GetParentObject(),
+ aContext->SampleRate(), MediaTrackGraph::DEFAULT_OUTPUT_DEVICE);
+ AudioNodeEngine* engine = new DestinationNodeEngine(this);
+
+ mTrack = AudioNodeTrack::Create(aContext, engine, kTrackFlags, graph);
+ mTrack->AddMainThreadListener(this);
+ // null key is fine: only one output per mTrack
+ mTrack->AddAudioOutput(nullptr);
+}
+
+void AudioDestinationNode::Init() {
+ // The reason we don't do that in ctor is because we have to keep AudioContext
+ // holding a strong reference to the destination node first. If we don't do
+ // that, initializing the agent would cause an unexpected destroy of the
+ // destination node when destroying the local weak reference inside
+ // `InitWithWeakCallback()`.
+ if (!mIsOffline) {
+ CreateAndStartAudioChannelAgent();
+ }
+}
+
+void AudioDestinationNode::Close() {
+ DestroyAudioChannelAgentIfExists();
+ ReleaseAudioWakeLockIfExists();
+}
+
+void AudioDestinationNode::CreateAndStartAudioChannelAgent() {
+ MOZ_ASSERT(!mIsOffline);
+ MOZ_ASSERT(!mAudioChannelAgent);
+
+ AudioChannelAgent* agent = new AudioChannelAgent();
+ nsresult rv = agent->InitWithWeakCallback(GetOwner(), this);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ AUDIO_CHANNEL_LOG("Failed to init audio channel agent");
+ return;
+ }
+
+ AudibleState state =
+ IsAudible() ? AudibleState::eAudible : AudibleState::eNotAudible;
+ rv = agent->NotifyStartedPlaying(state);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ AUDIO_CHANNEL_LOG("Failed to start audio channel agent");
+ return;
+ }
+
+ mAudioChannelAgent = agent;
+ mAudioChannelAgent->PullInitialUpdate();
+}
+
+AudioDestinationNode::~AudioDestinationNode() {
+ MOZ_ASSERT(!mAudioChannelAgent);
+ MOZ_ASSERT(!mWakeLock);
+ MOZ_ASSERT(!mCaptureTrackPort);
+}
+
+size_t AudioDestinationNode::SizeOfExcludingThis(
+ MallocSizeOf aMallocSizeOf) const {
+ size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf);
+ // Might be useful in the future:
+ // - mAudioChannelAgent
+ return amount;
+}
+
+size_t AudioDestinationNode::SizeOfIncludingThis(
+ MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
+
+AudioNodeTrack* AudioDestinationNode::Track() {
+ if (mTrack) {
+ return mTrack;
+ }
+
+ AudioContext* context = Context();
+ if (!context) { // This node has been unlinked.
+ return nullptr;
+ }
+
+ MOZ_ASSERT(mIsOffline, "Realtime tracks are created in constructor");
+
+ // GetParentObject can return nullptr here when the document has been
+ // unlinked.
+ MediaTrackGraph* graph = MediaTrackGraph::CreateNonRealtimeInstance(
+ context->SampleRate(), context->GetParentObject());
+ AudioNodeEngine* engine = new OfflineDestinationNodeEngine(this);
+
+ mTrack = AudioNodeTrack::Create(context, engine, kTrackFlags, graph);
+ mTrack->AddMainThreadListener(this);
+
+ return mTrack;
+}
+
+void AudioDestinationNode::DestroyAudioChannelAgentIfExists() {
+ if (mAudioChannelAgent) {
+ mAudioChannelAgent->NotifyStoppedPlaying();
+ mAudioChannelAgent = nullptr;
+ if (IsCapturingAudio()) {
+ StopAudioCapturingTrack();
+ }
+ }
+}
+
+void AudioDestinationNode::DestroyMediaTrack() {
+ Close();
+ if (!mTrack) {
+ return;
+ }
+
+ Context()->ShutdownWorklet();
+
+ mTrack->RemoveMainThreadListener(this);
+ AudioNode::DestroyMediaTrack();
+}
+
+void AudioDestinationNode::NotifyMainThreadTrackEnded() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mTrack->IsEnded());
+
+ if (mIsOffline && GetAbstractMainThread()) {
+ GetAbstractMainThread()->Dispatch(NewRunnableMethod(
+ "dom::AudioDestinationNode::FireOfflineCompletionEvent", this,
+ &AudioDestinationNode::FireOfflineCompletionEvent));
+ }
+}
+
+void AudioDestinationNode::FireOfflineCompletionEvent() {
+ AudioContext* context = Context();
+ context->OfflineClose();
+
+ OfflineDestinationNodeEngine* engine =
+ static_cast<OfflineDestinationNodeEngine*>(Track()->Engine());
+ RefPtr<AudioBuffer> renderedBuffer = engine->CreateAudioBuffer(context);
+ if (!renderedBuffer) {
+ return;
+ }
+ ResolvePromise(renderedBuffer);
+
+ context->Dispatch(do_AddRef(new OnCompleteTask(context, renderedBuffer)));
+
+ context->OnStateChanged(nullptr, AudioContextState::Closed);
+
+ mOfflineRenderingRef.Drop(this);
+}
+
+void AudioDestinationNode::ResolvePromise(AudioBuffer* aRenderedBuffer) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mIsOffline);
+ mOfflineRenderingPromise->MaybeResolve(aRenderedBuffer);
+}
+
+uint32_t AudioDestinationNode::MaxChannelCount() const {
+ return Context()->MaxChannelCount();
+}
+
+void AudioDestinationNode::SetChannelCount(uint32_t aChannelCount,
+ ErrorResult& aRv) {
+ if (aChannelCount > MaxChannelCount()) {
+ aRv.ThrowIndexSizeError(
+ nsPrintfCString("%u is larger than maxChannelCount", aChannelCount));
+ return;
+ }
+
+ if (aChannelCount == ChannelCount()) {
+ return;
+ }
+
+ AudioNode::SetChannelCount(aChannelCount, aRv);
+}
+
+void AudioDestinationNode::Mute() {
+ MOZ_ASSERT(Context() && !Context()->IsOffline());
+ SendDoubleParameterToTrack(DestinationNodeEngine::VOLUME, 0.0f);
+}
+
+void AudioDestinationNode::Unmute() {
+ MOZ_ASSERT(Context() && !Context()->IsOffline());
+ SendDoubleParameterToTrack(DestinationNodeEngine::VOLUME, 1.0f);
+}
+
+void AudioDestinationNode::Suspend() {
+ SendInt32ParameterToTrack(DestinationNodeEngine::SUSPENDED, 1);
+}
+
+void AudioDestinationNode::Resume() {
+ SendInt32ParameterToTrack(DestinationNodeEngine::SUSPENDED, 0);
+}
+
+void AudioDestinationNode::NotifyAudioContextStateChanged() {
+ UpdateFinalAudibleStateIfNeeded(AudibleChangedReasons::ePauseStateChanged);
+}
+
+void AudioDestinationNode::OfflineShutdown() {
+ MOZ_ASSERT(Context() && Context()->IsOffline(),
+ "Should only be called on a valid OfflineAudioContext");
+
+ mOfflineRenderingRef.Drop(this);
+}
+
+JSObject* AudioDestinationNode::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return AudioDestinationNode_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void AudioDestinationNode::StartRendering(Promise* aPromise) {
+ mOfflineRenderingPromise = aPromise;
+ mOfflineRenderingRef.Take(this);
+ Track()->Graph()->StartNonRealtimeProcessing(mFramesToProduce);
+}
+
+NS_IMETHODIMP
+AudioDestinationNode::WindowVolumeChanged(float aVolume, bool aMuted) {
+ MOZ_ASSERT(mAudioChannelAgent);
+ if (!mTrack) {
+ return NS_OK;
+ }
+
+ AUDIO_CHANNEL_LOG(
+ "AudioDestinationNode %p WindowVolumeChanged, "
+ "aVolume = %f, aMuted = %s\n",
+ this, aVolume, aMuted ? "true" : "false");
+
+ mAudioChannelVolume = aMuted ? 0.0f : aVolume;
+ mTrack->SetAudioOutputVolume(nullptr, mAudioChannelVolume);
+ UpdateFinalAudibleStateIfNeeded(AudibleChangedReasons::eVolumeChanged);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AudioDestinationNode::WindowSuspendChanged(nsSuspendedTypes aSuspend) {
+ MOZ_ASSERT(mAudioChannelAgent);
+ if (!mTrack) {
+ return NS_OK;
+ }
+
+ const bool shouldDisable = aSuspend == nsISuspendedTypes::SUSPENDED_BLOCK;
+ if (mAudioChannelDisabled == shouldDisable) {
+ return NS_OK;
+ }
+ mAudioChannelDisabled = shouldDisable;
+
+ AUDIO_CHANNEL_LOG(
+ "AudioDestinationNode %p WindowSuspendChanged, shouldDisable = %d\n",
+ this, mAudioChannelDisabled);
+
+ DisabledTrackMode disabledMode = mAudioChannelDisabled
+ ? DisabledTrackMode::SILENCE_BLACK
+ : DisabledTrackMode::ENABLED;
+ mTrack->SetDisabledTrackMode(disabledMode);
+ UpdateFinalAudibleStateIfNeeded(AudibleChangedReasons::ePauseStateChanged);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+AudioDestinationNode::WindowAudioCaptureChanged(bool aCapture) {
+ MOZ_ASSERT(mAudioChannelAgent);
+ if (!mTrack) {
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsPIDOMWindowInner> ownerWindow = GetOwner();
+ if (!ownerWindow) {
+ return NS_OK;
+ }
+
+ if (aCapture == IsCapturingAudio()) {
+ return NS_OK;
+ }
+
+ if (aCapture) {
+ StartAudioCapturingTrack();
+ } else {
+ StopAudioCapturingTrack();
+ }
+
+ return NS_OK;
+}
+
+bool AudioDestinationNode::IsCapturingAudio() const {
+ return mCaptureTrackPort != nullptr;
+}
+
+void AudioDestinationNode::StartAudioCapturingTrack() {
+ MOZ_ASSERT(!IsCapturingAudio());
+ nsCOMPtr<nsPIDOMWindowInner> window = Context()->GetParentObject();
+ uint64_t id = window->WindowID();
+ mCaptureTrackPort = mTrack->Graph()->ConnectToCaptureTrack(id, mTrack);
+}
+
+void AudioDestinationNode::StopAudioCapturingTrack() {
+ MOZ_ASSERT(IsCapturingAudio());
+ mCaptureTrackPort->Destroy();
+ mCaptureTrackPort = nullptr;
+}
+
+void AudioDestinationNode::CreateAudioWakeLockIfNeeded() {
+ if (!mWakeLock && IsAudible()) {
+ RefPtr<power::PowerManagerService> pmService =
+ power::PowerManagerService::GetInstance();
+ NS_ENSURE_TRUE_VOID(pmService);
+
+ ErrorResult rv;
+ mWakeLock = pmService->NewWakeLock(u"audio-playing"_ns, GetOwner(), rv);
+ }
+}
+
+void AudioDestinationNode::ReleaseAudioWakeLockIfExists() {
+ if (mWakeLock) {
+ IgnoredErrorResult rv;
+ mWakeLock->Unlock(rv);
+ mWakeLock = nullptr;
+ }
+}
+
+void AudioDestinationNode::NotifyDataAudibleStateChanged(bool aAudible) {
+ MOZ_ASSERT(!mIsOffline);
+
+ AUDIO_CHANNEL_LOG(
+ "AudioDestinationNode %p NotifyDataAudibleStateChanged, audible=%d", this,
+ aAudible);
+
+ if (mDurationBeforeFirstTimeAudible.IsZero()) {
+ MOZ_ASSERT(aAudible);
+ mDurationBeforeFirstTimeAudible = TimeStamp::Now() - mCreatedTime;
+ Telemetry::Accumulate(Telemetry::WEB_AUDIO_BECOMES_AUDIBLE_TIME,
+ mDurationBeforeFirstTimeAudible.ToSeconds());
+ }
+
+ mIsDataAudible = aAudible;
+ UpdateFinalAudibleStateIfNeeded(AudibleChangedReasons::eDataAudibleChanged);
+}
+
+void AudioDestinationNode::UpdateFinalAudibleStateIfNeeded(
+ AudibleChangedReasons aReason) {
+ // The audio context has been closed and we've destroyed the agent.
+ if (!mAudioChannelAgent) {
+ return;
+ }
+ const bool newAudibleState = IsAudible();
+ if (mFinalAudibleState == newAudibleState) {
+ return;
+ }
+ AUDIO_CHANNEL_LOG("AudioDestinationNode %p Final audible state=%d", this,
+ newAudibleState);
+ mFinalAudibleState = newAudibleState;
+ AudibleState state =
+ mFinalAudibleState ? AudibleState::eAudible : AudibleState::eNotAudible;
+ mAudioChannelAgent->NotifyStartedAudible(state, aReason);
+ if (mFinalAudibleState) {
+ CreateAudioWakeLockIfNeeded();
+ } else {
+ ReleaseAudioWakeLockIfExists();
+ }
+}
+
+bool AudioDestinationNode::IsAudible() const {
+ // The desitionation node will be regarded as audible if all following
+ // conditions are true.
+ // (1) data audible state : both audio input and output are audible
+ // (2) window audible state : the tab isn't muted by tab sound indicator
+ // (3) audio context state : audio context should be running
+ return Context()->State() == AudioContextState::Running && mIsDataAudible &&
+ mAudioChannelVolume != 0.0;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/AudioDestinationNode.h b/dom/media/webaudio/AudioDestinationNode.h
new file mode 100644
index 0000000000..df4a9b16ec
--- /dev/null
+++ b/dom/media/webaudio/AudioDestinationNode.h
@@ -0,0 +1,133 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef AudioDestinationNode_h_
+#define AudioDestinationNode_h_
+
+#include "AudioChannelService.h"
+#include "AudioNode.h"
+#include "AudioChannelAgent.h"
+#include "mozilla/TimeStamp.h"
+
+namespace mozilla::dom {
+
+class AudioContext;
+class WakeLock;
+
+class AudioDestinationNode final : public AudioNode,
+ public nsIAudioChannelAgentCallback,
+ public MainThreadMediaTrackListener {
+ public:
+ // This node type knows what MediaTrackGraph to use based on
+ // whether it's in offline mode.
+ AudioDestinationNode(AudioContext* aContext, bool aIsOffline,
+ uint32_t aNumberOfChannels, uint32_t aLength);
+
+ void DestroyMediaTrack() override;
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(AudioDestinationNode, AudioNode)
+ NS_DECL_NSIAUDIOCHANNELAGENTCALLBACK
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ uint16_t NumberOfOutputs() const final { return 0; }
+
+ uint32_t MaxChannelCount() const;
+ void SetChannelCount(uint32_t aChannelCount, ErrorResult& aRv) override;
+
+ void Init();
+ void Close();
+
+ // Returns the track or null after unlink.
+ AudioNodeTrack* Track();
+
+ void Mute();
+ void Unmute();
+
+ void Suspend();
+ void Resume();
+
+ void StartRendering(Promise* aPromise);
+
+ void OfflineShutdown();
+
+ void NotifyMainThreadTrackEnded() override;
+ void FireOfflineCompletionEvent();
+
+ const char* NodeType() const override { return "AudioDestinationNode"; }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override;
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override;
+
+ void NotifyDataAudibleStateChanged(bool aAudible);
+ void ResolvePromise(AudioBuffer* aRenderedBuffer);
+
+ unsigned long Length() {
+ MOZ_ASSERT(mIsOffline);
+ return mFramesToProduce;
+ }
+
+ void NotifyAudioContextStateChanged();
+
+ protected:
+ virtual ~AudioDestinationNode();
+
+ private:
+ // This would be created for non-offline audio context in order to receive
+ // tab's mute/suspend/audio capture state change and update the audible state
+ // to the tab.
+ void CreateAndStartAudioChannelAgent();
+ void DestroyAudioChannelAgentIfExists();
+ RefPtr<AudioChannelAgent> mAudioChannelAgent;
+
+ // These members are related to audio capturing. We would start capturing
+ // audio if we're starting capturing audio from whole window, and MUST stop
+ // capturing explicitly when we don't need to capture audio any more, because
+ // we have to release the resource we allocated before.
+ bool IsCapturingAudio() const;
+ void StartAudioCapturingTrack();
+ void StopAudioCapturingTrack();
+ RefPtr<MediaInputPort> mCaptureTrackPort;
+
+ // These members are used to determine if the destination node is actual
+ // audible and `mFinalAudibleState` represents the final result.
+ using AudibleChangedReasons = AudioChannelService::AudibleChangedReasons;
+ using AudibleState = AudioChannelService::AudibleState;
+ void UpdateFinalAudibleStateIfNeeded(AudibleChangedReasons aReason);
+ bool IsAudible() const;
+ bool mFinalAudibleState = false;
+ bool mIsDataAudible = false;
+ float mAudioChannelVolume = 1.0;
+
+ // True if the audio channel disables the track for unvisited tab, and the
+ // track will be enabled again when the tab gets first visited, or a user
+ // presses the tab play icon.
+ bool mAudioChannelDisabled = false;
+
+ // When the destination node is audible, we would request a wakelock to
+ // prevent computer from sleeping in order to keep audio playing.
+ void CreateAudioWakeLockIfNeeded();
+ void ReleaseAudioWakeLockIfExists();
+ RefPtr<WakeLock> mWakeLock;
+
+ SelfReference<AudioDestinationNode> mOfflineRenderingRef;
+ uint32_t mFramesToProduce;
+
+ RefPtr<Promise> mOfflineRenderingPromise;
+
+ bool mIsOffline;
+
+ // These varaibles are used to know how long AudioContext would become audible
+ // since it was created.
+ TimeStamp mCreatedTime;
+ TimeDuration mDurationBeforeFirstTimeAudible;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webaudio/AudioEventTimeline.cpp b/dom/media/webaudio/AudioEventTimeline.cpp
new file mode 100644
index 0000000000..4e6643b08e
--- /dev/null
+++ b/dom/media/webaudio/AudioEventTimeline.cpp
@@ -0,0 +1,369 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioEventTimeline.h"
+#include "AudioNodeTrack.h"
+
+#include "mozilla/ErrorResult.h"
+
+static float LinearInterpolate(double t0, float v0, double t1, float v1,
+ double t) {
+ return v0 + (v1 - v0) * ((t - t0) / (t1 - t0));
+}
+
+static float ExponentialInterpolate(double t0, float v0, double t1, float v1,
+ double t) {
+ return v0 * powf(v1 / v0, (t - t0) / (t1 - t0));
+}
+
+static float ExponentialApproach(double t0, double v0, float v1,
+ double timeConstant, double t) {
+ if (!mozilla::dom::WebAudioUtils::FuzzyEqual(timeConstant, 0.0)) {
+ return v1 + (v0 - v1) * expf(-(t - t0) / timeConstant);
+ } else {
+ return v1;
+ }
+}
+
+static float ExtractValueFromCurve(double startTime, float* aCurve,
+ uint32_t aCurveLength, double duration,
+ double t) {
+ if (t >= startTime + duration) {
+ // After the duration, return the last curve value
+ return aCurve[aCurveLength - 1];
+ }
+ double ratio = std::max((t - startTime) / duration, 0.0);
+ if (ratio >= 1.0) {
+ return aCurve[aCurveLength - 1];
+ }
+ uint32_t current = uint32_t(floor((aCurveLength - 1) * ratio));
+ uint32_t next = current + 1;
+ double step = duration / double(aCurveLength - 1);
+ if (next < aCurveLength) {
+ double t0 = current * step;
+ double t1 = next * step;
+ return LinearInterpolate(t0, aCurve[current], t1, aCurve[next],
+ t - startTime);
+ } else {
+ return aCurve[current];
+ }
+}
+
+namespace mozilla::dom {
+
+AudioTimelineEvent::AudioTimelineEvent(Type aType, double aTime, float aValue,
+ double aTimeConstant, double aDuration,
+ const float* aCurve,
+ uint32_t aCurveLength)
+ : mType(aType),
+ mCurve(nullptr),
+ mTimeConstant(aTimeConstant),
+ mDuration(aDuration)
+#ifdef DEBUG
+ ,
+ mTimeIsInTicks(false)
+#endif
+{
+ mTime = aTime;
+ if (aType == AudioTimelineEvent::SetValueCurve) {
+ SetCurveParams(aCurve, aCurveLength);
+ } else {
+ mValue = aValue;
+ }
+}
+
+AudioTimelineEvent::AudioTimelineEvent(AudioNodeTrack* aTrack)
+ : mType(Track),
+ mCurve(nullptr),
+ mTrack(aTrack),
+ mTimeConstant(0.0),
+ mDuration(0.0)
+#ifdef DEBUG
+ ,
+ mTimeIsInTicks(false)
+#endif
+ ,
+ mTime(0.0) {
+}
+
+AudioTimelineEvent::AudioTimelineEvent(const AudioTimelineEvent& rhs) {
+ PodCopy(this, &rhs, 1);
+
+ if (rhs.mType == AudioTimelineEvent::SetValueCurve) {
+ SetCurveParams(rhs.mCurve, rhs.mCurveLength);
+ } else if (rhs.mType == AudioTimelineEvent::Track) {
+ new (&mTrack) decltype(mTrack)(rhs.mTrack);
+ }
+}
+
+AudioTimelineEvent::~AudioTimelineEvent() {
+ if (mType == AudioTimelineEvent::SetValueCurve) {
+ delete[] mCurve;
+ }
+}
+
+// This method computes the AudioParam value at a given time based on the event
+// timeline
+template <class TimeType>
+void AudioEventTimeline::GetValuesAtTimeHelper(TimeType aTime, float* aBuffer,
+ const size_t aSize) {
+ MOZ_ASSERT(aBuffer);
+ MOZ_ASSERT(aSize);
+
+ auto TimeOf = [](const AudioTimelineEvent& aEvent) -> TimeType {
+ return aEvent.Time<TimeType>();
+ };
+
+ size_t eventIndex = 0;
+ const AudioTimelineEvent* previous = nullptr;
+
+ // Let's remove old events except the last one: we need it to calculate some
+ // curves.
+ CleanupEventsOlderThan(aTime);
+
+ for (size_t bufferIndex = 0; bufferIndex < aSize; ++bufferIndex, ++aTime) {
+ bool timeMatchesEventIndex = false;
+ const AudioTimelineEvent* next;
+ for (;; ++eventIndex) {
+ if (eventIndex >= mEvents.Length()) {
+ next = nullptr;
+ break;
+ }
+
+ next = &mEvents[eventIndex];
+ if (aTime < TimeOf(*next)) {
+ break;
+ }
+
+#ifdef DEBUG
+ MOZ_ASSERT(next->mType == AudioTimelineEvent::SetValueAtTime ||
+ next->mType == AudioTimelineEvent::SetTarget ||
+ next->mType == AudioTimelineEvent::LinearRamp ||
+ next->mType == AudioTimelineEvent::ExponentialRamp ||
+ next->mType == AudioTimelineEvent::SetValueCurve);
+#endif
+
+ if (TimesEqual(aTime, TimeOf(*next))) {
+ mLastComputedValue = mComputedValue;
+ // Find the last event with the same time
+ while (eventIndex < mEvents.Length() - 1 &&
+ TimesEqual(aTime, TimeOf(mEvents[eventIndex + 1]))) {
+ mLastComputedValue =
+ GetValueAtTimeOfEvent<TimeType>(&mEvents[eventIndex]);
+ ++eventIndex;
+ }
+
+ timeMatchesEventIndex = true;
+ break;
+ }
+
+ previous = next;
+ }
+
+ if (timeMatchesEventIndex) {
+ // The time matches one of the events exactly.
+ MOZ_ASSERT(TimesEqual(aTime, TimeOf(mEvents[eventIndex])));
+ mComputedValue = GetValueAtTimeOfEvent<TimeType>(&mEvents[eventIndex]);
+ } else {
+ mComputedValue = GetValuesAtTimeHelperInternal(aTime, previous, next);
+ }
+
+ aBuffer[bufferIndex] = mComputedValue;
+ }
+}
+template void AudioEventTimeline::GetValuesAtTimeHelper(double aTime,
+ float* aBuffer,
+ const size_t aSize);
+template void AudioEventTimeline::GetValuesAtTimeHelper(int64_t aTime,
+ float* aBuffer,
+ const size_t aSize);
+
+template <class TimeType>
+float AudioEventTimeline::GetValueAtTimeOfEvent(
+ const AudioTimelineEvent* aNext) {
+ TimeType time = aNext->Time<TimeType>();
+ switch (aNext->mType) {
+ case AudioTimelineEvent::SetTarget:
+ // SetTarget nodes can be handled no matter what their next node is
+ // (if they have one).
+ // Follow the curve, without regard to the next event, starting at
+ // the last value of the last event.
+ return ExponentialApproach(time, mLastComputedValue, aNext->mValue,
+ aNext->mTimeConstant, time);
+ break;
+ case AudioTimelineEvent::SetValueCurve:
+ // SetValueCurve events can be handled no matter what their event
+ // node is (if they have one)
+ return ExtractValueFromCurve(time, aNext->mCurve, aNext->mCurveLength,
+ aNext->mDuration, time);
+ break;
+ default:
+ // For other event types
+ return aNext->mValue;
+ }
+}
+
+template <class TimeType>
+float AudioEventTimeline::GetValuesAtTimeHelperInternal(
+ TimeType aTime, const AudioTimelineEvent* aPrevious,
+ const AudioTimelineEvent* aNext) {
+ // If the requested time is before all of the existing events
+ if (!aPrevious) {
+ return mValue;
+ }
+
+ // If this event is a curve event, this returns the end time of the curve.
+ // Otherwise, this returns the time of the event.
+ auto TimeOf = [](const AudioTimelineEvent* aEvent) -> TimeType {
+ if (aEvent->mType == AudioTimelineEvent::SetValueCurve) {
+ return aEvent->Time<TimeType>() + aEvent->mDuration;
+ }
+ return aEvent->Time<TimeType>();
+ };
+
+ // Value for an event. For a ValueCurve event, this is the value of the last
+ // element of the curve.
+ auto ValueOf = [](const AudioTimelineEvent* aEvent) -> float {
+ if (aEvent->mType == AudioTimelineEvent::SetValueCurve) {
+ return aEvent->mCurve[aEvent->mCurveLength - 1];
+ }
+ return aEvent->mValue;
+ };
+
+ // SetTarget nodes can be handled no matter what their next node is (if
+ // they have one)
+ if (aPrevious->mType == AudioTimelineEvent::SetTarget) {
+ return ExponentialApproach(TimeOf(aPrevious), mLastComputedValue,
+ ValueOf(aPrevious), aPrevious->mTimeConstant,
+ aTime);
+ }
+
+ // SetValueCurve events can be handled no matter what their next node is
+ // (if they have one), when aTime is in the curve region.
+ if (aPrevious->mType == AudioTimelineEvent::SetValueCurve &&
+ aTime <= aPrevious->Time<TimeType>() + aPrevious->mDuration) {
+ return ExtractValueFromCurve(aPrevious->Time<TimeType>(), aPrevious->mCurve,
+ aPrevious->mCurveLength, aPrevious->mDuration,
+ aTime);
+ }
+
+ // If the requested time is after all of the existing events
+ if (!aNext) {
+ switch (aPrevious->mType) {
+ case AudioTimelineEvent::SetValueAtTime:
+ case AudioTimelineEvent::LinearRamp:
+ case AudioTimelineEvent::ExponentialRamp:
+ // The value will be constant after the last event
+ return aPrevious->mValue;
+ case AudioTimelineEvent::SetValueCurve:
+ return ExtractValueFromCurve(aPrevious->Time<TimeType>(),
+ aPrevious->mCurve, aPrevious->mCurveLength,
+ aPrevious->mDuration, aTime);
+ case AudioTimelineEvent::SetTarget:
+ MOZ_FALLTHROUGH_ASSERT("AudioTimelineEvent::SetTarget");
+ case AudioTimelineEvent::SetValue:
+ case AudioTimelineEvent::Cancel:
+ case AudioTimelineEvent::Track:
+ MOZ_ASSERT(false, "Should have been handled earlier.");
+ }
+ MOZ_ASSERT(false, "unreached");
+ }
+
+ // Finally, handle the case where we have both a previous and a next event
+
+ // First, handle the case where our range ends up in a ramp event
+ switch (aNext->mType) {
+ case AudioTimelineEvent::LinearRamp:
+ return LinearInterpolate(TimeOf(aPrevious), ValueOf(aPrevious),
+ TimeOf(aNext), ValueOf(aNext), aTime);
+
+ case AudioTimelineEvent::ExponentialRamp:
+ return ExponentialInterpolate(TimeOf(aPrevious), ValueOf(aPrevious),
+ TimeOf(aNext), ValueOf(aNext), aTime);
+
+ case AudioTimelineEvent::SetValueAtTime:
+ case AudioTimelineEvent::SetTarget:
+ case AudioTimelineEvent::SetValueCurve:
+ break;
+ case AudioTimelineEvent::SetValue:
+ case AudioTimelineEvent::Cancel:
+ case AudioTimelineEvent::Track:
+ MOZ_ASSERT(false, "Should have been handled earlier.");
+ }
+
+ // Now handle all other cases
+ switch (aPrevious->mType) {
+ case AudioTimelineEvent::SetValueAtTime:
+ case AudioTimelineEvent::LinearRamp:
+ case AudioTimelineEvent::ExponentialRamp:
+ // If the next event type is neither linear or exponential ramp, the
+ // value is constant.
+ return aPrevious->mValue;
+ case AudioTimelineEvent::SetValueCurve:
+ return ExtractValueFromCurve(aPrevious->Time<TimeType>(),
+ aPrevious->mCurve, aPrevious->mCurveLength,
+ aPrevious->mDuration, aTime);
+ case AudioTimelineEvent::SetTarget:
+ MOZ_FALLTHROUGH_ASSERT("AudioTimelineEvent::SetTarget");
+ case AudioTimelineEvent::SetValue:
+ case AudioTimelineEvent::Cancel:
+ case AudioTimelineEvent::Track:
+ MOZ_ASSERT(false, "Should have been handled earlier.");
+ }
+
+ MOZ_ASSERT(false, "unreached");
+ return 0.0f;
+}
+template float AudioEventTimeline::GetValuesAtTimeHelperInternal(
+ double aTime, const AudioTimelineEvent* aPrevious,
+ const AudioTimelineEvent* aNext);
+template float AudioEventTimeline::GetValuesAtTimeHelperInternal(
+ int64_t aTime, const AudioTimelineEvent* aPrevious,
+ const AudioTimelineEvent* aNext);
+
+const AudioTimelineEvent* AudioEventTimeline::GetPreviousEvent(
+ double aTime) const {
+ const AudioTimelineEvent* previous = nullptr;
+ const AudioTimelineEvent* next = nullptr;
+
+ auto TimeOf = [](const AudioTimelineEvent& aEvent) -> double {
+ return aEvent.Time<double>();
+ };
+
+ bool bailOut = false;
+ for (unsigned i = 0; !bailOut && i < mEvents.Length(); ++i) {
+ switch (mEvents[i].mType) {
+ case AudioTimelineEvent::SetValueAtTime:
+ case AudioTimelineEvent::SetTarget:
+ case AudioTimelineEvent::LinearRamp:
+ case AudioTimelineEvent::ExponentialRamp:
+ case AudioTimelineEvent::SetValueCurve:
+ if (aTime == TimeOf(mEvents[i])) {
+ // Find the last event with the same time
+ do {
+ ++i;
+ } while (i < mEvents.Length() && aTime == TimeOf(mEvents[i]));
+ return &mEvents[i - 1];
+ }
+ previous = next;
+ next = &mEvents[i];
+ if (aTime < TimeOf(mEvents[i])) {
+ bailOut = true;
+ }
+ break;
+ default:
+ MOZ_ASSERT(false, "unreached");
+ }
+ }
+ // Handle the case where the time is past all of the events
+ if (!bailOut) {
+ previous = next;
+ }
+
+ return previous;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/AudioEventTimeline.h b/dom/media/webaudio/AudioEventTimeline.h
new file mode 100644
index 0000000000..7a136df245
--- /dev/null
+++ b/dom/media/webaudio/AudioEventTimeline.h
@@ -0,0 +1,387 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef AudioEventTimeline_h_
+#define AudioEventTimeline_h_
+
+#include <algorithm>
+#include "mozilla/Assertions.h"
+#include "mozilla/FloatingPoint.h"
+#include "mozilla/PodOperations.h"
+#include "mozilla/ErrorResult.h"
+
+#include "MainThreadUtils.h"
+#include "nsTArray.h"
+#include "math.h"
+#include "WebAudioUtils.h"
+
+// XXX Avoid including this here by moving function bodies to the cpp file
+#include "js/GCAPI.h"
+
+namespace mozilla {
+
+class AudioNodeTrack;
+
+namespace dom {
+
+struct AudioTimelineEvent final {
+ enum Type : uint32_t {
+ SetValue,
+ SetValueAtTime,
+ LinearRamp,
+ ExponentialRamp,
+ SetTarget,
+ SetValueCurve,
+ Track,
+ Cancel
+ };
+
+ AudioTimelineEvent(Type aType, double aTime, float aValue,
+ double aTimeConstant = 0.0, double aDuration = 0.0,
+ const float* aCurve = nullptr, uint32_t aCurveLength = 0);
+ explicit AudioTimelineEvent(AudioNodeTrack* aTrack);
+ AudioTimelineEvent(const AudioTimelineEvent& rhs);
+ ~AudioTimelineEvent();
+
+ template <class TimeType>
+ TimeType Time() const;
+
+ void SetTimeInTicks(int64_t aTimeInTicks) {
+ mTimeInTicks = aTimeInTicks;
+#ifdef DEBUG
+ mTimeIsInTicks = true;
+#endif
+ }
+
+ void SetCurveParams(const float* aCurve, uint32_t aCurveLength) {
+ mCurveLength = aCurveLength;
+ if (aCurveLength) {
+ mCurve = new float[aCurveLength];
+ PodCopy(mCurve, aCurve, aCurveLength);
+ } else {
+ mCurve = nullptr;
+ }
+ }
+
+ Type mType;
+ union {
+ float mValue;
+ uint32_t mCurveLength;
+ };
+ // mCurve contains a buffer of SetValueCurve samples. We sample the
+ // values in the buffer depending on how far along we are in time.
+ // If we're at time T and the event has started as time T0 and has a
+ // duration of D, we sample the buffer at floor(mCurveLength*(T-T0)/D)
+ // if T<T0+D, and just take the last sample in the buffer otherwise.
+ float* mCurve;
+ RefPtr<AudioNodeTrack> mTrack;
+ double mTimeConstant;
+ double mDuration;
+#ifdef DEBUG
+ bool mTimeIsInTicks;
+#endif
+
+ private:
+ // This member is accessed using the `Time` method, for safety.
+ //
+ // The time for an event can either be in absolute value or in ticks.
+ // Initially the time of the event is always in absolute value.
+ // In order to convert it to ticks, call SetTimeInTicks. Once this
+ // method has been called for an event, the time cannot be converted
+ // back to absolute value.
+ union {
+ double mTime;
+ int64_t mTimeInTicks;
+ };
+};
+
+template <>
+inline double AudioTimelineEvent::Time<double>() const {
+ MOZ_ASSERT(!mTimeIsInTicks);
+ return mTime;
+}
+
+template <>
+inline int64_t AudioTimelineEvent::Time<int64_t>() const {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(mTimeIsInTicks);
+ return mTimeInTicks;
+}
+
+class AudioEventTimeline {
+ public:
+ explicit AudioEventTimeline(float aDefaultValue)
+ : mValue(aDefaultValue),
+ mComputedValue(aDefaultValue),
+ mLastComputedValue(aDefaultValue) {}
+
+ bool ValidateEvent(const AudioTimelineEvent& aEvent, ErrorResult& aRv) const {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ auto TimeOf = [](const AudioTimelineEvent& aEvent) -> double {
+ return aEvent.Time<double>();
+ };
+
+ // Validate the event itself
+ if (!WebAudioUtils::IsTimeValid(TimeOf(aEvent))) {
+ aRv.ThrowRangeError<MSG_INVALID_AUDIOPARAM_METHOD_START_TIME_ERROR>();
+ return false;
+ }
+ if (!WebAudioUtils::IsTimeValid(aEvent.mTimeConstant)) {
+ aRv.ThrowRangeError(
+ "The exponential constant passed to setTargetAtTime must be "
+ "non-negative.");
+ return false;
+ }
+
+ if (aEvent.mType == AudioTimelineEvent::SetValueCurve) {
+ if (!aEvent.mCurve || aEvent.mCurveLength < 2) {
+ aRv.ThrowInvalidStateError("Curve length must be at least 2");
+ return false;
+ }
+ if (aEvent.mDuration <= 0) {
+ aRv.ThrowRangeError(
+ "The curve duration for setValueCurveAtTime must be strictly "
+ "positive.");
+ return false;
+ }
+ }
+
+ MOZ_ASSERT(IsValid(aEvent.mValue) && IsValid(aEvent.mDuration));
+
+ // Make sure that new events don't fall within the duration of a
+ // curve event.
+ for (unsigned i = 0; i < mEvents.Length(); ++i) {
+ if (mEvents[i].mType == AudioTimelineEvent::SetValueCurve &&
+ TimeOf(mEvents[i]) <= TimeOf(aEvent) &&
+ TimeOf(mEvents[i]) + mEvents[i].mDuration > TimeOf(aEvent)) {
+ aRv.ThrowNotSupportedError("Can't add events during a curve event");
+ return false;
+ }
+ }
+
+ // Make sure that new curve events don't fall in a range which includes
+ // other events.
+ if (aEvent.mType == AudioTimelineEvent::SetValueCurve) {
+ for (unsigned i = 0; i < mEvents.Length(); ++i) {
+ if (TimeOf(aEvent) < TimeOf(mEvents[i]) &&
+ TimeOf(aEvent) + aEvent.mDuration > TimeOf(mEvents[i])) {
+ aRv.ThrowNotSupportedError(
+ "Can't add curve events that overlap other events");
+ return false;
+ }
+ }
+ }
+
+ // Make sure that invalid values are not used for exponential curves
+ if (aEvent.mType == AudioTimelineEvent::ExponentialRamp) {
+ if (aEvent.mValue <= 0.f) {
+ aRv.ThrowRangeError(
+ "The value passed to exponentialRampToValueAtTime must be "
+ "positive.");
+ return false;
+ }
+ const AudioTimelineEvent* previousEvent =
+ GetPreviousEvent(TimeOf(aEvent));
+ if (previousEvent) {
+ if (previousEvent->mValue <= 0.f) {
+ // XXXbz I see no mention of SyntaxError in the Web Audio API spec
+ aRv.ThrowSyntaxError("Previous event value must be positive");
+ return false;
+ }
+ } else {
+ if (mValue <= 0.f) {
+ // XXXbz I see no mention of SyntaxError in the Web Audio API spec
+ aRv.ThrowSyntaxError("Our value must be positive");
+ return false;
+ }
+ }
+ }
+ return true;
+ }
+
+ template <typename TimeType>
+ void InsertEvent(const AudioTimelineEvent& aEvent) {
+ for (unsigned i = 0; i < mEvents.Length(); ++i) {
+ if (aEvent.Time<TimeType>() == mEvents[i].Time<TimeType>()) {
+ // If two events happen at the same time, have them in chronological
+ // order of insertion.
+ do {
+ ++i;
+ } while (i < mEvents.Length() &&
+ aEvent.Time<TimeType>() == mEvents[i].Time<TimeType>());
+ mEvents.InsertElementAt(i, aEvent);
+ return;
+ }
+ // Otherwise, place the event right after the latest existing event
+ if (aEvent.Time<TimeType>() < mEvents[i].Time<TimeType>()) {
+ mEvents.InsertElementAt(i, aEvent);
+ return;
+ }
+ }
+
+ // If we couldn't find a place for the event, just append it to the list
+ mEvents.AppendElement(aEvent);
+ }
+
+ bool HasSimpleValue() const { return mEvents.IsEmpty(); }
+
+ float GetValue() const {
+ // This method should only be called if HasSimpleValue() returns true
+ MOZ_ASSERT(HasSimpleValue());
+ return mValue;
+ }
+
+ void SetValue(float aValue) {
+ // Silently don't change anything if there are any events
+ if (mEvents.IsEmpty()) {
+ mLastComputedValue = mComputedValue = mValue = aValue;
+ }
+ }
+
+ void SetValueAtTime(float aValue, double aStartTime, ErrorResult& aRv) {
+ AudioTimelineEvent event(AudioTimelineEvent::SetValueAtTime, aStartTime,
+ aValue);
+
+ if (ValidateEvent(event, aRv)) {
+ InsertEvent<double>(event);
+ }
+ }
+
+ void LinearRampToValueAtTime(float aValue, double aEndTime,
+ ErrorResult& aRv) {
+ AudioTimelineEvent event(AudioTimelineEvent::LinearRamp, aEndTime, aValue);
+
+ if (ValidateEvent(event, aRv)) {
+ InsertEvent<double>(event);
+ }
+ }
+
+ void ExponentialRampToValueAtTime(float aValue, double aEndTime,
+ ErrorResult& aRv) {
+ AudioTimelineEvent event(AudioTimelineEvent::ExponentialRamp, aEndTime,
+ aValue);
+
+ if (ValidateEvent(event, aRv)) {
+ InsertEvent<double>(event);
+ }
+ }
+
+ void SetTargetAtTime(float aTarget, double aStartTime, double aTimeConstant,
+ ErrorResult& aRv) {
+ AudioTimelineEvent event(AudioTimelineEvent::SetTarget, aStartTime, aTarget,
+ aTimeConstant);
+
+ if (ValidateEvent(event, aRv)) {
+ InsertEvent<double>(event);
+ }
+ }
+
+ void SetValueCurveAtTime(const float* aValues, uint32_t aValuesLength,
+ double aStartTime, double aDuration,
+ ErrorResult& aRv) {
+ AudioTimelineEvent event(AudioTimelineEvent::SetValueCurve, aStartTime,
+ 0.0f, 0.0f, aDuration, aValues, aValuesLength);
+ if (ValidateEvent(event, aRv)) {
+ InsertEvent<double>(event);
+ }
+ }
+
+ template <typename TimeType>
+ void CancelScheduledValues(TimeType aStartTime) {
+ for (unsigned i = 0; i < mEvents.Length(); ++i) {
+ if (mEvents[i].Time<TimeType>() >= aStartTime) {
+#ifdef DEBUG
+ // Sanity check: the array should be sorted, so all of the following
+ // events should have a time greater than aStartTime too.
+ for (unsigned j = i + 1; j < mEvents.Length(); ++j) {
+ MOZ_ASSERT(mEvents[j].Time<TimeType>() >= aStartTime);
+ }
+#endif
+ mEvents.TruncateLength(i);
+ break;
+ }
+ }
+ }
+
+ void CancelAllEvents() { mEvents.Clear(); }
+
+ static bool TimesEqual(int64_t aLhs, int64_t aRhs) { return aLhs == aRhs; }
+
+ // Since we are going to accumulate error by adding 0.01 multiple time in a
+ // loop, we want to fuzz the equality check in GetValueAtTime.
+ static bool TimesEqual(double aLhs, double aRhs) {
+ const float kEpsilon = 0.0000000001f;
+ return fabs(aLhs - aRhs) < kEpsilon;
+ }
+
+ template <class TimeType>
+ float GetValueAtTime(TimeType aTime) {
+ float result;
+ GetValuesAtTimeHelper(aTime, &result, 1);
+ return result;
+ }
+
+ template <class TimeType>
+ void GetValuesAtTime(TimeType aTime, float* aBuffer, const size_t aSize) {
+ MOZ_ASSERT(aBuffer);
+ GetValuesAtTimeHelper(aTime, aBuffer, aSize);
+ }
+
+ // Return the number of events scheduled
+ uint32_t GetEventCount() const { return mEvents.Length(); }
+
+ template <class TimeType>
+ void CleanupEventsOlderThan(TimeType aTime) {
+ while (mEvents.Length() > 1 && aTime > mEvents[1].Time<TimeType>()) {
+ if (mEvents[1].mType == AudioTimelineEvent::SetTarget) {
+ mLastComputedValue = GetValuesAtTimeHelperInternal(
+ mEvents[1].Time<TimeType>(), &mEvents[0], nullptr);
+ }
+
+ MOZ_ASSERT(!mEvents[0].mTrack,
+ "AudioParam tracks should never be destroyed on the real-time "
+ "thread.");
+ JS::AutoSuppressGCAnalysis suppress;
+ mEvents.RemoveElementAt(0);
+ }
+ }
+
+ private:
+ template <class TimeType>
+ void GetValuesAtTimeHelper(TimeType aTime, float* aBuffer,
+ const size_t aSize);
+
+ template <class TimeType>
+ float GetValueAtTimeOfEvent(const AudioTimelineEvent* aNext);
+
+ template <class TimeType>
+ float GetValuesAtTimeHelperInternal(TimeType aTime,
+ const AudioTimelineEvent* aPrevious,
+ const AudioTimelineEvent* aNext);
+
+ const AudioTimelineEvent* GetPreviousEvent(double aTime) const;
+
+ static bool IsValid(double value) { return std::isfinite(value); }
+
+ // This is a sorted array of the events in the timeline. Queries of this
+ // data structure should probably be more frequent than modifications to it,
+ // and that is the reason why we're using a simple array as the data
+ // structure. We can optimize this in the future if the performance of the
+ // array ends up being a bottleneck.
+ nsTArray<AudioTimelineEvent> mEvents;
+ float mValue;
+ // This is the value of this AudioParam we computed at the last tick.
+ float mComputedValue;
+ // This is the value of this AudioParam at the last tick of the previous
+ // event.
+ float mLastComputedValue;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webaudio/AudioListener.cpp b/dom/media/webaudio/AudioListener.cpp
new file mode 100644
index 0000000000..2e00af9764
--- /dev/null
+++ b/dom/media/webaudio/AudioListener.cpp
@@ -0,0 +1,136 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioContext.h"
+#include "AudioListener.h"
+#include "MediaTrackGraphImpl.h"
+#include "Tracing.h"
+#include "mozilla/dom/AudioListenerBinding.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(AudioListener, mContext)
+
+AudioListenerEngine::AudioListenerEngine()
+ : mPosition(), mFrontVector(0., 0., -1.), mRightVector(1., 0., 0.) {}
+
+void AudioListenerEngine::RecvListenerEngineEvent(
+ AudioListenerEngine::AudioListenerParameter aParameter,
+ const ThreeDPoint& aValue) {
+ switch (aParameter) {
+ case AudioListenerParameter::POSITION:
+ mPosition = aValue;
+ break;
+ case AudioListenerParameter::FRONT:
+ mFrontVector = aValue;
+ break;
+ case AudioListenerParameter::RIGHT:
+ mRightVector = aValue;
+ break;
+ default:
+ MOZ_CRASH("Not handled");
+ }
+}
+
+const ThreeDPoint& AudioListenerEngine::Position() const { return mPosition; }
+const ThreeDPoint& AudioListenerEngine::FrontVector() const {
+ return mFrontVector;
+}
+const ThreeDPoint& AudioListenerEngine::RightVector() const {
+ return mRightVector;
+}
+
+AudioListener::AudioListener(AudioContext* aContext)
+ : mContext(aContext),
+ mEngine(new AudioListenerEngine()),
+ mPosition(),
+ mFrontVector(0., 0., -1.),
+ mRightVector(1., 0., 0.) {
+ MOZ_ASSERT(aContext);
+}
+
+JSObject* AudioListener::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return AudioListener_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void AudioListener::SetOrientation(double aX, double aY, double aZ, double aXUp,
+ double aYUp, double aZUp) {
+ ThreeDPoint front(aX, aY, aZ);
+ // The panning effect and the azimuth and elevation calculation in the Web
+ // Audio spec becomes undefined with linearly dependent vectors, so keep
+ // existing state in these situations.
+ if (front.IsZero()) {
+ return;
+ }
+ // Normalize before using CrossProduct() to avoid overflow.
+ front.Normalize();
+ ThreeDPoint up(aXUp, aYUp, aZUp);
+ if (up.IsZero()) {
+ return;
+ }
+ up.Normalize();
+ ThreeDPoint right = front.CrossProduct(up);
+ if (right.IsZero()) {
+ return;
+ }
+ right.Normalize();
+
+ if (!mFrontVector.FuzzyEqual(front)) {
+ mFrontVector = front;
+ SendListenerEngineEvent(AudioListenerEngine::AudioListenerParameter::FRONT,
+ mFrontVector);
+ }
+ if (!mRightVector.FuzzyEqual(right)) {
+ mRightVector = right;
+ SendListenerEngineEvent(AudioListenerEngine::AudioListenerParameter::RIGHT,
+ mRightVector);
+ }
+}
+
+void AudioListener::SetPosition(double aX, double aY, double aZ) {
+ if (WebAudioUtils::FuzzyEqual(mPosition.x, aX) &&
+ WebAudioUtils::FuzzyEqual(mPosition.y, aY) &&
+ WebAudioUtils::FuzzyEqual(mPosition.z, aZ)) {
+ return;
+ }
+ mPosition.x = aX;
+ mPosition.y = aY;
+ mPosition.z = aZ;
+ SendListenerEngineEvent(AudioListenerEngine::AudioListenerParameter::POSITION,
+ mPosition);
+}
+
+void AudioListener::SendListenerEngineEvent(
+ AudioListenerEngine::AudioListenerParameter aParameter,
+ const ThreeDPoint& aValue) {
+ class Message final : public ControlMessage {
+ public:
+ Message(AudioListenerEngine* aEngine,
+ AudioListenerEngine::AudioListenerParameter aParameter,
+ const ThreeDPoint& aValue)
+ : ControlMessage(nullptr),
+ mEngine(aEngine),
+ mParameter(aParameter),
+ mValue(aValue) {}
+ void Run() override {
+ TRACE("AudioListener::RecvListenerEngineEvent");
+ mEngine->RecvListenerEngineEvent(mParameter, mValue);
+ }
+ RefPtr<AudioListenerEngine> mEngine;
+ AudioListenerEngine::AudioListenerParameter mParameter;
+ ThreeDPoint mValue;
+ };
+
+ mContext->DestinationTrack()->GraphImpl()->AppendMessage(
+ MakeUnique<Message>(Engine(), aParameter, aValue));
+}
+
+size_t AudioListener::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/AudioListener.h b/dom/media/webaudio/AudioListener.h
new file mode 100644
index 0000000000..7d7fad3bfb
--- /dev/null
+++ b/dom/media/webaudio/AudioListener.h
@@ -0,0 +1,85 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef AudioListener_h_
+#define AudioListener_h_
+
+#include "nsWrapperCache.h"
+#include "nsCycleCollectionParticipant.h"
+#include "mozilla/Attributes.h"
+#include "ThreeDPoint.h"
+#include "AudioContext.h"
+#include "PannerNode.h"
+#include "WebAudioUtils.h"
+#include "js/TypeDecls.h"
+#include "mozilla/MemoryReporting.h"
+
+namespace mozilla::dom {
+
+class AudioListenerEngine final {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AudioListenerEngine)
+
+ AudioListenerEngine();
+
+ enum class AudioListenerParameter {
+ POSITION,
+ FRONT, // unit length
+ RIGHT // unit length, orthogonal to FRONT
+ };
+ void RecvListenerEngineEvent(
+ AudioListenerEngine::AudioListenerParameter aParameter,
+ const ThreeDPoint& aValue);
+ const ThreeDPoint& Position() const;
+ const ThreeDPoint& FrontVector() const;
+ const ThreeDPoint& RightVector() const;
+
+ private:
+ ~AudioListenerEngine() = default;
+
+ ThreeDPoint mPosition;
+ ThreeDPoint mFrontVector;
+ ThreeDPoint mRightVector;
+};
+
+class AudioListener final : public nsWrapperCache {
+ public:
+ explicit AudioListener(AudioContext* aContext);
+
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(AudioListener)
+ NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(AudioListener)
+
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ AudioContext* GetParentObject() const { return mContext; }
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ void SetPosition(double aX, double aY, double aZ);
+ void SetOrientation(double aX, double aY, double aZ, double aXUp, double aYUp,
+ double aZUp);
+
+ AudioListenerEngine* Engine() { return mEngine.get(); }
+
+ private:
+ void SendListenerEngineEvent(
+ AudioListenerEngine::AudioListenerParameter aParameter,
+ const ThreeDPoint& aValue);
+
+ ~AudioListener() = default;
+
+ private:
+ RefPtr<AudioContext> mContext;
+ RefPtr<AudioListenerEngine> mEngine;
+ ThreeDPoint mPosition;
+ ThreeDPoint mFrontVector;
+ ThreeDPoint mRightVector;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webaudio/AudioNode.cpp b/dom/media/webaudio/AudioNode.cpp
new file mode 100644
index 0000000000..8bbbffe1b7
--- /dev/null
+++ b/dom/media/webaudio/AudioNode.cpp
@@ -0,0 +1,613 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioNode.h"
+#include "mozilla/ErrorResult.h"
+#include "AudioNodeTrack.h"
+#include "AudioNodeEngine.h"
+#include "mozilla/dom/AudioParam.h"
+#include "mozilla/Services.h"
+#include "nsIObserverService.h"
+
+namespace mozilla::dom {
+
+static const uint32_t INVALID_PORT = 0xffffffff;
+static uint32_t gId = 0;
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(AudioNode)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(AudioNode, DOMEventTargetHelper)
+ tmp->DisconnectFromGraph();
+ if (tmp->mContext) {
+ tmp->mContext->UnregisterNode(tmp);
+ }
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mContext)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mParams)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mOutputNodes)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mOutputParams)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(AudioNode,
+ DOMEventTargetHelper)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mContext)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParams)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOutputNodes)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOutputParams)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_ADDREF_INHERITED(AudioNode, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(AudioNode, DOMEventTargetHelper)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AudioNode)
+ NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+AudioNode::AudioNode(AudioContext* aContext, uint32_t aChannelCount,
+ ChannelCountMode aChannelCountMode,
+ ChannelInterpretation aChannelInterpretation)
+ : DOMEventTargetHelper(aContext->GetParentObject()),
+ mContext(aContext),
+ mChannelCount(aChannelCount),
+ mChannelCountMode(aChannelCountMode),
+ mChannelInterpretation(aChannelInterpretation),
+ mId(gId++),
+ mPassThrough(false),
+ mAbstractMainThread(
+ aContext->GetOwnerGlobal()
+ ? aContext->GetOwnerGlobal()->AbstractMainThreadFor(
+ TaskCategory::Other)
+ : nullptr) {
+ MOZ_ASSERT(aContext);
+ aContext->RegisterNode(this);
+}
+
+AudioNode::~AudioNode() {
+ MOZ_ASSERT(mInputNodes.IsEmpty());
+ MOZ_ASSERT(mOutputNodes.IsEmpty());
+ MOZ_ASSERT(mOutputParams.IsEmpty());
+ MOZ_ASSERT(!mTrack,
+ "The webaudio-node-demise notification must have been sent");
+ if (mContext) {
+ mContext->UnregisterNode(this);
+ }
+}
+
+void AudioNode::Initialize(const AudioNodeOptions& aOptions, ErrorResult& aRv) {
+ if (aOptions.mChannelCount.WasPassed()) {
+ SetChannelCount(aOptions.mChannelCount.Value(), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+ }
+
+ if (aOptions.mChannelCountMode.WasPassed()) {
+ SetChannelCountModeValue(aOptions.mChannelCountMode.Value(), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+ }
+
+ if (aOptions.mChannelInterpretation.WasPassed()) {
+ SetChannelInterpretationValue(aOptions.mChannelInterpretation.Value(), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return;
+ }
+ }
+}
+
+size_t AudioNode::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ // Not owned:
+ // - mContext
+ // - mTrack
+ size_t amount = 0;
+
+ amount += mInputNodes.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (size_t i = 0; i < mInputNodes.Length(); i++) {
+ amount += mInputNodes[i].SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ // Just measure the array. The entire audio node graph is measured via the
+ // MediaTrackGraph's tracks, so we don't want to double-count the elements.
+ amount += mOutputNodes.ShallowSizeOfExcludingThis(aMallocSizeOf);
+
+ amount += mOutputParams.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (size_t i = 0; i < mOutputParams.Length(); i++) {
+ amount += mOutputParams[i]->SizeOfIncludingThis(aMallocSizeOf);
+ }
+
+ return amount;
+}
+
+size_t AudioNode::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
+
+template <class InputNode>
+static size_t FindIndexOfNode(const nsTArray<InputNode>& aInputNodes,
+ const AudioNode* aNode) {
+ for (size_t i = 0; i < aInputNodes.Length(); ++i) {
+ if (aInputNodes[i].mInputNode == aNode) {
+ return i;
+ }
+ }
+ return nsTArray<InputNode>::NoIndex;
+}
+
+template <class InputNode>
+static size_t FindIndexOfNodeWithPorts(const nsTArray<InputNode>& aInputNodes,
+ const AudioNode* aNode,
+ uint32_t aInputPort,
+ uint32_t aOutputPort) {
+ for (size_t i = 0; i < aInputNodes.Length(); ++i) {
+ if (aInputNodes[i].mInputNode == aNode &&
+ aInputNodes[i].mInputPort == aInputPort &&
+ aInputNodes[i].mOutputPort == aOutputPort) {
+ return i;
+ }
+ }
+ return nsTArray<InputNode>::NoIndex;
+}
+
+void AudioNode::DisconnectFromGraph() {
+ MOZ_ASSERT(mRefCnt.get() > mInputNodes.Length(),
+ "Caller should be holding a reference");
+
+ // The idea here is that we remove connections one by one, and at each step
+ // the graph is in a valid state.
+
+ // Disconnect inputs. We don't need them anymore.
+ while (!mInputNodes.IsEmpty()) {
+ InputNode inputNode = mInputNodes.PopLastElement();
+ inputNode.mInputNode->mOutputNodes.RemoveElement(this);
+ }
+
+ while (!mOutputNodes.IsEmpty()) {
+ RefPtr<AudioNode> output = mOutputNodes.PopLastElement();
+ size_t inputIndex = FindIndexOfNode(output->mInputNodes, this);
+ // It doesn't matter which one we remove, since we're going to remove all
+ // entries for this node anyway.
+ output->mInputNodes.RemoveElementAt(inputIndex);
+ // This effects of this connection will remain.
+ output->NotifyHasPhantomInput();
+ }
+
+ while (!mOutputParams.IsEmpty()) {
+ RefPtr<AudioParam> output = mOutputParams.PopLastElement();
+ size_t inputIndex = FindIndexOfNode(output->InputNodes(), this);
+ // It doesn't matter which one we remove, since we're going to remove all
+ // entries for this node anyway.
+ output->RemoveInputNode(inputIndex);
+ }
+
+ DestroyMediaTrack();
+}
+
+AudioNode* AudioNode::Connect(AudioNode& aDestination, uint32_t aOutput,
+ uint32_t aInput, ErrorResult& aRv) {
+ if (aOutput >= NumberOfOutputs()) {
+ aRv.ThrowIndexSizeError(
+ nsPrintfCString("Output index %u is out of bounds", aOutput));
+ return nullptr;
+ }
+
+ if (aInput >= aDestination.NumberOfInputs()) {
+ aRv.ThrowIndexSizeError(
+ nsPrintfCString("Input index %u is out of bounds", aInput));
+ return nullptr;
+ }
+
+ if (Context() != aDestination.Context()) {
+ aRv.ThrowInvalidAccessError(
+ "Can't connect nodes from different AudioContexts");
+ return nullptr;
+ }
+
+ if (FindIndexOfNodeWithPorts(aDestination.mInputNodes, this, aInput,
+ aOutput) !=
+ nsTArray<AudioNode::InputNode>::NoIndex) {
+ // connection already exists.
+ return &aDestination;
+ }
+
+ WEB_AUDIO_API_LOG("%f: %s %u Connect() to %s %u", Context()->CurrentTime(),
+ NodeType(), Id(), aDestination.NodeType(),
+ aDestination.Id());
+
+ // The MediaTrackGraph will handle cycle detection. We don't need to do it
+ // here.
+
+ mOutputNodes.AppendElement(&aDestination);
+ InputNode* input = aDestination.mInputNodes.AppendElement();
+ input->mInputNode = this;
+ input->mInputPort = aInput;
+ input->mOutputPort = aOutput;
+ AudioNodeTrack* destinationTrack = aDestination.mTrack;
+ if (mTrack && destinationTrack) {
+ // Connect tracks in the MediaTrackGraph
+ MOZ_ASSERT(aInput <= UINT16_MAX, "Unexpected large input port number");
+ MOZ_ASSERT(aOutput <= UINT16_MAX, "Unexpected large output port number");
+ input->mTrackPort = destinationTrack->AllocateInputPort(
+ mTrack, static_cast<uint16_t>(aInput), static_cast<uint16_t>(aOutput));
+ }
+ aDestination.NotifyInputsChanged();
+
+ return &aDestination;
+}
+
+void AudioNode::Connect(AudioParam& aDestination, uint32_t aOutput,
+ ErrorResult& aRv) {
+ if (aOutput >= NumberOfOutputs()) {
+ aRv.ThrowIndexSizeError(
+ nsPrintfCString("Output index %u is out of bounds", aOutput));
+ return;
+ }
+
+ if (Context() != aDestination.GetParentObject()) {
+ aRv.ThrowInvalidAccessError(
+ "Can't connect a node to an AudioParam from a different AudioContext");
+ return;
+ }
+
+ if (FindIndexOfNodeWithPorts(aDestination.InputNodes(), this, INVALID_PORT,
+ aOutput) !=
+ nsTArray<AudioNode::InputNode>::NoIndex) {
+ // connection already exists.
+ return;
+ }
+
+ mOutputParams.AppendElement(&aDestination);
+ InputNode* input = aDestination.AppendInputNode();
+ input->mInputNode = this;
+ input->mInputPort = INVALID_PORT;
+ input->mOutputPort = aOutput;
+
+ mozilla::MediaTrack* track = aDestination.Track();
+ MOZ_ASSERT(track->AsProcessedTrack());
+ ProcessedMediaTrack* ps = static_cast<ProcessedMediaTrack*>(track);
+ if (mTrack) {
+ // Setup our track as an input to the AudioParam's track
+ MOZ_ASSERT(aOutput <= UINT16_MAX, "Unexpected large output port number");
+ input->mTrackPort =
+ ps->AllocateInputPort(mTrack, 0, static_cast<uint16_t>(aOutput));
+ }
+}
+
+void AudioNode::SendDoubleParameterToTrack(uint32_t aIndex, double aValue) {
+ MOZ_ASSERT(mTrack, "How come we don't have a track here?");
+ mTrack->SetDoubleParameter(aIndex, aValue);
+}
+
+void AudioNode::SendInt32ParameterToTrack(uint32_t aIndex, int32_t aValue) {
+ MOZ_ASSERT(mTrack, "How come we don't have a track here?");
+ mTrack->SetInt32Parameter(aIndex, aValue);
+}
+
+void AudioNode::SendChannelMixingParametersToTrack() {
+ if (mTrack) {
+ mTrack->SetChannelMixingParameters(mChannelCount, mChannelCountMode,
+ mChannelInterpretation);
+ }
+}
+
+template <>
+bool AudioNode::DisconnectFromOutputIfConnected<AudioNode>(
+ uint32_t aOutputNodeIndex, uint32_t aInputIndex) {
+ WEB_AUDIO_API_LOG("%f: %s %u Disconnect()", Context()->CurrentTime(),
+ NodeType(), Id());
+
+ AudioNode* destination = mOutputNodes[aOutputNodeIndex];
+
+ MOZ_ASSERT(aOutputNodeIndex < mOutputNodes.Length());
+ MOZ_ASSERT(aInputIndex < destination->InputNodes().Length());
+
+ // An upstream node may be starting to play on the graph thread, and the
+ // engine for a downstream node may be sending a PlayingRefChangeHandler
+ // ADDREF message to this (main) thread. Wait for a round trip before
+ // releasing nodes, to give engines receiving sound now time to keep their
+ // nodes alive.
+ class RunnableRelease final : public Runnable {
+ public:
+ explicit RunnableRelease(already_AddRefed<AudioNode> aNode)
+ : mozilla::Runnable("RunnableRelease"), mNode(aNode) {}
+
+ NS_IMETHOD Run() override {
+ mNode = nullptr;
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<AudioNode> mNode;
+ };
+
+ InputNode& input = destination->mInputNodes[aInputIndex];
+ if (input.mInputNode != this) {
+ return false;
+ }
+
+ // Remove one instance of 'dest' from mOutputNodes. There could be
+ // others, and it's not correct to remove them all since some of them
+ // could be for different output ports.
+ RefPtr<AudioNode> output = std::move(mOutputNodes[aOutputNodeIndex]);
+ mOutputNodes.RemoveElementAt(aOutputNodeIndex);
+ // Destroying the InputNode here sends a message to the graph thread
+ // to disconnect the tracks, which should be sent before the
+ // RunAfterPendingUpdates() call below.
+ destination->mInputNodes.RemoveElementAt(aInputIndex);
+ output->NotifyInputsChanged();
+ if (mTrack) {
+ nsCOMPtr<nsIRunnable> runnable = new RunnableRelease(output.forget());
+ mTrack->RunAfterPendingUpdates(runnable.forget());
+ }
+ return true;
+}
+
+template <>
+bool AudioNode::DisconnectFromOutputIfConnected<AudioParam>(
+ uint32_t aOutputParamIndex, uint32_t aInputIndex) {
+ MOZ_ASSERT(aOutputParamIndex < mOutputParams.Length());
+
+ AudioParam* destination = mOutputParams[aOutputParamIndex];
+
+ MOZ_ASSERT(aInputIndex < destination->InputNodes().Length());
+
+ const InputNode& input = destination->InputNodes()[aInputIndex];
+ if (input.mInputNode != this) {
+ return false;
+ }
+ destination->RemoveInputNode(aInputIndex);
+ // Remove one instance of 'dest' from mOutputParams. There could be
+ // others, and it's not correct to remove them all since some of them
+ // could be for different output ports.
+ mOutputParams.RemoveElementAt(aOutputParamIndex);
+ return true;
+}
+
+template <>
+const nsTArray<AudioNode::InputNode>&
+AudioNode::InputsForDestination<AudioNode>(uint32_t aOutputNodeIndex) const {
+ return mOutputNodes[aOutputNodeIndex]->InputNodes();
+}
+
+template <>
+const nsTArray<AudioNode::InputNode>&
+AudioNode::InputsForDestination<AudioParam>(uint32_t aOutputNodeIndex) const {
+ return mOutputParams[aOutputNodeIndex]->InputNodes();
+}
+
+template <typename DestinationType, typename Predicate>
+bool AudioNode::DisconnectMatchingDestinationInputs(uint32_t aDestinationIndex,
+ Predicate aPredicate) {
+ bool wasConnected = false;
+ uint32_t inputCount =
+ InputsForDestination<DestinationType>(aDestinationIndex).Length();
+
+ for (int32_t inputIndex = inputCount - 1; inputIndex >= 0; --inputIndex) {
+ const InputNode& input =
+ InputsForDestination<DestinationType>(aDestinationIndex)[inputIndex];
+ if (aPredicate(input)) {
+ if (DisconnectFromOutputIfConnected<DestinationType>(aDestinationIndex,
+ inputIndex)) {
+ wasConnected = true;
+ break;
+ }
+ }
+ }
+ return wasConnected;
+}
+
+void AudioNode::Disconnect(ErrorResult& aRv) {
+ for (int32_t outputIndex = mOutputNodes.Length() - 1; outputIndex >= 0;
+ --outputIndex) {
+ DisconnectMatchingDestinationInputs<AudioNode>(
+ outputIndex, [](const InputNode&) { return true; });
+ }
+
+ for (int32_t outputIndex = mOutputParams.Length() - 1; outputIndex >= 0;
+ --outputIndex) {
+ DisconnectMatchingDestinationInputs<AudioParam>(
+ outputIndex, [](const InputNode&) { return true; });
+ }
+}
+
+void AudioNode::Disconnect(uint32_t aOutput, ErrorResult& aRv) {
+ if (aOutput >= NumberOfOutputs()) {
+ aRv.ThrowIndexSizeError(
+ nsPrintfCString("Output index %u is out of bounds", aOutput));
+ return;
+ }
+
+ for (int32_t outputIndex = mOutputNodes.Length() - 1; outputIndex >= 0;
+ --outputIndex) {
+ DisconnectMatchingDestinationInputs<AudioNode>(
+ outputIndex, [aOutput](const InputNode& aInputNode) {
+ return aInputNode.mOutputPort == aOutput;
+ });
+ }
+
+ for (int32_t outputIndex = mOutputParams.Length() - 1; outputIndex >= 0;
+ --outputIndex) {
+ DisconnectMatchingDestinationInputs<AudioParam>(
+ outputIndex, [aOutput](const InputNode& aInputNode) {
+ return aInputNode.mOutputPort == aOutput;
+ });
+ }
+}
+
+void AudioNode::Disconnect(AudioNode& aDestination, ErrorResult& aRv) {
+ bool wasConnected = false;
+
+ for (int32_t outputIndex = mOutputNodes.Length() - 1; outputIndex >= 0;
+ --outputIndex) {
+ if (mOutputNodes[outputIndex] != &aDestination) {
+ continue;
+ }
+ wasConnected |= DisconnectMatchingDestinationInputs<AudioNode>(
+ outputIndex, [](const InputNode&) { return true; });
+ }
+
+ if (!wasConnected) {
+ aRv.ThrowInvalidAccessError(
+ "Trying to disconnect from a node we're not connected to");
+ return;
+ }
+}
+
+void AudioNode::Disconnect(AudioNode& aDestination, uint32_t aOutput,
+ ErrorResult& aRv) {
+ if (aOutput >= NumberOfOutputs()) {
+ aRv.ThrowIndexSizeError(
+ nsPrintfCString("Output index %u is out of bounds", aOutput));
+ return;
+ }
+
+ bool wasConnected = false;
+
+ for (int32_t outputIndex = mOutputNodes.Length() - 1; outputIndex >= 0;
+ --outputIndex) {
+ if (mOutputNodes[outputIndex] != &aDestination) {
+ continue;
+ }
+ wasConnected |= DisconnectMatchingDestinationInputs<AudioNode>(
+ outputIndex, [aOutput](const InputNode& aInputNode) {
+ return aInputNode.mOutputPort == aOutput;
+ });
+ }
+
+ if (!wasConnected) {
+ aRv.ThrowInvalidAccessError(
+ "Trying to disconnect from a node we're not connected to");
+ return;
+ }
+}
+
+void AudioNode::Disconnect(AudioNode& aDestination, uint32_t aOutput,
+ uint32_t aInput, ErrorResult& aRv) {
+ if (aOutput >= NumberOfOutputs()) {
+ aRv.ThrowIndexSizeError(
+ nsPrintfCString("Output index %u is out of bounds", aOutput));
+ return;
+ }
+
+ if (aInput >= aDestination.NumberOfInputs()) {
+ aRv.ThrowIndexSizeError(
+ nsPrintfCString("Input index %u is out of bounds", aInput));
+ return;
+ }
+
+ bool wasConnected = false;
+
+ for (int32_t outputIndex = mOutputNodes.Length() - 1; outputIndex >= 0;
+ --outputIndex) {
+ if (mOutputNodes[outputIndex] != &aDestination) {
+ continue;
+ }
+ wasConnected |= DisconnectMatchingDestinationInputs<AudioNode>(
+ outputIndex, [aOutput, aInput](const InputNode& aInputNode) {
+ return aInputNode.mOutputPort == aOutput &&
+ aInputNode.mInputPort == aInput;
+ });
+ }
+
+ if (!wasConnected) {
+ aRv.ThrowInvalidAccessError(
+ "Trying to disconnect from a node we're not connected to");
+ return;
+ }
+}
+
+void AudioNode::Disconnect(AudioParam& aDestination, ErrorResult& aRv) {
+ bool wasConnected = false;
+
+ for (int32_t outputIndex = mOutputParams.Length() - 1; outputIndex >= 0;
+ --outputIndex) {
+ if (mOutputParams[outputIndex] != &aDestination) {
+ continue;
+ }
+ wasConnected |= DisconnectMatchingDestinationInputs<AudioParam>(
+ outputIndex, [](const InputNode&) { return true; });
+ }
+
+ if (!wasConnected) {
+ aRv.ThrowInvalidAccessError(
+ "Trying to disconnect from an AudioParam we're not connected to");
+ return;
+ }
+}
+
+void AudioNode::Disconnect(AudioParam& aDestination, uint32_t aOutput,
+ ErrorResult& aRv) {
+ if (aOutput >= NumberOfOutputs()) {
+ aRv.ThrowIndexSizeError(
+ nsPrintfCString("Output index %u is out of bounds", aOutput));
+ return;
+ }
+
+ bool wasConnected = false;
+
+ for (int32_t outputIndex = mOutputParams.Length() - 1; outputIndex >= 0;
+ --outputIndex) {
+ if (mOutputParams[outputIndex] != &aDestination) {
+ continue;
+ }
+ wasConnected |= DisconnectMatchingDestinationInputs<AudioParam>(
+ outputIndex, [aOutput](const InputNode& aInputNode) {
+ return aInputNode.mOutputPort == aOutput;
+ });
+ }
+
+ if (!wasConnected) {
+ aRv.ThrowInvalidAccessError(
+ "Trying to disconnect from an AudioParam we're not connected to");
+ return;
+ }
+}
+
+void AudioNode::DestroyMediaTrack() {
+ if (mTrack) {
+ // Remove the node pointer on the engine.
+ AudioNodeTrack* ns = mTrack;
+ MOZ_ASSERT(ns, "How come we don't have a track here?");
+ MOZ_ASSERT(ns->Engine()->NodeMainThread() == this,
+ "Invalid node reference");
+ ns->Engine()->ClearNode();
+
+ mTrack->Destroy();
+ mTrack = nullptr;
+
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ if (obs) {
+ nsAutoString id;
+ id.AppendPrintf("%u", mId);
+ obs->NotifyObservers(nullptr, "webaudio-node-demise", id.get());
+ }
+ }
+}
+
+void AudioNode::RemoveOutputParam(AudioParam* aParam) {
+ mOutputParams.RemoveElement(aParam);
+}
+
+bool AudioNode::PassThrough() const {
+ MOZ_ASSERT(NumberOfInputs() <= 1 && NumberOfOutputs() == 1);
+ return mPassThrough;
+}
+
+void AudioNode::SetPassThrough(bool aPassThrough) {
+ MOZ_ASSERT(NumberOfInputs() <= 1 && NumberOfOutputs() == 1);
+ mPassThrough = aPassThrough;
+ if (mTrack) {
+ mTrack->SetPassThrough(mPassThrough);
+ }
+}
+
+AudioParam* AudioNode::CreateAudioParam(uint32_t aIndex, const nsAString& aName,
+ float aDefaultValue, float aMinValue,
+ float aMaxValue) {
+ return *mParams.AppendElement(
+ new AudioParam(this, aIndex, aName, aDefaultValue, aMinValue, aMaxValue));
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/AudioNode.h b/dom/media/webaudio/AudioNode.h
new file mode 100644
index 0000000000..795123dcea
--- /dev/null
+++ b/dom/media/webaudio/AudioNode.h
@@ -0,0 +1,297 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef AudioNode_h_
+#define AudioNode_h_
+
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/dom/AudioNodeBinding.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsTArray.h"
+#include "AudioContext.h"
+#include "MediaTrackGraph.h"
+#include "WebAudioUtils.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/MemoryReporting.h"
+#include "nsPrintfCString.h"
+#include "nsWeakReference.h"
+#include "SelfRef.h"
+
+namespace mozilla {
+
+class AbstractThread;
+
+namespace dom {
+
+class AudioContext;
+class AudioBufferSourceNode;
+class AudioParam;
+class AudioParamTimeline;
+struct ThreeDPoint;
+
+/**
+ * The DOM object representing a Web Audio AudioNode.
+ *
+ * Each AudioNode has a MediaTrack representing the actual
+ * real-time processing and output of this AudioNode.
+ *
+ * We track the incoming and outgoing connections to other AudioNodes.
+ * Outgoing connections have strong ownership. Also, AudioNodes that will
+ * produce sound on their output even when they have silent or no input ask
+ * the AudioContext to keep playing or tail-time references to keep them alive
+ * until the context is finished.
+ *
+ * Explicit disconnections will only remove references from output nodes after
+ * the graph is notified and the main thread receives a reply. Similarly,
+ * nodes with playing or tail-time references release these references only
+ * after receiving notification from their engine on the graph thread that
+ * playing has stopped. Engines notifying the main thread that they have
+ * finished do so strictly *after* producing and returning their last block.
+ * In this way, an engine that receives non-null input knows that the input
+ * comes from nodes that are still alive and will keep their output nodes
+ * alive for at least as long as it takes to process messages from the graph
+ * thread. i.e. the engine receiving non-null input knows that its node is
+ * still alive, and will still be alive when it receives a message from the
+ * engine.
+ */
+class AudioNode : public DOMEventTargetHelper, public nsSupportsWeakReference {
+ protected:
+ // You can only use refcounting to delete this object
+ virtual ~AudioNode();
+
+ public:
+ AudioNode(AudioContext* aContext, uint32_t aChannelCount,
+ ChannelCountMode aChannelCountMode,
+ ChannelInterpretation aChannelInterpretation);
+
+ // This should be idempotent (safe to call multiple times).
+ virtual void DestroyMediaTrack();
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(AudioNode, DOMEventTargetHelper)
+
+ virtual AudioBufferSourceNode* AsAudioBufferSourceNode() { return nullptr; }
+
+ AudioContext* GetParentObject() const { return mContext; }
+
+ AudioContext* Context() const { return mContext; }
+
+ virtual AudioNode* Connect(AudioNode& aDestination, uint32_t aOutput,
+ uint32_t aInput, ErrorResult& aRv);
+
+ virtual void Connect(AudioParam& aDestination, uint32_t aOutput,
+ ErrorResult& aRv);
+
+ virtual void Disconnect(ErrorResult& aRv);
+ virtual void Disconnect(uint32_t aOutput, ErrorResult& aRv);
+ virtual void Disconnect(AudioNode& aDestination, ErrorResult& aRv);
+ virtual void Disconnect(AudioNode& aDestination, uint32_t aOutput,
+ ErrorResult& aRv);
+ virtual void Disconnect(AudioNode& aDestination, uint32_t aOutput,
+ uint32_t aInput, ErrorResult& aRv);
+ virtual void Disconnect(AudioParam& aDestination, ErrorResult& aRv);
+ virtual void Disconnect(AudioParam& aDestination, uint32_t aOutput,
+ ErrorResult& aRv);
+
+ // Called after input nodes have been explicitly added or removed through
+ // the Connect() or Disconnect() methods.
+ virtual void NotifyInputsChanged() {}
+ // Indicate that the node should continue indefinitely to behave as if an
+ // input is connected, even though there is no longer a corresponding entry
+ // in mInputNodes. Called after an input node has been removed because it
+ // is being garbage collected.
+ virtual void NotifyHasPhantomInput() {}
+
+ // The following two virtual methods must be implemented by each node type
+ // to provide their number of input and output ports. These numbers are
+ // constant for the lifetime of the node. Both default to 1.
+ virtual uint16_t NumberOfInputs() const { return 1; }
+ virtual uint16_t NumberOfOutputs() const { return 1; }
+
+ uint32_t Id() const { return mId; }
+
+ bool PassThrough() const;
+ void SetPassThrough(bool aPassThrough);
+
+ uint32_t ChannelCount() const { return mChannelCount; }
+ virtual void SetChannelCount(uint32_t aChannelCount, ErrorResult& aRv) {
+ if (aChannelCount == 0 || aChannelCount > WebAudioUtils::MaxChannelCount) {
+ aRv.ThrowNotSupportedError(
+ nsPrintfCString("Channel count (%u) must be in the range [1, max "
+ "supported channel count]",
+ aChannelCount));
+ return;
+ }
+ mChannelCount = aChannelCount;
+ SendChannelMixingParametersToTrack();
+ }
+ ChannelCountMode ChannelCountModeValue() const { return mChannelCountMode; }
+ virtual void SetChannelCountModeValue(ChannelCountMode aMode,
+ ErrorResult& aRv) {
+ mChannelCountMode = aMode;
+ SendChannelMixingParametersToTrack();
+ }
+ ChannelInterpretation ChannelInterpretationValue() const {
+ return mChannelInterpretation;
+ }
+ virtual void SetChannelInterpretationValue(ChannelInterpretation aMode,
+ ErrorResult& aRv) {
+ mChannelInterpretation = aMode;
+ SendChannelMixingParametersToTrack();
+ }
+
+ struct InputNode final {
+ InputNode() = default;
+ InputNode(const InputNode&) = delete;
+ InputNode(InputNode&&) = default;
+
+ ~InputNode() {
+ if (mTrackPort) {
+ mTrackPort->Destroy();
+ }
+ }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t amount = 0;
+ if (mTrackPort) {
+ amount += mTrackPort->SizeOfIncludingThis(aMallocSizeOf);
+ }
+
+ return amount;
+ }
+
+ // The InputNode is destroyed when mInputNode is disconnected.
+ AudioNode* MOZ_NON_OWNING_REF mInputNode;
+ RefPtr<MediaInputPort> mTrackPort;
+ // The index of the input port this node feeds into.
+ // This is not used for connections to AudioParams.
+ uint32_t mInputPort;
+ // The index of the output port this node comes out of.
+ uint32_t mOutputPort;
+ };
+
+ // Returns the track, if any.
+ AudioNodeTrack* GetTrack() const { return mTrack; }
+
+ const nsTArray<InputNode>& InputNodes() const { return mInputNodes; }
+ const nsTArray<RefPtr<AudioNode>>& OutputNodes() const {
+ return mOutputNodes;
+ }
+ const nsTArray<RefPtr<AudioParam>>& OutputParams() const {
+ return mOutputParams;
+ }
+
+ template <typename T>
+ const nsTArray<InputNode>& InputsForDestination(uint32_t aOutputIndex) const;
+
+ void RemoveOutputParam(AudioParam* aParam);
+
+ // MarkActive() asks the context to keep the AudioNode alive until the
+ // context is finished. This takes care of "playing" references and
+ // "tail-time" references.
+ void MarkActive() { Context()->RegisterActiveNode(this); }
+ // Active nodes call MarkInactive() when they have finished producing sound
+ // for the foreseeable future.
+ // Do not call MarkInactive from a node destructor. If the destructor is
+ // called, then the node is already inactive.
+ // MarkInactive() may delete |this|.
+ void MarkInactive() { Context()->UnregisterActiveNode(this); }
+
+ virtual size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const;
+ virtual size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const;
+
+ // Returns a string from constant static storage identifying the dom node
+ // type.
+ virtual const char* NodeType() const = 0;
+
+ // This can return nullptr, but only when the AudioNode has been created
+ // during document shutdown.
+ AbstractThread* GetAbstractMainThread() const { return mAbstractMainThread; }
+
+ const nsTArray<RefPtr<AudioParam>>& GetAudioParams() const { return mParams; }
+
+ private:
+ // Given:
+ //
+ // - a DestinationType, that can be an AudioNode or an AudioParam ;
+ // - a Predicate, a function that takes an InputNode& and returns a bool ;
+ //
+ // This method iterates on the InputNodes() of the node at the index
+ // aDestinationIndex, and calls `DisconnectFromOutputIfConnected` with this
+ // input node, if aPredicate returns true.
+ template <typename DestinationType, typename Predicate>
+ bool DisconnectMatchingDestinationInputs(uint32_t aDestinationIndex,
+ Predicate aPredicate);
+
+ virtual void LastRelease() override {
+ // We are about to be deleted, disconnect the object from the graph before
+ // the derived type is destroyed.
+ DisconnectFromGraph();
+ }
+ // Callers must hold a reference to 'this'.
+ void DisconnectFromGraph();
+
+ template <typename DestinationType>
+ bool DisconnectFromOutputIfConnected(uint32_t aOutputIndex,
+ uint32_t aInputIndex);
+
+ protected:
+ // Helper for the Constructors for nodes.
+ void Initialize(const AudioNodeOptions& aOptions, ErrorResult& aRv);
+
+ // Helpers for sending different value types to tracks
+ void SendDoubleParameterToTrack(uint32_t aIndex, double aValue);
+ void SendInt32ParameterToTrack(uint32_t aIndex, int32_t aValue);
+ void SendChannelMixingParametersToTrack();
+
+ private:
+ RefPtr<AudioContext> mContext;
+
+ protected:
+ // Set in the constructor of all nodes except offline AudioDestinationNode.
+ // Must not become null until finished.
+ RefPtr<AudioNodeTrack> mTrack;
+
+ // The reference pointing out all audio params which belong to this node.
+ nsTArray<RefPtr<AudioParam>> mParams;
+ // Use this function to create an AudioParam, so as to automatically add
+ // the new AudioParam to `mParams`.
+ AudioParam* CreateAudioParam(
+ uint32_t aIndex, const nsAString& aName, float aDefaultValue,
+ float aMinValue = std::numeric_limits<float>::lowest(),
+ float aMaxValue = std::numeric_limits<float>::max());
+
+ private:
+ // For every InputNode, there is a corresponding entry in mOutputNodes of the
+ // InputNode's mInputNode.
+ nsTArray<InputNode> mInputNodes;
+ // For every mOutputNode entry, there is a corresponding entry in mInputNodes
+ // of the mOutputNode entry. We won't necessarily be able to identify the
+ // exact matching entry, since mOutputNodes doesn't include the port
+ // identifiers and the same node could be connected on multiple ports.
+ nsTArray<RefPtr<AudioNode>> mOutputNodes;
+ // For every mOutputParams entry, there is a corresponding entry in
+ // AudioParam::mInputNodes of the mOutputParams entry. We won't necessarily be
+ // able to identify the exact matching entry, since mOutputParams doesn't
+ // include the port identifiers and the same node could be connected on
+ // multiple ports.
+ nsTArray<RefPtr<AudioParam>> mOutputParams;
+ uint32_t mChannelCount;
+ ChannelCountMode mChannelCountMode;
+ ChannelInterpretation mChannelInterpretation;
+ const uint32_t mId;
+ // Whether the node just passes through its input. This is a devtools API
+ // that only works for some node types.
+ bool mPassThrough;
+ // DocGroup-specifc AbstractThread::MainThread() for MediaTrackGraph
+ // operations.
+ const RefPtr<AbstractThread> mAbstractMainThread;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webaudio/AudioNodeEngine.cpp b/dom/media/webaudio/AudioNodeEngine.cpp
new file mode 100644
index 0000000000..75f0edde97
--- /dev/null
+++ b/dom/media/webaudio/AudioNodeEngine.cpp
@@ -0,0 +1,440 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioNodeEngine.h"
+
+#include "mozilla/AbstractThread.h"
+#ifdef USE_NEON
+# include "mozilla/arm.h"
+# include "AudioNodeEngineGeneric.h"
+#endif
+#ifdef USE_SSE2
+# include "mozilla/SSE.h"
+# include "AudioNodeEngineGeneric.h"
+#endif
+#if defined(USE_SSE42) && defined(USE_FMA3)
+# include "mozilla/SSE.h"
+# include "AudioNodeEngineGeneric.h"
+#endif
+#include "AudioBlock.h"
+#include "Tracing.h"
+
+namespace mozilla {
+
+already_AddRefed<ThreadSharedFloatArrayBufferList>
+ThreadSharedFloatArrayBufferList::Create(uint32_t aChannelCount, size_t aLength,
+ const mozilla::fallible_t&) {
+ RefPtr<ThreadSharedFloatArrayBufferList> buffer =
+ new ThreadSharedFloatArrayBufferList(aChannelCount);
+
+ for (uint32_t i = 0; i < aChannelCount; ++i) {
+ float* channelData = js_pod_malloc<float>(aLength);
+ if (!channelData) {
+ return nullptr;
+ }
+
+ buffer->SetData(i, channelData, js_free, channelData);
+ }
+
+ return buffer.forget();
+}
+
+void WriteZeroesToAudioBlock(AudioBlock* aChunk, uint32_t aStart,
+ uint32_t aLength) {
+ MOZ_ASSERT(aStart + aLength <= WEBAUDIO_BLOCK_SIZE);
+ MOZ_ASSERT(!aChunk->IsNull(), "You should pass a non-null chunk");
+ if (aLength == 0) {
+ return;
+ }
+
+ for (uint32_t i = 0; i < aChunk->ChannelCount(); ++i) {
+ PodZero(aChunk->ChannelFloatsForWrite(i) + aStart, aLength);
+ }
+}
+
+void AudioBufferCopyWithScale(const float* aInput, float aScale, float* aOutput,
+ uint32_t aSize) {
+ if (aScale == 1.0f) {
+ PodCopy(aOutput, aInput, aSize);
+ } else {
+ for (uint32_t i = 0; i < aSize; ++i) {
+ aOutput[i] = aInput[i] * aScale;
+ }
+ }
+}
+
+void AudioBufferAddWithScale(const float* aInput, float aScale, float* aOutput,
+ uint32_t aSize) {
+#ifdef USE_NEON
+ if (mozilla::supports_neon()) {
+ Engine<xsimd::neon>::AudioBufferAddWithScale(aInput, aScale, aOutput,
+ aSize);
+ return;
+ }
+#endif
+
+#ifdef USE_SSE2
+ if (mozilla::supports_sse2()) {
+# if defined(USE_SSE42) && defined(USE_FMA3)
+ if (mozilla::supports_fma3() && mozilla::supports_sse4_2()) {
+ Engine<xsimd::fma3<xsimd::sse4_2>>::AudioBufferAddWithScale(
+ aInput, aScale, aOutput, aSize);
+ } else
+# endif
+ {
+ Engine<xsimd::sse2>::AudioBufferAddWithScale(aInput, aScale, aOutput,
+ aSize);
+ }
+ return;
+ }
+#endif
+
+ if (aScale == 1.0f) {
+ for (uint32_t i = 0; i < aSize; ++i) {
+ aOutput[i] += aInput[i];
+ }
+ } else {
+ for (uint32_t i = 0; i < aSize; ++i) {
+ aOutput[i] += aInput[i] * aScale;
+ }
+ }
+}
+
+void AudioBlockAddChannelWithScale(const float aInput[WEBAUDIO_BLOCK_SIZE],
+ float aScale,
+ float aOutput[WEBAUDIO_BLOCK_SIZE]) {
+ AudioBufferAddWithScale(aInput, aScale, aOutput, WEBAUDIO_BLOCK_SIZE);
+}
+
+void AudioBlockCopyChannelWithScale(const float* aInput, float aScale,
+ float* aOutput) {
+ if (aScale == 1.0f) {
+ memcpy(aOutput, aInput, WEBAUDIO_BLOCK_SIZE * sizeof(float));
+ } else {
+#ifdef USE_NEON
+ if (mozilla::supports_neon()) {
+ Engine<xsimd::neon>::AudioBlockCopyChannelWithScale(aInput, aScale,
+ aOutput);
+ return;
+ }
+#endif
+
+#ifdef USE_SSE2
+ if (mozilla::supports_sse2()) {
+ Engine<xsimd::sse2>::AudioBlockCopyChannelWithScale(aInput, aScale,
+ aOutput);
+ return;
+ }
+#endif
+
+ for (uint32_t i = 0; i < WEBAUDIO_BLOCK_SIZE; ++i) {
+ aOutput[i] = aInput[i] * aScale;
+ }
+ }
+}
+
+void BufferComplexMultiply(const float* aInput, const float* aScale,
+ float* aOutput, uint32_t aSize) {
+#ifdef USE_NEON
+ if (mozilla::supports_neon()) {
+ Engine<xsimd::neon>::BufferComplexMultiply(aInput, aScale, aOutput, aSize);
+ return;
+ }
+#endif
+#ifdef USE_SSE2
+ if (mozilla::supports_sse()) {
+# if defined(USE_SSE42) && defined(USE_FMA3)
+ if (mozilla::supports_fma3() && mozilla::supports_sse4_2()) {
+ Engine<xsimd::fma3<xsimd::sse4_2>>::BufferComplexMultiply(aInput, aScale,
+ aOutput, aSize);
+ } else
+# endif
+ {
+ Engine<xsimd::sse2>::BufferComplexMultiply(aInput, aScale, aOutput,
+ aSize);
+ }
+ return;
+ }
+#endif
+
+ for (uint32_t i = 0; i < aSize * 2; i += 2) {
+ float real1 = aInput[i];
+ float imag1 = aInput[i + 1];
+ float real2 = aScale[i];
+ float imag2 = aScale[i + 1];
+ float realResult = real1 * real2 - imag1 * imag2;
+ float imagResult = real1 * imag2 + imag1 * real2;
+ aOutput[i] = realResult;
+ aOutput[i + 1] = imagResult;
+ }
+}
+
+float AudioBufferPeakValue(const float* aInput, uint32_t aSize) {
+ float max = 0.0f;
+ for (uint32_t i = 0; i < aSize; i++) {
+ float mag = fabs(aInput[i]);
+ if (mag > max) {
+ max = mag;
+ }
+ }
+ return max;
+}
+
+void AudioBlockCopyChannelWithScale(const float aInput[WEBAUDIO_BLOCK_SIZE],
+ const float aScale[WEBAUDIO_BLOCK_SIZE],
+ float aOutput[WEBAUDIO_BLOCK_SIZE]) {
+#ifdef USE_NEON
+ if (mozilla::supports_neon()) {
+ Engine<xsimd::neon>::AudioBlockCopyChannelWithScale(aInput, aScale,
+ aOutput);
+ return;
+ }
+#endif
+
+#ifdef USE_SSE2
+ if (mozilla::supports_sse2()) {
+ Engine<xsimd::sse2>::AudioBlockCopyChannelWithScale(aInput, aScale,
+ aOutput);
+ return;
+ }
+#endif
+
+ for (uint32_t i = 0; i < WEBAUDIO_BLOCK_SIZE; ++i) {
+ aOutput[i] = aInput[i] * aScale[i];
+ }
+}
+
+void AudioBlockInPlaceScale(float aBlock[WEBAUDIO_BLOCK_SIZE], float aScale) {
+ AudioBufferInPlaceScale(aBlock, aScale, WEBAUDIO_BLOCK_SIZE);
+}
+
+void AudioBlockInPlaceScale(float aBlock[WEBAUDIO_BLOCK_SIZE],
+ float aScale[WEBAUDIO_BLOCK_SIZE]) {
+ AudioBufferInPlaceScale(aBlock, aScale, WEBAUDIO_BLOCK_SIZE);
+}
+
+void AudioBufferInPlaceScale(float* aBlock, float aScale, uint32_t aSize) {
+ if (aScale == 1.0f) {
+ return;
+ }
+#ifdef USE_NEON
+ if (mozilla::supports_neon()) {
+ Engine<xsimd::neon>::AudioBufferInPlaceScale(aBlock, aScale, aSize);
+ return;
+ }
+#endif
+
+#ifdef USE_SSE2
+ if (mozilla::supports_sse2()) {
+ Engine<xsimd::sse2>::AudioBufferInPlaceScale(aBlock, aScale, aSize);
+ return;
+ }
+#endif
+
+ for (uint32_t i = 0; i < aSize; ++i) {
+ *aBlock++ *= aScale;
+ }
+}
+
+void AudioBufferInPlaceScale(float* aBlock, float* aScale, uint32_t aSize) {
+#ifdef USE_NEON
+ if (mozilla::supports_neon()) {
+ Engine<xsimd::neon>::AudioBufferInPlaceScale(aBlock, aScale, aSize);
+ return;
+ }
+#endif
+
+#ifdef USE_SSE2
+ if (mozilla::supports_sse2()) {
+ Engine<xsimd::sse2>::AudioBufferInPlaceScale(aBlock, aScale, aSize);
+ return;
+ }
+#endif
+
+ for (uint32_t i = 0; i < aSize; ++i) {
+ *aBlock++ *= *aScale++;
+ }
+}
+
+void AudioBlockPanMonoToStereo(const float aInput[WEBAUDIO_BLOCK_SIZE],
+ float aGainL[WEBAUDIO_BLOCK_SIZE],
+ float aGainR[WEBAUDIO_BLOCK_SIZE],
+ float aOutputL[WEBAUDIO_BLOCK_SIZE],
+ float aOutputR[WEBAUDIO_BLOCK_SIZE]) {
+ AudioBlockCopyChannelWithScale(aInput, aGainL, aOutputL);
+ AudioBlockCopyChannelWithScale(aInput, aGainR, aOutputR);
+}
+
+void AudioBlockPanMonoToStereo(const float aInput[WEBAUDIO_BLOCK_SIZE],
+ float aGainL, float aGainR,
+ float aOutputL[WEBAUDIO_BLOCK_SIZE],
+ float aOutputR[WEBAUDIO_BLOCK_SIZE]) {
+ AudioBlockCopyChannelWithScale(aInput, aGainL, aOutputL);
+ AudioBlockCopyChannelWithScale(aInput, aGainR, aOutputR);
+}
+
+void AudioBlockPanStereoToStereo(const float aInputL[WEBAUDIO_BLOCK_SIZE],
+ const float aInputR[WEBAUDIO_BLOCK_SIZE],
+ float aGainL, float aGainR, bool aIsOnTheLeft,
+ float aOutputL[WEBAUDIO_BLOCK_SIZE],
+ float aOutputR[WEBAUDIO_BLOCK_SIZE]) {
+#ifdef USE_NEON
+ if (mozilla::supports_neon()) {
+ Engine<xsimd::neon>::AudioBlockPanStereoToStereo(
+ aInputL, aInputR, aGainL, aGainR, aIsOnTheLeft, aOutputL, aOutputR);
+ return;
+ }
+#endif
+
+#ifdef USE_SSE2
+ if (mozilla::supports_sse2()) {
+# if defined(USE_SSE42) && defined(USE_FMA3)
+ if (mozilla::supports_fma3() && mozilla::supports_sse4_2()) {
+ Engine<xsimd::fma3<xsimd::sse4_2>>::AudioBlockPanStereoToStereo(
+ aInputL, aInputR, aGainL, aGainR, aIsOnTheLeft, aOutputL, aOutputR);
+ } else
+# endif
+ {
+ Engine<xsimd::sse2>::AudioBlockPanStereoToStereo(
+ aInputL, aInputR, aGainL, aGainR, aIsOnTheLeft, aOutputL, aOutputR);
+ }
+ return;
+ }
+#endif
+
+ uint32_t i;
+
+ if (aIsOnTheLeft) {
+ for (i = 0; i < WEBAUDIO_BLOCK_SIZE; ++i) {
+ aOutputL[i] = aInputL[i] + aInputR[i] * aGainL;
+ aOutputR[i] = aInputR[i] * aGainR;
+ }
+ } else {
+ for (i = 0; i < WEBAUDIO_BLOCK_SIZE; ++i) {
+ aOutputL[i] = aInputL[i] * aGainL;
+ aOutputR[i] = aInputR[i] + aInputL[i] * aGainR;
+ }
+ }
+}
+
+void AudioBlockPanStereoToStereo(const float aInputL[WEBAUDIO_BLOCK_SIZE],
+ const float aInputR[WEBAUDIO_BLOCK_SIZE],
+ const float aGainL[WEBAUDIO_BLOCK_SIZE],
+ const float aGainR[WEBAUDIO_BLOCK_SIZE],
+ const bool aIsOnTheLeft[WEBAUDIO_BLOCK_SIZE],
+ float aOutputL[WEBAUDIO_BLOCK_SIZE],
+ float aOutputR[WEBAUDIO_BLOCK_SIZE]) {
+#ifdef USE_NEON
+ if (mozilla::supports_neon()) {
+ Engine<xsimd::neon>::AudioBlockPanStereoToStereo(
+ aInputL, aInputR, aGainL, aGainR, aIsOnTheLeft, aOutputL, aOutputR);
+ return;
+ }
+#endif
+
+#ifdef USE_SSE2
+ if (mozilla::supports_sse2()) {
+# if defined(USE_SSE42) && defined(USE_FMA3)
+ if (mozilla::supports_fma3() && mozilla::supports_sse4_2()) {
+ Engine<xsimd::fma3<xsimd::sse2>>::AudioBlockPanStereoToStereo(
+ aInputL, aInputR, aGainL, aGainR, aIsOnTheLeft, aOutputL, aOutputR);
+ } else
+# endif
+ {
+ Engine<xsimd::sse2>::AudioBlockPanStereoToStereo(
+ aInputL, aInputR, aGainL, aGainR, aIsOnTheLeft, aOutputL, aOutputR);
+ }
+ return;
+ }
+#endif
+
+ uint32_t i;
+ for (i = 0; i < WEBAUDIO_BLOCK_SIZE; i++) {
+ if (aIsOnTheLeft[i]) {
+ aOutputL[i] = aInputL[i] + aInputR[i] * aGainL[i];
+ aOutputR[i] = aInputR[i] * aGainR[i];
+ } else {
+ aOutputL[i] = aInputL[i] * aGainL[i];
+ aOutputR[i] = aInputR[i] + aInputL[i] * aGainR[i];
+ }
+ }
+}
+
+float AudioBufferSumOfSquares(const float* aInput, uint32_t aLength) {
+#ifdef USE_NEON
+ if (mozilla::supports_neon()) {
+ return Engine<xsimd::neon>::AudioBufferSumOfSquares(aInput, aLength);
+ }
+#endif
+
+#ifdef USE_SSE2
+ if (mozilla::supports_sse()) {
+# if defined(USE_SSE42) && defined(USE_FMA3)
+ if (mozilla::supports_fma3() && mozilla::supports_sse4_2()) {
+ return Engine<xsimd::fma3<xsimd::sse4_2>>::AudioBufferSumOfSquares(
+ aInput, aLength);
+ } else
+# endif
+ {
+ return Engine<xsimd::sse2>::AudioBufferSumOfSquares(aInput, aLength);
+ }
+ }
+#endif
+
+ float sum = 0.f;
+ while (aLength--) {
+ sum += *aInput * *aInput;
+ ++aInput;
+ }
+ return sum;
+}
+
+void NaNToZeroInPlace(float* aSamples, size_t aCount) {
+#ifdef USE_SSE2
+ if (mozilla::supports_sse2()) {
+ Engine<xsimd::sse2>::NaNToZeroInPlace(aSamples, aCount);
+ return;
+ }
+#endif
+ for (size_t i = 0; i < aCount; i++) {
+ if (aSamples[i] != aSamples[i]) {
+ aSamples[i] = 0.0;
+ }
+ }
+}
+
+AudioNodeEngine::AudioNodeEngine(dom::AudioNode* aNode)
+ : mNode(aNode),
+ mNodeType(aNode ? aNode->NodeType() : nullptr),
+ mInputCount(aNode ? aNode->NumberOfInputs() : 1),
+ mOutputCount(aNode ? aNode->NumberOfOutputs() : 0),
+ mAbstractMainThread(aNode && aNode->GetAbstractMainThread()
+ ? aNode->GetAbstractMainThread()
+ : AbstractThread::MainThread()) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_COUNT_CTOR(AudioNodeEngine);
+}
+
+void AudioNodeEngine::ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom,
+ const AudioBlock& aInput,
+ AudioBlock* aOutput, bool* aFinished) {
+ MOZ_ASSERT(mInputCount <= 1 && mOutputCount <= 1);
+ TRACE("AudioNodeEngine::ProcessBlock");
+ *aOutput = aInput;
+}
+
+void AudioNodeEngine::ProcessBlocksOnPorts(AudioNodeTrack* aTrack,
+ GraphTime aFrom,
+ Span<const AudioBlock> aInput,
+ Span<AudioBlock> aOutput,
+ bool* aFinished) {
+ MOZ_ASSERT(mInputCount > 1 || mOutputCount > 1);
+ TRACE("AudioNodeEngine::ProcessBlocksOnPorts");
+ // Only produce one output port, and drop all other input ports.
+ aOutput[0] = aInput[0];
+}
+
+} // namespace mozilla
diff --git a/dom/media/webaudio/AudioNodeEngine.h b/dom/media/webaudio/AudioNodeEngine.h
new file mode 100644
index 0000000000..64dd3c642a
--- /dev/null
+++ b/dom/media/webaudio/AudioNodeEngine.h
@@ -0,0 +1,392 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef MOZILLA_AUDIONODEENGINE_H_
+#define MOZILLA_AUDIONODEENGINE_H_
+
+#include "AudioSegment.h"
+#include "mozilla/dom/AudioNode.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/Mutex.h"
+
+namespace WebCore {
+class Reverb;
+} // namespace WebCore
+
+namespace mozilla {
+
+namespace dom {
+struct ThreeDPoint;
+class AudioParamTimeline;
+class DelayNodeEngine;
+struct AudioTimelineEvent;
+} // namespace dom
+
+class AbstractThread;
+class AudioBlock;
+class AudioNodeTrack;
+
+/**
+ * This class holds onto a set of immutable channel buffers. The storage
+ * for the buffers must be malloced, but the buffer pointers and the malloc
+ * pointers can be different (e.g. if the buffers are contained inside
+ * some malloced object).
+ */
+class ThreadSharedFloatArrayBufferList final : public ThreadSharedObject {
+ public:
+ /**
+ * Construct with null channel data pointers.
+ */
+ explicit ThreadSharedFloatArrayBufferList(uint32_t aCount) {
+ mContents.SetLength(aCount);
+ }
+ /**
+ * Create with buffers suitable for transfer to
+ * JS::NewArrayBufferWithContents(). The buffer contents are uninitialized
+ * and so should be set using GetDataForWrite().
+ */
+ static already_AddRefed<ThreadSharedFloatArrayBufferList> Create(
+ uint32_t aChannelCount, size_t aLength, const mozilla::fallible_t&);
+
+ ThreadSharedFloatArrayBufferList* AsThreadSharedFloatArrayBufferList()
+ override {
+ return this;
+ };
+
+ struct Storage final {
+ Storage() : mDataToFree(nullptr), mFree(nullptr), mSampleData(nullptr) {}
+ ~Storage() {
+ if (mFree) {
+ mFree(mDataToFree);
+ } else {
+ MOZ_ASSERT(!mDataToFree);
+ }
+ }
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ // NB: mSampleData might not be owned, if it is it just points to
+ // mDataToFree.
+ return aMallocSizeOf(mDataToFree);
+ }
+ void* mDataToFree;
+ void (*mFree)(void*);
+ float* mSampleData;
+ };
+
+ /**
+ * This can be called on any thread.
+ */
+ uint32_t GetChannels() const { return mContents.Length(); }
+ /**
+ * This can be called on any thread.
+ */
+ const float* GetData(uint32_t aIndex) const {
+ return mContents[aIndex].mSampleData;
+ }
+ /**
+ * This can be called on any thread, but only when the calling thread is the
+ * only owner.
+ */
+ float* GetDataForWrite(uint32_t aIndex) {
+ MOZ_ASSERT(!IsShared());
+ return mContents[aIndex].mSampleData;
+ }
+
+ /**
+ * Call this only during initialization, before the object is handed to
+ * any other thread.
+ */
+ void SetData(uint32_t aIndex, void* aDataToFree, void (*aFreeFunc)(void*),
+ float* aData) {
+ Storage* s = &mContents[aIndex];
+ if (s->mFree) {
+ s->mFree(s->mDataToFree);
+ } else {
+ MOZ_ASSERT(!s->mDataToFree);
+ }
+
+ s->mDataToFree = aDataToFree;
+ s->mFree = aFreeFunc;
+ s->mSampleData = aData;
+ }
+
+ /**
+ * Put this object into an error state where there are no channels.
+ */
+ void Clear() { mContents.Clear(); }
+
+ size_t SizeOfExcludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const override {
+ size_t amount = ThreadSharedObject::SizeOfExcludingThis(aMallocSizeOf);
+ amount += mContents.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (size_t i = 0; i < mContents.Length(); i++) {
+ amount += mContents[i].SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ return amount;
+ }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ private:
+ AutoTArray<Storage, 2> mContents;
+};
+
+/**
+ * aChunk must have been allocated by AllocateAudioBlock.
+ */
+void WriteZeroesToAudioBlock(AudioBlock* aChunk, uint32_t aStart,
+ uint32_t aLength);
+
+/**
+ * Copy with scale. aScale == 1.0f should be optimized.
+ */
+void AudioBufferCopyWithScale(const float* aInput, float aScale, float* aOutput,
+ uint32_t aSize);
+
+/**
+ * Pointwise multiply-add operation. aScale == 1.0f should be optimized.
+ */
+void AudioBufferAddWithScale(const float* aInput, float aScale, float* aOutput,
+ uint32_t aSize);
+
+/**
+ * Pointwise multiply-add operation. aScale == 1.0f should be optimized.
+ */
+void AudioBlockAddChannelWithScale(const float aInput[WEBAUDIO_BLOCK_SIZE],
+ float aScale,
+ float aOutput[WEBAUDIO_BLOCK_SIZE]);
+
+/**
+ * Pointwise copy-scaled operation. aScale == 1.0f should be optimized.
+ *
+ * Buffer size is implicitly assumed to be WEBAUDIO_BLOCK_SIZE.
+ */
+void AudioBlockCopyChannelWithScale(const float* aInput, float aScale,
+ float* aOutput);
+
+/**
+ * Vector copy-scaled operation.
+ */
+void AudioBlockCopyChannelWithScale(const float aInput[WEBAUDIO_BLOCK_SIZE],
+ const float aScale[WEBAUDIO_BLOCK_SIZE],
+ float aOutput[WEBAUDIO_BLOCK_SIZE]);
+
+/**
+ * Vector complex multiplication on arbitrary sized buffers.
+ */
+void BufferComplexMultiply(const float* aInput, const float* aScale,
+ float* aOutput, uint32_t aSize);
+
+/**
+ * Vector maximum element magnitude ( max(abs(aInput)) ).
+ */
+float AudioBufferPeakValue(const float* aInput, uint32_t aSize);
+
+/**
+ * In place gain. aScale == 1.0f should be optimized.
+ */
+void AudioBlockInPlaceScale(float aBlock[WEBAUDIO_BLOCK_SIZE], float aScale);
+
+/**
+ * In place gain. aScale == 1.0f should be optimized.
+ */
+void AudioBufferInPlaceScale(float* aBlock, float aScale, uint32_t aSize);
+
+/**
+ * a-rate in place gain.
+ */
+void AudioBlockInPlaceScale(float aBlock[WEBAUDIO_BLOCK_SIZE],
+ float aScale[WEBAUDIO_BLOCK_SIZE]);
+/**
+ * a-rate in place gain.
+ */
+void AudioBufferInPlaceScale(float* aBlock, float* aScale, uint32_t aSize);
+
+/**
+ * Upmix a mono input to a stereo output, scaling the two output channels by two
+ * different gain value.
+ * This algorithm is specified in the WebAudio spec.
+ */
+void AudioBlockPanMonoToStereo(const float aInput[WEBAUDIO_BLOCK_SIZE],
+ float aGainL, float aGainR,
+ float aOutputL[WEBAUDIO_BLOCK_SIZE],
+ float aOutputR[WEBAUDIO_BLOCK_SIZE]);
+
+void AudioBlockPanMonoToStereo(const float aInput[WEBAUDIO_BLOCK_SIZE],
+ float aGainL[WEBAUDIO_BLOCK_SIZE],
+ float aGainR[WEBAUDIO_BLOCK_SIZE],
+ float aOutputL[WEBAUDIO_BLOCK_SIZE],
+ float aOutputR[WEBAUDIO_BLOCK_SIZE]);
+/**
+ * Pan a stereo source according to right and left gain, and the position
+ * (whether the listener is on the left of the source or not).
+ * This algorithm is specified in the WebAudio spec.
+ */
+void AudioBlockPanStereoToStereo(const float aInputL[WEBAUDIO_BLOCK_SIZE],
+ const float aInputR[WEBAUDIO_BLOCK_SIZE],
+ float aGainL, float aGainR, bool aIsOnTheLeft,
+ float aOutputL[WEBAUDIO_BLOCK_SIZE],
+ float aOutputR[WEBAUDIO_BLOCK_SIZE]);
+void AudioBlockPanStereoToStereo(const float aInputL[WEBAUDIO_BLOCK_SIZE],
+ const float aInputR[WEBAUDIO_BLOCK_SIZE],
+ const float aGainL[WEBAUDIO_BLOCK_SIZE],
+ const float aGainR[WEBAUDIO_BLOCK_SIZE],
+ const bool aIsOnTheLeft[WEBAUDIO_BLOCK_SIZE],
+ float aOutputL[WEBAUDIO_BLOCK_SIZE],
+ float aOutputR[WEBAUDIO_BLOCK_SIZE]);
+
+/**
+ * Replace NaN by zeros in aSamples.
+ */
+void NaNToZeroInPlace(float* aSamples, size_t aCount);
+
+/**
+ * Return the sum of squares of all of the samples in the input.
+ */
+float AudioBufferSumOfSquares(const float* aInput, uint32_t aLength);
+
+/**
+ * All methods of this class and its subclasses are called on the
+ * MediaTrackGraph thread.
+ */
+class AudioNodeEngine {
+ public:
+ // This should be compatible with AudioNodeTrack::OutputChunks.
+ typedef AutoTArray<AudioBlock, 1> OutputChunks;
+
+ explicit AudioNodeEngine(dom::AudioNode* aNode);
+
+ virtual ~AudioNodeEngine() {
+ MOZ_ASSERT(!mNode, "The node reference must be already cleared");
+ MOZ_COUNT_DTOR(AudioNodeEngine);
+ }
+
+ virtual dom::DelayNodeEngine* AsDelayNodeEngine() { return nullptr; }
+
+ virtual void SetTrackTimeParameter(uint32_t aIndex, TrackTime aParam) {
+ NS_ERROR("Invalid SetTrackTimeParameter index");
+ }
+ virtual void SetDoubleParameter(uint32_t aIndex, double aParam) {
+ NS_ERROR("Invalid SetDoubleParameter index");
+ }
+ virtual void SetInt32Parameter(uint32_t aIndex, int32_t aParam) {
+ NS_ERROR("Invalid SetInt32Parameter index");
+ }
+ virtual void RecvTimelineEvent(uint32_t aIndex,
+ dom::AudioTimelineEvent& aValue) {
+ NS_ERROR("Invalid RecvTimelineEvent index");
+ }
+ virtual void SetBuffer(AudioChunk&& aBuffer) {
+ NS_ERROR("SetBuffer called on engine that doesn't support it");
+ }
+ // This consumes the contents of aData. aData will be emptied after this
+ // returns.
+ virtual void SetRawArrayData(nsTArray<float>&& aData) {
+ NS_ERROR("SetRawArrayData called on an engine that doesn't support it");
+ }
+
+ virtual void SetReverb(WebCore::Reverb* aBuffer,
+ uint32_t aImpulseChannelCount) {
+ NS_ERROR("SetReverb called on engine that doesn't support it");
+ }
+
+ /**
+ * Produce the next block of audio samples, given input samples aInput
+ * (the mixed data for input 0).
+ * aInput is guaranteed to have float sample format (if it has samples at all)
+ * and to have been resampled to the sampling rate for the track, and to have
+ * exactly WEBAUDIO_BLOCK_SIZE samples.
+ * *aFinished is set to false by the caller. The callee must not set this to
+ * true unless silent output is produced. If set to true, we'll finish the
+ * track, consider this input inactive on any downstream nodes, and not
+ * call this again.
+ */
+ virtual void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom,
+ const AudioBlock& aInput, AudioBlock* aOutput,
+ bool* aFinished);
+ /**
+ * Produce the next block of audio samples, before input is provided.
+ * ProcessBlock() will be called later, and it then should not change
+ * aOutput. This is used only for DelayNodeEngine in a feedback loop.
+ */
+ virtual void ProduceBlockBeforeInput(AudioNodeTrack* aTrack, GraphTime aFrom,
+ AudioBlock* aOutput) {
+ MOZ_ASSERT_UNREACHABLE("ProduceBlockBeforeInput called on wrong engine");
+ }
+
+ /**
+ * Produce the next block of audio samples, given input samples in the aInput
+ * array. There is one input sample per port in aInput, in order.
+ * This is the multi-input/output version of ProcessBlock. Only one kind
+ * of ProcessBlock is called on each node. ProcessBlocksOnPorts() is called
+ * instead of ProcessBlock() if either the number of inputs or the number of
+ * outputs is greater than 1.
+ *
+ * The numbers of AudioBlocks in aInput and aOutput are always guaranteed to
+ * match the numbers of inputs and outputs for the node.
+ */
+ virtual void ProcessBlocksOnPorts(AudioNodeTrack* aTrack, GraphTime aFrom,
+ Span<const AudioBlock> aInput,
+ Span<AudioBlock> aOutput, bool* aFinished);
+
+ // IsActive() returns true if the engine needs to continue processing an
+ // unfinished track even when it has silent or no input connections. This
+ // includes tail-times and when sources have been scheduled to start. If
+ // returning false, then the track can be suspended.
+ virtual bool IsActive() const { return false; }
+
+ // Called on graph thread when the engine will not be used again.
+ virtual void OnGraphThreadDone() {}
+
+ bool HasNode() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return !!mNode;
+ }
+
+ dom::AudioNode* NodeMainThread() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mNode;
+ }
+
+ void ClearNode() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mNode != nullptr);
+ mNode = nullptr;
+ }
+
+ uint16_t InputCount() const { return mInputCount; }
+ uint16_t OutputCount() const { return mOutputCount; }
+
+ virtual size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ // NB: |mNode| is tracked separately so it is excluded here.
+ return 0;
+ }
+
+ virtual size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ void SizeOfIncludingThis(MallocSizeOf aMallocSizeOf,
+ AudioNodeSizes& aUsage) const {
+ aUsage.mEngine = SizeOfIncludingThis(aMallocSizeOf);
+ aUsage.mNodeType = mNodeType;
+ }
+
+ private:
+ // This is cleared from AudioNode::DestroyMediaTrack()
+ dom::AudioNode* MOZ_NON_OWNING_REF mNode; // main thread only
+ const char* const mNodeType;
+ const uint16_t mInputCount;
+ const uint16_t mOutputCount;
+
+ protected:
+ const RefPtr<AbstractThread> mAbstractMainThread;
+};
+
+} // namespace mozilla
+
+#endif /* MOZILLA_AUDIONODEENGINE_H_ */
diff --git a/dom/media/webaudio/AudioNodeEngineGeneric.h b/dom/media/webaudio/AudioNodeEngineGeneric.h
new file mode 100644
index 0000000000..de436ebc8d
--- /dev/null
+++ b/dom/media/webaudio/AudioNodeEngineGeneric.h
@@ -0,0 +1,58 @@
+/* -*- mode: c++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* this source code form is subject to the terms of the mozilla public
+ * license, v. 2.0. if a copy of the mpl was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_AUDIONODEENGINEGENERIC_H_
+#define MOZILLA_AUDIONODEENGINEGENERIC_H_
+
+#include "AudioNodeEngine.h"
+
+#include "xsimd/xsimd.hpp"
+
+namespace mozilla {
+
+template <class Arch>
+struct Engine {
+ static void AudioBufferAddWithScale(const float* aInput, float aScale,
+ float* aOutput, uint32_t aSize);
+
+ static void AudioBlockCopyChannelWithScale(const float* aInput, float aScale,
+ float* aOutput);
+
+ static void AudioBlockCopyChannelWithScale(
+ const float aInput[WEBAUDIO_BLOCK_SIZE],
+ const float aScale[WEBAUDIO_BLOCK_SIZE],
+ float aOutput[WEBAUDIO_BLOCK_SIZE]);
+
+ static void AudioBufferInPlaceScale(float* aBlock, float aScale,
+ uint32_t aSize);
+
+ static void AudioBufferInPlaceScale(float* aBlock, float* aScale,
+ uint32_t aSize);
+
+ static void AudioBlockPanStereoToStereo(
+ const float aInputL[WEBAUDIO_BLOCK_SIZE],
+ const float aInputR[WEBAUDIO_BLOCK_SIZE], float aGainL, float aGainR,
+ bool aIsOnTheLeft, float aOutputL[WEBAUDIO_BLOCK_SIZE],
+ float aOutputR[WEBAUDIO_BLOCK_SIZE]);
+
+ static void BufferComplexMultiply(const float* aInput, const float* aScale,
+ float* aOutput, uint32_t aSize);
+
+ static float AudioBufferSumOfSquares(const float* aInput, uint32_t aLength);
+
+ static void NaNToZeroInPlace(float* aSamples, size_t aCount);
+
+ static void AudioBlockPanStereoToStereo(
+ const float aInputL[WEBAUDIO_BLOCK_SIZE],
+ const float aInputR[WEBAUDIO_BLOCK_SIZE],
+ const float aGainL[WEBAUDIO_BLOCK_SIZE],
+ const float aGainR[WEBAUDIO_BLOCK_SIZE],
+ const bool aIsOnTheLeft[WEBAUDIO_BLOCK_SIZE],
+ float aOutputL[WEBAUDIO_BLOCK_SIZE], float aOutputR[WEBAUDIO_BLOCK_SIZE]);
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webaudio/AudioNodeEngineGenericImpl.h b/dom/media/webaudio/AudioNodeEngineGenericImpl.h
new file mode 100644
index 0000000000..b7fabc7a0b
--- /dev/null
+++ b/dom/media/webaudio/AudioNodeEngineGenericImpl.h
@@ -0,0 +1,341 @@
+/* -*- mode: c++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* this source code form is subject to the terms of the mozilla public
+ * license, v. 2.0. if a copy of the mpl was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_AUDIONODEENGINEGENERICIMPL_H_
+#define MOZILLA_AUDIONODEENGINEGENERICIMPL_H_
+
+#include "AudioNodeEngineGeneric.h"
+#include "AlignmentUtils.h"
+
+#if defined(__GNUC__) && __GNUC__ > 7
+# define MOZ_PRAGMA(tokens) _Pragma(#tokens)
+# define MOZ_UNROLL(factor) MOZ_PRAGMA(GCC unroll factor)
+#elif defined(__INTEL_COMPILER) || (defined(__clang__) && __clang_major__ > 3)
+# define MOZ_PRAGMA(tokens) _Pragma(#tokens)
+# define MOZ_UNROLL(factor) MOZ_PRAGMA(unroll factor)
+#else
+# define MOZ_UNROLL(_)
+#endif
+
+namespace mozilla {
+
+template <class Arch>
+static bool is_aligned(const void* ptr) {
+ return (reinterpret_cast<uintptr_t>(ptr) &
+ ~(static_cast<uintptr_t>(Arch::alignment()) - 1)) ==
+ reinterpret_cast<uintptr_t>(ptr);
+};
+
+template <class Arch>
+void Engine<Arch>::AudioBufferAddWithScale(const float* aInput, float aScale,
+ float* aOutput, uint32_t aSize) {
+ if constexpr (Arch::requires_alignment()) {
+ if (aScale == 1.0f) {
+ while (!is_aligned<Arch>(aInput) || !is_aligned<Arch>(aOutput)) {
+ if (!aSize) return;
+ *aOutput += *aInput;
+ ++aOutput;
+ ++aInput;
+ --aSize;
+ }
+ } else {
+ while (!is_aligned<Arch>(aInput) || !is_aligned<Arch>(aOutput)) {
+ if (!aSize) return;
+ *aOutput += *aInput * aScale;
+ ++aOutput;
+ ++aInput;
+ --aSize;
+ }
+ }
+ }
+ MOZ_ASSERT(is_aligned<Arch>(aInput), "aInput is aligned");
+ MOZ_ASSERT(is_aligned<Arch>(aOutput), "aOutput is aligned");
+
+ xsimd::batch<float, Arch> vgain(aScale);
+
+ uint32_t aVSize = aSize & ~(xsimd::batch<float, Arch>::size - 1);
+ MOZ_UNROLL(4)
+ for (unsigned i = 0; i < aVSize; i += xsimd::batch<float, Arch>::size) {
+ auto vin1 = xsimd::batch<float, Arch>::load_aligned(&aInput[i]);
+ auto vin2 = xsimd::batch<float, Arch>::load_aligned(&aOutput[i]);
+ auto vout = xsimd::fma(vin1, vgain, vin2);
+ vout.store_aligned(&aOutput[i]);
+ }
+
+ for (unsigned i = aVSize; i < aSize; ++i) {
+ aOutput[i] += aInput[i] * aScale;
+ }
+}
+
+template <class Arch>
+void Engine<Arch>::AudioBlockCopyChannelWithScale(const float* aInput,
+ float aScale,
+ float* aOutput) {
+ MOZ_ASSERT(is_aligned<Arch>(aInput), "aInput is aligned");
+ MOZ_ASSERT(is_aligned<Arch>(aOutput), "aOutput is aligned");
+
+ MOZ_ASSERT((WEBAUDIO_BLOCK_SIZE % xsimd::batch<float, Arch>::size == 0),
+ "requires tail processing");
+
+ xsimd::batch<float, Arch> vgain = (aScale);
+
+ MOZ_UNROLL(4)
+ for (unsigned i = 0; i < WEBAUDIO_BLOCK_SIZE;
+ i += xsimd::batch<float, Arch>::size) {
+ auto vin = xsimd::batch<float, Arch>::load_aligned(&aInput[i]);
+ auto vout = vin * vgain;
+ vout.store_aligned(&aOutput[i]);
+ }
+};
+
+template <class Arch>
+void Engine<Arch>::AudioBlockCopyChannelWithScale(
+ const float aInput[WEBAUDIO_BLOCK_SIZE],
+ const float aScale[WEBAUDIO_BLOCK_SIZE],
+ float aOutput[WEBAUDIO_BLOCK_SIZE]) {
+ MOZ_ASSERT(is_aligned<Arch>(aInput), "aInput is aligned");
+ MOZ_ASSERT(is_aligned<Arch>(aOutput), "aOutput is aligned");
+ MOZ_ASSERT(is_aligned<Arch>(aScale), "aScale is aligned");
+
+ MOZ_ASSERT((WEBAUDIO_BLOCK_SIZE % xsimd::batch<float, Arch>::size == 0),
+ "requires tail processing");
+
+ MOZ_UNROLL(4)
+ for (unsigned i = 0; i < WEBAUDIO_BLOCK_SIZE;
+ i += xsimd::batch<float, Arch>::size) {
+ auto vscaled = xsimd::batch<float, Arch>::load_aligned(&aScale[i]);
+ auto vin = xsimd::batch<float, Arch>::load_aligned(&aInput[i]);
+ auto vout = vin * vscaled;
+ vout.store_aligned(&aOutput[i]);
+ }
+};
+
+template <class Arch>
+void Engine<Arch>::AudioBufferInPlaceScale(float* aBlock, float aScale,
+ uint32_t aSize) {
+ MOZ_ASSERT(is_aligned<Arch>(aBlock), "aBlock is aligned");
+
+ xsimd::batch<float, Arch> vgain(aScale);
+
+ uint32_t aVSize = aSize & ~(xsimd::batch<float, Arch>::size - 1);
+ MOZ_UNROLL(4)
+ for (unsigned i = 0; i < aVSize; i += xsimd::batch<float, Arch>::size) {
+ auto vin = xsimd::batch<float, Arch>::load_aligned(&aBlock[i]);
+ auto vout = vin * vgain;
+ vout.store_aligned(&aBlock[i]);
+ }
+ for (unsigned i = aVSize; i < aSize; ++i) aBlock[i] *= aScale;
+};
+
+template <class Arch>
+void Engine<Arch>::AudioBufferInPlaceScale(float* aBlock, float* aScale,
+ uint32_t aSize) {
+ MOZ_ASSERT(is_aligned<Arch>(aBlock), "aBlock is aligned");
+ MOZ_ASSERT(is_aligned<Arch>(aScale), "aScale is aligned");
+
+ uint32_t aVSize = aSize & ~(xsimd::batch<float, Arch>::size - 1);
+ MOZ_UNROLL(4)
+ for (unsigned i = 0; i < aVSize; i += xsimd::batch<float, Arch>::size) {
+ auto vin = xsimd::batch<float, Arch>::load_aligned(&aBlock[i]);
+ auto vgain = xsimd::batch<float, Arch>::load_aligned(&aScale[i]);
+ auto vout = vin * vgain;
+ vout.store_aligned(&aBlock[i]);
+ }
+ for (uint32_t i = aVSize; i < aSize; ++i) {
+ *aBlock++ *= *aScale++;
+ }
+};
+
+template <class Arch>
+void Engine<Arch>::AudioBlockPanStereoToStereo(
+ const float aInputL[WEBAUDIO_BLOCK_SIZE],
+ const float aInputR[WEBAUDIO_BLOCK_SIZE], float aGainL, float aGainR,
+ bool aIsOnTheLeft, float aOutputL[WEBAUDIO_BLOCK_SIZE],
+ float aOutputR[WEBAUDIO_BLOCK_SIZE]) {
+ MOZ_ASSERT(is_aligned<Arch>(aInputL), "aInputL is aligned");
+ MOZ_ASSERT(is_aligned<Arch>(aInputR), "aInputR is aligned");
+ MOZ_ASSERT(is_aligned<Arch>(aOutputL), "aOutputL is aligned");
+ MOZ_ASSERT(is_aligned<Arch>(aOutputR), "aOutputR is aligned");
+
+ MOZ_ASSERT((WEBAUDIO_BLOCK_SIZE % xsimd::batch<float, Arch>::size == 0),
+ "requires tail processing");
+
+ xsimd::batch<float, Arch> vgainl(aGainL);
+ xsimd::batch<float, Arch> vgainr(aGainR);
+
+ if (aIsOnTheLeft) {
+ MOZ_UNROLL(2)
+ for (unsigned i = 0; i < WEBAUDIO_BLOCK_SIZE;
+ i += xsimd::batch<float, Arch>::size) {
+ auto vinl = xsimd::batch<float, Arch>::load_aligned(&aInputL[i]);
+ auto vinr = xsimd::batch<float, Arch>::load_aligned(&aInputR[i]);
+
+ /* left channel : aOutputL = aInputL + aInputR * gainL */
+ auto vout = xsimd::fma(vinr, vgainl, vinl);
+ vout.store_aligned(&aOutputL[i]);
+
+ /* right channel : aOutputR = aInputR * gainR */
+ auto vscaled = vinr * vgainr;
+ vscaled.store_aligned(&aOutputR[i]);
+ }
+ } else {
+ MOZ_UNROLL(2)
+ for (unsigned i = 0; i < WEBAUDIO_BLOCK_SIZE;
+ i += xsimd::batch<float, Arch>::size) {
+ auto vinl = xsimd::batch<float, Arch>::load_aligned(&aInputL[i]);
+ auto vinr = xsimd::batch<float, Arch>::load_aligned(&aInputR[i]);
+
+ /* left channel : aInputL * gainL */
+ auto vscaled = vinl * vgainl;
+ vscaled.store_aligned(&aOutputL[i]);
+
+ /* right channel: aOutputR = aInputR + aInputL * gainR */
+ auto vout = xsimd::fma(vinl, vgainr, vinr);
+ vout.store_aligned(&aOutputR[i]);
+ }
+ }
+};
+
+template <class Arch>
+void Engine<Arch>::BufferComplexMultiply(const float* aInput,
+ const float* aScale, float* aOutput,
+ uint32_t aSize) {
+ MOZ_ASSERT(is_aligned<Arch>(aInput), "aInput is aligned");
+ MOZ_ASSERT(is_aligned<Arch>(aOutput), "aOutput is aligned");
+ MOZ_ASSERT(is_aligned<Arch>(aScale), "aScale is aligned");
+ MOZ_ASSERT((aSize % xsimd::batch<float, Arch>::size == 0),
+ "requires tail processing");
+
+ MOZ_UNROLL(2)
+ for (unsigned i = 0; i < aSize * 2;
+ i += 2 * xsimd::batch<std::complex<float>, Arch>::size) {
+ auto in1 = xsimd::batch<std::complex<float>, Arch>::load_aligned(
+ reinterpret_cast<const std::complex<float>*>(&aInput[i]));
+ auto in2 = xsimd::batch<std::complex<float>, Arch>::load_aligned(
+ reinterpret_cast<const std::complex<float>*>(&aScale[i]));
+ auto out = in1 * in2;
+ out.store_aligned(reinterpret_cast<std::complex<float>*>(&aOutput[i]));
+ }
+};
+
+template <class Arch>
+float Engine<Arch>::AudioBufferSumOfSquares(const float* aInput,
+ uint32_t aLength) {
+ float sum = 0.f;
+
+ if constexpr (Arch::requires_alignment()) {
+ while (!is_aligned<Arch>(aInput)) {
+ if (!aLength) {
+ return sum;
+ }
+ sum += *aInput * *aInput;
+ ++aInput;
+ --aLength;
+ }
+ }
+
+ MOZ_ASSERT(is_aligned<Arch>(aInput), "aInput is aligned");
+
+ constexpr uint32_t unroll_factor = 4;
+ xsimd::batch<float, Arch> accs[unroll_factor] = {0.f, 0.f, 0.f, 0.f};
+
+ uint32_t vLength =
+ aLength & ~(unroll_factor * xsimd::batch<float, Arch>::size - 1);
+
+ for (uint32_t i = 0; i < vLength;
+ i += unroll_factor * xsimd::batch<float, Arch>::size) {
+ MOZ_UNROLL(4)
+ for (uint32_t j = 0; j < unroll_factor; ++j) {
+ auto in = xsimd::batch<float, Arch>::load_aligned(
+ &aInput[i + xsimd::batch<float, Arch>::size * j]);
+ accs[j] = xsimd::fma(in, in, accs[j]);
+ }
+ }
+
+ sum += reduce_add((accs[0] + accs[1]) + (accs[2] + accs[3]));
+ for (uint32_t i = vLength; i < aLength; ++i) sum += aInput[i] * aInput[i];
+ return sum;
+};
+
+template <class Arch>
+void Engine<Arch>::NaNToZeroInPlace(float* aSamples, size_t aCount) {
+ if constexpr (Arch::requires_alignment()) {
+ while (!is_aligned<Arch>(aSamples)) {
+ if (!aCount) {
+ return;
+ }
+ if (*aSamples != *aSamples) {
+ *aSamples = 0.0;
+ }
+ ++aSamples;
+ --aCount;
+ }
+ }
+
+ MOZ_ASSERT(is_aligned<Arch>(aSamples), "aSamples is aligned");
+
+ uint32_t vCount = aCount & ~(xsimd::batch<float, Arch>::size - 1);
+
+ MOZ_UNROLL(4)
+ for (uint32_t i = 0; i < vCount; i += xsimd::batch<float, Arch>::size) {
+ auto vin = xsimd::batch<float, Arch>::load_aligned(&aSamples[i]);
+ auto vout =
+ xsimd::select(xsimd::isnan(vin), xsimd::batch<float, Arch>(0.f), vin);
+ vout.store_aligned(&aSamples[i]);
+ }
+
+ for (uint32_t i = vCount; i < aCount; i++) {
+ if (aSamples[i] != aSamples[i]) {
+ aSamples[i] = 0.0;
+ }
+ }
+};
+
+template <class Arch>
+void Engine<Arch>::AudioBlockPanStereoToStereo(
+ const float aInputL[WEBAUDIO_BLOCK_SIZE],
+ const float aInputR[WEBAUDIO_BLOCK_SIZE],
+ const float aGainL[WEBAUDIO_BLOCK_SIZE],
+ const float aGainR[WEBAUDIO_BLOCK_SIZE],
+ const bool aIsOnTheLeft[WEBAUDIO_BLOCK_SIZE],
+ float aOutputL[WEBAUDIO_BLOCK_SIZE], float aOutputR[WEBAUDIO_BLOCK_SIZE]) {
+ MOZ_ASSERT(is_aligned<Arch>(aInputL), "aInputL is aligned");
+ MOZ_ASSERT(is_aligned<Arch>(aInputR), "aInputR is aligned");
+ MOZ_ASSERT(is_aligned<Arch>(aGainL), "aGainL is aligned");
+ MOZ_ASSERT(is_aligned<Arch>(aGainR), "aGainR is aligned");
+ MOZ_ASSERT(is_aligned<Arch>(aIsOnTheLeft), "aIsOnTheLeft is aligned");
+ MOZ_ASSERT(is_aligned<Arch>(aOutputL), "aOutputL is aligned");
+ MOZ_ASSERT(is_aligned<Arch>(aOutputR), "aOutputR is aligned");
+
+ MOZ_ASSERT((WEBAUDIO_BLOCK_SIZE % xsimd::batch<float, Arch>::size == 0),
+ "requires tail processing");
+
+ MOZ_UNROLL(2)
+ for (uint32_t i = 0; i < WEBAUDIO_BLOCK_SIZE;
+ i += xsimd::batch<float, Arch>::size) {
+ auto mask = xsimd::batch_bool<float, Arch>::load_aligned(&aIsOnTheLeft[i]);
+
+ auto inputL = xsimd::batch<float, Arch>::load_aligned(&aInputL[i]);
+ auto inputR = xsimd::batch<float, Arch>::load_aligned(&aInputR[i]);
+ auto gainL = xsimd::batch<float, Arch>::load_aligned(&aGainL[i]);
+ auto gainR = xsimd::batch<float, Arch>::load_aligned(&aGainR[i]);
+
+ auto outL_true = xsimd::fma(inputR, gainL, inputL);
+ auto outR_true = inputR * gainR;
+
+ auto outL_false = inputL * gainL;
+ auto outR_false = xsimd::fma(inputL, gainR, inputR);
+
+ auto outL = xsimd::select(mask, outL_true, outL_false);
+ auto outR = xsimd::select(mask, outR_true, outR_false);
+
+ outL.store_aligned(&aOutputL[i]);
+ outR.store_aligned(&aOutputR[i]);
+ }
+}
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webaudio/AudioNodeEngineNEON.cpp b/dom/media/webaudio/AudioNodeEngineNEON.cpp
new file mode 100644
index 0000000000..93f5c85285
--- /dev/null
+++ b/dom/media/webaudio/AudioNodeEngineNEON.cpp
@@ -0,0 +1,9 @@
+/* -*- mode: c++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* this source code form is subject to the terms of the mozilla public
+ * license, v. 2.0. if a copy of the mpl was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioNodeEngineGenericImpl.h"
+namespace mozilla {
+template struct Engine<xsimd::neon>;
+} // namespace mozilla
diff --git a/dom/media/webaudio/AudioNodeEngineSSE2.cpp b/dom/media/webaudio/AudioNodeEngineSSE2.cpp
new file mode 100644
index 0000000000..558cd866fd
--- /dev/null
+++ b/dom/media/webaudio/AudioNodeEngineSSE2.cpp
@@ -0,0 +1,10 @@
+/* -*- mode: c++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* this source code form is subject to the terms of the mozilla public
+ * license, v. 2.0. if a copy of the mpl was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioNodeEngineGenericImpl.h"
+
+namespace mozilla {
+template struct Engine<xsimd::sse2>;
+} // namespace mozilla
diff --git a/dom/media/webaudio/AudioNodeEngineSSE4_2_FMA3.cpp b/dom/media/webaudio/AudioNodeEngineSSE4_2_FMA3.cpp
new file mode 100644
index 0000000000..ce0134ee4b
--- /dev/null
+++ b/dom/media/webaudio/AudioNodeEngineSSE4_2_FMA3.cpp
@@ -0,0 +1,10 @@
+/* -*- mode: c++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* this source code form is subject to the terms of the mozilla public
+ * license, v. 2.0. if a copy of the mpl was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioNodeEngineGenericImpl.h"
+
+namespace mozilla {
+template struct Engine<xsimd::fma3<xsimd::sse4_2>>;
+} // namespace mozilla
diff --git a/dom/media/webaudio/AudioNodeExternalInputTrack.cpp b/dom/media/webaudio/AudioNodeExternalInputTrack.cpp
new file mode 100644
index 0000000000..2142752d39
--- /dev/null
+++ b/dom/media/webaudio/AudioNodeExternalInputTrack.cpp
@@ -0,0 +1,225 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AlignedTArray.h"
+#include "AlignmentUtils.h"
+#include "AudioNodeEngine.h"
+#include "AudioNodeExternalInputTrack.h"
+#include "AudioChannelFormat.h"
+#include "mozilla/dom/MediaStreamAudioSourceNode.h"
+
+using namespace mozilla::dom;
+
+namespace mozilla {
+
+AudioNodeExternalInputTrack::AudioNodeExternalInputTrack(
+ AudioNodeEngine* aEngine, TrackRate aSampleRate)
+ : AudioNodeTrack(aEngine, NO_TRACK_FLAGS, aSampleRate) {
+ MOZ_COUNT_CTOR(AudioNodeExternalInputTrack);
+}
+
+AudioNodeExternalInputTrack::~AudioNodeExternalInputTrack() {
+ MOZ_COUNT_DTOR(AudioNodeExternalInputTrack);
+}
+
+/* static */
+already_AddRefed<AudioNodeExternalInputTrack>
+AudioNodeExternalInputTrack::Create(MediaTrackGraph* aGraph,
+ AudioNodeEngine* aEngine) {
+ AudioContext* ctx = aEngine->NodeMainThread()->Context();
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aGraph == ctx->Graph());
+
+ RefPtr<AudioNodeExternalInputTrack> track =
+ new AudioNodeExternalInputTrack(aEngine, aGraph->GraphRate());
+ track->mSuspendedCount += ctx->ShouldSuspendNewTrack();
+ aGraph->AddTrack(track);
+ return track.forget();
+}
+
+/**
+ * Copies the data in aInput to aOffsetInBlock within aBlock.
+ * aBlock must have been allocated with AllocateInputBlock and have a channel
+ * count that's a superset of the channels in aInput.
+ */
+template <typename T>
+static void CopyChunkToBlock(AudioChunk& aInput, AudioBlock* aBlock,
+ uint32_t aOffsetInBlock) {
+ uint32_t blockChannels = aBlock->ChannelCount();
+ AutoTArray<const T*, 2> channels;
+ if (aInput.IsNull()) {
+ channels.SetLength(blockChannels);
+ PodZero(channels.Elements(), blockChannels);
+ } else {
+ const nsTArray<const T*>& inputChannels = aInput.ChannelData<T>();
+ channels.SetLength(inputChannels.Length());
+ PodCopy(channels.Elements(), inputChannels.Elements(), channels.Length());
+ if (channels.Length() != blockChannels) {
+ // We only need to upmix here because aBlock's channel count has been
+ // chosen to be a superset of the channel count of every chunk.
+ AudioChannelsUpMix(&channels, blockChannels, static_cast<T*>(nullptr));
+ }
+ }
+
+ for (uint32_t c = 0; c < blockChannels; ++c) {
+ float* outputData = aBlock->ChannelFloatsForWrite(c) + aOffsetInBlock;
+ if (channels[c]) {
+ ConvertAudioSamplesWithScale(channels[c], outputData,
+ aInput.GetDuration(), aInput.mVolume);
+ } else {
+ PodZero(outputData, aInput.GetDuration());
+ }
+ }
+}
+
+/**
+ * Converts the data in aSegment to a single chunk aBlock. aSegment must have
+ * duration WEBAUDIO_BLOCK_SIZE. aFallbackChannelCount is a superset of the
+ * channels in every chunk of aSegment. aBlock must be float format or null.
+ */
+static void ConvertSegmentToAudioBlock(AudioSegment* aSegment,
+ AudioBlock* aBlock,
+ int32_t aFallbackChannelCount) {
+ NS_ASSERTION(aSegment->GetDuration() == WEBAUDIO_BLOCK_SIZE,
+ "Bad segment duration");
+
+ {
+ AudioSegment::ChunkIterator ci(*aSegment);
+ NS_ASSERTION(!ci.IsEnded(), "Should be at least one chunk!");
+ if (ci->GetDuration() == WEBAUDIO_BLOCK_SIZE &&
+ (ci->IsNull() || ci->mBufferFormat == AUDIO_FORMAT_FLOAT32)) {
+ bool aligned = true;
+ for (size_t i = 0; i < ci->mChannelData.Length(); ++i) {
+ if (!IS_ALIGNED16(ci->mChannelData[i])) {
+ aligned = false;
+ break;
+ }
+ }
+
+ // Return this chunk directly to avoid copying data.
+ if (aligned) {
+ *aBlock = *ci;
+ return;
+ }
+ }
+ }
+
+ aBlock->AllocateChannels(aFallbackChannelCount);
+
+ uint32_t duration = 0;
+ for (AudioSegment::ChunkIterator ci(*aSegment); !ci.IsEnded(); ci.Next()) {
+ switch (ci->mBufferFormat) {
+ case AUDIO_FORMAT_S16: {
+ CopyChunkToBlock<int16_t>(*ci, aBlock, duration);
+ break;
+ }
+ case AUDIO_FORMAT_FLOAT32: {
+ CopyChunkToBlock<float>(*ci, aBlock, duration);
+ break;
+ }
+ case AUDIO_FORMAT_SILENCE: {
+ // The actual type of the sample does not matter here, but we still need
+ // to send some audio to the graph.
+ CopyChunkToBlock<float>(*ci, aBlock, duration);
+ break;
+ }
+ }
+ duration += ci->GetDuration();
+ }
+}
+
+void AudioNodeExternalInputTrack::ProcessInput(GraphTime aFrom, GraphTime aTo,
+ uint32_t aFlags) {
+ // According to spec, number of outputs is always 1.
+ MOZ_ASSERT(mLastChunks.Length() == 1);
+
+ // GC stuff can result in our input track being destroyed before this track.
+ // Handle that.
+ if (!IsEnabled() || mInputs.IsEmpty() || mPassThrough) {
+ mLastChunks[0].SetNull(WEBAUDIO_BLOCK_SIZE);
+ return;
+ }
+
+ MOZ_ASSERT(mInputs.Length() == 1);
+
+ MediaTrack* source = mInputs[0]->GetSource();
+ AutoTArray<AudioSegment, 1> audioSegments;
+ uint32_t inputChannels = 0;
+
+ MOZ_ASSERT(source->GetData()->GetType() == MediaSegment::AUDIO,
+ "AudioNodeExternalInputTrack shouldn't have a video input");
+
+ const AudioSegment& inputSegment =
+ *mInputs[0]->GetSource()->GetData<AudioSegment>();
+ if (!inputSegment.IsNull()) {
+ AudioSegment& segment = *audioSegments.AppendElement();
+ GraphTime next;
+ for (GraphTime t = aFrom; t < aTo; t = next) {
+ MediaInputPort::InputInterval interval =
+ MediaInputPort::GetNextInputInterval(mInputs[0], t);
+ interval.mEnd = std::min(interval.mEnd, aTo);
+ if (interval.mStart >= interval.mEnd) {
+ break;
+ }
+ next = interval.mEnd;
+
+ // We know this track does not block during the processing interval ---
+ // we're not finished, we don't underrun, and we're not suspended.
+ TrackTime outputStart = GraphTimeToTrackTime(interval.mStart);
+ TrackTime outputEnd = GraphTimeToTrackTime(interval.mEnd);
+ TrackTime ticks = outputEnd - outputStart;
+
+ if (interval.mInputIsBlocked) {
+ segment.AppendNullData(ticks);
+ } else {
+ // The input track is not blocked in this interval, so no need to call
+ // GraphTimeToTrackTimeWithBlocking.
+ TrackTime inputStart =
+ std::min(inputSegment.GetDuration(),
+ source->GraphTimeToTrackTime(interval.mStart));
+ TrackTime inputEnd =
+ std::min(inputSegment.GetDuration(),
+ source->GraphTimeToTrackTime(interval.mEnd));
+
+ segment.AppendSlice(inputSegment, inputStart, inputEnd);
+ // Pad if we're looking past the end of the track
+ segment.AppendNullData(ticks - (inputEnd - inputStart));
+ }
+ }
+
+ for (AudioSegment::ChunkIterator iter(segment); !iter.IsEnded();
+ iter.Next()) {
+ inputChannels =
+ GetAudioChannelsSuperset(inputChannels, iter->ChannelCount());
+ }
+ }
+
+ uint32_t accumulateIndex = 0;
+ if (inputChannels) {
+ DownmixBufferType downmixBuffer;
+ ASSERT_ALIGNED16(downmixBuffer.Elements());
+ for (auto& audioSegment : audioSegments) {
+ AudioBlock tmpChunk;
+ ConvertSegmentToAudioBlock(&audioSegment, &tmpChunk, inputChannels);
+ if (!tmpChunk.IsNull()) {
+ if (accumulateIndex == 0) {
+ mLastChunks[0].AllocateChannels(inputChannels);
+ }
+ AccumulateInputChunk(accumulateIndex, tmpChunk, &mLastChunks[0],
+ &downmixBuffer);
+ accumulateIndex++;
+ }
+ }
+ }
+ if (accumulateIndex == 0) {
+ mLastChunks[0].SetNull(WEBAUDIO_BLOCK_SIZE);
+ }
+}
+
+bool AudioNodeExternalInputTrack::IsEnabled() {
+ return ((MediaStreamAudioSourceNodeEngine*)Engine())->IsEnabled();
+}
+
+} // namespace mozilla
diff --git a/dom/media/webaudio/AudioNodeExternalInputTrack.h b/dom/media/webaudio/AudioNodeExternalInputTrack.h
new file mode 100644
index 0000000000..f273fee696
--- /dev/null
+++ b/dom/media/webaudio/AudioNodeExternalInputTrack.h
@@ -0,0 +1,46 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_AUDIONODEEXTERNALINPUTTRACK_H_
+#define MOZILLA_AUDIONODEEXTERNALINPUTTRACK_H_
+
+#include "MediaTrackGraph.h"
+#include "AudioNodeTrack.h"
+#include "mozilla/Atomics.h"
+
+namespace mozilla {
+
+class AbstractThread;
+
+/**
+ * This is a MediaTrack implementation that acts for a Web Audio node but
+ * unlike other AudioNodeTracks, supports any kind of MediaTrack as an
+ * input --- handling any number of audio tracks and handling blocking of
+ * the input MediaTrack.
+ */
+class AudioNodeExternalInputTrack final : public AudioNodeTrack {
+ public:
+ static already_AddRefed<AudioNodeExternalInputTrack> Create(
+ MediaTrackGraph* aGraph, AudioNodeEngine* aEngine);
+
+ protected:
+ AudioNodeExternalInputTrack(AudioNodeEngine* aEngine, TrackRate aSampleRate);
+ ~AudioNodeExternalInputTrack();
+
+ public:
+ void ProcessInput(GraphTime aFrom, GraphTime aTo, uint32_t aFlags) override;
+
+ private:
+ /**
+ * Determines if this is enabled or not. Disabled nodes produce silence.
+ * This node becomes disabled if the document principal does not subsume the
+ * DOMMediaStream principal.
+ */
+ bool IsEnabled();
+};
+
+} // namespace mozilla
+
+#endif /* MOZILLA_AUDIONODEEXTERNALINPUTTRACK_H_ */
diff --git a/dom/media/webaudio/AudioNodeTrack.cpp b/dom/media/webaudio/AudioNodeTrack.cpp
new file mode 100644
index 0000000000..7444c76822
--- /dev/null
+++ b/dom/media/webaudio/AudioNodeTrack.cpp
@@ -0,0 +1,723 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioNodeTrack.h"
+
+#include "MediaTrackGraphImpl.h"
+#include "MediaTrackListener.h"
+#include "AudioNodeEngine.h"
+#include "ThreeDPoint.h"
+#include "Tracing.h"
+#include "AudioChannelFormat.h"
+#include "AudioParamTimeline.h"
+#include "AudioContext.h"
+#include "nsMathUtils.h"
+#include "AlignmentUtils.h"
+#include "blink/Reverb.h"
+
+using namespace mozilla::dom;
+
+namespace mozilla {
+
+/**
+ * An AudioNodeTrack produces a single audio track with ID
+ * AUDIO_TRACK. This track has rate AudioContext::sIdealAudioRate
+ * for regular audio contexts, and the rate requested by the web content
+ * for offline audio contexts.
+ * Each chunk in the track is a single block of WEBAUDIO_BLOCK_SIZE samples.
+ * Note: This must be a different value than MEDIA_STREAM_DEST_TRACK_ID
+ */
+
+AudioNodeTrack::AudioNodeTrack(AudioNodeEngine* aEngine, Flags aFlags,
+ TrackRate aSampleRate)
+ : ProcessedMediaTrack(
+ aSampleRate, MediaSegment::AUDIO,
+ (aFlags & EXTERNAL_OUTPUT) ? new AudioSegment() : nullptr),
+ mEngine(aEngine),
+ mFlags(aFlags),
+ mNumberOfInputChannels(2),
+ mIsActive(aEngine->IsActive()),
+ mMarkAsEndedAfterThisBlock(false),
+ mAudioParamTrack(false),
+ mPassThrough(false) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mSuspendedCount = !(mIsActive || mFlags & EXTERNAL_OUTPUT);
+ mChannelCountMode = ChannelCountMode::Max;
+ mChannelInterpretation = ChannelInterpretation::Speakers;
+ mLastChunks.SetLength(std::max(uint16_t(1), mEngine->OutputCount()));
+ MOZ_COUNT_CTOR(AudioNodeTrack);
+}
+
+AudioNodeTrack::~AudioNodeTrack() {
+ MOZ_ASSERT(mActiveInputCount == 0);
+ MOZ_COUNT_DTOR(AudioNodeTrack);
+}
+
+void AudioNodeTrack::OnGraphThreadDone() { mEngine->OnGraphThreadDone(); }
+
+void AudioNodeTrack::DestroyImpl() {
+ // These are graph thread objects, so clean up on graph thread.
+ mInputChunks.Clear();
+ mLastChunks.Clear();
+
+ ProcessedMediaTrack::DestroyImpl();
+}
+
+/* static */
+already_AddRefed<AudioNodeTrack> AudioNodeTrack::Create(
+ AudioContext* aCtx, AudioNodeEngine* aEngine, Flags aFlags,
+ MediaTrackGraph* aGraph) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_RELEASE_ASSERT(aGraph);
+
+ // MediaRecorders use an AudioNodeTrack, but no AudioNode
+ AudioNode* node = aEngine->NodeMainThread();
+
+ RefPtr<AudioNodeTrack> track =
+ new AudioNodeTrack(aEngine, aFlags, aGraph->GraphRate());
+ if (node) {
+ track->SetChannelMixingParametersImpl(node->ChannelCount(),
+ node->ChannelCountModeValue(),
+ node->ChannelInterpretationValue());
+ }
+ // All realtime tracks are initially suspended.
+ // ApplyAudioContextOperation() is used to start tracks so that a new track
+ // will not be started before the existing tracks, which may be awaiting an
+ // AudioCallbackDriver to resume.
+ bool isRealtime = !aCtx->IsOffline();
+ track->mSuspendedCount += isRealtime;
+ aGraph->AddTrack(track);
+ if (isRealtime && !aCtx->ShouldSuspendNewTrack()) {
+ nsTArray<RefPtr<mozilla::MediaTrack>> tracks;
+ tracks.AppendElement(track);
+ aGraph->ApplyAudioContextOperation(aCtx->DestinationTrack(),
+ std::move(tracks),
+ AudioContextOperation::Resume);
+ }
+ return track.forget();
+}
+
+size_t AudioNodeTrack::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t amount = 0;
+
+ // Not reported:
+ // - mEngine
+
+ amount += ProcessedMediaTrack::SizeOfExcludingThis(aMallocSizeOf);
+ amount += mLastChunks.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (size_t i = 0; i < mLastChunks.Length(); i++) {
+ // NB: This is currently unshared only as there are instances of
+ // double reporting in DMD otherwise.
+ amount += mLastChunks[i].SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ }
+
+ return amount;
+}
+
+size_t AudioNodeTrack::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
+
+void AudioNodeTrack::SizeOfAudioNodesIncludingThis(
+ MallocSizeOf aMallocSizeOf, AudioNodeSizes& aUsage) const {
+ // Explicitly separate out the track memory.
+ aUsage.mTrack = SizeOfIncludingThis(aMallocSizeOf);
+
+ if (mEngine) {
+ // This will fill out the rest of |aUsage|.
+ mEngine->SizeOfIncludingThis(aMallocSizeOf, aUsage);
+ }
+}
+
+void AudioNodeTrack::SetTrackTimeParameter(uint32_t aIndex,
+ AudioContext* aContext,
+ double aTrackTime) {
+ class Message final : public ControlMessage {
+ public:
+ Message(AudioNodeTrack* aTrack, uint32_t aIndex,
+ MediaTrack* aRelativeToTrack, double aTrackTime)
+ : ControlMessage(aTrack),
+ mTrackTime(aTrackTime),
+ mRelativeToTrack(aRelativeToTrack),
+ mIndex(aIndex) {}
+ void Run() override {
+ TRACE("AudioNodeTrack::SetTrackTimeParameterImpl");
+ static_cast<AudioNodeTrack*>(mTrack)->SetTrackTimeParameterImpl(
+ mIndex, mRelativeToTrack, mTrackTime);
+ }
+ double mTrackTime;
+ MediaTrack* MOZ_UNSAFE_REF(
+ "ControlMessages are processed in order. This \
+destination track is not yet destroyed. Its (future) destroy message will be \
+processed after this message.") mRelativeToTrack;
+ uint32_t mIndex;
+ };
+
+ GraphImpl()->AppendMessage(MakeUnique<Message>(
+ this, aIndex, aContext->DestinationTrack(), aTrackTime));
+}
+
+void AudioNodeTrack::SetTrackTimeParameterImpl(uint32_t aIndex,
+ MediaTrack* aRelativeToTrack,
+ double aTrackTime) {
+ TrackTime ticks = aRelativeToTrack->SecondsToNearestTrackTime(aTrackTime);
+ mEngine->SetTrackTimeParameter(aIndex, ticks);
+}
+
+void AudioNodeTrack::SetDoubleParameter(uint32_t aIndex, double aValue) {
+ class Message final : public ControlMessage {
+ public:
+ Message(AudioNodeTrack* aTrack, uint32_t aIndex, double aValue)
+ : ControlMessage(aTrack), mValue(aValue), mIndex(aIndex) {}
+ void Run() override {
+ TRACE("AudioNodeTrack::SetDoubleParameter");
+ static_cast<AudioNodeTrack*>(mTrack)->Engine()->SetDoubleParameter(
+ mIndex, mValue);
+ }
+ double mValue;
+ uint32_t mIndex;
+ };
+
+ GraphImpl()->AppendMessage(MakeUnique<Message>(this, aIndex, aValue));
+}
+
+void AudioNodeTrack::SetInt32Parameter(uint32_t aIndex, int32_t aValue) {
+ class Message final : public ControlMessage {
+ public:
+ Message(AudioNodeTrack* aTrack, uint32_t aIndex, int32_t aValue)
+ : ControlMessage(aTrack), mValue(aValue), mIndex(aIndex) {}
+ void Run() override {
+ TRACE("AudioNodeTrack::SetInt32Parameter");
+ static_cast<AudioNodeTrack*>(mTrack)->Engine()->SetInt32Parameter(mIndex,
+ mValue);
+ }
+ int32_t mValue;
+ uint32_t mIndex;
+ };
+
+ GraphImpl()->AppendMessage(MakeUnique<Message>(this, aIndex, aValue));
+}
+
+void AudioNodeTrack::SendTimelineEvent(uint32_t aIndex,
+ const AudioTimelineEvent& aEvent) {
+ class Message final : public ControlMessage {
+ public:
+ Message(AudioNodeTrack* aTrack, uint32_t aIndex,
+ const AudioTimelineEvent& aEvent)
+ : ControlMessage(aTrack),
+ mEvent(aEvent),
+ mSampleRate(aTrack->mSampleRate),
+ mIndex(aIndex) {}
+ void Run() override {
+ TRACE("AudioNodeTrack::RecvTimelineEvent");
+ static_cast<AudioNodeTrack*>(mTrack)->Engine()->RecvTimelineEvent(mIndex,
+ mEvent);
+ }
+ AudioTimelineEvent mEvent;
+ TrackRate mSampleRate;
+ uint32_t mIndex;
+ };
+ GraphImpl()->AppendMessage(MakeUnique<Message>(this, aIndex, aEvent));
+}
+
+void AudioNodeTrack::SetBuffer(AudioChunk&& aBuffer) {
+ class Message final : public ControlMessage {
+ public:
+ Message(AudioNodeTrack* aTrack, AudioChunk&& aBuffer)
+ : ControlMessage(aTrack), mBuffer(aBuffer) {}
+ void Run() override {
+ TRACE("AudioNodeTrack::SetBuffer");
+ static_cast<AudioNodeTrack*>(mTrack)->Engine()->SetBuffer(
+ std::move(mBuffer));
+ }
+ AudioChunk mBuffer;
+ };
+
+ GraphImpl()->AppendMessage(MakeUnique<Message>(this, std::move(aBuffer)));
+}
+
+void AudioNodeTrack::SetReverb(WebCore::Reverb* aReverb,
+ uint32_t aImpulseChannelCount) {
+ class Message final : public ControlMessage {
+ public:
+ Message(AudioNodeTrack* aTrack, WebCore::Reverb* aReverb,
+ uint32_t aImpulseChannelCount)
+ : ControlMessage(aTrack),
+ mReverb(aReverb),
+ mImpulseChanelCount(aImpulseChannelCount) {}
+ void Run() override {
+ TRACE("AudioNodeTrack::SetReverb");
+ static_cast<AudioNodeTrack*>(mTrack)->Engine()->SetReverb(
+ mReverb.release(), mImpulseChanelCount);
+ }
+ UniquePtr<WebCore::Reverb> mReverb;
+ uint32_t mImpulseChanelCount;
+ };
+
+ GraphImpl()->AppendMessage(
+ MakeUnique<Message>(this, aReverb, aImpulseChannelCount));
+}
+
+void AudioNodeTrack::SetRawArrayData(nsTArray<float>&& aData) {
+ class Message final : public ControlMessage {
+ public:
+ Message(AudioNodeTrack* aTrack, nsTArray<float>&& aData)
+ : ControlMessage(aTrack), mData(std::move(aData)) {}
+ void Run() override {
+ TRACE("AudioNodeTrack::SetRawArrayData");
+ static_cast<AudioNodeTrack*>(mTrack)->Engine()->SetRawArrayData(
+ std::move(mData));
+ }
+ nsTArray<float> mData;
+ };
+
+ GraphImpl()->AppendMessage(MakeUnique<Message>(this, std::move(aData)));
+}
+
+void AudioNodeTrack::SetChannelMixingParameters(
+ uint32_t aNumberOfChannels, ChannelCountMode aChannelCountMode,
+ ChannelInterpretation aChannelInterpretation) {
+ class Message final : public ControlMessage {
+ public:
+ Message(AudioNodeTrack* aTrack, uint32_t aNumberOfChannels,
+ ChannelCountMode aChannelCountMode,
+ ChannelInterpretation aChannelInterpretation)
+ : ControlMessage(aTrack),
+ mNumberOfChannels(aNumberOfChannels),
+ mChannelCountMode(aChannelCountMode),
+ mChannelInterpretation(aChannelInterpretation) {}
+ void Run() override {
+ TRACE("AudioNodeTrack::SetChannelMixingParameters");
+ static_cast<AudioNodeTrack*>(mTrack)->SetChannelMixingParametersImpl(
+ mNumberOfChannels, mChannelCountMode, mChannelInterpretation);
+ }
+ uint32_t mNumberOfChannels;
+ ChannelCountMode mChannelCountMode;
+ ChannelInterpretation mChannelInterpretation;
+ };
+
+ GraphImpl()->AppendMessage(MakeUnique<Message>(
+ this, aNumberOfChannels, aChannelCountMode, aChannelInterpretation));
+}
+
+void AudioNodeTrack::SetPassThrough(bool aPassThrough) {
+ class Message final : public ControlMessage {
+ public:
+ Message(AudioNodeTrack* aTrack, bool aPassThrough)
+ : ControlMessage(aTrack), mPassThrough(aPassThrough) {}
+ void Run() override {
+ TRACE("AudioNodeTrack::SetPassThrough");
+ static_cast<AudioNodeTrack*>(mTrack)->mPassThrough = mPassThrough;
+ }
+ bool mPassThrough;
+ };
+
+ GraphImpl()->AppendMessage(MakeUnique<Message>(this, aPassThrough));
+}
+
+void AudioNodeTrack::SendRunnable(already_AddRefed<nsIRunnable> aRunnable) {
+ class Message final : public ControlMessage {
+ public:
+ Message(MediaTrack* aTrack, already_AddRefed<nsIRunnable> aRunnable)
+ : ControlMessage(aTrack), mRunnable(aRunnable) {}
+ void Run() override {
+ TRACE("AudioNodeTrack::SendRunnable");
+ mRunnable->Run();
+ }
+
+ private:
+ nsCOMPtr<nsIRunnable> mRunnable;
+ };
+
+ GraphImpl()->AppendMessage(MakeUnique<Message>(this, std::move(aRunnable)));
+}
+
+void AudioNodeTrack::SetChannelMixingParametersImpl(
+ uint32_t aNumberOfChannels, ChannelCountMode aChannelCountMode,
+ ChannelInterpretation aChannelInterpretation) {
+ mNumberOfInputChannels = aNumberOfChannels;
+ mChannelCountMode = aChannelCountMode;
+ mChannelInterpretation = aChannelInterpretation;
+}
+
+uint32_t AudioNodeTrack::ComputedNumberOfChannels(uint32_t aInputChannelCount) {
+ switch (mChannelCountMode) {
+ case ChannelCountMode::Explicit:
+ // Disregard the channel count we've calculated from inputs, and just use
+ // mNumberOfInputChannels.
+ return mNumberOfInputChannels;
+ case ChannelCountMode::Clamped_max:
+ // Clamp the computed output channel count to mNumberOfInputChannels.
+ return std::min(aInputChannelCount, mNumberOfInputChannels);
+ default:
+ case ChannelCountMode::Max:
+ // Nothing to do here, just shut up the compiler warning.
+ return aInputChannelCount;
+ }
+}
+
+uint32_t AudioNodeTrack::NumberOfChannels() const {
+ MOZ_ASSERT(GraphImpl()->OnGraphThread());
+
+ return mNumberOfInputChannels;
+}
+
+class AudioNodeTrack::AdvanceAndResumeMessage final : public ControlMessage {
+ public:
+ AdvanceAndResumeMessage(AudioNodeTrack* aTrack, TrackTime aAdvance)
+ : ControlMessage(aTrack), mAdvance(aAdvance) {}
+ void Run() override {
+ TRACE("AudioNodeTrack::AdvanceAndResumeMessage");
+ auto ns = static_cast<AudioNodeTrack*>(mTrack);
+ ns->mStartTime -= mAdvance;
+ ns->mSegment->AppendNullData(mAdvance);
+ ns->DecrementSuspendCount();
+ }
+
+ private:
+ TrackTime mAdvance;
+};
+
+void AudioNodeTrack::AdvanceAndResume(TrackTime aAdvance) {
+ mMainThreadCurrentTime += aAdvance;
+ GraphImpl()->AppendMessage(
+ MakeUnique<AdvanceAndResumeMessage>(this, aAdvance));
+}
+
+void AudioNodeTrack::ObtainInputBlock(AudioBlock& aTmpChunk,
+ uint32_t aPortIndex) {
+ uint32_t inputCount = mInputs.Length();
+ uint32_t outputChannelCount = 1;
+ AutoTArray<const AudioBlock*, 250> inputChunks;
+ for (uint32_t i = 0; i < inputCount; ++i) {
+ if (aPortIndex != mInputs[i]->InputNumber()) {
+ // This input is connected to a different port
+ continue;
+ }
+ MediaTrack* t = mInputs[i]->GetSource();
+ AudioNodeTrack* a = static_cast<AudioNodeTrack*>(t);
+ MOZ_ASSERT(a == t->AsAudioNodeTrack());
+ if (a->IsAudioParamTrack()) {
+ continue;
+ }
+
+ const AudioBlock* chunk = &a->mLastChunks[mInputs[i]->OutputNumber()];
+ MOZ_ASSERT(chunk);
+ if (chunk->IsNull() || chunk->mChannelData.IsEmpty()) {
+ continue;
+ }
+
+ inputChunks.AppendElement(chunk);
+ outputChannelCount =
+ GetAudioChannelsSuperset(outputChannelCount, chunk->ChannelCount());
+ }
+
+ outputChannelCount = ComputedNumberOfChannels(outputChannelCount);
+
+ uint32_t inputChunkCount = inputChunks.Length();
+ if (inputChunkCount == 0 ||
+ (inputChunkCount == 1 && inputChunks[0]->ChannelCount() == 0)) {
+ aTmpChunk.SetNull(WEBAUDIO_BLOCK_SIZE);
+ return;
+ }
+
+ if (inputChunkCount == 1 &&
+ inputChunks[0]->ChannelCount() == outputChannelCount) {
+ aTmpChunk = *inputChunks[0];
+ return;
+ }
+
+ if (outputChannelCount == 0) {
+ aTmpChunk.SetNull(WEBAUDIO_BLOCK_SIZE);
+ return;
+ }
+
+ aTmpChunk.AllocateChannels(outputChannelCount);
+ DownmixBufferType downmixBuffer;
+ ASSERT_ALIGNED16(downmixBuffer.Elements());
+
+ for (uint32_t i = 0; i < inputChunkCount; ++i) {
+ AccumulateInputChunk(i, *inputChunks[i], &aTmpChunk, &downmixBuffer);
+ }
+}
+
+void AudioNodeTrack::AccumulateInputChunk(uint32_t aInputIndex,
+ const AudioBlock& aChunk,
+ AudioBlock* aBlock,
+ DownmixBufferType* aDownmixBuffer) {
+ AutoTArray<const float*, GUESS_AUDIO_CHANNELS> channels;
+ UpMixDownMixChunk(&aChunk, aBlock->ChannelCount(), channels, *aDownmixBuffer);
+
+ for (uint32_t c = 0; c < channels.Length(); ++c) {
+ const float* inputData = static_cast<const float*>(channels[c]);
+ float* outputData = aBlock->ChannelFloatsForWrite(c);
+ if (inputData) {
+ if (aInputIndex == 0) {
+ AudioBlockCopyChannelWithScale(inputData, aChunk.mVolume, outputData);
+ } else {
+ AudioBlockAddChannelWithScale(inputData, aChunk.mVolume, outputData);
+ }
+ } else {
+ if (aInputIndex == 0) {
+ PodZero(outputData, WEBAUDIO_BLOCK_SIZE);
+ }
+ }
+ }
+}
+
+void AudioNodeTrack::UpMixDownMixChunk(const AudioBlock* aChunk,
+ uint32_t aOutputChannelCount,
+ nsTArray<const float*>& aOutputChannels,
+ DownmixBufferType& aDownmixBuffer) {
+ for (uint32_t i = 0; i < aChunk->ChannelCount(); i++) {
+ aOutputChannels.AppendElement(
+ static_cast<const float*>(aChunk->mChannelData[i]));
+ }
+ if (aOutputChannels.Length() < aOutputChannelCount) {
+ if (mChannelInterpretation == ChannelInterpretation::Speakers) {
+ AudioChannelsUpMix<float>(&aOutputChannels, aOutputChannelCount, nullptr);
+ NS_ASSERTION(aOutputChannelCount == aOutputChannels.Length(),
+ "We called GetAudioChannelsSuperset to avoid this");
+ } else {
+ // Fill up the remaining aOutputChannels by zeros
+ for (uint32_t j = aOutputChannels.Length(); j < aOutputChannelCount;
+ ++j) {
+ aOutputChannels.AppendElement(nullptr);
+ }
+ }
+ } else if (aOutputChannels.Length() > aOutputChannelCount) {
+ if (mChannelInterpretation == ChannelInterpretation::Speakers) {
+ AutoTArray<float*, GUESS_AUDIO_CHANNELS> outputChannels;
+ outputChannels.SetLength(aOutputChannelCount);
+ aDownmixBuffer.SetLength(aOutputChannelCount * WEBAUDIO_BLOCK_SIZE);
+ for (uint32_t j = 0; j < aOutputChannelCount; ++j) {
+ outputChannels[j] = &aDownmixBuffer[j * WEBAUDIO_BLOCK_SIZE];
+ }
+
+ AudioChannelsDownMix(aOutputChannels, outputChannels.Elements(),
+ aOutputChannelCount, WEBAUDIO_BLOCK_SIZE);
+
+ aOutputChannels.SetLength(aOutputChannelCount);
+ for (uint32_t j = 0; j < aOutputChannels.Length(); ++j) {
+ aOutputChannels[j] = outputChannels[j];
+ }
+ } else {
+ // Drop the remaining aOutputChannels
+ aOutputChannels.RemoveLastElements(aOutputChannels.Length() -
+ aOutputChannelCount);
+ }
+ }
+}
+
+// The MediaTrackGraph guarantees that this is actually one block, for
+// AudioNodeTracks.
+void AudioNodeTrack::ProcessInput(GraphTime aFrom, GraphTime aTo,
+ uint32_t aFlags) {
+ uint16_t outputCount = mLastChunks.Length();
+ MOZ_ASSERT(outputCount == std::max(uint16_t(1), mEngine->OutputCount()));
+
+ if (!mIsActive) {
+ // mLastChunks are already null.
+#ifdef DEBUG
+ for (const auto& chunk : mLastChunks) {
+ MOZ_ASSERT(chunk.IsNull());
+ }
+#endif
+ } else if (InMutedCycle()) {
+ mInputChunks.Clear();
+ for (uint16_t i = 0; i < outputCount; ++i) {
+ mLastChunks[i].SetNull(WEBAUDIO_BLOCK_SIZE);
+ }
+ } else {
+ // We need to generate at least one input
+ uint16_t maxInputs = std::max(uint16_t(1), mEngine->InputCount());
+ mInputChunks.SetLength(maxInputs);
+ for (uint16_t i = 0; i < maxInputs; ++i) {
+ ObtainInputBlock(mInputChunks[i], i);
+ }
+ bool finished = false;
+ if (mPassThrough) {
+ MOZ_ASSERT(outputCount == 1,
+ "For now, we only support nodes that have one output port");
+ mLastChunks[0] = mInputChunks[0];
+ } else {
+ if (maxInputs <= 1 && outputCount <= 1) {
+ mEngine->ProcessBlock(this, aFrom, mInputChunks[0], &mLastChunks[0],
+ &finished);
+ } else {
+ mEngine->ProcessBlocksOnPorts(
+ this, aFrom, Span(mInputChunks.Elements(), mEngine->InputCount()),
+ Span(mLastChunks.Elements(), mEngine->OutputCount()), &finished);
+ }
+ }
+ for (uint16_t i = 0; i < outputCount; ++i) {
+ NS_ASSERTION(mLastChunks[i].GetDuration() == WEBAUDIO_BLOCK_SIZE,
+ "Invalid WebAudio chunk size");
+ }
+ if (finished) {
+ mMarkAsEndedAfterThisBlock = true;
+ if (mIsActive) {
+ ScheduleCheckForInactive();
+ }
+ }
+
+ if (mDisabledMode != DisabledTrackMode::ENABLED) {
+ for (uint32_t i = 0; i < outputCount; ++i) {
+ mLastChunks[i].SetNull(WEBAUDIO_BLOCK_SIZE);
+ }
+ }
+ }
+
+ if (!mEnded) {
+ // Don't output anything while finished
+ if (mFlags & EXTERNAL_OUTPUT) {
+ AdvanceOutputSegment();
+ }
+ if (mMarkAsEndedAfterThisBlock && (aFlags & ALLOW_END)) {
+ // This track was ended the last time that we looked at it, and all
+ // of the depending tracks have ended their output as well, so now
+ // it's time to mark this track as ended.
+ mEnded = true;
+ }
+ }
+}
+
+void AudioNodeTrack::ProduceOutputBeforeInput(GraphTime aFrom) {
+ MOZ_ASSERT(mEngine->AsDelayNodeEngine());
+ MOZ_ASSERT(mEngine->OutputCount() == 1,
+ "DelayNodeEngine output count should be 1");
+ MOZ_ASSERT(!InMutedCycle(), "DelayNodes should break cycles");
+ MOZ_ASSERT(mLastChunks.Length() == 1);
+
+ if (!mIsActive) {
+ mLastChunks[0].SetNull(WEBAUDIO_BLOCK_SIZE);
+ } else {
+ mEngine->ProduceBlockBeforeInput(this, aFrom, &mLastChunks[0]);
+ NS_ASSERTION(mLastChunks[0].GetDuration() == WEBAUDIO_BLOCK_SIZE,
+ "Invalid WebAudio chunk size");
+ if (mDisabledMode != DisabledTrackMode::ENABLED) {
+ mLastChunks[0].SetNull(WEBAUDIO_BLOCK_SIZE);
+ }
+ }
+}
+
+void AudioNodeTrack::AdvanceOutputSegment() {
+ AudioSegment* segment = GetData<AudioSegment>();
+
+ AudioChunk copyChunk = *mLastChunks[0].AsMutableChunk();
+ AudioSegment tmpSegment;
+ tmpSegment.AppendAndConsumeChunk(std::move(copyChunk));
+
+ for (const auto& l : mTrackListeners) {
+ // Notify MediaTrackListeners.
+ l->NotifyQueuedChanges(Graph(), segment->GetDuration(), tmpSegment);
+ }
+
+ if (mLastChunks[0].IsNull()) {
+ segment->AppendNullData(tmpSegment.GetDuration());
+ } else {
+ segment->AppendFrom(&tmpSegment);
+ }
+}
+
+void AudioNodeTrack::AddInput(MediaInputPort* aPort) {
+ ProcessedMediaTrack::AddInput(aPort);
+ AudioNodeTrack* ns = aPort->GetSource()->AsAudioNodeTrack();
+ // Tracks that are not AudioNodeTracks are considered active.
+ if (!ns || (ns->mIsActive && !ns->IsAudioParamTrack())) {
+ IncrementActiveInputCount();
+ }
+}
+void AudioNodeTrack::RemoveInput(MediaInputPort* aPort) {
+ ProcessedMediaTrack::RemoveInput(aPort);
+ AudioNodeTrack* ns = aPort->GetSource()->AsAudioNodeTrack();
+ // Tracks that are not AudioNodeTracks are considered active.
+ if (!ns || (ns->mIsActive && !ns->IsAudioParamTrack())) {
+ DecrementActiveInputCount();
+ }
+}
+
+void AudioNodeTrack::SetActive() {
+ if (mIsActive || mMarkAsEndedAfterThisBlock) {
+ return;
+ }
+
+ mIsActive = true;
+ if (!(mFlags & EXTERNAL_OUTPUT)) {
+ DecrementSuspendCount();
+ }
+ if (IsAudioParamTrack()) {
+ // Consumers merely influence track order.
+ // They do not read from the track.
+ return;
+ }
+
+ for (const auto& consumer : mConsumers) {
+ AudioNodeTrack* ns = consumer->GetDestination()->AsAudioNodeTrack();
+ if (ns) {
+ ns->IncrementActiveInputCount();
+ }
+ }
+}
+
+class AudioNodeTrack::CheckForInactiveMessage final : public ControlMessage {
+ public:
+ explicit CheckForInactiveMessage(AudioNodeTrack* aTrack)
+ : ControlMessage(aTrack) {}
+ void Run() override {
+ TRACE("AudioNodeTrack::CheckForInactive");
+ auto ns = static_cast<AudioNodeTrack*>(mTrack);
+ ns->CheckForInactive();
+ }
+};
+
+void AudioNodeTrack::ScheduleCheckForInactive() {
+ if (mActiveInputCount > 0 && !mMarkAsEndedAfterThisBlock) {
+ return;
+ }
+
+ auto message = MakeUnique<CheckForInactiveMessage>(this);
+ GraphImpl()->RunMessageAfterProcessing(std::move(message));
+}
+
+void AudioNodeTrack::CheckForInactive() {
+ if (((mActiveInputCount > 0 || mEngine->IsActive()) &&
+ !mMarkAsEndedAfterThisBlock) ||
+ !mIsActive) {
+ return;
+ }
+
+ mIsActive = false;
+ mInputChunks.Clear(); // not required for foreseeable future
+ for (auto& chunk : mLastChunks) {
+ chunk.SetNull(WEBAUDIO_BLOCK_SIZE);
+ }
+ if (!(mFlags & EXTERNAL_OUTPUT)) {
+ IncrementSuspendCount();
+ }
+ if (IsAudioParamTrack()) {
+ return;
+ }
+
+ for (const auto& consumer : mConsumers) {
+ AudioNodeTrack* ns = consumer->GetDestination()->AsAudioNodeTrack();
+ if (ns) {
+ ns->DecrementActiveInputCount();
+ }
+ }
+}
+
+void AudioNodeTrack::IncrementActiveInputCount() {
+ ++mActiveInputCount;
+ SetActive();
+}
+
+void AudioNodeTrack::DecrementActiveInputCount() {
+ MOZ_ASSERT(mActiveInputCount > 0);
+ --mActiveInputCount;
+ CheckForInactive();
+}
+
+} // namespace mozilla
diff --git a/dom/media/webaudio/AudioNodeTrack.h b/dom/media/webaudio/AudioNodeTrack.h
new file mode 100644
index 0000000000..29c5218d0a
--- /dev/null
+++ b/dom/media/webaudio/AudioNodeTrack.h
@@ -0,0 +1,237 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_AUDIONODETRACK_H_
+#define MOZILLA_AUDIONODETRACK_H_
+
+#include "MediaTrackGraph.h"
+#include "mozilla/dom/AudioNodeBinding.h"
+#include "AlignedTArray.h"
+#include "AudioBlock.h"
+#include "AudioSegment.h"
+
+namespace WebCore {
+class Reverb;
+} // namespace WebCore
+
+namespace mozilla {
+
+namespace dom {
+struct ThreeDPoint;
+struct AudioTimelineEvent;
+class AudioContext;
+} // namespace dom
+
+class AbstractThread;
+class ThreadSharedFloatArrayBufferList;
+class AudioNodeEngine;
+
+typedef AlignedAutoTArray<float, GUESS_AUDIO_CHANNELS * WEBAUDIO_BLOCK_SIZE, 16>
+ DownmixBufferType;
+
+/**
+ * An AudioNodeTrack produces one audio track with ID AUDIO_TRACK.
+ * The start time of the AudioTrack is aligned to the start time of the
+ * AudioContext's destination node track, plus some multiple of BLOCK_SIZE
+ * samples.
+ *
+ * An AudioNodeTrack has an AudioNodeEngine plugged into it that does the
+ * actual audio processing. AudioNodeTrack contains the glue code that
+ * integrates audio processing with the MediaTrackGraph.
+ */
+class AudioNodeTrack : public ProcessedMediaTrack {
+ typedef dom::ChannelCountMode ChannelCountMode;
+ typedef dom::ChannelInterpretation ChannelInterpretation;
+
+ public:
+ typedef mozilla::dom::AudioContext AudioContext;
+
+ enum { AUDIO_TRACK = 1 };
+
+ typedef AutoTArray<AudioBlock, 1> OutputChunks;
+
+ // Flags re main thread updates and track output.
+ typedef unsigned Flags;
+ enum : Flags {
+ NO_TRACK_FLAGS = 0U,
+ NEED_MAIN_THREAD_ENDED = 1U << 0,
+ NEED_MAIN_THREAD_CURRENT_TIME = 1U << 1,
+ // Internal AudioNodeTracks can only pass their output to another
+ // AudioNode, whereas external AudioNodeTracks can pass their output
+ // to other ProcessedMediaTracks or hardware audio output.
+ EXTERNAL_OUTPUT = 1U << 2,
+ };
+ /**
+ * Create a track that will process audio for an AudioNode.
+ * Takes ownership of aEngine.
+ * aGraph is required and equals the graph of aCtx in most cases. An exception
+ * is AudioDestinationNode where the context's graph hasn't been set up yet.
+ */
+ static already_AddRefed<AudioNodeTrack> Create(AudioContext* aCtx,
+ AudioNodeEngine* aEngine,
+ Flags aKind,
+ MediaTrackGraph* aGraph);
+
+ protected:
+ /**
+ * Transfers ownership of aEngine to the new AudioNodeTrack.
+ */
+ AudioNodeTrack(AudioNodeEngine* aEngine, Flags aFlags, TrackRate aSampleRate);
+
+ ~AudioNodeTrack();
+
+ public:
+ // Control API
+ /**
+ * Sets a parameter that's a time relative to some track's played time.
+ * This time is converted to a time relative to this track when it's set.
+ */
+ void SetTrackTimeParameter(uint32_t aIndex, AudioContext* aContext,
+ double aTrackTime);
+ void SetDoubleParameter(uint32_t aIndex, double aValue);
+ void SetInt32Parameter(uint32_t aIndex, int32_t aValue);
+ void SetThreeDPointParameter(uint32_t aIndex, const dom::ThreeDPoint& aValue);
+ void SetBuffer(AudioChunk&& aBuffer);
+ void SetReverb(WebCore::Reverb* aReverb, uint32_t aImpulseChannelCount);
+ // This sends a single event to the timeline on the MTG thread side.
+ void SendTimelineEvent(uint32_t aIndex,
+ const dom::AudioTimelineEvent& aEvent);
+ // This consumes the contents of aData. aData will be emptied after this
+ // returns.
+ void SetRawArrayData(nsTArray<float>&& aData);
+ void SetChannelMixingParameters(uint32_t aNumberOfChannels,
+ ChannelCountMode aChannelCountMoe,
+ ChannelInterpretation aChannelInterpretation);
+ void SetPassThrough(bool aPassThrough);
+ void SendRunnable(already_AddRefed<nsIRunnable> aRunnable);
+ ChannelInterpretation GetChannelInterpretation() {
+ return mChannelInterpretation;
+ }
+
+ void SetAudioParamHelperTrack() {
+ MOZ_ASSERT(!mAudioParamTrack, "Can only do this once");
+ mAudioParamTrack = true;
+ }
+ // The value for channelCount on an AudioNode, but on the audio thread side.
+ uint32_t NumberOfChannels() const override;
+
+ /*
+ * Resume track after updating its concept of current time by aAdvance.
+ * Main thread. Used only from AudioDestinationNode when resuming a track
+ * suspended to save running the MediaTrackGraph when there are no other
+ * nodes in the AudioContext.
+ */
+ void AdvanceAndResume(TrackTime aAdvance);
+
+ AudioNodeTrack* AsAudioNodeTrack() override { return this; }
+ void AddInput(MediaInputPort* aPort) override;
+ void RemoveInput(MediaInputPort* aPort) override;
+
+ // Graph thread only
+ void SetTrackTimeParameterImpl(uint32_t aIndex, MediaTrack* aRelativeToTrack,
+ double aTrackTime);
+ void SetChannelMixingParametersImpl(
+ uint32_t aNumberOfChannels, ChannelCountMode aChannelCountMoe,
+ ChannelInterpretation aChannelInterpretation);
+ void ProcessInput(GraphTime aFrom, GraphTime aTo, uint32_t aFlags) override;
+ /**
+ * Produce the next block of output, before input is provided.
+ * ProcessInput() will be called later, and it then should not change
+ * the output. This is used only for DelayNodeEngine in a feedback loop.
+ */
+ void ProduceOutputBeforeInput(GraphTime aFrom);
+ bool IsAudioParamTrack() const { return mAudioParamTrack; }
+
+ const OutputChunks& LastChunks() const { return mLastChunks; }
+ bool MainThreadNeedsUpdates() const override {
+ return ((mFlags & NEED_MAIN_THREAD_ENDED) && mEnded) ||
+ (mFlags & NEED_MAIN_THREAD_CURRENT_TIME);
+ }
+
+ // Any thread
+ AudioNodeEngine* Engine() { return mEngine.get(); }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override;
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override;
+
+ void SizeOfAudioNodesIncludingThis(MallocSizeOf aMallocSizeOf,
+ AudioNodeSizes& aUsage) const;
+
+ /*
+ * SetActive() is called when either an active input is added or the engine
+ * for a source node transitions from inactive to active. This is not
+ * called from engines for processing nodes because they only become active
+ * when there are active input tracks, in which case this track is already
+ * active.
+ */
+ void SetActive();
+ /*
+ * ScheduleCheckForInactive() is called during track processing when the
+ * engine transitions from active to inactive, or the track finishes. It
+ * schedules a call to CheckForInactive() after track processing.
+ */
+ void ScheduleCheckForInactive();
+
+ protected:
+ class AdvanceAndResumeMessage;
+ class CheckForInactiveMessage;
+
+ void OnGraphThreadDone() override;
+ void DestroyImpl() override;
+
+ /*
+ * CheckForInactive() is called when the engine transitions from active to
+ * inactive, or an active input is removed, or the track finishes. If the
+ * track is now inactive, then mInputChunks will be cleared and mLastChunks
+ * will be set to null. ProcessBlock() will not be called on the engine
+ * again until SetActive() is called.
+ */
+ void CheckForInactive();
+
+ void AdvanceOutputSegment();
+ void FinishOutput();
+ void AccumulateInputChunk(uint32_t aInputIndex, const AudioBlock& aChunk,
+ AudioBlock* aBlock,
+ DownmixBufferType* aDownmixBuffer);
+ void UpMixDownMixChunk(const AudioBlock* aChunk, uint32_t aOutputChannelCount,
+ nsTArray<const float*>& aOutputChannels,
+ DownmixBufferType& aDownmixBuffer);
+
+ uint32_t ComputedNumberOfChannels(uint32_t aInputChannelCount);
+ void ObtainInputBlock(AudioBlock& aTmpChunk, uint32_t aPortIndex);
+ void IncrementActiveInputCount();
+ void DecrementActiveInputCount();
+
+ // The engine that will generate output for this node.
+ const UniquePtr<AudioNodeEngine> mEngine;
+ // The mixed input blocks are kept from iteration to iteration to avoid
+ // reallocating channel data arrays and any buffers for mixing.
+ OutputChunks mInputChunks;
+ // The last block produced by this node.
+ OutputChunks mLastChunks;
+ // Whether this is an internal or external track
+ const Flags mFlags;
+ // The number of input tracks that may provide non-silent input.
+ uint32_t mActiveInputCount = 0;
+ // The number of input channels that this track requires. 0 means don't care.
+ uint32_t mNumberOfInputChannels;
+ // The mixing modes
+ ChannelCountMode mChannelCountMode;
+ ChannelInterpretation mChannelInterpretation;
+ // Tracks are considered active if the track has not finished and either
+ // the engine is active or there are active input tracks.
+ bool mIsActive;
+ // Whether the track should be marked as ended as soon
+ // as the current time range has been computed block by block.
+ bool mMarkAsEndedAfterThisBlock;
+ // Whether the track is an AudioParamHelper track.
+ bool mAudioParamTrack;
+ // Whether the track just passes its input through.
+ bool mPassThrough;
+};
+
+} // namespace mozilla
+
+#endif /* MOZILLA_AUDIONODETRACK_H_ */
diff --git a/dom/media/webaudio/AudioParam.cpp b/dom/media/webaudio/AudioParam.cpp
new file mode 100644
index 0000000000..30c170208f
--- /dev/null
+++ b/dom/media/webaudio/AudioParam.cpp
@@ -0,0 +1,169 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioParam.h"
+#include "mozilla/dom/AudioParamBinding.h"
+#include "AudioNodeEngine.h"
+#include "AudioNodeTrack.h"
+#include "AudioContext.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(AudioParam)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(AudioParam)
+ tmp->DisconnectFromGraphAndDestroyTrack();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mNode)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(AudioParam)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mNode)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTING_NATIVE_ADDREF(AudioParam)
+NS_IMPL_CYCLE_COLLECTING_NATIVE_RELEASE(AudioParam)
+
+AudioParam::AudioParam(AudioNode* aNode, uint32_t aIndex,
+ const nsAString& aName, float aDefaultValue,
+ float aMinValue, float aMaxValue)
+ : AudioParamTimeline(aDefaultValue),
+ mNode(aNode),
+ mName(aName),
+ mIndex(aIndex),
+ mDefaultValue(aDefaultValue),
+ mMinValue(aMinValue),
+ mMaxValue(aMaxValue) {}
+
+AudioParam::~AudioParam() { DisconnectFromGraphAndDestroyTrack(); }
+
+JSObject* AudioParam::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return AudioParam_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void AudioParam::DisconnectFromGraphAndDestroyTrack() {
+ MOZ_ASSERT(mRefCnt.get() > mInputNodes.Length(),
+ "Caller should be holding a reference or have called "
+ "mRefCnt.stabilizeForDeletion()");
+
+ while (!mInputNodes.IsEmpty()) {
+ RefPtr<AudioNode> input = mInputNodes.PopLastElement().mInputNode;
+ input->RemoveOutputParam(this);
+ }
+
+ if (mNodeTrackPort) {
+ mNodeTrackPort->Destroy();
+ mNodeTrackPort = nullptr;
+ }
+
+ if (mTrack) {
+ mTrack->Destroy();
+ mTrack = nullptr;
+ }
+}
+
+mozilla::MediaTrack* AudioParam::GetTrack() const { return mTrack; }
+
+mozilla::MediaTrack* AudioParam::Track() {
+ if (mTrack) {
+ return mTrack;
+ }
+
+ AudioNodeEngine* engine = new AudioNodeEngine(nullptr);
+ mTrack = AudioNodeTrack::Create(mNode->Context(), engine,
+ AudioNodeTrack::NO_TRACK_FLAGS,
+ mNode->Context()->Graph());
+
+ // Force the input to have only one channel, and make it down-mix using
+ // the speaker rules if needed.
+ mTrack->SetChannelMixingParametersImpl(1, ChannelCountMode::Explicit,
+ ChannelInterpretation::Speakers);
+ // Mark as an AudioParam helper track
+ mTrack->SetAudioParamHelperTrack();
+
+ // Setup the AudioParam's track as an input to the owner AudioNode's track
+ AudioNodeTrack* nodeTrack = mNode->GetTrack();
+ if (nodeTrack) {
+ mNodeTrackPort = nodeTrack->AllocateInputPort(mTrack);
+ }
+
+ // Send the track to the timeline on the MTG side.
+ AudioTimelineEvent event(mTrack);
+ SendEventToEngine(event);
+
+ return mTrack;
+}
+
+static const char* ToString(AudioTimelineEvent::Type aType) {
+ switch (aType) {
+ case AudioTimelineEvent::SetValue:
+ return "SetValue";
+ case AudioTimelineEvent::SetValueAtTime:
+ return "SetValueAtTime";
+ case AudioTimelineEvent::LinearRamp:
+ return "LinearRamp";
+ case AudioTimelineEvent::ExponentialRamp:
+ return "ExponentialRamp";
+ case AudioTimelineEvent::SetTarget:
+ return "SetTarget";
+ case AudioTimelineEvent::SetValueCurve:
+ return "SetValueCurve";
+ case AudioTimelineEvent::Track:
+ return "Track";
+ case AudioTimelineEvent::Cancel:
+ return "Cancel";
+ default:
+ return "unknown AudioTimelineEvent";
+ }
+}
+
+void AudioParam::SendEventToEngine(const AudioTimelineEvent& aEvent) {
+ WEB_AUDIO_API_LOG(
+ "%f: %s for %u %s %s=%g time=%f %s=%g", GetParentObject()->CurrentTime(),
+ NS_ConvertUTF16toUTF8(mName).get(), ParentNodeId(),
+ ToString(aEvent.mType),
+ aEvent.mType == AudioTimelineEvent::SetValueCurve ? "length" : "value",
+ aEvent.mType == AudioTimelineEvent::SetValueCurve
+ ? static_cast<double>(aEvent.mCurveLength)
+ : static_cast<double>(aEvent.mValue),
+ aEvent.Time<double>(),
+ aEvent.mType == AudioTimelineEvent::SetValueCurve ? "duration"
+ : "constant",
+ aEvent.mType == AudioTimelineEvent::SetValueCurve ? aEvent.mDuration
+ : aEvent.mTimeConstant);
+
+ AudioNodeTrack* track = mNode->GetTrack();
+ if (track) {
+ track->SendTimelineEvent(mIndex, aEvent);
+ }
+}
+
+void AudioParam::CleanupOldEvents() {
+ MOZ_ASSERT(NS_IsMainThread());
+ double currentTime = mNode->Context()->CurrentTime();
+
+ CleanupEventsOlderThan(currentTime);
+}
+
+float AudioParamTimeline::AudioNodeInputValue(size_t aCounter) const {
+ MOZ_ASSERT(mTrack);
+
+ // If we have a chunk produced by the AudioNode inputs to the AudioParam,
+ // get its value now. We use aCounter to tell us which frame of the last
+ // AudioChunk to look at.
+ float audioNodeInputValue = 0.0f;
+ const AudioBlock& lastAudioNodeChunk = mTrack->LastChunks()[0];
+ if (!lastAudioNodeChunk.IsNull()) {
+ MOZ_ASSERT(lastAudioNodeChunk.GetDuration() == WEBAUDIO_BLOCK_SIZE);
+ audioNodeInputValue =
+ static_cast<const float*>(lastAudioNodeChunk.mChannelData[0])[aCounter];
+ audioNodeInputValue *= lastAudioNodeChunk.mVolume;
+ }
+
+ return audioNodeInputValue;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/AudioParam.h b/dom/media/webaudio/AudioParam.h
new file mode 100644
index 0000000000..397b078248
--- /dev/null
+++ b/dom/media/webaudio/AudioParam.h
@@ -0,0 +1,245 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef AudioParam_h_
+#define AudioParam_h_
+
+#include "AudioParamTimeline.h"
+#include "nsWrapperCache.h"
+#include "nsCycleCollectionParticipant.h"
+#include "AudioNode.h"
+#include "mozilla/dom/TypedArray.h"
+#include "WebAudioUtils.h"
+#include "js/TypeDecls.h"
+
+namespace mozilla::dom {
+
+class AudioParam final : public nsWrapperCache, public AudioParamTimeline {
+ virtual ~AudioParam();
+
+ public:
+ AudioParam(AudioNode* aNode, uint32_t aIndex, const nsAString& aName,
+ float aDefaultValue,
+ float aMinValue = std::numeric_limits<float>::lowest(),
+ float aMaxValue = std::numeric_limits<float>::max());
+
+ NS_IMETHOD_(MozExternalRefCountType) AddRef(void);
+ NS_IMETHOD_(MozExternalRefCountType) Release(void);
+ NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(AudioParam)
+
+ AudioContext* GetParentObject() const { return mNode->Context(); }
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ float Value() {
+ return AudioParamTimeline::GetValueAtTime<double>(
+ GetParentObject()->CurrentTime());
+ }
+
+ // We override SetValueCurveAtTime to convert the Float32Array to the wrapper
+ // object.
+ AudioParam* SetValueCurveAtTime(const nsTArray<float>& aValues,
+ double aStartTime, double aDuration,
+ ErrorResult& aRv) {
+ if (!WebAudioUtils::IsTimeValid(aStartTime)) {
+ aRv.ThrowRangeError<MSG_INVALID_AUDIOPARAM_METHOD_START_TIME_ERROR>();
+ return this;
+ }
+ aStartTime = std::max(aStartTime, GetParentObject()->CurrentTime());
+ EventInsertionHelper(aRv, AudioTimelineEvent::SetValueCurve, aStartTime,
+ 0.0f, 0.0f, aDuration, aValues.Elements(),
+ aValues.Length());
+
+ return this;
+ }
+
+ // Intended for use in AudioNode creation, when the setter should not throw.
+ void SetInitialValue(float aValue) {
+ MOZ_ASSERT(HasSimpleValue(), "Existing events unexpected");
+ AudioTimelineEvent event(AudioTimelineEvent::SetValue, 0.0f, aValue);
+
+ DebugOnly<ErrorResult> rv;
+ MOZ_ASSERT(ValidateEvent(event, rv), "This event should be valid");
+
+ AudioParamTimeline::SetValue(aValue);
+
+ SendEventToEngine(event);
+ }
+
+ void SetValue(float aValue, ErrorResult& aRv) {
+ AudioTimelineEvent event(AudioTimelineEvent::SetValue, 0.0f, aValue);
+
+ if (!ValidateEvent(event, aRv)) {
+ return;
+ }
+
+ AudioParamTimeline::SetValue(aValue);
+
+ SendEventToEngine(event);
+ }
+
+ AudioParam* SetValueAtTime(float aValue, double aStartTime,
+ ErrorResult& aRv) {
+ if (!WebAudioUtils::IsTimeValid(aStartTime)) {
+ aRv.ThrowRangeError<MSG_INVALID_AUDIOPARAM_METHOD_START_TIME_ERROR>();
+ return this;
+ }
+ aStartTime = std::max(aStartTime, GetParentObject()->CurrentTime());
+ EventInsertionHelper(aRv, AudioTimelineEvent::SetValueAtTime, aStartTime,
+ aValue);
+
+ return this;
+ }
+
+ AudioParam* LinearRampToValueAtTime(float aValue, double aEndTime,
+ ErrorResult& aRv) {
+ if (!WebAudioUtils::IsTimeValid(aEndTime)) {
+ aRv.ThrowRangeError<MSG_INVALID_AUDIOPARAM_METHOD_END_TIME_ERROR>();
+ return this;
+ }
+ aEndTime = std::max(aEndTime, GetParentObject()->CurrentTime());
+ EventInsertionHelper(aRv, AudioTimelineEvent::LinearRamp, aEndTime, aValue);
+ return this;
+ }
+
+ AudioParam* ExponentialRampToValueAtTime(float aValue, double aEndTime,
+ ErrorResult& aRv) {
+ if (!WebAudioUtils::IsTimeValid(aEndTime)) {
+ aRv.ThrowRangeError<MSG_INVALID_AUDIOPARAM_METHOD_END_TIME_ERROR>();
+ return this;
+ }
+ aEndTime = std::max(aEndTime, GetParentObject()->CurrentTime());
+ EventInsertionHelper(aRv, AudioTimelineEvent::ExponentialRamp, aEndTime,
+ aValue);
+ return this;
+ }
+
+ AudioParam* SetTargetAtTime(float aTarget, double aStartTime,
+ double aTimeConstant, ErrorResult& aRv) {
+ if (!WebAudioUtils::IsTimeValid(aStartTime) ||
+ !WebAudioUtils::IsTimeValid(aTimeConstant)) {
+ aRv.ThrowRangeError<MSG_INVALID_AUDIOPARAM_METHOD_START_TIME_ERROR>();
+ return this;
+ }
+ aStartTime = std::max(aStartTime, GetParentObject()->CurrentTime());
+ EventInsertionHelper(aRv, AudioTimelineEvent::SetTarget, aStartTime,
+ aTarget, aTimeConstant);
+
+ return this;
+ }
+
+ AudioParam* CancelScheduledValues(double aStartTime, ErrorResult& aRv) {
+ if (!WebAudioUtils::IsTimeValid(aStartTime)) {
+ aRv.ThrowRangeError<MSG_INVALID_AUDIOPARAM_METHOD_START_TIME_ERROR>();
+ return this;
+ }
+
+ aStartTime = std::max(aStartTime, GetParentObject()->CurrentTime());
+
+ // Remove some events on the main thread copy.
+ AudioEventTimeline::CancelScheduledValues(aStartTime);
+
+ AudioTimelineEvent event(AudioTimelineEvent::Cancel, aStartTime, 0.0f);
+
+ SendEventToEngine(event);
+
+ return this;
+ }
+
+ uint32_t ParentNodeId() { return mNode->Id(); }
+
+ void GetName(nsAString& aName) { aName.Assign(mName); }
+
+ float DefaultValue() const { return mDefaultValue; }
+
+ float MinValue() const { return mMinValue; }
+
+ float MaxValue() const { return mMaxValue; }
+
+ bool IsTrackSuspended() const {
+ return mTrack ? mTrack->IsSuspended() : false;
+ }
+
+ const nsTArray<AudioNode::InputNode>& InputNodes() const {
+ return mInputNodes;
+ }
+
+ void RemoveInputNode(uint32_t aIndex) { mInputNodes.RemoveElementAt(aIndex); }
+
+ AudioNode::InputNode* AppendInputNode() {
+ return mInputNodes.AppendElement();
+ }
+
+ // May create the track if it doesn't exist
+ mozilla::MediaTrack* Track();
+
+ // Return nullptr if track doesn't exist.
+ mozilla::MediaTrack* GetTrack() const;
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override {
+ size_t amount = AudioParamTimeline::SizeOfExcludingThis(aMallocSizeOf);
+ // Not owned:
+ // - mNode
+
+ // Just count the array, actual nodes are counted in mNode.
+ amount += mInputNodes.ShallowSizeOfExcludingThis(aMallocSizeOf);
+
+ if (mNodeTrackPort) {
+ amount += mNodeTrackPort->SizeOfIncludingThis(aMallocSizeOf);
+ }
+
+ return amount;
+ }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ private:
+ void EventInsertionHelper(ErrorResult& aRv, AudioTimelineEvent::Type aType,
+ double aTime, float aValue,
+ double aTimeConstant = 0.0, double aDuration = 0.0,
+ const float* aCurve = nullptr,
+ uint32_t aCurveLength = 0) {
+ AudioTimelineEvent event(aType, aTime, aValue, aTimeConstant, aDuration,
+ aCurve, aCurveLength);
+
+ if (!ValidateEvent(event, aRv)) {
+ return;
+ }
+
+ AudioEventTimeline::InsertEvent<double>(event);
+
+ SendEventToEngine(event);
+
+ CleanupOldEvents();
+ }
+
+ void CleanupOldEvents();
+
+ void SendEventToEngine(const AudioTimelineEvent& aEvent);
+
+ void DisconnectFromGraphAndDestroyTrack();
+
+ nsCycleCollectingAutoRefCnt mRefCnt;
+ NS_DECL_OWNINGTHREAD
+ RefPtr<AudioNode> mNode;
+ // For every InputNode, there is a corresponding entry in mOutputParams of the
+ // InputNode's mInputNode.
+ nsTArray<AudioNode::InputNode> mInputNodes;
+ const nsString mName;
+ // The input port used to connect the AudioParam's track to its node's track
+ RefPtr<MediaInputPort> mNodeTrackPort;
+ const uint32_t mIndex;
+ const float mDefaultValue;
+ const float mMinValue;
+ const float mMaxValue;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webaudio/AudioParamDescriptorMap.h b/dom/media/webaudio/AudioParamDescriptorMap.h
new file mode 100644
index 0000000000..978e8bc90c
--- /dev/null
+++ b/dom/media/webaudio/AudioParamDescriptorMap.h
@@ -0,0 +1,22 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_AudioParamDescriptor_h
+#define mozilla_dom_AudioParamDescriptor_h
+
+#include "mozilla/dom/AudioParamDescriptorBinding.h"
+#include "nsTArray.h"
+
+namespace mozilla::dom {
+
+// Note: we call this "map" to match the spec, but we store audio param
+// descriptors in an array, because we need ordered access, and don't need the
+// map functionalities.
+typedef nsTArray<AudioParamDescriptor> AudioParamDescriptorMap;
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_AudioParamDescriptor_h
diff --git a/dom/media/webaudio/AudioParamMap.cpp b/dom/media/webaudio/AudioParamMap.cpp
new file mode 100644
index 0000000000..2d4f936361
--- /dev/null
+++ b/dom/media/webaudio/AudioParamMap.cpp
@@ -0,0 +1,21 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#include "AudioParamMap.h"
+#include "mozilla/dom/AudioParamMapBinding.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(AudioParamMap, mParent)
+
+AudioParamMap::AudioParamMap(AudioWorkletNode* aParent) : mParent(aParent) {}
+
+JSObject* AudioParamMap::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return AudioParamMap_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/AudioParamMap.h b/dom/media/webaudio/AudioParamMap.h
new file mode 100644
index 0000000000..c7bb0bdbea
--- /dev/null
+++ b/dom/media/webaudio/AudioParamMap.h
@@ -0,0 +1,34 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#ifndef AudioParamMap_h_
+#define AudioParamMap_h_
+
+#include "nsWrapperCache.h"
+#include "AudioWorkletNode.h"
+
+namespace mozilla::dom {
+
+class AudioParamMap final : public nsWrapperCache {
+ public:
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(AudioParamMap)
+ NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(AudioParamMap)
+
+ explicit AudioParamMap(AudioWorkletNode* aParent);
+
+ AudioWorkletNode* GetParentObject() const { return mParent; }
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ private:
+ ~AudioParamMap() = default;
+ RefPtr<AudioWorkletNode> mParent;
+};
+
+} // namespace mozilla::dom
+
+#endif // AudioParamMap_h_
diff --git a/dom/media/webaudio/AudioParamTimeline.h b/dom/media/webaudio/AudioParamTimeline.h
new file mode 100644
index 0000000000..5c1c725c10
--- /dev/null
+++ b/dom/media/webaudio/AudioParamTimeline.h
@@ -0,0 +1,140 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef AudioParamTimeline_h_
+#define AudioParamTimeline_h_
+
+#include "AudioEventTimeline.h"
+#include "AudioNodeTrack.h"
+#include "AudioSegment.h"
+
+namespace mozilla::dom {
+
+// This helper class is used to represent the part of the AudioParam
+// class that gets sent to AudioNodeEngine instances. In addition to
+// AudioEventTimeline methods, it holds a pointer to an optional
+// AudioNodeTrack which represents the AudioNode inputs to the AudioParam.
+// This AudioNodeTrack is managed by the AudioParam subclass on the main
+// thread, and can only be obtained from the AudioNodeEngine instances
+// consuming this class.
+class AudioParamTimeline : public AudioEventTimeline {
+ typedef AudioEventTimeline BaseClass;
+
+ public:
+ explicit AudioParamTimeline(float aDefaultValue) : BaseClass(aDefaultValue) {}
+
+ AudioNodeTrack* Track() const { return mTrack; }
+
+ bool HasSimpleValue() const {
+ return BaseClass::HasSimpleValue() &&
+ (!mTrack || mTrack->LastChunks()[0].IsNull());
+ }
+
+ template <class TimeType>
+ float GetValueAtTime(TimeType aTime) {
+ return GetValueAtTime(aTime, 0);
+ }
+
+ template <typename TimeType>
+ void InsertEvent(const AudioTimelineEvent& aEvent) {
+ if (aEvent.mType == AudioTimelineEvent::Cancel) {
+ CancelScheduledValues(aEvent.Time<TimeType>());
+ return;
+ }
+ if (aEvent.mType == AudioTimelineEvent::Track) {
+ mTrack = aEvent.mTrack;
+ return;
+ }
+ if (aEvent.mType == AudioTimelineEvent::SetValue) {
+ AudioEventTimeline::SetValue(aEvent.mValue);
+ return;
+ }
+ AudioEventTimeline::InsertEvent<TimeType>(aEvent);
+ }
+
+ // Get the value of the AudioParam at time aTime + aCounter.
+ // aCounter here is an offset to aTime if we try to get the value in ticks,
+ // otherwise it should always be zero. aCounter is meant to be used when
+ template <class TimeType>
+ float GetValueAtTime(TimeType aTime, size_t aCounter);
+
+ // Get the values of the AudioParam at time aTime + (0 to aSize).
+ // aBuffer must have the correct aSize.
+ // aSize here is an offset to aTime if we try to get the value in ticks,
+ // otherwise it should always be zero. aSize is meant to be used when
+ // getting the value of an a-rate AudioParam for each tick inside an
+ // AudioNodeEngine implementation.
+ template <class TimeType>
+ void GetValuesAtTime(TimeType aTime, float* aBuffer, const size_t aSize);
+
+ virtual size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ return mTrack ? mTrack->SizeOfIncludingThis(aMallocSizeOf) : 0;
+ }
+
+ virtual size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ private:
+ float AudioNodeInputValue(size_t aCounter) const;
+
+ protected:
+ // This is created lazily when needed.
+ RefPtr<AudioNodeTrack> mTrack;
+};
+
+template <>
+inline float AudioParamTimeline::GetValueAtTime(double aTime, size_t aCounter) {
+ MOZ_ASSERT(!aCounter);
+
+ // Getting an AudioParam value on an AudioNode does not consider input from
+ // other AudioNodes, which is managed only on the graph thread.
+ return BaseClass::GetValueAtTime(aTime);
+}
+
+template <>
+inline float AudioParamTimeline::GetValueAtTime(int64_t aTime,
+ size_t aCounter) {
+ MOZ_ASSERT(aCounter < WEBAUDIO_BLOCK_SIZE);
+ MOZ_ASSERT(!aCounter || !HasSimpleValue());
+
+ // Mix the value of the AudioParam itself with that of the AudioNode inputs.
+ return BaseClass::GetValueAtTime(static_cast<int64_t>(aTime + aCounter)) +
+ (mTrack ? AudioNodeInputValue(aCounter) : 0.0f);
+}
+
+template <>
+inline void AudioParamTimeline::GetValuesAtTime(double aTime, float* aBuffer,
+ const size_t aSize) {
+ MOZ_ASSERT(aBuffer);
+ MOZ_ASSERT(aSize == 1);
+
+ // Getting an AudioParam value on an AudioNode does not consider input from
+ // other AudioNodes, which is managed only on the graph thread.
+ *aBuffer = BaseClass::GetValueAtTime(aTime);
+}
+
+template <>
+inline void AudioParamTimeline::GetValuesAtTime(int64_t aTime, float* aBuffer,
+ const size_t aSize) {
+ MOZ_ASSERT(aBuffer);
+ MOZ_ASSERT(aSize <= WEBAUDIO_BLOCK_SIZE);
+ MOZ_ASSERT(aSize == 1 || !HasSimpleValue());
+
+ // Mix the value of the AudioParam itself with that of the AudioNode inputs.
+ BaseClass::GetValuesAtTime(aTime, aBuffer, aSize);
+ if (mTrack) {
+ uint32_t blockOffset = aTime % WEBAUDIO_BLOCK_SIZE;
+ MOZ_ASSERT(blockOffset + aSize <= WEBAUDIO_BLOCK_SIZE);
+ for (size_t i = 0; i < aSize; ++i) {
+ aBuffer[i] += AudioNodeInputValue(blockOffset + i);
+ }
+ }
+}
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webaudio/AudioProcessingEvent.cpp b/dom/media/webaudio/AudioProcessingEvent.cpp
new file mode 100644
index 0000000000..7d9987db07
--- /dev/null
+++ b/dom/media/webaudio/AudioProcessingEvent.cpp
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioProcessingEvent.h"
+#include "mozilla/dom/AudioProcessingEventBinding.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "AudioContext.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(AudioProcessingEvent, Event, mInputBuffer,
+ mOutputBuffer, mNode)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AudioProcessingEvent)
+NS_INTERFACE_MAP_END_INHERITING(Event)
+
+NS_IMPL_ADDREF_INHERITED(AudioProcessingEvent, Event)
+NS_IMPL_RELEASE_INHERITED(AudioProcessingEvent, Event)
+
+AudioProcessingEvent::AudioProcessingEvent(ScriptProcessorNode* aOwner,
+ nsPresContext* aPresContext,
+ WidgetEvent* aEvent)
+ : Event(aOwner, aPresContext, aEvent), mPlaybackTime(0.0), mNode(aOwner) {}
+
+AudioProcessingEvent::~AudioProcessingEvent() = default;
+
+JSObject* AudioProcessingEvent::WrapObjectInternal(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return AudioProcessingEvent_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+already_AddRefed<AudioBuffer> AudioProcessingEvent::LazilyCreateBuffer(
+ uint32_t aNumberOfChannels, ErrorResult& aRv) {
+ RefPtr<AudioBuffer> buffer = AudioBuffer::Create(
+ mNode->Context()->GetOwner(), aNumberOfChannels, mNode->BufferSize(),
+ mNode->Context()->SampleRate(), aRv);
+ MOZ_ASSERT(buffer || aRv.ErrorCodeIs(NS_ERROR_OUT_OF_MEMORY));
+ return buffer.forget();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/AudioProcessingEvent.h b/dom/media/webaudio/AudioProcessingEvent.h
new file mode 100644
index 0000000000..3b8b9cf008
--- /dev/null
+++ b/dom/media/webaudio/AudioProcessingEvent.h
@@ -0,0 +1,71 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef AudioProcessingEvent_h_
+#define AudioProcessingEvent_h_
+
+#include "AudioBuffer.h"
+#include "ScriptProcessorNode.h"
+#include "mozilla/dom/Event.h"
+
+namespace mozilla::dom {
+
+class AudioProcessingEvent final : public Event {
+ public:
+ AudioProcessingEvent(ScriptProcessorNode* aOwner, nsPresContext* aPresContext,
+ WidgetEvent* aEvent);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(AudioProcessingEvent, Event)
+
+ JSObject* WrapObjectInternal(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ using Event::InitEvent;
+ void InitEvent(AudioBuffer* aInputBuffer, uint32_t aNumberOfInputChannels,
+ double aPlaybackTime) {
+ InitEvent(u"audioprocess"_ns, CanBubble::eNo, Cancelable::eNo);
+ mInputBuffer = aInputBuffer;
+ mNumberOfInputChannels = aNumberOfInputChannels;
+ mPlaybackTime = aPlaybackTime;
+ }
+
+ double PlaybackTime() const { return mPlaybackTime; }
+
+ AudioBuffer* GetInputBuffer(ErrorResult& aRv) {
+ if (!mInputBuffer) {
+ mInputBuffer = LazilyCreateBuffer(mNumberOfInputChannels, aRv);
+ }
+ return mInputBuffer;
+ }
+
+ AudioBuffer* GetOutputBuffer(ErrorResult& aRv) {
+ if (!mOutputBuffer) {
+ mOutputBuffer = LazilyCreateBuffer(mNode->NumberOfOutputChannels(), aRv);
+ }
+ return mOutputBuffer;
+ }
+
+ bool HasOutputBuffer() const { return !!mOutputBuffer; }
+
+ protected:
+ virtual ~AudioProcessingEvent();
+
+ private:
+ already_AddRefed<AudioBuffer> LazilyCreateBuffer(uint32_t aNumberOfChannels,
+ ErrorResult& rv);
+
+ private:
+ double mPlaybackTime;
+ RefPtr<AudioBuffer> mInputBuffer;
+ RefPtr<AudioBuffer> mOutputBuffer;
+ RefPtr<ScriptProcessorNode> mNode;
+ uint32_t mNumberOfInputChannels;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webaudio/AudioScheduledSourceNode.cpp b/dom/media/webaudio/AudioScheduledSourceNode.cpp
new file mode 100644
index 0000000000..c6609def51
--- /dev/null
+++ b/dom/media/webaudio/AudioScheduledSourceNode.cpp
@@ -0,0 +1,17 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioScheduledSourceNode.h"
+
+namespace mozilla::dom {
+
+AudioScheduledSourceNode::AudioScheduledSourceNode(
+ AudioContext* aContext, uint32_t aChannelCount,
+ ChannelCountMode aChannelCountMode,
+ ChannelInterpretation aChannelInterpretation)
+ : AudioNode(aContext, aChannelCount, aChannelCountMode,
+ aChannelInterpretation) {}
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/AudioScheduledSourceNode.h b/dom/media/webaudio/AudioScheduledSourceNode.h
new file mode 100644
index 0000000000..0102d4a6e8
--- /dev/null
+++ b/dom/media/webaudio/AudioScheduledSourceNode.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef AudioScheduledSourceNode_h_
+#define AudioScheduledSourceNode_h_
+
+#include "AudioNode.h"
+#include "mozilla/dom/AudioScheduledSourceNodeBinding.h"
+
+namespace mozilla::dom {
+
+class AudioContext;
+
+class AudioScheduledSourceNode : public AudioNode {
+ public:
+ virtual void Start(double aWhen, ErrorResult& aRv) = 0;
+ virtual void Stop(double aWhen, ErrorResult& aRv) = 0;
+
+ IMPL_EVENT_HANDLER(ended)
+
+ protected:
+ AudioScheduledSourceNode(AudioContext* aContext, uint32_t aChannelCount,
+ ChannelCountMode aChannelCountMode,
+ ChannelInterpretation aChannelInterpretation);
+ virtual ~AudioScheduledSourceNode() = default;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webaudio/AudioWorkletGlobalScope.cpp b/dom/media/webaudio/AudioWorkletGlobalScope.cpp
new file mode 100644
index 0000000000..cb13d7d8a5
--- /dev/null
+++ b/dom/media/webaudio/AudioWorkletGlobalScope.cpp
@@ -0,0 +1,370 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioWorkletGlobalScope.h"
+
+#include "AudioNodeEngine.h"
+#include "AudioNodeTrack.h"
+#include "AudioWorkletImpl.h"
+#include "jsapi.h"
+#include "js/ForOfIterator.h"
+#include "js/PropertyAndElement.h" // JS_GetProperty
+#include "mozilla/BasePrincipal.h"
+#include "mozilla/dom/AudioWorkletGlobalScopeBinding.h"
+#include "mozilla/dom/AudioWorkletProcessor.h"
+#include "mozilla/dom/BindingCallContext.h"
+#include "mozilla/dom/MessagePort.h"
+#include "mozilla/dom/StructuredCloneHolder.h"
+#include "mozilla/dom/AudioParamDescriptorBinding.h"
+#include "nsPrintfCString.h"
+#include "nsTHashSet.h"
+#include "Tracing.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(AudioWorkletGlobalScope, WorkletGlobalScope,
+ mNameToProcessorMap);
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AudioWorkletGlobalScope)
+NS_INTERFACE_MAP_END_INHERITING(WorkletGlobalScope)
+
+NS_IMPL_ADDREF_INHERITED(AudioWorkletGlobalScope, WorkletGlobalScope)
+NS_IMPL_RELEASE_INHERITED(AudioWorkletGlobalScope, WorkletGlobalScope)
+
+AudioWorkletGlobalScope::AudioWorkletGlobalScope(AudioWorkletImpl* aImpl)
+ : WorkletGlobalScope(aImpl) {}
+
+AudioWorkletImpl* AudioWorkletGlobalScope::Impl() const {
+ return static_cast<AudioWorkletImpl*>(mImpl.get());
+}
+
+bool AudioWorkletGlobalScope::WrapGlobalObject(
+ JSContext* aCx, JS::MutableHandle<JSObject*> aReflector) {
+ // |this| is being exposed to JS and content script will soon be running.
+ // The graph needs a handle on the JSContext so it can interrupt JS.
+ Impl()->DestinationTrack()->Graph()->NotifyJSContext(aCx);
+
+ JS::RealmOptions options;
+
+ // TODO(bug 1834744)
+ options.behaviors().setShouldResistFingerprinting(
+ ShouldResistFingerprinting(RFPTarget::IsAlwaysEnabledForPrecompute));
+
+ // The SharedArrayBuffer global constructor property should not be present in
+ // a fresh global object when shared memory objects aren't allowed (because
+ // COOP/COEP support isn't enabled, or because COOP/COEP don't act to isolate
+ // this worklet to a separate process).
+ options.creationOptions().setDefineSharedArrayBufferConstructor(
+ IsSharedMemoryAllowed());
+
+ return AudioWorkletGlobalScope_Binding::Wrap(
+ aCx, this, this, options, BasePrincipal::Cast(mImpl->Principal()), true,
+ aReflector);
+}
+
+void AudioWorkletGlobalScope::RegisterProcessor(
+ JSContext* aCx, const nsAString& aName,
+ AudioWorkletProcessorConstructor& aProcessorCtor, ErrorResult& aRv) {
+ TRACE_COMMENT("AudioWorkletGlobalScope::RegisterProcessor", "%s",
+ NS_ConvertUTF16toUTF8(aName).get());
+
+ JS::Rooted<JSObject*> processorConstructor(aCx,
+ aProcessorCtor.CallableOrNull());
+
+ /**
+ * 1. If the name is the empty string, throw a NotSupportedError
+ * exception and abort these steps because the empty string is not
+ * a valid key.
+ */
+ if (aName.IsEmpty()) {
+ aRv.ThrowNotSupportedError("Argument 1 should not be an empty string.");
+ return;
+ }
+
+ /**
+ * 2. If the name exists as a key in the node name to processor
+ * definition map, throw a NotSupportedError exception and abort
+ * these steps because registering a definition with a duplicated
+ * key is not allowed.
+ */
+ if (mNameToProcessorMap.GetWeak(aName)) {
+ // Duplicate names are not allowed
+ aRv.ThrowNotSupportedError(
+ "Argument 1 is invalid: a class with the same name is already "
+ "registered.");
+ return;
+ }
+
+ // We know processorConstructor is callable, so not a WindowProxy or Location.
+ JS::Rooted<JSObject*> constructorUnwrapped(
+ aCx, js::CheckedUnwrapStatic(processorConstructor));
+ if (!constructorUnwrapped) {
+ // If the caller's compartment does not have permission to access the
+ // unwrapped constructor then throw.
+ aRv.ThrowSecurityError("Constructor cannot be called");
+ return;
+ }
+
+ /**
+ * 3. If the result of IsConstructor(argument=processorCtor) is false,
+ * throw a TypeError and abort these steps.
+ */
+ if (!JS::IsConstructor(constructorUnwrapped)) {
+ aRv.ThrowTypeError<MSG_NOT_CONSTRUCTOR>("Argument 2");
+ return;
+ }
+
+ /**
+ * 4. Let prototype be the result of Get(O=processorCtor, P="prototype").
+ */
+ // The .prototype on the constructor passed could be an "expando" of a
+ // wrapper. So we should get it from wrapper instead of the underlying
+ // object.
+ JS::Rooted<JS::Value> prototype(aCx);
+ if (!JS_GetProperty(aCx, processorConstructor, "prototype", &prototype)) {
+ aRv.NoteJSContextException(aCx);
+ return;
+ }
+
+ /**
+ * 5. If the result of Type(argument=prototype) is not Object, throw a
+ * TypeError and abort all these steps.
+ */
+ if (!prototype.isObject()) {
+ aRv.ThrowTypeError<MSG_NOT_OBJECT>("processorCtor.prototype");
+ return;
+ }
+ /**
+ * 6. Let parameterDescriptorsValue be the result of Get(O=processorCtor,
+ * P="parameterDescriptors").
+ */
+ JS::Rooted<JS::Value> descriptors(aCx);
+ if (!JS_GetProperty(aCx, processorConstructor, "parameterDescriptors",
+ &descriptors)) {
+ aRv.NoteJSContextException(aCx);
+ return;
+ }
+
+ AudioParamDescriptorMap map;
+ /*
+ * 7. If parameterDescriptorsValue is not undefined
+ */
+ if (!descriptors.isUndefined()) {
+ /*
+ * 7.1. Let parameterDescriptorSequence be the result of the conversion
+ * from parameterDescriptorsValue to an IDL value of type
+ * sequence<AudioParamDescriptor>.
+ */
+ JS::Rooted<JS::Value> objectValue(aCx, descriptors);
+ JS::ForOfIterator iter(aCx);
+ if (!iter.init(objectValue, JS::ForOfIterator::AllowNonIterable)) {
+ aRv.NoteJSContextException(aCx);
+ return;
+ }
+ if (!iter.valueIsIterable()) {
+ aRv.ThrowTypeError<MSG_CONVERSION_ERROR>(
+ "AudioWorkletProcessor.parameterDescriptors", "sequence");
+ return;
+ }
+ /*
+ * 7.2 and 7.3 (and substeps)
+ */
+ map = DescriptorsFromJS(aCx, &iter, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+ }
+
+ /**
+ * 8. Append the key-value pair name → processorCtor to node name to processor
+ * constructor map of the associated AudioWorkletGlobalScope.
+ */
+ if (!mNameToProcessorMap.InsertOrUpdate(aName, RefPtr{&aProcessorCtor},
+ fallible)) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ /**
+ * 9. Queue a task to the control thread to add the key-value pair
+ * (name - descriptors) to the node name to parameter descriptor
+ * map of the associated BaseAudioContext.
+ */
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "AudioWorkletGlobalScope: parameter descriptors",
+ [impl = RefPtr{Impl()}, name = nsString(aName),
+ map = std::move(map)]() mutable {
+ AudioNode* destinationNode =
+ impl->DestinationTrack()->Engine()->NodeMainThread();
+ if (!destinationNode) {
+ return;
+ }
+ destinationNode->Context()->SetParamMapForWorkletName(name, &map);
+ }));
+}
+
+uint64_t AudioWorkletGlobalScope::CurrentFrame() const {
+ AudioNodeTrack* destinationTrack = Impl()->DestinationTrack();
+ GraphTime processedTime = destinationTrack->Graph()->ProcessedTime();
+ return destinationTrack->GraphTimeToTrackTime(processedTime);
+}
+
+double AudioWorkletGlobalScope::CurrentTime() const {
+ return static_cast<double>(CurrentFrame()) / SampleRate();
+}
+
+float AudioWorkletGlobalScope::SampleRate() const {
+ return static_cast<float>(Impl()->DestinationTrack()->mSampleRate);
+}
+
+AudioParamDescriptorMap AudioWorkletGlobalScope::DescriptorsFromJS(
+ JSContext* aCx, JS::ForOfIterator* aIter, ErrorResult& aRv) {
+ AudioParamDescriptorMap res;
+ // To check for duplicates
+ nsTHashSet<nsString> namesSet;
+
+ JS::Rooted<JS::Value> nextValue(aCx);
+ bool done = false;
+ size_t i = 0;
+ while (true) {
+ if (!aIter->next(&nextValue, &done)) {
+ aRv.NoteJSContextException(aCx);
+ return AudioParamDescriptorMap();
+ }
+ if (done) {
+ break;
+ }
+
+ BindingCallContext callCx(aCx, "AudioWorkletGlobalScope.registerProcessor");
+ nsPrintfCString sourceDescription("Element %zu in parameterDescriptors", i);
+ i++;
+ AudioParamDescriptor* descriptor = res.AppendElement(fallible);
+ if (!descriptor) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return AudioParamDescriptorMap();
+ }
+ if (!descriptor->Init(callCx, nextValue, sourceDescription.get())) {
+ aRv.NoteJSContextException(aCx);
+ return AudioParamDescriptorMap();
+ }
+ }
+
+ for (const auto& descriptor : res) {
+ if (namesSet.Contains(descriptor.mName)) {
+ aRv.ThrowNotSupportedError("Duplicated name \""_ns +
+ NS_ConvertUTF16toUTF8(descriptor.mName) +
+ "\" in parameterDescriptors."_ns);
+ return AudioParamDescriptorMap();
+ }
+
+ if (!namesSet.Insert(descriptor.mName, fallible)) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return AudioParamDescriptorMap();
+ }
+
+ if (descriptor.mMinValue > descriptor.mMaxValue) {
+ aRv.ThrowInvalidStateError(
+ "In parameterDescriptors, "_ns +
+ NS_ConvertUTF16toUTF8(descriptor.mName) +
+ " minValue should be smaller than maxValue."_ns);
+ return AudioParamDescriptorMap();
+ }
+
+ if (descriptor.mDefaultValue < descriptor.mMinValue ||
+ descriptor.mDefaultValue > descriptor.mMaxValue) {
+ aRv.ThrowInvalidStateError(
+ "In parameterDescriptors, "_ns +
+ NS_ConvertUTF16toUTF8(descriptor.mName) +
+ nsLiteralCString(" defaultValue is out of the range defined by "
+ "minValue and maxValue."));
+ return AudioParamDescriptorMap();
+ }
+ }
+
+ return res;
+}
+
+bool AudioWorkletGlobalScope::ConstructProcessor(
+ JSContext* aCx, const nsAString& aName,
+ NotNull<StructuredCloneHolder*> aSerializedOptions,
+ UniqueMessagePortId& aPortIdentifier,
+ JS::MutableHandle<JSObject*> aRetProcessor) {
+ TRACE_COMMENT("AudioWorkletProcessor::ConstructProcessor", "%s",
+ NS_ConvertUTF16toUTF8(aName).get());
+ /**
+ * See
+ * https://webaudio.github.io/web-audio-api/#AudioWorkletProcessor-instantiation
+ */
+ ErrorResult rv;
+ /**
+ * 4. Let deserializedPort be the result of
+ * StructuredDeserialize(serializedPort, the current Realm).
+ */
+ RefPtr<MessagePort> deserializedPort =
+ MessagePort::Create(this, aPortIdentifier, rv);
+ if (NS_WARN_IF(rv.MaybeSetPendingException(aCx))) {
+ return false;
+ }
+ /**
+ * 5. Let deserializedOptions be the result of
+ * StructuredDeserialize(serializedOptions, the current Realm).
+ */
+ JS::CloneDataPolicy cloneDataPolicy;
+ cloneDataPolicy.allowIntraClusterClonableSharedObjects();
+ cloneDataPolicy.allowSharedMemoryObjects();
+
+ JS::Rooted<JS::Value> deserializedOptions(aCx);
+ aSerializedOptions->Read(this, aCx, &deserializedOptions, cloneDataPolicy,
+ rv);
+ if (rv.MaybeSetPendingException(aCx)) {
+ return false;
+ }
+ /**
+ * 6. Let processorCtor be the result of looking up processorName on the
+ * AudioWorkletGlobalScope's node name to processor definition map.
+ */
+ RefPtr<AudioWorkletProcessorConstructor> processorCtor =
+ mNameToProcessorMap.Get(aName);
+ // AudioWorkletNode has already checked the definition exists.
+ // See also https://github.com/WebAudio/web-audio-api/issues/1854
+ MOZ_ASSERT(processorCtor);
+ /**
+ * 7. Store nodeReference and deserializedPort to node reference and
+ * transferred port of this AudioWorkletGlobalScope's pending processor
+ * construction data respectively.
+ */
+ // |nodeReference| is not required here because the "processorerror" event
+ // is thrown by WorkletNodeEngine::ConstructProcessor().
+ mPortForProcessor = std::move(deserializedPort);
+ /**
+ * 8. Construct a callback function from processorCtor with the argument
+ * of deserializedOptions.
+ */
+ // The options were an object before serialization and so will be an object
+ // if deserialization succeeded above. toObject() asserts.
+ JS::Rooted<JSObject*> options(aCx, &deserializedOptions.toObject());
+ RefPtr<AudioWorkletProcessor> processor = processorCtor->Construct(
+ options, rv, "AudioWorkletProcessor construction",
+ CallbackFunction::eRethrowExceptions);
+ // https://github.com/WebAudio/web-audio-api/issues/2096
+ mPortForProcessor = nullptr;
+ if (rv.MaybeSetPendingException(aCx)) {
+ return false;
+ }
+ JS::Rooted<JS::Value> processorVal(aCx);
+ if (NS_WARN_IF(!ToJSValue(aCx, processor, &processorVal))) {
+ return false;
+ }
+ MOZ_ASSERT(processorVal.isObject());
+ aRetProcessor.set(&processorVal.toObject());
+ return true;
+}
+
+RefPtr<MessagePort> AudioWorkletGlobalScope::TakePortForProcessorCtor() {
+ return std::move(mPortForProcessor);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/AudioWorkletGlobalScope.h b/dom/media/webaudio/AudioWorkletGlobalScope.h
new file mode 100644
index 0000000000..faaa731fa6
--- /dev/null
+++ b/dom/media/webaudio/AudioWorkletGlobalScope.h
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_AudioWorkletGlobalScope_h
+#define mozilla_dom_AudioWorkletGlobalScope_h
+
+#include "mozilla/dom/AudioParamDescriptorMap.h"
+#include "mozilla/dom/FunctionBinding.h"
+#include "mozilla/dom/WorkletGlobalScope.h"
+#include "js/ForOfIterator.h"
+#include "nsRefPtrHashtable.h"
+
+namespace mozilla {
+
+class AudioWorkletImpl;
+
+namespace dom {
+
+class AudioWorkletProcessorConstructor;
+class MessagePort;
+class StructuredCloneHolder;
+class UniqueMessagePortId;
+
+class AudioWorkletGlobalScope final : public WorkletGlobalScope {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(AudioWorkletGlobalScope,
+ WorkletGlobalScope);
+
+ explicit AudioWorkletGlobalScope(AudioWorkletImpl* aImpl);
+
+ bool WrapGlobalObject(JSContext* aCx,
+ JS::MutableHandle<JSObject*> aReflector) override;
+
+ void RegisterProcessor(JSContext* aCx, const nsAString& aName,
+ AudioWorkletProcessorConstructor& aProcessorCtor,
+ ErrorResult& aRv);
+
+ AudioWorkletImpl* Impl() const;
+
+ uint64_t CurrentFrame() const;
+
+ double CurrentTime() const;
+
+ float SampleRate() const;
+
+ // If successful, returns true and sets aRetProcessor, which will be in the
+ // compartment for the realm of this global. Returns false on failure.
+ MOZ_CAN_RUN_SCRIPT
+ bool ConstructProcessor(JSContext* aCx, const nsAString& aName,
+ NotNull<StructuredCloneHolder*> aSerializedOptions,
+ UniqueMessagePortId& aPortIdentifier,
+ JS::MutableHandle<JSObject*> aRetProcessor);
+
+ // Returns null if not called during ConstructProcessor() or if the port has
+ // already been taken.
+ RefPtr<MessagePort> TakePortForProcessorCtor();
+
+ private:
+ ~AudioWorkletGlobalScope() = default;
+
+ // Returns an AudioParamDescriptorMap filled with AudioParamDescriptor
+ // objects, extracted from JS. Returns an empty map in case of error and set
+ // aRv accordingly.
+ AudioParamDescriptorMap DescriptorsFromJS(JSContext* aCx,
+ JS::ForOfIterator* aIter,
+ ErrorResult& aRv);
+
+ typedef nsRefPtrHashtable<nsStringHashKey, AudioWorkletProcessorConstructor>
+ NodeNameToProcessorDefinitionMap;
+ NodeNameToProcessorDefinitionMap mNameToProcessorMap;
+ // https://webaudio.github.io/web-audio-api/#pending-processor-construction-data-transferred-port
+ // This does not need to be traversed during cycle-collection because it is
+ // only set while this AudioWorkletGlobalScope is on the stack.
+ RefPtr<MessagePort> mPortForProcessor;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_AudioWorkletGlobalScope_h
diff --git a/dom/media/webaudio/AudioWorkletImpl.cpp b/dom/media/webaudio/AudioWorkletImpl.cpp
new file mode 100644
index 0000000000..5eaa128c8a
--- /dev/null
+++ b/dom/media/webaudio/AudioWorkletImpl.cpp
@@ -0,0 +1,88 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#include "AudioWorkletImpl.h"
+
+#include "AudioContext.h"
+#include "AudioNodeTrack.h"
+#include "GeckoProfiler.h"
+#include "mozilla/dom/AudioWorkletBinding.h"
+#include "mozilla/dom/AudioWorkletGlobalScope.h"
+#include "mozilla/dom/Worklet.h"
+#include "mozilla/dom/WorkletThread.h"
+#include "nsGlobalWindowInner.h"
+
+namespace mozilla {
+
+/* static */ already_AddRefed<dom::Worklet> AudioWorkletImpl::CreateWorklet(
+ dom::AudioContext* aContext, ErrorResult& aRv) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsPIDOMWindowInner> window = aContext->GetOwner();
+ if (NS_WARN_IF(!window)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+ nsCOMPtr<nsIPrincipal> principal =
+ nsGlobalWindowInner::Cast(window)->GetPrincipal();
+ if (NS_WARN_IF(!principal)) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ RefPtr<AudioWorkletImpl> impl =
+ new AudioWorkletImpl(window, principal, aContext->DestinationTrack());
+
+ // The Worklet owns a reference to the AudioContext so as to keep the graph
+ // thread running as long as the Worklet is alive by keeping the
+ // AudioDestinationNode alive.
+ return MakeAndAddRef<dom::Worklet>(window, std::move(impl),
+ ToSupports(aContext));
+}
+
+AudioWorkletImpl::AudioWorkletImpl(nsPIDOMWindowInner* aWindow,
+ nsIPrincipal* aPrincipal,
+ AudioNodeTrack* aDestinationTrack)
+ : WorkletImpl(aWindow, aPrincipal), mDestinationTrack(aDestinationTrack) {}
+
+AudioWorkletImpl::~AudioWorkletImpl() = default;
+
+JSObject* AudioWorkletImpl::WrapWorklet(JSContext* aCx, dom::Worklet* aWorklet,
+ JS::Handle<JSObject*> aGivenProto) {
+ MOZ_ASSERT(NS_IsMainThread());
+ return dom::AudioWorklet_Binding::Wrap(aCx, aWorklet, aGivenProto);
+}
+
+nsresult AudioWorkletImpl::SendControlMessage(
+ already_AddRefed<nsIRunnable> aRunnable) {
+ mDestinationTrack->SendRunnable(std::move(aRunnable));
+ return NS_OK;
+}
+
+void AudioWorkletImpl::OnAddModuleStarted() const {
+#ifdef MOZ_GECKO_PROFILER
+ profiler_add_marker(ProfilerStringView("AudioWorklet.addModule"),
+ geckoprofiler::category::MEDIA_RT,
+ {MarkerTiming::IntervalStart()});
+#endif
+}
+
+void AudioWorkletImpl::OnAddModulePromiseSettled() const {
+#ifdef MOZ_GECKO_PROFILER
+ profiler_add_marker(ProfilerStringView("AudioWorklet.addModule"),
+ geckoprofiler::category::MEDIA_RT,
+ {MarkerTiming::IntervalEnd()});
+#endif
+}
+
+already_AddRefed<dom::WorkletGlobalScope>
+AudioWorkletImpl::ConstructGlobalScope() {
+ dom::WorkletThread::AssertIsOnWorkletThread();
+
+ return MakeAndAddRef<dom::AudioWorkletGlobalScope>(this);
+}
+
+} // namespace mozilla
diff --git a/dom/media/webaudio/AudioWorkletImpl.h b/dom/media/webaudio/AudioWorkletImpl.h
new file mode 100644
index 0000000000..39f347fdb2
--- /dev/null
+++ b/dom/media/webaudio/AudioWorkletImpl.h
@@ -0,0 +1,63 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#ifndef AudioWorkletImpl_h
+#define AudioWorkletImpl_h
+
+#include "mozilla/dom/WorkletImpl.h"
+#include "mozilla/dom/AudioWorkletGlobalScope.h"
+
+namespace mozilla {
+
+class AudioNodeTrack;
+
+namespace dom {
+class AudioContext;
+}
+
+class AudioWorkletImpl final : public WorkletImpl {
+ public:
+ // Methods for parent thread only:
+
+ static already_AddRefed<dom::Worklet> CreateWorklet(
+ dom::AudioContext* aContext, ErrorResult& aRv);
+
+ JSObject* WrapWorklet(JSContext* aCx, dom::Worklet* aWorklet,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ nsresult SendControlMessage(already_AddRefed<nsIRunnable> aRunnable) override;
+
+ nsContentPolicyType ContentPolicyType() const override {
+ return nsIContentPolicy::TYPE_INTERNAL_AUDIOWORKLET;
+ }
+
+ // Execution thread only.
+ dom::AudioWorkletGlobalScope* GetGlobalScope() {
+ return static_cast<dom::AudioWorkletGlobalScope*>(
+ WorkletImpl::GetGlobalScope());
+ }
+
+ // Any thread:
+ AudioNodeTrack* DestinationTrack() const { return mDestinationTrack; }
+
+ void OnAddModuleStarted() const override;
+ void OnAddModulePromiseSettled() const override;
+
+ protected:
+ // Execution thread only.
+ already_AddRefed<dom::WorkletGlobalScope> ConstructGlobalScope() override;
+
+ private:
+ AudioWorkletImpl(nsPIDOMWindowInner* aWindow, nsIPrincipal* aPrincipal,
+ AudioNodeTrack* aDestinationTrack);
+ ~AudioWorkletImpl();
+
+ const RefPtr<AudioNodeTrack> mDestinationTrack;
+};
+
+} // namespace mozilla
+
+#endif // AudioWorkletImpl_h
diff --git a/dom/media/webaudio/AudioWorkletNode.cpp b/dom/media/webaudio/AudioWorkletNode.cpp
new file mode 100644
index 0000000000..2b8afade3c
--- /dev/null
+++ b/dom/media/webaudio/AudioWorkletNode.cpp
@@ -0,0 +1,896 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#include "AudioWorkletNode.h"
+
+#include "AudioNodeEngine.h"
+#include "AudioParamMap.h"
+#include "AudioWorkletImpl.h"
+#include "js/Array.h" // JS::{Get,Set}ArrayLength, JS::NewArrayLength
+#include "js/CallAndConstruct.h" // JS::Call, JS::IsCallable
+#include "js/Exception.h"
+#include "js/experimental/TypedData.h" // JS_NewFloat32Array, JS_GetFloat32ArrayData, JS_GetTypedArrayLength, JS_GetArrayBufferViewBuffer
+#include "js/PropertyAndElement.h" // JS_DefineElement, JS_DefineUCProperty, JS_GetProperty
+#include "mozilla/dom/AudioWorkletNodeBinding.h"
+#include "mozilla/dom/AudioParamMapBinding.h"
+#include "mozilla/dom/AutoEntryScript.h"
+#include "mozilla/dom/RootedDictionary.h"
+#include "mozilla/dom/ErrorEvent.h"
+#include "mozilla/dom/Worklet.h"
+#include "nsIScriptGlobalObject.h"
+#include "AudioParam.h"
+#include "AudioDestinationNode.h"
+#include "mozilla/dom/MessageChannel.h"
+#include "mozilla/dom/MessagePort.h"
+#include "mozilla/ScopeExit.h"
+#include "nsReadableUtils.h"
+#include "mozilla/Span.h"
+#include "PlayingRefChangeHandler.h"
+#include "nsPrintfCString.h"
+#include "Tracing.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED_0(AudioWorkletNode, AudioNode)
+NS_IMPL_CYCLE_COLLECTION_INHERITED(AudioWorkletNode, AudioNode, mPort,
+ mParameters)
+
+struct NamedAudioParamTimeline {
+ explicit NamedAudioParamTimeline(const AudioParamDescriptor& aParamDescriptor)
+ : mName(aParamDescriptor.mName),
+ mTimeline(aParamDescriptor.mDefaultValue) {}
+ const nsString mName;
+ AudioParamTimeline mTimeline;
+};
+
+struct ProcessorErrorDetails {
+ ProcessorErrorDetails() : mLineno(0), mColno(0) {}
+ unsigned mLineno;
+ unsigned mColno;
+ nsString mFilename;
+ nsString mMessage;
+};
+
+class WorkletNodeEngine final : public AudioNodeEngine {
+ public:
+ WorkletNodeEngine(AudioWorkletNode* aNode,
+ AudioDestinationNode* aDestinationNode,
+ nsTArray<NamedAudioParamTimeline>&& aParamTimelines,
+ const Optional<Sequence<uint32_t>>& aOutputChannelCount)
+ : AudioNodeEngine(aNode),
+ mDestination(aDestinationNode->Track()),
+ mParamTimelines(std::move(aParamTimelines)) {
+ if (aOutputChannelCount.WasPassed()) {
+ mOutputChannelCount = aOutputChannelCount.Value();
+ }
+ }
+
+ MOZ_CAN_RUN_SCRIPT
+ void ConstructProcessor(AudioWorkletImpl* aWorkletImpl,
+ const nsAString& aName,
+ NotNull<StructuredCloneHolder*> aSerializedOptions,
+ UniqueMessagePortId& aPortIdentifier,
+ AudioNodeTrack* aTrack);
+
+ void RecvTimelineEvent(uint32_t aIndex, AudioTimelineEvent& aEvent) override {
+ MOZ_ASSERT(mDestination);
+ WebAudioUtils::ConvertAudioTimelineEventToTicks(aEvent, mDestination);
+
+ if (aIndex < mParamTimelines.Length()) {
+ mParamTimelines[aIndex].mTimeline.InsertEvent<int64_t>(aEvent);
+ } else {
+ NS_ERROR("Bad WorkletNodeEngine timeline event index");
+ }
+ }
+
+ void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom,
+ const AudioBlock& aInput, AudioBlock* aOutput,
+ bool* aFinished) override {
+ MOZ_ASSERT(InputCount() <= 1);
+ MOZ_ASSERT(OutputCount() <= 1);
+ TRACE("WorkletNodeEngine::ProcessBlock");
+ ProcessBlocksOnPorts(aTrack, aFrom, Span(&aInput, InputCount()),
+ Span(aOutput, OutputCount()), aFinished);
+ }
+
+ void ProcessBlocksOnPorts(AudioNodeTrack* aTrack, GraphTime aFrom,
+ Span<const AudioBlock> aInput,
+ Span<AudioBlock> aOutput, bool* aFinished) override;
+
+ void OnGraphThreadDone() override { ReleaseJSResources(); }
+
+ bool IsActive() const override { return mKeepEngineActive; }
+
+ // Vector<T> supports non-memmovable types such as PersistentRooted
+ // (without any need to jump through hoops like
+ // MOZ_DECLARE_RELOCATE_USING_MOVE_CONSTRUCTOR_FOR_TEMPLATE for nsTArray).
+ // PersistentRooted is used because these AudioWorkletGlobalScope scope
+ // objects may be kept alive as long as the AudioWorkletNode in the
+ // main-thread global.
+ struct Channels {
+ Vector<JS::PersistentRooted<JSObject*>, GUESS_AUDIO_CHANNELS>
+ mFloat32Arrays;
+ JS::PersistentRooted<JSObject*> mJSArray;
+ // For SetArrayElements():
+ operator JS::Handle<JSObject*>() const { return mJSArray; }
+ };
+ struct Ports {
+ Vector<Channels, 1> mPorts;
+ JS::PersistentRooted<JSObject*> mJSArray;
+ };
+ struct ParameterValues {
+ Vector<JS::PersistentRooted<JSObject*>> mFloat32Arrays;
+ JS::PersistentRooted<JSObject*> mJSObject;
+ };
+
+ private:
+ size_t ParameterCount() { return mParamTimelines.Length(); }
+ void SendProcessorError(AudioNodeTrack* aTrack, JSContext* aCx);
+ bool CallProcess(AudioNodeTrack* aTrack, JSContext* aCx,
+ JS::Handle<JS::Value> aCallable);
+ void ProduceSilence(AudioNodeTrack* aTrack, Span<AudioBlock> aOutput);
+ void SendErrorToMainThread(AudioNodeTrack* aTrack,
+ const ProcessorErrorDetails& aDetails);
+
+ void ReleaseJSResources() {
+ mInputs.mPorts.clearAndFree();
+ mOutputs.mPorts.clearAndFree();
+ mParameters.mFloat32Arrays.clearAndFree();
+ mInputs.mJSArray.reset();
+ mOutputs.mJSArray.reset();
+ mParameters.mJSObject.reset();
+ mGlobal = nullptr;
+ // This is equivalent to setting [[callable process]] to false.
+ mProcessor.reset();
+ }
+
+ nsCString mProcessorName;
+ RefPtr<AudioNodeTrack> mDestination;
+ nsTArray<uint32_t> mOutputChannelCount;
+ nsTArray<NamedAudioParamTimeline> mParamTimelines;
+ // The AudioWorkletGlobalScope-associated objects referenced from
+ // WorkletNodeEngine are typically kept alive as long as the
+ // AudioWorkletNode in the main-thread global. The objects must be released
+ // on the rendering thread, which usually happens simply because
+ // AudioWorkletNode is such that the last AudioNodeTrack reference is
+ // released by the MTG. That occurs on the rendering thread except during
+ // process shutdown, in which case NotifyForcedShutdown() is called on the
+ // rendering thread.
+ //
+ // mInputs, mOutputs and mParameters keep references to all objects passed to
+ // process(), for reuse of the same objects. The JS objects are all in the
+ // compartment of the realm of mGlobal. Properties on the objects may be
+ // replaced by script, so don't assume that getting indexed properties on the
+ // JS arrays will return the same objects. Only objects and buffers created
+ // by the implementation are modified or read by the implementation.
+ Ports mInputs;
+ Ports mOutputs;
+ ParameterValues mParameters;
+
+ RefPtr<AudioWorkletGlobalScope> mGlobal;
+ JS::PersistentRooted<JSObject*> mProcessor;
+
+ // mProcessorIsActive is named [[active source]] in the spec.
+ // It is initially true and so at least the first process()
+ // call will not be skipped when there are no active inputs.
+ bool mProcessorIsActive = true;
+ // mKeepEngineActive ensures another call to ProcessBlocksOnPorts(), even if
+ // there are no active inputs. Its transitions to false lag those of
+ // mProcessorIsActive by one call to ProcessBlocksOnPorts() so that
+ // downstream engines can addref their nodes before this engine's node is
+ // released.
+ bool mKeepEngineActive = true;
+};
+
+void WorkletNodeEngine::SendErrorToMainThread(
+ AudioNodeTrack* aTrack, const ProcessorErrorDetails& aDetails) {
+ RefPtr<AudioNodeTrack> track = aTrack;
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "WorkletNodeEngine::SendProcessorError",
+ [track = std::move(track), aDetails]() mutable {
+ AudioWorkletNode* node =
+ static_cast<AudioWorkletNode*>(track->Engine()->NodeMainThread());
+ if (!node) {
+ return;
+ }
+ node->DispatchProcessorErrorEvent(aDetails);
+ }));
+}
+
+void WorkletNodeEngine::SendProcessorError(AudioNodeTrack* aTrack,
+ JSContext* aCx) {
+ // Note that once an exception is thrown, the processor will output silence
+ // throughout its lifetime.
+ ReleaseJSResources();
+ // The processor errored out while getting a context, try to tell the node
+ // anyways.
+ if (!aCx || !JS_IsExceptionPending(aCx)) {
+ ProcessorErrorDetails details;
+ details.mMessage.Assign(u"Unknown processor error");
+ SendErrorToMainThread(aTrack, details);
+ return;
+ }
+
+ JS::ExceptionStack exnStack(aCx);
+ if (JS::StealPendingExceptionStack(aCx, &exnStack)) {
+ JS::ErrorReportBuilder jsReport(aCx);
+ if (!jsReport.init(aCx, exnStack,
+ JS::ErrorReportBuilder::WithSideEffects)) {
+ ProcessorErrorDetails details;
+ details.mMessage.Assign(u"Unknown processor error");
+ SendErrorToMainThread(aTrack, details);
+ // Set the exception and stack back to have it in the console with a stack
+ // trace.
+ JS::SetPendingExceptionStack(aCx, exnStack);
+ return;
+ }
+
+ ProcessorErrorDetails details;
+
+ CopyUTF8toUTF16(mozilla::MakeStringSpan(jsReport.report()->filename),
+ details.mFilename);
+
+ xpc::ErrorReport::ErrorReportToMessageString(jsReport.report(),
+ details.mMessage);
+ details.mLineno = jsReport.report()->lineno;
+ details.mColno = jsReport.report()->column;
+ MOZ_ASSERT(!jsReport.report()->isMuted);
+
+ SendErrorToMainThread(aTrack, details);
+
+ // Set the exception and stack back to have it in the console with a stack
+ // trace.
+ JS::SetPendingExceptionStack(aCx, exnStack);
+ } else {
+ NS_WARNING("No exception, but processor errored out?");
+ }
+}
+
+void WorkletNodeEngine::ConstructProcessor(
+ AudioWorkletImpl* aWorkletImpl, const nsAString& aName,
+ NotNull<StructuredCloneHolder*> aSerializedOptions,
+ UniqueMessagePortId& aPortIdentifier, AudioNodeTrack* aTrack) {
+ MOZ_ASSERT(mInputs.mPorts.empty() && mOutputs.mPorts.empty());
+ RefPtr<AudioWorkletGlobalScope> global = aWorkletImpl->GetGlobalScope();
+ if (!global) {
+ // A global was previously used to register this kind of processor. If it
+ // no longer exists now, that is because the document is going away and so
+ // there is no need to send an error.
+ return;
+ }
+ AutoJSAPI api;
+ if (NS_WARN_IF(!api.Init(global))) {
+ SendProcessorError(aTrack, nullptr);
+ return;
+ }
+ mProcessorName = NS_ConvertUTF16toUTF8(aName);
+ JSContext* cx = api.cx();
+ mProcessor.init(cx);
+ if (!global->ConstructProcessor(cx, aName, aSerializedOptions,
+ aPortIdentifier, &mProcessor) ||
+ // mInputs and mOutputs outer arrays are fixed length and so much of the
+ // initialization need only be performed once (i.e. here).
+ NS_WARN_IF(!mInputs.mPorts.growBy(InputCount())) ||
+ NS_WARN_IF(!mOutputs.mPorts.growBy(OutputCount()))) {
+ SendProcessorError(aTrack, cx);
+ return;
+ }
+ mGlobal = std::move(global);
+ mInputs.mJSArray.init(cx);
+ mOutputs.mJSArray.init(cx);
+ for (auto& port : mInputs.mPorts) {
+ port.mJSArray.init(cx);
+ }
+ for (auto& port : mOutputs.mPorts) {
+ port.mJSArray.init(cx);
+ }
+ JSObject* object = JS_NewPlainObject(cx);
+ if (NS_WARN_IF(!object)) {
+ SendProcessorError(aTrack, cx);
+ return;
+ }
+
+ mParameters.mJSObject.init(cx, object);
+ if (NS_WARN_IF(!mParameters.mFloat32Arrays.growBy(ParameterCount()))) {
+ SendProcessorError(aTrack, cx);
+ return;
+ }
+ for (size_t i = 0; i < mParamTimelines.Length(); i++) {
+ auto& float32ArraysRef = mParameters.mFloat32Arrays;
+ float32ArraysRef[i].init(cx);
+ JSObject* array = JS_NewFloat32Array(cx, WEBAUDIO_BLOCK_SIZE);
+ if (NS_WARN_IF(!array)) {
+ SendProcessorError(aTrack, cx);
+ return;
+ }
+
+ float32ArraysRef[i] = array;
+ if (NS_WARN_IF(!JS_DefineUCProperty(
+ cx, mParameters.mJSObject, mParamTimelines[i].mName.get(),
+ mParamTimelines[i].mName.Length(), float32ArraysRef[i],
+ JSPROP_ENUMERATE))) {
+ SendProcessorError(aTrack, cx);
+ return;
+ }
+ }
+ if (NS_WARN_IF(!JS_FreezeObject(cx, mParameters.mJSObject))) {
+ SendProcessorError(aTrack, cx);
+ return;
+ }
+}
+
+// Type T should support the length() and operator[]() methods and the return
+// type of |operator[]() const| should support conversion to Handle<JSObject*>.
+template <typename T>
+static bool SetArrayElements(JSContext* aCx, const T& aElements,
+ JS::Handle<JSObject*> aArray) {
+ for (size_t i = 0; i < aElements.length(); ++i) {
+ if (!JS_DefineElement(aCx, aArray, i, aElements[i], JSPROP_ENUMERATE)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+template <typename T>
+static bool PrepareArray(JSContext* aCx, const T& aElements,
+ JS::MutableHandle<JSObject*> aArray) {
+ size_t length = aElements.length();
+ if (aArray) {
+ // Attempt to reuse.
+ uint32_t oldLength;
+ if (JS::GetArrayLength(aCx, aArray, &oldLength) &&
+ (oldLength == length || JS::SetArrayLength(aCx, aArray, length)) &&
+ SetArrayElements(aCx, aElements, aArray)) {
+ return true;
+ }
+ // Script may have frozen the array. Try again with a new Array.
+ JS_ClearPendingException(aCx);
+ }
+ JSObject* array = JS::NewArrayObject(aCx, length);
+ if (NS_WARN_IF(!array)) {
+ return false;
+ }
+ aArray.set(array);
+ return SetArrayElements(aCx, aElements, aArray);
+}
+
+enum class ArrayElementInit { None, Zero };
+
+// Exactly when to create new Float32Array and Array objects is not specified.
+// This approach aims to minimize garbage creation, while continuing to
+// function after objects are modified by content.
+// See https://github.com/WebAudio/web-audio-api/issues/1934 and
+// https://github.com/WebAudio/web-audio-api/issues/1933
+static bool PrepareBufferArrays(JSContext* aCx, Span<const AudioBlock> aBlocks,
+ WorkletNodeEngine::Ports* aPorts,
+ ArrayElementInit aInit) {
+ MOZ_ASSERT(aBlocks.Length() == aPorts->mPorts.length());
+ for (size_t i = 0; i < aBlocks.Length(); ++i) {
+ size_t channelCount = aBlocks[i].ChannelCount();
+ WorkletNodeEngine::Channels& portRef = aPorts->mPorts[i];
+
+ auto& float32ArraysRef = portRef.mFloat32Arrays;
+ for (auto& channelRef : float32ArraysRef) {
+ size_t length = JS_GetTypedArrayLength(channelRef);
+ if (length != WEBAUDIO_BLOCK_SIZE) {
+ // Script has detached array buffers. Create new objects.
+ JSObject* array = JS_NewFloat32Array(aCx, WEBAUDIO_BLOCK_SIZE);
+ if (NS_WARN_IF(!array)) {
+ return false;
+ }
+ channelRef = array;
+ } else if (aInit == ArrayElementInit::Zero) {
+ // Need only zero existing arrays as new arrays are already zeroed.
+ JS::AutoCheckCannotGC nogc;
+ bool isShared;
+ float* elementData =
+ JS_GetFloat32ArrayData(channelRef, &isShared, nogc);
+ MOZ_ASSERT(!isShared); // Was created as unshared
+ std::fill_n(elementData, WEBAUDIO_BLOCK_SIZE, 0.0f);
+ }
+ }
+ // Enlarge if necessary...
+ if (NS_WARN_IF(!float32ArraysRef.reserve(channelCount))) {
+ return false;
+ }
+ while (float32ArraysRef.length() < channelCount) {
+ JSObject* array = JS_NewFloat32Array(aCx, WEBAUDIO_BLOCK_SIZE);
+ if (NS_WARN_IF(!array)) {
+ return false;
+ }
+ float32ArraysRef.infallibleEmplaceBack(aCx, array);
+ }
+ // ... or shrink if necessary.
+ float32ArraysRef.shrinkTo(channelCount);
+
+ if (NS_WARN_IF(!PrepareArray(aCx, float32ArraysRef, &portRef.mJSArray))) {
+ return false;
+ }
+ }
+
+ return !(NS_WARN_IF(!PrepareArray(aCx, aPorts->mPorts, &aPorts->mJSArray)));
+}
+
+// This runs JS script. MediaTrackGraph control messages, which would
+// potentially destroy the WorkletNodeEngine and its AudioNodeTrack, cannot
+// be triggered by script. They are not run from an nsIThread event loop and
+// do not run until after ProcessBlocksOnPorts() has returned.
+bool WorkletNodeEngine::CallProcess(AudioNodeTrack* aTrack, JSContext* aCx,
+ JS::Handle<JS::Value> aCallable) {
+ TRACE_COMMENT("AudioWorkletNodeEngine::CallProcess", mProcessorName.get());
+
+ JS::RootedVector<JS::Value> argv(aCx);
+ if (NS_WARN_IF(!argv.resize(3))) {
+ return false;
+ }
+ argv[0].setObject(*mInputs.mJSArray);
+ argv[1].setObject(*mOutputs.mJSArray);
+ argv[2].setObject(*mParameters.mJSObject);
+ JS::Rooted<JS::Value> rval(aCx);
+ if (!JS::Call(aCx, mProcessor, aCallable, argv, &rval)) {
+ return false;
+ }
+
+ mProcessorIsActive = JS::ToBoolean(rval);
+ // Transitions of mProcessorIsActive to false do not trigger
+ // PlayingRefChangeHandler::RELEASE until silence is produced in the next
+ // block. This allows downstream engines receiving this non-silence block
+ // to take a reference to their nodes before this engine's node releases its
+ // down node references.
+ if (mProcessorIsActive && !mKeepEngineActive) {
+ mKeepEngineActive = true;
+ RefPtr<PlayingRefChangeHandler> refchanged =
+ new PlayingRefChangeHandler(aTrack, PlayingRefChangeHandler::ADDREF);
+ aTrack->Graph()->DispatchToMainThreadStableState(refchanged.forget());
+ }
+ return true;
+}
+
+void WorkletNodeEngine::ProduceSilence(AudioNodeTrack* aTrack,
+ Span<AudioBlock> aOutput) {
+ if (mKeepEngineActive) {
+ mKeepEngineActive = false;
+ aTrack->ScheduleCheckForInactive();
+ RefPtr<PlayingRefChangeHandler> refchanged =
+ new PlayingRefChangeHandler(aTrack, PlayingRefChangeHandler::RELEASE);
+ aTrack->Graph()->DispatchToMainThreadStableState(refchanged.forget());
+ }
+ for (AudioBlock& output : aOutput) {
+ output.SetNull(WEBAUDIO_BLOCK_SIZE);
+ }
+}
+
+void WorkletNodeEngine::ProcessBlocksOnPorts(AudioNodeTrack* aTrack,
+ GraphTime aFrom,
+ Span<const AudioBlock> aInput,
+ Span<AudioBlock> aOutput,
+ bool* aFinished) {
+ MOZ_ASSERT(aInput.Length() == InputCount());
+ MOZ_ASSERT(aOutput.Length() == OutputCount());
+ TRACE("WorkletNodeEngine::ProcessBlocksOnPorts");
+
+ bool isSilent = true;
+ if (mProcessor) {
+ if (mProcessorIsActive) {
+ isSilent = false; // call process()
+ } else { // [[active source]] is false.
+ // Call process() only if an input is actively processing.
+ for (const AudioBlock& input : aInput) {
+ if (!input.IsNull()) {
+ isSilent = false;
+ break;
+ }
+ }
+ }
+ }
+ if (isSilent) {
+ ProduceSilence(aTrack, aOutput);
+ return;
+ }
+
+ if (!mOutputChannelCount.IsEmpty()) {
+ MOZ_ASSERT(mOutputChannelCount.Length() == aOutput.Length());
+ for (size_t o = 0; o < aOutput.Length(); ++o) {
+ aOutput[o].AllocateChannels(mOutputChannelCount[o]);
+ }
+ } else if (aInput.Length() == 1 && aOutput.Length() == 1) {
+ uint32_t channelCount = std::max(aInput[0].ChannelCount(), 1U);
+ aOutput[0].AllocateChannels(channelCount);
+ } else {
+ for (AudioBlock& output : aOutput) {
+ output.AllocateChannels(1);
+ }
+ }
+
+ AutoEntryScript aes(mGlobal, "Worklet Process");
+ JSContext* cx = aes.cx();
+ auto produceSilenceWithError = MakeScopeExit([this, aTrack, cx, &aOutput] {
+ SendProcessorError(aTrack, cx);
+ ProduceSilence(aTrack, aOutput);
+ });
+
+ JS::Rooted<JS::Value> process(cx);
+ if (!JS_GetProperty(cx, mProcessor, "process", &process) ||
+ !process.isObject() || !JS::IsCallable(&process.toObject()) ||
+ !PrepareBufferArrays(cx, aInput, &mInputs, ArrayElementInit::None) ||
+ !PrepareBufferArrays(cx, aOutput, &mOutputs, ArrayElementInit::Zero)) {
+ // process() not callable or OOM.
+ return;
+ }
+
+ // Copy input values to JS objects.
+ for (size_t i = 0; i < aInput.Length(); ++i) {
+ const AudioBlock& input = aInput[i];
+ size_t channelCount = input.ChannelCount();
+ if (channelCount == 0) {
+ // Null blocks have AUDIO_FORMAT_SILENCE.
+ // Don't call ChannelData<float>().
+ continue;
+ }
+ float volume = input.mVolume;
+ const auto& channelData = input.ChannelData<float>();
+ const auto& float32Arrays = mInputs.mPorts[i].mFloat32Arrays;
+ JS::AutoCheckCannotGC nogc;
+ for (size_t c = 0; c < channelCount; ++c) {
+ bool isShared;
+ float* dest = JS_GetFloat32ArrayData(float32Arrays[c], &isShared, nogc);
+ MOZ_ASSERT(!isShared); // Was created as unshared
+ AudioBlockCopyChannelWithScale(channelData[c], volume, dest);
+ }
+ }
+
+ TrackTime tick = mDestination->GraphTimeToTrackTime(aFrom);
+ // Compute and copy parameter values to JS objects.
+ for (size_t i = 0; i < mParamTimelines.Length(); ++i) {
+ const auto& float32Arrays = mParameters.mFloat32Arrays[i];
+ size_t length = JS_GetTypedArrayLength(float32Arrays);
+
+ // If the Float32Array that is supposed to hold the values for a particular
+ // AudioParam has been detached, error out. This is being worked on in
+ // https://github.com/WebAudio/web-audio-api/issues/1933 and
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1619486
+ if (length != WEBAUDIO_BLOCK_SIZE) {
+ return;
+ }
+ JS::AutoCheckCannotGC nogc;
+ bool isShared;
+ float* dest = JS_GetFloat32ArrayData(float32Arrays, &isShared, nogc);
+ MOZ_ASSERT(!isShared); // Was created as unshared
+
+ size_t frames =
+ mParamTimelines[i].mTimeline.HasSimpleValue() ? 1 : WEBAUDIO_BLOCK_SIZE;
+ mParamTimelines[i].mTimeline.GetValuesAtTime(tick, dest, frames);
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1616599
+ if (frames == 1) {
+ std::fill_n(dest + 1, WEBAUDIO_BLOCK_SIZE - 1, dest[0]);
+ }
+ }
+
+ if (!CallProcess(aTrack, cx, process)) {
+ // An exception occurred.
+ /**
+ * https://webaudio.github.io/web-audio-api/#dom-audioworkletnode-onprocessorerror
+ * Note that once an exception is thrown, the processor will output silence
+ * throughout its lifetime.
+ */
+ return;
+ }
+
+ // Copy output values from JS objects.
+ for (size_t o = 0; o < aOutput.Length(); ++o) {
+ AudioBlock* output = &aOutput[o];
+ size_t channelCount = output->ChannelCount();
+ const auto& float32Arrays = mOutputs.mPorts[o].mFloat32Arrays;
+ for (size_t c = 0; c < channelCount; ++c) {
+ size_t length = JS_GetTypedArrayLength(float32Arrays[c]);
+ if (length != WEBAUDIO_BLOCK_SIZE) {
+ // ArrayBuffer has been detached. Behavior is unspecified.
+ // https://github.com/WebAudio/web-audio-api/issues/1933 and
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1619486
+ return;
+ }
+ JS::AutoCheckCannotGC nogc;
+ bool isShared;
+ const float* src =
+ JS_GetFloat32ArrayData(float32Arrays[c], &isShared, nogc);
+ MOZ_ASSERT(!isShared); // Was created as unshared
+ PodCopy(output->ChannelFloatsForWrite(c), src, WEBAUDIO_BLOCK_SIZE);
+ }
+ }
+
+ produceSilenceWithError.release(); // have output and no error
+}
+
+AudioWorkletNode::AudioWorkletNode(AudioContext* aAudioContext,
+ const nsAString& aName,
+ const AudioWorkletNodeOptions& aOptions)
+ : AudioNode(aAudioContext, 2, ChannelCountMode::Max,
+ ChannelInterpretation::Speakers),
+ mNodeName(aName),
+ mInputCount(aOptions.mNumberOfInputs),
+ mOutputCount(aOptions.mNumberOfOutputs) {}
+
+void AudioWorkletNode::InitializeParameters(
+ nsTArray<NamedAudioParamTimeline>* aParamTimelines, ErrorResult& aRv) {
+ MOZ_ASSERT(!mParameters, "Only initialize the `parameters` attribute once.");
+ MOZ_ASSERT(aParamTimelines);
+
+ AudioContext* context = Context();
+ const AudioParamDescriptorMap* parameterDescriptors =
+ context->GetParamMapForWorkletName(mNodeName);
+ MOZ_ASSERT(parameterDescriptors);
+
+ size_t audioParamIndex = 0;
+ aParamTimelines->SetCapacity(parameterDescriptors->Length());
+
+ for (size_t i = 0; i < parameterDescriptors->Length(); i++) {
+ auto& paramEntry = (*parameterDescriptors)[i];
+ CreateAudioParam(audioParamIndex++, paramEntry.mName,
+ paramEntry.mDefaultValue, paramEntry.mMinValue,
+ paramEntry.mMaxValue);
+ aParamTimelines->AppendElement(paramEntry);
+ }
+}
+
+void AudioWorkletNode::SendParameterData(
+ const Optional<Record<nsString, double>>& aParameterData) {
+ MOZ_ASSERT(mTrack, "This method only works if the track has been created.");
+ nsAutoString name;
+ if (aParameterData.WasPassed()) {
+ const auto& paramData = aParameterData.Value();
+ for (const auto& paramDataEntry : paramData.Entries()) {
+ for (auto& audioParam : mParams) {
+ audioParam->GetName(name);
+ if (paramDataEntry.mKey.Equals(name)) {
+ audioParam->SetInitialValue(paramDataEntry.mValue);
+ }
+ }
+ }
+ }
+}
+
+/* static */
+already_AddRefed<AudioWorkletNode> AudioWorkletNode::Constructor(
+ const GlobalObject& aGlobal, AudioContext& aAudioContext,
+ const nsAString& aName, const AudioWorkletNodeOptions& aOptions,
+ ErrorResult& aRv) {
+ TRACE_COMMENT("AudioWorkletNode::Constructor", "%s",
+ NS_ConvertUTF16toUTF8(aName).get());
+ /**
+ * 1. If nodeName does not exist as a key in the BaseAudioContext’s node
+ * name to parameter descriptor map, throw a InvalidStateError exception
+ * and abort these steps.
+ */
+ const AudioParamDescriptorMap* parameterDescriptors =
+ aAudioContext.GetParamMapForWorkletName(aName);
+ if (!parameterDescriptors) {
+ // Not using nsPrintfCString in case aName has embedded nulls.
+ aRv.ThrowInvalidStateError("Unknown AudioWorklet name '"_ns +
+ NS_ConvertUTF16toUTF8(aName) + "'"_ns);
+ return nullptr;
+ }
+
+ // See https://github.com/WebAudio/web-audio-api/issues/2074 for ordering.
+ RefPtr<AudioWorkletNode> audioWorkletNode =
+ new AudioWorkletNode(&aAudioContext, aName, aOptions);
+ audioWorkletNode->Initialize(aOptions, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ /**
+ * 3. Configure input, output and output channels of node with options.
+ */
+ if (aOptions.mNumberOfInputs == 0 && aOptions.mNumberOfOutputs == 0) {
+ aRv.ThrowNotSupportedError(
+ "Must have nonzero numbers of inputs or outputs");
+ return nullptr;
+ }
+
+ if (aOptions.mOutputChannelCount.WasPassed()) {
+ /**
+ * 1. If any value in outputChannelCount is zero or greater than the
+ * implementation’s maximum number of channels, throw a
+ * NotSupportedError and abort the remaining steps.
+ */
+ for (uint32_t channelCount : aOptions.mOutputChannelCount.Value()) {
+ if (channelCount == 0 || channelCount > WebAudioUtils::MaxChannelCount) {
+ aRv.ThrowNotSupportedError(
+ nsPrintfCString("Channel count (%u) must be in the range [1, max "
+ "supported channel count]",
+ channelCount));
+ return nullptr;
+ }
+ }
+ /**
+ * 2. If the length of outputChannelCount does not equal numberOfOutputs,
+ * throw an IndexSizeError and abort the remaining steps.
+ */
+ if (aOptions.mOutputChannelCount.Value().Length() !=
+ aOptions.mNumberOfOutputs) {
+ aRv.ThrowIndexSizeError(
+ nsPrintfCString("Length of outputChannelCount (%zu) does not match "
+ "numberOfOutputs (%u)",
+ aOptions.mOutputChannelCount.Value().Length(),
+ aOptions.mNumberOfOutputs));
+ return nullptr;
+ }
+ }
+ // MTG does not support more than UINT16_MAX inputs or outputs.
+ if (aOptions.mNumberOfInputs > UINT16_MAX) {
+ aRv.ThrowRangeError<MSG_VALUE_OUT_OF_RANGE>("numberOfInputs");
+ return nullptr;
+ }
+ if (aOptions.mNumberOfOutputs > UINT16_MAX) {
+ aRv.ThrowRangeError<MSG_VALUE_OUT_OF_RANGE>("numberOfOutputs");
+ return nullptr;
+ }
+
+ /**
+ * 4. Let messageChannel be a new MessageChannel.
+ */
+ RefPtr<MessageChannel> messageChannel =
+ MessageChannel::Constructor(aGlobal, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+ /* 5. Let nodePort be the value of messageChannel’s port1 attribute.
+ * 6. Let processorPortOnThisSide be the value of messageChannel’s port2
+ * attribute.
+ * 7. Let serializedProcessorPort be the result of
+ * StructuredSerializeWithTransfer(processorPortOnThisSide,
+ * « processorPortOnThisSide »).
+ */
+ UniqueMessagePortId processorPortId;
+ messageChannel->Port2()->CloneAndDisentangle(processorPortId);
+ /**
+ * 8. Convert options dictionary to optionsObject.
+ */
+ JSContext* cx = aGlobal.Context();
+ JS::Rooted<JS::Value> optionsVal(cx);
+ if (NS_WARN_IF(!ToJSValue(cx, aOptions, &optionsVal))) {
+ aRv.NoteJSContextException(cx);
+ return nullptr;
+ }
+
+ /**
+ * 9. Let serializedOptions be the result of
+ * StructuredSerialize(optionsObject).
+ */
+
+ // This context and the worklet are part of the same agent cluster and they
+ // can share memory.
+ JS::CloneDataPolicy cloneDataPolicy;
+ cloneDataPolicy.allowIntraClusterClonableSharedObjects();
+ cloneDataPolicy.allowSharedMemoryObjects();
+
+ // StructuredCloneHolder does not have a move constructor. Instead allocate
+ // memory so that the pointer can be passed to the rendering thread.
+ UniquePtr<StructuredCloneHolder> serializedOptions =
+ MakeUnique<StructuredCloneHolder>(
+ StructuredCloneHolder::CloningSupported,
+ StructuredCloneHolder::TransferringNotSupported,
+ JS::StructuredCloneScope::SameProcess);
+ serializedOptions->Write(cx, optionsVal, JS::UndefinedHandleValue,
+ cloneDataPolicy, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+ /**
+ * 10. Set node’s port to nodePort.
+ */
+ audioWorkletNode->mPort = messageChannel->Port1();
+
+ /**
+ * 11. Let parameterDescriptors be the result of retrieval of nodeName from
+ * node name to parameter descriptor map.
+ */
+ nsTArray<NamedAudioParamTimeline> paramTimelines;
+ audioWorkletNode->InitializeParameters(&paramTimelines, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ auto engine = new WorkletNodeEngine(
+ audioWorkletNode, aAudioContext.Destination(), std::move(paramTimelines),
+ aOptions.mOutputChannelCount);
+ audioWorkletNode->mTrack = AudioNodeTrack::Create(
+ &aAudioContext, engine, AudioNodeTrack::NO_TRACK_FLAGS,
+ aAudioContext.Graph());
+
+ audioWorkletNode->SendParameterData(aOptions.mParameterData);
+
+ /**
+ * 12. Queue a control message to invoke the constructor of the
+ * corresponding AudioWorkletProcessor with the processor construction
+ * data that consists of: nodeName, node, serializedOptions, and
+ * serializedProcessorPort.
+ */
+ Worklet* worklet = aAudioContext.GetAudioWorklet(aRv);
+ MOZ_ASSERT(worklet, "Worklet already existed and so getter shouldn't fail.");
+ auto workletImpl = static_cast<AudioWorkletImpl*>(worklet->Impl());
+ audioWorkletNode->mTrack->SendRunnable(NS_NewRunnableFunction(
+ "WorkletNodeEngine::ConstructProcessor",
+ // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT.
+ // See bug 1535398.
+ //
+ // Note that clang and gcc have mutually incompatible rules about whether
+ // attributes should come before or after the `mutable` keyword here, so
+ // use a compatibility hack until we can switch to the standardized
+ // [[attr]] syntax (bug 1627007).
+#ifdef __clang__
+# define AND_MUTABLE(macro) macro mutable
+#else
+# define AND_MUTABLE(macro) mutable macro
+#endif
+ [track = audioWorkletNode->mTrack,
+ workletImpl = RefPtr<AudioWorkletImpl>(workletImpl),
+ name = nsString(aName), options = std::move(serializedOptions),
+ portId = std::move(processorPortId)]()
+ AND_MUTABLE(MOZ_CAN_RUN_SCRIPT_BOUNDARY) {
+ auto engine = static_cast<WorkletNodeEngine*>(track->Engine());
+ engine->ConstructProcessor(
+ workletImpl, name, WrapNotNull(options.get()), portId, track);
+ }));
+#undef AND_MUTABLE
+
+ // [[active source]] is initially true and so at least the first process()
+ // call will not be skipped when there are no active inputs.
+ audioWorkletNode->MarkActive();
+
+ return audioWorkletNode.forget();
+}
+
+AudioParamMap* AudioWorkletNode::GetParameters(ErrorResult& aRv) {
+ if (!mParameters) {
+ RefPtr<AudioParamMap> parameters = new AudioParamMap(this);
+ nsAutoString name;
+ for (const auto& audioParam : mParams) {
+ audioParam->GetName(name);
+ AudioParamMap_Binding::MaplikeHelpers::Set(parameters, name, *audioParam,
+ aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+ }
+ mParameters = std::move(parameters);
+ }
+ return mParameters.get();
+}
+
+void AudioWorkletNode::DispatchProcessorErrorEvent(
+ const ProcessorErrorDetails& aDetails) {
+ TRACE("AudioWorkletNode::DispatchProcessorErrorEvent");
+ if (HasListenersFor(nsGkAtoms::onprocessorerror)) {
+ RootedDictionary<ErrorEventInit> init(RootingCx());
+ init.mMessage = aDetails.mMessage;
+ init.mFilename = aDetails.mFilename;
+ init.mLineno = aDetails.mLineno;
+ init.mColno = aDetails.mColno;
+ RefPtr<ErrorEvent> errorEvent =
+ ErrorEvent::Constructor(this, u"processorerror"_ns, init);
+ MOZ_ASSERT(errorEvent);
+ DispatchTrustedEvent(errorEvent);
+ }
+}
+
+JSObject* AudioWorkletNode::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return AudioWorkletNode_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+size_t AudioWorkletNode::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf);
+ return amount;
+}
+
+size_t AudioWorkletNode::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/AudioWorkletNode.h b/dom/media/webaudio/AudioWorkletNode.h
new file mode 100644
index 0000000000..22e089f8cd
--- /dev/null
+++ b/dom/media/webaudio/AudioWorkletNode.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#ifndef AudioWorkletNode_h_
+#define AudioWorkletNode_h_
+
+#include "AudioNode.h"
+
+namespace mozilla::dom {
+
+class AudioParamMap;
+struct AudioWorkletNodeOptions;
+class MessagePort;
+struct NamedAudioParamTimeline;
+struct ProcessorErrorDetails;
+template <typename KeyType, typename ValueType>
+class Record;
+
+class AudioWorkletNode : public AudioNode {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(AudioWorkletNode, AudioNode)
+
+ IMPL_EVENT_HANDLER(processorerror)
+
+ static already_AddRefed<AudioWorkletNode> Constructor(
+ const GlobalObject& aGlobal, AudioContext& aAudioContext,
+ const nsAString& aName, const AudioWorkletNodeOptions& aOptions,
+ ErrorResult& aRv);
+
+ AudioParamMap* GetParameters(ErrorResult& aRv);
+
+ MessagePort* Port() const { return mPort; };
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // AudioNode methods
+ uint16_t NumberOfInputs() const override { return mInputCount; }
+ uint16_t NumberOfOutputs() const override { return mOutputCount; }
+ const char* NodeType() const override { return "AudioWorkletNode"; }
+ void DispatchProcessorErrorEvent(const ProcessorErrorDetails& aDetails);
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override;
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override;
+
+ private:
+ AudioWorkletNode(AudioContext* aAudioContext, const nsAString& aName,
+ const AudioWorkletNodeOptions& aOptions);
+ ~AudioWorkletNode() = default;
+ void InitializeParameters(nsTArray<NamedAudioParamTimeline>* aParamTimelines,
+ ErrorResult& aRv);
+ void SendParameterData(
+ const Optional<Record<nsString, double>>& aParameterData);
+
+ nsString mNodeName;
+ RefPtr<MessagePort> mPort;
+ RefPtr<AudioParamMap> mParameters;
+ uint16_t mInputCount;
+ uint16_t mOutputCount;
+};
+
+} // namespace mozilla::dom
+
+#endif // AudioWorkletNode_h_
diff --git a/dom/media/webaudio/AudioWorkletProcessor.cpp b/dom/media/webaudio/AudioWorkletProcessor.cpp
new file mode 100644
index 0000000000..194cc5bf97
--- /dev/null
+++ b/dom/media/webaudio/AudioWorkletProcessor.cpp
@@ -0,0 +1,49 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#include "AudioWorkletProcessor.h"
+
+#include "mozilla/dom/AudioWorkletNodeBinding.h"
+#include "mozilla/dom/AudioWorkletProcessorBinding.h"
+#include "mozilla/dom/AudioWorkletGlobalScope.h"
+#include "mozilla/dom/MessagePort.h"
+#include "mozilla/dom/WorkletGlobalScope.h"
+#include "nsIGlobalObject.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(AudioWorkletProcessor, mParent, mPort)
+
+AudioWorkletProcessor::AudioWorkletProcessor(nsIGlobalObject* aParent,
+ MessagePort* aPort)
+ : mParent(aParent), mPort(aPort) {}
+
+AudioWorkletProcessor::~AudioWorkletProcessor() = default;
+
+/* static */
+already_AddRefed<AudioWorkletProcessor> AudioWorkletProcessor::Constructor(
+ const GlobalObject& aGlobal, ErrorResult& aRv) {
+ nsCOMPtr<WorkletGlobalScope> global =
+ do_QueryInterface(aGlobal.GetAsSupports());
+ MOZ_ASSERT(global);
+ RefPtr<MessagePort> port = static_cast<AudioWorkletGlobalScope*>(global.get())
+ ->TakePortForProcessorCtor();
+ if (!port) {
+ aRv.ThrowTypeError<MSG_ILLEGAL_CONSTRUCTOR>();
+ return nullptr;
+ }
+ RefPtr<AudioWorkletProcessor> audioWorkletProcessor =
+ new AudioWorkletProcessor(global, port);
+
+ return audioWorkletProcessor.forget();
+}
+
+JSObject* AudioWorkletProcessor::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return AudioWorkletProcessor_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/AudioWorkletProcessor.h b/dom/media/webaudio/AudioWorkletProcessor.h
new file mode 100644
index 0000000000..3d37d61498
--- /dev/null
+++ b/dom/media/webaudio/AudioWorkletProcessor.h
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#ifndef AudioWorkletProcessor_h_
+#define AudioWorkletProcessor_h_
+
+#include "nsCOMPtr.h"
+#include "nsWrapperCache.h"
+
+class nsIGlobalObject;
+
+namespace mozilla {
+
+class ErrorResult;
+
+namespace dom {
+
+struct AudioWorkletNodeOptions;
+class GlobalObject;
+class MessagePort;
+
+class AudioWorkletProcessor final : public nsWrapperCache {
+ public:
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(AudioWorkletProcessor)
+ NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(AudioWorkletProcessor)
+
+ static already_AddRefed<AudioWorkletProcessor> Constructor(
+ const GlobalObject& aGlobal, ErrorResult& aRv);
+
+ nsIGlobalObject* GetParentObject() const { return mParent; }
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ MessagePort* Port() const { return mPort; };
+
+ private:
+ explicit AudioWorkletProcessor(nsIGlobalObject* aParent, MessagePort* aPort);
+ ~AudioWorkletProcessor();
+ nsCOMPtr<nsIGlobalObject> mParent;
+ RefPtr<MessagePort> mPort;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // AudioWorkletProcessor_h_
diff --git a/dom/media/webaudio/BiquadFilterNode.cpp b/dom/media/webaudio/BiquadFilterNode.cpp
new file mode 100644
index 0000000000..772043a5f2
--- /dev/null
+++ b/dom/media/webaudio/BiquadFilterNode.cpp
@@ -0,0 +1,349 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "BiquadFilterNode.h"
+#include "AlignmentUtils.h"
+#include "AudioNodeEngine.h"
+#include "AudioNodeTrack.h"
+#include "AudioDestinationNode.h"
+#include "PlayingRefChangeHandler.h"
+#include "WebAudioUtils.h"
+#include "blink/Biquad.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/ErrorResult.h"
+#include "AudioParamTimeline.h"
+#include "Tracing.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(BiquadFilterNode, AudioNode, mFrequency,
+ mDetune, mQ, mGain)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(BiquadFilterNode)
+NS_INTERFACE_MAP_END_INHERITING(AudioNode)
+
+NS_IMPL_ADDREF_INHERITED(BiquadFilterNode, AudioNode)
+NS_IMPL_RELEASE_INHERITED(BiquadFilterNode, AudioNode)
+
+static void SetParamsOnBiquad(WebCore::Biquad& aBiquad, float aSampleRate,
+ BiquadFilterType aType, double aFrequency,
+ double aQ, double aGain, double aDetune) {
+ const double nyquist = aSampleRate * 0.5;
+ double normalizedFrequency = aFrequency / nyquist;
+
+ if (aDetune) {
+ normalizedFrequency *= std::exp2(aDetune / 1200);
+ }
+
+ switch (aType) {
+ case BiquadFilterType::Lowpass:
+ aBiquad.setLowpassParams(normalizedFrequency, aQ);
+ break;
+ case BiquadFilterType::Highpass:
+ aBiquad.setHighpassParams(normalizedFrequency, aQ);
+ break;
+ case BiquadFilterType::Bandpass:
+ aBiquad.setBandpassParams(normalizedFrequency, aQ);
+ break;
+ case BiquadFilterType::Lowshelf:
+ aBiquad.setLowShelfParams(normalizedFrequency, aGain);
+ break;
+ case BiquadFilterType::Highshelf:
+ aBiquad.setHighShelfParams(normalizedFrequency, aGain);
+ break;
+ case BiquadFilterType::Peaking:
+ aBiquad.setPeakingParams(normalizedFrequency, aQ, aGain);
+ break;
+ case BiquadFilterType::Notch:
+ aBiquad.setNotchParams(normalizedFrequency, aQ);
+ break;
+ case BiquadFilterType::Allpass:
+ aBiquad.setAllpassParams(normalizedFrequency, aQ);
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("We should never see the alternate names here");
+ break;
+ }
+}
+
+class BiquadFilterNodeEngine final : public AudioNodeEngine {
+ public:
+ BiquadFilterNodeEngine(AudioNode* aNode, AudioDestinationNode* aDestination,
+ uint64_t aWindowID)
+ : AudioNodeEngine(aNode),
+ mDestination(aDestination->Track())
+ // Keep the default values in sync with the default values in
+ // BiquadFilterNode::BiquadFilterNode
+ ,
+ mType(BiquadFilterType::Lowpass),
+ mFrequency(350.f),
+ mDetune(0.f),
+ mQ(1.f),
+ mGain(0.f),
+ mWindowID(aWindowID) {}
+
+ enum Parameters { TYPE, FREQUENCY, DETUNE, Q, GAIN };
+ void SetInt32Parameter(uint32_t aIndex, int32_t aValue) override {
+ switch (aIndex) {
+ case TYPE:
+ mType = static_cast<BiquadFilterType>(aValue);
+ break;
+ default:
+ NS_ERROR("Bad BiquadFilterNode Int32Parameter");
+ }
+ }
+ void RecvTimelineEvent(uint32_t aIndex, AudioTimelineEvent& aEvent) override {
+ MOZ_ASSERT(mDestination);
+
+ WebAudioUtils::ConvertAudioTimelineEventToTicks(aEvent, mDestination);
+
+ switch (aIndex) {
+ case FREQUENCY:
+ mFrequency.InsertEvent<int64_t>(aEvent);
+ break;
+ case DETUNE:
+ mDetune.InsertEvent<int64_t>(aEvent);
+ break;
+ case Q:
+ mQ.InsertEvent<int64_t>(aEvent);
+ break;
+ case GAIN:
+ mGain.InsertEvent<int64_t>(aEvent);
+ break;
+ default:
+ NS_ERROR("Bad BiquadFilterNodeEngine TimelineParameter");
+ }
+ }
+
+ void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom,
+ const AudioBlock& aInput, AudioBlock* aOutput,
+ bool* aFinished) override {
+ TRACE("BiquadFilterNode::ProcessBlock");
+ float inputBuffer[WEBAUDIO_BLOCK_SIZE + 4];
+ float* alignedInputBuffer = ALIGNED16(inputBuffer);
+ ASSERT_ALIGNED16(alignedInputBuffer);
+
+ if (aInput.IsNull()) {
+ bool hasTail = false;
+ for (uint32_t i = 0; i < mBiquads.Length(); ++i) {
+ if (mBiquads[i].hasTail()) {
+ hasTail = true;
+ break;
+ }
+ }
+ if (!hasTail) {
+ if (!mBiquads.IsEmpty()) {
+ mBiquads.Clear();
+ aTrack->ScheduleCheckForInactive();
+
+ RefPtr<PlayingRefChangeHandler> refchanged =
+ new PlayingRefChangeHandler(aTrack,
+ PlayingRefChangeHandler::RELEASE);
+ aTrack->Graph()->DispatchToMainThreadStableState(refchanged.forget());
+ }
+
+ aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
+ return;
+ }
+
+ PodArrayZero(inputBuffer);
+
+ } else if (mBiquads.Length() != aInput.ChannelCount()) {
+ if (mBiquads.IsEmpty()) {
+ RefPtr<PlayingRefChangeHandler> refchanged =
+ new PlayingRefChangeHandler(aTrack,
+ PlayingRefChangeHandler::ADDREF);
+ aTrack->Graph()->DispatchToMainThreadStableState(refchanged.forget());
+ } else { // Help people diagnose bug 924718
+ WebAudioUtils::LogToDeveloperConsole(
+ mWindowID, "BiquadFilterChannelCountChangeWarning");
+ }
+
+ // Adjust the number of biquads based on the number of channels
+ mBiquads.SetLength(aInput.ChannelCount());
+ }
+
+ uint32_t numberOfChannels = mBiquads.Length();
+ aOutput->AllocateChannels(numberOfChannels);
+
+ TrackTime pos = mDestination->GraphTimeToTrackTime(aFrom);
+
+ double freq = mFrequency.GetValueAtTime(pos);
+ double q = mQ.GetValueAtTime(pos);
+ double gain = mGain.GetValueAtTime(pos);
+ double detune = mDetune.GetValueAtTime(pos);
+
+ for (uint32_t i = 0; i < numberOfChannels; ++i) {
+ const float* input;
+ if (aInput.IsNull()) {
+ input = alignedInputBuffer;
+ } else {
+ input = static_cast<const float*>(aInput.mChannelData[i]);
+ if (aInput.mVolume != 1.0) {
+ AudioBlockCopyChannelWithScale(input, aInput.mVolume,
+ alignedInputBuffer);
+ input = alignedInputBuffer;
+ }
+ }
+ SetParamsOnBiquad(mBiquads[i], aTrack->mSampleRate, mType, freq, q, gain,
+ detune);
+
+ mBiquads[i].process(input, aOutput->ChannelFloatsForWrite(i),
+ aInput.GetDuration());
+ }
+ }
+
+ bool IsActive() const override { return !mBiquads.IsEmpty(); }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override {
+ // Not owned:
+ // - mDestination - probably not owned
+ // - AudioParamTimelines - counted in the AudioNode
+ size_t amount = AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf);
+ amount += mBiquads.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ return amount;
+ }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ private:
+ RefPtr<AudioNodeTrack> mDestination;
+ BiquadFilterType mType;
+ AudioParamTimeline mFrequency;
+ AudioParamTimeline mDetune;
+ AudioParamTimeline mQ;
+ AudioParamTimeline mGain;
+ nsTArray<WebCore::Biquad> mBiquads;
+ uint64_t mWindowID;
+};
+
+BiquadFilterNode::BiquadFilterNode(AudioContext* aContext)
+ : AudioNode(aContext, 2, ChannelCountMode::Max,
+ ChannelInterpretation::Speakers),
+ mType(BiquadFilterType::Lowpass) {
+ mFrequency = CreateAudioParam(
+ BiquadFilterNodeEngine::FREQUENCY, u"frequency"_ns, 350.f,
+ -(aContext->SampleRate() / 2), aContext->SampleRate() / 2);
+ mDetune = CreateAudioParam(BiquadFilterNodeEngine::DETUNE, u"detune"_ns, 0.f);
+ mQ = CreateAudioParam(BiquadFilterNodeEngine::Q, u"Q"_ns, 1.f);
+ mGain = CreateAudioParam(BiquadFilterNodeEngine::GAIN, u"gain"_ns, 0.f);
+
+ uint64_t windowID = 0;
+ if (aContext->GetParentObject()) {
+ windowID = aContext->GetParentObject()->WindowID();
+ }
+ BiquadFilterNodeEngine* engine =
+ new BiquadFilterNodeEngine(this, aContext->Destination(), windowID);
+ mTrack = AudioNodeTrack::Create(
+ aContext, engine, AudioNodeTrack::NO_TRACK_FLAGS, aContext->Graph());
+}
+
+/* static */
+already_AddRefed<BiquadFilterNode> BiquadFilterNode::Create(
+ AudioContext& aAudioContext, const BiquadFilterOptions& aOptions,
+ ErrorResult& aRv) {
+ RefPtr<BiquadFilterNode> audioNode = new BiquadFilterNode(&aAudioContext);
+
+ audioNode->Initialize(aOptions, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ audioNode->SetType(aOptions.mType);
+ audioNode->Q()->SetInitialValue(aOptions.mQ);
+ audioNode->Detune()->SetInitialValue(aOptions.mDetune);
+ audioNode->Frequency()->SetInitialValue(aOptions.mFrequency);
+ audioNode->Gain()->SetInitialValue(aOptions.mGain);
+
+ return audioNode.forget();
+}
+
+size_t BiquadFilterNode::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf);
+
+ if (mFrequency) {
+ amount += mFrequency->SizeOfIncludingThis(aMallocSizeOf);
+ }
+
+ if (mDetune) {
+ amount += mDetune->SizeOfIncludingThis(aMallocSizeOf);
+ }
+
+ if (mQ) {
+ amount += mQ->SizeOfIncludingThis(aMallocSizeOf);
+ }
+
+ if (mGain) {
+ amount += mGain->SizeOfIncludingThis(aMallocSizeOf);
+ }
+
+ return amount;
+}
+
+size_t BiquadFilterNode::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
+
+JSObject* BiquadFilterNode::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return BiquadFilterNode_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void BiquadFilterNode::SetType(BiquadFilterType aType) {
+ mType = aType;
+ SendInt32ParameterToTrack(BiquadFilterNodeEngine::TYPE,
+ static_cast<int32_t>(aType));
+}
+
+void BiquadFilterNode::GetFrequencyResponse(const Float32Array& aFrequencyHz,
+ const Float32Array& aMagResponse,
+ const Float32Array& aPhaseResponse,
+ ErrorResult& aRv) {
+ aFrequencyHz.ComputeState();
+ aMagResponse.ComputeState();
+ aPhaseResponse.ComputeState();
+
+ if (!(aFrequencyHz.Length() == aMagResponse.Length() &&
+ aMagResponse.Length() == aPhaseResponse.Length())) {
+ aRv.ThrowInvalidAccessError("Parameter lengths must match");
+ return;
+ }
+
+ uint32_t length = aFrequencyHz.Length();
+ if (!length) {
+ return;
+ }
+
+ auto frequencies = MakeUnique<float[]>(length);
+ float* frequencyHz = aFrequencyHz.Data();
+ const double nyquist = Context()->SampleRate() * 0.5;
+
+ // Normalize the frequencies
+ for (uint32_t i = 0; i < length; ++i) {
+ if (frequencyHz[i] >= 0 && frequencyHz[i] <= nyquist) {
+ frequencies[i] = static_cast<float>(frequencyHz[i] / nyquist);
+ } else {
+ frequencies[i] = std::numeric_limits<float>::quiet_NaN();
+ }
+ }
+
+ const double currentTime = Context()->CurrentTime();
+
+ double freq = mFrequency->GetValueAtTime(currentTime);
+ double q = mQ->GetValueAtTime(currentTime);
+ double gain = mGain->GetValueAtTime(currentTime);
+ double detune = mDetune->GetValueAtTime(currentTime);
+
+ WebCore::Biquad biquad;
+ SetParamsOnBiquad(biquad, Context()->SampleRate(), mType, freq, q, gain,
+ detune);
+ biquad.getFrequencyResponse(int(length), frequencies.get(),
+ aMagResponse.Data(), aPhaseResponse.Data());
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/BiquadFilterNode.h b/dom/media/webaudio/BiquadFilterNode.h
new file mode 100644
index 0000000000..31b08c00c8
--- /dev/null
+++ b/dom/media/webaudio/BiquadFilterNode.h
@@ -0,0 +1,71 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef BiquadFilterNode_h_
+#define BiquadFilterNode_h_
+
+#include "AudioNode.h"
+#include "AudioParam.h"
+#include "mozilla/dom/BiquadFilterNodeBinding.h"
+
+namespace mozilla::dom {
+
+class AudioContext;
+struct BiquadFilterOptions;
+
+class BiquadFilterNode final : public AudioNode {
+ public:
+ static already_AddRefed<BiquadFilterNode> Create(
+ AudioContext& aAudioContext, const BiquadFilterOptions& aOptions,
+ ErrorResult& aRv);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(BiquadFilterNode, AudioNode)
+
+ static already_AddRefed<BiquadFilterNode> Constructor(
+ const GlobalObject& aGlobal, AudioContext& aAudioContext,
+ const BiquadFilterOptions& aOptions, ErrorResult& aRv) {
+ return Create(aAudioContext, aOptions, aRv);
+ }
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ BiquadFilterType Type() const { return mType; }
+ void SetType(BiquadFilterType aType);
+
+ AudioParam* Frequency() const { return mFrequency; }
+
+ AudioParam* Detune() const { return mDetune; }
+
+ AudioParam* Q() const { return mQ; }
+
+ AudioParam* Gain() const { return mGain; }
+
+ void GetFrequencyResponse(const Float32Array& aFrequencyHz,
+ const Float32Array& aMagResponse,
+ const Float32Array& aPhaseResponse,
+ ErrorResult& aRv);
+
+ const char* NodeType() const override { return "BiquadFilterNode"; }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override;
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override;
+
+ private:
+ explicit BiquadFilterNode(AudioContext* aContext);
+ ~BiquadFilterNode() = default;
+
+ BiquadFilterType mType;
+ RefPtr<AudioParam> mFrequency;
+ RefPtr<AudioParam> mDetune;
+ RefPtr<AudioParam> mQ;
+ RefPtr<AudioParam> mGain;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webaudio/ChannelMergerNode.cpp b/dom/media/webaudio/ChannelMergerNode.cpp
new file mode 100644
index 0000000000..4ffae02591
--- /dev/null
+++ b/dom/media/webaudio/ChannelMergerNode.cpp
@@ -0,0 +1,100 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/ChannelMergerNode.h"
+#include "mozilla/dom/ChannelMergerNodeBinding.h"
+#include "AudioNodeEngine.h"
+#include "AudioNodeTrack.h"
+#include "nsPrintfCString.h"
+#include "Tracing.h"
+
+namespace mozilla::dom {
+
+class ChannelMergerNodeEngine final : public AudioNodeEngine {
+ public:
+ explicit ChannelMergerNodeEngine(ChannelMergerNode* aNode)
+ : AudioNodeEngine(aNode) {
+ MOZ_ASSERT(NS_IsMainThread());
+ }
+
+ void ProcessBlocksOnPorts(AudioNodeTrack* aTrack, GraphTime aFrom,
+ Span<const AudioBlock> aInput,
+ Span<AudioBlock> aOutput,
+ bool* aFinished) override {
+ MOZ_ASSERT(aInput.Length() == InputCount());
+ MOZ_ASSERT(aOutput.Length() == 1, "Should have only one output port");
+ TRACE("ChannelMergerNodeEngine::ProcessBlocksOnPorts");
+
+ // Get the number of output channels, and allocate it
+ size_t channelCount = InputCount();
+ bool allNull = true;
+ for (size_t i = 0; i < channelCount; ++i) {
+ allNull &= aInput[i].IsNull();
+ }
+ if (allNull) {
+ aOutput[0].SetNull(WEBAUDIO_BLOCK_SIZE);
+ return;
+ }
+
+ aOutput[0].AllocateChannels(channelCount);
+
+ for (size_t i = 0; i < channelCount; ++i) {
+ float* output = aOutput[0].ChannelFloatsForWrite(i);
+ if (aInput[i].IsNull()) {
+ PodZero(output, WEBAUDIO_BLOCK_SIZE);
+ } else {
+ AudioBlockCopyChannelWithScale(
+ static_cast<const float*>(aInput[i].mChannelData[0]),
+ aInput[i].mVolume, output);
+ }
+ }
+ }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+};
+
+ChannelMergerNode::ChannelMergerNode(AudioContext* aContext,
+ uint16_t aInputCount)
+ : AudioNode(aContext, 1, ChannelCountMode::Explicit,
+ ChannelInterpretation::Speakers),
+ mInputCount(aInputCount) {
+ mTrack =
+ AudioNodeTrack::Create(aContext, new ChannelMergerNodeEngine(this),
+ AudioNodeTrack::NO_TRACK_FLAGS, aContext->Graph());
+}
+
+/* static */
+already_AddRefed<ChannelMergerNode> ChannelMergerNode::Create(
+ AudioContext& aAudioContext, const ChannelMergerOptions& aOptions,
+ ErrorResult& aRv) {
+ if (aOptions.mNumberOfInputs == 0 ||
+ aOptions.mNumberOfInputs > WebAudioUtils::MaxChannelCount) {
+ aRv.ThrowIndexSizeError(
+ nsPrintfCString("Number of inputs (%u) must be in the range [1, number "
+ "of supported channels]",
+ aOptions.mNumberOfInputs));
+ return nullptr;
+ }
+
+ RefPtr<ChannelMergerNode> audioNode =
+ new ChannelMergerNode(&aAudioContext, aOptions.mNumberOfInputs);
+
+ audioNode->Initialize(aOptions, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ return audioNode.forget();
+}
+
+JSObject* ChannelMergerNode::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return ChannelMergerNode_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/ChannelMergerNode.h b/dom/media/webaudio/ChannelMergerNode.h
new file mode 100644
index 0000000000..92327c8b1e
--- /dev/null
+++ b/dom/media/webaudio/ChannelMergerNode.h
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef ChannelMergerNode_h_
+#define ChannelMergerNode_h_
+
+#include "AudioNode.h"
+
+namespace mozilla::dom {
+
+class AudioContext;
+struct ChannelMergerOptions;
+
+class ChannelMergerNode final : public AudioNode {
+ public:
+ static already_AddRefed<ChannelMergerNode> Create(
+ AudioContext& aAudioContext, const ChannelMergerOptions& aOptions,
+ ErrorResult& aRv);
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(ChannelMergerNode, AudioNode)
+
+ static already_AddRefed<ChannelMergerNode> Constructor(
+ const GlobalObject& aGlobal, AudioContext& aAudioContext,
+ const ChannelMergerOptions& aOptions, ErrorResult& aRv) {
+ return Create(aAudioContext, aOptions, aRv);
+ }
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ uint16_t NumberOfInputs() const override { return mInputCount; }
+
+ const char* NodeType() const override { return "ChannelMergerNode"; }
+
+ virtual void SetChannelCount(uint32_t aChannelCount,
+ ErrorResult& aRv) override {
+ if (aChannelCount != 1) {
+ aRv.ThrowInvalidStateError(
+ "Cannot change channel count of ChannelMergerNode");
+ }
+ }
+
+ virtual void SetChannelCountModeValue(ChannelCountMode aMode,
+ ErrorResult& aRv) override {
+ if (aMode != ChannelCountMode::Explicit) {
+ aRv.ThrowInvalidStateError(
+ "Cannot change channel count mode of ChannelMergerNode");
+ }
+ }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ private:
+ ChannelMergerNode(AudioContext* aContext, uint16_t aInputCount);
+ ~ChannelMergerNode() = default;
+
+ const uint16_t mInputCount;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webaudio/ChannelSplitterNode.cpp b/dom/media/webaudio/ChannelSplitterNode.cpp
new file mode 100644
index 0000000000..52d3ae948f
--- /dev/null
+++ b/dom/media/webaudio/ChannelSplitterNode.cpp
@@ -0,0 +1,105 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/ChannelSplitterNode.h"
+#include "mozilla/dom/ChannelSplitterNodeBinding.h"
+#include "AudioNodeEngine.h"
+#include "AudioNodeTrack.h"
+#include "Tracing.h"
+
+namespace mozilla::dom {
+
+class ChannelSplitterNodeEngine final : public AudioNodeEngine {
+ public:
+ explicit ChannelSplitterNodeEngine(ChannelSplitterNode* aNode)
+ : AudioNodeEngine(aNode) {
+ MOZ_ASSERT(NS_IsMainThread());
+ }
+
+ void ProcessBlocksOnPorts(AudioNodeTrack* aTrack, GraphTime aFrom,
+ Span<const AudioBlock> aInput,
+ Span<AudioBlock> aOutput,
+ bool* aFinished) override {
+ MOZ_ASSERT(aInput.Length() == 1, "Should only have one input port");
+ MOZ_ASSERT(aOutput.Length() == OutputCount());
+ TRACE("ChannelSplitterNodeEngine::ProcessBlocksOnPorts");
+
+ for (uint16_t i = 0; i < OutputCount(); ++i) {
+ if (i < aInput[0].ChannelCount()) {
+ // Split out existing channels
+ aOutput[i].AllocateChannels(1);
+ AudioBlockCopyChannelWithScale(
+ static_cast<const float*>(aInput[0].mChannelData[i]),
+ aInput[0].mVolume, aOutput[i].ChannelFloatsForWrite(0));
+ } else {
+ // Pad with silent channels if needed
+ aOutput[i].SetNull(WEBAUDIO_BLOCK_SIZE);
+ }
+ }
+ }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+};
+
+ChannelSplitterNode::ChannelSplitterNode(AudioContext* aContext,
+ uint16_t aOutputCount)
+ : AudioNode(aContext, aOutputCount, ChannelCountMode::Explicit,
+ ChannelInterpretation::Discrete),
+ mOutputCount(aOutputCount) {
+ mTrack =
+ AudioNodeTrack::Create(aContext, new ChannelSplitterNodeEngine(this),
+ AudioNodeTrack::NO_TRACK_FLAGS, aContext->Graph());
+}
+
+/* static */
+already_AddRefed<ChannelSplitterNode> ChannelSplitterNode::Create(
+ AudioContext& aAudioContext, const ChannelSplitterOptions& aOptions,
+ ErrorResult& aRv) {
+ if (aOptions.mNumberOfOutputs == 0 ||
+ aOptions.mNumberOfOutputs > WebAudioUtils::MaxChannelCount) {
+ aRv.ThrowIndexSizeError(nsPrintfCString(
+ "%u is not a valid number of outputs", aOptions.mNumberOfOutputs));
+ return nullptr;
+ }
+
+ RefPtr<ChannelSplitterNode> audioNode =
+ new ChannelSplitterNode(&aAudioContext, aOptions.mNumberOfOutputs);
+
+ // Manually check that the other options are valid, this node has
+ // channelCount, channelCountMode and channelInterpretation constraints: they
+ // cannot be changed from the default.
+ if (aOptions.mChannelCount.WasPassed()) {
+ audioNode->SetChannelCount(aOptions.mChannelCount.Value(), aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+ }
+ if (aOptions.mChannelInterpretation.WasPassed()) {
+ audioNode->SetChannelInterpretationValue(
+ aOptions.mChannelInterpretation.Value(), aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+ }
+ if (aOptions.mChannelCountMode.WasPassed()) {
+ audioNode->SetChannelCountModeValue(aOptions.mChannelCountMode.Value(),
+ aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+ }
+
+ return audioNode.forget();
+}
+
+JSObject* ChannelSplitterNode::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return ChannelSplitterNode_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/ChannelSplitterNode.h b/dom/media/webaudio/ChannelSplitterNode.h
new file mode 100644
index 0000000000..c13eaa8f22
--- /dev/null
+++ b/dom/media/webaudio/ChannelSplitterNode.h
@@ -0,0 +1,72 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef ChannelSplitterNode_h_
+#define ChannelSplitterNode_h_
+
+#include "AudioNode.h"
+
+namespace mozilla::dom {
+
+class AudioContext;
+struct ChannelSplitterOptions;
+
+class ChannelSplitterNode final : public AudioNode {
+ public:
+ static already_AddRefed<ChannelSplitterNode> Create(
+ AudioContext& aAudioContext, const ChannelSplitterOptions& aOptions,
+ ErrorResult& aRv);
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(ChannelSplitterNode, AudioNode)
+
+ static already_AddRefed<ChannelSplitterNode> Constructor(
+ const GlobalObject& aGlobal, AudioContext& aAudioContext,
+ const ChannelSplitterOptions& aOptions, ErrorResult& aRv) {
+ return Create(aAudioContext, aOptions, aRv);
+ }
+
+ void SetChannelCount(uint32_t aChannelCount, ErrorResult& aRv) override {
+ if (aChannelCount != ChannelCount()) {
+ aRv.ThrowInvalidStateError(
+ "Cannot change channel count of ChannelSplitterNode");
+ }
+ }
+ void SetChannelCountModeValue(ChannelCountMode aMode,
+ ErrorResult& aRv) override {
+ if (aMode != ChannelCountModeValue()) {
+ aRv.ThrowInvalidStateError(
+ "Cannot change channel count mode of ChannelSplitterNode");
+ }
+ }
+ void SetChannelInterpretationValue(ChannelInterpretation aMode,
+ ErrorResult& aRv) override {
+ if (aMode != ChannelInterpretationValue()) {
+ aRv.ThrowInvalidStateError(
+ "Cannot change channel interpretation of ChannelSplitterNode");
+ }
+ }
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ uint16_t NumberOfOutputs() const override { return mOutputCount; }
+
+ const char* NodeType() const override { return "ChannelSplitterNode"; }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ private:
+ ChannelSplitterNode(AudioContext* aContext, uint16_t aOutputCount);
+ ~ChannelSplitterNode() = default;
+
+ const uint16_t mOutputCount;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webaudio/ConstantSourceNode.cpp b/dom/media/webaudio/ConstantSourceNode.cpp
new file mode 100644
index 0000000000..01ee570722
--- /dev/null
+++ b/dom/media/webaudio/ConstantSourceNode.cpp
@@ -0,0 +1,279 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ConstantSourceNode.h"
+
+#include "AudioDestinationNode.h"
+#include "nsContentUtils.h"
+#include "AudioNodeEngine.h"
+#include "AudioNodeTrack.h"
+#include "Tracing.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(ConstantSourceNode, AudioScheduledSourceNode,
+ mOffset)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ConstantSourceNode)
+NS_INTERFACE_MAP_END_INHERITING(AudioScheduledSourceNode)
+
+NS_IMPL_ADDREF_INHERITED(ConstantSourceNode, AudioScheduledSourceNode)
+NS_IMPL_RELEASE_INHERITED(ConstantSourceNode, AudioScheduledSourceNode)
+
+class ConstantSourceNodeEngine final : public AudioNodeEngine {
+ public:
+ ConstantSourceNodeEngine(AudioNode* aNode, AudioDestinationNode* aDestination)
+ : AudioNodeEngine(aNode),
+ mSource(nullptr),
+ mDestination(aDestination->Track()),
+ mStart(-1),
+ mStop(TRACK_TIME_MAX)
+ // Keep the default values in sync with
+ // ConstantSourceNode::ConstantSourceNode.
+ ,
+ mOffset(1.0f) {
+ MOZ_ASSERT(NS_IsMainThread());
+ }
+
+ void SetSourceTrack(AudioNodeTrack* aSource) { mSource = aSource; }
+
+ enum Parameters {
+ OFFSET,
+ START,
+ STOP,
+ };
+ void RecvTimelineEvent(uint32_t aIndex, AudioTimelineEvent& aEvent) override {
+ MOZ_ASSERT(mDestination);
+
+ WebAudioUtils::ConvertAudioTimelineEventToTicks(aEvent, mDestination);
+
+ switch (aIndex) {
+ case OFFSET:
+ mOffset.InsertEvent<int64_t>(aEvent);
+ break;
+ default:
+ NS_ERROR("Bad ConstantSourceNodeEngine TimelineParameter");
+ }
+ }
+
+ void SetTrackTimeParameter(uint32_t aIndex, TrackTime aParam) override {
+ switch (aIndex) {
+ case START:
+ mStart = aParam;
+ mSource->SetActive();
+ break;
+ case STOP:
+ mStop = aParam;
+ break;
+ default:
+ NS_ERROR("Bad ConstantSourceNodeEngine TrackTimeParameter");
+ }
+ }
+
+ void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom,
+ const AudioBlock& aInput, AudioBlock* aOutput,
+ bool* aFinished) override {
+ MOZ_ASSERT(mSource == aTrack, "Invalid source track");
+ TRACE("ConstantSourceNodeEngine::ProcessBlock");
+
+ TrackTime ticks = mDestination->GraphTimeToTrackTime(aFrom);
+ if (mStart == -1) {
+ aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
+ return;
+ }
+
+ if (ticks + WEBAUDIO_BLOCK_SIZE <= mStart || ticks >= mStop ||
+ mStop <= mStart) {
+ aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
+ } else {
+ aOutput->AllocateChannels(1);
+ float* output = aOutput->ChannelFloatsForWrite(0);
+ uint32_t writeOffset = 0;
+
+ if (ticks < mStart) {
+ MOZ_ASSERT(mStart - ticks <= WEBAUDIO_BLOCK_SIZE);
+ uint32_t count = mStart - ticks;
+ std::fill_n(output, count, 0.0f);
+ writeOffset += count;
+ }
+
+ MOZ_ASSERT(ticks + writeOffset >= mStart);
+ MOZ_ASSERT(mStop - ticks >= writeOffset);
+ uint32_t count =
+ std::min<TrackTime>(WEBAUDIO_BLOCK_SIZE, mStop - ticks) - writeOffset;
+
+ if (mOffset.HasSimpleValue()) {
+ float value = mOffset.GetValueAtTime(ticks);
+ std::fill_n(output + writeOffset, count, value);
+ } else {
+ mOffset.GetValuesAtTime(ticks + writeOffset, output + writeOffset,
+ count);
+ }
+
+ writeOffset += count;
+
+ std::fill_n(output + writeOffset, WEBAUDIO_BLOCK_SIZE - writeOffset,
+ 0.0f);
+ }
+
+ if (ticks + WEBAUDIO_BLOCK_SIZE >= mStop) {
+ // We've finished playing.
+ *aFinished = true;
+ }
+ }
+
+ bool IsActive() const override {
+ // start() has been called.
+ return mStart != -1;
+ }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override {
+ size_t amount = AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf);
+
+ // Not owned:
+ // - mSource
+ // - mDestination
+ // - mOffset (internal ref owned by node)
+
+ return amount;
+ }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ // mSource deletes the engine in its destructor.
+ AudioNodeTrack* MOZ_NON_OWNING_REF mSource;
+ RefPtr<AudioNodeTrack> mDestination;
+ TrackTime mStart;
+ TrackTime mStop;
+ AudioParamTimeline mOffset;
+};
+
+ConstantSourceNode::ConstantSourceNode(AudioContext* aContext)
+ : AudioScheduledSourceNode(aContext, 2, ChannelCountMode::Max,
+ ChannelInterpretation::Speakers),
+ mStartCalled(false) {
+ mOffset =
+ CreateAudioParam(ConstantSourceNodeEngine::OFFSET, u"offset"_ns, 1.0f);
+ ConstantSourceNodeEngine* engine =
+ new ConstantSourceNodeEngine(this, aContext->Destination());
+ mTrack = AudioNodeTrack::Create(aContext, engine,
+ AudioNodeTrack::NEED_MAIN_THREAD_ENDED,
+ aContext->Graph());
+ engine->SetSourceTrack(mTrack);
+ mTrack->AddMainThreadListener(this);
+}
+
+ConstantSourceNode::~ConstantSourceNode() = default;
+
+size_t ConstantSourceNode::SizeOfExcludingThis(
+ MallocSizeOf aMallocSizeOf) const {
+ size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf);
+
+ amount += mOffset->SizeOfIncludingThis(aMallocSizeOf);
+ return amount;
+}
+
+size_t ConstantSourceNode::SizeOfIncludingThis(
+ MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
+
+JSObject* ConstantSourceNode::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return ConstantSourceNode_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+already_AddRefed<ConstantSourceNode> ConstantSourceNode::Constructor(
+ const GlobalObject& aGlobal, AudioContext& aContext,
+ const ConstantSourceOptions& aOptions) {
+ RefPtr<ConstantSourceNode> object = new ConstantSourceNode(&aContext);
+ object->mOffset->SetInitialValue(aOptions.mOffset);
+ return object.forget();
+}
+
+void ConstantSourceNode::DestroyMediaTrack() {
+ if (mTrack) {
+ mTrack->RemoveMainThreadListener(this);
+ }
+ AudioNode::DestroyMediaTrack();
+}
+
+void ConstantSourceNode::Start(double aWhen, ErrorResult& aRv) {
+ if (!WebAudioUtils::IsTimeValid(aWhen)) {
+ aRv.ThrowRangeError<MSG_VALUE_OUT_OF_RANGE>("start time");
+ return;
+ }
+
+ if (mStartCalled) {
+ aRv.ThrowInvalidStateError("Can't call start() more than once");
+ return;
+ }
+ mStartCalled = true;
+
+ if (!mTrack) {
+ return;
+ }
+
+ mTrack->SetTrackTimeParameter(ConstantSourceNodeEngine::START, Context(),
+ aWhen);
+
+ MarkActive();
+ Context()->StartBlockedAudioContextIfAllowed();
+}
+
+void ConstantSourceNode::Stop(double aWhen, ErrorResult& aRv) {
+ if (!WebAudioUtils::IsTimeValid(aWhen)) {
+ aRv.ThrowRangeError<MSG_VALUE_OUT_OF_RANGE>("stop time");
+ return;
+ }
+
+ if (!mStartCalled) {
+ aRv.ThrowInvalidStateError("Can't call stop() without calling start()");
+ return;
+ }
+
+ if (!mTrack || !Context()) {
+ return;
+ }
+
+ mTrack->SetTrackTimeParameter(ConstantSourceNodeEngine::STOP, Context(),
+ std::max(0.0, aWhen));
+}
+
+void ConstantSourceNode::NotifyMainThreadTrackEnded() {
+ MOZ_ASSERT(mTrack->IsEnded());
+
+ class EndedEventDispatcher final : public Runnable {
+ public:
+ explicit EndedEventDispatcher(ConstantSourceNode* aNode)
+ : mozilla::Runnable("EndedEventDispatcher"), mNode(aNode) {}
+ NS_IMETHOD Run() override {
+ // If it's not safe to run scripts right now, schedule this to run later
+ if (!nsContentUtils::IsSafeToRunScript()) {
+ nsContentUtils::AddScriptRunner(this);
+ return NS_OK;
+ }
+
+ mNode->DispatchTrustedEvent(u"ended"_ns);
+ // Release track resources.
+ mNode->DestroyMediaTrack();
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<ConstantSourceNode> mNode;
+ };
+
+ Context()->Dispatch(do_AddRef(new EndedEventDispatcher(this)));
+
+ // Drop the playing reference
+ // Warning: The below line might delete this.
+ MarkInactive();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/ConstantSourceNode.h b/dom/media/webaudio/ConstantSourceNode.h
new file mode 100644
index 0000000000..61f939a7e6
--- /dev/null
+++ b/dom/media/webaudio/ConstantSourceNode.h
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef ConstantSourceNode_h_
+#define ConstantSourceNode_h_
+
+#include "AudioScheduledSourceNode.h"
+#include "AudioParam.h"
+#include "mozilla/dom/ConstantSourceNodeBinding.h"
+
+namespace mozilla::dom {
+
+class AudioContext;
+
+class ConstantSourceNode final : public AudioScheduledSourceNode,
+ public MainThreadMediaTrackListener {
+ public:
+ explicit ConstantSourceNode(AudioContext* aContext);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ConstantSourceNode,
+ AudioScheduledSourceNode)
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ static already_AddRefed<ConstantSourceNode> Constructor(
+ const GlobalObject& aGlobal, AudioContext& aContext,
+ const ConstantSourceOptions& aOptions);
+
+ void DestroyMediaTrack() override;
+
+ uint16_t NumberOfInputs() const final { return 0; }
+
+ AudioParam* Offset() const { return mOffset; }
+
+ void Start(double aWhen, ErrorResult& rv) override;
+ void Stop(double aWhen, ErrorResult& rv) override;
+
+ void NotifyMainThreadTrackEnded() override;
+
+ const char* NodeType() const override { return "ConstantSourceNode"; }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override;
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override;
+
+ protected:
+ virtual ~ConstantSourceNode();
+
+ private:
+ RefPtr<AudioParam> mOffset;
+ bool mStartCalled;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webaudio/ConvolverNode.cpp b/dom/media/webaudio/ConvolverNode.cpp
new file mode 100644
index 0000000000..65562ae6d0
--- /dev/null
+++ b/dom/media/webaudio/ConvolverNode.cpp
@@ -0,0 +1,479 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ConvolverNode.h"
+#include "mozilla/dom/ConvolverNodeBinding.h"
+#include "AlignmentUtils.h"
+#include "AudioNodeEngine.h"
+#include "AudioNodeTrack.h"
+#include "blink/Reverb.h"
+#include "PlayingRefChangeHandler.h"
+#include "Tracing.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(ConvolverNode, AudioNode, mBuffer)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ConvolverNode)
+NS_INTERFACE_MAP_END_INHERITING(AudioNode)
+
+NS_IMPL_ADDREF_INHERITED(ConvolverNode, AudioNode)
+NS_IMPL_RELEASE_INHERITED(ConvolverNode, AudioNode)
+
+class ConvolverNodeEngine final : public AudioNodeEngine {
+ typedef PlayingRefChangeHandler PlayingRefChanged;
+
+ public:
+ ConvolverNodeEngine(AudioNode* aNode, bool aNormalize)
+ : AudioNodeEngine(aNode) {}
+
+ // Indicates how the right output channel is generated.
+ enum class RightConvolverMode {
+ // A right convolver is always used when there is more than one impulse
+ // response channel.
+ Always,
+ // With a single response channel, the mode may be either Direct or
+ // Difference. The decision on which to use is made when stereo input is
+ // received. Once the right convolver is in use, convolver state is
+ // suitable only for the selected mode, and so the mode cannot change
+ // until the right convolver contains only silent history.
+ //
+ // With Direct mode, each convolver processes a corresponding channel.
+ // This mode is selected when input is initially stereo or
+ // channelInterpretation is "discrete" at the time or starting the right
+ // convolver when input changes from non-silent mono to stereo.
+ Direct,
+ // Difference mode is selected if channelInterpretation is "speakers" at
+ // the time starting the right convolver when the input changes from mono
+ // to stereo.
+ //
+ // When non-silent input is initially mono, with a single response
+ // channel, the right output channel is not produced until input becomes
+ // stereo. Only a single convolver is used for mono processing. When
+ // stereo input arrives after mono input, output must be as if the mono
+ // signal remaining in the left convolver is up-mixed, but the right
+ // convolver has not been initialized with the history of the mono input.
+ // Copying the state of the left convolver into the right convolver is not
+ // desirable, because there is considerable state to copy, and the
+ // different convolvers are intended to process out of phase, which means
+ // that state from one convolver would not directly map to state in
+ // another convolver.
+ //
+ // Instead the distributive property of convolution is used to generate
+ // the right output channel using information in the left output channel.
+ // Using l and r to denote the left and right channel input signals, g the
+ // impulse response, and * convolution, the convolution of the right
+ // channel can be given by
+ //
+ // r * g = (l + (r - l)) * g
+ // = l * g + (r - l) * g
+ //
+ // The left convolver continues to process the left channel l to produce
+ // l * g. The right convolver processes the difference of input channel
+ // signals r - l to produce (r - l) * g. The outputs of the two
+ // convolvers are added to generate the right channel output r * g.
+ //
+ // The benefit of doing this is that the history of the r - l input for a
+ // "speakers" up-mixed mono signal is zero, and so an empty convolver
+ // already has exactly the right history for mixing the previous mono
+ // signal with the new stereo signal.
+ Difference
+ };
+
+ void SetReverb(WebCore::Reverb* aReverb,
+ uint32_t aImpulseChannelCount) override {
+ mRemainingLeftOutput = INT32_MIN;
+ mRemainingRightOutput = 0;
+ mRemainingRightHistory = 0;
+
+ // Assume for now that convolution of channel difference is not required.
+ // Direct may change to Difference during processing.
+ if (aReverb) {
+ mRightConvolverMode = aImpulseChannelCount == 1
+ ? RightConvolverMode::Direct
+ : RightConvolverMode::Always;
+ } else {
+ mRightConvolverMode = RightConvolverMode::Always;
+ }
+
+ mReverb.reset(aReverb);
+ }
+
+ void AllocateReverbInput(const AudioBlock& aInput,
+ uint32_t aTotalChannelCount) {
+ uint32_t inputChannelCount = aInput.ChannelCount();
+ MOZ_ASSERT(inputChannelCount <= aTotalChannelCount);
+ mReverbInput.AllocateChannels(aTotalChannelCount);
+ // Pre-multiply the input's volume
+ for (uint32_t i = 0; i < inputChannelCount; ++i) {
+ const float* src = static_cast<const float*>(aInput.mChannelData[i]);
+ float* dest = mReverbInput.ChannelFloatsForWrite(i);
+ AudioBlockCopyChannelWithScale(src, aInput.mVolume, dest);
+ }
+ // Fill remaining channels with silence
+ for (uint32_t i = inputChannelCount; i < aTotalChannelCount; ++i) {
+ float* dest = mReverbInput.ChannelFloatsForWrite(i);
+ std::fill_n(dest, WEBAUDIO_BLOCK_SIZE, 0.0f);
+ }
+ }
+
+ void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom,
+ const AudioBlock& aInput, AudioBlock* aOutput,
+ bool* aFinished) override;
+
+ bool IsActive() const override { return mRemainingLeftOutput != INT32_MIN; }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override {
+ size_t amount = AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf);
+
+ amount += mReverbInput.SizeOfExcludingThis(aMallocSizeOf, false);
+
+ if (mReverb) {
+ amount += mReverb->sizeOfIncludingThis(aMallocSizeOf);
+ }
+
+ return amount;
+ }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ private:
+ // Keeping mReverbInput across process calls avoids unnecessary reallocation.
+ AudioBlock mReverbInput;
+ UniquePtr<WebCore::Reverb> mReverb;
+ // Tracks samples of the tail remaining to be output. INT32_MIN is a
+ // special value to indicate that the end of any previous tail has been
+ // handled.
+ int32_t mRemainingLeftOutput = INT32_MIN;
+ // mRemainingRightOutput and mRemainingRightHistory are only used when
+ // mRightOutputMode != Always. There is no special handling required at the
+ // end of tail times and so INT32_MIN is not used.
+ // mRemainingRightOutput tracks how much longer this node needs to continue
+ // to produce a right output channel.
+ int32_t mRemainingRightOutput = 0;
+ // mRemainingRightHistory tracks how much silent input would be required to
+ // drain the right convolver, which may sometimes be longer than the period
+ // a right output channel is required.
+ int32_t mRemainingRightHistory = 0;
+ RightConvolverMode mRightConvolverMode = RightConvolverMode::Always;
+};
+
+static void AddScaledLeftToRight(AudioBlock* aBlock, float aScale) {
+ const float* left = static_cast<const float*>(aBlock->mChannelData[0]);
+ float* right = aBlock->ChannelFloatsForWrite(1);
+ AudioBlockAddChannelWithScale(left, aScale, right);
+}
+
+void ConvolverNodeEngine::ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom,
+ const AudioBlock& aInput,
+ AudioBlock* aOutput, bool* aFinished) {
+ TRACE("ConvolverNodeEngine::ProcessBlock");
+ if (!mReverb) {
+ aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
+ return;
+ }
+
+ uint32_t inputChannelCount = aInput.ChannelCount();
+ if (aInput.IsNull()) {
+ if (mRemainingLeftOutput > 0) {
+ mRemainingLeftOutput -= WEBAUDIO_BLOCK_SIZE;
+ AllocateReverbInput(aInput, 1); // floats for silence
+ } else {
+ if (mRemainingLeftOutput != INT32_MIN) {
+ mRemainingLeftOutput = INT32_MIN;
+ MOZ_ASSERT(mRemainingRightOutput <= 0);
+ MOZ_ASSERT(mRemainingRightHistory <= 0);
+ aTrack->ScheduleCheckForInactive();
+ RefPtr<PlayingRefChanged> refchanged =
+ new PlayingRefChanged(aTrack, PlayingRefChanged::RELEASE);
+ aTrack->Graph()->DispatchToMainThreadStableState(refchanged.forget());
+ }
+ aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
+ return;
+ }
+ } else {
+ if (mRemainingLeftOutput <= 0) {
+ RefPtr<PlayingRefChanged> refchanged =
+ new PlayingRefChanged(aTrack, PlayingRefChanged::ADDREF);
+ aTrack->Graph()->DispatchToMainThreadStableState(refchanged.forget());
+ }
+
+ // Use mVolume as a flag to detect whether AllocateReverbInput() gets
+ // called.
+ mReverbInput.mVolume = 0.0f;
+
+ // Special handling of input channel count changes is used when there is
+ // only a single impulse response channel. See RightConvolverMode.
+ if (mRightConvolverMode != RightConvolverMode::Always) {
+ ChannelInterpretation channelInterpretation =
+ aTrack->GetChannelInterpretation();
+ if (inputChannelCount == 2) {
+ if (mRemainingRightHistory <= 0) {
+ // Will start the second convolver. Choose to convolve the right
+ // channel directly if there is no left tail to up-mix or up-mixing
+ // is "discrete".
+ mRightConvolverMode =
+ (mRemainingLeftOutput <= 0 ||
+ channelInterpretation == ChannelInterpretation::Discrete)
+ ? RightConvolverMode::Direct
+ : RightConvolverMode::Difference;
+ }
+ // The extra WEBAUDIO_BLOCK_SIZE is subtracted below.
+ mRemainingRightOutput =
+ mReverb->impulseResponseLength() + WEBAUDIO_BLOCK_SIZE;
+ mRemainingRightHistory = mRemainingRightOutput;
+ if (mRightConvolverMode == RightConvolverMode::Difference) {
+ AllocateReverbInput(aInput, 2);
+ // Subtract left from right.
+ AddScaledLeftToRight(&mReverbInput, -1.0f);
+ }
+ } else if (mRemainingRightHistory > 0) {
+ // There is one channel of input, but a second convolver also
+ // requires input. Up-mix appropriately for the second convolver.
+ if ((mRightConvolverMode == RightConvolverMode::Difference) ^
+ (channelInterpretation == ChannelInterpretation::Discrete)) {
+ MOZ_ASSERT(
+ (mRightConvolverMode == RightConvolverMode::Difference &&
+ channelInterpretation == ChannelInterpretation::Speakers) ||
+ (mRightConvolverMode == RightConvolverMode::Direct &&
+ channelInterpretation == ChannelInterpretation::Discrete));
+ // The state is one of the following combinations:
+ // 1) Difference and speakers.
+ // Up-mixing gives r = l.
+ // The input to the second convolver is r - l.
+ // 2) Direct and discrete.
+ // Up-mixing gives r = 0.
+ // The input to the second convolver is r.
+ //
+ // In each case the input for the second convolver is silence, which
+ // will drain the convolver.
+ AllocateReverbInput(aInput, 2);
+ } else {
+ if (channelInterpretation == ChannelInterpretation::Discrete) {
+ MOZ_ASSERT(mRightConvolverMode == RightConvolverMode::Difference);
+ // channelInterpretation has changed since the second convolver
+ // was added. "discrete" up-mixing of input would produce a
+ // silent right channel r = 0, but the second convolver needs
+ // r - l for RightConvolverMode::Difference.
+ AllocateReverbInput(aInput, 2);
+ AddScaledLeftToRight(&mReverbInput, -1.0f);
+ } else {
+ MOZ_ASSERT(channelInterpretation ==
+ ChannelInterpretation::Speakers);
+ MOZ_ASSERT(mRightConvolverMode == RightConvolverMode::Direct);
+ // The Reverb will essentially up-mix the single input channel by
+ // feeding it into both convolvers.
+ }
+ // The second convolver does not have silent input, and so it will
+ // not drain. It will need to continue processing up-mixed input
+ // because the next input block may be stereo, which would be mixed
+ // with the signal remaining in the convolvers.
+ // The extra WEBAUDIO_BLOCK_SIZE is subtracted below.
+ mRemainingRightHistory =
+ mReverb->impulseResponseLength() + WEBAUDIO_BLOCK_SIZE;
+ }
+ }
+ }
+
+ if (mReverbInput.mVolume == 0.0f) { // not yet set
+ if (aInput.mVolume != 1.0f) {
+ AllocateReverbInput(aInput, inputChannelCount); // pre-multiply
+ } else {
+ mReverbInput = aInput;
+ }
+ }
+
+ mRemainingLeftOutput = mReverb->impulseResponseLength();
+ MOZ_ASSERT(mRemainingLeftOutput > 0);
+ }
+
+ // "The ConvolverNode produces a mono output only in the single case where
+ // there is a single input channel and a single-channel buffer."
+ uint32_t outputChannelCount = 2;
+ uint32_t reverbOutputChannelCount = 2;
+ if (mRightConvolverMode != RightConvolverMode::Always) {
+ // When the input changes from stereo to mono, the output continues to be
+ // stereo for the length of the tail time, during which the two channels
+ // may differ.
+ if (mRemainingRightOutput > 0) {
+ MOZ_ASSERT(mRemainingRightHistory > 0);
+ mRemainingRightOutput -= WEBAUDIO_BLOCK_SIZE;
+ } else {
+ outputChannelCount = 1;
+ }
+ // The second convolver keeps processing until it drains.
+ if (mRemainingRightHistory > 0) {
+ mRemainingRightHistory -= WEBAUDIO_BLOCK_SIZE;
+ } else {
+ reverbOutputChannelCount = 1;
+ }
+ }
+
+ // If there are two convolvers, then they each need an output buffer, even
+ // if the second convolver is only processing to keep history of up-mixed
+ // input.
+ aOutput->AllocateChannels(reverbOutputChannelCount);
+
+ mReverb->process(&mReverbInput, aOutput);
+
+ if (mRightConvolverMode == RightConvolverMode::Difference &&
+ outputChannelCount == 2) {
+ // Add left to right.
+ AddScaledLeftToRight(aOutput, 1.0f);
+ } else {
+ // Trim if outputChannelCount < reverbOutputChannelCount
+ aOutput->mChannelData.TruncateLength(outputChannelCount);
+ }
+}
+
+ConvolverNode::ConvolverNode(AudioContext* aContext)
+ : AudioNode(aContext, 2, ChannelCountMode::Clamped_max,
+ ChannelInterpretation::Speakers),
+ mNormalize(true) {
+ ConvolverNodeEngine* engine = new ConvolverNodeEngine(this, mNormalize);
+ mTrack = AudioNodeTrack::Create(
+ aContext, engine, AudioNodeTrack::NO_TRACK_FLAGS, aContext->Graph());
+}
+
+/* static */
+already_AddRefed<ConvolverNode> ConvolverNode::Create(
+ JSContext* aCx, AudioContext& aAudioContext,
+ const ConvolverOptions& aOptions, ErrorResult& aRv) {
+ RefPtr<ConvolverNode> audioNode = new ConvolverNode(&aAudioContext);
+
+ audioNode->Initialize(aOptions, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ // This must be done before setting the buffer.
+ audioNode->SetNormalize(!aOptions.mDisableNormalization);
+
+ if (aOptions.mBuffer.WasPassed()) {
+ MOZ_ASSERT(aCx);
+ audioNode->SetBuffer(aCx, aOptions.mBuffer.Value(), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+ }
+
+ return audioNode.forget();
+}
+
+size_t ConvolverNode::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf);
+ if (mBuffer) {
+ // NB: mBuffer might be shared with the associated engine, by convention
+ // the AudioNode will report.
+ amount += mBuffer->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ return amount;
+}
+
+size_t ConvolverNode::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
+
+JSObject* ConvolverNode::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return ConvolverNode_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void ConvolverNode::SetBuffer(JSContext* aCx, AudioBuffer* aBuffer,
+ ErrorResult& aRv) {
+ if (aBuffer) {
+ switch (aBuffer->NumberOfChannels()) {
+ case 1:
+ case 2:
+ case 4:
+ // Supported number of channels
+ break;
+ default:
+ aRv.ThrowNotSupportedError(
+ nsPrintfCString("%u is not a supported number of channels",
+ aBuffer->NumberOfChannels()));
+ return;
+ }
+ }
+
+ if (aBuffer && (aBuffer->SampleRate() != Context()->SampleRate())) {
+ aRv.ThrowNotSupportedError(nsPrintfCString(
+ "Buffer sample rate (%g) does not match AudioContext sample rate (%g)",
+ aBuffer->SampleRate(), Context()->SampleRate()));
+ return;
+ }
+
+ // Send the buffer to the track
+ AudioNodeTrack* ns = mTrack;
+ MOZ_ASSERT(ns, "Why don't we have a track here?");
+ if (aBuffer) {
+ AudioChunk data = aBuffer->GetThreadSharedChannelsForRate(aCx);
+ if (data.mBufferFormat == AUDIO_FORMAT_S16) {
+ // Reverb expects data in float format.
+ // Convert on the main thread so as to minimize allocations on the audio
+ // thread.
+ // Reverb will dispose of the buffer once initialized, so convert here
+ // and leave the smaller arrays in the AudioBuffer.
+ // There is currently no value in providing 16/32-byte aligned data
+ // because PadAndMakeScaledDFT() will copy the data (without SIMD
+ // instructions) to aligned arrays for the FFT.
+ CheckedInt<size_t> bufferSize(sizeof(float));
+ bufferSize *= data.mDuration;
+ bufferSize *= data.ChannelCount();
+ RefPtr<SharedBuffer> floatBuffer =
+ SharedBuffer::Create(bufferSize, fallible);
+ if (!floatBuffer) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+ auto floatData = static_cast<float*>(floatBuffer->Data());
+ for (size_t i = 0; i < data.ChannelCount(); ++i) {
+ ConvertAudioSamples(data.ChannelData<int16_t>()[i], floatData,
+ data.mDuration);
+ data.mChannelData[i] = floatData;
+ floatData += data.mDuration;
+ }
+ data.mBuffer = std::move(floatBuffer);
+ data.mBufferFormat = AUDIO_FORMAT_FLOAT32;
+ } else if (data.mBufferFormat == AUDIO_FORMAT_SILENCE) {
+ // This is valid, but a signal convolved by a silent signal is silent, set
+ // the reverb to nullptr and return.
+ ns->SetReverb(nullptr, 0);
+ mBuffer = aBuffer;
+ return;
+ }
+
+ // Note about empirical tuning (this is copied from Blink)
+ // The maximum FFT size affects reverb performance and accuracy.
+ // If the reverb is single-threaded and processes entirely in the real-time
+ // audio thread, it's important not to make this too high. In this case
+ // 8192 is a good value. But, the Reverb object is multi-threaded, so we
+ // want this as high as possible without losing too much accuracy. Very
+ // large FFTs will have worse phase errors. Given these constraints 32768 is
+ // a good compromise.
+ const size_t MaxFFTSize = 32768;
+
+ bool allocationFailure = false;
+ UniquePtr<WebCore::Reverb> reverb(new WebCore::Reverb(
+ data, MaxFFTSize, !Context()->IsOffline(), mNormalize,
+ aBuffer->SampleRate(), &allocationFailure));
+ if (!allocationFailure) {
+ ns->SetReverb(reverb.release(), data.ChannelCount());
+ } else {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+ } else {
+ ns->SetReverb(nullptr, 0);
+ }
+ mBuffer = aBuffer;
+}
+
+void ConvolverNode::SetNormalize(bool aNormalize) { mNormalize = aNormalize; }
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/ConvolverNode.h b/dom/media/webaudio/ConvolverNode.h
new file mode 100644
index 0000000000..897f8d6547
--- /dev/null
+++ b/dom/media/webaudio/ConvolverNode.h
@@ -0,0 +1,77 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef ConvolverNode_h_
+#define ConvolverNode_h_
+
+#include "AudioNode.h"
+#include "AudioBuffer.h"
+#include "nsPrintfCString.h"
+
+namespace mozilla::dom {
+
+class AudioContext;
+struct ConvolverOptions;
+
+class ConvolverNode final : public AudioNode {
+ public:
+ static already_AddRefed<ConvolverNode> Create(
+ JSContext* aCx, AudioContext& aAudioContext,
+ const ConvolverOptions& aOptions, ErrorResult& aRv);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ConvolverNode, AudioNode);
+
+ static already_AddRefed<ConvolverNode> Constructor(
+ const GlobalObject& aGlobal, AudioContext& aAudioContext,
+ const ConvolverOptions& aOptions, ErrorResult& aRv) {
+ return Create(aGlobal.Context(), aAudioContext, aOptions, aRv);
+ }
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ AudioBuffer* GetBuffer(JSContext* aCx) const { return mBuffer; }
+
+ void SetBuffer(JSContext* aCx, AudioBuffer* aBuffer, ErrorResult& aRv);
+
+ bool Normalize() const { return mNormalize; }
+
+ void SetNormalize(bool aNormal);
+
+ void SetChannelCount(uint32_t aChannelCount, ErrorResult& aRv) override {
+ if (aChannelCount > 2) {
+ aRv.ThrowNotSupportedError(
+ nsPrintfCString("%u is greater than 2", aChannelCount));
+ return;
+ }
+ AudioNode::SetChannelCount(aChannelCount, aRv);
+ }
+ void SetChannelCountModeValue(ChannelCountMode aMode,
+ ErrorResult& aRv) override {
+ if (aMode == ChannelCountMode::Max) {
+ aRv.ThrowNotSupportedError("Cannot set channel count mode to \"max\"");
+ return;
+ }
+ AudioNode::SetChannelCountModeValue(aMode, aRv);
+ }
+
+ const char* NodeType() const override { return "ConvolverNode"; }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override;
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override;
+
+ private:
+ explicit ConvolverNode(AudioContext* aContext);
+ ~ConvolverNode() = default;
+
+ RefPtr<AudioBuffer> mBuffer;
+ bool mNormalize;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webaudio/DelayBuffer.cpp b/dom/media/webaudio/DelayBuffer.cpp
new file mode 100644
index 0000000000..cab2920080
--- /dev/null
+++ b/dom/media/webaudio/DelayBuffer.cpp
@@ -0,0 +1,235 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DelayBuffer.h"
+
+#include "mozilla/PodOperations.h"
+#include "AudioChannelFormat.h"
+#include "AudioNodeEngine.h"
+
+namespace mozilla {
+
+size_t DelayBuffer::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t amount = 0;
+ amount += mChunks.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (size_t i = 0; i < mChunks.Length(); i++) {
+ amount += mChunks[i].SizeOfExcludingThis(aMallocSizeOf, false);
+ }
+
+ amount += mUpmixChannels.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ return amount;
+}
+
+void DelayBuffer::Write(const AudioBlock& aInputChunk) {
+ // We must have a reference to the buffer if there are channels
+ MOZ_ASSERT(aInputChunk.IsNull() == !aInputChunk.ChannelCount());
+#ifdef DEBUG
+ MOZ_ASSERT(!mHaveWrittenBlock);
+ mHaveWrittenBlock = true;
+#endif
+
+ if (!EnsureBuffer()) {
+ return;
+ }
+
+ if (mCurrentChunk == mLastReadChunk) {
+ mLastReadChunk = -1; // invalidate cache
+ }
+ mChunks[mCurrentChunk] = aInputChunk.AsAudioChunk();
+}
+
+void DelayBuffer::Read(const float aPerFrameDelays[WEBAUDIO_BLOCK_SIZE],
+ AudioBlock* aOutputChunk,
+ ChannelInterpretation aChannelInterpretation) {
+ int chunkCount = mChunks.Length();
+ if (!chunkCount) {
+ aOutputChunk->SetNull(WEBAUDIO_BLOCK_SIZE);
+ return;
+ }
+
+ // Find the maximum number of contributing channels to determine the output
+ // channel count that retains all signal information. Buffered blocks will
+ // be upmixed if necessary.
+ //
+ // First find the range of "delay" offsets backwards from the current
+ // position. Note that these may be negative for frames that are after the
+ // current position (including i).
+ float minDelay = aPerFrameDelays[0];
+ float maxDelay = minDelay;
+ for (unsigned i = 1; i < WEBAUDIO_BLOCK_SIZE; ++i) {
+ minDelay = std::min(minDelay, aPerFrameDelays[i] - i);
+ maxDelay = std::max(maxDelay, aPerFrameDelays[i] - i);
+ }
+
+ // Now find the chunks touched by this range and check their channel counts.
+ int oldestChunk = ChunkForDelay(std::ceil(maxDelay));
+ int youngestChunk = ChunkForDelay(std::floor(minDelay));
+
+ uint32_t channelCount = 0;
+ for (int i = oldestChunk; true; i = (i + 1) % chunkCount) {
+ channelCount =
+ GetAudioChannelsSuperset(channelCount, mChunks[i].ChannelCount());
+ if (i == youngestChunk) {
+ break;
+ }
+ }
+
+ if (channelCount) {
+ aOutputChunk->AllocateChannels(channelCount);
+ ReadChannels(aPerFrameDelays, aOutputChunk, 0, channelCount,
+ aChannelInterpretation);
+ } else {
+ aOutputChunk->SetNull(WEBAUDIO_BLOCK_SIZE);
+ }
+}
+
+void DelayBuffer::ReadChannel(const float aPerFrameDelays[WEBAUDIO_BLOCK_SIZE],
+ AudioBlock* aOutputChunk, uint32_t aChannel,
+ ChannelInterpretation aChannelInterpretation) {
+ if (!mChunks.Length()) {
+ float* outputChannel = aOutputChunk->ChannelFloatsForWrite(aChannel);
+ PodZero(outputChannel, WEBAUDIO_BLOCK_SIZE);
+ return;
+ }
+
+ ReadChannels(aPerFrameDelays, aOutputChunk, aChannel, 1,
+ aChannelInterpretation);
+}
+
+void DelayBuffer::ReadChannels(const float aPerFrameDelays[WEBAUDIO_BLOCK_SIZE],
+ AudioBlock* aOutputChunk, uint32_t aFirstChannel,
+ uint32_t aNumChannelsToRead,
+ ChannelInterpretation aChannelInterpretation) {
+ uint32_t totalChannelCount = aOutputChunk->ChannelCount();
+ uint32_t readChannelsEnd = aFirstChannel + aNumChannelsToRead;
+ MOZ_ASSERT(readChannelsEnd <= totalChannelCount);
+
+ if (mUpmixChannels.Length() != totalChannelCount) {
+ mLastReadChunk = -1; // invalidate cache
+ }
+
+ for (uint32_t channel = aFirstChannel; channel < readChannelsEnd; ++channel) {
+ PodZero(aOutputChunk->ChannelFloatsForWrite(channel), WEBAUDIO_BLOCK_SIZE);
+ }
+
+ for (unsigned i = 0; i < WEBAUDIO_BLOCK_SIZE; ++i) {
+ float currentDelay = aPerFrameDelays[i];
+ MOZ_ASSERT(currentDelay >= 0.0f);
+ MOZ_ASSERT(currentDelay <= (mChunks.Length() - 1) * WEBAUDIO_BLOCK_SIZE);
+
+ // Interpolate two input frames in case the read position does not match
+ // an integer index.
+ // Use the larger delay, for the older frame, first, as this is more
+ // likely to use the cached upmixed channel arrays.
+ int floorDelay = int(currentDelay);
+ float interpolationFactor = currentDelay - floorDelay;
+ int positions[2];
+ positions[1] = PositionForDelay(floorDelay) + i;
+ positions[0] = positions[1] - 1;
+
+ for (unsigned tick = 0; tick < ArrayLength(positions); ++tick) {
+ int readChunk = ChunkForPosition(positions[tick]);
+ // The zero check on interpolationFactor is important because, when
+ // currentDelay is integer, positions[0] may be outside the range
+ // considered for determining totalChannelCount.
+ // mVolume is not set on default initialized chunks so also handle null
+ // chunks specially.
+ if (interpolationFactor != 0.0f && !mChunks[readChunk].IsNull()) {
+ int readOffset = OffsetForPosition(positions[tick]);
+ UpdateUpmixChannels(readChunk, totalChannelCount,
+ aChannelInterpretation);
+ float multiplier = interpolationFactor * mChunks[readChunk].mVolume;
+ for (uint32_t channel = aFirstChannel; channel < readChannelsEnd;
+ ++channel) {
+ aOutputChunk->ChannelFloatsForWrite(channel)[i] +=
+ multiplier * mUpmixChannels[channel][readOffset];
+ }
+ }
+
+ interpolationFactor = 1.0f - interpolationFactor;
+ }
+ }
+}
+
+void DelayBuffer::Read(float aDelayTicks, AudioBlock* aOutputChunk,
+ ChannelInterpretation aChannelInterpretation) {
+ float computedDelay[WEBAUDIO_BLOCK_SIZE];
+
+ for (unsigned i = 0; i < WEBAUDIO_BLOCK_SIZE; ++i) {
+ computedDelay[i] = aDelayTicks;
+ }
+
+ Read(computedDelay, aOutputChunk, aChannelInterpretation);
+}
+
+bool DelayBuffer::EnsureBuffer() {
+ if (mChunks.Length() == 0) {
+ // The length of the buffer is at least one block greater than the maximum
+ // delay so that writing an input block does not overwrite the block that
+ // would subsequently be read at maximum delay. Also round up to the next
+ // block size, so that no block of writes will need to wrap.
+ const int chunkCount = (mMaxDelayTicks + 2 * WEBAUDIO_BLOCK_SIZE - 1) >>
+ WEBAUDIO_BLOCK_SIZE_BITS;
+ if (!mChunks.SetLength(chunkCount, fallible)) {
+ return false;
+ }
+
+ mLastReadChunk = -1;
+ }
+ return true;
+}
+
+int DelayBuffer::PositionForDelay(int aDelay) {
+ // Adding mChunks.Length() keeps integers positive for defined and
+ // appropriate bitshift, remainder, and bitwise operations.
+ return ((mCurrentChunk + mChunks.Length()) * WEBAUDIO_BLOCK_SIZE) - aDelay;
+}
+
+int DelayBuffer::ChunkForPosition(int aPosition) {
+ MOZ_ASSERT(aPosition >= 0);
+ return (aPosition >> WEBAUDIO_BLOCK_SIZE_BITS) % mChunks.Length();
+}
+
+int DelayBuffer::OffsetForPosition(int aPosition) {
+ MOZ_ASSERT(aPosition >= 0);
+ return aPosition & (WEBAUDIO_BLOCK_SIZE - 1);
+}
+
+int DelayBuffer::ChunkForDelay(int aDelay) {
+ return ChunkForPosition(PositionForDelay(aDelay));
+}
+
+void DelayBuffer::UpdateUpmixChannels(
+ int aNewReadChunk, uint32_t aChannelCount,
+ ChannelInterpretation aChannelInterpretation) {
+ if (aNewReadChunk == mLastReadChunk) {
+ MOZ_ASSERT(mUpmixChannels.Length() == aChannelCount);
+ return;
+ }
+
+ NS_WARNING_ASSERTION(mHaveWrittenBlock || aNewReadChunk != mCurrentChunk,
+ "Smoothing is making feedback delay too small.");
+
+ mLastReadChunk = aNewReadChunk;
+ mUpmixChannels = mChunks[aNewReadChunk].ChannelData<float>().Clone();
+ MOZ_ASSERT(mUpmixChannels.Length() <= aChannelCount);
+ if (mUpmixChannels.Length() < aChannelCount) {
+ if (aChannelInterpretation == ChannelInterpretation::Speakers) {
+ AudioChannelsUpMix(&mUpmixChannels, aChannelCount,
+ SilentChannel::ZeroChannel<float>());
+ MOZ_ASSERT(mUpmixChannels.Length() == aChannelCount,
+ "We called GetAudioChannelsSuperset to avoid this");
+ } else {
+ // Fill up the remaining channels with zeros
+ for (uint32_t channel = mUpmixChannels.Length(); channel < aChannelCount;
+ ++channel) {
+ mUpmixChannels.AppendElement(SilentChannel::ZeroChannel<float>());
+ }
+ }
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/webaudio/DelayBuffer.h b/dom/media/webaudio/DelayBuffer.h
new file mode 100644
index 0000000000..5f238fde03
--- /dev/null
+++ b/dom/media/webaudio/DelayBuffer.h
@@ -0,0 +1,105 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DelayBuffer_h_
+#define DelayBuffer_h_
+
+#include "nsTArray.h"
+#include "AudioBlock.h"
+#include "AudioSegment.h"
+#include "mozilla/dom/AudioNodeBinding.h" // for ChannelInterpretation
+
+namespace mozilla {
+
+class DelayBuffer final {
+ typedef dom::ChannelInterpretation ChannelInterpretation;
+
+ public:
+ explicit DelayBuffer(float aMaxDelayTicks)
+ // Round the maximum delay up to the next tick.
+ : mMaxDelayTicks(std::ceil(aMaxDelayTicks)),
+ mCurrentChunk(0)
+ // mLastReadChunk is initialized in EnsureBuffer
+#ifdef DEBUG
+ ,
+ mHaveWrittenBlock(false)
+#endif
+ {
+ // The 180 second limit in AudioContext::CreateDelay() and the
+ // 1 << MEDIA_TIME_FRAC_BITS limit on sample rate provide a limit on the
+ // maximum delay.
+ MOZ_ASSERT(aMaxDelayTicks <=
+ float(std::numeric_limits<decltype(mMaxDelayTicks)>::max()));
+ }
+
+ // Write a WEBAUDIO_BLOCK_SIZE block for aChannelCount channels.
+ void Write(const AudioBlock& aInputChunk);
+
+ // Read a block with an array of delays, in ticks, for each sample frame.
+ // Each delay should be >= 0 and <= MaxDelayTicks().
+ void Read(const float aPerFrameDelays[WEBAUDIO_BLOCK_SIZE],
+ AudioBlock* aOutputChunk,
+ ChannelInterpretation aChannelInterpretation);
+ // Read a block with a constant delay. The delay should be >= 0 and
+ // <= MaxDelayTicks().
+ void Read(float aDelayTicks, AudioBlock* aOutputChunk,
+ ChannelInterpretation aChannelInterpretation);
+
+ // Read into one of the channels of aOutputChunk, given an array of
+ // delays in ticks. This is useful when delays are different on different
+ // channels. aOutputChunk must have already been allocated with at least as
+ // many channels as were in any of the blocks passed to Write().
+ void ReadChannel(const float aPerFrameDelays[WEBAUDIO_BLOCK_SIZE],
+ AudioBlock* aOutputChunk, uint32_t aChannel,
+ ChannelInterpretation aChannelInterpretation);
+
+ // Advance the buffer pointer
+ void NextBlock() {
+ mCurrentChunk = (mCurrentChunk + 1) % mChunks.Length();
+#ifdef DEBUG
+ MOZ_ASSERT(mHaveWrittenBlock);
+ mHaveWrittenBlock = false;
+#endif
+ }
+
+ void Reset() { mChunks.Clear(); };
+
+ int MaxDelayTicks() const { return mMaxDelayTicks; }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const;
+
+ private:
+ void ReadChannels(const float aPerFrameDelays[WEBAUDIO_BLOCK_SIZE],
+ AudioBlock* aOutputChunk, uint32_t aFirstChannel,
+ uint32_t aNumChannelsToRead,
+ ChannelInterpretation aChannelInterpretation);
+ bool EnsureBuffer();
+ int PositionForDelay(int aDelay);
+ int ChunkForPosition(int aPosition);
+ int OffsetForPosition(int aPosition);
+ int ChunkForDelay(int aDelay);
+ void UpdateUpmixChannels(int aNewReadChunk, uint32_t channelCount,
+ ChannelInterpretation aChannelInterpretation);
+
+ // Circular buffer for capturing delayed samples.
+ FallibleTArray<AudioChunk> mChunks;
+ // Cache upmixed channel arrays.
+ AutoTArray<const float*, GUESS_AUDIO_CHANNELS> mUpmixChannels;
+ // Maximum delay, in ticks
+ int mMaxDelayTicks;
+ // The current position in the circular buffer. The next write will be to
+ // this chunk, and the next read may begin before this chunk.
+ int mCurrentChunk;
+ // The chunk owning the pointers in mUpmixChannels
+ int mLastReadChunk;
+#ifdef DEBUG
+ bool mHaveWrittenBlock;
+#endif
+};
+
+} // namespace mozilla
+
+#endif // DelayBuffer_h_
diff --git a/dom/media/webaudio/DelayNode.cpp b/dom/media/webaudio/DelayNode.cpp
new file mode 100644
index 0000000000..3fb6819e41
--- /dev/null
+++ b/dom/media/webaudio/DelayNode.cpp
@@ -0,0 +1,223 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DelayNode.h"
+#include "mozilla/dom/DelayNodeBinding.h"
+#include "AudioNodeEngine.h"
+#include "AudioNodeTrack.h"
+#include "AudioDestinationNode.h"
+#include "WebAudioUtils.h"
+#include "DelayBuffer.h"
+#include "PlayingRefChangeHandler.h"
+#include "Tracing.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(DelayNode, AudioNode, mDelay)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DelayNode)
+NS_INTERFACE_MAP_END_INHERITING(AudioNode)
+
+NS_IMPL_ADDREF_INHERITED(DelayNode, AudioNode)
+NS_IMPL_RELEASE_INHERITED(DelayNode, AudioNode)
+
+class DelayNodeEngine final : public AudioNodeEngine {
+ typedef PlayingRefChangeHandler PlayingRefChanged;
+
+ public:
+ DelayNodeEngine(AudioNode* aNode, AudioDestinationNode* aDestination,
+ float aMaxDelayTicks)
+ : AudioNodeEngine(aNode),
+ mDestination(aDestination->Track())
+ // Keep the default value in sync with the default value in
+ // DelayNode::DelayNode.
+ ,
+ mDelay(0.f)
+ // Use a smoothing range of 20ms
+ ,
+ mBuffer(
+ std::max(aMaxDelayTicks, static_cast<float>(WEBAUDIO_BLOCK_SIZE))),
+ mMaxDelay(aMaxDelayTicks),
+ mHaveProducedBeforeInput(false),
+ mLeftOverData(INT32_MIN) {}
+
+ DelayNodeEngine* AsDelayNodeEngine() override { return this; }
+
+ enum Parameters {
+ DELAY,
+ };
+ void RecvTimelineEvent(uint32_t aIndex, AudioTimelineEvent& aEvent) override {
+ MOZ_ASSERT(mDestination);
+ WebAudioUtils::ConvertAudioTimelineEventToTicks(aEvent, mDestination);
+
+ switch (aIndex) {
+ case DELAY:
+ mDelay.InsertEvent<int64_t>(aEvent);
+ break;
+ default:
+ NS_ERROR("Bad DelayNodeEngine TimelineParameter");
+ }
+ }
+
+ void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom,
+ const AudioBlock& aInput, AudioBlock* aOutput,
+ bool* aFinished) override {
+ MOZ_ASSERT(aTrack->mSampleRate == mDestination->mSampleRate);
+ TRACE("DelayNodeEngine::ProcessBlock");
+
+ if (!aInput.IsSilentOrSubnormal()) {
+ if (mLeftOverData <= 0) {
+ RefPtr<PlayingRefChanged> refchanged =
+ new PlayingRefChanged(aTrack, PlayingRefChanged::ADDREF);
+ aTrack->Graph()->DispatchToMainThreadStableState(refchanged.forget());
+ }
+ mLeftOverData = mBuffer.MaxDelayTicks();
+ } else if (mLeftOverData > 0) {
+ mLeftOverData -= WEBAUDIO_BLOCK_SIZE;
+ } else {
+ if (mLeftOverData != INT32_MIN) {
+ mLeftOverData = INT32_MIN;
+ aTrack->ScheduleCheckForInactive();
+
+ // Delete our buffered data now we no longer need it
+ mBuffer.Reset();
+
+ RefPtr<PlayingRefChanged> refchanged =
+ new PlayingRefChanged(aTrack, PlayingRefChanged::RELEASE);
+ aTrack->Graph()->DispatchToMainThreadStableState(refchanged.forget());
+ }
+ aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
+ return;
+ }
+
+ mBuffer.Write(aInput);
+
+ // Skip output update if mLastChunks has already been set by
+ // ProduceBlockBeforeInput() when in a cycle.
+ if (!mHaveProducedBeforeInput) {
+ UpdateOutputBlock(aTrack, aFrom, aOutput, 0.0);
+ }
+ mHaveProducedBeforeInput = false;
+ mBuffer.NextBlock();
+ }
+
+ void UpdateOutputBlock(AudioNodeTrack* aTrack, GraphTime aFrom,
+ AudioBlock* aOutput, float minDelay) {
+ float maxDelay = mMaxDelay;
+ float sampleRate = aTrack->mSampleRate;
+ ChannelInterpretation channelInterpretation =
+ aTrack->GetChannelInterpretation();
+ if (mDelay.HasSimpleValue()) {
+ // If this DelayNode is in a cycle, make sure the delay value is at least
+ // one block, even if that is greater than maxDelay.
+ float delayFrames = mDelay.GetValue() * sampleRate;
+ float delayFramesClamped =
+ std::max(minDelay, std::min(delayFrames, maxDelay));
+ mBuffer.Read(delayFramesClamped, aOutput, channelInterpretation);
+ } else {
+ // Compute the delay values for the duration of the input AudioChunk
+ // If this DelayNode is in a cycle, make sure the delay value is at least
+ // one block.
+ TrackTime tick = mDestination->GraphTimeToTrackTime(aFrom);
+ float values[WEBAUDIO_BLOCK_SIZE];
+ mDelay.GetValuesAtTime(tick, values, WEBAUDIO_BLOCK_SIZE);
+
+ float computedDelay[WEBAUDIO_BLOCK_SIZE];
+ for (size_t counter = 0; counter < WEBAUDIO_BLOCK_SIZE; ++counter) {
+ float delayAtTick = values[counter] * sampleRate;
+ float delayAtTickClamped =
+ std::max(minDelay, std::min(delayAtTick, maxDelay));
+ computedDelay[counter] = delayAtTickClamped;
+ }
+ mBuffer.Read(computedDelay, aOutput, channelInterpretation);
+ }
+ }
+
+ void ProduceBlockBeforeInput(AudioNodeTrack* aTrack, GraphTime aFrom,
+ AudioBlock* aOutput) override {
+ if (mLeftOverData <= 0) {
+ aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
+ } else {
+ UpdateOutputBlock(aTrack, aFrom, aOutput, WEBAUDIO_BLOCK_SIZE);
+ }
+ mHaveProducedBeforeInput = true;
+ }
+
+ bool IsActive() const override { return mLeftOverData != INT32_MIN; }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override {
+ size_t amount = AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf);
+ // Not owned:
+ // - mDestination - probably not owned
+ // - mDelay - shares ref with AudioNode, don't count
+ amount += mBuffer.SizeOfExcludingThis(aMallocSizeOf);
+ return amount;
+ }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ RefPtr<AudioNodeTrack> mDestination;
+ AudioParamTimeline mDelay;
+ DelayBuffer mBuffer;
+ float mMaxDelay;
+ bool mHaveProducedBeforeInput;
+ // How much data we have in our buffer which needs to be flushed out when our
+ // inputs finish.
+ int32_t mLeftOverData;
+};
+
+DelayNode::DelayNode(AudioContext* aContext, double aMaxDelay)
+ : AudioNode(aContext, 2, ChannelCountMode::Max,
+ ChannelInterpretation::Speakers) {
+ mDelay = CreateAudioParam(DelayNodeEngine::DELAY, u"delayTime"_ns, 0.0f, 0.f,
+ aMaxDelay);
+ DelayNodeEngine* engine = new DelayNodeEngine(
+ this, aContext->Destination(), aContext->SampleRate() * aMaxDelay);
+ mTrack = AudioNodeTrack::Create(
+ aContext, engine, AudioNodeTrack::NO_TRACK_FLAGS, aContext->Graph());
+}
+
+/* static */
+already_AddRefed<DelayNode> DelayNode::Create(AudioContext& aAudioContext,
+ const DelayOptions& aOptions,
+ ErrorResult& aRv) {
+ if (aOptions.mMaxDelayTime <= 0. || aOptions.mMaxDelayTime >= 180.) {
+ aRv.ThrowNotSupportedError(
+ nsPrintfCString("\"maxDelayTime\" (%g) is not in the range (0,180)",
+ aOptions.mMaxDelayTime));
+ return nullptr;
+ }
+
+ RefPtr<DelayNode> audioNode =
+ new DelayNode(&aAudioContext, aOptions.mMaxDelayTime);
+
+ audioNode->Initialize(aOptions, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ audioNode->DelayTime()->SetInitialValue(aOptions.mDelayTime);
+ return audioNode.forget();
+}
+
+size_t DelayNode::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf);
+ amount += mDelay->SizeOfIncludingThis(aMallocSizeOf);
+ return amount;
+}
+
+size_t DelayNode::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
+
+JSObject* DelayNode::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return DelayNode_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/DelayNode.h b/dom/media/webaudio/DelayNode.h
new file mode 100644
index 0000000000..bdfe9eba68
--- /dev/null
+++ b/dom/media/webaudio/DelayNode.h
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DelayNode_h_
+#define DelayNode_h_
+
+#include "AudioNode.h"
+#include "AudioParam.h"
+
+namespace mozilla::dom {
+
+class AudioContext;
+struct DelayOptions;
+
+class DelayNode final : public AudioNode {
+ public:
+ static already_AddRefed<DelayNode> Create(AudioContext& aAudioContext,
+ const DelayOptions& aOptions,
+ ErrorResult& aRv);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(DelayNode, AudioNode)
+
+ static already_AddRefed<DelayNode> Constructor(const GlobalObject& aGlobal,
+ AudioContext& aAudioContext,
+ const DelayOptions& aOptions,
+ ErrorResult& aRv) {
+ return Create(aAudioContext, aOptions, aRv);
+ }
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ AudioParam* DelayTime() const { return mDelay; }
+
+ const char* NodeType() const override { return "DelayNode"; }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override;
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override;
+
+ private:
+ DelayNode(AudioContext* aContext, double aMaxDelay);
+ ~DelayNode() = default;
+
+ friend class DelayNodeEngine;
+
+ RefPtr<AudioParam> mDelay;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webaudio/DynamicsCompressorNode.cpp b/dom/media/webaudio/DynamicsCompressorNode.cpp
new file mode 100644
index 0000000000..7521380710
--- /dev/null
+++ b/dom/media/webaudio/DynamicsCompressorNode.cpp
@@ -0,0 +1,225 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DynamicsCompressorNode.h"
+#include "mozilla/dom/DynamicsCompressorNodeBinding.h"
+#include "AudioNodeEngine.h"
+#include "AudioNodeTrack.h"
+#include "AudioDestinationNode.h"
+#include "WebAudioUtils.h"
+#include "blink/DynamicsCompressor.h"
+#include "Tracing.h"
+
+using WebCore::DynamicsCompressor;
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(DynamicsCompressorNode, AudioNode,
+ mThreshold, mKnee, mRatio, mAttack, mRelease)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DynamicsCompressorNode)
+NS_INTERFACE_MAP_END_INHERITING(AudioNode)
+
+NS_IMPL_ADDREF_INHERITED(DynamicsCompressorNode, AudioNode)
+NS_IMPL_RELEASE_INHERITED(DynamicsCompressorNode, AudioNode)
+
+class DynamicsCompressorNodeEngine final : public AudioNodeEngine {
+ public:
+ explicit DynamicsCompressorNodeEngine(AudioNode* aNode,
+ AudioDestinationNode* aDestination)
+ : AudioNodeEngine(aNode),
+ mDestination(aDestination->Track())
+ // Keep the default value in sync with the default value in
+ // DynamicsCompressorNode::DynamicsCompressorNode.
+ ,
+ mThreshold(-24.f),
+ mKnee(30.f),
+ mRatio(12.f),
+ mAttack(0.003f),
+ mRelease(0.25f),
+ mCompressor(new DynamicsCompressor(mDestination->mSampleRate, 2)) {}
+
+ enum Parameters { THRESHOLD, KNEE, RATIO, ATTACK, RELEASE };
+ void RecvTimelineEvent(uint32_t aIndex, AudioTimelineEvent& aEvent) override {
+ MOZ_ASSERT(mDestination);
+
+ WebAudioUtils::ConvertAudioTimelineEventToTicks(aEvent, mDestination);
+
+ switch (aIndex) {
+ case THRESHOLD:
+ mThreshold.InsertEvent<int64_t>(aEvent);
+ break;
+ case KNEE:
+ mKnee.InsertEvent<int64_t>(aEvent);
+ break;
+ case RATIO:
+ mRatio.InsertEvent<int64_t>(aEvent);
+ break;
+ case ATTACK:
+ mAttack.InsertEvent<int64_t>(aEvent);
+ break;
+ case RELEASE:
+ mRelease.InsertEvent<int64_t>(aEvent);
+ break;
+ default:
+ NS_ERROR("Bad DynamicsCompresssorNodeEngine TimelineParameter");
+ }
+ }
+
+ void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom,
+ const AudioBlock& aInput, AudioBlock* aOutput,
+ bool* aFinished) override {
+ TRACE("DynamicsCompressorNodeEngine::ProcessBlock");
+ if (aInput.IsNull()) {
+ // Just output silence
+ *aOutput = aInput;
+ return;
+ }
+
+ const uint32_t channelCount = aInput.ChannelCount();
+ if (mCompressor->numberOfChannels() != channelCount) {
+ // Create a new compressor object with a new channel count
+ mCompressor = MakeUnique<WebCore::DynamicsCompressor>(
+ aTrack->mSampleRate, aInput.ChannelCount());
+ }
+
+ TrackTime pos = mDestination->GraphTimeToTrackTime(aFrom);
+ mCompressor->setParameterValue(DynamicsCompressor::ParamThreshold,
+ mThreshold.GetValueAtTime(pos));
+ mCompressor->setParameterValue(DynamicsCompressor::ParamKnee,
+ mKnee.GetValueAtTime(pos));
+ mCompressor->setParameterValue(DynamicsCompressor::ParamRatio,
+ mRatio.GetValueAtTime(pos));
+ mCompressor->setParameterValue(DynamicsCompressor::ParamAttack,
+ mAttack.GetValueAtTime(pos));
+ mCompressor->setParameterValue(DynamicsCompressor::ParamRelease,
+ mRelease.GetValueAtTime(pos));
+
+ aOutput->AllocateChannels(channelCount);
+ mCompressor->process(&aInput, aOutput, aInput.GetDuration());
+
+ SendReductionParamToMainThread(
+ aTrack,
+ mCompressor->parameterValue(DynamicsCompressor::ParamReduction));
+ }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override {
+ // Not owned:
+ // - mDestination (probably)
+ // - Don't count the AudioParamTimelines, their inner refs are owned by the
+ // AudioNode.
+ size_t amount = AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf);
+ amount += mCompressor->sizeOfIncludingThis(aMallocSizeOf);
+ return amount;
+ }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ private:
+ void SendReductionParamToMainThread(AudioNodeTrack* aTrack,
+ float aReduction) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ class Command final : public Runnable {
+ public:
+ Command(AudioNodeTrack* aTrack, float aReduction)
+ : mozilla::Runnable("Command"),
+ mTrack(aTrack),
+ mReduction(aReduction) {}
+
+ NS_IMETHOD Run() override {
+ RefPtr<DynamicsCompressorNode> node =
+ static_cast<DynamicsCompressorNode*>(
+ mTrack->Engine()->NodeMainThread());
+ if (node) {
+ node->SetReduction(mReduction);
+ }
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<AudioNodeTrack> mTrack;
+ float mReduction;
+ };
+
+ mAbstractMainThread->Dispatch(do_AddRef(new Command(aTrack, aReduction)));
+ }
+
+ private:
+ RefPtr<AudioNodeTrack> mDestination;
+ AudioParamTimeline mThreshold;
+ AudioParamTimeline mKnee;
+ AudioParamTimeline mRatio;
+ AudioParamTimeline mAttack;
+ AudioParamTimeline mRelease;
+ UniquePtr<DynamicsCompressor> mCompressor;
+};
+
+DynamicsCompressorNode::DynamicsCompressorNode(AudioContext* aContext)
+ : AudioNode(aContext, 2, ChannelCountMode::Clamped_max,
+ ChannelInterpretation::Speakers),
+ mReduction(0) {
+ mThreshold = CreateAudioParam(DynamicsCompressorNodeEngine::THRESHOLD,
+ u"threshold"_ns, -24.f, -100.f, 0.f);
+ mKnee = CreateAudioParam(DynamicsCompressorNodeEngine::KNEE, u"knee"_ns, 30.f,
+ 0.f, 40.f);
+ mRatio = CreateAudioParam(DynamicsCompressorNodeEngine::RATIO, u"ratio"_ns,
+ 12.f, 1.f, 20.f);
+ mAttack = CreateAudioParam(DynamicsCompressorNodeEngine::ATTACK, u"attack"_ns,
+ 0.003f, 0.f, 1.f);
+ mRelease = CreateAudioParam(DynamicsCompressorNodeEngine::RELEASE,
+ u"release"_ns, 0.25f, 0.f, 1.f);
+ DynamicsCompressorNodeEngine* engine =
+ new DynamicsCompressorNodeEngine(this, aContext->Destination());
+ mTrack = AudioNodeTrack::Create(
+ aContext, engine, AudioNodeTrack::NO_TRACK_FLAGS, aContext->Graph());
+}
+
+/* static */
+already_AddRefed<DynamicsCompressorNode> DynamicsCompressorNode::Create(
+ AudioContext& aAudioContext, const DynamicsCompressorOptions& aOptions,
+ ErrorResult& aRv) {
+ RefPtr<DynamicsCompressorNode> audioNode =
+ new DynamicsCompressorNode(&aAudioContext);
+
+ audioNode->Initialize(aOptions, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ audioNode->Attack()->SetInitialValue(aOptions.mAttack);
+ audioNode->Knee()->SetInitialValue(aOptions.mKnee);
+ audioNode->Ratio()->SetInitialValue(aOptions.mRatio);
+ audioNode->GetRelease()->SetInitialValue(aOptions.mRelease);
+ audioNode->Threshold()->SetInitialValue(aOptions.mThreshold);
+
+ return audioNode.forget();
+}
+
+size_t DynamicsCompressorNode::SizeOfExcludingThis(
+ MallocSizeOf aMallocSizeOf) const {
+ size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf);
+ amount += mThreshold->SizeOfIncludingThis(aMallocSizeOf);
+ amount += mKnee->SizeOfIncludingThis(aMallocSizeOf);
+ amount += mRatio->SizeOfIncludingThis(aMallocSizeOf);
+ amount += mAttack->SizeOfIncludingThis(aMallocSizeOf);
+ amount += mRelease->SizeOfIncludingThis(aMallocSizeOf);
+ return amount;
+}
+
+size_t DynamicsCompressorNode::SizeOfIncludingThis(
+ MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
+
+JSObject* DynamicsCompressorNode::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return DynamicsCompressorNode_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/DynamicsCompressorNode.h b/dom/media/webaudio/DynamicsCompressorNode.h
new file mode 100644
index 0000000000..1e0f9cda62
--- /dev/null
+++ b/dom/media/webaudio/DynamicsCompressorNode.h
@@ -0,0 +1,91 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DynamicsCompressorNode_h_
+#define DynamicsCompressorNode_h_
+
+#include "AudioNode.h"
+#include "AudioParam.h"
+
+namespace mozilla::dom {
+
+class AudioContext;
+struct DynamicsCompressorOptions;
+
+class DynamicsCompressorNode final : public AudioNode {
+ public:
+ static already_AddRefed<DynamicsCompressorNode> Create(
+ AudioContext& aAudioContext, const DynamicsCompressorOptions& aOptions,
+ ErrorResult& aRv);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(DynamicsCompressorNode, AudioNode)
+
+ static already_AddRefed<DynamicsCompressorNode> Constructor(
+ const GlobalObject& aGlobal, AudioContext& aAudioContext,
+ const DynamicsCompressorOptions& aOptions, ErrorResult& aRv) {
+ return Create(aAudioContext, aOptions, aRv);
+ }
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ AudioParam* Threshold() const { return mThreshold; }
+
+ AudioParam* Knee() const { return mKnee; }
+
+ AudioParam* Ratio() const { return mRatio; }
+
+ AudioParam* Attack() const { return mAttack; }
+
+ // Called GetRelease to prevent clashing with the nsISupports::Release name
+ AudioParam* GetRelease() const { return mRelease; }
+
+ float Reduction() const { return mReduction; }
+
+ const char* NodeType() const override { return "DynamicsCompressorNode"; }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override;
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override;
+
+ void SetReduction(float aReduction) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mReduction = aReduction;
+ }
+
+ void SetChannelCountModeValue(ChannelCountMode aMode,
+ ErrorResult& aRv) override {
+ if (aMode == ChannelCountMode::Max) {
+ aRv.ThrowNotSupportedError("Cannot set channel count mode to \"max\"");
+ return;
+ }
+ AudioNode::SetChannelCountModeValue(aMode, aRv);
+ }
+
+ void SetChannelCount(uint32_t aChannelCount, ErrorResult& aRv) override {
+ if (aChannelCount > 2) {
+ aRv.ThrowNotSupportedError(
+ nsPrintfCString("%u is greater than 2", aChannelCount));
+ return;
+ }
+ AudioNode::SetChannelCount(aChannelCount, aRv);
+ }
+
+ private:
+ explicit DynamicsCompressorNode(AudioContext* aContext);
+ ~DynamicsCompressorNode() = default;
+
+ RefPtr<AudioParam> mThreshold;
+ RefPtr<AudioParam> mKnee;
+ RefPtr<AudioParam> mRatio;
+ float mReduction;
+ RefPtr<AudioParam> mAttack;
+ RefPtr<AudioParam> mRelease;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webaudio/FFTBlock.cpp b/dom/media/webaudio/FFTBlock.cpp
new file mode 100644
index 0000000000..a4a08faaba
--- /dev/null
+++ b/dom/media/webaudio/FFTBlock.cpp
@@ -0,0 +1,223 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=4 sw=2 sts=2 et cindent: */
+/*
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "FFTBlock.h"
+
+#include <complex>
+
+namespace mozilla {
+
+typedef std::complex<double> Complex;
+
+#ifdef MOZ_LIBAV_FFT
+FFmpegRDFTFuncs FFTBlock::sRDFTFuncs;
+#endif
+
+FFTBlock* FFTBlock::CreateInterpolatedBlock(const FFTBlock& block0,
+ const FFTBlock& block1,
+ double interp) {
+ FFTBlock* newBlock = new FFTBlock(block0.FFTSize());
+
+ newBlock->InterpolateFrequencyComponents(block0, block1, interp);
+
+ // In the time-domain, the 2nd half of the response must be zero, to avoid
+ // circular convolution aliasing...
+ int fftSize = newBlock->FFTSize();
+ AlignedTArray<float> buffer(fftSize);
+ newBlock->GetInverseWithoutScaling(buffer.Elements());
+ AudioBufferInPlaceScale(buffer.Elements(), 1.0f / fftSize, fftSize / 2);
+ PodZero(buffer.Elements() + fftSize / 2, fftSize / 2);
+
+ // Put back into frequency domain.
+ newBlock->PerformFFT(buffer.Elements());
+
+ return newBlock;
+}
+
+void FFTBlock::InterpolateFrequencyComponents(const FFTBlock& block0,
+ const FFTBlock& block1,
+ double interp) {
+ // FIXME : with some work, this method could be optimized
+
+ ComplexU* dft = mOutputBuffer.Elements();
+
+ const ComplexU* dft1 = block0.mOutputBuffer.Elements();
+ const ComplexU* dft2 = block1.mOutputBuffer.Elements();
+
+ MOZ_ASSERT(mFFTSize == block0.FFTSize());
+ MOZ_ASSERT(mFFTSize == block1.FFTSize());
+ double s1base = (1.0 - interp);
+ double s2base = interp;
+
+ double phaseAccum = 0.0;
+ double lastPhase1 = 0.0;
+ double lastPhase2 = 0.0;
+
+ int n = mFFTSize / 2;
+
+ dft[0].r = static_cast<float>(s1base * dft1[0].r + s2base * dft2[0].r);
+ dft[n].r = static_cast<float>(s1base * dft1[n].r + s2base * dft2[n].r);
+
+ for (int i = 1; i < n; ++i) {
+ Complex c1(dft1[i].r, dft1[i].i);
+ Complex c2(dft2[i].r, dft2[i].i);
+
+ double mag1 = abs(c1);
+ double mag2 = abs(c2);
+
+ // Interpolate magnitudes in decibels
+ double mag1db = 20.0 * log10(mag1);
+ double mag2db = 20.0 * log10(mag2);
+
+ double s1 = s1base;
+ double s2 = s2base;
+
+ double magdbdiff = mag1db - mag2db;
+
+ // Empirical tweak to retain higher-frequency zeroes
+ double threshold = (i > 16) ? 5.0 : 2.0;
+
+ if (magdbdiff < -threshold && mag1db < 0.0) {
+ s1 = pow(s1, 0.75);
+ s2 = 1.0 - s1;
+ } else if (magdbdiff > threshold && mag2db < 0.0) {
+ s2 = pow(s2, 0.75);
+ s1 = 1.0 - s2;
+ }
+
+ // Average magnitude by decibels instead of linearly
+ double magdb = s1 * mag1db + s2 * mag2db;
+ double mag = pow(10.0, 0.05 * magdb);
+
+ // Now, deal with phase
+ double phase1 = arg(c1);
+ double phase2 = arg(c2);
+
+ double deltaPhase1 = phase1 - lastPhase1;
+ double deltaPhase2 = phase2 - lastPhase2;
+ lastPhase1 = phase1;
+ lastPhase2 = phase2;
+
+ // Unwrap phase deltas
+ if (deltaPhase1 > M_PI) deltaPhase1 -= 2.0 * M_PI;
+ if (deltaPhase1 < -M_PI) deltaPhase1 += 2.0 * M_PI;
+ if (deltaPhase2 > M_PI) deltaPhase2 -= 2.0 * M_PI;
+ if (deltaPhase2 < -M_PI) deltaPhase2 += 2.0 * M_PI;
+
+ // Blend group-delays
+ double deltaPhaseBlend;
+
+ if (deltaPhase1 - deltaPhase2 > M_PI)
+ deltaPhaseBlend = s1 * deltaPhase1 + s2 * (2.0 * M_PI + deltaPhase2);
+ else if (deltaPhase2 - deltaPhase1 > M_PI)
+ deltaPhaseBlend = s1 * (2.0 * M_PI + deltaPhase1) + s2 * deltaPhase2;
+ else
+ deltaPhaseBlend = s1 * deltaPhase1 + s2 * deltaPhase2;
+
+ phaseAccum += deltaPhaseBlend;
+
+ // Unwrap
+ if (phaseAccum > M_PI) phaseAccum -= 2.0 * M_PI;
+ if (phaseAccum < -M_PI) phaseAccum += 2.0 * M_PI;
+
+ dft[i].r = static_cast<float>(mag * cos(phaseAccum));
+ dft[i].i = static_cast<float>(mag * sin(phaseAccum));
+ }
+}
+
+double FFTBlock::ExtractAverageGroupDelay() {
+ ComplexU* dft = mOutputBuffer.Elements();
+
+ double aveSum = 0.0;
+ double weightSum = 0.0;
+ double lastPhase = 0.0;
+
+ int halfSize = FFTSize() / 2;
+
+ const double kSamplePhaseDelay = (2.0 * M_PI) / double(FFTSize());
+
+ // Remove DC offset
+ dft[0].r = 0.0f;
+
+ // Calculate weighted average group delay
+ for (int i = 1; i < halfSize; i++) {
+ Complex c(dft[i].r, dft[i].i);
+ double mag = abs(c);
+ double phase = arg(c);
+
+ double deltaPhase = phase - lastPhase;
+ lastPhase = phase;
+
+ // Unwrap
+ if (deltaPhase < -M_PI) deltaPhase += 2.0 * M_PI;
+ if (deltaPhase > M_PI) deltaPhase -= 2.0 * M_PI;
+
+ aveSum += mag * deltaPhase;
+ weightSum += mag;
+ }
+
+ // Note how we invert the phase delta wrt frequency since this is how group
+ // delay is defined
+ double ave = aveSum / weightSum;
+ double aveSampleDelay = -ave / kSamplePhaseDelay;
+
+ // Leave 20 sample headroom (for leading edge of impulse)
+ aveSampleDelay -= 20.0;
+ if (aveSampleDelay <= 0.0) return 0.0;
+
+ // Remove average group delay (minus 20 samples for headroom)
+ AddConstantGroupDelay(-aveSampleDelay);
+
+ return aveSampleDelay;
+}
+
+void FFTBlock::AddConstantGroupDelay(double sampleFrameDelay) {
+ int halfSize = FFTSize() / 2;
+
+ ComplexU* dft = mOutputBuffer.Elements();
+
+ const double kSamplePhaseDelay = (2.0 * M_PI) / double(FFTSize());
+
+ double phaseAdj = -sampleFrameDelay * kSamplePhaseDelay;
+
+ // Add constant group delay
+ for (int i = 1; i < halfSize; i++) {
+ Complex c(dft[i].r, dft[i].i);
+ double mag = abs(c);
+ double phase = arg(c);
+
+ phase += i * phaseAdj;
+
+ dft[i].r = static_cast<float>(mag * cos(phase));
+ dft[i].i = static_cast<float>(mag * sin(phase));
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/webaudio/FFTBlock.h b/dom/media/webaudio/FFTBlock.h
new file mode 100644
index 0000000000..406c70b64d
--- /dev/null
+++ b/dom/media/webaudio/FFTBlock.h
@@ -0,0 +1,346 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef FFTBlock_h_
+#define FFTBlock_h_
+
+#ifdef BUILD_ARM_NEON
+# include <cmath>
+# include "mozilla/arm.h"
+# include "dl/sp/api/omxSP.h"
+#endif
+
+#include "AlignedTArray.h"
+#include "AudioNodeEngine.h"
+#if defined(MOZ_LIBAV_FFT)
+# include "FFmpegRDFTTypes.h"
+# include "FFVPXRuntimeLinker.h"
+#else
+# include "kiss_fft/kiss_fftr.h"
+#endif
+
+namespace mozilla {
+
+// This class defines an FFT block, loosely modeled after Blink's FFTFrame
+// class to make sharing code with Blink easy.
+// Currently it's implemented on top of KissFFT on all platforms.
+class FFTBlock final {
+ union ComplexU {
+#if !defined(MOZ_LIBAV_FFT)
+ kiss_fft_cpx c;
+#endif
+ float f[2];
+ struct {
+ float r;
+ float i;
+ };
+ };
+
+ public:
+ static void MainThreadInit() {
+#ifdef MOZ_LIBAV_FFT
+ FFVPXRuntimeLinker::Init();
+ if (!sRDFTFuncs.init) {
+ FFVPXRuntimeLinker::GetRDFTFuncs(&sRDFTFuncs);
+ }
+#endif
+ }
+
+ explicit FFTBlock(uint32_t aFFTSize)
+#if defined(MOZ_LIBAV_FFT)
+ : mAvRDFT(nullptr),
+ mAvIRDFT(nullptr)
+#else
+ : mKissFFT(nullptr),
+ mKissIFFT(nullptr)
+# ifdef BUILD_ARM_NEON
+ ,
+ mOmxFFT(nullptr),
+ mOmxIFFT(nullptr)
+# endif
+#endif
+ {
+ MOZ_COUNT_CTOR(FFTBlock);
+ SetFFTSize(aFFTSize);
+ }
+ ~FFTBlock() {
+ MOZ_COUNT_DTOR(FFTBlock);
+ Clear();
+ }
+
+ // Return a new FFTBlock with frequency components interpolated between
+ // |block0| and |block1| with |interp| between 0.0 and 1.0.
+ static FFTBlock* CreateInterpolatedBlock(const FFTBlock& block0,
+ const FFTBlock& block1,
+ double interp);
+
+ // Transform FFTSize() points of aData and store the result internally.
+ void PerformFFT(const float* aData) {
+ if (!EnsureFFT()) {
+ return;
+ }
+
+#if defined(MOZ_LIBAV_FFT)
+ PodCopy(mOutputBuffer.Elements()->f, aData, mFFTSize);
+ sRDFTFuncs.calc(mAvRDFT, mOutputBuffer.Elements()->f);
+ // Recover packed Nyquist.
+ mOutputBuffer[mFFTSize / 2].r = mOutputBuffer[0].i;
+ mOutputBuffer[0].i = 0.0f;
+#else
+# ifdef BUILD_ARM_NEON
+ if (mozilla::supports_neon()) {
+ omxSP_FFTFwd_RToCCS_F32_Sfs(aData, mOutputBuffer.Elements()->f, mOmxFFT);
+ } else
+# endif
+ {
+ kiss_fftr(mKissFFT, aData, &(mOutputBuffer.Elements()->c));
+ }
+#endif
+ }
+ // Inverse-transform internal data and store the resulting FFTSize()
+ // points in aDataOut.
+ void GetInverse(float* aDataOut) {
+ GetInverseWithoutScaling(aDataOut);
+ AudioBufferInPlaceScale(aDataOut, 1.0f / mFFTSize, mFFTSize);
+ }
+
+ // Inverse-transform internal frequency data and store the resulting
+ // FFTSize() points in |aDataOut|. If frequency data has not already been
+ // scaled, then the output will need scaling by 1/FFTSize().
+ void GetInverseWithoutScaling(float* aDataOut) {
+ if (!EnsureIFFT()) {
+ std::fill_n(aDataOut, mFFTSize, 0.0f);
+ return;
+ };
+
+#if defined(MOZ_LIBAV_FFT)
+ {
+ // Even though this function doesn't scale, the libav forward transform
+ // gives a value that needs scaling by 2 in order for things to turn out
+ // similar to how we expect from kissfft/openmax.
+ AudioBufferCopyWithScale(mOutputBuffer.Elements()->f, 2.0f, aDataOut,
+ mFFTSize);
+ aDataOut[1] = 2.0f * mOutputBuffer[mFFTSize / 2].r; // Packed Nyquist
+ sRDFTFuncs.calc(mAvIRDFT, aDataOut);
+ }
+#else
+# ifdef BUILD_ARM_NEON
+ if (mozilla::supports_neon()) {
+ omxSP_FFTInv_CCSToR_F32_Sfs_unscaled(mOutputBuffer.Elements()->f,
+ aDataOut, mOmxIFFT);
+ } else
+# endif
+ {
+ kiss_fftri(mKissIFFT, &(mOutputBuffer.Elements()->c), aDataOut);
+ }
+#endif
+ }
+
+ void Multiply(const FFTBlock& aFrame) {
+ uint32_t halfSize = mFFTSize / 2;
+ // DFTs are not packed.
+ MOZ_ASSERT(mOutputBuffer[0].i == 0);
+ MOZ_ASSERT(aFrame.mOutputBuffer[0].i == 0);
+
+ BufferComplexMultiply(mOutputBuffer.Elements()->f,
+ aFrame.mOutputBuffer.Elements()->f,
+ mOutputBuffer.Elements()->f, halfSize);
+ mOutputBuffer[halfSize].r *= aFrame.mOutputBuffer[halfSize].r;
+ // This would have been set to NaN if either real component was NaN.
+ mOutputBuffer[0].i = 0.0f;
+ }
+
+ // Perform a forward FFT on |aData|, assuming zeros after dataSize samples,
+ // and pre-scale the generated internal frequency domain coefficients so
+ // that GetInverseWithoutScaling() can be used to transform to the time
+ // domain. This is useful for convolution kernels.
+ void PadAndMakeScaledDFT(const float* aData, size_t dataSize) {
+ MOZ_ASSERT(dataSize <= FFTSize());
+ AlignedTArray<float> paddedData;
+ paddedData.SetLength(FFTSize());
+ AudioBufferCopyWithScale(aData, 1.0f / FFTSize(), paddedData.Elements(),
+ dataSize);
+ PodZero(paddedData.Elements() + dataSize, mFFTSize - dataSize);
+ PerformFFT(paddedData.Elements());
+ }
+
+ void SetFFTSize(uint32_t aSize) {
+ mFFTSize = aSize;
+ mOutputBuffer.SetLength(aSize / 2 + 1);
+ PodZero(mOutputBuffer.Elements(), aSize / 2 + 1);
+ Clear();
+ }
+
+ // Return the average group delay and removes this from the frequency data.
+ double ExtractAverageGroupDelay();
+
+ uint32_t FFTSize() const { return mFFTSize; }
+ float RealData(uint32_t aIndex) const { return mOutputBuffer[aIndex].r; }
+ float& RealData(uint32_t aIndex) { return mOutputBuffer[aIndex].r; }
+ float ImagData(uint32_t aIndex) const { return mOutputBuffer[aIndex].i; }
+ float& ImagData(uint32_t aIndex) { return mOutputBuffer[aIndex].i; }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t amount = 0;
+
+#if defined(MOZ_LIBAV_FFT)
+ auto ComputedSizeOfContextIfSet = [this](void* aContext) -> size_t {
+ if (!aContext) {
+ return 0;
+ }
+ // RDFTContext is only forward declared in public headers, but this is
+ // an estimate based on a value of 231 seen requested from
+ // _aligned_alloc on Win64. Don't use malloc_usable_size() because the
+ // context pointer is not necessarily from malloc.
+ size_t amount = 232;
+ // Add size of allocations performed in ff_fft_init().
+ // The maximum FFT size used is 32768 = 2^15 and so revtab32 is not
+ // allocated.
+ MOZ_ASSERT(mFFTSize <= 32768);
+ amount += mFFTSize * (sizeof(uint16_t) + 2 * sizeof(float));
+
+ return amount;
+ };
+
+ amount += ComputedSizeOfContextIfSet(mAvRDFT);
+ amount += ComputedSizeOfContextIfSet(mAvIRDFT);
+#else
+# ifdef BUILD_ARM_NEON
+ amount += aMallocSizeOf(mOmxFFT);
+ amount += aMallocSizeOf(mOmxIFFT);
+# endif
+# ifdef USE_SIMD
+# error kiss fft uses malloc only when USE_SIMD is not defined
+# endif
+ amount += aMallocSizeOf(mKissFFT);
+ amount += aMallocSizeOf(mKissIFFT);
+#endif
+ amount += mOutputBuffer.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ return amount;
+ }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ private:
+ FFTBlock(const FFTBlock& other) = delete;
+ void operator=(const FFTBlock& other) = delete;
+
+ bool EnsureFFT() {
+#if defined(MOZ_LIBAV_FFT)
+ if (!mAvRDFT) {
+ if (!sRDFTFuncs.init) {
+ return false;
+ }
+
+ mAvRDFT = sRDFTFuncs.init(log((double)mFFTSize) / M_LN2, DFT_R2C);
+ }
+#else
+# ifdef BUILD_ARM_NEON
+ if (mozilla::supports_neon()) {
+ if (!mOmxFFT) {
+ mOmxFFT = createOmxFFT(mFFTSize);
+ }
+ } else
+# endif
+ {
+ if (!mKissFFT) {
+ mKissFFT = kiss_fftr_alloc(mFFTSize, 0, nullptr, nullptr);
+ }
+ }
+#endif
+ return true;
+ }
+
+ bool EnsureIFFT() {
+#if defined(MOZ_LIBAV_FFT)
+ if (!mAvIRDFT) {
+ if (!sRDFTFuncs.init) {
+ return false;
+ }
+
+ mAvIRDFT = sRDFTFuncs.init(log((double)mFFTSize) / M_LN2, IDFT_C2R);
+ }
+#else
+# ifdef BUILD_ARM_NEON
+ if (mozilla::supports_neon()) {
+ if (!mOmxIFFT) {
+ mOmxIFFT = createOmxFFT(mFFTSize);
+ }
+ } else
+# endif
+ {
+ if (!mKissIFFT) {
+ mKissIFFT = kiss_fftr_alloc(mFFTSize, 1, nullptr, nullptr);
+ }
+ }
+#endif
+ return true;
+ }
+
+#ifdef BUILD_ARM_NEON
+ static OMXFFTSpec_R_F32* createOmxFFT(uint32_t aFFTSize) {
+ MOZ_ASSERT((aFFTSize & (aFFTSize - 1)) == 0);
+ OMX_INT bufSize;
+ OMX_INT order = log((double)aFFTSize) / M_LN2;
+ MOZ_ASSERT(aFFTSize >> order == 1);
+ OMXResult status = omxSP_FFTGetBufSize_R_F32(order, &bufSize);
+ if (status == OMX_Sts_NoErr) {
+ OMXFFTSpec_R_F32* context =
+ static_cast<OMXFFTSpec_R_F32*>(malloc(bufSize));
+ if (omxSP_FFTInit_R_F32(context, order) != OMX_Sts_NoErr) {
+ return nullptr;
+ }
+ return context;
+ }
+ return nullptr;
+ }
+#endif
+
+ void Clear() {
+#if defined(MOZ_LIBAV_FFT)
+ if (mAvRDFT) {
+ sRDFTFuncs.end(mAvRDFT);
+ mAvRDFT = nullptr;
+ }
+ if (mAvIRDFT) {
+ sRDFTFuncs.end(mAvIRDFT);
+ mAvIRDFT = nullptr;
+ }
+#else
+# ifdef BUILD_ARM_NEON
+ free(mOmxFFT);
+ free(mOmxIFFT);
+ mOmxFFT = mOmxIFFT = nullptr;
+# endif
+ free(mKissFFT);
+ free(mKissIFFT);
+ mKissFFT = mKissIFFT = nullptr;
+#endif
+ }
+ void AddConstantGroupDelay(double sampleFrameDelay);
+ void InterpolateFrequencyComponents(const FFTBlock& block0,
+ const FFTBlock& block1, double interp);
+#if defined(MOZ_LIBAV_FFT)
+ static FFmpegRDFTFuncs sRDFTFuncs;
+ RDFTContext* mAvRDFT;
+ RDFTContext* mAvIRDFT;
+#else
+ kiss_fftr_cfg mKissFFT;
+ kiss_fftr_cfg mKissIFFT;
+# ifdef BUILD_ARM_NEON
+ OMXFFTSpec_R_F32* mOmxFFT;
+ OMXFFTSpec_R_F32* mOmxIFFT;
+# endif
+#endif
+ AlignedTArray<ComplexU> mOutputBuffer;
+ uint32_t mFFTSize;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webaudio/GainNode.cpp b/dom/media/webaudio/GainNode.cpp
new file mode 100644
index 0000000000..c858497353
--- /dev/null
+++ b/dom/media/webaudio/GainNode.cpp
@@ -0,0 +1,149 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GainNode.h"
+#include "mozilla/dom/GainNodeBinding.h"
+#include "AlignmentUtils.h"
+#include "AudioNodeEngine.h"
+#include "AudioNodeTrack.h"
+#include "AudioDestinationNode.h"
+#include "WebAudioUtils.h"
+#include "Tracing.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(GainNode, AudioNode, mGain)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(GainNode)
+NS_INTERFACE_MAP_END_INHERITING(AudioNode)
+
+NS_IMPL_ADDREF_INHERITED(GainNode, AudioNode)
+NS_IMPL_RELEASE_INHERITED(GainNode, AudioNode)
+
+class GainNodeEngine final : public AudioNodeEngine {
+ public:
+ GainNodeEngine(AudioNode* aNode, AudioDestinationNode* aDestination)
+ : AudioNodeEngine(aNode),
+ mDestination(aDestination->Track())
+ // Keep the default value in sync with the default value in
+ // GainNode::GainNode.
+ ,
+ mGain(1.f) {}
+
+ enum Parameters { GAIN };
+ void RecvTimelineEvent(uint32_t aIndex, AudioTimelineEvent& aEvent) override {
+ MOZ_ASSERT(mDestination);
+ WebAudioUtils::ConvertAudioTimelineEventToTicks(aEvent, mDestination);
+
+ switch (aIndex) {
+ case GAIN:
+ mGain.InsertEvent<int64_t>(aEvent);
+ break;
+ default:
+ NS_ERROR("Bad GainNodeEngine TimelineParameter");
+ }
+ }
+
+ void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom,
+ const AudioBlock& aInput, AudioBlock* aOutput,
+ bool* aFinished) override {
+ TRACE("GainNodeEngine::ProcessBlock");
+ if (aInput.IsNull()) {
+ // If input is silent, so is the output
+ aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
+ } else if (mGain.HasSimpleValue()) {
+ // Optimize the case where we only have a single value set as the volume
+ float gain = mGain.GetValue();
+ if (gain == 0.0f) {
+ aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
+ } else {
+ *aOutput = aInput;
+ aOutput->mVolume *= gain;
+ }
+ } else {
+ // First, compute a vector of gains for each track tick based on the
+ // timeline at hand, and then for each channel, multiply the values
+ // in the buffer with the gain vector.
+ aOutput->AllocateChannels(aInput.ChannelCount());
+
+ // Compute the gain values for the duration of the input AudioChunk
+ TrackTime tick = mDestination->GraphTimeToTrackTime(aFrom);
+ float computedGain[WEBAUDIO_BLOCK_SIZE + 4];
+ float* alignedComputedGain = ALIGNED16(computedGain);
+ ASSERT_ALIGNED16(alignedComputedGain);
+ mGain.GetValuesAtTime(tick, alignedComputedGain, WEBAUDIO_BLOCK_SIZE);
+
+ for (size_t counter = 0; counter < WEBAUDIO_BLOCK_SIZE; ++counter) {
+ alignedComputedGain[counter] *= aInput.mVolume;
+ }
+
+ // Apply the gain to the output buffer
+ for (size_t channel = 0; channel < aOutput->ChannelCount(); ++channel) {
+ const float* inputBuffer =
+ static_cast<const float*>(aInput.mChannelData[channel]);
+ float* buffer = aOutput->ChannelFloatsForWrite(channel);
+ AudioBlockCopyChannelWithScale(inputBuffer, alignedComputedGain,
+ buffer);
+ }
+ }
+ }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override {
+ // Not owned:
+ // - mDestination - MediaTrackGraphImpl::CollectSizesForMemoryReport()
+ // accounts for mDestination.
+ // - mGain - Internal ref owned by AudioNode
+ return AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ RefPtr<AudioNodeTrack> mDestination;
+ AudioParamTimeline mGain;
+};
+
+GainNode::GainNode(AudioContext* aContext)
+ : AudioNode(aContext, 2, ChannelCountMode::Max,
+ ChannelInterpretation::Speakers) {
+ mGain = CreateAudioParam(GainNodeEngine::GAIN, u"gain"_ns, 1.0f);
+ GainNodeEngine* engine = new GainNodeEngine(this, aContext->Destination());
+ mTrack = AudioNodeTrack::Create(
+ aContext, engine, AudioNodeTrack::NO_TRACK_FLAGS, aContext->Graph());
+}
+
+/* static */
+already_AddRefed<GainNode> GainNode::Create(AudioContext& aAudioContext,
+ const GainOptions& aOptions,
+ ErrorResult& aRv) {
+ RefPtr<GainNode> audioNode = new GainNode(&aAudioContext);
+
+ audioNode->Initialize(aOptions, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ audioNode->Gain()->SetInitialValue(aOptions.mGain);
+ return audioNode.forget();
+}
+
+size_t GainNode::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf);
+ amount += mGain->SizeOfIncludingThis(aMallocSizeOf);
+ return amount;
+}
+
+size_t GainNode::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
+
+JSObject* GainNode::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return GainNode_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/GainNode.h b/dom/media/webaudio/GainNode.h
new file mode 100644
index 0000000000..d2790af746
--- /dev/null
+++ b/dom/media/webaudio/GainNode.h
@@ -0,0 +1,53 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GainNode_h_
+#define GainNode_h_
+
+#include "AudioNode.h"
+#include "AudioParam.h"
+
+namespace mozilla::dom {
+
+class AudioContext;
+struct GainOptions;
+
+class GainNode final : public AudioNode {
+ public:
+ static already_AddRefed<GainNode> Create(AudioContext& aAudioContext,
+ const GainOptions& aOptions,
+ ErrorResult& aRv);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(GainNode, AudioNode)
+
+ static already_AddRefed<GainNode> Constructor(const GlobalObject& aGlobal,
+ AudioContext& aAudioContext,
+ const GainOptions& aOptions,
+ ErrorResult& aRv) {
+ return Create(aAudioContext, aOptions, aRv);
+ }
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ AudioParam* Gain() const { return mGain; }
+
+ const char* NodeType() const override { return "GainNode"; }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override;
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override;
+
+ private:
+ explicit GainNode(AudioContext* aContext);
+ ~GainNode() = default;
+
+ RefPtr<AudioParam> mGain;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webaudio/IIRFilterNode.cpp b/dom/media/webaudio/IIRFilterNode.cpp
new file mode 100644
index 0000000000..c78e232b39
--- /dev/null
+++ b/dom/media/webaudio/IIRFilterNode.cpp
@@ -0,0 +1,258 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "IIRFilterNode.h"
+#include "AudioNodeEngine.h"
+#include "AudioDestinationNode.h"
+#include "blink/IIRFilter.h"
+#include "PlayingRefChangeHandler.h"
+#include "AlignmentUtils.h"
+#include "nsPrintfCString.h"
+
+#include "nsGkAtoms.h"
+#include "Tracing.h"
+
+namespace mozilla::dom {
+
+class IIRFilterNodeEngine final : public AudioNodeEngine {
+ public:
+ IIRFilterNodeEngine(AudioNode* aNode, AudioDestinationNode* aDestination,
+ const AudioDoubleArray& aFeedforward,
+ const AudioDoubleArray& aFeedback, uint64_t aWindowID)
+ : AudioNodeEngine(aNode),
+ mDestination(aDestination->Track()),
+ mFeedforward(aFeedforward.Clone()),
+ mFeedback(aFeedback.Clone()),
+ mWindowID(aWindowID) {}
+
+ void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom,
+ const AudioBlock& aInput, AudioBlock* aOutput,
+ bool* aFinished) override {
+ TRACE("IIRFilterNodeEngine::ProcessBlock");
+ float inputBuffer[WEBAUDIO_BLOCK_SIZE + 4];
+ float* alignedInputBuffer = ALIGNED16(inputBuffer);
+ ASSERT_ALIGNED16(alignedInputBuffer);
+
+ if (aInput.IsNull()) {
+ if (!mIIRFilters.IsEmpty()) {
+ bool allZero = true;
+ for (uint32_t i = 0; i < mIIRFilters.Length(); ++i) {
+ allZero &= mIIRFilters[i]->buffersAreZero();
+ }
+
+ // all filter buffer values are zero, so the output will be zero
+ // as well.
+ if (allZero) {
+ mIIRFilters.Clear();
+ aTrack->ScheduleCheckForInactive();
+
+ RefPtr<PlayingRefChangeHandler> refchanged =
+ new PlayingRefChangeHandler(aTrack,
+ PlayingRefChangeHandler::RELEASE);
+ aTrack->Graph()->DispatchToMainThreadStableState(refchanged.forget());
+
+ aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
+ return;
+ }
+
+ PodZero(alignedInputBuffer, WEBAUDIO_BLOCK_SIZE);
+ }
+ } else if (mIIRFilters.Length() != aInput.ChannelCount()) {
+ if (mIIRFilters.IsEmpty()) {
+ RefPtr<PlayingRefChangeHandler> refchanged =
+ new PlayingRefChangeHandler(aTrack,
+ PlayingRefChangeHandler::ADDREF);
+ aTrack->Graph()->DispatchToMainThreadStableState(refchanged.forget());
+ } else {
+ WebAudioUtils::LogToDeveloperConsole(
+ mWindowID, "IIRFilterChannelCountChangeWarning");
+ }
+
+ // Adjust the number of filters based on the number of channels
+ mIIRFilters.SetLength(aInput.ChannelCount());
+ for (size_t i = 0; i < aInput.ChannelCount(); ++i) {
+ mIIRFilters[i] =
+ MakeUnique<blink::IIRFilter>(&mFeedforward, &mFeedback);
+ }
+ }
+
+ uint32_t numberOfChannels = mIIRFilters.Length();
+ aOutput->AllocateChannels(numberOfChannels);
+
+ for (uint32_t i = 0; i < numberOfChannels; ++i) {
+ const float* input;
+ if (aInput.IsNull()) {
+ input = alignedInputBuffer;
+ } else {
+ input = static_cast<const float*>(aInput.mChannelData[i]);
+ if (aInput.mVolume != 1.0) {
+ AudioBlockCopyChannelWithScale(input, aInput.mVolume,
+ alignedInputBuffer);
+ input = alignedInputBuffer;
+ }
+ }
+
+ mIIRFilters[i]->process(input, aOutput->ChannelFloatsForWrite(i),
+ aInput.GetDuration());
+ }
+ }
+
+ bool IsActive() const override { return !mIIRFilters.IsEmpty(); }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override {
+ // Not owned:
+ // - mDestination - probably not owned
+ // - AudioParamTimelines - counted in the AudioNode
+ size_t amount = AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf);
+ amount += mIIRFilters.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ return amount;
+ }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ private:
+ RefPtr<AudioNodeTrack> mDestination;
+ nsTArray<UniquePtr<blink::IIRFilter>> mIIRFilters;
+ AudioDoubleArray mFeedforward;
+ AudioDoubleArray mFeedback;
+ uint64_t mWindowID;
+};
+
+IIRFilterNode::IIRFilterNode(AudioContext* aContext,
+ const Sequence<double>& aFeedforward,
+ const Sequence<double>& aFeedback)
+ : AudioNode(aContext, 2, ChannelCountMode::Max,
+ ChannelInterpretation::Speakers) {
+ mFeedforward.SetLength(aFeedforward.Length());
+ PodCopy(mFeedforward.Elements(), aFeedforward.Elements(),
+ aFeedforward.Length());
+ mFeedback.SetLength(aFeedback.Length());
+ PodCopy(mFeedback.Elements(), aFeedback.Elements(), aFeedback.Length());
+
+ // Scale coefficients -- we guarantee that mFeedback != 0 when creating
+ // the IIRFilterNode.
+ double scale = mFeedback[0];
+ double* elements = mFeedforward.Elements();
+ for (size_t i = 0; i < mFeedforward.Length(); ++i) {
+ elements[i] /= scale;
+ }
+
+ elements = mFeedback.Elements();
+ for (size_t i = 0; i < mFeedback.Length(); ++i) {
+ elements[i] /= scale;
+ }
+
+ // We check that this is exactly equal to one later in blink/IIRFilter.cpp
+ elements[0] = 1.0;
+
+ uint64_t windowID = 0;
+ if (aContext->GetParentObject()) {
+ windowID = aContext->GetParentObject()->WindowID();
+ }
+ IIRFilterNodeEngine* engine = new IIRFilterNodeEngine(
+ this, aContext->Destination(), mFeedforward, mFeedback, windowID);
+ mTrack = AudioNodeTrack::Create(
+ aContext, engine, AudioNodeTrack::NO_TRACK_FLAGS, aContext->Graph());
+}
+
+/* static */
+already_AddRefed<IIRFilterNode> IIRFilterNode::Create(
+ AudioContext& aAudioContext, const IIRFilterOptions& aOptions,
+ ErrorResult& aRv) {
+ if (aOptions.mFeedforward.Length() == 0 ||
+ aOptions.mFeedforward.Length() > 20) {
+ aRv.ThrowNotSupportedError(
+ nsPrintfCString("\"feedforward\" length %zu is not in the range [1,20]",
+ aOptions.mFeedforward.Length()));
+ return nullptr;
+ }
+
+ if (aOptions.mFeedback.Length() == 0 || aOptions.mFeedback.Length() > 20) {
+ aRv.ThrowNotSupportedError(
+ nsPrintfCString("\"feedback\" length %zu is not in the range [1,20]",
+ aOptions.mFeedback.Length()));
+ return nullptr;
+ }
+
+ bool feedforwardAllZeros = true;
+ for (size_t i = 0; i < aOptions.mFeedforward.Length(); ++i) {
+ if (aOptions.mFeedforward.Elements()[i] != 0.0) {
+ feedforwardAllZeros = false;
+ break;
+ }
+ }
+
+ if (feedforwardAllZeros) {
+ aRv.ThrowInvalidStateError(
+ "\"feedforward\" must contain some nonzero values");
+ return nullptr;
+ }
+
+ if (aOptions.mFeedback[0] == 0.0) {
+ aRv.ThrowInvalidStateError("First value in \"feedback\" must be nonzero");
+ return nullptr;
+ }
+
+ RefPtr<IIRFilterNode> audioNode = new IIRFilterNode(
+ &aAudioContext, aOptions.mFeedforward, aOptions.mFeedback);
+
+ audioNode->Initialize(aOptions, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ return audioNode.forget();
+}
+
+size_t IIRFilterNode::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf);
+ return amount;
+}
+
+size_t IIRFilterNode::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
+
+JSObject* IIRFilterNode::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return IIRFilterNode_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void IIRFilterNode::GetFrequencyResponse(const Float32Array& aFrequencyHz,
+ const Float32Array& aMagResponse,
+ const Float32Array& aPhaseResponse) {
+ aFrequencyHz.ComputeState();
+ aMagResponse.ComputeState();
+ aPhaseResponse.ComputeState();
+
+ uint32_t length =
+ std::min(std::min(aFrequencyHz.Length(), aMagResponse.Length()),
+ aPhaseResponse.Length());
+ if (!length) {
+ return;
+ }
+
+ auto frequencies = MakeUnique<float[]>(length);
+ float* frequencyHz = aFrequencyHz.Data();
+ const double nyquist = Context()->SampleRate() * 0.5;
+
+ // Normalize the frequencies
+ for (uint32_t i = 0; i < length; ++i) {
+ if (frequencyHz[i] >= 0 && frequencyHz[i] <= nyquist) {
+ frequencies[i] = static_cast<float>(frequencyHz[i] / nyquist);
+ } else {
+ frequencies[i] = std::numeric_limits<float>::quiet_NaN();
+ }
+ }
+
+ blink::IIRFilter filter(&mFeedforward, &mFeedback);
+ filter.getFrequencyResponse(int(length), frequencies.get(),
+ aMagResponse.Data(), aPhaseResponse.Data());
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/IIRFilterNode.h b/dom/media/webaudio/IIRFilterNode.h
new file mode 100644
index 0000000000..d79f87f632
--- /dev/null
+++ b/dom/media/webaudio/IIRFilterNode.h
@@ -0,0 +1,56 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef IIRFilterNode_h_
+#define IIRFilterNode_h_
+
+#include "AudioNode.h"
+#include "AudioParam.h"
+#include "mozilla/dom/IIRFilterNodeBinding.h"
+
+namespace mozilla::dom {
+
+class AudioContext;
+struct IIRFilterOptions;
+
+class IIRFilterNode final : public AudioNode {
+ public:
+ static already_AddRefed<IIRFilterNode> Create(
+ AudioContext& aAudioContext, const IIRFilterOptions& aOptions,
+ ErrorResult& aRv);
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(IIRFilterNode, AudioNode)
+
+ static already_AddRefed<IIRFilterNode> Constructor(
+ const GlobalObject& aGlobal, AudioContext& aAudioContext,
+ const IIRFilterOptions& aOptions, ErrorResult& aRv) {
+ return Create(aAudioContext, aOptions, aRv);
+ }
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ void GetFrequencyResponse(const Float32Array& aFrequencyHz,
+ const Float32Array& aMagResponse,
+ const Float32Array& aPhaseResponse);
+
+ const char* NodeType() const override { return "IIRFilterNode"; }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override;
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override;
+
+ private:
+ IIRFilterNode(AudioContext* aContext, const Sequence<double>& aFeedforward,
+ const Sequence<double>& aFeedback);
+ ~IIRFilterNode() = default;
+
+ nsTArray<double> mFeedback;
+ nsTArray<double> mFeedforward;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webaudio/MediaBufferDecoder.cpp b/dom/media/webaudio/MediaBufferDecoder.cpp
new file mode 100644
index 0000000000..f0256659a0
--- /dev/null
+++ b/dom/media/webaudio/MediaBufferDecoder.cpp
@@ -0,0 +1,766 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaBufferDecoder.h"
+
+#include <speex/speex_resampler.h>
+
+#include "AudioBuffer.h"
+#include "AudioContext.h"
+#include "AudioNodeEngine.h"
+#include "BufferMediaResource.h"
+#include "DecoderTraits.h"
+#include "MediaContainerType.h"
+#include "MediaDataDecoderProxy.h"
+#include "MediaDataDemuxer.h"
+#include "MediaQueue.h"
+#include "PDMFactory.h"
+#include "VideoUtils.h"
+#include "WebAudioUtils.h"
+#include "js/MemoryFunctions.h"
+#include "mozilla/AbstractThread.h"
+#include "mozilla/Logging.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/dom/AudioContextBinding.h"
+#include "mozilla/dom/BaseAudioContextBinding.h"
+#include "mozilla/dom/DOMException.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "nsComponentManagerUtils.h"
+#include "nsContentUtils.h"
+#include "nsIScriptError.h"
+#include "nsIScriptObjectPrincipal.h"
+#include "nsMimeTypes.h"
+#include "nsPrintfCString.h"
+#include "nsXPCOMCIDInternal.h"
+
+namespace mozilla {
+
+extern LazyLogModule gMediaDecoderLog;
+
+#define LOG(x, ...) \
+ MOZ_LOG(gMediaDecoderLog, LogLevel::Debug, (x, ##__VA_ARGS__))
+
+using namespace dom;
+
+class ReportResultTask final : public Runnable {
+ public:
+ ReportResultTask(WebAudioDecodeJob& aDecodeJob,
+ WebAudioDecodeJob::ResultFn aFunction,
+ WebAudioDecodeJob::ErrorCode aErrorCode)
+ : Runnable("ReportResultTask"),
+ mDecodeJob(aDecodeJob),
+ mFunction(aFunction),
+ mErrorCode(aErrorCode) {
+ MOZ_ASSERT(aFunction);
+ }
+
+ NS_IMETHOD Run() override {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ (mDecodeJob.*mFunction)(mErrorCode);
+
+ return NS_OK;
+ }
+
+ private:
+ // Note that the mDecodeJob member will probably die when mFunction is run.
+ // Therefore, it is not safe to do anything fancy with it in this class.
+ // Really, this class is only used because nsRunnableMethod doesn't support
+ // methods accepting arguments.
+ WebAudioDecodeJob& mDecodeJob;
+ WebAudioDecodeJob::ResultFn mFunction;
+ WebAudioDecodeJob::ErrorCode mErrorCode;
+};
+
+enum class PhaseEnum : int { Decode, AllocateBuffer, Done };
+
+class MediaDecodeTask final : public Runnable {
+ public:
+ MediaDecodeTask(const MediaContainerType& aContainerType, uint8_t* aBuffer,
+ uint32_t aLength, WebAudioDecodeJob& aDecodeJob)
+ : Runnable("MediaDecodeTask"),
+ mContainerType(aContainerType),
+ mBuffer(aBuffer),
+ mLength(aLength),
+ mBatchSize(StaticPrefs::media_rdd_webaudio_batch_size()),
+ mDecodeJob(aDecodeJob),
+ mPhase(PhaseEnum::Decode) {
+ MOZ_ASSERT(aBuffer);
+ MOZ_ASSERT(NS_IsMainThread());
+ }
+
+ // MOZ_CAN_RUN_SCRIPT_BOUNDARY until Runnable::Run is MOZ_CAN_RUN_SCRIPT. See
+ // bug 1535398.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ NS_IMETHOD Run() override;
+ bool Init();
+ TaskQueue* PSupervisorTaskQueue() { return mPSupervisorTaskQueue; }
+ bool OnPSupervisorTaskQueue() const {
+ return mPSupervisorTaskQueue->IsCurrentThreadIn();
+ }
+
+ private:
+ MOZ_CAN_RUN_SCRIPT
+ void ReportFailureOnMainThread(WebAudioDecodeJob::ErrorCode aErrorCode) {
+ if (NS_IsMainThread()) {
+ Cleanup();
+ mDecodeJob.OnFailure(aErrorCode);
+ } else {
+ // Take extra care to cleanup on the main thread
+ mMainThread->Dispatch(NewRunnableMethod("MediaDecodeTask::Cleanup", this,
+ &MediaDecodeTask::Cleanup));
+
+ nsCOMPtr<nsIRunnable> event = new ReportResultTask(
+ mDecodeJob, &WebAudioDecodeJob::OnFailure, aErrorCode);
+ mMainThread->Dispatch(event.forget());
+ }
+ }
+
+ void Decode();
+
+ void OnCreateDecoderCompleted(RefPtr<MediaDataDecoder> aDecoder);
+ MOZ_CAN_RUN_SCRIPT void OnCreateDecoderFailed(const MediaResult& aError);
+
+ MOZ_CAN_RUN_SCRIPT void OnInitDemuxerCompleted();
+ MOZ_CAN_RUN_SCRIPT void OnInitDemuxerFailed(const MediaResult& aError);
+
+ void InitDecoder();
+ void OnInitDecoderCompleted();
+ MOZ_CAN_RUN_SCRIPT void OnInitDecoderFailed();
+
+ void DoDemux();
+ void OnAudioDemuxCompleted(RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples);
+ MOZ_CAN_RUN_SCRIPT void OnAudioDemuxFailed(const MediaResult& aError);
+
+ void DoDecode();
+ void OnAudioDecodeCompleted(MediaDataDecoder::DecodedData&& aResults);
+ MOZ_CAN_RUN_SCRIPT void OnAudioDecodeFailed(const MediaResult& aError);
+
+ void DoDrain();
+ MOZ_CAN_RUN_SCRIPT void OnAudioDrainCompleted(
+ MediaDataDecoder::DecodedData&& aResults);
+ MOZ_CAN_RUN_SCRIPT void OnAudioDrainFailed(const MediaResult& aError);
+
+ void ShutdownDecoder();
+
+ MOZ_CAN_RUN_SCRIPT void FinishDecode();
+ MOZ_CAN_RUN_SCRIPT void AllocateBuffer();
+ MOZ_CAN_RUN_SCRIPT void CallbackTheResult();
+
+ void Cleanup() {
+ MOZ_ASSERT(NS_IsMainThread());
+ JS_free(nullptr, mBuffer);
+ if (mTrackDemuxer) {
+ mTrackDemuxer->BreakCycles();
+ }
+ mTrackDemuxer = nullptr;
+ mDemuxer = nullptr;
+ mPSupervisorTaskQueue = nullptr;
+ mPDecoderTaskQueue = nullptr;
+ }
+
+ private:
+ MediaContainerType mContainerType;
+ uint8_t* mBuffer;
+ const uint32_t mLength;
+ const uint32_t mBatchSize;
+ WebAudioDecodeJob& mDecodeJob;
+ PhaseEnum mPhase;
+ RefPtr<TaskQueue> mPSupervisorTaskQueue;
+ RefPtr<TaskQueue> mPDecoderTaskQueue;
+ RefPtr<MediaDataDemuxer> mDemuxer;
+ RefPtr<MediaTrackDemuxer> mTrackDemuxer;
+ RefPtr<MediaDataDecoder> mDecoder;
+ nsTArray<RefPtr<MediaRawData>> mRawSamples;
+ MediaInfo mMediaInfo;
+ MediaQueue<AudioData> mAudioQueue;
+ RefPtr<AbstractThread> mMainThread;
+};
+
+NS_IMETHODIMP
+MediaDecodeTask::Run() {
+ switch (mPhase) {
+ case PhaseEnum::Decode:
+ Decode();
+ break;
+ case PhaseEnum::AllocateBuffer:
+ AllocateBuffer();
+ break;
+ case PhaseEnum::Done:
+ break;
+ }
+
+ return NS_OK;
+}
+
+bool MediaDecodeTask::Init() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<BufferMediaResource> resource =
+ new BufferMediaResource(static_cast<uint8_t*>(mBuffer), mLength);
+
+ mMainThread = mDecodeJob.mContext->GetOwnerGlobal()->AbstractMainThreadFor(
+ TaskCategory::Other);
+
+ mPSupervisorTaskQueue =
+ TaskQueue::Create(GetMediaThreadPool(MediaThreadType::SUPERVISOR),
+ "MediaBufferDecoder::mPSupervisorTaskQueue");
+ mPDecoderTaskQueue =
+ TaskQueue::Create(GetMediaThreadPool(MediaThreadType::PLATFORM_DECODER),
+ "MediaBufferDecoder::mPDecoderTaskQueue");
+
+ // If you change this list to add support for new decoders, please consider
+ // updating HTMLMediaElement::CreateDecoder as well.
+ mDemuxer = DecoderTraits::CreateDemuxer(mContainerType, resource);
+ if (!mDemuxer) {
+ return false;
+ }
+
+ return true;
+}
+
+class AutoResampler final {
+ public:
+ AutoResampler() : mResampler(nullptr) {}
+ ~AutoResampler() {
+ if (mResampler) {
+ speex_resampler_destroy(mResampler);
+ }
+ }
+ operator SpeexResamplerState*() const {
+ MOZ_ASSERT(mResampler);
+ return mResampler;
+ }
+ void operator=(SpeexResamplerState* aResampler) { mResampler = aResampler; }
+
+ private:
+ SpeexResamplerState* mResampler;
+};
+
+void MediaDecodeTask::Decode() {
+ MOZ_ASSERT(OnPSupervisorTaskQueue());
+
+ mDemuxer->Init()->Then(PSupervisorTaskQueue(), __func__, this,
+ &MediaDecodeTask::OnInitDemuxerCompleted,
+ &MediaDecodeTask::OnInitDemuxerFailed);
+}
+
+void MediaDecodeTask::OnInitDemuxerCompleted() {
+ MOZ_ASSERT(OnPSupervisorTaskQueue());
+
+ if (!!mDemuxer->GetNumberTracks(TrackInfo::kAudioTrack)) {
+ mTrackDemuxer = mDemuxer->GetTrackDemuxer(TrackInfo::kAudioTrack, 0);
+ if (!mTrackDemuxer) {
+ LOG("MediaDecodeTask: Could not get a track demuxer.");
+ ReportFailureOnMainThread(WebAudioDecodeJob::UnknownContent);
+ return;
+ }
+
+ RefPtr<PDMFactory> platform = new PDMFactory();
+ UniquePtr<TrackInfo> audioInfo = mTrackDemuxer->GetInfo();
+ // We actively ignore audio tracks that we know we can't play.
+ if (audioInfo && audioInfo->IsValid() &&
+ platform->SupportsMimeType(audioInfo->mMimeType) !=
+ media::DecodeSupport::Unsupported) {
+ mMediaInfo.mAudio = *audioInfo->GetAsAudioInfo();
+ }
+ }
+
+ RefPtr<PDMFactory> pdm = new PDMFactory();
+ pdm->CreateDecoder(
+ {*mMediaInfo.mAudio.GetAsAudioInfo(), TrackInfo::kAudioTrack})
+ ->Then(PSupervisorTaskQueue(), __func__, this,
+ &MediaDecodeTask::OnCreateDecoderCompleted,
+ &MediaDecodeTask::OnCreateDecoderFailed);
+}
+
+void MediaDecodeTask::OnCreateDecoderCompleted(
+ RefPtr<MediaDataDecoder> aDecoder) {
+ MOZ_ASSERT(OnPSupervisorTaskQueue());
+
+ mDecoder = new MediaDataDecoderProxy(aDecoder.forget(),
+ do_AddRef(mPDecoderTaskQueue.get()));
+ InitDecoder();
+}
+
+void MediaDecodeTask::OnCreateDecoderFailed(const MediaResult& aError) {
+ MOZ_ASSERT(OnPSupervisorTaskQueue());
+
+ LOG("MediaDecodeTask: Could not create a decoder.");
+ ReportFailureOnMainThread(WebAudioDecodeJob::UnknownContent);
+}
+
+void MediaDecodeTask::OnInitDemuxerFailed(const MediaResult& aError) {
+ MOZ_ASSERT(OnPSupervisorTaskQueue());
+
+ LOG("MediaDecodeTask: Could not initialize the demuxer.");
+ ReportFailureOnMainThread(WebAudioDecodeJob::InvalidContent);
+}
+
+void MediaDecodeTask::InitDecoder() {
+ MOZ_ASSERT(OnPSupervisorTaskQueue());
+
+ mDecoder->Init()->Then(PSupervisorTaskQueue(), __func__, this,
+ &MediaDecodeTask::OnInitDecoderCompleted,
+ &MediaDecodeTask::OnInitDecoderFailed);
+}
+
+void MediaDecodeTask::OnInitDecoderCompleted() {
+ MOZ_ASSERT(OnPSupervisorTaskQueue());
+
+ DoDemux();
+}
+
+void MediaDecodeTask::OnInitDecoderFailed() {
+ MOZ_ASSERT(OnPSupervisorTaskQueue());
+
+ ShutdownDecoder();
+ LOG("MediaDecodeTask: Could not initialize the decoder");
+ ReportFailureOnMainThread(WebAudioDecodeJob::InvalidContent);
+}
+
+void MediaDecodeTask::DoDemux() {
+ MOZ_ASSERT(OnPSupervisorTaskQueue());
+
+ mTrackDemuxer->GetSamples(mBatchSize)
+ ->Then(PSupervisorTaskQueue(), __func__, this,
+ &MediaDecodeTask::OnAudioDemuxCompleted,
+ &MediaDecodeTask::OnAudioDemuxFailed);
+}
+
+void MediaDecodeTask::OnAudioDemuxCompleted(
+ RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples) {
+ MOZ_ASSERT(OnPSupervisorTaskQueue());
+
+ mRawSamples.AppendElements(aSamples->GetSamples());
+
+ DoDemux();
+}
+
+void MediaDecodeTask::OnAudioDemuxFailed(const MediaResult& aError) {
+ MOZ_ASSERT(OnPSupervisorTaskQueue());
+
+ if (aError.Code() == NS_ERROR_DOM_MEDIA_END_OF_STREAM) {
+ DoDecode();
+ } else {
+ ShutdownDecoder();
+ LOG("MediaDecodeTask: Audio demux failed");
+ ReportFailureOnMainThread(WebAudioDecodeJob::InvalidContent);
+ }
+}
+
+void MediaDecodeTask::DoDecode() {
+ MOZ_ASSERT(OnPSupervisorTaskQueue());
+
+ if (mRawSamples.IsEmpty()) {
+ DoDrain();
+ return;
+ }
+
+ if (mBatchSize > 1 && mDecoder->CanDecodeBatch()) {
+ nsTArray<RefPtr<MediaRawData>> rawSampleBatch;
+ const int batchSize = std::min((unsigned long)mBatchSize,
+ (unsigned long)mRawSamples.Length());
+ for (int i = 0; i < batchSize; ++i) {
+ rawSampleBatch.AppendElement(std::move(mRawSamples[i]));
+ }
+
+ mDecoder->DecodeBatch(std::move(rawSampleBatch))
+ ->Then(PSupervisorTaskQueue(), __func__, this,
+ &MediaDecodeTask::OnAudioDecodeCompleted,
+ &MediaDecodeTask::OnAudioDecodeFailed);
+
+ mRawSamples.RemoveElementsAt(0, batchSize);
+ } else {
+ RefPtr<MediaRawData> sample = std::move(mRawSamples[0]);
+
+ mDecoder->Decode(sample)->Then(PSupervisorTaskQueue(), __func__, this,
+ &MediaDecodeTask::OnAudioDecodeCompleted,
+ &MediaDecodeTask::OnAudioDecodeFailed);
+
+ mRawSamples.RemoveElementAt(0);
+ }
+}
+
+void MediaDecodeTask::OnAudioDecodeCompleted(
+ MediaDataDecoder::DecodedData&& aResults) {
+ MOZ_ASSERT(OnPSupervisorTaskQueue());
+
+ for (auto&& sample : aResults) {
+ MOZ_ASSERT(sample->mType == MediaData::Type::AUDIO_DATA);
+ RefPtr<AudioData> audioData = sample->As<AudioData>();
+
+ mMediaInfo.mAudio.mRate = audioData->mRate;
+ mMediaInfo.mAudio.mChannels = audioData->mChannels;
+
+ mAudioQueue.Push(audioData.forget());
+ }
+
+ DoDecode();
+}
+
+void MediaDecodeTask::OnAudioDecodeFailed(const MediaResult& aError) {
+ MOZ_ASSERT(OnPSupervisorTaskQueue());
+
+ ShutdownDecoder();
+ LOG("MediaDecodeTask: decode audio failed.");
+ ReportFailureOnMainThread(WebAudioDecodeJob::InvalidContent);
+}
+
+void MediaDecodeTask::DoDrain() {
+ MOZ_ASSERT(OnPSupervisorTaskQueue());
+
+ mDecoder->Drain()->Then(PSupervisorTaskQueue(), __func__, this,
+ &MediaDecodeTask::OnAudioDrainCompleted,
+ &MediaDecodeTask::OnAudioDrainFailed);
+}
+
+void MediaDecodeTask::OnAudioDrainCompleted(
+ MediaDataDecoder::DecodedData&& aResults) {
+ MOZ_ASSERT(OnPSupervisorTaskQueue());
+
+ if (aResults.IsEmpty()) {
+ FinishDecode();
+ return;
+ }
+
+ for (auto&& sample : aResults) {
+ MOZ_ASSERT(sample->mType == MediaData::Type::AUDIO_DATA);
+ RefPtr<AudioData> audioData = sample->As<AudioData>();
+
+ mAudioQueue.Push(audioData.forget());
+ }
+ DoDrain();
+}
+
+void MediaDecodeTask::OnAudioDrainFailed(const MediaResult& aError) {
+ MOZ_ASSERT(OnPSupervisorTaskQueue());
+
+ ShutdownDecoder();
+ LOG("MediaDecodeTask: Drain audio failed");
+ ReportFailureOnMainThread(WebAudioDecodeJob::InvalidContent);
+}
+
+void MediaDecodeTask::ShutdownDecoder() {
+ MOZ_ASSERT(OnPSupervisorTaskQueue());
+
+ if (!mDecoder) {
+ return;
+ }
+
+ RefPtr<MediaDecodeTask> self = this;
+ mDecoder->Shutdown();
+ mDecoder = nullptr;
+}
+
+void MediaDecodeTask::FinishDecode() {
+ MOZ_ASSERT(OnPSupervisorTaskQueue());
+
+ ShutdownDecoder();
+
+ uint32_t frameCount = mAudioQueue.AudioFramesCount();
+ uint32_t channelCount = mMediaInfo.mAudio.mChannels;
+ uint32_t sampleRate = mMediaInfo.mAudio.mRate;
+
+ if (!frameCount || !channelCount || !sampleRate) {
+ LOG("MediaDecodeTask: invalid content frame count, channel count or "
+ "sample-rate");
+ ReportFailureOnMainThread(WebAudioDecodeJob::InvalidContent);
+ return;
+ }
+
+ const uint32_t destSampleRate = mDecodeJob.mContext->SampleRate();
+ AutoResampler resampler;
+
+ uint32_t resampledFrames = frameCount;
+ if (sampleRate != destSampleRate) {
+ resampledFrames = static_cast<uint32_t>(
+ static_cast<uint64_t>(destSampleRate) *
+ static_cast<uint64_t>(frameCount) / static_cast<uint64_t>(sampleRate));
+
+ resampler = speex_resampler_init(channelCount, sampleRate, destSampleRate,
+ SPEEX_RESAMPLER_QUALITY_DEFAULT, nullptr);
+ speex_resampler_skip_zeros(resampler);
+ resampledFrames += speex_resampler_get_output_latency(resampler);
+ }
+
+ // Allocate contiguous channel buffers. Note that if we end up resampling,
+ // we may write fewer bytes than mResampledFrames to the output buffer, in
+ // which case writeIndex will tell us how many valid samples we have.
+ mDecodeJob.mBuffer.mChannelData.SetLength(channelCount);
+#if AUDIO_OUTPUT_FORMAT == AUDIO_FORMAT_FLOAT32
+ // This buffer has separate channel arrays that could be transferred to
+ // JS::NewArrayBufferWithContents(), but AudioBuffer::RestoreJSChannelData()
+ // does not yet take advantage of this.
+ RefPtr<ThreadSharedFloatArrayBufferList> buffer =
+ ThreadSharedFloatArrayBufferList::Create(channelCount, resampledFrames,
+ fallible);
+ if (!buffer) {
+ LOG("MediaDecodeTask: Could not create final buffer (f32)");
+ ReportFailureOnMainThread(WebAudioDecodeJob::UnknownError);
+ return;
+ }
+ for (uint32_t i = 0; i < channelCount; ++i) {
+ mDecodeJob.mBuffer.mChannelData[i] = buffer->GetData(i);
+ }
+#else
+ CheckedInt<size_t> bufferSize(sizeof(AudioDataValue));
+ bufferSize *= resampledFrames;
+ bufferSize *= channelCount;
+ RefPtr<SharedBuffer> buffer = SharedBuffer::Create(bufferSize);
+ if (!buffer) {
+ LOG("MediaDecodeTask: Could not create final buffer (i16)");
+ ReportFailureOnMainThread(WebAudioDecodeJob::UnknownError);
+ return;
+ }
+ auto data = static_cast<AudioDataValue*>(floatBuffer->Data());
+ for (uint32_t i = 0; i < channelCount; ++i) {
+ mDecodeJob.mBuffer.mChannelData[i] = data;
+ data += resampledFrames;
+ }
+#endif
+ mDecodeJob.mBuffer.mBuffer = std::move(buffer);
+ mDecodeJob.mBuffer.mVolume = 1.0f;
+ mDecodeJob.mBuffer.mBufferFormat = AUDIO_OUTPUT_FORMAT;
+
+ uint32_t writeIndex = 0;
+ RefPtr<AudioData> audioData;
+ while ((audioData = mAudioQueue.PopFront())) {
+ if (!audioData->Frames()) {
+ // The packet contains no audio frames, skip it.
+ continue;
+ }
+ audioData->EnsureAudioBuffer(); // could lead to a copy :(
+ const AudioDataValue* bufferData =
+ static_cast<AudioDataValue*>(audioData->mAudioBuffer->Data());
+
+ if (sampleRate != destSampleRate) {
+ const uint32_t maxOutSamples = resampledFrames - writeIndex;
+
+ for (uint32_t i = 0; i < audioData->mChannels; ++i) {
+ uint32_t inSamples = audioData->Frames();
+ uint32_t outSamples = maxOutSamples;
+ AudioDataValue* outData =
+ mDecodeJob.mBuffer.ChannelDataForWrite<AudioDataValue>(i) +
+ writeIndex;
+
+ WebAudioUtils::SpeexResamplerProcess(
+ resampler, i, &bufferData[i * audioData->Frames()], &inSamples,
+ outData, &outSamples);
+
+ if (i == audioData->mChannels - 1) {
+ writeIndex += outSamples;
+ MOZ_ASSERT(writeIndex <= resampledFrames);
+ MOZ_ASSERT(inSamples == audioData->Frames());
+ }
+ }
+ } else {
+ for (uint32_t i = 0; i < audioData->mChannels; ++i) {
+ AudioDataValue* outData =
+ mDecodeJob.mBuffer.ChannelDataForWrite<AudioDataValue>(i) +
+ writeIndex;
+ PodCopy(outData, &bufferData[i * audioData->Frames()],
+ audioData->Frames());
+
+ if (i == audioData->mChannels - 1) {
+ writeIndex += audioData->Frames();
+ }
+ }
+ }
+ }
+
+ if (sampleRate != destSampleRate) {
+ uint32_t inputLatency = speex_resampler_get_input_latency(resampler);
+ const uint32_t maxOutSamples = resampledFrames - writeIndex;
+ for (uint32_t i = 0; i < channelCount; ++i) {
+ uint32_t inSamples = inputLatency;
+ uint32_t outSamples = maxOutSamples;
+ AudioDataValue* outData =
+ mDecodeJob.mBuffer.ChannelDataForWrite<AudioDataValue>(i) +
+ writeIndex;
+
+ WebAudioUtils::SpeexResamplerProcess(resampler, i,
+ (AudioDataValue*)nullptr, &inSamples,
+ outData, &outSamples);
+
+ if (i == channelCount - 1) {
+ writeIndex += outSamples;
+ MOZ_ASSERT(writeIndex <= resampledFrames);
+ MOZ_ASSERT(inSamples == inputLatency);
+ }
+ }
+ }
+
+ mDecodeJob.mBuffer.mDuration = writeIndex;
+ mPhase = PhaseEnum::AllocateBuffer;
+ mMainThread->Dispatch(do_AddRef(this));
+}
+
+void MediaDecodeTask::AllocateBuffer() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mDecodeJob.AllocateBuffer()) {
+ LOG("MediaDecodeTask: Could not allocate final buffer");
+ ReportFailureOnMainThread(WebAudioDecodeJob::UnknownError);
+ return;
+ }
+
+ mPhase = PhaseEnum::Done;
+ CallbackTheResult();
+}
+
+void MediaDecodeTask::CallbackTheResult() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ Cleanup();
+
+ // Now, we're ready to call the script back with the resulting buffer
+ mDecodeJob.OnSuccess(WebAudioDecodeJob::NoError);
+}
+
+bool WebAudioDecodeJob::AllocateBuffer() {
+ MOZ_ASSERT(!mOutput);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Now create the AudioBuffer
+ mOutput = AudioBuffer::Create(mContext->GetOwner(), mContext->SampleRate(),
+ std::move(mBuffer));
+ return mOutput != nullptr;
+}
+
+void AsyncDecodeWebAudio(const char* aContentType, uint8_t* aBuffer,
+ uint32_t aLength, WebAudioDecodeJob& aDecodeJob) {
+ Maybe<MediaContainerType> containerType =
+ MakeMediaContainerType(aContentType);
+ // Do not attempt to decode the media if we were not successful at sniffing
+ // the container type.
+ if (!*aContentType || strcmp(aContentType, APPLICATION_OCTET_STREAM) == 0 ||
+ !containerType) {
+ nsCOMPtr<nsIRunnable> event =
+ new ReportResultTask(aDecodeJob, &WebAudioDecodeJob::OnFailure,
+ WebAudioDecodeJob::UnknownContent);
+ JS_free(nullptr, aBuffer);
+ aDecodeJob.mContext->Dispatch(event.forget());
+ return;
+ }
+
+ RefPtr<MediaDecodeTask> task =
+ new MediaDecodeTask(*containerType, aBuffer, aLength, aDecodeJob);
+ if (!task->Init()) {
+ nsCOMPtr<nsIRunnable> event =
+ new ReportResultTask(aDecodeJob, &WebAudioDecodeJob::OnFailure,
+ WebAudioDecodeJob::UnknownError);
+ aDecodeJob.mContext->Dispatch(event.forget());
+ } else {
+ nsresult rv = task->PSupervisorTaskQueue()->Dispatch(task.forget());
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ }
+}
+
+WebAudioDecodeJob::WebAudioDecodeJob(AudioContext* aContext, Promise* aPromise,
+ DecodeSuccessCallback* aSuccessCallback,
+ DecodeErrorCallback* aFailureCallback)
+ : mContext(aContext),
+ mPromise(aPromise),
+ mSuccessCallback(aSuccessCallback),
+ mFailureCallback(aFailureCallback) {
+ MOZ_ASSERT(aContext);
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_COUNT_CTOR(WebAudioDecodeJob);
+}
+
+WebAudioDecodeJob::~WebAudioDecodeJob() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_COUNT_DTOR(WebAudioDecodeJob);
+}
+
+void WebAudioDecodeJob::OnSuccess(ErrorCode aErrorCode) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aErrorCode == NoError);
+
+ RefPtr<AudioBuffer> output(mOutput);
+ if (mSuccessCallback) {
+ RefPtr<DecodeSuccessCallback> callback(mSuccessCallback);
+ // Ignore errors in calling the callback, since there is not much that we
+ // can do about it here.
+ callback->Call(*output);
+ }
+ mPromise->MaybeResolve(output);
+
+ mContext->RemoveFromDecodeQueue(this);
+}
+
+void WebAudioDecodeJob::OnFailure(ErrorCode aErrorCode) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ const char* errorMessage;
+ switch (aErrorCode) {
+ case UnknownContent:
+ errorMessage =
+ "The buffer passed to decodeAudioData contains an unknown content "
+ "type.";
+ break;
+ case InvalidContent:
+ errorMessage =
+ "The buffer passed to decodeAudioData contains invalid content which "
+ "cannot be decoded successfully.";
+ break;
+ case NoAudio:
+ errorMessage =
+ "The buffer passed to decodeAudioData does not contain any audio.";
+ break;
+ case NoError:
+ MOZ_FALLTHROUGH_ASSERT("Who passed NoError to OnFailure?");
+ // Fall through to get some sort of a sane error message if this actually
+ // happens at runtime.
+ case UnknownError:
+ [[fallthrough]];
+ default:
+ errorMessage =
+ "An unknown error occurred while processing decodeAudioData.";
+ break;
+ }
+
+ // Ignore errors in calling the callback, since there is not much that we can
+ // do about it here.
+ nsAutoCString errorString(errorMessage);
+ if (mFailureCallback) {
+ RefPtr<DOMException> exception = DOMException::Create(
+ NS_ERROR_DOM_ENCODING_NOT_SUPPORTED_ERR, errorString);
+ RefPtr<DecodeErrorCallback> callback(mFailureCallback);
+ callback->Call(*exception);
+ }
+
+ mPromise->MaybeRejectWithEncodingError(errorString);
+
+ mContext->RemoveFromDecodeQueue(this);
+}
+
+size_t WebAudioDecodeJob::SizeOfExcludingThis(
+ MallocSizeOf aMallocSizeOf) const {
+ size_t amount = 0;
+ if (mSuccessCallback) {
+ amount += mSuccessCallback->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ if (mFailureCallback) {
+ amount += mFailureCallback->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ if (mOutput) {
+ amount += mOutput->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ amount += mBuffer.SizeOfExcludingThis(aMallocSizeOf, false);
+ return amount;
+}
+
+size_t WebAudioDecodeJob::SizeOfIncludingThis(
+ MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
+
+} // namespace mozilla
diff --git a/dom/media/webaudio/MediaBufferDecoder.h b/dom/media/webaudio/MediaBufferDecoder.h
new file mode 100644
index 0000000000..7af1acfd06
--- /dev/null
+++ b/dom/media/webaudio/MediaBufferDecoder.h
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MediaBufferDecoder_h_
+#define MediaBufferDecoder_h_
+
+#include "AudioSegment.h"
+#include "nsWrapperCache.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/TypedArray.h"
+#include "mozilla/MemoryReporting.h"
+
+namespace mozilla {
+
+class ThreadSharedFloatArrayBufferList;
+
+namespace dom {
+class AudioBuffer;
+class AudioContext;
+class DecodeErrorCallback;
+class DecodeSuccessCallback;
+class Promise;
+} // namespace dom
+
+struct WebAudioDecodeJob final {
+ // You may omit both the success and failure callback, or you must pass both.
+ // The callbacks are only necessary for asynchronous operation.
+ WebAudioDecodeJob(dom::AudioContext* aContext, dom::Promise* aPromise,
+ dom::DecodeSuccessCallback* aSuccessCallback = nullptr,
+ dom::DecodeErrorCallback* aFailureCallback = nullptr);
+ ~WebAudioDecodeJob();
+
+ enum ErrorCode {
+ NoError,
+ UnknownContent,
+ UnknownError,
+ InvalidContent,
+ NoAudio
+ };
+
+ typedef void (WebAudioDecodeJob::*ResultFn)(ErrorCode);
+
+ MOZ_CAN_RUN_SCRIPT void OnSuccess(ErrorCode /* ignored */);
+ MOZ_CAN_RUN_SCRIPT void OnFailure(ErrorCode aErrorCode);
+
+ bool AllocateBuffer();
+
+ size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ AudioChunk mBuffer;
+ RefPtr<dom::AudioContext> mContext;
+ RefPtr<dom::Promise> mPromise;
+ RefPtr<dom::DecodeSuccessCallback> mSuccessCallback;
+ RefPtr<dom::DecodeErrorCallback> mFailureCallback; // can be null
+ RefPtr<dom::AudioBuffer> mOutput;
+};
+
+void AsyncDecodeWebAudio(const char* aContentType, uint8_t* aBuffer,
+ uint32_t aLength, WebAudioDecodeJob& aDecodeJob);
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webaudio/MediaElementAudioSourceNode.cpp b/dom/media/webaudio/MediaElementAudioSourceNode.cpp
new file mode 100644
index 0000000000..d02ef2f4de
--- /dev/null
+++ b/dom/media/webaudio/MediaElementAudioSourceNode.cpp
@@ -0,0 +1,105 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaElementAudioSourceNode.h"
+#include "mozilla/dom/MediaElementAudioSourceNodeBinding.h"
+#include "AudioDestinationNode.h"
+#include "AudioNodeTrack.h"
+#include "MediaStreamTrack.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(MediaElementAudioSourceNode)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(MediaElementAudioSourceNode)
+ tmp->Destroy();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mElement)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END_INHERITED(AudioNode)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(MediaElementAudioSourceNode,
+ MediaStreamAudioSourceNode)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mElement)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaElementAudioSourceNode)
+NS_INTERFACE_MAP_END_INHERITING(MediaStreamAudioSourceNode)
+
+NS_IMPL_ADDREF_INHERITED(MediaElementAudioSourceNode,
+ MediaStreamAudioSourceNode)
+NS_IMPL_RELEASE_INHERITED(MediaElementAudioSourceNode,
+ MediaStreamAudioSourceNode)
+
+MediaElementAudioSourceNode::MediaElementAudioSourceNode(
+ AudioContext* aContext, HTMLMediaElement* aElement)
+ : MediaStreamAudioSourceNode(aContext, TrackChangeBehavior::FollowChanges),
+ mElement(aElement) {
+ MOZ_ASSERT(aElement);
+}
+
+/* static */
+already_AddRefed<MediaElementAudioSourceNode>
+MediaElementAudioSourceNode::Create(
+ AudioContext& aAudioContext, const MediaElementAudioSourceOptions& aOptions,
+ ErrorResult& aRv) {
+ // The spec has a pointless check here. See
+ // https://github.com/WebAudio/web-audio-api/issues/2149
+ MOZ_RELEASE_ASSERT(!aAudioContext.IsOffline(), "Bindings messed up?");
+
+ RefPtr<MediaElementAudioSourceNode> node =
+ new MediaElementAudioSourceNode(&aAudioContext, aOptions.mMediaElement);
+
+ RefPtr<DOMMediaStream> stream = aOptions.mMediaElement->CaptureAudio(
+ aRv, aAudioContext.Destination()->Track()->Graph());
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+ MOZ_ASSERT(stream, "CaptureAudio should report failure via aRv!");
+
+ node->Init(*stream, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ node->ListenForAllowedToPlay(aOptions);
+ return node.forget();
+}
+
+JSObject* MediaElementAudioSourceNode::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return MediaElementAudioSourceNode_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void MediaElementAudioSourceNode::ListenForAllowedToPlay(
+ const MediaElementAudioSourceOptions& aOptions) {
+ if (!GetAbstractMainThread()) {
+ // The AudioContext must have been closed. It won't be able to start anyway.
+ return;
+ }
+
+ aOptions.mMediaElement->GetAllowedToPlayPromise()
+ ->Then(
+ GetAbstractMainThread(), __func__,
+ // Capture by reference to bypass the mozilla-refcounted-inside-lambda
+ // static analysis. We capture a non-owning reference so as to allow
+ // cycle collection of the node. The reference is cleared via
+ // DisconnectIfExists() from Destroy() when the node is collected.
+ [&self = *this]() {
+ self.Context()->StartBlockedAudioContextIfAllowed();
+ self.mAllowedToPlayRequest.Complete();
+ })
+ ->Track(mAllowedToPlayRequest);
+}
+
+void MediaElementAudioSourceNode::Destroy() {
+ mAllowedToPlayRequest.DisconnectIfExists();
+ MediaStreamAudioSourceNode::Destroy();
+}
+
+HTMLMediaElement* MediaElementAudioSourceNode::MediaElement() {
+ return mElement;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/MediaElementAudioSourceNode.h b/dom/media/webaudio/MediaElementAudioSourceNode.h
new file mode 100644
index 0000000000..6ba21279fe
--- /dev/null
+++ b/dom/media/webaudio/MediaElementAudioSourceNode.h
@@ -0,0 +1,67 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MediaElementAudioSourceNode_h_
+#define MediaElementAudioSourceNode_h_
+
+#include "MediaStreamAudioSourceNode.h"
+
+namespace mozilla::dom {
+
+class AudioContext;
+struct MediaElementAudioSourceOptions;
+
+class MediaElementAudioSourceNode final : public MediaStreamAudioSourceNode {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MediaElementAudioSourceNode,
+ MediaStreamAudioSourceNode)
+ static already_AddRefed<MediaElementAudioSourceNode> Create(
+ AudioContext& aAudioContext,
+ const MediaElementAudioSourceOptions& aOptions, ErrorResult& aRv);
+
+ static already_AddRefed<MediaElementAudioSourceNode> Constructor(
+ const GlobalObject& aGlobal, AudioContext& aAudioContext,
+ const MediaElementAudioSourceOptions& aOptions, ErrorResult& aRv) {
+ return Create(aAudioContext, aOptions, aRv);
+ }
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ const char* NodeType() const override {
+ return "MediaElementAudioSourceNode";
+ }
+
+ const char* CrossOriginErrorString() const override {
+ return "MediaElementAudioSourceNodeCrossOrigin";
+ }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ HTMLMediaElement* MediaElement();
+
+ private:
+ explicit MediaElementAudioSourceNode(AudioContext* aContext,
+ HTMLMediaElement* aElement);
+ ~MediaElementAudioSourceNode() = default;
+
+ void Destroy() override;
+
+ // If AudioContext was not allowed to start, we would try to start it when
+ // source starts.
+ void ListenForAllowedToPlay(const MediaElementAudioSourceOptions& aOptions);
+
+ MozPromiseRequestHolder<GenericNonExclusivePromise> mAllowedToPlayRequest;
+
+ RefPtr<HTMLMediaElement> mElement;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webaudio/MediaStreamAudioDestinationNode.cpp b/dom/media/webaudio/MediaStreamAudioDestinationNode.cpp
new file mode 100644
index 0000000000..704adaa304
--- /dev/null
+++ b/dom/media/webaudio/MediaStreamAudioDestinationNode.cpp
@@ -0,0 +1,150 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaStreamAudioDestinationNode.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/MediaStreamAudioDestinationNodeBinding.h"
+#include "AudioNodeEngine.h"
+#include "AudioNodeTrack.h"
+#include "AudioStreamTrack.h"
+#include "DOMMediaStream.h"
+#include "ForwardedInputTrack.h"
+
+namespace mozilla::dom {
+
+class AudioDestinationTrackSource final : public MediaStreamTrackSource {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(AudioDestinationTrackSource,
+ MediaStreamTrackSource)
+
+ AudioDestinationTrackSource(MediaStreamAudioDestinationNode* aNode,
+ mozilla::MediaTrack* aInputTrack,
+ ProcessedMediaTrack* aTrack,
+ nsIPrincipal* aPrincipal)
+ : MediaStreamTrackSource(
+ aPrincipal, nsString(),
+ // We pass 0 here because tracking ids are video only for now.
+ TrackingId(TrackingId::Source::AudioDestinationNode, 0)),
+ mTrack(aTrack),
+ mPort(mTrack->AllocateInputPort(aInputTrack)),
+ mNode(aNode) {}
+
+ void Destroy() override {
+ if (!mTrack->IsDestroyed()) {
+ mTrack->Destroy();
+ mPort->Destroy();
+ }
+ if (mNode) {
+ mNode->DestroyMediaTrack();
+ mNode = nullptr;
+ }
+ }
+
+ MediaSourceEnum GetMediaSource() const override {
+ return MediaSourceEnum::AudioCapture;
+ }
+
+ void Stop() override { Destroy(); }
+
+ void Disable() override {}
+
+ void Enable() override {}
+
+ const RefPtr<ProcessedMediaTrack> mTrack;
+ const RefPtr<MediaInputPort> mPort;
+
+ private:
+ ~AudioDestinationTrackSource() = default;
+
+ RefPtr<MediaStreamAudioDestinationNode> mNode;
+};
+
+NS_IMPL_ADDREF_INHERITED(AudioDestinationTrackSource, MediaStreamTrackSource)
+NS_IMPL_RELEASE_INHERITED(AudioDestinationTrackSource, MediaStreamTrackSource)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(AudioDestinationTrackSource)
+NS_INTERFACE_MAP_END_INHERITING(MediaStreamTrackSource)
+NS_IMPL_CYCLE_COLLECTION_INHERITED(AudioDestinationTrackSource,
+ MediaStreamTrackSource, mNode)
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(MediaStreamAudioDestinationNode, AudioNode,
+ mDOMStream)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaStreamAudioDestinationNode)
+NS_INTERFACE_MAP_END_INHERITING(AudioNode)
+
+NS_IMPL_ADDREF_INHERITED(MediaStreamAudioDestinationNode, AudioNode)
+NS_IMPL_RELEASE_INHERITED(MediaStreamAudioDestinationNode, AudioNode)
+
+MediaStreamAudioDestinationNode::MediaStreamAudioDestinationNode(
+ AudioContext* aContext)
+ : AudioNode(aContext, 2, ChannelCountMode::Explicit,
+ ChannelInterpretation::Speakers),
+ mDOMStream(MakeAndAddRef<DOMMediaStream>(GetOwner())) {
+ // Ensure an audio track with the correct ID is exposed to JS. If we can't get
+ // a principal here because the document is not available, pass in a null
+ // principal. This happens in edge cases when the document is being unloaded
+ // and it does not matter too much to have something working as long as it's
+ // not dangerous.
+ nsCOMPtr<nsIPrincipal> principal = nullptr;
+ if (aContext->GetParentObject()) {
+ Document* doc = aContext->GetParentObject()->GetExtantDoc();
+ principal = doc->NodePrincipal();
+ }
+ mTrack = AudioNodeTrack::Create(aContext, new AudioNodeEngine(this),
+ AudioNodeTrack::EXTERNAL_OUTPUT,
+ aContext->Graph());
+ auto source = MakeRefPtr<AudioDestinationTrackSource>(
+ this, mTrack,
+ aContext->Graph()->CreateForwardedInputTrack(MediaSegment::AUDIO),
+ principal);
+ auto track = MakeRefPtr<AudioStreamTrack>(GetOwner(), source->mTrack, source);
+ mDOMStream->AddTrackInternal(track);
+}
+
+/* static */
+already_AddRefed<MediaStreamAudioDestinationNode>
+MediaStreamAudioDestinationNode::Create(AudioContext& aAudioContext,
+ const AudioNodeOptions& aOptions,
+ ErrorResult& aRv) {
+ // The spec has a pointless check here. See
+ // https://github.com/WebAudio/web-audio-api/issues/2149
+ MOZ_RELEASE_ASSERT(!aAudioContext.IsOffline(), "Bindings messed up?");
+
+ RefPtr<MediaStreamAudioDestinationNode> audioNode =
+ new MediaStreamAudioDestinationNode(&aAudioContext);
+
+ audioNode->Initialize(aOptions, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ return audioNode.forget();
+}
+
+size_t MediaStreamAudioDestinationNode::SizeOfExcludingThis(
+ MallocSizeOf aMallocSizeOf) const {
+ // Future:
+ // - mDOMStream
+ size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf);
+ return amount;
+}
+
+size_t MediaStreamAudioDestinationNode::SizeOfIncludingThis(
+ MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
+
+void MediaStreamAudioDestinationNode::DestroyMediaTrack() {
+ AudioNode::DestroyMediaTrack();
+}
+
+JSObject* MediaStreamAudioDestinationNode::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return MediaStreamAudioDestinationNode_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/MediaStreamAudioDestinationNode.h b/dom/media/webaudio/MediaStreamAudioDestinationNode.h
new file mode 100644
index 0000000000..24923955df
--- /dev/null
+++ b/dom/media/webaudio/MediaStreamAudioDestinationNode.h
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MediaStreamAudioDestinationNode_h_
+#define MediaStreamAudioDestinationNode_h_
+
+#include "AudioNode.h"
+
+namespace mozilla::dom {
+
+class AudioContext;
+struct AudioNodeOptions;
+
+class MediaStreamAudioDestinationNode final : public AudioNode {
+ public:
+ static already_AddRefed<MediaStreamAudioDestinationNode> Create(
+ AudioContext& aAudioContext, const AudioNodeOptions& aOptions,
+ ErrorResult& aRv);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MediaStreamAudioDestinationNode,
+ AudioNode)
+
+ static already_AddRefed<MediaStreamAudioDestinationNode> Constructor(
+ const GlobalObject& aGlobal, AudioContext& aAudioContext,
+ const AudioNodeOptions& aOptions, ErrorResult& aRv) {
+ return Create(aAudioContext, aOptions, aRv);
+ }
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ uint16_t NumberOfOutputs() const final { return 0; }
+
+ void DestroyMediaTrack() override;
+
+ DOMMediaStream* DOMStream() const { return mDOMStream; }
+
+ const char* NodeType() const override {
+ return "MediaStreamAudioDestinationNode";
+ }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override;
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override;
+
+ private:
+ explicit MediaStreamAudioDestinationNode(AudioContext* aContext);
+ ~MediaStreamAudioDestinationNode() = default;
+
+ RefPtr<DOMMediaStream> mDOMStream;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webaudio/MediaStreamAudioSourceNode.cpp b/dom/media/webaudio/MediaStreamAudioSourceNode.cpp
new file mode 100644
index 0000000000..a915e78859
--- /dev/null
+++ b/dom/media/webaudio/MediaStreamAudioSourceNode.cpp
@@ -0,0 +1,278 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaStreamAudioSourceNode.h"
+#include "mozilla/dom/MediaStreamAudioSourceNodeBinding.h"
+#include "AudioNodeEngine.h"
+#include "AudioNodeExternalInputTrack.h"
+#include "AudioStreamTrack.h"
+#include "mozilla/dom/Document.h"
+#include "nsContentUtils.h"
+#include "nsIScriptError.h"
+#include "nsID.h"
+#include "Tracing.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(MediaStreamAudioSourceNode)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(MediaStreamAudioSourceNode)
+ tmp->Destroy();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mInputStream)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mInputTrack)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END_INHERITED(AudioNode)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(MediaStreamAudioSourceNode,
+ AudioNode)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInputStream)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInputTrack)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaStreamAudioSourceNode)
+NS_INTERFACE_MAP_END_INHERITING(AudioNode)
+
+NS_IMPL_ADDREF_INHERITED(MediaStreamAudioSourceNode, AudioNode)
+NS_IMPL_RELEASE_INHERITED(MediaStreamAudioSourceNode, AudioNode)
+
+MediaStreamAudioSourceNode::MediaStreamAudioSourceNode(
+ AudioContext* aContext, TrackChangeBehavior aBehavior)
+ : AudioNode(aContext, 2, ChannelCountMode::Max,
+ ChannelInterpretation::Speakers),
+ mBehavior(aBehavior) {}
+
+/* static */
+already_AddRefed<MediaStreamAudioSourceNode> MediaStreamAudioSourceNode::Create(
+ AudioContext& aAudioContext, const MediaStreamAudioSourceOptions& aOptions,
+ ErrorResult& aRv) {
+ // The spec has a pointless check here. See
+ // https://github.com/WebAudio/web-audio-api/issues/2149
+ MOZ_RELEASE_ASSERT(!aAudioContext.IsOffline(), "Bindings messed up?");
+
+ RefPtr<MediaStreamAudioSourceNode> node =
+ new MediaStreamAudioSourceNode(&aAudioContext, LockOnTrackPicked);
+
+ // aOptions.mMediaStream is not nullable.
+ node->Init(*aOptions.mMediaStream, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ return node.forget();
+}
+
+void MediaStreamAudioSourceNode::Init(DOMMediaStream& aMediaStream,
+ ErrorResult& aRv) {
+ mInputStream = &aMediaStream;
+ AudioNodeEngine* engine = new MediaStreamAudioSourceNodeEngine(this);
+ mTrack = AudioNodeExternalInputTrack::Create(Context()->Graph(), engine);
+ mInputStream->AddConsumerToKeepAlive(ToSupports(this));
+
+ mInputStream->RegisterTrackListener(this);
+ if (mInputStream->Audible()) {
+ NotifyAudible();
+ }
+ AttachToRightTrack(mInputStream, aRv);
+}
+
+void MediaStreamAudioSourceNode::Destroy() {
+ if (mInputStream) {
+ mInputStream->UnregisterTrackListener(this);
+ mInputStream = nullptr;
+ }
+ DetachFromTrack();
+}
+
+MediaStreamAudioSourceNode::~MediaStreamAudioSourceNode() { Destroy(); }
+
+void MediaStreamAudioSourceNode::AttachToTrack(
+ const RefPtr<MediaStreamTrack>& aTrack, ErrorResult& aRv) {
+ MOZ_ASSERT(!mInputTrack);
+ MOZ_ASSERT(aTrack->AsAudioStreamTrack());
+ MOZ_DIAGNOSTIC_ASSERT(!aTrack->Ended());
+
+ if (!mTrack) {
+ return;
+ }
+
+ if (NS_WARN_IF(Context()->Graph() != aTrack->Graph())) {
+ nsCOMPtr<nsPIDOMWindowInner> pWindow = Context()->GetParentObject();
+ Document* document = pWindow ? pWindow->GetExtantDoc() : nullptr;
+ nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "Web Audio"_ns,
+ document, nsContentUtils::eDOM_PROPERTIES,
+ "MediaStreamAudioSourceNodeDifferentRate");
+ // This is not a spec-required exception, just a limitation of our
+ // implementation.
+ aRv.ThrowNotSupportedError(
+ "Connecting AudioNodes from AudioContexts with different sample-rate "
+ "is currently not supported.");
+ return;
+ }
+
+ mInputTrack = aTrack;
+ ProcessedMediaTrack* outputTrack =
+ static_cast<ProcessedMediaTrack*>(mTrack.get());
+ mInputPort = mInputTrack->ForwardTrackContentsTo(outputTrack);
+ PrincipalChanged(mInputTrack); // trigger enabling/disabling of the connector
+ mInputTrack->AddPrincipalChangeObserver(this);
+ MarkActive();
+}
+
+void MediaStreamAudioSourceNode::DetachFromTrack() {
+ if (mInputTrack) {
+ mInputTrack->RemovePrincipalChangeObserver(this);
+ mInputTrack = nullptr;
+ }
+ if (mInputPort) {
+ mInputPort->Destroy();
+ mInputPort = nullptr;
+ }
+}
+
+static int AudioTrackCompare(const RefPtr<AudioStreamTrack>& aLhs,
+ const RefPtr<AudioStreamTrack>& aRhs) {
+ nsAutoStringN<NSID_LENGTH> IDLhs;
+ nsAutoStringN<NSID_LENGTH> IDRhs;
+ aLhs->GetId(IDLhs);
+ aRhs->GetId(IDRhs);
+ return Compare(NS_ConvertUTF16toUTF8(IDLhs), NS_ConvertUTF16toUTF8(IDRhs));
+}
+
+void MediaStreamAudioSourceNode::AttachToRightTrack(
+ const RefPtr<DOMMediaStream>& aMediaStream, ErrorResult& aRv) {
+ nsTArray<RefPtr<AudioStreamTrack>> tracks;
+ aMediaStream->GetAudioTracks(tracks);
+
+ if (tracks.IsEmpty() && mBehavior == LockOnTrackPicked) {
+ aRv.ThrowInvalidStateError("No audio tracks in MediaStream");
+ return;
+ }
+
+ // Sort the track to have a stable order, on their ID by lexicographic
+ // ordering on sequences of code unit values.
+ tracks.Sort(AudioTrackCompare);
+
+ for (const RefPtr<AudioStreamTrack>& track : tracks) {
+ if (mBehavior == FollowChanges) {
+ if (track->Ended()) {
+ continue;
+ }
+ }
+
+ if (!track->Ended()) {
+ AttachToTrack(track, aRv);
+ }
+ return;
+ }
+
+ // There was no track available. We'll allow the node to be garbage collected.
+ MarkInactive();
+}
+
+void MediaStreamAudioSourceNode::NotifyTrackAdded(
+ const RefPtr<MediaStreamTrack>& aTrack) {
+ if (mBehavior != FollowChanges) {
+ return;
+ }
+ if (mInputTrack) {
+ return;
+ }
+
+ if (!aTrack->AsAudioStreamTrack()) {
+ return;
+ }
+
+ AttachToTrack(aTrack, IgnoreErrors());
+}
+
+void MediaStreamAudioSourceNode::NotifyTrackRemoved(
+ const RefPtr<MediaStreamTrack>& aTrack) {
+ if (mBehavior == FollowChanges) {
+ if (aTrack != mInputTrack) {
+ return;
+ }
+
+ DetachFromTrack();
+ AttachToRightTrack(mInputStream, IgnoreErrors());
+ }
+}
+
+void MediaStreamAudioSourceNode::NotifyAudible() {
+ MOZ_ASSERT(mInputStream);
+ Context()->StartBlockedAudioContextIfAllowed();
+}
+
+/**
+ * Changes the principal. Note that this will be called on the main thread, but
+ * changes will be enacted on the MediaTrackGraph thread. If the principal
+ * change results in the document principal losing access to the stream, then
+ * there needs to be other measures in place to ensure that any media that is
+ * governed by the new stream principal is not available to the MediaTrackGraph
+ * before this change completes. Otherwise, a site could get access to
+ * media that they are not authorized to receive.
+ *
+ * One solution is to block the altered content, call this method, then dispatch
+ * another change request to the MediaTrackGraph thread that allows the content
+ * under the new principal to flow. This might be unnecessary if the principal
+ * change is changing to be the document principal.
+ */
+void MediaStreamAudioSourceNode::PrincipalChanged(
+ MediaStreamTrack* aMediaStreamTrack) {
+ MOZ_ASSERT(aMediaStreamTrack == mInputTrack);
+
+ bool subsumes = false;
+ Document* doc = nullptr;
+ if (nsPIDOMWindowInner* parent = Context()->GetParentObject()) {
+ doc = parent->GetExtantDoc();
+ if (doc) {
+ nsIPrincipal* docPrincipal = doc->NodePrincipal();
+ nsIPrincipal* trackPrincipal = aMediaStreamTrack->GetPrincipal();
+ if (!trackPrincipal ||
+ NS_FAILED(docPrincipal->Subsumes(trackPrincipal, &subsumes))) {
+ subsumes = false;
+ }
+ }
+ }
+ auto track = static_cast<AudioNodeExternalInputTrack*>(mTrack.get());
+ bool enabled = subsumes;
+ track->SetInt32Parameter(MediaStreamAudioSourceNodeEngine::ENABLE, enabled);
+
+ if (!enabled && doc) {
+ nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "Web Audio"_ns,
+ doc, nsContentUtils::eDOM_PROPERTIES,
+ CrossOriginErrorString());
+ }
+}
+
+size_t MediaStreamAudioSourceNode::SizeOfExcludingThis(
+ MallocSizeOf aMallocSizeOf) const {
+ // Future:
+ // - mInputStream
+ size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf);
+ if (mInputPort) {
+ amount += mInputPort->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ return amount;
+}
+
+size_t MediaStreamAudioSourceNode::SizeOfIncludingThis(
+ MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
+
+void MediaStreamAudioSourceNode::DestroyMediaTrack() {
+ if (mInputPort) {
+ mInputPort->Destroy();
+ mInputPort = nullptr;
+ }
+ AudioNode::DestroyMediaTrack();
+}
+
+JSObject* MediaStreamAudioSourceNode::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return MediaStreamAudioSourceNode_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/MediaStreamAudioSourceNode.h b/dom/media/webaudio/MediaStreamAudioSourceNode.h
new file mode 100644
index 0000000000..1875fc2e83
--- /dev/null
+++ b/dom/media/webaudio/MediaStreamAudioSourceNode.h
@@ -0,0 +1,127 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MediaStreamAudioSourceNode_h_
+#define MediaStreamAudioSourceNode_h_
+
+#include "AudioNode.h"
+#include "AudioNodeEngine.h"
+#include "DOMMediaStream.h"
+#include "PrincipalChangeObserver.h"
+
+namespace mozilla::dom {
+
+class AudioContext;
+struct MediaStreamAudioSourceOptions;
+
+class MediaStreamAudioSourceNodeEngine final : public AudioNodeEngine {
+ public:
+ explicit MediaStreamAudioSourceNodeEngine(AudioNode* aNode)
+ : AudioNodeEngine(aNode), mEnabled(false) {}
+
+ bool IsEnabled() const { return mEnabled; }
+ enum Parameters { ENABLE };
+ void SetInt32Parameter(uint32_t aIndex, int32_t aValue) override {
+ switch (aIndex) {
+ case ENABLE:
+ mEnabled = !!aValue;
+ break;
+ default:
+ NS_ERROR("MediaStreamAudioSourceNodeEngine bad parameter index");
+ }
+ }
+
+ private:
+ bool mEnabled;
+};
+
+class MediaStreamAudioSourceNode
+ : public AudioNode,
+ public DOMMediaStream::TrackListener,
+ public PrincipalChangeObserver<MediaStreamTrack> {
+ public:
+ static already_AddRefed<MediaStreamAudioSourceNode> Create(
+ AudioContext& aContext, const MediaStreamAudioSourceOptions& aOptions,
+ ErrorResult& aRv);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MediaStreamAudioSourceNode,
+ AudioNode)
+
+ static already_AddRefed<MediaStreamAudioSourceNode> Constructor(
+ const GlobalObject& aGlobal, AudioContext& aAudioContext,
+ const MediaStreamAudioSourceOptions& aOptions, ErrorResult& aRv) {
+ return Create(aAudioContext, aOptions, aRv);
+ }
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ void DestroyMediaTrack() override;
+
+ uint16_t NumberOfInputs() const override { return 0; }
+
+ DOMMediaStream* GetMediaStream() { return mInputStream; }
+
+ const char* NodeType() const override { return "MediaStreamAudioSourceNode"; }
+
+ virtual const char* CrossOriginErrorString() const {
+ return "MediaStreamAudioSourceNodeCrossOrigin";
+ }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override;
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override;
+
+ // Attaches to aTrack so that its audio content will be used as input.
+ void AttachToTrack(const RefPtr<MediaStreamTrack>& aTrack, ErrorResult& aRv);
+
+ // Detaches from the currently attached track if there is one.
+ void DetachFromTrack();
+
+ // Attaches to the first audio track in the MediaStream, when the tracks are
+ // ordered by id.
+ void AttachToRightTrack(const RefPtr<DOMMediaStream>& aMediaStream,
+ ErrorResult& aRv);
+
+ // From DOMMediaStream::TrackListener.
+ void NotifyTrackAdded(const RefPtr<MediaStreamTrack>& aTrack) override;
+ void NotifyTrackRemoved(const RefPtr<MediaStreamTrack>& aTrack) override;
+ void NotifyAudible() override;
+
+ // From PrincipalChangeObserver<MediaStreamTrack>.
+ void PrincipalChanged(MediaStreamTrack* aMediaStreamTrack) override;
+
+ // This allows implementing the correct behaviour for both
+ // MediaElementAudioSourceNode and MediaStreamAudioSourceNode, that have most
+ // of their behaviour shared.
+ enum TrackChangeBehavior {
+ // MediaStreamAudioSourceNode locks on the track it picked, and never
+ // changes.
+ LockOnTrackPicked,
+ // MediaElementAudioSourceNode can change track, depending on what the
+ // HTMLMediaElement does.
+ FollowChanges
+ };
+
+ protected:
+ MediaStreamAudioSourceNode(AudioContext* aContext,
+ TrackChangeBehavior aBehavior);
+ void Init(DOMMediaStream& aMediaStream, ErrorResult& aRv);
+ virtual void Destroy();
+ virtual ~MediaStreamAudioSourceNode();
+
+ private:
+ const TrackChangeBehavior mBehavior;
+ RefPtr<MediaInputPort> mInputPort;
+ RefPtr<DOMMediaStream> mInputStream;
+
+ // On construction we set this to the first audio track of mInputStream.
+ RefPtr<MediaStreamTrack> mInputTrack;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webaudio/MediaStreamTrackAudioSourceNode.cpp b/dom/media/webaudio/MediaStreamTrackAudioSourceNode.cpp
new file mode 100644
index 0000000000..f3948a33ad
--- /dev/null
+++ b/dom/media/webaudio/MediaStreamTrackAudioSourceNode.cpp
@@ -0,0 +1,200 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaStreamTrackAudioSourceNode.h"
+#include "mozilla/dom/MediaStreamTrackAudioSourceNodeBinding.h"
+#include "AudioNodeEngine.h"
+#include "AudioNodeExternalInputTrack.h"
+#include "AudioStreamTrack.h"
+#include "mozilla/dom/Document.h"
+#include "nsContentUtils.h"
+#include "nsIScriptError.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(MediaStreamTrackAudioSourceNode)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(MediaStreamTrackAudioSourceNode)
+ tmp->Destroy();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mInputTrack)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_PTR
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END_INHERITED(AudioNode)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(
+ MediaStreamTrackAudioSourceNode, AudioNode)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mInputTrack)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaStreamTrackAudioSourceNode)
+NS_INTERFACE_MAP_END_INHERITING(AudioNode)
+
+NS_IMPL_ADDREF_INHERITED(MediaStreamTrackAudioSourceNode, AudioNode)
+NS_IMPL_RELEASE_INHERITED(MediaStreamTrackAudioSourceNode, AudioNode)
+
+MediaStreamTrackAudioSourceNode::MediaStreamTrackAudioSourceNode(
+ AudioContext* aContext)
+ : AudioNode(aContext, 2, ChannelCountMode::Max,
+ ChannelInterpretation::Speakers),
+ mTrackListener(this) {}
+
+/* static */ already_AddRefed<MediaStreamTrackAudioSourceNode>
+MediaStreamTrackAudioSourceNode::Create(
+ AudioContext& aAudioContext,
+ const MediaStreamTrackAudioSourceOptions& aOptions, ErrorResult& aRv) {
+ // The spec has a pointless check here. See
+ // https://github.com/WebAudio/web-audio-api/issues/2149
+ MOZ_RELEASE_ASSERT(!aAudioContext.IsOffline(), "Bindings messed up?");
+
+ if (!aOptions.mMediaStreamTrack->Ended() &&
+ aAudioContext.Graph() != aOptions.mMediaStreamTrack->Graph()) {
+ nsCOMPtr<nsPIDOMWindowInner> pWindow = aAudioContext.GetParentObject();
+ Document* document = pWindow ? pWindow->GetExtantDoc() : nullptr;
+ nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "Web Audio"_ns,
+ document, nsContentUtils::eDOM_PROPERTIES,
+ "MediaStreamAudioSourceNodeDifferentRate");
+ // This is not a spec-required exception, just a limitation of our
+ // implementation.
+ aRv.ThrowNotSupportedError(
+ "Connecting AudioNodes from AudioContexts with different sample-rate "
+ "is currently not supported.");
+ return nullptr;
+ }
+
+ RefPtr<MediaStreamTrackAudioSourceNode> node =
+ new MediaStreamTrackAudioSourceNode(&aAudioContext);
+
+ node->Init(aOptions.mMediaStreamTrack, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ return node.forget();
+}
+
+void MediaStreamTrackAudioSourceNode::Init(MediaStreamTrack* aMediaStreamTrack,
+ ErrorResult& aRv) {
+ MOZ_ASSERT(aMediaStreamTrack);
+
+ if (!aMediaStreamTrack->AsAudioStreamTrack()) {
+ aRv.ThrowInvalidStateError("\"mediaStreamTrack\" must be an audio track");
+ return;
+ }
+
+ if (aMediaStreamTrack->Ended()) {
+ // The track is ended and will never produce any data. Pretend like this is
+ // fine.
+ return;
+ }
+
+ MarkActive();
+
+ MediaTrackGraph* graph = Context()->Graph();
+
+ AudioNodeEngine* engine = new MediaStreamTrackAudioSourceNodeEngine(this);
+ mTrack = AudioNodeExternalInputTrack::Create(graph, engine);
+
+ MOZ_ASSERT(mTrack);
+
+ mInputTrack = aMediaStreamTrack;
+ ProcessedMediaTrack* outputTrack =
+ static_cast<ProcessedMediaTrack*>(mTrack.get());
+ mInputPort = mInputTrack->ForwardTrackContentsTo(outputTrack);
+ PrincipalChanged(mInputTrack); // trigger enabling/disabling of the connector
+ mInputTrack->AddPrincipalChangeObserver(this);
+
+ mInputTrack->AddConsumer(&mTrackListener);
+}
+
+void MediaStreamTrackAudioSourceNode::Destroy() {
+ if (mInputTrack) {
+ mTrackListener.NotifyEnded(mInputTrack);
+ mInputTrack->RemovePrincipalChangeObserver(this);
+ mInputTrack->RemoveConsumer(&mTrackListener);
+ mInputTrack = nullptr;
+ }
+
+ if (mInputPort) {
+ mInputPort->Destroy();
+ mInputPort = nullptr;
+ }
+}
+
+MediaStreamTrackAudioSourceNode::~MediaStreamTrackAudioSourceNode() {
+ Destroy();
+}
+
+/**
+ * Changes the principal. Note that this will be called on the main thread, but
+ * changes will be enacted on the MediaTrackGraph thread. If the principal
+ * change results in the document principal losing access to the track, then
+ * there needs to be other measures in place to ensure that any media that is
+ * governed by the new track principal is not available to the MediaTrackGraph
+ * before this change completes. Otherwise, a site could get access to
+ * media that they are not authorized to receive.
+ *
+ * One solution is to block the altered content, call this method, then dispatch
+ * another change request to the MediaTrackGraph thread that allows the content
+ * under the new principal to flow. This might be unnecessary if the principal
+ * change is changing to be the document principal.
+ */
+void MediaStreamTrackAudioSourceNode::PrincipalChanged(
+ MediaStreamTrack* aMediaStreamTrack) {
+ MOZ_ASSERT(aMediaStreamTrack == mInputTrack);
+
+ bool subsumes = false;
+ Document* doc = nullptr;
+ if (nsPIDOMWindowInner* parent = Context()->GetParentObject()) {
+ doc = parent->GetExtantDoc();
+ if (doc) {
+ nsIPrincipal* docPrincipal = doc->NodePrincipal();
+ nsIPrincipal* trackPrincipal = aMediaStreamTrack->GetPrincipal();
+ if (!trackPrincipal ||
+ NS_FAILED(docPrincipal->Subsumes(trackPrincipal, &subsumes))) {
+ subsumes = false;
+ }
+ }
+ }
+ auto track = static_cast<AudioNodeExternalInputTrack*>(mTrack.get());
+ bool enabled = subsumes;
+ track->SetInt32Parameter(MediaStreamTrackAudioSourceNodeEngine::ENABLE,
+ enabled);
+ fprintf(stderr, "NOW: %s", enabled ? "enabled" : "disabled");
+
+ if (!enabled && doc) {
+ nsContentUtils::ReportToConsole(nsIScriptError::warningFlag, "Web Audio"_ns,
+ doc, nsContentUtils::eDOM_PROPERTIES,
+ CrossOriginErrorString());
+ }
+}
+
+size_t MediaStreamTrackAudioSourceNode::SizeOfExcludingThis(
+ MallocSizeOf aMallocSizeOf) const {
+ size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf);
+ if (mInputPort) {
+ amount += mInputPort->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ return amount;
+}
+
+size_t MediaStreamTrackAudioSourceNode::SizeOfIncludingThis(
+ MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
+
+void MediaStreamTrackAudioSourceNode::DestroyMediaTrack() {
+ if (mInputPort) {
+ mInputPort->Destroy();
+ mInputPort = nullptr;
+ }
+ AudioNode::DestroyMediaTrack();
+}
+
+JSObject* MediaStreamTrackAudioSourceNode::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return MediaStreamTrackAudioSourceNode_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/MediaStreamTrackAudioSourceNode.h b/dom/media/webaudio/MediaStreamTrackAudioSourceNode.h
new file mode 100644
index 0000000000..bb3d5d6c94
--- /dev/null
+++ b/dom/media/webaudio/MediaStreamTrackAudioSourceNode.h
@@ -0,0 +1,113 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MediaStreamTrackAudioSourceNode_h_
+#define MediaStreamTrackAudioSourceNode_h_
+
+#include "AudioNode.h"
+#include "AudioNodeEngine.h"
+#include "mozilla/dom/MediaStreamTrack.h"
+#include "mozilla/WeakPtr.h"
+#include "PrincipalChangeObserver.h"
+
+namespace mozilla::dom {
+
+class AudioContext;
+struct MediaStreamTrackAudioSourceOptions;
+
+class MediaStreamTrackAudioSourceNodeEngine final : public AudioNodeEngine {
+ public:
+ explicit MediaStreamTrackAudioSourceNodeEngine(AudioNode* aNode)
+ : AudioNodeEngine(aNode), mEnabled(false) {}
+
+ bool IsEnabled() const { return mEnabled; }
+ enum Parameters { ENABLE };
+ void SetInt32Parameter(uint32_t aIndex, int32_t aValue) override {
+ switch (aIndex) {
+ case ENABLE:
+ mEnabled = !!aValue;
+ break;
+ default:
+ NS_ERROR("MediaStreamTrackAudioSourceNodeEngine bad parameter index");
+ }
+ }
+
+ private:
+ bool mEnabled;
+};
+
+class MediaStreamTrackAudioSourceNode
+ : public AudioNode,
+ public PrincipalChangeObserver<MediaStreamTrack>,
+ public SupportsWeakPtr {
+ public:
+ static already_AddRefed<MediaStreamTrackAudioSourceNode> Create(
+ AudioContext& aContext,
+ const MediaStreamTrackAudioSourceOptions& aOptions, ErrorResult& aRv);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MediaStreamTrackAudioSourceNode,
+ AudioNode)
+
+ static already_AddRefed<MediaStreamTrackAudioSourceNode> Constructor(
+ const GlobalObject& aGlobal, AudioContext& aAudioContext,
+ const MediaStreamTrackAudioSourceOptions& aOptions, ErrorResult& aRv) {
+ return Create(aAudioContext, aOptions, aRv);
+ }
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ void DestroyMediaTrack() override;
+
+ uint16_t NumberOfInputs() const override { return 0; }
+
+ const char* NodeType() const override {
+ return "MediaStreamTrackAudioSourceNode";
+ }
+
+ virtual const char* CrossOriginErrorString() const {
+ return "MediaStreamTrackAudioSourceNodeCrossOrigin";
+ }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override;
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override;
+
+ // From PrincipalChangeObserver<MediaStreamTrack>.
+ void PrincipalChanged(MediaStreamTrack* aMediaStreamTrack) override;
+
+ protected:
+ explicit MediaStreamTrackAudioSourceNode(AudioContext* aContext);
+ void Init(MediaStreamTrack* aMediaStreamTrack, ErrorResult& aRv);
+ void Destroy();
+ virtual ~MediaStreamTrackAudioSourceNode();
+
+ class TrackListener : public MediaStreamTrackConsumer {
+ public:
+ explicit TrackListener(MediaStreamTrackAudioSourceNode* aNode)
+ : mNode(aNode) {}
+
+ void NotifyEnded(MediaStreamTrack* aTrack) override {
+ if (mNode) {
+ mNode->MarkInactive();
+ mNode->DestroyMediaTrack();
+ mNode = nullptr;
+ }
+ }
+
+ private:
+ WeakPtr<MediaStreamTrackAudioSourceNode> mNode;
+ };
+
+ private:
+ RefPtr<MediaInputPort> mInputPort;
+ RefPtr<MediaStreamTrack> mInputTrack;
+ TrackListener mTrackListener;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webaudio/OscillatorNode.cpp b/dom/media/webaudio/OscillatorNode.cpp
new file mode 100644
index 0000000000..b18b0a0fe1
--- /dev/null
+++ b/dom/media/webaudio/OscillatorNode.cpp
@@ -0,0 +1,542 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "OscillatorNode.h"
+#include "AudioNodeEngine.h"
+#include "AudioNodeTrack.h"
+#include "AudioDestinationNode.h"
+#include "nsContentUtils.h"
+#include "WebAudioUtils.h"
+#include "blink/PeriodicWave.h"
+#include "Tracing.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(OscillatorNode, AudioScheduledSourceNode,
+ mPeriodicWave, mFrequency, mDetune)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(OscillatorNode)
+NS_INTERFACE_MAP_END_INHERITING(AudioScheduledSourceNode)
+
+NS_IMPL_ADDREF_INHERITED(OscillatorNode, AudioScheduledSourceNode)
+NS_IMPL_RELEASE_INHERITED(OscillatorNode, AudioScheduledSourceNode)
+
+class OscillatorNodeEngine final : public AudioNodeEngine {
+ public:
+ OscillatorNodeEngine(AudioNode* aNode, AudioDestinationNode* aDestination)
+ : AudioNodeEngine(aNode),
+ mSource(nullptr),
+ mDestination(aDestination->Track()),
+ mStart(-1),
+ mStop(TRACK_TIME_MAX)
+ // Keep the default values in sync with OscillatorNode::OscillatorNode.
+ ,
+ mFrequency(440.f),
+ mDetune(0.f),
+ mType(OscillatorType::Sine),
+ mPhase(0.),
+ mFinalFrequency(0.),
+ mPhaseIncrement(0.),
+ mRecomputeParameters(true),
+ mCustomDisableNormalization(false) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mBasicWaveFormCache = aDestination->Context()->GetBasicWaveFormCache();
+ }
+
+ void SetSourceTrack(AudioNodeTrack* aSource) { mSource = aSource; }
+
+ enum Parameters {
+ FREQUENCY,
+ DETUNE,
+ TYPE,
+ DISABLE_NORMALIZATION,
+ START,
+ STOP,
+ };
+ void RecvTimelineEvent(uint32_t aIndex, AudioTimelineEvent& aEvent) override {
+ mRecomputeParameters = true;
+
+ MOZ_ASSERT(mDestination);
+
+ WebAudioUtils::ConvertAudioTimelineEventToTicks(aEvent, mDestination);
+
+ switch (aIndex) {
+ case FREQUENCY:
+ mFrequency.InsertEvent<int64_t>(aEvent);
+ break;
+ case DETUNE:
+ mDetune.InsertEvent<int64_t>(aEvent);
+ break;
+ default:
+ NS_ERROR("Bad OscillatorNodeEngine TimelineParameter");
+ }
+ }
+
+ void SetTrackTimeParameter(uint32_t aIndex, TrackTime aParam) override {
+ switch (aIndex) {
+ case START:
+ mStart = aParam;
+ mSource->SetActive();
+ break;
+ case STOP:
+ mStop = aParam;
+ break;
+ default:
+ NS_ERROR("Bad OscillatorNodeEngine TrackTimeParameter");
+ }
+ }
+
+ void SetInt32Parameter(uint32_t aIndex, int32_t aParam) override {
+ switch (aIndex) {
+ case TYPE:
+ // Set the new type.
+ mType = static_cast<OscillatorType>(aParam);
+ if (mType == OscillatorType::Sine) {
+ // Forget any previous custom data.
+ mCustomDisableNormalization = false;
+ mPeriodicWave = nullptr;
+ mRecomputeParameters = true;
+ }
+ switch (mType) {
+ case OscillatorType::Sine:
+ mPhase = 0.0;
+ break;
+ case OscillatorType::Square:
+ case OscillatorType::Triangle:
+ case OscillatorType::Sawtooth:
+ mPeriodicWave = mBasicWaveFormCache->GetBasicWaveForm(mType);
+ break;
+ case OscillatorType::Custom:
+ break;
+ default:
+ NS_ERROR("Bad OscillatorNodeEngine type parameter.");
+ }
+ // End type switch.
+ break;
+ case DISABLE_NORMALIZATION:
+ MOZ_ASSERT(aParam >= 0, "negative custom array length");
+ mCustomDisableNormalization = static_cast<uint32_t>(aParam);
+ break;
+ default:
+ NS_ERROR("Bad OscillatorNodeEngine Int32Parameter.");
+ }
+ // End index switch.
+ }
+
+ void SetBuffer(AudioChunk&& aBuffer) override {
+ MOZ_ASSERT(aBuffer.ChannelCount() == 2,
+ "PeriodicWave should have sent two channels");
+ MOZ_ASSERT(aBuffer.mVolume == 1.0f);
+ mPeriodicWave = WebCore::PeriodicWave::create(
+ mSource->mSampleRate, aBuffer.ChannelData<float>()[0],
+ aBuffer.ChannelData<float>()[1], aBuffer.mDuration,
+ mCustomDisableNormalization);
+ }
+
+ void IncrementPhase() {
+ const float twoPiFloat = float(2 * M_PI);
+ mPhase += mPhaseIncrement;
+ if (mPhase > twoPiFloat) {
+ mPhase -= twoPiFloat;
+ } else if (mPhase < -twoPiFloat) {
+ mPhase += twoPiFloat;
+ }
+ }
+
+ // Returns true if the final frequency (and thus the phase increment) changed,
+ // false otherwise. This allow some optimizations at callsite.
+ bool UpdateParametersIfNeeded(TrackTime ticks, size_t count) {
+ double frequency, detune;
+
+ // Shortcut if frequency-related AudioParam are not automated, and we
+ // already have computed the frequency information and related parameters.
+ if (!ParametersMayNeedUpdate()) {
+ return false;
+ }
+
+ bool simpleFrequency = mFrequency.HasSimpleValue();
+ bool simpleDetune = mDetune.HasSimpleValue();
+
+ if (simpleFrequency) {
+ frequency = mFrequency.GetValue();
+ } else {
+ frequency = mFrequency.GetValueAtTime(ticks, count);
+ }
+ if (simpleDetune) {
+ detune = mDetune.GetValue();
+ } else {
+ detune = mDetune.GetValueAtTime(ticks, count);
+ }
+
+ float finalFrequency = frequency * exp2(detune / 1200.);
+ float signalPeriod = mSource->mSampleRate / finalFrequency;
+ mRecomputeParameters = false;
+
+ mPhaseIncrement = 2 * M_PI / signalPeriod;
+
+ if (finalFrequency != mFinalFrequency) {
+ mFinalFrequency = finalFrequency;
+ return true;
+ }
+ return false;
+ }
+
+ void FillBounds(float* output, TrackTime ticks, uint32_t& start,
+ uint32_t& end) {
+ MOZ_ASSERT(output);
+ static_assert(TrackTime(WEBAUDIO_BLOCK_SIZE) < UINT_MAX,
+ "WEBAUDIO_BLOCK_SIZE overflows interator bounds.");
+ start = 0;
+ if (ticks < mStart) {
+ start = mStart - ticks;
+ for (uint32_t i = 0; i < start; ++i) {
+ output[i] = 0.0;
+ }
+ }
+ end = WEBAUDIO_BLOCK_SIZE;
+ if (ticks + end > mStop) {
+ end = mStop - ticks;
+ for (uint32_t i = end; i < WEBAUDIO_BLOCK_SIZE; ++i) {
+ output[i] = 0.0;
+ }
+ }
+ }
+
+ void ComputeSine(float* aOutput, TrackTime ticks, uint32_t aStart,
+ uint32_t aEnd) {
+ for (uint32_t i = aStart; i < aEnd; ++i) {
+ // We ignore the return value, changing the frequency has no impact on
+ // performances here.
+ UpdateParametersIfNeeded(ticks, i);
+
+ aOutput[i] = sin(mPhase);
+
+ IncrementPhase();
+ }
+ }
+
+ bool ParametersMayNeedUpdate() {
+ return !mDetune.HasSimpleValue() || !mFrequency.HasSimpleValue() ||
+ mRecomputeParameters;
+ }
+
+ void ComputeCustom(float* aOutput, TrackTime ticks, uint32_t aStart,
+ uint32_t aEnd) {
+ MOZ_ASSERT(mPeriodicWave, "No custom waveform data");
+
+ uint32_t periodicWaveSize = mPeriodicWave->periodicWaveSize();
+ // Mask to wrap wave data indices into the range [0,periodicWaveSize).
+ uint32_t indexMask = periodicWaveSize - 1;
+ MOZ_ASSERT(periodicWaveSize && (periodicWaveSize & indexMask) == 0,
+ "periodicWaveSize must be power of 2");
+ float* higherWaveData = nullptr;
+ float* lowerWaveData = nullptr;
+ float tableInterpolationFactor;
+ // Phase increment at frequency of 1 Hz.
+ // mPhase runs [0,periodicWaveSize) here instead of [0,2*M_PI).
+ float basePhaseIncrement = mPeriodicWave->rateScale();
+
+ bool needToFetchWaveData = UpdateParametersIfNeeded(ticks, aStart);
+
+ bool parametersMayNeedUpdate = ParametersMayNeedUpdate();
+ mPeriodicWave->waveDataForFundamentalFrequency(
+ mFinalFrequency, lowerWaveData, higherWaveData,
+ tableInterpolationFactor);
+
+ for (uint32_t i = aStart; i < aEnd; ++i) {
+ if (parametersMayNeedUpdate) {
+ if (needToFetchWaveData) {
+ mPeriodicWave->waveDataForFundamentalFrequency(
+ mFinalFrequency, lowerWaveData, higherWaveData,
+ tableInterpolationFactor);
+ }
+ needToFetchWaveData = UpdateParametersIfNeeded(ticks, i);
+ }
+ // Bilinear interpolation between adjacent samples in each table.
+ float floorPhase = floorf(mPhase);
+ int j1Signed = static_cast<int>(floorPhase);
+ uint32_t j1 = j1Signed & indexMask;
+ uint32_t j2 = j1 + 1;
+ j2 &= indexMask;
+
+ float sampleInterpolationFactor = mPhase - floorPhase;
+
+ float lower = (1.0f - sampleInterpolationFactor) * lowerWaveData[j1] +
+ sampleInterpolationFactor * lowerWaveData[j2];
+ float higher = (1.0f - sampleInterpolationFactor) * higherWaveData[j1] +
+ sampleInterpolationFactor * higherWaveData[j2];
+ aOutput[i] = (1.0f - tableInterpolationFactor) * lower +
+ tableInterpolationFactor * higher;
+
+ // Calculate next phase position from wrapped value j1 to avoid loss of
+ // precision at large values.
+ mPhase =
+ j1 + sampleInterpolationFactor + basePhaseIncrement * mFinalFrequency;
+ }
+ }
+
+ void ComputeSilence(AudioBlock* aOutput) {
+ aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
+ }
+
+ void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom,
+ const AudioBlock& aInput, AudioBlock* aOutput,
+ bool* aFinished) override {
+ MOZ_ASSERT(mSource == aTrack, "Invalid source track");
+ TRACE("OscillatorNodeEngine::ProcessBlock");
+
+ TrackTime ticks = mDestination->GraphTimeToTrackTime(aFrom);
+ if (mStart == -1) {
+ ComputeSilence(aOutput);
+ return;
+ }
+
+ if (ticks + WEBAUDIO_BLOCK_SIZE <= mStart || ticks >= mStop) {
+ ComputeSilence(aOutput);
+
+ } else {
+ aOutput->AllocateChannels(1);
+ float* output = aOutput->ChannelFloatsForWrite(0);
+
+ uint32_t start, end;
+ FillBounds(output, ticks, start, end);
+
+ // Synthesize the correct waveform.
+ switch (mType) {
+ case OscillatorType::Sine:
+ ComputeSine(output, ticks, start, end);
+ break;
+ case OscillatorType::Square:
+ case OscillatorType::Triangle:
+ case OscillatorType::Sawtooth:
+ case OscillatorType::Custom:
+ ComputeCustom(output, ticks, start, end);
+ break;
+ default:
+ ComputeSilence(aOutput);
+ };
+ }
+
+ if (ticks + WEBAUDIO_BLOCK_SIZE >= mStop) {
+ // We've finished playing.
+ *aFinished = true;
+ }
+ }
+
+ bool IsActive() const override {
+ // start() has been called.
+ return mStart != -1;
+ }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override {
+ size_t amount = AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf);
+
+ // Not owned:
+ // - mSource
+ // - mDestination
+ // - mFrequency (internal ref owned by node)
+ // - mDetune (internal ref owned by node)
+
+ if (mPeriodicWave) {
+ amount += mPeriodicWave->sizeOfIncludingThis(aMallocSizeOf);
+ }
+
+ return amount;
+ }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ // mSource deletes this engine in its destructor
+ AudioNodeTrack* MOZ_NON_OWNING_REF mSource;
+ RefPtr<AudioNodeTrack> mDestination;
+ TrackTime mStart;
+ TrackTime mStop;
+ AudioParamTimeline mFrequency;
+ AudioParamTimeline mDetune;
+ OscillatorType mType;
+ float mPhase;
+ float mFinalFrequency;
+ float mPhaseIncrement;
+ bool mRecomputeParameters;
+ RefPtr<BasicWaveFormCache> mBasicWaveFormCache;
+ bool mCustomDisableNormalization;
+ RefPtr<WebCore::PeriodicWave> mPeriodicWave;
+};
+
+OscillatorNode::OscillatorNode(AudioContext* aContext)
+ : AudioScheduledSourceNode(aContext, 2, ChannelCountMode::Max,
+ ChannelInterpretation::Speakers),
+ mType(OscillatorType::Sine),
+ mStartCalled(false) {
+ mFrequency = CreateAudioParam(
+ OscillatorNodeEngine::FREQUENCY, u"frequency"_ns, 440.0f,
+ -(aContext->SampleRate() / 2), aContext->SampleRate() / 2);
+ mDetune = CreateAudioParam(OscillatorNodeEngine::DETUNE, u"detune"_ns, 0.0f);
+ OscillatorNodeEngine* engine =
+ new OscillatorNodeEngine(this, aContext->Destination());
+ mTrack = AudioNodeTrack::Create(aContext, engine,
+ AudioNodeTrack::NEED_MAIN_THREAD_ENDED,
+ aContext->Graph());
+ engine->SetSourceTrack(mTrack);
+ mTrack->AddMainThreadListener(this);
+}
+
+/* static */
+already_AddRefed<OscillatorNode> OscillatorNode::Create(
+ AudioContext& aAudioContext, const OscillatorOptions& aOptions,
+ ErrorResult& aRv) {
+ RefPtr<OscillatorNode> audioNode = new OscillatorNode(&aAudioContext);
+
+ audioNode->Initialize(aOptions, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ audioNode->Frequency()->SetInitialValue(aOptions.mFrequency);
+ audioNode->Detune()->SetInitialValue(aOptions.mDetune);
+
+ if (aOptions.mPeriodicWave.WasPassed()) {
+ audioNode->SetPeriodicWave(aOptions.mPeriodicWave.Value());
+ } else {
+ audioNode->SetType(aOptions.mType, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+ }
+
+ return audioNode.forget();
+}
+
+size_t OscillatorNode::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf);
+
+ // For now only report if we know for sure that it's not shared.
+ if (mPeriodicWave) {
+ amount += mPeriodicWave->SizeOfIncludingThisIfNotShared(aMallocSizeOf);
+ }
+ amount += mFrequency->SizeOfIncludingThis(aMallocSizeOf);
+ amount += mDetune->SizeOfIncludingThis(aMallocSizeOf);
+ return amount;
+}
+
+size_t OscillatorNode::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
+
+JSObject* OscillatorNode::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return OscillatorNode_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void OscillatorNode::DestroyMediaTrack() {
+ if (mTrack) {
+ mTrack->RemoveMainThreadListener(this);
+ }
+ AudioNode::DestroyMediaTrack();
+}
+
+void OscillatorNode::SendTypeToTrack() {
+ if (!mTrack) {
+ return;
+ }
+ if (mType == OscillatorType::Custom) {
+ // The engine assumes we'll send the custom data before updating the type.
+ SendPeriodicWaveToTrack();
+ }
+ SendInt32ParameterToTrack(OscillatorNodeEngine::TYPE,
+ static_cast<int32_t>(mType));
+}
+
+void OscillatorNode::SendPeriodicWaveToTrack() {
+ NS_ASSERTION(mType == OscillatorType::Custom,
+ "Sending custom waveform to engine thread with non-custom type");
+ MOZ_ASSERT(mTrack, "Missing node track.");
+ MOZ_ASSERT(mPeriodicWave, "Send called without PeriodicWave object.");
+ SendInt32ParameterToTrack(OscillatorNodeEngine::DISABLE_NORMALIZATION,
+ mPeriodicWave->DisableNormalization());
+ AudioChunk data = mPeriodicWave->GetThreadSharedBuffer();
+ mTrack->SetBuffer(std::move(data));
+}
+
+void OscillatorNode::Start(double aWhen, ErrorResult& aRv) {
+ if (!WebAudioUtils::IsTimeValid(aWhen)) {
+ aRv.ThrowRangeError<MSG_VALUE_OUT_OF_RANGE>("start time");
+ return;
+ }
+
+ if (mStartCalled) {
+ aRv.ThrowInvalidStateError("Can't call start() more than once");
+ return;
+ }
+ mStartCalled = true;
+
+ if (!mTrack) {
+ // Nothing to play, or we're already dead for some reason
+ return;
+ }
+
+ // TODO: Perhaps we need to do more here.
+ mTrack->SetTrackTimeParameter(OscillatorNodeEngine::START, Context(), aWhen);
+
+ MarkActive();
+ Context()->StartBlockedAudioContextIfAllowed();
+}
+
+void OscillatorNode::Stop(double aWhen, ErrorResult& aRv) {
+ if (!WebAudioUtils::IsTimeValid(aWhen)) {
+ aRv.ThrowRangeError<MSG_VALUE_OUT_OF_RANGE>("stop time");
+ return;
+ }
+
+ if (!mStartCalled) {
+ aRv.ThrowInvalidStateError("Can't call stop() without calling start()");
+ return;
+ }
+
+ if (!mTrack || !Context()) {
+ // We've already stopped and had our track shut down
+ return;
+ }
+
+ // TODO: Perhaps we need to do more here.
+ mTrack->SetTrackTimeParameter(OscillatorNodeEngine::STOP, Context(),
+ std::max(0.0, aWhen));
+}
+
+void OscillatorNode::NotifyMainThreadTrackEnded() {
+ MOZ_ASSERT(mTrack->IsEnded());
+
+ class EndedEventDispatcher final : public Runnable {
+ public:
+ explicit EndedEventDispatcher(OscillatorNode* aNode)
+ : mozilla::Runnable("EndedEventDispatcher"), mNode(aNode) {}
+ NS_IMETHOD Run() override {
+ // If it's not safe to run scripts right now, schedule this to run later
+ if (!nsContentUtils::IsSafeToRunScript()) {
+ nsContentUtils::AddScriptRunner(this);
+ return NS_OK;
+ }
+
+ mNode->DispatchTrustedEvent(u"ended"_ns);
+ // Release track resources.
+ mNode->DestroyMediaTrack();
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<OscillatorNode> mNode;
+ };
+
+ Context()->Dispatch(do_AddRef(new EndedEventDispatcher(this)));
+
+ // Drop the playing reference
+ // Warning: The below line might delete this.
+ MarkInactive();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/OscillatorNode.h b/dom/media/webaudio/OscillatorNode.h
new file mode 100644
index 0000000000..1e207b9d67
--- /dev/null
+++ b/dom/media/webaudio/OscillatorNode.h
@@ -0,0 +1,92 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef OscillatorNode_h_
+#define OscillatorNode_h_
+
+#include "AudioScheduledSourceNode.h"
+#include "AudioParam.h"
+#include "PeriodicWave.h"
+#include "mozilla/dom/OscillatorNodeBinding.h"
+
+namespace mozilla::dom {
+
+class AudioContext;
+struct OscillatorOptions;
+
+class OscillatorNode final : public AudioScheduledSourceNode,
+ public MainThreadMediaTrackListener {
+ public:
+ static already_AddRefed<OscillatorNode> Create(
+ AudioContext& aAudioContext, const OscillatorOptions& aOptions,
+ ErrorResult& aRv);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(OscillatorNode,
+ AudioScheduledSourceNode)
+
+ static already_AddRefed<OscillatorNode> Constructor(
+ const GlobalObject& aGlobal, AudioContext& aAudioContext,
+ const OscillatorOptions& aOptions, ErrorResult& aRv) {
+ return Create(aAudioContext, aOptions, aRv);
+ }
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ void DestroyMediaTrack() override;
+
+ uint16_t NumberOfInputs() const final { return 0; }
+
+ OscillatorType Type() const { return mType; }
+ void SetType(OscillatorType aType, ErrorResult& aRv) {
+ if (aType == OscillatorType::Custom) {
+ // ::Custom can only be set by setPeriodicWave().
+ // https://github.com/WebAudio/web-audio-api/issues/105 for exception.
+ aRv.ThrowInvalidStateError("Can't set type to \"custom\"");
+ return;
+ }
+ mType = aType;
+ SendTypeToTrack();
+ }
+
+ AudioParam* Frequency() const { return mFrequency; }
+ AudioParam* Detune() const { return mDetune; }
+
+ void Start(double aWhen, ErrorResult& aRv) override;
+ void Stop(double aWhen, ErrorResult& aRv) override;
+
+ void SetPeriodicWave(PeriodicWave& aPeriodicWave) {
+ mPeriodicWave = &aPeriodicWave;
+ // SendTypeToTrack will call SendPeriodicWaveToTrack for us.
+ mType = OscillatorType::Custom;
+ SendTypeToTrack();
+ }
+
+ void NotifyMainThreadTrackEnded() override;
+
+ const char* NodeType() const override { return "OscillatorNode"; }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override;
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override;
+
+ private:
+ explicit OscillatorNode(AudioContext* aContext);
+ ~OscillatorNode() = default;
+
+ void SendTypeToTrack();
+ void SendPeriodicWaveToTrack();
+
+ OscillatorType mType;
+ RefPtr<PeriodicWave> mPeriodicWave;
+ RefPtr<AudioParam> mFrequency;
+ RefPtr<AudioParam> mDetune;
+ bool mStartCalled;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webaudio/PannerNode.cpp b/dom/media/webaudio/PannerNode.cpp
new file mode 100644
index 0000000000..6e51507105
--- /dev/null
+++ b/dom/media/webaudio/PannerNode.cpp
@@ -0,0 +1,726 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "PannerNode.h"
+#include "AlignmentUtils.h"
+#include "AudioDestinationNode.h"
+#include "AudioNodeEngine.h"
+#include "AudioNodeTrack.h"
+#include "AudioListener.h"
+#include "PanningUtils.h"
+#include "AudioBufferSourceNode.h"
+#include "PlayingRefChangeHandler.h"
+#include "blink/HRTFPanner.h"
+#include "blink/HRTFDatabaseLoader.h"
+#include "Tracing.h"
+
+using WebCore::HRTFDatabaseLoader;
+using WebCore::HRTFPanner;
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(PannerNode)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(PannerNode, AudioNode)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mPositionX, mPositionY, mPositionZ,
+ mOrientationX, mOrientationY, mOrientationZ)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(PannerNode, AudioNode)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mPositionX, mPositionY, mPositionZ,
+ mOrientationX, mOrientationY, mOrientationZ)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PannerNode)
+NS_INTERFACE_MAP_END_INHERITING(AudioNode)
+
+NS_IMPL_ADDREF_INHERITED(PannerNode, AudioNode)
+NS_IMPL_RELEASE_INHERITED(PannerNode, AudioNode)
+
+class PannerNodeEngine final : public AudioNodeEngine {
+ public:
+ explicit PannerNodeEngine(AudioNode* aNode,
+ AudioDestinationNode* aDestination,
+ AudioListenerEngine* aListenerEngine)
+ : AudioNodeEngine(aNode),
+ mDestination(aDestination->Track()),
+ mListenerEngine(aListenerEngine)
+ // Please keep these default values consistent with
+ // PannerNode::PannerNode below.
+ ,
+ mPanningModelFunction(&PannerNodeEngine::EqualPowerPanningFunction),
+ mDistanceModelFunction(&PannerNodeEngine::InverseGainFunction),
+ mPositionX(0.),
+ mPositionY(0.),
+ mPositionZ(0.),
+ mOrientationX(1.),
+ mOrientationY(0.),
+ mOrientationZ(0.),
+ mRefDistance(1.),
+ mMaxDistance(10000.),
+ mRolloffFactor(1.),
+ mConeInnerAngle(360.),
+ mConeOuterAngle(360.),
+ mConeOuterGain(0.),
+ mLeftOverData(INT_MIN) {}
+
+ void RecvTimelineEvent(uint32_t aIndex, AudioTimelineEvent& aEvent) override {
+ MOZ_ASSERT(mDestination);
+ WebAudioUtils::ConvertAudioTimelineEventToTicks(aEvent, mDestination);
+ switch (aIndex) {
+ case PannerNode::POSITIONX:
+ mPositionX.InsertEvent<int64_t>(aEvent);
+ break;
+ case PannerNode::POSITIONY:
+ mPositionY.InsertEvent<int64_t>(aEvent);
+ break;
+ case PannerNode::POSITIONZ:
+ mPositionZ.InsertEvent<int64_t>(aEvent);
+ break;
+ case PannerNode::ORIENTATIONX:
+ mOrientationX.InsertEvent<int64_t>(aEvent);
+ break;
+ case PannerNode::ORIENTATIONY:
+ mOrientationY.InsertEvent<int64_t>(aEvent);
+ break;
+ case PannerNode::ORIENTATIONZ:
+ mOrientationZ.InsertEvent<int64_t>(aEvent);
+ break;
+ default:
+ NS_ERROR("Bad PannerNode TimelineParameter");
+ }
+ }
+
+ void CreateHRTFPanner() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mHRTFPanner) {
+ return;
+ }
+ // HRTFDatabaseLoader needs to be fetched on the main thread.
+ RefPtr<HRTFDatabaseLoader> loader =
+ HRTFDatabaseLoader::createAndLoadAsynchronouslyIfNecessary(
+ NodeMainThread()->Context()->SampleRate());
+ mHRTFPanner = MakeUnique<HRTFPanner>(
+ NodeMainThread()->Context()->SampleRate(), loader.forget());
+ }
+
+ void SetInt32Parameter(uint32_t aIndex, int32_t aParam) override {
+ switch (aIndex) {
+ case PannerNode::PANNING_MODEL:
+ switch (PanningModelType(aParam)) {
+ case PanningModelType::Equalpower:
+ mPanningModelFunction =
+ &PannerNodeEngine::EqualPowerPanningFunction;
+ break;
+ case PanningModelType::HRTF:
+ mPanningModelFunction = &PannerNodeEngine::HRTFPanningFunction;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("We should never see alternate names here");
+ break;
+ }
+ break;
+ case PannerNode::DISTANCE_MODEL:
+ switch (DistanceModelType(aParam)) {
+ case DistanceModelType::Inverse:
+ mDistanceModelFunction = &PannerNodeEngine::InverseGainFunction;
+ break;
+ case DistanceModelType::Linear:
+ mDistanceModelFunction = &PannerNodeEngine::LinearGainFunction;
+ break;
+ case DistanceModelType::Exponential:
+ mDistanceModelFunction = &PannerNodeEngine::ExponentialGainFunction;
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("We should never see alternate names here");
+ break;
+ }
+ break;
+ default:
+ NS_ERROR("Bad PannerNodeEngine Int32Parameter");
+ }
+ }
+ void SetDoubleParameter(uint32_t aIndex, double aParam) override {
+ switch (aIndex) {
+ case PannerNode::REF_DISTANCE:
+ mRefDistance = aParam;
+ break;
+ case PannerNode::MAX_DISTANCE:
+ mMaxDistance = aParam;
+ break;
+ case PannerNode::ROLLOFF_FACTOR:
+ mRolloffFactor = aParam;
+ break;
+ case PannerNode::CONE_INNER_ANGLE:
+ mConeInnerAngle = aParam;
+ break;
+ case PannerNode::CONE_OUTER_ANGLE:
+ mConeOuterAngle = aParam;
+ break;
+ case PannerNode::CONE_OUTER_GAIN:
+ mConeOuterGain = aParam;
+ break;
+ default:
+ NS_ERROR("Bad PannerNodeEngine DoubleParameter");
+ }
+ }
+
+ void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom,
+ const AudioBlock& aInput, AudioBlock* aOutput,
+ bool* aFinished) override {
+ TRACE("PannerNodeEngine::ProcessBlock");
+
+ if (aInput.IsNull()) {
+ // mLeftOverData != INT_MIN means that the panning model was HRTF and a
+ // tail-time reference was added. Even if the model is now equalpower,
+ // the reference will need to be removed.
+ if (mLeftOverData > 0 &&
+ mPanningModelFunction == &PannerNodeEngine::HRTFPanningFunction) {
+ mLeftOverData -= WEBAUDIO_BLOCK_SIZE;
+ } else {
+ if (mLeftOverData != INT_MIN) {
+ mLeftOverData = INT_MIN;
+ aTrack->ScheduleCheckForInactive();
+ mHRTFPanner->reset();
+
+ RefPtr<PlayingRefChangeHandler> refchanged =
+ new PlayingRefChangeHandler(aTrack,
+ PlayingRefChangeHandler::RELEASE);
+ aTrack->Graph()->DispatchToMainThreadStableState(refchanged.forget());
+ }
+ aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
+ return;
+ }
+ } else if (mPanningModelFunction ==
+ &PannerNodeEngine::HRTFPanningFunction) {
+ if (mLeftOverData == INT_MIN) {
+ RefPtr<PlayingRefChangeHandler> refchanged =
+ new PlayingRefChangeHandler(aTrack,
+ PlayingRefChangeHandler::ADDREF);
+ aTrack->Graph()->DispatchToMainThreadStableState(refchanged.forget());
+ }
+ mLeftOverData = mHRTFPanner->maxTailFrames();
+ }
+
+ TrackTime tick = mDestination->GraphTimeToTrackTime(aFrom);
+ (this->*mPanningModelFunction)(aInput, aOutput, tick);
+ }
+
+ bool IsActive() const override { return mLeftOverData != INT_MIN; }
+
+ void ComputeAzimuthAndElevation(const ThreeDPoint& position, float& aAzimuth,
+ float& aElevation);
+ float ComputeConeGain(const ThreeDPoint& position,
+ const ThreeDPoint& orientation);
+ // Compute how much the distance contributes to the gain reduction.
+ double ComputeDistanceGain(const ThreeDPoint& position);
+
+ void EqualPowerPanningFunction(const AudioBlock& aInput, AudioBlock* aOutput,
+ TrackTime tick);
+ void HRTFPanningFunction(const AudioBlock& aInput, AudioBlock* aOutput,
+ TrackTime tick);
+
+ float LinearGainFunction(double aDistance);
+ float InverseGainFunction(double aDistance);
+ float ExponentialGainFunction(double aDistance);
+
+ ThreeDPoint ConvertAudioParamTimelineTo3DP(AudioParamTimeline& aX,
+ AudioParamTimeline& aY,
+ AudioParamTimeline& aZ,
+ TrackTime& tick);
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override {
+ size_t amount = AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf);
+ if (mHRTFPanner) {
+ amount += mHRTFPanner->sizeOfIncludingThis(aMallocSizeOf);
+ }
+
+ return amount;
+ }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ RefPtr<AudioNodeTrack> mDestination;
+ // This member is set on the main thread, but is not accessed on the rendering
+ // thread untile mPanningModelFunction has changed, and this happens strictly
+ // later, via a MediaTrackGraph ControlMessage.
+ UniquePtr<HRTFPanner> mHRTFPanner;
+ RefPtr<AudioListenerEngine> mListenerEngine;
+ typedef void (PannerNodeEngine::*PanningModelFunction)(
+ const AudioBlock& aInput, AudioBlock* aOutput, TrackTime tick);
+ PanningModelFunction mPanningModelFunction;
+ typedef float (PannerNodeEngine::*DistanceModelFunction)(double aDistance);
+ DistanceModelFunction mDistanceModelFunction;
+ AudioParamTimeline mPositionX;
+ AudioParamTimeline mPositionY;
+ AudioParamTimeline mPositionZ;
+ AudioParamTimeline mOrientationX;
+ AudioParamTimeline mOrientationY;
+ AudioParamTimeline mOrientationZ;
+ double mRefDistance;
+ double mMaxDistance;
+ double mRolloffFactor;
+ double mConeInnerAngle;
+ double mConeOuterAngle;
+ double mConeOuterGain;
+ int mLeftOverData;
+};
+
+PannerNode::PannerNode(AudioContext* aContext)
+ : AudioNode(aContext, 2, ChannelCountMode::Clamped_max,
+ ChannelInterpretation::Speakers)
+ // Please keep these default values consistent with
+ // PannerNodeEngine::PannerNodeEngine above.
+ ,
+ mPanningModel(PanningModelType::Equalpower),
+ mDistanceModel(DistanceModelType::Inverse),
+ mRefDistance(1.),
+ mMaxDistance(10000.),
+ mRolloffFactor(1.),
+ mConeInnerAngle(360.),
+ mConeOuterAngle(360.),
+ mConeOuterGain(0.) {
+ mPositionX = CreateAudioParam(PannerNode::POSITIONX, u"PositionX"_ns, 0.f);
+ mPositionY = CreateAudioParam(PannerNode::POSITIONY, u"PositionY"_ns, 0.f);
+ mPositionZ = CreateAudioParam(PannerNode::POSITIONZ, u"PositionZ"_ns, 0.f);
+ mOrientationX =
+ CreateAudioParam(PannerNode::ORIENTATIONX, u"OrientationX"_ns, 1.0f);
+ mOrientationY =
+ CreateAudioParam(PannerNode::ORIENTATIONY, u"OrientationY"_ns, 0.f);
+ mOrientationZ =
+ CreateAudioParam(PannerNode::ORIENTATIONZ, u"OrientationZ"_ns, 0.f);
+ mTrack = AudioNodeTrack::Create(
+ aContext,
+ new PannerNodeEngine(this, aContext->Destination(),
+ aContext->Listener()->Engine()),
+ AudioNodeTrack::NO_TRACK_FLAGS, aContext->Graph());
+}
+
+/* static */
+already_AddRefed<PannerNode> PannerNode::Create(AudioContext& aAudioContext,
+ const PannerOptions& aOptions,
+ ErrorResult& aRv) {
+ RefPtr<PannerNode> audioNode = new PannerNode(&aAudioContext);
+
+ audioNode->Initialize(aOptions, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ audioNode->SetPanningModel(aOptions.mPanningModel);
+ audioNode->SetDistanceModel(aOptions.mDistanceModel);
+ audioNode->mPositionX->SetInitialValue(aOptions.mPositionX);
+ audioNode->mPositionY->SetInitialValue(aOptions.mPositionY);
+ audioNode->mPositionZ->SetInitialValue(aOptions.mPositionZ);
+ audioNode->mOrientationX->SetInitialValue(aOptions.mOrientationX);
+ audioNode->mOrientationY->SetInitialValue(aOptions.mOrientationY);
+ audioNode->mOrientationZ->SetInitialValue(aOptions.mOrientationZ);
+ audioNode->SetRefDistance(aOptions.mRefDistance, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+ audioNode->SetMaxDistance(aOptions.mMaxDistance, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+ audioNode->SetRolloffFactor(aOptions.mRolloffFactor, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+ audioNode->SetConeInnerAngle(aOptions.mConeInnerAngle);
+ audioNode->SetConeOuterAngle(aOptions.mConeOuterAngle);
+ audioNode->SetConeOuterGain(aOptions.mConeOuterGain, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ return audioNode.forget();
+}
+
+void PannerNode::SetPanningModel(PanningModelType aPanningModel) {
+ mPanningModel = aPanningModel;
+ if (mPanningModel == PanningModelType::HRTF) {
+ // We can set the engine's `mHRTFPanner` member here from the main thread,
+ // because the engine will not touch it from the MediaTrackGraph
+ // thread until the PANNING_MODEL message sent below is received.
+ static_cast<PannerNodeEngine*>(mTrack->Engine())->CreateHRTFPanner();
+ }
+ SendInt32ParameterToTrack(PANNING_MODEL, int32_t(mPanningModel));
+}
+
+static bool SetParamFromDouble(AudioParam* aParam, double aValue,
+ const char (&aParamName)[2], ErrorResult& aRv) {
+ float value = static_cast<float>(aValue);
+ if (!std::isfinite(value)) {
+ aRv.ThrowTypeError<MSG_NOT_FINITE>(aParamName);
+ return false;
+ }
+ aParam->SetValue(value, aRv);
+ return !aRv.Failed();
+}
+
+void PannerNode::SetPosition(double aX, double aY, double aZ,
+ ErrorResult& aRv) {
+ if (!SetParamFromDouble(mPositionX, aX, "x", aRv)) {
+ return;
+ }
+ if (!SetParamFromDouble(mPositionY, aY, "y", aRv)) {
+ return;
+ }
+ SetParamFromDouble(mPositionZ, aZ, "z", aRv);
+}
+
+void PannerNode::SetOrientation(double aX, double aY, double aZ,
+ ErrorResult& aRv) {
+ if (!SetParamFromDouble(mOrientationX, aX, "x", aRv)) {
+ return;
+ }
+ if (!SetParamFromDouble(mOrientationY, aY, "y", aRv)) {
+ return;
+ }
+ SetParamFromDouble(mOrientationZ, aZ, "z", aRv);
+}
+
+size_t PannerNode::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ return AudioNode::SizeOfExcludingThis(aMallocSizeOf);
+}
+
+size_t PannerNode::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
+
+JSObject* PannerNode::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return PannerNode_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+// Those three functions are described in the spec.
+float PannerNodeEngine::LinearGainFunction(double aDistance) {
+ return 1 - mRolloffFactor *
+ (std::max(std::min(aDistance, mMaxDistance), mRefDistance) -
+ mRefDistance) /
+ (mMaxDistance - mRefDistance);
+}
+
+float PannerNodeEngine::InverseGainFunction(double aDistance) {
+ return mRefDistance /
+ (mRefDistance +
+ mRolloffFactor * (std::max(aDistance, mRefDistance) - mRefDistance));
+}
+
+float PannerNodeEngine::ExponentialGainFunction(double aDistance) {
+ return pow(std::max(aDistance, mRefDistance) / mRefDistance, -mRolloffFactor);
+}
+
+void PannerNodeEngine::HRTFPanningFunction(const AudioBlock& aInput,
+ AudioBlock* aOutput,
+ TrackTime tick) {
+ // The output of this node is always stereo, no matter what the inputs are.
+ aOutput->AllocateChannels(2);
+
+ float azimuth, elevation;
+
+ ThreeDPoint position =
+ ConvertAudioParamTimelineTo3DP(mPositionX, mPositionY, mPositionZ, tick);
+ ThreeDPoint orientation = ConvertAudioParamTimelineTo3DP(
+ mOrientationX, mOrientationY, mOrientationZ, tick);
+ if (!orientation.IsZero()) {
+ orientation.Normalize();
+ }
+ ComputeAzimuthAndElevation(position, azimuth, elevation);
+
+ AudioBlock input = aInput;
+ // Gain is applied before the delay and convolution of the HRTF.
+ input.mVolume *=
+ ComputeConeGain(position, orientation) * ComputeDistanceGain(position);
+
+ mHRTFPanner->pan(azimuth, elevation, &input, aOutput);
+}
+
+ThreeDPoint PannerNodeEngine::ConvertAudioParamTimelineTo3DP(
+ AudioParamTimeline& aX, AudioParamTimeline& aY, AudioParamTimeline& aZ,
+ TrackTime& tick) {
+ return ThreeDPoint(aX.GetValueAtTime(tick), aY.GetValueAtTime(tick),
+ aZ.GetValueAtTime(tick));
+}
+
+void PannerNodeEngine::EqualPowerPanningFunction(const AudioBlock& aInput,
+ AudioBlock* aOutput,
+ TrackTime tick) {
+ float azimuth, elevation, gainL, gainR, normalizedAzimuth, distanceGain,
+ coneGain;
+ int inputChannels = aInput.ChannelCount();
+
+ // Optimize the case where the position and orientation is constant for this
+ // processing block: we can just apply a constant gain on the left and right
+ // channel
+ if (mPositionX.HasSimpleValue() && mPositionY.HasSimpleValue() &&
+ mPositionZ.HasSimpleValue() && mOrientationX.HasSimpleValue() &&
+ mOrientationY.HasSimpleValue() && mOrientationZ.HasSimpleValue()) {
+ ThreeDPoint position = ConvertAudioParamTimelineTo3DP(
+ mPositionX, mPositionY, mPositionZ, tick);
+ ThreeDPoint orientation = ConvertAudioParamTimelineTo3DP(
+ mOrientationX, mOrientationY, mOrientationZ, tick);
+ if (!orientation.IsZero()) {
+ orientation.Normalize();
+ }
+
+ // For a stereo source, when both the listener and the panner are in
+ // the same spot, and no cone gain is specified, this node is noop.
+ if (inputChannels == 2 && mListenerEngine->Position() == position &&
+ mConeInnerAngle == 360 && mConeOuterAngle == 360) {
+ *aOutput = aInput;
+ return;
+ }
+
+ ComputeAzimuthAndElevation(position, azimuth, elevation);
+ coneGain = ComputeConeGain(position, orientation);
+
+ // The following algorithm is described in the spec.
+ // Clamp azimuth in the [-90, 90] range.
+ azimuth = std::min(180.f, std::max(-180.f, azimuth));
+
+ // Wrap around
+ if (azimuth < -90.f) {
+ azimuth = -180.f - azimuth;
+ } else if (azimuth > 90) {
+ azimuth = 180.f - azimuth;
+ }
+
+ // Normalize the value in the [0, 1] range.
+ if (inputChannels == 1) {
+ normalizedAzimuth = (azimuth + 90.f) / 180.f;
+ } else {
+ if (azimuth <= 0) {
+ normalizedAzimuth = (azimuth + 90.f) / 90.f;
+ } else {
+ normalizedAzimuth = azimuth / 90.f;
+ }
+ }
+
+ distanceGain = ComputeDistanceGain(position);
+
+ // Actually compute the left and right gain.
+ gainL = cos(0.5 * M_PI * normalizedAzimuth);
+ gainR = sin(0.5 * M_PI * normalizedAzimuth);
+
+ // Compute the output.
+ ApplyStereoPanning(aInput, aOutput, gainL, gainR, azimuth <= 0);
+
+ aOutput->mVolume *= distanceGain * coneGain;
+ } else {
+ float positionX[WEBAUDIO_BLOCK_SIZE];
+ float positionY[WEBAUDIO_BLOCK_SIZE];
+ float positionZ[WEBAUDIO_BLOCK_SIZE];
+ float orientationX[WEBAUDIO_BLOCK_SIZE];
+ float orientationY[WEBAUDIO_BLOCK_SIZE];
+ float orientationZ[WEBAUDIO_BLOCK_SIZE];
+
+ if (!mPositionX.HasSimpleValue()) {
+ mPositionX.GetValuesAtTime(tick, positionX, WEBAUDIO_BLOCK_SIZE);
+ } else {
+ positionX[0] = mPositionX.GetValueAtTime(tick);
+ }
+ if (!mPositionY.HasSimpleValue()) {
+ mPositionY.GetValuesAtTime(tick, positionY, WEBAUDIO_BLOCK_SIZE);
+ } else {
+ positionY[0] = mPositionY.GetValueAtTime(tick);
+ }
+ if (!mPositionZ.HasSimpleValue()) {
+ mPositionZ.GetValuesAtTime(tick, positionZ, WEBAUDIO_BLOCK_SIZE);
+ } else {
+ positionZ[0] = mPositionZ.GetValueAtTime(tick);
+ }
+ if (!mOrientationX.HasSimpleValue()) {
+ mOrientationX.GetValuesAtTime(tick, orientationX, WEBAUDIO_BLOCK_SIZE);
+ } else {
+ orientationX[0] = mOrientationX.GetValueAtTime(tick);
+ }
+ if (!mOrientationY.HasSimpleValue()) {
+ mOrientationY.GetValuesAtTime(tick, orientationY, WEBAUDIO_BLOCK_SIZE);
+ } else {
+ orientationY[0] = mOrientationY.GetValueAtTime(tick);
+ }
+ if (!mOrientationZ.HasSimpleValue()) {
+ mOrientationZ.GetValuesAtTime(tick, orientationZ, WEBAUDIO_BLOCK_SIZE);
+ } else {
+ orientationZ[0] = mOrientationZ.GetValueAtTime(tick);
+ }
+
+ float buffer[3 * WEBAUDIO_BLOCK_SIZE + 4];
+ bool onLeft[WEBAUDIO_BLOCK_SIZE];
+
+ float* alignedPanningL = ALIGNED16(buffer);
+ float* alignedPanningR = alignedPanningL + WEBAUDIO_BLOCK_SIZE;
+ float* alignedGain = alignedPanningR + WEBAUDIO_BLOCK_SIZE;
+ ASSERT_ALIGNED16(alignedPanningL);
+ ASSERT_ALIGNED16(alignedPanningR);
+ ASSERT_ALIGNED16(alignedGain);
+
+ for (size_t counter = 0; counter < WEBAUDIO_BLOCK_SIZE; ++counter) {
+ ThreeDPoint position(
+ mPositionX.HasSimpleValue() ? positionX[0] : positionX[counter],
+ mPositionY.HasSimpleValue() ? positionY[0] : positionY[counter],
+ mPositionZ.HasSimpleValue() ? positionZ[0] : positionZ[counter]);
+ ThreeDPoint orientation(
+ mOrientationX.HasSimpleValue() ? orientationX[0]
+ : orientationX[counter],
+ mOrientationY.HasSimpleValue() ? orientationY[0]
+ : orientationY[counter],
+ mOrientationZ.HasSimpleValue() ? orientationZ[0]
+ : orientationZ[counter]);
+ if (!orientation.IsZero()) {
+ orientation.Normalize();
+ }
+
+ ComputeAzimuthAndElevation(position, azimuth, elevation);
+ coneGain = ComputeConeGain(position, orientation);
+
+ // The following algorithm is described in the spec.
+ // Clamp azimuth in the [-90, 90] range.
+ azimuth = std::min(180.f, std::max(-180.f, azimuth));
+
+ // Wrap around
+ if (azimuth < -90.f) {
+ azimuth = -180.f - azimuth;
+ } else if (azimuth > 90) {
+ azimuth = 180.f - azimuth;
+ }
+
+ // Normalize the value in the [0, 1] range.
+ if (inputChannels == 1) {
+ normalizedAzimuth = (azimuth + 90.f) / 180.f;
+ } else {
+ if (azimuth <= 0) {
+ normalizedAzimuth = (azimuth + 90.f) / 90.f;
+ } else {
+ normalizedAzimuth = azimuth / 90.f;
+ }
+ }
+
+ distanceGain = ComputeDistanceGain(position);
+
+ // Actually compute the left and right gain.
+ float gainL = cos(0.5 * M_PI * normalizedAzimuth);
+ float gainR = sin(0.5 * M_PI * normalizedAzimuth);
+
+ alignedPanningL[counter] = gainL;
+ alignedPanningR[counter] = gainR;
+ alignedGain[counter] = distanceGain * coneGain;
+ onLeft[counter] = azimuth <= 0;
+ }
+
+ // Apply the panning to the output buffer
+ ApplyStereoPanning(aInput, aOutput, alignedPanningL, alignedPanningR,
+ onLeft);
+
+ // Apply the input volume, cone and distance gain to the output buffer.
+ float* outputL = aOutput->ChannelFloatsForWrite(0);
+ float* outputR = aOutput->ChannelFloatsForWrite(1);
+ AudioBlockInPlaceScale(outputL, alignedGain);
+ AudioBlockInPlaceScale(outputR, alignedGain);
+ }
+}
+
+// This algorithm is specified in the webaudio spec.
+void PannerNodeEngine::ComputeAzimuthAndElevation(const ThreeDPoint& position,
+ float& aAzimuth,
+ float& aElevation) {
+ ThreeDPoint sourceListener = position - mListenerEngine->Position();
+ if (sourceListener.IsZero()) {
+ aAzimuth = 0.0;
+ aElevation = 0.0;
+ return;
+ }
+
+ sourceListener.Normalize();
+
+ // Project the source-listener vector on the x-z plane.
+ const ThreeDPoint& listenerFront = mListenerEngine->FrontVector();
+ const ThreeDPoint& listenerRight = mListenerEngine->RightVector();
+ ThreeDPoint up = listenerRight.CrossProduct(listenerFront);
+
+ double upProjection = sourceListener.DotProduct(up);
+ aElevation = 90 - 180 * acos(upProjection) / M_PI;
+
+ if (aElevation > 90) {
+ aElevation = 180 - aElevation;
+ } else if (aElevation < -90) {
+ aElevation = -180 - aElevation;
+ }
+
+ ThreeDPoint projectedSource = sourceListener - up * upProjection;
+ if (projectedSource.IsZero()) {
+ // source - listener direction is up or down.
+ aAzimuth = 0.0;
+ return;
+ }
+ projectedSource.Normalize();
+
+ // Actually compute the angle, and convert to degrees
+ double projection = projectedSource.DotProduct(listenerRight);
+ aAzimuth = 180 * acos(projection) / M_PI;
+
+ // Compute whether the source is in front or behind the listener.
+ double frontBack = projectedSource.DotProduct(listenerFront);
+ if (frontBack < 0) {
+ aAzimuth = 360 - aAzimuth;
+ }
+ // Rotate the azimuth so it is relative to the listener front vector instead
+ // of the right vector.
+ if ((aAzimuth >= 0) && (aAzimuth <= 270)) {
+ aAzimuth = 90 - aAzimuth;
+ } else {
+ aAzimuth = 450 - aAzimuth;
+ }
+}
+
+// This algorithm is described in the WebAudio spec.
+float PannerNodeEngine::ComputeConeGain(const ThreeDPoint& position,
+ const ThreeDPoint& orientation) {
+ // Omnidirectional source
+ if (orientation.IsZero() ||
+ ((mConeInnerAngle == 360) && (mConeOuterAngle == 360))) {
+ return 1;
+ }
+
+ // Normalized source-listener vector
+ ThreeDPoint sourceToListener = mListenerEngine->Position() - position;
+ sourceToListener.Normalize();
+
+ // Angle between the source orientation vector and the source-listener vector
+ double dotProduct = sourceToListener.DotProduct(orientation);
+ double angle = 180 * acos(dotProduct) / M_PI;
+ double absAngle = fabs(angle);
+
+ // Divide by 2 here since API is entire angle (not half-angle)
+ double absInnerAngle = fabs(mConeInnerAngle) / 2;
+ double absOuterAngle = fabs(mConeOuterAngle) / 2;
+ double gain = 1;
+
+ if (absAngle <= absInnerAngle) {
+ // No attenuation
+ gain = 1;
+ } else if (absAngle >= absOuterAngle) {
+ // Max attenuation
+ gain = mConeOuterGain;
+ } else {
+ // Between inner and outer cones
+ // inner -> outer, x goes from 0 -> 1
+ double x = (absAngle - absInnerAngle) / (absOuterAngle - absInnerAngle);
+ gain = (1 - x) + mConeOuterGain * x;
+ }
+
+ return gain;
+}
+
+double PannerNodeEngine::ComputeDistanceGain(const ThreeDPoint& position) {
+ ThreeDPoint distanceVec = position - mListenerEngine->Position();
+ float distance = sqrt(distanceVec.DotProduct(distanceVec));
+ return std::max(0.0f, (this->*mDistanceModelFunction)(distance));
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/PannerNode.h b/dom/media/webaudio/PannerNode.h
new file mode 100644
index 0000000000..94876c371c
--- /dev/null
+++ b/dom/media/webaudio/PannerNode.h
@@ -0,0 +1,221 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef PannerNode_h_
+#define PannerNode_h_
+
+#include "AudioNode.h"
+#include "AudioParam.h"
+#include "nsPrintfCString.h"
+#include "mozilla/dom/PannerNodeBinding.h"
+#include "ThreeDPoint.h"
+#include <limits>
+#include <set>
+
+namespace mozilla::dom {
+
+class AudioContext;
+class AudioBufferSourceNode;
+struct PannerOptions;
+
+class PannerNode final : public AudioNode {
+ public:
+ static already_AddRefed<PannerNode> Create(AudioContext& aAudioContext,
+ const PannerOptions& aOptions,
+ ErrorResult& aRv);
+
+ static already_AddRefed<PannerNode> Constructor(const GlobalObject& aGlobal,
+ AudioContext& aAudioContext,
+ const PannerOptions& aOptions,
+ ErrorResult& aRv) {
+ return Create(aAudioContext, aOptions, aRv);
+ }
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ void SetChannelCount(uint32_t aChannelCount, ErrorResult& aRv) override {
+ if (aChannelCount > 2) {
+ aRv.ThrowNotSupportedError(
+ nsPrintfCString("%u is greater than 2", aChannelCount));
+ return;
+ }
+ AudioNode::SetChannelCount(aChannelCount, aRv);
+ }
+ void SetChannelCountModeValue(ChannelCountMode aMode,
+ ErrorResult& aRv) override {
+ if (aMode == ChannelCountMode::Max) {
+ aRv.ThrowNotSupportedError("Cannot set channel count mode to \"max\"");
+ return;
+ }
+ AudioNode::SetChannelCountModeValue(aMode, aRv);
+ }
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(PannerNode, AudioNode)
+
+ PanningModelType PanningModel() const { return mPanningModel; }
+ void SetPanningModel(PanningModelType aPanningModel);
+
+ DistanceModelType DistanceModel() const { return mDistanceModel; }
+ void SetDistanceModel(DistanceModelType aDistanceModel) {
+ mDistanceModel = aDistanceModel;
+ SendInt32ParameterToTrack(DISTANCE_MODEL, int32_t(mDistanceModel));
+ }
+
+ void SetPosition(double aX, double aY, double aZ, ErrorResult& aRv);
+ void SetOrientation(double aX, double aY, double aZ, ErrorResult& aRv);
+
+ double RefDistance() const { return mRefDistance; }
+ void SetRefDistance(double aRefDistance, ErrorResult& aRv) {
+ if (WebAudioUtils::FuzzyEqual(mRefDistance, aRefDistance)) {
+ return;
+ }
+
+ if (aRefDistance < 0) {
+ aRv.ThrowRangeError(
+ "The refDistance value passed to PannerNode must not be negative.");
+ return;
+ }
+
+ mRefDistance = aRefDistance;
+ SendDoubleParameterToTrack(REF_DISTANCE, mRefDistance);
+ }
+
+ double MaxDistance() const { return mMaxDistance; }
+ void SetMaxDistance(double aMaxDistance, ErrorResult& aRv) {
+ if (WebAudioUtils::FuzzyEqual(mMaxDistance, aMaxDistance)) {
+ return;
+ }
+
+ if (aMaxDistance <= 0) {
+ aRv.ThrowRangeError(
+ "The maxDistance value passed to PannerNode must be positive.");
+ return;
+ }
+
+ mMaxDistance = aMaxDistance;
+ SendDoubleParameterToTrack(MAX_DISTANCE, mMaxDistance);
+ }
+
+ double RolloffFactor() const { return mRolloffFactor; }
+ void SetRolloffFactor(double aRolloffFactor, ErrorResult& aRv) {
+ if (WebAudioUtils::FuzzyEqual(mRolloffFactor, aRolloffFactor)) {
+ return;
+ }
+
+ if (aRolloffFactor < 0) {
+ aRv.ThrowRangeError(
+ "The rolloffFactor value passed to PannerNode must not be negative.");
+ return;
+ }
+
+ mRolloffFactor = aRolloffFactor;
+ SendDoubleParameterToTrack(ROLLOFF_FACTOR, mRolloffFactor);
+ }
+
+ double ConeInnerAngle() const { return mConeInnerAngle; }
+ void SetConeInnerAngle(double aConeInnerAngle) {
+ if (WebAudioUtils::FuzzyEqual(mConeInnerAngle, aConeInnerAngle)) {
+ return;
+ }
+ mConeInnerAngle = aConeInnerAngle;
+ SendDoubleParameterToTrack(CONE_INNER_ANGLE, mConeInnerAngle);
+ }
+
+ double ConeOuterAngle() const { return mConeOuterAngle; }
+ void SetConeOuterAngle(double aConeOuterAngle) {
+ if (WebAudioUtils::FuzzyEqual(mConeOuterAngle, aConeOuterAngle)) {
+ return;
+ }
+ mConeOuterAngle = aConeOuterAngle;
+ SendDoubleParameterToTrack(CONE_OUTER_ANGLE, mConeOuterAngle);
+ }
+
+ double ConeOuterGain() const { return mConeOuterGain; }
+ void SetConeOuterGain(double aConeOuterGain, ErrorResult& aRv) {
+ if (WebAudioUtils::FuzzyEqual(mConeOuterGain, aConeOuterGain)) {
+ return;
+ }
+
+ if (aConeOuterGain < 0 || aConeOuterGain > 1) {
+ aRv.ThrowInvalidStateError(
+ nsPrintfCString("%g is not in the range [0, 1]", aConeOuterGain));
+ return;
+ }
+
+ mConeOuterGain = aConeOuterGain;
+ SendDoubleParameterToTrack(CONE_OUTER_GAIN, mConeOuterGain);
+ }
+
+ AudioParam* PositionX() { return mPositionX; }
+
+ AudioParam* PositionY() { return mPositionY; }
+
+ AudioParam* PositionZ() { return mPositionZ; }
+
+ AudioParam* OrientationX() { return mOrientationX; }
+
+ AudioParam* OrientationY() { return mOrientationY; }
+
+ AudioParam* OrientationZ() { return mOrientationZ; }
+
+ const char* NodeType() const override { return "PannerNode"; }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override;
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override;
+
+ private:
+ explicit PannerNode(AudioContext* aContext);
+ ~PannerNode() = default;
+
+ friend class AudioListener;
+ friend class PannerNodeEngine;
+ enum EngineParameters {
+ PANNING_MODEL,
+ DISTANCE_MODEL,
+ POSITION,
+ POSITIONX,
+ POSITIONY,
+ POSITIONZ,
+ ORIENTATION, // unit length or zero
+ ORIENTATIONX,
+ ORIENTATIONY,
+ ORIENTATIONZ,
+ REF_DISTANCE,
+ MAX_DISTANCE,
+ ROLLOFF_FACTOR,
+ CONE_INNER_ANGLE,
+ CONE_OUTER_ANGLE,
+ CONE_OUTER_GAIN
+ };
+
+ ThreeDPoint ConvertAudioParamTo3DP(RefPtr<AudioParam> aX,
+ RefPtr<AudioParam> aY,
+ RefPtr<AudioParam> aZ) {
+ return ThreeDPoint(aX->GetValue(), aY->GetValue(), aZ->GetValue());
+ }
+
+ PanningModelType mPanningModel;
+ DistanceModelType mDistanceModel;
+ RefPtr<AudioParam> mPositionX;
+ RefPtr<AudioParam> mPositionY;
+ RefPtr<AudioParam> mPositionZ;
+ RefPtr<AudioParam> mOrientationX;
+ RefPtr<AudioParam> mOrientationY;
+ RefPtr<AudioParam> mOrientationZ;
+
+ double mRefDistance;
+ double mMaxDistance;
+ double mRolloffFactor;
+ double mConeInnerAngle;
+ double mConeOuterAngle;
+ double mConeOuterGain;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webaudio/PanningUtils.h b/dom/media/webaudio/PanningUtils.h
new file mode 100644
index 0000000000..eef8ae78c9
--- /dev/null
+++ b/dom/media/webaudio/PanningUtils.h
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef PANNING_UTILS_H
+#define PANNING_UTILS_H
+
+#include "AudioSegment.h"
+#include "AudioNodeEngine.h"
+
+namespace mozilla::dom {
+
+template <typename T>
+void GainMonoToStereo(const AudioBlock& aInput, AudioBlock* aOutput, T aGainL,
+ T aGainR) {
+ float* outputL = aOutput->ChannelFloatsForWrite(0);
+ float* outputR = aOutput->ChannelFloatsForWrite(1);
+ const float* input = static_cast<const float*>(aInput.mChannelData[0]);
+
+ MOZ_ASSERT(aInput.ChannelCount() == 1);
+ MOZ_ASSERT(aOutput->ChannelCount() == 2);
+
+ AudioBlockPanMonoToStereo(input, aGainL, aGainR, outputL, outputR);
+}
+
+// T can be float or an array of float, and U can be bool or an array of bool,
+// depending if the value of the parameters are constant for this block.
+template <typename T, typename U>
+void GainStereoToStereo(const AudioBlock& aInput, AudioBlock* aOutput, T aGainL,
+ T aGainR, U aOnLeft) {
+ float* outputL = aOutput->ChannelFloatsForWrite(0);
+ float* outputR = aOutput->ChannelFloatsForWrite(1);
+ const float* inputL = static_cast<const float*>(aInput.mChannelData[0]);
+ const float* inputR = static_cast<const float*>(aInput.mChannelData[1]);
+
+ MOZ_ASSERT(aInput.ChannelCount() == 2);
+ MOZ_ASSERT(aOutput->ChannelCount() == 2);
+
+ AudioBlockPanStereoToStereo(inputL, inputR, aGainL, aGainR, aOnLeft, outputL,
+ outputR);
+}
+
+// T can be float or an array of float, and U can be bool or an array of bool,
+// depending if the value of the parameters are constant for this block.
+template <typename T, typename U>
+void ApplyStereoPanning(const AudioBlock& aInput, AudioBlock* aOutput, T aGainL,
+ T aGainR, U aOnLeft) {
+ aOutput->AllocateChannels(2);
+
+ if (aInput.ChannelCount() == 1) {
+ GainMonoToStereo(aInput, aOutput, aGainL, aGainR);
+ } else {
+ GainStereoToStereo(aInput, aOutput, aGainL, aGainR, aOnLeft);
+ }
+ aOutput->mVolume = aInput.mVolume;
+}
+
+} // namespace mozilla::dom
+
+#endif // PANNING_UTILS_H
diff --git a/dom/media/webaudio/PeriodicWave.cpp b/dom/media/webaudio/PeriodicWave.cpp
new file mode 100644
index 0000000000..e036161724
--- /dev/null
+++ b/dom/media/webaudio/PeriodicWave.cpp
@@ -0,0 +1,146 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "PeriodicWave.h"
+#include "AudioContext.h"
+#include "mozilla/dom/PeriodicWaveBinding.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(PeriodicWave, mContext)
+
+PeriodicWave::PeriodicWave(AudioContext* aContext, const float* aRealData,
+ const uint32_t aRealSize, const float* aImagData,
+ const uint32_t aImagSize,
+ const bool aDisableNormalization, ErrorResult& aRv)
+ : mContext(aContext), mDisableNormalization(aDisableNormalization) {
+ if (aRealData && aImagData && aRealSize != aImagSize) {
+ aRv.ThrowIndexSizeError("\"real\" and \"imag\" are different in length");
+ return;
+ }
+
+ uint32_t length = 0;
+ if (aRealData) {
+ length = aRealSize;
+ } else if (aImagData) {
+ length = aImagSize;
+ } else {
+ // If nothing has been passed, this PeriodicWave will be a sine wave: 2
+ // elements for each array, the second imaginary component set to 1.0.
+ length = 2;
+ }
+
+ if (length < 2) {
+ aRv.ThrowIndexSizeError(
+ "\"real\" and \"imag\" must have a length of "
+ "at least 2");
+ return;
+ }
+
+ MOZ_ASSERT(aContext);
+ MOZ_ASSERT((aRealData || aImagData) || length == 2);
+
+ // Caller should have checked this and thrown.
+ MOZ_ASSERT(length >= 2);
+ mCoefficients.mDuration = length;
+
+ // Copy coefficient data.
+ // The SharedBuffer and two arrays share a single allocation.
+ CheckedInt<size_t> bufferSize(sizeof(float));
+ bufferSize *= length;
+ bufferSize *= 2;
+ RefPtr<SharedBuffer> buffer = SharedBuffer::Create(bufferSize, fallible);
+ if (!buffer) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ auto data = static_cast<float*>(buffer->Data());
+ mCoefficients.mBuffer = std::move(buffer);
+
+ if (!aRealData && !aImagData) {
+ PodZero(data, length);
+ mCoefficients.mChannelData.AppendElement(data);
+ data += length;
+ data[0] = 0.0f;
+ data[1] = 1.0f;
+ mCoefficients.mChannelData.AppendElement(data);
+ } else {
+ if (aRealData) {
+ PodCopy(data, aRealData, length);
+ } else {
+ PodZero(data, length);
+ }
+ mCoefficients.mChannelData.AppendElement(data);
+
+ data += length;
+ if (aImagData) {
+ PodCopy(data, aImagData, length);
+ } else {
+ PodZero(data, length);
+ }
+ mCoefficients.mChannelData.AppendElement(data);
+ }
+ mCoefficients.mVolume = 1.0f;
+ mCoefficients.mBufferFormat = AUDIO_FORMAT_FLOAT32;
+}
+
+/* static */
+already_AddRefed<PeriodicWave> PeriodicWave::Constructor(
+ const GlobalObject& aGlobal, AudioContext& aAudioContext,
+ const PeriodicWaveOptions& aOptions, ErrorResult& aRv) {
+ const float* realData;
+ const float* imagData;
+ uint32_t realSize;
+ uint32_t imagSize;
+
+ if (aOptions.mReal.WasPassed()) {
+ realData = aOptions.mReal.Value().Elements();
+ realSize = aOptions.mReal.Value().Length();
+ } else {
+ realData = nullptr;
+ realSize = 0;
+ }
+
+ if (aOptions.mImag.WasPassed()) {
+ imagData = aOptions.mImag.Value().Elements();
+ imagSize = aOptions.mImag.Value().Length();
+ } else {
+ imagData = nullptr;
+ imagSize = 0;
+ }
+
+ RefPtr<PeriodicWave> wave =
+ new PeriodicWave(&aAudioContext, realData, realSize, imagData, imagSize,
+ aOptions.mDisableNormalization, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ return wave.forget();
+}
+
+size_t PeriodicWave::SizeOfExcludingThisIfNotShared(
+ MallocSizeOf aMallocSizeOf) const {
+ // Not owned:
+ // - mContext
+ size_t amount = 0;
+ amount += mCoefficients.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+
+ return amount;
+}
+
+size_t PeriodicWave::SizeOfIncludingThisIfNotShared(
+ MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThisIfNotShared(aMallocSizeOf);
+}
+
+JSObject* PeriodicWave::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return PeriodicWave_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/PeriodicWave.h b/dom/media/webaudio/PeriodicWave.h
new file mode 100644
index 0000000000..18408789a9
--- /dev/null
+++ b/dom/media/webaudio/PeriodicWave.h
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef PeriodicWave_h_
+#define PeriodicWave_h_
+
+#include "nsWrapperCache.h"
+#include "nsCycleCollectionParticipant.h"
+#include "mozilla/Attributes.h"
+#include "AudioContext.h"
+#include "AudioNodeEngine.h"
+
+namespace mozilla::dom {
+
+class AudioContext;
+struct PeriodicWaveOptions;
+
+class PeriodicWave final : public nsWrapperCache {
+ public:
+ PeriodicWave(AudioContext* aContext, const float* aRealData,
+ const uint32_t aRealSize, const float* aImagData,
+ const uint32_t aImagSize, const bool aDisableNormalization,
+ ErrorResult& aRv);
+
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(PeriodicWave)
+ NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(PeriodicWave)
+
+ static already_AddRefed<PeriodicWave> Constructor(
+ const GlobalObject& aGlobal, AudioContext& aAudioContext,
+ const PeriodicWaveOptions& aOptions, ErrorResult& aRv);
+
+ AudioContext* GetParentObject() const { return mContext; }
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ uint32_t DataLength() const { return mCoefficients.mDuration; }
+
+ bool DisableNormalization() const { return mDisableNormalization; }
+
+ const AudioChunk& GetThreadSharedBuffer() const { return mCoefficients; }
+
+ size_t SizeOfExcludingThisIfNotShared(MallocSizeOf aMallocSizeOf) const;
+ size_t SizeOfIncludingThisIfNotShared(MallocSizeOf aMallocSizeOf) const;
+
+ private:
+ ~PeriodicWave() = default;
+
+ AudioChunk mCoefficients;
+ RefPtr<AudioContext> mContext;
+ bool mDisableNormalization;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webaudio/PlayingRefChangeHandler.h b/dom/media/webaudio/PlayingRefChangeHandler.h
new file mode 100644
index 0000000000..cda01fdcd3
--- /dev/null
+++ b/dom/media/webaudio/PlayingRefChangeHandler.h
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef PlayingRefChangeHandler_h__
+#define PlayingRefChangeHandler_h__
+
+#include "nsThreadUtils.h"
+#include "AudioNodeTrack.h"
+
+namespace mozilla::dom {
+
+class PlayingRefChangeHandler final : public Runnable {
+ public:
+ enum ChangeType { ADDREF, RELEASE };
+ PlayingRefChangeHandler(AudioNodeTrack* aTrack, ChangeType aChange)
+ : Runnable("dom::PlayingRefChangeHandler"),
+ mTrack(aTrack),
+ mChange(aChange) {}
+
+ NS_IMETHOD Run() override {
+ RefPtr<AudioNode> node = mTrack->Engine()->NodeMainThread();
+ if (node) {
+ if (mChange == ADDREF) {
+ node->MarkActive();
+ } else if (mChange == RELEASE) {
+ node->MarkInactive();
+ }
+ }
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<AudioNodeTrack> mTrack;
+ ChangeType mChange;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webaudio/ReportDecodeResultTask.h b/dom/media/webaudio/ReportDecodeResultTask.h
new file mode 100644
index 0000000000..49b7e39e04
--- /dev/null
+++ b/dom/media/webaudio/ReportDecodeResultTask.h
@@ -0,0 +1,37 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef ReportDecodeResultTask_h_
+#define ReportDecodeResultTask_h_
+
+#include "mozilla/Attributes.h"
+#include "MediaBufferDecoder.h"
+
+namespace mozilla {
+
+class ReportDecodeResultTask final : public Runnable {
+ public:
+ ReportDecodeResultTask(DecodeJob& aDecodeJob, DecodeJob::ResultFn aFunction)
+ : mDecodeJob(aDecodeJob), mFunction(aFunction) {
+ MOZ_ASSERT(aFunction);
+ }
+
+ NS_IMETHOD Run() override {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ (mDecodeJob.*mFunction)();
+
+ return NS_OK;
+ }
+
+ private:
+ DecodeJob& mDecodeJob;
+ DecodeJob::ResultFn mFunction;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webaudio/ScriptProcessorNode.cpp b/dom/media/webaudio/ScriptProcessorNode.cpp
new file mode 100644
index 0000000000..aafbec4202
--- /dev/null
+++ b/dom/media/webaudio/ScriptProcessorNode.cpp
@@ -0,0 +1,549 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ScriptProcessorNode.h"
+#include "mozilla/dom/ScriptProcessorNodeBinding.h"
+#include "AudioBuffer.h"
+#include "AudioDestinationNode.h"
+#include "AudioNodeEngine.h"
+#include "AudioNodeTrack.h"
+#include "AudioProcessingEvent.h"
+#include "WebAudioUtils.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/PodOperations.h"
+#include <deque>
+#include "Tracing.h"
+
+namespace mozilla::dom {
+
+// The maximum latency, in seconds, that we can live with before dropping
+// buffers.
+static const float MAX_LATENCY_S = 0.5;
+
+// This class manages a queue of output buffers shared between
+// the main thread and the Media Track Graph thread.
+class SharedBuffers final {
+ private:
+ class OutputQueue final {
+ public:
+ explicit OutputQueue(const char* aName) : mMutex(aName) {}
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const
+ MOZ_REQUIRES(mMutex) {
+ mMutex.AssertCurrentThreadOwns();
+
+ size_t amount = 0;
+ for (size_t i = 0; i < mBufferList.size(); i++) {
+ amount += mBufferList[i].SizeOfExcludingThis(aMallocSizeOf, false);
+ }
+
+ return amount;
+ }
+
+ Mutex& Lock() const MOZ_RETURN_CAPABILITY(mMutex) {
+ return const_cast<OutputQueue*>(this)->mMutex;
+ }
+
+ size_t ReadyToConsume() const MOZ_REQUIRES(mMutex) {
+ // Accessed on both main thread and media graph thread.
+ mMutex.AssertCurrentThreadOwns();
+ return mBufferList.size();
+ }
+
+ // Produce one buffer
+ AudioChunk& Produce() MOZ_REQUIRES(mMutex) {
+ mMutex.AssertCurrentThreadOwns();
+ MOZ_ASSERT(NS_IsMainThread());
+ mBufferList.push_back(AudioChunk());
+ return mBufferList.back();
+ }
+
+ // Consumes one buffer.
+ AudioChunk Consume() MOZ_REQUIRES(mMutex) {
+ mMutex.AssertCurrentThreadOwns();
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(ReadyToConsume() > 0);
+ AudioChunk front = mBufferList.front();
+ mBufferList.pop_front();
+ return front;
+ }
+
+ // Empties the buffer queue.
+ void Clear() MOZ_REQUIRES(mMutex) {
+ mMutex.AssertCurrentThreadOwns();
+ mBufferList.clear();
+ }
+
+ private:
+ typedef std::deque<AudioChunk> BufferList;
+
+ // Synchronizes access to mBufferList. Note that it's the responsibility
+ // of the callers to perform the required locking, and we assert that every
+ // time we access mBufferList.
+ Mutex mMutex MOZ_UNANNOTATED;
+ // The list representing the queue.
+ BufferList mBufferList;
+ };
+
+ public:
+ explicit SharedBuffers(float aSampleRate)
+ : mOutputQueue("SharedBuffers::outputQueue"),
+ mDelaySoFar(TRACK_TIME_MAX),
+ mSampleRate(aSampleRate),
+ mLatency(0.0),
+ mDroppingBuffers(false) {}
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t amount = aMallocSizeOf(this);
+
+ {
+ MutexAutoLock lock(mOutputQueue.Lock());
+ amount += mOutputQueue.SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ return amount;
+ }
+
+ // main thread
+
+ // NotifyNodeIsConnected() may be called even when the state has not
+ // changed.
+ void NotifyNodeIsConnected(bool aIsConnected) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!aIsConnected) {
+ // Reset main thread state for FinishProducingOutputBuffer().
+ mLatency = 0.0f;
+ mLastEventTime = TimeStamp();
+ mDroppingBuffers = false;
+ // Don't flush the output buffer here because the graph thread may be
+ // using it now. The graph thread will flush when it knows it is
+ // disconnected.
+ }
+ mNodeIsConnected = aIsConnected;
+ }
+
+ void FinishProducingOutputBuffer(const AudioChunk& aBuffer) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mNodeIsConnected) {
+ // The output buffer is not used, and mLastEventTime will not be
+ // initialized until the node is re-connected.
+ return;
+ }
+
+ TimeStamp now = TimeStamp::Now();
+
+ if (mLastEventTime.IsNull()) {
+ mLastEventTime = now;
+ } else {
+ // When main thread blocking has built up enough so
+ // |mLatency > MAX_LATENCY_S|, frame dropping starts. It continues until
+ // the output buffer is completely empty, at which point the accumulated
+ // latency is also reset to 0.
+ // It could happen that the output queue becomes empty before the input
+ // node has fully caught up. In this case there will be events where
+ // |(now - mLastEventTime)| is very short, making mLatency negative.
+ // As this happens and the size of |mLatency| becomes greater than
+ // MAX_LATENCY_S, frame dropping starts again to maintain an as short
+ // output queue as possible.
+ float latency = (now - mLastEventTime).ToSeconds();
+ float bufferDuration = aBuffer.mDuration / mSampleRate;
+ mLatency += latency - bufferDuration;
+ mLastEventTime = now;
+ if (fabs(mLatency) > MAX_LATENCY_S) {
+ mDroppingBuffers = true;
+ }
+ }
+
+ MutexAutoLock lock(mOutputQueue.Lock());
+ if (mDroppingBuffers) {
+ if (mOutputQueue.ReadyToConsume()) {
+ return;
+ }
+ mDroppingBuffers = false;
+ mLatency = 0;
+ }
+
+ for (uint32_t offset = 0; offset < aBuffer.mDuration;
+ offset += WEBAUDIO_BLOCK_SIZE) {
+ AudioChunk& chunk = mOutputQueue.Produce();
+ chunk = aBuffer;
+ chunk.SliceTo(offset, offset + WEBAUDIO_BLOCK_SIZE);
+ }
+ }
+
+ // graph thread
+
+ AudioChunk GetOutputBuffer() {
+ MOZ_ASSERT(!NS_IsMainThread());
+ AudioChunk buffer;
+
+ {
+ MutexAutoLock lock(mOutputQueue.Lock());
+ if (mOutputQueue.ReadyToConsume() > 0) {
+ if (mDelaySoFar == TRACK_TIME_MAX) {
+ mDelaySoFar = 0;
+ }
+ buffer = mOutputQueue.Consume();
+ } else {
+ // If we're out of buffers to consume, just output silence
+ buffer.SetNull(WEBAUDIO_BLOCK_SIZE);
+ if (mDelaySoFar != TRACK_TIME_MAX) {
+ // Remember the delay that we just hit
+ mDelaySoFar += WEBAUDIO_BLOCK_SIZE;
+ }
+ }
+ }
+
+ return buffer;
+ }
+
+ TrackTime DelaySoFar() const {
+ MOZ_ASSERT(!NS_IsMainThread());
+ return mDelaySoFar == TRACK_TIME_MAX ? 0 : mDelaySoFar;
+ }
+
+ void Flush() {
+ MOZ_ASSERT(!NS_IsMainThread());
+ mDelaySoFar = TRACK_TIME_MAX;
+ {
+ MutexAutoLock lock(mOutputQueue.Lock());
+ mOutputQueue.Clear();
+ }
+ }
+
+ private:
+ OutputQueue mOutputQueue;
+ // How much delay we've seen so far. This measures the amount of delay
+ // caused by the main thread lagging behind in producing output buffers.
+ // TRACK_TIME_MAX means that we have not received our first buffer yet.
+ // Graph thread only.
+ TrackTime mDelaySoFar;
+ // The samplerate of the context.
+ const float mSampleRate;
+ // The remaining members are main thread only.
+ // This is the latency caused by the buffering. If this grows too high, we
+ // will drop buffers until it is acceptable.
+ float mLatency;
+ // This is the time at which we last produced a buffer, to detect if the main
+ // thread has been blocked.
+ TimeStamp mLastEventTime;
+ // True if we should be dropping buffers.
+ bool mDroppingBuffers;
+ // True iff the AudioNode has at least one input or output connected.
+ bool mNodeIsConnected;
+};
+
+class ScriptProcessorNodeEngine final : public AudioNodeEngine {
+ public:
+ ScriptProcessorNodeEngine(ScriptProcessorNode* aNode,
+ AudioDestinationNode* aDestination,
+ uint32_t aBufferSize,
+ uint32_t aNumberOfInputChannels)
+ : AudioNodeEngine(aNode),
+ mDestination(aDestination->Track()),
+ mSharedBuffers(new SharedBuffers(mDestination->mSampleRate)),
+ mBufferSize(aBufferSize),
+ mInputChannelCount(aNumberOfInputChannels),
+ mInputWriteIndex(0) {}
+
+ SharedBuffers* GetSharedBuffers() const { return mSharedBuffers.get(); }
+
+ enum {
+ IS_CONNECTED,
+ };
+
+ void SetInt32Parameter(uint32_t aIndex, int32_t aParam) override {
+ switch (aIndex) {
+ case IS_CONNECTED:
+ mIsConnected = aParam;
+ break;
+ default:
+ NS_ERROR("Bad Int32Parameter");
+ } // End index switch.
+ }
+
+ void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom,
+ const AudioBlock& aInput, AudioBlock* aOutput,
+ bool* aFinished) override {
+ TRACE("ScriptProcessorNodeEngine::ProcessBlock");
+
+ // This node is not connected to anything. Per spec, we don't fire the
+ // onaudioprocess event. We also want to clear out the input and output
+ // buffer queue, and output a null buffer.
+ if (!mIsConnected) {
+ aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
+ mSharedBuffers->Flush();
+ mInputWriteIndex = 0;
+ return;
+ }
+
+ // The input buffer is allocated lazily when non-null input is received.
+ if (!aInput.IsNull() && !mInputBuffer) {
+ mInputBuffer = ThreadSharedFloatArrayBufferList::Create(
+ mInputChannelCount, mBufferSize, fallible);
+ if (mInputBuffer && mInputWriteIndex) {
+ // Zero leading for null chunks that were skipped.
+ for (uint32_t i = 0; i < mInputChannelCount; ++i) {
+ float* channelData = mInputBuffer->GetDataForWrite(i);
+ PodZero(channelData, mInputWriteIndex);
+ }
+ }
+ }
+
+ // First, record our input buffer, if its allocation succeeded.
+ uint32_t inputChannelCount = mInputBuffer ? mInputBuffer->GetChannels() : 0;
+ for (uint32_t i = 0; i < inputChannelCount; ++i) {
+ float* writeData = mInputBuffer->GetDataForWrite(i) + mInputWriteIndex;
+ if (aInput.IsNull()) {
+ PodZero(writeData, aInput.GetDuration());
+ } else {
+ MOZ_ASSERT(aInput.GetDuration() == WEBAUDIO_BLOCK_SIZE, "sanity check");
+ MOZ_ASSERT(aInput.ChannelCount() == inputChannelCount);
+ AudioBlockCopyChannelWithScale(
+ static_cast<const float*>(aInput.mChannelData[i]), aInput.mVolume,
+ writeData);
+ }
+ }
+ mInputWriteIndex += aInput.GetDuration();
+
+ // Now, see if we have data to output
+ // Note that we need to do this before sending the buffer to the main
+ // thread so that our delay time is updated.
+ *aOutput = mSharedBuffers->GetOutputBuffer();
+
+ if (mInputWriteIndex >= mBufferSize) {
+ SendBuffersToMainThread(aTrack, aFrom);
+ mInputWriteIndex -= mBufferSize;
+ }
+ }
+
+ bool IsActive() const override {
+ // Could return false when !mIsConnected after all output chunks produced
+ // by main thread events calling
+ // SharedBuffers::FinishProducingOutputBuffer() have been processed.
+ return true;
+ }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override {
+ // Not owned:
+ // - mDestination (probably)
+ size_t amount = AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf);
+ amount += mSharedBuffers->SizeOfIncludingThis(aMallocSizeOf);
+ if (mInputBuffer) {
+ amount += mInputBuffer->SizeOfIncludingThis(aMallocSizeOf);
+ }
+
+ return amount;
+ }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ private:
+ void SendBuffersToMainThread(AudioNodeTrack* aTrack, GraphTime aFrom) {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ // we now have a full input buffer ready to be sent to the main thread.
+ TrackTime playbackTick = mDestination->GraphTimeToTrackTime(aFrom);
+ // Add the duration of the current sample
+ playbackTick += WEBAUDIO_BLOCK_SIZE;
+ // Add the delay caused by the main thread
+ playbackTick += mSharedBuffers->DelaySoFar();
+ // Compute the playback time in the coordinate system of the destination
+ double playbackTime = mDestination->TrackTimeToSeconds(playbackTick);
+
+ class Command final : public Runnable {
+ public:
+ Command(AudioNodeTrack* aTrack,
+ already_AddRefed<ThreadSharedFloatArrayBufferList> aInputBuffer,
+ double aPlaybackTime)
+ : mozilla::Runnable("Command"),
+ mTrack(aTrack),
+ mInputBuffer(aInputBuffer),
+ mPlaybackTime(aPlaybackTime) {}
+
+ NS_IMETHOD Run() override {
+ auto engine = static_cast<ScriptProcessorNodeEngine*>(mTrack->Engine());
+ AudioChunk output;
+ output.SetNull(engine->mBufferSize);
+ {
+ auto node =
+ static_cast<ScriptProcessorNode*>(engine->NodeMainThread());
+ if (!node) {
+ return NS_OK;
+ }
+
+ if (node->HasListenersFor(nsGkAtoms::onaudioprocess)) {
+ DispatchAudioProcessEvent(node, &output);
+ }
+ // The node may have been destroyed during event dispatch.
+ }
+
+ // Append it to our output buffer queue
+ engine->GetSharedBuffers()->FinishProducingOutputBuffer(output);
+
+ return NS_OK;
+ }
+
+ // Sets up |output| iff buffers are set in event handlers.
+ void DispatchAudioProcessEvent(ScriptProcessorNode* aNode,
+ AudioChunk* aOutput) {
+ AudioContext* context = aNode->Context();
+ if (!context) {
+ return;
+ }
+
+ AutoJSAPI jsapi;
+ if (NS_WARN_IF(!jsapi.Init(aNode->GetOwner()))) {
+ return;
+ }
+ JSContext* cx = jsapi.cx();
+ uint32_t inputChannelCount = aNode->ChannelCount();
+
+ // Create the input buffer
+ RefPtr<AudioBuffer> inputBuffer;
+ if (mInputBuffer) {
+ ErrorResult rv;
+ inputBuffer = AudioBuffer::Create(
+ context->GetOwner(), inputChannelCount, aNode->BufferSize(),
+ context->SampleRate(), mInputBuffer.forget(), rv);
+ if (rv.Failed()) {
+ rv.SuppressException();
+ return;
+ }
+ }
+
+ // Ask content to produce data in the output buffer
+ // Note that we always avoid creating the output buffer here, and we try
+ // to avoid creating the input buffer as well. The AudioProcessingEvent
+ // class knows how to lazily create them if needed once the script tries
+ // to access them. Otherwise, we may be able to get away without
+ // creating them!
+ RefPtr<AudioProcessingEvent> event =
+ new AudioProcessingEvent(aNode, nullptr, nullptr);
+ event->InitEvent(inputBuffer, inputChannelCount, mPlaybackTime);
+ aNode->DispatchTrustedEvent(event);
+
+ // Steal the output buffers if they have been set.
+ // Don't create a buffer if it hasn't been used to return output;
+ // FinishProducingOutputBuffer() will optimize output = null.
+ // GetThreadSharedChannelsForRate() may also return null after OOM.
+ if (event->HasOutputBuffer()) {
+ ErrorResult rv;
+ AudioBuffer* buffer = event->GetOutputBuffer(rv);
+ // HasOutputBuffer() returning true means that GetOutputBuffer()
+ // will not fail.
+ MOZ_ASSERT(!rv.Failed());
+ *aOutput = buffer->GetThreadSharedChannelsForRate(cx);
+ MOZ_ASSERT(aOutput->IsNull() ||
+ aOutput->mBufferFormat == AUDIO_FORMAT_FLOAT32,
+ "AudioBuffers initialized from JS have float data");
+ }
+ }
+
+ private:
+ RefPtr<AudioNodeTrack> mTrack;
+ RefPtr<ThreadSharedFloatArrayBufferList> mInputBuffer;
+ double mPlaybackTime;
+ };
+
+ RefPtr<Command> command =
+ new Command(aTrack, mInputBuffer.forget(), playbackTime);
+ mAbstractMainThread->Dispatch(command.forget());
+ }
+
+ friend class ScriptProcessorNode;
+
+ RefPtr<AudioNodeTrack> mDestination;
+ UniquePtr<SharedBuffers> mSharedBuffers;
+ RefPtr<ThreadSharedFloatArrayBufferList> mInputBuffer;
+ const uint32_t mBufferSize;
+ const uint32_t mInputChannelCount;
+ // The write index into the current input buffer
+ uint32_t mInputWriteIndex;
+ bool mIsConnected = false;
+};
+
+ScriptProcessorNode::ScriptProcessorNode(AudioContext* aContext,
+ uint32_t aBufferSize,
+ uint32_t aNumberOfInputChannels,
+ uint32_t aNumberOfOutputChannels)
+ : AudioNode(aContext, aNumberOfInputChannels,
+ mozilla::dom::ChannelCountMode::Explicit,
+ mozilla::dom::ChannelInterpretation::Speakers),
+ mBufferSize(aBufferSize ? aBufferSize
+ : // respect what the web developer requested
+ 4096) // choose our own buffer size -- 4KB for now
+ ,
+ mNumberOfOutputChannels(aNumberOfOutputChannels) {
+ MOZ_ASSERT(BufferSize() % WEBAUDIO_BLOCK_SIZE == 0, "Invalid buffer size");
+ ScriptProcessorNodeEngine* engine = new ScriptProcessorNodeEngine(
+ this, aContext->Destination(), BufferSize(), aNumberOfInputChannels);
+ mTrack = AudioNodeTrack::Create(
+ aContext, engine, AudioNodeTrack::NO_TRACK_FLAGS, aContext->Graph());
+}
+
+ScriptProcessorNode::~ScriptProcessorNode() = default;
+
+size_t ScriptProcessorNode::SizeOfExcludingThis(
+ MallocSizeOf aMallocSizeOf) const {
+ size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf);
+ return amount;
+}
+
+size_t ScriptProcessorNode::SizeOfIncludingThis(
+ MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
+
+void ScriptProcessorNode::EventListenerAdded(nsAtom* aType) {
+ AudioNode::EventListenerAdded(aType);
+ if (aType == nsGkAtoms::onaudioprocess) {
+ UpdateConnectedStatus();
+ }
+}
+
+void ScriptProcessorNode::EventListenerRemoved(nsAtom* aType) {
+ AudioNode::EventListenerRemoved(aType);
+ if (aType == nsGkAtoms::onaudioprocess && mTrack) {
+ UpdateConnectedStatus();
+ }
+}
+
+JSObject* ScriptProcessorNode::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return ScriptProcessorNode_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void ScriptProcessorNode::UpdateConnectedStatus() {
+ bool isConnected =
+ mHasPhantomInput || !(OutputNodes().IsEmpty() &&
+ OutputParams().IsEmpty() && InputNodes().IsEmpty());
+
+ // Events are queued even when there is no listener because a listener
+ // may be added while events are in the queue.
+ SendInt32ParameterToTrack(ScriptProcessorNodeEngine::IS_CONNECTED,
+ isConnected);
+
+ if (isConnected && HasListenersFor(nsGkAtoms::onaudioprocess)) {
+ MarkActive();
+ } else {
+ MarkInactive();
+ }
+
+ // MarkInactive above might have released this node, check if it has a track.
+ if (!mTrack) {
+ return;
+ }
+
+ auto engine = static_cast<ScriptProcessorNodeEngine*>(mTrack->Engine());
+ engine->GetSharedBuffers()->NotifyNodeIsConnected(isConnected);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/ScriptProcessorNode.h b/dom/media/webaudio/ScriptProcessorNode.h
new file mode 100644
index 0000000000..c8f023cb17
--- /dev/null
+++ b/dom/media/webaudio/ScriptProcessorNode.h
@@ -0,0 +1,131 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef ScriptProcessorNode_h_
+#define ScriptProcessorNode_h_
+
+#include "AudioNode.h"
+#include "mozilla/ErrorResult.h"
+
+namespace mozilla::dom {
+
+class AudioContext;
+class SharedBuffers;
+
+class ScriptProcessorNode final : public AudioNode {
+ public:
+ ScriptProcessorNode(AudioContext* aContext, uint32_t aBufferSize,
+ uint32_t aNumberOfInputChannels,
+ uint32_t aNumberOfOutputChannels);
+
+ NS_INLINE_DECL_REFCOUNTING_INHERITED(ScriptProcessorNode, AudioNode)
+
+ IMPL_EVENT_HANDLER(audioprocess)
+
+ using EventTarget::EventListenerAdded;
+ void EventListenerAdded(nsAtom* aType) override;
+
+ using EventTarget::EventListenerRemoved;
+ void EventListenerRemoved(nsAtom* aType) override;
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ AudioNode* Connect(AudioNode& aDestination, uint32_t aOutput, uint32_t aInput,
+ ErrorResult& aRv) override {
+ AudioNode* node = AudioNode::Connect(aDestination, aOutput, aInput, aRv);
+ if (!aRv.Failed()) {
+ UpdateConnectedStatus();
+ }
+ return node;
+ }
+
+ void Connect(AudioParam& aDestination, uint32_t aOutput,
+ ErrorResult& aRv) override {
+ AudioNode::Connect(aDestination, aOutput, aRv);
+ if (!aRv.Failed()) {
+ UpdateConnectedStatus();
+ }
+ }
+ void Disconnect(ErrorResult& aRv) override {
+ AudioNode::Disconnect(aRv);
+ UpdateConnectedStatus();
+ }
+ void Disconnect(uint32_t aOutput, ErrorResult& aRv) override {
+ AudioNode::Disconnect(aOutput, aRv);
+ UpdateConnectedStatus();
+ }
+ void NotifyInputsChanged() override { UpdateConnectedStatus(); }
+ void NotifyHasPhantomInput() override {
+ mHasPhantomInput = true;
+ // No need to UpdateConnectedStatus() because there was previously an
+ // input in InputNodes().
+ }
+ void Disconnect(AudioNode& aDestination, ErrorResult& aRv) override {
+ AudioNode::Disconnect(aDestination, aRv);
+ UpdateConnectedStatus();
+ }
+ void Disconnect(AudioNode& aDestination, uint32_t aOutput,
+ ErrorResult& aRv) override {
+ AudioNode::Disconnect(aDestination, aOutput, aRv);
+ UpdateConnectedStatus();
+ }
+ void Disconnect(AudioNode& aDestination, uint32_t aOutput, uint32_t aInput,
+ ErrorResult& aRv) override {
+ AudioNode::Disconnect(aDestination, aOutput, aInput, aRv);
+ UpdateConnectedStatus();
+ }
+ void Disconnect(AudioParam& aDestination, ErrorResult& aRv) override {
+ AudioNode::Disconnect(aDestination, aRv);
+ UpdateConnectedStatus();
+ }
+ void Disconnect(AudioParam& aDestination, uint32_t aOutput,
+ ErrorResult& aRv) override {
+ AudioNode::Disconnect(aDestination, aOutput, aRv);
+ UpdateConnectedStatus();
+ }
+ void SetChannelCount(uint32_t aChannelCount, ErrorResult& aRv) override {
+ if (aChannelCount != ChannelCount()) {
+ // Spec says to throw InvalidStateError, but see
+ // https://github.com/WebAudio/web-audio-api/issues/2153
+ aRv.ThrowNotSupportedError(
+ "Cannot change channel count of ScriptProcessorNode");
+ }
+ }
+ void SetChannelCountModeValue(ChannelCountMode aMode,
+ ErrorResult& aRv) override {
+ if (aMode != ChannelCountMode::Explicit) {
+ // Spec says to throw InvalidStateError, but see
+ // https://github.com/WebAudio/web-audio-api/issues/2154
+ aRv.ThrowNotSupportedError(
+ "Cannot change channel count mode of ScriptProcessorNode");
+ }
+ }
+
+ uint32_t BufferSize() const { return mBufferSize; }
+
+ uint32_t NumberOfOutputChannels() const { return mNumberOfOutputChannels; }
+
+ using DOMEventTargetHelper::DispatchTrustedEvent;
+
+ const char* NodeType() const override { return "ScriptProcessorNode"; }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override;
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override;
+
+ private:
+ virtual ~ScriptProcessorNode();
+
+ void UpdateConnectedStatus();
+
+ const uint32_t mBufferSize;
+ const uint32_t mNumberOfOutputChannels;
+ bool mHasPhantomInput = false;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webaudio/StereoPannerNode.cpp b/dom/media/webaudio/StereoPannerNode.cpp
new file mode 100644
index 0000000000..eefc86017b
--- /dev/null
+++ b/dom/media/webaudio/StereoPannerNode.cpp
@@ -0,0 +1,197 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "StereoPannerNode.h"
+#include "mozilla/dom/StereoPannerNodeBinding.h"
+#include "AudioNodeEngine.h"
+#include "AudioNodeTrack.h"
+#include "AudioDestinationNode.h"
+#include "AlignmentUtils.h"
+#include "WebAudioUtils.h"
+#include "PanningUtils.h"
+#include "AudioParamTimeline.h"
+#include "AudioParam.h"
+#include "Tracing.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(StereoPannerNode, AudioNode, mPan)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(StereoPannerNode)
+NS_INTERFACE_MAP_END_INHERITING(AudioNode)
+
+NS_IMPL_ADDREF_INHERITED(StereoPannerNode, AudioNode)
+NS_IMPL_RELEASE_INHERITED(StereoPannerNode, AudioNode)
+
+class StereoPannerNodeEngine final : public AudioNodeEngine {
+ public:
+ StereoPannerNodeEngine(AudioNode* aNode, AudioDestinationNode* aDestination)
+ : AudioNodeEngine(aNode),
+ mDestination(aDestination->Track())
+ // Keep the default value in sync with the default value in
+ // StereoPannerNode::StereoPannerNode.
+ ,
+ mPan(0.f) {}
+
+ enum Parameters { PAN };
+ void RecvTimelineEvent(uint32_t aIndex, AudioTimelineEvent& aEvent) override {
+ MOZ_ASSERT(mDestination);
+ WebAudioUtils::ConvertAudioTimelineEventToTicks(aEvent, mDestination);
+
+ switch (aIndex) {
+ case PAN:
+ mPan.InsertEvent<int64_t>(aEvent);
+ break;
+ default:
+ NS_ERROR("Bad StereoPannerNode TimelineParameter");
+ }
+ }
+
+ void GetGainValuesForPanning(float aPanning, bool aMonoToStereo,
+ float& aLeftGain, float& aRightGain) {
+ // Clamp and normalize the panning in [0; 1]
+ aPanning = std::min(std::max(aPanning, -1.f), 1.f);
+
+ if (aMonoToStereo) {
+ aPanning += 1;
+ aPanning /= 2;
+ } else if (aPanning <= 0) {
+ aPanning += 1;
+ }
+
+ aLeftGain = cos(0.5 * M_PI * aPanning);
+ aRightGain = sin(0.5 * M_PI * aPanning);
+ }
+
+ void SetToSilentStereoBlock(AudioBlock* aChunk) {
+ for (uint32_t channel = 0; channel < 2; channel++) {
+ float* samples = aChunk->ChannelFloatsForWrite(channel);
+ for (uint32_t i = 0; i < WEBAUDIO_BLOCK_SIZE; i++) {
+ samples[i] = 0.f;
+ }
+ }
+ }
+
+ void UpmixToStereoIfNeeded(const AudioBlock& aInput, AudioBlock* aOutput) {
+ if (aInput.ChannelCount() == 2) {
+ *aOutput = aInput;
+ } else {
+ MOZ_ASSERT(aInput.ChannelCount() == 1);
+ aOutput->SetBuffer(aInput.GetBuffer());
+ aOutput->mChannelData.SetLength(2);
+ for (uint32_t i = 0; i < 2; ++i) {
+ aOutput->mChannelData[i] = aInput.ChannelData<float>()[0];
+ }
+ // 1/sqrt(2) multiplier is because StereoPanner up-mixing differs from
+ // input up-mixing.
+ aOutput->mVolume = M_SQRT1_2 * aInput.mVolume;
+ aOutput->mBufferFormat = AUDIO_FORMAT_FLOAT32;
+ }
+ }
+
+ virtual void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom,
+ const AudioBlock& aInput, AudioBlock* aOutput,
+ bool* aFinished) override {
+ // The output of this node is always stereo, no matter what the inputs are.
+ MOZ_ASSERT(aInput.ChannelCount() <= 2);
+ TRACE("StereoPannerNodeEngine::ProcessBlock");
+
+ bool monoToStereo = aInput.ChannelCount() == 1;
+
+ if (aInput.IsNull()) {
+ // If input is silent, so is the output
+ aOutput->SetNull(WEBAUDIO_BLOCK_SIZE);
+ } else if (mPan.HasSimpleValue()) {
+ float panning = mPan.GetValue();
+ // If the panning is 0.0, we can simply copy the input to the
+ // output with gain applied, up-mixing to stereo if needed.
+ if (panning == 0.0f) {
+ UpmixToStereoIfNeeded(aInput, aOutput);
+ } else {
+ // Optimize the case where the panning is constant for this processing
+ // block: we can just apply a constant gain on the left and right
+ // channel
+ float gainL, gainR;
+
+ GetGainValuesForPanning(panning, monoToStereo, gainL, gainR);
+ ApplyStereoPanning(aInput, aOutput, gainL, gainR, panning <= 0);
+ }
+ } else {
+ float computedGain[2 * WEBAUDIO_BLOCK_SIZE + 4];
+ bool onLeft[WEBAUDIO_BLOCK_SIZE];
+
+ float values[WEBAUDIO_BLOCK_SIZE];
+ TrackTime tick = mDestination->GraphTimeToTrackTime(aFrom);
+ mPan.GetValuesAtTime(tick, values, WEBAUDIO_BLOCK_SIZE);
+
+ float* alignedComputedGain = ALIGNED16(computedGain);
+ ASSERT_ALIGNED16(alignedComputedGain);
+ for (size_t counter = 0; counter < WEBAUDIO_BLOCK_SIZE; ++counter) {
+ float left, right;
+ GetGainValuesForPanning(values[counter], monoToStereo, left, right);
+
+ alignedComputedGain[counter] = left;
+ alignedComputedGain[WEBAUDIO_BLOCK_SIZE + counter] = right;
+ onLeft[counter] = values[counter] <= 0;
+ }
+
+ // Apply the gain to the output buffer
+ ApplyStereoPanning(aInput, aOutput, alignedComputedGain,
+ &alignedComputedGain[WEBAUDIO_BLOCK_SIZE], onLeft);
+ }
+ }
+
+ virtual size_t SizeOfIncludingThis(
+ MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ RefPtr<AudioNodeTrack> mDestination;
+ AudioParamTimeline mPan;
+};
+
+StereoPannerNode::StereoPannerNode(AudioContext* aContext)
+ : AudioNode(aContext, 2, ChannelCountMode::Clamped_max,
+ ChannelInterpretation::Speakers) {
+ mPan =
+ CreateAudioParam(StereoPannerNodeEngine::PAN, u"pan"_ns, 0.f, -1.f, 1.f);
+ StereoPannerNodeEngine* engine =
+ new StereoPannerNodeEngine(this, aContext->Destination());
+ mTrack = AudioNodeTrack::Create(
+ aContext, engine, AudioNodeTrack::NO_TRACK_FLAGS, aContext->Graph());
+}
+
+/* static */
+already_AddRefed<StereoPannerNode> StereoPannerNode::Create(
+ AudioContext& aAudioContext, const StereoPannerOptions& aOptions,
+ ErrorResult& aRv) {
+ RefPtr<StereoPannerNode> audioNode = new StereoPannerNode(&aAudioContext);
+
+ audioNode->Initialize(aOptions, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ audioNode->Pan()->SetInitialValue(aOptions.mPan);
+ return audioNode.forget();
+}
+
+size_t StereoPannerNode::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t amount = AudioNode::SizeOfExcludingThis(aMallocSizeOf);
+ amount += mPan->SizeOfIncludingThis(aMallocSizeOf);
+ return amount;
+}
+
+size_t StereoPannerNode::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
+
+JSObject* StereoPannerNode::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return StereoPannerNode_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/StereoPannerNode.h b/dom/media/webaudio/StereoPannerNode.h
new file mode 100644
index 0000000000..9dc7370fd1
--- /dev/null
+++ b/dom/media/webaudio/StereoPannerNode.h
@@ -0,0 +1,74 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef StereoPannerNode_h_
+#define StereoPannerNode_h_
+
+#include "AudioNode.h"
+#include "nsPrintfCString.h"
+#include "mozilla/RefCounted.h"
+#include "mozilla/dom/StereoPannerNodeBinding.h"
+
+namespace mozilla::dom {
+
+class AudioContext;
+struct StereoPannerOptions;
+
+class StereoPannerNode final : public AudioNode {
+ public:
+ static already_AddRefed<StereoPannerNode> Create(
+ AudioContext& aAudioContext, const StereoPannerOptions& aOptions,
+ ErrorResult& aRv);
+
+ MOZ_DECLARE_REFCOUNTED_TYPENAME(StereoPannerNode)
+
+ static already_AddRefed<StereoPannerNode> Constructor(
+ const GlobalObject& aGlobal, AudioContext& aAudioContext,
+ const StereoPannerOptions& aOptions, ErrorResult& aRv) {
+ return Create(aAudioContext, aOptions, aRv);
+ }
+
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ virtual void SetChannelCount(uint32_t aChannelCount,
+ ErrorResult& aRv) override {
+ if (aChannelCount > 2) {
+ aRv.ThrowNotSupportedError(
+ nsPrintfCString("%u is greater than 2", aChannelCount));
+ return;
+ }
+ AudioNode::SetChannelCount(aChannelCount, aRv);
+ }
+ virtual void SetChannelCountModeValue(ChannelCountMode aMode,
+ ErrorResult& aRv) override {
+ if (aMode == ChannelCountMode::Max) {
+ aRv.ThrowNotSupportedError("Cannot set channel count mode to \"max\"");
+ return;
+ }
+ AudioNode::SetChannelCountModeValue(aMode, aRv);
+ }
+
+ AudioParam* Pan() const { return mPan; }
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(StereoPannerNode, AudioNode)
+
+ virtual const char* NodeType() const override { return "StereoPannerNode"; }
+
+ virtual size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override;
+ virtual size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override;
+
+ private:
+ explicit StereoPannerNode(AudioContext* aContext);
+ ~StereoPannerNode() = default;
+
+ RefPtr<AudioParam> mPan;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webaudio/ThreeDPoint.cpp b/dom/media/webaudio/ThreeDPoint.cpp
new file mode 100644
index 0000000000..d54d03e24c
--- /dev/null
+++ b/dom/media/webaudio/ThreeDPoint.cpp
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Other similar methods can be added if needed.
+ */
+
+#include "ThreeDPoint.h"
+#include "WebAudioUtils.h"
+
+namespace mozilla::dom {
+
+bool ThreeDPoint::FuzzyEqual(const ThreeDPoint& other) {
+ return WebAudioUtils::FuzzyEqual(x, other.x) &&
+ WebAudioUtils::FuzzyEqual(y, other.y) &&
+ WebAudioUtils::FuzzyEqual(z, other.z);
+}
+
+ThreeDPoint operator-(const ThreeDPoint& lhs, const ThreeDPoint& rhs) {
+ return ThreeDPoint(lhs.x - rhs.x, lhs.y - rhs.y, lhs.z - rhs.z);
+}
+
+ThreeDPoint operator*(const ThreeDPoint& lhs, const ThreeDPoint& rhs) {
+ return ThreeDPoint(lhs.x * rhs.x, lhs.y * rhs.y, lhs.z * rhs.z);
+}
+
+ThreeDPoint operator*(const ThreeDPoint& lhs, const double rhs) {
+ return ThreeDPoint(lhs.x * rhs, lhs.y * rhs, lhs.z * rhs);
+}
+
+bool operator==(const ThreeDPoint& lhs, const ThreeDPoint& rhs) {
+ return lhs.x == rhs.x && lhs.y == rhs.y && lhs.z == rhs.z;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/ThreeDPoint.h b/dom/media/webaudio/ThreeDPoint.h
new file mode 100644
index 0000000000..4e2c7358f8
--- /dev/null
+++ b/dom/media/webaudio/ThreeDPoint.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef ThreeDPoint_h_
+#define ThreeDPoint_h_
+
+#include <cmath>
+#include <algorithm>
+
+namespace mozilla::dom {
+
+struct ThreeDPoint final {
+ ThreeDPoint() : x(0.), y(0.), z(0.) {}
+ ThreeDPoint(double aX, double aY, double aZ) : x(aX), y(aY), z(aZ) {}
+
+ double Magnitude() const { return sqrt(x * x + y * y + z * z); }
+
+ void Normalize() {
+ // Zero vectors cannot be normalized. For our purpose, normalizing a zero
+ // vector results in a zero vector.
+ if (IsZero()) {
+ return;
+ }
+ // Normalize with the maximum norm first to avoid overflow and underflow.
+ double invMax = 1 / MaxNorm();
+ x *= invMax;
+ y *= invMax;
+ z *= invMax;
+
+ double invDistance = 1 / Magnitude();
+ x *= invDistance;
+ y *= invDistance;
+ z *= invDistance;
+ }
+
+ ThreeDPoint CrossProduct(const ThreeDPoint& rhs) const {
+ return ThreeDPoint(y * rhs.z - z * rhs.y, z * rhs.x - x * rhs.z,
+ x * rhs.y - y * rhs.x);
+ }
+
+ double DotProduct(const ThreeDPoint& rhs) {
+ return x * rhs.x + y * rhs.y + z * rhs.z;
+ }
+
+ bool IsZero() const { return x == 0 && y == 0 && z == 0; }
+
+ // For comparing two vectors of close to unit magnitude.
+ bool FuzzyEqual(const ThreeDPoint& other);
+
+ double x, y, z;
+
+ private:
+ double MaxNorm() const {
+ return std::max(fabs(x), std::max(fabs(y), fabs(z)));
+ }
+};
+
+ThreeDPoint operator-(const ThreeDPoint& lhs, const ThreeDPoint& rhs);
+ThreeDPoint operator*(const ThreeDPoint& lhs, const ThreeDPoint& rhs);
+ThreeDPoint operator*(const ThreeDPoint& lhs, const double rhs);
+bool operator==(const ThreeDPoint& lhs, const ThreeDPoint& rhs);
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webaudio/WaveShaperNode.cpp b/dom/media/webaudio/WaveShaperNode.cpp
new file mode 100644
index 0000000000..ced9a81b20
--- /dev/null
+++ b/dom/media/webaudio/WaveShaperNode.cpp
@@ -0,0 +1,390 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WaveShaperNode.h"
+#include "mozilla/dom/WaveShaperNodeBinding.h"
+#include "AlignmentUtils.h"
+#include "AudioNode.h"
+#include "AudioNodeEngine.h"
+#include "AudioNodeTrack.h"
+#include "mozilla/PodOperations.h"
+#include "Tracing.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(WaveShaperNode)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(WaveShaperNode, AudioNode)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(WaveShaperNode, AudioNode)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(WaveShaperNode)
+ NS_IMPL_CYCLE_COLLECTION_TRACE_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_TRACE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WaveShaperNode)
+NS_INTERFACE_MAP_END_INHERITING(AudioNode)
+
+NS_IMPL_ADDREF_INHERITED(WaveShaperNode, AudioNode)
+NS_IMPL_RELEASE_INHERITED(WaveShaperNode, AudioNode)
+
+static uint32_t ValueOf(OverSampleType aType) {
+ switch (aType) {
+ case OverSampleType::None:
+ return 1;
+ case OverSampleType::_2x:
+ return 2;
+ case OverSampleType::_4x:
+ return 4;
+ default:
+ MOZ_ASSERT_UNREACHABLE("We should never reach here");
+ return 1;
+ }
+}
+
+class Resampler final {
+ public:
+ Resampler()
+ : mType(OverSampleType::None),
+ mUpSampler(nullptr),
+ mDownSampler(nullptr),
+ mChannels(0),
+ mSampleRate(0) {}
+
+ ~Resampler() { Destroy(); }
+
+ void Reset(uint32_t aChannels, TrackRate aSampleRate, OverSampleType aType) {
+ if (aChannels == mChannels && aSampleRate == mSampleRate &&
+ aType == mType) {
+ return;
+ }
+
+ mChannels = aChannels;
+ mSampleRate = aSampleRate;
+ mType = aType;
+
+ Destroy();
+
+ if (aType == OverSampleType::None) {
+ mBuffer.Clear();
+ return;
+ }
+
+ mUpSampler = speex_resampler_init(aChannels, aSampleRate,
+ aSampleRate * ValueOf(aType),
+ SPEEX_RESAMPLER_QUALITY_MIN, nullptr);
+ mDownSampler =
+ speex_resampler_init(aChannels, aSampleRate * ValueOf(aType),
+ aSampleRate, SPEEX_RESAMPLER_QUALITY_MIN, nullptr);
+ mBuffer.SetLength(WEBAUDIO_BLOCK_SIZE * ValueOf(aType));
+ }
+
+ float* UpSample(uint32_t aChannel, const float* aInputData,
+ uint32_t aBlocks) {
+ uint32_t inSamples = WEBAUDIO_BLOCK_SIZE;
+ uint32_t outSamples = WEBAUDIO_BLOCK_SIZE * aBlocks;
+ float* outputData = mBuffer.Elements();
+
+ MOZ_ASSERT(mBuffer.Length() == outSamples);
+
+ WebAudioUtils::SpeexResamplerProcess(mUpSampler, aChannel, aInputData,
+ &inSamples, outputData, &outSamples);
+
+ MOZ_ASSERT(inSamples == WEBAUDIO_BLOCK_SIZE &&
+ outSamples == WEBAUDIO_BLOCK_SIZE * aBlocks);
+
+ return outputData;
+ }
+
+ void DownSample(uint32_t aChannel, float* aOutputData, uint32_t aBlocks) {
+ uint32_t inSamples = WEBAUDIO_BLOCK_SIZE * aBlocks;
+ uint32_t outSamples = WEBAUDIO_BLOCK_SIZE;
+ const float* inputData = mBuffer.Elements();
+
+ MOZ_ASSERT(mBuffer.Length() == inSamples);
+
+ WebAudioUtils::SpeexResamplerProcess(mDownSampler, aChannel, inputData,
+ &inSamples, aOutputData, &outSamples);
+
+ MOZ_ASSERT(inSamples == WEBAUDIO_BLOCK_SIZE * aBlocks &&
+ outSamples == WEBAUDIO_BLOCK_SIZE);
+ }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const {
+ size_t amount = 0;
+ // Future: properly measure speex memory
+ amount += aMallocSizeOf(mUpSampler);
+ amount += aMallocSizeOf(mDownSampler);
+ amount += mBuffer.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ return amount;
+ }
+
+ private:
+ void Destroy() {
+ if (mUpSampler) {
+ speex_resampler_destroy(mUpSampler);
+ mUpSampler = nullptr;
+ }
+ if (mDownSampler) {
+ speex_resampler_destroy(mDownSampler);
+ mDownSampler = nullptr;
+ }
+ }
+
+ private:
+ OverSampleType mType;
+ SpeexResamplerState* mUpSampler;
+ SpeexResamplerState* mDownSampler;
+ uint32_t mChannels;
+ TrackRate mSampleRate;
+ nsTArray<float> mBuffer;
+};
+
+class WaveShaperNodeEngine final : public AudioNodeEngine {
+ public:
+ explicit WaveShaperNodeEngine(AudioNode* aNode)
+ : AudioNodeEngine(aNode), mType(OverSampleType::None) {}
+
+ enum Parameters { TYPE };
+
+ void SetRawArrayData(nsTArray<float>&& aCurve) override {
+ mCurve = std::move(aCurve);
+ }
+
+ void SetInt32Parameter(uint32_t aIndex, int32_t aValue) override {
+ switch (aIndex) {
+ case TYPE:
+ mType = static_cast<OverSampleType>(aValue);
+ break;
+ default:
+ NS_ERROR("Bad WaveShaperNode Int32Parameter");
+ }
+ }
+
+ template <uint32_t blocks>
+ void ProcessCurve(const float* aInputBuffer, float* aOutputBuffer) {
+ for (uint32_t j = 0; j < WEBAUDIO_BLOCK_SIZE * blocks; ++j) {
+ // Index into the curve array based on the amplitude of the
+ // incoming signal by using an amplitude range of [-1, 1] and
+ // performing a linear interpolation of the neighbor values.
+ float index = (mCurve.Length() - 1) * (aInputBuffer[j] + 1.0f) / 2.0f;
+ if (index < 0.0f) {
+ aOutputBuffer[j] = mCurve[0];
+ } else {
+ int32_t indexLower = index;
+ if (static_cast<uint32_t>(indexLower) >= mCurve.Length() - 1) {
+ aOutputBuffer[j] = mCurve[mCurve.Length() - 1];
+ } else {
+ uint32_t indexHigher = indexLower + 1;
+ float interpolationFactor = index - indexLower;
+ aOutputBuffer[j] = (1.0f - interpolationFactor) * mCurve[indexLower] +
+ interpolationFactor * mCurve[indexHigher];
+ }
+ }
+ }
+ }
+
+ void ProcessBlock(AudioNodeTrack* aTrack, GraphTime aFrom,
+ const AudioBlock& aInput, AudioBlock* aOutput,
+ bool* aFinished) override {
+ TRACE("WaveShaperNodeEngine::ProcessBlock");
+
+ uint32_t channelCount = aInput.ChannelCount();
+ if (!mCurve.Length()) {
+ // Optimize the case where we don't have a curve buffer
+ *aOutput = aInput;
+ return;
+ }
+
+ // If the input is null, check to see if non-null output will be produced
+ bool nullInput = false;
+ if (channelCount == 0) {
+ float index = (mCurve.Length() - 1) * 0.5;
+ uint32_t indexLower = index;
+ uint32_t indexHigher = indexLower + 1;
+ float interpolationFactor = index - indexLower;
+ if ((1.0f - interpolationFactor) * mCurve[indexLower] +
+ interpolationFactor * mCurve[indexHigher] ==
+ 0.0) {
+ *aOutput = aInput;
+ return;
+ }
+ nullInput = true;
+ channelCount = 1;
+ }
+
+ aOutput->AllocateChannels(channelCount);
+ for (uint32_t i = 0; i < channelCount; ++i) {
+ const float* inputSamples;
+ float scaledInput[WEBAUDIO_BLOCK_SIZE + 4];
+ float* alignedScaledInput = ALIGNED16(scaledInput);
+ ASSERT_ALIGNED16(alignedScaledInput);
+ if (!nullInput) {
+ if (aInput.mVolume != 1.0f) {
+ AudioBlockCopyChannelWithScale(
+ static_cast<const float*>(aInput.mChannelData[i]), aInput.mVolume,
+ alignedScaledInput);
+ inputSamples = alignedScaledInput;
+ } else {
+ inputSamples = static_cast<const float*>(aInput.mChannelData[i]);
+ }
+ } else {
+ PodZero(alignedScaledInput, WEBAUDIO_BLOCK_SIZE);
+ inputSamples = alignedScaledInput;
+ }
+ float* outputBuffer = aOutput->ChannelFloatsForWrite(i);
+ float* sampleBuffer;
+
+ switch (mType) {
+ case OverSampleType::None:
+ mResampler.Reset(channelCount, aTrack->mSampleRate,
+ OverSampleType::None);
+ ProcessCurve<1>(inputSamples, outputBuffer);
+ break;
+ case OverSampleType::_2x:
+ mResampler.Reset(channelCount, aTrack->mSampleRate,
+ OverSampleType::_2x);
+ sampleBuffer = mResampler.UpSample(i, inputSamples, 2);
+ ProcessCurve<2>(sampleBuffer, sampleBuffer);
+ mResampler.DownSample(i, outputBuffer, 2);
+ break;
+ case OverSampleType::_4x:
+ mResampler.Reset(channelCount, aTrack->mSampleRate,
+ OverSampleType::_4x);
+ sampleBuffer = mResampler.UpSample(i, inputSamples, 4);
+ ProcessCurve<4>(sampleBuffer, sampleBuffer);
+ mResampler.DownSample(i, outputBuffer, 4);
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("We should never reach here");
+ }
+ }
+ }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override {
+ size_t amount = AudioNodeEngine::SizeOfExcludingThis(aMallocSizeOf);
+ amount += mCurve.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ amount += mResampler.SizeOfExcludingThis(aMallocSizeOf);
+ return amount;
+ }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ private:
+ nsTArray<float> mCurve;
+ OverSampleType mType;
+ Resampler mResampler;
+};
+
+WaveShaperNode::WaveShaperNode(AudioContext* aContext)
+ : AudioNode(aContext, 2, ChannelCountMode::Max,
+ ChannelInterpretation::Speakers),
+ mType(OverSampleType::None) {
+ WaveShaperNodeEngine* engine = new WaveShaperNodeEngine(this);
+ mTrack = AudioNodeTrack::Create(
+ aContext, engine, AudioNodeTrack::NO_TRACK_FLAGS, aContext->Graph());
+}
+
+/* static */
+already_AddRefed<WaveShaperNode> WaveShaperNode::Create(
+ AudioContext& aAudioContext, const WaveShaperOptions& aOptions,
+ ErrorResult& aRv) {
+ RefPtr<WaveShaperNode> audioNode = new WaveShaperNode(&aAudioContext);
+
+ audioNode->Initialize(aOptions, aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+
+ if (aOptions.mCurve.WasPassed()) {
+ audioNode->SetCurveInternal(aOptions.mCurve.Value(), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return nullptr;
+ }
+ }
+
+ audioNode->SetOversample(aOptions.mOversample);
+ return audioNode.forget();
+}
+
+JSObject* WaveShaperNode::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return WaveShaperNode_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void WaveShaperNode::SetCurve(const Nullable<Float32Array>& aCurve,
+ ErrorResult& aRv) {
+ // Let's purge the cached value for the curve attribute.
+ WaveShaperNode_Binding::ClearCachedCurveValue(this);
+
+ if (aCurve.IsNull()) {
+ CleanCurveInternal();
+ return;
+ }
+
+ const Float32Array& floats = aCurve.Value();
+ floats.ComputeState();
+
+ nsTArray<float> curve;
+ uint32_t argLength = floats.Length();
+ if (!curve.SetLength(argLength, fallible)) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return;
+ }
+
+ PodCopy(curve.Elements(), floats.Data(), argLength);
+ SetCurveInternal(curve, aRv);
+}
+
+void WaveShaperNode::SetCurveInternal(const nsTArray<float>& aCurve,
+ ErrorResult& aRv) {
+ if (aCurve.Length() < 2) {
+ aRv.ThrowInvalidStateError("Must have at least two entries");
+ return;
+ }
+
+ mCurve = aCurve.Clone();
+ SendCurveToTrack();
+}
+
+void WaveShaperNode::CleanCurveInternal() {
+ mCurve.Clear();
+ SendCurveToTrack();
+}
+
+void WaveShaperNode::SendCurveToTrack() {
+ AudioNodeTrack* ns = mTrack;
+ MOZ_ASSERT(ns, "Why don't we have a track here?");
+
+ nsTArray<float> copyCurve(mCurve.Clone());
+ ns->SetRawArrayData(std::move(copyCurve));
+}
+
+void WaveShaperNode::GetCurve(JSContext* aCx,
+ JS::MutableHandle<JSObject*> aRetval) {
+ // Let's return a null value if the list is empty.
+ if (mCurve.IsEmpty()) {
+ aRetval.set(nullptr);
+ return;
+ }
+
+ MOZ_ASSERT(mCurve.Length() >= 2);
+ aRetval.set(
+ Float32Array::Create(aCx, this, mCurve.Length(), mCurve.Elements()));
+}
+
+void WaveShaperNode::SetOversample(OverSampleType aType) {
+ mType = aType;
+ SendInt32ParameterToTrack(WaveShaperNodeEngine::TYPE,
+ static_cast<int32_t>(aType));
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webaudio/WaveShaperNode.h b/dom/media/webaudio/WaveShaperNode.h
new file mode 100644
index 0000000000..3c5d8c8ef4
--- /dev/null
+++ b/dom/media/webaudio/WaveShaperNode.h
@@ -0,0 +1,71 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef WaveShaperNode_h_
+#define WaveShaperNode_h_
+
+#include "AudioNode.h"
+#include "mozilla/dom/WaveShaperNodeBinding.h"
+#include "mozilla/dom/TypedArray.h"
+
+namespace mozilla::dom {
+
+class AudioContext;
+struct WaveShaperOptions;
+
+class WaveShaperNode final : public AudioNode {
+ public:
+ static already_AddRefed<WaveShaperNode> Create(
+ AudioContext& aAudioContext, const WaveShaperOptions& aOptions,
+ ErrorResult& aRv);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_SCRIPT_HOLDER_CLASS_INHERITED(WaveShaperNode,
+ AudioNode)
+
+ static already_AddRefed<WaveShaperNode> Constructor(
+ const GlobalObject& aGlobal, AudioContext& aAudioContext,
+ const WaveShaperOptions& aOptions, ErrorResult& aRv) {
+ return Create(aAudioContext, aOptions, aRv);
+ }
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ void GetCurve(JSContext* aCx, JS::MutableHandle<JSObject*> aRetval);
+ void SetCurve(const Nullable<Float32Array>& aData, ErrorResult& aRv);
+
+ OverSampleType Oversample() const { return mType; }
+ void SetOversample(OverSampleType aType);
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override {
+ // Possibly track in the future:
+ // - mCurve
+ return AudioNode::SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ const char* NodeType() const override { return "WaveShaperNode"; }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ private:
+ explicit WaveShaperNode(AudioContext* aContext);
+ ~WaveShaperNode() = default;
+
+ void SetCurveInternal(const nsTArray<float>& aCurve, ErrorResult& aRv);
+ void CleanCurveInternal();
+
+ void SendCurveToTrack();
+
+ nsTArray<float> mCurve;
+ OverSampleType mType;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webaudio/WebAudioUtils.cpp b/dom/media/webaudio/WebAudioUtils.cpp
new file mode 100644
index 0000000000..699f397532
--- /dev/null
+++ b/dom/media/webaudio/WebAudioUtils.cpp
@@ -0,0 +1,149 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WebAudioUtils.h"
+#include "AudioNodeTrack.h"
+#include "blink/HRTFDatabaseLoader.h"
+
+#include "nsComponentManagerUtils.h"
+#include "nsContentUtils.h"
+#include "nsIConsoleService.h"
+#include "nsIScriptError.h"
+#include "nsJSUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "AudioEventTimeline.h"
+
+#include "mozilla/SchedulerGroup.h"
+
+namespace mozilla {
+
+LazyLogModule gWebAudioAPILog("WebAudioAPI");
+
+namespace dom {
+
+void WebAudioUtils::ConvertAudioTimelineEventToTicks(AudioTimelineEvent& aEvent,
+ AudioNodeTrack* aDest) {
+ aEvent.SetTimeInTicks(
+ aDest->SecondsToNearestTrackTime(aEvent.Time<double>()));
+ aEvent.mTimeConstant *= aDest->mSampleRate;
+ aEvent.mDuration *= aDest->mSampleRate;
+}
+
+void WebAudioUtils::Shutdown() { WebCore::HRTFDatabaseLoader::shutdown(); }
+
+int WebAudioUtils::SpeexResamplerProcess(SpeexResamplerState* aResampler,
+ uint32_t aChannel, const float* aIn,
+ uint32_t* aInLen, float* aOut,
+ uint32_t* aOutLen) {
+#ifdef MOZ_SAMPLE_TYPE_S16
+ AutoTArray<AudioDataValue, WEBAUDIO_BLOCK_SIZE * 4> tmp1;
+ AutoTArray<AudioDataValue, WEBAUDIO_BLOCK_SIZE * 4> tmp2;
+ tmp1.SetLength(*aInLen);
+ tmp2.SetLength(*aOutLen);
+ ConvertAudioSamples(aIn, tmp1.Elements(), *aInLen);
+ int result = speex_resampler_process_int(
+ aResampler, aChannel, tmp1.Elements(), aInLen, tmp2.Elements(), aOutLen);
+ ConvertAudioSamples(tmp2.Elements(), aOut, *aOutLen);
+ return result;
+#else
+ return speex_resampler_process_float(aResampler, aChannel, aIn, aInLen, aOut,
+ aOutLen);
+#endif
+}
+
+int WebAudioUtils::SpeexResamplerProcess(SpeexResamplerState* aResampler,
+ uint32_t aChannel, const int16_t* aIn,
+ uint32_t* aInLen, float* aOut,
+ uint32_t* aOutLen) {
+ AutoTArray<AudioDataValue, WEBAUDIO_BLOCK_SIZE * 4> tmp;
+#ifdef MOZ_SAMPLE_TYPE_S16
+ tmp.SetLength(*aOutLen);
+ int result = speex_resampler_process_int(aResampler, aChannel, aIn, aInLen,
+ tmp.Elements(), aOutLen);
+ ConvertAudioSamples(tmp.Elements(), aOut, *aOutLen);
+ return result;
+#else
+ tmp.SetLength(*aInLen);
+ ConvertAudioSamples(aIn, tmp.Elements(), *aInLen);
+ int result = speex_resampler_process_float(
+ aResampler, aChannel, tmp.Elements(), aInLen, aOut, aOutLen);
+ return result;
+#endif
+}
+
+int WebAudioUtils::SpeexResamplerProcess(SpeexResamplerState* aResampler,
+ uint32_t aChannel, const int16_t* aIn,
+ uint32_t* aInLen, int16_t* aOut,
+ uint32_t* aOutLen) {
+#ifdef MOZ_SAMPLE_TYPE_S16
+ return speex_resampler_process_int(aResampler, aChannel, aIn, aInLen, aOut,
+ aOutLen);
+#else
+ AutoTArray<AudioDataValue, WEBAUDIO_BLOCK_SIZE * 4> tmp1;
+ AutoTArray<AudioDataValue, WEBAUDIO_BLOCK_SIZE * 4> tmp2;
+ tmp1.SetLength(*aInLen);
+ tmp2.SetLength(*aOutLen);
+ ConvertAudioSamples(aIn, tmp1.Elements(), *aInLen);
+ int result = speex_resampler_process_float(
+ aResampler, aChannel, tmp1.Elements(), aInLen, tmp2.Elements(), aOutLen);
+ ConvertAudioSamples(tmp2.Elements(), aOut, *aOutLen);
+ return result;
+#endif
+}
+
+void WebAudioUtils::LogToDeveloperConsole(uint64_t aWindowID,
+ const char* aKey) {
+ // This implementation is derived from dom/media/VideoUtils.cpp, but we
+ // use a windowID so that the message is delivered to the developer console.
+ // It is similar to ContentUtils::ReportToConsole, but also works off main
+ // thread.
+ if (!NS_IsMainThread()) {
+ nsCOMPtr<nsIRunnable> task = NS_NewRunnableFunction(
+ "dom::WebAudioUtils::LogToDeveloperConsole",
+ [aWindowID, aKey] { LogToDeveloperConsole(aWindowID, aKey); });
+ SchedulerGroup::Dispatch(TaskCategory::Other, task.forget());
+ return;
+ }
+
+ nsCOMPtr<nsIConsoleService> console(
+ do_GetService("@mozilla.org/consoleservice;1"));
+ if (!console) {
+ NS_WARNING("Failed to log message to console.");
+ return;
+ }
+
+ nsAutoString spec;
+ uint32_t aLineNumber = 0, aColumnNumber = 0;
+ JSContext* cx = nsContentUtils::GetCurrentJSContext();
+ if (cx) {
+ nsJSUtils::GetCallingLocation(cx, spec, &aLineNumber, &aColumnNumber);
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIScriptError> errorObject =
+ do_CreateInstance(NS_SCRIPTERROR_CONTRACTID, &rv);
+ if (!errorObject) {
+ NS_WARNING("Failed to log message to console.");
+ return;
+ }
+
+ nsAutoString result;
+ rv = nsContentUtils::GetLocalizedString(nsContentUtils::eDOM_PROPERTIES, aKey,
+ result);
+
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to log message to console.");
+ return;
+ }
+
+ errorObject->InitWithWindowID(result, spec, u""_ns, aLineNumber,
+ aColumnNumber, nsIScriptError::warningFlag,
+ "Web Audio", aWindowID);
+ console->LogMessage(errorObject);
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/media/webaudio/WebAudioUtils.h b/dom/media/webaudio/WebAudioUtils.h
new file mode 100644
index 0000000000..0b01efde69
--- /dev/null
+++ b/dom/media/webaudio/WebAudioUtils.h
@@ -0,0 +1,209 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef WebAudioUtils_h_
+#define WebAudioUtils_h_
+
+#include <cmath>
+#include <limits>
+#include <type_traits>
+#include "mozilla/FloatingPoint.h"
+#include "mozilla/Logging.h"
+#include "MediaSegment.h"
+
+// Forward declaration
+typedef struct SpeexResamplerState_ SpeexResamplerState;
+
+namespace mozilla {
+
+class AudioNodeTrack;
+
+extern LazyLogModule gWebAudioAPILog;
+#define WEB_AUDIO_API_LOG(...) \
+ MOZ_LOG(gWebAudioAPILog, LogLevel::Debug, (__VA_ARGS__))
+
+namespace dom {
+
+struct AudioTimelineEvent;
+
+namespace WebAudioUtils {
+// 32 is the minimum required by the spec for createBuffer() and
+// createScriptProcessor() and matches what is used by Blink. The limit
+// protects against large memory allocations.
+const size_t MaxChannelCount = 32;
+// AudioContext::CreateBuffer() "must support sample-rates in at least the
+// range 22050 to 96000."
+const uint32_t MinSampleRate = 8000;
+const uint32_t MaxSampleRate = 192000;
+
+inline bool FuzzyEqual(float v1, float v2) { return fabsf(v1 - v2) < 1e-7f; }
+inline bool FuzzyEqual(double v1, double v2) { return fabs(v1 - v2) < 1e-7; }
+
+/**
+ * Converts an AudioTimelineEvent's floating point time values to tick values
+ * with respect to a destination AudioNodeTrack.
+ *
+ * This needs to be called for each AudioTimelineEvent that gets sent to an
+ * AudioNodeEngine, on the engine side where the AudioTimlineEvent is
+ * received. This means that such engines need to be aware of their
+ * destination tracks as well.
+ */
+void ConvertAudioTimelineEventToTicks(AudioTimelineEvent& aEvent,
+ AudioNodeTrack* aDest);
+
+/**
+ * Converts a linear value to decibels. Returns aMinDecibels if the linear
+ * value is 0.
+ */
+inline float ConvertLinearToDecibels(float aLinearValue, float aMinDecibels) {
+ return aLinearValue ? 20.0f * std::log10(aLinearValue) : aMinDecibels;
+}
+
+/**
+ * Converts a decibel value to a linear value.
+ */
+inline float ConvertDecibelsToLinear(float aDecibels) {
+ return std::pow(10.0f, 0.05f * aDecibels);
+}
+
+/**
+ * Converts a decibel to a linear value.
+ */
+inline float ConvertDecibelToLinear(float aDecibel) {
+ return std::pow(10.0f, 0.05f * aDecibel);
+}
+
+inline void FixNaN(double& aDouble) {
+ if (std::isnan(aDouble) || std::isinf(aDouble)) {
+ aDouble = 0.0;
+ }
+}
+
+inline double DiscreteTimeConstantForSampleRate(double timeConstant,
+ double sampleRate) {
+ return 1.0 - std::exp(-1.0 / (sampleRate * timeConstant));
+}
+
+inline bool IsTimeValid(double aTime) {
+ return aTime >= 0 && aTime <= (MEDIA_TIME_MAX >> TRACK_RATE_MAX_BITS);
+}
+
+/**
+ * Converts a floating point value to an integral type in a safe and
+ * platform agnostic way. The following program demonstrates the kinds
+ * of ways things can go wrong depending on the CPU architecture you're
+ * compiling for:
+ *
+ * #include <stdio.h>
+ * volatile float r;
+ * int main()
+ * {
+ * unsigned int q;
+ * r = 1e100;
+ * q = r;
+ * printf("%f %d\n", r, q);
+ * r = -1e100;
+ * q = r;
+ * printf("%f %d\n", r, q);
+ * r = 1e15;
+ * q = r;
+ * printf("%f %x\n", r, q);
+ * r = 0/0.;
+ * q = r;
+ * printf("%f %d\n", r, q);
+ * }
+ *
+ * This program, when compiled for unsigned int, generates the following
+ * results depending on the architecture:
+ *
+ * x86 and x86-64
+ * ---
+ * inf 0
+ * -inf 0
+ * 999999995904.000000 -727384064 d4a50000
+ * nan 0
+ *
+ * ARM
+ * ---
+ * inf -1
+ * -inf 0
+ * 999999995904.000000 -1
+ * nan 0
+ *
+ * When compiled for int, this program generates the following results:
+ *
+ * x86 and x86-64
+ * ---
+ * inf -2147483648
+ * -inf -2147483648
+ * 999999995904.000000 -2147483648
+ * nan -2147483648
+ *
+ * ARM
+ * ---
+ * inf 2147483647
+ * -inf -2147483648
+ * 999999995904.000000 2147483647
+ * nan 0
+ *
+ * Note that the caller is responsible to make sure that the value
+ * passed to this function is not a NaN. This function will abort if
+ * it sees a NaN.
+ */
+template <typename IntType, typename FloatType>
+IntType TruncateFloatToInt(FloatType f) {
+ using std::numeric_limits;
+ static_assert(std::is_integral_v<IntType> == true,
+ "IntType must be an integral type");
+ static_assert(std::is_floating_point_v<FloatType> == true,
+ "FloatType must be a floating point type");
+
+ if (std::isnan(f)) {
+ // It is the responsibility of the caller to deal with NaN values.
+ // If we ever get to this point, we have a serious bug to fix.
+ MOZ_CRASH("We should never see a NaN here");
+ }
+
+ // If the floating point value is outside of the range of maximum
+ // integral value for this type, just clamp to the maximum value.
+ // The equality case must also return max() due to loss of precision when
+ // converting max() to float.
+ if (f >= FloatType(numeric_limits<IntType>::max())) {
+ return numeric_limits<IntType>::max();
+ }
+
+ if (f <= FloatType(numeric_limits<IntType>::min())) {
+ // If the floating point value is outside of the range of minimum
+ // integral value for this type, just clamp to the minimum value.
+ return numeric_limits<IntType>::min();
+ }
+
+ // Otherwise, this conversion must be well defined.
+ return IntType(f);
+}
+
+void Shutdown();
+
+int SpeexResamplerProcess(SpeexResamplerState* aResampler, uint32_t aChannel,
+ const float* aIn, uint32_t* aInLen, float* aOut,
+ uint32_t* aOutLen);
+
+int SpeexResamplerProcess(SpeexResamplerState* aResampler, uint32_t aChannel,
+ const int16_t* aIn, uint32_t* aInLen, float* aOut,
+ uint32_t* aOutLen);
+
+int SpeexResamplerProcess(SpeexResamplerState* aResampler, uint32_t aChannel,
+ const int16_t* aIn, uint32_t* aInLen, int16_t* aOut,
+ uint32_t* aOutLen);
+
+void LogToDeveloperConsole(uint64_t aWindowID, const char* aKey);
+
+} // namespace WebAudioUtils
+
+} // namespace dom
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webaudio/blink/Biquad.cpp b/dom/media/webaudio/blink/Biquad.cpp
new file mode 100644
index 0000000000..51c3ec4487
--- /dev/null
+++ b/dom/media/webaudio/blink/Biquad.cpp
@@ -0,0 +1,439 @@
+/*
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Biquad.h"
+
+#include "DenormalDisabler.h"
+
+#include <float.h>
+#include <algorithm>
+#include <math.h>
+
+namespace WebCore {
+
+Biquad::Biquad() {
+ // Initialize as pass-thru (straight-wire, no filter effect)
+ setNormalizedCoefficients(1, 0, 0, 1, 0, 0);
+
+ reset(); // clear filter memory
+}
+
+Biquad::~Biquad() = default;
+
+void Biquad::process(const float* sourceP, float* destP,
+ size_t framesToProcess) {
+ // Create local copies of member variables
+ double x1 = m_x1;
+ double x2 = m_x2;
+ double y1 = m_y1;
+ double y2 = m_y2;
+
+ double b0 = m_b0;
+ double b1 = m_b1;
+ double b2 = m_b2;
+ double a1 = m_a1;
+ double a2 = m_a2;
+
+ for (size_t i = 0; i < framesToProcess; ++i) {
+ // FIXME: this can be optimized by pipelining the multiply adds...
+ double x = sourceP[i];
+ double y = b0 * x + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2;
+
+ destP[i] = y;
+
+ // Update state variables
+ x2 = x1;
+ x1 = x;
+ y2 = y1;
+ y1 = y;
+ }
+
+ // Avoid introducing a stream of subnormals when input is silent and the
+ // tail approaches zero.
+ if (x1 == 0.0 && x2 == 0.0 && (y1 != 0.0 || y2 != 0.0) &&
+ fabs(y1) < FLT_MIN && fabs(y2) < FLT_MIN) {
+ // Flush future values to zero (until there is new input).
+ y1 = y2 = 0.0;
+// Flush calculated values.
+#ifndef HAVE_DENORMAL
+ for (int i = framesToProcess; i-- && fabsf(destP[i]) < FLT_MIN;) {
+ destP[i] = 0.0f;
+ }
+#endif
+ }
+ // Local variables back to member.
+ m_x1 = x1;
+ m_x2 = x2;
+ m_y1 = y1;
+ m_y2 = y2;
+}
+
+void Biquad::reset() { m_x1 = m_x2 = m_y1 = m_y2 = 0; }
+
+void Biquad::setLowpassParams(double cutoff, double resonance) {
+ // Limit cutoff to 0 to 1.
+ cutoff = std::max(0.0, std::min(cutoff, 1.0));
+
+ if (cutoff == 1) {
+ // When cutoff is 1, the z-transform is 1.
+ setNormalizedCoefficients(1, 0, 0, 1, 0, 0);
+ } else if (cutoff > 0) {
+ // Compute biquad coefficients for lowpass filter
+ double g = pow(10.0, -0.05 * resonance);
+ double w0 = M_PI * cutoff;
+ double cos_w0 = cos(w0);
+ double alpha = 0.5 * sin(w0) * g;
+
+ double b1 = 1.0 - cos_w0;
+ double b0 = 0.5 * b1;
+ double b2 = b0;
+ double a0 = 1.0 + alpha;
+ double a1 = -2.0 * cos_w0;
+ double a2 = 1.0 - alpha;
+
+ setNormalizedCoefficients(b0, b1, b2, a0, a1, a2);
+ } else {
+ // When cutoff is zero, nothing gets through the filter, so set
+ // coefficients up correctly.
+ setNormalizedCoefficients(0, 0, 0, 1, 0, 0);
+ }
+}
+
+void Biquad::setHighpassParams(double cutoff, double resonance) {
+ // Limit cutoff to 0 to 1.
+ cutoff = std::max(0.0, std::min(cutoff, 1.0));
+
+ if (cutoff == 1) {
+ // The z-transform is 0.
+ setNormalizedCoefficients(0, 0, 0, 1, 0, 0);
+ } else if (cutoff > 0) {
+ // Compute biquad coefficients for highpass filter
+ double g = pow(10.0, -0.05 * resonance);
+ double w0 = M_PI * cutoff;
+ double cos_w0 = cos(w0);
+ double alpha = 0.5 * sin(w0) * g;
+
+ double b1 = -1.0 - cos_w0;
+ double b0 = -0.5 * b1;
+ double b2 = b0;
+ double a0 = 1.0 + alpha;
+ double a1 = -2.0 * cos_w0;
+ double a2 = 1.0 - alpha;
+
+ setNormalizedCoefficients(b0, b1, b2, a0, a1, a2);
+ } else {
+ // When cutoff is zero, we need to be careful because the above
+ // gives a quadratic divided by the same quadratic, with poles
+ // and zeros on the unit circle in the same place. When cutoff
+ // is zero, the z-transform is 1.
+ setNormalizedCoefficients(1, 0, 0, 1, 0, 0);
+ }
+}
+
+void Biquad::setNormalizedCoefficients(double b0, double b1, double b2,
+ double a0, double a1, double a2) {
+ double a0Inverse = 1 / a0;
+
+ m_b0 = b0 * a0Inverse;
+ m_b1 = b1 * a0Inverse;
+ m_b2 = b2 * a0Inverse;
+ m_a1 = a1 * a0Inverse;
+ m_a2 = a2 * a0Inverse;
+}
+
+void Biquad::setLowShelfParams(double frequency, double dbGain) {
+ // Clip frequencies to between 0 and 1, inclusive.
+ frequency = std::max(0.0, std::min(frequency, 1.0));
+
+ double A = pow(10.0, dbGain / 40);
+
+ if (frequency == 1) {
+ // The z-transform is a constant gain.
+ setNormalizedCoefficients(A * A, 0, 0, 1, 0, 0);
+ } else if (frequency > 0) {
+ double w0 = M_PI * frequency;
+ double S = 1; // filter slope (1 is max value)
+ double alpha = 0.5 * sin(w0) * sqrt((A + 1 / A) * (1 / S - 1) + 2);
+ double k = cos(w0);
+ double k2 = 2 * sqrt(A) * alpha;
+ double aPlusOne = A + 1;
+ double aMinusOne = A - 1;
+
+ double b0 = A * (aPlusOne - aMinusOne * k + k2);
+ double b1 = 2 * A * (aMinusOne - aPlusOne * k);
+ double b2 = A * (aPlusOne - aMinusOne * k - k2);
+ double a0 = aPlusOne + aMinusOne * k + k2;
+ double a1 = -2 * (aMinusOne + aPlusOne * k);
+ double a2 = aPlusOne + aMinusOne * k - k2;
+
+ setNormalizedCoefficients(b0, b1, b2, a0, a1, a2);
+ } else {
+ // When frequency is 0, the z-transform is 1.
+ setNormalizedCoefficients(1, 0, 0, 1, 0, 0);
+ }
+}
+
+void Biquad::setHighShelfParams(double frequency, double dbGain) {
+ // Clip frequencies to between 0 and 1, inclusive.
+ frequency = std::max(0.0, std::min(frequency, 1.0));
+
+ double A = pow(10.0, dbGain / 40);
+
+ if (frequency == 1) {
+ // The z-transform is 1.
+ setNormalizedCoefficients(1, 0, 0, 1, 0, 0);
+ } else if (frequency > 0) {
+ double w0 = M_PI * frequency;
+ double S = 1; // filter slope (1 is max value)
+ double alpha = 0.5 * sin(w0) * sqrt((A + 1 / A) * (1 / S - 1) + 2);
+ double k = cos(w0);
+ double k2 = 2 * sqrt(A) * alpha;
+ double aPlusOne = A + 1;
+ double aMinusOne = A - 1;
+
+ double b0 = A * (aPlusOne + aMinusOne * k + k2);
+ double b1 = -2 * A * (aMinusOne + aPlusOne * k);
+ double b2 = A * (aPlusOne + aMinusOne * k - k2);
+ double a0 = aPlusOne - aMinusOne * k + k2;
+ double a1 = 2 * (aMinusOne - aPlusOne * k);
+ double a2 = aPlusOne - aMinusOne * k - k2;
+
+ setNormalizedCoefficients(b0, b1, b2, a0, a1, a2);
+ } else {
+ // When frequency = 0, the filter is just a gain, A^2.
+ setNormalizedCoefficients(A * A, 0, 0, 1, 0, 0);
+ }
+}
+
+void Biquad::setPeakingParams(double frequency, double Q, double dbGain) {
+ // Clip frequencies to between 0 and 1, inclusive.
+ frequency = std::max(0.0, std::min(frequency, 1.0));
+
+ // Don't let Q go negative, which causes an unstable filter.
+ Q = std::max(0.0, Q);
+
+ double A = pow(10.0, dbGain / 40);
+
+ if (frequency > 0 && frequency < 1) {
+ if (Q > 0) {
+ double w0 = M_PI * frequency;
+ double alpha = sin(w0) / (2 * Q);
+ double k = cos(w0);
+
+ double b0 = 1 + alpha * A;
+ double b1 = -2 * k;
+ double b2 = 1 - alpha * A;
+ double a0 = 1 + alpha / A;
+ double a1 = -2 * k;
+ double a2 = 1 - alpha / A;
+
+ setNormalizedCoefficients(b0, b1, b2, a0, a1, a2);
+ } else {
+ // When Q = 0, the above formulas have problems. If we look at
+ // the z-transform, we can see that the limit as Q->0 is A^2, so
+ // set the filter that way.
+ setNormalizedCoefficients(A * A, 0, 0, 1, 0, 0);
+ }
+ } else {
+ // When frequency is 0 or 1, the z-transform is 1.
+ setNormalizedCoefficients(1, 0, 0, 1, 0, 0);
+ }
+}
+
+void Biquad::setAllpassParams(double frequency, double Q) {
+ // Clip frequencies to between 0 and 1, inclusive.
+ frequency = std::max(0.0, std::min(frequency, 1.0));
+
+ // Don't let Q go negative, which causes an unstable filter.
+ Q = std::max(0.0, Q);
+
+ if (frequency > 0 && frequency < 1) {
+ if (Q > 0) {
+ double w0 = M_PI * frequency;
+ double alpha = sin(w0) / (2 * Q);
+ double k = cos(w0);
+
+ double b0 = 1 - alpha;
+ double b1 = -2 * k;
+ double b2 = 1 + alpha;
+ double a0 = 1 + alpha;
+ double a1 = -2 * k;
+ double a2 = 1 - alpha;
+
+ setNormalizedCoefficients(b0, b1, b2, a0, a1, a2);
+ } else {
+ // When Q = 0, the above formulas have problems. If we look at
+ // the z-transform, we can see that the limit as Q->0 is -1, so
+ // set the filter that way.
+ setNormalizedCoefficients(-1, 0, 0, 1, 0, 0);
+ }
+ } else {
+ // When frequency is 0 or 1, the z-transform is 1.
+ setNormalizedCoefficients(1, 0, 0, 1, 0, 0);
+ }
+}
+
+void Biquad::setNotchParams(double frequency, double Q) {
+ // Clip frequencies to between 0 and 1, inclusive.
+ frequency = std::max(0.0, std::min(frequency, 1.0));
+
+ // Don't let Q go negative, which causes an unstable filter.
+ Q = std::max(0.0, Q);
+
+ if (frequency > 0 && frequency < 1) {
+ if (Q > 0) {
+ double w0 = M_PI * frequency;
+ double alpha = sin(w0) / (2 * Q);
+ double k = cos(w0);
+
+ double b0 = 1;
+ double b1 = -2 * k;
+ double b2 = 1;
+ double a0 = 1 + alpha;
+ double a1 = -2 * k;
+ double a2 = 1 - alpha;
+
+ setNormalizedCoefficients(b0, b1, b2, a0, a1, a2);
+ } else {
+ // When Q = 0, the above formulas have problems. If we look at
+ // the z-transform, we can see that the limit as Q->0 is 0, so
+ // set the filter that way.
+ setNormalizedCoefficients(0, 0, 0, 1, 0, 0);
+ }
+ } else {
+ // When frequency is 0 or 1, the z-transform is 1.
+ setNormalizedCoefficients(1, 0, 0, 1, 0, 0);
+ }
+}
+
+void Biquad::setBandpassParams(double frequency, double Q) {
+ // No negative frequencies allowed.
+ frequency = std::max(0.0, frequency);
+
+ // Don't let Q go negative, which causes an unstable filter.
+ Q = std::max(0.0, Q);
+
+ if (frequency > 0 && frequency < 1) {
+ double w0 = M_PI * frequency;
+ if (Q > 0) {
+ double alpha = sin(w0) / (2 * Q);
+ double k = cos(w0);
+
+ double b0 = alpha;
+ double b1 = 0;
+ double b2 = -alpha;
+ double a0 = 1 + alpha;
+ double a1 = -2 * k;
+ double a2 = 1 - alpha;
+
+ setNormalizedCoefficients(b0, b1, b2, a0, a1, a2);
+ } else {
+ // When Q = 0, the above formulas have problems. If we look at
+ // the z-transform, we can see that the limit as Q->0 is 1, so
+ // set the filter that way.
+ setNormalizedCoefficients(1, 0, 0, 1, 0, 0);
+ }
+ } else {
+ // When the cutoff is zero, the z-transform approaches 0, if Q
+ // > 0. When both Q and cutoff are zero, the z-transform is
+ // pretty much undefined. What should we do in this case?
+ // For now, just make the filter 0. When the cutoff is 1, the
+ // z-transform also approaches 0.
+ setNormalizedCoefficients(0, 0, 0, 1, 0, 0);
+ }
+}
+
+void Biquad::setZeroPolePairs(const Complex& zero, const Complex& pole) {
+ double b0 = 1;
+ double b1 = -2 * zero.real();
+
+ double zeroMag = abs(zero);
+ double b2 = zeroMag * zeroMag;
+
+ double a1 = -2 * pole.real();
+
+ double poleMag = abs(pole);
+ double a2 = poleMag * poleMag;
+ setNormalizedCoefficients(b0, b1, b2, 1, a1, a2);
+}
+
+void Biquad::setAllpassPole(const Complex& pole) {
+ Complex zero = Complex(1, 0) / pole;
+ setZeroPolePairs(zero, pole);
+}
+
+void Biquad::getFrequencyResponse(int nFrequencies, const float* frequency,
+ float* magResponse, float* phaseResponse) {
+ // Evaluate the Z-transform of the filter at given normalized
+ // frequency from 0 to 1. (1 corresponds to the Nyquist
+ // frequency.)
+ //
+ // The z-transform of the filter is
+ //
+ // H(z) = (b0 + b1*z^(-1) + b2*z^(-2))/(1 + a1*z^(-1) + a2*z^(-2))
+ //
+ // Evaluate as
+ //
+ // b0 + (b1 + b2*z1)*z1
+ // --------------------
+ // 1 + (a1 + a2*z1)*z1
+ //
+ // with z1 = 1/z and z = exp(j*pi*frequency). Hence z1 = exp(-j*pi*frequency)
+
+ // Make local copies of the coefficients as a micro-optimization.
+ double b0 = m_b0;
+ double b1 = m_b1;
+ double b2 = m_b2;
+ double a1 = m_a1;
+ double a2 = m_a2;
+
+ for (int k = 0; k < nFrequencies; ++k) {
+ double omega = -M_PI * frequency[k];
+ Complex z = Complex(cos(omega), sin(omega));
+ Complex numerator = b0 + (b1 + b2 * z) * z;
+ Complex denominator = Complex(1, 0) + (a1 + a2 * z) * z;
+ // Strangely enough, using complex division:
+ // e.g. Complex response = numerator / denominator;
+ // fails on our test machines, yielding infinities and NaNs, so we do
+ // things the long way here.
+ double n = norm(denominator);
+ double r = (real(numerator) * real(denominator) +
+ imag(numerator) * imag(denominator)) /
+ n;
+ double i = (imag(numerator) * real(denominator) -
+ real(numerator) * imag(denominator)) /
+ n;
+ std::complex<double> response = std::complex<double>(r, i);
+
+ magResponse[k] = static_cast<float>(abs(response));
+ phaseResponse[k] =
+ static_cast<float>(atan2(imag(response), real(response)));
+ }
+}
+
+} // namespace WebCore
diff --git a/dom/media/webaudio/blink/Biquad.h b/dom/media/webaudio/blink/Biquad.h
new file mode 100644
index 0000000000..d64ed72b50
--- /dev/null
+++ b/dom/media/webaudio/blink/Biquad.h
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef Biquad_h
+#define Biquad_h
+
+#include <complex>
+
+namespace WebCore {
+
+typedef std::complex<double> Complex;
+
+// A basic biquad (two-zero / two-pole digital filter)
+//
+// It can be configured to a number of common and very useful filters:
+// lowpass, highpass, shelving, parameteric, notch, allpass, ...
+
+class Biquad {
+ public:
+ Biquad();
+ ~Biquad();
+
+ void process(const float* sourceP, float* destP, size_t framesToProcess);
+
+ // frequency is 0 - 1 normalized, resonance and dbGain are in decibels.
+ // Q is a unitless quality factor.
+ void setLowpassParams(double frequency, double resonance);
+ void setHighpassParams(double frequency, double resonance);
+ void setBandpassParams(double frequency, double Q);
+ void setLowShelfParams(double frequency, double dbGain);
+ void setHighShelfParams(double frequency, double dbGain);
+ void setPeakingParams(double frequency, double Q, double dbGain);
+ void setAllpassParams(double frequency, double Q);
+ void setNotchParams(double frequency, double Q);
+
+ // Set the biquad coefficients given a single zero (other zero will be
+ // conjugate) and a single pole (other pole will be conjugate)
+ void setZeroPolePairs(const Complex& zero, const Complex& pole);
+
+ // Set the biquad coefficients given a single pole (other pole will be
+ // conjugate) (The zeroes will be the inverse of the poles)
+ void setAllpassPole(const Complex& pole);
+
+ // Return true iff the next output block will contain sound even with
+ // silent input.
+ bool hasTail() const { return m_y1 || m_y2 || m_x1 || m_x2; }
+
+ // Resets filter state
+ void reset();
+
+ // Filter response at a set of n frequencies. The magnitude and
+ // phase response are returned in magResponse and phaseResponse.
+ // The phase response is in radians.
+ void getFrequencyResponse(int nFrequencies, const float* frequency,
+ float* magResponse, float* phaseResponse);
+
+ private:
+ void setNormalizedCoefficients(double b0, double b1, double b2, double a0,
+ double a1, double a2);
+
+ // Filter coefficients. The filter is defined as
+ //
+ // y[n] + m_a1*y[n-1] + m_a2*y[n-2] = m_b0*x[n] + m_b1*x[n-1] + m_b2*x[n-2].
+ double m_b0;
+ double m_b1;
+ double m_b2;
+ double m_a1;
+ double m_a2;
+
+ // Filter memory
+ //
+ // Double precision for the output values is valuable because errors can
+ // accumulate. Input values are also stored as double so they need not be
+ // converted again for computation.
+ double m_x1; // input delayed by 1 sample
+ double m_x2; // input delayed by 2 samples
+ double m_y1; // output delayed by 1 sample
+ double m_y2; // output delayed by 2 samples
+};
+
+} // namespace WebCore
+
+#endif // Biquad_h
diff --git a/dom/media/webaudio/blink/DenormalDisabler.h b/dom/media/webaudio/blink/DenormalDisabler.h
new file mode 100644
index 0000000000..646482b74f
--- /dev/null
+++ b/dom/media/webaudio/blink/DenormalDisabler.h
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2011, Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef DenormalDisabler_h
+#define DenormalDisabler_h
+
+#include <cmath>
+#include <cinttypes>
+#include <cstring>
+#include <float.h>
+
+namespace WebCore {
+
+// Deal with denormals. They can very seriously impact performance on x86.
+
+// Define HAVE_DENORMAL if we support flushing denormals to zero.
+
+#if defined(XP_WIN) && defined(_MSC_VER)
+// Windows compiled using MSVC with SSE2
+# define HAVE_DENORMAL 1
+#endif
+
+#if defined(__GNUC__) && defined(__SSE__)
+# define HAVE_DENORMAL 1
+#endif
+
+#if defined(__arm__) || defined(__aarch64__)
+# define HAVE_DENORMAL 1
+#endif
+
+#ifdef HAVE_DENORMAL
+class DenormalDisabler {
+ public:
+ DenormalDisabler() : m_savedCSR(0) { disableDenormals(); }
+
+ ~DenormalDisabler() { restoreState(); }
+
+ // This is a nop if we can flush denormals to zero in hardware.
+ static inline float flushDenormalFloatToZero(float f) { return f; }
+
+ private:
+ unsigned m_savedCSR;
+
+# if defined(__GNUC__) && defined(__SSE__)
+ static inline bool isDAZSupported() {
+# if defined(__x86_64__)
+ return true;
+# else
+ static bool s_isInited = false;
+ static bool s_isSupported = false;
+ if (s_isInited) {
+ return s_isSupported;
+ }
+
+ struct fxsaveResult {
+ uint8_t before[28];
+ uint32_t CSRMask;
+ uint8_t after[480];
+ } __attribute__((aligned(16)));
+
+ fxsaveResult registerData;
+ memset(&registerData, 0, sizeof(fxsaveResult));
+ asm volatile("fxsave %0" : "=m"(registerData));
+ s_isSupported = registerData.CSRMask & 0x0040;
+ s_isInited = true;
+ return s_isSupported;
+# endif
+ }
+
+ inline void disableDenormals() {
+ m_savedCSR = getCSR();
+ setCSR(m_savedCSR | (isDAZSupported() ? 0x8040 : 0x8000));
+ }
+
+ inline void restoreState() { setCSR(m_savedCSR); }
+
+ inline int getCSR() {
+ int result;
+ asm volatile("stmxcsr %0" : "=m"(result));
+ return result;
+ }
+
+ inline void setCSR(int a) {
+ int temp = a;
+ asm volatile("ldmxcsr %0" : : "m"(temp));
+ }
+
+# elif defined(XP_WIN) && defined(_MSC_VER)
+ inline void disableDenormals() {
+ // Save the current state, and set mode to flush denormals.
+ //
+ // http://stackoverflow.com/questions/637175/possible-bug-in-controlfp-s-may-not-restore-control-word-correctly
+ _controlfp_s(&m_savedCSR, 0, 0);
+ unsigned unused;
+ _controlfp_s(&unused, _DN_FLUSH, _MCW_DN);
+ }
+
+ inline void restoreState() {
+ unsigned unused;
+ _controlfp_s(&unused, m_savedCSR, _MCW_DN);
+ }
+# elif defined(__arm__) || defined(__aarch64__)
+ inline void disableDenormals() {
+ m_savedCSR = getStatusWord();
+ // Bit 24 is the flush-to-zero mode control bit. Setting it to 1 flushes
+ // denormals to 0.
+ setStatusWord(m_savedCSR | (1 << 24));
+ }
+
+ inline void restoreState() { setStatusWord(m_savedCSR); }
+
+ inline int getStatusWord() {
+ int result;
+# if defined(__aarch64__)
+ asm volatile("mrs %x[result], FPCR" : [result] "=r"(result));
+# else
+ asm volatile("vmrs %[result], FPSCR" : [result] "=r"(result));
+# endif
+ return result;
+ }
+
+ inline void setStatusWord(int a) {
+# if defined(__aarch64__)
+ asm volatile("msr FPCR, %x[src]" : : [src] "r"(a));
+# else
+ asm volatile("vmsr FPSCR, %[src]" : : [src] "r"(a));
+# endif
+ }
+
+# endif
+};
+
+#else
+// FIXME: add implementations for other architectures and compilers
+class DenormalDisabler {
+ public:
+ DenormalDisabler() {}
+
+ // Assume the worst case that other architectures and compilers
+ // need to flush denormals to zero manually.
+ static inline float flushDenormalFloatToZero(float f) {
+ return (fabs(f) < FLT_MIN) ? 0.0f : f;
+ }
+};
+
+#endif
+
+} // namespace WebCore
+#endif // DenormalDisabler_h
diff --git a/dom/media/webaudio/blink/DynamicsCompressor.cpp b/dom/media/webaudio/blink/DynamicsCompressor.cpp
new file mode 100644
index 0000000000..d31dfb3c17
--- /dev/null
+++ b/dom/media/webaudio/blink/DynamicsCompressor.cpp
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2011 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "DynamicsCompressor.h"
+#include "AlignmentUtils.h"
+#include "AudioBlock.h"
+
+#include <cmath>
+#include "AudioNodeEngine.h"
+#include "nsDebug.h"
+
+using mozilla::AudioBlockCopyChannelWithScale;
+using mozilla::WEBAUDIO_BLOCK_SIZE;
+
+namespace WebCore {
+
+DynamicsCompressor::DynamicsCompressor(float sampleRate,
+ unsigned numberOfChannels)
+ : m_numberOfChannels(numberOfChannels),
+ m_sampleRate(sampleRate),
+ m_compressor(sampleRate, numberOfChannels) {
+ // Uninitialized state - for parameter recalculation.
+ m_lastFilterStageRatio = -1;
+ m_lastAnchor = -1;
+ m_lastFilterStageGain = -1;
+
+ setNumberOfChannels(numberOfChannels);
+ initializeParameters();
+}
+
+size_t DynamicsCompressor::sizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t amount = aMallocSizeOf(this);
+ amount += m_preFilterPacks.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (size_t i = 0; i < m_preFilterPacks.Length(); i++) {
+ if (m_preFilterPacks[i]) {
+ amount += m_preFilterPacks[i]->sizeOfIncludingThis(aMallocSizeOf);
+ }
+ }
+
+ amount += m_postFilterPacks.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (size_t i = 0; i < m_postFilterPacks.Length(); i++) {
+ if (m_postFilterPacks[i]) {
+ amount += m_postFilterPacks[i]->sizeOfIncludingThis(aMallocSizeOf);
+ }
+ }
+
+ amount += aMallocSizeOf(m_sourceChannels.get());
+ amount += aMallocSizeOf(m_destinationChannels.get());
+ amount += m_compressor.sizeOfExcludingThis(aMallocSizeOf);
+ return amount;
+}
+
+void DynamicsCompressor::setParameterValue(unsigned parameterID, float value) {
+ MOZ_ASSERT(parameterID < ParamLast);
+ if (parameterID < ParamLast) m_parameters[parameterID] = value;
+}
+
+void DynamicsCompressor::initializeParameters() {
+ // Initializes compressor to default values.
+
+ m_parameters[ParamThreshold] = -24; // dB
+ m_parameters[ParamKnee] = 30; // dB
+ m_parameters[ParamRatio] = 12; // unit-less
+ m_parameters[ParamAttack] = 0.003f; // seconds
+ m_parameters[ParamRelease] = 0.250f; // seconds
+ m_parameters[ParamPreDelay] = 0.006f; // seconds
+
+ // Release zone values 0 -> 1.
+ m_parameters[ParamReleaseZone1] = 0.09f;
+ m_parameters[ParamReleaseZone2] = 0.16f;
+ m_parameters[ParamReleaseZone3] = 0.42f;
+ m_parameters[ParamReleaseZone4] = 0.98f;
+
+ m_parameters[ParamFilterStageGain] = 4.4f; // dB
+ m_parameters[ParamFilterStageRatio] = 2;
+ m_parameters[ParamFilterAnchor] = 15000 / nyquist();
+
+ m_parameters[ParamPostGain] = 0; // dB
+ m_parameters[ParamReduction] = 0; // dB
+
+ // Linear crossfade (0 -> 1).
+ m_parameters[ParamEffectBlend] = 1;
+}
+
+float DynamicsCompressor::parameterValue(unsigned parameterID) {
+ MOZ_ASSERT(parameterID < ParamLast);
+ return m_parameters[parameterID];
+}
+
+void DynamicsCompressor::setEmphasisStageParameters(
+ unsigned stageIndex, float gain, float normalizedFrequency /* 0 -> 1 */) {
+ float gk = 1 - gain / 20;
+ float f1 = normalizedFrequency * gk;
+ float f2 = normalizedFrequency / gk;
+ float r1 = expf(-f1 * M_PI);
+ float r2 = expf(-f2 * M_PI);
+
+ MOZ_ASSERT(m_numberOfChannels == m_preFilterPacks.Length());
+
+ for (unsigned i = 0; i < m_numberOfChannels; ++i) {
+ // Set pre-filter zero and pole to create an emphasis filter.
+ ZeroPole& preFilter = m_preFilterPacks[i]->filters[stageIndex];
+ preFilter.setZero(r1);
+ preFilter.setPole(r2);
+
+ // Set post-filter with zero and pole reversed to create the de-emphasis
+ // filter. If there were no compressor kernel in between, they would cancel
+ // each other out (allpass filter).
+ ZeroPole& postFilter = m_postFilterPacks[i]->filters[stageIndex];
+ postFilter.setZero(r2);
+ postFilter.setPole(r1);
+ }
+}
+
+void DynamicsCompressor::setEmphasisParameters(float gain, float anchorFreq,
+ float filterStageRatio) {
+ setEmphasisStageParameters(0, gain, anchorFreq);
+ setEmphasisStageParameters(1, gain, anchorFreq / filterStageRatio);
+ setEmphasisStageParameters(
+ 2, gain, anchorFreq / (filterStageRatio * filterStageRatio));
+ setEmphasisStageParameters(
+ 3, gain,
+ anchorFreq / (filterStageRatio * filterStageRatio * filterStageRatio));
+}
+
+void DynamicsCompressor::process(const AudioBlock* sourceChunk,
+ AudioBlock* destinationChunk,
+ unsigned framesToProcess) {
+ // Though numberOfChannels is retrived from destinationBus, we still name it
+ // numberOfChannels instead of numberOfDestinationChannels. It's because we
+ // internally match sourceChannels's size to destinationBus by channel up/down
+ // mix. Thus we need numberOfChannels to do the loop work for both
+ // m_sourceChannels and m_destinationChannels.
+
+ unsigned numberOfChannels = destinationChunk->ChannelCount();
+ unsigned numberOfSourceChannels = sourceChunk->ChannelCount();
+
+ MOZ_ASSERT(numberOfChannels == m_numberOfChannels && numberOfSourceChannels);
+
+ if (numberOfChannels != m_numberOfChannels || !numberOfSourceChannels) {
+ destinationChunk->SetNull(WEBAUDIO_BLOCK_SIZE);
+ return;
+ }
+
+ switch (numberOfChannels) {
+ case 2: // stereo
+ m_sourceChannels[0] =
+ static_cast<const float*>(sourceChunk->mChannelData[0]);
+
+ if (numberOfSourceChannels > 1)
+ m_sourceChannels[1] =
+ static_cast<const float*>(sourceChunk->mChannelData[1]);
+ else
+ // Simply duplicate mono channel input data to right channel for stereo
+ // processing.
+ m_sourceChannels[1] = m_sourceChannels[0];
+
+ break;
+ case 1:
+ m_sourceChannels[0] =
+ static_cast<const float*>(sourceChunk->mChannelData[0]);
+ break;
+ default:
+ MOZ_CRASH("not supported.");
+ }
+
+ for (unsigned i = 0; i < numberOfChannels; ++i)
+ m_destinationChannels[i] = const_cast<float*>(
+ static_cast<const float*>(destinationChunk->mChannelData[i]));
+
+ float filterStageGain = parameterValue(ParamFilterStageGain);
+ float filterStageRatio = parameterValue(ParamFilterStageRatio);
+ float anchor = parameterValue(ParamFilterAnchor);
+
+ if (filterStageGain != m_lastFilterStageGain ||
+ filterStageRatio != m_lastFilterStageRatio || anchor != m_lastAnchor) {
+ m_lastFilterStageGain = filterStageGain;
+ m_lastFilterStageRatio = filterStageRatio;
+ m_lastAnchor = anchor;
+
+ setEmphasisParameters(filterStageGain, anchor, filterStageRatio);
+ }
+
+ float sourceWithVolume[WEBAUDIO_BLOCK_SIZE + 4];
+ float* alignedSourceWithVolume = ALIGNED16(sourceWithVolume);
+ ASSERT_ALIGNED16(alignedSourceWithVolume);
+
+ // Apply pre-emphasis filter.
+ // Note that the final three stages are computed in-place in the destination
+ // buffer.
+ for (unsigned i = 0; i < numberOfChannels; ++i) {
+ const float* sourceData;
+ if (sourceChunk->mVolume == 1.0f) {
+ // Fast path, the volume scale doesn't need to get taken into account
+ sourceData = m_sourceChannels[i];
+ } else {
+ AudioBlockCopyChannelWithScale(m_sourceChannels[i], sourceChunk->mVolume,
+ alignedSourceWithVolume);
+ sourceData = alignedSourceWithVolume;
+ }
+
+ float* destinationData = m_destinationChannels[i];
+ ZeroPole* preFilters = m_preFilterPacks[i]->filters;
+
+ preFilters[0].process(sourceData, destinationData, framesToProcess);
+ preFilters[1].process(destinationData, destinationData, framesToProcess);
+ preFilters[2].process(destinationData, destinationData, framesToProcess);
+ preFilters[3].process(destinationData, destinationData, framesToProcess);
+ }
+
+ float dbThreshold = parameterValue(ParamThreshold);
+ float dbKnee = parameterValue(ParamKnee);
+ float ratio = parameterValue(ParamRatio);
+ float attackTime = parameterValue(ParamAttack);
+ float releaseTime = parameterValue(ParamRelease);
+ float preDelayTime = parameterValue(ParamPreDelay);
+
+ // This is effectively a master volume on the compressed signal
+ // (pre-blending).
+ float dbPostGain = parameterValue(ParamPostGain);
+
+ // Linear blending value from dry to completely processed (0 -> 1)
+ // 0 means the signal is completely unprocessed.
+ // 1 mixes in only the compressed signal.
+ float effectBlend = parameterValue(ParamEffectBlend);
+
+ float releaseZone1 = parameterValue(ParamReleaseZone1);
+ float releaseZone2 = parameterValue(ParamReleaseZone2);
+ float releaseZone3 = parameterValue(ParamReleaseZone3);
+ float releaseZone4 = parameterValue(ParamReleaseZone4);
+
+ // Apply compression to the pre-filtered signal.
+ // The processing is performed in place.
+ m_compressor.process(m_destinationChannels.get(), m_destinationChannels.get(),
+ numberOfChannels, framesToProcess,
+
+ dbThreshold, dbKnee, ratio, attackTime, releaseTime,
+ preDelayTime, dbPostGain, effectBlend,
+
+ releaseZone1, releaseZone2, releaseZone3, releaseZone4);
+
+ // Update the compression amount.
+ setParameterValue(ParamReduction, m_compressor.meteringGain());
+
+ // Apply de-emphasis filter.
+ for (unsigned i = 0; i < numberOfChannels; ++i) {
+ float* destinationData = m_destinationChannels[i];
+ ZeroPole* postFilters = m_postFilterPacks[i]->filters;
+
+ postFilters[0].process(destinationData, destinationData, framesToProcess);
+ postFilters[1].process(destinationData, destinationData, framesToProcess);
+ postFilters[2].process(destinationData, destinationData, framesToProcess);
+ postFilters[3].process(destinationData, destinationData, framesToProcess);
+ }
+}
+
+void DynamicsCompressor::reset() {
+ m_lastFilterStageRatio = -1; // for recalc
+ m_lastAnchor = -1;
+ m_lastFilterStageGain = -1;
+
+ for (unsigned channel = 0; channel < m_numberOfChannels; ++channel) {
+ for (unsigned stageIndex = 0; stageIndex < 4; ++stageIndex) {
+ m_preFilterPacks[channel]->filters[stageIndex].reset();
+ m_postFilterPacks[channel]->filters[stageIndex].reset();
+ }
+ }
+
+ m_compressor.reset();
+}
+
+void DynamicsCompressor::setNumberOfChannels(unsigned numberOfChannels) {
+ if (m_preFilterPacks.Length() == numberOfChannels) return;
+
+ m_preFilterPacks.Clear();
+ m_postFilterPacks.Clear();
+ for (unsigned i = 0; i < numberOfChannels; ++i) {
+ m_preFilterPacks.AppendElement(new ZeroPoleFilterPack4());
+ m_postFilterPacks.AppendElement(new ZeroPoleFilterPack4());
+ }
+
+ m_sourceChannels = mozilla::MakeUnique<const float*[]>(numberOfChannels);
+ m_destinationChannels = mozilla::MakeUnique<float*[]>(numberOfChannels);
+
+ m_compressor.setNumberOfChannels(numberOfChannels);
+ m_numberOfChannels = numberOfChannels;
+}
+
+} // namespace WebCore
diff --git a/dom/media/webaudio/blink/DynamicsCompressor.h b/dom/media/webaudio/blink/DynamicsCompressor.h
new file mode 100644
index 0000000000..1838e268fe
--- /dev/null
+++ b/dom/media/webaudio/blink/DynamicsCompressor.h
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2011 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef DynamicsCompressor_h
+#define DynamicsCompressor_h
+
+#include "DynamicsCompressorKernel.h"
+#include "ZeroPole.h"
+
+#include "nsTArray.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/UniquePtr.h"
+
+namespace mozilla {
+class AudioBlock;
+} // namespace mozilla
+
+namespace WebCore {
+
+using mozilla::AudioBlock;
+using mozilla::UniquePtr;
+
+// DynamicsCompressor implements a flexible audio dynamics compression effect
+// such as is commonly used in musical production and game audio. It lowers the
+// volume of the loudest parts of the signal and raises the volume of the
+// softest parts, making the sound richer, fuller, and more controlled.
+
+class DynamicsCompressor {
+ public:
+ enum {
+ ParamThreshold,
+ ParamKnee,
+ ParamRatio,
+ ParamAttack,
+ ParamRelease,
+ ParamPreDelay,
+ ParamReleaseZone1,
+ ParamReleaseZone2,
+ ParamReleaseZone3,
+ ParamReleaseZone4,
+ ParamPostGain,
+ ParamFilterStageGain,
+ ParamFilterStageRatio,
+ ParamFilterAnchor,
+ ParamEffectBlend,
+ ParamReduction,
+ ParamLast
+ };
+
+ DynamicsCompressor(float sampleRate, unsigned numberOfChannels);
+
+ void process(const AudioBlock* sourceChunk, AudioBlock* destinationChunk,
+ unsigned framesToProcess);
+ void reset();
+ void setNumberOfChannels(unsigned);
+ unsigned numberOfChannels() const { return m_numberOfChannels; }
+
+ void setParameterValue(unsigned parameterID, float value);
+ float parameterValue(unsigned parameterID);
+
+ float sampleRate() const { return m_sampleRate; }
+ float nyquist() const { return m_sampleRate / 2; }
+
+ double tailTime() const { return 0; }
+ double latencyTime() const {
+ return m_compressor.latencyFrames() / static_cast<double>(sampleRate());
+ }
+
+ size_t sizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ protected:
+ unsigned m_numberOfChannels;
+
+ // m_parameters holds the tweakable compressor parameters.
+ float m_parameters[ParamLast];
+ void initializeParameters();
+
+ float m_sampleRate;
+
+ // Emphasis filter controls.
+ float m_lastFilterStageRatio;
+ float m_lastAnchor;
+ float m_lastFilterStageGain;
+
+ struct ZeroPoleFilterPack4 {
+ ZeroPole filters[4];
+ size_t sizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this);
+ }
+ };
+
+ // Per-channel emphasis filters.
+ nsTArray<UniquePtr<ZeroPoleFilterPack4> > m_preFilterPacks;
+ nsTArray<UniquePtr<ZeroPoleFilterPack4> > m_postFilterPacks;
+
+ mozilla::UniquePtr<const float*[]> m_sourceChannels;
+ mozilla::UniquePtr<float*[]> m_destinationChannels;
+
+ void setEmphasisStageParameters(unsigned stageIndex, float gain,
+ float normalizedFrequency /* 0 -> 1 */);
+ void setEmphasisParameters(float gain, float anchorFreq,
+ float filterStageRatio);
+
+ // The core compressor.
+ DynamicsCompressorKernel m_compressor;
+};
+
+} // namespace WebCore
+
+#endif // DynamicsCompressor_h
diff --git a/dom/media/webaudio/blink/DynamicsCompressorKernel.cpp b/dom/media/webaudio/blink/DynamicsCompressorKernel.cpp
new file mode 100644
index 0000000000..cffbe49967
--- /dev/null
+++ b/dom/media/webaudio/blink/DynamicsCompressorKernel.cpp
@@ -0,0 +1,494 @@
+/*
+ * Copyright (C) 2011 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "DynamicsCompressorKernel.h"
+
+#include "DenormalDisabler.h"
+#include <algorithm>
+#include <cmath>
+
+#include "mozilla/FloatingPoint.h"
+#include "WebAudioUtils.h"
+
+using namespace mozilla::dom; // for WebAudioUtils
+using mozilla::MakeUnique;
+using mozilla::PositiveInfinity;
+
+namespace WebCore {
+
+// Metering hits peaks instantly, but releases this fast (in seconds).
+const float meteringReleaseTimeConstant = 0.325f;
+
+const float uninitializedValue = -1;
+
+DynamicsCompressorKernel::DynamicsCompressorKernel(float sampleRate,
+ unsigned numberOfChannels)
+ : m_sampleRate(sampleRate),
+ m_lastPreDelayFrames(DefaultPreDelayFrames),
+ m_preDelayReadIndex(0),
+ m_preDelayWriteIndex(DefaultPreDelayFrames),
+ m_ratio(uninitializedValue),
+ m_slope(uninitializedValue),
+ m_linearThreshold(uninitializedValue),
+ m_dbThreshold(uninitializedValue),
+ m_dbKnee(uninitializedValue),
+ m_kneeThreshold(uninitializedValue),
+ m_kneeThresholdDb(uninitializedValue),
+ m_ykneeThresholdDb(uninitializedValue),
+ m_K(uninitializedValue) {
+ setNumberOfChannels(numberOfChannels);
+
+ // Initializes most member variables
+ reset();
+
+ m_meteringReleaseK =
+ static_cast<float>(WebAudioUtils::DiscreteTimeConstantForSampleRate(
+ meteringReleaseTimeConstant, sampleRate));
+}
+
+size_t DynamicsCompressorKernel::sizeOfExcludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t amount = 0;
+ amount += m_preDelayBuffers.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (size_t i = 0; i < m_preDelayBuffers.Length(); i++) {
+ amount += aMallocSizeOf(m_preDelayBuffers[i].get());
+ }
+
+ return amount;
+}
+
+void DynamicsCompressorKernel::setNumberOfChannels(unsigned numberOfChannels) {
+ if (m_preDelayBuffers.Length() == numberOfChannels) return;
+
+ m_preDelayBuffers.Clear();
+ for (unsigned i = 0; i < numberOfChannels; ++i)
+ m_preDelayBuffers.AppendElement(MakeUnique<float[]>(MaxPreDelayFrames));
+}
+
+void DynamicsCompressorKernel::setPreDelayTime(float preDelayTime) {
+ // Re-configure look-ahead section pre-delay if delay time has changed.
+ unsigned preDelayFrames = preDelayTime * sampleRate();
+ if (preDelayFrames > MaxPreDelayFrames - 1)
+ preDelayFrames = MaxPreDelayFrames - 1;
+
+ if (m_lastPreDelayFrames != preDelayFrames) {
+ m_lastPreDelayFrames = preDelayFrames;
+ for (unsigned i = 0; i < m_preDelayBuffers.Length(); ++i)
+ memset(m_preDelayBuffers[i].get(), 0, sizeof(float) * MaxPreDelayFrames);
+
+ m_preDelayReadIndex = 0;
+ m_preDelayWriteIndex = preDelayFrames;
+ }
+}
+
+// Exponential curve for the knee.
+// It is 1st derivative matched at m_linearThreshold and asymptotically
+// approaches the value m_linearThreshold + 1 / k.
+float DynamicsCompressorKernel::kneeCurve(float x, float k) {
+ // Linear up to threshold.
+ if (x < m_linearThreshold) return x;
+
+ return m_linearThreshold + (1 - expf(-k * (x - m_linearThreshold))) / k;
+}
+
+// Full compression curve with constant ratio after knee.
+float DynamicsCompressorKernel::saturate(float x, float k) {
+ float y;
+
+ if (x < m_kneeThreshold)
+ y = kneeCurve(x, k);
+ else {
+ // Constant ratio after knee.
+ float xDb = WebAudioUtils::ConvertLinearToDecibels(x, -1000.0f);
+ float yDb = m_ykneeThresholdDb + m_slope * (xDb - m_kneeThresholdDb);
+
+ y = WebAudioUtils::ConvertDecibelsToLinear(yDb);
+ }
+
+ return y;
+}
+
+// Approximate 1st derivative with input and output expressed in dB.
+// This slope is equal to the inverse of the compression "ratio".
+// In other words, a compression ratio of 20 would be a slope of 1/20.
+float DynamicsCompressorKernel::slopeAt(float x, float k) {
+ if (x < m_linearThreshold) return 1;
+
+ float x2 = x * 1.001;
+
+ float xDb = WebAudioUtils::ConvertLinearToDecibels(x, -1000.0f);
+ float x2Db = WebAudioUtils::ConvertLinearToDecibels(x2, -1000.0f);
+
+ float yDb = WebAudioUtils::ConvertLinearToDecibels(kneeCurve(x, k), -1000.0f);
+ float y2Db =
+ WebAudioUtils::ConvertLinearToDecibels(kneeCurve(x2, k), -1000.0f);
+
+ float m = (y2Db - yDb) / (x2Db - xDb);
+
+ return m;
+}
+
+float DynamicsCompressorKernel::kAtSlope(float desiredSlope) {
+ float xDb = m_dbThreshold + m_dbKnee;
+ float x = WebAudioUtils::ConvertDecibelsToLinear(xDb);
+
+ // Approximate k given initial values.
+ float minK = 0.1f;
+ float maxK = 10000;
+ float k = 5;
+
+ for (int i = 0; i < 15; ++i) {
+ // A high value for k will more quickly asymptotically approach a slope of
+ // 0.
+ float slope = slopeAt(x, k);
+
+ if (slope < desiredSlope) {
+ // k is too high.
+ maxK = k;
+ } else {
+ // k is too low.
+ minK = k;
+ }
+
+ // Re-calculate based on geometric mean.
+ k = sqrtf(minK * maxK);
+ }
+
+ return k;
+}
+
+float DynamicsCompressorKernel::updateStaticCurveParameters(float dbThreshold,
+ float dbKnee,
+ float ratio) {
+ if (dbThreshold != m_dbThreshold || dbKnee != m_dbKnee || ratio != m_ratio) {
+ // Threshold and knee.
+ m_dbThreshold = dbThreshold;
+ m_linearThreshold = WebAudioUtils::ConvertDecibelsToLinear(dbThreshold);
+ m_dbKnee = dbKnee;
+
+ // Compute knee parameters.
+ m_ratio = ratio;
+ m_slope = 1 / m_ratio;
+
+ float k = kAtSlope(1 / m_ratio);
+
+ m_kneeThresholdDb = dbThreshold + dbKnee;
+ m_kneeThreshold = WebAudioUtils::ConvertDecibelsToLinear(m_kneeThresholdDb);
+
+ m_ykneeThresholdDb = WebAudioUtils::ConvertLinearToDecibels(
+ kneeCurve(m_kneeThreshold, k), -1000.0f);
+
+ m_K = k;
+ }
+ return m_K;
+}
+
+void DynamicsCompressorKernel::process(
+ float* sourceChannels[], float* destinationChannels[],
+ unsigned numberOfChannels, unsigned framesToProcess,
+
+ float dbThreshold, float dbKnee, float ratio, float attackTime,
+ float releaseTime, float preDelayTime, float dbPostGain,
+ float effectBlend, /* equal power crossfade */
+
+ float releaseZone1, float releaseZone2, float releaseZone3,
+ float releaseZone4) {
+ MOZ_ASSERT(m_preDelayBuffers.Length() == numberOfChannels);
+
+ float sampleRate = this->sampleRate();
+
+ float dryMix = 1 - effectBlend;
+ float wetMix = effectBlend;
+
+ float k = updateStaticCurveParameters(dbThreshold, dbKnee, ratio);
+
+ // Makeup gain.
+ float fullRangeGain = saturate(1, k);
+ float fullRangeMakeupGain = 1 / fullRangeGain;
+
+ // Empirical/perceptual tuning.
+ fullRangeMakeupGain = powf(fullRangeMakeupGain, 0.6f);
+
+ float masterLinearGain =
+ WebAudioUtils::ConvertDecibelsToLinear(dbPostGain) * fullRangeMakeupGain;
+
+ // Attack parameters.
+ attackTime = std::max(0.001f, attackTime);
+ float attackFrames = attackTime * sampleRate;
+
+ // Release parameters.
+ float releaseFrames = sampleRate * releaseTime;
+
+ // Detector release time.
+ float satReleaseTime = 0.0025f;
+ float satReleaseFrames = satReleaseTime * sampleRate;
+
+ // Create a smooth function which passes through four points.
+
+ // Polynomial of the form
+ // y = a + b*x + c*x^2 + d*x^3 + e*x^4;
+
+ float y1 = releaseFrames * releaseZone1;
+ float y2 = releaseFrames * releaseZone2;
+ float y3 = releaseFrames * releaseZone3;
+ float y4 = releaseFrames * releaseZone4;
+
+ // All of these coefficients were derived for 4th order polynomial curve
+ // fitting where the y values match the evenly spaced x values as follows:
+ // (y1 : x == 0, y2 : x == 1, y3 : x == 2, y4 : x == 3)
+ float kA = 0.9999999999999998f * y1 + 1.8432219684323923e-16f * y2 -
+ 1.9373394351676423e-16f * y3 + 8.824516011816245e-18f * y4;
+ float kB = -1.5788320352845888f * y1 + 2.3305837032074286f * y2 -
+ 0.9141194204840429f * y3 + 0.1623677525612032f * y4;
+ float kC = 0.5334142869106424f * y1 - 1.272736789213631f * y2 +
+ 0.9258856042207512f * y3 - 0.18656310191776226f * y4;
+ float kD = 0.08783463138207234f * y1 - 0.1694162967925622f * y2 +
+ 0.08588057951595272f * y3 - 0.00429891410546283f * y4;
+ float kE = -0.042416883008123074f * y1 + 0.1115693827987602f * y2 -
+ 0.09764676325265872f * y3 + 0.028494263462021576f * y4;
+
+ // x ranges from 0 -> 3 0 1 2 3
+ // -15 -10 -5 0db
+
+ // y calculates adaptive release frames depending on the amount of
+ // compression.
+
+ setPreDelayTime(preDelayTime);
+
+ const int nDivisionFrames = 32;
+
+ const int nDivisions = framesToProcess / nDivisionFrames;
+
+ unsigned frameIndex = 0;
+ for (int i = 0; i < nDivisions; ++i) {
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ // Calculate desired gain
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ // Fix gremlins.
+ if (std::isnan(m_detectorAverage)) m_detectorAverage = 1;
+ if (std::isinf(m_detectorAverage)) m_detectorAverage = 1;
+
+ float desiredGain = m_detectorAverage;
+
+ // Pre-warp so we get desiredGain after sin() warp below.
+ float scaledDesiredGain = asinf(desiredGain) / (0.5f * M_PI);
+
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ // Deal with envelopes
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ // envelopeRate is the rate we slew from current compressor level to the
+ // desired level. The exact rate depends on if we're attacking or releasing
+ // and by how much.
+ float envelopeRate;
+
+ bool isReleasing = scaledDesiredGain > m_compressorGain;
+
+ // compressionDiffDb is the difference between current compression level and
+ // the desired level.
+ float compressionDiffDb;
+ if (scaledDesiredGain == 0.0) {
+ compressionDiffDb = PositiveInfinity<float>();
+ } else {
+ compressionDiffDb = WebAudioUtils::ConvertLinearToDecibels(
+ m_compressorGain / scaledDesiredGain, -1000.0f);
+ }
+
+ if (isReleasing) {
+ // Release mode - compressionDiffDb should be negative dB
+ m_maxAttackCompressionDiffDb = -1;
+
+ // Fix gremlins.
+ if (std::isnan(compressionDiffDb)) compressionDiffDb = -1;
+ if (std::isinf(compressionDiffDb)) compressionDiffDb = -1;
+
+ // Adaptive release - higher compression (lower compressionDiffDb)
+ // releases faster.
+
+ // Contain within range: -12 -> 0 then scale to go from 0 -> 3
+ float x = compressionDiffDb;
+ x = std::max(-12.0f, x);
+ x = std::min(0.0f, x);
+ x = 0.25f * (x + 12);
+
+ // Compute adaptive release curve using 4th order polynomial.
+ // Normal values for the polynomial coefficients would create a
+ // monotonically increasing function.
+ float x2 = x * x;
+ float x3 = x2 * x;
+ float x4 = x2 * x2;
+ float releaseFrames = kA + kB * x + kC * x2 + kD * x3 + kE * x4;
+
+#define kSpacingDb 5
+ float dbPerFrame = kSpacingDb / releaseFrames;
+
+ envelopeRate = WebAudioUtils::ConvertDecibelsToLinear(dbPerFrame);
+ } else {
+ // Attack mode - compressionDiffDb should be positive dB
+
+ // Fix gremlins.
+ if (std::isnan(compressionDiffDb)) compressionDiffDb = 1;
+ if (std::isinf(compressionDiffDb)) compressionDiffDb = 1;
+
+ // As long as we're still in attack mode, use a rate based off
+ // the largest compressionDiffDb we've encountered so far.
+ if (m_maxAttackCompressionDiffDb == -1 ||
+ m_maxAttackCompressionDiffDb < compressionDiffDb)
+ m_maxAttackCompressionDiffDb = compressionDiffDb;
+
+ float effAttenDiffDb = std::max(0.5f, m_maxAttackCompressionDiffDb);
+
+ float x = 0.25f / effAttenDiffDb;
+ envelopeRate = 1 - powf(x, 1 / attackFrames);
+ }
+
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ // Inner loop - calculate shaped power average - apply compression.
+ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+ {
+ int preDelayReadIndex = m_preDelayReadIndex;
+ int preDelayWriteIndex = m_preDelayWriteIndex;
+ float detectorAverage = m_detectorAverage;
+ float compressorGain = m_compressorGain;
+
+ int loopFrames = nDivisionFrames;
+ while (loopFrames--) {
+ float compressorInput = 0;
+
+ // Predelay signal, computing compression amount from un-delayed
+ // version.
+ for (unsigned i = 0; i < numberOfChannels; ++i) {
+ float* delayBuffer = m_preDelayBuffers[i].get();
+ float undelayedSource = sourceChannels[i][frameIndex];
+ delayBuffer[preDelayWriteIndex] = undelayedSource;
+
+ float absUndelayedSource =
+ undelayedSource > 0 ? undelayedSource : -undelayedSource;
+ if (compressorInput < absUndelayedSource)
+ compressorInput = absUndelayedSource;
+ }
+
+ // Calculate shaped power on undelayed input.
+
+ float scaledInput = compressorInput;
+ float absInput = scaledInput > 0 ? scaledInput : -scaledInput;
+
+ // Put through shaping curve.
+ // This is linear up to the threshold, then enters a "knee" portion
+ // followed by the "ratio" portion. The transition from the threshold to
+ // the knee is smooth (1st derivative matched). The transition from the
+ // knee to the ratio portion is smooth (1st derivative matched).
+ float shapedInput = saturate(absInput, k);
+
+ float attenuation = absInput <= 0.0001f ? 1 : shapedInput / absInput;
+
+ float attenuationDb =
+ -WebAudioUtils::ConvertLinearToDecibels(attenuation, -1000.0f);
+ attenuationDb = std::max(2.0f, attenuationDb);
+
+ float dbPerFrame = attenuationDb / satReleaseFrames;
+
+ float satReleaseRate =
+ WebAudioUtils::ConvertDecibelsToLinear(dbPerFrame) - 1;
+
+ bool isRelease = (attenuation > detectorAverage);
+ float rate = isRelease ? satReleaseRate : 1;
+
+ detectorAverage += (attenuation - detectorAverage) * rate;
+ detectorAverage = std::min(1.0f, detectorAverage);
+
+ // Fix gremlins.
+ if (std::isnan(detectorAverage)) detectorAverage = 1;
+ if (std::isinf(detectorAverage)) detectorAverage = 1;
+
+ // Exponential approach to desired gain.
+ if (envelopeRate < 1) {
+ // Attack - reduce gain to desired.
+ compressorGain += (scaledDesiredGain - compressorGain) * envelopeRate;
+ } else {
+ // Release - exponentially increase gain to 1.0
+ compressorGain *= envelopeRate;
+ compressorGain = std::min(1.0f, compressorGain);
+ }
+
+ // Warp pre-compression gain to smooth out sharp exponential transition
+ // points.
+ float postWarpCompressorGain = sinf(0.5f * M_PI * compressorGain);
+
+ // Calculate total gain using master gain and effect blend.
+ float totalGain =
+ dryMix + wetMix * masterLinearGain * postWarpCompressorGain;
+
+ // Calculate metering.
+ float dbRealGain = 20 * log10(postWarpCompressorGain);
+ if (dbRealGain < m_meteringGain)
+ m_meteringGain = dbRealGain;
+ else
+ m_meteringGain += (dbRealGain - m_meteringGain) * m_meteringReleaseK;
+
+ // Apply final gain.
+ for (unsigned i = 0; i < numberOfChannels; ++i) {
+ float* delayBuffer = m_preDelayBuffers[i].get();
+ destinationChannels[i][frameIndex] =
+ delayBuffer[preDelayReadIndex] * totalGain;
+ }
+
+ frameIndex++;
+ preDelayReadIndex = (preDelayReadIndex + 1) & MaxPreDelayFramesMask;
+ preDelayWriteIndex = (preDelayWriteIndex + 1) & MaxPreDelayFramesMask;
+ }
+
+ // Locals back to member variables.
+ m_preDelayReadIndex = preDelayReadIndex;
+ m_preDelayWriteIndex = preDelayWriteIndex;
+ m_detectorAverage =
+ DenormalDisabler::flushDenormalFloatToZero(detectorAverage);
+ m_compressorGain =
+ DenormalDisabler::flushDenormalFloatToZero(compressorGain);
+ }
+ }
+}
+
+void DynamicsCompressorKernel::reset() {
+ m_detectorAverage = 0;
+ m_compressorGain = 1;
+ m_meteringGain = 1;
+
+ // Predelay section.
+ for (unsigned i = 0; i < m_preDelayBuffers.Length(); ++i)
+ memset(m_preDelayBuffers[i].get(), 0, sizeof(float) * MaxPreDelayFrames);
+
+ m_preDelayReadIndex = 0;
+ m_preDelayWriteIndex = DefaultPreDelayFrames;
+
+ m_maxAttackCompressionDiffDb = -1; // uninitialized state
+}
+
+} // namespace WebCore
diff --git a/dom/media/webaudio/blink/DynamicsCompressorKernel.h b/dom/media/webaudio/blink/DynamicsCompressorKernel.h
new file mode 100644
index 0000000000..bb7defbd7e
--- /dev/null
+++ b/dom/media/webaudio/blink/DynamicsCompressorKernel.h
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2011 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef DynamicsCompressorKernel_h
+#define DynamicsCompressorKernel_h
+
+#include "nsTArray.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/UniquePtr.h"
+
+namespace WebCore {
+
+class DynamicsCompressorKernel {
+ public:
+ DynamicsCompressorKernel(float sampleRate, unsigned numberOfChannels);
+
+ void setNumberOfChannels(unsigned);
+
+ // Performs stereo-linked compression.
+ void process(float* sourceChannels[], float* destinationChannels[],
+ unsigned numberOfChannels, unsigned framesToProcess,
+
+ float dbThreshold, float dbKnee, float ratio, float attackTime,
+ float releaseTime, float preDelayTime, float dbPostGain,
+ float effectBlend,
+
+ float releaseZone1, float releaseZone2, float releaseZone3,
+ float releaseZone4);
+
+ void reset();
+
+ unsigned latencyFrames() const { return m_lastPreDelayFrames; }
+
+ float sampleRate() const { return m_sampleRate; }
+
+ float meteringGain() const { return m_meteringGain; }
+
+ size_t sizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ protected:
+ float m_sampleRate;
+
+ float m_detectorAverage;
+ float m_compressorGain;
+
+ // Metering
+ float m_meteringReleaseK;
+ float m_meteringGain;
+
+ // Lookahead section.
+ enum { MaxPreDelayFrames = 1024 };
+ enum { MaxPreDelayFramesMask = MaxPreDelayFrames - 1 };
+ enum {
+ DefaultPreDelayFrames = 256
+ }; // setPreDelayTime() will override this initial value
+ unsigned m_lastPreDelayFrames;
+ void setPreDelayTime(float);
+
+ nsTArray<mozilla::UniquePtr<float[]>> m_preDelayBuffers;
+ int m_preDelayReadIndex;
+ int m_preDelayWriteIndex;
+
+ float m_maxAttackCompressionDiffDb;
+
+ // Static compression curve.
+ float kneeCurve(float x, float k);
+ float saturate(float x, float k);
+ float slopeAt(float x, float k);
+ float kAtSlope(float desiredSlope);
+
+ float updateStaticCurveParameters(float dbThreshold, float dbKnee,
+ float ratio);
+
+ // Amount of input change in dB required for 1 dB of output change.
+ // This applies to the portion of the curve above m_kneeThresholdDb (see
+ // below).
+ float m_ratio;
+ float m_slope; // Inverse ratio.
+
+ // The input to output change below the threshold is linear 1:1.
+ float m_linearThreshold;
+ float m_dbThreshold;
+
+ // m_dbKnee is the number of dB above the threshold before we enter the
+ // "ratio" portion of the curve. m_kneeThresholdDb = m_dbThreshold + m_dbKnee
+ // The portion between m_dbThreshold and m_kneeThresholdDb is the "soft knee"
+ // portion of the curve which transitions smoothly from the linear portion to
+ // the ratio portion.
+ float m_dbKnee;
+ float m_kneeThreshold;
+ float m_kneeThresholdDb;
+ float m_ykneeThresholdDb;
+
+ // Internal parameter for the knee portion of the curve.
+ float m_K;
+};
+
+} // namespace WebCore
+
+#endif // DynamicsCompressorKernel_h
diff --git a/dom/media/webaudio/blink/FFTConvolver.cpp b/dom/media/webaudio/blink/FFTConvolver.cpp
new file mode 100644
index 0000000000..2ade9031ce
--- /dev/null
+++ b/dom/media/webaudio/blink/FFTConvolver.cpp
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "FFTConvolver.h"
+#include "mozilla/PodOperations.h"
+
+using namespace mozilla;
+
+namespace WebCore {
+
+FFTConvolver::FFTConvolver(size_t fftSize, size_t renderPhase)
+ : m_frame(fftSize), m_readWriteIndex(renderPhase % (fftSize / 2)) {
+ MOZ_ASSERT(fftSize >= 2 * WEBAUDIO_BLOCK_SIZE);
+ m_inputBuffer.SetLength(fftSize);
+ PodZero(m_inputBuffer.Elements(), fftSize);
+ m_outputBuffer.SetLength(fftSize);
+ PodZero(m_outputBuffer.Elements(), fftSize);
+ m_lastOverlapBuffer.SetLength(fftSize / 2);
+ PodZero(m_lastOverlapBuffer.Elements(), fftSize / 2);
+}
+
+size_t FFTConvolver::sizeOfExcludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t amount = 0;
+ amount += m_frame.SizeOfExcludingThis(aMallocSizeOf);
+ amount += m_inputBuffer.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ amount += m_outputBuffer.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ amount += m_lastOverlapBuffer.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ return amount;
+}
+
+size_t FFTConvolver::sizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + sizeOfExcludingThis(aMallocSizeOf);
+}
+
+const float* FFTConvolver::process(FFTBlock* fftKernel, const float* sourceP) {
+ size_t halfSize = fftSize() / 2;
+
+ // WEBAUDIO_BLOCK_SIZE must be an exact multiple of halfSize,
+ // halfSize must be a multiple of WEBAUDIO_BLOCK_SIZE
+ // and > WEBAUDIO_BLOCK_SIZE.
+ MOZ_ASSERT(halfSize % WEBAUDIO_BLOCK_SIZE == 0 &&
+ WEBAUDIO_BLOCK_SIZE <= halfSize);
+
+ // Copy samples to input buffer (note contraint above!)
+ float* inputP = m_inputBuffer.Elements();
+
+ MOZ_ASSERT(sourceP && inputP &&
+ m_readWriteIndex + WEBAUDIO_BLOCK_SIZE <= m_inputBuffer.Length());
+
+ memcpy(inputP + m_readWriteIndex, sourceP,
+ sizeof(float) * WEBAUDIO_BLOCK_SIZE);
+
+ float* outputP = m_outputBuffer.Elements();
+ m_readWriteIndex += WEBAUDIO_BLOCK_SIZE;
+
+ // Check if it's time to perform the next FFT
+ if (m_readWriteIndex == halfSize) {
+ // The input buffer is now filled (get frequency-domain version)
+ m_frame.PerformFFT(m_inputBuffer.Elements());
+ m_frame.Multiply(*fftKernel);
+ m_frame.GetInverseWithoutScaling(m_outputBuffer.Elements());
+
+ // Overlap-add 1st half from previous time
+ AudioBufferAddWithScale(m_lastOverlapBuffer.Elements(), 1.0f,
+ m_outputBuffer.Elements(), halfSize);
+
+ // Finally, save 2nd half of result
+ MOZ_ASSERT(m_outputBuffer.Length() == 2 * halfSize &&
+ m_lastOverlapBuffer.Length() == halfSize);
+
+ memcpy(m_lastOverlapBuffer.Elements(), m_outputBuffer.Elements() + halfSize,
+ sizeof(float) * halfSize);
+
+ // Reset index back to start for next time
+ m_readWriteIndex = 0;
+ }
+
+ return outputP + m_readWriteIndex;
+}
+
+void FFTConvolver::reset() {
+ PodZero(m_lastOverlapBuffer.Elements(), m_lastOverlapBuffer.Length());
+ m_readWriteIndex = 0;
+}
+
+size_t FFTConvolver::latencyFrames() const {
+ return std::max<size_t>(fftSize() / 2, WEBAUDIO_BLOCK_SIZE) -
+ WEBAUDIO_BLOCK_SIZE;
+}
+
+} // namespace WebCore
diff --git a/dom/media/webaudio/blink/FFTConvolver.h b/dom/media/webaudio/blink/FFTConvolver.h
new file mode 100644
index 0000000000..d54b07f7a4
--- /dev/null
+++ b/dom/media/webaudio/blink/FFTConvolver.h
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef FFTConvolver_h
+#define FFTConvolver_h
+
+#include "nsTArray.h"
+#include "mozilla/FFTBlock.h"
+#include "mozilla/MemoryReporting.h"
+
+namespace WebCore {
+
+typedef AlignedTArray<float> AlignedAudioFloatArray;
+using mozilla::FFTBlock;
+
+class FFTConvolver {
+ public:
+ // |fftSize| must be a power of two.
+ //
+ // |renderPhase| is the initial offset in the initially zero input buffer.
+ // It is coordinated with the other stages, so they don't all do their
+ // FFTs at the same time.
+ explicit FFTConvolver(size_t fftSize, size_t renderPhase = 0);
+
+ // Process WEBAUDIO_BLOCK_SIZE elements of array |sourceP| and return a
+ // pointer to an output array of the same size.
+ //
+ // |fftKernel| must be pre-scaled for FFTBlock::GetInverseWithoutScaling().
+ //
+ // FIXME: Later, we can do more sophisticated buffering to relax this
+ // requirement...
+ const float* process(FFTBlock* fftKernel, const float* sourceP);
+
+ void reset();
+
+ size_t fftSize() const { return m_frame.FFTSize(); }
+
+ // The input to output latency is up to fftSize / 2, but alignment of the
+ // FFTs with the blocks reduces this by one block.
+ size_t latencyFrames() const;
+
+ size_t sizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+ size_t sizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ private:
+ FFTBlock m_frame;
+
+ // Buffer input until we get fftSize / 2 samples then do an FFT
+ size_t m_readWriteIndex;
+ AlignedAudioFloatArray m_inputBuffer;
+
+ // Stores output which we read a little at a time
+ AlignedAudioFloatArray m_outputBuffer;
+
+ // Saves the 2nd half of the FFT buffer, so we can do an overlap-add with the
+ // 1st half of the next one
+ AlignedAudioFloatArray m_lastOverlapBuffer;
+};
+
+} // namespace WebCore
+
+#endif // FFTConvolver_h
diff --git a/dom/media/webaudio/blink/HRTFDatabase.cpp b/dom/media/webaudio/blink/HRTFDatabase.cpp
new file mode 100644
index 0000000000..54be5ea958
--- /dev/null
+++ b/dom/media/webaudio/blink/HRTFDatabase.cpp
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "HRTFDatabase.h"
+
+#include "HRTFElevation.h"
+
+namespace WebCore {
+
+const int HRTFDatabase::MinElevation = -45;
+const int HRTFDatabase::MaxElevation = 90;
+const unsigned HRTFDatabase::RawElevationAngleSpacing = 15;
+const unsigned HRTFDatabase::NumberOfRawElevations =
+ 10; // -45 -> +90 (each 15 degrees)
+const unsigned HRTFDatabase::InterpolationFactor = 1;
+const unsigned HRTFDatabase::NumberOfTotalElevations =
+ NumberOfRawElevations * InterpolationFactor;
+
+nsReturnRef<HRTFDatabase> HRTFDatabase::create(float sampleRate) {
+ return nsReturnRef<HRTFDatabase>(new HRTFDatabase(sampleRate));
+}
+
+HRTFDatabase::HRTFDatabase(float sampleRate) : m_sampleRate(sampleRate) {
+ m_elevations.SetLength(NumberOfTotalElevations);
+
+ unsigned elevationIndex = 0;
+ for (int elevation = MinElevation; elevation <= MaxElevation;
+ elevation += RawElevationAngleSpacing) {
+ nsAutoRef<HRTFElevation> hrtfElevation(
+ HRTFElevation::createBuiltin(elevation, sampleRate));
+ MOZ_ASSERT(hrtfElevation.get());
+ if (!hrtfElevation.get()) return;
+
+ m_elevations[elevationIndex] = hrtfElevation.out();
+ elevationIndex += InterpolationFactor;
+ }
+
+ // Now, go back and interpolate elevations.
+ if (InterpolationFactor > 1) {
+ for (unsigned i = 0; i < NumberOfTotalElevations;
+ i += InterpolationFactor) {
+ unsigned j = (i + InterpolationFactor);
+ if (j >= NumberOfTotalElevations)
+ j = i; // for last elevation interpolate with itself
+
+ // Create the interpolated convolution kernels and delays.
+ for (unsigned jj = 1; jj < InterpolationFactor; ++jj) {
+ float x =
+ static_cast<float>(jj) / static_cast<float>(InterpolationFactor);
+ m_elevations[i + jj] = HRTFElevation::createByInterpolatingSlices(
+ m_elevations[i].get(), m_elevations[j].get(), x, sampleRate);
+ MOZ_ASSERT(m_elevations[i + jj].get());
+ }
+ }
+ }
+}
+
+size_t HRTFDatabase::sizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t amount = aMallocSizeOf(this);
+ amount += m_elevations.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (size_t i = 0; i < m_elevations.Length(); i++) {
+ amount += m_elevations[i]->sizeOfIncludingThis(aMallocSizeOf);
+ }
+
+ return amount;
+}
+
+void HRTFDatabase::getKernelsFromAzimuthElevation(
+ double azimuthBlend, unsigned azimuthIndex, double elevationAngle,
+ HRTFKernel*& kernelL, HRTFKernel*& kernelR, double& frameDelayL,
+ double& frameDelayR) {
+ unsigned elevationIndex = indexFromElevationAngle(elevationAngle);
+ MOZ_ASSERT(elevationIndex < m_elevations.Length() &&
+ m_elevations.Length() > 0);
+
+ if (!m_elevations.Length()) {
+ kernelL = 0;
+ kernelR = 0;
+ return;
+ }
+
+ if (elevationIndex > m_elevations.Length() - 1)
+ elevationIndex = m_elevations.Length() - 1;
+
+ HRTFElevation* hrtfElevation = m_elevations[elevationIndex].get();
+ MOZ_ASSERT(hrtfElevation);
+ if (!hrtfElevation) {
+ kernelL = 0;
+ kernelR = 0;
+ return;
+ }
+
+ hrtfElevation->getKernelsFromAzimuth(azimuthBlend, azimuthIndex, kernelL,
+ kernelR, frameDelayL, frameDelayR);
+}
+
+unsigned HRTFDatabase::indexFromElevationAngle(double elevationAngle) {
+ // Clamp to allowed range.
+ elevationAngle =
+ mozilla::clamped(elevationAngle, static_cast<double>(MinElevation),
+ static_cast<double>(MaxElevation));
+
+ unsigned elevationIndex =
+ static_cast<int>(InterpolationFactor * (elevationAngle - MinElevation) /
+ RawElevationAngleSpacing);
+ return elevationIndex;
+}
+
+} // namespace WebCore
diff --git a/dom/media/webaudio/blink/HRTFDatabase.h b/dom/media/webaudio/blink/HRTFDatabase.h
new file mode 100644
index 0000000000..50cf2958bd
--- /dev/null
+++ b/dom/media/webaudio/blink/HRTFDatabase.h
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef HRTFDatabase_h
+#define HRTFDatabase_h
+
+#include "HRTFElevation.h"
+#include "nsAutoRef.h"
+#include "nsTArray.h"
+#include "mozilla/MemoryReporting.h"
+
+namespace WebCore {
+
+class HRTFKernel;
+
+class HRTFDatabase {
+ public:
+ static nsReturnRef<HRTFDatabase> create(float sampleRate);
+
+ // clang-format off
+ // getKernelsFromAzimuthElevation() returns a left and right ear kernel, and an interpolated left and right frame delay for the given azimuth and elevation.
+ // azimuthBlend must be in the range 0 -> 1.
+ // Valid values for azimuthIndex are 0 -> HRTFElevation::NumberOfTotalAzimuths - 1 (corresponding to angles of 0 -> 360).
+ // Valid values for elevationAngle are MinElevation -> MaxElevation.
+ // clang-format on
+ void getKernelsFromAzimuthElevation(double azimuthBlend,
+ unsigned azimuthIndex,
+ double elevationAngle,
+ HRTFKernel*& kernelL,
+ HRTFKernel*& kernelR, double& frameDelayL,
+ double& frameDelayR);
+
+ // Returns the number of different azimuth angles.
+ static unsigned numberOfAzimuths() {
+ return HRTFElevation::NumberOfTotalAzimuths;
+ }
+
+ float sampleRate() const { return m_sampleRate; }
+
+ // Number of elevations loaded from resource.
+ static const unsigned NumberOfRawElevations;
+
+ size_t sizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ private:
+ HRTFDatabase(const HRTFDatabase& other) = delete;
+ void operator=(const HRTFDatabase& other) = delete;
+
+ explicit HRTFDatabase(float sampleRate);
+
+ // Minimum and maximum elevation angles (inclusive) for a HRTFDatabase.
+ static const int MinElevation;
+ static const int MaxElevation;
+ static const unsigned RawElevationAngleSpacing;
+
+ // Interpolates by this factor to get the total number of elevations from
+ // every elevation loaded from resource.
+ static const unsigned InterpolationFactor;
+
+ // Total number of elevations after interpolation.
+ static const unsigned NumberOfTotalElevations;
+
+ // Returns the index for the correct HRTFElevation given the elevation angle.
+ static unsigned indexFromElevationAngle(double);
+
+ nsTArray<nsAutoRef<HRTFElevation> > m_elevations;
+ float m_sampleRate;
+};
+
+} // namespace WebCore
+
+template <>
+class nsAutoRefTraits<WebCore::HRTFDatabase>
+ : public nsPointerRefTraits<WebCore::HRTFDatabase> {
+ public:
+ static void Release(WebCore::HRTFDatabase* ptr) { delete (ptr); }
+};
+
+#endif // HRTFDatabase_h
diff --git a/dom/media/webaudio/blink/HRTFDatabaseLoader.cpp b/dom/media/webaudio/blink/HRTFDatabaseLoader.cpp
new file mode 100644
index 0000000000..3db455545e
--- /dev/null
+++ b/dom/media/webaudio/blink/HRTFDatabaseLoader.cpp
@@ -0,0 +1,215 @@
+/*
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "HRTFDatabaseLoader.h"
+#include "HRTFDatabase.h"
+#include "GeckoProfiler.h"
+#include "nsThreadUtils.h"
+
+using namespace mozilla;
+
+namespace WebCore {
+
+// Singleton
+nsTHashtable<HRTFDatabaseLoader::LoaderByRateEntry>*
+ HRTFDatabaseLoader::s_loaderMap = nullptr;
+
+size_t HRTFDatabaseLoader::sizeOfLoaders(mozilla::MallocSizeOf aMallocSizeOf) {
+ return s_loaderMap ? s_loaderMap->SizeOfIncludingThis(aMallocSizeOf) : 0;
+}
+
+already_AddRefed<HRTFDatabaseLoader>
+HRTFDatabaseLoader::createAndLoadAsynchronouslyIfNecessary(float sampleRate) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ RefPtr<HRTFDatabaseLoader> loader;
+
+ if (!s_loaderMap) {
+ s_loaderMap = new nsTHashtable<LoaderByRateEntry>();
+ }
+
+ LoaderByRateEntry* entry = s_loaderMap->PutEntry(sampleRate);
+ loader = entry->mLoader;
+ if (loader) { // existing entry
+ MOZ_ASSERT(sampleRate == loader->databaseSampleRate());
+ return loader.forget();
+ }
+
+ loader = new HRTFDatabaseLoader(sampleRate);
+ entry->mLoader = loader;
+
+ loader->loadAsynchronously();
+
+ return loader.forget();
+}
+
+HRTFDatabaseLoader::HRTFDatabaseLoader(float sampleRate)
+ : m_refCnt(0),
+ m_threadLock("HRTFDatabaseLoader"),
+ m_databaseLoaderThread(nullptr),
+ m_databaseSampleRate(sampleRate),
+ m_databaseLoaded(false) {
+ MOZ_ASSERT(NS_IsMainThread());
+}
+
+HRTFDatabaseLoader::~HRTFDatabaseLoader() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ waitForLoaderThreadCompletion();
+ m_hrtfDatabase.reset();
+
+ if (s_loaderMap) {
+ // Remove ourself from the map.
+ s_loaderMap->RemoveEntry(m_databaseSampleRate);
+ if (s_loaderMap->Count() == 0) {
+ delete s_loaderMap;
+ s_loaderMap = nullptr;
+ }
+ }
+}
+
+size_t HRTFDatabaseLoader::sizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t amount = aMallocSizeOf(this);
+
+ // NB: Need to make sure we're not competing with the loader thread.
+ const_cast<HRTFDatabaseLoader*>(this)->waitForLoaderThreadCompletion();
+
+ if (m_hrtfDatabase) {
+ amount += m_hrtfDatabase->sizeOfIncludingThis(aMallocSizeOf);
+ }
+
+ return amount;
+}
+
+class HRTFDatabaseLoader::ProxyReleaseEvent final : public Runnable {
+ public:
+ explicit ProxyReleaseEvent(HRTFDatabaseLoader* loader)
+ : mozilla::Runnable("WebCore::HRTFDatabaseLoader::ProxyReleaseEvent"),
+ mLoader(loader) {}
+ NS_IMETHOD Run() override {
+ mLoader->MainThreadRelease();
+ return NS_OK;
+ }
+
+ private:
+ // Ownership transferred by ProxyRelease
+ HRTFDatabaseLoader* MOZ_OWNING_REF mLoader;
+};
+
+void HRTFDatabaseLoader::ProxyRelease() {
+ nsCOMPtr<nsIEventTarget> mainTarget = GetMainThreadSerialEventTarget();
+ if (MOZ_LIKELY(mainTarget)) {
+ RefPtr<ProxyReleaseEvent> event = new ProxyReleaseEvent(this);
+ DebugOnly<nsresult> rv = mainTarget->Dispatch(event, NS_DISPATCH_NORMAL);
+ MOZ_ASSERT(NS_SUCCEEDED(rv), "Failed to dispatch release event");
+ } else {
+ // Should be in XPCOM shutdown.
+ MOZ_ASSERT(NS_IsMainThread(), "Main thread is not available for dispatch.");
+ MainThreadRelease();
+ }
+}
+
+void HRTFDatabaseLoader::MainThreadRelease() {
+ MOZ_ASSERT(NS_IsMainThread());
+ int count = --m_refCnt;
+ MOZ_ASSERT(count >= 0, "extra release");
+ NS_LOG_RELEASE(this, count, "HRTFDatabaseLoader");
+ if (count == 0) {
+ // It is safe to delete here as the first reference can only be added
+ // on this (main) thread.
+ delete this;
+ }
+}
+
+// Asynchronously load the database in this thread.
+static void databaseLoaderEntry(void* threadData) {
+ AUTO_PROFILER_REGISTER_THREAD("HRTFDatabaseLdr");
+ NS_SetCurrentThreadName("HRTFDatabaseLdr");
+
+ HRTFDatabaseLoader* loader =
+ reinterpret_cast<HRTFDatabaseLoader*>(threadData);
+ MOZ_ASSERT(loader);
+ loader->load();
+}
+
+void HRTFDatabaseLoader::load() {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(!m_hrtfDatabase.get(), "Called twice");
+ // Load the default HRTF database.
+ m_hrtfDatabase = HRTFDatabase::create(m_databaseSampleRate);
+ m_databaseLoaded = true;
+ // Notifies the main thread of completion. See loadAsynchronously().
+ Release();
+}
+
+void HRTFDatabaseLoader::loadAsynchronously() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(m_refCnt, "Must not be called before a reference is added");
+
+ // Add a reference so that the destructor won't run and wait for the
+ // loader thread, until load() has completed.
+ AddRef();
+
+ MutexAutoLock locker(m_threadLock);
+
+ MOZ_ASSERT(!m_hrtfDatabase.get() && !m_databaseLoaderThread, "Called twice");
+ // Start the asynchronous database loading process.
+ m_databaseLoaderThread = PR_CreateThread(
+ PR_USER_THREAD, databaseLoaderEntry, this, PR_PRIORITY_NORMAL,
+ PR_GLOBAL_THREAD, PR_JOINABLE_THREAD, 0);
+}
+
+bool HRTFDatabaseLoader::isLoaded() const { return m_hrtfDatabase.get(); }
+
+void HRTFDatabaseLoader::waitForLoaderThreadCompletion() {
+ MutexAutoLock locker(m_threadLock);
+
+ // waitForThreadCompletion() should not be called twice for the same thread.
+ if (m_databaseLoaderThread) {
+ DebugOnly<PRStatus> status = PR_JoinThread(m_databaseLoaderThread);
+ MOZ_ASSERT(status == PR_SUCCESS, "PR_JoinThread failed");
+ }
+ m_databaseLoaderThread = nullptr;
+}
+
+void HRTFDatabaseLoader::shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (s_loaderMap) {
+ // Set s_loaderMap to nullptr so that the hashtable is not modified on
+ // reference release during enumeration.
+ nsTHashtable<LoaderByRateEntry>* loaderMap = s_loaderMap;
+ s_loaderMap = nullptr;
+ for (auto iter = loaderMap->Iter(); !iter.Done(); iter.Next()) {
+ iter.Get()->mLoader->waitForLoaderThreadCompletion();
+ }
+ delete loaderMap;
+ }
+}
+
+} // namespace WebCore
diff --git a/dom/media/webaudio/blink/HRTFDatabaseLoader.h b/dom/media/webaudio/blink/HRTFDatabaseLoader.h
new file mode 100644
index 0000000000..5d4548d247
--- /dev/null
+++ b/dom/media/webaudio/blink/HRTFDatabaseLoader.h
@@ -0,0 +1,155 @@
+/*
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef HRTFDatabaseLoader_h
+#define HRTFDatabaseLoader_h
+
+#include "nsHashKeys.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/Mutex.h"
+#include "HRTFDatabase.h"
+
+template <class EntryType>
+class nsTHashtable;
+template <class T>
+class nsAutoRef;
+
+namespace WebCore {
+
+// HRTFDatabaseLoader will asynchronously load the default HRTFDatabase in a new
+// thread.
+
+class HRTFDatabaseLoader {
+ public:
+ // Lazily creates a HRTFDatabaseLoader (if not already created) for the given
+ // sample-rate and starts loading asynchronously (when created the first
+ // time). Returns the HRTFDatabaseLoader. Must be called from the main thread.
+ static already_AddRefed<HRTFDatabaseLoader>
+ createAndLoadAsynchronouslyIfNecessary(float sampleRate);
+
+ // AddRef and Release may be called from any thread.
+ void AddRef() {
+#if defined(DEBUG) || defined(NS_BUILD_REFCNT_LOGGING)
+ int count =
+#endif
+ ++m_refCnt;
+ MOZ_ASSERT(count > 0, "invalid ref count");
+ NS_LOG_ADDREF(this, count, "HRTFDatabaseLoader", sizeof(*this));
+ }
+
+ void Release() {
+ // The last reference can't be removed on a non-main thread because
+ // the object can be accessed on the main thread from the hash
+ // table via createAndLoadAsynchronouslyIfNecessary().
+ int count = m_refCnt;
+ MOZ_ASSERT(count > 0, "extra release");
+ // Optimization attempt to possibly skip proxying the release to the
+ // main thread.
+ if (count != 1 && m_refCnt.compareExchange(count, count - 1)) {
+ NS_LOG_RELEASE(this, count - 1, "HRTFDatabaseLoader");
+ return;
+ }
+
+ ProxyRelease();
+ }
+
+ // Returns true once the default database has been completely loaded.
+ bool isLoaded() const;
+
+ // waitForLoaderThreadCompletion() may be called more than once,
+ // on any thread except m_databaseLoaderThread.
+ void waitForLoaderThreadCompletion();
+
+ HRTFDatabase* database() {
+ if (!m_databaseLoaded) {
+ return nullptr;
+ }
+ return m_hrtfDatabase.get();
+ }
+
+ float databaseSampleRate() const { return m_databaseSampleRate; }
+
+ static void shutdown();
+
+ // Called in asynchronous loading thread.
+ void load();
+
+ // Sums the size of all cached database loaders.
+ static size_t sizeOfLoaders(mozilla::MallocSizeOf aMallocSizeOf);
+
+ private:
+ // Both constructor and destructor must be called from the main thread.
+ explicit HRTFDatabaseLoader(float sampleRate);
+ ~HRTFDatabaseLoader();
+
+ size_t sizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ void ProxyRelease(); // any thread
+ void MainThreadRelease(); // main thread only
+ class ProxyReleaseEvent;
+
+ // If it hasn't already been loaded, creates a new thread and initiates
+ // asynchronous loading of the default database. This must be called from the
+ // main thread.
+ void loadAsynchronously();
+
+ // Map from sample-rate to loader.
+ class LoaderByRateEntry : public nsFloatHashKey {
+ public:
+ explicit LoaderByRateEntry(KeyTypePointer aKey)
+ : nsFloatHashKey(aKey),
+ mLoader() // so PutEntry() will zero-initialize
+ {}
+
+ size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const {
+ return mLoader ? mLoader->sizeOfIncludingThis(aMallocSizeOf) : 0;
+ }
+
+ // The HRTFDatabaseLoader removes itself from s_loaderMap on destruction.
+ HRTFDatabaseLoader* MOZ_NON_OWNING_REF mLoader;
+ };
+
+ // Keeps track of loaders on a per-sample-rate basis.
+ static nsTHashtable<LoaderByRateEntry>* s_loaderMap; // singleton
+
+ mozilla::Atomic<int> m_refCnt;
+
+ nsAutoRef<HRTFDatabase> m_hrtfDatabase;
+
+ // Holding a m_threadLock is required when accessing m_databaseLoaderThread.
+ mozilla::Mutex m_threadLock;
+ PRThread* m_databaseLoaderThread MOZ_GUARDED_BY(m_threadLock);
+
+ float m_databaseSampleRate;
+ mozilla::Atomic<bool> m_databaseLoaded;
+};
+
+} // namespace WebCore
+
+#endif // HRTFDatabaseLoader_h
diff --git a/dom/media/webaudio/blink/HRTFElevation.cpp b/dom/media/webaudio/blink/HRTFElevation.cpp
new file mode 100644
index 0000000000..af4e520384
--- /dev/null
+++ b/dom/media/webaudio/blink/HRTFElevation.cpp
@@ -0,0 +1,338 @@
+/*
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "HRTFElevation.h"
+
+#include <speex/speex_resampler.h>
+#include "mozilla/PodOperations.h"
+#include "AudioSampleFormat.h"
+
+#include "IRC_Composite_C_R0195-incl.cpp"
+
+using namespace mozilla;
+
+namespace WebCore {
+
+const int elevationSpacing = irc_composite_c_r0195_elevation_interval;
+const int firstElevation = irc_composite_c_r0195_first_elevation;
+const int numberOfElevations = MOZ_ARRAY_LENGTH(irc_composite_c_r0195);
+
+const unsigned HRTFElevation::NumberOfTotalAzimuths = 360 / 15 * 8;
+
+const int rawSampleRate = irc_composite_c_r0195_sample_rate;
+
+// Number of frames in an individual impulse response.
+const size_t ResponseFrameSize = 256;
+
+size_t HRTFElevation::sizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t amount = aMallocSizeOf(this);
+
+ amount += m_kernelListL.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (size_t i = 0; i < m_kernelListL.Length(); i++) {
+ amount += m_kernelListL[i]->sizeOfIncludingThis(aMallocSizeOf);
+ }
+
+ return amount;
+}
+
+size_t HRTFElevation::fftSizeForSampleRate(float sampleRate) {
+ // The IRCAM HRTF impulse responses were 512 sample-frames @44.1KHz,
+ // but these have been truncated to 256 samples.
+ // An FFT-size of twice impulse response size is used (for convolution).
+ // So for sample rates of 44.1KHz an FFT size of 512 is good.
+ // We double the FFT-size only for sample rates at least double this.
+ // If the FFT size is too large then the impulse response will be padded
+ // with zeros without the fade-out provided by HRTFKernel.
+ MOZ_ASSERT(sampleRate > 1.0 && sampleRate < 1048576.0);
+
+ // This is the size if we were to use all raw response samples.
+ unsigned resampledLength =
+ floorf(ResponseFrameSize * sampleRate / rawSampleRate);
+ // Keep things semi-sane, with max FFT size of 1024.
+ unsigned size = std::min(resampledLength, 1023U);
+ // Ensure a minimum of 2 * WEBAUDIO_BLOCK_SIZE (with the size++ below) for
+ // FFTConvolver and set the 8 least significant bits for rounding up to
+ // the next power of 2 below.
+ size |= 2 * WEBAUDIO_BLOCK_SIZE - 1;
+ // Round up to the next power of 2, making the FFT size no more than twice
+ // the impulse response length. This doubles size for values that are
+ // already powers of 2. This works by filling in alls bit to right of the
+ // most significant bit. The most significant bit is no greater than
+ // 1 << 9, and the least significant 8 bits were already set above, so
+ // there is at most one bit to add.
+ size |= (size >> 1);
+ size++;
+ MOZ_ASSERT((size & (size - 1)) == 0);
+
+ return size;
+}
+
+nsReturnRef<HRTFKernel> HRTFElevation::calculateKernelForAzimuthElevation(
+ int azimuth, int elevation, SpeexResamplerState* resampler,
+ float sampleRate) {
+ int elevationIndex = (elevation - firstElevation) / elevationSpacing;
+ MOZ_ASSERT(elevationIndex >= 0 && elevationIndex <= numberOfElevations);
+
+ int numberOfAzimuths = irc_composite_c_r0195[elevationIndex].count;
+ int azimuthSpacing = 360 / numberOfAzimuths;
+ MOZ_ASSERT(numberOfAzimuths * azimuthSpacing == 360);
+
+ int azimuthIndex = azimuth / azimuthSpacing;
+ MOZ_ASSERT(azimuthIndex * azimuthSpacing == azimuth);
+
+ const int16_t(&impulse_response_data)[ResponseFrameSize] =
+ irc_composite_c_r0195[elevationIndex].azimuths[azimuthIndex];
+
+ // When libspeex_resampler is compiled with FIXED_POINT, samples in
+ // speex_resampler_process_float are rounded directly to int16_t, which
+ // only works well if the floats are in the range +/-32767. On such
+ // platforms it's better to resample before converting to float anyway.
+#ifdef MOZ_SAMPLE_TYPE_S16
+# define RESAMPLER_PROCESS speex_resampler_process_int
+ const int16_t* response = impulse_response_data;
+ const int16_t* resampledResponse;
+#else
+# define RESAMPLER_PROCESS speex_resampler_process_float
+ float response[ResponseFrameSize];
+ ConvertAudioSamples(impulse_response_data, response, ResponseFrameSize);
+ float* resampledResponse;
+#endif
+
+ // Note that depending on the fftSize returned by the panner, we may be
+ // truncating the impulse response.
+ const size_t resampledResponseLength = fftSizeForSampleRate(sampleRate) / 2;
+
+ AutoTArray<AudioDataValue, 2 * ResponseFrameSize> resampled;
+ if (sampleRate == rawSampleRate) {
+ resampledResponse = response;
+ MOZ_ASSERT(resampledResponseLength == ResponseFrameSize);
+ } else {
+ resampled.SetLength(resampledResponseLength);
+ resampledResponse = resampled.Elements();
+ speex_resampler_skip_zeros(resampler);
+
+ // Feed the input buffer into the resampler.
+ spx_uint32_t in_len = ResponseFrameSize;
+ spx_uint32_t out_len = resampled.Length();
+ RESAMPLER_PROCESS(resampler, 0, response, &in_len, resampled.Elements(),
+ &out_len);
+
+ if (out_len < resampled.Length()) {
+ // The input should have all been processed.
+ MOZ_ASSERT(in_len == ResponseFrameSize);
+ // Feed in zeros get the data remaining in the resampler.
+ spx_uint32_t out_index = out_len;
+ in_len = speex_resampler_get_input_latency(resampler);
+ out_len = resampled.Length() - out_index;
+ RESAMPLER_PROCESS(resampler, 0, nullptr, &in_len,
+ resampled.Elements() + out_index, &out_len);
+ out_index += out_len;
+ // There may be some uninitialized samples remaining for very low
+ // sample rates.
+ PodZero(resampled.Elements() + out_index, resampled.Length() - out_index);
+ }
+
+ speex_resampler_reset_mem(resampler);
+ }
+
+#ifdef MOZ_SAMPLE_TYPE_S16
+ AutoTArray<float, 2 * ResponseFrameSize> floatArray;
+ floatArray.SetLength(resampledResponseLength);
+ float* floatResponse = floatArray.Elements();
+ ConvertAudioSamples(resampledResponse, floatResponse,
+ resampledResponseLength);
+#else
+ float* floatResponse = resampledResponse;
+#endif
+#undef RESAMPLER_PROCESS
+
+ return HRTFKernel::create(floatResponse, resampledResponseLength, sampleRate);
+}
+
+// The range of elevations for the IRCAM impulse responses varies depending on
+// azimuth, but the minimum elevation appears to always be -45.
+//
+// Here's how it goes:
+static int maxElevations[] = {
+ // Azimuth
+ //
+ 90, // 0
+ 45, // 15
+ 60, // 30
+ 45, // 45
+ 75, // 60
+ 45, // 75
+ 60, // 90
+ 45, // 105
+ 75, // 120
+ 45, // 135
+ 60, // 150
+ 45, // 165
+ 75, // 180
+ 45, // 195
+ 60, // 210
+ 45, // 225
+ 75, // 240
+ 45, // 255
+ 60, // 270
+ 45, // 285
+ 75, // 300
+ 45, // 315
+ 60, // 330
+ 45 // 345
+};
+
+nsReturnRef<HRTFElevation> HRTFElevation::createBuiltin(int elevation,
+ float sampleRate) {
+ if (elevation < firstElevation ||
+ elevation > firstElevation + numberOfElevations * elevationSpacing ||
+ (elevation / elevationSpacing) * elevationSpacing != elevation)
+ return nsReturnRef<HRTFElevation>();
+
+ // Spacing, in degrees, between every azimuth loaded from resource.
+ // Some elevations do not have data for all these intervals.
+ // See maxElevations.
+ static const unsigned AzimuthSpacing = 15;
+ static const unsigned NumberOfRawAzimuths = 360 / AzimuthSpacing;
+ static_assert(AzimuthSpacing * NumberOfRawAzimuths == 360, "Not a multiple");
+ static const unsigned InterpolationFactor =
+ NumberOfTotalAzimuths / NumberOfRawAzimuths;
+ static_assert(
+ NumberOfTotalAzimuths == NumberOfRawAzimuths * InterpolationFactor,
+ "Not a multiple");
+
+ HRTFKernelList kernelListL;
+ kernelListL.SetLength(NumberOfTotalAzimuths);
+
+ SpeexResamplerState* resampler =
+ sampleRate == rawSampleRate
+ ? nullptr
+ : speex_resampler_init(1, rawSampleRate, sampleRate,
+ SPEEX_RESAMPLER_QUALITY_MIN, nullptr);
+
+ // Load convolution kernels from HRTF files.
+ int interpolatedIndex = 0;
+ for (unsigned rawIndex = 0; rawIndex < NumberOfRawAzimuths; ++rawIndex) {
+ // Don't let elevation exceed maximum for this azimuth.
+ int maxElevation = maxElevations[rawIndex];
+ int actualElevation = std::min(elevation, maxElevation);
+
+ kernelListL[interpolatedIndex] = calculateKernelForAzimuthElevation(
+ rawIndex * AzimuthSpacing, actualElevation, resampler, sampleRate);
+
+ interpolatedIndex += InterpolationFactor;
+ }
+
+ if (resampler) speex_resampler_destroy(resampler);
+
+ // Now go back and interpolate intermediate azimuth values.
+ for (unsigned i = 0; i < NumberOfTotalAzimuths; i += InterpolationFactor) {
+ int j = (i + InterpolationFactor) % NumberOfTotalAzimuths;
+
+ // Create the interpolated convolution kernels and delays.
+ for (unsigned jj = 1; jj < InterpolationFactor; ++jj) {
+ float x =
+ float(jj) / float(InterpolationFactor); // interpolate from 0 -> 1
+
+ kernelListL[i + jj] = HRTFKernel::createInterpolatedKernel(
+ kernelListL[i], kernelListL[j], x);
+ }
+ }
+
+ return nsReturnRef<HRTFElevation>(
+ new HRTFElevation(std::move(kernelListL), elevation, sampleRate));
+}
+
+nsReturnRef<HRTFElevation> HRTFElevation::createByInterpolatingSlices(
+ HRTFElevation* hrtfElevation1, HRTFElevation* hrtfElevation2, float x,
+ float sampleRate) {
+ MOZ_ASSERT(hrtfElevation1 && hrtfElevation2);
+ if (!hrtfElevation1 || !hrtfElevation2) return nsReturnRef<HRTFElevation>();
+
+ MOZ_ASSERT(x >= 0.0 && x < 1.0);
+
+ HRTFKernelList kernelListL;
+ kernelListL.SetLength(NumberOfTotalAzimuths);
+
+ const HRTFKernelList& kernelListL1 = hrtfElevation1->kernelListL();
+ const HRTFKernelList& kernelListL2 = hrtfElevation2->kernelListL();
+
+ // Interpolate kernels of corresponding azimuths of the two elevations.
+ for (unsigned i = 0; i < NumberOfTotalAzimuths; ++i) {
+ kernelListL[i] = HRTFKernel::createInterpolatedKernel(kernelListL1[i],
+ kernelListL2[i], x);
+ }
+
+ // Interpolate elevation angle.
+ double angle = (1.0 - x) * hrtfElevation1->elevationAngle() +
+ x * hrtfElevation2->elevationAngle();
+
+ return nsReturnRef<HRTFElevation>(new HRTFElevation(
+ std::move(kernelListL), static_cast<int>(angle), sampleRate));
+}
+
+void HRTFElevation::getKernelsFromAzimuth(
+ double azimuthBlend, unsigned azimuthIndex, HRTFKernel*& kernelL,
+ HRTFKernel*& kernelR, double& frameDelayL, double& frameDelayR) {
+ bool checkAzimuthBlend = azimuthBlend >= 0.0 && azimuthBlend < 1.0;
+ MOZ_ASSERT(checkAzimuthBlend);
+ if (!checkAzimuthBlend) azimuthBlend = 0.0;
+
+ unsigned numKernels = m_kernelListL.Length();
+
+ bool isIndexGood = azimuthIndex < numKernels;
+ MOZ_ASSERT(isIndexGood);
+ if (!isIndexGood) {
+ kernelL = 0;
+ kernelR = 0;
+ return;
+ }
+
+ // Return the left and right kernels,
+ // using symmetry to produce the right kernel.
+ kernelL = m_kernelListL[azimuthIndex];
+ int azimuthIndexR = (numKernels - azimuthIndex) % numKernels;
+ kernelR = m_kernelListL[azimuthIndexR];
+
+ frameDelayL = kernelL->frameDelay();
+ frameDelayR = kernelR->frameDelay();
+
+ int azimuthIndex2L = (azimuthIndex + 1) % numKernels;
+ double frameDelay2L = m_kernelListL[azimuthIndex2L]->frameDelay();
+ int azimuthIndex2R = (numKernels - azimuthIndex2L) % numKernels;
+ double frameDelay2R = m_kernelListL[azimuthIndex2R]->frameDelay();
+
+ // Linearly interpolate delays.
+ frameDelayL =
+ (1.0 - azimuthBlend) * frameDelayL + azimuthBlend * frameDelay2L;
+ frameDelayR =
+ (1.0 - azimuthBlend) * frameDelayR + azimuthBlend * frameDelay2R;
+}
+
+} // namespace WebCore
diff --git a/dom/media/webaudio/blink/HRTFElevation.h b/dom/media/webaudio/blink/HRTFElevation.h
new file mode 100644
index 0000000000..cd54bbc04a
--- /dev/null
+++ b/dom/media/webaudio/blink/HRTFElevation.h
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef HRTFElevation_h
+#define HRTFElevation_h
+
+#include "HRTFKernel.h"
+#include "nsAutoRef.h"
+#include "mozilla/MemoryReporting.h"
+
+struct SpeexResamplerState_;
+typedef struct SpeexResamplerState_ SpeexResamplerState;
+
+namespace WebCore {
+
+// HRTFElevation contains all of the HRTFKernels (one left ear and one right ear
+// per azimuth angle) for a particular elevation.
+
+class HRTFElevation {
+ public:
+ // Loads and returns an HRTFElevation with the given HRTF database subject
+ // name and elevation from browser (or WebKit.framework) resources. Normally,
+ // there will only be a single HRTF database set, but this API supports the
+ // possibility of multiple ones with different names. Interpolated azimuths
+ // will be generated based on InterpolationFactor. Valid values for elevation
+ // are -45 -> +90 in 15 degree increments.
+ static nsReturnRef<HRTFElevation> createBuiltin(int elevation,
+ float sampleRate);
+
+ // Given two HRTFElevations, and an interpolation factor x: 0 -> 1, returns an
+ // interpolated HRTFElevation.
+ static nsReturnRef<HRTFElevation> createByInterpolatingSlices(
+ HRTFElevation* hrtfElevation1, HRTFElevation* hrtfElevation2, float x,
+ float sampleRate);
+
+ double elevationAngle() const { return m_elevationAngle; }
+ unsigned numberOfAzimuths() const { return NumberOfTotalAzimuths; }
+ float sampleRate() const { return m_sampleRate; }
+
+ // Returns the left and right kernels for the given azimuth index.
+ // The interpolated delays based on azimuthBlend: 0 -> 1 are returned in
+ // frameDelayL and frameDelayR.
+ void getKernelsFromAzimuth(double azimuthBlend, unsigned azimuthIndex,
+ HRTFKernel*& kernelL, HRTFKernel*& kernelR,
+ double& frameDelayL, double& frameDelayR);
+
+ // Total number of azimuths after interpolation.
+ static const unsigned NumberOfTotalAzimuths;
+
+ static size_t fftSizeForSampleRate(float sampleRate);
+
+ size_t sizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ private:
+ HRTFElevation(const HRTFElevation& other) = delete;
+ void operator=(const HRTFElevation& other) = delete;
+
+ HRTFElevation(HRTFKernelList&& kernelListL, int elevation, float sampleRate)
+ : m_kernelListL(std::move(kernelListL)),
+ m_elevationAngle(elevation),
+ m_sampleRate(sampleRate) {}
+
+ // Returns the list of left ear HRTFKernels for all the azimuths going from 0
+ // to 360 degrees.
+ const HRTFKernelList& kernelListL() { return m_kernelListL; }
+
+ // Given a specific azimuth and elevation angle, returns the left HRTFKernel.
+ // Values for azimuth must be multiples of 15 in 0 -> 345,
+ // but not all azimuths are available for elevations > +45.
+ // Valid values for elevation are -45 -> +90 in 15 degree increments.
+ static nsReturnRef<HRTFKernel> calculateKernelForAzimuthElevation(
+ int azimuth, int elevation, SpeexResamplerState* resampler,
+ float sampleRate);
+
+ HRTFKernelList m_kernelListL;
+ double m_elevationAngle;
+ float m_sampleRate;
+};
+
+} // namespace WebCore
+
+template <>
+class nsAutoRefTraits<WebCore::HRTFElevation>
+ : public nsPointerRefTraits<WebCore::HRTFElevation> {
+ public:
+ static void Release(WebCore::HRTFElevation* ptr) { delete (ptr); }
+};
+
+#endif // HRTFElevation_h
diff --git a/dom/media/webaudio/blink/HRTFKernel.cpp b/dom/media/webaudio/blink/HRTFKernel.cpp
new file mode 100644
index 0000000000..ecaa846a66
--- /dev/null
+++ b/dom/media/webaudio/blink/HRTFKernel.cpp
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "HRTFKernel.h"
+namespace WebCore {
+
+// Takes the input audio channel |impulseP| as an input impulse response and
+// calculates the average group delay. This represents the initial delay before
+// the most energetic part of the impulse response. The sample-frame delay is
+// removed from the |impulseP| impulse response, and this value is returned.
+// The |length| of the passed in |impulseP| must be must be a power of 2.
+static float extractAverageGroupDelay(float* impulseP, size_t length) {
+ // Check for power-of-2.
+ MOZ_ASSERT(length && (length & (length - 1)) == 0);
+
+ FFTBlock estimationFrame(length);
+ estimationFrame.PerformFFT(impulseP);
+
+ float frameDelay =
+ static_cast<float>(estimationFrame.ExtractAverageGroupDelay());
+ estimationFrame.GetInverse(impulseP);
+
+ return frameDelay;
+}
+
+HRTFKernel::HRTFKernel(float* impulseResponse, size_t length, float sampleRate)
+ : m_frameDelay(0), m_sampleRate(sampleRate) {
+ AlignedTArray<float> buffer;
+ // copy to a 32-byte aligned buffer
+ if (((uintptr_t)impulseResponse & 31) != 0) {
+ buffer.SetLength(length);
+ mozilla::PodCopy(buffer.Elements(), impulseResponse, length);
+ impulseResponse = buffer.Elements();
+ }
+
+ // Determine the leading delay (average group delay) for the response.
+ m_frameDelay = extractAverageGroupDelay(impulseResponse, length);
+
+ // The FFT size (with zero padding) needs to be twice the response length
+ // in order to do proper convolution.
+ unsigned fftSize = 2 * length;
+
+ // Quick fade-out (apply window) at truncation point
+ // because the impulse response has been truncated.
+ unsigned numberOfFadeOutFrames = static_cast<unsigned>(
+ sampleRate / 4410); // 10 sample-frames @44.1KHz sample-rate
+ MOZ_ASSERT(numberOfFadeOutFrames < length);
+ if (numberOfFadeOutFrames < length) {
+ for (unsigned i = length - numberOfFadeOutFrames; i < length; ++i) {
+ float x =
+ 1.0f - static_cast<float>(i - (length - numberOfFadeOutFrames)) /
+ numberOfFadeOutFrames;
+ impulseResponse[i] *= x;
+ }
+ }
+
+ m_fftFrame = mozilla::MakeUnique<FFTBlock>(fftSize);
+ m_fftFrame->PadAndMakeScaledDFT(impulseResponse, length);
+}
+
+// Interpolates two kernels with x: 0 -> 1 and returns the result.
+nsReturnRef<HRTFKernel> HRTFKernel::createInterpolatedKernel(
+ HRTFKernel* kernel1, HRTFKernel* kernel2, float x) {
+ MOZ_ASSERT(kernel1 && kernel2);
+ if (!kernel1 || !kernel2) return nsReturnRef<HRTFKernel>();
+
+ MOZ_ASSERT(x >= 0.0 && x < 1.0);
+ x = mozilla::clamped(x, 0.0f, 1.0f);
+
+ float sampleRate1 = kernel1->sampleRate();
+ float sampleRate2 = kernel2->sampleRate();
+ MOZ_ASSERT(sampleRate1 == sampleRate2);
+ if (sampleRate1 != sampleRate2) return nsReturnRef<HRTFKernel>();
+
+ float frameDelay =
+ (1 - x) * kernel1->frameDelay() + x * kernel2->frameDelay();
+
+ UniquePtr<FFTBlock> interpolatedFrame(FFTBlock::CreateInterpolatedBlock(
+ *kernel1->fftFrame(), *kernel2->fftFrame(), x));
+ return HRTFKernel::create(std::move(interpolatedFrame), frameDelay,
+ sampleRate1);
+}
+
+} // namespace WebCore
diff --git a/dom/media/webaudio/blink/HRTFKernel.h b/dom/media/webaudio/blink/HRTFKernel.h
new file mode 100644
index 0000000000..34479f6301
--- /dev/null
+++ b/dom/media/webaudio/blink/HRTFKernel.h
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef HRTFKernel_h
+#define HRTFKernel_h
+
+#include "nsAutoRef.h"
+#include "nsTArray.h"
+#include "mozilla/FFTBlock.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/UniquePtr.h"
+
+namespace WebCore {
+
+using mozilla::FFTBlock;
+using mozilla::UniquePtr;
+
+// HRTF stands for Head-Related Transfer Function.
+// HRTFKernel is a frequency-domain representation of an impulse-response used
+// as part of the spatialized panning system. For a given azimuth / elevation
+// angle there will be one HRTFKernel for the left ear transfer function, and
+// one for the right ear. The leading delay (average group delay) for each
+// impulse response is extracted:
+// m_fftFrame is the frequency-domain representation of the impulse
+// response with the delay removed
+// m_frameDelay is the leading delay of the original impulse response.
+class HRTFKernel {
+ public:
+ // Note: this is destructive on the passed in |impulseResponse|.
+ // The |length| of |impulseResponse| must be a power of two.
+ // The size of the DFT will be |2 * length|.
+ static nsReturnRef<HRTFKernel> create(float* impulseResponse, size_t length,
+ float sampleRate);
+
+ static nsReturnRef<HRTFKernel> create(UniquePtr<FFTBlock> fftFrame,
+ float frameDelay, float sampleRate);
+
+ // Given two HRTFKernels, and an interpolation factor x: 0 -> 1, returns an
+ // interpolated HRTFKernel.
+ static nsReturnRef<HRTFKernel> createInterpolatedKernel(HRTFKernel* kernel1,
+ HRTFKernel* kernel2,
+ float x);
+
+ FFTBlock* fftFrame() { return m_fftFrame.get(); }
+
+ size_t fftSize() const { return m_fftFrame->FFTSize(); }
+ float frameDelay() const { return m_frameDelay; }
+
+ float sampleRate() const { return m_sampleRate; }
+ double nyquist() const { return 0.5 * sampleRate(); }
+
+ size_t sizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t amount = aMallocSizeOf(this);
+ amount += m_fftFrame->SizeOfIncludingThis(aMallocSizeOf);
+ return amount;
+ }
+
+ private:
+ HRTFKernel(const HRTFKernel& other) = delete;
+ void operator=(const HRTFKernel& other) = delete;
+
+ // Note: this is destructive on the passed in |impulseResponse|.
+ HRTFKernel(float* impulseResponse, size_t fftSize, float sampleRate);
+
+ HRTFKernel(UniquePtr<FFTBlock> fftFrame, float frameDelay, float sampleRate)
+ : m_fftFrame(std::move(fftFrame)),
+ m_frameDelay(frameDelay),
+ m_sampleRate(sampleRate) {}
+
+ UniquePtr<FFTBlock> m_fftFrame;
+ float m_frameDelay;
+ float m_sampleRate;
+};
+
+typedef nsTArray<nsAutoRef<HRTFKernel> > HRTFKernelList;
+
+} // namespace WebCore
+
+template <>
+class nsAutoRefTraits<WebCore::HRTFKernel>
+ : public nsPointerRefTraits<WebCore::HRTFKernel> {
+ public:
+ static void Release(WebCore::HRTFKernel* ptr) { delete (ptr); }
+};
+
+namespace WebCore {
+
+inline nsReturnRef<HRTFKernel> HRTFKernel::create(float* impulseResponse,
+ size_t length,
+ float sampleRate) {
+ return nsReturnRef<HRTFKernel>(
+ new HRTFKernel(impulseResponse, length, sampleRate));
+}
+
+inline nsReturnRef<HRTFKernel> HRTFKernel::create(UniquePtr<FFTBlock> fftFrame,
+ float frameDelay,
+ float sampleRate) {
+ return nsReturnRef<HRTFKernel>(
+ new HRTFKernel(std::move(fftFrame), frameDelay, sampleRate));
+}
+
+} // namespace WebCore
+
+#endif // HRTFKernel_h
diff --git a/dom/media/webaudio/blink/HRTFPanner.cpp b/dom/media/webaudio/blink/HRTFPanner.cpp
new file mode 100644
index 0000000000..3d01fb5b39
--- /dev/null
+++ b/dom/media/webaudio/blink/HRTFPanner.cpp
@@ -0,0 +1,328 @@
+/*
+ * Copyright (C) 2010, Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "HRTFPanner.h"
+#include "HRTFDatabaseLoader.h"
+
+#include "FFTConvolver.h"
+#include "HRTFDatabase.h"
+#include "AudioBlock.h"
+
+using namespace mozilla;
+using dom::ChannelInterpretation;
+
+namespace WebCore {
+
+// The value of 2 milliseconds is larger than the largest delay which exists in
+// any HRTFKernel from the default HRTFDatabase (0.0136 seconds). We ASSERT the
+// delay values used in process() with this value.
+const float MaxDelayTimeSeconds = 0.002f;
+
+const int UninitializedAzimuth = -1;
+
+HRTFPanner::HRTFPanner(float sampleRate,
+ already_AddRefed<HRTFDatabaseLoader> databaseLoader)
+ : m_databaseLoader(databaseLoader),
+ m_sampleRate(sampleRate),
+ m_crossfadeSelection(CrossfadeSelection1),
+ m_azimuthIndex1(UninitializedAzimuth),
+ m_azimuthIndex2(UninitializedAzimuth)
+ // m_elevation1 and m_elevation2 are initialized in pan()
+ ,
+ m_crossfadeX(0),
+ m_crossfadeIncr(0),
+ m_convolverL1(HRTFElevation::fftSizeForSampleRate(sampleRate)),
+ m_convolverR1(m_convolverL1.fftSize()),
+ m_convolverL2(m_convolverL1.fftSize()),
+ m_convolverR2(m_convolverL1.fftSize()),
+ m_delayLine(MaxDelayTimeSeconds * sampleRate) {
+ MOZ_ASSERT(m_databaseLoader);
+ MOZ_COUNT_CTOR(HRTFPanner);
+}
+
+HRTFPanner::~HRTFPanner() { MOZ_COUNT_DTOR(HRTFPanner); }
+
+size_t HRTFPanner::sizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t amount = aMallocSizeOf(this);
+
+ // NB: m_databaseLoader can be shared, so it is not measured here
+ amount += m_convolverL1.sizeOfExcludingThis(aMallocSizeOf);
+ amount += m_convolverR1.sizeOfExcludingThis(aMallocSizeOf);
+ amount += m_convolverL2.sizeOfExcludingThis(aMallocSizeOf);
+ amount += m_convolverR2.sizeOfExcludingThis(aMallocSizeOf);
+ amount += m_delayLine.SizeOfExcludingThis(aMallocSizeOf);
+
+ return amount;
+}
+
+void HRTFPanner::reset() {
+ m_azimuthIndex1 = UninitializedAzimuth;
+ m_azimuthIndex2 = UninitializedAzimuth;
+ // m_elevation1 and m_elevation2 are initialized in pan()
+ m_crossfadeSelection = CrossfadeSelection1;
+ m_crossfadeX = 0.0f;
+ m_crossfadeIncr = 0.0f;
+ m_convolverL1.reset();
+ m_convolverR1.reset();
+ m_convolverL2.reset();
+ m_convolverR2.reset();
+ m_delayLine.Reset();
+}
+
+int HRTFPanner::calculateDesiredAzimuthIndexAndBlend(double azimuth,
+ double& azimuthBlend) {
+ // Convert the azimuth angle from the range -180 -> +180 into the range 0 ->
+ // 360. The azimuth index may then be calculated from this positive value.
+ if (azimuth < 0) azimuth += 360.0;
+
+ int numberOfAzimuths = HRTFDatabase::numberOfAzimuths();
+ const double angleBetweenAzimuths = 360.0 / numberOfAzimuths;
+
+ // Calculate the azimuth index and the blend (0 -> 1) for interpolation.
+ double desiredAzimuthIndexFloat = azimuth / angleBetweenAzimuths;
+ int desiredAzimuthIndex = static_cast<int>(desiredAzimuthIndexFloat);
+ azimuthBlend =
+ desiredAzimuthIndexFloat - static_cast<double>(desiredAzimuthIndex);
+
+ // We don't immediately start using this azimuth index, but instead approach
+ // this index from the last index we rendered at. This minimizes the clicks
+ // and graininess for moving sources which occur otherwise.
+ desiredAzimuthIndex = std::max(0, desiredAzimuthIndex);
+ desiredAzimuthIndex = std::min(numberOfAzimuths - 1, desiredAzimuthIndex);
+ return desiredAzimuthIndex;
+}
+
+void HRTFPanner::pan(double desiredAzimuth, double elevation,
+ const AudioBlock* inputBus, AudioBlock* outputBus) {
+#ifdef DEBUG
+ unsigned numInputChannels = inputBus->IsNull() ? 0 : inputBus->ChannelCount();
+
+ MOZ_ASSERT(numInputChannels <= 2);
+ MOZ_ASSERT(inputBus->GetDuration() == WEBAUDIO_BLOCK_SIZE);
+#endif
+
+ bool isOutputGood = outputBus && outputBus->ChannelCount() == 2 &&
+ outputBus->GetDuration() == WEBAUDIO_BLOCK_SIZE;
+ MOZ_ASSERT(isOutputGood);
+
+ if (!isOutputGood) {
+ if (outputBus) outputBus->SetNull(outputBus->GetDuration());
+ return;
+ }
+
+ HRTFDatabase* database = m_databaseLoader->database();
+ if (!database) { // not yet loaded
+ outputBus->SetNull(outputBus->GetDuration());
+ return;
+ }
+
+ // IRCAM HRTF azimuths values from the loaded database is reversed from the
+ // panner's notion of azimuth.
+ double azimuth = -desiredAzimuth;
+
+ bool isAzimuthGood = azimuth >= -180.0 && azimuth <= 180.0;
+ MOZ_ASSERT(isAzimuthGood);
+ if (!isAzimuthGood) {
+ outputBus->SetNull(outputBus->GetDuration());
+ return;
+ }
+
+ // Normally, we'll just be dealing with mono sources.
+ // If we have a stereo input, implement stereo panning with left source
+ // processed by left HRTF, and right source by right HRTF.
+
+ // Get destination pointers.
+ float* destinationL =
+ static_cast<float*>(const_cast<void*>(outputBus->mChannelData[0]));
+ float* destinationR =
+ static_cast<float*>(const_cast<void*>(outputBus->mChannelData[1]));
+
+ double azimuthBlend;
+ int desiredAzimuthIndex =
+ calculateDesiredAzimuthIndexAndBlend(azimuth, azimuthBlend);
+
+ // Initially snap azimuth and elevation values to first values encountered.
+ if (m_azimuthIndex1 == UninitializedAzimuth) {
+ m_azimuthIndex1 = desiredAzimuthIndex;
+ m_elevation1 = elevation;
+ }
+ if (m_azimuthIndex2 == UninitializedAzimuth) {
+ m_azimuthIndex2 = desiredAzimuthIndex;
+ m_elevation2 = elevation;
+ }
+
+ // Cross-fade / transition over a period of around 45 milliseconds.
+ // This is an empirical value tuned to be a reasonable trade-off between
+ // smoothness and speed.
+ const double fadeFrames = sampleRate() <= 48000 ? 2048 : 4096;
+
+ // Check for azimuth and elevation changes, initiating a cross-fade if needed.
+ if (!m_crossfadeX && m_crossfadeSelection == CrossfadeSelection1) {
+ if (desiredAzimuthIndex != m_azimuthIndex1 || elevation != m_elevation1) {
+ // Cross-fade from 1 -> 2
+ m_crossfadeIncr = 1 / fadeFrames;
+ m_azimuthIndex2 = desiredAzimuthIndex;
+ m_elevation2 = elevation;
+ }
+ }
+ if (m_crossfadeX == 1 && m_crossfadeSelection == CrossfadeSelection2) {
+ if (desiredAzimuthIndex != m_azimuthIndex2 || elevation != m_elevation2) {
+ // Cross-fade from 2 -> 1
+ m_crossfadeIncr = -1 / fadeFrames;
+ m_azimuthIndex1 = desiredAzimuthIndex;
+ m_elevation1 = elevation;
+ }
+ }
+
+ // Get the HRTFKernels and interpolated delays.
+ HRTFKernel* kernelL1;
+ HRTFKernel* kernelR1;
+ HRTFKernel* kernelL2;
+ HRTFKernel* kernelR2;
+ double frameDelayL1;
+ double frameDelayR1;
+ double frameDelayL2;
+ double frameDelayR2;
+ database->getKernelsFromAzimuthElevation(azimuthBlend, m_azimuthIndex1,
+ m_elevation1, kernelL1, kernelR1,
+ frameDelayL1, frameDelayR1);
+ database->getKernelsFromAzimuthElevation(azimuthBlend, m_azimuthIndex2,
+ m_elevation2, kernelL2, kernelR2,
+ frameDelayL2, frameDelayR2);
+
+ bool areKernelsGood = kernelL1 && kernelR1 && kernelL2 && kernelR2;
+ MOZ_ASSERT(areKernelsGood);
+ if (!areKernelsGood) {
+ outputBus->SetNull(outputBus->GetDuration());
+ return;
+ }
+
+ MOZ_ASSERT(frameDelayL1 / sampleRate() < MaxDelayTimeSeconds &&
+ frameDelayR1 / sampleRate() < MaxDelayTimeSeconds);
+ MOZ_ASSERT(frameDelayL2 / sampleRate() < MaxDelayTimeSeconds &&
+ frameDelayR2 / sampleRate() < MaxDelayTimeSeconds);
+
+ // Crossfade inter-aural delays based on transitions.
+ float frameDelaysL[WEBAUDIO_BLOCK_SIZE];
+ float frameDelaysR[WEBAUDIO_BLOCK_SIZE];
+ {
+ float x = m_crossfadeX;
+ float incr = m_crossfadeIncr;
+ for (unsigned i = 0; i < WEBAUDIO_BLOCK_SIZE; ++i) {
+ frameDelaysL[i] = (1 - x) * frameDelayL1 + x * frameDelayL2;
+ frameDelaysR[i] = (1 - x) * frameDelayR1 + x * frameDelayR2;
+ x += incr;
+ }
+ }
+
+ // First run through delay lines for inter-aural time difference.
+ m_delayLine.Write(*inputBus);
+ // "Speakers" means a mono input is read into both outputs (with possibly
+ // different delays).
+ m_delayLine.ReadChannel(frameDelaysL, outputBus, 0,
+ ChannelInterpretation::Speakers);
+ m_delayLine.ReadChannel(frameDelaysR, outputBus, 1,
+ ChannelInterpretation::Speakers);
+ m_delayLine.NextBlock();
+
+ bool needsCrossfading = m_crossfadeIncr;
+
+ const float* convolutionDestinationL1;
+ const float* convolutionDestinationR1;
+ const float* convolutionDestinationL2;
+ const float* convolutionDestinationR2;
+
+ // Now do the convolutions.
+ // Note that we avoid doing convolutions on both sets of convolvers if we're
+ // not currently cross-fading.
+
+ if (m_crossfadeSelection == CrossfadeSelection1 || needsCrossfading) {
+ convolutionDestinationL1 =
+ m_convolverL1.process(kernelL1->fftFrame(), destinationL);
+ convolutionDestinationR1 =
+ m_convolverR1.process(kernelR1->fftFrame(), destinationR);
+ }
+
+ if (m_crossfadeSelection == CrossfadeSelection2 || needsCrossfading) {
+ convolutionDestinationL2 =
+ m_convolverL2.process(kernelL2->fftFrame(), destinationL);
+ convolutionDestinationR2 =
+ m_convolverR2.process(kernelR2->fftFrame(), destinationR);
+ }
+
+ if (needsCrossfading) {
+ // Apply linear cross-fade.
+ float x = m_crossfadeX;
+ float incr = m_crossfadeIncr;
+ for (unsigned i = 0; i < WEBAUDIO_BLOCK_SIZE; ++i) {
+ destinationL[i] = (1 - x) * convolutionDestinationL1[i] +
+ x * convolutionDestinationL2[i];
+ destinationR[i] = (1 - x) * convolutionDestinationR1[i] +
+ x * convolutionDestinationR2[i];
+ x += incr;
+ }
+ // Update cross-fade value from local.
+ m_crossfadeX = x;
+
+ if (m_crossfadeIncr > 0 && fabs(m_crossfadeX - 1) < m_crossfadeIncr) {
+ // We've fully made the crossfade transition from 1 -> 2.
+ m_crossfadeSelection = CrossfadeSelection2;
+ m_crossfadeX = 1;
+ m_crossfadeIncr = 0;
+ } else if (m_crossfadeIncr < 0 && fabs(m_crossfadeX) < -m_crossfadeIncr) {
+ // We've fully made the crossfade transition from 2 -> 1.
+ m_crossfadeSelection = CrossfadeSelection1;
+ m_crossfadeX = 0;
+ m_crossfadeIncr = 0;
+ }
+ } else {
+ const float* sourceL;
+ const float* sourceR;
+ if (m_crossfadeSelection == CrossfadeSelection1) {
+ sourceL = convolutionDestinationL1;
+ sourceR = convolutionDestinationR1;
+ } else {
+ sourceL = convolutionDestinationL2;
+ sourceR = convolutionDestinationR2;
+ }
+ PodCopy(destinationL, sourceL, WEBAUDIO_BLOCK_SIZE);
+ PodCopy(destinationR, sourceR, WEBAUDIO_BLOCK_SIZE);
+ }
+}
+
+int HRTFPanner::maxTailFrames() const {
+ // Although the ideal tail time would be the length of the impulse
+ // response, there is additional tail time from the approximations in the
+ // implementation. Because HRTFPanner is implemented with a DelayKernel
+ // and a FFTConvolver, the tailTime of the HRTFPanner is the sum of the
+ // tailTime of the DelayKernel and the tailTime of the FFTConvolver. The
+ // FFTs of the convolver are fftSize(), half of which is latency, but this
+ // is aligned with blocks and so is reduced by the one block which is
+ // processed immediately.
+ return m_delayLine.MaxDelayTicks() + m_convolverL1.fftSize() / 2 +
+ m_convolverL1.latencyFrames();
+}
+
+} // namespace WebCore
diff --git a/dom/media/webaudio/blink/HRTFPanner.h b/dom/media/webaudio/blink/HRTFPanner.h
new file mode 100644
index 0000000000..b30689557b
--- /dev/null
+++ b/dom/media/webaudio/blink/HRTFPanner.h
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2010, Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS BE LIABLE FOR
+ * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef HRTFPanner_h
+#define HRTFPanner_h
+
+#include "FFTConvolver.h"
+#include "DelayBuffer.h"
+#include "mozilla/MemoryReporting.h"
+
+namespace mozilla {
+class AudioBlock;
+} // namespace mozilla
+
+namespace WebCore {
+
+typedef nsTArray<float> AudioFloatArray;
+
+class HRTFDatabaseLoader;
+
+using mozilla::AudioBlock;
+
+class HRTFPanner {
+ public:
+ HRTFPanner(float sampleRate,
+ already_AddRefed<HRTFDatabaseLoader> databaseLoader);
+ ~HRTFPanner();
+
+ // chunk durations must be 128
+ void pan(double azimuth, double elevation, const AudioBlock* inputBus,
+ AudioBlock* outputBus);
+ void reset();
+
+ size_t fftSize() const { return m_convolverL1.fftSize(); }
+
+ float sampleRate() const { return m_sampleRate; }
+
+ int maxTailFrames() const;
+
+ size_t sizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ private:
+ // Given an azimuth angle in the range -180 -> +180, returns the corresponding
+ // azimuth index for the database, and azimuthBlend which is an interpolation
+ // value from 0 -> 1.
+ int calculateDesiredAzimuthIndexAndBlend(double azimuth,
+ double& azimuthBlend);
+
+ RefPtr<HRTFDatabaseLoader> m_databaseLoader;
+
+ float m_sampleRate;
+
+ // We maintain two sets of convolvers for smooth cross-faded interpolations
+ // when then azimuth and elevation are dynamically changing. When the azimuth
+ // and elevation are not changing, we simply process with one of the two sets.
+ // Initially we use CrossfadeSelection1 corresponding to m_convolverL1 and
+ // m_convolverR1. Whenever the azimuth or elevation changes, a crossfade is
+ // initiated to transition to the new position. So if we're currently
+ // processing with CrossfadeSelection1, then we transition to
+ // CrossfadeSelection2 (and vice versa). If we're in the middle of a
+ // transition, then we wait until it is complete before initiating a new
+ // transition.
+
+ // Selects either the convolver set (m_convolverL1, m_convolverR1) or
+ // (m_convolverL2, m_convolverR2).
+ enum CrossfadeSelection { CrossfadeSelection1, CrossfadeSelection2 };
+
+ CrossfadeSelection m_crossfadeSelection;
+
+ // azimuth/elevation for CrossfadeSelection1.
+ int m_azimuthIndex1;
+ double m_elevation1;
+
+ // azimuth/elevation for CrossfadeSelection2.
+ int m_azimuthIndex2;
+ double m_elevation2;
+
+ // A crossfade value 0 <= m_crossfadeX <= 1.
+ float m_crossfadeX;
+
+ // Per-sample-frame crossfade value increment.
+ float m_crossfadeIncr;
+
+ FFTConvolver m_convolverL1;
+ FFTConvolver m_convolverR1;
+ FFTConvolver m_convolverL2;
+ FFTConvolver m_convolverR2;
+
+ mozilla::DelayBuffer m_delayLine;
+
+ AudioFloatArray m_tempL1;
+ AudioFloatArray m_tempR1;
+ AudioFloatArray m_tempL2;
+ AudioFloatArray m_tempR2;
+};
+
+} // namespace WebCore
+
+#endif // HRTFPanner_h
diff --git a/dom/media/webaudio/blink/IIRFilter.cpp b/dom/media/webaudio/blink/IIRFilter.cpp
new file mode 100644
index 0000000000..8eaa461bc3
--- /dev/null
+++ b/dom/media/webaudio/blink/IIRFilter.cpp
@@ -0,0 +1,178 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "IIRFilter.h"
+
+#include "DenormalDisabler.h"
+#include "mozilla/FloatingPoint.h"
+
+#include <mozilla/Assertions.h>
+
+#include <complex>
+
+namespace blink {
+
+// The length of the memory buffers for the IIR filter. This MUST be a power of
+// two and must be greater than the possible length of the filter coefficients.
+const int kBufferLength = 32;
+static_assert(kBufferLength >= IIRFilter::kMaxOrder + 1,
+ "Internal IIR buffer length must be greater than maximum IIR "
+ "Filter order.");
+
+IIRFilter::IIRFilter(const AudioDoubleArray* feedforwardCoef,
+ const AudioDoubleArray* feedbackCoef)
+ : m_bufferIndex(0),
+ m_feedback(feedbackCoef),
+ m_feedforward(feedforwardCoef) {
+ m_xBuffer.SetLength(kBufferLength);
+ m_yBuffer.SetLength(kBufferLength);
+ reset();
+}
+
+IIRFilter::~IIRFilter() = default;
+
+void IIRFilter::reset() {
+ memset(m_xBuffer.Elements(), 0, m_xBuffer.Length() * sizeof(double));
+ memset(m_yBuffer.Elements(), 0, m_yBuffer.Length() * sizeof(double));
+}
+
+static std::complex<double> evaluatePolynomial(const double* coef,
+ std::complex<double> z,
+ int order) {
+ // Use Horner's method to evaluate the polynomial P(z) = sum(coef[k]*z^k, k,
+ // 0, order);
+ std::complex<double> result = 0;
+
+ for (int k = order; k >= 0; --k)
+ result = result * z + std::complex<double>(coef[k]);
+
+ return result;
+}
+
+void IIRFilter::process(const float* sourceP, float* destP,
+ size_t framesToProcess) {
+ // Compute
+ //
+ // y[n] = sum(b[k] * x[n - k], k = 0, M) - sum(a[k] * y[n - k], k = 1, N)
+ //
+ // where b[k] are the feedforward coefficients and a[k] are the feedback
+ // coefficients of the filter.
+
+ // This is a Direct Form I implementation of an IIR Filter. Should we
+ // consider doing a different implementation such as Transposed Direct Form
+ // II?
+ const double* feedback = m_feedback->Elements();
+ const double* feedforward = m_feedforward->Elements();
+
+ MOZ_ASSERT(feedback);
+ MOZ_ASSERT(feedforward);
+
+ // Sanity check to see if the feedback coefficients have been scaled
+ // appropriately. It must be EXACTLY 1!
+ MOZ_ASSERT(feedback[0] == 1);
+
+ int feedbackLength = m_feedback->Length();
+ int feedforwardLength = m_feedforward->Length();
+ int minLength = std::min(feedbackLength, feedforwardLength);
+
+ double* xBuffer = m_xBuffer.Elements();
+ double* yBuffer = m_yBuffer.Elements();
+
+ for (size_t n = 0; n < framesToProcess; ++n) {
+ // To help minimize roundoff, we compute using double's, even though the
+ // filter coefficients only have single precision values.
+ double yn = feedforward[0] * sourceP[n];
+
+ // Run both the feedforward and feedback terms together, when possible.
+ for (int k = 1; k < minLength; ++k) {
+ int n = (m_bufferIndex - k) & (kBufferLength - 1);
+ yn += feedforward[k] * xBuffer[n];
+ yn -= feedback[k] * yBuffer[n];
+ }
+
+ // Handle any remaining feedforward or feedback terms.
+ for (int k = minLength; k < feedforwardLength; ++k)
+ yn += feedforward[k] * xBuffer[(m_bufferIndex - k) & (kBufferLength - 1)];
+
+ for (int k = minLength; k < feedbackLength; ++k)
+ yn -= feedback[k] * yBuffer[(m_bufferIndex - k) & (kBufferLength - 1)];
+
+ // Save the current input and output values in the memory buffers for the
+ // next output.
+ m_xBuffer[m_bufferIndex] = sourceP[n];
+ m_yBuffer[m_bufferIndex] = yn;
+
+ m_bufferIndex = (m_bufferIndex + 1) & (kBufferLength - 1);
+
+ // Avoid introducing a stream of subnormals
+ destP[n] = WebCore::DenormalDisabler::flushDenormalFloatToZero(yn);
+ MOZ_ASSERT(destP[n] == 0.0 || std::fabs(destP[n]) > FLT_MIN ||
+ std::isnan(destP[n]),
+ "output should not be subnormal, but can be NaN");
+ }
+}
+
+void IIRFilter::getFrequencyResponse(int nFrequencies, const float* frequency,
+ float* magResponse, float* phaseResponse) {
+ // Evaluate the z-transform of the filter at the given normalized frequencies
+ // from 0 to 1. (One corresponds to the Nyquist frequency.)
+ //
+ // The z-tranform of the filter is
+ //
+ // H(z) = sum(b[k]*z^(-k), k, 0, M) / sum(a[k]*z^(-k), k, 0, N);
+ //
+ // The desired frequency response is H(exp(j*omega)), where omega is in
+ // [0, 1).
+ //
+ // Let P(x) = sum(c[k]*x^k, k, 0, P) be a polynomial of order P. Then each of
+ // the sums in H(z) is equivalent to evaluating a polynomial at the point 1/z.
+
+ for (int k = 0; k < nFrequencies; ++k) {
+ // zRecip = 1/z = exp(-j*frequency)
+ double omega = -M_PI * frequency[k];
+ std::complex<double> zRecip = std::complex<double>(cos(omega), sin(omega));
+
+ std::complex<double> numerator = evaluatePolynomial(
+ m_feedforward->Elements(), zRecip, m_feedforward->Length() - 1);
+ std::complex<double> denominator = evaluatePolynomial(
+ m_feedback->Elements(), zRecip, m_feedback->Length() - 1);
+ // Strangely enough, using complex division:
+ // e.g. Complex response = numerator / denominator;
+ // fails on our test machines, yielding infinities and NaNs, so we do
+ // things the long way here.
+ double n = norm(denominator);
+ double r = (real(numerator) * real(denominator) +
+ imag(numerator) * imag(denominator)) /
+ n;
+ double i = (imag(numerator) * real(denominator) -
+ real(numerator) * imag(denominator)) /
+ n;
+ std::complex<double> response = std::complex<double>(r, i);
+
+ magResponse[k] = static_cast<float>(abs(response));
+ phaseResponse[k] =
+ static_cast<float>(atan2(imag(response), real(response)));
+ }
+}
+
+bool IIRFilter::buffersAreZero() {
+ double* xBuffer = m_xBuffer.Elements();
+ double* yBuffer = m_yBuffer.Elements();
+
+ for (size_t k = 0; k < m_feedforward->Length(); ++k) {
+ if (xBuffer[(m_bufferIndex - k) & (kBufferLength - 1)] != 0.0) {
+ return false;
+ }
+ }
+
+ for (size_t k = 0; k < m_feedback->Length(); ++k) {
+ if (fabs(yBuffer[(m_bufferIndex - k) & (kBufferLength - 1)]) >= FLT_MIN) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+} // namespace blink
diff --git a/dom/media/webaudio/blink/IIRFilter.h b/dom/media/webaudio/blink/IIRFilter.h
new file mode 100644
index 0000000000..87522add06
--- /dev/null
+++ b/dom/media/webaudio/blink/IIRFilter.h
@@ -0,0 +1,62 @@
+// Copyright 2016 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef IIRFilter_h
+#define IIRFilter_h
+
+#include "nsTArray.h"
+
+typedef nsTArray<double> AudioDoubleArray;
+
+namespace blink {
+
+class IIRFilter final {
+ public:
+ // The maximum IIR filter order. This also limits the number of feedforward
+ // coefficients. The maximum number of coefficients is 20 according to the
+ // spec.
+ const static size_t kMaxOrder = 19;
+ IIRFilter(const AudioDoubleArray* feedforwardCoef,
+ const AudioDoubleArray* feedbackCoef);
+ ~IIRFilter();
+
+ void process(const float* sourceP, float* destP, size_t framesToProcess);
+
+ void reset();
+
+ void getFrequencyResponse(int nFrequencies, const float* frequency,
+ float* magResponse, float* phaseResponse);
+
+ bool buffersAreZero();
+
+ private:
+ // Filter memory
+ //
+ // For simplicity, we assume |m_xBuffer| and |m_yBuffer| have the same length,
+ // and the length is a power of two. Since the number of coefficients has a
+ // fixed upper length, the size of xBuffer and yBuffer is fixed. |m_xBuffer|
+ // holds the old input values and |m_yBuffer| holds the old output values
+ // needed to compute the new output value.
+ //
+ // m_yBuffer[m_bufferIndex] holds the most recent output value, say, y[n].
+ // Then m_yBuffer[m_bufferIndex - k] is y[n - k]. Similarly for m_xBuffer.
+ //
+ // To minimize roundoff, these arrays are double's instead of floats.
+ AudioDoubleArray m_xBuffer;
+ AudioDoubleArray m_yBuffer;
+
+ // Index into the xBuffer and yBuffer arrays where the most current x and y
+ // values should be stored. xBuffer[bufferIndex] corresponds to x[n], the
+ // current x input value and yBuffer[bufferIndex] is where y[n], the current
+ // output value.
+ int m_bufferIndex;
+
+ // Coefficients of the IIR filter.
+ const AudioDoubleArray* m_feedback;
+ const AudioDoubleArray* m_feedforward;
+};
+
+} // namespace blink
+
+#endif // IIRFilter_h
diff --git a/dom/media/webaudio/blink/IRC_Composite_C_R0195-incl.cpp b/dom/media/webaudio/blink/IRC_Composite_C_R0195-incl.cpp
new file mode 100644
index 0000000000..37d731d3a2
--- /dev/null
+++ b/dom/media/webaudio/blink/IRC_Composite_C_R0195-incl.cpp
@@ -0,0 +1,4571 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * The sample data in the arrays here was derived for Webkit by Chris Rogers
+ * through averaging of impulse responses from the IRCAM Listen HRTF Database.
+ * The responses here are half the length of the Listen responses. This
+ * sample data has been granted to the Public Domain.
+ *
+ * This file is intended to be included in compilation of a single
+ * implementation file.
+ *
+ * Each elevation (p) contains impulse responses at a varying number of
+ * equally spaced azimuths for the left ear, ordered clockwise from in front
+ * the listener.
+ */
+
+#include "mozilla/ArrayUtils.h"
+
+using mozilla::ArrayLength;
+
+const int16_t irc_composite_c_r0195_p315[][256] =
+ {/* IRC_Composite_C_R0195_T000_P315.wav */
+ {-37, 37, -38, 39, -39, 40, -41, 42, -42, 43, -43,
+ 44, -44, 44, -44, 44, -43, 42, -39, 36, -31, 23,
+ -10, -10, 5, -655, -410, -552, -353, -474, -1525, -758, 656,
+ -263, 70, 1414, -1528, -731, -1811, 1646, 1312, -1501, -407, 8893,
+ -1543, 7696, 8084, 2629, -2452, -234, 3799, 1676, 177, -1077, -474,
+ -1325, 3527, 985, 265, -884, 373, 971, 1024, -412, 507, -173,
+ 259, -799, 635, -628, 507, -344, 394, -359, 178, -276, 349,
+ -201, 137, -249, 377, -311, 263, -404, 284, -244, 173, -243,
+ 330, -320, 112, -150, 164, -142, 174, -300, 158, -197, 13,
+ -141, 85, -190, 64, -122, 41, -122, 60, -195, 125, -163,
+ 10, -67, -6, -122, 77, -133, 26, -71, -42, -156, 48,
+ -152, -12, -89, -120, -104, -37, -154, -57, -139, -80, -165,
+ -95, -242, -81, -146, -111, -178, -109, -208, -48, -178, -131,
+ -163, -68, -169, -94, -190, -139, -190, -118, -204, -160, -220,
+ -140, -204, -171, -238, -126, -203, -114, -209, -138, -177, -124,
+ -184, -130, -175, -170, -185, -180, -231, -189, -233, -210, -236,
+ -245, -288, -208, -329, -246, -274, -199, -273, -189, -267, -208,
+ -215, -199, -187, -209, -206, -210, -123, -197, -156, -173, -142,
+ -97, -123, -97, -107, -73, -84, -39, -50, -66, -11, -50,
+ -12, -51, 8, -27, 19, -48, -9, -18, 5, -42, -15,
+ -35, -31, -27, -27, -64, -33, -54, -1, -98, -47, -56,
+ -7, -76, -47, -70, -42, -54, -65, -76, -43, -57, -9,
+ -61, -39, -58, 33, -39, 3, -34, 20, -19, 4, -71,
+ 61, -22, 10},
+ /* IRC_Composite_C_R0195_T015_P315.wav */
+ {81, -82, 83, -84, 84, -85, 86, -87, 87, -87, 87,
+ -87, 87, -86, 84, -82, 78, -72, 63, -49, 23, 33,
+ -344, -481, -563, -443, -265, -1527, -713, 251, -1453, 939, 2510,
+ -1221, -1282, -1307, 806, 585, -990, -82, 9029, -4621, 9096, 11230,
+ 4611, -3051, 400, 2105, 749, 1644, 165, -1556, -1521, 3009, 1430,
+ 723, -902, 933, 187, 28, 480, 951, -214, 122, -730, -95,
+ -137, 573, -593, 558, -692, -62, 28, 16, -505, 426, -283,
+ 227, -320, 210, -374, 303, -435, 127, -128, 76, -349, 106,
+ -364, 139, -348, 184, -425, -36, -441, 91, -413, 93, -444,
+ 33, -257, -74, -414, 93, -379, 19, -327, -43, -366, 79,
+ -293, 20, -199, -76, -207, 149, -239, 68, -247, 66, -219,
+ 61, -232, 38, -212, 36, -209, -27, -209, -58, -227, -6,
+ -309, -12, -225, -60, -199, -10, -277, 19, -207, -69, -211,
+ -30, -261, -24, -233, -102, -217, -131, -247, -144, -229, -111,
+ -256, -104, -254, -130, -241, -66, -249, -88, -223, -144, -199,
+ -148, -229, -101, -242, -189, -240, -163, -308, -147, -285, -217,
+ -239, -224, -291, -173, -269, -220, -209, -208, -240, -180, -212,
+ -201, -217, -169, -184, -174, -178, -146, -209, -108, -186, -96,
+ -108, -90, -120, -44, -101, -49, -39, -22, -44, 1, -58,
+ 3, -19, -21, -8, -6, -22, -9, 18, -15, -7, -23,
+ -12, -24, -11, -43, -33, -33, -31, -57, -5, -64, -22,
+ -45, -16, -87, -8, -40, -45, -57, -14, -53, -5, -47,
+ -21, -45, -8, -33, 13, -16, 3, -29, 6, -18, 23,
+ -31, 24, -41},
+ /* IRC_Composite_C_R0195_T030_P315.wav */
+ {9, -9, 8, -8, 8, -8, 8, -7, 7, -6, 5,
+ -4, 3, -2, 0, 2, -5, 9, -14, 23, -127, -380,
+ -922, -742, -548, 257, -2362, 972, -1653, 1079, 1193, 1855, -423,
+ -3573, -1945, 3974, 274, -2796, 11656, -988, 5085, 9362, 10293, -750,
+ -3105, -1864, 2111, 2283, -318, -221, -1158, -110, 2923, 410, -981,
+ 900, 122, -303, 833, 532, -152, 20, -462, 17, -48, 336,
+ 235, 63, -647, 577, -362, -19, -259, 102, 168, -103, -338,
+ -333, 24, -9, -458, 287, -470, -273, -216, 46, -484, 13,
+ -220, -143, -392, -290, -340, -198, -301, -237, -241, -176, -489,
+ -201, -271, -252, -335, -60, -395, -225, -412, -191, -198, -164,
+ -259, -64, -380, -75, -287, -9, -159, -126, -100, -116, -155,
+ -96, -113, -23, -100, -117, -115, -164, -180, 8, -173, -57,
+ -208, -89, -129, -73, -188, -102, -184, -93, -204, -81, -217,
+ -128, -180, -66, -279, -127, -244, -206, -146, -178, -190, -135,
+ -228, -154, -205, -100, -196, -143, -185, -140, -235, -135, -209,
+ -144, -158, -250, -202, -234, -227, -157, -214, -201, -260, -180,
+ -244, -184, -234, -151, -192, -198, -223, -199, -199, -202, -189,
+ -164, -233, -143, -220, -175, -186, -107, -156, -84, -181, -106,
+ -116, -72, -63, -29, -76, -11, -83, 15, -65, 30, -2,
+ -4, -41, 4, -49, 38, -44, 57, -2, 9, -61, 11,
+ -29, 35, -27, 2, -50, -23, -17, 1, -57, 2, -26,
+ -14, -67, -39, -9, -30, -30, -21, -44, -39, -75, -12,
+ -27, -13, -32, 13, -22, -16, 5, -2, 23, -25, 48,
+ -24, -32, -30},
+ /* IRC_Composite_C_R0195_T045_P315.wav */
+ {52, -54, 57, -59, 62, -64, 67, -71, 74, -78, 82,
+ -86, 91, -95, 100, -104, 107, -105, 80, -502, -332, -993,
+ -162, -1907, 1505, -2021, -314, 756, -842, 5057, -1148, -2263, -3462,
+ 3639, -1435, 4503, -272, 9367, 494, 8006, 14353, 933, -5033, -2453,
+ 2315, 1073, 2918, -1236, -3010, 2416, 371, 1956, -531, -1294, -177,
+ 691, 1104, -312, 402, -1300, 693, -586, 315, -26, 538, -594,
+ 294, -267, -119, -580, 378, 28, 261, -1010, 385, -342, 8,
+ -224, 217, -399, 43, -603, -1, -295, -252, 167, -340, -419,
+ -328, -348, -86, -497, -57, -294, -381, -499, -201, -454, -232,
+ -518, -17, -717, -310, -542, -84, -458, -96, -313, -154, -496,
+ -112, -226, -165, -242, -163, -234, -306, -270, -70, -238, -108,
+ -139, -101, -251, -218, -175, -17, -215, -23, -205, -109, -221,
+ -12, -131, -27, -205, -45, -264, -158, -151, -4, -169, -113,
+ -251, -102, -247, -100, -151, -158, -212, -170, -190, -217, -220,
+ -105, -182, -191, -216, -210, -208, -188, -144, -93, -288, -132,
+ -248, -124, -281, -112, -217, -92, -321, -80, -343, -94, -222,
+ -100, -231, -229, -216, -139, -209, -164, -138, -158, -204, -180,
+ -171, -145, -125, -68, -150, -139, -133, -97, -43, -133, -83,
+ -49, -109, -53, -101, -20, -84, 23, -64, -10, -116, 38,
+ -50, 15, -23, -2, -3, -27, -28, 3, 14, 36, -13,
+ -21, -18, -38, 28, -26, 9, -12, -28, -46, -29, -8,
+ 8, 18, -21, -12, -47, 16, -23, -21, -22, -28, -21,
+ -46, -32, 37, -14, -1, -31, 11, -4, -17, 51, -42,
+ 23, -30, 55},
+ /* IRC_Composite_C_R0195_T060_P315.wav */
+ {-9, 10, -10, 10, -11, 11, -11, 12, -12, 13, -13,
+ 14, -14, 14, -15, 15, -14, 14, -237, -853, -211, -839,
+ -537, -1171, 1035, -1099, 1039, 294, -1596, 6549, -2739, -2660, -4050,
+ 4749, 2134, 7195, 4024, 6882, -1377, 13010, 8996, -6905, -3319, -3088,
+ 4606, 2892, 2461, -4423, -1310, 1787, 1273, 1672, -1868, -79, 1190,
+ 70, -141, -131, 222, -677, 570, -820, 675, -811, 128, -382,
+ 165, -353, -183, -560, 68, -440, -382, -163, -67, -467, -152,
+ -570, 177, -472, -46, -374, -58, -324, -179, -380, -114, -308,
+ -223, -231, -266, -228, -188, -382, -93, -468, -172, -439, -277,
+ -467, -233, -484, -158, -431, -177, -375, -153, -360, -208, -377,
+ -90, -359, -252, -321, -261, -288, -236, -352, -117, -397, -126,
+ -318, -103, -229, -203, -302, -132, -152, -113, -159, -188, -103,
+ -160, -141, -121, -237, -158, -186, -215, -126, -180, -203, -129,
+ -258, -131, -232, -90, -192, -139, -222, -58, -238, -81, -214,
+ -111, -210, -119, -232, -97, -257, -79, -254, -124, -244, -169,
+ -224, -170, -218, -187, -194, -208, -142, -212, -136, -216, -224,
+ -142, -210, -181, -238, -144, -243, -81, -233, -107, -193, -82,
+ -124, -113, -114, -86, -61, -72, -88, -74, -42, -62, -68,
+ -57, -28, -20, -32, -42, -4, -55, -25, 11, -34, -17,
+ -21, 12, -38, -17, -4, -21, -19, -51, -19, -40, -1,
+ -58, -23, -65, -46, -5, -23, -26, -24, -22, 36, -42,
+ 4, -20, 73, -50, 19, -61, 15, -46, 1, -36, -37,
+ -6, -9, 13, -34, 18, -10, 58, -13, 38, 32, 29,
+ 4, 16, -6},
+ /* IRC_Composite_C_R0195_T075_P315.wav */
+ {-13, 13, -14, 14, -14, 14, -14, 14, -14, 13, -13,
+ 11, -9, 5, 2, -16, 50, -741, 214, -932, 242, -1432,
+ -236, -662, 1347, -571, -1196, 4105, -1805, 3633, -3512, -1059, -3340,
+ 6144, 8904, 2949, -3056, 13761, 4639, 6581, 1016, -7994, -537, 2069,
+ 9498, -3772, -2314, -3272, 3945, 434, 437, -1200, -83, 923, 138,
+ 258, 258, -7, 455, -141, 284, -478, 398, -1090, 528, -789,
+ -29, -665, -287, -554, -73, -808, -317, -229, -409, -754, -201,
+ -562, 103, -767, -233, -393, 90, -725, -54, -341, -112, -375,
+ -320, -304, -39, -501, -232, -150, -220, -432, -164, -401, -81,
+ -462, -293, -278, -139, -468, -172, -335, -180, -318, -233, -226,
+ -103, -361, -214, -194, -168, -399, -129, -254, -92, -285, -156,
+ -134, -28, -353, -136, -218, -68, -245, -164, -257, -128, -294,
+ -188, -259, -264, -283, -216, -266, -192, -249, -243, -221, -193,
+ -297, -159, -234, -247, -176, -224, -169, -180, -199, -220, -117,
+ -192, -112, -197, -146, -176, -92, -190, -108, -218, -108, -210,
+ -116, -237, -105, -215, -104, -222, -112, -224, -93, -273, -105,
+ -266, -128, -293, -88, -283, -37, -279, -49, -211, -28, -227,
+ 20, -197, 2, -189, 32, -205, 58, -176, 59, -144, 57,
+ -167, 132, -119, 93, -104, 104, -78, 87, -109, 101, -75,
+ 67, -98, 89, -87, 61, -85, 56, -78, 56, -103, 66,
+ -107, 43, -101, 53, -122, 26, -89, 31, -77, 5, -71,
+ 25, -79, 9, -93, 21, -74, 23, -83, 51, -92, 20,
+ -72, 56, -59, 56, -36, 38, 19, 39, 4, 62, -17,
+ 56, -23, 68},
+ /* IRC_Composite_C_R0195_T090_P315.wav */
+ {87, -92, 97, -103, 109, -116, 123, -131, 140, -150, 162,
+ -174, 189, -205, 224, -244, 266, -286, 51, -859, 535, -1364,
+ 139, -1195, 1589, -390, 911, -1048, 4305, 473, -5108, 558, 766,
+ 2725, 10227, -95, 2100, 7357, 10939, 4034, -9033, 1273, -876, 7923,
+ 448, -413, -5710, 1509, 967, 2067, -1395, -1318, 853, 624, -242,
+ 51, 284, 401, 299, 341, -114, 602, -538, 101, -460, -168,
+ -515, -276, -591, -395, -537, -566, -206, -633, -470, -595, -421,
+ -387, -497, -528, -554, -277, -415, -451, -399, -376, -198, -441,
+ -287, -406, -190, -420, -196, -316, -218, -371, -230, -316, -251,
+ -337, -304, -180, -217, -314, -203, -175, -197, -285, -220, -238,
+ -109, -305, -167, -234, -146, -316, -92, -192, -70, -236, -136,
+ -80, -80, -227, -175, -115, -77, -157, -162, -216, -61, -249,
+ -168, -273, -182, -316, -207, -288, -178, -282, -287, -286, -206,
+ -333, -257, -356, -226, -357, -253, -372, -144, -341, -201, -306,
+ -120, -225, -145, -256, -69, -151, -103, -191, -78, -159, -72,
+ -231, -86, -209, -102, -254, -75, -225, -131, -246, -122, -189,
+ -129, -238, -123, -178, -85, -185, -80, -172, -78, -165, -42,
+ -131, -62, -158, -35, -137, -15, -123, -23, -84, 14, -68,
+ 23, -29, 8, 12, -7, 21, 17, -7, 6, -12, 18,
+ -60, 33, -67, 51, -69, 41, -34, 41, -45, 33, -37,
+ 31, -56, 11, -29, -2, -76, 4, -43, 17, -73, -1,
+ -63, 19, -62, -23, -71, -23, -50, -30, -62, 24, -59,
+ 23, -75, 63, -53, 35, -32, 38, -9, 16, 4, 53,
+ -2, 34, -15},
+ /* IRC_Composite_C_R0195_T105_P315.wav */
+ {-4, 4, -4, 4, -4, 4, -4, 4, -5, 5, -5,
+ 6, -7, 9, -11, 17, -28, 67, 21, -444, -141, -224,
+ -503, -492, 56, 581, 659, -254, 970, 3243, -1085, -4435, 2584,
+ 328, 6386, 5564, -809, 5990, 6762, 10583, -5626, -2592, 332, 5216,
+ 4379, -2326, -2369, -2841, 2364, 794, 414, -2336, 841, 434, 185,
+ -245, 467, 54, 226, 66, 291, 109, -14, -250, -37, -434,
+ -55, -468, -301, -501, -528, -390, -260, -361, -561, -460, -370,
+ -387, -354, -670, -414, -508, -325, -671, -240, -470, -287, -539,
+ -267, -403, -309, -523, -407, -423, -444, -437, -347, -265, -394,
+ -345, -296, -231, -221, -203, -113, -208, -173, -209, -71, -127,
+ -120, -168, -119, -150, -164, -160, -171, -155, -129, -132, -133,
+ -114, -84, -152, -145, -150, -50, -140, -100, -199, -121, -188,
+ -145, -209, -221, -240, -266, -216, -273, -254, -311, -241, -309,
+ -303, -306, -288, -304, -324, -325, -280, -286, -254, -285, -213,
+ -282, -199, -235, -168, -198, -151, -152, -157, -138, -157, -155,
+ -189, -176, -212, -179, -192, -145, -184, -171, -174, -110, -150,
+ -113, -170, -84, -115, -76, -135, -71, -161, -62, -153, -49,
+ -144, -73, -126, -80, -100, -82, -63, -82, -57, -74, 14,
+ -40, -10, -30, 25, -21, 31, -22, 45, -26, 43, -15,
+ 48, -18, 20, -19, 34, -32, 26, -30, 33, -39, 33,
+ -38, 25, -62, 21, -45, -12, -65, -6, -49, -13, -61,
+ 8, -71, 9, -80, 15, -87, 37, -62, 49, -58, 33,
+ -30, 20, -31, -30, -19, -18, -22, 12, -19, 18, -18,
+ 62, -6, 19},
+ /* IRC_Composite_C_R0195_T120_P315.wav */
+ {-8, 8, -9, 10, -10, 11, -12, 13, -14, 15, -17,
+ 18, -20, 22, -24, 27, -30, 33, -36, -6, -353, -241,
+ 59, -307, -656, 81, 435, 931, -462, 1098, 2228, 1185, -5297,
+ 2389, 1039, 4366, 5054, 563, 5052, 5065, 10308, -3912, -1912, -658,
+ 6384, 3010, -2420, -2503, -2288, 1665, 1300, -465, -1526, 865, 689,
+ -244, 192, 664, -256, -73, 265, -57, -312, -7, -322, -127,
+ -354, -230, -494, -275, -378, -331, -253, -267, -433, -173, -259,
+ -320, -294, -344, -567, -293, -505, -543, -515, -293, -398, -306,
+ -402, -421, -360, -360, -466, -547, -514, -519, -327, -326, -417,
+ -471, -219, -314, -177, -232, -161, -243, -135, -249, -154, -136,
+ -89, -210, -64, -203, -123, -169, -90, -247, -100, -141, -68,
+ -108, -41, -101, -97, -95, -145, -67, -56, -122, -193, -178,
+ -180, -188, -181, -281, -271, -252, -234, -263, -302, -242, -343,
+ -214, -391, -204, -407, -189, -457, -208, -383, -206, -352, -186,
+ -336, -183, -268, -158, -243, -145, -229, -99, -205, -110, -223,
+ -83, -256, -120, -241, -124, -258, -103, -238, -73, -205, -48,
+ -194, -12, -168, -14, -161, -15, -200, -2, -184, -29, -168,
+ -39, -203, -35, -143, -59, -138, -57, -120, -39, -78, -38,
+ -71, 4, -85, 25, -41, 42, -65, 60, -35, 71, -28,
+ 56, -41, 66, -43, 40, -26, -6, -23, -11, -1, -28,
+ 3, -16, -8, -25, -4, -23, -19, -21, -43, 6, -38,
+ -12, -49, 4, -21, -1, -32, 9, -8, 22, -23, -1,
+ -11, -14, -32, -12, -40, -9, -31, 23, -39, 25, -13,
+ 20, -8, 9},
+ /* IRC_Composite_C_R0195_T135_P315.wav */
+ {-23, 24, -24, 24, -24, 24, -24, 24, -24, 24, -24,
+ 24, -24, 23, -23, 22, -21, 19, -17, 12, 2, -321,
+ 142, -352, -116, -71, -511, 212, 1241, -690, 1132, 1447, 1818,
+ -2325, -1459, 2842, 2322, 3950, -401, 7294, 1799, 9821, 384, -3014,
+ -458, 4980, 4067, -3265, -1817, -2557, 1536, 1050, 141, -1408, 467,
+ 945, 477, -318, 841, -225, -162, -191, 333, -541, -289, -847,
+ 48, -224, -228, -453, -42, -332, -101, -393, -171, -405, -103,
+ -444, -70, -591, -149, -459, -69, -570, -117, -584, -95, -396,
+ -42, -496, -210, -651, -204, -618, -333, -581, -255, -540, -313,
+ -471, -202, -369, -230, -385, -202, -278, -178, -259, -149, -205,
+ -123, -195, -100, -150, -73, -188, -126, -225, -124, -193, -92,
+ -182, -65, -172, -74, -168, -103, -186, -57, -178, -89, -280,
+ -140, -220, -97, -251, -226, -268, -204, -268, -271, -286, -293,
+ -290, -360, -269, -322, -274, -307, -261, -331, -275, -290, -250,
+ -261, -307, -242, -239, -199, -225, -211, -141, -180, -148, -189,
+ -147, -163, -178, -155, -195, -114, -193, -96, -201, -69, -171,
+ -54, -158, -66, -130, -90, -112, -83, -108, -98, -108, -90,
+ -89, -94, -103, -90, -117, -87, -105, -51, -97, -64, -70,
+ -56, -23, -79, 7, -88, 34, -80, 43, -77, 63, -50,
+ 59, -68, 43, -51, 41, -54, 60, -92, 29, -102, 48,
+ -85, 28, -79, 9, -67, 13, -66, 37, -57, 38, -64,
+ 47, -47, 53, -38, 54, -42, 32, -33, 27, -15, 13,
+ -24, -26, -34, -6, -41, 23, -23, 1, -31, 23, -1,
+ 22, -48, -14},
+ /* IRC_Composite_C_R0195_T150_P315.wav */
+ {-14, 14, -14, 14, -14, 14, -14, 14, -14, 14, -15,
+ 15, -15, 15, -15, 15, -15, 15, -15, 15, -14, 13,
+ -9, -299, 105, -109, -149, -174, -101, 123, 928, 331, 409,
+ 1091, 1957, -1219, -1800, 2020, 2641, 4225, -365, 6765, 1276, 7468,
+ 2095, -1421, -1941, 2847, 4183, -2838, -2013, -2585, 1301, 1369, 1048,
+ -1018, 299, 712, 620, -358, 419, -160, -391, -365, 85, -244,
+ -278, -735, 101, -159, -46, -323, -112, -300, -73, -375, -177,
+ -408, -241, -349, -232, -359, -171, -320, -131, -293, -2, -318,
+ -62, -331, -78, -385, -166, -449, -212, -441, -266, -408, -251,
+ -382, -246, -414, -254, -434, -274, -407, -251, -444, -252, -399,
+ -189, -349, -176, -289, -151, -238, -106, -181, -45, -201, -17,
+ -170, -8, -151, 12, -150, -68, -196, -148, -187, -145, -232,
+ -247, -286, -284, -266, -250, -295, -275, -287, -281, -302, -308,
+ -288, -299, -267, -330, -280, -301, -267, -269, -255, -300, -276,
+ -267, -240, -267, -229, -279, -186, -258, -159, -229, -176, -170,
+ -148, -137, -189, -140, -182, -128, -193, -123, -144, -95, -99,
+ -87, -75, -90, -47, -89, -71, -115, -103, -102, -112, -110,
+ -131, -87, -120, -99, -104, -130, -68, -115, -50, -116, -33,
+ -115, -23, -91, -20, -77, -32, -46, -36, -20, -19, -1,
+ -1, -16, 2, -30, -4, -10, 2, -12, -25, -22, -46,
+ -39, -40, -29, -51, -40, -76, -30, -47, -17, -48, -11,
+ -52, 8, -41, 31, -26, 41, 1, 54, -33, 53, -2,
+ 35, -28, 34, -23, 25, -31, 33, -25, -2, -7, 10,
+ -26, -60, -40},
+ /* IRC_Composite_C_R0195_T165_P315.wav */
+ {-17, 17, -18, 19, -19, 20, -21, 21, -22, 23, -24,
+ 25, -26, 27, -29, 30, -32, 33, -35, 37, -39, 42,
+ -45, 48, -52, 67, -40, 152, 75, -119, -31, 837, 302,
+ 348, 1063, 1535, 471, -104, -785, 1551, 1039, 5803, 545, 3679,
+ 3043, 5986, 50, -1878, 1830, -1451, 2642, -2294, -1114, -2432, 2146,
+ 1209, -135, -693, 285, 141, 308, -50, -77, -237, 196, -409,
+ 80, -447, -119, -467, -15, -415, -52, -429, -63, -216, -215,
+ -423, -211, -250, -99, -343, -232, -231, -78, -380, -66, -225,
+ 10, -172, -61, -149, -128, -331, -281, -369, -348, -420, -336,
+ -400, -303, -402, -320, -360, -377, -402, -338, -363, -356, -382,
+ -392, -353, -371, -352, -344, -280, -280, -250, -233, -166, -154,
+ -160, -136, -102, -71, -93, -69, -118, -111, -164, -179, -223,
+ -230, -272, -255, -285, -312, -330, -270, -347, -306, -400, -338,
+ -356, -324, -353, -315, -327, -313, -301, -321, -289, -309, -258,
+ -317, -227, -285, -196, -252, -189, -237, -155, -182, -95, -165,
+ -128, -123, -87, -127, -113, -150, -118, -122, -92, -114, -79,
+ -90, -44, -70, -19, -82, -37, -76, -88, -84, -86, -94,
+ -117, -109, -92, -108, -75, -136, -74, -119, -62, -120, -51,
+ -126, -38, -97, -20, -110, -3, -105, 29, -72, 41, -66,
+ 43, -69, 58, -54, 55, -37, 38, -45, 18, -47, 10,
+ -73, -1, -104, 15, -120, 0, -103, 12, -106, 19, -73,
+ 20, -41, 43, -9, 42, -5, 48, -4, 46, 3, 29,
+ -8, 24, 11, 13, -8, 11, 55, 13, 32, 16, 31,
+ 9, -51, -42},
+ /* IRC_Composite_C_R0195_T180_P315.wav */
+ {24, -25, 26, -27, 28, -30, 31, -33, 34, -36, 38, -40,
+ 42, -45, 47, -50, 53, -57, 61, -65, 70, -76, 82, -90,
+ 100, -116, 158, -120, 110, -47, 143, -200, 288, -83, 509, 566,
+ 678, 584, 1717, -752, -218, -645, 3620, 1547, 2553, 2618, 2850, 3932,
+ 1154, 2478, -3553, 2655, -1012, 521, -1943, -220, 418, 465, 358, -445,
+ 32, 572, 410, 189, 111, 242, -109, 240, 99, 120, -178, -44,
+ -146, -12, -251, -169, -110, -64, -56, -37, -65, 38, 137, -75,
+ -42, -159, -127, -127, -11, -39, -7, -121, -97, -173, -207, -322,
+ -357, -395, -311, -334, -329, -369, -303, -285, -259, -281, -323, -274,
+ -364, -264, -415, -314, -459, -317, -463, -346, -399, -298, -323, -239,
+ -217, -185, -167, -162, -146, -115, -142, -125, -194, -139, -196, -150,
+ -224, -208, -224, -201, -206, -219, -254, -241, -239, -289, -306, -358,
+ -320, -362, -312, -398, -353, -429, -362, -421, -352, -421, -332, -364,
+ -272, -351, -188, -256, -150, -222, -109, -165, -57, -142, -72, -111,
+ -54, -116, -66, -134, -87, -132, -39, -149, -55, -144, -3, -111,
+ -37, -108, -57, -98, -89, -106, -109, -135, -117, -147, -114, -137,
+ -99, -140, -85, -140, -56, -119, -68, -119, -57, -94, -53, -95,
+ -61, -85, -42, -76, -22, -61, -39, -37, -44, -27, -31, -14,
+ -37, -23, -16, -39, 0, -62, -11, -92, 2, -64, -27, -61,
+ -33, -39, -34, -35, -26, -27, 5, -20, 14, -33, 30, -28,
+ 12, -27, 25, -7, 7, 29, -15, 24, -9, 67, 32, 52,
+ -7, 19, -10, -34},
+ /* IRC_Composite_C_R0195_T195_P315.wav */
+ {-3, 3, -4, 4, -4, 4, -4, 4, -4, 4, -4, 4,
+ -5, 5, -5, 5, -5, 5, -6, 6, -6, 7, -7, 8,
+ -10, 16, -53, -21, 71, -52, 123, 66, -67, 138, 16, 149,
+ 471, 552, 491, 879, 834, 185, -864, 886, 1624, 2747, 1377, 2335,
+ 2675, 2476, 2941, -204, -424, -902, 1080, -594, -652, -972, -15, 737,
+ 158, 15, -103, 673, 669, 117, 134, 496, 369, 6, 174, 211,
+ 204, 39, 24, 7, -16, 8, -20, 185, 63, 80, 52, 87,
+ -18, -50, -107, -83, -58, -90, -149, -191, -215, -179, -280, -228,
+ -426, -325, -403, -260, -377, -309, -454, -294, -358, -263, -350, -288,
+ -312, -298, -376, -336, -392, -351, -440, -368, -423, -300, -356, -221,
+ -309, -156, -237, -133, -233, -159, -209, -147, -233, -211, -247, -210,
+ -236, -217, -261, -238, -270, -208, -309, -227, -327, -269, -364, -336,
+ -391, -375, -368, -412, -407, -388, -417, -354, -411, -328, -386, -273,
+ -319, -198, -244, -162, -205, -101, -178, -129, -177, -104, -161, -77,
+ -156, -48, -139, -49, -168, -54, -132, -40, -129, -49, -113, -43,
+ -97, -66, -144, -76, -174, -72, -177, -115, -186, -100, -175, -109,
+ -149, -106, -124, -101, -86, -76, -56, -81, -27, -107, 3, -87,
+ -27, -99, -52, -77, -57, -80, -68, -65, -62, -77, -17, -71,
+ -16, -67, -12, -47, -11, -33, -14, -33, -31, -4, -26, -4,
+ -79, 6, -73, -6, -74, -4, -61, -7, -62, 10, -50, 33,
+ -27, 0, -4, 51, 21, 29, 16, 43, 17, 21, 38, 16,
+ 70, -26, 47, -23},
+ /* IRC_Composite_C_R0195_T210_P315.wav */
+ {6, -6, 6, -6, 6, -6, 6, -6, 6, -6, 6, -7,
+ 7, -7, 7, -7, 7, -7, 8, -8, 8, -8, 9, -9,
+ 9, -9, 9, -9, 9, -6, -16, -81, -27, -64, -11, -116,
+ 37, -7, 308, 224, 339, 624, 595, 47, -450, 456, 1108, 1630,
+ 1528, 1730, 2147, 2381, 2098, 1287, -731, 283, 215, 213, -551, -518,
+ 308, 203, 729, -194, 652, 579, 759, 441, 763, 571, 528, 360,
+ 442, 343, 364, 229, 431, 125, 293, 81, 275, 88, 238, 45,
+ 215, 155, 198, 82, 61, 12, 5, -51, -58, -119, -156, -200,
+ -208, -255, -244, -261, -244, -232, -236, -252, -249, -226, -258, -256,
+ -283, -248, -281, -296, -308, -266, -304, -256, -333, -220, -281, -198,
+ -262, -186, -220, -164, -184, -148, -198, -168, -234, -150, -270, -190,
+ -317, -221, -358, -246, -358, -268, -356, -277, -357, -276, -375, -272,
+ -357, -257, -384, -239, -370, -230, -374, -258, -374, -262, -363, -281,
+ -344, -263, -317, -224, -292, -194, -243, -172, -216, -149, -196, -138,
+ -151, -112, -138, -121, -140, -121, -158, -136, -149, -135, -124, -140,
+ -124, -136, -125, -160, -126, -165, -139, -151, -144, -151, -125, -132,
+ -116, -99, -80, -92, -84, -64, -97, -68, -84, -76, -83, -93,
+ -75, -65, -93, -87, -89, -70, -103, -55, -102, -68, -112, -68,
+ -70, -61, -55, -62, -34, -41, -24, -58, -48, -63, -63, -52,
+ -80, -56, -81, -54, -68, -55, -48, -48, -24, -69, -21, -60,
+ -6, -21, -10, -12, -6, 14, -8, 19, 1, 57, 22, 38,
+ 15, 27, 32, 33},
+ /* IRC_Composite_C_R0195_T225_P315.wav */
+ {-7, 7, -8, 8, -8, 8, -8, 8, -8, 8, -8, 8,
+ -8, 8, -8, 8, -8, 8, -8, 9, -9, 9, -9, 9,
+ -9, 9, -9, 9, -6, 33, 28, 58, 51, 52, 71, 102,
+ 56, 94, 137, 218, 326, 455, 483, 632, 249, 92, 428, 904,
+ 1132, 1053, 1398, 1725, 1892, 1761, 996, 419, -35, 393, 230, -426,
+ 83, 187, 561, 443, 405, 624, 567, 778, 322, 714, 220, 555,
+ 133, 272, 57, 175, 33, -14, 10, 34, 86, 73, 113, 24,
+ -32, -47, -92, -107, -159, -198, -236, -286, -255, -307, -290, -357,
+ -323, -402, -335, -423, -329, -409, -335, -383, -288, -312, -274, -328,
+ -286, -276, -227, -303, -252, -323, -243, -382, -269, -374, -281, -400,
+ -300, -362, -289, -363, -292, -337, -287, -341, -275, -329, -275, -353,
+ -270, -350, -272, -365, -283, -346, -284, -356, -310, -344, -298, -351,
+ -295, -331, -289, -350, -307, -364, -293, -334, -305, -333, -280, -301,
+ -259, -287, -211, -253, -202, -233, -185, -196, -178, -176, -188, -164,
+ -153, -127, -135, -117, -145, -120, -134, -127, -143, -116, -126, -80,
+ -113, -89, -119, -102, -89, -86, -92, -78, -108, -66, -94, -63,
+ -86, -58, -73, -44, -52, -66, -55, -76, -50, -92, -33, -91,
+ -19, -106, -44, -97, -52, -76, -62, -65, -52, -53, -56, -64,
+ -38, -57, -27, -67, -32, -61, -26, -56, -44, -40, -36, -27,
+ -22, -5, -13, -6, 12, -15, 8, -1, 18, 1, -3, 6,
+ -13, 24, 6, 33, 17, 44, 11, 63, 31, 57, 47, 73,
+ 42, 34, 14, 5},
+ /* IRC_Composite_C_R0195_T240_P315.wav */
+ {0, 0, 0, -1, 1, -1, 1, -1, 1, -1, 1, -1,
+ 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1,
+ 1, -1, 2, 58, 78, 16, 22, 51, 114, 84, 41, 96,
+ 70, 113, 111, 119, 154, 190, 332, 340, 437, 381, 393, 351,
+ 291, 690, 869, 1203, 1249, 1386, 1308, 1507, 1378, 1185, 595, 424,
+ 365, 626, 420, 458, 252, 382, 637, 446, 207, 255, 488, 305,
+ 321, 49, 221, 53, 63, 79, -40, 108, -82, 101, -101, -13,
+ -203, -152, -221, -220, -235, -289, -282, -281, -296, -224, -344, -241,
+ -368, -252, -366, -268, -359, -308, -318, -288, -298, -317, -264, -285,
+ -261, -303, -269, -298, -270, -320, -331, -336, -351, -327, -351, -348,
+ -331, -347, -276, -351, -246, -372, -243, -370, -280, -365, -300, -410,
+ -295, -413, -330, -447, -345, -440, -333, -430, -333, -415, -326, -403,
+ -318, -379, -309, -372, -303, -357, -286, -300, -274, -299, -260, -274,
+ -222, -258, -207, -249, -178, -228, -179, -202, -181, -164, -194, -132,
+ -181, -100, -149, -105, -124, -85, -117, -92, -115, -104, -129, -96,
+ -119, -72, -137, -65, -104, -51, -87, -75, -80, -56, -63, -66,
+ -73, -81, -74, -88, -75, -87, -74, -68, -80, -63, -77, -50,
+ -74, -36, -76, -42, -77, -46, -59, -47, -48, -54, -32, -62,
+ -21, -50, -24, -43, -33, -23, -40, -10, -7, -13, -16, -4,
+ 2, 7, 17, -10, 37, -7, 35, 11, 27, -4, 32, -1,
+ 15, -21, -1, -28, 7, -9, 16, 21, 30, 38, 52, 37,
+ 48, 38, 34, 3},
+ /* IRC_Composite_C_R0195_T255_P315.wav */
+ {-2, 2, -2, 2, -2, 2, -2, 2, -2, 2, -2, 2,
+ -2, 2, -2, 2, -2, 2, -2, 2, -2, 2, -2, 2,
+ -3, 3, -3, 3, -3, 4, -6, 74, 75, 79, 61, 128,
+ 72, 79, 70, 94, 189, 182, 258, 204, 285, 366, 406, 480,
+ 409, 503, 603, 776, 875, 1073, 1366, 1703, 1601, 1585, 1344, 1253,
+ 1054, 708, 209, 184, 245, 471, 220, 82, 328, 234, 143, -43,
+ 229, 63, 146, 52, 46, 143, -26, 108, -162, -81, -337, -290,
+ -357, -257, -274, -266, -282, -295, -253, -242, -177, -217, -241, -242,
+ -180, -187, -261, -282, -286, -285, -278, -318, -290, -350, -241, -329,
+ -216, -356, -250, -344, -253, -301, -333, -307, -381, -310, -406, -310,
+ -398, -327, -389, -328, -383, -321, -397, -364, -416, -386, -447, -400,
+ -469, -437, -477, -423, -470, -435, -472, -439, -441, -374, -396, -361,
+ -359, -340, -300, -318, -278, -293, -236, -257, -203, -214, -171, -197,
+ -164, -178, -159, -160, -150, -162, -167, -187, -166, -177, -176, -193,
+ -156, -159, -125, -174, -110, -158, -92, -129, -96, -113, -107, -94,
+ -103, -101, -73, -98, -45, -93, -27, -108, -14, -122, -36, -134,
+ -52, -131, -66, -121, -83, -114, -73, -107, -51, -91, -32, -79,
+ 2, -74, 2, -68, 4, -68, 20, -45, 9, -63, -6, -50,
+ 8, -15, -2, -22, -9, 6, 4, 25, -11, 21, -12, 18,
+ -6, -1, -16, -16, -15, -8, 7, 8, -10, 14, -14, 21,
+ -9, 18, -34, -11, 0, 0, 38, 9, 28, 24, 33, 41,
+ 32, 56, 10, 45},
+ /* IRC_Composite_C_R0195_T270_P315.wav */
+ {5, -5, 5, -5, 5, -5, 5, -6, 6, -6, 6, -6,
+ 6, -6, 7, -7, 7, -7, 8, -8, 8, -9, 9, -10,
+ 10, -11, 12, -13, 14, -16, 19, -24, 35, -112, -87, -172,
+ -147, -119, -127, -152, -113, -103, -89, -113, -68, -116, -46, 22,
+ 42, 151, 321, 392, 548, 700, 898, 1192, 1367, 1289, 1342, 1432,
+ 1348, 1134, 1055, 823, 716, 745, 988, 819, 628, 418, 468, 438,
+ 382, 415, 357, 462, 457, 454, 331, 457, 286, 349, 199, 216,
+ 110, 272, 103, 141, 96, 107, 157, 39, 151, 90, 143, 187,
+ 135, 145, 132, 119, 90, 29, 67, 9, 33, -99, -4, -78,
+ -31, -119, -118, -125, -133, -39, -116, 6, -53, -9, -2, -28,
+ -8, -68, -109, -175, -231, -243, -320, -296, -388, -312, -439, -323,
+ -431, -308, -403, -341, -379, -320, -333, -304, -317, -322, -321, -302,
+ -340, -277, -367, -233, -348, -216, -344, -197, -304, -195, -255, -199,
+ -226, -173, -179, -167, -168, -175, -177, -169, -197, -168, -238, -156,
+ -231, -167, -240, -168, -219, -192, -218, -202, -205, -205, -186, -197,
+ -157, -172, -143, -163, -152, -143, -150, -149, -177, -148, -159, -149,
+ -150, -163, -154, -183, -160, -189, -132, -190, -124, -156, -96, -125,
+ -92, -99, -99, -74, -85, -61, -83, -67, -68, -74, -62, -91,
+ -54, -91, -46, -108, -22, -86, -15, -70, -6, -63, -3, -43,
+ -35, -76, -52, -91, -99, -89, -83, -72, -67, -70, -42, -53,
+ -31, -59, -32, -49, -39, -55, -65, -43, -43, -36, -50, -25,
+ -39, -34, -26, -31},
+ /* IRC_Composite_C_R0195_T285_P315.wav */
+ {0, 0, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1,
+ -1, 1, -1, 1, -1, 1, -1, 1, -1, 2, -2, 2,
+ -2, 3, -3, 0, -53, -54, -56, -85, -47, -114, -135, -181,
+ -153, -193, -267, -244, -55, -96, 1, 65, -104, -149, -27, 77,
+ 318, 544, 620, 776, 1096, 1388, 1457, 1232, 925, 941, 734, 587,
+ 918, 901, 616, 762, 669, 803, 637, 587, 517, 641, 585, 459,
+ 366, 454, 473, 472, 377, 381, 369, 477, 434, 303, 311, 258,
+ 424, 217, 330, 203, 348, 239, 206, 163, 147, 190, 93, 131,
+ 3, 88, 29, 61, 25, -4, 30, -29, -32, -79, -68, -48,
+ -73, -75, -119, -13, -30, 17, -48, 5, -35, 5, -39, -80,
+ -142, -105, -167, -166, -226, -200, -281, -233, -317, -276, -334, -290,
+ -353, -338, -366, -316, -342, -297, -343, -306, -273, -252, -266, -278,
+ -281, -276, -239, -265, -258, -270, -226, -250, -191, -215, -181, -217,
+ -149, -202, -131, -195, -119, -225, -148, -263, -146, -220, -148, -221,
+ -170, -215, -156, -215, -188, -262, -202, -269, -194, -235, -202, -241,
+ -188, -209, -153, -204, -154, -203, -167, -217, -151, -214, -172, -225,
+ -158, -182, -145, -170, -150, -164, -121, -136, -87, -134, -90, -102,
+ -41, -87, -63, -73, -53, -73, -60, -73, -67, -76, -98, -68,
+ -64, -72, -69, -72, -35, -71, -32, -55, -37, -56, -47, -39,
+ -69, -58, -90, -53, -99, -57, -83, -34, -50, -55, -58, -67,
+ -57, -63, -70, -62, -74, -53, -70, -30, -49, -56, -37, -35,
+ -21, -10, -19, -29},
+ /* IRC_Composite_C_R0195_T300_P315.wav */
+ {2, -2, 2, -2, 2, -2, 2, -2, 2, -2, 2, -2,
+ 2, -2, 2, -2, 2, -2, 2, -2, 2, -2, 2, -2,
+ 2, -2, 2, -3, 4, -10, -133, -133, -170, -242, -249, -433,
+ -299, -139, -159, 192, 133, -70, 22, -303, 18, -19, 635, 218,
+ 706, 1507, 1430, 1383, 2387, 1796, 1075, 392, 368, 909, 611, 309,
+ -168, 353, 2, 566, 420, 570, 464, 600, 533, 643, 640, 611,
+ 592, 536, 352, 641, 435, 676, 299, 466, 377, 487, 301, 349,
+ 330, 262, 326, 234, 228, 152, 89, 163, 56, 54, -10, 7,
+ -78, -45, -66, -85, -68, -104, -18, 4, -26, 41, 2, 31,
+ 0, 79, 5, 36, 26, 46, 69, 42, -19, -67, -82, -80,
+ -133, -166, -222, -198, -222, -228, -274, -302, -279, -261, -261, -282,
+ -253, -280, -237, -241, -240, -230, -249, -229, -231, -221, -230, -219,
+ -245, -228, -234, -188, -202, -201, -222, -167, -182, -137, -203, -165,
+ -172, -150, -180, -163, -208, -169, -194, -164, -229, -193, -245, -204,
+ -252, -223, -246, -236, -257, -266, -272, -274, -271, -280, -267, -251,
+ -243, -225, -207, -200, -174, -184, -165, -180, -160, -164, -160, -158,
+ -167, -156, -169, -151, -138, -125, -125, -108, -104, -80, -94, -71,
+ -82, -75, -85, -57, -68, -57, -49, -58, -52, -65, -51, -59,
+ -53, -74, -44, -57, -44, -43, -49, -55, -42, -57, -55, -65,
+ -60, -62, -53, -54, -78, -70, -64, -55, -66, -73, -66, -48,
+ -68, -87, -49, -57, -32, -77, -35, -66, -33, -71, -35, -70,
+ 15, -17, -11, -33},
+ /* IRC_Composite_C_R0195_T315_P315.wav */
+ {-3, 3, -4, 4, -4, 4, -4, 4, -4, 4, -4, 5,
+ -5, 5, -5, 5, -5, 5, -6, 6, -6, 6, -6, 6,
+ -6, 6, -5, 3, 2, -152, -177, -483, -235, -110, -653, -348,
+ -150, -38, 208, 331, 33, -385, -315, -27, 364, 811, -94, 2504,
+ 888, 2086, 3574, 3328, 531, 314, 789, 729, 404, -337, 56, 36,
+ 119, 405, 418, 656, 613, 503, 327, 557, 477, 459, 703, 343,
+ 212, 405, 546, 432, 317, 280, 459, 304, 364, 271, 334, 99,
+ 204, 199, 155, 38, 34, 17, 94, 27, 75, -28, 51, -48,
+ 69, -29, 44, -56, 45, -30, 20, 5, 41, -28, 41, 31,
+ 56, 13, 68, 19, 90, -37, 31, -65, 21, -87, -51, -142,
+ -103, -155, -98, -191, -137, -221, -116, -207, -139, -209, -136, -203,
+ -147, -207, -168, -209, -164, -226, -176, -243, -198, -258, -194, -267,
+ -197, -246, -196, -229, -191, -208, -190, -182, -197, -201, -171, -193,
+ -170, -221, -151, -220, -141, -245, -167, -251, -184, -255, -210, -246,
+ -238, -245, -223, -258, -247, -284, -254, -278, -249, -311, -255, -297,
+ -217, -263, -193, -236, -176, -198, -176, -178, -155, -172, -140, -181,
+ -132, -195, -91, -184, -92, -165, -90, -121, -68, -117, -88, -97,
+ -47, -73, -55, -70, -16, -46, -13, -61, -26, -38, -50, -48,
+ -41, -43, -40, -60, -49, -55, -58, -57, -47, -68, -57, -81,
+ -49, -74, -61, -74, -29, -70, -35, -79, -39, -87, -35, -94,
+ -49, -86, -55, -65, -49, -58, -43, -30, -32, -23, -33, -5,
+ -23, 19, -13, -17},
+ /* IRC_Composite_C_R0195_T330_P315.wav */
+ {-7, 7, -7, 7, -7, 7, -7, 7, -7, 7, -7, 7,
+ -7, 7, -7, 7, -7, 8, -8, 8, -8, 9, -9, 9,
+ -10, 11, -11, -55, -296, -386, -360, -162, -486, -689, -594, 323,
+ -279, 489, 253, -225, -799, -208, -290, 1653, -875, 1421, 3185, 831,
+ 3972, 4758, 3089, -835, 674, 981, 1685, -351, -114, -17, 248, 390,
+ 777, 359, 517, 535, 503, 290, 712, 58, 681, 346, 142, -203,
+ 198, 199, 331, -307, 34, 132, 139, 70, 114, 91, 13, 123,
+ 184, 191, 4, 23, 205, 163, 125, 72, 141, -40, 14, 124,
+ -28, 33, -51, 40, -5, 35, -58, 73, -52, -16, 56, 78,
+ -40, -37, 45, 32, 27, 55, -7, -15, -18, 29, -29, 22,
+ -117, 6, -57, -69, -79, -66, -162, -105, -133, -121, -164, -165,
+ -210, -152, -236, -167, -217, -215, -247, -204, -244, -201, -239, -194,
+ -273, -180, -249, -181, -223, -192, -224, -139, -212, -188, -198, -169,
+ -226, -134, -219, -153, -208, -148, -204, -149, -219, -168, -214, -162,
+ -245, -160, -257, -219, -229, -208, -279, -218, -292, -266, -282, -268,
+ -275, -227, -298, -216, -236, -164, -238, -151, -226, -142, -217, -147,
+ -203, -145, -177, -146, -138, -122, -131, -73, -151, -51, -110, -31,
+ -107, -6, -86, -9, -40, -10, -36, -1, -49, -9, -42, -8,
+ -74, 2, -73, 3, -80, -26, -79, 6, -86, -19, -76, -29,
+ -75, -32, -78, -38, -65, -46, -77, -46, -88, -55, -75, -74,
+ -82, -34, -77, -58, -74, -44, -61, -16, -21, -21, -1, -32,
+ 25, -5, -9, -4},
+ /* IRC_Composite_C_R0195_T345_P315.wav */
+ {-3, 3, -3, 3, -3, 3, -3, 3, -3, 3, -2, 2,
+ -2, 2, -1, 1, 0, 0, 1, -2, 3, -4, 6, -9,
+ 13, -23, -205, -456, -570, -178, -609, -516, -1049, -349, 515, 157,
+ -280, 1141, -1675, -135, -1096, 1502, 703, -567, 2409, 4580, 1615, 5569,
+ 6660, -925, 461, 21, 3064, -94, 275, -939, 176, 137, 1125, 718,
+ -145, 338, 911, 169, 330, 365, 552, 148, 369, -393, 179, -127,
+ 204, 80, -84, -339, 8, 81, -65, -288, 145, 50, -22, -24,
+ 61, 62, 45, -71, 194, 159, 54, -19, 226, -56, 24, 48,
+ 99, -31, -91, -8, 53, 24, -79, 12, 113, -69, -3, 43,
+ 52, -53, 25, 22, 42, -13, -5, -1, 64, -132, -15, -42,
+ -12, -176, -19, -103, -113, -146, -76, -99, -119, -177, -139, -96,
+ -193, -229, -107, -210, -181, -214, -106, -219, -211, -156, -157, -151,
+ -246, -153, -144, -151, -254, -173, -147, -195, -202, -155, -196, -154,
+ -199, -151, -187, -147, -192, -144, -157, -216, -170, -139, -200, -196,
+ -193, -158, -269, -172, -256, -190, -293, -226, -252, -229, -318, -238,
+ -250, -265, -306, -166, -254, -191, -300, -137, -229, -129, -286, -136,
+ -186, -156, -221, -108, -162, -109, -143, -76, -137, -51, -129, -13,
+ -99, -24, -56, 17, -62, -20, -41, 40, -86, 44, -52, 39,
+ -123, 42, -65, 39, -115, -16, -46, 0, -101, -28, -69, -10,
+ -115, -27, -101, 20, -128, 3, -110, 13, -133, -3, -101, -28,
+ -103, -3, -79, -10, -82, 10, -56, 27, -77, 25, -50, 57,
+ -33, 20, -49, 39}};
+
+const int16_t irc_composite_c_r0195_p330[][256] =
+ {/* IRC_Composite_C_R0195_T000_P330.wav */
+ {-59, 61, -63, 65, -67, 69, -71, 73, -76, 78, -81,
+ 83, -86, 89, -92, 95, -98, 100, -102, 103, -103, 99,
+ -90, 66, 27, -206, -447, -812, -1040, -532, -532, -221, -2282,
+ 2737, -1254, 634, -40, 577, -3131, -820, -289, 5598, -4456, 4514,
+ 11627, -957, 5804, 7393, 108, -5260, 1410, 3318, 1621, -1983, 2287,
+ 284, 1686, 1238, 2060, -1196, 399, 489, 1551, 51, 439, -136,
+ 620, 290, -51, 26, 548, -418, 535, -429, 119, -693, 256,
+ -317, 191, -412, 18, -258, -48, -331, 42, -245, -151, -84,
+ -244, -168, -112, -248, 56, -243, -130, -346, 3, -311, -140,
+ -201, -205, -208, -164, -257, -153, -216, -142, -212, -32, -240,
+ -169, -200, -80, -259, -56, -266, -49, -259, -153, -247, -131,
+ -186, -180, -157, -174, -170, -168, -133, -130, -144, -118, -156,
+ -112, -157, -115, -125, -52, -108, -95, -100, -113, -69, -143,
+ -89, -121, -105, -124, -163, -161, -185, -102, -218, -93, -213,
+ -136, -158, -144, -182, -122, -122, -161, -147, -153, -151, -116,
+ -162, -100, -155, -94, -176, -98, -138, -94, -127, -119, -122,
+ -158, -125, -151, -130, -178, -168, -144, -200, -186, -199, -181,
+ -157, -192, -201, -216, -176, -230, -170, -197, -219, -207, -194,
+ -203, -187, -195, -174, -204, -175, -205, -170, -207, -182, -206,
+ -156, -195, -149, -193, -165, -200, -125, -166, -120, -182, -142,
+ -113, -73, -137, -86, -97, -75, -88, -72, -83, -37, -82,
+ -24, -72, -40, -53, 24, -38, -23, -48, 32, 10, 7,
+ -19, 18, 7, 43, 7, 34, 32, 65, 53, 56, 21,
+ 94, 55, 42},
+ /* IRC_Composite_C_R0195_T015_P330.wav */
+ {-33, 32, -32, 31, -31, 30, -28, 27, -25, 22, -19,
+ 15, -10, 4, 5, -16, 31, -53, 84, -136, 240, -673,
+ 177, -174, -1030, -1321, -76, -1250, -372, 289, -2348, 2418, 1442,
+ -314, -2260, 351, -1076, 96, -4201, 12653, -2793, 350, 16253, 7416,
+ -1734, 373, 1945, -1722, 32, 2367, 1990, -2906, 2200, 2573, 955,
+ -397, 1580, -424, 684, 371, 1473, 401, 8, -1088, 1156, -110,
+ 164, -526, 361, -233, -337, -706, 160, -330, -24, -130, 132,
+ -810, 170, -491, 242, -434, 33, -622, 355, -505, -308, -68,
+ -61, -264, -71, -244, -294, -284, -179, -295, 41, -462, -220,
+ -369, -36, -507, -206, -424, -65, -416, -180, -392, -246, -316,
+ -59, -245, -204, -339, -224, -205, -78, -425, -107, -254, -66,
+ -399, -77, -350, 46, -239, -96, -256, -101, -268, -138, -142,
+ -190, -179, -136, -172, -84, -261, -40, -173, 25, -283, -32,
+ -282, 9, -234, -87, -198, -101, -238, -154, -207, -159, -166,
+ -106, -164, -136, -199, -109, -121, -73, -175, -65, -149, -73,
+ -162, -108, -135, -78, -99, -124, -126, -179, -91, -133, -84,
+ -188, -122, -167, -148, -155, -182, -183, -160, -158, -219, -207,
+ -224, -181, -161, -192, -206, -207, -174, -243, -140, -189, -173,
+ -194, -138, -203, -173, -192, -143, -155, -154, -216, -158, -170,
+ -138, -202, -110, -182, -132, -154, -83, -201, -104, -130, -44,
+ -129, -80, -139, -19, -72, -80, -88, -16, -110, -20, -46,
+ -35, -88, 15, -32, 6, -67, -9, -16, 52, -18, 27,
+ 27, 26, -2, 60, 51, 76, 28, 98, 40, 77, 33,
+ 94, 11, 62},
+ /* IRC_Composite_C_R0195_T030_P330.wav */
+ {15, -16, 18, -19, 21, -23, 26, -29, 32, -36,
+ 41, -47, 54, -62, 73, -86, 104, -126, 155, -102,
+ 601, -1085, -197, -1634, 218, -1892, 836, -1472, 627, 332,
+ 2248, 1679, -4388, -1076, 2778, -5228, 3611, 10385, -5976, 10952,
+ 11266, 9110, -4453, 651, -313, -354, 33, 2898, -224, -1072,
+ 2571, 1824, -115, 539, 940, -84, 401, 1263, 337, 3,
+ -171, -426, 685, -69, -27, -94, -916, -1001, 131, -752,
+ -458, -101, -74, -99, -305, -309, -166, -102, 238, -512,
+ 98, -429, -43, -482, 424, -183, -138, -249, -301, -240,
+ -13, -418, -203, -184, -296, -341, -269, -474, -99, -447,
+ -210, -601, -403, -560, -148, -418, -397, -430, -241, -495,
+ -140, -391, -94, -358, -170, -431, -28, -368, -72, -337,
+ -69, -376, -11, -267, -144, -348, -177, -200, -156, -317,
+ -161, -307, -97, -238, -69, -237, -166, -271, -82, -187,
+ -69, -237, -35, -248, -105, -213, -38, -207, -108, -284,
+ -91, -256, -64, -223, -92, -227, -52, -205, -48, -178,
+ -94, -174, -26, -226, -71, -202, -71, -184, -34, -207,
+ -88, -154, -79, -180, -41, -222, -49, -215, -56, -245,
+ -90, -251, -51, -260, -109, -246, -150, -229, -109, -239,
+ -125, -253, -146, -228, -85, -219, -106, -252, -82, -250,
+ -58, -194, -116, -232, -74, -193, -81, -184, -110, -195,
+ -65, -162, -92, -193, -65, -199, -29, -203, 0, -193,
+ 28, -151, -16, -159, -4, -109, -9, -90, -21, -99,
+ 36, -92, 27, -82, 30, -74, 71, -74, 64, -43,
+ 102, -28, 93, 18, 88, 22, 98, 29, 116, 21,
+ 126, -20, 133, -40, 143, -66},
+ /* IRC_Composite_C_R0195_T045_P330.wav */
+ {60, -60, 60, -60, 60, -60, 59, -57, 55, -51, 47,
+ -40, 30, -15, -9, 50, -138, 582, -538, -783, -136, 149,
+ -1756, -848, -253, -449, 1379, -797, 1611, 1075, 2352, -3846, -4473,
+ 1829, 1877, 4778, 13625, -4798, 10293, 11126, 4895, -5087, -2315, -1117,
+ -112, 3354, 2968, -606, -2642, 2599, 370, 2356, -155, 634, -118,
+ 50, -430, 371, -366, 83, 83, 846, -691, 615, -552, -124,
+ -1033, -101, -1482, 300, -868, 142, -849, -166, -356, 234, -620,
+ -26, -586, 234, -687, 38, -602, 184, -537, -8, -502, 109,
+ -408, 41, -516, 144, -611, 54, -513, -33, -440, -242, -381,
+ -176, -498, -276, -575, -140, -578, -79, -706, -148, -475, -195,
+ -498, -214, -378, -197, -385, -257, -315, -80, -443, -59, -376,
+ -102, -363, -123, -440, -39, -442, -136, -304, -144, -309, -84,
+ -271, -165, -242, -238, -200, -117, -273, -150, -258, -130, -283,
+ -92, -309, -42, -296, -47, -253, -76, -218, 8, -252, -78,
+ -168, -84, -181, -86, -207, -54, -197, -48, -239, -19, -220,
+ -19, -245, -20, -221, -34, -247, -31, -246, -93, -181, -96,
+ -211, -94, -210, -85, -235, -118, -230, -101, -229, -107, -245,
+ -109, -215, -112, -220, -113, -204, -100, -188, -108, -185, -89,
+ -182, -62, -197, -34, -195, -28, -194, -18, -182, -55, -157,
+ -81, -141, -90, -141, -91, -129, -74, -112, -85, -107, -64,
+ -103, -64, -86, -50, -93, -43, -62, -35, -57, -57, -38,
+ -24, -28, -39, -24, -20, -16, 22, 2, 35, 17, 56,
+ 35, 75, 47, 56, 59, 88, 45, 61, 91, 80, 67,
+ 62, 68, 59},
+ /* IRC_Composite_C_R0195_T060_P330.wav */
+ {-16, 18, -21, 24, -27, 30, -34, 39, -45, 51, -59,
+ 68, -79, 93, -109, 120, -488, -209, -843, -446, 581, -2538,
+ 150, -1048, 1674, -298, 921, -422, 4633, -1379, -3456, -5235, 5897,
+ -232, 13802, 4771, -1871, 13790, 9150, 509, -7321, -2062, 426, 4376,
+ 2825, -66, -4120, 2160, 1014, 1965, -182, 10, -66, 1012, -1092,
+ 605, -694, -103, -630, 876, -1446, 848, -616, 147, -237, 208,
+ -605, 430, -846, 156, -639, -33, -577, -169, -800, -107, -681,
+ -261, -491, -122, -613, -72, -646, -89, -523, -258, -835, -111,
+ -630, -192, -684, -120, -546, -114, -589, -198, -484, -123, -592,
+ -31, -441, -137, -356, -160, -262, -88, -337, -230, -206, -197,
+ -286, -158, -371, -90, -386, -164, -357, -120, -396, -123, -371,
+ -159, -394, -268, -385, -156, -411, -140, -387, -213, -351, -179,
+ -344, -165, -326, -182, -213, -154, -216, -130, -224, -133, -226,
+ -137, -212, -88, -294, -83, -283, -112, -241, -166, -252, -129,
+ -207, -171, -148, -189, -146, -134, -197, -75, -135, -114, -111,
+ -109, -99, -40, -137, -112, -65, -123, -140, -104, -208, -112,
+ -191, -189, -188, -164, -205, -119, -255, -149, -161, -150, -183,
+ -173, -162, -174, -126, -195, -97, -145, -155, -118, -129, -86,
+ -145, -98, -141, -61, -119, -118, -63, -133, -28, -155, 2,
+ -161, -3, -128, -39, -100, -78, -65, -82, -72, -98, -47,
+ -85, -70, -46, -75, -12, -101, 21, -108, 22, -100, 40,
+ -92, 45, -75, 34, -63, 31, -40, 44, -41, 58, -23,
+ 74, -14, 74, -19, 131, -21, 113, 30, 101, 49, 110,
+ 57, 108, 51},
+ /* IRC_Composite_C_R0195_T075_P330.wav */
+ {19, -20, 21, -23, 25, -26, 28, -31, 33, -36, 39,
+ -42, 45, -49, 49, -225, -118, -411, -201, -767, -125, -1198,
+ -1086, 1587, 699, -114, 366, 4108, -2207, 1045, -7292, 822, 4055,
+ 16836, -2188, 1091, 15052, 6204, 1571, -5363, -5126, 3112, 6714, 709,
+ -1466, -3132, 1975, 682, 2154, -1147, 580, -537, 152, -77, -200,
+ -771, 391, -354, -354, -457, 389, -921, 324, -902, 497, -455,
+ -10, -125, 532, -709, 91, -249, -171, -384, -149, -591, -267,
+ -649, -63, -714, -477, -400, -204, -742, -437, -554, -445, -510,
+ -517, -561, -379, -611, -347, -592, -417, -611, -301, -482, -385,
+ -503, -245, -302, -310, -258, -221, -225, -194, -239, -127, -248,
+ -118, -185, -116, -277, -78, -132, -151, -167, -94, -212, -171,
+ -287, -162, -301, -224, -259, -202, -317, -238, -263, -235, -226,
+ -328, -285, -227, -274, -225, -257, -270, -272, -203, -280, -198,
+ -229, -168, -195, -197, -135, -134, -188, -159, -143, -143, -184,
+ -122, -175, -115, -188, -85, -144, -140, -133, -147, -139, -167,
+ -127, -183, -140, -187, -127, -176, -160, -172, -135, -190, -179,
+ -159, -144, -201, -142, -175, -140, -206, -117, -148, -146, -179,
+ -121, -175, -129, -135, -92, -156, -126, -159, -68, -176, -108,
+ -129, -91, -133, -75, -91, -67, -116, -63, -52, -31, -122,
+ -11, -115, 3, -129, 15, -110, -22, -119, -11, -83, -62,
+ -34, -39, -48, -27, -43, -19, -79, 23, -48, -1, -86,
+ 40, -64, 16, -37, 23, -6, -6, 9, 18, 18, 23,
+ 24, 45, 17, 82, 40, 87, 28, 105, 40, 116, 77,
+ 81, 80, 85},
+ /* IRC_Composite_C_R0195_T090_P330.wav */
+ {6, -4, 3, -1, 0, 3, -5, 8, -11, 16,
+ -21, 29, -39, 57, -101, -332, 89, -835, -16, 8,
+ -1022, -629, -474, 1050, 924, -984, 2683, 3640, -2207, -5863,
+ 2261, -3476, 15168, 2953, -3266, 10727, 14390, 3174, -8455, -1966,
+ 1449, 7129, 3075, -3700, -2692, 1997, 423, 1234, -1087, 216,
+ -471, 1061, -1107, 458, -1402, 142, 319, -88, -425, 76,
+ -1043, 226, -230, 12, -536, -207, -663, 129, -455, 253,
+ -222, -284, -472, 86, -636, 265, -368, -261, -539, -160,
+ -407, 43, -778, -256, -683, -475, -586, -306, -628, -455,
+ -647, -525, -513, -416, -500, -430, -812, -419, -489, -274,
+ -452, -386, -597, -258, -363, -159, -236, -327, -241, -259,
+ -214, -139, -145, -206, -216, -123, -79, -29, -96, -65,
+ -217, -263, -177, -100, -202, -186, -272, -223, -240, -138,
+ -180, -221, -352, -224, -206, -195, -213, -254, -233, -244,
+ -227, -156, -224, -178, -249, -190, -199, -133, -154, -164,
+ -183, -191, -143, -185, -101, -190, -166, -201, -98, -150,
+ -133, -119, -165, -142, -178, -120, -115, -180, -149, -197,
+ -152, -187, -75, -218, -143, -264, -128, -205, -139, -221,
+ -148, -269, -159, -215, -126, -214, -123, -256, -105, -233,
+ -71, -203, -97, -215, -79, -182, -53, -120, -82, -122,
+ -49, -119, 20, -120, 24, -141, 6, -133, 24, -115,
+ 14, -116, -39, -71, -8, -71, 6, -93, -11, -108,
+ 40, -59, 43, -147, 22, -98, 28, -64, 17, -87,
+ -13, -67, 63, -36, 44, -53, 62, -77, 123, -52,
+ 145, -81, 82, -33, 141, -4, 116, 8, 98, 14,
+ 137, 56, 139, -7, 138, 4},
+ /* IRC_Composite_C_R0195_T105_P330.wav */
+ {61, -61, 61, -61, 62, -62, 62, -62, 63, -63, 63,
+ -64, 64, -65, 67, -301, 294, -741, -246, -201, 457, -1576,
+ -144, -73, 2001, -1275, 1763, 1153, 6179, -10039, 184, 1999, 2771,
+ 10917, -378, 522, 13686, 11704, -7376, -5609, 1748, 7382, 4547, -1426,
+ -5565, -24, 2068, 630, -441, -1191, 186, 450, -243, 220, -909,
+ -111, -78, 770, -727, 124, -899, -156, -79, 202, -685, -336,
+ -650, -314, -474, -53, -350, -261, -756, 191, -463, 128, -417,
+ -139, -484, -53, -226, -113, -375, -280, -491, -342, -457, -195,
+ -589, -428, -623, -328, -572, -393, -551, -458, -577, -399, -453,
+ -401, -526, -392, -496, -278, -442, -319, -518, -259, -377, -158,
+ -346, -171, -366, -187, -308, -53, -156, -44, -233, -111, -203,
+ -9, -156, -124, -286, -249, -248, -172, -147, -210, -273, -296,
+ -215, -151, -154, -166, -209, -170, -207, -76, -118, -174, -253,
+ -208, -154, -172, -145, -172, -242, -211, -169, -70, -176, -103,
+ -198, -93, -133, -36, -100, -118, -191, -149, -134, -127, -138,
+ -220, -229, -254, -179, -191, -192, -230, -211, -235, -161, -158,
+ -161, -230, -195, -206, -174, -165, -144, -216, -190, -173, -127,
+ -136, -156, -157, -179, -150, -128, -122, -138, -124, -152, -83,
+ -109, -30, -73, -66, -103, -55, -77, -32, -75, -113, -125,
+ -86, -47, -60, -39, -89, -29, -60, 20, -18, -21, -32,
+ -8, -50, 24, -43, -1, -69, -6, -57, 11, -53, 21,
+ -58, -2, -13, 35, -10, 30, 5, 44, 12, 60, 31,
+ 79, -4, 79, 46, 97, 47, 97, 36, 128, 43, 132,
+ 30, 81, 38},
+ /* IRC_Composite_C_R0195_T120_P330.wav */
+ {1, 4, -8, 14, -20, 27, -35, 44, -54, 66, -79,
+ 95, -114, 135, -159, 177, -87, 615, -830, 163, -909, 1304,
+ -1626, -325, 297, 1206, 69, -817, 4405, 1017, -2560, -616, -3770,
+ 4387, 14092, -5772, 3858, 10124, 9882, -2074, -2959, -3337, 7410, 7437,
+ -1780, -6636, -2212, 2639, 1700, -1078, -1371, -262, 1153, -566, 417,
+ -320, 27, -39, -111, 485, -441, -792, -288, -146, -108, -616,
+ -333, -464, -419, -367, -303, -262, -308, -516, -133, -270, -200,
+ -382, -324, -225, -347, -205, -250, -254, -201, -400, -249, -330,
+ -242, -364, -366, -273, -444, -377, -352, -336, -480, -405, -375,
+ -496, -436, -362, -461, -436, -395, -354, -445, -362, -274, -397,
+ -353, -222, -328, -290, -351, -239, -329, -117, -69, -109, -182,
+ -35, -73, -135, -239, -228, -363, -234, -203, -243, -333, -218,
+ -233, -231, -229, -116, -190, -172, -177, -158, -144, -144, -172,
+ -173, -215, -124, -180, -106, -203, -150, -185, -121, -123, -149,
+ -106, -93, -132, -84, -116, -33, -138, -72, -186, -81, -187,
+ -113, -205, -194, -255, -235, -238, -195, -263, -202, -264, -198,
+ -268, -158, -241, -175, -267, -183, -249, -160, -199, -167, -203,
+ -153, -184, -118, -137, -101, -164, -93, -143, -66, -128, -30,
+ -127, -42, -113, -31, -78, -23, -59, -78, -78, -48, -79,
+ -53, -70, -40, -106, -22, -46, -9, -51, -12, -37, -31,
+ -54, -23, -49, -43, -68, -43, -55, -39, -14, -29, -16,
+ -13, -8, 32, 0, 28, 5, 31, 33, 68, 33, 69,
+ 42, 71, 38, 89, 64, 82, 64, 81, 94, 57, 103,
+ 62, 51, 87},
+ /* IRC_Composite_C_R0195_T135_P330.wav */
+ {13, -11, 8, -5, 2, 2, -7, 12, -18, 26, -34,
+ 45, -58, 75, -96, 126, -173, 263, -756, 239, -382, 327,
+ -504, 374, -988, 304, -762, 1963, 32, 1243, 612, 772, 3408,
+ -6680, -2100, 10509, 4890, -1044, 6633, 2426, 10000, 1888, -3554, -3952,
+ 7154, 5846, -3202, -5792, -2133, 1379, 2382, -1044, -683, -846, 1056,
+ -24, 511, 18, 303, -710, -25, -39, -85, -861, -206, -321,
+ -34, -428, -245, -428, -30, -639, -166, -459, -198, -463, -354,
+ -339, -255, -329, -348, -224, -286, -255, -287, -204, -181, -232,
+ -210, -296, -199, -313, -262, -347, -268, -316, -307, -365, -310,
+ -360, -308, -316, -324, -335, -371, -355, -377, -361, -310, -401,
+ -283, -402, -244, -368, -238, -331, -274, -247, -165, -162, -178,
+ -185, -197, -205, -193, -250, -203, -288, -187, -284, -187, -262,
+ -183, -219, -206, -232, -196, -178, -177, -216, -242, -166, -214,
+ -147, -219, -161, -195, -148, -178, -168, -190, -170, -141, -148,
+ -164, -119, -116, -35, -121, -54, -95, -56, -120, -107, -112,
+ -149, -152, -172, -185, -195, -227, -175, -253, -211, -280, -223,
+ -250, -252, -243, -251, -235, -239, -213, -184, -218, -176, -225,
+ -139, -205, -123, -184, -114, -154, -113, -98, -100, -89, -89,
+ -91, -67, -61, -32, -65, -41, -68, -21, -70, -18, -87,
+ -13, -97, -10, -79, -5, -41, -42, -23, -67, 8, -74,
+ 15, -75, -43, -63, -41, -18, -81, -7, -69, 6, -49,
+ 2, -9, -7, 21, -8, 24, -11, 44, 32, 44, 38,
+ 36, 66, 51, 91, 52, 103, 44, 90, 55, 87, 41,
+ 60, 28, 49},
+ /* IRC_Composite_C_R0195_T150_P330.wav */
+ {15, -18, 20, -22, 25, -28, 31, -35, 38, -42, 47,
+ -52, 57, -63, 70, -76, 84, -91, 97, -95, 51, -395,
+ 348, -546, 384, -515, 230, -347, 78, 797, 314, 1274, 475,
+ 2177, 1227, -4216, -2348, 10076, 258, 3084, 3609, 6149, 5472, 3566,
+ -4049, -1019, 5212, 3739, -5321, -3637, -1580, 2270, 1066, -633, -1395,
+ -136, 1340, -32, 75, -243, 507, -628, 158, 108, -226, -702,
+ -291, -112, -126, -404, -330, -254, -212, -362, -151, -436, -14,
+ -424, -215, -583, -44, -508, -144, -379, -62, -360, -158, -295,
+ -27, -305, -69, -339, -66, -367, -167, -326, -112, -361, -177,
+ -468, -151, -402, -103, -407, -145, -397, -208, -387, -142, -366,
+ -207, -368, -232, -411, -224, -349, -159, -358, -152, -366, -101,
+ -313, -105, -307, -196, -327, -268, -341, -250, -319, -245, -350,
+ -228, -325, -151, -267, -108, -294, -109, -251, -84, -251, -104,
+ -207, -91, -253, -103, -244, -91, -252, -85, -272, -130, -311,
+ -129, -241, -94, -173, -90, -147, -80, -140, -35, -126, -79,
+ -186, -118, -197, -120, -210, -155, -272, -176, -269, -156, -268,
+ -151, -277, -170, -275, -153, -283, -181, -265, -186, -257, -194,
+ -217, -154, -209, -128, -189, -106, -201, -80, -156, -34, -154,
+ -17, -151, 12, -97, 27, -70, 9, -39, -39, -3, -46,
+ -29, -50, -61, -28, -93, -9, -89, -8, -103, 1, -74,
+ -1, -70, -9, -35, -44, -51, -51, -29, -31, -30, -13,
+ -35, 10, 8, 7, -4, 11, 13, 35, 12, 38, 37,
+ 49, 36, 56, 47, 69, 81, 52, 69, 15, 90, 11,
+ 52, -17, 56},
+ /* IRC_Composite_C_R0195_T165_P330.wav */
+ {-38, 38, -39, 39, -39, 40, -40, 41, -42, 43, -44,
+ 46, -47, 49, -52, 55, -59, 64, -70, 79, -90, 108,
+ -143, 381, -197, 374, -312, 378, -424, 566, -428, 1134, 331,
+ 1309, 501, 1274, 2077, -920, -3821, 4175, 5452, 953, 5098, 2533,
+ 5399, 3026, 943, -4554, 1848, 1742, -912, -4176, -875, -267, 1882,
+ -418, -1283, -863, 1021, 217, -28, -198, 445, -494, -277, -183,
+ 13, -551, -347, -206, -115, -218, -241, -334, -209, -134, -196,
+ -318, -365, -189, -242, -376, -315, -159, -285, -247, -217, -119,
+ -295, -192, -233, -63, -298, -72, -221, -78, -309, -142, -336,
+ -207, -384, -220, -389, -223, -371, -217, -392, -165, -378, -175,
+ -375, -156, -391, -200, -346, -196, -386, -169, -293, -171, -325,
+ -127, -312, -180, -413, -244, -413, -275, -437, -268, -434, -258,
+ -381, -194, -377, -183, -352, -126, -339, -126, -286, -92, -262,
+ -50, -229, -59, -226, -70, -231, -92, -226, -106, -251, -108,
+ -219, -79, -217, -73, -199, -48, -168, -54, -223, -113, -224,
+ -121, -228, -134, -243, -180, -267, -148, -265, -176, -290, -166,
+ -274, -160, -279, -151, -277, -159, -262, -146, -233, -125, -202,
+ -129, -198, -90, -162, -74, -165, -69, -153, -33, -141, -13,
+ -136, 2, -114, 40, -123, 47, -102, 43, -95, 48, -108,
+ 50, -112, 44, -100, 32, -92, 18, -95, 22, -82, 17,
+ -82, 23, -102, 19, -84, 22, -128, 24, -87, 44, -69,
+ 35, -66, 63, -29, 79, -26, 54, -19, 109, -9, 110,
+ -5, 137, -8, 111, 4, 120, 34, 92, -13, 96, -16,
+ 93, -54, 92},
+ /* IRC_Composite_C_R0195_T180_P330.wav */
+ {-26, 26, -26, 25, -25, 25, -24, 23, -23, 22, -21,
+ 20, -19, 17, -16, 14, -11, 8, -4, -1, 7, -15,
+ 27, -46, 89, 61, -145, 147, -71, 284, -270, 217, -379,
+ 526, 177, 1579, 111, 1304, -624, 3003, -1570, -1854, 2371, 5215,
+ 2766, 3717, 2594, 2071, 4289, 53, -1269, -2397, 2282, -1207, -426,
+ -1536, -96, 41, 749, -948, -416, 181, 838, -208, 144, -15,
+ -109, -323, 76, 15, 16, -276, 4, -8, -1, -123, -108,
+ -209, -91, -130, 71, -58, -162, -230, -119, -177, -166, -207,
+ -161, -106, -120, -36, -71, -88, -115, -141, -23, -102, -161,
+ -225, -242, -194, -306, -250, -347, -260, -382, -276, -340, -235,
+ -319, -231, -283, -196, -267, -199, -293, -242, -296, -261, -227,
+ -164, -163, -202, -268, -256, -325, -304, -440, -389, -451, -306,
+ -367, -292, -374, -282, -281, -248, -233, -251, -211, -225, -190,
+ -161, -223, -149, -241, -144, -229, -124, -173, -155, -158, -194,
+ -95, -175, -83, -151, -123, -82, -138, -35, -195, -70, -203,
+ -140, -178, -222, -181, -287, -200, -285, -259, -281, -289, -246,
+ -302, -246, -260, -227, -227, -242, -189, -224, -180, -220, -169,
+ -184, -146, -121, -121, -86, -121, -59, -86, -47, -78, -63,
+ -71, -90, -66, -82, -53, -82, -82, -54, -72, -16, -90,
+ -24, -88, -44, -31, -45, -32, -70, -43, -25, -59, -33,
+ -76, -24, -59, -46, -55, -45, -56, -56, -64, -32, -54,
+ -6, -57, -1, -22, 23, 2, 9, -2, 35, 54, 34,
+ 72, 22, 84, 15, 94, 10, 75, 34, 67, 47, 21,
+ 57, 14, 36},
+ /* IRC_Composite_C_R0195_T195_P330.wav */
+ {19, -19, 19, -20, 20, -21, 21, -22, 23, -23, 24, -24,
+ 25, -26, 26, -27, 27, -28, 29, -29, 30, -30, 30, -29,
+ 28, -25, 18, 14, 179, 64, -45, 11, 282, 14, -136, -276,
+ 1166, 711, 738, -60, 1508, 162, 166, 165, 449, 1532, 5355, 3392,
+ 931, 2171, 2302, 2446, -1456, -644, -2020, 1267, 216, -603, -1662, -126,
+ 427, -150, -158, 310, 15, 459, -99, 240, 14, 137, -140, 123,
+ 54, 92, -77, 39, 37, 33, -11, 20, 88, 136, -9, 51,
+ -50, -40, -148, -156, -244, -108, -193, -138, -117, -78, -113, -124,
+ -103, -88, -193, -190, -214, -191, -267, -312, -314, -321, -363, -318,
+ -371, -313, -329, -304, -279, -293, -244, -274, -270, -284, -268, -270,
+ -226, -229, -218, -220, -244, -283, -323, -321, -343, -387, -412, -395,
+ -369, -338, -385, -339, -361, -243, -279, -233, -263, -196, -226, -166,
+ -214, -179, -210, -188, -205, -188, -194, -186, -176, -189, -187, -188,
+ -151, -148, -132, -148, -138, -144, -161, -155, -165, -177, -165, -222,
+ -217, -269, -237, -294, -251, -318, -235, -297, -234, -275, -203, -229,
+ -195, -221, -176, -219, -151, -204, -120, -189, -136, -166, -97, -148,
+ -79, -121, -68, -94, -95, -78, -72, -67, -80, -75, -79, -69,
+ -70, -61, -76, -83, -71, -84, -52, -102, -40, -87, -65, -74,
+ -59, -59, -44, -53, -35, -32, -38, -24, -30, -33, -41, -31,
+ -32, -32, -53, -10, -53, 7, -39, 3, 11, -3, 30, 29,
+ 44, 32, 63, 46, 79, 39, 81, 48, 51, 38, 73, 36,
+ 50, 22, 77, -11},
+ /* IRC_Composite_C_R0195_T210_P330.wav */
+ {4, -4, 4, -4, 4, -4, 4, -4, 4, -4, 4, -4,
+ 4, -4, 4, -4, 4, -4, 4, -3, 3, -3, 2, -2,
+ 1, 0, -1, 4, -7, 14, -37, -66, 67, -14, -33, -186,
+ 91, -96, 90, 124, 958, 676, 187, 107, -283, 310, 1299, 1265,
+ 1017, 1358, 3762, 3924, 898, 885, 85, 1188, 235, 180, -1266, -91,
+ 662, -183, -222, -323, 535, 278, 681, 120, 382, 366, 409, 299,
+ 413, 216, 314, 49, 299, 205, 244, 15, 349, 134, 369, -25,
+ 355, 37, 155, -122, 149, -90, 17, -126, 0, -119, -101, -98,
+ -45, -101, -108, -137, -41, -165, -128, -190, -90, -220, -248, -213,
+ -218, -183, -289, -234, -287, -213, -287, -181, -291, -252, -334, -216,
+ -249, -215, -305, -231, -259, -234, -279, -267, -321, -303, -381, -272,
+ -334, -278, -372, -241, -327, -217, -356, -191, -320, -190, -311, -163,
+ -271, -146, -273, -141, -249, -181, -245, -176, -249, -177, -262, -171,
+ -258, -201, -269, -174, -249, -165, -249, -157, -236, -157, -222, -150,
+ -201, -185, -232, -186, -231, -212, -272, -199, -268, -197, -269, -185,
+ -248, -203, -236, -188, -226, -202, -206, -202, -200, -198, -175, -174,
+ -139, -154, -129, -144, -119, -116, -115, -116, -101, -118, -88, -110,
+ -115, -124, -128, -104, -139, -117, -161, -110, -128, -96, -116, -100,
+ -91, -78, -46, -54, -38, -54, -28, -28, -31, -22, -47, -22,
+ -45, -45, -34, -47, -26, -63, -19, -69, 15, -46, 17, -34,
+ 36, -7, 28, 11, 12, 21, 18, 27, 33, 12, 47, 14,
+ 27, 2, 38, -2},
+ /* IRC_Composite_C_R0195_T225_P330.wav */
+ {-1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1,
+ -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1,
+ -1, 1, -1, 1, -1, 1, 2, -68, -59, -5, -96, 48,
+ -93, -175, -114, 61, 113, 32, 173, 565, 369, -5, 6, 42,
+ 629, 908, 1232, 1079, 1509, 2842, 2279, 709, 505, 658, 652, 456,
+ 31, -132, 16, 714, 22, 392, 243, 724, 581, 610, 769, 414,
+ 780, 295, 589, 257, 440, 139, 342, 210, 326, 138, 213, 205,
+ 269, 65, 206, 39, 192, -75, 77, -72, 119, -93, 32, -98,
+ -25, -124, -61, -115, -124, -100, -152, -32, -169, -66, -171, -60,
+ -174, -142, -204, -153, -209, -250, -258, -282, -284, -268, -319, -270,
+ -333, -243, -293, -243, -293, -258, -255, -244, -253, -268, -246, -247,
+ -251, -255, -254, -239, -252, -237, -244, -241, -235, -232, -243, -208,
+ -257, -193, -268, -204, -268, -227, -248, -235, -265, -257, -230, -233,
+ -218, -240, -222, -215, -193, -210, -173, -175, -183, -176, -191, -180,
+ -202, -196, -223, -223, -252, -229, -258, -254, -257, -250, -252, -220,
+ -262, -227, -250, -215, -243, -223, -230, -208, -202, -158, -170, -134,
+ -160, -123, -166, -123, -164, -100, -174, -109, -180, -112, -164, -109,
+ -168, -131, -171, -106, -158, -104, -162, -121, -153, -114, -132, -112,
+ -125, -95, -82, -74, -52, -73, -50, -54, -44, -32, -38, -20,
+ -57, -17, -36, -14, -46, -36, -11, -10, -23, -34, -7, -16,
+ 7, -20, -30, -21, -13, -19, -3, -32, -10, -25, 9, -16,
+ 17, 18, -2, 23},
+ /* IRC_Composite_C_R0195_T240_P330.wav */
+ {6, -6, 6, -6, 6, -6, 6, -6, 7, -7, 7, -7,
+ 7, -7, 7, -7, 7, -7, 7, -7, 8, -8, 8, -8,
+ 8, -8, 8, -8, 1, -47, -37, -124, -93, -95, -58, -38,
+ -70, -58, -105, -121, -151, -76, 14, 175, 104, 51, 147, 291,
+ 191, -40, 158, 429, 1090, 1311, 1373, 1241, 1326, 1526, 1181, 419,
+ 583, 828, 657, 621, 656, 466, 551, 572, 739, 747, 685, 612,
+ 423, 667, 583, 601, 619, 480, 483, 323, 428, 299, 276, 192,
+ 162, 105, 83, 89, 89, 51, 98, -46, 97, -2, 117, -65,
+ 34, -69, 1, -69, -69, -127, -170, -131, -173, -121, -145, -147,
+ -134, -204, -102, -232, -83, -273, -123, -325, -159, -309, -183, -275,
+ -249, -249, -252, -185, -266, -178, -245, -201, -259, -208, -248, -243,
+ -270, -238, -253, -227, -254, -209, -249, -203, -269, -184, -271, -141,
+ -258, -121, -230, -113, -188, -120, -198, -167, -178, -200, -214, -242,
+ -233, -264, -244, -276, -244, -267, -267, -261, -267, -239, -274, -233,
+ -272, -226, -277, -260, -268, -247, -282, -253, -289, -226, -270, -213,
+ -255, -189, -247, -158, -192, -146, -173, -170, -171, -177, -143, -184,
+ -151, -187, -146, -147, -141, -154, -121, -150, -139, -166, -139, -155,
+ -135, -176, -141, -174, -144, -174, -160, -177, -162, -156, -152, -157,
+ -114, -126, -89, -103, -86, -98, -78, -68, -62, -53, -84, -35,
+ -53, -7, -51, -15, -38, -6, -34, -13, -34, -15, -32, -28,
+ -35, -19, -32, 1, -19, 7, -27, 9, 3, 4, -14, 3,
+ -5, 36, 2, 41},
+ /* IRC_Composite_C_R0195_T255_P330.wav */
+ {-1, 1, -1, 1, -1, 1, -2, 2, -2, 2, -2, 2,
+ -2, 2, -2, 2, -2, 3, -3, 3, -3, 3, -3, 4,
+ -4, 4, -5, 5, -6, 7, -8, 10, -13, 22, -83, -97,
+ -106, -152, -99, -59, -90, -92, -128, -74, -117, -122, -12, 68,
+ 86, 137, 156, 199, 190, 329, 533, 767, 1036, 1233, 1574, 1651,
+ 1491, 1169, 954, 781, 906, 1168, 950, 370, 704, 786, 629, 605,
+ 494, 653, 561, 698, 550, 738, 501, 619, 385, 441, 218, 218,
+ 189, 132, 100, 39, 147, 27, 91, -38, 77, -24, 27, -36,
+ -30, -17, -41, -95, -49, -122, -68, -169, -78, -186, -103, -225,
+ -135, -197, -120, -153, -145, -183, -142, -167, -150, -231, -159, -244,
+ -153, -214, -165, -219, -193, -224, -173, -215, -157, -238, -162, -231,
+ -149, -238, -155, -243, -185, -203, -182, -175, -206, -184, -214, -165,
+ -184, -162, -168, -165, -138, -160, -133, -192, -158, -236, -216, -272,
+ -273, -290, -295, -306, -316, -279, -301, -261, -303, -259, -299, -287,
+ -289, -292, -273, -302, -247, -283, -207, -253, -202, -230, -201, -189,
+ -205, -172, -192, -162, -193, -165, -169, -166, -191, -174, -179, -167,
+ -155, -172, -157, -151, -142, -148, -140, -156, -161, -162, -168, -167,
+ -172, -156, -177, -164, -174, -152, -168, -156, -171, -153, -173, -152,
+ -171, -139, -162, -124, -144, -85, -111, -53, -108, -40, -71, -33,
+ -75, -21, -62, -20, -68, 0, -50, 5, -37, -3, -30, 28,
+ -23, 3, -23, 12, -19, 28, -12, 24, -5, 41, 17, 24,
+ -7, 17, -24, 23},
+ /* IRC_Composite_C_R0195_T270_P330.wav */
+ {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 1, -1, 1, -1, 1, -1, 1, -2, 3,
+ -127, -152, -188, -165, -160, -178, -195, -131, -72, -37, -53, -57,
+ -34, 48, 106, 135, 127, 188, 385, 765, 884, 1201, 1389, 1575,
+ 1756, 1541, 1264, 1282, 908, 615, 982, 1053, 498, 403, 667, 706,
+ 647, 681, 663, 732, 712, 700, 422, 527, 505, 426, 401, 192,
+ 156, 184, 190, 100, 26, -34, 55, -50, 19, -119, 28, -59,
+ -54, -94, -103, -24, -139, -24, -165, -13, -154, -5, -159, -81,
+ -104, -118, -99, -136, -98, -118, -106, -89, -142, -101, -168, -112,
+ -178, -178, -148, -180, -114, -217, -134, -255, -104, -252, -140, -238,
+ -148, -235, -187, -236, -202, -242, -197, -183, -140, -137, -108, -143,
+ -129, -142, -128, -211, -166, -246, -182, -269, -212, -271, -233, -292,
+ -267, -287, -263, -282, -269, -312, -249, -304, -257, -293, -240, -287,
+ -230, -257, -242, -248, -234, -224, -236, -219, -241, -188, -227, -176,
+ -221, -206, -210, -185, -185, -166, -179, -163, -176, -164, -168, -164,
+ -186, -168, -180, -154, -159, -141, -142, -150, -142, -147, -152, -145,
+ -159, -150, -168, -167, -187, -177, -186, -176, -192, -163, -197, -167,
+ -186, -164, -170, -159, -148, -137, -112, -102, -104, -88, -70, -56,
+ -50, -28, -30, -11, -12, -16, -10, -21, -31, -4, -26, 2,
+ 15, 8, 10, -3, -2, 5, 10, 12, -9, 0, -5, 9,
+ -13, -19, -29, -16},
+ /* IRC_Composite_C_R0195_T285_P330.wav */
+ {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 1, -1, 1, -1, 1,
+ -1, 1, -1, 2, -2, 3, -6, 13, -111, -184, -215, -184,
+ -140, -139, -179, -220, -168, -247, -124, 1, -91, -84, 42, -80,
+ 17, -54, 56, 218, 634, 881, 967, 1181, 1587, 1636, 1273, 1643,
+ 1650, 702, 357, 915, 823, 759, 711, 555, 754, 615, 798, 685,
+ 720, 707, 793, 638, 630, 510, 595, 460, 429, 211, 188, 178,
+ 148, 102, 16, 162, 0, 142, -40, 140, -32, 103, 24, -7,
+ -30, 11, -24, -1, -87, 12, -168, 51, -103, 64, -105, 39,
+ -118, 10, -91, -91, -119, -125, -90, -149, -75, -130, -138, -153,
+ -195, -185, -254, -205, -313, -223, -303, -243, -266, -224, -223, -230,
+ -188, -216, -155, -167, -129, -140, -137, -102, -110, -74, -146, -82,
+ -177, -89, -192, -110, -215, -160, -231, -232, -220, -242, -237, -262,
+ -238, -271, -237, -259, -263, -234, -261, -228, -278, -222, -281, -252,
+ -268, -270, -265, -263, -228, -244, -187, -230, -176, -214, -164, -194,
+ -141, -175, -152, -173, -143, -161, -171, -179, -174, -184, -157, -194,
+ -163, -211, -148, -196, -144, -205, -158, -219, -155, -190, -162, -178,
+ -166, -174, -156, -186, -169, -200, -161, -200, -167, -208, -167, -218,
+ -158, -186, -143, -155, -119, -123, -104, -117, -86, -101, -54, -82,
+ -39, -73, -25, -68, -7, -55, -18, -42, 23, -14, 19, -21,
+ 7, -24, 19, -4, 2, -14, 7, -10, 12, -14, 13, -7,
+ 26, -23, 8, -14},
+ /* IRC_Composite_C_R0195_T300_P330.wav */
+ {-3, 3, -3, 3, -3, 3, -3, 3, -3, 3, -3, 3,
+ -3, 3, -3, 3, -3, 3, -3, 3, -3, 3, -3, 4,
+ -4, 4, -4, 4, -4, 4, -5, -62, -157, -83, -247, -284,
+ -202, -303, -294, -235, -54, -305, 15, 156, 241, -112, -479, -226,
+ 774, -232, 495, 1765, 1236, 1265, 2221, 2705, 1763, 850, 505, 963,
+ 731, 757, 546, 519, 206, 795, 441, 708, 577, 742, 596, 545,
+ 491, 526, 666, 413, 377, 307, 300, 457, 188, 260, 34, 250,
+ 133, 251, 58, 87, 249, 132, 194, -1, 200, 55, 156, 95,
+ 139, 122, 50, 62, 57, 84, -3, -71, -62, -99, -113, -176,
+ -122, -254, -180, -227, -166, -230, -202, -293, -196, -279, -248, -326,
+ -256, -320, -217, -260, -211, -244, -154, -191, -125, -171, -113, -160,
+ -81, -135, -55, -102, -70, -95, -24, -42, -53, -78, -73, -93,
+ -89, -149, -139, -195, -180, -235, -192, -245, -233, -268, -234, -245,
+ -224, -256, -221, -249, -225, -220, -233, -215, -250, -226, -218, -236,
+ -221, -224, -181, -198, -176, -191, -173, -185, -156, -184, -162, -178,
+ -171, -172, -156, -184, -183, -174, -192, -186, -179, -189, -180, -212,
+ -184, -191, -167, -199, -217, -182, -203, -172, -210, -178, -224, -155,
+ -220, -185, -224, -199, -194, -203, -201, -206, -192, -196, -188, -165,
+ -158, -177, -147, -145, -112, -128, -98, -113, -74, -77, -54, -65,
+ -67, -32, -44, -10, -54, -13, -29, -7, -15, -1, -23, 19,
+ -18, 10, -27, 11, -16, 8, 2, 12, -12, 4, -1, 37,
+ 18, 19, 17, 36},
+ /* IRC_Composite_C_R0195_T315_P330.wav */
+ {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, -1, 1, -1, 1, -1, 1, -1, 1, -1,
+ 1, -1, 1, -1, 2, -3, -227, -204, -310, -344, -179, -311,
+ -505, -298, -88, -402, 115, 712, -76, -577, -211, 9, 558, -404,
+ 914, 2627, 1047, 2118, 4245, 2542, 908, 788, 1382, 865, 422, 674,
+ -11, 248, 192, 1314, 191, 817, 366, 966, 306, 467, 192, 289,
+ 334, 100, 222, 7, 227, 236, 315, 254, 209, 305, 479, 300,
+ 240, 335, 445, 260, 199, 112, 202, 84, 42, 63, 85, -45,
+ -100, -80, -73, -157, -181, -120, -209, -167, -259, -161, -262, -234,
+ -281, -209, -243, -208, -221, -245, -173, -195, -197, -241, -151, -201,
+ -166, -240, -132, -182, -104, -150, -85, -104, -75, -148, -59, -141,
+ -36, -144, -33, -133, -71, -89, -7, -47, -109, -70, -108, -44,
+ -152, -113, -164, -122, -182, -216, -170, -212, -172, -263, -155, -228,
+ -178, -252, -176, -207, -200, -242, -206, -220, -204, -208, -195, -218,
+ -188, -193, -156, -217, -159, -200, -119, -211, -133, -181, -131, -177,
+ -169, -158, -166, -171, -195, -175, -181, -214, -193, -224, -184, -218,
+ -204, -219, -183, -222, -197, -222, -197, -226, -215, -204, -205, -207,
+ -216, -212, -210, -224, -222, -213, -210, -205, -202, -194, -174, -155,
+ -148, -134, -144, -126, -89, -101, -75, -94, -84, -69, -47, -37,
+ -69, -67, -26, -35, -13, -29, -17, -23, -3, 13, -18, -8,
+ -26, 22, -30, 20, -15, 30, -13, 8, -11, 23, 49, 30,
+ 39, 22, 36, 16},
+ /* IRC_Composite_C_R0195_T330_P330.wav */
+ {9, -9, 9, -10, 10, -10, 10, -11, 11, -12, 12, -12,
+ 13, -13, 14, -15, 15, -16, 17, -17, 18, -19, 20, -22,
+ 23, -25, 30, -50, -332, -388, -131, -617, -214, -313, -733, -659,
+ 255, -155, -66, 699, -77, -372, -1255, -242, 2041, -707, -6, 4736,
+ 2036, 2809, 3545, 4509, 22, -72, 1207, 1973, -173, 508, 959, 302,
+ 542, 540, 656, 123, 461, 665, 400, 314, 251, 504, 190, 265,
+ 35, 373, 467, 91, 154, 128, 447, 203, 170, 90, 232, 80,
+ 149, 193, 21, -6, -90, -101, -59, -80, -255, -144, -154, -358,
+ -29, -238, -96, -237, -126, -195, 15, -288, -107, -147, -89, -276,
+ -120, -223, -134, -174, -181, -152, -158, -179, -71, -138, -116, -177,
+ -93, -189, -60, -206, -162, -159, -113, -111, -150, -123, -93, -106,
+ -83, -101, -52, -113, -43, -99, -10, -99, -66, -88, -58, -127,
+ -64, -152, -99, -152, -144, -200, -127, -259, -116, -227, -129, -245,
+ -98, -258, -94, -239, -152, -227, -154, -225, -147, -242, -169, -202,
+ -172, -212, -140, -232, -141, -182, -139, -168, -130, -205, -127, -137,
+ -140, -176, -175, -160, -166, -171, -205, -188, -193, -212, -199, -238,
+ -185, -248, -173, -246, -186, -227, -206, -245, -184, -218, -210, -235,
+ -210, -228, -175, -238, -206, -213, -189, -196, -171, -200, -159, -123,
+ -146, -114, -128, -128, -81, -88, -83, -80, -52, -124, -70, -58,
+ -40, -45, -69, -50, -10, -13, -16, -42, -4, -25, 50, -37,
+ 11, -37, 37, -4, 24, -11, 12, -7, 33, 10, 69, 29,
+ 60, 20, 87, -1},
+ /* IRC_Composite_C_R0195_T345_P330.wav */
+ {1, 0, 0, 0, -1, 1, -2, 2, -3, 4, -5, 6,
+ -7, 8, -9, 11, -13, 15, -17, 20, -24, 28, -33, 40,
+ -47, 52, -211, -235, -688, -550, -297, -690, -704, -444, 713, -1568,
+ 989, 470, -39, -1205, 323, -714, 276, -2380, 5748, 2929, 602, 8080,
+ 5302, -453, -722, 3812, 715, -74, -365, 1626, 369, 1163, 1544, 781,
+ -4, 229, 668, 441, 364, 676, 214, 441, 36, 409, 174, 565,
+ -210, 423, -90, 172, -132, 46, -99, 78, -186, 159, -181, 96,
+ -244, 13, -280, -20, -156, -41, -291, -109, -365, -55, -322, -117,
+ -274, -136, -169, -159, -189, -140, -191, 12, -178, 0, -295, -70,
+ -206, -88, -236, -75, -169, -78, -191, -95, -204, -87, -146, -66,
+ -185, -107, -238, -113, -249, -119, -185, -98, -187, -131, -176, -95,
+ -137, -31, -142, -17, -163, -50, -120, -40, -114, -19, -100, -43,
+ -142, -64, -163, -81, -167, -107, -184, -140, -220, -122, -200, -146,
+ -203, -155, -165, -178, -177, -198, -166, -175, -153, -160, -161, -178,
+ -163, -171, -143, -138, -136, -139, -146, -131, -151, -132, -167, -143,
+ -153, -146, -165, -157, -219, -159, -215, -151, -222, -190, -225, -204,
+ -218, -208, -214, -189, -218, -191, -221, -190, -214, -185, -216, -196,
+ -215, -178, -200, -217, -218, -194, -162, -182, -170, -189, -185, -152,
+ -157, -123, -144, -122, -111, -99, -99, -89, -76, -63, -81, -56,
+ -61, -31, -49, -85, -23, -30, -14, -9, -21, -45, -18, -1,
+ 16, 3, 5, -5, 5, 8, -5, 64, 39, 55, 37, 73,
+ 42, 56, 31, 84}};
+
+const int16_t irc_composite_c_r0195_p345[][256] =
+ {/* IRC_Composite_C_R0195_T000_P345.wav */
+ {2, -1, 1, -1, 1, -1, 1, -1, 0, 0, 0,
+ 0, -1, 1, -2, 2, -3, 4, -5, 7, -9, 11,
+ -15, 21, -203, -88, -927, -196, -604, -22, -2141, -496, -650,
+ 1585, -548, 803, -263, 412, -5131, 5346, -2103, -7267, 12288, 1672,
+ 1403, 10338, 7456, -3182, -874, 2680, 1965, -5296, 1356, 1865, 68,
+ -578, 2523, 1092, 1554, 2056, 2530, 1058, 1432, 379, 1062, -82,
+ -380, -971, 369, -376, 62, -56, 669, -390, 190, 379, 537,
+ 98, -46, -35, 315, 348, 110, 183, 491, -38, 19, 211,
+ 170, -22, -231, -125, -221, 138, -512, -336, -126, 10, -387,
+ -311, -283, -176, -241, -345, -432, -207, -330, -489, -386, -392,
+ -351, -423, -274, -457, -352, -361, -311, -213, -391, -323, -302,
+ -143, -325, -378, -206, -229, -190, -319, -334, -196, -236, -126,
+ -387, -117, -339, -105, -264, -135, -207, -172, -176, -254, -160,
+ -183, -177, -200, -218, -201, -189, -116, -205, -120, -224, -113,
+ -159, -119, -241, -118, -127, -119, -188, -91, -119, -15, -186,
+ -30, -132, 28, -213, -19, -166, 21, -190, -46, -185, -49,
+ -159, -65, -168, -65, -190, -59, -218, -46, -217, -24, -231,
+ -74, -204, -52, -181, -89, -211, -94, -196, -70, -219, -104,
+ -200, -55, -181, -86, -186, -39, -155, -66, -178, -1, -187,
+ -47, -188, 6, -142, -11, -161, -5, -114, -9, -113, 6,
+ -145, -7, -123, -21, -161, -34, -116, -47, -173, -82, -145,
+ -76, -157, -72, -137, -75, -186, -79, -163, -31, -131, -17,
+ -156, -51, -99, 26, -74, -5, -101, 37, -82, 48, -83,
+ 35, -130, 20},
+ /* IRC_Composite_C_R0195_T015_P345.wav */
+ {-8, 8, -9, 9, -9, 10, -10, 11, -11, 12, -13,
+ 14, -15, 17, -19, 21, -23, 26, -30, 34, -31, -453,
+ -1036, -33, -665, 328, -1202, -749, -1695, -16, 154, 1160, 807,
+ 430, -250, 202, -9659, 9217, 1210, -9352, 17225, 6331, 1506, 7124,
+ 7939, -4308, -4730, 1188, 3176, -2475, 1424, -48, -469, 790, 3944,
+ 1309, 680, 1752, 2484, 1048, 341, -282, 654, -429, -824, -317,
+ 110, -114, -77, 514, 87, -336, 314, 152, 794, -136, 169,
+ -632, 188, -172, 280, -21, -59, -628, -366, -701, 125, -342,
+ 195, -484, 14, -385, 142, -203, -25, -63, -111, -438, -237,
+ -144, -19, -603, -408, -283, -225, -622, -635, -157, -385, -331,
+ -577, -30, -389, -404, -427, -106, -226, -443, -405, -162, -392,
+ -396, -469, 17, -462, -322, -529, -39, -465, -308, -396, -19,
+ -390, -307, -364, 11, -326, -213, -317, -16, -354, -213, -313,
+ -60, -343, -187, -297, -56, -344, -144, -249, -9, -231, -112,
+ -162, -24, -173, -102, -136, 27, -150, -25, -210, 56, -219,
+ 64, -254, 58, -220, 23, -244, -12, -203, -8, -244, -39,
+ -226, -35, -259, -51, -231, -34, -263, -76, -247, -60, -275,
+ -44, -227, -27, -295, -46, -242, -22, -251, -17, -192, -18,
+ -252, -5, -165, 49, -233, 62, -170, 73, -217, 68, -124,
+ 45, -174, 23, -102, 4, -167, 36, -98, 28, -187, 19,
+ -138, 2, -165, -15, -133, -54, -155, -42, -139, -76, -178,
+ -15, -149, -36, -176, 14, -154, -30, -174, 22, -136, -10,
+ -157, 29, -146, 8, -148, 67, -147, 48, -150, 87, -144,
+ 95, -120, 68},
+ /* IRC_Composite_C_R0195_T030_P345.wav */
+ {-5, 4, -2, 1, 1, -3, 5, -8, 11, -14,
+ 18, -22, 28, -33, 40, -47, 52, -40, -408, -1246,
+ -239, -636, 412, -1166, -641, -1047, -627, -510, 85, 1903,
+ 1495, 1980, -2101, -8149, 8335, -7570, 2561, 15248, -2261, 7441,
+ 12350, 8152, -6960, -897, -1312, -80, 35, 3139, -1223, -111,
+ 782, 2694, 1229, 2248, 830, 1733, -78, 616, 211, 239,
+ -1106, 180, -577, 174, -80, 366, -414, -235, 48, 899,
+ -101, -343, 251, 6, -139, -259, 84, -149, -439, -392,
+ -377, -353, -641, -180, -347, -645, -512, 18, -444, -412,
+ -353, -372, 29, -540, -286, -340, 91, -270, 81, -387,
+ -257, -287, -105, -191, -258, -436, 11, -476, -269, -411,
+ -52, -449, -147, -462, -317, -370, -394, -207, -298, -370,
+ -445, -321, -432, -282, -273, -429, -392, -579, -276, -340,
+ -267, -422, -237, -397, -288, -363, -183, -311, -252, -339,
+ -167, -369, -181, -301, -85, -308, -243, -297, -206, -198,
+ -141, -225, -133, -188, -51, -81, -5, -156, 39, -131,
+ -42, -137, -12, -105, -59, -165, -33, -91, -103, -131,
+ -61, -132, -15, -166, -70, -196, -74, -136, -77, -240,
+ -119, -181, -167, -207, -153, -210, -116, -267, -128, -236,
+ -136, -218, -68, -191, -86, -215, -66, -87, -41, -115,
+ -57, -149, 39, -73, 32, -134, 21, -60, 58, -96,
+ 16, -33, 13, -37, -10, -59, 0, -62, -11, -108,
+ -26, -126, 5, -173, -25, -133, -22, -188, -62, -152,
+ -27, -144, -75, -133, -61, -105, -49, -114, -43, -128,
+ -43, -94, -30, -105, -18, -125, -14, -97, 30, -123,
+ 23, -65, 42, -66, 35, -56},
+ /* IRC_Composite_C_R0195_T045_P345.wav */
+ {-8, 9, -9, 10, -11, 12, -14, 15, -17, 18, -20,
+ 23, -26, 29, -32, 27, -914, -992, 248, -1147, 746, -600,
+ -1411, -744, -565, -282, 187, 2409, 2683, -221, -1093, -6934, 4033,
+ -4276, 11276, -297, 4328, 20553, 5592, -1738, -1835, 48, -2700, 3273,
+ 333, -1462, -612, 3497, 663, 1061, 1222, 2492, 754, 1168, -36,
+ 273, -1174, -272, -1054, 891, -844, 132, -415, 315, -117, 257,
+ -335, 440, -662, 37, -141, 164, -657, 106, -514, -245, -612,
+ -181, -587, -119, -354, -272, -370, 70, -460, -182, -631, -316,
+ -708, -110, -803, -99, -670, -498, -427, -54, -498, -454, -369,
+ -295, -361, -311, -286, -279, -189, -173, -337, -51, -132, -118,
+ -321, -115, -360, -197, -296, -134, -359, -223, -370, -154, -278,
+ -322, -354, -297, -436, -355, -372, -287, -465, -352, -358, -269,
+ -478, -297, -336, -244, -461, -251, -336, -244, -377, -295, -277,
+ -241, -357, -246, -212, -210, -283, -149, -224, -96, -233, -70,
+ -163, -39, -261, -32, -98, -58, -219, -32, -96, -67, -134,
+ 7, -101, -50, -139, 18, -115, -50, -164, -70, -125, -101,
+ -200, -71, -185, -130, -181, -136, -176, -132, -203, -147, -173,
+ -138, -199, -159, -176, -127, -141, -96, -163, -71, -143, -38,
+ -126, -27, -77, -59, -103, 5, -43, -32, -25, -36, -5,
+ -42, 40, -34, 18, -45, 1, -59, 7, -47, -28, -49,
+ -55, -63, -33, -55, -79, -59, -82, -109, -94, -40, -81,
+ -89, -121, -82, -83, -49, -111, -62, -79, -97, -59, -52,
+ -74, -97, -50, -98, -31, -116, 11, -85, 32, -106, 43,
+ -88, 79, -70},
+ /* IRC_Composite_C_R0195_T060_P345.wav */
+ {2, 0, -1, 2, -4, 7, -10, 13, -18, 24, -33,
+ 46, -69, 124, -1066, 18, -1103, -58, -112, -921, -201, 357,
+ -2572, -292, 84, 3712, 705, 1846, -3182, 733, -1997, -2694, 2428,
+ -595, 23701, 7754, -2253, 5078, 3945, -1381, 793, -3491, -1163, 3714,
+ 3613, -2506, 296, 680, 1730, 1310, 1740, 569, 508, 99, -722,
+ -392, -453, -871, 66, -328, -669, -71, -484, -147, -35, 146,
+ 49, -532, -397, -139, -139, -522, -373, -116, -571, -415, -445,
+ -69, -274, -275, -429, -422, -271, -360, -287, -382, -22, -407,
+ -385, -428, -388, -334, -422, -213, -696, -500, -450, -310, -488,
+ -360, -442, -369, -341, -387, -436, -315, -312, -227, -524, -284,
+ -360, -145, -448, -103, -329, -255, -335, -170, -226, -169, -275,
+ -186, -277, -175, -313, -153, -328, -106, -340, -187, -359, -196,
+ -395, -186, -410, -204, -401, -242, -445, -212, -399, -269, -333,
+ -214, -369, -275, -249, -157, -224, -209, -240, -93, -196, -87,
+ -134, -136, -170, -118, -141, -152, -108, -143, -171, -122, -121,
+ -75, -167, -118, -171, -34, -161, -63, -166, -73, -182, -54,
+ -115, -102, -170, -130, -176, -78, -151, -106, -187, -132, -212,
+ -115, -143, -110, -184, -158, -155, -100, -103, -72, -126, -49,
+ -120, -26, -63, 20, -83, 6, -50, 25, -17, 26, -29,
+ 39, -31, 8, -37, 34, -77, 20, -77, 4, -80, -26,
+ -93, -19, -70, -86, -71, -70, -82, -94, -51, -114, -61,
+ -80, -78, -73, -90, -68, -70, -42, -80, -51, -78, -111,
+ -16, -79, -18, -107, -18, -114, -3, -27, -1, -36, -19,
+ -21, -10, 39},
+ /* IRC_Composite_C_R0195_T075_P345.wav */
+ {13, -13, 13, -13, 13, -13, 13, -14, 14, -14, 14,
+ -14, 13, -149, -486, -602, -335, -455, -212, -518, 0, -1171,
+ -540, -311, 1579, 3683, 3360, -5772, -426, -1232, -715, 5798, -5995,
+ 21017, 15656, -2523, -3222, 5350, 1893, 1313, -2953, -885, 4326, 1209,
+ -279, -1161, 869, 861, 1767, 953, -85, 210, -139, -663, -17,
+ -553, 268, -673, -215, -760, -236, -646, -139, -393, -427, -462,
+ -55, -562, -131, -287, -193, -567, -273, -625, -345, -491, -150,
+ -487, -111, -512, -283, -603, -50, -418, -328, -443, -111, -346,
+ -355, -349, -35, -305, -248, -516, -491, -547, -144, -443, -470,
+ -467, -394, -484, -216, -365, -370, -505, -259, -440, -318, -386,
+ -196, -476, -376, -474, -304, -362, -195, -334, -380, -413, -292,
+ -310, -213, -289, -263, -333, -155, -255, -179, -252, -180, -265,
+ -153, -285, -190, -264, -147, -236, -237, -304, -248, -194, -182,
+ -204, -186, -250, -173, -172, -144, -229, -114, -172, -167, -183,
+ -136, -161, -106, -122, -161, -186, -139, -156, -91, -182, -139,
+ -216, -134, -193, -139, -180, -115, -198, -155, -201, -90, -175,
+ -115, -207, -160, -200, -119, -149, -119, -195, -93, -130, -96,
+ -135, -84, -130, -121, -106, -85, -107, -61, -110, -53, -77,
+ 2, -45, -15, -51, -20, 4, 33, -23, -40, -36, 1,
+ -53, 16, -46, -46, -69, 1, -46, -34, -47, -35, -46,
+ -45, -73, -11, -76, -35, -107, -21, -105, -57, -70, -68,
+ -101, -73, -74, -59, -95, -48, -78, -69, -89, -82, -50,
+ -61, -44, -75, -23, -52, -44, 10, -43, -23, -47, 63,
+ -13, 31, 19},
+ /* IRC_Composite_C_R0195_T090_P345.wav */
+ {-40, 45, -52, 60, -69, 79, -92, 108, -129, 156, -197,
+ 267, -462, -149, 208, -635, -826, 268, -390, -920, 244, -62,
+ -1260, -320, 2937, 4521, -564, -4829, 2706, -6796, 9962, -6783, 6356,
+ 23745, 4526, -4606, -155, 5413, 3814, -1343, -3207, 4343, 2939, -733,
+ -2665, 998, 738, 1892, 332, -416, -415, 112, -618, 265, -680,
+ -48, -750, 405, -729, 216, -673, -99, -730, -121, -855, -310,
+ -693, -146, -680, -198, -695, -260, -522, -283, -523, -143, -597,
+ -333, -173, -342, -516, -211, -519, -364, -344, -136, -671, -213,
+ -411, -202, -398, -103, -578, -290, -294, -138, -306, -230, -402,
+ -283, -321, -289, -337, -289, -473, -414, -279, -358, -413, -275,
+ -431, -453, -313, -307, -448, -301, -341, -461, -354, -322, -417,
+ -358, -269, -383, -284, -242, -302, -305, -257, -285, -308, -182,
+ -268, -314, -270, -212, -228, -190, -249, -276, -158, -168, -143,
+ -161, -146, -207, -91, -103, -158, -110, -126, -88, -73, -13,
+ -110, -35, -46, -106, -57, -116, -140, -215, -98, -197, -197,
+ -174, -246, -265, -202, -185, -227, -181, -218, -214, -125, -146,
+ -175, -178, -153, -217, -80, -87, -137, -151, -98, -122, -94,
+ -88, -136, -141, -168, -115, -121, -70, -143, -102, -56, -76,
+ -36, 6, -27, -49, 15, 48, -26, 23, -19, 19, -21,
+ 34, -59, 8, -22, -47, 2, -38, -69, -64, -5, -74,
+ -105, -59, -66, -97, -94, -96, -78, -97, -52, -113, -64,
+ -142, -56, -66, -39, -129, -40, -66, -56, -70, 5, -96,
+ -30, -55, 4, -41, 13, -55, 20, -26, 32, -22, 71,
+ -18, 17, 30},
+ /* IRC_Composite_C_R0195_T105_P345.wav */
+ {-67, 70, -74, 77, -81, 86, -90, 96, -101, 108,
+ -115, 124, -133, 69, -792, -387, 271, -596, -717, 859,
+ -864, -953, -174, 2178, -1005, 1282, 2249, 6443, -10856, 1162,
+ -3444, 7735, 12493, -6470, 12789, 13090, -1110, -7098, 1720, 6128,
+ 6522, -3157, -2418, 823, 2649, -1545, -82, 75, 412, 176,
+ -227, -1102, -27, 27, -108, -705, 430, -492, -144, -623,
+ 458, -659, -400, -488, -145, -709, -561, -722, -446, -592,
+ -392, -773, -320, -376, -219, -435, -230, -433, -344, -257,
+ -282, -422, -405, -416, -298, -443, -368, -500, -217, -519,
+ -166, -433, -279, -366, -130, -440, -244, -224, -238, -388,
+ -222, -308, -238, -178, -169, -397, -297, -264, -256, -355,
+ -298, -417, -426, -330, -305, -333, -364, -344, -434, -356,
+ -283, -355, -382, -364, -318, -364, -236, -292, -356, -334,
+ -239, -297, -263, -218, -274, -335, -226, -222, -269, -272,
+ -264, -249, -207, -170, -177, -194, -173, -129, -105, -104,
+ -107, -136, -46, -4, 64, 55, 23, -30, -30, -40,
+ -113, -234, -240, -226, -142, -197, -242, -304, -222, -141,
+ -127, -177, -202, -148, -92, -95, -154, -222, -191, -141,
+ -114, -142, -174, -129, -161, -67, -128, -34, -186, -89,
+ -116, -46, -141, -77, -118, -122, -94, -41, -91, -117,
+ -59, -17, -48, -43, -49, -43, -26, 26, 1, -33,
+ -44, -8, 1, 24, -53, -52, -67, -16, -53, -12,
+ -77, -55, -119, -25, -52, -50, -119, -50, -99, -48,
+ -93, -38, -125, -90, -97, -23, -111, -77, -96, -23,
+ -117, -28, -57, -25, -82, -14, -37, -3, -37, 48,
+ -10, 30, -29, 94, 17, 67},
+ /* IRC_Composite_C_R0195_T120_P345.wav */
+ {-3, 4, -6, 7, -9, 12, -14, 18, -21, 26, -32,
+ 40, -51, 68, -105, -237, -115, -196, -240, -710, 503, -404,
+ -299, -610, 1573, -1052, 1538, 3156, 4734, -10274, 2428, 47, 3883,
+ 7208, -4454, 16464, 10228, -4264, -5316, 5989, 6995, 2607, -3203, -668,
+ 1640, 904, -2642, -469, 203, 1050, -729, -1586, -33, 324, -227,
+ 138, -165, -76, 50, -374, -903, 385, -637, -751, -371, 7,
+ -958, -172, -379, -347, -638, -287, -600, -251, -397, -596, -514,
+ -222, -363, -323, -357, -509, -371, -223, -433, -356, -378, -472,
+ -223, -149, -381, -362, -256, -360, -249, -245, -414, -343, -215,
+ -318, -296, -253, -281, -260, -186, -267, -334, -184, -218, -221,
+ -298, -323, -280, -235, -266, -346, -291, -364, -250, -269, -330,
+ -407, -337, -288, -297, -291, -367, -331, -313, -227, -277, -310,
+ -324, -354, -249, -294, -307, -326, -246, -243, -266, -239, -219,
+ -134, -170, -168, -181, -178, -179, -104, -105, -182, -117, -16,
+ 21, -1, -16, -123, -97, -70, -125, -190, -191, -224, -174,
+ -191, -187, -240, -111, -186, -142, -146, -92, -134, -84, -176,
+ -166, -110, -109, -146, -139, -231, -119, -73, -70, -193, -83,
+ -143, -72, -63, -63, -163, -69, -90, -96, -97, -84, -157,
+ -37, -91, -88, -116, -29, -99, -31, -73, -68, -76, 24,
+ -65, -8, -90, -71, -32, 33, -119, -46, -74, -20, -92,
+ 37, -144, -45, -100, 37, -86, 3, -132, 2, -42, -29,
+ -104, -4, -152, -16, -85, -19, -175, 14, -94, -9, -106,
+ -24, -72, 42, -77, -27, -22, 2, -33, 78, -29, -21,
+ 21, 111, -5},
+ /* IRC_Composite_C_R0195_T135_P345.wav */
+ {-8, 8, -9, 9, -9, 10, -10, 11, -11, 11, -12,
+ 12, -12, 13, -12, 12, -14, -199, -345, 194, -292, -20,
+ -363, 219, -924, 441, 900, 798, -558, 3720, 1651, -3208, -514,
+ -4991, 13814, 1350, -1342, 10873, 8019, 1098, -3160, 1031, 6880, 5706,
+ -3917, -4208, 1661, 708, -1088, -1854, -201, 229, -177, -273, -648,
+ -82, 475, 5, -734, 169, -151, -581, -653, 471, -491, -236,
+ -587, -109, -475, -248, -581, -317, -480, -514, -555, -293, -524,
+ -197, -571, -212, -393, -202, -577, -116, -375, -344, -418, -218,
+ -355, -263, -348, -305, -348, -210, -371, -214, -394, -179, -361,
+ -184, -347, -189, -348, -251, -234, -293, -267, -213, -252, -242,
+ -269, -147, -370, -183, -325, -201, -333, -191, -269, -230, -243,
+ -283, -228, -273, -293, -252, -342, -267, -359, -215, -428, -224,
+ -365, -239, -330, -269, -348, -257, -251, -286, -250, -246, -272,
+ -177, -242, -155, -277, -116, -236, -94, -218, -96, -109, -104,
+ -71, -63, -76, -143, -84, -111, -151, -139, -218, -149, -229,
+ -159, -185, -165, -192, -165, -156, -158, -137, -140, -159, -114,
+ -167, -123, -164, -97, -148, -122, -112, -140, -90, -115, -78,
+ -111, -104, -86, -114, -49, -142, 12, -166, -28, -168, -17,
+ -113, -58, -89, -81, -116, -67, -58, -65, -96, -68, -92,
+ -33, -99, -37, -87, -67, -90, -37, -68, -114, -65, -87,
+ -50, -124, -30, -83, -1, -96, -4, -55, -19, -87, 13,
+ -80, -10, -90, -5, -84, -42, -79, 4, -67, -63, -38,
+ -27, -36, -23, -7, 11, -34, 14, -18, 51, -74, 41,
+ 9, 19, -7},
+ /* IRC_Composite_C_R0195_T150_P345.wav */
+ {9, -10, 11, -12, 13, -14, 16, -18, 20, -22, 25,
+ -28, 32, -36, 41, -48, 57, -69, 92, -187, -93, -65,
+ 148, -471, 61, 62, -108, -513, 1146, 660, 197, 907, 4643,
+ -2481, -3069, 2384, -436, 10331, -1444, 3529, 10790, 4133, -3114, 1101,
+ 5334, 2231, 175, -4639, -668, 1364, -942, -1832, -179, 106, -201,
+ -621, -288, 256, -14, 7, -70, 10, -62, -163, -538, -397,
+ 250, -662, -393, -549, -98, -597, -308, -388, -216, -452, -393,
+ -291, -335, -411, -150, -445, -160, -290, -89, -416, -142, -299,
+ -403, -360, -239, -271, -373, -246, -311, -338, -233, -221, -259,
+ -317, -192, -311, -255, -207, -237, -276, -272, -275, -277, -233,
+ -252, -250, -181, -293, -170, -245, -207, -274, -163, -258, -249,
+ -201, -205, -239, -243, -229, -292, -303, -216, -288, -278, -312,
+ -241, -345, -256, -284, -302, -346, -258, -265, -246, -271, -236,
+ -265, -215, -277, -207, -240, -196, -255, -168, -178, -155, -144,
+ -107, -152, -110, -104, -74, -180, -121, -180, -122, -196, -144,
+ -245, -129, -207, -127, -182, -107, -225, -82, -192, -124, -217,
+ -97, -227, -156, -196, -108, -174, -119, -141, -77, -130, -30,
+ -126, -49, -162, -24, -161, -3, -167, -53, -117, -53, -120,
+ -68, -81, -70, -85, -45, -100, -52, -108, -40, -95, -39,
+ -123, -50, -123, -64, -117, -58, -114, -101, -106, -61, -84,
+ -90, -69, -81, -96, -40, -52, -52, -64, -50, -43, -17,
+ -37, -37, -34, -28, -67, -18, -49, -26, -48, -2, -23,
+ -34, -24, 7, 4, -14, 13, 9, -1, -21, 7, 5,
+ 3, -11, -9},
+ /* IRC_Composite_C_R0195_T165_P345.wav */
+ {-16, 16, -16, 16, -16, 17, -17, 17, -17, 18, -18,
+ 18, -18, 19, -19, 19, -19, 19, -19, 19, -18, 15,
+ -25, -180, 39, 144, -199, 84, -114, -19, -188, 1254, 741,
+ 544, 587, 3703, -1657, -2617, 2350, 59, 8499, 1392, 3068, 7765,
+ 4121, -1476, -112, 3753, -281, 908, -3882, -912, -13, 232, -1645,
+ 225, -231, 30, -284, 242, 364, 101, -416, -192, -215, -224,
+ -211, -588, -413, 94, -532, -410, -203, -224, -476, -149, -162,
+ -206, -299, -157, -323, -203, -258, -155, -285, -195, -259, -160,
+ -310, -161, -188, -219, -325, -228, -252, -329, -249, -318, -288,
+ -289, -215, -279, -273, -237, -265, -253, -240, -166, -308, -223,
+ -268, -204, -257, -185, -200, -214, -215, -169, -225, -233, -270,
+ -224, -251, -229, -254, -245, -245, -239, -242, -271, -250, -255,
+ -257, -319, -229, -320, -280, -334, -233, -335, -241, -307, -225,
+ -319, -212, -265, -239, -293, -245, -235, -213, -218, -190, -158,
+ -146, -114, -164, -138, -176, -126, -194, -163, -222, -185, -202,
+ -181, -221, -210, -185, -167, -169, -169, -149, -149, -121, -122,
+ -91, -166, -97, -138, -90, -161, -94, -134, -93, -167, -101,
+ -146, -89, -171, -96, -130, -90, -122, -75, -116, -77, -102,
+ -73, -86, -102, -91, -92, -79, -122, -82, -99, -73, -113,
+ -89, -86, -78, -89, -91, -110, -85, -90, -61, -78, -110,
+ -68, -88, -12, -123, -18, -120, 12, -117, 12, -119, 3,
+ -74, 28, -74, -4, -63, 7, -46, 0, -67, 7, -52,
+ 21, -52, 5, -33, 17, 5, -5, -18, -6, 3, -14,
+ -17, -13, -5},
+ /* IRC_Composite_C_R0195_T180_P345.wav */
+ {14, -14, 15, -15, 15, -16, 16, -17, 18, -18, 19, -20,
+ 20, -21, 22, -23, 25, -26, 28, -31, 33, -37, 43, -53,
+ 84, -38, 163, -71, -158, 103, 194, -92, 53, 482, 91, 607,
+ 1972, 2332, -1572, -66, -260, 1180, 4049, -804, 6508, 7047, 3077, -817,
+ 1693, 1744, 560, 329, -3303, 313, -242, -704, -831, 249, -87, -229,
+ 183, 93, 110, 39, -28, -382, -360, -250, -254, -387, -156, -146,
+ -568, -151, -222, 7, -192, -113, -168, -1, -224, -13, -332, -98,
+ -241, -68, -295, -42, -210, -57, -298, -57, -251, -128, -317, -192,
+ -299, -254, -305, -255, -341, -262, -251, -237, -260, -298, -266, -251,
+ -226, -202, -261, -186, -262, -164, -227, -165, -247, -142, -218, -142,
+ -250, -192, -303, -264, -330, -282, -325, -281, -325, -258, -295, -215,
+ -312, -214, -308, -229, -274, -251, -309, -306, -294, -311, -271, -299,
+ -256, -284, -250, -220, -205, -223, -287, -266, -239, -195, -135, -147,
+ -134, -149, -99, -176, -199, -275, -263, -263, -286, -264, -295, -223,
+ -247, -187, -176, -146, -128, -120, -103, -115, -67, -85, -89, -116,
+ -143, -100, -117, -73, -127, -58, -97, -32, -121, -65, -147, -77,
+ -157, -103, -172, -126, -140, -135, -140, -152, -146, -132, -154, -119,
+ -152, -105, -161, -118, -130, -105, -117, -95, -88, -108, -66, -84,
+ -63, -104, -55, -48, -36, -55, -20, -46, -8, -40, 5, -55,
+ -25, -61, -13, -59, -44, -51, -47, -33, -43, -29, -37, -41,
+ -40, -27, -26, -28, -32, -23, -2, -7, 24, -14, -21, -16,
+ -7, -26, -52, -44},
+ /* IRC_Composite_C_R0195_T195_P345.wav */
+ {6, -6, 6, -7, 7, -7, 7, -7, 7, -7, 8, -8,
+ 8, -8, 9, -9, 9, -10, 10, -11, 11, -12, 14, -15,
+ 17, -21, 38, -16, 75, -220, -59, 106, 197, -177, 5, -30,
+ -104, 721, 1540, 1146, -1007, 1149, 558, -528, 530, 189, 5406, 5771,
+ 1722, 1227, 2989, 2251, -549, 199, -1093, 649, 351, -1192, -797, 86,
+ 474, -276, 161, -98, 513, -55, 324, -259, 35, -250, 203, -294,
+ -48, -254, -20, -107, 43, -102, 74, 7, 58, 5, 31, 16,
+ -81, -113, -13, -32, -61, -116, -90, -161, -57, -141, -149, -200,
+ -221, -127, -211, -175, -301, -256, -303, -211, -249, -283, -281, -255,
+ -186, -218, -164, -228, -189, -224, -141, -185, -133, -235, -194, -211,
+ -160, -216, -234, -256, -252, -243, -263, -310, -294, -327, -270, -308,
+ -298, -307, -298, -251, -283, -236, -306, -260, -308, -239, -300, -271,
+ -297, -271, -265, -262, -234, -272, -265, -257, -272, -222, -275, -166,
+ -216, -116, -201, -132, -209, -165, -253, -247, -300, -289, -266, -286,
+ -274, -274, -215, -180, -166, -157, -142, -89, -78, -66, -105, -65,
+ -95, -58, -92, -79, -99, -64, -63, -57, -120, -89, -112, -118,
+ -168, -135, -190, -134, -199, -134, -197, -155, -189, -166, -196, -156,
+ -193, -139, -163, -148, -164, -142, -130, -130, -110, -101, -105, -93,
+ -75, -95, -30, -101, -23, -80, -12, -54, -3, -54, -21, -40,
+ -15, -43, -40, -44, -53, -61, -41, -51, -45, -65, -44, -54,
+ -43, -56, -70, -49, -64, -4, -42, 4, -66, 5, -19, 28,
+ -23, -34, -32, -54},
+ /* IRC_Composite_C_R0195_T210_P345.wav */
+ {-2, 2, -2, 2, -2, 2, -2, 1, -1, 1, -1, 1,
+ -1, 0, 0, 0, 1, -1, 1, -2, 3, -3, 4, -6,
+ 7, -9, 13, -20, 43, -89, -222, 10, 103, -107, -34, -36,
+ 142, -400, -30, 464, 582, 998, 189, -102, 357, 928, -496, 3,
+ 2407, 3014, 4127, 2156, 97, 2572, 2367, 192, -1077, 553, 225, 383,
+ -471, -511, -191, 662, 222, 22, 339, 294, 362, 131, 306, 286,
+ 107, 75, 262, 201, 100, 126, 189, 137, 172, 10, 156, 70,
+ 103, -4, -19, -25, -76, -86, -32, -105, -27, -153, -34, -185,
+ -5, -211, -79, -248, -114, -227, -127, -231, -155, -259, -155, -256,
+ -173, -269, -198, -265, -178, -210, -163, -209, -150, -214, -137, -236,
+ -119, -268, -155, -295, -185, -296, -225, -287, -248, -275, -270, -274,
+ -255, -294, -292, -305, -257, -323, -287, -335, -263, -321, -272, -309,
+ -253, -288, -250, -283, -256, -298, -279, -264, -275, -235, -286, -206,
+ -259, -185, -242, -173, -250, -179, -242, -182, -246, -189, -235, -187,
+ -222, -180, -217, -154, -198, -159, -175, -140, -165, -133, -123, -114,
+ -152, -125, -126, -116, -132, -139, -111, -130, -90, -128, -110, -154,
+ -128, -148, -134, -149, -163, -162, -154, -157, -161, -181, -160, -162,
+ -162, -166, -161, -175, -145, -160, -146, -183, -159, -151, -121, -142,
+ -111, -108, -118, -82, -70, -76, -70, -81, -45, -58, -32, -55,
+ -61, -50, -56, -13, -58, -19, -65, -23, -55, -34, -79, -47,
+ -53, -75, -77, -59, -49, -49, -45, -23, -65, -15, -50, -1,
+ -35, -44, -51, -18},
+ /* IRC_Composite_C_R0195_T225_P345.wav */
+ {4, -4, 4, -4, 5, -5, 5, -5, 5, -5, 5, -5,
+ 5, -5, 5, -6, 6, -6, 6, -6, 6, -6, 7, -7,
+ 7, -7, 8, -8, 8, -8, 9, -22, -68, -102, -39, -33,
+ -2, -125, -43, -65, -81, -88, 504, 491, 271, 136, 50, 242,
+ 413, 575, 246, 1154, 3270, 2343, 1173, 1360, 1386, 1335, 693, 41,
+ -16, 626, 497, -208, 385, -141, 534, 407, 815, 196, 540, 538,
+ 608, 534, 497, 553, 348, 395, 314, 320, 191, 167, 89, 75,
+ -22, 80, -26, 77, -33, 113, -103, 86, -85, 66, -164, -34,
+ -138, -32, -150, -66, -143, -81, -162, -134, -151, -126, -180, -176,
+ -206, -176, -215, -184, -242, -174, -257, -171, -240, -144, -234, -156,
+ -238, -145, -213, -186, -241, -198, -236, -200, -304, -237, -309, -223,
+ -315, -256, -320, -273, -306, -276, -282, -301, -281, -293, -281, -297,
+ -281, -281, -278, -300, -296, -308, -282, -258, -291, -257, -276, -228,
+ -231, -194, -203, -185, -194, -181, -173, -165, -183, -152, -208, -152,
+ -227, -158, -213, -156, -222, -165, -206, -169, -192, -153, -170, -166,
+ -179, -180, -153, -149, -139, -152, -153, -126, -111, -90, -107, -114,
+ -128, -119, -123, -144, -150, -167, -156, -182, -180, -204, -160, -191,
+ -173, -198, -185, -178, -167, -163, -174, -180, -175, -186, -163, -152,
+ -140, -147, -132, -122, -102, -97, -72, -79, -75, -66, -54, -46,
+ -62, -28, -64, -26, -69, -23, -69, -28, -68, -48, -80, -55,
+ -84, -64, -71, -62, -66, -82, -71, -59, -36, -53, -39, -61,
+ -45, -48, -32, -64},
+ /* IRC_Composite_C_R0195_T240_P345.wav */
+ {1, -1, 1, -1, 1, -1, 1, -2, 2, -2, 2, -2,
+ 2, -2, 2, -2, 2, -2, 2, -2, 2, -2, 3, -3,
+ 3, -3, 4, -4, 5, -7, 11, -87, -91, -69, -34, -17,
+ -83, -130, -72, -103, -104, -134, 114, -4, -17, 85, 373, 202,
+ -49, -101, 219, 365, 876, 782, 785, 1628, 2056, 1505, 913, 728,
+ 668, 1183, 655, 491, 465, 893, 831, 430, 584, 712, 1020, 649,
+ 708, 723, 730, 613, 504, 566, 414, 315, 187, 278, 133, 226,
+ 116, 259, 129, 186, 116, 185, 45, 88, -32, 30, -121, -4,
+ -138, -59, -242, -77, -196, -88, -164, -95, -154, -158, -144, -123,
+ -166, -155, -131, -163, -193, -126, -158, -197, -221, -177, -191, -199,
+ -207, -207, -232, -259, -213, -257, -223, -245, -210, -232, -214, -199,
+ -250, -237, -259, -286, -308, -300, -276, -316, -306, -323, -315, -283,
+ -322, -259, -334, -262, -308, -246, -279, -237, -252, -210, -235, -204,
+ -211, -204, -216, -219, -187, -208, -184, -188, -172, -182, -147, -178,
+ -171, -209, -148, -207, -140, -214, -153, -202, -132, -178, -141, -158,
+ -129, -140, -132, -148, -104, -170, -94, -160, -103, -179, -127, -186,
+ -148, -195, -164, -196, -178, -176, -178, -191, -206, -200, -201, -208,
+ -201, -208, -200, -195, -166, -167, -160, -164, -138, -176, -171, -170,
+ -145, -157, -125, -127, -91, -104, -77, -96, -65, -75, -71, -96,
+ -65, -84, -60, -84, -57, -72, -49, -85, -51, -85, -52, -82,
+ -51, -93, -48, -69, -42, -81, -54, -69, -69, -69, -62, -61,
+ -61, -54, -82, -67},
+ /* IRC_Composite_C_R0195_T255_P345.wav */
+ {1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1,
+ 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1,
+ 1, -1, 2, -2, 2, -2, 2, -2, 3, -3, 5, -29,
+ -81, -145, -126, -102, -79, -99, -83, -32, 26, -25, -104, -85,
+ -9, -133, -67, -57, 165, 465, 613, 590, 447, 814, 1074, 1050,
+ 1231, 1480, 1384, 1691, 1545, 721, 693, 1027, 967, 911, 897, 961,
+ 823, 595, 850, 817, 872, 522, 568, 476, 335, 222, 147, 215,
+ 189, 255, 142, 248, 142, 175, 30, 4, 8, -59, -17, -77,
+ -20, -108, -60, -97, -148, -132, -187, -115, -138, -127, -111, -127,
+ -113, -156, -101, -162, -102, -180, -108, -179, -120, -179, -195, -199,
+ -229, -185, -270, -215, -278, -237, -258, -241, -261, -210, -244, -198,
+ -260, -213, -287, -247, -340, -255, -344, -240, -343, -221, -330, -201,
+ -299, -199, -288, -250, -262, -234, -258, -219, -245, -213, -243, -209,
+ -226, -210, -206, -229, -185, -199, -171, -204, -168, -186, -151, -175,
+ -154, -197, -147, -168, -130, -180, -149, -171, -122, -158, -129, -152,
+ -132, -156, -139, -160, -164, -170, -174, -154, -162, -126, -151, -134,
+ -162, -148, -177, -191, -199, -212, -200, -209, -220, -215, -220, -217,
+ -209, -214, -187, -188, -163, -169, -145, -176, -134, -160, -141, -157,
+ -132, -139, -118, -136, -115, -131, -104, -132, -107, -134, -96, -102,
+ -76, -89, -80, -83, -80, -72, -65, -58, -64, -74, -61, -66,
+ -46, -79, -66, -94, -77, -85, -74, -86, -77, -80, -67, -80,
+ -78, -81, -61, -58},
+ /* IRC_Composite_C_R0195_T270_P345.wav */
+ {1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1,
+ 1, -1, 1, -1, 2, -2, 2, -2, 2, -2, 2, -2,
+ 2, -3, 3, -3, 3, -3, 4, -4, 5, -5, 7, -13,
+ -9, 25, 107, 159, 235, 223, 228, 197, 135, 148, 180, 104,
+ 249, 332, 345, 533, 679, 812, 953, 1029, 929, 1077, 1230, 1424,
+ 1718, 1696, 1725, 1631, 997, 986, 1012, 146, -166, 111, 353, 347,
+ -6, -43, 73, 193, 90, 62, -93, -92, -338, -310, -405, -336,
+ -407, -441, -401, -511, -347, -531, -317, -432, -349, -454, -400, -327,
+ -440, -322, -442, -318, -433, -382, -424, -384, -375, -393, -396, -458,
+ -428, -480, -383, -434, -382, -464, -371, -432, -337, -394, -307, -399,
+ -335, -417, -322, -442, -324, -436, -303, -449, -304, -409, -298, -363,
+ -284, -317, -307, -308, -323, -288, -319, -293, -322, -285, -281, -248,
+ -239, -242, -269, -244, -286, -239, -275, -228, -281, -236, -270, -215,
+ -230, -183, -189, -147, -168, -142, -140, -138, -148, -145, -122, -112,
+ -122, -100, -101, -92, -130, -111, -155, -125, -154, -111, -187, -117,
+ -190, -95, -171, -106, -163, -109, -148, -117, -153, -132, -173, -135,
+ -163, -114, -185, -122, -191, -116, -168, -92, -150, -108, -141, -94,
+ -122, -95, -150, -97, -145, -88, -132, -65, -125, -53, -97, -33,
+ -83, -12, -48, -5, -27, 7, -11, 35, -15, 26, -18, 26,
+ -29, 3, -32, -9, -39, 15, 11, 7, 13, 12, 6, 12,
+ 11, 20, 5, 2, 4, 3, -12, -24, -23, -5, -3, -22,
+ 4, 5, 1, 4},
+ /* IRC_Composite_C_R0195_T285_P345.wav */
+ {0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, -1,
+ 1, -1, 1, -1, 1, -1, 1, -1, 1, -2, 2, -2,
+ 2, -2, 3, -3, 3, -4, 5, -6, 8, -11, 22, -196,
+ -173, -113, -87, -175, -179, -137, -214, -236, -209, -197, -203, -89,
+ 115, 94, 63, 76, -21, -123, -58, 550, 837, 1139, 1631, 2133,
+ 1768, 1607, 1992, 1501, 200, 482, 1172, 950, 840, 796, 772, 845,
+ 1106, 940, 1271, 764, 758, 561, 601, 592, 456, 196, 274, 22,
+ 269, 50, 133, 94, 119, 18, 4, 153, 30, 97, 24, 85,
+ 20, 33, 14, -38, -26, -152, -46, -172, -60, -155, -115, -148,
+ -198, -89, -195, -112, -261, -100, -214, -212, -183, -197, -150, -219,
+ -140, -179, -164, -230, -178, -187, -230, -226, -257, -158, -281, -201,
+ -228, -189, -261, -210, -220, -259, -267, -256, -233, -276, -267, -249,
+ -227, -294, -254, -243, -276, -267, -297, -257, -292, -260, -305, -245,
+ -279, -264, -243, -230, -212, -214, -151, -148, -130, -127, -88, -74,
+ -103, -75, -88, -67, -122, -69, -96, -106, -123, -96, -146, -128,
+ -172, -140, -152, -152, -190, -154, -179, -153, -191, -155, -209, -127,
+ -197, -114, -197, -134, -193, -131, -195, -130, -172, -126, -176, -158,
+ -191, -169, -206, -154, -203, -183, -230, -172, -205, -185, -204, -179,
+ -196, -157, -166, -116, -150, -115, -126, -82, -123, -71, -85, -83,
+ -83, -68, -72, -63, -78, -79, -49, -75, -79, -92, -70, -98,
+ -66, -95, -75, -105, -104, -85, -82, -92, -105, -73, -107, -87,
+ -82, -75, -79, -91},
+ /* IRC_Composite_C_R0195_T300_P345.wav */
+ {-1, 1, -1, 1, -1, 1, -1, 1, -1, 2, -2, 2,
+ -2, 2, -2, 2, -2, 2, -2, 2, -2, 3, -3, 3,
+ -3, 4, -4, 5, -6, 8, -10, 16, -33, 4, 109, 115,
+ 258, 119, 96, 128, 286, 309, 258, 544, 795, 778, 461, 339,
+ 836, 601, 406, 1682, 1975, 1154, 2276, 2834, 1223, 701, 931, 69,
+ -668, -254, 489, 635, -539, 173, 339, 531, 396, 94, 191, -102,
+ 150, -127, -75, -251, -176, -409, -489, -562, -686, -623, -666, -571,
+ -644, -520, -569, -434, -437, -442, -489, -355, -376, -438, -545, -399,
+ -497, -467, -538, -479, -549, -412, -479, -420, -458, -466, -435, -392,
+ -372, -360, -342, -413, -353, -363, -348, -419, -344, -451, -370, -415,
+ -367, -468, -389, -500, -406, -491, -367, -474, -360, -481, -350, -435,
+ -296, -421, -285, -402, -267, -363, -222, -350, -221, -334, -215, -270,
+ -177, -235, -146, -214, -118, -175, -89, -140, -81, -133, -96, -92,
+ -54, -91, -56, -86, -58, -64, -32, -51, -40, -76, -57, -55,
+ -36, -113, -96, -83, -105, -91, -129, -152, -109, -131, -124, -142,
+ -150, -156, -119, -136, -143, -152, -125, -140, -130, -126, -129, -99,
+ -125, -119, -110, -121, -109, -118, -102, -117, -98, -117, -101, -93,
+ -114, -105, -94, -69, -112, -75, -86, -60, -49, -25, -59, 6,
+ -33, -1, -8, 8, -2, 17, 23, 0, 29, 13, 17, 24,
+ 18, 21, 18, 23, -13, 11, -8, 15, 5, 10, -10, 8,
+ -24, 7, 1, -11, 1, 27, -4, 48, 5, 53, 24, 86,
+ 43, 68, 50, 99},
+ /* IRC_Composite_C_R0195_T315_P345.wav */
+ {1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -2,
+ 2, -2, 2, -2, 2, -2, 2, -2, 2, -2, 2, -2,
+ 2, -2, 2, -2, 2, -1, 0, 6, -137, -110, -314, -483,
+ -264, -184, -467, -482, 89, -336, 204, 277, 141, -93, -770, -71,
+ 819, -837, 1577, 3127, 1312, 2383, 3512, 2495, -80, 350, 890, 699,
+ 71, 561, 954, 294, 1113, 1117, 1259, 495, 984, 849, 877, 553,
+ 746, 524, 537, 393, 441, 428, 542, 429, 296, 210, 204, 231,
+ 133, -55, 33, -7, -111, -11, -91, 34, -190, -56, -131, 39,
+ -14, 66, -79, 58, -96, 105, -60, 169, -64, 84, -143, 118,
+ -100, -84, -157, -91, -304, -233, -224, -201, -251, -259, -309, -306,
+ -307, -286, -299, -265, -407, -357, -301, -345, -348, -313, -294, -342,
+ -334, -338, -298, -266, -288, -261, -298, -254, -268, -227, -236, -260,
+ -244, -223, -227, -260, -233, -195, -202, -171, -228, -156, -170, -114,
+ -170, -107, -146, -105, -89, -96, -83, -40, -73, -59, -66, -65,
+ -71, -53, -107, -74, -75, -92, -122, -102, -105, -87, -130, -86,
+ -139, -115, -142, -131, -148, -147, -160, -144, -151, -139, -173, -139,
+ -186, -126, -170, -147, -194, -137, -189, -141, -184, -162, -186, -151,
+ -208, -180, -195, -172, -186, -165, -204, -165, -157, -155, -139, -132,
+ -141, -108, -126, -89, -110, -98, -101, -113, -94, -111, -133, -99,
+ -126, -121, -126, -97, -115, -103, -122, -84, -114, -58, -120, -106,
+ -109, -97, -103, -75, -128, -91, -104, -73, -91, -53, -59, -46,
+ -63, -33, -22, -14},
+ /* IRC_Composite_C_R0195_T330_P345.wav */
+ {-9, 9, -9, 9, -9, 9, -9, 9, -9, 9, -9, 9,
+ -9, 9, -9, 9, -9, 9, -9, 10, -10, 10, -10, 10,
+ -10, 10, -10, 10, -7, -378, -458, 118, -356, -539, -320, -251,
+ -977, -150, 272, 171, 262, 290, -552, -354, -2031, 2393, -54, -667,
+ 4888, 3245, 1575, 3792, 4173, -210, -907, 432, 1658, -334, 338, 623,
+ 940, 723, 1598, 1314, 1216, 764, 1038, 976, 401, 116, 250, 473,
+ 239, 334, 528, 294, 340, 65, 340, 194, 348, 152, 304, 92,
+ 279, 123, 93, -24, 89, -192, -125, -56, -130, 68, 194, -86,
+ 77, 174, 220, 128, 98, -234, -216, -168, -160, -251, -283, -410,
+ -399, -240, -227, -363, -327, -361, -302, -370, -271, -312, -309, -428,
+ -402, -318, -223, -266, -304, -253, -264, -272, -246, -203, -278, -299,
+ -319, -254, -236, -260, -281, -188, -203, -212, -220, -185, -220, -178,
+ -230, -207, -215, -227, -248, -182, -197, -163, -179, -101, -143, -112,
+ -138, -84, -115, -82, -111, -83, -102, -83, -51, -69, -49, -89,
+ -18, -67, -73, -108, -72, -63, -79, -111, -167, -94, -130, -99,
+ -166, -109, -181, -121, -156, -124, -154, -131, -162, -169, -165, -170,
+ -136, -151, -159, -185, -163, -122, -139, -141, -175, -141, -148, -148,
+ -172, -157, -130, -154, -150, -148, -142, -153, -141, -116, -159, -97,
+ -155, -91, -162, -92, -141, -53, -134, -97, -132, -68, -111, -97,
+ -127, -98, -128, -105, -137, -81, -141, -76, -120, -73, -136, -97,
+ -105, -48, -102, -73, -108, -56, -97, -41, -75, -56, -70, -20,
+ -33, -9, -51, -32},
+ /* IRC_Composite_C_R0195_T345_P345.wav */
+ {1, -2, 2, -3, 4, -4, 5, -6, 7, -8, 9,
+ -10, 11, -13, 14, -16, 17, -19, 21, -22, 24, -25,
+ 25, -22, 13, 15, -152, -764, 140, -602, 18, -900, -519,
+ -1092, -545, 489, 261, 706, -1198, 1895, -1912, -2689, 2900, 30,
+ -3177, 8188, 3071, 2405, 4243, 6073, 615, -3233, 507, 1858, -510,
+ -537, 1688, 110, 230, 1724, 2185, 1547, 1263, 1871, 1145, 573,
+ 342, -23, 261, -614, -9, -121, 805, -137, 38, 285, 272,
+ -29, 312, 280, 184, 234, 346, -32, 603, 80, 194, 153,
+ 323, -216, 458, 198, 10, -275, 28, -275, -40, -248, -266,
+ -416, -305, -349, -213, -336, -338, -460, -267, -400, -247, -403,
+ -303, -379, -264, -460, -298, -347, -227, -420, -309, -405, -246,
+ -238, -250, -319, -267, -250, -266, -221, -240, -318, -184, -236,
+ -175, -305, -161, -225, -161, -213, -190, -219, -158, -206, -155,
+ -198, -165, -211, -169, -178, -141, -180, -148, -191, -154, -191,
+ -120, -222, -141, -171, -68, -131, -104, -142, -95, -72, -93,
+ -71, -91, -94, -90, -92, -83, -97, -91, -117, -108, -109,
+ -102, -101, -101, -116, -95, -82, -119, -115, -158, -119, -137,
+ -108, -151, -151, -163, -123, -125, -148, -167, -118, -168, -149,
+ -196, -146, -169, -130, -179, -147, -164, -153, -114, -109, -119,
+ -128, -121, -82, -111, -89, -98, -81, -102, -105, -71, -85,
+ -85, -116, -85, -98, -76, -124, -102, -118, -107, -84, -126,
+ -141, -114, -78, -80, -110, -127, -121, -93, -112, -96, -109,
+ -98, -90, -55, -53, -66, -54, -41, -26, -18, -19, -5,
+ -33, -57, -33}};
+
+const int16_t irc_composite_c_r0195_p000[][256] =
+ {/* IRC_Composite_C_R0195_T000_P000.wav */
+ {1, -1, 1, -1, 0, 0, 0, 0, 0, 1, -1,
+ 2, -2, 2, -3, 4, -5, 6, -7, 9, -11, 14,
+ -20, 33, -756, -534, -348, 102, -1032, -582, -1506, 65, -959,
+ 133, -210, 1928, 576, -291, -5368, 5774, -6962, -2265, 14308, -3483,
+ 7090, 10599, 2076, -2731, 3149, -192, -3412, -1652, 3866, 548, -1226,
+ -1088, 2012, 2077, 1960, -209, 1087, 1404, 1871, 1150, 1665, 536,
+ 626, 1200, 1268, 592, -114, -160, -95, 133, -37, -246, 158,
+ -144, -190, -548, 421, 96, 105, -192, 181, 129, 158, 143,
+ 73, -164, -261, -64, -147, -128, -55, -166, -60, 98, 349,
+ -253, 126, -172, 110, -391, -118, 265, -317, 256, -97, 416,
+ -62, 274, -244, -355, -104, -427, -357, -741, -368, -489, -365,
+ -418, -498, -150, -384, -361, -530, -257, -297, -381, -491, -511,
+ -393, -429, -294, -395, -373, -408, -292, -342, -260, -297, -286,
+ -427, -324, -291, -291, -276, -319, -244, -386, -221, -241, -141,
+ -260, -273, -231, -153, -86, -203, -202, -152, -95, -140, -176,
+ -127, -110, -156, -237, -148, -139, -94, -248, -190, -202, -85,
+ -155, -205, -223, -169, -158, -183, -216, -167, -164, -146, -219,
+ -103, -162, -71, -220, -10, -169, 7, -134, 32, -76, -42,
+ -100, -66, -50, -44, -71, -88, -122, -40, -75, 21, -78,
+ -18, -125, -33, -69, 53, -77, -22, -138, -41, -62, -55,
+ -92, -83, -99, -115, -150, -17, -53, -12, -184, -50, -96,
+ 28, -81, -1, -78, -41, -118, -14, -66, 38, -82, -29,
+ -104, 29, 5, 34, -62, 14, 7, 47, -61, 67, -43,
+ 51, -145, 61},
+ /* IRC_Composite_C_R0195_T015_P000.wav */
+ {123, -125, 127, -129, 131, -133, 135, -137, 138, -140,
+ 141, -141, 141, -140, 137, -131, 122, -106, 76, -10,
+ -238, -830, -319, -1069, 17, -859, 165, -1424, -309, -2002,
+ 526, 695, 3378, -155, -423, -6705, 9134, -12025, 1719, 14823,
+ -7846, 16119, 11510, -1243, -4202, 5454, -1293, -2996, -1156, 2894,
+ -853, -440, -59, 2319, 814, 827, 428, 2620, 1829, 1181,
+ 817, 1748, 469, 1133, 599, 1385, -490, 316, -1523, 369,
+ -621, 391, -835, 160, -740, 77, -149, 751, -195, -103,
+ -622, 191, -100, 342, -303, -90, -465, 82, 62, 259,
+ -409, -385, 44, 87, -50, 21, 54, -467, -207, -79,
+ -324, -108, -90, -152, -620, 71, -110, 47, -749, -161,
+ -523, -153, -446, -157, -336, 40, -137, -86, -507, -177,
+ -322, -14, -430, -277, -557, -360, -326, -296, -274, -463,
+ -359, -502, -263, -388, -316, -369, -353, -459, -464, -335,
+ -293, -324, -333, -369, -324, -380, -288, -268, -287, -294,
+ -317, -270, -395, -181, -204, -149, -383, -214, -212, -113,
+ -188, -174, -211, -188, -99, -166, -173, -203, -111, -134,
+ -148, -226, -166, -126, -86, -216, -199, -234, -119, -193,
+ -118, -250, -172, -245, -101, -156, -114, -172, -153, -162,
+ -98, -70, -71, -106, -70, -81, -14, -14, 37, -38,
+ -36, -107, -3, -9, 52, -91, -63, -116, 20, -50,
+ 18, -151, -37, -125, 57, -97, 31, -149, 5, -133,
+ 4, -141, 9, -132, -7, -168, 9, -124, 33, -144,
+ 22, -166, 15, -117, 46, -156, 11, -127, 36, -124,
+ 23, -58, 69, -65, 71, -34, 110, -36, 130, -45,
+ 96, -68, 96, -59, 38, -71},
+ /* IRC_Composite_C_R0195_T030_P000.wav */
+ {-63, 66, -69, 73, -76, 80, -84, 89, -93, 98,
+ -102, 107, -110, 111, -108, 95, -54, -154, -785, -302,
+ -1153, 268, -1139, 240, -1422, 320, -2283, 272, -629, 4046,
+ 237, 3112, -7137, 2285, 231, -13166, 20036, -5068, 11914, 16533,
+ 2581, -5753, 2916, 1267, -3077, -1961, 1642, -123, 210, 665,
+ 1080, 1069, 485, 1122, 1846, 1948, 901, 1486, 921, 1132,
+ 470, 865, -186, -57, -358, -153, -489, -118, -724, -365,
+ -517, -156, -335, 155, -255, 96, -315, 376, -169, 22,
+ -435, 5, -544, -35, -484, -12, -457, 280, -453, -177,
+ -399, 435, -423, -188, -484, 161, -217, -38, -732, -416,
+ -157, 73, -480, -440, -302, -140, -260, -335, -490, -420,
+ -470, -291, -617, -112, -315, 20, -559, -210, -412, 154,
+ -162, -102, -453, -225, -205, -23, -247, -203, -409, -147,
+ -366, -147, -367, -150, -477, -326, -502, -222, -390, -294,
+ -498, -249, -423, -313, -510, -239, -382, -176, -516, -351,
+ -496, -92, -337, -150, -462, -212, -366, -74, -288, -94,
+ -322, -110, -334, -53, -240, 11, -306, -108, -332, -45,
+ -271, -52, -295, -76, -308, -99, -319, -57, -226, -50,
+ -303, -130, -254, -54, -183, -89, -261, -98, -193, -6,
+ -184, -12, -164, 45, -175, 27, -120, 129, -69, 82,
+ -89, 120, -26, 113, -48, 59, -77, 18, -85, -3,
+ -107, 0, -78, -29, -159, 0, -62, 67, -139, -9,
+ -138, 19, -136, -3, -124, -27, -149, -30, -136, -11,
+ -97, -7, -137, -19, -96, 20, -84, 15, -64, 28,
+ -92, 9, 15, 121, -12, 24, -32, 74, 36, 98,
+ -9, 3, -30, 36, -42, 4},
+ /* IRC_Composite_C_R0195_T045_P000.wav */
+ {72, -74, 77, -79, 82, -86, 91, -96, 102, -111, 121,
+ -134, 152, -179, 228, -635, 490, -1252, -114, -1202, 742, -1779,
+ 495, -1563, 532, -2895, 2675, 1167, 4718, -4709, 4646, -5559, -6673,
+ 7608, -8298, 21201, 11442, 4596, 920, 5308, -1818, -1353, -1082, -283,
+ -725, 2349, -984, -140, 614, 1696, 1859, 2003, 557, 2080, 1067,
+ 416, 1112, 525, 38, -87, -851, -336, 38, -375, -495, -291,
+ -403, -90, -299, -393, -130, -647, -37, -270, -227, -182, -2,
+ -485, -21, -380, -221, -179, -364, -244, -343, -247, -161, -213,
+ -443, -201, -386, -349, -198, -453, -401, -479, -187, -349, -331,
+ -486, -40, -343, -132, -248, -346, -150, -185, -498, -200, -280,
+ -512, -271, -152, -472, -405, -421, -235, -444, -272, -369, -299,
+ -285, -209, -483, -192, -140, -287, -241, -120, -328, 36, -232,
+ -275, -345, -26, -488, -78, -319, -205, -488, 22, -390, -294,
+ -391, -131, -450, -198, -416, -347, -391, -95, -530, -266, -359,
+ -162, -403, -129, -438, -172, -313, -124, -327, -146, -338, -102,
+ -245, -121, -328, -124, -244, -79, -277, -128, -320, -47, -241,
+ -144, -306, -70, -274, -85, -274, -147, -219, -71, -212, -78,
+ -214, -60, -127, 0, -182, -21, -106, 67, -97, 35, -113,
+ 84, 16, 65, -67, 99, -24, 79, 7, 29, -85, 99,
+ -35, 17, -91, 57, -100, 58, -112, -18, -130, 73, -160,
+ -25, -148, 6, -136, 45, -217, 2, -142, 79, -180, 64,
+ -198, 81, -156, 114, -200, 98, -162, 123, -120, 152, -170,
+ 176, -85, 130, -107, 189, -99, 158, -113, 138, -127, 161,
+ -140, 111, -221},
+ /* IRC_Composite_C_R0195_T060_P000.wav */
+ {123, -132, 142, -153, 166, -180, 196, -215, 238, -264, 296,
+ -338, 397, -545, -26, -1070, 548, -1189, 312, -1132, -540, -593,
+ 703, -1932, 23, 1834, 2821, 3650, -4896, 2244, -8990, 3547, -3667,
+ 11688, 19991, 4002, -67, 5265, -1066, -618, 257, -237, -93, 2490,
+ -1554, -1096, 1601, 944, 1501, 1048, 1374, 1754, 1792, 566, 335,
+ -350, 932, -271, -1525, -771, -646, -466, 146, -610, 113, -1035,
+ 510, -222, -23, -608, -443, -725, 203, -887, -434, -589, -19,
+ -438, -40, -610, 14, -467, -137, -372, -214, -629, -55, -580,
+ -263, -517, -350, -563, -317, -499, -281, -660, -233, -369, 6,
+ -533, -180, -457, -90, -331, -140, -395, -150, -246, -124, -225,
+ -134, -388, -151, -322, -330, -363, -280, -394, -241, -503, -138,
+ -532, -169, -402, -263, -433, -255, -363, -244, -372, -271, -364,
+ -268, -337, -126, -472, -58, -441, 6, -373, -75, -345, -91,
+ -290, -171, -369, -128, -311, -189, -312, -183, -241, -230, -309,
+ -78, -303, -168, -337, -178, -236, -164, -270, -238, -313, -113,
+ -261, -195, -270, -208, -224, -136, -312, -153, -332, -79, -287,
+ -193, -257, -149, -227, -124, -297, -82, -209, -107, -181, -87,
+ -173, -18, -146, 9, -126, 28, -74, -22, -25, 22, -50,
+ 40, -41, 25, -17, -14, 5, 9, -67, 23, -34, 57,
+ -46, 5, 3, 37, -39, 6, -36, 11, -54, -39, -69,
+ -61, -79, -35, -99, -38, -98, -29, -88, -12, -86, -19,
+ -71, -19, -37, -12, -58, 7, -41, 34, -37, 47, -22,
+ 42, -26, 97, 2, 68, -13, 74, 7, 35, -6, 34,
+ -1, 0, -38},
+ /* IRC_Composite_C_R0195_T075_P000.wav */
+ {30, -35, 41, -47, 55, -63, 72, -84, 97, -113, 134,
+ -162, 202, -472, -304, -372, 274, -1176, -371, -433, -535, -54,
+ 364, -1802, 1909, 3073, 5773, -6217, -2323, -1631, -4047, 10163, -533,
+ 23315, 9574, -7064, 764, 4451, 279, -686, 938, 2910, -1594, -781,
+ 31, 2302, -241, 1013, 1336, 1127, 1162, 699, 28, 761, 380,
+ -426, -314, -917, -750, -202, -732, -632, -472, -517, -73, -213,
+ -19, -699, -215, -440, -368, -595, -354, -716, -262, -604, -448,
+ -306, -492, -226, -481, -404, -393, -315, -472, -290, -442, -401,
+ -227, -459, -350, -606, -194, -594, -308, -438, -436, -423, -217,
+ -461, -220, -298, -300, -185, -218, -342, -120, -282, -142, -260,
+ -82, -276, -46, -282, -219, -258, -93, -447, -193, -418, -296,
+ -377, -249, -479, -177, -358, -304, -416, -185, -366, -179, -486,
+ -287, -406, -134, -512, -167, -454, -201, -352, -195, -386, -169,
+ -289, -223, -334, -186, -280, -123, -302, -167, -268, -36, -301,
+ -139, -264, -133, -195, -111, -276, -131, -175, -123, -195, -148,
+ -226, -77, -211, -165, -231, -123, -257, -132, -285, -135, -289,
+ -147, -278, -141, -274, -166, -238, -154, -189, -134, -212, -101,
+ -127, -6, -142, -21, -106, 41, -81, 41, -105, 54, -54,
+ 6, -41, -3, -62, 27, -57, -25, -42, 29, -39, 10,
+ -90, 56, -38, 43, -106, 38, -63, 27, -58, -21, -110,
+ -19, -82, -36, -124, -57, -83, 23, -113, 9, -73, 55,
+ -109, 34, -55, 45, -13, 62, -54, 27, -33, 52, 12,
+ 35, -34, 72, -19, 33, -32, 74, -59, 39, -25, 53,
+ 1, 10, -3},
+ /* IRC_Composite_C_R0195_T090_P000.wav */
+ {-3, 3, -4, 5, -6, 7, -9, 11, -13, 17,
+ -23, 36, -967, 38, -432, 430, -1179, 327, -1275, 279,
+ -1297, 1913, -1800, 360, 826, 8131, -4106, 182, -6660, -1308,
+ 8763, -6147, 21004, 13661, -1493, -3341, 815, 6070, 1330, -1696,
+ 3689, 487, -871, -2812, 2923, 1220, 319, -441, 2019, 161,
+ 1147, -214, 819, -1063, 179, -1133, 125, -1120, 701, -1109,
+ -441, -308, -3, -1227, 135, -661, -529, -162, -375, -951,
+ -262, -371, -533, -415, -491, -644, -459, -451, -508, -551,
+ -360, -465, -434, -399, -416, -458, -204, -423, -358, -363,
+ -290, -434, -346, -447, -174, -475, -424, -400, -239, -439,
+ -175, -468, -418, -188, -90, -409, -214, -233, -225, -147,
+ -140, -439, -299, -104, -157, -229, -145, -201, -232, -109,
+ -193, -310, -284, -271, -306, -318, -342, -330, -320, -362,
+ -208, -295, -346, -295, -212, -394, -231, -297, -357, -348,
+ -177, -356, -301, -275, -291, -319, -184, -255, -307, -178,
+ -245, -228, -206, -194, -280, -137, -218, -191, -183, -203,
+ -186, -105, -193, -151, -164, -132, -164, -77, -203, -113,
+ -167, -192, -164, -75, -239, -194, -160, -166, -186, -77,
+ -232, -182, -148, -63, -204, -92, -164, -69, -54, 4,
+ -140, -18, -43, 36, -61, 41, -116, -2, -92, 42,
+ -126, -61, -150, -8, -117, -15, -62, -69, -114, 61,
+ -14, 10, -74, 8, -25, 119, -37, 0, -69, 10,
+ -56, 37, -94, -111, -86, 30, -107, -71, -119, 4,
+ -92, 38, -125, 29, -50, 58, -57, 64, -84, 92,
+ -4, 67, -73, 54, -8, 88, -67, 71, 21, 64,
+ -36, 123, -7, 23, -36, 115},
+ /* IRC_Composite_C_R0195_T105_P000.wav */
+ {67, -63, 58, -51, 43, -33, 19, -1, -24, 61, -121,
+ 244, -927, 183, -686, 327, -496, -330, -486, -340, -314, 396,
+ 182, -1212, 1259, 6063, -556, -2378, -2582, -3838, 9614, -8512, 18557,
+ 14219, -782, -2750, 529, 5714, 4180, -1314, 2811, 874, -1413, -2611,
+ 2334, 2590, -537, -811, 436, 1284, 515, -112, -879, -106, -221,
+ -558, -955, -184, -84, -312, -553, -219, -379, -172, -456, -790,
+ -576, -350, -708, -606, -553, -357, -404, -422, -537, -414, -291,
+ -515, -518, -625, -263, -417, -492, -719, -360, -360, -368, -573,
+ -481, -304, -142, -306, -536, -356, -164, -154, -398, -429, -388,
+ -147, -288, -315, -532, -144, -267, -143, -527, -186, -329, 6,
+ -447, -226, -375, 20, -329, -159, -444, -77, -329, -143, -450,
+ -93, -327, 2, -390, -87, -374, 55, -418, -96, -445, -37,
+ -436, -129, -505, -118, -444, -152, -487, -172, -395, -135, -432,
+ -170, -368, -96, -414, -145, -394, -32, -326, -123, -434, -83,
+ -276, -94, -355, -149, -242, -56, -256, -166, -269, -75, -228,
+ -119, -313, -87, -207, -58, -304, -97, -234, -23, -285, -126,
+ -259, -11, -242, -91, -247, -1, -204, -10, -242, -5, -194,
+ 74, -169, 102, -92, 170, -62, 135, -93, 108, -134, 5,
+ -190, -8, -203, -47, -223, 16, -187, 1, -174, 58, -131,
+ 78, -113, 75, -130, 98, -75, 44, -121, 101, -58, 53,
+ -146, 47, -70, 70, -128, -19, -108, 21, -89, -23, -119,
+ -27, -39, 31, -124, -51, -37, 65, -45, 0, 4, 66,
+ 6, 17, 11, 14, 30, 64, 44, 27, -16, 79, -9,
+ -11, -60, 55},
+ /* IRC_Composite_C_R0195_T120_P000.wav */
+ {-76, 75, -75, 74, -72, 69, -65, 59, -49, 35,
+ -14, -21, 85, -263, 347, 27, -533, -67, -385, -42,
+ -335, 645, -2065, 1305, 1033, 1922, -2255, 6706, -3555, 1838,
+ -9048, 1510, 15767, 522, 6485, 7416, 2582, -1635, -1123, 7704,
+ 6890, -3746, -2349, -815, 4058, -1305, 633, -1081, 2060, -1086,
+ 490, -1435, 1038, -465, -103, -1558, 594, -576, -246, -797,
+ -411, -729, 216, -627, -444, -642, 122, -670, -147, -796,
+ -255, -906, 76, -717, -449, -584, -119, -685, -337, -606,
+ -352, -326, -478, -502, -420, -468, -445, -428, -466, -351,
+ -293, -513, -155, -443, -243, -371, -82, -573, -58, -388,
+ -209, -303, -195, -394, -165, -220, -376, -192, -262, -255,
+ -289, -234, -278, -284, -271, -328, -227, -325, -185, -358,
+ -162, -289, -168, -301, -148, -278, -221, -237, -227, -212,
+ -236, -164, -236, -223, -147, -209, -256, -222, -209, -360,
+ -125, -328, -206, -254, -219, -307, -162, -296, -276, -174,
+ -324, -187, -268, -155, -286, -159, -317, -134, -252, -195,
+ -181, -236, -204, -162, -148, -226, -141, -227, -132, -141,
+ -218, -189, -165, -114, -288, -111, -256, -76, -252, -50,
+ -308, -80, -158, -63, -217, -126, -102, 12, -10, -77,
+ 13, 74, 33, 49, -22, 50, -56, 57, -177, -13,
+ -201, 19, -144, -101, -181, 59, -32, -49, -45, -72,
+ -16, 32, -40, -48, -33, -42, -45, 64, -145, 8,
+ -69, 33, -144, 44, -92, 56, -118, -2, -60, 8,
+ -77, -3, -91, -37, -73, -11, -53, 0, -49, 56,
+ -45, 17, -73, 100, 6, 64, -7, 125, -47, 94,
+ 22, 33, -55, 12, -30, 7},
+ /* IRC_Composite_C_R0195_T135_P000.wav */
+ {-47, 46, -46, 44, -43, 40, -37, 33, -28, 21, -12,
+ -1, 20, -48, 94, -206, 95, -290, -258, 153, -592, 203,
+ -293, 297, -1153, 1348, 644, 1791, -1433, 6094, -3856, -260, -5383,
+ 6042, 11744, -1699, 5991, 8208, 1865, -2201, 2510, 6226, 4590, -5126,
+ -1361, 782, 2433, -2030, 854, -833, 853, -1117, 568, -1205, 680,
+ -1070, -374, -797, 196, -738, -381, -460, 54, -509, 80, -809,
+ -117, -538, -17, -916, 203, -876, -170, -815, -257, -782, -178,
+ -520, -324, -433, -344, -353, -383, -382, -403, -522, -240, -512,
+ -312, -523, -189, -533, -114, -477, -247, -366, -173, -363, -247,
+ -204, -351, -273, -220, -239, -329, -247, -257, -330, -207, -331,
+ -212, -342, -200, -298, -246, -261, -277, -228, -264, -213, -328,
+ -212, -273, -219, -256, -244, -234, -265, -159, -264, -191, -244,
+ -157, -229, -190, -221, -203, -203, -265, -198, -238, -189, -235,
+ -164, -189, -220, -183, -216, -189, -257, -194, -227, -199, -234,
+ -209, -214, -214, -195, -212, -179, -255, -200, -207, -151, -257,
+ -182, -194, -172, -200, -169, -219, -215, -208, -180, -207, -192,
+ -215, -173, -203, -137, -219, -101, -232, -121, -175, -4, -148,
+ -45, -92, 5, -35, 0, -5, -45, 0, 8, 10, -53,
+ -107, -5, -81, 6, -157, 3, -130, 19, -128, 39, -138,
+ 5, -65, 57, -90, 7, -52, 45, -79, 13, -78, 29,
+ -109, 14, -79, 25, -126, 7, -61, -11, -75, 1, -51,
+ -46, -64, -35, -74, -40, -48, 36, -46, 3, -64, 53,
+ -59, 33, -26, 105, 4, 63, 13, 64, -28, 24, -1,
+ 3, -80, -21},
+ /* IRC_Composite_C_R0195_T150_P000.wav */
+ {-5, 6, -7, 7, -8, 9, -10, 11, -12, 13, -15,
+ 16, -17, 19, -20, 20, -17, 1, -234, 227, -421, 117,
+ -50, -175, -40, 33, -222, 178, 1388, 586, 910, 1027, 4228,
+ -7050, 1223, 2017, 6153, 4669, 1121, 8628, 6102, -3597, 1339, 6488,
+ 2001, -1618, -2670, 715, 665, -9, -1202, 163, -501, 22, -977,
+ 268, -483, 330, -922, 182, -469, 62, -791, -223, -334, -215,
+ -485, -347, -491, -176, -429, -178, -696, -63, -705, -191, -727,
+ -100, -715, -18, -605, -161, -531, -137, -475, -197, -485, -201,
+ -433, -135, -408, -232, -404, -136, -441, -131, -469, -151, -475,
+ -144, -469, -155, -431, -165, -416, -207, -381, -199, -342, -208,
+ -344, -172, -301, -189, -339, -193, -297, -159, -303, -169, -283,
+ -144, -295, -190, -323, -198, -312, -172, -324, -214, -279, -169,
+ -264, -159, -230, -157, -232, -160, -206, -167, -240, -200, -232,
+ -191, -234, -175, -238, -189, -225, -142, -206, -214, -206, -200,
+ -152, -232, -176, -232, -184, -217, -152, -205, -194, -188, -192,
+ -160, -199, -163, -211, -186, -234, -194, -218, -212, -270, -194,
+ -202, -181, -219, -182, -202, -169, -194, -142, -192, -142, -138,
+ -58, -121, -63, -104, -7, -62, -12, -40, 0, -55, -16,
+ -33, -41, -87, -38, -71, -48, -89, -7, -81, -22, -59,
+ 5, -41, -28, -47, -20, -14, -23, -20, -10, -31, -22,
+ -25, -11, -55, -34, -32, -33, -44, -53, 1, -30, -56,
+ -58, -29, -61, -47, -27, -44, -54, -52, -32, -37, -63,
+ -16, 18, 1, -3, 18, 72, 0, 21, 9, 42, -23,
+ -7, -40, -42},
+ /* IRC_Composite_C_R0195_T165_P000.wav */
+ {-31, 31, -31, 31, -30, 30, -30, 29, -28, 27, -25, 23,
+ -20, 16, -12, 5, 5, -18, 40, -78, 187, -74, -49, 55,
+ -110, 112, -199, 218, -425, 530, 199, 1304, 62, 2352, 168, 2823,
+ -5315, 837, 4953, 2252, 4624, 4623, 6361, 3213, -1540, 938, 5698, -601,
+ -1840, -1321, 415, -514, -55, -892, 3, -75, 186, -753, 471, -260,
+ 192, -752, 21, -345, -220, -374, -258, -349, -282, -436, -427, -398,
+ -253, -570, -291, -449, -37, -524, -88, -564, -148, -493, -85, -435,
+ -73, -317, -105, -359, -181, -335, -176, -321, -195, -365, -206, -391,
+ -180, -383, -187, -437, -200, -456, -199, -434, -221, -404, -185, -335,
+ -239, -304, -221, -288, -247, -317, -222, -256, -192, -283, -227, -290,
+ -193, -268, -185, -298, -185, -261, -149, -305, -194, -283, -162, -272,
+ -207, -271, -209, -214, -185, -206, -201, -195, -156, -200, -178, -267,
+ -179, -273, -180, -271, -149, -245, -175, -193, -143, -182, -204, -213,
+ -175, -213, -191, -252, -204, -249, -193, -210, -188, -202, -194, -140,
+ -186, -146, -188, -134, -188, -186, -187, -199, -210, -191, -202, -211,
+ -250, -183, -225, -176, -254, -154, -188, -96, -150, -85, -102, -69,
+ -80, -63, -71, -81, -74, -55, -81, -81, -69, -57, -79, -51,
+ -37, -34, -57, -26, -18, 6, -12, 5, -5, 22, -15, 19,
+ -13, 6, -49, 9, -38, -32, -76, -25, -65, -43, -75, -1,
+ -82, -14, -96, -21, -95, -25, -47, -4, -67, -76, -41, -67,
+ -39, -53, -5, -25, -37, 30, 14, 61, -21, 72, -39, 30,
+ -59, 25, -48, -21},
+ /* IRC_Composite_C_R0195_T180_P000.wav */
+ {9, -9, 9, -10, 10, -11, 11, -12, 13, -13, 14, -15,
+ 16, -17, 18, -19, 20, -21, 22, -24, 25, -26, 25, -17,
+ 104, -114, 196, -98, 54, -126, 232, -41, 337, 459, 418, 1258,
+ 2024, 1306, -3081, 2067, -322, 1935, 2599, 4829, 7530, 3384, -1993, 2649,
+ 3619, -256, -653, -863, -84, -381, -1124, -459, 826, -56, -554, -375,
+ 247, 362, 403, -767, -54, -307, 253, -528, -72, -499, -251, -538,
+ -224, -365, -429, -387, -270, -98, -240, -189, -323, -221, -114, -200,
+ -172, -265, -34, -233, -42, -325, -228, -392, -171, -360, -298, -341,
+ -265, -231, -220, -299, -332, -289, -282, -307, -286, -295, -293, -265,
+ -194, -288, -280, -341, -219, -319, -258, -367, -240, -308, -190, -294,
+ -215, -308, -212, -271, -223, -283, -231, -230, -192, -212, -231, -187,
+ -195, -184, -197, -212, -220, -239, -198, -224, -201, -254, -209, -229,
+ -205, -256, -239, -265, -231, -229, -194, -202, -206, -179, -140, -169,
+ -205, -189, -208, -194, -220, -219, -227, -219, -190, -210, -179, -230,
+ -143, -194, -136, -216, -131, -181, -147, -210, -176, -226, -209, -222,
+ -193, -214, -206, -190, -190, -205, -213, -200, -179, -148, -109, -85,
+ -53, -73, -40, -100, -69, -150, -91, -144, -106, -152, -99, -85,
+ -54, -38, -16, 3, 2, 19, 74, 35, 28, 11, 42, 13,
+ 8, -18, -32, -36, -29, -19, -47, -86, -72, -66, -42, -83,
+ -71, -83, -83, -72, -54, -20, -67, -68, -58, -74, -64, -79,
+ -16, -60, -46, -42, -7, 11, 22, 53, 51, 32, -13, 33,
+ 28, 3, -22, -46},
+ /* IRC_Composite_C_R0195_T195_P000.wav */
+ {12, -12, 12, -12, 12, -12, 12, -12, 12, -12, 12, -12,
+ 12, -12, 12, -12, 11, -11, 10, -9, 8, -6, 4, -1,
+ -5, 16, -58, -48, -46, 77, 37, -65, -49, -7, -27, 197,
+ 332, 255, 848, 1561, 1608, -2167, 729, -55, 1458, 1975, 2964, 6210,
+ 4289, 99, 1163, 3083, 300, -110, -1053, 97, -72, -273, -635, 580,
+ 371, -146, -5, 150, 225, 128, 22, 74, -75, 62, -60, -62,
+ -162, -184, -359, -146, -99, 33, -264, -53, -91, -68, -97, -68,
+ -107, -168, -116, -102, -205, -156, -272, -180, -287, -225, -284, -228,
+ -198, -203, -177, -240, -212, -239, -265, -245, -245, -239, -269, -232,
+ -261, -248, -259, -262, -292, -271, -282, -251, -272, -265, -298, -265,
+ -285, -267, -290, -263, -263, -239, -279, -219, -264, -196, -232, -191,
+ -206, -170, -181, -179, -220, -200, -244, -184, -248, -231, -259, -234,
+ -254, -249, -268, -279, -250, -255, -219, -236, -226, -171, -197, -163,
+ -222, -176, -205, -187, -189, -200, -188, -195, -184, -172, -221, -174,
+ -245, -154, -259, -156, -238, -169, -237, -202, -213, -230, -237, -242,
+ -223, -236, -210, -202, -201, -217, -219, -206, -204, -158, -180, -128,
+ -112, -64, -38, -58, -71, -95, -94, -116, -117, -160, -118, -117,
+ -105, -102, -88, -47, -31, -21, -9, 0, 24, -11, 6, -19,
+ 6, 1, -16, -22, -27, -22, -10, -73, -58, -79, -51, -76,
+ -65, -82, -99, -73, -70, -36, -82, -58, -110, -76, -105, -83,
+ -46, -122, -54, -86, 17, -33, 15, 14, -1, 0, 13, 44,
+ 23, -6, 2, -42},
+ /* IRC_Composite_C_R0195_T210_P000.wav */
+ {6, -6, 6, -6, 7, -7, 7, -7, 7, -7, 7, -7,
+ 8, -8, 8, -8, 8, -9, 9, -9, 10, -10, 10, -11,
+ 12, -12, 13, -13, -3, -140, -62, 16, 74, -75, -81, -9,
+ -154, 52, 75, 214, 374, 933, 1063, -50, -424, 1215, -28, 199,
+ 1671, 2854, 5292, 2238, 1086, 1897, 2061, -301, -116, -17, 89, 220,
+ -368, -4, 403, 517, 47, 242, 331, 363, 284, 327, 289, 188,
+ 152, 291, 134, 170, 136, 286, 56, 186, 15, 53, -103, 44,
+ -142, -179, -241, -86, -171, -94, -229, -133, -165, -58, -222, -146,
+ -225, -113, -173, -105, -202, -191, -235, -184, -216, -202, -258, -257,
+ -241, -211, -214, -256, -271, -278, -250, -239, -276, -254, -285, -245,
+ -277, -265, -277, -285, -295, -284, -275, -264, -248, -242, -249, -215,
+ -225, -190, -221, -208, -231, -195, -210, -191, -211, -203, -222, -199,
+ -222, -228, -234, -247, -238, -233, -235, -232, -214, -216, -211, -201,
+ -215, -188, -250, -191, -224, -183, -218, -210, -195, -212, -196, -217,
+ -202, -221, -224, -225, -263, -219, -268, -220, -277, -225, -254, -223,
+ -243, -232, -228, -229, -221, -217, -194, -205, -169, -170, -122, -124,
+ -95, -89, -83, -76, -91, -74, -103, -96, -96, -80, -91, -101,
+ -107, -123, -107, -115, -88, -113, -70, -93, -25, -70, -18, -56,
+ -11, -38, -15, -23, -20, -23, -29, -31, -55, -70, -58, -57,
+ -52, -93, -74, -95, -46, -80, -63, -97, -83, -108, -85, -90,
+ -88, -102, -101, -88, -79, -54, -29, -30, -33, -9, -5, -3,
+ 24, -9, 7, -23},
+ /* IRC_Composite_C_R0195_T225_P000.wav */
+ {-1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1,
+ -1, 1, -1, 1, -1, 1, -1, 1, -2, 2, -2, 2,
+ -3, 4, -4, 6, -9, 17, -16, -98, -60, -77, 48, 3,
+ -81, -121, -43, -55, 1, 19, 93, 305, 537, 631, 131, -260,
+ 545, 567, 27, 1207, 2128, 2827, 2558, 1188, 1325, 1915, 744, 102,
+ 304, 255, 211, 393, 327, 58, 643, 377, 590, 456, 611, 727,
+ 425, 704, 395, 620, 277, 569, 148, 252, 218, 128, 12, -134,
+ -32, -106, -54, -94, -7, 3, -19, -31, -59, -78, -49, -114,
+ -79, -187, -72, -177, -103, -203, -112, -200, -151, -256, -183, -276,
+ -182, -258, -181, -256, -202, -239, -241, -240, -281, -205, -284, -252,
+ -257, -288, -259, -319, -259, -309, -239, -302, -232, -305, -231, -282,
+ -235, -251, -227, -244, -183, -227, -177, -216, -169, -213, -156, -216,
+ -165, -230, -183, -214, -199, -211, -207, -215, -203, -236, -236, -213,
+ -269, -197, -284, -219, -271, -227, -256, -210, -245, -209, -247, -203,
+ -231, -195, -235, -194, -248, -194, -245, -212, -237, -203, -241, -198,
+ -226, -200, -216, -221, -205, -209, -210, -188, -216, -146, -202, -108,
+ -186, -110, -157, -108, -129, -121, -125, -129, -118, -125, -104, -120,
+ -104, -95, -101, -78, -84, -57, -71, -28, -65, -16, -83, -25,
+ -68, -41, -63, -42, -59, -29, -46, -33, -54, -48, -58, -50,
+ -85, -80, -93, -89, -117, -98, -103, -82, -104, -94, -118, -105,
+ -87, -86, -85, -96, -100, -64, -69, -48, -63, -65, -45, -15,
+ -39, -27, -55, -31},
+ /* IRC_Composite_C_R0195_T240_P000.wav */
+ {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, 1, -1,
+ 1, -1, 1, -1, 2, -2, 2, -3, 5, -9, -97, -133,
+ -196, -147, -157, -47, -93, 37, -29, 1, 0, 59, -116, -55,
+ 150, 274, 295, 91, 361, 1004, 1112, 1011, 1324, 1700, 2156, 1559,
+ 331, 574, 1032, 523, 614, 629, 882, 990, 1087, 1066, 943, 977,
+ 621, 691, 470, 536, 382, 281, 280, 209, 295, 117, 203, 196,
+ 393, 300, 250, 185, 141, 127, -15, -42, -136, -73, -208, -111,
+ -261, -80, -157, -42, -115, -38, -121, -105, -155, -167, -199, -209,
+ -235, -206, -227, -259, -253, -267, -251, -281, -214, -260, -213, -271,
+ -234, -259, -192, -277, -204, -294, -201, -341, -226, -327, -238, -309,
+ -206, -263, -189, -240, -185, -208, -186, -211, -192, -209, -158, -223,
+ -155, -231, -165, -251, -159, -233, -168, -259, -189, -266, -206, -255,
+ -200, -268, -211, -290, -218, -286, -233, -302, -227, -283, -216, -289,
+ -207, -262, -193, -248, -196, -236, -182, -219, -173, -221, -200, -230,
+ -207, -207, -205, -216, -209, -217, -217, -212, -207, -184, -207, -178,
+ -189, -131, -147, -115, -129, -101, -99, -95, -93, -92, -108, -91,
+ -105, -77, -114, -73, -104, -68, -90, -52, -74, -36, -70, -50,
+ -80, -45, -81, -54, -78, -26, -78, -29, -94, -58, -114, -94,
+ -144, -115, -134, -101, -99, -82, -78, -81, -65, -58, -71, -110,
+ -125, -130, -133, -109, -102, -88, -107, -78, -90, -39, -58, -31,
+ -64, -17, -39, -13},
+ /* IRC_Composite_C_R0195_T255_P000.wav */
+ {2, -2, 2, -2, 2, -2, 2, -2, 2, -2, 2, -2,
+ 2, -2, 2, -2, 2, -2, 2, -2, 2, -3, 3, -3,
+ 3, -3, 3, -3, 3, -3, 4, -4, 4, -5, 5, -6,
+ 9, -19, -71, -126, -111, -131, -223, -213, -179, -73, -136, -101,
+ -19, 30, 101, 69, 89, 95, 323, 406, 515, 721, 768, 1140,
+ 1389, 925, 919, 1504, 1350, 1027, 1508, 1800, 2083, 1870, 1354, 950,
+ 539, 490, 494, 392, 378, 293, 344, 461, 473, 522, 375, 344,
+ 271, 237, 162, 56, 42, 11, 83, -5, 55, -30, 81, -3,
+ 71, -18, -50, 0, -151, -66, -240, -143, -251, -198, -256, -250,
+ -192, -239, -205, -271, -195, -240, -195, -267, -238, -257, -227, -242,
+ -241, -251, -226, -256, -185, -246, -212, -252, -224, -227, -235, -216,
+ -249, -192, -222, -170, -209, -167, -231, -184, -255, -188, -250, -181,
+ -283, -185, -279, -146, -264, -164, -273, -161, -261, -181, -298, -216,
+ -272, -226, -251, -181, -242, -222, -248, -216, -260, -249, -302, -271,
+ -289, -255, -276, -256, -246, -225, -218, -198, -202, -222, -210, -223,
+ -219, -210, -230, -209, -218, -149, -175, -141, -182, -152, -139, -146,
+ -160, -169, -152, -140, -120, -129, -130, -102, -120, -59, -92, -67,
+ -121, -84, -93, -62, -69, -69, -85, -101, -60, -94, -80, -123,
+ -92, -126, -73, -83, -80, -90, -79, -71, -42, -42, -54, -59,
+ -77, -88, -83, -86, -100, -126, -130, -119, -130, -124, -145, -127,
+ -148, -111, -124, -92, -85, -75, -85, -70, -68, -66, -66, -59,
+ -78, -67, -59, -65},
+ /* IRC_Composite_C_R0195_T270_P000.wav */
+ {-1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1,
+ -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1,
+ -1, 1, -1, 1, -1, 1, -1, 2, -2, 2, -2, 3,
+ -4, 9, -91, -159, -163, -111, -130, -163, -90, -102, -89, -163,
+ -184, -188, -171, -83, 30, 36, 169, 298, 65, 214, 405, 559,
+ 898, 896, 1271, 2043, 2157, 2404, 2912, 2261, 1344, 1103, 1268, 1220,
+ 756, 484, 263, 283, 379, 505, 398, 413, 455, 527, 338, 482,
+ 334, 406, 255, 187, 211, 119, 189, 4, 182, -16, 145, -56,
+ 26, -74, -83, -100, -148, -133, -197, -139, -191, -198, -217, -194,
+ -192, -215, -224, -197, -182, -180, -242, -199, -243, -153, -261, -180,
+ -264, -151, -250, -164, -239, -176, -235, -178, -231, -191, -258, -227,
+ -259, -218, -280, -188, -268, -159, -259, -154, -260, -137, -244, -185,
+ -245, -228, -251, -260, -250, -247, -239, -233, -209, -205, -224, -262,
+ -274, -268, -248, -262, -263, -277, -234, -255, -222, -233, -226, -262,
+ -221, -257, -211, -267, -218, -251, -186, -221, -179, -224, -195, -213,
+ -161, -226, -193, -212, -178, -219, -191, -204, -183, -190, -175, -182,
+ -165, -164, -140, -131, -97, -107, -80, -102, -45, -85, -60, -81,
+ -66, -74, -59, -82, -82, -91, -88, -102, -100, -120, -85, -122,
+ -72, -129, -98, -137, -55, -101, -62, -69, -41, -72, -56, -83,
+ -69, -90, -53, -98, -81, -105, -80, -107, -83, -112, -102, -115,
+ -97, -125, -111, -136, -123, -145, -118, -131, -95, -114, -62, -70,
+ -59, -73, -48, -57},
+ /* IRC_Composite_C_R0195_T285_P000.wav */
+ {-1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1,
+ -1, 1, -1, 1, -1, 1, -1, 2, -2, 2, -2, 2,
+ -2, 2, -2, 2, -2, 2, -2, 2, -3, 3, -4, 6,
+ -9, 19, -100, -184, -163, -233, -161, -194, -211, -190, -252, -368,
+ -121, 58, 37, -12, 35, 111, -96, -223, -19, 464, 559, 1425,
+ 2382, 1927, 1709, 2204, 1586, 984, 1062, 1267, 1442, 1113, 1078, 1182,
+ 1130, 1148, 848, 575, 534, 584, 541, 405, 456, 300, 346, 294,
+ 405, 279, 183, 240, 258, 176, 160, 100, -27, 100, -24, 93,
+ -139, 0, -270, 60, -268, 20, -249, -123, -250, -178, -127, -185,
+ -84, -288, -109, -237, -116, -161, -179, -137, -193, -134, -189, -164,
+ -211, -174, -211, -244, -187, -259, -227, -263, -160, -220, -206, -234,
+ -213, -214, -205, -225, -212, -266, -182, -254, -229, -238, -166, -272,
+ -223, -282, -263, -305, -287, -272, -261, -288, -239, -247, -227, -247,
+ -224, -236, -226, -259, -230, -256, -220, -217, -226, -224, -232, -214,
+ -225, -205, -261, -251, -252, -219, -235, -230, -230, -236, -236, -209,
+ -203, -226, -221, -222, -180, -160, -190, -161, -159, -154, -156, -97,
+ -111, -129, -127, -107, -66, -95, -69, -92, -64, -55, -64, -43,
+ -78, -65, -103, -71, -87, -110, -101, -114, -93, -117, -90, -112,
+ -88, -109, -72, -82, -69, -68, -63, -65, -90, -69, -67, -77,
+ -62, -56, -47, -91, -59, -73, -34, -101, -70, -91, -61, -105,
+ -91, -128, -154, -149, -128, -150, -140, -142, -93, -111, -108, -78,
+ -62, -77, -59, -62},
+ /* IRC_Composite_C_R0195_T300_P000.wav */
+ {4, -4, 4, -4, 4, -4, 4, -4, 4, -5, 5, -5,
+ 5, -5, 5, -5, 5, -6, 6, -6, 6, -6, 6, -7,
+ 7, -7, 8, -8, 8, -9, 10, -11, 13, -16, 24, -75,
+ -192, -243, -206, -121, -200, -413, -335, -275, -203, -85, -38, 193,
+ -95, -382, 283, -596, -531, 1085, 630, 1024, 2620, 2194, 1518, 1499,
+ 1392, 607, 112, 846, 1164, 950, 675, 1005, 1305, 1174, 1521, 1142,
+ 1208, 859, 1074, 771, 735, 552, 498, 679, 274, 454, 160, 457,
+ 127, 176, 172, 280, 101, 64, 67, -27, 98, 11, 3, -150,
+ -68, -72, -105, -163, -160, -139, -135, -141, -133, -136, -55, -184,
+ -109, -200, -81, -104, -78, -213, -147, -100, -188, -136, -214, -162,
+ -281, -139, -265, -190, -228, -184, -320, -138, -205, -130, -238, -158,
+ -209, -100, -297, -175, -328, -233, -318, -199, -385, -256, -355, -254,
+ -336, -269, -310, -262, -286, -237, -272, -206, -279, -217, -262, -212,
+ -280, -260, -296, -269, -321, -249, -293, -297, -294, -263, -252, -259,
+ -248, -231, -189, -223, -204, -205, -214, -202, -172, -186, -170, -216,
+ -162, -160, -149, -167, -137, -119, -139, -138, -127, -91, -90, -111,
+ -123, -105, -94, -88, -92, -101, -104, -94, -87, -104, -82, -106,
+ -68, -106, -75, -113, -53, -76, -65, -86, -59, -75, -81, -73,
+ -84, -59, -75, -52, -59, -73, -55, -69, -45, -87, -49, -62,
+ -47, -76, -53, -57, -57, -74, -83, -72, -109, -94, -135, -114,
+ -127, -112, -128, -125, -121, -104, -114, -94, -97, -102, -114, -98,
+ -73, -61, -46, -72},
+ /* IRC_Composite_C_R0195_T315_P000.wav */
+ {-3, 3, -3, 3, -3, 3, -3, 3, -3, 3, -3, 3,
+ -3, 3, -3, 3, -3, 3, -4, 4, -4, 4, -4, 4,
+ -5, 5, -5, 6, -7, 8, -10, 14, -29, -234, -230, -266,
+ -204, -305, -247, -524, -359, -150, 55, 142, 71, 572, -570, -1004,
+ 861, -803, -374, 3189, 1120, 2120, 3144, 2428, 636, 1026, 1269, 59,
+ -187, 571, 1369, 181, 332, 532, 1375, 1240, 1554, 1033, 1045, 866,
+ 1172, 1026, 1022, 606, 720, 597, 671, 349, 376, 201, 259, 47,
+ 222, 146, 93, -124, 20, -35, -174, -23, 6, -91, -108, -18,
+ 6, 38, -2, -118, 67, -77, -79, -121, -96, -206, -171, -74,
+ -193, -141, -167, -54, -168, -65, -180, -137, -281, -209, -164, -172,
+ -178, -158, -127, -188, -174, -11, -152, -142, -222, -182, -348, -203,
+ -338, -287, -380, -316, -332, -319, -347, -297, -281, -364, -362, -314,
+ -304, -376, -347, -321, -343, -330, -367, -276, -306, -278, -290, -268,
+ -295, -267, -229, -264, -248, -290, -244, -263, -201, -223, -197, -193,
+ -189, -154, -159, -137, -178, -126, -217, -115, -163, -129, -166, -130,
+ -150, -147, -107, -147, -129, -143, -117, -141, -126, -115, -135, -99,
+ -146, -90, -112, -81, -119, -52, -80, -79, -84, -83, -64, -59,
+ -53, -89, -91, -74, -101, -29, -121, -49, -139, -64, -94, -23,
+ -57, -36, -29, -38, -50, -50, -47, -30, -53, -56, -81, -58,
+ -97, -62, -69, -56, -82, -74, -103, -94, -111, -122, -150, -117,
+ -148, -135, -117, -104, -87, -95, -77, -107, -55, -89, -56, -82,
+ -65, -70, -36, -13},
+ /* IRC_Composite_C_R0195_T330_P000.wav */
+ {-15, 15, -15, 15, -15, 15, -15, 16, -16, 16, -16, 16,
+ -16, 16, -16, 16, -16, 16, -16, 16, -16, 15, -15, 14,
+ -13, 11, -8, 5, 0, -7, 12, -293, -526, -71, -505, -401,
+ -1003, -83, -138, 277, -121, 450, 638, -529, -2538, 1974, -1021, -928,
+ 6018, 1862, 2117, 4170, 3177, -540, 80, 773, 465, -382, 760, 707,
+ 287, -96, 895, 1260, 1490, 749, 709, 1138, 1758, 1405, 855, 490,
+ 661, 833, 888, 382, 366, 310, 491, 203, 204, 155, 246, 122,
+ -32, -122, -37, 50, 94, -114, -242, -173, 94, -97, -233, -239,
+ 44, 3, 91, -64, -78, -19, 148, 9, 2, -196, 62, -119,
+ -154, -277, 128, -102, -103, -182, -189, -136, -49, 51, -260, -285,
+ -17, -46, -344, -260, -264, -310, -481, -356, -385, -299, -442, -338,
+ -388, -433, -413, -352, -281, -444, -379, -419, -361, -361, -316, -320,
+ -407, -371, -354, -283, -325, -263, -281, -264, -300, -242, -200, -218,
+ -231, -208, -283, -185, -218, -137, -254, -173, -210, -124, -178, -189,
+ -155, -197, -150, -148, -83, -183, -134, -157, -104, -154, -134, -133,
+ -118, -172, -161, -120, -119, -156, -145, -96, -143, -172, -117, -71,
+ -107, -120, -113, -53, -111, -85, -65, -51, -97, -63, -15, -77,
+ -94, -67, -18, -45, -126, -60, -101, -21, -115, 3, -114, -45,
+ -90, -5, -69, -55, -89, -42, -99, -38, -100, -24, -95, -21,
+ -116, -37, -113, -13, -85, -64, -143, -57, -122, -61, -126, -85,
+ -123, -78, -94, -71, -113, -55, -72, -37, -93, -41, -42, -10,
+ -34, -37, -13, -34},
+ /* IRC_Composite_C_R0195_T345_P000.wav */
+ {-1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1,
+ 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1,
+ -1, 1, -2, 2, -3, -196, -727, -30, -506, -136, -1125,
+ -612, -692, -135, 283, 619, 802, -51, -2221, 1398, -964, -5308,
+ 8059, 961, 2013, 8066, 4451, -858, 1438, 1808, -955, -2183, 475,
+ 1387, 742, 458, 140, 184, 1030, 1768, 1316, 965, 1001, 1155,
+ 1147, 1222, 1301, 1124, 461, 289, 297, 592, 383, 273, -220,
+ -259, -53, 323, 264, -32, -200, 42, 191, 421, 77, -195,
+ -257, 18, -154, -38, -140, -135, -290, -47, 107, 49, -29,
+ -98, -94, -50, 49, 264, 36, 153, -148, 86, -24, 287,
+ -156, 101, -86, 125, -186, -182, -278, -236, -501, -636, -484,
+ -401, -330, -428, -463, -554, -283, -366, -318, -494, -441, -375,
+ -318, -358, -465, -416, -438, -337, -400, -344, -393, -247, -308,
+ -313, -356, -181, -229, -204, -374, -214, -320, -200, -307, -179,
+ -339, -189, -252, -152, -246, -143, -210, -166, -196, -69, -139,
+ -172, -216, -103, -73, -127, -214, -116, -118, -96, -247, -135,
+ -187, -78, -264, -166, -235, -95, -173, -142, -167, -169, -129,
+ -117, -114, -125, -125, -103, -137, -47, -110, -26, -121, -21,
+ -72, -15, -71, -68, -34, -53, -38, -72, -96, -96, -90,
+ -11, -126, -33, -166, -22, -92, 18, -75, -35, -69, -60,
+ -67, -82, -72, -61, -96, -68, -103, -26, -100, -19, -108,
+ 0, -110, -34, -106, -39, -54, -75, -76, -83, -35, -42,
+ -69, -51, -59, -38, -55, 19, -49, -3, -43, 27, -14,
+ -5, -46, -24}};
+
+const int16_t irc_composite_c_r0195_p015[][256] =
+ {/* IRC_Composite_C_R0195_T000_P015.wav */
+ {4, -4, 4, -4, 4, -4, 4, -4, 4, -4, 4,
+ -5, 5, -5, 4, -4, 4, -4, 3, -2, 1, 2,
+ -5, -101, -665, -424, -897, -170, -507, -460, -1001, -828, -356,
+ 47, 474, 980, 404, 2103, -3700, -5968, 7765, -9133, 9741, 10867,
+ 71, 6925, 3456, -1455, -2458, 2718, -170, -1201, -1751, 1809, 507,
+ 632, -1165, 1945, 1684, 1705, -450, 1152, 360, 1527, 269, 952,
+ 644, 931, 727, 1576, 725, 835, 741, 915, 186, 162, 327,
+ 384, 20, 24, -311, 60, 225, 484, -123, -28, -210, 118,
+ 15, 76, -335, -153, -249, 49, -120, 24, -200, -169, -213,
+ -175, -235, -158, -280, -399, -410, -304, -104, -355, -319, -311,
+ -79, -366, -75, -79, -120, -487, 147, -42, -198, -175, 135,
+ -242, 55, 233, 123, -314, -58, -179, -96, -352, -26, 7,
+ 11, -105, -129, 125, -110, -293, -276, -461, -507, -406, -422,
+ -497, -481, -446, -459, -325, -336, -443, -420, -320, -391, -360,
+ -309, -407, -375, -288, -289, -385, -315, -272, -292, -271, -256,
+ -318, -173, -220, -185, -329, -142, -239, -76, -219, -117, -277,
+ -107, -204, -117, -233, -199, -220, -177, -238, -171, -170, -187,
+ -285, -154, -168, -145, -192, -131, -239, -194, -170, -109, -205,
+ -155, -164, -163, -187, -126, -85, -138, -195, -140, -189, -84,
+ -134, -22, -177, -51, -128, -26, -105, 26, -144, -65, -123,
+ -28, -136, 19, -69, -43, -112, 40, -43, 59, -67, 32,
+ -35, 97, -21, 99, -27, 90, 23, 141, 21, 38, 39,
+ 61, 12, 10, -1, 37, -3, 7, 2, 41, -19, -12,
+ -12, 16, -53},
+ /* IRC_Composite_C_R0195_T015_P015.wav */
+ {100, -101, 102, -102, 102, -102, 102, -102, 101, -99, 96,
+ -93, 88, -81, 71, -58, 37, -6, -46, 154, -587, -280,
+ -52, -712, -530, -842, -228, -637, -217, -1535, -463, 274, 2688,
+ 1298, -390, -2760, 4349, -16532, 13572, -2117, 2580, 22789, -850, 559,
+ 1346, 2618, -4202, 397, 316, 686, -1654, 1356, -848, 1484, 164,
+ 1873, 376, 1720, 39, 1069, 309, 1623, 748, 807, 488, 1192,
+ 547, 1364, 679, 594, 48, 334, -179, 362, 98, -149, -587,
+ -275, -81, 371, -430, -372, -369, 412, -126, -39, -294, 169,
+ -117, 61, -209, -100, -271, -40, -262, -244, -406, 12, -243,
+ -253, -484, 54, -243, -283, -575, -138, -358, -262, -307, -61,
+ -567, -206, 25, 51, -696, -10, -132, -245, -388, 187, -132,
+ -194, -316, -92, -71, -291, -374, 27, -295, -84, -4, 145,
+ -576, 122, -217, -153, -724, 53, -337, -126, -502, -305, -401,
+ -138, -372, -296, -502, -318, -375, -140, -645, -283, -375, -217,
+ -529, -172, -316, -324, -287, -239, -309, -305, -230, -228, -310,
+ -332, -289, -168, -302, -286, -241, -197, -266, -181, -156, -260,
+ -272, -185, -148, -225, -221, -239, -178, -223, -142, -203, -226,
+ -260, -124, -148, -200, -259, -124, -203, -182, -207, -78, -186,
+ -163, -136, -74, -179, -123, -45, -87, -180, -103, -28, -117,
+ -135, -64, -29, -148, -90, -13, -28, -111, -40, 7, -42,
+ -98, -26, -4, -57, -22, 22, -38, -72, 76, 67, 1,
+ -21, 80, 49, 39, 63, 66, 26, 23, 62, 97, 29,
+ 39, 49, 28, -1, 52, 74, -6, -4, 19, 15, -15,
+ -12, 17, -50},
+ /* IRC_Composite_C_R0195_T030_P015.wav */
+ {-101, 102, -103, 105, -105, 106, -106, 106, -104, 102, -97,
+ 90, -79, 62, -35, -12, 109, -431, -649, 311, -951, 117,
+ -1193, 10, -1439, 943, -1996, 57, -1866, 3892, -160, 3817, -4940,
+ 7695, -14389, 320, 6811, -6312, 29122, 1548, 2820, 2754, 2735, -5703,
+ -20, 838, 313, -2387, 2394, -1371, 1731, 262, 1937, 34, 1456,
+ -294, 1713, 221, 997, 660, 1535, 549, 1179, 928, 928, 293,
+ 321, -76, 273, -164, -356, -622, 24, -278, -26, -520, -116,
+ -436, -61, -412, -21, -436, -263, -534, -25, -24, 127, -328,
+ -77, -189, 107, -200, -313, -487, -278, -245, -413, -269, -216,
+ -100, -434, -149, -238, -270, -527, -94, -144, -244, -529, -94,
+ -120, -125, -544, -205, -477, -214, -375, -52, -404, -184, -361,
+ -14, -189, -53, -346, -89, -276, -127, -233, -126, -417, -169,
+ -225, -263, -470, -227, -238, -276, -329, -339, -304, -226, -209,
+ -212, -255, -261, -275, -96, -168, -289, -298, -134, -296, -334,
+ -291, -248, -272, -268, -362, -278, -286, -108, -360, -215, -330,
+ -76, -394, -179, -365, -104, -449, -206, -420, -128, -409, -167,
+ -434, -175, -416, -105, -318, -125, -408, -157, -270, -45, -277,
+ -160, -326, -56, -220, -114, -280, -69, -199, -100, -242, -24,
+ -143, -39, -214, 3, -104, 20, -184, 20, -140, -7, -183,
+ 31, -135, -21, -164, 13, -128, 42, -188, 49, -113, 71,
+ -189, 90, -97, 102, -87, 84, -61, 147, -63, 149, -94,
+ 170, -108, 183, -108, 208, -150, 179, -163, 250, -98, 215,
+ -137, 235, -107, 214, -135, 246, -126, 201, -171, 186, -149,
+ 135, -194, 121},
+ /* IRC_Composite_C_R0195_T045_P015.wav */
+ {-146, 153, -161, 170, -179, 189, -200, 212, -224, 238, -252,
+ 266, -278, 280, -241, -262, -1208, 369, -877, 618, -1865, 344,
+ -1624, 1098, -2418, 1166, -2204, 3338, -287, 6351, -5670, 4490, -9471,
+ -5595, 10415, -3658, 26422, 6163, 571, 640, 3020, -3458, 345, 262,
+ -17, -2379, 1504, -915, 2155, 96, 1778, -36, 1408, 800, 1533,
+ 117, 618, 355, 1940, 474, 1443, 116, 436, -13, 320, -248,
+ -589, -590, -422, -164, -365, -343, -251, -474, 20, -343, -59,
+ -619, -421, -460, -72, -443, -399, -435, -238, -311, -325, -256,
+ -210, -285, -282, -222, -283, -212, -374, -178, -386, -213, -343,
+ -35, -317, -276, -390, -79, -244, -300, -445, -222, -296, -233,
+ -458, -263, -403, -259, -462, -188, -423, -283, -371, -78, -225,
+ -208, -380, -203, -156, -140, -307, -202, -208, -86, -293, -179,
+ -233, -185, -390, -305, -381, -170, -416, -344, -426, -229, -331,
+ -201, -370, -183, -273, -88, -257, -116, -206, -60, -298, -72,
+ -233, -115, -304, -131, -336, -187, -271, -44, -375, -190, -355,
+ -35, -321, -189, -400, -136, -365, -198, -383, -200, -395, -244,
+ -401, -188, -398, -184, -393, -179, -365, -150, -314, -118, -327,
+ -118, -291, -73, -272, -87, -247, -52, -258, -27, -226, 25,
+ -210, 8, -202, 51, -136, 62, -162, 35, -135, 61, -156,
+ -12, -167, 55, -165, 15, -174, 47, -131, 28, -125, 76,
+ -91, 69, -71, 126, -60, 104, -40, 141, -51, 117, -33,
+ 141, -58, 103, -38, 158, -48, 95, -52, 149, -45, 86,
+ -47, 120, -68, 88, -39, 116, -45, 74, -35, 89, -68,
+ 62, -90, 53},
+ /* IRC_Composite_C_R0195_T060_P015.wav */
+ {29, -34, 39, -45, 52, -59, 69, -80, 93, -110, 131,
+ -160, 205, -308, -459, -920, -171, 388, -674, -719, -1064, 350,
+ -1149, 552, -1891, 2109, -28, 5773, -3032, 5413, -11840, 194, -1388,
+ 1646, 23561, 8535, 2547, 1107, -66, 257, 913, 517, -131, -1162,
+ -856, 344, 2136, -879, 1332, -592, 2048, 385, 1977, 174, 2217,
+ -131, 2091, -69, 1390, -1343, 963, -905, 515, -1176, -81, -1557,
+ 250, -301, -119, -467, -479, -337, 138, -523, -373, -525, -22,
+ -558, -74, -857, -193, -765, -208, -1034, -15, -947, -96, -803,
+ 45, -665, 15, -523, -1, -312, 49, -446, -141, -311, -126,
+ -246, -344, -467, -27, -388, -328, -427, -198, -495, -118, -356,
+ -277, -493, -134, -525, -71, -411, -262, -445, -175, -367, -96,
+ -400, -267, -383, -49, -313, -260, -205, -179, -166, -56, -272,
+ -194, -146, -166, -310, -204, -405, -207, -321, -211, -468, -223,
+ -383, -150, -326, -281, -312, -146, -220, -208, -241, -286, -189,
+ -157, -134, -243, -213, -136, -145, -92, -188, -205, -244, -73,
+ -253, -131, -321, -224, -277, -194, -364, -238, -342, -254, -284,
+ -301, -272, -246, -239, -245, -226, -236, -207, -220, -191, -251,
+ -189, -226, -188, -154, -195, -171, -163, -122, -121, -97, -111,
+ -91, -108, -20, -120, -59, -33, -136, -42, -55, -62, -49,
+ -97, -100, 16, -97, -13, -104, 7, -74, -20, -54, 27,
+ -6, 45, -12, 56, 35, 24, 86, 33, 48, 24, 68,
+ 72, -3, 50, 22, 52, 12, 43, -32, 109, -68, 106,
+ -66, 49, -61, 102, -74, 44, -72, 28, -60, 21, -71,
+ -4, -76, 16},
+ /* IRC_Composite_C_R0195_T075_P015.wav */
+ {19, -16, 12, -7, 2, 4, -11, 18, -27, 36, -47,
+ 59, -68, 37, -553, 36, -916, -89, -13, -916, -389, -1224,
+ 572, -284, 1453, 853, 1354, 3254, 1573, -10498, -1005, -5881, 20609,
+ 10333, 4130, 9878, -3085, -1702, 1001, 5191, 773, -2104, -1269, -41,
+ 1645, -349, 309, 387, 447, 1304, 620, 1533, 360, 1697, 867,
+ 1545, -94, 149, -359, 473, -1422, -432, -1225, -246, -886, 186,
+ -914, -144, -892, 309, -602, 60, -603, -112, -520, -150, -750,
+ -486, -681, -262, -736, -448, -1017, -103, -734, -305, -903, -61,
+ -752, 39, -644, -19, -565, 33, -324, -70, -458, -205, -229,
+ -182, -403, -322, -245, -211, -302, -350, -295, -222, -298, -293,
+ -385, -263, -357, -235, -379, -252, -470, -139, -429, -84, -457,
+ -178, -385, -120, -380, -232, -339, -192, -273, -205, -196, -146,
+ -164, -174, -129, -218, -203, -145, -229, -290, -273, -262, -283,
+ -222, -302, -215, -314, -150, -291, -151, -311, -154, -281, -177,
+ -260, -179, -190, -177, -222, -159, -209, -215, -215, -185, -191,
+ -224, -214, -221, -182, -209, -269, -260, -254, -234, -284, -241,
+ -293, -201, -269, -218, -264, -188, -203, -199, -218, -207, -118,
+ -151, -146, -184, -126, -92, -92, -129, -108, -109, -42, -120,
+ -57, -144, -45, -102, -23, -164, -63, -98, -36, -106, -81,
+ -3, -74, -55, -79, -49, -60, -59, -60, -31, -36, -35,
+ 57, -37, 99, -7, 135, -26, 115, 14, 154, -6, 78,
+ 28, 119, -3, 53, -48, 80, -38, 55, -56, 13, -65,
+ 74, -45, 19, -79, 50, -39, 24, -95, 40, -41, 15,
+ -61, 55, -129},
+ /* IRC_Composite_C_R0195_T090_P015.wav */
+ {106, -108, 111, -113, 116, -119, 121, -123, 125, -125, 121,
+ -102, -150, -386, -21, -846, -117, 203, -762, -759, -425, -20,
+ 26, 535, 183, 3678, 866, 180, 877, -12284, 4742, -888, 21444,
+ 10158, -2546, 4612, -145, -630, 3778, 3819, 1940, -4361, -1372, 2229,
+ 1116, -1596, -423, 1713, 840, 536, 1040, 2123, 618, 888, -302,
+ 898, -618, 52, -845, 240, -1279, -435, -1143, 105, -929, -50,
+ -1077, -180, -541, 98, -930, -161, -671, -158, -656, -338, -774,
+ -263, -620, -260, -792, -238, -787, -172, -874, -235, -797, -199,
+ -767, -178, -702, -101, -607, -63, -479, -72, -504, -49, -400,
+ -118, -387, -113, -404, -73, -442, -100, -429, -15, -496, -79,
+ -480, -58, -494, -80, -498, -112, -411, -138, -447, -136, -406,
+ -145, -485, -166, -440, -131, -485, -87, -417, -104, -413, -102,
+ -426, -31, -350, -62, -299, 27, -272, -36, -302, -22, -320,
+ -120, -364, -124, -307, -111, -382, -185, -231, -108, -275, -163,
+ -225, -123, -157, -158, -254, -183, -208, -146, -274, -183, -229,
+ -148, -301, -148, -275, -212, -388, -177, -304, -193, -358, -180,
+ -265, -195, -243, -171, -184, -212, -200, -212, -156, -173, -149,
+ -232, -145, -162, -100, -147, -119, -102, -57, -53, -87, -18,
+ -36, -11, -87, -46, -46, 17, -89, -82, -57, 6, -100,
+ -61, -101, -38, -60, -46, -108, -40, -41, 19, -104, -21,
+ -14, 77, -24, 51, 22, 136, 25, 103, 19, 102, 3,
+ 92, -42, 26, -51, 25, -38, -15, -65, -12, -35, -8,
+ 28, 24, -31, -1, 36, 92, 2, 6, 8, 52, -33,
+ -7, -25, -27},
+ /* IRC_Composite_C_R0195_T105_P015.wav */
+ {16, -19, 23, -28, 34, -40, 48, -57, 68, -82, 100,
+ -121, 33, -823, 199, -561, -96, -481, 741, -1752, -67, -480,
+ 1840, -966, 591, 1601, 5280, -3652, 770, -7901, 214, 10553, 2446,
+ 18816, 1377, -3359, -1411, 6633, 5595, -115, -232, 1823, -2575, -717,
+ -242, 2719, -2255, 509, 511, 2747, 225, 1585, 549, 951, -478,
+ -34, -731, -13, -1246, -512, -881, -341, -1127, 207, -442, -495,
+ -702, -517, -566, 35, -620, -983, -450, -179, -571, -441, -658,
+ -297, -211, -340, -730, -514, -334, -479, -562, -662, -463, -524,
+ -336, -537, -522, -336, -226, -483, -343, -332, -364, -181, -306,
+ -365, -271, -175, -316, -213, -283, -326, -115, -221, -216, -290,
+ -146, -290, -98, -334, -301, -279, -138, -349, -278, -318, -250,
+ -285, -237, -388, -326, -257, -206, -416, -256, -293, -207, -350,
+ -156, -397, -149, -314, -125, -323, -153, -327, -64, -258, -145,
+ -241, -10, -249, -51, -197, -108, -234, 33, -286, -138, -267,
+ -26, -273, -117, -347, -77, -229, -117, -301, -119, -229, -104,
+ -247, -175, -311, -162, -227, -248, -287, -181, -293, -170, -318,
+ -206, -285, -159, -334, -167, -232, -161, -235, -76, -246, -154,
+ -204, -10, -221, -106, -229, 14, -182, -12, -251, 30, -154,
+ 32, -190, 61, -152, 90, -145, 48, -122, 71, -96, 29,
+ -121, 6, -57, 66, -80, 40, -49, 59, -112, -8, -129,
+ 54, -18, 76, -21, 97, 37, 171, 23, 59, -61, 75,
+ -37, 21, -127, 1, -154, 68, -112, -12, -180, 86, -58,
+ 91, -120, 128, -28, 129, -100, 167, -17, 106, -84, 94,
+ -49, 76, -37},
+ /* IRC_Composite_C_R0195_T120_P015.wav */
+ {39, -39, 39, -39, 39, -38, 38, -37, 35, -32, 27,
+ -19, 0, -223, 40, -675, 329, -841, 457, -348, -274, -1647,
+ 1770, 432, -494, 3, 4260, 289, 887, -4454, -3947, 7446, -2378,
+ 15614, 7773, 95, -2028, 2409, 7171, 2953, 950, 1439, -1677, -1118,
+ -1770, 1575, 491, 137, -1226, 1181, 2451, 1325, -149, 862, 100,
+ -196, -613, -302, -1148, -1325, -632, -731, -1011, -688, 174, -67,
+ -430, -378, -168, -383, -445, -570, -934, -576, -71, -333, -939,
+ -576, -180, -299, -551, -542, -536, -312, -421, -506, -398, -463,
+ -360, -423, -342, -463, -390, -485, -299, -416, -291, -519, -227,
+ -313, -272, -376, -199, -241, -176, -354, -259, -245, -94, -289,
+ -288, -266, -79, -203, -210, -332, -200, -224, -227, -331, -258,
+ -315, -219, -293, -232, -358, -231, -306, -201, -298, -234, -413,
+ -216, -295, -220, -417, -228, -272, -126, -326, -197, -254, -104,
+ -275, -161, -280, -75, -215, -110, -276, -33, -130, -19, -261,
+ -8, -171, 30, -242, -55, -229, -65, -254, -74, -218, -134,
+ -301, -99, -211, -155, -300, -156, -234, -131, -322, -164, -311,
+ -142, -349, -229, -334, -148, -297, -165, -289, -133, -215, -100,
+ -254, -101, -185, -51, -234, -38, -186, -6, -168, 0, -111,
+ -29, -125, 29, -96, -23, -153, 22, -59, -32, -103, 28,
+ -118, 11, -64, 83, -103, 41, -112, 28, -109, 99, -41,
+ 39, -86, 157, 43, 121, -15, 157, -11, 121, -30, 90,
+ -113, 25, -109, 38, -172, -47, -151, 15, -102, 21, -97,
+ 16, -33, 104, 12, 59, -55, 121, -5, 63, -116, 114,
+ -30, 115, -85},
+ /* IRC_Composite_C_R0195_T135_P015.wav */
+ {15, -17, 19, -21, 24, -28, 32, -37, 43, -50, 60,
+ -73, 90, -115, 157, -297, 361, -673, 287, -116, -598, -544,
+ 1021, -902, -263, 1113, 1563, -848, 3395, 810, -1816, -1445, -4755,
+ 10984, 3295, 5362, 6262, 2589, -529, 1444, 7698, 4758, -2233, -2138,
+ -1622, 980, -226, 100, -517, 871, 952, 682, 983, 1138, -542,
+ -341, -164, -377, -526, -992, -910, -828, -413, -830, -380, -392,
+ -124, -316, -225, -315, -25, -678, -691, -468, -412, -545, -395,
+ -676, -301, -467, -356, -548, -300, -516, -260, -553, -295, -485,
+ -232, -525, -220, -421, -217, -488, -227, -455, -268, -426, -269,
+ -474, -232, -395, -199, -378, -219, -372, -176, -401, -202, -354,
+ -114, -340, -124, -267, -86, -322, -166, -298, -181, -316, -179,
+ -307, -163, -290, -175, -321, -186, -325, -234, -372, -200, -335,
+ -202, -352, -199, -268, -180, -319, -236, -296, -200, -331, -209,
+ -300, -142, -273, -118, -226, -91, -203, -97, -250, -91, -176,
+ -72, -201, -65, -156, -22, -171, -59, -200, -36, -210, -93,
+ -201, -48, -197, -73, -185, -73, -195, -95, -266, -166, -294,
+ -169, -299, -216, -315, -187, -265, -167, -287, -146, -275, -175,
+ -265, -144, -282, -127, -249, -66, -222, -39, -146, -32, -126,
+ -20, -76, 5, -92, 18, -56, 21, -91, 13, -119, -23,
+ -127, 37, -71, 16, -116, 10, -105, 12, -96, 55, -69,
+ 43, -76, 137, -20, 86, -3, 123, 28, 100, 8, 49,
+ -58, 60, -31, -10, -117, -15, -44, -19, -89, 2, -51,
+ -18, -51, 62, -39, 31, -46, 59, -59, 67, -57, 112,
+ -72, 62, -22},
+ /* IRC_Composite_C_R0195_T150_P015.wav */
+ {22, -23, 24, -24, 25, -26, 27, -27, 28, -28, 28,
+ -28, 26, -24, 19, -9, -13, 134, -205, 5, 10, -146,
+ -122, -75, -78, -152, 345, -100, 1711, 198, 1882, 367, 2730,
+ -6333, 870, 5933, 2367, 6468, 4276, 4587, 970, 719, 5822, 3692,
+ -1207, -2971, -1058, 797, 482, -202, -740, 507, 536, 875, 480,
+ 668, -204, -211, -690, 500, -1107, -874, -841, -223, -910, -122,
+ -465, -407, -494, -192, -244, -202, -337, -294, -451, -519, -512,
+ -353, -580, -345, -407, -307, -608, -176, -450, -303, -475, -205,
+ -453, -277, -342, -237, -284, -212, -314, -272, -380, -283, -368,
+ -307, -417, -297, -337, -321, -374, -329, -283, -379, -283, -310,
+ -317, -249, -232, -217, -269, -190, -252, -183, -270, -238, -227,
+ -187, -286, -191, -260, -230, -245, -233, -289, -230, -246, -218,
+ -241, -208, -260, -204, -276, -211, -286, -243, -284, -211, -281,
+ -227, -258, -199, -238, -185, -241, -133, -220, -133, -189, -80,
+ -190, -65, -146, -88, -167, -80, -129, -110, -166, -124, -142,
+ -139, -178, -111, -146, -116, -143, -102, -118, -138, -137, -152,
+ -173, -190, -174, -190, -193, -230, -208, -208, -216, -246, -203,
+ -231, -209, -216, -167, -210, -170, -158, -128, -148, -142, -127,
+ -104, -88, -98, -82, -75, -58, -53, -46, -85, -53, -56,
+ -1, -14, 17, -80, -58, -41, -57, -6, -70, 6, -78,
+ 37, -14, 58, 10, 69, 35, 20, 45, 42, 27, 3,
+ 3, -29, -5, -10, 26, -54, -21, -21, 39, -58, -58,
+ -23, 10, -16, -35, 6, -22, 2, -8, 54, -13, 7,
+ -2, 51, 6},
+ /* IRC_Composite_C_R0195_T165_P015.wav */
+ {20, -21, 21, -22, 23, -23, 24, -25, 26, -26, 27, -28,
+ 29, -29, 30, -30, 29, -28, 22, -5, 163, -209, 156, -276,
+ 329, -325, 157, -151, 120, 93, 1138, 522, 872, 2060, 255, 503,
+ -4749, 3937, 3841, 3248, 5654, 4851, 3110, 1024, 1230, 4380, 981, -2433,
+ -2102, 345, 461, 292, -696, 50, 937, 951, -195, 227, 100, -111,
+ -682, -242, -295, -695, -542, -371, -167, -471, -237, -441, -313, -257,
+ -355, -428, -338, -330, -347, -256, -340, -455, -298, -395, -419, -365,
+ -319, -336, -313, -224, -306, -223, -245, -277, -258, -289, -268, -318,
+ -289, -331, -299, -363, -319, -390, -363, -384, -327, -342, -339, -372,
+ -297, -284, -259, -319, -264, -296, -233, -297, -258, -289, -255, -235,
+ -239, -231, -238, -221, -219, -255, -237, -261, -205, -265, -219, -253,
+ -180, -250, -196, -250, -200, -248, -207, -278, -194, -276, -219, -261,
+ -199, -283, -219, -241, -187, -217, -180, -196, -141, -180, -121, -175,
+ -110, -181, -79, -171, -84, -195, -62, -164, -99, -185, -115, -180,
+ -164, -155, -140, -152, -147, -164, -113, -154, -117, -178, -132, -201,
+ -137, -183, -162, -227, -171, -200, -151, -199, -165, -161, -150, -178,
+ -144, -157, -158, -179, -144, -176, -163, -168, -98, -168, -122, -152,
+ -52, -148, -86, -125, -9, -56, -37, -107, -79, -44, -26, -30,
+ -92, -55, -14, 29, 14, 24, 62, 67, 48, 30, 48, 10,
+ 57, -9, 40, -55, 25, -77, 17, -96, 25, -64, 21, -101,
+ -1, 4, 58, -21, 25, 24, 70, 3, 30, 9, 36, -16,
+ 6, -21, -39, -15},
+ /* IRC_Composite_C_R0195_T180_P015.wav */
+ {-9, 9, -9, 9, -10, 10, -10, 10, -11, 11, -11, 11,
+ -12, 12, -13, 13, -14, 15, -15, 16, -17, 19, -21, 50,
+ -66, 25, 121, 28, -120, 155, -168, 335, 416, 671, 222, 2210,
+ 806, 1236, -2882, 612, 2593, 898, 4860, 6139, 4465, 870, -196, 3886,
+ 2840, -2197, -1283, -67, 115, -435, 2, 221, 731, -37, -52, 179,
+ 744, -513, -450, -510, 328, -437, -225, -503, 1, -204, -76, -530,
+ -331, -379, -166, -435, -279, -284, -151, -266, -225, -384, -293, -305,
+ -154, -293, -219, -273, -134, -244, -264, -371, -311, -317, -245, -345,
+ -336, -343, -237, -329, -370, -441, -342, -336, -321, -387, -336, -319,
+ -260, -281, -304, -299, -268, -249, -331, -318, -306, -253, -254, -267,
+ -280, -263, -244, -262, -278, -297, -251, -213, -235, -246, -260, -197,
+ -223, -206, -272, -188, -228, -176, -268, -213, -279, -202, -229, -233,
+ -258, -216, -166, -186, -223, -208, -202, -136, -198, -161, -225, -147,
+ -174, -123, -192, -165, -169, -119, -164, -157, -169, -110, -156, -134,
+ -190, -131, -196, -117, -175, -127, -176, -95, -111, -96, -149, -112,
+ -153, -144, -212, -161, -218, -171, -217, -170, -191, -155, -154, -114,
+ -156, -141, -134, -77, -145, -152, -168, -124, -167, -164, -158, -156,
+ -173, -143, -139, -75, -112, -48, -151, -85, -139, -30, -81, -70,
+ -106, -105, -40, -41, 29, 15, 57, 54, 103, 128, 65, 65,
+ 35, 45, -4, -23, -56, -53, -39, -44, -11, -66, 11, -23,
+ 53, -3, 44, 26, 45, 23, 36, 66, 32, 13, 3, 39,
+ 11, -14, 8, -12},
+ /* IRC_Composite_C_R0195_T195_P015.wav */
+ {3, -3, 4, -4, 4, -5, 5, -5, 6, -6, 7, -7,
+ 8, -9, 9, -10, 11, -13, 14, -16, 19, -22, 27, -33,
+ 44, -87, 114, -132, 102, -154, 155, -97, -20, -37, -29, 363,
+ 412, 474, 853, 1336, 1092, -788, -1724, 2098, 1099, 2640, 4844, 4905,
+ 2895, -39, 1681, 2632, 175, -1400, -248, 325, 22, -206, 196, 419,
+ 561, 41, 192, 277, 401, -173, 2, -114, 106, -178, -24, -176,
+ 30, -91, 54, -173, -183, -124, -65, -136, -127, -106, -163, -59,
+ -115, -115, -267, -191, -291, -145, -314, -229, -397, -205, -300, -229,
+ -373, -215, -360, -218, -346, -234, -397, -220, -360, -257, -382, -254,
+ -389, -213, -322, -234, -324, -214, -345, -227, -381, -255, -363, -174,
+ -354, -207, -318, -230, -299, -263, -316, -271, -243, -219, -239, -195,
+ -263, -183, -262, -181, -310, -210, -258, -227, -229, -243, -215, -263,
+ -194, -269, -182, -235, -184, -209, -147, -196, -148, -196, -147, -207,
+ -155, -221, -187, -223, -184, -221, -208, -205, -186, -173, -165, -148,
+ -132, -141, -127, -151, -120, -154, -131, -136, -104, -119, -115, -117,
+ -108, -145, -140, -188, -146, -212, -174, -199, -183, -203, -176, -190,
+ -174, -178, -155, -183, -156, -192, -139, -178, -158, -185, -162, -172,
+ -198, -158, -136, -126, -116, -127, -125, -157, -109, -154, -85, -152,
+ -60, -135, -26, -117, 10, -26, 16, -15, 36, 69, 64, 91,
+ 41, 52, -5, 31, -40, -16, -60, -3, -25, -20, -13, 18,
+ 23, 6, 15, 15, 42, 21, 45, 37, 32, 17, 30, 46,
+ 26, 13, 23, -19},
+ /* IRC_Composite_C_R0195_T210_P015.wav */
+ {5, -5, 6, -6, 6, -6, 6, -6, 6, -6, 7, -7,
+ 7, -7, 7, -8, 8, -8, 8, -9, 9, -9, 9, -9,
+ 9, -9, 7, -1, -45, -76, 21, -111, 31, -35, -9, -141,
+ 17, -81, 104, 351, 414, 314, 1167, 1284, -698, -334, -392, 1578,
+ 2039, 2332, 5286, 3230, 840, 735, 2013, 886, -250, -668, 388, 293,
+ -154, 194, 485, 539, 373, 247, 362, 554, 379, 14, 200, 206,
+ 243, 111, 161, 197, 127, 193, 183, 265, -4, 123, -60, 64,
+ -188, -26, -316, -89, -293, -150, -287, -153, -256, -136, -244, -221,
+ -246, -144, -274, -195, -276, -224, -307, -223, -340, -289, -302, -287,
+ -323, -301, -317, -303, -243, -292, -298, -294, -312, -280, -306, -243,
+ -309, -226, -276, -214, -262, -251, -287, -252, -261, -239, -268, -241,
+ -251, -220, -263, -226, -289, -222, -266, -235, -299, -243, -268, -249,
+ -232, -227, -224, -212, -188, -201, -205, -192, -215, -147, -197, -162,
+ -188, -158, -182, -181, -170, -210, -185, -204, -179, -201, -174, -187,
+ -176, -157, -173, -157, -158, -147, -138, -131, -123, -128, -126, -106,
+ -147, -126, -173, -126, -184, -147, -199, -168, -203, -173, -188, -192,
+ -200, -201, -192, -187, -199, -212, -196, -185, -179, -192, -178, -197,
+ -173, -188, -148, -169, -132, -157, -149, -165, -140, -138, -136, -123,
+ -135, -83, -122, -54, -79, -22, -60, -30, -15, -6, 29, -8,
+ 28, -22, 45, -13, 39, -33, 30, 5, 20, -21, 0, 10,
+ 30, -5, 10, -9, 35, 17, 55, 16, 50, 34, 50, 43,
+ 21, 55, 18, 41},
+ /* IRC_Composite_C_R0195_T225_P015.wav */
+ {3, -3, 3, -3, 3, -3, 3, -3, 3, -3, 3, -3,
+ 3, -3, 3, -3, 3, -3, 3, -3, 3, -3, 3, -2,
+ 2, -2, 2, -1, 1, 0, -5, -60, -132, -64, -20, -85,
+ -7, -176, -5, -52, -74, -72, 84, 430, 320, 474, 759, 338,
+ -353, -244, 418, 1855, 1977, 2351, 3573, 2234, 1049, 638, 1145, 830,
+ 98, -47, 162, 541, 334, 496, 426, 623, 595, 701, 586, 611,
+ 652, 468, 517, 289, 478, 320, 343, 117, 277, 121, 176, -24,
+ 22, -90, -30, -74, -66, -98, -115, -117, -110, -81, -130, -60,
+ -133, -100, -189, -113, -212, -176, -266, -224, -323, -264, -344, -286,
+ -346, -280, -345, -296, -322, -294, -296, -283, -287, -282, -269, -247,
+ -271, -247, -275, -227, -292, -244, -283, -265, -275, -259, -269, -255,
+ -263, -256, -260, -233, -264, -234, -273, -263, -267, -260, -261, -252,
+ -268, -235, -241, -202, -245, -183, -233, -168, -240, -165, -221, -162,
+ -203, -155, -192, -162, -177, -157, -183, -162, -190, -153, -205, -157,
+ -215, -152, -221, -147, -199, -142, -182, -144, -171, -142, -156, -148,
+ -173, -154, -163, -157, -167, -170, -162, -167, -179, -174, -184, -178,
+ -180, -174, -206, -178, -198, -188, -207, -204, -201, -199, -190, -214,
+ -206, -229, -200, -212, -173, -192, -170, -215, -166, -189, -131, -174,
+ -137, -168, -105, -105, -64, -83, -43, -48, 2, -25, -6, -28,
+ 14, -21, 8, -16, 16, 3, 33, 17, 37, 28, 49, 34,
+ 62, 28, 60, 13, 34, 6, 35, -8, 13, -2, 6, 11,
+ 8, 6, -3, 12},
+ /* IRC_Composite_C_R0195_T240_P015.wav */
+ {0, 0, 0, 0, 1, -1, 1, -1, 1, -1, 1, -1,
+ 1, -1, 1, -1, 2, -2, 2, -2, 2, -2, 3, -3,
+ 3, -4, 4, -5, 6, -7, 9, -12, 19, -47, -65, -42,
+ -103, -56, -86, -61, -84, -136, -84, -125, -76, 46, 139, 205,
+ 243, 514, 311, -296, -329, 431, 1034, 1553, 1739, 2266, 2575, 1097,
+ 709, 1340, 1199, 680, 407, 592, 719, 747, 950, 891, 924, 905,
+ 835, 742, 679, 646, 246, 443, 284, 396, 76, 270, 79, 360,
+ 141, 234, 132, 176, 91, 44, 66, -33, 37, -24, -26, -52,
+ -140, -134, -171, -129, -209, -205, -236, -225, -189, -236, -238, -284,
+ -228, -319, -232, -313, -237, -314, -261, -304, -270, -301, -338, -291,
+ -284, -273, -275, -283, -246, -253, -222, -299, -253, -305, -231, -286,
+ -251, -299, -224, -259, -244, -287, -247, -273, -229, -277, -233, -278,
+ -222, -256, -210, -244, -213, -226, -197, -200, -180, -187, -177, -191,
+ -166, -184, -157, -198, -181, -196, -166, -179, -163, -184, -173, -192,
+ -155, -187, -160, -198, -161, -183, -151, -173, -150, -191, -183, -210,
+ -175, -205, -187, -208, -163, -185, -168, -170, -162, -180, -159, -180,
+ -156, -158, -160, -186, -181, -225, -214, -217, -218, -234, -238, -231,
+ -223, -221, -238, -230, -217, -206, -178, -170, -207, -185, -183, -154,
+ -152, -154, -148, -132, -105, -76, -64, -31, -49, -9, -20, 6,
+ -12, 13, 19, 25, 27, 32, 34, 31, 31, 36, 25, 9,
+ 26, 16, 20, -7, 0, -5, 22, 12, 4, 3, 18, 14,
+ 9, -1, -12, -43},
+ /* IRC_Composite_C_R0195_T255_P015.wav */
+ {1, -1, 2, -2, 2, -2, 2, -2, 2, -2, 2, -2,
+ 2, -2, 2, -2, 2, -2, 2, -2, 2, -2, 2, -2,
+ 2, -2, 3, -3, 3, -3, 3, -4, 5, -6, 10, -30,
+ -120, -103, -93, -24, -84, -108, -106, -140, -67, -76, -73, -121,
+ -133, -119, 68, 29, 241, 313, 294, 61, 72, 517, 913, 1238,
+ 1676, 2099, 2348, 2019, 1198, 861, 1666, 1655, 972, 1040, 933, 753,
+ 835, 866, 579, 456, 471, 608, 472, 520, 250, 352, 304, 298,
+ 249, 242, 223, 199, 145, 120, 235, 93, 122, 49, 3, -39,
+ -15, -81, -71, -213, -81, -243, -130, -266, -181, -248, -213, -210,
+ -258, -180, -292, -166, -311, -258, -301, -258, -288, -304, -246, -283,
+ -255, -296, -284, -260, -336, -252, -351, -234, -370, -214, -328, -219,
+ -303, -234, -296, -242, -261, -235, -272, -252, -263, -194, -230, -174,
+ -222, -179, -253, -165, -264, -178, -236, -165, -217, -158, -210, -155,
+ -187, -164, -190, -169, -218, -178, -196, -162, -199, -188, -233, -170,
+ -224, -161, -189, -146, -207, -166, -198, -168, -188, -182, -169, -203,
+ -189, -199, -180, -192, -203, -183, -200, -212, -208, -215, -216, -240,
+ -188, -209, -178, -223, -162, -198, -167, -209, -211, -239, -196, -203,
+ -168, -214, -197, -239, -145, -209, -149, -231, -167, -214, -131, -185,
+ -129, -161, -129, -137, -105, -119, -97, -124, -84, -109, -56, -30,
+ 29, 13, 51, 35, 66, 39, 42, 23, 8, -3, 12, 8,
+ 29, 13, 32, 3, 26, 10, 21, -16, -7, -1, 7, 2,
+ -5, -3, -3, -1},
+ /* IRC_Composite_C_R0195_T270_P015.wav */
+ {-1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1,
+ -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1,
+ -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -2, 2,
+ -2, 3, -5, 38, 41, 81, 155, 185, 109, 43, 168, 195,
+ 276, 233, 321, 381, 601, 818, 813, 1005, 694, 582, 706, 813,
+ 1380, 2408, 2859, 3417, 3192, 1234, 164, 601, 379, 145, -151, -609,
+ -543, -195, 49, -120, -111, 136, 61, -67, -105, -152, -114, -346,
+ -267, -397, -310, -240, -454, -334, -493, -338, -595, -418, -579, -472,
+ -506, -514, -489, -592, -416, -592, -471, -574, -491, -531, -515, -529,
+ -526, -514, -446, -512, -519, -497, -434, -454, -430, -467, -415, -434,
+ -400, -422, -405, -387, -422, -391, -428, -343, -360, -326, -389, -310,
+ -335, -241, -318, -246, -341, -202, -264, -207, -278, -221, -234, -239,
+ -210, -224, -186, -240, -226, -220, -207, -184, -222, -220, -245, -206,
+ -189, -231, -234, -238, -195, -237, -129, -227, -188, -216, -145, -224,
+ -225, -241, -252, -274, -214, -216, -202, -212, -164, -190, -171, -187,
+ -134, -175, -141, -168, -164, -167, -150, -174, -138, -128, -101, -135,
+ -76, -112, -70, -116, -70, -102, -92, -120, -98, -123, -112, -146,
+ -92, -145, -88, -135, -96, -115, -84, -87, -67, -61, -82, -42,
+ -48, 11, -13, 5, 25, 35, 52, 70, 95, 98, 101, 116,
+ 108, 116, 94, 137, 109, 102, 83, 89, 88, 89, 64, 59,
+ 40, 43, 14, 53, 3, 16, -3, 7, 12, -13, 16, -2,
+ 28, 6, 37, 18},
+ /* IRC_Composite_C_R0195_T285_P015.wav */
+ {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 1, -1, 1, -1, 1, -1,
+ 2, -3, 6, -109, -159, -182, -252, -225, -252, -209, -220, -323,
+ -249, -123, 27, 108, 237, 39, -227, -176, -141, 26, 983, 1563,
+ 1989, 2256, 2042, 1443, 1643, 1525, 1088, 1602, 1474, 1170, 1150, 824,
+ 708, 924, 1176, 1036, 269, 443, 781, 769, 425, 308, 405, 367,
+ 433, 461, 448, 234, 194, 163, 218, 246, 100, -62, 56, -52,
+ 109, -114, 68, -271, 33, -231, -10, -251, -107, -211, -189, -193,
+ -271, -173, -258, -220, -344, -246, -222, -245, -241, -310, -276, -282,
+ -207, -275, -244, -325, -317, -257, -224, -252, -269, -249, -255, -262,
+ -227, -233, -233, -271, -256, -189, -188, -164, -249, -173, -198, -150,
+ -210, -124, -172, -127, -154, -156, -239, -193, -248, -200, -220, -169,
+ -212, -123, -176, -157, -249, -249, -297, -272, -295, -303, -316, -296,
+ -263, -235, -217, -173, -234, -208, -250, -182, -255, -227, -234, -247,
+ -244, -205, -187, -219, -199, -160, -156, -146, -178, -163, -199, -166,
+ -183, -164, -214, -208, -196, -160, -168, -196, -193, -160, -204, -162,
+ -188, -175, -206, -218, -198, -177, -203, -194, -169, -169, -172, -168,
+ -160, -149, -149, -113, -128, -93, -92, -23, -69, -13, -56, -10,
+ -44, 8, -28, 9, -21, 0, 13, 41, 16, 24, 3, 43,
+ 28, 54, 11, 12, -13, 29, -17, 4, -27, 25, -15, 29,
+ -5, 31, 21, 23},
+ /* IRC_Composite_C_R0195_T300_P015.wav */
+ {3, -3, 3, -3, 3, -3, 3, -3, 4, -4, 4, -4,
+ 4, -4, 4, -4, 4, -4, 5, -5, 5, -5, 5, -6,
+ 6, -6, 6, -7, 7, -8, 9, -10, 13, -17, 28, -93,
+ -234, -180, -207, -251, -293, -262, -250, -346, -376, -130, 67, -6,
+ 108, -151, -710, -124, 223, 55, 1941, 2161, 1520, 2079, 1404, 552,
+ 750, 680, 497, 384, 1277, 1177, 733, 896, 1394, 1657, 1268, 1283,
+ 1309, 1208, 892, 805, 954, 655, 670, 296, 482, 320, 570, 596,
+ 507, 445, 118, 348, 106, 469, 16, 25, -157, 191, 5, 47,
+ -157, -78, -270, -124, -129, -61, -219, -206, -188, -118, -157, -193,
+ -216, -244, -222, -155, -190, -248, -263, -189, -260, -255, -305, -285,
+ -272, -179, -250, -320, -353, -177, -207, -158, -311, -279, -294, -136,
+ -181, -176, -231, -183, -184, -86, -72, -138, -172, -154, -185, -208,
+ -132, -194, -245, -197, -155, -169, -170, -257, -250, -273, -291, -308,
+ -267, -292, -322, -261, -262, -223, -282, -280, -302, -284, -249, -260,
+ -293, -276, -203, -199, -209, -219, -251, -230, -225, -236, -239, -254,
+ -234, -253, -179, -216, -170, -211, -150, -212, -180, -190, -146, -198,
+ -192, -173, -159, -174, -166, -187, -188, -211, -146, -166, -163, -201,
+ -184, -171, -129, -130, -154, -144, -145, -167, -132, -161, -149, -160,
+ -132, -127, -113, -104, -89, -73, -95, -76, -83, -55, -53, -22,
+ -5, -13, -3, 5, 22, 0, 15, 5, -11, -15, 15, -13,
+ -5, -8, 7, -15, 26, 37, 33, 12, 52, 50, 45, 34,
+ 80, 35, 79, 31},
+ /* IRC_Composite_C_R0195_T315_P015.wav */
+ {-3, 3, -3, 3, -3, 3, -3, 3, -3, 3, -3, 3,
+ -3, 3, -3, 3, -3, 3, -3, 3, -3, 3, -3, 3,
+ -3, 3, -3, 3, -3, 2, -2, 2, -3, -145, -419, -273,
+ -234, -320, -494, -368, -332, -137, 23, 435, 141, -274, 189, -707,
+ -1152, 1545, 415, 1656, 4094, 1604, 1580, 2053, 473, -525, 351, 1046,
+ 431, 4, 613, 555, 843, 494, 1071, 1048, 1375, 1139, 994, 1431,
+ 1283, 1617, 838, 1011, 491, 1146, 716, 701, 358, 388, 443, 310,
+ 457, 32, -5, -93, 169, 269, -27, 91, -165, 86, -189, 135,
+ -120, -103, -270, -74, -56, -217, -55, -238, -119, -288, -120, -263,
+ -264, -223, -279, -179, -302, -173, -211, -262, -189, -255, -177, -382,
+ -172, -263, -206, -302, -155, -222, -165, -156, -91, -226, -129, -208,
+ -104, -116, -7, -182, -57, -43, -59, -165, -192, -254, -334, -329,
+ -222, -307, -218, -299, -183, -230, -153, -307, -262, -341, -352, -360,
+ -306, -320, -349, -329, -284, -313, -262, -287, -276, -348, -332, -282,
+ -281, -235, -311, -213, -232, -205, -232, -209, -189, -252, -237, -224,
+ -214, -199, -229, -157, -205, -196, -185, -173, -156, -212, -157, -188,
+ -156, -172, -131, -151, -175, -157, -139, -172, -126, -151, -157, -177,
+ -123, -115, -119, -116, -103, -151, -157, -157, -103, -111, -83, -143,
+ -134, -106, -78, -57, -83, -76, -143, -96, -110, -30, -81, -37,
+ -72, -10, -21, -9, 18, 1, -1, -42, 21, 2, 14, 26,
+ 52, 25, 26, 34, 53, 32, 44, 40, 74, 42, 66, 49,
+ 90, 62, 69, 50},
+ /* IRC_Composite_C_R0195_T330_P015.wav */
+ {1, -1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1,
+ -1, 1, -2, 2, -2, 3, -3, 4, -5, 6, -7, 9,
+ -11, 14, -18, 25, -38, 72, -494, -377, -163, -458, -490, -5,
+ -649, -505, -448, 737, 42, 720, -5, -1466, 655, -1957, -272, 3638,
+ 1401, 3790, 5414, 320, 1005, 1351, 716, -1658, 736, 755, 629, -241,
+ 367, 775, 655, 1313, 522, 711, 607, 1017, 814, 1153, 1014, 727,
+ 1072, 1193, 1369, 911, 877, 665, 747, 788, 602, 389, -2, 301,
+ 79, 299, 235, 78, -171, -171, -12, -204, -96, -124, -285, -390,
+ -109, 136, -127, -79, -244, -125, -236, -21, -257, -313, -264, -266,
+ -248, -188, -112, -345, -304, -50, -265, -235, -235, -38, -345, -176,
+ -119, -52, -156, -49, -98, -184, -269, -46, -139, -85, -206, -15,
+ -64, -75, -25, 28, -69, -306, -216, -145, -321, -127, -396, -359,
+ -364, -161, -402, -370, -313, -329, -419, -337, -409, -317, -386, -319,
+ -505, -396, -337, -244, -368, -365, -380, -286, -289, -230, -330, -297,
+ -331, -175, -233, -184, -243, -195, -204, -144, -176, -187, -238, -158,
+ -239, -170, -186, -124, -214, -122, -133, -166, -196, -122, -151, -204,
+ -200, -147, -209, -159, -148, -140, -151, -108, -106, -148, -137, -107,
+ -109, -157, -138, -152, -159, -152, -143, -132, -169, -84, -121, -99,
+ -110, -53, -57, -49, -65, -56, -59, -72, -39, -80, -76, -50,
+ -24, -78, -31, -4, 10, -54, 43, 12, 18, 43, 54, 13,
+ -6, 86, 38, 22, 50, 62, 50, 38, 99, 31, 62, 60,
+ 58, 29, 11, 38},
+ /* IRC_Composite_C_R0195_T345_P015.wav */
+ {-11, 11, -12, 12, -13, 13, -14, 15, -16, 16, -17,
+ 18, -19, 20, -22, 23, -24, 26, -27, 29, -32, 34,
+ -38, 43, -52, 76, -328, -363, -214, -611, -509, -335, -643,
+ -447, -869, 31, 57, 1040, 355, -374, 1188, -2297, -4332, 5821,
+ -1811, 4636, 8422, 1800, 2573, 2446, 333, -2543, 348, 441, 1038,
+ -1221, -120, 503, 1705, 557, 994, 621, 1108, 529, 1387, 553,
+ 713, 268, 987, 1168, 1373, 742, 1063, 633, 1079, 800, 733,
+ 478, 430, -8, 448, 243, 389, -34, 240, -156, 40, -68,
+ 208, -262, -163, -284, -85, -119, 4, -292, -169, -173, -9,
+ -177, -185, -284, -261, -239, -151, -333, -444, -278, -212, -269,
+ -320, -207, -353, -167, -122, 5, -101, -146, -236, 26, 158,
+ -193, -6, 31, -110, -13, 194, -336, -291, 166, -187, -339,
+ 71, 83, -256, -36, 64, -291, -253, -258, -423, -483, -404,
+ -542, -450, -398, -404, -517, -415, -395, -300, -370, -455, -406,
+ -370, -334, -365, -318, -452, -367, -267, -228, -288, -345, -283,
+ -288, -195, -237, -211, -198, -242, -243, -148, -120, -215, -182,
+ -175, -171, -178, -118, -143, -239, -153, -169, -160, -247, -180,
+ -208, -203, -145, -188, -199, -188, -106, -162, -137, -157, -131,
+ -109, -183, -154, -130, -109, -197, -150, -64, -168, -155, -207,
+ -78, -176, 15, -176, -113, -192, -34, -58, -13, -144, -90,
+ -50, -81, -89, 24, -35, -100, -56, -23, -9, 29, 37,
+ -35, -51, 58, 55, 55, -8, 24, 51, 71, 28, 39,
+ 46, 49, 51, 27, -11, 48, 41, 22, -3, 23, -14,
+ 20, -19, -13}};
+
+const int16_t irc_composite_c_r0195_p030[][256] =
+ {/* IRC_Composite_C_R0195_T000_P030.wav */
+ {-54, 55, -55, 56, -57, 57, -58, 58, -58, 59, -58,
+ 58, -58, 56, -55, 52, -48, 42, -34, 21, 1, -38,
+ 103, -854, -293, -223, -413, -662, -699, -525, -649, -153, -821,
+ -339, 218, 2022, 2054, -2330, -2792, 1747, -9777, 10764, 1443, 5425,
+ 12121, 258, -1458, 190, 2417, 73, -781, 460, -747, -560, 1297,
+ 150, 875, 435, 1456, 292, 1300, -209, 948, 616, 962, -100,
+ 649, 571, 794, 577, 979, 597, 904, 611, 978, 804, 976,
+ 113, 890, 241, 591, 176, 615, -381, 92, -14, 208, -196,
+ 144, -25, -87, -290, -51, -201, -11, -376, 13, -401, 6,
+ -326, 16, -348, -73, -269, -196, -276, -77, -266, -298, -332,
+ -42, -326, -208, -519, -147, -423, -93, -432, -257, -451, -109,
+ -270, -192, -375, -151, -315, -188, -206, -37, -459, -168, -241,
+ -10, -278, -73, -257, 87, -155, -18, -234, -126, -305, -82,
+ -294, -405, -294, -172, -409, -47, -89, -22, -89, 128, -65,
+ 30, 48, -38, -108, -174, -371, -254, -219, -255, -366, -375,
+ -485, -192, -333, -204, -374, -171, -415, -254, -437, -213, -353,
+ -257, -320, -265, -229, -230, -199, -344, -342, -208, -291, -328,
+ -415, -224, -366, -172, -311, -156, -289, -178, -259, -84, -212,
+ -136, -195, -136, -204, -23, -192, -135, -219, -38, -190, -41,
+ -200, -34, -195, -41, -117, -47, -160, -96, -111, -97, -131,
+ -73, -186, -78, -137, -70, -202, -93, -119, -55, -149, -67,
+ -139, -48, -119, -34, -122, -39, -119, -31, -62, -1, -61,
+ -28, -48, 31, 6, 17, -8, 75, 29, 40, 45, 65,
+ 21, 45, 14},
+ /* IRC_Composite_C_R0195_T015_P030.wav */
+ {45, -49, 52, -57, 61, -66, 71, -77, 83, -90,
+ 98, -107, 116, -126, 138, -150, 164, -177, 188, -179,
+ -73, -966, -102, -989, 560, -1173, -327, -1684, 597, -1115,
+ 259, -1628, 2183, 1183, 3440, -5302, 3131, -10502, -529, 12738,
+ -6827, 21598, 5258, -7, -3187, 1963, 495, 327, 502, -665,
+ -1769, 1964, -371, 1009, 596, 896, 151, 1819, -137, 401,
+ 915, 974, 249, 114, 559, 962, 751, 584, 371, 576,
+ 838, 1123, 906, 351, -23, 332, 521, 427, -138, -265,
+ -100, 55, -54, 59, -205, -261, -77, 2, -107, -229,
+ -219, -403, -235, -134, -143, -470, -234, -177, -61, -236,
+ -83, -258, -197, -192, -101, -300, -331, -254, -216, -242,
+ -271, -289, -310, -261, -262, -333, -321, -293, -270, -333,
+ -255, -243, -275, -316, -256, -252, -176, -243, -182, -212,
+ -172, -289, -118, -228, -303, -307, -77, -225, -298, -230,
+ -122, -403, -282, -152, -387, -451, 8, 13, -160, -49,
+ 124, -112, -104, 87, -45, -358, -217, -135, -206, -167,
+ -172, -229, -317, -197, -162, -307, -272, -363, -290, -325,
+ -168, -360, -289, -359, -212, -286, -239, -278, -241, -270,
+ -234, -333, -266, -308, -234, -344, -218, -357, -274, -331,
+ -208, -244, -205, -284, -219, -226, -149, -177, -181, -201,
+ -159, -198, -109, -172, -113, -218, -95, -145, -67, -151,
+ -73, -148, -84, -129, -52, -118, -51, -142, -54, -113,
+ -29, -150, -66, -118, -44, -150, -8, -105, -59, -133,
+ -10, -103, -51, -102, -26, -114, 2, -62, -14, -78,
+ 24, -62, 35, -36, 28, -33, 74, -22, 57, 13,
+ 101, -23, 94, 36, 50, 32},
+ /* IRC_Composite_C_R0195_T030_P030.wav */
+ {22, -22, 22, -22, 22, -22, 21, -21, 20, -19, 18,
+ -16, 13, -9, 3, 6, -23, 62, -927, -68, -994, 540,
+ -922, -246, -1369, 328, -1205, -194, -1442, 1439, 570, 2758, -885,
+ 3393, -4925, -8851, 7444, -11725, 26992, 7407, 2376, 4575, -342, -2312,
+ -724, 2866, -938, -1366, 86, -585, 1150, 1637, 627, 500, 364,
+ 713, 726, 1042, -231, 616, 243, 1337, 315, 722, 212, 833,
+ 509, 1128, 741, 763, -23, 706, -315, 392, -241, 100, -771,
+ -188, -212, 298, -422, -413, -205, 37, -281, -130, -523, -88,
+ -503, -112, -573, -211, -454, -80, -504, -132, -294, -80, -239,
+ -22, -204, -163, -320, -82, -296, -297, -476, -149, -313, -195,
+ -414, -189, -328, -173, -333, -197, -351, -236, -353, -212, -335,
+ -289, -358, -212, -232, -235, -269, -127, -247, -255, -385, -167,
+ -387, -267, -392, -201, -373, -245, -334, -180, -331, -245, -322,
+ -210, -238, -125, -198, -59, -138, -44, -137, -3, -58, 9,
+ -199, -115, -324, -112, -304, -122, -334, -195, -342, -88, -186,
+ -146, -296, -157, -238, -181, -256, -164, -321, -198, -320, -193,
+ -346, -189, -318, -169, -297, -240, -369, -178, -273, -261, -370,
+ -222, -299, -226, -327, -186, -311, -192, -283, -157, -277, -144,
+ -267, -140, -249, -109, -220, -95, -203, -109, -172, -51, -152,
+ -88, -149, -49, -133, -83, -123, -30, -107, -62, -145, -55,
+ -94, -23, -81, -49, -86, -1, -42, -24, -119, -38, -58,
+ -5, -105, -50, -71, 1, -68, -36, -53, -6, -19, 20,
+ -14, 18, -29, 55, -17, 56, -9, 73, 3, 81, 35,
+ 97, 32, 73},
+ /* IRC_Composite_C_R0195_T045_P030.wav */
+ {9, -7, 5, -3, 0, 3, -7, 12, -18, 25, -34,
+ 45, -60, 79, -107, 151, -363, -677, -240, -871, 518, -941,
+ -269, -1276, 265, -1859, 1187, -1350, 2149, 1, 5698, -4523, 4866,
+ -12427, -2128, 5167, 3666, 25146, 2435, 401, 417, 1825, -3164, 2605,
+ 613, -1851, -1719, 979, -329, 2301, -426, 1606, -126, 1675, 119,
+ 1149, -57, 595, -80, 1644, 19, 793, -129, 1326, 678, 1045,
+ -17, 856, -22, 267, -380, 190, -844, -146, -664, 199, -735,
+ -167, -428, 32, -402, -217, -411, -308, -427, -284, -543, -438,
+ -454, -307, -561, -259, -430, -199, -381, -181, -382, -148, -228,
+ -87, -405, -169, -303, -91, -361, -175, -378, -291, -318, -127,
+ -301, -269, -260, -171, -316, -164, -268, -241, -361, -178, -341,
+ -229, -322, -201, -335, -221, -314, -343, -328, -221, -321, -372,
+ -308, -210, -400, -380, -322, -269, -386, -304, -356, -223, -292,
+ -166, -317, -192, -91, -100, -158, -51, -73, -116, -23, -56,
+ -150, -243, -92, -175, -169, -181, -177, -294, -169, -187, -210,
+ -286, -130, -227, -213, -253, -159, -238, -150, -211, -249, -260,
+ -144, -232, -247, -283, -227, -289, -178, -293, -252, -299, -148,
+ -285, -211, -271, -197, -276, -200, -222, -204, -252, -176, -235,
+ -205, -223, -107, -218, -119, -198, -109, -182, -35, -149, -101,
+ -171, -62, -123, -44, -128, -75, -141, -37, -172, -54, -143,
+ -11, -144, -27, -104, 26, -103, 23, -105, 37, -73, 18,
+ -74, 15, -63, 4, -83, 16, -68, -5, -50, 47, -28,
+ 24, -35, 41, -27, 78, -21, 77, -17, 105, 16, 96,
+ 7, 122, 24},
+ /* IRC_Composite_C_R0195_T060_P030.wav */
+ {8, -7, 7, -6, 5, -5, 4, -3, 3, -3, 3,
+ -3, 5, -9, 20, 86, -790, 103, -872, 55, -678, -196,
+ -962, -387, -893, 162, 722, 1609, 608, 2929, 1713, -6837, -667,
+ -12691, 15713, 13906, 9242, 5982, 83, -1809, 1076, 1596, 1326, -1529,
+ -1392, -366, 731, 78, 1368, -246, 850, 592, 521, 699, 1115,
+ 310, 930, 973, 592, 664, 159, 708, 710, 489, -74, 485,
+ -697, 374, -695, 249, -1252, 204, -915, 137, -777, -91, -241,
+ -403, -350, -466, -269, -521, -499, -540, -380, -320, -709, -233,
+ -517, -228, -634, -244, -652, -290, -447, -147, -545, -241, -352,
+ 5, -488, -69, -437, 53, -465, -6, -353, -78, -525, -24,
+ -335, -157, -418, -27, -367, -103, -287, -241, -397, -158, -276,
+ -304, -351, -181, -369, -225, -401, -209, -512, -184, -447, -153,
+ -476, -152, -484, -76, -492, -243, -498, -127, -515, -186, -392,
+ -135, -408, -53, -239, -82, -191, 13, -138, -32, -118, -6,
+ -177, -44, -187, -143, -246, -50, -262, -108, -264, -71, -256,
+ -48, -325, -90, -349, -63, -352, -110, -280, -159, -311, -153,
+ -350, -187, -295, -152, -318, -137, -192, -178, -252, -142, -284,
+ -157, -277, -190, -286, -151, -282, -119, -257, -91, -310, -54,
+ -224, -93, -207, -75, -245, -9, -203, -48, -237, 12, -182,
+ -40, -201, 33, -146, -26, -200, -37, -156, -43, -159, -28,
+ -130, -32, -114, 20, -143, 6, -113, 12, -126, 58, -103,
+ 21, -121, 59, -91, 44, -108, 59, -43, 47, -58, 60,
+ -38, 52, -61, 83, -36, 62, 3, 66, 27, 84, 84,
+ 65, 66, 103},
+ /* IRC_Composite_C_R0195_T075_P030.wav */
+ {100, -103, 106, -109, 112, -115, 118, -120, 122, -122, 119,
+ -110, 88, -25, 53, -874, 378, -897, 369, -1226, 537, -1263,
+ 224, -1798, 1274, 98, 2195, -462, 4170, 361, -3989, -3149, -11328,
+ 17722, 9812, 11683, 3944, -511, -808, 1931, 2611, 1751, -2704, -605,
+ -430, 728, -349, 908, 612, 941, -687, 931, 759, -66, 894,
+ 2123, 310, 1489, 449, 746, 475, 494, -493, -725, 213, -1074,
+ 381, -1100, -205, -670, -115, -645, -170, -469, -431, 31, -824,
+ -248, -707, -458, -476, -267, -901, -228, -228, -601, -420, -335,
+ -514, -545, -367, -632, -446, -566, -351, -391, -306, -444, -134,
+ -388, -144, -333, -243, -194, -98, -434, -105, -173, -206, -249,
+ -121, -377, -102, -345, -159, -256, -146, -470, -106, -327, -152,
+ -330, -205, -368, -192, -409, -197, -444, -310, -359, -229, -461,
+ -161, -393, -318, -351, -227, -387, -261, -314, -291, -270, -249,
+ -242, -213, -299, -192, -269, -108, -253, -73, -278, 80, -171,
+ -11, -111, 7, -198, -30, -155, -148, -117, -164, -168, -181,
+ -155, -172, -221, -156, -207, -176, -229, -130, -292, -171, -280,
+ -233, -288, -132, -237, -242, -209, -166, -245, -147, -263, -160,
+ -240, -126, -261, -109, -276, -150, -274, -92, -247, -135, -225,
+ -43, -207, -57, -174, -33, -157, -10, -172, -20, -116, -56,
+ -146, 22, -132, -61, -119, -53, -166, -22, -107, -81, -99,
+ -40, -65, -89, -70, -32, -77, -31, -75, -25, -75, 36,
+ -146, 3, -52, 3, -100, 13, -55, -5, -17, 13, -41,
+ 5, -12, 59, -13, 32, 3, 61, 44, 107, 53, 129,
+ 85, 118, 95},
+ /* IRC_Composite_C_R0195_T090_P030.wav */
+ {-85, 89, -94, 98, -102, 107, -112, 116, -119, 121,
+ -119, 106, -58, -455, 60, 99, -1054, -36, -88, 242,
+ -1635, 433, -1255, 1196, -1210, 2540, -840, 5344, -2798, 4670,
+ -10472, -874, 1281, 9198, 19443, 266, 2404, -1705, 3816, 1662,
+ 3038, 83, -1499, -2490, 1368, -37, 500, -334, 1490, -626,
+ 909, -15, 1075, 678, 1886, 658, 852, 386, 1418, 146,
+ 400, -1073, -338, -633, -165, -1024, -383, -1251, -264, -407,
+ -25, -1062, -140, -559, 70, -716, -459, -642, -96, -725,
+ -472, -412, -384, -647, -281, -605, -395, -572, -399, -706,
+ -351, -583, -297, -495, -434, -423, -201, -406, -407, -323,
+ -268, -341, -186, -360, -201, -241, -141, -313, -94, -257,
+ -169, -208, -194, -224, -147, -275, -276, -255, -215, -224,
+ -195, -256, -265, -271, -252, -352, -290, -430, -350, -309,
+ -268, -387, -267, -289, -271, -262, -303, -332, -237, -287,
+ -328, -292, -220, -307, -211, -297, -184, -216, -183, -248,
+ -116, -180, -163, -187, -112, -160, -80, -117, -46, -81,
+ -25, -91, -53, -114, -120, -176, -181, -173, -184, -205,
+ -224, -154, -233, -229, -178, -204, -243, -197, -188, -194,
+ -206, -154, -204, -205, -213, -116, -191, -127, -210, -145,
+ -216, -173, -183, -177, -263, -209, -141, -116, -148, -107,
+ -124, -68, -87, -45, -121, -68, -134, -77, -87, -29,
+ -115, -67, -117, -46, -114, -27, -115, -45, -100, -16,
+ -82, -32, -85, -17, -74, -28, -41, -27, -85, -23,
+ -68, -40, -47, -15, -96, -13, -38, 11, -39, -38,
+ -19, 1, -19, -3, -8, -15, -22, 28, 17, 70,
+ 76, 128, 80, 124, 112, 151},
+ /* IRC_Composite_C_R0195_T105_P030.wav */
+ {104, -111, 117, -125, 133, -143, 153, -166, 180, -197, 218,
+ -245, 288, -401, 132, -429, -287, -538, 503, -701, -185, -1083,
+ 867, -1151, 1236, -192, 2589, 704, 2171, -548, -4634, -1245, -3979,
+ 18483, 6281, 7009, -196, 501, 1762, 4063, 3838, -1, -2732, -1085,
+ -409, 884, 444, -393, -9, 733, 362, 138, 1179, 1594, 1300,
+ 947, 317, 734, 700, 55, -900, -943, -310, -712, -521, -933,
+ -377, -1554, -143, -410, 48, -924, -152, -440, -212, -554, -286,
+ -601, -527, -371, -454, -653, -533, -426, -450, -579, -486, -483,
+ -369, -609, -354, -543, -201, -457, -293, -586, -146, -430, -339,
+ -522, -196, -439, -324, -340, -141, -423, -157, -344, -39, -390,
+ -88, -363, -66, -244, -152, -284, -176, -153, -268, -264, -245,
+ -148, -256, -290, -221, -325, -190, -340, -211, -490, -165, -333,
+ -238, -471, -203, -235, -228, -397, -218, -264, -147, -365, -267,
+ -367, -142, -319, -211, -356, -158, -290, -141, -243, -127, -217,
+ -86, -135, -64, -128, -75, -153, -67, -155, -25, -190, -45,
+ -193, -18, -259, 1, -235, -77, -284, -88, -227, -151, -250,
+ -186, -245, -111, -256, -126, -274, -46, -272, -88, -254, -32,
+ -214, -71, -201, -144, -187, -98, -154, -182, -196, -78, -154,
+ -85, -159, -36, -221, -26, -121, -4, -188, -47, -127, -22,
+ -132, -24, -158, -49, -155, -2, -158, -21, -135, -30, -121,
+ 10, -87, -16, -118, 10, -48, -22, -86, -12, -80, 14,
+ -80, 13, -86, 50, -103, 29, -96, 64, -85, 45, -104,
+ 50, -45, 64, -67, -11, -39, 117, -14, 158, 27, 234,
+ 13, 225, 68},
+ /* IRC_Composite_C_R0195_T120_P030.wav */
+ {28, -28, 29, -29, 29, -29, 29, -27, 25, -21, 15,
+ -5, -12, 43, -140, 88, -559, -189, 139, -567, 198, -314,
+ -739, -383, 1538, -450, 794, 642, 4608, -2302, 1277, -7331, 2563,
+ 5148, 5379, 12280, 2713, -323, -1693, 7682, 3305, 1850, -1530, -2098,
+ -1411, 1431, -392, 275, -715, 741, -246, 1255, 618, 1870, 987,
+ 1146, 368, 730, -169, 18, -891, -805, -1522, -439, -421, -616,
+ -1006, -319, -441, -263, -511, -164, -630, -92, -636, -184, -746,
+ -202, -725, -323, -658, -347, -529, -471, -673, -383, -415, -310,
+ -627, -358, -521, -225, -567, -258, -424, -264, -432, -273, -312,
+ -333, -413, -308, -330, -340, -367, -280, -273, -329, -276, -223,
+ -220, -254, -266, -200, -177, -199, -276, -232, -217, -244, -204,
+ -238, -182, -299, -214, -297, -195, -311, -235, -299, -177, -299,
+ -241, -285, -222, -309, -277, -284, -244, -293, -235, -236, -241,
+ -258, -206, -250, -223, -292, -255, -245, -224, -254, -179, -205,
+ -152, -237, -116, -146, -78, -191, -75, -140, -27, -176, -63,
+ -134, -82, -199, -79, -164, -84, -202, -123, -164, -105, -201,
+ -110, -207, -105, -211, -126, -202, -134, -203, -129, -204, -131,
+ -154, -100, -194, -94, -163, -74, -166, -82, -216, -107, -149,
+ -27, -161, -56, -129, -40, -130, -55, -114, -53, -102, -63,
+ -77, -31, -89, -106, -127, -83, -123, -74, -94, -24, -173,
+ -31, -94, 11, -133, -8, -83, -23, -88, 12, -65, -46,
+ -111, 25, -94, 13, -117, 28, -88, 37, -79, 24, -35,
+ 51, -27, 22, 7, 50, -3, 53, 7, 103, 86, 163,
+ 91, 162, 75},
+ /* IRC_Composite_C_R0195_T135_P030.wav */
+ {6, -6, 7, -7, 8, -8, 9, -9, 10, -10, 11, -12,
+ 13, -14, 16, -18, -114, -234, -98, -110, -115, -173, -170, -232,
+ -274, 363, 317, 1353, 133, 2229, 1323, 703, -7423, 4287, 1521, 5913,
+ 8831, 4880, 804, -872, 5713, 3941, 3130, -2817, -2290, -315, 304, -50,
+ -26, -209, 494, 295, 1100, 751, 1288, 533, 716, 964, 54, -449,
+ -399, -528, -808, -934, -947, -951, -457, -538, -157, -625, -308, -385,
+ -95, -571, -310, -374, -278, -494, -514, -429, -461, -556, -433, -529,
+ -432, -535, -327, -580, -236, -492, -287, -555, -216, -430, -280, -451,
+ -246, -351, -270, -341, -290, -325, -254, -404, -265, -383, -232, -420,
+ -232, -398, -198, -399, -199, -300, -210, -291, -213, -230, -263, -273,
+ -261, -215, -220, -262, -203, -257, -189, -275, -199, -298, -243, -292,
+ -198, -258, -246, -263, -220, -238, -243, -271, -259, -242, -227, -263,
+ -220, -247, -166, -281, -180, -274, -165, -251, -184, -245, -176, -216,
+ -217, -207, -158, -156, -155, -166, -106, -127, -108, -139, -81, -152,
+ -98, -167, -88, -170, -109, -150, -125, -150, -119, -119, -124, -135,
+ -111, -142, -113, -144, -112, -168, -76, -193, -154, -199, -103, -124,
+ -149, -137, -125, -90, -105, -96, -120, -90, -84, -94, -99, -96,
+ -69, -90, -69, -82, -94, -80, -67, -60, -94, -70, -110, -76,
+ -102, -51, -103, -55, -74, -38, -105, -57, -91, -76, -96, -51,
+ -36, -59, -52, -57, -56, -61, -46, -70, -31, -32, -25, -47,
+ 6, -6, 17, -33, -1, 20, 49, 32, 43, 24, 63, 112,
+ 138, 114, 43, 112},
+ /* IRC_Composite_C_R0195_T150_P030.wav */
+ {40, -42, 44, -47, 49, -53, 56, -60, 64, -69, 75, -82,
+ 90, -99, 110, -125, 148, -207, 85, -316, 199, -301, 148, -341,
+ 137, -377, 122, 76, 967, 463, 1542, 593, 2935, -2335, -3746, 3892,
+ 1126, 7018, 5853, 6116, -1287, 2364, 4148, 3574, 181, -2393, -1125, -178,
+ 512, -398, -17, -462, 310, 1083, 929, 1228, 538, 479, 431, 450,
+ -124, -446, -926, -681, -484, -854, -858, -886, -425, -380, -163, -187,
+ -475, -333, -267, -193, -398, -286, -470, -331, -472, -511, -390, -535,
+ -398, -443, -447, -458, -316, -355, -323, -348, -312, -366, -331, -282,
+ -304, -410, -276, -324, -270, -334, -288, -347, -263, -345, -283, -348,
+ -294, -324, -322, -384, -264, -346, -287, -352, -258, -326, -246, -345,
+ -247, -332, -220, -276, -243, -276, -171, -257, -208, -277, -184, -237,
+ -184, -265, -186, -245, -165, -259, -201, -253, -180, -238, -198, -272,
+ -176, -246, -187, -277, -161, -252, -150, -275, -137, -267, -126, -254,
+ -141, -241, -149, -227, -151, -206, -151, -203, -126, -181, -101, -170,
+ -85, -185, -87, -146, -87, -170, -106, -170, -90, -160, -111, -150,
+ -107, -140, -90, -144, -63, -132, -90, -238, -87, -160, -61, -203,
+ -110, -165, -57, -102, -36, -116, -53, -98, -41, -96, -62, -105,
+ -61, -102, -70, -120, -86, -109, -103, -103, -70, -67, -72, -85,
+ -74, -124, -64, -94, -56, -139, -102, -99, -75, -97, -58, -61,
+ -30, -94, -25, -74, -43, -104, -48, -76, -56, -75, -27, -39,
+ -24, -3, 21, -10, -1, 6, 97, 22, 44, 1, 101, 93,
+ 134, 63, 82, 45},
+ /* IRC_Composite_C_R0195_T165_P030.wav */
+ {-5, 5, -6, 6, -6, 6, -6, 7, -7, 7, -8, 8,
+ -9, 9, -10, 11, -12, 14, -17, 25, -163, 17, -80, 12,
+ 2, -73, 14, -200, 92, 163, 755, 471, 1371, 723, 2348, -678,
+ -3403, 2150, 1314, 5172, 5430, 6608, 508, 1066, 3048, 2691, 1177, -1954,
+ -1312, -864, 1114, -99, -395, -170, 755, 800, 840, 925, 689, -7,
+ -2, -92, -86, -565, -703, -538, -415, -580, -306, -607, -337, -594,
+ -235, -197, -71, -393, -276, -461, -131, -367, -319, -372, -331, -590,
+ -309, -448, -316, -391, -370, -330, -202, -345, -331, -285, -252, -333,
+ -332, -343, -285, -341, -299, -355, -341, -319, -335, -324, -306, -321,
+ -304, -329, -296, -343, -321, -372, -317, -381, -294, -371, -299, -341,
+ -289, -334, -273, -311, -239, -276, -234, -285, -217, -256, -202, -250,
+ -169, -221, -150, -205, -167, -217, -204, -247, -225, -236, -214, -247,
+ -214, -207, -155, -197, -183, -192, -155, -160, -191, -236, -192, -219,
+ -188, -240, -195, -229, -141, -216, -140, -210, -108, -198, -110, -186,
+ -131, -164, -118, -161, -128, -132, -117, -113, -132, -130, -137, -165,
+ -120, -157, -110, -125, -107, -149, -159, -131, -114, -123, -178, -132,
+ -136, -75, -82, -32, -23, -71, -33, -49, -17, -94, -64, -87,
+ -53, -94, -80, -79, -100, -87, -113, -66, -94, -109, -102, -113,
+ -90, -94, -92, -105, -128, -124, -129, -89, -94, -67, -93, -83,
+ -95, -80, -94, -87, -89, -91, -53, -81, -31, -57, 18, -49,
+ -13, -37, 25, -7, 9, -13, -1, 53, 58, 59, 79, 109,
+ 133, 124, 84, 60},
+ /* IRC_Composite_C_R0195_T180_P030.wav */
+ {11, -12, 12, -12, 13, -13, 14, -14, 15, -15, 16, -17,
+ 18, -19, 20, -21, 23, -25, 28, -33, 39, -51, 85, 16,
+ -87, 129, -81, 94, 33, -96, -40, 382, 84, 940, 857, 674,
+ 1733, 496, -329, -2997, 2450, 4178, 3824, 5230, 3832, 1334, 27, 2852,
+ 1755, 841, -2811, -750, 709, 798, -492, -489, 295, 1105, 873, 238,
+ 383, 361, 52, -483, -293, -277, -430, -468, -128, -283, -308, -272,
+ -363, -267, -297, -189, -289, -143, -258, -242, -373, -291, -355, -331,
+ -315, -246, -385, -325, -276, -197, -314, -270, -313, -267, -313, -307,
+ -364, -269, -366, -341, -361, -328, -371, -363, -339, -357, -363, -329,
+ -335, -334, -353, -302, -358, -291, -419, -330, -381, -332, -384, -319,
+ -331, -309, -321, -276, -296, -256, -286, -256, -306, -268, -277, -214,
+ -200, -211, -199, -179, -152, -213, -196, -243, -196, -273, -196, -241,
+ -183, -226, -151, -195, -173, -198, -169, -171, -172, -166, -179, -179,
+ -198, -192, -224, -204, -227, -203, -207, -198, -181, -177, -142, -148,
+ -122, -136, -110, -124, -113, -120, -142, -140, -137, -157, -161, -213,
+ -116, -179, -81, -164, -112, -182, -121, -113, -114, -132, -189, -130,
+ -138, -73, -121, -47, -93, -7, -49, 37, -29, -7, -77, -47,
+ -80, -70, -85, -99, -107, -91, -49, -61, -81, -124, -69, -122,
+ -37, -160, -57, -192, -64, -193, -77, -181, -87, -158, -111, -159,
+ -105, -113, -89, -109, -88, -83, -83, -88, -75, -44, -73, -39,
+ -53, -1, -6, 52, 20, 54, 20, 68, 72, 99, 107, 107,
+ 128, 102, 141, 66},
+ /* IRC_Composite_C_R0195_T195_P030.wav */
+ {-2, 2, -2, 2, -2, 2, -2, 2, -2, 2, -2, 2,
+ -2, 2, -2, 1, -1, 1, -1, 0, 0, -1, 2, -3,
+ 4, -1, 20, -103, 88, -97, 77, -98, 0, -92, 123, 108,
+ 628, 541, 868, 946, 950, -323, -2426, 1960, 1868, 3655, 4432, 4145,
+ 1520, 425, 2257, 2243, 324, -1505, -101, 360, 566, -340, -97, 521,
+ 852, 598, 427, 177, 417, 31, 149, -102, -24, -395, -11, -98,
+ -149, -157, -72, -31, -41, -10, -27, -82, -183, -287, -120, -245,
+ -255, -309, -131, -235, -200, -246, -222, -253, -210, -246, -249, -280,
+ -281, -300, -278, -328, -316, -325, -352, -332, -302, -334, -406, -331,
+ -345, -314, -353, -298, -355, -316, -331, -341, -377, -390, -356, -375,
+ -325, -347, -301, -303, -263, -297, -274, -288, -262, -288, -280, -284,
+ -231, -214, -205, -233, -199, -221, -205, -231, -218, -253, -209, -218,
+ -203, -211, -199, -179, -191, -186, -199, -201, -193, -181, -187, -195,
+ -182, -193, -188, -207, -197, -194, -197, -186, -192, -163, -178, -160,
+ -165, -142, -165, -124, -155, -125, -159, -112, -160, -116, -190, -111,
+ -165, -107, -172, -122, -190, -155, -169, -127, -164, -144, -157, -122,
+ -135, -85, -113, -68, -116, -46, -67, -29, -54, -47, -65, -70,
+ -92, -108, -92, -118, -99, -92, -87, -77, -99, -78, -106, -86,
+ -118, -92, -123, -105, -136, -119, -126, -128, -143, -162, -171, -163,
+ -154, -125, -147, -120, -134, -91, -125, -91, -103, -88, -80, -77,
+ -45, -46, -3, 16, 24, 24, 27, 58, 42, 84, 85, 129,
+ 127, 141, 127, 125},
+ /* IRC_Composite_C_R0195_T210_P030.wav */
+ {-3, 3, -3, 3, -3, 3, -3, 3, -3, 3, -3, 3,
+ -3, 3, -3, 4, -4, 4, -4, 4, -4, 5, -5, 6,
+ -6, 7, -9, 12, -23, -83, -41, -51, -44, -54, -36, -72,
+ -62, 13, 62, 362, 515, 745, 618, 964, 123, -1474, 242, 1605,
+ 2935, 3779, 3927, 2051, 644, 1570, 1770, 938, -679, -243, 210, 595,
+ 104, 204, 284, 657, 698, 453, 338, 507, 437, 72, 227, 224,
+ 63, -55, 88, 141, 215, 131, 225, 97, 158, 60, 38, -69,
+ -107, -265, -85, -124, -202, -245, -131, -257, -191, -192, -178, -241,
+ -145, -269, -233, -270, -235, -338, -254, -338, -292, -359, -271, -388,
+ -290, -377, -344, -357, -293, -419, -370, -380, -349, -397, -346, -388,
+ -357, -344, -302, -335, -306, -279, -276, -246, -273, -284, -282, -237,
+ -264, -250, -226, -225, -248, -218, -230, -247, -240, -246, -259, -217,
+ -216, -201, -233, -193, -207, -183, -205, -202, -228, -190, -207, -213,
+ -222, -215, -237, -186, -220, -176, -235, -136, -217, -116, -201, -133,
+ -200, -133, -159, -165, -150, -164, -152, -134, -165, -144, -175, -119,
+ -164, -116, -139, -114, -152, -126, -132, -128, -147, -152, -173, -154,
+ -145, -152, -115, -135, -100, -99, -65, -75, -79, -71, -101, -104,
+ -126, -96, -135, -132, -139, -125, -80, -131, -108, -168, -90, -142,
+ -81, -142, -146, -133, -121, -109, -142, -113, -121, -117, -148, -144,
+ -150, -144, -150, -171, -140, -133, -106, -126, -118, -107, -114, -61,
+ -79, -59, -59, -5, -31, 13, -2, 37, 21, 52, 67, 76,
+ 116, 109, 131, 125},
+ /* IRC_Composite_C_R0195_T225_P030.wav */
+ {-1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1,
+ -1, 1, -1, 2, -2, 2, -2, 2, -2, 2, -2, 3,
+ -3, 3, -4, 5, -7, 14, -133, -89, -52, -68, -90, -53,
+ -98, -77, -65, -141, -33, -38, 144, 467, 407, 586, 730, 265,
+ -702, -561, 1147, 2053, 3022, 3625, 2128, 1090, 1205, 1750, 1123, 130,
+ -65, 370, 545, 406, 83, 446, 636, 787, 529, 661, 549, 526,
+ 605, 368, 384, 101, 455, 156, 304, 149, 290, 238, 156, 207,
+ 48, 113, -86, -57, -36, -176, -93, -129, -107, -209, -107, -181,
+ -195, -158, -169, -157, -239, -205, -240, -206, -253, -259, -282, -324,
+ -296, -377, -347, -379, -340, -400, -365, -377, -398, -363, -379, -335,
+ -398, -330, -337, -283, -328, -250, -335, -246, -279, -228, -301, -284,
+ -299, -253, -272, -251, -277, -231, -269, -213, -274, -207, -256, -219,
+ -248, -212, -205, -234, -191, -244, -197, -238, -194, -226, -213, -226,
+ -214, -216, -219, -201, -233, -193, -216, -184, -183, -195, -171, -198,
+ -136, -183, -121, -177, -126, -151, -131, -156, -138, -160, -140, -152,
+ -130, -143, -109, -146, -113, -168, -114, -165, -123, -191, -139, -186,
+ -143, -163, -101, -126, -91, -147, -80, -140, -85, -158, -107, -181,
+ -100, -149, -100, -152, -116, -152, -120, -144, -111, -159, -127, -155,
+ -129, -148, -146, -173, -157, -145, -134, -136, -128, -146, -120, -128,
+ -115, -125, -133, -149, -144, -153, -151, -147, -139, -128, -113, -110,
+ -89, -78, -59, -67, -27, -39, -8, -19, 9, 9, 34, 44,
+ 63, 65, 98, 113},
+ /* IRC_Composite_C_R0195_T240_P030.wav */
+ {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 1, -1, 1, -1, 1, -1,
+ 1, -1, 1, -1, 1, -1, 1, -1, 1, 1, -97, -166,
+ -68, -82, -98, -145, 6, -178, -148, -79, -61, -4, 226, 289,
+ 466, 695, 150, -363, -564, 127, 1778, 2371, 2730, 2418, 1314, 1333,
+ 1958, 1573, 362, 208, 618, 691, 672, 358, 520, 767, 841, 691,
+ 635, 738, 675, 472, 476, 447, 387, 278, 358, 303, 322, 169,
+ 236, 146, 167, 153, 93, 161, -50, 82, -137, 22, -215, -48,
+ -220, -102, -183, -156, -150, -189, -134, -246, -183, -293, -242, -320,
+ -276, -333, -344, -324, -341, -336, -359, -336, -345, -340, -353, -365,
+ -331, -325, -327, -353, -302, -293, -288, -330, -303, -271, -296, -272,
+ -313, -253, -304, -231, -298, -228, -279, -211, -267, -230, -276, -219,
+ -263, -229, -267, -208, -254, -187, -265, -150, -261, -152, -276, -148,
+ -255, -165, -256, -184, -237, -198, -227, -197, -220, -180, -224, -157,
+ -210, -127, -200, -118, -181, -83, -171, -100, -162, -99, -139, -124,
+ -137, -122, -137, -107, -131, -113, -152, -146, -173, -133, -190, -141,
+ -182, -105, -142, -132, -151, -164, -146, -160, -134, -193, -153, -203,
+ -128, -188, -144, -178, -164, -173, -166, -129, -130, -161, -160, -179,
+ -136, -174, -125, -201, -162, -176, -100, -152, -132, -174, -152, -170,
+ -137, -167, -127, -157, -111, -151, -85, -120, -73, -114, -88, -97,
+ -72, -75, -82, -80, -55, -47, -53, -51, -32, -31, 18, -8,
+ 35, 22, 73, 65},
+ /* IRC_Composite_C_R0195_T255_P030.wav */
+ {1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1,
+ 1, -1, 1, -1, 1, -1, 1, -1, 2, -2, 2, -2,
+ 2, -2, 3, -3, 4, -4, 5, -6, 8, -12, 21, -67,
+ -186, -86, -78, -102, -151, -102, -59, -120, -188, -201, -77, -93,
+ -133, 190, 315, 388, 642, 206, -429, -627, -41, 1512, 2532, 2996,
+ 2313, 1394, 1764, 1723, 1247, 1241, 976, 437, 321, 813, 834, 517,
+ 405, 589, 1007, 744, 553, 490, 641, 609, 429, 381, 224, 460,
+ 347, 308, 191, 277, 332, 175, 143, 27, 143, -12, 2, -104,
+ -86, -53, -114, -79, -254, -137, -211, -109, -349, -252, -297, -195,
+ -348, -341, -330, -262, -295, -325, -316, -289, -220, -305, -292, -333,
+ -256, -294, -316, -394, -312, -314, -286, -387, -317, -323, -233, -326,
+ -302, -311, -250, -238, -292, -290, -296, -199, -259, -241, -292, -230,
+ -245, -240, -270, -264, -223, -199, -230, -230, -203, -176, -179, -217,
+ -226, -180, -203, -190, -239, -188, -230, -173, -226, -190, -237, -149,
+ -184, -187, -190, -158, -126, -139, -130, -127, -103, -111, -83, -113,
+ -124, -148, -136, -140, -136, -138, -118, -132, -127, -165, -140, -179,
+ -161, -224, -175, -209, -159, -182, -149, -200, -170, -196, -150, -218,
+ -152, -222, -177, -254, -168, -210, -186, -215, -137, -166, -128, -166,
+ -128, -184, -106, -195, -174, -233, -137, -162, -153, -164, -155, -113,
+ -146, -99, -141, -92, -113, -93, -86, -120, -80, -115, -69, -118,
+ -75, -88, -73, -78, -77, -48, -78, -41, -54, -26, -50, -11,
+ -21, 13, 14, 31},
+ /* IRC_Composite_C_R0195_T270_P030.wav */
+ {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 1, -1, 2, -2, 4,
+ -6, 13, -118, -205, -234, -125, -147, -202, -210, -154, -149, -302,
+ -174, -138, 74, 139, 366, 494, 501, -30, -788, -660, 656, 2336,
+ 3548, 2933, 1466, 1698, 2087, 1596, 1331, 660, 745, 348, 609, 649,
+ 510, 592, 471, 837, 634, 881, 692, 658, 657, 499, 658, 390,
+ 588, 291, 406, 283, 323, 211, 84, 205, 87, 193, 12, 79,
+ -62, 28, -174, -101, -266, -102, -288, -156, -317, -186, -313, -245,
+ -324, -271, -272, -274, -260, -294, -238, -276, -248, -278, -258, -319,
+ -271, -317, -281, -337, -312, -324, -337, -313, -331, -296, -324, -287,
+ -323, -272, -287, -250, -300, -247, -285, -236, -292, -228, -244, -210,
+ -272, -214, -253, -227, -239, -213, -239, -229, -229, -214, -226, -214,
+ -215, -199, -227, -177, -203, -180, -193, -178, -164, -152, -146, -149,
+ -125, -151, -141, -143, -150, -155, -139, -130, -168, -120, -114, -108,
+ -126, -125, -154, -199, -182, -165, -181, -236, -178, -180, -150, -191,
+ -167, -205, -200, -201, -200, -203, -212, -213, -204, -217, -180, -215,
+ -180, -171, -178, -161, -169, -169, -194, -215, -198, -197, -181, -198,
+ -150, -180, -147, -175, -139, -160, -138, -133, -143, -113, -144, -115,
+ -133, -126, -110, -112, -106, -129, -97, -110, -96, -107, -98, -88,
+ -83, -69, -93, -79, -84, -51, -65, -56, -47, -41, -18, -38,
+ 4, -20, 19, 6},
+ /* IRC_Composite_C_R0195_T285_P030.wav */
+ {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1,
+ 1, -1, 1, -1, 1, -1, 2, -2, 3, -3, 4, -7,
+ 13, -190, -183, -235, -156, -218, -208, -304, -188, -211, -199, -139,
+ -161, 153, 336, 233, -123, -164, -557, -685, 1147, 1908, 2035, 3160,
+ 1492, 785, 1242, 1667, 1506, 818, 1143, 817, 1178, 780, 1057, 657,
+ 807, 713, 683, 790, 563, 729, 595, 855, 599, 704, 615, 614,
+ 540, 520, 362, 304, 245, 337, 219, 250, 133, 197, 59, 126,
+ -65, -70, -160, -89, -159, -231, -254, -291, -203, -274, -185, -304,
+ -208, -285, -189, -264, -256, -331, -243, -311, -262, -308, -280, -304,
+ -246, -335, -250, -318, -269, -309, -282, -300, -260, -340, -268, -307,
+ -236, -284, -269, -325, -251, -300, -247, -310, -244, -277, -183, -239,
+ -198, -269, -229, -228, -237, -229, -245, -249, -264, -220, -228, -201,
+ -208, -179, -162, -96, -70, -76, -134, -147, -121, -145, -142, -175,
+ -192, -136, -108, -69, -153, -156, -210, -206, -236, -198, -320, -233,
+ -263, -173, -277, -125, -224, -144, -166, -131, -224, -164, -197, -216,
+ -260, -240, -244, -177, -196, -187, -157, -155, -171, -202, -195, -206,
+ -203, -187, -195, -236, -200, -188, -186, -176, -232, -177, -163, -133,
+ -189, -164, -179, -140, -153, -146, -152, -146, -135, -108, -114, -110,
+ -119, -88, -135, -96, -118, -89, -127, -122, -110, -95, -72, -108,
+ -98, -91, -59, -48, -71, -44, -52, -26, -30, -20, -36, -34,
+ -3, -13, 9, 16},
+ /* IRC_Composite_C_R0195_T300_P030.wav */
+ {1, -1, 2, -2, 2, -2, 2, -2, 2, -2, 2, -2,
+ 2, -2, 2, -3, 3, -3, 3, -3, 3, -4, 4, -4,
+ 5, -5, 5, -6, 7, -8, 9, -10, 12, -16, 27, -117,
+ -227, -314, -248, -219, -228, -232, -272, -389, -204, -111, 300, 56,
+ 366, 86, -1011, -298, 44, -215, 2477, 2714, 2032, 2711, 645, 499,
+ 536, 1654, 449, 374, 611, 764, 837, 821, 1350, 980, 1002, 808,
+ 1149, 967, 761, 841, 707, 633, 745, 777, 758, 579, 642, 512,
+ 570, 444, 429, 371, 457, 296, 276, 181, 191, 36, 17, 10,
+ -72, -108, -107, -232, -151, -205, -165, -300, -163, -265, -253, -269,
+ -203, -258, -301, -275, -269, -292, -272, -260, -302, -297, -289, -229,
+ -300, -264, -305, -243, -283, -264, -269, -279, -280, -287, -259, -269,
+ -276, -247, -263, -262, -247, -231, -256, -218, -280, -241, -295, -239,
+ -252, -218, -256, -230, -258, -241, -251, -148, -189, -159, -149, -45,
+ -91, -37, -119, -83, -194, -47, -124, -58, -134, -130, -231, -259,
+ -239, -276, -269, -285, -258, -248, -198, -188, -240, -206, -250, -202,
+ -249, -205, -247, -163, -226, -240, -217, -219, -189, -209, -212, -237,
+ -257, -142, -184, -180, -176, -167, -213, -202, -187, -188, -191, -235,
+ -229, -241, -179, -197, -185, -210, -199, -189, -160, -163, -174, -186,
+ -151, -189, -138, -133, -147, -130, -137, -107, -118, -101, -102, -120,
+ -113, -110, -78, -102, -64, -110, -74, -76, -59, -92, -76, -85,
+ -71, -79, -43, -88, -39, -85, -19, -81, 13, -60, 8, -26,
+ 31, 8, 16, 23},
+ /* IRC_Composite_C_R0195_T315_P030.wav */
+ {-3, 3, -3, 3, -3, 3, -3, 3, -3, 3, -3, 3,
+ -3, 3, -3, 3, -3, 3, -3, 3, -4, 4, -4, 4,
+ -4, 5, -5, 6, -7, 10, -15, 27, -80, -353, -110, -370,
+ -400, -195, -345, -309, -394, -97, -188, 193, 754, 56, -290, -194,
+ -1922, 214, 1653, 1159, 4327, 2754, 1527, 1212, 743, 317, 539, 1293,
+ -56, -21, 321, 1262, 295, 442, 708, 1213, 856, 813, 872, 1000,
+ 917, 1073, 730, 897, 944, 834, 864, 775, 610, 537, 692, 383,
+ 605, 370, 536, 245, 348, 155, 280, 101, 83, 31, 90, -127,
+ -63, -179, -119, -272, -116, -304, -232, -233, -212, -345, -176, -295,
+ -247, -334, -204, -342, -233, -195, -284, -324, -229, -236, -255, -304,
+ -258, -323, -258, -297, -224, -323, -252, -263, -224, -317, -252, -224,
+ -255, -281, -221, -218, -205, -288, -209, -253, -193, -244, -249, -201,
+ -218, -210, -200, -172, -180, -181, -84, -203, -75, -54, 19, -108,
+ -2, -46, -171, -209, -203, -294, -294, -286, -264, -295, -154, -187,
+ -222, -182, -252, -197, -249, -211, -305, -232, -289, -224, -284, -267,
+ -256, -184, -221, -260, -295, -281, -237, -233, -228, -269, -251, -203,
+ -162, -193, -192, -189, -166, -237, -188, -201, -208, -209, -181, -224,
+ -228, -151, -192, -200, -209, -131, -196, -159, -162, -159, -166, -116,
+ -160, -151, -131, -47, -142, -119, -122, -73, -93, -108, -96, -133,
+ -90, -84, -86, -83, -90, -56, -96, -65, -65, -48, -80, -67,
+ -79, -50, -60, -46, -63, -39, -44, -12, -27, 9, -35, 23,
+ -3, 24, 6, 41},
+ /* IRC_Composite_C_R0195_T330_P030.wav */
+ {-5, 5, -5, 5, -5, 5, -5, 5, -5, 6, -6, 6,
+ -6, 6, -6, 6, -6, 6, -7, 7, -7, 7, -8, 8,
+ -9, 9, -11, 14, -26, -447, -195, -107, -505, -457, -480, -118,
+ -514, -260, -345, 82, 416, 511, 1319, -1763, -994, 103, -1768, 3014,
+ 4385, 2231, 5234, 1274, 290, -49, 1679, 111, -604, 148, 524, 573,
+ -122, 973, 360, 1000, 586, 1035, 552, 578, 788, 885, 854, 618,
+ 749, 894, 854, 1021, 930, 968, 502, 1006, 765, 768, 438, 460,
+ 339, 388, 236, 299, 159, 177, -65, 162, -6, -74, -134, -51,
+ -230, -132, -193, -105, -327, -152, -257, -191, -291, -227, -315, -280,
+ -252, -192, -306, -253, -281, -182, -326, -252, -283, -225, -353, -255,
+ -282, -258, -322, -236, -271, -288, -246, -248, -299, -265, -254, -312,
+ -330, -129, -295, -141, -204, -129, -190, -22, -154, -203, -125, -172,
+ -156, -188, -111, -223, -112, -106, -241, -86, -37, -262, -238, -193,
+ -103, -105, -7, -250, -241, -39, -63, -184, -238, -261, -364, -243,
+ -263, -310, -361, -286, -407, -227, -301, -289, -311, -319, -263, -215,
+ -201, -304, -287, -339, -226, -246, -239, -335, -276, -276, -181, -234,
+ -181, -257, -180, -234, -151, -165, -203, -213, -194, -170, -205, -157,
+ -164, -191, -168, -154, -86, -133, -128, -162, -96, -147, -81, -165,
+ -68, -173, -90, -164, -64, -127, -68, -162, -97, -99, -50, -115,
+ -84, -111, -102, -87, -74, -80, -123, -76, -57, -95, -59, -42,
+ -45, -74, -38, -27, -9, -30, 0, -55, 36, -23, 58, -21,
+ 61, -7, 59, 29},
+ /* IRC_Composite_C_R0195_T345_P030.wav */
+ {0, 0, 0, 0, 0, 0, 0, 1, -1, 1, -1,
+ 2, -2, 2, -3, 3, -4, 5, -5, 6, -7, 8,
+ -9, 10, -10, -1, -563, -328, -175, -595, -424, -853, 130,
+ -981, 36, -843, 608, 167, 1104, 1422, -2010, -2282, 1073, -3818,
+ 6278, 4848, 2400, 7303, 1229, -443, -1130, 2494, 95, -616, -743,
+ 335, 108, 968, 417, 599, 684, 1073, 630, 810, 221, 839,
+ 469, 1136, 218, 739, 427, 945, 692, 942, 955, 757, 852,
+ 1008, 762, 614, 598, 745, 200, 403, 51, 357, 29, 333,
+ -147, -65, 9, 35, -114, -161, -231, -52, -197, -139, -202,
+ -61, -310, -211, -214, -165, -307, -207, -293, -261, -259, -204,
+ -201, -310, -323, -244, -250, -260, -335, -226, -421, -206, -333,
+ -191, -401, -188, -244, -264, -283, -225, -208, -338, -233, -69,
+ -304, -181, -213, -18, -265, 39, -68, -82, -113, -107, 16,
+ -329, -80, -324, -205, -399, -184, -328, -391, -87, -9, 36,
+ 1, 59, -85, 124, -60, -143, -307, -231, -406, -296, -456,
+ -311, -297, -269, -433, -350, -189, -283, -277, -303, -343, -325,
+ -287, -213, -328, -275, -279, -293, -296, -268, -217, -284, -286,
+ -294, -277, -236, -247, -201, -305, -195, -224, -135, -244, -185,
+ -128, -225, -126, -186, -74, -235, -93, -209, -95, -133, -90,
+ -131, -165, -80, -89, -69, -148, -104, -72, -157, -77, -134,
+ -88, -171, -67, -113, -124, -110, -76, -110, -130, -59, -96,
+ -88, -119, -61, -109, -81, -81, -67, -82, -54, -22, -49,
+ -19, -18, 2, -15, 25, 13, 24, 8, 45, 57, 4,
+ 20, 23, 60}};
+
+const int16_t irc_composite_c_r0195_p045[][256] =
+ {/* IRC_Composite_C_R0195_T000_P045.wav */
+ {-30, 30, -31, 32, -33, 34, -35, 36, -37, 38, -39,
+ 40, -41, 42, -43, 43, -44, 44, -42, 38, -28, -9,
+ -568, -169, -659, -127, -629, -131, -879, -529, -702, -207, -686,
+ -146, 757, 1500, 46, 1169, -4599, -1790, 163, -4363, 15423, 4884,
+ 3940, 951, 722, -172, 1158, 1933, -227, -750, 303, -613, 1137,
+ 856, 493, 193, 1077, 347, 1002, 300, 420, 882, 521, 444,
+ 512, 395, 442, 484, 453, 404, 610, 620, 839, 826, 758,
+ 649, 736, 767, 608, 484, 493, 147, 169, 427, 10, 204,
+ -311, 19, -245, 98, -241, 0, -167, 16, -127, -26, -105,
+ -158, -191, -79, -246, -206, -268, -177, -349, -216, -328, -216,
+ -366, -178, -332, -214, -308, -168, -352, -204, -312, -132, -301,
+ -215, -309, -152, -304, -202, -336, -200, -308, -229, -340, -205,
+ -331, -196, -326, -240, -274, -198, -310, -200, -321, -159, -282,
+ -190, -264, -176, -207, -74, -246, -157, -167, -77, -245, 18,
+ -155, -133, -274, -40, -289, -273, -267, -139, -520, -214, -265,
+ -252, -317, -46, -133, -17, 38, 121, -153, 151, 27, -13,
+ -114, 13, -297, -74, -188, -147, -332, -197, -268, -297, -199,
+ -221, -310, -309, -263, -206, -258, -237, -414, -155, -291, -200,
+ -367, -189, -287, -139, -241, -261, -247, -142, -190, -233, -239,
+ -158, -249, -110, -242, -173, -207, -82, -217, -136, -180, -136,
+ -164, -112, -177, -97, -134, -40, -164, -32, -115, -16, -91,
+ -12, -112, 4, -81, -19, -100, -20, -70, -21, -87, -6,
+ -59, 22, -68, -7, -66, 36, -20, 13, -47, -2, -27,
+ 28, -4, 13},
+ /* IRC_Composite_C_R0195_T015_P045.wav */
+ {16, -14, 12, -9, 7, -4, 1, 3, -7, 12, -17,
+ 22, -29, 36, -45, 56, -69, 85, -107, 143, -247, -403,
+ -616, -230, -517, -270, -663, -670, -583, -556, -570, -300, 850,
+ 1495, 298, 1490, -1579, -5393, 1137, -9580, 17573, 8715, 3526, 4288,
+ -490, -1083, 280, 3534, -555, -1021, -341, -114, 804, 1521, 139,
+ 355, 844, 673, 325, 735, -35, 1245, 91, 909, -18, 912,
+ 280, 465, 82, 792, 554, 703, 257, 849, 594, 952, 381,
+ 794, 169, 438, 96, 219, 18, 183, -220, -11, -303, 284,
+ -345, -44, -429, -44, -355, -58, -224, -155, -342, -30, -264,
+ -140, -360, -95, -355, -133, -396, -161, -354, -174, -312, -65,
+ -314, -194, -357, -142, -289, -170, -390, -213, -337, -152, -369,
+ -215, -364, -191, -332, -194, -330, -206, -298, -234, -318, -198,
+ -344, -225, -368, -198, -333, -240, -308, -215, -230, -223, -291,
+ -224, -258, -133, -220, -212, -260, -84, -207, -164, -294, -157,
+ -248, -263, -285, -134, -218, -267, -295, -223, -237, -192, -163,
+ -155, -68, 86, -74, -67, -51, -46, -49, -45, -144, -181,
+ -126, -51, -106, -136, -213, -189, -163, -189, -238, -230, -238,
+ -276, -288, -300, -274, -264, -268, -228, -288, -202, -201, -180,
+ -232, -217, -238, -174, -222, -220, -242, -187, -203, -190, -211,
+ -143, -223, -148, -187, -110, -202, -179, -175, -124, -140, -207,
+ -165, -148, -117, -138, -129, -91, -111, -52, -96, -31, -72,
+ -57, -38, -41, -39, -63, -12, -65, -2, -67, 12, -64,
+ 37, -48, 34, -49, 20, -4, 26, -16, 4, -9, 34,
+ -12, 34, -23},
+ /* IRC_Composite_C_R0195_T030_P045.wav */
+ {53, -54, 56, -58, 59, -62, 64, -67, 70, -73, 77,
+ -81, 87, -93, 100, -110, 122, -140, 176, -592, -266, -620,
+ -93, -807, 111, -1033, -244, -1451, 661, -1378, 1038, 309, 3279,
+ -1940, 4376, -7187, -1452, -4658, 38, 22380, 4913, 3629, 1106, -370,
+ -1337, 2958, 848, -390, -2691, 1295, -1155, 2599, -36, 956, 635,
+ 787, 53, 821, 200, 581, 67, 983, 274, 565, 247, 646,
+ 211, 751, 653, 273, 546, 759, 707, 776, 230, 704, 98,
+ 258, -440, 41, -44, -99, -570, -276, 125, -89, -316, -287,
+ -67, -256, -409, -175, -243, -300, -369, -371, -102, -291, -350,
+ -382, -109, -199, -343, -360, -210, -168, -316, -283, -194, -249,
+ -97, -252, -166, -374, -161, -245, -317, -297, -364, -202, -337,
+ -267, -324, -305, -165, -347, -151, -302, -171, -371, -213, -207,
+ -334, -383, -313, -191, -297, -398, -235, -273, -140, -391, -214,
+ -292, -119, -316, -254, -221, -194, -227, -232, -269, -229, -362,
+ -187, -364, -206, -335, -144, -247, -146, -237, -158, -174, -97,
+ -117, 50, -80, 66, -91, 178, -80, -68, -202, -96, -198,
+ -132, -223, -177, -186, -153, -217, -140, -249, -182, -298, -91,
+ -285, -230, -334, -153, -250, -218, -295, -154, -260, -140, -294,
+ -141, -233, -87, -273, -133, -222, -68, -214, -169, -225, -107,
+ -209, -114, -264, -92, -242, -49, -295, -99, -226, -66, -252,
+ -85, -193, -65, -189, -57, -171, -14, -172, -4, -179, 85,
+ -157, 3, -134, 92, -113, -4, -109, 69, -51, 13, -79,
+ 68, -27, 61, -58, 66, -43, 79, -23, 54, -24, 30,
+ -27, 66, -41},
+ /* IRC_Composite_C_R0195_T045_P045.wav */
+ {-71, 74, -76, 79, -82, 84, -87, 90, -93, 96, -99, 101,
+ -102, 102, -97, 85, -52, -97, -607, -227, -812, 438, -1024, -204,
+ -760, 95, -1636, 140, -766, 2279, -763, 3056, 150, 2120, -5760, -6480,
+ 1711, -14, 24602, 5276, 1036, -813, 1031, 1166, 848, 674, -478, -1487,
+ -945, 687, 1855, -292, 799, 1002, 786, 207, 559, 505, 363, 364,
+ 343, 530, 502, 11, 500, 785, 837, 217, 463, 907, 599, 564,
+ 357, 380, -197, 84, -367, 169, -530, -810, -282, -180, -171, -519,
+ -179, -442, -72, -160, -239, -494, -294, -88, -553, -268, -512, -278,
+ -503, -198, -473, -229, -317, -213, -370, -187, -331, -249, -344, -154,
+ -381, -135, -317, -147, -279, -176, -355, -133, -274, -305, -330, -235,
+ -314, -312, -303, -290, -257, -159, -269, -234, -243, -173, -350, -204,
+ -383, -239, -309, -233, -404, -183, -289, -321, -344, -212, -319, -251,
+ -253, -168, -303, -204, -277, -189, -284, -216, -333, -204, -348, -208,
+ -298, -166, -339, -189, -186, -273, -152, -87, -184, -166, 10, 48,
+ -40, 2, 0, -18, -97, -3, -134, -124, -166, -216, -161, -149,
+ -234, -162, -175, -239, -198, -150, -294, -241, -224, -190, -303, -189,
+ -150, -236, -232, -162, -159, -199, -136, -231, -166, -169, -136, -155,
+ -152, -125, -146, -133, -100, -123, -156, -121, -135, -145, -164, -174,
+ -157, -151, -189, -118, -198, -112, -121, -108, -138, -101, -93, -63,
+ -79, -82, -83, -26, -44, -58, -96, 20, -91, -15, -43, 22,
+ -69, 29, 5, 6, -19, 24, 3, 1, 25, 26, 26, 17,
+ 31, 13, 22, 22},
+ /* IRC_Composite_C_R0195_T060_P045.wav */
+ {59, -58, 56, -55, 52, -49, 45, -40, 34, -25, 14,
+ 1, -23, 55, -108, 214, -619, -300, -123, -537, -172, -393,
+ -415, -586, -104, -1306, 69, -335, 1375, 535, 2761, -686, 3824,
+ -8072, -1237, -7735, 11382, 19138, 4865, 2658, -2047, 1916, 571, 2806,
+ -624, -694, -1625, 70, 232, 950, -64, 1404, -414, 1196, 34,
+ 1403, -261, 1089, 343, 834, -188, 587, -251, 1086, 518, 827,
+ 135, 641, 494, 842, -31, 161, -360, -291, -317, 171, -783,
+ -241, -1031, 130, -617, -201, -704, -152, -418, -199, -454, -247,
+ -285, -342, -410, -378, -429, -306, -467, -327, -419, -184, -471,
+ -188, -474, -157, -446, -174, -469, -146, -371, -239, -326, -267,
+ -268, -265, -279, -218, -230, -216, -236, -148, -289, -212, -358,
+ -216, -330, -124, -259, -173, -298, -166, -287, -241, -319, -291,
+ -323, -261, -320, -293, -286, -298, -318, -332, -305, -269, -323,
+ -212, -266, -206, -301, -143, -304, -236, -268, -212, -344, -239,
+ -265, -225, -364, -157, -292, -187, -206, -129, -241, -186, -152,
+ -141, -106, -103, -15, -115, 56, -68, 26, -47, -92, -125,
+ -141, -154, -176, -196, -181, -250, -197, -136, -112, -240, -89,
+ -180, -145, -235, -146, -223, -248, -178, -229, -152, -219, -112,
+ -250, -98, -141, -152, -155, -116, -137, -150, -110, -137, -128,
+ -94, -113, -72, -189, -29, -176, -93, -125, -129, -123, -190,
+ -65, -169, -92, -169, -68, -159, -48, -109, -46, -109, -25,
+ -81, -31, -72, -27, -71, 7, -65, -16, -30, -30, -44,
+ 30, -39, 8, -56, 53, -36, 81, -51, 97, -64, 95,
+ -35, 60, -30},
+ /* IRC_Composite_C_R0195_T075_P045.wav */
+ {7, -7, 6, -6, 5, -5, 4, -3, 2, 0, -2,
+ 5, -9, 14, -23, 45, -729, -86, -463, -18, -765, 398,
+ -1336, 518, -1262, -130, -544, 1878, -781, 3861, -694, 2694, -2479,
+ -6040, -3168, 1816, 21907, 5475, 5383, -2274, 1082, 1645, 3196, -180,
+ -862, -1506, -288, -182, 870, -91, 1493, -388, 779, 238, 870,
+ 64, 614, 1007, 850, 122, 95, 413, 1397, 214, 480, 297,
+ 844, -36, 272, 27, -433, -791, -467, 60, -441, -446, -697,
+ -441, -417, -402, -239, -587, -268, -547, -343, -464, -296, -435,
+ -388, -347, -416, -430, -420, -403, -296, -406, -376, -408, -228,
+ -422, -330, -353, -273, -416, -325, -234, -301, -372, -327, -219,
+ -336, -296, -307, -215, -240, -202, -250, -161, -199, -207, -315,
+ -154, -247, -142, -267, -158, -266, -221, -313, -201, -303, -302,
+ -339, -264, -278, -318, -314, -301, -319, -312, -315, -204, -334,
+ -223, -340, -127, -300, -165, -343, -198, -307, -246, -285, -207,
+ -293, -254, -293, -183, -246, -202, -252, -100, -213, -78, -138,
+ -60, -197, -164, -160, -109, -141, -50, -142, 2, -155, 50,
+ -176, 0, -217, -67, -265, -39, -241, -180, -198, -196, -186,
+ -182, -111, -135, -134, -125, -176, -98, -184, -100, -212, -114,
+ -228, -113, -95, -140, -143, -141, -78, -133, -100, -67, -178,
+ -75, -161, -2, -319, 35, -236, -7, -269, 53, -223, -47,
+ -143, -20, -108, -80, -80, -54, -96, -67, -96, -24, -164,
+ -15, -108, -24, -125, -8, -47, -26, -57, 4, -8, -39,
+ -35, 32, -69, 30, -49, 58, -82, 89, -80, 107, -80,
+ 104, -56, 112},
+ /* IRC_Composite_C_R0195_T090_P045.wav */
+ {82, -84, 86, -88, 90, -91, 93, -94, 95, -95, 94,
+ -92, 87, -78, 61, -33, -240, -426, 56, -683, 113, -744,
+ 373, -1306, 284, -1194, 1644, -667, 2652, -291, 4245, -2423, -2014,
+ -4070, -4168, 13696, 10251, 10475, -3031, 1940, 668, 3850, 1809, -625,
+ -1492, -994, -456, 184, 563, 568, -96, 1027, 330, 539, 32,
+ 648, 1068, 571, 102, 392, 437, 1555, 535, 636, 312, 223,
+ -470, 382, -426, -676, -1277, -176, -342, -251, -706, -452, -477,
+ -459, -100, -511, -346, -481, -454, -509, -470, -397, -448, -337,
+ -587, -233, -611, -143, -563, -280, -521, -364, -455, -244, -466,
+ -275, -326, -321, -388, -219, -441, -210, -415, -297, -415, -244,
+ -390, -213, -373, -184, -325, -193, -152, -257, -243, -230, -170,
+ -254, -69, -214, -166, -228, -242, -227, -305, -281, -292, -232,
+ -343, -218, -305, -280, -254, -331, -243, -363, -249, -287, -245,
+ -320, -255, -262, -272, -215, -275, -191, -269, -200, -292, -200,
+ -287, -217, -267, -214, -217, -218, -189, -189, -148, -203, -120,
+ -154, -118, -165, -102, -132, -161, -134, -144, -132, -168, -94,
+ -134, -87, -100, -96, -67, -104, -58, -178, -114, -154, -134,
+ -226, -145, -224, -215, -130, -174, -117, -174, -71, -88, -63,
+ -109, -58, -78, -120, -50, -84, -79, -101, -63, -97, -110,
+ -65, -129, -123, -160, -83, -162, -109, -152, -92, -143, -76,
+ -108, -73, -110, -42, -74, -64, -99, -35, -69, -11, -79,
+ -57, -78, -51, -41, -69, -60, -68, -23, -64, 48, -114,
+ 27, -96, 75, -109, 79, -84, 99, -121, 109, -64, 69,
+ -52, 115, -98},
+ /* IRC_Composite_C_R0195_T105_P045.wav */
+ {28, -30, 31, -33, 35, -38, 40, -43, 47, -51, 55,
+ -60, 67, -75, 85, -99, 111, -72, -376, -88, -540, 307,
+ -524, -278, -931, 669, 115, 481, 924, 2601, 65, 2397, -5614,
+ -1324, -2647, 10799, 8614, 10099, 118, -2294, 4895, 2386, 2569, -875,
+ -924, -2723, 797, 264, 147, -195, 968, 521, 465, -376, 1032,
+ 858, 1253, -135, 495, 389, 792, 755, 975, 0, 305, -239,
+ -39, -629, -469, -1320, -587, -582, -54, -751, -376, -721, -115,
+ -425, -260, -335, -443, -566, -430, -612, -353, -563, -240, -666,
+ -197, -569, -261, -523, -199, -719, -213, -574, -181, -663, -112,
+ -511, -96, -492, -191, -414, -208, -419, -314, -403, -269, -455,
+ -207, -442, -146, -453, -78, -424, -85, -318, -146, -327, -204,
+ -254, -94, -214, -119, -280, -105, -318, -172, -355, -253, -350,
+ -232, -337, -213, -346, -174, -337, -196, -365, -187, -307, -214,
+ -347, -231, -304, -233, -293, -202, -273, -150, -270, -125, -278,
+ -116, -278, -165, -297, -205, -241, -198, -221, -169, -202, -83,
+ -240, -100, -225, -103, -190, -146, -171, -133, -169, -147, -152,
+ -62, -157, -83, -150, -63, -116, -82, -115, -86, -88, -91,
+ -126, -129, -161, -111, -196, -127, -178, -79, -197, -117, -103,
+ -73, -136, -26, -65, -11, -114, 16, -69, 41, -165, 27,
+ -83, -32, -83, -83, -89, -127, -138, -85, -115, -105, -151,
+ -76, -90, -77, -130, -45, -81, -85, -105, -60, -35, -80,
+ -62, -86, -27, -81, -18, -56, -46, -45, -2, 5, -61,
+ -31, -63, -14, -64, -36, 8, -66, 36, -43, 73, -62,
+ 61, -61, 80},
+ /* IRC_Composite_C_R0195_T120_P045.wav */
+ {-12, 12, -13, 13, -14, 14, -15, 16, -17, 18, -20,
+ 22, -25, 29, -34, 41, -54, 124, -315, 231, -915, 409,
+ -381, 76, -1011, 336, -106, 1161, -312, 2462, 834, 1649, -538,
+ -4939, -381, 2968, 10089, 6155, 8410, -2705, 920, 4167, 3787, 745,
+ -2352, -1802, -28, 292, -651, 645, 868, -434, 709, 424, 388,
+ 1070, 1285, -64, 322, 849, 520, 234, 673, 260, -252, -169,
+ -219, -404, -898, -522, -1000, -760, -659, -37, -489, -625, -480,
+ -117, -279, -318, -554, -444, -479, -476, -447, -397, -506, -426,
+ -374, -367, -517, -341, -428, -392, -509, -321, -429, -316, -424,
+ -291, -372, -230, -336, -266, -337, -314, -357, -245, -417, -326,
+ -393, -223, -400, -276, -346, -207, -347, -196, -289, -214, -324,
+ -195, -253, -149, -240, -179, -253, -165, -279, -187, -327, -224,
+ -367, -235, -338, -247, -316, -185, -311, -218, -272, -162, -301,
+ -187, -304, -196, -322, -148, -322, -157, -312, -104, -274, -142,
+ -240, -141, -246, -193, -235, -171, -192, -186, -214, -189, -212,
+ -181, -191, -123, -229, -156, -208, -159, -193, -117, -178, -146,
+ -179, -127, -164, -97, -166, -64, -139, -51, -149, -26, -150,
+ -13, -111, -43, -117, -85, -121, -102, -128, -113, -107, -143,
+ -80, -75, -82, -86, -55, -52, -89, -61, -20, -13, -63,
+ 41, -66, -14, -65, -25, -99, -66, -90, -83, -119, -73,
+ -60, -96, -94, -16, -118, -43, -110, -43, -94, -27, -153,
+ -76, -126, -21, -94, -45, -92, -51, -33, 3, -63, -11,
+ -52, -9, -73, 9, -41, -20, -40, 3, -44, -34, 0,
+ 0, -32, 16},
+ /* IRC_Composite_C_R0195_T135_P045.wav */
+ {15, -16, 17, -18, 19, -20, 21, -22, 23, -24, 25,
+ -27, 28, -29, 29, -28, 24, -11, -117, -145, -90, -121,
+ -109, -173, -39, -435, 83, -128, 1107, 344, 1609, 548, 3339,
+ -3005, -2346, -79, 2825, 6596, 9304, 4876, -1449, 2789, 1884, 4671,
+ 84, -2193, -1608, -211, -70, 580, 333, 233, 35, 298, -56,
+ 1401, 612, 725, 299, 829, 803, 425, -150, 290, -261, -139,
+ -438, -491, -740, -322, -692, -659, -1013, -400, -489, -130, -431,
+ -461, -332, -161, -302, -454, -443, -489, -403, -443, -472, -442,
+ -454, -357, -427, -305, -456, -382, -411, -333, -418, -365, -303,
+ -376, -334, -316, -292, -311, -265, -339, -352, -281, -323, -329,
+ -379, -310, -346, -316, -320, -334, -291, -274, -280, -297, -290,
+ -272, -270, -230, -233, -229, -239, -263, -224, -254, -270, -293,
+ -279, -264, -289, -263, -287, -260, -261, -232, -231, -207, -225,
+ -198, -204, -183, -223, -189, -221, -183, -247, -188, -250, -189,
+ -248, -190, -209, -183, -176, -163, -144, -170, -178, -173, -185,
+ -158, -195, -164, -234, -222, -222, -206, -165, -198, -160, -175,
+ -170, -167, -137, -130, -154, -126, -141, -104, -142, -91, -116,
+ -80, -71, -71, -88, -42, -93, -59, -74, -48, -70, -66,
+ -85, -79, -76, -90, -60, -94, -51, -65, -11, -46, -25,
+ -16, -13, -19, -24, -51, -74, -67, -91, -21, -96, -77,
+ -66, 2, -73, -36, -56, -44, -134, -42, -35, -70, -118,
+ -47, -68, -116, -72, -82, -75, -136, -24, -85, -36, -90,
+ 1, -62, -4, -20, -33, -14, 1, -17, -33, -28, -35,
+ -37, -9, -16},
+ /* IRC_Composite_C_R0195_T150_P045.wav */
+ {13, -14, 16, -17, 19, -20, 22, -25, 27, -30, 34, -38,
+ 42, -48, 55, -63, 75, -91, 119, -208, 14, -179, 43, -143,
+ 14, -237, 127, -445, 347, 318, 882, 1092, 671, 2337, -2, -2406,
+ -2033, 3403, 2686, 9080, 6065, 1654, -385, 2378, 3537, 1656, -964, -2514,
+ -65, -61, 604, 313, -548, 8, 506, 751, 592, 750, 799, 256,
+ 788, 738, 691, -486, -503, -169, -52, -560, -771, -513, -275, -418,
+ -623, -527, -620, -550, -389, -73, -175, -357, -452, -335, -134, -523,
+ -381, -574, -280, -554, -346, -501, -295, -395, -265, -376, -302, -406,
+ -287, -380, -314, -400, -273, -369, -283, -407, -233, -368, -270, -408,
+ -280, -353, -282, -379, -324, -362, -292, -349, -288, -317, -293, -299,
+ -273, -329, -303, -342, -223, -328, -247, -359, -217, -332, -232, -362,
+ -243, -293, -224, -266, -223, -255, -188, -248, -204, -263, -170, -234,
+ -148, -228, -150, -221, -123, -200, -152, -223, -169, -222, -177, -244,
+ -174, -210, -124, -206, -129, -206, -127, -177, -96, -151, -166, -237,
+ -192, -200, -169, -204, -186, -233, -213, -217, -169, -178, -153, -156,
+ -130, -156, -129, -136, -88, -131, -105, -128, -101, -129, -90, -83,
+ -84, -106, -52, -43, -49, -65, -44, -66, -38, -70, 26, -76,
+ 6, -88, -13, -82, -34, -41, -4, -34, -1, -14, 9, -50,
+ -23, -69, -16, -101, -10, -110, -17, -119, -37, -74, -19, -102,
+ -24, -41, -56, -111, -39, -111, -124, -147, -53, -65, -109, -46,
+ -41, -68, -52, -7, -29, -64, -43, -18, -36, -49, -47, 1,
+ -26, -14, -36, -17},
+ /* IRC_Composite_C_R0195_T165_P045.wav */
+ {-5, 6, -6, 7, -7, 8, -9, 9, -10, 11, -13, 14,
+ -16, 18, -20, 23, -27, 31, -37, 45, -57, 83, -131, 98,
+ -140, 102, -154, 88, -172, 82, 24, 745, 704, 1159, 942, 1377,
+ 685, -3350, -75, 2310, 5071, 6483, 6504, -342, 276, 2310, 3436, 1307,
+ -1968, -1879, -504, 1149, 580, -184, -486, 250, 728, 683, 585, 651,
+ 265, 901, 267, 694, -240, -105, -607, -269, -676, -500, -479, -378,
+ -278, -303, -345, -367, -327, -471, -422, -217, -111, -233, -480, -413,
+ -399, -332, -479, -354, -471, -387, -344, -260, -374, -267, -230, -298,
+ -347, -353, -301, -344, -328, -357, -343, -372, -361, -346, -368, -366,
+ -338, -358, -344, -365, -318, -353, -318, -336, -351, -319, -335, -289,
+ -334, -309, -321, -326, -336, -348, -335, -354, -321, -327, -304, -329,
+ -269, -270, -235, -245, -249, -208, -236, -185, -237, -214, -241, -196,
+ -206, -169, -200, -166, -161, -150, -185, -187, -205, -193, -210, -166,
+ -170, -138, -171, -155, -201, -170, -152, -109, -129, -189, -178, -185,
+ -130, -167, -150, -206, -233, -222, -196, -166, -206, -190, -201, -159,
+ -173, -138, -175, -147, -160, -114, -124, -105, -119, -77, -100, -117,
+ -136, -90, -111, -91, -103, -87, -79, -69, -28, -22, -44, -41,
+ -25, -6, -27, -13, -21, -10, -27, 9, 1, -13, 14, -2,
+ 20, 6, -17, -7, -23, -45, -82, -88, -89, -86, -91, -67,
+ -101, -82, -96, -74, -71, -81, -64, -98, -60, -94, -66, -105,
+ -103, -119, -65, -33, -28, -21, -24, -24, -66, -22, -25, -29,
+ -63, -45, -61, -77},
+ /* IRC_Composite_C_R0195_T180_P045.wav */
+ {1, -1, 1, -1, 2, -2, 2, -2, 2, -2, 2, -2,
+ 3, -3, 3, -3, 3, -4, 4, -4, 4, -4, 3, -2,
+ 9, -9, 70, -59, 121, -126, 127, -45, 222, 619, 653, 1289,
+ 587, 1432, 300, -1888, -1240, 3381, 3947, 6141, 5035, 479, -148, 2113,
+ 2998, 773, -1353, -1664, 690, 1041, -58, -664, -159, 711, 746, 445,
+ 284, 522, 787, 289, 291, 216, 96, -622, -296, -616, -684, -562,
+ -153, -193, -156, -160, -230, -111, -119, -291, -584, -298, -144, -240,
+ -398, -579, -379, -509, -208, -402, -311, -292, -227, -267, -259, -266,
+ -213, -263, -327, -375, -294, -371, -362, -434, -364, -436, -381, -431,
+ -375, -404, -373, -398, -329, -365, -326, -409, -345, -385, -333, -367,
+ -288, -357, -297, -388, -309, -398, -314, -430, -330, -408, -271, -349,
+ -243, -310, -230, -234, -197, -214, -201, -187, -203, -238, -232, -238,
+ -212, -225, -205, -211, -176, -179, -201, -211, -228, -202, -224, -150,
+ -202, -129, -214, -123, -184, -94, -133, -120, -193, -166, -163, -113,
+ -144, -135, -197, -168, -225, -183, -213, -177, -199, -157, -207, -155,
+ -195, -92, -173, -121, -200, -130, -155, -103, -148, -111, -166, -85,
+ -143, -56, -147, -56, -154, -59, -141, -36, -120, -42, -95, -13,
+ -67, 16, -28, 44, -44, 24, -33, 43, -27, 82, -21, 103,
+ -17, 122, -2, 81, -69, 27, -35, -19, -94, -112, -111, -109,
+ -91, -94, -119, -134, -85, -80, -83, -129, -79, -97, -58, -112,
+ -41, -119, -24, -104, -33, -99, -41, -79, -41, -68, -44, -36,
+ -44, -71, -84, -70},
+ /* IRC_Composite_C_R0195_T195_P045.wav */
+ {-10, 11, -11, 11, -12, 12, -12, 13, -13, 14, -14, 15,
+ -15, 16, -17, 17, -18, 19, -20, 21, -22, 23, -24, 26,
+ -28, 29, -70, 1, 21, -50, -12, 7, -98, 118, -221, 432,
+ 295, 945, 530, 962, 1220, -497, -1827, 293, 2669, 3717, 6133, 2421,
+ 667, 647, 2517, 2286, 876, -1019, -811, 715, 567, 146, -552, 68,
+ 761, 748, 406, 192, 794, 584, 428, 293, 114, -104, -113, -153,
+ -376, -408, -234, -64, -66, -15, 75, -43, -32, -84, -58, -289,
+ -255, -381, -227, -301, -267, -362, -351, -257, -250, -265, -197, -240,
+ -267, -205, -195, -288, -234, -313, -331, -391, -314, -376, -403, -414,
+ -376, -388, -386, -415, -381, -386, -367, -392, -359, -382, -358, -395,
+ -338, -337, -339, -329, -314, -348, -381, -383, -383, -385, -396, -353,
+ -328, -277, -291, -244, -249, -220, -237, -207, -212, -209, -222, -227,
+ -219, -186, -196, -201, -196, -190, -194, -210, -219, -243, -209, -201,
+ -163, -195, -186, -186, -145, -123, -130, -154, -175, -167, -170, -122,
+ -126, -124, -185, -179, -196, -160, -179, -161, -199, -183, -176, -134,
+ -131, -147, -153, -166, -151, -161, -167, -166, -166, -160, -162, -139,
+ -166, -135, -153, -143, -142, -125, -105, -102, -94, -90, -78, -72,
+ -57, -50, -51, -21, -44, -9, -31, -16, -46, -3, -13, 24,
+ 10, 27, 32, 25, 66, -13, -17, -41, -15, -51, -100, -128,
+ -126, -123, -107, -116, -127, -132, -96, -99, -88, -127, -87, -121,
+ -70, -130, -87, -96, -70, -95, -94, -64, -80, -73, -64, -54,
+ -55, -52, -44, -57},
+ /* IRC_Composite_C_R0195_T210_P045.wav */
+ {-1, 1, -1, 1, -1, 1, -1, 1, -2, 2, -2, 2,
+ -2, 2, -2, 3, -3, 3, -3, 4, -4, 4, -5, 5,
+ -6, 6, -7, 9, -41, -44, -65, -30, -65, -8, -83, -60,
+ -92, 30, 110, 507, 445, 762, 568, 1174, -1167, -1019, 628, 2221,
+ 3797, 4340, 2039, 369, 1655, 2128, 2575, -74, -532, -44, 828, 327,
+ -261, -60, 394, 940, 522, 463, 465, 701, 615, 239, 186, 177,
+ 226, 24, 55, -107, -121, -144, 122, 80, 101, 60, 168, 75,
+ 69, -42, -103, -208, -202, -275, -257, -213, -175, -274, -214, -201,
+ -239, -203, -186, -264, -269, -249, -233, -294, -292, -329, -314, -341,
+ -364, -402, -341, -398, -395, -448, -334, -429, -332, -431, -329, -423,
+ -331, -395, -337, -340, -332, -347, -337, -368, -357, -389, -353, -376,
+ -312, -331, -265, -307, -238, -251, -202, -250, -204, -228, -195, -243,
+ -216, -235, -186, -234, -184, -216, -198, -218, -214, -243, -226, -238,
+ -184, -210, -192, -215, -154, -146, -144, -168, -181, -177, -171, -140,
+ -114, -169, -137, -193, -142, -178, -151, -186, -152, -183, -144, -144,
+ -116, -143, -141, -149, -130, -174, -129, -185, -122, -193, -143, -180,
+ -134, -169, -148, -166, -136, -141, -122, -130, -103, -114, -106, -102,
+ -72, -91, -60, -67, -53, -70, -51, -40, -61, -20, -57, 16,
+ -56, 22, -58, 21, -49, -10, -48, 3, -48, -36, -55, -46,
+ -67, -83, -96, -101, -127, -82, -139, -122, -145, -101, -121, -109,
+ -139, -130, -124, -118, -116, -120, -95, -97, -71, -73, -66, -58,
+ -48, -24, -31, -11},
+ /* IRC_Composite_C_R0195_T225_P045.wav */
+ {-3, 3, -3, 3, -3, 3, -3, 4, -4, 4, -4, 4,
+ -4, 4, -4, 5, -5, 5, -5, 5, -6, 6, -6, 7,
+ -7, 8, -9, 10, -13, 20, -75, -97, -66, -47, -85, -74,
+ -101, -34, -123, -132, -22, 222, 292, 490, 466, 903, 275, -1049,
+ -389, 584, 2358, 3669, 3152, 1258, 1261, 1738, 2331, 1624, 37, -28,
+ 186, 674, 40, 219, 313, 646, 713, 656, 528, 603, 598, 473,
+ 348, 444, 144, 373, 151, 223, 11, 95, 112, 186, 158, 153,
+ 282, 115, 221, 30, 18, -91, -32, -200, -106, -202, -164, -212,
+ -141, -234, -274, -248, -205, -234, -252, -279, -220, -289, -259, -337,
+ -286, -358, -337, -398, -361, -383, -377, -397, -403, -387, -416, -383,
+ -391, -375, -383, -384, -351, -350, -337, -371, -325, -339, -314, -362,
+ -307, -327, -273, -302, -260, -293, -265, -270, -253, -272, -249, -242,
+ -222, -243, -203, -221, -193, -215, -188, -249, -219, -244, -222, -251,
+ -227, -225, -219, -214, -206, -186, -149, -158, -170, -204, -178, -182,
+ -146, -152, -156, -169, -175, -163, -157, -142, -159, -167, -156, -155,
+ -165, -170, -148, -148, -131, -118, -114, -117, -133, -119, -145, -148,
+ -151, -147, -145, -167, -157, -166, -139, -171, -158, -145, -135, -121,
+ -133, -97, -119, -63, -84, -67, -101, -69, -67, -57, -75, -59,
+ -62, -49, -63, -41, -77, -33, -68, -14, -83, -22, -64, -11,
+ -77, -48, -83, -73, -129, -98, -117, -128, -152, -116, -144, -123,
+ -154, -107, -170, -138, -138, -97, -115, -107, -82, -89, -65, -72,
+ -83, -58, -72, -30},
+ /* IRC_Composite_C_R0195_T240_P045.wav */
+ {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1,
+ -1, 1, -1, 1, -1, 2, -5, -83, -84, -72, -135, -73,
+ -91, -109, -87, -133, -84, -173, -91, -36, 199, 307, 354, 685,
+ 469, -284, -594, -516, 1131, 3046, 2851, 2215, 1571, 1578, 1884, 1712,
+ 836, 241, 420, 708, 320, 266, 499, 693, 588, 631, 732, 645,
+ 553, 384, 505, 486, 415, 335, 428, 309, 250, 327, 166, 248,
+ 139, 364, 147, 249, 184, 201, 90, 89, -4, -84, -57, -140,
+ -149, -268, -170, -229, -181, -241, -235, -260, -225, -272, -259, -355,
+ -270, -344, -221, -429, -254, -394, -305, -443, -348, -406, -390, -405,
+ -383, -420, -382, -385, -345, -366, -308, -315, -295, -317, -301, -307,
+ -307, -319, -316, -337, -273, -310, -260, -332, -260, -287, -238, -267,
+ -235, -236, -259, -223, -218, -231, -226, -221, -205, -245, -216, -236,
+ -203, -244, -208, -224, -221, -211, -210, -180, -186, -192, -188, -204,
+ -183, -150, -161, -144, -182, -151, -199, -152, -176, -159, -170, -168,
+ -142, -147, -118, -110, -117, -97, -125, -84, -116, -88, -125, -124,
+ -122, -160, -117, -177, -157, -199, -179, -171, -156, -148, -174, -137,
+ -139, -117, -130, -135, -112, -124, -104, -131, -80, -123, -82, -99,
+ -80, -101, -46, -83, -75, -90, -80, -63, -59, -51, -122, -65,
+ -82, -66, -92, -66, -83, -92, -88, -117, -122, -158, -126, -150,
+ -152, -142, -139, -83, -139, -101, -146, -84, -121, -95, -110, -91,
+ -88, -79, -72, -47},
+ /* IRC_Composite_C_R0195_T255_P045.wav */
+ {-1, 1, -1, 1, -1, 1, -1, 1, -2, 2, -2, 2,
+ -2, 2, -2, 2, -3, 3, -3, 3, -4, 4, -4, 5,
+ -5, 6, -6, 7, -8, 10, -12, 15, -21, 35, -138, -127,
+ -91, -119, -131, -100, -136, -154, -123, -162, -225, -17, 208, 157,
+ 422, 539, 315, -186, -874, -513, 1317, 2834, 3212, 2035, 1165, 1380,
+ 1985, 1825, 760, 470, 698, 708, 299, 840, 390, 551, 566, 771,
+ 555, 435, 745, 529, 606, 365, 682, 399, 459, 406, 443, 291,
+ 358, 302, 320, 305, 286, 249, 233, 146, 69, 90, 0, -26,
+ -85, -106, -54, -197, -179, -210, -226, -306, -252, -328, -293, -273,
+ -273, -342, -265, -302, -315, -353, -309, -387, -325, -457, -297, -442,
+ -355, -408, -318, -407, -303, -346, -296, -308, -269, -329, -219, -337,
+ -263, -338, -282, -364, -295, -326, -281, -306, -274, -302, -265, -294,
+ -241, -286, -262, -250, -230, -264, -201, -225, -215, -219, -198, -213,
+ -212, -205, -209, -201, -251, -202, -242, -212, -196, -186, -200, -172,
+ -211, -195, -206, -149, -217, -164, -212, -147, -189, -117, -143, -88,
+ -129, -55, -121, -125, -117, -90, -114, -92, -138, -75, -136, -123,
+ -181, -119, -197, -132, -134, -131, -172, -162, -136, -187, -159, -156,
+ -181, -172, -159, -163, -167, -116, -199, -172, -135, -102, -126, -131,
+ -78, -101, -129, -76, -94, -120, -143, -86, -127, -74, -98, -46,
+ -113, -79, -97, -44, -125, -131, -113, -86, -115, -124, -120, -129,
+ -130, -91, -134, -118, -138, -78, -131, -110, -118, -103, -103, -97,
+ -94, -71, -62, -45},
+ /* IRC_Composite_C_R0195_T270_P045.wav */
+ {-2, 2, -2, 2, -2, 2, -2, 2, -2, 2, -2, 2,
+ -2, 2, -2, 2, -2, 2, -2, 2, -2, 2, -2, 2,
+ -2, 2, -2, 3, -3, 4, -4, 6, -9, 16, -64, -161,
+ -143, -126, -145, -185, -145, -161, -193, -254, -120, -230, -52, 146,
+ 316, 329, 329, 253, -502, -967, -275, 2022, 3347, 2710, 947, 1504,
+ 1884, 1375, 1901, 809, 618, 609, 671, 737, 376, 610, 640, 652,
+ 766, 573, 669, 526, 783, 441, 547, 504, 550, 598, 531, 667,
+ 314, 551, 275, 554, 115, 507, 96, 221, 103, 170, 93, 44,
+ 90, -71, 5, -109, -137, -246, -250, -241, -295, -289, -315, -295,
+ -325, -258, -316, -295, -327, -300, -348, -332, -372, -354, -355, -374,
+ -324, -355, -324, -341, -316, -283, -312, -268, -304, -257, -336, -239,
+ -313, -303, -322, -321, -295, -351, -234, -324, -242, -342, -262, -303,
+ -265, -288, -272, -254, -289, -193, -250, -195, -234, -188, -219, -212,
+ -194, -217, -186, -231, -179, -233, -211, -209, -215, -227, -234, -228,
+ -223, -193, -202, -196, -175, -156, -144, -145, -108, -127, -113, -111,
+ -50, -105, -58, -119, -95, -117, -140, -166, -165, -130, -150, -114,
+ -120, -117, -146, -129, -172, -175, -166, -189, -168, -160, -197, -193,
+ -208, -220, -186, -186, -158, -220, -160, -145, -123, -177, -155, -188,
+ -132, -141, -109, -129, -71, -127, -88, -107, -92, -141, -124, -132,
+ -136, -104, -99, -93, -109, -90, -81, -97, -101, -102, -111, -126,
+ -107, -115, -121, -123, -88, -123, -117, -104, -75, -79, -90, -92,
+ -70, -77, -68, -59},
+ /* IRC_Composite_C_R0195_T285_P045.wav */
+ {5, -5, 5, -5, 5, -5, 5, -5, 5, -5, 5, -5,
+ 5, -5, 5, -5, 5, -5, 5, -5, 5, -5, 5, -5,
+ 5, -5, 6, -6, 6, -7, 8, -12, 29, 101, 50, 50,
+ 117, 188, 58, 104, 258, 239, 166, 306, 686, 455, 833, 834,
+ 700, 494, 424, 49, 743, 2627, 3265, 3156, 1306, -112, 1114, 559,
+ 356, 246, 113, -51, -113, 695, 8, 344, 126, 384, -46, 304,
+ 134, 58, 125, -42, 60, -117, 150, -115, 60, -66, -74, -290,
+ -174, -222, -336, -410, -393, -454, -500, -436, -457, -579, -566, -638,
+ -618, -691, -652, -777, -695, -736, -701, -743, -633, -655, -650, -600,
+ -573, -602, -585, -524, -544, -563, -496, -527, -483, -513, -414, -513,
+ -387, -442, -351, -441, -335, -395, -360, -389, -346, -380, -350, -337,
+ -339, -350, -286, -303, -303, -289, -278, -317, -289, -279, -256, -274,
+ -224, -223, -201, -206, -165, -204, -151, -205, -147, -195, -148, -200,
+ -177, -178, -170, -179, -162, -156, -149, -146, -135, -109, -105, -142,
+ -94, -76, -93, -61, -56, -26, -52, -21, -71, -105, -113, -130,
+ -140, -154, -74, -110, -107, -129, -114, -156, -108, -147, -143, -197,
+ -121, -103, -164, -177, -160, -133, -135, -137, -138, -104, -105, -75,
+ -105, -83, -126, -87, -67, -44, -97, -18, -59, -13, -45, -23,
+ -53, -56, -43, -12, -14, -6, -40, 1, -18, 27, -19, 35,
+ -53, 6, -9, 7, 11, 2, -8, -3, 22, 16, -2, 32,
+ 44, 18, -5, 28, 17, 14, 19, 9, 27, 26, 56, 44,
+ 54, 64, 65, 88},
+ /* IRC_Composite_C_R0195_T300_P045.wav */
+ {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 0, 0, 0, 1, -2, 6, -170, -102, -259, -234,
+ -199, -268, -51, -528, -137, -354, -180, 92, -180, 340, 604, 11,
+ -371, -583, -984, 10, 2699, 3142, 3080, 1412, 327, 1987, 956, 830,
+ 764, 347, 196, 382, 1091, 294, 859, 889, 945, 523, 1199, 597,
+ 582, 854, 625, 694, 469, 918, 446, 675, 763, 646, 482, 552,
+ 534, 540, 563, 394, 300, 434, 396, 321, 144, 359, -94, 142,
+ -43, 165, -138, 6, -183, -179, -221, -161, -328, -285, -267, -402,
+ -333, -327, -294, -327, -363, -296, -325, -259, -291, -320, -304, -304,
+ -298, -291, -297, -314, -280, -329, -297, -314, -281, -234, -352, -241,
+ -283, -217, -315, -280, -263, -279, -310, -268, -299, -296, -261, -277,
+ -280, -277, -225, -216, -261, -203, -214, -192, -239, -227, -233, -213,
+ -266, -208, -195, -200, -197, -191, -188, -186, -205, -150, -205, -153,
+ -112, -151, -87, -35, -120, -124, -179, -137, -157, -174, -117, -144,
+ -171, -153, -152, -138, -131, -174, -135, -128, -150, -114, -214, -174,
+ -206, -228, -215, -210, -194, -196, -208, -219, -189, -187, -156, -164,
+ -222, -168, -262, -148, -197, -176, -237, -214, -212, -100, -129, -146,
+ -213, -165, -123, -124, -133, -162, -159, -151, -116, -118, -122, -155,
+ -138, -137, -123, -99, -108, -104, -101, -84, -78, -75, -64, -83,
+ -84, -92, -41, -94, -60, -107, -53, -86, -49, -66, -60, -68,
+ -31, -65, -31, -29},
+ /* IRC_Composite_C_R0195_T315_P045.wav */
+ {-1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1,
+ -1, 1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1,
+ -1, 2, -2, 4, -7, 17, -100, -204, -340, -371, -160, -344,
+ -275, -415, -419, -249, 41, -182, 77, 891, 131, 2, -1052, -1535,
+ 924, 162, 4003, 4715, 1544, 1179, 621, 1424, 674, 1203, -197, 55,
+ 273, 545, 402, 710, 753, 673, 897, 715, 647, 832, 758, 686,
+ 732, 814, 620, 688, 779, 690, 604, 649, 586, 620, 586, 617,
+ 478, 512, 430, 467, 385, 389, 275, 162, 250, 131, 159, 97,
+ 100, -42, -92, -107, -156, -193, -270, -280, -274, -295, -312, -336,
+ -301, -367, -310, -306, -332, -327, -275, -299, -308, -302, -264, -320,
+ -240, -345, -250, -325, -245, -353, -231, -309, -234, -308, -240, -335,
+ -236, -284, -266, -330, -242, -316, -259, -281, -275, -309, -222, -254,
+ -279, -277, -236, -254, -242, -235, -247, -199, -188, -240, -203, -141,
+ -139, -214, -131, -200, -170, -111, -70, -193, -145, -148, -101, -173,
+ -117, -218, -163, -214, -197, -208, -111, -206, -113, -191, -104, -100,
+ -70, -142, -89, -169, -106, -184, -156, -187, -185, -255, -195, -151,
+ -192, -223, -180, -149, -215, -245, -216, -248, -232, -230, -274, -292,
+ -203, -223, -171, -218, -208, -215, -141, -139, -194, -204, -163, -184,
+ -155, -159, -154, -164, -134, -151, -130, -134, -130, -152, -122, -129,
+ -133, -120, -106, -118, -102, -72, -84, -69, -84, -65, -68, -55,
+ -55, -56, -72, -45, -57, -43, -50, -64, -34, -44, -40, -36,
+ -22, -14, -32, 2},
+ /* IRC_Composite_C_R0195_T330_P045.wav */
+ {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
+ 0, 0, 1, -1, 1, -1, 1, -1, 1, -2, 2, -2,
+ 3, -4, 5, -8, -246, -348, -392, -294, -392, -112, -819, -204,
+ -558, 205, -381, 275, 768, 476, 319, -1186, -2693, 1397, 173, 2904,
+ 7126, 2191, 1840, -141, 1440, 643, 1148, 280, -721, 280, 227, 474,
+ 413, 704, 690, 753, 889, 309, 920, 476, 802, 503, 682, 628,
+ 623, 869, 602, 783, 626, 695, 707, 691, 850, 635, 757, 447,
+ 558, 418, 394, 227, 133, 146, 217, 152, 239, 143, 20, 23,
+ 13, -113, -130, -110, -235, -231, -231, -223, -290, -311, -279, -339,
+ -297, -335, -258, -358, -326, -332, -275, -283, -293, -287, -240, -279,
+ -296, -244, -248, -261, -257, -280, -251, -271, -271, -291, -272, -276,
+ -292, -293, -287, -273, -297, -261, -259, -291, -282, -238, -254, -285,
+ -269, -242, -239, -234, -237, -211, -232, -160, -167, -153, -54, -121,
+ -133, -68, -127, -55, -93, -170, -253, -307, -209, -255, -315, -307,
+ -329, -320, -102, -172, -106, -41, -45, 45, -125, 12, -25, -98,
+ -177, -185, -220, -166, -179, -213, -164, -210, -152, -240, -217, -183,
+ -300, -182, -230, -224, -316, -238, -276, -217, -287, -209, -290, -207,
+ -191, -176, -237, -198, -251, -191, -184, -201, -245, -147, -177, -164,
+ -190, -112, -183, -108, -171, -100, -163, -85, -153, -101, -137, -74,
+ -146, -57, -120, -58, -110, -43, -90, -50, -94, -38, -79, -64,
+ -64, -54, -43, -59, -47, -30, -23, -30, -38, -10, -18, 12,
+ -13, 10, -24, 5},
+ /* IRC_Composite_C_R0195_T345_P045.wav */
+ {16, -16, 16, -16, 16, -16, 16, -16, 16, -15, 15, -14,
+ 14, -13, 11, -9, 7, -4, 0, 5, -13, 25, -45, 84,
+ -233, -338, -363, -485, -200, -447, -145, -898, -330, -612, 95, -690,
+ 294, 625, 1287, -151, 69, -4722, 1521, -1345, 1556, 10396, 3052, 2742,
+ 68, 1282, 287, 1583, 813, -614, -309, 335, 28, 768, 552, 558,
+ 584, 902, 480, 734, 438, 738, 500, 576, 491, 546, 493, 681,
+ 435, 611, 561, 752, 683, 864, 812, 738, 819, 537, 703, 581,
+ 367, 218, 0, 254, 20, 344, -127, 37, -166, 211, -28, -62,
+ -54, -160, -27, -117, -166, -287, -249, -103, -237, -262, -426, -232,
+ -222, -305, -355, -320, -241, -298, -264, -311, -247, -264, -248, -253,
+ -288, -211, -240, -212, -321, -248, -261, -248, -306, -335, -270, -295,
+ -235, -360, -279, -274, -198, -287, -320, -253, -257, -194, -270, -236,
+ -243, -201, -255, -169, -230, -203, -38, -204, -189, 64, -47, -32,
+ -96, -24, -149, -249, -211, -363, -445, -322, -199, -451, -374, -212,
+ -206, -56, -101, 52, -41, 90, 144, 4, -84, 38, -111, -234,
+ -118, -217, -227, -246, -157, -274, -235, -253, -192, -341, -200, -287,
+ -253, -258, -299, -242, -317, -247, -273, -208, -264, -246, -273, -176,
+ -209, -205, -204, -238, -186, -184, -158, -218, -216, -193, -143, -174,
+ -141, -179, -144, -152, -78, -161, -124, -122, -54, -126, -102, -95,
+ -62, -64, -102, -54, -75, -51, -32, -78, -64, -53, -39, -81,
+ -33, -75, -14, -32, -27, -68, 2, -35, 2, -22, 5, -24,
+ 24, -12, -6, 0}};
+
+const int16_t irc_composite_c_r0195_p060[][256] =
+ {/* IRC_Composite_C_R0195_T000_P060.wav */
+ {6, -6, 6, -6, 6, -6, 6, -6, 6, -6, 6,
+ -6, 6, -6, 5, -5, 5, -6, 7, -9, 18, -62,
+ -477, -304, -336, -458, -335, -491, -402, -829, -409, -707, -293,
+ -66, 983, 571, 504, 539, -3665, -1218, -4247, 2405, 12687, 3792,
+ 3387, 222, 562, 745, 2239, 1323, -741, -286, -83, 813, 783,
+ 665, -6, 1300, 432, 669, 373, 824, 513, 741, 332, 740,
+ 326, 679, 126, 625, 66, 777, 213, 737, 590, 515, 621,
+ 819, 662, 597, 612, 571, 221, 378, 180, 456, -17, 193,
+ -19, 201, -103, 188, -114, -50, -223, 41, -272, -7, -204,
+ -73, -274, -27, -179, -118, -266, -52, -236, -150, -298, -185,
+ -328, -211, -330, -359, -321, -288, -288, -390, -233, -301, -238,
+ -287, -186, -257, -179, -231, -185, -249, -207, -244, -242, -257,
+ -261, -239, -326, -213, -312, -252, -311, -214, -308, -230, -270,
+ -210, -269, -227, -231, -256, -281, -231, -249, -231, -210, -238,
+ -174, -214, -204, -218, -156, -225, -179, -158, -114, -151, -171,
+ -128, -150, -184, -87, -97, -145, -171, -201, -199, -179, -259,
+ -239, -309, -310, -314, -227, -196, -170, -192, -26, -115, 43,
+ -61, 74, -180, -52, -139, -83, -192, -18, -187, -104, -111,
+ -67, -175, -118, -149, -164, -160, -118, -201, -219, -161, -187,
+ -241, -189, -199, -195, -205, -163, -179, -154, -122, -150, -171,
+ -167, -152, -190, -183, -192, -193, -159, -180, -150, -160, -87,
+ -151, -131, -124, -50, -117, -92, -81, -58, -70, -16, -38,
+ -34, -12, 39, -10, 47, 38, 87, 17, 83, 31, 82,
+ 15, 52, 1},
+ /* IRC_Composite_C_R0195_T030_P060.wav */
+ {20, -21, 22, -23, 24, -25, 26, -28, 29, -31, 33,
+ -35, 37, -40, 44, -49, 57, -69, 99, -268, -386, -296,
+ -341, -306, -439, -461, -219, -926, -450, -515, -185, -151, 2064,
+ -248, 2208, -557, -2551, -2664, -6482, 6173, 13499, 9000, -215, 137,
+ 1397, 897, 1847, 1331, -989, -1152, -31, 458, 894, 968, 314,
+ 922, 758, 306, 312, 862, 265, 458, 604, 466, 114, 555,
+ 424, 349, 321, 475, 497, 583, 497, 442, 464, 481, 505,
+ 592, 441, 239, 76, 20, 163, 45, -273, -262, -336, -166,
+ -64, -152, -189, -166, -140, -223, -230, -243, -348, -277, -328,
+ -243, -252, -223, -247, -223, -225, -225, -211, -310, -229, -259,
+ -253, -320, -259, -356, -330, -309, -289, -302, -271, -256, -257,
+ -245, -269, -281, -283, -267, -256, -268, -267, -280, -263, -261,
+ -261, -284, -289, -275, -272, -247, -274, -279, -271, -276, -305,
+ -292, -268, -282, -266, -248, -248, -229, -247, -221, -202, -217,
+ -233, -213, -204, -239, -209, -216, -194, -227, -155, -201, -185,
+ -243, -159, -264, -210, -189, -201, -282, -175, -151, -178, -186,
+ -73, -151, -201, -182, -71, -254, -132, -94, -52, -235, -22,
+ -27, -101, -159, -17, -130, -166, -55, -23, -206, -132, -126,
+ -159, -211, -63, -187, -189, -148, -84, -174, -142, -124, -126,
+ -185, -149, -155, -156, -187, -149, -205, -136, -153, -105, -164,
+ -138, -137, -75, -158, -128, -135, -115, -148, -94, -125, -133,
+ -123, -51, -120, -102, -85, -18, -84, -52, -42, 8, -51,
+ 10, 0, 9, -20, 40, 16, 14, 24, 69, 21, 25,
+ 23, 33, 2},
+ /* IRC_Composite_C_R0195_T060_P060.wav */
+ {49, -49, 50, -50, 49, -49, 48, -47, 45, -43, 39,
+ -34, 27, -17, 1, 24, -67, 158, -573, 27, -273, -533,
+ -199, -358, -57, -1237, 277, -801, -585, 81, 1678, -52, 2584,
+ -635, 2281, -6550, -1907, -4733, 12287, 16138, 2134, 560, -1141, 3390,
+ 1047, 2071, -850, -288, -1730, 541, 499, 654, 282, 1081, 13,
+ 1053, -59, 982, 296, 639, 439, 387, 97, 735, -102, 457,
+ 298, 541, 229, 951, 144, 642, -123, 725, 239, 371, -407,
+ 173, -217, 5, -418, 64, -709, -238, -708, -195, -402, -221,
+ -414, -199, -238, -175, -279, -243, -447, -227, -531, -270, -429,
+ -178, -455, -173, -350, -170, -312, -238, -284, -268, -355, -248,
+ -402, -300, -358, -251, -472, -244, -350, -274, -353, -195, -309,
+ -279, -276, -243, -232, -248, -185, -198, -344, -177, -263, -274,
+ -355, -198, -341, -222, -358, -178, -344, -238, -289, -271, -338,
+ -286, -218, -316, -206, -256, -230, -264, -214, -239, -277, -261,
+ -259, -225, -268, -221, -134, -246, -182, -192, -154, -260, -116,
+ -218, -234, -234, -227, -254, -254, -224, -164, -220, -284, -56,
+ -257, -45, -289, -17, -299, 22, -225, 103, -264, -36, -33,
+ -158, -37, -147, 39, -238, -62, -117, -69, -262, 42, -153,
+ -162, -86, -22, -214, -31, -193, -19, -225, -87, -128, -143,
+ -84, -102, -114, -95, -106, -146, -143, -5, -308, -86, -140,
+ -102, -254, 41, -68, -208, 37, -26, -47, -124, 31, -99,
+ -131, -35, -104, -59, -141, 32, -115, 3, -48, 1, -54,
+ 32, -67, -4, -31, -40, -50, 45, -30, -17, 43, 3,
+ 8, 10, 37},
+ /* IRC_Composite_C_R0195_T090_P060.wav */
+ {20, -22, 23, -25, 26, -28, 30, -33, 35, -38, 41,
+ -45, 49, -54, 60, -66, 75, -87, -10, -413, -67, -233,
+ -277, -289, -247, -282, -617, -150, -183, 967, 613, 1964, 132,
+ 3132, -4890, -30, -7492, 6867, 13211, 9263, 1898, -2424, 3417, 1010,
+ 3400, -250, -877, -1856, 320, -121, 560, 520, 754, 353, 491,
+ -65, 931, 362, 494, 135, 792, 604, 460, -39, 664, 258,
+ 476, 82, 671, 151, 141, 64, 562, -256, -248, -550, -170,
+ -357, -396, -602, -158, -390, -415, -610, -298, -495, -302, -712,
+ -205, -411, -237, -411, -197, -452, -250, -501, -239, -421, -284,
+ -481, -170, -438, -181, -420, -184, -419, -262, -417, -218, -376,
+ -336, -394, -333, -356, -335, -390, -304, -355, -247, -352, -237,
+ -395, -250, -384, -156, -227, -147, -266, -148, -197, -207, -288,
+ -280, -286, -312, -317, -266, -316, -238, -306, -223, -320, -187,
+ -342, -177, -339, -198, -318, -212, -319, -219, -304, -241, -273,
+ -207, -279, -190, -248, -107, -255, -126, -251, -112, -272, -95,
+ -249, -163, -250, -133, -247, -177, -254, -198, -235, -194, -229,
+ -212, -194, -191, -142, -191, -92, -153, -45, -145, -65, -138,
+ -64, -93, -82, -65, -113, -49, -140, -78, -118, -32, -137,
+ -37, -107, -30, -59, -142, -106, -132, -11, -240, 49, -159,
+ 20, -179, 55, -117, -17, -81, -27, -68, -101, -72, -98,
+ -48, -107, -136, -19, -93, -74, -119, 94, -211, 28, -129,
+ 49, -157, 78, -96, 21, -143, 105, -58, -15, -28, 13,
+ -42, 14, -40, -16, -41, -38, -39, 21, -40, -15, -19,
+ -23, -62, 3},
+ /* IRC_Composite_C_R0195_T120_P060.wav */
+ {-11, 11, -11, 11, -11, 10, -10, 10, -9, 9, -8,
+ 7, -6, 4, -1, -4, 11, -23, 54, -588, 228, -347,
+ 10, -393, 78, -279, -133, -619, 373, 107, 880, 1033, 1066,
+ 2024, -241, -1833, -4036, 1812, 3742, 13338, 5090, 1325, -1146, 2733,
+ 2814, 1516, -118, -2246, -488, 96, 262, 192, 815, 13, 476,
+ 529, 495, 181, 475, 265, 780, 210, 341, 81, 498, 784,
+ 541, 47, -30, -8, 36, -29, -17, -592, -609, -210, -251,
+ -604, -616, -485, -190, -407, -234, -662, -290, -550, -301, -639,
+ -361, -458, -250, -462, -279, -431, -205, -390, -287, -423, -271,
+ -396, -265, -443, -263, -432, -176, -477, -278, -447, -188, -475,
+ -218, -453, -216, -462, -223, -420, -282, -431, -248, -385, -293,
+ -403, -276, -425, -204, -367, -74, -400, -116, -375, -65, -372,
+ -120, -338, -171, -350, -192, -321, -226, -351, -196, -350, -234,
+ -344, -164, -341, -158, -306, -138, -338, -156, -268, -116, -265,
+ -176, -243, -204, -212, -198, -229, -226, -192, -144, -137, -153,
+ -181, -152, -194, -135, -178, -195, -187, -227, -175, -208, -170,
+ -244, -159, -254, -137, -245, -163, -226, -152, -162, -107, -156,
+ -107, -123, -75, -95, -2, -107, -23, -62, -12, -74, -60,
+ -54, -88, -41, -86, -11, -107, -13, -74, -29, -98, -23,
+ -52, -89, -64, -61, -71, -89, -73, 19, -96, 13, -86,
+ 41, -106, 92, -5, 1, -25, -7, -37, -56, 34, -91,
+ -24, -86, 10, -49, -33, -31, -56, 10, -83, 56, -62,
+ 45, -72, 47, -53, 7, -38, 12, -40, -11, -4, -41,
+ -34, -33, -33},
+ /* IRC_Composite_C_R0195_T150_P060.wav */
+ {-16, 16, -17, 17, -18, 18, -19, 20, -20, 21, -21, 22,
+ -22, 23, -23, 23, -23, 22, -20, 15, -1, -129, 61, -83,
+ -118, -17, -51, -30, -188, -95, 109, 510, 730, 1005, 940, 2137,
+ -1039, -410, -2971, 1165, 5406, 9365, 3937, 2137, -649, 991, 4216, 933,
+ -931, -1885, 270, 39, 471, -2, 92, 175, 614, 97, 357, 338,
+ 829, 636, 315, -32, 510, 435, 434, 61, 257, -175, -293, -260,
+ -101, -474, -492, -360, -265, -315, -367, -291, -300, -567, -400, -366,
+ -215, -495, -451, -522, -369, -523, -275, -402, -310, -403, -263, -346,
+ -267, -319, -256, -370, -300, -367, -298, -401, -295, -443, -304, -417,
+ -322, -444, -298, -381, -312, -439, -329, -390, -317, -384, -301, -362,
+ -302, -383, -295, -368, -322, -392, -272, -324, -246, -326, -211, -303,
+ -225, -323, -240, -326, -214, -295, -235, -325, -259, -307, -231, -265,
+ -204, -266, -221, -283, -193, -226, -150, -215, -156, -235, -140, -226,
+ -173, -226, -138, -181, -139, -211, -149, -228, -146, -230, -122, -202,
+ -104, -157, -89, -169, -122, -180, -141, -172, -119, -242, -182, -247,
+ -146, -221, -160, -209, -160, -222, -134, -194, -158, -172, -105, -115,
+ -90, -104, -68, -78, -70, -76, -8, -32, 19, -27, 16, -5,
+ -14, -32, 0, -60, -11, -71, 10, -79, -60, -81, -34, -99,
+ -28, -10, -16, 0, 23, 9, -66, -19, -7, -4, -45, 41,
+ 7, -5, -25, -4, 7, -31, -3, 22, -19, -42, 6, -62,
+ -7, -36, 56, -32, 47, -21, 39, -84, -6, -47, -14, -45,
+ 33, -19, -3, -57},
+ /* IRC_Composite_C_R0195_T180_P060.wav */
+ {10, -10, 10, -11, 11, -10, 10, -10, 10, -10, 10, -10,
+ 9, -9, 8, -7, 6, -5, 3, -1, -2, 6, -12, 19,
+ -22, 153, -20, -54, 110, 44, 10, -80, 172, 36, 605, 639,
+ 998, 719, 1758, -623, -249, -1810, 1268, 4590, 6942, 3540, 1131, 148,
+ 816, 4013, 533, -755, -1438, 271, -13, 605, 142, 55, 202, 291,
+ -131, 465, 665, 844, 217, 120, 213, 663, 92, -57, -100, 67,
+ -191, -270, -528, -381, -572, -177, -319, -6, -278, -198, -211, 58,
+ -313, -463, -592, -248, -465, -368, -496, -344, -514, -270, -361, -242,
+ -341, -263, -349, -193, -335, -278, -416, -272, -416, -335, -439, -313,
+ -451, -348, -453, -349, -443, -392, -472, -363, -430, -386, -408, -312,
+ -392, -320, -379, -310, -370, -331, -403, -335, -385, -348, -366, -269,
+ -311, -276, -302, -234, -290, -316, -341, -275, -241, -217, -208, -227,
+ -228, -238, -222, -184, -224, -208, -241, -186, -232, -205, -245, -203,
+ -214, -163, -151, -128, -135, -145, -180, -187, -204, -171, -188, -145,
+ -158, -112, -157, -75, -116, -69, -149, -129, -160, -136, -115, -133,
+ -152, -184, -177, -167, -165, -197, -218, -169, -194, -138, -205, -133,
+ -162, -112, -132, -87, -117, -57, -79, -38, -55, -39, -50, -29,
+ -58, -18, 2, 13, -3, 31, 17, 9, 8, 29, -11, -10,
+ -3, 19, -49, -35, -14, -39, -21, -64, -32, -53, -24, -15,
+ -14, -23, 27, 17, 51, -9, -15, -18, 18, -37, 5, -74,
+ -13, -18, 63, 0, 46, 7, 63, -15, 19, -10, 7, -30,
+ -32, -44, -79, -107},
+ /* IRC_Composite_C_R0195_T210_P060.wav */
+ {-5, 5, -5, 5, -5, 5, -5, 5, -5, 5, -5, 5,
+ -5, 5, -5, 5, -5, 5, -5, 5, -5, 5, -4, 4,
+ -4, 4, -4, -36, -70, 2, -43, -45, -73, 17, -83, -76,
+ -100, 274, 192, 709, 499, 1049, 474, 192, -1587, -214, 1449, 4357,
+ 5043, 2092, 784, 976, 2790, 1402, 1131, -633, 70, 310, 385, 217,
+ 300, 87, 393, 309, 640, 644, 573, 357, 469, 403, 414, 393,
+ 291, 95, 9, 177, 41, -38, -147, -145, -178, -91, 51, -23,
+ 82, 6, 81, -66, -34, -205, -207, -298, -223, -348, -275, -305,
+ -257, -377, -308, -322, -230, -324, -269, -300, -305, -334, -286, -346,
+ -334, -384, -351, -411, -405, -389, -413, -415, -387, -382, -401, -384,
+ -393, -405, -388, -417, -364, -399, -357, -398, -374, -392, -376, -343,
+ -359, -316, -314, -243, -324, -280, -326, -283, -293, -262, -262, -275,
+ -249, -231, -206, -195, -215, -224, -241, -210, -220, -242, -276, -255,
+ -264, -198, -192, -190, -189, -210, -164, -172, -169, -172, -189, -153,
+ -193, -127, -166, -117, -158, -112, -128, -133, -107, -136, -117, -161,
+ -108, -110, -142, -127, -184, -138, -172, -147, -192, -188, -193, -158,
+ -153, -183, -168, -189, -142, -164, -152, -151, -135, -105, -91, -78,
+ -46, -70, -9, -22, -10, 6, -3, 11, 9, 0, -18, -34,
+ -49, -47, -78, -56, -80, -88, -84, -41, -76, -89, -89, -34,
+ -62, 21, -11, 29, -63, 38, 13, 32, -26, -6, -18, -15,
+ -14, -11, -43, -37, -18, 40, -10, 3, -52, 3, -65, -18,
+ -66, -33, -83, -53},
+ /* IRC_Composite_C_R0195_T240_P060.wav */
+ {-5, 5, -5, 5, -5, 6, -6, 6, -6, 6, -6, 6,
+ -6, 6, -6, 6, -6, 6, -6, 6, -6, 7, -7, 7,
+ -6, 6, -6, 4, 1, -68, -141, -43, -120, -98, -71, -122,
+ -111, -111, -115, -151, -61, 253, 299, 522, 567, 502, 242, -1198,
+ -647, 857, 3187, 4265, 2111, 1152, 1068, 2588, 1288, 1124, 171, 189,
+ 245, 613, 495, 306, 631, 410, 664, 513, 726, 614, 455, 361,
+ 493, 538, 415, 305, 397, 398, 328, 162, 355, 127, 167, 95,
+ 91, 92, 127, 159, 137, 126, 136, 112, 42, -54, -59, -214,
+ -99, -334, -210, -340, -229, -357, -264, -345, -232, -312, -253, -294,
+ -304, -352, -288, -347, -349, -364, -352, -373, -394, -376, -368, -434,
+ -376, -402, -393, -434, -406, -412, -357, -430, -351, -354, -374, -337,
+ -289, -323, -276, -287, -233, -304, -270, -311, -283, -321, -267, -321,
+ -256, -305, -231, -272, -202, -256, -217, -272, -229, -238, -234, -247,
+ -261, -261, -253, -218, -198, -218, -190, -226, -148, -209, -150, -201,
+ -174, -181, -174, -172, -158, -155, -141, -131, -122, -138, -117, -118,
+ -119, -130, -144, -143, -130, -146, -135, -155, -149, -172, -147, -158,
+ -167, -175, -169, -144, -176, -150, -108, -129, -129, -136, -93, -143,
+ -97, -99, -87, -71, -74, -15, -38, -48, -75, -66, -95, -101,
+ -91, -93, -103, -154, -122, -128, -116, -84, -70, -79, -65, -51,
+ -28, -23, -20, -21, -53, -15, -9, 15, -49, 5, -40, -18,
+ -42, -33, -30, -86, -81, -68, -62, -85, -44, -26, -89, -49,
+ -55, -24, -61, -16},
+ /* IRC_Composite_C_R0195_T270_P060.wav */
+ {5, -5, 6, -6, 6, -6, 6, -6, 7, -7, 7, -7,
+ 8, -8, 8, -9, 9, -10, 10, -11, 12, -12, 13, -15,
+ 16, -19, 22, -27, 38, -79, -217, -116, -99, -179, -211, -36,
+ -231, -204, -130, -219, -231, -133, 80, 277, 396, 430, 429, -88,
+ -768, -1080, 327, 3345, 4354, 1751, 849, 1821, 1217, 1828, 885, 517,
+ 269, 311, 1017, 365, 820, 357, 716, 549, 590, 621, 516, 736,
+ 326, 780, 263, 770, 281, 705, 378, 636, 371, 475, 311, 429,
+ 255, 409, 238, 273, 140, 314, 213, 208, 138, 105, 115, -23,
+ 74, -205, -25, -248, -155, -287, -280, -252, -297, -307, -288, -293,
+ -267, -333, -245, -352, -304, -348, -348, -351, -433, -337, -451, -334,
+ -472, -324, -423, -375, -381, -372, -314, -375, -276, -267, -321, -301,
+ -279, -273, -291, -287, -302, -304, -304, -290, -236, -296, -289, -263,
+ -294, -284, -324, -265, -281, -292, -245, -245, -266, -260, -214, -224,
+ -254, -200, -260, -209, -252, -220, -200, -254, -196, -233, -158, -228,
+ -136, -185, -148, -205, -143, -175, -148, -149, -166, -120, -148, -104,
+ -128, -140, -125, -152, -101, -130, -77, -199, -88, -178, -108, -197,
+ -126, -198, -117, -194, -113, -172, -133, -128, -87, -131, -57, -112,
+ -60, -136, -71, -150, -125, -162, -106, -205, -139, -165, -138, -149,
+ -118, -134, -84, -134, -83, -120, -64, -109, -79, -116, -53, -90,
+ -64, -49, -97, -69, -54, -86, -38, -63, -120, -64, -43, -89,
+ -78, -45, -32, -110, -9, -49, -30, -91, -36, -87, -34, -90,
+ -12, -62, -20, -67},
+ /* IRC_Composite_C_R0195_T300_P060.wav */
+ {5, -5, 5, -5, 6, -6, 6, -6, 6, -6, 6, -6,
+ 6, -7, 7, -7, 7, -7, 7, -8, 8, -8, 8, -9,
+ 9, -9, 10, -11, 16, -103, -316, -221, -172, -255, -229, -364,
+ -48, -533, -236, -67, -300, 259, 642, 185, 100, 427, -1196, -1745,
+ 476, 2467, 5309, 2589, 680, 1682, 925, 1569, 1041, 949, -237, 234,
+ 798, 421, 371, 569, 750, 604, 969, 544, 826, 529, 786, 297,
+ 628, 582, 490, 514, 782, 585, 536, 518, 533, 446, 484, 585,
+ 264, 478, 477, 367, 328, 417, 65, 219, 65, 246, -91, 156,
+ -64, 72, -138, 50, -297, -48, -254, -210, -356, -206, -256, -287,
+ -317, -214, -367, -254, -373, -339, -401, -317, -430, -376, -368, -350,
+ -418, -316, -298, -353, -273, -305, -281, -287, -227, -371, -293, -252,
+ -310, -323, -260, -272, -324, -243, -257, -310, -290, -276, -297, -255,
+ -201, -324, -280, -270, -230, -282, -221, -248, -298, -222, -268, -241,
+ -260, -255, -277, -235, -187, -236, -210, -172, -224, -173, -145, -191,
+ -157, -155, -157, -126, -186, -84, -150, -177, -153, -72, -128, -133,
+ -127, -100, -229, -197, -160, -241, -236, -171, -176, -120, -75, -47,
+ -120, -105, -128, -76, -160, -120, -127, -109, -139, -135, -144, -192,
+ -179, -152, -203, -173, -143, -161, -61, -179, -143, -104, -126, -147,
+ -78, -176, -138, -177, -87, -160, -110, -137, -186, -155, -86, -135,
+ -127, -105, -97, -124, -34, -111, -50, -117, -72, -123, -75, -65,
+ -94, -51, -83, -75, -56, -47, -45, -13, -39, -55, 20, -56,
+ 36, -41, 12, -24},
+ /* IRC_Composite_C_R0195_T330_P060.wav */
+ {3, -3, 4, -4, 4, -4, 5, -5, 5, -6, 6, -6,
+ 7, -7, 8, -9, 10, -11, 13, -15, 18, -22, 28, -41,
+ 71, -233, -373, -96, -345, -318, -371, -265, -252, -594, -439, -312,
+ -197, 197, 323, 301, 1318, -757, -1616, -868, -2081, 2422, 6337, 5378,
+ 1528, 381, 1195, 1007, 1545, 810, 158, -165, 224, 315, 652, 555,
+ 370, 728, 945, 600, 540, 730, 606, 560, 567, 567, 493, 662,
+ 473, 574, 547, 639, 468, 638, 527, 625, 475, 584, 476, 643,
+ 403, 465, 284, 372, 243, 235, 43, 108, 49, 102, 20, -28,
+ -86, -48, -49, -120, -149, -133, -123, -173, -198, -229, -246, -291,
+ -242, -300, -328, -373, -296, -357, -299, -377, -351, -356, -276, -311,
+ -324, -289, -282, -268, -271, -255, -256, -273, -249, -285, -230, -270,
+ -219, -299, -282, -269, -275, -287, -307, -267, -290, -268, -297, -284,
+ -263, -248, -257, -244, -243, -257, -290, -273, -259, -265, -237, -261,
+ -238, -273, -209, -234, -171, -171, -157, -118, -120, -137, -80, -115,
+ -96, -166, -45, -140, -79, -126, -114, -259, -243, -265, -228, -274,
+ -267, -264, -220, -203, -117, -137, -118, -112, -33, -82, -41, -93,
+ -102, -121, -120, -145, -175, -177, -147, -143, -65, -127, -138, -136,
+ -94, -127, -130, -217, -183, -159, -168, -202, -184, -203, -240, -202,
+ -146, -195, -160, -147, -169, -143, -72, -128, -191, -139, -128, -170,
+ -166, -129, -160, -152, -97, -112, -123, -68, -72, -87, -76, -27,
+ -63, -48, -50, -16, -43, 16, -15, -10, -1, 57, 47, 30,
+ 49, 50, 35, 14}};
+
+const int16_t irc_composite_c_r0195_p075[][256] =
+ {/* IRC_Composite_C_R0195_T000_P075.wav */
+ {12, -12, 13, -13, 13, -13, 13, -13, 14, -14, 14, -14,
+ 14, -14, 15, -15, 14, -14, 13, -12, 7, 8, -224, -367,
+ -189, -595, -344, -165, -801, -581, -302, -836, -618, 407, 530, 134,
+ 1455, -966, -265, -4329, -4113, 4525, 6992, 8542, 304, -199, 2762, 1323,
+ 1544, 1359, 195, -1272, 815, 548, 454, 489, 1188, 303, 1217, 433,
+ 606, 486, 940, 200, 865, 394, 415, 452, 439, 167, 687, 262,
+ 388, 668, 618, 250, 776, 505, 518, 525, 594, 287, 552, 461,
+ 358, 276, 304, 160, 239, 30, 35, -21, -62, -125, -17, -31,
+ -371, 39, -211, -122, -197, 40, -391, 72, -166, -93, -262, -84,
+ -337, -76, -325, -174, -301, -196, -378, -120, -367, -324, -293, -268,
+ -402, -208, -271, -377, -322, -212, -314, -262, -200, -245, -294, -186,
+ -222, -275, -226, -280, -246, -300, -262, -182, -254, -272, -206, -217,
+ -307, -182, -214, -248, -250, -184, -258, -303, -196, -229, -312, -240,
+ -213, -256, -268, -167, -202, -256, -145, -174, -141, -306, -39, -235,
+ -192, -132, -161, -142, -205, -78, -192, -93, -268, -65, -239, -166,
+ -229, -88, -215, -227, -134, -262, -99, -230, -94, -297, -147, -247,
+ -15, -245, -197, -51, -171, -94, -46, -60, -97, -14, -100, 14,
+ -99, -80, -92, -24, -178, 55, -112, -23, -104, 47, -121, -73,
+ -80, -27, -196, -98, -74, -188, -162, -54, -110, -182, -133, -30,
+ -121, -83, -72, -116, -145, -93, -89, -136, -159, -163, -75, -145,
+ -145, -85, -61, -103, -107, 5, -74, -43, -3, -22, -72, -26,
+ -24, 6, -85, 5},
+ /* IRC_Composite_C_R0195_T060_P075.wav */
+ {-14, 15, -15, 16, -17, 18, -19, 20, -21, 22, -23,
+ 24, -25, 27, -28, 30, -32, 35, -42, 65, -281, -389,
+ 123, -583, -268, -119, -384, -436, -660, 282, -1424, 931, -191,
+ 1870, -113, 1526, 243, -2974, -2922, -3038, 6766, 14120, 3000, -177,
+ 427, 2482, 2472, 607, 1128, -2092, 683, -982, 2038, -839, 1456,
+ -30, 1050, 188, 720, 669, 391, 864, 257, 483, 214, 493,
+ 238, 321, 378, 114, 592, 45, 886, -9, 646, 103, 701,
+ 13, 345, 57, 116, 121, -88, 171, -386, 97, -335, -262,
+ -369, -373, -308, -409, 33, -533, 5, -484, 24, -435, -237,
+ -319, -208, -233, -416, -146, -317, -166, -309, -232, -304, -340,
+ -158, -439, -169, -482, -113, -521, -200, -450, -258, -409, -296,
+ -298, -425, -296, -402, -280, -434, -185, -317, -227, -279, -194,
+ -241, -311, -232, -320, -314, -306, -233, -329, -276, -236, -245,
+ -297, -256, -187, -267, -262, -210, -246, -287, -248, -205, -331,
+ -278, -229, -232, -260, -191, -174, -217, -208, -141, -202, -243,
+ -151, -110, -284, -132, -132, -134, -277, -115, -207, -229, -254,
+ -153, -213, -329, -111, -236, -248, -245, -82, -281, -102, -144,
+ -93, -133, -52, -82, -139, -23, -121, -40, -135, 7, -127,
+ -34, -127, -9, -67, -119, -37, -34, -134, 4, -70, -77,
+ -81, -62, -64, -127, -18, -109, 11, -156, 60, -224, 27,
+ -77, -130, 6, -101, -38, -151, 93, -175, -123, 0, -32,
+ -129, -111, 115, -229, -48, 12, -46, -196, -37, -14, -163,
+ -83, -1, -137, -48, -101, 91, -141, 15, -60, 28, -35,
+ -33, -10, -28},
+ /* IRC_Composite_C_R0195_T120_P075.wav */
+ {-13, 13, -14, 14, -15, 15, -16, 16, -17, 17, -18,
+ 18, -19, 19, -20, 20, -19, 19, -16, 12, -1, -57,
+ -48, -189, -132, -75, -227, -56, -343, 28, -501, 199, 201,
+ 917, 964, 1130, 545, 1226, -3782, -981, -1166, 8252, 10379, 3341,
+ -521, -669, 4572, 428, 2096, -1386, -405, -1098, 1383, -2, 236,
+ 525, 510, 124, 361, 340, 604, 403, 459, 20, 703, 550,
+ 361, 4, 507, 76, 379, 200, 87, 66, -36, 226, 133,
+ 30, -418, -183, -144, -119, -262, -333, -294, -550, -263, -353,
+ -297, -518, -525, -481, -405, -172, -382, -282, -383, -195, -329,
+ -205, -358, -340, -304, -296, -417, -305, -321, -319, -400, -254,
+ -454, -283, -418, -294, -482, -262, -427, -329, -482, -271, -419,
+ -319, -403, -345, -351, -373, -409, -381, -310, -297, -225, -271,
+ -290, -302, -223, -252, -250, -249, -220, -293, -267, -259, -270,
+ -267, -281, -238, -321, -208, -300, -203, -357, -197, -295, -169,
+ -277, -177, -273, -210, -234, -196, -252, -188, -188, -144, -221,
+ -121, -160, -150, -180, -111, -124, -171, -110, -188, -133, -213,
+ -124, -220, -168, -203, -176, -182, -229, -156, -255, -117, -221,
+ -99, -240, -61, -158, -122, -129, -39, -92, -133, 4, -74,
+ -53, -77, 35, -129, 8, -50, 5, -61, -24, -33, -46,
+ 31, -56, 58, -35, 39, -37, 57, -74, 2, -108, -10,
+ -136, 18, -141, 8, -114, 4, -132, -22, -37, -31, -20,
+ -71, 79, -149, 14, -32, 50, -85, -47, 94, -59, 21,
+ -39, 82, -145, 47, -47, -14, -82, -16, -3, -58, -48,
+ -44, -22, -40},
+ /* IRC_Composite_C_R0195_T180_P075.wav */
+ {-17, 17, -17, 17, -16, 16, -16, 16, -15, 15, -15, 14,
+ -14, 14, -13, 12, -12, 11, -10, 9, -7, 5, -2, -2,
+ 11, 164, -118, 156, -74, 171, -166, 281, -259, 331, -163, 647,
+ 483, 1342, 96, 2125, -652, 369, -2506, 1012, 3788, 6928, 5436, -854,
+ 927, 1011, 2973, 419, 642, -1377, -224, 371, 309, -128, 78, 268,
+ 228, 233, 434, 427, 397, -91, 692, 541, 379, -60, 222, -23,
+ 131, 178, 60, -172, -56, -82, -145, -311, -297, -431, -248, -412,
+ -107, -371, -72, -353, -277, -448, -332, -408, -362, -461, -379, -530,
+ -388, -407, -290, -364, -334, -409, -288, -419, -285, -384, -338, -447,
+ -343, -386, -344, -453, -378, -390, -377, -502, -386, -429, -361, -457,
+ -343, -463, -441, -462, -348, -398, -369, -416, -375, -435, -312, -336,
+ -244, -309, -307, -321, -263, -272, -272, -262, -236, -196, -217, -232,
+ -215, -261, -261, -304, -256, -296, -220, -241, -218, -248, -199, -190,
+ -206, -213, -223, -195, -209, -182, -150, -161, -155, -165, -145, -155,
+ -129, -127, -102, -102, -126, -107, -145, -133, -143, -141, -131, -140,
+ -117, -119, -86, -121, -100, -131, -130, -130, -139, -142, -162, -155,
+ -148, -128, -171, -136, -140, -117, -101, -76, -112, -44, -78, -20,
+ -37, -38, -18, 15, -6, 2, 39, 44, 55, -20, 46, 41,
+ 30, 8, 37, 20, -14, 0, -10, 0, -57, -38, -55, -62,
+ -113, -27, -88, -33, -69, -21, -80, 1, -86, 62, -11, 16,
+ 23, 128, 6, 102, -12, 152, 3, 46, 29, 36, -97, 2,
+ -15, 22, -109, -54},
+ /* IRC_Composite_C_R0195_T240_P075.wav */
+ {3, -3, 3, -3, 3, -3, 3, -2, 2, -2, 2, -2,
+ 1, -1, 1, 0, 0, 1, -2, 3, -4, 5, -7, 10,
+ -13, 20, -42, -111, -129, -71, -115, -137, -61, -159, -123, -192,
+ -19, -219, 406, 390, 680, 549, 886, -353, -736, -1471, 543, 4694,
+ 6064, 1476, 785, 1081, 2341, 1848, 567, 793, -1054, 878, -19, 694,
+ -27, 624, 453, 574, 713, 561, 524, 289, 447, 341, 517, 436,
+ 227, 504, 311, 411, 259, 396, 204, 319, 114, 96, 38, -78,
+ 39, -23, 9, 123, -104, 210, -61, 44, -245, -60, -225, -163,
+ -284, -292, -366, -364, -318, -284, -326, -284, -257, -269, -279, -348,
+ -246, -376, -367, -329, -388, -355, -428, -316, -418, -375, -426, -318,
+ -396, -436, -393, -407, -383, -405, -383, -442, -422, -393, -344, -375,
+ -309, -340, -270, -311, -270, -269, -256, -268, -233, -283, -233, -230,
+ -236, -283, -256, -265, -301, -274, -317, -261, -298, -265, -239, -262,
+ -221, -248, -188, -287, -192, -249, -152, -246, -140, -231, -148, -218,
+ -130, -170, -165, -154, -171, -127, -181, -99, -138, -109, -120, -99,
+ -88, -129, -61, -150, -63, -199, -44, -206, -106, -201, -113, -189,
+ -182, -146, -180, -174, -177, -146, -150, -167, -103, -131, -76, -138,
+ -14, -119, -32, -66, -17, -34, -48, 8, -80, -30, -22, -34,
+ -12, -52, 1, -29, -7, 12, -34, -30, -36, -43, -95, -29,
+ -80, -102, -17, -119, -24, -119, 10, -78, -16, -105, -38, -66,
+ -45, -15, 9, -36, -32, 33, -12, -9, -66, -12, -59, 8,
+ -23, -10, -6, -22},
+ /* IRC_Composite_C_R0195_T300_P075.wav */
+ {5, -5, 5, -6, 6, -6, 6, -7, 7, -7, 8, -8,
+ 9, -9, 10, -10, 11, -12, 13, -14, 16, -19, 23, -30,
+ 49, -174, -216, -220, -266, -146, -276, -310, -167, -398, -257, -446,
+ -119, 2, 150, 904, 282, 399, -249, -994, -2605, 51, 5569, 5353,
+ 3304, -294, 1237, 2394, 748, 1912, 84, -494, 511, 465, 372, 641,
+ 761, 120, 1277, 373, 764, 283, 1087, -27, 731, 558, 213, 625,
+ 490, 578, 331, 755, 261, 576, 308, 533, 270, 531, 241, 337,
+ 345, 292, 249, 254, 130, 252, 95, 124, 0, -94, 22, -124,
+ -166, -120, -222, -195, -188, -102, -288, -138, -247, -177, -285, -135,
+ -368, -230, -298, -370, -301, -355, -365, -394, -255, -460, -300, -338,
+ -376, -356, -289, -388, -412, -303, -449, -224, -403, -214, -327, -319,
+ -233, -280, -268, -283, -281, -308, -227, -300, -251, -210, -330, -195,
+ -309, -196, -294, -197, -342, -211, -310, -249, -268, -272, -295, -243,
+ -275, -237, -249, -206, -274, -172, -248, -191, -211, -154, -258, -145,
+ -194, -183, -134, -143, -154, -131, -111, -163, -86, -129, -136, -111,
+ -159, -135, -153, -128, -194, -126, -204, -111, -239, -106, -205, -215,
+ -163, -175, -185, -155, -144, -130, -163, -92, -105, -142, -4, -111,
+ -109, -19, -118, 13, -189, 9, -76, -118, -8, -6, -126, -54,
+ 46, -138, -64, 43, -92, -25, -132, 48, -164, -57, -43, -117,
+ -199, -11, -164, -58, -105, -75, -100, -150, -59, -62, -137, -80,
+ -148, -65, -126, -49, -62, -33, -77, -10, -18, -11, -21, -15,
+ -36, -38, -38, 9}};
+
+const int16_t irc_composite_c_r0195_p090[][256] =
+ {/* IRC_Composite_C_R0195_T000_P090.wav */
+ {-1, 1, -1, 1, -1, 1, -1, 2, -2, 2, -3, 3,
+ -3, 4, -5, 5, -7, 8, -10, 14, -25, -385, -133, -356,
+ -344, -390, -312, -438, -400, -852, -107, -995, -245, -186, 1358, -771,
+ 1473, 28, -1710, -3762, -3025, 4119, 8200, 5927, -785, 1558, 2250, 1837,
+ 2098, 631, -318, 150, -77, 1215, -23, 792, 865, 600, 981, 659,
+ 366, 950, 511, 766, 502, 516, 589, 299, 331, 571, 105, 609,
+ 462, 485, 538, 594, 470, 640, 393, 432, 325, 604, 259, 355,
+ 336, 454, 33, 263, 182, -24, -124, 121, -231, -50, -93, 49,
+ -230, -101, 25, -196, -123, -21, -160, -161, -132, -136, -155, -268,
+ -121, -146, -311, -112, -328, -201, -220, -280, -214, -304, -233, -227,
+ -250, -242, -334, -256, -283, -393, -267, -285, -332, -331, -224, -295,
+ -279, -171, -151, -361, -199, -239, -227, -317, -168, -211, -370, -182,
+ -150, -343, -204, -192, -224, -319, -203, -137, -357, -146, -223, -237,
+ -276, -162, -201, -262, -186, -209, -147, -267, -46, -252, -162, -146,
+ -182, -199, -143, -218, -204, -108, -250, -163, -90, -241, -137, -125,
+ -122, -272, -111, -188, -211, -160, -160, -164, -201, -231, -114, -175,
+ -180, -212, -172, -181, -129, -130, -77, -150, -114, -39, 12, -122,
+ -9, -11, -46, -21, -4, 7, -73, -20, -50, -21, -80, -102,
+ 2, -103, -66, -67, -8, -41, -119, -26, 29, -208, 3, -108,
+ -34, -228, 114, -196, -84, -28, 1, -109, -92, 77, -169, -27,
+ 48, -12, -79, 3, -100, -44, -53, 13, -103, -90, -14, -91,
+ -23, -64, -21, -116}};
+
+struct Elevation {
+ /**
+ * An array of |count| impulse responses of 256 samples for the left ear.
+ * The impulse responses in each elevation are at equally spaced azimuths
+ * for a full 360 degree revolution, ordered clockwise from in front the
+ * listener.
+ */
+ const int16_t (*azimuths)[256];
+ int count;
+};
+
+/**
+ * irc_composite_c_r0195 is an array with each element containing data for one
+ * elevation.
+ */
+const Elevation irc_composite_c_r0195[] = {
+ {irc_composite_c_r0195_p315, MOZ_ARRAY_LENGTH(irc_composite_c_r0195_p315)},
+ {irc_composite_c_r0195_p330, MOZ_ARRAY_LENGTH(irc_composite_c_r0195_p330)},
+ {irc_composite_c_r0195_p345, MOZ_ARRAY_LENGTH(irc_composite_c_r0195_p345)},
+ {irc_composite_c_r0195_p000, MOZ_ARRAY_LENGTH(irc_composite_c_r0195_p000)},
+ {irc_composite_c_r0195_p015, MOZ_ARRAY_LENGTH(irc_composite_c_r0195_p015)},
+ {irc_composite_c_r0195_p030, MOZ_ARRAY_LENGTH(irc_composite_c_r0195_p030)},
+ {irc_composite_c_r0195_p045, MOZ_ARRAY_LENGTH(irc_composite_c_r0195_p045)},
+ {irc_composite_c_r0195_p060, MOZ_ARRAY_LENGTH(irc_composite_c_r0195_p060)},
+ {irc_composite_c_r0195_p075, MOZ_ARRAY_LENGTH(irc_composite_c_r0195_p075)},
+ {irc_composite_c_r0195_p090, MOZ_ARRAY_LENGTH(irc_composite_c_r0195_p090)}};
+
+const int irc_composite_c_r0195_first_elevation = -45; /* degrees */
+const int irc_composite_c_r0195_elevation_interval = 15; /* degrees */
+const int irc_composite_c_r0195_sample_rate = 44100; /* Hz */
diff --git a/dom/media/webaudio/blink/PeriodicWave.cpp b/dom/media/webaudio/blink/PeriodicWave.cpp
new file mode 100644
index 0000000000..52113a2d07
--- /dev/null
+++ b/dom/media/webaudio/blink/PeriodicWave.cpp
@@ -0,0 +1,353 @@
+/*
+ * Copyright (C) 2012 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "PeriodicWave.h"
+#include <algorithm>
+#include <cmath>
+#include <limits>
+#include "mozilla/FFTBlock.h"
+
+const unsigned MinPeriodicWaveSize = 4096; // This must be a power of two.
+const unsigned MaxPeriodicWaveSize = 8192; // This must be a power of two.
+const float CentsPerRange = 1200 / 3; // 1/3 Octave.
+
+using namespace mozilla;
+using mozilla::dom::OscillatorType;
+
+namespace WebCore {
+
+already_AddRefed<PeriodicWave> PeriodicWave::create(float sampleRate,
+ const float* real,
+ const float* imag,
+ size_t numberOfComponents,
+ bool disableNormalization) {
+ bool isGood = real && imag && numberOfComponents > 0;
+ MOZ_ASSERT(isGood);
+ if (isGood) {
+ RefPtr<PeriodicWave> periodicWave =
+ new PeriodicWave(sampleRate, numberOfComponents, disableNormalization);
+
+ // Limit the number of components used to those for frequencies below the
+ // Nyquist of the fixed length inverse FFT.
+ size_t halfSize = periodicWave->m_periodicWaveSize / 2;
+ numberOfComponents = std::min(numberOfComponents, halfSize);
+ periodicWave->m_numberOfComponents = numberOfComponents;
+ periodicWave->m_realComponents =
+ MakeUnique<AudioFloatArray>(numberOfComponents);
+ periodicWave->m_imagComponents =
+ MakeUnique<AudioFloatArray>(numberOfComponents);
+ memcpy(periodicWave->m_realComponents->Elements(), real,
+ numberOfComponents * sizeof(float));
+ memcpy(periodicWave->m_imagComponents->Elements(), imag,
+ numberOfComponents * sizeof(float));
+
+ return periodicWave.forget();
+ }
+ return nullptr;
+}
+
+already_AddRefed<PeriodicWave> PeriodicWave::createSine(float sampleRate) {
+ RefPtr<PeriodicWave> periodicWave =
+ new PeriodicWave(sampleRate, MinPeriodicWaveSize, false);
+ periodicWave->generateBasicWaveform(OscillatorType::Sine);
+ return periodicWave.forget();
+}
+
+already_AddRefed<PeriodicWave> PeriodicWave::createSquare(float sampleRate) {
+ RefPtr<PeriodicWave> periodicWave =
+ new PeriodicWave(sampleRate, MinPeriodicWaveSize, false);
+ periodicWave->generateBasicWaveform(OscillatorType::Square);
+ return periodicWave.forget();
+}
+
+already_AddRefed<PeriodicWave> PeriodicWave::createSawtooth(float sampleRate) {
+ RefPtr<PeriodicWave> periodicWave =
+ new PeriodicWave(sampleRate, MinPeriodicWaveSize, false);
+ periodicWave->generateBasicWaveform(OscillatorType::Sawtooth);
+ return periodicWave.forget();
+}
+
+already_AddRefed<PeriodicWave> PeriodicWave::createTriangle(float sampleRate) {
+ RefPtr<PeriodicWave> periodicWave =
+ new PeriodicWave(sampleRate, MinPeriodicWaveSize, false);
+ periodicWave->generateBasicWaveform(OscillatorType::Triangle);
+ return periodicWave.forget();
+}
+
+PeriodicWave::PeriodicWave(float sampleRate, size_t numberOfComponents,
+ bool disableNormalization)
+ : m_sampleRate(sampleRate),
+ m_centsPerRange(CentsPerRange),
+ m_maxPartialsInBandLimitedTable(0),
+ m_normalizationScale(1.0f),
+ m_disableNormalization(disableNormalization) {
+ float nyquist = 0.5 * m_sampleRate;
+
+ if (numberOfComponents <= MinPeriodicWaveSize) {
+ m_periodicWaveSize = MinPeriodicWaveSize;
+ } else {
+ unsigned npow2 =
+ exp2f(floorf(logf(numberOfComponents - 1.0) / logf(2.0f) + 1.0f));
+ m_periodicWaveSize = std::min(MaxPeriodicWaveSize, npow2);
+ }
+
+ m_numberOfRanges = (unsigned)(3.0f * logf(m_periodicWaveSize) / logf(2.0f));
+ m_bandLimitedTables.SetLength(m_numberOfRanges);
+ m_lowestFundamentalFrequency = nyquist / maxNumberOfPartials();
+ m_rateScale = m_periodicWaveSize / m_sampleRate;
+}
+
+size_t PeriodicWave::sizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t amount = aMallocSizeOf(this);
+
+ amount += m_bandLimitedTables.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (size_t i = 0; i < m_bandLimitedTables.Length(); i++) {
+ if (m_bandLimitedTables[i]) {
+ amount +=
+ m_bandLimitedTables[i]->ShallowSizeOfIncludingThis(aMallocSizeOf);
+ }
+ }
+
+ return amount;
+}
+
+void PeriodicWave::waveDataForFundamentalFrequency(
+ float fundamentalFrequency, float*& lowerWaveData, float*& higherWaveData,
+ float& tableInterpolationFactor) {
+ // Negative frequencies are allowed, in which case we alias
+ // to the positive frequency.
+ fundamentalFrequency = fabsf(fundamentalFrequency);
+
+ // We only need to rebuild to the tables if the new fundamental
+ // frequency is low enough to allow for more partials below the
+ // Nyquist frequency.
+ unsigned numberOfPartials = numberOfPartialsForRange(0);
+ float nyquist = 0.5 * m_sampleRate;
+ if (fundamentalFrequency != 0.0) {
+ numberOfPartials =
+ std::min(numberOfPartials, (unsigned)(nyquist / fundamentalFrequency));
+ }
+ if (numberOfPartials > m_maxPartialsInBandLimitedTable) {
+ for (unsigned rangeIndex = 0; rangeIndex < m_numberOfRanges; ++rangeIndex) {
+ m_bandLimitedTables[rangeIndex] = 0;
+ }
+
+ // We need to create the first table to determine the normalization
+ // constant.
+ createBandLimitedTables(fundamentalFrequency, 0);
+ m_maxPartialsInBandLimitedTable = numberOfPartials;
+ }
+
+ // Calculate the pitch range.
+ float ratio = fundamentalFrequency > 0
+ ? fundamentalFrequency / m_lowestFundamentalFrequency
+ : 0.5;
+ float centsAboveLowestFrequency = logf(ratio) / logf(2.0f) * 1200;
+
+ // Add one to round-up to the next range just in time to truncate
+ // partials before aliasing occurs.
+ float pitchRange = 1 + centsAboveLowestFrequency / m_centsPerRange;
+
+ pitchRange = std::max(pitchRange, 0.0f);
+ pitchRange = std::min(pitchRange, static_cast<float>(m_numberOfRanges - 1));
+
+ // The words "lower" and "higher" refer to the table data having
+ // the lower and higher numbers of partials. It's a little confusing
+ // since the range index gets larger the more partials we cull out.
+ // So the lower table data will have a larger range index.
+ unsigned rangeIndex1 = static_cast<unsigned>(pitchRange);
+ unsigned rangeIndex2 =
+ rangeIndex1 < m_numberOfRanges - 1 ? rangeIndex1 + 1 : rangeIndex1;
+
+ if (!m_bandLimitedTables[rangeIndex1].get())
+ createBandLimitedTables(fundamentalFrequency, rangeIndex1);
+
+ if (!m_bandLimitedTables[rangeIndex2].get())
+ createBandLimitedTables(fundamentalFrequency, rangeIndex2);
+
+ lowerWaveData = m_bandLimitedTables[rangeIndex2]->Elements();
+ higherWaveData = m_bandLimitedTables[rangeIndex1]->Elements();
+
+ // Ranges from 0 -> 1 to interpolate between lower -> higher.
+ tableInterpolationFactor = rangeIndex2 - pitchRange;
+}
+
+unsigned PeriodicWave::maxNumberOfPartials() const {
+ return m_periodicWaveSize / 2;
+}
+
+unsigned PeriodicWave::numberOfPartialsForRange(unsigned rangeIndex) const {
+ // Number of cents below nyquist where we cull partials.
+ float centsToCull = rangeIndex * m_centsPerRange;
+
+ // A value from 0 -> 1 representing what fraction of the partials to keep.
+ float cullingScale = exp2(-centsToCull / 1200);
+
+ // The very top range will have all the partials culled.
+ unsigned numberOfPartials = cullingScale * maxNumberOfPartials();
+
+ return numberOfPartials;
+}
+
+// Convert into time-domain wave buffers.
+// One table is created for each range for non-aliasing playback
+// at different playback rates. Thus, higher ranges have more
+// high-frequency partials culled out.
+void PeriodicWave::createBandLimitedTables(float fundamentalFrequency,
+ unsigned rangeIndex) {
+ unsigned fftSize = m_periodicWaveSize;
+ unsigned i;
+
+ const float* realData = m_realComponents->Elements();
+ const float* imagData = m_imagComponents->Elements();
+
+ // This FFTBlock is used to cull partials (represented by frequency bins).
+ FFTBlock frame(fftSize);
+
+ // Find the starting bin where we should start culling the aliasing
+ // partials for this pitch range. We need to clear out the highest
+ // frequencies to band-limit the waveform.
+ unsigned numberOfPartials = numberOfPartialsForRange(rangeIndex);
+ // Also limit to the number of components that are provided.
+ numberOfPartials = std::min(numberOfPartials, m_numberOfComponents - 1);
+
+ // Limit number of partials to those below Nyquist frequency
+ float nyquist = 0.5 * m_sampleRate;
+ if (fundamentalFrequency != 0.0) {
+ numberOfPartials =
+ std::min(numberOfPartials, (unsigned)(nyquist / fundamentalFrequency));
+ }
+
+ // Copy from loaded frequency data and generate complex conjugate
+ // because of the way the inverse FFT is defined.
+ // The coefficients of higher partials remain zero, as initialized in
+ // the FFTBlock constructor.
+ for (i = 0; i < numberOfPartials + 1; ++i) {
+ frame.RealData(i) = realData[i];
+ frame.ImagData(i) = -imagData[i];
+ }
+
+ // Clear any DC-offset.
+ frame.RealData(0) = 0;
+ // Clear value which has no effect.
+ frame.ImagData(0) = 0;
+
+ // Create the band-limited table.
+ m_bandLimitedTables[rangeIndex] =
+ MakeUnique<AlignedAudioFloatArray>(m_periodicWaveSize);
+
+ // Apply an inverse FFT to generate the time-domain table data.
+ float* data = m_bandLimitedTables[rangeIndex]->Elements();
+ frame.GetInverseWithoutScaling(data);
+
+ // For the first range (which has the highest power), calculate
+ // its peak value then compute normalization scale.
+ if (m_disableNormalization) {
+ // See Bug 1424906, results need to be scaled by 0.5 even
+ // when normalization is disabled.
+ m_normalizationScale = 0.5;
+ } else if (!rangeIndex) {
+ float maxValue;
+ maxValue = AudioBufferPeakValue(data, m_periodicWaveSize);
+
+ if (maxValue) m_normalizationScale = 1.0f / maxValue;
+ }
+
+ // Apply normalization scale.
+ AudioBufferInPlaceScale(data, m_normalizationScale, m_periodicWaveSize);
+}
+
+void PeriodicWave::generateBasicWaveform(OscillatorType shape) {
+ const float piFloat = float(M_PI);
+ unsigned fftSize = periodicWaveSize();
+ unsigned halfSize = fftSize / 2;
+
+ m_numberOfComponents = halfSize;
+ m_realComponents = MakeUnique<AudioFloatArray>(halfSize);
+ m_imagComponents = MakeUnique<AudioFloatArray>(halfSize);
+ float* realP = m_realComponents->Elements();
+ float* imagP = m_imagComponents->Elements();
+
+ // Clear DC and imag value which is ignored.
+ realP[0] = 0;
+ imagP[0] = 0;
+
+ for (unsigned n = 1; n < halfSize; ++n) {
+ float omega = 2 * piFloat * n;
+ float invOmega = 1 / omega;
+
+ // Fourier coefficients according to standard definition.
+ float a; // Coefficient for cos().
+ float b; // Coefficient for sin().
+
+ // Calculate Fourier coefficients depending on the shape.
+ // Note that the overall scaling (magnitude) of the waveforms
+ // is normalized in createBandLimitedTables().
+ switch (shape) {
+ case OscillatorType::Sine:
+ // Standard sine wave function.
+ a = 0;
+ b = (n == 1) ? 1 : 0;
+ break;
+ case OscillatorType::Square:
+ // Square-shaped waveform with the first half its maximum value
+ // and the second half its minimum value.
+ a = 0;
+ b = invOmega * ((n & 1) ? 2 : 0);
+ break;
+ case OscillatorType::Sawtooth:
+ // Sawtooth-shaped waveform with the first half ramping from
+ // zero to maximum and the second half from minimum to zero.
+ a = 0;
+ b = -invOmega * cos(0.5 * omega);
+ break;
+ case OscillatorType::Triangle:
+ // Triangle-shaped waveform going from its maximum value to
+ // its minimum value then back to the maximum value.
+ a = 0;
+ if (n & 1) {
+ b = 2 * (2 / (n * piFloat) * 2 / (n * piFloat)) *
+ ((((n - 1) >> 1) & 1) ? -1 : 1);
+ } else {
+ b = 0;
+ }
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("invalid oscillator type");
+ a = 0;
+ b = 0;
+ break;
+ }
+
+ realP[n] = a;
+ imagP[n] = b;
+ }
+}
+
+} // namespace WebCore
diff --git a/dom/media/webaudio/blink/PeriodicWave.h b/dom/media/webaudio/blink/PeriodicWave.h
new file mode 100644
index 0000000000..fd4e50287c
--- /dev/null
+++ b/dom/media/webaudio/blink/PeriodicWave.h
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2012 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef PeriodicWave_h
+#define PeriodicWave_h
+
+#include "mozilla/dom/OscillatorNodeBinding.h"
+#include "mozilla/UniquePtr.h"
+#include <nsTArray.h>
+#include "AlignedTArray.h"
+#include "mozilla/MemoryReporting.h"
+
+namespace WebCore {
+
+typedef AlignedTArray<float> AlignedAudioFloatArray;
+typedef nsTArray<float> AudioFloatArray;
+
+using mozilla::UniquePtr;
+
+class PeriodicWave {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WebCore::PeriodicWave);
+
+ static already_AddRefed<PeriodicWave> createSine(float sampleRate);
+ static already_AddRefed<PeriodicWave> createSquare(float sampleRate);
+ static already_AddRefed<PeriodicWave> createSawtooth(float sampleRate);
+ static already_AddRefed<PeriodicWave> createTriangle(float sampleRate);
+
+ // Creates an arbitrary periodic wave given the frequency components
+ // (Fourier coefficients).
+ static already_AddRefed<PeriodicWave> create(float sampleRate,
+ const float* real,
+ const float* imag,
+ size_t numberOfComponents,
+ bool disableNormalization);
+
+ // Returns pointers to the lower and higher wave data for the pitch range
+ // containing the given fundamental frequency. These two tables are in
+ // adjacent "pitch" ranges where the higher table will have the maximum
+ // number of partials which won't alias when played back at this
+ // fundamental frequency. The lower wave is the next range containing fewer
+ // partials than the higher wave. Interpolation between these two tables
+ // can be made according to tableInterpolationFactor. Where values
+ // from 0 -> 1 interpolate between lower -> higher.
+ void waveDataForFundamentalFrequency(float, float*& lowerWaveData,
+ float*& higherWaveData,
+ float& tableInterpolationFactor);
+
+ // Returns the scalar multiplier to the oscillator frequency to calculate
+ // wave buffer phase increment.
+ float rateScale() const { return m_rateScale; }
+
+ unsigned periodicWaveSize() const { return m_periodicWaveSize; }
+ float sampleRate() const { return m_sampleRate; }
+
+ size_t sizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ private:
+ explicit PeriodicWave(float sampleRate, size_t numberOfComponents,
+ bool disableNormalization);
+ ~PeriodicWave() = default;
+
+ void generateBasicWaveform(mozilla::dom::OscillatorType);
+
+ float m_sampleRate;
+ unsigned m_periodicWaveSize;
+ unsigned m_numberOfRanges;
+ float m_centsPerRange;
+ unsigned m_numberOfComponents;
+ UniquePtr<AudioFloatArray> m_realComponents;
+ UniquePtr<AudioFloatArray> m_imagComponents;
+
+ // The lowest frequency (in Hertz) where playback will include all of the
+ // partials. Playing back lower than this frequency will gradually lose
+ // more high-frequency information.
+ // This frequency is quite low (~10Hz @ // 44.1KHz)
+ float m_lowestFundamentalFrequency;
+
+ float m_rateScale;
+
+ unsigned numberOfRanges() const { return m_numberOfRanges; }
+
+ // Maximum possible number of partials (before culling).
+ unsigned maxNumberOfPartials() const;
+
+ unsigned numberOfPartialsForRange(unsigned rangeIndex) const;
+
+ // Creates table for specified index based on fundamental frequency.
+ void createBandLimitedTables(float fundamentalFrequency, unsigned rangeIndex);
+ unsigned m_maxPartialsInBandLimitedTable;
+ float m_normalizationScale;
+ bool m_disableNormalization;
+ nsTArray<UniquePtr<AlignedAudioFloatArray> > m_bandLimitedTables;
+};
+
+} // namespace WebCore
+
+#endif // PeriodicWave_h
diff --git a/dom/media/webaudio/blink/README b/dom/media/webaudio/blink/README
new file mode 100644
index 0000000000..96d209dfc8
--- /dev/null
+++ b/dom/media/webaudio/blink/README
@@ -0,0 +1,24 @@
+This directory contains the code originally borrowed from the Blink Web Audio
+implementation. We are forking the code here because in many cases the burden
+of adopting Blink specific utilities is too large compared to the prospect of
+importing upstream fixes by just copying newer versions of the code in the
+future.
+
+The process of borrowing code from Blink is as follows:
+
+* Try to borrow utility classes only, and avoid borrowing code which depends
+ too much on the Blink specific utilities.
+* First, import the pristine files from the Blink repository before adding
+ them to the build system, noting the SVN revision of Blink from which the
+ original files were copied in the commit message.
+* In a separate commit, add the imported source files to the build system,
+ and apply the necessary changes to make it build successfully.
+* Use the code in a separate commit.
+* Never add headers as exported headers. All headers should be included
+ using the following convention: #include "blink/Header.h".
+* Leave the imported code in the WebCore namespace, and import the needed
+ names into the Mozilla code via `using'.
+* Cherry-pick upsteam fixes manually when needed. In case you fix a problem
+ that is not Mozilla specific locally, try to upstream your changes into
+ Blink.
+* Ping ehsan for any questions.
diff --git a/dom/media/webaudio/blink/Reverb.cpp b/dom/media/webaudio/blink/Reverb.cpp
new file mode 100644
index 0000000000..bd56a5af27
--- /dev/null
+++ b/dom/media/webaudio/blink/Reverb.cpp
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "Reverb.h"
+#include "ReverbConvolverStage.h"
+
+#include <math.h>
+#include "ReverbConvolver.h"
+#include "mozilla/FloatingPoint.h"
+
+using namespace mozilla;
+
+namespace WebCore {
+
+// Empirical gain calibration tested across many impulse responses to ensure
+// perceived volume is same as dry (unprocessed) signal
+const float GainCalibration = 0.00125f;
+const float GainCalibrationSampleRate = 44100;
+
+// A minimum power value to when normalizing a silent (or very quiet) impulse
+// response
+const float MinPower = 0.000125f;
+
+static float calculateNormalizationScale(const nsTArray<const float*>& response,
+ size_t aLength, float sampleRate) {
+ // Normalize by RMS power
+ size_t numberOfChannels = response.Length();
+
+ float power = 0;
+
+ for (size_t i = 0; i < numberOfChannels; ++i) {
+ float channelPower = AudioBufferSumOfSquares(response[i], aLength);
+ power += channelPower;
+ }
+
+ power = sqrt(power / (numberOfChannels * aLength));
+
+ // Protect against accidental overload
+ if (!std::isfinite(power) || std::isnan(power) || power < MinPower)
+ power = MinPower;
+
+ float scale = 1 / power;
+
+ scale *= GainCalibration; // calibrate to make perceived volume same as
+ // unprocessed
+
+ // Scale depends on sample-rate.
+ if (sampleRate) scale *= GainCalibrationSampleRate / sampleRate;
+
+ // True-stereo compensation
+ if (numberOfChannels == 4) scale *= 0.5f;
+
+ return scale;
+}
+
+Reverb::Reverb(const AudioChunk& impulseResponse, size_t maxFFTSize,
+ bool useBackgroundThreads, bool normalize, float sampleRate,
+ bool* aAllocationFailure) {
+ MOZ_ASSERT(aAllocationFailure);
+ size_t impulseResponseBufferLength = impulseResponse.mDuration;
+ float scale = impulseResponse.mVolume;
+
+ CopyableAutoTArray<const float*, 4> irChannels(
+ impulseResponse.ChannelData<float>());
+ AutoTArray<float, 1024> tempBuf;
+
+ if (normalize) {
+ scale = calculateNormalizationScale(irChannels, impulseResponseBufferLength,
+ sampleRate);
+ }
+
+ if (scale != 1.0f) {
+ bool rv = tempBuf.SetLength(
+ irChannels.Length() * impulseResponseBufferLength, mozilla::fallible);
+ *aAllocationFailure = !rv;
+ if (*aAllocationFailure) {
+ return;
+ }
+
+ for (uint32_t i = 0; i < irChannels.Length(); ++i) {
+ float* buf = &tempBuf[i * impulseResponseBufferLength];
+ AudioBufferCopyWithScale(irChannels[i], scale, buf,
+ impulseResponseBufferLength);
+ irChannels[i] = buf;
+ }
+ }
+
+ *aAllocationFailure = !initialize(irChannels, impulseResponseBufferLength,
+ maxFFTSize, useBackgroundThreads);
+}
+
+size_t Reverb::sizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t amount = aMallocSizeOf(this);
+ amount += m_convolvers.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (size_t i = 0; i < m_convolvers.Length(); i++) {
+ if (m_convolvers[i]) {
+ amount += m_convolvers[i]->sizeOfIncludingThis(aMallocSizeOf);
+ }
+ }
+
+ amount += m_tempBuffer.SizeOfExcludingThis(aMallocSizeOf, false);
+ return amount;
+}
+
+bool Reverb::initialize(const nsTArray<const float*>& impulseResponseBuffer,
+ size_t impulseResponseBufferLength, size_t maxFFTSize,
+ bool useBackgroundThreads) {
+ m_impulseResponseLength = impulseResponseBufferLength;
+
+ // The reverb can handle a mono impulse response and still do stereo
+ // processing
+ size_t numResponseChannels = impulseResponseBuffer.Length();
+ MOZ_ASSERT(numResponseChannels > 0);
+ // The number of convolvers required is at least the number of audio
+ // channels. Even if there is initially only one audio channel, another
+ // may be added later, and so a second convolver is created now while the
+ // impulse response is available.
+ size_t numConvolvers = std::max<size_t>(numResponseChannels, 2);
+ m_convolvers.SetCapacity(numConvolvers);
+
+ int convolverRenderPhase = 0;
+ for (size_t i = 0; i < numConvolvers; ++i) {
+ size_t channelIndex = i < numResponseChannels ? i : 0;
+ const float* channel = impulseResponseBuffer[channelIndex];
+ size_t length = impulseResponseBufferLength;
+
+ bool allocationFailure;
+ UniquePtr<ReverbConvolver> convolver(
+ new ReverbConvolver(channel, length, maxFFTSize, convolverRenderPhase,
+ useBackgroundThreads, &allocationFailure));
+ if (allocationFailure) {
+ return false;
+ }
+ m_convolvers.AppendElement(std::move(convolver));
+
+ convolverRenderPhase += WEBAUDIO_BLOCK_SIZE;
+ }
+
+ // For "True" stereo processing we allocate a temporary buffer to avoid
+ // repeatedly allocating it in the process() method. It can be bad to allocate
+ // memory in a real-time thread.
+ if (numResponseChannels == 4) {
+ m_tempBuffer.AllocateChannels(2);
+ WriteZeroesToAudioBlock(&m_tempBuffer, 0, WEBAUDIO_BLOCK_SIZE);
+ }
+ return true;
+}
+
+void Reverb::process(const AudioBlock* sourceBus, AudioBlock* destinationBus) {
+ // Do a fairly comprehensive sanity check.
+ // If these conditions are satisfied, all of the source and destination
+ // pointers will be valid for the various matrixing cases.
+ bool isSafeToProcess =
+ sourceBus && destinationBus && sourceBus->ChannelCount() > 0 &&
+ destinationBus->mChannelData.Length() > 0 &&
+ WEBAUDIO_BLOCK_SIZE <= MaxFrameSize &&
+ WEBAUDIO_BLOCK_SIZE <= size_t(sourceBus->GetDuration()) &&
+ WEBAUDIO_BLOCK_SIZE <= size_t(destinationBus->GetDuration());
+
+ MOZ_ASSERT(isSafeToProcess);
+ if (!isSafeToProcess) return;
+
+ // For now only handle mono or stereo output
+ MOZ_ASSERT(destinationBus->ChannelCount() <= 2);
+
+ float* destinationChannelL =
+ static_cast<float*>(const_cast<void*>(destinationBus->mChannelData[0]));
+ const float* sourceBusL =
+ static_cast<const float*>(sourceBus->mChannelData[0]);
+
+ // Handle input -> output matrixing...
+ size_t numInputChannels = sourceBus->ChannelCount();
+ size_t numOutputChannels = destinationBus->ChannelCount();
+ size_t numReverbChannels = m_convolvers.Length();
+
+ if (numInputChannels == 2 && numReverbChannels == 2 &&
+ numOutputChannels == 2) {
+ // 2 -> 2 -> 2
+ const float* sourceBusR =
+ static_cast<const float*>(sourceBus->mChannelData[1]);
+ float* destinationChannelR =
+ static_cast<float*>(const_cast<void*>(destinationBus->mChannelData[1]));
+ m_convolvers[0]->process(sourceBusL, destinationChannelL);
+ m_convolvers[1]->process(sourceBusR, destinationChannelR);
+ } else if (numInputChannels == 1 && numOutputChannels == 2 &&
+ numReverbChannels == 2) {
+ // 1 -> 2 -> 2
+ for (int i = 0; i < 2; ++i) {
+ float* destinationChannel = static_cast<float*>(
+ const_cast<void*>(destinationBus->mChannelData[i]));
+ m_convolvers[i]->process(sourceBusL, destinationChannel);
+ }
+ } else if (numInputChannels == 1 && numOutputChannels == 1) {
+ // 1 -> 1 -> 1 (Only one of the convolvers is used.)
+ m_convolvers[0]->process(sourceBusL, destinationChannelL);
+ } else if (numInputChannels == 2 && numReverbChannels == 4 &&
+ numOutputChannels == 2) {
+ // 2 -> 4 -> 2 ("True" stereo)
+ const float* sourceBusR =
+ static_cast<const float*>(sourceBus->mChannelData[1]);
+ float* destinationChannelR =
+ static_cast<float*>(const_cast<void*>(destinationBus->mChannelData[1]));
+
+ float* tempChannelL =
+ static_cast<float*>(const_cast<void*>(m_tempBuffer.mChannelData[0]));
+ float* tempChannelR =
+ static_cast<float*>(const_cast<void*>(m_tempBuffer.mChannelData[1]));
+
+ // Process left virtual source
+ m_convolvers[0]->process(sourceBusL, destinationChannelL);
+ m_convolvers[1]->process(sourceBusL, destinationChannelR);
+
+ // Process right virtual source
+ m_convolvers[2]->process(sourceBusR, tempChannelL);
+ m_convolvers[3]->process(sourceBusR, tempChannelR);
+
+ AudioBufferAddWithScale(tempChannelL, 1.0f, destinationChannelL,
+ sourceBus->GetDuration());
+ AudioBufferAddWithScale(tempChannelR, 1.0f, destinationChannelR,
+ sourceBus->GetDuration());
+ } else if (numInputChannels == 1 && numReverbChannels == 4 &&
+ numOutputChannels == 2) {
+ // 1 -> 4 -> 2 (Processing mono with "True" stereo impulse response)
+ // This is an inefficient use of a four-channel impulse response, but we
+ // should handle the case.
+ float* destinationChannelR =
+ static_cast<float*>(const_cast<void*>(destinationBus->mChannelData[1]));
+
+ float* tempChannelL =
+ static_cast<float*>(const_cast<void*>(m_tempBuffer.mChannelData[0]));
+ float* tempChannelR =
+ static_cast<float*>(const_cast<void*>(m_tempBuffer.mChannelData[1]));
+
+ // Process left virtual source
+ m_convolvers[0]->process(sourceBusL, destinationChannelL);
+ m_convolvers[1]->process(sourceBusL, destinationChannelR);
+
+ // Process right virtual source
+ m_convolvers[2]->process(sourceBusL, tempChannelL);
+ m_convolvers[3]->process(sourceBusL, tempChannelR);
+
+ AudioBufferAddWithScale(tempChannelL, 1.0f, destinationChannelL,
+ sourceBus->GetDuration());
+ AudioBufferAddWithScale(tempChannelR, 1.0f, destinationChannelR,
+ sourceBus->GetDuration());
+ } else {
+ MOZ_ASSERT_UNREACHABLE("Unexpected Reverb configuration");
+ destinationBus->SetNull(destinationBus->GetDuration());
+ }
+}
+
+} // namespace WebCore
diff --git a/dom/media/webaudio/blink/Reverb.h b/dom/media/webaudio/blink/Reverb.h
new file mode 100644
index 0000000000..16459532a9
--- /dev/null
+++ b/dom/media/webaudio/blink/Reverb.h
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef Reverb_h
+#define Reverb_h
+
+#include "ReverbConvolver.h"
+#include "nsTArray.h"
+#include "AudioBlock.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/UniquePtr.h"
+
+namespace WebCore {
+
+using mozilla::UniquePtr;
+
+// Multi-channel convolution reverb with channel matrixing - one or more
+// ReverbConvolver objects are used internally.
+
+class Reverb {
+ public:
+ enum { MaxFrameSize = 256 };
+
+ // renderSliceSize is a rendering hint, so the FFTs can be optimized to not
+ // all occur at the same time (very bad when rendering on a real-time thread).
+ // aAllocation failure is to be checked by the caller. If false, internal
+ // memory could not be allocated, and this Reverb instance is not to be
+ // used.
+ Reverb(const mozilla::AudioChunk& impulseResponseBuffer, size_t maxFFTSize,
+ bool useBackgroundThreads, bool normalize, float sampleRate,
+ bool* aAllocationFailure);
+
+ void process(const mozilla::AudioBlock* sourceBus,
+ mozilla::AudioBlock* destinationBus);
+
+ size_t impulseResponseLength() const { return m_impulseResponseLength; }
+
+ size_t sizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ private:
+ bool initialize(const nsTArray<const float*>& impulseResponseBuffer,
+ size_t impulseResponseBufferLength, size_t maxFFTSize,
+ bool useBackgroundThreads);
+
+ size_t m_impulseResponseLength;
+
+ nsTArray<UniquePtr<ReverbConvolver> > m_convolvers;
+
+ // For "True" stereo processing
+ mozilla::AudioBlock m_tempBuffer;
+};
+
+} // namespace WebCore
+
+#endif // Reverb_h
diff --git a/dom/media/webaudio/blink/ReverbAccumulationBuffer.cpp b/dom/media/webaudio/blink/ReverbAccumulationBuffer.cpp
new file mode 100644
index 0000000000..ec745c294f
--- /dev/null
+++ b/dom/media/webaudio/blink/ReverbAccumulationBuffer.cpp
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ReverbAccumulationBuffer.h"
+#include "AudioNodeEngine.h"
+#include "mozilla/PodOperations.h"
+#include <algorithm>
+
+using namespace mozilla;
+
+namespace WebCore {
+
+ReverbAccumulationBuffer::ReverbAccumulationBuffer()
+ : m_readIndex(0), m_readTimeFrame(0) {}
+
+bool ReverbAccumulationBuffer::allocate(size_t length) {
+ if (!m_buffer.SetLength(length, fallible)) {
+ return false;
+ }
+ PodZero(m_buffer.Elements(), length);
+ return true;
+}
+
+void ReverbAccumulationBuffer::readAndClear(float* destination,
+ size_t numberOfFrames) {
+ size_t bufferLength = m_buffer.Length();
+ bool isCopySafe =
+ m_readIndex <= bufferLength && numberOfFrames <= bufferLength;
+
+ MOZ_ASSERT(isCopySafe);
+ if (!isCopySafe) return;
+
+ size_t framesAvailable = bufferLength - m_readIndex;
+ size_t numberOfFrames1 = std::min(numberOfFrames, framesAvailable);
+ size_t numberOfFrames2 = numberOfFrames - numberOfFrames1;
+
+ float* source = m_buffer.Elements();
+ memcpy(destination, source + m_readIndex, sizeof(float) * numberOfFrames1);
+ memset(source + m_readIndex, 0, sizeof(float) * numberOfFrames1);
+
+ // Handle wrap-around if necessary
+ if (numberOfFrames2 > 0) {
+ memcpy(destination + numberOfFrames1, source,
+ sizeof(float) * numberOfFrames2);
+ memset(source, 0, sizeof(float) * numberOfFrames2);
+ }
+
+ m_readIndex = (m_readIndex + numberOfFrames) % bufferLength;
+ m_readTimeFrame += numberOfFrames;
+}
+
+void ReverbAccumulationBuffer::accumulate(const float* source,
+ size_t numberOfFrames,
+ size_t* readIndex,
+ size_t delayFrames) {
+ size_t bufferLength = m_buffer.Length();
+
+ size_t writeIndex = (*readIndex + delayFrames) % bufferLength;
+
+ // Update caller's readIndex
+ *readIndex = (*readIndex + numberOfFrames) % bufferLength;
+
+ size_t framesAvailable = bufferLength - writeIndex;
+ size_t numberOfFrames1 = std::min(numberOfFrames, framesAvailable);
+ size_t numberOfFrames2 = numberOfFrames - numberOfFrames1;
+
+ float* destination = m_buffer.Elements();
+
+ bool isSafe = writeIndex <= bufferLength &&
+ numberOfFrames1 + writeIndex <= bufferLength &&
+ numberOfFrames2 <= bufferLength;
+ MOZ_ASSERT(isSafe);
+ if (!isSafe) return;
+
+ AudioBufferAddWithScale(source, 1.0f, destination + writeIndex,
+ numberOfFrames1);
+ if (numberOfFrames2 > 0) {
+ AudioBufferAddWithScale(source + numberOfFrames1, 1.0f, destination,
+ numberOfFrames2);
+ }
+}
+
+void ReverbAccumulationBuffer::reset() {
+ PodZero(m_buffer.Elements(), m_buffer.Length());
+ m_readIndex = 0;
+ m_readTimeFrame = 0;
+}
+
+} // namespace WebCore
diff --git a/dom/media/webaudio/blink/ReverbAccumulationBuffer.h b/dom/media/webaudio/blink/ReverbAccumulationBuffer.h
new file mode 100644
index 0000000000..8883a56a73
--- /dev/null
+++ b/dom/media/webaudio/blink/ReverbAccumulationBuffer.h
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef ReverbAccumulationBuffer_h
+#define ReverbAccumulationBuffer_h
+
+#include "AlignedTArray.h"
+#include "mozilla/MemoryReporting.h"
+
+namespace WebCore {
+
+// ReverbAccumulationBuffer is a circular delay buffer with one client reading
+// from it and multiple clients writing/accumulating to it at different delay
+// offsets from the read position. The read operation will zero the memory just
+// read from the buffer, so it will be ready for accumulation the next time
+// around.
+class ReverbAccumulationBuffer {
+ public:
+ ReverbAccumulationBuffer();
+ // Returns false on failure.
+ bool allocate(size_t length);
+
+ // This will read from, then clear-out numberOfFrames
+ void readAndClear(float* destination, size_t numberOfFrames);
+
+ // Each ReverbConvolverStage will accumulate its output at the appropriate
+ // delay from the read position. We need to pass in and update readIndex here,
+ // since each ReverbConvolverStage may be running in a different thread than
+ // the realtime thread calling ReadAndClear() and maintaining m_readIndex
+ void accumulate(const float* source, size_t numberOfFrames, size_t* readIndex,
+ size_t delayFrames);
+
+ size_t readIndex() const { return m_readIndex; }
+
+ size_t readTimeFrame() const { return m_readTimeFrame; }
+
+ void reset();
+
+ size_t sizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const {
+ return m_buffer.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ private:
+ AlignedTArray<float, 16> m_buffer;
+ size_t m_readIndex;
+ size_t m_readTimeFrame; // for debugging (frame on continuous timeline)
+};
+
+} // namespace WebCore
+
+#endif // ReverbAccumulationBuffer_h
diff --git a/dom/media/webaudio/blink/ReverbConvolver.cpp b/dom/media/webaudio/blink/ReverbConvolver.cpp
new file mode 100644
index 0000000000..6965a28535
--- /dev/null
+++ b/dom/media/webaudio/blink/ReverbConvolver.cpp
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ReverbConvolver.h"
+#include "ReverbConvolverStage.h"
+
+using namespace mozilla;
+
+namespace WebCore {
+
+const int InputBufferSize = 8 * 16384;
+
+// We only process the leading portion of the impulse response in the real-time
+// thread. We don't exceed this length. It turns out then, that the background
+// thread has about 278msec of scheduling slop. Empirically, this has been found
+// to be a good compromise between giving enough time for scheduling slop, while
+// still minimizing the amount of processing done in the primary (high-priority)
+// thread. This was found to be a good value on Mac OS X, and may work well on
+// other platforms as well, assuming the very rough scheduling latencies are
+// similar on these time-scales. Of course, this code may need to be tuned for
+// individual platforms if this assumption is found to be incorrect.
+const size_t RealtimeFrameLimit = 8192 + 4096 // ~278msec @ 44.1KHz
+ - WEBAUDIO_BLOCK_SIZE;
+// First stage will have size MinFFTSize - successive stages will double in
+// size each time until we hit the maximum size.
+const size_t MinFFTSize = 256;
+// If we are using background threads then don't exceed this FFT size for the
+// stages which run in the real-time thread. This avoids having only one or
+// two large stages (size 16384 or so) at the end which take a lot of time
+// every several processing slices. This way we amortize the cost over more
+// processing slices.
+const size_t MaxRealtimeFFTSize = 4096;
+
+ReverbConvolver::ReverbConvolver(const float* impulseResponseData,
+ size_t impulseResponseLength,
+ size_t maxFFTSize, size_t convolverRenderPhase,
+ bool useBackgroundThreads,
+ bool* aAllocationFailure)
+ : m_impulseResponseLength(impulseResponseLength),
+ m_accumulationBuffer(),
+ m_inputBuffer(InputBufferSize),
+ m_backgroundThread("ConvolverWorker"),
+ m_backgroundThreadMonitor("ConvolverMonitor"),
+ m_useBackgroundThreads(useBackgroundThreads),
+ m_wantsToExit(false),
+ m_moreInputBuffered(false) {
+ *aAllocationFailure = !m_accumulationBuffer.allocate(impulseResponseLength +
+ WEBAUDIO_BLOCK_SIZE);
+ if (*aAllocationFailure) {
+ return;
+ }
+ // For the moment, a good way to know if we have real-time constraint is to
+ // check if we're using background threads. Otherwise, assume we're being run
+ // from a command-line tool.
+ bool hasRealtimeConstraint = useBackgroundThreads;
+
+ const float* response = impulseResponseData;
+ size_t totalResponseLength = impulseResponseLength;
+
+ // The total latency is zero because the first FFT stage is small enough
+ // to return output in the first block.
+ size_t reverbTotalLatency = 0;
+
+ size_t stageOffset = 0;
+ size_t stagePhase = 0;
+ size_t fftSize = MinFFTSize;
+ while (stageOffset < totalResponseLength) {
+ size_t stageSize = fftSize / 2;
+
+ // For the last stage, it's possible that stageOffset is such that we're
+ // straddling the end of the impulse response buffer (if we use stageSize),
+ // so reduce the last stage's length...
+ if (stageSize + stageOffset > totalResponseLength) {
+ stageSize = totalResponseLength - stageOffset;
+ // Use smallest FFT that is large enough to cover the last stage.
+ fftSize = MinFFTSize;
+ while (stageSize * 2 > fftSize) {
+ fftSize *= 2;
+ }
+ }
+
+ // This "staggers" the time when each FFT happens so they don't all happen
+ // at the same time
+ int renderPhase = convolverRenderPhase + stagePhase;
+
+ UniquePtr<ReverbConvolverStage> stage(new ReverbConvolverStage(
+ response, totalResponseLength, reverbTotalLatency, stageOffset,
+ stageSize, fftSize, renderPhase, &m_accumulationBuffer));
+
+ bool isBackgroundStage = false;
+
+ if (this->useBackgroundThreads() && stageOffset > RealtimeFrameLimit) {
+ m_backgroundStages.AppendElement(std::move(stage));
+ isBackgroundStage = true;
+ } else
+ m_stages.AppendElement(std::move(stage));
+
+ // Figure out next FFT size
+ fftSize *= 2;
+
+ stageOffset += stageSize;
+
+ if (hasRealtimeConstraint && !isBackgroundStage &&
+ fftSize > MaxRealtimeFFTSize) {
+ fftSize = MaxRealtimeFFTSize;
+ // Custom phase positions for all but the first of the realtime
+ // stages of largest size. These spread out the work of the
+ // larger realtime stages. None of the FFTs of size 1024, 2048 or
+ // 4096 are performed when processing the same block. The first
+ // MaxRealtimeFFTSize = 4096 stage, at the end of the doubling,
+ // performs its FFT at block 7. The FFTs of size 2048 are
+ // performed in blocks 3 + 8 * n and size 1024 at 1 + 4 * n.
+ const uint32_t phaseLookup[] = {14, 0, 10, 4};
+ stagePhase = WEBAUDIO_BLOCK_SIZE *
+ phaseLookup[m_stages.Length() % ArrayLength(phaseLookup)];
+ } else if (fftSize > maxFFTSize) {
+ fftSize = maxFFTSize;
+ // A prime offset spreads out FFTs in a way that all
+ // available phase positions will be used if there are sufficient
+ // stages.
+ stagePhase += 5 * WEBAUDIO_BLOCK_SIZE;
+ } else if (stageSize > WEBAUDIO_BLOCK_SIZE) {
+ // As the stages are doubling in size, the next FFT will occur
+ // mid-way between FFTs for this stage.
+ stagePhase = stageSize - WEBAUDIO_BLOCK_SIZE;
+ }
+ }
+
+ // Start up background thread
+ // FIXME: would be better to up the thread priority here. It doesn't need to
+ // be real-time, but higher than the default...
+ if (this->useBackgroundThreads() && m_backgroundStages.Length() > 0) {
+ if (!m_backgroundThread.Start()) {
+ NS_WARNING("Cannot start convolver thread.");
+ return;
+ }
+ m_backgroundThread.message_loop()->PostTask(NewNonOwningRunnableMethod(
+ "WebCore::ReverbConvolver::backgroundThreadEntry", this,
+ &ReverbConvolver::backgroundThreadEntry));
+ }
+}
+
+ReverbConvolver::~ReverbConvolver() {
+ // Wait for background thread to stop
+ if (useBackgroundThreads() && m_backgroundThread.IsRunning()) {
+ m_wantsToExit = true;
+
+ // Wake up thread so it can return
+ {
+ MonitorAutoLock locker(m_backgroundThreadMonitor);
+ m_moreInputBuffered = true;
+ m_backgroundThreadMonitor.Notify();
+ }
+
+ m_backgroundThread.Stop();
+ }
+}
+
+size_t ReverbConvolver::sizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t amount = aMallocSizeOf(this);
+ amount += m_stages.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (size_t i = 0; i < m_stages.Length(); i++) {
+ if (m_stages[i]) {
+ amount += m_stages[i]->sizeOfIncludingThis(aMallocSizeOf);
+ }
+ }
+
+ amount += m_backgroundStages.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (size_t i = 0; i < m_backgroundStages.Length(); i++) {
+ if (m_backgroundStages[i]) {
+ amount += m_backgroundStages[i]->sizeOfIncludingThis(aMallocSizeOf);
+ }
+ }
+
+ // NB: The buffer sizes are static, so even though they might be accessed
+ // in another thread it's safe to measure them.
+ amount += m_accumulationBuffer.sizeOfExcludingThis(aMallocSizeOf);
+ amount += m_inputBuffer.sizeOfExcludingThis(aMallocSizeOf);
+
+ // Possible future measurements:
+ // - m_backgroundThread
+ // - m_backgroundThreadMonitor
+ return amount;
+}
+
+void ReverbConvolver::backgroundThreadEntry() {
+ while (!m_wantsToExit) {
+ // Wait for realtime thread to give us more input
+ m_moreInputBuffered = false;
+ {
+ MonitorAutoLock locker(m_backgroundThreadMonitor);
+ while (!m_moreInputBuffered && !m_wantsToExit)
+ m_backgroundThreadMonitor.Wait();
+ }
+
+ // Process all of the stages until their read indices reach the input
+ // buffer's write index
+ int writeIndex = m_inputBuffer.writeIndex();
+
+ // Even though it doesn't seem like every stage needs to maintain its own
+ // version of readIndex we do this in case we want to run in more than one
+ // background thread.
+ int readIndex;
+
+ while ((readIndex = m_backgroundStages[0]->inputReadIndex()) !=
+ writeIndex) { // FIXME: do better to detect buffer overrun...
+ // Accumulate contributions from each stage
+ for (size_t i = 0; i < m_backgroundStages.Length(); ++i)
+ m_backgroundStages[i]->processInBackground(this);
+ }
+ }
+}
+
+void ReverbConvolver::process(const float* sourceChannelData,
+ float* destinationChannelData) {
+ const float* source = sourceChannelData;
+ float* destination = destinationChannelData;
+ bool isDataSafe = source && destination;
+ MOZ_ASSERT(isDataSafe);
+ if (!isDataSafe) return;
+
+ // Feed input buffer (read by all threads)
+ m_inputBuffer.write(source, WEBAUDIO_BLOCK_SIZE);
+
+ // Accumulate contributions from each stage
+ for (size_t i = 0; i < m_stages.Length(); ++i) m_stages[i]->process(source);
+
+ // Finally read from accumulation buffer
+ m_accumulationBuffer.readAndClear(destination, WEBAUDIO_BLOCK_SIZE);
+
+ // Now that we've buffered more input, wake up our background thread.
+
+ // Not using a MonitorAutoLock looks strange, but we use a TryLock() instead
+ // because this is run on the real-time thread where it is a disaster for the
+ // lock to be contended (causes audio glitching). It's OK if we fail to
+ // signal from time to time, since we'll get to it the next time we're called.
+ // We're called repeatedly and frequently (around every 3ms). The background
+ // thread is processing well into the future and has a considerable amount of
+ // leeway here...
+ if (m_backgroundThreadMonitor.TryLock()) {
+ m_moreInputBuffered = true;
+ m_backgroundThreadMonitor.Notify();
+ m_backgroundThreadMonitor.Unlock();
+ }
+}
+
+} // namespace WebCore
diff --git a/dom/media/webaudio/blink/ReverbConvolver.h b/dom/media/webaudio/blink/ReverbConvolver.h
new file mode 100644
index 0000000000..8000ba811e
--- /dev/null
+++ b/dom/media/webaudio/blink/ReverbConvolver.h
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef ReverbConvolver_h
+#define ReverbConvolver_h
+
+#include "ReverbAccumulationBuffer.h"
+#include "ReverbInputBuffer.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/UniquePtr.h"
+#ifdef LOG
+# undef LOG
+#endif
+#include "base/thread.h"
+#include <atomic>
+
+namespace WebCore {
+
+using mozilla::UniquePtr;
+
+class ReverbConvolverStage;
+
+class ReverbConvolver {
+ public:
+ // maxFFTSize can be adjusted (from say 2048 to 32768) depending on how much
+ // precision is necessary. For certain tweaky de-convolving applications the
+ // phase errors add up quickly and lead to non-sensical results with larger
+ // FFT sizes and single-precision floats. In these cases 2048 is a good size.
+ // If not doing multi-threaded convolution, then should not go > 8192.
+ ReverbConvolver(const float* impulseResponseData,
+ size_t impulseResponseLength, size_t maxFFTSize,
+ size_t convolverRenderPhase, bool useBackgroundThreads,
+ bool* aAllocationFailure);
+ ~ReverbConvolver();
+
+ void process(const float* sourceChannelData, float* destinationChannelData);
+
+ size_t impulseResponseLength() const { return m_impulseResponseLength; }
+
+ ReverbInputBuffer* inputBuffer() { return &m_inputBuffer; }
+
+ bool useBackgroundThreads() const { return m_useBackgroundThreads; }
+ void backgroundThreadEntry();
+
+ size_t sizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ private:
+ nsTArray<UniquePtr<ReverbConvolverStage> > m_stages;
+ nsTArray<UniquePtr<ReverbConvolverStage> > m_backgroundStages;
+ size_t m_impulseResponseLength;
+
+ ReverbAccumulationBuffer m_accumulationBuffer;
+
+ // One or more background threads read from this input buffer which is fed
+ // from the realtime thread.
+ ReverbInputBuffer m_inputBuffer;
+
+ // Background thread and synchronization
+ base::Thread m_backgroundThread;
+ mozilla::Monitor m_backgroundThreadMonitor MOZ_UNANNOTATED;
+ bool m_useBackgroundThreads;
+ std::atomic<bool> m_wantsToExit;
+ std::atomic<bool> m_moreInputBuffered;
+};
+
+} // namespace WebCore
+
+#endif // ReverbConvolver_h
diff --git a/dom/media/webaudio/blink/ReverbConvolverStage.cpp b/dom/media/webaudio/blink/ReverbConvolverStage.cpp
new file mode 100644
index 0000000000..56354f0271
--- /dev/null
+++ b/dom/media/webaudio/blink/ReverbConvolverStage.cpp
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ReverbConvolverStage.h"
+
+#include "ReverbAccumulationBuffer.h"
+#include "ReverbConvolver.h"
+#include "ReverbInputBuffer.h"
+#include "mozilla/PodOperations.h"
+
+using namespace mozilla;
+
+namespace WebCore {
+
+ReverbConvolverStage::ReverbConvolverStage(
+ const float* impulseResponse, size_t, size_t reverbTotalLatency,
+ size_t stageOffset, size_t stageLength, size_t fftSize, size_t renderPhase,
+ ReverbAccumulationBuffer* accumulationBuffer)
+ : m_accumulationBuffer(accumulationBuffer),
+ m_accumulationReadIndex(0),
+ m_inputReadIndex(0) {
+ MOZ_ASSERT(impulseResponse);
+ MOZ_ASSERT(accumulationBuffer);
+
+ m_fftKernel = MakeUnique<FFTBlock>(fftSize);
+ m_fftKernel->PadAndMakeScaledDFT(impulseResponse + stageOffset, stageLength);
+ m_fftConvolver = MakeUnique<FFTConvolver>(fftSize, renderPhase);
+
+ // The convolution stage at offset stageOffset needs to have a corresponding
+ // delay to cancel out the offset.
+ size_t totalDelay = stageOffset + reverbTotalLatency;
+
+ // But, the FFT convolution itself incurs latency, so subtract this out...
+ size_t fftLatency = m_fftConvolver->latencyFrames();
+ MOZ_ASSERT(totalDelay >= fftLatency);
+ totalDelay -= fftLatency;
+
+ m_postDelayLength = totalDelay;
+}
+
+size_t ReverbConvolverStage::sizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t amount = aMallocSizeOf(this);
+
+ if (m_fftKernel) {
+ amount += m_fftKernel->SizeOfIncludingThis(aMallocSizeOf);
+ }
+
+ if (m_fftConvolver) {
+ amount += m_fftConvolver->sizeOfIncludingThis(aMallocSizeOf);
+ }
+
+ return amount;
+}
+
+void ReverbConvolverStage::processInBackground(ReverbConvolver* convolver) {
+ ReverbInputBuffer* inputBuffer = convolver->inputBuffer();
+ float* source =
+ inputBuffer->directReadFrom(&m_inputReadIndex, WEBAUDIO_BLOCK_SIZE);
+ process(source);
+}
+
+void ReverbConvolverStage::process(const float* source) {
+ MOZ_ASSERT(source);
+ if (!source) return;
+
+ // Now, run the convolution (into the delay buffer).
+ // An expensive FFT will happen every fftSize / 2 frames.
+ const float* output = m_fftConvolver->process(m_fftKernel.get(), source);
+
+ // Now accumulate into reverb's accumulation buffer.
+ m_accumulationBuffer->accumulate(output, WEBAUDIO_BLOCK_SIZE,
+ &m_accumulationReadIndex, m_postDelayLength);
+}
+
+} // namespace WebCore
diff --git a/dom/media/webaudio/blink/ReverbConvolverStage.h b/dom/media/webaudio/blink/ReverbConvolverStage.h
new file mode 100644
index 0000000000..f770e76d45
--- /dev/null
+++ b/dom/media/webaudio/blink/ReverbConvolverStage.h
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef ReverbConvolverStage_h
+#define ReverbConvolverStage_h
+
+#include "FFTConvolver.h"
+
+#include "nsTArray.h"
+#include "mozilla/FFTBlock.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/UniquePtr.h"
+
+namespace WebCore {
+
+using mozilla::FFTBlock;
+
+class ReverbAccumulationBuffer;
+class ReverbConvolver;
+
+// A ReverbConvolverStage represents the convolution associated with a
+// sub-section of a large impulse response. It incorporates a delay line to
+// account for the offset of the sub-section within the larger impulse response.
+class ReverbConvolverStage {
+ public:
+ // renderPhase is useful to know so that we can manipulate the pre versus post
+ // delay so that stages will perform their heavy work (FFT processing) on
+ // different slices to balance the load in a real-time thread.
+ ReverbConvolverStage(const float* impulseResponse, size_t responseLength,
+ size_t reverbTotalLatency, size_t stageOffset,
+ size_t stageLength, size_t fftSize, size_t renderPhase,
+ ReverbAccumulationBuffer*);
+
+ // |source| must point to an array of WEBAUDIO_BLOCK_SIZE elements.
+ void process(const float* source);
+
+ void processInBackground(ReverbConvolver* convolver);
+
+ // Useful for background processing
+ int inputReadIndex() const { return m_inputReadIndex; }
+
+ size_t sizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ private:
+ mozilla::UniquePtr<FFTBlock> m_fftKernel;
+ mozilla::UniquePtr<FFTConvolver> m_fftConvolver;
+
+ ReverbAccumulationBuffer* m_accumulationBuffer;
+ size_t m_accumulationReadIndex;
+ int m_inputReadIndex;
+
+ size_t m_postDelayLength;
+
+ nsTArray<float> m_temporaryBuffer;
+};
+
+} // namespace WebCore
+
+#endif // ReverbConvolverStage_h
diff --git a/dom/media/webaudio/blink/ReverbInputBuffer.cpp b/dom/media/webaudio/blink/ReverbInputBuffer.cpp
new file mode 100644
index 0000000000..d6a69263c9
--- /dev/null
+++ b/dom/media/webaudio/blink/ReverbInputBuffer.cpp
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ReverbInputBuffer.h"
+#include "mozilla/PodOperations.h"
+
+using namespace mozilla;
+
+namespace WebCore {
+
+ReverbInputBuffer::ReverbInputBuffer(size_t length) : m_writeIndex(0) {
+ m_buffer.SetLength(length);
+ PodZero(m_buffer.Elements(), length);
+}
+
+void ReverbInputBuffer::write(const float* sourceP, size_t numberOfFrames) {
+ // m_writeIndex is atomic and checked by other threads, so only touch
+ // it at the start and end.
+ size_t bufferLength = m_buffer.Length();
+ size_t index = m_writeIndex;
+ size_t newIndex = index + numberOfFrames;
+
+ MOZ_RELEASE_ASSERT(newIndex <= bufferLength);
+
+ memcpy(m_buffer.Elements() + index, sourceP, sizeof(float) * numberOfFrames);
+
+ if (newIndex >= bufferLength) {
+ newIndex = 0;
+ }
+ m_writeIndex = newIndex;
+}
+
+float* ReverbInputBuffer::directReadFrom(int* readIndex,
+ size_t numberOfFrames) {
+ size_t bufferLength = m_buffer.Length();
+ bool isPointerGood = readIndex && *readIndex >= 0 &&
+ *readIndex + numberOfFrames <= bufferLength;
+ MOZ_ASSERT(isPointerGood);
+ if (!isPointerGood) {
+ // Should never happen in practice but return pointer to start of buffer
+ // (avoid crash)
+ if (readIndex) *readIndex = 0;
+ return m_buffer.Elements();
+ }
+
+ float* sourceP = m_buffer.Elements();
+ float* p = sourceP + *readIndex;
+
+ // Update readIndex
+ *readIndex = (*readIndex + numberOfFrames) % bufferLength;
+
+ return p;
+}
+
+void ReverbInputBuffer::reset() {
+ PodZero(m_buffer.Elements(), m_buffer.Length());
+ m_writeIndex = 0;
+}
+
+} // namespace WebCore
diff --git a/dom/media/webaudio/blink/ReverbInputBuffer.h b/dom/media/webaudio/blink/ReverbInputBuffer.h
new file mode 100644
index 0000000000..93ef97a010
--- /dev/null
+++ b/dom/media/webaudio/blink/ReverbInputBuffer.h
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2010 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef ReverbInputBuffer_h
+#define ReverbInputBuffer_h
+
+#include "nsTArray.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/MemoryReporting.h"
+
+namespace WebCore {
+
+// ReverbInputBuffer is used to buffer input samples for deferred processing by
+// the background threads.
+class ReverbInputBuffer {
+ public:
+ explicit ReverbInputBuffer(size_t length);
+
+ // The realtime audio thread keeps writing samples here.
+ // The assumption is that the buffer's length is evenly divisible by
+ // numberOfFrames (for nearly all cases this will be fine).
+ // FIXME: remove numberOfFrames restriction...
+ void write(const float* sourceP, size_t numberOfFrames);
+
+ // Background threads can call this to check if there's anything to read...
+ size_t writeIndex() const { return m_writeIndex; }
+
+ // The individual background threads read here (and hope that they can keep up
+ // with the buffer writing). readIndex is updated with the next readIndex to
+ // read from... The assumption is that the buffer's length is evenly divisible
+ // by numberOfFrames.
+ // FIXME: remove numberOfFrames restriction...
+ float* directReadFrom(int* readIndex, size_t numberOfFrames);
+
+ void reset();
+
+ size_t sizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const {
+ return m_buffer.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ private:
+ nsTArray<float> m_buffer;
+ mozilla::Atomic<size_t, mozilla::ReleaseAcquire> m_writeIndex;
+};
+
+} // namespace WebCore
+
+#endif // ReverbInputBuffer_h
diff --git a/dom/media/webaudio/blink/ZeroPole.cpp b/dom/media/webaudio/blink/ZeroPole.cpp
new file mode 100644
index 0000000000..4d0888d758
--- /dev/null
+++ b/dom/media/webaudio/blink/ZeroPole.cpp
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2011 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include "ZeroPole.h"
+
+#include "DenormalDisabler.h"
+
+#include <cmath>
+#include <float.h>
+
+namespace WebCore {
+
+void ZeroPole::process(const float* source, float* destination,
+ int framesToProcess) {
+ float zero = m_zero;
+ float pole = m_pole;
+
+ // Gain compensation to make 0dB @ 0Hz
+ const float k1 = 1 / (1 - zero);
+ const float k2 = 1 - pole;
+
+ // Member variables to locals.
+ float lastX = m_lastX;
+ float lastY = m_lastY;
+
+ for (int i = 0; i < framesToProcess; ++i) {
+ float input = source[i];
+
+ // Zero
+ float output1 = k1 * (input - zero * lastX);
+ lastX = input;
+
+ // Pole
+ float output2 = k2 * output1 + pole * lastY;
+ lastY = output2;
+
+ destination[i] = output2;
+ }
+
+// Locals to member variables. Flush denormals here so we don't
+// slow down the inner loop above.
+#ifndef HAVE_DENORMAL
+ if (lastX == 0.0f && lastY != 0.0f && fabsf(lastY) < FLT_MIN) {
+ // Flush future values to zero (until there is new input).
+ lastY = 0.0;
+
+ // Flush calculated values.
+ for (int i = framesToProcess; i-- && fabsf(destination[i]) < FLT_MIN;) {
+ destination[i] = 0.0f;
+ }
+ }
+#endif
+
+ m_lastX = lastX;
+ m_lastY = lastY;
+}
+
+} // namespace WebCore
diff --git a/dom/media/webaudio/blink/ZeroPole.h b/dom/media/webaudio/blink/ZeroPole.h
new file mode 100644
index 0000000000..07f3f04f53
--- /dev/null
+++ b/dom/media/webaudio/blink/ZeroPole.h
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2011 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
+ * its contributors may be used to endorse or promote products derived
+ * from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
+ * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+ * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
+ * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+ * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+ * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef ZeroPole_h
+#define ZeroPole_h
+
+namespace WebCore {
+
+// ZeroPole is a simple filter with one zero and one pole.
+
+class ZeroPole {
+ public:
+ ZeroPole() : m_zero(0), m_pole(0), m_lastX(0), m_lastY(0) {}
+
+ void process(const float* source, float* destination, int framesToProcess);
+
+ // Reset filter state.
+ void reset() {
+ m_lastX = 0;
+ m_lastY = 0;
+ }
+
+ void setZero(float zero) { m_zero = zero; }
+ void setPole(float pole) { m_pole = pole; }
+
+ float zero() const { return m_zero; }
+ float pole() const { return m_pole; }
+
+ private:
+ float m_zero;
+ float m_pole;
+ float m_lastX;
+ float m_lastY;
+};
+
+} // namespace WebCore
+
+#endif // ZeroPole_h
diff --git a/dom/media/webaudio/blink/moz.build b/dom/media/webaudio/blink/moz.build
new file mode 100644
index 0000000000..5f00d01ed3
--- /dev/null
+++ b/dom/media/webaudio/blink/moz.build
@@ -0,0 +1,36 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES += [
+ "Biquad.cpp",
+ "DynamicsCompressor.cpp",
+ "DynamicsCompressorKernel.cpp",
+ "FFTConvolver.cpp",
+ "HRTFDatabase.cpp",
+ "HRTFDatabaseLoader.cpp",
+ "HRTFElevation.cpp",
+ "HRTFKernel.cpp",
+ "HRTFPanner.cpp",
+ "IIRFilter.cpp",
+ "PeriodicWave.cpp",
+ "Reverb.cpp",
+ "ReverbAccumulationBuffer.cpp",
+ "ReverbConvolver.cpp",
+ "ReverbConvolverStage.cpp",
+ "ReverbInputBuffer.cpp",
+ "ZeroPole.cpp",
+]
+
+# Are we targeting x86 or x64? If so, build SSE2 files.
+if CONFIG["INTEL_ARCHITECTURE"]:
+ DEFINES["USE_SSE2"] = True
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+LOCAL_INCLUDES += [
+ "/dom/media/webaudio",
+]
diff --git a/dom/media/webaudio/moz.build b/dom/media/webaudio/moz.build
new file mode 100644
index 0000000000..3fb372dbca
--- /dev/null
+++ b/dom/media/webaudio/moz.build
@@ -0,0 +1,153 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("*"):
+ BUG_COMPONENT = ("Core", "Web Audio")
+
+DIRS += ["blink"]
+
+MOCHITEST_MANIFESTS += [
+ "test/blink/mochitest.ini",
+ "test/mochitest.ini",
+ "test/mochitest_audio.ini",
+ "test/mochitest_bugs.ini",
+ "test/mochitest_media.ini",
+]
+
+TEST_HARNESS_FILES.testing.mochitest.tests.dom.media.webaudio.test.blink += [
+ "test/blink/audio-testing.js",
+ "test/blink/convolution-testing.js",
+ "test/blink/panner-model-testing.js",
+]
+
+EXPORTS += [
+ "AlignedTArray.h",
+ "AudioBlock.h",
+ "AudioEventTimeline.h",
+ "AudioNodeEngine.h",
+ "AudioNodeExternalInputTrack.h",
+ "AudioNodeTrack.h",
+ "AudioParamTimeline.h",
+ "MediaBufferDecoder.h",
+ "ThreeDPoint.h",
+ "WebAudioUtils.h",
+]
+
+EXPORTS.mozilla += [
+ "FFTBlock.h",
+ "MediaStreamAudioDestinationNode.h",
+]
+
+EXPORTS.mozilla.dom += [
+ "AnalyserNode.h",
+ "AudioBuffer.h",
+ "AudioBufferSourceNode.h",
+ "AudioContext.h",
+ "AudioDestinationNode.h",
+ "AudioListener.h",
+ "AudioNode.h",
+ "AudioParam.h",
+ "AudioParamDescriptorMap.h",
+ "AudioParamMap.h",
+ "AudioProcessingEvent.h",
+ "AudioScheduledSourceNode.h",
+ "AudioWorkletGlobalScope.h",
+ "AudioWorkletNode.h",
+ "AudioWorkletProcessor.h",
+ "BiquadFilterNode.h",
+ "ChannelMergerNode.h",
+ "ChannelSplitterNode.h",
+ "ConstantSourceNode.h",
+ "ConvolverNode.h",
+ "DelayNode.h",
+ "DynamicsCompressorNode.h",
+ "GainNode.h",
+ "IIRFilterNode.h",
+ "MediaElementAudioSourceNode.h",
+ "MediaStreamAudioDestinationNode.h",
+ "MediaStreamAudioSourceNode.h",
+ "MediaStreamTrackAudioSourceNode.h",
+ "OscillatorNode.h",
+ "PannerNode.h",
+ "PeriodicWave.h",
+ "ScriptProcessorNode.h",
+ "StereoPannerNode.h",
+ "WaveShaperNode.h",
+]
+
+UNIFIED_SOURCES += [
+ "AnalyserNode.cpp",
+ "AudioBlock.cpp",
+ "AudioBuffer.cpp",
+ "AudioBufferSourceNode.cpp",
+ "AudioContext.cpp",
+ "AudioDestinationNode.cpp",
+ "AudioEventTimeline.cpp",
+ "AudioListener.cpp",
+ "AudioNode.cpp",
+ "AudioNodeEngine.cpp",
+ "AudioNodeExternalInputTrack.cpp",
+ "AudioNodeTrack.cpp",
+ "AudioParam.cpp",
+ "AudioParamMap.cpp",
+ "AudioProcessingEvent.cpp",
+ "AudioScheduledSourceNode.cpp",
+ "AudioWorkletGlobalScope.cpp",
+ "AudioWorkletImpl.cpp",
+ "AudioWorkletNode.cpp",
+ "AudioWorkletProcessor.cpp",
+ "BiquadFilterNode.cpp",
+ "ChannelMergerNode.cpp",
+ "ChannelSplitterNode.cpp",
+ "ConstantSourceNode.cpp",
+ "ConvolverNode.cpp",
+ "DelayBuffer.cpp",
+ "DelayNode.cpp",
+ "DynamicsCompressorNode.cpp",
+ "FFTBlock.cpp",
+ "GainNode.cpp",
+ "IIRFilterNode.cpp",
+ "MediaBufferDecoder.cpp",
+ "MediaElementAudioSourceNode.cpp",
+ "MediaStreamAudioDestinationNode.cpp",
+ "MediaStreamAudioSourceNode.cpp",
+ "MediaStreamTrackAudioSourceNode.cpp",
+ "OscillatorNode.cpp",
+ "PannerNode.cpp",
+ "PeriodicWave.cpp",
+ "ScriptProcessorNode.cpp",
+ "StereoPannerNode.cpp",
+ "ThreeDPoint.cpp",
+ "WaveShaperNode.cpp",
+ "WebAudioUtils.cpp",
+]
+
+if CONFIG["CPU_ARCH"] == "aarch64" or CONFIG["BUILD_ARM_NEON"]:
+ DEFINES["USE_NEON"] = True
+ LOCAL_INCLUDES += ["/third_party/xsimd/include"]
+ SOURCES += ["AudioNodeEngineNEON.cpp"]
+ SOURCES["AudioNodeEngineNEON.cpp"].flags += CONFIG["NEON_FLAGS"]
+ if CONFIG["BUILD_ARM_NEON"]:
+ LOCAL_INCLUDES += ["/media/openmax_dl/dl/api/"]
+
+# Are we targeting x86 or x64? If so, build SSEX files.
+if CONFIG["INTEL_ARCHITECTURE"]:
+ DEFINES["USE_SSE2"] = True
+ SOURCES += ["AudioNodeEngineSSE2.cpp"]
+ LOCAL_INCLUDES += ["/third_party/xsimd/include"]
+ SOURCES["AudioNodeEngineSSE2.cpp"].flags += CONFIG["SSE2_FLAGS"]
+ if CONFIG["SSE4_2_FLAGS"] and CONFIG["FMA_FLAGS"]:
+ DEFINES["USE_SSE4_2"] = True
+ DEFINES["USE_FMA3"] = True
+ SOURCES += ["AudioNodeEngineSSE4_2_FMA3.cpp"]
+ SOURCES["AudioNodeEngineSSE4_2_FMA3.cpp"].flags += (
+ CONFIG["SSE4_2_FLAGS"] + CONFIG["FMA_FLAGS"]
+ )
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+LOCAL_INCLUDES += [".."]
diff --git a/dom/media/webaudio/test/8kHz-320kbps-6ch.aac b/dom/media/webaudio/test/8kHz-320kbps-6ch.aac
new file mode 100644
index 0000000000..8981d40dfd
--- /dev/null
+++ b/dom/media/webaudio/test/8kHz-320kbps-6ch.aac
Binary files differ
diff --git a/dom/media/webaudio/test/audio-expected.wav b/dom/media/webaudio/test/audio-expected.wav
new file mode 100644
index 0000000000..1519270776
--- /dev/null
+++ b/dom/media/webaudio/test/audio-expected.wav
Binary files differ
diff --git a/dom/media/webaudio/test/audio-mono-expected-2.wav b/dom/media/webaudio/test/audio-mono-expected-2.wav
new file mode 100644
index 0000000000..68c90dfa1e
--- /dev/null
+++ b/dom/media/webaudio/test/audio-mono-expected-2.wav
Binary files differ
diff --git a/dom/media/webaudio/test/audio-mono-expected.wav b/dom/media/webaudio/test/audio-mono-expected.wav
new file mode 100644
index 0000000000..bf00e5cdf2
--- /dev/null
+++ b/dom/media/webaudio/test/audio-mono-expected.wav
Binary files differ
diff --git a/dom/media/webaudio/test/audio-quad.wav b/dom/media/webaudio/test/audio-quad.wav
new file mode 100644
index 0000000000..093f0197ae
--- /dev/null
+++ b/dom/media/webaudio/test/audio-quad.wav
Binary files differ
diff --git a/dom/media/webaudio/test/audio.ogv b/dom/media/webaudio/test/audio.ogv
new file mode 100644
index 0000000000..68dee3cf2b
--- /dev/null
+++ b/dom/media/webaudio/test/audio.ogv
Binary files differ
diff --git a/dom/media/webaudio/test/audioBufferSourceNodeDetached_worker.js b/dom/media/webaudio/test/audioBufferSourceNodeDetached_worker.js
new file mode 100644
index 0000000000..2a5a4bff89
--- /dev/null
+++ b/dom/media/webaudio/test/audioBufferSourceNodeDetached_worker.js
@@ -0,0 +1,3 @@
+onmessage = function (event) {
+ postMessage("Pong");
+};
diff --git a/dom/media/webaudio/test/audiovideo.mp4 b/dom/media/webaudio/test/audiovideo.mp4
new file mode 100644
index 0000000000..fe93122d29
--- /dev/null
+++ b/dom/media/webaudio/test/audiovideo.mp4
Binary files differ
diff --git a/dom/media/webaudio/test/blink/README b/dom/media/webaudio/test/blink/README
new file mode 100644
index 0000000000..1d819221fd
--- /dev/null
+++ b/dom/media/webaudio/test/blink/README
@@ -0,0 +1,9 @@
+This directory contains tests originally borrowed from the Blink Web Audio test
+suite.
+
+The process of borrowing tests from Blink is as follows:
+
+* Import the pristine file from the Blink repo, noting the revision in the
+ commit message.
+* Modify the test files to turn the LayoutTest into a mochitest-plain and add
+* them to the test suite in a separate commit.
diff --git a/dom/media/webaudio/test/blink/audio-testing.js b/dom/media/webaudio/test/blink/audio-testing.js
new file mode 100644
index 0000000000..c66d32c7f2
--- /dev/null
+++ b/dom/media/webaudio/test/blink/audio-testing.js
@@ -0,0 +1,192 @@
+if (window.testRunner)
+ testRunner.overridePreference("WebKitWebAudioEnabled", "1");
+
+function writeString(s, a, offset) {
+ for (var i = 0; i < s.length; ++i) {
+ a[offset + i] = s.charCodeAt(i);
+ }
+}
+
+function writeInt16(n, a, offset) {
+ n = Math.floor(n);
+
+ var b1 = n & 255;
+ var b2 = (n >> 8) & 255;
+
+ a[offset + 0] = b1;
+ a[offset + 1] = b2;
+}
+
+function writeInt32(n, a, offset) {
+ n = Math.floor(n);
+ var b1 = n & 255;
+ var b2 = (n >> 8) & 255;
+ var b3 = (n >> 16) & 255;
+ var b4 = (n >> 24) & 255;
+
+ a[offset + 0] = b1;
+ a[offset + 1] = b2;
+ a[offset + 2] = b3;
+ a[offset + 3] = b4;
+}
+
+function writeAudioBuffer(audioBuffer, a, offset) {
+ var n = audioBuffer.length;
+ var channels = audioBuffer.numberOfChannels;
+
+ for (var i = 0; i < n; ++i) {
+ for (var k = 0; k < channels; ++k) {
+ var buffer = audioBuffer.getChannelData(k);
+ var sample = buffer[i] * 32768.0;
+
+ // Clip samples to the limitations of 16-bit.
+ // If we don't do this then we'll get nasty wrap-around distortion.
+ if (sample < -32768)
+ sample = -32768;
+ if (sample > 32767)
+ sample = 32767;
+
+ writeInt16(sample, a, offset);
+ offset += 2;
+ }
+ }
+}
+
+function createWaveFileData(audioBuffer) {
+ var frameLength = audioBuffer.length;
+ var numberOfChannels = audioBuffer.numberOfChannels;
+ var sampleRate = audioBuffer.sampleRate;
+ var bitsPerSample = 16;
+ var byteRate = sampleRate * numberOfChannels * bitsPerSample/8;
+ var blockAlign = numberOfChannels * bitsPerSample/8;
+ var wavDataByteLength = frameLength * numberOfChannels * 2; // 16-bit audio
+ var headerByteLength = 44;
+ var totalLength = headerByteLength + wavDataByteLength;
+
+ var waveFileData = new Uint8Array(totalLength);
+
+ var subChunk1Size = 16; // for linear PCM
+ var subChunk2Size = wavDataByteLength;
+ var chunkSize = 4 + (8 + subChunk1Size) + (8 + subChunk2Size);
+
+ writeString("RIFF", waveFileData, 0);
+ writeInt32(chunkSize, waveFileData, 4);
+ writeString("WAVE", waveFileData, 8);
+ writeString("fmt ", waveFileData, 12);
+
+ writeInt32(subChunk1Size, waveFileData, 16); // SubChunk1Size (4)
+ writeInt16(1, waveFileData, 20); // AudioFormat (2)
+ writeInt16(numberOfChannels, waveFileData, 22); // NumChannels (2)
+ writeInt32(sampleRate, waveFileData, 24); // SampleRate (4)
+ writeInt32(byteRate, waveFileData, 28); // ByteRate (4)
+ writeInt16(blockAlign, waveFileData, 32); // BlockAlign (2)
+ writeInt32(bitsPerSample, waveFileData, 34); // BitsPerSample (4)
+
+ writeString("data", waveFileData, 36);
+ writeInt32(subChunk2Size, waveFileData, 40); // SubChunk2Size (4)
+
+ // Write actual audio data starting at offset 44.
+ writeAudioBuffer(audioBuffer, waveFileData, 44);
+
+ return waveFileData;
+}
+
+function createAudioData(audioBuffer) {
+ return createWaveFileData(audioBuffer);
+}
+
+function finishAudioTest(event) {
+ var audioData = createAudioData(event.renderedBuffer);
+ testRunner.setAudioData(audioData);
+ testRunner.notifyDone();
+}
+
+// Create an impulse in a buffer of length sampleFrameLength
+function createImpulseBuffer(context, sampleFrameLength) {
+ var audioBuffer = context.createBuffer(1, sampleFrameLength, context.sampleRate);
+ var n = audioBuffer.length;
+ var dataL = audioBuffer.getChannelData(0);
+
+ for (var k = 0; k < n; ++k) {
+ dataL[k] = 0;
+ }
+ dataL[0] = 1;
+
+ return audioBuffer;
+}
+
+// Create a buffer of the given length with a linear ramp having values 0 <= x < 1.
+function createLinearRampBuffer(context, sampleFrameLength) {
+ var audioBuffer = context.createBuffer(1, sampleFrameLength, context.sampleRate);
+ var n = audioBuffer.length;
+ var dataL = audioBuffer.getChannelData(0);
+
+ for (var i = 0; i < n; ++i)
+ dataL[i] = i / n;
+
+ return audioBuffer;
+}
+
+// Create a buffer of the given length having a constant value.
+function createConstantBuffer(context, sampleFrameLength, constantValue) {
+ var audioBuffer = context.createBuffer(1, sampleFrameLength, context.sampleRate);
+ var n = audioBuffer.length;
+ var dataL = audioBuffer.getChannelData(0);
+
+ for (var i = 0; i < n; ++i)
+ dataL[i] = constantValue;
+
+ return audioBuffer;
+}
+
+// Create a stereo impulse in a buffer of length sampleFrameLength
+function createStereoImpulseBuffer(context, sampleFrameLength) {
+ var audioBuffer = context.createBuffer(2, sampleFrameLength, context.sampleRate);
+ var n = audioBuffer.length;
+ var dataL = audioBuffer.getChannelData(0);
+ var dataR = audioBuffer.getChannelData(1);
+
+ for (var k = 0; k < n; ++k) {
+ dataL[k] = 0;
+ dataR[k] = 0;
+ }
+ dataL[0] = 1;
+ dataR[0] = 1;
+
+ return audioBuffer;
+}
+
+// Convert time (in seconds) to sample frames.
+function timeToSampleFrame(time, sampleRate) {
+ return Math.floor(0.5 + time * sampleRate);
+}
+
+// Compute the number of sample frames consumed by start with
+// the specified |grainOffset|, |duration|, and |sampleRate|.
+function grainLengthInSampleFrames(grainOffset, duration, sampleRate) {
+ var startFrame = timeToSampleFrame(grainOffset, sampleRate);
+ var endFrame = timeToSampleFrame(grainOffset + duration, sampleRate);
+
+ return endFrame - startFrame;
+}
+
+// True if the number is not an infinity or NaN
+function isValidNumber(x) {
+ return !isNaN(x) && (x != Infinity) && (x != -Infinity);
+}
+
+function shouldThrowTypeError(func, text) {
+ var ok = false;
+ try {
+ func();
+ } catch (e) {
+ if (e instanceof TypeError) {
+ ok = true;
+ }
+ }
+ if (ok) {
+ testPassed(text + " threw TypeError.");
+ } else {
+ testFailed(text + " should throw TypeError.");
+ }
+}
diff --git a/dom/media/webaudio/test/blink/biquad-filters.js b/dom/media/webaudio/test/blink/biquad-filters.js
new file mode 100644
index 0000000000..06fff98b18
--- /dev/null
+++ b/dom/media/webaudio/test/blink/biquad-filters.js
@@ -0,0 +1,368 @@
+// Taken from WebKit/LayoutTests/webaudio/resources/biquad-filters.js
+
+// A biquad filter has a z-transform of
+// H(z) = (b0 + b1 / z + b2 / z^2) / (1 + a1 / z + a2 / z^2)
+//
+// The formulas for the various filters were taken from
+// http://www.musicdsp.org/files/Audio-EQ-Cookbook.txt.
+
+
+// Lowpass filter.
+function createLowpassFilter(freq, q, gain) {
+ var b0;
+ var b1;
+ var b2;
+ var a0;
+ var a1;
+ var a2;
+
+ if (freq == 1) {
+ // The formula below works, except for roundoff. When freq = 1,
+ // the filter is just a wire, so hardwire the coefficients.
+ b0 = 1;
+ b1 = 0;
+ b2 = 0;
+ a0 = 1;
+ a1 = 0;
+ a2 = 0;
+ } else {
+ var w0 = Math.PI * freq;
+ var alpha = 0.5 * Math.sin(w0) / Math.pow(10, q / 20);
+ var cos_w0 = Math.cos(w0);
+
+ b0 = 0.5 * (1 - cos_w0);
+ b1 = 1 - cos_w0;
+ b2 = b0;
+ a0 = 1 + alpha;
+ a1 = -2.0 * cos_w0;
+ a2 = 1 - alpha;
+ }
+
+ return normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2);
+}
+
+function createHighpassFilter(freq, q, gain) {
+ var b0;
+ var b1;
+ var b2;
+ var a1;
+ var a2;
+
+ if (freq == 1) {
+ // The filter is 0
+ b0 = 0;
+ b1 = 0;
+ b2 = 0;
+ a0 = 1;
+ a1 = 0;
+ a2 = 0;
+ } else if (freq == 0) {
+ // The filter is 1. Computation of coefficients below is ok, but
+ // there's a pole at 1 and a zero at 1, so round-off could make
+ // the filter unstable.
+ b0 = 1;
+ b1 = 0;
+ b2 = 0;
+ a0 = 1;
+ a1 = 0;
+ a2 = 0;
+ } else {
+ var w0 = Math.PI * freq;
+ var alpha = 0.5 * Math.sin(w0) / Math.pow(10, q / 20);
+ var cos_w0 = Math.cos(w0);
+
+ b0 = 0.5 * (1 + cos_w0);
+ b1 = -1 - cos_w0;
+ b2 = b0;
+ a0 = 1 + alpha;
+ a1 = -2.0 * cos_w0;
+ a2 = 1 - alpha;
+ }
+
+ return normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2);
+}
+
+function normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2) {
+ var scale = 1 / a0;
+
+ return {b0 : b0 * scale,
+ b1 : b1 * scale,
+ b2 : b2 * scale,
+ a1 : a1 * scale,
+ a2 : a2 * scale};
+}
+
+function createBandpassFilter(freq, q, gain) {
+ var b0;
+ var b1;
+ var b2;
+ var a0;
+ var a1;
+ var a2;
+ var coef;
+
+ if (freq > 0 && freq < 1) {
+ var w0 = Math.PI * freq;
+ if (q > 0) {
+ var alpha = Math.sin(w0) / (2 * q);
+ var k = Math.cos(w0);
+
+ b0 = alpha;
+ b1 = 0;
+ b2 = -alpha;
+ a0 = 1 + alpha;
+ a1 = -2 * k;
+ a2 = 1 - alpha;
+
+ coef = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2);
+ } else {
+ // q = 0, and frequency is not 0 or 1. The above formula has a
+ // divide by zero problem. The limit of the z-transform as q
+ // approaches 0 is 1, so set the filter that way.
+ coef = {b0 : 1, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
+ }
+ } else {
+ // When freq = 0 or 1, the z-transform is identically 0,
+ // independent of q.
+ coef = {b0 : 0, b1 : 0, b2 : 0, a1 : 0, a2 : 0}
+ }
+
+ return coef;
+}
+
+function createLowShelfFilter(freq, q, gain) {
+ // q not used
+ var b0;
+ var b1;
+ var b2;
+ var a0;
+ var a1;
+ var a2;
+ var coef;
+
+ var S = 1;
+ var A = Math.pow(10, gain / 40);
+
+ if (freq == 1) {
+ // The filter is just a constant gain
+ coef = {b0 : A * A, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
+ } else if (freq == 0) {
+ // The filter is 1
+ coef = {b0 : 1, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
+ } else {
+ var w0 = Math.PI * freq;
+ var alpha = 1 / 2 * Math.sin(w0) * Math.sqrt((A + 1 / A) * (1 / S - 1) + 2);
+ var k = Math.cos(w0);
+ var k2 = 2 * Math.sqrt(A) * alpha;
+ var Ap1 = A + 1;
+ var Am1 = A - 1;
+
+ b0 = A * (Ap1 - Am1 * k + k2);
+ b1 = 2 * A * (Am1 - Ap1 * k);
+ b2 = A * (Ap1 - Am1 * k - k2);
+ a0 = Ap1 + Am1 * k + k2;
+ a1 = -2 * (Am1 + Ap1 * k);
+ a2 = Ap1 + Am1 * k - k2;
+ coef = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2);
+ }
+
+ return coef;
+}
+
+function createHighShelfFilter(freq, q, gain) {
+ // q not used
+ var b0;
+ var b1;
+ var b2;
+ var a0;
+ var a1;
+ var a2;
+ var coef;
+
+ var A = Math.pow(10, gain / 40);
+
+ if (freq == 1) {
+ // When freq = 1, the z-transform is 1
+ coef = {b0 : 1, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
+ } else if (freq > 0) {
+ var w0 = Math.PI * freq;
+ var S = 1;
+ var alpha = 0.5 * Math.sin(w0) * Math.sqrt((A + 1 / A) * (1 / S - 1) + 2);
+ var k = Math.cos(w0);
+ var k2 = 2 * Math.sqrt(A) * alpha;
+ var Ap1 = A + 1;
+ var Am1 = A - 1;
+
+ b0 = A * (Ap1 + Am1 * k + k2);
+ b1 = -2 * A * (Am1 + Ap1 * k);
+ b2 = A * (Ap1 + Am1 * k - k2);
+ a0 = Ap1 - Am1 * k + k2;
+ a1 = 2 * (Am1 - Ap1*k);
+ a2 = Ap1 - Am1 * k-k2;
+
+ coef = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2);
+ } else {
+ // When freq = 0, the filter is just a gain
+ coef = {b0 : A * A, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
+ }
+
+ return coef;
+}
+
+function createPeakingFilter(freq, q, gain) {
+ var b0;
+ var b1;
+ var b2;
+ var a0;
+ var a1;
+ var a2;
+ var coef;
+
+ var A = Math.pow(10, gain / 40);
+
+ if (freq > 0 && freq < 1) {
+ if (q > 0) {
+ var w0 = Math.PI * freq;
+ var alpha = Math.sin(w0) / (2 * q);
+ var k = Math.cos(w0);
+
+ b0 = 1 + alpha * A;
+ b1 = -2 * k;
+ b2 = 1 - alpha * A;
+ a0 = 1 + alpha / A;
+ a1 = -2 * k;
+ a2 = 1 - alpha / A;
+
+ coef = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2);
+ } else {
+ // q = 0, we have a divide by zero problem in the formulas
+ // above. But if we look at the z-transform, we see that the
+ // limit as q approaches 0 is A^2.
+ coef = {b0 : A * A, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
+ }
+ } else {
+ // freq = 0 or 1, the z-transform is 1
+ coef = {b0 : 1, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
+ }
+
+ return coef;
+}
+
+function createNotchFilter(freq, q, gain) {
+ var b0;
+ var b1;
+ var b2;
+ var a0;
+ var a1;
+ var a2;
+ var coef;
+
+ if (freq > 0 && freq < 1) {
+ if (q > 0) {
+ var w0 = Math.PI * freq;
+ var alpha = Math.sin(w0) / (2 * q);
+ var k = Math.cos(w0);
+
+ b0 = 1;
+ b1 = -2 * k;
+ b2 = 1;
+ a0 = 1 + alpha;
+ a1 = -2 * k;
+ a2 = 1 - alpha;
+ coef = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2);
+ } else {
+ // When q = 0, we get a divide by zero above. The limit of the
+ // z-transform as q approaches 0 is 0, so set the coefficients
+ // appropriately.
+ coef = {b0 : 0, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
+ }
+ } else {
+ // When freq = 0 or 1, the z-transform is 1
+ coef = {b0 : 1, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
+ }
+
+ return coef;
+}
+
+function createAllpassFilter(freq, q, gain) {
+ var b0;
+ var b1;
+ var b2;
+ var a0;
+ var a1;
+ var a2;
+ var coef;
+
+ if (freq > 0 && freq < 1) {
+ if (q > 0) {
+ var w0 = Math.PI * freq;
+ var alpha = Math.sin(w0) / (2 * q);
+ var k = Math.cos(w0);
+
+ b0 = 1 - alpha;
+ b1 = -2 * k;
+ b2 = 1 + alpha;
+ a0 = 1 + alpha;
+ a1 = -2 * k;
+ a2 = 1 - alpha;
+ coef = normalizeFilterCoefficients(b0, b1, b2, a0, a1, a2);
+ } else {
+ // q = 0
+ coef = {b0 : -1, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
+ }
+ } else {
+ coef = {b0 : 1, b1 : 0, b2 : 0, a1 : 0, a2 : 0};
+ }
+
+ return coef;
+}
+
+function filterData(filterCoef, signal, len) {
+ var y = new Array(len);
+ var b0 = filterCoef.b0;
+ var b1 = filterCoef.b1;
+ var b2 = filterCoef.b2;
+ var a1 = filterCoef.a1;
+ var a2 = filterCoef.a2;
+
+ // Prime the pump. (Assumes the signal has length >= 2!)
+ y[0] = b0 * signal[0];
+ y[1] = b0 * signal[1] + b1 * signal[0] - a1 * y[0];
+
+ // Filter all of the signal that we have.
+ for (var k = 2; k < Math.min(signal.length, len); ++k) {
+ y[k] = b0 * signal[k] + b1 * signal[k-1] + b2 * signal[k-2] - a1 * y[k-1] - a2 * y[k-2];
+ }
+
+ // If we need to filter more, but don't have any signal left,
+ // assume the signal is zero.
+ for (var k = signal.length; k < len; ++k) {
+ y[k] = - a1 * y[k-1] - a2 * y[k-2];
+ }
+
+ return y;
+}
+
+// Map the filter type name to a function that computes the filter coefficents for the given filter
+// type.
+var filterCreatorFunction = {"lowpass": createLowpassFilter,
+ "highpass": createHighpassFilter,
+ "bandpass": createBandpassFilter,
+ "lowshelf": createLowShelfFilter,
+ "highshelf": createHighShelfFilter,
+ "peaking": createPeakingFilter,
+ "notch": createNotchFilter,
+ "allpass": createAllpassFilter};
+
+var filterTypeName = {"lowpass": "Lowpass filter",
+ "highpass": "Highpass filter",
+ "bandpass": "Bandpass filter",
+ "lowshelf": "Lowshelf filter",
+ "highshelf": "Highshelf filter",
+ "peaking": "Peaking filter",
+ "notch": "Notch filter",
+ "allpass": "Allpass filter"};
+
+function createFilter(filterType, freq, q, gain) {
+ return filterCreatorFunction[filterType](freq, q, gain);
+}
diff --git a/dom/media/webaudio/test/blink/biquad-testing.js b/dom/media/webaudio/test/blink/biquad-testing.js
new file mode 100644
index 0000000000..795adf6012
--- /dev/null
+++ b/dom/media/webaudio/test/blink/biquad-testing.js
@@ -0,0 +1,153 @@
+// Globals, to make testing and debugging easier.
+var context;
+var filter;
+var signal;
+var renderedBuffer;
+var renderedData;
+
+var sampleRate = 44100.0;
+var pulseLengthFrames = .1 * sampleRate;
+
+// Maximum allowed error for the test to succeed. Experimentally determined.
+var maxAllowedError = 5.9e-8;
+
+// This must be large enough so that the filtered result is
+// essentially zero. See comments for createTestAndRun.
+var timeStep = .1;
+
+// Maximum number of filters we can process (mostly for setting the
+// render length correctly.)
+var maxFilters = 5;
+
+// How long to render. Must be long enough for all of the filters we
+// want to test.
+var renderLengthSeconds = timeStep * (maxFilters + 1) ;
+
+var renderLengthSamples = Math.round(renderLengthSeconds * sampleRate);
+
+// Number of filters that will be processed.
+var nFilters;
+
+function createImpulseBuffer(context, length) {
+ var impulse = context.createBuffer(1, length, context.sampleRate);
+ var data = impulse.getChannelData(0);
+ for (var k = 1; k < data.length; ++k) {
+ data[k] = 0;
+ }
+ data[0] = 1;
+
+ return impulse;
+}
+
+
+function createTestAndRun(context, filterType, filterParameters) {
+ // To test the filters, we apply a signal (an impulse) to each of
+ // the specified filters, with each signal starting at a different
+ // time. The output of the filters is summed together at the
+ // output. Thus for filter k, the signal input to the filter
+ // starts at time k * timeStep. For this to work well, timeStep
+ // must be large enough for the output of each filter to have
+ // decayed to zero with timeStep seconds. That way the filter
+ // outputs don't interfere with each other.
+
+ nFilters = Math.min(filterParameters.length, maxFilters);
+
+ signal = new Array(nFilters);
+ filter = new Array(nFilters);
+
+ impulse = createImpulseBuffer(context, pulseLengthFrames);
+
+ // Create all of the signal sources and filters that we need.
+ for (var k = 0; k < nFilters; ++k) {
+ signal[k] = context.createBufferSource();
+ signal[k].buffer = impulse;
+
+ filter[k] = context.createBiquadFilter();
+ filter[k].type = filterType;
+ filter[k].frequency.value = context.sampleRate / 2 * filterParameters[k].cutoff;
+ filter[k].detune.value = (filterParameters[k].detune === undefined) ? 0 : filterParameters[k].detune;
+ filter[k].Q.value = filterParameters[k].q;
+ filter[k].gain.value = filterParameters[k].gain;
+
+ signal[k].connect(filter[k]);
+ filter[k].connect(context.destination);
+
+ signal[k].start(timeStep * k);
+ }
+
+ context.oncomplete = checkFilterResponse(filterType, filterParameters);
+ context.startRendering();
+}
+
+function addSignal(dest, src, destOffset) {
+ // Add src to dest at the given dest offset.
+ for (var k = destOffset, j = 0; k < dest.length, j < src.length; ++k, ++j) {
+ dest[k] += src[j];
+ }
+}
+
+function generateReference(filterType, filterParameters) {
+ var result = new Array(renderLengthSamples);
+ var data = new Array(renderLengthSamples);
+ // Initialize the result array and data.
+ for (var k = 0; k < result.length; ++k) {
+ result[k] = 0;
+ data[k] = 0;
+ }
+ // Make data an impulse.
+ data[0] = 1;
+
+ for (var k = 0; k < nFilters; ++k) {
+ // Filter an impulse
+ var detune = (filterParameters[k].detune === undefined) ? 0 : filterParameters[k].detune;
+ var frequency = filterParameters[k].cutoff * Math.pow(2, detune / 1200); // Apply detune, converting from Cents.
+
+ var filterCoef = createFilter(filterType,
+ frequency,
+ filterParameters[k].q,
+ filterParameters[k].gain);
+ var y = filterData(filterCoef, data, renderLengthSamples);
+
+ // Accumulate this filtered data into the final output at the desired offset.
+ addSignal(result, y, timeToSampleFrame(timeStep * k, sampleRate));
+ }
+
+ return result;
+}
+
+function checkFilterResponse(filterType, filterParameters) {
+ return function(event) {
+ renderedBuffer = event.renderedBuffer;
+ renderedData = renderedBuffer.getChannelData(0);
+
+ reference = generateReference(filterType, filterParameters);
+
+ var len = Math.min(renderedData.length, reference.length);
+
+ var success = true;
+
+ // Maximum error between rendered data and expected data
+ var maxError = 0;
+
+ // Sample offset where the maximum error occurred.
+ var maxPosition = 0;
+
+ // Number of infinities or NaNs that occurred in the rendered data.
+ var invalidNumberCount = 0;
+
+ ok(nFilters == filterParameters.length, "Test wanted " + filterParameters.length + " filters but only " + maxFilters + " allowed.");
+
+ compareChannels(renderedData, reference, len, 0, 0, true);
+
+ // Check for bad numbers in the rendered output too.
+ // There shouldn't be any.
+ for (var k = 0; k < len; ++k) {
+ if (!isValidNumber(renderedData[k])) {
+ ++invalidNumberCount;
+ }
+ }
+
+ ok(invalidNumberCount == 0, "Rendered output has " + invalidNumberCount + " infinities or NaNs.");
+ SimpleTest.finish();
+ }
+}
diff --git a/dom/media/webaudio/test/blink/convolution-testing.js b/dom/media/webaudio/test/blink/convolution-testing.js
new file mode 100644
index 0000000000..98ff0c7756
--- /dev/null
+++ b/dom/media/webaudio/test/blink/convolution-testing.js
@@ -0,0 +1,182 @@
+var sampleRate = 44100.0;
+
+var renderLengthSeconds = 8;
+var pulseLengthSeconds = 1;
+var pulseLengthFrames = pulseLengthSeconds * sampleRate;
+
+function createSquarePulseBuffer(context, sampleFrameLength) {
+ var audioBuffer = context.createBuffer(1, sampleFrameLength, context.sampleRate);
+
+ var n = audioBuffer.length;
+ var data = audioBuffer.getChannelData(0);
+
+ for (var i = 0; i < n; ++i)
+ data[i] = 1;
+
+ return audioBuffer;
+}
+
+// The triangle buffer holds the expected result of the convolution.
+// It linearly ramps up from 0 to its maximum value (at the center)
+// then linearly ramps down to 0. The center value corresponds to the
+// point where the two square pulses overlap the most.
+function createTrianglePulseBuffer(context, sampleFrameLength) {
+ var audioBuffer = context.createBuffer(1, sampleFrameLength, context.sampleRate);
+
+ var n = audioBuffer.length;
+ var halfLength = n / 2;
+ var data = audioBuffer.getChannelData(0);
+
+ for (var i = 0; i < halfLength; ++i)
+ data[i] = i + 1;
+
+ for (var i = halfLength; i < n; ++i)
+ data[i] = n - i - 1;
+
+ return audioBuffer;
+}
+
+function log10(x) {
+ return Math.log(x)/Math.LN10;
+}
+
+function linearToDecibel(x) {
+ return 20*log10(x);
+}
+
+// Verify that the rendered result is very close to the reference
+// triangular pulse.
+function checkTriangularPulse(rendered, reference) {
+ var match = true;
+ var maxDelta = 0;
+ var valueAtMaxDelta = 0;
+ var maxDeltaIndex = 0;
+
+ for (var i = 0; i < reference.length; ++i) {
+ var diff = rendered[i] - reference[i];
+ var x = Math.abs(diff);
+ if (x > maxDelta) {
+ maxDelta = x;
+ valueAtMaxDelta = reference[i];
+ maxDeltaIndex = i;
+ }
+ }
+
+ // allowedDeviationFraction was determined experimentally. It
+ // is the threshold of the relative error at the maximum
+ // difference between the true triangular pulse and the
+ // rendered pulse.
+ var allowedDeviationDecibels = -129.4;
+ var maxDeviationDecibels = linearToDecibel(maxDelta / valueAtMaxDelta);
+
+ if (maxDeviationDecibels <= allowedDeviationDecibels) {
+ testPassed("Triangular portion of convolution is correct.");
+ } else {
+ testFailed("Triangular portion of convolution is not correct. Max deviation = " + maxDeviationDecibels + " dB at " + maxDeltaIndex);
+ match = false;
+ }
+
+ return match;
+}
+
+// Verify that the rendered data is close to zero for the first part
+// of the tail.
+function checkTail1(data, reference, breakpoint) {
+ var isZero = true;
+ var tail1Max = 0;
+
+ for (var i = reference.length; i < reference.length + breakpoint; ++i) {
+ var mag = Math.abs(data[i]);
+ if (mag > tail1Max) {
+ tail1Max = mag;
+ }
+ }
+
+ // Let's find the peak of the reference (even though we know a
+ // priori what it is).
+ var refMax = 0;
+ for (var i = 0; i < reference.length; ++i) {
+ refMax = Math.max(refMax, Math.abs(reference[i]));
+ }
+
+ // This threshold is experimentally determined by examining the
+ // value of tail1MaxDecibels.
+ var threshold1 = -129.7;
+
+ var tail1MaxDecibels = linearToDecibel(tail1Max/refMax);
+ if (tail1MaxDecibels <= threshold1) {
+ testPassed("First part of tail of convolution is sufficiently small.");
+ } else {
+ testFailed("First part of tail of convolution is not sufficiently small: " + tail1MaxDecibels + " dB");
+ isZero = false;
+ }
+
+ return isZero;
+}
+
+// Verify that the second part of the tail of the convolution is
+// exactly zero.
+function checkTail2(data, reference, breakpoint) {
+ var isZero = true;
+ var tail2Max = 0;
+ // For the second part of the tail, the maximum value should be
+ // exactly zero.
+ var threshold2 = 0;
+ for (var i = reference.length + breakpoint; i < data.length; ++i) {
+ if (Math.abs(data[i]) > 0) {
+ isZero = false;
+ break;
+ }
+ }
+
+ if (isZero) {
+ testPassed("Rendered signal after tail of convolution is silent.");
+ } else {
+ testFailed("Rendered signal after tail of convolution should be silent.");
+ }
+
+ return isZero;
+}
+
+function checkConvolvedResult(trianglePulse) {
+ return function(event) {
+ var renderedBuffer = event.renderedBuffer;
+
+ var referenceData = trianglePulse.getChannelData(0);
+ var renderedData = renderedBuffer.getChannelData(0);
+
+ var success = true;
+
+ // Verify the triangular pulse is actually triangular.
+
+ success = success && checkTriangularPulse(renderedData, referenceData);
+
+ // Make sure that portion after convolved portion is totally
+ // silent. But round-off prevents this from being completely
+ // true. At the end of the triangle, it should be close to
+ // zero. If we go farther out, it should be even closer and
+ // eventually zero.
+
+ // For the tail of the convolution (where the result would be
+ // theoretically zero), we partition the tail into two
+ // parts. The first is the at the beginning of the tail,
+ // where we tolerate a small but non-zero value. The second part is
+ // farther along the tail where the result should be zero.
+
+ // breakpoint is the point dividing the first two tail parts
+ // we're looking at. Experimentally determined.
+ var breakpoint = 12800;
+
+ success = success && checkTail1(renderedData, referenceData, breakpoint);
+
+ success = success && checkTail2(renderedData, referenceData, breakpoint);
+
+ if (success) {
+ testPassed("Test signal was correctly convolved.");
+ } else {
+ testFailed("Test signal was not correctly convolved.");
+ }
+
+ finishJSTest();
+ }
+}
diff --git a/dom/media/webaudio/test/blink/mochitest.ini b/dom/media/webaudio/test/blink/mochitest.ini
new file mode 100644
index 0000000000..8d115b2e8e
--- /dev/null
+++ b/dom/media/webaudio/test/blink/mochitest.ini
@@ -0,0 +1,22 @@
+[DEFAULT]
+tags = mtg webaudio
+subsuite = media
+support-files =
+ biquad-filters.js
+ biquad-testing.js
+ ../webaudio.js
+
+[test_biquadFilterNodeAllPass.html]
+[test_biquadFilterNodeAutomation.html]
+skip-if = true # Known problems with Biquad automation, e.g. Bug 1155709
+[test_biquadFilterNodeBandPass.html]
+[test_biquadFilterNodeGetFrequencyResponse.html]
+[test_biquadFilterNodeHighPass.html]
+[test_biquadFilterNodeHighShelf.html]
+[test_biquadFilterNodeLowPass.html]
+[test_biquadFilterNodeLowShelf.html]
+[test_biquadFilterNodeNotch.html]
+[test_biquadFilterNodePeaking.html]
+[test_biquadFilterNodeTail.html]
+[test_iirFilterNode.html]
+[test_iirFilterNodeGetFrequencyResponse.html]
diff --git a/dom/media/webaudio/test/blink/panner-model-testing.js b/dom/media/webaudio/test/blink/panner-model-testing.js
new file mode 100644
index 0000000000..45460e2768
--- /dev/null
+++ b/dom/media/webaudio/test/blink/panner-model-testing.js
@@ -0,0 +1,210 @@
+var sampleRate = 48000.0;
+
+var numberOfChannels = 1;
+
+// Time step when each panner node starts.
+var timeStep = 0.001;
+
+// Length of the impulse signal.
+var pulseLengthFrames = Math.round(timeStep * sampleRate);
+
+// How many panner nodes to create for the test
+var nodesToCreate = 100;
+
+// Be sure we render long enough for all of our nodes.
+var renderLengthSeconds = timeStep * (nodesToCreate + 1);
+
+// These are global mostly for debugging.
+var context;
+var impulse;
+var bufferSource;
+var panner;
+var position;
+var time;
+
+var renderedBuffer;
+var renderedLeft;
+var renderedRight;
+
+function createGraph(context, nodeCount) {
+ bufferSource = new Array(nodeCount);
+ panner = new Array(nodeCount);
+ position = new Array(nodeCount);
+ time = new Array(nodeCount);
+ // Angle between panner locations. (nodeCount - 1 because we want
+ // to include both 0 and 180 deg.
+ var angleStep = Math.PI / (nodeCount - 1);
+
+ if (numberOfChannels == 2) {
+ impulse = createStereoImpulseBuffer(context, pulseLengthFrames);
+ }
+ else
+ impulse = createImpulseBuffer(context, pulseLengthFrames);
+
+ for (var k = 0; k < nodeCount; ++k) {
+ bufferSource[k] = context.createBufferSource();
+ bufferSource[k].buffer = impulse;
+
+ panner[k] = context.createPanner();
+ panner[k].panningModel = "equalpower";
+ panner[k].distanceModel = "linear";
+
+ var angle = angleStep * k;
+ position[k] = {angle : angle, x : Math.cos(angle), z : Math.sin(angle)};
+ panner[k].positionX.value = position[k].x;
+ panner[k].positionZ.value = position[k].z;
+
+ bufferSource[k].connect(panner[k]);
+ panner[k].connect(context.destination);
+
+ // Start the source
+ time[k] = k * timeStep;
+ bufferSource[k].start(time[k]);
+ }
+}
+
+function createTestAndRun(context, nodeCount, numberOfSourceChannels) {
+ numberOfChannels = numberOfSourceChannels;
+
+ createGraph(context, nodeCount);
+
+ context.oncomplete = checkResult;
+ context.startRendering();
+}
+
+// Map our position angle to the azimuth angle (in degrees).
+//
+// An angle of 0 corresponds to an azimuth of 90 deg; pi, to -90 deg.
+function angleToAzimuth(angle) {
+ return 90 - angle * 180 / Math.PI;
+}
+
+// The gain caused by the EQUALPOWER panning model
+function equalPowerGain(angle) {
+ var azimuth = angleToAzimuth(angle);
+
+ if (numberOfChannels == 1) {
+ var panPosition = (azimuth + 90) / 180;
+
+ var gainL = Math.cos(0.5 * Math.PI * panPosition);
+ var gainR = Math.sin(0.5 * Math.PI * panPosition);
+
+ return { left : gainL, right : gainR };
+ } else {
+ if (azimuth <= 0) {
+ var panPosition = (azimuth + 90) / 90;
+
+ var gainL = 1 + Math.cos(0.5 * Math.PI * panPosition);
+ var gainR = Math.sin(0.5 * Math.PI * panPosition);
+
+ return { left : gainL, right : gainR };
+ } else {
+ var panPosition = azimuth / 90;
+
+ var gainL = Math.cos(0.5 * Math.PI * panPosition);
+ var gainR = 1 + Math.sin(0.5 * Math.PI * panPosition);
+
+ return { left : gainL, right : gainR };
+ }
+ }
+}
+
+function checkResult(event) {
+ renderedBuffer = event.renderedBuffer;
+ renderedLeft = renderedBuffer.getChannelData(0);
+ renderedRight = renderedBuffer.getChannelData(1);
+
+ // The max error we allow between the rendered impulse and the
+ // expected value. This value is experimentally determined. Set
+ // to 0 to make the test fail to see what the actual error is.
+ var maxAllowedError = 1.3e-6;
+
+ var success = true;
+
+ // Number of impulses found in the rendered result.
+ var impulseCount = 0;
+
+ // Max (relative) error and the index of the maxima for the left
+ // and right channels.
+ var maxErrorL = 0;
+ var maxErrorIndexL = 0;
+ var maxErrorR = 0;
+ var maxErrorIndexR = 0;
+
+ // Number of impulses that don't match our expected locations.
+ var timeCount = 0;
+
+ // Locations of where the impulses aren't at the expected locations.
+ var timeErrors = new Array();
+
+ for (var k = 0; k < renderedLeft.length; ++k) {
+ // We assume that the left and right channels start at the same instant.
+ if (renderedLeft[k] != 0 || renderedRight[k] != 0) {
+ // The expected gain for the left and right channels.
+ var pannerGain = equalPowerGain(position[impulseCount].angle);
+ var expectedL = pannerGain.left;
+ var expectedR = pannerGain.right;
+
+ // Absolute error in the gain.
+ var errorL = Math.abs(renderedLeft[k] - expectedL);
+ var errorR = Math.abs(renderedRight[k] - expectedR);
+
+ if (Math.abs(errorL) > maxErrorL) {
+ maxErrorL = Math.abs(errorL);
+ maxErrorIndexL = impulseCount;
+ }
+ if (Math.abs(errorR) > maxErrorR) {
+ maxErrorR = Math.abs(errorR);
+ maxErrorIndexR = impulseCount;
+ }
+
+ // Keep track of the impulses that didn't show up where we
+ // expected them to be.
+ var expectedOffset = timeToSampleFrame(time[impulseCount], sampleRate);
+ if (k != expectedOffset) {
+ timeErrors[timeCount] = { actual : k, expected : expectedOffset};
+ ++timeCount;
+ }
+ ++impulseCount;
+ }
+ }
+
+ if (impulseCount == nodesToCreate) {
+ testPassed("Number of impulses matches the number of panner nodes.");
+ } else {
+ testFailed("Number of impulses is incorrect. (Found " + impulseCount + " but expected " + nodesToCreate + ")");
+ success = false;
+ }
+
+ if (timeErrors.length > 0) {
+ success = false;
+ testFailed(timeErrors.length + " timing errors found in " + nodesToCreate + " panner nodes.");
+ for (var k = 0; k < timeErrors.length; ++k) {
+ testFailed("Impulse at sample " + timeErrors[k].actual + " but expected " + timeErrors[k].expected);
+ }
+ } else {
+ testPassed("All impulses at expected offsets.");
+ }
+
+ if (maxErrorL <= maxAllowedError) {
+ testPassed("Left channel gain values are correct.");
+ } else {
+ testFailed("Left channel gain values are incorrect. Max error = " + maxErrorL + " at time " + time[maxErrorIndexL] + " (threshold = " + maxAllowedError + ")");
+ success = false;
+ }
+
+ if (maxErrorR <= maxAllowedError) {
+ testPassed("Right channel gain values are correct.");
+ } else {
+ testFailed("Right channel gain values are incorrect. Max error = " + maxErrorR + " at time " + time[maxErrorIndexR] + " (threshold = " + maxAllowedError + ")");
+ success = false;
+ }
+
+ if (success) {
+ testPassed("EqualPower panner test passed");
+ } else {
+ testFailed("EqualPower panner test failed");
+ }
+
+ finishJSTest();
+}
diff --git a/dom/media/webaudio/test/blink/test_biquadFilterNodeAllPass.html b/dom/media/webaudio/test/blink/test_biquadFilterNodeAllPass.html
new file mode 100644
index 0000000000..024c2f50df
--- /dev/null
+++ b/dom/media/webaudio/test/blink/test_biquadFilterNodeAllPass.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test BiquadFilterNode All Pass Filter</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script src="audio-testing.js"></script>
+<script src="biquad-filters.js"></script>
+<script src="biquad-testing.js"></script>
+<script src="webaudio.js" type="text/javascript"></script>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(function() {
+ // Create offline audio context.
+ var context = new OfflineAudioContext(2, sampleRate * renderLengthSeconds, sampleRate);
+
+ var filterParameters = [{cutoff : 0, q : 10, gain : 1 },
+ {cutoff : 1, q : 10, gain : 1 },
+ {cutoff : .5, q : 0, gain : 1 },
+ {cutoff : 0.25, q : 10, gain : 1 },
+ ];
+ createTestAndRun(context, "allpass", filterParameters);
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/blink/test_biquadFilterNodeAutomation.html b/dom/media/webaudio/test/blink/test_biquadFilterNodeAutomation.html
new file mode 100644
index 0000000000..5a71ce46e5
--- /dev/null
+++ b/dom/media/webaudio/test/blink/test_biquadFilterNodeAutomation.html
@@ -0,0 +1,351 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test BiquadFilterNode All Pass Filter</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script src="audio-testing.js"></script>
+<script src="biquad-filters.js"></script>
+<script src="biquad-testing.js"></script>
+<script src="webaudio.js" type="text/javascript"></script>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(function() {
+ // Don't need to run these tests at high sampling rate, so just use a low one to reduce memory
+ // usage and complexity.
+ var sampleRate = 16000;
+
+ // How long to render for each test.
+ var renderDuration = 1;
+
+ // The definition of the linear ramp automation function.
+ function linearRamp(t, v0, v1, t0, t1) {
+ return v0 + (v1 - v0) * (t - t0) / (t1 - t0);
+ }
+
+ // Generate the filter coefficients for the specified filter using the given parameters for
+ // the given duration. |filterTypeFunction| is a function that returns the filter
+ // coefficients for one set of parameters. |parameters| is a property bag that contains the
+ // start and end values (as an array) for each of the biquad attributes. The properties are
+ // |freq|, |Q|, |gain|, and |detune|. |duration| is the number of seconds for which the
+ // coefficients are generated.
+ //
+ // A property bag with properties |b0|, |b1|, |b2|, |a1|, |a2|. Each propery is an array
+ // consisting of the coefficients for the time-varying biquad filter.
+ function generateFilterCoefficients(filterTypeFunction, parameters, duration) {
+ var endFrame = Math.ceil(duration * sampleRate);
+ var nCoef = endFrame;
+ var b0 = new Float64Array(nCoef);
+ var b1 = new Float64Array(nCoef);
+ var b2 = new Float64Array(nCoef);
+ var a1 = new Float64Array(nCoef);
+ var a2 = new Float64Array(nCoef);
+
+ var k = 0;
+ // If the property is not given, use the defaults.
+ var freqs = parameters.freq || [350, 350];
+ var qs = parameters.Q || [1, 1];
+ var gains = parameters.gain || [0, 0];
+ var detunes = parameters.detune || [0, 0];
+
+ for (var frame = 0; frame < endFrame; ++frame) {
+ // Apply linear ramp at frame |frame|.
+ var f = linearRamp(frame / sampleRate, freqs[0], freqs[1], 0, duration);
+ var q = linearRamp(frame / sampleRate, qs[0], qs[1], 0, duration);
+ var g = linearRamp(frame / sampleRate, gains[0], gains[1], 0, duration);
+ var d = linearRamp(frame / sampleRate, detunes[0], detunes[1], 0, duration);
+
+ // Compute actual frequency parameter
+ f = f * Math.pow(2, d / 1200);
+
+ // Compute filter coefficients
+ var coef = filterTypeFunction(f / (sampleRate / 2), q, g);
+ b0[k] = coef.b0;
+ b1[k] = coef.b1;
+ b2[k] = coef.b2;
+ a1[k] = coef.a1;
+ a2[k] = coef.a2;
+ ++k;
+ }
+
+ return {b0: b0, b1: b1, b2: b2, a1: a1, a2: a2};
+ }
+
+ // Apply the given time-varying biquad filter to the given signal, |signal|. |coef| should be
+ // the time-varying coefficients of the filter, as returned by |generateFilterCoefficients|.
+ function timeVaryingFilter(signal, coef) {
+ var length = signal.length;
+ // Use double precision for the internal computations.
+ var y = new Float64Array(length);
+
+ // Prime the pump. (Assumes the signal has length >= 2!)
+ y[0] = coef.b0[0] * signal[0];
+ y[1] = coef.b0[1] * signal[1] + coef.b1[1] * signal[0] - coef.a1[1] * y[0];
+
+ for (var n = 2; n < length; ++n) {
+ y[n] = coef.b0[n] * signal[n] + coef.b1[n] * signal[n-1] + coef.b2[n] * signal[n-2];
+ y[n] -= coef.a1[n] * y[n-1] + coef.a2[n] * y[n-2];
+ }
+
+ // But convert the result to single precision for comparison.
+ return y.map(Math.fround);
+ }
+
+ // Configure the audio graph using |context|. Returns the biquad filter node and the
+ // AudioBuffer used for the source.
+ function configureGraph(context, toneFrequency) {
+ // The source is just a simple sine wave.
+ var src = context.createBufferSource();
+ var b = context.createBuffer(1, renderDuration * sampleRate, sampleRate);
+ var data = b.getChannelData(0);
+ var omega = 2 * Math.PI * toneFrequency / sampleRate;
+ for (var k = 0; k < data.length; ++k) {
+ data[k] = Math.sin(omega * k);
+ }
+ src.buffer = b;
+ var f = context.createBiquadFilter();
+ src.connect(f);
+ f.connect(context.destination);
+
+ src.start();
+
+ return {filter: f, source: b};
+ }
+
+ function createFilterVerifier(filterCreator, threshold, parameters, input, message) {
+ return function (resultBuffer) {
+ var actual = resultBuffer.getChannelData(0);
+ var coefs = generateFilterCoefficients(filterCreator, parameters, renderDuration);
+
+ reference = timeVaryingFilter(input, coefs);
+
+ compareChannels(actual, reference);
+ };
+ }
+
+ var testPromises = [];
+
+ // Automate just the frequency parameter. A bandpass filter is used where the center
+ // frequency is swept across the source (which is a simple tone).
+ testPromises.push(function () {
+ var context = new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);
+
+ // Center frequency of bandpass filter and also the frequency of the test tone.
+ var centerFreq = 10*440;
+
+ // Sweep the frequency +/- 9*440 Hz from the center. This should cause the output to low at
+ // the beginning and end of the test where the done is outside the pass band of the filter,
+ // but high in the center where the tone is near the center of the pass band.
+ var parameters = {
+ freq: [centerFreq - 9*440, centerFreq + 9*440]
+ }
+ var graph = configureGraph(context, centerFreq);
+ var f = graph.filter;
+ var b = graph.source;
+
+ f.type = "bandpass";
+ f.frequency.setValueAtTime(parameters.freq[0], 0);
+ f.frequency.linearRampToValueAtTime(parameters.freq[1], renderDuration);
+
+ return context.startRendering()
+ .then(createFilterVerifier(createBandpassFilter, 5e-5, parameters, b.getChannelData(0),
+ "Output of bandpass filter with frequency automation"));
+ }());
+
+ // Automate just the Q parameter. A bandpass filter is used where the Q of the filter is
+ // swept.
+ testPromises.push(function() {
+ var context = new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);
+
+ // The frequency of the test tone.
+ var centerFreq = 440;
+
+ // Sweep the Q paramter between 1 and 200. This will cause the output of the filter to pass
+ // most of the tone at the beginning to passing less of the tone at the end. This is
+ // because we set center frequency of the bandpass filter to be slightly off from the actual
+ // tone.
+ var parameters = {
+ Q: [1, 200],
+ // Center frequency of the bandpass filter is just 25 Hz above the tone frequency.
+ freq: [centerFreq + 25, centerFreq + 25]
+ };
+ var graph = configureGraph(context, centerFreq);
+ var f = graph.filter;
+ var b = graph.source;
+
+ f.type = "bandpass";
+ f.frequency.value = parameters.freq[0];
+ f.Q.setValueAtTime(parameters.Q[0], 0);
+ f.Q.linearRampToValueAtTime(parameters.Q[1], renderDuration);
+
+ return context.startRendering()
+ .then(createFilterVerifier(createBandpassFilter, 1.4e-6, parameters, b.getChannelData(0),
+ "Output of bandpass filter with Q automation"));
+ }());
+
+ // Automate just the gain of the lowshelf filter. A test tone will be in the lowshelf part of
+ // the filter. The output will vary as the gain of the lowshelf is changed.
+ testPromises.push(function() {
+ var context = new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);
+
+ // Frequency of the test tone.
+ var centerFreq = 440;
+
+ // Set the cutoff frequency of the lowshelf to be significantly higher than the test tone.
+ // Sweep the gain from 20 dB to -20 dB. (We go from 20 to -20 to easily verify that the
+ // filter didn't go unstable.)
+ var parameters = {
+ freq: [3500, 3500],
+ gain: [20, -20]
+ }
+ var graph = configureGraph(context, centerFreq);
+ var f = graph.filter;
+ var b = graph.source;
+
+ f.type = "lowshelf";
+ f.frequency.value = parameters.freq[0];
+ f.gain.setValueAtTime(parameters.gain[0], 0);
+ f.gain.linearRampToValueAtTime(parameters.gain[1], renderDuration);
+
+ context.startRendering()
+ .then(createFilterVerifier(createLowShelfFilter, 8e-6, parameters, b.getChannelData(0),
+ "Output of lowshelf filter with gain automation"));
+ }());
+
+ // Automate just the detune parameter. Basically the same test as for the frequncy parameter
+ // but we just use the detune parameter to modulate the frequency parameter.
+ testPromises.push(function() {
+ var context = new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);
+ var centerFreq = 10*440;
+ var parameters = {
+ freq: [centerFreq, centerFreq],
+ detune: [-10*1200, 10*1200]
+ };
+ var graph = configureGraph(context, centerFreq);
+ var f = graph.filter;
+ var b = graph.source;
+
+ f.type = "bandpass";
+ f.frequency.value = parameters.freq[0];
+ f.detune.setValueAtTime(parameters.detune[0], 0);
+ f.detune.linearRampToValueAtTime(parameters.detune[1], renderDuration);
+
+ context.startRendering()
+ .then(createFilterVerifier(createBandpassFilter, 5e-6, parameters, b.getChannelData(0),
+ "Output of bandpass filter with detune automation"));
+ }());
+
+ // Automate all of the filter parameters at once. This is a basic check that everything is
+ // working. A peaking filter is used because it uses all of the parameters.
+ testPromises.push(function() {
+ var context = new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);
+ var graph = configureGraph(context, 10*440);
+ var f = graph.filter;
+ var b = graph.source;
+
+ // Sweep all of the filter parameters. These are pretty much arbitrary.
+ var parameters = {
+ freq: [10000, 100],
+ Q: [f.Q.value, .0001],
+ gain: [f.gain.value, 20],
+ detune: [2400, -2400]
+ };
+
+ f.type = "peaking";
+ // Set starting points for all parameters of the filter. Start at 10 kHz for the center
+ // frequency, and the defaults for Q and gain.
+ f.frequency.setValueAtTime(parameters.freq[0], 0);
+ f.Q.setValueAtTime(parameters.Q[0], 0);
+ f.gain.setValueAtTime(parameters.gain[0], 0);
+ f.detune.setValueAtTime(parameters.detune[0], 0);
+
+ // Linear ramp each parameter
+ f.frequency.linearRampToValueAtTime(parameters.freq[1], renderDuration);
+ f.Q.linearRampToValueAtTime(parameters.Q[1], renderDuration);
+ f.gain.linearRampToValueAtTime(parameters.gain[1], renderDuration);
+ f.detune.linearRampToValueAtTime(parameters.detune[1], renderDuration);
+
+ context.startRendering()
+ .then(createFilterVerifier(createPeakingFilter, 3.3e-4, parameters, b.getChannelData(0),
+ "Output of peaking filter with automation of all parameters"));
+ }());
+
+ // Test that modulation of the frequency parameter of the filter works. A sinusoid of 440 Hz
+ // is the test signal that is applied to a bandpass biquad filter. The frequency parameter of
+ // the filter is modulated by a sinusoid at 103 Hz, and the frequency modulation varies from
+ // 116 to 412 Hz. (This test was taken from the description in
+ // https://github.com/WebAudio/web-audio-api/issues/509#issuecomment-94731355)
+ testPromises.push(function() {
+ var context = new OfflineAudioContext(1, renderDuration * sampleRate, sampleRate);
+
+ // Create a graph with the sinusoidal source at 440 Hz as the input to a biquad filter.
+ var graph = configureGraph(context, 440);
+ var f = graph.filter;
+ var b = graph.source;
+
+ f.type = "bandpass";
+ f.Q.value = 5;
+ f.frequency.value = 264;
+
+ // Create the modulation source, a sinusoid with frequency 103 Hz and amplitude 148. (The
+ // amplitude of 148 is added to the filter's frequency value of 264 to produce a sinusoidal
+ // modulation of the frequency parameter from 116 to 412 Hz.)
+ var mod = context.createBufferSource();
+ var mbuffer = context.createBuffer(1, renderDuration * sampleRate, sampleRate);
+ var d = mbuffer.getChannelData(0);
+ var omega = 2 * Math.PI * 103 / sampleRate;
+ for (var k = 0; k < d.length; ++k) {
+ d[k] = 148 * Math.sin(omega * k);
+ }
+ mod.buffer = mbuffer;
+
+ mod.connect(f.frequency);
+
+ mod.start();
+ return context.startRendering()
+ .then(function (resultBuffer) {
+ var actual = resultBuffer.getChannelData(0);
+ // Compute the filter coefficients using the mod sine wave
+
+ var endFrame = Math.ceil(renderDuration * sampleRate);
+ var nCoef = endFrame;
+ var b0 = new Float64Array(nCoef);
+ var b1 = new Float64Array(nCoef);
+ var b2 = new Float64Array(nCoef);
+ var a1 = new Float64Array(nCoef);
+ var a2 = new Float64Array(nCoef);
+
+ // Generate the filter coefficients when the frequency varies from 116 to 248 Hz using
+ // the 103 Hz sinusoid.
+ for (var k = 0; k < nCoef; ++k) {
+ var freq = f.frequency.value + d[k];
+ var c = createBandpassFilter(freq / (sampleRate / 2), f.Q.value, f.gain.value);
+ b0[k] = c.b0;
+ b1[k] = c.b1;
+ b2[k] = c.b2;
+ a1[k] = c.a1;
+ a2[k] = c.a2;
+ }
+ reference = timeVaryingFilter(b.getChannelData(0),
+ {b0: b0, b1: b1, b2: b2, a1: a1, a2: a2});
+
+ compareChannels(actual, reference);
+ });
+ }());
+
+ // Wait for all tests
+ Promise.all(testPromises).then(function () {
+ SimpleTest.finish();
+ }, function () {
+ SimpleTest.finish();
+ });
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/blink/test_biquadFilterNodeBandPass.html b/dom/media/webaudio/test/blink/test_biquadFilterNodeBandPass.html
new file mode 100644
index 0000000000..05c4c3a265
--- /dev/null
+++ b/dom/media/webaudio/test/blink/test_biquadFilterNodeBandPass.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test BiquadFilterNode Band Pass Filter</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script src="audio-testing.js"></script>
+<script src="biquad-filters.js"></script>
+<script src="biquad-testing.js"></script>
+<script src="webaudio.js" type="text/javascript"></script>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(function() {
+ // Create offline audio context.
+ var context = new OfflineAudioContext(2, sampleRate * renderLengthSeconds, sampleRate);
+
+ // The filters we want to test.
+ var filterParameters = [{cutoff : 0, q : 0, gain : 1 },
+ {cutoff : 1, q : 0, gain : 1 },
+ {cutoff : 0.5, q : 0, gain : 1 },
+ {cutoff : 0.25, q : 1, gain : 1 },
+ ];
+
+ createTestAndRun(context, "bandpass", filterParameters);
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/blink/test_biquadFilterNodeGetFrequencyResponse.html b/dom/media/webaudio/test/blink/test_biquadFilterNodeGetFrequencyResponse.html
new file mode 100644
index 0000000000..c2b6612034
--- /dev/null
+++ b/dom/media/webaudio/test/blink/test_biquadFilterNodeGetFrequencyResponse.html
@@ -0,0 +1,261 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test BiquadFilterNode All Pass Filter</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script src="audio-testing.js"></script>
+<script src="biquad-filters.js"></script>
+<script src="biquad-testing.js"></script>
+<script src="webaudio.js" type="text/javascript"></script>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(function() {
+// Test the frequency response of a biquad filter. We compute the frequency response for a simple
+// peaking biquad filter and compare it with the expected frequency response. The actual filter
+// used doesn't matter since we're testing getFrequencyResponse and not the actual filter output.
+// The filters are extensively tested in other biquad tests.
+
+var context;
+
+// The biquad filter node.
+var filter;
+
+// The magnitude response of the biquad filter.
+var magResponse;
+
+// The phase response of the biquad filter.
+var phaseResponse;
+
+// Number of frequency samples to take.
+var numberOfFrequencies = 1000;
+
+// The filter parameters.
+var filterCutoff = 1000; // Hz.
+var filterQ = 1;
+var filterGain = 5; // Decibels.
+
+// The maximum allowed error in the magnitude response.
+var maxAllowedMagError = 5.7e-7;
+
+// The maximum allowed error in the phase response.
+var maxAllowedPhaseError = 4.7e-8;
+
+// The magnitudes and phases of the reference frequency response.
+var magResponse;
+var phaseResponse;
+
+// The magnitudes and phases of the reference frequency response.
+var expectedMagnitudes;
+var expectedPhases;
+
+// Convert frequency in Hz to a normalized frequency between 0 to 1 with 1 corresponding to the
+// Nyquist frequency.
+function normalizedFrequency(freqHz, sampleRate)
+{
+ var nyquist = sampleRate / 2;
+ return freqHz / nyquist;
+}
+
+// Get the filter response at a (normalized) frequency |f| for the filter with coefficients |coef|.
+function getResponseAt(coef, f)
+{
+ var b0 = coef.b0;
+ var b1 = coef.b1;
+ var b2 = coef.b2;
+ var a1 = coef.a1;
+ var a2 = coef.a2;
+
+ // H(z) = (b0 + b1 / z + b2 / z^2) / (1 + a1 / z + a2 / z^2)
+ //
+ // Compute H(exp(i * pi * f)). No native complex numbers in javascript, so break H(exp(i * pi * // f))
+ // in to the real and imaginary parts of the numerator and denominator. Let omega = pi * f.
+ // Then the numerator is
+ //
+ // b0 + b1 * cos(omega) + b2 * cos(2 * omega) - i * (b1 * sin(omega) + b2 * sin(2 * omega))
+ //
+ // and the denominator is
+ //
+ // 1 + a1 * cos(omega) + a2 * cos(2 * omega) - i * (a1 * sin(omega) + a2 * sin(2 * omega))
+ //
+ // Compute the magnitude and phase from the real and imaginary parts.
+
+ var omega = Math.PI * f;
+ var numeratorReal = b0 + b1 * Math.cos(omega) + b2 * Math.cos(2 * omega);
+ var numeratorImag = -(b1 * Math.sin(omega) + b2 * Math.sin(2 * omega));
+ var denominatorReal = 1 + a1 * Math.cos(omega) + a2 * Math.cos(2 * omega);
+ var denominatorImag = -(a1 * Math.sin(omega) + a2 * Math.sin(2 * omega));
+
+ var magnitude = Math.sqrt((numeratorReal * numeratorReal + numeratorImag * numeratorImag)
+ / (denominatorReal * denominatorReal + denominatorImag * denominatorImag));
+ var phase = Math.atan2(numeratorImag, numeratorReal) - Math.atan2(denominatorImag, denominatorReal);
+
+ if (phase >= Math.PI) {
+ phase -= 2 * Math.PI;
+ } else if (phase <= -Math.PI) {
+ phase += 2 * Math.PI;
+ }
+
+ return {magnitude : magnitude, phase : phase};
+}
+
+// Compute the reference frequency response for the biquad filter |filter| at the frequency samples
+// given by |frequencies|.
+function frequencyResponseReference(filter, frequencies)
+{
+ var sampleRate = filter.context.sampleRate;
+ var normalizedFreq = normalizedFrequency(filter.frequency.value, sampleRate);
+ var filterCoefficients = createFilter(filter.type, normalizedFreq, filter.Q.value, filter.gain.value);
+
+ var magnitudes = [];
+ var phases = [];
+
+ for (var k = 0; k < frequencies.length; ++k) {
+ var response = getResponseAt(filterCoefficients, normalizedFrequency(frequencies[k], sampleRate));
+ magnitudes.push(response.magnitude);
+ phases.push(response.phase);
+ }
+
+ return {magnitudes : magnitudes, phases : phases};
+}
+
+// Compute a set of linearly spaced frequencies.
+function createFrequencies(nFrequencies, sampleRate)
+{
+ var frequencies = new Float32Array(nFrequencies);
+ var nyquist = sampleRate / 2;
+ var freqDelta = nyquist / nFrequencies;
+
+ for (var k = 0; k < nFrequencies; ++k) {
+ frequencies[k] = k * freqDelta;
+ }
+
+ return frequencies;
+}
+
+function linearToDecibels(x)
+{
+ if (x) {
+ return 20 * Math.log(x) / Math.LN10;
+ } else {
+ return -1000;
+ }
+}
+
+// Look through the array and find any NaN or infinity. Returns the index of the first occurence or
+// -1 if none.
+function findBadNumber(signal)
+{
+ for (var k = 0; k < signal.length; ++k) {
+ if (!isValidNumber(signal[k])) {
+ return k;
+ }
+ }
+ return -1;
+}
+
+// Compute absolute value of the difference between phase angles, taking into account the wrapping
+// of phases.
+function absolutePhaseDifference(x, y)
+{
+ var diff = Math.abs(x - y);
+
+ if (diff > Math.PI) {
+ diff = 2 * Math.PI - diff;
+ }
+ return diff;
+}
+
+// Compare the frequency response with our expected response.
+function compareResponses(filter, frequencies, magResponse, phaseResponse)
+{
+ var expectedResponse = frequencyResponseReference(filter, frequencies);
+
+ expectedMagnitudes = expectedResponse.magnitudes;
+ expectedPhases = expectedResponse.phases;
+
+ var n = magResponse.length;
+ var success = true;
+ var badResponse = false;
+
+ var maxMagError = -1;
+ var maxMagErrorIndex = -1;
+
+ var k;
+ var hasBadNumber;
+
+ hasBadNumber = findBadNumber(magResponse);
+ ok (hasBadNumber < 0, "Magnitude response has NaN or infinity at " + hasBadNumber);
+
+ hasBadNumber = findBadNumber(phaseResponse);
+ ok (hasBadNumber < 0, "Phase response has NaN or infinity at " + hasBadNumber);
+
+ // These aren't testing the implementation itself. Instead, these are sanity checks on the
+ // reference. Failure here does not imply an error in the implementation.
+ hasBadNumber = findBadNumber(expectedMagnitudes);
+ ok (hasBadNumber < 0, "Expected magnitude response has NaN or infinity at " + hasBadNumber);
+
+ hasBadNumber = findBadNumber(expectedPhases);
+ ok (hasBadNumber < 0, "Expected phase response has NaN or infinity at " + hasBadNumber);
+
+ for (k = 0; k < n; ++k) {
+ var error = Math.abs(linearToDecibels(magResponse[k]) - linearToDecibels(expectedMagnitudes[k]));
+ if (error > maxMagError) {
+ maxMagError = error;
+ maxMagErrorIndex = k;
+ }
+ }
+
+ var message = "Magnitude error (" + maxMagError + " dB)";
+ message += " exceeded threshold at " + frequencies[maxMagErrorIndex];
+ message += " Hz. Actual: " + linearToDecibels(magResponse[maxMagErrorIndex]);
+ message += " dB, expected: " + linearToDecibels(expectedMagnitudes[maxMagErrorIndex]) + " dB.";
+ ok(maxMagError < maxAllowedMagError, message);
+
+ var maxPhaseError = -1;
+ var maxPhaseErrorIndex = -1;
+
+ for (k = 0; k < n; ++k) {
+ var error = absolutePhaseDifference(phaseResponse[k], expectedPhases[k]);
+ if (error > maxPhaseError) {
+ maxPhaseError = error;
+ maxPhaseErrorIndex = k;
+ }
+ }
+
+ message = "Phase error (radians) (" + maxPhaseError;
+ message += ") exceeded threshold at " + frequencies[maxPhaseErrorIndex];
+ message += " Hz. Actual: " + phaseResponse[maxPhaseErrorIndex];
+ message += " expected: " + expectedPhases[maxPhaseErrorIndex];
+
+ ok(maxPhaseError < maxAllowedPhaseError, message);
+}
+
+context = new AudioContext();
+
+filter = context.createBiquadFilter();
+
+// Arbitrarily test a peaking filter, but any kind of filter can be tested.
+filter.type = "peaking";
+filter.frequency.value = filterCutoff;
+filter.Q.value = filterQ;
+filter.gain.value = filterGain;
+
+var frequencies = createFrequencies(numberOfFrequencies, context.sampleRate);
+magResponse = new Float32Array(numberOfFrequencies);
+phaseResponse = new Float32Array(numberOfFrequencies);
+
+filter.getFrequencyResponse(frequencies, magResponse, phaseResponse);
+compareResponses(filter, frequencies, magResponse, phaseResponse);
+
+SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/blink/test_biquadFilterNodeHighPass.html b/dom/media/webaudio/test/blink/test_biquadFilterNodeHighPass.html
new file mode 100644
index 0000000000..a615574c2d
--- /dev/null
+++ b/dom/media/webaudio/test/blink/test_biquadFilterNodeHighPass.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test BiquadFilterNode High Pass Filter</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script src="audio-testing.js"></script>
+<script src="biquad-filters.js"></script>
+<script src="biquad-testing.js"></script>
+<script src="webaudio.js" type="text/javascript"></script>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(function() {
+ // Create offline audio context.
+ var context = new OfflineAudioContext(2, sampleRate * renderLengthSeconds, sampleRate);
+
+ // The filters we want to test.
+ var filterParameters = [{cutoff : 0, q : 1, gain : 1 },
+ {cutoff : 1, q : 1, gain : 1 },
+ {cutoff : 0.25, q : 1, gain : 1 },
+ ];
+
+ createTestAndRun(context, "highpass", filterParameters);
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/blink/test_biquadFilterNodeHighShelf.html b/dom/media/webaudio/test/blink/test_biquadFilterNodeHighShelf.html
new file mode 100644
index 0000000000..c8f6815930
--- /dev/null
+++ b/dom/media/webaudio/test/blink/test_biquadFilterNodeHighShelf.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test BiquadFilterNode High Shelf Filter</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script src="audio-testing.js"></script>
+<script src="biquad-filters.js"></script>
+<script src="biquad-testing.js"></script>
+<script src="webaudio.js" type="text/javascript"></script>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(function() {
+ // Create offline audio context.
+ var context = new OfflineAudioContext(2, sampleRate * renderLengthSeconds, sampleRate);
+
+ // The filters we want to test.
+ var filterParameters = [{cutoff : 0, q : 10, gain : 10 },
+ {cutoff : 1, q : 10, gain : 10 },
+ {cutoff : 0.25, q : 10, gain : 10 },
+ ];
+
+ createTestAndRun(context, "highshelf", filterParameters);
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/blink/test_biquadFilterNodeLowPass.html b/dom/media/webaudio/test/blink/test_biquadFilterNodeLowPass.html
new file mode 100644
index 0000000000..dcea18551a
--- /dev/null
+++ b/dom/media/webaudio/test/blink/test_biquadFilterNodeLowPass.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test BiquadFilterNode Low Pass Filter</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script src="audio-testing.js"></script>
+<script src="biquad-filters.js"></script>
+<script src="biquad-testing.js"></script>
+<script src="webaudio.js" type="text/javascript"></script>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(function() {
+ // Create offline audio context.
+ var context = new OfflineAudioContext(2, sampleRate * renderLengthSeconds, sampleRate);
+
+ // The filters we want to test.
+ var filterParameters = [{cutoff : 0, q : 1, gain : 1 },
+ {cutoff : 1, q : 1, gain : 1 },
+ {cutoff : 0.25, q : 1, gain : 1 },
+ {cutoff : 0.25, q : 1, gain : 1, detune : 100 },
+ {cutoff : 0.01, q : 1, gain : 1, detune : -200 },
+ ];
+ createTestAndRun(context, "lowpass", filterParameters);
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/blink/test_biquadFilterNodeLowShelf.html b/dom/media/webaudio/test/blink/test_biquadFilterNodeLowShelf.html
new file mode 100644
index 0000000000..c1349f8e7e
--- /dev/null
+++ b/dom/media/webaudio/test/blink/test_biquadFilterNodeLowShelf.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test BiquadFilterNode Low Shelf Filter</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script src="audio-testing.js"></script>
+<script src="biquad-filters.js"></script>
+<script src="biquad-testing.js"></script>
+<script src="webaudio.js" type="text/javascript"></script>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(function() {
+
+ // Create offline audio context.
+ var context = new OfflineAudioContext(2, sampleRate * renderLengthSeconds, sampleRate);
+
+ // The filters we want to test.
+ var filterParameters = [{cutoff : 0, q : 10, gain : 10 },
+ {cutoff : 1, q : 10, gain : 10 },
+ {cutoff : 0.25, q : 10, gain : 10 },
+ ];
+
+ createTestAndRun(context, "lowshelf", filterParameters);
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/blink/test_biquadFilterNodeNotch.html b/dom/media/webaudio/test/blink/test_biquadFilterNodeNotch.html
new file mode 100644
index 0000000000..0fcbc5546e
--- /dev/null
+++ b/dom/media/webaudio/test/blink/test_biquadFilterNodeNotch.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test BiquadFilterNode Notch Filter</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script src="audio-testing.js"></script>
+<script src="biquad-filters.js"></script>
+<script src="biquad-testing.js"></script>
+<script src="webaudio.js" type="text/javascript"></script>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(function() {
+ // Create offline audio context.
+ var context = new OfflineAudioContext(2, sampleRate * renderLengthSeconds, sampleRate);
+
+ var filterParameters = [{cutoff : 0, q : 10, gain : 1 },
+ {cutoff : 1, q : 10, gain : 1 },
+ {cutoff : .5, q : 0, gain : 1 },
+ {cutoff : 0.25, q : 10, gain : 1 },
+ ];
+
+ createTestAndRun(context, "notch", filterParameters);
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/blink/test_biquadFilterNodePeaking.html b/dom/media/webaudio/test/blink/test_biquadFilterNodePeaking.html
new file mode 100644
index 0000000000..8e4727a37e
--- /dev/null
+++ b/dom/media/webaudio/test/blink/test_biquadFilterNodePeaking.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test BiquadFilterNode Low Pass Filter</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script src="audio-testing.js"></script>
+<script src="biquad-filters.js"></script>
+<script src="biquad-testing.js"></script>
+<script src="webaudio.js" type="text/javascript"></script>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(function() {
+ // Create offline audio context.
+ var context = new OfflineAudioContext(2, sampleRate * renderLengthSeconds, sampleRate);
+
+ // The filters we want to test.
+ var filterParameters = [{cutoff : 0, q : 10, gain : 10 },
+ {cutoff : 1, q : 10, gain : 10 },
+ {cutoff : .5, q : 0, gain : 10 },
+ {cutoff : 0.25, q : 10, gain : 10 },
+ ];
+
+ createTestAndRun(context, "peaking", filterParameters);
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/blink/test_biquadFilterNodeTail.html b/dom/media/webaudio/test/blink/test_biquadFilterNodeTail.html
new file mode 100644
index 0000000000..cc170df2a5
--- /dev/null
+++ b/dom/media/webaudio/test/blink/test_biquadFilterNodeTail.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test BiquadFilterNode All Pass Filter</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script src="audio-testing.js"></script>
+<script src="biquad-filters.js"></script>
+<script src="biquad-testing.js"></script>
+<script src="webaudio.js" type="text/javascript"></script>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(function() {
+ // A high sample rate shows the issue more clearly.
+ var sampleRate = 192000;
+ // Some short duration because we don't need to run the test for very long.
+ var testDurationSec = 0.5;
+ var testDurationFrames = testDurationSec * sampleRate;
+
+ // Amplitude experimentally determined to give a biquad output close to 1. (No attempt was
+ // made to produce exactly 1; it's not needed.)
+ var sourceAmplitude = 100;
+
+ // The output of the biquad filter should not change by more than this much between output
+ // samples. Threshold was determined experimentally.
+ var glitchThreshold = 0.01292;
+
+ // Test that a Biquad filter doesn't have it's output terminated because the input has gone
+ // away. Generally, when a source node is finished, it disconnects itself from any downstream
+ // nodes. This is the correct behavior. Nodes that have no inputs (disconnected) are
+ // generally assumed to output zeroes. This is also desired behavior. However, biquad
+ // filters have memory so they should not suddenly output zeroes when the input is
+ // disconnected. This test checks to see if the output doesn't suddenly change to zero.
+ var context = new OfflineAudioContext(1, testDurationFrames, sampleRate);
+
+ // Create an impulse source.
+ var buffer = context.createBuffer(1, 1, context.sampleRate);
+ buffer.getChannelData(0)[0] = sourceAmplitude;
+ var source = context.createBufferSource();
+ source.buffer = buffer;
+
+ // Create the biquad filter. It doesn't really matter what kind, so the default filter type
+ // and parameters is fine. Connect the source to it.
+ var biquad = context.createBiquadFilter();
+ source.connect(biquad);
+ biquad.connect(context.destination);
+
+ source.start();
+
+ context.startRendering().then(function(result) {
+ // There should be no large discontinuities in the output
+ var buffer = result.getChannelData(0);
+ var maxGlitchIndex = 0;
+ var maxGlitchValue = 0.0;
+ for (var i = 1; i < buffer.length; i++) {
+ var diff = Math.abs(buffer[i-1] - buffer[i]);
+ if (diff >= glitchThreshold) {
+ if (diff > maxGlitchValue) {
+ maxGlitchIndex = i;
+ maxGlitchValue = diff;
+ }
+ }
+ }
+ ok(maxGlitchIndex == 0, 'glitches detected in biquad output: maximum glitch at ' + maxGlitchIndex + ' with diff of ' + maxGlitchValue);
+ SimpleTest.finish();
+ })
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/blink/test_iirFilterNode.html b/dom/media/webaudio/test/blink/test_iirFilterNode.html
new file mode 100644
index 0000000000..0ef7a37e3b
--- /dev/null
+++ b/dom/media/webaudio/test/blink/test_iirFilterNode.html
@@ -0,0 +1,467 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test IIRFilterNode GetFrequencyResponse</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <script type="text/javascript" src="biquad-filters.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(function() {
+ var sampleRate = 48000;
+ var testDurationSec = 1;
+ var testFrames = testDurationSec * sampleRate;
+
+ var testPromises = []
+ testPromises.push(function () {
+ // Test that the feedback coefficients are normalized. Do this be creating two
+ // IIRFilterNodes. One has normalized coefficients, and one doesn't. Compute the
+ // difference and make sure they're the same.
+ var context = new OfflineAudioContext(2, testFrames, sampleRate);
+
+ // Use a simple impulse as the source.
+ var buffer = context.createBuffer(1, 1, sampleRate);
+ buffer.getChannelData(0)[0] = 1;
+ var source = context.createBufferSource();
+ source.buffer = buffer;
+
+ // Gain node for computing the difference between the filters.
+ var gain = context.createGain();
+ gain.gain.value = -1;
+
+ // The IIR filters. Use a common feedforward array.
+ var ff = [1];
+
+ var fb1 = [1, .9];
+
+ var fb2 = new Float64Array(2);
+ // Scale the feedback coefficients by an arbitrary factor.
+ var coefScaleFactor = 2;
+ for (var k = 0; k < fb2.length; ++k) {
+ fb2[k] = coefScaleFactor * fb1[k];
+ }
+
+ var iir1 = context.createIIRFilter(ff, fb1);
+ var iir2 = context.createIIRFilter(ff, fb2);
+
+ // Create the graph. The output of iir1 (normalized coefficients) is channel 0, and the
+ // output of iir2 (unnormalized coefficients), with appropriate scaling, is channel 1.
+ var merger = context.createChannelMerger(2);
+ source.connect(iir1);
+ source.connect(iir2);
+ iir1.connect(merger, 0, 0);
+ iir2.connect(gain);
+
+ // The gain for the gain node should be set to compensate for the scaling of the
+ // coefficients. Since iir2 has scaled the coefficients by coefScaleFactor, the output is
+ // reduced by the same factor, so adjust the gain to scale the output of iir2 back up.
+ gain.gain.value = coefScaleFactor;
+ gain.connect(merger, 0, 1);
+
+ merger.connect(context.destination);
+
+ source.start();
+
+ // Rock and roll!
+
+ return context.startRendering().then(function (result) {
+ // Find the max amplitude of the result, which should be near zero.
+ var iir1Data = result.getChannelData(0);
+ var iir2Data = result.getChannelData(1);
+
+ // Threshold isn't exactly zero because the arithmetic is done differently between the
+ // IIRFilterNode and the BiquadFilterNode.
+ compareChannels(iir1Data, iir2Data);
+ });
+ }());
+
+ testPromises.push(function () {
+ // Create a simple 1-zero filter and compare with the expected output.
+ var context = new OfflineAudioContext(1, testFrames, sampleRate);
+
+ // Use a simple impulse as the source
+ var buffer = context.createBuffer(1, 1, sampleRate);
+ buffer.getChannelData(0)[0] = 1;
+ var source = context.createBufferSource();
+ source.buffer = buffer;
+
+ // The filter is y(n) = 0.5*(x(n) + x(n-1)), a simple 2-point moving average. This is
+ // rather arbitrary; keep it simple.
+
+ var iir = context.createIIRFilter([0.5, 0.5], [1]);
+
+ // Create the graph
+ source.connect(iir);
+ iir.connect(context.destination);
+
+ // Rock and roll!
+ source.start();
+
+ return context.startRendering().then(function (result) {
+ var actual = result.getChannelData(0);
+ var expected = new Float64Array(testFrames);
+ // The filter is a simple 2-point moving average of an impulse, so the first two values
+ // are non-zero and the rest are zero.
+ expected[0] = 0.5;
+ expected[1] = 0.5;
+ compareChannels(actual, expected);
+ });
+ }());
+
+ testPromises.push(function () {
+ // Create a simple 1-pole filter and compare with the expected output.
+
+ // The filter is y(n) + c*y(n-1)= x(n). The analytical response is (-c)^n, so choose a
+ // suitable number of frames to run the test for where the output isn't flushed to zero.
+ var c = 0.9;
+ var eps = 1e-20;
+ var duration = Math.floor(Math.log(eps) / Math.log(Math.abs(c)));
+ var context = new OfflineAudioContext(1, duration, sampleRate);
+
+ // Use a simple impulse as the source
+ var buffer = context.createBuffer(1, 1, sampleRate);
+ buffer.getChannelData(0)[0] = 1;
+ var source = context.createBufferSource();
+ source.buffer = buffer;
+
+ var iir = context.createIIRFilter([1], [1, c]);
+
+ // Create the graph
+ source.connect(iir);
+ iir.connect(context.destination);
+
+ // Rock and roll!
+ source.start();
+
+ return context.startRendering().then(function (result) {
+ var actual = result.getChannelData(0);
+ var expected = new Float64Array(actual.length);
+
+ // The filter is a simple 1-pole filter: y(n) = -c*y(n-k)+x(n), with an impulse as the
+ // input.
+ expected[0] = 1;
+ for (k = 1; k < testFrames; ++k) {
+ expected[k] = -c * expected[k-1];
+ }
+
+ compareChannels(actual, expected);
+ });
+ }());
+
+ // This function creates an IIRFilterNode equivalent to the specified
+ // BiquadFilterNode and compares the outputs. The
+ // outputs from the two filters should be virtually identical.
+ function testWithBiquadFilter(filterType) {
+ var context = new OfflineAudioContext(2, testFrames, sampleRate);
+
+ // Use a constant (step function) as the source
+ var buffer = context.createBuffer(1, testFrames, context.sampleRate);
+ for (var i = 0; i < testFrames; ++i) {
+ buffer.getChannelData(0)[i] = 1;
+ }
+ var source = context.createBufferSource();
+ source.buffer = buffer;
+
+ // Create the biquad. Choose some rather arbitrary values for Q and gain for the biquad
+ // so that the shelf filters aren't identical.
+ var biquad = context.createBiquadFilter();
+ biquad.type = filterType;
+ biquad.Q.value = 10;
+ biquad.gain.value = 10;
+
+ // Create the equivalent IIR Filter node by computing the coefficients of the given biquad
+ // filter type.
+ var nyquist = sampleRate / 2;
+ var coef = createFilter(filterType,
+ biquad.frequency.value / nyquist,
+ biquad.Q.value,
+ biquad.gain.value);
+
+ var iir = context.createIIRFilter([coef.b0, coef.b1, coef.b2], [1, coef.a1, coef.a2]);
+
+ var merger = context.createChannelMerger(2);
+ // Create the graph
+ source.connect(biquad);
+ source.connect(iir);
+
+ biquad.connect(merger, 0, 0);
+ iir.connect(merger, 0, 1);
+
+ merger.connect(context.destination);
+
+ // Rock and roll!
+ source.start();
+
+ return context.startRendering().then(function (result) {
+ // Find the max amplitude of the result, which should be near zero.
+ var expected = result.getChannelData(0);
+ var actual = result.getChannelData(1);
+ compareChannels(actual, expected);
+ });
+ }
+
+ biquadFilterTypes = ["lowpass", "highpass", "bandpass", "notch",
+ "allpass", "lowshelf", "highshelf", "peaking"];
+
+ // Create a set of tasks based on biquadTestConfigs.
+ for (var i = 0; i < biquadFilterTypes.length; ++i) {
+ testPromises.push(testWithBiquadFilter(biquadFilterTypes[i]));
+ }
+
+ testPromises.push(function () {
+ // Multi-channel test. Create a biquad filter and the equivalent IIR filter. Filter the
+ // same multichannel signal and compare the results.
+ var nChannels = 3;
+ var context = new OfflineAudioContext(nChannels, testFrames, sampleRate);
+
+ // Create a set of oscillators as the multi-channel source.
+ var source = [];
+
+ for (k = 0; k < nChannels; ++k) {
+ source[k] = context.createOscillator();
+ source[k].type = "sawtooth";
+ // The frequency of the oscillator is pretty arbitrary, but each oscillator should have a
+ // different frequency.
+ source[k].frequency.value = 100 + k * 100;
+ }
+
+ var merger = context.createChannelMerger(3);
+
+ var biquad = context.createBiquadFilter();
+
+ // Create the equivalent IIR Filter node.
+ var nyquist = sampleRate / 2;
+ var coef = createFilter(biquad.type,
+ biquad.frequency.value / nyquist,
+ biquad.Q.value,
+ biquad.gain.value);
+ var fb = [1, coef.a1, coef.a2];
+ var ff = [coef.b0, coef.b1, coef.b2];
+
+ var iir = context.createIIRFilter(ff, fb);
+ // Gain node to compute the difference between the IIR and biquad filter.
+ var gain = context.createGain();
+ gain.gain.value = -1;
+
+ // Create the graph.
+ for (k = 0; k < nChannels; ++k)
+ source[k].connect(merger, 0, k);
+
+ merger.connect(biquad);
+ merger.connect(iir);
+ iir.connect(gain);
+ biquad.connect(context.destination);
+ gain.connect(context.destination);
+
+ for (k = 0; k < nChannels; ++k)
+ source[k].start();
+
+ return context.startRendering().then(function (result) {
+ var errorThresholds = [3.7671e-5, 3.0071e-5, 2.6241e-5];
+
+ // Check the difference signal on each channel
+ for (channel = 0; channel < result.numberOfChannels; ++channel) {
+ // Find the max amplitude of the result, which should be near zero.
+ var data = result.getChannelData(channel);
+ var maxError = data.reduce(function(reducedValue, currentValue) {
+ return Math.max(reducedValue, Math.abs(currentValue));
+ });
+
+ ok(maxError <= errorThresholds[channel], "Max difference between IIR and Biquad on channel " + channel);
+ }
+ });
+ }());
+
+ testPromises.push(function () {
+ // Apply an IIRFilter to the given input signal.
+ //
+ // IIR filter in the time domain is
+ //
+ // y[n] = sum(ff[k]*x[n-k], k, 0, M) - sum(fb[k]*y[n-k], k, 1, N)
+ //
+ function iirFilter(input, feedforward, feedback) {
+ // For simplicity, create an x buffer that contains the input, and a y buffer that contains
+ // the output. Both of these buffers have an initial work space to implement the initial
+ // memory of the filter.
+ var workSize = Math.max(feedforward.length, feedback.length);
+ var x = new Float32Array(input.length + workSize);
+
+ // Float64 because we want to match the implementation that uses doubles to minimize
+ // roundoff.
+ var y = new Float64Array(input.length + workSize);
+
+ // Copy the input over.
+ for (var k = 0; k < input.length; ++k)
+ x[k + feedforward.length] = input[k];
+
+ // Run the filter
+ for (var n = 0; n < input.length; ++n) {
+ var index = n + workSize;
+ var yn = 0;
+ for (var k = 0; k < feedforward.length; ++k)
+ yn += feedforward[k] * x[index - k];
+ for (var k = 0; k < feedback.length; ++k)
+ yn -= feedback[k] * y[index - k];
+
+ y[index] = yn;
+ }
+
+ return y.slice(workSize).map(Math.fround);
+ }
+
+ // Cascade the two given biquad filters to create one IIR filter.
+ function cascadeBiquads(f1Coef, f2Coef) {
+ // The biquad filters are:
+ //
+ // f1 = (b10 + b11/z + b12/z^2)/(1 + a11/z + a12/z^2);
+ // f2 = (b20 + b21/z + b22/z^2)/(1 + a21/z + a22/z^2);
+ //
+ // To cascade them, multiply the two transforms together to get a fourth order IIR filter.
+
+ var numProduct = [f1Coef.b0 * f2Coef.b0,
+ f1Coef.b0 * f2Coef.b1 + f1Coef.b1 * f2Coef.b0,
+ f1Coef.b0 * f2Coef.b2 + f1Coef.b1 * f2Coef.b1 + f1Coef.b2 * f2Coef.b0,
+ f1Coef.b1 * f2Coef.b2 + f1Coef.b2 * f2Coef.b1,
+ f1Coef.b2 * f2Coef.b2
+ ];
+
+ var denProduct = [1,
+ f2Coef.a1 + f1Coef.a1,
+ f2Coef.a2 + f1Coef.a1 * f2Coef.a1 + f1Coef.a2,
+ f1Coef.a1 * f2Coef.a2 + f1Coef.a2 * f2Coef.a1,
+ f1Coef.a2 * f2Coef.a2
+ ];
+
+ return {
+ ff: numProduct,
+ fb: denProduct
+ }
+ }
+
+ // Find the magnitude of the root of the quadratic that has the maximum magnitude.
+ //
+ // The quadratic is z^2 + a1 * z + a2 and we want the root z that has the largest magnitude.
+ function largestRootMagnitude(a1, a2) {
+ var discriminant = a1 * a1 - 4 * a2;
+ if (discriminant < 0) {
+ // Complex roots: -a1/2 +/- i*sqrt(-d)/2. Thus the magnitude of each root is the same
+ // and is sqrt(a1^2/4 + |d|/4)
+ var d = Math.sqrt(-discriminant);
+ return Math.hypot(a1 / 2, d / 2);
+ } else {
+ // Real roots
+ var d = Math.sqrt(discriminant);
+ return Math.max(Math.abs((-a1 + d) / 2), Math.abs((-a1 - d) / 2));
+ }
+ }
+
+ // Cascade 2 lowpass biquad filters and compare that with the equivalent 4th order IIR
+ // filter.
+
+ var nyquist = sampleRate / 2;
+ // Compute the coefficients of a lowpass filter.
+
+ // First some preliminary stuff. Compute the coefficients of the biquad. This is used to
+ // figure out how frames to use in the test.
+ var biquadType = "lowpass";
+ var biquadCutoff = 350;
+ var biquadQ = 5;
+ var biquadGain = 1;
+
+ var coef = createFilter(biquadType,
+ biquadCutoff / nyquist,
+ biquadQ,
+ biquadGain);
+
+ // Cascade the biquads together to create an equivalent IIR filter.
+ var cascade = cascadeBiquads(coef, coef);
+
+ // Since we're cascading two identical biquads, the root of denominator of the IIR filter is
+ // repeated, so the root of the denominator with the largest magnitude occurs twice. The
+ // impulse response of the IIR filter will be roughly c*(r*r)^n at time n, where r is the
+ // root of largest magnitude. This approximation gets better as n increases. We can use
+ // this to get a rough idea of when the response has died down to a small value.
+
+ // This is the value we will use to determine how many frames to render. Rendering too many
+ // is a waste of time and also makes it hard to compare the actual result to the expected
+ // because the magnitudes are so small that they could be mostly round-off noise.
+ //
+ // Find magnitude of the root with largest magnitude
+ var rootMagnitude = largestRootMagnitude(coef.a1, coef.a2);
+
+ // Find n such that |r|^(2*n) <= eps. That is, n = log(eps)/(2*log(r)). Somewhat
+ // arbitrarily choose eps = 1e-20;
+ var eps = 1e-20;
+ var framesForTest = Math.floor(Math.log(eps) / (2 * Math.log(rootMagnitude)));
+
+ // We're ready to create the graph for the test. The offline context has two channels:
+ // channel 0 is the expected (cascaded biquad) result and channel 1 is the actual IIR filter
+ // result.
+ var context = new OfflineAudioContext(2, framesForTest, sampleRate);
+
+ // Use a simple impulse with a large (arbitrary) amplitude as the source
+ var amplitude = 1;
+ var buffer = context.createBuffer(1, testFrames, sampleRate);
+ buffer.getChannelData(0)[0] = amplitude;
+ var source = context.createBufferSource();
+ source.buffer = buffer;
+
+ // Create the two biquad filters. Doesn't really matter what, but for simplicity we choose
+ // identical lowpass filters with the same parameters.
+ var biquad1 = context.createBiquadFilter();
+ biquad1.type = biquadType;
+ biquad1.frequency.value = biquadCutoff;
+ biquad1.Q.value = biquadQ;
+
+ var biquad2 = context.createBiquadFilter();
+ biquad2.type = biquadType;
+ biquad2.frequency.value = biquadCutoff;
+ biquad2.Q.value = biquadQ;
+
+ var iir = context.createIIRFilter(cascade.ff, cascade.fb);
+
+ // Create the merger to get the signals into multiple channels
+ var merger = context.createChannelMerger(2);
+
+ // Create the graph, filtering the source through two biquads.
+ source.connect(biquad1);
+ biquad1.connect(biquad2);
+ biquad2.connect(merger, 0, 0);
+
+ source.connect(iir);
+ iir.connect(merger, 0, 1);
+
+ merger.connect(context.destination);
+
+ // Now filter the source through the IIR filter.
+ var y = iirFilter(buffer.getChannelData(0), cascade.ff, cascade.fb);
+
+ // Rock and roll!
+ source.start();
+
+ return context.startRendering().then(function(result) {
+ var expected = result.getChannelData(0);
+ var actual = result.getChannelData(1);
+
+ compareChannels(actual, expected);
+
+ });
+ }());
+
+ // Wait for all tests
+ Promise.all(testPromises).then(function () {
+ SimpleTest.finish();
+ }, function () {
+ SimpleTest.finish();
+ });
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/blink/test_iirFilterNodeGetFrequencyResponse.html b/dom/media/webaudio/test/blink/test_iirFilterNodeGetFrequencyResponse.html
new file mode 100644
index 0000000000..09782f73be
--- /dev/null
+++ b/dom/media/webaudio/test/blink/test_iirFilterNodeGetFrequencyResponse.html
@@ -0,0 +1,97 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test IIRFilterNode GetFrequencyResponse</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <script type="text/javascript" src="biquad-filters.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ // Modified from WebKit/LayoutTests/webaudio/iirfilter-getFrequencyResponse.html
+ var sampleRate = 48000;
+ var testDurationSec = 0.01;
+
+ // Compute a set of linearly spaced frequencies.
+ function createFrequencies(nFrequencies, sampleRate)
+ {
+ var frequencies = new Float32Array(nFrequencies);
+ var nyquist = sampleRate / 2;
+ var freqDelta = nyquist / nFrequencies;
+
+ for (var k = 0; k < nFrequencies; ++k) {
+ frequencies[k] = k * freqDelta;
+ }
+
+ return frequencies;
+ }
+
+ // Number of frequency samples to take.
+ var numberOfFrequencies = 1000;
+
+ var context = new OfflineAudioContext(1, testDurationSec * sampleRate, sampleRate);
+
+ var frequencies = createFrequencies(numberOfFrequencies, context.sampleRate);
+
+ // 1-Pole IIR Filter
+ var iir = context.createIIRFilter([1], [1, -0.9]);
+
+ var iirMag = new Float32Array(numberOfFrequencies);
+ var iirPhase = new Float32Array(numberOfFrequencies);
+ var trueMag = new Float32Array(numberOfFrequencies);
+ var truePhase = new Float32Array(numberOfFrequencies);
+
+ // The IIR filter is
+ // H(z) = 1/(1 - 0.9*z^(-1)).
+ //
+ // The frequency response is
+ // H(exp(j*w)) = 1/(1 - 0.9*exp(-j*w)).
+ //
+ // Thus, the magnitude is
+ // |H(exp(j*w))| = 1/sqrt(1.81-1.8*cos(w)).
+ //
+ // The phase is
+ // arg(H(exp(j*w)) = atan(0.9*sin(w)/(.9*cos(w)-1))
+
+ var frequencyScale = Math.PI / (sampleRate / 2);
+
+ for (var k = 0; k < frequencies.length; ++k) {
+ var omega = frequencyScale * frequencies[k];
+ trueMag[k] = 1/Math.sqrt(1.81-1.8*Math.cos(omega));
+ truePhase[k] = Math.atan(0.9 * Math.sin(omega) / (0.9 * Math.cos(omega) - 1));
+ }
+
+ iir.getFrequencyResponse(frequencies, iirMag, iirPhase);
+ compareChannels(iirMag, trueMag);
+ compareChannels(iirPhase, truePhase);
+
+ // Compare IIR and Biquad Filter
+ // Create an IIR filter equivalent to the biquad filter. Compute the frequency response for both and verify that they are the same.
+ var biquad = context.createBiquadFilter();
+ var coef = createFilter(biquad.type,
+ biquad.frequency.value / (context.sampleRate / 2),
+ biquad.Q.value,
+ biquad.gain.value);
+
+ var iir = context.createIIRFilter([coef.b0, coef.b1, coef.b2], [1, coef.a1, coef.a2]);
+
+ var biquadMag = new Float32Array(numberOfFrequencies);
+ var biquadPhase = new Float32Array(numberOfFrequencies);
+ var iirMag = new Float32Array(numberOfFrequencies);
+ var iirPhase = new Float32Array(numberOfFrequencies);
+
+ biquad.getFrequencyResponse(frequencies, biquadMag, biquadPhase);
+ iir.getFrequencyResponse(frequencies, iirMag, iirPhase);
+ compareChannels(iirMag, biquadMag);
+ compareChannels(iirPhase, biquadPhase);
+
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/corsServer.sjs b/dom/media/webaudio/test/corsServer.sjs
new file mode 100644
index 0000000000..45c694cf08
--- /dev/null
+++ b/dom/media/webaudio/test/corsServer.sjs
@@ -0,0 +1,26 @@
+function handleRequest(request, response) {
+ var file = Components.classes["@mozilla.org/file/directory_service;1"]
+ .getService(Components.interfaces.nsIProperties)
+ .get("CurWorkD", Components.interfaces.nsIFile);
+ var fis = Components.classes[
+ "@mozilla.org/network/file-input-stream;1"
+ ].createInstance(Components.interfaces.nsIFileInputStream);
+ var bis = Components.classes[
+ "@mozilla.org/binaryinputstream;1"
+ ].createInstance(Components.interfaces.nsIBinaryInputStream);
+ var paths = "tests/dom/media/webaudio/test/small-shot.ogg";
+ var split = paths.split("/");
+ for (var i = 0; i < split.length; ++i) {
+ file.append(split[i]);
+ }
+ fis.init(file, -1, -1, false);
+ bis.setInputStream(fis);
+ var bytes = bis.readBytes(bis.available());
+ response.setHeader("Content-Type", "video/ogg", false);
+ response.setHeader("Content-Length", "" + bytes.length, false);
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ response.write(bytes, bytes.length);
+ response.processAsync();
+ response.finish();
+ bis.close();
+}
diff --git a/dom/media/webaudio/test/file_nodeCreationDocumentGone.html b/dom/media/webaudio/test/file_nodeCreationDocumentGone.html
new file mode 100644
index 0000000000..aedf16702f
--- /dev/null
+++ b/dom/media/webaudio/test/file_nodeCreationDocumentGone.html
@@ -0,0 +1,4 @@
+<!DOCTYPE html>
+<html><script>
+var context = new AudioContext();
+setTimeout(function(){window.close();},1000);</script></html>
diff --git a/dom/media/webaudio/test/generate-test-files.py b/dom/media/webaudio/test/generate-test-files.py
new file mode 100755
index 0000000000..af4bc3bb49
--- /dev/null
+++ b/dom/media/webaudio/test/generate-test-files.py
@@ -0,0 +1,52 @@
+#!/usr/bin/env python3
+
+import os
+
+rates = [44100, 48000]
+channels = [1, 2]
+duration = "0.5"
+frequency = "1000"
+volume = "-3dB"
+name = "half-a-second"
+formats = {
+ "aac-in-adts": [{"codec": "aac", "extension": "aac"}],
+ "mp3": [{"codec": "libmp3lame", "extension": "mp3"}],
+ "mp4": [
+ {
+ "codec": "libopus",
+ "extension": "mp4",
+ },
+ {"codec": "aac", "extension": "mp4"},
+ ],
+ "ogg": [
+ {"codec": "libvorbis", "extension": "ogg"},
+ {"codec": "libopus", "extension": "opus"},
+ ],
+ "flac": [
+ {"codec": "flac", "extension": "flac"},
+ ],
+ "webm": [
+ {"codec": "libopus", "extension": "webm"},
+ {"codec": "libvorbis", "extension": "webm"},
+ ],
+}
+
+for rate in rates:
+ for channel_count in channels:
+ wav_filename = "{}-{}ch-{}.wav".format(name, channel_count, rate)
+ wav_command = "sox -V -r {} -n -b 16 -c {} {} synth {} sin {} vol {}".format(
+ rate, channel_count, wav_filename, duration, frequency, volume
+ )
+ print(wav_command)
+ os.system(wav_command)
+ for container, codecs in formats.items():
+ for codec in codecs:
+ encoded_filename = "{}-{}ch-{}-{}.{}".format(
+ name, channel_count, rate, codec["codec"], codec["extension"]
+ )
+ print(encoded_filename)
+ encoded_command = "ffmpeg -y -i {} -acodec {} {}".format(
+ wav_filename, codec["codec"], encoded_filename
+ )
+ print(encoded_command)
+ os.system(encoded_command)
diff --git a/dom/media/webaudio/test/half-a-second-1ch-44100-aac-afconvert.mp4 b/dom/media/webaudio/test/half-a-second-1ch-44100-aac-afconvert.mp4
new file mode 100644
index 0000000000..7e3b008376
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-1ch-44100-aac-afconvert.mp4
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-1ch-44100-aac.aac b/dom/media/webaudio/test/half-a-second-1ch-44100-aac.aac
new file mode 100644
index 0000000000..28435589ae
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-1ch-44100-aac.aac
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-1ch-44100-aac.mp4 b/dom/media/webaudio/test/half-a-second-1ch-44100-aac.mp4
new file mode 100644
index 0000000000..c603635f6e
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-1ch-44100-aac.mp4
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-1ch-44100-flac.flac b/dom/media/webaudio/test/half-a-second-1ch-44100-flac.flac
new file mode 100644
index 0000000000..49f71674f5
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-1ch-44100-flac.flac
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-1ch-44100-libmp3lame.mp3 b/dom/media/webaudio/test/half-a-second-1ch-44100-libmp3lame.mp3
new file mode 100644
index 0000000000..fb1ec45af2
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-1ch-44100-libmp3lame.mp3
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-1ch-44100-libopus.mp4 b/dom/media/webaudio/test/half-a-second-1ch-44100-libopus.mp4
new file mode 100644
index 0000000000..3a7df17582
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-1ch-44100-libopus.mp4
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-1ch-44100-libopus.opus b/dom/media/webaudio/test/half-a-second-1ch-44100-libopus.opus
new file mode 100644
index 0000000000..304b9f9d1d
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-1ch-44100-libopus.opus
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-1ch-44100-libopus.webm b/dom/media/webaudio/test/half-a-second-1ch-44100-libopus.webm
new file mode 100644
index 0000000000..71be30de9c
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-1ch-44100-libopus.webm
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-1ch-44100-libvorbis.ogg b/dom/media/webaudio/test/half-a-second-1ch-44100-libvorbis.ogg
new file mode 100644
index 0000000000..ab5ec06e50
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-1ch-44100-libvorbis.ogg
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-1ch-44100-libvorbis.webm b/dom/media/webaudio/test/half-a-second-1ch-44100-libvorbis.webm
new file mode 100644
index 0000000000..b5142703ba
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-1ch-44100-libvorbis.webm
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-1ch-44100.wav b/dom/media/webaudio/test/half-a-second-1ch-44100.wav
new file mode 100644
index 0000000000..0028a66007
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-1ch-44100.wav
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-1ch-48000-aac.aac b/dom/media/webaudio/test/half-a-second-1ch-48000-aac.aac
new file mode 100644
index 0000000000..e1c5ba4631
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-1ch-48000-aac.aac
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-1ch-48000-aac.mp4 b/dom/media/webaudio/test/half-a-second-1ch-48000-aac.mp4
new file mode 100644
index 0000000000..089d2a93e1
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-1ch-48000-aac.mp4
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-1ch-48000-flac.flac b/dom/media/webaudio/test/half-a-second-1ch-48000-flac.flac
new file mode 100644
index 0000000000..783bbf2c97
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-1ch-48000-flac.flac
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-1ch-48000-libmp3lame.mp3 b/dom/media/webaudio/test/half-a-second-1ch-48000-libmp3lame.mp3
new file mode 100644
index 0000000000..f9dfe29a89
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-1ch-48000-libmp3lame.mp3
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-1ch-48000-libopus.mp4 b/dom/media/webaudio/test/half-a-second-1ch-48000-libopus.mp4
new file mode 100644
index 0000000000..eb48fdac1b
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-1ch-48000-libopus.mp4
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-1ch-48000-libopus.opus b/dom/media/webaudio/test/half-a-second-1ch-48000-libopus.opus
new file mode 100644
index 0000000000..1b7cefcb3f
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-1ch-48000-libopus.opus
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-1ch-48000-libopus.webm b/dom/media/webaudio/test/half-a-second-1ch-48000-libopus.webm
new file mode 100644
index 0000000000..c06e5d7583
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-1ch-48000-libopus.webm
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-1ch-48000-libvorbis.ogg b/dom/media/webaudio/test/half-a-second-1ch-48000-libvorbis.ogg
new file mode 100644
index 0000000000..ad88da968c
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-1ch-48000-libvorbis.ogg
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-1ch-48000-libvorbis.webm b/dom/media/webaudio/test/half-a-second-1ch-48000-libvorbis.webm
new file mode 100644
index 0000000000..d63e2c31d3
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-1ch-48000-libvorbis.webm
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-1ch-48000.wav b/dom/media/webaudio/test/half-a-second-1ch-48000.wav
new file mode 100644
index 0000000000..d1fcb21134
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-1ch-48000.wav
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-2ch-44100-aac.aac b/dom/media/webaudio/test/half-a-second-2ch-44100-aac.aac
new file mode 100644
index 0000000000..d2255e982b
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-2ch-44100-aac.aac
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-2ch-44100-aac.mp4 b/dom/media/webaudio/test/half-a-second-2ch-44100-aac.mp4
new file mode 100644
index 0000000000..fbdd17e416
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-2ch-44100-aac.mp4
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-2ch-44100-flac.flac b/dom/media/webaudio/test/half-a-second-2ch-44100-flac.flac
new file mode 100644
index 0000000000..9cc57c24bd
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-2ch-44100-flac.flac
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-2ch-44100-libmp3lame.mp3 b/dom/media/webaudio/test/half-a-second-2ch-44100-libmp3lame.mp3
new file mode 100644
index 0000000000..399df50839
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-2ch-44100-libmp3lame.mp3
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-2ch-44100-libopus.mp4 b/dom/media/webaudio/test/half-a-second-2ch-44100-libopus.mp4
new file mode 100644
index 0000000000..242fb3e12e
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-2ch-44100-libopus.mp4
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-2ch-44100-libopus.opus b/dom/media/webaudio/test/half-a-second-2ch-44100-libopus.opus
new file mode 100644
index 0000000000..a9311b5f9c
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-2ch-44100-libopus.opus
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-2ch-44100-libopus.webm b/dom/media/webaudio/test/half-a-second-2ch-44100-libopus.webm
new file mode 100644
index 0000000000..ca8876d5e1
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-2ch-44100-libopus.webm
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-2ch-44100-libvorbis.ogg b/dom/media/webaudio/test/half-a-second-2ch-44100-libvorbis.ogg
new file mode 100644
index 0000000000..edf76edf89
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-2ch-44100-libvorbis.ogg
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-2ch-44100-libvorbis.webm b/dom/media/webaudio/test/half-a-second-2ch-44100-libvorbis.webm
new file mode 100644
index 0000000000..b01575c526
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-2ch-44100-libvorbis.webm
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-2ch-44100.wav b/dom/media/webaudio/test/half-a-second-2ch-44100.wav
new file mode 100644
index 0000000000..ae37e12813
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-2ch-44100.wav
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-2ch-48000-aac.aac b/dom/media/webaudio/test/half-a-second-2ch-48000-aac.aac
new file mode 100644
index 0000000000..d26803d76f
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-2ch-48000-aac.aac
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-2ch-48000-aac.mp4 b/dom/media/webaudio/test/half-a-second-2ch-48000-aac.mp4
new file mode 100644
index 0000000000..d7e3140580
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-2ch-48000-aac.mp4
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-2ch-48000-flac.flac b/dom/media/webaudio/test/half-a-second-2ch-48000-flac.flac
new file mode 100644
index 0000000000..624e5280ff
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-2ch-48000-flac.flac
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-2ch-48000-libmp3lame.mp3 b/dom/media/webaudio/test/half-a-second-2ch-48000-libmp3lame.mp3
new file mode 100644
index 0000000000..bd009ebfb4
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-2ch-48000-libmp3lame.mp3
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-2ch-48000-libopus.mp4 b/dom/media/webaudio/test/half-a-second-2ch-48000-libopus.mp4
new file mode 100644
index 0000000000..89e2b19256
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-2ch-48000-libopus.mp4
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-2ch-48000-libopus.opus b/dom/media/webaudio/test/half-a-second-2ch-48000-libopus.opus
new file mode 100644
index 0000000000..1e3c72b7b2
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-2ch-48000-libopus.opus
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-2ch-48000-libopus.webm b/dom/media/webaudio/test/half-a-second-2ch-48000-libopus.webm
new file mode 100644
index 0000000000..c7306df2ef
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-2ch-48000-libopus.webm
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-2ch-48000-libvorbis.ogg b/dom/media/webaudio/test/half-a-second-2ch-48000-libvorbis.ogg
new file mode 100644
index 0000000000..63e6d2bb87
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-2ch-48000-libvorbis.ogg
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-2ch-48000-libvorbis.webm b/dom/media/webaudio/test/half-a-second-2ch-48000-libvorbis.webm
new file mode 100644
index 0000000000..589370d6e2
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-2ch-48000-libvorbis.webm
Binary files differ
diff --git a/dom/media/webaudio/test/half-a-second-2ch-48000.wav b/dom/media/webaudio/test/half-a-second-2ch-48000.wav
new file mode 100644
index 0000000000..e9fc21e30b
--- /dev/null
+++ b/dom/media/webaudio/test/half-a-second-2ch-48000.wav
Binary files differ
diff --git a/dom/media/webaudio/test/invalid.txt b/dom/media/webaudio/test/invalid.txt
new file mode 100644
index 0000000000..c44840faf1
--- /dev/null
+++ b/dom/media/webaudio/test/invalid.txt
@@ -0,0 +1 @@
+Surely this is not an audio file!
diff --git a/dom/media/webaudio/test/invalidContent.flac b/dom/media/webaudio/test/invalidContent.flac
new file mode 100644
index 0000000000..b2f4e1ff7a
--- /dev/null
+++ b/dom/media/webaudio/test/invalidContent.flac
@@ -0,0 +1 @@
+fLaC
diff --git a/dom/media/webaudio/test/layouttest-glue.js b/dom/media/webaudio/test/layouttest-glue.js
new file mode 100644
index 0000000000..0ed0b9dc90
--- /dev/null
+++ b/dom/media/webaudio/test/layouttest-glue.js
@@ -0,0 +1,18 @@
+// Reimplementation of the LayoutTest API from Blink so we can easily port
+// WebAudio tests to Simpletest, without touching the internals of the test.
+
+function testFailed(msg) {
+ ok(false, msg);
+}
+
+function testPassed(msg) {
+ ok(true, msg);
+}
+
+function finishJSTest() {
+ SimpleTest.finish();
+}
+
+function description(str) {
+ info(str);
+}
diff --git a/dom/media/webaudio/test/mochitest.ini b/dom/media/webaudio/test/mochitest.ini
new file mode 100644
index 0000000000..0d303702eb
--- /dev/null
+++ b/dom/media/webaudio/test/mochitest.ini
@@ -0,0 +1,215 @@
+[DEFAULT]
+tags = mtg webaudio
+subsuite = media
+support-files =
+ 8kHz-320kbps-6ch.aac
+ audio-expected.wav
+ audio-mono-expected-2.wav
+ audio-mono-expected.wav
+ audio-quad.wav
+ audio.ogv
+ audiovideo.mp4
+ audioBufferSourceNodeDetached_worker.js
+ corsServer.sjs
+ !/dom/events/test/event_leak_utils.js
+ file_nodeCreationDocumentGone.html
+ invalid.txt
+ invalidContent.flac
+ layouttest-glue.js
+ nil-packet.ogg
+ noaudio.webm
+ small-shot-expected.wav
+ small-shot-mono-expected.wav
+ small-shot.ogg
+ small-shot.mp3
+ sweep-300-330-1sec.opus
+ ting-44.1k-1ch.ogg
+ ting-44.1k-2ch.ogg
+ ting-48k-1ch.ogg
+ ting-48k-2ch.ogg
+ ting-44.1k-1ch.wav
+ ting-44.1k-2ch.wav
+ ting-48k-1ch.wav
+ ting-48k-2ch.wav
+ sine-440-10s.opus
+ webaudio.js
+ # See ./generate-test-files.py
+ half-a-second-1ch-44100-aac.aac
+ half-a-second-1ch-44100-aac.mp4
+ half-a-second-1ch-44100-flac.flac
+ half-a-second-1ch-44100-libmp3lame.mp3
+ half-a-second-1ch-44100-libopus.mp4
+ half-a-second-1ch-44100-libopus.opus
+ half-a-second-1ch-44100-libopus.webm
+ half-a-second-1ch-44100-libvorbis.ogg
+ half-a-second-1ch-44100-libvorbis.webm
+ half-a-second-1ch-44100.wav
+ half-a-second-1ch-48000-aac.aac
+ half-a-second-1ch-48000-aac.mp4
+ half-a-second-1ch-48000-flac.flac
+ half-a-second-1ch-48000-libmp3lame.mp3
+ half-a-second-1ch-48000-libopus.mp4
+ half-a-second-1ch-48000-libopus.opus
+ half-a-second-1ch-48000-libopus.webm
+ half-a-second-1ch-48000-libvorbis.ogg
+ half-a-second-1ch-48000-libvorbis.webm
+ half-a-second-1ch-48000.wav
+ half-a-second-2ch-44100-aac.aac
+ half-a-second-2ch-44100-aac.mp4
+ half-a-second-2ch-44100-flac.flac
+ half-a-second-2ch-44100-libmp3lame.mp3
+ half-a-second-2ch-44100-libopus.mp4
+ half-a-second-2ch-44100-libopus.opus
+ half-a-second-2ch-44100-libopus.webm
+ half-a-second-2ch-44100-libvorbis.ogg
+ half-a-second-2ch-44100-libvorbis.webm
+ half-a-second-2ch-44100.wav
+ half-a-second-2ch-48000-aac.aac
+ half-a-second-2ch-48000-aac.mp4
+ half-a-second-2ch-48000-flac.flac
+ half-a-second-2ch-48000-libmp3lame.mp3
+ half-a-second-2ch-48000-libopus.mp4
+ half-a-second-2ch-48000-libopus.opus
+ half-a-second-2ch-48000-libopus.webm
+ half-a-second-2ch-48000-libvorbis.ogg
+ half-a-second-2ch-48000-libvorbis.webm
+ half-a-second-2ch-48000.wav
+ half-a-second-1ch-44100-aac-afconvert.mp4
+ sixteen-frames.mp3 # only 16 frames of valid audio
+ ../../webrtc/tests/mochitests/mediaStreamPlayback.js
+ ../../webrtc/tests/mochitests/head.js
+
+[test_analyserNode.html]
+skip-if = !asan && toolkit != android # These are tested in web-platform-tests, except on ASan and Android which don't run WPT.
+[test_analyserScale.html]
+skip-if = !asan && toolkit != android # These are tested in web-platform-tests, except on ASan and Android which don't run WPT.
+[test_analyserNodeOutput.html]
+skip-if = !asan && toolkit != android # These are tested in web-platform-tests, except on ASan and Android which don't run WPT.
+[test_analyserNodePassThrough.html]
+[test_analyserNodeWithGain.html]
+skip-if = !asan && toolkit != android # These are tested in web-platform-tests, except on ASan and Android which don't run WPT.
+[test_analyserNodeMinimum.html]
+skip-if = !asan && toolkit != android # These are tested in web-platform-tests, except on ASan and Android which don't run WPT.
+[test_channelMergerNode.html]
+[test_channelMergerNodeWithVolume.html]
+[test_channelSplitterNode.html]
+[test_channelSplitterNodeWithVolume.html]
+[test_convolverNode.html]
+[test_convolverNode_mono_mono.html]
+[test_convolverNodeChannelCount.html]
+[test_convolverNodeChannelInterpretationChanges.html]
+[test_convolverNodeDelay.html]
+[test_convolverNodeFiniteInfluence.html]
+[test_convolverNodeOOM.html]
+skip-if = asan
+ tsan # 1672869
+[test_convolverNodeNormalization.html]
+[test_convolverNodePassThrough.html]
+[test_convolverNodeWithGain.html]
+[test_convolver-upmixing-1-channel-response.html]
+# This is a copy of
+# testing/web-platform/tests/webaudio/the-audio-api/the-convolvernode-interface/convolver-upmixing-1-channel-response.html,
+# but WPT are not run with ASan or Android builds.
+skip-if = !asan && toolkit != android
+[test_currentTime.html]
+[test_decodeAudioDataOnDetachedBuffer.html]
+[test_decodeAudioDataPromise.html]
+[test_decodeAudioError.html]
+[test_decodeMultichannel.html]
+[test_decodeOpusTail.html]
+[test_decoderDelay.html]
+[test_delayNode.html]
+[test_delayNodeAtMax.html]
+[test_delayNodeChannelChanges.html]
+[test_delayNodeCycles.html]
+[test_delayNodePassThrough.html]
+[test_delayNodeSmallMaxDelay.html]
+[test_delayNodeTailIncrease.html]
+[test_delayNodeTailWithDisconnect.html]
+[test_delayNodeTailWithGain.html]
+[test_delayNodeTailWithReconnect.html]
+[test_delayNodeWithGain.html]
+[test_delaynode-channel-count-1.html]
+# This is a copy of
+# testing/web-platform/tests/webaudio/the-audio-api/the-delaynode-interface/delaynode-channel-count-1.html
+# but WPT are not run with ASan or Android builds.
+skip-if = !asan && toolkit != android
+[test_disconnectAll.html]
+[test_disconnectAudioParam.html]
+[test_disconnectAudioParamFromOutput.html]
+[test_disconnectExceptions.html]
+[test_disconnectFromAudioNode.html]
+[test_disconnectFromAudioNodeAndOutput.html]
+[test_disconnectFromAudioNodeAndOutputAndInput.html]
+[test_disconnectFromAudioNodeMultipleConnection.html]
+[test_disconnectFromOutput.html]
+[test_dynamicsCompressorNode.html]
+[test_dynamicsCompressorNodePassThrough.html]
+[test_dynamicsCompressorNodeWithGain.html]
+[test_event_listener_leaks.html]
+skip-if = (os == 'win' && processor == 'aarch64') # bug 1531927
+[test_gainNode.html]
+[test_gainNodeInLoop.html]
+[test_gainNodePassThrough.html]
+[test_iirFilterNodePassThrough.html]
+[test_maxChannelCount.html]
+skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1538360
+[test_mixingRules.html]
+[test_nodeToParamConnection.html]
+[test_nodeCreationDocumentGone.html]
+[test_notAllowedToStartAudioContextGC.html]
+[test_OfflineAudioContext.html]
+[test_offlineDestinationChannelCountLess.html]
+[test_offlineDestinationChannelCountMore.html]
+[test_oscillatorNode.html]
+[test_oscillatorNode2.html]
+[test_oscillatorNodeNegativeFrequency.html]
+[test_oscillatorNodePassThrough.html]
+[test_oscillatorNodeStart.html]
+[test_oscillatorTypeChange.html]
+[test_pannerNode.html]
+[test_pannerNode_equalPower.html]
+[test_pannerNode_audioparam_distance.html]
+[test_pannerNodeAbove.html]
+[test_pannerNodeAtZeroDistance.html]
+[test_pannerNodeChannelCount.html]
+[test_pannerNodeHRTFSymmetry.html]
+[test_pannerNodeTail.html]
+[test_pannerNode_maxDistance.html]
+[test_slowStart.html]
+[test_setValueCurveWithNonFiniteElements.html]
+[test_stereoPannerNode.html]
+[test_stereoPannerNodePassThrough.html]
+[test_periodicWave.html]
+[test_periodicWaveDisableNormalization.html]
+[test_periodicWaveBandLimiting.html]
+[test_retrospective-exponentialRampToValueAtTime.html]
+[test_retrospective-linearRampToValueAtTime.html]
+[test_retrospective-setTargetAtTime.html]
+[test_retrospective-setValueAtTime.html]
+[test_retrospective-setValueCurveAtTime.html]
+[test_ScriptProcessorCollected1.html]
+[test_scriptProcessorNode.html]
+[test_scriptProcessorNodeChannelCount.html]
+[test_scriptProcessorNodePassThrough.html]
+[test_scriptProcessorNode_playbackTime1.html]
+[test_scriptProcessorNodeZeroInputOutput.html]
+[test_scriptProcessorNodeNotConnected.html]
+[test_sequentialBufferSourceWithResampling.html]
+[test_singleSourceDest.html]
+skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1538360
+[test_stereoPanningWithGain.html]
+[test_waveDecoder.html]
+[test_waveShaper.html]
+[test_waveShaperGain.html]
+[test_waveShaperNoCurve.html]
+[test_waveShaperPassThrough.html]
+[test_waveShaperInvalidLengthCurve.html]
+[test_WebAudioMemoryReporting.html]
+[test_audioContextParams_sampleRate.html]
+[test_webAudio_muteTab.html]
+scheme = https
+skip-if = os == 'mac'
+ os == 'win'
+ toolkit == 'android' # Bug 1404995, no loopback devices on some platforms
+[test_audioContextParams_recordNonDefaultSampleRate.html]
diff --git a/dom/media/webaudio/test/mochitest_audio.ini b/dom/media/webaudio/test/mochitest_audio.ini
new file mode 100644
index 0000000000..8f6c35f9a7
--- /dev/null
+++ b/dom/media/webaudio/test/mochitest_audio.ini
@@ -0,0 +1,69 @@
+[DEFAULT]
+tags = mtg webaudio
+subsuite = media
+support-files =
+ audio-expected.wav
+ audio-mono-expected-2.wav
+ audio-mono-expected.wav
+ audio-quad.wav
+ audio.ogv
+ audiovideo.mp4
+ audioBufferSourceNodeDetached_worker.js
+ corsServer.sjs
+ !/dom/events/test/event_leak_utils.js
+ file_nodeCreationDocumentGone.html
+ invalid.txt
+ invalidContent.flac
+ layouttest-glue.js
+ nil-packet.ogg
+ noaudio.webm
+ small-shot-expected.wav
+ small-shot-mono-expected.wav
+ small-shot.ogg
+ small-shot.mp3
+ sweep-300-330-1sec.opus
+ ting-44.1k-1ch.ogg
+ ting-44.1k-2ch.ogg
+ ting-48k-1ch.ogg
+ ting-48k-2ch.ogg
+ ting-44.1k-1ch.wav
+ ting-44.1k-2ch.wav
+ ting-48k-1ch.wav
+ ting-48k-2ch.wav
+ sine-440-10s.opus
+ webaudio.js
+ ../../webrtc/tests/mochitests/mediaStreamPlayback.js
+ ../../webrtc/tests/mochitests/head.js
+
+[test_AudioBuffer.html]
+[test_audioBufferSourceNode.html]
+[test_audioBufferSourceNodeEnded.html]
+[test_audioBufferSourceNodeLazyLoopParam.html]
+[test_audioBufferSourceNodeLoop.html]
+[test_audioBufferSourceNodeLoopStartEnd.html]
+[test_audioBufferSourceNodeLoopStartEndSame.html]
+[test_audioBufferSourceNodeDetached.html]
+[test_audioBufferSourceNodeNoStart.html]
+[test_audioBufferSourceNodeNullBuffer.html]
+[test_audioBufferSourceNodeOffset.html]
+[test_audioBufferSourceNodePassThrough.html]
+[test_audioBufferSourceNodeRate.html]
+[test_AudioContext.html]
+[test_AudioContext_disabled.html]
+[test_audioContextGC.html]
+[test_audioContextSuspendResumeClose.html]
+tags=capturestream
+skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1539522
+[test_audioDestinationNode.html]
+[test_AudioListener.html]
+[test_AudioNodeDevtoolsAPI.html]
+[test_audioParamChaining.html]
+[test_AudioParamDevtoolsAPI.html]
+[test_audioParamExponentialRamp.html]
+[test_audioParamGain.html]
+[test_audioParamLinearRamp.html]
+[test_audioParamSetCurveAtTime.html]
+[test_audioParamSetTargetAtTime.html]
+[test_audioParamSetTargetAtTimeZeroTimeConstant.html]
+[test_audioParamSetValueAtTime.html]
+[test_audioParamTimelineDestinationOffset.html]
diff --git a/dom/media/webaudio/test/mochitest_bugs.ini b/dom/media/webaudio/test/mochitest_bugs.ini
new file mode 100644
index 0000000000..66b645673a
--- /dev/null
+++ b/dom/media/webaudio/test/mochitest_bugs.ini
@@ -0,0 +1,65 @@
+[DEFAULT]
+tags = mtg webaudio
+subsuite = media
+support-files =
+ audio-expected.wav
+ audio-mono-expected-2.wav
+ audio-mono-expected.wav
+ audio-quad.wav
+ audio.ogv
+ audiovideo.mp4
+ audioBufferSourceNodeDetached_worker.js
+ corsServer.sjs
+ !/dom/events/test/event_leak_utils.js
+ file_nodeCreationDocumentGone.html
+ invalid.txt
+ invalidContent.flac
+ layouttest-glue.js
+ nil-packet.ogg
+ noaudio.webm
+ small-shot-expected.wav
+ small-shot-mono-expected.wav
+ small-shot.ogg
+ small-shot.mp3
+ sweep-300-330-1sec.opus
+ ting-44.1k-1ch.ogg
+ ting-44.1k-2ch.ogg
+ ting-48k-1ch.ogg
+ ting-48k-2ch.ogg
+ ting-44.1k-1ch.wav
+ ting-44.1k-2ch.wav
+ ting-48k-1ch.wav
+ ting-48k-2ch.wav
+ sine-440-10s.opus
+ webaudio.js
+ ../../webrtc/tests/mochitests/mediaStreamPlayback.js
+ ../../webrtc/tests/mochitests/head.js
+
+[test_bug808374.html]
+[test_bug827541.html]
+[test_bug839753.html]
+[test_bug845960.html]
+[test_bug856771.html]
+[test_bug866570.html]
+[test_bug866737.html]
+[test_bug867089.html]
+[test_bug867174.html]
+[test_bug873335.html]
+[test_bug875221.html]
+[test_bug875402.html]
+[test_bug894150.html]
+[test_bug956489.html]
+[test_bug964376.html]
+[test_bug966247.html]
+tags=capturestream
+[test_bug972678.html]
+[test_bug1113634.html]
+[test_bug1118372.html]
+[test_bug1027864.html]
+skip-if = true #Bug 1650930
+[test_bug1056032.html]
+[test_bug1255618.html]
+skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1538360
+[test_bug1267579.html]
+[test_bug1355798.html]
+[test_bug1447273.html]
diff --git a/dom/media/webaudio/test/mochitest_media.ini b/dom/media/webaudio/test/mochitest_media.ini
new file mode 100644
index 0000000000..be153c90c8
--- /dev/null
+++ b/dom/media/webaudio/test/mochitest_media.ini
@@ -0,0 +1,64 @@
+[DEFAULT]
+tags = mtg webaudio
+subsuite = media
+support-files =
+ audio-expected.wav
+ audio-mono-expected-2.wav
+ audio-mono-expected.wav
+ audio-quad.wav
+ audio.ogv
+ audiovideo.mp4
+ audioBufferSourceNodeDetached_worker.js
+ corsServer.sjs
+ !/dom/events/test/event_leak_utils.js
+ file_nodeCreationDocumentGone.html
+ invalid.txt
+ invalidContent.flac
+ layouttest-glue.js
+ nil-packet.ogg
+ noaudio.webm
+ small-shot-expected.wav
+ small-shot-mono-expected.wav
+ small-shot.ogg
+ small-shot.mp3
+ sweep-300-330-1sec.opus
+ ting-44.1k-1ch.ogg
+ ting-44.1k-2ch.ogg
+ ting-48k-1ch.ogg
+ ting-48k-2ch.ogg
+ ting-44.1k-1ch.wav
+ ting-44.1k-2ch.wav
+ ting-48k-1ch.wav
+ ting-48k-2ch.wav
+ sine-440-10s.opus
+ webaudio.js
+ ../../webrtc/tests/mochitests/mediaStreamPlayback.js
+ ../../webrtc/tests/mochitests/head.js
+
+[test_mediaDecoding.html]
+[test_mediaElementAudioSourceNode.html]
+tags=capturestream
+[test_mediaElementAudioSourceNodeFidelity.html]
+tags=capturestream
+skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1538360
+[test_mediaElementAudioSourceNodePassThrough.html]
+tags=capturestream
+[test_mediaElementAudioSourceNodeVideo.html]
+tags=capturestream
+
+[test_mediaElementAudioSourceNodeCrossOrigin.html]
+tags=capturestream
+[test_mediaStreamAudioDestinationNode.html]
+[test_mediaStreamAudioSourceNode.html]
+[test_mediaStreamAudioSourceNodeCrossOrigin.html]
+tags=capturestream
+[test_mediaStreamAudioSourceNodeNoGC.html]
+skip-if = os == "mac" && debug # Bug 1756880 - lower frequency shutdown hangs
+scheme=https
+
+[test_mediaStreamAudioSourceNodePassThrough.html]
+[test_mediaStreamAudioSourceNodeResampling.html]
+tags=capturestream
+[test_mediaStreamTrackAudioSourceNode.html]
+[test_mediaStreamTrackAudioSourceNodeVideo.html]
+[test_mediaStreamTrackAudioSourceNodeCrossOrigin.html]
diff --git a/dom/media/webaudio/test/nil-packet.ogg b/dom/media/webaudio/test/nil-packet.ogg
new file mode 100644
index 0000000000..7b00b5a63e
--- /dev/null
+++ b/dom/media/webaudio/test/nil-packet.ogg
Binary files differ
diff --git a/dom/media/webaudio/test/noaudio.webm b/dom/media/webaudio/test/noaudio.webm
new file mode 100644
index 0000000000..9207017fb6
--- /dev/null
+++ b/dom/media/webaudio/test/noaudio.webm
Binary files differ
diff --git a/dom/media/webaudio/test/sine-440-10s.opus b/dom/media/webaudio/test/sine-440-10s.opus
new file mode 100644
index 0000000000..eb91020168
--- /dev/null
+++ b/dom/media/webaudio/test/sine-440-10s.opus
Binary files differ
diff --git a/dom/media/webaudio/test/sixteen-frames.mp3 b/dom/media/webaudio/test/sixteen-frames.mp3
new file mode 100644
index 0000000000..1d15dcad59
--- /dev/null
+++ b/dom/media/webaudio/test/sixteen-frames.mp3
Binary files differ
diff --git a/dom/media/webaudio/test/small-shot-expected.wav b/dom/media/webaudio/test/small-shot-expected.wav
new file mode 100644
index 0000000000..2faaa8258b
--- /dev/null
+++ b/dom/media/webaudio/test/small-shot-expected.wav
Binary files differ
diff --git a/dom/media/webaudio/test/small-shot-mono-expected.wav b/dom/media/webaudio/test/small-shot-mono-expected.wav
new file mode 100644
index 0000000000..d4e2647e42
--- /dev/null
+++ b/dom/media/webaudio/test/small-shot-mono-expected.wav
Binary files differ
diff --git a/dom/media/webaudio/test/small-shot.mp3 b/dom/media/webaudio/test/small-shot.mp3
new file mode 100644
index 0000000000..f9397a5106
--- /dev/null
+++ b/dom/media/webaudio/test/small-shot.mp3
Binary files differ
diff --git a/dom/media/webaudio/test/small-shot.ogg b/dom/media/webaudio/test/small-shot.ogg
new file mode 100644
index 0000000000..1a41623f81
--- /dev/null
+++ b/dom/media/webaudio/test/small-shot.ogg
Binary files differ
diff --git a/dom/media/webaudio/test/sweep-300-330-1sec.opus b/dom/media/webaudio/test/sweep-300-330-1sec.opus
new file mode 100644
index 0000000000..619d1e0844
--- /dev/null
+++ b/dom/media/webaudio/test/sweep-300-330-1sec.opus
Binary files differ
diff --git a/dom/media/webaudio/test/test_AudioBuffer.html b/dom/media/webaudio/test/test_AudioBuffer.html
new file mode 100644
index 0000000000..05957f679e
--- /dev/null
+++ b/dom/media/webaudio/test/test_AudioBuffer.html
@@ -0,0 +1,104 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test whether we can create an AudioContext interface</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script src="webaudio.js" type="text/javascript"></script>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var context = new AudioContext();
+ var buffer = context.createBuffer(2, 2048, context.sampleRate);
+ SpecialPowers.gc(); // Make sure that our channels are accessible after GC
+ ok(buffer, "Buffer was allocated successfully");
+ is(buffer.sampleRate, context.sampleRate, "Correct sample rate");
+ is(buffer.length, 2048, "Correct length");
+ ok(Math.abs(buffer.duration - 2048 / context.sampleRate) < 0.0001, "Correct duration");
+ is(buffer.numberOfChannels, 2, "Correct number of channels");
+ for (var i = 0; i < buffer.numberOfChannels; ++i) {
+ var buf = buffer.getChannelData(i);
+ ok(buf, "Buffer index " + i + " exists");
+ ok(buf instanceof Float32Array, "Result is a typed array");
+ is(buf.length, buffer.length, "Correct length");
+ var foundNonZero = false;
+ for (var j = 0; j < buf.length; ++j) {
+ if (buf[j] != 0) {
+ foundNonZero = true;
+ break;
+ }
+ buf[j] = j;
+ }
+ ok(!foundNonZero, "Buffer " + i + " should be initialized to 0");
+ }
+
+ // Now test copying the channel data out of a normal buffer
+ var copy = new Float32Array(100);
+ buffer.copyFromChannel(copy, 0, 1024);
+ for (var i = 0; i < copy.length; ++i) {
+ is(copy[i], 1024 + i, "Correct sample");
+ }
+ // Test copying the channel data out of a playing buffer
+ var srcNode = context.createBufferSource();
+ srcNode.buffer = buffer;
+ srcNode.start(0);
+ copy = new Float32Array(100);
+ buffer.copyFromChannel(copy, 0, 1024);
+ for (var i = 0; i < copy.length; ++i) {
+ is(copy[i], 1024 + i, "Correct sample");
+ }
+
+ // Test copying to the channel data
+ var newData = new Float32Array(200);
+ buffer.copyToChannel(newData, 0, 100);
+ var changedData = buffer.getChannelData(0);
+ for (var i = 0; i < changedData.length; ++i) {
+ if (i < 100 || i >= 300) {
+ is(changedData[i], i, "Untouched sample");
+ } else {
+ is(changedData[i], 0, "Correct sample");
+ }
+ }
+
+ // Now, detach the array buffer
+ var worker = new Worker("audioBufferSourceNodeDetached_worker.js");
+ var data = buffer.getChannelData(0).buffer;
+ worker.postMessage(data, [data]);
+ SpecialPowers.gc();
+
+ expectNoException(function() {
+ buffer.copyFromChannel(copy, 0, 1024);
+ });
+
+ expectNoException(function() {
+ buffer.copyToChannel(newData, 0, 100);
+ });
+
+ expectException(function() {
+ context.createBuffer(2, 2048, 7999);
+ }, DOMException.NOT_SUPPORTED_ERR);
+ expectException(function() {
+ context.createBuffer(2, 2048, 192001);
+ }, DOMException.NOT_SUPPORTED_ERR);
+ context.createBuffer(2, 2048, 8000); // no exception
+ context.createBuffer(2, 2048, 192000); // no exception
+ context.createBuffer(32, 2048, 48000); // no exception
+ // Null length
+ expectException(function() {
+ context.createBuffer(2, 0, 48000);
+ }, DOMException.NOT_SUPPORTED_ERR);
+ // Null number of channels
+ expectException(function() {
+ context.createBuffer(0, 2048, 48000);
+ }, DOMException.NOT_SUPPORTED_ERR);
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_AudioContext.html b/dom/media/webaudio/test/test_AudioContext.html
new file mode 100644
index 0000000000..50aeee489e
--- /dev/null
+++ b/dom/media/webaudio/test/test_AudioContext.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test whether we can create an AudioContext interface</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var ac = new AudioContext();
+ ok(ac, "Create a AudioContext object");
+ ok(ac instanceof EventTarget, "AudioContexts must be EventTargets");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_AudioContext_disabled.html b/dom/media/webaudio/test/test_AudioContext_disabled.html
new file mode 100644
index 0000000000..9c3651a883
--- /dev/null
+++ b/dom/media/webaudio/test/test_AudioContext_disabled.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test whether we can disable the AudioContext interface</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+const webaudio_interfaces = [
+ "AudioContext",
+ "OfflineAudioContext",
+ "AudioContext",
+ "OfflineAudioCompletionEvent",
+ "AudioNode",
+ "AudioDestinationNode",
+ "AudioParam",
+ "GainNode",
+ "DelayNode",
+ "AudioBuffer",
+ "AudioBufferSourceNode",
+ "MediaElementAudioSourceNode",
+ "ScriptProcessorNode",
+ "AudioProcessingEvent",
+ "PannerNode",
+ "AudioListener",
+ "StereoPannerNode",
+ "ConvolverNode",
+ "AnalyserNode",
+ "ChannelSplitterNode",
+ "ChannelMergerNode",
+ "DynamicsCompressorNode",
+ "BiquadFilterNode",
+ "IIRFilterNode",
+ "WaveShaperNode",
+ "OscillatorNode",
+ "PeriodicWave",
+ "MediaStreamAudioSourceNode",
+ "MediaStreamAudioDestinationNode"
+];
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ SpecialPowers.pushPrefEnv({"set": [["dom.webaudio.enabled", false]]}, function() {
+ webaudio_interfaces.forEach((e) => ok(!window[e], e + " must be disabled when the Web Audio API is disabled"));
+ SimpleTest.finish();
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_AudioListener.html b/dom/media/webaudio/test/test_AudioListener.html
new file mode 100644
index 0000000000..e3d605cbcc
--- /dev/null
+++ b/dom/media/webaudio/test/test_AudioListener.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AudioContext.listener and the AudioListener interface</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var context = new AudioContext();
+ ok("listener" in context, "AudioContext.listener should exist");
+ // The values set by the following cannot be read from script, but the
+ // implementation is simple enough, so we just make sure that nothing throws.
+ context.listener.setPosition(1.0, 1.0, 1.0);
+ context.listener.setOrientation(1.0, 1.0, 1.0, 1.0, 1.0, 1.0);
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_AudioNodeDevtoolsAPI.html b/dom/media/webaudio/test/test_AudioNodeDevtoolsAPI.html
new file mode 100644
index 0000000000..f032ed88f0
--- /dev/null
+++ b/dom/media/webaudio/test/test_AudioNodeDevtoolsAPI.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test the devtool AudioNode API</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+ SimpleTest.waitForExplicitFinish();
+
+ function id(node) {
+ return SpecialPowers.getPrivilegedProps(node, "id");
+ }
+
+ var ac = new AudioContext();
+ var ids;
+ var weak;
+ (function() {
+ var src1 = ac.createBufferSource();
+ var src2 = ac.createBufferSource();
+ ok(id(src2) > id(src1), "The ID should be monotonic");
+ ok(id(src1) > id(ac.destination), "The ID of the destination node should be the lowest");
+ ids = [id(src1), id(src2)];
+ weak = SpecialPowers.Cu.getWeakReference(src1);
+ is(SpecialPowers.unwrap(weak.get()), src1, "The node should support a weak reference");
+ })();
+ function observer(subject, topic, data) {
+ var id = parseInt(data);
+ var index = ids.indexOf(id);
+ if (index != -1) {
+ info("Dropping id " + id + " at index " + index);
+ ids.splice(index, 1);
+ if (!ids.length) {
+ SimpleTest.executeSoon(function() {
+ is(weak.get(), null, "The weak reference must be dropped now");
+ SpecialPowers.removeObserver(observer, "webaudio-node-demise");
+ SimpleTest.finish();
+ });
+ }
+ }
+ }
+ SpecialPowers.addObserver(observer, "webaudio-node-demise");
+
+ forceCC();
+ forceCC();
+
+ function forceCC() {
+ SpecialPowers.DOMWindowUtils.cycleCollect();
+ SpecialPowers.DOMWindowUtils.garbageCollect();
+ SpecialPowers.DOMWindowUtils.garbageCollect();
+ }
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_AudioParamDevtoolsAPI.html b/dom/media/webaudio/test/test_AudioParamDevtoolsAPI.html
new file mode 100644
index 0000000000..81fdd357c9
--- /dev/null
+++ b/dom/media/webaudio/test/test_AudioParamDevtoolsAPI.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test the devtool AudioParam API</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+ function checkIdAndName(node, name) {
+ is(SpecialPowers.getPrivilegedProps(node, "id"),
+ SpecialPowers.getPrivilegedProps(node[name], "parentNodeId"),
+ "The parent id should be correct");
+ is(SpecialPowers.getPrivilegedProps(node[name], "name"), name,
+ "The name of the AudioParam should be correct.");
+ }
+
+ var ac = new AudioContext(),
+ gain = ac.createGain(),
+ osc = ac.createOscillator(),
+ del = ac.createDelay(),
+ source = ac.createBufferSource(),
+ stereoPanner = ac.createStereoPanner(),
+ comp = ac.createDynamicsCompressor(),
+ biquad = ac.createBiquadFilter();
+
+ checkIdAndName(gain, "gain");
+ checkIdAndName(osc, "frequency");
+ checkIdAndName(osc, "detune");
+ checkIdAndName(del, "delayTime");
+ checkIdAndName(source, "playbackRate");
+ checkIdAndName(source, "detune");
+ checkIdAndName(stereoPanner, "pan");
+ checkIdAndName(comp, "threshold");
+ checkIdAndName(comp, "knee");
+ checkIdAndName(comp, "ratio");
+ checkIdAndName(comp, "attack");
+ checkIdAndName(comp, "release");
+ checkIdAndName(biquad, "frequency");
+ checkIdAndName(biquad, "detune");
+ checkIdAndName(biquad, "Q");
+ checkIdAndName(biquad, "gain");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_OfflineAudioContext.html b/dom/media/webaudio/test/test_OfflineAudioContext.html
new file mode 100644
index 0000000000..d9403566ae
--- /dev/null
+++ b/dom/media/webaudio/test/test_OfflineAudioContext.html
@@ -0,0 +1,118 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test OfflineAudioContext</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var renderedBuffer = null;
+var finished = 0;
+
+function finish() {
+ finished++;
+ if (finished == 2) {
+ SimpleTest.finish();
+ }
+}
+
+function setOrCompareRenderedBuffer(aRenderedBuffer) {
+ if (renderedBuffer) {
+ is(renderedBuffer, aRenderedBuffer, "Rendered buffers from the event and the promise should be the same");
+ finish();
+ } else {
+ renderedBuffer = aRenderedBuffer;
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ let ctxs = [
+ new OfflineAudioContext(2, 100, 22050),
+ new OfflineAudioContext({length: 100, sampleRate: 22050}),
+ new OfflineAudioContext({channels: 2, length: 100, sampleRate: 22050}),
+ ];
+
+ for (let ctx of ctxs) {
+ ok(ctx instanceof EventTarget, "OfflineAudioContexts must be EventTargets");
+ is(ctx.length, 100, "OfflineAudioContext.length is equal to the value passed to the ctor.");
+
+ var buf = ctx.createBuffer(2, 100, ctx.sampleRate);
+ for (var i = 0; i < 2; ++i) {
+ for (var j = 0; j < 100; ++j) {
+ buf.getChannelData(i)[j] = Math.sin(2 * Math.PI * 200 * j / ctx.sampleRate);
+ }
+ }
+ }
+
+ is(ctxs[1].destination.channelCount, 1, "OfflineAudioContext defaults to to correct channelCount.");
+
+ let ctx = ctxs[0];
+
+ expectException(function() {
+ new OfflineAudioContext(2, 100, 0);
+ }, DOMException.NOT_SUPPORTED_ERR);
+ expectException(function() {
+ new OfflineAudioContext(2, 100, -1);
+ }, DOMException.NOT_SUPPORTED_ERR);
+ expectException(function() {
+ new OfflineAudioContext(0, 100, 44100);
+ }, DOMException.NOT_SUPPORTED_ERR);
+ new OfflineAudioContext(32, 100, 44100);
+ expectException(function() {
+ new OfflineAudioContext(33, 100, 44100);
+ }, DOMException.NOT_SUPPORTED_ERR);
+ expectException(function() {
+ new OfflineAudioContext(2, 0, 44100);
+ }, DOMException.NOT_SUPPORTED_ERR);
+ expectTypeError(function() {
+ new OfflineAudioContext({});
+ });
+ expectTypeError(function() {
+ new OfflineAudioContext({sampleRate: 44100});
+ });
+ expectTypeError(function() {
+ new OfflineAudioContext({length: 44100*40});
+ });
+
+ var src = ctx.createBufferSource();
+ src.buffer = buf;
+ src.start(0);
+ src.connect(ctx.destination);
+
+ ctx.addEventListener("complete", function(e) {
+ ok(e instanceof OfflineAudioCompletionEvent, "Correct event received");
+ is(e.renderedBuffer.numberOfChannels, 2, "Correct expected number of buffers");
+ ok(renderedBuffer != null, "The event should be fired after the promise callback.");
+ expectNoException(function() {
+ ctx.startRendering().then(function() {
+ ok(false, "Promise should not resolve when startRendering is called a second time on an OfflineAudioContext")
+ finish();
+ }).catch(function(err) {
+ ok(true, "Promise should reject when startRendering is called a second time on an OfflineAudioContext")
+ finish();
+ });
+ });
+ compareBuffers(e.renderedBuffer, buf);
+ setOrCompareRenderedBuffer(e.renderedBuffer);
+
+ });
+
+ expectNoException(function() {
+ ctx.startRendering().then(function(b) {
+ is(renderedBuffer, null, "The promise callback should be called first.");
+ setOrCompareRenderedBuffer(b);
+ }).catch(function (error) {
+ ok(false, "The promise from OfflineAudioContext.startRendering should never be rejected");
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_ScriptProcessorCollected1.html b/dom/media/webaudio/test/test_ScriptProcessorCollected1.html
new file mode 100644
index 0000000000..8f05889d26
--- /dev/null
+++ b/dom/media/webaudio/test/test_ScriptProcessorCollected1.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test ScriptProcessorNode in cycle with no listener is collected</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+var observer = function(subject, topic, data) {
+ var id = parseInt(data);
+ var index = ids.indexOf(id);
+ if (index != -1) {
+ ok(true, "Collected AudioNode id " + id + " at index " + index);
+ ids.splice(index, 1);
+ }
+}
+
+SpecialPowers.addObserver(observer, "webaudio-node-demise");
+
+SimpleTest.registerCleanupFunction(function() {
+ if (observer) {
+ SpecialPowers.removeObserver(observer, "webaudio-node-demise");
+ }
+});
+
+var ac = new AudioContext();
+
+var testProcessor = ac.createScriptProcessor(256, 1, 0);
+var delay = ac.createDelay();
+testProcessor.connect(delay);
+delay.connect(testProcessor);
+
+var referenceProcessor = ac.createScriptProcessor(256, 1, 0);
+var gain = ac.createGain();
+gain.connect(referenceProcessor);
+
+var processCount = 0;
+testProcessor.onaudioprocess = function(event) {
+ ++processCount;
+ switch (processCount) {
+ case 1:
+ // Switch to listening to referenceProcessor;
+ referenceProcessor.onaudioprocess = event.target.onaudioprocess;
+ referenceProcessor = null;
+ event.target.onaudioprocess = null;
+ break;
+ case 2:
+ // There are no references to testProcessor and so GC can begin.
+ SpecialPowers.exactGC(function() {
+ SpecialPowers.removeObserver(observer, "webaudio-node-demise");
+ observer = null;
+ event.target.onaudioprocess = null;
+ ok(!ids.length, "All expected nodes should be collected");
+ SimpleTest.finish();
+ });
+ break;
+ }
+};
+
+function id(node) {
+ return SpecialPowers.getPrivilegedProps(node, "id");
+}
+
+// Nodes with these ids should be collected.
+var ids = [ id(testProcessor), id(delay), id(gain) ];
+testProcessor = null;
+delay = null;
+gain = null;
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_WebAudioMemoryReporting.html b/dom/media/webaudio/test/test_WebAudioMemoryReporting.html
new file mode 100644
index 0000000000..693e558304
--- /dev/null
+++ b/dom/media/webaudio/test/test_WebAudioMemoryReporting.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Web Audio memory reporting</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+var ac = new AudioContext();
+var sp = ac.createScriptProcessor(4096, 1, 1);
+sp.connect(ac.destination);
+
+// Not started so as to test
+// https://bugzilla.mozilla.org/show_bug.cgi?id=1225003#c2
+var oac = new OfflineAudioContext(1, 1, 48000);
+
+var nodeTypes = ["ScriptProcessorNode", "AudioDestinationNode"];
+var objectTypes = ["dom-nodes", "engine-objects", "track-objects"];
+
+var usages = { "explicit/webaudio/audiocontext": 0 };
+
+for (var i = 0; i < nodeTypes.length; ++i) {
+ for (var j = 0; j < objectTypes.length; ++j) {
+ usages["explicit/webaudio/audio-node/" +
+ nodeTypes[i] + "/" + objectTypes[j]] = 0;
+ }
+}
+
+var handleReport = function(aProcess, aPath, aKind, aUnits, aAmount, aDesc) {
+ if (aPath in usages) {
+ usages[aPath] += aAmount;
+ }
+}
+
+var finished = function () {
+ ok(true, "Yay didn't crash!");
+ for (var resource in usages) {
+ ok(usages[resource] > 0, "Non-zero usage for " + resource);
+ };
+ SimpleTest.finish();
+}
+
+SpecialPowers.Cc["@mozilla.org/memory-reporter-manager;1"].
+ getService(SpecialPowers.Ci.nsIMemoryReporterManager).
+ getReports(handleReport, null, finished, null, /* anonymized = */ false);
+
+// To test bug 1225003, run a failing decodeAudioData() job over a time when
+// the tasks from getReports() are expected to run.
+ac.decodeAudioData(new ArrayBuffer(4), function(){}, function(){});
+</script>
+</html>
diff --git a/dom/media/webaudio/test/test_analyserNode.html b/dom/media/webaudio/test/test_analyserNode.html
new file mode 100644
index 0000000000..0793eeb2cb
--- /dev/null
+++ b/dom/media/webaudio/test/test_analyserNode.html
@@ -0,0 +1,178 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AnalyserNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+function testNode() {
+ var context = new AudioContext();
+ var buffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ }
+
+ var destination = context.destination;
+
+ var source = context.createBufferSource();
+
+ var analyser = context.createAnalyser();
+
+ source.buffer = buffer;
+
+ source.connect(analyser);
+ analyser.connect(destination);
+
+ is(analyser.channelCount, 2, "analyser node has 2 input channels by default");
+ is(analyser.channelCountMode, "max", "Correct channelCountMode for the analyser node");
+ is(analyser.channelInterpretation, "speakers", "Correct channelCountInterpretation for the analyser node");
+
+ is(analyser.fftSize, 2048, "Correct default value for fftSize");
+ is(analyser.frequencyBinCount, 1024, "Correct default value for frequencyBinCount");
+ expectException(function() {
+ analyser.fftSize = 0;
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ analyser.fftSize = 1;
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ analyser.fftSize = 8;
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ analyser.fftSize = 100; // non-power of two
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ analyser.fftSize = 2049;
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ analyser.fftSize = 4097;
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ analyser.fftSize = 8193;
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ analyser.fftSize = 16385;
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ analyser.fftSize = 32769;
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ analyser.fftSize = 65536;
+ }, DOMException.INDEX_SIZE_ERR);
+ analyser.fftSize = 1024;
+ is(analyser.frequencyBinCount, 512, "Correct new value for frequencyBinCount");
+
+ is(analyser.minDecibels, -100, "Correct default value for minDecibels");
+ is(analyser.maxDecibels, -30, "Correct default value for maxDecibels");
+ expectException(function() {
+ analyser.minDecibels = -30;
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ analyser.minDecibels = -29;
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ analyser.maxDecibels = -100;
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ analyser.maxDecibels = -101;
+ }, DOMException.INDEX_SIZE_ERR);
+
+ ok(Math.abs(analyser.smoothingTimeConstant - 0.8) < 0.001, "Correct default value for smoothingTimeConstant");
+ expectException(function() {
+ analyser.smoothingTimeConstant = -0.1;
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ analyser.smoothingTimeConstant = 1.1;
+ }, DOMException.INDEX_SIZE_ERR);
+ analyser.smoothingTimeConstant = 0;
+ analyser.smoothingTimeConstant = 1;
+}
+
+function testConstructor() {
+ var context = new AudioContext();
+
+ var analyser = new AnalyserNode(context);
+ is(analyser.channelCount, 2, "analyser node has 2 input channels by default");
+ is(analyser.channelCountMode, "max", "Correct channelCountMode for the analyser node");
+ is(analyser.channelInterpretation, "speakers", "Correct channelCountInterpretation for the analyser node");
+
+ is(analyser.fftSize, 2048, "Correct default value for fftSize");
+ is(analyser.frequencyBinCount, 1024, "Correct default value for frequencyBinCount");
+ is(analyser.minDecibels, -100, "Correct default value for minDecibels");
+ is(analyser.maxDecibels, -30, "Correct default value for maxDecibels");
+ ok(Math.abs(analyser.smoothingTimeConstant - 0.8) < 0.001, "Correct default value for smoothingTimeConstant");
+
+ expectException(function() {
+ analyser = new AnalyserNode(context, { fftSize: 0 });
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ analyser = new AnalyserNode(context, { fftSize: 1 });
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ analyser = new AnalyserNode(context, { fftSize: 8 });
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ analyser = new AnalyserNode(context, { fftSize: 100 }); // non-power of two
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ analyser = new AnalyserNode(context, { fftSize: 2049 });
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ analyser = new AnalyserNode(context, { fftSize: 4097 });
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ analyser = new AnalyserNode(context, { fftSize: 8193 });
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ analyser = new AnalyserNode(context, { fftSize: 16385 });
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ analyser = new AnalyserNode(context, { fftSize: 32769 });
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ analyser = new AnalyserNode(context, { fftSize: 65536 });
+ }, DOMException.INDEX_SIZE_ERR);
+ analyser = new AnalyserNode(context, { fftSize: 1024 });
+ is(analyser.frequencyBinCount, 512, "Correct new value for frequencyBinCount");
+
+ expectException(function() {
+ analyser = new AnalyserNode(context, { minDecibels: -30 });
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ analyser = new AnalyserNode(context, { minDecibels: -29 });
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ analyser = new AnalyserNode(context, { maxDecibels: -100 });
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ analyser = new AnalyserNode(context, { maxDecibels: -101 });
+ }, DOMException.INDEX_SIZE_ERR);
+
+ expectException(function() {
+ analyser = new AnalyserNode(context, { smoothingTimeConstant: -0.1 });
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ analyser = new AnalyserNode(context, { smoothingTimeConstant: -1.1 });
+ }, DOMException.INDEX_SIZE_ERR);
+ analyser = new AnalyserNode(context, { smoothingTimeConstant: 0 });
+ analyser = new AnalyserNode(context, { smoothingTimeConstant: 1 });
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+
+ testNode();
+ testConstructor();
+
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_analyserNodeMinimum.html b/dom/media/webaudio/test/test_analyserNodeMinimum.html
new file mode 100644
index 0000000000..950fd1812b
--- /dev/null
+++ b/dom/media/webaudio/test/test_analyserNodeMinimum.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AnalyserNode when the input is silent</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+
+ var ac = new AudioContext();
+ var analyser = ac.createAnalyser();
+ var constant = ac.createConstantSource();
+ var sp = ac.createScriptProcessor(2048, 1, 0);
+
+ constant.offset.value = 0.0;
+
+ constant.connect(analyser)
+ .connect(ac.destination);
+
+ constant.connect(sp);
+
+ var buf = new Float32Array(analyser.frequencyBinCount);
+ var iteration_count = 10;
+ sp.onaudioprocess = function() {
+ analyser.getFloatFrequencyData(buf);
+ var correct = true;
+ for (var i = 0; i < buf.length; i++) {
+ correct &= buf[i] == -Infinity;
+ }
+ ok(correct, "silent input process -Infinity in decibel bins");
+ if(!(iteration_count--)) {
+ sp.onaudioprocess = null;
+ constant.stop();
+ ac.close();
+ SimpleTest.finish();
+ }
+ }
+
+ constant.start();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_analyserNodeOutput.html b/dom/media/webaudio/test/test_analyserNodeOutput.html
new file mode 100644
index 0000000000..27b354e92d
--- /dev/null
+++ b/dom/media/webaudio/test/test_analyserNodeOutput.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AnalyserNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 2048,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var source = context.createBufferSource();
+
+ var analyser = context.createAnalyser();
+
+ source.buffer = this.buffer;
+
+ source.connect(analyser);
+
+ source.start(0);
+ return analyser;
+ },
+ createExpectedBuffers(context) {
+ this.buffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ this.buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ }
+
+ return [this.buffer];
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_analyserNodePassThrough.html b/dom/media/webaudio/test/test_analyserNodePassThrough.html
new file mode 100644
index 0000000000..50ff94a8c7
--- /dev/null
+++ b/dom/media/webaudio/test/test_analyserNodePassThrough.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AnalyserNode with passthrough</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 2048,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var source = context.createBufferSource();
+
+ var analyser = context.createAnalyser();
+
+ source.buffer = this.buffer;
+
+ source.connect(analyser);
+
+ var analyserWrapped = SpecialPowers.wrap(analyser);
+ ok("passThrough" in analyserWrapped, "AnalyserNode should support the passThrough API");
+ analyserWrapped.passThrough = true;
+
+ source.start(0);
+ return analyser;
+ },
+ createExpectedBuffers(context) {
+ this.buffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ this.buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ }
+
+ return [this.buffer];
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_analyserNodeWithGain.html b/dom/media/webaudio/test/test_analyserNodeWithGain.html
new file mode 100644
index 0000000000..fa0a2caa75
--- /dev/null
+++ b/dom/media/webaudio/test/test_analyserNodeWithGain.html
@@ -0,0 +1,47 @@
+<!DOCTYPE html>
+<title>Test effect of AnalyserNode on GainNode output</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+promise_test(function() {
+ // fftSize <= bufferSize so that the time domain data is full of input after
+ // processing the buffer.
+ const fftSize = 32;
+ const bufferSize = 128;
+
+ var context = new OfflineAudioContext(1, bufferSize, 48000);
+
+ var analyser1 = context.createAnalyser();
+ analyser1.fftSize = fftSize;
+ analyser1.connect(context.destination);
+ var analyser2 = context.createAnalyser();
+ analyser2.fftSize = fftSize;
+
+ var gain = context.createGain();
+ gain.gain.value = 2.0;
+ gain.connect(analyser1);
+ gain.connect(analyser2);
+
+ // Create a DC input to make getFloatTimeDomainData() output consistent at
+ // any time.
+ var buffer = context.createBuffer(1, 1, context.sampleRate);
+ buffer.getChannelData(0)[0] = 1.0 / gain.gain.value;
+ var source = context.createBufferSource();
+ source.buffer = buffer;
+ source.loop = true;
+ source.connect(gain);
+ source.start();
+
+ return context.startRendering().
+ then(function(buffer) {
+ assert_equals(buffer.getChannelData(0)[0], 1.0,
+ "analyser1 output");
+
+ var data = new Float32Array(1);
+ analyser1.getFloatTimeDomainData(data);
+ assert_equals(data[0], 1.0, "analyser1 time domain data");
+ analyser2.getFloatTimeDomainData(data);
+ assert_equals(data[0], 1.0, "analyser2 time domain data");
+ });
+});
+</script>
diff --git a/dom/media/webaudio/test/test_analyserScale.html b/dom/media/webaudio/test/test_analyserScale.html
new file mode 100644
index 0000000000..f11e4f2b28
--- /dev/null
+++ b/dom/media/webaudio/test/test_analyserScale.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AnalyserNode when the input is scaled </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+
+ var context = new AudioContext();
+
+ var gain = context.createGain();
+ var analyser = context.createAnalyser();
+ var osc = context.createOscillator();
+
+
+ osc.connect(gain);
+ gain.connect(analyser);
+
+ osc.start();
+
+ var array = new Uint8Array(analyser.frequencyBinCount);
+
+ function getAnalyserData() {
+ gain.gain.setValueAtTime(currentGain, context.currentTime);
+ analyser.getByteTimeDomainData(array);
+ var inrange = true;
+ var max = -1;
+ for (var i = 0; i < array.length; i++) {
+ if (array[i] > max) {
+ max = Math.abs(array[i] - 128);
+ }
+ }
+ if (max <= currentGain * 128) {
+ ok(true, "Analyser got scaled data for " + currentGain);
+ currentGain = tests.shift();
+ if (currentGain == undefined) {
+ SimpleTest.finish();
+ return;
+ }
+ }
+ requestAnimationFrame(getAnalyserData);
+ }
+
+ var tests = [1.0, 0.5, 0.0];
+ var currentGain = tests.shift();
+ requestAnimationFrame(getAnalyserData);
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_audioBufferSourceNode.html b/dom/media/webaudio/test/test_audioBufferSourceNode.html
new file mode 100644
index 0000000000..fc7c0b48d1
--- /dev/null
+++ b/dom/media/webaudio/test/test_audioBufferSourceNode.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AudioBufferSourceNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 4096,
+ createGraph(context) {
+ var buffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ }
+
+ var source = context.createBufferSource();
+ source.start(0);
+ source.buffer = buffer;
+ return source;
+ },
+ createExpectedBuffers(context) {
+ var buffers = [];
+ var buffer = context.createBuffer(2, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ buffer.getChannelData(1)[i] = buffer.getChannelData(0)[i];
+ }
+ buffers.push(buffer);
+ buffers.push(getEmptyBuffer(context, 2048));
+ return buffers;
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_audioBufferSourceNodeDetached.html b/dom/media/webaudio/test/test_audioBufferSourceNodeDetached.html
new file mode 100644
index 0000000000..e84c33e585
--- /dev/null
+++ b/dom/media/webaudio/test/test_audioBufferSourceNodeDetached.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AudioBufferSourceNode when an AudioBuffer's getChanneData buffer is detached</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+function createGarbage() {
+ var s = [];
+ for (var i = 0; i < 10000000; ++i) {
+ s.push(i);
+ }
+ var sum = 0;
+ for (var i = 0; i < s.length; ++i) {
+ sum += s[i];
+ }
+ return sum;
+}
+
+var worker = new Worker("audioBufferSourceNodeDetached_worker.js");
+
+var gTest = {
+ length: 2048,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var buffer = context.createBuffer(1, 10000000, context.sampleRate);
+ var data = buffer.getChannelData(0);
+ for (var i = 0; i < data.length; ++i) {
+ data[i] = (i%100)/100 - 0.5;
+ }
+
+ // Detach the buffer now
+ var data = buffer.getChannelData(0).buffer;
+ worker.postMessage(data, [data]);
+ // Create garbage and GC to replace the buffer data with garbage
+ SpecialPowers.gc();
+ createGarbage();
+ SpecialPowers.gc();
+
+ var source = context.createBufferSource();
+ source.buffer = buffer;
+ source.start();
+ // This should play silence
+ return source;
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_audioBufferSourceNodeEnded.html b/dom/media/webaudio/test/test_audioBufferSourceNodeEnded.html
new file mode 100644
index 0000000000..a11bb880a2
--- /dev/null
+++ b/dom/media/webaudio/test/test_audioBufferSourceNodeEnded.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test ended event on AudioBufferSourceNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var context = new AudioContext();
+ var buffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ }
+
+ var source = context.createBufferSource();
+
+ source.onended = function(e) {
+ is(e.target, source, "Correct target for the ended event");
+ SimpleTest.finish();
+ };
+
+ source.start(0);
+ source.buffer = buffer;
+ source.connect(context.destination);
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_audioBufferSourceNodeLazyLoopParam.html b/dom/media/webaudio/test/test_audioBufferSourceNodeLazyLoopParam.html
new file mode 100644
index 0000000000..757d4487c4
--- /dev/null
+++ b/dom/media/webaudio/test/test_audioBufferSourceNodeLazyLoopParam.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AudioBufferSourceNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 4096,
+ numberOfChannels: 1,
+ createGraph(context) {
+ // silence for half of the buffer, ones after that.
+ var buffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 1024; i < 2048; i++) {
+ buffer.getChannelData(0)[i] = 1;
+ }
+
+ var source = context.createBufferSource();
+
+ // we start at the 1024 frames, we should only have ones.
+ source.loop = true;
+ source.loopStart = 1024 / context.sampleRate;
+ source.loopEnd = 2048 / context.sampleRate;
+ source.buffer = buffer;
+ source.start(0, 1024 / context.sampleRate, 2048 / context.sampleRate);
+ return source;
+ },
+ createExpectedBuffers(context) {
+ var expectedBuffer = context.createBuffer(1, 4096, context.sampleRate);
+ for (var i = 0; i < 2048; i++) {
+ expectedBuffer.getChannelData(0)[i] = 1;
+ }
+ return expectedBuffer;
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_audioBufferSourceNodeLoop.html b/dom/media/webaudio/test/test_audioBufferSourceNodeLoop.html
new file mode 100644
index 0000000000..10d5d99108
--- /dev/null
+++ b/dom/media/webaudio/test/test_audioBufferSourceNodeLoop.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AudioBufferSourceNode looping</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 2048 * 4,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var buffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ }
+
+ var source = context.createBufferSource();
+ source.buffer = buffer;
+
+ source.start(0);
+ source.loop = true;
+ return source;
+ },
+ createExpectedBuffers(context) {
+ var expectedBuffer = context.createBuffer(1, 2048 * 4, context.sampleRate);
+ for (var i = 0; i < 4; ++i) {
+ for (var j = 0; j < 2048; ++j) {
+ expectedBuffer.getChannelData(0)[i * 2048 + j] = Math.sin(440 * 2 * Math.PI * j / context.sampleRate);
+ }
+ }
+ return expectedBuffer;
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_audioBufferSourceNodeLoopStartEnd.html b/dom/media/webaudio/test/test_audioBufferSourceNodeLoopStartEnd.html
new file mode 100644
index 0000000000..1ef08e0b83
--- /dev/null
+++ b/dom/media/webaudio/test/test_audioBufferSourceNodeLoopStartEnd.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AudioBufferSourceNode looping</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 2048 * 4,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var buffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ }
+
+ var source = new AudioBufferSourceNode(context, {buffer, loop: true, loopStart: buffer.duration * 0.25, loopEnd: buffer.duration * 0.75 });
+ source.start(0);
+ return source;
+ },
+ createExpectedBuffers(context) {
+ var expectedBuffer = context.createBuffer(1, 2048 * 4, context.sampleRate);
+ for (var i = 0; i < 1536; ++i) {
+ expectedBuffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ }
+ for (var i = 0; i < 6; ++i) {
+ for (var j = 512; j < 1536; ++j) {
+ expectedBuffer.getChannelData(0)[1536 + i * 1024 + j - 512] = Math.sin(440 * 2 * Math.PI * j / context.sampleRate);
+ }
+ }
+ for (var j = 7680; j < 2048 * 4; ++j) {
+ expectedBuffer.getChannelData(0)[j] = Math.sin(440 * 2 * Math.PI * (j - 7168) / context.sampleRate);
+ }
+ return expectedBuffer;
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_audioBufferSourceNodeLoopStartEndSame.html b/dom/media/webaudio/test/test_audioBufferSourceNodeLoopStartEndSame.html
new file mode 100644
index 0000000000..cfe054f838
--- /dev/null
+++ b/dom/media/webaudio/test/test_audioBufferSourceNodeLoopStartEndSame.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AudioBufferSourceNode looping</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 2048,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var buffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ }
+
+ var source = context.createBufferSource();
+ source.buffer = buffer;
+
+ source.loop = true;
+ source.loopStart = source.loopEnd = 1 / context.sampleRate;
+ source.start(0);
+ return source;
+ },
+ createExpectedBuffers(context) {
+ var buffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ }
+ return buffer;
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_audioBufferSourceNodeNoStart.html b/dom/media/webaudio/test/test_audioBufferSourceNodeNoStart.html
new file mode 100644
index 0000000000..e9a0472e2a
--- /dev/null
+++ b/dom/media/webaudio/test/test_audioBufferSourceNodeNoStart.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AudioBufferSourceNode when start() is not called</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 2048,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var buffer = context.createBuffer(1, 2048, context.sampleRate);
+ var data = buffer.getChannelData(0);
+ for (var i = 0; i < data.length; ++i) {
+ data[i] = (i%100)/100 - 0.5;
+ }
+ var source = context.createBufferSource();
+ source.buffer = buffer;
+ return source;
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_audioBufferSourceNodeNullBuffer.html b/dom/media/webaudio/test/test_audioBufferSourceNodeNullBuffer.html
new file mode 100644
index 0000000000..b0b405b366
--- /dev/null
+++ b/dom/media/webaudio/test/test_audioBufferSourceNodeNullBuffer.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AudioBufferSourceNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 2048,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var source = context.createBufferSource();
+
+ source.start(0);
+ source.buffer = null;
+ is(source.buffer, null, "Try playing back a null buffer");
+ return source;
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_audioBufferSourceNodeOffset.html b/dom/media/webaudio/test/test_audioBufferSourceNodeOffset.html
new file mode 100644
index 0000000000..0411b74ce5
--- /dev/null
+++ b/dom/media/webaudio/test/test_audioBufferSourceNodeOffset.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test the offset property on AudioBufferSourceNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var fuzz = 0.3;
+
+if (navigator.platform.startsWith("Mac")) {
+ // bug 895720
+ fuzz = 0.6;
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var samplesFromSource = 0;
+ var context = new AudioContext();
+ var sp = context.createScriptProcessor(256);
+
+ sp.onaudioprocess = function(e) {
+ samplesFromSource += e.inputBuffer.length;
+ }
+
+ var buffer = context.createBuffer(1, context.sampleRate, context.sampleRate);
+ for (var i = 0; i < context.sampleRate; ++i) {
+ buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ }
+
+ var source = context.createBufferSource();
+
+ source.onended = function(e) {
+ // The timing at which the audioprocess and ended listeners are called can
+ // change, hence the fuzzy equal here.
+ var errorRatio = samplesFromSource / (0.5 * context.sampleRate);
+ ok(errorRatio > (1.0 - fuzz) && errorRatio < (1.0 + fuzz),
+ "Correct number of samples received (expected: " +
+ (0.5 * context.sampleRate) + ", actual: " + samplesFromSource + ").");
+ SimpleTest.finish();
+ };
+
+ source.buffer = buffer;
+ source.connect(sp);
+ source.start(0, 0.5);
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_audioBufferSourceNodePassThrough.html b/dom/media/webaudio/test/test_audioBufferSourceNodePassThrough.html
new file mode 100644
index 0000000000..6cb0cccf99
--- /dev/null
+++ b/dom/media/webaudio/test/test_audioBufferSourceNodePassThrough.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AudioBufferSourceNode with passthrough</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 2048,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var buffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ }
+
+ var source = context.createBufferSource();
+
+ source.buffer = buffer;
+
+ var srcWrapped = SpecialPowers.wrap(source);
+ ok("passThrough" in srcWrapped, "AudioBufferSourceNode should support the passThrough API");
+ srcWrapped.passThrough = true;
+
+ source.start(0);
+ return source;
+ },
+ createExpectedBuffers(context) {
+ var expectedBuffer = context.createBuffer(1, 2048, context.sampleRate);
+
+ return [expectedBuffer];
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_audioBufferSourceNodeRate.html b/dom/media/webaudio/test/test_audioBufferSourceNodeRate.html
new file mode 100644
index 0000000000..85049bfd6d
--- /dev/null
+++ b/dom/media/webaudio/test/test_audioBufferSourceNodeRate.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AudioBufferSourceNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+var rate = 44100;
+var off = new OfflineAudioContext(1, rate, rate);
+var off2 = new OfflineAudioContext(1, rate, rate);
+
+var source = off.createBufferSource();
+var source2 = off2.createBufferSource();
+
+// a buffer of a 440Hz at half the length. If we detune by -1200 or set the
+// playbackRate to 0.5, we should get 44100 samples back with a sine at 220Hz.
+var buf = off.createBuffer(1, rate / 2, rate);
+var bufarray = buf.getChannelData(0);
+for (var i = 0; i < bufarray.length; i++) {
+ bufarray[i] = Math.sin(i * 440 * 2 * Math.PI / rate);
+}
+
+source.buffer = buf;
+source.playbackRate.value = 0.5; // 50% slowdown
+source.connect(off.destination);
+source.start(0);
+
+source2.buffer = buf;
+source2.detune.value = -1200; // one octave -> 50% slowdown
+source2.connect(off2.destination);
+source2.start(0);
+
+off.startRendering().then((renderedPlaybackRate) => {
+ // we don't care about comparing the value here, we just want to know whether
+ // the second part is noisy.
+ var rmsValue = rms(renderedPlaybackRate, 0, 22050);
+ ok(rmsValue != 0, "Resampling happened (rms of the second part " + rmsValue + ")");
+
+ off2.startRendering().then((renderedDetune) => {
+ var rmsValue = rms(renderedDetune, 0, 22050);
+ ok(rmsValue != 0, "Resampling happened (rms of the second part " + rmsValue + ")");
+ // The two buffers should be the same: detune of -1200 is a 50% slowdown
+ compareBuffers(renderedPlaybackRate, renderedDetune);
+ SimpleTest.finish();
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_audioContextGC.html b/dom/media/webaudio/test/test_audioContextGC.html
new file mode 100644
index 0000000000..be9990cfad
--- /dev/null
+++ b/dom/media/webaudio/test/test_audioContextGC.html
@@ -0,0 +1,162 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test inactive AudioContext is garbage collected</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+let ids;
+
+const observer = (subject, topic, data) => {
+ const id = parseInt(data);
+ if (ids) {
+ ok(ids.delete(id), "Collected AudioNode id " + id);
+ }
+}
+SpecialPowers.addObserver(observer, "webaudio-node-demise");
+
+SimpleTest.registerCleanupFunction(function() {
+ if (observer) {
+ SpecialPowers.removeObserver(observer, "webaudio-node-demise");
+ }
+});
+
+function id(node) {
+ return SpecialPowers.getPrivilegedProps(node, "id");
+}
+
+let tests = [{
+ name: "Bare running AudioContext", setup: () => {
+ const ac = new AudioContext();
+ ids.add(id(ac.destination));
+ // Await state change notification before collection.
+ return new Promise((resolve) => {
+ ac.onstatechange = () => {
+ is(ac.state, "running", "ac.state");
+ resolve();
+ };
+ });
+ }
+}, {
+ name: "Stopped source", setup: () => {
+ const ac = new AudioContext();
+ ids.add(id(ac.destination));
+ const source = new ConstantSourceNode(ac);
+ ids.add(id(source));
+ source.start();
+ source.stop();
+ // Await ended notification before collection.
+ return new Promise((resolve) => {
+ source.onended = () => {
+ is(ac.state, "running", "ac.state");
+ resolve();
+ };
+ });
+ }
+}, {
+ name: "OfflineAudioContext not started", setup: () => {
+ const ac = new OfflineAudioContext({
+ numberOfChannels: 1, length: 1, sampleRate: 48000
+ });
+ ids.add(id(ac.destination));
+ const source = new ConstantSourceNode(ac);
+ ids.add(id(source));
+ source.start();
+ }
+}, {
+ name: "Completed OfflineAudioContext", setup: async () => {
+ const ac = new OfflineAudioContext({
+ numberOfChannels: 1, length: 1, sampleRate: 48000
+ });
+ ids.add(id(ac.destination));
+ const sourceBeforeStart = new ConstantSourceNode(ac);
+ ids.add(id(sourceBeforeStart));
+ sourceBeforeStart.start();
+ ac.startRendering();
+ await new Promise((resolve) => {
+ ac.oncomplete = () => {
+ resolve();
+ };
+ });
+ const sourceAfterComplete = new ConstantSourceNode(ac);
+ ids.add(id(sourceAfterComplete));
+ sourceAfterComplete.start();
+ }
+}, {
+ name: "suspended AudioContext", setup: async () => {
+ const ac = new AudioContext();
+ ids.add(id(ac.destination));
+ const sourceBeforeSuspend = new ConstantSourceNode(ac);
+ ids.add(id(sourceBeforeSuspend));
+ sourceBeforeSuspend.start();
+ ac.suspend();
+ const sourceAfterSuspend = new ConstantSourceNode(ac);
+ ids.add(id(sourceAfterSuspend));
+ sourceAfterSuspend.start();
+ await new Promise((resolve) => {
+ ac.onstatechange = () => {
+ if (ac.state == "suspended") {
+ resolve();
+ }
+ };
+ });
+ const sourceAfterSuspended = new ConstantSourceNode(ac);
+ ids.add(id(sourceAfterSuspended));
+ sourceAfterSuspended.start();
+ }
+}, {
+ name: "closed AudioContext", setup: async () => {
+ const ac = new AudioContext();
+ ids.add(id(ac.destination));
+ const sourceBeforeClose = new ConstantSourceNode(ac);
+ ids.add(id(sourceBeforeClose));
+ sourceBeforeClose.start();
+ ac.close();
+ const sourceAfterClose = new ConstantSourceNode(ac);
+ ids.add(id(sourceAfterClose));
+ sourceAfterClose.start();
+ await new Promise((resolve) => {
+ ac.onstatechange = () => {
+ if (ac.state == "closed") {
+ resolve();
+ }
+ };
+ });
+ const sourceAfterClosed = new ConstantSourceNode(ac);
+ ids.add(id(sourceAfterClosed));
+ sourceAfterClosed.start();
+ }
+}];
+
+const start_next_test = async () => {
+ const test = tests.shift();
+ if (!test) {
+ SimpleTest.finish();
+ return;
+ }
+ // Collect all audio nodes from previous tests.
+ if (!ids) {
+ await new Promise(resolve => {
+ SpecialPowers.exactGC(resolve);
+ });
+ }
+ ids = new Set();
+ await test.setup();
+ SpecialPowers.exactGC(() => {
+ is(ids.size, 0,
+ `All expected nodes for "${test.name}" should be collected`);
+ start_next_test();
+ });
+}
+
+start_next_test();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_audioContextParams_recordNonDefaultSampleRate.html b/dom/media/webaudio/test/test_audioContextParams_recordNonDefaultSampleRate.html
new file mode 100644
index 0000000000..8177202117
--- /dev/null
+++ b/dom/media/webaudio/test/test_audioContextParams_recordNonDefaultSampleRate.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script type="text/javascript" src="manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script class="testbody" type="text/javascript">
+function startTest() {
+ let ctx = new AudioContext({sampleRate: 32000});
+ oscillator = ctx.createOscillator();
+ let dest = ctx.createMediaStreamDestination();
+ oscillator.connect(dest);
+ oscillator.start();
+ let stream = dest.stream;
+
+ recorder = new MediaRecorder(stream);
+ recorder.ondataavailable = (e) => {
+ ok(true, 'recorder ondataavailable event');
+ if (recorder.state == 'recording') {
+ ok(e.data.size > 0, 'check blob has data');
+ recorder.stop();
+ }
+ }
+
+ recorder.onstop = () => {
+ ok(true, 'recorder stop event');
+ SimpleTest.finish();
+ }
+
+ try {
+ recorder.start(1000);
+ ok(true, 'recorder started');
+ is('recording', recorder.state, 'check record state recording');
+ } catch (e) {
+ ok(false, 'Can t record audio context');
+ }
+}
+
+startTest();
+SimpleTest.waitForExplicitFinish();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_audioContextParams_sampleRate.html b/dom/media/webaudio/test/test_audioContextParams_sampleRate.html
new file mode 100644
index 0000000000..280452403d
--- /dev/null
+++ b/dom/media/webaudio/test/test_audioContextParams_sampleRate.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script>
+createHTML({
+ title: "Parallel MTG by setting AudioContextParam sample rate",
+ bug: "1387454",
+ visible: true
+});
+
+runTest(async () => {
+ // Test an AudioContext of specific sample rate.
+ // Verify that the oscillator produces a tone.
+ const rate1 = 500;
+ const ac1 = new AudioContext({sampleRate: 44100});
+ const dest_ac1 = ac1.createMediaStreamDestination();
+ const osc_ac1 = ac1.createOscillator();
+ osc_ac1.frequency.value = rate1;
+ osc_ac1.connect(dest_ac1);
+ osc_ac1.start(0);
+
+ const analyser = new AudioStreamAnalyser(ac1, dest_ac1.stream);
+ analyser.enableDebugCanvas();
+ await analyser.waitForAnalysisSuccess( array => {
+ const freg_50Hz = array[analyser.binIndexForFrequency(50)];
+ const freq_rate1 = array[analyser.binIndexForFrequency(rate1)];
+ const freq_4000Hz = array[analyser.binIndexForFrequency(4000)];
+
+ info("Analysing audio frequency - low:target1:high = "
+ + freg_50Hz + ':' + freq_rate1 + ':' + freq_4000Hz);
+ return freg_50Hz < 50 && freq_rate1 > 200 && freq_4000Hz < 50;
+ })
+ osc_ac1.stop();
+
+ // Same test using a new AudioContext of different sample rate.
+ const rate2 = 1500;
+ const ac2 = new AudioContext({sampleRate: 48000});
+ const dest_ac2 = ac2.createMediaStreamDestination();
+ const osc_ac2 = ac2.createOscillator();
+ osc_ac2.frequency.value = rate2;
+ osc_ac2.connect(dest_ac2);
+ osc_ac2.start(0);
+
+ const analyser2 = new AudioStreamAnalyser(ac2, dest_ac2.stream);
+ analyser2.enableDebugCanvas();
+ await analyser2.waitForAnalysisSuccess( array => {
+ const freg_50Hz = array[analyser2.binIndexForFrequency(50)];
+ const freq_rate2 = array[analyser2.binIndexForFrequency(rate2)];
+ const freq_4000Hz = array[analyser2.binIndexForFrequency(4000)];
+
+ info("Analysing audio frequency - low:target2:high = "
+ + freg_50Hz + ':' + freq_rate2 + ':' + freq_4000Hz);
+ return freg_50Hz < 50 && freq_rate2 > 200 && freq_4000Hz < 50;
+ })
+ osc_ac2.stop();
+
+ // Two AudioContexts with different sample rate cannot communicate.
+ mustThrowWith("Connect nodes with different sample rate", "NotSupportedError",
+ () => ac2.createMediaStreamSource(dest_ac1.stream));
+
+ // Two AudioContext with the same sample rate can communicate.
+ const ac3 = new AudioContext({sampleRate: 48000});
+ const dest_ac3 = ac3.createMediaStreamDestination();
+ const source_ac2 = ac2.createMediaStreamSource(dest_ac3.stream);
+ ok(true, "Connect nodes with the same sample rate is ok");
+
+ mustThrowWith("Invalid zero samplerate", "NotSupportedError",
+ () => new AudioContext({sampleRate: 0}));
+
+ mustThrowWith("Invalid negative samplerate", "NotSupportedError",
+ () => new AudioContext({sampleRate: -1}));
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_audioContextSuspendResumeClose.html b/dom/media/webaudio/test/test_audioContextSuspendResumeClose.html
new file mode 100644
index 0000000000..36cf8f720c
--- /dev/null
+++ b/dom/media/webaudio/test/test_audioContextSuspendResumeClose.html
@@ -0,0 +1,419 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test suspend, resume and close method of the AudioContext</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+function tryToCreateNodeOnClosedContext(ctx) {
+ is(ctx.state, "closed", "The context is in closed state");
+
+ [ { name: "createBufferSource" },
+ { name: "createMediaStreamDestination",
+ onOfflineAudioContext: false},
+ { name: "createScriptProcessor" },
+ { name: "createStereoPanner" },
+ { name: "createAnalyser" },
+ { name: "createGain" },
+ { name: "createDelay" },
+ { name: "createBiquadFilter" },
+ { name: "createWaveShaper" },
+ { name: "createPanner" },
+ { name: "createConvolver" },
+ { name: "createChannelSplitter" },
+ { name: "createChannelMerger" },
+ { name: "createDynamicsCompressor" },
+ { name: "createOscillator" },
+ { name: "createMediaElementSource",
+ args: [new Audio()],
+ onOfflineAudioContext: false },
+ { name: "createMediaStreamSource",
+ args: [(new AudioContext()).createMediaStreamDestination().stream],
+ onOfflineAudioContext: false } ].forEach(function(e) {
+
+ if (e.onOfflineAudioContext == false &&
+ ctx instanceof OfflineAudioContext) {
+ return;
+ }
+
+ expectNoException(function() {
+ ctx[e.name].apply(ctx, e.args);
+ }, DOMException.INVALID_STATE_ERR);
+ });
+}
+
+function loadFile(url, callback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", url, true);
+ xhr.responseType = "arraybuffer";
+ xhr.onload = function() {
+ callback(xhr.response);
+ };
+ xhr.send();
+}
+
+// createBuffer, createPeriodicWave and decodeAudioData should work on a context
+// that has `state` == "closed"
+function tryLegalOpeerationsOnClosedContext(ctx) {
+ is(ctx.state, "closed", "The context is in closed state");
+
+ [ { name: "createBuffer",
+ args: [1, 44100, 44100] },
+ { name: "createPeriodicWave",
+ args: [new Float32Array(10), new Float32Array(10)] }
+ ].forEach(function(e) {
+ expectNoException(function() {
+ ctx[e.name].apply(ctx, e.args);
+ });
+ });
+ loadFile("ting-44.1k-1ch.ogg", function(buf) {
+ ctx.decodeAudioData(buf).then(function(decodedBuf) {
+ ok(true, "decodeAudioData on a closed context should work, it did.")
+ finish();
+ }).catch(function(e){
+ ok(false, "decodeAudioData on a closed context should work, it did not");
+ finish();
+ });
+ });
+}
+
+// Test that MediaStreams that are the output of a suspended AudioContext are
+// producing silence
+// ac1 produce a sine fed to a MediaStreamAudioDestinationNode
+// ac2 is connected to ac1 with a MediaStreamAudioSourceNode, and check that
+// there is silence when ac1 is suspended
+function testMultiContextOutput() {
+ var ac1 = new AudioContext(),
+ ac2 = new AudioContext();
+
+ ac1.onstatechange = function() {
+ ac1.onstatechange = null;
+
+ var osc1 = ac1.createOscillator(),
+ mediaStreamDestination1 = ac1.createMediaStreamDestination();
+
+ var mediaStreamAudioSourceNode2 =
+ ac2.createMediaStreamSource(mediaStreamDestination1.stream),
+ sp2 = ac2.createScriptProcessor(),
+ silentBuffersInARow = 0;
+
+
+ sp2.onaudioprocess = function(e) {
+ ac1.suspend().then(function() {
+ is(ac1.state, "suspended", "ac1 is suspended");
+ sp2.onaudioprocess = checkSilence;
+ });
+ sp2.onaudioprocess = null;
+ }
+
+ function checkSilence(e) {
+ var input = e.inputBuffer.getChannelData(0);
+ var silent = true;
+ for (var i = 0; i < input.length; i++) {
+ if (input[i] != 0.0) {
+ silent = false;
+ }
+ }
+
+ if (silent) {
+ silentBuffersInARow++;
+ if (silentBuffersInARow == 10) {
+ ok(true,
+ "MediaStreams produce silence when their input is blocked.");
+ sp2.onaudioprocess = null;
+ ac1.close();
+ ac2.close();
+ finish();
+ }
+ } else {
+ is(silentBuffersInARow, 0,
+ "No non silent buffer inbetween silent buffers.");
+ }
+ }
+
+ osc1.connect(mediaStreamDestination1);
+
+ mediaStreamAudioSourceNode2.connect(sp2);
+ osc1.start();
+ }
+}
+
+
+// Test that there is no buffering between contexts when connecting a running
+// AudioContext to a suspended AudioContext. Our ScriptProcessorNode does some
+// buffering internally, so we ensure this by using a very very low frequency
+// on a sine, and oberve that the phase has changed by a big enough margin.
+function testMultiContextInput() {
+ var ac1 = new AudioContext(),
+ ac2 = new AudioContext();
+
+ ac1.onstatechange = function() {
+ ac1.onstatechange = null;
+
+ var osc1 = ac1.createOscillator(),
+ mediaStreamDestination1 = ac1.createMediaStreamDestination(),
+ sp1 = ac1.createScriptProcessor();
+
+ var mediaStreamAudioSourceNode2 =
+ ac2.createMediaStreamSource(mediaStreamDestination1.stream),
+ sp2 = ac2.createScriptProcessor(),
+ eventReceived = 0;
+
+
+ osc1.frequency.value = 0.0001;
+
+ function checkDiscontinuity(e) {
+ var inputBuffer = e.inputBuffer.getChannelData(0);
+ if (eventReceived++ == 3) {
+ var delta = Math.abs(inputBuffer[1] - sp2.value),
+ theoreticalIncrement = 2048 * 3 * Math.PI * 2 * osc1.frequency.value / ac1.sampleRate;
+ ok(delta >= theoreticalIncrement,
+ "Buffering did not occur when the context was suspended (delta:" + delta + " increment: " + theoreticalIncrement+")");
+ ac1.close();
+ ac2.close();
+ sp1.onaudioprocess = null;
+ sp2.onaudioprocess = null;
+ finish();
+ }
+ }
+
+ sp2.onaudioprocess = function(e) {
+ var inputBuffer = e.inputBuffer.getChannelData(0);
+ sp2.value = inputBuffer[inputBuffer.length - 1];
+ ac2.suspend().then(function() {
+ ac2.resume().then(function() {
+ sp2.onaudioprocess = checkDiscontinuity;
+ });
+ });
+ }
+
+ osc1.connect(mediaStreamDestination1);
+ osc1.connect(sp1);
+
+ mediaStreamAudioSourceNode2.connect(sp2);
+ osc1.start();
+ }
+}
+
+// Test that ScriptProcessorNode's onaudioprocess don't get called while the
+// context is suspended/closed. It is possible that we get the handler called
+// exactly once after suspend, because the event has already been sent to the
+// event loop.
+function testScriptProcessNodeSuspended() {
+ var ac = new AudioContext();
+ var sp = ac.createScriptProcessor();
+ var remainingIterations = 30;
+ var afterResume = false;
+ ac.onstatechange = function() {
+ ac.onstatechange = null;
+ sp.onaudioprocess = function() {
+ ok(ac.state == "running", "If onaudioprocess is called, the context" +
+ " must be running (was " + ac.state + ", remainingIterations:" + remainingIterations +")");
+ remainingIterations--;
+ if (!afterResume) {
+ if (remainingIterations == 0) {
+ ac.suspend().then(function() {
+ ac.resume().then(function() {
+ remainingIterations = 30;
+ afterResume = true;
+ });
+ });
+ }
+ } else {
+ sp.onaudioprocess = null;
+ finish();
+ }
+ }
+ }
+ sp.connect(ac.destination);
+}
+
+// Take an AudioContext, make sure it switches to running when the audio starts
+// flowing, and then, call suspend, resume and close on it, tracking its state.
+function testAudioContext() {
+ var ac = new AudioContext();
+ is(ac.state, "suspended", "AudioContext should start in suspended state.");
+ var stateTracker = {
+ previous: ac.state,
+ // no promise for the initial suspended -> running
+ initial: { handler: false },
+ suspend: { promise: false, handler: false },
+ resume: { promise: false, handler: false },
+ close: { promise: false, handler: false }
+ };
+
+ function initialSuspendToRunning() {
+ ok(stateTracker.previous == "suspended" &&
+ ac.state == "running",
+ "AudioContext should switch to \"running\" when the audio hardware is" +
+ " ready.");
+
+ stateTracker.previous = ac.state;
+ ac.onstatechange = afterSuspend;
+ stateTracker.initial.handler = true;
+
+ ac.suspend().then(function() {
+ ok(!stateTracker.suspend.promise && !stateTracker.suspend.handler,
+ "Promise should be resolved before the callback, and only once.")
+ stateTracker.suspend.promise = true;
+ });
+ }
+
+ function afterSuspend() {
+ ok(stateTracker.previous == "running" &&
+ ac.state == "suspended",
+ "AudioContext should switch to \"suspend\" when the audio stream is" +
+ "suspended.");
+ ok(stateTracker.suspend.promise && !stateTracker.suspend.handler,
+ "Handler should be called after the callback, and only once");
+
+ stateTracker.suspend.handler = true;
+ stateTracker.previous = ac.state;
+ ac.onstatechange = afterResume;
+
+ ac.resume().then(function() {
+ ok(!stateTracker.resume.promise && !stateTracker.resume.handler,
+ "Promise should be called before the callback, and only once");
+ stateTracker.resume.promise = true;
+ });
+ }
+
+ function afterResume() {
+ ok(stateTracker.previous == "suspended" &&
+ ac.state == "running",
+ "AudioContext should switch to \"running\" when the audio stream resumes.");
+
+ ok(stateTracker.resume.promise && !stateTracker.resume.handler,
+ "Handler should be called after the callback, and only once");
+
+ stateTracker.resume.handler = true;
+ stateTracker.previous = ac.state;
+ ac.onstatechange = afterClose;
+
+ ac.close().then(function() {
+ ok(!stateTracker.close.promise && !stateTracker.close.handler,
+ "Promise should be called before the callback, and only once");
+ stateTracker.close.promise = true;
+ tryToCreateNodeOnClosedContext(ac);
+ tryLegalOpeerationsOnClosedContext(ac);
+ });
+ }
+
+ function afterClose() {
+ ok(stateTracker.previous == "running" &&
+ ac.state == "closed",
+ "AudioContext should switch to \"closed\" when the audio stream is" +
+ " closed.");
+ ok(stateTracker.close.promise && !stateTracker.close.handler,
+ "Handler should be called after the callback, and only once");
+ }
+
+ ac.onstatechange = initialSuspendToRunning;
+}
+
+function testOfflineAudioContext() {
+ var o = new OfflineAudioContext(1, 44100, 44100);
+ is(o.state, "suspended", "OfflineAudioContext should start in suspended state.");
+
+ expectRejectedPromise(o, "resume", "NotSupportedError");
+
+ var previousState = o.state,
+ finishedRendering = false;
+ function beforeStartRendering() {
+ ok(previousState == "suspended" && o.state == "running", "onstatechanged" +
+ "handler is called on state changed, and the new state is running");
+ previousState = o.state;
+ o.onstatechange = onRenderingFinished;
+ }
+
+ function onRenderingFinished() {
+ ok(previousState == "running" && o.state == "closed",
+ "onstatechanged handler is called when rendering finishes, " +
+ "and the new state is closed");
+ ok(finishedRendering, "The Promise that is resolved when the rendering is" +
+ "done should be resolved earlier than the state change.");
+ previousState = o.state;
+ o.onstatechange = afterRenderingFinished;
+
+ tryToCreateNodeOnClosedContext(o);
+ tryLegalOpeerationsOnClosedContext(o);
+ }
+
+ function afterRenderingFinished() {
+ ok(false, "There should be no transition out of the closed state.");
+ }
+
+ o.onstatechange = beforeStartRendering;
+
+ o.startRendering().then(function(buffer) {
+ finishedRendering = true;
+ });
+}
+
+function testSuspendResumeEventLoop() {
+ var ac = new AudioContext();
+ var source = ac.createBufferSource();
+ source.buffer = ac.createBuffer(1, 44100, 44100);
+ source.onended = function() {
+ ok(true, "The AudioContext did resume.");
+ finish();
+ }
+ ac.onstatechange = function() {
+ ac.onstatechange = null;
+
+ ok(ac.state == "running", "initial state is running");
+ ac.suspend();
+ source.start();
+ ac.resume();
+ }
+}
+
+function testResumeInStateChangeForResumeCallback() {
+ // Regression test for bug 1468085.
+ var ac = new AudioContext;
+ ac.onstatechange = function() {
+ ac.resume().then(() => {
+ ok(true, "resume promise resolved as expected.");
+ finish();
+ });
+ }
+}
+
+var remaining = 0;
+function finish() {
+ remaining--;
+ if (remaining == 0) {
+ SimpleTest.finish();
+ }
+}
+
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var tests = [
+ testOfflineAudioContext,
+ testScriptProcessNodeSuspended,
+ testMultiContextOutput,
+ testMultiContextInput,
+ testSuspendResumeEventLoop,
+ testResumeInStateChangeForResumeCallback
+ ];
+
+ // See Bug 1305136, many intermittent failures on Linux
+ if (!navigator.platform.startsWith("Linux")) {
+ tests.push(testAudioContext);
+ }
+
+ remaining = tests.length;
+ tests.forEach(function(f) { f() });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_audioDestinationNode.html b/dom/media/webaudio/test/test_audioDestinationNode.html
new file mode 100644
index 0000000000..e7a8b091ba
--- /dev/null
+++ b/dom/media/webaudio/test/test_audioDestinationNode.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AudioDestinationNode as EventTarget</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+var ac = new AudioContext()
+ac.destination.addEventListener("foo", function() {
+ ok(true, "Event received!");
+ SimpleTest.finish();
+});
+ac.destination.dispatchEvent(new CustomEvent("foo"));
+
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/media/webaudio/test/test_audioParamChaining.html b/dom/media/webaudio/test/test_audioParamChaining.html
new file mode 100644
index 0000000000..85b8099e2e
--- /dev/null
+++ b/dom/media/webaudio/test/test_audioParamChaining.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test whether we can create an AudioContext interface</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish()
+
+function frameToTime(frame, rate)
+{
+ return frame / rate;
+}
+
+const RATE = 44100;
+
+var oc = new OfflineAudioContext(1, 44100, RATE);
+// This allows us to have a source that is simply a DC offset.
+var source = oc.createBufferSource();
+var buf = oc.createBuffer(1, 1, RATE);
+buf.getChannelData(0)[0] = 1;
+source.loop = true;
+source.buffer = buf;
+
+source.start(0);
+
+var gain = oc.createGain();
+
+source.connect(gain).connect(oc.destination);
+
+var gain2 = oc.createGain();
+var rv2 = gain2.gain.linearRampToValueAtTime(0.1, 0.5);
+ok(rv2 instanceof AudioParam, "linearRampToValueAtTime returns an AudioParam.");
+ok(rv2 == gain2.gain, "linearRampToValueAtTime returns the right AudioParam.");
+
+rv2 = gain2.gain.exponentialRampToValueAtTime(0.01, 1.0);
+ok(rv2 instanceof AudioParam,
+ "exponentialRampToValueAtTime returns an AudioParam.");
+ok(rv2 == gain2.gain,
+ "exponentialRampToValueAtTime returns the right AudioParam.");
+
+rv2 = gain2.gain.setTargetAtTime(1.0, 2.0, 0.1);
+ok(rv2 instanceof AudioParam, "setTargetAtTime returns an AudioParam.");
+ok(rv2 == gain2.gain, "setTargetAtTime returns the right AudioParam.");
+
+var array = new Float32Array(10);
+rv2 = gain2.gain.setValueCurveAtTime(array, 10, 11);
+ok(rv2 instanceof AudioParam, "setValueCurveAtTime returns an AudioParam.");
+ok(rv2 == gain2.gain, "setValueCurveAtTime returns the right AudioParam.");
+
+// We chain three automation methods, making a gain step.
+var rv = gain.gain.setValueAtTime(0, frameToTime(0, RATE))
+ .setValueAtTime(0.5, frameToTime(22000, RATE))
+ .setValueAtTime(1, frameToTime(44000, RATE));
+
+ok(rv instanceof AudioParam, "setValueAtTime returns an AudioParam.");
+ok(rv == gain.gain, "setValueAtTime returns the right AudioParam.");
+
+oc.startRendering().then(function(rendered) {
+ console.log(rendered.getChannelData(0));
+ is(rendered.getChannelData(0)[0], 0,
+ "The value of the first step is correct.");
+ is(rendered.getChannelData(0)[22050], 0.5,
+ "The value of the second step is correct");
+ is(rendered.getChannelData(0)[44099], 1,
+ "The value of the third step is correct.");
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_audioParamExponentialRamp.html b/dom/media/webaudio/test/test_audioParamExponentialRamp.html
new file mode 100644
index 0000000000..2416b5de14
--- /dev/null
+++ b/dom/media/webaudio/test/test_audioParamExponentialRamp.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AudioParam.exponentialRampToValue</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var V0 = 0.1;
+var V1 = 0.9;
+var T0 = 0;
+
+var gTest = {
+ length: 2048,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var sourceBuffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ sourceBuffer.getChannelData(0)[i] = 1;
+ }
+
+ var source = context.createBufferSource();
+ source.buffer = sourceBuffer;
+
+ var gain = context.createGain();
+ gain.gain.setValueAtTime(V0, 0);
+ gain.gain.exponentialRampToValueAtTime(V1, 2048/context.sampleRate);
+
+ source.connect(gain);
+
+ source.start(0);
+ return gain;
+ },
+ createExpectedBuffers(context) {
+ var T1 = 2048 / context.sampleRate;
+ var expectedBuffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ var t = i / context.sampleRate;
+ expectedBuffer.getChannelData(0)[i] = V0 * Math.pow(V1 / V0, (t - T0) / (T1 - T0));
+ }
+ return expectedBuffer;
+ },
+};
+
+
+SimpleTest.waitForExplicitFinish();
+// Comparing different AudioContexts may result in different timing reated information being reported
+// when we jitter time, as they are on different Relative Timelines.
+SpecialPowers.pushPrefEnv({"set": [["privacy.resistFingerprinting.reduceTimerPrecision.jitter", false]]}, runTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_audioParamGain.html b/dom/media/webaudio/test/test_audioParamGain.html
new file mode 100644
index 0000000000..3977b94703
--- /dev/null
+++ b/dom/media/webaudio/test/test_audioParamGain.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AudioParam with pre-gain </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+var ctx = new AudioContext();
+var source = ctx.createOscillator();
+var lfo = ctx.createOscillator();
+var lfoIntensity = ctx.createGain();
+var effect = ctx.createGain();
+var sp = ctx.createScriptProcessor(2048, 1);
+
+source.frequency.value = 440;
+lfo.frequency.value = 2;
+// Very low gain, so the LFO should have very little influence
+// on the source, its RMS value should be close to the nominal value
+// for a sine wave.
+lfoIntensity.gain.value = 0.0001;
+
+lfo.connect(lfoIntensity);
+lfoIntensity.connect(effect.gain);
+source.connect(effect);
+effect.connect(sp);
+
+sp.onaudioprocess = function(e) {
+ var buffer = e.inputBuffer.getChannelData(0);
+ var rms = 0;
+ for (var i = 0; i < buffer.length; i++) {
+ rms += buffer[i] * buffer[i];
+ }
+
+ rms /= buffer.length;
+ rms = Math.sqrt(rms);
+
+ // 1 / Math.sqrt(2) is the theoretical RMS value for a sine wave.
+ ok(fuzzyCompare(rms, 1 / Math.sqrt(2)),
+ "Gain correctly applied to the AudioParam.");
+
+ ctx = null;
+ sp.onaudioprocess = null;
+ lfo.stop(0);
+ source.stop(0);
+
+ SimpleTest.finish();
+}
+
+lfo.start(0);
+source.start(0);
+
+</script>
+</pre>
+</body>
diff --git a/dom/media/webaudio/test/test_audioParamLinearRamp.html b/dom/media/webaudio/test/test_audioParamLinearRamp.html
new file mode 100644
index 0000000000..5ec26467e8
--- /dev/null
+++ b/dom/media/webaudio/test/test_audioParamLinearRamp.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AudioParam.linearRampToValue</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var V0 = 0.1;
+var V1 = 0.9;
+var T0 = 0;
+
+var gTest = {
+ length: 2048,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var sourceBuffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ sourceBuffer.getChannelData(0)[i] = 1;
+ }
+
+ var source = context.createBufferSource();
+ source.buffer = sourceBuffer;
+
+ var gain = context.createGain();
+ gain.gain.setValueAtTime(V0, 0);
+ gain.gain.linearRampToValueAtTime(V1, 2048/context.sampleRate);
+
+ source.connect(gain);
+
+ source.start(0);
+ return gain;
+ },
+ createExpectedBuffers(context) {
+ var T1 = 2048 / context.sampleRate;
+ var expectedBuffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ var t = i / context.sampleRate;
+ expectedBuffer.getChannelData(0)[i] = V0 + (V1 - V0) * ((t - T0) / (T1 - T0));
+ }
+ return expectedBuffer;
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_audioParamSetCurveAtTime.html b/dom/media/webaudio/test/test_audioParamSetCurveAtTime.html
new file mode 100644
index 0000000000..e21b58bb19
--- /dev/null
+++ b/dom/media/webaudio/test/test_audioParamSetCurveAtTime.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AudioParam.linearRampToValue</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var T0 = 0;
+
+var gTest = {
+ length: 2048,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var source = context.createConstantSource();
+
+ var gain = context.createGain();
+ gain.gain.setValueCurveAtTime(this.curve, T0, this.duration);
+ source.connect(gain);
+
+ source.start(0);
+ return gain;
+ },
+ createExpectedBuffers(context) {
+ this.duration = 1024 / context.sampleRate;
+ this.curve = new Float32Array([1.0, 0.5, 0.75, 0.25]);
+ var expectedBuffer = context.createBuffer(1, 2048, context.sampleRate);
+ var data = expectedBuffer.getChannelData(0);
+ var step = 1024 / 3;
+ for (var i = 0; i < 2048; ++i) {
+ if (i < step) {
+ data[i] = 1.0 - 0.5*i/step;
+ } else if (i < 2*step) {
+ data[i] = 0.5 + 0.25*(i - step)/step;
+ } else if (i < 3*step) {
+ data[i] = 0.75 - 0.5*(i - 2*step)/step;
+ } else {
+ data[i] = 0.25;
+ }
+ }
+ return expectedBuffer;
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_audioParamSetTargetAtTime.html b/dom/media/webaudio/test/test_audioParamSetTargetAtTime.html
new file mode 100644
index 0000000000..3328519f12
--- /dev/null
+++ b/dom/media/webaudio/test/test_audioParamSetTargetAtTime.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AudioParam.setTargetAtTime</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var V0 = 0.9;
+var V1 = 0.1;
+var T0 = 0;
+var TimeConstant = 10;
+
+var gTest = {
+ length: 2048,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var sourceBuffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ sourceBuffer.getChannelData(0)[i] = 1;
+ }
+
+ var source = context.createBufferSource();
+ source.buffer = sourceBuffer;
+
+ var gain = context.createGain();
+ gain.gain.value = V0;
+ gain.gain.setTargetAtTime(V1, T0, TimeConstant);
+
+ source.connect(gain);
+
+ source.start(0);
+ return gain;
+ },
+ createExpectedBuffers(context) {
+ var T1 = 2048 / context.sampleRate;
+ var expectedBuffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ var t = i / context.sampleRate;
+ expectedBuffer.getChannelData(0)[i] = V1 + (V0 - V1) * Math.exp(-(t - T0) / TimeConstant);
+ }
+ return expectedBuffer;
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_audioParamSetTargetAtTimeZeroTimeConstant.html b/dom/media/webaudio/test/test_audioParamSetTargetAtTimeZeroTimeConstant.html
new file mode 100644
index 0000000000..9982023c21
--- /dev/null
+++ b/dom/media/webaudio/test/test_audioParamSetTargetAtTimeZeroTimeConstant.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AudioParam.setTargetAtTime with zero time constant</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var V0 = 0.9;
+var V1 = 0.1;
+var T0 = 0;
+var TimeConstant = 0;
+
+var gTest = {
+ length: 2048,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var sourceBuffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ sourceBuffer.getChannelData(0)[i] = 1;
+ }
+
+ var source = context.createBufferSource();
+ source.buffer = sourceBuffer;
+
+ var gain = context.createGain();
+ gain.gain.value = V0;
+ gain.gain.setTargetAtTime(V1, T0, TimeConstant);
+
+ source.connect(gain);
+
+ source.start(0);
+ return gain;
+ },
+ createExpectedBuffers(context) {
+ var T1 = 2048 / context.sampleRate;
+ var expectedBuffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ var t = i / context.sampleRate;
+ expectedBuffer.getChannelData(0)[i] = V1;
+ }
+ return expectedBuffer;
+ },
+};
+
+SimpleTest.waitForExplicitFinish();
+// Comparing different AudioContexts may result in different timing reated information being reported
+// when we jitter time, as they are on different Relative Timelines.
+SpecialPowers.pushPrefEnv({"set": [["privacy.resistFingerprinting.reduceTimerPrecision.jitter", false]]}, runTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_audioParamSetValueAtTime.html b/dom/media/webaudio/test/test_audioParamSetValueAtTime.html
new file mode 100644
index 0000000000..18c02837e6
--- /dev/null
+++ b/dom/media/webaudio/test/test_audioParamSetValueAtTime.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AudioParam.linearRampToValue</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var V0 = 0.1;
+var V1 = 0.9;
+var T0 = 0;
+
+var gTest = {
+ length: 2048,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var sourceBuffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ sourceBuffer.getChannelData(0)[i] = 1;
+ }
+
+ var source = context.createBufferSource();
+ source.buffer = sourceBuffer;
+
+ var gain = context.createGain();
+ gain.gain.value = 0;
+ gain.gain.setValueAtTime(V0, 1024/context.sampleRate);
+
+ source.connect(gain);
+
+ source.start(0);
+ return gain;
+ },
+ createExpectedBuffers(context) {
+ var expectedBuffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 1024; i < 2048; ++i) {
+ expectedBuffer.getChannelData(0)[i] = 0.1;
+ }
+ return expectedBuffer;
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_audioParamTimelineDestinationOffset.html b/dom/media/webaudio/test/test_audioParamTimelineDestinationOffset.html
new file mode 100644
index 0000000000..76db12f88a
--- /dev/null
+++ b/dom/media/webaudio/test/test_audioParamTimelineDestinationOffset.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AudioParam timeline events scheduled after the destination stream has started playback</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.requestFlakyTimeout("This test needs to wait until the AudioDestinationNode's stream's timer starts.");
+
+var gTest = {
+ length: 16384,
+ numberOfChannels: 1,
+ createGraphAsync(context, callback) {
+ var sourceBuffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ sourceBuffer.getChannelData(0)[i] = 1;
+ }
+
+ setTimeout(function() {
+ var source = context.createBufferSource();
+ source.buffer = sourceBuffer;
+ source.start(context.currentTime);
+ source.stop(context.currentTime + sourceBuffer.duration);
+
+ var gain = context.createGain();
+ gain.gain.setValueAtTime(0, context.currentTime);
+ gain.gain.setTargetAtTime(0, context.currentTime + sourceBuffer.duration, 1);
+ source.connect(gain);
+
+ callback(gain);
+ }, 100);
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_badConnect.html b/dom/media/webaudio/test/test_badConnect.html
new file mode 100644
index 0000000000..51e83f6088
--- /dev/null
+++ b/dom/media/webaudio/test/test_badConnect.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test whether we can create an AudioContext interface</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script src="webaudio.js" type="text/javascript"></script>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var context1 = new OfflineAudioContext(1, 128, 44100);
+ var context2 = new OfflineAudioContext(1, 128, 44100);
+
+ var destination1 = context1.destination;
+ var destination2 = context2.destination;
+ var gain1 = new GainNode(context2);
+
+ isnot(destination1, destination2, "Destination nodes should not be the same");
+ isnot(destination1.context, destination2.context, "Destination nodes should not have the same context");
+
+ var source1 = context1.createBufferSource();
+
+ expectException(function() {
+ source1.connect(destination1, 1);
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ source1.connect(destination1, 0, 1);
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ source1.connect(destination2);
+ }, DOMException.INVALID_ACCESS_ERR);
+ expectException(function() {
+ source1.connect(gain1.gain);
+ }, DOMException.INVALID_ACCESS_ERR);
+
+ source1.connect(destination1);
+
+ expectException(function() {
+ source1.disconnect(1);
+ }, DOMException.INDEX_SIZE_ERR);
+
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_biquadFilterNode.html b/dom/media/webaudio/test/test_biquadFilterNode.html
new file mode 100644
index 0000000000..561198c491
--- /dev/null
+++ b/dom/media/webaudio/test/test_biquadFilterNode.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test BiquadFilterNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script src="webaudio.js" type="text/javascript"></script>
+<script class="testbody" type="text/javascript">
+
+function near(a, b, msg) {
+ ok(Math.abs(a - b) < 1e-3, msg);
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var context = new AudioContext();
+ var buffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ }
+
+ var destination = context.destination;
+
+ var source = context.createBufferSource();
+
+ var filter = context.createBiquadFilter();
+
+ source.buffer = buffer;
+
+ source.connect(filter);
+ filter.connect(destination);
+
+ // Verify default values
+ is(filter.type, "lowpass", "Correct default value for type");
+ near(filter.frequency.defaultValue, 350, "Correct default value for filter frequency");
+ near(filter.detune.defaultValue, 0, "Correct default value for filter detune");
+ near(filter.Q.defaultValue, 1, "Correct default value for filter Q");
+ near(filter.gain.defaultValue, 0, "Correct default value for filter gain");
+ is(filter.channelCount, 2, "Biquad filter node has 2 input channels by default");
+ is(filter.channelCountMode, "max", "Correct channelCountMode for the biquad filter node");
+ is(filter.channelInterpretation, "speakers", "Correct channelCountInterpretation for the biquad filter node");
+
+ // Make sure that we can set all of the valid type values
+ var types = [
+ "lowpass",
+ "highpass",
+ "bandpass",
+ "lowshelf",
+ "highshelf",
+ "peaking",
+ "notch",
+ "allpass",
+ ];
+ for (var i = 0; i < types.length; ++i) {
+ filter.type = types[i];
+ }
+
+ // Make sure getFrequencyResponse handles invalid frequencies properly
+ var frequencies = new Float32Array([-1.0, context.sampleRate*0.5 - 1.0, context.sampleRate]);
+ var magResults = new Float32Array(3);
+ var phaseResults = new Float32Array(3);
+ filter.getFrequencyResponse(frequencies, magResults, phaseResults);
+ ok(isNaN(magResults[0]), "Invalid input frequency should give NaN magnitude response");
+ ok(!isNaN(magResults[1]), "Valid input frequency should not give NaN magnitude response");
+ ok(isNaN(magResults[2]), "Invalid input frequency should give NaN magnitude response");
+ ok(isNaN(phaseResults[0]), "Invalid input frquency should give NaN phase response");
+ ok(!isNaN(phaseResults[1]), "Valid input frquency should not give NaN phase response");
+ ok(isNaN(phaseResults[2]), "Invalid input frquency should give NaN phase response");
+
+ source.start(0);
+ SimpleTest.executeSoon(function() {
+ source.stop(0);
+ source.disconnect();
+ filter.disconnect();
+
+ SimpleTest.finish();
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_biquadFilterNodePassThrough.html b/dom/media/webaudio/test/test_biquadFilterNodePassThrough.html
new file mode 100644
index 0000000000..4db01b0b14
--- /dev/null
+++ b/dom/media/webaudio/test/test_biquadFilterNodePassThrough.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test BiquadFilterNode with passthrough</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 2048,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var source = context.createBufferSource();
+
+ var filter = context.createBiquadFilter();
+
+ source.buffer = this.buffer;
+
+ source.connect(filter);
+
+ var filterWrapped = SpecialPowers.wrap(filter);
+ ok("passThrough" in filterWrapped, "BiquadFilterNode should support the passThrough API");
+ filterWrapped.passThrough = true;
+
+ source.start(0);
+ return filter;
+ },
+ createExpectedBuffers(context) {
+ this.buffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ this.buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ }
+
+ return [this.buffer];
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_biquadFilterNodeWithGain.html b/dom/media/webaudio/test/test_biquadFilterNodeWithGain.html
new file mode 100644
index 0000000000..d97a753de9
--- /dev/null
+++ b/dom/media/webaudio/test/test_biquadFilterNodeWithGain.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test BiquadFilterNode after a GainNode and tail - Bugs 924286 and 924288</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script src="webaudio.js" type="text/javascript"></script>
+<script class="testbody" type="text/javascript">
+
+const signalLength = 2048;
+
+var gTest = {
+ length: signalLength,
+ numberOfChannels: 1,
+ createGraph(context) {
+ // Two oscillators scheduled sequentially
+ var signalDuration = signalLength / context.sampleRate;
+ var osc1 = context.createOscillator();
+ osc1.type = "square";
+ osc1.start(0);
+ osc1.stop(signalDuration / 2);
+ var osc2 = context.createOscillator();
+ osc2.start(signalDuration / 2);
+ osc2.stop(signalDuration);
+
+ // Comparing a biquad on each source with one on both sources checks that
+ // the biquad on the first source doesn't shut down early.
+ var biquad1 = context.createBiquadFilter();
+ osc1.connect(biquad1);
+ var biquad2 = context.createBiquadFilter();
+ osc2.connect(biquad2);
+
+ var gain = context.createGain();
+ gain.gain.value = -1;
+ osc1.connect(gain);
+ osc2.connect(gain);
+
+ var biquadWithGain = context.createBiquadFilter();
+ gain.connect(biquadWithGain);
+
+ // The output of biquadWithGain should be the inverse of the sum of the
+ // outputs of biquad1 and biquad2, so blend them together and expect
+ // silence.
+ var blend = context.createGain();
+ biquad1.connect(blend);
+ biquad2.connect(blend);
+ biquadWithGain.connect(blend);
+
+ return blend;
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_bug1027864.html b/dom/media/webaudio/test/test_bug1027864.html
new file mode 100644
index 0000000000..847485ff88
--- /dev/null
+++ b/dom/media/webaudio/test/test_bug1027864.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test bug 1027864</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+function observer(subject, topic, data) {
+ var id = parseInt(data);
+ var index = ids.indexOf(id);
+ if (index != -1) {
+ ok(true, "Dropping id " + id + " at index " + index);
+ ids.splice(index, 1);
+ if (!ids.length) {
+ SimpleTest.executeSoon(function() {
+ SimpleTest.finish();
+ });
+ }
+ }
+}
+
+function id(node) {
+ return SpecialPowers.getPrivilegedProps(node, "id");
+}
+
+SpecialPowers.addAsyncObserver(observer, "webaudio-node-demise", false);
+
+SimpleTest.registerCleanupFunction(function() {
+ SpecialPowers.removeAsyncObserver(observer, "webaudio-node-demise");
+});
+
+var ac = new AudioContext();
+var ids;
+
+(function() {
+ var delay = ac.createDelay();
+ delay.delayTime.value = 0.03;
+
+ var gain = ac.createGain();
+ gain.gain.value = 0.6;
+
+ delay.connect(gain);
+ gain.connect(delay);
+
+ gain.connect(ac.destination);
+
+ var source = ac.createOscillator();
+
+ source.connect(gain);
+ source.start(ac.currentTime);
+ source.stop(ac.currentTime + 0.1);
+
+ ids = [ id(delay), id(gain), id(source) ];
+})();
+
+setInterval(function() {
+ forceCC();
+}, 200);
+
+function forceCC() {
+ SpecialPowers.DOMWindowUtils.cycleCollect();
+ SpecialPowers.DOMWindowUtils.garbageCollect();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_bug1056032.html b/dom/media/webaudio/test/test_bug1056032.html
new file mode 100644
index 0000000000..ba38267e19
--- /dev/null
+++ b/dom/media/webaudio/test/test_bug1056032.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<meta charset=utf-8>
+<head>
+ <title>Test that we can decode an mp3 (bug 1056032)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var filename = "small-shot.mp3";
+
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(function() {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", filename);
+ xhr.responseType = "arraybuffer";
+ xhr.onload = function() {
+ var context = new AudioContext();
+ context.decodeAudioData(xhr.response, function(b) {
+ ok(true, "We can decode an mp3 using decodeAudioData");
+ SimpleTest.finish();
+ }, function() {
+ ok(false, "We should be able to decode an mp3 using decodeAudioData but couldn't");
+ SimpleTest.finish();
+ });
+ };
+ xhr.send(null);
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_bug1113634.html b/dom/media/webaudio/test/test_bug1113634.html
new file mode 100644
index 0000000000..acdcba7c25
--- /dev/null
+++ b/dom/media/webaudio/test/test_bug1113634.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AudioParam.setTargetAtTime where the target time is the same as the time of a previous event</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var V0 = 0.9;
+var V1 = 0.1;
+var T0 = 0;
+var TimeConstant = 0.1;
+
+var gTest = {
+ length: 2048,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var sourceBuffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ sourceBuffer.getChannelData(0)[i] = 1;
+ }
+
+ var source = context.createBufferSource();
+ source.buffer = sourceBuffer;
+
+ var gain = context.createGain();
+ gain.gain.setValueAtTime(V0, T0);
+ gain.gain.setTargetAtTime(V1, T0, TimeConstant);
+
+ source.connect(gain);
+
+ source.start(0);
+ return gain;
+ },
+ createExpectedBuffers(context) {
+ var expectedBuffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ var t = i / context.sampleRate;
+ expectedBuffer.getChannelData(0)[i] = V1 + (V0 - V1) * Math.exp(-(t - T0) / TimeConstant);
+ }
+ return expectedBuffer;
+ },
+};
+
+SimpleTest.waitForExplicitFinish();
+// Comparing different AudioContexts may result in different timing reated information being reported
+// when we jitter time, as they are on different Relative Timelines.
+SpecialPowers.pushPrefEnv({"set": [["privacy.resistFingerprinting.reduceTimerPrecision.jitter", false]]}, runTest);
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_bug1118372.html b/dom/media/webaudio/test/test_bug1118372.html
new file mode 100644
index 0000000000..f049b221e8
--- /dev/null
+++ b/dom/media/webaudio/test/test_bug1118372.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test WaveShaperNode with no curve</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(function() {
+ var context = new OfflineAudioContext(1, 2048, 44100);
+
+ var osc=context.createOscillator();
+ var gain=context.createGain();
+ var shaper=context.createWaveShaper();
+ gain.gain.value=0.1;
+ shaper.curve=new Float32Array([-0.5,-0.5,1,1]);
+
+ osc.connect(gain);
+ gain.connect(shaper);
+ shaper.connect(context.destination);
+ osc.start(0);
+
+ context.startRendering().then(function(buffer) {
+ var samples = buffer.getChannelData(0);
+ // the signal should be scaled
+ var failures = 0;
+ for (var i = 0; i < 2048; ++i) {
+ if (samples[i] > 0.5) {
+ failures = failures + 1;
+ }
+ }
+ ok(failures == 0, "signal should have been rescaled by gain: found " + failures + " points too loud.");
+ SimpleTest.finish();
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_bug1255618.html b/dom/media/webaudio/test/test_bug1255618.html
new file mode 100644
index 0000000000..15e7351995
--- /dev/null
+++ b/dom/media/webaudio/test/test_bug1255618.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test sync XHR does not crash unlinked AudioContext</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script>
+SimpleTest.waitForExplicitFinish();
+
+const filename = "test_bug1255618.html";
+
+function collect_and_fetch() {
+ SpecialPowers.forceGC();
+ SpecialPowers.forceCC();
+
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", filename, false);
+ var ended = false;
+ xhr.onloadend = function() { ended = true; }
+ // Sync XHR will suspend timeouts, which involves any AudioContexts still
+ // registered with the window.
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1255618#c0
+ xhr.send(null);
+
+ ok(ended, "No crash during fetch");
+ SimpleTest.finish();
+}
+
+var ac = new AudioContext();
+
+ac.onstatechange = function () {
+ ac.onstatechange = null;
+ is(ac.state, "running", "statechange to running");
+ ac = null;
+ SimpleTest.executeSoon(collect_and_fetch);
+}
+
+</script>
+</body>
diff --git a/dom/media/webaudio/test/test_bug1267579.html b/dom/media/webaudio/test/test_bug1267579.html
new file mode 100644
index 0000000000..7003b345f5
--- /dev/null
+++ b/dom/media/webaudio/test/test_bug1267579.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test that PeriodicWave handles fundamental fequency of zero</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// This is the smallest value that the test framework will accept
+const testLength = 256;
+
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(function() {
+ runTest();
+});
+
+var gTest = {
+ numberOfChannels: 1,
+ createGraph(context) {
+ var osc = context.createOscillator();
+ osc.setPeriodicWave(context.
+ createPeriodicWave(new Float32Array([0.0, 1.0]),
+ new Float32Array(2)));
+ osc.frequency.value = 0.0;
+ osc.start();
+ return osc;
+ },
+ createExpectedBuffers(context) {
+ var buffer = context.createBuffer(1, testLength, context.sampleRate);
+
+ for (var i = 0; i < buffer.length; ++i) {
+ buffer.getChannelData(0)[i] = 1.0;
+ }
+ return buffer;
+ },
+};
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_bug1355798.html b/dom/media/webaudio/test/test_bug1355798.html
new file mode 100644
index 0000000000..9b46322bbc
--- /dev/null
+++ b/dom/media/webaudio/test/test_bug1355798.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test PannerNode produces output even when the even when the distance is
+ from the listener is zero, and the cone gain is present, regression test for
+ bug 1355798.</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+var off = new OfflineAudioContext(1, 128, 44100);
+var panner = off.createPanner();
+var osc = off.createOscillator();
+panner.setPosition(1, 1, 1);
+off.listener.setPosition(1, 1, 1);
+osc.connect(panner).connect(off.destination);
+panner.coneOuterAngle = 359;
+osc.start();
+off.startRendering().then(function(b) {
+ is(b.getChannelData(0).filter(x => isNaN(x)).length, 0);
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_bug1447273.html b/dom/media/webaudio/test/test_bug1447273.html
new file mode 100644
index 0000000000..f4b473d8ab
--- /dev/null
+++ b/dom/media/webaudio/test/test_bug1447273.html
@@ -0,0 +1,175 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test bug 1447273 - GainNode with a stereo input and changing volume</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <script type="text/javascript" src="head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+/**
+ * Sets up a stereo BufferSource and plumbs it through different gain node
+ * configurations. A control gain path with no changes to gain is used along
+ * with 2 other paths which should increase their gain. The result should be
+ * that audio travelling along the increased gain paths is louder than the
+ * control path.
+ */
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout(
+ "This test uses a live audio context and uses a setTimeout to schedule a " +
+ "change to the graph.");
+addLoadEvent(function() {
+ let context = new AudioContext();
+
+ let numChannels = 2;
+ let sampleRate = context.sampleRate;
+ // 60 seconds to mitigate timing issues on slow test machines
+ let recordingLength = 60;
+ let bufferLength = sampleRate * recordingLength;
+ let gainExplicitlyIncreased = false;
+ let sourceFinished = false;
+
+ // Create source buffer
+ let sourceBuffer = context.createBuffer(numChannels, bufferLength, sampleRate);
+ for (let i = 0; i < bufferLength; ++i) {
+ sourceBuffer.getChannelData(0)[i] = 1;
+ sourceBuffer.getChannelData(1)[i] = 1;
+ }
+ let source = context.createBufferSource();
+ source.buffer = sourceBuffer;
+
+ let gainNoChange = context.createGain();
+ let gainExplicitAssignment = context.createGain();
+ let gainSetValueAtTime = context.createGain();
+
+ // All gain nodes start of with the same gain
+ gainNoChange.gain.value = 0.25;
+ gainExplicitAssignment.gain.value = 0.25;
+ gainSetValueAtTime.gain.value = 0.25;
+
+ // Connect source to gain nodes:
+ // source--> gainNoChange
+ // |-> gainExplicitAssignment
+ // \-> gainSetValueAtTime
+ source.connect(gainNoChange);
+ source.connect(gainExplicitAssignment);
+ source.connect(gainSetValueAtTime);
+
+ // Create intermediate media streams (required to repro bug 1447273)
+ let destNoChange = context.createMediaStreamDestination();
+ let destExplicitAssignement = context.createMediaStreamDestination();
+ let destSetValueAtTime = context.createMediaStreamDestination();
+
+ let sourceNoChange = context.createMediaStreamSource(destNoChange.stream);
+ let sourceExplicitAssignement = context.createMediaStreamSource(destExplicitAssignement.stream);
+ let sourceSetValueAtTime = context.createMediaStreamSource(destSetValueAtTime.stream);
+
+ // Connect gain nodes to our intermediate streams:
+ // source--> gainNoChange -> destNoChange -> sourceNoChange
+ // |-> gainExplicitAssignment -> destExplicitAssignement -> sourceExplicitAssignement
+ // \-> gainSetValueAtTime -> destSetValueAtTime -> sourceSetValueAtTime
+ gainNoChange.connect(destNoChange);
+ gainExplicitAssignment.connect(destExplicitAssignement);
+ gainSetValueAtTime.connect(destSetValueAtTime);
+
+ // Create analysers for each path
+ let analyserNoChange = context.createAnalyser();
+ let analyserExplicitAssignment = context.createAnalyser();
+ let analyserSetValueAtTime = context.createAnalyser();
+
+ // Connect our intermediate media streams to analysers:
+ // source--> gainNoChange -> destNoChange -> sourceNoChange -> analyserNoChange
+ // |-> gainExplicitAssignment -> destExplicitAssignement -> sourceExplicitAssignement -> analyserExplicitAssignment
+ // \-> gainSetValueAtTime -> destSetValueAtTime -> sourceSetValueAtTime -> analyserSetValueAtTime
+ sourceNoChange.connect(analyserNoChange);
+ sourceExplicitAssignement.connect(analyserExplicitAssignment);
+ sourceSetValueAtTime.connect(analyserSetValueAtTime);
+
+ // Two seconds in, increase gain for setValueAt path
+ gainSetValueAtTime.gain.setValueAtTime(0.5, 2);
+
+ // Maximum values seen at each analyser node, will be updated by
+ // checkAnalysersForMaxValues() during test.
+ let maxNoGainChange = 0;
+ let maxExplicitAssignment = 0;
+ let maxSetValueAtTime = 0;
+
+ // Poll analysers and check for max values
+ function checkAnalysersForMaxValues() {
+ let findMaxValue =
+ (array) => array.reduce((a, b) => Math.max(Math.abs(a), Math.abs(b)));
+
+ let dataArray = new Float32Array(analyserNoChange.fftSize);
+ analyserNoChange.getFloatTimeDomainData(dataArray);
+ maxNoGainChange = Math.max(maxNoGainChange, findMaxValue(dataArray));
+
+ analyserExplicitAssignment.getFloatTimeDomainData(dataArray);
+ maxExplicitAssignment = Math.max(maxExplicitAssignment, findMaxValue(dataArray));
+
+ analyserSetValueAtTime.getFloatTimeDomainData(dataArray);
+ maxSetValueAtTime = Math.max(maxSetValueAtTime, findMaxValue(dataArray));
+
+ // End test if we've met our conditions
+ // Add a small amount to initial gain to make sure we're not getting
+ // passes due to rounding errors.
+ let epsilon = 0.01;
+ if (maxExplicitAssignment > (maxNoGainChange + epsilon) &&
+ maxSetValueAtTime > (maxNoGainChange + epsilon)) {
+ source.stop();
+ }
+ }
+
+ source.onended = () => {
+ sourceFinished = true;
+ info(`maxNoGainChange: ${maxNoGainChange}`);
+ info(`maxExplicitAssignment: ${maxExplicitAssignment}`);
+ info(`maxSetValueAtTime: ${maxSetValueAtTime}`);
+ ok(gainExplicitlyIncreased,
+ "Gain should be explicitly assinged during test!");
+ // Add a small amount to initial gain to make sure we're not getting
+ // passes due to rounding errors.
+ let epsilon = 0.01;
+ ok(maxExplicitAssignment > (maxNoGainChange + epsilon),
+ "Volume should increase due to explicit assignment to gain.value");
+ ok(maxSetValueAtTime > (maxNoGainChange + epsilon),
+ "Volume should increase due to setValueAtTime on gain.value");
+ SimpleTest.finish();
+ };
+
+ // Start the graph
+ source.start(0);
+
+ // We'll use this callback to check our analysers for gain
+ function animationFrameCb() {
+ if (sourceFinished) {
+ return;
+ }
+ requestAnimationFrame(animationFrameCb);
+ checkAnalysersForMaxValues();
+ }
+
+ // Using timers is gross, but as of writing there doesn't appear to be a
+ // nicer way to perform gain.value = 0.5 on our node. When/if we support
+ // suspend(time) on offline contexts we could potentially use that instead.
+
+ // Roughly 2 seconds through our source buffer (setTimeout flakiness) increase
+ // our gain on gainExplicitAssignment path.
+ window.setTimeout(() => {
+ gainExplicitAssignment.gain.value = 0.5;
+ // Make debugging flaky timeouts in test easier
+ info("Gain explicitly set!")
+ gainExplicitlyIncreased = true;
+ // Start checking analysers, we do this only after changing volume to avoid
+ // possible starvation of this timeout from requestAnimationFrame calls.
+ animationFrameCb();
+ }, 2000);
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_bug808374.html b/dom/media/webaudio/test/test_bug808374.html
new file mode 100644
index 0000000000..b255c0b9c3
--- /dev/null
+++ b/dom/media/webaudio/test/test_bug808374.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Crashtest for bug 808374</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+try {
+ var ctx = new AudioContext();
+ ctx.createBuffer(0, 1, ctx.sampleRate);
+} catch (e) {
+ ok(true, "The test should not crash during CC");
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_bug827541.html b/dom/media/webaudio/test/test_bug827541.html
new file mode 100644
index 0000000000..f205c7edf9
--- /dev/null
+++ b/dom/media/webaudio/test/test_bug827541.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Tell the cycle collector about the audio contexts owned by nsGlobalWindow</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+ var iframe = document.createElementNS("http://www.w3.org/1999/xhtml", "iframe");
+ document.body.appendChild(iframe);
+ var frameWin = iframe.contentWindow;
+ new frameWin.AudioContext();
+ document.body.removeChild(iframe);
+ expectException(() => new frameWin.AudioContext(),
+ DOMException.INVALID_STATE_ERR);
+
+ // This test should not leak.
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_bug839753.html b/dom/media/webaudio/test/test_bug839753.html
new file mode 100644
index 0000000000..f3e6598116
--- /dev/null
+++ b/dom/media/webaudio/test/test_bug839753.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Crashtest for bug 839753</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+(new AudioContext()).destination.expando = null;
+ok(true, "The test should not trigger wrapper cache assertions");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_bug845960.html b/dom/media/webaudio/test/test_bug845960.html
new file mode 100644
index 0000000000..17bf9a5700
--- /dev/null
+++ b/dom/media/webaudio/test/test_bug845960.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Crashtest for bug 845960</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+(new AudioContext()).decodeAudioData(new ArrayBuffer(0), function() {});
+ok(true, "Should not crash when the optional failure callback is not specified");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_bug856771.html b/dom/media/webaudio/test/test_bug856771.html
new file mode 100644
index 0000000000..24a527ccd5
--- /dev/null
+++ b/dom/media/webaudio/test/test_bug856771.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for bug 856771</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var context = new AudioContext();
+
+ var source = context.createBufferSource();
+ source.connect(context.destination);
+ ok(true, "Nothing should leak");
+
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_bug866570.html b/dom/media/webaudio/test/test_bug866570.html
new file mode 100644
index 0000000000..90bd8f6985
--- /dev/null
+++ b/dom/media/webaudio/test/test_bug866570.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Crashtest for bug 859600</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+(new AudioContext()).foo = null;
+ok(true, "The test should not fatally assert");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_bug866737.html b/dom/media/webaudio/test/test_bug866737.html
new file mode 100644
index 0000000000..e8db6b76e8
--- /dev/null
+++ b/dom/media/webaudio/test/test_bug866737.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for bug 866737</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var context = new AudioContext();
+
+(function() {
+ var d = context.createDelay();
+ var panner = context.createPanner();
+ d.connect(panner);
+ var gain = context.createGain();
+ panner.connect(gain);
+ gain.connect(context.destination);
+ gain.disconnect(0);
+})();
+
+SpecialPowers.forceGC();
+SpecialPowers.forceCC();
+
+var gain = context.createGain();
+gain.connect(context.destination);
+gain.disconnect(0);
+
+ok(true, "No crashes should happen!");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_bug867089.html b/dom/media/webaudio/test/test_bug867089.html
new file mode 100644
index 0000000000..e5a5179530
--- /dev/null
+++ b/dom/media/webaudio/test/test_bug867089.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Crashtest for bug 867089</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var ctx = new AudioContext();
+
+ // Test invalid playbackRate values for AudioBufferSourceNode.
+ var source = ctx.createBufferSource();
+ var buffer = ctx.createBuffer(2, 2048, 8000);
+ source.buffer = buffer;
+ source.playbackRate.value = 0.0;
+ source.connect(ctx.destination);
+ source.start(0);
+
+ var source2 = ctx.createBufferSource();
+ source2.buffer = buffer;
+ source2.playbackRate.value = -1.0;
+ source2.connect(ctx.destination);
+ source2.start(0);
+
+ var source3 = ctx.createBufferSource();
+ source3.buffer = buffer;
+ source3.playbackRate.value = 3000000.0;
+ source3.connect(ctx.destination);
+ source3.start(0);
+ ok(true, "We did not crash.");
+ SimpleTest.finish();
+});
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_bug867174.html b/dom/media/webaudio/test/test_bug867174.html
new file mode 100644
index 0000000000..e949bcec41
--- /dev/null
+++ b/dom/media/webaudio/test/test_bug867174.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Crashtest for bug 867174</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var ctx = new AudioContext();
+
+ var source = ctx.createBufferSource();
+ var buffer = ctx.createBuffer(2, 2048, 8000);
+ source.playbackRate.setTargetAtTime(0, 2, 3);
+ var sp = ctx.createScriptProcessor();
+ source.connect(sp);
+ sp.connect(ctx.destination);
+ source.start(0);
+
+ sp.onaudioprocess = function(e) {
+ // Now set the buffer
+ source.buffer = buffer;
+
+ ok(true, "We did not crash.");
+ sp.onaudioprocess = null;
+ SimpleTest.finish();
+ };
+});
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_bug873335.html b/dom/media/webaudio/test/test_bug873335.html
new file mode 100644
index 0000000000..19fd6d4dae
--- /dev/null
+++ b/dom/media/webaudio/test/test_bug873335.html
@@ -0,0 +1,22 @@
+<html>
+<head>
+<meta charset="UTF-8">
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script>
+
+function boom()
+{
+ (new AudioContext()).createScriptProcessor().hamster = {};
+ SpecialPowers.forceCC();
+ SpecialPowers.forceGC();
+ ok(true, "test finished");
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</head>
+
+<body onload="boom();"></body>
+</html>
diff --git a/dom/media/webaudio/test/test_bug875221.html b/dom/media/webaudio/test/test_bug875221.html
new file mode 100644
index 0000000000..5eb017d011
--- /dev/null
+++ b/dom/media/webaudio/test/test_bug875221.html
@@ -0,0 +1,239 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Crashtest for bug 875221</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("This test is generated by a fuzzer, so we leave these setTimeouts untouched.");
+
+try { o0 = document.createElement('audio'); } catch(e) { }
+try { (document.body || document.documentElement).appendChild(o0); } catch(e) { }
+try { o1 = new OfflineAudioContext(1, 10, (new AudioContext()).sampleRate); } catch(e) { }
+try { o1.startRendering(); } catch(e) { }
+try { o1.listener.dopplerFactor = 1; } catch(e) { }
+try { o2 = o1.createScriptProcessor(); } catch(e) { }
+try { o3 = o1.createChannelMerger(4); } catch(e) { }
+try { o1.listener.dopplerFactor = 3; } catch(e) { }
+try { o1.listener.setPosition(0, 134217728, 64) } catch(e) { }
+try { o1.listener.dopplerFactor = 15; } catch(e) { }
+try { o1.startRendering(); } catch(e) { }
+try { o4 = new OfflineAudioContext(1, 10, (new AudioContext()).sampleRate); } catch(e) { }
+try { o4.listener.speedOfSound = 2048; } catch(e) { }
+try { o4.listener.setPosition(32768, 1, 1) } catch(e) { }
+try { o5 = o1.createChannelSplitter(4); } catch(e) { }
+try { o4.listener.setVelocity(4, 1, 0) } catch(e) { }
+try { o4.startRendering(); } catch(e) { }
+try { o4.startRendering(); } catch(e) { }
+try { o4.listener.setPosition(64, 1, 0) } catch(e) { }
+try { o1.listener.setOrientation(4194304, 15, 8388608, 15, 1, 1) } catch(e) { }
+try { o1.listener.dopplerFactor = 256; } catch(e) { }
+try { o6 = o4.createDelay(16); } catch(e) { }
+try { o4.startRendering(); } catch(e) { }
+try { o4.listener.setOrientation(0, 1, 0, 0, 31, 1073741824) } catch(e) { }
+try { o4.listener.speedOfSound = 1; } catch(e) { }
+try { o1.listener.speedOfSound = 0; } catch(e) { }
+try { o1.startRendering(); } catch(e) { }
+try { o6.connect(o3, 1, 0) } catch(e) { }
+try { o1.listener.setPosition(4294967296, 32, 1) } catch(e) { }
+try { o1.listener.speedOfSound = 0; } catch(e) { }
+try { o1.listener.speedOfSound = 0; } catch(e) { }
+try { o1.listener.setVelocity(1, 256, 0) } catch(e) { }
+try { o4.startRendering(); } catch(e) { }
+try { o3.disconnect() } catch(e) { }
+setTimeout("try { o4.startRendering(); } catch(e) { }",50)
+try { o4.listener.setOrientation(0, 0, 2048, 128, 16384, 127) } catch(e) { }
+try { o4.listener.setVelocity(0, 4, 1) } catch(e) { }
+try { o7 = o4.createScriptProcessor(1024, 4, 1); } catch(e) { }
+try { o8 = o4.createDynamicsCompressor(); } catch(e) { }
+try { o1.startRendering(); } catch(e) { }
+try { o2.connect(o7); } catch(e) { }
+try { o7.connect(o4); } catch(e) { }
+try { o2.connect(o7); } catch(e) { }
+try { o7.connect(o4); } catch(e) { }
+SpecialPowers.forceCC();
+SpecialPowers.forceGC();
+try { o4.listener.setOrientation(8192, 1, 1, 512, 0, 15) } catch(e) { }
+setTimeout("try { o7.onaudioprocess = function() {}; } catch(e) { }",50)
+try { o1.startRendering(); } catch(e) { }
+try { o1.listener.speedOfSound = 1073741824; } catch(e) { }
+try { o2.connect(o7); } catch(e) { }
+try { o7.connect(o4); } catch(e) { }
+try { o9 = o4.createScriptProcessor(1024, 1, 4); } catch(e) { }
+try { o10 = o4.createAnalyser(); } catch(e) { }
+try { o4.listener.speedOfSound = 0; } catch(e) { }
+try { o2.connect(o7); } catch(e) { }
+try { o9.connect(o1); } catch(e) { }
+try { o4.listener.setVelocity(524288, 1, 65536) } catch(e) { }
+setTimeout("try { o2.connect(o9); } catch(e) { } setTimeout(done, 0);",1000)
+try { o7.connect(o4); } catch(e) { }
+try { o1.listener.setVelocity(1, 127, 31) } catch(e) { }
+try { o2.connect(o7); } catch(e) { }
+try { o7.connect(o1); } catch(e) { }
+setTimeout("try { o5.disconnect() } catch(e) { }",100)
+try { o2.connect(o9); } catch(e) { }
+try { o7.connect(o4); } catch(e) { }
+try { o4.startRendering(); } catch(e) { }
+setTimeout("try { o1.listener.dopplerFactor = 1; } catch(e) { }",100)
+try { o5.disconnect() } catch(e) { }
+try { o1.startRendering(); } catch(e) { }
+try { o1.startRendering(); } catch(e) { }
+try { o10.disconnect() } catch(e) { }
+try { o1.startRendering(); } catch(e) { }
+try { o11 = o1.createGain(); } catch(e) { }
+try { o2.connect(o7); } catch(e) { }
+try { o9.connect(o4); } catch(e) { }
+try { o4.listener.setOrientation(31, 0, 15, 0, 33554432, 1) } catch(e) { }
+try { o4.listener.dopplerFactor = 1; } catch(e) { }
+try { o2.connect(o7); } catch(e) { }
+try { o7.connect(o4); } catch(e) { }
+try { o2.connect(o7); } catch(e) { }
+setTimeout("try { o9.connect(o4); } catch(e) { }",50)
+try { o2.connect(o9); } catch(e) { }
+setTimeout("try { o9.connect(o1); } catch(e) { }",200)
+try { o2.connect(o7); } catch(e) { }
+try { o7.connect(o1); } catch(e) { }
+try { o12 = o4.createDynamicsCompressor(); } catch(e) { }
+try { o2.connect(o7); } catch(e) { }
+try { o9.connect(o1); } catch(e) { }
+try { o9.onaudioprocess = function() {}; } catch(e) { }
+try { o1.listener.speedOfSound = 262144; } catch(e) { }
+try { o2.connect(o7); } catch(e) { }
+try { o7.connect(o4); } catch(e) { }
+try { o2.connect(o9); } catch(e) { }
+setTimeout("try { o7.connect(o4); } catch(e) { }",50)
+try { o2.connect(o9); } catch(e) { }
+try { o7.connect(o4); } catch(e) { }
+try { o13 = o4.createGain(); } catch(e) { }
+try { o4.listener.dopplerFactor = 31; } catch(e) { }
+try { o11.gain.value = 268435456; } catch(e) { }
+try { o1.listener.setOrientation(63, 3, 1, 63, 1, 2147483648) } catch(e) { }
+try { o2.connect(o9); } catch(e) { }
+try { o7.connect(o4); } catch(e) { }
+try { o4.listener.setVelocity(1, 0, 1) } catch(e) { }
+try { o11.gain.value = 65536; } catch(e) { }
+try { o2.connect(o9); } catch(e) { }
+setTimeout("try { o7.connect(o4); } catch(e) { }",200)
+try { o14 = o4.createDynamicsCompressor(); } catch(e) { }
+setTimeout("try { o2.connect(o9); } catch(e) { }",50)
+try { o7.connect(o1); } catch(e) { }
+try { o15 = o1.createWaveShaper(); } catch(e) { }
+try { o2.connect(o7); } catch(e) { }
+try { o7.connect(o1); } catch(e) { }
+try { o16 = o1.createWaveShaper(); } catch(e) { }
+try { o11.gain.value = 1; } catch(e) { }
+try { o1.listener.speedOfSound = 16; } catch(e) { }
+try { o4.listener.setVelocity(0, 127, 15) } catch(e) { }
+try { o1.listener.setVelocity(0, 2048, 16777216) } catch(e) { }
+try { o13.gain.value = 0; } catch(e) { }
+try { o2.connect(o9); } catch(e) { }
+try { o7.connect(o4); } catch(e) { }
+try { o2.connect(o9); } catch(e) { }
+try { o9.connect(o1); } catch(e) { }
+try { o17 = document.createElement('audio'); } catch(e) { }
+try { (document.body || document.documentElement).appendChild(o0); } catch(e) { }
+try { o4.listener.setVelocity(3, 1, 256) } catch(e) { }
+try { o11.gain.cancelScheduledValues(1) } catch(e) { }
+try { o1.listener.dopplerFactor = 524288; } catch(e) { }
+try { o9.onaudioprocess = function() {}; } catch(e) { }
+setTimeout("try { o7.connect(o13, 0, 0) } catch(e) { }",50)
+try { o1.listener.speedOfSound = 0; } catch(e) { }
+try { o10.disconnect() } catch(e) { }
+try { o2.connect(o9); } catch(e) { }
+try { o9.connect(o4); } catch(e) { }
+try { o2.connect(o7); } catch(e) { }
+try { o9.connect(o4); } catch(e) { }
+try { o1.listener.speedOfSound = 1; } catch(e) { }
+try { o15.disconnect() } catch(e) { }
+try { o11.gain.exponentialRampToValueAtTime(0, 15) } catch(e) { }
+try { o15.curve = new Float32Array(15); } catch(e) { }
+try { o4.listener.setVelocity(1, 1, 1) } catch(e) { }
+try { o14.connect(o6, 0, 0) } catch(e) { }
+try { o2.connect(o9); } catch(e) { }
+try { o9.connect(o1); } catch(e) { }
+try { o2.connect(o9); } catch(e) { }
+try { o7.connect(o4); } catch(e) { }
+try { o2.connect(o7); } catch(e) { }
+setTimeout("try { o7.connect(o1); } catch(e) { }",100)
+try { o4.listener.setVelocity(1, 7, 1) } catch(e) { }
+try { o18 = document.createElement('audio'); } catch(e) { }
+try { (document.body || document.documentElement).appendChild(o18); } catch(e) { }
+try { o19 = o4.createGain(); } catch(e) { }
+try { o2.connect(o7); } catch(e) { }
+try { o7.connect(o1); } catch(e) { }
+try { o4.listener.dopplerFactor = 0; } catch(e) { }
+try { o1.listener.setPosition(256, 16, 1) } catch(e) { }
+setTimeout("try { o2.connect(o9); } catch(e) { }",50)
+try { o7.connect(o1); } catch(e) { }
+try { o4.listener.speedOfSound = 31; } catch(e) { }
+try { o2.connect(o7); } catch(e) { }
+setTimeout("try { o9.connect(o4); } catch(e) { }",1000)
+try { o11.gain.value = 127; } catch(e) { }
+try { o7.connect(o7, 0, 0) } catch(e) { }
+try { o4.listener.speedOfSound = 63; } catch(e) { }
+try { o11.gain.value = 33554432; } catch(e) { }
+try { o2.connect(o7); } catch(e) { }
+try { o9.connect(o4); } catch(e) { }
+try { o4.listener.speedOfSound = 16; } catch(e) { }
+try { o4.listener.setVelocity(1048576, 0, 127) } catch(e) { }
+try { o1.listener.dopplerFactor = 0; } catch(e) { }
+try { o6.connect(o2, 0, 1) } catch(e) { }
+try { o5.disconnect() } catch(e) { }
+try { o3.disconnect() } catch(e) { }
+try { o2.connect(o9); } catch(e) { }
+try { o7.connect(o1); } catch(e) { }
+try { o16.disconnect() } catch(e) { }
+try { o2.connect(o9); } catch(e) { }
+try { o7.connect(o1); } catch(e) { }
+try { o9.disconnect() } catch(e) { }
+try { o4.listener.speedOfSound = 1; } catch(e) { }
+try { o2.connect(o7); } catch(e) { }
+try { o7.connect(o4); } catch(e) { }
+try { o11.gain.setValueCurveAtTime(new Float32Array(3), 2048, 3) } catch(e) { }
+try { o13.gain.value = 8; } catch(e) { }
+try { o2.connect(o7); } catch(e) { }
+try { o9.connect(o4); } catch(e) { }
+try { o4.listener.setOrientation(1, 2048, 1, 1, 0, 31) } catch(e) { }
+try { o2.connect(o9); } catch(e) { }
+try { o7.connect(o1); } catch(e) { }
+try { o1.listener.speedOfSound = 256; } catch(e) { }
+try { o2.connect(o7); } catch(e) { }
+try { o9.connect(o4); } catch(e) { }
+try { o4.listener.setVelocity(1, 67108864, 128) } catch(e) { }
+setTimeout("try { o1.listener.setVelocity(0, 1, 1) } catch(e) { }",100)
+try { o2.connect(o9); } catch(e) { }
+try { o9.connect(o1); } catch(e) { }
+setTimeout("try { o20 = o1.createBiquadFilter(); } catch(e) { }",200)
+try { o13.gain.value = 4096; } catch(e) { }
+try { o1.listener.dopplerFactor = 0; } catch(e) { }
+try { o2.connect(o7); } catch(e) { }
+try { o9.connect(o1); } catch(e) { }
+try { o2.connect(o9); } catch(e) { }
+try { o7.connect(o4); } catch(e) { }
+setTimeout("try { o2.connect(o9); } catch(e) { }",200)
+try { o7.connect(o1); } catch(e) { }
+try { o3.connect(o15, 1, 1) } catch(e) { }
+try { o2.connect(o12, 0, 0) } catch(e) { }
+try { o19.gain.exponentialRampToValueAtTime(1, 0) } catch(e) { }
+try { o2.connect(o7); } catch(e) { }
+try { o7.connect(o4); } catch(e) { }
+try { o2.connect(o7); } catch(e) { }
+try { o9.connect(o1); } catch(e) { }
+try { o2.connect(o7); } catch(e) { }
+try { o7.connect(o4); } catch(e) { }
+
+function done() {
+ ok(true, "We did not crash.");
+ SimpleTest.finish();
+}
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_bug875402.html b/dom/media/webaudio/test/test_bug875402.html
new file mode 100644
index 0000000000..95c8e0e236
--- /dev/null
+++ b/dom/media/webaudio/test/test_bug875402.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Crashtest for bug 875402</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+SimpleTest.requestFlakyTimeout("This test is generated by a fuzzer, so we leave these setTimeouts untouched.");
+
+try { o1 = new OfflineAudioContext(1, 10, (new AudioContext()).sampleRate); } catch(e) { }
+try { o2 = o1.createScriptProcessor(); } catch(e) { }
+try { o4 = new OfflineAudioContext(1, 10, (new AudioContext()).sampleRate); } catch(e) { }
+try { o5 = o1.createChannelSplitter(4); } catch(e) { }
+try { o7 = o4.createScriptProcessor(1024, 4, 1); } catch(e) { }
+SpecialPowers.forceCC();
+SpecialPowers.forceGC();
+try { o1.startRendering(); } catch(e) { }
+try { o2.connect(o7); } catch(e) { }
+try { o7.connect(o4); } catch(e) { }
+try { o9 = o4.createScriptProcessor(1024, 1, 4); } catch(e) { }
+try { o2.connect(o7); } catch(e) { }
+try { o9.connect(o1); } catch(e) { }
+setTimeout("try { o2.connect(o9); } catch(e) { } done();",1000)
+try { o7.connect(o4); } catch(e) { }
+setTimeout("try { o5.disconnect() } catch(e) { }",100)
+try { o2.connect(o9); } catch(e) { }
+try { o4.startRendering(); } catch(e) { }
+try { o2.connect(o9); } catch(e) { }
+setTimeout("try { o7.connect(o4); } catch(e) { }",50)
+try { o13 = o4.createGain(); } catch(e) { }
+setTimeout("try { o7.connect(o13, 0, 0) } catch(e) { }",50)
+
+function done() {
+ ok(true, "We did not crash.");
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_bug894150.html b/dom/media/webaudio/test/test_bug894150.html
new file mode 100644
index 0000000000..4577232d71
--- /dev/null
+++ b/dom/media/webaudio/test/test_bug894150.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test whether we can create an AudioContext interface</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script>
+
+var ac = new AudioContext();
+ac.createPanner();
+var listener = ac.listener;
+SpecialPowers.forceGC();
+SpecialPowers.forceCC();
+listener.setOrientation(0, 0, -1, 0, 0, 0);
+
+ok(true, "No crashes should happen!");
+
+</script>
+</body>
diff --git a/dom/media/webaudio/test/test_bug956489.html b/dom/media/webaudio/test/test_bug956489.html
new file mode 100644
index 0000000000..f0ae559b05
--- /dev/null
+++ b/dom/media/webaudio/test/test_bug956489.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test when and currentTime are in the same coordinate system</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("This test needs to wait a while for the AudioContext's timer to start.");
+addLoadEvent(function() {
+ var freq = 330;
+
+ var context = new AudioContext();
+
+ var buffer = context.createBuffer(1, context.sampleRate / freq, context.sampleRate);
+ for (var i = 0; i < buffer.length; ++i) {
+ buffer.getChannelData(0)[i] = Math.sin(2 * Math.PI * i / buffer.length);
+ }
+
+ var source = context.createBufferSource();
+ source.loop = true;
+ source.buffer = buffer;
+
+ setTimeout(function () {
+ var finished = false;
+
+ source.start(context.currentTime);
+ var processor = context.createScriptProcessor(256, 1, 1);
+ processor.onaudioprocess = function (e) {
+ if (finished) return;
+ var c = e.inputBuffer.getChannelData(0);
+ var result = true;
+
+ for (var i = 0; i < buffer.length; ++i) {
+ if (Math.abs(c[i] - buffer.getChannelData(0)[i]) > 1e-9) {
+ result = false;
+ break;
+ }
+ }
+ finished = true;
+ ok(result, "when and currentTime are in same time coordinate system");
+ SimpleTest.finish();
+ }
+ processor.connect(context.destination);
+ source.connect(processor);
+ }, 500);
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_bug964376.html b/dom/media/webaudio/test/test_bug964376.html
new file mode 100644
index 0000000000..bc9d167dcd
--- /dev/null
+++ b/dom/media/webaudio/test/test_bug964376.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test repeating audio is not distorted</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+function gcd(a, b) {
+ if (b === 0) {
+ return a;
+ }
+ return gcd(b, a % b);
+}
+
+var SAMPLE_PLACEMENT = 128;
+
+var gTest = {
+ length: 2048,
+ numberOfChannels: 1,
+
+ createGraph(context) {
+ var freq = Math.round(context.sampleRate / SAMPLE_PLACEMENT);
+ var dur = context.sampleRate / gcd(freq, context.sampleRate);
+ var buffer = context.createBuffer(1, dur, context.sampleRate);
+
+ for (var i = 0; i < context.sampleRate; ++i) {
+ buffer.getChannelData(0)[i] = Math.sin(freq * 2 * Math.PI * i / context.sampleRate);
+ }
+
+ var source = context.createBufferSource();
+ source.buffer = buffer;
+ source.loop = true;
+ source.playbackRate.setValueAtTime(0.5, SAMPLE_PLACEMENT / context.sampleRate);
+ source.start(0);
+
+ return source;
+ },
+
+ createExpectedBuffers(context) {
+ var freq = Math.round(context.sampleRate / SAMPLE_PLACEMENT);
+ var expectedBuffer = context.createBuffer(1, 2048, context.sampleRate);
+ var c = expectedBuffer.getChannelData(0);
+ for (var i = 0; i < c.length; ++i) {
+ if (i < SAMPLE_PLACEMENT) {
+ c[i] = Math.sin(freq * 2 * Math.PI * i / context.sampleRate);
+ } else {
+ c[i] = Math.sin(freq / 2 * 2 * Math.PI * (i + SAMPLE_PLACEMENT) / context.sampleRate);
+ }
+ }
+
+ return expectedBuffer;
+ },
+};
+
+runTest();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_bug966247.html b/dom/media/webaudio/test/test_bug966247.html
new file mode 100644
index 0000000000..69831c33b6
--- /dev/null
+++ b/dom/media/webaudio/test/test_bug966247.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test whether an audio file played with a volume set to 0 plays silence</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<audio preload=none src="ting-48k-1ch.ogg" controls> </audio>
+<script>
+ SimpleTest.waitForExplicitFinish();
+
+ var count = 20;
+
+ function isSilent(b) {
+ for (var i = 0; i < b.length; i++) {
+ if (b[i] != 0.0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ var a = document.getElementsByTagName("audio")[0];
+ a.volume = 0.0;
+ var ac = new AudioContext();
+ var measn = ac.createMediaElementSource(a);
+ var sp = ac.createScriptProcessor();
+
+ sp.onaudioprocess = function(e) {
+ var inputBuffer = e.inputBuffer.getChannelData(0);
+ ok(isSilent(inputBuffer), "The volume is set to 0, so all the elements of the buffer are supposed to be equal to 0.0");
+ }
+ // Connect the MediaElementAudioSourceNode to the ScriptProcessorNode to check
+ // the audio volume.
+ measn.connect(sp);
+ a.play();
+
+ a.addEventListener("ended", function() {
+ sp.onaudioprocess = null;
+ SimpleTest.finish();
+ });
+
+</script>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_bug972678.html b/dom/media/webaudio/test/test_bug972678.html
new file mode 100644
index 0000000000..1450c19645
--- /dev/null
+++ b/dom/media/webaudio/test/test_bug972678.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test buffers do not interfere when scheduled in sequence</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var OFFSETS = [0.005, 0.01, 0.02, 0.03];
+var LENGTH = 128;
+
+var gTest = {
+ length: 128 * OFFSETS.length,
+ numberOfChannels: 1,
+
+ createGraph(context) {
+ var gain = context.createGain();
+
+ // create a repeating sample
+ var repeatingSample = context.createBuffer(1, 2, context.sampleRate);
+ var c = repeatingSample.getChannelData(0);
+ for (var i = 0; i < repeatingSample.length; ++i) {
+ c[i] = i % 2 == 0 ? 1 : -1;
+ }
+
+ OFFSETS.forEach(function (offset, offsetIdx) {
+ // Schedule a set of nodes to repeat the sample.
+ for (var i = 0; i < LENGTH; i += repeatingSample.length) {
+ var source = context.createBufferSource();
+ source.buffer = repeatingSample;
+ source.connect(gain);
+ source.start((offsetIdx * LENGTH + i + offset) / context.sampleRate);
+ }
+
+ buffer = context.createBuffer(1, LENGTH, context.sampleRate);
+ c = buffer.getChannelData(0);
+ for (var i = 0; i < buffer.length; ++i) {
+ c[i] = i % 2 == 0 ? -1 : 1;
+ }
+
+ var source = context.createBufferSource();
+ source.buffer = buffer;
+ source.connect(gain);
+ source.start((offsetIdx * LENGTH + offset) / context.sampleRate);
+ });
+
+ return gain;
+ },
+
+ createExpectedBuffers(context) {
+ return context.createBuffer(1, gTest.length, context.sampleRate);
+ },
+};
+
+runTest();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_channelMergerNode.html b/dom/media/webaudio/test/test_channelMergerNode.html
new file mode 100644
index 0000000000..b62d34d6ba
--- /dev/null
+++ b/dom/media/webaudio/test/test_channelMergerNode.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test ChannelMergerNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 2048,
+ numberOfChannels: 6,
+ createGraph(context) {
+ var buffers = [];
+ for (var j = 0; j < 6; ++j) {
+ var buffer = context.createBuffer(2, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ buffer.getChannelData(0)[i] = Math.sin(440 * 2 * (j + 1) * Math.PI * i / context.sampleRate);
+ // Second channel is silent
+ }
+ buffers.push(buffer);
+ }
+
+ var merger = new ChannelMergerNode(context);
+ is(merger.channelCount, 1, "merger node has 1 input channels");
+ is(merger.channelCountMode, "explicit", "Correct channelCountMode for the merger node");
+ is(merger.channelInterpretation, "speakers", "Correct channelCountInterpretation for the merger node");
+
+ for (var i = 0; i < 6; ++i) {
+ var source = context.createBufferSource();
+ source.buffer = buffers[i];
+ source.connect(merger, 0, i);
+ source.start(0);
+ }
+
+ return merger;
+ },
+ createExpectedBuffers(context) {
+ var expectedBuffer = context.createBuffer(6, 2048, context.sampleRate);
+ for (var i = 0; i < 6; ++i) {
+ for (var j = 0; j < 2048; ++j) {
+ expectedBuffer.getChannelData(i)[j] = 0.5 * Math.sin(440 * 2 * (i + 1) * Math.PI * j / context.sampleRate);
+ }
+ }
+ return expectedBuffer;
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_channelMergerNodeWithVolume.html b/dom/media/webaudio/test/test_channelMergerNodeWithVolume.html
new file mode 100644
index 0000000000..55b9ec0c0b
--- /dev/null
+++ b/dom/media/webaudio/test/test_channelMergerNodeWithVolume.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test ChannelMergerNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 2048,
+ numberOfChannels: 6,
+ createGraph(context) {
+ var buffers = [];
+ for (var j = 0; j < 6; ++j) {
+ var buffer = context.createBuffer(2, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ buffer.getChannelData(0)[i] = Math.sin(440 * 2 * (j + 1) * Math.PI * i / context.sampleRate);
+ // Second channel is silent
+ }
+ buffers.push(buffer);
+ }
+
+ var merger = context.createChannelMerger();
+ is(merger.channelCount, 1, "merger node has 1 input channels");
+ is(merger.channelCountMode, "explicit", "Correct channelCountMode for the merger node");
+ is(merger.channelInterpretation, "speakers", "Correct channelCountInterpretation for the merger node");
+
+ for (var i = 0; i < 6; ++i) {
+ var source = context.createBufferSource();
+ source.buffer = buffers[i];
+ var gain = context.createGain();
+ gain.gain.value = 0.5;
+ source.connect(gain);
+ gain.connect(merger, 0, i);
+ source.start(0);
+ }
+
+ return merger;
+ },
+ createExpectedBuffers(context) {
+ var expectedBuffer = context.createBuffer(6, 2048, context.sampleRate);
+ for (var i = 0; i < 6; ++i) {
+ for (var j = 0; j < 2048; ++j) {
+ expectedBuffer.getChannelData(i)[j] = 0.5 * 0.5 * Math.sin(440 * 2 * (i + 1) * Math.PI * j / context.sampleRate);
+ }
+ }
+ return expectedBuffer;
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_channelSplitterNode.html b/dom/media/webaudio/test/test_channelSplitterNode.html
new file mode 100644
index 0000000000..d74845f821
--- /dev/null
+++ b/dom/media/webaudio/test/test_channelSplitterNode.html
@@ -0,0 +1,71 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test ChannelSplitterNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// We do not use our generic graph test framework here because
+// the splitter node is special in that it creates multiple
+// output ports.
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var context = new AudioContext();
+ var buffer = context.createBuffer(4, 2048, context.sampleRate);
+ for (var j = 0; j < 4; ++j) {
+ for (var i = 0; i < 2048; ++i) {
+ buffer.getChannelData(j)[i] = Math.sin(440 * 2 * (j + 1) * Math.PI * i / context.sampleRate);
+ }
+ }
+ var emptyBuffer = context.createBuffer(1, 2048, context.sampleRate);
+
+ var destination = context.destination;
+
+ var source = context.createBufferSource();
+
+ var splitter = new ChannelSplitterNode(context);
+ is(splitter.channelCount, 6, "splitter node has 2 input channels by default");
+ is(splitter.channelCountMode, "explicit", "Correct channelCountMode for the splitter node");
+ is(splitter.channelInterpretation, "discrete", "Correct channelCountInterpretation for the splitter node");
+
+ source.buffer = buffer;
+ source.connect(splitter);
+
+ var channelsSeen = 0;
+ function createHandler(i) {
+ return function(e) {
+ is(e.inputBuffer.numberOfChannels, 1, "Correct input channel count");
+ if (i < 4) {
+ compareChannels(e.inputBuffer.getChannelData(0), buffer.getChannelData(i));
+ } else {
+ compareChannels(e.inputBuffer.getChannelData(0), emptyBuffer.getChannelData(0));
+ }
+ e.target.onaudioprocess = null;
+ ++channelsSeen;
+
+ if (channelsSeen == 6) {
+ SimpleTest.finish();
+ }
+ };
+ }
+
+ for (var i = 0; i < 6; ++i) {
+ var sp = context.createScriptProcessor(2048, 1);
+ splitter.connect(sp, i);
+ sp.onaudioprocess = createHandler(i);
+ sp.connect(destination);
+ }
+
+ source.start(0);
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_channelSplitterNodeWithVolume.html b/dom/media/webaudio/test/test_channelSplitterNodeWithVolume.html
new file mode 100644
index 0000000000..c03f6deeaf
--- /dev/null
+++ b/dom/media/webaudio/test/test_channelSplitterNodeWithVolume.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test ChannelSplitterNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// We do not use our generic graph test framework here because
+// the splitter node is special in that it creates multiple
+// output ports.
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var context = new AudioContext();
+ var buffer = context.createBuffer(4, 2048, context.sampleRate);
+ var expectedBuffer = context.createBuffer(4, 2048, context.sampleRate);
+ for (var j = 0; j < 4; ++j) {
+ for (var i = 0; i < 2048; ++i) {
+ buffer.getChannelData(j)[i] = Math.sin(440 * 2 * (j + 1) * Math.PI * i / context.sampleRate);
+ expectedBuffer.getChannelData(j)[i] = buffer.getChannelData(j)[i] / 2;
+ }
+ }
+ var emptyBuffer = context.createBuffer(1, 2048, context.sampleRate);
+
+ var destination = context.destination;
+
+ var source = context.createBufferSource();
+
+ var splitter = context.createChannelSplitter();
+ is(splitter.channelCount, 6, "splitter node has 2 input channels by default");
+ is(splitter.channelCountMode, "explicit", "Correct channelCountMode for the splitter node");
+ is(splitter.channelInterpretation, "discrete", "Correct channelCountInterpretation for the splitter node");
+
+ source.buffer = buffer;
+ var gain = context.createGain();
+ gain.gain.value = 0.5;
+ source.connect(gain);
+ gain.connect(splitter);
+
+ var channelsSeen = 0;
+ function createHandler(i) {
+ return function(e) {
+ is(e.inputBuffer.numberOfChannels, 1, "Correct input channel count");
+ if (i < 4) {
+ compareBuffers(e.inputBuffer.getChannelData(0), expectedBuffer.getChannelData(i));
+ } else {
+ compareBuffers(e.inputBuffer.getChannelData(0), emptyBuffer.getChannelData(0));
+ }
+ e.target.onaudioprocess = null;
+ ++channelsSeen;
+
+ if (channelsSeen == 6) {
+ SimpleTest.finish();
+ }
+ };
+ }
+
+ for (var i = 0; i < 6; ++i) {
+ var sp = context.createScriptProcessor(2048, 1);
+ splitter.connect(sp, i);
+ sp.onaudioprocess = createHandler(i);
+ sp.connect(destination);
+ }
+
+ source.start(0);
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_convolver-upmixing-1-channel-response.html b/dom/media/webaudio/test/test_convolver-upmixing-1-channel-response.html
new file mode 100644
index 0000000000..50bd594821
--- /dev/null
+++ b/dom/media/webaudio/test/test_convolver-upmixing-1-channel-response.html
@@ -0,0 +1,143 @@
+<!DOCTYPE html>
+<title>Test that up-mixing signals in ConvolverNode processing is linear</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+const EPSILON = 3.0 * Math.pow(2, -22);
+// sampleRate is a power of two so that delay times are exact in base-2
+// floating point arithmetic.
+const SAMPLE_RATE = 32768;
+// Length of stereo convolver input in frames (arbitrary):
+const STEREO_FRAMES = 256;
+// Length of mono signal in frames. This is more than two blocks to ensure
+// that at least one block will be mono, even if interpolation in the
+// DelayNode means that stereo is output one block earlier and later than
+// if frames are delayed without interpolation.
+const MONO_FRAMES = 384;
+// Length of response buffer:
+const RESPONSE_FRAMES = 256;
+
+function test_linear_upmixing(channelInterpretation, initial_mono_frames)
+{
+ let stereo_input_end = initial_mono_frames + STEREO_FRAMES;
+ // Total length:
+ let length = stereo_input_end + RESPONSE_FRAMES + MONO_FRAMES + STEREO_FRAMES;
+ // The first two channels contain signal where some up-mixing occurs
+ // internally to a ConvolverNode when a stereo signal is added and removed.
+ // The last two channels are expected to contain the same signal, but mono
+ // and stereo signals are convolved independently before up-mixing the mono
+ // output to mix with the stereo output.
+ let context = new OfflineAudioContext({numberOfChannels: 4,
+ length,
+ sampleRate: SAMPLE_RATE});
+
+ let response = new AudioBuffer({numberOfChannels: 1,
+ length: RESPONSE_FRAMES,
+ sampleRate: context.sampleRate});
+
+ // Two stereo channel splitters will collect test and reference outputs.
+ let destinationMerger = new ChannelMergerNode(context, {numberOfInputs: 4});
+ destinationMerger.connect(context.destination);
+ let testSplitter =
+ new ChannelSplitterNode(context, {numberOfOutputs: 2});
+ let referenceSplitter =
+ new ChannelSplitterNode(context, {numberOfOutputs: 2});
+ testSplitter.connect(destinationMerger, 0, 0);
+ testSplitter.connect(destinationMerger, 1, 1);
+ referenceSplitter.connect(destinationMerger, 0, 2);
+ referenceSplitter.connect(destinationMerger, 1, 3);
+
+ // A GainNode mixes reference stereo and mono signals because up-mixing
+ // cannot be performed at a channel splitter.
+ let referenceGain = new GainNode(context);
+ referenceGain.connect(referenceSplitter);
+ referenceGain.channelInterpretation = channelInterpretation;
+
+ // The impulse response for convolution contains two impulses so as to test
+ // effects in at least two processing blocks.
+ response.getChannelData(0)[0] = 0.5;
+ response.getChannelData(0)[response.length - 1] = 0.5;
+
+ let testConvolver = new ConvolverNode(context, {disableNormalization: true,
+ buffer: response});
+ testConvolver.channelInterpretation = channelInterpretation;
+ let referenceMonoConvolver = new ConvolverNode(context,
+ {disableNormalization: true,
+ buffer: response});
+ let referenceStereoConvolver = new ConvolverNode(context,
+ {disableNormalization: true,
+ buffer: response});
+ // No need to set referenceStereoConvolver.channelInterpretation because
+ // input is either silent or stereo.
+ testConvolver.connect(testSplitter);
+ // Mix reference convolver output.
+ referenceMonoConvolver.connect(referenceGain);
+ referenceStereoConvolver.connect(referenceGain);
+
+ // The DelayNode initially has a single channel of silence, which is used to
+ // switch the stereo signal in and out. The output of the delay node is
+ // first mono silence (if there is a non-zero initial_mono_frames), then
+ // stereo, then mono silence, and finally stereo again. maxDelayTime is
+ // used to generate the middle mono silence period from the initial silence
+ // in the DelayNode and then generate the final period of stereo from its
+ // initial input.
+ let maxDelayTime = (length - STEREO_FRAMES) / context.sampleRate;
+ let delay =
+ new DelayNode(context,
+ {maxDelayTime,
+ delayTime: initial_mono_frames / context.sampleRate});
+ // Schedule an increase in the delay to return to mono silence.
+ delay.delayTime.setValueAtTime(maxDelayTime,
+ stereo_input_end / context.sampleRate);
+ delay.connect(testConvolver);
+ delay.connect(referenceStereoConvolver);
+
+ let stereoMerger = new ChannelMergerNode(context, {numberOfInputs: 2});
+ stereoMerger.connect(delay);
+
+ // Three independent signals
+ let monoSignal = new OscillatorNode(context, {frequency: 440});
+ let leftSignal = new OscillatorNode(context, {frequency: 450});
+ let rightSignal = new OscillatorNode(context, {frequency: 460});
+ monoSignal.connect(testConvolver);
+ monoSignal.connect(referenceMonoConvolver);
+ leftSignal.connect(stereoMerger, 0, 0);
+ rightSignal.connect(stereoMerger, 0, 1);
+ monoSignal.start();
+ leftSignal.start();
+ rightSignal.start();
+
+ return context.startRendering().
+ then((buffer) => {
+ let maxDiff = -1.0;
+ let frameIndex = 0;
+ let channelIndex = 0;
+ for (let c = 0; c < 2; ++c) {
+ let testOutput = buffer.getChannelData(0 + c);
+ let referenceOutput = buffer.getChannelData(2 + c);
+ for (var i = 0; i < buffer.length; ++i) {
+ var diff = Math.abs(testOutput[i] - referenceOutput[i]);
+ if (diff > maxDiff) {
+ maxDiff = diff;
+ frameIndex = i;
+ channelIndex = c;
+ }
+ }
+ }
+ assert_approx_equals(buffer.getChannelData(0 + channelIndex)[frameIndex],
+ buffer.getChannelData(2 + channelIndex)[frameIndex],
+ EPSILON,
+ `output at ${frameIndex} ` +
+ `in channel ${channelIndex}` );
+ });
+}
+
+promise_test(() => test_linear_upmixing("speakers", MONO_FRAMES),
+ "speakers, initially mono");
+promise_test(() => test_linear_upmixing("discrete", MONO_FRAMES),
+ "discrete");
+// Gecko uses a separate path for "speakers" up-mixing when the convolver's
+// first input is stereo, so test that separately.
+promise_test(() => test_linear_upmixing("speakers", 0),
+ "speakers, initially stereo");
+</script>
diff --git a/dom/media/webaudio/test/test_convolverNode.html b/dom/media/webaudio/test/test_convolverNode.html
new file mode 100644
index 0000000000..c1677aafab
--- /dev/null
+++ b/dom/media/webaudio/test/test_convolverNode.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test the ConvolverNode interface</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var context = new AudioContext();
+ var conv = new ConvolverNode(context);
+
+ is(conv.channelCount, 2, "Convolver node has 2 input channels by default");
+ is(conv.channelCountMode, "clamped-max", "Correct channelCountMode for the Convolver node");
+ is(conv.channelInterpretation, "speakers", "Correct channelCountInterpretation for the Convolver node");
+
+ is(conv.buffer, null, "Default buffer value");
+ is(conv.normalize, true, "Default normalize value");
+
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_convolverNodeChannelCount.html b/dom/media/webaudio/test/test_convolverNodeChannelCount.html
new file mode 100644
index 0000000000..03824578ea
--- /dev/null
+++ b/dom/media/webaudio/test/test_convolverNodeChannelCount.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test ConvolverNode channel count</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script src="webaudio.js" type="text/javascript"></script>
+<script class="testbody" type="text/javascript">
+
+const signalLength = 2048;
+const responseLength = 1000;
+const outputLength = 2048; // < signalLength + responseLength to test bug 910171
+
+var gTest = {
+ length: outputLength,
+ numberOfChannels: 2,
+ createGraph(context) {
+ var buffer = context.createBuffer(2, signalLength, context.sampleRate);
+ for (var i = 0; i < signalLength; ++i) {
+ var sample = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ // When mixed into a single channel, this produces silence
+ buffer.getChannelData(0)[i] = sample;
+ buffer.getChannelData(1)[i] = -sample;
+ }
+
+ var response = context.createBuffer(2, responseLength, context.sampleRate);
+ for (var i = 0; i < responseLength; ++i) {
+ response.getChannelData(0)[i] = i / responseLength;
+ response.getChannelData(1)[i] = 1 - (i / responseLength);
+ }
+
+ var convolver = context.createConvolver();
+ convolver.buffer = response;
+ convolver.channelCount = 1;
+
+ expectException(function() { convolver.channelCount = 3; },
+ DOMException.NOT_SUPPORTED_ERR);
+ convolver.channelCountMode = "explicit";
+ expectException(function() { convolver.channelCountMode = "max"; },
+ DOMException.NOT_SUPPORTED_ERR);
+ convolver.channelInterpretation = "discrete";
+ convolver.channelInterpretation = "speakers";
+
+ var source = context.createBufferSource();
+ source.buffer = buffer;
+ source.connect(convolver);
+ source.start(0);
+
+ return convolver;
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_convolverNodeChannelInterpretationChanges.html b/dom/media/webaudio/test/test_convolverNodeChannelInterpretationChanges.html
new file mode 100644
index 0000000000..bede517b2e
--- /dev/null
+++ b/dom/media/webaudio/test/test_convolverNodeChannelInterpretationChanges.html
@@ -0,0 +1,169 @@
+<!DOCTYPE html>
+<title>Test up-mixing in ConvolverNode after ChannelInterpretation change</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+// This test is not in wpt because it requires that multiple changes to the
+// nodes in an AudioContext during a single event will be processed by the
+// audio thread in a single transaction. Gecko provides that, but this is not
+// currently required by the Web Audio API.
+
+const EPSILON = Math.pow(2, -23);
+// sampleRate is a power of two so that delay times are exact in base-2
+// floating point arithmetic.
+const SAMPLE_RATE = 32768;
+// Length of initial mono signal in frames, if the test has an initial mono
+// signal. This is more than one block to ensure that at least one block
+// will be mono, even if interpolation in the DelayNode means that stereo is
+// output one block earlier than if frames are delayed without interpolation.
+const MONO_FRAMES = 256;
+// Length of response buffer. This is greater than 1 to ensure that the
+// convolver has stereo output at least one block after stereo input is
+// disconnected.
+const RESPONSE_FRAMES = 2;
+
+function test_interpretation_change(t, initialInterpretation, initialMonoFrames)
+{
+ let context = new AudioContext({sampleRate: SAMPLE_RATE});
+
+ // Three independent signals. These are constant so that results are
+ // independent of the timing of the `ended` event.
+ let monoOffset = 0.25
+ let monoSource = new ConstantSourceNode(context, {offset: monoOffset});
+ let leftOffset = 0.125;
+ let rightOffset = 0.5;
+ let leftSource = new ConstantSourceNode(context, {offset: leftOffset});
+ let rightSource = new ConstantSourceNode(context, {offset: rightOffset});
+ monoSource.start();
+ leftSource.start();
+ rightSource.start();
+
+ let stereoMerger = new ChannelMergerNode(context, {numberOfInputs: 2});
+ leftSource.connect(stereoMerger, 0, 0);
+ rightSource.connect(stereoMerger, 0, 1);
+
+ // The DelayNode initially has a single channel of silence, and so the
+ // output of the delay node is first mono silence (if there is a non-zero
+ // initialMonoFrames), then stereo. In Gecko, this triggers a convolver
+ // configuration that is different for different channelInterpretations.
+ let delay =
+ new DelayNode(context,
+ {maxDelayTime: MONO_FRAMES / context.sampleRate,
+ delayTime: initialMonoFrames / context.sampleRate});
+ stereoMerger.connect(delay);
+
+ // Two convolvers with the same impulse response. The test convolver will
+ // process a mix of stereo and mono signals. The reference convolver will
+ // always process stereo, including the up-mixed mono signal.
+ let response = new AudioBuffer({numberOfChannels: 1,
+ length: RESPONSE_FRAMES,
+ sampleRate: context.sampleRate});
+ response.getChannelData(0)[response.length - 1] = 1;
+
+ let testConvolver = new ConvolverNode(context,
+ {disableNormalization: true,
+ buffer: response});
+ testConvolver.channelInterpretation = initialInterpretation;
+ let referenceConvolver = new ConvolverNode(context,
+ {disableNormalization: true,
+ buffer: response});
+ // No need to set referenceConvolver.channelInterpretation because
+ // input is always stereo, due to up-mixing at gain node.
+ let referenceMixer = new GainNode(context);
+ referenceMixer.channelCount = 2;
+ referenceMixer.channelCountMode = "explicit";
+ referenceMixer.channelInterpretation = initialInterpretation;
+ referenceMixer.connect(referenceConvolver);
+
+ delay.connect(testConvolver);
+ delay.connect(referenceMixer);
+
+ monoSource.connect(testConvolver);
+ monoSource.connect(referenceMixer);
+
+ // A timer sends 'ended' when the convolvers are known to be processing
+ // stereo.
+ let timer = new ConstantSourceNode(context);
+ timer.start();
+ timer.stop((initialMonoFrames + 1) / context.sampleRate);
+
+ timer.onended = t.step_func(() => {
+ let changedInterpretation =
+ initialInterpretation == "speakers" ? "discrete" : "speakers";
+
+ // Switch channelInterpretation in test and reference paths.
+ testConvolver.channelInterpretation = changedInterpretation;
+ referenceMixer.channelInterpretation = changedInterpretation;
+
+ // Disconnect the stereo input from both test and reference convolvers.
+ // The disconnected convolvers will continue to output stereo for at least
+ // one frame. The test convolver will up-mix its mono input into its two
+ // buffers.
+ delay.disconnect();
+
+ // Capture the outputs in a script processor.
+ //
+ // The first two channels contain signal where some up-mixing occurs
+ // internally to the test convolver.
+ //
+ // The last two channels are expected to contain the same signal, but
+ // up-mixing was performed at a GainNode prior to convolution.
+ //
+ // Two stereo splitters will collect test and reference outputs.
+ let testSplitter =
+ new ChannelSplitterNode(context, {numberOfOutputs: 2});
+ let referenceSplitter =
+ new ChannelSplitterNode(context, {numberOfOutputs: 2});
+ testConvolver.connect(testSplitter);
+ referenceConvolver.connect(referenceSplitter);
+
+ let outputMerger = new ChannelMergerNode(context, {numberOfInputs: 4});
+ testSplitter.connect(outputMerger, 0, 0);
+ testSplitter.connect(outputMerger, 1, 1);
+ referenceSplitter.connect(outputMerger, 0, 2);
+ referenceSplitter.connect(outputMerger, 1, 3);
+
+ let processor = context.createScriptProcessor(256, 4, 0);
+ outputMerger.connect(processor);
+
+ processor.onaudioprocess = t.step_func_done((e) => {
+ e.target.onaudioprocess = null;
+ outputMerger.disconnect();
+
+ // The test convolver output is stereo for the first block.
+ let length = 128;
+
+ let buffer = e.inputBuffer;
+ let maxDiff = -1.0;
+ let frameIndex = 0;
+ let channelIndex = 0;
+ for (let c = 0; c < 2; ++c) {
+ let testOutput = buffer.getChannelData(0 + c);
+ let referenceOutput = buffer.getChannelData(2 + c);
+ for (var i = 0; i < length; ++i) {
+ var diff = Math.abs(testOutput[i] - referenceOutput[i]);
+ if (diff > maxDiff) {
+ maxDiff = diff;
+ frameIndex = i;
+ channelIndex = c;
+ }
+ }
+ }
+ assert_approx_equals(buffer.getChannelData(0 + channelIndex)[frameIndex],
+ buffer.getChannelData(2 + channelIndex)[frameIndex],
+ EPSILON,
+ `output at ${frameIndex} ` +
+ `in channel ${channelIndex}` );
+ });
+ });
+}
+
+async_test((t) => test_interpretation_change(t, "speakers", MONO_FRAMES),
+ "speakers to discrete, initially mono");
+async_test((t) => test_interpretation_change(t, "discrete", MONO_FRAMES),
+ "discrete to speakers");
+// Gecko uses a separate path for "speakers" initial up-mixing when the
+// convolver's first input is stereo, so test that separately.
+async_test((t) => test_interpretation_change(t, "speakers", 0),
+ "speakers to discrete, initially stereo");
+</script>
diff --git a/dom/media/webaudio/test/test_convolverNodeDelay.html b/dom/media/webaudio/test/test_convolverNodeDelay.html
new file mode 100644
index 0000000000..2e8caf8027
--- /dev/null
+++ b/dom/media/webaudio/test/test_convolverNodeDelay.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<title>Test convolution to delay a triangle pulse</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+const sampleRate = 48000;
+const LENGTH = 12800;
+// tolerate 16-bit math.
+const EPSILON = 1.0 / Math.pow(2, 15);
+
+// Triangle pulse
+var sourceBuffer = new OfflineAudioContext(1, 1, sampleRate).
+ createBuffer(1, 2 * 128, sampleRate);
+var channelData = sourceBuffer.getChannelData(0);
+for (var i = 0; i < 128; ++i) {
+ channelData[i] = i/128;
+ channelData[128 + i] = 1.0 - i/128;
+}
+
+function test_delay_index(delayIndex) {
+
+ var context = new OfflineAudioContext(2, LENGTH, sampleRate);
+
+ var merger = context.createChannelMerger(2);
+ merger.connect(context.destination);
+
+ var impulse = context.createBuffer(1, delayIndex + 1, sampleRate);
+ impulse.getChannelData(0)[delayIndex] = 1.0;
+ var convolver = context.createConvolver();
+ convolver.normalize = false;
+ convolver.buffer = impulse;
+ convolver.connect(merger, 0, 0);
+
+ var delayTime = delayIndex/sampleRate;
+ var delay = context.createDelay(delayTime || 1/sampleRate);
+ delay.delayTime.value = delayTime;
+ delay.connect(merger, 0, 1);
+
+ var source = context.createBufferSource();
+ source.buffer = sourceBuffer;
+ source.connect(convolver);
+ source.connect(delay);
+ source.start(0);
+
+ return context.startRendering().
+ then((buffer) => {
+ var convolverOutput = buffer.getChannelData(0);
+ var delayOutput = buffer.getChannelData(1);
+ var maxDiff = 0.0;
+ var maxIndex = 0;
+ for (var i = 0; i < buffer.length; ++i) {
+ var diff = Math.abs(convolverOutput[i] - delayOutput[i]);
+ if (diff > maxDiff) {
+ maxDiff = diff;
+ maxIndex = i;
+ }
+ }
+ // The convolver should produce similar output to the delay.
+ assert_approx_equals(convolverOutput[maxIndex], delayOutput[maxIndex],
+ EPSILON, "output at " + maxIndex);
+ });
+}
+
+// The 5/4 ratio provides sampling across a range of delays and offsets within
+// blocks.
+for (var delayIndex = 0;
+ delayIndex < LENGTH;
+ delayIndex = Math.floor((5 * (delayIndex + 1)) / 4)) {
+ promise_test(test_delay_index.bind(null, delayIndex),
+ "Delay " + delayIndex);
+}
+</script>
diff --git a/dom/media/webaudio/test/test_convolverNodeFiniteInfluence.html b/dom/media/webaudio/test/test_convolverNodeFiniteInfluence.html
new file mode 100644
index 0000000000..1cfb51ce8a
--- /dev/null
+++ b/dom/media/webaudio/test/test_convolverNodeFiniteInfluence.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html>
+<title>Test convolution effect has finite duration</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+promise_test(function() {
+
+ const responseLength = 256;
+ // Accept an influence period of twice the responseLength to accept FFT
+ // implementations.
+ const tolerancePeriod = 2 * responseLength;
+ const totalSize = tolerancePeriod + responseLength;
+
+ var context = new OfflineAudioContext(1, totalSize, 48000);
+
+ var responseBuffer =
+ context.createBuffer(1, responseLength, context.sampleRate);
+ var responseChannelData = responseBuffer.getChannelData(0);
+ responseChannelData[0] = 1;
+ responseChannelData[responseLength - 1] = 1;
+ var convolver = context.createConvolver();
+ convolver.buffer = responseBuffer;
+ convolver.connect(context.destination);
+
+ var sourceBuffer = context.createBuffer(1, totalSize, context.sampleRate);
+ sourceBuffer.getChannelData(0)[0] = NaN;
+ var source = context.createBufferSource();
+ source.buffer = sourceBuffer;
+ source.connect(convolver);
+ source.start();
+
+ return context.startRendering().
+ then((buffer) => {
+ var convolverOutput = buffer.getChannelData(0);
+ // There should be no non-zeros after the tolerance period.
+ var testIndex = tolerancePeriod;
+ for (;
+ testIndex < buffer.length - 1 && convolverOutput[testIndex] == 0;
+ ++testIndex) {
+ }
+ assert_equals(convolverOutput[testIndex], 0, "output at " + testIndex);
+ });
+});
+</script>
diff --git a/dom/media/webaudio/test/test_convolverNodeNormalization.html b/dom/media/webaudio/test/test_convolverNodeNormalization.html
new file mode 100644
index 0000000000..24cb7d1670
--- /dev/null
+++ b/dom/media/webaudio/test/test_convolverNodeNormalization.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<title>Test normalization of convolution buffers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+// Constants from
+// https://www.w3.org/TR/2015/WD-webaudio-20151208/#widl-ConvolverNode-normalize
+const GainCalibration = 0.00125;
+const GainCalibrationSampleRate = 44100;
+
+const sampleRate = GainCalibrationSampleRate;
+const LENGTH = 12800;
+// tolerate 16-bit math.
+const EPSILON = 1.0 / Math.pow(2, 15);
+
+function test_normalization_via_response_concat(delayIndex)
+{
+ var context = new OfflineAudioContext(1, LENGTH, sampleRate);
+
+ var impulse = context.createBuffer(1, 1, sampleRate);
+ impulse.getChannelData(0)[0] = 1.0;
+ var source = context.createBufferSource();
+ source.buffer = impulse;
+ source.start(0);
+
+ // Construct a set of adjacent responses in such a way that, when each is
+ // convolved with the impulse, they can be merged to produce a constant.
+
+ // The 5/4 ratio provides a range of lengths with different offsets within
+ // blocks.
+ var i = 0;
+ for (var responseEnd = 1;
+ i < LENGTH;
+ responseEnd = Math.floor((5 * responseEnd) / 4) + 1) {
+ var responseBuffer = context.createBuffer(1, responseEnd, sampleRate);
+ var response = responseBuffer.getChannelData(0);
+ var responseStart = i;
+ // The values in the response should be normalized, and so the output
+ // should not be dependent on the value. Pick a changing value to test
+ // this.
+ var value = responseStart + 1;
+ for (; i < responseEnd; ++i) {
+ response[i] = value;
+ }
+ var convolver = context.createConvolver();
+ convolver.normalize = true;
+ convolver.buffer = responseBuffer;
+ convolver.connect(context.destination);
+ // Undo the normalization calibration by scaling the impulse so as to
+ // expect unit pulse output from the convolver.
+ var gain = context.createGain();
+ gain.gain.value =
+ Math.sqrt((responseEnd - responseStart) / responseEnd) / GainCalibration;
+ gain.connect(convolver);
+ source.connect(gain);
+ }
+
+ return context.startRendering().
+ then((buffer) => {
+ var output = buffer.getChannelData(0);
+ var max = output[0];
+ var maxIndex = 0;
+ var min = max;
+ var minIndex = 0;
+ for (var i = 1; i < buffer.length; ++i) {
+ if (output[i] > max) {
+ max = output[i];
+ maxIndex = i;
+ } else if (output[i] < min) {
+ min = output[i];
+ minIndex = i;
+ }
+ }
+ assert_approx_equals(output[maxIndex], 1.0, EPSILON,
+ "max output at " + maxIndex);
+ assert_approx_equals(output[minIndex], 1.0, EPSILON,
+ "min output at " + minIndex);
+ });
+}
+
+promise_test(test_normalization_via_response_concat,
+ "via response concatenation");
+</script>
diff --git a/dom/media/webaudio/test/test_convolverNodeOOM.html b/dom/media/webaudio/test/test_convolverNodeOOM.html
new file mode 100644
index 0000000000..2983d2f65c
--- /dev/null
+++ b/dom/media/webaudio/test/test_convolverNodeOOM.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test ConvolverNode with very large buffer that triggers an OOM</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 2048,
+ numberOfChannels: 1,
+ skipOfflineContextTests: true,
+ createGraph(context) {
+ var source = context.createOscillator();
+ var convolver = context.createConvolver();
+ // Very big buffer that results in an OOM
+ try {
+ var buffer = context.createBuffer(2, 300000000, context.sampleRate)
+ var channel = buffer.getChannelData(0);
+ } catch(e) {
+ // OOM when attempting to create the buffer, this can happen on 32bits
+ // OSes. Simply return here.
+ return convolver;
+ }
+ source.connect(convolver);
+ try {
+ convolver.buffer = buffer;
+ } catch (e) {
+ // This can also OOM.
+ return convolver;
+ }
+ source.start();
+ return convolver;
+ }
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_convolverNodePassThrough.html b/dom/media/webaudio/test/test_convolverNodePassThrough.html
new file mode 100644
index 0000000000..54682aee0c
--- /dev/null
+++ b/dom/media/webaudio/test/test_convolverNodePassThrough.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test ConvolverNode with passthrough</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 2048,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var source = context.createBufferSource();
+
+ var convolver = context.createConvolver();
+
+ source.buffer = this.buffer;
+ convolver.buffer = this.buffer;
+
+ source.connect(convolver);
+
+ var convolverWrapped = SpecialPowers.wrap(convolver);
+ ok("passThrough" in convolverWrapped, "ConvolverNode should support the passThrough API");
+ convolverWrapped.passThrough = true;
+
+ source.start(0);
+ return convolver;
+ },
+ createExpectedBuffers(context) {
+ this.buffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ this.buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ }
+
+ return [this.buffer];
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_convolverNodeWithGain.html b/dom/media/webaudio/test/test_convolverNodeWithGain.html
new file mode 100644
index 0000000000..0762f16329
--- /dev/null
+++ b/dom/media/webaudio/test/test_convolverNodeWithGain.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test ConvolverNode after a GainNode - Bug 891254 </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script src="webaudio.js" type="text/javascript"></script>
+<script class="testbody" type="text/javascript">
+
+const signalLength = 2048;
+const responseLength = 100;
+const outputLength = 4096; // > signalLength + responseLength
+
+var gTest = {
+ length: outputLength,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var buffer = context.createBuffer(1, signalLength, context.sampleRate);
+ for (var i = 0; i < signalLength; ++i) {
+ buffer.getChannelData(0)[i] = Math.sin(2 * Math.PI * i / signalLength);
+ }
+
+ var source = context.createBufferSource();
+ source.buffer = buffer;
+ source.start(0);
+
+ var response = context.createBuffer(1, responseLength, context.sampleRate);
+ for (var i = 0; i < responseLength; ++i) {
+ response.getChannelData(0)[i] = i / responseLength;
+ }
+
+ var gain = context.createGain();
+ gain.gain.value = -1;
+ source.connect(gain);
+
+ var convolver1 = context.createConvolver();
+ convolver1.buffer = response;
+ gain.connect(convolver1);
+
+ var convolver2 = context.createConvolver();
+ convolver2.buffer = response;
+ source.connect(convolver2);
+
+ // The output of convolver1 should be the inverse of convolver2, so blend
+ // them together and expect silence.
+ var blend = context.createGain();
+ convolver1.connect(blend);
+ convolver2.connect(blend);
+
+ return blend;
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_convolverNode_mono_mono.html b/dom/media/webaudio/test/test_convolverNode_mono_mono.html
new file mode 100644
index 0000000000..1585e1b619
--- /dev/null
+++ b/dom/media/webaudio/test/test_convolverNode_mono_mono.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+
+<html>
+<head>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script type="text/javascript" src="webaudio.js"></script>
+<script type="text/javascript" src="layouttest-glue.js"></script>
+<script type="text/javascript" src="blink/audio-testing.js"></script>
+<script type="text/javascript" src="blink/convolution-testing.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+
+<div id="description"></div>
+<div id="console"></div>
+
+<script>
+description("Tests ConvolverNode processing a mono channel with mono impulse response.");
+SimpleTest.waitForExplicitFinish();
+
+// To test the convolver, we convolve two square pulses together to
+// produce a triangular pulse. To verify the result is correct we
+// check several parts of the result. First, we make sure the initial
+// part of the result is zero (due to the latency in the convolver).
+// Next, the triangular pulse should match the theoretical result to
+// within some roundoff. After the triangular pulse, the result
+// should be exactly zero, but round-off prevents that. We make sure
+// the part after the pulse is sufficiently close to zero. Finally,
+// the result should be exactly zero because the inputs are exactly
+// zero.
+function runTest() {
+ if (window.testRunner) {
+ testRunner.dumpAsText();
+ testRunner.waitUntilDone();
+ }
+
+ window.jsTestIsAsync = true;
+
+ // Create offline audio context.
+ var context = new OfflineAudioContext(2, sampleRate * renderLengthSeconds, sampleRate);
+
+ var squarePulse = createSquarePulseBuffer(context, pulseLengthFrames);
+ var trianglePulse = createTrianglePulseBuffer(context, 2 * pulseLengthFrames);
+
+ var bufferSource = context.createBufferSource();
+ bufferSource.buffer = squarePulse;
+
+ var convolver = context.createConvolver();
+ convolver.normalize = false;
+ convolver.buffer = squarePulse;
+
+ bufferSource.connect(convolver);
+ convolver.connect(context.destination);
+
+ bufferSource.start(0);
+
+ context.oncomplete = checkConvolvedResult(trianglePulse);
+ context.startRendering();
+}
+
+function finishJSTest() {
+ SimpleTest.finish();
+}
+
+runTest();
+successfullyParsed = true;
+
+</script>
+
+<script src="../fast/js/resources/js-test-post.js"></script>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_currentTime.html b/dom/media/webaudio/test/test_currentTime.html
new file mode 100644
index 0000000000..66fdf42653
--- /dev/null
+++ b/dom/media/webaudio/test/test_currentTime.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AudioContext.currentTime</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("This test needs to wait a while for the AudioContext's timer to start.");
+addLoadEvent(function() {
+ var ac = new AudioContext();
+ is(ac.currentTime, 0, "AudioContext.currentTime should be 0 initially");
+ ac.onstatechange = function () {
+ ok(ac.state == "running", "AudioContext.currentTime should eventually start");
+ ok(ac.currentTime > 0, "AudioContext.currentTime should have increased by now");
+ SimpleTest.finish();
+ }
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_decodeAudioDataOnDetachedBuffer.html b/dom/media/webaudio/test/test_decodeAudioDataOnDetachedBuffer.html
new file mode 100644
index 0000000000..e7c6d2db0c
--- /dev/null
+++ b/dom/media/webaudio/test/test_decodeAudioDataOnDetachedBuffer.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+ <meta charset=utf-8>
+<head>
+ <title>Bug 1308434 - Test DecodeAudioData on detached buffer</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script type="text/javascript">
+var testDecodeAudioDataOnDetachedBuffer = function(buffer) {
+ var context = new AudioContext();
+
+ // make the buffer detached
+ context.decodeAudioData(buffer);
+
+ // check that the buffer is detached
+ is(buffer.byteLength, 0, "Buffer should be detached");
+
+ // call decodeAudioData on detached buffer
+ context.decodeAudioData(buffer).then(function(b) {
+ ok(false, "We should not be able to decode the detached buffer but we do");
+ SimpleTest.finish();
+ }, function(r) {
+ SimpleTest.isa(r, TypeError);
+ is(r.message, "BaseAudioContext.decodeAudioData: Buffer argument can't be a detached buffer", "Incorrect message");
+ SimpleTest.finish();
+ });
+};
+
+var filename = "small-shot.mp3";
+
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(function() {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", filename);
+ xhr.responseType = "arraybuffer";
+
+ xhr.onload = function() {
+ testDecodeAudioDataOnDetachedBuffer(xhr.response);
+ };
+
+ xhr.send();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_decodeAudioDataPromise.html b/dom/media/webaudio/test/test_decodeAudioDataPromise.html
new file mode 100644
index 0000000000..139a686db1
--- /dev/null
+++ b/dom/media/webaudio/test/test_decodeAudioDataPromise.html
@@ -0,0 +1,62 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test the decodeAudioData API with Promise</title>
+
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script src="webaudio.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+
+var finished = 0;
+
+function finish() {
+ if (++finished == 2) {
+ SimpleTest.finish();
+ }
+}
+
+var ac = new AudioContext();
+// Test that a the promise is rejected with an invalid source buffer.
+expectNoException(function() {
+ var p = ac.decodeAudioData(" ");
+ ok(p instanceof Promise, "AudioContext.decodeAudioData should return a Promise");
+ p.then(function(data) {
+ ok(false, "Promise should not resolve with an invalid source buffer.");
+ finish();
+ }).catch(function(e) {
+ ok(true, "Promise should be rejected with an invalid source buffer.");
+ ok(e.name == "TypeError", "The error should be TypeError");
+ finish();
+ })
+});
+
+// Test that a the promise is resolved with a valid source buffer.
+var xhr = new XMLHttpRequest();
+xhr.open("GET", "ting-44.1k-1ch.ogg", true);
+xhr.responseType = "arraybuffer";
+xhr.onload = function() {
+ var p = ac.decodeAudioData(xhr.response);
+ ok(p instanceof Promise, "AudioContext.decodeAudioData should return a Promise");
+ p.then(function(data) {
+ ok(data instanceof AudioBuffer, "Promise should resolve, passing an AudioBuffer");
+ ok(true, "Promise should resolve with a valid source buffer.");
+ finish();
+ }).catch(function() {
+ ok(false, "Promise should not be rejected with a valid source buffer.");
+ finish();
+ });
+};
+xhr.send();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_decodeAudioError.html b/dom/media/webaudio/test/test_decodeAudioError.html
new file mode 100644
index 0000000000..f18b971ac4
--- /dev/null
+++ b/dom/media/webaudio/test/test_decodeAudioError.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test the decodeAudioData Errors</title>
+
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ <script src="webaudio.js"></script>
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+
+var finished = 0;
+
+var ctx = new AudioContext();
+
+function errorExpectedWithFile(file, errorMsg) {
+ var xhr = new XMLHttpRequest();
+ function test(e) {
+ ok(e instanceof DOMException,
+ "The exception should be an instance of DOMException");
+ ok(e.name == "EncodingError",
+ "The exception name should be EncodingError");
+ ok(e.message == errorMsg,
+ "The exception message is not the one intended.\n" +
+ "\tExpected : " + errorMsg + "\n" +
+ "\tGot : " + e.message );
+ finish();
+ }
+ xhr.open("GET", file, true);
+ xhr.responseType = "arraybuffer";
+ xhr.onload = function() {
+ ctx.decodeAudioData(xhr.response, buffer => {
+ ok(false, "You should not be able to decode that");
+ finish();
+ }, e => test(e))
+ .then(buffer => {
+ ok(false, "You should not be able to decode that");
+ finish();
+ })
+ .catch(e => test(e));
+ };
+ xhr.send();
+}
+
+function finish() {
+ if (++finished == 4) {
+ SimpleTest.finish();
+ }
+}
+
+// Unknown Content
+errorExpectedWithFile("404", "The buffer passed to decodeAudioData contains an unknown content type.");
+
+// Invalid Content
+errorExpectedWithFile("invalidContent.flac", "The buffer passed to decodeAudioData contains invalid content which cannot be decoded successfully.");
+
+// No Audio
+// # Bug 1656032
+// Think about increasing the finish counter to 6 when activating this line
+// errorExpectedWithFile("noaudio.webm", "The buffer passed to decodeAudioData does not contain any audio.");
+
+// Unknown Error
+// errorExpectedWithFile("There is no file we can't handle", "An unknown error occurred while processing decodeAudioData.");
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_decodeMultichannel.html b/dom/media/webaudio/test/test_decodeMultichannel.html
new file mode 100644
index 0000000000..a799c641ee
--- /dev/null
+++ b/dom/media/webaudio/test/test_decodeMultichannel.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+<meta charset=utf-8>
+<head>
+ <title>Test that we can decode multichannel file with webaudio and &lt;audio&gt;</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var testcases = [
+ {
+ filename: "audio-quad.wav",
+ channels: 4
+ },
+ {
+ filename: "8kHz-320kbps-6ch.aac",
+ channels: 6
+ }
+];
+
+SimpleTest.waitForExplicitFinish();
+
+function decodeUsingAudioElement(filename, resolve) {
+ var a = new Audio();
+ a.addEventListener("error", function() {
+ ok(false, "Error loading metadata");
+ resolve();
+ });
+ a.addEventListener("loadedmetadata", function() {
+ ok(true, "Metadata Loaded");
+ resolve();
+ });
+
+ a.src = filename;
+ a.load();
+}
+
+function testOne({filename, channels}) {
+ return new Promise(resolve => {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", filename);
+ xhr.responseType = "arraybuffer";
+ xhr.onload = function() {
+ var context = new AudioContext();
+ context.decodeAudioData(xhr.response, function(b) {
+ ok(true, "Decoding of a wave file with four channels succeded.");
+ is(b.numberOfChannels,
+ channels,
+ `The AudioBuffer decoded from ${filename} should have ${channels} channels.`);
+ decodeUsingAudioElement(filename, resolve);
+ }, function() {
+ ok(false, `Decoding ${filename} failed)`);
+ decodeUsingAudioElement(filename, resolve);
+ });
+ };
+ xhr.send(null);
+ });
+}
+
+async function runTest() {
+ for (var testcase of testcases) {
+ await testOne(testcase);
+ }
+
+ SimpleTest.finish();
+}
+
+addLoadEvent(runTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_decodeOpusTail.html b/dom/media/webaudio/test/test_decodeOpusTail.html
new file mode 100644
index 0000000000..451b2b6a23
--- /dev/null
+++ b/dom/media/webaudio/test/test_decodeOpusTail.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<meta charset="utf-8">
+<head>
+ <title>Regression test to check that opus files don't have a tail at the end.</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+// This gets a 1 second Opus file and decodes it to a buffer. The opus file is
+// decoded at 48kHz, and the OfflineAudioContext is also at 48kHz, no resampling
+// is taking place.
+fetch('sweep-300-330-1sec.opus')
+.then(function(response) { return response.arrayBuffer(); })
+.then(function(buffer) {
+ var off = new OfflineAudioContext(1, 128, 48000);
+ off.decodeAudioData(buffer, function(decoded) {
+ var pcm = decoded.getChannelData(0);
+ is(pcm.length, 48000, "The length of the decoded file is correct.");
+ SimpleTest.finish();
+ });
+});
+
+</script>
diff --git a/dom/media/webaudio/test/test_decoderDelay.html b/dom/media/webaudio/test/test_decoderDelay.html
new file mode 100644
index 0000000000..d0fbfbed29
--- /dev/null
+++ b/dom/media/webaudio/test/test_decoderDelay.html
@@ -0,0 +1,144 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>Test that decoder delay is handled</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+ <script class="testbody" type="text/javascript">
+ SimpleTest.waitForExplicitFinish();
+ const {AppConstants} =
+ SpecialPowers.ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+
+ var tests_half_a_second = [
+ "half-a-second-1ch-44100-aac.mp4",
+ "half-a-second-1ch-44100-flac.flac",
+ "half-a-second-1ch-44100-libmp3lame.mp3",
+ "half-a-second-1ch-44100-libopus.opus",
+ "half-a-second-1ch-44100-libopus.webm",
+ "half-a-second-1ch-44100-libvorbis.ogg",
+ "half-a-second-1ch-44100.wav",
+ "half-a-second-1ch-48000-aac.mp4",
+ "half-a-second-1ch-48000-flac.flac",
+ "half-a-second-1ch-48000-libmp3lame.mp3",
+ "half-a-second-1ch-48000-libopus.opus",
+ "half-a-second-1ch-48000-libopus.webm",
+ "half-a-second-1ch-48000-libvorbis.ogg",
+ "half-a-second-1ch-48000.wav",
+ "half-a-second-2ch-44100-aac.mp4",
+ "half-a-second-2ch-44100-flac.flac",
+ "half-a-second-2ch-44100-libmp3lame.mp3",
+ "half-a-second-2ch-44100-libopus.opus",
+ "half-a-second-2ch-44100-libopus.webm",
+ "half-a-second-2ch-44100-libvorbis.ogg",
+ "half-a-second-2ch-44100.wav",
+ "half-a-second-2ch-48000-aac.mp4",
+ "half-a-second-2ch-48000-flac.flac",
+ "half-a-second-2ch-48000-libmp3lame.mp3",
+ "half-a-second-2ch-48000-libopus.opus",
+ "half-a-second-2ch-48000-libopus.webm",
+ "half-a-second-2ch-48000-libvorbis.ogg",
+ "half-a-second-2ch-48000.wav",
+ ];
+
+ // Those files are almost exactly half a second, but don't have enough pre-roll/padding
+ // information in the container, or the container isn't parsed properly, so
+ // aren't trimmed appropriately.
+ // vorbis webm, opus mp4, aac adts
+ var tests_adts = [
+ "half-a-second-1ch-44100-aac.aac",
+ "half-a-second-1ch-44100-libopus.mp4",
+ "half-a-second-1ch-44100-libvorbis.webm",
+ "half-a-second-1ch-48000-aac.aac",
+ "half-a-second-1ch-48000-libopus.mp4",
+ "half-a-second-1ch-48000-libvorbis.webm",
+ "half-a-second-2ch-44100-aac.aac",
+ "half-a-second-2ch-44100-libopus.mp4",
+ "half-a-second-2ch-44100-libvorbis.webm",
+ "half-a-second-2ch-48000-aac.aac",
+ "half-a-second-2ch-48000-libopus.mp4",
+ "half-a-second-2ch-48000-libvorbis.webm",
+ ];
+
+ // Other files that have interesting characteristics.
+ var tests_others = [
+ {
+ // Very short VBR file, 16 frames of audio at 44100. Padding spanning two
+ // packets.
+ "path": "sixteen-frames.mp3",
+ "frameCount": 16,
+ "samplerate": 44100,
+ "fuzz": {}
+ },
+ {
+ // This is incorrect (the duration should be 0.5s exactly)
+ // This is tracked in https://github.com/mozilla/mp4parse-rust/issues/404
+ "path":"half-a-second-1ch-44100-aac-afconvert.mp4",
+ "frameCount": 22464,
+ "samplerate": 44100,
+ "fuzz": {
+ "android": 2
+ }
+ }
+ ];
+
+ var all_tests = [tests_half_a_second, tests_adts, tests_others].flat();
+
+ var count = 0;
+ function checkDone() {
+ if (++count == all_tests.length) {
+ SimpleTest.finish();
+ }
+ }
+
+ async function doit() {
+ var context = new OfflineAudioContext(1, 128, 48000);
+ tests_half_a_second.forEach(async testfile => {
+ var response = await fetch(testfile);
+ var buffer = await response.arrayBuffer();
+ var decoded = await context.decodeAudioData(buffer);
+ is(
+ decoded.duration,
+ 0.5,
+ "The file " + testfile + " is half a second."
+ );
+ // Value found empirically after looking at the files. The initial
+ // amplitude should be 0 at phase 0 because those files are sine wave.
+ // The compression is sometimes lossy and the first sample is not always
+ // exactly 0.0.
+ ok(
+ Math.abs(decoded.getChannelData(0)[0]) <= 0.022,
+ `The start point for ${testfile} is correct ${ decoded.getChannelData(0)[0] }`
+ );
+ checkDone();
+ });
+ tests_adts.forEach(async testfile => {
+ var response = await fetch(testfile);
+ var buffer = await response.arrayBuffer();
+ var decoded = await context.decodeAudioData(buffer);
+ // Value found empirically after looking at the files. ADTS containers
+ // don't have encoder delay / padding info so we can't trim correctly.
+ ok(
+ Math.abs(decoded.duration - 0.5) < 0.02,
+ `The ADTS file ${testfile} is about half a second (${decoded.duration}, error: ${Math.abs(decoded.duration-0.5)}).`
+ );
+ checkDone();
+ });
+ tests_others.forEach(async test => {
+ // Get an context at a specific rate to avoid duration changes due to resampling.
+ var contextAtRate = new OfflineAudioContext(1, 128, test.samplerate);
+ var response = await fetch(test.path);
+ var buffer = await response.arrayBuffer();
+ var decoded = await contextAtRate.decodeAudioData(buffer);
+ const fuzz = test.fuzz[AppConstants.platform] ?? 0;
+ ok(Math.abs(decoded.length - test.frameCount) <= fuzz, `${test.path} is ${decoded.length} frames long`);
+ checkDone();
+ });
+ }
+
+ doit();
+ </script>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_delayNode.html b/dom/media/webaudio/test/test_delayNode.html
new file mode 100644
index 0000000000..89172aa86f
--- /dev/null
+++ b/dom/media/webaudio/test/test_delayNode.html
@@ -0,0 +1,101 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test DelayNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script src="webaudio.js" type="text/javascript"></script>
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 4096,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var delay = new DelayNode(context);
+ ok(delay.delayTime, "The audioparam member must exist");
+ is(delay.delayTime.value, 0, "Correct initial value");
+ is(delay.delayTime.defaultValue, 0, "Correct default value");
+ delay.delayTime.value = 0.5;
+ is(delay.delayTime.value, 0.5, "Correct initial value");
+ is(delay.delayTime.defaultValue, 0, "Correct default value");
+
+ delay = new DelayNode(context, { delayTime: 0.5 });
+ ok(delay.delayTime, "The audioparam member must exist");
+ is(delay.delayTime.value, 0.5, "Correct initial value");
+ is(delay.delayTime.defaultValue, 0, "Correct default value");
+
+ var buffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ }
+
+ var source = context.createBufferSource();
+
+ delay = context.createDelay();
+
+ source.buffer = buffer;
+
+ source.connect(delay);
+
+ ok(delay.delayTime, "The audioparam member must exist");
+ is(delay.delayTime.value, 0, "Correct initial value");
+ is(delay.delayTime.defaultValue, 0, "Correct default value");
+ delay.delayTime.value = 0.5;
+ is(delay.delayTime.value, 0.5, "Correct initial value");
+ is(delay.delayTime.defaultValue, 0, "Correct default value");
+ is(delay.channelCount, 2, "delay node has 2 input channels by default");
+ is(delay.channelCountMode, "max", "Correct channelCountMode for the delay node");
+ is(delay.channelInterpretation, "speakers", "Correct channelCountInterpretation for the delay node");
+
+ expectException(function() {
+ context.createDelay(0);
+ }, DOMException.NOT_SUPPORTED_ERR);
+ expectException(function() {
+ context.createDelay(180);
+ }, DOMException.NOT_SUPPORTED_ERR);
+ expectTypeError(function() {
+ context.createDelay(NaN);
+ }, DOMException.NOT_SUPPORTED_ERR);
+ expectException(function() {
+ context.createDelay(-1);
+ }, DOMException.NOT_SUPPORTED_ERR);
+
+ expectException(function() {
+ new DelayNode(context, { maxDelayTime: 0 });
+ }, DOMException.NOT_SUPPORTED_ERR);
+ expectException(function() {
+ new DelayNode(context, { maxDelayTime: 180 });
+ }, DOMException.NOT_SUPPORTED_ERR);
+ expectTypeError(function() {
+ new DelayNode(context, { maxDelayTime: NaN });
+ }, DOMException.NOT_SUPPORTED_ERR);
+ expectException(function() {
+ new DelayNode(context, { maxDelayTime: -1 });
+ }, DOMException.NOT_SUPPORTED_ERR);
+
+ context.createDelay(1); // should not throw
+
+ // Delay the source stream by 2048 frames
+ delay.delayTime.value = 2048 / context.sampleRate;
+
+ source.start(0);
+ return delay;
+ },
+ createExpectedBuffers(context) {
+ var expectedBuffer = context.createBuffer(1, 2048 * 2, context.sampleRate);
+ for (var i = 2048; i < 2048 * 2; ++i) {
+ expectedBuffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * (i - 2048) / context.sampleRate);
+ }
+ return expectedBuffer;
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_delayNodeAtMax.html b/dom/media/webaudio/test/test_delayNodeAtMax.html
new file mode 100644
index 0000000000..3d0afba0ac
--- /dev/null
+++ b/dom/media/webaudio/test/test_delayNodeAtMax.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test DelayNode with maxDelayTime delay - bug 890528</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script src="webaudio.js" type="text/javascript"></script>
+<script class="testbody" type="text/javascript">
+
+const signalLength = 2048;
+const delayLength = 1000; // Not on a block boundary
+const outputLength = 4096 // > signalLength + 2 * delayLength;
+
+function applySignal(buffer, offset) {
+ for (var i = 0; i < signalLength; ++i) {
+ buffer.getChannelData(0)[offset + i] = Math.cos(Math.PI * i / signalLength);
+ }
+}
+
+var gTest = {
+ numberOfChannels: 1,
+ createGraph(context) {
+ var buffer = context.createBuffer(1, signalLength, context.sampleRate);
+ applySignal(buffer, 0);
+
+ var source = context.createBufferSource();
+ source.buffer = buffer;
+
+ const delayTime = delayLength / context.sampleRate;
+ var delay = context.createDelay(delayTime);
+ delay.delayTime.value = delayTime;
+
+ source.connect(delay);
+
+ source.start(0);
+ return delay;
+ },
+ createExpectedBuffers(context) {
+ var expectedBuffer = context.createBuffer(1, outputLength, context.sampleRate);
+ applySignal(expectedBuffer, delayLength);
+ return expectedBuffer;
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_delayNodeChannelChanges.html b/dom/media/webaudio/test/test_delayNodeChannelChanges.html
new file mode 100644
index 0000000000..d40c792ef7
--- /dev/null
+++ b/dom/media/webaudio/test/test_delayNodeChannelChanges.html
@@ -0,0 +1,98 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>test DelayNode channel count changes</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestCompleteLog();
+
+const bufferSize = 4096;
+
+var ctx;
+var testDelay;
+var stereoDelay;
+var invertor;
+
+function compareOutputs(callback) {
+ var processor = ctx.createScriptProcessor(bufferSize, 2, 0);
+ testDelay.connect(processor);
+ invertor.connect(processor);
+ processor.onaudioprocess =
+ function(e) {
+ compareBuffers(e.inputBuffer,
+ ctx.createBuffer(2, bufferSize, ctx.sampleRate));
+ e.target.onaudioprocess = null;
+ callback();
+ }
+}
+
+function startTest() {
+ // And a two-channel signal
+ var merger = ctx.createChannelMerger();
+ merger.connect(testDelay);
+ merger.connect(stereoDelay);
+ var oscL = ctx.createOscillator();
+ oscL.connect(merger, 0, 0);
+ oscL.start(0);
+ var oscR = ctx.createOscillator();
+ oscR.type = "sawtooth";
+ oscR.connect(merger, 0, 1);
+ oscR.start(0);
+
+ compareOutputs(
+ function () {
+ // Disconnect the two-channel signal and test again
+ merger.disconnect();
+ compareOutputs(SimpleTest.finish);
+ });
+}
+
+function prepareTest() {
+ ctx = new AudioContext();
+
+ // The output of a test delay node with mono and stereo input will be
+ // compared with that of separate mono and stereo delay nodes.
+ const delayTime = 0.3 * bufferSize / ctx.sampleRate;
+ testDelay = ctx.createDelay(delayTime);
+ testDelay.delayTime.value = delayTime;
+ monoDelay = ctx.createDelay(delayTime);
+ monoDelay.delayTime.value = delayTime;
+ stereoDelay = ctx.createDelay(delayTime);
+ stereoDelay.delayTime.value = delayTime;
+
+ // Create a one-channel signal and connect to the delay nodes
+ var monoOsc = ctx.createOscillator();
+ monoOsc.frequency.value = 110;
+ monoOsc.connect(testDelay);
+ monoOsc.connect(monoDelay);
+ monoOsc.start(0);
+
+ // Invert the expected so that mixing with the test will find the difference.
+ invertor = ctx.createGain();
+ invertor.gain.value = -1.0;
+ monoDelay.connect(invertor);
+ stereoDelay.connect(invertor);
+
+ // Start the test after the delay nodes have begun processing.
+ var processor = ctx.createScriptProcessor(bufferSize, 1, 0);
+ processor.connect(ctx.destination);
+
+ processor.onaudioprocess =
+ function(e) {
+ e.target.onaudioprocess = null;
+ processor.disconnect();
+ startTest();
+ };
+}
+prepareTest();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_delayNodeCycles.html b/dom/media/webaudio/test/test_delayNodeCycles.html
new file mode 100644
index 0000000000..82c5f62504
--- /dev/null
+++ b/dom/media/webaudio/test/test_delayNodeCycles.html
@@ -0,0 +1,157 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test the support of cycles.</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script src="webaudio.js" type="text/javascript"></script>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+const sampleRate = 48000;
+const inputLength = 2048;
+
+addLoadEvent(function() {
+ function addSine(b) {
+ for (var i = 0; i < b.length; i++) {
+ b[i] += Math.sin(440 * 2 * Math.PI * i / sampleRate);
+ }
+ }
+
+ function getSineBuffer(ctx) {
+ var buffer = ctx.createBuffer(1, inputLength, ctx.sampleRate);
+ addSine(buffer.getChannelData(0));
+ return buffer;
+ }
+
+ function createAndPlayWithCycleAndDelayNode(ctx, delayFrames) {
+ var source = ctx.createBufferSource();
+ source.buffer = getSineBuffer(ctx);
+
+ var gain = ctx.createGain();
+ var delay = ctx.createDelay();
+ delay.delayTime.value = delayFrames/ctx.sampleRate;
+
+ source.connect(gain);
+ gain.connect(delay);
+ delay.connect(ctx.destination);
+ // cycle
+ delay.connect(gain);
+
+ source.start(0);
+ }
+
+ function createAndPlayWithCycleAndNoDelayNode(ctx) {
+ var source = ctx.createBufferSource();
+ source.loop = true;
+ source.buffer = getSineBuffer(ctx);
+
+ var gain = ctx.createGain();
+ var gain2 = ctx.createGain();
+
+ source.connect(gain);
+ gain.connect(gain2);
+ // cycle
+ gain2.connect(gain);
+ gain2.connect(ctx.destination);
+
+ source.start(0);
+ }
+
+ function createAndPlayWithCycleAndNoDelayNodeInCycle(ctx) {
+ var source = ctx.createBufferSource();
+ source.loop = true;
+ source.buffer = getSineBuffer(ctx);
+
+ var delay = ctx.createDelay();
+ var gain = ctx.createGain();
+ var gain2 = ctx.createGain();
+
+ // Their is a cycle, a delay, but the delay is not in the cycle.
+ source.connect(delay);
+ delay.connect(gain);
+ gain.connect(gain2);
+ // cycle
+ gain2.connect(gain);
+ gain2.connect(ctx.destination);
+
+ source.start(0);
+ }
+
+ var remainingTests = 0;
+ function finish() {
+ if (--remainingTests == 0) {
+ SimpleTest.finish();
+ }
+ }
+
+ function getOfflineContext(oncomplete) {
+ var ctx = new OfflineAudioContext(1, sampleRate, sampleRate);
+ ctx.oncomplete = oncomplete;
+ return ctx;
+ }
+
+ function checkSilentBuffer(e) {
+ var buffer = e.renderedBuffer.getChannelData(0);
+ for (var i = 0; i < buffer.length; i++) {
+ if (buffer[i] != 0.0) {
+ ok(false, "buffer should be silent.");
+ finish();
+ return;
+ }
+ }
+ ok(true, "buffer should be silent.");
+ finish();
+ }
+
+ function checkNoisyBuffer(e, aDelayFrames) {
+ delayFrames = Math.max(128, aDelayFrames);
+
+ var expected = new Float32Array(e.renderedBuffer.length);
+ for (var i = delayFrames; i < expected.length; i += delayFrames) {
+ addSine(expected.subarray(i, i + inputLength));
+ }
+
+ compareChannels(e.renderedBuffer.getChannelData(0), expected);
+ finish();
+ }
+
+ function expectSilentOutput(f) {
+ remainingTests++;
+ var ctx = getOfflineContext(checkSilentBuffer);
+ f(ctx);
+ ctx.startRendering();
+ }
+
+ function expectNoisyOutput(delayFrames) {
+ remainingTests++;
+ var ctx = getOfflineContext();
+ ctx.oncomplete = function(e) { checkNoisyBuffer(e, delayFrames); };
+ createAndPlayWithCycleAndDelayNode(ctx, delayFrames);
+ ctx.startRendering();
+ }
+
+ // This is trying to make a graph with a cycle and no DelayNode in the graph.
+ // The cycle subgraph should be muted, in this graph the output should be silent.
+ expectSilentOutput(createAndPlayWithCycleAndNoDelayNode);
+ // This is trying to make a graph with a cycle and a DelayNode in the graph, but
+ // not part of the cycle.
+ // The cycle subgraph should be muted, in this graph the output should be silent.
+ expectSilentOutput(createAndPlayWithCycleAndNoDelayNodeInCycle);
+ // Those are making legal graphs, with at least one DelayNode in the cycle.
+ // There should be some non-silent output.
+ expectNoisyOutput(sampleRate/4);
+ // DelayNode.delayTime will be clamped to 128/ctx.sampleRate.
+ // There should be some non-silent output.
+ expectNoisyOutput(0);
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_delayNodePassThrough.html b/dom/media/webaudio/test/test_delayNodePassThrough.html
new file mode 100644
index 0000000000..0c2d1db30a
--- /dev/null
+++ b/dom/media/webaudio/test/test_delayNodePassThrough.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test DelayNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script src="webaudio.js" type="text/javascript"></script>
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 4096,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var source = context.createBufferSource();
+
+ var delay = context.createDelay();
+
+ source.buffer = this.buffer;
+
+ source.connect(delay);
+
+ delay.delayTime.value = 0.5;
+
+ // Delay the source stream by 2048 frames
+ delay.delayTime.value = 2048 / context.sampleRate;
+
+ var delayWrapped = SpecialPowers.wrap(delay);
+ ok("passThrough" in delayWrapped, "DelayNode should support the passThrough API");
+ delayWrapped.passThrough = true;
+
+ source.start(0);
+ return delay;
+ },
+ createExpectedBuffers(context) {
+ this.buffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ this.buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ }
+ var silence = context.createBuffer(1, 2048, context.sampleRate);
+
+ return [this.buffer, silence];
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_delayNodeSmallMaxDelay.html b/dom/media/webaudio/test/test_delayNodeSmallMaxDelay.html
new file mode 100644
index 0000000000..235a55a4c8
--- /dev/null
+++ b/dom/media/webaudio/test/test_delayNodeSmallMaxDelay.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test DelayNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script src="webaudio.js" type="text/javascript"></script>
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 2048,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var source = context.createBufferSource();
+
+ var delay = context.createDelay(0.02);
+
+ source.buffer = this.buffer;
+
+ source.connect(delay);
+
+ source.start(0);
+ return delay;
+ },
+ createExpectedBuffers(context) {
+ var expectedBuffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ expectedBuffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ }
+ this.buffer = expectedBuffer;
+ return expectedBuffer;
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_delayNodeTailIncrease.html b/dom/media/webaudio/test/test_delayNodeTailIncrease.html
new file mode 100644
index 0000000000..a511a4a3ff
--- /dev/null
+++ b/dom/media/webaudio/test/test_delayNodeTailIncrease.html
@@ -0,0 +1,71 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test increasing delay of DelayNode after input finishes</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+const signalLength = 100;
+const bufferSize = 1024;
+// Delay should be long enough to allow CC to run
+const delayBufferCount = 50;
+const delayLength = delayBufferCount * bufferSize + 700;
+
+var count = 0;
+
+function applySignal(buffer, offset) {
+ for (var i = 0; i < signalLength; ++i) {
+ buffer.getChannelData(0)[offset + i] = Math.cos(Math.PI * i / signalLength);
+ }
+}
+
+function onAudioProcess(e) {
+ switch(count) {
+ case 5:
+ SpecialPowers.forceGC();
+ SpecialPowers.forceCC();
+ break;
+ case delayBufferCount:
+ var offset = delayLength - count * bufferSize;
+ var ctx = e.target.context;
+ var expected = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
+ applySignal(expected, offset);
+ compareBuffers(e.inputBuffer, expected);
+ SimpleTest.finish();
+ }
+ count++;
+}
+
+function startTest() {
+ var ctx = new AudioContext();
+ var processor = ctx.createScriptProcessor(bufferSize, 1, 0);
+ processor.onaudioprocess = onAudioProcess;
+
+ // Switch on delay at a time in the future.
+ var delayDuration = delayLength / ctx.sampleRate;
+ var delayStartTime = (delayLength - bufferSize) / ctx.sampleRate;
+ var delay = ctx.createDelay(delayDuration);
+ delay.delayTime.setValueAtTime(delayDuration, delayStartTime);
+ delay.connect(processor);
+
+ // Short signal that finishes before switching to long delay
+ var buffer = ctx.createBuffer(1, signalLength, ctx.sampleRate);
+ applySignal(buffer, 0);
+ var source = ctx.createBufferSource();
+ source.buffer = buffer;
+ source.start();
+ source.connect(delay);
+};
+
+startTest();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_delayNodeTailWithDisconnect.html b/dom/media/webaudio/test/test_delayNodeTailWithDisconnect.html
new file mode 100644
index 0000000000..c6723f643d
--- /dev/null
+++ b/dom/media/webaudio/test/test_delayNodeTailWithDisconnect.html
@@ -0,0 +1,95 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test tail time lifetime of DelayNode after input is disconnected</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// Web Audio doesn't provide a means to precisely time disconnect()s but we
+// can test that the output of delay nodes matches the output from their
+// sources before they are disconnected.
+
+SimpleTest.waitForExplicitFinish();
+
+const signalLength = 128;
+const bufferSize = 4096;
+const sourceCount = bufferSize / signalLength;
+// Delay should be long enough to allow CC to run
+var delayBufferCount = 20;
+const delayLength = delayBufferCount * bufferSize;
+
+var sourceOutput = new Float32Array(bufferSize);
+var delayOutputCount = 0;
+var sources = [];
+
+function onDelayOutput(e) {
+ if (delayOutputCount < delayBufferCount) {
+ delayOutputCount++;
+ return;
+ }
+
+ compareChannels(e.inputBuffer.getChannelData(0), sourceOutput);
+ e.target.onaudioprocess = null;
+ SimpleTest.finish();
+}
+
+function onSourceOutput(e) {
+ // Record the first buffer
+ e.inputBuffer.copyFromChannel(sourceOutput, 0);
+ e.target.onaudioprocess = null;
+}
+
+function disconnectSources() {
+ for (var i = 0; i < sourceCount; ++i) {
+ sources[i].disconnect();
+ }
+
+ SpecialPowers.forceGC();
+ SpecialPowers.forceCC();
+}
+
+function startTest() {
+ var ctx = new AudioContext();
+
+ var sourceProcessor = ctx.createScriptProcessor(bufferSize, 1, 0);
+ sourceProcessor.onaudioprocess = onSourceOutput;
+ // Keep audioprocess events going after source disconnect.
+ sourceProcessor.connect(ctx.destination);
+
+ var delayProcessor = ctx.createScriptProcessor(bufferSize, 1, 0);
+ delayProcessor.onaudioprocess = onDelayOutput;
+
+ var delayDuration = delayLength / ctx.sampleRate;
+ for (var i = 0; i < sourceCount; ++i) {
+ var delay = ctx.createDelay(delayDuration);
+ delay.delayTime.value = delayDuration;
+ delay.connect(delayProcessor);
+
+ var source = ctx.createOscillator();
+ source.frequency.value = 440 + 10 * i
+ source.start(i * signalLength / ctx.sampleRate);
+ source.stop((i + 1) * signalLength / ctx.sampleRate);
+ source.connect(delay);
+ source.connect(sourceProcessor);
+
+ sources[i] = source;
+ }
+
+ // Assuming the above Web Audio operations have already scheduled an event
+ // to run in stable state and start the graph thread, schedule a subsequent
+ // event to disconnect the sources, which will remove main thread connection
+ // references before it knows the graph thread has started using the source
+ // streams.
+ SimpleTest.executeSoon(disconnectSources);
+};
+
+startTest();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_delayNodeTailWithGain.html b/dom/media/webaudio/test/test_delayNodeTailWithGain.html
new file mode 100644
index 0000000000..60cca276c0
--- /dev/null
+++ b/dom/media/webaudio/test/test_delayNodeTailWithGain.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test tail time lifetime of DelayNode indirectly connected to source</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+const signalLength = 130;
+const bufferSize = 1024;
+// Delay should be long enough to allow CC to run
+const delayBufferCount = 50;
+const delayLength = delayBufferCount * bufferSize + 700;
+
+var count = 0;
+
+function applySignal(buffer, offset) {
+ for (var i = 0; i < signalLength; ++i) {
+ buffer.getChannelData(0)[offset + i] = Math.cos(Math.PI * i / signalLength);
+ }
+}
+
+function onAudioProcess(e) {
+ switch(count) {
+ case 5:
+ SpecialPowers.forceGC();
+ SpecialPowers.forceCC();
+ break;
+ case delayBufferCount:
+ var offset = delayLength - count * bufferSize;
+ var ctx = e.target.context;
+ var expected = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
+ applySignal(expected, offset);
+ compareBuffers(e.inputBuffer, expected);
+ SimpleTest.finish();
+ }
+ count++;
+}
+
+function startTest() {
+ var ctx = new AudioContext();
+ var processor = ctx.createScriptProcessor(bufferSize, 1, 0);
+ processor.onaudioprocess = onAudioProcess;
+
+ var delayDuration = delayLength / ctx.sampleRate;
+ var delay = ctx.createDelay(delayDuration);
+ delay.delayTime.value = delayDuration;
+ delay.connect(processor);
+
+ var gain = ctx.createGain();
+ gain.connect(delay);
+
+ // Short signal that finishes before garbage collection
+ var buffer = ctx.createBuffer(1, signalLength, ctx.sampleRate);
+ applySignal(buffer, 0);
+ var source = ctx.createBufferSource();
+ source.buffer = buffer;
+ source.start();
+ source.connect(gain);
+};
+
+startTest();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_delayNodeTailWithReconnect.html b/dom/media/webaudio/test/test_delayNodeTailWithReconnect.html
new file mode 100644
index 0000000000..da5f02b052
--- /dev/null
+++ b/dom/media/webaudio/test/test_delayNodeTailWithReconnect.html
@@ -0,0 +1,136 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test tail time lifetime of DelayNode after input finishes and new input added</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// The buffer source will start on a block boundary, so keeping the signal
+// within one block ensures that it will not cross AudioProcessingEvent buffer
+// boundaries.
+const signalLength = 128;
+const bufferSize = 1024;
+// Delay should be long enough to allow CC to run
+var delayBufferCount = 50;
+var delayBufferOffset;
+const delayLength = delayBufferCount * bufferSize;
+
+var phase = "initial";
+var sourceCount = 0;
+var delayCount = 0;
+var oscillator;
+var delay;
+var source;
+
+function applySignal(buffer, offset) {
+ for (var i = 0; i < signalLength; ++i) {
+ buffer.getChannelData(0)[offset + i] = Math.cos(Math.PI * i / signalLength);
+ }
+}
+
+function bufferIsSilent(buffer, out) {
+ for (var i = 0; i < buffer.length; ++i) {
+ if (buffer.getChannelData(0)[i] != 0) {
+ if (out) {
+ out.soundOffset = i;
+ }
+ return false;
+ }
+ }
+ return true;
+}
+
+function onDelayOutput(e) {
+ switch(phase) {
+
+ case "initial":
+ // Wait for oscillator sound to exit delay
+ if (bufferIsSilent(e.inputBuffer))
+ break;
+
+ phase = "played oscillator";
+ break;
+
+ case "played oscillator":
+ // First tail time has expired. Start second source and remove references
+ // to the delay and connected second source.
+ oscillator.disconnect();
+ source.connect(delay);
+ source.start();
+ source = null;
+ delay = null;
+ phase = "started second source";
+ break;
+
+ case "second tail time":
+ if (delayCount == delayBufferCount) {
+ var ctx = e.target.context;
+ var expected = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
+ applySignal(expected, delayBufferOffset);
+ compareBuffers(e.inputBuffer, expected);
+ e.target.onaudioprocess = null;
+ SimpleTest.finish();
+ }
+ }
+
+ delayCount++;
+}
+
+function onSourceOutput(e) {
+ switch(phase) {
+ case "started second source":
+ var out = {};
+ if (!bufferIsSilent(e.inputBuffer, out)) {
+ delayBufferCount += sourceCount;
+ delayBufferOffset = out.soundOffset;
+ phase = "played second source";
+ }
+ break;
+ case "played second source":
+ SpecialPowers.forceGC();
+ SpecialPowers.forceCC();
+ phase = "second tail time";
+ e.target.onaudioprocess = null;
+ }
+
+ sourceCount++;
+}
+
+function startTest() {
+ var ctx = new AudioContext();
+ var delayDuration = delayLength / ctx.sampleRate;
+ delay = ctx.createDelay(delayDuration);
+ delay.delayTime.value = delayDuration;
+ var processor1 = ctx.createScriptProcessor(bufferSize, 1, 0);
+ delay.connect(processor1);
+ processor1.onaudioprocess = onDelayOutput;
+
+ // Signal to trigger initial tail time reference
+ oscillator = ctx.createOscillator();
+ oscillator.start(0);
+ oscillator.stop(100/ctx.sampleRate);
+ oscillator.connect(delay);
+
+ // Short signal, not started yet, with a ScriptProcessor to detect when it
+ // starts. It should finish before garbage collection.
+ var buffer = ctx.createBuffer(1, signalLength, ctx.sampleRate);
+ applySignal(buffer, 0);
+ source = ctx.createBufferSource();
+ source.buffer = buffer;
+ var processor2 = ctx.createScriptProcessor(bufferSize, 1, 0);
+ source.connect(processor2);
+ processor2.onaudioprocess = onSourceOutput;
+};
+
+startTest();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_delayNodeWithGain.html b/dom/media/webaudio/test/test_delayNodeWithGain.html
new file mode 100644
index 0000000000..af075c7439
--- /dev/null
+++ b/dom/media/webaudio/test/test_delayNodeWithGain.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test DelayNode with a GainNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script src="webaudio.js" type="text/javascript"></script>
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 4096,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var buffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ }
+
+ var source = context.createBufferSource();
+
+ var delay = context.createDelay();
+
+ source.buffer = buffer;
+
+ var gain = context.createGain();
+ gain.gain.value = 0.5;
+
+ source.connect(gain);
+ gain.connect(delay);
+
+ // Delay the source stream by 2048 frames
+ delay.delayTime.value = 2048 / context.sampleRate;
+
+ source.start(0);
+ return delay;
+ },
+ createExpectedBuffers(context) {
+ var expectedBuffer = context.createBuffer(1, 2048 * 2, context.sampleRate);
+ for (var i = 2048; i < 2048 * 2; ++i) {
+ expectedBuffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * (i - 2048) / context.sampleRate) / 2;
+ }
+ return expectedBuffer;
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_delaynode-channel-count-1.html b/dom/media/webaudio/test/test_delaynode-channel-count-1.html
new file mode 100644
index 0000000000..dd964ef9e3
--- /dev/null
+++ b/dom/media/webaudio/test/test_delaynode-channel-count-1.html
@@ -0,0 +1,104 @@
+<!DOCTYPE html>
+<title>Test that DelayNode output channelCount matches that of the delayed input</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+// See https://github.com/WebAudio/web-audio-api/issues/25
+
+// sampleRate is a power of two so that delay times are exact in base-2
+// floating point arithmetic.
+const SAMPLE_RATE = 32768;
+// Arbitrary delay time in frames (but this is assumed a multiple of block
+// size below):
+const DELAY_FRAMES = 3 * 128;
+// Implementations may apply interpolation to input samples, which can spread
+// the effect of input with larger channel counts over neighbouring blocks.
+// This test ignores enough neighbouring blocks to ignore the effects of
+// filter radius of up to this number of frames:
+const INTERPOLATION_GRACE = 128;
+// Number of frames of DelayNode output that are known to be stereo:
+const STEREO_FRAMES = 128;
+// The delay will be increased at this frame to switch DelayNode output back
+// to mono.
+const MONO_OUTPUT_START_FRAME =
+ DELAY_FRAMES + INTERPOLATION_GRACE + STEREO_FRAMES;
+// Number of frames of output that are known to be mono after the known stereo
+// and interpolation grace.
+const MONO_FRAMES = 128;
+// Total length allows for interpolation after effects of stereo input are
+// finished and one block to test return to mono output:
+const TOTAL_LENGTH =
+ MONO_OUTPUT_START_FRAME + INTERPOLATION_GRACE + MONO_FRAMES;
+// maxDelayTime, is a multiple of block size, because the Gecko implementation
+// once had a bug with delayTime = maxDelayTime in this situation:
+const MAX_DELAY_FRAMES = TOTAL_LENGTH + INTERPOLATION_GRACE;
+
+promise_test(() => {
+ let context = new OfflineAudioContext({numberOfChannels: 1,
+ length: TOTAL_LENGTH,
+ sampleRate: SAMPLE_RATE});
+
+ // Only channel 1 of the splitter is connected to the destination.
+ let splitter = new ChannelSplitterNode(context, {numberOfOutputs: 2});
+ splitter.connect(context.destination, 1);
+
+ // A gain node has channelCountMode "max" and channelInterpretation
+ // "speakers", and so will up-mix a mono input when there is stereo input.
+ let gain = new GainNode(context);
+ gain.connect(splitter);
+
+ // The delay node initially outputs a single channel of silence, when it
+ // does not have enough signal in its history to output what it has
+ // previously received. After the delay period, it will then output the
+ // stereo signal it received.
+ let delay =
+ new DelayNode(context,
+ {maxDelayTime: MAX_DELAY_FRAMES / context.sampleRate,
+ delayTime: DELAY_FRAMES / context.sampleRate});
+ // Schedule an increase in the delay to return to mono silent output from
+ // the unfilled portion of the DelayNode's buffer.
+ delay.delayTime.setValueAtTime(MAX_DELAY_FRAMES / context.sampleRate,
+ MONO_OUTPUT_START_FRAME / context.sampleRate);
+ delay.connect(gain);
+
+ let stereoMerger = new ChannelMergerNode(context, {numberOfInputs: 2});
+ stereoMerger.connect(delay);
+
+ let leftOffset = 0.125;
+ let rightOffset = 0.5;
+ let leftSource = new ConstantSourceNode(context, {offset: leftOffset});
+ let rightSource = new ConstantSourceNode(context, {offset: rightOffset});
+ leftSource.start();
+ rightSource.start();
+ leftSource.connect(stereoMerger, 0, 0);
+ rightSource.connect(stereoMerger, 0, 1);
+ // Connect a mono source directly to the gain, so that even stereo silence
+ // will be detected in channel 1 of the gain output because it will cause
+ // the mono source to be up-mixed.
+ let monoOffset = 0.25
+ let monoSource = new ConstantSourceNode(context, {offset: monoOffset});
+ monoSource.start();
+ monoSource.connect(gain);
+
+ return context.startRendering().
+ then((buffer) => {
+ let output = buffer.getChannelData(0);
+
+ function assert_samples_equal(startIndex, length, expected, description)
+ {
+ for (let i = startIndex; i < startIndex + length; ++i) {
+ assert_equals(output[i], expected, description + ` at ${i}`);
+ }
+ }
+
+ assert_samples_equal(0, DELAY_FRAMES - INTERPOLATION_GRACE,
+ 0, "Initial mono");
+ assert_samples_equal(DELAY_FRAMES + INTERPOLATION_GRACE, STEREO_FRAMES,
+ monoOffset + rightOffset, "Stereo");
+ assert_samples_equal(MONO_OUTPUT_START_FRAME + INTERPOLATION_GRACE,
+ MONO_FRAMES,
+ 0, "Final mono");
+ });
+});
+
+</script>
diff --git a/dom/media/webaudio/test/test_disconnectAll.html b/dom/media/webaudio/test/test_disconnectAll.html
new file mode 100644
index 0000000000..f67b969949
--- /dev/null
+++ b/dom/media/webaudio/test/test_disconnectAll.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Test whether we can disconnect an AudioNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ </head>
+ <body>
+ <pre id="test">
+ <script class="testbody" type="text/javascript">
+ var gTest = {
+ length: 256,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var sourceBuffer = context.createBuffer(1, 256, context.sampleRate);
+ var data = sourceBuffer.getChannelData(0);
+ for (var j = 0; j < data.length; j++) {
+ data[j] = 1;
+ }
+
+ var source = context.createBufferSource();
+ source.buffer = sourceBuffer;
+
+ var source = context.createBufferSource();
+ source.buffer = sourceBuffer;
+
+ var gain1 = context.createGain();
+ var gain2 = context.createGain();
+ var gain3 = context.createGain();
+ var merger = context.createChannelMerger(3);
+
+ source.connect(gain1);
+ source.connect(gain2);
+ source.connect(gain3);
+ gain1.connect(merger);
+ gain2.connect(merger);
+ gain3.connect(merger);
+ source.start();
+
+ source.disconnect();
+
+ return merger;
+ }
+ };
+
+ runTest();
+ </script>
+ </pre>
+ </body>
+</html> \ No newline at end of file
diff --git a/dom/media/webaudio/test/test_disconnectAudioParam.html b/dom/media/webaudio/test/test_disconnectAudioParam.html
new file mode 100644
index 0000000000..bfa4f92312
--- /dev/null
+++ b/dom/media/webaudio/test/test_disconnectAudioParam.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Test whether we can disconnect an AudioParam</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ </head>
+ <body>
+ <pre id="test">
+ <script class="testbody" type="text/javascript">
+ var gTest = {
+ length: 256,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var sourceBuffer = context.createBuffer(1, 256, context.sampleRate);
+ var data = sourceBuffer.getChannelData(0);
+ for (var j = 0; j < data.length; j++) {
+ data[j] = 1;
+ }
+
+ var source = context.createBufferSource();
+ source.buffer = sourceBuffer;
+
+ var half = context.createGain();
+ var gain1 = context.createGain();
+ var gain2 = context.createGain();
+
+ half.gain.value = 0.5;
+
+ source.connect(gain1);
+ gain1.connect(gain2);
+ source.connect(half);
+
+ half.connect(gain1.gain);
+ half.connect(gain2.gain);
+
+ half.disconnect(gain2.gain);
+
+ source.start();
+
+ return gain2;
+ },
+ createExpectedBuffers(context) {
+ expectedBuffer = context.createBuffer(1, 256, context.sampleRate);
+ for (var i = 0; i < 256; ++i) {
+ expectedBuffer.getChannelData(0)[i] = 1.5;
+ }
+
+ return expectedBuffer;
+ }
+ };
+
+ runTest();
+ </script>
+ </pre>
+ </body>
+</html>
diff --git a/dom/media/webaudio/test/test_disconnectAudioParamFromOutput.html b/dom/media/webaudio/test/test_disconnectAudioParamFromOutput.html
new file mode 100644
index 0000000000..533091f920
--- /dev/null
+++ b/dom/media/webaudio/test/test_disconnectAudioParamFromOutput.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Test whether we can disconnect an AudioParam</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ </head>
+ <body>
+ <pre id="test">
+ <script class="testbody" type="text/javascript">
+ var gTest = {
+ length: 256,
+ numberOfChannels: 2,
+ createGraph(context) {
+ var sourceBuffer = context.createBuffer(2, 256, context.sampleRate);
+ for (var i = 1; i <= 2; i++) {
+ var data = sourceBuffer.getChannelData(i-1);
+ for (var j = 0; j < data.length; j++) {
+ data[j] = i;
+ }
+ }
+
+ var source = context.createBufferSource();
+ source.buffer = sourceBuffer;
+
+ var source = context.createBufferSource();
+ source.buffer = sourceBuffer;
+
+ var half = context.createGain();
+ var gain1 = context.createGain();
+ var gain2 = context.createGain();
+ var splitter = context.createChannelSplitter(2);
+
+ half.gain.value = 0.5;
+
+ source.connect(gain1);
+ gain1.connect(gain2);
+ source.connect(half);
+ half.connect(splitter);
+ splitter.connect(gain1.gain, 0);
+ splitter.connect(gain2.gain, 1);
+
+ splitter.disconnect(gain2.gain, 1);
+
+ source.start();
+
+ return gain2;
+ },
+ createExpectedBuffers(context) {
+ var expectedBuffer = context.createBuffer(2, 256, context.sampleRate);
+ for (var i = 1; i <= 2; i++) {
+ var data = expectedBuffer.getChannelData(i-1);
+ for (var j = 0; j < data.length; j++) {
+ data[j] = (i == 1) ? 1.5 : 3.0;
+ }
+ }
+
+ return expectedBuffer;
+ }
+ };
+
+ runTest();
+ </script>
+ </pre>
+ </body>
+</html>
diff --git a/dom/media/webaudio/test/test_disconnectExceptions.html b/dom/media/webaudio/test/test_disconnectExceptions.html
new file mode 100644
index 0000000000..54fde4df8d
--- /dev/null
+++ b/dom/media/webaudio/test/test_disconnectExceptions.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Test whether we can disconnect an AudioNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ </head>
+ <body>
+ <pre id="test">
+ <script class="testbody" type="text/javascript">
+ var ctx = new AudioContext();
+ var sourceBuffer = ctx.createBuffer(2, 256, ctx.sampleRate);
+ for (var i = 1; i <= 2; i++) {
+ var data = sourceBuffer.getChannelData(i-1);
+ for (var j = 0; j < data.length; j++) {
+ data[j] = i;
+ }
+ }
+
+ var source = ctx.createBufferSource();
+ source.buffer = sourceBuffer;
+
+ var gain1 = ctx.createGain();
+ var splitter = ctx.createChannelSplitter(2);
+ var merger = ctx.createChannelMerger(2);
+ var gain2 = ctx.createGain();
+ var gain3 = ctx.createGain();
+
+ gain1.connect(splitter);
+ splitter.connect(gain2, 0);
+ splitter.connect(gain3, 1);
+ splitter.connect(merger, 0, 0);
+ splitter.connect(merger, 1, 1);
+ gain2.connect(gain3);
+ gain3.connect(ctx.destination);
+ merger.connect(ctx.destination);
+
+ expectException(function() {
+ splitter.disconnect(2);
+ }, DOMException.INDEX_SIZE_ERR);
+
+ expectNoException(function() {
+ splitter.disconnect(1);
+ splitter.disconnect(1);
+ });
+
+ expectException(function() {
+ gain1.disconnect(gain2);
+ }, DOMException.INVALID_ACCESS_ERR);
+
+ expectException(function() {
+ gain1.disconnect(gain3);
+ ok(false, 'Should get InvalidAccessError exception');
+ }, DOMException.INVALID_ACCESS_ERR);
+
+ expectException(function() {
+ splitter.disconnect(gain2, 2);
+ }, DOMException.INDEX_SIZE_ERR);
+
+ expectException(function() {
+ splitter.disconnect(gain1, 0);
+ }, DOMException.INVALID_ACCESS_ERR);
+
+ expectException(function() {
+ splitter.disconnect(gain3, 0, 0);
+ }, DOMException.INVALID_ACCESS_ERR);
+
+ expectException(function() {
+ splitter.disconnect(merger, 3, 0);
+ }, DOMException.INDEX_SIZE_ERR);
+ </script>
+ </pre>
+ </body>
+</html>
diff --git a/dom/media/webaudio/test/test_disconnectFromAudioNode.html b/dom/media/webaudio/test/test_disconnectFromAudioNode.html
new file mode 100644
index 0000000000..e6ec9d941c
--- /dev/null
+++ b/dom/media/webaudio/test/test_disconnectFromAudioNode.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Test whether we can disconnect an AudioNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ </head>
+ <body>
+ <pre id="test">
+ <script class="testbody" type="text/javascript">
+ var gTest = {
+ length: 256,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var sourceBuffer = context.createBuffer(1, 256, context.sampleRate);
+ var data = sourceBuffer.getChannelData(0);
+ for (var j = 0; j < data.length; j++) {
+ data[j] = 1;
+ }
+
+ var source = context.createBufferSource();
+ source.buffer = sourceBuffer;
+
+ var gain1 = context.createGain();
+ var gain2 = context.createGain();
+ var gain3 = context.createGain();
+
+ source.connect(gain1);
+ source.connect(gain2);
+
+ gain1.connect(gain3);
+ gain2.connect(gain3);
+
+ source.start();
+
+ source.disconnect(gain2);
+
+ return gain3;
+ },
+ createExpectedBuffers(context) {
+ expectedBuffer = context.createBuffer(1, 256, context.sampleRate);
+ for (var i = 0; i < 256; ++i) {
+ expectedBuffer.getChannelData(0)[i] = 1.0;
+ }
+
+ return expectedBuffer;
+ }
+ };
+
+ runTest();
+ </script>
+ </pre>
+ </body>
+</html>
diff --git a/dom/media/webaudio/test/test_disconnectFromAudioNodeAndOutput.html b/dom/media/webaudio/test/test_disconnectFromAudioNodeAndOutput.html
new file mode 100644
index 0000000000..566a84edbd
--- /dev/null
+++ b/dom/media/webaudio/test/test_disconnectFromAudioNodeAndOutput.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Test whether we can disconnect an AudioNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ </head>
+ <body>
+ <pre id="test">
+ <script class="testbody" type="text/javascript">
+ var gTest = {
+ length: 256,
+ numberOfChannels: 2,
+ createGraph(context) {
+ var sourceBuffer = context.createBuffer(2, 256, context.sampleRate);
+ for (var i = 1; i <= 2; i++) {
+ var data = sourceBuffer.getChannelData(i-1);
+ for (var j = 0; j < data.length; j++) {
+ data[j] = i;
+ }
+ }
+
+ var source = context.createBufferSource();
+ source.buffer = sourceBuffer;
+
+ var splitter = context.createChannelSplitter(2);
+ var gain1 = context.createGain();
+ var gain2 = context.createGain();
+ var merger = context.createChannelMerger(2);
+
+ source.connect(splitter);
+ splitter.connect(gain1, 0);
+ splitter.connect(gain2, 0);
+ splitter.connect(gain2, 1);
+ gain1.connect(merger, 0, 1);
+ gain2.connect(merger, 0, 1);
+ source.start();
+
+ splitter.disconnect(gain2, 0);
+
+ return merger;
+ },
+ createExpectedBuffers(context) {
+ expectedBuffer = context.createBuffer(2, 256, context.sampleRate);
+ for (var i = 0; i < 256; ++i) {
+ expectedBuffer.getChannelData(0)[i] = 0;
+ expectedBuffer.getChannelData(1)[i] = 3;
+ }
+
+ return expectedBuffer;
+ }
+ };
+
+ runTest();
+ </script>
+ </pre>
+ </body>
+</html>
diff --git a/dom/media/webaudio/test/test_disconnectFromAudioNodeAndOutputAndInput.html b/dom/media/webaudio/test/test_disconnectFromAudioNodeAndOutputAndInput.html
new file mode 100644
index 0000000000..478768c62d
--- /dev/null
+++ b/dom/media/webaudio/test/test_disconnectFromAudioNodeAndOutputAndInput.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Test whether we can disconnect an AudioNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ </head>
+ <body>
+ <pre id="test">
+ <script class="testbody" type="text/javascript">
+ var gTest = {
+ length: 256,
+ numberOfChannels: 3,
+ createGraph(context) {
+ var sourceBuffer = context.createBuffer(3, 256, context.sampleRate);
+ for (var i = 1; i <= 3; i++) {
+ var data = sourceBuffer.getChannelData(i-1);
+ for (var j = 0; j < data.length; j++) {
+ data[j] = i;
+ }
+ }
+
+ var source = context.createBufferSource();
+ source.buffer = sourceBuffer;
+
+ var splitter = context.createChannelSplitter(3);
+ var merger = context.createChannelMerger(3);
+
+ source.connect(splitter);
+ splitter.connect(merger, 0, 0);
+ splitter.connect(merger, 1, 1);
+ splitter.connect(merger, 2, 2);
+ source.start();
+
+ splitter.disconnect(merger, 2, 2);
+
+ return merger;
+ },
+ createExpectedBuffers(context) {
+ var expectedBuffer = context.createBuffer(3, 256, context.sampleRate);
+ for (var i = 1; i <= 3; i++) {
+ var data = expectedBuffer.getChannelData(i-1);
+ for (var j = 0; j < data.length; j++) {
+ data[j] = (i == 3) ? 0 : i;
+ }
+ }
+
+ return expectedBuffer;
+ }
+ };
+
+ runTest();
+ </script>
+ </pre>
+ </body>
+</html> \ No newline at end of file
diff --git a/dom/media/webaudio/test/test_disconnectFromAudioNodeMultipleConnection.html b/dom/media/webaudio/test/test_disconnectFromAudioNodeMultipleConnection.html
new file mode 100644
index 0000000000..dff1562d7a
--- /dev/null
+++ b/dom/media/webaudio/test/test_disconnectFromAudioNodeMultipleConnection.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>
+ Test whether we can disconnect all outbound connection of an AudioNode
+ </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ </head>
+ <body>
+ <pre id="test">
+ <script class="testbody" type="text/javascript">
+ var gTest = {
+ length: 256,
+ numberOfChannels: 2,
+ createGraph(context) {
+ var sourceBuffer = context.createBuffer(1, 256, context.sampleRate);
+ var data = sourceBuffer.getChannelData(0);
+ for (var j = 0; j < data.length; j++) {
+ data[j] = 1;
+ }
+
+ var source = context.createBufferSource();
+ source.buffer = sourceBuffer;
+
+ var merger = context.createChannelMerger(2);
+ var gain = context.createGain();
+
+ source.connect(merger, 0, 0);
+ source.connect(gain);
+ source.connect(merger, 0, 1);
+
+ source.disconnect(merger);
+
+ source.start();
+
+ return merger;
+ },
+ createExpectedBuffers(context) {
+ expectedBuffer = context.createBuffer(2, 256, context.sampleRate);
+ for (var channel = 0; channel < 2; channel++) {
+ for (var i = 0; i < 256; ++i) {
+ expectedBuffer.getChannelData(0)[i] = 0;
+ }
+ }
+
+ return expectedBuffer;
+ }
+ };
+
+ runTest();
+ </script>
+ </pre>
+ </body>
+</html>
diff --git a/dom/media/webaudio/test/test_disconnectFromOutput.html b/dom/media/webaudio/test/test_disconnectFromOutput.html
new file mode 100644
index 0000000000..9a7fe354a9
--- /dev/null
+++ b/dom/media/webaudio/test/test_disconnectFromOutput.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+ <head>
+ <title>Test whether we can disconnect an AudioNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+ </head>
+ <body>
+ <pre id="test">
+ <script class="testbody" type="text/javascript">
+ var gTest = {
+ length: 256,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var sourceBuffer = context.createBuffer(3, 256, context.sampleRate);
+ for (var i = 1; i <= 3; i++) {
+ var data = sourceBuffer.getChannelData(i-1);
+ for (var j = 0; j < data.length; j++) {
+ data[j] = i;
+ }
+ }
+
+ var source = context.createBufferSource();
+ source.buffer = sourceBuffer;
+
+ var splitter = context.createChannelSplitter(3);
+ var sum = context.createGain();
+
+ source.connect(splitter);
+ splitter.connect(sum, 0);
+ splitter.connect(sum, 1);
+ splitter.connect(sum, 2);
+ source.start();
+
+ splitter.disconnect(1);
+
+ return sum;
+ },
+ createExpectedBuffers(context) {
+ expectedBuffer = context.createBuffer(1, 256, context.sampleRate);
+ for (var i = 0; i < 256; ++i) {
+ expectedBuffer.getChannelData(0)[i] = 4;
+ }
+
+ return expectedBuffer;
+ },
+ };
+
+ runTest();
+ </script>
+ </pre>
+ </body>
+</html> \ No newline at end of file
diff --git a/dom/media/webaudio/test/test_dynamicsCompressorNode.html b/dom/media/webaudio/test/test_dynamicsCompressorNode.html
new file mode 100644
index 0000000000..05b6887a53
--- /dev/null
+++ b/dom/media/webaudio/test/test_dynamicsCompressorNode.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test DynamicsCompressorNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+function near(a, b, msg) {
+ ok(Math.abs(a - b) < 1e-4, msg);
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var context = new AudioContext();
+
+ var osc = context.createOscillator();
+ var sp = context.createScriptProcessor();
+
+ var compressor = new DynamicsCompressorNode(context);
+
+ osc.connect(compressor);
+ osc.connect(sp);
+ compressor.connect(context.destination);
+
+ is(compressor.channelCount, 2, "compressor node has 2 input channels by default");
+ is(compressor.channelCountMode, "clamped-max", "Correct channelCountMode for the compressor node");
+ is(compressor.channelInterpretation, "speakers", "Correct channelCountInterpretation for the compressor node");
+
+ // Verify default values
+ ok(compressor.threshold instanceof AudioParam, "treshold is an AudioParam");
+ near(compressor.threshold.defaultValue, -24, "Correct default value for threshold");
+ ok(compressor.knee instanceof AudioParam, "knee is an AudioParam");
+ near(compressor.knee.defaultValue, 30, "Correct default value for knee");
+ ok(compressor.ratio instanceof AudioParam, "knee is an AudioParam");
+ near(compressor.ratio.defaultValue, 12, "Correct default value for ratio");
+ is(typeof compressor.reduction, "number", "reduction is a number");
+ near(compressor.reduction, 0, "Correct default value for reduction");
+ ok(compressor.attack instanceof AudioParam, "attack is an AudioParam");
+ near(compressor.attack.defaultValue, 0.003, "Correct default value for attack");
+ ok(compressor.release instanceof AudioParam, "release is an AudioParam");
+ near(compressor.release.defaultValue, 0.25, "Correct default value for release");
+
+ compressor.threshold.value = -80;
+
+ osc.start();
+ var iteration = 0;
+ sp.onaudioprocess = function(e) {
+ if (iteration > 10) {
+ ok(compressor.reduction < 0,
+ "Feeding a full-scale sine to a compressor should result in an db" +
+ "reduction.");
+ sp.onaudioprocess = null;
+ osc.stop(0);
+
+ SimpleTest.finish();
+ }
+ iteration++;
+ }
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_dynamicsCompressorNodePassThrough.html b/dom/media/webaudio/test/test_dynamicsCompressorNodePassThrough.html
new file mode 100644
index 0000000000..9e8d794547
--- /dev/null
+++ b/dom/media/webaudio/test/test_dynamicsCompressorNodePassThrough.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test DynamicsCompressorNode with passthrough</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 2048,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var source = context.createBufferSource();
+
+ var compressor = context.createDynamicsCompressor();
+
+ source.buffer = this.buffer;
+
+ source.connect(compressor);
+
+ var compressorWrapped = SpecialPowers.wrap(compressor);
+ ok("passThrough" in compressorWrapped, "DynamicsCompressorNode should support the passThrough API");
+ compressorWrapped.passThrough = true;
+
+ source.start(0);
+ return compressor;
+ },
+ createExpectedBuffers(context) {
+ this.buffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ this.buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ }
+
+ return [this.buffer];
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_dynamicsCompressorNodeWithGain.html b/dom/media/webaudio/test/test_dynamicsCompressorNodeWithGain.html
new file mode 100644
index 0000000000..7e6487e3f8
--- /dev/null
+++ b/dom/media/webaudio/test/test_dynamicsCompressorNodeWithGain.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+ <title>Test DynamicsCompressor with Gain</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+addLoadEvent(function() {
+ var samplerate = 44100;
+ var context = new OfflineAudioContext(1, samplerate/100, samplerate);
+
+ var osc = context.createOscillator();
+ osc.frequency.value = 2400;
+
+ var gain = context.createGain();
+ gain.gain.value = 1.5;
+
+ // These numbers are borrowed from the example code on MDN
+ // https://developer.mozilla.org/en-US/docs/Web/API/DynamicsCompressorNode
+ var compressor = context.createDynamicsCompressor();
+ compressor.threshold.value = -50;
+ compressor.knee.value = 40;
+ compressor.ratio.value = 12;
+ compressor.reduction.value = -20;
+ compressor.attack.value = 0;
+ compressor.release.value = 0.25;
+
+ osc.connect(gain);
+ gain.connect(compressor);
+ compressor.connect(context.destination);
+ osc.start();
+
+ context.startRendering().then(buffer => {
+ var peak = Math.max(...buffer.getChannelData(0));
+ console.log(peak);
+ // These values are experimentally determined. Without dynamics compression
+ // the peak should be just under 1.5. We also check for a minimum value
+ // to make sure we are not getting all zeros.
+ ok(peak >= 0.2 && peak < 1.0, "Peak value should be greater than 0.25 and less than 1.0");
+ SimpleTest.finish();
+ });
+});
+</script>
+<pre>
+</pre>
+</body>
diff --git a/dom/media/webaudio/test/test_event_listener_leaks.html b/dom/media/webaudio/test/test_event_listener_leaks.html
new file mode 100644
index 0000000000..a3bcc9259e
--- /dev/null
+++ b/dom/media/webaudio/test/test_event_listener_leaks.html
@@ -0,0 +1,47 @@
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+-->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1450358 - Test AudioContext event listener leak conditions</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/dom/events/test/event_leak_utils.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+// Manipulate AudioContext objects in the frame's context.
+// Its important here that we create a listener callback from
+// the DOM objects back to the frame's global in order to
+// exercise the leak condition.
+async function useAudioContext(contentWindow) {
+ let ctx = new contentWindow.AudioContext();
+ ctx.onstatechange = e => {
+ contentWindow.stateChangeCount += 1;
+ };
+
+ let osc = ctx.createOscillator();
+ osc.type = "sine";
+ osc.frequency.value = 440;
+ osc.start();
+}
+
+async function runTest() {
+ try {
+ await checkForEventListenerLeaks("AudioContext", useAudioContext);
+ } catch (e) {
+ ok(false, e);
+ } finally {
+ SimpleTest.finish();
+ }
+}
+
+SimpleTest.waitForExplicitFinish();
+addEventListener("load", runTest, { once: true });
+</script>
+</pre>
+</body>
+</html>
+
diff --git a/dom/media/webaudio/test/test_gainNode.html b/dom/media/webaudio/test/test_gainNode.html
new file mode 100644
index 0000000000..77b0ae88b0
--- /dev/null
+++ b/dom/media/webaudio/test/test_gainNode.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test GainNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 2048,
+ numberOfChannels: 1,
+ createGraph(context) {
+
+ var gain = new GainNode(context);
+ ok(gain.gain, "The audioparam member must exist");
+ is(gain.gain.value, 1.0, "Correct initial value");
+ is(gain.gain.defaultValue, 1.0, "Correct default value");
+ gain.gain.value = 0.5;
+ is(gain.gain.value, 0.5, "Correct initial value");
+ is(gain.gain.defaultValue, 1.0, "Correct default value");
+
+ gain = new GainNode(context, { gain: 0.5 });
+ ok(gain.gain, "The audioparam member must exist");
+ is(gain.gain.value, 0.5, "Correct initial value");
+ is(gain.gain.defaultValue, 1.0, "Correct default value");
+ gain.gain.value = 0.5;
+ is(gain.gain.value, 0.5, "Correct initial value");
+ is(gain.gain.defaultValue, 1.0, "Correct default value");
+
+ var buffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ }
+
+ var source = context.createBufferSource();
+ source.buffer = buffer;
+
+ gain = context.createGain();
+ source.connect(gain);
+
+ ok(gain.gain, "The audioparam member must exist");
+ is(gain.gain.value, 1.0, "Correct initial value");
+ is(gain.gain.defaultValue, 1.0, "Correct default value");
+ gain.gain.value = 0.5;
+ is(gain.gain.value, 0.5, "Correct initial value");
+ is(gain.gain.defaultValue, 1.0, "Correct default value");
+ is(gain.channelCount, 2, "gain node has 2 input channels by default");
+ is(gain.channelCountMode, "max", "Correct channelCountMode for the gain node");
+ is(gain.channelInterpretation, "speakers", "Correct channelCountInterpretation for the gain node");
+
+ source.start(0);
+ return gain;
+ },
+ createExpectedBuffers(context) {
+ var expectedBuffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ expectedBuffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate) / 2;
+ }
+ return expectedBuffer;
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_gainNodeInLoop.html b/dom/media/webaudio/test/test_gainNodeInLoop.html
new file mode 100644
index 0000000000..90dedc0ef4
--- /dev/null
+++ b/dom/media/webaudio/test/test_gainNodeInLoop.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test GainNode in presence of loops</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 4096,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var sourceBuffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ sourceBuffer.getChannelData(0)[i] = 1;
+ }
+
+ var source = context.createBufferSource();
+ source.buffer = sourceBuffer;
+ source.loop = true;
+ source.start(0);
+ source.stop(sourceBuffer.duration * 2);
+
+ var gain = context.createGain();
+ // Adjust the gain in a way that we don't just end up modifying AudioChunk::mVolume
+ gain.gain.setValueAtTime(0.5, 0);
+ source.connect(gain);
+ return gain;
+ },
+ createExpectedBuffers(context) {
+ var expectedBuffer = context.createBuffer(1, 4096, context.sampleRate);
+ for (var i = 0; i < 4096; ++i) {
+ expectedBuffer.getChannelData(0)[i] = 0.5;
+ }
+ return expectedBuffer;
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_gainNodePassThrough.html b/dom/media/webaudio/test/test_gainNodePassThrough.html
new file mode 100644
index 0000000000..5a973ccaa2
--- /dev/null
+++ b/dom/media/webaudio/test/test_gainNodePassThrough.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test GainNode with passthrough</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 2048,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var source = context.createBufferSource();
+
+ var gain = context.createGain();
+
+ source.buffer = this.buffer;
+
+ source.connect(gain);
+
+ gain.gain.value = 0.5;
+
+ var gainWrapped = SpecialPowers.wrap(gain);
+ ok("passThrough" in gainWrapped, "GainNode should support the passThrough API");
+ gainWrapped.passThrough = true;
+
+ source.start(0);
+ return gain;
+ },
+ createExpectedBuffers(context) {
+ this.buffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ this.buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ }
+
+ return [this.buffer];
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_iirFilterNodePassThrough.html b/dom/media/webaudio/test/test_iirFilterNodePassThrough.html
new file mode 100644
index 0000000000..ab54499e04
--- /dev/null
+++ b/dom/media/webaudio/test/test_iirFilterNodePassThrough.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test IIRFilterNode with passthrough</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 2048,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var source = context.createBufferSource();
+
+ var filter = context.createIIRFilter([0.5, 0.5], [1.0]);
+
+ source.buffer = this.buffer;
+
+ source.connect(filter);
+
+ var filterWrapped = SpecialPowers.wrap(filter);
+ ok("passThrough" in filterWrapped, "BiquadFilterNode should support the passThrough API");
+ filterWrapped.passThrough = true;
+
+ source.start(0);
+ return filter;
+ },
+ createExpectedBuffers(context) {
+ this.buffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ this.buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ }
+
+ return [this.buffer];
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_maxChannelCount.html b/dom/media/webaudio/test/test_maxChannelCount.html
new file mode 100644
index 0000000000..1a4e5c856e
--- /dev/null
+++ b/dom/media/webaudio/test/test_maxChannelCount.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test the AudioContext.destination interface</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// Work around bug 911777
+SpecialPowers.forceGC();
+SpecialPowers.forceCC();
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var ac = new AudioContext();
+ ok(ac.destination.maxChannelCount > 0, "We can query the maximum number of channels");
+
+ var oac = new OfflineAudioContext(2, 1024, 48000);
+ is(oac.destination.maxChannelCount, 2, "This OfflineAudioContext should have 2 max channels.");
+
+ oac = new OfflineAudioContext(6, 1024, 48000);
+ is(oac.destination.maxChannelCount, 6, "This OfflineAudioContext should have 6 max channels.");
+
+ expectException(function() {
+ oac.destination.channelCount = oac.destination.channelCount + 1;
+ }, DOMException.INDEX_SIZE_ERR);
+
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_mediaDecoding.html b/dom/media/webaudio/test/test_mediaDecoding.html
new file mode 100644
index 0000000000..d796ef6add
--- /dev/null
+++ b/dom/media/webaudio/test/test_mediaDecoding.html
@@ -0,0 +1,388 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test the decodeAudioData API and Resampling</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script src="webaudio.js" type="text/javascript"></script>
+<script type="text/javascript">
+
+// These routines have been copied verbatim from WebKit, and are used in order
+// to convert a memory buffer into a wave buffer.
+function writeString(s, a, offset) {
+ for (var i = 0; i < s.length; ++i) {
+ a[offset + i] = s.charCodeAt(i);
+ }
+}
+
+function writeInt16(n, a, offset) {
+ n = Math.floor(n);
+
+ var b1 = n & 255;
+ var b2 = (n >> 8) & 255;
+
+ a[offset + 0] = b1;
+ a[offset + 1] = b2;
+}
+
+function writeInt32(n, a, offset) {
+ n = Math.floor(n);
+ var b1 = n & 255;
+ var b2 = (n >> 8) & 255;
+ var b3 = (n >> 16) & 255;
+ var b4 = (n >> 24) & 255;
+
+ a[offset + 0] = b1;
+ a[offset + 1] = b2;
+ a[offset + 2] = b3;
+ a[offset + 3] = b4;
+}
+
+function writeAudioBuffer(audioBuffer, a, offset) {
+ var n = audioBuffer.length;
+ var channels = audioBuffer.numberOfChannels;
+
+ for (var i = 0; i < n; ++i) {
+ for (var k = 0; k < channels; ++k) {
+ var buffer = audioBuffer.getChannelData(k);
+ var sample = buffer[i] * 32768.0;
+
+ // Clip samples to the limitations of 16-bit.
+ // If we don't do this then we'll get nasty wrap-around distortion.
+ if (sample < -32768)
+ sample = -32768;
+ if (sample > 32767)
+ sample = 32767;
+
+ writeInt16(sample, a, offset);
+ offset += 2;
+ }
+ }
+}
+
+function createWaveFileData(audioBuffer) {
+ var frameLength = audioBuffer.length;
+ var numberOfChannels = audioBuffer.numberOfChannels;
+ var sampleRate = audioBuffer.sampleRate;
+ var bitsPerSample = 16;
+ var byteRate = sampleRate * numberOfChannels * bitsPerSample/8;
+ var blockAlign = numberOfChannels * bitsPerSample/8;
+ var wavDataByteLength = frameLength * numberOfChannels * 2; // 16-bit audio
+ var headerByteLength = 44;
+ var totalLength = headerByteLength + wavDataByteLength;
+
+ var waveFileData = new Uint8Array(totalLength);
+
+ var subChunk1Size = 16; // for linear PCM
+ var subChunk2Size = wavDataByteLength;
+ var chunkSize = 4 + (8 + subChunk1Size) + (8 + subChunk2Size);
+
+ writeString("RIFF", waveFileData, 0);
+ writeInt32(chunkSize, waveFileData, 4);
+ writeString("WAVE", waveFileData, 8);
+ writeString("fmt ", waveFileData, 12);
+
+ writeInt32(subChunk1Size, waveFileData, 16); // SubChunk1Size (4)
+ writeInt16(1, waveFileData, 20); // AudioFormat (2)
+ writeInt16(numberOfChannels, waveFileData, 22); // NumChannels (2)
+ writeInt32(sampleRate, waveFileData, 24); // SampleRate (4)
+ writeInt32(byteRate, waveFileData, 28); // ByteRate (4)
+ writeInt16(blockAlign, waveFileData, 32); // BlockAlign (2)
+ writeInt32(bitsPerSample, waveFileData, 34); // BitsPerSample (4)
+
+ writeString("data", waveFileData, 36);
+ writeInt32(subChunk2Size, waveFileData, 40); // SubChunk2Size (4)
+
+ // Write actual audio data starting at offset 44.
+ writeAudioBuffer(audioBuffer, waveFileData, 44);
+
+ return waveFileData;
+}
+
+</script>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// fuzzTolerance and fuzzToleranceMobile are used to determine fuzziness
+// thresholds. They're needed to make sure that we can deal with neglibible
+// differences in the binary buffer caused as a result of resampling the
+// audio. fuzzToleranceMobile is typically larger on mobile platforms since
+// we do fixed-point resampling as opposed to floating-point resampling on
+// those platforms.
+var files = [
+ // An ogg file, 44.1khz, mono
+ {
+ url: "ting-44.1k-1ch.ogg",
+ valid: true,
+ expectedUrl: "ting-44.1k-1ch.wav",
+ numberOfChannels: 1,
+ frames: 30592,
+ sampleRate: 44100,
+ duration: 0.693,
+ fuzzTolerance: 5,
+ fuzzToleranceMobile: 1284
+ },
+ // An ogg file, 44.1khz, stereo
+ {
+ url: "ting-44.1k-2ch.ogg",
+ valid: true,
+ expectedUrl: "ting-44.1k-2ch.wav",
+ numberOfChannels: 2,
+ frames: 30592,
+ sampleRate: 44100,
+ duration: 0.693,
+ fuzzTolerance: 6,
+ fuzzToleranceMobile: 2544
+ },
+ // An ogg file, 48khz, mono
+ {
+ url: "ting-48k-1ch.ogg",
+ valid: true,
+ expectedUrl: "ting-48k-1ch.wav",
+ numberOfChannels: 1,
+ frames: 33297,
+ sampleRate: 48000,
+ duration: 0.693,
+ fuzzTolerance: 5,
+ fuzzToleranceMobile: 1388
+ },
+ // An ogg file, 48khz, stereo
+ {
+ url: "ting-48k-2ch.ogg",
+ valid: true,
+ expectedUrl: "ting-48k-2ch.wav",
+ numberOfChannels: 2,
+ frames: 33297,
+ sampleRate: 48000,
+ duration: 0.693,
+ fuzzTolerance: 14,
+ fuzzToleranceMobile: 2752
+ },
+ // Make sure decoding a wave file results in the same buffer (for both the
+ // resampling and non-resampling cases)
+ {
+ url: "ting-44.1k-1ch.wav",
+ valid: true,
+ expectedUrl: "ting-44.1k-1ch.wav",
+ numberOfChannels: 1,
+ frames: 30592,
+ sampleRate: 44100,
+ duration: 0.693,
+ fuzzTolerance: 0,
+ fuzzToleranceMobile: 0
+ },
+ {
+ url: "ting-48k-1ch.wav",
+ valid: true,
+ expectedUrl: "ting-48k-1ch.wav",
+ numberOfChannels: 1,
+ frames: 33297,
+ sampleRate: 48000,
+ duration: 0.693,
+ fuzzTolerance: 0,
+ fuzzToleranceMobile: 0
+ },
+ // // A wave file
+ // //{ url: "24bit-44khz.wav", valid: true, expectedUrl: "24bit-44khz-expected.wav" },
+ // A non-audio file
+ { url: "invalid.txt", valid: false, sampleRate: 44100 },
+ // A webm file with no audio
+ { url: "noaudio.webm", valid: false, sampleRate: 48000 },
+ // A video ogg file with audio
+ {
+ url: "audio.ogv",
+ valid: true,
+ expectedUrl: "audio-expected.wav",
+ numberOfChannels: 2,
+ sampleRate: 44100,
+ frames: 47680,
+ duration: 1.0807,
+ fuzzTolerance: 106,
+ fuzzToleranceMobile: 3482
+ },
+ {
+ url: "nil-packet.ogg",
+ expectedUrl: null,
+ valid: true,
+ numberOfChannels: 2,
+ sampleRate: 48000,
+ frames: 18600,
+ duration: 0.3874,
+ }
+];
+
+// Returns true if the memory buffers are less different that |fuzz| bytes
+function fuzzyMemcmp(buf1, buf2, fuzz) {
+ var result = true;
+ var difference = 0;
+ is(buf1.length, buf2.length, "same length");
+ for (var i = 0; i < buf1.length; ++i) {
+ if (Math.abs(buf1[i] - buf2[i])) {
+ ++difference;
+ }
+ }
+ if (difference > fuzz) {
+ ok(false, "Expected at most " + fuzz + " bytes difference, found " + difference + " bytes");
+ }
+ return difference <= fuzz;
+}
+
+function getFuzzTolerance(test) {
+ var kIsMobile =
+ navigator.userAgent.includes("Mobile") || // b2g
+ navigator.userAgent.includes("Android"); // android
+ return kIsMobile ? test.fuzzToleranceMobile : test.fuzzTolerance;
+}
+
+function bufferIsSilent(buffer) {
+ for (var i = 0; i < buffer.length; ++i) {
+ if (buffer.getChannelData(0)[i] != 0) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function checkAudioBuffer(buffer, test) {
+ if (buffer.numberOfChannels != test.numberOfChannels) {
+ is(buffer.numberOfChannels, test.numberOfChannels, "Correct number of channels");
+ return;
+ }
+ ok(Math.abs(buffer.duration - test.duration) < 1e-3, "Correct duration");
+ if (Math.abs(buffer.duration - test.duration) >= 1e-3) {
+ ok(false, "got: " + buffer.duration + ", expected: " + test.duration);
+ }
+ is(buffer.sampleRate, test.sampleRate, "Correct sample rate");
+ is(buffer.length, test.frames, "Correct length");
+
+ var wave = createWaveFileData(buffer);
+ if (test.expectedWaveData) {
+ ok(fuzzyMemcmp(wave, test.expectedWaveData, getFuzzTolerance(test)), "Received expected decoded data");
+ }
+}
+
+function checkResampledBuffer(buffer, test, callback) {
+ if (buffer.numberOfChannels != test.numberOfChannels) {
+ is(buffer.numberOfChannels, test.numberOfChannels, "Correct number of channels");
+ return;
+ }
+ ok(Math.abs(buffer.duration - test.duration) < 1e-3, "Correct duration");
+ if (Math.abs(buffer.duration - test.duration) >= 1e-3) {
+ ok(false, "got: " + buffer.duration + ", expected: " + test.duration);
+ }
+ // Take into account the resampling when checking the size
+ var expectedLength = test.frames * buffer.sampleRate / test.sampleRate;
+ SimpleTest.ok(
+ Math.abs(buffer.length - expectedLength) < 1.0,
+ "Correct length - got " + buffer.length +
+ ", expected about " + expectedLength
+ );
+
+ // Playback the buffer in the original context, to resample back to the
+ // original rate and compare with the decoded buffer without resampling.
+ cx = test.nativeContext;
+ var expected = cx.createBufferSource();
+ expected.buffer = test.expectedBuffer;
+ expected.start();
+ var inverse = cx.createGain();
+ inverse.gain.value = -1;
+ expected.connect(inverse);
+ inverse.connect(cx.destination);
+ var resampled = cx.createBufferSource();
+ resampled.buffer = buffer;
+ resampled.start();
+ // This stop should do nothing, but it tests for bug 937475
+ resampled.stop(test.frames / cx.sampleRate);
+ resampled.connect(cx.destination);
+ cx.oncomplete = function(e) {
+ ok(!bufferIsSilent(e.renderedBuffer), "Expect buffer not silent");
+ // Resampling will lose the highest frequency components, so we should
+ // pass the difference through a low pass filter. However, either the
+ // input files don't have significant high frequency components or the
+ // tolerance in compareBuffers() is too high to detect them.
+ compareBuffers(e.renderedBuffer,
+ cx.createBuffer(test.numberOfChannels,
+ test.frames, test.sampleRate));
+ callback();
+ }
+ cx.startRendering();
+}
+
+function runResampling(test, response, callback) {
+ var sampleRate = test.sampleRate == 44100 ? 48000 : 44100;
+ var cx = new OfflineAudioContext(1, 1, sampleRate);
+ cx.decodeAudioData(response, function onSuccess(asyncResult) {
+ is(asyncResult.sampleRate, sampleRate, "Correct sample rate");
+
+ checkResampledBuffer(asyncResult, test, callback);
+ }, function onFailure() {
+ ok(false, "Expected successful decode with resample");
+ callback();
+ });
+}
+
+function runTest(test, response, callback) {
+ // We need to copy the array here, because decodeAudioData will detach the
+ // array's buffer.
+ var compressedAudio = response.slice(0);
+ var expectCallback = false;
+ var cx = new OfflineAudioContext(test.numberOfChannels || 1,
+ test.frames || 1, test.sampleRate);
+ cx.decodeAudioData(response, function onSuccess(asyncResult) {
+ ok(expectCallback, "Success callback should fire asynchronously");
+ ok(test.valid, "Did expect success for test " + test.url);
+
+ checkAudioBuffer(asyncResult, test);
+
+ test.expectedBuffer = asyncResult;
+ test.nativeContext = cx;
+ runResampling(test, compressedAudio, callback);
+ }, function onFailure(e) {
+ ok(e instanceof DOMException, "We want to see an exception here");
+ is(e.name, "EncodingError", "Exception name matches");
+ ok(expectCallback, "Failure callback should fire asynchronously");
+ ok(!test.valid, "Did expect failure for test " + test.url);
+ callback();
+ });
+ expectCallback = true;
+}
+
+function loadTest(test, callback) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", test.url, true);
+ xhr.responseType = "arraybuffer";
+ xhr.onload = function() {
+ if (!test.expectedUrl) {
+ runTest(test, xhr.response, callback);
+ return;
+ }
+ var getExpected = new XMLHttpRequest();
+ getExpected.open("GET", test.expectedUrl, true);
+ getExpected.responseType = "arraybuffer";
+ getExpected.onload = function() {
+ test.expectedWaveData = new Uint8Array(getExpected.response);
+ runTest(test, xhr.response, callback);
+ };
+ getExpected.send();
+ };
+ xhr.send();
+}
+
+function loadNextTest() {
+ if (files.length) {
+ loadTest(files.shift(), loadNextTest);
+ } else {
+ SimpleTest.finish();
+ }
+}
+
+loadNextTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_mediaElementAudioSourceNode.html b/dom/media/webaudio/test/test_mediaElementAudioSourceNode.html
new file mode 100644
index 0000000000..36e1e9f2cc
--- /dev/null
+++ b/dom/media/webaudio/test/test_mediaElementAudioSourceNode.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<meta charset="utf-8">
+<head>
+ <title>Test MediaElementAudioSourceNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+function test() {
+ var audio = new Audio("small-shot.ogg");
+ var context = new AudioContext();
+ var expectedMinNonzeroSampleCount;
+ var expectedMaxNonzeroSampleCount;
+ var nonzeroSampleCount = 0;
+ var complete = false;
+ var iterationCount = 0;
+
+ // This test ensures we receive at least expectedSampleCount nonzero samples
+ function processSamples(e) {
+ if (complete) {
+ return;
+ }
+
+ if (iterationCount == 0) {
+ // Don't start playing the audio until the AudioContext stuff is connected
+ // and running.
+ audio.play();
+ }
+ ++iterationCount;
+
+ var buf = e.inputBuffer.getChannelData(0);
+ var nonzeroSamplesThisBuffer = 0;
+ for (var i = 0; i < buf.length; ++i) {
+ if (buf[i] != 0) {
+ ++nonzeroSamplesThisBuffer;
+ }
+ }
+ nonzeroSampleCount += nonzeroSamplesThisBuffer;
+ is(e.inputBuffer.numberOfChannels, 1,
+ "Checking data channel count (nonzeroSamplesThisBuffer=" +
+ nonzeroSamplesThisBuffer + ")");
+ ok(nonzeroSampleCount <= expectedMaxNonzeroSampleCount,
+ "Too many nonzero samples (got " + nonzeroSampleCount + ", expected max " + expectedMaxNonzeroSampleCount + ")");
+ if (nonzeroSampleCount >= expectedMinNonzeroSampleCount &&
+ nonzeroSamplesThisBuffer == 0) {
+ ok(true,
+ "Check received enough nonzero samples (got " + nonzeroSampleCount + ", expected min " + expectedMinNonzeroSampleCount + ")");
+ SimpleTest.finish();
+ complete = true;
+ }
+ }
+
+ audio.onloadedmetadata = function() {
+ var node = new MediaElementAudioSourceNode(context, { mediaElement: audio });
+ var sp = context.createScriptProcessor(2048, 1);
+ node.connect(sp);
+ // Use a fuzz factor of 100 to account for samples that just happen to be zero
+ expectedMinNonzeroSampleCount = Math.floor(audio.duration*context.sampleRate) - 100;
+ expectedMaxNonzeroSampleCount = Math.floor(audio.duration*context.sampleRate) + 500;
+ sp.onaudioprocess = processSamples;
+ };
+}
+
+SpecialPowers.pushPrefEnv({"set": [["media.preload.default", 2], ["media.preload.auto", 3]]}, test);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_mediaElementAudioSourceNodeCrossOrigin.html b/dom/media/webaudio/test/test_mediaElementAudioSourceNodeCrossOrigin.html
new file mode 100644
index 0000000000..778658b332
--- /dev/null
+++ b/dom/media/webaudio/test/test_mediaElementAudioSourceNodeCrossOrigin.html
@@ -0,0 +1,94 @@
+<!DOCTYPE HTML>
+<html>
+<meta charset="utf-8">
+<head>
+ <title>Test MediaStreamAudioSourceNode doesn't get data from cross-origin media resources</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+// Turn off the authentication dialog blocking for this test.
+SpecialPowers.setIntPref("network.auth.subresource-http-auth-allow", 2)
+
+var tests = [
+ // Not the same origin no CORS asked for, should have silence
+ { url: "http://example.org:80/tests/dom/media/webaudio/test/small-shot.ogg",
+ cors: null,
+ expectSilence: true },
+ // Same origin, should have sound
+ { url: "small-shot.ogg",
+ cors: null,
+ expectSilence: false },
+ // Cross-origin but we asked for CORS and the server answered with the right
+ // header, should have
+ { url: "http://example.org:80/tests/dom/media/webaudio/test/corsServer.sjs",
+ cors: "anonymous",
+ expectSilence: false }
+];
+
+var testsRemaining = tests.length;
+
+tests.forEach(function(e) {
+ e.ac = new AudioContext();
+ var a = new Audio();
+ if (e.cors) {
+ a.crossOrigin = e.cors;
+ }
+ a.src = e.url;
+ document.body.appendChild(a);
+
+ a.onloadedmetadata = () => {
+ // Wait for "loadedmetadata" before capturing since tracks are then known
+ // directly. If we set up the capture before "loadedmetadata" we
+ // (internally) have to wait an extra async jump for tracks to become known
+ // to main thread, before setting up audio data forwarding to the node.
+ // As that happens, the audio resource may have already ended on slow test
+ // machines, causing failures.
+ a.onloadedmetadata = null;
+ var measn = e.ac.createMediaElementSource(a);
+ var sp = e.ac.createScriptProcessor(2048, 1);
+ sp.seenSound = false;
+ sp.onaudioprocess = checkBufferSilent;
+
+ measn.connect(sp);
+ a.play();
+ };
+
+ function checkFinished(sp) {
+ if (a.ended) {
+ sp.onaudioprocess = null;
+ var not = e.expectSilence ? "" : "not";
+ is(e.expectSilence, !sp.seenSound,
+ "Buffer is " + not + " silent as expected, for " +
+ e.url + " (cors: " + e.cors + ")");
+ if (--testsRemaining == 0) {
+ SimpleTest.finish();
+ }
+ }
+ }
+
+ function checkBufferSilent(e) {
+ var inputArrayBuffer = e.inputBuffer.getChannelData(0);
+ var silent = true;
+ for (var i = 0; i < inputArrayBuffer.length; i++) {
+ if (inputArrayBuffer[i] != 0.0) {
+ silent = false;
+ break;
+ }
+ }
+ // It is acceptable to find a full buffer of silence here, even if we expect
+ // sound, because Gecko's looping on media elements is not seamless and we
+ // can underrun. We are looking for at least one buffer of non-silent data.
+ e.target.seenSound = !silent || e.target.seenSound;
+ checkFinished(e.target);
+ return silent;
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_mediaElementAudioSourceNodeFidelity.html b/dom/media/webaudio/test/test_mediaElementAudioSourceNodeFidelity.html
new file mode 100644
index 0000000000..42d6d6a045
--- /dev/null
+++ b/dom/media/webaudio/test/test_mediaElementAudioSourceNodeFidelity.html
@@ -0,0 +1,137 @@
+<!DOCTYPE HTML>
+<html>
+<meta charset="utf-8">
+<head>
+ <title>Test MediaStreamAudioSourceNode doesn't get data from cross-origin media resources</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+function binIndexForFrequency(frequency, analyser) {
+ return 1 + Math.round(frequency *
+ analyser.fftSize /
+ analyser.context.sampleRate);
+}
+
+function debugCanvas(analyser) {
+ var cvs = document.createElement("canvas");
+ document.body.appendChild(cvs);
+
+ // Easy: 1px per bin
+ cvs.width = analyser.frequencyBinCount;
+ cvs.height = 256;
+ cvs.style.border = "1px solid red";
+
+ var c = cvs.getContext('2d');
+ var buf = new Uint8Array(analyser.frequencyBinCount);
+
+ function render() {
+ c.clearRect(0, 0, cvs.width, cvs.height);
+ analyser.getByteFrequencyData(buf);
+ for (var i = 0; i < buf.length; i++) {
+ c.fillRect(i, (256 - (buf[i])), 1, 256);
+ }
+ requestAnimationFrame(render);
+ }
+ requestAnimationFrame(render);
+}
+
+
+function checkFrequency(an) {
+ an.getFloatFrequencyData(frequencyArray);
+ // We should have no energy when checking the data largely outside the index
+ // for 440Hz (the frequency of the sine wave), start checking an octave above,
+ // the Opus compression can add some harmonics to the pure since wave.
+ var maxNoiseIndex = binIndexForFrequency(880, an);
+ for (var i = maxNoiseIndex + 1; i < frequencyArray.length; i++) {
+ if (frequencyArray[i] > frequencyArray[maxNoiseIndex]) {
+ maxNoiseIndex = i;
+ }
+ }
+
+ // On the other hand, we should find a peak at 440Hz. Our sine wave is not
+ // attenuated, we're expecting the peak to reach 0dBFs.
+ var index = binIndexForFrequency(440, an);
+ info("energy at 440: " + frequencyArray[index] +
+ ", threshold " + (an.maxDecibels - 10) +
+ "; max noise at index " + maxNoiseIndex +
+ ": " + frequencyArray[maxNoiseIndex] );
+ if (frequencyArray[index] < (an.maxDecibels - 10)) {
+ return false;
+ }
+ // Let some slack, there might be some noise here because of int -> float
+ // conversion or the Opus encoding.
+ if (frequencyArray[maxNoiseIndex] > an.minDecibels + 40) {
+ return false;
+ }
+
+ return true;
+}
+
+var audioElement = new Audio();
+audioElement.src = 'sine-440-10s.opus'
+audioElement.loop = true;
+var ac = new AudioContext();
+var mediaElementSource = ac.createMediaElementSource(audioElement);
+var an = ac.createAnalyser();
+// Use no smoothing as this would just average with previous
+// getFloatFrequencyData() calls. Non-seamless looping would introduce noise,
+// and smoothing would spread this into calls after the loop point.
+an.smoothingTimeConstant = 0;
+frequencyArray = new Float32Array(an.frequencyBinCount);
+
+// Uncomment this to check what the analyser is doing.
+// debugCanvas(an);
+
+mediaElementSource.connect(an)
+
+audioElement.play();
+// We want to check the we have the expected audio for at least two loop of
+// the HTMLMediaElement, piped into an AudioContext. The file is ten seconds,
+// and we use the default FFT size.
+var lastCurrentTime = 0;
+var loopCount = 0;
+audioElement.onplaying = function() {
+ audioElement.ontimeupdate = function() {
+ // We don't run the analysis when close to loop point or at the
+ // beginning, since looping is not seamless, there could be an
+ // unpredictable amount of silence
+ var rv = checkFrequency(an);
+ info("currentTime: " + audioElement.currentTime);
+ if (audioElement.currentTime < 4 ||
+ audioElement.currentTime > 8){
+ return;
+ }
+ if (!rv) {
+ ok(false, "Found unexpected noise during analysis.");
+ audioElement.ontimeupdate = null;
+ audioElement.onplaying = null;
+ ac.close();
+ audioElement.src = '';
+ SimpleTest.finish()
+ return;
+ }
+ ok(true, "Found correct audio signal during analysis");
+ info(lastCurrentTime + " " + audioElement.currentTime);
+ if (lastCurrentTime > audioElement.currentTime) {
+ info("loopCount: " + loopCount);
+ if (loopCount > 1) {
+ audioElement.ontimeupdate = null;
+ audioElement.onplaying = null;
+ ac.close();
+ audioElement.src = '';
+ SimpleTest.finish();
+ }
+ lastCurrentTime = audioElement.currentTime;
+ loopCount++;
+ } else {
+ lastCurrentTime = audioElement.currentTime;
+ }
+ }
+}
+
+</script>
diff --git a/dom/media/webaudio/test/test_mediaElementAudioSourceNodePassThrough.html b/dom/media/webaudio/test/test_mediaElementAudioSourceNodePassThrough.html
new file mode 100644
index 0000000000..5ee12b16ad
--- /dev/null
+++ b/dom/media/webaudio/test/test_mediaElementAudioSourceNodePassThrough.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<meta charset="utf-8">
+<head>
+ <title>Test MediaElementAudioSourceNode with passthrough</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+function test() {
+ var audio = new Audio("small-shot.ogg");
+ var context = new AudioContext();
+ var node = context.createMediaElementSource(audio);
+ var sp = context.createScriptProcessor(2048, 1);
+ node.connect(sp);
+ var nonzeroSampleCount = 0;
+ var complete = false;
+ var iterationCount = 0;
+
+ var srcWrapped = SpecialPowers.wrap(node);
+ ok("passThrough" in srcWrapped, "MediaElementAudioSourceNode should support the passThrough API");
+ srcWrapped.passThrough = true;
+
+ // This test ensures we receive at least expectedSampleCount nonzero samples
+ function processSamples(e) {
+ if (complete) {
+ return;
+ }
+
+ if (iterationCount == 0) {
+ // Don't start playing the audio until the AudioContext stuff is connected
+ // and running.
+ audio.play();
+ }
+ ++iterationCount;
+
+ var buf = e.inputBuffer.getChannelData(0);
+ var nonzeroSamplesThisBuffer = 0;
+ for (var i = 0; i < buf.length; ++i) {
+ if (buf[i] != 0) {
+ ++nonzeroSamplesThisBuffer;
+ }
+ }
+ nonzeroSampleCount += nonzeroSamplesThisBuffer;
+ if (iterationCount == 10) {
+ is(nonzeroSampleCount, 0, "The input must be silence");
+ SimpleTest.finish();
+ complete = true;
+ }
+ }
+
+ audio.oncanplaythrough = function() {
+ sp.onaudioprocess = processSamples;
+ };
+}
+
+SpecialPowers.pushPrefEnv({"set": [["media.preload.default", 2], ["media.preload.auto", 3]]}, test);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_mediaElementAudioSourceNodeVideo.html b/dom/media/webaudio/test/test_mediaElementAudioSourceNodeVideo.html
new file mode 100644
index 0000000000..dcb85f12cb
--- /dev/null
+++ b/dom/media/webaudio/test/test_mediaElementAudioSourceNodeVideo.html
@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<html>
+<meta charset="utf-8">
+<head>
+ <title>Test MediaElementAudioSourceNode before "loadedmetadata"</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+var video = document.createElement("video");
+function test() {
+ video.src = "audiovideo.mp4";
+
+ var context = new AudioContext();
+ var complete = false;
+
+ video.onended = () => {
+ if (complete) {
+ return;
+ }
+
+ complete = true;
+ ok(false, "Video ended without any samples seen");
+ SimpleTest.finish();
+ };
+
+ video.ontimeupdate = () => {
+ info("Timeupdate: " + video.currentTime);
+ };
+
+ var node = context.createMediaElementSource(video);
+ var sp = context.createScriptProcessor(2048, 1);
+ node.connect(sp);
+
+ // This test ensures we receive some nonzero samples when we capture to
+ // WebAudio before "loadedmetadata".
+ sp.onaudioprocess = e => {
+ if (complete) {
+ return;
+ }
+
+ var buf = e.inputBuffer.getChannelData(0);
+ for (var i = 0; i < buf.length; ++i) {
+ if (buf[i] != 0) {
+ complete = true;
+ ok(true, "Got non-zero samples");
+ SimpleTest.finish();
+ return;
+ }
+ }
+ };
+
+ video.play();
+}
+
+if (video.canPlayType("video/mp4")) {
+ test();
+} else {
+ ok(true, "MP4 not supported. Skipping.");
+ SimpleTest.finish();
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_mediaStreamAudioDestinationNode.html b/dom/media/webaudio/test/test_mediaStreamAudioDestinationNode.html
new file mode 100644
index 0000000000..fd0ce8141b
--- /dev/null
+++ b/dom/media/webaudio/test/test_mediaStreamAudioDestinationNode.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test MediaStreamAudioDestinationNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<audio id="audioelem"></audio>
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("This test uses a live media element so it needs to wait for the media stack to do some work.");
+addLoadEvent(function() {
+ var context = new AudioContext();
+ var buffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ }
+
+ var source = context.createBufferSource();
+ source.buffer = buffer;
+
+ var dest = new MediaStreamAudioDestinationNode(context);
+ source.connect(dest);
+
+ var elem = document.getElementById('audioelem');
+ elem.srcObject = dest.stream;
+ elem.onloadedmetadata = function() {
+ ok(true, "got metadata event");
+ setTimeout(function() {
+ is(elem.played.length, 1, "should have a played interval");
+ is(elem.played.start(0), 0, "should have played immediately");
+ isnot(elem.played.end(0), 0, "should have played for a non-zero interval");
+
+ // This will end the media element.
+ dest.stream.getTracks()[0].stop();
+ }, 2000);
+ };
+ elem.onended = function() {
+ ok(true, "media element ended after destination track.stop()");
+ SimpleTest.finish();
+ };
+
+ source.start(0);
+ elem.play();
+});
+</script>
diff --git a/dom/media/webaudio/test/test_mediaStreamAudioSourceNode.html b/dom/media/webaudio/test/test_mediaStreamAudioSourceNode.html
new file mode 100644
index 0000000000..eaa8a564b9
--- /dev/null
+++ b/dom/media/webaudio/test/test_mediaStreamAudioSourceNode.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<meta charset="utf-8">
+<head>
+ <title>Test MediaStreamAudioSourceNode processing is correct</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+function createBuffer(context) {
+ var buffer = context.createBuffer(2, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ buffer.getChannelData(1)[i] = -buffer.getChannelData(0)[i];
+ }
+ return buffer;
+}
+
+var gTest = {
+ length: 2048,
+ skipOfflineContextTests: true,
+ createGraph(context) {
+ var sourceGraph = new AudioContext();
+ var source = sourceGraph.createBufferSource();
+ source.buffer = createBuffer(context);
+ var dest = sourceGraph.createMediaStreamDestination();
+ source.connect(dest);
+ source.start(0);
+
+ var mediaStreamSource = new MediaStreamAudioSourceNode(context, { mediaStream: dest.stream });
+ // channelCount and channelCountMode should have no effect
+ mediaStreamSource.channelCount = 1;
+ mediaStreamSource.channelCountMode = "explicit";
+ return mediaStreamSource;
+ },
+ createExpectedBuffers(context) {
+ return createBuffer(context);
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_mediaStreamAudioSourceNodeCrossOrigin.html b/dom/media/webaudio/test/test_mediaStreamAudioSourceNodeCrossOrigin.html
new file mode 100644
index 0000000000..d79ce50ab8
--- /dev/null
+++ b/dom/media/webaudio/test/test_mediaStreamAudioSourceNodeCrossOrigin.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<meta charset="utf-8">
+<head>
+ <title>Test MediaStreamAudioSourceNode doesn't get data from cross-origin media resources</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+var audio = new Audio("http://example.org:80/tests/dom/media/webaudio/test/small-shot.ogg");
+audio.load();
+var context = new AudioContext();
+audio.onloadedmetadata = function() {
+ var node = context.createMediaStreamSource(audio.mozCaptureStreamUntilEnded());
+ var sp = context.createScriptProcessor(2048, 1);
+ node.connect(sp);
+ var nonzeroSampleCount = 0;
+ var complete = false;
+ var iterationCount = 0;
+
+ // This test ensures we receive at least expectedSampleCount nonzero samples
+ function processSamples(e) {
+ if (complete) {
+ return;
+ }
+
+ if (iterationCount == 0) {
+ // Don't start playing the audio until the AudioContext stuff is connected
+ // and running.
+ audio.play();
+ }
+ ++iterationCount;
+
+ var buf = e.inputBuffer.getChannelData(0);
+ var nonzeroSamplesThisBuffer = 0;
+ for (var i = 0; i < buf.length; ++i) {
+ if (buf[i] != 0) {
+ ++nonzeroSamplesThisBuffer;
+ }
+ }
+ is(nonzeroSamplesThisBuffer, 0,
+ "Checking all samples are zero");
+ if (iterationCount >= 20) {
+ SimpleTest.finish();
+ complete = true;
+ }
+ }
+
+ audio.oncanplaythrough = function() {
+ sp.onaudioprocess = processSamples;
+ };
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_mediaStreamAudioSourceNodeNoGC.html b/dom/media/webaudio/test/test_mediaStreamAudioSourceNodeNoGC.html
new file mode 100644
index 0000000000..7920af9f7b
--- /dev/null
+++ b/dom/media/webaudio/test/test_mediaStreamAudioSourceNodeNoGC.html
@@ -0,0 +1,116 @@
+<!DOCTYPE HTML>
+<html>
+<meta charset="utf-8">
+<head>
+ <title>Test that MediaStreamAudioSourceNode and its input MediaStream stays alive while there are active tracks</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("gUM and WebAudio data is async to main thread. " +
+ "We need a timeout to see that something does " +
+ "NOT happen to data.");
+
+let context = new AudioContext();
+let analyser = context.createAnalyser();
+
+function wait(millis, resolveWithThis) {
+ return new Promise(resolve => setTimeout(() => resolve(resolveWithThis), millis));
+}
+
+function binIndexForFrequency(frequency) {
+ return 1 + Math.round(frequency * analyser.fftSize / context.sampleRate);
+}
+
+function waitForAudio(analysisFunction, cancelPromise) {
+ let data = new Uint8Array(analyser.frequencyBinCount);
+ let cancelled = false;
+ let cancelledMsg = "";
+ cancelPromise.then(msg => {
+ cancelled = true;
+ cancelledMsg = msg;
+ });
+ return new Promise((resolve, reject) => {
+ let loop = () => {
+ analyser.getByteFrequencyData(data);
+ if (cancelled) {
+ reject(new Error("waitForAudio cancelled: " + cancelledMsg));
+ return;
+ }
+ if (analysisFunction(data)) {
+ resolve();
+ return;
+ }
+ requestAnimationFrame(loop);
+ };
+ loop();
+ });
+}
+
+async function test(sourceNode) {
+ try {
+ await analyser.connect(context.destination);
+
+ ok(true, "Waiting for audio to pass through the analyser")
+ await waitForAudio(arr => arr[binIndexForFrequency(1000)] > 200,
+ wait(60000, "Timeout waiting for audio"));
+
+ ok(true, "Audio was detected by the analyser. Forcing CC.");
+ SpecialPowers.forceCC();
+ SpecialPowers.forceGC();
+ SpecialPowers.forceCC();
+ SpecialPowers.forceGC();
+
+ info("Checking that GC didn't destroy the stream or source node");
+ await waitForAudio(arr => arr[binIndexForFrequency(1000)] < 50,
+ wait(5000, "Timeout waiting for GC (timeout OK)"))
+ .then(() => Promise.reject("Audio stopped unexpectedly"),
+ () => Promise.resolve());
+
+ ok(true, "Audio is still flowing");
+ } catch(e) {
+ ok(false, "Error executing test: " + e + (e.stack ? "\n" + e.stack : ""));
+ SimpleTest.finish();
+ }
+}
+
+(async function() {
+ try {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // This test expects the fake audio device, specifically for the tones
+ // it outputs. Explicitly disable the audio loopback device and enable
+ // fake streams.
+ ['media.audio_loopback_dev', ''],
+ ['media.navigator.streams.fake', true],
+ ['media.navigator.permission.disabled', true]
+ ]
+ });
+
+ // Test stream source GC
+ let stream = await navigator.mediaDevices.getUserMedia({audio: true});
+ let source = context.createMediaStreamSource(stream);
+ stream = null;
+ source.connect(analyser);
+ await test(source);
+
+ // Test track source GC
+ stream = await navigator.mediaDevices.getUserMedia({audio: true});
+ source = context.createMediaStreamTrackSource(stream.getAudioTracks()[0]);
+ stream = null;
+ source.connect(analyser);
+ await test(source);
+ } catch(e) {
+ ok(false, `Error executing test: ${e}${e.stack ? "\n" + e.stack : ""}`);
+ } finally {
+ context.close();
+ SimpleTest.finish();
+ }
+})();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_mediaStreamAudioSourceNodePassThrough.html b/dom/media/webaudio/test/test_mediaStreamAudioSourceNodePassThrough.html
new file mode 100644
index 0000000000..379bfdbc6a
--- /dev/null
+++ b/dom/media/webaudio/test/test_mediaStreamAudioSourceNodePassThrough.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<meta charset="utf-8">
+<head>
+ <title>Test MediaStreamAudioSourceNode passthrough</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+function createBuffer(context, delay) {
+ var buffer = context.createBuffer(2, 2048, context.sampleRate);
+ for (var i = 0; i < 2048 - delay; ++i) {
+ buffer.getChannelData(0)[i + delay] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ buffer.getChannelData(1)[i + delay] = -buffer.getChannelData(0)[i + delay];
+ }
+ return buffer;
+}
+
+var gTest = {
+ length: 2048,
+ skipOfflineContextTests: true,
+ createGraph(context) {
+ var sourceGraph = new AudioContext();
+ var source = sourceGraph.createBufferSource();
+ source.buffer = createBuffer(context, 0);
+ var dest = sourceGraph.createMediaStreamDestination();
+ source.connect(dest);
+ source.start(0);
+
+ var mediaStreamSource = context.createMediaStreamSource(dest.stream);
+ // channelCount and channelCountMode should have no effect
+ mediaStreamSource.channelCount = 1;
+ mediaStreamSource.channelCountMode = "explicit";
+
+ var srcWrapped = SpecialPowers.wrap(mediaStreamSource);
+ ok("passThrough" in srcWrapped, "MediaStreamAudioSourceNode should support the passThrough API");
+ srcWrapped.passThrough = true;
+
+ return mediaStreamSource;
+ },
+ createExpectedBuffers(context) {
+ return context.createBuffer(2, 2048, context.sampleRate);
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_mediaStreamAudioSourceNodeResampling.html b/dom/media/webaudio/test/test_mediaStreamAudioSourceNodeResampling.html
new file mode 100644
index 0000000000..efacf1ecc5
--- /dev/null
+++ b/dom/media/webaudio/test/test_mediaStreamAudioSourceNodeResampling.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<meta charset="utf-8">
+<head>
+ <title>Test MediaStreamAudioSourceNode processing is correct</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+function test() {
+ var audio = new Audio("small-shot.ogg");
+ var context = new AudioContext();
+ var expectedMinNonzeroSampleCount;
+ var expectedMaxNonzeroSampleCount;
+ var nonzeroSampleCount = 0;
+ var complete = false;
+ var iterationCount = 0;
+
+ // This test ensures we receive at least expectedSampleCount nonzero samples
+ function processSamples(e) {
+ if (complete) {
+ return;
+ }
+
+ if (iterationCount == 0) {
+ // Don't start playing the audio until the AudioContext stuff is connected
+ // and running.
+ audio.play();
+ }
+ ++iterationCount;
+
+ var buf = e.inputBuffer.getChannelData(0);
+ var nonzeroSamplesThisBuffer = 0;
+ for (var i = 0; i < buf.length; ++i) {
+ if (buf[i] != 0) {
+ ++nonzeroSamplesThisBuffer;
+ }
+ }
+ nonzeroSampleCount += nonzeroSamplesThisBuffer;
+ is(e.inputBuffer.numberOfChannels, 1,
+ "Checking data channel count (nonzeroSamplesThisBuffer=" +
+ nonzeroSamplesThisBuffer + ")");
+ ok(nonzeroSampleCount <= expectedMaxNonzeroSampleCount,
+ "Too many nonzero samples (got " + nonzeroSampleCount + ", expected max " + expectedMaxNonzeroSampleCount + ")");
+ if (nonzeroSampleCount >= expectedMinNonzeroSampleCount &&
+ nonzeroSamplesThisBuffer == 0) {
+ ok(true,
+ "Check received enough nonzero samples (got " + nonzeroSampleCount + ", expected min " + expectedMinNonzeroSampleCount + ")");
+ SimpleTest.finish();
+ complete = true;
+ }
+ }
+
+ audio.onloadedmetadata = function() {
+ var node = context.createMediaStreamSource(audio.mozCaptureStreamUntilEnded());
+ var sp = context.createScriptProcessor(2048, 1, 0);
+ node.connect(sp);
+ // Use a fuzz factor of 100 to account for samples that just happen to be zero
+ expectedMinNonzeroSampleCount = Math.floor(audio.duration*context.sampleRate) - 100;
+ expectedMaxNonzeroSampleCount = Math.floor(audio.duration*context.sampleRate) + 500;
+ sp.onaudioprocess = processSamples;
+ };
+}
+
+SpecialPowers.pushPrefEnv({"set": [["media.preload.default", 2], ["media.preload.auto", 3]]}, test);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_mediaStreamTrackAudioSourceNode.html b/dom/media/webaudio/test/test_mediaStreamTrackAudioSourceNode.html
new file mode 100644
index 0000000000..350e9e0fab
--- /dev/null
+++ b/dom/media/webaudio/test/test_mediaStreamTrackAudioSourceNode.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<meta charset="utf-8">
+<head>
+ <title>Test MediaStreamTrackAudioSourceNode processing is correct</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+function createBuffer(context) {
+ let buffer = context.createBuffer(2, 2048, context.sampleRate);
+ for (let i = 0; i < 2048; ++i) {
+ buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ buffer.getChannelData(1)[i] = -buffer.getChannelData(0)[i];
+ }
+ return buffer;
+}
+
+let gTest = {
+ length: 2048,
+ skipOfflineContextTests: true,
+ createGraph(context) {
+ let sourceGraph = new AudioContext();
+ let source = sourceGraph.createBufferSource();
+ source.buffer = createBuffer(context);
+ let dest = sourceGraph.createMediaStreamDestination();
+ source.connect(dest);
+
+ // Extract first audio track from dest.stream
+ let track = dest.stream.getAudioTracks()[0];
+
+ source.start(0);
+
+ let mediaStreamTrackSource = new MediaStreamTrackAudioSourceNode(context, { mediaStreamTrack: track });
+ // channelCount and channelCountMode should have no effect
+ mediaStreamTrackSource.channelCount = 1;
+ mediaStreamTrackSource.channelCountMode = "explicit";
+ return mediaStreamTrackSource;
+ },
+ createExpectedBuffers(context) {
+ return createBuffer(context);
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_mediaStreamTrackAudioSourceNodeCrossOrigin.html b/dom/media/webaudio/test/test_mediaStreamTrackAudioSourceNodeCrossOrigin.html
new file mode 100644
index 0000000000..313cd424c0
--- /dev/null
+++ b/dom/media/webaudio/test/test_mediaStreamTrackAudioSourceNodeCrossOrigin.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<meta charset="utf-8">
+<head>
+<title>Test MediaStreamTrackAudioSourceNode doesn't get data from cross-origin media resources</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+const CROSS_ORIGIN_URL = "http://example.org:80/tests/dom/media/webaudio/test/sine-440-10s.opus"
+let iterationCount = 0;
+let context = null;
+
+function processSamples(e) {
+ ++iterationCount;
+
+ let buf = e.inputBuffer.getChannelData(0);
+ let nonzeroSamplesThisBuffer = 0;
+ for (let i = 0; i < buf.length; ++i) {
+ if (buf[i] != 0) {
+ ++nonzeroSamplesThisBuffer;
+ }
+ }
+ is(nonzeroSamplesThisBuffer, 0,
+ "a source that is cross origin cannot be inspected by Web Audio");
+
+ if (iterationCount == 40) {
+ sp.onaudioprocess = null;
+ context.close();
+ SimpleTest.finish();
+ }
+}
+
+let audio = new Audio();
+audio.src = CROSS_ORIGIN_URL;
+audio.onloadedmetadata = function () {
+ context = new AudioContext();
+ let stream = audio.mozCaptureStream();
+ let track = stream.getAudioTracks()[0];
+ let node = context.createMediaStreamTrackSource(track);
+ node.connect(context.destination);
+ sp = context.createScriptProcessor(2048, 1);
+ sp.onaudioprocess = processSamples;
+ node.connect(sp);
+}
+
+</script>
+</pre>
+</body>
diff --git a/dom/media/webaudio/test/test_mediaStreamTrackAudioSourceNodeVideo.html b/dom/media/webaudio/test/test_mediaStreamTrackAudioSourceNodeVideo.html
new file mode 100644
index 0000000000..b98cfc6a4f
--- /dev/null
+++ b/dom/media/webaudio/test/test_mediaStreamTrackAudioSourceNodeVideo.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<meta charset="utf-8">
+<head>
+ <title>Test MediaStreamTrackAudioSourceNode throw video track</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="/tests/dom/media/webaudio/test/webaudio.js"></script>
+ <script type="text/javascript" src="/tests/dom/media/webrtc/tests/mochitests/head.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+ let context = new AudioContext();
+ let canvas = document.createElement("canvas");
+ canvas.getContext("2d");
+ let track = canvas.captureStream().getTracks()[0];
+
+ expectException(() => {
+ let mediaStreamTrackSource = new MediaStreamTrackAudioSourceNode(
+ context,
+ { mediaStreamTrack: track });
+ }, DOMException.INVALID_STATE_ERR);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_mixingRules.html b/dom/media/webaudio/test/test_mixingRules.html
new file mode 100644
index 0000000000..719175fbfb
--- /dev/null
+++ b/dom/media/webaudio/test/test_mixingRules.html
@@ -0,0 +1,402 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title>Testcase for AudioNode channel up-mix/down-mix rules</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+
+<body>
+
+<script>
+
+// This test is based on http://src.chromium.org/viewvc/blink/trunk/LayoutTests/webaudio/audionode-channel-rules.html
+
+var context = null;
+var sp = null;
+var renderNumberOfChannels = 8;
+var singleTestFrameLength = 8;
+var testBuffers;
+
+// A list of connections to an AudioNode input, each of which is to be used in one or more specific test cases.
+// Each element in the list is a string, with the number of connections corresponding to the length of the string,
+// and each character in the string is from '1' to '8' representing a 1 to 8 channel connection (from an AudioNode output).
+// For example, the string "128" means 3 connections, having 1, 2, and 8 channels respectively.
+var connectionsList = [];
+for (var i = 1; i <= 8; ++i) {
+ connectionsList.push(i.toString());
+ for (var j = 1; j <= 8; ++j) {
+ connectionsList.push(i.toString() + j.toString());
+ }
+}
+
+// A list of mixing rules, each of which will be tested against all of the connections in connectionsList.
+var mixingRulesList = [
+ {channelCount: 1, channelCountMode: "max", channelInterpretation: "speakers"},
+ {channelCount: 2, channelCountMode: "clamped-max", channelInterpretation: "speakers"},
+ {channelCount: 3, channelCountMode: "clamped-max", channelInterpretation: "speakers"},
+ {channelCount: 4, channelCountMode: "clamped-max", channelInterpretation: "speakers"},
+ {channelCount: 5, channelCountMode: "clamped-max", channelInterpretation: "speakers"},
+ {channelCount: 6, channelCountMode: "clamped-max", channelInterpretation: "speakers"},
+ {channelCount: 7, channelCountMode: "clamped-max", channelInterpretation: "speakers"},
+ {channelCount: 2, channelCountMode: "explicit", channelInterpretation: "speakers"},
+ {channelCount: 3, channelCountMode: "explicit", channelInterpretation: "speakers"},
+ {channelCount: 4, channelCountMode: "explicit", channelInterpretation: "speakers"},
+ {channelCount: 5, channelCountMode: "explicit", channelInterpretation: "speakers"},
+ {channelCount: 6, channelCountMode: "explicit", channelInterpretation: "speakers"},
+ {channelCount: 7, channelCountMode: "explicit", channelInterpretation: "speakers"},
+ {channelCount: 8, channelCountMode: "explicit", channelInterpretation: "speakers"},
+ {channelCount: 1, channelCountMode: "max", channelInterpretation: "discrete"},
+ {channelCount: 2, channelCountMode: "clamped-max", channelInterpretation: "discrete"},
+ {channelCount: 3, channelCountMode: "clamped-max", channelInterpretation: "discrete"},
+ {channelCount: 4, channelCountMode: "clamped-max", channelInterpretation: "discrete"},
+ {channelCount: 5, channelCountMode: "clamped-max", channelInterpretation: "discrete"},
+ {channelCount: 6, channelCountMode: "clamped-max", channelInterpretation: "discrete"},
+ {channelCount: 3, channelCountMode: "explicit", channelInterpretation: "discrete"},
+ {channelCount: 4, channelCountMode: "explicit", channelInterpretation: "discrete"},
+ {channelCount: 5, channelCountMode: "explicit", channelInterpretation: "discrete"},
+ {channelCount: 6, channelCountMode: "explicit", channelInterpretation: "discrete"},
+ {channelCount: 7, channelCountMode: "explicit", channelInterpretation: "discrete"},
+ {channelCount: 8, channelCountMode: "explicit", channelInterpretation: "discrete"},
+];
+
+var numberOfTests = mixingRulesList.length * connectionsList.length;
+
+// Create an n-channel buffer, with all sample data zero except for a shifted impulse.
+// The impulse position depends on the channel index.
+// For example, for a 4-channel buffer:
+// channel0: 1 0 0 0 0 0 0 0
+// channel1: 0 1 0 0 0 0 0 0
+// channel2: 0 0 1 0 0 0 0 0
+// channel3: 0 0 0 1 0 0 0 0
+function createTestBuffer(numberOfChannels) {
+ var buffer = context.createBuffer(numberOfChannels, singleTestFrameLength, context.sampleRate);
+ for (var i = 0; i < numberOfChannels; ++i) {
+ var data = buffer.getChannelData(i);
+ data[i] = 1;
+ }
+ return buffer;
+}
+
+// Discrete channel interpretation mixing:
+// https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html#UpMix
+// up-mix by filling channels until they run out then ignore remaining dest channels.
+// down-mix by filling as many channels as possible, then dropping remaining source channels.
+function discreteSum(sourceBuffer, destBuffer) {
+ if (sourceBuffer.length != destBuffer.length) {
+ is(sourceBuffer.length, destBuffer.length, "source and destination buffers should have the same length");
+ }
+
+ var numberOfChannels = Math.min(sourceBuffer.numberOfChannels, destBuffer.numberOfChannels);
+ var length = sourceBuffer.length;
+
+ for (var c = 0; c < numberOfChannels; ++c) {
+ var source = sourceBuffer.getChannelData(c);
+ var dest = destBuffer.getChannelData(c);
+ for (var i = 0; i < length; ++i) {
+ dest[i] += source[i];
+ }
+ }
+}
+
+// Speaker channel interpretation mixing:
+// https://dvcs.w3.org/hg/audio/raw-file/tip/webaudio/specification.html#UpMix
+// eslint-disable-next-line complexity
+function speakersSum(sourceBuffer, destBuffer)
+{
+ var numberOfSourceChannels = sourceBuffer.numberOfChannels;
+ var numberOfDestinationChannels = destBuffer.numberOfChannels;
+ var length = destBuffer.length;
+
+ if ((numberOfDestinationChannels == 2 && numberOfSourceChannels == 1) ||
+ (numberOfDestinationChannels == 4 && numberOfSourceChannels == 1)) {
+ // Handle mono -> stereo/Quad case (summing mono channel into both left and right).
+ var source = sourceBuffer.getChannelData(0);
+ var destL = destBuffer.getChannelData(0);
+ var destR = destBuffer.getChannelData(1);
+
+ for (var i = 0; i < length; ++i) {
+ destL[i] += source[i];
+ destR[i] += source[i];
+ }
+ } else if ((numberOfDestinationChannels == 4 && numberOfSourceChannels == 2) ||
+ (numberOfDestinationChannels == 6 && numberOfSourceChannels == 2)) {
+ // Handle stereo -> Quad/5.1 case (summing left and right channels into the output's left and right).
+ var sourceL = sourceBuffer.getChannelData(0);
+ var sourceR = sourceBuffer.getChannelData(1);
+ var destL = destBuffer.getChannelData(0);
+ var destR = destBuffer.getChannelData(1);
+
+ for (var i = 0; i < length; ++i) {
+ destL[i] += sourceL[i];
+ destR[i] += sourceR[i];
+ }
+ } else if (numberOfDestinationChannels == 1 && numberOfSourceChannels == 2) {
+ // Handle stereo -> mono case. output += 0.5 * (input.L + input.R).
+ var sourceL = sourceBuffer.getChannelData(0);
+ var sourceR = sourceBuffer.getChannelData(1);
+ var dest = destBuffer.getChannelData(0);
+
+ for (var i = 0; i < length; ++i) {
+ dest[i] += 0.5 * (sourceL[i] + sourceR[i]);
+ }
+ } else if (numberOfDestinationChannels == 1 && numberOfSourceChannels == 4) {
+ // Handle Quad -> mono case. output += 0.25 * (input.L + input.R + input.SL + input.SR).
+ var sourceL = sourceBuffer.getChannelData(0);
+ var sourceR = sourceBuffer.getChannelData(1);
+ var sourceSL = sourceBuffer.getChannelData(2);
+ var sourceSR = sourceBuffer.getChannelData(3);
+ var dest = destBuffer.getChannelData(0);
+
+ for (var i = 0; i < length; ++i) {
+ dest[i] += 0.25 * (sourceL[i] + sourceR[i] + sourceSL[i] + sourceSR[i]);
+ }
+ } else if (numberOfDestinationChannels == 2 && numberOfSourceChannels == 4) {
+ // Handle Quad -> stereo case. outputLeft += 0.5 * (input.L + input.SL),
+ // outputRight += 0.5 * (input.R + input.SR).
+ var sourceL = sourceBuffer.getChannelData(0);
+ var sourceR = sourceBuffer.getChannelData(1);
+ var sourceSL = sourceBuffer.getChannelData(2);
+ var sourceSR = sourceBuffer.getChannelData(3);
+ var destL = destBuffer.getChannelData(0);
+ var destR = destBuffer.getChannelData(1);
+
+ for (var i = 0; i < length; ++i) {
+ destL[i] += 0.5 * (sourceL[i] + sourceSL[i]);
+ destR[i] += 0.5 * (sourceR[i] + sourceSR[i]);
+ }
+ } else if (numberOfDestinationChannels == 6 && numberOfSourceChannels == 4) {
+ // Handle Quad -> 5.1 case. outputLeft += (inputL, inputR, 0, 0, inputSL, inputSR)
+ var sourceL = sourceBuffer.getChannelData(0);
+ var sourceR = sourceBuffer.getChannelData(1);
+ var sourceSL = sourceBuffer.getChannelData(2);
+ var sourceSR = sourceBuffer.getChannelData(3);
+ var destL = destBuffer.getChannelData(0);
+ var destR = destBuffer.getChannelData(1);
+ var destSL = destBuffer.getChannelData(4);
+ var destSR = destBuffer.getChannelData(5);
+
+ for (var i = 0; i < length; ++i) {
+ destL[i] += sourceL[i];
+ destR[i] += sourceR[i];
+ destSL[i] += sourceSL[i];
+ destSR[i] += sourceSR[i];
+ }
+ } else if (numberOfDestinationChannels == 6 && numberOfSourceChannels == 1) {
+ // Handle mono -> 5.1 case, sum mono channel into center.
+ var source = sourceBuffer.getChannelData(0);
+ var dest = destBuffer.getChannelData(2);
+
+ for (var i = 0; i < length; ++i) {
+ dest[i] += source[i];
+ }
+ } else if (numberOfDestinationChannels == 1 && numberOfSourceChannels == 6) {
+ // Handle 5.1 -> mono.
+ var sourceL = sourceBuffer.getChannelData(0);
+ var sourceR = sourceBuffer.getChannelData(1);
+ var sourceC = sourceBuffer.getChannelData(2);
+ // skip LFE for now, according to current spec.
+ var sourceSL = sourceBuffer.getChannelData(4);
+ var sourceSR = sourceBuffer.getChannelData(5);
+ var dest = destBuffer.getChannelData(0);
+
+ for (var i = 0; i < length; ++i) {
+ dest[i] += 0.7071 * (sourceL[i] + sourceR[i]) + sourceC[i] + 0.5 * (sourceSL[i] + sourceSR[i]);
+ }
+ } else if (numberOfDestinationChannels == 2 && numberOfSourceChannels == 6) {
+ // Handle 5.1 -> stereo.
+ var sourceL = sourceBuffer.getChannelData(0);
+ var sourceR = sourceBuffer.getChannelData(1);
+ var sourceC = sourceBuffer.getChannelData(2);
+ // skip LFE for now, according to current spec.
+ var sourceSL = sourceBuffer.getChannelData(4);
+ var sourceSR = sourceBuffer.getChannelData(5);
+ var destL = destBuffer.getChannelData(0);
+ var destR = destBuffer.getChannelData(1);
+
+ for (var i = 0; i < length; ++i) {
+ destL[i] += sourceL[i] + 0.7071 * (sourceC[i] + sourceSL[i]);
+ destR[i] += sourceR[i] + 0.7071 * (sourceC[i] + sourceSR[i]);
+ }
+ } else if (numberOfDestinationChannels == 4 && numberOfSourceChannels == 6) {
+ // Handle 5.1 -> Quad.
+ var sourceL = sourceBuffer.getChannelData(0);
+ var sourceR = sourceBuffer.getChannelData(1);
+ var sourceC = sourceBuffer.getChannelData(2);
+ // skip LFE for now, according to current spec.
+ var sourceSL = sourceBuffer.getChannelData(4);
+ var sourceSR = sourceBuffer.getChannelData(5);
+ var destL = destBuffer.getChannelData(0);
+ var destR = destBuffer.getChannelData(1);
+ var destSL = destBuffer.getChannelData(2);
+ var destSR = destBuffer.getChannelData(3);
+
+ for (var i = 0; i < length; ++i) {
+ destL[i] += sourceL[i] + 0.7071 * sourceC[i];
+ destR[i] += sourceR[i] + 0.7071 * sourceC[i];
+ destSL[i] += sourceSL[i];
+ destSR[i] += sourceSR[i];
+ }
+ } else {
+ // Fallback for unknown combinations.
+ discreteSum(sourceBuffer, destBuffer);
+ }
+}
+
+function scheduleTest(testNumber, connections, channelCount, channelCountMode, channelInterpretation) {
+ var mixNode = context.createGain();
+ mixNode.channelCount = channelCount;
+ mixNode.channelCountMode = channelCountMode;
+ mixNode.channelInterpretation = channelInterpretation;
+ mixNode.connect(sp);
+
+ for (var i = 0; i < connections.length; ++i) {
+ var connectionNumberOfChannels = connections.charCodeAt(i) - "0".charCodeAt(0);
+
+ var source = context.createBufferSource();
+ // Get a buffer with the right number of channels, converting from 1-based to 0-based index.
+ var buffer = testBuffers[connectionNumberOfChannels - 1];
+ source.buffer = buffer;
+ source.connect(mixNode);
+
+ // Start at the right offset.
+ var sampleFrameOffset = testNumber * singleTestFrameLength;
+ var time = sampleFrameOffset / context.sampleRate;
+ source.start(time);
+ }
+}
+
+function computeNumberOfChannels(connections, channelCount, channelCountMode) {
+ if (channelCountMode == "explicit")
+ return channelCount;
+
+ var computedNumberOfChannels = 1; // Must have at least one channel.
+
+ // Compute "computedNumberOfChannels" based on all the connections.
+ for (var i = 0; i < connections.length; ++i) {
+ var connectionNumberOfChannels = connections.charCodeAt(i) - "0".charCodeAt(0);
+ computedNumberOfChannels = Math.max(computedNumberOfChannels, connectionNumberOfChannels);
+ }
+
+ if (channelCountMode == "clamped-max")
+ computedNumberOfChannels = Math.min(computedNumberOfChannels, channelCount);
+
+ return computedNumberOfChannels;
+}
+
+function checkTestResult(renderedBuffer, testNumber, connections, channelCount, channelCountMode, channelInterpretation) {
+ var computedNumberOfChannels = computeNumberOfChannels(connections, channelCount, channelCountMode);
+
+ // Create a zero-initialized silent AudioBuffer with computedNumberOfChannels.
+ var destBuffer = context.createBuffer(computedNumberOfChannels, singleTestFrameLength, context.sampleRate);
+
+ // Mix all of the connections into the destination buffer.
+ for (var i = 0; i < connections.length; ++i) {
+ var connectionNumberOfChannels = connections.charCodeAt(i) - "0".charCodeAt(0);
+ var sourceBuffer = testBuffers[connectionNumberOfChannels - 1]; // convert from 1-based to 0-based index
+
+ if (channelInterpretation == "speakers") {
+ speakersSum(sourceBuffer, destBuffer);
+ } else if (channelInterpretation == "discrete") {
+ discreteSum(sourceBuffer, destBuffer);
+ } else {
+ ok(false, "Invalid channel interpretation!");
+ }
+ }
+
+ // Validate that destBuffer matches the rendered output.
+ // We need to check the rendered output at a specific sample-frame-offset corresponding
+ // to the specific test case we're checking for based on testNumber.
+
+ var sampleFrameOffset = testNumber * singleTestFrameLength;
+ for (var c = 0; c < renderNumberOfChannels; ++c) {
+ var renderedData = renderedBuffer.getChannelData(c);
+ for (var frame = 0; frame < singleTestFrameLength; ++frame) {
+ var renderedValue = renderedData[frame + sampleFrameOffset];
+
+ var expectedValue = 0;
+ if (c < destBuffer.numberOfChannels) {
+ var expectedData = destBuffer.getChannelData(c);
+ expectedValue = expectedData[frame];
+ }
+
+ if (Math.abs(renderedValue - expectedValue) > 1e-4) {
+ var s = "connections: " + connections + ", " + channelCountMode;
+
+ // channelCount is ignored in "max" mode.
+ if (channelCountMode == "clamped-max" || channelCountMode == "explicit") {
+ s += "(" + channelCount + ")";
+ }
+
+ s += ", " + channelInterpretation + ". ";
+
+ var message = s + "rendered: " + renderedValue + " expected: " + expectedValue + " channel: " + c + " frame: " + frame;
+ is(renderedValue, expectedValue, message);
+ }
+ }
+ }
+}
+
+function checkResult(event) {
+ var buffer = event.inputBuffer;
+
+ // Sanity check result.
+ ok(buffer.length != numberOfTests * singleTestFrameLength ||
+ buffer.numberOfChannels != renderNumberOfChannels, "Sanity check");
+
+ // Check all the tests.
+ var testNumber = 0;
+ for (var m = 0; m < mixingRulesList.length; ++m) {
+ var mixingRules = mixingRulesList[m];
+ for (var i = 0; i < connectionsList.length; ++i, ++testNumber) {
+ checkTestResult(buffer, testNumber, connectionsList[i], mixingRules.channelCount, mixingRules.channelCountMode, mixingRules.channelInterpretation);
+ }
+ }
+
+ sp.onaudioprocess = null;
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+function runTest() {
+ // Create 8-channel offline audio context.
+ // Each test will render 8 sample-frames starting at sample-frame position testNumber * 8.
+ var totalFrameLength = numberOfTests * singleTestFrameLength;
+ context = new AudioContext();
+ var nextPowerOfTwo = 256;
+ while (nextPowerOfTwo < totalFrameLength) {
+ nextPowerOfTwo *= 2;
+ }
+ sp = context.createScriptProcessor(nextPowerOfTwo, renderNumberOfChannels);
+
+ // Set destination to discrete mixing.
+ sp.channelCount = renderNumberOfChannels;
+ sp.channelCountMode = "explicit";
+ sp.channelInterpretation = "discrete";
+
+ // Create test buffers from 1 to 8 channels.
+ testBuffers = new Array();
+ for (var i = 0; i < renderNumberOfChannels; ++i) {
+ testBuffers[i] = createTestBuffer(i + 1);
+ }
+
+ // Schedule all the tests.
+ var testNumber = 0;
+ for (var m = 0; m < mixingRulesList.length; ++m) {
+ var mixingRules = mixingRulesList[m];
+ for (var i = 0; i < connectionsList.length; ++i, ++testNumber) {
+ scheduleTest(testNumber, connectionsList[i], mixingRules.channelCount, mixingRules.channelCountMode, mixingRules.channelInterpretation);
+ }
+ }
+
+ // Render then check results.
+ sp.onaudioprocess = checkResult;
+}
+
+runTest();
+
+</script>
+
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_nodeCreationDocumentGone.html b/dom/media/webaudio/test/test_nodeCreationDocumentGone.html
new file mode 100644
index 0000000000..07a4f7a97d
--- /dev/null
+++ b/dom/media/webaudio/test/test_nodeCreationDocumentGone.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test whether we can create an AudioContext interface</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.requestCompleteLog();
+SimpleTest.waitForExplicitFinish();
+
+var a = window.open("file_nodeCreationDocumentGone.html");
+a.onbeforeunload = function() {
+ setTimeout(function(){
+ try {
+ a.context.createScriptProcessor(512, 1, 1);
+ } catch(e) {
+ ok (true,"got exception");
+ }
+ setTimeout(function() {
+ ok (true,"no crash");
+ SimpleTest.finish();
+ }, 0);
+ }, 0);
+}
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_nodeToParamConnection.html b/dom/media/webaudio/test/test_nodeToParamConnection.html
new file mode 100644
index 0000000000..8a77e7d0a2
--- /dev/null
+++ b/dom/media/webaudio/test/test_nodeToParamConnection.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test connecting an AudioNode to an AudioParam</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 2048,
+ createGraph(context) {
+ var sourceBuffer = context.createBuffer(2, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ sourceBuffer.getChannelData(0)[i] = 1;
+ sourceBuffer.getChannelData(1)[i] = -1;
+ }
+
+ var destination = context.destination;
+
+ var paramSource = context.createBufferSource();
+ paramSource.buffer = this.buffer;
+
+ var source = context.createBufferSource();
+ source.buffer = sourceBuffer;
+
+ var gain = context.createGain();
+
+ paramSource.connect(gain.gain);
+ source.connect(gain);
+
+ paramSource.start(0);
+ source.start(0);
+ return gain;
+ },
+ createExpectedBuffers(context) {
+ this.buffer = context.createBuffer(2, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ for (var j = 0; j < 2; ++j) {
+ this.buffer.getChannelData(j)[i] = Math.sin(440 * 2 * (j + 1) * Math.PI * i / context.sampleRate);
+ }
+ }
+ var expectedBuffer = context.createBuffer(2, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ expectedBuffer.getChannelData(0)[i] = 1 + (this.buffer.getChannelData(0)[i] + this.buffer.getChannelData(1)[i]) / 2;
+ expectedBuffer.getChannelData(1)[i] = -(1 + (this.buffer.getChannelData(0)[i] + this.buffer.getChannelData(1)[i]) / 2);
+ }
+ return expectedBuffer;
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_notAllowedToStartAudioContextGC.html b/dom/media/webaudio/test/test_notAllowedToStartAudioContextGC.html
new file mode 100644
index 0000000000..b8715c1644
--- /dev/null
+++ b/dom/media/webaudio/test/test_notAllowedToStartAudioContextGC.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test GC for not-allow-to-start audio context</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.requestFlakyTimeout(`Checking that something does not happen`);
+
+SimpleTest.waitForExplicitFinish();
+
+var destId;
+
+function observer(subject, topic, data) {
+ let id = parseInt(data);
+ ok(id != destId, "dropping another node, not the context's destination");
+}
+
+SpecialPowers.addAsyncObserver(observer, "webaudio-node-demise", false);
+SimpleTest.registerCleanupFunction(function() {
+ SpecialPowers.removeAsyncObserver(observer, "webaudio-node-demise");
+});
+
+SpecialPowers.pushPrefEnv({"set": [["media.autoplay.default", SpecialPowers.Ci.nsIAutoplay.BLOCKED],
+ ["media.autoplay.blocking_policy", 0]]},
+ startTest);
+
+function startTest() {
+ info("- create audio context -");
+ let ac = new AudioContext();
+
+ info("- get node Id -");
+ destId = SpecialPowers.getPrivilegedProps(ac.destination, "id");
+
+ info("- trigger GCs -");
+ SpecialPowers.forceGC();
+ SpecialPowers.forceCC();
+ SpecialPowers.forceGC();
+
+ info("- after three GCs -");
+
+ // We're doing this async so that we can receive observerservice messages.
+ setTimeout(function() {
+ ok(true, `AudioContext that has been prevented
+ from starting has correctly survived GC`)
+ SimpleTest.finish();
+ }, 1);
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_offlineDestinationChannelCountLess.html b/dom/media/webaudio/test/test_offlineDestinationChannelCountLess.html
new file mode 100644
index 0000000000..8ff1deac4b
--- /dev/null
+++ b/dom/media/webaudio/test/test_offlineDestinationChannelCountLess.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test OfflineAudioContext with a channel count less than the specified number</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var ctx = new OfflineAudioContext(2, 100, 22050);
+
+ var buf = ctx.createBuffer(6, 100, ctx.sampleRate);
+ for (var i = 0; i < 6; ++i) {
+ for (var j = 0; j < 100; ++j) {
+ buf.getChannelData(i)[j] = Math.sin(2 * Math.PI * 200 * j / ctx.sampleRate);
+ }
+ }
+
+ var src = ctx.createBufferSource();
+ src.buffer = buf;
+ src.start(0);
+ src.connect(ctx.destination);
+ ctx.destination.channelCountMode = "max";
+ ctx.startRendering();
+ ctx.oncomplete = function(e) {
+ is(e.renderedBuffer.numberOfChannels, 2, "Correct expected number of buffers");
+ compareChannels(e.renderedBuffer.getChannelData(0), buf.getChannelData(0));
+ compareChannels(e.renderedBuffer.getChannelData(1), buf.getChannelData(1));
+
+ SimpleTest.finish();
+ };
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_offlineDestinationChannelCountMore.html b/dom/media/webaudio/test/test_offlineDestinationChannelCountMore.html
new file mode 100644
index 0000000000..fa38114e2b
--- /dev/null
+++ b/dom/media/webaudio/test/test_offlineDestinationChannelCountMore.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test OfflineAudioContext with a channel count less than the specified number</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var ctx = new OfflineAudioContext(6, 100, 22050);
+
+ var buf = ctx.createBuffer(2, 100, ctx.sampleRate);
+ for (var i = 0; i < 2; ++i) {
+ for (var j = 0; j < 100; ++j) {
+ buf.getChannelData(i)[j] = Math.sin(2 * Math.PI * 200 * j / ctx.sampleRate);
+ }
+ }
+ var emptyBuffer = ctx.createBuffer(1, 100, ctx.sampleRate);
+
+ var src = ctx.createBufferSource();
+ src.buffer = buf;
+ src.start(0);
+ src.connect(ctx.destination);
+ ctx.destination.channelCountMode = "max";
+ ctx.startRendering();
+ ctx.oncomplete = function(e) {
+ is(e.renderedBuffer.numberOfChannels, 6, "Correct expected number of buffers");
+ compareChannels(e.renderedBuffer.getChannelData(0), buf.getChannelData(0));
+ compareChannels(e.renderedBuffer.getChannelData(1), buf.getChannelData(1));
+ for (var i = 2; i < 6; ++i) {
+ compareChannels(e.renderedBuffer.getChannelData(i), emptyBuffer.getChannelData(0));
+ }
+
+ SimpleTest.finish();
+ };
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_oscillatorNode.html b/dom/media/webaudio/test/test_oscillatorNode.html
new file mode 100644
index 0000000000..e2a47a4e1e
--- /dev/null
+++ b/dom/media/webaudio/test/test_oscillatorNode.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test the OscillatorNode interface</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+
+ var context = new AudioContext();
+ var osc = new OscillatorNode(context);
+
+ is(osc.channelCount, 2, "Oscillator node has 2 input channels by default");
+ is(osc.channelCountMode, "max", "Correct channelCountMode for the Oscillator node");
+ is(osc.channelInterpretation, "speakers", "Correct channelCountInterpretation for the Oscillator node");
+ is(osc.type, "sine", "Correct default type");
+ expectException(function() {
+ osc.type = "custom";
+ }, DOMException.INVALID_STATE_ERR);
+ is(osc.type, "sine", "Cannot set the type to custom");
+ is(osc.frequency.value, 440, "Correct default frequency value");
+ is(osc.detune.value, 0, "Correct default detine value");
+
+ // Make sure that we can set all of the valid type values
+ var types = [
+ "sine",
+ "square",
+ "sawtooth",
+ "triangle",
+ ];
+ for (var i = 0; i < types.length; ++i) {
+ osc.type = types[i];
+ }
+
+ // Verify setPeriodicWave()
+ var real = new Float32Array([1.0, 0.5, 0.25, 0.125]);
+ var imag = new Float32Array([1.0, 0.7, -1.0, 0.5]);
+ osc.setPeriodicWave(context.createPeriodicWave(real, imag));
+ is(osc.type, "custom", "Failed to set custom waveform");
+
+ expectNoException(function() {
+ osc.start();
+ });
+ expectNoException(function() {
+ osc.stop();
+ });
+
+ SimpleTest.finish();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_oscillatorNode2.html b/dom/media/webaudio/test/test_oscillatorNode2.html
new file mode 100644
index 0000000000..69a6655ff1
--- /dev/null
+++ b/dom/media/webaudio/test/test_oscillatorNode2.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test OscillatorNode lifetime and sine phase</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script src="webaudio.js" type="text/javascript"></script>
+<script class="testbody" type="text/javascript">
+
+const signalLength = 2048;
+
+function createOscillator(context) {
+ var osc = context.createOscillator();
+ osc.start(0);
+ osc.stop(signalLength/context.sampleRate);
+ return osc;
+}
+
+function connectUnreferencedOscillator(context, destination) {
+ var osc = createOscillator(context);
+ osc.connect(destination);
+}
+
+var gTest = {
+ length: signalLength,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var blend = context.createGain();
+
+ connectUnreferencedOscillator(context, blend);
+ // Test that the unreferenced oscillator remains alive until it has finished.
+ SpecialPowers.forceGC();
+ SpecialPowers.forceCC();
+
+ // Create another sine wave oscillator in negative time, which should
+ // cancel when mixed with the unreferenced oscillator.
+ var oscillator = createOscillator(context);
+ oscillator.frequency.value = -440;
+ oscillator.connect(blend);
+
+ return blend;
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_oscillatorNodeNegativeFrequency.html b/dom/media/webaudio/test/test_oscillatorNodeNegativeFrequency.html
new file mode 100644
index 0000000000..c46c0fea13
--- /dev/null
+++ b/dom/media/webaudio/test/test_oscillatorNodeNegativeFrequency.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test the OscillatorNode when the frequency is negative</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+
+ var types = ["sine",
+ "square",
+ "sawtooth",
+ "triangle"];
+
+ var finished = 0;
+ function finish() {
+ if (++finished == types.length) {
+ SimpleTest.finish();
+ }
+ }
+
+ types.forEach(function(t) {
+ var context = new OfflineAudioContext(1, 256, 44100);
+ var osc = context.createOscillator();
+
+ osc.frequency.value = -440;
+ osc.type = t;
+
+ osc.connect(context.destination);
+ osc.start();
+ context.startRendering().then(function(buffer) {
+ var samples = buffer.getChannelData(0);
+ // This samples the wave form in the middle of the first period, the value
+ // should be negative.
+ ok(samples[Math.floor(44100 / 440 / 4)] < 0., "Phase should be inverted when using a " + t + " waveform");
+ finish();
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_oscillatorNodePassThrough.html b/dom/media/webaudio/test/test_oscillatorNodePassThrough.html
new file mode 100644
index 0000000000..63c0848d06
--- /dev/null
+++ b/dom/media/webaudio/test/test_oscillatorNodePassThrough.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test Oscillator with passthrough</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 2048,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var buffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ }
+
+ var source = context.createOscillator();
+
+ var srcWrapped = SpecialPowers.wrap(source);
+ ok("passThrough" in srcWrapped, "OscillatorNode should support the passThrough API");
+ srcWrapped.passThrough = true;
+
+ source.start(0);
+ return source;
+ },
+ createExpectedBuffers(context) {
+ var expectedBuffer = context.createBuffer(1, 2048, context.sampleRate);
+
+ return [expectedBuffer];
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_oscillatorNodeStart.html b/dom/media/webaudio/test/test_oscillatorNodeStart.html
new file mode 100644
index 0000000000..4df129170f
--- /dev/null
+++ b/dom/media/webaudio/test/test_oscillatorNodeStart.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test the OscillatorNode interface</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+
+ var context = new AudioContext();
+ var osc = context.createOscillator();
+ var sp = context.createScriptProcessor(0, 1, 0);
+
+ osc.connect(sp);
+
+ sp.onaudioprocess = function (e) {
+ var input = e.inputBuffer.getChannelData(0);
+ var isSilent = true;
+ for (var i = 0; i < input.length; i++) {
+ if (input[i] != 0.0) {
+ isSilent = false;
+ }
+ }
+ sp.onaudioprocess = null;
+ ok(isSilent, "OscillatorNode should be silent before calling start.");
+ SimpleTest.finish();
+ }
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_oscillatorTypeChange.html b/dom/media/webaudio/test/test_oscillatorTypeChange.html
new file mode 100644
index 0000000000..e4b4944703
--- /dev/null
+++ b/dom/media/webaudio/test/test_oscillatorTypeChange.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test OscillatorNode type change after it has started and triangle phase</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+const bufferSize = 1024;
+
+function startTest() {
+ var ctx = new AudioContext();
+
+ var oscillator1 = ctx.createOscillator();
+ oscillator1.connect(ctx.destination);
+ oscillator1.start(0);
+
+ // Assuming the above Web Audio operations have already scheduled an event
+ // to run in stable state and start the graph thread, schedule a subsequent
+ // event to change the type of oscillator1.
+ SimpleTest.executeSoon(function() {
+ oscillator1.type = "triangle";
+
+ // Another triangle wave with -1 gain should cancel the first. This is
+ // starting at the same time as the type change, assuming that the phase
+ // is reset on type change. A negative frequency should achieve the same
+ // as the -1 gain but for bug 916285.
+ var oscillator2 = ctx.createOscillator();
+ oscillator2.type = "triangle";
+ oscillator2.start(0);
+
+ var processor = ctx.createScriptProcessor(bufferSize, 1, 0);
+ oscillator1.connect(processor);
+ var gain = ctx.createGain();
+ gain.gain.value = -1;
+ gain.connect(processor);
+ oscillator2.connect(gain);
+
+ processor.onaudioprocess = function(e) {
+ compareChannels(e.inputBuffer.getChannelData(0),
+ new Float32Array(bufferSize));
+ e.target.onaudioprocess = null;
+ SimpleTest.finish();
+ }
+ });
+};
+
+startTest();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_pannerNode.html b/dom/media/webaudio/test/test_pannerNode.html
new file mode 100644
index 0000000000..7f4d3ea915
--- /dev/null
+++ b/dom/media/webaudio/test/test_pannerNode.html
@@ -0,0 +1,71 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test PannerNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+function near(a, b, msg) {
+ ok(Math.abs(a - b) < 1e-4, msg);
+}
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var context = new AudioContext();
+ var buffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ }
+
+ var destination = context.destination;
+
+ var source = context.createBufferSource();
+
+ var panner = new PannerNode(context);
+
+ source.buffer = buffer;
+
+ source.connect(panner);
+ panner.connect(destination);
+
+ // Verify default values
+ is(panner.panningModel, "equalpower", "Correct default value for panning model");
+ is(panner.distanceModel, "inverse", "Correct default value for distance model");
+ near(panner.refDistance, 1, "Correct default value for ref distance");
+ near(panner.maxDistance, 10000, "Correct default value for max distance");
+ near(panner.rolloffFactor, 1, "Correct default value for rolloff factor");
+ near(panner.coneInnerAngle, 360, "Correct default value for cone inner angle");
+ near(panner.coneOuterAngle, 360, "Correct default value for cone outer angle");
+ near(panner.coneOuterGain, 0, "Correct default value for cone outer gain");
+ is(panner.channelCount, 2, "panner node has 2 input channels by default");
+ is(panner.channelCountMode, "clamped-max", "Correct channelCountMode for the panner node");
+ is(panner.channelInterpretation, "speakers", "Correct channelCountInterpretation for the panner node");
+
+ panner.setPosition(1, 1, 1);
+ near(panner.positionX.value, 1, "setPosition sets AudioParam properly");
+ near(panner.positionY.value, 1, "setPosition sets AudioParam properly");
+ near(panner.positionZ.value, 1, "setPosition sets AudioParam properly");
+
+ panner.setOrientation(0, 1, 0);
+ near(panner.orientationX.value, 0, "setOrientation sets AudioParam properly");
+ near(panner.orientationY.value, 1, "setOrientation sets AudioParam properly");
+ near(panner.orientationZ.value, 0, "setOrientation sets AudioParam properly");
+
+ source.start(0);
+ SimpleTest.executeSoon(function() {
+ source.stop(0);
+ source.disconnect();
+ panner.disconnect();
+
+ SimpleTest.finish();
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_pannerNodeAbove.html b/dom/media/webaudio/test/test_pannerNodeAbove.html
new file mode 100644
index 0000000000..5931fa04de
--- /dev/null
+++ b/dom/media/webaudio/test/test_pannerNodeAbove.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test PannerNode directly above</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script src="webaudio.js" type="text/javascript"></script>
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ numberOfChannels: 2,
+ createGraph(context) {
+ // An up vector will be made perpendicular to the front vector, in the
+ // front-up plane.
+ context.listener.setOrientation(0, 6.311749985202524e+307, 0, 0.1, 1000, 0);
+ // Linearly dependent vectors are ignored.
+ context.listener.setOrientation(0, 0, -6.311749985202524e+307, 0, 0, 6.311749985202524e+307);
+ var panner = context.createPanner();
+ panner.positionX.value = 2; // directly above
+ panner.rolloffFactor = 0; // no distance gain
+ panner.panningModel = "equalpower"; // no effect when directly above
+
+ var source = context.createBufferSource();
+ source.buffer = this.buffer;
+ source.connect(panner);
+ source.start(0);
+
+ return panner;
+ },
+ createExpectedBuffers(context) {
+ var expectedBuffer = context.createBuffer(2, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ // Different signals in left and right buffers
+ expectedBuffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ expectedBuffer.getChannelData(1)[i] = Math.sin(220 * 2 * Math.PI * i / context.sampleRate);
+ }
+ this.buffer = expectedBuffer;
+ return expectedBuffer;
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_pannerNodeAtZeroDistance.html b/dom/media/webaudio/test/test_pannerNodeAtZeroDistance.html
new file mode 100644
index 0000000000..a0c20f01fe
--- /dev/null
+++ b/dom/media/webaudio/test/test_pannerNodeAtZeroDistance.html
@@ -0,0 +1,149 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test PannerNode produces output even when the even when the distance is from the listener is zero</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var BUF_SIZE = 128;
+
+var types = [
+ "equalpower",
+ "HRTF"
+]
+
+var finished = 2 * types.length;
+
+function finish() {
+ if (!--finished) {
+ SimpleTest.finish();
+ }
+}
+
+function testMono(type) {
+ var ac = new OfflineAudioContext(1, BUF_SIZE, 44100);
+
+ // A sine to be used to fill the buffers
+ function sine(t) {
+ return Math.sin(440 * 2 * Math.PI * t / ac.sampleRate);
+ }
+
+ var monoBuffer = ac.createBuffer(1, BUF_SIZE, ac.sampleRate);
+ for (var i = 0; i < BUF_SIZE; ++i) {
+ monoBuffer.getChannelData(0)[i] = sine(i);
+ }
+
+ var monoSource = ac.createBufferSource();
+ monoSource.buffer = monoBuffer;
+ monoSource.start(0);
+
+ var panner = ac.createPanner();
+ panner.distanceModel = "linear";
+ panner.refDistance = 1;
+ panner.positionX.value = 0;
+ panner.positionY.value = 0;
+ panner.positionZ.value = 0;
+ monoSource.connect(panner);
+
+ var panner2 = ac.createPanner();
+ panner2.distanceModel = "inverse";
+ panner2.refDistance = 1;
+ panner2.positionX.value = 0;
+ panner2.positionY.value = 0;
+ panner2.positionZ.value = 0;
+ panner.connect(panner2);
+
+ var panner3 = ac.createPanner();
+ panner3.distanceModel = "exponential";
+ panner3.refDistance = 1;
+ panner3.positionX.value = 0;
+ panner3.positionY.value = 0;
+ panner3.positionZ.value = 0;
+ panner2.connect(panner3);
+
+ panner3.connect(ac.destination);
+
+ // Use the input buffer to compare the output. According to the spec,
+ // mono input at zero distance will apply gain = cos(0.5 * Math.PI / 2)
+ // https://webaudio.github.io/web-audio-api/#Spatialzation-equal-power-panning
+ const gain = Math.cos(0.5 * Math.PI / 2);
+ for (var i = 0; i < BUF_SIZE; ++i) {
+ monoBuffer.getChannelData(0)[i] = gain * monoBuffer.getChannelData(0)[i];
+ }
+
+ ac.startRendering().then(function(buffer) {
+ compareBuffers(buffer, monoBuffer);
+ finish();
+ });
+}
+
+function testStereo(type) {
+ var ac = new OfflineAudioContext(2, BUF_SIZE, 44100);
+
+ // A sine to be used to fill the buffers
+ function sine(t) {
+ return Math.sin(440 * 2 * Math.PI * t / ac.sampleRate);
+ }
+
+ var stereoBuffer = ac.createBuffer(2, BUF_SIZE, ac.sampleRate);
+ for (var i = 0; i < BUF_SIZE; ++i) {
+ stereoBuffer.getChannelData(0)[i] = sine(i);
+ stereoBuffer.getChannelData(1)[i] = sine(i);
+ }
+
+ var stereoSource = ac.createBufferSource();
+ stereoSource.buffer = stereoBuffer;
+ stereoSource.start(0);
+
+ var panner = ac.createPanner();
+ panner.distanceModel = "linear";
+ panner.refDistance = 1;
+ panner.positionX.value = 0;
+ panner.positionY.value = 0;
+ panner.positionZ.value = 0;
+ stereoSource.connect(panner);
+
+ var panner2 = ac.createPanner();
+ panner2.distanceModel = "inverse";
+ panner2.refDistance = 1;
+ panner2.positionX.value = 0;
+ panner2.positionY.value = 0;
+ panner2.positionZ.value = 0;
+ panner.connect(panner2);
+
+ var panner3 = ac.createPanner();
+ panner3.distanceModel = "exponential";
+ panner3.refDistance = 1;
+ panner3.positionX.value = 0;
+ panner3.positionY.value = 0;
+ panner3.positionZ.value = 0;
+ panner2.connect(panner3);
+
+ panner3.connect(ac.destination);
+
+ ac.startRendering().then(function(buffer) {
+ compareBuffers(buffer, stereoBuffer);
+ finish();
+ });
+}
+
+function test(type) {
+ testMono(type);
+ testStereo(type);
+}
+
+addLoadEvent(function() {
+ types.forEach(test);
+});
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_pannerNodeChannelCount.html b/dom/media/webaudio/test/test_pannerNodeChannelCount.html
new file mode 100644
index 0000000000..9cb90f32da
--- /dev/null
+++ b/dom/media/webaudio/test/test_pannerNodeChannelCount.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test PannerNode directly above</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script src="webaudio.js" type="text/javascript"></script>
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 2048,
+ numberOfChannels: 2,
+ createGraph(context) {
+ var buffer = context.createBuffer(2, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ var sample = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ // When mixed into a single channel, this produces silence
+ buffer.getChannelData(0)[i] = sample;
+ buffer.getChannelData(1)[i] = -sample;
+ }
+
+ var panner = context.createPanner();
+ panner.positionX.value = 1;
+ panner.positionY.value = 2;
+ panner.positionZ.value = 3;
+ panner.channelCount = 1;
+ expectException(function() { panner.channelCount = 3; },
+ DOMException.NOT_SUPPORTED_ERR);
+ panner.channelCountMode = "explicit";
+ expectException(function() { panner.channelCountMode = "max"; },
+ DOMException.NOT_SUPPORTED_ERR);
+ panner.channelInterpretation = "discrete";
+ panner.channelInterpretation = "speakers";
+
+ var source = context.createBufferSource();
+ source.buffer = buffer;
+ source.connect(panner);
+ source.start(0);
+
+ return panner;
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_pannerNodeHRTFSymmetry.html b/dom/media/webaudio/test/test_pannerNodeHRTFSymmetry.html
new file mode 100644
index 0000000000..abd03b3898
--- /dev/null
+++ b/dom/media/webaudio/test/test_pannerNodeHRTFSymmetry.html
@@ -0,0 +1,107 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test left/right symmetry and block-offset invariance of HRTF panner</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+const blockSize = 128;
+const bufferSize = 4096; // > HRTF panner latency
+
+var ctx = new AudioContext();
+
+function isChannelSilent(channel) {
+ for (var i = 0; i < channel.length; ++i) {
+ if (channel[i] != 0.0) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function startTest() {
+ var leftPanner = ctx.createPanner();
+ var rightPanner = ctx.createPanner();
+ leftPanner.panningModel = "HRTF";
+ rightPanner.panningModel = "HRTF";
+ leftPanner.positionX.value = -1;
+ rightPanner.positionX.value = 1;
+
+ // Test that PannerNode processes the signal consistently irrespective of
+ // the offset in the processing block. This is done by inserting a delay of
+ // less than a block size before one panner.
+ const delayTime = 0.7 * blockSize / ctx.sampleRate;
+ var leftDelay = ctx.createDelay(delayTime);
+ leftDelay.delayTime.value = delayTime;
+ leftDelay.connect(leftPanner);
+ // and compensating for the delay after the other.
+ var rightDelay = ctx.createDelay(delayTime);
+ rightDelay.delayTime.value = delayTime;
+ rightPanner.connect(rightDelay);
+
+ // Feed the panners with a signal having some harmonics to fill the spectrum.
+ var oscillator = ctx.createOscillator();
+ oscillator.frequency.value = 110;
+ oscillator.type = "sawtooth";
+ oscillator.connect(leftDelay);
+ oscillator.connect(rightPanner);
+ oscillator.start(0);
+
+ // Switch the channels on one panner output, and it should match the other.
+ var splitter = ctx.createChannelSplitter();
+ leftPanner.connect(splitter);
+ var merger = ctx.createChannelMerger();
+ splitter.connect(merger, 0, 1);
+ splitter.connect(merger, 1, 0);
+
+ // Invert one signal so that mixing with the other will find the difference.
+ var gain = ctx.createGain();
+ gain.gain.value = -1.0;
+ merger.connect(gain);
+
+ var processor = ctx.createScriptProcessor(bufferSize, 2, 0);
+ gain.connect(processor);
+ rightDelay.connect(processor);
+ processor.onaudioprocess =
+ function(e) {
+ compareBuffers(e.inputBuffer,
+ ctx.createBuffer(2, bufferSize, ctx.sampleRate));
+ e.target.onaudioprocess = null;
+ SimpleTest.finish();
+ }
+}
+
+function prepareTest() {
+ // A PannerNode will produce no output until it has loaded its HRIR
+ // database. Wait for this to load before starting the test.
+ var processor = ctx.createScriptProcessor(bufferSize, 2, 0);
+ var panner = ctx.createPanner();
+ panner.panningModel = "HRTF";
+ panner.connect(processor);
+ var oscillator = ctx.createOscillator();
+ oscillator.connect(panner);
+ oscillator.start(0);
+
+ processor.onaudioprocess =
+ function(e) {
+ if (isChannelSilent(e.inputBuffer.getChannelData(0)))
+ return;
+
+ oscillator.stop(0);
+ panner.disconnect();
+ e.target.onaudioprocess = null;
+ startTest();
+ };
+}
+prepareTest();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_pannerNodePassThrough.html b/dom/media/webaudio/test/test_pannerNodePassThrough.html
new file mode 100644
index 0000000000..d8c809a2e2
--- /dev/null
+++ b/dom/media/webaudio/test/test_pannerNodePassThrough.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test PannerNode with passthrough</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 2048,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var source = context.createBufferSource();
+
+ var panner = context.createPanner();
+
+ source.buffer = this.buffer;
+
+ source.connect(panner);
+
+ context.listener.setOrientation(0, 6.311749985202524e+307, 0, 0.1, 1000, 0);
+ context.listener.setOrientation(0, 0, -6.311749985202524e+307, 0, 0, 6.311749985202524e+307);
+ panner.positionX = 2;
+ panner.rolloffFactor = 0;
+ panner.panningModel = "equalpower";
+
+ var pannerWrapped = SpecialPowers.wrap(panner);
+ ok("passThrough" in pannerWrapped, "PannerNode should support the passThrough API");
+ pannerWrapped.passThrough = true;
+
+ source.start(0);
+ return panner;
+ },
+ createExpectedBuffers(context) {
+ this.buffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ this.buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ }
+
+ return [this.buffer];
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_pannerNodeTail.html b/dom/media/webaudio/test/test_pannerNodeTail.html
new file mode 100644
index 0000000000..1f6483b581
--- /dev/null
+++ b/dom/media/webaudio/test/test_pannerNodeTail.html
@@ -0,0 +1,232 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test tail time lifetime of PannerNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// This tests that a PannerNode does not release its reference before
+// it finishes emitting sound.
+//
+// The PannerNode tail time is short, so, when a PannerNode is destroyed on
+// the main thread, it is unlikely to notify the graph thread before the tail
+// time expires. However, by adding DelayNodes downstream from the
+// PannerNodes, the graph thread can have enough time to notice that a
+// DelayNode has been destroyed.
+//
+// In the current implementation, DelayNodes will take a tail-time reference
+// immediately when they receive the first block of sound from an upstream
+// node, so this test connects the downstream DelayNodes while the upstream
+// nodes are finishing, and then runs GC (on the main thread) before the
+// DelayNodes receive any input (on the graph thread).
+//
+// Web Audio doesn't provide a means to precisely time connect()s but we can
+// test that the output of delay nodes matches the output from a reference
+// PannerNode that we know will not be GCed.
+//
+// Another set of delay nodes is added upstream to ensure that the source node
+// has removed its self-reference after dispatching its "ended" event.
+
+SimpleTest.waitForExplicitFinish();
+
+const blockSize = 128;
+// bufferSize should be long enough that to allow an audioprocess event to be
+// sent to the main thread and a connect message to return to the graph
+// thread.
+const bufferSize = 4096;
+const pannerCount = bufferSize / blockSize;
+// sourceDelayBufferCount should be long enough to allow the source node
+// onended to finish and remove the source self-reference.
+const sourceDelayBufferCount = 3;
+var gotEnded = false;
+// ccDelayLength should be long enough to allow CC to run
+var ccDelayBufferCount = 20;
+const ccDelayLength = ccDelayBufferCount * bufferSize;
+
+var ctx;
+var testPanners = [];
+var referencePanner;
+var referenceProcessCount = 0;
+var referenceOutput = [new Float32Array(bufferSize),
+ new Float32Array(bufferSize)];
+var testProcessor;
+var testProcessCount = 0;
+
+function isChannelSilent(channel) {
+ for (var i = 0; i < channel.length; ++i) {
+ if (channel[i] != 0.0) {
+ return false;
+ }
+ }
+ return true;
+}
+
+function onReferenceOutput(e) {
+ switch(referenceProcessCount) {
+
+ case sourceDelayBufferCount - 1:
+ // The panners are about to finish.
+ if (!gotEnded) {
+ todo(false, "Source hasn't ended. Increase sourceDelayBufferCount?");
+ }
+
+ // Connect each PannerNode output to a downstream DelayNode,
+ // and connect ScriptProcessors to compare test and reference panners.
+ var delayDuration = ccDelayLength / ctx.sampleRate;
+ for (var i = 0; i < pannerCount; ++i) {
+ var delay = ctx.createDelay(delayDuration);
+ delay.delayTime.value = delayDuration;
+ delay.connect(testProcessor);
+ testPanners[i].connect(delay);
+ }
+ testProcessor = null;
+ testPanners = null;
+
+ // The panning effect is linear so only one reference panner is required.
+ // This also checks that the individual panners don't chop their output
+ // too soon.
+ referencePanner.connect(e.target);
+
+ // Assuming the above operations have already scheduled an event to run in
+ // stable state and ask the graph thread to make connections, schedule a
+ // subsequent event to run cycle collection, which should not collect
+ // panners that are still producing sound.
+ SimpleTest.executeSoon(function() {
+ SpecialPowers.forceGC();
+ SpecialPowers.forceCC();
+ });
+
+ break;
+
+ case sourceDelayBufferCount:
+ // Record this buffer during which PannerNode outputs were connected.
+ for (var i = 0; i < 2; ++i) {
+ e.inputBuffer.copyFromChannel(referenceOutput[i], i);
+ }
+ e.target.onaudioprocess = null;
+ e.target.disconnect();
+
+ // If the buffer is silent, there is probably not much point just
+ // increasing the buffer size, because, with the buffer size already
+ // significantly larger than panner tail time, it demonstrates that the
+ // lag between threads is much greater than the tail time.
+ if (isChannelSilent(referenceOutput[0])) {
+ todo(false, "Connections not detected.");
+ }
+ }
+
+ referenceProcessCount++;
+}
+
+function onTestOutput(e) {
+ if (testProcessCount < sourceDelayBufferCount + ccDelayBufferCount) {
+ testProcessCount++;
+ return;
+ }
+
+ for (var i = 0; i < 2; ++i) {
+ compareChannels(e.inputBuffer.getChannelData(i), referenceOutput[i]);
+ }
+ e.target.onaudioprocess = null;
+ e.target.disconnect();
+ SimpleTest.finish();
+}
+
+function startTest() {
+ // 0.002 is MaxDelayTimeSeconds in HRTFpanner.cpp
+ // and 512 is fftSize() at 48 kHz.
+ const expectedPannerTailTime = 0.002 * ctx.sampleRate + 512;
+
+ // Create some PannerNodes downstream from DelayNodes with delays long
+ // enough for their source to finish, dispatch its "ended" event
+ // and release its playing reference. The DelayNodes should expire their
+ // tail-time references before the PannerNodes and so only the PannerNode
+ // lifetimes depends on their tail-time references. Many DelayNodes are
+ // created and timed to finish at different times so that one PannerNode
+ // will be finishing the block processed immediately after the connect is
+ // received.
+ var source = ctx.createBufferSource();
+ // Just short of blockSize here to avoid rounding into the next block
+ var buffer = ctx.createBuffer(1, blockSize - 1, ctx.sampleRate);
+ for (var i = 0; i < buffer.length; ++i) {
+ buffer.getChannelData(0)[i] = Math.cos(Math.PI * i / buffer.length);
+ }
+ source.buffer = buffer;
+ source.start(0);
+ source.onended = function(e) {
+ gotEnded = true;
+ };
+
+ // Time the first test panner to finish just before downstream DelayNodes
+ // are about the be connected. Note that DelayNode lifetime depends on
+ // maxDelayTime so set that equal to the delay.
+ var delayDuration =
+ (sourceDelayBufferCount * bufferSize
+ - expectedPannerTailTime - 2 * blockSize) / ctx.sampleRate;
+
+ for (var i = 0; i < pannerCount; ++i) {
+ var delay = ctx.createDelay(delayDuration);
+ delay.delayTime.value = delayDuration;
+ source.connect(delay);
+ delay.connect(referencePanner)
+
+ var panner = ctx.createPanner();
+ panner.panningModel = "HRTF";
+ delay.connect(panner);
+ testPanners[i] = panner;
+
+ delayDuration += blockSize / ctx.sampleRate;
+ }
+
+ // Create a ScriptProcessor now to use as a timer to trigger connection of
+ // downstream nodes. It will also be used to record reference output.
+ var referenceProcessor = ctx.createScriptProcessor(bufferSize, 2, 0);
+ referenceProcessor.onaudioprocess = onReferenceOutput;
+ // Start audioprocess events before source delays are connected.
+ referenceProcessor.connect(ctx.destination);
+
+ // The test ScriptProcessor will record output of testPanners.
+ // Create it now so that it is synchronized with the referenceProcessor.
+ testProcessor = ctx.createScriptProcessor(bufferSize, 2, 0);
+ testProcessor.onaudioprocess = onTestOutput;
+ // Start audioprocess events before source delays are connected.
+ testProcessor.connect(ctx.destination);
+}
+
+function prepareTest() {
+ ctx = new AudioContext();
+ // Place the listener to the side of the origin, where the panners are
+ // positioned, to maximize delay in one ear.
+ ctx.listener.setPosition(1,0,0);
+
+ // A PannerNode will produce no output until it has loaded its HRIR
+ // database. Wait for this to load before starting the test.
+ var processor = ctx.createScriptProcessor(bufferSize, 2, 0);
+ referencePanner = ctx.createPanner();
+ referencePanner.panningModel = "HRTF";
+ referencePanner.connect(processor);
+ var oscillator = ctx.createOscillator();
+ oscillator.connect(referencePanner);
+ oscillator.start(0);
+
+ processor.onaudioprocess = function(e) {
+ if (isChannelSilent(e.inputBuffer.getChannelData(0)))
+ return;
+
+ oscillator.stop(0);
+ oscillator.disconnect();
+ referencePanner.disconnect();
+ e.target.onaudioprocess = null;
+ SimpleTest.executeSoon(startTest);
+ };
+}
+prepareTest();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_pannerNode_audioparam_distance.html b/dom/media/webaudio/test/test_pannerNode_audioparam_distance.html
new file mode 100644
index 0000000000..2d955de19d
--- /dev/null
+++ b/dom/media/webaudio/test/test_pannerNode_audioparam_distance.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Distance effect of a PannerNode with the position set via AudioParams (Bug 1472550)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+ SimpleTest.waitForExplicitFinish();
+ var o = new OfflineAudioContext(2, 256, 44100);
+
+ // We want a stereo constant source.
+ var b = o.createBuffer(2, 1, 44100);
+ b.getChannelData(0)[0] = 1;
+ b.getChannelData(1)[0] = 1;
+ var c = o.createBufferSource();
+ c.buffer = b;
+ c.loop = true;
+
+ var p = o.createPanner();
+ p.positionY.setValueAtTime(1, 0);
+ p.positionX.setValueAtTime(1, 0);
+ p.positionZ.setValueAtTime(1, 0);
+
+ // Set the listener somewhere far
+ o.listener.setPosition(20, 2, 20);
+
+ c.start();
+ c.connect(p).connect(o.destination);
+
+ o.startRendering().then((ab) => {
+ // Check that the distance attenuates the sound.
+ ok(ab.getChannelData(0)[0] < 0.1, "left channel must be very quiet");
+ ok(ab.getChannelData(1)[0] < 0.1, "right channel must be very quiet");
+ SimpleTest.finish();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_pannerNode_equalPower.html b/dom/media/webaudio/test/test_pannerNode_equalPower.html
new file mode 100644
index 0000000000..127a87b254
--- /dev/null
+++ b/dom/media/webaudio/test/test_pannerNode_equalPower.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<title>Test PannerNode</title>
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+<script type="text/javascript" src="webaudio.js"></script>
+<script type="text/javascript" src="layouttest-glue.js"></script>
+<script type="text/javascript" src="blink/audio-testing.js"></script>
+<script type="text/javascript" src="blink/panner-model-testing.js"></script>
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ function checkFinished() {
+ SimpleTest.finish();
+ }
+ var ctx = new OfflineAudioContext(2, sampleRate * renderLengthSeconds, sampleRate);
+ createTestAndRun(ctx, nodesToCreate, 2, checkFinished);
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_pannerNode_maxDistance.html b/dom/media/webaudio/test/test_pannerNode_maxDistance.html
new file mode 100644
index 0000000000..b5286e56e1
--- /dev/null
+++ b/dom/media/webaudio/test/test_pannerNode_maxDistance.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test PannerNode outputs silence when the distance is greater than maxDist</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var types = [
+ "equalpower",
+ "HRTF"
+]
+
+var finished = types.length;
+
+function finish() {
+ if (!--finished) {
+ SimpleTest.finish();
+ }
+}
+
+function test(type) {
+ var ac = new OfflineAudioContext(1, 128, 44100);
+ var osc = ac.createOscillator();
+ var panner = ac.createPanner();
+
+ panner.distanceModel = "linear";
+ panner.maxDistance = 100;
+ panner.positionY.value = 200;
+ ac.listener.setPosition(0, 0, 0);
+
+ osc.connect(panner);
+ panner.connect(ac.destination);
+
+ osc.start();
+
+ ac.startRendering().then(function(buffer) {
+ var silence = true;
+ var array = buffer.getChannelData(0);
+ for (var i = 0; i < buffer.length; i++) {
+ if (array[i] != 0) {
+ ok(false, "Found noise in the buffer.");
+ silence = false;
+ }
+ }
+ ok(silence, "The buffer is silent.");
+ finish();
+ });
+}
+
+
+addLoadEvent(function() {
+ types.forEach(test);
+});
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_periodicWave.html b/dom/media/webaudio/test/test_periodicWave.html
new file mode 100644
index 0000000000..7b8a6ab12c
--- /dev/null
+++ b/dom/media/webaudio/test/test_periodicWave.html
@@ -0,0 +1,130 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test the PeriodicWave interface</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// real and imag are used in separate PeriodicWaves to make their peak values
+// easy to determine.
+const realMax = 99;
+var real = new Float32Array(realMax + 1);
+real[1] = 2.0; // fundamental
+real[realMax] = 3.0;
+const realPeak = real[1] + real[realMax];
+const realFundamental = 19.0;
+var imag = new Float32Array(4);
+imag[0] = 6.0; // should be ignored.
+imag[3] = 0.5;
+const imagPeak = imag[3];
+const imagFundamental = 551.0;
+
+const testLength = 4096;
+
+addLoadEvent(function() {
+ var ac = new AudioContext();
+ ac.createPeriodicWave(new Float32Array(4096), new Float32Array(4096));
+ expectException(function() {
+ ac.createPeriodicWave(new Float32Array(512), imag);
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ ac.createPeriodicWave(new Float32Array(0), new Float32Array(0));
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ ac.createPeriodicWave(new Float32Array(1), new Float32Array(1));
+ }, DOMException.INDEX_SIZE_ERR);
+ expectNoException(function() {
+ ac.createPeriodicWave(new Float32Array(4097), new Float32Array(4097));
+ });
+
+ expectNoException(function() {
+ new PeriodicWave(ac, {});
+ });
+
+ // real.size == imag.size
+ expectException(function() {
+ new PeriodicWave(ac, {real: new Float32Array(10), imag: new Float32Array(9)});
+ }, DOMException.INDEX_SIZE_ERR);
+
+ // size lower than 2 is not allowed
+ expectException(function() {
+ new PeriodicWave(ac, {real: new Float32Array(0)});
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ new PeriodicWave(ac, {imag: new Float32Array(0)});
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ new PeriodicWave(ac, {real: new Float32Array(1)});
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ new PeriodicWave(ac, {imag: new Float32Array(1)});
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ new PeriodicWave(ac, {real: new Float32Array(0), imag: new Float32Array(0)});
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ new PeriodicWave(ac, {real: new Float32Array(1), imag: new Float32Array(1)});
+ }, DOMException.INDEX_SIZE_ERR);
+
+ new PeriodicWave(ac, {real: new Float32Array(4096), imag: new Float32Array(4096)});
+ new PeriodicWave(ac, {real: new Float32Array(4096) });
+ new PeriodicWave(ac, {imag: new Float32Array(4096) });
+
+ runTest();
+});
+
+var gTest = {
+ createGraph(context) {
+ var merger = context.createChannelMerger();
+
+ var osc0 = context.createOscillator();
+ var osc1 = context.createOscillator();
+
+ osc0.setPeriodicWave(context.
+ createPeriodicWave(real,
+ new Float32Array(real.length)));
+ osc1.setPeriodicWave(context.
+ createPeriodicWave(new Float32Array(imag.length),
+ imag));
+
+ osc0.frequency.value = realFundamental;
+ osc1.frequency.value = imagFundamental;
+
+ osc0.start();
+ osc1.start();
+
+ osc0.connect(merger, 0, 0);
+ osc1.connect(merger, 0, 1);
+
+ return merger;
+ },
+ createExpectedBuffers(context) {
+ var buffer = context.createBuffer(2, testLength, context.sampleRate);
+
+ for (var i = 0; i < buffer.length; ++i) {
+
+ buffer.getChannelData(0)[i] = 1.0 / realPeak *
+ (real[1] * Math.cos(2 * Math.PI * realFundamental * i /
+ context.sampleRate) +
+ real[realMax] * Math.cos(2 * Math.PI * realMax * realFundamental * i /
+ context.sampleRate));
+
+ buffer.getChannelData(1)[i] = 1.0 / imagPeak *
+ imag[3] * Math.sin(2 * Math.PI * 3 * imagFundamental * i /
+ context.sampleRate);
+ }
+ return buffer;
+ },
+};
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_periodicWaveBandLimiting.html b/dom/media/webaudio/test/test_periodicWaveBandLimiting.html
new file mode 100644
index 0000000000..70fbb09e2a
--- /dev/null
+++ b/dom/media/webaudio/test/test_periodicWaveBandLimiting.html
@@ -0,0 +1,86 @@
+<!DOCTYPE html>
+<title>Test effect of band limiting on PeriodicWave signals</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+const sampleRate = 48000;
+const bufferSize = 12800;
+const epsilon = 0.01;
+
+// "All implementations must support arrays up to at least 8192", but the
+// linear interpolation of the current implementation distorts the higher
+// frequency components too much to pass this test.
+const frequencyIndexMax = 200;
+
+// A set of oscillators are created near the Nyquist frequency.
+// These are factors giving each oscillator frequency relative to the Nyquist.
+// The first is an octave below Nyquist and the last is just above.
+const OCTAVE_BELOW = 0;
+const HALF_BELOW = 1;
+const NEAR_BELOW = 2;
+const ABOVE = 3;
+const oscillatorFactors = [0.5, Math.sqrt(0.5), 0.99, 1.01];
+const oscillatorCount = oscillatorFactors.length;
+
+// Return magnitude relative to unit sine wave
+function magnitude(array) {
+ var mag = 0
+ for (var i = 0; i < array.length; ++i) {
+ sample = array[i];
+ mag += sample * sample;
+ }
+ return Math.sqrt(2 * mag / array.length);
+}
+
+function test_frequency_index(frequencyIndex) {
+
+ var context =
+ new OfflineAudioContext(oscillatorCount, bufferSize, sampleRate);
+
+ var merger = context.createChannelMerger(oscillatorCount);
+ merger.connect(context.destination);
+
+ var real = new Float32Array(frequencyIndex + 1);
+ real[frequencyIndex] = 1;
+ var image = new Float32Array(real.length);
+ var wave = context.createPeriodicWave(real, image);
+
+ for (var i = 0; i < oscillatorCount; ++i) {
+ var oscillator = context.createOscillator();
+ oscillator.frequency.value =
+ oscillatorFactors[i] * sampleRate / (2 * frequencyIndex);
+ oscillator.connect(merger, 0, i);
+ oscillator.setPeriodicWave(wave);
+ oscillator.start(0);
+ }
+
+ return context.startRendering().
+ then((buffer) => {
+ assert_equals(buffer.numberOfChannels, oscillatorCount);
+ var magnitudes = [];
+ for (var i = 0; i < oscillatorCount; ++i) {
+ magnitudes[i] = magnitude(buffer.getChannelData(i));
+ }
+ // Unaffected by band-limiting one octave below Nyquist.
+ assert_approx_equals(magnitudes[OCTAVE_BELOW], 1, epsilon,
+ "magnitude with frequency octave below Nyquist");
+ // Still at least half the amplitude at half octave below Nyquist.
+ assert_greater_than(magnitudes[HALF_BELOW], 0.5 * (1 - epsilon),
+ "magnitude with frequency half octave below Nyquist");
+ // Approaching zero or zero near Nyquist.
+ assert_less_than(magnitudes[NEAR_BELOW], 0.1,
+ "magnitude with frequency near Nyquist");
+ assert_equals(magnitudes[ABOVE], 0,
+ "magnitude with frequency above Nyquist");
+ });
+}
+
+// The 5/4 ratio with rounding up provides sampling across a range of
+// octaves and offsets within octaves.
+for (var frequencyIndex = 1;
+ frequencyIndex < frequencyIndexMax;
+ frequencyIndex = Math.floor((5 * frequencyIndex + 3) / 4)) {
+ promise_test(test_frequency_index.bind(null, frequencyIndex),
+ "Frequency " + frequencyIndex);
+}
+</script>
diff --git a/dom/media/webaudio/test/test_periodicWaveDisableNormalization.html b/dom/media/webaudio/test/test_periodicWaveDisableNormalization.html
new file mode 100644
index 0000000000..229d48282e
--- /dev/null
+++ b/dom/media/webaudio/test/test_periodicWaveDisableNormalization.html
@@ -0,0 +1,98 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test PeriodicWave disableNormalization Parameter</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// We create PerodicWave instances containing two tones and compare it to
+// buffers created directly in JavaScript by adding the two waves together.
+// Two of the PeriodicWaves are normalized, the other is not. This test is
+// a modification of test_periodicWave.html.
+//
+// These constants are borrowed from test_periodicWave.html and modified
+// so that the realPeak (which is the normalization factor) will be small
+// enough that the errors are within the bounds for the test.
+const realMax = 99;
+var real = new Float32Array(realMax + 1);
+real[1] = 2.0; // fundamental
+real[realMax] = 0.25;
+
+const realPeak = real[1] + real[realMax];
+const realFundamental = 19.0;
+
+const testLength = 4096;
+
+addLoadEvent(function() {
+ runTest();
+});
+
+var gTest = {
+ createGraph(context) {
+ var merger = context.createChannelMerger();
+
+ var osc0 = context.createOscillator();
+ var osc1 = context.createOscillator();
+ var osc2 = context.createOscillator();
+
+ osc0.setPeriodicWave(context.
+ createPeriodicWave(real,
+ new Float32Array(real.length),
+ {disableNormalization: false}));
+ osc1.setPeriodicWave(context.
+ createPeriodicWave(real,
+ new Float32Array(real.length)));
+ osc2.setPeriodicWave(context.
+ createPeriodicWave(real,
+ new Float32Array(real.length),
+ {disableNormalization: true}));
+
+ osc0.frequency.value = realFundamental;
+ osc1.frequency.value = realFundamental;
+ osc2.frequency.value = realFundamental;
+
+ osc0.start();
+ osc1.start();
+ osc2.start();
+
+ osc0.connect(merger, 0, 0);
+ osc1.connect(merger, 0, 1);
+ osc2.connect(merger, 0, 2);
+
+ return merger;
+ },
+ createExpectedBuffers(context) {
+ var buffer = context.createBuffer(3, testLength, context.sampleRate);
+
+ for (var i = 0; i < buffer.length; ++i) {
+
+ buffer.getChannelData(0)[i] = 1.0 / realPeak *
+ (real[1] * Math.cos(2 * Math.PI * realFundamental * i /
+ context.sampleRate) +
+ real[realMax] * Math.cos(2 * Math.PI * realMax * realFundamental * i /
+ context.sampleRate));
+
+ buffer.getChannelData(1)[i] = buffer.getChannelData(0)[i];
+
+ buffer.getChannelData(2)[i] =
+ (real[1] * Math.cos(2 * Math.PI * realFundamental * i /
+ context.sampleRate) +
+ real[realMax] * Math.cos(2 * Math.PI * realMax * realFundamental * i /
+ context.sampleRate));
+ }
+ return buffer;
+ },
+ 'numberOfChannels': 3,
+};
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_retrospective-exponentialRampToValueAtTime.html b/dom/media/webaudio/test/test_retrospective-exponentialRampToValueAtTime.html
new file mode 100644
index 0000000000..20d3d59faf
--- /dev/null
+++ b/dom/media/webaudio/test/test_retrospective-exponentialRampToValueAtTime.html
@@ -0,0 +1,51 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Test exponentialRampToValue with end time in the past</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+function do_test(t, context) {
+ var source = context.createConstantSource();
+ source.start();
+
+ var test = context.createGain();
+ test.gain.exponentialRampToValueAtTime(0.1, 0.5*context.currentTime);
+ test.gain.exponentialRampToValueAtTime(0.9, 2.0);
+
+ var reference = context.createGain();
+ reference.gain.exponentialRampToValueAtTime(0.1, context.currentTime);
+ reference.gain.exponentialRampToValueAtTime(0.9, 2.0);
+
+ source.connect(test);
+ source.connect(reference);
+
+ var merger = context.createChannelMerger();
+ test.connect(merger, 0, 0);
+ reference.connect(merger, 0, 1);
+
+ var processor = context.createScriptProcessor(0, 2, 0);
+ merger.connect(processor);
+ processor.onaudioprocess =
+ t.step_func_done((e) => {
+ source.stop();
+ processor.onaudioprocess = null;
+
+ var testValue = e.inputBuffer.getChannelData(0)[0];
+ var referenceValue = e.inputBuffer.getChannelData(1)[0];
+
+ assert_equals(testValue, referenceValue,
+ "value matches expected");
+ });
+}
+
+async_test(function(t) {
+ var context = new AudioContext;
+ (function waitForTimeAdvance() {
+ if (context.currentTime == 0) {
+ t.step_timeout(waitForTimeAdvance, 0);
+ } else {
+ do_test(t, context);
+ }
+ })();
+});
+</script>
diff --git a/dom/media/webaudio/test/test_retrospective-linearRampToValueAtTime.html b/dom/media/webaudio/test/test_retrospective-linearRampToValueAtTime.html
new file mode 100644
index 0000000000..1594a30bd1
--- /dev/null
+++ b/dom/media/webaudio/test/test_retrospective-linearRampToValueAtTime.html
@@ -0,0 +1,51 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Test linearRampToValue with end time in the past</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+function do_test(t, context) {
+ var source = context.createConstantSource();
+ source.start();
+
+ var test = context.createGain();
+ test.gain.linearRampToValueAtTime(0.1, 0.5*context.currentTime);
+ test.gain.linearRampToValueAtTime(0.9, 2.0);
+
+ var reference = context.createGain();
+ reference.gain.linearRampToValueAtTime(0.1, context.currentTime);
+ reference.gain.linearRampToValueAtTime(0.9, 2.0);
+
+ source.connect(test);
+ source.connect(reference);
+
+ var merger = context.createChannelMerger();
+ test.connect(merger, 0, 0);
+ reference.connect(merger, 0, 1);
+
+ var processor = context.createScriptProcessor(0, 2, 0);
+ merger.connect(processor);
+ processor.onaudioprocess =
+ t.step_func_done((e) => {
+ source.stop();
+ processor.onaudioprocess = null;
+
+ var testValue = e.inputBuffer.getChannelData(0)[0];
+ var referenceValue = e.inputBuffer.getChannelData(1)[0];
+
+ assert_equals(testValue, referenceValue,
+ "value matches expected");
+ });
+}
+
+async_test(function(t) {
+ var context = new AudioContext;
+ (function waitForTimeAdvance() {
+ if (context.currentTime == 0) {
+ t.step_timeout(waitForTimeAdvance, 0);
+ } else {
+ do_test(t, context);
+ }
+ })();
+});
+</script>
diff --git a/dom/media/webaudio/test/test_retrospective-setTargetAtTime.html b/dom/media/webaudio/test/test_retrospective-setTargetAtTime.html
new file mode 100644
index 0000000000..9b04fe22bb
--- /dev/null
+++ b/dom/media/webaudio/test/test_retrospective-setTargetAtTime.html
@@ -0,0 +1,51 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Test setTargetAtTime with start time in the past</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+function do_test(t, context) {
+ var source = context.createConstantSource();
+ source.start();
+
+ var test = context.createGain();
+ test.gain.setTargetAtTime(0.1, 0.5*context.currentTime, 0.1);
+ test.gain.linearRampToValueAtTime(0.9, 2.0);
+
+ var reference = context.createGain();
+ reference.gain.setTargetAtTime(0.1, context.currentTime, 0.1);
+ reference.gain.linearRampToValueAtTime(0.9, 2.0);
+
+ source.connect(test);
+ source.connect(reference);
+
+ var merger = context.createChannelMerger();
+ test.connect(merger, 0, 0);
+ reference.connect(merger, 0, 1);
+
+ var processor = context.createScriptProcessor(0, 2, 0);
+ merger.connect(processor);
+ processor.onaudioprocess =
+ t.step_func_done((e) => {
+ source.stop();
+ processor.onaudioprocess = null;
+
+ var testValue = e.inputBuffer.getChannelData(0)[0];
+ var referenceValue = e.inputBuffer.getChannelData(1)[0];
+
+ assert_equals(testValue, referenceValue,
+ "value matches expected");
+ });
+}
+
+async_test(function(t) {
+ var context = new AudioContext;
+ (function waitForTimeAdvance() {
+ if (context.currentTime == 0) {
+ t.step_timeout(waitForTimeAdvance, 0);
+ } else {
+ do_test(t, context);
+ }
+ })();
+});
+</script>
diff --git a/dom/media/webaudio/test/test_retrospective-setValueAtTime.html b/dom/media/webaudio/test/test_retrospective-setValueAtTime.html
new file mode 100644
index 0000000000..b9657ef211
--- /dev/null
+++ b/dom/media/webaudio/test/test_retrospective-setValueAtTime.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<title>Test setValueAtTime with startTime in the past</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+function do_test(t, context) {
+ var source = context.createConstantSource();
+ source.start();
+
+ // Use a ramp of slope 1/sample to measure time.
+ // The end value is the extent of exact precision in single precision float.
+ const rampEnd = Math.pow(2, 24);
+ const rampEndSeconds = rampEnd / context.sampleRate;
+ var test = context.createGain();
+ test.gain.setValueAtTime(0.0, 0.5*context.currentTime);
+ test.gain.linearRampToValueAtTime(rampEnd, rampEndSeconds);
+
+ var reference = context.createGain();
+ reference.gain.setValueAtTime(0.0, context.currentTime);
+ reference.gain.linearRampToValueAtTime(rampEnd, rampEndSeconds);
+
+ source.connect(test);
+ source.connect(reference);
+
+ var merger = context.createChannelMerger();
+ test.connect(merger, 0, 0);
+ reference.connect(merger, 0, 1);
+
+ var processor = context.createScriptProcessor(0, 2, 0);
+ merger.connect(processor);
+ processor.onaudioprocess =
+ t.step_func_done((e) => {
+ source.stop();
+ processor.onaudioprocess = null;
+
+ var testValue = e.inputBuffer.getChannelData(0)[0];
+ var referenceValue = e.inputBuffer.getChannelData(1)[0];
+
+ assert_equals(testValue, referenceValue,
+ "ramp value matches expected");
+ });
+}
+
+async_test(function(t) {
+ var context = new AudioContext;
+ (function waitForTimeAdvance() {
+ if (context.currentTime == 0) {
+ t.step_timeout(waitForTimeAdvance, 0);
+ } else {
+ do_test(t, context);
+ }
+ })();
+});
+</script>
diff --git a/dom/media/webaudio/test/test_retrospective-setValueCurveAtTime.html b/dom/media/webaudio/test/test_retrospective-setValueCurveAtTime.html
new file mode 100644
index 0000000000..008b240129
--- /dev/null
+++ b/dom/media/webaudio/test/test_retrospective-setValueCurveAtTime.html
@@ -0,0 +1,49 @@
+<!doctype html>
+<meta charset=utf-8>
+<title>Test SetValueCurve with start time in the past</title>
+<script src=/resources/testharness.js></script>
+<script src=/resources/testharnessreport.js></script>
+<script>
+function do_test(t, context) {
+ var source = context.createConstantSource();
+ source.start();
+
+ var test = context.createGain();
+ test.gain.setValueCurveAtTime(new Float32Array([1.0, 0.1]), 0.0, 1.0);
+
+ var reference = context.createGain();
+ reference.gain.setValueCurveAtTime(new Float32Array([1.0, 0.1]), 0.5*context.currentTime, 1.0);
+
+ source.connect(test);
+ source.connect(reference);
+
+ var merger = context.createChannelMerger();
+ test.connect(merger, 0, 0);
+ reference.connect(merger, 0, 1);
+
+ var processor = context.createScriptProcessor(0, 2, 0);
+ merger.connect(processor);
+ processor.onaudioprocess =
+ t.step_func_done((e) => {
+ source.stop();
+ processor.onaudioprocess = null;
+
+ var testValue = e.inputBuffer.getChannelData(0)[0];
+ var referenceValue = e.inputBuffer.getChannelData(1)[0];
+
+ assert_equals(testValue, referenceValue,
+ "value matches expected");
+ });
+}
+
+async_test(function(t) {
+ var context = new AudioContext;
+ (function waitForTimeAdvance() {
+ if (context.currentTime == 0) {
+ t.step_timeout(waitForTimeAdvance, 0);
+ } else {
+ do_test(t, context);
+ }
+ })();
+});
+</script>
diff --git a/dom/media/webaudio/test/test_scriptProcessorNode.html b/dom/media/webaudio/test/test_scriptProcessorNode.html
new file mode 100644
index 0000000000..ec263755cb
--- /dev/null
+++ b/dom/media/webaudio/test/test_scriptProcessorNode.html
@@ -0,0 +1,132 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test ScriptProcessorNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// We do not use our generic graph test framework here because
+// the testing logic here is sort of complicated, and would
+// not be easy to map to OfflineAudioContext, as ScriptProcessorNodes
+// can experience delays.
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var context = new AudioContext();
+ var buffer = null;
+
+ var sourceSP = context.createScriptProcessor(2048);
+ sourceSP.addEventListener("audioprocess", function(e) {
+ // generate the audio
+ for (var i = 0; i < 2048; ++i) {
+ // Make sure our first sample won't be zero
+ e.outputBuffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * (i + 1) / context.sampleRate);
+ e.outputBuffer.getChannelData(1)[i] = Math.sin(880 * 2 * Math.PI * (i + 1) / context.sampleRate);
+ }
+ // Remember our generated audio
+ buffer = e.outputBuffer;
+
+ sourceSP.removeEventListener("audioprocess", arguments.callee);
+ });
+
+ expectException(function() {
+ context.createScriptProcessor(1);
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ context.createScriptProcessor(2);
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ context.createScriptProcessor(128);
+ }, DOMException.INDEX_SIZE_ERR);
+ expectException(function() {
+ context.createScriptProcessor(255);
+ }, DOMException.INDEX_SIZE_ERR);
+
+ is(sourceSP.channelCount, 2, "script processor node has 2 input channels by default");
+ is(sourceSP.channelCountMode, "explicit", "Correct channelCountMode for the script processor node");
+ is(sourceSP.channelInterpretation, "speakers", "Correct channelCountInterpretation for the script processor node");
+
+ function findFirstNonZeroSample(buffer) {
+ for (var i = 0; i < buffer.length; ++i) {
+ if (buffer.getChannelData(0)[i] != 0) {
+ return i;
+ }
+ }
+ return buffer.length;
+ }
+
+ var sp = context.createScriptProcessor(2048);
+ sourceSP.connect(sp);
+ sp.connect(context.destination);
+ var lastPlaybackTime = 0;
+
+ var emptyBuffer = context.createBuffer(1, 2048, context.sampleRate);
+
+ function checkAudioProcessingEvent(e) {
+ is(e.target, sp, "Correct event target");
+ ok(e.playbackTime > lastPlaybackTime, "playbackTime correctly set");
+ lastPlaybackTime = e.playbackTime;
+ is(e.inputBuffer.numberOfChannels, 2, "Correct number of channels for the input buffer");
+ is(e.inputBuffer.length, 2048, "Correct length for the input buffer");
+ is(e.inputBuffer.sampleRate, context.sampleRate, "Correct sample rate for the input buffer");
+ is(e.outputBuffer.numberOfChannels, 2, "Correct number of channels for the output buffer");
+ is(e.outputBuffer.length, 2048, "Correct length for the output buffer");
+ is(e.outputBuffer.sampleRate, context.sampleRate, "Correct sample rate for the output buffer");
+
+ compareChannels(e.outputBuffer.getChannelData(0), emptyBuffer.getChannelData(0));
+ compareChannels(e.outputBuffer.getChannelData(1), emptyBuffer.getChannelData(0));
+ }
+
+ sp.onaudioprocess = function(e) {
+ isnot(buffer, null, "The audioprocess handler for sourceSP must be run at this point");
+ checkAudioProcessingEvent(e);
+
+ // Because of the initial latency added by the second script processor node,
+ // we will never see any generated audio frames in the first callback.
+ compareChannels(e.inputBuffer.getChannelData(0), emptyBuffer.getChannelData(0));
+ compareChannels(e.inputBuffer.getChannelData(1), emptyBuffer.getChannelData(0));
+
+ sp.onaudioprocess = function(e) {
+ checkAudioProcessingEvent(e);
+
+ var firstNonZero = findFirstNonZeroSample(e.inputBuffer);
+ ok(firstNonZero <= 2048, "First non-zero sample within range");
+
+ compareChannels(e.inputBuffer.getChannelData(0), emptyBuffer.getChannelData(0), firstNonZero);
+ compareChannels(e.inputBuffer.getChannelData(1), emptyBuffer.getChannelData(0), firstNonZero);
+ compareChannels(e.inputBuffer.getChannelData(0), buffer.getChannelData(0), 2048 - firstNonZero, firstNonZero, 0);
+ compareChannels(e.inputBuffer.getChannelData(1), buffer.getChannelData(1), 2048 - firstNonZero, firstNonZero, 0);
+
+ if (firstNonZero == 0) {
+ // If we did not experience any delays, the test is done!
+ sp.onaudioprocess = null;
+
+ SimpleTest.finish();
+ } else if (firstNonZero != 2048) {
+ // In case we just saw a zero buffer this time, wait one more round
+ sp.onaudioprocess = function(e) {
+ checkAudioProcessingEvent(e);
+
+ compareChannels(e.inputBuffer.getChannelData(0), buffer.getChannelData(0), firstNonZero, 0, 2048 - firstNonZero);
+ compareChannels(e.inputBuffer.getChannelData(1), buffer.getChannelData(1), firstNonZero, 0, 2048 - firstNonZero);
+ compareChannels(e.inputBuffer.getChannelData(0), emptyBuffer.getChannelData(0), undefined, firstNonZero);
+ compareChannels(e.inputBuffer.getChannelData(1), emptyBuffer.getChannelData(0), undefined, firstNonZero);
+
+ sp.onaudioprocess = null;
+
+ SimpleTest.finish();
+ };
+ }
+ };
+ };
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_scriptProcessorNodeChannelCount.html b/dom/media/webaudio/test/test_scriptProcessorNodeChannelCount.html
new file mode 100644
index 0000000000..5e9a9960b7
--- /dev/null
+++ b/dom/media/webaudio/test/test_scriptProcessorNodeChannelCount.html
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AudioBufferSourceNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// We do not use our generic graph test framework here because
+// the testing logic here is sort of complicated, and would
+// not be easy to map to OfflineAudioContext, as ScriptProcessorNodes
+// can experience delays.
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var context = new AudioContext();
+ var buffer = context.createBuffer(6, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ for (var j = 0; j < 6; ++j) {
+ buffer.getChannelData(0)[i] = Math.sin(440 * j * Math.PI * i / context.sampleRate);
+ }
+ }
+
+ var monoBuffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ monoBuffer.getChannelData(0)[i] = 1;
+ }
+
+ var source = context.createBufferSource();
+
+ var sp = context.createScriptProcessor(2048, 3);
+ expectException(function() { sp.channelCount = 2; },
+ DOMException.NOT_SUPPORTED_ERR);
+ sp.channelCountMode = "explicit";
+ expectException(function() { sp.channelCountMode = "max"; },
+ DOMException.NOT_SUPPORTED_ERR);
+ expectException(function() { sp.channelCountMode = "clamped-max"; },
+ DOMException.NOT_SUPPORTED_ERR);
+ sp.channelInterpretation = "discrete";
+ source.start(0);
+ source.buffer = buffer;
+ source.connect(sp);
+ sp.connect(context.destination);
+
+ var monoSource = context.createBufferSource();
+ monoSource.buffer = monoBuffer;
+ monoSource.connect(sp);
+ monoSource.start(2048 / context.sampleRate);
+
+ sp.onaudioprocess = function(e) {
+ is(e.inputBuffer.numberOfChannels, 3, "Should be correctly down-mixed to three channels");
+ for (var i = 0; i < 3; ++i) {
+ compareChannels(e.inputBuffer.getChannelData(i), buffer.getChannelData(i));
+ }
+
+ // On the next iteration, we'll get a silence buffer
+ sp.onaudioprocess = function(e) {
+ var emptyBuffer = context.createBuffer(1, 2048, context.sampleRate);
+ is(e.inputBuffer.numberOfChannels, 3, "Should be correctly up-mixed to three channels");
+ compareChannels(e.inputBuffer.getChannelData(0), monoBuffer.getChannelData(0));
+ for (var i = 1; i < 3; ++i) {
+ compareChannels(e.inputBuffer.getChannelData(i), emptyBuffer.getChannelData(0));
+ }
+
+ sp.onaudioprocess = null;
+ sp.disconnect(context.destination);
+
+ SimpleTest.finish();
+ };
+ };
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_scriptProcessorNodeNotConnected.html b/dom/media/webaudio/test/test_scriptProcessorNodeNotConnected.html
new file mode 100644
index 0000000000..fb45895380
--- /dev/null
+++ b/dom/media/webaudio/test/test_scriptProcessorNodeNotConnected.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AudioBufferSourceNode: should not fire audioprocess if not connected.</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("This test needs to wait a while to ensure that a given event does not happen.");
+addLoadEvent(function() {
+ var context = new AudioContext();
+
+ var sp = context.createScriptProcessor(2048, 2, 2);
+ sp.onaudioprocess = function(e) {
+ ok(false, "Should not call onaudioprocess if the node is not connected.");
+ sp.onaudioprocess = null;
+ SimpleTest.finish();
+ };
+ setTimeout(function() {
+ console.log(sp.onaudioprocess);
+ if (sp.onaudioprocess) {
+ ok(true, "onaudioprocess not fired.");
+ SimpleTest.finish();
+ }
+ }, 4000);
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_scriptProcessorNodePassThrough.html b/dom/media/webaudio/test/test_scriptProcessorNodePassThrough.html
new file mode 100644
index 0000000000..5d2d8170e2
--- /dev/null
+++ b/dom/media/webaudio/test/test_scriptProcessorNodePassThrough.html
@@ -0,0 +1,103 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test ScriptProcessorNode with passthrough</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// We do not use our generic graph test framework here because
+// the testing logic here is sort of complicated, and would
+// not be easy to map to OfflineAudioContext, as ScriptProcessorNodes
+// can experience delays.
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var context = new AudioContext();
+ var buffer = null;
+
+ var sourceSP = context.createScriptProcessor(2048);
+ sourceSP.addEventListener("audioprocess", function(e) {
+ // generate the audio
+ for (var i = 0; i < 2048; ++i) {
+ // Make sure our first sample won't be zero
+ e.outputBuffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * (i + 1) / context.sampleRate);
+ e.outputBuffer.getChannelData(1)[i] = Math.sin(880 * 2 * Math.PI * (i + 1) / context.sampleRate);
+ }
+ // Remember our generated audio
+ buffer = e.outputBuffer;
+
+ sourceSP.removeEventListener("audioprocess", arguments.callee);
+ });
+
+ function findFirstNonZeroSample(buffer) {
+ for (var i = 0; i < buffer.length; ++i) {
+ if (buffer.getChannelData(0)[i] != 0) {
+ return i;
+ }
+ }
+ return buffer.length;
+ }
+
+ var sp = context.createScriptProcessor(2048);
+ sourceSP.connect(sp);
+
+ var spWrapped = SpecialPowers.wrap(sp);
+ ok("passThrough" in spWrapped, "ScriptProcessorNode should support the passThrough API");
+ spWrapped.passThrough = true;
+
+ sp.onaudioprocess = function() {
+ ok(false, "The audioprocess event must never be dispatched on the passthrough ScriptProcessorNode");
+ };
+
+ var sp2 = context.createScriptProcessor(2048);
+ sp.connect(sp2);
+ sp2.connect(context.destination);
+
+ var emptyBuffer = context.createBuffer(1, 2048, context.sampleRate);
+
+ sp2.onaudioprocess = function(e) {
+ // Because of the initial latency added by the second script processor node,
+ // we will never see any generated audio frames in the first callback.
+ compareChannels(e.inputBuffer.getChannelData(0), emptyBuffer.getChannelData(0));
+ compareChannels(e.inputBuffer.getChannelData(1), emptyBuffer.getChannelData(0));
+
+ sp2.onaudioprocess = function(e) {
+ var firstNonZero = findFirstNonZeroSample(e.inputBuffer);
+ ok(firstNonZero <= 2048, "First non-zero sample within range");
+
+ compareChannels(e.inputBuffer.getChannelData(0), emptyBuffer.getChannelData(0), firstNonZero);
+ compareChannels(e.inputBuffer.getChannelData(1), emptyBuffer.getChannelData(0), firstNonZero);
+ compareChannels(e.inputBuffer.getChannelData(0), buffer.getChannelData(0), 2048 - firstNonZero, firstNonZero, 0);
+ compareChannels(e.inputBuffer.getChannelData(1), buffer.getChannelData(1), 2048 - firstNonZero, firstNonZero, 0);
+
+ if (firstNonZero == 0) {
+ // If we did not experience any delays, the test is done!
+ sp2.onaudioprocess = null;
+
+ SimpleTest.finish();
+ } else if (firstNonZero != 2048) {
+ // In case we just saw a zero buffer this time, wait one more round
+ sp2.onaudioprocess = function(e) {
+ compareChannels(e.inputBuffer.getChannelData(0), buffer.getChannelData(0), firstNonZero, 0, 2048 - firstNonZero);
+ compareChannels(e.inputBuffer.getChannelData(1), buffer.getChannelData(1), firstNonZero, 0, 2048 - firstNonZero);
+ compareChannels(e.inputBuffer.getChannelData(0), emptyBuffer.getChannelData(0), undefined, firstNonZero);
+ compareChannels(e.inputBuffer.getChannelData(1), emptyBuffer.getChannelData(0), undefined, firstNonZero);
+
+ sp2.onaudioprocess = null;
+
+ SimpleTest.finish();
+ };
+ }
+ };
+ };
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_scriptProcessorNodeZeroInputOutput.html b/dom/media/webaudio/test/test_scriptProcessorNodeZeroInputOutput.html
new file mode 100644
index 0000000000..f4b25d49dd
--- /dev/null
+++ b/dom/media/webaudio/test/test_scriptProcessorNodeZeroInputOutput.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AudioBufferSourceNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var context = new AudioContext();
+
+ var sp = context.createScriptProcessor(2048, 0, 2);
+ sp.onaudioprocess = function(e) {
+ is(e.inputBuffer.numberOfChannels, 0, "Should have 0 input channels");
+ is(e.outputBuffer.numberOfChannels, 2, "Should have 2 output channels");
+ sp.onaudioprocess = null;
+
+ sp = context.createScriptProcessor(2048, 2, 0);
+ sp.onaudioprocess = function(e) {
+ is(e.inputBuffer.numberOfChannels, 2, "Should have 2 input channels");
+ is(e.outputBuffer.numberOfChannels, 0, "Should have 0 output channels");
+ sp.onaudioprocess = null;
+
+ SimpleTest.finish();
+ };
+ sp.connect(context.destination);
+ };
+ sp.connect(context.destination);
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_scriptProcessorNode_playbackTime1.html b/dom/media/webaudio/test/test_scriptProcessorNode_playbackTime1.html
new file mode 100644
index 0000000000..ec695f952b
--- /dev/null
+++ b/dom/media/webaudio/test/test_scriptProcessorNode_playbackTime1.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test ScriptProcessorNode playbackTime for bug 970773</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+var context = new AudioContext();
+const delay = 0.1;
+
+function doTest() {
+ const processorBufferLength = 256;
+ // |currentTime| may include double precision floating point
+ // rounding errors, so round to nearest integer sample to ignore these.
+ var minimumPlaybackSample =
+ Math.round(context.currentTime * context.sampleRate) +
+ processorBufferLength;
+ var sp = context.createScriptProcessor(processorBufferLength);
+ sp.connect(context.destination);
+ sp.onaudioprocess =
+ function(e) {
+ is(e.inputBuffer.length, processorBufferLength,
+ "expected buffer length");
+ var playbackSample = Math.round(e.playbackTime * context.sampleRate)
+ ok(playbackSample >= minimumPlaybackSample,
+ "playbackSample " + playbackSample +
+ " beyond expected minimum " + minimumPlaybackSample);
+ sp.onaudioprocess = null;
+ SimpleTest.finish();
+ };
+}
+
+// Wait until AudioDestinationNode has accumulated enough 'extra' time so that
+// a failure would be easily detected.
+(function waitForExtraTime() {
+ if (context.currentTime < delay) {
+ SimpleTest.executeSoon(waitForExtraTime);
+ } else {
+ doTest();
+ }
+})();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_sequentialBufferSourceWithResampling.html b/dom/media/webaudio/test/test_sequentialBufferSourceWithResampling.html
new file mode 100644
index 0000000000..5c03a8a911
--- /dev/null
+++ b/dom/media/webaudio/test/test_sequentialBufferSourceWithResampling.html
@@ -0,0 +1,72 @@
+<!DOCTYPE html>
+<title>Test seamless playback of a series of resampled buffers</title>
+<script src="/resources/testharness.js"></script>
+<script src="/resources/testharnessreport.js"></script>
+<script>
+// Permitting some accumulation of rounding to int16_t.
+// 64/2^15 would be only just small enough to detect off-by-one-subsample
+// scheduling errors with the frequencies here.
+const EPSILON = 4.0 / Math.pow(2, 15);
+// Offsets test for rounding to nearest rather than up or down.
+const OFFSETS = [EPSILON, 1.0 - EPSILON];
+// The ratio of resampling is 147:160, so 256 start points is enough to cover
+// every fractional offset.
+const LENGTH = 256;
+
+function do_test(context_rate, buffer_rate, start_offset) {
+
+ var context =
+ new OfflineAudioContext(2, LENGTH, context_rate);
+
+ var merger = context.createChannelMerger(context.destination.channelCount);
+ merger.connect(context.destination);
+
+ // Create an audio signal that will be repeated
+ var repeating_signal = context.createBuffer(1, 1, buffer_rate);
+ repeating_signal.getChannelData(0)[0] = 0.5;
+
+ // Schedule a series of nodes to repeat the signal.
+ for (var i = 0; i < LENGTH; ++i) {
+ var source = context.createBufferSource();
+ source.buffer = repeating_signal;
+ source.connect(merger, 0, 0);
+ source.start((i + start_offset) / buffer_rate);
+ }
+
+ // A single long signal should produce the same result.
+ var long_signal = context.createBuffer(1, LENGTH, buffer_rate);
+ var c = long_signal.getChannelData(0);
+ for (var i = 0; i < c.length; ++i) {
+ c[i] = 0.5;
+ }
+
+ var source = context.createBufferSource();
+ source.buffer = long_signal;
+ source.connect(merger, 0, 1);
+ source.start(start_offset / buffer_rate);
+
+ return context.startRendering().
+ then((buffer) => {
+ series_output = buffer.getChannelData(0);
+ expected = buffer.getChannelData(1);
+
+ for (var i = 0; i < buffer.length; ++i) {
+ assert_approx_equals(series_output[i], expected[i], EPSILON,
+ "series output at " + i);
+ }
+ });
+}
+
+function start_tests(context_rate, buffer_rate) {
+ OFFSETS.forEach((start_offset) => {
+ promise_test(() => do_test(context_rate, buffer_rate, start_offset),
+ "" + context_rate + " context, "
+ + buffer_rate + " buffer, "
+ + start_offset + " start");
+ });
+}
+
+start_tests(48000, 44100);
+start_tests(44100, 48000);
+
+</script>
diff --git a/dom/media/webaudio/test/test_setValueCurveWithNonFiniteElements.html b/dom/media/webaudio/test/test_setValueCurveWithNonFiniteElements.html
new file mode 100644
index 0000000000..28829e1ec2
--- /dev/null
+++ b/dom/media/webaudio/test/test_setValueCurveWithNonFiniteElements.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<meta charset=utf-8>
+<head>
+ <title>Bug 1308437 - setValueCurve should throw on non-finite elements</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+function testInfiniteElement(audioContext, audioParam) {
+ // create value curve with infinite element
+ var arr = new Float32Array(5);
+ arr[0] = 0.5;
+ arr[1] = 1;
+ arr[2] = Infinity;
+ arr[3] = 1;
+ arr[4] = 0.5;
+
+ try {
+ audioParam.setValueCurveAtTime(arr, audioContext.currentTime(), 2);
+ ok(false, "We shouldn't be able to call setValueCurve with Infinity but we can");
+ } catch(e) {
+ ok(e instanceof TypeError, "TypeError is thrown");
+ }
+};
+
+function testNanElement(audioContext, audioParam) {
+ // create value curve with infinite element
+ var arr = new Float32Array(5);
+ arr[0] = 0.5;
+ arr[1] = 1;
+ arr[2] = NaN;
+ arr[3] = 1;
+ arr[4] = 0.5;
+
+ try {
+ audioParam.setValueCurveAtTime(arr, audioContext.currentTime(), 2);
+ ok(false, "We shouldn't be able to call setValueCurve with NaN but we can");
+ } catch(e) {
+ ok(e instanceof TypeError, "TypeError is thrown");
+ }
+};
+
+addLoadEvent(function() {
+ var audioContext = new AudioContext();
+ var gainNode = audioContext.createGain();
+
+ testInfiniteElement(audioContext, gainNode.gain);
+ testNanElement(audioContext, gainNode.gain);
+
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html> \ No newline at end of file
diff --git a/dom/media/webaudio/test/test_singleSourceDest.html b/dom/media/webaudio/test/test_singleSourceDest.html
new file mode 100644
index 0000000000..fd4de50f5d
--- /dev/null
+++ b/dom/media/webaudio/test/test_singleSourceDest.html
@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test whether we can create an AudioContext interface</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+addLoadEvent(function() {
+ var context = new AudioContext();
+ var buffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ }
+
+ var destination = context.destination;
+ is(destination.context, context, "Destination node has proper context");
+ is(destination.context, context, "Destination node has proper context");
+ is(destination.numberOfInputs, 1, "Destination node has 1 inputs");
+ is(destination.numberOfOutputs, 0, "Destination node has 0 outputs");
+ is(destination.channelCount, 2, "Destination node has 2 input channels by default");
+ is(destination.channelCountMode, "explicit", "Correct channelCountMode for the destination node");
+ is(destination.channelInterpretation, "speakers", "Correct channelCountInterpretation for the destination node");
+ ok(destination instanceof EventTarget, "AudioNodes must be EventTargets");
+
+ var source = context.createBufferSource();
+ is(source.context, context, "Source node has proper context");
+ is(source.numberOfInputs, 0, "Source node has 0 inputs");
+ is(source.numberOfOutputs, 1, "Source node has 1 outputs");
+ is(source.loop, false, "Source node is not looping");
+ is(source.loopStart, 0, "Correct default value for loopStart");
+ is(source.loopEnd, 0, "Correct default value for loopEnd");
+ ok(!source.buffer, "Source node should not have a buffer when it's created");
+ is(source.channelCount, 2, "source node has 2 input channels by default");
+ is(source.channelCountMode, "max", "Correct channelCountMode for the source node");
+ is(source.channelInterpretation, "speakers", "Correct channelCountInterpretation for the source node");
+
+ expectException(function() {
+ source.channelCount = 0;
+ }, DOMException.NOT_SUPPORTED_ERR);
+
+ source.buffer = buffer;
+ ok(source.buffer, "Source node should have a buffer now");
+
+ source.connect(destination);
+
+ is(source.numberOfInputs, 0, "Source node has 0 inputs");
+ is(source.numberOfOutputs, 1, "Source node has 0 outputs");
+ is(destination.numberOfInputs, 1, "Destination node has 0 inputs");
+ is(destination.numberOfOutputs, 0, "Destination node has 0 outputs");
+
+ source.start(0);
+ SimpleTest.executeSoon(function() {
+ source.stop(0);
+ source.disconnect();
+
+ SpecialPowers.clearUserPref("media.webaudio.enabled");
+ SimpleTest.finish();
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_slowStart.html b/dom/media/webaudio/test/test_slowStart.html
new file mode 100644
index 0000000000..17de7351c1
--- /dev/null
+++ b/dom/media/webaudio/test/test_slowStart.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AudioContext.currentTime</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("This test needs to periodically query the AudioContext's position.");
+const CUBEB_INIT_DELAY = 5000;
+// Delay audio stream start by a good 5 seconds
+SpecialPowers.pushPrefEnv({"set": [["media.cubeb.slow_stream_init_ms",
+ CUBEB_INIT_DELAY]]}, runTest);
+
+
+function runTest() {
+ let ac = new AudioContext();
+ let notStartedYetCount = 0;
+ let startWallClockTime = performance.now();
+ is(ac.currentTime, 0, "AudioContext.currentTime should be 0 initially");
+ is(ac.state, "suspended", "AudioContext.currentTime is initially suspended");
+ let intervalHandle = setInterval(function() {
+ if (ac.state == "running" || ac.currentTime > 0) {
+ clearInterval(intervalHandle);
+ return;
+ }
+ is(ac.currentTime, 0, "AudioContext.currentTime is still 0");
+ is(ac.state, "suspended", "AudioContext.currentTime is still suspended");
+ notStartedYetCount++;
+ });
+ ac.onstatechange = function() {
+ is(ac.state, "running", "The AudioContext eventually started.");
+ var startDuration = performance.now() - startWallClockTime;
+ info(`AudioContext start time with a delay of ${CUBEB_INIT_DELAY}): ${startDuration}`);
+ ok(notStartedYetCount > 0, "We should have observed the AudioContext in \"suspended\" state");
+ ok(startDuration >= CUBEB_INIT_DELAY, "The AudioContext state transition was correct.");
+ SimpleTest.finish();
+ }
+}
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_stereoPannerNode.html b/dom/media/webaudio/test/test_stereoPannerNode.html
new file mode 100644
index 0000000000..d08e1640b2
--- /dev/null
+++ b/dom/media/webaudio/test/test_stereoPannerNode.html
@@ -0,0 +1,295 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test StereoPannerNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var SR = 44100;
+var BUF_SIZE = 128;
+var PANNING = 0.1;
+var GAIN = 0.5;
+
+// Cheap reimplementation of some bits of the spec
+function gainForPanningMonoToStereo(panning) {
+ panning += 1;
+ panning /= 2;
+ return [ Math.cos(0.5 * Math.PI * panning),
+ Math.sin(0.5 * Math.PI * panning) ];
+}
+
+function gainForPanningStereoToStereo(panning) {
+ if (panning <= 0) {
+ panning += 1.;
+ }
+ return [ Math.cos(0.5 * Math.PI * panning),
+ Math.sin(0.5 * Math.PI * panning) ];
+}
+
+function applyStereoToStereoPanning(l, r, panningValues, panning) {
+ var outL, outR;
+ if (panning <= 0) {
+ outL = l + r * panningValues[0];
+ outR = r * panningValues[1];
+ } else {
+ outL = l * panningValues[0];
+ outR = r + l * panningValues[1];
+ }
+ return [outL,outR];
+}
+
+function applyMonoToStereoPanning(c, panning) {
+ return [c * panning[0], c * panning[1]];
+}
+
+// Test the DOM interface
+var context = new OfflineAudioContext(1, 1, SR);
+var stereoPanner = new StereoPannerNode(context);
+ok(stereoPanner.pan, "The AudioParam member must exist");
+is(stereoPanner.pan.value, 0.0, "Correct initial value");
+is(stereoPanner.pan.defaultValue, 0.0, "Correct default value");
+is(stereoPanner.channelCount, 2, "StereoPannerNode node has 2 input channels by default");
+is(stereoPanner.channelCountMode, "clamped-max", "Correct channelCountMode for the StereoPannerNode");
+is(stereoPanner.channelInterpretation, "speakers", "Correct channelCountInterpretation for the StereoPannerNode");
+expectException(function() {
+ stereoPanner.channelCount = 3;
+}, DOMException.NOT_SUPPORTED_ERR);
+expectException(function() {
+ stereoPanner.channelCountMode = "max";
+}, DOMException.NOT_SUPPORTED_ERR);
+
+// A sine to be used to fill the buffers
+function sine(t) {
+ return Math.sin(440 * 2 * Math.PI * t / context.sampleRate);
+}
+
+// A couple mono and stereo buffers: the StereoPannerNode equation is different
+// if the input is mono or stereo
+var stereoBuffer = new AudioBuffer({ numberOfChannels: 2,
+ length: BUF_SIZE,
+ sampleRate: context.sampleRate });
+var monoBuffer = new AudioBuffer({ numberOfChannels: 1,
+ length: BUF_SIZE,
+ sampleRate: context.sampleRate });
+for (var i = 0; i < BUF_SIZE; ++i) {
+ monoBuffer.getChannelData(0)[i] =
+ stereoBuffer.getChannelData(0)[i] =
+ stereoBuffer.getChannelData(1)[i] = sine(i);
+}
+
+// Expected test vectors
+function expectedBufferNoop(gain) {
+ gain = gain || 1.0;
+ var expectedBuffer = new AudioBuffer({ numberOfChannels: 2,
+ length: BUF_SIZE,
+ sampleRate: SR });
+ for (var i = 0; i < BUF_SIZE; i++) {
+ expectedBuffer.getChannelData(0)[i] = gain * sine(i);
+ expectedBuffer.getChannelData(1)[i] = gain * sine(i);
+ }
+ return expectedBuffer;
+}
+
+function expectedBufferForStereo(panning, gain) {
+ gain = gain || 1.0;
+ var expectedBuffer = new AudioBuffer({ numberOfChannels: 2,
+ length: BUF_SIZE,
+ sampleRate: SR });
+ var gainPanning = gainForPanningStereoToStereo(panning);
+ for (var i = 0; i < BUF_SIZE; i++) {
+ var values = [ gain * sine(i), gain * sine(i) ];
+ var processed = applyStereoToStereoPanning(values[0], values[1], gainPanning, PANNING);
+ expectedBuffer.getChannelData(0)[i] = processed[0];
+ expectedBuffer.getChannelData(1)[i] = processed[1];
+ }
+ return expectedBuffer;
+}
+
+function expectedBufferForMono(panning, gain) {
+ gain = gain || 1.0;
+ var expectedBuffer = new AudioBuffer({ numberOfChannels: 2,
+ length: BUF_SIZE,
+ sampleRate: SR });
+ var gainPanning = gainForPanningMonoToStereo(panning);
+ gainPanning[0] *= gain;
+ gainPanning[1] *= gain;
+ for (var i = 0; i < BUF_SIZE; i++) {
+ var value = sine(i);
+ var processed = applyMonoToStereoPanning(value, gainPanning);
+ expectedBuffer.getChannelData(0)[i] = processed[0];
+ expectedBuffer.getChannelData(1)[i] = processed[1];
+ }
+ return expectedBuffer;
+}
+
+// Actual test cases
+var tests = [
+ function monoPanningNoop(ctx, panner) {
+ var monoSource = ctx.createBufferSource();
+ monoSource.connect(panner);
+ monoSource.buffer = monoBuffer;
+ monoSource.start(0);
+ return expectedBufferForMono(0);
+ },
+ function stereoPanningNoop(ctx, panner) {
+ var stereoSource = ctx.createBufferSource();
+ stereoSource.connect(panner);
+ stereoSource.buffer = stereoBuffer;
+ stereoSource.start(0);
+ return expectedBufferNoop();
+ },
+ function monoPanningNoopWithGain(ctx, panner) {
+ var monoSource = ctx.createBufferSource();
+ var gain = ctx.createGain();
+ gain.gain.value = GAIN;
+ monoSource.connect(gain);
+ gain.connect(panner);
+ monoSource.buffer = monoBuffer;
+ monoSource.start(0);
+ return expectedBufferForMono(0, GAIN);
+ },
+ function stereoPanningNoopWithGain(ctx, panner) {
+ var stereoSource = ctx.createBufferSource();
+ var gain = ctx.createGain();
+ gain.gain.value = GAIN;
+ stereoSource.connect(gain);
+ gain.connect(panner);
+ stereoSource.buffer = stereoBuffer;
+ stereoSource.start(0);
+ return expectedBufferNoop(GAIN);
+ },
+ function stereoPanningAutomation(ctx, panner) {
+ var stereoSource = ctx.createBufferSource();
+ stereoSource.connect(panner);
+ stereoSource.buffer = stereoBuffer;
+ panner.pan.setValueAtTime(0.1, 0.0);
+ stereoSource.start(0);
+ return expectedBufferForStereo(PANNING);
+ },
+ function stereoPanning(ctx, panner) {
+ var stereoSource = ctx.createBufferSource();
+ stereoSource.buffer = stereoBuffer;
+ stereoSource.connect(panner);
+ panner.pan.value = 0.1;
+ stereoSource.start(0);
+ return expectedBufferForStereo(PANNING);
+ },
+ function monoPanningAutomation(ctx, panner) {
+ var monoSource = ctx.createBufferSource();
+ monoSource.connect(panner);
+ monoSource.buffer = monoBuffer;
+ panner.pan.setValueAtTime(PANNING, 0.0);
+ monoSource.start(0);
+ return expectedBufferForMono(PANNING);
+ },
+ function monoPanning(ctx, panner) {
+ var monoSource = ctx.createBufferSource();
+ monoSource.connect(panner);
+ monoSource.buffer = monoBuffer;
+ panner.pan.value = 0.1;
+ monoSource.start(0);
+ return expectedBufferForMono(PANNING);
+ },
+ function monoPanningWithGain(ctx, panner) {
+ var monoSource = ctx.createBufferSource();
+ var gain = ctx.createGain();
+ gain.gain.value = GAIN;
+ monoSource.connect(gain);
+ gain.connect(panner);
+ monoSource.buffer = monoBuffer;
+ panner.pan.value = 0.1;
+ monoSource.start(0);
+ return expectedBufferForMono(PANNING, GAIN);
+ },
+ function stereoPanningWithGain(ctx, panner) {
+ var stereoSource = ctx.createBufferSource();
+ var gain = ctx.createGain();
+ gain.gain.value = GAIN;
+ stereoSource.connect(gain);
+ gain.connect(panner);
+ stereoSource.buffer = stereoBuffer;
+ panner.pan.value = 0.1;
+ stereoSource.start(0);
+ return expectedBufferForStereo(PANNING, GAIN);
+ },
+ function monoPanningWithGainAndAutomation(ctx, panner) {
+ var monoSource = ctx.createBufferSource();
+ var gain = ctx.createGain();
+ gain.gain.value = GAIN;
+ monoSource.connect(gain);
+ gain.connect(panner);
+ monoSource.buffer = monoBuffer;
+ panner.pan.setValueAtTime(PANNING, 0);
+ monoSource.start(0);
+ return expectedBufferForMono(PANNING, GAIN);
+ },
+ function stereoPanningWithGainAndAutomation(ctx, panner) {
+ var stereoSource = ctx.createBufferSource();
+ var gain = ctx.createGain();
+ gain.gain.value = GAIN;
+ stereoSource.connect(gain);
+ gain.connect(panner);
+ stereoSource.buffer = stereoBuffer;
+ panner.pan.setValueAtTime(PANNING, 0);
+ stereoSource.start(0);
+ return expectedBufferForStereo(PANNING, GAIN);
+ },
+ function bug_1783181(ctx, panner) {
+ const length = 128;
+ const buffer = new AudioBuffer({ length, numberOfChannels: 2, sampleRate: ctx.sampleRate });
+
+ buffer.copyToChannel(new Float32Array([1, 0.5, 0, -0.5, -1]), 0);
+ buffer.copyToChannel(new Float32Array([-0.5, -0.25, 0, 0.25, 0.5]), 1);
+
+ const audioBufferSourceNode = new AudioBufferSourceNode(ctx, { buffer });
+
+ audioBufferSourceNode.connect(panner);
+
+ panner.pan.setValueAtTime(0.5, 0);
+ panner.pan.setValueAtTime(0, 2 / ctx.sampleRate);
+ panner.pan.linearRampToValueAtTime(1, 5 / ctx.sampleRate);
+ panner.pan.cancelScheduledValues(3 / ctx.sampleRate);
+
+ audioBufferSourceNode.start(0);
+
+ const expected = new AudioBuffer({ length, numberOfChannels: 2, sampleRate: ctx.sampleRate });
+ expected.copyToChannel(new Float32Array([ 0.7071067690849304, 0.3535533845424652, 0, -0.5, -1 ]), 0);
+ expected.copyToChannel(new Float32Array([ 0.20710676908493042, 0.10355338454246521, 0, 0.25, 0.5 ]), 1);
+
+ return expected;
+ }
+];
+
+var finished = 0;
+function finish() {
+ if (++finished == tests.length) {
+ SimpleTest.finish();
+ }
+}
+
+tests.forEach(function(f) {
+ var ac = new OfflineAudioContext(2, BUF_SIZE, SR);
+ var panner = ac.createStereoPanner();
+ panner.connect(ac.destination);
+ var expected = f(ac, panner);
+ ac.oncomplete = function(e) {
+ info(f.name);
+ compareBuffers(e.renderedBuffer, expected);
+ finish();
+ };
+ ac.startRendering()
+});
+
+SimpleTest.waitForExplicitFinish();
+
+</script>
+</pre>
+<pre id=dump>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_stereoPannerNodePassThrough.html b/dom/media/webaudio/test/test_stereoPannerNodePassThrough.html
new file mode 100644
index 0000000000..2d774d366b
--- /dev/null
+++ b/dom/media/webaudio/test/test_stereoPannerNodePassThrough.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test StereoPanerNode with passthrough</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 2048,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var source = context.createBufferSource();
+
+ var stereoPanner = context.createStereoPanner();
+
+ source.buffer = this.buffer;
+
+ source.connect(stereoPanner);
+
+ var stereoPannerWrapped = SpecialPowers.wrap(stereoPanner);
+ ok("passThrough" in stereoPannerWrapped, "StereoPannerNode should support the passThrough API");
+ stereoPannerWrapped.passThrough = true;
+
+ source.start(0);
+ return stereoPanner;
+ },
+ createExpectedBuffers(context) {
+ this.buffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ this.buffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ }
+
+ return [this.buffer];
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_stereoPanningWithGain.html b/dom/media/webaudio/test/test_stereoPanningWithGain.html
new file mode 100644
index 0000000000..94dfa3bb92
--- /dev/null
+++ b/dom/media/webaudio/test/test_stereoPanningWithGain.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test stereo equalpower panning with a GainNode</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script src="webaudio.js" type="text/javascript"></script>
+<script class="testbody" type="text/javascript">
+
+const size = 256;
+
+var gTest = {
+ numberOfChannels: 2,
+ createGraph(context) {
+ var panner = context.createPanner();
+ panner.setPosition(1.0, 0.0, 0.0); // reference distance the right
+ panner.panningModel = "equalpower";
+
+ var gain = context.createGain();
+ gain.gain.value = -0.5;
+ gain.connect(panner);
+
+ var buffer = context.createBuffer(2, 2, context.sampleRate);
+ buffer.getChannelData(0)[0] = 1.0;
+ buffer.getChannelData(1)[1] = 1.0;
+ var source = context.createBufferSource();
+ source.buffer = buffer;
+ source.connect(gain);
+ source.start(0);
+
+ return panner;
+ },
+ createExpectedBuffers(context) {
+ var expectedBuffer = context.createBuffer(2, size, context.sampleRate);
+ expectedBuffer.getChannelData(1)[0] = -0.5;
+ expectedBuffer.getChannelData(1)[1] = -0.5;
+ return expectedBuffer;
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_waveDecoder.html b/dom/media/webaudio/test/test_waveDecoder.html
new file mode 100644
index 0000000000..65c429f2ba
--- /dev/null
+++ b/dom/media/webaudio/test/test_waveDecoder.html
@@ -0,0 +1,69 @@
+<!DOCTYPE HTML>
+<html>
+<meta charset=utf-8>
+<head>
+ <title>Test that we decode uint8 and sint16 wave files with correct conversion to float64</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+var testsDone = 0;
+var tests = ["UklGRjUrAABXQVZFZm10IBAAAAABAAEAESsAABErAAABAAgAZGF0YQMAAAD/AIA=",
+ "UklGRkZWAABXQVZFZm10IBAAAAABAAEAESsAACJWAAACABAAZGF0YQYAAAD/fwCAAAA="];
+
+SimpleTest.waitForExplicitFinish();
+
+function base64ToUint8Buffer(b64) {
+ var str = atob(b64)
+ var u8 = new Uint8Array(str.length);
+ for (var i = 0; i < str.length; ++i) {
+ u8[i] = str.charCodeAt(i);
+ }
+ return u8;
+}
+
+function fixupBufferSampleRate(u8, rate) {
+ u8[24] = (rate & 0x000000ff) >> 0;
+ u8[25] = (rate & 0x0000ff00) >> 8;
+ u8[26] = (rate & 0x00ff0000) >> 16;
+ u8[27] = (rate & 0xff000000) >> 24;
+}
+
+function finishTest() {
+ testsDone += 1;
+ if (testsDone == tests.length) {
+ SimpleTest.finish();
+ }
+}
+
+function decodeComplete(b) {
+ ok(true, "Decoding succeeded.");
+ is(b.numberOfChannels, 1, "Should have 1 channel.");
+ is(b.length, 3, "Should have three samples.");
+ var samples = b.getChannelData(0);
+ ok(samples[0] > 0.99 && samples[0] < 1.01, "Check near 1.0. Got " + samples[0]);
+ ok(samples[1] > -1.01 && samples[1] < -0.99, "Check near -1.0. Got " + samples[1]);
+ ok(samples[2] > -0.01 && samples[2] < 0.01, "Check near 0.0. Got " + samples[2]);
+ finishTest();
+}
+
+function decodeFailed() {
+ ok(false, "Decoding failed.");
+ finishTest();
+}
+
+addLoadEvent(function() {
+ var context = new AudioContext();
+
+ for (var i = 0; i < tests.length; ++i) {
+ var u8 = base64ToUint8Buffer(tests[i]);
+ fixupBufferSampleRate(u8, context.sampleRate);
+ context.decodeAudioData(u8.buffer, decodeComplete, decodeFailed);
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_waveShaper.html b/dom/media/webaudio/test/test_waveShaper.html
new file mode 100644
index 0000000000..9d2f1b3fa2
--- /dev/null
+++ b/dom/media/webaudio/test/test_waveShaper.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test WaveShaperNode with no curve</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 4096,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var source = context.createBufferSource();
+ source.buffer = this.buffer;
+
+ var shaper = new WaveShaperNode(context);
+ shaper.curve = this.curve;
+
+ source.connect(shaper);
+
+ source.start(0);
+ return shaper;
+ },
+ createExpectedBuffers(context) {
+ this.buffer = context.createBuffer(1, 4096, context.sampleRate);
+ for (var i = 1; i < 4095; ++i) {
+ this.buffer.getChannelData(0)[i] = 2 * (i / 4096) - 1;
+ }
+ // Two out of range values
+ this.buffer.getChannelData(0)[0] = -2;
+ this.buffer.getChannelData(0)[4095] = 2;
+
+ this.curve = new Float32Array(2048);
+ for (var i = 0; i < 2048; ++i) {
+ this.curve[i] = Math.sin(100 * Math.PI * (i + 1) / context.sampleRate);
+ }
+
+ var expectedBuffer = context.createBuffer(1, 4096, context.sampleRate);
+ for (var i = 1; i < 4095; ++i) {
+ var input = this.buffer.getChannelData(0)[i];
+ var index = Math.floor(this.curve.length * (input + 1) / 2);
+ index = Math.max(0, Math.min(this.curve.length - 1, index));
+ expectedBuffer.getChannelData(0)[i] = this.curve[index];
+ }
+ expectedBuffer.getChannelData(0)[0] = this.curve[0];
+ expectedBuffer.getChannelData(0)[4095] = this.curve[2047];
+ return expectedBuffer;
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_waveShaperGain.html b/dom/media/webaudio/test/test_waveShaperGain.html
new file mode 100644
index 0000000000..45411eca02
--- /dev/null
+++ b/dom/media/webaudio/test/test_waveShaperGain.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<meta charset="utf-8">
+ <title>Test that WaveShaperNode doesn't corrupt its inputs when the gain is !=
+ 1.0 (bug 1203616)</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre>
+</pre>
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+var samplerate = 44100;
+var context = new OfflineAudioContext(1, 44100, samplerate);
+
+var dc = context.createBufferSource();
+
+var buffer = context.createBuffer(1, 1, samplerate);
+buffer.getChannelData(0)[0] = 1.0;
+dc.buffer = buffer;
+
+var gain = context.createGain();
+var ws2 = context.createWaveShaper();
+var ws = [];
+
+// No-op waveshaper curves.
+for (var i = 0; i < 2; i++) {
+ ws[i] = context.createWaveShaper();
+ var curve = new Float32Array(2);
+ curve[0] = -1.0;
+ curve[1] = 1.0;
+ ws[i].curve = curve;
+ ws[i].connect(context.destination);
+ gain.connect(ws[i]);
+}
+
+dc.connect(gain);
+dc.start();
+
+gain.gain.value = 0.5;
+
+context.startRendering().then(buffer => {
+ document.querySelector("pre").innerHTML = buffer.getChannelData(0)[0];
+ ok(buffer.getChannelData(0)[0] == 1.0, "Volume was handled properly");
+
+ context = new OfflineAudioContext(1, 100, samplerate);
+ var oscillator = context.createOscillator();
+ var gain = context.createGain();
+ var waveShaper = context.createWaveShaper();
+
+ oscillator.start(0);
+ oscillator.connect(gain);
+
+ // to silence
+ gain.gain.value = 0;
+ gain.connect(waveShaper);
+
+ // convert all signal into 1.0. The non unity values are to detect the use
+ // of uninitialized buffers (see Bug 1283910).
+ waveShaper.curve = new Float32Array([ 0.5, 0.5, 0.5, 0.5, 0.5, 1, 1, 0.5, 0.5, 0.5, 0.5, 0.5 ]);
+ waveShaper.connect(context.destination);
+
+ context.startRendering().then((buffer) => {
+ var result = buffer.getChannelData(0);
+ ok(result.every(x => x === 1), "WaveShaper handles zero gain properly");
+ SimpleTest.finish();
+ });
+});
+</script>
+</body>
+
diff --git a/dom/media/webaudio/test/test_waveShaperInvalidLengthCurve.html b/dom/media/webaudio/test/test_waveShaperInvalidLengthCurve.html
new file mode 100644
index 0000000000..0901521a7b
--- /dev/null
+++ b/dom/media/webaudio/test/test_waveShaperInvalidLengthCurve.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test WaveShaperNode with an invalid curve</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 2048,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var source = context.createBufferSource();
+ source.buffer = this.buffer;
+
+ var shaper = context.createWaveShaper();
+
+ expectException(() => {
+ shaper.curve = new Float32Array(0);
+ }, DOMException.INVALID_STATE_ERR);
+
+ is(shaper.curve, null, "The curve mustn't have been set");
+
+ expectException(() => {
+ shaper.curve = new Float32Array(1);
+ }, DOMException.INVALID_STATE_ERR);
+
+ is(shaper.curve, null, "The curve mustn't have been set");
+
+ expectNoException(() => {
+ shaper.curve = new Float32Array(2);
+ });
+
+ isnot(shaper.curve, null, "The curve must have been set");
+
+ expectNoException(() => {
+ shaper.curve = null;
+ });
+
+ is(shaper.curve, null, "The curve must be null by default");
+
+ source.connect(shaper);
+
+ source.start(0);
+ return shaper;
+ },
+ createExpectedBuffers(context) {
+ var expectedBuffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ expectedBuffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ }
+ this.buffer = expectedBuffer;
+ return expectedBuffer;
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_waveShaperNoCurve.html b/dom/media/webaudio/test/test_waveShaperNoCurve.html
new file mode 100644
index 0000000000..2da0b511af
--- /dev/null
+++ b/dom/media/webaudio/test/test_waveShaperNoCurve.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test WaveShaperNode with no curve</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 2048,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var source = context.createBufferSource();
+ source.buffer = this.buffer;
+
+ var shaper = context.createWaveShaper();
+ is(shaper.curve, null, "The shaper curve must be null by default");
+
+ source.connect(shaper);
+
+ source.start(0);
+ return shaper;
+ },
+ createExpectedBuffers(context) {
+ var expectedBuffer = context.createBuffer(1, 2048, context.sampleRate);
+ for (var i = 0; i < 2048; ++i) {
+ expectedBuffer.getChannelData(0)[i] = Math.sin(440 * 2 * Math.PI * i / context.sampleRate);
+ }
+ this.buffer = expectedBuffer;
+ return expectedBuffer;
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_waveShaperPassThrough.html b/dom/media/webaudio/test/test_waveShaperPassThrough.html
new file mode 100644
index 0000000000..d34add9c90
--- /dev/null
+++ b/dom/media/webaudio/test/test_waveShaperPassThrough.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test WaveShaperNode with passthrough</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="webaudio.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+var gTest = {
+ length: 4096,
+ numberOfChannels: 1,
+ createGraph(context) {
+ var source = context.createBufferSource();
+ source.buffer = this.buffer;
+
+ var shaper = context.createWaveShaper();
+ shaper.curve = this.curve;
+
+ var shaperWrapped = SpecialPowers.wrap(shaper);
+ ok("passThrough" in shaperWrapped, "WaveShaperNode should support the passThrough API");
+ shaperWrapped.passThrough = true;
+
+ source.connect(shaper);
+
+ source.start(0);
+ return shaper;
+ },
+ createExpectedBuffers(context) {
+ this.buffer = context.createBuffer(1, 4096, context.sampleRate);
+ for (var i = 1; i < 4095; ++i) {
+ this.buffer.getChannelData(0)[i] = 2 * (i / 4096) - 1;
+ }
+ // Two out of range values
+ this.buffer.getChannelData(0)[0] = -2;
+ this.buffer.getChannelData(0)[4095] = 2;
+
+ this.curve = new Float32Array(2048);
+ for (var i = 0; i < 2048; ++i) {
+ this.curve[i] = Math.sin(100 * Math.PI * (i + 1) / context.sampleRate);
+ }
+
+ return [this.buffer];
+ },
+};
+
+runTest();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/test_webAudio_muteTab.html b/dom/media/webaudio/test/test_webAudio_muteTab.html
new file mode 100644
index 0000000000..ced5c20c9d
--- /dev/null
+++ b/dom/media/webaudio/test/test_webAudio_muteTab.html
@@ -0,0 +1,95 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script>
+createHTML({
+ title: "Check tab muting when the tab plays audio via the Web Audio API",
+ bug: "1346880",
+ visible: false
+});
+
+/**
+ * Check that muting a tab results in no audible audio: mute a tab, in which
+ * an OscillatorNode is playing. The default audio output device is a
+ * pulseaudio null-sink. Simulateously, record the other side of the null
+ * sink, and check that no audio has been written to the sink, because the tab
+ * was muted. Then, umute the tab and check that audio is being sent to the
+ * null-sink. */
+runTest(async () => {
+ if (!SpecialPowers.getCharPref("media.audio_loopback_dev", "")) {
+ todo(false, "No loopback device set by framework. Try --use-test-media-devices");
+ return;
+ }
+
+ // Mute the tab
+ await SpecialPowers.toggleMuteState(true, window.top);
+ // Don't use a loopback tone, the loopback device is here to check that
+ // nothing is output because the tab is muted.
+ DISABLE_LOOPBACK_TONE = true;
+
+ const stream = await getUserMedia({audio: {
+ noiseSuppression: false,
+ echoCancellation: false,
+ autoGainControl: false,
+ }});
+ try {
+ const ac = new AudioContext();
+ const osc = new OscillatorNode(ac);
+ osc.connect(ac.destination);
+ osc.start();
+
+ const analyser = new AudioStreamAnalyser(ac, stream);
+ // Wait for some time, checking there is only ever silent audio in the
+ // loopback stream. `waitForAnalysisSuccess` runs off requestAnimationFrame
+ let silenceFor = 3 / (1 / 60);
+ await analyser.waitForAnalysisSuccess(array => {
+ // `array` has values between 0 and 255, 0 being silence.
+ const sum = array.reduce((acc, v) => { return acc + v; });
+ if (sum == 0) {
+ silenceFor--;
+ } else {
+ info(`Sum of the array values ${sum}`);
+ ok(false, `Found non-silent data in the loopback stream while the tab was muted.`);
+ return true;
+ }
+ if (silenceFor == 0) {
+ ok(true, "Muting the tab was effective");
+ }
+ return silenceFor == 0;
+ });
+
+ // Unmute the tab
+ await SpecialPowers.toggleMuteState(false, window.top);
+
+ await analyser.waitForAnalysisSuccess(array => {
+ // `array` has values between 0 and 255, 0 being silence.
+ const sum = array.reduce((acc, v) => { return acc + v; });
+ if (sum != 0) {
+ info(`Sum after unmuting ${sum}`);
+ ok(true, "Unmuting the tab was effective");
+ return true;
+ } else {
+ // Increment again if we find silence.
+ silenceFor++;
+ if (silenceFor > 100) {
+ ok(false, "Unmuting wasn't effective")
+ return true;
+ }
+ return false;
+ }
+ });
+ } finally {
+ for (let t of stream.getTracks()) {
+ t.stop();
+ }
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webaudio/test/ting-44.1k-1ch.ogg b/dom/media/webaudio/test/ting-44.1k-1ch.ogg
new file mode 100644
index 0000000000..a11aaf1cbf
--- /dev/null
+++ b/dom/media/webaudio/test/ting-44.1k-1ch.ogg
Binary files differ
diff --git a/dom/media/webaudio/test/ting-44.1k-1ch.wav b/dom/media/webaudio/test/ting-44.1k-1ch.wav
new file mode 100644
index 0000000000..6854c9d898
--- /dev/null
+++ b/dom/media/webaudio/test/ting-44.1k-1ch.wav
Binary files differ
diff --git a/dom/media/webaudio/test/ting-44.1k-2ch.ogg b/dom/media/webaudio/test/ting-44.1k-2ch.ogg
new file mode 100644
index 0000000000..94e0014858
--- /dev/null
+++ b/dom/media/webaudio/test/ting-44.1k-2ch.ogg
Binary files differ
diff --git a/dom/media/webaudio/test/ting-44.1k-2ch.wav b/dom/media/webaudio/test/ting-44.1k-2ch.wav
new file mode 100644
index 0000000000..703d885892
--- /dev/null
+++ b/dom/media/webaudio/test/ting-44.1k-2ch.wav
Binary files differ
diff --git a/dom/media/webaudio/test/ting-48k-1ch.ogg b/dom/media/webaudio/test/ting-48k-1ch.ogg
new file mode 100644
index 0000000000..f45ce33a58
--- /dev/null
+++ b/dom/media/webaudio/test/ting-48k-1ch.ogg
Binary files differ
diff --git a/dom/media/webaudio/test/ting-48k-1ch.wav b/dom/media/webaudio/test/ting-48k-1ch.wav
new file mode 100644
index 0000000000..8fe471666c
--- /dev/null
+++ b/dom/media/webaudio/test/ting-48k-1ch.wav
Binary files differ
diff --git a/dom/media/webaudio/test/ting-48k-2ch.ogg b/dom/media/webaudio/test/ting-48k-2ch.ogg
new file mode 100644
index 0000000000..e4c564abbd
--- /dev/null
+++ b/dom/media/webaudio/test/ting-48k-2ch.ogg
Binary files differ
diff --git a/dom/media/webaudio/test/ting-48k-2ch.wav b/dom/media/webaudio/test/ting-48k-2ch.wav
new file mode 100644
index 0000000000..ad4d0466da
--- /dev/null
+++ b/dom/media/webaudio/test/ting-48k-2ch.wav
Binary files differ
diff --git a/dom/media/webaudio/test/ting-dualchannel44.1.wav b/dom/media/webaudio/test/ting-dualchannel44.1.wav
new file mode 100644
index 0000000000..62954394d3
--- /dev/null
+++ b/dom/media/webaudio/test/ting-dualchannel44.1.wav
Binary files differ
diff --git a/dom/media/webaudio/test/ting-dualchannel48.wav b/dom/media/webaudio/test/ting-dualchannel48.wav
new file mode 100644
index 0000000000..a0b8247888
--- /dev/null
+++ b/dom/media/webaudio/test/ting-dualchannel48.wav
Binary files differ
diff --git a/dom/media/webaudio/test/webaudio.js b/dom/media/webaudio/test/webaudio.js
new file mode 100644
index 0000000000..dd8ce7fc54
--- /dev/null
+++ b/dom/media/webaudio/test/webaudio.js
@@ -0,0 +1,319 @@
+// Helpers for Web Audio tests
+
+function expectException(func, exceptionCode) {
+ var threw = false;
+ try {
+ func();
+ } catch (ex) {
+ threw = true;
+ is(ex.constructor.name, "DOMException", "Expect a DOM exception");
+ is(ex.code, exceptionCode, "Expect the correct exception code");
+ }
+ ok(threw, "The exception was thrown");
+}
+
+function expectNoException(func) {
+ var threw = false;
+ try {
+ func();
+ } catch (ex) {
+ threw = true;
+ }
+ ok(!threw, "An exception was not thrown");
+}
+
+function expectTypeError(func) {
+ var threw = false;
+ try {
+ func();
+ } catch (ex) {
+ threw = true;
+ ok(ex instanceof TypeError, "Expect a TypeError");
+ }
+ ok(threw, "The exception was thrown");
+}
+
+function expectRejectedPromise(that, func, exceptionName) {
+ var promise = that[func]();
+
+ ok(promise instanceof Promise, "Expect a Promise");
+
+ promise
+ .then(function (res) {
+ ok(false, "Promise resolved when it should have been rejected.");
+ })
+ .catch(function (err) {
+ is(
+ err.name,
+ exceptionName,
+ "Promise correctly reject with " + exceptionName
+ );
+ });
+}
+
+function fuzzyCompare(a, b) {
+ return Math.abs(a - b) < 9e-3;
+}
+
+function compareChannels(
+ buf1,
+ buf2,
+ /*optional*/ length,
+ /*optional*/ sourceOffset,
+ /*optional*/ destOffset,
+ /*optional*/ skipLengthCheck
+) {
+ if (!skipLengthCheck) {
+ is(buf1.length, buf2.length, "Channels must have the same length");
+ }
+ sourceOffset = sourceOffset || 0;
+ destOffset = destOffset || 0;
+ if (length == undefined) {
+ length = buf1.length - sourceOffset;
+ }
+ var difference = 0;
+ var maxDifference = 0;
+ var firstBadIndex = -1;
+ for (var i = 0; i < length; ++i) {
+ if (!fuzzyCompare(buf1[i + sourceOffset], buf2[i + destOffset])) {
+ difference++;
+ maxDifference = Math.max(
+ maxDifference,
+ Math.abs(buf1[i + sourceOffset] - buf2[i + destOffset])
+ );
+ if (firstBadIndex == -1) {
+ firstBadIndex = i;
+ }
+ }
+ }
+
+ is(
+ difference,
+ 0,
+ "maxDifference: " +
+ maxDifference +
+ ", first bad index: " +
+ firstBadIndex +
+ " with test-data offset " +
+ sourceOffset +
+ " and expected-data offset " +
+ destOffset +
+ "; corresponding values " +
+ buf1[firstBadIndex + sourceOffset] +
+ " and " +
+ buf2[firstBadIndex + destOffset] +
+ " --- differences"
+ );
+}
+
+function compareBuffers(got, expected) {
+ if (got.numberOfChannels != expected.numberOfChannels) {
+ is(
+ got.numberOfChannels,
+ expected.numberOfChannels,
+ "Correct number of buffer channels"
+ );
+ return;
+ }
+ if (got.length != expected.length) {
+ is(got.length, expected.length, "Correct buffer length");
+ return;
+ }
+ if (got.sampleRate != expected.sampleRate) {
+ is(got.sampleRate, expected.sampleRate, "Correct sample rate");
+ return;
+ }
+
+ for (var i = 0; i < got.numberOfChannels; ++i) {
+ compareChannels(
+ got.getChannelData(i),
+ expected.getChannelData(i),
+ got.length,
+ 0,
+ 0,
+ true
+ );
+ }
+}
+
+/**
+ * Compute the root mean square (RMS,
+ * <http://en.wikipedia.org/wiki/Root_mean_square>) of a channel of a slice
+ * (defined by `start` and `end`) of an AudioBuffer.
+ *
+ * This is useful to detect that a buffer is noisy or silent.
+ */
+function rms(audiobuffer, channel = 0, start = 0, end = audiobuffer.length) {
+ var buffer = audiobuffer.getChannelData(channel);
+ var rms = 0;
+ for (var i = start; i < end; i++) {
+ rms += buffer[i] * buffer[i];
+ }
+
+ rms /= buffer.length;
+ rms = Math.sqrt(rms);
+ return rms;
+}
+
+function getEmptyBuffer(context, length) {
+ return context.createBuffer(
+ gTest.numberOfChannels,
+ length,
+ context.sampleRate
+ );
+}
+
+/**
+ * This function assumes that the test file defines a single gTest variable with
+ * the following properties and methods:
+ *
+ * + numberOfChannels: optional property which specifies the number of channels
+ * in the output. The default value is 2.
+ * + createGraph: mandatory method which takes a context object and does
+ * everything needed in order to set up the Web Audio graph.
+ * This function returns the node to be inspected.
+ * + createGraphAsync: async version of createGraph. This function takes
+ * a callback which should be called with an argument
+ * set to the node to be inspected when the callee is
+ * ready to proceed with the test. Either this function
+ * or createGraph must be provided.
+ * + createExpectedBuffers: optional method which takes a context object and
+ * returns either one expected buffer or an array of
+ * them, designating what is expected to be observed
+ * in the output. If omitted, the output is expected
+ * to be silence. All buffers must have the same
+ * length, which must be a bufferSize supported by
+ * ScriptProcessorNode. This function is guaranteed
+ * to be called before createGraph.
+ * + length: property equal to the total number of frames which we are waiting
+ * to see in the output, mandatory if createExpectedBuffers is not
+ * provided, in which case it must be a bufferSize supported by
+ * ScriptProcessorNode (256, 512, 1024, 2048, 4096, 8192, or 16384).
+ * If createExpectedBuffers is provided then this must be equal to
+ * the number of expected buffers * the expected buffer length.
+ *
+ * + skipOfflineContextTests: optional. when true, skips running tests on an offline
+ * context by circumventing testOnOfflineContext.
+ */
+function runTest() {
+ function done() {
+ SimpleTest.finish();
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ function runTestFunction() {
+ if (!gTest.numberOfChannels) {
+ gTest.numberOfChannels = 2; // default
+ }
+
+ var testLength;
+
+ function runTestOnContext(context, callback, testOutput) {
+ if (!gTest.createExpectedBuffers) {
+ // Assume that the output is silence
+ var expectedBuffers = getEmptyBuffer(context, gTest.length);
+ } else {
+ var expectedBuffers = gTest.createExpectedBuffers(context);
+ }
+ if (!(expectedBuffers instanceof Array)) {
+ expectedBuffers = [expectedBuffers];
+ }
+ var expectedFrames = 0;
+ for (var i = 0; i < expectedBuffers.length; ++i) {
+ is(
+ expectedBuffers[i].numberOfChannels,
+ gTest.numberOfChannels,
+ "Correct number of channels for expected buffer " + i
+ );
+ expectedFrames += expectedBuffers[i].length;
+ }
+ if (gTest.length && gTest.createExpectedBuffers) {
+ is(expectedFrames, gTest.length, "Correct number of expected frames");
+ }
+
+ if (gTest.createGraphAsync) {
+ gTest.createGraphAsync(context, function (nodeToInspect) {
+ testOutput(nodeToInspect, expectedBuffers, callback);
+ });
+ } else {
+ testOutput(gTest.createGraph(context), expectedBuffers, callback);
+ }
+ }
+
+ function testOnNormalContext(callback) {
+ function testOutput(nodeToInspect, expectedBuffers, callback) {
+ testLength = 0;
+ var sp = context.createScriptProcessor(
+ expectedBuffers[0].length,
+ gTest.numberOfChannels,
+ 0
+ );
+ nodeToInspect.connect(sp);
+ sp.onaudioprocess = function (e) {
+ var expectedBuffer = expectedBuffers.shift();
+ testLength += expectedBuffer.length;
+ compareBuffers(e.inputBuffer, expectedBuffer);
+ if (!expectedBuffers.length) {
+ sp.onaudioprocess = null;
+ callback();
+ }
+ };
+ }
+ var context = new AudioContext();
+ runTestOnContext(context, callback, testOutput);
+ }
+
+ function testOnOfflineContext(callback, sampleRate) {
+ function testOutput(nodeToInspect, expectedBuffers, callback) {
+ nodeToInspect.connect(context.destination);
+ context.oncomplete = function (e) {
+ var samplesSeen = 0;
+ while (expectedBuffers.length) {
+ var expectedBuffer = expectedBuffers.shift();
+ is(
+ e.renderedBuffer.numberOfChannels,
+ expectedBuffer.numberOfChannels,
+ "Correct number of input buffer channels"
+ );
+ for (var i = 0; i < e.renderedBuffer.numberOfChannels; ++i) {
+ compareChannels(
+ e.renderedBuffer.getChannelData(i),
+ expectedBuffer.getChannelData(i),
+ expectedBuffer.length,
+ samplesSeen,
+ undefined,
+ true
+ );
+ }
+ samplesSeen += expectedBuffer.length;
+ }
+ callback();
+ };
+ context.startRendering();
+ }
+
+ var context = new OfflineAudioContext(
+ gTest.numberOfChannels,
+ testLength,
+ sampleRate
+ );
+ runTestOnContext(context, callback, testOutput);
+ }
+
+ testOnNormalContext(function () {
+ if (!gTest.skipOfflineContextTests) {
+ testOnOfflineContext(function () {
+ testOnOfflineContext(done, 44100);
+ }, 48000);
+ } else {
+ done();
+ }
+ });
+ }
+
+ if (document.readyState !== "complete") {
+ addLoadEvent(runTestFunction);
+ } else {
+ runTestFunction();
+ }
+}
diff --git a/dom/media/webcodecs/VideoColorSpace.cpp b/dom/media/webcodecs/VideoColorSpace.cpp
new file mode 100644
index 0000000000..5b0dad0f31
--- /dev/null
+++ b/dom/media/webcodecs/VideoColorSpace.cpp
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/VideoColorSpace.h"
+#include "mozilla/dom/VideoColorSpaceBinding.h"
+#include "nsIGlobalObject.h"
+
+namespace mozilla::dom {
+
+// Only needed for refcounted objects.
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(VideoColorSpace, mParent)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(VideoColorSpace)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(VideoColorSpace)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(VideoColorSpace)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+VideoColorSpace::VideoColorSpace(nsIGlobalObject* aParent,
+ const VideoColorSpaceInit& aInit)
+ : mParent(aParent), mInit(aInit) {
+ MOZ_ASSERT(mParent);
+}
+
+nsIGlobalObject* VideoColorSpace::GetParentObject() const { return mParent; }
+
+JSObject* VideoColorSpace::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return VideoColorSpace_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+/* static */
+already_AddRefed<VideoColorSpace> VideoColorSpace::Constructor(
+ const GlobalObject& aGlobal, const VideoColorSpaceInit& aInit,
+ ErrorResult& aRv) {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+ if (!global) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+ RefPtr<VideoColorSpace> videoColorSpace(new VideoColorSpace(global, aInit));
+ return aRv.Failed() ? nullptr : videoColorSpace.forget();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webcodecs/VideoColorSpace.h b/dom/media/webcodecs/VideoColorSpace.h
new file mode 100644
index 0000000000..e6fdc22317
--- /dev/null
+++ b/dom/media/webcodecs/VideoColorSpace.h
@@ -0,0 +1,64 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_VideoColorSpace_h
+#define mozilla_dom_VideoColorSpace_h
+
+#include "js/TypeDecls.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/VideoColorSpaceBinding.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsWrapperCache.h"
+
+class nsIGlobalObject;
+
+namespace mozilla::dom {
+
+class VideoColorSpace final : public nsISupports, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(VideoColorSpace)
+
+ public:
+ VideoColorSpace(nsIGlobalObject* aParent, const VideoColorSpaceInit& aInit);
+
+ protected:
+ ~VideoColorSpace() = default;
+
+ public:
+ nsIGlobalObject* GetParentObject() const;
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ static already_AddRefed<VideoColorSpace> Constructor(
+ const GlobalObject& aGlobal, const VideoColorSpaceInit& aInit,
+ ErrorResult& aRv);
+
+ const Nullable<VideoColorPrimaries>& GetPrimaries() const {
+ return mInit.mPrimaries;
+ }
+
+ const Nullable<VideoTransferCharacteristics>& GetTransfer() const {
+ return mInit.mTransfer;
+ }
+
+ const Nullable<VideoMatrixCoefficients>& GetMatrix() const {
+ return mInit.mMatrix;
+ }
+
+ const Nullable<bool>& GetFullRange() const { return mInit.mFullRange; }
+
+ private:
+ nsCOMPtr<nsIGlobalObject> mParent;
+ const VideoColorSpaceInit mInit;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_VideoColorSpace_h
diff --git a/dom/media/webcodecs/VideoFrame.cpp b/dom/media/webcodecs/VideoFrame.cpp
new file mode 100644
index 0000000000..b6b88e9b1e
--- /dev/null
+++ b/dom/media/webcodecs/VideoFrame.cpp
@@ -0,0 +1,2391 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/VideoFrame.h"
+#include "mozilla/dom/VideoFrameBinding.h"
+
+#include <math.h>
+#include <limits>
+#include <utility>
+
+#include "ImageContainer.h"
+#include "VideoColorSpace.h"
+#include "js/StructuredClone.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Result.h"
+#include "mozilla/ResultVariant.h"
+#include "mozilla/ScopeExit.h"
+
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/CanvasUtils.h"
+#include "mozilla/dom/DOMRect.h"
+#include "mozilla/dom/HTMLCanvasElement.h"
+#include "mozilla/dom/HTMLImageElement.h"
+#include "mozilla/dom/HTMLVideoElement.h"
+#include "mozilla/dom/ImageBitmap.h"
+#include "mozilla/dom/ImageUtils.h"
+#include "mozilla/dom/OffscreenCanvas.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/SVGImageElement.h"
+#include "mozilla/dom/StructuredCloneHolder.h"
+#include "mozilla/dom/StructuredCloneTags.h"
+#include "mozilla/dom/UnionTypes.h"
+#include "mozilla/gfx/2D.h"
+#include "mozilla/gfx/Swizzle.h"
+#include "nsLayoutUtils.h"
+#include "nsIPrincipal.h"
+#include "nsIURI.h"
+
+namespace mozilla::dom {
+
+// Only needed for refcounted objects.
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(VideoFrame, mParent)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(VideoFrame)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(VideoFrame)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(VideoFrame)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+/*
+ * The below are helpers to operate ArrayBuffer or ArrayBufferView.
+ */
+template <class T>
+static Result<Span<uint8_t>, nsresult> GetArrayBufferData(const T& aBuffer) {
+ // Get buffer's data and length before using it.
+ aBuffer.ComputeState();
+
+ CheckedInt<size_t> byteLength(sizeof(typename T::element_type));
+ byteLength *= aBuffer.Length();
+ if (!byteLength.isValid()) {
+ return Err(NS_ERROR_INVALID_ARG);
+ }
+
+ return Span<uint8_t>(aBuffer.Data(), byteLength.value());
+}
+
+static Result<Span<uint8_t>, nsresult> GetSharedArrayBufferData(
+ const MaybeSharedArrayBufferViewOrMaybeSharedArrayBuffer& aBuffer) {
+ if (aBuffer.IsArrayBufferView()) {
+ return GetArrayBufferData(aBuffer.GetAsArrayBufferView());
+ }
+
+ MOZ_ASSERT(aBuffer.IsArrayBuffer());
+ return GetArrayBufferData(aBuffer.GetAsArrayBuffer());
+}
+
+/*
+ * The following are utilities to convert between VideoColorSpace values to
+ * gfx's values.
+ */
+
+static gfx::YUVColorSpace ToColorSpace(VideoMatrixCoefficients aMatrix) {
+ switch (aMatrix) {
+ case VideoMatrixCoefficients::Rgb:
+ return gfx::YUVColorSpace::Identity;
+ case VideoMatrixCoefficients::Bt709:
+ case VideoMatrixCoefficients::Bt470bg:
+ return gfx::YUVColorSpace::BT709;
+ case VideoMatrixCoefficients::Smpte170m:
+ return gfx::YUVColorSpace::BT601;
+ case VideoMatrixCoefficients::Bt2020_ncl:
+ return gfx::YUVColorSpace::BT2020;
+ case VideoMatrixCoefficients::EndGuard_:
+ break;
+ }
+ MOZ_ASSERT_UNREACHABLE("unsupported VideoMatrixCoefficients");
+ return gfx::YUVColorSpace::Default;
+}
+
+static gfx::TransferFunction ToTransferFunction(
+ VideoTransferCharacteristics aTransfer) {
+ switch (aTransfer) {
+ case VideoTransferCharacteristics::Bt709:
+ case VideoTransferCharacteristics::Smpte170m:
+ return gfx::TransferFunction::BT709;
+ case VideoTransferCharacteristics::Iec61966_2_1:
+ return gfx::TransferFunction::SRGB;
+ case VideoTransferCharacteristics::Pq:
+ return gfx::TransferFunction::PQ;
+ case VideoTransferCharacteristics::Hlg:
+ return gfx::TransferFunction::HLG;
+ case VideoTransferCharacteristics::Linear:
+ case VideoTransferCharacteristics::EndGuard_:
+ break;
+ }
+ MOZ_ASSERT_UNREACHABLE("unsupported VideoTransferCharacteristics");
+ return gfx::TransferFunction::Default;
+}
+
+static gfx::ColorSpace2 ToPrimaries(VideoColorPrimaries aPrimaries) {
+ switch (aPrimaries) {
+ case VideoColorPrimaries::Bt709:
+ return gfx::ColorSpace2::BT709;
+ case VideoColorPrimaries::Bt470bg:
+ return gfx::ColorSpace2::BT601_625;
+ case VideoColorPrimaries::Smpte170m:
+ return gfx::ColorSpace2::BT601_525;
+ case VideoColorPrimaries::Bt2020:
+ return gfx::ColorSpace2::BT2020;
+ case VideoColorPrimaries::Smpte432:
+ return gfx::ColorSpace2::DISPLAY_P3;
+ case VideoColorPrimaries::EndGuard_:
+ break;
+ }
+ MOZ_ASSERT_UNREACHABLE("unsupported VideoTransferCharacteristics");
+ return gfx::ColorSpace2::UNKNOWN;
+}
+
+static Maybe<VideoPixelFormat> ToVideoPixelFormat(gfx::SurfaceFormat aFormat) {
+ switch (aFormat) {
+ case gfx::SurfaceFormat::B8G8R8A8:
+ return Some(VideoPixelFormat::BGRA);
+ case gfx::SurfaceFormat::B8G8R8X8:
+ return Some(VideoPixelFormat::BGRX);
+ case gfx::SurfaceFormat::R8G8B8A8:
+ return Some(VideoPixelFormat::RGBA);
+ case gfx::SurfaceFormat::R8G8B8X8:
+ return Some(VideoPixelFormat::RGBX);
+ case gfx::SurfaceFormat::NV12:
+ return Some(VideoPixelFormat::NV12);
+ default:
+ break;
+ }
+ return Nothing();
+}
+
+static Maybe<VideoPixelFormat> ToVideoPixelFormat(ImageBitmapFormat aFormat) {
+ switch (aFormat) {
+ case ImageBitmapFormat::RGBA32:
+ return Some(VideoPixelFormat::RGBA);
+ case ImageBitmapFormat::BGRA32:
+ return Some(VideoPixelFormat::BGRA);
+ case ImageBitmapFormat::YUV444P:
+ return Some(VideoPixelFormat::I444);
+ case ImageBitmapFormat::YUV422P:
+ return Some(VideoPixelFormat::I422);
+ case ImageBitmapFormat::YUV420P:
+ return Some(VideoPixelFormat::I420);
+ case ImageBitmapFormat::YUV420SP_NV12:
+ return Some(VideoPixelFormat::NV12);
+ default:
+ break;
+ }
+ return Nothing();
+}
+
+/*
+ * The following are helpers to read the image data from the given buffer and
+ * the format. The data layout is illustrated in the comments for
+ * `VideoFrame::Format` below.
+ */
+
+static int32_t CeilingOfHalf(int32_t aValue) {
+ MOZ_ASSERT(aValue >= 0);
+ return aValue / 2 + (aValue % 2);
+}
+
+class YUVBufferReaderBase {
+ public:
+ YUVBufferReaderBase(const Span<uint8_t>& aBuffer, int32_t aWidth,
+ int32_t aHeight)
+ : mWidth(aWidth), mHeight(aHeight), mStrideY(aWidth), mBuffer(aBuffer) {}
+ virtual ~YUVBufferReaderBase() = default;
+
+ const uint8_t* DataY() const { return mBuffer.data(); }
+ const int32_t mWidth;
+ const int32_t mHeight;
+ const int32_t mStrideY;
+
+ protected:
+ CheckedInt<size_t> YByteSize() const {
+ return CheckedInt<size_t>(mStrideY) * mHeight;
+ }
+
+ const Span<uint8_t> mBuffer;
+};
+
+class I420ABufferReader;
+class I420BufferReader : public YUVBufferReaderBase {
+ public:
+ I420BufferReader(const Span<uint8_t>& aBuffer, int32_t aWidth,
+ int32_t aHeight)
+ : YUVBufferReaderBase(aBuffer, aWidth, aHeight),
+ mStrideU(CeilingOfHalf(aWidth)),
+ mStrideV(CeilingOfHalf(aWidth)) {}
+ virtual ~I420BufferReader() = default;
+
+ const uint8_t* DataU() const { return &mBuffer[YByteSize().value()]; }
+ const uint8_t* DataV() const {
+ return &mBuffer[YByteSize().value() + UByteSize().value()];
+ }
+ virtual I420ABufferReader* AsI420ABufferReader() { return nullptr; }
+
+ const int32_t mStrideU;
+ const int32_t mStrideV;
+
+ protected:
+ CheckedInt<size_t> UByteSize() const {
+ return CheckedInt<size_t>(CeilingOfHalf(mHeight)) * mStrideU;
+ }
+
+ CheckedInt<size_t> VSize() const {
+ return CheckedInt<size_t>(CeilingOfHalf(mHeight)) * mStrideV;
+ }
+};
+
+class I420ABufferReader final : public I420BufferReader {
+ public:
+ I420ABufferReader(const Span<uint8_t>& aBuffer, int32_t aWidth,
+ int32_t aHeight)
+ : I420BufferReader(aBuffer, aWidth, aHeight), mStrideA(aWidth) {
+ MOZ_ASSERT(mStrideA == mStrideY);
+ }
+ virtual ~I420ABufferReader() = default;
+
+ const uint8_t* DataA() const {
+ return &mBuffer[YByteSize().value() + UByteSize().value() +
+ VSize().value()];
+ }
+
+ virtual I420ABufferReader* AsI420ABufferReader() override { return this; }
+
+ const int32_t mStrideA;
+};
+
+class NV12BufferReader final : public YUVBufferReaderBase {
+ public:
+ NV12BufferReader(const Span<uint8_t>& aBuffer, int32_t aWidth,
+ int32_t aHeight)
+ : YUVBufferReaderBase(aBuffer, aWidth, aHeight),
+ mStrideUV(aWidth + aWidth % 2) {}
+ virtual ~NV12BufferReader() = default;
+
+ const uint8_t* DataUV() const { return &mBuffer[YByteSize().value()]; }
+
+ const int32_t mStrideUV;
+};
+
+/*
+ * The followings are helpers defined in
+ * https://w3c.github.io/webcodecs/#videoframe-algorithms
+ */
+static bool IsSameOrigin(nsIGlobalObject* aGlobal, nsIURI* aURI) {
+ MOZ_ASSERT(aGlobal);
+
+ nsIPrincipal* principal = aGlobal->PrincipalOrNull();
+ // If VideoFrames is created in worker, then it's from the same origin. In
+ // this case, principal or aURI is null. Otherwise, check the origin.
+ return !principal || !aURI || principal->IsSameOrigin(aURI);
+}
+
+static bool IsSameOrigin(nsIGlobalObject* aGlobal, const VideoFrame& aFrame) {
+ MOZ_ASSERT(aGlobal);
+ MOZ_ASSERT(aFrame.GetParentObject());
+
+ nsIPrincipal* principalX = aGlobal->PrincipalOrNull();
+ nsIPrincipal* principalY = aFrame.GetParentObject()->PrincipalOrNull();
+
+ // If both of VideoFrames are created in worker, they are in the same origin
+ // domain.
+ if (!principalX) {
+ return !principalY;
+ }
+ // Otherwise, check their domains.
+ return principalX->Equals(principalY);
+}
+
+static bool IsSameOrigin(nsIGlobalObject* aGlobal,
+ HTMLVideoElement& aVideoElement) {
+ MOZ_ASSERT(aGlobal);
+
+ // If CORS is in use, consider the video source is same-origin.
+ if (aVideoElement.GetCORSMode() != CORS_NONE) {
+ return true;
+ }
+
+ // Otherwise, check if video source has cross-origin redirect or not.
+ if (aVideoElement.HadCrossOriginRedirects()) {
+ return false;
+ }
+
+ // Finally, compare the VideoFrame's domain and video's one.
+ nsIPrincipal* principal = aGlobal->PrincipalOrNull();
+ nsCOMPtr<nsIPrincipal> elementPrincipal =
+ aVideoElement.GetCurrentVideoPrincipal();
+ // <video> cannot be created in worker, so it should have a valid principal.
+ if (NS_WARN_IF(!elementPrincipal) || !principal) {
+ return false;
+ }
+ return principal->Subsumes(elementPrincipal);
+}
+
+// A sub-helper to convert DOMRectInit to gfx::IntRect.
+static Result<gfx::IntRect, nsCString> ToIntRect(const DOMRectInit& aRectInit) {
+ auto EQ = [](const double& a, const double& b) {
+ constexpr double e = std::numeric_limits<double>::epsilon();
+ return std::fabs(a - b) <= e;
+ };
+ auto GT = [&](const double& a, const double& b) {
+ return !EQ(a, b) && a > b;
+ };
+
+ // Make sure the double values are in the gfx::IntRect's valid range, before
+ // checking the spec's valid range. The double's infinity value is larger than
+ // gfx::IntRect's max value so it will be filtered out here.
+ constexpr double MAX = static_cast<double>(
+ std::numeric_limits<decltype(gfx::IntRect::x)>::max());
+ constexpr double MIN = static_cast<double>(
+ std::numeric_limits<decltype(gfx::IntRect::x)>::min());
+ if (GT(aRectInit.mX, MAX) || GT(MIN, aRectInit.mX)) {
+ return Err(nsCString("x is out of the valid range"));
+ }
+ if (GT(aRectInit.mY, MAX) || GT(MIN, aRectInit.mY)) {
+ return Err(nsCString("y is out of the valid range"));
+ }
+ if (GT(aRectInit.mWidth, MAX) || GT(MIN, aRectInit.mWidth)) {
+ return Err(nsCString("width is out of the valid range"));
+ }
+ if (GT(aRectInit.mHeight, MAX) || GT(MIN, aRectInit.mHeight)) {
+ return Err(nsCString("height is out of the valid range"));
+ }
+
+ gfx::IntRect rect(
+ static_cast<decltype(gfx::IntRect::x)>(aRectInit.mX),
+ static_cast<decltype(gfx::IntRect::y)>(aRectInit.mY),
+ static_cast<decltype(gfx::IntRect::width)>(aRectInit.mWidth),
+ static_cast<decltype(gfx::IntRect::height)>(aRectInit.mHeight));
+ // Check the spec's valid range.
+ if (rect.X() < 0) {
+ return Err(nsCString("x must be non-negative"));
+ }
+ if (rect.Y() < 0) {
+ return Err(nsCString("y must be non-negative"));
+ }
+ if (rect.Width() <= 0) {
+ return Err(nsCString("width must be positive"));
+ }
+ if (rect.Height() <= 0) {
+ return Err(nsCString("height must be positive"));
+ }
+
+ return rect;
+}
+
+// A sub-helper to convert a (width, height) pair to gfx::IntRect.
+static Result<gfx::IntSize, nsCString> ToIntSize(const uint32_t& aWidth,
+ const uint32_t& aHeight) {
+ // Make sure the given values are in the gfx::IntSize's valid range, before
+ // checking the spec's valid range.
+ constexpr uint32_t MAX = static_cast<uint32_t>(
+ std::numeric_limits<decltype(gfx::IntRect::width)>::max());
+ if (aWidth > MAX) {
+ return Err(nsCString("Width exceeds the implementation's range"));
+ }
+ if (aHeight > MAX) {
+ return Err(nsCString("Height exceeds the implementation's range"));
+ }
+
+ gfx::IntSize size(static_cast<decltype(gfx::IntRect::width)>(aWidth),
+ static_cast<decltype(gfx::IntRect::height)>(aHeight));
+ // Check the spec's valid range.
+ if (size.Width() <= 0) {
+ return Err(nsCString("Width must be positive"));
+ }
+ if (size.Height() <= 0) {
+ return Err(nsCString("Height must be positive"));
+ }
+ return size;
+}
+
+// A sub-helper to make sure visible range is in the picture.
+static Result<Ok, nsCString> ValidateVisibility(
+ const gfx::IntRect& aVisibleRect, const gfx::IntSize& aPicSize) {
+ MOZ_ASSERT(aVisibleRect.X() >= 0);
+ MOZ_ASSERT(aVisibleRect.Y() >= 0);
+ MOZ_ASSERT(aVisibleRect.Width() > 0);
+ MOZ_ASSERT(aVisibleRect.Height() > 0);
+
+ const auto w = CheckedInt<uint32_t>(aVisibleRect.Width()) + aVisibleRect.X();
+ if (w.value() > static_cast<uint32_t>(aPicSize.Width())) {
+ return Err(nsCString(
+ "Sum of visible rectangle's x and width exceeds the picture's width"));
+ }
+
+ const auto h = CheckedInt<uint32_t>(aVisibleRect.Height()) + aVisibleRect.Y();
+ if (h.value() > static_cast<uint32_t>(aPicSize.Height())) {
+ return Err(
+ nsCString("Sum of visible rectangle's y and height exceeds the "
+ "picture's height"));
+ }
+
+ return Ok();
+}
+
+// A sub-helper to check and get display{Width, Height} in
+// VideoFrame(Buffer)Init.
+template <class T>
+static Result<Maybe<gfx::IntSize>, nsCString> MaybeGetDisplaySize(
+ const T& aInit) {
+ if (aInit.mDisplayWidth.WasPassed() != aInit.mDisplayHeight.WasPassed()) {
+ return Err(nsCString(
+ "displayWidth and displayHeight cannot be set without the other"));
+ }
+
+ Maybe<gfx::IntSize> displaySize;
+ if (aInit.mDisplayWidth.WasPassed() && aInit.mDisplayHeight.WasPassed()) {
+ displaySize.emplace();
+ MOZ_TRY_VAR(displaySize.ref(), ToIntSize(aInit.mDisplayWidth.Value(),
+ aInit.mDisplayHeight.Value())
+ .mapErr([](nsCString error) {
+ error.Insert("display", 0);
+ return error;
+ }));
+ }
+ return displaySize;
+}
+
+// https://w3c.github.io/webcodecs/#valid-videoframebufferinit
+static Result<
+ std::tuple<gfx::IntSize, Maybe<gfx::IntRect>, Maybe<gfx::IntSize>>,
+ nsCString>
+ValidateVideoFrameBufferInit(const VideoFrameBufferInit& aInit) {
+ gfx::IntSize codedSize;
+ MOZ_TRY_VAR(codedSize, ToIntSize(aInit.mCodedWidth, aInit.mCodedHeight)
+ .mapErr([](nsCString error) {
+ error.Insert("coded", 0);
+ return error;
+ }));
+
+ Maybe<gfx::IntRect> visibleRect;
+ if (aInit.mVisibleRect.WasPassed()) {
+ visibleRect.emplace();
+ MOZ_TRY_VAR(
+ visibleRect.ref(),
+ ToIntRect(aInit.mVisibleRect.Value()).mapErr([](nsCString error) {
+ error.Insert("visibleRect's ", 0);
+ return error;
+ }));
+ MOZ_TRY(ValidateVisibility(visibleRect.ref(), codedSize));
+ }
+
+ Maybe<gfx::IntSize> displaySize;
+ MOZ_TRY_VAR(displaySize, MaybeGetDisplaySize(aInit));
+
+ return std::make_tuple(codedSize, visibleRect, displaySize);
+}
+
+// https://w3c.github.io/webcodecs/#videoframe-verify-rect-offset-alignment
+static Result<Ok, nsCString> VerifyRectOffsetAlignment(
+ const VideoFrame::Format& aFormat, const gfx::IntRect& aRect) {
+ for (const VideoFrame::Format::Plane& p : aFormat.Planes()) {
+ const gfx::IntSize sample = aFormat.SampleSize(p);
+ if (aRect.X() % sample.Width() != 0) {
+ return Err(nsCString("Mismatch between format and given left offset"));
+ }
+
+ if (aRect.Y() % sample.Height() != 0) {
+ return Err(nsCString("Mismatch between format and given top offset"));
+ }
+ }
+ return Ok();
+}
+
+// https://w3c.github.io/webcodecs/#videoframe-parse-visible-rect
+static Result<gfx::IntRect, nsCString> ParseVisibleRect(
+ const gfx::IntRect& aDefaultRect, const Maybe<gfx::IntRect>& aOverrideRect,
+ const gfx::IntSize& aCodedSize, const VideoFrame::Format& aFormat) {
+ MOZ_ASSERT(ValidateVisibility(aDefaultRect, aCodedSize).isOk());
+
+ gfx::IntRect rect = aDefaultRect;
+ if (aOverrideRect) {
+ // Skip checking overrideRect's width and height here. They should be
+ // checked before reaching here, and ValidateVisibility will assert it.
+
+ MOZ_TRY(ValidateVisibility(aOverrideRect.ref(), aCodedSize));
+ rect = *aOverrideRect;
+ }
+
+ MOZ_TRY(VerifyRectOffsetAlignment(aFormat, rect));
+
+ return rect;
+}
+
+// https://w3c.github.io/webcodecs/#computed-plane-layout
+struct ComputedPlaneLayout {
+ // The offset from the beginning of the buffer in one plane.
+ uint32_t mDestinationOffset = 0;
+ // The stride of the image data in one plane.
+ uint32_t mDestinationStride = 0;
+ // Sample count of picture's top offset (a.k.a samples of y).
+ uint32_t mSourceTop = 0;
+ // Sample count of the picture's height.
+ uint32_t mSourceHeight = 0;
+ // Byte count of the picture's left offset (a.k.a bytes of x).
+ uint32_t mSourceLeftBytes = 0;
+ // Byte count of the picture's width.
+ uint32_t mSourceWidthBytes = 0;
+};
+
+// https://w3c.github.io/webcodecs/#combined-buffer-layout
+struct CombinedBufferLayout {
+ CombinedBufferLayout() : mAllocationSize(0) {}
+ CombinedBufferLayout(uint32_t aAllocationSize,
+ nsTArray<ComputedPlaneLayout>&& aLayout)
+ : mAllocationSize(aAllocationSize),
+ mComputedLayouts(std::move(aLayout)) {}
+ uint32_t mAllocationSize = 0;
+ nsTArray<ComputedPlaneLayout> mComputedLayouts;
+};
+
+// https://w3c.github.io/webcodecs/#videoframe-compute-layout-and-allocation-size
+static Result<CombinedBufferLayout, nsCString> ComputeLayoutAndAllocationSize(
+ const gfx::IntRect& aRect, const VideoFrame::Format& aFormat,
+ const Sequence<PlaneLayout>* aPlaneLayouts) {
+ nsTArray<VideoFrame::Format::Plane> planes = aFormat.Planes();
+
+ if (aPlaneLayouts && aPlaneLayouts->Length() != planes.Length()) {
+ return Err(nsCString("Mismatch between format and layout"));
+ }
+
+ uint32_t minAllocationSize = 0;
+ nsTArray<ComputedPlaneLayout> layouts;
+ nsTArray<uint32_t> endOffsets;
+
+ for (size_t i = 0; i < planes.Length(); ++i) {
+ const VideoFrame::Format::Plane& p = planes[i];
+ const gfx::IntSize sampleSize = aFormat.SampleSize(p);
+
+ // TODO: Spec here is wrong so we do differently:
+ // https://github.com/w3c/webcodecs/issues/511
+ // This comment should be removed once the issue is resolved.
+ ComputedPlaneLayout layout{
+ .mDestinationOffset = 0,
+ .mDestinationStride = 0,
+ .mSourceTop = (CheckedUint32(aRect.Y()) / sampleSize.Height()).value(),
+ .mSourceHeight =
+ (CheckedUint32(aRect.Height()) / sampleSize.Height()).value(),
+ .mSourceLeftBytes = (CheckedUint32(aRect.X()) / sampleSize.Width() *
+ aFormat.SampleBytes(p))
+ .value(),
+ .mSourceWidthBytes = (CheckedUint32(aRect.Width()) /
+ sampleSize.Width() * aFormat.SampleBytes(p))
+ .value()};
+ if (aPlaneLayouts) {
+ const PlaneLayout& planeLayout = aPlaneLayouts->ElementAt(i);
+ if (planeLayout.mStride < layout.mSourceWidthBytes) {
+ return Err(nsPrintfCString("The stride in %s plane is too small",
+ aFormat.PlaneName(p)));
+ }
+ layout.mDestinationOffset = planeLayout.mOffset;
+ layout.mDestinationStride = planeLayout.mStride;
+ } else {
+ layout.mDestinationOffset = minAllocationSize;
+ layout.mDestinationStride = layout.mSourceWidthBytes;
+ }
+
+ const CheckedInt<uint32_t> planeSize =
+ CheckedInt<uint32_t>(layout.mDestinationStride) * layout.mSourceHeight;
+ if (!planeSize.isValid()) {
+ return Err(nsCString("Invalid layout with an over-sized plane"));
+ }
+ const CheckedInt<uint32_t> planeEnd = planeSize + layout.mDestinationOffset;
+ if (!planeEnd.isValid()) {
+ return Err(nsCString("Invalid layout with the out-out-bound offset"));
+ }
+ endOffsets.AppendElement(planeEnd.value());
+
+ minAllocationSize = std::max(minAllocationSize, planeEnd.value());
+
+ for (size_t j = 0; j < i; ++j) {
+ const ComputedPlaneLayout& earlier = layouts[j];
+ // If the current data's end is smaller or equal to the previous one's
+ // head, or if the previous data's end is smaller or equal to the current
+ // one's head, then they do not overlap. Otherwise, they do.
+ if (endOffsets[i] > earlier.mDestinationOffset &&
+ endOffsets[j] > layout.mDestinationOffset) {
+ return Err(nsCString("Invalid layout with the overlapped planes"));
+ }
+ }
+ layouts.AppendElement(layout);
+ }
+
+ return CombinedBufferLayout(minAllocationSize, std::move(layouts));
+}
+
+// https://w3c.github.io/webcodecs/#videoframe-verify-rect-size-alignment
+static Result<Ok, nsCString> VerifyRectSizeAlignment(
+ const VideoFrame::Format& aFormat, const gfx::IntRect& aRect) {
+ for (const VideoFrame::Format::Plane& p : aFormat.Planes()) {
+ const gfx::IntSize sample = aFormat.SampleSize(p);
+ if (aRect.Width() % sample.Width() != 0) {
+ return Err(nsCString("Mismatch between format and given rect's width"));
+ }
+
+ if (aRect.Height() % sample.Height() != 0) {
+ return Err(nsCString("Mismatch between format and given rect's height"));
+ }
+ }
+ return Ok();
+}
+
+// https://w3c.github.io/webcodecs/#videoframe-parse-videoframecopytooptions
+static Result<CombinedBufferLayout, nsCString> ParseVideoFrameCopyToOptions(
+ const VideoFrameCopyToOptions& aOptions, const gfx::IntRect& aVisibleRect,
+ const gfx::IntSize& aCodedSize, const VideoFrame::Format& aFormat) {
+ Maybe<gfx::IntRect> overrideRect;
+ if (aOptions.mRect.WasPassed()) {
+ // TODO: We handle some edge cases that spec misses:
+ // https://github.com/w3c/webcodecs/issues/513
+ // This comment should be removed once the issue is resolved.
+ overrideRect.emplace();
+ MOZ_TRY_VAR(overrideRect.ref(),
+ ToIntRect(aOptions.mRect.Value()).mapErr([](nsCString error) {
+ error.Insert("rect's ", 0);
+ return error;
+ }));
+
+ MOZ_TRY(VerifyRectSizeAlignment(aFormat, overrideRect.ref()));
+ }
+
+ gfx::IntRect parsedRect;
+ MOZ_TRY_VAR(parsedRect, ParseVisibleRect(aVisibleRect, overrideRect,
+ aCodedSize, aFormat));
+
+ const Sequence<PlaneLayout>* optLayout = nullptr;
+ if (aOptions.mLayout.WasPassed()) {
+ optLayout = &aOptions.mLayout.Value();
+ }
+
+ return ComputeLayoutAndAllocationSize(parsedRect, aFormat, optLayout);
+}
+
+static bool IsYUVFormat(const VideoPixelFormat& aFormat) {
+ switch (aFormat) {
+ case VideoPixelFormat::I420:
+ case VideoPixelFormat::I420A:
+ case VideoPixelFormat::I422:
+ case VideoPixelFormat::I444:
+ case VideoPixelFormat::NV12:
+ return true;
+ case VideoPixelFormat::RGBA:
+ case VideoPixelFormat::RGBX:
+ case VideoPixelFormat::BGRA:
+ case VideoPixelFormat::BGRX:
+ return false;
+ case VideoPixelFormat::EndGuard_:
+ MOZ_ASSERT_UNREACHABLE("unsupported format");
+ }
+ return false;
+}
+
+// https://w3c.github.io/webcodecs/#videoframe-pick-color-space
+static VideoColorSpaceInit PickColorSpace(
+ const VideoColorSpaceInit* aInitColorSpace,
+ const VideoPixelFormat& aFormat) {
+ VideoColorSpaceInit colorSpace;
+ if (aInitColorSpace) {
+ colorSpace = *aInitColorSpace;
+ // By spec, we MAY replace null members of aInitColorSpace with guessed
+ // values so we can always use these in CreateYUVImageFromBuffer.
+ if (IsYUVFormat(aFormat) && colorSpace.mMatrix.IsNull()) {
+ colorSpace.mMatrix.SetValue(VideoMatrixCoefficients::Bt709);
+ }
+ return colorSpace;
+ }
+
+ switch (aFormat) {
+ case VideoPixelFormat::I420:
+ case VideoPixelFormat::I420A:
+ case VideoPixelFormat::I422:
+ case VideoPixelFormat::I444:
+ case VideoPixelFormat::NV12:
+ // https://w3c.github.io/webcodecs/#rec709-color-space
+ colorSpace.mFullRange.SetValue(false);
+ colorSpace.mMatrix.SetValue(VideoMatrixCoefficients::Bt709);
+ colorSpace.mPrimaries.SetValue(VideoColorPrimaries::Bt709);
+ colorSpace.mTransfer.SetValue(VideoTransferCharacteristics::Bt709);
+ break;
+ case VideoPixelFormat::RGBA:
+ case VideoPixelFormat::RGBX:
+ case VideoPixelFormat::BGRA:
+ case VideoPixelFormat::BGRX:
+ // https://w3c.github.io/webcodecs/#srgb-color-space
+ colorSpace.mFullRange.SetValue(true);
+ colorSpace.mMatrix.SetValue(VideoMatrixCoefficients::Rgb);
+ colorSpace.mPrimaries.SetValue(VideoColorPrimaries::Bt709);
+ colorSpace.mTransfer.SetValue(VideoTransferCharacteristics::Iec61966_2_1);
+ break;
+ case VideoPixelFormat::EndGuard_:
+ MOZ_ASSERT_UNREACHABLE("unsupported format");
+ }
+
+ return colorSpace;
+}
+
+// https://w3c.github.io/webcodecs/#validate-videoframeinit
+static Result<std::pair<Maybe<gfx::IntRect>, Maybe<gfx::IntSize>>, nsCString>
+ValidateVideoFrameInit(const VideoFrameInit& aInit,
+ const VideoFrame::Format& aFormat,
+ const gfx::IntSize& aCodedSize) {
+ if (aCodedSize.Width() <= 0 || aCodedSize.Height() <= 0) {
+ return Err(nsCString("codedWidth and codedHeight must be positive"));
+ }
+
+ Maybe<gfx::IntRect> visibleRect;
+ if (aInit.mVisibleRect.WasPassed()) {
+ visibleRect.emplace();
+ MOZ_TRY_VAR(
+ visibleRect.ref(),
+ ToIntRect(aInit.mVisibleRect.Value()).mapErr([](nsCString error) {
+ error.Insert("visibleRect's ", 0);
+ return error;
+ }));
+ MOZ_TRY(ValidateVisibility(visibleRect.ref(), aCodedSize));
+
+ MOZ_TRY(VerifyRectOffsetAlignment(aFormat, visibleRect.ref()));
+ }
+
+ Maybe<gfx::IntSize> displaySize;
+ MOZ_TRY_VAR(displaySize, MaybeGetDisplaySize(aInit));
+
+ return std::make_pair(visibleRect, displaySize);
+}
+
+/*
+ * The followings are helpers to create a VideoFrame from a given buffer
+ */
+
+static Result<RefPtr<gfx::DataSourceSurface>, nsCString> AllocateBGRASurface(
+ gfx::DataSourceSurface* aSurface) {
+ MOZ_ASSERT(aSurface);
+
+ // Memory allocation relies on CreateDataSourceSurfaceWithStride so we still
+ // need to do this even if the format is SurfaceFormat::BGR{A, X}.
+
+ gfx::DataSourceSurface::ScopedMap surfaceMap(aSurface,
+ gfx::DataSourceSurface::READ);
+ if (!surfaceMap.IsMapped()) {
+ return Err(nsCString("The source surface is not readable"));
+ }
+
+ RefPtr<gfx::DataSourceSurface> bgraSurface =
+ gfx::Factory::CreateDataSourceSurfaceWithStride(
+ aSurface->GetSize(), gfx::SurfaceFormat::B8G8R8A8,
+ surfaceMap.GetStride());
+ if (!bgraSurface) {
+ return Err(nsCString("Failed to allocate a BGRA surface"));
+ }
+
+ gfx::DataSourceSurface::ScopedMap bgraMap(bgraSurface,
+ gfx::DataSourceSurface::WRITE);
+ if (!bgraMap.IsMapped()) {
+ return Err(nsCString("The allocated BGRA surface is not writable"));
+ }
+
+ gfx::SwizzleData(surfaceMap.GetData(), surfaceMap.GetStride(),
+ aSurface->GetFormat(), bgraMap.GetData(),
+ bgraMap.GetStride(), bgraSurface->GetFormat(),
+ bgraSurface->GetSize());
+
+ return bgraSurface;
+}
+
+static Result<RefPtr<layers::Image>, nsCString> CreateImageFromRawData(
+ const gfx::IntSize& aSize, int32_t aStride, gfx::SurfaceFormat aFormat,
+ const Span<uint8_t>& aBuffer) {
+ MOZ_ASSERT(!aSize.IsEmpty());
+
+ // Wrap the source buffer into a DataSourceSurface.
+ RefPtr<gfx::DataSourceSurface> surface =
+ gfx::Factory::CreateWrappingDataSourceSurface(aBuffer.data(), aStride,
+ aSize, aFormat);
+ if (!surface) {
+ return Err(nsCString("Failed to wrap the raw data into a surface"));
+ }
+
+ // Gecko favors BGRA so we convert surface into BGRA format first.
+ RefPtr<gfx::DataSourceSurface> bgraSurface;
+ MOZ_TRY_VAR(bgraSurface, AllocateBGRASurface(surface));
+ MOZ_ASSERT(bgraSurface);
+
+ return RefPtr<layers::Image>(
+ new layers::SourceSurfaceImage(bgraSurface.get()));
+}
+
+static Result<RefPtr<layers::Image>, nsCString> CreateRGBAImageFromBuffer(
+ const VideoFrame::Format& aFormat, const gfx::IntSize& aSize,
+ const Span<uint8_t>& aBuffer) {
+ const gfx::SurfaceFormat format = aFormat.ToSurfaceFormat();
+ MOZ_ASSERT(format == gfx::SurfaceFormat::R8G8B8A8 ||
+ format == gfx::SurfaceFormat::R8G8B8X8 ||
+ format == gfx::SurfaceFormat::B8G8R8A8 ||
+ format == gfx::SurfaceFormat::B8G8R8X8);
+ // TODO: Use aFormat.SampleBytes() instead?
+ CheckedInt<int32_t> stride(BytesPerPixel(format));
+ stride *= aSize.Width();
+ if (!stride.isValid()) {
+ return Err(nsCString("Image size exceeds implementation's limit"));
+ }
+ return CreateImageFromRawData(aSize, stride.value(), format, aBuffer);
+}
+
+static Result<RefPtr<layers::Image>, nsCString> CreateYUVImageFromBuffer(
+ const VideoFrame::Format& aFormat, const VideoColorSpaceInit& aColorSpace,
+ const gfx::IntSize& aSize, const Span<uint8_t>& aBuffer) {
+ if (aFormat.PixelFormat() == VideoPixelFormat::I420 ||
+ aFormat.PixelFormat() == VideoPixelFormat::I420A) {
+ UniquePtr<I420BufferReader> reader;
+ if (aFormat.PixelFormat() == VideoPixelFormat::I420) {
+ reader.reset(
+ new I420BufferReader(aBuffer, aSize.Width(), aSize.Height()));
+ } else {
+ reader.reset(
+ new I420ABufferReader(aBuffer, aSize.Width(), aSize.Height()));
+ }
+
+ layers::PlanarYCbCrData data;
+ data.mPictureRect = gfx::IntRect(0, 0, reader->mWidth, reader->mHeight);
+
+ // Y plane.
+ data.mYChannel = const_cast<uint8_t*>(reader->DataY());
+ data.mYStride = reader->mStrideY;
+ data.mYSkip = 0;
+ // Cb plane.
+ data.mCbChannel = const_cast<uint8_t*>(reader->DataU());
+ data.mCbSkip = 0;
+ // Cr plane.
+ data.mCrChannel = const_cast<uint8_t*>(reader->DataV());
+ data.mCbSkip = 0;
+ // A plane.
+ if (aFormat.PixelFormat() == VideoPixelFormat::I420A) {
+ data.mAlpha.emplace();
+ data.mAlpha->mChannel =
+ const_cast<uint8_t*>(reader->AsI420ABufferReader()->DataA());
+ data.mAlpha->mSize = data.mPictureRect.Size();
+ // No values for mDepth and mPremultiplied.
+ }
+
+ // CbCr plane vector.
+ MOZ_RELEASE_ASSERT(reader->mStrideU == reader->mStrideV);
+ data.mCbCrStride = reader->mStrideU;
+ data.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT;
+ // Color settings.
+ if (!aColorSpace.mFullRange.IsNull() && aColorSpace.mFullRange.Value()) {
+ data.mColorRange = gfx::ColorRange::FULL;
+ }
+ MOZ_RELEASE_ASSERT(!aColorSpace.mMatrix.IsNull());
+ data.mYUVColorSpace = ToColorSpace(aColorSpace.mMatrix.Value());
+ if (!aColorSpace.mTransfer.IsNull()) {
+ data.mTransferFunction =
+ ToTransferFunction(aColorSpace.mTransfer.Value());
+ }
+ if (!aColorSpace.mPrimaries.IsNull()) {
+ data.mColorPrimaries = ToPrimaries(aColorSpace.mPrimaries.Value());
+ }
+
+ RefPtr<layers::PlanarYCbCrImage> image =
+ new layers::RecyclingPlanarYCbCrImage(new layers::BufferRecycleBin());
+ if (!image->CopyData(data)) {
+ return Err(nsPrintfCString(
+ "Failed to create I420%s image",
+ (aFormat.PixelFormat() == VideoPixelFormat::I420A ? "A" : "")));
+ }
+ // Manually cast type to make Result work.
+ return RefPtr<layers::Image>(image.forget());
+ }
+
+ if (aFormat.PixelFormat() == VideoPixelFormat::NV12) {
+ NV12BufferReader reader(aBuffer, aSize.Width(), aSize.Height());
+
+ layers::PlanarYCbCrData data;
+ data.mPictureRect = gfx::IntRect(0, 0, reader.mWidth, reader.mHeight);
+
+ // Y plane.
+ data.mYChannel = const_cast<uint8_t*>(reader.DataY());
+ data.mYStride = reader.mStrideY;
+ data.mYSkip = 0;
+ // Cb plane.
+ data.mCbChannel = const_cast<uint8_t*>(reader.DataUV());
+ data.mCbSkip = 1;
+ // Cr plane.
+ data.mCrChannel = data.mCbChannel + 1;
+ data.mCrSkip = 1;
+ // CbCr plane vector.
+ data.mCbCrStride = reader.mStrideUV;
+ data.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT;
+ // Color settings.
+ if (!aColorSpace.mFullRange.IsNull() && aColorSpace.mFullRange.Value()) {
+ data.mColorRange = gfx::ColorRange::FULL;
+ }
+ MOZ_RELEASE_ASSERT(!aColorSpace.mMatrix.IsNull());
+ data.mYUVColorSpace = ToColorSpace(aColorSpace.mMatrix.Value());
+ if (!aColorSpace.mTransfer.IsNull()) {
+ data.mTransferFunction =
+ ToTransferFunction(aColorSpace.mTransfer.Value());
+ }
+ if (!aColorSpace.mPrimaries.IsNull()) {
+ data.mColorPrimaries = ToPrimaries(aColorSpace.mPrimaries.Value());
+ }
+
+ RefPtr<layers::NVImage> image = new layers::NVImage();
+ if (!image->SetData(data)) {
+ return Err(nsCString("Failed to create NV12 image"));
+ }
+ // Manually cast type to make Result work.
+ return RefPtr<layers::Image>(image.forget());
+ }
+
+ return Err(nsCString("Unsupported image format"));
+}
+
+static Result<RefPtr<layers::Image>, nsCString> CreateImageFromBuffer(
+ const VideoFrame::Format& aFormat, const VideoColorSpaceInit& aColorSpace,
+ const gfx::IntSize& aSize, const Span<uint8_t>& aBuffer) {
+ switch (aFormat.PixelFormat()) {
+ case VideoPixelFormat::I420:
+ case VideoPixelFormat::I420A:
+ case VideoPixelFormat::NV12:
+ return CreateYUVImageFromBuffer(aFormat, aColorSpace, aSize, aBuffer);
+ case VideoPixelFormat::I422:
+ case VideoPixelFormat::I444:
+ // Not yet support for now.
+ break;
+ case VideoPixelFormat::RGBA:
+ case VideoPixelFormat::RGBX:
+ case VideoPixelFormat::BGRA:
+ case VideoPixelFormat::BGRX:
+ return CreateRGBAImageFromBuffer(aFormat, aSize, aBuffer);
+ case VideoPixelFormat::EndGuard_:
+ MOZ_ASSERT_UNREACHABLE("unsupported format");
+ }
+ return Err(nsCString("Invalid image format"));
+}
+
+// https://w3c.github.io/webcodecs/#dom-videoframe-videoframe-data-init
+template <class T>
+static Result<RefPtr<VideoFrame>, nsCString> CreateVideoFrameFromBuffer(
+ nsIGlobalObject* aGlobal, const T& aBuffer,
+ const VideoFrameBufferInit& aInit) {
+ if (aInit.mColorSpace.WasPassed() &&
+ !aInit.mColorSpace.Value().mTransfer.IsNull() &&
+ aInit.mColorSpace.Value().mTransfer.Value() ==
+ VideoTransferCharacteristics::Linear) {
+ return Err(nsCString("linear RGB is not supported"));
+ }
+
+ std::tuple<gfx::IntSize, Maybe<gfx::IntRect>, Maybe<gfx::IntSize>> init;
+ MOZ_TRY_VAR(init, ValidateVideoFrameBufferInit(aInit));
+ gfx::IntSize codedSize = std::get<0>(init);
+ Maybe<gfx::IntRect> visibleRect = std::get<1>(init);
+ Maybe<gfx::IntSize> displaySize = std::get<2>(init);
+
+ VideoFrame::Format format(aInit.mFormat);
+ // TODO: Spec doesn't ask for this in ctor but Pixel Format does. See
+ // https://github.com/w3c/webcodecs/issues/512
+ // This comment should be removed once the issue is resolved.
+ if (!format.IsValidSize(codedSize)) {
+ return Err(nsCString("coded width and/or height is invalid"));
+ }
+
+ gfx::IntRect parsedRect;
+ MOZ_TRY_VAR(parsedRect, ParseVisibleRect(gfx::IntRect({0, 0}, codedSize),
+ visibleRect, codedSize, format));
+
+ const Sequence<PlaneLayout>* optLayout = nullptr;
+ if (aInit.mLayout.WasPassed()) {
+ optLayout = &aInit.mLayout.Value();
+ }
+
+ CombinedBufferLayout combinedLayout;
+ MOZ_TRY_VAR(combinedLayout,
+ ComputeLayoutAndAllocationSize(parsedRect, format, optLayout));
+
+ Span<uint8_t> buffer;
+ MOZ_TRY_VAR(buffer, GetArrayBufferData(aBuffer).mapErr([](nsresult aError) {
+ return nsPrintfCString("Failed to get buffer data: %x",
+ static_cast<uint32_t>(aError));
+ }));
+
+ if (buffer.size_bytes() <
+ static_cast<size_t>(combinedLayout.mAllocationSize)) {
+ return Err(nsCString("data is too small"));
+ }
+
+ // TODO: If codedSize is (3, 3) and visibleRect is (0, 0, 1, 1) but the data
+ // is 2 x 2 RGBA buffer (2 x 2 x 4 bytes), it pass the above check. In this
+ // case, we can crop it to a 1 x 1-codedSize image (Bug 1782128).
+ if (buffer.size_bytes() < format.SampleCount(codedSize)) { // 1 byte/sample
+ return Err(nsCString("data is too small"));
+ }
+
+ // By spec, we should set visible* here. But if we don't change the image,
+ // visible* is same as parsedRect here. The display{Width, Height} is
+ // visible{Width, Height} if it's not set.
+
+ Maybe<uint64_t> duration =
+ aInit.mDuration.WasPassed() ? Some(aInit.mDuration.Value()) : Nothing();
+
+ VideoColorSpaceInit colorSpace = PickColorSpace(
+ aInit.mColorSpace.WasPassed() ? &aInit.mColorSpace.Value() : nullptr,
+ aInit.mFormat);
+
+ RefPtr<layers::Image> data;
+ MOZ_TRY_VAR(data,
+ CreateImageFromBuffer(format, colorSpace, codedSize, buffer));
+ MOZ_ASSERT(data);
+ MOZ_ASSERT(data->GetSize() == codedSize);
+
+ // TODO: Spec should assign aInit.mFormat to inner format value:
+ // https://github.com/w3c/webcodecs/issues/509.
+ // This comment should be removed once the issue is resolved.
+ return MakeRefPtr<VideoFrame>(aGlobal, data, aInit.mFormat, codedSize,
+ parsedRect,
+ displaySize ? *displaySize : parsedRect.Size(),
+ duration, aInit.mTimestamp, colorSpace);
+}
+
+template <class T>
+static already_AddRefed<VideoFrame> CreateVideoFrameFromBuffer(
+ const GlobalObject& aGlobal, const T& aBuffer,
+ const VideoFrameBufferInit& aInit, ErrorResult& aRv) {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+ if (!global) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ auto r = CreateVideoFrameFromBuffer(global, aBuffer, aInit);
+ if (r.isErr()) {
+ aRv.ThrowTypeError(r.unwrapErr());
+ return nullptr;
+ }
+ return r.unwrap().forget();
+}
+
+// https://w3c.github.io/webcodecs/#videoframe-initialize-visible-rect-and-display-size
+static void InitializeVisibleRectAndDisplaySize(
+ Maybe<gfx::IntRect>& aVisibleRect, Maybe<gfx::IntSize>& aDisplaySize,
+ gfx::IntRect aDefaultVisibleRect, gfx::IntSize aDefaultDisplaySize) {
+ if (!aVisibleRect) {
+ aVisibleRect.emplace(aDefaultVisibleRect);
+ }
+ if (!aDisplaySize) {
+ double wScale = static_cast<double>(aDefaultDisplaySize.Width()) /
+ aDefaultVisibleRect.Width();
+ double hScale = static_cast<double>(aDefaultDisplaySize.Height()) /
+ aDefaultVisibleRect.Height();
+ double w = wScale * aVisibleRect->Width();
+ double h = hScale * aVisibleRect->Height();
+ aDisplaySize.emplace(gfx::IntSize(static_cast<uint32_t>(round(w)),
+ static_cast<uint32_t>(round(h))));
+ }
+}
+
+// https://w3c.github.io/webcodecs/#videoframe-initialize-frame-with-resource-and-size
+static Result<already_AddRefed<VideoFrame>, nsCString>
+InitializeFrameWithResourceAndSize(
+ nsIGlobalObject* aGlobal, const VideoFrameInit& aInit,
+ already_AddRefed<layers::SourceSurfaceImage> aImage) {
+ MOZ_ASSERT(aInit.mTimestamp.WasPassed());
+
+ RefPtr<layers::SourceSurfaceImage> image(aImage);
+ MOZ_ASSERT(image);
+
+ RefPtr<gfx::SourceSurface> surface = image->GetAsSourceSurface();
+ Maybe<VideoFrame::Format> format =
+ ToVideoPixelFormat(surface->GetFormat())
+ .map([](const VideoPixelFormat& aFormat) {
+ return VideoFrame::Format(aFormat);
+ });
+ // TODO: Handle `ToVideoPixelFormat` failure.
+ if (NS_WARN_IF(!format)) {
+ return Err(nsCString("This image has unsupport format"));
+ }
+
+ std::pair<Maybe<gfx::IntRect>, Maybe<gfx::IntSize>> init;
+ MOZ_TRY_VAR(init,
+ ValidateVideoFrameInit(aInit, format.ref(), image->GetSize()));
+ Maybe<gfx::IntRect> visibleRect = init.first;
+ Maybe<gfx::IntSize> displaySize = init.second;
+
+ if (aInit.mAlpha == AlphaOption::Discard) {
+ format->MakeOpaque();
+ // Keep the alpha data in image for now until it's being rendered.
+ }
+
+ InitializeVisibleRectAndDisplaySize(visibleRect, displaySize,
+ gfx::IntRect({0, 0}, image->GetSize()),
+ image->GetSize());
+
+ Maybe<uint64_t> duration =
+ aInit.mDuration.WasPassed() ? Some(aInit.mDuration.Value()) : Nothing();
+
+ // TODO: WPT will fail if we guess a VideoColorSpace here.
+ const VideoColorSpaceInit colorSpace{};
+ return MakeAndAddRef<VideoFrame>(aGlobal, image, format->PixelFormat(),
+ image->GetSize(), visibleRect.value(),
+ displaySize.value(), duration,
+ aInit.mTimestamp.Value(), colorSpace);
+}
+
+// https://w3c.github.io/webcodecs/#videoframe-initialize-frame-from-other-frame
+static Result<already_AddRefed<VideoFrame>, nsCString>
+InitializeFrameFromOtherFrame(nsIGlobalObject* aGlobal, VideoFrameData&& aData,
+ const VideoFrameInit& aInit) {
+ MOZ_ASSERT(aGlobal);
+ MOZ_ASSERT(aData.mImage);
+
+ VideoFrame::Format format(aData.mFormat);
+ if (aInit.mAlpha == AlphaOption::Discard) {
+ format.MakeOpaque();
+ // Keep the alpha data in image for now until it's being rendered.
+ }
+
+ std::pair<Maybe<gfx::IntRect>, Maybe<gfx::IntSize>> init;
+ MOZ_TRY_VAR(init,
+ ValidateVideoFrameInit(aInit, format, aData.mImage->GetSize()));
+ Maybe<gfx::IntRect> visibleRect = init.first;
+ Maybe<gfx::IntSize> displaySize = init.second;
+
+ InitializeVisibleRectAndDisplaySize(visibleRect, displaySize,
+ aData.mVisibleRect, aData.mDisplaySize);
+
+ Maybe<uint64_t> duration = aInit.mDuration.WasPassed()
+ ? Some(aInit.mDuration.Value())
+ : aData.mDuration;
+ int64_t timestamp = aInit.mTimestamp.WasPassed() ? aInit.mTimestamp.Value()
+ : aData.mTimestamp;
+
+ return MakeAndAddRef<VideoFrame>(
+ aGlobal, aData.mImage, format.PixelFormat(), aData.mImage->GetSize(),
+ *visibleRect, *displaySize, duration, timestamp, aData.mColorSpace);
+}
+
+/*
+ * Helper classes carrying VideoFrame data
+ */
+
+VideoFrameData::VideoFrameData(layers::Image* aImage,
+ const VideoPixelFormat& aFormat,
+ gfx::IntRect aVisibleRect,
+ gfx::IntSize aDisplaySize,
+ Maybe<uint64_t> aDuration, int64_t aTimestamp,
+ const VideoColorSpaceInit& aColorSpace)
+ : mImage(aImage),
+ mFormat(aFormat),
+ mVisibleRect(aVisibleRect),
+ mDisplaySize(aDisplaySize),
+ mDuration(aDuration),
+ mTimestamp(aTimestamp),
+ mColorSpace(aColorSpace) {}
+
+VideoFrameSerializedData::VideoFrameSerializedData(
+ layers::Image* aImage, const VideoPixelFormat& aFormat,
+ gfx::IntSize aCodedSize, gfx::IntRect aVisibleRect,
+ gfx::IntSize aDisplaySize, Maybe<uint64_t> aDuration, int64_t aTimestamp,
+ const VideoColorSpaceInit& aColorSpace,
+ already_AddRefed<nsIURI> aPrincipalURI)
+ : VideoFrameData(aImage, aFormat, aVisibleRect, aDisplaySize, aDuration,
+ aTimestamp, aColorSpace),
+ mCodedSize(aCodedSize),
+ mPrincipalURI(aPrincipalURI) {}
+
+/*
+ * W3C Webcodecs VideoFrame implementation
+ */
+
+VideoFrame::VideoFrame(nsIGlobalObject* aParent,
+ const RefPtr<layers::Image>& aImage,
+ const VideoPixelFormat& aFormat, gfx::IntSize aCodedSize,
+ gfx::IntRect aVisibleRect, gfx::IntSize aDisplaySize,
+ const Maybe<uint64_t>& aDuration, int64_t aTimestamp,
+ const VideoColorSpaceInit& aColorSpace)
+ : mParent(aParent),
+ mResource(Some(Resource(aImage, VideoFrame::Format(aFormat)))),
+ mCodedSize(aCodedSize),
+ mVisibleRect(aVisibleRect),
+ mDisplaySize(aDisplaySize),
+ mDuration(aDuration),
+ mTimestamp(aTimestamp),
+ mColorSpace(aColorSpace) {
+ MOZ_ASSERT(mParent);
+}
+
+VideoFrame::VideoFrame(const VideoFrame& aOther)
+ : mParent(aOther.mParent),
+ mResource(aOther.mResource),
+ mCodedSize(aOther.mCodedSize),
+ mVisibleRect(aOther.mVisibleRect),
+ mDisplaySize(aOther.mDisplaySize),
+ mDuration(aOther.mDuration),
+ mTimestamp(aOther.mTimestamp),
+ mColorSpace(aOther.mColorSpace) {
+ MOZ_ASSERT(mParent);
+}
+
+nsIGlobalObject* VideoFrame::GetParentObject() const {
+ AssertIsOnOwningThread();
+
+ return mParent.get();
+}
+
+JSObject* VideoFrame::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ AssertIsOnOwningThread();
+
+ return VideoFrame_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+// The following constructors are defined in
+// https://w3c.github.io/webcodecs/#dom-videoframe-videoframe
+
+/* static */
+already_AddRefed<VideoFrame> VideoFrame::Constructor(
+ const GlobalObject& aGlobal, HTMLImageElement& aImageElement,
+ const VideoFrameInit& aInit, ErrorResult& aRv) {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+ if (!global) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ // Check the usability.
+ if (aImageElement.IntrinsicState().HasState(ElementState::BROKEN)) {
+ aRv.ThrowInvalidStateError("The image's state is broken");
+ return nullptr;
+ }
+ if (!aImageElement.Complete()) {
+ aRv.ThrowInvalidStateError("The image is not completely loaded yet");
+ return nullptr;
+ }
+ if (aImageElement.NaturalWidth() == 0) {
+ aRv.ThrowInvalidStateError("The image has a width of 0");
+ return nullptr;
+ }
+ if (aImageElement.NaturalHeight() == 0) {
+ aRv.ThrowInvalidStateError("The image has a height of 0");
+ return nullptr;
+ }
+
+ // If the origin of HTMLImageElement's image data is not same origin with the
+ // entry settings object's origin, then throw a SecurityError DOMException.
+ SurfaceFromElementResult res = nsLayoutUtils::SurfaceFromElement(
+ &aImageElement, nsLayoutUtils::SFE_WANT_FIRST_FRAME_IF_IMAGE);
+ if (res.mIsWriteOnly) {
+ // Being write-only implies its image is cross-origin w/out CORS headers.
+ aRv.ThrowSecurityError("The image is not same-origin");
+ return nullptr;
+ }
+
+ RefPtr<gfx::SourceSurface> surface = res.GetSourceSurface();
+ if (NS_WARN_IF(!surface)) {
+ aRv.ThrowInvalidStateError("The image's surface acquisition failed");
+ return nullptr;
+ }
+
+ if (!aInit.mTimestamp.WasPassed()) {
+ aRv.ThrowTypeError("Missing timestamp");
+ return nullptr;
+ }
+
+ RefPtr<layers::SourceSurfaceImage> image =
+ new layers::SourceSurfaceImage(surface.get());
+ auto r = InitializeFrameWithResourceAndSize(global, aInit, image.forget());
+ if (r.isErr()) {
+ aRv.ThrowTypeError(r.unwrapErr());
+ return nullptr;
+ }
+ return r.unwrap();
+}
+
+/* static */
+already_AddRefed<VideoFrame> VideoFrame::Constructor(
+ const GlobalObject& aGlobal, SVGImageElement& aSVGImageElement,
+ const VideoFrameInit& aInit, ErrorResult& aRv) {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+ if (!global) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ // Check the usability.
+ if (aSVGImageElement.IntrinsicState().HasState(ElementState::BROKEN)) {
+ aRv.ThrowInvalidStateError("The SVG's state is broken");
+ return nullptr;
+ }
+
+ // Check the image width and height.
+ if (!aSVGImageElement.HasValidDimensions()) {
+ aRv.ThrowInvalidStateError("The SVG does not have valid dimensions");
+ return nullptr;
+ }
+
+ // If the origin of SVGImageElement's image data is not same origin with the
+ // entry settings object's origin, then throw a SecurityError DOMException.
+ SurfaceFromElementResult res = nsLayoutUtils::SurfaceFromElement(
+ &aSVGImageElement, nsLayoutUtils::SFE_WANT_FIRST_FRAME_IF_IMAGE);
+ if (res.mIsWriteOnly) {
+ // Being write-only implies its image is cross-origin w/out CORS headers.
+ aRv.ThrowSecurityError("The SVG is not same-origin");
+ return nullptr;
+ }
+
+ RefPtr<gfx::SourceSurface> surface = res.GetSourceSurface();
+ if (NS_WARN_IF(!surface)) {
+ aRv.ThrowInvalidStateError("The SVG's surface acquisition failed");
+ return nullptr;
+ }
+
+ if (!aInit.mTimestamp.WasPassed()) {
+ aRv.ThrowTypeError("Missing timestamp");
+ return nullptr;
+ }
+
+ RefPtr<layers::SourceSurfaceImage> image =
+ new layers::SourceSurfaceImage(surface.get());
+ auto r = InitializeFrameWithResourceAndSize(global, aInit, image.forget());
+ if (r.isErr()) {
+ aRv.ThrowTypeError(r.unwrapErr());
+ return nullptr;
+ }
+ return r.unwrap();
+}
+
+/* static */
+already_AddRefed<VideoFrame> VideoFrame::Constructor(
+ const GlobalObject& aGlobal, HTMLCanvasElement& aCanvasElement,
+ const VideoFrameInit& aInit, ErrorResult& aRv) {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+ if (!global) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ // Check the usability.
+ if (aCanvasElement.Width() == 0) {
+ aRv.ThrowInvalidStateError("The canvas has a width of 0");
+ return nullptr;
+ }
+
+ if (aCanvasElement.Height() == 0) {
+ aRv.ThrowInvalidStateError("The canvas has a height of 0");
+ return nullptr;
+ }
+
+ // If the origin of HTMLCanvasElement's image data is not same origin with the
+ // entry settings object's origin, then throw a SecurityError DOMException.
+ SurfaceFromElementResult res = nsLayoutUtils::SurfaceFromElement(
+ &aCanvasElement, nsLayoutUtils::SFE_WANT_FIRST_FRAME_IF_IMAGE);
+ if (res.mIsWriteOnly) {
+ // Being write-only implies its image is cross-origin w/out CORS headers.
+ aRv.ThrowSecurityError("The canvas is not same-origin");
+ return nullptr;
+ }
+
+ RefPtr<gfx::SourceSurface> surface = res.GetSourceSurface();
+ if (NS_WARN_IF(!surface)) {
+ aRv.ThrowInvalidStateError("The canvas' surface acquisition failed");
+ return nullptr;
+ }
+
+ if (!aInit.mTimestamp.WasPassed()) {
+ aRv.ThrowTypeError("Missing timestamp");
+ return nullptr;
+ }
+
+ RefPtr<layers::SourceSurfaceImage> image =
+ new layers::SourceSurfaceImage(surface.get());
+ auto r = InitializeFrameWithResourceAndSize(global, aInit, image.forget());
+ if (r.isErr()) {
+ aRv.ThrowTypeError(r.unwrapErr());
+ return nullptr;
+ }
+ return r.unwrap();
+}
+
+/* static */
+already_AddRefed<VideoFrame> VideoFrame::Constructor(
+ const GlobalObject& aGlobal, HTMLVideoElement& aVideoElement,
+ const VideoFrameInit& aInit, ErrorResult& aRv) {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+ if (!global) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ aVideoElement.LogVisibility(
+ mozilla::dom::HTMLVideoElement::CallerAPI::CREATE_VIDEOFRAME);
+
+ // Check the usability.
+ if (aVideoElement.NetworkState() == HTMLMediaElement_Binding::NETWORK_EMPTY) {
+ aRv.ThrowInvalidStateError("The video has not been initialized yet");
+ return nullptr;
+ }
+ if (aVideoElement.ReadyState() <= HTMLMediaElement_Binding::HAVE_METADATA) {
+ aRv.ThrowInvalidStateError("The video is not ready yet");
+ return nullptr;
+ }
+ RefPtr<layers::Image> image = aVideoElement.GetCurrentImage();
+ if (!image) {
+ aRv.ThrowInvalidStateError("The video doesn't have any image yet");
+ return nullptr;
+ }
+
+ // If the origin of HTMLVideoElement's image data is not same origin with the
+ // entry settings object's origin, then throw a SecurityError DOMException.
+ if (!IsSameOrigin(global.get(), aVideoElement)) {
+ aRv.ThrowSecurityError("The video is not same-origin");
+ return nullptr;
+ }
+
+ const ImageUtils imageUtils(image);
+ Maybe<VideoPixelFormat> format = ToVideoPixelFormat(imageUtils.GetFormat());
+ if (!format) {
+ aRv.ThrowTypeError("The video's image is in unsupported format");
+ return nullptr;
+ }
+
+ // TODO: Retrive/infer the duration, and colorspace.
+ auto r = InitializeFrameFromOtherFrame(
+ global.get(),
+ VideoFrameData(image.get(), format.ref(), image->GetPictureRect(),
+ image->GetSize(), Nothing(),
+ static_cast<int64_t>(aVideoElement.CurrentTime()), {}),
+ aInit);
+ if (r.isErr()) {
+ aRv.ThrowTypeError(r.unwrapErr());
+ return nullptr;
+ }
+ return r.unwrap();
+}
+
+/* static */
+already_AddRefed<VideoFrame> VideoFrame::Constructor(
+ const GlobalObject& aGlobal, OffscreenCanvas& aOffscreenCanvas,
+ const VideoFrameInit& aInit, ErrorResult& aRv) {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+ if (!global) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ // Check the usability.
+ if (aOffscreenCanvas.Width() == 0) {
+ aRv.ThrowInvalidStateError("The canvas has a width of 0");
+ return nullptr;
+ }
+ if (aOffscreenCanvas.Height() == 0) {
+ aRv.ThrowInvalidStateError("The canvas has a height of 0");
+ return nullptr;
+ }
+
+ // If the origin of the OffscreenCanvas's image data is not same origin with
+ // the entry settings object's origin, then throw a SecurityError
+ // DOMException.
+ SurfaceFromElementResult res = nsLayoutUtils::SurfaceFromOffscreenCanvas(
+ &aOffscreenCanvas, nsLayoutUtils::SFE_WANT_FIRST_FRAME_IF_IMAGE);
+ if (res.mIsWriteOnly) {
+ // Being write-only implies its image is cross-origin w/out CORS headers.
+ aRv.ThrowSecurityError("The canvas is not same-origin");
+ return nullptr;
+ }
+
+ RefPtr<gfx::SourceSurface> surface = res.GetSourceSurface();
+ if (NS_WARN_IF(!surface)) {
+ aRv.ThrowInvalidStateError("The canvas' surface acquisition failed");
+ return nullptr;
+ }
+
+ if (!aInit.mTimestamp.WasPassed()) {
+ aRv.ThrowTypeError("Missing timestamp");
+ return nullptr;
+ }
+
+ RefPtr<layers::SourceSurfaceImage> image =
+ new layers::SourceSurfaceImage(surface.get());
+ auto r = InitializeFrameWithResourceAndSize(global, aInit, image.forget());
+ if (r.isErr()) {
+ aRv.ThrowTypeError(r.unwrapErr());
+ return nullptr;
+ }
+ return r.unwrap();
+}
+
+/* static */
+already_AddRefed<VideoFrame> VideoFrame::Constructor(
+ const GlobalObject& aGlobal, ImageBitmap& aImageBitmap,
+ const VideoFrameInit& aInit, ErrorResult& aRv) {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+ if (!global) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ // Check the usability.
+ UniquePtr<ImageBitmapCloneData> data = aImageBitmap.ToCloneData();
+ if (!data || !data->mSurface) {
+ aRv.ThrowInvalidStateError(
+ "The ImageBitmap is closed or its surface acquisition failed");
+ return nullptr;
+ }
+
+ // If the origin of the ImageBitmap's image data is not same origin with the
+ // entry settings object's origin, then throw a SecurityError DOMException.
+ if (data->mWriteOnly) {
+ // Being write-only implies its image is cross-origin w/out CORS headers.
+ aRv.ThrowSecurityError("The ImageBitmap is not same-origin");
+ return nullptr;
+ }
+
+ if (!aInit.mTimestamp.WasPassed()) {
+ aRv.ThrowTypeError("Missing timestamp");
+ return nullptr;
+ }
+
+ RefPtr<layers::SourceSurfaceImage> image =
+ new layers::SourceSurfaceImage(data->mSurface.get());
+ // TODO: Take care of data->mAlphaType
+ auto r = InitializeFrameWithResourceAndSize(global, aInit, image.forget());
+ if (r.isErr()) {
+ aRv.ThrowTypeError(r.unwrapErr());
+ return nullptr;
+ }
+ return r.unwrap();
+}
+
+/* static */
+already_AddRefed<VideoFrame> VideoFrame::Constructor(
+ const GlobalObject& aGlobal, VideoFrame& aVideoFrame,
+ const VideoFrameInit& aInit, ErrorResult& aRv) {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+ if (!global) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ // Check the usability.
+ if (!aVideoFrame.mResource) {
+ aRv.ThrowInvalidStateError(
+ "The VideoFrame is closed or no image found there");
+ return nullptr;
+ }
+ MOZ_ASSERT(aVideoFrame.mResource->mImage->GetSize() ==
+ aVideoFrame.mCodedSize);
+ MOZ_ASSERT(!aVideoFrame.mCodedSize.IsEmpty());
+ MOZ_ASSERT(!aVideoFrame.mVisibleRect.IsEmpty());
+ MOZ_ASSERT(!aVideoFrame.mDisplaySize.IsEmpty());
+
+ // If the origin of the VideoFrame is not same origin with the entry settings
+ // object's origin, then throw a SecurityError DOMException.
+ if (!IsSameOrigin(global.get(), aVideoFrame)) {
+ aRv.ThrowSecurityError("The VideoFrame is not same-origin");
+ return nullptr;
+ }
+
+ auto r = InitializeFrameFromOtherFrame(
+ global.get(),
+ VideoFrameData(aVideoFrame.mResource->mImage.get(),
+ aVideoFrame.mResource->mFormat.PixelFormat(),
+ aVideoFrame.mVisibleRect, aVideoFrame.mDisplaySize,
+ aVideoFrame.mDuration, aVideoFrame.mTimestamp,
+ aVideoFrame.mColorSpace),
+ aInit);
+ if (r.isErr()) {
+ aRv.ThrowTypeError(r.unwrapErr());
+ return nullptr;
+ }
+ return r.unwrap();
+}
+
+// The following constructors are defined in
+// https://w3c.github.io/webcodecs/#dom-videoframe-videoframe-data-init
+
+/* static */
+already_AddRefed<VideoFrame> VideoFrame::Constructor(
+ const GlobalObject& aGlobal, const ArrayBufferView& aBufferView,
+ const VideoFrameBufferInit& aInit, ErrorResult& aRv) {
+ return CreateVideoFrameFromBuffer(aGlobal, aBufferView, aInit, aRv);
+}
+
+/* static */
+already_AddRefed<VideoFrame> VideoFrame::Constructor(
+ const GlobalObject& aGlobal, const ArrayBuffer& aBuffer,
+ const VideoFrameBufferInit& aInit, ErrorResult& aRv) {
+ return CreateVideoFrameFromBuffer(aGlobal, aBuffer, aInit, aRv);
+}
+
+// https://w3c.github.io/webcodecs/#dom-videoframe-format
+Nullable<VideoPixelFormat> VideoFrame::GetFormat() const {
+ AssertIsOnOwningThread();
+
+ return mResource
+ ? Nullable<VideoPixelFormat>(mResource->mFormat.PixelFormat())
+ : Nullable<VideoPixelFormat>();
+}
+
+// https://w3c.github.io/webcodecs/#dom-videoframe-codedwidth
+uint32_t VideoFrame::CodedWidth() const {
+ AssertIsOnOwningThread();
+
+ return static_cast<uint32_t>(mCodedSize.Width());
+}
+
+// https://w3c.github.io/webcodecs/#dom-videoframe-codedheight
+uint32_t VideoFrame::CodedHeight() const {
+ AssertIsOnOwningThread();
+
+ return static_cast<uint32_t>(mCodedSize.Height());
+}
+
+// https://w3c.github.io/webcodecs/#dom-videoframe-codedrect
+already_AddRefed<DOMRectReadOnly> VideoFrame::GetCodedRect() const {
+ AssertIsOnOwningThread();
+
+ return mResource
+ ? MakeAndAddRef<DOMRectReadOnly>(
+ mParent, 0.0f, 0.0f, static_cast<double>(mCodedSize.Width()),
+ static_cast<double>(mCodedSize.Height()))
+ : nullptr;
+}
+
+// https://w3c.github.io/webcodecs/#dom-videoframe-visiblerect
+already_AddRefed<DOMRectReadOnly> VideoFrame::GetVisibleRect() const {
+ AssertIsOnOwningThread();
+
+ return mResource ? MakeAndAddRef<DOMRectReadOnly>(
+ mParent, static_cast<double>(mVisibleRect.X()),
+ static_cast<double>(mVisibleRect.Y()),
+ static_cast<double>(mVisibleRect.Width()),
+ static_cast<double>(mVisibleRect.Height()))
+ : nullptr;
+}
+
+// https://w3c.github.io/webcodecs/#dom-videoframe-displaywidth
+uint32_t VideoFrame::DisplayWidth() const {
+ AssertIsOnOwningThread();
+
+ return static_cast<uint32_t>(mDisplaySize.Width());
+}
+
+// https://w3c.github.io/webcodecs/#dom-videoframe-displayheight
+uint32_t VideoFrame::DisplayHeight() const {
+ AssertIsOnOwningThread();
+
+ return static_cast<uint32_t>(mDisplaySize.Height());
+}
+
+// https://w3c.github.io/webcodecs/#dom-videoframe-duration
+Nullable<uint64_t> VideoFrame::GetDuration() const {
+ AssertIsOnOwningThread();
+
+ return mDuration ? Nullable<uint64_t>(*mDuration) : Nullable<uint64_t>();
+}
+
+// https://w3c.github.io/webcodecs/#dom-videoframe-timestamp
+int64_t VideoFrame::Timestamp() const {
+ AssertIsOnOwningThread();
+
+ return mTimestamp;
+}
+
+// https://w3c.github.io/webcodecs/#dom-videoframe-colorspace
+already_AddRefed<VideoColorSpace> VideoFrame::ColorSpace() const {
+ AssertIsOnOwningThread();
+
+ return MakeAndAddRef<VideoColorSpace>(mParent, mColorSpace);
+}
+
+// https://w3c.github.io/webcodecs/#dom-videoframe-allocationsize
+uint32_t VideoFrame::AllocationSize(const VideoFrameCopyToOptions& aOptions,
+ ErrorResult& aRv) {
+ AssertIsOnOwningThread();
+
+ if (!mResource) {
+ aRv.ThrowInvalidStateError("No media resource in VideoFrame");
+ return 0;
+ }
+
+ auto r = ParseVideoFrameCopyToOptions(aOptions, mVisibleRect, mCodedSize,
+ mResource->mFormat);
+ if (r.isErr()) {
+ // TODO: Should throw layout.
+ aRv.ThrowTypeError(r.unwrapErr());
+ return 0;
+ }
+ CombinedBufferLayout layout = r.unwrap();
+
+ return layout.mAllocationSize;
+}
+
+// https://w3c.github.io/webcodecs/#dom-videoframe-copyto
+already_AddRefed<Promise> VideoFrame::CopyTo(
+ const MaybeSharedArrayBufferViewOrMaybeSharedArrayBuffer& aDestination,
+ const VideoFrameCopyToOptions& aOptions, ErrorResult& aRv) {
+ AssertIsOnOwningThread();
+
+ if (!mResource) {
+ aRv.ThrowInvalidStateError("No media resource in VideoFrame");
+ return nullptr;
+ }
+
+ RefPtr<Promise> p = Promise::Create(mParent.get(), aRv);
+ if (NS_WARN_IF(aRv.Failed())) {
+ return p.forget();
+ }
+
+ CombinedBufferLayout layout;
+ auto r1 = ParseVideoFrameCopyToOptions(aOptions, mVisibleRect, mCodedSize,
+ mResource->mFormat);
+ if (r1.isErr()) {
+ // TODO: Should reject with layout.
+ p->MaybeRejectWithTypeError(r1.unwrapErr());
+ return p.forget();
+ }
+ layout = r1.unwrap();
+
+ auto r2 = GetSharedArrayBufferData(aDestination);
+ if (r2.isErr()) {
+ p->MaybeRejectWithTypeError("Failed to get buffer");
+ return p.forget();
+ }
+ Span<uint8_t> buffer = r2.unwrap();
+
+ if (buffer.size_bytes() < layout.mAllocationSize) {
+ p->MaybeRejectWithTypeError("Destination buffer is too small");
+ return p.forget();
+ }
+
+ Sequence<PlaneLayout> planeLayouts;
+
+ nsTArray<Format::Plane> planes = mResource->mFormat.Planes();
+ MOZ_ASSERT(layout.mComputedLayouts.Length() == planes.Length());
+
+ // TODO: These jobs can be run in a thread pool (bug 1780656) to unblock the
+ // current thread.
+ for (size_t i = 0; i < layout.mComputedLayouts.Length(); ++i) {
+ ComputedPlaneLayout& l = layout.mComputedLayouts[i];
+ uint32_t destinationOffset = l.mDestinationOffset;
+
+ PlaneLayout* pl = planeLayouts.AppendElement(fallible);
+ if (!pl) {
+ p->MaybeRejectWithTypeError("Out of memory");
+ return p.forget();
+ }
+ pl->mOffset = l.mDestinationOffset;
+ pl->mStride = l.mDestinationStride;
+
+ // Copy pixels of `size` starting from `origin` on planes[i] to
+ // `aDestination`.
+ gfx::IntPoint origin(
+ l.mSourceLeftBytes / mResource->mFormat.SampleBytes(planes[i]),
+ l.mSourceTop);
+ gfx::IntSize size(
+ l.mSourceWidthBytes / mResource->mFormat.SampleBytes(planes[i]),
+ l.mSourceHeight);
+ if (!mResource->CopyTo(planes[i], {origin, size},
+ buffer.From(destinationOffset),
+ static_cast<size_t>(l.mDestinationStride))) {
+ p->MaybeRejectWithTypeError(
+ nsPrintfCString("Failed to copy image data in %s plane",
+ mResource->mFormat.PlaneName(planes[i])));
+ return p.forget();
+ }
+ }
+
+ MOZ_ASSERT(layout.mComputedLayouts.Length() == planes.Length());
+ // TODO: Spec doesn't resolve with a value. See
+ // https://github.com/w3c/webcodecs/issues/510 This comment should be removed
+ // once the issue is resolved.
+ p->MaybeResolve(planeLayouts);
+ return p.forget();
+}
+
+// https://w3c.github.io/webcodecs/#dom-videoframe-clone
+already_AddRefed<VideoFrame> VideoFrame::Clone(ErrorResult& aRv) {
+ AssertIsOnOwningThread();
+
+ if (!mResource) {
+ aRv.ThrowInvalidStateError("No media resource in the VideoFrame now");
+ return nullptr;
+ }
+ // The VideoFrame's data must be shared instead of copied:
+ // https://w3c.github.io/webcodecs/#raw-media-memory-model-reference-counting
+ return MakeAndAddRef<VideoFrame>(*this);
+}
+
+// https://w3c.github.io/webcodecs/#close-videoframe
+void VideoFrame::Close() {
+ AssertIsOnOwningThread();
+
+ mResource.reset();
+ mCodedSize = gfx::IntSize();
+ mVisibleRect = gfx::IntRect();
+ mDisplaySize = gfx::IntSize();
+ mDuration.reset();
+}
+
+// https://w3c.github.io/webcodecs/#ref-for-deserialization-steps%E2%91%A0
+/* static */
+JSObject* VideoFrame::ReadStructuredClone(
+ JSContext* aCx, nsIGlobalObject* aGlobal, JSStructuredCloneReader* aReader,
+ const VideoFrameSerializedData& aData) {
+ if (!IsSameOrigin(aGlobal, aData.mPrincipalURI.get())) {
+ return nullptr;
+ }
+
+ JS::Rooted<JS::Value> value(aCx, JS::NullValue());
+ // To avoid a rooting hazard error from returning a raw JSObject* before
+ // running the RefPtr destructor, RefPtr needs to be destructed before
+ // returning the raw JSObject*, which is why the RefPtr<VideoFrame> is created
+ // in the scope below. Otherwise, the static analysis infers the RefPtr cannot
+ // be safely destructed while the unrooted return JSObject* is on the stack.
+ {
+ RefPtr<VideoFrame> frame = MakeAndAddRef<VideoFrame>(
+ aGlobal, aData.mImage, aData.mFormat, aData.mCodedSize,
+ aData.mVisibleRect, aData.mDisplaySize, aData.mDuration,
+ aData.mTimestamp, aData.mColorSpace);
+ if (!GetOrCreateDOMReflector(aCx, frame, &value) || !value.isObject()) {
+ return nullptr;
+ }
+ }
+ return value.toObjectOrNull();
+}
+
+// https://w3c.github.io/webcodecs/#ref-for-serialization-steps%E2%91%A0
+bool VideoFrame::WriteStructuredClone(JSStructuredCloneWriter* aWriter,
+ StructuredCloneHolder* aHolder) const {
+ AssertIsOnOwningThread();
+
+ if (!mResource) {
+ return false;
+ }
+
+ // Indexing the image and send the index to the receiver.
+ const uint32_t index = aHolder->VideoFrames().Length();
+ RefPtr<layers::Image> image(mResource->mImage.get());
+ // The serialization is limited to the same process scope so it's ok to
+ // serialize a reference instead of a copy.
+ aHolder->VideoFrames().AppendElement(VideoFrameSerializedData(
+ image.get(), mResource->mFormat.PixelFormat(), mCodedSize, mVisibleRect,
+ mDisplaySize, mDuration, mTimestamp, mColorSpace, GetPrincipalURI()));
+
+ return !NS_WARN_IF(!JS_WriteUint32Pair(aWriter, SCTAG_DOM_VIDEOFRAME, index));
+}
+
+// https://w3c.github.io/webcodecs/#ref-for-transfer-steps%E2%91%A0
+UniquePtr<VideoFrame::TransferredData> VideoFrame::Transfer() {
+ AssertIsOnOwningThread();
+
+ if (!mResource) {
+ return nullptr;
+ }
+
+ Resource r = mResource.extract();
+ auto frame = MakeUnique<TransferredData>(
+ r.mImage.get(), r.mFormat.PixelFormat(), mCodedSize, mVisibleRect,
+ mDisplaySize, mDuration, mTimestamp, mColorSpace, GetPrincipalURI());
+ Close();
+ return frame;
+}
+
+// https://w3c.github.io/webcodecs/#ref-for-transfer-receiving-steps%E2%91%A0
+/* static */
+already_AddRefed<VideoFrame> VideoFrame::FromTransferred(
+ nsIGlobalObject* aGlobal, TransferredData* aData) {
+ MOZ_ASSERT(aData);
+
+ if (!IsSameOrigin(aGlobal, aData->mPrincipalURI.get())) {
+ return nullptr;
+ }
+
+ return MakeAndAddRef<VideoFrame>(aGlobal, aData->mImage, aData->mFormat,
+ aData->mCodedSize, aData->mVisibleRect,
+ aData->mDisplaySize, aData->mDuration,
+ aData->mTimestamp, aData->mColorSpace);
+}
+
+already_AddRefed<nsIURI> VideoFrame::GetPrincipalURI() const {
+ AssertIsOnOwningThread();
+
+ nsIPrincipal* principal = mParent->PrincipalOrNull();
+ return principal ? principal->GetURI() : nullptr;
+}
+
+/*
+ * VideoFrame::Format
+ *
+ * This class wraps a VideoPixelFormat defined in [1] and provides some
+ * utilities for the VideoFrame's functions. Each sample in the format is 8
+ * bits. The pixel layouts for a 4 x 2 image in the spec are illustrated below:
+ * [1] https://w3c.github.io/webcodecs/#pixel-format
+ *
+ * I420 - 3 planes: Y, U, V
+ * ------
+ * <- width ->
+ * Y: Y1 Y2 Y3 Y4 ^ height
+ * Y5 Y6 Y7 Y8 v
+ * U: U1 U2 => 1/2 Y's width, 1/2 Y's height
+ * V: V1 V2 => 1/2 Y's width, 1/2 Y's height
+ *
+ * I420A - 4 planes: Y, U, V, A
+ * ------
+ * <- width ->
+ * Y: Y1 Y2 Y3 Y4 ^ height
+ * Y5 Y6 Y7 Y8 v
+ * U: U1 U2 => 1/2 Y's width, 1/2 Y's height
+ * V: V1 V2 => 1/2 Y's width, 1/2 Y's height
+ * A: A1 A2 A3 A4 => Y's width, Y's height
+ * A5 A6 A7 A8
+ *
+ * I422 - 3 planes: Y, U, V
+ * ------
+ * <- width ->
+ * Y: Y1 Y2 Y3 Y4 ^ height
+ * Y5 Y6 Y7 Y8 v
+ * U: U1 U2 U3 U4 => Y's width, 1/2 Y's height
+ * V: V1 V2 V3 V4 => Y's width, 1/2 Y's height
+ *
+ * I444 - 3 planes: Y, U, V
+ * ------
+ * <- width ->
+ * Y: Y1 Y2 Y3 Y4 ^ height
+ * Y5 Y6 Y7 Y8 v
+ * U: U1 U2 U3 U4 => Y's width, Y's height
+ * U5 U6 U7 U8
+ * V: V1 V2 V3 V4 => Y's width, Y's height
+ * V5 V6 V7 B8
+ *
+ * NV12 - 2 planes: Y, UV
+ * ------
+ * <- width ->
+ * Y: Y1 Y2 Y3 Y4 ^ height
+ * Y5 Y6 Y7 Y8 v
+ * UV: U1 V1 U2 V2 => Y's width, 1/2 Y's height
+ *
+ * RGBA - 1 plane encoding 3 colors: Red, Green, Blue, and an Alpha value
+ * ------
+ * <---------------------- width ---------------------->
+ * R1 G1 B1 A1 | R2 G2 B2 A2 | R3 G3 B3 A3 | R4 G4 B4 A4 ^ height
+ * R5 G5 B5 A5 | R6 G6 B6 A6 | R7 G7 B7 A7 | R8 G8 B8 A8 v
+ *
+ * RGBX - 1 plane encoding 3 colors: Red, Green, Blue, and an padding value
+ * This is the opaque version of RGBA
+ * ------
+ * <---------------------- width ---------------------->
+ * R1 G1 B1 X1 | R2 G2 B2 X2 | R3 G3 B3 X3 | R4 G4 B4 X4 ^ height
+ * R5 G5 B5 X5 | R6 G6 B6 X6 | R7 G7 B7 X7 | R8 G8 B8 X8 v
+ *
+ * BGRA - 1 plane encoding 3 colors: Blue, Green, Red, and an Alpha value
+ * ------
+ * <---------------------- width ---------------------->
+ * B1 G1 R1 A1 | B2 G2 R2 A2 | B3 G3 R3 A3 | B4 G4 R4 A4 ^ height
+ * B5 G5 R5 A5 | B6 G6 R6 A6 | B7 G7 R7 A7 | B8 G8 R8 A8 v
+ *
+ * BGRX - 1 plane encoding 3 colors: Blue, Green, Red, and an padding value
+ * This is the opaque version of BGRA
+ * ------
+ * <---------------------- width ---------------------->
+ * B1 G1 R1 X1 | B2 G2 R2 X2 | B3 G3 R3 X3 | B4 G4 R4 X4 ^ height
+ * B5 G5 R5 X5 | B6 G6 R6 X6 | B7 G7 R7 X7 | B8 G8 R8 X8 v
+ */
+
+VideoFrame::Format::Format(const VideoPixelFormat& aFormat)
+ : mFormat(aFormat) {}
+
+const VideoPixelFormat& VideoFrame::Format::PixelFormat() const {
+ return mFormat;
+}
+
+gfx::SurfaceFormat VideoFrame::Format::ToSurfaceFormat() const {
+ gfx::SurfaceFormat format = gfx::SurfaceFormat::UNKNOWN;
+ switch (mFormat) {
+ case VideoPixelFormat::I420:
+ case VideoPixelFormat::I420A:
+ case VideoPixelFormat::I422:
+ case VideoPixelFormat::I444:
+ case VideoPixelFormat::NV12:
+ // Not yet support for now.
+ break;
+ case VideoPixelFormat::RGBA:
+ format = gfx::SurfaceFormat::R8G8B8A8;
+ break;
+ case VideoPixelFormat::RGBX:
+ format = gfx::SurfaceFormat::R8G8B8X8;
+ break;
+ case VideoPixelFormat::BGRA:
+ format = gfx::SurfaceFormat::B8G8R8A8;
+ break;
+ case VideoPixelFormat::BGRX:
+ format = gfx::SurfaceFormat::B8G8R8X8;
+ break;
+ case VideoPixelFormat::EndGuard_:
+ MOZ_ASSERT_UNREACHABLE("unsupported format");
+ }
+ return format;
+}
+
+void VideoFrame::Format::MakeOpaque() {
+ switch (mFormat) {
+ case VideoPixelFormat::I420A:
+ mFormat = VideoPixelFormat::I420;
+ return;
+ case VideoPixelFormat::RGBA:
+ mFormat = VideoPixelFormat::RGBX;
+ return;
+ case VideoPixelFormat::BGRA:
+ mFormat = VideoPixelFormat::BGRX;
+ return;
+ case VideoPixelFormat::I420:
+ case VideoPixelFormat::I422:
+ case VideoPixelFormat::I444:
+ case VideoPixelFormat::NV12:
+ case VideoPixelFormat::RGBX:
+ case VideoPixelFormat::BGRX:
+ return;
+ case VideoPixelFormat::EndGuard_:
+ break;
+ }
+ MOZ_ASSERT_UNREACHABLE("unsupported format");
+}
+
+nsTArray<VideoFrame::Format::Plane> VideoFrame::Format::Planes() const {
+ switch (mFormat) {
+ case VideoPixelFormat::I420:
+ case VideoPixelFormat::I422:
+ case VideoPixelFormat::I444:
+ return {Plane::Y, Plane::U, Plane::V};
+ case VideoPixelFormat::I420A:
+ return {Plane::Y, Plane::U, Plane::V, Plane::A};
+ case VideoPixelFormat::NV12:
+ return {Plane::Y, Plane::UV};
+ case VideoPixelFormat::RGBA:
+ case VideoPixelFormat::RGBX:
+ case VideoPixelFormat::BGRA:
+ case VideoPixelFormat::BGRX:
+ return {Plane::RGBA};
+ case VideoPixelFormat::EndGuard_:
+ break;
+ }
+ MOZ_ASSERT_UNREACHABLE("unsupported format");
+ return {};
+}
+
+const char* VideoFrame::Format::PlaneName(const Plane& aPlane) const {
+ switch (aPlane) {
+ case Format::Plane::Y: // and RGBA
+ return IsYUV() ? "Y" : "RGBA";
+ case Format::Plane::U: // and UV
+ MOZ_ASSERT(IsYUV());
+ return mFormat == VideoPixelFormat::NV12 ? "UV" : "U";
+ case Format::Plane::V:
+ MOZ_ASSERT(IsYUV());
+ return "V";
+ case Format::Plane::A:
+ MOZ_ASSERT(IsYUV());
+ return "A";
+ }
+ MOZ_ASSERT_UNREACHABLE("invalid plane");
+ return "Unknown";
+}
+
+uint32_t VideoFrame::Format::SampleBytes(const Plane& aPlane) const {
+ switch (mFormat) {
+ case VideoPixelFormat::I420:
+ case VideoPixelFormat::I420A:
+ case VideoPixelFormat::I422:
+ case VideoPixelFormat::I444:
+ return 1; // 8 bits/sample on the Y, U, V, A plane.
+ case VideoPixelFormat::NV12:
+ switch (aPlane) {
+ case Plane::Y:
+ return 1; // 8 bits/sample on the Y plane
+ case Plane::UV:
+ return 2; // Interleaved U and V values on the UV plane.
+ case Plane::V:
+ case Plane::A:
+ MOZ_ASSERT_UNREACHABLE("invalid plane");
+ }
+ return 0;
+ case VideoPixelFormat::RGBA:
+ case VideoPixelFormat::RGBX:
+ case VideoPixelFormat::BGRA:
+ case VideoPixelFormat::BGRX:
+ return 4; // 8 bits/sample, 32 bits/pixel
+ case VideoPixelFormat::EndGuard_:
+ break;
+ }
+ MOZ_ASSERT_UNREACHABLE("unsupported format");
+ return 0;
+}
+
+gfx::IntSize VideoFrame::Format::SampleSize(const Plane& aPlane) const {
+ // The sample width and height refers to
+ // https://w3c.github.io/webcodecs/#sub-sampling-factor
+ switch (aPlane) {
+ case Plane::Y: // and RGBA
+ case Plane::A:
+ return gfx::IntSize(1, 1);
+ case Plane::U: // and UV
+ case Plane::V:
+ switch (mFormat) {
+ case VideoPixelFormat::I420:
+ case VideoPixelFormat::I420A:
+ case VideoPixelFormat::NV12:
+ return gfx::IntSize(2, 2);
+ case VideoPixelFormat::I422:
+ return gfx::IntSize(2, 1);
+ case VideoPixelFormat::I444:
+ return gfx::IntSize(1, 1);
+ case VideoPixelFormat::RGBA:
+ case VideoPixelFormat::RGBX:
+ case VideoPixelFormat::BGRA:
+ case VideoPixelFormat::BGRX:
+ case VideoPixelFormat::EndGuard_:
+ MOZ_ASSERT_UNREACHABLE("invalid format");
+ return {0, 0};
+ }
+ }
+ MOZ_ASSERT_UNREACHABLE("invalid plane");
+ return {0, 0};
+}
+
+bool VideoFrame::Format::IsValidSize(const gfx::IntSize& aSize) const {
+ switch (mFormat) {
+ case VideoPixelFormat::I420:
+ case VideoPixelFormat::I420A:
+ case VideoPixelFormat::NV12:
+ return (aSize.Width() % 2 == 0) && (aSize.Height() % 2 == 0);
+ case VideoPixelFormat::I422:
+ return aSize.Height() % 2 == 0;
+ case VideoPixelFormat::I444:
+ case VideoPixelFormat::RGBA:
+ case VideoPixelFormat::RGBX:
+ case VideoPixelFormat::BGRA:
+ case VideoPixelFormat::BGRX:
+ return true;
+ case VideoPixelFormat::EndGuard_:
+ break;
+ }
+ MOZ_ASSERT_UNREACHABLE("unsupported format");
+ return false;
+}
+
+size_t VideoFrame::Format::SampleCount(const gfx::IntSize& aSize) const {
+ MOZ_ASSERT(IsValidSize(aSize));
+
+ CheckedInt<size_t> count(aSize.Width());
+ count *= aSize.Height();
+
+ switch (mFormat) {
+ case VideoPixelFormat::I420:
+ case VideoPixelFormat::NV12:
+ return (count + count / 2).value();
+ case VideoPixelFormat::I420A:
+ return (count * 2 + count / 2).value();
+ case VideoPixelFormat::I422:
+ return (count * 2).value();
+ case VideoPixelFormat::I444:
+ return (count * 3).value();
+ case VideoPixelFormat::RGBA:
+ case VideoPixelFormat::RGBX:
+ case VideoPixelFormat::BGRA:
+ case VideoPixelFormat::BGRX:
+ return (count * 4).value();
+ case VideoPixelFormat::EndGuard_:
+ break;
+ }
+
+ MOZ_ASSERT_UNREACHABLE("unsupported format");
+ return 0;
+}
+
+bool VideoFrame::Format::IsYUV() const { return IsYUVFormat(mFormat); }
+
+/*
+ * VideoFrame::Resource
+ */
+
+VideoFrame::Resource::Resource(const RefPtr<layers::Image>& aImage,
+ const class Format& aFormat)
+ : mImage(aImage), mFormat(aFormat) {
+ MOZ_ASSERT(mImage);
+}
+
+VideoFrame::Resource::Resource(const Resource& aOther)
+ : mImage(aOther.mImage), mFormat(aOther.mFormat) {
+ MOZ_ASSERT(mImage);
+}
+
+uint32_t VideoFrame::Resource::Stride(const Format::Plane& aPlane) const {
+ CheckedInt<uint32_t> width(mImage->GetSize().Width());
+ switch (aPlane) {
+ case Format::Plane::Y: // and RGBA
+ case Format::Plane::A:
+ switch (mFormat.PixelFormat()) {
+ case VideoPixelFormat::I420:
+ case VideoPixelFormat::I420A:
+ case VideoPixelFormat::I422:
+ case VideoPixelFormat::I444:
+ case VideoPixelFormat::NV12:
+ case VideoPixelFormat::RGBA:
+ case VideoPixelFormat::RGBX:
+ case VideoPixelFormat::BGRA:
+ case VideoPixelFormat::BGRX:
+ return (width * mFormat.SampleBytes(aPlane)).value();
+ case VideoPixelFormat::EndGuard_:
+ MOZ_ASSERT_UNREACHABLE("invalid format");
+ }
+ return 0;
+ case Format::Plane::U: // and UV
+ case Format::Plane::V:
+ switch (mFormat.PixelFormat()) {
+ case VideoPixelFormat::I420:
+ case VideoPixelFormat::I420A:
+ case VideoPixelFormat::I422:
+ case VideoPixelFormat::I444:
+ case VideoPixelFormat::NV12:
+ return (((width + 1) / 2) * mFormat.SampleBytes(aPlane)).value();
+ case VideoPixelFormat::RGBA:
+ case VideoPixelFormat::RGBX:
+ case VideoPixelFormat::BGRA:
+ case VideoPixelFormat::BGRX:
+ case VideoPixelFormat::EndGuard_:
+ MOZ_ASSERT_UNREACHABLE("invalid format");
+ }
+ return 0;
+ }
+ MOZ_ASSERT_UNREACHABLE("invalid plane");
+ return 0;
+}
+
+bool VideoFrame::Resource::CopyTo(const Format::Plane& aPlane,
+ const gfx::IntRect& aRect,
+ Span<uint8_t>&& aPlaneDest,
+ size_t aDestinationStride) const {
+ auto copyPlane = [&](const uint8_t* aPlaneData) {
+ MOZ_ASSERT(aPlaneData);
+
+ CheckedInt<size_t> offset(aRect.Y());
+ offset *= Stride(aPlane);
+ offset += aRect.X() * mFormat.SampleBytes(aPlane);
+ if (!offset.isValid()) {
+ return false;
+ }
+
+ CheckedInt<size_t> elementsBytes(aRect.Width());
+ elementsBytes *= mFormat.SampleBytes(aPlane);
+ if (!elementsBytes.isValid()) {
+ return false;
+ }
+
+ aPlaneData += offset.value();
+ for (int32_t row = 0; row < aRect.Height(); ++row) {
+ PodCopy(aPlaneDest.data(), aPlaneData, elementsBytes.value());
+ aPlaneData += Stride(aPlane);
+ // Spec asks to move `aDestinationStride` bytes instead of
+ // `Stride(aPlane)` forward.
+ aPlaneDest = aPlaneDest.From(aDestinationStride);
+ }
+ return true;
+ };
+
+ if (mImage->GetFormat() == ImageFormat::MOZ2D_SURFACE) {
+ RefPtr<gfx::SourceSurface> surface = mImage->GetAsSourceSurface();
+ if (NS_WARN_IF(!surface)) {
+ return false;
+ }
+
+ RefPtr<gfx::DataSourceSurface> dataSurface = surface->GetDataSurface();
+ if (NS_WARN_IF(!surface)) {
+ return false;
+ }
+
+ gfx::DataSourceSurface::ScopedMap map(dataSurface,
+ gfx::DataSourceSurface::READ);
+ if (NS_WARN_IF(!map.IsMapped())) {
+ return false;
+ }
+
+ const gfx::SurfaceFormat format = dataSurface->GetFormat();
+
+ if (format == gfx::SurfaceFormat::R8G8B8A8 ||
+ format == gfx::SurfaceFormat::R8G8B8X8 ||
+ format == gfx::SurfaceFormat::B8G8R8A8 ||
+ format == gfx::SurfaceFormat::B8G8R8X8) {
+ MOZ_ASSERT(aPlane == Format::Plane::RGBA);
+
+ // The mImage's format can be different from mFormat (since Gecko prefers
+ // BGRA). To get the data in the matched format, we create a temp buffer
+ // holding the image data in that format and then copy them to
+ // `aDestination`.
+ const gfx::SurfaceFormat f = mFormat.ToSurfaceFormat();
+ MOZ_ASSERT(f == gfx::SurfaceFormat::R8G8B8A8 ||
+ f == gfx::SurfaceFormat::R8G8B8X8 ||
+ f == gfx::SurfaceFormat::B8G8R8A8 ||
+ f == gfx::SurfaceFormat::B8G8R8X8);
+
+ // TODO: We could use Factory::CreateWrappingDataSourceSurface to wrap
+ // `aDestination` to avoid extra copy.
+ RefPtr<gfx::DataSourceSurface> tempSurface =
+ gfx::Factory::CreateDataSourceSurfaceWithStride(
+ dataSurface->GetSize(), f, map.GetStride());
+ if (NS_WARN_IF(!tempSurface)) {
+ return false;
+ }
+
+ gfx::DataSourceSurface::ScopedMap tempMap(tempSurface,
+ gfx::DataSourceSurface::WRITE);
+ if (NS_WARN_IF(!tempMap.IsMapped())) {
+ return false;
+ }
+
+ if (!gfx::SwizzleData(map.GetData(), map.GetStride(),
+ dataSurface->GetFormat(), tempMap.GetData(),
+ tempMap.GetStride(), tempSurface->GetFormat(),
+ tempSurface->GetSize())) {
+ return false;
+ }
+
+ return copyPlane(tempMap.GetData());
+ }
+
+ return false;
+ }
+
+ if (mImage->GetFormat() == ImageFormat::PLANAR_YCBCR) {
+ switch (aPlane) {
+ case Format::Plane::Y:
+ return copyPlane(mImage->AsPlanarYCbCrImage()->GetData()->mYChannel);
+ case Format::Plane::U:
+ return copyPlane(mImage->AsPlanarYCbCrImage()->GetData()->mCbChannel);
+ case Format::Plane::V:
+ return copyPlane(mImage->AsPlanarYCbCrImage()->GetData()->mCrChannel);
+ case Format::Plane::A:
+ MOZ_ASSERT(mFormat.PixelFormat() == VideoPixelFormat::I420A);
+ MOZ_ASSERT(mImage->AsPlanarYCbCrImage()->GetData()->mAlpha);
+ return copyPlane(
+ mImage->AsPlanarYCbCrImage()->GetData()->mAlpha->mChannel);
+ }
+ MOZ_ASSERT_UNREACHABLE("invalid plane");
+ }
+
+ if (mImage->GetFormat() == ImageFormat::NV_IMAGE) {
+ switch (aPlane) {
+ case Format::Plane::Y:
+ return copyPlane(mImage->AsNVImage()->GetData()->mYChannel);
+ case Format::Plane::UV:
+ return copyPlane(mImage->AsNVImage()->GetData()->mCbChannel);
+ case Format::Plane::V:
+ case Format::Plane::A:
+ MOZ_ASSERT_UNREACHABLE("invalid plane");
+ }
+ }
+
+ return false;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webcodecs/VideoFrame.h b/dom/media/webcodecs/VideoFrame.h
new file mode 100644
index 0000000000..13ee39c0c6
--- /dev/null
+++ b/dom/media/webcodecs/VideoFrame.h
@@ -0,0 +1,248 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_VideoFrame_h
+#define mozilla_dom_VideoFrame_h
+
+#include "js/TypeDecls.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/NotNull.h"
+#include "mozilla/Span.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/TypedArray.h"
+#include "mozilla/dom/VideoColorSpaceBinding.h"
+#include "mozilla/gfx/Point.h"
+#include "mozilla/gfx/Rect.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsWrapperCache.h"
+
+class nsIGlobalObject;
+class nsIURI;
+
+namespace mozilla {
+
+namespace layers {
+class Image;
+} // namespace layers
+
+namespace dom {
+
+class DOMRectReadOnly;
+class HTMLCanvasElement;
+class HTMLImageElement;
+class HTMLVideoElement;
+class ImageBitmap;
+class MaybeSharedArrayBufferViewOrMaybeSharedArrayBuffer;
+class OffscreenCanvas;
+class OwningMaybeSharedArrayBufferViewOrMaybeSharedArrayBuffer;
+class Promise;
+class SVGImageElement;
+class StructuredCloneHolder;
+class VideoColorSpace;
+class VideoFrame;
+enum class VideoPixelFormat : uint8_t;
+struct VideoFrameBufferInit;
+struct VideoFrameInit;
+struct VideoFrameCopyToOptions;
+
+} // namespace dom
+} // namespace mozilla
+
+namespace mozilla::dom {
+
+struct VideoFrameData {
+ VideoFrameData(layers::Image* aImage, const VideoPixelFormat& aFormat,
+ gfx::IntRect aVisibleRect, gfx::IntSize aDisplaySize,
+ Maybe<uint64_t> aDuration, int64_t aTimestamp,
+ const VideoColorSpaceInit& aColorSpace);
+
+ const RefPtr<layers::Image> mImage;
+ const VideoPixelFormat mFormat;
+ const gfx::IntRect mVisibleRect;
+ const gfx::IntSize mDisplaySize;
+ const Maybe<uint64_t> mDuration;
+ const int64_t mTimestamp;
+ const VideoColorSpaceInit mColorSpace;
+};
+
+struct VideoFrameSerializedData : VideoFrameData {
+ VideoFrameSerializedData(layers::Image* aImage,
+ const VideoPixelFormat& aFormat,
+ gfx::IntSize aCodedSize, gfx::IntRect aVisibleRect,
+ gfx::IntSize aDisplaySize, Maybe<uint64_t> aDuration,
+ int64_t aTimestamp,
+ const VideoColorSpaceInit& aColorSpace,
+ already_AddRefed<nsIURI> aPrincipalURI);
+
+ const gfx::IntSize mCodedSize;
+ const nsCOMPtr<nsIURI> mPrincipalURI;
+};
+
+class VideoFrame final : public nsISupports, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(VideoFrame)
+
+ public:
+ VideoFrame(nsIGlobalObject* aParent, const RefPtr<layers::Image>& aImage,
+ const VideoPixelFormat& aFormat, gfx::IntSize aCodedSize,
+ gfx::IntRect aVisibleRect, gfx::IntSize aDisplaySize,
+ const Maybe<uint64_t>& aDuration, int64_t aTimestamp,
+ const VideoColorSpaceInit& aColorSpace);
+
+ VideoFrame(const VideoFrame& aOther);
+
+ protected:
+ ~VideoFrame() = default;
+
+ public:
+ nsIGlobalObject* GetParentObject() const;
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ static already_AddRefed<VideoFrame> Constructor(
+ const GlobalObject& aGlobal, HTMLImageElement& aImageElement,
+ const VideoFrameInit& aInit, ErrorResult& aRv);
+ static already_AddRefed<VideoFrame> Constructor(
+ const GlobalObject& aGlobal, SVGImageElement& aSVGImageElement,
+ const VideoFrameInit& aInit, ErrorResult& aRv);
+ static already_AddRefed<VideoFrame> Constructor(
+ const GlobalObject& aGlobal, HTMLCanvasElement& aCanvasElement,
+ const VideoFrameInit& aInit, ErrorResult& aRv);
+ static already_AddRefed<VideoFrame> Constructor(
+ const GlobalObject& aGlobal, HTMLVideoElement& aVideoElement,
+ const VideoFrameInit& aInit, ErrorResult& aRv);
+ static already_AddRefed<VideoFrame> Constructor(
+ const GlobalObject& aGlobal, OffscreenCanvas& aOffscreenCanvas,
+ const VideoFrameInit& aInit, ErrorResult& aRv);
+ static already_AddRefed<VideoFrame> Constructor(const GlobalObject& aGlobal,
+ ImageBitmap& aImageBitmap,
+ const VideoFrameInit& aInit,
+ ErrorResult& aRv);
+ static already_AddRefed<VideoFrame> Constructor(const GlobalObject& aGlobal,
+ VideoFrame& aVideoFrame,
+ const VideoFrameInit& aInit,
+ ErrorResult& aRv);
+ static already_AddRefed<VideoFrame> Constructor(
+ const GlobalObject& aGlobal, const ArrayBufferView& aBufferView,
+ const VideoFrameBufferInit& aInit, ErrorResult& aRv);
+ static already_AddRefed<VideoFrame> Constructor(
+ const GlobalObject& aGlobal, const ArrayBuffer& aBuffer,
+ const VideoFrameBufferInit& aInit, ErrorResult& aRv);
+
+ Nullable<VideoPixelFormat> GetFormat() const;
+
+ uint32_t CodedWidth() const;
+
+ uint32_t CodedHeight() const;
+
+ already_AddRefed<DOMRectReadOnly> GetCodedRect() const;
+
+ already_AddRefed<DOMRectReadOnly> GetVisibleRect() const;
+
+ uint32_t DisplayWidth() const;
+
+ uint32_t DisplayHeight() const;
+
+ Nullable<uint64_t> GetDuration() const;
+
+ int64_t Timestamp() const;
+
+ already_AddRefed<VideoColorSpace> ColorSpace() const;
+
+ uint32_t AllocationSize(const VideoFrameCopyToOptions& aOptions,
+ ErrorResult& aRv);
+
+ already_AddRefed<Promise> CopyTo(
+ const MaybeSharedArrayBufferViewOrMaybeSharedArrayBuffer& aDestination,
+ const VideoFrameCopyToOptions& aOptions, ErrorResult& aRv);
+
+ already_AddRefed<VideoFrame> Clone(ErrorResult& aRv);
+
+ void Close();
+
+ // [Serializable] implementations: {Read, Write}StructuredClone
+ static JSObject* ReadStructuredClone(JSContext* aCx, nsIGlobalObject* aGlobal,
+ JSStructuredCloneReader* aReader,
+ const VideoFrameSerializedData& aData);
+
+ bool WriteStructuredClone(JSStructuredCloneWriter* aWriter,
+ StructuredCloneHolder* aHolder) const;
+
+ // [Transferable] implementations: Transfer, FromTransferred
+ using TransferredData = VideoFrameSerializedData;
+
+ UniquePtr<TransferredData> Transfer();
+
+ static already_AddRefed<VideoFrame> FromTransferred(nsIGlobalObject* aGlobal,
+ TransferredData* aData);
+
+ public:
+ // A VideoPixelFormat wrapper providing utilities for VideoFrame.
+ class Format final {
+ public:
+ explicit Format(const VideoPixelFormat& aFormat);
+ ~Format() = default;
+ const VideoPixelFormat& PixelFormat() const;
+ gfx::SurfaceFormat ToSurfaceFormat() const;
+ void MakeOpaque();
+
+ // TODO: Assign unique value for each plane?
+ // The value indicates the order of the plane in format.
+ enum class Plane : uint8_t { Y = 0, RGBA = Y, U = 1, UV = U, V = 2, A = 3 };
+ nsTArray<Plane> Planes() const;
+ const char* PlaneName(const Plane& aPlane) const;
+ uint32_t SampleBytes(const Plane& aPlane) const;
+ gfx::IntSize SampleSize(const Plane& aPlane) const;
+ bool IsValidSize(const gfx::IntSize& aSize) const;
+ size_t SampleCount(const gfx::IntSize& aSize) const;
+
+ private:
+ bool IsYUV() const;
+ VideoPixelFormat mFormat;
+ };
+
+ private:
+ // VideoFrame can run on either main thread or worker thread.
+ void AssertIsOnOwningThread() const { NS_ASSERT_OWNINGTHREAD(VideoFrame); }
+
+ already_AddRefed<nsIURI> GetPrincipalURI() const;
+
+ // A class representing the VideoFrame's data.
+ class Resource final {
+ public:
+ Resource(const RefPtr<layers::Image>& aImage, const Format& aFormat);
+ Resource(const Resource& aOther);
+ ~Resource() = default;
+ uint32_t Stride(const Format::Plane& aPlane) const;
+ bool CopyTo(const Format::Plane& aPlane, const gfx::IntRect& aRect,
+ Span<uint8_t>&& aPlaneDest, size_t aDestinationStride) const;
+
+ const RefPtr<layers::Image> mImage;
+ const Format mFormat;
+ };
+
+ nsCOMPtr<nsIGlobalObject> mParent;
+
+ // Use Maybe instead of UniquePtr to allow copy ctor.
+ // The mResource's existence is used as the [[Detached]] for [Transferable].
+ Maybe<const Resource> mResource; // Nothing() after `Close()`d
+
+ // TODO: Replace this by mResource->mImage->GetSize()?
+ gfx::IntSize mCodedSize;
+ gfx::IntRect mVisibleRect;
+ gfx::IntSize mDisplaySize;
+
+ Maybe<uint64_t> mDuration; // Nothing() after `Close()`d
+ int64_t mTimestamp;
+ VideoColorSpaceInit mColorSpace;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_VideoFrame_h
diff --git a/dom/media/webcodecs/moz.build b/dom/media/webcodecs/moz.build
new file mode 100644
index 0000000000..551ad35c6f
--- /dev/null
+++ b/dom/media/webcodecs/moz.build
@@ -0,0 +1,19 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+MOCHITEST_MANIFESTS += ["test/mochitest.ini"]
+
+EXPORTS.mozilla.dom += [
+ "VideoColorSpace.h",
+ "VideoFrame.h",
+]
+
+UNIFIED_SOURCES += [
+ "VideoColorSpace.cpp",
+ "VideoFrame.cpp",
+]
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/webcodecs/test/mochitest.ini b/dom/media/webcodecs/test/mochitest.ini
new file mode 100644
index 0000000000..3d0268cc9d
--- /dev/null
+++ b/dom/media/webcodecs/test/mochitest.ini
@@ -0,0 +1,7 @@
+[DEFAULT]
+subsuite = media
+tags = webcodecs
+prefs =
+ dom.media.webcodecs.enabled=true
+
+[test_videoFrame_mismatched_codedSize.html]
diff --git a/dom/media/webcodecs/test/test_videoFrame_mismatched_codedSize.html b/dom/media/webcodecs/test/test_videoFrame_mismatched_codedSize.html
new file mode 100644
index 0000000000..52e26f7854
--- /dev/null
+++ b/dom/media/webcodecs/test/test_videoFrame_mismatched_codedSize.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title></title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script>
+let data = new Uint8Array([
+ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
+]);
+// TODO: Crop the image instead of returning errors (Bug 1782128).
+try {
+ // Bug 1793814: Remove eslint-disable-line below
+ let frame = new VideoFrame(data, { // eslint-disable-line no-undef
+ timestamp: 10,
+ codedWidth: 3,
+ codedHeight: 3,
+ visibleRect: { x: 0, y: 0, width: 1, height: 1 },
+ format: "RGBA",
+ });
+ frame.close();
+ ok(false, "Should not create a VideoFrame from a mismatched-size buffer");
+} catch (e) {
+ ok(e instanceof TypeError, "Should throw a TypeError");
+}
+</script>
+</body>
+</html>
diff --git a/dom/media/webm/EbmlComposer.cpp b/dom/media/webm/EbmlComposer.cpp
new file mode 100644
index 0000000000..e3f04fd89b
--- /dev/null
+++ b/dom/media/webm/EbmlComposer.cpp
@@ -0,0 +1,185 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "EbmlComposer.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/EndianUtils.h"
+#include "libmkv/EbmlIDs.h"
+#include "libmkv/EbmlWriter.h"
+#include "libmkv/WebMElement.h"
+#include "prtime.h"
+#include "limits.h"
+
+namespace mozilla {
+
+// Timecode scale in nanoseconds
+constexpr unsigned long TIME_CODE_SCALE = 1000000;
+// The WebM header size without audio CodecPrivateData
+constexpr int32_t DEFAULT_HEADER_SIZE = 1024;
+// Number of milliseconds after which we flush audio-only clusters
+constexpr int32_t FLUSH_AUDIO_ONLY_AFTER_MS = 1000;
+
+void EbmlComposer::GenerateHeader() {
+ MOZ_RELEASE_ASSERT(!mMetadataFinished);
+ MOZ_RELEASE_ASSERT(mHasAudio || mHasVideo);
+
+ // Write the EBML header.
+ EbmlGlobal ebml;
+ // The WEbM header default size usually smaller than 1k.
+ auto buffer =
+ MakeUnique<uint8_t[]>(DEFAULT_HEADER_SIZE + mCodecPrivateData.Length());
+ ebml.buf = buffer.get();
+ ebml.offset = 0;
+ writeHeader(&ebml);
+ {
+ EbmlLoc segEbmlLoc, ebmlLocseg, ebmlLoc;
+ Ebml_StartSubElement(&ebml, &segEbmlLoc, Segment);
+ {
+ Ebml_StartSubElement(&ebml, &ebmlLocseg, SeekHead);
+ // Todo: We don't know the exact sizes of encoded data and
+ // ignore this section.
+ Ebml_EndSubElement(&ebml, &ebmlLocseg);
+ writeSegmentInformation(&ebml, &ebmlLoc, TIME_CODE_SCALE, 0);
+ {
+ EbmlLoc trackLoc;
+ Ebml_StartSubElement(&ebml, &trackLoc, Tracks);
+ {
+ // Video
+ if (mWidth > 0 && mHeight > 0) {
+ writeVideoTrack(&ebml, 0x1, 0, "V_VP8", mWidth, mHeight,
+ mDisplayWidth, mDisplayHeight);
+ }
+ // Audio
+ if (mCodecPrivateData.Length() > 0) {
+ // Extract the pre-skip from mCodecPrivateData
+ // then convert it to nanoseconds.
+ // For more details see
+ // https://tools.ietf.org/html/rfc7845#section-4.2
+ uint64_t codecDelay = (uint64_t)LittleEndian::readUint16(
+ mCodecPrivateData.Elements() + 10) *
+ PR_NSEC_PER_SEC / 48000;
+ // Fixed 80ms, convert into nanoseconds.
+ uint64_t seekPreRoll = 80 * PR_NSEC_PER_MSEC;
+ writeAudioTrack(&ebml, 0x2, 0x0, "A_OPUS", mSampleFreq, mChannels,
+ codecDelay, seekPreRoll,
+ mCodecPrivateData.Elements(),
+ mCodecPrivateData.Length());
+ }
+ }
+ Ebml_EndSubElement(&ebml, &trackLoc);
+ }
+ }
+ // The Recording length is unknown and
+ // ignore write the whole Segment element size
+ }
+ MOZ_ASSERT(ebml.offset <= DEFAULT_HEADER_SIZE + mCodecPrivateData.Length(),
+ "write more data > EBML_BUFFER_SIZE");
+ auto block = mBuffer.AppendElement();
+ block->SetLength(ebml.offset);
+ memcpy(block->Elements(), ebml.buf, ebml.offset);
+ mMetadataFinished = true;
+}
+
+nsresult EbmlComposer::WriteSimpleBlock(EncodedFrame* aFrame) {
+ MOZ_RELEASE_ASSERT(mMetadataFinished);
+ auto frameType = aFrame->mFrameType;
+ const bool isVP8IFrame = (frameType == EncodedFrame::FrameType::VP8_I_FRAME);
+ const bool isVP8PFrame = (frameType == EncodedFrame::FrameType::VP8_P_FRAME);
+ const bool isOpus = (frameType == EncodedFrame::FrameType::OPUS_AUDIO_FRAME);
+
+ MOZ_ASSERT_IF(isVP8IFrame, mHasVideo);
+ MOZ_ASSERT_IF(isVP8PFrame, mHasVideo);
+ MOZ_ASSERT_IF(isOpus, mHasAudio);
+
+ if (isVP8PFrame && !mHasWrittenCluster) {
+ // We ensure there is a cluster header and an I-frame prior to any P-frame.
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ int64_t timeCode = aFrame->mTime.ToMicroseconds() / PR_USEC_PER_MSEC -
+ mCurrentClusterTimecode;
+
+ const bool needClusterHeader =
+ !mHasWrittenCluster ||
+ (!mHasVideo && timeCode >= FLUSH_AUDIO_ONLY_AFTER_MS) || isVP8IFrame;
+
+ auto block = mBuffer.AppendElement();
+ block->SetLength(aFrame->mFrameData->Length() + DEFAULT_HEADER_SIZE);
+
+ EbmlGlobal ebml;
+ ebml.offset = 0;
+ ebml.buf = block->Elements();
+
+ if (needClusterHeader) {
+ mHasWrittenCluster = true;
+ EbmlLoc ebmlLoc;
+ // This starts the Cluster element. Note that we never end this element
+ // through Ebml_EndSubElement. What the ending would allow us to do is write
+ // the full length of the cluster in the element header. That would also
+ // force us to keep the entire cluster in memory until we know where it
+ // ends. Now it instead ends through the start of the next cluster. This
+ // allows us to stream the muxed data with much lower latency than if we
+ // would have to wait for clusters to end.
+ Ebml_StartSubElement(&ebml, &ebmlLoc, Cluster);
+ // if timeCode didn't under/overflow before, it shouldn't after this
+ mCurrentClusterTimecode = aFrame->mTime.ToMicroseconds() / PR_USEC_PER_MSEC;
+ Ebml_SerializeUnsigned(&ebml, Timecode, mCurrentClusterTimecode);
+
+ // Can't under-/overflow now
+ timeCode = 0;
+ }
+
+ if (MOZ_UNLIKELY(timeCode < SHRT_MIN || timeCode > SHRT_MAX)) {
+ MOZ_CRASH_UNSAFE_PRINTF(
+ "Invalid cluster timecode! audio=%d, video=%d, timeCode=%" PRId64
+ "ms, currentClusterTimecode=%" PRIu64 "ms",
+ mHasAudio, mHasVideo, timeCode, mCurrentClusterTimecode);
+ }
+
+ writeSimpleBlock(&ebml, isOpus ? 0x2 : 0x1, static_cast<short>(timeCode),
+ isVP8IFrame, 0, 0,
+ (unsigned char*)aFrame->mFrameData->Elements(),
+ aFrame->mFrameData->Length());
+ MOZ_ASSERT(ebml.offset <= DEFAULT_HEADER_SIZE + aFrame->mFrameData->Length(),
+ "write more data > EBML_BUFFER_SIZE");
+ block->SetLength(ebml.offset);
+
+ return NS_OK;
+}
+
+void EbmlComposer::SetVideoConfig(uint32_t aWidth, uint32_t aHeight,
+ uint32_t aDisplayWidth,
+ uint32_t aDisplayHeight) {
+ MOZ_RELEASE_ASSERT(!mMetadataFinished);
+ MOZ_ASSERT(aWidth > 0, "Width should > 0");
+ MOZ_ASSERT(aHeight > 0, "Height should > 0");
+ MOZ_ASSERT(aDisplayWidth > 0, "DisplayWidth should > 0");
+ MOZ_ASSERT(aDisplayHeight > 0, "DisplayHeight should > 0");
+ mWidth = aWidth;
+ mHeight = aHeight;
+ mDisplayWidth = aDisplayWidth;
+ mDisplayHeight = aDisplayHeight;
+ mHasVideo = true;
+}
+
+void EbmlComposer::SetAudioConfig(uint32_t aSampleFreq, uint32_t aChannels) {
+ MOZ_RELEASE_ASSERT(!mMetadataFinished);
+ MOZ_ASSERT(aSampleFreq > 0, "SampleFreq should > 0");
+ MOZ_ASSERT(aChannels > 0, "Channels should > 0");
+ mSampleFreq = aSampleFreq;
+ mChannels = aChannels;
+ mHasAudio = true;
+}
+
+void EbmlComposer::ExtractBuffer(nsTArray<nsTArray<uint8_t> >* aDestBufs,
+ uint32_t aFlag) {
+ if (!mMetadataFinished) {
+ return;
+ }
+ aDestBufs->AppendElements(std::move(mBuffer));
+ MOZ_ASSERT(mBuffer.IsEmpty());
+}
+
+} // namespace mozilla
diff --git a/dom/media/webm/EbmlComposer.h b/dom/media/webm/EbmlComposer.h
new file mode 100644
index 0000000000..a037e4ef8b
--- /dev/null
+++ b/dom/media/webm/EbmlComposer.h
@@ -0,0 +1,81 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef EbmlComposer_h_
+#define EbmlComposer_h_
+#include "nsTArray.h"
+#include "ContainerWriter.h"
+
+namespace mozilla {
+
+/*
+ * A WebM muxer helper for package the valid WebM format.
+ */
+class EbmlComposer {
+ public:
+ EbmlComposer() = default;
+ /*
+ * Assign the parameters which header requires. These can be called multiple
+ * times to change paramter values until GenerateHeader() is called, when this
+ * becomes illegal to call again.
+ */
+ void SetVideoConfig(uint32_t aWidth, uint32_t aHeight, uint32_t aDisplayWidth,
+ uint32_t aDisplayHeight);
+ void SetAudioConfig(uint32_t aSampleFreq, uint32_t aChannels);
+ /*
+ * Set the CodecPrivateData for writing in header.
+ */
+ void SetAudioCodecPrivateData(nsTArray<uint8_t>& aBufs) {
+ mCodecPrivateData.AppendElements(aBufs);
+ }
+ /*
+ * Generate the whole WebM header with the configured tracks, and make
+ * available to ExtractBuffer. Must only be called once.
+ */
+ void GenerateHeader();
+ /*
+ * Insert media encoded buffer into muxer and it would be package
+ * into SimpleBlock. If no cluster is opened, new cluster will start for
+ * writing. Frames passed to this function should already have any codec delay
+ * applied.
+ */
+ nsresult WriteSimpleBlock(EncodedFrame* aFrame);
+ /*
+ * Get valid cluster data.
+ */
+ void ExtractBuffer(nsTArray<nsTArray<uint8_t>>* aDestBufs,
+ uint32_t aFlag = 0);
+
+ private:
+ // True once we have written the first cluster header. We cannot serialize any
+ // P-frames until this is true, since we start a new cluster every I-frame.
+ bool mHasWrittenCluster = false;
+ // The timecode of the cluster.
+ uint64_t mCurrentClusterTimecode = 0;
+
+ // Written data to be flushed out by ExtractBuffer().
+ nsTArray<nsTArray<uint8_t>> mBuffer;
+
+ // True when Metadata has been serialized into mBuffer.
+ bool mMetadataFinished = false;
+
+ // Video configuration
+ int mWidth = 0;
+ int mHeight = 0;
+ int mDisplayWidth = 0;
+ int mDisplayHeight = 0;
+ bool mHasVideo = false;
+
+ // Audio configuration
+ float mSampleFreq = 0;
+ int mChannels = 0;
+ bool mHasAudio = false;
+ // Audio codec specific header data.
+ nsTArray<uint8_t> mCodecPrivateData;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webm/NesteggPacketHolder.h b/dom/media/webm/NesteggPacketHolder.h
new file mode 100644
index 0000000000..7c74f752d3
--- /dev/null
+++ b/dom/media/webm/NesteggPacketHolder.h
@@ -0,0 +1,135 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#if !defined(NesteggPacketHolder_h_)
+# define NesteggPacketHolder_h_
+
+# include <deque>
+# include <stdint.h>
+# include "nsAutoRef.h"
+# include "nestegg/nestegg.h"
+
+namespace mozilla {
+
+// Holds a nestegg_packet, and its file offset. This is needed so we
+// know the offset in the file we've played up to, in order to calculate
+// whether it's likely we can play through to the end without needing
+// to stop to buffer, given the current download rate.
+class NesteggPacketHolder {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(NesteggPacketHolder)
+ NesteggPacketHolder()
+ : mPacket(nullptr),
+ mOffset(-1),
+ mTimestamp(-1),
+ mDuration(-1),
+ mTrack(0),
+ mIsKeyframe(false) {}
+
+ bool Init(nestegg_packet* aPacket, int64_t aOffset, unsigned aTrack,
+ bool aIsKeyframe) {
+ uint64_t timestamp_ns;
+ if (nestegg_packet_tstamp(aPacket, &timestamp_ns) == -1) {
+ return false;
+ }
+
+ // We store the timestamp as signed microseconds so that it's easily
+ // comparable to other timestamps we have in the system.
+ mTimestamp = timestamp_ns / 1000;
+ mPacket = aPacket;
+ mOffset = aOffset;
+ mTrack = aTrack;
+ mIsKeyframe = aIsKeyframe;
+
+ uint64_t duration_ns;
+ if (!nestegg_packet_duration(aPacket, &duration_ns)) {
+ mDuration = duration_ns / 1000;
+ }
+ return true;
+ }
+
+ nestegg_packet* Packet() {
+ MOZ_ASSERT(IsInitialized());
+ return mPacket;
+ }
+ int64_t Offset() {
+ MOZ_ASSERT(IsInitialized());
+ return mOffset;
+ }
+ int64_t Timestamp() {
+ MOZ_ASSERT(IsInitialized());
+ return mTimestamp;
+ }
+ int64_t Duration() {
+ MOZ_ASSERT(IsInitialized());
+ return mDuration;
+ }
+ unsigned Track() {
+ MOZ_ASSERT(IsInitialized());
+ return mTrack;
+ }
+ bool IsKeyframe() {
+ MOZ_ASSERT(IsInitialized());
+ return mIsKeyframe;
+ }
+
+ private:
+ ~NesteggPacketHolder() { nestegg_free_packet(mPacket); }
+
+ bool IsInitialized() { return mOffset >= 0; }
+
+ nestegg_packet* mPacket;
+
+ // Offset in bytes. This is the offset of the end of the Block
+ // which contains the packet.
+ int64_t mOffset;
+
+ // Packet presentation timestamp in microseconds.
+ int64_t mTimestamp;
+
+ // Packet duration in microseconds; -1 if unknown or retrieval failed.
+ int64_t mDuration;
+
+ // Track ID.
+ unsigned mTrack;
+
+ // Does this packet contain a keyframe?
+ bool mIsKeyframe;
+
+ // Copy constructor and assignment operator not implemented. Don't use them!
+ NesteggPacketHolder(const NesteggPacketHolder& aOther);
+ NesteggPacketHolder& operator=(NesteggPacketHolder const& aOther);
+};
+
+// Queue for holding nestegg packets.
+class WebMPacketQueue {
+ public:
+ int32_t GetSize() { return mQueue.size(); }
+
+ void Push(NesteggPacketHolder* aItem) { mQueue.push_back(aItem); }
+
+ void PushFront(NesteggPacketHolder* aItem) {
+ mQueue.push_front(std::move(aItem));
+ }
+
+ already_AddRefed<NesteggPacketHolder> PopFront() {
+ RefPtr<NesteggPacketHolder> result = std::move(mQueue.front());
+ mQueue.pop_front();
+ return result.forget();
+ }
+
+ void Reset() {
+ while (!mQueue.empty()) {
+ mQueue.pop_front();
+ }
+ }
+
+ private:
+ std::deque<RefPtr<NesteggPacketHolder>> mQueue;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webm/WebMBufferedParser.cpp b/dom/media/webm/WebMBufferedParser.cpp
new file mode 100644
index 0000000000..114fd7df89
--- /dev/null
+++ b/dom/media/webm/WebMBufferedParser.cpp
@@ -0,0 +1,676 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WebMBufferedParser.h"
+
+#include <algorithm>
+
+#include "mozilla/CheckedInt.h"
+#include "nsAlgorithm.h"
+#include "nsThreadUtils.h"
+
+extern mozilla::LazyLogModule gMediaDemuxerLog;
+#define WEBM_DEBUG(arg, ...) \
+ MOZ_LOG(gMediaDemuxerLog, mozilla::LogLevel::Debug, \
+ ("WebMBufferedParser(%p)::%s: " arg, this, __func__, ##__VA_ARGS__))
+
+namespace mozilla {
+
+static uint32_t VIntLength(unsigned char aFirstByte, uint32_t* aMask) {
+ uint32_t count = 1;
+ uint32_t mask = 1 << 7;
+ while (count < 8) {
+ if ((aFirstByte & mask) != 0) {
+ break;
+ }
+ mask >>= 1;
+ count += 1;
+ }
+ if (aMask) {
+ *aMask = mask;
+ }
+ NS_ASSERTION(count >= 1 && count <= 8, "Insane VInt length.");
+ return count;
+}
+
+constexpr uint8_t EBML_MAX_ID_LENGTH_DEFAULT = 4;
+constexpr uint8_t EBML_MAX_SIZE_LENGTH_DEFAULT = 8;
+
+WebMBufferedParser::WebMBufferedParser(int64_t aOffset)
+ : mStartOffset(aOffset),
+ mCurrentOffset(aOffset),
+ mInitEndOffset(-1),
+ mBlockEndOffset(-1),
+ mState(READ_ELEMENT_ID),
+ mNextState(READ_ELEMENT_ID),
+ mVIntRaw(false),
+ mLastInitStartOffset(-1),
+ mLastInitSize(0),
+ mEBMLMaxIdLength(EBML_MAX_ID_LENGTH_DEFAULT),
+ mEBMLMaxSizeLength(EBML_MAX_SIZE_LENGTH_DEFAULT),
+ mClusterSyncPos(0),
+ mVIntLeft(0),
+ mBlockSize(0),
+ mClusterTimecode(0),
+ mClusterOffset(-1),
+ mClusterEndOffset(-1),
+ mBlockOffset(0),
+ mBlockTimecode(0),
+ mBlockTimecodeLength(0),
+ mSkipBytes(0),
+ mTimecodeScale(1000000),
+ mGotTimecodeScale(false),
+ mGotClusterTimecode(false) {
+ if (mStartOffset != 0) {
+ mState = FIND_CLUSTER_SYNC;
+ }
+}
+
+MediaResult WebMBufferedParser::Append(const unsigned char* aBuffer,
+ uint32_t aLength,
+ nsTArray<WebMTimeDataOffset>& aMapping) {
+ static const uint32_t EBML_ID = 0x1a45dfa3;
+ static const uint32_t SEGMENT_ID = 0x18538067;
+ static const uint32_t SEGINFO_ID = 0x1549a966;
+ static const uint32_t TRACKS_ID = 0x1654AE6B;
+ static const uint32_t CLUSTER_ID = 0x1f43b675;
+ static const uint32_t TIMECODESCALE_ID = 0x2ad7b1;
+ static const unsigned char TIMECODE_ID = 0xe7;
+ static const unsigned char BLOCKGROUP_ID = 0xa0;
+ static const unsigned char BLOCK_ID = 0xa1;
+ static const unsigned char SIMPLEBLOCK_ID = 0xa3;
+ static const uint16_t EBML_MAX_ID_LENGTH_ID = 0x42f2;
+ static const uint16_t EBML_MAX_SIZE_LENGTH_ID = 0x42f3;
+ static const uint32_t BLOCK_TIMECODE_LENGTH = 2;
+
+ static const unsigned char CLUSTER_SYNC_ID[] = {0x1f, 0x43, 0xb6, 0x75};
+
+ const unsigned char* p = aBuffer;
+
+ // Parse each byte in aBuffer one-by-one, producing timecodes and updating
+ // aMapping as we go. Parser pauses at end of stream (which may be at any
+ // point within the parse) and resumes parsing the next time Append is
+ // called with new data.
+ while (p < aBuffer + aLength) {
+ switch (mState) {
+ case READ_ELEMENT_ID:
+ mVIntRaw = true;
+ mState = READ_VINT;
+ mNextState = READ_ELEMENT_SIZE;
+ break;
+ case READ_ELEMENT_SIZE:
+ if (mVInt.mLength > mEBMLMaxIdLength) {
+ nsPrintfCString detail("Invalid element id of length %" PRIu64,
+ mVInt.mLength);
+ WEBM_DEBUG("%s", detail.get());
+ return MediaResult(NS_ERROR_FAILURE, detail);
+ }
+ mVIntRaw = false;
+ mElement.mID = mVInt;
+ mState = READ_VINT;
+ mNextState = PARSE_ELEMENT;
+ break;
+ case FIND_CLUSTER_SYNC:
+ if (*p++ == CLUSTER_SYNC_ID[mClusterSyncPos]) {
+ mClusterSyncPos += 1;
+ } else {
+ mClusterSyncPos = 0;
+ }
+ if (mClusterSyncPos == sizeof(CLUSTER_SYNC_ID)) {
+ mVInt.mValue = CLUSTER_ID;
+ mVInt.mLength = sizeof(CLUSTER_SYNC_ID);
+ mState = READ_ELEMENT_SIZE;
+ }
+ break;
+ case PARSE_ELEMENT:
+ if (mVInt.mLength > mEBMLMaxSizeLength) {
+ nsPrintfCString detail("Invalid element size of length %" PRIu64,
+ mVInt.mLength);
+ WEBM_DEBUG("%s", detail.get());
+ return MediaResult(NS_ERROR_FAILURE, detail);
+ }
+ mElement.mSize = mVInt;
+ switch (mElement.mID.mValue) {
+ case SEGMENT_ID:
+ mState = READ_ELEMENT_ID;
+ break;
+ case SEGINFO_ID:
+ mGotTimecodeScale = true;
+ mState = READ_ELEMENT_ID;
+ break;
+ case TIMECODE_ID:
+ mVInt = VInt();
+ mVIntLeft = mElement.mSize.mValue;
+ mState = READ_VINT_REST;
+ mNextState = READ_CLUSTER_TIMECODE;
+ break;
+ case TIMECODESCALE_ID:
+ mVInt = VInt();
+ mVIntLeft = mElement.mSize.mValue;
+ mState = READ_VINT_REST;
+ mNextState = READ_TIMECODESCALE;
+ break;
+ case CLUSTER_ID:
+ mClusterOffset = mCurrentOffset + (p - aBuffer) -
+ (mElement.mID.mLength + mElement.mSize.mLength);
+ // Handle "unknown" length;
+ if (mElement.mSize.mValue + 1 !=
+ uint64_t(1) << (mElement.mSize.mLength * 7)) {
+ mClusterEndOffset = mClusterOffset + mElement.mID.mLength +
+ mElement.mSize.mLength +
+ mElement.mSize.mValue;
+ } else {
+ mClusterEndOffset = -1;
+ }
+ mGotClusterTimecode = false;
+ mState = READ_ELEMENT_ID;
+ break;
+ case BLOCKGROUP_ID:
+ mState = READ_ELEMENT_ID;
+ break;
+ case SIMPLEBLOCK_ID:
+ /* FALLTHROUGH */
+ case BLOCK_ID:
+ if (!mGotClusterTimecode) {
+ WEBM_DEBUG(
+ "The Timecode element must appear before any Block or "
+ "SimpleBlock elements in a Cluster");
+ return MediaResult(
+ NS_ERROR_FAILURE,
+ "The Timecode element must appear before any Block or "
+ "SimpleBlock elements in a Cluster");
+ }
+ mBlockSize = mElement.mSize.mValue;
+ mBlockTimecode = 0;
+ mBlockTimecodeLength = BLOCK_TIMECODE_LENGTH;
+ mBlockOffset = mCurrentOffset + (p - aBuffer) -
+ (mElement.mID.mLength + mElement.mSize.mLength);
+ mState = READ_VINT;
+ mNextState = READ_BLOCK_TIMECODE;
+ break;
+ case TRACKS_ID:
+ mSkipBytes = mElement.mSize.mValue;
+ mState = CHECK_INIT_FOUND;
+ break;
+ case EBML_MAX_ID_LENGTH_ID:
+ case EBML_MAX_SIZE_LENGTH_ID:
+ if (int64_t currentOffset = mCurrentOffset + (p - aBuffer);
+ currentOffset < mLastInitStartOffset ||
+ currentOffset >= mLastInitStartOffset + mLastInitSize) {
+ nsPrintfCString str("Unexpected %s outside init segment",
+ mElement.mID.mValue == EBML_MAX_ID_LENGTH_ID
+ ? "EBMLMaxIdLength"
+ : "EBMLMaxSizeLength");
+ WEBM_DEBUG("%s", str.get());
+ return MediaResult(NS_ERROR_FAILURE, str);
+ }
+ if (mElement.mSize.mValue > 8) {
+ // https://www.rfc-editor.org/rfc/rfc8794.html (EBML):
+ // An Unsigned Integer Element MUST declare a length from zero
+ // to eight octets.
+ nsPrintfCString str("Bad length of %s size",
+ mElement.mID.mValue == EBML_MAX_ID_LENGTH_ID
+ ? "EBMLMaxIdLength"
+ : "EBMLMaxSizeLength");
+ WEBM_DEBUG("%s", str.get());
+ return MediaResult(NS_ERROR_FAILURE, str);
+ }
+ mVInt = VInt();
+ mVIntLeft = mElement.mSize.mValue;
+ mState = READ_VINT_REST;
+ mNextState = mElement.mID.mValue == EBML_MAX_ID_LENGTH_ID
+ ? READ_EBML_MAX_ID_LENGTH
+ : READ_EBML_MAX_SIZE_LENGTH;
+ break;
+ case EBML_ID:
+ mLastInitStartOffset =
+ mCurrentOffset + (p - aBuffer) -
+ (mElement.mID.mLength + mElement.mSize.mLength);
+ mLastInitSize = mElement.mSize.mValue;
+ mEBMLMaxIdLength = EBML_MAX_ID_LENGTH_DEFAULT;
+ mEBMLMaxSizeLength = EBML_MAX_SIZE_LENGTH_DEFAULT;
+ mState = READ_ELEMENT_ID;
+ break;
+ default:
+ mSkipBytes = mElement.mSize.mValue;
+ mState = SKIP_DATA;
+ mNextState = READ_ELEMENT_ID;
+ break;
+ }
+ break;
+ case READ_VINT: {
+ unsigned char c = *p++;
+ uint32_t mask;
+ mVInt.mLength = VIntLength(c, &mask);
+ mVIntLeft = mVInt.mLength - 1;
+ mVInt.mValue = mVIntRaw ? c : c & ~mask;
+ mState = READ_VINT_REST;
+ break;
+ }
+ case READ_VINT_REST:
+ if (mVIntLeft) {
+ mVInt.mValue <<= 8;
+ mVInt.mValue |= *p++;
+ mVIntLeft -= 1;
+ } else {
+ mState = mNextState;
+ }
+ break;
+ case READ_TIMECODESCALE:
+ if (!mGotTimecodeScale) {
+ WEBM_DEBUG("Should get the SegmentInfo first");
+ return MediaResult(NS_ERROR_FAILURE,
+ "TimecodeScale appeared before SegmentInfo");
+ }
+ mTimecodeScale = mVInt.mValue;
+ mState = READ_ELEMENT_ID;
+ break;
+ case READ_CLUSTER_TIMECODE:
+ mClusterTimecode = mVInt.mValue;
+ mGotClusterTimecode = true;
+ mState = READ_ELEMENT_ID;
+ break;
+ case READ_BLOCK_TIMECODE:
+ if (mBlockTimecodeLength) {
+ mBlockTimecode <<= 8;
+ mBlockTimecode |= *p++;
+ mBlockTimecodeLength -= 1;
+ } else {
+ // It's possible we've parsed this data before, so avoid inserting
+ // duplicate WebMTimeDataOffset entries.
+ {
+ int64_t endOffset = mBlockOffset + mBlockSize +
+ mElement.mID.mLength + mElement.mSize.mLength;
+ uint32_t idx = aMapping.IndexOfFirstElementGt(endOffset);
+ if (idx == 0 || aMapping[idx - 1] != endOffset) {
+ // Don't insert invalid negative timecodes.
+ if (mBlockTimecode >= 0 ||
+ mClusterTimecode >= uint16_t(abs(mBlockTimecode))) {
+ if (!mGotTimecodeScale) {
+ WEBM_DEBUG("Should get the TimecodeScale first");
+ return MediaResult(NS_ERROR_FAILURE,
+ "Timecode appeared before SegmentInfo");
+ }
+ uint64_t absTimecode = mClusterTimecode + mBlockTimecode;
+ absTimecode *= mTimecodeScale;
+ // Avoid creating an entry if the timecode is out of order
+ // (invalid according to the WebM specification) so that
+ // ordering invariants of aMapping are not violated.
+ if (idx == 0 || aMapping[idx - 1].mTimecode <= absTimecode ||
+ (idx + 1 < aMapping.Length() &&
+ aMapping[idx + 1].mTimecode >= absTimecode)) {
+ WebMTimeDataOffset entry(endOffset, absTimecode,
+ mLastInitStartOffset, mClusterOffset,
+ mClusterEndOffset);
+ aMapping.InsertElementAt(idx, entry);
+ } else {
+ WEBM_DEBUG("Out of order timecode %" PRIu64
+ " in Cluster at %" PRId64 " ignored",
+ absTimecode, mClusterOffset);
+ }
+ }
+ }
+ }
+
+ // Skip rest of block header and the block's payload.
+ mBlockSize -= mVInt.mLength;
+ mBlockSize -= BLOCK_TIMECODE_LENGTH;
+ mSkipBytes = uint32_t(mBlockSize);
+ mState = SKIP_DATA;
+ mNextState = READ_ELEMENT_ID;
+ }
+ break;
+ case READ_EBML_MAX_ID_LENGTH:
+ if (mElement.mSize.mLength == 0) {
+ // https://www.rfc-editor.org/rfc/rfc8794.html (EBML):
+ // If an Empty Element has a default value declared, then the EBML
+ // Reader MUST interpret the value of the Empty Element as the
+ // default value.
+ mVInt.mValue = EBML_MAX_ID_LENGTH_DEFAULT;
+ }
+ if (mVInt.mValue < 4 || mVInt.mValue > 5) {
+ // https://www.ietf.org/archive/id/draft-ietf-cellar-matroska-13.html
+ // (Matroska):
+ // The EBMLMaxIDLength of the EBML Header MUST be "4".
+ //
+ // Also Matroska:
+ // Element IDs are encoded using the VINT mechanism described in
+ // Section 4 of [RFC8794] and can be between one and five octets
+ // long. Five-octet-long Element IDs are possible only if declared
+ // in the EBML header.
+ nsPrintfCString detail("Invalid EMBLMaxIdLength %" PRIu64,
+ mVInt.mValue);
+ WEBM_DEBUG("%s", detail.get());
+ return MediaResult(NS_ERROR_FAILURE, detail);
+ }
+ mEBMLMaxIdLength = mVInt.mValue;
+ mState = READ_ELEMENT_ID;
+ break;
+ case READ_EBML_MAX_SIZE_LENGTH:
+ if (mElement.mSize.mLength == 0) {
+ // https://www.rfc-editor.org/rfc/rfc8794.html (EBML):
+ // If an Empty Element has a default value declared, then the EBML
+ // Reader MUST interpret the value of the Empty Element as the
+ // default value.
+ mVInt.mValue = EBML_MAX_SIZE_LENGTH_DEFAULT;
+ }
+ if (mVInt.mValue < 1 || mVInt.mValue > 8) {
+ // https://www.ietf.org/archive/id/draft-ietf-cellar-matroska-13.html
+ // (Matroska):
+ // The EBMLMaxSizeLength of the EBML Header MUST be between "1" and
+ // "8" inclusive.
+ nsPrintfCString detail("Invalid EMBLMaxSizeLength %" PRIu64,
+ mVInt.mValue);
+ WEBM_DEBUG("%s", detail.get());
+ return MediaResult(NS_ERROR_FAILURE, detail);
+ }
+ mEBMLMaxSizeLength = mVInt.mValue;
+ mState = READ_ELEMENT_ID;
+ break;
+ case SKIP_DATA:
+ if (mSkipBytes) {
+ uint32_t left = aLength - (p - aBuffer);
+ left = std::min(left, mSkipBytes);
+ p += left;
+ mSkipBytes -= left;
+ }
+ if (!mSkipBytes) {
+ mBlockEndOffset = mCurrentOffset + (p - aBuffer);
+ mState = mNextState;
+ }
+ break;
+ case CHECK_INIT_FOUND:
+ if (mSkipBytes) {
+ uint32_t left = aLength - (p - aBuffer);
+ left = std::min(left, mSkipBytes);
+ p += left;
+ mSkipBytes -= left;
+ }
+ if (!mSkipBytes) {
+ if (mInitEndOffset < 0) {
+ mInitEndOffset = mCurrentOffset + (p - aBuffer);
+ mBlockEndOffset = mCurrentOffset + (p - aBuffer);
+ }
+ mState = READ_ELEMENT_ID;
+ }
+ break;
+ }
+ }
+
+ NS_ASSERTION(p == aBuffer + aLength, "Must have parsed to end of data.");
+ mCurrentOffset += aLength;
+
+ return NS_OK;
+}
+
+int64_t WebMBufferedParser::EndSegmentOffset(int64_t aOffset) {
+ if (mLastInitStartOffset > aOffset || mClusterOffset > aOffset) {
+ return std::min(
+ mLastInitStartOffset >= 0 ? mLastInitStartOffset : INT64_MAX,
+ mClusterOffset >= 0 ? mClusterOffset : INT64_MAX);
+ }
+ return mBlockEndOffset;
+}
+
+int64_t WebMBufferedParser::GetClusterOffset() const { return mClusterOffset; }
+
+// SyncOffsetComparator and TimeComparator are slightly confusing, in that
+// the nsTArray they're used with (mTimeMapping) is sorted by mEndOffset and
+// these comparators are used on the other fields of WebMTimeDataOffset.
+// This is only valid because timecodes are required to be monotonically
+// increasing within a file (thus establishing an ordering relationship with
+// mTimecode), and mEndOffset is derived from mSyncOffset.
+struct SyncOffsetComparator {
+ bool Equals(const WebMTimeDataOffset& a, const int64_t& b) const {
+ return a.mSyncOffset == b;
+ }
+
+ bool LessThan(const WebMTimeDataOffset& a, const int64_t& b) const {
+ return a.mSyncOffset < b;
+ }
+};
+
+struct TimeComparator {
+ bool Equals(const WebMTimeDataOffset& a, const uint64_t& b) const {
+ return a.mTimecode == b;
+ }
+
+ bool LessThan(const WebMTimeDataOffset& a, const uint64_t& b) const {
+ return a.mTimecode < b;
+ }
+};
+
+bool WebMBufferedState::CalculateBufferedForRange(int64_t aStartOffset,
+ int64_t aEndOffset,
+ uint64_t* aStartTime,
+ uint64_t* aEndTime) {
+ MutexAutoLock lock(mMutex);
+
+ // Find the first WebMTimeDataOffset at or after aStartOffset.
+ uint32_t start = mTimeMapping.IndexOfFirstElementGt(aStartOffset - 1,
+ SyncOffsetComparator());
+ if (start == mTimeMapping.Length()) {
+ return false;
+ }
+
+ // Find the first WebMTimeDataOffset at or before aEndOffset.
+ uint32_t end = mTimeMapping.IndexOfFirstElementGt(aEndOffset);
+ if (end > 0) {
+ end -= 1;
+ }
+
+ // Range is empty.
+ if (end <= start) {
+ return false;
+ }
+
+ NS_ASSERTION(mTimeMapping[start].mSyncOffset >= aStartOffset &&
+ mTimeMapping[end].mEndOffset <= aEndOffset,
+ "Computed time range must lie within data range.");
+ if (start > 0) {
+ NS_ASSERTION(mTimeMapping[start - 1].mSyncOffset < aStartOffset,
+ "Must have found least WebMTimeDataOffset for start");
+ }
+ if (end < mTimeMapping.Length() - 1) {
+ NS_ASSERTION(mTimeMapping[end + 1].mEndOffset > aEndOffset,
+ "Must have found greatest WebMTimeDataOffset for end");
+ }
+
+ MOZ_ASSERT(mTimeMapping[end].mTimecode >= mTimeMapping[end - 1].mTimecode);
+ uint64_t frameDuration =
+ mTimeMapping[end].mTimecode - mTimeMapping[end - 1].mTimecode;
+ *aStartTime = mTimeMapping[start].mTimecode;
+ CheckedUint64 endTime{mTimeMapping[end].mTimecode};
+ endTime += frameDuration;
+ if (!endTime.isValid()) {
+ WEBM_DEBUG("End time overflow during CalculateBufferedForRange.");
+ return false;
+ }
+ *aEndTime = endTime.value();
+ return true;
+}
+
+bool WebMBufferedState::GetOffsetForTime(uint64_t aTime, int64_t* aOffset) {
+ MutexAutoLock lock(mMutex);
+
+ if (mTimeMapping.IsEmpty()) {
+ return false;
+ }
+
+ uint64_t time = aTime;
+ if (time > 0) {
+ time = time - 1;
+ }
+ uint32_t idx = mTimeMapping.IndexOfFirstElementGt(time, TimeComparator());
+ if (idx == mTimeMapping.Length()) {
+ // Clamp to end
+ *aOffset = mTimeMapping[mTimeMapping.Length() - 1].mSyncOffset;
+ } else {
+ // Idx is within array or has been clamped to start
+ *aOffset = mTimeMapping[idx].mSyncOffset;
+ }
+ return true;
+}
+
+void WebMBufferedState::NotifyDataArrived(const unsigned char* aBuffer,
+ uint32_t aLength, int64_t aOffset) {
+ uint32_t idx = mRangeParsers.IndexOfFirstElementGt(aOffset - 1);
+ if (idx == 0 || !(mRangeParsers[idx - 1] == aOffset)) {
+ // If the incoming data overlaps an already parsed range, adjust the
+ // buffer so that we only reparse the new data. It's also possible to
+ // have an overlap where the end of the incoming data is within an
+ // already parsed range, but we don't bother handling that other than by
+ // avoiding storing duplicate timecodes when the parser runs.
+ if (idx != mRangeParsers.Length() &&
+ mRangeParsers[idx].mStartOffset <= aOffset) {
+ // Complete overlap, skip parsing.
+ if (aOffset + aLength <= mRangeParsers[idx].mCurrentOffset) {
+ return;
+ }
+
+ // Partial overlap, adjust the buffer to parse only the new data.
+ int64_t adjust = mRangeParsers[idx].mCurrentOffset - aOffset;
+ NS_ASSERTION(adjust >= 0, "Overlap detection bug.");
+ aBuffer += adjust;
+ aLength -= uint32_t(adjust);
+ } else {
+ mRangeParsers.InsertElementAt(idx, WebMBufferedParser(aOffset));
+ if (idx != 0) {
+ mRangeParsers[idx].SetTimecodeScale(
+ mRangeParsers[0].GetTimecodeScale());
+ }
+ }
+ }
+
+ {
+ MutexAutoLock lock(mMutex);
+ mRangeParsers[idx].Append(aBuffer, aLength, mTimeMapping);
+ }
+
+ // Merge parsers with overlapping regions and clean up the remnants.
+ uint32_t i = 0;
+ while (i + 1 < mRangeParsers.Length()) {
+ if (mRangeParsers[i].mCurrentOffset >= mRangeParsers[i + 1].mStartOffset) {
+ mRangeParsers[i + 1].mStartOffset = mRangeParsers[i].mStartOffset;
+ mRangeParsers[i + 1].mInitEndOffset = mRangeParsers[i].mInitEndOffset;
+ mRangeParsers.RemoveElementAt(i);
+ } else {
+ i += 1;
+ }
+ }
+
+ if (mRangeParsers.IsEmpty()) {
+ return;
+ }
+
+ MutexAutoLock lock(mMutex);
+ mLastBlockOffset = mRangeParsers.LastElement().mBlockEndOffset;
+}
+
+void WebMBufferedState::Reset() {
+ MutexAutoLock lock(mMutex);
+ mRangeParsers.Clear();
+ mTimeMapping.Clear();
+}
+
+void WebMBufferedState::UpdateIndex(const MediaByteRangeSet& aRanges,
+ MediaResource* aResource) {
+ for (uint32_t index = 0; index < aRanges.Length(); index++) {
+ const MediaByteRange& range = aRanges[index];
+ int64_t offset = range.mStart;
+ uint32_t length = range.mEnd - range.mStart;
+
+ uint32_t idx = mRangeParsers.IndexOfFirstElementGt(offset - 1);
+ if (!idx || !(mRangeParsers[idx - 1] == offset)) {
+ // If the incoming data overlaps an already parsed range, adjust the
+ // buffer so that we only reparse the new data. It's also possible to
+ // have an overlap where the end of the incoming data is within an
+ // already parsed range, but we don't bother handling that other than by
+ // avoiding storing duplicate timecodes when the parser runs.
+ if (idx != mRangeParsers.Length() &&
+ mRangeParsers[idx].mStartOffset <= offset) {
+ // Complete overlap, skip parsing.
+ if (offset + length <= mRangeParsers[idx].mCurrentOffset) {
+ continue;
+ }
+
+ // Partial overlap, adjust the buffer to parse only the new data.
+ int64_t adjust = mRangeParsers[idx].mCurrentOffset - offset;
+ NS_ASSERTION(adjust >= 0, "Overlap detection bug.");
+ offset += adjust;
+ length -= uint32_t(adjust);
+ } else {
+ mRangeParsers.InsertElementAt(idx, WebMBufferedParser(offset));
+ if (idx) {
+ mRangeParsers[idx].SetTimecodeScale(
+ mRangeParsers[0].GetTimecodeScale());
+ }
+ }
+ }
+
+ MediaResourceIndex res(aResource);
+ while (length > 0) {
+ static const uint32_t BLOCK_SIZE = 1048576;
+ uint32_t block = std::min(length, BLOCK_SIZE);
+ RefPtr<MediaByteBuffer> bytes = res.CachedMediaReadAt(offset, block);
+ if (!bytes) {
+ break;
+ }
+ NotifyDataArrived(bytes->Elements(), bytes->Length(), offset);
+ length -= bytes->Length();
+ offset += bytes->Length();
+ }
+ }
+}
+
+int64_t WebMBufferedState::GetInitEndOffset() {
+ if (mRangeParsers.IsEmpty()) {
+ return -1;
+ }
+ return mRangeParsers[0].mInitEndOffset;
+}
+
+int64_t WebMBufferedState::GetLastBlockOffset() {
+ MutexAutoLock lock(mMutex);
+
+ return mLastBlockOffset;
+}
+
+bool WebMBufferedState::GetStartTime(uint64_t* aTime) {
+ MutexAutoLock lock(mMutex);
+
+ if (mTimeMapping.IsEmpty()) {
+ return false;
+ }
+
+ uint32_t idx = mTimeMapping.IndexOfFirstElementGt(0, SyncOffsetComparator());
+ if (idx == mTimeMapping.Length()) {
+ return false;
+ }
+
+ *aTime = mTimeMapping[idx].mTimecode;
+ return true;
+}
+
+bool WebMBufferedState::GetNextKeyframeTime(uint64_t aTime,
+ uint64_t* aKeyframeTime) {
+ MutexAutoLock lock(mMutex);
+ int64_t offset = 0;
+ bool rv = GetOffsetForTime(aTime, &offset);
+ if (!rv) {
+ return false;
+ }
+ uint32_t idx =
+ mTimeMapping.IndexOfFirstElementGt(offset, SyncOffsetComparator());
+ if (idx == mTimeMapping.Length()) {
+ return false;
+ }
+ *aKeyframeTime = mTimeMapping[idx].mTimecode;
+ return true;
+}
+} // namespace mozilla
+
+#undef WEBM_DEBUG
diff --git a/dom/media/webm/WebMBufferedParser.h b/dom/media/webm/WebMBufferedParser.h
new file mode 100644
index 0000000000..bf553d8f24
--- /dev/null
+++ b/dom/media/webm/WebMBufferedParser.h
@@ -0,0 +1,309 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#if !defined(WebMBufferedParser_h_)
+# define WebMBufferedParser_h_
+
+# include "nsISupportsImpl.h"
+# include "nsTArray.h"
+# include "mozilla/Mutex.h"
+# include "MediaResource.h"
+# include "MediaResult.h"
+
+namespace mozilla {
+
+// Stores a stream byte offset and the scaled timecode of the block at
+// that offset.
+struct WebMTimeDataOffset {
+ WebMTimeDataOffset(int64_t aEndOffset, uint64_t aTimecode,
+ int64_t aInitOffset, int64_t aSyncOffset,
+ int64_t aClusterEndOffset)
+ : mEndOffset(aEndOffset),
+ mInitOffset(aInitOffset),
+ mSyncOffset(aSyncOffset),
+ mClusterEndOffset(aClusterEndOffset),
+ mTimecode(aTimecode) {}
+
+ bool operator==(int64_t aEndOffset) const { return mEndOffset == aEndOffset; }
+
+ bool operator!=(int64_t aEndOffset) const { return mEndOffset != aEndOffset; }
+
+ bool operator<(int64_t aEndOffset) const { return mEndOffset < aEndOffset; }
+
+ int64_t mEndOffset;
+ int64_t mInitOffset;
+ int64_t mSyncOffset;
+ int64_t mClusterEndOffset;
+ // In nanoseconds
+ uint64_t mTimecode;
+};
+
+// A simple WebM parser that produces data offset to timecode pairs as it
+// consumes blocks. A new parser is created for each distinct range of data
+// received and begins parsing from the first WebM cluster within that
+// range. Old parsers are destroyed when their range merges with a later
+// parser or an already parsed range. The parser may start at any position
+// within the stream.
+struct WebMBufferedParser {
+ explicit WebMBufferedParser(int64_t aOffset);
+
+ uint32_t GetTimecodeScale() {
+ MOZ_ASSERT(mGotTimecodeScale);
+ return mTimecodeScale;
+ }
+
+ // Use this function when we would only feed media segment for the parser.
+ void AppendMediaSegmentOnly() { mGotTimecodeScale = true; }
+
+ // If this parser is not expected to parse a segment info, it must be told
+ // the appropriate timecode scale to use from elsewhere.
+ void SetTimecodeScale(uint32_t aTimecodeScale) {
+ mTimecodeScale = aTimecodeScale;
+ mGotTimecodeScale = true;
+ }
+
+ // Steps the parser through aLength bytes of data. Always consumes
+ // aLength bytes. Updates mCurrentOffset before returning.
+ // Returns false if an error was encountered.
+ MediaResult Append(const unsigned char* aBuffer, uint32_t aLength,
+ nsTArray<WebMTimeDataOffset>& aMapping);
+
+ bool operator==(int64_t aOffset) const { return mCurrentOffset == aOffset; }
+
+ bool operator<(int64_t aOffset) const { return mCurrentOffset < aOffset; }
+
+ // Returns the start offset of the init (EBML) or media segment (Cluster)
+ // following the aOffset position. If none were found, returns
+ // mBlockEndOffset. This allows to determine the end of the interval containg
+ // aOffset.
+ int64_t EndSegmentOffset(int64_t aOffset);
+
+ // Return the Cluster offset, return -1 if we can't find the Cluster.
+ int64_t GetClusterOffset() const;
+
+ // The offset at which this parser started parsing. Used to merge
+ // adjacent parsers, in which case the later parser adopts the earlier
+ // parser's mStartOffset.
+ int64_t mStartOffset;
+
+ // Current offset within the stream. Updated in chunks as Append() consumes
+ // data.
+ int64_t mCurrentOffset;
+
+ // Tracks element's end offset. This indicates the end of the first init
+ // segment. Will only be set if a Segment Information has been found.
+ int64_t mInitEndOffset;
+
+ // End offset of the last block parsed.
+ // Will only be set if a complete block has been parsed.
+ int64_t mBlockEndOffset;
+
+ private:
+ enum State {
+ // Parser start state. Expects to begin at a valid EBML element. Move
+ // to READ_VINT with mVIntRaw true, then return to READ_ELEMENT_SIZE.
+ READ_ELEMENT_ID,
+
+ // Store element ID read into mVInt into mElement.mID. Move to
+ // READ_VINT with mVIntRaw false, then return to PARSE_ELEMENT.
+ READ_ELEMENT_SIZE,
+
+ // Parser start state for parsers started at an arbitrary offset. Scans
+ // forward for the first cluster, then move to READ_ELEMENT_ID.
+ FIND_CLUSTER_SYNC,
+
+ // Simplistic core of the parser. Does not pay attention to nesting of
+ // elements. Checks mElement for an element ID of interest, then moves
+ // to the next state as determined by the element ID.
+ PARSE_ELEMENT,
+
+ // Read the first byte of a variable length integer. The first byte
+ // encodes both the variable integer's length and part of the value.
+ // The value read so far is stored in mVInt.mValue and the length is
+ // stored in mVInt.mLength. The number of bytes left to read is stored
+ // in mVIntLeft.
+ READ_VINT,
+
+ // Reads the remaining mVIntLeft bytes into mVInt.mValue.
+ READ_VINT_REST,
+
+ // mVInt holds the parsed timecode scale, store it in mTimecodeScale,
+ // then return READ_ELEMENT_ID.
+ READ_TIMECODESCALE,
+
+ // mVInt holds the parsed cluster timecode, store it in
+ // mClusterTimecode, then return to READ_ELEMENT_ID.
+ READ_CLUSTER_TIMECODE,
+
+ // mBlockTimecodeLength holds the remaining length of the block timecode
+ // left to read. Read each byte of the timecode into mBlockTimecode.
+ // Once complete, calculate the scaled timecode from the cluster
+ // timecode, block timecode, and timecode scale, and insert a
+ // WebMTimeDataOffset entry into aMapping if one is not already present
+ // for this offset.
+ READ_BLOCK_TIMECODE,
+
+ // mVInt holds the parsed EBMLMaxIdLength, store it in mEBMLMaxIdLength,
+ // then return to READ_ELEMENT_ID.
+ READ_EBML_MAX_ID_LENGTH,
+
+ // mVInt holds the parsed EBMLMaxSizeLength, store it in mEBMLMaxSizeLength,
+ // then return to READ_ELEMENT_ID.
+ READ_EBML_MAX_SIZE_LENGTH,
+
+ // Will skip the current tracks element and set mInitEndOffset if an init
+ // segment has been found.
+ // Currently, only assumes it's the end of the tracks element.
+ CHECK_INIT_FOUND,
+
+ // Skip mSkipBytes of data before resuming parse at mNextState.
+ SKIP_DATA,
+ };
+
+ // Current state machine action.
+ State mState;
+
+ // Next state machine action. SKIP_DATA and READ_VINT_REST advance to
+ // mNextState when the current action completes.
+ State mNextState;
+
+ struct VInt {
+ VInt() : mValue(0), mLength(0) {}
+ uint64_t mValue;
+ uint64_t mLength;
+ };
+
+ struct EBMLElement {
+ uint64_t Length() { return mID.mLength + mSize.mLength; }
+ VInt mID;
+ VInt mSize;
+ };
+
+ EBMLElement mElement;
+
+ VInt mVInt;
+
+ bool mVIntRaw;
+
+ // EBML start offset. This indicates the start of the last init segment
+ // parsed. Will only be set if an EBML element has been found.
+ int64_t mLastInitStartOffset;
+
+ // EBML element size. This indicates the size of the body of the last init
+ // segment parsed. Will only be set if an EBML element has been found.
+ uint32_t mLastInitSize;
+
+ // EBML max id length is the max number of bytes allowed for an element id
+ // vint.
+ uint8_t mEBMLMaxIdLength;
+
+ // EBML max size length is the max number of bytes allowed for an element size
+ // vint.
+ uint8_t mEBMLMaxSizeLength;
+
+ // Current match position within CLUSTER_SYNC_ID. Used to find sync
+ // within arbitrary data.
+ uint32_t mClusterSyncPos;
+
+ // Number of bytes of mVInt left to read. mVInt is complete once this
+ // reaches 0.
+ uint32_t mVIntLeft;
+
+ // Size of the block currently being parsed. Any unused data within the
+ // block is skipped once the block timecode has been parsed.
+ uint64_t mBlockSize;
+
+ // Cluster-level timecode.
+ uint64_t mClusterTimecode;
+
+ // Start offset of the cluster currently being parsed. Used as the sync
+ // point offset for the offset-to-time mapping as each block timecode is
+ // been parsed. -1 if unknown.
+ int64_t mClusterOffset;
+
+ // End offset of the cluster currently being parsed. -1 if unknown.
+ int64_t mClusterEndOffset;
+
+ // Start offset of the block currently being parsed. Used as the byte
+ // offset for the offset-to-time mapping once the block timecode has been
+ // parsed.
+ int64_t mBlockOffset;
+
+ // Block-level timecode. This is summed with mClusterTimecode to produce
+ // an absolute timecode for the offset-to-time mapping.
+ int16_t mBlockTimecode;
+
+ // Number of bytes of mBlockTimecode left to read.
+ uint32_t mBlockTimecodeLength;
+
+ // Count of bytes left to skip before resuming parse at mNextState.
+ // Mostly used to skip block payload data after reading a block timecode.
+ uint32_t mSkipBytes;
+
+ // Timecode scale read from the segment info and used to scale absolute
+ // timecodes.
+ uint32_t mTimecodeScale;
+
+ // True if we read the timecode scale from the segment info or have
+ // confirmed that the default value is to be used.
+ bool mGotTimecodeScale;
+
+ // True if we've read the cluster time code.
+ bool mGotClusterTimecode;
+};
+
+class WebMBufferedState final {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WebMBufferedState)
+
+ public:
+ WebMBufferedState() : mMutex("WebMBufferedState"), mLastBlockOffset(-1) {
+ MOZ_COUNT_CTOR(WebMBufferedState);
+ }
+
+ void NotifyDataArrived(const unsigned char* aBuffer, uint32_t aLength,
+ int64_t aOffset);
+ void Reset();
+ void UpdateIndex(const MediaByteRangeSet& aRanges, MediaResource* aResource);
+ bool CalculateBufferedForRange(int64_t aStartOffset, int64_t aEndOffset,
+ uint64_t* aStartTime, uint64_t* aEndTime);
+
+ // Returns true if mTimeMapping is not empty and sets aOffset to
+ // the latest offset for which decoding can resume without data
+ // dependencies to arrive at aTime. aTime will be clamped to the start
+ // of mTimeMapping if it is earlier than the first element, and to the end
+ // if later than the last
+ bool GetOffsetForTime(uint64_t aTime, int64_t* aOffset);
+
+ // Returns end offset of init segment or -1 if none found.
+ int64_t GetInitEndOffset();
+ // Returns the end offset of the last complete block or -1 if none found.
+ int64_t GetLastBlockOffset();
+
+ // Returns start time
+ bool GetStartTime(uint64_t* aTime);
+
+ // Returns keyframe for time
+ bool GetNextKeyframeTime(uint64_t aTime, uint64_t* aKeyframeTime);
+
+ private:
+ // Private destructor, to discourage deletion outside of Release():
+ MOZ_COUNTED_DTOR(WebMBufferedState)
+
+ // Synchronizes access to the mTimeMapping array and mLastBlockOffset.
+ Mutex mMutex;
+
+ // Sorted (by offset) map of data offsets to timecodes. Populated
+ // on the main thread as data is received and parsed by WebMBufferedParsers.
+ nsTArray<WebMTimeDataOffset> mTimeMapping MOZ_GUARDED_BY(mMutex);
+ // The last complete block parsed. -1 if not set.
+ int64_t mLastBlockOffset MOZ_GUARDED_BY(mMutex);
+
+ // Sorted (by offset) live parser instances. Main thread only.
+ nsTArray<WebMBufferedParser> mRangeParsers;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webm/WebMDecoder.cpp b/dom/media/webm/WebMDecoder.cpp
new file mode 100644
index 0000000000..814eb8cb64
--- /dev/null
+++ b/dom/media/webm/WebMDecoder.cpp
@@ -0,0 +1,124 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WebMDecoder.h"
+
+#include <utility>
+
+#include "mozilla/Preferences.h"
+#include "VPXDecoder.h"
+#include "mozilla/StaticPrefs_media.h"
+#ifdef MOZ_AV1
+# include "AOMDecoder.h"
+#endif
+#include "MediaContainerType.h"
+#include "PDMFactory.h"
+#include "PlatformDecoderModule.h"
+#include "VideoUtils.h"
+
+namespace mozilla {
+
+/* static */
+nsTArray<UniquePtr<TrackInfo>> WebMDecoder::GetTracksInfo(
+ const MediaContainerType& aType, MediaResult& aError) {
+ nsTArray<UniquePtr<TrackInfo>> tracks;
+ const bool isVideo = aType.Type() == MEDIAMIMETYPE("video/webm");
+
+ if (aType.Type() != MEDIAMIMETYPE("audio/webm") && !isVideo) {
+ aError = MediaResult(
+ NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Invalid type:%s", aType.Type().AsString().get()));
+ return tracks;
+ }
+
+ aError = NS_OK;
+
+ const MediaCodecs& codecs = aType.ExtendedType().Codecs();
+ if (codecs.IsEmpty()) {
+ return tracks;
+ }
+
+ for (const auto& codec : codecs.Range()) {
+ if (codec.EqualsLiteral("opus") || codec.EqualsLiteral("vorbis")) {
+ tracks.AppendElement(
+ CreateTrackInfoWithMIMETypeAndContainerTypeExtraParameters(
+ "audio/"_ns + NS_ConvertUTF16toUTF8(codec), aType));
+ continue;
+ }
+ if (isVideo) {
+ UniquePtr<TrackInfo> trackInfo;
+ if (IsVP9CodecString(codec)) {
+ trackInfo = CreateTrackInfoWithMIMETypeAndContainerTypeExtraParameters(
+ "video/vp9"_ns, aType);
+ } else if (IsVP8CodecString(codec)) {
+ trackInfo = CreateTrackInfoWithMIMETypeAndContainerTypeExtraParameters(
+ "video/vp8"_ns, aType);
+ }
+ if (trackInfo) {
+ VPXDecoder::SetVideoInfo(trackInfo->GetAsVideoInfo(), codec);
+ tracks.AppendElement(std::move(trackInfo));
+ continue;
+ }
+ }
+#ifdef MOZ_AV1
+ if (StaticPrefs::media_av1_enabled() && IsAV1CodecString(codec)) {
+ auto trackInfo =
+ CreateTrackInfoWithMIMETypeAndContainerTypeExtraParameters(
+ "video/av1"_ns, aType);
+ AOMDecoder::SetVideoInfo(trackInfo->GetAsVideoInfo(), codec);
+ tracks.AppendElement(std::move(trackInfo));
+ continue;
+ }
+#endif
+ // Unknown codec
+ aError = MediaResult(
+ NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ RESULT_DETAIL("Unknown codec:%s", NS_ConvertUTF16toUTF8(codec).get()));
+ }
+ return tracks;
+}
+
+/* static */
+bool WebMDecoder::IsSupportedType(const MediaContainerType& aContainerType) {
+ if (!StaticPrefs::media_webm_enabled()) {
+ return false;
+ }
+
+ MediaResult rv = NS_OK;
+ auto tracks = GetTracksInfo(aContainerType, rv);
+
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ if (tracks.IsEmpty()) {
+ // WebM guarantees that the only codecs it contained are vp8, vp9, opus or
+ // vorbis.
+ return true;
+ }
+
+ // Verify that we have a PDM that supports the whitelisted types, include
+ // color depth
+ RefPtr<PDMFactory> platform = new PDMFactory();
+ for (const auto& track : tracks) {
+ if (!track || platform->Supports(SupportDecoderParams(*track),
+ nullptr /* diagnostic */) ==
+ media::DecodeSupport::Unsupported) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+/* static */
+nsTArray<UniquePtr<TrackInfo>> WebMDecoder::GetTracksInfo(
+ const MediaContainerType& aType) {
+ MediaResult rv = NS_OK;
+ return GetTracksInfo(aType, rv);
+}
+
+} // namespace mozilla
diff --git a/dom/media/webm/WebMDecoder.h b/dom/media/webm/WebMDecoder.h
new file mode 100644
index 0000000000..9bbe5ce4d2
--- /dev/null
+++ b/dom/media/webm/WebMDecoder.h
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#if !defined(WebMDecoder_h_)
+# define WebMDecoder_h_
+
+# include "mozilla/UniquePtr.h"
+# include "nsTArray.h"
+
+namespace mozilla {
+
+class MediaContainerType;
+class MediaResult;
+class TrackInfo;
+
+class WebMDecoder {
+ public:
+ // Returns true if aContainerType is a WebM type that we think we can render
+ // with an enabled platform decoder backend.
+ // If provided, codecs are checked for support.
+ static bool IsSupportedType(const MediaContainerType& aContainerType);
+
+ static nsTArray<UniquePtr<TrackInfo>> GetTracksInfo(
+ const MediaContainerType& aType);
+
+ private:
+ static nsTArray<UniquePtr<TrackInfo>> GetTracksInfo(
+ const MediaContainerType& aType, MediaResult& aError);
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webm/WebMDemuxer.cpp b/dom/media/webm/WebMDemuxer.cpp
new file mode 100644
index 0000000000..fce8f0e512
--- /dev/null
+++ b/dom/media/webm/WebMDemuxer.cpp
@@ -0,0 +1,1345 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsError.h"
+#include "MediaResource.h"
+#ifdef MOZ_AV1
+# include "AOMDecoder.h"
+#endif
+#include "OpusDecoder.h"
+#include "VPXDecoder.h"
+#include "WebMDemuxer.h"
+#include "WebMBufferedParser.h"
+#include "gfx2DGlue.h"
+#include "gfxUtils.h"
+#include "mozilla/EndianUtils.h"
+#include "mozilla/SharedThreadPool.h"
+#include "MediaDataDemuxer.h"
+#include "nsAutoRef.h"
+#include "NesteggPacketHolder.h"
+#include "XiphExtradata.h"
+#include "prprf.h" // leaving it for PR_vsnprintf()
+#include "mozilla/IntegerPrintfMacros.h"
+#include "mozilla/Sprintf.h"
+#include "VideoUtils.h"
+
+#include <algorithm>
+#include <numeric>
+#include <stdint.h>
+
+#define WEBM_DEBUG(arg, ...) \
+ DDMOZ_LOG(gMediaDemuxerLog, mozilla::LogLevel::Debug, "::%s: " arg, \
+ __func__, ##__VA_ARGS__)
+extern mozilla::LazyLogModule gMediaDemuxerLog;
+
+namespace mozilla {
+
+using namespace gfx;
+using media::TimeUnit;
+
+LazyLogModule gNesteggLog("Nestegg");
+
+// How far ahead will we look when searching future keyframe. In microseconds.
+// This value is based on what appears to be a reasonable value as most webm
+// files encountered appear to have keyframes located < 4s.
+#define MAX_LOOK_AHEAD 10000000
+
+// Functions for reading and seeking using WebMDemuxer required for
+// nestegg_io. The 'user data' passed to these functions is the
+// demuxer.
+static int webmdemux_read(void* aBuffer, size_t aLength, void* aUserData) {
+ MOZ_ASSERT(aUserData);
+ MOZ_ASSERT(aLength < UINT32_MAX);
+ WebMDemuxer::NestEggContext* context =
+ reinterpret_cast<WebMDemuxer::NestEggContext*>(aUserData);
+ uint32_t count = aLength;
+ if (context->IsMediaSource()) {
+ int64_t length = context->GetEndDataOffset();
+ int64_t position = context->GetResource()->Tell();
+ MOZ_ASSERT(position <= context->GetResource()->GetLength());
+ MOZ_ASSERT(position <= length);
+ if (length >= 0 && count + position > length) {
+ count = length - position;
+ }
+ MOZ_ASSERT(count <= aLength);
+ }
+ uint32_t bytes = 0;
+ nsresult rv =
+ context->GetResource()->Read(static_cast<char*>(aBuffer), count, &bytes);
+ bool eof = bytes < aLength;
+ return NS_FAILED(rv) ? -1 : eof ? 0 : 1;
+}
+
+static int webmdemux_seek(int64_t aOffset, int aWhence, void* aUserData) {
+ MOZ_ASSERT(aUserData);
+ WebMDemuxer::NestEggContext* context =
+ reinterpret_cast<WebMDemuxer::NestEggContext*>(aUserData);
+ nsresult rv = context->GetResource()->Seek(aWhence, aOffset);
+ return NS_SUCCEEDED(rv) ? 0 : -1;
+}
+
+static int64_t webmdemux_tell(void* aUserData) {
+ MOZ_ASSERT(aUserData);
+ WebMDemuxer::NestEggContext* context =
+ reinterpret_cast<WebMDemuxer::NestEggContext*>(aUserData);
+ return context->GetResource()->Tell();
+}
+
+static void webmdemux_log(nestegg* aContext, unsigned int aSeverity,
+ char const* aFormat, ...) {
+ if (!MOZ_LOG_TEST(gNesteggLog, LogLevel::Debug)) {
+ return;
+ }
+
+ va_list args;
+ char msg[256];
+ const char* sevStr;
+
+ switch (aSeverity) {
+ case NESTEGG_LOG_DEBUG:
+ sevStr = "DBG";
+ break;
+ case NESTEGG_LOG_INFO:
+ sevStr = "INF";
+ break;
+ case NESTEGG_LOG_WARNING:
+ sevStr = "WRN";
+ break;
+ case NESTEGG_LOG_ERROR:
+ sevStr = "ERR";
+ break;
+ case NESTEGG_LOG_CRITICAL:
+ sevStr = "CRT";
+ break;
+ default:
+ sevStr = "UNK";
+ break;
+ }
+
+ va_start(args, aFormat);
+
+ SprintfLiteral(msg, "%p [Nestegg-%s] ", aContext, sevStr);
+ PR_vsnprintf(msg + strlen(msg), sizeof(msg) - strlen(msg), aFormat, args);
+ MOZ_LOG(gNesteggLog, LogLevel::Debug, ("%s", msg));
+
+ va_end(args);
+}
+
+WebMDemuxer::NestEggContext::~NestEggContext() {
+ if (mContext) {
+ nestegg_destroy(mContext);
+ }
+}
+
+int WebMDemuxer::NestEggContext::Init() {
+ nestegg_io io;
+ io.read = webmdemux_read;
+ io.seek = webmdemux_seek;
+ io.tell = webmdemux_tell;
+ io.userdata = this;
+
+ // While reading the metadata, we do not really care about which nestegg
+ // context is being used so long that they are both initialised.
+ // For reading the metadata however, we will use mVideoContext.
+ return nestegg_init(&mContext, io, &webmdemux_log,
+ mParent->IsMediaSource() ? mResource.GetLength() : -1);
+}
+
+WebMDemuxer::WebMDemuxer(MediaResource* aResource)
+ : WebMDemuxer(aResource, false) {}
+
+WebMDemuxer::WebMDemuxer(MediaResource* aResource, bool aIsMediaSource)
+ : mVideoContext(this, aResource),
+ mAudioContext(this, aResource),
+ mBufferedState(nullptr),
+ mInitData(nullptr),
+ mVideoTrack(0),
+ mAudioTrack(0),
+ mSeekPreroll(0),
+ mAudioCodec(-1),
+ mVideoCodec(-1),
+ mHasVideo(false),
+ mHasAudio(false),
+ mNeedReIndex(true),
+ mLastWebMBlockOffset(-1),
+ mIsMediaSource(aIsMediaSource) {
+ DDLINKCHILD("resource", aResource);
+ // Audio/video contexts hold a MediaResourceIndex.
+ DDLINKCHILD("video context", mVideoContext.GetResource());
+ DDLINKCHILD("audio context", mAudioContext.GetResource());
+}
+
+WebMDemuxer::~WebMDemuxer() {
+ Reset(TrackInfo::kVideoTrack);
+ Reset(TrackInfo::kAudioTrack);
+}
+
+RefPtr<WebMDemuxer::InitPromise> WebMDemuxer::Init() {
+ InitBufferedState();
+
+ if (NS_FAILED(ReadMetadata())) {
+ return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_METADATA_ERR,
+ __func__);
+ }
+
+ if (!GetNumberTracks(TrackInfo::kAudioTrack) &&
+ !GetNumberTracks(TrackInfo::kVideoTrack)) {
+ return InitPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_METADATA_ERR,
+ __func__);
+ }
+
+ return InitPromise::CreateAndResolve(NS_OK, __func__);
+}
+
+void WebMDemuxer::InitBufferedState() {
+ MOZ_ASSERT(!mBufferedState);
+ mBufferedState = new WebMBufferedState;
+}
+
+uint32_t WebMDemuxer::GetNumberTracks(TrackInfo::TrackType aType) const {
+ switch (aType) {
+ case TrackInfo::kAudioTrack:
+ return mHasAudio ? 1 : 0;
+ case TrackInfo::kVideoTrack:
+ return mHasVideo ? 1 : 0;
+ default:
+ return 0;
+ }
+}
+
+UniquePtr<TrackInfo> WebMDemuxer::GetTrackInfo(TrackInfo::TrackType aType,
+ size_t aTrackNumber) const {
+ switch (aType) {
+ case TrackInfo::kAudioTrack:
+ return mInfo.mAudio.Clone();
+ case TrackInfo::kVideoTrack:
+ return mInfo.mVideo.Clone();
+ default:
+ return nullptr;
+ }
+}
+
+already_AddRefed<MediaTrackDemuxer> WebMDemuxer::GetTrackDemuxer(
+ TrackInfo::TrackType aType, uint32_t aTrackNumber) {
+ if (GetNumberTracks(aType) <= aTrackNumber) {
+ return nullptr;
+ }
+ RefPtr<WebMTrackDemuxer> e = new WebMTrackDemuxer(this, aType, aTrackNumber);
+ DDLINKCHILD("track demuxer", e.get());
+ mDemuxers.AppendElement(e);
+
+ return e.forget();
+}
+
+void WebMDemuxer::Reset(TrackInfo::TrackType aType) {
+ if (aType == TrackInfo::kVideoTrack) {
+ mVideoPackets.Reset();
+ } else {
+ mAudioPackets.Reset();
+ }
+}
+
+nsresult WebMDemuxer::ReadMetadata() {
+ int r = mVideoContext.Init();
+ if (r == -1) {
+ WEBM_DEBUG("mVideoContext::Init failure");
+ return NS_ERROR_FAILURE;
+ }
+ if (mAudioContext.Init() == -1) {
+ WEBM_DEBUG("mAudioContext::Init failure");
+ return NS_ERROR_FAILURE;
+ }
+
+ // For reading the metadata we can only use the video resource/context.
+ MediaResourceIndex& resource = Resource(TrackInfo::kVideoTrack);
+ nestegg* context = Context(TrackInfo::kVideoTrack);
+
+ {
+ // Check how much data nestegg read and force feed it to BufferedState.
+ RefPtr<MediaByteBuffer> buffer = resource.MediaReadAt(0, resource.Tell());
+ if (!buffer) {
+ WEBM_DEBUG("resource.MediaReadAt error");
+ return NS_ERROR_FAILURE;
+ }
+ mBufferedState->NotifyDataArrived(buffer->Elements(), buffer->Length(), 0);
+ if (mBufferedState->GetInitEndOffset() < 0) {
+ WEBM_DEBUG("Couldn't find init end");
+ return NS_ERROR_FAILURE;
+ }
+ MOZ_ASSERT(mBufferedState->GetInitEndOffset() <= resource.Tell());
+ }
+ mInitData = resource.MediaReadAt(0, mBufferedState->GetInitEndOffset());
+ if (!mInitData ||
+ mInitData->Length() != size_t(mBufferedState->GetInitEndOffset())) {
+ WEBM_DEBUG("Couldn't read init data");
+ return NS_ERROR_FAILURE;
+ }
+
+ unsigned int ntracks = 0;
+ r = nestegg_track_count(context, &ntracks);
+ if (r == -1) {
+ WEBM_DEBUG("nestegg_track_count error");
+ return NS_ERROR_FAILURE;
+ }
+
+ for (unsigned int track = 0; track < ntracks; ++track) {
+ int id = nestegg_track_codec_id(context, track);
+ if (id == -1) {
+ WEBM_DEBUG("nestegg_track_codec_id error");
+ return NS_ERROR_FAILURE;
+ }
+ int type = nestegg_track_type(context, track);
+ if (type == NESTEGG_TRACK_VIDEO && !mHasVideo) {
+ nestegg_video_params params;
+ r = nestegg_track_video_params(context, track, &params);
+ if (r == -1) {
+ WEBM_DEBUG("nestegg_track_video_params error");
+ return NS_ERROR_FAILURE;
+ }
+ mVideoCodec = nestegg_track_codec_id(context, track);
+ switch (mVideoCodec) {
+ case NESTEGG_CODEC_VP8:
+ mInfo.mVideo.mMimeType = "video/vp8";
+ break;
+ case NESTEGG_CODEC_VP9:
+ mInfo.mVideo.mMimeType = "video/vp9";
+ break;
+ case NESTEGG_CODEC_AV1:
+ mInfo.mVideo.mMimeType = "video/av1";
+ break;
+ default:
+ NS_WARNING("Unknown WebM video codec");
+ return NS_ERROR_FAILURE;
+ }
+
+ mInfo.mVideo.mColorPrimaries = gfxUtils::CicpToColorPrimaries(
+ static_cast<gfx::CICP::ColourPrimaries>(params.primaries),
+ gMediaDemuxerLog);
+
+ // For VPX, this is our only chance to capture the transfer
+ // characteristics, which we can't get from a VPX bitstream later.
+ // We only need this value if the video is using the BT2020
+ // colorspace, which will be determined on a per-frame basis later.
+ mInfo.mVideo.mTransferFunction = gfxUtils::CicpToTransferFunction(
+ static_cast<gfx::CICP::TransferCharacteristics>(
+ params.transfer_characteristics));
+
+ // Picture region, taking into account cropping, before scaling
+ // to the display size.
+ unsigned int cropH = params.crop_right + params.crop_left;
+ unsigned int cropV = params.crop_bottom + params.crop_top;
+ gfx::IntRect pictureRect(params.crop_left, params.crop_top,
+ params.width - cropH, params.height - cropV);
+
+ // If the cropping data appears invalid then use the frame data
+ if (pictureRect.width <= 0 || pictureRect.height <= 0 ||
+ pictureRect.x < 0 || pictureRect.y < 0) {
+ pictureRect.x = 0;
+ pictureRect.y = 0;
+ pictureRect.width = params.width;
+ pictureRect.height = params.height;
+ }
+
+ // Validate the container-reported frame and pictureRect sizes. This
+ // ensures that our video frame creation code doesn't overflow.
+ gfx::IntSize displaySize(params.display_width, params.display_height);
+ gfx::IntSize frameSize(params.width, params.height);
+ if (!IsValidVideoRegion(frameSize, pictureRect, displaySize)) {
+ // Video track's frame sizes will overflow. Ignore the video track.
+ continue;
+ }
+
+ mVideoTrack = track;
+ mHasVideo = true;
+
+ mInfo.mVideo.mDisplay = displaySize;
+ mInfo.mVideo.mImage = frameSize;
+ mInfo.mVideo.SetImageRect(pictureRect);
+ mInfo.mVideo.SetAlpha(params.alpha_mode);
+
+ switch (params.stereo_mode) {
+ case NESTEGG_VIDEO_MONO:
+ mInfo.mVideo.mStereoMode = StereoMode::MONO;
+ break;
+ case NESTEGG_VIDEO_STEREO_LEFT_RIGHT:
+ mInfo.mVideo.mStereoMode = StereoMode::LEFT_RIGHT;
+ break;
+ case NESTEGG_VIDEO_STEREO_BOTTOM_TOP:
+ mInfo.mVideo.mStereoMode = StereoMode::BOTTOM_TOP;
+ break;
+ case NESTEGG_VIDEO_STEREO_TOP_BOTTOM:
+ mInfo.mVideo.mStereoMode = StereoMode::TOP_BOTTOM;
+ break;
+ case NESTEGG_VIDEO_STEREO_RIGHT_LEFT:
+ mInfo.mVideo.mStereoMode = StereoMode::RIGHT_LEFT;
+ break;
+ }
+ uint64_t duration = 0;
+ r = nestegg_duration(context, &duration);
+ if (!r) {
+ mInfo.mVideo.mDuration = TimeUnit::FromNanoseconds(duration);
+ }
+ WEBM_DEBUG("stream duration: %lf\n", mInfo.mVideo.mDuration.ToSeconds());
+ mInfo.mVideo.mCrypto = GetTrackCrypto(TrackInfo::kVideoTrack, track);
+ if (mInfo.mVideo.mCrypto.IsEncrypted()) {
+ MOZ_ASSERT(mInfo.mVideo.mCrypto.mCryptoScheme == CryptoScheme::Cenc,
+ "WebM should only use cenc scheme");
+ mCrypto.AddInitData(u"webm"_ns, mInfo.mVideo.mCrypto.mKeyId);
+ }
+ } else if (type == NESTEGG_TRACK_AUDIO && !mHasAudio) {
+ nestegg_audio_params params;
+ r = nestegg_track_audio_params(context, track, &params);
+ if (r == -1) {
+ WEBM_DEBUG("nestegg_track_audio_params error");
+ return NS_ERROR_FAILURE;
+ }
+ if (params.rate >
+ static_cast<decltype(params.rate)>(AudioInfo::MAX_RATE) ||
+ params.rate <= static_cast<decltype(params.rate)>(0) ||
+ params.channels > AudioConfig::ChannelLayout::MAX_CHANNELS) {
+ WEBM_DEBUG("Invalid audio param rate: %lf channel count: %d",
+ params.rate, params.channels);
+ return NS_ERROR_DOM_MEDIA_METADATA_ERR;
+ }
+
+ mAudioTrack = track;
+ mHasAudio = true;
+ mAudioCodec = nestegg_track_codec_id(context, track);
+ if (mAudioCodec == NESTEGG_CODEC_VORBIS) {
+ mInfo.mAudio.mCodecSpecificConfig =
+ AudioCodecSpecificVariant{VorbisCodecSpecificData{}};
+ mInfo.mAudio.mMimeType = "audio/vorbis";
+ } else if (mAudioCodec == NESTEGG_CODEC_OPUS) {
+ uint64_t codecDelayUs = params.codec_delay / 1000;
+ mInfo.mAudio.mMimeType = "audio/opus";
+ OpusCodecSpecificData opusCodecSpecificData;
+ opusCodecSpecificData.mContainerCodecDelayMicroSeconds =
+ AssertedCast<int64_t>(codecDelayUs);
+ mInfo.mAudio.mCodecSpecificConfig =
+ AudioCodecSpecificVariant{std::move(opusCodecSpecificData)};
+ WEBM_DEBUG("Preroll for Opus: %" PRIu64, codecDelayUs);
+ }
+ mSeekPreroll = params.seek_preroll;
+ mInfo.mAudio.mRate = AssertedCast<uint32_t>(params.rate);
+ mInfo.mAudio.mChannels = params.channels;
+
+ unsigned int nheaders = 0;
+ r = nestegg_track_codec_data_count(context, track, &nheaders);
+ if (r == -1) {
+ WEBM_DEBUG("nestegg_track_codec_data_count error");
+ return NS_ERROR_FAILURE;
+ }
+
+ AutoTArray<const unsigned char*, 4> headers;
+ AutoTArray<size_t, 4> headerLens;
+ for (uint32_t header = 0; header < nheaders; ++header) {
+ unsigned char* data = 0;
+ size_t length = 0;
+ r = nestegg_track_codec_data(context, track, header, &data, &length);
+ if (r == -1) {
+ WEBM_DEBUG("nestegg_track_codec_data error");
+ return NS_ERROR_FAILURE;
+ }
+ headers.AppendElement(data);
+ headerLens.AppendElement(length);
+ }
+
+ // Vorbis has 3 headers, convert to Xiph extradata format to send them to
+ // the demuxer.
+ // TODO: This is already the format WebM stores them in. Would be nice
+ // to avoid having libnestegg split them only for us to pack them again,
+ // but libnestegg does not give us an API to access this data directly.
+ RefPtr<MediaByteBuffer> audioCodecSpecificBlob =
+ GetAudioCodecSpecificBlob(mInfo.mAudio.mCodecSpecificConfig);
+ if (nheaders > 1) {
+ if (!XiphHeadersToExtradata(audioCodecSpecificBlob, headers,
+ headerLens)) {
+ WEBM_DEBUG("Couldn't parse Xiph headers");
+ return NS_ERROR_FAILURE;
+ }
+ } else {
+ audioCodecSpecificBlob->AppendElements(headers[0], headerLens[0]);
+ }
+ uint64_t duration = 0;
+ r = nestegg_duration(context, &duration);
+ if (!r) {
+ mInfo.mAudio.mDuration = TimeUnit::FromNanoseconds(duration);
+ WEBM_DEBUG("audio track duration: %lf",
+ mInfo.mAudio.mDuration.ToSeconds());
+ }
+ mInfo.mAudio.mCrypto = GetTrackCrypto(TrackInfo::kAudioTrack, track);
+ if (mInfo.mAudio.mCrypto.IsEncrypted()) {
+ MOZ_ASSERT(mInfo.mAudio.mCrypto.mCryptoScheme == CryptoScheme::Cenc,
+ "WebM should only use cenc scheme");
+ mCrypto.AddInitData(u"webm"_ns, mInfo.mAudio.mCrypto.mKeyId);
+ }
+ }
+ }
+ WEBM_DEBUG("Read metadata OK");
+ return NS_OK;
+}
+
+bool WebMDemuxer::IsSeekable() const {
+ return Context(TrackInfo::kVideoTrack) &&
+ nestegg_has_cues(Context(TrackInfo::kVideoTrack));
+}
+
+bool WebMDemuxer::IsSeekableOnlyInBufferedRanges() const {
+ return Context(TrackInfo::kVideoTrack) &&
+ !nestegg_has_cues(Context(TrackInfo::kVideoTrack));
+}
+
+void WebMDemuxer::EnsureUpToDateIndex() {
+ if (!mNeedReIndex || !mInitData) {
+ return;
+ }
+ AutoPinned<MediaResource> resource(
+ Resource(TrackInfo::kVideoTrack).GetResource());
+ MediaByteRangeSet byteRanges;
+ nsresult rv = resource->GetCachedRanges(byteRanges);
+ if (NS_FAILED(rv) || byteRanges.IsEmpty()) {
+ return;
+ }
+ mBufferedState->UpdateIndex(byteRanges, resource);
+
+ mNeedReIndex = false;
+
+ if (!mIsMediaSource) {
+ return;
+ }
+ mLastWebMBlockOffset = mBufferedState->GetLastBlockOffset();
+ MOZ_ASSERT(mLastWebMBlockOffset <= resource->GetLength());
+}
+
+void WebMDemuxer::NotifyDataArrived() {
+ WEBM_DEBUG("");
+ mNeedReIndex = true;
+}
+
+void WebMDemuxer::NotifyDataRemoved() {
+ mBufferedState->Reset();
+ if (mInitData) {
+ mBufferedState->NotifyDataArrived(mInitData->Elements(),
+ mInitData->Length(), 0);
+ }
+ mNeedReIndex = true;
+}
+
+UniquePtr<EncryptionInfo> WebMDemuxer::GetCrypto() {
+ return mCrypto.IsEncrypted() ? MakeUnique<EncryptionInfo>(mCrypto) : nullptr;
+}
+
+CryptoTrack WebMDemuxer::GetTrackCrypto(TrackInfo::TrackType aType,
+ size_t aTrackNumber) {
+ const int WEBM_IV_SIZE = 16;
+ const unsigned char* contentEncKeyId;
+ size_t contentEncKeyIdLength;
+ CryptoTrack crypto;
+ nestegg* context = Context(aType);
+
+ int r = nestegg_track_content_enc_key_id(
+ context, aTrackNumber, &contentEncKeyId, &contentEncKeyIdLength);
+
+ if (r == -1) {
+ WEBM_DEBUG("nestegg_track_content_enc_key_id failed r=%d", r);
+ return crypto;
+ }
+
+ uint32_t i;
+ nsTArray<uint8_t> initData;
+ for (i = 0; i < contentEncKeyIdLength; i++) {
+ initData.AppendElement(contentEncKeyId[i]);
+ }
+
+ if (!initData.IsEmpty()) {
+ // Webm only uses a cenc style scheme.
+ crypto.mCryptoScheme = CryptoScheme::Cenc;
+ crypto.mIVSize = WEBM_IV_SIZE;
+ crypto.mKeyId = std::move(initData);
+ }
+
+ return crypto;
+}
+
+nsresult WebMDemuxer::GetNextPacket(TrackInfo::TrackType aType,
+ MediaRawDataQueue* aSamples) {
+ if (mIsMediaSource) {
+ // To ensure mLastWebMBlockOffset is properly up to date.
+ EnsureUpToDateIndex();
+ }
+
+ RefPtr<NesteggPacketHolder> holder;
+ nsresult rv = NextPacket(aType, holder);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ int r = 0;
+ unsigned int count = 0;
+ r = nestegg_packet_count(holder->Packet(), &count);
+ if (r == -1) {
+ return NS_ERROR_DOM_MEDIA_DEMUXER_ERR;
+ }
+ int64_t tstamp = holder->Timestamp();
+ int64_t duration = holder->Duration();
+
+ // The end time of this frame is the start time of the next frame. Fetch
+ // the timestamp of the next packet for this track. If we've reached the
+ // end of the resource, use the file's duration as the end time of this
+ // video frame.
+ RefPtr<NesteggPacketHolder> next_holder;
+ rv = NextPacket(aType, next_holder);
+ if (NS_FAILED(rv) && rv != NS_ERROR_DOM_MEDIA_END_OF_STREAM) {
+ return rv;
+ }
+
+ int64_t next_tstamp = INT64_MIN;
+ auto calculateNextTimestamp = [&](auto&& pushPacket, auto&& lastFrameTime,
+ int64_t trackEndTime) {
+ if (next_holder) {
+ next_tstamp = next_holder->Timestamp();
+ (this->*pushPacket)(next_holder);
+ } else if (duration >= 0) {
+ next_tstamp = tstamp + duration;
+ } else if (lastFrameTime.isSome()) {
+ next_tstamp = tstamp + (tstamp - lastFrameTime.ref());
+ } else if (mIsMediaSource) {
+ (this->*pushPacket)(holder);
+ } else {
+ // If we can't get frame's duration, it means either we need to wait for
+ // more data for MSE case or this is the last frame for file resource
+ // case.
+ if (tstamp > trackEndTime) {
+ // This shouldn't happen, but some muxers give incorrect durations to
+ // segments, then have samples appear beyond those durations.
+ WEBM_DEBUG("Found tstamp=%" PRIi64 " > trackEndTime=%" PRIi64
+ " while calculating next timestamp! Indicates a bad mux! "
+ "Will use tstamp value.",
+ tstamp, trackEndTime);
+ }
+ next_tstamp = std::max<int64_t>(tstamp, trackEndTime);
+ }
+ lastFrameTime = Some(tstamp);
+ };
+
+ if (aType == TrackInfo::kAudioTrack) {
+ calculateNextTimestamp(&WebMDemuxer::PushAudioPacket, mLastAudioFrameTime,
+ mInfo.mAudio.mDuration.ToMicroseconds());
+ } else {
+ calculateNextTimestamp(&WebMDemuxer::PushVideoPacket, mLastVideoFrameTime,
+ mInfo.mVideo.mDuration.ToMicroseconds());
+ }
+
+ if (mIsMediaSource && next_tstamp == INT64_MIN) {
+ return NS_ERROR_DOM_MEDIA_END_OF_STREAM;
+ }
+
+ int64_t discardPadding = 0;
+ if (aType == TrackInfo::kAudioTrack) {
+ (void)nestegg_packet_discard_padding(holder->Packet(), &discardPadding);
+ }
+
+ int packetEncryption = nestegg_packet_encryption(holder->Packet());
+
+ for (uint32_t i = 0; i < count; ++i) {
+ unsigned char* data = nullptr;
+ size_t length;
+ r = nestegg_packet_data(holder->Packet(), i, &data, &length);
+ if (r == -1) {
+ WEBM_DEBUG("nestegg_packet_data failed r=%d", r);
+ return NS_ERROR_DOM_MEDIA_DEMUXER_ERR;
+ }
+ unsigned char* alphaData = nullptr;
+ size_t alphaLength = 0;
+ // Check packets for alpha information if file has declared alpha frames
+ // may be present.
+ if (mInfo.mVideo.HasAlpha()) {
+ r = nestegg_packet_additional_data(holder->Packet(), 1, &alphaData,
+ &alphaLength);
+ if (r == -1) {
+ WEBM_DEBUG(
+ "nestegg_packet_additional_data failed to retrieve alpha data r=%d",
+ r);
+ }
+ }
+ bool isKeyframe = false;
+ if (aType == TrackInfo::kAudioTrack) {
+ isKeyframe = true;
+ } else if (aType == TrackInfo::kVideoTrack) {
+ if (packetEncryption == NESTEGG_PACKET_HAS_SIGNAL_BYTE_ENCRYPTED ||
+ packetEncryption == NESTEGG_PACKET_HAS_SIGNAL_BYTE_PARTITIONED) {
+ // Packet is encrypted, can't peek, use packet info
+ isKeyframe = nestegg_packet_has_keyframe(holder->Packet()) ==
+ NESTEGG_PACKET_HAS_KEYFRAME_TRUE;
+ } else {
+ MOZ_ASSERT(
+ packetEncryption == NESTEGG_PACKET_HAS_SIGNAL_BYTE_UNENCRYPTED ||
+ packetEncryption == NESTEGG_PACKET_HAS_SIGNAL_BYTE_FALSE,
+ "Unencrypted packet expected");
+ auto sample = Span(data, length);
+ auto alphaSample = Span(alphaData, alphaLength);
+
+ switch (mVideoCodec) {
+ case NESTEGG_CODEC_VP8:
+ isKeyframe = VPXDecoder::IsKeyframe(sample, VPXDecoder::Codec::VP8);
+ if (isKeyframe && alphaLength) {
+ isKeyframe =
+ VPXDecoder::IsKeyframe(alphaSample, VPXDecoder::Codec::VP8);
+ }
+ break;
+ case NESTEGG_CODEC_VP9:
+ isKeyframe = VPXDecoder::IsKeyframe(sample, VPXDecoder::Codec::VP9);
+ if (isKeyframe && alphaLength) {
+ isKeyframe =
+ VPXDecoder::IsKeyframe(alphaSample, VPXDecoder::Codec::VP9);
+ }
+ break;
+#ifdef MOZ_AV1
+ case NESTEGG_CODEC_AV1:
+ isKeyframe = AOMDecoder::IsKeyframe(sample);
+ if (isKeyframe && alphaLength) {
+ isKeyframe = AOMDecoder::IsKeyframe(alphaSample);
+ }
+ break;
+#endif
+ default:
+ NS_WARNING("Cannot detect keyframes in unknown WebM video codec");
+ return NS_ERROR_FAILURE;
+ }
+ }
+ }
+
+ WEBM_DEBUG("push sample tstamp: %" PRId64 " next_tstamp: %" PRId64
+ " length: %zu kf: %d",
+ tstamp, next_tstamp, length, isKeyframe);
+ RefPtr<MediaRawData> sample;
+ if (mInfo.mVideo.HasAlpha() && alphaLength != 0) {
+ sample = new MediaRawData(data, length, alphaData, alphaLength);
+ if ((length && !sample->Data()) ||
+ (alphaLength && !sample->AlphaData())) {
+ // OOM.
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ } else {
+ sample = new MediaRawData(data, length);
+ if (length && !sample->Data()) {
+ // OOM.
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+ sample->mTimecode = TimeUnit::FromMicroseconds(tstamp);
+ sample->mTime = TimeUnit::FromMicroseconds(tstamp);
+ if (next_tstamp > tstamp) {
+ sample->mDuration = TimeUnit::FromMicroseconds(next_tstamp - tstamp);
+ }
+ sample->mOffset = holder->Offset();
+ sample->mKeyframe = isKeyframe;
+ if (discardPadding && i == count - 1) {
+ CheckedInt64 discardFrames;
+ if (discardPadding < 0) {
+ // This is an invalid value as discard padding should never be negative.
+ // Set to maximum value so that the decoder will reject it as it's
+ // greater than the number of frames available.
+ discardFrames = INT32_MAX;
+ WEBM_DEBUG("Invalid negative discard padding");
+ } else {
+ discardFrames = TimeUnitToFrames(
+ TimeUnit::FromNanoseconds(discardPadding), mInfo.mAudio.mRate);
+ }
+ if (discardFrames.isValid()) {
+ sample->mDiscardPadding = discardFrames.value();
+ }
+ }
+
+ if (packetEncryption == NESTEGG_PACKET_HAS_SIGNAL_BYTE_ENCRYPTED ||
+ packetEncryption == NESTEGG_PACKET_HAS_SIGNAL_BYTE_PARTITIONED) {
+ UniquePtr<MediaRawDataWriter> writer(sample->CreateWriter());
+ unsigned char const* iv;
+ size_t ivLength;
+ nestegg_packet_iv(holder->Packet(), &iv, &ivLength);
+ writer->mCrypto.mCryptoScheme = CryptoScheme::Cenc;
+ writer->mCrypto.mIVSize = ivLength;
+ if (ivLength == 0) {
+ // Frame is not encrypted. This shouldn't happen as it means the
+ // encryption bit is set on a frame with no IV, but we gracefully
+ // handle incase.
+ MOZ_ASSERT_UNREACHABLE(
+ "Unencrypted packets should not have the encryption bit set!");
+ WEBM_DEBUG("Unencrypted packet with encryption bit set");
+ writer->mCrypto.mPlainSizes.AppendElement(length);
+ writer->mCrypto.mEncryptedSizes.AppendElement(0);
+ } else {
+ // Frame is encrypted
+ writer->mCrypto.mIV.AppendElements(iv, 8);
+ // Iv from a sample is 64 bits, must be padded with 64 bits more 0s
+ // in compliance with spec
+ for (uint32_t i = 0; i < 8; i++) {
+ writer->mCrypto.mIV.AppendElement(0);
+ }
+
+ if (packetEncryption == NESTEGG_PACKET_HAS_SIGNAL_BYTE_ENCRYPTED) {
+ writer->mCrypto.mPlainSizes.AppendElement(0);
+ writer->mCrypto.mEncryptedSizes.AppendElement(length);
+ } else if (packetEncryption ==
+ NESTEGG_PACKET_HAS_SIGNAL_BYTE_PARTITIONED) {
+ uint8_t numPartitions = 0;
+ const uint32_t* partitions = NULL;
+ nestegg_packet_offsets(holder->Packet(), &partitions, &numPartitions);
+
+ // WebM stores a list of 'partitions' in the data, which alternate
+ // clear, encrypted. The data in the first partition is always clear.
+ // So, and sample might look as follows:
+ // 00|XXXX|000|XX, where | represents a partition, 0 a clear byte and
+ // X an encrypted byte. If the first bytes in sample are unencrypted,
+ // the first partition will be at zero |XXXX|000|XX.
+ //
+ // As GMP expects the lengths of the clear and encrypted chunks of
+ // data, we calculate these from the difference between the last two
+ // partitions.
+ uint32_t lastOffset = 0;
+ bool encrypted = false;
+
+ for (uint8_t i = 0; i < numPartitions; i++) {
+ uint32_t partition = partitions[i];
+ uint32_t currentLength = partition - lastOffset;
+
+ if (encrypted) {
+ writer->mCrypto.mEncryptedSizes.AppendElement(currentLength);
+ } else {
+ writer->mCrypto.mPlainSizes.AppendElement(currentLength);
+ }
+
+ encrypted = !encrypted;
+ lastOffset = partition;
+
+ MOZ_ASSERT(lastOffset <= length);
+ }
+
+ // Add the data between the last offset and the end of the data.
+ // 000|XXX|000
+ // ^---^
+ if (encrypted) {
+ writer->mCrypto.mEncryptedSizes.AppendElement(length - lastOffset);
+ } else {
+ writer->mCrypto.mPlainSizes.AppendElement(length - lastOffset);
+ }
+
+ // Make sure we have an equal number of encrypted and plain sizes (GMP
+ // expects this). This simple check is sufficient as there are two
+ // possible cases at this point:
+ // 1. The number of samples are even (so we don't need to do anything)
+ // 2. There is one more clear sample than encrypted samples, so add a
+ // zero length encrypted chunk.
+ // There can never be more encrypted partitions than clear partitions
+ // due to the alternating structure of the WebM samples and the
+ // restriction that the first chunk is always clear.
+ if (numPartitions % 2 == 0) {
+ writer->mCrypto.mEncryptedSizes.AppendElement(0);
+ }
+
+ // Assert that the lengths of the encrypted and plain samples add to
+ // the length of the data.
+ MOZ_ASSERT(
+ ((size_t)(std::accumulate(writer->mCrypto.mPlainSizes.begin(),
+ writer->mCrypto.mPlainSizes.end(), 0) +
+ std::accumulate(writer->mCrypto.mEncryptedSizes.begin(),
+ writer->mCrypto.mEncryptedSizes.end(),
+ 0)) == length));
+ }
+ }
+ }
+ aSamples->Push(sample);
+ }
+ return NS_OK;
+}
+
+nsresult WebMDemuxer::NextPacket(TrackInfo::TrackType aType,
+ RefPtr<NesteggPacketHolder>& aPacket) {
+ bool isVideo = aType == TrackInfo::kVideoTrack;
+
+ // Flag to indicate that we do need to playback these types of
+ // packets.
+ bool hasType = isVideo ? mHasVideo : mHasAudio;
+
+ if (!hasType) {
+ return NS_ERROR_DOM_MEDIA_DEMUXER_ERR;
+ }
+
+ // The packet queue for the type that we are interested in.
+ WebMPacketQueue& packets = isVideo ? mVideoPackets : mAudioPackets;
+
+ if (packets.GetSize() > 0) {
+ aPacket = packets.PopFront();
+ return NS_OK;
+ }
+
+ // Track we are interested in
+ uint32_t ourTrack = isVideo ? mVideoTrack : mAudioTrack;
+
+ do {
+ RefPtr<NesteggPacketHolder> holder;
+ nsresult rv = DemuxPacket(aType, holder);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (!holder) {
+ return NS_ERROR_DOM_MEDIA_DEMUXER_ERR;
+ }
+
+ if (ourTrack == holder->Track()) {
+ aPacket = holder;
+ return NS_OK;
+ }
+ } while (true);
+}
+
+nsresult WebMDemuxer::DemuxPacket(TrackInfo::TrackType aType,
+ RefPtr<NesteggPacketHolder>& aPacket) {
+ nestegg_packet* packet;
+ int r = nestegg_read_packet(Context(aType), &packet);
+ if (r == 0) {
+ nestegg_read_reset(Context(aType));
+ return NS_ERROR_DOM_MEDIA_END_OF_STREAM;
+ } else if (r < 0) {
+ return NS_ERROR_DOM_MEDIA_DEMUXER_ERR;
+ }
+
+ unsigned int track = 0;
+ r = nestegg_packet_track(packet, &track);
+ if (r == -1) {
+ return NS_ERROR_DOM_MEDIA_DEMUXER_ERR;
+ }
+
+ int64_t offset = Resource(aType).Tell();
+ RefPtr<NesteggPacketHolder> holder = new NesteggPacketHolder();
+ if (!holder->Init(packet, offset, track, false)) {
+ return NS_ERROR_DOM_MEDIA_DEMUXER_ERR;
+ }
+
+ aPacket = holder;
+ return NS_OK;
+}
+
+void WebMDemuxer::PushAudioPacket(NesteggPacketHolder* aItem) {
+ mAudioPackets.PushFront(aItem);
+}
+
+void WebMDemuxer::PushVideoPacket(NesteggPacketHolder* aItem) {
+ mVideoPackets.PushFront(aItem);
+}
+
+nsresult WebMDemuxer::SeekInternal(TrackInfo::TrackType aType,
+ const TimeUnit& aTarget) {
+ EnsureUpToDateIndex();
+ uint32_t trackToSeek = mHasVideo ? mVideoTrack : mAudioTrack;
+ MOZ_ASSERT(aTarget.ToNanoseconds() >= 0, "Seek time can't be negative");
+ uint64_t target = static_cast<uint64_t>(aTarget.ToNanoseconds());
+ WEBM_DEBUG("Seeking to %lf", aTarget.ToSeconds());
+
+ Reset(aType);
+
+ if (mSeekPreroll) {
+ uint64_t startTime = 0;
+ if (!mBufferedState->GetStartTime(&startTime)) {
+ startTime = 0;
+ }
+ WEBM_DEBUG("Seek Target: %f",
+ TimeUnit::FromNanoseconds(target).ToSeconds());
+ if (target < mSeekPreroll || target - mSeekPreroll < startTime) {
+ target = startTime;
+ } else {
+ target -= mSeekPreroll;
+ }
+ WEBM_DEBUG("SeekPreroll: %f StartTime: %f Adjusted Target: %f",
+ TimeUnit::FromNanoseconds(mSeekPreroll).ToSeconds(),
+ TimeUnit::FromNanoseconds(startTime).ToSeconds(),
+ TimeUnit::FromNanoseconds(target).ToSeconds());
+ }
+ int r = nestegg_track_seek(Context(aType), trackToSeek, target);
+ if (r == -1) {
+ WEBM_DEBUG("track_seek for track %u to %f failed, r=%d", trackToSeek,
+ TimeUnit::FromNanoseconds(target).ToSeconds(), r);
+ // Try seeking directly based on cluster information in memory.
+ int64_t offset = 0;
+ bool rv = mBufferedState->GetOffsetForTime(target, &offset);
+ if (!rv) {
+ WEBM_DEBUG("mBufferedState->GetOffsetForTime failed too");
+ return NS_ERROR_FAILURE;
+ }
+
+ if (offset < 0) {
+ WEBM_DEBUG("Unknow byte offset time for seek target %" PRIu64 "ns",
+ target);
+ return NS_ERROR_FAILURE;
+ }
+
+ r = nestegg_offset_seek(Context(aType), static_cast<uint64_t>(offset));
+ if (r == -1) {
+ WEBM_DEBUG("and nestegg_offset_seek to %" PRIu64 " failed", offset);
+ return NS_ERROR_FAILURE;
+ }
+ WEBM_DEBUG("got offset from buffered state: %" PRIu64 "", offset);
+ }
+
+ if (aType == TrackInfo::kAudioTrack) {
+ mLastAudioFrameTime.reset();
+ } else {
+ mLastVideoFrameTime.reset();
+ }
+
+ return NS_OK;
+}
+
+bool WebMDemuxer::IsBufferedIntervalValid(uint64_t start, uint64_t end) {
+ if (start > end) {
+ // Buffered ranges are clamped to the media's start time and duration. Any
+ // frames with timestamps outside that range are ignored, see bug 1697641
+ // for more info.
+ WEBM_DEBUG("Ignoring range %" PRIu64 "-%" PRIu64
+ ", due to invalid interval (start > end).",
+ start, end);
+ return false;
+ }
+
+ auto startTime = TimeUnit::FromNanoseconds(start);
+ auto endTime = TimeUnit::FromNanoseconds(end);
+
+ if (startTime.IsNegative() || endTime.IsNegative()) {
+ // We can get timestamps that are conceptually valid, but become
+ // negative due to uint64 -> int64 conversion from TimeUnit. We should
+ // not get negative timestamps, so guard against them.
+ WEBM_DEBUG(
+ "Invalid range %f-%f, likely result of uint64 -> int64 conversion.",
+ startTime.ToSeconds(), endTime.ToSeconds());
+ return false;
+ }
+
+ return true;
+}
+
+media::TimeIntervals WebMDemuxer::GetBuffered() {
+ EnsureUpToDateIndex();
+ AutoPinned<MediaResource> resource(
+ Resource(TrackInfo::kVideoTrack).GetResource());
+
+ media::TimeIntervals buffered;
+
+ MediaByteRangeSet ranges;
+ nsresult rv = resource->GetCachedRanges(ranges);
+ if (NS_FAILED(rv)) {
+ return media::TimeIntervals();
+ }
+ uint64_t duration = 0;
+ uint64_t startOffset = 0;
+ if (!nestegg_duration(Context(TrackInfo::kVideoTrack), &duration)) {
+ if (mBufferedState->GetStartTime(&startOffset)) {
+ duration += startOffset;
+ }
+ WEBM_DEBUG("Duration: %f StartTime: %f",
+ TimeUnit::FromNanoseconds(duration).ToSeconds(),
+ TimeUnit::FromNanoseconds(startOffset).ToSeconds());
+ }
+ for (uint32_t index = 0; index < ranges.Length(); index++) {
+ uint64_t start, end;
+ bool rv = mBufferedState->CalculateBufferedForRange(
+ ranges[index].mStart, ranges[index].mEnd, &start, &end);
+ if (rv) {
+ NS_ASSERTION(startOffset <= start,
+ "startOffset negative or larger than start time");
+
+ if (duration && end > duration) {
+ WEBM_DEBUG("limit range to duration, end: %f duration: %f",
+ TimeUnit::FromNanoseconds(end).ToSeconds(),
+ TimeUnit::FromNanoseconds(duration).ToSeconds());
+ end = duration;
+ }
+
+ if (!IsBufferedIntervalValid(start, end)) {
+ WEBM_DEBUG("Invalid interval, bailing");
+ break;
+ }
+
+ auto startTime = TimeUnit::FromNanoseconds(start);
+ auto endTime = TimeUnit::FromNanoseconds(end);
+
+ WEBM_DEBUG("add range %f-%f", startTime.ToSeconds(), endTime.ToSeconds());
+ buffered += media::TimeInterval(startTime, endTime);
+ }
+ }
+ return buffered;
+}
+
+bool WebMDemuxer::GetOffsetForTime(uint64_t aTime, int64_t* aOffset) {
+ EnsureUpToDateIndex();
+ return mBufferedState && mBufferedState->GetOffsetForTime(aTime, aOffset);
+}
+
+// WebMTrackDemuxer
+WebMTrackDemuxer::WebMTrackDemuxer(WebMDemuxer* aParent,
+ TrackInfo::TrackType aType,
+ uint32_t aTrackNumber)
+ : mParent(aParent), mType(aType), mNeedKeyframe(true) {
+ mInfo = mParent->GetTrackInfo(aType, aTrackNumber);
+ MOZ_ASSERT(mInfo);
+}
+
+WebMTrackDemuxer::~WebMTrackDemuxer() { mSamples.Reset(); }
+
+UniquePtr<TrackInfo> WebMTrackDemuxer::GetInfo() const {
+ return mInfo->Clone();
+}
+
+RefPtr<WebMTrackDemuxer::SeekPromise> WebMTrackDemuxer::Seek(
+ const TimeUnit& aTime) {
+ // Seeks to aTime. Upon success, SeekPromise will be resolved with the
+ // actual time seeked to. Typically the random access point time
+
+ auto seekTime = aTime;
+ bool keyframe = false;
+
+ mNeedKeyframe = true;
+
+ do {
+ mSamples.Reset();
+ mParent->SeekInternal(mType, seekTime);
+ nsresult rv = mParent->GetNextPacket(mType, &mSamples);
+ if (NS_FAILED(rv)) {
+ if (rv == NS_ERROR_DOM_MEDIA_END_OF_STREAM) {
+ // Ignore the error for now, the next GetSample will be rejected with
+ // EOS.
+ return SeekPromise::CreateAndResolve(TimeUnit::Zero(), __func__);
+ }
+ return SeekPromise::CreateAndReject(rv, __func__);
+ }
+
+ // Check what time we actually seeked to.
+ if (mSamples.GetSize() == 0) {
+ // We can't determine if the seek succeeded at this stage, so break the
+ // loop.
+ break;
+ }
+
+ for (const auto& sample : mSamples) {
+ seekTime = sample->mTime;
+ keyframe = sample->mKeyframe;
+ if (keyframe) {
+ break;
+ }
+ }
+ if (mType == TrackInfo::kVideoTrack &&
+ !mInfo->GetAsVideoInfo()->HasAlpha()) {
+ // We only perform a search for a keyframe on videos with alpha layer to
+ // prevent potential regression for normal video (even though invalid)
+ break;
+ }
+ if (!keyframe) {
+ // We didn't find any keyframe, attempt to seek to the previous cluster.
+ seekTime = mSamples.First()->mTime - TimeUnit::FromMicroseconds(1);
+ }
+ } while (!keyframe && seekTime >= TimeUnit::Zero());
+
+ SetNextKeyFrameTime();
+
+ return SeekPromise::CreateAndResolve(seekTime, __func__);
+}
+
+nsresult WebMTrackDemuxer::NextSample(RefPtr<MediaRawData>& aData) {
+ nsresult rv = NS_ERROR_DOM_MEDIA_END_OF_STREAM;
+ while (mSamples.GetSize() < 1 &&
+ NS_SUCCEEDED((rv = mParent->GetNextPacket(mType, &mSamples)))) {
+ }
+ if (mSamples.GetSize()) {
+ aData = mSamples.PopFront();
+ return NS_OK;
+ }
+ return rv;
+}
+
+RefPtr<WebMTrackDemuxer::SamplesPromise> WebMTrackDemuxer::GetSamples(
+ int32_t aNumSamples) {
+ RefPtr<SamplesHolder> samples = new SamplesHolder;
+ MOZ_ASSERT(aNumSamples);
+
+ nsresult rv = NS_ERROR_DOM_MEDIA_END_OF_STREAM;
+
+ while (aNumSamples) {
+ RefPtr<MediaRawData> sample;
+ rv = NextSample(sample);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+ // Ignore empty samples.
+ if (sample->Size() == 0) {
+ WEBM_DEBUG(
+ "0 sized sample encountered while getting samples, skipping it");
+ continue;
+ }
+ if (mNeedKeyframe && !sample->mKeyframe) {
+ continue;
+ }
+ if (!sample->HasValidTime()) {
+ return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_DEMUXER_ERR,
+ __func__);
+ }
+ mNeedKeyframe = false;
+ samples->AppendSample(sample);
+ aNumSamples--;
+ }
+
+ if (samples->GetSamples().IsEmpty()) {
+ return SamplesPromise::CreateAndReject(rv, __func__);
+ } else {
+ UpdateSamples(samples->GetSamples());
+ return SamplesPromise::CreateAndResolve(samples, __func__);
+ }
+}
+
+void WebMTrackDemuxer::SetNextKeyFrameTime() {
+ if (mType != TrackInfo::kVideoTrack || mParent->IsMediaSource()) {
+ return;
+ }
+
+ auto frameTime = TimeUnit::Invalid();
+
+ mNextKeyframeTime.reset();
+
+ MediaRawDataQueue skipSamplesQueue;
+ bool foundKeyframe = false;
+ while (!foundKeyframe && mSamples.GetSize()) {
+ RefPtr<MediaRawData> sample = mSamples.PopFront();
+ if (sample->mKeyframe) {
+ frameTime = sample->mTime;
+ foundKeyframe = true;
+ }
+ skipSamplesQueue.Push(sample.forget());
+ }
+ Maybe<int64_t> startTime;
+ if (skipSamplesQueue.GetSize()) {
+ const RefPtr<MediaRawData>& sample = skipSamplesQueue.First();
+ startTime.emplace(sample->mTimecode.ToMicroseconds());
+ }
+ // Demux and buffer frames until we find a keyframe.
+ RefPtr<MediaRawData> sample;
+ nsresult rv = NS_OK;
+ while (!foundKeyframe && NS_SUCCEEDED((rv = NextSample(sample)))) {
+ if (sample->mKeyframe) {
+ frameTime = sample->mTime;
+ foundKeyframe = true;
+ }
+ int64_t sampleTimecode = sample->mTimecode.ToMicroseconds();
+ skipSamplesQueue.Push(sample.forget());
+ if (!startTime) {
+ startTime.emplace(sampleTimecode);
+ } else if (!foundKeyframe &&
+ sampleTimecode > startTime.ref() + MAX_LOOK_AHEAD) {
+ WEBM_DEBUG("Couldn't find keyframe in a reasonable time, aborting");
+ break;
+ }
+ }
+ // We may have demuxed more than intended, so ensure that all frames are kept
+ // in the right order.
+ mSamples.PushFront(std::move(skipSamplesQueue));
+
+ if (frameTime.IsValid()) {
+ mNextKeyframeTime.emplace(frameTime);
+ WEBM_DEBUG(
+ "Next Keyframe %f (%u queued %.02fs)",
+ mNextKeyframeTime.value().ToSeconds(), uint32_t(mSamples.GetSize()),
+ (mSamples.Last()->mTimecode - mSamples.First()->mTimecode).ToSeconds());
+ } else {
+ WEBM_DEBUG("Couldn't determine next keyframe time (%u queued)",
+ uint32_t(mSamples.GetSize()));
+ }
+}
+
+void WebMTrackDemuxer::Reset() {
+ mSamples.Reset();
+ media::TimeIntervals buffered = GetBuffered();
+ mNeedKeyframe = true;
+ if (!buffered.IsEmpty()) {
+ WEBM_DEBUG("Seek to start point: %f", buffered.Start(0).ToSeconds());
+ mParent->SeekInternal(mType, buffered.Start(0));
+ SetNextKeyFrameTime();
+ } else {
+ mNextKeyframeTime.reset();
+ }
+}
+
+void WebMTrackDemuxer::UpdateSamples(
+ const nsTArray<RefPtr<MediaRawData>>& aSamples) {
+ for (const auto& sample : aSamples) {
+ if (sample->mCrypto.IsEncrypted()) {
+ UniquePtr<MediaRawDataWriter> writer(sample->CreateWriter());
+ writer->mCrypto.mIVSize = mInfo->mCrypto.mIVSize;
+ writer->mCrypto.mKeyId.AppendElements(mInfo->mCrypto.mKeyId);
+ }
+ }
+ if (mNextKeyframeTime.isNothing() ||
+ aSamples.LastElement()->mTime >= mNextKeyframeTime.value()) {
+ SetNextKeyFrameTime();
+ }
+}
+
+nsresult WebMTrackDemuxer::GetNextRandomAccessPoint(TimeUnit* aTime) {
+ if (mNextKeyframeTime.isNothing()) {
+ // There's no next key frame.
+ *aTime = TimeUnit::FromInfinity();
+ } else {
+ *aTime = mNextKeyframeTime.ref();
+ }
+ return NS_OK;
+}
+
+RefPtr<WebMTrackDemuxer::SkipAccessPointPromise>
+WebMTrackDemuxer::SkipToNextRandomAccessPoint(const TimeUnit& aTimeThreshold) {
+ uint32_t parsed = 0;
+ bool found = false;
+ RefPtr<MediaRawData> sample;
+ nsresult rv = NS_OK;
+
+ WEBM_DEBUG("TimeThreshold: %f", aTimeThreshold.ToSeconds());
+ while (!found && NS_SUCCEEDED((rv = NextSample(sample)))) {
+ parsed++;
+ if (sample->mKeyframe && sample->mTime >= aTimeThreshold) {
+ WEBM_DEBUG("next sample: %f (parsed: %d)", sample->mTime.ToSeconds(),
+ parsed);
+ found = true;
+ mSamples.Reset();
+ mSamples.PushFront(sample.forget());
+ }
+ }
+ if (NS_SUCCEEDED(rv)) {
+ SetNextKeyFrameTime();
+ }
+ if (found) {
+ return SkipAccessPointPromise::CreateAndResolve(parsed, __func__);
+ } else {
+ SkipFailureHolder failure(NS_ERROR_DOM_MEDIA_END_OF_STREAM, parsed);
+ return SkipAccessPointPromise::CreateAndReject(std::move(failure),
+ __func__);
+ }
+}
+
+media::TimeIntervals WebMTrackDemuxer::GetBuffered() {
+ return mParent->GetBuffered();
+}
+
+void WebMTrackDemuxer::BreakCycles() { mParent = nullptr; }
+
+int64_t WebMTrackDemuxer::GetEvictionOffset(const TimeUnit& aTime) {
+ int64_t offset;
+ int64_t nanos = aTime.ToNanoseconds();
+ if (nanos < 0 ||
+ !mParent->GetOffsetForTime(static_cast<uint64_t>(nanos), &offset)) {
+ return 0;
+ }
+
+ return offset;
+}
+} // namespace mozilla
+
+#undef WEBM_DEBUG
diff --git a/dom/media/webm/WebMDemuxer.h b/dom/media/webm/WebMDemuxer.h
new file mode 100644
index 0000000000..3d8ff67f9e
--- /dev/null
+++ b/dom/media/webm/WebMDemuxer.h
@@ -0,0 +1,288 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef WebMDemuxer_h_
+#define WebMDemuxer_h_
+
+#include "nsTArray.h"
+#include "MediaDataDemuxer.h"
+#include "MediaResource.h"
+#include "NesteggPacketHolder.h"
+
+#include <deque>
+#include <stdint.h>
+#include <utility>
+
+typedef struct nestegg nestegg;
+
+namespace mozilla {
+
+class WebMBufferedState;
+
+// Queue for holding MediaRawData samples
+class MediaRawDataQueue {
+ typedef std::deque<RefPtr<MediaRawData>> ContainerType;
+
+ public:
+ uint32_t GetSize() { return mQueue.size(); }
+
+ void Push(MediaRawData* aItem) { mQueue.push_back(aItem); }
+
+ void Push(already_AddRefed<MediaRawData>&& aItem) {
+ mQueue.push_back(std::move(aItem));
+ }
+
+ void PushFront(MediaRawData* aItem) { mQueue.push_front(aItem); }
+
+ void PushFront(already_AddRefed<MediaRawData>&& aItem) {
+ mQueue.push_front(std::move(aItem));
+ }
+
+ void PushFront(MediaRawDataQueue&& aOther) {
+ while (!aOther.mQueue.empty()) {
+ PushFront(aOther.Pop());
+ }
+ }
+
+ already_AddRefed<MediaRawData> PopFront() {
+ RefPtr<MediaRawData> result = std::move(mQueue.front());
+ mQueue.pop_front();
+ return result.forget();
+ }
+
+ already_AddRefed<MediaRawData> Pop() {
+ RefPtr<MediaRawData> result = std::move(mQueue.back());
+ mQueue.pop_back();
+ return result.forget();
+ }
+
+ void Reset() {
+ while (!mQueue.empty()) {
+ mQueue.pop_front();
+ }
+ }
+
+ MediaRawDataQueue& operator=(const MediaRawDataQueue& aOther) = delete;
+
+ const RefPtr<MediaRawData>& First() const { return mQueue.front(); }
+
+ const RefPtr<MediaRawData>& Last() const { return mQueue.back(); }
+
+ // Methods for range-based for loops.
+ ContainerType::iterator begin() { return mQueue.begin(); }
+
+ ContainerType::const_iterator begin() const { return mQueue.begin(); }
+
+ ContainerType::iterator end() { return mQueue.end(); }
+
+ ContainerType::const_iterator end() const { return mQueue.end(); }
+
+ private:
+ ContainerType mQueue;
+};
+
+class WebMTrackDemuxer;
+
+DDLoggedTypeDeclNameAndBase(WebMDemuxer, MediaDataDemuxer);
+DDLoggedTypeNameAndBase(WebMTrackDemuxer, MediaTrackDemuxer);
+
+class WebMDemuxer : public MediaDataDemuxer,
+ public DecoderDoctorLifeLogger<WebMDemuxer> {
+ public:
+ explicit WebMDemuxer(MediaResource* aResource);
+ // Indicate if the WebMDemuxer is to be used with MediaSource. In which
+ // case the demuxer will stop reads to the last known complete block.
+ WebMDemuxer(MediaResource* aResource, bool aIsMediaSource);
+
+ RefPtr<InitPromise> Init() override;
+
+ uint32_t GetNumberTracks(TrackInfo::TrackType aType) const override;
+
+ UniquePtr<TrackInfo> GetTrackInfo(TrackInfo::TrackType aType,
+ size_t aTrackNumber) const;
+
+ already_AddRefed<MediaTrackDemuxer> GetTrackDemuxer(
+ TrackInfo::TrackType aType, uint32_t aTrackNumber) override;
+
+ bool IsSeekable() const override;
+
+ bool IsSeekableOnlyInBufferedRanges() const override;
+
+ UniquePtr<EncryptionInfo> GetCrypto() override;
+
+ bool GetOffsetForTime(uint64_t aTime, int64_t* aOffset);
+
+ // Demux next WebM packet and append samples to MediaRawDataQueue
+ nsresult GetNextPacket(TrackInfo::TrackType aType,
+ MediaRawDataQueue* aSamples);
+
+ void Reset(TrackInfo::TrackType aType);
+
+ // Pushes a packet to the front of the audio packet queue.
+ void PushAudioPacket(NesteggPacketHolder* aItem);
+
+ // Pushes a packet to the front of the video packet queue.
+ void PushVideoPacket(NesteggPacketHolder* aItem);
+
+ // Public accessor for nestegg callbacks
+ bool IsMediaSource() const { return mIsMediaSource; }
+
+ int64_t LastWebMBlockOffset() const { return mLastWebMBlockOffset; }
+
+ struct NestEggContext {
+ NestEggContext(WebMDemuxer* aParent, MediaResource* aResource)
+ : mParent(aParent), mResource(aResource), mContext(nullptr) {}
+
+ ~NestEggContext();
+
+ int Init();
+
+ // Public accessor for nestegg callbacks
+
+ bool IsMediaSource() const { return mParent->IsMediaSource(); }
+ MediaResourceIndex* GetResource() { return &mResource; }
+
+ int64_t GetEndDataOffset() const {
+ return (!mParent->IsMediaSource() || mParent->LastWebMBlockOffset() < 0)
+ ? mResource.GetLength()
+ : mParent->LastWebMBlockOffset();
+ }
+
+ WebMDemuxer* mParent;
+ MediaResourceIndex mResource;
+ nestegg* mContext;
+ };
+
+ private:
+ friend class WebMTrackDemuxer;
+
+ ~WebMDemuxer();
+ void InitBufferedState();
+ nsresult ReadMetadata();
+ void NotifyDataArrived() override;
+ void NotifyDataRemoved() override;
+ void EnsureUpToDateIndex();
+
+ // A helper to catch bad intervals during `GetBuffered`.
+ // Verifies if the interval given by start and end is valid, returning true if
+ // it is, or false if not. Logs failure reason if the interval is invalid.
+ bool IsBufferedIntervalValid(uint64_t start, uint64_t end);
+
+ media::TimeIntervals GetBuffered();
+ nsresult SeekInternal(TrackInfo::TrackType aType,
+ const media::TimeUnit& aTarget);
+ CryptoTrack GetTrackCrypto(TrackInfo::TrackType aType, size_t aTrackNumber);
+
+ // Read a packet from the nestegg file. Returns nullptr if all packets for
+ // the particular track have been read. Pass TrackInfo::kVideoTrack or
+ // TrackInfo::kVideoTrack to indicate the type of the packet we want to read.
+ nsresult NextPacket(TrackInfo::TrackType aType,
+ RefPtr<NesteggPacketHolder>& aPacket);
+
+ // Internal method that demuxes the next packet from the stream. The caller
+ // is responsible for making sure it doesn't get lost.
+ nsresult DemuxPacket(TrackInfo::TrackType aType,
+ RefPtr<NesteggPacketHolder>& aPacket);
+
+ // libnestegg audio and video context for webm container.
+ // Access on reader's thread only.
+ NestEggContext mVideoContext;
+ NestEggContext mAudioContext;
+ MediaResourceIndex& Resource(TrackInfo::TrackType aType) {
+ return aType == TrackInfo::kVideoTrack ? mVideoContext.mResource
+ : mAudioContext.mResource;
+ }
+ nestegg* Context(TrackInfo::TrackType aType) const {
+ return aType == TrackInfo::kVideoTrack ? mVideoContext.mContext
+ : mAudioContext.mContext;
+ }
+
+ MediaInfo mInfo;
+ nsTArray<RefPtr<WebMTrackDemuxer>> mDemuxers;
+
+ // Parser state and computed offset-time mappings. Shared by multiple
+ // readers when decoder has been cloned. Main thread only.
+ RefPtr<WebMBufferedState> mBufferedState;
+ RefPtr<MediaByteBuffer> mInitData;
+
+ // Queue of video and audio packets that have been read but not decoded.
+ WebMPacketQueue mVideoPackets;
+ WebMPacketQueue mAudioPackets;
+
+ // Index of video and audio track to play
+ uint32_t mVideoTrack;
+ uint32_t mAudioTrack;
+
+ // Nanoseconds to discard after seeking.
+ uint64_t mSeekPreroll;
+
+ // Calculate the frame duration from the last decodeable frame using the
+ // previous frame's timestamp. In NS.
+ Maybe<int64_t> mLastAudioFrameTime;
+ Maybe<int64_t> mLastVideoFrameTime;
+
+ // Codec ID of audio track
+ int mAudioCodec;
+ // Codec ID of video track
+ int mVideoCodec;
+
+ // Booleans to indicate if we have audio and/or video data
+ bool mHasVideo;
+ bool mHasAudio;
+ bool mNeedReIndex;
+
+ // The last complete block parsed by the WebMBufferedState. -1 if not set.
+ // We cache those values rather than retrieving them for performance reasons
+ // as nestegg only performs 1-byte read at a time.
+ int64_t mLastWebMBlockOffset;
+ const bool mIsMediaSource;
+
+ EncryptionInfo mCrypto;
+};
+
+class WebMTrackDemuxer : public MediaTrackDemuxer,
+ public DecoderDoctorLifeLogger<WebMTrackDemuxer> {
+ public:
+ WebMTrackDemuxer(WebMDemuxer* aParent, TrackInfo::TrackType aType,
+ uint32_t aTrackNumber);
+
+ UniquePtr<TrackInfo> GetInfo() const override;
+
+ RefPtr<SeekPromise> Seek(const media::TimeUnit& aTime) override;
+
+ RefPtr<SamplesPromise> GetSamples(int32_t aNumSamples = 1) override;
+
+ void Reset() override;
+
+ nsresult GetNextRandomAccessPoint(media::TimeUnit* aTime) override;
+
+ RefPtr<SkipAccessPointPromise> SkipToNextRandomAccessPoint(
+ const media::TimeUnit& aTimeThreshold) override;
+
+ media::TimeIntervals GetBuffered() override;
+
+ int64_t GetEvictionOffset(const media::TimeUnit& aTime) override;
+
+ void BreakCycles() override;
+
+ private:
+ friend class WebMDemuxer;
+ ~WebMTrackDemuxer();
+ void UpdateSamples(const nsTArray<RefPtr<MediaRawData>>& aSamples);
+ void SetNextKeyFrameTime();
+ nsresult NextSample(RefPtr<MediaRawData>& aData);
+ RefPtr<WebMDemuxer> mParent;
+ TrackInfo::TrackType mType;
+ UniquePtr<TrackInfo> mInfo;
+ Maybe<media::TimeUnit> mNextKeyframeTime;
+ bool mNeedKeyframe;
+
+ // Queued samples extracted by the demuxer, but not yet returned.
+ MediaRawDataQueue mSamples;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webm/WebMWriter.cpp b/dom/media/webm/WebMWriter.cpp
new file mode 100644
index 0000000000..96c4169091
--- /dev/null
+++ b/dom/media/webm/WebMWriter.cpp
@@ -0,0 +1,112 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WebMWriter.h"
+#include "EbmlComposer.h"
+#include "mozilla/ProfilerLabels.h"
+#include "OpusTrackEncoder.h"
+
+namespace mozilla {
+
+WebMWriter::WebMWriter()
+ : ContainerWriter(), mEbmlComposer(new EbmlComposer()) {}
+
+WebMWriter::~WebMWriter() {
+ // Out-of-line dtor so mEbmlComposer UniquePtr can delete a complete type.
+}
+
+nsresult WebMWriter::WriteEncodedTrack(
+ const nsTArray<RefPtr<EncodedFrame>>& aData, uint32_t aFlags) {
+ AUTO_PROFILER_LABEL("WebMWriter::WriteEncodedTrack", OTHER);
+ for (uint32_t i = 0; i < aData.Length(); i++) {
+ nsresult rv = mEbmlComposer->WriteSimpleBlock(aData.ElementAt(i).get());
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+nsresult WebMWriter::GetContainerData(nsTArray<nsTArray<uint8_t>>* aOutputBufs,
+ uint32_t aFlags) {
+ AUTO_PROFILER_LABEL("WebMWriter::GetContainerData", OTHER);
+ mEbmlComposer->ExtractBuffer(aOutputBufs, aFlags);
+ if (aFlags & ContainerWriter::FLUSH_NEEDED) {
+ mIsWritingComplete = true;
+ }
+ return NS_OK;
+}
+
+nsresult WebMWriter::SetMetadata(
+ const nsTArray<RefPtr<TrackMetadataBase>>& aMetadata) {
+ AUTO_PROFILER_LABEL("WebMWriter::SetMetadata", OTHER);
+ MOZ_DIAGNOSTIC_ASSERT(!aMetadata.IsEmpty());
+
+ // Integrity checks
+ bool bad = false;
+ for (const RefPtr<TrackMetadataBase>& metadata : aMetadata) {
+ MOZ_ASSERT(metadata);
+
+ if (metadata->GetKind() == TrackMetadataBase::METADATA_VP8) {
+ VP8Metadata* meta = static_cast<VP8Metadata*>(metadata.get());
+ if (meta->mWidth == 0 || meta->mHeight == 0 || meta->mDisplayWidth == 0 ||
+ meta->mDisplayHeight == 0) {
+ bad = true;
+ }
+ }
+
+ if (metadata->GetKind() == TrackMetadataBase::METADATA_VORBIS) {
+ VorbisMetadata* meta = static_cast<VorbisMetadata*>(metadata.get());
+ if (meta->mSamplingFrequency == 0 || meta->mChannels == 0 ||
+ meta->mData.IsEmpty()) {
+ bad = true;
+ }
+ }
+
+ if (metadata->GetKind() == TrackMetadataBase::METADATA_OPUS) {
+ OpusMetadata* meta = static_cast<OpusMetadata*>(metadata.get());
+ if (meta->mSamplingFrequency == 0 || meta->mChannels == 0 ||
+ meta->mIdHeader.IsEmpty()) {
+ bad = true;
+ }
+ }
+ }
+ if (bad) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Storing
+ DebugOnly<bool> hasAudio = false;
+ DebugOnly<bool> hasVideo = false;
+ for (const RefPtr<TrackMetadataBase>& metadata : aMetadata) {
+ MOZ_ASSERT(metadata);
+
+ if (metadata->GetKind() == TrackMetadataBase::METADATA_VP8) {
+ MOZ_ASSERT(!hasVideo);
+ VP8Metadata* meta = static_cast<VP8Metadata*>(metadata.get());
+ mEbmlComposer->SetVideoConfig(meta->mWidth, meta->mHeight,
+ meta->mDisplayWidth, meta->mDisplayHeight);
+ hasVideo = true;
+ }
+
+ if (metadata->GetKind() == TrackMetadataBase::METADATA_VORBIS) {
+ MOZ_ASSERT(!hasAudio);
+ VorbisMetadata* meta = static_cast<VorbisMetadata*>(metadata.get());
+ mEbmlComposer->SetAudioConfig(meta->mSamplingFrequency, meta->mChannels);
+ mEbmlComposer->SetAudioCodecPrivateData(meta->mData);
+ hasAudio = true;
+ }
+
+ if (metadata->GetKind() == TrackMetadataBase::METADATA_OPUS) {
+ MOZ_ASSERT(!hasAudio);
+ OpusMetadata* meta = static_cast<OpusMetadata*>(metadata.get());
+ mEbmlComposer->SetAudioConfig(meta->mSamplingFrequency, meta->mChannels);
+ mEbmlComposer->SetAudioCodecPrivateData(meta->mIdHeader);
+ hasAudio = true;
+ }
+ }
+ mEbmlComposer->GenerateHeader();
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/dom/media/webm/WebMWriter.h b/dom/media/webm/WebMWriter.h
new file mode 100644
index 0000000000..71d2e18311
--- /dev/null
+++ b/dom/media/webm/WebMWriter.h
@@ -0,0 +1,69 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef WebMWriter_h_
+#define WebMWriter_h_
+
+#include "ContainerWriter.h"
+
+namespace mozilla {
+
+class EbmlComposer;
+
+// Vorbis meta data structure
+class VorbisMetadata : public TrackMetadataBase {
+ public:
+ nsTArray<uint8_t> mData;
+ int32_t mChannels;
+ float mSamplingFrequency;
+ MetadataKind GetKind() const override { return METADATA_VORBIS; }
+};
+
+// VP8 meta data structure
+class VP8Metadata : public TrackMetadataBase {
+ public:
+ int32_t mWidth;
+ int32_t mHeight;
+ int32_t mDisplayWidth;
+ int32_t mDisplayHeight;
+ MetadataKind GetKind() const override { return METADATA_VP8; }
+};
+
+/**
+ * WebM writer helper
+ * This class accepts encoder to set audio or video meta data or
+ * encoded data to ebml Composer, and get muxing data through GetContainerData.
+ * The ctor/dtor run in the MediaRecorder thread, others run in MediaEncoder
+ * thread.
+ */
+class WebMWriter : public ContainerWriter {
+ public:
+ // Run in MediaRecorder thread
+ WebMWriter();
+ virtual ~WebMWriter();
+
+ // WriteEncodedTrack inserts raw packets into WebM stream. Does not accept
+ // any flags: any specified will be ignored. Writing is finalized via
+ // flushing via GetContainerData().
+ nsresult WriteEncodedTrack(const nsTArray<RefPtr<EncodedFrame>>& aData,
+ uint32_t aFlags = 0) override;
+
+ // GetContainerData outputs multiplexing data.
+ // aFlags indicates the muxer should enter into finished stage and flush out
+ // queue data.
+ nsresult GetContainerData(nsTArray<nsTArray<uint8_t>>* aOutputBufs,
+ uint32_t aFlags = 0) override;
+
+ // Assign metadata into muxer
+ nsresult SetMetadata(
+ const nsTArray<RefPtr<TrackMetadataBase>>& aMetadata) override;
+
+ private:
+ UniquePtr<EbmlComposer> mEbmlComposer;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webm/moz.build b/dom/media/webm/moz.build
new file mode 100644
index 0000000000..f65fe5bc6b
--- /dev/null
+++ b/dom/media/webm/moz.build
@@ -0,0 +1,28 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS += [
+ "NesteggPacketHolder.h",
+ "WebMBufferedParser.h",
+ "WebMDecoder.h",
+ "WebMDemuxer.h",
+ "WebMWriter.h",
+]
+
+UNIFIED_SOURCES += [
+ "EbmlComposer.cpp",
+ "WebMBufferedParser.cpp",
+ "WebMDecoder.cpp",
+ "WebMDemuxer.cpp",
+ "WebMWriter.cpp",
+]
+
+CXXFLAGS += CONFIG["MOZ_LIBVPX_CFLAGS"]
+
+FINAL_LIBRARY = "xul"
+
+# Add libFuzzer configuration directives
+include("/tools/fuzzing/libfuzzer-config.mozbuild")
diff --git a/dom/media/webrtc/CubebDeviceEnumerator.cpp b/dom/media/webrtc/CubebDeviceEnumerator.cpp
new file mode 100644
index 0000000000..b5e705ea7d
--- /dev/null
+++ b/dom/media/webrtc/CubebDeviceEnumerator.cpp
@@ -0,0 +1,334 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CubebDeviceEnumerator.h"
+
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/media/MediaUtils.h"
+#include "nsThreadUtils.h"
+#ifdef XP_WIN
+# include "mozilla/mscom/EnsureMTA.h"
+#endif
+
+namespace mozilla {
+
+using namespace CubebUtils;
+using AudioDeviceSet = CubebDeviceEnumerator::AudioDeviceSet;
+
+/* static */
+static StaticRefPtr<CubebDeviceEnumerator> sInstance;
+static StaticMutex sInstanceMutex MOZ_UNANNOTATED;
+
+/* static */
+CubebDeviceEnumerator* CubebDeviceEnumerator::GetInstance() {
+ StaticMutexAutoLock lock(sInstanceMutex);
+ if (!sInstance) {
+ sInstance = new CubebDeviceEnumerator();
+ static bool clearOnShutdownSetup = []() -> bool {
+ auto setClearOnShutdown = []() -> void {
+ ClearOnShutdown(&sInstance, ShutdownPhase::XPCOMShutdownThreads);
+ };
+ if (NS_IsMainThread()) {
+ setClearOnShutdown();
+ } else {
+ SchedulerGroup::Dispatch(
+ TaskCategory::Other,
+ NS_NewRunnableFunction("CubebDeviceEnumerator::::GetInstance()",
+ std::move(setClearOnShutdown)));
+ }
+ return true;
+ }();
+ Unused << clearOnShutdownSetup;
+ }
+ return sInstance.get();
+}
+
+CubebDeviceEnumerator::CubebDeviceEnumerator()
+ : mMutex("CubebDeviceListMutex"),
+ mManualInputInvalidation(false),
+ mManualOutputInvalidation(false) {
+#ifdef XP_WIN
+ // Ensure the MTA thread exists and gets instantiated before the
+ // CubebDeviceEnumerator so that this instance will always gets destructed
+ // before the MTA thread gets shutdown.
+ mozilla::mscom::EnsureMTA([&]() -> void {
+#endif
+ int rv = cubeb_register_device_collection_changed(
+ GetCubebContext(), CUBEB_DEVICE_TYPE_OUTPUT,
+ &OutputAudioDeviceListChanged_s, this);
+ if (rv != CUBEB_OK) {
+ NS_WARNING(
+ "Could not register the audio output"
+ " device collection changed callback.");
+ mManualOutputInvalidation = true;
+ }
+ rv = cubeb_register_device_collection_changed(
+ GetCubebContext(), CUBEB_DEVICE_TYPE_INPUT,
+ &InputAudioDeviceListChanged_s, this);
+ if (rv != CUBEB_OK) {
+ NS_WARNING(
+ "Could not register the audio input"
+ " device collection changed callback.");
+ mManualInputInvalidation = true;
+ }
+#ifdef XP_WIN
+ });
+#endif
+}
+
+/* static */
+void CubebDeviceEnumerator::Shutdown() {
+ StaticMutexAutoLock lock(sInstanceMutex);
+ if (sInstance) {
+ sInstance = nullptr;
+ }
+}
+
+CubebDeviceEnumerator::~CubebDeviceEnumerator() {
+#ifdef XP_WIN
+ mozilla::mscom::EnsureMTA([&]() -> void {
+#endif
+ int rv = cubeb_register_device_collection_changed(
+ GetCubebContext(), CUBEB_DEVICE_TYPE_OUTPUT, nullptr, this);
+ if (rv != CUBEB_OK) {
+ NS_WARNING(
+ "Could not unregister the audio output"
+ " device collection changed callback.");
+ }
+ rv = cubeb_register_device_collection_changed(
+ GetCubebContext(), CUBEB_DEVICE_TYPE_INPUT, nullptr, this);
+ if (rv != CUBEB_OK) {
+ NS_WARNING(
+ "Could not unregister the audio input"
+ " device collection changed callback.");
+ }
+#ifdef XP_WIN
+ });
+#endif
+}
+
+RefPtr<const AudioDeviceSet>
+CubebDeviceEnumerator::EnumerateAudioInputDevices() {
+ return EnumerateAudioDevices(Side::INPUT);
+}
+
+RefPtr<const AudioDeviceSet>
+CubebDeviceEnumerator::EnumerateAudioOutputDevices() {
+ return EnumerateAudioDevices(Side::OUTPUT);
+}
+
+#ifndef ANDROID
+static uint16_t ConvertCubebType(cubeb_device_type aType) {
+ uint16_t map[] = {
+ nsIAudioDeviceInfo::TYPE_UNKNOWN, // CUBEB_DEVICE_TYPE_UNKNOWN
+ nsIAudioDeviceInfo::TYPE_INPUT, // CUBEB_DEVICE_TYPE_INPUT,
+ nsIAudioDeviceInfo::TYPE_OUTPUT // CUBEB_DEVICE_TYPE_OUTPUT
+ };
+ return map[aType];
+}
+
+static uint16_t ConvertCubebState(cubeb_device_state aState) {
+ uint16_t map[] = {
+ nsIAudioDeviceInfo::STATE_DISABLED, // CUBEB_DEVICE_STATE_DISABLED
+ nsIAudioDeviceInfo::STATE_UNPLUGGED, // CUBEB_DEVICE_STATE_UNPLUGGED
+ nsIAudioDeviceInfo::STATE_ENABLED // CUBEB_DEVICE_STATE_ENABLED
+ };
+ return map[aState];
+}
+
+static uint16_t ConvertCubebPreferred(cubeb_device_pref aPreferred) {
+ if (aPreferred == CUBEB_DEVICE_PREF_NONE) {
+ return nsIAudioDeviceInfo::PREF_NONE;
+ }
+ if (aPreferred == CUBEB_DEVICE_PREF_ALL) {
+ return nsIAudioDeviceInfo::PREF_ALL;
+ }
+
+ uint16_t preferred = 0;
+ if (aPreferred & CUBEB_DEVICE_PREF_MULTIMEDIA) {
+ preferred |= nsIAudioDeviceInfo::PREF_MULTIMEDIA;
+ }
+ if (aPreferred & CUBEB_DEVICE_PREF_VOICE) {
+ preferred |= nsIAudioDeviceInfo::PREF_VOICE;
+ }
+ if (aPreferred & CUBEB_DEVICE_PREF_NOTIFICATION) {
+ preferred |= nsIAudioDeviceInfo::PREF_NOTIFICATION;
+ }
+ return preferred;
+}
+
+static uint16_t ConvertCubebFormat(cubeb_device_fmt aFormat) {
+ uint16_t format = 0;
+ if (aFormat & CUBEB_DEVICE_FMT_S16LE) {
+ format |= nsIAudioDeviceInfo::FMT_S16LE;
+ }
+ if (aFormat & CUBEB_DEVICE_FMT_S16BE) {
+ format |= nsIAudioDeviceInfo::FMT_S16BE;
+ }
+ if (aFormat & CUBEB_DEVICE_FMT_F32LE) {
+ format |= nsIAudioDeviceInfo::FMT_F32LE;
+ }
+ if (aFormat & CUBEB_DEVICE_FMT_F32BE) {
+ format |= nsIAudioDeviceInfo::FMT_F32BE;
+ }
+ return format;
+}
+
+static RefPtr<AudioDeviceSet> GetDeviceCollection(Side aSide) {
+ RefPtr set = new AudioDeviceSet();
+ cubeb* context = GetCubebContext();
+ if (context) {
+ cubeb_device_collection collection = {nullptr, 0};
+# ifdef XP_WIN
+ mozilla::mscom::EnsureMTA([&]() -> void {
+# endif
+ if (cubeb_enumerate_devices(context,
+ aSide == Input ? CUBEB_DEVICE_TYPE_INPUT
+ : CUBEB_DEVICE_TYPE_OUTPUT,
+ &collection) == CUBEB_OK) {
+ for (unsigned int i = 0; i < collection.count; ++i) {
+ auto device = collection.device[i];
+ if (device.max_channels == 0) {
+ continue;
+ }
+ RefPtr<AudioDeviceInfo> info = new AudioDeviceInfo(
+ device.devid, NS_ConvertUTF8toUTF16(device.friendly_name),
+ NS_ConvertUTF8toUTF16(device.group_id),
+ NS_ConvertUTF8toUTF16(device.vendor_name),
+ ConvertCubebType(device.type), ConvertCubebState(device.state),
+ ConvertCubebPreferred(device.preferred),
+ ConvertCubebFormat(device.format),
+ ConvertCubebFormat(device.default_format), device.max_channels,
+ device.default_rate, device.max_rate, device.min_rate,
+ device.latency_hi, device.latency_lo);
+ set->AppendElement(std::move(info));
+ }
+ }
+ cubeb_device_collection_destroy(context, &collection);
+# ifdef XP_WIN
+ });
+# endif
+ }
+ return set;
+}
+#endif // non ANDROID
+
+RefPtr<const AudioDeviceSet> CubebDeviceEnumerator::EnumerateAudioDevices(
+ CubebDeviceEnumerator::Side aSide) {
+ MOZ_ASSERT(aSide == Side::INPUT || aSide == Side::OUTPUT);
+
+ RefPtr<const AudioDeviceSet>* devicesCache;
+ bool manualInvalidation = true;
+
+ if (aSide == Side::INPUT) {
+ devicesCache = &mInputDevices;
+ manualInvalidation = mManualInputInvalidation;
+ } else {
+ MOZ_ASSERT(aSide == Side::OUTPUT);
+ devicesCache = &mOutputDevices;
+ manualInvalidation = mManualOutputInvalidation;
+ }
+
+ cubeb* context = GetCubebContext();
+ if (!context) {
+ return new AudioDeviceSet();
+ }
+ if (!manualInvalidation) {
+ MutexAutoLock lock(mMutex);
+ if (*devicesCache) {
+ return *devicesCache;
+ }
+ }
+
+#ifdef ANDROID
+ cubeb_device_type type = CUBEB_DEVICE_TYPE_UNKNOWN;
+ uint32_t channels = 0;
+ nsAutoString name;
+ if (aSide == Side::INPUT) {
+ type = CUBEB_DEVICE_TYPE_INPUT;
+ channels = 1;
+ name = u"Default audio input device"_ns;
+ } else {
+ MOZ_ASSERT(aSide == Side::OUTPUT);
+ type = CUBEB_DEVICE_TYPE_OUTPUT;
+ channels = 2;
+ name = u"Default audio output device"_ns;
+ }
+ RefPtr devices = new AudioDeviceSet();
+ // Bug 1473346: enumerating devices is not supported on Android in cubeb,
+ // simply state that there is a single sink, that it is the default, and has
+ // a single channel. All the other values are made up and are not to be used.
+ // Bug 1660391: we can't use fluent here yet to get localized strings, so
+ // those are hard-coded en_US strings for now.
+ RefPtr<AudioDeviceInfo> info = new AudioDeviceInfo(
+ nullptr, name, u""_ns, u""_ns, type, CUBEB_DEVICE_STATE_ENABLED,
+ CUBEB_DEVICE_PREF_ALL, CUBEB_DEVICE_FMT_ALL, CUBEB_DEVICE_FMT_S16NE,
+ channels, 44100, 44100, 44100, 441, 128);
+ devices->AppendElement(std::move(info));
+#else
+ RefPtr devices = GetDeviceCollection(
+ (aSide == Side::INPUT) ? CubebUtils::Input : CubebUtils::Output);
+#endif
+ {
+ MutexAutoLock lock(mMutex);
+ *devicesCache = devices;
+ }
+ return devices;
+}
+
+already_AddRefed<AudioDeviceInfo> CubebDeviceEnumerator::DeviceInfoFromName(
+ const nsString& aName, Side aSide) {
+ RefPtr devices = EnumerateAudioDevices(aSide);
+ for (const RefPtr<AudioDeviceInfo>& device : *devices) {
+ if (device->Name().Equals(aName)) {
+ RefPtr<AudioDeviceInfo> other = device;
+ return other.forget();
+ }
+ }
+
+ return nullptr;
+}
+
+RefPtr<AudioDeviceInfo> CubebDeviceEnumerator::DefaultDevice(Side aSide) {
+ RefPtr devices = EnumerateAudioDevices(aSide);
+ for (const RefPtr<AudioDeviceInfo>& device : *devices) {
+ if (device->Preferred()) {
+ RefPtr<AudioDeviceInfo> other = device;
+ return other.forget();
+ }
+ }
+
+ return nullptr;
+}
+
+void CubebDeviceEnumerator::InputAudioDeviceListChanged_s(cubeb* aContext,
+ void* aUser) {
+ CubebDeviceEnumerator* self = reinterpret_cast<CubebDeviceEnumerator*>(aUser);
+ self->AudioDeviceListChanged(CubebDeviceEnumerator::Side::INPUT);
+}
+
+void CubebDeviceEnumerator::OutputAudioDeviceListChanged_s(cubeb* aContext,
+ void* aUser) {
+ CubebDeviceEnumerator* self = reinterpret_cast<CubebDeviceEnumerator*>(aUser);
+ self->AudioDeviceListChanged(CubebDeviceEnumerator::Side::OUTPUT);
+}
+
+void CubebDeviceEnumerator::AudioDeviceListChanged(Side aSide) {
+ MutexAutoLock lock(mMutex);
+ if (aSide == Side::INPUT) {
+ mInputDevices = nullptr;
+ mOnInputDeviceListChange.Notify();
+ } else {
+ MOZ_ASSERT(aSide == Side::OUTPUT);
+ mOutputDevices = nullptr;
+ mOnOutputDeviceListChange.Notify();
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/CubebDeviceEnumerator.h b/dom/media/webrtc/CubebDeviceEnumerator.h
new file mode 100644
index 0000000000..6b6499f728
--- /dev/null
+++ b/dom/media/webrtc/CubebDeviceEnumerator.h
@@ -0,0 +1,87 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef CUBEBDEVICEENUMERATOR_H_
+#define CUBEBDEVICEENUMERATOR_H_
+
+#include "AudioDeviceInfo.h"
+#include "cubeb/cubeb.h"
+#include "MediaEventSource.h"
+#include "mozilla/Mutex.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+
+namespace media {
+template <typename T>
+class Refcountable;
+}
+
+// This class implements a cache for accessing the audio device list.
+// It can be accessed on any thread.
+class CubebDeviceEnumerator final {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CubebDeviceEnumerator)
+
+ static CubebDeviceEnumerator* GetInstance();
+ static void Shutdown();
+ using AudioDeviceSet = media::Refcountable<nsTArray<RefPtr<AudioDeviceInfo>>>;
+ // This method returns a list of all the input audio devices
+ // (sources) available on this machine.
+ // This method is safe to call from all threads.
+ RefPtr<const AudioDeviceSet> EnumerateAudioInputDevices();
+ // Similar for the audio audio devices (sinks). Also thread safe.
+ RefPtr<const AudioDeviceSet> EnumerateAudioOutputDevices();
+ // From a device name, return the info for this device, if it's a valid name,
+ // or nullptr otherwise.
+ // This method is safe to call from any thread.
+ enum class Side {
+ INPUT,
+ OUTPUT,
+ };
+ already_AddRefed<AudioDeviceInfo> DeviceInfoFromName(const nsString& aName,
+ Side aSide);
+ // Event source to listen for changes to the audio input device list on.
+ MediaEventSource<void>& OnAudioInputDeviceListChange() {
+ return mOnInputDeviceListChange;
+ }
+
+ // Event source to listen for changes to the audio output device list on.
+ MediaEventSource<void>& OnAudioOutputDeviceListChange() {
+ return mOnOutputDeviceListChange;
+ }
+
+ // Return the default device for a particular side.
+ RefPtr<AudioDeviceInfo> DefaultDevice(Side aSide);
+
+ private:
+ CubebDeviceEnumerator();
+ ~CubebDeviceEnumerator();
+ // Static functions called by cubeb when the audio device list changes
+ // (i.e. when a new device is made available, or non-available). This
+ // simply calls `AudioDeviceListChanged` below.
+ static void InputAudioDeviceListChanged_s(cubeb* aContext, void* aUser);
+ static void OutputAudioDeviceListChanged_s(cubeb* aContext, void* aUser);
+ // Invalidates the cached audio input device list, can be called on any
+ // thread.
+ void AudioDeviceListChanged(Side aSide);
+ RefPtr<const AudioDeviceSet> EnumerateAudioDevices(Side aSide);
+ // Synchronize access to mInputDevices and mOutputDevices;
+ Mutex mMutex MOZ_UNANNOTATED;
+ RefPtr<const AudioDeviceSet> mInputDevices;
+ RefPtr<const AudioDeviceSet> mOutputDevices;
+ // If mManual*Invalidation is true, then it is necessary to query the device
+ // list each time instead of relying on automatic invalidation of the cache by
+ // cubeb itself. Set in the constructor and then can be access on any thread.
+ bool mManualInputInvalidation;
+ bool mManualOutputInvalidation;
+ MediaEventProducer<void> mOnInputDeviceListChange;
+ MediaEventProducer<void> mOnOutputDeviceListChange;
+};
+
+typedef CubebDeviceEnumerator Enumerator;
+typedef CubebDeviceEnumerator::Side EnumeratorSide;
+} // namespace mozilla
+
+#endif // CUBEBDEVICEENUMERATOR_H_
diff --git a/dom/media/webrtc/MediaEngine.h b/dom/media/webrtc/MediaEngine.h
new file mode 100644
index 0000000000..d3e1ece452
--- /dev/null
+++ b/dom/media/webrtc/MediaEngine.h
@@ -0,0 +1,66 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MEDIAENGINE_H_
+#define MEDIAENGINE_H_
+
+#include "DOMMediaStream.h"
+#include "MediaEventSource.h"
+#include "MediaTrackGraph.h"
+#include "MediaTrackConstraints.h"
+#include "mozilla/dom/MediaStreamTrackBinding.h"
+#include "mozilla/dom/VideoStreamTrack.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/ThreadSafeWeakPtr.h"
+
+namespace mozilla {
+
+namespace dom {
+class Blob;
+} // namespace dom
+
+class AllocationHandle;
+class MediaDevice;
+class MediaEngineSource;
+
+enum MediaSinkEnum {
+ Speaker,
+ Other,
+};
+
+enum { kVideoTrack = 1, kAudioTrack = 2, kTrackCount };
+
+class MediaEngine {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaEngine)
+ NS_DECL_OWNINGEVENTTARGET
+
+ void AssertIsOnOwningThread() const { NS_ASSERT_OWNINGTHREAD(MediaEngine); }
+
+ /**
+ * Populate an array of sources of the requested type in the nsTArray.
+ * Also include devices that are currently unavailable.
+ */
+ virtual void EnumerateDevices(dom::MediaSourceEnum, MediaSinkEnum,
+ nsTArray<RefPtr<MediaDevice>>*) = 0;
+
+ virtual void Shutdown() = 0;
+
+ virtual RefPtr<MediaEngineSource> CreateSource(
+ const MediaDevice* aDevice) = 0;
+
+ virtual MediaEventSource<void>& DeviceListChangeEvent() = 0;
+ /**
+ * Return true if devices returned from EnumerateDevices are emulated media
+ * devices.
+ */
+ virtual bool IsFake() const = 0;
+
+ protected:
+ virtual ~MediaEngine() = default;
+};
+
+} // namespace mozilla
+
+#endif /* MEDIAENGINE_H_ */
diff --git a/dom/media/webrtc/MediaEngineFake.cpp b/dom/media/webrtc/MediaEngineFake.cpp
new file mode 100644
index 0000000000..bba6c18694
--- /dev/null
+++ b/dom/media/webrtc/MediaEngineFake.cpp
@@ -0,0 +1,653 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaEngineFake.h"
+
+#include "AudioSegment.h"
+#include "DOMMediaStream.h"
+#include "ImageContainer.h"
+#include "ImageTypes.h"
+#include "MediaEnginePrefs.h"
+#include "MediaEngineSource.h"
+#include "MediaTrackGraph.h"
+#include "MediaTrackListener.h"
+#include "MediaTrackConstraints.h"
+#include "mozilla/dom/File.h"
+#include "mozilla/MediaManager.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/SyncRunnable.h"
+#include "mozilla/UniquePtr.h"
+#include "nsComponentManagerUtils.h"
+#include "nsCOMPtr.h"
+#include "nsContentUtils.h"
+#include "nsITimer.h"
+#include "SineWaveGenerator.h"
+#include "Tracing.h"
+#include "VideoSegment.h"
+#include "VideoUtils.h"
+
+#ifdef MOZ_WIDGET_ANDROID
+# include "nsISupportsUtils.h"
+#endif
+
+#ifdef MOZ_WEBRTC
+# include "YuvStamper.h"
+#endif
+
+#define VIDEO_WIDTH_MIN 160
+#define VIDEO_WIDTH_MAX 4096
+#define VIDEO_HEIGHT_MIN 90
+#define VIDEO_HEIGHT_MAX 2160
+#define DEFAULT_AUDIO_TIMER_MS 10
+namespace mozilla {
+
+using namespace mozilla::gfx;
+using dom::MediaSourceEnum;
+using dom::MediaTrackConstraints;
+using dom::MediaTrackSettings;
+using dom::VideoFacingModeEnum;
+
+static nsString FakeVideoName() {
+ // For the purpose of testing we allow to change the name of the fake device
+ // by pref.
+ nsAutoString cameraNameFromPref;
+ nsresult rv;
+ auto getPref = [&]() {
+ rv = Preferences::GetString("media.getusermedia.fake-camera-name",
+ cameraNameFromPref);
+ };
+ if (NS_IsMainThread()) {
+ getPref();
+ } else {
+ // Here it is preferred a "hard" block, instead of "soft" block provided
+ // by sync dispatch, which allows the waiting thread to spin its event
+ // loop. The latter would allow multiple enumeration requests being
+ // processed out-of-order.
+ RefPtr runnable = NS_NewRunnableFunction(__func__, getPref);
+ SyncRunnable::DispatchToThread(GetMainThreadSerialEventTarget(), runnable);
+ }
+
+ if (NS_SUCCEEDED(rv)) {
+ return std::move(cameraNameFromPref);
+ }
+ return u"Default Video Device"_ns;
+}
+
+/**
+ * Fake video source.
+ */
+class MediaEngineFakeVideoSource : public MediaEngineSource {
+ public:
+ MediaEngineFakeVideoSource();
+
+ static nsString GetGroupId();
+
+ nsresult Allocate(const dom::MediaTrackConstraints& aConstraints,
+ const MediaEnginePrefs& aPrefs, uint64_t aWindowID,
+ const char** aOutBadConstraint) override;
+ void SetTrack(const RefPtr<MediaTrack>& aTrack,
+ const PrincipalHandle& aPrincipal) override;
+ nsresult Start() override;
+ nsresult Reconfigure(const dom::MediaTrackConstraints& aConstraints,
+ const MediaEnginePrefs& aPrefs,
+ const char** aOutBadConstraint) override;
+ nsresult Stop() override;
+ nsresult Deallocate() override;
+
+ uint32_t GetBestFitnessDistance(
+ const nsTArray<const NormalizedConstraintSet*>& aConstraintSets)
+ const override;
+ void GetSettings(dom::MediaTrackSettings& aOutSettings) const override;
+
+ bool IsFake() const override { return true; }
+
+ protected:
+ ~MediaEngineFakeVideoSource() = default;
+
+ /**
+ * Called by mTimer when it's time to generate a new frame.
+ */
+ void GenerateFrame();
+
+ nsCOMPtr<nsITimer> mTimer;
+
+ RefPtr<layers::ImageContainer> mImageContainer;
+
+ // Current state of this source.
+ MediaEngineSourceState mState = kReleased;
+ RefPtr<layers::Image> mImage;
+ RefPtr<SourceMediaTrack> mTrack;
+ PrincipalHandle mPrincipalHandle = PRINCIPAL_HANDLE_NONE;
+
+ MediaEnginePrefs mOpts;
+ int mCb = 16;
+ int mCr = 16;
+
+ // Main thread only.
+ const RefPtr<media::Refcountable<dom::MediaTrackSettings>> mSettings;
+};
+
+MediaEngineFakeVideoSource::MediaEngineFakeVideoSource()
+ : mTimer(nullptr),
+ mSettings(MakeAndAddRef<media::Refcountable<MediaTrackSettings>>()) {
+ mSettings->mWidth.Construct(
+ int32_t(MediaEnginePrefs::DEFAULT_43_VIDEO_WIDTH));
+ mSettings->mHeight.Construct(
+ int32_t(MediaEnginePrefs::DEFAULT_43_VIDEO_HEIGHT));
+ mSettings->mFrameRate.Construct(double(MediaEnginePrefs::DEFAULT_VIDEO_FPS));
+ mSettings->mFacingMode.Construct(
+ NS_ConvertASCIItoUTF16(dom::VideoFacingModeEnumValues::strings
+ [uint8_t(VideoFacingModeEnum::Environment)]
+ .value));
+}
+
+nsString MediaEngineFakeVideoSource::GetGroupId() {
+ return u"Fake Video Group"_ns;
+}
+
+uint32_t MediaEngineFakeVideoSource::GetBestFitnessDistance(
+ const nsTArray<const NormalizedConstraintSet*>& aConstraintSets) const {
+ AssertIsOnOwningThread();
+
+ uint64_t distance = 0;
+
+#ifdef MOZ_WEBRTC
+ // distance is read from first entry only
+ if (aConstraintSets.Length() >= 1) {
+ const auto* cs = aConstraintSets.ElementAt(0);
+ Maybe<nsString> facingMode = Nothing();
+ distance +=
+ MediaConstraintsHelper::FitnessDistance(facingMode, cs->mFacingMode);
+
+ if (cs->mWidth.mMax < VIDEO_WIDTH_MIN ||
+ cs->mWidth.mMin > VIDEO_WIDTH_MAX) {
+ distance += UINT32_MAX;
+ }
+
+ if (cs->mHeight.mMax < VIDEO_HEIGHT_MIN ||
+ cs->mHeight.mMin > VIDEO_HEIGHT_MAX) {
+ distance += UINT32_MAX;
+ }
+ }
+#endif
+
+ return uint32_t(std::min(distance, uint64_t(UINT32_MAX)));
+}
+
+void MediaEngineFakeVideoSource::GetSettings(
+ MediaTrackSettings& aOutSettings) const {
+ MOZ_ASSERT(NS_IsMainThread());
+ aOutSettings = *mSettings;
+}
+
+nsresult MediaEngineFakeVideoSource::Allocate(
+ const MediaTrackConstraints& aConstraints, const MediaEnginePrefs& aPrefs,
+ uint64_t aWindowID, const char** aOutBadConstraint) {
+ AssertIsOnOwningThread();
+
+ MOZ_ASSERT(mState == kReleased);
+
+ FlattenedConstraints c(aConstraints);
+
+ // emulator debug is very, very slow; reduce load on it with smaller/slower
+ // fake video
+ mOpts = aPrefs;
+ mOpts.mWidth =
+ c.mWidth.Get(aPrefs.mWidth ? aPrefs.mWidth :
+#ifdef DEBUG
+ MediaEnginePrefs::DEFAULT_43_VIDEO_WIDTH / 2
+#else
+ MediaEnginePrefs::DEFAULT_43_VIDEO_WIDTH
+#endif
+ );
+ mOpts.mHeight =
+ c.mHeight.Get(aPrefs.mHeight ? aPrefs.mHeight :
+#ifdef DEBUG
+ MediaEnginePrefs::DEFAULT_43_VIDEO_HEIGHT / 2
+#else
+ MediaEnginePrefs::DEFAULT_43_VIDEO_HEIGHT
+#endif
+ );
+ mOpts.mWidth =
+ std::max(VIDEO_WIDTH_MIN, std::min(mOpts.mWidth, VIDEO_WIDTH_MAX)) & ~1;
+ mOpts.mHeight =
+ std::max(VIDEO_HEIGHT_MIN, std::min(mOpts.mHeight, VIDEO_HEIGHT_MAX)) &
+ ~1;
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ __func__, [settings = mSettings, frameRate = mOpts.mFPS,
+ width = mOpts.mWidth, height = mOpts.mHeight]() {
+ settings->mFrameRate.Value() = frameRate;
+ settings->mWidth.Value() = width;
+ settings->mHeight.Value() = height;
+ }));
+
+ mState = kAllocated;
+ return NS_OK;
+}
+
+nsresult MediaEngineFakeVideoSource::Deallocate() {
+ AssertIsOnOwningThread();
+
+ MOZ_ASSERT(!mImage);
+ MOZ_ASSERT(mState == kStopped || mState == kAllocated);
+
+ if (mTrack) {
+ mTrack->End();
+ mTrack = nullptr;
+ mPrincipalHandle = PRINCIPAL_HANDLE_NONE;
+ }
+ mState = kReleased;
+ mImageContainer = nullptr;
+
+ return NS_OK;
+}
+
+static bool AllocateSolidColorFrame(layers::PlanarYCbCrData& aData, int aWidth,
+ int aHeight, int aY, int aCb, int aCr) {
+ MOZ_ASSERT(!(aWidth & 1));
+ MOZ_ASSERT(!(aHeight & 1));
+ // Allocate a single frame with a solid color
+ int yLen = aWidth * aHeight;
+ int cbLen = yLen >> 2;
+ int crLen = cbLen;
+ uint8_t* frame = (uint8_t*)malloc(yLen + cbLen + crLen);
+ if (!frame) {
+ return false;
+ }
+ memset(frame, aY, yLen);
+ memset(frame + yLen, aCb, cbLen);
+ memset(frame + yLen + cbLen, aCr, crLen);
+
+ aData.mYChannel = frame;
+ aData.mYStride = aWidth;
+ aData.mCbCrStride = aWidth >> 1;
+ aData.mCbChannel = frame + yLen;
+ aData.mCrChannel = aData.mCbChannel + cbLen;
+ aData.mPictureRect = IntRect(0, 0, aWidth, aHeight);
+ aData.mStereoMode = StereoMode::MONO;
+ aData.mYUVColorSpace = gfx::YUVColorSpace::BT601;
+ aData.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT;
+ return true;
+}
+
+static void ReleaseFrame(layers::PlanarYCbCrData& aData) {
+ free(aData.mYChannel);
+}
+
+void MediaEngineFakeVideoSource::SetTrack(const RefPtr<MediaTrack>& aTrack,
+ const PrincipalHandle& aPrincipal) {
+ AssertIsOnOwningThread();
+
+ MOZ_ASSERT(mState == kAllocated);
+ MOZ_ASSERT(!mTrack);
+ MOZ_ASSERT(aTrack->AsSourceTrack());
+
+ mTrack = aTrack->AsSourceTrack();
+ mPrincipalHandle = aPrincipal;
+}
+
+nsresult MediaEngineFakeVideoSource::Start() {
+ AssertIsOnOwningThread();
+
+ MOZ_ASSERT(mState == kAllocated || mState == kStopped);
+ MOZ_ASSERT(mTrack, "SetTrack() must happen before Start()");
+
+ mTimer = NS_NewTimer(GetCurrentSerialEventTarget());
+ if (!mTimer) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!mImageContainer) {
+ mImageContainer = MakeAndAddRef<layers::ImageContainer>(
+ layers::ImageContainer::ASYNCHRONOUS);
+ }
+
+ // Start timer for subsequent frames
+ uint32_t interval;
+#if defined(MOZ_WIDGET_ANDROID) && defined(DEBUG)
+ // emulator debug is very, very slow and has problems dealing with realtime
+ // audio inputs
+ interval = 10 * (1000 / mOpts.mFPS);
+#else
+ interval = 1000 / mOpts.mFPS;
+#endif
+ mTimer->InitWithNamedFuncCallback(
+ [](nsITimer* aTimer, void* aClosure) {
+ RefPtr<MediaEngineFakeVideoSource> source =
+ static_cast<MediaEngineFakeVideoSource*>(aClosure);
+ source->GenerateFrame();
+ },
+ this, interval, nsITimer::TYPE_REPEATING_SLACK,
+ "MediaEngineFakeVideoSource::GenerateFrame");
+
+ mState = kStarted;
+ return NS_OK;
+}
+
+nsresult MediaEngineFakeVideoSource::Stop() {
+ AssertIsOnOwningThread();
+
+ if (mState == kStopped || mState == kAllocated) {
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(mState == kStarted);
+ MOZ_ASSERT(mTimer);
+ MOZ_ASSERT(mTrack);
+
+ mTimer->Cancel();
+ mTimer = nullptr;
+
+ mState = kStopped;
+
+ return NS_OK;
+}
+
+nsresult MediaEngineFakeVideoSource::Reconfigure(
+ const MediaTrackConstraints& aConstraints, const MediaEnginePrefs& aPrefs,
+ const char** aOutBadConstraint) {
+ return NS_OK;
+}
+
+void MediaEngineFakeVideoSource::GenerateFrame() {
+ AssertIsOnOwningThread();
+
+ // Update the target color
+ if (mCr <= 16) {
+ if (mCb < 240) {
+ mCb++;
+ } else {
+ mCr++;
+ }
+ } else if (mCb >= 240) {
+ if (mCr < 240) {
+ mCr++;
+ } else {
+ mCb--;
+ }
+ } else if (mCr >= 240) {
+ if (mCb > 16) {
+ mCb--;
+ } else {
+ mCr--;
+ }
+ } else {
+ mCr--;
+ }
+
+ // Allocate a single solid color image
+ RefPtr<layers::PlanarYCbCrImage> ycbcr_image =
+ mImageContainer->CreatePlanarYCbCrImage();
+ layers::PlanarYCbCrData data;
+ if (NS_WARN_IF(!AllocateSolidColorFrame(data, mOpts.mWidth, mOpts.mHeight,
+ 0x80, mCb, mCr))) {
+ return;
+ }
+
+#ifdef MOZ_WEBRTC
+ uint64_t timestamp = PR_Now();
+ YuvStamper::Encode(mOpts.mWidth, mOpts.mHeight, mOpts.mWidth, data.mYChannel,
+ reinterpret_cast<unsigned char*>(&timestamp),
+ sizeof(timestamp), 0, 0);
+#endif
+
+ bool setData = ycbcr_image->CopyData(data);
+ MOZ_ASSERT(setData);
+
+ // SetData copies data, so we can free the frame
+ ReleaseFrame(data);
+
+ if (!setData) {
+ return;
+ }
+
+ VideoSegment segment;
+ segment.AppendFrame(ycbcr_image.forget(),
+ gfx::IntSize(mOpts.mWidth, mOpts.mHeight),
+ mPrincipalHandle);
+ mTrack->AppendData(&segment);
+}
+
+// This class is created on the media thread, as part of Start(), then entirely
+// self-sustained until destruction, just forwarding calls to Pull().
+class AudioSourcePullListener : public MediaTrackListener {
+ public:
+ AudioSourcePullListener(RefPtr<SourceMediaTrack> aTrack,
+ const PrincipalHandle& aPrincipalHandle,
+ uint32_t aFrequency)
+ : mTrack(std::move(aTrack)),
+ mPrincipalHandle(aPrincipalHandle),
+ mSineGenerator(MakeUnique<SineWaveGenerator<int16_t>>(
+ mTrack->mSampleRate, aFrequency)) {
+ MOZ_COUNT_CTOR(AudioSourcePullListener);
+ }
+
+ MOZ_COUNTED_DTOR(AudioSourcePullListener)
+
+ void NotifyPull(MediaTrackGraph* aGraph, TrackTime aEndOfAppendedData,
+ TrackTime aDesiredTime) override;
+
+ const RefPtr<SourceMediaTrack> mTrack;
+ const PrincipalHandle mPrincipalHandle;
+ const UniquePtr<SineWaveGenerator<int16_t>> mSineGenerator;
+};
+
+/**
+ * Fake audio source.
+ */
+class MediaEngineFakeAudioSource : public MediaEngineSource {
+ public:
+ MediaEngineFakeAudioSource() = default;
+
+ static nsString GetUUID();
+ static nsString GetGroupId();
+
+ nsresult Allocate(const dom::MediaTrackConstraints& aConstraints,
+ const MediaEnginePrefs& aPrefs, uint64_t aWindowID,
+ const char** aOutBadConstraint) override;
+ void SetTrack(const RefPtr<MediaTrack>& aTrack,
+ const PrincipalHandle& aPrincipal) override;
+ nsresult Start() override;
+ nsresult Reconfigure(const dom::MediaTrackConstraints& aConstraints,
+ const MediaEnginePrefs& aPrefs,
+ const char** aOutBadConstraint) override;
+ nsresult Stop() override;
+ nsresult Deallocate() override;
+
+ bool IsFake() const override { return true; }
+
+ void GetSettings(dom::MediaTrackSettings& aOutSettings) const override;
+
+ protected:
+ ~MediaEngineFakeAudioSource() = default;
+
+ // Current state of this source.
+ MediaEngineSourceState mState = kReleased;
+ RefPtr<SourceMediaTrack> mTrack;
+ PrincipalHandle mPrincipalHandle = PRINCIPAL_HANDLE_NONE;
+ uint32_t mFrequency = 1000;
+ RefPtr<AudioSourcePullListener> mPullListener;
+};
+
+nsString MediaEngineFakeAudioSource::GetUUID() {
+ return u"B7CBD7C1-53EF-42F9-8353-73F61C70C092"_ns;
+}
+
+nsString MediaEngineFakeAudioSource::GetGroupId() {
+ return u"Fake Audio Group"_ns;
+}
+
+void MediaEngineFakeAudioSource::GetSettings(
+ MediaTrackSettings& aOutSettings) const {
+ MOZ_ASSERT(NS_IsMainThread());
+ aOutSettings.mAutoGainControl.Construct(false);
+ aOutSettings.mEchoCancellation.Construct(false);
+ aOutSettings.mNoiseSuppression.Construct(false);
+ aOutSettings.mChannelCount.Construct(1);
+}
+
+nsresult MediaEngineFakeAudioSource::Allocate(
+ const MediaTrackConstraints& aConstraints, const MediaEnginePrefs& aPrefs,
+ uint64_t aWindowID, const char** aOutBadConstraint) {
+ AssertIsOnOwningThread();
+
+ MOZ_ASSERT(mState == kReleased);
+
+ mFrequency = aPrefs.mFreq ? aPrefs.mFreq : 1000;
+
+ mState = kAllocated;
+ return NS_OK;
+}
+
+nsresult MediaEngineFakeAudioSource::Deallocate() {
+ AssertIsOnOwningThread();
+
+ MOZ_ASSERT(mState == kStopped || mState == kAllocated);
+
+ if (mTrack) {
+ mTrack->End();
+ mTrack = nullptr;
+ mPrincipalHandle = PRINCIPAL_HANDLE_NONE;
+ }
+ mState = kReleased;
+ return NS_OK;
+}
+
+void MediaEngineFakeAudioSource::SetTrack(const RefPtr<MediaTrack>& aTrack,
+ const PrincipalHandle& aPrincipal) {
+ AssertIsOnOwningThread();
+
+ MOZ_ASSERT(mState == kAllocated);
+ MOZ_ASSERT(!mTrack);
+ MOZ_ASSERT(aTrack->AsSourceTrack());
+
+ mTrack = aTrack->AsSourceTrack();
+ mPrincipalHandle = aPrincipal;
+}
+
+nsresult MediaEngineFakeAudioSource::Start() {
+ AssertIsOnOwningThread();
+
+ if (mState == kStarted) {
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(mState == kAllocated || mState == kStopped);
+ MOZ_ASSERT(mTrack, "SetTrack() must happen before Start()");
+
+ if (!mPullListener) {
+ mPullListener = MakeAndAddRef<AudioSourcePullListener>(
+ mTrack, mPrincipalHandle, mFrequency);
+ }
+
+ mState = kStarted;
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ __func__, [track = mTrack, listener = mPullListener]() {
+ if (track->IsDestroyed()) {
+ return;
+ }
+ track->AddListener(listener);
+ track->SetPullingEnabled(true);
+ }));
+
+ return NS_OK;
+}
+
+nsresult MediaEngineFakeAudioSource::Stop() {
+ AssertIsOnOwningThread();
+
+ if (mState == kStopped || mState == kAllocated) {
+ return NS_OK;
+ }
+ MOZ_ASSERT(mState == kStarted);
+ mState = kStopped;
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ __func__, [track = mTrack, listener = std::move(mPullListener)]() {
+ if (track->IsDestroyed()) {
+ return;
+ }
+ track->RemoveListener(listener);
+ track->SetPullingEnabled(false);
+ }));
+ return NS_OK;
+}
+
+nsresult MediaEngineFakeAudioSource::Reconfigure(
+ const MediaTrackConstraints& aConstraints, const MediaEnginePrefs& aPrefs,
+ const char** aOutBadConstraint) {
+ return NS_OK;
+}
+
+void AudioSourcePullListener::NotifyPull(MediaTrackGraph* aGraph,
+ TrackTime aEndOfAppendedData,
+ TrackTime aDesiredTime) {
+ TRACE_COMMENT("SourceMediaTrack::NotifyPull", "SourceMediaTrack %p",
+ mTrack.get());
+ AudioSegment segment;
+ TrackTicks delta = aDesiredTime - aEndOfAppendedData;
+ CheckedInt<size_t> bufferSize(sizeof(int16_t));
+ bufferSize *= delta;
+ RefPtr<SharedBuffer> buffer = SharedBuffer::Create(bufferSize);
+ int16_t* dest = static_cast<int16_t*>(buffer->Data());
+ mSineGenerator->generate(dest, delta);
+ AutoTArray<const int16_t*, 1> channels;
+ channels.AppendElement(dest);
+ segment.AppendFrames(buffer.forget(), channels, delta, mPrincipalHandle);
+ mTrack->AppendData(&segment);
+}
+
+MediaEngineFake::MediaEngineFake() = default;
+MediaEngineFake::~MediaEngineFake() = default;
+
+void MediaEngineFake::EnumerateDevices(
+ MediaSourceEnum aMediaSource, MediaSinkEnum aMediaSink,
+ nsTArray<RefPtr<MediaDevice>>* aDevices) {
+ AssertIsOnOwningThread();
+ using IsScary = MediaDevice::IsScary;
+ using OsPromptable = MediaDevice::OsPromptable;
+
+ if (aMediaSink == MediaSinkEnum::Speaker) {
+ NS_WARNING("No default implementation for MediaSinkEnum::Speaker");
+ }
+
+ switch (aMediaSource) {
+ case MediaSourceEnum::Camera: {
+ nsString name = FakeVideoName();
+ aDevices->EmplaceBack(
+ new MediaDevice(this, aMediaSource, name, /*aRawId=*/name,
+ MediaEngineFakeVideoSource::GetGroupId(), IsScary::No,
+ OsPromptable::No));
+ return;
+ }
+ case MediaSourceEnum::Microphone:
+ aDevices->EmplaceBack(
+ new MediaDevice(this, aMediaSource, u"Default Audio Device"_ns,
+ MediaEngineFakeAudioSource::GetUUID(),
+ MediaEngineFakeAudioSource::GetGroupId(), IsScary::No,
+ OsPromptable::No));
+ return;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unsupported source type");
+ return;
+ }
+}
+
+RefPtr<MediaEngineSource> MediaEngineFake::CreateSource(
+ const MediaDevice* aMediaDevice) {
+ MOZ_ASSERT(aMediaDevice->mEngine == this);
+ switch (aMediaDevice->mMediaSource) {
+ case MediaSourceEnum::Camera:
+ return new MediaEngineFakeVideoSource();
+ case MediaSourceEnum::Microphone:
+ return new MediaEngineFakeAudioSource();
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unsupported source type");
+ return nullptr;
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/MediaEngineFake.h b/dom/media/webrtc/MediaEngineFake.h
new file mode 100644
index 0000000000..611529a067
--- /dev/null
+++ b/dom/media/webrtc/MediaEngineFake.h
@@ -0,0 +1,40 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MEDIAENGINEFAKE_H_
+#define MEDIAENGINEFAKE_H_
+
+#include "nsTArrayForwardDeclare.h"
+#include "MediaEngine.h"
+
+namespace mozilla {
+
+template <typename...>
+class MediaEventProducer;
+
+/**
+ * The fake implementation of the MediaEngine interface.
+ */
+class MediaEngineFake : public MediaEngine {
+ public:
+ MediaEngineFake();
+
+ void EnumerateDevices(dom::MediaSourceEnum, MediaSinkEnum,
+ nsTArray<RefPtr<MediaDevice>>*) override;
+ void Shutdown() override {}
+ RefPtr<MediaEngineSource> CreateSource(const MediaDevice* aDevice) override;
+
+ MediaEventSource<void>& DeviceListChangeEvent() override {
+ return mDeviceListChangeEvent;
+ }
+ bool IsFake() const override { return true; }
+
+ private:
+ ~MediaEngineFake();
+ MediaEventProducer<void> mDeviceListChangeEvent;
+};
+
+} // namespace mozilla
+
+#endif /* NSMEDIAENGINEFAKE_H_ */
diff --git a/dom/media/webrtc/MediaEnginePrefs.h b/dom/media/webrtc/MediaEnginePrefs.h
new file mode 100644
index 0000000000..6c9775a238
--- /dev/null
+++ b/dom/media/webrtc/MediaEnginePrefs.h
@@ -0,0 +1,101 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MediaEnginePrefs_h
+#define MediaEnginePrefs_h
+
+#include <stdint.h>
+#include <string.h>
+
+namespace mozilla {
+
+/**
+ * Video source and friends.
+ */
+class MediaEnginePrefs {
+ public:
+ static const int DEFAULT_VIDEO_FPS = 30;
+ static const int DEFAULT_43_VIDEO_WIDTH = 640;
+ static const int DEFAULT_43_VIDEO_HEIGHT = 480;
+ static const int DEFAULT_169_VIDEO_WIDTH = 1280;
+ static const int DEFAULT_169_VIDEO_HEIGHT = 720;
+
+ MediaEnginePrefs()
+ : mWidth(0),
+ mHeight(0),
+ mFPS(0),
+ mFreq(0),
+ mAecOn(false),
+ mUseAecMobile(false),
+ mAgcOn(false),
+ mHPFOn(false),
+ mNoiseOn(false),
+ mTransientOn(false),
+ mResidualEchoOn(false),
+ mAgc2Forced(false),
+ mAgc(0),
+ mNoise(0),
+ mChannels(0) {}
+
+ int32_t mWidth;
+ int32_t mHeight;
+ int32_t mFPS;
+ int32_t mFreq; // for test tones (fake:true)
+ bool mAecOn;
+ bool mUseAecMobile;
+ bool mAgcOn;
+ bool mHPFOn;
+ bool mNoiseOn;
+ bool mTransientOn;
+ bool mResidualEchoOn;
+ bool mAgc2Forced;
+ int32_t mAgc;
+ int32_t mNoise;
+ int32_t mChannels;
+
+ bool operator==(const MediaEnginePrefs& aRhs) {
+ return memcmp(this, &aRhs, sizeof(MediaEnginePrefs)) == 0;
+ };
+
+ // mWidth and/or mHeight may be zero (=adaptive default), so use functions.
+
+ int32_t GetWidth(bool aHD = false) const {
+ return mWidth ? mWidth
+ : (mHeight ? (mHeight * GetDefWidth(aHD)) / GetDefHeight(aHD)
+ : GetDefWidth(aHD));
+ }
+
+ int32_t GetHeight(bool aHD = false) const {
+ return mHeight ? mHeight
+ : (mWidth ? (mWidth * GetDefHeight(aHD)) / GetDefWidth(aHD)
+ : GetDefHeight(aHD));
+ }
+
+ private:
+ static int32_t GetDefWidth(bool aHD = false) {
+ // It'd be nice if we could use the ternary operator here, but we can't
+ // because of bug 1002729.
+ if (aHD) {
+ return DEFAULT_169_VIDEO_WIDTH;
+ }
+
+ return DEFAULT_43_VIDEO_WIDTH;
+ }
+
+ static int32_t GetDefHeight(bool aHD = false) {
+ // It'd be nice if we could use the ternary operator here, but we can't
+ // because of bug 1002729.
+ if (aHD) {
+ return DEFAULT_169_VIDEO_HEIGHT;
+ }
+
+ return DEFAULT_43_VIDEO_HEIGHT;
+ }
+};
+
+} // namespace mozilla
+
+#endif // MediaEnginePrefs_h
diff --git a/dom/media/webrtc/MediaEngineRemoteVideoSource.cpp b/dom/media/webrtc/MediaEngineRemoteVideoSource.cpp
new file mode 100644
index 0000000000..ce9f4ad9a8
--- /dev/null
+++ b/dom/media/webrtc/MediaEngineRemoteVideoSource.cpp
@@ -0,0 +1,907 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaEngineRemoteVideoSource.h"
+
+#include "CamerasChild.h"
+#include "MediaManager.h"
+#include "MediaTrackConstraints.h"
+#include "mozilla/dom/MediaTrackSettingsBinding.h"
+#include "mozilla/ErrorNames.h"
+#include "mozilla/gfx/Point.h"
+#include "mozilla/RefPtr.h"
+#include "PerformanceRecorder.h"
+#include "Tracing.h"
+#include "VideoFrameUtils.h"
+#include "VideoUtils.h"
+#include "ImageContainer.h"
+#include "common_video/include/video_frame_buffer.h"
+#include "common_video/libyuv/include/webrtc_libyuv.h"
+
+namespace mozilla {
+
+extern LazyLogModule gMediaManagerLog;
+#define LOG(...) MOZ_LOG(gMediaManagerLog, LogLevel::Debug, (__VA_ARGS__))
+#define LOG_FRAME(...) \
+ MOZ_LOG(gMediaManagerLog, LogLevel::Verbose, (__VA_ARGS__))
+
+using dom::ConstrainLongRange;
+using dom::MediaSourceEnum;
+using dom::MediaTrackConstraints;
+using dom::MediaTrackConstraintSet;
+using dom::MediaTrackSettings;
+using dom::VideoFacingModeEnum;
+
+/* static */
+camera::CaptureEngine MediaEngineRemoteVideoSource::CaptureEngine(
+ MediaSourceEnum aMediaSource) {
+ switch (aMediaSource) {
+ case MediaSourceEnum::Browser:
+ return camera::BrowserEngine;
+ case MediaSourceEnum::Camera:
+ return camera::CameraEngine;
+ case MediaSourceEnum::Screen:
+ return camera::ScreenEngine;
+ case MediaSourceEnum::Window:
+ return camera::WinEngine;
+ default:
+ MOZ_CRASH();
+ }
+}
+
+static Maybe<VideoFacingModeEnum> GetFacingMode(const nsString& aDeviceName) {
+ // Set facing mode based on device name.
+#if defined(ANDROID)
+ // Names are generated. Example: "Camera 0, Facing back, Orientation 90"
+ //
+ // See media/webrtc/trunk/webrtc/modules/video_capture/android/java/src/org/
+ // webrtc/videoengine/VideoCaptureDeviceInfoAndroid.java
+
+ if (aDeviceName.Find(u"Facing back"_ns) != kNotFound) {
+ return Some(VideoFacingModeEnum::Environment);
+ }
+ if (aDeviceName.Find(u"Facing front"_ns) != kNotFound) {
+ return Some(VideoFacingModeEnum::User);
+ }
+#endif // ANDROID
+#ifdef XP_MACOSX
+ // Kludge to test user-facing cameras on OSX.
+ if (aDeviceName.Find(u"Face"_ns) != -1) {
+ return Some(VideoFacingModeEnum::User);
+ }
+#endif
+#ifdef XP_WIN
+ // The cameras' name of Surface book are "Microsoft Camera Front" and
+ // "Microsoft Camera Rear" respectively.
+
+ if (aDeviceName.Find(u"Front"_ns) != kNotFound) {
+ return Some(VideoFacingModeEnum::User);
+ }
+ if (aDeviceName.Find(u"Rear"_ns) != kNotFound) {
+ return Some(VideoFacingModeEnum::Environment);
+ }
+#endif // WINDOWS
+
+ return Nothing();
+}
+
+MediaEngineRemoteVideoSource::MediaEngineRemoteVideoSource(
+ const MediaDevice* aMediaDevice)
+ : mCapEngine(CaptureEngine(aMediaDevice->mMediaSource)),
+ mTrackingId(CaptureEngineToTrackingSourceStr(mCapEngine), 0),
+ mMutex("MediaEngineRemoteVideoSource::mMutex"),
+ mRescalingBufferPool(/* zero_initialize */ false,
+ /* max_number_of_buffers */ 1),
+ mSettingsUpdatedByFrame(MakeAndAddRef<media::Refcountable<AtomicBool>>()),
+ mSettings(MakeAndAddRef<media::Refcountable<MediaTrackSettings>>()),
+ mFirstFramePromise(mFirstFramePromiseHolder.Ensure(__func__)),
+ mMediaDevice(aMediaDevice),
+ mDeviceUUID(NS_ConvertUTF16toUTF8(aMediaDevice->mRawID)) {
+ LOG("%s", __PRETTY_FUNCTION__);
+ mSettings->mWidth.Construct(0);
+ mSettings->mHeight.Construct(0);
+ mSettings->mFrameRate.Construct(0);
+ if (mCapEngine == camera::CameraEngine) {
+ // Only cameras can have a facing mode.
+ Maybe<VideoFacingModeEnum> facingMode =
+ GetFacingMode(mMediaDevice->mRawName);
+ if (facingMode.isSome()) {
+ NS_ConvertASCIItoUTF16 facingString(
+ dom::VideoFacingModeEnumValues::GetString(*facingMode));
+ mSettings->mFacingMode.Construct(facingString);
+ mFacingMode.emplace(facingString);
+ }
+ }
+}
+
+MediaEngineRemoteVideoSource::~MediaEngineRemoteVideoSource() {
+ mFirstFramePromiseHolder.RejectIfExists(NS_ERROR_ABORT, __func__);
+}
+
+nsresult MediaEngineRemoteVideoSource::Allocate(
+ const MediaTrackConstraints& aConstraints, const MediaEnginePrefs& aPrefs,
+ uint64_t aWindowID, const char** aOutBadConstraint) {
+ LOG("%s", __PRETTY_FUNCTION__);
+ AssertIsOnOwningThread();
+
+ MOZ_ASSERT(mState == kReleased);
+
+ NormalizedConstraints constraints(aConstraints);
+ webrtc::CaptureCapability newCapability;
+ LOG("ChooseCapability(kFitness) for mCapability (Allocate) ++");
+ if (!ChooseCapability(constraints, aPrefs, newCapability, kFitness)) {
+ *aOutBadConstraint =
+ MediaConstraintsHelper::FindBadConstraint(constraints, mMediaDevice);
+ return NS_ERROR_FAILURE;
+ }
+ LOG("ChooseCapability(kFitness) for mCapability (Allocate) --");
+
+ mCaptureId =
+ camera::GetChildAndCall(&camera::CamerasChild::AllocateCapture,
+ mCapEngine, mDeviceUUID.get(), aWindowID);
+ if (mCaptureId < 0) {
+ return NS_ERROR_FAILURE;
+ }
+
+ {
+ MutexAutoLock lock(mMutex);
+ mState = kAllocated;
+ mCapability = newCapability;
+ mTrackingId =
+ TrackingId(CaptureEngineToTrackingSourceStr(mCapEngine), mCaptureId);
+ }
+
+ LOG("Video device %d allocated", mCaptureId);
+ return NS_OK;
+}
+
+nsresult MediaEngineRemoteVideoSource::Deallocate() {
+ LOG("%s", __PRETTY_FUNCTION__);
+ AssertIsOnOwningThread();
+
+ MOZ_ASSERT(mState == kStopped || mState == kAllocated);
+
+ if (mTrack) {
+ mTrack->End();
+ }
+
+ {
+ MutexAutoLock lock(mMutex);
+
+ mTrack = nullptr;
+ mPrincipal = PRINCIPAL_HANDLE_NONE;
+ mState = kReleased;
+ }
+
+ // Stop() has stopped capture synchronously on the media thread before we get
+ // here, so there are no longer any callbacks on an IPC thread accessing
+ // mImageContainer or mRescalingBufferPool.
+ mImageContainer = nullptr;
+ mRescalingBufferPool.Release();
+
+ LOG("Video device %d deallocated", mCaptureId);
+
+ if (camera::GetChildAndCall(&camera::CamerasChild::ReleaseCapture, mCapEngine,
+ mCaptureId)) {
+ // Failure can occur when the parent process is shutting down.
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+void MediaEngineRemoteVideoSource::SetTrack(const RefPtr<MediaTrack>& aTrack,
+ const PrincipalHandle& aPrincipal) {
+ LOG("%s", __PRETTY_FUNCTION__);
+ AssertIsOnOwningThread();
+
+ MOZ_ASSERT(mState == kAllocated);
+ MOZ_ASSERT(!mTrack);
+ MOZ_ASSERT(aTrack);
+ MOZ_ASSERT(aTrack->AsSourceTrack());
+
+ if (!mImageContainer) {
+ mImageContainer = MakeAndAddRef<layers::ImageContainer>(
+ layers::ImageContainer::ASYNCHRONOUS);
+ }
+
+ {
+ MutexAutoLock lock(mMutex);
+ mTrack = aTrack->AsSourceTrack();
+ mPrincipal = aPrincipal;
+ }
+}
+
+nsresult MediaEngineRemoteVideoSource::Start() {
+ LOG("%s", __PRETTY_FUNCTION__);
+ AssertIsOnOwningThread();
+
+ MOZ_ASSERT(mState == kAllocated || mState == kStopped);
+ MOZ_ASSERT(mTrack);
+
+ {
+ MutexAutoLock lock(mMutex);
+ mState = kStarted;
+ }
+
+ mSettingsUpdatedByFrame->mValue = false;
+
+ if (camera::GetChildAndCall(&camera::CamerasChild::StartCapture, mCapEngine,
+ mCaptureId, mCapability, this)) {
+ LOG("StartCapture failed");
+ MutexAutoLock lock(mMutex);
+ mState = kStopped;
+ return NS_ERROR_FAILURE;
+ }
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "MediaEngineRemoteVideoSource::SetLastCapability",
+ [settings = mSettings, updated = mSettingsUpdatedByFrame,
+ capEngine = mCapEngine, cap = mCapability]() mutable {
+ switch (capEngine) {
+ case camera::ScreenEngine:
+ case camera::WinEngine:
+ // Undo the hack where ideal and max constraints are crammed
+ // together in mCapability for consumption by low-level code. We
+ // don't actually know the real resolution yet, so report min(ideal,
+ // max) for now.
+ // TODO: This can be removed in bug 1453269.
+ cap.width = std::min(cap.width >> 16, cap.width & 0xffff);
+ cap.height = std::min(cap.height >> 16, cap.height & 0xffff);
+ break;
+ default:
+ break;
+ }
+
+ if (!updated->mValue) {
+ settings->mWidth.Value() = cap.width;
+ settings->mHeight.Value() = cap.height;
+ }
+ settings->mFrameRate.Value() = cap.maxFPS;
+ }));
+
+ return NS_OK;
+}
+
+nsresult MediaEngineRemoteVideoSource::FocusOnSelectedSource() {
+ LOG("%s", __PRETTY_FUNCTION__);
+ AssertIsOnOwningThread();
+
+ int result;
+ result = camera::GetChildAndCall(&camera::CamerasChild::FocusOnSelectedSource,
+ mCapEngine, mCaptureId);
+ return result == 0 ? NS_OK : NS_ERROR_FAILURE;
+}
+
+nsresult MediaEngineRemoteVideoSource::Stop() {
+ LOG("%s", __PRETTY_FUNCTION__);
+ AssertIsOnOwningThread();
+
+ if (mState == kStopped || mState == kAllocated) {
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(mState == kStarted);
+
+ if (camera::GetChildAndCall(&camera::CamerasChild::StopCapture, mCapEngine,
+ mCaptureId)) {
+ // Failure can occur when the parent process is shutting down.
+ return NS_ERROR_FAILURE;
+ }
+
+ {
+ MutexAutoLock lock(mMutex);
+ mState = kStopped;
+ }
+
+ return NS_OK;
+}
+
+nsresult MediaEngineRemoteVideoSource::Reconfigure(
+ const MediaTrackConstraints& aConstraints, const MediaEnginePrefs& aPrefs,
+ const char** aOutBadConstraint) {
+ LOG("%s", __PRETTY_FUNCTION__);
+ AssertIsOnOwningThread();
+
+ NormalizedConstraints constraints(aConstraints);
+ webrtc::CaptureCapability newCapability;
+ LOG("ChooseCapability(kFitness) for mTargetCapability (Reconfigure) ++");
+ if (!ChooseCapability(constraints, aPrefs, newCapability, kFitness)) {
+ *aOutBadConstraint =
+ MediaConstraintsHelper::FindBadConstraint(constraints, mMediaDevice);
+ return NS_ERROR_INVALID_ARG;
+ }
+ LOG("ChooseCapability(kFitness) for mTargetCapability (Reconfigure) --");
+
+ if (mCapability == newCapability) {
+ return NS_OK;
+ }
+
+ bool started = mState == kStarted;
+ if (started) {
+ nsresult rv = Stop();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ nsAutoCString name;
+ GetErrorName(rv, name);
+ LOG("Video source %p for video device %d Reconfigure() failed "
+ "unexpectedly in Stop(). rv=%s",
+ this, mCaptureId, name.Data());
+ return NS_ERROR_UNEXPECTED;
+ }
+ }
+
+ {
+ MutexAutoLock lock(mMutex);
+ // Start() applies mCapability on the device.
+ mCapability = newCapability;
+ }
+
+ if (started) {
+ nsresult rv = Start();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ nsAutoCString name;
+ GetErrorName(rv, name);
+ LOG("Video source %p for video device %d Reconfigure() failed "
+ "unexpectedly in Start(). rv=%s",
+ this, mCaptureId, name.Data());
+ return NS_ERROR_UNEXPECTED;
+ }
+ }
+
+ return NS_OK;
+}
+
+size_t MediaEngineRemoteVideoSource::NumCapabilities() const {
+ AssertIsOnOwningThread();
+
+ if (!mCapabilities.IsEmpty()) {
+ return mCapabilities.Length();
+ }
+
+ int num = camera::GetChildAndCall(&camera::CamerasChild::NumberOfCapabilities,
+ mCapEngine, mDeviceUUID.get());
+ if (num > 0) {
+ mCapabilities.SetLength(num);
+ } else {
+ // The default for devices that don't return discrete capabilities: treat
+ // them as supporting all capabilities orthogonally. E.g. screensharing.
+ // CaptureCapability defaults key values to 0, which means accept any value.
+ mCapabilities.AppendElement(MakeUnique<webrtc::CaptureCapability>());
+ mCapabilitiesAreHardcoded = true;
+ }
+
+ return mCapabilities.Length();
+}
+
+webrtc::CaptureCapability& MediaEngineRemoteVideoSource::GetCapability(
+ size_t aIndex) const {
+ AssertIsOnOwningThread();
+ MOZ_RELEASE_ASSERT(aIndex < mCapabilities.Length());
+ if (!mCapabilities[aIndex]) {
+ mCapabilities[aIndex] = MakeUnique<webrtc::CaptureCapability>();
+ camera::GetChildAndCall(&camera::CamerasChild::GetCaptureCapability,
+ mCapEngine, mDeviceUUID.get(), aIndex,
+ mCapabilities[aIndex].get());
+ }
+ return *mCapabilities[aIndex];
+}
+
+const TrackingId& MediaEngineRemoteVideoSource::GetTrackingId() const {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mState != kReleased);
+ return mTrackingId;
+}
+
+int MediaEngineRemoteVideoSource::DeliverFrame(
+ uint8_t* aBuffer, const camera::VideoFrameProperties& aProps) {
+ // Cameras IPC thread - take great care with accessing members!
+
+ Maybe<int32_t> req_max_width;
+ Maybe<int32_t> req_max_height;
+ Maybe<int32_t> req_ideal_width;
+ Maybe<int32_t> req_ideal_height;
+ {
+ MutexAutoLock lock(mMutex);
+ MOZ_ASSERT(mState == kStarted);
+ // TODO: These can be removed in bug 1453269.
+ const int32_t max_width = mCapability.width & 0xffff;
+ const int32_t max_height = mCapability.height & 0xffff;
+ const int32_t ideal_width = (mCapability.width >> 16) & 0xffff;
+ const int32_t ideal_height = (mCapability.height >> 16) & 0xffff;
+
+ req_max_width = max_width ? Some(max_width) : Nothing();
+ req_max_height = max_height ? Some(max_height) : Nothing();
+ req_ideal_width = ideal_width ? Some(ideal_width) : Nothing();
+ req_ideal_height = ideal_height ? Some(ideal_height) : Nothing();
+ if (!mFrameDeliveringTrackingId) {
+ mFrameDeliveringTrackingId = Some(mTrackingId);
+ }
+ }
+
+ // This is only used in the case of screen sharing, see bug 1453269.
+
+ if (aProps.rotation() == 90 || aProps.rotation() == 270) {
+ // This frame is rotated, so what was negotiated as width is now height,
+ // and vice versa.
+ std::swap(req_max_width, req_max_height);
+ std::swap(req_ideal_width, req_ideal_height);
+ }
+
+ int32_t dst_max_width =
+ std::min(aProps.width(), req_max_width.valueOr(aProps.width()));
+ int32_t dst_max_height =
+ std::min(aProps.height(), req_max_height.valueOr(aProps.height()));
+ // This logic works for both camera and screen sharing case.
+ // for camera case, req_ideal_width and req_ideal_height are absent.
+ int32_t dst_width = req_ideal_width.valueOr(aProps.width());
+ int32_t dst_height = req_ideal_height.valueOr(aProps.height());
+
+ if (!req_ideal_width && req_ideal_height) {
+ dst_width = *req_ideal_height * aProps.width() / aProps.height();
+ } else if (!req_ideal_height && req_ideal_width) {
+ dst_height = *req_ideal_width * aProps.height() / aProps.width();
+ }
+ dst_width = std::min(dst_width, dst_max_width);
+ dst_height = std::min(dst_height, dst_max_height);
+
+ // Apply scaling for screen sharing, see bug 1453269.
+ switch (mCapEngine) {
+ case camera::ScreenEngine:
+ case camera::WinEngine: {
+ // scale to average of portrait and landscape
+ float scale_width = (float)dst_width / (float)aProps.width();
+ float scale_height = (float)dst_height / (float)aProps.height();
+ float scale = (scale_width + scale_height) / 2;
+ // If both req_ideal_width & req_ideal_height are absent, scale is 1, but
+ // if one is present and the other not, scale precisely to the one present
+ if (!req_ideal_width) {
+ scale = scale_height;
+ } else if (!req_ideal_height) {
+ scale = scale_width;
+ }
+ dst_width = int32_t(scale * (float)aProps.width());
+ dst_height = int32_t(scale * (float)aProps.height());
+
+ // if scaled rectangle exceeds max rectangle, scale to minimum of portrait
+ // and landscape
+ if (dst_width > dst_max_width || dst_height > dst_max_height) {
+ scale_width = (float)dst_max_width / (float)dst_width;
+ scale_height = (float)dst_max_height / (float)dst_height;
+ scale = std::min(scale_width, scale_height);
+ dst_width = int32_t(scale * dst_width);
+ dst_height = int32_t(scale * dst_height);
+ }
+ break;
+ }
+ default: {
+ break;
+ }
+ }
+
+ // Ensure width and height are at least two. Smaller frames can lead to
+ // problems with scaling and video encoding.
+ dst_width = std::max(2, dst_width);
+ dst_height = std::max(2, dst_height);
+
+ std::function<void()> callback_unused = []() {};
+ rtc::scoped_refptr<webrtc::I420BufferInterface> buffer =
+ webrtc::WrapI420Buffer(
+ aProps.width(), aProps.height(), aBuffer, aProps.yStride(),
+ aBuffer + aProps.yAllocatedSize(), aProps.uStride(),
+ aBuffer + aProps.yAllocatedSize() + aProps.uAllocatedSize(),
+ aProps.vStride(), callback_unused);
+
+ if ((dst_width != aProps.width() || dst_height != aProps.height()) &&
+ dst_width <= aProps.width() && dst_height <= aProps.height()) {
+ PerformanceRecorder<CopyVideoStage> rec("MERVS::CropAndScale"_ns,
+ *mFrameDeliveringTrackingId,
+ dst_width, dst_height);
+ // Destination resolution is smaller than source buffer. We'll rescale.
+ rtc::scoped_refptr<webrtc::I420Buffer> scaledBuffer =
+ mRescalingBufferPool.CreateI420Buffer(dst_width, dst_height);
+ if (!scaledBuffer) {
+ MOZ_ASSERT_UNREACHABLE(
+ "We might fail to allocate a buffer, but with this "
+ "being a recycling pool that shouldn't happen");
+ return 0;
+ }
+ scaledBuffer->CropAndScaleFrom(*buffer);
+ buffer = scaledBuffer;
+ rec.Record();
+ }
+
+ layers::PlanarYCbCrData data;
+ data.mYChannel = const_cast<uint8_t*>(buffer->DataY());
+ data.mYStride = buffer->StrideY();
+ MOZ_ASSERT(buffer->StrideU() == buffer->StrideV());
+ data.mCbCrStride = buffer->StrideU();
+ data.mCbChannel = const_cast<uint8_t*>(buffer->DataU());
+ data.mCrChannel = const_cast<uint8_t*>(buffer->DataV());
+ data.mPictureRect = gfx::IntRect(0, 0, buffer->width(), buffer->height());
+ data.mYUVColorSpace = gfx::YUVColorSpace::BT601;
+ data.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT;
+
+ RefPtr<layers::PlanarYCbCrImage> image;
+ {
+ PerformanceRecorder<CopyVideoStage> rec(
+ "MERVS::Copy"_ns, *mFrameDeliveringTrackingId, dst_width, dst_height);
+ image = mImageContainer->CreatePlanarYCbCrImage();
+ if (!image->CopyData(data)) {
+ MOZ_ASSERT_UNREACHABLE(
+ "We might fail to allocate a buffer, but with this "
+ "being a recycling container that shouldn't happen");
+ return 0;
+ }
+ rec.Record();
+ }
+
+#ifdef DEBUG
+ static uint32_t frame_num = 0;
+ LOG_FRAME(
+ "frame %d (%dx%d)->(%dx%d); rotation %d, timeStamp %u, ntpTimeMs %" PRIu64
+ ", renderTimeMs %" PRIu64,
+ frame_num++, aProps.width(), aProps.height(), dst_width, dst_height,
+ aProps.rotation(), aProps.timeStamp(), aProps.ntpTimeMs(),
+ aProps.renderTimeMs());
+#endif
+
+ if (mImageSize.width != dst_width || mImageSize.height != dst_height) {
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "MediaEngineRemoteVideoSource::FrameSizeChange",
+ [settings = mSettings, updated = mSettingsUpdatedByFrame,
+ holder = std::move(mFirstFramePromiseHolder), dst_width,
+ dst_height]() mutable {
+ settings->mWidth.Value() = dst_width;
+ settings->mHeight.Value() = dst_height;
+ updated->mValue = true;
+ // Since mImageSize was initialized to (0,0), we end up here on the
+ // arrival of the first frame. We resolve the promise representing
+ // arrival of first frame, after correct settings values have been
+ // made available (Resolve() is idempotent if already resolved).
+ holder.ResolveIfExists(true, __func__);
+ }));
+ }
+
+ {
+ MutexAutoLock lock(mMutex);
+ MOZ_ASSERT(mState == kStarted);
+ VideoSegment segment;
+ mImageSize = image->GetSize();
+ segment.AppendFrame(image.forget(), mImageSize, mPrincipal);
+ mTrack->AppendData(&segment);
+ }
+
+ return 0;
+}
+
+uint32_t MediaEngineRemoteVideoSource::GetDistance(
+ const webrtc::CaptureCapability& aCandidate,
+ const NormalizedConstraintSet& aConstraints,
+ const DistanceCalculation aCalculate) const {
+ if (aCalculate == kFeasibility) {
+ return GetFeasibilityDistance(aCandidate, aConstraints);
+ }
+ return GetFitnessDistance(aCandidate, aConstraints);
+}
+
+uint32_t MediaEngineRemoteVideoSource::GetFitnessDistance(
+ const webrtc::CaptureCapability& aCandidate,
+ const NormalizedConstraintSet& aConstraints) const {
+ AssertIsOnOwningThread();
+
+ // Treat width|height|frameRate == 0 on capability as "can do any".
+ // This allows for orthogonal capabilities that are not in discrete steps.
+
+ typedef MediaConstraintsHelper H;
+ uint64_t distance =
+ uint64_t(H::FitnessDistance(mFacingMode, aConstraints.mFacingMode)) +
+ uint64_t(aCandidate.width ? H::FitnessDistance(int32_t(aCandidate.width),
+ aConstraints.mWidth)
+ : 0) +
+ uint64_t(aCandidate.height
+ ? H::FitnessDistance(int32_t(aCandidate.height),
+ aConstraints.mHeight)
+ : 0) +
+ uint64_t(aCandidate.maxFPS ? H::FitnessDistance(double(aCandidate.maxFPS),
+ aConstraints.mFrameRate)
+ : 0);
+ return uint32_t(std::min(distance, uint64_t(UINT32_MAX)));
+}
+
+uint32_t MediaEngineRemoteVideoSource::GetFeasibilityDistance(
+ const webrtc::CaptureCapability& aCandidate,
+ const NormalizedConstraintSet& aConstraints) const {
+ AssertIsOnOwningThread();
+
+ // Treat width|height|frameRate == 0 on capability as "can do any".
+ // This allows for orthogonal capabilities that are not in discrete steps.
+
+ typedef MediaConstraintsHelper H;
+ uint64_t distance =
+ uint64_t(H::FitnessDistance(mFacingMode, aConstraints.mFacingMode)) +
+ uint64_t(aCandidate.width
+ ? H::FeasibilityDistance(int32_t(aCandidate.width),
+ aConstraints.mWidth)
+ : 0) +
+ uint64_t(aCandidate.height
+ ? H::FeasibilityDistance(int32_t(aCandidate.height),
+ aConstraints.mHeight)
+ : 0) +
+ uint64_t(aCandidate.maxFPS
+ ? H::FeasibilityDistance(double(aCandidate.maxFPS),
+ aConstraints.mFrameRate)
+ : 0);
+ return uint32_t(std::min(distance, uint64_t(UINT32_MAX)));
+}
+
+// Find best capability by removing inferiors. May leave >1 of equal distance
+
+/* static */
+void MediaEngineRemoteVideoSource::TrimLessFitCandidates(
+ nsTArray<CapabilityCandidate>& aSet) {
+ uint32_t best = UINT32_MAX;
+ for (auto& candidate : aSet) {
+ if (best > candidate.mDistance) {
+ best = candidate.mDistance;
+ }
+ }
+ aSet.RemoveElementsBy(
+ [best](const auto& set) { return set.mDistance > best; });
+ MOZ_ASSERT(aSet.Length());
+}
+
+uint32_t MediaEngineRemoteVideoSource::GetBestFitnessDistance(
+ const nsTArray<const NormalizedConstraintSet*>& aConstraintSets) const {
+ AssertIsOnOwningThread();
+
+ size_t num = NumCapabilities();
+ nsTArray<CapabilityCandidate> candidateSet;
+ for (size_t i = 0; i < num; i++) {
+ candidateSet.AppendElement(CapabilityCandidate(GetCapability(i)));
+ }
+
+ bool first = true;
+ for (const NormalizedConstraintSet* ns : aConstraintSets) {
+ for (size_t i = 0; i < candidateSet.Length();) {
+ auto& candidate = candidateSet[i];
+ uint32_t distance = GetFitnessDistance(candidate.mCapability, *ns);
+ if (distance == UINT32_MAX) {
+ candidateSet.RemoveElementAt(i);
+ } else {
+ ++i;
+ if (first) {
+ candidate.mDistance = distance;
+ }
+ }
+ }
+ first = false;
+ }
+ if (!candidateSet.Length()) {
+ return UINT32_MAX;
+ }
+ TrimLessFitCandidates(candidateSet);
+ return candidateSet[0].mDistance;
+}
+
+static const char* ConvertVideoTypeToCStr(webrtc::VideoType aType) {
+ switch (aType) {
+ case webrtc::VideoType::kI420:
+ return "I420";
+ case webrtc::VideoType::kIYUV:
+ case webrtc::VideoType::kYV12:
+ return "YV12";
+ case webrtc::VideoType::kRGB24:
+ return "24BG";
+ case webrtc::VideoType::kABGR:
+ return "ABGR";
+ case webrtc::VideoType::kARGB:
+ return "ARGB";
+ case webrtc::VideoType::kARGB4444:
+ return "R444";
+ case webrtc::VideoType::kRGB565:
+ return "RGBP";
+ case webrtc::VideoType::kARGB1555:
+ return "RGBO";
+ case webrtc::VideoType::kYUY2:
+ return "YUY2";
+ case webrtc::VideoType::kUYVY:
+ return "UYVY";
+ case webrtc::VideoType::kMJPEG:
+ return "MJPG";
+ case webrtc::VideoType::kNV21:
+ return "NV21";
+ case webrtc::VideoType::kNV12:
+ return "NV12";
+ case webrtc::VideoType::kBGRA:
+ return "BGRA";
+ case webrtc::VideoType::kUnknown:
+ default:
+ return "unknown";
+ }
+}
+
+static void LogCapability(const char* aHeader,
+ const webrtc::CaptureCapability& aCapability,
+ uint32_t aDistance) {
+ LOG("%s: %4u x %4u x %2u maxFps, %s. Distance = %" PRIu32, aHeader,
+ aCapability.width, aCapability.height, aCapability.maxFPS,
+ ConvertVideoTypeToCStr(aCapability.videoType), aDistance);
+}
+
+bool MediaEngineRemoteVideoSource::ChooseCapability(
+ const NormalizedConstraints& aConstraints, const MediaEnginePrefs& aPrefs,
+ webrtc::CaptureCapability& aCapability,
+ const DistanceCalculation aCalculate) {
+ LOG("%s", __PRETTY_FUNCTION__);
+ AssertIsOnOwningThread();
+
+ if (MOZ_LOG_TEST(gMediaManagerLog, LogLevel::Debug)) {
+ LOG("ChooseCapability: prefs: %dx%d @%dfps", aPrefs.GetWidth(),
+ aPrefs.GetHeight(), aPrefs.mFPS);
+ MediaConstraintsHelper::LogConstraints(aConstraints);
+ if (!aConstraints.mAdvanced.empty()) {
+ LOG("Advanced array[%zu]:", aConstraints.mAdvanced.size());
+ for (auto& advanced : aConstraints.mAdvanced) {
+ MediaConstraintsHelper::LogConstraints(advanced);
+ }
+ }
+ }
+
+ switch (mCapEngine) {
+ case camera::ScreenEngine:
+ case camera::WinEngine: {
+ FlattenedConstraints c(aConstraints);
+ // The actual resolution to constrain around is not easy to find ahead of
+ // time (and may in fact change over time), so as a hack, we push ideal
+ // and max constraints down to desktop_capture_impl.cc and finish the
+ // algorithm there.
+ // TODO: This can be removed in bug 1453269.
+ aCapability.width =
+ (std::min(0xffff, c.mWidth.mIdeal.valueOr(0)) & 0xffff) << 16 |
+ (std::min(0xffff, c.mWidth.mMax) & 0xffff);
+ aCapability.height =
+ (std::min(0xffff, c.mHeight.mIdeal.valueOr(0)) & 0xffff) << 16 |
+ (std::min(0xffff, c.mHeight.mMax) & 0xffff);
+ aCapability.maxFPS =
+ c.mFrameRate.Clamp(c.mFrameRate.mIdeal.valueOr(aPrefs.mFPS));
+ return true;
+ }
+ case camera::BrowserEngine: {
+ FlattenedConstraints c(aConstraints);
+ aCapability.maxFPS =
+ c.mFrameRate.Clamp(c.mFrameRate.mIdeal.valueOr(aPrefs.mFPS));
+ return true;
+ }
+ default:
+ break;
+ }
+
+ nsTArray<CapabilityCandidate> candidateSet;
+ size_t num = NumCapabilities();
+ for (size_t i = 0; i < num; i++) {
+ candidateSet.AppendElement(CapabilityCandidate(GetCapability(i)));
+ }
+
+ if (mCapabilitiesAreHardcoded && mCapEngine == camera::CameraEngine) {
+ // We have a hardcoded capability, which means this camera didn't report
+ // discrete capabilities. It might still allow a ranged capability, so we
+ // add a couple of default candidates based on prefs and constraints.
+ // The chosen candidate will be propagated to StartCapture() which will fail
+ // for an invalid candidate.
+ MOZ_DIAGNOSTIC_ASSERT(mCapabilities.Length() == 1);
+ MOZ_DIAGNOSTIC_ASSERT(candidateSet.Length() == 1);
+ candidateSet.Clear();
+
+ FlattenedConstraints c(aConstraints);
+ // Reuse the code across both the low-definition (`false`) pref and
+ // the high-definition (`true`) pref.
+ // If there are constraints we try to satisfy them but we default to prefs.
+ // Note that since constraints are from content and can literally be
+ // anything we put (rather generous) caps on them.
+ for (bool isHd : {false, true}) {
+ webrtc::CaptureCapability cap;
+ int32_t prefWidth = aPrefs.GetWidth(isHd);
+ int32_t prefHeight = aPrefs.GetHeight(isHd);
+
+ cap.width = c.mWidth.Get(prefWidth);
+ cap.width = std::max(0, std::min(cap.width, 7680));
+
+ cap.height = c.mHeight.Get(prefHeight);
+ cap.height = std::max(0, std::min(cap.height, 4320));
+
+ cap.maxFPS = c.mFrameRate.Get(aPrefs.mFPS);
+ cap.maxFPS = std::max(0, std::min(cap.maxFPS, 480));
+
+ if (cap.width != prefWidth) {
+ // Width was affected by constraints.
+ // We'll adjust the height too so the aspect ratio is retained.
+ cap.height = cap.width * prefHeight / prefWidth;
+ } else if (cap.height != prefHeight) {
+ // Height was affected by constraints but not width.
+ // We'll adjust the width too so the aspect ratio is retained.
+ cap.width = cap.height * prefWidth / prefHeight;
+ }
+
+ if (candidateSet.Contains(cap, CapabilityComparator())) {
+ continue;
+ }
+ LogCapability("Hardcoded capability", cap, 0);
+ candidateSet.AppendElement(cap);
+ }
+ }
+
+ // First, filter capabilities by required constraints (min, max, exact).
+
+ for (size_t i = 0; i < candidateSet.Length();) {
+ auto& candidate = candidateSet[i];
+ candidate.mDistance =
+ GetDistance(candidate.mCapability, aConstraints, aCalculate);
+ LogCapability("Capability", candidate.mCapability, candidate.mDistance);
+ if (candidate.mDistance == UINT32_MAX) {
+ candidateSet.RemoveElementAt(i);
+ } else {
+ ++i;
+ }
+ }
+
+ if (candidateSet.IsEmpty()) {
+ LOG("failed to find capability match from %zu choices",
+ candidateSet.Length());
+ return false;
+ }
+
+ // Filter further with all advanced constraints (that don't overconstrain).
+
+ for (const auto& cs : aConstraints.mAdvanced) {
+ nsTArray<CapabilityCandidate> rejects;
+ for (size_t i = 0; i < candidateSet.Length();) {
+ if (GetDistance(candidateSet[i].mCapability, cs, aCalculate) ==
+ UINT32_MAX) {
+ rejects.AppendElement(candidateSet[i]);
+ candidateSet.RemoveElementAt(i);
+ } else {
+ ++i;
+ }
+ }
+ if (!candidateSet.Length()) {
+ candidateSet.AppendElements(std::move(rejects));
+ }
+ }
+ MOZ_ASSERT(
+ candidateSet.Length(),
+ "advanced constraints filtering step can't reduce candidates to zero");
+
+ // Remaining algorithm is up to the UA.
+
+ TrimLessFitCandidates(candidateSet);
+
+ // Any remaining multiples all have the same distance. A common case of this
+ // occurs when no ideal is specified. Lean toward defaults.
+ uint32_t sameDistance = candidateSet[0].mDistance;
+ {
+ MediaTrackConstraintSet prefs;
+ prefs.mWidth.Construct().SetAsLong() = aPrefs.GetWidth();
+ prefs.mHeight.Construct().SetAsLong() = aPrefs.GetHeight();
+ prefs.mFrameRate.Construct().SetAsDouble() = aPrefs.mFPS;
+ NormalizedConstraintSet normPrefs(prefs, false);
+
+ for (auto& candidate : candidateSet) {
+ candidate.mDistance =
+ GetDistance(candidate.mCapability, normPrefs, aCalculate);
+ }
+ TrimLessFitCandidates(candidateSet);
+ }
+
+ aCapability = candidateSet[0].mCapability;
+
+ LogCapability("Chosen capability", aCapability, sameDistance);
+ return true;
+}
+
+void MediaEngineRemoteVideoSource::GetSettings(
+ MediaTrackSettings& aOutSettings) const {
+ aOutSettings = *mSettings;
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/MediaEngineRemoteVideoSource.h b/dom/media/webrtc/MediaEngineRemoteVideoSource.h
new file mode 100644
index 0000000000..7e4f852701
--- /dev/null
+++ b/dom/media/webrtc/MediaEngineRemoteVideoSource.h
@@ -0,0 +1,242 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MEDIAENGINE_REMOTE_VIDEO_SOURCE_H_
+#define MEDIAENGINE_REMOTE_VIDEO_SOURCE_H_
+
+#include "prcvar.h"
+#include "prthread.h"
+
+#include "mozilla/Mutex.h"
+#include "nsCOMPtr.h"
+#include "nsThreadUtils.h"
+#include "DOMMediaStream.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsComponentManagerUtils.h"
+
+// Avoid warnings about redefinition of WARN_UNUSED_RESULT
+#include "ipc/IPCMessageUtils.h"
+#include "VideoUtils.h"
+#include "MediaEngineSource.h"
+#include "VideoSegment.h"
+#include "AudioSegment.h"
+#include "MediaTrackGraph.h"
+
+#include "MediaEngineWrapper.h"
+#include "mozilla/dom/MediaStreamTrackBinding.h"
+
+// Camera Access via IPC
+#include "CamerasChild.h"
+
+#include "NullTransport.h"
+
+// WebRTC includes
+#include "common_video/include/video_frame_buffer_pool.h"
+#include "modules/video_capture/video_capture_defines.h"
+
+namespace webrtc {
+using CaptureCapability = VideoCaptureCapability;
+}
+
+namespace mozilla {
+
+// Fitness distance is defined in
+// https://w3c.github.io/mediacapture-main/getusermedia.html#dfn-selectsettings
+
+// The main difference of feasibility and fitness distance is that if the
+// constraint is required ('max', or 'exact'), and the settings dictionary's
+// value for the constraint does not satisfy the constraint, the fitness
+// distance is positive infinity. Given a continuous space of settings
+// dictionaries comprising all discrete combinations of dimension and frame-rate
+// related properties, the feasibility distance is still in keeping with the
+// constraints algorithm.
+enum DistanceCalculation { kFitness, kFeasibility };
+
+/**
+ * The WebRTC implementation of the MediaEngine interface.
+ */
+class MediaEngineRemoteVideoSource : public MediaEngineSource,
+ public camera::FrameRelay {
+ ~MediaEngineRemoteVideoSource();
+
+ struct CapabilityCandidate {
+ explicit CapabilityCandidate(webrtc::CaptureCapability aCapability,
+ uint32_t aDistance = 0)
+ : mCapability(aCapability), mDistance(aDistance) {}
+
+ const webrtc::CaptureCapability mCapability;
+ uint32_t mDistance;
+ };
+
+ class CapabilityComparator {
+ public:
+ bool Equals(const CapabilityCandidate& aCandidate,
+ const webrtc::CaptureCapability& aCapability) const {
+ return aCandidate.mCapability == aCapability;
+ }
+ };
+
+ bool ChooseCapability(const NormalizedConstraints& aConstraints,
+ const MediaEnginePrefs& aPrefs,
+ webrtc::CaptureCapability& aCapability,
+ const DistanceCalculation aCalculate);
+
+ uint32_t GetDistance(const webrtc::CaptureCapability& aCandidate,
+ const NormalizedConstraintSet& aConstraints,
+ const DistanceCalculation aCalculate) const;
+
+ uint32_t GetFitnessDistance(
+ const webrtc::CaptureCapability& aCandidate,
+ const NormalizedConstraintSet& aConstraints) const;
+
+ uint32_t GetFeasibilityDistance(
+ const webrtc::CaptureCapability& aCandidate,
+ const NormalizedConstraintSet& aConstraints) const;
+
+ static void TrimLessFitCandidates(nsTArray<CapabilityCandidate>& aSet);
+
+ public:
+ explicit MediaEngineRemoteVideoSource(const MediaDevice* aMediaDevice);
+
+ // ExternalRenderer
+ int DeliverFrame(uint8_t* aBuffer,
+ const camera::VideoFrameProperties& aProps) override;
+
+ // MediaEngineSource
+ nsresult Allocate(const dom::MediaTrackConstraints& aConstraints,
+ const MediaEnginePrefs& aPrefs, uint64_t aWindowID,
+ const char** aOutBadConstraint) override;
+ nsresult Deallocate() override;
+ void SetTrack(const RefPtr<MediaTrack>& aTrack,
+ const PrincipalHandle& aPrincipal) override;
+ nsresult Start() override;
+ nsresult Reconfigure(const dom::MediaTrackConstraints& aConstraints,
+ const MediaEnginePrefs& aPrefs,
+ const char** aOutBadConstraint) override;
+ nsresult FocusOnSelectedSource() override;
+ nsresult Stop() override;
+
+ uint32_t GetBestFitnessDistance(
+ const nsTArray<const NormalizedConstraintSet*>& aConstraintSets)
+ const override;
+ void GetSettings(dom::MediaTrackSettings& aOutSettings) const override;
+
+ RefPtr<GenericNonExclusivePromise> GetFirstFramePromise() const override {
+ return mFirstFramePromise;
+ }
+
+ const TrackingId& GetTrackingId() const override;
+
+ static camera::CaptureEngine CaptureEngine(dom::MediaSourceEnum aMediaSource);
+
+ private:
+ /**
+ * Returns the number of capabilities for the underlying device.
+ *
+ * Guaranteed to return at least one capability.
+ */
+ size_t NumCapabilities() const;
+
+ /**
+ * Returns the capability with index `aIndex` for our assigned device.
+ *
+ * It is an error to call this with `aIndex >= NumCapabilities()`.
+ *
+ * The lifetime of the returned capability is the same as for this source.
+ */
+ webrtc::CaptureCapability& GetCapability(size_t aIndex) const;
+
+ int mCaptureId = -1;
+ const camera::CaptureEngine mCapEngine; // source of media (cam, screen etc)
+
+ // A tracking id used to uniquely identify the source of video frames.
+ // Set under mMutex on the owning thread. Accessed under one of the two.
+ TrackingId mTrackingId;
+
+ // Mirror of mTrackingId on the frame-delivering thread (Cameras IPC).
+ Maybe<TrackingId> mFrameDeliveringTrackingId;
+
+ // mMutex protects certain members on 3 threads:
+ // MediaManager, Cameras IPC and MediaTrackGraph.
+ Mutex mMutex MOZ_UNANNOTATED;
+
+ // Current state of this source.
+ // Set under mMutex on the owning thread. Accessed under one of the two.
+ MediaEngineSourceState mState = kReleased;
+
+ // The source track that we feed video data to.
+ // Set under mMutex on the owning thread. Accessed under one of the two.
+ RefPtr<SourceMediaTrack> mTrack;
+
+ // The PrincipalHandle that gets attached to the frames we feed to mTrack.
+ // Set under mMutex on the owning thread. Accessed under one of the two.
+ PrincipalHandle mPrincipal = PRINCIPAL_HANDLE_NONE;
+
+ // Set in Start() and Deallocate() on the owning thread.
+ // Accessed in DeliverFrame() on the camera IPC thread, guaranteed to happen
+ // after Start() and before the end of Stop().
+ RefPtr<layers::ImageContainer> mImageContainer;
+
+ // A buffer pool used to manage the temporary buffer used when rescaling
+ // incoming images. Cameras IPC thread only.
+ webrtc::VideoFrameBufferPool mRescalingBufferPool;
+
+ // The intrinsic size of the latest captured image, so we can feed black
+ // images of the same size while stopped.
+ // Set under mMutex on the Cameras IPC thread. Accessed under one of the two.
+ gfx::IntSize mImageSize = gfx::IntSize(0, 0);
+
+ struct AtomicBool {
+ Atomic<bool> mValue;
+ };
+
+ // True when resolution settings have been updated from a real frame's
+ // resolution. Threadsafe.
+ // TODO: This can be removed in bug 1453269.
+ const RefPtr<media::Refcountable<AtomicBool>> mSettingsUpdatedByFrame;
+
+ // The current settings of this source.
+ // Note that these may be different from the settings of the underlying device
+ // since we scale frames to avoid fingerprinting.
+ // Members are main thread only.
+ const RefPtr<media::Refcountable<dom::MediaTrackSettings>> mSettings;
+ MozPromiseHolder<GenericNonExclusivePromise> mFirstFramePromiseHolder;
+ RefPtr<GenericNonExclusivePromise> mFirstFramePromise;
+
+ // The capability currently chosen by constraints of the user of this source.
+ // Set under mMutex on the owning thread. Accessed under one of the two.
+ webrtc::CaptureCapability mCapability;
+
+ /**
+ * Capabilities that we choose between when applying constraints.
+ *
+ * This allows for memoization of capabilities as they're requested from the
+ * parent process.
+ *
+ * This is mutable so that the const methods NumCapabilities() and
+ * GetCapability() can reset it. Owning thread only.
+ */
+ mutable nsTArray<UniquePtr<webrtc::CaptureCapability>> mCapabilities;
+
+ /**
+ * True if mCapabilities only contains hardcoded capabilities. This can happen
+ * if the underlying device is not reporting any capabilities. These can be
+ * affected by constraints, so they're evaluated in ChooseCapability() rather
+ * than GetCapability().
+ *
+ * This is mutable so that the const methods NumCapabilities() and
+ * GetCapability() can reset it. Owning thread only.
+ */
+ mutable bool mCapabilitiesAreHardcoded = false;
+
+ const RefPtr<const MediaDevice> mMediaDevice;
+ const nsCString mDeviceUUID;
+ Maybe<nsString> mFacingMode;
+};
+
+} // namespace mozilla
+
+#endif /* MEDIAENGINE_REMOTE_VIDEO_SOURCE_H_ */
diff --git a/dom/media/webrtc/MediaEngineSource.cpp b/dom/media/webrtc/MediaEngineSource.cpp
new file mode 100644
index 0000000000..d0e5e23ae7
--- /dev/null
+++ b/dom/media/webrtc/MediaEngineSource.cpp
@@ -0,0 +1,69 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaEngineSource.h"
+
+#include "mozilla/dom/MediaTrackSettingsBinding.h"
+
+namespace mozilla {
+
+using dom::MediaSourceEnum;
+using dom::MediaTrackSettings;
+
+// These need a definition somewhere because template
+// code is allowed to take their address, and they aren't
+// guaranteed to have one without this.
+const unsigned int MediaEngineSource::kMaxDeviceNameLength;
+const unsigned int MediaEngineSource::kMaxUniqueIdLength;
+
+/* static */
+bool MediaEngineSource::IsVideo(MediaSourceEnum aSource) {
+ switch (aSource) {
+ case MediaSourceEnum::Camera:
+ case MediaSourceEnum::Screen:
+ case MediaSourceEnum::Window:
+ case MediaSourceEnum::Browser:
+ return true;
+ case MediaSourceEnum::Microphone:
+ case MediaSourceEnum::AudioCapture:
+ case MediaSourceEnum::Other:
+ return false;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unknown type");
+ return false;
+ }
+}
+
+/* static */
+bool MediaEngineSource::IsAudio(MediaSourceEnum aSource) {
+ switch (aSource) {
+ case MediaSourceEnum::Microphone:
+ case MediaSourceEnum::AudioCapture:
+ return true;
+ case MediaSourceEnum::Camera:
+ case MediaSourceEnum::Screen:
+ case MediaSourceEnum::Window:
+ case MediaSourceEnum::Browser:
+ case MediaSourceEnum::Other:
+ return false;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unknown type");
+ return false;
+ }
+}
+
+bool MediaEngineSource::IsFake() const { return false; }
+
+nsresult MediaEngineSource::FocusOnSelectedSource() {
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+nsresult MediaEngineSource::TakePhoto(MediaEnginePhotoCallback* aCallback) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+MediaEngineSource::~MediaEngineSource() = default;
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/MediaEngineSource.h b/dom/media/webrtc/MediaEngineSource.h
new file mode 100644
index 0000000000..d32796b094
--- /dev/null
+++ b/dom/media/webrtc/MediaEngineSource.h
@@ -0,0 +1,255 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MediaEngineSource_h
+#define MediaEngineSource_h
+
+#include "MediaSegment.h"
+#include "MediaTrackConstraints.h"
+#include "mozilla/dom/MediaStreamTrackBinding.h"
+#include "mozilla/media/MediaUtils.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/ThreadSafeWeakPtr.h"
+#include "nsStringFwd.h"
+#include "PerformanceRecorder.h"
+
+namespace mozilla {
+
+namespace dom {
+class Blob;
+struct MediaTrackSettings;
+} // namespace dom
+
+namespace ipc {
+class PrincipalInfo;
+} // namespace ipc
+
+class MediaEnginePhotoCallback;
+class MediaEnginePrefs;
+class MediaTrack;
+
+/**
+ * Callback interface for TakePhoto(). Either PhotoComplete() or PhotoError()
+ * should be called.
+ */
+class MediaEnginePhotoCallback {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaEnginePhotoCallback)
+
+ // aBlob is the image captured by MediaEngineSource. It is
+ // called on main thread.
+ virtual nsresult PhotoComplete(already_AddRefed<dom::Blob> aBlob) = 0;
+
+ // It is called on main thread. aRv is the error code.
+ virtual nsresult PhotoError(nsresult aRv) = 0;
+
+ protected:
+ virtual ~MediaEnginePhotoCallback() = default;
+};
+
+/**
+ * Lifecycle state of MediaEngineSource.
+ */
+enum MediaEngineSourceState {
+ kAllocated, // Allocated, not yet started.
+ kStarted, // Previously allocated or stopped, then started.
+ kStopped, // Previously started, then stopped.
+ kReleased // Not allocated.
+};
+
+/**
+ * The pure interface of a MediaEngineSource.
+ *
+ * Most sources are helped by the defaults implemented in MediaEngineSource.
+ */
+class MediaEngineSourceInterface {
+ public:
+ /**
+ * Return true if this is a fake source. I.e., if it is generating media
+ * itself rather than being an interface to underlying hardware.
+ */
+ virtual bool IsFake() const = 0;
+
+ /**
+ * Override w/a promise if source has frames, in order to potentially allow
+ * deferring success of source acquisition until first frame has arrived.
+ */
+ virtual RefPtr<GenericNonExclusivePromise> GetFirstFramePromise() const {
+ return nullptr;
+ }
+
+ /**
+ * Get an id uniquely identifying the source of video frames that this
+ * MediaEngineSource represents. This can be used in profiler markers to
+ * separate markers from different sources into different lanes.
+ */
+ virtual const TrackingId& GetTrackingId() const = 0;
+
+ /**
+ * Called by MediaEngine to allocate an instance of this source.
+ */
+ virtual nsresult Allocate(const dom::MediaTrackConstraints& aConstraints,
+ const MediaEnginePrefs& aPrefs, uint64_t aWindowID,
+ const char** aOutBadConstraint) = 0;
+
+ /**
+ * Called by MediaEngine when a MediaTrack has been provided for the source to
+ * feed data to.
+ *
+ * This must be called before Start.
+ */
+ virtual void SetTrack(const RefPtr<MediaTrack>& aTrack,
+ const PrincipalHandle& aPrincipal) = 0;
+
+ /**
+ * Called by MediaEngine to start feeding data to the track.
+ *
+ * NB: Audio sources handle the enabling of pulling themselves.
+ */
+ virtual nsresult Start() = 0;
+
+ /**
+ * This brings focus to the selected source, e.g. to bring a captured window
+ * to the front.
+ *
+ * We return one of the following:
+ * NS_OK - Success.
+ * NS_ERROR_NOT_AVAILABLE - For backends where focusing does not make sense.
+ * NS_ERROR_NOT_IMPLEMENTED - For backends where focusing makes sense, but
+ * is not yet implemented.
+ * NS_ERROR_FAILURE - Failures reported from underlying code.
+ */
+ virtual nsresult FocusOnSelectedSource() = 0;
+
+ /**
+ * Applies new constraints to the capability selection for the underlying
+ * device.
+ *
+ * Should the constraints lead to choosing a new capability while the device
+ * is actively being captured, the device will restart using the new
+ * capability.
+ *
+ * We return one of the following:
+ * NS_OK - Successful reconfigure.
+ * NS_ERROR_INVALID_ARG - Couldn't find a capability fitting aConstraints.
+ * See aBadConstraint for details.
+ * NS_ERROR_UNEXPECTED - Reconfiguring the underlying device failed
+ * unexpectedly. This leaves the device in a stopped
+ * state.
+ */
+ virtual nsresult Reconfigure(const dom::MediaTrackConstraints& aConstraints,
+ const MediaEnginePrefs& aPrefs,
+ const char** aOutBadConstraint) = 0;
+
+ /**
+ * Called by MediaEngine to stop feeding data to the track.
+ *
+ * Double-stopping is allowed and will return NS_OK. This is necessary
+ * sometimes during shutdown.
+ *
+ * NB: Audio sources handle the disabling of pulling themselves.
+ */
+ virtual nsresult Stop() = 0;
+
+ /**
+ * Called by MediaEngine to deallocate an underlying device.
+ */
+ virtual nsresult Deallocate() = 0;
+
+ /**
+ * If implementation of MediaEngineSource supports TakePhoto(), the picture
+ * should be returned via aCallback object. Otherwise, it returns
+ * NS_ERROR_NOT_IMPLEMENTED.
+ */
+ virtual nsresult TakePhoto(MediaEnginePhotoCallback* aCallback) = 0;
+
+ /**
+ * GetBestFitnessDistance returns the best distance the capture device can
+ * offer as a whole, given an accumulated number of ConstraintSets. Ideal
+ * values are considered in the first ConstraintSet only. Plain values are
+ * treated as Ideal in the first ConstraintSet. Plain values are treated as
+ * Exact in subsequent ConstraintSets. Infinity = UINT32_MAX e.g. device
+ * cannot satisfy accumulated ConstraintSets. A finite result may be used to
+ * calculate this device's ranking as a choice.
+ */
+ virtual uint32_t GetBestFitnessDistance(
+ const nsTArray<const NormalizedConstraintSet*>& aConstraintSets)
+ const = 0;
+
+ /**
+ * Returns the current settings of the underlying device.
+ *
+ * Note that this might not be the settings of the underlying hardware.
+ * In case of a camera where we intervene and scale frames to avoid
+ * leaking information from other documents than the current one,
+ * GetSettings() will return the scaled resolution. I.e., the
+ * device settings as seen by js.
+ */
+ virtual void GetSettings(dom::MediaTrackSettings& aOutSettings) const = 0;
+};
+
+/**
+ * Abstract base class for MediaEngineSources.
+ *
+ * Implements defaults for some common MediaEngineSourceInterface methods below.
+ * Also implements RefPtr support and an owning-thread model for thread safety
+ * checks in subclasses.
+ */
+class MediaEngineSource : public MediaEngineSourceInterface {
+ public:
+ // code inside webrtc.org assumes these sizes; don't use anything smaller
+ // without verifying it's ok
+ static const unsigned int kMaxDeviceNameLength = 128;
+ static const unsigned int kMaxUniqueIdLength = 256;
+
+ /**
+ * Returns true if the given source type is for video, false otherwise.
+ * Only call with real types.
+ */
+ static bool IsVideo(dom::MediaSourceEnum aSource);
+
+ /**
+ * Returns true if the given source type is for audio, false otherwise.
+ * Only call with real types.
+ */
+ static bool IsAudio(dom::MediaSourceEnum aSource);
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaEngineSource)
+ NS_DECL_OWNINGEVENTTARGET
+
+ void AssertIsOnOwningThread() const {
+ NS_ASSERT_OWNINGTHREAD(MediaEngineSource);
+ }
+
+ const TrackingId& GetTrackingId() const override {
+ static auto notImplementedId = TrackingId();
+ return notImplementedId;
+ }
+
+ // Not fake by default.
+ bool IsFake() const override;
+
+ // Returns NS_ERROR_NOT_AVAILABLE by default.
+ nsresult FocusOnSelectedSource() override;
+
+ // TakePhoto returns NS_ERROR_NOT_IMPLEMENTED by default,
+ // to tell the caller to fallback to other methods.
+ nsresult TakePhoto(MediaEnginePhotoCallback* aCallback) override;
+
+ // Returns a default distance of 0 for devices that don't have capabilities.
+ uint32_t GetBestFitnessDistance(
+ const nsTArray<const NormalizedConstraintSet*>& aConstraintSets)
+ const override {
+ return 0;
+ }
+
+ protected:
+ virtual ~MediaEngineSource();
+};
+
+} // namespace mozilla
+
+#endif /* MediaEngineSource_h */
diff --git a/dom/media/webrtc/MediaEngineWebRTC.cpp b/dom/media/webrtc/MediaEngineWebRTC.cpp
new file mode 100644
index 0000000000..cf638497a9
--- /dev/null
+++ b/dom/media/webrtc/MediaEngineWebRTC.cpp
@@ -0,0 +1,299 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaEngineWebRTC.h"
+
+#include "CamerasChild.h"
+#include "MediaEngineRemoteVideoSource.h"
+#include "MediaEngineWebRTCAudio.h"
+#include "MediaManager.h"
+#include "mozilla/Logging.h"
+
+// Pipewire detection support
+#if defined(WEBRTC_USE_PIPEWIRE)
+# include "mozilla/StaticPrefs_media.h"
+# include "modules/desktop_capture/desktop_capturer.h"
+#endif
+
+#define FAKE_ONDEVICECHANGE_EVENT_PERIOD_IN_MS 500
+
+static mozilla::LazyLogModule sGetUserMediaLog("GetUserMedia");
+#undef LOG
+#define LOG(args) MOZ_LOG(sGetUserMediaLog, mozilla::LogLevel::Debug, args)
+
+namespace mozilla {
+
+using AudioDeviceSet = CubebDeviceEnumerator::AudioDeviceSet;
+using camera::CamerasChild;
+using camera::GetChildAndCall;
+using dom::MediaSourceEnum;
+
+CubebDeviceEnumerator* GetEnumerator() {
+ return CubebDeviceEnumerator::GetInstance();
+}
+
+MediaEngineWebRTC::MediaEngineWebRTC() {
+ AssertIsOnOwningThread();
+
+ GetChildAndCall(
+ &CamerasChild::ConnectDeviceListChangeListener<MediaEngineWebRTC>,
+ &mCameraListChangeListener, AbstractThread::MainThread(), this,
+ &MediaEngineWebRTC::DeviceListChanged);
+ mMicrophoneListChangeListener =
+ GetEnumerator()->OnAudioInputDeviceListChange().Connect(
+ AbstractThread::MainThread(), this,
+ &MediaEngineWebRTC::DeviceListChanged);
+ mSpeakerListChangeListener =
+ GetEnumerator()->OnAudioOutputDeviceListChange().Connect(
+ AbstractThread::MainThread(), this,
+ &MediaEngineWebRTC::DeviceListChanged);
+}
+
+void MediaEngineWebRTC::EnumerateVideoDevices(
+ MediaSourceEnum aMediaSource, nsTArray<RefPtr<MediaDevice>>* aDevices) {
+ AssertIsOnOwningThread();
+ // flag sources with cross-origin exploit potential
+ bool scaryKind = (aMediaSource == MediaSourceEnum::Screen ||
+ aMediaSource == MediaSourceEnum::Browser);
+#if defined(WEBRTC_USE_PIPEWIRE)
+ bool canRequestOsLevelPrompt =
+ mozilla::StaticPrefs::media_webrtc_capture_allow_pipewire() &&
+ webrtc::DesktopCapturer::IsRunningUnderWayland() &&
+ (aMediaSource == MediaSourceEnum::Application ||
+ aMediaSource == MediaSourceEnum::Screen ||
+ aMediaSource == MediaSourceEnum::Window);
+#else
+ bool canRequestOsLevelPrompt = false;
+#endif
+ /*
+ * We still enumerate every time, in case a new device was plugged in since
+ * the last call. TODO: Verify that WebRTC actually does deal with hotplugging
+ * new devices (with or without new engine creation) and accordingly adjust.
+ * Enumeration is not neccessary if GIPS reports the same set of devices
+ * for a given instance of the engine.
+ */
+ int num;
+#if defined(_ARM64_) && defined(XP_WIN)
+ // There are problems with using DirectShow on versions of Windows before
+ // 19H1 on arm64. This disables the camera on older versions of Windows.
+ if (aMediaSource == MediaSourceEnum::Camera) {
+ typedef ULONG (*RtlGetVersionFn)(LPOSVERSIONINFOEXW);
+ RtlGetVersionFn RtlGetVersion;
+ RtlGetVersion = (RtlGetVersionFn)GetProcAddress(GetModuleHandleA("ntdll"),
+ "RtlGetVersion");
+ if (RtlGetVersion) {
+ OSVERSIONINFOEXW info;
+ info.dwOSVersionInfoSize = sizeof(info);
+ RtlGetVersion(&info);
+ // 19H1 is 18346
+ if (info.dwBuildNumber < 18346) {
+ return;
+ }
+ }
+ }
+#endif
+ camera::CaptureEngine capEngine =
+ MediaEngineRemoteVideoSource::CaptureEngine(aMediaSource);
+ num = GetChildAndCall(&CamerasChild::NumberOfCaptureDevices, capEngine);
+
+ for (int i = 0; i < num; i++) {
+ char deviceName[MediaEngineSource::kMaxDeviceNameLength];
+ char uniqueId[MediaEngineSource::kMaxUniqueIdLength];
+ bool scarySource = false;
+
+ // paranoia
+ deviceName[0] = '\0';
+ uniqueId[0] = '\0';
+ int error;
+
+ error = GetChildAndCall(&CamerasChild::GetCaptureDevice, capEngine, i,
+ deviceName, sizeof(deviceName), uniqueId,
+ sizeof(uniqueId), &scarySource);
+ if (error) {
+ LOG(("camera:GetCaptureDevice: Failed %d", error));
+ continue;
+ }
+#ifdef DEBUG
+ LOG((" Capture Device Index %d, Name %s", i, deviceName));
+
+ webrtc::CaptureCapability cap;
+ int numCaps = GetChildAndCall(&CamerasChild::NumberOfCapabilities,
+ capEngine, uniqueId);
+ LOG(("Number of Capabilities %d", numCaps));
+ for (int j = 0; j < numCaps; j++) {
+ if (GetChildAndCall(&CamerasChild::GetCaptureCapability, capEngine,
+ uniqueId, j, &cap) != 0) {
+ break;
+ }
+ LOG(("type=%d width=%d height=%d maxFPS=%d",
+ static_cast<int>(cap.videoType), cap.width, cap.height, cap.maxFPS));
+ }
+#endif
+
+ NS_ConvertUTF8toUTF16 name(deviceName);
+ NS_ConvertUTF8toUTF16 uuid(uniqueId);
+ // The remote video backend doesn't implement group id. We return the
+ // device name and higher layers will correlate this with the name of
+ // audio devices.
+ aDevices->EmplaceBack(new MediaDevice(
+ this, aMediaSource, name, uuid, uuid,
+ MediaDevice::IsScary(scaryKind || scarySource),
+ canRequestOsLevelPrompt ? MediaDevice::OsPromptable::Yes
+ : MediaDevice::OsPromptable::No));
+ }
+}
+
+void MediaEngineWebRTC::EnumerateMicrophoneDevices(
+ nsTArray<RefPtr<MediaDevice>>* aDevices) {
+ AssertIsOnOwningThread();
+
+ RefPtr<const AudioDeviceSet> devices =
+ GetEnumerator()->EnumerateAudioInputDevices();
+
+ DebugOnly<bool> foundPreferredDevice = false;
+
+ for (const auto& deviceInfo : *devices) {
+#ifndef ANDROID
+ MOZ_ASSERT(deviceInfo->DeviceID());
+#endif
+ LOG(("Cubeb device: type 0x%x, state 0x%x, name %s, id %p",
+ deviceInfo->Type(), deviceInfo->State(),
+ NS_ConvertUTF16toUTF8(deviceInfo->Name()).get(),
+ deviceInfo->DeviceID()));
+
+ if (deviceInfo->State() == CUBEB_DEVICE_STATE_ENABLED) {
+ MOZ_ASSERT(deviceInfo->Type() == CUBEB_DEVICE_TYPE_INPUT);
+ // Lie and provide the name as UUID
+ RefPtr device = new MediaDevice(this, deviceInfo, deviceInfo->Name());
+ if (deviceInfo->Preferred()) {
+#ifdef DEBUG
+ if (!foundPreferredDevice) {
+ foundPreferredDevice = true;
+ } else {
+ // This is possible on windows, there is a default communication
+ // device, and a default device:
+ // See https://bugzilla.mozilla.org/show_bug.cgi?id=1542739
+# ifndef XP_WIN
+ MOZ_ASSERT(!foundPreferredDevice,
+ "Found more than one preferred audio input device"
+ "while enumerating");
+# endif
+ }
+#endif
+ aDevices->InsertElementAt(0, std::move(device));
+ } else {
+ aDevices->AppendElement(std::move(device));
+ }
+ }
+ }
+}
+
+void MediaEngineWebRTC::EnumerateSpeakerDevices(
+ nsTArray<RefPtr<MediaDevice>>* aDevices) {
+ AssertIsOnOwningThread();
+
+ RefPtr<const AudioDeviceSet> devices =
+ GetEnumerator()->EnumerateAudioOutputDevices();
+
+#ifndef XP_WIN
+ DebugOnly<bool> preferredDeviceFound = false;
+#endif
+ for (const auto& deviceInfo : *devices) {
+ LOG(("Cubeb device: type 0x%x, state 0x%x, name %s, id %p",
+ deviceInfo->Type(), deviceInfo->State(),
+ NS_ConvertUTF16toUTF8(deviceInfo->Name()).get(),
+ deviceInfo->DeviceID()));
+ if (deviceInfo->State() == CUBEB_DEVICE_STATE_ENABLED) {
+ MOZ_ASSERT(deviceInfo->Type() == CUBEB_DEVICE_TYPE_OUTPUT);
+ nsString uuid(deviceInfo->Name());
+ // If, for example, input and output are in the same device, uuid
+ // would be the same for both which ends up to create the same
+ // deviceIDs (in JS).
+ uuid.Append(u"_Speaker"_ns);
+ RefPtr device = new MediaDevice(this, deviceInfo, uuid);
+ if (deviceInfo->Preferred()) {
+ // In windows is possible to have more than one preferred device
+#if defined(DEBUG) && !defined(XP_WIN)
+ MOZ_ASSERT(!preferredDeviceFound, "More than one preferred device");
+ preferredDeviceFound = true;
+#endif
+ aDevices->InsertElementAt(0, std::move(device));
+ } else {
+ aDevices->AppendElement(std::move(device));
+ }
+ }
+ }
+}
+
+void MediaEngineWebRTC::EnumerateDevices(
+ MediaSourceEnum aMediaSource, MediaSinkEnum aMediaSink,
+ nsTArray<RefPtr<MediaDevice>>* aDevices) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aMediaSource != MediaSourceEnum::Other ||
+ aMediaSink != MediaSinkEnum::Other);
+ if (MediaEngineSource::IsVideo(aMediaSource)) {
+ switch (aMediaSource) {
+ case MediaSourceEnum::Window:
+ // Since the mediaSource constraint is deprecated, treat the Window
+ // value as a request for getDisplayMedia-equivalent sharing: Combine
+ // window and fullscreen into a single list of choices. The other values
+ // are still useful for testing.
+ EnumerateVideoDevices(MediaSourceEnum::Window, aDevices);
+ EnumerateVideoDevices(MediaSourceEnum::Browser, aDevices);
+ EnumerateVideoDevices(MediaSourceEnum::Screen, aDevices);
+ break;
+ case MediaSourceEnum::Screen:
+ case MediaSourceEnum::Browser:
+ case MediaSourceEnum::Camera:
+ EnumerateVideoDevices(aMediaSource, aDevices);
+ break;
+ default:
+ MOZ_CRASH("No valid video source");
+ break;
+ }
+ } else if (aMediaSource == MediaSourceEnum::AudioCapture) {
+ aDevices->EmplaceBack(new MediaDevice(
+ this, aMediaSource, u"AudioCapture"_ns,
+ MediaEngineWebRTCAudioCaptureSource::GetUUID(),
+ MediaEngineWebRTCAudioCaptureSource::GetGroupId(),
+ MediaDevice::IsScary::No, MediaDevice::OsPromptable::No));
+ } else if (aMediaSource == MediaSourceEnum::Microphone) {
+ EnumerateMicrophoneDevices(aDevices);
+ }
+
+ if (aMediaSink == MediaSinkEnum::Speaker) {
+ EnumerateSpeakerDevices(aDevices);
+ }
+}
+
+RefPtr<MediaEngineSource> MediaEngineWebRTC::CreateSource(
+ const MediaDevice* aMediaDevice) {
+ MOZ_ASSERT(aMediaDevice->mEngine == this);
+ if (MediaEngineSource::IsVideo(aMediaDevice->mMediaSource)) {
+ return new MediaEngineRemoteVideoSource(aMediaDevice);
+ }
+ switch (aMediaDevice->mMediaSource) {
+ case MediaSourceEnum::AudioCapture:
+ return new MediaEngineWebRTCAudioCaptureSource(aMediaDevice);
+ case MediaSourceEnum::Microphone:
+ return new MediaEngineWebRTCMicrophoneSource(aMediaDevice);
+ default:
+ MOZ_CRASH("Unsupported source type");
+ return nullptr;
+ }
+}
+
+void MediaEngineWebRTC::Shutdown() {
+ AssertIsOnOwningThread();
+ mCameraListChangeListener.DisconnectIfExists();
+ mMicrophoneListChangeListener.DisconnectIfExists();
+ mSpeakerListChangeListener.DisconnectIfExists();
+
+ LOG(("%s", __FUNCTION__));
+ mozilla::camera::Shutdown();
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/MediaEngineWebRTC.h b/dom/media/webrtc/MediaEngineWebRTC.h
new file mode 100644
index 0000000000..918558dfab
--- /dev/null
+++ b/dom/media/webrtc/MediaEngineWebRTC.h
@@ -0,0 +1,53 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MEDIAENGINEWEBRTC_H_
+#define MEDIAENGINEWEBRTC_H_
+
+#include "MediaEngine.h"
+#include "MediaEventSource.h"
+#include "MediaEngineSource.h"
+#include "nsTArray.h"
+#include "CubebDeviceEnumerator.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/dom/MediaStreamTrackBinding.h"
+
+namespace mozilla {
+
+class MediaEngineWebRTC : public MediaEngine {
+ public:
+ MediaEngineWebRTC();
+
+ // Clients should ensure to clean-up sources video/audio sources
+ // before invoking Shutdown on this class.
+ void Shutdown() override;
+
+ void EnumerateDevices(dom::MediaSourceEnum, MediaSinkEnum,
+ nsTArray<RefPtr<MediaDevice>>*) override;
+ RefPtr<MediaEngineSource> CreateSource(const MediaDevice* aDevice) override;
+
+ MediaEventSource<void>& DeviceListChangeEvent() override {
+ return mDeviceListChangeEvent;
+ }
+ bool IsFake() const override { return false; }
+
+ private:
+ ~MediaEngineWebRTC() = default;
+ void EnumerateVideoDevices(dom::MediaSourceEnum,
+ nsTArray<RefPtr<MediaDevice>>*);
+ void EnumerateMicrophoneDevices(nsTArray<RefPtr<MediaDevice>>*);
+ void EnumerateSpeakerDevices(nsTArray<RefPtr<MediaDevice>>*);
+
+ void DeviceListChanged() { mDeviceListChangeEvent.Notify(); }
+
+ MediaEventListener mCameraListChangeListener;
+ MediaEventListener mMicrophoneListChangeListener;
+ MediaEventListener mSpeakerListChangeListener;
+ MediaEventProducer<void> mDeviceListChangeEvent;
+};
+
+} // namespace mozilla
+
+#endif /* NSMEDIAENGINEWEBRTC_H_ */
diff --git a/dom/media/webrtc/MediaEngineWebRTCAudio.cpp b/dom/media/webrtc/MediaEngineWebRTCAudio.cpp
new file mode 100644
index 0000000000..64ed88c625
--- /dev/null
+++ b/dom/media/webrtc/MediaEngineWebRTCAudio.cpp
@@ -0,0 +1,1329 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaEngineWebRTCAudio.h"
+
+#include <stdio.h>
+#include <algorithm>
+
+#include "AudioConverter.h"
+#include "MediaManager.h"
+#include "MediaTrackGraphImpl.h"
+#include "MediaTrackConstraints.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/ErrorNames.h"
+#include "nsIDUtils.h"
+#include "transport/runnable_utils.h"
+#include "Tracing.h"
+#include "mozilla/Sprintf.h"
+#include "mozilla/Logging.h"
+
+#include "common_audio/include/audio_util.h"
+#include "modules/audio_processing/include/audio_processing.h"
+
+using namespace webrtc;
+
+// These are restrictions from the webrtc.org code
+#define MAX_CHANNELS 2
+#define MONO 1
+#define MAX_SAMPLING_FREQ 48000 // Hz - multiple of 100
+
+namespace mozilla {
+
+using dom::MediaSourceEnum;
+
+extern LazyLogModule gMediaManagerLog;
+#define LOG(...) MOZ_LOG(gMediaManagerLog, LogLevel::Debug, (__VA_ARGS__))
+#define LOG_FRAME(...) \
+ MOZ_LOG(gMediaManagerLog, LogLevel::Verbose, (__VA_ARGS__))
+#define LOG_ERROR(...) MOZ_LOG(gMediaManagerLog, LogLevel::Error, (__VA_ARGS__))
+
+/**
+ * WebRTC Microphone MediaEngineSource.
+ */
+
+MediaEngineWebRTCMicrophoneSource::MediaEngineWebRTCMicrophoneSource(
+ const MediaDevice* aMediaDevice)
+ : mPrincipal(PRINCIPAL_HANDLE_NONE),
+ mDeviceInfo(aMediaDevice->mAudioDeviceInfo),
+ mDeviceMaxChannelCount(mDeviceInfo->MaxChannels()),
+ mSettings(new nsMainThreadPtrHolder<
+ media::Refcountable<dom::MediaTrackSettings>>(
+ "MediaEngineWebRTCMicrophoneSource::mSettings",
+ new media::Refcountable<dom::MediaTrackSettings>(),
+ // Non-strict means it won't assert main thread for us.
+ // It would be great if it did but we're already on the media thread.
+ /* aStrict = */ false)) {
+ MOZ_ASSERT(aMediaDevice->mMediaSource == MediaSourceEnum::Microphone);
+#ifndef ANDROID
+ MOZ_ASSERT(mDeviceInfo->DeviceID());
+#endif
+
+ // We'll init lazily as needed
+ mSettings->mEchoCancellation.Construct(0);
+ mSettings->mAutoGainControl.Construct(0);
+ mSettings->mNoiseSuppression.Construct(0);
+ mSettings->mChannelCount.Construct(0);
+
+ mState = kReleased;
+}
+
+nsresult MediaEngineWebRTCMicrophoneSource::EvaluateSettings(
+ const NormalizedConstraints& aConstraintsUpdate,
+ const MediaEnginePrefs& aInPrefs, MediaEnginePrefs* aOutPrefs,
+ const char** aOutBadConstraint) {
+ AssertIsOnOwningThread();
+
+ FlattenedConstraints c(aConstraintsUpdate);
+ MediaEnginePrefs prefs = aInPrefs;
+
+ prefs.mAecOn = c.mEchoCancellation.Get(aInPrefs.mAecOn);
+ prefs.mAgcOn = c.mAutoGainControl.Get(aInPrefs.mAgcOn && prefs.mAecOn);
+ prefs.mNoiseOn = c.mNoiseSuppression.Get(aInPrefs.mNoiseOn && prefs.mAecOn);
+
+ // Determine an actual channel count to use for this source. Three factors at
+ // play here: the device capabilities, the constraints passed in by content,
+ // and a pref that can force things (for testing)
+ int32_t maxChannels = static_cast<int32_t>(mDeviceInfo->MaxChannels());
+
+ // First, check channelCount violation wrt constraints. This fails in case of
+ // error.
+ if (c.mChannelCount.mMin > maxChannels) {
+ *aOutBadConstraint = "channelCount";
+ return NS_ERROR_FAILURE;
+ }
+ // A pref can force the channel count to use. If the pref has a value of zero
+ // or lower, it has no effect.
+ if (aInPrefs.mChannels <= 0) {
+ prefs.mChannels = maxChannels;
+ }
+
+ // Get the number of channels asked for by content, and clamp it between the
+ // pref and the maximum number of channels that the device supports.
+ prefs.mChannels = c.mChannelCount.Get(std::min(prefs.mChannels, maxChannels));
+ prefs.mChannels = std::max(1, std::min(prefs.mChannels, maxChannels));
+
+ LOG("Audio config: agc: %d, noise: %d, channels: %d",
+ prefs.mAgcOn ? prefs.mAgc : -1, prefs.mNoiseOn ? prefs.mNoise : -1,
+ prefs.mChannels);
+
+ *aOutPrefs = prefs;
+
+ return NS_OK;
+}
+
+nsresult MediaEngineWebRTCMicrophoneSource::Reconfigure(
+ const dom::MediaTrackConstraints& aConstraints,
+ const MediaEnginePrefs& aPrefs, const char** aOutBadConstraint) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(mTrack);
+
+ LOG("Mic source %p Reconfigure ", this);
+
+ NormalizedConstraints constraints(aConstraints);
+ MediaEnginePrefs outputPrefs;
+ nsresult rv =
+ EvaluateSettings(constraints, aPrefs, &outputPrefs, aOutBadConstraint);
+ if (NS_FAILED(rv)) {
+ if (aOutBadConstraint) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsAutoCString name;
+ GetErrorName(rv, name);
+ LOG("Mic source %p Reconfigure() failed unexpectedly. rv=%s", this,
+ name.Data());
+ Stop();
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ ApplySettings(outputPrefs);
+
+ mCurrentPrefs = outputPrefs;
+
+ return NS_OK;
+}
+
+void MediaEngineWebRTCMicrophoneSource::ApplySettings(
+ const MediaEnginePrefs& aPrefs) {
+ AssertIsOnOwningThread();
+
+ TRACE("ApplySettings");
+ MOZ_ASSERT(
+ mTrack,
+ "ApplySetting is to be called only after SetTrack has been called");
+
+ mAudioProcessingConfig.pipeline.multi_channel_render = true;
+ mAudioProcessingConfig.pipeline.multi_channel_capture = true;
+
+ mAudioProcessingConfig.echo_canceller.enabled = aPrefs.mAecOn;
+ mAudioProcessingConfig.echo_canceller.mobile_mode = aPrefs.mUseAecMobile;
+
+ if ((mAudioProcessingConfig.gain_controller1.enabled =
+ aPrefs.mAgcOn && !aPrefs.mAgc2Forced)) {
+ auto mode = static_cast<AudioProcessing::Config::GainController1::Mode>(
+ aPrefs.mAgc);
+ if (mode != AudioProcessing::Config::GainController1::kAdaptiveAnalog &&
+ mode != AudioProcessing::Config::GainController1::kAdaptiveDigital &&
+ mode != AudioProcessing::Config::GainController1::kFixedDigital) {
+ LOG_ERROR("AudioInputProcessing %p Attempt to set invalid AGC mode %d",
+ mInputProcessing.get(), static_cast<int>(mode));
+ mode = AudioProcessing::Config::GainController1::kAdaptiveDigital;
+ }
+#if defined(WEBRTC_IOS) || defined(ATA) || defined(WEBRTC_ANDROID)
+ if (mode == AudioProcessing::Config::GainController1::kAdaptiveAnalog) {
+ LOG_ERROR(
+ "AudioInputProcessing %p Invalid AGC mode kAdaptiveAnalog on "
+ "mobile",
+ mInputProcessing.get());
+ MOZ_ASSERT_UNREACHABLE(
+ "Bad pref set in all.js or in about:config"
+ " for the auto gain, on mobile.");
+ mode = AudioProcessing::Config::GainController1::kFixedDigital;
+ }
+#endif
+ mAudioProcessingConfig.gain_controller1.mode = mode;
+ }
+ mAudioProcessingConfig.gain_controller2.enabled =
+ mAudioProcessingConfig.gain_controller2.adaptive_digital.enabled =
+ aPrefs.mAgcOn && aPrefs.mAgc2Forced;
+
+ if ((mAudioProcessingConfig.noise_suppression.enabled = aPrefs.mNoiseOn)) {
+ auto level = static_cast<AudioProcessing::Config::NoiseSuppression::Level>(
+ aPrefs.mNoise);
+ if (level != AudioProcessing::Config::NoiseSuppression::kLow &&
+ level != AudioProcessing::Config::NoiseSuppression::kModerate &&
+ level != AudioProcessing::Config::NoiseSuppression::kHigh &&
+ level != AudioProcessing::Config::NoiseSuppression::kVeryHigh) {
+ LOG_ERROR(
+ "AudioInputProcessing %p Attempt to set invalid noise suppression "
+ "level %d",
+ mInputProcessing.get(), static_cast<int>(level));
+
+ level = AudioProcessing::Config::NoiseSuppression::kModerate;
+ }
+ mAudioProcessingConfig.noise_suppression.level = level;
+ }
+
+ mAudioProcessingConfig.transient_suppression.enabled = aPrefs.mTransientOn;
+
+ mAudioProcessingConfig.high_pass_filter.enabled = aPrefs.mHPFOn;
+
+ // See https://bugs.chromium.org/p/webrtc/issues/detail?id=11539 for more
+ // info. Our pref defaults to false, and if this is truly as unhelpful
+ // as the upstream bug claim, we could delete the pref that drive this:
+ // media.getusermedia.residual_echo_enabled. See Bug 1779498.
+ // mAudioProcessingConfig.residual_echo_detector.enabled =
+ // aPrefs.mResidualEchoOn;
+
+ RefPtr<MediaEngineWebRTCMicrophoneSource> that = this;
+ CubebUtils::AudioDeviceID deviceID = mDeviceInfo->DeviceID();
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ __func__, [this, that, deviceID, track = mTrack, prefs = aPrefs,
+ audioProcessingConfig = mAudioProcessingConfig] {
+ mSettings->mEchoCancellation.Value() = prefs.mAecOn;
+ mSettings->mAutoGainControl.Value() = prefs.mAgcOn;
+ mSettings->mNoiseSuppression.Value() = prefs.mNoiseOn;
+ mSettings->mChannelCount.Value() = prefs.mChannels;
+
+ class Message : public ControlMessage {
+ CubebUtils::AudioDeviceID mDeviceID;
+ const RefPtr<AudioInputProcessing> mInputProcessing;
+ const AudioProcessing::Config mAudioProcessingConfig;
+ const bool mPassThrough;
+ const uint32_t mRequestedInputChannelCount;
+
+ public:
+ Message(MediaTrack* aTrack, CubebUtils::AudioDeviceID aDeviceID,
+ AudioInputProcessing* aInputProcessing,
+ const AudioProcessing::Config& aAudioProcessingConfig,
+ bool aPassThrough, uint32_t aRequestedInputChannelCount)
+ : ControlMessage(aTrack),
+ mDeviceID(aDeviceID),
+ mInputProcessing(aInputProcessing),
+ mAudioProcessingConfig(aAudioProcessingConfig),
+ mPassThrough(aPassThrough),
+ mRequestedInputChannelCount(aRequestedInputChannelCount) {}
+
+ void Run() override {
+ mInputProcessing->ApplyConfig(mTrack->GraphImpl(),
+ mAudioProcessingConfig);
+ {
+ TRACE("SetRequestedInputChannelCount");
+ mInputProcessing->SetRequestedInputChannelCount(
+ mTrack->GraphImpl(), mDeviceID, mRequestedInputChannelCount);
+ }
+ {
+ TRACE("SetPassThrough")
+ mInputProcessing->SetPassThrough(mTrack->GraphImpl(),
+ mPassThrough);
+ }
+ }
+ };
+
+ // The high-pass filter is not taken into account when activating the
+ // pass through, since it's not controllable from content.
+ bool passThrough = !(prefs.mAecOn || prefs.mAgcOn || prefs.mNoiseOn);
+
+ if (track->IsDestroyed()) {
+ return;
+ }
+ track->GraphImpl()->AppendMessage(MakeUnique<Message>(
+ track, deviceID, mInputProcessing, audioProcessingConfig,
+ passThrough, prefs.mChannels));
+ }));
+}
+
+nsresult MediaEngineWebRTCMicrophoneSource::Allocate(
+ const dom::MediaTrackConstraints& aConstraints,
+ const MediaEnginePrefs& aPrefs, uint64_t aWindowID,
+ const char** aOutBadConstraint) {
+ AssertIsOnOwningThread();
+
+ mState = kAllocated;
+
+ NormalizedConstraints normalized(aConstraints);
+ MediaEnginePrefs outputPrefs;
+ nsresult rv =
+ EvaluateSettings(normalized, aPrefs, &outputPrefs, aOutBadConstraint);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ __func__, [settings = mSettings, prefs = outputPrefs] {
+ settings->mEchoCancellation.Value() = prefs.mAecOn;
+ settings->mAutoGainControl.Value() = prefs.mAgcOn;
+ settings->mNoiseSuppression.Value() = prefs.mNoiseOn;
+ settings->mChannelCount.Value() = prefs.mChannels;
+ }));
+
+ mCurrentPrefs = outputPrefs;
+
+ return rv;
+}
+
+nsresult MediaEngineWebRTCMicrophoneSource::Deallocate() {
+ AssertIsOnOwningThread();
+
+ MOZ_ASSERT(mState == kStopped || mState == kAllocated);
+
+ class EndTrackMessage : public ControlMessage {
+ const RefPtr<AudioInputProcessing> mInputProcessing;
+
+ public:
+ explicit EndTrackMessage(AudioInputProcessing* aAudioInputProcessing)
+ : ControlMessage(nullptr), mInputProcessing(aAudioInputProcessing) {}
+
+ void Run() override {
+ TRACE("mInputProcessing::End");
+ mInputProcessing->End();
+ }
+ };
+
+ if (mTrack) {
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ __func__,
+ [track = std::move(mTrack), inputProcessing = mInputProcessing] {
+ if (track->IsDestroyed()) {
+ // This track has already been destroyed on main thread by its
+ // DOMMediaStream. No cleanup left to do.
+ return;
+ }
+ track->GraphImpl()->AppendMessage(
+ MakeUnique<EndTrackMessage>(inputProcessing));
+ }));
+ }
+
+ // Reset all state. This is not strictly necessary, this instance will get
+ // destroyed soon.
+ mTrack = nullptr;
+ mPrincipal = PRINCIPAL_HANDLE_NONE;
+
+ // If empty, no callbacks to deliver data should be occuring
+ MOZ_ASSERT(mState != kReleased, "Source not allocated");
+ MOZ_ASSERT(mState != kStarted, "Source not stopped");
+
+ mState = kReleased;
+ LOG("Mic source %p Audio device %s deallocated", this,
+ NS_ConvertUTF16toUTF8(mDeviceInfo->Name()).get());
+ return NS_OK;
+}
+
+void MediaEngineWebRTCMicrophoneSource::SetTrack(
+ const RefPtr<MediaTrack>& aTrack, const PrincipalHandle& aPrincipal) {
+ AssertIsOnOwningThread();
+ MOZ_ASSERT(aTrack);
+ MOZ_ASSERT(aTrack->AsAudioProcessingTrack());
+
+ MOZ_ASSERT(!mTrack);
+ MOZ_ASSERT(mPrincipal == PRINCIPAL_HANDLE_NONE);
+ mTrack = aTrack->AsAudioProcessingTrack();
+ mPrincipal = aPrincipal;
+
+ mInputProcessing =
+ MakeAndAddRef<AudioInputProcessing>(mDeviceMaxChannelCount);
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ __func__, [track = mTrack, processing = mInputProcessing]() mutable {
+ track->SetInputProcessing(std::move(processing));
+ track->Resume(); // Suspended by MediaManager
+ }));
+
+ LOG("Mic source %p Track %p registered for microphone capture", this,
+ aTrack.get());
+}
+
+class StartStopMessage : public ControlMessage {
+ public:
+ enum StartStop { Start, Stop };
+
+ StartStopMessage(MediaTrack* aTrack, AudioInputProcessing* aInputProcessing,
+ StartStop aAction)
+ : ControlMessage(aTrack),
+ mInputProcessing(aInputProcessing),
+ mAction(aAction) {}
+
+ void Run() override {
+ if (mAction == StartStopMessage::Start) {
+ TRACE("InputProcessing::Start")
+ mInputProcessing->Start(mTrack->GraphImpl());
+ } else if (mAction == StartStopMessage::Stop) {
+ TRACE("InputProcessing::Stop")
+ mInputProcessing->Stop(mTrack->GraphImpl());
+ } else {
+ MOZ_CRASH("Invalid enum value");
+ }
+ }
+
+ protected:
+ const RefPtr<AudioInputProcessing> mInputProcessing;
+ const StartStop mAction;
+};
+
+nsresult MediaEngineWebRTCMicrophoneSource::Start() {
+ AssertIsOnOwningThread();
+
+ // This spans setting both the enabled state and mState.
+ if (mState == kStarted) {
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(mState == kAllocated || mState == kStopped);
+
+ ApplySettings(mCurrentPrefs);
+
+ CubebUtils::AudioDeviceID deviceID = mDeviceInfo->DeviceID();
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ __func__, [inputProcessing = mInputProcessing, deviceID, track = mTrack,
+ principal = mPrincipal] {
+ if (track->IsDestroyed()) {
+ return;
+ }
+
+ track->GraphImpl()->AppendMessage(MakeUnique<StartStopMessage>(
+ track, inputProcessing, StartStopMessage::Start));
+ track->ConnectDeviceInput(deviceID, inputProcessing.get(), principal);
+ }));
+
+ MOZ_ASSERT(mState != kReleased);
+ mState = kStarted;
+
+ return NS_OK;
+}
+
+nsresult MediaEngineWebRTCMicrophoneSource::Stop() {
+ AssertIsOnOwningThread();
+
+ LOG("Mic source %p Stop()", this);
+ MOZ_ASSERT(mTrack, "SetTrack must have been called before ::Stop");
+
+ if (mState == kStopped) {
+ // Already stopped - this is allowed
+ return NS_OK;
+ }
+
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ __func__, [inputProcessing = mInputProcessing, deviceInfo = mDeviceInfo,
+ track = mTrack] {
+ if (track->IsDestroyed()) {
+ return;
+ }
+
+ MOZ_ASSERT(track->DeviceId().value() == deviceInfo->DeviceID());
+ track->DisconnectDeviceInput();
+ track->GraphImpl()->AppendMessage(MakeUnique<StartStopMessage>(
+ track, inputProcessing, StartStopMessage::Stop));
+ }));
+
+ MOZ_ASSERT(mState == kStarted, "Should be started when stopping");
+ mState = kStopped;
+
+ return NS_OK;
+}
+
+void MediaEngineWebRTCMicrophoneSource::GetSettings(
+ dom::MediaTrackSettings& aOutSettings) const {
+ MOZ_ASSERT(NS_IsMainThread());
+ aOutSettings = *mSettings;
+}
+
+AudioInputProcessing::AudioInputProcessing(uint32_t aMaxChannelCount)
+ : mAudioProcessing(AudioProcessingBuilder().Create().release()),
+ mRequestedInputChannelCount(aMaxChannelCount),
+ mSkipProcessing(false),
+ mInputDownmixBuffer(MAX_SAMPLING_FREQ * MAX_CHANNELS / 100),
+ mEnabled(false),
+ mEnded(false),
+ mPacketCount(0) {}
+
+void AudioInputProcessing::Disconnect(MediaTrackGraphImpl* aGraph) {
+ // This method is just for asserts.
+ MOZ_ASSERT(aGraph->OnGraphThread());
+}
+
+bool AudioInputProcessing::PassThrough(MediaTrackGraphImpl* aGraph) const {
+ MOZ_ASSERT(aGraph->OnGraphThread());
+ return mSkipProcessing;
+}
+
+void AudioInputProcessing::SetPassThrough(MediaTrackGraphImpl* aGraph,
+ bool aPassThrough) {
+ MOZ_ASSERT(aGraph->OnGraphThread());
+
+ if (aPassThrough == mSkipProcessing) {
+ return;
+ }
+ mSkipProcessing = aPassThrough;
+
+ if (!mEnabled) {
+ MOZ_ASSERT(!mPacketizerInput);
+ return;
+ }
+
+ if (aPassThrough) {
+ // Turn on pass-through
+ ResetAudioProcessing(aGraph);
+ } else {
+ // Turn off pass-through
+ MOZ_ASSERT(!mPacketizerInput);
+ EnsureAudioProcessing(aGraph, mRequestedInputChannelCount);
+ }
+}
+
+uint32_t AudioInputProcessing::GetRequestedInputChannelCount() {
+ return mRequestedInputChannelCount;
+}
+
+void AudioInputProcessing::SetRequestedInputChannelCount(
+ MediaTrackGraphImpl* aGraph, CubebUtils::AudioDeviceID aDeviceId,
+ uint32_t aRequestedInputChannelCount) {
+ mRequestedInputChannelCount = aRequestedInputChannelCount;
+
+ aGraph->ReevaluateInputDevice(aDeviceId);
+}
+
+void AudioInputProcessing::Start(MediaTrackGraphImpl* aGraph) {
+ MOZ_ASSERT(aGraph->OnGraphThread());
+
+ if (mEnabled) {
+ return;
+ }
+ mEnabled = true;
+
+ if (mSkipProcessing) {
+ return;
+ }
+
+ MOZ_ASSERT(!mPacketizerInput);
+ EnsureAudioProcessing(aGraph, mRequestedInputChannelCount);
+}
+
+void AudioInputProcessing::Stop(MediaTrackGraphImpl* aGraph) {
+ MOZ_ASSERT(aGraph->OnGraphThread());
+
+ if (!mEnabled) {
+ return;
+ }
+
+ mEnabled = false;
+
+ if (mSkipProcessing) {
+ return;
+ }
+
+ // Packetizer is active and we were just stopped. Stop the packetizer and
+ // processing.
+ ResetAudioProcessing(aGraph);
+}
+
+// The following is how how Process() works in pass-through and non-pass-through
+// mode. In both mode, Process() outputs the same amount of the frames as its
+// input data.
+//
+// I. In non-pass-through mode:
+//
+// We will use webrtc::AudioProcessing to process the input audio data in this
+// mode. The data input in webrtc::AudioProcessing needs to be a 10ms chunk,
+// while the input data passed to Process() is not necessary to have times of
+// 10ms-chunk length. To divide the input data into 10ms chunks,
+// mPacketizerInput is introduced.
+//
+// We will add one 10ms-chunk silence into the internal buffer before Process()
+// works. Those extra frames is called pre-buffering. It aims to avoid glitches
+// we may have when producing data in mPacketizerInput. Without pre-buffering,
+// when the input data length is not 10ms-times, we could end up having no
+// enough output needs since mPacketizerInput would keep some input data, which
+// is the remainder of the 10ms-chunk length. To force processing those data
+// left in mPacketizerInput, we would need to add some extra frames to make
+// mPacketizerInput produce a 10ms-chunk. For example, if the sample rate is
+// 44100 Hz, then the packet-size is 441 frames. When we only have 384 input
+// frames, we would need to put additional 57 frames to mPacketizerInput to
+// produce a packet. However, those extra 57 frames result in a glitch sound.
+//
+// By adding one 10ms-chunk silence in advance to the internal buffer, we won't
+// need to add extra frames between the input data no matter what data length it
+// is. The only drawback is the input data won't be processed and send to output
+// immediately. Process() will consume pre-buffering data for its output first.
+// The below describes how it works:
+//
+//
+// Process()
+// +-----------------------------+
+// input D(N) | +--------+ +--------+ | output D(N)
+// --------------|-->| P(N) |-->| S(N) |---|-------------->
+// | +--------+ +--------+ |
+// | packetizer mSegment |
+// +-----------------------------+
+// <------ internal buffer ------>
+//
+//
+// D(N): number of frames from the input and the output needs in the N round
+// Z: number of frames of a 10ms chunk(packet) in mPacketizerInput, Z >= 1
+// (if Z = 1, packetizer has no effect)
+// P(N): number of frames left in mPacketizerInput after the N round. Once the
+// frames in packetizer >= Z, packetizer will produce a packet to
+// mSegment, so P(N) = (P(N-1) + D(N)) % Z, 0 <= P(N) <= Z-1
+// S(N): number of frames left in mSegment after the N round. The input D(N)
+// frames will be passed to mPacketizerInput first, and then
+// mPacketizerInput may append some packets to mSegment, so
+// S(N) = S(N-1) + Z * floor((P(N-1) + D(N)) / Z) - D(N)
+//
+// At the first, we set P(0) = 0, S(0) = X, where X >= Z-1. X is the
+// pre-buffering put in the internal buffer. With this settings, P(K) + S(K) = X
+// always holds.
+//
+// Intuitively, this seems true: We put X frames in the internal buffer at
+// first. If the data won't be blocked in packetizer, after the Process(), the
+// internal buffer should still hold X frames since the number of frames coming
+// from input is the same as the output needs. The key of having enough data for
+// output needs, while the input data is piled up in packetizer, is by putting
+// at least Z-1 frames as pre-buffering, since the maximum number of frames
+// stuck in the packetizer before it can emit a packet is packet-size - 1.
+// Otherwise, we don't have enough data for output if the new input data plus
+// the data left in packetizer produces a smaller-than-10ms chunk, which will be
+// left in packetizer. Thus we must have some pre-buffering frames in the
+// mSegment to make up the length of the left chunk we need for output. This can
+// also be told by by induction:
+// (1) This holds when K = 0
+// (2) Assume this holds when K = N: so P(N) + S(N) = X
+// => P(N) + S(N) = X >= Z-1 => S(N) >= Z-1-P(N)
+// (3) When K = N+1, we have D(N+1) input frames comes
+// a. if P(N) + D(N+1) < Z, then packetizer has no enough data for one
+// packet. No data produced by packertizer, so the mSegment now has
+// S(N) >= Z-1-P(N) frames. Output needs D(N+1) < Z-P(N) frames. So it
+// needs at most Z-P(N)-1 frames, and mSegment has enough frames for
+// output, Then, P(N+1) = P(N) + D(N+1) and S(N+1) = S(N) - D(N+1)
+// => P(N+1) + S(N+1) = P(N) + S(N) = X
+// b. if P(N) + D(N+1) = Z, then packetizer will produce one packet for
+// mSegment, so mSegment now has S(N) + Z frames. Output needs D(N+1)
+// = Z-P(N) frames. S(N) has at least Z-1-P(N)+Z >= Z-P(N) frames, since
+// Z >= 1. So mSegment has enough frames for output. Then, P(N+1) = 0 and
+// S(N+1) = S(N) + Z - D(N+1) = S(N) + P(N)
+// => P(N+1) + S(N+1) = P(N) + S(N) = X
+// c. if P(N) + D(N+1) > Z, and let P(N) + D(N+1) = q * Z + r, where q >= 1
+// and 0 <= r <= Z-1, then packetizer will produce can produce q packets
+// for mSegment. Output needs D(N+1) = q * Z - P(N) + r frames and
+// mSegment has S(N) + q * z >= q * z - P(N) + Z-1 >= q*z -P(N) + r,
+// since r <= Z-1. So mSegment has enough frames for output. Then,
+// P(N+1) = r and S(N+1) = S(N) + q * Z - D(N+1)
+// => P(N+1) + S(N+1) = S(N) + (q * Z + r - D(N+1)) = S(N) + P(N) = X
+// => P(K) + S(K) = X always holds
+//
+// Since P(K) + S(K) = X and P(K) is in [0, Z-1], the S(K) is in [X-Z+1, X]
+// range. In our implementation, X is set to Z so S(K) is in [1, Z].
+// By the above workflow, we always have enough data for output and no extra
+// frames put into packetizer. It means we don't have any glitch!
+//
+// II. In pass-through mode:
+//
+// Process()
+// +--------+
+// input D(N) | | output D(N)
+// -------------->-------->--------------->
+// | |
+// +--------+
+//
+// The D(N) frames of data are just forwarded from input to output without any
+// processing
+void AudioInputProcessing::Process(MediaTrackGraphImpl* aGraph, GraphTime aFrom,
+ GraphTime aTo, AudioSegment* aInput,
+ AudioSegment* aOutput) {
+ MOZ_ASSERT(aGraph->OnGraphThread());
+ MOZ_ASSERT(aFrom <= aTo);
+ MOZ_ASSERT(!mEnded);
+
+ TrackTime need = aTo - aFrom;
+ if (need == 0) {
+ return;
+ }
+
+ if (!mEnabled) {
+ LOG_FRAME("(Graph %p, Driver %p) AudioInputProcessing %p Filling %" PRId64
+ " frames of silence to output (disabled)",
+ aGraph, aGraph->CurrentDriver(), this, need);
+ aOutput->AppendNullData(need);
+ return;
+ }
+
+ MOZ_ASSERT(aInput->GetDuration() == need,
+ "Wrong data length from input port source");
+
+ if (PassThrough(aGraph)) {
+ LOG_FRAME(
+ "(Graph %p, Driver %p) AudioInputProcessing %p Forwarding %" PRId64
+ " frames of input data to output directly (PassThrough)",
+ aGraph, aGraph->CurrentDriver(), this, aInput->GetDuration());
+ aOutput->AppendSegment(aInput);
+ return;
+ }
+
+ // SetPassThrough(false) must be called before reaching here.
+ MOZ_ASSERT(mPacketizerInput);
+ // If mRequestedInputChannelCount is updated, create a new packetizer. No
+ // need to change the pre-buffering since the rate is always the same. The
+ // frames left in the packetizer would be replaced by null data and then
+ // transferred to mSegment.
+ EnsureAudioProcessing(aGraph, mRequestedInputChannelCount);
+
+ // Preconditions of the audio-processing logic.
+ MOZ_ASSERT(static_cast<uint32_t>(mSegment.GetDuration()) +
+ mPacketizerInput->FramesAvailable() ==
+ mPacketizerInput->mPacketSize);
+ // We pre-buffer mPacketSize frames, but the maximum number of frames stuck in
+ // the packetizer before it can emit a packet is mPacketSize-1. Thus that
+ // remaining 1 frame will always be present in mSegment.
+ MOZ_ASSERT(mSegment.GetDuration() >= 1);
+ MOZ_ASSERT(mSegment.GetDuration() <= mPacketizerInput->mPacketSize);
+
+ PacketizeAndProcess(aGraph, *aInput);
+ LOG_FRAME("(Graph %p, Driver %p) AudioInputProcessing %p Buffer has %" PRId64
+ " frames of data now, after packetizing and processing",
+ aGraph, aGraph->CurrentDriver(), this, mSegment.GetDuration());
+
+ // By setting pre-buffering to the number of frames of one packet, and
+ // because the maximum number of frames stuck in the packetizer before
+ // it can emit a packet is the mPacketSize-1, we always have at least
+ // one more frame than output needs.
+ MOZ_ASSERT(mSegment.GetDuration() > need);
+ aOutput->AppendSlice(mSegment, 0, need);
+ mSegment.RemoveLeading(need);
+ LOG_FRAME("(Graph %p, Driver %p) AudioInputProcessing %p moving %" PRId64
+ " frames of data to output, leaving %" PRId64 " frames in buffer",
+ aGraph, aGraph->CurrentDriver(), this, need,
+ mSegment.GetDuration());
+
+ // Postconditions of the audio-processing logic.
+ MOZ_ASSERT(static_cast<uint32_t>(mSegment.GetDuration()) +
+ mPacketizerInput->FramesAvailable() ==
+ mPacketizerInput->mPacketSize);
+ MOZ_ASSERT(mSegment.GetDuration() >= 1);
+ MOZ_ASSERT(mSegment.GetDuration() <= mPacketizerInput->mPacketSize);
+}
+
+void AudioInputProcessing::ProcessOutputData(MediaTrackGraphImpl* aGraph,
+ AudioDataValue* aBuffer,
+ size_t aFrames, TrackRate aRate,
+ uint32_t aChannels) {
+ MOZ_ASSERT(aGraph->OnGraphThread());
+
+ if (!mEnabled || PassThrough(aGraph)) {
+ return;
+ }
+
+ if (!mPacketizerOutput ||
+ mPacketizerOutput->mPacketSize != GetPacketSize(aRate) ||
+ mPacketizerOutput->mChannels != aChannels) {
+ // It's ok to drop the audio still in the packetizer here: if this changes,
+ // we changed devices or something.
+ mPacketizerOutput = Nothing();
+ mPacketizerOutput.emplace(GetPacketSize(aRate), aChannels);
+ }
+
+ mPacketizerOutput->Input(aBuffer, aFrames);
+
+ while (mPacketizerOutput->PacketsAvailable()) {
+ uint32_t samplesPerPacket =
+ mPacketizerOutput->mPacketSize * mPacketizerOutput->mChannels;
+ if (mOutputBuffer.Length() < samplesPerPacket) {
+ mOutputBuffer.SetLength(samplesPerPacket);
+ }
+ if (mDeinterleavedBuffer.Length() < samplesPerPacket) {
+ mDeinterleavedBuffer.SetLength(samplesPerPacket);
+ }
+ float* packet = mOutputBuffer.Data();
+ mPacketizerOutput->Output(packet);
+
+ AutoTArray<float*, MAX_CHANNELS> deinterleavedPacketDataChannelPointers;
+ float* interleavedFarend = nullptr;
+ uint32_t channelCountFarend = 0;
+ uint32_t framesPerPacketFarend = 0;
+
+ // Downmix from aChannels to MAX_CHANNELS if needed. We always have
+ // floats here, the packetized performed the conversion.
+ if (aChannels > MAX_CHANNELS) {
+ AudioConverter converter(
+ AudioConfig(aChannels, 0, AudioConfig::FORMAT_FLT),
+ AudioConfig(MAX_CHANNELS, 0, AudioConfig::FORMAT_FLT));
+ framesPerPacketFarend = mPacketizerOutput->mPacketSize;
+ framesPerPacketFarend =
+ converter.Process(mInputDownmixBuffer, packet, framesPerPacketFarend);
+ interleavedFarend = mInputDownmixBuffer.Data();
+ channelCountFarend = MAX_CHANNELS;
+ deinterleavedPacketDataChannelPointers.SetLength(MAX_CHANNELS);
+ } else {
+ interleavedFarend = packet;
+ channelCountFarend = aChannels;
+ framesPerPacketFarend = mPacketizerOutput->mPacketSize;
+ deinterleavedPacketDataChannelPointers.SetLength(aChannels);
+ }
+
+ MOZ_ASSERT(interleavedFarend &&
+ (channelCountFarend == 1 || channelCountFarend == 2) &&
+ framesPerPacketFarend);
+
+ if (mInputBuffer.Length() < framesPerPacketFarend * channelCountFarend) {
+ mInputBuffer.SetLength(framesPerPacketFarend * channelCountFarend);
+ }
+
+ size_t offset = 0;
+ for (size_t i = 0; i < deinterleavedPacketDataChannelPointers.Length();
+ ++i) {
+ deinterleavedPacketDataChannelPointers[i] = mInputBuffer.Data() + offset;
+ offset += framesPerPacketFarend;
+ }
+
+ // Deinterleave, prepare a channel pointers array, with enough storage for
+ // the frames.
+ DeinterleaveAndConvertBuffer(
+ interleavedFarend, framesPerPacketFarend, channelCountFarend,
+ deinterleavedPacketDataChannelPointers.Elements());
+
+ // Having the same config for input and output means we potentially save
+ // some CPU.
+ StreamConfig inputConfig(aRate, channelCountFarend);
+ StreamConfig outputConfig = inputConfig;
+
+ // Passing the same pointers here saves a copy inside this function.
+ DebugOnly<int> err = mAudioProcessing->ProcessReverseStream(
+ deinterleavedPacketDataChannelPointers.Elements(), inputConfig,
+ outputConfig, deinterleavedPacketDataChannelPointers.Elements());
+
+ MOZ_ASSERT(!err, "Could not process the reverse stream.");
+ }
+}
+
+// Only called if we're not in passthrough mode
+void AudioInputProcessing::PacketizeAndProcess(MediaTrackGraphImpl* aGraph,
+ const AudioSegment& aSegment) {
+ MOZ_ASSERT(!PassThrough(aGraph),
+ "This should be bypassed when in PassThrough mode.");
+ MOZ_ASSERT(mEnabled);
+ MOZ_ASSERT(mPacketizerInput);
+ MOZ_ASSERT(mPacketizerInput->mPacketSize ==
+ GetPacketSize(aGraph->GraphRate()));
+
+ // Calculate number of the pending frames in mChunksInPacketizer.
+ auto pendingFrames = [&]() {
+ TrackTime frames = 0;
+ for (const auto& p : mChunksInPacketizer) {
+ frames += p.first;
+ }
+ return frames;
+ };
+
+ // Precondition of the Principal-labelling logic below.
+ MOZ_ASSERT(mPacketizerInput->FramesAvailable() ==
+ static_cast<uint32_t>(pendingFrames()));
+
+ // The WriteToInterleavedBuffer will do upmix or downmix if the channel-count
+ // in aSegment's chunks is different from mPacketizerInput->mChannels
+ // WriteToInterleavedBuffer could be avoided once Bug 1729041 is done.
+ size_t sampleCount = aSegment.WriteToInterleavedBuffer(
+ mInterleavedBuffer, mPacketizerInput->mChannels);
+ size_t frameCount =
+ sampleCount / static_cast<size_t>(mPacketizerInput->mChannels);
+
+ // Packetize our input data into 10ms chunks, deinterleave into planar channel
+ // buffers, process, and append to the right MediaStreamTrack.
+ mPacketizerInput->Input(mInterleavedBuffer.Elements(),
+ static_cast<uint32_t>(frameCount));
+
+ // Update mChunksInPacketizer and make sure the precondition for the
+ // Principal-labelling logic still holds.
+ for (AudioSegment::ConstChunkIterator iter(aSegment); !iter.IsEnded();
+ iter.Next()) {
+ MOZ_ASSERT(iter->mDuration > 0);
+ mChunksInPacketizer.emplace_back(
+ std::make_pair(iter->mDuration, iter->mPrincipalHandle));
+ }
+ MOZ_ASSERT(mPacketizerInput->FramesAvailable() ==
+ static_cast<uint32_t>(pendingFrames()));
+
+ LOG_FRAME(
+ "(Graph %p, Driver %p) AudioInputProcessing %p Packetizing %zu frames. "
+ "Packetizer has %u frames (enough for %u packets) now",
+ aGraph, aGraph->CurrentDriver(), this, frameCount,
+ mPacketizerInput->FramesAvailable(),
+ mPacketizerInput->PacketsAvailable());
+
+ size_t offset = 0;
+
+ while (mPacketizerInput->PacketsAvailable()) {
+ mPacketCount++;
+ uint32_t samplesPerPacket =
+ mPacketizerInput->mPacketSize * mPacketizerInput->mChannels;
+ if (mInputBuffer.Length() < samplesPerPacket) {
+ mInputBuffer.SetLength(samplesPerPacket);
+ }
+ if (mDeinterleavedBuffer.Length() < samplesPerPacket) {
+ mDeinterleavedBuffer.SetLength(samplesPerPacket);
+ }
+ float* packet = mInputBuffer.Data();
+ mPacketizerInput->Output(packet);
+
+ // Downmix from mPacketizerInput->mChannels to mono if needed. We always
+ // have floats here, the packetizer performed the conversion.
+ AutoTArray<float*, 8> deinterleavedPacketizedInputDataChannelPointers;
+ uint32_t channelCountInput = 0;
+ if (mPacketizerInput->mChannels > MAX_CHANNELS) {
+ channelCountInput = MONO;
+ deinterleavedPacketizedInputDataChannelPointers.SetLength(
+ channelCountInput);
+ deinterleavedPacketizedInputDataChannelPointers[0] =
+ mDeinterleavedBuffer.Data();
+ // Downmix to mono (and effectively have a planar buffer) by summing all
+ // channels in the first channel, and scaling by the number of channels to
+ // avoid clipping.
+ float gain = 1.f / mPacketizerInput->mChannels;
+ size_t readIndex = 0;
+ for (size_t i = 0; i < mPacketizerInput->mPacketSize; i++) {
+ mDeinterleavedBuffer.Data()[i] = 0.;
+ for (size_t j = 0; j < mPacketizerInput->mChannels; j++) {
+ mDeinterleavedBuffer.Data()[i] += gain * packet[readIndex++];
+ }
+ }
+ } else {
+ channelCountInput = mPacketizerInput->mChannels;
+ // Deinterleave the input data
+ // Prepare an array pointing to deinterleaved channels.
+ deinterleavedPacketizedInputDataChannelPointers.SetLength(
+ channelCountInput);
+ offset = 0;
+ for (size_t i = 0;
+ i < deinterleavedPacketizedInputDataChannelPointers.Length(); ++i) {
+ deinterleavedPacketizedInputDataChannelPointers[i] =
+ mDeinterleavedBuffer.Data() + offset;
+ offset += mPacketizerInput->mPacketSize;
+ }
+ // Deinterleave to mInputBuffer, pointed to by inputBufferChannelPointers.
+ Deinterleave(packet, mPacketizerInput->mPacketSize, channelCountInput,
+ deinterleavedPacketizedInputDataChannelPointers.Elements());
+ }
+
+ StreamConfig inputConfig(aGraph->GraphRate(), channelCountInput);
+ StreamConfig outputConfig = inputConfig;
+
+ // Bug 1404965: Get the right delay here, it saves some work down the line.
+ mAudioProcessing->set_stream_delay_ms(0);
+
+ // Bug 1414837: find a way to not allocate here.
+ CheckedInt<size_t> bufferSize(sizeof(float));
+ bufferSize *= mPacketizerInput->mPacketSize;
+ bufferSize *= channelCountInput;
+ RefPtr<SharedBuffer> buffer = SharedBuffer::Create(bufferSize);
+
+ // Prepare channel pointers to the SharedBuffer created above.
+ AutoTArray<float*, 8> processedOutputChannelPointers;
+ AutoTArray<const float*, 8> processedOutputChannelPointersConst;
+ processedOutputChannelPointers.SetLength(channelCountInput);
+ processedOutputChannelPointersConst.SetLength(channelCountInput);
+
+ offset = 0;
+ for (size_t i = 0; i < processedOutputChannelPointers.Length(); ++i) {
+ processedOutputChannelPointers[i] =
+ static_cast<float*>(buffer->Data()) + offset;
+ processedOutputChannelPointersConst[i] =
+ static_cast<float*>(buffer->Data()) + offset;
+ offset += mPacketizerInput->mPacketSize;
+ }
+
+ mAudioProcessing->ProcessStream(
+ deinterleavedPacketizedInputDataChannelPointers.Elements(), inputConfig,
+ outputConfig, processedOutputChannelPointers.Elements());
+
+ // If logging is enabled, dump the audio processing stats twice a second
+ if (MOZ_LOG_TEST(gMediaManagerLog, LogLevel::Debug) &&
+ !(mPacketCount % 50)) {
+ AudioProcessingStats stats = mAudioProcessing->GetStatistics();
+ char msg[1024];
+ size_t offset = 0;
+#define AddIfValue(format, member) \
+ if (stats.member.has_value()) { \
+ offset += SprintfBuf(msg + offset, sizeof(msg) - offset, \
+ #member ":" format ", ", stats.member.value()); \
+ }
+ AddIfValue("%d", voice_detected);
+ AddIfValue("%lf", echo_return_loss);
+ AddIfValue("%lf", echo_return_loss_enhancement);
+ AddIfValue("%lf", divergent_filter_fraction);
+ AddIfValue("%d", delay_median_ms);
+ AddIfValue("%d", delay_standard_deviation_ms);
+ AddIfValue("%lf", residual_echo_likelihood);
+ AddIfValue("%lf", residual_echo_likelihood_recent_max);
+ AddIfValue("%d", delay_ms);
+#undef AddIfValue
+ LOG("AudioProcessing statistics: %s", msg);
+ }
+
+ if (mEnded) {
+ continue;
+ }
+
+ // We already have planar audio data of the right format. Insert into the
+ // MTG.
+ MOZ_ASSERT(processedOutputChannelPointers.Length() == channelCountInput);
+
+ // Insert the processed data chunk by chunk to mSegment with the paired
+ // PrincipalHandle value. The chunks are tracked in mChunksInPacketizer.
+
+ auto getAudioChunk = [&](TrackTime aStart, TrackTime aEnd,
+ const PrincipalHandle& aPrincipalHandle) {
+ if (aStart == aEnd) {
+ return AudioChunk();
+ }
+ RefPtr<SharedBuffer> other = buffer;
+ AudioChunk c =
+ AudioChunk(other.forget(), processedOutputChannelPointersConst,
+ static_cast<TrackTime>(mPacketizerInput->mPacketSize),
+ aPrincipalHandle);
+ c.SliceTo(aStart, aEnd);
+ return c;
+ };
+
+ // The number of frames of data that needs to be labelled with Principal
+ // values.
+ TrackTime len = static_cast<TrackTime>(mPacketizerInput->mPacketSize);
+ // The start offset of the unlabelled chunk.
+ TrackTime start = 0;
+ // By mChunksInPacketizer's information, we can keep labelling the
+ // unlabelled frames chunk by chunk.
+ while (!mChunksInPacketizer.empty()) {
+ auto& [frames, principal] = mChunksInPacketizer.front();
+ const TrackTime end = start + frames;
+ if (end > len) {
+ // If the left unlabelled frames are part of this chunk, then we need to
+ // adjust the number of frames in the chunk.
+ if (len > start) {
+ mSegment.AppendAndConsumeChunk(getAudioChunk(start, len, principal));
+ frames -= len - start;
+ }
+ break;
+ }
+ // Otherwise, the number of unlabelled frames is larger than or equal to
+ // this chunk. We can label the whole chunk directly.
+ mSegment.AppendAndConsumeChunk(getAudioChunk(start, end, principal));
+ start = end;
+ mChunksInPacketizer.pop_front();
+ }
+
+ LOG_FRAME(
+ "(Graph %p, Driver %p) AudioInputProcessing %p Appending %u frames of "
+ "packetized audio, leaving %u frames in packetizer (%" PRId64
+ " frames in mChunksInPacketizer)",
+ aGraph, aGraph->CurrentDriver(), this, mPacketizerInput->mPacketSize,
+ mPacketizerInput->FramesAvailable(), pendingFrames());
+
+ // Postcondition of the Principal-labelling logic.
+ MOZ_ASSERT(mPacketizerInput->FramesAvailable() ==
+ static_cast<uint32_t>(pendingFrames()));
+ }
+}
+
+void AudioInputProcessing::DeviceChanged(MediaTrackGraphImpl* aGraph) {
+ MOZ_ASSERT(aGraph->OnGraphThread());
+
+ // Reset some processing
+ mAudioProcessing->Initialize();
+ LOG_FRAME(
+ "(Graph %p, Driver %p) AudioInputProcessing %p Reinitializing audio "
+ "processing",
+ aGraph, aGraph->CurrentDriver(), this);
+}
+
+void AudioInputProcessing::ApplyConfig(MediaTrackGraphImpl* aGraph,
+ const AudioProcessing::Config& aConfig) {
+ MOZ_ASSERT(aGraph->OnGraphThread());
+ mAudioProcessing->ApplyConfig(aConfig);
+}
+
+void AudioInputProcessing::End() {
+ mEnded = true;
+ mSegment.Clear();
+}
+
+TrackTime AudioInputProcessing::NumBufferedFrames(
+ MediaTrackGraphImpl* aGraph) const {
+ MOZ_ASSERT(aGraph->OnGraphThread());
+ return mSegment.GetDuration();
+}
+
+void AudioInputProcessing::EnsureAudioProcessing(MediaTrackGraphImpl* aGraph,
+ uint32_t aChannels) {
+ MOZ_ASSERT(aGraph->OnGraphThread());
+ MOZ_ASSERT(aChannels > 0);
+ MOZ_ASSERT(mEnabled);
+ MOZ_ASSERT(!mSkipProcessing);
+
+ if (mPacketizerInput && mPacketizerInput->mChannels == aChannels) {
+ return;
+ }
+
+ // If mPacketizerInput exists but with different channel-count, there is no
+ // need to change pre-buffering since the packet size is the same as the old
+ // one, since the rate is a constant.
+ MOZ_ASSERT_IF(mPacketizerInput, mPacketizerInput->mPacketSize ==
+ GetPacketSize(aGraph->GraphRate()));
+ bool needPreBuffering = !mPacketizerInput;
+ if (mPacketizerInput) {
+ const TrackTime numBufferedFrames =
+ static_cast<TrackTime>(mPacketizerInput->FramesAvailable());
+ mSegment.AppendNullData(numBufferedFrames);
+ mPacketizerInput = Nothing();
+ mChunksInPacketizer.clear();
+ }
+
+ mPacketizerInput.emplace(GetPacketSize(aGraph->GraphRate()), aChannels);
+
+ if (needPreBuffering) {
+ LOG_FRAME(
+ "(Graph %p, Driver %p) AudioInputProcessing %p: Adding %u frames of "
+ "silence as pre-buffering",
+ aGraph, aGraph->CurrentDriver(), this, mPacketizerInput->mPacketSize);
+
+ AudioSegment buffering;
+ buffering.AppendNullData(
+ static_cast<TrackTime>(mPacketizerInput->mPacketSize));
+ PacketizeAndProcess(aGraph, buffering);
+ }
+}
+
+void AudioInputProcessing::ResetAudioProcessing(MediaTrackGraphImpl* aGraph) {
+ MOZ_ASSERT(aGraph->OnGraphThread());
+ MOZ_ASSERT(mSkipProcessing || !mEnabled);
+ MOZ_ASSERT(mPacketizerInput);
+
+ LOG_FRAME(
+ "(Graph %p, Driver %p) AudioInputProcessing %p Resetting audio "
+ "processing",
+ aGraph, aGraph->CurrentDriver(), this);
+
+ // Reset AudioProcessing so that if we resume processing in the future it
+ // doesn't depend on old state.
+ mAudioProcessing->Initialize();
+
+ MOZ_ASSERT(static_cast<uint32_t>(mSegment.GetDuration()) +
+ mPacketizerInput->FramesAvailable() ==
+ mPacketizerInput->mPacketSize);
+
+ // It's ok to clear all the internal buffer here since we won't use mSegment
+ // in pass-through mode or when audio processing is disabled.
+ LOG_FRAME(
+ "(Graph %p, Driver %p) AudioInputProcessing %p Emptying out %" PRId64
+ " frames of data",
+ aGraph, aGraph->CurrentDriver(), this, mSegment.GetDuration());
+ mSegment.Clear();
+
+ mPacketizerInput = Nothing();
+ mChunksInPacketizer.clear();
+}
+
+void AudioProcessingTrack::Destroy() {
+ MOZ_ASSERT(NS_IsMainThread());
+ DisconnectDeviceInput();
+
+ MediaTrack::Destroy();
+}
+
+void AudioProcessingTrack::SetInputProcessing(
+ RefPtr<AudioInputProcessing> aInputProcessing) {
+ class Message : public ControlMessage {
+ const RefPtr<AudioProcessingTrack> mTrack;
+ const RefPtr<AudioInputProcessing> mProcessing;
+
+ public:
+ Message(RefPtr<AudioProcessingTrack> aTrack,
+ RefPtr<AudioInputProcessing> aProcessing)
+ : ControlMessage(aTrack),
+ mTrack(std::move(aTrack)),
+ mProcessing(std::move(aProcessing)) {}
+ void Run() override {
+ TRACE("AudioProcessingTrack::SetInputProcessingImpl");
+ mTrack->SetInputProcessingImpl(mProcessing);
+ }
+ };
+
+ if (IsDestroyed()) {
+ return;
+ }
+ GraphImpl()->AppendMessage(
+ MakeUnique<Message>(std::move(this), std::move(aInputProcessing)));
+}
+
+AudioProcessingTrack* AudioProcessingTrack::Create(MediaTrackGraph* aGraph) {
+ MOZ_ASSERT(NS_IsMainThread());
+ AudioProcessingTrack* track = new AudioProcessingTrack(aGraph->GraphRate());
+ aGraph->AddTrack(track);
+ return track;
+}
+
+void AudioProcessingTrack::DestroyImpl() {
+ ProcessedMediaTrack::DestroyImpl();
+ if (mInputProcessing) {
+ mInputProcessing->End();
+ }
+}
+
+void AudioProcessingTrack::ProcessInput(GraphTime aFrom, GraphTime aTo,
+ uint32_t aFlags) {
+ TRACE_COMMENT("AudioProcessingTrack::ProcessInput", "AudioProcessingTrack %p",
+ this);
+ MOZ_ASSERT(mInputProcessing);
+
+ LOG_FRAME(
+ "(Graph %p, Driver %p) AudioProcessingTrack %p ProcessInput from %" PRId64
+ " to %" PRId64 ", needs %" PRId64 " frames",
+ mGraph, mGraph->CurrentDriver(), this, aFrom, aTo, aTo - aFrom);
+
+ if (aFrom >= aTo) {
+ return;
+ }
+
+ if (!mInputProcessing->IsEnded()) {
+ MOZ_ASSERT(TrackTimeToGraphTime(GetEnd()) == aFrom);
+ if (mInputs.IsEmpty()) {
+ GetData<AudioSegment>()->AppendNullData(aTo - aFrom);
+ LOG_FRAME("(Graph %p, Driver %p) AudioProcessingTrack %p Filling %" PRId64
+ " frames of null data (no input source)",
+ mGraph, mGraph->CurrentDriver(), this, aTo - aFrom);
+ } else {
+ MOZ_ASSERT(mInputs.Length() == 1);
+ AudioSegment data;
+ DeviceInputConsumerTrack::GetInputSourceData(data, mInputs[0], aFrom,
+ aTo);
+ mInputProcessing->Process(GraphImpl(), aFrom, aTo, &data,
+ GetData<AudioSegment>());
+ }
+ MOZ_ASSERT(TrackTimeToGraphTime(GetEnd()) == aTo);
+
+ ApplyTrackDisabling(mSegment.get());
+ } else if (aFlags & ALLOW_END) {
+ mEnded = true;
+ }
+}
+
+void AudioProcessingTrack::NotifyOutputData(MediaTrackGraphImpl* aGraph,
+ AudioDataValue* aBuffer,
+ size_t aFrames, TrackRate aRate,
+ uint32_t aChannels) {
+ MOZ_ASSERT(mGraph == aGraph, "Cannot feed audio output to another graph");
+ MOZ_ASSERT(mGraph->OnGraphThread());
+ if (mInputProcessing) {
+ mInputProcessing->ProcessOutputData(aGraph, aBuffer, aFrames, aRate,
+ aChannels);
+ }
+}
+
+void AudioProcessingTrack::SetInputProcessingImpl(
+ RefPtr<AudioInputProcessing> aInputProcessing) {
+ MOZ_ASSERT(GraphImpl()->OnGraphThread());
+ mInputProcessing = std::move(aInputProcessing);
+}
+
+MediaEngineWebRTCAudioCaptureSource::MediaEngineWebRTCAudioCaptureSource(
+ const MediaDevice* aMediaDevice) {
+ MOZ_ASSERT(aMediaDevice->mMediaSource == MediaSourceEnum::AudioCapture);
+}
+
+/* static */
+nsString MediaEngineWebRTCAudioCaptureSource::GetUUID() {
+ nsID uuid{};
+ char uuidBuffer[NSID_LENGTH];
+ nsCString asciiString;
+ ErrorResult rv;
+
+ rv = nsID::GenerateUUIDInPlace(uuid);
+ if (rv.Failed()) {
+ return u""_ns;
+ }
+
+ uuid.ToProvidedString(uuidBuffer);
+ asciiString.AssignASCII(uuidBuffer);
+
+ // Remove {} and the null terminator
+ return NS_ConvertASCIItoUTF16(Substring(asciiString, 1, NSID_LENGTH - 3));
+}
+
+/* static */
+nsString MediaEngineWebRTCAudioCaptureSource::GetGroupId() {
+ return u"AudioCaptureGroup"_ns;
+}
+
+void MediaEngineWebRTCAudioCaptureSource::SetTrack(
+ const RefPtr<MediaTrack>& aTrack, const PrincipalHandle& aPrincipalHandle) {
+ AssertIsOnOwningThread();
+ // Nothing to do here. aTrack is a placeholder dummy and not exposed.
+}
+
+nsresult MediaEngineWebRTCAudioCaptureSource::Start() {
+ AssertIsOnOwningThread();
+ return NS_OK;
+}
+
+nsresult MediaEngineWebRTCAudioCaptureSource::Stop() {
+ AssertIsOnOwningThread();
+ return NS_OK;
+}
+
+nsresult MediaEngineWebRTCAudioCaptureSource::Reconfigure(
+ const dom::MediaTrackConstraints& aConstraints,
+ const MediaEnginePrefs& aPrefs, const char** aOutBadConstraint) {
+ return NS_OK;
+}
+
+void MediaEngineWebRTCAudioCaptureSource::GetSettings(
+ dom::MediaTrackSettings& aOutSettings) const {
+ aOutSettings.mAutoGainControl.Construct(false);
+ aOutSettings.mEchoCancellation.Construct(false);
+ aOutSettings.mNoiseSuppression.Construct(false);
+ aOutSettings.mChannelCount.Construct(1);
+}
+
+} // namespace mozilla
+
+// Don't allow our macros to leak into other cpps in our unified build unit.
+#undef MAX_CHANNELS
+#undef MONO
+#undef MAX_SAMPLING_FREQ
diff --git a/dom/media/webrtc/MediaEngineWebRTCAudio.h b/dom/media/webrtc/MediaEngineWebRTCAudio.h
new file mode 100644
index 0000000000..d9a602962d
--- /dev/null
+++ b/dom/media/webrtc/MediaEngineWebRTCAudio.h
@@ -0,0 +1,295 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MediaEngineWebRTCAudio_h
+#define MediaEngineWebRTCAudio_h
+
+#include "AudioPacketizer.h"
+#include "AudioSegment.h"
+#include "AudioDeviceInfo.h"
+#include "DeviceInputTrack.h"
+#include "MediaEngineWebRTC.h"
+#include "MediaEnginePrefs.h"
+#include "MediaTrackListener.h"
+#include "modules/audio_processing/include/audio_processing.h"
+
+namespace mozilla {
+
+class AudioInputProcessing;
+class AudioProcessingTrack;
+
+// This class is created and used exclusively on the Media Manager thread, with
+// exactly two exceptions:
+// - Pull is always called on the MTG thread. It only ever uses
+// mInputProcessing. mInputProcessing is set, then a message is sent first to
+// the main thread and then the MTG thread so that it can be used as part of
+// the graph processing. On destruction, similarly, a message is sent to the
+// graph so that it stops using it, and then it is deleted.
+// - mSettings is created on the MediaManager thread is always ever accessed on
+// the Main Thread. It is const.
+class MediaEngineWebRTCMicrophoneSource : public MediaEngineSource {
+ public:
+ explicit MediaEngineWebRTCMicrophoneSource(const MediaDevice* aMediaDevice);
+
+ nsresult Allocate(const dom::MediaTrackConstraints& aConstraints,
+ const MediaEnginePrefs& aPrefs, uint64_t aWindowID,
+ const char** aOutBadConstraint) override;
+ nsresult Deallocate() override;
+ void SetTrack(const RefPtr<MediaTrack>& aTrack,
+ const PrincipalHandle& aPrincipal) override;
+ nsresult Start() override;
+ nsresult Stop() override;
+ nsresult Reconfigure(const dom::MediaTrackConstraints& aConstraints,
+ const MediaEnginePrefs& aPrefs,
+ const char** aOutBadConstraint) override;
+
+ /**
+ * Assigns the current settings of the capture to aOutSettings.
+ * Main thread only.
+ */
+ void GetSettings(dom::MediaTrackSettings& aOutSettings) const override;
+
+ nsresult TakePhoto(MediaEnginePhotoCallback* aCallback) override {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ protected:
+ ~MediaEngineWebRTCMicrophoneSource() = default;
+
+ private:
+ /**
+ * From a set of constraints and about:config preferences, output the correct
+ * set of preferences that can be sent to AudioInputProcessing.
+ *
+ * This can fail if the number of channels requested is zero, negative, or
+ * more than the device supports.
+ */
+ nsresult EvaluateSettings(const NormalizedConstraints& aConstraintsUpdate,
+ const MediaEnginePrefs& aInPrefs,
+ MediaEnginePrefs* aOutPrefs,
+ const char** aOutBadConstraint);
+ /**
+ * From settings output by EvaluateSettings, send those settings to the
+ * AudioInputProcessing instance and the main thread (for use in GetSettings).
+ */
+ void ApplySettings(const MediaEnginePrefs& aPrefs);
+
+ PrincipalHandle mPrincipal = PRINCIPAL_HANDLE_NONE;
+
+ const RefPtr<AudioDeviceInfo> mDeviceInfo;
+
+ // The maximum number of channels that this device supports.
+ const uint32_t mDeviceMaxChannelCount;
+ // The current settings for the underlying device.
+ // Constructed on the MediaManager thread, and then only ever accessed on the
+ // main thread.
+ const nsMainThreadPtrHandle<media::Refcountable<dom::MediaTrackSettings>>
+ mSettings;
+
+ // Current state of the resource for this source.
+ MediaEngineSourceState mState;
+
+ // The current preferences that will be forwarded to mAudioProcessingConfig
+ // below.
+ MediaEnginePrefs mCurrentPrefs;
+
+ // The AudioProcessingTrack used to inteface with the MediaTrackGraph. Set in
+ // SetTrack as part of the initialization, and nulled in ::Deallocate.
+ RefPtr<AudioProcessingTrack> mTrack;
+
+ // See note at the top of this class.
+ RefPtr<AudioInputProcessing> mInputProcessing;
+
+ // Copy of the config currently applied to AudioProcessing through
+ // mInputProcessing.
+ webrtc::AudioProcessing::Config mAudioProcessingConfig;
+};
+
+// This class is created on the MediaManager thread, and then exclusively used
+// on the MTG thread.
+// All communication is done via message passing using MTG ControlMessages
+class AudioInputProcessing : public AudioDataListener {
+ public:
+ explicit AudioInputProcessing(uint32_t aMaxChannelCount);
+ void Process(MediaTrackGraphImpl* aGraph, GraphTime aFrom, GraphTime aTo,
+ AudioSegment* aInput, AudioSegment* aOutput);
+
+ void ProcessOutputData(MediaTrackGraphImpl* aGraph, AudioDataValue* aBuffer,
+ size_t aFrames, TrackRate aRate, uint32_t aChannels);
+ bool IsVoiceInput(MediaTrackGraphImpl* aGraph) const override {
+ // If we're passing data directly without AEC or any other process, this
+ // means that all voice-processing has been disabled intentionaly. In this
+ // case, consider that the device is not used for voice input.
+ return !PassThrough(aGraph);
+ }
+
+ void Start(MediaTrackGraphImpl* aGraph);
+ void Stop(MediaTrackGraphImpl* aGraph);
+
+ void DeviceChanged(MediaTrackGraphImpl* aGraph) override;
+
+ uint32_t RequestedInputChannelCount(MediaTrackGraphImpl*) override {
+ return GetRequestedInputChannelCount();
+ }
+
+ void Disconnect(MediaTrackGraphImpl* aGraph) override;
+
+ void PacketizeAndProcess(MediaTrackGraphImpl* aGraph,
+ const AudioSegment& aSegment);
+
+ void SetPassThrough(MediaTrackGraphImpl* aGraph, bool aPassThrough);
+ uint32_t GetRequestedInputChannelCount();
+ void SetRequestedInputChannelCount(MediaTrackGraphImpl* aGraph,
+ CubebUtils::AudioDeviceID aDeviceId,
+ uint32_t aRequestedInputChannelCount);
+ // This is true when all processing is disabled, we can skip
+ // packetization, resampling and other processing passes.
+ bool PassThrough(MediaTrackGraphImpl* aGraphImpl) const;
+
+ // This allow changing the APM options, enabling or disabling processing
+ // steps. The config gets applied the next time we're about to process input
+ // data.
+ void ApplyConfig(MediaTrackGraphImpl* aGraph,
+ const webrtc::AudioProcessing::Config& aConfig);
+
+ void End();
+
+ TrackTime NumBufferedFrames(MediaTrackGraphImpl* aGraph) const;
+
+ // The packet size contains samples in 10ms. The unit of aRate is hz.
+ constexpr static uint32_t GetPacketSize(TrackRate aRate) {
+ return static_cast<uint32_t>(aRate) / 100u;
+ }
+
+ bool IsEnded() const { return mEnded; }
+
+ private:
+ ~AudioInputProcessing() = default;
+ void EnsureAudioProcessing(MediaTrackGraphImpl* aGraph, uint32_t aChannels);
+ void ResetAudioProcessing(MediaTrackGraphImpl* aGraph);
+ PrincipalHandle GetCheckedPrincipal(const AudioSegment& aSegment);
+ // This implements the processing algoritm to apply to the input (e.g. a
+ // microphone). If all algorithms are disabled, this class in not used. This
+ // class only accepts audio chunks of 10ms. It has two inputs and one output:
+ // it is fed the speaker data and the microphone data. It outputs processed
+ // input data.
+ const UniquePtr<webrtc::AudioProcessing> mAudioProcessing;
+ // Packetizer to be able to feed 10ms packets to the input side of
+ // mAudioProcessing. Not used if the processing is bypassed.
+ Maybe<AudioPacketizer<AudioDataValue, float>> mPacketizerInput;
+ // Packetizer to be able to feed 10ms packets to the output side of
+ // mAudioProcessing. Not used if the processing is bypassed.
+ Maybe<AudioPacketizer<AudioDataValue, float>> mPacketizerOutput;
+ // The number of channels asked for by content, after clamping to the range of
+ // legal channel count for this particular device.
+ uint32_t mRequestedInputChannelCount;
+ // mSkipProcessing is true if none of the processing passes are enabled,
+ // because of prefs or constraints. This allows simply copying the audio into
+ // the MTG, skipping resampling and the whole webrtc.org code.
+ bool mSkipProcessing;
+ // Stores the mixed audio output for the reverse-stream of the AEC (the
+ // speaker data).
+ AlignedFloatBuffer mOutputBuffer;
+ // Stores the input audio, to be processed by the APM.
+ AlignedFloatBuffer mInputBuffer;
+ // Stores the deinterleaved microphone audio
+ AlignedFloatBuffer mDeinterleavedBuffer;
+ // Stores the mixed down input audio
+ AlignedFloatBuffer mInputDownmixBuffer;
+ // Stores data waiting to be pulled.
+ AudioSegment mSegment;
+ // Whether or not this MediaEngine is enabled. If it's not enabled, it
+ // operates in "pull" mode, and we append silence only, releasing the audio
+ // input track.
+ bool mEnabled;
+ // Whether or not we've ended and removed the AudioProcessingTrack.
+ bool mEnded;
+ // When processing is enabled, the number of packets received by this
+ // instance, to implement periodic logging.
+ uint64_t mPacketCount;
+ // A storage holding the interleaved audio data converted the AudioSegment.
+ // This will be used as an input parameter for PacketizeAndProcess. This
+ // should be removed once bug 1729041 is done.
+ AutoTArray<AudioDataValue,
+ SilentChannel::AUDIO_PROCESSING_FRAMES * GUESS_AUDIO_CHANNELS>
+ mInterleavedBuffer;
+ // Tracks the pending frames with paired principals piled up in packetizer.
+ std::deque<std::pair<TrackTime, PrincipalHandle>> mChunksInPacketizer;
+};
+
+// MediaTrack subclass tailored for MediaEngineWebRTCMicrophoneSource.
+class AudioProcessingTrack : public DeviceInputConsumerTrack {
+ // Only accessed on the graph thread.
+ RefPtr<AudioInputProcessing> mInputProcessing;
+
+ explicit AudioProcessingTrack(TrackRate aSampleRate)
+ : DeviceInputConsumerTrack(aSampleRate) {}
+
+ ~AudioProcessingTrack() = default;
+
+ public:
+ // Main Thread API
+ void Destroy() override;
+ void SetInputProcessing(RefPtr<AudioInputProcessing> aInputProcessing);
+ static AudioProcessingTrack* Create(MediaTrackGraph* aGraph);
+
+ // Graph Thread API
+ void DestroyImpl() override;
+ void ProcessInput(GraphTime aFrom, GraphTime aTo, uint32_t aFlags) override;
+ uint32_t NumberOfChannels() const override {
+ MOZ_DIAGNOSTIC_ASSERT(
+ mInputProcessing,
+ "Must set mInputProcessing before exposing to content");
+ return mInputProcessing->GetRequestedInputChannelCount();
+ }
+ // Pass the graph's mixed audio output to mInputProcessing for processing as
+ // the reverse stream.
+ void NotifyOutputData(MediaTrackGraphImpl* aGraph, AudioDataValue* aBuffer,
+ size_t aFrames, TrackRate aRate, uint32_t aChannels);
+
+ // Any thread
+ AudioProcessingTrack* AsAudioProcessingTrack() override { return this; }
+
+ private:
+ // Graph thread API
+ void SetInputProcessingImpl(RefPtr<AudioInputProcessing> aInputProcessing);
+};
+
+class MediaEngineWebRTCAudioCaptureSource : public MediaEngineSource {
+ public:
+ explicit MediaEngineWebRTCAudioCaptureSource(const MediaDevice* aMediaDevice);
+ static nsString GetUUID();
+ static nsString GetGroupId();
+ nsresult Allocate(const dom::MediaTrackConstraints& aConstraints,
+ const MediaEnginePrefs& aPrefs, uint64_t aWindowID,
+ const char** aOutBadConstraint) override {
+ // Nothing to do here, everything is managed in MediaManager.cpp
+ return NS_OK;
+ }
+ nsresult Deallocate() override {
+ // Nothing to do here, everything is managed in MediaManager.cpp
+ return NS_OK;
+ }
+ void SetTrack(const RefPtr<MediaTrack>& aTrack,
+ const PrincipalHandle& aPrincipal) override;
+ nsresult Start() override;
+ nsresult Stop() override;
+ nsresult Reconfigure(const dom::MediaTrackConstraints& aConstraints,
+ const MediaEnginePrefs& aPrefs,
+ const char** aOutBadConstraint) override;
+
+ nsresult TakePhoto(MediaEnginePhotoCallback* aCallback) override {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ void GetSettings(dom::MediaTrackSettings& aOutSettings) const override;
+
+ protected:
+ virtual ~MediaEngineWebRTCAudioCaptureSource() = default;
+};
+
+} // end namespace mozilla
+
+#endif // MediaEngineWebRTCAudio_h
diff --git a/dom/media/webrtc/MediaTrackConstraints.cpp b/dom/media/webrtc/MediaTrackConstraints.cpp
new file mode 100644
index 0000000000..101cd0240a
--- /dev/null
+++ b/dom/media/webrtc/MediaTrackConstraints.cpp
@@ -0,0 +1,560 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaTrackConstraints.h"
+
+#include <limits>
+#include <algorithm>
+#include <iterator>
+
+#include "MediaEngineSource.h"
+#include "mozilla/dom/MediaStreamTrackBinding.h"
+#include "mozilla/MediaManager.h"
+
+#ifdef MOZ_WEBRTC
+namespace mozilla {
+extern LazyLogModule gMediaManagerLog;
+}
+#else
+static mozilla::LazyLogModule gMediaManagerLog("MediaManager");
+#endif
+#define LOG(...) MOZ_LOG(gMediaManagerLog, LogLevel::Debug, (__VA_ARGS__))
+
+namespace mozilla {
+
+using dom::CallerType;
+using dom::ConstrainBooleanParameters;
+
+template <class ValueType>
+template <class ConstrainRange>
+void NormalizedConstraintSet::Range<ValueType>::SetFrom(
+ const ConstrainRange& aOther) {
+ if (aOther.mIdeal.WasPassed()) {
+ mIdeal.emplace(aOther.mIdeal.Value());
+ }
+ if (aOther.mExact.WasPassed()) {
+ mMin = aOther.mExact.Value();
+ mMax = aOther.mExact.Value();
+ } else {
+ if (aOther.mMin.WasPassed()) {
+ mMin = aOther.mMin.Value();
+ }
+ if (aOther.mMax.WasPassed()) {
+ mMax = aOther.mMax.Value();
+ }
+ }
+}
+
+// The Range code works surprisingly well for bool, except when averaging
+// ideals.
+template <>
+bool NormalizedConstraintSet::Range<bool>::Merge(const Range& aOther) {
+ if (!Intersects(aOther)) {
+ return false;
+ }
+ Intersect(aOther);
+
+ // To avoid "unsafe use of type 'bool'", we keep counter in mMergeDenominator
+ uint32_t counter = mMergeDenominator >> 16;
+ uint32_t denominator = mMergeDenominator & 0xffff;
+
+ if (aOther.mIdeal.isSome()) {
+ if (mIdeal.isNothing()) {
+ mIdeal.emplace(aOther.Get(false));
+ counter = aOther.Get(false);
+ denominator = 1;
+ } else {
+ if (!denominator) {
+ counter = Get(false);
+ denominator = 1;
+ }
+ counter += aOther.Get(false);
+ denominator++;
+ }
+ }
+ mMergeDenominator = ((counter & 0xffff) << 16) + (denominator & 0xffff);
+ return true;
+}
+
+template <>
+void NormalizedConstraintSet::Range<bool>::FinalizeMerge() {
+ if (mMergeDenominator) {
+ uint32_t counter = mMergeDenominator >> 16;
+ uint32_t denominator = mMergeDenominator & 0xffff;
+
+ *mIdeal = !!(counter / denominator);
+ mMergeDenominator = 0;
+ }
+}
+
+NormalizedConstraintSet::LongRange::LongRange(
+ LongPtrType aMemberPtr, const char* aName,
+ const dom::Optional<dom::OwningLongOrConstrainLongRange>& aOther,
+ bool advanced, nsTArray<MemberPtrType>* aList)
+ : Range<int32_t>((MemberPtrType)aMemberPtr, aName, 1 + INT32_MIN,
+ INT32_MAX, // +1 avoids Windows compiler bug
+ aList) {
+ if (!aOther.WasPassed()) {
+ return;
+ }
+ auto& other = aOther.Value();
+ if (other.IsLong()) {
+ if (advanced) {
+ mMin = mMax = other.GetAsLong();
+ } else {
+ mIdeal.emplace(other.GetAsLong());
+ }
+ } else {
+ SetFrom(other.GetAsConstrainLongRange());
+ }
+}
+
+NormalizedConstraintSet::LongLongRange::LongLongRange(
+ LongLongPtrType aMemberPtr, const char* aName, const long long& aOther,
+ nsTArray<MemberPtrType>* aList)
+ : Range<int64_t>((MemberPtrType)aMemberPtr, aName, 1 + INT64_MIN,
+ INT64_MAX, // +1 avoids Windows compiler bug
+ aList) {
+ mIdeal.emplace(aOther);
+}
+
+NormalizedConstraintSet::DoubleRange::DoubleRange(
+ DoublePtrType aMemberPtr, const char* aName,
+ const dom::Optional<dom::OwningDoubleOrConstrainDoubleRange>& aOther,
+ bool advanced, nsTArray<MemberPtrType>* aList)
+ : Range<double>((MemberPtrType)aMemberPtr, aName,
+ -std::numeric_limits<double>::infinity(),
+ std::numeric_limits<double>::infinity(), aList) {
+ if (!aOther.WasPassed()) {
+ return;
+ }
+ auto& other = aOther.Value();
+ if (other.IsDouble()) {
+ if (advanced) {
+ mMin = mMax = other.GetAsDouble();
+ } else {
+ mIdeal.emplace(other.GetAsDouble());
+ }
+ } else {
+ SetFrom(other.GetAsConstrainDoubleRange());
+ }
+}
+
+NormalizedConstraintSet::BooleanRange::BooleanRange(
+ BooleanPtrType aMemberPtr, const char* aName,
+ const dom::Optional<dom::OwningBooleanOrConstrainBooleanParameters>& aOther,
+ bool advanced, nsTArray<MemberPtrType>* aList)
+ : Range<bool>((MemberPtrType)aMemberPtr, aName, false, true, aList) {
+ if (!aOther.WasPassed()) {
+ return;
+ }
+ auto& other = aOther.Value();
+ if (other.IsBoolean()) {
+ if (advanced) {
+ mMin = mMax = other.GetAsBoolean();
+ } else {
+ mIdeal.emplace(other.GetAsBoolean());
+ }
+ } else {
+ auto& r = other.GetAsConstrainBooleanParameters();
+ if (r.mIdeal.WasPassed()) {
+ mIdeal.emplace(r.mIdeal.Value());
+ }
+ if (r.mExact.WasPassed()) {
+ mMin = r.mExact.Value();
+ mMax = r.mExact.Value();
+ }
+ }
+}
+
+NormalizedConstraintSet::StringRange::StringRange(
+ StringPtrType aMemberPtr, const char* aName,
+ const dom::Optional<
+ dom::OwningStringOrStringSequenceOrConstrainDOMStringParameters>&
+ aOther,
+ bool advanced, nsTArray<MemberPtrType>* aList)
+ : BaseRange((MemberPtrType)aMemberPtr, aName, aList) {
+ if (!aOther.WasPassed()) {
+ return;
+ }
+ auto& other = aOther.Value();
+ if (other.IsString()) {
+ if (advanced) {
+ mExact.insert(other.GetAsString());
+ } else {
+ mIdeal.insert(other.GetAsString());
+ }
+ } else if (other.IsStringSequence()) {
+ if (advanced) {
+ mExact.clear();
+ for (auto& str : other.GetAsStringSequence()) {
+ mExact.insert(str);
+ }
+ } else {
+ mIdeal.clear();
+ for (auto& str : other.GetAsStringSequence()) {
+ mIdeal.insert(str);
+ }
+ }
+ } else {
+ SetFrom(other.GetAsConstrainDOMStringParameters());
+ }
+}
+
+void NormalizedConstraintSet::StringRange::SetFrom(
+ const dom::ConstrainDOMStringParameters& aOther) {
+ if (aOther.mIdeal.WasPassed()) {
+ mIdeal.clear();
+ if (aOther.mIdeal.Value().IsString()) {
+ mIdeal.insert(aOther.mIdeal.Value().GetAsString());
+ } else {
+ for (auto& str : aOther.mIdeal.Value().GetAsStringSequence()) {
+ mIdeal.insert(str);
+ }
+ }
+ }
+ if (aOther.mExact.WasPassed()) {
+ mExact.clear();
+ if (aOther.mExact.Value().IsString()) {
+ mExact.insert(aOther.mExact.Value().GetAsString());
+ } else {
+ for (auto& str : aOther.mExact.Value().GetAsStringSequence()) {
+ mExact.insert(str);
+ }
+ }
+ }
+}
+
+auto NormalizedConstraintSet::StringRange::Clamp(const ValueType& n) const
+ -> ValueType {
+ if (mExact.empty()) {
+ return n;
+ }
+ ValueType result;
+ for (auto& entry : n) {
+ if (mExact.find(entry) != mExact.end()) {
+ result.insert(entry);
+ }
+ }
+ return result;
+}
+
+bool NormalizedConstraintSet::StringRange::Intersects(
+ const StringRange& aOther) const {
+ if (mExact.empty() || aOther.mExact.empty()) {
+ return true;
+ }
+
+ ValueType intersection;
+ set_intersection(mExact.begin(), mExact.end(), aOther.mExact.begin(),
+ aOther.mExact.end(),
+ std::inserter(intersection, intersection.begin()));
+ return !intersection.empty();
+}
+
+void NormalizedConstraintSet::StringRange::Intersect(
+ const StringRange& aOther) {
+ if (aOther.mExact.empty()) {
+ return;
+ }
+
+ ValueType intersection;
+ set_intersection(mExact.begin(), mExact.end(), aOther.mExact.begin(),
+ aOther.mExact.end(),
+ std::inserter(intersection, intersection.begin()));
+ mExact = intersection;
+}
+
+bool NormalizedConstraintSet::StringRange::Merge(const StringRange& aOther) {
+ if (!Intersects(aOther)) {
+ return false;
+ }
+ Intersect(aOther);
+
+ ValueType unioned;
+ set_union(mIdeal.begin(), mIdeal.end(), aOther.mIdeal.begin(),
+ aOther.mIdeal.end(), std::inserter(unioned, unioned.begin()));
+ mIdeal = unioned;
+ return true;
+}
+
+NormalizedConstraints::NormalizedConstraints(
+ const dom::MediaTrackConstraints& aOther, nsTArray<MemberPtrType>* aList)
+ : NormalizedConstraintSet(aOther, false, aList), mBadConstraint(nullptr) {
+ if (aOther.mAdvanced.WasPassed()) {
+ for (auto& entry : aOther.mAdvanced.Value()) {
+ mAdvanced.push_back(NormalizedConstraintSet(entry, true));
+ }
+ }
+}
+
+FlattenedConstraints::FlattenedConstraints(const NormalizedConstraints& aOther)
+ : NormalizedConstraintSet(aOther) {
+ for (auto& set : aOther.mAdvanced) {
+ // Must only apply compatible i.e. inherently non-overconstraining sets
+ // This rule is pretty much why this code is centralized here.
+ if (mWidth.Intersects(set.mWidth) && mHeight.Intersects(set.mHeight) &&
+ mFrameRate.Intersects(set.mFrameRate)) {
+ mWidth.Intersect(set.mWidth);
+ mHeight.Intersect(set.mHeight);
+ mFrameRate.Intersect(set.mFrameRate);
+ }
+ if (mEchoCancellation.Intersects(set.mEchoCancellation)) {
+ mEchoCancellation.Intersect(set.mEchoCancellation);
+ }
+ if (mNoiseSuppression.Intersects(set.mNoiseSuppression)) {
+ mNoiseSuppression.Intersect(set.mNoiseSuppression);
+ }
+ if (mAutoGainControl.Intersects(set.mAutoGainControl)) {
+ mAutoGainControl.Intersect(set.mAutoGainControl);
+ }
+ if (mChannelCount.Intersects(set.mChannelCount)) {
+ mChannelCount.Intersect(set.mChannelCount);
+ }
+ }
+}
+
+// MediaEngine helper
+//
+// The full algorithm for all devices.
+//
+// Fitness distance returned as integer math * 1000. Infinity = UINT32_MAX
+
+// First, all devices have a minimum distance based on their deviceId.
+// If you have no other constraints, use this one. Reused by all device types.
+
+/* static */
+bool MediaConstraintsHelper::SomeSettingsFit(
+ const NormalizedConstraints& aConstraints,
+ const nsTArray<RefPtr<LocalMediaDevice>>& aDevices) {
+ nsTArray<const NormalizedConstraintSet*> sets;
+ sets.AppendElement(&aConstraints);
+
+ MOZ_ASSERT(!aDevices.IsEmpty());
+ for (auto& device : aDevices) {
+ auto distance = device->GetBestFitnessDistance(sets, CallerType::NonSystem);
+ if (distance != UINT32_MAX) {
+ return true;
+ }
+ }
+ return false;
+}
+
+// Fitness distance returned as integer math * 1000. Infinity = UINT32_MAX
+
+/* static */
+uint32_t MediaConstraintsHelper::FitnessDistance(
+ const Maybe<nsString>& aN,
+ const NormalizedConstraintSet::StringRange& aParams) {
+ if (!aParams.mExact.empty() &&
+ (aN.isNothing() || aParams.mExact.find(*aN) == aParams.mExact.end())) {
+ return UINT32_MAX;
+ }
+ if (!aParams.mIdeal.empty() &&
+ (aN.isNothing() || aParams.mIdeal.find(*aN) == aParams.mIdeal.end())) {
+ return 1000;
+ }
+ return 0;
+}
+
+/* static */ const char* MediaConstraintsHelper::SelectSettings(
+ const NormalizedConstraints& aConstraints,
+ nsTArray<RefPtr<LocalMediaDevice>>& aDevices, CallerType aCallerType) {
+ auto& c = aConstraints;
+ LogConstraints(c);
+
+ // First apply top-level constraints.
+
+ // Stack constraintSets that pass, starting with the required one, because the
+ // whole stack must be re-satisfied each time a capability-set is ruled out
+ // (this avoids storing state or pushing algorithm into the lower-level code).
+ nsTArray<RefPtr<LocalMediaDevice>> unsatisfactory;
+ nsTArray<const NormalizedConstraintSet*> aggregateConstraints;
+ aggregateConstraints.AppendElement(&c);
+
+ std::multimap<uint32_t, RefPtr<LocalMediaDevice>> ordered;
+
+ for (uint32_t i = 0; i < aDevices.Length();) {
+ uint32_t distance =
+ aDevices[i]->GetBestFitnessDistance(aggregateConstraints, aCallerType);
+ if (distance == UINT32_MAX) {
+ unsatisfactory.AppendElement(std::move(aDevices[i]));
+ aDevices.RemoveElementAt(i);
+ } else {
+ ordered.insert(std::make_pair(distance, aDevices[i]));
+ ++i;
+ }
+ }
+ if (aDevices.IsEmpty()) {
+ return FindBadConstraint(c, unsatisfactory);
+ }
+
+ // Order devices by shortest distance
+ for (auto& ordinal : ordered) {
+ aDevices.RemoveElement(ordinal.second);
+ aDevices.AppendElement(ordinal.second);
+ }
+
+ // Then apply advanced constraints.
+
+ for (const auto& advanced : c.mAdvanced) {
+ aggregateConstraints.AppendElement(&advanced);
+ nsTArray<RefPtr<LocalMediaDevice>> rejects;
+ for (uint32_t j = 0; j < aDevices.Length();) {
+ uint32_t distance = aDevices[j]->GetBestFitnessDistance(
+ aggregateConstraints, aCallerType);
+ if (distance == UINT32_MAX) {
+ rejects.AppendElement(std::move(aDevices[j]));
+ aDevices.RemoveElementAt(j);
+ } else {
+ ++j;
+ }
+ }
+ if (aDevices.IsEmpty()) {
+ aDevices.AppendElements(std::move(rejects));
+ aggregateConstraints.RemoveLastElement();
+ }
+ }
+ return nullptr;
+}
+
+/* static */ const char* MediaConstraintsHelper::FindBadConstraint(
+ const NormalizedConstraints& aConstraints,
+ const nsTArray<RefPtr<LocalMediaDevice>>& aDevices) {
+ // The spec says to report a constraint that satisfies NONE
+ // of the sources. Unfortunately, this is a bit laborious to find out, and
+ // requires updating as new constraints are added!
+ auto& c = aConstraints;
+ dom::MediaTrackConstraints empty;
+
+ if (aDevices.IsEmpty() ||
+ !SomeSettingsFit(NormalizedConstraints(empty), aDevices)) {
+ return "";
+ }
+ {
+ NormalizedConstraints fresh(empty);
+ fresh.mDeviceId = c.mDeviceId;
+ if (!SomeSettingsFit(fresh, aDevices)) {
+ return "deviceId";
+ }
+ }
+ {
+ NormalizedConstraints fresh(empty);
+ fresh.mGroupId = c.mGroupId;
+ if (!SomeSettingsFit(fresh, aDevices)) {
+ return "groupId";
+ }
+ }
+ {
+ NormalizedConstraints fresh(empty);
+ fresh.mWidth = c.mWidth;
+ if (!SomeSettingsFit(fresh, aDevices)) {
+ return "width";
+ }
+ }
+ {
+ NormalizedConstraints fresh(empty);
+ fresh.mHeight = c.mHeight;
+ if (!SomeSettingsFit(fresh, aDevices)) {
+ return "height";
+ }
+ }
+ {
+ NormalizedConstraints fresh(empty);
+ fresh.mFrameRate = c.mFrameRate;
+ if (!SomeSettingsFit(fresh, aDevices)) {
+ return "frameRate";
+ }
+ }
+ {
+ NormalizedConstraints fresh(empty);
+ fresh.mFacingMode = c.mFacingMode;
+ if (!SomeSettingsFit(fresh, aDevices)) {
+ return "facingMode";
+ }
+ }
+ return "";
+}
+
+/* static */
+const char* MediaConstraintsHelper::FindBadConstraint(
+ const NormalizedConstraints& aConstraints,
+ const MediaDevice* aMediaDevice) {
+ NormalizedConstraints c(aConstraints);
+ NormalizedConstraints empty((dom::MediaTrackConstraints()));
+ c.mDeviceId = empty.mDeviceId;
+ c.mGroupId = empty.mGroupId;
+ AutoTArray<RefPtr<LocalMediaDevice>, 1> devices;
+ devices.EmplaceBack(
+ new LocalMediaDevice(aMediaDevice, u""_ns, u""_ns, u""_ns));
+ return FindBadConstraint(c, devices);
+}
+
+static void LogConstraintStringRange(
+ const NormalizedConstraintSet::StringRange& aRange) {
+ if (aRange.mExact.size() <= 1 && aRange.mIdeal.size() <= 1) {
+ LOG(" %s: { exact: [%s], ideal: [%s] }", aRange.mName,
+ (aRange.mExact.empty()
+ ? ""
+ : NS_ConvertUTF16toUTF8(*aRange.mExact.begin()).get()),
+ (aRange.mIdeal.empty()
+ ? ""
+ : NS_ConvertUTF16toUTF8(*aRange.mIdeal.begin()).get()));
+ } else {
+ LOG(" %s: { exact: [", aRange.mName);
+ for (auto& entry : aRange.mExact) {
+ LOG(" %s,", NS_ConvertUTF16toUTF8(entry).get());
+ }
+ LOG(" ], ideal: [");
+ for (auto& entry : aRange.mIdeal) {
+ LOG(" %s,", NS_ConvertUTF16toUTF8(entry).get());
+ }
+ LOG(" ]}");
+ }
+}
+
+template <typename T>
+static void LogConstraintRange(
+ const NormalizedConstraintSet::Range<T>& aRange) {
+ if (aRange.mIdeal.isSome()) {
+ LOG(" %s: { min: %d, max: %d, ideal: %d }", aRange.mName, aRange.mMin,
+ aRange.mMax, aRange.mIdeal.valueOr(0));
+ } else {
+ LOG(" %s: { min: %d, max: %d }", aRange.mName, aRange.mMin, aRange.mMax);
+ }
+}
+
+template <>
+void LogConstraintRange(const NormalizedConstraintSet::Range<double>& aRange) {
+ if (aRange.mIdeal.isSome()) {
+ LOG(" %s: { min: %f, max: %f, ideal: %f }", aRange.mName, aRange.mMin,
+ aRange.mMax, aRange.mIdeal.valueOr(0));
+ } else {
+ LOG(" %s: { min: %f, max: %f }", aRange.mName, aRange.mMin, aRange.mMax);
+ }
+}
+
+/* static */
+void MediaConstraintsHelper::LogConstraints(
+ const NormalizedConstraintSet& aConstraints) {
+ auto& c = aConstraints;
+ LOG("Constraints: {");
+ LOG("%s", [&]() {
+ LogConstraintRange(c.mWidth);
+ LogConstraintRange(c.mHeight);
+ LogConstraintRange(c.mFrameRate);
+ LogConstraintStringRange(c.mMediaSource);
+ LogConstraintStringRange(c.mFacingMode);
+ LogConstraintStringRange(c.mDeviceId);
+ LogConstraintStringRange(c.mGroupId);
+ LogConstraintRange(c.mEchoCancellation);
+ LogConstraintRange(c.mAutoGainControl);
+ LogConstraintRange(c.mNoiseSuppression);
+ LogConstraintRange(c.mChannelCount);
+ return "}";
+ }());
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/MediaTrackConstraints.h b/dom/media/webrtc/MediaTrackConstraints.h
new file mode 100644
index 0000000000..61e0ed85ea
--- /dev/null
+++ b/dom/media/webrtc/MediaTrackConstraints.h
@@ -0,0 +1,371 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file should not be included by other includes, as it contains code
+
+#ifndef MEDIATRACKCONSTRAINTS_H_
+#define MEDIATRACKCONSTRAINTS_H_
+
+#include <map>
+#include <set>
+#include <vector>
+
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/MediaStreamTrackBinding.h"
+#include "mozilla/dom/MediaTrackSupportedConstraintsBinding.h"
+
+namespace mozilla {
+
+class LocalMediaDevice;
+class MediaDevice;
+
+template <class EnumValuesStrings, class Enum>
+static Enum StringToEnum(const EnumValuesStrings& aStrings,
+ const nsAString& aValue, Enum aDefaultValue) {
+ for (size_t i = 0; aStrings[i].value; i++) {
+ if (aValue.EqualsASCII(aStrings[i].value)) {
+ return Enum(i);
+ }
+ }
+ return aDefaultValue;
+}
+
+// Helper classes for orthogonal constraints without interdependencies.
+// Instead of constraining values, constrain the constraints themselves.
+class NormalizedConstraintSet {
+ protected:
+ class BaseRange {
+ protected:
+ typedef BaseRange NormalizedConstraintSet::*MemberPtrType;
+
+ BaseRange(MemberPtrType aMemberPtr, const char* aName,
+ nsTArray<MemberPtrType>* aList)
+ : mName(aName) {
+ if (aList) {
+ aList->AppendElement(aMemberPtr);
+ }
+ }
+ virtual ~BaseRange() = default;
+
+ public:
+ virtual bool Merge(const BaseRange& aOther) = 0;
+ virtual void FinalizeMerge() = 0;
+
+ const char* mName;
+ };
+
+ typedef BaseRange NormalizedConstraintSet::*MemberPtrType;
+
+ public:
+ template <class ValueType>
+ class Range : public BaseRange {
+ public:
+ ValueType mMin, mMax;
+ Maybe<ValueType> mIdeal;
+
+ Range(MemberPtrType aMemberPtr, const char* aName, ValueType aMin,
+ ValueType aMax, nsTArray<MemberPtrType>* aList)
+ : BaseRange(aMemberPtr, aName, aList),
+ mMin(aMin),
+ mMax(aMax),
+ mMergeDenominator(0) {}
+ virtual ~Range() = default;
+
+ template <class ConstrainRange>
+ void SetFrom(const ConstrainRange& aOther);
+ ValueType Clamp(ValueType n) const {
+ return std::max(mMin, std::min(n, mMax));
+ }
+ ValueType Get(ValueType defaultValue) const {
+ return Clamp(mIdeal.valueOr(defaultValue));
+ }
+ bool Intersects(const Range& aOther) const {
+ return mMax >= aOther.mMin && mMin <= aOther.mMax;
+ }
+ void Intersect(const Range& aOther) {
+ mMin = std::max(mMin, aOther.mMin);
+ if (Intersects(aOther)) {
+ mMax = std::min(mMax, aOther.mMax);
+ } else {
+ // If there is no intersection, we will down-scale or drop frame
+ mMax = std::max(mMax, aOther.mMax);
+ }
+ }
+ bool Merge(const Range& aOther) {
+ if (strcmp(mName, "width") != 0 && strcmp(mName, "height") != 0 &&
+ strcmp(mName, "frameRate") != 0 && !Intersects(aOther)) {
+ return false;
+ }
+ Intersect(aOther);
+
+ if (aOther.mIdeal.isSome()) {
+ // Ideal values, as stored, may be outside their min max range, so use
+ // clamped values in averaging, to avoid extreme outliers skewing
+ // results.
+ if (mIdeal.isNothing()) {
+ mIdeal.emplace(aOther.Get(0));
+ mMergeDenominator = 1;
+ } else {
+ if (!mMergeDenominator) {
+ *mIdeal = Get(0);
+ mMergeDenominator = 1;
+ }
+ *mIdeal += aOther.Get(0);
+ mMergeDenominator++;
+ }
+ }
+ return true;
+ }
+ void FinalizeMerge() override {
+ if (mMergeDenominator) {
+ *mIdeal /= mMergeDenominator;
+ mMergeDenominator = 0;
+ }
+ }
+ void TakeHighestIdeal(const Range& aOther) {
+ if (aOther.mIdeal.isSome()) {
+ if (mIdeal.isNothing()) {
+ mIdeal.emplace(aOther.Get(0));
+ } else {
+ *mIdeal = std::max(Get(0), aOther.Get(0));
+ }
+ }
+ }
+
+ private:
+ bool Merge(const BaseRange& aOther) override {
+ return Merge(static_cast<const Range&>(aOther));
+ }
+
+ uint32_t mMergeDenominator;
+ };
+
+ struct LongRange : public Range<int32_t> {
+ typedef LongRange NormalizedConstraintSet::*LongPtrType;
+
+ LongRange(LongPtrType aMemberPtr, const char* aName,
+ const dom::Optional<dom::OwningLongOrConstrainLongRange>& aOther,
+ bool advanced, nsTArray<MemberPtrType>* aList);
+ };
+
+ struct LongLongRange : public Range<int64_t> {
+ typedef LongLongRange NormalizedConstraintSet::*LongLongPtrType;
+
+ LongLongRange(LongLongPtrType aMemberPtr, const char* aName,
+ const long long& aOther, nsTArray<MemberPtrType>* aList);
+ };
+
+ struct DoubleRange : public Range<double> {
+ typedef DoubleRange NormalizedConstraintSet::*DoublePtrType;
+
+ DoubleRange(
+ DoublePtrType aMemberPtr, const char* aName,
+ const dom::Optional<dom::OwningDoubleOrConstrainDoubleRange>& aOther,
+ bool advanced, nsTArray<MemberPtrType>* aList);
+ };
+
+ struct BooleanRange : public Range<bool> {
+ typedef BooleanRange NormalizedConstraintSet::*BooleanPtrType;
+
+ BooleanRange(
+ BooleanPtrType aMemberPtr, const char* aName,
+ const dom::Optional<dom::OwningBooleanOrConstrainBooleanParameters>&
+ aOther,
+ bool advanced, nsTArray<MemberPtrType>* aList);
+
+ BooleanRange(BooleanPtrType aMemberPtr, const char* aName,
+ const bool& aOther, nsTArray<MemberPtrType>* aList)
+ : Range<bool>((MemberPtrType)aMemberPtr, aName, false, true, aList) {
+ mIdeal.emplace(aOther);
+ }
+ };
+
+ struct StringRange : public BaseRange {
+ typedef std::set<nsString> ValueType;
+ ValueType mExact, mIdeal;
+
+ typedef StringRange NormalizedConstraintSet::*StringPtrType;
+
+ StringRange(
+ StringPtrType aMemberPtr, const char* aName,
+ const dom::Optional<
+ dom::OwningStringOrStringSequenceOrConstrainDOMStringParameters>&
+ aOther,
+ bool advanced, nsTArray<MemberPtrType>* aList);
+
+ StringRange(StringPtrType aMemberPtr, const char* aName,
+ const dom::Optional<nsString>& aOther,
+ nsTArray<MemberPtrType>* aList)
+ : BaseRange((MemberPtrType)aMemberPtr, aName, aList) {
+ if (aOther.WasPassed()) {
+ mIdeal.insert(aOther.Value());
+ }
+ }
+
+ ~StringRange() = default;
+
+ void SetFrom(const dom::ConstrainDOMStringParameters& aOther);
+ ValueType Clamp(const ValueType& n) const;
+ ValueType Get(const ValueType& defaultValue) const {
+ return Clamp(mIdeal.empty() ? defaultValue : mIdeal);
+ }
+ bool Intersects(const StringRange& aOther) const;
+ void Intersect(const StringRange& aOther);
+ bool Merge(const StringRange& aOther);
+ void FinalizeMerge() override {}
+
+ private:
+ bool Merge(const BaseRange& aOther) override {
+ return Merge(static_cast<const StringRange&>(aOther));
+ }
+ };
+
+ // All new constraints should be added here whether they use flattening or not
+ LongRange mWidth, mHeight;
+ DoubleRange mFrameRate;
+ StringRange mFacingMode;
+ StringRange mMediaSource;
+ LongLongRange mBrowserWindow;
+ StringRange mDeviceId;
+ StringRange mGroupId;
+ LongRange mViewportOffsetX, mViewportOffsetY, mViewportWidth, mViewportHeight;
+ BooleanRange mEchoCancellation, mNoiseSuppression, mAutoGainControl;
+ LongRange mChannelCount;
+
+ private:
+ typedef NormalizedConstraintSet T;
+
+ public:
+ NormalizedConstraintSet(const dom::MediaTrackConstraintSet& aOther,
+ bool advanced,
+ nsTArray<MemberPtrType>* aList = nullptr)
+ : mWidth(&T::mWidth, "width", aOther.mWidth, advanced, aList),
+ mHeight(&T::mHeight, "height", aOther.mHeight, advanced, aList),
+ mFrameRate(&T::mFrameRate, "frameRate", aOther.mFrameRate, advanced,
+ aList),
+ mFacingMode(&T::mFacingMode, "facingMode", aOther.mFacingMode, advanced,
+ aList),
+ mMediaSource(&T::mMediaSource, "mediaSource", aOther.mMediaSource,
+ aList),
+ mBrowserWindow(&T::mBrowserWindow, "browserWindow",
+ aOther.mBrowserWindow.WasPassed()
+ ? aOther.mBrowserWindow.Value()
+ : 0,
+ aList),
+ mDeviceId(&T::mDeviceId, "deviceId", aOther.mDeviceId, advanced, aList),
+ mGroupId(&T::mGroupId, "groupId", aOther.mGroupId, advanced, aList),
+ mViewportOffsetX(&T::mViewportOffsetX, "viewportOffsetX",
+ aOther.mViewportOffsetX, advanced, aList),
+ mViewportOffsetY(&T::mViewportOffsetY, "viewportOffsetY",
+ aOther.mViewportOffsetY, advanced, aList),
+ mViewportWidth(&T::mViewportWidth, "viewportWidth",
+ aOther.mViewportWidth, advanced, aList),
+ mViewportHeight(&T::mViewportHeight, "viewportHeight",
+ aOther.mViewportHeight, advanced, aList),
+ mEchoCancellation(&T::mEchoCancellation, "echoCancellation",
+ aOther.mEchoCancellation, advanced, aList),
+ mNoiseSuppression(&T::mNoiseSuppression, "noiseSuppression",
+ aOther.mNoiseSuppression, advanced, aList),
+ mAutoGainControl(&T::mAutoGainControl, "autoGainControl",
+ aOther.mAutoGainControl, advanced, aList),
+ mChannelCount(&T::mChannelCount, "channelCount", aOther.mChannelCount,
+ advanced, aList) {}
+};
+
+template <>
+bool NormalizedConstraintSet::Range<bool>::Merge(const Range& aOther);
+template <>
+void NormalizedConstraintSet::Range<bool>::FinalizeMerge();
+
+// Used instead of MediaTrackConstraints in lower-level code.
+struct NormalizedConstraints : public NormalizedConstraintSet {
+ explicit NormalizedConstraints(const dom::MediaTrackConstraints& aOther,
+ nsTArray<MemberPtrType>* aList = nullptr);
+
+ std::vector<NormalizedConstraintSet> mAdvanced;
+ const char* mBadConstraint;
+};
+
+// Flattened version is used in low-level code with orthogonal constraints only.
+struct FlattenedConstraints : public NormalizedConstraintSet {
+ explicit FlattenedConstraints(const NormalizedConstraints& aOther);
+
+ explicit FlattenedConstraints(const dom::MediaTrackConstraints& aOther)
+ : FlattenedConstraints(NormalizedConstraints(aOther)) {}
+};
+
+// A helper class for MediaEngineSources
+class MediaConstraintsHelper {
+ public:
+ template <class ValueType, class NormalizedRange>
+ static uint32_t FitnessDistance(ValueType aN, const NormalizedRange& aRange) {
+ if (aRange.mMin > aN || aRange.mMax < aN) {
+ return UINT32_MAX;
+ }
+ if (aN == aRange.mIdeal.valueOr(aN)) {
+ return 0;
+ }
+ return uint32_t(
+ ValueType((std::abs(aN - aRange.mIdeal.value()) * 1000) /
+ std::max(std::abs(aN), std::abs(aRange.mIdeal.value()))));
+ }
+
+ template <class ValueType, class NormalizedRange>
+ static uint32_t FeasibilityDistance(ValueType aN,
+ const NormalizedRange& aRange) {
+ if (aRange.mMin > aN) {
+ return UINT32_MAX;
+ }
+ // We prefer larger resolution because now we support downscaling
+ if (aN == aRange.mIdeal.valueOr(aN)) {
+ return 0;
+ }
+
+ if (aN > aRange.mIdeal.value()) {
+ return uint32_t(
+ ValueType((std::abs(aN - aRange.mIdeal.value()) * 1000) /
+ std::max(std::abs(aN), std::abs(aRange.mIdeal.value()))));
+ }
+
+ return 10000 +
+ uint32_t(ValueType(
+ (std::abs(aN - aRange.mIdeal.value()) * 1000) /
+ std::max(std::abs(aN), std::abs(aRange.mIdeal.value()))));
+ }
+
+ static uint32_t FitnessDistance(
+ const Maybe<nsString>& aN,
+ const NormalizedConstraintSet::StringRange& aParams);
+
+ protected:
+ static bool SomeSettingsFit(
+ const NormalizedConstraints& aConstraints,
+ const nsTArray<RefPtr<LocalMediaDevice>>& aDevices);
+
+ public:
+ static uint32_t GetMinimumFitnessDistance(
+ const NormalizedConstraintSet& aConstraints, const nsString& aDeviceId,
+ const nsString& aGroupId);
+
+ // Apply constrains to a supplied list of devices (removes items from the
+ // list)
+ static const char* SelectSettings(
+ const NormalizedConstraints& aConstraints,
+ nsTArray<RefPtr<LocalMediaDevice>>& aDevices,
+ dom::CallerType aCallerType);
+
+ static const char* FindBadConstraint(
+ const NormalizedConstraints& aConstraints,
+ const nsTArray<RefPtr<LocalMediaDevice>>& aDevices);
+
+ static const char* FindBadConstraint(
+ const NormalizedConstraints& aConstraints,
+ const MediaDevice* aMediaDevice);
+
+ static void LogConstraints(const NormalizedConstraintSet& aConstraints);
+};
+
+} // namespace mozilla
+
+#endif /* MEDIATRACKCONSTRAINTS_H_ */
diff --git a/dom/media/webrtc/MediaTransportChild.h b/dom/media/webrtc/MediaTransportChild.h
new file mode 100644
index 0000000000..b84dce70b2
--- /dev/null
+++ b/dom/media/webrtc/MediaTransportChild.h
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _MTRANSPORTCHILD_H__
+#define _MTRANSPORTCHILD_H__
+
+#include "mozilla/dom/PMediaTransportChild.h"
+
+namespace mozilla {
+class MediaTransportHandlerIPC;
+
+class MediaTransportChild : public dom::PMediaTransportChild {
+ public:
+#ifdef MOZ_WEBRTC
+ explicit MediaTransportChild(MediaTransportHandlerIPC* aUser);
+ virtual ~MediaTransportChild();
+ mozilla::ipc::IPCResult RecvOnCandidate(const string& transportId,
+ const CandidateInfo& candidateInfo);
+ mozilla::ipc::IPCResult RecvOnAlpnNegotiated(const string& alpn);
+ mozilla::ipc::IPCResult RecvOnGatheringStateChange(const int& state);
+ mozilla::ipc::IPCResult RecvOnConnectionStateChange(const int& state);
+ mozilla::ipc::IPCResult RecvOnPacketReceived(const string& transportId,
+ const MediaPacket& packet);
+ mozilla::ipc::IPCResult RecvOnEncryptedSending(const string& transportId,
+ const MediaPacket& packet);
+ mozilla::ipc::IPCResult RecvOnStateChange(const string& transportId,
+ const int& state);
+ mozilla::ipc::IPCResult RecvOnRtcpStateChange(const string& transportId,
+ const int& state);
+
+ private:
+ RefPtr<MediaTransportHandlerIPC> mUser;
+#endif // MOZ_WEBRTC
+};
+
+} // namespace mozilla
+#endif
diff --git a/dom/media/webrtc/MediaTransportParent.h b/dom/media/webrtc/MediaTransportParent.h
new file mode 100644
index 0000000000..8c698d0f54
--- /dev/null
+++ b/dom/media/webrtc/MediaTransportParent.h
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _MTRANSPORTHANDLER_PARENT_H__
+#define _MTRANSPORTHANDLER_PARENT_H__
+
+#include "mozilla/dom/PMediaTransportParent.h"
+#include <memory>
+
+namespace mozilla {
+
+class MediaTransportParent : public dom::PMediaTransportParent {
+ public:
+#ifdef MOZ_WEBRTC
+ MediaTransportParent();
+ virtual ~MediaTransportParent();
+
+ mozilla::ipc::IPCResult RecvGetIceLog(const nsCString& pattern,
+ GetIceLogResolver&& aResolve);
+ mozilla::ipc::IPCResult RecvClearIceLog();
+ mozilla::ipc::IPCResult RecvEnterPrivateMode();
+ mozilla::ipc::IPCResult RecvExitPrivateMode();
+ mozilla::ipc::IPCResult RecvCreateIceCtx(const string& name);
+ mozilla::ipc::IPCResult RecvSetIceConfig(
+ nsTArray<RTCIceServer>&& iceServers,
+ const RTCIceTransportPolicy& icePolicy);
+ mozilla::ipc::IPCResult RecvSetProxyConfig(
+ const net::WebrtcProxyConfig& aProxyConfig);
+ mozilla::ipc::IPCResult RecvEnsureProvisionalTransport(
+ const string& transportId, const string& localUfrag,
+ const string& localPwd, const int& componentCount);
+ mozilla::ipc::IPCResult RecvSetTargetForDefaultLocalAddressLookup(
+ const string& targetIp, uint16_t targetPort);
+ mozilla::ipc::IPCResult RecvStartIceGathering(
+ const bool& defaultRouteOnly, const bool& obfuscateAddresses,
+ const net::NrIceStunAddrArray& stunAddrs);
+ mozilla::ipc::IPCResult RecvActivateTransport(
+ const string& transportId, const string& localUfrag,
+ const string& localPwd, const int& componentCount,
+ const string& remoteUfrag, const string& remotePwd,
+ nsTArray<uint8_t>&& keyDer, nsTArray<uint8_t>&& certDer,
+ const int& authType, const bool& dtlsClient,
+ const DtlsDigestList& digests, const bool& privacyRequested);
+ mozilla::ipc::IPCResult RecvRemoveTransportsExcept(
+ const StringVector& transportIds);
+ mozilla::ipc::IPCResult RecvStartIceChecks(const bool& isControlling,
+ const StringVector& iceOptions);
+ mozilla::ipc::IPCResult RecvSendPacket(const string& transportId,
+ MediaPacket&& packet);
+ mozilla::ipc::IPCResult RecvAddIceCandidate(const string& transportId,
+ const string& candidate,
+ const string& ufrag,
+ const string& obfuscatedAddress);
+ mozilla::ipc::IPCResult RecvUpdateNetworkState(const bool& online);
+ mozilla::ipc::IPCResult RecvGetIceStats(const string& transportId,
+ const double& now,
+ GetIceStatsResolver&& aResolve);
+
+ void ActorDestroy(ActorDestroyReason aWhy);
+
+ private:
+ // Hide the sigslot/MediaTransportHandler stuff from IPC.
+ class Impl;
+ std::unique_ptr<Impl> mImpl;
+#endif // MOZ_WEBRTC
+};
+} // namespace mozilla
+#endif //_MTRANSPORTHANDLER_PARENT_H__
diff --git a/dom/media/webrtc/PMediaTransport.ipdl b/dom/media/webrtc/PMediaTransport.ipdl
new file mode 100644
index 0000000000..b754d34ad2
--- /dev/null
+++ b/dom/media/webrtc/PMediaTransport.ipdl
@@ -0,0 +1,105 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PBackground;
+
+#ifdef MOZ_WEBRTC
+include WebrtcProxyConfig;
+
+// ParamTraits stuff for generated code and other classes we don't want to change
+include "mozilla/net/NrIceStunAddrMessageUtils.h";
+include "mozilla/media/webrtc/WebrtcIPCTraits.h";
+using mozilla::StringVector from "mozilla/media/webrtc/WebrtcIPCTraits.h";
+using mozilla::CandidateInfo from "mozilla/media/webrtc/WebrtcIPCTraits.h";
+using mozilla::DtlsDigestList from "mozilla/media/webrtc/WebrtcIPCTraits.h";
+using std::string from "string";
+using struct mozilla::dom::RTCStatsCollection from "mozilla/dom/RTCStatsReportBinding.h";
+using WebrtcGlobalLog from "mozilla/media/webrtc/WebrtcGlobal.h";
+using mozilla::dom::RTCIceServer from "mozilla/dom/RTCConfigurationBinding.h";
+using mozilla::dom::RTCIceTransportPolicy from "mozilla/dom/RTCConfigurationBinding.h";
+
+// ParamTraits stuff for our own classes
+using mozilla::MediaPacket from "transport/mediapacket.h";
+include "mozilla/net/NrIceStunAddrMessageUtils.h";
+using mozilla::net::NrIceStunAddrArray from "mozilla/net/PStunAddrsParams.h";
+#endif // MOZ_WEBRTC
+
+namespace mozilla {
+namespace dom {
+
+[ManualDealloc]
+async protocol PMediaTransport {
+ manager PBackground;
+
+parent:
+ async __delete__();
+
+#ifdef MOZ_WEBRTC
+ async GetIceLog(nsCString pattern) returns (WebrtcGlobalLog loglines);
+ async ClearIceLog();
+ async EnterPrivateMode();
+ async ExitPrivateMode();
+
+ async CreateIceCtx(string name);
+
+ async SetIceConfig(RTCIceServer[] iceServers,
+ RTCIceTransportPolicy icePolicy);
+
+ async SetProxyConfig(WebrtcProxyConfig proxyConfig);
+
+ async EnsureProvisionalTransport(string transportId,
+ string localUfrag,
+ string localPwd,
+ int componentCount);
+
+ async SetTargetForDefaultLocalAddressLookup(string targetIp,
+ uint16_t targetPort);
+
+ async StartIceGathering(bool defaultRouteOnly,
+ bool obfuscateHostAddresses,
+ NrIceStunAddrArray stunAddrs);
+
+ async ActivateTransport(string transportId,
+ string localUfrag,
+ string localPwd,
+ int componentCount,
+ string remoteUfrag,
+ string remotePwd,
+ uint8_t[] keyDer,
+ uint8_t[] certDer,
+ int authType,
+ bool dtlsClient,
+ DtlsDigestList digests,
+ bool privacyRequested);
+
+ async RemoveTransportsExcept(StringVector transportIds);
+
+ async StartIceChecks(bool isControlling,
+ StringVector iceOptions);
+
+ async SendPacket(string transportId, MediaPacket packet);
+
+ async AddIceCandidate(string transportId,
+ string candidate,
+ string ufrag,
+ string obfuscatedAddr);
+
+ async UpdateNetworkState(bool online);
+
+ async GetIceStats(string transportId, double now) returns (UniquePtr<RTCStatsCollection> stats);
+
+child:
+ async OnCandidate(string transportId, CandidateInfo candidateInfo);
+ async OnAlpnNegotiated(string alpn);
+ async OnGatheringStateChange(int state);
+ async OnConnectionStateChange(int state);
+ async OnPacketReceived(string transportId, MediaPacket packet);
+ async OnEncryptedSending(string transportId, MediaPacket packet);
+ async OnStateChange(string transportId, int state);
+ async OnRtcpStateChange(string transportId, int state);
+
+#endif // MOZ_WEBRTC
+};
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/media/webrtc/PWebrtcGlobal.ipdl b/dom/media/webrtc/PWebrtcGlobal.ipdl
new file mode 100644
index 0000000000..e274b23636
--- /dev/null
+++ b/dom/media/webrtc/PWebrtcGlobal.ipdl
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PContent;
+
+#ifdef MOZ_WEBRTC
+include "mozilla/media/webrtc/WebrtcGlobal.h";
+
+using struct mozilla::dom::RTCStatsReportInternal from "mozilla/dom/RTCStatsReportBinding.h";
+using WebrtcGlobalLog from "mozilla/media/webrtc/WebrtcGlobal.h";
+#endif
+
+namespace mozilla {
+namespace dom {
+
+[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual]
+async protocol PWebrtcGlobal {
+ manager PContent;
+
+parent: // child -> parent messages
+ async __delete__();
+#ifdef MOZ_WEBRTC
+ async PeerConnectionCreated(nsString aPcId, bool aIsLongTermStatsDisabled);
+ async PeerConnectionDestroyed(nsString aPcId);
+ async PeerConnectionFinalStats(RTCStatsReportInternal aFinalStats);
+
+child: // parent -> child messages
+ async GetStats(nsString aPcIdFilter) returns (RTCStatsReportInternal[] stats);
+ async ClearStats();
+ async GetLog() returns (WebrtcGlobalLog logs);
+ async ClearLog();
+ async SetAecLogging(bool aEnable);
+ async SetDebugMode(int aLevel);
+#endif
+};
+
+} // end namespace net
+} // end namespace mozilla
+
+
diff --git a/dom/media/webrtc/PeerIdentity.cpp b/dom/media/webrtc/PeerIdentity.cpp
new file mode 100644
index 0000000000..d36fea5ea3
--- /dev/null
+++ b/dom/media/webrtc/PeerIdentity.cpp
@@ -0,0 +1,80 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 sts=2 expandtab
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "PeerIdentity.h"
+
+#include "mozilla/DebugOnly.h"
+#include "nsCOMPtr.h"
+#include "nsIIDNService.h"
+#include "nsNetCID.h"
+#include "nsServiceManagerUtils.h"
+
+namespace mozilla {
+
+bool PeerIdentity::Equals(const PeerIdentity& aOther) const {
+ return Equals(aOther.mPeerIdentity);
+}
+
+bool PeerIdentity::Equals(const nsAString& aOtherString) const {
+ nsString user;
+ GetUser(mPeerIdentity, user);
+ nsString otherUser;
+ GetUser(aOtherString, otherUser);
+ if (user != otherUser) {
+ return false;
+ }
+
+ nsString host;
+ GetHost(mPeerIdentity, host);
+ nsString otherHost;
+ GetHost(aOtherString, otherHost);
+
+ nsresult rv;
+ nsCOMPtr<nsIIDNService> idnService =
+ do_GetService("@mozilla.org/network/idn-service;1", &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return host == otherHost;
+ }
+
+ nsCString normHost;
+ GetNormalizedHost(idnService, host, normHost);
+ nsCString normOtherHost;
+ GetNormalizedHost(idnService, otherHost, normOtherHost);
+ return normHost == normOtherHost;
+}
+
+/* static */
+void PeerIdentity::GetUser(const nsAString& aPeerIdentity, nsAString& aUser) {
+ int32_t at = aPeerIdentity.FindChar('@');
+ if (at >= 0) {
+ aUser = Substring(aPeerIdentity, 0, at);
+ } else {
+ aUser.Truncate();
+ }
+}
+
+/* static */
+void PeerIdentity::GetHost(const nsAString& aPeerIdentity, nsAString& aHost) {
+ int32_t at = aPeerIdentity.FindChar('@');
+ if (at >= 0) {
+ aHost = Substring(aPeerIdentity, at + 1);
+ } else {
+ aHost = aPeerIdentity;
+ }
+}
+
+/* static */
+void PeerIdentity::GetNormalizedHost(const nsCOMPtr<nsIIDNService>& aIdnService,
+ const nsAString& aHost,
+ nsACString& aNormalizedHost) {
+ const nsCString chost = NS_ConvertUTF16toUTF8(aHost);
+ DebugOnly<nsresult> rv =
+ aIdnService->ConvertUTF8toACE(chost, aNormalizedHost);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "Failed to convert UTF-8 host to ASCII");
+}
+
+} /* namespace mozilla */
diff --git a/dom/media/webrtc/PeerIdentity.h b/dom/media/webrtc/PeerIdentity.h
new file mode 100644
index 0000000000..64364d3359
--- /dev/null
+++ b/dom/media/webrtc/PeerIdentity.h
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=2 sts=2 expandtab
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef PeerIdentity_h
+#define PeerIdentity_h
+
+#include "nsISupportsImpl.h"
+#include "nsString.h"
+
+template <class T>
+class nsCOMPtr;
+class nsIIDNService;
+
+namespace mozilla {
+
+/**
+ * This class implements the identifier used in WebRTC identity. Peers are
+ * identified using a string in the form [<user>@]<domain>, for instance,
+ * "user@example.com'. The (optional) user portion is a site-controlled string
+ * containing any character other than '@'. The domain portion is a valid IDN
+ * domain name and is compared accordingly.
+ *
+ * See:
+ * http://tools.ietf.org/html/draft-ietf-rtcweb-security-arch-09#section-5.6.5.3.3.1
+ */
+class PeerIdentity final {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(PeerIdentity)
+
+ explicit PeerIdentity(const nsAString& aPeerIdentity)
+ : mPeerIdentity(aPeerIdentity) {}
+
+ bool Equals(const PeerIdentity& aOther) const;
+ bool Equals(const nsAString& aOtherString) const;
+ const nsString& ToString() const { return mPeerIdentity; }
+
+ private:
+ ~PeerIdentity() = default;
+ static void GetUser(const nsAString& aPeerIdentity, nsAString& aUser);
+ static void GetHost(const nsAString& aPeerIdentity, nsAString& aHost);
+
+ static void GetNormalizedHost(const nsCOMPtr<nsIIDNService>& aIdnService,
+ const nsAString& aHost,
+ nsACString& aNormalizedHost);
+
+ nsString mPeerIdentity;
+};
+
+inline bool operator==(const PeerIdentity& aOne, const PeerIdentity& aTwo) {
+ return aOne.Equals(aTwo);
+}
+
+inline bool operator==(const PeerIdentity& aOne, const nsAString& aString) {
+ return aOne.Equals(aString);
+}
+
+inline bool operator!=(const PeerIdentity& aOne, const PeerIdentity& aTwo) {
+ return !aOne.Equals(aTwo);
+}
+
+inline bool operator!=(const PeerIdentity& aOne, const nsAString& aString) {
+ return !aOne.Equals(aString);
+}
+
+} /* namespace mozilla */
+
+#endif /* PeerIdentity_h */
diff --git a/dom/media/webrtc/RTCCertificate.cpp b/dom/media/webrtc/RTCCertificate.cpp
new file mode 100644
index 0000000000..69b6b0f563
--- /dev/null
+++ b/dom/media/webrtc/RTCCertificate.cpp
@@ -0,0 +1,438 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/RTCCertificate.h"
+
+#include <cstdio>
+#include <cstring>
+#include <memory>
+#include <new>
+#include <utility>
+#include "ErrorList.h"
+#include "MainThreadUtils.h"
+#include "cert.h"
+#include "cryptohi.h"
+#include "js/StructuredClone.h"
+#include "js/TypeDecls.h"
+#include "js/Value.h"
+#include "keyhi.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/MacroForEach.h"
+#include "mozilla/OwningNonNull.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/CryptoBuffer.h"
+#include "mozilla/dom/CryptoKey.h"
+#include "mozilla/dom/KeyAlgorithmBinding.h"
+#include "mozilla/dom/KeyAlgorithmProxy.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/RTCCertificateBinding.h"
+#include "mozilla/dom/StructuredCloneHolder.h"
+#include "mozilla/dom/SubtleCryptoBinding.h"
+#include "mozilla/dom/UnionTypes.h"
+#include "mozilla/dom/WebCryptoCommon.h"
+#include "mozilla/dom/WebCryptoTask.h"
+#include "mozilla/fallible.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsLiteralString.h"
+#include "nsStringFlags.h"
+#include "nsStringFwd.h"
+#include "nsTLiteralString.h"
+#include "pk11pub.h"
+#include "plarena.h"
+#include "secasn1.h"
+#include "secasn1t.h"
+#include "seccomon.h"
+#include "secmodt.h"
+#include "secoid.h"
+#include "secoidt.h"
+#include "transport/dtlsidentity.h"
+#include "xpcpublic.h"
+
+namespace mozilla::dom {
+
+#define RTCCERTIFICATE_SC_VERSION 0x00000001
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(RTCCertificate, mGlobal)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(RTCCertificate)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(RTCCertificate)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(RTCCertificate)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+// Note: explicit casts necessary to avoid
+// warning C4307: '*' : integral constant overflow
+#define ONE_DAY \
+ PRTime(PR_USEC_PER_SEC) * PRTime(60) /*sec*/ \
+ * PRTime(60) /*min*/ * PRTime(24) /*hours*/
+#define EXPIRATION_DEFAULT ONE_DAY* PRTime(30)
+#define EXPIRATION_SLACK ONE_DAY
+#define EXPIRATION_MAX ONE_DAY* PRTime(365) /*year*/
+
+const size_t RTCCertificateCommonNameLength = 16;
+const size_t RTCCertificateMinRsaSize = 1024;
+
+class GenerateRTCCertificateTask : public GenerateAsymmetricKeyTask {
+ public:
+ GenerateRTCCertificateTask(nsIGlobalObject* aGlobal, JSContext* aCx,
+ const ObjectOrString& aAlgorithm,
+ const Sequence<nsString>& aKeyUsages,
+ PRTime aExpires)
+ : GenerateAsymmetricKeyTask(aGlobal, aCx, aAlgorithm, true, aKeyUsages),
+ mExpires(aExpires),
+ mAuthType(ssl_kea_null),
+ mCertificate(nullptr),
+ mSignatureAlg(SEC_OID_UNKNOWN) {
+ if (NS_FAILED(mEarlyRv)) {
+ // webrtc-pc says to throw NotSupportedError if we have passed "an
+ // algorithm that the user agent cannot or will not use to generate a
+ // certificate". This catches these cases.
+ mEarlyRv = NS_ERROR_DOM_NOT_SUPPORTED_ERR;
+ }
+ }
+
+ private:
+ PRTime mExpires;
+ SSLKEAType mAuthType;
+ UniqueCERTCertificate mCertificate;
+ SECOidTag mSignatureAlg;
+
+ static CERTName* GenerateRandomName(PK11SlotInfo* aSlot) {
+ uint8_t randomName[RTCCertificateCommonNameLength];
+ SECStatus rv =
+ PK11_GenerateRandomOnSlot(aSlot, randomName, sizeof(randomName));
+ if (rv != SECSuccess) {
+ return nullptr;
+ }
+
+ char buf[sizeof(randomName) * 2 + 4];
+ strncpy(buf, "CN=", 4);
+ for (size_t i = 0; i < sizeof(randomName); ++i) {
+ snprintf(&buf[i * 2 + 3], 3, "%.2x", randomName[i]);
+ }
+ buf[sizeof(buf) - 1] = '\0';
+
+ return CERT_AsciiToName(buf);
+ }
+
+ nsresult GenerateCertificate() {
+ UniquePK11SlotInfo slot(PK11_GetInternalSlot());
+ MOZ_ASSERT(slot.get());
+
+ UniqueCERTName subjectName(GenerateRandomName(slot.get()));
+ if (!subjectName) {
+ return NS_ERROR_DOM_UNKNOWN_ERR;
+ }
+
+ UniqueSECKEYPublicKey publicKey(mKeyPair->mPublicKey->GetPublicKey());
+ UniqueCERTSubjectPublicKeyInfo spki(
+ SECKEY_CreateSubjectPublicKeyInfo(publicKey.get()));
+ if (!spki) {
+ return NS_ERROR_DOM_UNKNOWN_ERR;
+ }
+
+ UniqueCERTCertificateRequest certreq(
+ CERT_CreateCertificateRequest(subjectName.get(), spki.get(), nullptr));
+ if (!certreq) {
+ return NS_ERROR_DOM_UNKNOWN_ERR;
+ }
+
+ PRTime now = PR_Now();
+ PRTime notBefore = now - EXPIRATION_SLACK;
+ mExpires += now;
+
+ UniqueCERTValidity validity(CERT_CreateValidity(notBefore, mExpires));
+ if (!validity) {
+ return NS_ERROR_DOM_UNKNOWN_ERR;
+ }
+
+ unsigned long serial;
+ // Note: This serial in principle could collide, but it's unlikely, and we
+ // don't expect anyone to be validating certificates anyway.
+ SECStatus rv = PK11_GenerateRandomOnSlot(
+ slot.get(), reinterpret_cast<unsigned char*>(&serial), sizeof(serial));
+ if (rv != SECSuccess) {
+ return NS_ERROR_DOM_UNKNOWN_ERR;
+ }
+
+ // NB: CERTCertificates created with CERT_CreateCertificate are not safe to
+ // use with other NSS functions like CERT_DupCertificate. The strategy
+ // here is to create a tbsCertificate ("to-be-signed certificate"), encode
+ // it, and sign it, resulting in a signed DER certificate that can be
+ // decoded into a CERTCertificate.
+ UniqueCERTCertificate tbsCertificate(CERT_CreateCertificate(
+ serial, subjectName.get(), validity.get(), certreq.get()));
+ if (!tbsCertificate) {
+ return NS_ERROR_DOM_UNKNOWN_ERR;
+ }
+
+ MOZ_ASSERT(mSignatureAlg != SEC_OID_UNKNOWN);
+ PLArenaPool* arena = tbsCertificate->arena;
+
+ rv = SECOID_SetAlgorithmID(arena, &tbsCertificate->signature, mSignatureAlg,
+ nullptr);
+ if (rv != SECSuccess) {
+ return NS_ERROR_DOM_UNKNOWN_ERR;
+ }
+
+ // Set version to X509v3.
+ *(tbsCertificate->version.data) = SEC_CERTIFICATE_VERSION_3;
+ tbsCertificate->version.len = 1;
+
+ SECItem innerDER = {siBuffer, nullptr, 0};
+ if (!SEC_ASN1EncodeItem(arena, &innerDER, tbsCertificate.get(),
+ SEC_ASN1_GET(CERT_CertificateTemplate))) {
+ return NS_ERROR_DOM_UNKNOWN_ERR;
+ }
+
+ SECItem* certDer = PORT_ArenaZNew(arena, SECItem);
+ if (!certDer) {
+ return NS_ERROR_DOM_UNKNOWN_ERR;
+ }
+
+ UniqueSECKEYPrivateKey privateKey(mKeyPair->mPrivateKey->GetPrivateKey());
+ rv = SEC_DerSignData(arena, certDer, innerDER.data, innerDER.len,
+ privateKey.get(), mSignatureAlg);
+ if (rv != SECSuccess) {
+ return NS_ERROR_DOM_UNKNOWN_ERR;
+ }
+
+ mCertificate.reset(CERT_NewTempCertificate(CERT_GetDefaultCertDB(), certDer,
+ nullptr, false, true));
+ if (!mCertificate) {
+ return NS_ERROR_DOM_UNKNOWN_ERR;
+ }
+ return NS_OK;
+ }
+
+ nsresult BeforeCrypto() override {
+ if (mAlgName.EqualsLiteral(WEBCRYPTO_ALG_RSASSA_PKCS1)) {
+ // Double check that size is OK.
+ auto sz = static_cast<size_t>(mRsaParams.keySizeInBits);
+ if (sz < RTCCertificateMinRsaSize) {
+ return NS_ERROR_DOM_NOT_SUPPORTED_ERR;
+ }
+
+ KeyAlgorithmProxy& alg = mKeyPair->mPublicKey->Algorithm();
+ if (alg.mType != KeyAlgorithmProxy::RSA ||
+ !alg.mRsa.mHash.mName.EqualsLiteral(WEBCRYPTO_ALG_SHA256)) {
+ return NS_ERROR_DOM_NOT_SUPPORTED_ERR;
+ }
+
+ mSignatureAlg = SEC_OID_PKCS1_SHA256_WITH_RSA_ENCRYPTION;
+ mAuthType = ssl_kea_rsa;
+
+ } else if (mAlgName.EqualsLiteral(WEBCRYPTO_ALG_ECDSA)) {
+ // We only support good curves in WebCrypto.
+ // If that ever changes, check that a good one was chosen.
+
+ mSignatureAlg = SEC_OID_ANSIX962_ECDSA_SHA256_SIGNATURE;
+ mAuthType = ssl_kea_ecdh;
+ } else {
+ return NS_ERROR_DOM_NOT_SUPPORTED_ERR;
+ }
+ return NS_OK;
+ }
+
+ nsresult DoCrypto() override {
+ nsresult rv = GenerateAsymmetricKeyTask::DoCrypto();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ rv = GenerateCertificate();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ return NS_OK;
+ }
+
+ virtual void Resolve() override {
+ // Make copies of the private key and certificate, otherwise, when this
+ // object is deleted, the structures they reference will be deleted too.
+ UniqueSECKEYPrivateKey key = mKeyPair->mPrivateKey->GetPrivateKey();
+ CERTCertificate* cert = CERT_DupCertificate(mCertificate.get());
+ RefPtr<RTCCertificate> result =
+ new RTCCertificate(mResultPromise->GetParentObject(), key.release(),
+ cert, mAuthType, mExpires);
+ mResultPromise->MaybeResolve(result);
+ }
+};
+
+static PRTime ReadExpires(JSContext* aCx, const ObjectOrString& aOptions,
+ ErrorResult& aRv) {
+ // This conversion might fail, but we don't really care; use the default.
+ // If this isn't an object, or it doesn't coerce into the right type,
+ // then we won't get the |expires| value. Either will be caught later.
+ RTCCertificateExpiration expiration;
+ if (!aOptions.IsObject()) {
+ return EXPIRATION_DEFAULT;
+ }
+ JS::Rooted<JS::Value> value(aCx, JS::ObjectValue(*aOptions.GetAsObject()));
+ if (!expiration.Init(aCx, value)) {
+ aRv.NoteJSContextException(aCx);
+ return 0;
+ }
+
+ if (!expiration.mExpires.WasPassed()) {
+ return EXPIRATION_DEFAULT;
+ }
+ static const uint64_t max =
+ static_cast<uint64_t>(EXPIRATION_MAX / PR_USEC_PER_MSEC);
+ if (expiration.mExpires.Value() > max) {
+ return EXPIRATION_MAX;
+ }
+ return static_cast<PRTime>(expiration.mExpires.Value() * PR_USEC_PER_MSEC);
+}
+
+already_AddRefed<Promise> RTCCertificate::GenerateCertificate(
+ const GlobalObject& aGlobal, const ObjectOrString& aOptions,
+ ErrorResult& aRv, JS::Compartment* aCompartment) {
+ nsIGlobalObject* global = xpc::NativeGlobal(aGlobal.Get());
+ RefPtr<Promise> p = Promise::Create(global, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+ Sequence<nsString> usages;
+ if (!usages.AppendElement(u"sign"_ns, fallible)) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return nullptr;
+ }
+
+ PRTime expires = ReadExpires(aGlobal.Context(), aOptions, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+ RefPtr<WebCryptoTask> task = new GenerateRTCCertificateTask(
+ global, aGlobal.Context(), aOptions, usages, expires);
+ task->DispatchWithPromise(p);
+ return p.forget();
+}
+
+RTCCertificate::RTCCertificate(nsIGlobalObject* aGlobal)
+ : mGlobal(aGlobal),
+ mPrivateKey(nullptr),
+ mCertificate(nullptr),
+ mAuthType(ssl_kea_null),
+ mExpires(0) {}
+
+RTCCertificate::RTCCertificate(nsIGlobalObject* aGlobal,
+ SECKEYPrivateKey* aPrivateKey,
+ CERTCertificate* aCertificate,
+ SSLKEAType aAuthType, PRTime aExpires)
+ : mGlobal(aGlobal),
+ mPrivateKey(aPrivateKey),
+ mCertificate(aCertificate),
+ mAuthType(aAuthType),
+ mExpires(aExpires) {}
+
+RefPtr<DtlsIdentity> RTCCertificate::CreateDtlsIdentity() const {
+ if (!mPrivateKey || !mCertificate) {
+ return nullptr;
+ }
+ UniqueSECKEYPrivateKey key(SECKEY_CopyPrivateKey(mPrivateKey.get()));
+ UniqueCERTCertificate cert(CERT_DupCertificate(mCertificate.get()));
+ RefPtr<DtlsIdentity> id =
+ new DtlsIdentity(std::move(key), std::move(cert), mAuthType);
+ return id;
+}
+
+JSObject* RTCCertificate::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return RTCCertificate_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+bool RTCCertificate::WritePrivateKey(JSStructuredCloneWriter* aWriter) const {
+ JsonWebKey jwk;
+ nsresult rv = CryptoKey::PrivateKeyToJwk(mPrivateKey.get(), jwk);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+ nsString json;
+ if (!jwk.ToJSON(json)) {
+ return false;
+ }
+ return StructuredCloneHolder::WriteString(aWriter, json);
+}
+
+bool RTCCertificate::WriteCertificate(JSStructuredCloneWriter* aWriter) const {
+ UniqueCERTCertificateList certs(CERT_CertListFromCert(mCertificate.get()));
+ if (!certs || certs->len <= 0) {
+ return false;
+ }
+ if (!JS_WriteUint32Pair(aWriter, certs->certs[0].len, 0)) {
+ return false;
+ }
+ return JS_WriteBytes(aWriter, certs->certs[0].data, certs->certs[0].len);
+}
+
+bool RTCCertificate::WriteStructuredClone(
+ JSContext* aCx, JSStructuredCloneWriter* aWriter) const {
+ if (!mPrivateKey || !mCertificate) {
+ return false;
+ }
+
+ return JS_WriteUint32Pair(aWriter, RTCCERTIFICATE_SC_VERSION, mAuthType) &&
+ JS_WriteUint32Pair(aWriter, (mExpires >> 32) & 0xffffffff,
+ mExpires & 0xffffffff) &&
+ WritePrivateKey(aWriter) && WriteCertificate(aWriter);
+}
+
+bool RTCCertificate::ReadPrivateKey(JSStructuredCloneReader* aReader) {
+ nsString json;
+ if (!StructuredCloneHolder::ReadString(aReader, json)) {
+ return false;
+ }
+ JsonWebKey jwk;
+ if (!jwk.Init(json)) {
+ return false;
+ }
+ mPrivateKey = CryptoKey::PrivateKeyFromJwk(jwk);
+ return !!mPrivateKey;
+}
+
+bool RTCCertificate::ReadCertificate(JSStructuredCloneReader* aReader) {
+ CryptoBuffer cert;
+ if (!ReadBuffer(aReader, cert) || cert.Length() == 0) {
+ return false;
+ }
+
+ SECItem der = {siBuffer, cert.Elements(),
+ static_cast<unsigned int>(cert.Length())};
+ mCertificate.reset(CERT_NewTempCertificate(CERT_GetDefaultCertDB(), &der,
+ nullptr, true, true));
+ return !!mCertificate;
+}
+
+// static
+already_AddRefed<RTCCertificate> RTCCertificate::ReadStructuredClone(
+ JSContext* aCx, nsIGlobalObject* aGlobal,
+ JSStructuredCloneReader* aReader) {
+ if (!NS_IsMainThread()) {
+ // These objects are mainthread-only.
+ return nullptr;
+ }
+ uint32_t version, authType;
+ if (!JS_ReadUint32Pair(aReader, &version, &authType) ||
+ version != RTCCERTIFICATE_SC_VERSION) {
+ return nullptr;
+ }
+ RefPtr<RTCCertificate> cert = new RTCCertificate(aGlobal);
+ cert->mAuthType = static_cast<SSLKEAType>(authType);
+
+ uint32_t high, low;
+ if (!JS_ReadUint32Pair(aReader, &high, &low)) {
+ return nullptr;
+ }
+ cert->mExpires = static_cast<PRTime>(high) << 32 | low;
+
+ if (!cert->ReadPrivateKey(aReader) || !cert->ReadCertificate(aReader)) {
+ return nullptr;
+ }
+
+ return cert.forget();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webrtc/RTCCertificate.h b/dom/media/webrtc/RTCCertificate.h
new file mode 100644
index 0000000000..a5cc6bde32
--- /dev/null
+++ b/dom/media/webrtc/RTCCertificate.h
@@ -0,0 +1,98 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_RTCCertificate_h
+#define mozilla_dom_RTCCertificate_h
+
+#include <cstdint>
+#include "ScopedNSSTypes.h"
+#include "certt.h"
+#include "js/RootingAPI.h"
+#include "keythi.h"
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/RefPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIGlobalObject.h"
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+#include "prtime.h"
+#include "sslt.h"
+
+class JSObject;
+struct JSContext;
+struct JSStructuredCloneReader;
+struct JSStructuredCloneWriter;
+
+namespace JS {
+class Compartment;
+}
+
+namespace mozilla {
+class DtlsIdentity;
+class ErrorResult;
+
+namespace dom {
+
+class GlobalObject;
+class ObjectOrString;
+class Promise;
+
+class RTCCertificate final : public nsISupports, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(RTCCertificate)
+
+ // WebIDL method that implements RTCPeerConnection.generateCertificate.
+ static already_AddRefed<Promise> GenerateCertificate(
+ const GlobalObject& aGlobal, const ObjectOrString& aOptions,
+ ErrorResult& aRv, JS::Compartment* aCompartment = nullptr);
+
+ explicit RTCCertificate(nsIGlobalObject* aGlobal);
+ RTCCertificate(nsIGlobalObject* aGlobal, SECKEYPrivateKey* aPrivateKey,
+ CERTCertificate* aCertificate, SSLKEAType aAuthType,
+ PRTime aExpires);
+
+ nsIGlobalObject* GetParentObject() const { return mGlobal; }
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // WebIDL expires attribute. Note: JS dates are milliseconds since epoch;
+ // NSPR PRTime is in microseconds since the same epoch.
+ uint64_t Expires() const { return mExpires / PR_USEC_PER_MSEC; }
+
+ // Accessors for use by PeerConnectionImpl.
+ RefPtr<DtlsIdentity> CreateDtlsIdentity() const;
+ const UniqueCERTCertificate& Certificate() const { return mCertificate; }
+
+ // Structured clone methods
+ bool WriteStructuredClone(JSContext* aCx,
+ JSStructuredCloneWriter* aWriter) const;
+ static already_AddRefed<RTCCertificate> ReadStructuredClone(
+ JSContext* aCx, nsIGlobalObject* aGlobal,
+ JSStructuredCloneReader* aReader);
+
+ private:
+ ~RTCCertificate() = default;
+ void operator=(const RTCCertificate&) = delete;
+ RTCCertificate(const RTCCertificate&) = delete;
+
+ bool ReadCertificate(JSStructuredCloneReader* aReader);
+ bool ReadPrivateKey(JSStructuredCloneReader* aReader);
+ bool WriteCertificate(JSStructuredCloneWriter* aWriter) const;
+ bool WritePrivateKey(JSStructuredCloneWriter* aWriter) const;
+
+ RefPtr<nsIGlobalObject> mGlobal;
+ UniqueSECKEYPrivateKey mPrivateKey;
+ UniqueCERTCertificate mCertificate;
+ SSLKEAType mAuthType;
+ PRTime mExpires;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_RTCCertificate_h
diff --git a/dom/media/webrtc/RTCIdentityProviderRegistrar.cpp b/dom/media/webrtc/RTCIdentityProviderRegistrar.cpp
new file mode 100644
index 0000000000..44e4c0d775
--- /dev/null
+++ b/dom/media/webrtc/RTCIdentityProviderRegistrar.cpp
@@ -0,0 +1,70 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "RTCIdentityProviderRegistrar.h"
+#include "mozilla/Attributes.h"
+#include "nsCycleCollectionParticipant.h"
+
+namespace mozilla::dom {
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(RTCIdentityProviderRegistrar)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(RTCIdentityProviderRegistrar)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(RTCIdentityProviderRegistrar)
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(RTCIdentityProviderRegistrar, mGlobal,
+ mGenerateAssertionCallback,
+ mValidateAssertionCallback)
+
+RTCIdentityProviderRegistrar::RTCIdentityProviderRegistrar(
+ nsIGlobalObject* aGlobal)
+ : mGlobal(aGlobal),
+ mGenerateAssertionCallback(nullptr),
+ mValidateAssertionCallback(nullptr) {}
+
+RTCIdentityProviderRegistrar::~RTCIdentityProviderRegistrar() = default;
+
+nsIGlobalObject* RTCIdentityProviderRegistrar::GetParentObject() const {
+ return mGlobal;
+}
+
+JSObject* RTCIdentityProviderRegistrar::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return RTCIdentityProviderRegistrar_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void RTCIdentityProviderRegistrar::Register(const RTCIdentityProvider& aIdp) {
+ mGenerateAssertionCallback = aIdp.mGenerateAssertion;
+ mValidateAssertionCallback = aIdp.mValidateAssertion;
+}
+
+bool RTCIdentityProviderRegistrar::HasIdp() const {
+ return mGenerateAssertionCallback && mValidateAssertionCallback;
+}
+
+already_AddRefed<Promise> RTCIdentityProviderRegistrar::GenerateAssertion(
+ const nsAString& aContents, const nsAString& aOrigin,
+ const RTCIdentityProviderOptions& aOptions, ErrorResult& aRv) {
+ if (!mGenerateAssertionCallback) {
+ aRv.Throw(NS_ERROR_NOT_INITIALIZED);
+ return nullptr;
+ }
+ RefPtr<GenerateAssertionCallback> callback(mGenerateAssertionCallback);
+ return callback->Call(aContents, aOrigin, aOptions, aRv);
+}
+already_AddRefed<Promise> RTCIdentityProviderRegistrar::ValidateAssertion(
+ const nsAString& aAssertion, const nsAString& aOrigin, ErrorResult& aRv) {
+ if (!mValidateAssertionCallback) {
+ aRv.Throw(NS_ERROR_NOT_INITIALIZED);
+ return nullptr;
+ }
+ RefPtr<ValidateAssertionCallback> callback(mValidateAssertionCallback);
+ return callback->Call(aAssertion, aOrigin, aRv);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webrtc/RTCIdentityProviderRegistrar.h b/dom/media/webrtc/RTCIdentityProviderRegistrar.h
new file mode 100644
index 0000000000..0510471f17
--- /dev/null
+++ b/dom/media/webrtc/RTCIdentityProviderRegistrar.h
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef RTCIDENTITYPROVIDER_H_
+#define RTCIDENTITYPROVIDER_H_
+
+#include "mozilla/RefPtr.h"
+#include "nsCOMPtr.h"
+#include "nsISupportsImpl.h"
+#include "nsIGlobalObject.h"
+#include "nsWrapperCache.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/RTCIdentityProviderBinding.h"
+
+namespace mozilla::dom {
+
+struct RTCIdentityProvider;
+
+class RTCIdentityProviderRegistrar final : public nsISupports,
+ public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(RTCIdentityProviderRegistrar)
+
+ explicit RTCIdentityProviderRegistrar(nsIGlobalObject* aGlobal);
+
+ // As required
+ nsIGlobalObject* GetParentObject() const;
+ virtual JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // setter and checker
+ void Register(const RTCIdentityProvider& aIdp);
+ bool HasIdp() const;
+
+ MOZ_CAN_RUN_SCRIPT
+ already_AddRefed<Promise> GenerateAssertion(
+ const nsAString& aContents, const nsAString& aOrigin,
+ const RTCIdentityProviderOptions& aOptions, ErrorResult& aRv);
+ MOZ_CAN_RUN_SCRIPT
+ already_AddRefed<Promise> ValidateAssertion(const nsAString& assertion,
+ const nsAString& origin,
+ ErrorResult& aRv);
+
+ private:
+ ~RTCIdentityProviderRegistrar();
+
+ nsCOMPtr<nsIGlobalObject> mGlobal;
+ RefPtr<GenerateAssertionCallback> mGenerateAssertionCallback;
+ RefPtr<ValidateAssertionCallback> mValidateAssertionCallback;
+};
+
+} // namespace mozilla::dom
+
+#endif /* RTCIDENTITYPROVIDER_H_ */
diff --git a/dom/media/webrtc/SineWaveGenerator.h b/dom/media/webrtc/SineWaveGenerator.h
new file mode 100644
index 0000000000..73bab53f19
--- /dev/null
+++ b/dom/media/webrtc/SineWaveGenerator.h
@@ -0,0 +1,58 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef SINEWAVEGENERATOR_H_
+#define SINEWAVEGENERATOR_H_
+
+#include "MediaSegment.h"
+#include "prtime.h"
+
+namespace mozilla {
+
+// generate 1k sine wave per second
+template <typename Sample>
+class SineWaveGenerator {
+ static_assert(std::is_same<Sample, int16_t>::value ||
+ std::is_same<Sample, float>::value);
+
+ public:
+ static const int bytesPerSample = sizeof(Sample);
+ static const int millisecondsPerSecond = PR_MSEC_PER_SEC;
+ static constexpr float twopi = 2 * M_PI;
+
+ /* If more than 1 channel, generated samples are interleaved. */
+ SineWaveGenerator(uint32_t aSampleRate, uint32_t aFrequency)
+ : mPhase(0.), mPhaseIncrement(twopi * aFrequency / aSampleRate) {}
+
+ // NOTE: only safely called from a single thread (MTG callback)
+ void generate(Sample* aBuffer, TrackTicks aFrameCount,
+ uint32_t aChannelCount = 1) {
+ while (aFrameCount--) {
+ Sample value = sin(mPhase) * Amplitude();
+ for (uint32_t channel = 0; channel < aChannelCount; channel++) {
+ *aBuffer++ = value;
+ }
+ mPhase += mPhaseIncrement;
+ if (mPhase > twopi) {
+ mPhase -= twopi;
+ }
+ }
+ }
+
+ static float Amplitude() {
+ // Set volume to -20db.
+ if (std::is_same<Sample, int16_t>::value) {
+ return 3276.8; // 32768.0 * 10^(-20/20) = 3276.8
+ }
+ return 0.1f; // 1.0 * 10^(-20/20) = 0.1
+ }
+
+ private:
+ double mPhase;
+ const double mPhaseIncrement;
+};
+
+} // namespace mozilla
+
+#endif /* SINEWAVEGENERATOR_H_ */
diff --git a/dom/media/webrtc/WebrtcGlobal.h b/dom/media/webrtc/WebrtcGlobal.h
new file mode 100644
index 0000000000..dac51dcf6b
--- /dev/null
+++ b/dom/media/webrtc/WebrtcGlobal.h
@@ -0,0 +1,506 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _WEBRTC_GLOBAL_H_
+#define _WEBRTC_GLOBAL_H_
+
+#include "WebrtcIPCTraits.h"
+#include "ipc/EnumSerializer.h"
+#include "ipc/IPCMessageUtilsSpecializations.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/RTCDataChannelBinding.h"
+#include "mozilla/dom/RTCStatsReportBinding.h"
+#include "mozilla/UniquePtr.h"
+
+typedef mozilla::dom::RTCStatsReportInternal StatsReport;
+typedef nsTArray<mozilla::UniquePtr<StatsReport>> RTCReports;
+typedef mozilla::dom::Sequence<nsString> WebrtcGlobalLog;
+
+namespace mozilla {
+namespace dom {
+// Calls aFunction with all public members of aStats.
+// Typical usage would have aFunction take a parameter pack.
+// To avoid inconsistencies, this should be the only explicit list of the
+// public RTCStatscollection members in C++.
+template <typename Collection, typename Function>
+static auto ForAllPublicRTCStatsCollectionMembers(Collection& aStats,
+ Function aFunction) {
+ static_assert(std::is_same_v<typename std::remove_const<Collection>::type,
+ RTCStatsCollection>,
+ "aStats must be a const or non-const RTCStatsCollection");
+ return aFunction(
+ aStats.mInboundRtpStreamStats, aStats.mOutboundRtpStreamStats,
+ aStats.mRemoteInboundRtpStreamStats, aStats.mRemoteOutboundRtpStreamStats,
+ aStats.mMediaSourceStats, aStats.mPeerConnectionStats,
+ aStats.mRtpContributingSourceStats, aStats.mIceCandidatePairStats,
+ aStats.mIceCandidateStats, aStats.mTrickledIceCandidateStats,
+ aStats.mDataChannelStats, aStats.mCodecStats);
+}
+
+// Calls aFunction with all members of aStats, including internal ones.
+// Typical usage would have aFunction take a parameter pack.
+// To avoid inconsistencies, this should be the only explicit list of the
+// internal RTCStatscollection members in C++.
+template <typename Collection, typename Function>
+static auto ForAllRTCStatsCollectionMembers(Collection& aStats,
+ Function aFunction) {
+ static_assert(std::is_same_v<typename std::remove_const<Collection>::type,
+ RTCStatsCollection>,
+ "aStats must be a const or non-const RTCStatsCollection");
+ return ForAllPublicRTCStatsCollectionMembers(aStats, [&](auto&... aMember) {
+ return aFunction(aMember..., aStats.mRawLocalCandidates,
+ aStats.mRawRemoteCandidates, aStats.mVideoFrameHistories,
+ aStats.mBandwidthEstimations);
+ });
+}
+} // namespace dom
+} // namespace mozilla
+
+namespace IPC {
+
+template <>
+struct ParamTraits<mozilla::dom::RTCStatsType>
+ : public ContiguousEnumSerializer<mozilla::dom::RTCStatsType,
+ mozilla::dom::RTCStatsType::Codec,
+ mozilla::dom::RTCStatsType::EndGuard_> {};
+
+template <>
+struct ParamTraits<mozilla::dom::RTCStatsIceCandidatePairState>
+ : public ContiguousEnumSerializer<
+ mozilla::dom::RTCStatsIceCandidatePairState,
+ mozilla::dom::RTCStatsIceCandidatePairState::Frozen,
+ mozilla::dom::RTCStatsIceCandidatePairState::EndGuard_> {};
+
+template <>
+struct ParamTraits<mozilla::dom::RTCIceCandidateType>
+ : public ContiguousEnumSerializer<
+ mozilla::dom::RTCIceCandidateType,
+ mozilla::dom::RTCIceCandidateType::Host,
+ mozilla::dom::RTCIceCandidateType::EndGuard_> {};
+
+template <>
+struct ParamTraits<mozilla::dom::RTCBundlePolicy>
+ : public ContiguousEnumSerializer<
+ mozilla::dom::RTCBundlePolicy,
+ mozilla::dom::RTCBundlePolicy::Balanced,
+ mozilla::dom::RTCBundlePolicy::EndGuard_> {};
+
+DEFINE_IPC_SERIALIZER_WITH_FIELDS(mozilla::dom::RTCIceServerInternal, mUrls,
+ mCredentialProvided, mUserNameProvided);
+
+DEFINE_IPC_SERIALIZER_WITH_FIELDS(mozilla::dom::RTCConfigurationInternal,
+ mBundlePolicy, mCertificatesProvided,
+ mIceServers, mIceTransportPolicy,
+ mPeerIdentityProvided, mSdpSemantics);
+
+DEFINE_IPC_SERIALIZER_WITH_FIELDS(mozilla::dom::RTCSdpParsingErrorInternal,
+ mLineNumber, mError);
+
+DEFINE_IPC_SERIALIZER_WITH_FIELDS(mozilla::dom::RTCSdpHistoryEntryInternal,
+ mTimestamp, mIsLocal, mSdp, mErrors);
+
+template <>
+struct ParamTraits<mozilla::dom::RTCStatsCollection> {
+ static void Write(MessageWriter* aWriter,
+ const mozilla::dom::RTCStatsCollection& aParam) {
+ mozilla::dom::ForAllRTCStatsCollectionMembers(
+ aParam,
+ [&](const auto&... aMember) { WriteParams(aWriter, aMember...); });
+ }
+
+ static bool Read(MessageReader* aReader,
+ mozilla::dom::RTCStatsCollection* aResult) {
+ return mozilla::dom::ForAllRTCStatsCollectionMembers(
+ *aResult,
+ [&](auto&... aMember) { return ReadParams(aReader, aMember...); });
+ }
+};
+
+DEFINE_IPC_SERIALIZER_WITH_SUPER_CLASS_AND_FIELDS(
+ mozilla::dom::RTCStatsReportInternal, mozilla::dom::RTCStatsCollection,
+ mClosed, mSdpHistory, mPcid, mBrowserId, mTimestamp, mCallDurationMs,
+ mIceRestarts, mIceRollbacks, mOfferer, mConfiguration);
+
+typedef mozilla::dom::RTCStats RTCStats;
+
+static void WriteRTCStats(MessageWriter* aWriter, const RTCStats& aParam) {
+ // RTCStats base class
+ WriteParam(aWriter, aParam.mId);
+ WriteParam(aWriter, aParam.mTimestamp);
+ WriteParam(aWriter, aParam.mType);
+}
+
+static bool ReadRTCStats(MessageReader* aReader, RTCStats* aResult) {
+ // RTCStats base class
+ if (!ReadParam(aReader, &(aResult->mId)) ||
+ !ReadParam(aReader, &(aResult->mTimestamp)) ||
+ !ReadParam(aReader, &(aResult->mType))) {
+ return false;
+ }
+
+ return true;
+}
+
+template <>
+struct ParamTraits<mozilla::dom::RTCIceCandidatePairStats> {
+ typedef mozilla::dom::RTCIceCandidatePairStats paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mTransportId);
+ WriteParam(aWriter, aParam.mLocalCandidateId);
+ WriteParam(aWriter, aParam.mPriority);
+ WriteParam(aWriter, aParam.mNominated);
+ WriteParam(aWriter, aParam.mWritable);
+ WriteParam(aWriter, aParam.mReadable);
+ WriteParam(aWriter, aParam.mRemoteCandidateId);
+ WriteParam(aWriter, aParam.mSelected);
+ WriteParam(aWriter, aParam.mComponentId);
+ WriteParam(aWriter, aParam.mState);
+ WriteParam(aWriter, aParam.mBytesSent);
+ WriteParam(aWriter, aParam.mBytesReceived);
+ WriteParam(aWriter, aParam.mLastPacketSentTimestamp);
+ WriteParam(aWriter, aParam.mLastPacketReceivedTimestamp);
+ WriteRTCStats(aWriter, aParam);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ if (!ReadParam(aReader, &(aResult->mTransportId)) ||
+ !ReadParam(aReader, &(aResult->mLocalCandidateId)) ||
+ !ReadParam(aReader, &(aResult->mPriority)) ||
+ !ReadParam(aReader, &(aResult->mNominated)) ||
+ !ReadParam(aReader, &(aResult->mWritable)) ||
+ !ReadParam(aReader, &(aResult->mReadable)) ||
+ !ReadParam(aReader, &(aResult->mRemoteCandidateId)) ||
+ !ReadParam(aReader, &(aResult->mSelected)) ||
+ !ReadParam(aReader, &(aResult->mComponentId)) ||
+ !ReadParam(aReader, &(aResult->mState)) ||
+ !ReadParam(aReader, &(aResult->mBytesSent)) ||
+ !ReadParam(aReader, &(aResult->mBytesReceived)) ||
+ !ReadParam(aReader, &(aResult->mLastPacketSentTimestamp)) ||
+ !ReadParam(aReader, &(aResult->mLastPacketReceivedTimestamp)) ||
+ !ReadRTCStats(aReader, aResult)) {
+ return false;
+ }
+
+ return true;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::dom::RTCIceCandidateStats> {
+ typedef mozilla::dom::RTCIceCandidateStats paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mCandidateType);
+ WriteParam(aWriter, aParam.mPriority);
+ WriteParam(aWriter, aParam.mTransportId);
+ WriteParam(aWriter, aParam.mAddress);
+ WriteParam(aWriter, aParam.mRelayProtocol);
+ WriteParam(aWriter, aParam.mPort);
+ WriteParam(aWriter, aParam.mProtocol);
+ WriteParam(aWriter, aParam.mProxied);
+ WriteRTCStats(aWriter, aParam);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ if (!ReadParam(aReader, &(aResult->mCandidateType)) ||
+ !ReadParam(aReader, &(aResult->mPriority)) ||
+ !ReadParam(aReader, &(aResult->mTransportId)) ||
+ !ReadParam(aReader, &(aResult->mAddress)) ||
+ !ReadParam(aReader, &(aResult->mRelayProtocol)) ||
+ !ReadParam(aReader, &(aResult->mPort)) ||
+ !ReadParam(aReader, &(aResult->mProtocol)) ||
+ !ReadParam(aReader, &(aResult->mProxied)) ||
+ !ReadRTCStats(aReader, aResult)) {
+ return false;
+ }
+
+ return true;
+ }
+};
+
+static void WriteRTCRtpStreamStats(
+ MessageWriter* aWriter, const mozilla::dom::RTCRtpStreamStats& aParam) {
+ WriteParam(aWriter, aParam.mSsrc);
+ WriteParam(aWriter, aParam.mMediaType);
+ WriteParam(aWriter, aParam.mKind);
+ WriteParam(aWriter, aParam.mTransportId);
+ WriteParam(aWriter, aParam.mCodecId);
+ WriteRTCStats(aWriter, aParam);
+}
+
+static bool ReadRTCRtpStreamStats(MessageReader* aReader,
+ mozilla::dom::RTCRtpStreamStats* aResult) {
+ return ReadParam(aReader, &(aResult->mSsrc)) &&
+ ReadParam(aReader, &(aResult->mMediaType)) &&
+ ReadParam(aReader, &(aResult->mKind)) &&
+ ReadParam(aReader, &(aResult->mTransportId)) &&
+ ReadParam(aReader, &(aResult->mCodecId)) &&
+ ReadRTCStats(aReader, aResult);
+}
+
+static void WriteRTCReceivedRtpStreamStats(
+ MessageWriter* aWriter,
+ const mozilla::dom::RTCReceivedRtpStreamStats& aParam) {
+ WriteParam(aWriter, aParam.mPacketsReceived);
+ WriteParam(aWriter, aParam.mPacketsLost);
+ WriteParam(aWriter, aParam.mJitter);
+ WriteParam(aWriter, aParam.mDiscardedPackets);
+ WriteParam(aWriter, aParam.mPacketsDiscarded);
+ WriteRTCRtpStreamStats(aWriter, aParam);
+}
+
+static bool ReadRTCReceivedRtpStreamStats(
+ MessageReader* aReader, mozilla::dom::RTCReceivedRtpStreamStats* aResult) {
+ return ReadParam(aReader, &(aResult->mPacketsReceived)) &&
+ ReadParam(aReader, &(aResult->mPacketsLost)) &&
+ ReadParam(aReader, &(aResult->mJitter)) &&
+ ReadParam(aReader, &(aResult->mDiscardedPackets)) &&
+ ReadParam(aReader, &(aResult->mPacketsDiscarded)) &&
+ ReadRTCRtpStreamStats(aReader, aResult);
+}
+
+template <>
+struct ParamTraits<mozilla::dom::RTCInboundRtpStreamStats> {
+ typedef mozilla::dom::RTCInboundRtpStreamStats paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mTrackIdentifier);
+ WriteParam(aWriter, aParam.mRemoteId);
+ WriteParam(aWriter, aParam.mFramesDecoded);
+ WriteParam(aWriter, aParam.mFramesDropped);
+ WriteParam(aWriter, aParam.mFrameWidth);
+ WriteParam(aWriter, aParam.mFrameHeight);
+ WriteParam(aWriter, aParam.mFramesPerSecond);
+ WriteParam(aWriter, aParam.mQpSum);
+ WriteParam(aWriter, aParam.mTotalDecodeTime);
+ WriteParam(aWriter, aParam.mTotalInterFrameDelay);
+ WriteParam(aWriter, aParam.mTotalSquaredInterFrameDelay);
+ WriteParam(aWriter, aParam.mLastPacketReceivedTimestamp);
+ WriteParam(aWriter, aParam.mHeaderBytesReceived);
+ WriteParam(aWriter, aParam.mFecPacketsReceived);
+ WriteParam(aWriter, aParam.mFecPacketsDiscarded);
+ WriteParam(aWriter, aParam.mBytesReceived);
+ WriteParam(aWriter, aParam.mNackCount);
+ WriteParam(aWriter, aParam.mFirCount);
+ WriteParam(aWriter, aParam.mPliCount);
+ WriteParam(aWriter, aParam.mTotalProcessingDelay);
+ // Always missing from libwebrtc stats
+ // WriteParam(aWriter, aParam.mEstimatedPlayoutTimestamp);
+ WriteParam(aWriter, aParam.mFramesReceived);
+ WriteParam(aWriter, aParam.mJitterBufferDelay);
+ WriteParam(aWriter, aParam.mJitterBufferEmittedCount);
+ WriteParam(aWriter, aParam.mTotalSamplesReceived);
+ WriteParam(aWriter, aParam.mConcealedSamples);
+ WriteParam(aWriter, aParam.mSilentConcealedSamples);
+ WriteParam(aWriter, aParam.mConcealmentEvents);
+ WriteParam(aWriter, aParam.mInsertedSamplesForDeceleration);
+ WriteParam(aWriter, aParam.mRemovedSamplesForAcceleration);
+ WriteParam(aWriter, aParam.mAudioLevel);
+ WriteParam(aWriter, aParam.mTotalAudioEnergy);
+ WriteParam(aWriter, aParam.mTotalSamplesDuration);
+ WriteRTCReceivedRtpStreamStats(aWriter, aParam);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &(aResult->mTrackIdentifier)) &&
+ ReadParam(aReader, &(aResult->mRemoteId)) &&
+ ReadParam(aReader, &(aResult->mFramesDecoded)) &&
+ ReadParam(aReader, &(aResult->mFramesDropped)) &&
+ ReadParam(aReader, &(aResult->mFrameWidth)) &&
+ ReadParam(aReader, &(aResult->mFrameHeight)) &&
+ ReadParam(aReader, &(aResult->mFramesPerSecond)) &&
+ ReadParam(aReader, &(aResult->mQpSum)) &&
+ ReadParam(aReader, &(aResult->mTotalDecodeTime)) &&
+ ReadParam(aReader, &(aResult->mTotalInterFrameDelay)) &&
+ ReadParam(aReader, &(aResult->mTotalSquaredInterFrameDelay)) &&
+ ReadParam(aReader, &(aResult->mLastPacketReceivedTimestamp)) &&
+ ReadParam(aReader, &(aResult->mHeaderBytesReceived)) &&
+ ReadParam(aReader, &(aResult->mFecPacketsReceived)) &&
+ ReadParam(aReader, &(aResult->mFecPacketsDiscarded)) &&
+ ReadParam(aReader, &(aResult->mBytesReceived)) &&
+ ReadParam(aReader, &(aResult->mNackCount)) &&
+ ReadParam(aReader, &(aResult->mFirCount)) &&
+ ReadParam(aReader, &(aResult->mPliCount)) &&
+ ReadParam(aReader, &(aResult->mTotalProcessingDelay)) &&
+ // Always missing from libwebrtc
+ // ReadParam(aReader, &(aResult->mEstimatedPlayoutTimestamp)) &&
+ ReadParam(aReader, &(aResult->mFramesReceived)) &&
+ ReadParam(aReader, &(aResult->mJitterBufferDelay)) &&
+ ReadParam(aReader, &(aResult->mJitterBufferEmittedCount)) &&
+ ReadParam(aReader, &(aResult->mTotalSamplesReceived)) &&
+ ReadParam(aReader, &(aResult->mConcealedSamples)) &&
+ ReadParam(aReader, &(aResult->mSilentConcealedSamples)) &&
+ ReadParam(aReader, &(aResult->mConcealmentEvents)) &&
+ ReadParam(aReader, &(aResult->mInsertedSamplesForDeceleration)) &&
+ ReadParam(aReader, &(aResult->mRemovedSamplesForAcceleration)) &&
+ ReadParam(aReader, &(aResult->mAudioLevel)) &&
+ ReadParam(aReader, &(aResult->mTotalAudioEnergy)) &&
+ ReadParam(aReader, &(aResult->mTotalSamplesDuration)) &&
+ ReadRTCReceivedRtpStreamStats(aReader, aResult);
+ }
+};
+
+static void WriteRTCSentRtpStreamStats(
+ MessageWriter* aWriter, const mozilla::dom::RTCSentRtpStreamStats& aParam) {
+ WriteParam(aWriter, aParam.mPacketsSent);
+ WriteParam(aWriter, aParam.mBytesSent);
+ WriteRTCRtpStreamStats(aWriter, aParam);
+}
+
+static bool ReadRTCSentRtpStreamStats(
+ MessageReader* aReader, mozilla::dom::RTCSentRtpStreamStats* aResult) {
+ return ReadParam(aReader, &(aResult->mPacketsSent)) &&
+ ReadParam(aReader, &(aResult->mBytesSent)) &&
+ ReadRTCRtpStreamStats(aReader, aResult);
+}
+
+template <>
+struct ParamTraits<mozilla::dom::RTCOutboundRtpStreamStats> {
+ typedef mozilla::dom::RTCOutboundRtpStreamStats paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mRemoteId);
+ WriteParam(aWriter, aParam.mFramesEncoded);
+ WriteParam(aWriter, aParam.mQpSum);
+ WriteParam(aWriter, aParam.mNackCount);
+ WriteParam(aWriter, aParam.mFirCount);
+ WriteParam(aWriter, aParam.mPliCount);
+ WriteParam(aWriter, aParam.mHeaderBytesSent);
+ WriteParam(aWriter, aParam.mRetransmittedPacketsSent);
+ WriteParam(aWriter, aParam.mRetransmittedBytesSent);
+ WriteParam(aWriter, aParam.mTotalEncodedBytesTarget);
+ WriteParam(aWriter, aParam.mFrameWidth);
+ WriteParam(aWriter, aParam.mFrameHeight);
+ WriteParam(aWriter, aParam.mFramesSent);
+ WriteParam(aWriter, aParam.mHugeFramesSent);
+ WriteParam(aWriter, aParam.mTotalEncodeTime);
+ WriteRTCSentRtpStreamStats(aWriter, aParam);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &(aResult->mRemoteId)) &&
+ ReadParam(aReader, &(aResult->mFramesEncoded)) &&
+ ReadParam(aReader, &(aResult->mQpSum)) &&
+ ReadParam(aReader, &(aResult->mNackCount)) &&
+ ReadParam(aReader, &(aResult->mFirCount)) &&
+ ReadParam(aReader, &(aResult->mPliCount)) &&
+ ReadParam(aReader, &(aResult->mHeaderBytesSent)) &&
+ ReadParam(aReader, &(aResult->mRetransmittedPacketsSent)) &&
+ ReadParam(aReader, &(aResult->mRetransmittedBytesSent)) &&
+ ReadParam(aReader, &(aResult->mTotalEncodedBytesTarget)) &&
+ ReadParam(aReader, &(aResult->mFrameWidth)) &&
+ ReadParam(aReader, &(aResult->mFrameHeight)) &&
+ ReadParam(aReader, &(aResult->mFramesSent)) &&
+ ReadParam(aReader, &(aResult->mHugeFramesSent)) &&
+ ReadParam(aReader, &(aResult->mTotalEncodeTime)) &&
+ ReadRTCSentRtpStreamStats(aReader, aResult);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::dom::RTCRemoteInboundRtpStreamStats> {
+ typedef mozilla::dom::RTCRemoteInboundRtpStreamStats paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mLocalId);
+ WriteParam(aWriter, aParam.mRoundTripTime);
+ WriteParam(aWriter, aParam.mTotalRoundTripTime);
+ WriteParam(aWriter, aParam.mFractionLost);
+ WriteParam(aWriter, aParam.mRoundTripTimeMeasurements);
+ WriteRTCReceivedRtpStreamStats(aWriter, aParam);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &(aResult->mLocalId)) &&
+ ReadParam(aReader, &(aResult->mRoundTripTime)) &&
+ ReadParam(aReader, &(aResult->mTotalRoundTripTime)) &&
+ ReadParam(aReader, &(aResult->mFractionLost)) &&
+ ReadParam(aReader, &(aResult->mRoundTripTimeMeasurements)) &&
+ ReadRTCReceivedRtpStreamStats(aReader, aResult);
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::dom::RTCRemoteOutboundRtpStreamStats> {
+ typedef mozilla::dom::RTCRemoteOutboundRtpStreamStats paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mLocalId);
+ WriteParam(aWriter, aParam.mRemoteTimestamp);
+ WriteRTCSentRtpStreamStats(aWriter, aParam);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ return ReadParam(aReader, &(aResult->mLocalId)) &&
+ ReadParam(aReader, &(aResult->mRemoteTimestamp)) &&
+ ReadRTCSentRtpStreamStats(aReader, aResult);
+ }
+};
+
+DEFINE_IPC_SERIALIZER_WITH_FIELDS(mozilla::dom::RTCMediaSourceStats, mId,
+ mTimestamp, mType, mTrackIdentifier, mKind);
+
+template <>
+struct ParamTraits<mozilla::dom::RTCRTPContributingSourceStats> {
+ typedef mozilla::dom::RTCRTPContributingSourceStats paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mContributorSsrc);
+ WriteParam(aWriter, aParam.mInboundRtpStreamId);
+ WriteRTCStats(aWriter, aParam);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ if (!ReadParam(aReader, &(aResult->mContributorSsrc)) ||
+ !ReadParam(aReader, &(aResult->mInboundRtpStreamId)) ||
+ !ReadRTCStats(aReader, aResult)) {
+ return false;
+ }
+ return true;
+ }
+};
+
+DEFINE_IPC_SERIALIZER_WITH_FIELDS(mozilla::dom::RTCPeerConnectionStats, mId,
+ mTimestamp, mType, mDataChannelsOpened,
+ mDataChannelsClosed);
+
+DEFINE_IPC_SERIALIZER_WITH_FIELDS(
+ mozilla::dom::RTCVideoFrameHistoryEntryInternal, mWidth, mHeight,
+ mRotationAngle, mFirstFrameTimestamp, mLastFrameTimestamp,
+ mConsecutiveFrames, mLocalSsrc, mRemoteSsrc);
+
+DEFINE_IPC_SERIALIZER_WITH_FIELDS(mozilla::dom::RTCVideoFrameHistoryInternal,
+ mTrackIdentifier, mEntries);
+
+DEFINE_IPC_SERIALIZER_WITH_FIELDS(mozilla::dom::RTCBandwidthEstimationInternal,
+ mTrackIdentifier, mSendBandwidthBps,
+ mMaxPaddingBps, mReceiveBandwidthBps,
+ mPacerDelayMs, mRttMs);
+
+DEFINE_IPC_SERIALIZER_WITH_FIELDS(mozilla::dom::RTCDataChannelStats, mId,
+ mTimestamp, mType, mLabel, mProtocol,
+ mDataChannelIdentifier, mState, mMessagesSent,
+ mBytesSent, mMessagesReceived, mBytesReceived)
+
+template <>
+struct ParamTraits<mozilla::dom::RTCDataChannelState>
+ : public ContiguousEnumSerializer<
+ mozilla::dom::RTCDataChannelState,
+ mozilla::dom::RTCDataChannelState::Connecting,
+ mozilla::dom::RTCDataChannelState::EndGuard_> {};
+
+DEFINE_IPC_SERIALIZER_WITH_FIELDS(mozilla::dom::RTCCodecStats, mTimestamp,
+ mType, mId, mPayloadType, mCodecType,
+ mTransportId, mMimeType, mClockRate,
+ mChannels, mSdpFmtpLine)
+
+template <>
+struct ParamTraits<mozilla::dom::RTCCodecType>
+ : public ContiguousEnumSerializer<mozilla::dom::RTCCodecType,
+ mozilla::dom::RTCCodecType::Encode,
+ mozilla::dom::RTCCodecType::EndGuard_> {};
+} // namespace IPC
+
+#endif // _WEBRTC_GLOBAL_H_
diff --git a/dom/media/webrtc/WebrtcIPCTraits.h b/dom/media/webrtc/WebrtcIPCTraits.h
new file mode 100644
index 0000000000..b076745608
--- /dev/null
+++ b/dom/media/webrtc/WebrtcIPCTraits.h
@@ -0,0 +1,89 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _WEBRTC_IPC_TRAITS_H_
+#define _WEBRTC_IPC_TRAITS_H_
+
+#include "ipc/EnumSerializer.h"
+#include "ipc/IPCMessageUtils.h"
+#include "ipc/IPCMessageUtilsSpecializations.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/RTCConfigurationBinding.h"
+#include "mozilla/media/webrtc/WebrtcGlobal.h"
+#include "mozilla/dom/CandidateInfo.h"
+#include "mozilla/MacroForEach.h"
+#include "transport/dtlsidentity.h"
+#include <vector>
+
+namespace mozilla {
+typedef std::vector<std::string> StringVector;
+}
+
+namespace IPC {
+
+template <>
+struct ParamTraits<mozilla::dom::OwningStringOrStringSequence> {
+ typedef mozilla::dom::OwningStringOrStringSequence paramType;
+
+ // Ugh. OwningStringOrStringSequence already has this enum, but it is
+ // private generated code. So we have to re-create it.
+ enum Type { kUninitialized, kString, kStringSequence };
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ if (aParam.IsString()) {
+ aWriter->WriteInt16(kString);
+ WriteParam(aWriter, aParam.GetAsString());
+ } else if (aParam.IsStringSequence()) {
+ aWriter->WriteInt16(kStringSequence);
+ WriteParam(aWriter, aParam.GetAsStringSequence());
+ } else {
+ aWriter->WriteInt16(kUninitialized);
+ }
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ int16_t type;
+ if (!aReader->ReadInt16(&type)) {
+ return false;
+ }
+
+ switch (type) {
+ case kUninitialized:
+ aResult->Uninit();
+ return true;
+ case kString:
+ return ReadParam(aReader, &aResult->SetAsString());
+ case kStringSequence:
+ return ReadParam(aReader, &aResult->SetAsStringSequence());
+ }
+
+ return false;
+ }
+};
+
+template <typename T>
+struct WebidlEnumSerializer
+ : public ContiguousEnumSerializer<T, T(0), T::EndGuard_> {};
+
+template <>
+struct ParamTraits<mozilla::dom::RTCIceCredentialType>
+ : public WebidlEnumSerializer<mozilla::dom::RTCIceCredentialType> {};
+
+template <>
+struct ParamTraits<mozilla::dom::RTCIceTransportPolicy>
+ : public WebidlEnumSerializer<mozilla::dom::RTCIceTransportPolicy> {};
+
+DEFINE_IPC_SERIALIZER_WITH_FIELDS(mozilla::dom::RTCIceServer, mCredential,
+ mCredentialType, mUrl, mUrls, mUsername)
+
+DEFINE_IPC_SERIALIZER_WITH_FIELDS(mozilla::CandidateInfo, mCandidate, mUfrag,
+ mDefaultHostRtp, mDefaultPortRtp,
+ mDefaultHostRtcp, mDefaultPortRtcp,
+ mMDNSAddress, mActualAddress)
+
+DEFINE_IPC_SERIALIZER_WITH_FIELDS(mozilla::DtlsDigest, algorithm_, value_)
+
+} // namespace IPC
+
+#endif // _WEBRTC_IPC_TRAITS_H_
diff --git a/dom/media/webrtc/common/CandidateInfo.h b/dom/media/webrtc/common/CandidateInfo.h
new file mode 100644
index 0000000000..eb5b2ea299
--- /dev/null
+++ b/dom/media/webrtc/common/CandidateInfo.h
@@ -0,0 +1,27 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _CANDIDATE_INFO_H__
+#define _CANDIDATE_INFO_H__
+
+#include <string>
+#include <cstdint>
+
+namespace mozilla {
+
+// This is used both by IPDL code, and by signaling code.
+struct CandidateInfo {
+ std::string mCandidate;
+ std::string mMDNSAddress;
+ std::string mActualAddress;
+ std::string mUfrag;
+ std::string mDefaultHostRtp;
+ uint16_t mDefaultPortRtp = 0;
+ std::string mDefaultHostRtcp;
+ uint16_t mDefaultPortRtcp = 0;
+};
+
+} // namespace mozilla
+
+#endif //_CANDIDATE_INFO_H__
diff --git a/dom/media/webrtc/common/CommonTypes.h b/dom/media/webrtc/common/CommonTypes.h
new file mode 100644
index 0000000000..e9c9ffdefc
--- /dev/null
+++ b/dom/media/webrtc/common/CommonTypes.h
@@ -0,0 +1,52 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#pragma once
+
+#include <string>
+
+namespace csf {
+
+namespace ProviderStateEnum {
+enum ProviderState {
+ Ready,
+ Registering,
+ AwaitingIpAddress,
+ FetchingDeviceConfig,
+ Idle,
+ RecoveryPending,
+ Connected
+};
+const std::string toString(ProviderState);
+} // namespace ProviderStateEnum
+namespace LoginErrorStatusEnum {
+enum LoginErrorStatus {
+ Ok, // No Error
+ Unknown, // Unknown Error
+ NoCallManagerConfigured, // No Primary or Backup Call Manager
+ NoDevicesFound, // No devices
+ NoCsfDevicesFound, // Devices but none of type CSF
+ PhoneConfigGenError, // Could not generate phone config
+ SipProfileGenError, // Could not build SIP profile
+ ConfigNotSet, // Config not set before calling login()
+ CreateConfigProviderFailed, // Could not create ConfigProvider
+ CreateSoftPhoneProviderFailed, // Could not create SoftPhoneProvider
+ MissingUsername, // Username argument missing,
+ ManualLogout, // logout() has been called
+ LoggedInElseWhere, // Another process has the mutex indicating it is logged
+ // in
+ AuthenticationFailure, // Authentication failure (probably bad password, but
+ // best not to say for sure)
+ CtiCouldNotConnect, // Could not connect to CTI service
+ InvalidServerSearchList
+};
+const std::string toString(LoginErrorStatus);
+} // namespace LoginErrorStatusEnum
+
+namespace ErrorCodeEnum {
+enum ErrorCode { Ok, Unknown, InvalidState, InvalidArgument };
+const std::string toString(ErrorCode);
+} // namespace ErrorCodeEnum
+
+} // namespace csf
diff --git a/dom/media/webrtc/common/EncodingConstraints.h b/dom/media/webrtc/common/EncodingConstraints.h
new file mode 100644
index 0000000000..0bfa7f9f75
--- /dev/null
+++ b/dom/media/webrtc/common/EncodingConstraints.h
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _ENCODING_CONSTRAINTS_H_
+#define _ENCODING_CONSTRAINTS_H_
+
+#include <algorithm>
+#include "mozilla/Maybe.h"
+
+namespace mozilla {
+class EncodingConstraints {
+ public:
+ EncodingConstraints()
+ : maxWidth(0),
+ maxHeight(0),
+ maxFs(0),
+ maxBr(0),
+ maxPps(0),
+ maxMbps(0),
+ maxCpb(0),
+ maxDpb(0),
+ scaleDownBy(1.0) {}
+
+ bool operator==(const EncodingConstraints& constraints) const {
+ return maxWidth == constraints.maxWidth &&
+ maxHeight == constraints.maxHeight && maxFps == constraints.maxFps &&
+ maxFs == constraints.maxFs && maxBr == constraints.maxBr &&
+ maxPps == constraints.maxPps && maxMbps == constraints.maxMbps &&
+ maxCpb == constraints.maxCpb && maxDpb == constraints.maxDpb &&
+ scaleDownBy == constraints.scaleDownBy;
+ }
+
+ /**
+ * This returns true if the constraints affecting resolution are equal.
+ */
+ bool ResolutionEquals(const EncodingConstraints& constraints) const {
+ return maxWidth == constraints.maxWidth &&
+ maxHeight == constraints.maxHeight && maxFs == constraints.maxFs &&
+ scaleDownBy == constraints.scaleDownBy;
+ }
+
+ uint32_t maxWidth;
+ uint32_t maxHeight;
+ Maybe<double> maxFps;
+ uint32_t maxFs;
+ uint32_t maxBr;
+ uint32_t maxPps;
+ uint32_t maxMbps; // macroblocks per second
+ uint32_t maxCpb; // coded picture buffer size
+ uint32_t maxDpb; // decoded picture buffer size
+ double scaleDownBy; // To preserve resolution
+};
+} // namespace mozilla
+
+#endif // _ENCODING_CONSTRAINTS_H_
diff --git a/dom/media/webrtc/common/MediaEngineWrapper.h b/dom/media/webrtc/common/MediaEngineWrapper.h
new file mode 100644
index 0000000000..ba48ad57d8
--- /dev/null
+++ b/dom/media/webrtc/common/MediaEngineWrapper.h
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MEDIA_ENGINE_WRAPPER_H_
+#define MEDIA_ENGINE_WRAPPER_H_
+
+#include <mozilla/Scoped.h>
+
+namespace mozilla {
+/**
+ * A Custom scoped template to release a resoure of Type T
+ * with a function of Type F
+ * ScopedCustomReleasePtr<webrtc::VoENetwork> ptr =
+ * webrtc::VoENetwork->GetInterface(voiceEngine);
+ *
+ */
+template <typename T>
+struct ScopedCustomReleaseTraits0 {
+ typedef T* type;
+ static T* empty() { return nullptr; }
+ static void release(T* ptr) {
+ if (ptr) {
+ (ptr)->Release();
+ }
+ }
+};
+
+SCOPED_TEMPLATE(ScopedCustomReleasePtr, ScopedCustomReleaseTraits0)
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/common/NullDeleter.h b/dom/media/webrtc/common/NullDeleter.h
new file mode 100644
index 0000000000..76326c197e
--- /dev/null
+++ b/dom/media/webrtc/common/NullDeleter.h
@@ -0,0 +1,14 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#pragma once
+
+/*
+ * Helper class to allow smart pointers to stack objects to be constructed for
+ * ease of unit testing. Recycled here to help expose a shared_ptr interface to
+ * objects which are really raw pointers.
+ */
+struct null_deleter {
+ void operator()(void const*) const {}
+};
diff --git a/dom/media/webrtc/common/NullTransport.h b/dom/media/webrtc/common/NullTransport.h
new file mode 100644
index 0000000000..a9b270c6d6
--- /dev/null
+++ b/dom/media/webrtc/common/NullTransport.h
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef NULL_TRANSPORT_H_
+#define NULL_TRANSPORT_H_
+
+#include "mozilla/Attributes.h"
+
+#include "api/call/transport.h"
+
+namespace mozilla {
+
+/**
+ * NullTransport is registered as ExternalTransport to throw away data
+ */
+class NullTransport : public webrtc::Transport {
+ public:
+ virtual bool SendRtp(const uint8_t* packet, size_t length,
+ const webrtc::PacketOptions& options) override {
+ (void)packet;
+ (void)length;
+ (void)options;
+ return true;
+ }
+
+ virtual bool SendRtcp(const uint8_t* packet, size_t length) override {
+ (void)packet;
+ (void)length;
+ return true;
+ }
+#if 0
+ virtual int SendPacket(int channel, const void *data, size_t len)
+ {
+ (void) channel; (void) data;
+ return len;
+ }
+
+ virtual int SendRTCPPacket(int channel, const void *data, size_t len)
+ {
+ (void) channel; (void) data;
+ return len;
+ }
+#endif
+ NullTransport() {}
+
+ virtual ~NullTransport() {}
+
+ private:
+ NullTransport(const NullTransport& other) = delete;
+ void operator=(const NullTransport& other) = delete;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/common/Wrapper.h b/dom/media/webrtc/common/Wrapper.h
new file mode 100644
index 0000000000..69c7406b07
--- /dev/null
+++ b/dom/media/webrtc/common/Wrapper.h
@@ -0,0 +1,157 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#pragma once
+
+/*
+ * Wrapper - Helper class for wrapper objects.
+ *
+ * This helps to construct a shared_ptr object which wraps access to an
+ * underlying handle. (The handle could be a pointer to some low-level type, a
+ * conventional C handle, an int ID, a GUID, etc.)
+ *
+ * Usage:
+ * To obtain a FooPtr from a foo_handle_t, call
+ * FooPtr Foo::wrap(foo_handle_t);
+ *
+ * To implement Foo using Wrapper, Foo needs to include this macro in its class
+ * definition:
+ * CSF_DECLARE_WRAP(Foo, foo_handle_t);
+ * It also needs to include this in the cpp file, to provide the wrap()
+ * implementation and define the static Wrapper.
+ * CSF_IMPLEMENT_WRAP(Foo, foo_handle_t);
+ * These are all declared in common/Wrapper.h - Foo.h needs to include this
+ * too.
+ * The client needs to declare Foo(foo_handle_t) as private, and provide a
+ * suitable implementation, as well as implementing wrappers for any other
+ * functions to be exposed.
+ * The client needs to implement ~Foo() to perform any cleanup as usual.
+ *
+ * wrap() will always return the same FooPtr for a given foo_handle_t, it will
+ * not construct additional objects if a suitable one already exists.
+ * changeHandle() is used in rare cases where the underlying handle is changed,
+ * but the wrapper object is intended to remain. This is the
+ * case for the "fake" CC_DPCall generated on
+ * CC_DPLine::CreateCall(), where the correct IDPCall* is
+ * provided later.
+ * reset() is a cleanup step to wipe the handle map and allow memory to be
+ * reclaimed.
+ *
+ * Future enhancements:
+ * - For now, objects remain in the map forever. Better would be to add a
+ * releaseHandle() function which would allow the map to be emptied as
+ * underlying handles expired. While we can't force the client to give up
+ * its shared_ptr<Foo> objects, we can remove our own copy, for instance on a
+ * call ended event.
+ */
+
+#include <map>
+#include "prlock.h"
+#include "mozilla/Assertions.h"
+
+/*
+ * Wrapper has its own autolock class because the instances are declared
+ * statically and mozilla::Mutex will not work properly when instantiated
+ * in a static constructor.
+ */
+
+class LockNSPR {
+ public:
+ LockNSPR() : lock_(nullptr) {
+ lock_ = PR_NewLock();
+ MOZ_ASSERT(lock_);
+ }
+ ~LockNSPR() { PR_DestroyLock(lock_); }
+
+ void Acquire() { PR_Lock(lock_); }
+
+ void Release() { PR_Unlock(lock_); }
+
+ private:
+ PRLock* lock_;
+};
+
+class AutoLockNSPR {
+ public:
+ explicit AutoLockNSPR(LockNSPR& lock) : lock_(lock) { lock_.Acquire(); }
+ ~AutoLockNSPR() { lock_.Release(); }
+
+ private:
+ LockNSPR& lock_;
+};
+
+template <class T>
+class Wrapper {
+ private:
+ typedef std::map<typename T::Handle, typename T::Ptr> HandleMapType;
+ HandleMapType handleMap;
+ LockNSPR handleMapMutex;
+
+ public:
+ Wrapper() {}
+
+ typename T::Ptr wrap(typename T::Handle handle) {
+ AutoLockNSPR lock(handleMapMutex);
+ typename HandleMapType::iterator it = handleMap.find(handle);
+ if (it != handleMap.end()) {
+ return it->second;
+ } else {
+ typename T::Ptr p(new T(handle));
+ handleMap[handle] = p;
+ return p;
+ }
+ }
+
+ bool changeHandle(typename T::Handle oldHandle,
+ typename T::Handle newHandle) {
+ AutoLockNSPR lock(handleMapMutex);
+ typename HandleMapType::iterator it = handleMap.find(oldHandle);
+ if (it != handleMap.end()) {
+ typename T::Ptr p = it->second;
+ handleMap.erase(it);
+ handleMap[newHandle] = p;
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ bool release(typename T::Handle handle) {
+ AutoLockNSPR lock(handleMapMutex);
+ typename HandleMapType::iterator it = handleMap.find(handle);
+ if (it != handleMap.end()) {
+ handleMap.erase(it);
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ void reset() {
+ AutoLockNSPR lock(handleMapMutex);
+ handleMap.clear();
+ }
+};
+
+#define CSF_DECLARE_WRAP(classname, handletype) \
+ public: \
+ static classname##Ptr wrap(handletype handle); \
+ static void reset(); \
+ static void release(handletype handle); \
+ \
+ private: \
+ friend class Wrapper<classname>; \
+ typedef classname##Ptr Ptr; \
+ typedef handletype Handle; \
+ static Wrapper<classname>& getWrapper() { \
+ static Wrapper<classname> wrapper; \
+ return wrapper; \
+ }
+
+#define CSF_IMPLEMENT_WRAP(classname, handletype) \
+ classname##Ptr classname::wrap(handletype handle) { \
+ return getWrapper().wrap(handle); \
+ } \
+ void classname::reset() { getWrapper().reset(); } \
+ void classname::release(handletype handle) { getWrapper().release(handle); }
diff --git a/dom/media/webrtc/common/YuvStamper.cpp b/dom/media/webrtc/common/YuvStamper.cpp
new file mode 100644
index 0000000000..b0c8426983
--- /dev/null
+++ b/dom/media/webrtc/common/YuvStamper.cpp
@@ -0,0 +1,394 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifdef HAVE_NETINET_IN_H
+# include <netinet/in.h>
+#elif defined XP_WIN
+# include <winsock2.h>
+#endif
+#include <string.h>
+
+#include "nspr.h"
+#include "YuvStamper.h"
+#include "mozilla/Sprintf.h"
+
+typedef uint32_t UINT4; // Needed for r_crc32() call
+extern "C" {
+#include "r_crc32.h"
+}
+
+namespace mozilla {
+
+#define ON_5 0x20
+#define ON_4 0x10
+#define ON_3 0x08
+#define ON_2 0x04
+#define ON_1 0x02
+#define ON_0 0x01
+
+/*
+ 0, 0, 1, 1, 0, 0,
+ 0, 1, 0, 0, 1, 0,
+ 1, 0, 0, 0, 0, 1,
+ 1, 0, 0, 0, 0, 1,
+ 1, 0, 0, 0, 0, 1,
+ 0, 1, 0, 0, 1, 0,
+ 0, 0, 1, 1, 0, 0
+*/
+static unsigned char DIGIT_0[] = {ON_3 | ON_2, ON_4 | ON_1, ON_5 | ON_0,
+ ON_5 | ON_0, ON_5 | ON_0, ON_4 | ON_1,
+ ON_3 | ON_2};
+
+/*
+ 0, 0, 0, 1, 0, 0,
+ 0, 0, 0, 1, 0, 0,
+ 0, 0, 0, 1, 0, 0,
+ 0, 0, 0, 1, 0, 0,
+ 0, 0, 0, 1, 0, 0,
+ 0, 0, 0, 1, 0, 0,
+ 0, 0, 0, 1, 0, 0,
+*/
+static unsigned char DIGIT_1[] = {ON_2, ON_2, ON_2, ON_2, ON_2, ON_2, ON_2};
+
+/*
+ 1, 1, 1, 1, 1, 0,
+ 0, 0, 0, 0, 0, 1,
+ 0, 0, 0, 0, 0, 1,
+ 0, 1, 1, 1, 1, 0,
+ 1, 0, 0, 0, 0, 0,
+ 1, 0, 0, 0, 0, 0,
+ 0, 1, 1, 1, 1, 1,
+*/
+static unsigned char DIGIT_2[] = {
+ ON_5 | ON_4 | ON_3 | ON_2 | ON_1, ON_0, ON_0,
+ ON_4 | ON_3 | ON_2 | ON_1, ON_5, ON_5,
+ ON_4 | ON_3 | ON_2 | ON_1 | ON_0,
+};
+
+/*
+ 1, 1, 1, 1, 1, 0,
+ 0, 0, 0, 0, 0, 1,
+ 0, 0, 0, 0, 0, 1,
+ 0, 1, 1, 1, 1, 1,
+ 0, 0, 0, 0, 0, 1,
+ 0, 0, 0, 0, 0, 1,
+ 1, 1, 1, 1, 1, 0,
+*/
+static unsigned char DIGIT_3[] = {
+ ON_5 | ON_4 | ON_3 | ON_2 | ON_1, ON_0, ON_0,
+ ON_4 | ON_3 | ON_2 | ON_1 | ON_0, ON_0, ON_0,
+ ON_5 | ON_4 | ON_3 | ON_2 | ON_1,
+};
+
+/*
+ 0, 1, 0, 0, 0, 1,
+ 0, 1, 0, 0, 0, 1,
+ 0, 1, 0, 0, 0, 1,
+ 0, 1, 1, 1, 1, 1,
+ 0, 0, 0, 0, 0, 1,
+ 0, 0, 0, 0, 0, 1,
+ 0, 0, 0, 0, 0, 1
+*/
+static unsigned char DIGIT_4[] = {
+ ON_4 | ON_0, ON_4 | ON_0, ON_4 | ON_0, ON_4 | ON_3 | ON_2 | ON_1 | ON_0,
+ ON_0, ON_0, ON_0,
+};
+
+/*
+ 0, 1, 1, 1, 1, 1,
+ 1, 0, 0, 0, 0, 0,
+ 1, 0, 0, 0, 0, 0,
+ 0, 1, 1, 1, 1, 0,
+ 0, 0, 0, 0, 0, 1,
+ 0, 0, 0, 0, 0, 1,
+ 1, 1, 1, 1, 1, 0,
+*/
+static unsigned char DIGIT_5[] = {
+ ON_4 | ON_3 | ON_2 | ON_1 | ON_0, ON_5, ON_5,
+ ON_4 | ON_3 | ON_2 | ON_1, ON_0, ON_0,
+ ON_5 | ON_4 | ON_3 | ON_2 | ON_1,
+};
+
+/*
+ 0, 1, 1, 1, 1, 1,
+ 1, 0, 0, 0, 0, 0,
+ 1, 0, 0, 0, 0, 0,
+ 1, 1, 1, 1, 1, 0,
+ 1, 0, 0, 0, 0, 1,
+ 1, 0, 0, 0, 0, 1,
+ 0, 1, 1, 1, 1, 0,
+*/
+static unsigned char DIGIT_6[] = {
+ ON_4 | ON_3 | ON_2 | ON_1 | ON_0, ON_5, ON_5,
+ ON_4 | ON_3 | ON_2 | ON_1, ON_5 | ON_0, ON_5 | ON_0,
+ ON_4 | ON_3 | ON_2 | ON_1,
+};
+
+/*
+ 1, 1, 1, 1, 1, 1,
+ 0, 0, 0, 0, 0, 1,
+ 0, 0, 0, 0, 1, 0,
+ 0, 0, 0, 1, 0, 0,
+ 0, 0, 1, 0, 0, 0,
+ 0, 1, 0, 0, 0, 0,
+ 1, 0, 0, 0, 0, 0
+*/
+static unsigned char DIGIT_7[] = {ON_5 | ON_4 | ON_3 | ON_2 | ON_1 | ON_0,
+ ON_0,
+ ON_1,
+ ON_2,
+ ON_3,
+ ON_4,
+ ON_5};
+
+/*
+ 0, 1, 1, 1, 1, 1,
+ 1, 0, 0, 0, 0, 1,
+ 1, 0, 0, 0, 0, 1,
+ 0, 1, 1, 1, 1, 0,
+ 1, 0, 0, 0, 0, 1,
+ 1, 0, 0, 0, 0, 1,
+ 0, 1, 1, 1, 1, 0
+*/
+static unsigned char DIGIT_8[] = {
+ ON_4 | ON_3 | ON_2 | ON_1, ON_5 | ON_0, ON_5 | ON_0,
+ ON_4 | ON_3 | ON_2 | ON_1, ON_5 | ON_0, ON_5 | ON_0,
+ ON_4 | ON_3 | ON_2 | ON_1,
+};
+
+/*
+ 0, 1, 1, 1, 1, 1,
+ 1, 0, 0, 0, 0, 1,
+ 1, 0, 0, 0, 0, 1,
+ 0, 1, 1, 1, 1, 1,
+ 0, 0, 0, 0, 0, 1,
+ 0, 0, 0, 0, 0, 1,
+ 0, 1, 1, 1, 1, 0
+*/
+static unsigned char DIGIT_9[] = {
+ ON_4 | ON_3 | ON_2 | ON_1 | ON_0, ON_5 | ON_0, ON_5 | ON_0,
+ ON_4 | ON_3 | ON_2 | ON_1 | ON_0, ON_0, ON_0,
+ ON_4 | ON_3 | ON_2 | ON_1,
+};
+
+static unsigned char* DIGITS[] = {DIGIT_0, DIGIT_1, DIGIT_2, DIGIT_3, DIGIT_4,
+ DIGIT_5, DIGIT_6, DIGIT_7, DIGIT_8, DIGIT_9};
+
+YuvStamper::YuvStamper(unsigned char* pYData, uint32_t width, uint32_t height,
+ uint32_t stride, uint32_t x, uint32_t y,
+ unsigned char symbol_width, unsigned char symbol_height)
+ : pYData(pYData),
+ mStride(stride),
+ mWidth(width),
+ mHeight(height),
+ mSymbolWidth(symbol_width),
+ mSymbolHeight(symbol_height),
+ mCursor(x, y) {}
+
+bool YuvStamper::Encode(uint32_t width, uint32_t height, uint32_t stride,
+ unsigned char* pYData, unsigned char* pMsg,
+ size_t msg_len, uint32_t x, uint32_t y) {
+ YuvStamper stamper(pYData, width, height, stride, x, y, sBitSize, sBitSize);
+
+ // Reserve space for a checksum.
+ if (stamper.Capacity() < 8 * (msg_len + sizeof(uint32_t))) {
+ return false;
+ }
+
+ bool ok = false;
+ uint32_t crc;
+ unsigned char* pCrc = reinterpret_cast<unsigned char*>(&crc);
+ r_crc32(reinterpret_cast<char*>(pMsg), (int)msg_len, &crc);
+ crc = htonl(crc);
+
+ while (msg_len-- > 0) {
+ if (!stamper.Write8(*pMsg++)) {
+ return false;
+ }
+ }
+
+ // Add checksum after the message.
+ ok = stamper.Write8(*pCrc++) && stamper.Write8(*pCrc++) &&
+ stamper.Write8(*pCrc++) && stamper.Write8(*pCrc++);
+
+ return ok;
+}
+
+bool YuvStamper::Decode(uint32_t width, uint32_t height, uint32_t stride,
+ unsigned char* pYData, unsigned char* pMsg,
+ size_t msg_len, uint32_t x, uint32_t y) {
+ YuvStamper stamper(pYData, width, height, stride, x, y, sBitSize, sBitSize);
+
+ unsigned char* ptr = pMsg;
+ size_t len = msg_len;
+ uint32_t crc, msg_crc;
+ unsigned char* pCrc = reinterpret_cast<unsigned char*>(&crc);
+
+ // Account for space reserved for the checksum
+ if (stamper.Capacity() < 8 * (len + sizeof(uint32_t))) {
+ return false;
+ }
+
+ while (len-- > 0) {
+ if (!stamper.Read8(*ptr++)) {
+ return false;
+ }
+ }
+
+ if (!(stamper.Read8(*pCrc++) && stamper.Read8(*pCrc++) &&
+ stamper.Read8(*pCrc++) && stamper.Read8(*pCrc++))) {
+ return false;
+ }
+
+ r_crc32(reinterpret_cast<char*>(pMsg), (int)msg_len, &msg_crc);
+ return crc == htonl(msg_crc);
+}
+
+inline uint32_t YuvStamper::Capacity() {
+ // Enforce at least a symbol width and height offset from outer edges.
+ if (mCursor.y + mSymbolHeight > mHeight) {
+ return 0;
+ }
+
+ if (mCursor.x + mSymbolWidth > mWidth && !AdvanceCursor()) {
+ return 0;
+ }
+
+ // Normalize frame integral to mSymbolWidth x mSymbolHeight
+ uint32_t width = mWidth / mSymbolWidth;
+ uint32_t height = mHeight / mSymbolHeight;
+ uint32_t x = mCursor.x / mSymbolWidth;
+ uint32_t y = mCursor.y / mSymbolHeight;
+
+ return (width * height - width * y) - x;
+}
+
+bool YuvStamper::Write8(unsigned char value) {
+ // Encode MSB to LSB.
+ unsigned char mask = 0x80;
+ while (mask) {
+ if (!WriteBit(!!(value & mask))) {
+ return false;
+ }
+ mask >>= 1;
+ }
+ return true;
+}
+
+bool YuvStamper::WriteBit(bool one) {
+ // A bit is mapped to a mSymbolWidth x mSymbolHeight square of luma data
+ // points. Don't use ternary op.:
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1001708
+ unsigned char value;
+ if (one)
+ value = sYOn;
+ else
+ value = sYOff;
+
+ for (uint32_t y = 0; y < mSymbolHeight; y++) {
+ for (uint32_t x = 0; x < mSymbolWidth; x++) {
+ *(pYData + (mCursor.x + x) + ((mCursor.y + y) * mStride)) = value;
+ }
+ }
+
+ return AdvanceCursor();
+}
+
+bool YuvStamper::AdvanceCursor() {
+ mCursor.x += mSymbolWidth;
+ if (mCursor.x + mSymbolWidth > mWidth) {
+ // move to the start of the next row if possible.
+ mCursor.y += mSymbolHeight;
+ if (mCursor.y + mSymbolHeight > mHeight) {
+ // end of frame, do not advance
+ mCursor.y -= mSymbolHeight;
+ mCursor.x -= mSymbolWidth;
+ return false;
+ } else {
+ mCursor.x = 0;
+ }
+ }
+
+ return true;
+}
+
+bool YuvStamper::Read8(unsigned char& value) {
+ unsigned char octet = 0;
+ unsigned char bit = 0;
+
+ for (int i = 8; i > 0; --i) {
+ if (!ReadBit(bit)) {
+ return false;
+ }
+ octet <<= 1;
+ octet |= bit;
+ }
+
+ value = octet;
+ return true;
+}
+
+bool YuvStamper::ReadBit(unsigned char& bit) {
+ uint32_t sum = 0;
+ for (uint32_t y = 0; y < mSymbolHeight; y++) {
+ for (uint32_t x = 0; x < mSymbolWidth; x++) {
+ sum += *(pYData + mStride * (mCursor.y + y) + mCursor.x + x);
+ }
+ }
+
+ // apply threshold to collected bit square
+ bit = (sum > (sBitThreshold * mSymbolWidth * mSymbolHeight)) ? 1 : 0;
+ return AdvanceCursor();
+}
+
+bool YuvStamper::WriteDigits(uint32_t value) {
+ char buf[20];
+ SprintfLiteral(buf, "%.5u", value);
+ size_t size = strlen(buf);
+
+ if (Capacity() < size) {
+ return false;
+ }
+
+ for (size_t i = 0; i < size; ++i) {
+ if (!WriteDigit(buf[i] - '0')) return false;
+ if (!AdvanceCursor()) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool YuvStamper::WriteDigit(unsigned char digit) {
+ if (digit > sizeof(DIGITS) / sizeof(DIGITS[0])) return false;
+
+ unsigned char* dig = DIGITS[digit];
+ for (uint32_t row = 0; row < sDigitHeight; ++row) {
+ unsigned char mask = 0x01 << (sDigitWidth - 1);
+ for (uint32_t col = 0; col < sDigitWidth; ++col, mask >>= 1) {
+ if (dig[row] & mask) {
+ for (uint32_t xx = 0; xx < sPixelSize; ++xx) {
+ for (uint32_t yy = 0; yy < sPixelSize; ++yy) {
+ WritePixel(pYData, mCursor.x + (col * sPixelSize) + xx,
+ mCursor.y + (row * sPixelSize) + yy);
+ }
+ }
+ }
+ }
+ }
+
+ return true;
+}
+
+void YuvStamper::WritePixel(unsigned char* data, uint32_t x, uint32_t y) {
+ unsigned char* ptr = &data[y * mStride + x];
+ // Don't use ternary op.: https://bugzilla.mozilla.org/show_bug.cgi?id=1001708
+ if (*ptr > sLumaThreshold)
+ *ptr = sLumaMin;
+ else
+ *ptr = sLumaMax;
+}
+
+} // namespace mozilla.
diff --git a/dom/media/webrtc/common/YuvStamper.h b/dom/media/webrtc/common/YuvStamper.h
new file mode 100644
index 0000000000..f355055cd9
--- /dev/null
+++ b/dom/media/webrtc/common/YuvStamper.h
@@ -0,0 +1,77 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef YUV_STAMPER_H_
+#define YUV_STAMPER_H_
+
+#include <cstdint>
+
+namespace mozilla {
+
+class YuvStamper {
+ public:
+ bool WriteDigits(uint32_t value);
+
+ template <typename T>
+ static bool Write(uint32_t width, uint32_t height, uint32_t stride,
+ unsigned char* pYData, const T& value, uint32_t x = 0,
+ uint32_t y = 0) {
+ YuvStamper stamper(pYData, width, height, stride, x, y,
+ (sDigitWidth + sInterDigit) * sPixelSize,
+ (sDigitHeight + sInterLine) * sPixelSize);
+ return stamper.WriteDigits(value);
+ }
+
+ static bool Encode(uint32_t width, uint32_t height, uint32_t stride,
+ unsigned char* pYData, unsigned char* pMsg, size_t msg_len,
+ uint32_t x = 0, uint32_t y = 0);
+
+ static bool Decode(uint32_t width, uint32_t height, uint32_t stride,
+ unsigned char* pYData, unsigned char* pMsg, size_t msg_len,
+ uint32_t x = 0, uint32_t y = 0);
+
+ private:
+ YuvStamper(unsigned char* pYData, uint32_t width, uint32_t height,
+ uint32_t stride, uint32_t x, uint32_t y,
+ unsigned char symbol_width, unsigned char symbol_height);
+
+ bool WriteDigit(unsigned char digit);
+ void WritePixel(unsigned char* data, uint32_t x, uint32_t y);
+ uint32_t Capacity();
+ bool AdvanceCursor();
+ bool WriteBit(bool one);
+ bool Write8(unsigned char value);
+ bool ReadBit(unsigned char& value);
+ bool Read8(unsigned char& bit);
+
+ const static unsigned char sPixelSize = 3;
+ const static unsigned char sDigitWidth = 6;
+ const static unsigned char sDigitHeight = 7;
+ const static unsigned char sInterDigit = 1;
+ const static unsigned char sInterLine = 1;
+ const static uint32_t sBitSize = 4;
+ const static uint32_t sBitThreshold = 60;
+ const static unsigned char sYOn = 0x80;
+ const static unsigned char sYOff = 0;
+ const static unsigned char sLumaThreshold = 96;
+ const static unsigned char sLumaMin = 16;
+ const static unsigned char sLumaMax = 235;
+
+ unsigned char* pYData;
+ uint32_t mStride;
+ uint32_t mWidth;
+ uint32_t mHeight;
+ unsigned char mSymbolWidth;
+ unsigned char mSymbolHeight;
+
+ struct Cursor {
+ Cursor(uint32_t x, uint32_t y) : x(x), y(y) {}
+ uint32_t x;
+ uint32_t y;
+ } mCursor;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/common/browser_logging/CSFLog.cpp b/dom/media/webrtc/common/browser_logging/CSFLog.cpp
new file mode 100644
index 0000000000..d58f5726a5
--- /dev/null
+++ b/dom/media/webrtc/common/browser_logging/CSFLog.cpp
@@ -0,0 +1,85 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <stdio.h>
+#include <string.h>
+#include <stdarg.h>
+
+#include "CSFLog.h"
+
+#include <map>
+#include "prrwlock.h"
+#include "prthread.h"
+#include "nsThreadUtils.h"
+
+#include "mozilla/Logging.h"
+#include "mozilla/Sprintf.h"
+
+mozilla::LazyLogModule gSignalingLog("signaling");
+
+void CSFLogV(CSFLogLevel priority, const char* sourceFile, int sourceLine,
+ const char* tag, const char* format, va_list args) {
+#ifdef STDOUT_LOGGING
+ printf("%s\n:", tag);
+ vprintf(format, args);
+#else
+
+ mozilla::LogLevel level =
+ static_cast<mozilla::LogLevel>(static_cast<unsigned int>(priority));
+
+ // Skip doing any of this work if we're not logging the indicated level...
+ if (!MOZ_LOG_TEST(gSignalingLog, level)) {
+ return;
+ }
+
+ // Trim the path component from the filename
+ const char* lastSlash = sourceFile;
+ while (*sourceFile) {
+ if (*sourceFile == '/' || *sourceFile == '\\') {
+ lastSlash = sourceFile;
+ }
+ sourceFile++;
+ }
+ sourceFile = lastSlash;
+ if (*sourceFile == '/' || *sourceFile == '\\') {
+ sourceFile++;
+ }
+
+# define MAX_MESSAGE_LENGTH 1024
+ char message[MAX_MESSAGE_LENGTH];
+
+ const char* threadName = NULL;
+
+ // Check if we're the main thread...
+ if (NS_IsMainThread()) {
+ threadName = "main";
+ } else {
+ threadName = PR_GetThreadName(PR_GetCurrentThread());
+ }
+
+ // If we can't find it anywhere, use a blank string
+ if (!threadName) {
+ threadName = "";
+ }
+
+ VsprintfLiteral(message, format, args);
+ MOZ_LOG(
+ gSignalingLog, level,
+ ("[%s|%s] %s:%d: %s", threadName, tag, sourceFile, sourceLine, message));
+#endif
+}
+
+void CSFLog(CSFLogLevel priority, const char* sourceFile, int sourceLine,
+ const char* tag, const char* format, ...) {
+ va_list ap;
+ va_start(ap, format);
+
+ CSFLogV(priority, sourceFile, sourceLine, tag, format, ap);
+ va_end(ap);
+}
+
+int CSFLogTestLevel(CSFLogLevel priority) {
+ return MOZ_LOG_TEST(gSignalingLog, static_cast<mozilla::LogLevel>(
+ static_cast<unsigned int>(priority)));
+}
diff --git a/dom/media/webrtc/common/browser_logging/CSFLog.h b/dom/media/webrtc/common/browser_logging/CSFLog.h
new file mode 100644
index 0000000000..eb46b37cc3
--- /dev/null
+++ b/dom/media/webrtc/common/browser_logging/CSFLog.h
@@ -0,0 +1,58 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef CSFLOG_H
+#define CSFLOG_H
+
+#include <stdarg.h>
+
+typedef enum {
+ CSF_LOG_ERROR = 1,
+ CSF_LOG_WARNING,
+ CSF_LOG_INFO,
+ CSF_LOG_DEBUG,
+ CSF_LOG_VERBOSE,
+} CSFLogLevel;
+
+#define CSFLogError(tag, format, ...) \
+ CSFLog(CSF_LOG_ERROR, __FILE__, __LINE__, tag, format, ##__VA_ARGS__)
+#define CSFLogErrorV(tag, format, va_list_arg) \
+ CSFLogV(CSF_LOG_ERROR, __FILE__, __LINE__, tag, format, va_list_arg)
+#define CSFLogWarn(tag, format, ...) \
+ CSFLog(CSF_LOG_WARNING, __FILE__, __LINE__, tag, format, ##__VA_ARGS__)
+#define CSFLogWarnV(tag, format, va_list_arg) \
+ CSFLogV(CSF_LOG_WARNING, __FILE__, __LINE__, tag, format, va_list_arg)
+#define CSFLogInfo(tag, format, ...) \
+ CSFLog(CSF_LOG_INFO, __FILE__, __LINE__, tag, format, ##__VA_ARGS__)
+#define CSFLogInfoV(tag, format, va_list_arg) \
+ CSFLogV(CSF_LOG_INFO, __FILE__, __LINE__, tag, format, va_list_arg)
+#define CSFLogDebug(tag, format, ...) \
+ CSFLog(CSF_LOG_DEBUG, __FILE__, __LINE__, tag, format, ##__VA_ARGS__)
+#define CSFLogDebugV(tag, format, va_list_arg) \
+ CSFLogV(CSF_LOG_DEBUG, __FILE__, __LINE__, tag, format, va_list_arg)
+#define CSFLogVerbose(tag, format, ...) \
+ CSFLog(CSF_LOG_VERBOSE, __FILE__, __LINE__, tag, format, ##__VA_ARGS__)
+#define CSFLogVerboseV(tag, format, va_list_arg) \
+ CSFLogV(CSF_LOG_VERBOSE, __FILE__, __LINE__, tag, format, va_list_arg)
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+void CSFLog(CSFLogLevel priority, const char* sourceFile, int sourceLine,
+ const char* tag, const char* format, ...)
+#ifdef __GNUC__
+ __attribute__((format(printf, 5, 6)))
+#endif
+ ;
+
+void CSFLogV(CSFLogLevel priority, const char* sourceFile, int sourceLine,
+ const char* tag, const char* format, va_list args);
+
+int CSFLogTestLevel(CSFLogLevel priority);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/dom/media/webrtc/common/browser_logging/WebRtcLog.cpp b/dom/media/webrtc/common/browser_logging/WebRtcLog.cpp
new file mode 100644
index 0000000000..0f3769604e
--- /dev/null
+++ b/dom/media/webrtc/common/browser_logging/WebRtcLog.cpp
@@ -0,0 +1,162 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WebRtcLog.h"
+
+#include "mozilla/Logging.h"
+#include "mozilla/StaticPtr.h"
+#include "prenv.h"
+#include "rtc_base/logging.h"
+
+#include "nscore.h"
+#include "nsString.h"
+#include "nsXULAppAPI.h"
+#include "mozilla/Preferences.h"
+
+#include "nsIFile.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsNativeCharsetUtils.h"
+
+using mozilla::LogLevel;
+
+static mozilla::LazyLogModule sWebRtcLog("webrtc_trace");
+static mozilla::LazyLogModule sLogAEC("AEC");
+
+class LogSinkImpl : public rtc::LogSink {
+ public:
+ LogSinkImpl() {}
+
+ private:
+ void OnLogMessage(const std::string& message) override {
+ MOZ_LOG(sWebRtcLog, LogLevel::Debug, ("%s", message.data()));
+ }
+};
+
+// For RTC_LOG()
+static mozilla::StaticAutoPtr<LogSinkImpl> sSink;
+
+void GetWebRtcLogPrefs() {
+ rtc::LogMessage::set_aec_debug_size(
+ mozilla::Preferences::GetUint("media.webrtc.debug.aec_dump_max_size"));
+}
+
+mozilla::LogLevel CheckOverrides() {
+ mozilla::LogModule* log_info = sWebRtcLog;
+ mozilla::LogLevel log_level = log_info->Level();
+
+ log_info = sLogAEC;
+ if (sLogAEC && (log_info->Level() != mozilla::LogLevel::Disabled)) {
+ rtc::LogMessage::set_aec_debug(true);
+ }
+
+ return log_level;
+}
+
+void ConfigWebRtcLog(mozilla::LogLevel level) {
+ rtc::LoggingSeverity log_level;
+ switch (level) {
+ case mozilla::LogLevel::Verbose:
+ log_level = rtc::LoggingSeverity::LS_VERBOSE;
+ break;
+ case mozilla::LogLevel::Debug:
+ case mozilla::LogLevel::Info:
+ log_level = rtc::LoggingSeverity::LS_INFO;
+ break;
+ case mozilla::LogLevel::Warning:
+ log_level = rtc::LoggingSeverity::LS_WARNING;
+ break;
+ case mozilla::LogLevel::Error:
+ log_level = rtc::LoggingSeverity::LS_ERROR;
+ break;
+ case mozilla::LogLevel::Disabled:
+ log_level = rtc::LoggingSeverity::LS_NONE;
+ break;
+ default:
+ MOZ_ASSERT(false);
+ break;
+ }
+ rtc::LogMessage::LogToDebug(log_level);
+ if (level != mozilla::LogLevel::Disabled) {
+ // always capture LOG(...) << ... logging in webrtc.org code to nspr logs
+ if (!sSink) {
+ sSink = new LogSinkImpl();
+ rtc::LogMessage::AddLogToStream(sSink, log_level);
+ // it's ok if this leaks to program end
+ }
+ } else if (sSink) {
+ rtc::LogMessage::RemoveLogToStream(sSink);
+ sSink = nullptr;
+ }
+}
+
+void StartWebRtcLog(mozilla::LogLevel log_level) {
+ if (log_level == mozilla::LogLevel::Disabled) {
+ return;
+ }
+
+ GetWebRtcLogPrefs();
+ mozilla::LogLevel level = CheckOverrides();
+
+ ConfigWebRtcLog(level);
+}
+
+void EnableWebRtcLog() {
+ GetWebRtcLogPrefs();
+ mozilla::LogLevel level = CheckOverrides();
+ ConfigWebRtcLog(level);
+}
+
+// Called when we destroy the singletons from PeerConnectionCtx or if the
+// user changes logging in about:webrtc
+void StopWebRtcLog() {
+ if (sSink) {
+ rtc::LogMessage::RemoveLogToStream(sSink);
+ sSink = nullptr;
+ }
+}
+
+nsCString ConfigAecLog() {
+ nsCString aecLogDir;
+ if (rtc::LogMessage::aec_debug()) {
+ return ""_ns;
+ }
+#if defined(ANDROID)
+ const char* default_tmp_dir = "/dev/null";
+ aecLogDir.Assign(default_tmp_dir);
+#else
+ nsCOMPtr<nsIFile> tempDir;
+ nsresult rv = NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(tempDir));
+ if (NS_SUCCEEDED(rv)) {
+# ifdef XP_WIN
+ // WebRTC wants a path encoded in the native charset, not UTF-8.
+ nsAutoString temp;
+ tempDir->GetPath(temp);
+ NS_CopyUnicodeToNative(temp, aecLogDir);
+# else
+ tempDir->GetNativePath(aecLogDir);
+# endif
+ }
+#endif
+ rtc::LogMessage::set_aec_debug_filename(aecLogDir.get());
+
+ return aecLogDir;
+}
+
+nsCString StartAecLog() {
+ nsCString aecLogDir;
+ if (rtc::LogMessage::aec_debug()) {
+ return ""_ns;
+ }
+
+ GetWebRtcLogPrefs();
+ CheckOverrides();
+ aecLogDir = ConfigAecLog();
+
+ rtc::LogMessage::set_aec_debug(true);
+
+ return aecLogDir;
+}
+
+void StopAecLog() { rtc::LogMessage::set_aec_debug(false); }
diff --git a/dom/media/webrtc/common/browser_logging/WebRtcLog.h b/dom/media/webrtc/common/browser_logging/WebRtcLog.h
new file mode 100644
index 0000000000..b933ff43a5
--- /dev/null
+++ b/dom/media/webrtc/common/browser_logging/WebRtcLog.h
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef WEBRTCLOG_H_
+#define WEBRTCLOG_H_
+
+#include "mozilla/Logging.h"
+#include "nsStringFwd.h"
+
+nsCString StartAecLog();
+void StopAecLog();
+void EnableWebRtcLog();
+void StartWebRtcLog(mozilla::LogLevel level);
+void StopWebRtcLog();
+
+#endif
diff --git a/dom/media/webrtc/common/csf_common.h b/dom/media/webrtc/common/csf_common.h
new file mode 100644
index 0000000000..8b01047553
--- /dev/null
+++ b/dom/media/webrtc/common/csf_common.h
@@ -0,0 +1,90 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _CSF_COMMON_E58E5677_950A_424c_B6C2_CA180092E6A2_H
+#define _CSF_COMMON_E58E5677_950A_424c_B6C2_CA180092E6A2_H
+
+#include <assert.h>
+#include <memory>
+#include <vector>
+#include <stdlib.h>
+
+/*
+
+This header file defines:
+
+csf_countof
+csf_sprintf
+csf_vsprintf
+
+*/
+
+/*
+ General security tip: Ensure that "format" is never a user-defined string.
+ Format should ALWAYS be something that's built into your code, not user
+ supplied. For example: never write:
+
+ csf_sprintf(buffer, csf_countof(buffer), pUserSuppliedString);
+
+ Instead write:
+
+ csf_sprintf(buffer, csf_countof(buffer), "%s", pUserSuppliedString);
+
+*/
+
+#ifdef WIN32
+# if !defined(_countof)
+# if !defined(__cplusplus)
+# define _countof(_Array) (sizeof(_Array) / sizeof(_Array[0]))
+# else
+extern "C++" {
+template <typename _CountofType, size_t _SizeOfArray>
+char (*_csf_countof_helper(_CountofType (&_Array)[_SizeOfArray]))[_SizeOfArray];
+# define _countof(_Array) sizeof(*_csf_countof_helper(_Array))
+}
+# endif
+# endif
+#else
+# define _countof(_Array) (sizeof(_Array) / sizeof(_Array[0]))
+#endif
+// csf_countof
+
+#define csf_countof(anArray) _countof(anArray)
+
+// csf_sprintf
+
+#ifdef _WIN32
+// Unlike snprintf, sprintf_s guarantees that the buffer will be null-terminated
+// (unless the buffer size is zero).
+# define csf_sprintf(/* char* */ buffer, \
+ /* size_t */ sizeOfBufferInCharsInclNullTerm, \
+ /* const char * */ format, ...) \
+ _snprintf_s(buffer, sizeOfBufferInCharsInclNullTerm, _TRUNCATE, format, \
+ __VA_ARGS__)
+#else
+# define csf_sprintf(/* char */ buffer, \
+ /* size_t */ sizeOfBufferInCharsInclNullTerm, \
+ /* const char * */ format, ...) \
+ snprintf(buffer, sizeOfBufferInCharsInclNullTerm, format, __VA_ARGS__); \
+ buffer[sizeOfBufferInCharsInclNullTerm - 1] = '\0'
+#endif
+
+// csf_vsprintf
+
+#ifdef _WIN32
+# define csf_vsprintf(/* char* */ buffer, \
+ /* size_t */ sizeOfBufferInCharsInclNullTerm, \
+ /* const char * */ format, /* va_list */ vaList) \
+ vsnprintf_s(buffer, sizeOfBufferInCharsInclNullTerm, _TRUNCATE, format, \
+ vaList); \
+ buffer[sizeOfBufferInCharsInclNullTerm - 1] = '\0'
+#else
+# define csf_vsprintf(/* char */ buffer, \
+ /* size_t */ sizeOfBufferInCharsInclNullTerm, \
+ /* const char * */ format, /* va_list */ vaList) \
+ vsprintf(buffer, format, vaList); \
+ buffer[sizeOfBufferInCharsInclNullTerm - 1] = '\0'
+#endif
+
+#endif
diff --git a/dom/media/webrtc/common/moz.build b/dom/media/webrtc/common/moz.build
new file mode 100644
index 0000000000..7605d27835
--- /dev/null
+++ b/dom/media/webrtc/common/moz.build
@@ -0,0 +1,23 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+include("/dom/media/webrtc/third_party_build/webrtc.mozbuild")
+
+EXPORTS.mozilla.dom += ["CandidateInfo.h"]
+
+LOCAL_INCLUDES += [
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr",
+ "/third_party/libwebrtc",
+ "/third_party/libwebrtc/third_party/abseil-cpp",
+]
+
+UNIFIED_SOURCES += [
+ "browser_logging/CSFLog.cpp",
+ "browser_logging/WebRtcLog.cpp",
+ "time_profiling/timecard.c",
+ "YuvStamper.cpp",
+]
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/webrtc/common/time_profiling/timecard.c b/dom/media/webrtc/common/time_profiling/timecard.c
new file mode 100644
index 0000000000..c0218cb92d
--- /dev/null
+++ b/dom/media/webrtc/common/time_profiling/timecard.c
@@ -0,0 +1,112 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <stdio.h>
+#include <string.h>
+#include "timecard.h"
+#include "mozilla/mozalloc.h"
+
+Timecard* create_timecard() {
+ Timecard* tc = moz_xcalloc(1, sizeof(Timecard));
+ tc->entries_allocated = TIMECARD_INITIAL_TABLE_SIZE;
+ tc->entries = moz_xcalloc(tc->entries_allocated, sizeof(TimecardEntry));
+ tc->start_time = PR_Now();
+ return tc;
+}
+
+void destroy_timecard(Timecard* tc) {
+ free(tc->entries);
+ free(tc);
+}
+
+void stamp_timecard(Timecard* tc, const char* event, const char* file,
+ unsigned int line, const char* function) {
+ TimecardEntry* entry = NULL;
+
+ /* Trim the path component from the filename */
+ const char* last_slash = file;
+ while (*file) {
+ if (*file == '/' || *file == '\\') {
+ last_slash = file;
+ }
+ file++;
+ }
+ file = last_slash;
+ if (*file == '/' || *file == '\\') {
+ file++;
+ }
+
+ /* Ensure there is enough space left in the entries list */
+ if (tc->curr_entry == tc->entries_allocated) {
+ tc->entries_allocated *= 2;
+ tc->entries = moz_xrealloc(tc->entries,
+ tc->entries_allocated * sizeof(TimecardEntry));
+ }
+
+ /* Record the data into the timecard entry */
+ entry = &tc->entries[tc->curr_entry];
+ entry->timestamp = PR_Now();
+ entry->event = event;
+ entry->file = file;
+ entry->line = line;
+ entry->function = function;
+ tc->curr_entry++;
+}
+
+void print_timecard(Timecard* tc) {
+ size_t i;
+ TimecardEntry* entry;
+ size_t event_width = 5;
+ size_t file_width = 4;
+ size_t function_width = 8;
+ size_t line_width;
+ PRTime offset, delta;
+
+ for (i = 0; i < tc->curr_entry; i++) {
+ entry = &tc->entries[i];
+ if (strlen(entry->event) > event_width) {
+ event_width = strlen(entry->event);
+ }
+ if (strlen(entry->file) > file_width) {
+ file_width = strlen(entry->file);
+ }
+ if (strlen(entry->function) > function_width) {
+ function_width = strlen(entry->function);
+ }
+ }
+
+ printf("\nTimecard created %4ld.%6.6ld\n\n",
+ (long)(tc->start_time / PR_USEC_PER_SEC),
+ (long)(tc->start_time % PR_USEC_PER_SEC));
+
+ line_width =
+ 1 + 11 + 11 + event_width + file_width + 6 + function_width + (4 * 3);
+
+ printf(" %-11s | %-11s | %-*s | %-*s | %-*s\n", "Timestamp", "Delta",
+ (int)event_width, "Event", (int)file_width + 6, "File",
+ (int)function_width, "Function");
+
+ for (i = 0; i <= line_width; i++) {
+ printf("=");
+ }
+ printf("\n");
+
+ for (i = 0; i < tc->curr_entry; i++) {
+ entry = &tc->entries[i];
+ offset = entry->timestamp - tc->start_time;
+ if (i > 0) {
+ delta = entry->timestamp - tc->entries[i - 1].timestamp;
+ } else {
+ delta = entry->timestamp - tc->start_time;
+ }
+ printf(" %4ld.%6.6ld | %4ld.%6.6ld | %-*s | %*s:%-5d | %-*s\n",
+ (long)(offset / PR_USEC_PER_SEC), (long)(offset % PR_USEC_PER_SEC),
+ (long)(delta / PR_USEC_PER_SEC), (long)(delta % PR_USEC_PER_SEC),
+ (int)event_width, entry->event, (int)file_width, entry->file,
+ entry->line, (int)function_width, entry->function);
+ }
+ printf("\n");
+}
diff --git a/dom/media/webrtc/common/time_profiling/timecard.h b/dom/media/webrtc/common/time_profiling/timecard.h
new file mode 100644
index 0000000000..38d4a8d1ec
--- /dev/null
+++ b/dom/media/webrtc/common/time_profiling/timecard.h
@@ -0,0 +1,74 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef timecard_h__
+#define timecard_h__
+
+#include <stdlib.h>
+#include "prtime.h"
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+#define STAMP_TIMECARD(card, event) \
+ do { \
+ if (card) { \
+ stamp_timecard((card), (event), __FILE__, __LINE__, __FUNCTION__); \
+ } \
+ } while (0)
+
+#define TIMECARD_INITIAL_TABLE_SIZE 16
+
+/*
+ * The "const char *" members of this structure point to static strings.
+ * We do not own them, and should not attempt to deallocate them.
+ */
+
+typedef struct {
+ PRTime timestamp;
+ const char* event;
+ const char* file;
+ unsigned int line;
+ const char* function;
+} TimecardEntry;
+
+typedef struct Timecard {
+ size_t curr_entry;
+ size_t entries_allocated;
+ TimecardEntry* entries;
+ PRTime start_time;
+} Timecard;
+
+/**
+ * Creates a new Timecard structure for tracking events.
+ */
+Timecard* create_timecard();
+
+/**
+ * Frees the memory associated with a timecard. After returning, the
+ * timecard pointed to by tc is no longer valid.
+ */
+void destroy_timecard(Timecard* tc);
+
+/**
+ * Records a new event in the indicated timecard. This should not be
+ * called directly; code should instead use the STAMP_TIMECARD macro,
+ * above.
+ */
+void stamp_timecard(Timecard* tc, const char* event, const char* file,
+ unsigned int line, const char* function);
+
+/**
+ * Formats and outputs the contents of a timecard onto stdout.
+ */
+void print_timecard(Timecard* tc);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif
diff --git a/dom/media/webrtc/jsapi/MediaTransportHandler.cpp b/dom/media/webrtc/jsapi/MediaTransportHandler.cpp
new file mode 100644
index 0000000000..3ee95f36f6
--- /dev/null
+++ b/dom/media/webrtc/jsapi/MediaTransportHandler.cpp
@@ -0,0 +1,1727 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaTransportHandler.h"
+#include "MediaTransportHandlerIPC.h"
+#include "transport/nricemediastream.h"
+#include "transport/nriceresolver.h"
+#include "transport/transportflow.h"
+#include "transport/transportlayerice.h"
+#include "transport/transportlayerdtls.h"
+#include "transport/transportlayersrtp.h"
+
+// Config stuff
+#include "mozilla/dom/RTCConfigurationBinding.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_network.h"
+
+// Parsing STUN/TURN URIs
+#include "nsIURI.h"
+#include "nsNetUtil.h"
+#include "nsURLHelper.h"
+#include "nsIURLParser.h"
+
+// Logging stuff
+#include "common/browser_logging/CSFLog.h"
+
+// For fetching ICE logging
+#include "transport/rlogconnector.h"
+
+// DTLS
+#include "sdp/SdpAttribute.h"
+
+#include "transport/runnable_utils.h"
+
+#include "mozilla/Algorithm.h"
+#include "mozilla/Telemetry.h"
+
+#include "mozilla/dom/RTCStatsReportBinding.h"
+
+#include "nss.h" // For NSS_NoDB_Init
+#include "mozilla/PublicSSL.h" // For psm::InitializeCipherSuite
+
+#include "nsISocketTransportService.h"
+#include "nsDNSService2.h"
+
+#include <string>
+#include <vector>
+#include <map>
+
+#ifdef MOZ_GECKO_PROFILER
+# include "mozilla/ProfilerMarkers.h"
+
+# define MEDIA_TRANSPORT_HANDLER_PACKET_RECEIVED(aPacket) \
+ PROFILER_MARKER_TEXT("WebRTC Packet Received", MEDIA_RT, {}, \
+ ProfilerString8View::WrapNullTerminatedString( \
+ PacketTypeToString((aPacket).type())));
+#else
+# define MEDIA_TRANSPORT_HANDLER_PACKET_RECEIVED(aPacket)
+#endif
+
+namespace mozilla {
+
+static const char* mthLogTag = "MediaTransportHandler";
+#ifdef LOGTAG
+# undef LOGTAG
+#endif
+#define LOGTAG mthLogTag
+
+class MediaTransportHandlerSTS : public MediaTransportHandler,
+ public sigslot::has_slots<> {
+ public:
+ explicit MediaTransportHandlerSTS(nsISerialEventTarget* aCallbackThread);
+
+ RefPtr<IceLogPromise> GetIceLog(const nsCString& aPattern) override;
+ void ClearIceLog() override;
+ void EnterPrivateMode() override;
+ void ExitPrivateMode() override;
+
+ void CreateIceCtx(const std::string& aName) override;
+
+ nsresult SetIceConfig(const nsTArray<dom::RTCIceServer>& aIceServers,
+ dom::RTCIceTransportPolicy aIcePolicy) override;
+
+ // We will probably be able to move the proxy lookup stuff into
+ // this class once we move mtransport to its own process.
+ void SetProxyConfig(NrSocketProxyConfig&& aProxyConfig) override;
+
+ void EnsureProvisionalTransport(const std::string& aTransportId,
+ const std::string& aUfrag,
+ const std::string& aPwd,
+ int aComponentCount) override;
+
+ void SetTargetForDefaultLocalAddressLookup(const std::string& aTargetIp,
+ uint16_t aTargetPort) override;
+
+ // We set default-route-only as late as possible because it depends on what
+ // capture permissions have been granted on the window, which could easily
+ // change between Init (ie; when the PC is created) and StartIceGathering
+ // (ie; when we set the local description).
+ void StartIceGathering(bool aDefaultRouteOnly, bool aObfuscateHostAddresses,
+ // This will go away once mtransport moves to its
+ // own process, because we won't need to get this
+ // via IPC anymore
+ const nsTArray<NrIceStunAddr>& aStunAddrs) override;
+
+ void ActivateTransport(
+ const std::string& aTransportId, const std::string& aLocalUfrag,
+ const std::string& aLocalPwd, size_t aComponentCount,
+ const std::string& aUfrag, const std::string& aPassword,
+ const nsTArray<uint8_t>& aKeyDer, const nsTArray<uint8_t>& aCertDer,
+ SSLKEAType aAuthType, bool aDtlsClient, const DtlsDigestList& aDigests,
+ bool aPrivacyRequested) override;
+
+ void RemoveTransportsExcept(
+ const std::set<std::string>& aTransportIds) override;
+
+ void StartIceChecks(bool aIsControlling,
+ const std::vector<std::string>& aIceOptions) override;
+
+ void AddIceCandidate(const std::string& aTransportId,
+ const std::string& aCandidate, const std::string& aUfrag,
+ const std::string& aObfuscatedAddress) override;
+
+ void UpdateNetworkState(bool aOnline) override;
+
+ void SendPacket(const std::string& aTransportId,
+ MediaPacket&& aPacket) override;
+
+ RefPtr<dom::RTCStatsPromise> GetIceStats(const std::string& aTransportId,
+ DOMHighResTimeStamp aNow) override;
+
+ void Shutdown();
+
+ private:
+ void Destroy() override;
+ void Destroy_s();
+ void DestroyFinal();
+ void Shutdown_s();
+ RefPtr<TransportFlow> CreateTransportFlow(
+ const std::string& aTransportId, bool aIsRtcp,
+ const RefPtr<DtlsIdentity>& aDtlsIdentity, bool aDtlsClient,
+ const DtlsDigestList& aDigests, bool aPrivacyRequested);
+
+ struct Transport {
+ RefPtr<TransportFlow> mFlow;
+ RefPtr<TransportFlow> mRtcpFlow;
+ };
+
+ using MediaTransportHandler::OnAlpnNegotiated;
+ using MediaTransportHandler::OnCandidate;
+ using MediaTransportHandler::OnConnectionStateChange;
+ using MediaTransportHandler::OnEncryptedSending;
+ using MediaTransportHandler::OnGatheringStateChange;
+ using MediaTransportHandler::OnPacketReceived;
+ using MediaTransportHandler::OnRtcpStateChange;
+ using MediaTransportHandler::OnStateChange;
+
+ void OnGatheringStateChange(NrIceCtx* aIceCtx,
+ NrIceCtx::GatheringState aState);
+ void OnConnectionStateChange(NrIceCtx* aIceCtx,
+ NrIceCtx::ConnectionState aState);
+ void OnCandidateFound(NrIceMediaStream* aStream,
+ const std::string& aCandidate,
+ const std::string& aUfrag, const std::string& aMDNSAddr,
+ const std::string& aActualAddr);
+ void OnStateChange(TransportLayer* aLayer, TransportLayer::State);
+ void OnRtcpStateChange(TransportLayer* aLayer, TransportLayer::State);
+ void PacketReceived(TransportLayer* aLayer, MediaPacket& aPacket);
+ void EncryptedPacketSending(TransportLayer* aLayer, MediaPacket& aPacket);
+ RefPtr<TransportFlow> GetTransportFlow(const std::string& aTransportId,
+ bool aIsRtcp) const;
+ void GetIceStats(const NrIceMediaStream& aStream, DOMHighResTimeStamp aNow,
+ dom::RTCStatsCollection* aStats) const;
+
+ virtual ~MediaTransportHandlerSTS() = default;
+ nsCOMPtr<nsISerialEventTarget> mStsThread;
+ RefPtr<NrIceCtx> mIceCtx;
+ RefPtr<NrIceResolver> mDNSResolver;
+ std::map<std::string, Transport> mTransports;
+ bool mObfuscateHostAddresses = false;
+ bool mTurnDisabled = false;
+ uint32_t mMinDtlsVersion = 0;
+ uint32_t mMaxDtlsVersion = 0;
+ bool mForceNoHost = false;
+ Maybe<NrIceCtx::NatSimulatorConfig> mNatConfig;
+
+ std::set<std::string> mSignaledAddresses;
+
+ // Init can only be done on main, but we want this to be usable on any thread
+ using InitPromise = MozPromise<bool, std::string, false>;
+ RefPtr<InitPromise> mInitPromise;
+};
+
+/* static */
+already_AddRefed<MediaTransportHandler> MediaTransportHandler::Create(
+ nsISerialEventTarget* aCallbackThread) {
+ RefPtr<MediaTransportHandler> result;
+ if (XRE_IsContentProcess() &&
+ Preferences::GetBool("media.peerconnection.mtransport_process") &&
+ StaticPrefs::network_process_enabled()) {
+ result = new MediaTransportHandlerIPC(aCallbackThread);
+ } else {
+ result = new MediaTransportHandlerSTS(aCallbackThread);
+ }
+ result->Initialize();
+ return result.forget();
+}
+
+class STSShutdownHandler : public nsISTSShutdownObserver {
+ public:
+ NS_DECL_ISUPPORTS
+
+ // Lazy singleton
+ static RefPtr<STSShutdownHandler>& Instance() {
+ MOZ_ASSERT(NS_IsMainThread());
+ static RefPtr<STSShutdownHandler> sHandler(new STSShutdownHandler);
+ return sHandler;
+ }
+
+ void Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ for (const auto& handler : mHandlers) {
+ handler->Shutdown();
+ }
+ mHandlers.clear();
+ }
+
+ STSShutdownHandler() {
+ CSFLogDebug(LOGTAG, "%s", __func__);
+ nsresult res;
+ nsCOMPtr<nsISocketTransportService> sts =
+ do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &res);
+ MOZ_RELEASE_ASSERT(NS_SUCCEEDED(res));
+ MOZ_RELEASE_ASSERT(sts);
+ sts->AddShutdownObserver(this);
+ }
+
+ NS_IMETHOD Observe() override {
+ CSFLogDebug(LOGTAG, "%s", __func__);
+ Shutdown();
+ nsresult res;
+ nsCOMPtr<nsISocketTransportService> sts =
+ do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &res);
+ MOZ_RELEASE_ASSERT(NS_SUCCEEDED(res));
+ MOZ_RELEASE_ASSERT(sts);
+ sts->RemoveShutdownObserver(this);
+ Instance() = nullptr;
+ return NS_OK;
+ }
+
+ void Register(MediaTransportHandlerSTS* aHandler) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mHandlers.insert(aHandler);
+ }
+
+ void Deregister(MediaTransportHandlerSTS* aHandler) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mHandlers.erase(aHandler);
+ }
+
+ private:
+ virtual ~STSShutdownHandler() = default;
+
+ // Raw ptrs, registered on init, deregistered on destruction, all on main
+ std::set<MediaTransportHandlerSTS*> mHandlers;
+};
+
+NS_IMPL_ISUPPORTS(STSShutdownHandler, nsISTSShutdownObserver);
+
+MediaTransportHandlerSTS::MediaTransportHandlerSTS(
+ nsISerialEventTarget* aCallbackThread)
+ : MediaTransportHandler(aCallbackThread) {
+ nsresult rv;
+ mStsThread = do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv);
+ if (!mStsThread) {
+ MOZ_CRASH();
+ }
+
+ RLogConnector::CreateInstance();
+
+ CSFLogDebug(LOGTAG, "%s done %p", __func__, this);
+
+ // We do not set up mDNSService here, because we are not running on main (we
+ // use PBackground), and the DNS service asserts.
+}
+
+static NrIceCtx::Policy toNrIcePolicy(dom::RTCIceTransportPolicy aPolicy) {
+ switch (aPolicy) {
+ case dom::RTCIceTransportPolicy::Relay:
+ return NrIceCtx::ICE_POLICY_RELAY;
+ case dom::RTCIceTransportPolicy::All:
+ return NrIceCtx::ICE_POLICY_ALL;
+ default:
+ MOZ_CRASH();
+ }
+ return NrIceCtx::ICE_POLICY_ALL;
+}
+
+// list of known acceptable ports for webrtc
+int16_t gGoodWebrtcPortList[] = {
+ 53, // Some deplyoments use DNS port to punch through overzealous NATs
+ 3478, // stun or turn
+ 5349, // stuns or turns
+ 0, // Sentinel value: This MUST be zero
+};
+
+static nsresult addNrIceServer(const nsString& aIceUrl,
+ const dom::RTCIceServer& aIceServer,
+ std::vector<NrIceStunServer>* aStunServersOut,
+ std::vector<NrIceTurnServer>* aTurnServersOut) {
+ // Without STUN/TURN handlers, NS_NewURI returns nsSimpleURI rather than
+ // nsStandardURL. To parse STUN/TURN URI's to spec
+ // http://tools.ietf.org/html/draft-nandakumar-rtcweb-stun-uri-02#section-3
+ // http://tools.ietf.org/html/draft-petithuguenin-behave-turn-uri-03#section-3
+ // we parse out the query-string, and use ParseAuthority() on the rest
+ RefPtr<nsIURI> url;
+ nsresult rv = NS_NewURI(getter_AddRefs(url), aIceUrl);
+ NS_ENSURE_SUCCESS(rv, rv);
+ bool isStun = url->SchemeIs("stun");
+ bool isStuns = url->SchemeIs("stuns");
+ bool isTurn = url->SchemeIs("turn");
+ bool isTurns = url->SchemeIs("turns");
+ if (!(isStun || isStuns || isTurn || isTurns)) {
+ return NS_ERROR_FAILURE;
+ }
+ if (isStuns) {
+ return NS_OK; // TODO: Support STUNS (Bug 1056934)
+ }
+
+ nsAutoCString spec;
+ rv = url->GetSpec(spec);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // TODO(jib@mozilla.com): Revisit once nsURI supports STUN/TURN (Bug 833509)
+ int32_t port;
+ nsAutoCString host;
+ nsAutoCString transport;
+ {
+ uint32_t hostPos;
+ int32_t hostLen;
+ nsAutoCString path;
+ rv = url->GetPathQueryRef(path);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // Tolerate query-string + parse 'transport=[udp|tcp]' by hand.
+ int32_t questionmark = path.FindChar('?');
+ if (questionmark >= 0) {
+ const nsCString match = "transport="_ns;
+
+ for (int32_t i = questionmark, endPos; i >= 0; i = endPos) {
+ endPos = path.FindCharInSet("&", i + 1);
+ const nsDependentCSubstring fieldvaluepair =
+ Substring(path, i + 1, endPos);
+ if (StringBeginsWith(fieldvaluepair, match)) {
+ transport = Substring(fieldvaluepair, match.Length());
+ ToLowerCase(transport);
+ }
+ }
+ path.SetLength(questionmark);
+ }
+
+ rv = net_GetAuthURLParser()->ParseAuthority(
+ path.get(), static_cast<int>(path.Length()), nullptr, nullptr, nullptr,
+ nullptr, &hostPos, &hostLen, &port);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!hostLen) {
+ return NS_ERROR_FAILURE;
+ }
+ if (hostPos > 1) {
+ /* The username was removed */
+ return NS_ERROR_FAILURE;
+ }
+ path.Mid(host, hostPos, hostLen);
+ // Strip off brackets around IPv6 literals
+ host.Trim("[]");
+ }
+ if (port == -1) port = (isStuns || isTurns) ? 5349 : 3478;
+
+ // First check the known good ports for webrtc
+ bool goodPort = false;
+ for (int i = 0; !goodPort && gGoodWebrtcPortList[i]; i++) {
+ if (port == gGoodWebrtcPortList[i]) {
+ goodPort = true;
+ }
+ }
+
+ // if not in the list of known good ports for webrtc, check
+ // the generic block list using NS_CheckPortSafety.
+ if (!goodPort) {
+ rv = NS_CheckPortSafety(port, nullptr);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (isStuns || isTurns) {
+ // Should we barf if transport is set to udp or something?
+ transport = kNrIceTransportTls;
+ }
+
+ if (transport.IsEmpty()) {
+ transport = kNrIceTransportUdp;
+ }
+
+ if (isTurn || isTurns) {
+ std::string pwd(
+ NS_ConvertUTF16toUTF8(aIceServer.mCredential.Value()).get());
+ std::string username(
+ NS_ConvertUTF16toUTF8(aIceServer.mUsername.Value()).get());
+
+ std::vector<unsigned char> password(pwd.begin(), pwd.end());
+
+ UniquePtr<NrIceTurnServer> server(NrIceTurnServer::Create(
+ host.get(), port, username, password, transport.get()));
+ if (!server) {
+ return NS_ERROR_FAILURE;
+ }
+ if (server->HasFqdn()) {
+ // Add an IPv4 entry, then an IPv6 entry
+ aTurnServersOut->push_back(*server);
+ server->SetUseIPv6IfFqdn();
+ }
+ aTurnServersOut->emplace_back(std::move(*server));
+ } else {
+ UniquePtr<NrIceStunServer> server(
+ NrIceStunServer::Create(host.get(), port, transport.get()));
+ if (!server) {
+ return NS_ERROR_FAILURE;
+ }
+ if (server->HasFqdn()) {
+ // Add an IPv4 entry, then an IPv6 entry
+ aStunServersOut->push_back(*server);
+ server->SetUseIPv6IfFqdn();
+ }
+ aStunServersOut->emplace_back(std::move(*server));
+ }
+ return NS_OK;
+}
+
+/* static */
+nsresult MediaTransportHandler::ConvertIceServers(
+ const nsTArray<dom::RTCIceServer>& aIceServers,
+ std::vector<NrIceStunServer>* aStunServers,
+ std::vector<NrIceTurnServer>* aTurnServers) {
+ for (const auto& iceServer : aIceServers) {
+ NS_ENSURE_STATE(iceServer.mUrls.WasPassed());
+ NS_ENSURE_STATE(iceServer.mUrls.Value().IsStringSequence());
+ for (const auto& iceUrl : iceServer.mUrls.Value().GetAsStringSequence()) {
+ nsresult rv =
+ addNrIceServer(iceUrl, iceServer, aStunServers, aTurnServers);
+ if (NS_FAILED(rv)) {
+ CSFLogError(LOGTAG, "%s: invalid STUN/TURN server: %s", __FUNCTION__,
+ NS_ConvertUTF16toUTF8(iceUrl).get());
+ return rv;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+static NrIceCtx::GlobalConfig GetGlobalConfig() {
+ NrIceCtx::GlobalConfig config;
+ config.mAllowLinkLocal =
+ Preferences::GetBool("media.peerconnection.ice.link_local", false);
+ config.mAllowLoopback =
+ Preferences::GetBool("media.peerconnection.ice.loopback", false);
+ config.mTcpEnabled =
+ Preferences::GetBool("media.peerconnection.ice.tcp", false);
+ config.mStunClientMaxTransmits = Preferences::GetInt(
+ "media.peerconnection.ice.stun_client_maximum_transmits",
+ config.mStunClientMaxTransmits);
+ config.mTrickleIceGracePeriod =
+ Preferences::GetInt("media.peerconnection.ice.trickle_grace_period",
+ config.mTrickleIceGracePeriod);
+ config.mIceTcpSoSockCount = Preferences::GetInt(
+ "media.peerconnection.ice.tcp_so_sock_count", config.mIceTcpSoSockCount);
+ config.mIceTcpListenBacklog =
+ Preferences::GetInt("media.peerconnection.ice.tcp_listen_backlog",
+ config.mIceTcpListenBacklog);
+ (void)Preferences::GetCString("media.peerconnection.ice.force_interface",
+ config.mForceNetInterface);
+ return config;
+}
+
+static Maybe<NrIceCtx::NatSimulatorConfig> GetNatConfig() {
+ bool block_tcp = Preferences::GetBool(
+ "media.peerconnection.nat_simulator.block_tcp", false);
+ bool block_udp = Preferences::GetBool(
+ "media.peerconnection.nat_simulator.block_udp", false);
+ bool block_tls = Preferences::GetBool(
+ "media.peerconnection.nat_simulator.block_tls", false);
+ int error_code_for_drop = Preferences::GetInt(
+ "media.peerconnection.nat_simulator.error_code_for_drop", 0);
+ nsAutoCString mapping_type;
+ (void)Preferences::GetCString(
+ "media.peerconnection.nat_simulator.mapping_type", mapping_type);
+ nsAutoCString filtering_type;
+ (void)Preferences::GetCString(
+ "media.peerconnection.nat_simulator.filtering_type", filtering_type);
+ nsAutoCString redirect_address;
+ (void)Preferences::GetCString(
+ "media.peerconnection.nat_simulator.redirect_address", redirect_address);
+ nsAutoCString redirect_targets;
+ (void)Preferences::GetCString(
+ "media.peerconnection.nat_simulator.redirect_targets", redirect_targets);
+
+ if (block_udp || block_tcp || block_tls || !mapping_type.IsEmpty() ||
+ !filtering_type.IsEmpty() || !redirect_address.IsEmpty()) {
+ CSFLogDebug(LOGTAG, "NAT filtering type: %s", filtering_type.get());
+ CSFLogDebug(LOGTAG, "NAT mapping type: %s", mapping_type.get());
+ NrIceCtx::NatSimulatorConfig natConfig;
+ natConfig.mBlockUdp = block_udp;
+ natConfig.mBlockTcp = block_tcp;
+ natConfig.mBlockTls = block_tls;
+ natConfig.mErrorCodeForDrop = error_code_for_drop;
+ natConfig.mFilteringType = filtering_type;
+ natConfig.mMappingType = mapping_type;
+ if (redirect_address.Length()) {
+ CSFLogDebug(LOGTAG, "Redirect address: %s", redirect_address.get());
+ CSFLogDebug(LOGTAG, "Redirect targets: %s", redirect_targets.get());
+ natConfig.mRedirectAddress = redirect_address;
+ std::stringstream str(redirect_targets.Data());
+ std::string target;
+ while (getline(str, target, ',')) {
+ CSFLogDebug(LOGTAG, "Adding target: %s", target.c_str());
+ natConfig.mRedirectTargets.AppendElement(target);
+ }
+ }
+ return Some(natConfig);
+ }
+ return Nothing();
+}
+
+void MediaTransportHandlerSTS::CreateIceCtx(const std::string& aName) {
+ mInitPromise = InvokeAsync(
+ GetMainThreadSerialEventTarget(), __func__,
+ [=, self = RefPtr<MediaTransportHandlerSTS>(this)]() {
+ CSFLogDebug(LOGTAG, "%s starting", __func__);
+ if (!NSS_IsInitialized()) {
+ if (NSS_NoDB_Init(nullptr) != SECSuccess) {
+ MOZ_CRASH();
+ return InitPromise::CreateAndReject("NSS_NoDB_Init failed",
+ __func__);
+ }
+
+ if (NS_FAILED(mozilla::psm::InitializeCipherSuite())) {
+ MOZ_CRASH();
+ return InitPromise::CreateAndReject("InitializeCipherSuite failed",
+ __func__);
+ }
+
+ mozilla::psm::DisableMD5();
+ }
+
+ static bool globalInitDone = false;
+ if (!globalInitDone) {
+ // Ensure the DNS service is initted for the first time on main
+ DebugOnly<RefPtr<nsIDNSService>> dnsService =
+ RefPtr<nsIDNSService>(nsDNSService::GetXPCOMSingleton());
+ MOZ_ASSERT(dnsService.value);
+ mStsThread->Dispatch(
+ WrapRunnableNM(&NrIceCtx::InitializeGlobals, GetGlobalConfig()),
+ NS_DISPATCH_NORMAL);
+ globalInitDone = true;
+ }
+
+ // Give us a way to globally turn off TURN support
+ mTurnDisabled =
+ Preferences::GetBool("media.peerconnection.turn.disable", false);
+ // We are reading these here, because when we setup the DTLS transport
+ // we are on the wrong thread to read prefs
+ mMinDtlsVersion =
+ Preferences::GetUint("media.peerconnection.dtls.version.min");
+ mMaxDtlsVersion =
+ Preferences::GetUint("media.peerconnection.dtls.version.max");
+ mForceNoHost =
+ Preferences::GetBool("media.peerconnection.ice.no_host", false);
+ mNatConfig = GetNatConfig();
+
+ MOZ_RELEASE_ASSERT(STSShutdownHandler::Instance());
+ STSShutdownHandler::Instance()->Register(this);
+
+ return InvokeAsync(
+ mStsThread, __func__,
+ [=, self = RefPtr<MediaTransportHandlerSTS>(this)]() {
+ mIceCtx = NrIceCtx::Create(aName);
+ if (!mIceCtx) {
+ return InitPromise::CreateAndReject("NrIceCtx::Create failed",
+ __func__);
+ }
+
+ mIceCtx->SignalGatheringStateChange.connect(
+ this, &MediaTransportHandlerSTS::OnGatheringStateChange);
+ mIceCtx->SignalConnectionStateChange.connect(
+ this, &MediaTransportHandlerSTS::OnConnectionStateChange);
+
+ mDNSResolver = new NrIceResolver;
+ nsresult rv;
+ if (NS_FAILED(rv = mDNSResolver->Init())) {
+ CSFLogError(LOGTAG, "%s: Failed to initialize dns resolver",
+ __FUNCTION__);
+ return InitPromise::CreateAndReject(
+ "Failed to initialize dns resolver", __func__);
+ }
+ if (NS_FAILED(rv = mIceCtx->SetResolver(
+ mDNSResolver->AllocateResolver()))) {
+ CSFLogError(LOGTAG, "%s: Failed to get dns resolver",
+ __FUNCTION__);
+ return InitPromise::CreateAndReject(
+ "Failed to get dns resolver", __func__);
+ }
+
+ CSFLogDebug(LOGTAG, "%s done", __func__);
+ return InitPromise::CreateAndResolve(true, __func__);
+ });
+ });
+}
+
+nsresult MediaTransportHandlerSTS::SetIceConfig(
+ const nsTArray<dom::RTCIceServer>& aIceServers,
+ dom::RTCIceTransportPolicy aIcePolicy) {
+ // We rely on getting an error when this happens, so do it up front.
+ std::vector<NrIceStunServer> stunServers;
+ std::vector<NrIceTurnServer> turnServers;
+ nsresult rv = ConvertIceServers(aIceServers, &stunServers, &turnServers);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ MOZ_RELEASE_ASSERT(mInitPromise);
+
+ mInitPromise->Then(
+ mStsThread, __func__,
+ [=, self = RefPtr<MediaTransportHandlerSTS>(this)]() {
+ if (!mIceCtx) {
+ CSFLogError(LOGTAG, "%s: mIceCtx is null", __FUNCTION__);
+ return;
+ }
+ NrIceCtx::Config config;
+ config.mPolicy = toNrIcePolicy(aIcePolicy);
+ if (config.mPolicy == NrIceCtx::ICE_POLICY_ALL && mForceNoHost) {
+ config.mPolicy = NrIceCtx::ICE_POLICY_NO_HOST;
+ }
+ config.mNatSimulatorConfig = mNatConfig;
+
+ nsresult rv;
+
+ if (NS_FAILED(rv = mIceCtx->SetStunServers(stunServers))) {
+ CSFLogError(LOGTAG, "%s: Failed to set stun servers", __FUNCTION__);
+ return;
+ }
+ if (!mTurnDisabled) {
+ if (NS_FAILED(rv = mIceCtx->SetTurnServers(turnServers))) {
+ CSFLogError(LOGTAG, "%s: Failed to set turn servers", __FUNCTION__);
+ return;
+ }
+ } else if (!turnServers.empty()) {
+ CSFLogError(LOGTAG, "%s: Setting turn servers disabled",
+ __FUNCTION__);
+ }
+ if (NS_FAILED(rv = mIceCtx->SetIceConfig(config))) {
+ CSFLogError(LOGTAG, "%s: Failed to set config", __FUNCTION__);
+ }
+ });
+
+ return NS_OK;
+}
+
+void MediaTransportHandlerSTS::Shutdown() {
+ CSFLogDebug(LOGTAG, "%s", __func__);
+ MOZ_ASSERT(NS_IsMainThread());
+ mStsThread->Dispatch(NewNonOwningRunnableMethod(
+ __func__, this, &MediaTransportHandlerSTS::Shutdown_s));
+}
+
+void MediaTransportHandlerSTS::Shutdown_s() {
+ CSFLogDebug(LOGTAG, "%s", __func__);
+ disconnect_all();
+ // Clear the transports before destroying the ice ctx so that
+ // the close_notify alerts have a chance to be sent as the
+ // TransportFlow destructors execute.
+ mTransports.clear();
+ if (mIceCtx) {
+ NrIceStats stats = mIceCtx->Destroy();
+ CSFLogDebug(LOGTAG,
+ "Ice Telemetry: stun (retransmits: %d)"
+ " turn (401s: %d 403s: %d 438s: %d)",
+ stats.stun_retransmits, stats.turn_401s, stats.turn_403s,
+ stats.turn_438s);
+ }
+ mIceCtx = nullptr;
+ mDNSResolver = nullptr;
+}
+
+void MediaTransportHandlerSTS::Destroy() {
+ CSFLogDebug(LOGTAG, "%s %p", __func__, this);
+ // Our "destruction tour" starts on main, because we need to deregister.
+ if (!NS_IsMainThread()) {
+ GetMainThreadSerialEventTarget()->Dispatch(
+ NewNonOwningRunnableMethod("MediaTransportHandlerSTS::Destroy", this,
+ &MediaTransportHandlerSTS::Destroy));
+ return;
+ }
+
+ MOZ_ASSERT(NS_IsMainThread());
+ if (STSShutdownHandler::Instance()) {
+ STSShutdownHandler::Instance()->Deregister(this);
+ Shutdown();
+ }
+
+ // mIceCtx still has a reference to us via sigslot! We must dispach to STS,
+ // and clean up there. However, by the time _that_ happens, we may have
+ // dispatched a signal callback to mCallbackThread, so we have to dispatch
+ // the final destruction to mCallbackThread.
+ nsresult rv = mStsThread->Dispatch(
+ NewNonOwningRunnableMethod("MediaTransportHandlerSTS::Destroy_s", this,
+ &MediaTransportHandlerSTS::Destroy_s));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ CSFLogError(LOGTAG,
+ "Unable to dispatch to STS: why has the XPCOM shutdown handler "
+ "not been invoked?");
+ delete this;
+ }
+}
+
+void MediaTransportHandlerSTS::Destroy_s() {
+ if (mCallbackThread && !mCallbackThread->IsOnCurrentThread()) {
+ nsresult rv = mCallbackThread->Dispatch(NewNonOwningRunnableMethod(
+ __func__, this, &MediaTransportHandlerSTS::DestroyFinal));
+ if (NS_SUCCEEDED(rv)) {
+ return;
+ }
+ }
+
+ DestroyFinal();
+}
+
+void MediaTransportHandlerSTS::DestroyFinal() { delete this; }
+
+void MediaTransportHandlerSTS::SetProxyConfig(
+ NrSocketProxyConfig&& aProxyConfig) {
+ MOZ_RELEASE_ASSERT(mInitPromise);
+
+ mInitPromise->Then(
+ mStsThread, __func__,
+ [this, self = RefPtr<MediaTransportHandlerSTS>(this),
+ aProxyConfig = std::move(aProxyConfig)]() mutable {
+ if (!mIceCtx) {
+ return; // Probably due to XPCOM shutdown
+ }
+
+ mIceCtx->SetProxyConfig(std::move(aProxyConfig));
+ },
+ [](const std::string& aError) {});
+}
+
+void MediaTransportHandlerSTS::EnsureProvisionalTransport(
+ const std::string& aTransportId, const std::string& aUfrag,
+ const std::string& aPwd, int aComponentCount) {
+ MOZ_RELEASE_ASSERT(mInitPromise);
+
+ mInitPromise->Then(
+ mStsThread, __func__,
+ [=, self = RefPtr<MediaTransportHandlerSTS>(this)]() {
+ if (!mIceCtx) {
+ return; // Probably due to XPCOM shutdown
+ }
+
+ RefPtr<NrIceMediaStream> stream(mIceCtx->GetStream(aTransportId));
+ if (!stream) {
+ CSFLogDebug(LOGTAG, "%s: Creating ICE media stream=%s components=%d",
+ mIceCtx->name().c_str(), aTransportId.c_str(),
+ aComponentCount);
+
+ std::ostringstream os;
+ os << mIceCtx->name() << " transport-id=" << aTransportId;
+ stream =
+ mIceCtx->CreateStream(aTransportId, os.str(), aComponentCount);
+
+ if (!stream) {
+ CSFLogError(LOGTAG, "Failed to create ICE stream.");
+ return;
+ }
+
+ stream->SignalCandidate.connect(
+ this, &MediaTransportHandlerSTS::OnCandidateFound);
+ }
+
+ // Begins an ICE restart if this stream has a different ufrag/pwd
+ stream->SetIceCredentials(aUfrag, aPwd);
+
+ // Make sure there's an entry in mTransports
+ mTransports[aTransportId];
+ },
+ [](const std::string& aError) {});
+}
+
+void MediaTransportHandlerSTS::ActivateTransport(
+ const std::string& aTransportId, const std::string& aLocalUfrag,
+ const std::string& aLocalPwd, size_t aComponentCount,
+ const std::string& aUfrag, const std::string& aPassword,
+ const nsTArray<uint8_t>& aKeyDer, const nsTArray<uint8_t>& aCertDer,
+ SSLKEAType aAuthType, bool aDtlsClient, const DtlsDigestList& aDigests,
+ bool aPrivacyRequested) {
+ MOZ_RELEASE_ASSERT(mInitPromise);
+
+ mInitPromise->Then(
+ mStsThread, __func__,
+ [=, keyDer = aKeyDer.Clone(), certDer = aCertDer.Clone(),
+ self = RefPtr<MediaTransportHandlerSTS>(this)]() {
+ if (!mIceCtx) {
+ return; // Probably due to XPCOM shutdown
+ }
+
+ MOZ_ASSERT(aComponentCount);
+ RefPtr<DtlsIdentity> dtlsIdentity(
+ DtlsIdentity::Deserialize(keyDer, certDer, aAuthType));
+ if (!dtlsIdentity) {
+ MOZ_ASSERT(false);
+ return;
+ }
+
+ RefPtr<NrIceMediaStream> stream(mIceCtx->GetStream(aTransportId));
+ if (!stream) {
+ MOZ_ASSERT(false);
+ return;
+ }
+
+ CSFLogDebug(LOGTAG, "%s: Activating ICE media stream=%s components=%u",
+ mIceCtx->name().c_str(), aTransportId.c_str(),
+ static_cast<unsigned>(aComponentCount));
+
+ std::vector<std::string> attrs;
+ attrs.reserve(2 /* ufrag + pwd */);
+ attrs.push_back("ice-ufrag:" + aUfrag);
+ attrs.push_back("ice-pwd:" + aPassword);
+
+ // If we started an ICE restart in EnsureProvisionalTransport, this is
+ // where we decide whether to commit or rollback.
+ nsresult rv = stream->ConnectToPeer(aLocalUfrag, aLocalPwd, attrs);
+ if (NS_FAILED(rv)) {
+ CSFLogError(LOGTAG, "Couldn't parse ICE attributes, rv=%u",
+ static_cast<unsigned>(rv));
+ MOZ_ASSERT(false);
+ return;
+ }
+
+ Transport transport = mTransports[aTransportId];
+ if (!transport.mFlow) {
+ transport.mFlow =
+ CreateTransportFlow(aTransportId, false, dtlsIdentity,
+ aDtlsClient, aDigests, aPrivacyRequested);
+ if (!transport.mFlow) {
+ return;
+ }
+ TransportLayer* dtls =
+ transport.mFlow->GetLayer(TransportLayerDtls::ID());
+ dtls->SignalStateChange.connect(
+ this, &MediaTransportHandlerSTS::OnStateChange);
+ if (aComponentCount < 2) {
+ dtls->SignalStateChange.connect(
+ this, &MediaTransportHandlerSTS::OnRtcpStateChange);
+ }
+ }
+
+ if (aComponentCount == 2) {
+ if (!transport.mRtcpFlow) {
+ transport.mRtcpFlow =
+ CreateTransportFlow(aTransportId, true, dtlsIdentity,
+ aDtlsClient, aDigests, aPrivacyRequested);
+ if (!transport.mRtcpFlow) {
+ return;
+ }
+ TransportLayer* dtls =
+ transport.mRtcpFlow->GetLayer(TransportLayerDtls::ID());
+ dtls->SignalStateChange.connect(
+ this, &MediaTransportHandlerSTS::OnRtcpStateChange);
+ }
+ } else {
+ transport.mRtcpFlow = nullptr;
+ // components are 1-indexed
+ stream->DisableComponent(2);
+ }
+
+ mTransports[aTransportId] = transport;
+ },
+ [](const std::string& aError) {});
+}
+
+void MediaTransportHandlerSTS::SetTargetForDefaultLocalAddressLookup(
+ const std::string& aTargetIp, uint16_t aTargetPort) {
+ MOZ_RELEASE_ASSERT(mInitPromise);
+
+ mInitPromise->Then(
+ mStsThread, __func__,
+ [=, self = RefPtr<MediaTransportHandlerSTS>(this)]() {
+ if (!mIceCtx) {
+ return; // Probably due to XPCOM shutdown
+ }
+
+ mIceCtx->SetTargetForDefaultLocalAddressLookup(aTargetIp, aTargetPort);
+ },
+ [](const std::string& aError) {});
+}
+
+void MediaTransportHandlerSTS::StartIceGathering(
+ bool aDefaultRouteOnly, bool aObfuscateHostAddresses,
+ const nsTArray<NrIceStunAddr>& aStunAddrs) {
+ MOZ_RELEASE_ASSERT(mInitPromise);
+
+ mInitPromise->Then(
+ mStsThread, __func__,
+ [=, stunAddrs = aStunAddrs.Clone(),
+ self = RefPtr<MediaTransportHandlerSTS>(this)]() {
+ if (!mIceCtx) {
+ return; // Probably due to XPCOM shutdown
+ }
+
+ mObfuscateHostAddresses = aObfuscateHostAddresses;
+
+ // Belt and suspenders - in e10s mode, the call below to SetStunAddrs
+ // needs to have the proper flags set on ice ctx. For non-e10s,
+ // setting those flags happens in StartGathering. We could probably
+ // just set them here, and only do it here.
+ mIceCtx->SetCtxFlags(aDefaultRouteOnly);
+
+ if (stunAddrs.Length()) {
+ mIceCtx->SetStunAddrs(stunAddrs);
+ }
+
+ // Start gathering, but only if there are streams
+ if (!mIceCtx->GetStreams().empty()) {
+ mIceCtx->StartGathering(aDefaultRouteOnly, aObfuscateHostAddresses);
+ }
+ },
+ [](const std::string& aError) {});
+}
+
+void MediaTransportHandlerSTS::StartIceChecks(
+ bool aIsControlling, const std::vector<std::string>& aIceOptions) {
+ MOZ_RELEASE_ASSERT(mInitPromise);
+
+ mInitPromise->Then(
+ mStsThread, __func__,
+ [=, self = RefPtr<MediaTransportHandlerSTS>(this)]() {
+ if (!mIceCtx) {
+ return; // Probably due to XPCOM shutdown
+ }
+
+ nsresult rv = mIceCtx->ParseGlobalAttributes(aIceOptions);
+ if (NS_FAILED(rv)) {
+ CSFLogError(LOGTAG, "%s: couldn't parse global parameters",
+ __FUNCTION__);
+ return;
+ }
+
+ rv = mIceCtx->SetControlling(aIsControlling ? NrIceCtx::ICE_CONTROLLING
+ : NrIceCtx::ICE_CONTROLLED);
+ if (NS_FAILED(rv)) {
+ CSFLogError(LOGTAG, "%s: couldn't set controlling to %d",
+ __FUNCTION__, aIsControlling);
+ return;
+ }
+
+ rv = mIceCtx->StartChecks();
+ if (NS_FAILED(rv)) {
+ CSFLogError(LOGTAG, "%s: couldn't start checks", __FUNCTION__);
+ return;
+ }
+ },
+ [](const std::string& aError) {});
+}
+
+void TokenizeCandidate(const std::string& aCandidate,
+ std::vector<std::string>& aTokens) {
+ aTokens.clear();
+
+ std::istringstream iss(aCandidate);
+ std::string token;
+ while (std::getline(iss, token, ' ')) {
+ aTokens.push_back(token);
+ }
+}
+
+void MediaTransportHandlerSTS::AddIceCandidate(
+ const std::string& aTransportId, const std::string& aCandidate,
+ const std::string& aUfrag, const std::string& aObfuscatedAddress) {
+ MOZ_RELEASE_ASSERT(mInitPromise);
+
+ mInitPromise->Then(
+ mStsThread, __func__,
+ [=, self = RefPtr<MediaTransportHandlerSTS>(this)]() {
+ if (!mIceCtx) {
+ return; // Probably due to XPCOM shutdown
+ }
+
+ std::vector<std::string> tokens;
+ TokenizeCandidate(aCandidate, tokens);
+
+ RefPtr<NrIceMediaStream> stream(mIceCtx->GetStream(aTransportId));
+ if (!stream) {
+ CSFLogError(LOGTAG,
+ "No ICE stream for candidate with transport id %s: %s",
+ aTransportId.c_str(), aCandidate.c_str());
+ return;
+ }
+
+ nsresult rv = stream->ParseTrickleCandidate(aCandidate, aUfrag,
+ aObfuscatedAddress);
+ if (NS_SUCCEEDED(rv)) {
+ // If the address is not obfuscated, we want to track it as
+ // explicitly signaled so that we know it is fine to reveal
+ // the address later on.
+ if (mObfuscateHostAddresses && tokens.size() > 4 &&
+ aObfuscatedAddress.empty()) {
+ mSignaledAddresses.insert(tokens[4]);
+ }
+ } else {
+ CSFLogError(LOGTAG,
+ "Couldn't process ICE candidate with transport id %s: "
+ "%s",
+ aTransportId.c_str(), aCandidate.c_str());
+ }
+ },
+ [](const std::string& aError) {});
+}
+
+void MediaTransportHandlerSTS::UpdateNetworkState(bool aOnline) {
+ MOZ_RELEASE_ASSERT(mInitPromise);
+
+ mInitPromise->Then(
+ mStsThread, __func__,
+ [=, self = RefPtr<MediaTransportHandlerSTS>(this)]() {
+ if (!mIceCtx) {
+ return; // Probably due to XPCOM shutdown
+ }
+
+ mIceCtx->UpdateNetworkState(aOnline);
+ },
+ [](const std::string& aError) {});
+}
+
+void MediaTransportHandlerSTS::RemoveTransportsExcept(
+ const std::set<std::string>& aTransportIds) {
+ MOZ_RELEASE_ASSERT(mInitPromise);
+
+ mInitPromise->Then(
+ mStsThread, __func__,
+ [=, self = RefPtr<MediaTransportHandlerSTS>(this)]() {
+ if (!mIceCtx) {
+ return; // Probably due to XPCOM shutdown
+ }
+
+ for (auto it = mTransports.begin(); it != mTransports.end();) {
+ const std::string transportId(it->first);
+ if (!aTransportIds.count(transportId)) {
+ if (it->second.mFlow) {
+ OnStateChange(transportId, TransportLayer::TS_NONE);
+ OnRtcpStateChange(transportId, TransportLayer::TS_NONE);
+ }
+ // Erase the transport before destroying the ice stream so that
+ // the close_notify alerts have a chance to be sent as the
+ // TransportFlow destructors execute.
+ it = mTransports.erase(it);
+ // We're already on the STS thread, but the TransportFlow
+ // destructor executed when mTransports.erase(it) is called
+ // above dispatches the call to DestroyFinal to the STS thread. If
+ // we don't also dispatch the call to destroy the NrIceMediaStream
+ // to the STS thread, it will tear down the NrIceMediaStream
+ // before the TransportFlow is destroyed. Without a valid
+ // NrIceMediaStream the close_notify alert cannot be sent.
+ mStsThread->Dispatch(NS_NewRunnableFunction(
+ __func__, [iceCtx = RefPtr<NrIceCtx>(mIceCtx), transportId] {
+ iceCtx->DestroyStream(transportId);
+ }));
+ } else {
+ MOZ_ASSERT(it->second.mFlow);
+ ++it;
+ }
+ }
+ },
+ [](const std::string& aError) {});
+}
+
+void MediaTransportHandlerSTS::SendPacket(const std::string& aTransportId,
+ MediaPacket&& aPacket) {
+ MOZ_RELEASE_ASSERT(mInitPromise);
+
+ mInitPromise->Then(
+ mStsThread, __func__,
+ [this, self = RefPtr<MediaTransportHandlerSTS>(this), aTransportId,
+ aPacket = std::move(aPacket)]() mutable {
+ if (!mIceCtx) {
+ return; // Probably due to XPCOM shutdown
+ }
+
+ MOZ_ASSERT(aPacket.type() != MediaPacket::UNCLASSIFIED);
+ RefPtr<TransportFlow> flow =
+ GetTransportFlow(aTransportId, aPacket.type() == MediaPacket::RTCP);
+
+ if (!flow) {
+ CSFLogError(LOGTAG,
+ "%s: No such transport flow (%s) for outgoing packet",
+ mIceCtx->name().c_str(), aTransportId.c_str());
+ return;
+ }
+
+ TransportLayer* layer = nullptr;
+ switch (aPacket.type()) {
+ case MediaPacket::SCTP:
+ layer = flow->GetLayer(TransportLayerDtls::ID());
+ break;
+ case MediaPacket::RTP:
+ case MediaPacket::RTCP:
+ layer = flow->GetLayer(TransportLayerSrtp::ID());
+ break;
+ default:
+ // Maybe it would be useful to allow the injection of other packet
+ // types for testing?
+ MOZ_ASSERT(false);
+ return;
+ }
+
+ MOZ_ASSERT(layer);
+
+ if (layer->SendPacket(aPacket) < 0) {
+ CSFLogError(LOGTAG, "%s: Transport flow (%s) failed to send packet",
+ mIceCtx->name().c_str(), aTransportId.c_str());
+ }
+ },
+ [](const std::string& aError) {});
+}
+
+TransportLayer::State MediaTransportHandler::GetState(
+ const std::string& aTransportId, bool aRtcp) const {
+ // TODO Bug 1520692: we should allow Datachannel to connect without
+ // DTLS SRTP keys
+ if (mCallbackThread) {
+ MOZ_ASSERT(mCallbackThread->IsOnCurrentThread());
+ }
+
+ const std::map<std::string, TransportLayer::State>* cache = nullptr;
+ if (aRtcp) {
+ cache = &mRtcpStateCache;
+ } else {
+ cache = &mStateCache;
+ }
+
+ auto it = cache->find(aTransportId);
+ if (it != cache->end()) {
+ return it->second;
+ }
+ return TransportLayer::TS_NONE;
+}
+
+void MediaTransportHandler::OnCandidate(const std::string& aTransportId,
+ const CandidateInfo& aCandidateInfo) {
+ if (mCallbackThread && !mCallbackThread->IsOnCurrentThread()) {
+ mCallbackThread->Dispatch(
+ // This is being called from sigslot, which does not hold a strong ref.
+ WrapRunnable(this, &MediaTransportHandler::OnCandidate, aTransportId,
+ aCandidateInfo),
+ NS_DISPATCH_NORMAL);
+ return;
+ }
+
+ SignalCandidate(aTransportId, aCandidateInfo);
+}
+
+void MediaTransportHandler::OnAlpnNegotiated(const std::string& aAlpn) {
+ if (mCallbackThread && !mCallbackThread->IsOnCurrentThread()) {
+ mCallbackThread->Dispatch(
+ // This is being called from sigslot, which does not hold a strong ref.
+ WrapRunnable(this, &MediaTransportHandler::OnAlpnNegotiated, aAlpn),
+ NS_DISPATCH_NORMAL);
+ return;
+ }
+
+ const bool privacyRequested = aAlpn == "c-webrtc";
+ SignalAlpnNegotiated(aAlpn, privacyRequested);
+}
+
+void MediaTransportHandler::OnGatheringStateChange(
+ dom::RTCIceGatheringState aState) {
+ if (mCallbackThread && !mCallbackThread->IsOnCurrentThread()) {
+ mCallbackThread->Dispatch(
+ // This is being called from sigslot, which does not hold a strong ref.
+ WrapRunnable(this, &MediaTransportHandler::OnGatheringStateChange,
+ aState),
+ NS_DISPATCH_NORMAL);
+ return;
+ }
+
+ SignalGatheringStateChange(aState);
+}
+
+void MediaTransportHandler::OnConnectionStateChange(
+ dom::RTCIceConnectionState aState) {
+ if (mCallbackThread && !mCallbackThread->IsOnCurrentThread()) {
+ mCallbackThread->Dispatch(
+ // This is being called from sigslot, which does not hold a strong ref.
+ WrapRunnable(this, &MediaTransportHandler::OnConnectionStateChange,
+ aState),
+ NS_DISPATCH_NORMAL);
+ return;
+ }
+
+ SignalConnectionStateChange(aState);
+}
+
+void MediaTransportHandler::OnPacketReceived(const std::string& aTransportId,
+ const MediaPacket& aPacket) {
+ if (mCallbackThread && !mCallbackThread->IsOnCurrentThread()) {
+ mCallbackThread->Dispatch(
+ // This is being called from sigslot, which does not hold a strong ref.
+ WrapRunnable(this, &MediaTransportHandler::OnPacketReceived,
+ aTransportId, aPacket.Clone()),
+ NS_DISPATCH_NORMAL);
+ return;
+ }
+
+ SignalPacketReceived(aTransportId, aPacket);
+}
+
+void MediaTransportHandler::OnEncryptedSending(const std::string& aTransportId,
+ const MediaPacket& aPacket) {
+ if (mCallbackThread && !mCallbackThread->IsOnCurrentThread()) {
+ mCallbackThread->Dispatch(
+ // This is being called from sigslot, which does not hold a strong ref.
+ WrapRunnable(this, &MediaTransportHandler::OnEncryptedSending,
+ aTransportId, aPacket.Clone()),
+ NS_DISPATCH_NORMAL);
+ return;
+ }
+
+ SignalEncryptedSending(aTransportId, aPacket);
+}
+
+void MediaTransportHandler::OnStateChange(const std::string& aTransportId,
+ TransportLayer::State aState) {
+ if (mCallbackThread && !mCallbackThread->IsOnCurrentThread()) {
+ mCallbackThread->Dispatch(
+ // This is being called from sigslot, which does not hold a strong ref.
+ WrapRunnable(this, &MediaTransportHandler::OnStateChange, aTransportId,
+ aState),
+ NS_DISPATCH_NORMAL);
+ return;
+ }
+
+ if (aState == TransportLayer::TS_NONE) {
+ mStateCache.erase(aTransportId);
+ } else {
+ mStateCache[aTransportId] = aState;
+ }
+ SignalStateChange(aTransportId, aState);
+}
+
+void MediaTransportHandler::OnRtcpStateChange(const std::string& aTransportId,
+ TransportLayer::State aState) {
+ if (mCallbackThread && !mCallbackThread->IsOnCurrentThread()) {
+ mCallbackThread->Dispatch(
+ // This is being called from sigslot, which does not hold a strong ref.
+ WrapRunnable(this, &MediaTransportHandler::OnRtcpStateChange,
+ aTransportId, aState),
+ NS_DISPATCH_NORMAL);
+ return;
+ }
+
+ if (aState == TransportLayer::TS_NONE) {
+ mRtcpStateCache.erase(aTransportId);
+ } else {
+ mRtcpStateCache[aTransportId] = aState;
+ }
+ SignalRtcpStateChange(aTransportId, aState);
+}
+
+RefPtr<dom::RTCStatsPromise> MediaTransportHandlerSTS::GetIceStats(
+ const std::string& aTransportId, DOMHighResTimeStamp aNow) {
+ MOZ_RELEASE_ASSERT(mInitPromise);
+
+ return mInitPromise->Then(mStsThread, __func__, [=, self = RefPtr(this)]() {
+ UniquePtr<dom::RTCStatsCollection> stats(new dom::RTCStatsCollection);
+ if (mIceCtx) {
+ for (const auto& stream : mIceCtx->GetStreams()) {
+ if (aTransportId.empty() || aTransportId == stream->GetId()) {
+ GetIceStats(*stream, aNow, stats.get());
+ }
+ }
+ }
+ return dom::RTCStatsPromise::CreateAndResolve(std::move(stats), __func__);
+ });
+}
+
+RefPtr<MediaTransportHandler::IceLogPromise>
+MediaTransportHandlerSTS::GetIceLog(const nsCString& aPattern) {
+ return InvokeAsync(
+ mStsThread, __func__, [=, self = RefPtr<MediaTransportHandlerSTS>(this)] {
+ dom::Sequence<nsString> converted;
+ RLogConnector* logs = RLogConnector::GetInstance();
+ std::deque<std::string> result;
+ // Might not exist yet.
+ if (logs) {
+ logs->Filter(aPattern.get(), 0, &result);
+ }
+ /// XXX(Bug 1631386) Check if we should reject the promise instead of
+ /// crashing in an OOM situation.
+ if (!converted.SetCapacity(result.size(), fallible)) {
+ mozalloc_handle_oom(sizeof(nsString) * result.size());
+ }
+ for (auto& line : result) {
+ // Cannot fail, SetCapacity was called before.
+ (void)converted.AppendElement(NS_ConvertUTF8toUTF16(line.c_str()),
+ fallible);
+ }
+ return IceLogPromise::CreateAndResolve(std::move(converted), __func__);
+ });
+}
+
+void MediaTransportHandlerSTS::ClearIceLog() {
+ if (!mStsThread->IsOnCurrentThread()) {
+ mStsThread->Dispatch(WrapRunnable(RefPtr<MediaTransportHandlerSTS>(this),
+ &MediaTransportHandlerSTS::ClearIceLog),
+ NS_DISPATCH_NORMAL);
+ return;
+ }
+
+ RLogConnector* logs = RLogConnector::GetInstance();
+ if (logs) {
+ logs->Clear();
+ }
+}
+
+void MediaTransportHandlerSTS::EnterPrivateMode() {
+ if (!mStsThread->IsOnCurrentThread()) {
+ mStsThread->Dispatch(
+ WrapRunnable(RefPtr<MediaTransportHandlerSTS>(this),
+ &MediaTransportHandlerSTS::EnterPrivateMode),
+ NS_DISPATCH_NORMAL);
+ return;
+ }
+
+ RLogConnector::GetInstance()->EnterPrivateMode();
+}
+
+void MediaTransportHandlerSTS::ExitPrivateMode() {
+ if (!mStsThread->IsOnCurrentThread()) {
+ mStsThread->Dispatch(
+ WrapRunnable(RefPtr<MediaTransportHandlerSTS>(this),
+ &MediaTransportHandlerSTS::ExitPrivateMode),
+ NS_DISPATCH_NORMAL);
+ return;
+ }
+
+ auto* log = RLogConnector::GetInstance();
+ MOZ_ASSERT(log);
+ if (log) {
+ log->ExitPrivateMode();
+ }
+}
+
+static void ToRTCIceCandidateStats(
+ const std::vector<NrIceCandidate>& candidates,
+ dom::RTCStatsType candidateType, const nsString& transportId,
+ DOMHighResTimeStamp now, dom::RTCStatsCollection* stats,
+ bool obfuscateHostAddresses,
+ const std::set<std::string>& signaledAddresses) {
+ MOZ_ASSERT(stats);
+ for (const auto& candidate : candidates) {
+ dom::RTCIceCandidateStats cand;
+ cand.mType.Construct(candidateType);
+ NS_ConvertASCIItoUTF16 codeword(candidate.codeword.c_str());
+ cand.mTransportId.Construct(transportId);
+ cand.mId.Construct(codeword);
+ cand.mTimestamp.Construct(now);
+ cand.mCandidateType.Construct(dom::RTCIceCandidateType(candidate.type));
+ cand.mPriority.Construct(candidate.priority);
+ // https://tools.ietf.org/html/draft-ietf-rtcweb-mdns-ice-candidates-03#section-3.3.1
+ // This obfuscates the address with the mDNS address if one exists
+ if (!candidate.mdns_addr.empty()) {
+ cand.mAddress.Construct(
+ NS_ConvertASCIItoUTF16(candidate.mdns_addr.c_str()));
+ } else if (obfuscateHostAddresses &&
+ candidate.type == NrIceCandidate::ICE_PEER_REFLEXIVE &&
+ signaledAddresses.find(candidate.cand_addr.host) ==
+ signaledAddresses.end()) {
+ cand.mAddress.Construct(NS_ConvertASCIItoUTF16("(redacted)"));
+ } else {
+ cand.mAddress.Construct(
+ NS_ConvertASCIItoUTF16(candidate.cand_addr.host.c_str()));
+ }
+ cand.mPort.Construct(candidate.cand_addr.port);
+ cand.mProtocol.Construct(
+ NS_ConvertASCIItoUTF16(candidate.cand_addr.transport.c_str()));
+ if (candidateType == dom::RTCStatsType::Local_candidate &&
+ dom::RTCIceCandidateType(candidate.type) ==
+ dom::RTCIceCandidateType::Relay) {
+ cand.mRelayProtocol.Construct(
+ NS_ConvertASCIItoUTF16(candidate.local_addr.transport.c_str()));
+ }
+ cand.mProxied.Construct(NS_ConvertASCIItoUTF16(
+ candidate.is_proxied ? "proxied" : "non-proxied"));
+ if (!stats->mIceCandidateStats.AppendElement(cand, fallible)) {
+ // XXX(Bug 1632090) Instead of extending the array 1-by-1 (which might
+ // involve multiple reallocations) and potentially crashing here,
+ // SetCapacity could be called outside the loop once.
+ mozalloc_handle_oom(0);
+ }
+ if (candidate.trickled) {
+ if (!stats->mTrickledIceCandidateStats.AppendElement(cand, fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ }
+ }
+}
+
+void MediaTransportHandlerSTS::GetIceStats(
+ const NrIceMediaStream& aStream, DOMHighResTimeStamp aNow,
+ dom::RTCStatsCollection* aStats) const {
+ MOZ_ASSERT(mStsThread->IsOnCurrentThread());
+
+ NS_ConvertASCIItoUTF16 transportId(aStream.GetId().c_str());
+
+ std::vector<NrIceCandidatePair> candPairs;
+ nsresult res = aStream.GetCandidatePairs(&candPairs);
+ if (NS_FAILED(res)) {
+ CSFLogError(LOGTAG,
+ "%s: Error getting candidate pairs for transport id \"%s\"",
+ __FUNCTION__, aStream.GetId().c_str());
+ return;
+ }
+
+ for (auto& candPair : candPairs) {
+ NS_ConvertASCIItoUTF16 codeword(candPair.codeword.c_str());
+ NS_ConvertASCIItoUTF16 localCodeword(candPair.local.codeword.c_str());
+ NS_ConvertASCIItoUTF16 remoteCodeword(candPair.remote.codeword.c_str());
+ // Only expose candidate-pair statistics to chrome, until we've thought
+ // through the implications of exposing it to content.
+
+ dom::RTCIceCandidatePairStats s;
+ s.mId.Construct(codeword);
+ s.mTransportId.Construct(transportId);
+ s.mTimestamp.Construct(aNow);
+ s.mType.Construct(dom::RTCStatsType::Candidate_pair);
+ s.mLocalCandidateId.Construct(localCodeword);
+ s.mRemoteCandidateId.Construct(remoteCodeword);
+ s.mNominated.Construct(candPair.nominated);
+ s.mWritable.Construct(candPair.writable);
+ s.mReadable.Construct(candPair.readable);
+ s.mPriority.Construct(candPair.priority);
+ s.mSelected.Construct(candPair.selected);
+ s.mBytesSent.Construct(candPair.bytes_sent);
+ s.mBytesReceived.Construct(candPair.bytes_recvd);
+ s.mLastPacketSentTimestamp.Construct(candPair.ms_since_last_send);
+ s.mLastPacketReceivedTimestamp.Construct(candPair.ms_since_last_recv);
+ s.mState.Construct(dom::RTCStatsIceCandidatePairState(candPair.state));
+ s.mComponentId.Construct(candPair.component_id);
+ if (!aStats->mIceCandidatePairStats.AppendElement(s, fallible)) {
+ // XXX(Bug 1632090) Instead of extending the array 1-by-1 (which might
+ // involve multiple reallocations) and potentially crashing here,
+ // SetCapacity could be called outside the loop once.
+ mozalloc_handle_oom(0);
+ }
+ }
+
+ std::vector<NrIceCandidate> candidates;
+ if (NS_SUCCEEDED(aStream.GetLocalCandidates(&candidates))) {
+ ToRTCIceCandidateStats(candidates, dom::RTCStatsType::Local_candidate,
+ transportId, aNow, aStats, mObfuscateHostAddresses,
+ mSignaledAddresses);
+ // add the local candidates unparsed string to a sequence
+ for (const auto& candidate : candidates) {
+ if (!aStats->mRawLocalCandidates.AppendElement(
+ NS_ConvertASCIItoUTF16(candidate.label.c_str()), fallible)) {
+ // XXX(Bug 1632090) Instead of extending the array 1-by-1 (which might
+ // involve multiple reallocations) and potentially crashing here,
+ // SetCapacity could be called outside the loop once.
+ mozalloc_handle_oom(0);
+ }
+ }
+ }
+ candidates.clear();
+
+ if (NS_SUCCEEDED(aStream.GetRemoteCandidates(&candidates))) {
+ ToRTCIceCandidateStats(candidates, dom::RTCStatsType::Remote_candidate,
+ transportId, aNow, aStats, mObfuscateHostAddresses,
+ mSignaledAddresses);
+ // add the remote candidates unparsed string to a sequence
+ for (const auto& candidate : candidates) {
+ if (!aStats->mRawRemoteCandidates.AppendElement(
+ NS_ConvertASCIItoUTF16(candidate.label.c_str()), fallible)) {
+ // XXX(Bug 1632090) Instead of extending the array 1-by-1 (which might
+ // involve multiple reallocations) and potentially crashing here,
+ // SetCapacity could be called outside the loop once.
+ mozalloc_handle_oom(0);
+ }
+ }
+ }
+}
+
+RefPtr<TransportFlow> MediaTransportHandlerSTS::GetTransportFlow(
+ const std::string& aTransportId, bool aIsRtcp) const {
+ auto it = mTransports.find(aTransportId);
+ if (it == mTransports.end()) {
+ return nullptr;
+ }
+
+ if (aIsRtcp) {
+ return it->second.mRtcpFlow ? it->second.mRtcpFlow : it->second.mFlow;
+ ;
+ }
+
+ return it->second.mFlow;
+}
+
+RefPtr<TransportFlow> MediaTransportHandlerSTS::CreateTransportFlow(
+ const std::string& aTransportId, bool aIsRtcp,
+ const RefPtr<DtlsIdentity>& aDtlsIdentity, bool aDtlsClient,
+ const DtlsDigestList& aDigests, bool aPrivacyRequested) {
+ nsresult rv;
+ RefPtr<TransportFlow> flow = new TransportFlow(aTransportId);
+
+ // The media streams are made on STS so we need to defer setup.
+ auto ice = MakeUnique<TransportLayerIce>();
+ auto dtls = MakeUnique<TransportLayerDtls>();
+ auto srtp = MakeUnique<TransportLayerSrtp>(*dtls);
+ dtls->SetRole(aDtlsClient ? TransportLayerDtls::CLIENT
+ : TransportLayerDtls::SERVER);
+
+ dtls->SetIdentity(aDtlsIdentity);
+
+ dtls->SetMinMaxVersion(
+ static_cast<TransportLayerDtls::Version>(mMinDtlsVersion),
+ static_cast<TransportLayerDtls::Version>(mMaxDtlsVersion));
+
+ for (const auto& digest : aDigests) {
+ rv = dtls->SetVerificationDigest(digest);
+ if (NS_FAILED(rv)) {
+ CSFLogError(LOGTAG, "Could not set fingerprint");
+ return nullptr;
+ }
+ }
+
+ std::vector<uint16_t> srtpCiphers =
+ TransportLayerDtls::GetDefaultSrtpCiphers();
+
+ rv = dtls->SetSrtpCiphers(srtpCiphers);
+ if (NS_FAILED(rv)) {
+ CSFLogError(LOGTAG, "Couldn't set SRTP ciphers");
+ return nullptr;
+ }
+
+ // Always permits negotiation of the confidential mode.
+ // Only allow non-confidential (which is an allowed default),
+ // if we aren't confidential.
+ std::set<std::string> alpn = {"c-webrtc"};
+ std::string alpnDefault;
+ if (!aPrivacyRequested) {
+ alpnDefault = "webrtc";
+ alpn.insert(alpnDefault);
+ }
+ rv = dtls->SetAlpn(alpn, alpnDefault);
+ if (NS_FAILED(rv)) {
+ CSFLogError(LOGTAG, "Couldn't set ALPN");
+ return nullptr;
+ }
+
+ ice->SetParameters(mIceCtx->GetStream(aTransportId), aIsRtcp ? 2 : 1);
+ NS_ENSURE_SUCCESS(ice->Init(), nullptr);
+ NS_ENSURE_SUCCESS(dtls->Init(), nullptr);
+ NS_ENSURE_SUCCESS(srtp->Init(), nullptr);
+ dtls->Chain(ice.get());
+ srtp->Chain(ice.get());
+
+ dtls->SignalPacketReceived.connect(this,
+ &MediaTransportHandlerSTS::PacketReceived);
+ srtp->SignalPacketReceived.connect(this,
+ &MediaTransportHandlerSTS::PacketReceived);
+ ice->SignalPacketSending.connect(
+ this, &MediaTransportHandlerSTS::EncryptedPacketSending);
+ flow->PushLayer(ice.release());
+ flow->PushLayer(dtls.release());
+ flow->PushLayer(srtp.release());
+ return flow;
+}
+
+static mozilla::dom::RTCIceGatheringState toDomIceGatheringState(
+ NrIceCtx::GatheringState aState) {
+ switch (aState) {
+ case NrIceCtx::ICE_CTX_GATHER_INIT:
+ return dom::RTCIceGatheringState::New;
+ case NrIceCtx::ICE_CTX_GATHER_STARTED:
+ return dom::RTCIceGatheringState::Gathering;
+ case NrIceCtx::ICE_CTX_GATHER_COMPLETE:
+ return dom::RTCIceGatheringState::Complete;
+ }
+ MOZ_CRASH();
+}
+
+void MediaTransportHandlerSTS::OnGatheringStateChange(
+ NrIceCtx* aIceCtx, NrIceCtx::GatheringState aState) {
+ OnGatheringStateChange(toDomIceGatheringState(aState));
+}
+
+static mozilla::dom::RTCIceConnectionState toDomIceConnectionState(
+ NrIceCtx::ConnectionState aState) {
+ switch (aState) {
+ case NrIceCtx::ICE_CTX_INIT:
+ return dom::RTCIceConnectionState::New;
+ case NrIceCtx::ICE_CTX_CHECKING:
+ return dom::RTCIceConnectionState::Checking;
+ case NrIceCtx::ICE_CTX_CONNECTED:
+ return dom::RTCIceConnectionState::Connected;
+ case NrIceCtx::ICE_CTX_COMPLETED:
+ return dom::RTCIceConnectionState::Completed;
+ case NrIceCtx::ICE_CTX_FAILED:
+ return dom::RTCIceConnectionState::Failed;
+ case NrIceCtx::ICE_CTX_DISCONNECTED:
+ return dom::RTCIceConnectionState::Disconnected;
+ case NrIceCtx::ICE_CTX_CLOSED:
+ return dom::RTCIceConnectionState::Closed;
+ }
+ MOZ_CRASH();
+}
+
+void MediaTransportHandlerSTS::OnConnectionStateChange(
+ NrIceCtx* aIceCtx, NrIceCtx::ConnectionState aState) {
+ OnConnectionStateChange(toDomIceConnectionState(aState));
+}
+
+// The stuff below here will eventually go into the MediaTransportChild class
+void MediaTransportHandlerSTS::OnCandidateFound(
+ NrIceMediaStream* aStream, const std::string& aCandidate,
+ const std::string& aUfrag, const std::string& aMDNSAddr,
+ const std::string& aActualAddr) {
+ CandidateInfo info;
+ info.mCandidate = aCandidate;
+ MOZ_ASSERT(!aUfrag.empty());
+ info.mUfrag = aUfrag;
+ NrIceCandidate defaultRtpCandidate;
+ NrIceCandidate defaultRtcpCandidate;
+ nsresult rv = aStream->GetDefaultCandidate(1, &defaultRtpCandidate);
+ if (NS_SUCCEEDED(rv)) {
+ if (!defaultRtpCandidate.mdns_addr.empty()) {
+ info.mDefaultHostRtp = "0.0.0.0";
+ info.mDefaultPortRtp = 9;
+ } else {
+ info.mDefaultHostRtp = defaultRtpCandidate.cand_addr.host;
+ info.mDefaultPortRtp = defaultRtpCandidate.cand_addr.port;
+ }
+ } else {
+ CSFLogError(LOGTAG,
+ "%s: GetDefaultCandidates failed for transport id %s, "
+ "res=%u",
+ __FUNCTION__, aStream->GetId().c_str(),
+ static_cast<unsigned>(rv));
+ }
+
+ // Optional; component won't exist if doing rtcp-mux
+ if (NS_SUCCEEDED(aStream->GetDefaultCandidate(2, &defaultRtcpCandidate))) {
+ if (!defaultRtcpCandidate.mdns_addr.empty()) {
+ info.mDefaultHostRtcp = defaultRtcpCandidate.mdns_addr;
+ } else {
+ info.mDefaultHostRtcp = defaultRtcpCandidate.cand_addr.host;
+ }
+ info.mDefaultPortRtcp = defaultRtcpCandidate.cand_addr.port;
+ }
+
+ info.mMDNSAddress = aMDNSAddr;
+ info.mActualAddress = aActualAddr;
+
+ OnCandidate(aStream->GetId(), info);
+}
+
+void MediaTransportHandlerSTS::OnStateChange(TransportLayer* aLayer,
+ TransportLayer::State aState) {
+ if (aState == TransportLayer::TS_OPEN) {
+ MOZ_ASSERT(aLayer->id() == TransportLayerDtls::ID());
+ TransportLayerDtls* dtlsLayer = static_cast<TransportLayerDtls*>(aLayer);
+ OnAlpnNegotiated(dtlsLayer->GetNegotiatedAlpn());
+ }
+
+ // DTLS state indicates the readiness of the transport as a whole, because
+ // SRTP uses the keys from the DTLS handshake.
+ MediaTransportHandler::OnStateChange(aLayer->flow_id(), aState);
+}
+
+void MediaTransportHandlerSTS::OnRtcpStateChange(TransportLayer* aLayer,
+ TransportLayer::State aState) {
+ MediaTransportHandler::OnRtcpStateChange(aLayer->flow_id(), aState);
+}
+
+constexpr static const char* PacketTypeToString(MediaPacket::Type type) {
+ switch (type) {
+ case MediaPacket::Type::UNCLASSIFIED:
+ return "UNCLASSIFIED";
+ case MediaPacket::Type::SRTP:
+ return "SRTP";
+ case MediaPacket::Type::SRTCP:
+ return "SRTCP";
+ case MediaPacket::Type::DTLS:
+ return "DTLS";
+ case MediaPacket::Type::RTP:
+ return "RTP";
+ case MediaPacket::Type::RTCP:
+ return "RTCP";
+ case MediaPacket::Type::SCTP:
+ return "SCTP";
+ default:
+ MOZ_ASSERT(false, "unreached");
+ return "";
+ }
+}
+
+void MediaTransportHandlerSTS::PacketReceived(TransportLayer* aLayer,
+ MediaPacket& aPacket) {
+ MEDIA_TRANSPORT_HANDLER_PACKET_RECEIVED(aPacket);
+ OnPacketReceived(aLayer->flow_id(), aPacket);
+}
+
+void MediaTransportHandlerSTS::EncryptedPacketSending(TransportLayer* aLayer,
+ MediaPacket& aPacket) {
+ OnEncryptedSending(aLayer->flow_id(), aPacket);
+}
+
+} // namespace mozilla
+
+#undef MEDIA_TRANSPORT_HANDLER_PACKET_RECEIVED
diff --git a/dom/media/webrtc/jsapi/MediaTransportHandler.h b/dom/media/webrtc/jsapi/MediaTransportHandler.h
new file mode 100644
index 0000000000..a776cb6fd7
--- /dev/null
+++ b/dom/media/webrtc/jsapi/MediaTransportHandler.h
@@ -0,0 +1,167 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _MTRANSPORTHANDLER_H__
+#define _MTRANSPORTHANDLER_H__
+
+#include "mozilla/RefPtr.h"
+#include "nsISupportsImpl.h"
+#include "transport/sigslot.h"
+#include "transport/transportlayer.h" // Need the State enum
+#include "transport/dtlsidentity.h" // For DtlsDigest
+#include "mozilla/dom/RTCPeerConnectionBinding.h"
+#include "mozilla/dom/RTCConfigurationBinding.h"
+#include "transport/nricectx.h" // Need some enums
+#include "common/CandidateInfo.h"
+#include "transport/nr_socket_proxy_config.h"
+#include "RTCStatsReport.h"
+
+#include "nsString.h"
+
+#include <string>
+#include <set>
+#include <vector>
+
+namespace mozilla {
+class DtlsIdentity;
+class NrIceCtx;
+class NrIceMediaStream;
+class NrIceResolver;
+class TransportFlow;
+class RTCStatsQuery;
+
+namespace dom {
+struct RTCStatsReportInternal;
+}
+
+class MediaTransportHandler {
+ public:
+ // Creates either a MediaTransportHandlerSTS or a MediaTransportHandlerIPC,
+ // as appropriate. If you want signals to fire on a specific thread, pass
+ // the event target here, otherwise they will fire on whatever is convenient.
+ // Note: This also determines what thread the state cache is updated on!
+ // Don't call GetState on any other thread!
+ static already_AddRefed<MediaTransportHandler> Create(
+ nsISerialEventTarget* aCallbackThread);
+
+ explicit MediaTransportHandler(nsISerialEventTarget* aCallbackThread)
+ : mCallbackThread(aCallbackThread) {}
+
+ // Exposed so we can synchronously validate ICE servers from PeerConnection
+ static nsresult ConvertIceServers(
+ const nsTArray<dom::RTCIceServer>& aIceServers,
+ std::vector<NrIceStunServer>* aStunServers,
+ std::vector<NrIceTurnServer>* aTurnServers);
+
+ typedef MozPromise<dom::Sequence<nsString>, nsresult, true> IceLogPromise;
+
+ virtual void Initialize() {}
+
+ // There's a wrinkle here; the ICE logging is not separated out by
+ // MediaTransportHandler. These are a little more like static methods, but
+ // to avoid needing yet another IPC interface, we bolt them on here.
+ virtual RefPtr<IceLogPromise> GetIceLog(const nsCString& aPattern) = 0;
+ virtual void ClearIceLog() = 0;
+ virtual void EnterPrivateMode() = 0;
+ virtual void ExitPrivateMode() = 0;
+
+ virtual void CreateIceCtx(const std::string& aName) = 0;
+
+ virtual nsresult SetIceConfig(const nsTArray<dom::RTCIceServer>& aIceServers,
+ dom::RTCIceTransportPolicy aIcePolicy) = 0;
+
+ // We will probably be able to move the proxy lookup stuff into
+ // this class once we move mtransport to its own process.
+ virtual void SetProxyConfig(NrSocketProxyConfig&& aProxyConfig) = 0;
+
+ virtual void EnsureProvisionalTransport(const std::string& aTransportId,
+ const std::string& aLocalUfrag,
+ const std::string& aLocalPwd,
+ int aComponentCount) = 0;
+
+ virtual void SetTargetForDefaultLocalAddressLookup(
+ const std::string& aTargetIp, uint16_t aTargetPort) = 0;
+
+ // We set default-route-only as late as possible because it depends on what
+ // capture permissions have been granted on the window, which could easily
+ // change between Init (ie; when the PC is created) and StartIceGathering
+ // (ie; when we set the local description).
+ virtual void StartIceGathering(bool aDefaultRouteOnly,
+ bool aObfuscateHostAddresses,
+ // TODO: It probably makes sense to look
+ // this up internally
+ const nsTArray<NrIceStunAddr>& aStunAddrs) = 0;
+
+ virtual void ActivateTransport(
+ const std::string& aTransportId, const std::string& aLocalUfrag,
+ const std::string& aLocalPwd, size_t aComponentCount,
+ const std::string& aUfrag, const std::string& aPassword,
+ const nsTArray<uint8_t>& aKeyDer, const nsTArray<uint8_t>& aCertDer,
+ SSLKEAType aAuthType, bool aDtlsClient, const DtlsDigestList& aDigests,
+ bool aPrivacyRequested) = 0;
+
+ virtual void RemoveTransportsExcept(
+ const std::set<std::string>& aTransportIds) = 0;
+
+ virtual void StartIceChecks(bool aIsControlling,
+ const std::vector<std::string>& aIceOptions) = 0;
+
+ virtual void SendPacket(const std::string& aTransportId,
+ MediaPacket&& aPacket) = 0;
+
+ virtual void AddIceCandidate(const std::string& aTransportId,
+ const std::string& aCandidate,
+ const std::string& aUFrag,
+ const std::string& aObfuscatedAddress) = 0;
+
+ virtual void UpdateNetworkState(bool aOnline) = 0;
+
+ virtual RefPtr<dom::RTCStatsPromise> GetIceStats(
+ const std::string& aTransportId, DOMHighResTimeStamp aNow) = 0;
+
+ sigslot::signal2<const std::string&, const CandidateInfo&> SignalCandidate;
+ sigslot::signal2<const std::string&, bool> SignalAlpnNegotiated;
+ sigslot::signal1<dom::RTCIceGatheringState> SignalGatheringStateChange;
+ sigslot::signal1<dom::RTCIceConnectionState> SignalConnectionStateChange;
+
+ sigslot::signal2<const std::string&, const MediaPacket&> SignalPacketReceived;
+ sigslot::signal2<const std::string&, const MediaPacket&>
+ SignalEncryptedSending;
+ sigslot::signal2<const std::string&, TransportLayer::State> SignalStateChange;
+ sigslot::signal2<const std::string&, TransportLayer::State>
+ SignalRtcpStateChange;
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING_WITH_DESTROY(MediaTransportHandler,
+ Destroy())
+
+ TransportLayer::State GetState(const std::string& aTransportId,
+ bool aRtcp) const;
+
+ protected:
+ void OnCandidate(const std::string& aTransportId,
+ const CandidateInfo& aCandidateInfo);
+ void OnAlpnNegotiated(const std::string& aAlpn);
+ void OnGatheringStateChange(dom::RTCIceGatheringState aState);
+ void OnConnectionStateChange(dom::RTCIceConnectionState aState);
+ void OnPacketReceived(const std::string& aTransportId,
+ const MediaPacket& aPacket);
+ void OnEncryptedSending(const std::string& aTransportId,
+ const MediaPacket& aPacket);
+ void OnStateChange(const std::string& aTransportId,
+ TransportLayer::State aState);
+ void OnRtcpStateChange(const std::string& aTransportId,
+ TransportLayer::State aState);
+ virtual void Destroy() = 0;
+ virtual ~MediaTransportHandler() = default;
+ std::map<std::string, TransportLayer::State> mStateCache;
+ std::map<std::string, TransportLayer::State> mRtcpStateCache;
+ RefPtr<nsISerialEventTarget> mCallbackThread;
+};
+
+void TokenizeCandidate(const std::string& aCandidate,
+ std::vector<std::string>& aTokens);
+
+} // namespace mozilla
+
+#endif //_MTRANSPORTHANDLER_H__
diff --git a/dom/media/webrtc/jsapi/MediaTransportHandlerIPC.cpp b/dom/media/webrtc/jsapi/MediaTransportHandlerIPC.cpp
new file mode 100644
index 0000000000..6369d229ee
--- /dev/null
+++ b/dom/media/webrtc/jsapi/MediaTransportHandlerIPC.cpp
@@ -0,0 +1,414 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaTransportHandlerIPC.h"
+#include "mozilla/dom/MediaTransportChild.h"
+#include "nsThreadUtils.h"
+#include "mozilla/net/SocketProcessBridgeChild.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/ipc/BackgroundChild.h"
+#include "mozilla/ipc/PBackgroundChild.h"
+#include "common/browser_logging/CSFLog.h"
+
+namespace mozilla {
+
+static const char* mthipcLogTag = "MediaTransportHandler";
+#ifdef LOGTAG
+# undef LOGTAG
+#endif
+#define LOGTAG mthipcLogTag
+
+MediaTransportHandlerIPC::MediaTransportHandlerIPC(
+ nsISerialEventTarget* aCallbackThread)
+ : MediaTransportHandler(aCallbackThread) {}
+
+void MediaTransportHandlerIPC::Initialize() {
+ mInitPromise = net::SocketProcessBridgeChild::GetSocketProcessBridge()->Then(
+ mCallbackThread, __func__,
+ [this, self = RefPtr<MediaTransportHandlerIPC>(this)](
+ const RefPtr<net::SocketProcessBridgeChild>& aBridge) {
+ ipc::PBackgroundChild* actor =
+ ipc::BackgroundChild::GetOrCreateSocketActorForCurrentThread();
+ // An actor that can't send is possible if the socket process has
+ // crashed but hasn't been reconnected properly. See
+ // SocketProcessBridgeChild::ActorDestroy for more info.
+ if (!actor || !actor->CanSend()) {
+ NS_WARNING(
+ "MediaTransportHandlerIPC async init failed! Webrtc networking "
+ "will not work!");
+ return InitPromise::CreateAndReject(
+ nsCString("GetOrCreateSocketActorForCurrentThread failed!"),
+ __func__);
+ }
+ MediaTransportChild* child = new MediaTransportChild(this);
+ // PBackgroungChild owns mChild! When it is done with it,
+ // mChild will let us know it it going away.
+ mChild = actor->SendPMediaTransportConstructor(child);
+ CSFLogDebug(LOGTAG, "%s Init done", __func__);
+ return InitPromise::CreateAndResolve(true, __func__);
+ },
+ [=](const nsCString& aError) {
+ CSFLogError(LOGTAG,
+ "MediaTransportHandlerIPC async init failed! Webrtc "
+ "networking will not work! Error was %s",
+ aError.get());
+ NS_WARNING(
+ "MediaTransportHandlerIPC async init failed! Webrtc networking "
+ "will not work!");
+ return InitPromise::CreateAndReject(aError, __func__);
+ });
+}
+
+RefPtr<MediaTransportHandler::IceLogPromise>
+MediaTransportHandlerIPC::GetIceLog(const nsCString& aPattern) {
+ return mInitPromise->Then(
+ mCallbackThread, __func__,
+ [=, self = RefPtr<MediaTransportHandlerIPC>(this)](bool /* dummy */) {
+ if (!mChild) {
+ return IceLogPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+ // Compiler has trouble deducing the return type here for some reason,
+ // so we use a temp variable as a hint.
+ // SendGetIceLog _almost_ returns an IceLogPromise; the reject value
+ // differs (ipc::ResponseRejectReason vs nsresult) so we need to
+ // convert.
+ RefPtr<IceLogPromise> promise = mChild->SendGetIceLog(aPattern)->Then(
+ mCallbackThread, __func__,
+ [](WebrtcGlobalLog&& aLogLines) {
+ return IceLogPromise::CreateAndResolve(std::move(aLogLines),
+ __func__);
+ },
+ [](ipc::ResponseRejectReason aReason) {
+ return IceLogPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ });
+ return promise;
+ },
+ [](const nsCString& aError) {
+ return IceLogPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ });
+}
+
+void MediaTransportHandlerIPC::ClearIceLog() {
+ mInitPromise->Then(
+ mCallbackThread, __func__,
+ [=, self = RefPtr<MediaTransportHandlerIPC>(this)](bool /*dummy*/) {
+ if (mChild) {
+ mChild->SendClearIceLog();
+ }
+ },
+ [](const nsCString& aError) {});
+}
+
+void MediaTransportHandlerIPC::EnterPrivateMode() {
+ mInitPromise->Then(
+ mCallbackThread, __func__,
+ [=, self = RefPtr<MediaTransportHandlerIPC>(this)](bool /*dummy*/) {
+ if (mChild) {
+ mChild->SendEnterPrivateMode();
+ }
+ },
+ [](const nsCString& aError) {});
+}
+
+void MediaTransportHandlerIPC::ExitPrivateMode() {
+ mInitPromise->Then(
+ mCallbackThread, __func__,
+ [=, self = RefPtr<MediaTransportHandlerIPC>(this)](bool /*dummy*/) {
+ if (mChild) {
+ mChild->SendExitPrivateMode();
+ }
+ },
+ [](const nsCString& aError) {});
+}
+
+void MediaTransportHandlerIPC::CreateIceCtx(const std::string& aName) {
+ CSFLogDebug(LOGTAG, "MediaTransportHandlerIPC::CreateIceCtx start");
+
+ mInitPromise->Then(
+ mCallbackThread, __func__,
+ [=, self = RefPtr<MediaTransportHandlerIPC>(this)](bool /*dummy*/) {
+ if (mChild) {
+ CSFLogDebug(LOGTAG, "%s starting", __func__);
+ if (NS_WARN_IF(!mChild->SendCreateIceCtx(aName))) {
+ CSFLogError(LOGTAG, "%s failed!", __func__);
+ }
+ }
+ },
+ [](const nsCString& aError) {});
+}
+
+nsresult MediaTransportHandlerIPC::SetIceConfig(
+ const nsTArray<dom::RTCIceServer>& aIceServers,
+ dom::RTCIceTransportPolicy aIcePolicy) {
+ // Run some validation on this side of the IPC boundary so we can return
+ // errors synchronously. We don't actually use the results. It might make
+ // sense to move this check to PeerConnection and have this API take the
+ // converted form, but we would need to write IPC serialization code for
+ // the NrIce*Server types.
+ std::vector<NrIceStunServer> stunServers;
+ std::vector<NrIceTurnServer> turnServers;
+ nsresult rv = ConvertIceServers(aIceServers, &stunServers, &turnServers);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ mInitPromise->Then(
+ mCallbackThread, __func__,
+ [=, iceServers = aIceServers.Clone(),
+ self = RefPtr<MediaTransportHandlerIPC>(this)](bool /*dummy*/) {
+ if (mChild) {
+ if (NS_WARN_IF(!mChild->SendSetIceConfig(std::move(iceServers),
+ aIcePolicy))) {
+ CSFLogError(LOGTAG, "%s failed!", __func__);
+ }
+ }
+ },
+ [](const nsCString& aError) {});
+
+ return NS_OK;
+}
+
+void MediaTransportHandlerIPC::Destroy() {
+ if (mChild) {
+ MediaTransportChild::Send__delete__(mChild);
+ mChild = nullptr;
+ }
+ delete this;
+}
+
+// We will probably be able to move the proxy lookup stuff into
+// this class once we move mtransport to its own process.
+void MediaTransportHandlerIPC::SetProxyConfig(
+ NrSocketProxyConfig&& aProxyConfig) {
+ mInitPromise->Then(
+ mCallbackThread, __func__,
+ [aProxyConfig = std::move(aProxyConfig), this,
+ self = RefPtr<MediaTransportHandlerIPC>(this)](bool /*dummy*/) mutable {
+ if (mChild) {
+ mChild->SendSetProxyConfig(aProxyConfig.GetConfig());
+ }
+ },
+ [](const nsCString& aError) {});
+}
+
+void MediaTransportHandlerIPC::EnsureProvisionalTransport(
+ const std::string& aTransportId, const std::string& aLocalUfrag,
+ const std::string& aLocalPwd, int aComponentCount) {
+ mInitPromise->Then(
+ mCallbackThread, __func__,
+ [=, self = RefPtr<MediaTransportHandlerIPC>(this)](bool /*dummy*/) {
+ if (mChild) {
+ mChild->SendEnsureProvisionalTransport(aTransportId, aLocalUfrag,
+ aLocalPwd, aComponentCount);
+ }
+ },
+ [](const nsCString& aError) {});
+}
+
+void MediaTransportHandlerIPC::SetTargetForDefaultLocalAddressLookup(
+ const std::string& aTargetIp, uint16_t aTargetPort) {
+ mInitPromise->Then(
+ mCallbackThread, __func__,
+ [=, self = RefPtr<MediaTransportHandlerIPC>(this)](bool /*dummy*/) {
+ if (mChild) {
+ mChild->SendSetTargetForDefaultLocalAddressLookup(aTargetIp,
+ aTargetPort);
+ }
+ },
+ [](const nsCString& aError) {});
+}
+
+// We set default-route-only as late as possible because it depends on what
+// capture permissions have been granted on the window, which could easily
+// change between Init (ie; when the PC is created) and StartIceGathering
+// (ie; when we set the local description).
+void MediaTransportHandlerIPC::StartIceGathering(
+ bool aDefaultRouteOnly, bool aObfuscateHostAddresses,
+ // TODO(bug 1522205): It probably makes sense to look this up internally
+ const nsTArray<NrIceStunAddr>& aStunAddrs) {
+ mInitPromise->Then(
+ mCallbackThread, __func__,
+ [=, stunAddrs = aStunAddrs.Clone(),
+ self = RefPtr<MediaTransportHandlerIPC>(this)](bool /*dummy*/) {
+ if (mChild) {
+ mChild->SendStartIceGathering(aDefaultRouteOnly,
+ aObfuscateHostAddresses, stunAddrs);
+ }
+ },
+ [](const nsCString& aError) {});
+}
+
+void MediaTransportHandlerIPC::ActivateTransport(
+ const std::string& aTransportId, const std::string& aLocalUfrag,
+ const std::string& aLocalPwd, size_t aComponentCount,
+ const std::string& aUfrag, const std::string& aPassword,
+ const nsTArray<uint8_t>& aKeyDer, const nsTArray<uint8_t>& aCertDer,
+ SSLKEAType aAuthType, bool aDtlsClient, const DtlsDigestList& aDigests,
+ bool aPrivacyRequested) {
+ mInitPromise->Then(
+ mCallbackThread, __func__,
+ [=, keyDer = aKeyDer.Clone(), certDer = aCertDer.Clone(),
+ self = RefPtr<MediaTransportHandlerIPC>(this)](bool /*dummy*/) {
+ if (mChild) {
+ mChild->SendActivateTransport(aTransportId, aLocalUfrag, aLocalPwd,
+ aComponentCount, aUfrag, aPassword,
+ keyDer, certDer, aAuthType, aDtlsClient,
+ aDigests, aPrivacyRequested);
+ }
+ },
+ [](const nsCString& aError) {});
+}
+
+void MediaTransportHandlerIPC::RemoveTransportsExcept(
+ const std::set<std::string>& aTransportIds) {
+ std::vector<std::string> transportIds(aTransportIds.begin(),
+ aTransportIds.end());
+ mInitPromise->Then(
+ mCallbackThread, __func__,
+ [=, self = RefPtr<MediaTransportHandlerIPC>(this)](bool /*dummy*/) {
+ if (mChild) {
+ mChild->SendRemoveTransportsExcept(transportIds);
+ }
+ },
+ [](const nsCString& aError) {});
+}
+
+void MediaTransportHandlerIPC::StartIceChecks(
+ bool aIsControlling, const std::vector<std::string>& aIceOptions) {
+ mInitPromise->Then(
+ mCallbackThread, __func__,
+ [=, self = RefPtr<MediaTransportHandlerIPC>(this)](bool /*dummy*/) {
+ if (mChild) {
+ mChild->SendStartIceChecks(aIsControlling, aIceOptions);
+ }
+ },
+ [](const nsCString& aError) {});
+}
+
+void MediaTransportHandlerIPC::SendPacket(const std::string& aTransportId,
+ MediaPacket&& aPacket) {
+ mInitPromise->Then(
+ mCallbackThread, __func__,
+ [this, self = RefPtr<MediaTransportHandlerIPC>(this), aTransportId,
+ aPacket = std::move(aPacket)](bool /*dummy*/) mutable {
+ if (mChild) {
+ mChild->SendSendPacket(aTransportId, aPacket);
+ }
+ },
+ [](const nsCString& aError) {});
+}
+
+void MediaTransportHandlerIPC::AddIceCandidate(
+ const std::string& aTransportId, const std::string& aCandidate,
+ const std::string& aUfrag, const std::string& aObfuscatedAddress) {
+ mInitPromise->Then(
+ mCallbackThread, __func__,
+ [=, self = RefPtr<MediaTransportHandlerIPC>(this)](bool /*dummy*/) {
+ if (mChild) {
+ mChild->SendAddIceCandidate(aTransportId, aCandidate, aUfrag,
+ aObfuscatedAddress);
+ }
+ },
+ [](const nsCString& aError) {});
+}
+
+void MediaTransportHandlerIPC::UpdateNetworkState(bool aOnline) {
+ mInitPromise->Then(
+ mCallbackThread, __func__,
+ [=, self = RefPtr<MediaTransportHandlerIPC>(this)](bool /*dummy*/) {
+ if (mChild) {
+ mChild->SendUpdateNetworkState(aOnline);
+ }
+ },
+ [](const nsCString& aError) {});
+}
+
+RefPtr<dom::RTCStatsPromise> MediaTransportHandlerIPC::GetIceStats(
+ const std::string& aTransportId, DOMHighResTimeStamp aNow) {
+ using IPCPromise = dom::PMediaTransportChild::GetIceStatsPromise;
+ return mInitPromise
+ ->Then(mCallbackThread, __func__,
+ [aTransportId, aNow, this, self = RefPtr(this)](
+ const InitPromise::ResolveOrRejectValue& aValue) {
+ if (aValue.IsReject()) {
+ return IPCPromise::CreateAndResolve(
+ MakeUnique<dom::RTCStatsCollection>(),
+ "MediaTransportHandlerIPC::GetIceStats_1");
+ }
+ if (!mChild) {
+ return IPCPromise::CreateAndResolve(
+ MakeUnique<dom::RTCStatsCollection>(),
+ "MediaTransportHandlerIPC::GetIceStats_1");
+ }
+ return mChild->SendGetIceStats(aTransportId, aNow);
+ })
+ ->Then(mCallbackThread, __func__,
+ [](IPCPromise::ResolveOrRejectValue&& aValue) {
+ if (aValue.IsReject()) {
+ return dom::RTCStatsPromise::CreateAndResolve(
+ MakeUnique<dom::RTCStatsCollection>(),
+ "MediaTransportHandlerIPC::GetIceStats_2");
+ }
+ return dom::RTCStatsPromise::CreateAndResolve(
+ std::move(aValue.ResolveValue()),
+ "MediaTransportHandlerIPC::GetIceStats_2");
+ });
+}
+
+MediaTransportChild::MediaTransportChild(MediaTransportHandlerIPC* aUser)
+ : mUser(aUser) {}
+
+MediaTransportChild::~MediaTransportChild() { mUser->mChild = nullptr; }
+
+mozilla::ipc::IPCResult MediaTransportChild::RecvOnCandidate(
+ const string& transportId, const CandidateInfo& candidateInfo) {
+ mUser->OnCandidate(transportId, candidateInfo);
+ return ipc::IPCResult::Ok();
+}
+
+mozilla::ipc::IPCResult MediaTransportChild::RecvOnAlpnNegotiated(
+ const string& alpn) {
+ mUser->OnAlpnNegotiated(alpn);
+ return ipc::IPCResult::Ok();
+}
+
+mozilla::ipc::IPCResult MediaTransportChild::RecvOnGatheringStateChange(
+ const int& state) {
+ mUser->OnGatheringStateChange(static_cast<dom::RTCIceGatheringState>(state));
+ return ipc::IPCResult::Ok();
+}
+
+mozilla::ipc::IPCResult MediaTransportChild::RecvOnConnectionStateChange(
+ const int& state) {
+ mUser->OnConnectionStateChange(
+ static_cast<dom::RTCIceConnectionState>(state));
+ return ipc::IPCResult::Ok();
+}
+
+mozilla::ipc::IPCResult MediaTransportChild::RecvOnPacketReceived(
+ const string& transportId, const MediaPacket& packet) {
+ mUser->OnPacketReceived(transportId, packet);
+ return ipc::IPCResult::Ok();
+}
+
+mozilla::ipc::IPCResult MediaTransportChild::RecvOnEncryptedSending(
+ const string& transportId, const MediaPacket& packet) {
+ mUser->OnEncryptedSending(transportId, packet);
+ return ipc::IPCResult::Ok();
+}
+
+mozilla::ipc::IPCResult MediaTransportChild::RecvOnStateChange(
+ const string& transportId, const int& state) {
+ mUser->OnStateChange(transportId, static_cast<TransportLayer::State>(state));
+ return ipc::IPCResult::Ok();
+}
+
+mozilla::ipc::IPCResult MediaTransportChild::RecvOnRtcpStateChange(
+ const string& transportId, const int& state) {
+ mUser->OnRtcpStateChange(transportId,
+ static_cast<TransportLayer::State>(state));
+ return ipc::IPCResult::Ok();
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/jsapi/MediaTransportHandlerIPC.h b/dom/media/webrtc/jsapi/MediaTransportHandlerIPC.h
new file mode 100644
index 0000000000..6b285dee2a
--- /dev/null
+++ b/dom/media/webrtc/jsapi/MediaTransportHandlerIPC.h
@@ -0,0 +1,96 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _MTRANSPORTHANDLER_IPC_H__
+#define _MTRANSPORTHANDLER_IPC_H__
+
+#include "jsapi/MediaTransportHandler.h"
+#include "mozilla/dom/PMediaTransportChild.h"
+
+namespace mozilla {
+
+class MediaTransportChild;
+
+// Implementation of MediaTransportHandler that uses IPC (PMediaTransport) to
+// talk to mtransport on another process.
+class MediaTransportHandlerIPC final : public MediaTransportHandler {
+ public:
+ explicit MediaTransportHandlerIPC(nsISerialEventTarget* aCallbackThread);
+ void Initialize() override;
+ RefPtr<IceLogPromise> GetIceLog(const nsCString& aPattern) override;
+ void ClearIceLog() override;
+ void EnterPrivateMode() override;
+ void ExitPrivateMode() override;
+
+ void CreateIceCtx(const std::string& aName) override;
+
+ nsresult SetIceConfig(const nsTArray<dom::RTCIceServer>& aIceServers,
+ dom::RTCIceTransportPolicy aIcePolicy) override;
+
+ // We will probably be able to move the proxy lookup stuff into
+ // this class once we move mtransport to its own process.
+ void SetProxyConfig(NrSocketProxyConfig&& aProxyConfig) override;
+
+ void EnsureProvisionalTransport(const std::string& aTransportId,
+ const std::string& aLocalUfrag,
+ const std::string& aLocalPwd,
+ int aComponentCount) override;
+
+ void SetTargetForDefaultLocalAddressLookup(const std::string& aTargetIp,
+ uint16_t aTargetPort) override;
+
+ // We set default-route-only as late as possible because it depends on what
+ // capture permissions have been granted on the window, which could easily
+ // change between Init (ie; when the PC is created) and StartIceGathering
+ // (ie; when we set the local description).
+ void StartIceGathering(bool aDefaultRouteOnly, bool aObfuscateHostAddresses,
+ // TODO: It probably makes sense to look
+ // this up internally
+ const nsTArray<NrIceStunAddr>& aStunAddrs) override;
+
+ void ActivateTransport(
+ const std::string& aTransportId, const std::string& aLocalUfrag,
+ const std::string& aLocalPwd, size_t aComponentCount,
+ const std::string& aUfrag, const std::string& aPassword,
+ const nsTArray<uint8_t>& aKeyDer, const nsTArray<uint8_t>& aCertDer,
+ SSLKEAType aAuthType, bool aDtlsClient, const DtlsDigestList& aDigests,
+ bool aPrivacyRequested) override;
+
+ void RemoveTransportsExcept(
+ const std::set<std::string>& aTransportIds) override;
+
+ void StartIceChecks(bool aIsControlling,
+ const std::vector<std::string>& aIceOptions) override;
+
+ void SendPacket(const std::string& aTransportId,
+ MediaPacket&& aPacket) override;
+
+ void AddIceCandidate(const std::string& aTransportId,
+ const std::string& aCandidate, const std::string& aUfrag,
+ const std::string& aObfuscatedAddress) override;
+
+ void UpdateNetworkState(bool aOnline) override;
+
+ RefPtr<dom::RTCStatsPromise> GetIceStats(const std::string& aTransportId,
+ DOMHighResTimeStamp aNow) override;
+
+ private:
+ friend class MediaTransportChild;
+ void Destroy() override;
+
+ // We do not own this; it will tell us when it is going away.
+ dom::PMediaTransportChild* mChild = nullptr;
+
+ // |mChild| can only be initted asynchronously, |mInitPromise| resolves
+ // when that happens. The |Then| calls make it convenient to dispatch API
+ // calls to main, which is a bonus.
+ // Init promise is not exclusive; this lets us call |Then| on it for every
+ // API call we get, instead of creating another promise each time.
+ typedef MozPromise<bool, nsCString, false> InitPromise;
+ RefPtr<InitPromise> mInitPromise;
+};
+
+} // namespace mozilla
+
+#endif //_MTRANSPORTHANDLER_IPC_H__
diff --git a/dom/media/webrtc/jsapi/MediaTransportParent.cpp b/dom/media/webrtc/jsapi/MediaTransportParent.cpp
new file mode 100644
index 0000000000..d0f90df8c5
--- /dev/null
+++ b/dom/media/webrtc/jsapi/MediaTransportParent.cpp
@@ -0,0 +1,240 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/MediaTransportParent.h"
+#include "jsapi/MediaTransportHandler.h"
+
+#include "transport/sigslot.h"
+#include "common/browser_logging/CSFLog.h"
+
+namespace mozilla {
+
+// Deals with the MediaTransportHandler interface, so MediaTransportParent
+// doesn't have to..
+class MediaTransportParent::Impl : public sigslot::has_slots<> {
+ public:
+ explicit Impl(MediaTransportParent* aParent)
+ : mHandler(MediaTransportHandler::Create(GetCurrentSerialEventTarget())),
+ mParent(aParent) {
+ mHandler->SignalCandidate.connect(this,
+ &MediaTransportParent::Impl::OnCandidate);
+ mHandler->SignalAlpnNegotiated.connect(
+ this, &MediaTransportParent::Impl::OnAlpnNegotiated);
+ mHandler->SignalGatheringStateChange.connect(
+ this, &MediaTransportParent::Impl::OnGatheringStateChange);
+ mHandler->SignalConnectionStateChange.connect(
+ this, &MediaTransportParent::Impl::OnConnectionStateChange);
+ mHandler->SignalPacketReceived.connect(
+ this, &MediaTransportParent::Impl::OnPacketReceived);
+ mHandler->SignalEncryptedSending.connect(
+ this, &MediaTransportParent::Impl::OnEncryptedSending);
+ mHandler->SignalStateChange.connect(
+ this, &MediaTransportParent::Impl::OnStateChange);
+ mHandler->SignalRtcpStateChange.connect(
+ this, &MediaTransportParent::Impl::OnRtcpStateChange);
+ }
+
+ virtual ~Impl() {
+ disconnect_all();
+ mHandler = nullptr;
+ }
+
+ void OnCandidate(const std::string& aTransportId,
+ const CandidateInfo& aCandidateInfo) {
+ NS_ENSURE_TRUE_VOID(mParent->SendOnCandidate(aTransportId, aCandidateInfo));
+ }
+
+ void OnAlpnNegotiated(const std::string& aAlpn, bool aPrivacyRequested) {
+ NS_ENSURE_TRUE_VOID(mParent->SendOnAlpnNegotiated(aAlpn));
+ }
+
+ void OnGatheringStateChange(dom::RTCIceGatheringState aState) {
+ NS_ENSURE_TRUE_VOID(
+ mParent->SendOnGatheringStateChange(static_cast<int>(aState)));
+ }
+
+ void OnConnectionStateChange(dom::RTCIceConnectionState aState) {
+ NS_ENSURE_TRUE_VOID(
+ mParent->SendOnConnectionStateChange(static_cast<int>(aState)));
+ }
+
+ void OnPacketReceived(const std::string& aTransportId,
+ const MediaPacket& aPacket) {
+ NS_ENSURE_TRUE_VOID(mParent->SendOnPacketReceived(aTransportId, aPacket));
+ }
+
+ void OnEncryptedSending(const std::string& aTransportId,
+ const MediaPacket& aPacket) {
+ NS_ENSURE_TRUE_VOID(mParent->SendOnEncryptedSending(aTransportId, aPacket));
+ }
+
+ void OnStateChange(const std::string& aTransportId,
+ TransportLayer::State aState) {
+ NS_ENSURE_TRUE_VOID(mParent->SendOnStateChange(aTransportId, aState));
+ }
+
+ void OnRtcpStateChange(const std::string& aTransportId,
+ TransportLayer::State aState) {
+ NS_ENSURE_TRUE_VOID(mParent->SendOnRtcpStateChange(aTransportId, aState));
+ }
+
+ RefPtr<MediaTransportHandler> mHandler;
+
+ private:
+ MediaTransportParent* mParent;
+};
+
+MediaTransportParent::MediaTransportParent() : mImpl(new Impl(this)) {}
+
+MediaTransportParent::~MediaTransportParent() {}
+
+mozilla::ipc::IPCResult MediaTransportParent::RecvGetIceLog(
+ const nsCString& pattern, GetIceLogResolver&& aResolve) {
+ mImpl->mHandler->GetIceLog(pattern)->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ // IPDL doesn't give us a reject function, so we cannot reject async, so
+ // we are forced to resolve with an empty result. Laaaaaaame.
+ [aResolve = std::move(aResolve)](
+ MediaTransportHandler::IceLogPromise::ResolveOrRejectValue&&
+ aResult) mutable {
+ WebrtcGlobalLog logLines;
+ if (aResult.IsResolve()) {
+ logLines = std::move(aResult.ResolveValue());
+ }
+ aResolve(logLines);
+ });
+
+ return ipc::IPCResult::Ok();
+}
+
+mozilla::ipc::IPCResult MediaTransportParent::RecvClearIceLog() {
+ mImpl->mHandler->ClearIceLog();
+ return ipc::IPCResult::Ok();
+}
+
+mozilla::ipc::IPCResult MediaTransportParent::RecvEnterPrivateMode() {
+ mImpl->mHandler->EnterPrivateMode();
+ return ipc::IPCResult::Ok();
+}
+
+mozilla::ipc::IPCResult MediaTransportParent::RecvExitPrivateMode() {
+ mImpl->mHandler->ExitPrivateMode();
+ return ipc::IPCResult::Ok();
+}
+
+mozilla::ipc::IPCResult MediaTransportParent::RecvCreateIceCtx(
+ const string& name) {
+ mImpl->mHandler->CreateIceCtx(name);
+ return ipc::IPCResult::Ok();
+}
+
+mozilla::ipc::IPCResult MediaTransportParent::RecvSetIceConfig(
+ nsTArray<RTCIceServer>&& iceServers,
+ const RTCIceTransportPolicy& icePolicy) {
+ nsresult rv = mImpl->mHandler->SetIceConfig(iceServers, icePolicy);
+ if (NS_FAILED(rv)) {
+ return ipc::IPCResult::Fail(WrapNotNull(this), __func__,
+ "MediaTransportHandler::SetIceConfig failed");
+ }
+ return ipc::IPCResult::Ok();
+}
+
+mozilla::ipc::IPCResult MediaTransportParent::RecvSetProxyConfig(
+ const net::WebrtcProxyConfig& aProxyConfig) {
+ mImpl->mHandler->SetProxyConfig(NrSocketProxyConfig(aProxyConfig));
+ return ipc::IPCResult::Ok();
+}
+
+mozilla::ipc::IPCResult MediaTransportParent::RecvEnsureProvisionalTransport(
+ const string& transportId, const string& localUfrag, const string& localPwd,
+ const int& componentCount) {
+ mImpl->mHandler->EnsureProvisionalTransport(transportId, localUfrag, localPwd,
+ componentCount);
+ return ipc::IPCResult::Ok();
+}
+
+mozilla::ipc::IPCResult
+MediaTransportParent::RecvSetTargetForDefaultLocalAddressLookup(
+ const std::string& targetIp, uint16_t targetPort) {
+ mImpl->mHandler->SetTargetForDefaultLocalAddressLookup(targetIp, targetPort);
+ return ipc::IPCResult::Ok();
+}
+
+mozilla::ipc::IPCResult MediaTransportParent::RecvStartIceGathering(
+ const bool& defaultRouteOnly, const bool& obfuscateHostAddresses,
+ const net::NrIceStunAddrArray& stunAddrs) {
+ mImpl->mHandler->StartIceGathering(defaultRouteOnly, obfuscateHostAddresses,
+ stunAddrs);
+ return ipc::IPCResult::Ok();
+}
+
+mozilla::ipc::IPCResult MediaTransportParent::RecvActivateTransport(
+ const string& transportId, const string& localUfrag, const string& localPwd,
+ const int& componentCount, const string& remoteUfrag,
+ const string& remotePwd, nsTArray<uint8_t>&& keyDer,
+ nsTArray<uint8_t>&& certDer, const int& authType, const bool& dtlsClient,
+ const DtlsDigestList& digests, const bool& privacyRequested) {
+ mImpl->mHandler->ActivateTransport(
+ transportId, localUfrag, localPwd, componentCount, remoteUfrag, remotePwd,
+ keyDer, certDer, static_cast<SSLKEAType>(authType), dtlsClient, digests,
+ privacyRequested);
+ return ipc::IPCResult::Ok();
+}
+
+mozilla::ipc::IPCResult MediaTransportParent::RecvRemoveTransportsExcept(
+ const StringVector& transportIds) {
+ std::set<std::string> ids(transportIds.begin(), transportIds.end());
+ mImpl->mHandler->RemoveTransportsExcept(ids);
+ return ipc::IPCResult::Ok();
+}
+
+mozilla::ipc::IPCResult MediaTransportParent::RecvStartIceChecks(
+ const bool& isControlling, const StringVector& iceOptions) {
+ mImpl->mHandler->StartIceChecks(isControlling, iceOptions);
+ return ipc::IPCResult::Ok();
+}
+
+mozilla::ipc::IPCResult MediaTransportParent::RecvSendPacket(
+ const string& transportId, MediaPacket&& packet) {
+ mImpl->mHandler->SendPacket(transportId, std::move(packet));
+ return ipc::IPCResult::Ok();
+}
+
+mozilla::ipc::IPCResult MediaTransportParent::RecvAddIceCandidate(
+ const string& transportId, const string& candidate, const string& ufrag,
+ const string& obfuscatedAddr) {
+ mImpl->mHandler->AddIceCandidate(transportId, candidate, ufrag,
+ obfuscatedAddr);
+ return ipc::IPCResult::Ok();
+}
+
+mozilla::ipc::IPCResult MediaTransportParent::RecvUpdateNetworkState(
+ const bool& online) {
+ mImpl->mHandler->UpdateNetworkState(online);
+ return ipc::IPCResult::Ok();
+}
+
+mozilla::ipc::IPCResult MediaTransportParent::RecvGetIceStats(
+ const string& transportId, const double& now,
+ GetIceStatsResolver&& aResolve) {
+ mImpl->mHandler->GetIceStats(transportId, now)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ // IPDL doesn't give us a reject function, so we cannot reject async,
+ // so we are forced to resolve with an unmodified result. Laaaaaaame.
+ [aResolve = std::move(aResolve)](
+ dom::RTCStatsPromise::ResolveOrRejectValue&& aResult) {
+ if (aResult.IsResolve()) {
+ aResolve(aResult.ResolveValue());
+ } else {
+ aResolve(MakeUnique<dom::RTCStatsCollection>());
+ }
+ });
+
+ return ipc::IPCResult::Ok();
+}
+
+void MediaTransportParent::ActorDestroy(ActorDestroyReason aWhy) {}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/jsapi/PacketDumper.cpp b/dom/media/webrtc/jsapi/PacketDumper.cpp
new file mode 100644
index 0000000000..2b62e0806e
--- /dev/null
+++ b/dom/media/webrtc/jsapi/PacketDumper.cpp
@@ -0,0 +1,124 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "jsapi/PacketDumper.h"
+#include "jsapi/PeerConnectionImpl.h"
+#include "mozilla/media/MediaUtils.h" // NewRunnableFrom
+#include "nsThreadUtils.h" // NS_DispatchToMainThread
+
+namespace mozilla {
+
+/* static */
+RefPtr<PacketDumper> PacketDumper::GetPacketDumper(
+ const std::string& aPcHandle) {
+ MOZ_ASSERT(NS_IsMainThread());
+ PeerConnectionWrapper pcw(aPcHandle);
+ if (pcw.impl()) {
+ return pcw.impl()->GetPacketDumper();
+ }
+
+ return new PacketDumper("");
+}
+
+PacketDumper::PacketDumper(const std::string& aPcHandle)
+ : mPcHandle(aPcHandle),
+ mPacketDumpEnabled(false),
+ mPacketDumpFlagsMutex("Packet dump flags mutex") {}
+
+void PacketDumper::Dump(size_t aLevel, dom::mozPacketDumpType aType,
+ bool aSending, const void* aData, size_t aSize) {
+ // Optimization; avoids making a copy of the buffer, but we need to lock a
+ // mutex and check the flags. Could be optimized further, if we really want to
+ if (!ShouldDumpPacket(aLevel, aType, aSending)) {
+ return;
+ }
+
+ UniquePtr<uint8_t[]> ownedPacket = MakeUnique<uint8_t[]>(aSize);
+ memcpy(ownedPacket.get(), aData, aSize);
+
+ RefPtr<Runnable> dumpRunnable = media::NewRunnableFrom(std::bind(
+ [this, self = RefPtr<PacketDumper>(this), aLevel, aType, aSending,
+ aSize](UniquePtr<uint8_t[]>& aPacket) -> nsresult {
+ // Check again; packet dump might have been disabled since the dispatch
+ if (ShouldDumpPacket(aLevel, aType, aSending)) {
+ PeerConnectionWrapper pcw(mPcHandle);
+ RefPtr<PeerConnectionImpl> pc = pcw.impl();
+ if (pc) {
+ pc->DumpPacket_m(aLevel, aType, aSending, aPacket, aSize);
+ }
+ }
+ return NS_OK;
+ },
+ std::move(ownedPacket)));
+
+ NS_DispatchToMainThread(dumpRunnable);
+}
+
+nsresult PacketDumper::EnablePacketDump(unsigned long aLevel,
+ dom::mozPacketDumpType aType,
+ bool aSending) {
+ mPacketDumpEnabled = true;
+ std::vector<unsigned>* packetDumpFlags;
+ if (aSending) {
+ packetDumpFlags = &mSendPacketDumpFlags;
+ } else {
+ packetDumpFlags = &mRecvPacketDumpFlags;
+ }
+
+ unsigned flag = 1 << (unsigned)aType;
+
+ MutexAutoLock lock(mPacketDumpFlagsMutex);
+ if (aLevel >= packetDumpFlags->size()) {
+ packetDumpFlags->resize(aLevel + 1);
+ }
+
+ (*packetDumpFlags)[aLevel] |= flag;
+ return NS_OK;
+}
+
+nsresult PacketDumper::DisablePacketDump(unsigned long aLevel,
+ dom::mozPacketDumpType aType,
+ bool aSending) {
+ std::vector<unsigned>* packetDumpFlags;
+ if (aSending) {
+ packetDumpFlags = &mSendPacketDumpFlags;
+ } else {
+ packetDumpFlags = &mRecvPacketDumpFlags;
+ }
+
+ unsigned flag = 1 << (unsigned)aType;
+
+ MutexAutoLock lock(mPacketDumpFlagsMutex);
+ if (aLevel < packetDumpFlags->size()) {
+ (*packetDumpFlags)[aLevel] &= ~flag;
+ }
+
+ return NS_OK;
+}
+
+bool PacketDumper::ShouldDumpPacket(size_t aLevel, dom::mozPacketDumpType aType,
+ bool aSending) const {
+ if (!mPacketDumpEnabled) {
+ return false;
+ }
+
+ MutexAutoLock lock(mPacketDumpFlagsMutex);
+
+ const std::vector<unsigned>* packetDumpFlags;
+
+ if (aSending) {
+ packetDumpFlags = &mSendPacketDumpFlags;
+ } else {
+ packetDumpFlags = &mRecvPacketDumpFlags;
+ }
+
+ if (aLevel < packetDumpFlags->size()) {
+ unsigned flag = 1 << (unsigned)aType;
+ return flag & packetDumpFlags->at(aLevel);
+ }
+
+ return false;
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/jsapi/PacketDumper.h b/dom/media/webrtc/jsapi/PacketDumper.h
new file mode 100644
index 0000000000..e998b3871f
--- /dev/null
+++ b/dom/media/webrtc/jsapi/PacketDumper.h
@@ -0,0 +1,52 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _PACKET_DUMPER_H_
+#define _PACKET_DUMPER_H_
+
+#include "nsISupportsImpl.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/dom/RTCPeerConnectionBinding.h"
+
+#include <vector>
+
+namespace mozilla {
+class PeerConnectionImpl;
+
+class PacketDumper {
+ public:
+ static RefPtr<PacketDumper> GetPacketDumper(const std::string& aPcHandle);
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(PacketDumper)
+
+ PacketDumper(const PacketDumper&) = delete;
+ PacketDumper& operator=(const PacketDumper&) = delete;
+
+ void Dump(size_t aLevel, dom::mozPacketDumpType aType, bool aSending,
+ const void* aData, size_t aSize);
+
+ nsresult EnablePacketDump(unsigned long aLevel, dom::mozPacketDumpType aType,
+ bool aSending);
+
+ nsresult DisablePacketDump(unsigned long aLevel, dom::mozPacketDumpType aType,
+ bool aSending);
+
+ private:
+ friend class PeerConnectionImpl;
+ explicit PacketDumper(const std::string& aPcHandle);
+ ~PacketDumper() = default;
+ bool ShouldDumpPacket(size_t aLevel, dom::mozPacketDumpType aType,
+ bool aSending) const;
+
+ // This class is not cycle-collected, so it cannot hold onto a strong ref
+ const std::string mPcHandle;
+ std::vector<unsigned> mSendPacketDumpFlags;
+ std::vector<unsigned> mRecvPacketDumpFlags;
+ Atomic<bool> mPacketDumpEnabled;
+ mutable Mutex mPacketDumpFlagsMutex;
+};
+
+} // namespace mozilla
+
+#endif // _PACKET_DUMPER_H_
diff --git a/dom/media/webrtc/jsapi/PeerConnectionCtx.cpp b/dom/media/webrtc/jsapi/PeerConnectionCtx.cpp
new file mode 100644
index 0000000000..9a8f27fb59
--- /dev/null
+++ b/dom/media/webrtc/jsapi/PeerConnectionCtx.cpp
@@ -0,0 +1,650 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "PeerConnectionCtx.h"
+
+#include "WebrtcGlobalStatsHistory.h"
+#include "api/audio/audio_mixer.h"
+#include "api/audio_codecs/builtin_audio_decoder_factory.h"
+#include "modules/rtp_rtcp/source/rtp_header_extensions.h"
+#include "call/audio_state.h"
+#include "common/browser_logging/CSFLog.h"
+#include "common/browser_logging/WebRtcLog.h"
+#include "gmp-video-decode.h" // GMP_API_VIDEO_DECODER
+#include "gmp-video-encode.h" // GMP_API_VIDEO_ENCODER
+#include "libwebrtcglue/CallWorkerThread.h"
+#include "modules/audio_device/include/fake_audio_device.h"
+#include "modules/audio_processing/include/audio_processing.h"
+#include "modules/audio_processing/include/aec_dump.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/RTCPeerConnectionBinding.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/Types.h"
+#include "mozilla/dom/RTCStatsReportBinding.h"
+#include "nsCRTGlue.h"
+#include "nsIIOService.h"
+#include "nsIObserver.h"
+#include "nsIObserverService.h"
+#include "nsNetCID.h" // NS_SOCKETTRANSPORTSERVICE_CONTRACTID
+#include "nsServiceManagerUtils.h" // do_GetService
+#include "PeerConnectionImpl.h"
+#include "prcvar.h"
+#include "transport/runnable_utils.h"
+#include "WebrtcGlobalChild.h"
+#include "WebrtcGlobalInformation.h"
+
+static const char* pccLogTag = "PeerConnectionCtx";
+#ifdef LOGTAG
+# undef LOGTAG
+#endif
+#define LOGTAG pccLogTag
+
+using namespace webrtc;
+
+namespace {
+class DummyAudioMixer : public AudioMixer {
+ public:
+ bool AddSource(Source*) override { return true; }
+ void RemoveSource(Source*) override {}
+ void Mix(size_t, AudioFrame*) override { MOZ_CRASH("Unexpected call"); }
+};
+
+class DummyAudioProcessing : public AudioProcessing {
+ public:
+ int Initialize() override {
+ MOZ_CRASH("Unexpected call");
+ return kNoError;
+ }
+ int Initialize(const ProcessingConfig&) override { return Initialize(); }
+ void ApplyConfig(const Config&) override { MOZ_CRASH("Unexpected call"); }
+ int proc_sample_rate_hz() const override {
+ MOZ_CRASH("Unexpected call");
+ return 0;
+ }
+ int proc_split_sample_rate_hz() const override {
+ MOZ_CRASH("Unexpected call");
+ return 0;
+ }
+ size_t num_input_channels() const override {
+ MOZ_CRASH("Unexpected call");
+ return 0;
+ }
+ size_t num_proc_channels() const override {
+ MOZ_CRASH("Unexpected call");
+ return 0;
+ }
+ size_t num_output_channels() const override {
+ MOZ_CRASH("Unexpected call");
+ return 0;
+ }
+ size_t num_reverse_channels() const override {
+ MOZ_CRASH("Unexpected call");
+ return 0;
+ }
+ void set_output_will_be_muted(bool) override { MOZ_CRASH("Unexpected call"); }
+ void SetRuntimeSetting(RuntimeSetting) override {
+ MOZ_CRASH("Unexpected call");
+ }
+ bool PostRuntimeSetting(RuntimeSetting setting) override { return false; }
+ int ProcessStream(const int16_t* const, const StreamConfig&,
+ const StreamConfig&, int16_t* const) override {
+ MOZ_CRASH("Unexpected call");
+ return kNoError;
+ }
+ int ProcessStream(const float* const*, const StreamConfig&,
+ const StreamConfig&, float* const*) override {
+ MOZ_CRASH("Unexpected call");
+ return kNoError;
+ }
+ int ProcessReverseStream(const int16_t* const, const StreamConfig&,
+ const StreamConfig&, int16_t* const) override {
+ MOZ_CRASH("Unexpected call");
+ return kNoError;
+ }
+ int ProcessReverseStream(const float* const*, const StreamConfig&,
+ const StreamConfig&, float* const*) override {
+ MOZ_CRASH("Unexpected call");
+ return kNoError;
+ }
+ int AnalyzeReverseStream(const float* const*, const StreamConfig&) override {
+ MOZ_CRASH("Unexpected call");
+ return kNoError;
+ }
+ bool GetLinearAecOutput(
+ rtc::ArrayView<std::array<float, 160>>) const override {
+ MOZ_CRASH("Unexpected call");
+ return false;
+ }
+ void set_stream_analog_level(int) override { MOZ_CRASH("Unexpected call"); }
+ int recommended_stream_analog_level() const override {
+ MOZ_CRASH("Unexpected call");
+ return -1;
+ }
+ int set_stream_delay_ms(int) override {
+ MOZ_CRASH("Unexpected call");
+ return kNoError;
+ }
+ int stream_delay_ms() const override {
+ MOZ_CRASH("Unexpected call");
+ return 0;
+ }
+ void set_stream_key_pressed(bool) override { MOZ_CRASH("Unexpected call"); }
+ bool CreateAndAttachAecDump(absl::string_view, int64_t,
+ rtc::TaskQueue*) override {
+ MOZ_CRASH("Unexpected call");
+ return false;
+ }
+ bool CreateAndAttachAecDump(FILE*, int64_t, rtc::TaskQueue*) override {
+ MOZ_CRASH("Unexpected call");
+ return false;
+ }
+ void AttachAecDump(std::unique_ptr<AecDump>) override {
+ MOZ_CRASH("Unexpected call");
+ }
+ void DetachAecDump() override { MOZ_CRASH("Unexpected call"); }
+ AudioProcessingStats GetStatistics() override {
+ return AudioProcessingStats();
+ }
+ AudioProcessingStats GetStatistics(bool) override { return GetStatistics(); }
+ AudioProcessing::Config GetConfig() const override {
+ MOZ_CRASH("Unexpected call");
+ return Config();
+ }
+};
+} // namespace
+
+namespace mozilla {
+
+using namespace dom;
+
+SharedWebrtcState::SharedWebrtcState(
+ RefPtr<AbstractThread> aCallWorkerThread,
+ webrtc::AudioState::Config&& aAudioStateConfig,
+ RefPtr<webrtc::AudioDecoderFactory> aAudioDecoderFactory,
+ UniquePtr<webrtc::WebRtcKeyValueConfig> aTrials)
+ : mCallWorkerThread(std::move(aCallWorkerThread)),
+ mAudioStateConfig(std::move(aAudioStateConfig)),
+ mAudioDecoderFactory(std::move(aAudioDecoderFactory)),
+ mTrials(std::move(aTrials)) {}
+
+SharedWebrtcState::~SharedWebrtcState() = default;
+
+class PeerConnectionCtxObserver : public nsIObserver {
+ public:
+ NS_DECL_ISUPPORTS
+
+ PeerConnectionCtxObserver() {}
+
+ void Init() {
+ nsCOMPtr<nsIObserverService> observerService =
+ services::GetObserverService();
+ if (!observerService) return;
+
+ nsresult rv = NS_OK;
+
+ rv = observerService->AddObserver(this, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID,
+ false);
+ MOZ_ALWAYS_SUCCEEDS(rv);
+ rv = observerService->AddObserver(this, NS_IOSERVICE_OFFLINE_STATUS_TOPIC,
+ false);
+ MOZ_ALWAYS_SUCCEEDS(rv);
+ (void)rv;
+ }
+
+ NS_IMETHOD Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) override {
+ if (strcmp(aTopic, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID) == 0) {
+ CSFLogDebug(LOGTAG, "Shutting down PeerConnectionCtx");
+ PeerConnectionCtx::Destroy();
+
+ nsCOMPtr<nsIObserverService> observerService =
+ services::GetObserverService();
+ if (!observerService) return NS_ERROR_FAILURE;
+
+ nsresult rv = observerService->RemoveObserver(
+ this, NS_IOSERVICE_OFFLINE_STATUS_TOPIC);
+ MOZ_ALWAYS_SUCCEEDS(rv);
+ rv = observerService->RemoveObserver(this,
+ NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID);
+ MOZ_ALWAYS_SUCCEEDS(rv);
+
+ // Make sure we're not deleted while still inside ::Observe()
+ RefPtr<PeerConnectionCtxObserver> kungFuDeathGrip(this);
+ PeerConnectionCtx::gPeerConnectionCtxObserver = nullptr;
+ }
+ if (strcmp(aTopic, NS_IOSERVICE_OFFLINE_STATUS_TOPIC) == 0) {
+ if (NS_strcmp(aData, u"" NS_IOSERVICE_OFFLINE) == 0) {
+ CSFLogDebug(LOGTAG, "Updating network state to offline");
+ PeerConnectionCtx::UpdateNetworkState(false);
+ } else if (NS_strcmp(aData, u"" NS_IOSERVICE_ONLINE) == 0) {
+ CSFLogDebug(LOGTAG, "Updating network state to online");
+ PeerConnectionCtx::UpdateNetworkState(true);
+ } else {
+ CSFLogDebug(LOGTAG, "Received unsupported network state event");
+ MOZ_CRASH();
+ }
+ }
+ return NS_OK;
+ }
+
+ private:
+ virtual ~PeerConnectionCtxObserver() {
+ nsCOMPtr<nsIObserverService> observerService =
+ services::GetObserverService();
+ if (observerService) {
+ observerService->RemoveObserver(this, NS_IOSERVICE_OFFLINE_STATUS_TOPIC);
+ observerService->RemoveObserver(this, NS_XPCOM_WILL_SHUTDOWN_OBSERVER_ID);
+ }
+ }
+};
+
+NS_IMPL_ISUPPORTS(PeerConnectionCtxObserver, nsIObserver);
+
+PeerConnectionCtx* PeerConnectionCtx::gInstance;
+StaticRefPtr<PeerConnectionCtxObserver>
+ PeerConnectionCtx::gPeerConnectionCtxObserver;
+
+nsresult PeerConnectionCtx::InitializeGlobal() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsresult res;
+
+ if (!gInstance) {
+ CSFLogDebug(LOGTAG, "Creating PeerConnectionCtx");
+ PeerConnectionCtx* ctx = new PeerConnectionCtx();
+
+ res = ctx->Initialize();
+ PR_ASSERT(NS_SUCCEEDED(res));
+ if (!NS_SUCCEEDED(res)) return res;
+
+ gInstance = ctx;
+
+ if (!PeerConnectionCtx::gPeerConnectionCtxObserver) {
+ PeerConnectionCtx::gPeerConnectionCtxObserver =
+ new PeerConnectionCtxObserver();
+ PeerConnectionCtx::gPeerConnectionCtxObserver->Init();
+ }
+ }
+
+ EnableWebRtcLog();
+ return NS_OK;
+}
+
+PeerConnectionCtx* PeerConnectionCtx::GetInstance() {
+ MOZ_ASSERT(gInstance);
+ return gInstance;
+}
+
+bool PeerConnectionCtx::isActive() { return gInstance; }
+
+void PeerConnectionCtx::Destroy() {
+ CSFLogDebug(LOGTAG, "%s", __FUNCTION__);
+
+ if (gInstance) {
+ // Null out gInstance first, so PeerConnectionImpl doesn't try to use it
+ // in Cleanup.
+ auto* instance = gInstance;
+ gInstance = nullptr;
+ instance->Cleanup();
+ delete instance;
+ }
+
+ StopWebRtcLog();
+}
+
+template <typename T>
+static void RecordCommonRtpTelemetry(const T& list, const T& lastList,
+ const bool isRemote) {
+ using namespace Telemetry;
+ for (const auto& s : list) {
+ const bool isAudio = s.mKind.Find(u"audio") != -1;
+ if (s.mPacketsLost.WasPassed() && s.mPacketsReceived.WasPassed()) {
+ if (const uint64_t total =
+ s.mPacketsLost.Value() + s.mPacketsReceived.Value()) {
+ HistogramID id =
+ isRemote ? (isAudio ? WEBRTC_AUDIO_QUALITY_OUTBOUND_PACKETLOSS_RATE
+ : WEBRTC_VIDEO_QUALITY_OUTBOUND_PACKETLOSS_RATE)
+ : (isAudio ? WEBRTC_AUDIO_QUALITY_INBOUND_PACKETLOSS_RATE
+ : WEBRTC_VIDEO_QUALITY_INBOUND_PACKETLOSS_RATE);
+ Accumulate(id, (s.mPacketsLost.Value() * 1000) / total);
+ }
+ }
+ if (s.mJitter.WasPassed()) {
+ HistogramID id = isRemote
+ ? (isAudio ? WEBRTC_AUDIO_QUALITY_OUTBOUND_JITTER
+ : WEBRTC_VIDEO_QUALITY_OUTBOUND_JITTER)
+ : (isAudio ? WEBRTC_AUDIO_QUALITY_INBOUND_JITTER
+ : WEBRTC_VIDEO_QUALITY_INBOUND_JITTER);
+ Accumulate(id, s.mJitter.Value() * 1000);
+ }
+ }
+}
+
+// Telemetry reporting every second after start of first call.
+// The threading model around the media pipelines is weird:
+// - The pipelines are containers,
+// - containers that are only safe on main thread, with members only safe on
+// STS,
+// - hence the there and back again approach.
+
+void PeerConnectionCtx::DeliverStats(
+ UniquePtr<dom::RTCStatsReportInternal>&& aReport) {
+ using namespace Telemetry;
+
+ // First, get reports from a second ago, if any, for calculations below
+ UniquePtr<dom::RTCStatsReportInternal> lastReport;
+ {
+ auto i = mLastReports.find(aReport->mPcid);
+ if (i != mLastReports.end()) {
+ lastReport = std::move(i->second);
+ } else {
+ lastReport = MakeUnique<dom::RTCStatsReportInternal>();
+ }
+ }
+ // Record Telemetery
+ RecordCommonRtpTelemetry(aReport->mInboundRtpStreamStats,
+ lastReport->mInboundRtpStreamStats, false);
+ // Record bandwidth telemetry
+ for (const auto& s : aReport->mInboundRtpStreamStats) {
+ if (s.mBytesReceived.WasPassed()) {
+ const bool isAudio = s.mKind.Find(u"audio") != -1;
+ for (const auto& lastS : lastReport->mInboundRtpStreamStats) {
+ if (lastS.mId == s.mId) {
+ int32_t deltaMs = s.mTimestamp.Value() - lastS.mTimestamp.Value();
+ // In theory we're called every second, so delta *should* be in that
+ // range. Small deltas could cause errors due to division
+ if (deltaMs < 500 || deltaMs > 60000 ||
+ !lastS.mBytesReceived.WasPassed()) {
+ break;
+ }
+ HistogramID id = isAudio
+ ? WEBRTC_AUDIO_QUALITY_INBOUND_BANDWIDTH_KBITS
+ : WEBRTC_VIDEO_QUALITY_INBOUND_BANDWIDTH_KBITS;
+ // We could accumulate values until enough time has passed
+ // and then Accumulate() but this isn't that important
+ Accumulate(
+ id,
+ ((s.mBytesReceived.Value() - lastS.mBytesReceived.Value()) * 8) /
+ deltaMs);
+ break;
+ }
+ }
+ }
+ }
+ RecordCommonRtpTelemetry(aReport->mRemoteInboundRtpStreamStats,
+ lastReport->mRemoteInboundRtpStreamStats, true);
+ for (const auto& s : aReport->mRemoteInboundRtpStreamStats) {
+ if (s.mRoundTripTime.WasPassed()) {
+ const bool isAudio = s.mKind.Find(u"audio") != -1;
+ HistogramID id = isAudio ? WEBRTC_AUDIO_QUALITY_OUTBOUND_RTT
+ : WEBRTC_VIDEO_QUALITY_OUTBOUND_RTT;
+ Accumulate(id, s.mRoundTripTime.Value() * 1000);
+ }
+ }
+
+ mLastReports[aReport->mPcid] = std::move(aReport);
+}
+
+void PeerConnectionCtx::EverySecondTelemetryCallback_m(nsITimer* timer,
+ void* closure) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(PeerConnectionCtx::isActive());
+
+ for (auto& idAndPc : GetInstance()->mPeerConnections) {
+ if (!idAndPc.second->IsClosed()) {
+ idAndPc.second->GetStats(nullptr, true)
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [=](UniquePtr<dom::RTCStatsReportInternal>&& aReport) {
+ if (PeerConnectionCtx::isActive()) {
+ PeerConnectionCtx::GetInstance()->DeliverStats(
+ std::move(aReport));
+ }
+ },
+ [=](nsresult aError) {});
+ idAndPc.second->CollectConduitTelemetryData();
+ }
+ }
+}
+
+void PeerConnectionCtx::UpdateNetworkState(bool online) {
+ auto ctx = GetInstance();
+ if (ctx->mPeerConnections.empty()) {
+ return;
+ }
+ for (auto pc : ctx->mPeerConnections) {
+ pc.second->UpdateNetworkState(online);
+ }
+}
+
+SharedWebrtcState* PeerConnectionCtx::GetSharedWebrtcState() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mSharedWebrtcState;
+}
+
+void PeerConnectionCtx::RemovePeerConnection(const std::string& aKey) {
+ MOZ_ASSERT(NS_IsMainThread());
+ auto it = mPeerConnections.find(aKey);
+ if (it != mPeerConnections.end()) {
+ if (it->second->GetFinalStats() && !it->second->LongTermStatsIsDisabled()) {
+ WebrtcGlobalInformation::StashStats(*(it->second->GetFinalStats()));
+ }
+ nsAutoString pcId = NS_ConvertASCIItoUTF16(it->second->GetName().c_str());
+ if (XRE_IsContentProcess()) {
+ if (auto* child = WebrtcGlobalChild::Get(); child) {
+ auto pcId = NS_ConvertASCIItoUTF16(it->second->GetName().c_str());
+ child->SendPeerConnectionFinalStats(*(it->second->GetFinalStats()));
+ child->SendPeerConnectionDestroyed(pcId);
+ }
+ } else {
+ using Update = WebrtcGlobalInformation::PcTrackingUpdate;
+ auto update = Update::Remove(pcId);
+ auto finalStats =
+ MakeUnique<RTCStatsReportInternal>(*(it->second->GetFinalStats()));
+ WebrtcGlobalStatsHistory::Record(std::move(finalStats));
+ WebrtcGlobalInformation::PeerConnectionTracking(update);
+ }
+
+ mPeerConnections.erase(it);
+ if (mPeerConnections.empty()) {
+ mSharedWebrtcState = nullptr;
+ StopTelemetryTimer();
+ }
+ }
+}
+
+void PeerConnectionCtx::AddPeerConnection(const std::string& aKey,
+ PeerConnectionImpl* aPeerConnection) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mPeerConnections.count(aKey) == 0,
+ "PeerConnection with this key should not already exist");
+ if (mPeerConnections.empty()) {
+ AudioState::Config audioStateConfig;
+ audioStateConfig.audio_mixer = new rtc::RefCountedObject<DummyAudioMixer>();
+ AudioProcessingBuilder audio_processing_builder;
+ audioStateConfig.audio_processing =
+ new rtc::RefCountedObject<DummyAudioProcessing>();
+ audioStateConfig.audio_device_module =
+ new rtc::RefCountedObject<FakeAudioDeviceModule>();
+
+ SharedThreadPoolWebRtcTaskQueueFactory taskQueueFactory;
+ constexpr bool supportTailDispatch = true;
+ // Note the NonBlocking DeletionPolicy!
+ // This task queue is passed into libwebrtc as a raw pointer.
+ // WebrtcCallWrapper guarantees that it outlives its webrtc::Call instance.
+ // Outside of libwebrtc we must use ref-counting to either the
+ // WebrtcCallWrapper or to the CallWorkerThread to keep it alive.
+ auto callWorkerThread =
+ WrapUnique(taskQueueFactory
+ .CreateTaskQueueWrapper<DeletionPolicy::NonBlocking>(
+ "CallWorker", supportTailDispatch,
+ webrtc::TaskQueueFactory::Priority::NORMAL,
+ MediaThreadType::WEBRTC_CALL_THREAD)
+ .release());
+
+ UniquePtr<webrtc::WebRtcKeyValueConfig> trials =
+ WrapUnique(new NoTrialsConfig());
+
+ mSharedWebrtcState = MakeAndAddRef<SharedWebrtcState>(
+ new CallWorkerThread(std::move(callWorkerThread)),
+ std::move(audioStateConfig),
+ already_AddRefed(CreateBuiltinAudioDecoderFactory().release()),
+ std::move(trials));
+ StartTelemetryTimer();
+ }
+ auto pcId = NS_ConvertASCIItoUTF16(aPeerConnection->GetName().c_str());
+ if (XRE_IsContentProcess()) {
+ if (auto* child = WebrtcGlobalChild::Get(); child) {
+ child->SendPeerConnectionCreated(
+ pcId, aPeerConnection->LongTermStatsIsDisabled());
+ }
+ } else {
+ using Update = WebrtcGlobalInformation::PcTrackingUpdate;
+ auto update = Update::Add(pcId, aPeerConnection->LongTermStatsIsDisabled());
+ WebrtcGlobalInformation::PeerConnectionTracking(update);
+ }
+ mPeerConnections[aKey] = aPeerConnection;
+}
+
+PeerConnectionImpl* PeerConnectionCtx::GetPeerConnection(
+ const std::string& aKey) const {
+ MOZ_ASSERT(NS_IsMainThread());
+ auto iterator = mPeerConnections.find(aKey);
+ if (iterator == mPeerConnections.end()) {
+ return nullptr;
+ }
+ return iterator->second;
+}
+
+void PeerConnectionCtx::ClearClosedStats() {
+ for (auto& [id, pc] : mPeerConnections) {
+ Unused << id;
+ if (pc->IsClosed()) {
+ // Rare case
+ pc->DisableLongTermStats();
+ }
+ }
+}
+
+nsresult PeerConnectionCtx::Initialize() {
+ MOZ_ASSERT(NS_IsMainThread());
+ initGMP();
+ SdpRidAttributeList::kMaxRidLength =
+ webrtc::BaseRtpStringExtension::kMaxValueSizeBytes;
+
+ if (XRE_IsContentProcess()) {
+ WebrtcGlobalChild::Get();
+ }
+
+ return NS_OK;
+}
+
+nsresult PeerConnectionCtx::StartTelemetryTimer() {
+ return NS_NewTimerWithFuncCallback(getter_AddRefs(mTelemetryTimer),
+ EverySecondTelemetryCallback_m, this, 1000,
+ nsITimer::TYPE_REPEATING_PRECISE_CAN_SKIP,
+ "EverySecondTelemetryCallback_m");
+}
+
+void PeerConnectionCtx::StopTelemetryTimer() {
+ if (mTelemetryTimer) {
+ mTelemetryTimer->Cancel();
+ mTelemetryTimer = nullptr;
+ }
+}
+
+static void GMPReady_m() {
+ if (PeerConnectionCtx::isActive()) {
+ PeerConnectionCtx::GetInstance()->onGMPReady();
+ }
+};
+
+static void GMPReady() {
+ GetMainThreadSerialEventTarget()->Dispatch(WrapRunnableNM(&GMPReady_m),
+ NS_DISPATCH_NORMAL);
+};
+
+void PeerConnectionCtx::initGMP() {
+ mGMPService = do_GetService("@mozilla.org/gecko-media-plugin-service;1");
+
+ if (!mGMPService) {
+ CSFLogError(LOGTAG, "%s failed to get the gecko-media-plugin-service",
+ __FUNCTION__);
+ return;
+ }
+
+ nsCOMPtr<nsIThread> thread;
+ nsresult rv = mGMPService->GetThread(getter_AddRefs(thread));
+
+ if (NS_FAILED(rv)) {
+ mGMPService = nullptr;
+ CSFLogError(LOGTAG,
+ "%s failed to get the gecko-media-plugin thread, err=%u",
+ __FUNCTION__, static_cast<unsigned>(rv));
+ return;
+ }
+
+ // presumes that all GMP dir scans have been queued for the GMPThread
+ thread->Dispatch(WrapRunnableNM(&GMPReady), NS_DISPATCH_NORMAL);
+}
+
+nsresult PeerConnectionCtx::Cleanup() {
+ CSFLogDebug(LOGTAG, "%s", __FUNCTION__);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mQueuedJSEPOperations.Clear();
+ mGMPService = nullptr;
+ mTransportHandler = nullptr;
+ for (auto& [id, pc] : mPeerConnections) {
+ (void)id;
+ pc->Close();
+ }
+ mPeerConnections.clear();
+ mSharedWebrtcState = nullptr;
+ return NS_OK;
+}
+
+void PeerConnectionCtx::queueJSEPOperation(nsIRunnable* aOperation) {
+ mQueuedJSEPOperations.AppendElement(aOperation);
+}
+
+void PeerConnectionCtx::onGMPReady() {
+ mGMPReady = true;
+ for (size_t i = 0; i < mQueuedJSEPOperations.Length(); ++i) {
+ mQueuedJSEPOperations[i]->Run();
+ }
+ mQueuedJSEPOperations.Clear();
+}
+
+bool PeerConnectionCtx::gmpHasH264() {
+ if (!mGMPService) {
+ return false;
+ }
+
+ // XXX I'd prefer if this was all known ahead of time...
+
+ AutoTArray<nsCString, 1> tags;
+ tags.AppendElement("h264"_ns);
+
+ bool has_gmp;
+ nsresult rv;
+ rv = mGMPService->HasPluginForAPI(nsLiteralCString(GMP_API_VIDEO_ENCODER),
+ tags, &has_gmp);
+ if (NS_FAILED(rv) || !has_gmp) {
+ return false;
+ }
+
+ rv = mGMPService->HasPluginForAPI(nsLiteralCString(GMP_API_VIDEO_DECODER),
+ tags, &has_gmp);
+ if (NS_FAILED(rv) || !has_gmp) {
+ return false;
+ }
+
+ return true;
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/jsapi/PeerConnectionCtx.h b/dom/media/webrtc/jsapi/PeerConnectionCtx.h
new file mode 100644
index 0000000000..fdd81f6406
--- /dev/null
+++ b/dom/media/webrtc/jsapi/PeerConnectionCtx.h
@@ -0,0 +1,194 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef peerconnectionctx_h___h__
+#define peerconnectionctx_h___h__
+
+#include <map>
+#include <string>
+
+#include "WebrtcGlobalChild.h"
+#include "api/field_trials_view.h"
+#include "api/scoped_refptr.h"
+#include "call/audio_state.h"
+#include "MediaTransportHandler.h" // Mostly for IceLogPromise
+#include "mozIGeckoMediaPluginService.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/StaticPtr.h"
+#include "nsIRunnable.h"
+#include "PeerConnectionImpl.h"
+
+namespace webrtc {
+class AudioDecoderFactory;
+
+// Used for testing in mediapipeline_unittest.cpp, MockCall.h
+class NoTrialsConfig : public FieldTrialsView {
+ public:
+ NoTrialsConfig() = default;
+ std::string Lookup(absl::string_view key) const override {
+ // Upstream added a new default field trial string for
+ // CongestionWindow, that we don't want. In
+ // third_party/libwebrtc/rtc_base/experiments/rate_control_settings.cc
+ // they set kCongestionWindowDefaultFieldTrialString to
+ // "QueueSize:350,MinBitrate:30000,DropFrame:true". With QueueSize
+ // set, GoogCcNetworkController::UpdateCongestionWindowSize is
+ // called. Because negative values are calculated in
+ // feedback_rtt, an assert fires when calculating data_window in
+ // GoogCcNetworkController::UpdateCongestionWindowSize. We probably
+ // need to figure out why we're calculating negative feedback_rtt.
+ // See Bug 1780620.
+ if ("WebRTC-CongestionWindow" == key) {
+ return std::string("MinBitrate:30000,DropFrame:true");
+ }
+ return std::string();
+ }
+};
+} // namespace webrtc
+
+namespace mozilla {
+class PeerConnectionCtxObserver;
+
+namespace dom {
+class WebrtcGlobalInformation;
+}
+
+/**
+ * Refcounted class containing state shared across all PeerConnections and all
+ * Call instances. Managed by PeerConnectionCtx, and kept around while there are
+ * registered peer connections.
+ */
+class SharedWebrtcState {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SharedWebrtcState)
+
+ SharedWebrtcState(RefPtr<AbstractThread> aCallWorkerThread,
+ webrtc::AudioState::Config&& aAudioStateConfig,
+ RefPtr<webrtc::AudioDecoderFactory> aAudioDecoderFactory,
+ UniquePtr<webrtc::FieldTrialsView> aTrials);
+
+ // A global Call worker thread shared between all Call instances. Implements
+ // AbstractThread for running tasks that call into a Call instance through its
+ // webrtc::TaskQueue member, and for using AbstractThread-specific higher
+ // order constructs like StateMirroring.
+ const RefPtr<AbstractThread> mCallWorkerThread;
+
+ // AudioState config containing dummy implementations of the audio stack,
+ // since we use our own audio stack instead. Shared across all Call instances.
+ const webrtc::AudioState::Config mAudioStateConfig;
+
+ // AudioDecoderFactory instance shared between calls, to limit the number of
+ // instances in large calls.
+ const RefPtr<webrtc::AudioDecoderFactory> mAudioDecoderFactory;
+
+ // Trials instance shared between calls, to limit the number of instances in
+ // large calls.
+ const UniquePtr<webrtc::FieldTrialsView> mTrials;
+
+ private:
+ virtual ~SharedWebrtcState();
+};
+
+// A class to hold some of the singleton objects we need:
+// * The global PeerConnectionImpl table and its associated lock.
+// * Stats report objects for PCs that are gone
+// * GMP related state
+// * Upstream webrtc state shared across all Calls (processing thread)
+class PeerConnectionCtx {
+ public:
+ static nsresult InitializeGlobal();
+ static PeerConnectionCtx* GetInstance();
+ static bool isActive();
+ static void Destroy();
+
+ bool isReady() {
+ // If mGMPService is not set, we aren't using GMP.
+ if (mGMPService) {
+ return mGMPReady;
+ }
+ return true;
+ }
+
+ void queueJSEPOperation(nsIRunnable* aJSEPOperation);
+ void onGMPReady();
+
+ bool gmpHasH264();
+
+ static void UpdateNetworkState(bool online);
+
+ RefPtr<MediaTransportHandler> GetTransportHandler() const {
+ return mTransportHandler;
+ }
+
+ SharedWebrtcState* GetSharedWebrtcState() const;
+
+ void RemovePeerConnection(const std::string& aKey);
+ void AddPeerConnection(const std::string& aKey,
+ PeerConnectionImpl* aPeerConnection);
+ PeerConnectionImpl* GetPeerConnection(const std::string& aKey) const;
+ template <typename Function>
+ void ForEachPeerConnection(Function&& aFunction) const {
+ MOZ_ASSERT(NS_IsMainThread());
+ for (const auto& pair : mPeerConnections) {
+ aFunction(pair.second);
+ }
+ }
+
+ void ClearClosedStats();
+
+ private:
+ std::map<const std::string, PeerConnectionImpl*> mPeerConnections;
+
+ PeerConnectionCtx()
+ : mGMPReady(false),
+ mTransportHandler(
+ MediaTransportHandler::Create(GetMainThreadSerialEventTarget())) {}
+
+ // This is a singleton, so don't copy construct it, etc.
+ PeerConnectionCtx(const PeerConnectionCtx& other) = delete;
+ void operator=(const PeerConnectionCtx& other) = delete;
+ virtual ~PeerConnectionCtx() = default;
+
+ nsresult Initialize();
+ nsresult StartTelemetryTimer();
+ void StopTelemetryTimer();
+ nsresult Cleanup();
+
+ void initGMP();
+
+ static void EverySecondTelemetryCallback_m(nsITimer* timer, void*);
+
+ nsCOMPtr<nsITimer> mTelemetryTimer;
+
+ private:
+ void DeliverStats(UniquePtr<dom::RTCStatsReportInternal>&& aReport);
+
+ std::map<nsString, UniquePtr<dom::RTCStatsReportInternal>> mLastReports;
+ // We cannot form offers/answers properly until the Gecko Media Plugin stuff
+ // has been initted, which is a complicated mess of thread dispatches,
+ // including sync dispatches to main. So, we need to be able to queue up
+ // offer creation (or SetRemote, when we're the answerer) until all of this is
+ // ready to go, since blocking on this init is just begging for deadlock.
+ nsCOMPtr<mozIGeckoMediaPluginService> mGMPService;
+ bool mGMPReady;
+ nsTArray<nsCOMPtr<nsIRunnable>> mQueuedJSEPOperations;
+
+ // Not initted, just for ICE logging stuff
+ RefPtr<MediaTransportHandler> mTransportHandler;
+
+ // State used by libwebrtc that needs to be shared across all PeerConnections
+ // and all Call instances. Set while there is at least one peer connection
+ // registered. CallWrappers can hold a ref to this object to be sure members
+ // are alive long enough.
+ RefPtr<SharedWebrtcState> mSharedWebrtcState;
+
+ static PeerConnectionCtx* gInstance;
+
+ public:
+ static mozilla::StaticRefPtr<mozilla::PeerConnectionCtxObserver>
+ gPeerConnectionCtxObserver;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/jsapi/PeerConnectionImpl.cpp b/dom/media/webrtc/jsapi/PeerConnectionImpl.cpp
new file mode 100644
index 0000000000..567b682b2a
--- /dev/null
+++ b/dom/media/webrtc/jsapi/PeerConnectionImpl.cpp
@@ -0,0 +1,4640 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <cstdlib>
+#include <cerrno>
+#include <deque>
+#include <set>
+#include <sstream>
+#include <vector>
+
+#include "common/browser_logging/CSFLog.h"
+#include "base/histogram.h"
+#include "common/time_profiling/timecard.h"
+
+#include "jsapi.h"
+#include "nspr.h"
+#include "nss.h"
+#include "pk11pub.h"
+
+#include "nsNetCID.h"
+#include "nsIIDNService.h"
+#include "nsILoadContext.h"
+#include "nsEffectiveTLDService.h"
+#include "nsServiceManagerUtils.h"
+#include "nsThreadUtils.h"
+#include "nsIPrefService.h"
+#include "nsIPrefBranch.h"
+#include "nsProxyRelease.h"
+#include "prtime.h"
+
+#include "libwebrtcglue/AudioConduit.h"
+#include "libwebrtcglue/VideoConduit.h"
+#include "libwebrtcglue/WebrtcCallWrapper.h"
+#include "MediaTrackGraph.h"
+#include "transport/runnable_utils.h"
+#include "IPeerConnection.h"
+#include "PeerConnectionCtx.h"
+#include "PeerConnectionImpl.h"
+#include "RemoteTrackSource.h"
+#include "nsDOMDataChannelDeclarations.h"
+#include "transport/dtlsidentity.h"
+#include "sdp/SdpAttribute.h"
+
+#include "jsep/JsepTrack.h"
+#include "jsep/JsepSession.h"
+#include "jsep/JsepSessionImpl.h"
+
+#include "transportbridge/MediaPipeline.h"
+#include "jsapi/RTCRtpReceiver.h"
+#include "jsapi/RTCRtpSender.h"
+
+#include "mozilla/IntegerPrintfMacros.h"
+#include "mozilla/Sprintf.h"
+
+#ifdef XP_WIN
+// We need to undef the MS macro for Document::CreateEvent
+# ifdef CreateEvent
+# undef CreateEvent
+# endif
+#endif // XP_WIN
+
+#include "mozilla/dom/Document.h"
+#include "nsGlobalWindow.h"
+#include "nsDOMDataChannel.h"
+#include "mozilla/dom/Location.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/NullPrincipal.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/PublicSSL.h"
+#include "nsXULAppAPI.h"
+#include "nsContentUtils.h"
+#include "nsDOMJSUtils.h"
+#include "nsPrintfCString.h"
+#include "nsURLHelper.h"
+#include "nsNetUtil.h"
+#include "js/ArrayBuffer.h" // JS::NewArrayBufferWithContents
+#include "js/GCAnnotations.h" // JS_HAZ_ROOTED
+#include "js/RootingAPI.h" // JS::{{,Mutable}Handle,Rooted}
+#include "mozilla/PeerIdentity.h"
+#include "mozilla/dom/RTCCertificate.h"
+#include "mozilla/dom/RTCSctpTransportBinding.h" // RTCSctpTransportState
+#include "mozilla/dom/RTCDtlsTransportBinding.h" // RTCDtlsTransportState
+#include "mozilla/dom/RTCRtpReceiverBinding.h"
+#include "mozilla/dom/RTCRtpSenderBinding.h"
+#include "mozilla/dom/RTCStatsReportBinding.h"
+#include "mozilla/dom/RTCPeerConnectionBinding.h"
+#include "mozilla/dom/PeerConnectionImplBinding.h"
+#include "mozilla/dom/RTCDataChannelBinding.h"
+#include "mozilla/dom/PluginCrashedEvent.h"
+#include "MediaStreamTrack.h"
+#include "AudioStreamTrack.h"
+#include "VideoStreamTrack.h"
+#include "nsIScriptGlobalObject.h"
+#include "DOMMediaStream.h"
+#include "WebrtcGlobalInformation.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/EventDispatcher.h"
+#include "mozilla/net/DataChannelProtocol.h"
+#include "MediaManager.h"
+
+#include "transport/nr_socket_proxy_config.h"
+#include "RTCSctpTransport.h"
+#include "RTCDtlsTransport.h"
+#include "jsep/JsepTransport.h"
+
+#include "nsILoadInfo.h"
+#include "nsIPrincipal.h"
+#include "mozilla/LoadInfo.h"
+#include "nsIProxiedChannel.h"
+
+#include "mozilla/dom/BrowserChild.h"
+#include "mozilla/net/WebrtcProxyConfig.h"
+
+#ifdef XP_WIN
+// We need to undef the MS macro again in case the windows include file
+// got imported after we included mozilla/dom/Document.h
+# ifdef CreateEvent
+# undef CreateEvent
+# endif
+#endif // XP_WIN
+
+#include "MediaSegment.h"
+
+#ifdef USE_FAKE_PCOBSERVER
+# include "FakePCObserver.h"
+#else
+# include "mozilla/dom/PeerConnectionObserverBinding.h"
+#endif
+#include "mozilla/dom/PeerConnectionObserverEnumsBinding.h"
+
+#define ICE_PARSING \
+ "In RTCConfiguration passed to RTCPeerConnection constructor"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+
+typedef PCObserverString ObString;
+
+static const char* pciLogTag = "PeerConnectionImpl";
+#ifdef LOGTAG
+# undef LOGTAG
+#endif
+#define LOGTAG pciLogTag
+
+static mozilla::LazyLogModule logModuleInfo("signaling");
+
+// Getting exceptions back down from PCObserver is generally not harmful.
+namespace {
+// This is a terrible hack. The problem is that SuppressException is not
+// inline, and we link this file without libxul in some cases (e.g. for our test
+// setup). So we can't use ErrorResult or IgnoredErrorResult because those call
+// SuppressException... And we can't use FastErrorResult because we can't
+// include BindingUtils.h, because our linking is completely broken. Use
+// BaseErrorResult directly. Please do not let me see _anyone_ doing this
+// without really careful review from someone who knows what they are doing.
+class JSErrorResult : public binding_danger::TErrorResult<
+ binding_danger::JustAssertCleanupPolicy> {
+ public:
+ ~JSErrorResult() { SuppressException(); }
+} JS_HAZ_ROOTED;
+
+// The WrapRunnable() macros copy passed-in args and passes them to the function
+// later on the other thread. ErrorResult cannot be passed like this because it
+// disallows copy-semantics.
+//
+// This WrappableJSErrorResult hack solves this by not actually copying the
+// ErrorResult, but creating a new one instead, which works because we don't
+// care about the result.
+//
+// Since this is for JS-calls, these can only be dispatched to the main thread.
+
+class WrappableJSErrorResult {
+ public:
+ WrappableJSErrorResult() : mRv(MakeUnique<JSErrorResult>()), isCopy(false) {}
+ WrappableJSErrorResult(const WrappableJSErrorResult& other)
+ : mRv(MakeUnique<JSErrorResult>()), isCopy(true) {}
+ ~WrappableJSErrorResult() {
+ if (isCopy) {
+ MOZ_ASSERT(NS_IsMainThread());
+ }
+ }
+ operator ErrorResult&() { return *mRv; }
+
+ private:
+ mozilla::UniquePtr<JSErrorResult> mRv;
+ bool isCopy;
+} JS_HAZ_ROOTED;
+
+} // namespace
+
+static nsresult InitNSSInContent() {
+ NS_ENSURE_TRUE(NS_IsMainThread(), NS_ERROR_NOT_SAME_THREAD);
+
+ if (!XRE_IsContentProcess()) {
+ MOZ_ASSERT_UNREACHABLE("Must be called in content process");
+ return NS_ERROR_FAILURE;
+ }
+
+ static bool nssStarted = false;
+ if (nssStarted) {
+ return NS_OK;
+ }
+
+ if (NSS_NoDB_Init(nullptr) != SECSuccess) {
+ CSFLogError(LOGTAG, "NSS_NoDB_Init failed.");
+ return NS_ERROR_FAILURE;
+ }
+
+ if (NS_FAILED(mozilla::psm::InitializeCipherSuite())) {
+ CSFLogError(LOGTAG, "Fail to set up nss cipher suite.");
+ return NS_ERROR_FAILURE;
+ }
+
+ mozilla::psm::DisableMD5();
+
+ nssStarted = true;
+
+ return NS_OK;
+}
+
+namespace mozilla {
+class DataChannel;
+}
+
+namespace mozilla {
+
+void PeerConnectionAutoTimer::RegisterConnection() { mRefCnt++; }
+
+void PeerConnectionAutoTimer::UnregisterConnection(bool aContainedAV) {
+ MOZ_ASSERT(mRefCnt);
+ mRefCnt--;
+ mUsedAV |= aContainedAV;
+ if (mRefCnt == 0) {
+ if (mUsedAV) {
+ Telemetry::Accumulate(
+ Telemetry::WEBRTC_AV_CALL_DURATION,
+ static_cast<uint32_t>((TimeStamp::Now() - mStart).ToSeconds()));
+ }
+ Telemetry::Accumulate(
+ Telemetry::WEBRTC_CALL_DURATION,
+ static_cast<uint32_t>((TimeStamp::Now() - mStart).ToSeconds()));
+ }
+}
+
+bool PeerConnectionAutoTimer::IsStopped() { return mRefCnt == 0; }
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(PeerConnectionImpl)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(PeerConnectionImpl)
+ tmp->Close();
+ tmp->BreakCycles();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mPCObserver, mWindow, mCertificate,
+ mSTSThread, mReceiveStreams, mOperations,
+ mSctpTransport, mKungFuDeathGrip)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(PeerConnectionImpl)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(
+ mPCObserver, mWindow, mCertificate, mSTSThread, mReceiveStreams,
+ mOperations, mTransceivers, mSctpTransport, mKungFuDeathGrip)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(PeerConnectionImpl)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(PeerConnectionImpl)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PeerConnectionImpl)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+already_AddRefed<PeerConnectionImpl> PeerConnectionImpl::Constructor(
+ const dom::GlobalObject& aGlobal) {
+ RefPtr<PeerConnectionImpl> pc = new PeerConnectionImpl(&aGlobal);
+
+ CSFLogDebug(LOGTAG, "Created PeerConnection: %p", pc.get());
+
+ return pc.forget();
+}
+
+JSObject* PeerConnectionImpl::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return PeerConnectionImpl_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsPIDOMWindowInner* PeerConnectionImpl::GetParentObject() const {
+ return mWindow;
+}
+
+bool PCUuidGenerator::Generate(std::string* idp) {
+ nsresult rv;
+
+ if (!mGenerator) {
+ mGenerator = do_GetService("@mozilla.org/uuid-generator;1", &rv);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+ if (!mGenerator) {
+ return false;
+ }
+ }
+
+ nsID id;
+ rv = mGenerator->GenerateUUIDInPlace(&id);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+ char buffer[NSID_LENGTH];
+ id.ToProvidedString(buffer);
+ idp->assign(buffer);
+
+ return true;
+}
+
+bool IsPrivateBrowsing(nsPIDOMWindowInner* aWindow) {
+ if (!aWindow) {
+ return false;
+ }
+
+ Document* doc = aWindow->GetExtantDoc();
+ if (!doc) {
+ return false;
+ }
+
+ nsILoadContext* loadContext = doc->GetLoadContext();
+ return loadContext && loadContext->UsePrivateBrowsing();
+}
+
+PeerConnectionImpl::PeerConnectionImpl(const GlobalObject* aGlobal)
+ : mTimeCard(MOZ_LOG_TEST(logModuleInfo, LogLevel::Error) ? create_timecard()
+ : nullptr),
+ mJsConfiguration(),
+ mSignalingState(RTCSignalingState::Stable),
+ mIceConnectionState(RTCIceConnectionState::New),
+ mIceGatheringState(RTCIceGatheringState::New),
+ mConnectionState(RTCPeerConnectionState::New),
+ mWindow(do_QueryInterface(aGlobal ? aGlobal->GetAsSupports() : nullptr)),
+ mCertificate(nullptr),
+ mSTSThread(nullptr),
+ mForceIceTcp(false),
+ mTransportHandler(nullptr),
+ mUuidGen(MakeUnique<PCUuidGenerator>()),
+ mIceRestartCount(0),
+ mIceRollbackCount(0),
+ mHaveConfiguredCodecs(false),
+ mTrickle(true) // TODO(ekr@rtfm.com): Use pref
+ ,
+ mPrivateWindow(false),
+ mActiveOnWindow(false),
+ mTimestampMaker(dom::RTCStatsTimestampMaker::Create(mWindow)),
+ mIdGenerator(new RTCStatsIdGenerator()),
+ listenPort(0),
+ connectPort(0),
+ connectStr(nullptr) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT_IF(aGlobal, mWindow);
+ mKungFuDeathGrip = this;
+ if (aGlobal) {
+ if (IsPrivateBrowsing(mWindow)) {
+ mPrivateWindow = true;
+ mDisableLongTermStats = true;
+ }
+ mWindow->AddPeerConnection();
+ mActiveOnWindow = true;
+
+ if (mWindow->GetDocumentURI()) {
+ mWindow->GetDocumentURI()->GetAsciiHost(mHostname);
+ nsresult rv;
+ nsCOMPtr<nsIEffectiveTLDService> eTLDService(
+ do_GetService(NS_EFFECTIVETLDSERVICE_CONTRACTID, &rv));
+ if (eTLDService) {
+ Unused << eTLDService->GetBaseDomain(mWindow->GetDocumentURI(), 0,
+ mEffectiveTLDPlus1);
+ }
+
+ mRtxIsAllowed = !HostnameInPref(
+ "media.peerconnection.video.use_rtx.blocklist", mHostname);
+ }
+ }
+
+ if (!mUuidGen->Generate(&mHandle)) {
+ MOZ_CRASH();
+ }
+
+ CSFLogInfo(LOGTAG, "%s: PeerConnectionImpl constructor for %s", __FUNCTION__,
+ mHandle.c_str());
+ STAMP_TIMECARD(mTimeCard, "Constructor Completed");
+ mForceIceTcp =
+ Preferences::GetBool("media.peerconnection.ice.force_ice_tcp", false);
+ memset(mMaxReceiving, 0, sizeof(mMaxReceiving));
+ memset(mMaxSending, 0, sizeof(mMaxSending));
+ mJsConfiguration.mCertificatesProvided = false;
+ mJsConfiguration.mPeerIdentityProvided = false;
+}
+
+PeerConnectionImpl::~PeerConnectionImpl() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ MOZ_ASSERT(!mTransportHandler,
+ "PeerConnection should either be closed, or not initted in the "
+ "first place.");
+
+ if (mTimeCard) {
+ STAMP_TIMECARD(mTimeCard, "Destructor Invoked");
+ STAMP_TIMECARD(mTimeCard, mHandle.c_str());
+ print_timecard(mTimeCard);
+ destroy_timecard(mTimeCard);
+ mTimeCard = nullptr;
+ }
+
+ CSFLogInfo(LOGTAG, "%s: PeerConnectionImpl destructor invoked for %s",
+ __FUNCTION__, mHandle.c_str());
+}
+
+nsresult PeerConnectionImpl::Initialize(PeerConnectionObserver& aObserver,
+ nsGlobalWindowInner* aWindow) {
+ nsresult res;
+
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mPCObserver = &aObserver;
+
+ // Find the STS thread
+
+ mSTSThread = do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &res);
+ MOZ_ASSERT(mSTSThread);
+
+ // We do callback handling on STS instead of main to avoid media jank.
+ // Someday, we may have a dedicated thread for this.
+ mTransportHandler = MediaTransportHandler::Create(mSTSThread);
+ if (mPrivateWindow) {
+ mTransportHandler->EnterPrivateMode();
+ }
+
+ // Initialize NSS if we are in content process. For chrome process, NSS should
+ // already been initialized.
+ if (XRE_IsParentProcess()) {
+ // This code interferes with the C++ unit test startup code.
+ nsCOMPtr<nsISupports> nssDummy = do_GetService("@mozilla.org/psm;1", &res);
+ NS_ENSURE_SUCCESS(res, res);
+ } else {
+ NS_ENSURE_SUCCESS(res = InitNSSInContent(), res);
+ }
+
+ // Currently no standalone unit tests for DataChannel,
+ // which is the user of mWindow
+ MOZ_ASSERT(aWindow);
+ mWindow = aWindow;
+ NS_ENSURE_STATE(mWindow);
+
+ PRTime timestamp = PR_Now();
+ // Ok if we truncate this, but we want it to be large enough to reliably
+ // contain the location on the tests we run in CI.
+ char temp[256];
+
+ nsAutoCString locationCStr;
+
+ RefPtr<Location> location = mWindow->Location();
+ nsAutoString locationAStr;
+ res = location->ToString(locationAStr);
+ NS_ENSURE_SUCCESS(res, res);
+
+ CopyUTF16toUTF8(locationAStr, locationCStr);
+
+ SprintfLiteral(temp, "%s %" PRIu64 " (id=%" PRIu64 " url=%s)",
+ mHandle.c_str(), static_cast<uint64_t>(timestamp),
+ static_cast<uint64_t>(mWindow ? mWindow->WindowID() : 0),
+ locationCStr.get() ? locationCStr.get() : "NULL");
+
+ mName = temp;
+
+ STAMP_TIMECARD(mTimeCard, "Initializing PC Ctx");
+ res = PeerConnectionCtx::InitializeGlobal();
+ NS_ENSURE_SUCCESS(res, res);
+
+ mTransportHandler->CreateIceCtx("PC:" + GetName());
+
+ mJsepSession =
+ MakeUnique<JsepSessionImpl>(mName, MakeUnique<PCUuidGenerator>());
+ mJsepSession->SetRtxIsAllowed(mRtxIsAllowed);
+
+ res = mJsepSession->Init();
+ if (NS_FAILED(res)) {
+ CSFLogError(LOGTAG, "%s: Couldn't init JSEP Session, res=%u", __FUNCTION__,
+ static_cast<unsigned>(res));
+ return res;
+ }
+
+ std::vector<UniquePtr<JsepCodecDescription>> preferredCodecs;
+ SetupPreferredCodecs(preferredCodecs);
+ mJsepSession->SetDefaultCodecs(preferredCodecs);
+
+ std::vector<RtpExtensionHeader> preferredHeaders;
+ SetupPreferredRtpExtensions(preferredHeaders);
+
+ for (const auto& header : preferredHeaders) {
+ mJsepSession->AddRtpExtension(header.mMediaType, header.extensionname,
+ header.direction);
+ }
+
+ if (XRE_IsContentProcess()) {
+ mStunAddrsRequest =
+ new net::StunAddrsRequestChild(new StunAddrsHandler(this));
+ }
+
+ // Initialize the media object.
+ mForceProxy = ShouldForceProxy();
+
+ // We put this here, in case we later want to set this based on a non-standard
+ // param in RTCConfiguration.
+ mAllowOldSetParameters = Preferences::GetBool(
+ "media.peerconnection.allow_old_setParameters", false);
+
+ // setup the stun local addresses IPC async call
+ InitLocalAddrs();
+
+ mSignalHandler = MakeUnique<SignalHandler>(this, mTransportHandler.get());
+
+ PeerConnectionCtx::GetInstance()->AddPeerConnection(mHandle, this);
+
+ return NS_OK;
+}
+
+void PeerConnectionImpl::Initialize(PeerConnectionObserver& aObserver,
+ nsGlobalWindowInner& aWindow,
+ ErrorResult& rv) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsresult res = Initialize(aObserver, &aWindow);
+ if (NS_FAILED(res)) {
+ rv.Throw(res);
+ return;
+ }
+}
+
+void PeerConnectionImpl::SetCertificate(
+ mozilla::dom::RTCCertificate& aCertificate) {
+ PC_AUTO_ENTER_API_CALL_NO_CHECK();
+ MOZ_ASSERT(!mCertificate, "This can only be called once");
+ mCertificate = &aCertificate;
+
+ std::vector<uint8_t> fingerprint;
+ nsresult rv =
+ CalculateFingerprint(DtlsIdentity::DEFAULT_HASH_ALGORITHM, &fingerprint);
+ if (NS_FAILED(rv)) {
+ CSFLogError(LOGTAG, "%s: Couldn't calculate fingerprint, rv=%u",
+ __FUNCTION__, static_cast<unsigned>(rv));
+ mCertificate = nullptr;
+ return;
+ }
+ rv = mJsepSession->AddDtlsFingerprint(DtlsIdentity::DEFAULT_HASH_ALGORITHM,
+ fingerprint);
+ if (NS_FAILED(rv)) {
+ CSFLogError(LOGTAG, "%s: Couldn't set DTLS credentials, rv=%u",
+ __FUNCTION__, static_cast<unsigned>(rv));
+ mCertificate = nullptr;
+ }
+
+ if (mUncommittedJsepSession) {
+ Unused << mUncommittedJsepSession->AddDtlsFingerprint(
+ DtlsIdentity::DEFAULT_HASH_ALGORITHM, fingerprint);
+ }
+}
+
+const RefPtr<mozilla::dom::RTCCertificate>& PeerConnectionImpl::Certificate()
+ const {
+ PC_AUTO_ENTER_API_CALL_NO_CHECK();
+ return mCertificate;
+}
+
+RefPtr<DtlsIdentity> PeerConnectionImpl::Identity() const {
+ PC_AUTO_ENTER_API_CALL_NO_CHECK();
+ MOZ_ASSERT(mCertificate);
+ return mCertificate->CreateDtlsIdentity();
+}
+
+class CompareCodecPriority {
+ public:
+ void SetPreferredCodec(int32_t preferredCodec) {
+ // This pref really ought to be a string, preferably something like
+ // "H264" or "VP8" instead of a payload type.
+ // Bug 1101259.
+ std::ostringstream os;
+ os << preferredCodec;
+ mPreferredCodec = os.str();
+ }
+
+ bool operator()(const UniquePtr<JsepCodecDescription>& lhs,
+ const UniquePtr<JsepCodecDescription>& rhs) const {
+ if (!mPreferredCodec.empty() && lhs->mDefaultPt == mPreferredCodec &&
+ rhs->mDefaultPt != mPreferredCodec) {
+ return true;
+ }
+
+ if (lhs->mStronglyPreferred && !rhs->mStronglyPreferred) {
+ return true;
+ }
+
+ return false;
+ }
+
+ private:
+ std::string mPreferredCodec;
+};
+
+class ConfigureCodec {
+ public:
+ explicit ConfigureCodec(nsCOMPtr<nsIPrefBranch>& branch)
+ : mHardwareH264Enabled(false),
+ mSoftwareH264Enabled(false),
+ mH264Enabled(false),
+ mVP9Enabled(true),
+ mVP9Preferred(false),
+ mH264Level(13), // minimum suggested for WebRTC spec
+ mH264MaxBr(0), // Unlimited
+ mH264MaxMbps(0), // Unlimited
+ mVP8MaxFs(0),
+ mVP8MaxFr(0),
+ mUseTmmbr(false),
+ mUseRemb(false),
+ mUseTransportCC(false),
+ mUseAudioFec(false),
+ mRedUlpfecEnabled(false),
+ mDtmfEnabled(false) {
+ mSoftwareH264Enabled = PeerConnectionCtx::GetInstance()->gmpHasH264();
+
+ if (WebrtcVideoConduit::HasH264Hardware()) {
+ Telemetry::Accumulate(Telemetry::WEBRTC_HAS_H264_HARDWARE, true);
+ branch->GetBoolPref("media.webrtc.hw.h264.enabled",
+ &mHardwareH264Enabled);
+ }
+
+ mH264Enabled = mHardwareH264Enabled || mSoftwareH264Enabled;
+ Telemetry::Accumulate(Telemetry::WEBRTC_SOFTWARE_H264_ENABLED,
+ mSoftwareH264Enabled);
+ Telemetry::Accumulate(Telemetry::WEBRTC_HARDWARE_H264_ENABLED,
+ mHardwareH264Enabled);
+ Telemetry::Accumulate(Telemetry::WEBRTC_H264_ENABLED, mH264Enabled);
+
+ branch->GetIntPref("media.navigator.video.h264.level", &mH264Level);
+ mH264Level &= 0xFF;
+
+ branch->GetIntPref("media.navigator.video.h264.max_br", &mH264MaxBr);
+
+ branch->GetIntPref("media.navigator.video.h264.max_mbps", &mH264MaxMbps);
+
+ branch->GetBoolPref("media.peerconnection.video.vp9_enabled", &mVP9Enabled);
+
+ branch->GetBoolPref("media.peerconnection.video.vp9_preferred",
+ &mVP9Preferred);
+
+ branch->GetIntPref("media.navigator.video.max_fs", &mVP8MaxFs);
+ if (mVP8MaxFs <= 0) {
+ mVP8MaxFs = 12288; // We must specify something other than 0
+ }
+
+ branch->GetIntPref("media.navigator.video.max_fr", &mVP8MaxFr);
+ if (mVP8MaxFr <= 0) {
+ mVP8MaxFr = 60; // We must specify something other than 0
+ }
+
+ // TMMBR is enabled from a pref in about:config
+ branch->GetBoolPref("media.navigator.video.use_tmmbr", &mUseTmmbr);
+
+ // REMB is enabled by default, but can be disabled from about:config
+ branch->GetBoolPref("media.navigator.video.use_remb", &mUseRemb);
+
+ branch->GetBoolPref("media.navigator.video.use_transport_cc",
+ &mUseTransportCC);
+
+ branch->GetBoolPref("media.navigator.audio.use_fec", &mUseAudioFec);
+
+ branch->GetBoolPref("media.navigator.video.red_ulpfec_enabled",
+ &mRedUlpfecEnabled);
+
+ // media.peerconnection.dtmf.enabled controls both sdp generation for
+ // DTMF support as well as DTMF exposure to DOM
+ branch->GetBoolPref("media.peerconnection.dtmf.enabled", &mDtmfEnabled);
+ }
+
+ void operator()(UniquePtr<JsepCodecDescription>& codec) const {
+ switch (codec->Type()) {
+ case SdpMediaSection::kAudio: {
+ JsepAudioCodecDescription& audioCodec =
+ static_cast<JsepAudioCodecDescription&>(*codec);
+ if (audioCodec.mName == "opus") {
+ audioCodec.mFECEnabled = mUseAudioFec;
+ } else if (audioCodec.mName == "telephone-event") {
+ audioCodec.mEnabled = mDtmfEnabled;
+ }
+ } break;
+ case SdpMediaSection::kVideo: {
+ JsepVideoCodecDescription& videoCodec =
+ static_cast<JsepVideoCodecDescription&>(*codec);
+
+ if (videoCodec.mName == "H264") {
+ // Override level
+ videoCodec.mProfileLevelId &= 0xFFFF00;
+ videoCodec.mProfileLevelId |= mH264Level;
+
+ videoCodec.mConstraints.maxBr = mH264MaxBr;
+
+ videoCodec.mConstraints.maxMbps = mH264MaxMbps;
+
+ // Might disable it, but we set up other params anyway
+ videoCodec.mEnabled = mH264Enabled;
+
+ if (videoCodec.mPacketizationMode == 0 && !mSoftwareH264Enabled) {
+ // We're assuming packetization mode 0 is unsupported by
+ // hardware.
+ videoCodec.mEnabled = false;
+ }
+
+ if (mHardwareH264Enabled) {
+ videoCodec.mStronglyPreferred = true;
+ }
+ } else if (videoCodec.mName == "red") {
+ videoCodec.mEnabled = mRedUlpfecEnabled;
+ } else if (videoCodec.mName == "ulpfec") {
+ videoCodec.mEnabled = mRedUlpfecEnabled;
+ } else if (videoCodec.mName == "VP8" || videoCodec.mName == "VP9") {
+ if (videoCodec.mName == "VP9") {
+ if (!mVP9Enabled) {
+ videoCodec.mEnabled = false;
+ break;
+ }
+ if (mVP9Preferred) {
+ videoCodec.mStronglyPreferred = true;
+ }
+ }
+ videoCodec.mConstraints.maxFs = mVP8MaxFs;
+ videoCodec.mConstraints.maxFps = Some(mVP8MaxFr);
+ }
+
+ if (mUseTmmbr) {
+ videoCodec.EnableTmmbr();
+ }
+ if (mUseRemb) {
+ videoCodec.EnableRemb();
+ }
+ if (mUseTransportCC) {
+ videoCodec.EnableTransportCC();
+ }
+ } break;
+ case SdpMediaSection::kText:
+ case SdpMediaSection::kApplication:
+ case SdpMediaSection::kMessage: {
+ } // Nothing to configure for these.
+ }
+ }
+
+ private:
+ bool mHardwareH264Enabled;
+ bool mSoftwareH264Enabled;
+ bool mH264Enabled;
+ bool mVP9Enabled;
+ bool mVP9Preferred;
+ int32_t mH264Level;
+ int32_t mH264MaxBr;
+ int32_t mH264MaxMbps;
+ int32_t mVP8MaxFs;
+ int32_t mVP8MaxFr;
+ bool mUseTmmbr;
+ bool mUseRemb;
+ bool mUseTransportCC;
+ bool mUseAudioFec;
+ bool mRedUlpfecEnabled;
+ bool mDtmfEnabled;
+};
+
+class ConfigureRedCodec {
+ public:
+ explicit ConfigureRedCodec(nsCOMPtr<nsIPrefBranch>& branch,
+ std::vector<uint8_t>* redundantEncodings)
+ : mRedundantEncodings(redundantEncodings) {
+ // if we wanted to override or modify which encodings are considered
+ // for redundant encodings, we'd probably want to handle it here by
+ // checking prefs modifying the operator() code below
+ }
+
+ void operator()(UniquePtr<JsepCodecDescription>& codec) const {
+ if (codec->Type() == SdpMediaSection::kVideo && !codec->mEnabled) {
+ uint8_t pt = (uint8_t)strtoul(codec->mDefaultPt.c_str(), nullptr, 10);
+ // don't search for the codec payload type unless we have a valid
+ // conversion (non-zero)
+ if (pt != 0) {
+ std::vector<uint8_t>::iterator it = std::find(
+ mRedundantEncodings->begin(), mRedundantEncodings->end(), pt);
+ if (it != mRedundantEncodings->end()) {
+ mRedundantEncodings->erase(it);
+ }
+ }
+ }
+ }
+
+ private:
+ std::vector<uint8_t>* mRedundantEncodings;
+};
+
+nsresult PeerConnectionImpl::ConfigureJsepSessionCodecs() {
+ nsresult res;
+ nsCOMPtr<nsIPrefService> prefs =
+ do_GetService("@mozilla.org/preferences-service;1", &res);
+
+ if (NS_FAILED(res)) {
+ CSFLogError(LOGTAG, "%s: Couldn't get prefs service, res=%u", __FUNCTION__,
+ static_cast<unsigned>(res));
+ return res;
+ }
+
+ nsCOMPtr<nsIPrefBranch> branch = do_QueryInterface(prefs);
+ if (!branch) {
+ CSFLogError(LOGTAG, "%s: Couldn't get prefs branch", __FUNCTION__);
+ return NS_ERROR_FAILURE;
+ }
+
+ ConfigureCodec configurer(branch);
+ mJsepSession->ForEachCodec(configurer);
+
+ // if red codec is enabled, configure it for the other enabled codecs
+ for (auto& codec : mJsepSession->Codecs()) {
+ if (codec->mName == "red" && codec->mEnabled) {
+ JsepVideoCodecDescription* redCodec =
+ static_cast<JsepVideoCodecDescription*>(codec.get());
+ ConfigureRedCodec configureRed(branch, &(redCodec->mRedundantEncodings));
+ mJsepSession->ForEachCodec(configureRed);
+ break;
+ }
+ }
+
+ // We use this to sort the list of codecs once everything is configured
+ CompareCodecPriority comparator;
+
+ // Sort by priority
+ int32_t preferredCodec = 0;
+ branch->GetIntPref("media.navigator.video.preferred_codec", &preferredCodec);
+
+ if (preferredCodec) {
+ comparator.SetPreferredCodec(preferredCodec);
+ }
+
+ mJsepSession->SortCodecs(comparator);
+ return NS_OK;
+}
+
+// Data channels won't work without a window, so in order for the C++ unit
+// tests to work (it doesn't have a window available) we ifdef the following
+// two implementations.
+//
+// Note: 'media.peerconnection.sctp.force_maximum_message_size' changes
+// behaviour triggered by these parameters.
+NS_IMETHODIMP
+PeerConnectionImpl::EnsureDataConnection(uint16_t aLocalPort,
+ uint16_t aNumstreams,
+ uint32_t aMaxMessageSize,
+ bool aMMSSet) {
+ PC_AUTO_ENTER_API_CALL(false);
+
+ if (mDataConnection) {
+ CSFLogDebug(LOGTAG, "%s DataConnection already connected", __FUNCTION__);
+ mDataConnection->SetMaxMessageSize(aMMSSet, aMaxMessageSize);
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsISerialEventTarget> target =
+ mWindow ? mWindow->EventTargetFor(TaskCategory::Other) : nullptr;
+ Maybe<uint64_t> mms = aMMSSet ? Some(aMaxMessageSize) : Nothing();
+ if (auto res = DataChannelConnection::Create(this, target, mTransportHandler,
+ aLocalPort, aNumstreams, mms)) {
+ mDataConnection = res.value();
+ CSFLogDebug(LOGTAG, "%s DataChannelConnection %p attached to %s",
+ __FUNCTION__, (void*)mDataConnection.get(), mHandle.c_str());
+ return NS_OK;
+ }
+ CSFLogError(LOGTAG, "%s DataConnection Create Failed", __FUNCTION__);
+ return NS_ERROR_FAILURE;
+}
+
+nsresult PeerConnectionImpl::GetDatachannelParameters(
+ uint32_t* channels, uint16_t* localport, uint16_t* remoteport,
+ uint32_t* remotemaxmessagesize, bool* mmsset, std::string* transportId,
+ bool* client) const {
+ // Clear, just in case we fail.
+ *channels = 0;
+ *localport = 0;
+ *remoteport = 0;
+ *remotemaxmessagesize = 0;
+ *mmsset = false;
+ transportId->clear();
+
+ Maybe<const JsepTransceiver> datachannelTransceiver =
+ mJsepSession->FindTransceiver([](const JsepTransceiver& aTransceiver) {
+ return aTransceiver.GetMediaType() == SdpMediaSection::kApplication;
+ });
+
+ if (!datachannelTransceiver ||
+ !datachannelTransceiver->mTransport.mComponents ||
+ !datachannelTransceiver->mSendTrack.GetNegotiatedDetails()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // This will release assert if there is no such index, and that's ok
+ const JsepTrackEncoding& encoding =
+ datachannelTransceiver->mSendTrack.GetNegotiatedDetails()->GetEncoding(0);
+
+ if (NS_WARN_IF(encoding.GetCodecs().empty())) {
+ CSFLogError(LOGTAG,
+ "%s: Negotiated m=application with no codec. "
+ "This is likely to be broken.",
+ __FUNCTION__);
+ return NS_ERROR_FAILURE;
+ }
+
+ for (const auto& codec : encoding.GetCodecs()) {
+ if (codec->Type() != SdpMediaSection::kApplication) {
+ CSFLogError(LOGTAG,
+ "%s: Codec type for m=application was %u, this "
+ "is a bug.",
+ __FUNCTION__, static_cast<unsigned>(codec->Type()));
+ MOZ_ASSERT(false, "Codec for m=application was not \"application\"");
+ return NS_ERROR_FAILURE;
+ }
+
+ if (codec->mName != "webrtc-datachannel") {
+ CSFLogWarn(LOGTAG,
+ "%s: Codec for m=application was not "
+ "webrtc-datachannel (was instead %s). ",
+ __FUNCTION__, codec->mName.c_str());
+ continue;
+ }
+
+ if (codec->mChannels) {
+ *channels = codec->mChannels;
+ } else {
+ *channels = WEBRTC_DATACHANNEL_STREAMS_DEFAULT;
+ }
+ const JsepApplicationCodecDescription* appCodec =
+ static_cast<const JsepApplicationCodecDescription*>(codec.get());
+ *localport = appCodec->mLocalPort;
+ *remoteport = appCodec->mRemotePort;
+ *remotemaxmessagesize = appCodec->mRemoteMaxMessageSize;
+ *mmsset = appCodec->mRemoteMMSSet;
+ MOZ_ASSERT(!datachannelTransceiver->mTransport.mTransportId.empty());
+ *transportId = datachannelTransceiver->mTransport.mTransportId;
+ *client = datachannelTransceiver->mTransport.mDtls->GetRole() ==
+ JsepDtlsTransport::kJsepDtlsClient;
+ return NS_OK;
+ }
+ return NS_ERROR_FAILURE;
+}
+
+nsresult PeerConnectionImpl::AddRtpTransceiverToJsepSession(
+ JsepTransceiver& transceiver) {
+ nsresult res = ConfigureJsepSessionCodecs();
+ if (NS_FAILED(res)) {
+ CSFLogError(LOGTAG, "Failed to configure codecs");
+ return res;
+ }
+
+ mJsepSession->AddTransceiver(transceiver);
+ return NS_OK;
+}
+
+static Maybe<SdpMediaSection::MediaType> ToSdpMediaType(
+ const nsAString& aKind) {
+ if (aKind.EqualsASCII("audio")) {
+ return Some(SdpMediaSection::MediaType::kAudio);
+ } else if (aKind.EqualsASCII("video")) {
+ return Some(SdpMediaSection::MediaType::kVideo);
+ }
+ return Nothing();
+}
+
+already_AddRefed<RTCRtpTransceiver> PeerConnectionImpl::AddTransceiver(
+ const dom::RTCRtpTransceiverInit& aInit, const nsAString& aKind,
+ dom::MediaStreamTrack* aSendTrack, bool aAddTrackMagic, ErrorResult& aRv) {
+ // Copy, because we might need to modify
+ RTCRtpTransceiverInit init(aInit);
+
+ Maybe<SdpMediaSection::MediaType> type = ToSdpMediaType(aKind);
+ if (NS_WARN_IF(!type.isSome())) {
+ MOZ_ASSERT(false, "Invalid media kind");
+ aRv = NS_ERROR_INVALID_ARG;
+ return nullptr;
+ }
+
+ JsepTransceiver jsepTransceiver(*type, *mUuidGen);
+ jsepTransceiver.SetRtxIsAllowed(mRtxIsAllowed);
+
+ // Do this last, since it is not possible to roll back.
+ nsresult rv = AddRtpTransceiverToJsepSession(jsepTransceiver);
+ if (NS_FAILED(rv)) {
+ CSFLogError(LOGTAG, "%s: AddRtpTransceiverToJsepSession failed, res=%u",
+ __FUNCTION__, static_cast<unsigned>(rv));
+ aRv = rv;
+ return nullptr;
+ }
+
+ auto& sendEncodings = init.mSendEncodings;
+
+ // CheckAndRectifyEncodings covers these six:
+ // If any encoding contains a rid member whose value does not conform to the
+ // grammar requirements specified in Section 10 of [RFC8851], throw a
+ // TypeError.
+
+ // If some but not all encodings contain a rid member, throw a TypeError.
+
+ // If any encoding contains a rid member whose value is the same as that of a
+ // rid contained in another encoding in sendEncodings, throw a TypeError.
+
+ // If kind is "audio", remove the scaleResolutionDownBy member from all
+ // encodings that contain one.
+
+ // If any encoding contains a scaleResolutionDownBy member whose value is
+ // less than 1.0, throw a RangeError.
+
+ // Verify that the value of each maxFramerate member in sendEncodings that is
+ // defined is greater than 0.0. If one of the maxFramerate values does not
+ // meet this requirement, throw a RangeError.
+ RTCRtpSender::CheckAndRectifyEncodings(sendEncodings,
+ *type == SdpMediaSection::kVideo, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ // If any encoding contains a read-only parameter other than rid, throw an
+ // InvalidAccessError.
+ // NOTE: We don't support any additional read-only params right now. Also,
+ // spec shoehorns this in between checks that setParameters also performs
+ // (between the rid checks and the scaleResolutionDownBy checks).
+
+ // If any encoding contains a scaleResolutionDownBy member, then for each
+ // encoding without one, add a scaleResolutionDownBy member with the value
+ // 1.0.
+ for (const auto& constEncoding : sendEncodings) {
+ if (constEncoding.mScaleResolutionDownBy.WasPassed()) {
+ for (auto& encoding : sendEncodings) {
+ if (!encoding.mScaleResolutionDownBy.WasPassed()) {
+ encoding.mScaleResolutionDownBy.Construct(1.0f);
+ }
+ }
+ break;
+ }
+ }
+
+ // Let maxN be the maximum number of total simultaneous encodings the user
+ // agent may support for this kind, at minimum 1.This should be an optimistic
+ // number since the codec to be used is not known yet.
+ size_t maxN =
+ (*type == SdpMediaSection::kVideo) ? webrtc::kMaxSimulcastStreams : 1;
+
+ // If the number of encodings stored in sendEncodings exceeds maxN, then trim
+ // sendEncodings from the tail until its length is maxN.
+ // NOTE: Spec has this after all validation steps; even if there are elements
+ // that we will trim off, we still validate them.
+ if (sendEncodings.Length() > maxN) {
+ sendEncodings.TruncateLength(maxN);
+ }
+
+ // If kind is "video" and none of the encodings contain a
+ // scaleResolutionDownBy member, then for each encoding, add a
+ // scaleResolutionDownBy member with the value 2^(length of sendEncodings -
+ // encoding index - 1). This results in smaller-to-larger resolutions where
+ // the last encoding has no scaling applied to it, e.g. 4:2:1 if the length
+ // is 3.
+ // NOTE: The code above ensures that these are all set, or all unset, so we
+ // can just check the first one.
+ if (sendEncodings.Length() && *type == SdpMediaSection::kVideo &&
+ !sendEncodings[0].mScaleResolutionDownBy.WasPassed()) {
+ double scale = 1.0f;
+ for (auto it = sendEncodings.rbegin(); it != sendEncodings.rend(); ++it) {
+ it->mScaleResolutionDownBy.Construct(scale);
+ scale *= 2;
+ }
+ }
+
+ // If the number of encodings now stored in sendEncodings is 1, then remove
+ // any rid member from the lone entry.
+ if (sendEncodings.Length() == 1) {
+ sendEncodings[0].mRid.Reset();
+ }
+
+ RefPtr<RTCRtpTransceiver> transceiver = CreateTransceiver(
+ jsepTransceiver.GetUuid(),
+ jsepTransceiver.GetMediaType() == SdpMediaSection::kVideo, init,
+ aSendTrack, aAddTrackMagic, aRv);
+
+ if (aRv.Failed()) {
+ // Would be nice if we could peek at the rv without stealing it, so we
+ // could log...
+ CSFLogError(LOGTAG, "%s: failed", __FUNCTION__);
+ return nullptr;
+ }
+
+ mTransceivers.AppendElement(transceiver);
+ return transceiver.forget();
+}
+
+bool PeerConnectionImpl::CheckNegotiationNeeded() {
+ MOZ_ASSERT(mSignalingState == RTCSignalingState::Stable);
+ SyncToJsep();
+ return !mLocalIceCredentialsToReplace.empty() ||
+ mJsepSession->CheckNegotiationNeeded();
+}
+
+bool PeerConnectionImpl::CreatedSender(const dom::RTCRtpSender& aSender) const {
+ return aSender.IsMyPc(this);
+}
+
+nsresult PeerConnectionImpl::InitializeDataChannel() {
+ PC_AUTO_ENTER_API_CALL(false);
+ CSFLogDebug(LOGTAG, "%s", __FUNCTION__);
+
+ uint32_t channels = 0;
+ uint16_t localport = 0;
+ uint16_t remoteport = 0;
+ uint32_t remotemaxmessagesize = 0;
+ bool mmsset = false;
+ std::string transportId;
+ bool client = false;
+ nsresult rv = GetDatachannelParameters(&channels, &localport, &remoteport,
+ &remotemaxmessagesize, &mmsset,
+ &transportId, &client);
+
+ if (NS_FAILED(rv)) {
+ CSFLogDebug(LOGTAG, "%s: We did not negotiate datachannel", __FUNCTION__);
+ return NS_OK;
+ }
+
+ if (channels > MAX_NUM_STREAMS) {
+ channels = MAX_NUM_STREAMS;
+ }
+
+ rv = EnsureDataConnection(localport, channels, remotemaxmessagesize, mmsset);
+ if (NS_SUCCEEDED(rv)) {
+ if (mDataConnection->ConnectToTransport(transportId, client, localport,
+ remoteport)) {
+ return NS_OK;
+ }
+ // If we inited the DataConnection, call Destroy() before releasing it
+ mDataConnection->Destroy();
+ }
+ mDataConnection = nullptr;
+ return NS_ERROR_FAILURE;
+}
+
+already_AddRefed<nsDOMDataChannel> PeerConnectionImpl::CreateDataChannel(
+ const nsAString& aLabel, const nsAString& aProtocol, uint16_t aType,
+ bool ordered, uint16_t aMaxTime, uint16_t aMaxNum, bool aExternalNegotiated,
+ uint16_t aStream, ErrorResult& rv) {
+ RefPtr<nsDOMDataChannel> result;
+ rv = CreateDataChannel(aLabel, aProtocol, aType, ordered, aMaxTime, aMaxNum,
+ aExternalNegotiated, aStream, getter_AddRefs(result));
+ return result.forget();
+}
+
+NS_IMETHODIMP
+PeerConnectionImpl::CreateDataChannel(
+ const nsAString& aLabel, const nsAString& aProtocol, uint16_t aType,
+ bool ordered, uint16_t aMaxTime, uint16_t aMaxNum, bool aExternalNegotiated,
+ uint16_t aStream, nsDOMDataChannel** aRetval) {
+ PC_AUTO_ENTER_API_CALL(false);
+ MOZ_ASSERT(aRetval);
+
+ RefPtr<DataChannel> dataChannel;
+ DataChannelConnection::Type theType =
+ static_cast<DataChannelConnection::Type>(aType);
+
+ nsresult rv = EnsureDataConnection(
+ WEBRTC_DATACHANNEL_PORT_DEFAULT, WEBRTC_DATACHANNEL_STREAMS_DEFAULT,
+ WEBRTC_DATACHANNEL_MAX_MESSAGE_SIZE_REMOTE_DEFAULT, false);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ dataChannel = mDataConnection->Open(
+ NS_ConvertUTF16toUTF8(aLabel), NS_ConvertUTF16toUTF8(aProtocol), theType,
+ ordered,
+ aType == DataChannelConnection::PARTIAL_RELIABLE_REXMIT
+ ? aMaxNum
+ : (aType == DataChannelConnection::PARTIAL_RELIABLE_TIMED ? aMaxTime
+ : 0),
+ nullptr, nullptr, aExternalNegotiated, aStream);
+ NS_ENSURE_TRUE(dataChannel, NS_ERROR_NOT_AVAILABLE);
+
+ CSFLogDebug(LOGTAG, "%s: making DOMDataChannel", __FUNCTION__);
+
+ Maybe<JsepTransceiver> dcTransceiver =
+ mJsepSession->FindTransceiver([](const JsepTransceiver& aTransceiver) {
+ return aTransceiver.GetMediaType() == SdpMediaSection::kApplication;
+ });
+
+ if (dcTransceiver) {
+ dcTransceiver->RestartDatachannelTransceiver();
+ mJsepSession->SetTransceiver(*dcTransceiver);
+ } else {
+ mJsepSession->AddTransceiver(
+ JsepTransceiver(SdpMediaSection::MediaType::kApplication, *mUuidGen));
+ }
+
+ RefPtr<nsDOMDataChannel> retval;
+ rv = NS_NewDOMDataChannel(dataChannel.forget(), mWindow,
+ getter_AddRefs(retval));
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ retval.forget(aRetval);
+ return NS_OK;
+}
+
+NS_IMPL_CYCLE_COLLECTION(PeerConnectionImpl::Operation, mPromise, mPc)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PeerConnectionImpl::Operation)
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+NS_IMPL_CYCLE_COLLECTING_ADDREF(PeerConnectionImpl::Operation)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(PeerConnectionImpl::Operation)
+
+PeerConnectionImpl::Operation::Operation(PeerConnectionImpl* aPc,
+ ErrorResult& aError)
+ : mPromise(aPc->MakePromise(aError)), mPc(aPc) {}
+
+PeerConnectionImpl::Operation::~Operation() = default;
+
+void PeerConnectionImpl::Operation::Call(ErrorResult& aError) {
+ RefPtr<dom::Promise> opPromise = CallImpl(aError);
+ if (aError.Failed()) {
+ return;
+ }
+ // Upon fulfillment or rejection of the promise returned by the operation,
+ // run the following steps:
+ // (NOTE: mPromise is p from https://w3c.github.io/webrtc-pc/#dfn-chain,
+ // and CallImpl() is what returns the promise for the operation itself)
+ opPromise->AppendNativeHandler(this);
+}
+
+void PeerConnectionImpl::Operation::ResolvedCallback(
+ JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv) {
+ // If connection.[[IsClosed]] is true, abort these steps.
+ // (the spec wants p to never settle in this event)
+ if (!mPc->IsClosed()) {
+ // If the promise returned by operation was fulfilled with a
+ // value, fulfill p with that value.
+ mPromise->MaybeResolveWithClone(aCx, aValue);
+ // Upon fulfillment or rejection of p, execute the following
+ // steps:
+ // (Static analysis forces us to use a temporary)
+ RefPtr<PeerConnectionImpl> pc = mPc;
+ pc->RunNextOperation(aRv);
+ }
+}
+
+void PeerConnectionImpl::Operation::RejectedCallback(
+ JSContext* aCx, JS::Handle<JS::Value> aValue, ErrorResult& aRv) {
+ // If connection.[[IsClosed]] is true, abort these steps.
+ // (the spec wants p to never settle in this event)
+ if (!mPc->IsClosed()) {
+ // If the promise returned by operation was rejected with a
+ // value, reject p with that value.
+ mPromise->MaybeRejectWithClone(aCx, aValue);
+ // Upon fulfillment or rejection of p, execute the following
+ // steps:
+ // (Static analysis forces us to use a temporary)
+ RefPtr<PeerConnectionImpl> pc = mPc;
+ pc->RunNextOperation(aRv);
+ }
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(PeerConnectionImpl::JSOperation,
+ PeerConnectionImpl::Operation, mOperation)
+
+NS_IMPL_ADDREF_INHERITED(PeerConnectionImpl::JSOperation,
+ PeerConnectionImpl::Operation)
+NS_IMPL_RELEASE_INHERITED(PeerConnectionImpl::JSOperation,
+ PeerConnectionImpl::Operation)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(PeerConnectionImpl::JSOperation)
+NS_INTERFACE_MAP_END_INHERITING(PeerConnectionImpl::Operation)
+
+PeerConnectionImpl::JSOperation::JSOperation(PeerConnectionImpl* aPc,
+ dom::ChainedOperation& aOp,
+ ErrorResult& aError)
+ : Operation(aPc, aError), mOperation(&aOp) {}
+
+RefPtr<dom::Promise> PeerConnectionImpl::JSOperation::CallImpl(
+ ErrorResult& aError) {
+ // Static analysis will not let us call this without a temporary :(
+ RefPtr<dom::ChainedOperation> op = mOperation;
+ return op->Call(aError);
+}
+
+already_AddRefed<dom::Promise> PeerConnectionImpl::Chain(
+ dom::ChainedOperation& aOperation, ErrorResult& aError) {
+ MOZ_RELEASE_ASSERT(!mChainingOperation);
+ mChainingOperation = true;
+ RefPtr<Operation> operation = new JSOperation(this, aOperation, aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+ RefPtr<Promise> promise = Chain(operation, aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+ mChainingOperation = false;
+ return promise.forget();
+}
+
+// This is kinda complicated, but it is what the spec requires us to do. The
+// core of what makes this complicated is the requirement that |aOperation| be
+// run _immediately_ (without any Promise.Then!) if the operations chain is
+// empty.
+already_AddRefed<dom::Promise> PeerConnectionImpl::Chain(
+ const RefPtr<Operation>& aOperation, ErrorResult& aError) {
+ // If connection.[[IsClosed]] is true, return a promise rejected with a newly
+ // created InvalidStateError.
+ if (IsClosed()) {
+ CSFLogDebug(LOGTAG, "%s:%d: Peer connection is closed", __FILE__, __LINE__);
+ RefPtr<dom::Promise> error = MakePromise(aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+ error->MaybeRejectWithInvalidStateError("Peer connection is closed");
+ return error.forget();
+ }
+
+ // Append operation to [[Operations]].
+ mOperations.AppendElement(aOperation);
+
+ // If the length of [[Operations]] is exactly 1, execute operation.
+ if (mOperations.Length() == 1) {
+ aOperation->Call(aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+ }
+
+ // This is the promise p from https://w3c.github.io/webrtc-pc/#dfn-chain
+ return do_AddRef(aOperation->GetPromise());
+}
+
+void PeerConnectionImpl::RunNextOperation(ErrorResult& aError) {
+ // If connection.[[IsClosed]] is true, abort these steps.
+ if (IsClosed()) {
+ return;
+ }
+
+ // Remove the first element of [[Operations]].
+ mOperations.RemoveElementAt(0);
+
+ // If [[Operations]] is non-empty, execute the operation represented by the
+ // first element of [[Operations]], and abort these steps.
+ if (mOperations.Length()) {
+ // Cannot call without a temporary :(
+ RefPtr<Operation> op = mOperations[0];
+ op->Call(aError);
+ return;
+ }
+
+ // If connection.[[UpdateNegotiationNeededFlagOnEmptyChain]] is false, abort
+ // these steps.
+ if (!mUpdateNegotiationNeededFlagOnEmptyChain) {
+ return;
+ }
+
+ // Set connection.[[UpdateNegotiationNeededFlagOnEmptyChain]] to false.
+ mUpdateNegotiationNeededFlagOnEmptyChain = false;
+ // Update the negotiation-needed flag for connection.
+ UpdateNegotiationNeeded();
+}
+
+void PeerConnectionImpl::SyncToJsep() {
+ for (const auto& transceiver : mTransceivers) {
+ transceiver->SyncToJsep(*mJsepSession);
+ }
+}
+
+void PeerConnectionImpl::SyncFromJsep() {
+ CSFLogDebug(LOGTAG, "%s", __FUNCTION__);
+ mJsepSession->ForEachTransceiver(
+ [this, self = RefPtr<PeerConnectionImpl>(this)](
+ const JsepTransceiver& jsepTransceiver) {
+ if (jsepTransceiver.GetMediaType() ==
+ SdpMediaSection::MediaType::kApplication) {
+ return;
+ }
+
+ CSFLogDebug(LOGTAG, "%s: Looking for match", __FUNCTION__);
+ RefPtr<RTCRtpTransceiver> transceiver;
+ for (auto& temp : mTransceivers) {
+ if (temp->GetJsepTransceiverId() == jsepTransceiver.GetUuid()) {
+ CSFLogDebug(LOGTAG, "%s: Found match", __FUNCTION__);
+ transceiver = temp;
+ break;
+ }
+ }
+
+ if (!transceiver) {
+ CSFLogDebug(LOGTAG, "%s: No match, making new", __FUNCTION__);
+ dom::RTCRtpTransceiverInit init;
+ init.mDirection = RTCRtpTransceiverDirection::Recvonly;
+ IgnoredErrorResult rv;
+ transceiver = CreateTransceiver(
+ jsepTransceiver.GetUuid(),
+ jsepTransceiver.GetMediaType() == SdpMediaSection::kVideo, init,
+ nullptr, false, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ MOZ_ASSERT(false);
+ return;
+ }
+ mTransceivers.AppendElement(transceiver);
+ }
+
+ CSFLogDebug(LOGTAG, "%s: Syncing transceiver", __FUNCTION__);
+ transceiver->SyncFromJsep(*mJsepSession);
+ });
+}
+
+already_AddRefed<dom::Promise> PeerConnectionImpl::MakePromise(
+ ErrorResult& aError) const {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mWindow);
+ return dom::Promise::Create(global, aError);
+}
+
+void PeerConnectionImpl::UpdateNegotiationNeeded() {
+ // If the length of connection.[[Operations]] is not 0, then set
+ // connection.[[UpdateNegotiationNeededFlagOnEmptyChain]] to true, and abort
+ // these steps.
+ if (mOperations.Length() != 0) {
+ mUpdateNegotiationNeededFlagOnEmptyChain = true;
+ return;
+ }
+
+ // Queue a task to run the following steps:
+ GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction(
+ __func__, [this, self = RefPtr<PeerConnectionImpl>(this)] {
+ // If connection.[[IsClosed]] is true, abort these steps.
+ if (IsClosed()) {
+ return;
+ }
+ // If the length of connection.[[Operations]] is not 0, then set
+ // connection.[[UpdateNegotiationNeededFlagOnEmptyChain]] to true, and
+ // abort these steps.
+ if (mOperations.Length()) {
+ mUpdateNegotiationNeededFlagOnEmptyChain = true;
+ return;
+ }
+ // If connection's signaling state is not "stable", abort these steps.
+ if (mSignalingState != RTCSignalingState::Stable) {
+ return;
+ }
+ // If the result of checking if negotiation is needed is false, clear
+ // the negotiation-needed flag by setting
+ // connection.[[NegotiationNeeded]] to false, and abort these steps.
+ if (!CheckNegotiationNeeded()) {
+ mNegotiationNeeded = false;
+ return;
+ }
+
+ // If connection.[[NegotiationNeeded]] is already true, abort these
+ // steps.
+ if (mNegotiationNeeded) {
+ return;
+ }
+
+ // Set connection.[[NegotiationNeeded]] to true.
+ mNegotiationNeeded = true;
+
+ // Fire an event named negotiationneeded at connection.
+ ErrorResult rv;
+ mPCObserver->FireNegotiationNeededEvent(rv);
+ }));
+}
+
+void PeerConnectionImpl::NotifyDataChannel(
+ already_AddRefed<DataChannel> aChannel) {
+ PC_AUTO_ENTER_API_CALL_NO_CHECK();
+
+ RefPtr<DataChannel> channel(aChannel);
+ MOZ_ASSERT(channel);
+ CSFLogDebug(LOGTAG, "%s: channel: %p", __FUNCTION__, channel.get());
+
+ RefPtr<nsDOMDataChannel> domchannel;
+ nsresult rv = NS_NewDOMDataChannel(channel.forget(), mWindow,
+ getter_AddRefs(domchannel));
+ NS_ENSURE_SUCCESS_VOID(rv);
+
+ JSErrorResult jrv;
+ mPCObserver->NotifyDataChannel(*domchannel, jrv);
+}
+
+void PeerConnectionImpl::NotifyDataChannelOpen(DataChannel*) {
+ mDataChannelsOpened++;
+}
+
+void PeerConnectionImpl::NotifyDataChannelClosed(DataChannel*) {
+ mDataChannelsClosed++;
+}
+
+void PeerConnectionImpl::NotifySctpConnected() {
+ if (!mSctpTransport) {
+ MOZ_ASSERT(false);
+ return;
+ }
+
+ mSctpTransport->UpdateState(RTCSctpTransportState::Connected);
+}
+
+void PeerConnectionImpl::NotifySctpClosed() {
+ if (!mSctpTransport) {
+ MOZ_ASSERT(false);
+ return;
+ }
+
+ mSctpTransport->UpdateState(RTCSctpTransportState::Closed);
+}
+
+NS_IMETHODIMP
+PeerConnectionImpl::CreateOffer(const RTCOfferOptions& aOptions) {
+ JsepOfferOptions options;
+ // convert the RTCOfferOptions to JsepOfferOptions
+ if (aOptions.mOfferToReceiveAudio.WasPassed()) {
+ options.mOfferToReceiveAudio =
+ mozilla::Some(size_t(aOptions.mOfferToReceiveAudio.Value()));
+ }
+
+ if (aOptions.mOfferToReceiveVideo.WasPassed()) {
+ options.mOfferToReceiveVideo =
+ mozilla::Some(size_t(aOptions.mOfferToReceiveVideo.Value()));
+ }
+
+ options.mIceRestart = mozilla::Some(aOptions.mIceRestart ||
+ !mLocalIceCredentialsToReplace.empty());
+
+ return CreateOffer(options);
+}
+
+static void DeferredCreateOffer(const std::string& aPcHandle,
+ const JsepOfferOptions& aOptions) {
+ PeerConnectionWrapper wrapper(aPcHandle);
+
+ if (wrapper.impl()) {
+ if (!PeerConnectionCtx::GetInstance()->isReady()) {
+ MOZ_CRASH(
+ "Why is DeferredCreateOffer being executed when the "
+ "PeerConnectionCtx isn't ready?");
+ }
+ wrapper.impl()->CreateOffer(aOptions);
+ }
+}
+
+// Have to use unique_ptr because webidl enums are generated without a
+// copy c'tor.
+static std::unique_ptr<dom::PCErrorData> buildJSErrorData(
+ const JsepSession::Result& aResult, const std::string& aMessage) {
+ std::unique_ptr<dom::PCErrorData> result(new dom::PCErrorData);
+ result->mName = *aResult.mError;
+ result->mMessage = NS_ConvertASCIItoUTF16(aMessage.c_str());
+ return result;
+}
+
+// Used by unit tests and the IDL CreateOffer.
+NS_IMETHODIMP
+PeerConnectionImpl::CreateOffer(const JsepOfferOptions& aOptions) {
+ PC_AUTO_ENTER_API_CALL(true);
+
+ if (!PeerConnectionCtx::GetInstance()->isReady()) {
+ // Uh oh. We're not ready yet. Enqueue this operation.
+ PeerConnectionCtx::GetInstance()->queueJSEPOperation(
+ WrapRunnableNM(DeferredCreateOffer, mHandle, aOptions));
+ STAMP_TIMECARD(mTimeCard, "Deferring CreateOffer (not ready)");
+ return NS_OK;
+ }
+
+ CSFLogDebug(LOGTAG, "CreateOffer()");
+
+ nsresult nrv = ConfigureJsepSessionCodecs();
+ if (NS_FAILED(nrv)) {
+ CSFLogError(LOGTAG, "Failed to configure codecs");
+ return nrv;
+ }
+
+ STAMP_TIMECARD(mTimeCard, "Create Offer");
+
+ GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction(
+ __func__, [this, self = RefPtr<PeerConnectionImpl>(this), aOptions] {
+ std::string offer;
+
+ SyncToJsep();
+ UniquePtr<JsepSession> uncommittedJsepSession(mJsepSession->Clone());
+ JsepSession::Result result =
+ uncommittedJsepSession->CreateOffer(aOptions, &offer);
+ JSErrorResult rv;
+ if (result.mError.isSome()) {
+ std::string errorString = uncommittedJsepSession->GetLastError();
+
+ CSFLogError(LOGTAG, "%s: pc = %s, error = %s", __FUNCTION__,
+ mHandle.c_str(), errorString.c_str());
+
+ mPCObserver->OnCreateOfferError(
+ *buildJSErrorData(result, errorString), rv);
+ } else {
+ mJsepSession = std::move(uncommittedJsepSession);
+ mPCObserver->OnCreateOfferSuccess(ObString(offer.c_str()), rv);
+ }
+ }));
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PeerConnectionImpl::CreateAnswer() {
+ PC_AUTO_ENTER_API_CALL(true);
+
+ CSFLogDebug(LOGTAG, "CreateAnswer()");
+
+ STAMP_TIMECARD(mTimeCard, "Create Answer");
+ // TODO(bug 1098015): Once RTCAnswerOptions is standardized, we'll need to
+ // add it as a param to CreateAnswer, and convert it here.
+ JsepAnswerOptions options;
+
+ GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction(
+ __func__, [this, self = RefPtr<PeerConnectionImpl>(this), options] {
+ std::string answer;
+ SyncToJsep();
+ UniquePtr<JsepSession> uncommittedJsepSession(mJsepSession->Clone());
+ JsepSession::Result result =
+ uncommittedJsepSession->CreateAnswer(options, &answer);
+ JSErrorResult rv;
+ if (result.mError.isSome()) {
+ std::string errorString = uncommittedJsepSession->GetLastError();
+
+ CSFLogError(LOGTAG, "%s: pc = %s, error = %s", __FUNCTION__,
+ mHandle.c_str(), errorString.c_str());
+
+ mPCObserver->OnCreateAnswerError(
+ *buildJSErrorData(result, errorString), rv);
+ } else {
+ mJsepSession = std::move(uncommittedJsepSession);
+ mPCObserver->OnCreateAnswerSuccess(ObString(answer.c_str()), rv);
+ }
+ }));
+
+ return NS_OK;
+}
+
+dom::RTCSdpType ToDomSdpType(JsepSdpType aType) {
+ switch (aType) {
+ case kJsepSdpOffer:
+ return dom::RTCSdpType::Offer;
+ case kJsepSdpAnswer:
+ return dom::RTCSdpType::Answer;
+ case kJsepSdpPranswer:
+ return dom::RTCSdpType::Pranswer;
+ case kJsepSdpRollback:
+ return dom::RTCSdpType::Rollback;
+ }
+
+ MOZ_CRASH("Nonexistent JsepSdpType");
+}
+
+JsepSdpType ToJsepSdpType(dom::RTCSdpType aType) {
+ switch (aType) {
+ case dom::RTCSdpType::Offer:
+ return kJsepSdpOffer;
+ case dom::RTCSdpType::Pranswer:
+ return kJsepSdpPranswer;
+ case dom::RTCSdpType::Answer:
+ return kJsepSdpAnswer;
+ case dom::RTCSdpType::Rollback:
+ return kJsepSdpRollback;
+ case dom::RTCSdpType::EndGuard_:;
+ }
+
+ MOZ_CRASH("Nonexistent dom::RTCSdpType");
+}
+
+NS_IMETHODIMP
+PeerConnectionImpl::SetLocalDescription(int32_t aAction, const char* aSDP) {
+ PC_AUTO_ENTER_API_CALL(true);
+
+ if (!aSDP) {
+ CSFLogError(LOGTAG, "%s - aSDP is NULL", __FUNCTION__);
+ return NS_ERROR_FAILURE;
+ }
+
+ STAMP_TIMECARD(mTimeCard, "Set Local Description");
+
+ if (AnyLocalTrackHasPeerIdentity()) {
+ mRequestedPrivacy = Some(PrincipalPrivacy::Private);
+ }
+
+ mozilla::dom::RTCSdpHistoryEntryInternal sdpEntry;
+ sdpEntry.mIsLocal = true;
+ sdpEntry.mTimestamp = mTimestampMaker.GetNow().ToDom();
+ sdpEntry.mSdp = NS_ConvertASCIItoUTF16(aSDP);
+ auto appendHistory = [&]() {
+ if (!mSdpHistory.AppendElement(sdpEntry, fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ };
+
+ mLocalRequestedSDP = aSDP;
+
+ SyncToJsep();
+
+ bool wasRestartingIce = mJsepSession->IsIceRestarting();
+ JsepSdpType sdpType;
+ switch (aAction) {
+ case IPeerConnection::kActionOffer:
+ sdpType = mozilla::kJsepSdpOffer;
+ break;
+ case IPeerConnection::kActionAnswer:
+ sdpType = mozilla::kJsepSdpAnswer;
+ break;
+ case IPeerConnection::kActionPRAnswer:
+ sdpType = mozilla::kJsepSdpPranswer;
+ break;
+ case IPeerConnection::kActionRollback:
+ sdpType = mozilla::kJsepSdpRollback;
+ break;
+ default:
+ MOZ_ASSERT(false);
+ appendHistory();
+ return NS_ERROR_FAILURE;
+ }
+ MOZ_ASSERT(!mUncommittedJsepSession);
+ mUncommittedJsepSession.reset(mJsepSession->Clone());
+ JsepSession::Result result =
+ mUncommittedJsepSession->SetLocalDescription(sdpType, mLocalRequestedSDP);
+ JSErrorResult rv;
+ if (result.mError.isSome()) {
+ std::string errorString = mUncommittedJsepSession->GetLastError();
+ mUncommittedJsepSession = nullptr;
+ CSFLogError(LOGTAG, "%s: pc = %s, error = %s", __FUNCTION__,
+ mHandle.c_str(), errorString.c_str());
+ mPCObserver->OnSetDescriptionError(*buildJSErrorData(result, errorString),
+ rv);
+ sdpEntry.mErrors = GetLastSdpParsingErrors();
+ } else {
+ if (wasRestartingIce) {
+ RecordIceRestartStatistics(sdpType);
+ }
+
+ mPCObserver->OnSetDescriptionSuccess(rv);
+ }
+
+ appendHistory();
+
+ if (rv.Failed()) {
+ return rv.StealNSResult();
+ }
+
+ return NS_OK;
+}
+
+static void DeferredSetRemote(const std::string& aPcHandle, int32_t aAction,
+ const std::string& aSdp) {
+ PeerConnectionWrapper wrapper(aPcHandle);
+
+ if (wrapper.impl()) {
+ if (!PeerConnectionCtx::GetInstance()->isReady()) {
+ MOZ_CRASH(
+ "Why is DeferredSetRemote being executed when the "
+ "PeerConnectionCtx isn't ready?");
+ }
+ wrapper.impl()->SetRemoteDescription(aAction, aSdp.c_str());
+ }
+}
+
+NS_IMETHODIMP
+PeerConnectionImpl::SetRemoteDescription(int32_t action, const char* aSDP) {
+ PC_AUTO_ENTER_API_CALL(true);
+
+ if (!aSDP) {
+ CSFLogError(LOGTAG, "%s - aSDP is NULL", __FUNCTION__);
+ return NS_ERROR_FAILURE;
+ }
+
+ if (action == IPeerConnection::kActionOffer) {
+ if (!PeerConnectionCtx::GetInstance()->isReady()) {
+ // Uh oh. We're not ready yet. Enqueue this operation. (This must be a
+ // remote offer, or else we would not have gotten this far)
+ PeerConnectionCtx::GetInstance()->queueJSEPOperation(WrapRunnableNM(
+ DeferredSetRemote, mHandle, action, std::string(aSDP)));
+ STAMP_TIMECARD(mTimeCard, "Deferring SetRemote (not ready)");
+ return NS_OK;
+ }
+
+ nsresult nrv = ConfigureJsepSessionCodecs();
+ if (NS_FAILED(nrv)) {
+ CSFLogError(LOGTAG, "Failed to configure codecs");
+ return nrv;
+ }
+ }
+
+ STAMP_TIMECARD(mTimeCard, "Set Remote Description");
+
+ mozilla::dom::RTCSdpHistoryEntryInternal sdpEntry;
+ sdpEntry.mIsLocal = false;
+ sdpEntry.mTimestamp = mTimestampMaker.GetNow().ToDom();
+ sdpEntry.mSdp = NS_ConvertASCIItoUTF16(aSDP);
+ auto appendHistory = [&]() {
+ if (!mSdpHistory.AppendElement(sdpEntry, fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ };
+
+ SyncToJsep();
+
+ mRemoteRequestedSDP = aSDP;
+ bool wasRestartingIce = mJsepSession->IsIceRestarting();
+ JsepSdpType sdpType;
+ switch (action) {
+ case IPeerConnection::kActionOffer:
+ sdpType = mozilla::kJsepSdpOffer;
+ break;
+ case IPeerConnection::kActionAnswer:
+ sdpType = mozilla::kJsepSdpAnswer;
+ break;
+ case IPeerConnection::kActionPRAnswer:
+ sdpType = mozilla::kJsepSdpPranswer;
+ break;
+ case IPeerConnection::kActionRollback:
+ sdpType = mozilla::kJsepSdpRollback;
+ break;
+ default:
+ MOZ_ASSERT(false);
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_ASSERT(!mUncommittedJsepSession);
+ mUncommittedJsepSession.reset(mJsepSession->Clone());
+ JsepSession::Result result = mUncommittedJsepSession->SetRemoteDescription(
+ sdpType, mRemoteRequestedSDP);
+ JSErrorResult jrv;
+ if (result.mError.isSome()) {
+ std::string errorString = mUncommittedJsepSession->GetLastError();
+ mUncommittedJsepSession = nullptr;
+ sdpEntry.mErrors = GetLastSdpParsingErrors();
+ CSFLogError(LOGTAG, "%s: pc = %s, error = %s", __FUNCTION__,
+ mHandle.c_str(), errorString.c_str());
+ mPCObserver->OnSetDescriptionError(*buildJSErrorData(result, errorString),
+ jrv);
+ } else {
+ if (wasRestartingIce) {
+ RecordIceRestartStatistics(sdpType);
+ }
+
+ mPCObserver->OnSetDescriptionSuccess(jrv);
+ }
+
+ appendHistory();
+
+ if (jrv.Failed()) {
+ return jrv.StealNSResult();
+ }
+
+ return NS_OK;
+}
+
+already_AddRefed<dom::Promise> PeerConnectionImpl::GetStats(
+ MediaStreamTrack* aSelector) {
+ if (!mWindow) {
+ MOZ_CRASH("Cannot create a promise without a window!");
+ }
+
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mWindow);
+ ErrorResult rv;
+ RefPtr<Promise> promise = Promise::Create(global, rv);
+ if (NS_WARN_IF(rv.Failed())) {
+ MOZ_CRASH("Failed to create a promise!");
+ }
+
+ if (!IsClosed()) {
+ GetStats(aSelector, false)
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [promise, window = mWindow](
+ UniquePtr<dom::RTCStatsReportInternal>&& aReport) {
+ RefPtr<RTCStatsReport> report(new RTCStatsReport(window));
+ report->Incorporate(*aReport);
+ promise->MaybeResolve(std::move(report));
+ },
+ [promise, window = mWindow](nsresult aError) {
+ RefPtr<RTCStatsReport> report(new RTCStatsReport(window));
+ promise->MaybeResolve(std::move(report));
+ });
+ } else {
+ promise->MaybeReject(NS_ERROR_DOM_INVALID_STATE_ERR);
+ }
+
+ return promise.forget();
+}
+
+void PeerConnectionImpl::GetRemoteStreams(
+ nsTArray<RefPtr<DOMMediaStream>>& aStreamsOut) const {
+ aStreamsOut = mReceiveStreams.Clone();
+}
+
+NS_IMETHODIMP
+PeerConnectionImpl::AddIceCandidate(
+ const char* aCandidate, const char* aMid, const char* aUfrag,
+ const dom::Nullable<unsigned short>& aLevel) {
+ PC_AUTO_ENTER_API_CALL(true);
+
+ if (mForceIceTcp &&
+ std::string::npos != std::string(aCandidate).find(" UDP ")) {
+ CSFLogError(LOGTAG, "Blocking remote UDP candidate: %s", aCandidate);
+ return NS_OK;
+ }
+
+ STAMP_TIMECARD(mTimeCard, "Add Ice Candidate");
+
+ CSFLogDebug(LOGTAG, "AddIceCandidate: %s %s", aCandidate, aUfrag);
+
+ std::string transportId;
+ Maybe<unsigned short> level;
+ if (!aLevel.IsNull()) {
+ level = Some(aLevel.Value());
+ }
+ MOZ_DIAGNOSTIC_ASSERT(
+ !mUncommittedJsepSession,
+ "AddIceCandidate is chained, which means it should never "
+ "run while an sRD/sLD is in progress");
+ JsepSession::Result result = mJsepSession->AddRemoteIceCandidate(
+ aCandidate, aMid, level, aUfrag, &transportId);
+
+ if (!result.mError.isSome()) {
+ // We do not bother the MediaTransportHandler about this before
+ // offer/answer concludes. Once offer/answer concludes, we will extract
+ // these candidates from the remote SDP.
+ if (mSignalingState == RTCSignalingState::Stable && !transportId.empty()) {
+ AddIceCandidate(aCandidate, transportId, aUfrag);
+ mRawTrickledCandidates.push_back(aCandidate);
+ }
+ // Spec says we queue a task for these updates
+ GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction(
+ __func__, [this, self = RefPtr<PeerConnectionImpl>(this)] {
+ if (IsClosed()) {
+ return;
+ }
+ mPendingRemoteDescription =
+ mJsepSession->GetRemoteDescription(kJsepDescriptionPending);
+ mCurrentRemoteDescription =
+ mJsepSession->GetRemoteDescription(kJsepDescriptionCurrent);
+ JSErrorResult rv;
+ mPCObserver->OnAddIceCandidateSuccess(rv);
+ }));
+ } else {
+ std::string errorString = mJsepSession->GetLastError();
+
+ CSFLogError(LOGTAG,
+ "Failed to incorporate remote candidate into SDP:"
+ " res = %u, candidate = %s, level = %i, error = %s",
+ static_cast<unsigned>(*result.mError), aCandidate,
+ level.valueOr(-1), errorString.c_str());
+
+ GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction(
+ __func__,
+ [this, self = RefPtr<PeerConnectionImpl>(this), errorString, result] {
+ if (IsClosed()) {
+ return;
+ }
+ JSErrorResult rv;
+ mPCObserver->OnAddIceCandidateError(
+ *buildJSErrorData(result, errorString), rv);
+ }));
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PeerConnectionImpl::CloseStreams() {
+ PC_AUTO_ENTER_API_CALL(false);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PeerConnectionImpl::SetPeerIdentity(const nsAString& aPeerIdentity) {
+ PC_AUTO_ENTER_API_CALL(true);
+ MOZ_ASSERT(!aPeerIdentity.IsEmpty());
+
+ // once set, this can't be changed
+ if (mPeerIdentity) {
+ if (!mPeerIdentity->Equals(aPeerIdentity)) {
+ return NS_ERROR_FAILURE;
+ }
+ } else {
+ mPeerIdentity = new PeerIdentity(aPeerIdentity);
+ Document* doc = mWindow->GetExtantDoc();
+ if (!doc) {
+ CSFLogInfo(LOGTAG, "Can't update principal on streams; document gone");
+ return NS_ERROR_FAILURE;
+ }
+ for (const auto& transceiver : mTransceivers) {
+ transceiver->Sender()->GetPipeline()->UpdateSinkIdentity(
+ doc->NodePrincipal(), mPeerIdentity);
+ }
+ }
+ return NS_OK;
+}
+
+nsresult PeerConnectionImpl::OnAlpnNegotiated(bool aPrivacyRequested) {
+ PC_AUTO_ENTER_API_CALL(false);
+ MOZ_DIAGNOSTIC_ASSERT(!mRequestedPrivacy ||
+ (*mRequestedPrivacy == PrincipalPrivacy::Private) ==
+ aPrivacyRequested);
+
+ mRequestedPrivacy = Some(aPrivacyRequested ? PrincipalPrivacy::Private
+ : PrincipalPrivacy::NonPrivate);
+ // This updates the MediaPipelines with a private PrincipalHandle. Note that
+ // MediaPipelineReceive has its own AlpnNegotiated handler so it can get
+ // signaled off-main to drop data until it receives the new PrincipalHandle
+ // from us.
+ UpdateMediaPipelines();
+ return NS_OK;
+}
+
+void PeerConnectionImpl::OnDtlsStateChange(const std::string& aTransportId,
+ TransportLayer::State aState) {
+ auto it = mTransportIdToRTCDtlsTransport.find(aTransportId);
+ if (it != mTransportIdToRTCDtlsTransport.end()) {
+ it->second->UpdateState(aState);
+ }
+ UpdateConnectionState();
+}
+
+RTCPeerConnectionState PeerConnectionImpl::GetNewConnectionState() const {
+ // closed The RTCPeerConnection object's [[IsClosed]] slot is true.
+ if (IsClosed()) {
+ return RTCPeerConnectionState::Closed;
+ }
+
+ // Would use a bitset, but that requires lots of static_cast<size_t>
+ // Oh well.
+ std::set<RTCDtlsTransportState> statesFound;
+ for (const auto& [id, dtlsTransport] : mTransportIdToRTCDtlsTransport) {
+ Unused << id;
+ statesFound.insert(dtlsTransport->State());
+ }
+
+ // failed The previous state doesn't apply and any RTCIceTransports are
+ // in the "failed" state or any RTCDtlsTransports are in the "failed" state.
+ // NOTE: "any RTCIceTransports are in the failed state" is equivalent to
+ // mIceConnectionState == Failed
+ if (mIceConnectionState == RTCIceConnectionState::Failed ||
+ statesFound.count(RTCDtlsTransportState::Failed)) {
+ return RTCPeerConnectionState::Failed;
+ }
+
+ // disconnected None of the previous states apply and any
+ // RTCIceTransports are in the "disconnected" state.
+ // NOTE: "any RTCIceTransports are in the disconnected state" is equivalent to
+ // mIceConnectionState == Disconnected.
+ if (mIceConnectionState == RTCIceConnectionState::Disconnected) {
+ return RTCPeerConnectionState::Disconnected;
+ }
+
+ // new None of the previous states apply and all RTCIceTransports are
+ // in the "new" or "closed" state, and all RTCDtlsTransports are in the "new"
+ // or "closed" state, or there are no transports.
+ // NOTE: "all RTCIceTransports are in the new or closed state" is equivalent
+ // to mIceConnectionState == New.
+ if (mIceConnectionState == RTCIceConnectionState::New &&
+ !statesFound.count(RTCDtlsTransportState::Connecting) &&
+ !statesFound.count(RTCDtlsTransportState::Connected) &&
+ !statesFound.count(RTCDtlsTransportState::Failed)) {
+ return RTCPeerConnectionState::New;
+ }
+
+ // No transports
+ if (statesFound.empty()) {
+ return RTCPeerConnectionState::New;
+ }
+
+ // connecting None of the previous states apply and any
+ // RTCIceTransport is in the "new" or "checking" state or any
+ // RTCDtlsTransport is in the "new" or "connecting" state.
+ // NOTE: "None of the previous states apply and any RTCIceTransport is in the
+ // "new" or "checking" state" is equivalent to mIceConnectionState ==
+ // Checking.
+ if (mIceConnectionState == RTCIceConnectionState::Checking ||
+ statesFound.count(RTCDtlsTransportState::New) ||
+ statesFound.count(RTCDtlsTransportState::Connecting)) {
+ return RTCPeerConnectionState::Connecting;
+ }
+
+ // connected None of the previous states apply and all RTCIceTransports are
+ // in the "connected", "completed" or "closed" state, and all
+ // RTCDtlsTransports are in the "connected" or "closed" state.
+ // NOTE: "None of the previous states apply and all RTCIceTransports are in
+ // the "connected", "completed" or "closed" state" is equivalent to
+ // mIceConnectionState == Connected.
+ if (mIceConnectionState == RTCIceConnectionState::Connected &&
+ !statesFound.count(RTCDtlsTransportState::New) &&
+ !statesFound.count(RTCDtlsTransportState::Failed) &&
+ !statesFound.count(RTCDtlsTransportState::Connecting)) {
+ return RTCPeerConnectionState::Connected;
+ }
+
+ // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+ // THERE IS NO CATCH-ALL NONE-OF-THE-ABOVE IN THE SPEC! THIS IS REALLY BAD! !!
+ // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+
+ // Let's try to figure out how bad, precisely.
+ // Any one of these will cause us to bail above.
+ MOZ_ASSERT(mIceConnectionState != RTCIceConnectionState::Failed &&
+ mIceConnectionState != RTCIceConnectionState::Disconnected &&
+ mIceConnectionState != RTCIceConnectionState::Checking);
+ MOZ_ASSERT(!statesFound.count(RTCDtlsTransportState::New) &&
+ !statesFound.count(RTCDtlsTransportState::Connecting) &&
+ !statesFound.count(RTCDtlsTransportState::Failed));
+
+ // One of these must be set, or the empty() check would have failed above.
+ MOZ_ASSERT(statesFound.count(RTCDtlsTransportState::Connected) ||
+ statesFound.count(RTCDtlsTransportState::Closed));
+
+ // Here are our remaining possibilities:
+ // ICE connected, !statesFound.count(Connected), statesFound.count(Closed)
+ // ICE connected, statesFound.count(Connected), !statesFound.count(Closed)
+ // ICE connected, statesFound.count(Connected), statesFound.count(Closed)
+ // All three of these would result in returning Connected above.
+
+ // ICE new, !statesFound.count(Connected), statesFound.count(Closed)
+ // This results in returning New above. Whew.
+
+ // ICE new, statesFound.count(Connected), !statesFound.count(Closed)
+ // ICE new, statesFound.count(Connected), statesFound.count(Closed)
+ // These would make it all the way here! Very weird state though, for all
+ // ICE transports to be new/closed, but having a connected DTLS transport.
+ // Handle this as a non-transition, just in case.
+ return mConnectionState;
+}
+
+void PeerConnectionImpl::UpdateConnectionState() {
+ auto newState = GetNewConnectionState();
+ if (newState != mConnectionState) {
+ CSFLogDebug(LOGTAG, "%s: %d -> %d (%p)", __FUNCTION__,
+ static_cast<int>(mConnectionState), static_cast<int>(newState),
+ this);
+ mConnectionState = newState;
+ if (mConnectionState != RTCPeerConnectionState::Closed) {
+ JSErrorResult jrv;
+ mPCObserver->OnStateChange(PCObserverStateType::ConnectionState, jrv);
+ }
+ }
+}
+
+void PeerConnectionImpl::OnMediaError(const std::string& aError) {
+ CSFLogError(LOGTAG, "Encountered media error! %s", aError.c_str());
+ // TODO: Let content know about this somehow.
+}
+
+void PeerConnectionImpl::DumpPacket_m(size_t level, dom::mozPacketDumpType type,
+ bool sending,
+ UniquePtr<uint8_t[]>& packet,
+ size_t size) {
+ if (IsClosed()) {
+ return;
+ }
+
+ // TODO: Is this efficient? Should we try grabbing our JS ctx from somewhere
+ // else?
+ AutoJSAPI jsapi;
+ if (!jsapi.Init(mWindow)) {
+ return;
+ }
+
+ JS::Rooted<JSObject*> jsobj(
+ jsapi.cx(),
+ JS::NewArrayBufferWithContents(jsapi.cx(), size, packet.release()));
+
+ RootedSpiderMonkeyInterface<ArrayBuffer> arrayBuffer(jsapi.cx());
+ if (!arrayBuffer.Init(jsobj)) {
+ return;
+ }
+
+ JSErrorResult jrv;
+ mPCObserver->OnPacket(level, type, sending, arrayBuffer, jrv);
+}
+
+bool PeerConnectionImpl::HostnameInPref(const char* aPref,
+ const nsCString& aHostName) {
+ auto HostInDomain = [](const nsCString& aHost, const nsCString& aPattern) {
+ int32_t patternOffset = 0;
+ int32_t hostOffset = 0;
+
+ // Act on '*.' wildcard in the left-most position in a domain pattern.
+ if (StringBeginsWith(aPattern, nsCString("*."))) {
+ patternOffset = 2;
+
+ // Ignore the lowest level sub-domain for the hostname.
+ hostOffset = aHost.FindChar('.') + 1;
+
+ if (hostOffset <= 1) {
+ // Reject a match between a wildcard and a TLD or '.foo' form.
+ return false;
+ }
+ }
+
+ nsDependentCString hostRoot(aHost, hostOffset);
+ return hostRoot.EqualsIgnoreCase(aPattern.BeginReading() + patternOffset);
+ };
+
+ nsCString domainList;
+ nsresult nr = Preferences::GetCString(aPref, domainList);
+
+ if (NS_FAILED(nr)) {
+ return false;
+ }
+
+ domainList.StripWhitespace();
+
+ if (domainList.IsEmpty() || aHostName.IsEmpty()) {
+ return false;
+ }
+
+ // Get UTF8 to ASCII domain name normalization service
+ nsresult rv;
+ nsCOMPtr<nsIIDNService> idnService =
+ do_GetService("@mozilla.org/network/idn-service;1", &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ // Test each domain name in the comma separated list
+ // after converting from UTF8 to ASCII. Each domain
+ // must match exactly or have a single leading '*.' wildcard.
+ for (const nsACString& each : domainList.Split(',')) {
+ nsCString domainPattern;
+ rv = idnService->ConvertUTF8toACE(each, domainPattern);
+ if (NS_SUCCEEDED(rv)) {
+ if (HostInDomain(aHostName, domainPattern)) {
+ return true;
+ }
+ } else {
+ NS_WARNING("Failed to convert UTF-8 host to ASCII");
+ }
+ }
+
+ return false;
+}
+
+nsresult PeerConnectionImpl::EnablePacketDump(unsigned long level,
+ dom::mozPacketDumpType type,
+ bool sending) {
+ return GetPacketDumper()->EnablePacketDump(level, type, sending);
+}
+
+nsresult PeerConnectionImpl::DisablePacketDump(unsigned long level,
+ dom::mozPacketDumpType type,
+ bool sending) {
+ return GetPacketDumper()->DisablePacketDump(level, type, sending);
+}
+
+void PeerConnectionImpl::StampTimecard(const char* aEvent) {
+ MOZ_ASSERT(NS_IsMainThread());
+ STAMP_TIMECARD(mTimeCard, aEvent);
+}
+
+void PeerConnectionImpl::SendWarningToConsole(const nsCString& aWarning) {
+ nsAutoString msg = NS_ConvertASCIItoUTF16(aWarning);
+ nsContentUtils::ReportToConsoleByWindowID(msg, nsIScriptError::warningFlag,
+ "WebRTC"_ns, mWindow->WindowID());
+}
+
+void PeerConnectionImpl::GetDefaultVideoCodecs(
+ std::vector<UniquePtr<JsepCodecDescription>>& aSupportedCodecs,
+ bool aUseRtx) {
+ // Supported video codecs.
+ // Note: order here implies priority for building offers!
+ aSupportedCodecs.emplace_back(
+ JsepVideoCodecDescription::CreateDefaultVP8(aUseRtx));
+ aSupportedCodecs.emplace_back(
+ JsepVideoCodecDescription::CreateDefaultVP9(aUseRtx));
+ aSupportedCodecs.emplace_back(
+ JsepVideoCodecDescription::CreateDefaultH264_1(aUseRtx));
+ aSupportedCodecs.emplace_back(
+ JsepVideoCodecDescription::CreateDefaultH264_0(aUseRtx));
+ aSupportedCodecs.emplace_back(
+ JsepVideoCodecDescription::CreateDefaultUlpFec());
+ aSupportedCodecs.emplace_back(
+ JsepApplicationCodecDescription::CreateDefault());
+ aSupportedCodecs.emplace_back(JsepVideoCodecDescription::CreateDefaultRed());
+}
+
+void PeerConnectionImpl::GetDefaultAudioCodecs(
+ std::vector<UniquePtr<JsepCodecDescription>>& aSupportedCodecs) {
+ aSupportedCodecs.emplace_back(JsepAudioCodecDescription::CreateDefaultOpus());
+ aSupportedCodecs.emplace_back(JsepAudioCodecDescription::CreateDefaultG722());
+ aSupportedCodecs.emplace_back(JsepAudioCodecDescription::CreateDefaultPCMU());
+ aSupportedCodecs.emplace_back(JsepAudioCodecDescription::CreateDefaultPCMA());
+ aSupportedCodecs.emplace_back(
+ JsepAudioCodecDescription::CreateDefaultTelephoneEvent());
+}
+
+void PeerConnectionImpl::GetDefaultRtpExtensions(
+ std::vector<RtpExtensionHeader>& aRtpExtensions) {
+ RtpExtensionHeader audioLevel = {JsepMediaType::kAudio,
+ SdpDirectionAttribute::Direction::kSendrecv,
+ webrtc::RtpExtension::kAudioLevelUri};
+ aRtpExtensions.push_back(audioLevel);
+
+ RtpExtensionHeader csrcAudioLevels = {
+ JsepMediaType::kAudio, SdpDirectionAttribute::Direction::kRecvonly,
+ webrtc::RtpExtension::kCsrcAudioLevelsUri};
+ aRtpExtensions.push_back(csrcAudioLevels);
+
+ RtpExtensionHeader mid = {JsepMediaType::kAudioVideo,
+ SdpDirectionAttribute::Direction::kSendrecv,
+ webrtc::RtpExtension::kMidUri};
+ aRtpExtensions.push_back(mid);
+
+ RtpExtensionHeader absSendTime = {JsepMediaType::kVideo,
+ SdpDirectionAttribute::Direction::kSendrecv,
+ webrtc::RtpExtension::kAbsSendTimeUri};
+ aRtpExtensions.push_back(absSendTime);
+
+ RtpExtensionHeader timestampOffset = {
+ JsepMediaType::kVideo, SdpDirectionAttribute::Direction::kSendrecv,
+ webrtc::RtpExtension::kTimestampOffsetUri};
+ aRtpExtensions.push_back(timestampOffset);
+
+ RtpExtensionHeader playoutDelay = {
+ JsepMediaType::kVideo, SdpDirectionAttribute::Direction::kRecvonly,
+ webrtc::RtpExtension::kPlayoutDelayUri};
+ aRtpExtensions.push_back(playoutDelay);
+
+ RtpExtensionHeader transportSequenceNumber = {
+ JsepMediaType::kVideo, SdpDirectionAttribute::Direction::kSendrecv,
+ webrtc::RtpExtension::kTransportSequenceNumberUri};
+ aRtpExtensions.push_back(transportSequenceNumber);
+}
+
+void PeerConnectionImpl::GetCapabilities(
+ const nsAString& aKind, dom::Nullable<dom::RTCRtpCapabilities>& aResult,
+ sdp::Direction aDirection) {
+ std::vector<UniquePtr<JsepCodecDescription>> codecs;
+ std::vector<RtpExtensionHeader> headers;
+ auto mediaType = JsepMediaType::kNone;
+
+ if (aKind.EqualsASCII("video")) {
+ GetDefaultVideoCodecs(codecs, true);
+ mediaType = JsepMediaType::kVideo;
+ } else if (aKind.EqualsASCII("audio")) {
+ GetDefaultAudioCodecs(codecs);
+ mediaType = JsepMediaType::kAudio;
+ } else {
+ return;
+ }
+
+ GetDefaultRtpExtensions(headers);
+
+ // Use the codecs for kind to fill out the RTCRtpCodecCapability
+ for (const auto& codec : codecs) {
+ // To avoid misleading information on codec capabilities skip those
+ // not signaled for audio/video (webrtc-datachannel)
+ // and any disabled by default (ulpfec and red).
+ if (codec->mName == "webrtc-datachannel" || codec->mName == "ulpfec" ||
+ codec->mName == "red") {
+ continue;
+ }
+
+ dom::RTCRtpCodecCapability capability;
+ capability.mMimeType = aKind + NS_ConvertASCIItoUTF16("/" + codec->mName);
+ capability.mClockRate = codec->mClock;
+
+ if (codec->mChannels) {
+ capability.mChannels.Construct(codec->mChannels);
+ }
+
+ UniquePtr<SdpFmtpAttributeList::Parameters> params;
+ codec->ApplyConfigToFmtp(params);
+
+ if (params != nullptr) {
+ std::ostringstream paramsString;
+ params->Serialize(paramsString);
+ nsTString<char16_t> fmtp;
+ fmtp.AssignASCII(paramsString.str());
+ capability.mSdpFmtpLine.Construct(fmtp);
+ }
+
+ if (!aResult.SetValue().mCodecs.AppendElement(capability, fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ }
+
+ // We need to manually add rtx for video.
+ if (mediaType == JsepMediaType::kVideo) {
+ dom::RTCRtpCodecCapability capability;
+ capability.mMimeType = aKind + NS_ConvertASCIItoUTF16("/rtx");
+ capability.mClockRate = 90000;
+ if (!aResult.SetValue().mCodecs.AppendElement(capability, fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ }
+
+ // Add headers that match the direction and media type requested.
+ for (const auto& header : headers) {
+ if ((header.direction & aDirection) && (header.mMediaType & mediaType)) {
+ dom::RTCRtpHeaderExtensionCapability rtpHeader;
+ rtpHeader.mUri.AssignASCII(header.extensionname);
+ if (!aResult.SetValue().mHeaderExtensions.AppendElement(rtpHeader,
+ fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ }
+ }
+}
+
+void PeerConnectionImpl::SetupPreferredCodecs(
+ std::vector<UniquePtr<JsepCodecDescription>>& aPreferredCodecs) {
+ bool useRtx =
+ Preferences::GetBool("media.peerconnection.video.use_rtx", false);
+
+ GetDefaultVideoCodecs(aPreferredCodecs, useRtx);
+ GetDefaultAudioCodecs(aPreferredCodecs);
+
+ // With red update the redundant encodings list
+ for (auto& videoCodec : aPreferredCodecs) {
+ if (videoCodec->mName == "red") {
+ JsepVideoCodecDescription& red =
+ static_cast<JsepVideoCodecDescription&>(*videoCodec);
+ red.UpdateRedundantEncodings(aPreferredCodecs);
+ }
+ }
+}
+
+void PeerConnectionImpl::SetupPreferredRtpExtensions(
+ std::vector<RtpExtensionHeader>& aPreferredheaders) {
+ GetDefaultRtpExtensions(aPreferredheaders);
+
+ if (!Preferences::GetBool("media.navigator.video.use_transport_cc", false)) {
+ aPreferredheaders.erase(
+ std::remove_if(
+ aPreferredheaders.begin(), aPreferredheaders.end(),
+ [&](const RtpExtensionHeader& header) {
+ return header.extensionname ==
+ webrtc::RtpExtension::kTransportSequenceNumberUri;
+ }),
+ aPreferredheaders.end());
+ }
+}
+
+nsresult PeerConnectionImpl::CalculateFingerprint(
+ const std::string& algorithm, std::vector<uint8_t>* fingerprint) const {
+ DtlsDigest digest(algorithm);
+
+ MOZ_ASSERT(fingerprint);
+ const UniqueCERTCertificate& cert = mCertificate->Certificate();
+ nsresult rv = DtlsIdentity::ComputeFingerprint(cert, &digest);
+ if (NS_FAILED(rv)) {
+ CSFLogError(LOGTAG, "Unable to calculate certificate fingerprint, rv=%u",
+ static_cast<unsigned>(rv));
+ return rv;
+ }
+ *fingerprint = digest.value_;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PeerConnectionImpl::GetFingerprint(char** fingerprint) {
+ MOZ_ASSERT(fingerprint);
+ MOZ_ASSERT(mCertificate);
+ std::vector<uint8_t> fp;
+ nsresult rv = CalculateFingerprint(DtlsIdentity::DEFAULT_HASH_ALGORITHM, &fp);
+ NS_ENSURE_SUCCESS(rv, rv);
+ std::ostringstream os;
+ os << DtlsIdentity::DEFAULT_HASH_ALGORITHM << ' '
+ << SdpFingerprintAttributeList::FormatFingerprint(fp);
+ std::string fpStr = os.str();
+
+ char* tmp = new char[fpStr.size() + 1];
+ std::copy(fpStr.begin(), fpStr.end(), tmp);
+ tmp[fpStr.size()] = '\0';
+
+ *fingerprint = tmp;
+ return NS_OK;
+}
+
+void PeerConnectionImpl::GetCurrentLocalDescription(nsAString& aSDP) const {
+ aSDP = NS_ConvertASCIItoUTF16(mCurrentLocalDescription.c_str());
+}
+
+void PeerConnectionImpl::GetPendingLocalDescription(nsAString& aSDP) const {
+ aSDP = NS_ConvertASCIItoUTF16(mPendingLocalDescription.c_str());
+}
+
+void PeerConnectionImpl::GetCurrentRemoteDescription(nsAString& aSDP) const {
+ aSDP = NS_ConvertASCIItoUTF16(mCurrentRemoteDescription.c_str());
+}
+
+void PeerConnectionImpl::GetPendingRemoteDescription(nsAString& aSDP) const {
+ aSDP = NS_ConvertASCIItoUTF16(mPendingRemoteDescription.c_str());
+}
+
+dom::Nullable<bool> PeerConnectionImpl::GetCurrentOfferer() const {
+ dom::Nullable<bool> result;
+ if (mCurrentOfferer.isSome()) {
+ result.SetValue(*mCurrentOfferer);
+ }
+ return result;
+}
+
+dom::Nullable<bool> PeerConnectionImpl::GetPendingOfferer() const {
+ dom::Nullable<bool> result;
+ if (mPendingOfferer.isSome()) {
+ result.SetValue(*mPendingOfferer);
+ }
+ return result;
+}
+
+NS_IMETHODIMP
+PeerConnectionImpl::SignalingState(RTCSignalingState* aState) {
+ PC_AUTO_ENTER_API_CALL_NO_CHECK();
+ MOZ_ASSERT(aState);
+
+ *aState = mSignalingState;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PeerConnectionImpl::IceConnectionState(RTCIceConnectionState* aState) {
+ PC_AUTO_ENTER_API_CALL_NO_CHECK();
+ MOZ_ASSERT(aState);
+
+ *aState = mIceConnectionState;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PeerConnectionImpl::IceGatheringState(RTCIceGatheringState* aState) {
+ PC_AUTO_ENTER_API_CALL_NO_CHECK();
+ MOZ_ASSERT(aState);
+
+ *aState = mIceGatheringState;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+PeerConnectionImpl::ConnectionState(RTCPeerConnectionState* aState) {
+ PC_AUTO_ENTER_API_CALL_NO_CHECK();
+ MOZ_ASSERT(aState);
+
+ *aState = mConnectionState;
+ return NS_OK;
+}
+
+nsresult PeerConnectionImpl::CheckApiState(bool assert_ice_ready) const {
+ PC_AUTO_ENTER_API_CALL_NO_CHECK();
+ MOZ_ASSERT(mTrickle || !assert_ice_ready ||
+ (mIceGatheringState == RTCIceGatheringState::Complete));
+
+ if (IsClosed()) {
+ CSFLogError(LOGTAG, "%s: called API while closed", __FUNCTION__);
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+void PeerConnectionImpl::StoreFinalStats(
+ UniquePtr<RTCStatsReportInternal>&& report) {
+ using namespace Telemetry;
+
+ report->mClosed = true;
+
+ for (const auto& inboundRtpStats : report->mInboundRtpStreamStats) {
+ bool isVideo = (inboundRtpStats.mId.Value().Find(u"video") != -1);
+ if (!isVideo) {
+ continue;
+ }
+ if (inboundRtpStats.mDiscardedPackets.WasPassed() &&
+ report->mCallDurationMs.WasPassed()) {
+ double mins = report->mCallDurationMs.Value() / (1000 * 60);
+ if (mins > 0) {
+ Accumulate(
+ WEBRTC_VIDEO_DECODER_DISCARDED_PACKETS_PER_CALL_PPM,
+ uint32_t(double(inboundRtpStats.mDiscardedPackets.Value()) / mins));
+ }
+ }
+ }
+
+ // Finally, store the stats
+ mFinalStats = std::move(report);
+}
+
+NS_IMETHODIMP
+PeerConnectionImpl::Close() {
+ CSFLogDebug(LOGTAG, "%s: for %s", __FUNCTION__, mHandle.c_str());
+ PC_AUTO_ENTER_API_CALL_NO_CHECK();
+
+ if (IsClosed()) {
+ return NS_OK;
+ }
+
+ STAMP_TIMECARD(mTimeCard, "Close");
+
+ // When ICE completes, we record some telemetry. We do this at the end of the
+ // call because we want to make sure we've waited for all trickle ICE
+ // candidates to come in; this can happen well after we've transitioned to
+ // connected. As a bonus, this allows us to detect race conditions where a
+ // stats dispatch happens right as the PC closes.
+ RecordEndOfCallTelemetry();
+
+ CSFLogInfo(LOGTAG,
+ "%s: Closing PeerConnectionImpl %s; "
+ "ending call",
+ __FUNCTION__, mHandle.c_str());
+ if (mJsepSession) {
+ mJsepSession->Close();
+ }
+ if (mDataConnection) {
+ CSFLogInfo(LOGTAG, "%s: Destroying DataChannelConnection %p for %s",
+ __FUNCTION__, (void*)mDataConnection.get(), mHandle.c_str());
+ mDataConnection->Destroy();
+ mDataConnection =
+ nullptr; // it may not go away until the runnables are dead
+ }
+
+ if (mStunAddrsRequest) {
+ for (const auto& hostname : mRegisteredMDNSHostnames) {
+ mStunAddrsRequest->SendUnregisterMDNSHostname(
+ nsCString(hostname.c_str()));
+ }
+ mRegisteredMDNSHostnames.clear();
+ mStunAddrsRequest->Cancel();
+ mStunAddrsRequest = nullptr;
+ }
+
+ for (auto& transceiver : mTransceivers) {
+ transceiver->Close();
+ }
+
+ mTransportIdToRTCDtlsTransport.clear();
+
+ mQueuedIceCtxOperations.clear();
+
+ mOperations.Clear();
+
+ // Uncount this connection as active on the inner window upon close.
+ if (mWindow && mActiveOnWindow) {
+ mWindow->RemovePeerConnection();
+ mActiveOnWindow = false;
+ }
+
+ mSignalingState = RTCSignalingState::Closed;
+ mConnectionState = RTCPeerConnectionState::Closed;
+
+ if (!mTransportHandler) {
+ // We were never initialized, apparently.
+ return NS_OK;
+ }
+
+ // Clear any resources held by libwebrtc through our Call instance.
+ RefPtr<GenericPromise> callDestroyPromise;
+ if (mCall) {
+ // Make sure the compiler does not get confused and try to acquire a
+ // reference to this thread _after_ we null out mCall.
+ auto callThread = mCall->mCallThread;
+ callDestroyPromise =
+ InvokeAsync(callThread, __func__, [call = std::move(mCall)]() {
+ call->Destroy();
+ return GenericPromise::CreateAndResolve(
+ true, "PCImpl->WebRtcCallWrapper::Destroy");
+ });
+ } else {
+ callDestroyPromise = GenericPromise::CreateAndResolve(true, __func__);
+ }
+
+ mFinalStatsQuery =
+ GetStats(nullptr, true)
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [this, self = RefPtr<PeerConnectionImpl>(this)](
+ UniquePtr<dom::RTCStatsReportInternal>&& aReport) mutable {
+ StoreFinalStats(std::move(aReport));
+ return GenericNonExclusivePromise::CreateAndResolve(true,
+ __func__);
+ },
+ [](nsresult aError) {
+ return GenericNonExclusivePromise::CreateAndResolve(true,
+ __func__);
+ });
+
+ // 1. Allow final stats query to complete.
+ // 2. Tear down call, if necessary. We do this before we shut down the
+ // transport handler, so RTCP BYE can be sent.
+ // 3. Unhook from the signal handler (sigslot) for transport stuff. This must
+ // be done before we tear down the transport handler.
+ // 4. Tear down the transport handler, and deregister from PeerConnectionCtx.
+ // When we deregister from PeerConnectionCtx, our final stats (if any)
+ // will be stored.
+ MOZ_RELEASE_ASSERT(mSTSThread);
+ mFinalStatsQuery
+ ->Then(GetMainThreadSerialEventTarget(), __func__,
+ [callDestroyPromise]() mutable { return callDestroyPromise; })
+ ->Then(
+ mSTSThread, __func__,
+ [signalHandler = std::move(mSignalHandler)]() mutable {
+ CSFLogDebug(
+ LOGTAG,
+ "Destroying PeerConnectionImpl::SignalHandler on STS thread");
+ return GenericPromise::CreateAndResolve(
+ true, "PeerConnectionImpl::~SignalHandler");
+ })
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [this, self = RefPtr<PeerConnectionImpl>(this)]() mutable {
+ CSFLogDebug(LOGTAG, "PCImpl->mTransportHandler::RemoveTransports");
+ mTransportHandler->RemoveTransportsExcept(std::set<std::string>());
+ if (mPrivateWindow) {
+ mTransportHandler->ExitPrivateMode();
+ }
+ mTransportHandler = nullptr;
+ if (PeerConnectionCtx::isActive()) {
+ // If we're shutting down xpcom, this Instance will be unset
+ // before calling Close() on all remaining PCs, to avoid
+ // reentrancy.
+ PeerConnectionCtx::GetInstance()->RemovePeerConnection(mHandle);
+ }
+ });
+
+ return NS_OK;
+}
+
+void PeerConnectionImpl::BreakCycles() {
+ for (auto& transceiver : mTransceivers) {
+ transceiver->BreakCycles();
+ }
+ mTransceivers.Clear();
+}
+
+bool PeerConnectionImpl::HasPendingSetParameters() const {
+ for (const auto& transceiver : mTransceivers) {
+ if (transceiver->Sender()->HasPendingSetParameters()) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void PeerConnectionImpl::InvalidateLastReturnedParameters() {
+ for (const auto& transceiver : mTransceivers) {
+ transceiver->Sender()->InvalidateLastReturnedParameters();
+ }
+}
+
+nsresult PeerConnectionImpl::SetConfiguration(
+ const RTCConfiguration& aConfiguration) {
+ nsresult rv = mTransportHandler->SetIceConfig(
+ aConfiguration.mIceServers, aConfiguration.mIceTransportPolicy);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ JsepBundlePolicy bundlePolicy;
+ switch (aConfiguration.mBundlePolicy) {
+ case dom::RTCBundlePolicy::Balanced:
+ bundlePolicy = kBundleBalanced;
+ break;
+ case dom::RTCBundlePolicy::Max_compat:
+ bundlePolicy = kBundleMaxCompat;
+ break;
+ case dom::RTCBundlePolicy::Max_bundle:
+ bundlePolicy = kBundleMaxBundle;
+ break;
+ default:
+ MOZ_CRASH();
+ }
+
+ // Ignore errors, since those ought to be handled earlier.
+ Unused << mJsepSession->SetBundlePolicy(bundlePolicy);
+
+ if (!aConfiguration.mPeerIdentity.IsEmpty()) {
+ mPeerIdentity = new PeerIdentity(aConfiguration.mPeerIdentity);
+ mRequestedPrivacy = Some(PrincipalPrivacy::Private);
+ }
+
+ auto proxyConfig = GetProxyConfig();
+ if (proxyConfig) {
+ // Note that this could check if PrivacyRequested() is set on the PC and
+ // remove "webrtc" from the ALPN list. But that would only work if the PC
+ // was constructed with a peerIdentity constraint, not when isolated
+ // streams are added. If we ever need to signal to the proxy that the
+ // media is isolated, then we would need to restructure this code.
+ mTransportHandler->SetProxyConfig(std::move(*proxyConfig));
+ }
+
+ // Store the configuration for about:webrtc
+ StoreConfigurationForAboutWebrtc(aConfiguration);
+
+ return NS_OK;
+}
+
+RTCSctpTransport* PeerConnectionImpl::GetSctp() const {
+ return mSctpTransport.get();
+}
+
+void PeerConnectionImpl::RestartIce() {
+ RestartIceNoRenegotiationNeeded();
+ // Update the negotiation-needed flag for connection.
+ UpdateNegotiationNeeded();
+}
+
+// webrtc-pc does not specify any situations where this is done, but the JSEP
+// spec does, in some situations due to setConfiguration.
+void PeerConnectionImpl::RestartIceNoRenegotiationNeeded() {
+ // Empty connection.[[LocalIceCredentialsToReplace]], and populate it with
+ // all ICE credentials (ice-ufrag and ice-pwd as defined in section 15.4 of
+ // [RFC5245]) found in connection.[[CurrentLocalDescription]], as well as all
+ // ICE credentials found in connection.[[PendingLocalDescription]].
+ mLocalIceCredentialsToReplace = mJsepSession->GetLocalIceCredentials();
+}
+
+bool PeerConnectionImpl::PluginCrash(uint32_t aPluginID,
+ const nsAString& aPluginName) {
+ // fire an event to the DOM window if this is "ours"
+ if (!AnyCodecHasPluginID(aPluginID)) {
+ return false;
+ }
+
+ CSFLogError(LOGTAG, "%s: Our plugin %llu crashed", __FUNCTION__,
+ static_cast<unsigned long long>(aPluginID));
+
+ RefPtr<Document> doc = mWindow->GetExtantDoc();
+ if (!doc) {
+ NS_WARNING("Couldn't get document for PluginCrashed event!");
+ return true;
+ }
+
+ PluginCrashedEventInit init;
+ init.mPluginID = aPluginID;
+ init.mPluginName = aPluginName;
+ init.mSubmittedCrashReport = false;
+ init.mGmpPlugin = true;
+ init.mBubbles = true;
+ init.mCancelable = true;
+
+ RefPtr<PluginCrashedEvent> event =
+ PluginCrashedEvent::Constructor(doc, u"PluginCrashed"_ns, init);
+
+ event->SetTrusted(true);
+ event->WidgetEventPtr()->mFlags.mOnlyChromeDispatch = true;
+
+ nsCOMPtr<nsPIDOMWindowInner> window = mWindow;
+ EventDispatcher::DispatchDOMEvent(window, nullptr, event, nullptr, nullptr);
+
+ return true;
+}
+
+void PeerConnectionImpl::RecordEndOfCallTelemetry() {
+ if (!mCallTelemStarted) {
+ return;
+ }
+ MOZ_RELEASE_ASSERT(!mCallTelemEnded, "Don't end telemetry twice");
+ MOZ_RELEASE_ASSERT(mJsepSession,
+ "Call telemetry only starts after jsep session start");
+ MOZ_RELEASE_ASSERT(mJsepSession->GetNegotiations() > 0,
+ "Call telemetry only starts after first connection");
+
+ // Bitmask used for WEBRTC/LOOP_CALL_TYPE telemetry reporting
+ static const uint32_t kAudioTypeMask = 1;
+ static const uint32_t kVideoTypeMask = 2;
+ static const uint32_t kDataChannelTypeMask = 4;
+
+ // Report end-of-call Telemetry
+ Telemetry::Accumulate(Telemetry::WEBRTC_RENEGOTIATIONS,
+ mJsepSession->GetNegotiations() - 1);
+ Telemetry::Accumulate(Telemetry::WEBRTC_MAX_VIDEO_SEND_TRACK,
+ mMaxSending[SdpMediaSection::MediaType::kVideo]);
+ Telemetry::Accumulate(Telemetry::WEBRTC_MAX_VIDEO_RECEIVE_TRACK,
+ mMaxReceiving[SdpMediaSection::MediaType::kVideo]);
+ Telemetry::Accumulate(Telemetry::WEBRTC_MAX_AUDIO_SEND_TRACK,
+ mMaxSending[SdpMediaSection::MediaType::kAudio]);
+ Telemetry::Accumulate(Telemetry::WEBRTC_MAX_AUDIO_RECEIVE_TRACK,
+ mMaxReceiving[SdpMediaSection::MediaType::kAudio]);
+ // DataChannels appear in both Sending and Receiving
+ Telemetry::Accumulate(Telemetry::WEBRTC_DATACHANNEL_NEGOTIATED,
+ mMaxSending[SdpMediaSection::MediaType::kApplication]);
+ // Enumerated/bitmask: 1 = Audio, 2 = Video, 4 = DataChannel
+ // A/V = 3, A/V/D = 7, etc
+ uint32_t type = 0;
+ if (mMaxSending[SdpMediaSection::MediaType::kAudio] ||
+ mMaxReceiving[SdpMediaSection::MediaType::kAudio]) {
+ type = kAudioTypeMask;
+ }
+ if (mMaxSending[SdpMediaSection::MediaType::kVideo] ||
+ mMaxReceiving[SdpMediaSection::MediaType::kVideo]) {
+ type |= kVideoTypeMask;
+ }
+ if (mMaxSending[SdpMediaSection::MediaType::kApplication]) {
+ type |= kDataChannelTypeMask;
+ }
+ Telemetry::Accumulate(Telemetry::WEBRTC_CALL_TYPE, type);
+
+ MOZ_RELEASE_ASSERT(mWindow);
+ auto found = sCallDurationTimers.find(mWindow->WindowID());
+ if (found != sCallDurationTimers.end()) {
+ found->second.UnregisterConnection((type & kAudioTypeMask) ||
+ (type & kVideoTypeMask));
+ if (found->second.IsStopped()) {
+ sCallDurationTimers.erase(found);
+ }
+ }
+ mCallTelemEnded = true;
+}
+
+DOMMediaStream* PeerConnectionImpl::GetReceiveStream(
+ const std::string& aId) const {
+ nsString wanted = NS_ConvertASCIItoUTF16(aId.c_str());
+ for (auto& stream : mReceiveStreams) {
+ nsString id;
+ stream->GetId(id);
+ if (id == wanted) {
+ return stream;
+ }
+ }
+ return nullptr;
+}
+
+DOMMediaStream* PeerConnectionImpl::CreateReceiveStream(
+ const std::string& aId) {
+ mReceiveStreams.AppendElement(new DOMMediaStream(mWindow));
+ mReceiveStreams.LastElement()->AssignId(NS_ConvertASCIItoUTF16(aId.c_str()));
+ return mReceiveStreams.LastElement();
+}
+
+already_AddRefed<dom::Promise> PeerConnectionImpl::OnSetDescriptionSuccess(
+ dom::RTCSdpType aSdpType, bool aRemote, ErrorResult& aError) {
+ CSFLogDebug(LOGTAG, __FUNCTION__);
+
+ RefPtr<dom::Promise> p = MakePromise(aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+
+ DoSetDescriptionSuccessPostProcessing(aSdpType, aRemote, p);
+
+ return p.forget();
+}
+
+void PeerConnectionImpl::DoSetDescriptionSuccessPostProcessing(
+ dom::RTCSdpType aSdpType, bool aRemote, const RefPtr<dom::Promise>& aP) {
+ // Spec says we queue a task for all the stuff that ends up back in JS
+ GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction(
+ __func__,
+ [this, self = RefPtr<PeerConnectionImpl>(this), aSdpType, aRemote, aP] {
+ if (IsClosed()) {
+ // Yes, we do not settle the promise here. Yes, this is what the spec
+ // wants.
+ return;
+ }
+
+ MOZ_ASSERT(mUncommittedJsepSession);
+
+ // sRD/sLD needs to be redone in certain circumstances
+ bool needsRedo = HasPendingSetParameters();
+ if (!needsRedo && aRemote && (aSdpType == dom::RTCSdpType::Offer)) {
+ for (auto& transceiver : mTransceivers) {
+ if (!mUncommittedJsepSession->GetTransceiver(
+ transceiver->GetJsepTransceiverId())) {
+ needsRedo = true;
+ break;
+ }
+ }
+ }
+
+ if (needsRedo) {
+ // Spec says to abort, and re-do the sRD!
+ // This happens either when there is a SetParameters call in
+ // flight (that will race against the [[SendEncodings]]
+ // modification caused by sRD(offer)), or when addTrack has been
+ // called while sRD(offer) was in progress.
+ mUncommittedJsepSession.reset(mJsepSession->Clone());
+ JsepSession::Result result;
+ if (aRemote) {
+ mUncommittedJsepSession->SetRemoteDescription(
+ ToJsepSdpType(aSdpType), mRemoteRequestedSDP);
+ } else {
+ mUncommittedJsepSession->SetLocalDescription(
+ ToJsepSdpType(aSdpType), mLocalRequestedSDP);
+ }
+ if (result.mError.isSome()) {
+ // wat
+ nsCString error(
+ "When redoing sRD/sLD because it raced against "
+ "addTrack or setParameters, we encountered a failure that "
+ "did not happen "
+ "the first time. This should never happen. The error was: ");
+ error += mUncommittedJsepSession->GetLastError().c_str();
+ aP->MaybeRejectWithOperationError(error);
+ MOZ_ASSERT(false);
+ } else {
+ DoSetDescriptionSuccessPostProcessing(aSdpType, aRemote, aP);
+ }
+ return;
+ }
+
+ for (auto& transceiver : mTransceivers) {
+ if (!mUncommittedJsepSession->GetTransceiver(
+ transceiver->GetJsepTransceiverId())) {
+ // sLD, or sRD(answer), just make sure the new transceiver is
+ // added, no need to re-do anything.
+ mUncommittedJsepSession->AddTransceiver(
+ transceiver->GetJsepTransceiver());
+ }
+ }
+
+ mJsepSession = std::move(mUncommittedJsepSession);
+
+ auto newSignalingState = GetSignalingState();
+ SyncFromJsep();
+ if (aRemote || aSdpType == dom::RTCSdpType::Pranswer ||
+ aSdpType == dom::RTCSdpType::Answer) {
+ InvalidateLastReturnedParameters();
+ }
+
+ // Section 4.4.1.5 Set the RTCSessionDescription:
+ if (aSdpType == dom::RTCSdpType::Rollback) {
+ // - step 4.5.10, type is rollback
+ RollbackRTCDtlsTransports();
+ } else if (!(aRemote && aSdpType == dom::RTCSdpType::Offer)) {
+ // - step 4.5.9 type is not rollback
+ // - step 4.5.9.1 when remote is false
+ // - step 4.5.9.2.13 when remote is true, type answer or pranswer
+ // More simply: not rollback, and not for remote offers.
+ bool markAsStable = aSdpType == dom::RTCSdpType::Offer &&
+ mSignalingState == RTCSignalingState::Stable;
+ UpdateRTCDtlsTransports(markAsStable);
+ }
+
+ // Did we just apply a local description?
+ if (!aRemote) {
+ // We'd like to handle this in PeerConnectionImpl::UpdateNetworkState.
+ // Unfortunately, if the WiFi switch happens quickly, we never see
+ // that state change. We need to detect the ice restart here and
+ // reset the PeerConnectionImpl's stun addresses so they are
+ // regathered when PeerConnectionImpl::GatherIfReady is called.
+ if (mJsepSession->IsIceRestarting()) {
+ ResetStunAddrsForIceRestart();
+ }
+ EnsureTransports(*mJsepSession);
+ }
+
+ if (mJsepSession->GetState() == kJsepStateStable) {
+ if (aSdpType != dom::RTCSdpType::Rollback) {
+ // We need this initted for UpdateTransports
+ InitializeDataChannel();
+ }
+
+ // If we're rolling back a local offer, we might need to remove some
+ // transports, and stomp some MediaPipeline setup, but nothing further
+ // needs to be done.
+ UpdateTransports(*mJsepSession, mForceIceTcp);
+ if (NS_FAILED(UpdateMediaPipelines())) {
+ CSFLogError(LOGTAG, "Error Updating MediaPipelines");
+ NS_ASSERTION(
+ false,
+ "Error Updating MediaPipelines in OnSetDescriptionSuccess()");
+ aP->MaybeRejectWithOperationError("Error Updating MediaPipelines");
+ }
+
+ if (aSdpType != dom::RTCSdpType::Rollback) {
+ StartIceChecks(*mJsepSession);
+ }
+
+ // Telemetry: record info on the current state of
+ // streams/renegotiations/etc Note: this code gets run on rollbacks as
+ // well!
+
+ // Update the max channels used with each direction for each type
+ uint16_t receiving[SdpMediaSection::kMediaTypes];
+ uint16_t sending[SdpMediaSection::kMediaTypes];
+ mJsepSession->CountTracksAndDatachannels(receiving, sending);
+ for (size_t i = 0; i < SdpMediaSection::kMediaTypes; i++) {
+ if (mMaxReceiving[i] < receiving[i]) {
+ mMaxReceiving[i] = receiving[i];
+ }
+ if (mMaxSending[i] < sending[i]) {
+ mMaxSending[i] = sending[i];
+ }
+ }
+ }
+
+ mPendingRemoteDescription =
+ mJsepSession->GetRemoteDescription(kJsepDescriptionPending);
+ mCurrentRemoteDescription =
+ mJsepSession->GetRemoteDescription(kJsepDescriptionCurrent);
+ mPendingLocalDescription =
+ mJsepSession->GetLocalDescription(kJsepDescriptionPending);
+ mCurrentLocalDescription =
+ mJsepSession->GetLocalDescription(kJsepDescriptionCurrent);
+ mPendingOfferer = mJsepSession->IsPendingOfferer();
+ mCurrentOfferer = mJsepSession->IsCurrentOfferer();
+
+ if (aSdpType == dom::RTCSdpType::Answer) {
+ std::set<std::pair<std::string, std::string>> iceCredentials =
+ mJsepSession->GetLocalIceCredentials();
+ std::vector<std::pair<std::string, std::string>>
+ iceCredentialsNotReplaced;
+ std::set_intersection(mLocalIceCredentialsToReplace.begin(),
+ mLocalIceCredentialsToReplace.end(),
+ iceCredentials.begin(), iceCredentials.end(),
+ std::back_inserter(iceCredentialsNotReplaced));
+
+ if (iceCredentialsNotReplaced.empty()) {
+ mLocalIceCredentialsToReplace.clear();
+ }
+ }
+
+ if (newSignalingState == RTCSignalingState::Stable) {
+ mNegotiationNeeded = false;
+ UpdateNegotiationNeeded();
+ }
+
+ // Spec does not actually tell us to do this, but that is probably a
+ // spec bug.
+ UpdateConnectionState();
+
+ JSErrorResult jrv;
+ if (newSignalingState != mSignalingState) {
+ mSignalingState = newSignalingState;
+ mPCObserver->OnStateChange(PCObserverStateType::SignalingState, jrv);
+ }
+
+ if (aRemote) {
+ dom::RTCRtpReceiver::StreamAssociationChanges changes;
+ for (const auto& transceiver : mTransceivers) {
+ transceiver->Receiver()->UpdateStreams(&changes);
+ }
+
+ for (const auto& receiver : changes.mReceiversToMute) {
+ // This sets the muted state for the recv track and all its clones.
+ receiver->SetTrackMuteFromRemoteSdp();
+ }
+
+ for (const auto& association : changes.mStreamAssociationsRemoved) {
+ RefPtr<DOMMediaStream> stream =
+ GetReceiveStream(association.mStreamId);
+ if (stream && stream->HasTrack(*association.mTrack)) {
+ stream->RemoveTrackInternal(association.mTrack);
+ }
+ }
+
+ // TODO(Bug 1241291): For legacy event, remove eventually
+ std::vector<RefPtr<DOMMediaStream>> newStreams;
+
+ for (const auto& association : changes.mStreamAssociationsAdded) {
+ RefPtr<DOMMediaStream> stream =
+ GetReceiveStream(association.mStreamId);
+ if (!stream) {
+ stream = CreateReceiveStream(association.mStreamId);
+ newStreams.push_back(stream);
+ }
+
+ if (!stream->HasTrack(*association.mTrack)) {
+ stream->AddTrackInternal(association.mTrack);
+ }
+ }
+
+ // Make sure to wait until after we've calculated track changes before
+ // doing this.
+ for (size_t i = 0; i < mTransceivers.Length();) {
+ auto& transceiver = mTransceivers[i];
+ if (transceiver->ShouldRemove()) {
+ mTransceivers[i]->Close();
+ mTransceivers[i]->SetRemovedFromPc();
+ mTransceivers.RemoveElementAt(i);
+ } else {
+ ++i;
+ }
+ }
+
+ for (const auto& trackEvent : changes.mTrackEvents) {
+ dom::Sequence<OwningNonNull<DOMMediaStream>> streams;
+ for (const auto& id : trackEvent.mStreamIds) {
+ RefPtr<DOMMediaStream> stream = GetReceiveStream(id);
+ if (!stream) {
+ MOZ_ASSERT(false);
+ continue;
+ }
+ if (!streams.AppendElement(*stream, fallible)) {
+ // XXX(Bug 1632090) Instead of extending the array 1-by-1 (which
+ // might involve multiple reallocations) and potentially
+ // crashing here, SetCapacity could be called outside the loop
+ // once.
+ mozalloc_handle_oom(0);
+ }
+ }
+ mPCObserver->FireTrackEvent(*trackEvent.mReceiver, streams, jrv);
+ }
+
+ // TODO(Bug 1241291): Legacy event, remove eventually
+ for (const auto& stream : newStreams) {
+ mPCObserver->FireStreamEvent(*stream, jrv);
+ }
+ }
+ aP->MaybeResolveWithUndefined();
+ }));
+}
+
+void PeerConnectionImpl::OnSetDescriptionError() {
+ mUncommittedJsepSession = nullptr;
+}
+
+RTCSignalingState PeerConnectionImpl::GetSignalingState() const {
+ switch (mJsepSession->GetState()) {
+ case kJsepStateStable:
+ return RTCSignalingState::Stable;
+ break;
+ case kJsepStateHaveLocalOffer:
+ return RTCSignalingState::Have_local_offer;
+ break;
+ case kJsepStateHaveRemoteOffer:
+ return RTCSignalingState::Have_remote_offer;
+ break;
+ case kJsepStateHaveLocalPranswer:
+ return RTCSignalingState::Have_local_pranswer;
+ break;
+ case kJsepStateHaveRemotePranswer:
+ return RTCSignalingState::Have_remote_pranswer;
+ break;
+ case kJsepStateClosed:
+ return RTCSignalingState::Closed;
+ break;
+ }
+ MOZ_CRASH("Invalid JSEP state");
+}
+
+bool PeerConnectionImpl::IsClosed() const {
+ return mSignalingState == RTCSignalingState::Closed;
+}
+
+PeerConnectionWrapper::PeerConnectionWrapper(const std::string& handle)
+ : impl_(nullptr) {
+ if (PeerConnectionCtx::isActive()) {
+ impl_ = PeerConnectionCtx::GetInstance()->GetPeerConnection(handle);
+ }
+}
+
+const RefPtr<MediaTransportHandler> PeerConnectionImpl::GetTransportHandler()
+ const {
+ return mTransportHandler;
+}
+
+const std::string& PeerConnectionImpl::GetHandle() { return mHandle; }
+
+const std::string& PeerConnectionImpl::GetName() {
+ PC_AUTO_ENTER_API_CALL_NO_CHECK();
+ return mName;
+}
+
+void PeerConnectionImpl::CandidateReady(const std::string& candidate,
+ const std::string& transportId,
+ const std::string& ufrag) {
+ STAMP_TIMECARD(mTimeCard, "Ice Candidate gathered");
+ PC_AUTO_ENTER_API_CALL_VOID_RETURN(false);
+
+ if (mForceIceTcp && std::string::npos != candidate.find(" UDP ")) {
+ CSFLogWarn(LOGTAG, "Blocking local UDP candidate: %s", candidate.c_str());
+ STAMP_TIMECARD(mTimeCard, "UDP Ice Candidate blocked");
+ return;
+ }
+
+ // One of the very few places we still use level; required by the JSEP API
+ uint16_t level = 0;
+ std::string mid;
+ bool skipped = false;
+
+ if (mUncommittedJsepSession) {
+ // An sLD or sRD is in progress, and while that is the case, we need to add
+ // the candidate to both the current JSEP engine, and the uncommitted JSEP
+ // engine. We ignore errors because the spec says to only take into account
+ // the current/pending local descriptions when determining whether to
+ // surface the candidate to content, which does not take into account any
+ // in-progress sRD/sLD.
+ Unused << mUncommittedJsepSession->AddLocalIceCandidate(
+ candidate, transportId, ufrag, &level, &mid, &skipped);
+ }
+
+ nsresult res = mJsepSession->AddLocalIceCandidate(
+ candidate, transportId, ufrag, &level, &mid, &skipped);
+
+ if (NS_FAILED(res)) {
+ std::string errorString = mJsepSession->GetLastError();
+
+ STAMP_TIMECARD(mTimeCard, "Local Ice Candidate invalid");
+ CSFLogError(LOGTAG,
+ "Failed to incorporate local candidate into SDP:"
+ " res = %u, candidate = %s, transport-id = %s,"
+ " error = %s",
+ static_cast<unsigned>(res), candidate.c_str(),
+ transportId.c_str(), errorString.c_str());
+ return;
+ }
+
+ if (skipped) {
+ STAMP_TIMECARD(mTimeCard, "Local Ice Candidate skipped");
+ CSFLogInfo(LOGTAG,
+ "Skipped adding local candidate %s (transport-id %s) "
+ "to SDP, this typically happens because the m-section "
+ "is bundled, which means it doesn't make sense for it "
+ "to have its own transport-related attributes.",
+ candidate.c_str(), transportId.c_str());
+ return;
+ }
+
+ mPendingLocalDescription =
+ mJsepSession->GetLocalDescription(kJsepDescriptionPending);
+ mCurrentLocalDescription =
+ mJsepSession->GetLocalDescription(kJsepDescriptionCurrent);
+ CSFLogInfo(LOGTAG, "Passing local candidate to content: %s",
+ candidate.c_str());
+ SendLocalIceCandidateToContent(level, mid, candidate, ufrag);
+}
+
+void PeerConnectionImpl::SendLocalIceCandidateToContent(
+ uint16_t level, const std::string& mid, const std::string& candidate,
+ const std::string& ufrag) {
+ STAMP_TIMECARD(mTimeCard, "Send Ice Candidate to content");
+ JSErrorResult rv;
+ mPCObserver->OnIceCandidate(level, ObString(mid.c_str()),
+ ObString(candidate.c_str()),
+ ObString(ufrag.c_str()), rv);
+}
+
+void PeerConnectionImpl::IceConnectionStateChange(
+ dom::RTCIceConnectionState domState) {
+ PC_AUTO_ENTER_API_CALL_VOID_RETURN(false);
+
+ CSFLogDebug(LOGTAG, "%s: %d -> %d", __FUNCTION__,
+ static_cast<int>(mIceConnectionState),
+ static_cast<int>(domState));
+
+ if (domState == mIceConnectionState) {
+ // no work to be done since the states are the same.
+ // this can happen during ICE rollback situations.
+ return;
+ }
+
+ mIceConnectionState = domState;
+
+ // Would be nice if we had a means of converting one of these dom enums
+ // to a string that wasn't almost as much text as this switch statement...
+ switch (mIceConnectionState) {
+ case RTCIceConnectionState::New:
+ STAMP_TIMECARD(mTimeCard, "Ice state: new");
+ break;
+ case RTCIceConnectionState::Checking:
+ // For telemetry
+ mIceStartTime = TimeStamp::Now();
+ STAMP_TIMECARD(mTimeCard, "Ice state: checking");
+ break;
+ case RTCIceConnectionState::Connected:
+ STAMP_TIMECARD(mTimeCard, "Ice state: connected");
+ StartCallTelem();
+ break;
+ case RTCIceConnectionState::Completed:
+ STAMP_TIMECARD(mTimeCard, "Ice state: completed");
+ break;
+ case RTCIceConnectionState::Failed:
+ STAMP_TIMECARD(mTimeCard, "Ice state: failed");
+ break;
+ case RTCIceConnectionState::Disconnected:
+ STAMP_TIMECARD(mTimeCard, "Ice state: disconnected");
+ break;
+ case RTCIceConnectionState::Closed:
+ STAMP_TIMECARD(mTimeCard, "Ice state: closed");
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unexpected mIceConnectionState!");
+ }
+
+ WrappableJSErrorResult rv;
+ mPCObserver->OnStateChange(PCObserverStateType::IceConnectionState, rv);
+ UpdateConnectionState();
+}
+
+void PeerConnectionImpl::OnCandidateFound(const std::string& aTransportId,
+ const CandidateInfo& aCandidateInfo) {
+ if (mStunAddrsRequest && !aCandidateInfo.mMDNSAddress.empty()) {
+ MOZ_ASSERT(!aCandidateInfo.mActualAddress.empty());
+
+ if (mCanRegisterMDNSHostnamesDirectly) {
+ auto itor = mRegisteredMDNSHostnames.find(aCandidateInfo.mMDNSAddress);
+
+ // We'll see the address twice if we're generating both UDP and TCP
+ // candidates.
+ if (itor == mRegisteredMDNSHostnames.end()) {
+ mRegisteredMDNSHostnames.insert(aCandidateInfo.mMDNSAddress);
+ mStunAddrsRequest->SendRegisterMDNSHostname(
+ nsCString(aCandidateInfo.mMDNSAddress.c_str()),
+ nsCString(aCandidateInfo.mActualAddress.c_str()));
+ }
+ } else {
+ mMDNSHostnamesToRegister.emplace(aCandidateInfo.mMDNSAddress,
+ aCandidateInfo.mActualAddress);
+ }
+ }
+
+ if (!aCandidateInfo.mDefaultHostRtp.empty()) {
+ UpdateDefaultCandidate(aCandidateInfo.mDefaultHostRtp,
+ aCandidateInfo.mDefaultPortRtp,
+ aCandidateInfo.mDefaultHostRtcp,
+ aCandidateInfo.mDefaultPortRtcp, aTransportId);
+ }
+ CandidateReady(aCandidateInfo.mCandidate, aTransportId,
+ aCandidateInfo.mUfrag);
+}
+
+void PeerConnectionImpl::IceGatheringStateChange(
+ dom::RTCIceGatheringState state) {
+ PC_AUTO_ENTER_API_CALL_VOID_RETURN(false);
+
+ CSFLogDebug(LOGTAG, "%s %d", __FUNCTION__, static_cast<int>(state));
+ if (mIceGatheringState == state) {
+ return;
+ }
+
+ mIceGatheringState = state;
+
+ // Would be nice if we had a means of converting one of these dom enums
+ // to a string that wasn't almost as much text as this switch statement...
+ switch (mIceGatheringState) {
+ case RTCIceGatheringState::New:
+ STAMP_TIMECARD(mTimeCard, "Ice gathering state: new");
+ break;
+ case RTCIceGatheringState::Gathering:
+ STAMP_TIMECARD(mTimeCard, "Ice gathering state: gathering");
+ break;
+ case RTCIceGatheringState::Complete:
+ STAMP_TIMECARD(mTimeCard, "Ice gathering state: complete");
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unexpected mIceGatheringState!");
+ }
+
+ JSErrorResult rv;
+ mPCObserver->OnStateChange(PCObserverStateType::IceGatheringState, rv);
+}
+
+void PeerConnectionImpl::UpdateDefaultCandidate(
+ const std::string& defaultAddr, uint16_t defaultPort,
+ const std::string& defaultRtcpAddr, uint16_t defaultRtcpPort,
+ const std::string& transportId) {
+ CSFLogDebug(LOGTAG, "%s", __FUNCTION__);
+ mJsepSession->UpdateDefaultCandidate(
+ defaultAddr, defaultPort, defaultRtcpAddr, defaultRtcpPort, transportId);
+ if (mUncommittedJsepSession) {
+ mUncommittedJsepSession->UpdateDefaultCandidate(
+ defaultAddr, defaultPort, defaultRtcpAddr, defaultRtcpPort,
+ transportId);
+ }
+}
+
+static UniquePtr<dom::RTCStatsCollection> GetDataChannelStats_s(
+ const RefPtr<DataChannelConnection>& aDataConnection,
+ const DOMHighResTimeStamp aTimestamp) {
+ UniquePtr<dom::RTCStatsCollection> report(new dom::RTCStatsCollection);
+ if (aDataConnection) {
+ aDataConnection->AppendStatsToReport(report, aTimestamp);
+ }
+ return report;
+}
+
+RefPtr<dom::RTCStatsPromise> PeerConnectionImpl::GetDataChannelStats(
+ const RefPtr<DataChannelConnection>& aDataChannelConnection,
+ const DOMHighResTimeStamp aTimestamp) {
+ // Gather stats from DataChannels
+ return InvokeAsync(
+ GetMainThreadSerialEventTarget(), __func__,
+ [aDataChannelConnection, aTimestamp]() {
+ return dom::RTCStatsPromise::CreateAndResolve(
+ GetDataChannelStats_s(aDataChannelConnection, aTimestamp),
+ __func__);
+ });
+}
+
+void PeerConnectionImpl::CollectConduitTelemetryData() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsTArray<RefPtr<VideoSessionConduit>> conduits;
+ for (const auto& transceiver : mTransceivers) {
+ if (RefPtr<MediaSessionConduit> conduit = transceiver->GetConduit()) {
+ conduit->AsVideoSessionConduit().apply(
+ [&](const auto& aVideo) { conduits.AppendElement(aVideo); });
+ }
+ }
+
+ if (!conduits.IsEmpty() && mCall) {
+ mCall->mCallThread->Dispatch(
+ NS_NewRunnableFunction(__func__, [conduits = std::move(conduits)] {
+ for (const auto& conduit : conduits) {
+ conduit->CollectTelemetryData();
+ }
+ }));
+ }
+}
+
+nsTArray<dom::RTCCodecStats> PeerConnectionImpl::GetCodecStats(
+ DOMHighResTimeStamp aNow) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsTArray<dom::RTCCodecStats> result;
+
+ struct CodecComparator {
+ bool operator()(const JsepCodecDescription* aA,
+ const JsepCodecDescription* aB) const {
+ return aA->StatsId() < aB->StatsId();
+ }
+ };
+
+ // transportId -> codec; per direction (whether the codecType
+ // shall be "encode", "decode" or absent (if a codec exists in both maps for a
+ // transport)). These do the bookkeeping to ensure codec stats get coalesced
+ // to transport level.
+ std::map<std::string, std::set<JsepCodecDescription*, CodecComparator>>
+ sendCodecMap;
+ std::map<std::string, std::set<JsepCodecDescription*, CodecComparator>>
+ recvCodecMap;
+
+ // Find all JsepCodecDescription instances we want to turn into codec stats.
+ for (const auto& transceiver : mTransceivers) {
+ // TODO: Grab these from the JSEP transceivers instead
+ auto sendCodecs = transceiver->GetNegotiatedSendCodecs();
+ auto recvCodecs = transceiver->GetNegotiatedRecvCodecs();
+
+ const std::string transportId = transceiver->GetTransportId();
+ // This ensures both codec maps have the same size.
+ auto& sendMap = sendCodecMap[transportId];
+ auto& recvMap = recvCodecMap[transportId];
+
+ sendCodecs.apply([&](const auto& aCodecs) {
+ for (const auto& codec : aCodecs) {
+ sendMap.insert(codec.get());
+ }
+ });
+ recvCodecs.apply([&](const auto& aCodecs) {
+ for (const auto& codec : aCodecs) {
+ recvMap.insert(codec.get());
+ }
+ });
+ }
+
+ auto createCodecStat = [&](const JsepCodecDescription* aCodec,
+ const nsString& aTransportId,
+ Maybe<RTCCodecType> aCodecType) {
+ uint16_t pt;
+ {
+ DebugOnly<bool> rv = aCodec->GetPtAsInt(&pt);
+ MOZ_ASSERT(rv);
+ }
+ nsString mimeType;
+ mimeType.AppendPrintf(
+ "%s/%s", aCodec->Type() == SdpMediaSection::kVideo ? "video" : "audio",
+ aCodec->mName.c_str());
+ nsString id = aTransportId;
+ id.Append(u"_");
+ id.Append(aCodec->StatsId());
+
+ dom::RTCCodecStats codec;
+ codec.mId.Construct(std::move(id));
+ codec.mTimestamp.Construct(aNow);
+ codec.mType.Construct(RTCStatsType::Codec);
+ codec.mPayloadType = pt;
+ if (aCodecType) {
+ codec.mCodecType.Construct(*aCodecType);
+ }
+ codec.mTransportId = aTransportId;
+ codec.mMimeType = std::move(mimeType);
+ codec.mClockRate.Construct(aCodec->mClock);
+ if (aCodec->Type() == SdpMediaSection::MediaType::kAudio) {
+ codec.mChannels.Construct(aCodec->mChannels);
+ }
+ if (aCodec->mSdpFmtpLine) {
+ codec.mSdpFmtpLine.Construct(
+ NS_ConvertUTF8toUTF16(aCodec->mSdpFmtpLine->c_str()));
+ }
+
+ result.AppendElement(std::move(codec));
+ };
+
+ // Create codec stats for the gathered codec descriptions, sorted primarily
+ // by transportId, secondarily by payload type (from StatsId()).
+ for (const auto& [transportId, sendCodecs] : sendCodecMap) {
+ const auto& recvCodecs = recvCodecMap[transportId];
+ const nsString tid = NS_ConvertASCIItoUTF16(transportId);
+ AutoTArray<JsepCodecDescription*, 16> bidirectionalCodecs;
+ AutoTArray<JsepCodecDescription*, 16> unidirectionalCodecs;
+ std::set_intersection(sendCodecs.cbegin(), sendCodecs.cend(),
+ recvCodecs.cbegin(), recvCodecs.cend(),
+ MakeBackInserter(bidirectionalCodecs),
+ CodecComparator());
+ std::set_symmetric_difference(sendCodecs.cbegin(), sendCodecs.cend(),
+ recvCodecs.cbegin(), recvCodecs.cend(),
+ MakeBackInserter(unidirectionalCodecs),
+ CodecComparator());
+ for (const auto* codec : bidirectionalCodecs) {
+ createCodecStat(codec, tid, Nothing());
+ }
+ for (const auto* codec : unidirectionalCodecs) {
+ createCodecStat(
+ codec, tid,
+ Some(codec->mDirection == sdp::kSend ? RTCCodecType::Encode
+ : RTCCodecType::Decode));
+ }
+ }
+
+ return result;
+}
+
+RefPtr<dom::RTCStatsReportPromise> PeerConnectionImpl::GetStats(
+ dom::MediaStreamTrack* aSelector, bool aInternalStats) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mFinalStatsQuery) {
+ // This case should be _extremely_ rare; this will basically only happen
+ // when WebrtcGlobalInformation tries to get our stats while we are tearing
+ // down.
+ return mFinalStatsQuery->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [this, self = RefPtr<PeerConnectionImpl>(this)]() {
+ UniquePtr<dom::RTCStatsReportInternal> finalStats =
+ MakeUnique<dom::RTCStatsReportInternal>();
+ // Might not be set if this encountered some error.
+ if (mFinalStats) {
+ *finalStats = *mFinalStats;
+ }
+ return RTCStatsReportPromise::CreateAndResolve(std::move(finalStats),
+ __func__);
+ });
+ }
+
+ nsTArray<RefPtr<dom::RTCStatsPromise>> promises;
+ DOMHighResTimeStamp now = mTimestampMaker.GetNow().ToDom();
+
+ nsTArray<dom::RTCCodecStats> codecStats = GetCodecStats(now);
+ std::set<std::string> transportIds;
+
+ if (!aSelector) {
+ // There might not be any senders/receivers if we're DataChannel only, so we
+ // don't handle the null selector case in the loop below.
+ transportIds.insert("");
+ }
+
+ nsTArray<
+ std::tuple<RTCRtpTransceiver*, RefPtr<RTCStatsPromise::AllPromiseType>>>
+ transceiverStatsPromises;
+ for (const auto& transceiver : mTransceivers) {
+ const bool sendSelected = transceiver->Sender()->HasTrack(aSelector);
+ const bool recvSelected = transceiver->Receiver()->HasTrack(aSelector);
+ if (!sendSelected && !recvSelected) {
+ continue;
+ }
+
+ if (aSelector) {
+ transportIds.insert(transceiver->GetTransportId());
+ }
+
+ nsTArray<RefPtr<RTCStatsPromise>> rtpStreamPromises;
+ // Get all rtp stream stats for the given selector. Then filter away any
+ // codec stat not related to the selector, and assign codec ids to the
+ // stream stats.
+ // Skips the ICE stats; we do our own queries based on |transportIds| to
+ // avoid duplicates
+ if (sendSelected) {
+ rtpStreamPromises.AppendElements(
+ transceiver->Sender()->GetStatsInternal(true));
+ }
+ if (recvSelected) {
+ rtpStreamPromises.AppendElements(
+ transceiver->Receiver()->GetStatsInternal(true));
+ }
+ transceiverStatsPromises.AppendElement(
+ std::make_tuple(transceiver.get(),
+ RTCStatsPromise::All(GetMainThreadSerialEventTarget(),
+ rtpStreamPromises)));
+ }
+
+ promises.AppendElement(RTCRtpTransceiver::ApplyCodecStats(
+ std::move(codecStats), std::move(transceiverStatsPromises)));
+
+ for (const auto& transportId : transportIds) {
+ promises.AppendElement(mTransportHandler->GetIceStats(transportId, now));
+ }
+
+ promises.AppendElement(GetDataChannelStats(mDataConnection, now));
+
+ auto pcStatsCollection = MakeUnique<dom::RTCStatsCollection>();
+ RTCPeerConnectionStats pcStats;
+ pcStats.mTimestamp.Construct(now);
+ pcStats.mType.Construct(RTCStatsType::Peer_connection);
+ pcStats.mId.Construct(NS_ConvertUTF8toUTF16(mHandle.c_str()));
+ pcStats.mDataChannelsOpened.Construct(mDataChannelsOpened);
+ pcStats.mDataChannelsClosed.Construct(mDataChannelsClosed);
+ if (!pcStatsCollection->mPeerConnectionStats.AppendElement(std::move(pcStats),
+ fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ promises.AppendElement(RTCStatsPromise::CreateAndResolve(
+ std::move(pcStatsCollection), __func__));
+
+ // This is what we're going to return; all the stuff in |promises| will be
+ // accumulated here.
+ UniquePtr<dom::RTCStatsReportInternal> report(
+ new dom::RTCStatsReportInternal);
+ report->mPcid = NS_ConvertASCIItoUTF16(mName.c_str());
+ if (mWindow && mWindow->GetBrowsingContext()) {
+ report->mBrowserId = mWindow->GetBrowsingContext()->BrowserId();
+ }
+ report->mConfiguration.Construct(mJsConfiguration);
+ // TODO(bug 1589416): We need to do better here.
+ if (!mIceStartTime.IsNull()) {
+ report->mCallDurationMs.Construct(
+ (TimeStamp::Now() - mIceStartTime).ToMilliseconds());
+ }
+ report->mIceRestarts = mIceRestartCount;
+ report->mIceRollbacks = mIceRollbackCount;
+ report->mClosed = false;
+ report->mTimestamp = now;
+
+ if (aInternalStats && mJsepSession) {
+ for (const auto& candidate : mRawTrickledCandidates) {
+ if (!report->mRawRemoteCandidates.AppendElement(
+ NS_ConvertASCIItoUTF16(candidate.c_str()), fallible)) {
+ // XXX(Bug 1632090) Instead of extending the array 1-by-1 (which might
+ // involve multiple reallocations) and potentially crashing here,
+ // SetCapacity could be called outside the loop once.
+ mozalloc_handle_oom(0);
+ }
+ }
+
+ if (mJsepSession) {
+ // TODO we probably should report Current and Pending SDPs here
+ // separately. Plus the raw SDP we got from JS (mLocalRequestedSDP).
+ // And if it's the offer or answer would also be nice.
+ std::string localDescription =
+ mJsepSession->GetLocalDescription(kJsepDescriptionPendingOrCurrent);
+ std::string remoteDescription =
+ mJsepSession->GetRemoteDescription(kJsepDescriptionPendingOrCurrent);
+ if (!report->mSdpHistory.AppendElements(mSdpHistory, fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ if (mJsepSession->IsPendingOfferer().isSome()) {
+ report->mOfferer.Construct(*mJsepSession->IsPendingOfferer());
+ } else if (mJsepSession->IsCurrentOfferer().isSome()) {
+ report->mOfferer.Construct(*mJsepSession->IsCurrentOfferer());
+ } else {
+ // Silly.
+ report->mOfferer.Construct(false);
+ }
+ }
+ }
+
+ return dom::RTCStatsPromise::All(GetMainThreadSerialEventTarget(), promises)
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [report = std::move(report), idGen = mIdGenerator](
+ nsTArray<UniquePtr<dom::RTCStatsCollection>> aStats) mutable {
+ idGen->RewriteIds(std::move(aStats), report.get());
+ return dom::RTCStatsReportPromise::CreateAndResolve(
+ std::move(report), __func__);
+ },
+ [](nsresult rv) {
+ return dom::RTCStatsReportPromise::CreateAndReject(rv, __func__);
+ });
+}
+
+void PeerConnectionImpl::RecordIceRestartStatistics(JsepSdpType type) {
+ switch (type) {
+ case mozilla::kJsepSdpOffer:
+ case mozilla::kJsepSdpPranswer:
+ break;
+ case mozilla::kJsepSdpAnswer:
+ ++mIceRestartCount;
+ break;
+ case mozilla::kJsepSdpRollback:
+ ++mIceRollbackCount;
+ break;
+ }
+}
+
+void PeerConnectionImpl::StoreConfigurationForAboutWebrtc(
+ const dom::RTCConfiguration& aConfig) {
+ // This will only be called once, when the PeerConnection is initially
+ // configured, at least until setConfiguration is implemented
+ // see https://bugzilla.mozilla.org/show_bug.cgi?id=1253706
+ // @TODO bug 1739451 call this from setConfiguration
+ mJsConfiguration.mIceServers.Clear();
+ for (const auto& server : aConfig.mIceServers) {
+ RTCIceServerInternal internal;
+ internal.mCredentialProvided = server.mCredential.WasPassed();
+ internal.mUserNameProvided = server.mUsername.WasPassed();
+ if (server.mUrl.WasPassed()) {
+ if (!internal.mUrls.AppendElement(server.mUrl.Value(), fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ }
+ if (server.mUrls.WasPassed()) {
+ for (const auto& url : server.mUrls.Value().GetAsStringSequence()) {
+ if (!internal.mUrls.AppendElement(url, fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ }
+ }
+ if (!mJsConfiguration.mIceServers.AppendElement(internal, fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ }
+ mJsConfiguration.mSdpSemantics.Reset();
+ if (aConfig.mSdpSemantics.WasPassed()) {
+ mJsConfiguration.mSdpSemantics.Construct(aConfig.mSdpSemantics.Value());
+ }
+
+ mJsConfiguration.mIceTransportPolicy.Reset();
+ mJsConfiguration.mIceTransportPolicy.Construct(aConfig.mIceTransportPolicy);
+ mJsConfiguration.mBundlePolicy.Reset();
+ mJsConfiguration.mBundlePolicy.Construct(aConfig.mBundlePolicy);
+ mJsConfiguration.mPeerIdentityProvided = !aConfig.mPeerIdentity.IsEmpty();
+ mJsConfiguration.mCertificatesProvided = !aConfig.mCertificates.Length();
+}
+
+dom::Sequence<dom::RTCSdpParsingErrorInternal>
+PeerConnectionImpl::GetLastSdpParsingErrors() const {
+ const auto& sdpErrors = mJsepSession->GetLastSdpParsingErrors();
+ dom::Sequence<dom::RTCSdpParsingErrorInternal> domErrors;
+ if (!domErrors.SetCapacity(domErrors.Length(), fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ for (const auto& error : sdpErrors) {
+ mozilla::dom::RTCSdpParsingErrorInternal internal;
+ internal.mLineNumber = error.first;
+ if (!AppendASCIItoUTF16(MakeStringSpan(error.second.c_str()),
+ internal.mError, fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ if (!domErrors.AppendElement(std::move(internal), fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ }
+ return domErrors;
+}
+
+// Telemetry for when calls start
+void PeerConnectionImpl::StartCallTelem() {
+ if (mCallTelemStarted) {
+ return;
+ }
+ MOZ_RELEASE_ASSERT(mWindow);
+ uint64_t windowId = mWindow->WindowID();
+ auto found = sCallDurationTimers.find(windowId);
+ if (found == sCallDurationTimers.end()) {
+ found =
+ sCallDurationTimers.emplace(windowId, PeerConnectionAutoTimer()).first;
+ }
+ found->second.RegisterConnection();
+ mCallTelemStarted = true;
+
+ // Increment session call counter
+ // If we want to track Loop calls independently here, we need two
+ // histograms.
+ //
+ // NOTE: As of bug 1654248 landing we are no longer counting renegotiations
+ // as separate calls. Expect numbers to drop compared to
+ // WEBRTC_CALL_COUNT_2.
+ Telemetry::Accumulate(Telemetry::WEBRTC_CALL_COUNT_3, 1);
+}
+
+void PeerConnectionImpl::StunAddrsHandler::OnMDNSQueryComplete(
+ const nsCString& hostname, const Maybe<nsCString>& address) {
+ MOZ_ASSERT(NS_IsMainThread());
+ PeerConnectionWrapper pcw(mPcHandle);
+ if (!pcw.impl()) {
+ return;
+ }
+ auto itor = pcw.impl()->mQueriedMDNSHostnames.find(hostname.BeginReading());
+ if (itor != pcw.impl()->mQueriedMDNSHostnames.end()) {
+ if (address) {
+ for (auto& cand : itor->second) {
+ // Replace obfuscated address with actual address
+ std::string obfuscatedAddr = cand.mTokenizedCandidate[4];
+ cand.mTokenizedCandidate[4] = address->BeginReading();
+ std::ostringstream o;
+ for (size_t i = 0; i < cand.mTokenizedCandidate.size(); ++i) {
+ o << cand.mTokenizedCandidate[i];
+ if (i + 1 != cand.mTokenizedCandidate.size()) {
+ o << " ";
+ }
+ }
+ std::string mungedCandidate = o.str();
+ pcw.impl()->StampTimecard("Done looking up mDNS name");
+ pcw.impl()->mTransportHandler->AddIceCandidate(
+ cand.mTransportId, mungedCandidate, cand.mUfrag, obfuscatedAddr);
+ }
+ } else {
+ pcw.impl()->StampTimecard("Failed looking up mDNS name");
+ }
+ pcw.impl()->mQueriedMDNSHostnames.erase(itor);
+ }
+}
+
+void PeerConnectionImpl::StunAddrsHandler::OnStunAddrsAvailable(
+ const mozilla::net::NrIceStunAddrArray& addrs) {
+ CSFLogInfo(LOGTAG, "%s: receiving (%d) stun addrs", __FUNCTION__,
+ (int)addrs.Length());
+ PeerConnectionWrapper pcw(mPcHandle);
+ if (!pcw.impl()) {
+ return;
+ }
+ pcw.impl()->mStunAddrs = addrs.Clone();
+ pcw.impl()->mLocalAddrsRequestState = STUN_ADDR_REQUEST_COMPLETE;
+ pcw.impl()->FlushIceCtxOperationQueueIfReady();
+ // If parent process returns 0 STUN addresses, change ICE connection
+ // state to failed.
+ if (!pcw.impl()->mStunAddrs.Length()) {
+ pcw.impl()->IceConnectionStateChange(dom::RTCIceConnectionState::Failed);
+ }
+}
+
+void PeerConnectionImpl::InitLocalAddrs() {
+ if (mLocalAddrsRequestState == STUN_ADDR_REQUEST_PENDING) {
+ return;
+ }
+ if (mStunAddrsRequest) {
+ mLocalAddrsRequestState = STUN_ADDR_REQUEST_PENDING;
+ mStunAddrsRequest->SendGetStunAddrs();
+ } else {
+ mLocalAddrsRequestState = STUN_ADDR_REQUEST_COMPLETE;
+ }
+}
+
+bool PeerConnectionImpl::ShouldForceProxy() const {
+ if (Preferences::GetBool("media.peerconnection.ice.proxy_only", false)) {
+ return true;
+ }
+
+ bool isPBM = false;
+ // This complicated null check is being extra conservative to avoid
+ // introducing crashes. It may not be needed.
+ if (mWindow && mWindow->GetExtantDoc() &&
+ mWindow->GetExtantDoc()->GetPrincipal() &&
+ mWindow->GetExtantDoc()
+ ->GetPrincipal()
+ ->OriginAttributesRef()
+ .mPrivateBrowsingId > 0) {
+ isPBM = true;
+ }
+
+ if (isPBM && Preferences::GetBool(
+ "media.peerconnection.ice.proxy_only_if_pbmode", false)) {
+ return true;
+ }
+
+ if (!Preferences::GetBool(
+ "media.peerconnection.ice.proxy_only_if_behind_proxy", false)) {
+ return false;
+ }
+
+ // Ok, we're supposed to be proxy_only, but only if a proxy is configured.
+ // Let's just see if the document was loaded via a proxy.
+
+ nsCOMPtr<nsIHttpChannelInternal> httpChannelInternal = GetChannel();
+ if (!httpChannelInternal) {
+ return false;
+ }
+
+ bool proxyUsed = false;
+ Unused << httpChannelInternal->GetIsProxyUsed(&proxyUsed);
+ return proxyUsed;
+}
+
+void PeerConnectionImpl::EnsureTransports(const JsepSession& aSession) {
+ mJsepSession->ForEachTransceiver([this,
+ self = RefPtr<PeerConnectionImpl>(this)](
+ const JsepTransceiver& transceiver) {
+ if (transceiver.HasOwnTransport()) {
+ mTransportHandler->EnsureProvisionalTransport(
+ transceiver.mTransport.mTransportId,
+ transceiver.mTransport.mLocalUfrag, transceiver.mTransport.mLocalPwd,
+ transceiver.mTransport.mComponents);
+ }
+ });
+
+ GatherIfReady();
+}
+
+void PeerConnectionImpl::UpdateRTCDtlsTransports(bool aMarkAsStable) {
+ mJsepSession->ForEachTransceiver(
+ [this, self = RefPtr<PeerConnectionImpl>(this)](
+ const JsepTransceiver& jsepTransceiver) {
+ std::string transportId = jsepTransceiver.mTransport.mTransportId;
+ if (transportId.empty()) {
+ return;
+ }
+ if (!mTransportIdToRTCDtlsTransport.count(transportId)) {
+ mTransportIdToRTCDtlsTransport.emplace(
+ transportId, new RTCDtlsTransport(GetParentObject()));
+ }
+ });
+
+ for (auto& transceiver : mTransceivers) {
+ std::string transportId = transceiver->GetTransportId();
+ if (transportId.empty()) {
+ continue;
+ }
+ if (mTransportIdToRTCDtlsTransport.count(transportId)) {
+ transceiver->SetDtlsTransport(mTransportIdToRTCDtlsTransport[transportId],
+ aMarkAsStable);
+ }
+ }
+
+ // Spec says we only update the RTCSctpTransport when negotiation completes
+}
+
+void PeerConnectionImpl::RollbackRTCDtlsTransports() {
+ for (auto& transceiver : mTransceivers) {
+ transceiver->RollbackToStableDtlsTransport();
+ }
+}
+
+void PeerConnectionImpl::RemoveRTCDtlsTransportsExcept(
+ const std::set<std::string>& aTransportIds) {
+ for (auto iter = mTransportIdToRTCDtlsTransport.begin();
+ iter != mTransportIdToRTCDtlsTransport.end();) {
+ if (!aTransportIds.count(iter->first)) {
+ iter = mTransportIdToRTCDtlsTransport.erase(iter);
+ } else {
+ ++iter;
+ }
+ }
+}
+
+nsresult PeerConnectionImpl::UpdateTransports(const JsepSession& aSession,
+ const bool forceIceTcp) {
+ std::set<std::string> finalTransports;
+ Maybe<std::string> sctpTransport;
+ mJsepSession->ForEachTransceiver(
+ [&, this, self = RefPtr<PeerConnectionImpl>(this)](
+ const JsepTransceiver& transceiver) {
+ if (transceiver.GetMediaType() == SdpMediaSection::kApplication &&
+ transceiver.HasTransport()) {
+ sctpTransport = Some(transceiver.mTransport.mTransportId);
+ }
+
+ if (transceiver.HasOwnTransport()) {
+ finalTransports.insert(transceiver.mTransport.mTransportId);
+ UpdateTransport(transceiver, forceIceTcp);
+ }
+ });
+
+ // clean up the unused RTCDtlsTransports
+ RemoveRTCDtlsTransportsExcept(finalTransports);
+
+ mTransportHandler->RemoveTransportsExcept(finalTransports);
+
+ for (const auto& transceiverImpl : mTransceivers) {
+ transceiverImpl->UpdateTransport();
+ }
+
+ if (sctpTransport.isSome()) {
+ auto it = mTransportIdToRTCDtlsTransport.find(*sctpTransport);
+ if (it == mTransportIdToRTCDtlsTransport.end()) {
+ // What?
+ MOZ_ASSERT(false);
+ return NS_ERROR_FAILURE;
+ }
+ if (!mDataConnection) {
+ // What?
+ MOZ_ASSERT(false);
+ return NS_ERROR_FAILURE;
+ }
+ RefPtr<RTCDtlsTransport> dtlsTransport = it->second;
+ // Why on earth does the spec use a floating point for this?
+ double maxMessageSize =
+ static_cast<double>(mDataConnection->GetMaxMessageSize());
+ Nullable<uint16_t> maxChannels;
+
+ if (!mSctpTransport) {
+ mSctpTransport = new RTCSctpTransport(GetParentObject(), *dtlsTransport,
+ maxMessageSize, maxChannels);
+ } else {
+ mSctpTransport->SetTransport(*dtlsTransport);
+ mSctpTransport->SetMaxMessageSize(maxMessageSize);
+ mSctpTransport->SetMaxChannels(maxChannels);
+ }
+ } else {
+ mSctpTransport = nullptr;
+ }
+
+ return NS_OK;
+}
+
+void PeerConnectionImpl::UpdateTransport(const JsepTransceiver& aTransceiver,
+ bool aForceIceTcp) {
+ std::string ufrag;
+ std::string pwd;
+ std::vector<std::string> candidates;
+ size_t components = 0;
+
+ const JsepTransport& transport = aTransceiver.mTransport;
+ unsigned level = aTransceiver.GetLevel();
+
+ CSFLogDebug(LOGTAG, "ACTIVATING TRANSPORT! - PC %s: level=%u components=%u",
+ mHandle.c_str(), (unsigned)level,
+ (unsigned)transport.mComponents);
+
+ ufrag = transport.mIce->GetUfrag();
+ pwd = transport.mIce->GetPassword();
+ candidates = transport.mIce->GetCandidates();
+ components = transport.mComponents;
+ if (aForceIceTcp) {
+ candidates.erase(
+ std::remove_if(candidates.begin(), candidates.end(),
+ [](const std::string& s) {
+ return s.find(" UDP ") != std::string::npos ||
+ s.find(" udp ") != std::string::npos;
+ }),
+ candidates.end());
+ }
+
+ nsTArray<uint8_t> keyDer;
+ nsTArray<uint8_t> certDer;
+ nsresult rv = Identity()->Serialize(&keyDer, &certDer);
+ if (NS_FAILED(rv)) {
+ CSFLogError(LOGTAG, "%s: Failed to serialize DTLS identity: %d",
+ __FUNCTION__, (int)rv);
+ return;
+ }
+
+ DtlsDigestList digests;
+ for (const auto& fingerprint :
+ transport.mDtls->GetFingerprints().mFingerprints) {
+ std::ostringstream ss;
+ ss << fingerprint.hashFunc;
+ digests.emplace_back(ss.str(), fingerprint.fingerprint);
+ }
+
+ mTransportHandler->ActivateTransport(
+ transport.mTransportId, transport.mLocalUfrag, transport.mLocalPwd,
+ components, ufrag, pwd, keyDer, certDer, Identity()->auth_type(),
+ transport.mDtls->GetRole() == JsepDtlsTransport::kJsepDtlsClient, digests,
+ PrivacyRequested());
+
+ for (auto& candidate : candidates) {
+ AddIceCandidate("candidate:" + candidate, transport.mTransportId, ufrag);
+ }
+}
+
+nsresult PeerConnectionImpl::UpdateMediaPipelines() {
+ for (RefPtr<RTCRtpTransceiver>& transceiver : mTransceivers) {
+ transceiver->ResetSync();
+ }
+
+ for (RefPtr<RTCRtpTransceiver>& transceiver : mTransceivers) {
+ if (!transceiver->IsVideo()) {
+ nsresult rv = transceiver->SyncWithMatchingVideoConduits(mTransceivers);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ transceiver->UpdatePrincipalPrivacy(PrivacyRequested()
+ ? PrincipalPrivacy::Private
+ : PrincipalPrivacy::NonPrivate);
+
+ nsresult rv = transceiver->UpdateConduit();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+void PeerConnectionImpl::StartIceChecks(const JsepSession& aSession) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mCanRegisterMDNSHostnamesDirectly) {
+ for (auto& pair : mMDNSHostnamesToRegister) {
+ mRegisteredMDNSHostnames.insert(pair.first);
+ mStunAddrsRequest->SendRegisterMDNSHostname(
+ nsCString(pair.first.c_str()), nsCString(pair.second.c_str()));
+ }
+ mMDNSHostnamesToRegister.clear();
+ mCanRegisterMDNSHostnamesDirectly = true;
+ }
+
+ std::vector<std::string> attributes;
+ if (aSession.RemoteIsIceLite()) {
+ attributes.push_back("ice-lite");
+ }
+
+ if (!aSession.GetIceOptions().empty()) {
+ attributes.push_back("ice-options:");
+ for (const auto& option : aSession.GetIceOptions()) {
+ attributes.back() += option + ' ';
+ }
+ }
+
+ nsCOMPtr<nsIRunnable> runnable(
+ WrapRunnable(mTransportHandler, &MediaTransportHandler::StartIceChecks,
+ aSession.IsIceControlling(), attributes));
+
+ PerformOrEnqueueIceCtxOperation(runnable);
+}
+
+bool PeerConnectionImpl::GetPrefDefaultAddressOnly() const {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ uint64_t winId = mWindow->WindowID();
+
+ bool default_address_only = Preferences::GetBool(
+ "media.peerconnection.ice.default_address_only", false);
+ default_address_only |=
+ !MediaManager::Get()->IsActivelyCapturingOrHasAPermission(winId);
+ return default_address_only;
+}
+
+bool PeerConnectionImpl::GetPrefObfuscateHostAddresses() const {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ uint64_t winId = mWindow->WindowID();
+
+ bool obfuscate_host_addresses = Preferences::GetBool(
+ "media.peerconnection.ice.obfuscate_host_addresses", false);
+ obfuscate_host_addresses &=
+ !MediaManager::Get()->IsActivelyCapturingOrHasAPermission(winId);
+ obfuscate_host_addresses &= !PeerConnectionImpl::HostnameInPref(
+ "media.peerconnection.ice.obfuscate_host_addresses.blocklist", mHostname);
+ obfuscate_host_addresses &= XRE_IsContentProcess();
+
+ return obfuscate_host_addresses;
+}
+
+PeerConnectionImpl::SignalHandler::SignalHandler(PeerConnectionImpl* aPc,
+ MediaTransportHandler* aSource)
+ : mHandle(aPc->GetHandle()),
+ mSource(aSource),
+ mSTSThread(aPc->GetSTSThread()) {
+ ConnectSignals();
+}
+
+PeerConnectionImpl::SignalHandler::~SignalHandler() {
+ ASSERT_ON_THREAD(mSTSThread);
+}
+
+void PeerConnectionImpl::SignalHandler::ConnectSignals() {
+ mSource->SignalGatheringStateChange.connect(
+ this, &PeerConnectionImpl::SignalHandler::IceGatheringStateChange_s);
+ mSource->SignalConnectionStateChange.connect(
+ this, &PeerConnectionImpl::SignalHandler::IceConnectionStateChange_s);
+ mSource->SignalCandidate.connect(
+ this, &PeerConnectionImpl::SignalHandler::OnCandidateFound_s);
+ mSource->SignalAlpnNegotiated.connect(
+ this, &PeerConnectionImpl::SignalHandler::AlpnNegotiated_s);
+ mSource->SignalStateChange.connect(
+ this, &PeerConnectionImpl::SignalHandler::ConnectionStateChange_s);
+ mSource->SignalRtcpStateChange.connect(
+ this, &PeerConnectionImpl::SignalHandler::ConnectionStateChange_s);
+}
+
+void PeerConnectionImpl::AddIceCandidate(const std::string& aCandidate,
+ const std::string& aTransportId,
+ const std::string& aUfrag) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!aTransportId.empty());
+
+ bool obfuscate_host_addresses = Preferences::GetBool(
+ "media.peerconnection.ice.obfuscate_host_addresses", false);
+
+ if (obfuscate_host_addresses && !RelayOnly()) {
+ std::vector<std::string> tokens;
+ TokenizeCandidate(aCandidate, tokens);
+
+ if (tokens.size() > 4) {
+ std::string addr = tokens[4];
+
+ // Check for address ending with .local
+ size_t nPeriods = std::count(addr.begin(), addr.end(), '.');
+ size_t dotLocalLength = 6; // length of ".local"
+
+ if (nPeriods == 1 &&
+ addr.rfind(".local") + dotLocalLength == addr.length()) {
+ if (mStunAddrsRequest) {
+ PendingIceCandidate cand;
+ cand.mTokenizedCandidate = std::move(tokens);
+ cand.mTransportId = aTransportId;
+ cand.mUfrag = aUfrag;
+ mQueriedMDNSHostnames[addr].push_back(cand);
+
+ GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction(
+ "PeerConnectionImpl::SendQueryMDNSHostname",
+ [self = RefPtr<PeerConnectionImpl>(this), addr]() mutable {
+ if (self->mStunAddrsRequest) {
+ self->StampTimecard("Look up mDNS name");
+ self->mStunAddrsRequest->SendQueryMDNSHostname(
+ nsCString(nsAutoCString(addr.c_str())));
+ }
+ NS_ReleaseOnMainThread(
+ "PeerConnectionImpl::SendQueryMDNSHostname", self.forget());
+ }));
+ }
+ // TODO: Bug 1535690, we don't want to tell the ICE context that remote
+ // trickle is done if we are waiting to resolve a mDNS candidate.
+ return;
+ }
+ }
+ }
+
+ mTransportHandler->AddIceCandidate(aTransportId, aCandidate, aUfrag, "");
+}
+
+void PeerConnectionImpl::UpdateNetworkState(bool online) {
+ if (mTransportHandler) {
+ mTransportHandler->UpdateNetworkState(online);
+ }
+}
+
+void PeerConnectionImpl::FlushIceCtxOperationQueueIfReady() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (IsIceCtxReady()) {
+ for (auto& queuedIceCtxOperation : mQueuedIceCtxOperations) {
+ queuedIceCtxOperation->Run();
+ }
+ mQueuedIceCtxOperations.clear();
+ }
+}
+
+void PeerConnectionImpl::PerformOrEnqueueIceCtxOperation(
+ nsIRunnable* runnable) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (IsIceCtxReady()) {
+ runnable->Run();
+ } else {
+ mQueuedIceCtxOperations.push_back(runnable);
+ }
+}
+
+void PeerConnectionImpl::GatherIfReady() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Init local addrs here so that if we re-gather after an ICE restart
+ // resulting from changing WiFi networks, we get new local addrs.
+ // Otherwise, we would reuse the addrs from the original WiFi network
+ // and the ICE restart will fail.
+ if (!mStunAddrs.Length()) {
+ InitLocalAddrs();
+ }
+
+ // If we had previously queued gathering or ICE start, unqueue them
+ mQueuedIceCtxOperations.clear();
+ nsCOMPtr<nsIRunnable> runnable(WrapRunnable(
+ RefPtr<PeerConnectionImpl>(this), &PeerConnectionImpl::EnsureIceGathering,
+ GetPrefDefaultAddressOnly(), GetPrefObfuscateHostAddresses()));
+
+ PerformOrEnqueueIceCtxOperation(runnable);
+}
+
+already_AddRefed<nsIHttpChannelInternal> PeerConnectionImpl::GetChannel()
+ const {
+ Document* doc = mWindow->GetExtantDoc();
+ if (NS_WARN_IF(!doc)) {
+ NS_WARNING("Unable to get document from window");
+ return nullptr;
+ }
+
+ if (!doc->GetDocumentURI()->SchemeIs("file")) {
+ nsIChannel* channel = doc->GetChannel();
+ if (!channel) {
+ NS_WARNING("Unable to get channel from document");
+ return nullptr;
+ }
+
+ nsCOMPtr<nsIHttpChannelInternal> httpChannelInternal =
+ do_QueryInterface(channel);
+ if (NS_WARN_IF(!httpChannelInternal)) {
+ CSFLogInfo(LOGTAG, "%s: Document does not have an HTTP channel",
+ __FUNCTION__);
+ return nullptr;
+ }
+ return httpChannelInternal.forget();
+ }
+ return nullptr;
+}
+
+nsresult PeerConnectionImpl::SetTargetForDefaultLocalAddressLookup() {
+ nsCOMPtr<nsIHttpChannelInternal> httpChannelInternal = GetChannel();
+ if (!httpChannelInternal) {
+ return NS_OK;
+ }
+
+ nsCString remoteIp;
+ nsresult rv = httpChannelInternal->GetRemoteAddress(remoteIp);
+ if (NS_FAILED(rv) || remoteIp.IsEmpty()) {
+ CSFLogError(LOGTAG, "%s: Failed to get remote IP address: %d", __FUNCTION__,
+ (int)rv);
+ return rv;
+ }
+
+ int32_t remotePort;
+ rv = httpChannelInternal->GetRemotePort(&remotePort);
+ if (NS_FAILED(rv)) {
+ CSFLogError(LOGTAG, "%s: Failed to get remote port number: %d",
+ __FUNCTION__, (int)rv);
+ return rv;
+ }
+
+ mTransportHandler->SetTargetForDefaultLocalAddressLookup(remoteIp.get(),
+ remotePort);
+
+ return NS_OK;
+}
+
+void PeerConnectionImpl::EnsureIceGathering(bool aDefaultRouteOnly,
+ bool aObfuscateHostAddresses) {
+ if (!mTargetForDefaultLocalAddressLookupIsSet) {
+ nsresult rv = SetTargetForDefaultLocalAddressLookup();
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Unable to set target for default local address lookup");
+ }
+ mTargetForDefaultLocalAddressLookupIsSet = true;
+ }
+
+ // Make sure we don't call StartIceGathering if we're in e10s mode
+ // and we received no STUN addresses from the parent process. In the
+ // absence of previously provided STUN addresses, StartIceGathering will
+ // attempt to gather them (as in non-e10s mode), and this will cause a
+ // sandboxing exception in e10s mode.
+ if (!mStunAddrs.Length() && XRE_IsContentProcess()) {
+ CSFLogInfo(LOGTAG, "%s: No STUN addresses returned from parent process",
+ __FUNCTION__);
+ return;
+ }
+
+ mTransportHandler->StartIceGathering(aDefaultRouteOnly,
+ aObfuscateHostAddresses, mStunAddrs);
+}
+
+already_AddRefed<dom::RTCRtpTransceiver> PeerConnectionImpl::CreateTransceiver(
+ const std::string& aId, bool aIsVideo, const RTCRtpTransceiverInit& aInit,
+ dom::MediaStreamTrack* aSendTrack, bool aAddTrackMagic, ErrorResult& aRv) {
+ PeerConnectionCtx* ctx = PeerConnectionCtx::GetInstance();
+ if (!mCall) {
+ mCall = WebrtcCallWrapper::Create(
+ GetTimestampMaker(),
+ media::ShutdownBlockingTicket::Create(
+ u"WebrtcCallWrapper shutdown blocker"_ns,
+ NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__),
+ ctx->GetSharedWebrtcState());
+ }
+
+ if (aAddTrackMagic) {
+ mJsepSession->ApplyToTransceiver(aId, [](JsepTransceiver& aTransceiver) {
+ aTransceiver.SetAddTrackMagic();
+ });
+ }
+
+ RefPtr<RTCRtpTransceiver> transceiver = new RTCRtpTransceiver(
+ mWindow, PrivacyRequested(), this, mTransportHandler, mJsepSession.get(),
+ aId, aIsVideo, mSTSThread.get(), aSendTrack, mCall.get(), mIdGenerator);
+
+ transceiver->Init(aInit, aRv);
+ if (aRv.Failed()) {
+ return nullptr;
+ }
+
+ if (aSendTrack) {
+ // implement checking for peerIdentity (where failure == black/silence)
+ Document* doc = mWindow->GetExtantDoc();
+ if (doc) {
+ transceiver->Sender()->GetPipeline()->UpdateSinkIdentity(
+ doc->NodePrincipal(), GetPeerIdentity());
+ } else {
+ MOZ_CRASH();
+ aRv = NS_ERROR_FAILURE;
+ return nullptr; // Don't remove this till we know it's safe.
+ }
+ }
+
+ return transceiver.forget();
+}
+
+std::string PeerConnectionImpl::GetTransportIdMatchingSendTrack(
+ const dom::MediaStreamTrack& aTrack) const {
+ for (const RefPtr<RTCRtpTransceiver>& transceiver : mTransceivers) {
+ if (transceiver->Sender()->HasTrack(&aTrack)) {
+ return transceiver->GetTransportId();
+ }
+ }
+ return std::string();
+}
+
+void PeerConnectionImpl::SignalHandler::IceGatheringStateChange_s(
+ dom::RTCIceGatheringState aState) {
+ ASSERT_ON_THREAD(mSTSThread);
+
+ GetMainThreadSerialEventTarget()->Dispatch(
+ NS_NewRunnableFunction(__func__,
+ [handle = mHandle, aState] {
+ PeerConnectionWrapper wrapper(handle);
+ if (wrapper.impl()) {
+ wrapper.impl()->IceGatheringStateChange(
+ aState);
+ }
+ }),
+ NS_DISPATCH_NORMAL);
+}
+
+void PeerConnectionImpl::SignalHandler::IceConnectionStateChange_s(
+ dom::RTCIceConnectionState aState) {
+ ASSERT_ON_THREAD(mSTSThread);
+
+ GetMainThreadSerialEventTarget()->Dispatch(
+ NS_NewRunnableFunction(__func__,
+ [handle = mHandle, aState] {
+ PeerConnectionWrapper wrapper(handle);
+ if (wrapper.impl()) {
+ wrapper.impl()->IceConnectionStateChange(
+ aState);
+ }
+ }),
+ NS_DISPATCH_NORMAL);
+}
+
+void PeerConnectionImpl::SignalHandler::OnCandidateFound_s(
+ const std::string& aTransportId, const CandidateInfo& aCandidateInfo) {
+ ASSERT_ON_THREAD(mSTSThread);
+ CSFLogDebug(LOGTAG, "%s: %s", __FUNCTION__, aTransportId.c_str());
+
+ MOZ_ASSERT(!aCandidateInfo.mUfrag.empty());
+
+ GetMainThreadSerialEventTarget()->Dispatch(
+ NS_NewRunnableFunction(__func__,
+ [handle = mHandle, aTransportId, aCandidateInfo] {
+ PeerConnectionWrapper wrapper(handle);
+ if (wrapper.impl()) {
+ wrapper.impl()->OnCandidateFound(
+ aTransportId, aCandidateInfo);
+ }
+ }),
+ NS_DISPATCH_NORMAL);
+}
+
+void PeerConnectionImpl::SignalHandler::AlpnNegotiated_s(
+ const std::string& aAlpn, bool aPrivacyRequested) {
+ MOZ_DIAGNOSTIC_ASSERT((aAlpn == "c-webrtc") == aPrivacyRequested);
+ GetMainThreadSerialEventTarget()->Dispatch(
+ NS_NewRunnableFunction(__func__,
+ [handle = mHandle, aPrivacyRequested] {
+ PeerConnectionWrapper wrapper(handle);
+ if (wrapper.impl()) {
+ wrapper.impl()->OnAlpnNegotiated(
+ aPrivacyRequested);
+ }
+ }),
+ NS_DISPATCH_NORMAL);
+}
+
+void PeerConnectionImpl::SignalHandler::ConnectionStateChange_s(
+ const std::string& aTransportId, TransportLayer::State aState) {
+ GetMainThreadSerialEventTarget()->Dispatch(
+ NS_NewRunnableFunction(__func__,
+ [handle = mHandle, aTransportId, aState] {
+ PeerConnectionWrapper wrapper(handle);
+ if (wrapper.impl()) {
+ wrapper.impl()->OnDtlsStateChange(aTransportId,
+ aState);
+ }
+ }),
+ NS_DISPATCH_NORMAL);
+}
+
+/**
+ * Tells you if any local track is isolated to a specific peer identity.
+ * Obviously, we want all the tracks to be isolated equally so that they can
+ * all be sent or not. We check once when we are setting a local description
+ * and that determines if we flip the "privacy requested" bit on. Once the bit
+ * is on, all media originating from this peer connection is isolated.
+ *
+ * @returns true if any track has a peerIdentity set on it
+ */
+bool PeerConnectionImpl::AnyLocalTrackHasPeerIdentity() const {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ for (const RefPtr<RTCRtpTransceiver>& transceiver : mTransceivers) {
+ if (transceiver->Sender()->GetTrack() &&
+ transceiver->Sender()->GetTrack()->GetPeerIdentity()) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool PeerConnectionImpl::AnyCodecHasPluginID(uint64_t aPluginID) {
+ for (RefPtr<RTCRtpTransceiver>& transceiver : mTransceivers) {
+ if (transceiver->ConduitHasPluginID(aPluginID)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+std::unique_ptr<NrSocketProxyConfig> PeerConnectionImpl::GetProxyConfig()
+ const {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mForceProxy &&
+ Preferences::GetBool("media.peerconnection.disable_http_proxy", false)) {
+ return nullptr;
+ }
+
+ nsCString alpn = "webrtc,c-webrtc"_ns;
+ auto* browserChild = BrowserChild::GetFrom(mWindow);
+ if (!browserChild) {
+ // Android doesn't have browser child apparently...
+ return nullptr;
+ }
+
+ Document* doc = mWindow->GetExtantDoc();
+ if (NS_WARN_IF(!doc)) {
+ NS_WARNING("Unable to get document from window");
+ return nullptr;
+ }
+
+ TabId id = browserChild->GetTabId();
+ nsCOMPtr<nsILoadInfo> loadInfo =
+ new net::LoadInfo(doc->NodePrincipal(), doc->NodePrincipal(), doc, 0,
+ nsIContentPolicy::TYPE_INVALID);
+
+ Maybe<net::LoadInfoArgs> loadInfoArgs;
+ MOZ_ALWAYS_SUCCEEDS(
+ mozilla::ipc::LoadInfoToLoadInfoArgs(loadInfo, &loadInfoArgs));
+ return std::unique_ptr<NrSocketProxyConfig>(new NrSocketProxyConfig(
+ net::WebrtcProxyConfig(id, alpn, *loadInfoArgs, mForceProxy)));
+}
+
+std::map<uint64_t, PeerConnectionAutoTimer>
+ PeerConnectionImpl::sCallDurationTimers;
+} // namespace mozilla
diff --git a/dom/media/webrtc/jsapi/PeerConnectionImpl.h b/dom/media/webrtc/jsapi/PeerConnectionImpl.h
new file mode 100644
index 0000000000..66af1aa0e9
--- /dev/null
+++ b/dom/media/webrtc/jsapi/PeerConnectionImpl.h
@@ -0,0 +1,969 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _PEER_CONNECTION_IMPL_H_
+#define _PEER_CONNECTION_IMPL_H_
+
+#include <string>
+#include <vector>
+#include <map>
+#include <cmath>
+
+#include "prlock.h"
+#include "mozilla/RefPtr.h"
+#include "nsComponentManagerUtils.h"
+#include "nsPIDOMWindow.h"
+#include "nsIUUIDGenerator.h"
+#include "nsIThread.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/Attributes.h"
+
+// Work around nasty macro in webrtc/voice_engine/voice_engine_defines.h
+#ifdef GetLastError
+# undef GetLastError
+#endif
+
+#include "jsep/JsepSession.h"
+#include "jsep/JsepSessionImpl.h"
+#include "sdp/SdpMediaSection.h"
+
+#include "mozilla/ErrorResult.h"
+#include "jsapi/PacketDumper.h"
+#include "mozilla/dom/RTCPeerConnectionBinding.h" // mozPacketDumpType, maybe move?
+#include "mozilla/dom/PeerConnectionImplBinding.h" // ChainedOperation
+#include "mozilla/dom/RTCRtpCapabilitiesBinding.h"
+#include "mozilla/dom/RTCRtpTransceiverBinding.h"
+#include "mozilla/dom/RTCConfigurationBinding.h"
+#include "PrincipalChangeObserver.h"
+#include "mozilla/dom/PromiseNativeHandler.h"
+
+#include "mozilla/TimeStamp.h"
+#include "mozilla/net/DataChannel.h"
+#include "VideoUtils.h"
+#include "VideoSegment.h"
+#include "mozilla/dom/RTCStatsReportBinding.h"
+#include "mozilla/PeerIdentity.h"
+#include "RTCStatsIdGenerator.h"
+#include "RTCStatsReport.h"
+
+#include "mozilla/net/StunAddrsRequestChild.h"
+#include "MediaTransportHandler.h"
+#include "nsIHttpChannelInternal.h"
+#include "RTCDtlsTransport.h"
+#include "RTCRtpTransceiver.h"
+
+namespace test {
+#ifdef USE_FAKE_PCOBSERVER
+class AFakePCObserver;
+#endif
+} // namespace test
+
+class nsDOMDataChannel;
+class nsIPrincipal;
+
+namespace mozilla {
+struct CandidateInfo;
+class DataChannel;
+class DtlsIdentity;
+class MediaPipeline;
+class MediaPipelineReceive;
+class MediaPipelineTransmit;
+enum class PrincipalPrivacy : uint8_t;
+class SharedWebrtcState;
+
+namespace dom {
+class RTCCertificate;
+struct RTCConfiguration;
+struct RTCRtpSourceEntry;
+struct RTCIceServer;
+struct RTCOfferOptions;
+struct RTCRtpParameters;
+class RTCRtpSender;
+class MediaStreamTrack;
+
+#ifdef USE_FAKE_PCOBSERVER
+typedef test::AFakePCObserver PeerConnectionObserver;
+typedef const char* PCObserverString;
+#else
+class PeerConnectionObserver;
+typedef NS_ConvertUTF8toUTF16 PCObserverString;
+#endif
+} // namespace dom
+} // namespace mozilla
+
+#if defined(__cplusplus) && __cplusplus >= 201103L
+typedef struct Timecard Timecard;
+#else
+# include "common/time_profiling/timecard.h"
+#endif
+
+// To preserve blame, convert nsresult to ErrorResult with wrappers. These
+// macros help declare wrappers w/function being wrapped when there are no
+// differences.
+
+#define NS_IMETHODIMP_TO_ERRORRESULT(func, rv, ...) \
+ NS_IMETHODIMP func(__VA_ARGS__); \
+ void func(__VA_ARGS__, rv)
+
+#define NS_IMETHODIMP_TO_ERRORRESULT_RETREF(resulttype, func, rv, ...) \
+ NS_IMETHODIMP func(__VA_ARGS__, resulttype** result); \
+ already_AddRefed<resulttype> func(__VA_ARGS__, rv)
+
+namespace mozilla {
+
+using mozilla::DtlsIdentity;
+using mozilla::ErrorResult;
+using mozilla::PeerIdentity;
+using mozilla::dom::PeerConnectionObserver;
+using mozilla::dom::RTCConfiguration;
+using mozilla::dom::RTCIceServer;
+using mozilla::dom::RTCOfferOptions;
+
+class PeerConnectionWrapper;
+class RemoteSourceStreamInfo;
+
+// Uuid Generator
+class PCUuidGenerator : public mozilla::JsepUuidGenerator {
+ public:
+ virtual bool Generate(std::string* idp) override;
+ virtual mozilla::JsepUuidGenerator* Clone() const override {
+ return new PCUuidGenerator(*this);
+ }
+
+ private:
+ nsCOMPtr<nsIUUIDGenerator> mGenerator;
+};
+
+// This is a variation of Telemetry::AutoTimer that keeps a reference
+// count and records the elapsed time when the count falls to zero. The
+// elapsed time is recorded in seconds.
+struct PeerConnectionAutoTimer {
+ PeerConnectionAutoTimer()
+ : mRefCnt(0), mStart(TimeStamp::Now()), mUsedAV(false){};
+ void RegisterConnection();
+ void UnregisterConnection(bool aContainedAV);
+ bool IsStopped();
+
+ private:
+ int64_t mRefCnt;
+ TimeStamp mStart;
+ bool mUsedAV;
+};
+
+// Enter an API call and check that the state is OK,
+// the PC isn't closed, etc.
+#define PC_AUTO_ENTER_API_CALL(assert_ice_ready) \
+ do { \
+ /* do/while prevents res from conflicting with locals */ \
+ nsresult res = CheckApiState(assert_ice_ready); \
+ if (NS_FAILED(res)) return res; \
+ } while (0)
+#define PC_AUTO_ENTER_API_CALL_VOID_RETURN(assert_ice_ready) \
+ do { \
+ /* do/while prevents res from conflicting with locals */ \
+ nsresult res = CheckApiState(assert_ice_ready); \
+ if (NS_FAILED(res)) return; \
+ } while (0)
+#define PC_AUTO_ENTER_API_CALL_NO_CHECK() CheckThread()
+
+class PeerConnectionImpl final
+ : public nsISupports,
+ public nsWrapperCache,
+ public mozilla::DataChannelConnection::DataConnectionListener {
+ struct Internal; // Avoid exposing c includes to bindings
+
+ public:
+ explicit PeerConnectionImpl(
+ const mozilla::dom::GlobalObject* aGlobal = nullptr);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(PeerConnectionImpl)
+
+ struct RtpExtensionHeader {
+ JsepMediaType mMediaType;
+ SdpDirectionAttribute::Direction direction;
+ std::string extensionname;
+ };
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+ nsPIDOMWindowInner* GetParentObject() const;
+
+ static already_AddRefed<PeerConnectionImpl> Constructor(
+ const mozilla::dom::GlobalObject& aGlobal);
+
+ // DataConnection observers
+ void NotifyDataChannel(already_AddRefed<mozilla::DataChannel> aChannel)
+ // PeerConnectionImpl only inherits from mozilla::DataChannelConnection
+ // inside libxul.
+ override;
+
+ void NotifyDataChannelOpen(DataChannel*) override;
+
+ void NotifyDataChannelClosed(DataChannel*) override;
+
+ void NotifySctpConnected() override;
+
+ void NotifySctpClosed() override;
+
+ const RefPtr<MediaTransportHandler> GetTransportHandler() const;
+
+ // Handle system to allow weak references to be passed through C code
+ virtual const std::string& GetHandle();
+
+ // Name suitable for exposing to content
+ virtual const std::string& GetName();
+
+ // ICE events
+ void IceConnectionStateChange(dom::RTCIceConnectionState state);
+ void IceGatheringStateChange(dom::RTCIceGatheringState state);
+ void OnCandidateFound(const std::string& aTransportId,
+ const CandidateInfo& aCandidateInfo);
+ void UpdateDefaultCandidate(const std::string& defaultAddr,
+ uint16_t defaultPort,
+ const std::string& defaultRtcpAddr,
+ uint16_t defaultRtcpPort,
+ const std::string& transportId);
+
+ static void ListenThread(void* aData);
+ static void ConnectThread(void* aData);
+
+ // Get the STS thread
+ nsISerialEventTarget* GetSTSThread() {
+ PC_AUTO_ENTER_API_CALL_NO_CHECK();
+ return mSTSThread;
+ }
+
+ nsresult Initialize(PeerConnectionObserver& aObserver,
+ nsGlobalWindowInner* aWindow);
+
+ // Initialize PeerConnection from an RTCConfiguration object (JS entrypoint)
+ void Initialize(PeerConnectionObserver& aObserver,
+ nsGlobalWindowInner& aWindow, ErrorResult& rv);
+
+ void SetCertificate(mozilla::dom::RTCCertificate& aCertificate);
+ const RefPtr<mozilla::dom::RTCCertificate>& Certificate() const;
+ // This is a hack to support external linkage.
+ RefPtr<DtlsIdentity> Identity() const;
+
+ NS_IMETHODIMP_TO_ERRORRESULT(CreateOffer, ErrorResult& rv,
+ const RTCOfferOptions& aOptions) {
+ rv = CreateOffer(aOptions);
+ }
+
+ NS_IMETHODIMP CreateAnswer();
+ void CreateAnswer(ErrorResult& rv) { rv = CreateAnswer(); }
+
+ NS_IMETHODIMP CreateOffer(const mozilla::JsepOfferOptions& aConstraints);
+
+ NS_IMETHODIMP SetLocalDescription(int32_t aAction, const char* aSDP);
+
+ void SetLocalDescription(int32_t aAction, const nsAString& aSDP,
+ ErrorResult& rv) {
+ rv = SetLocalDescription(aAction, NS_ConvertUTF16toUTF8(aSDP).get());
+ }
+
+ NS_IMETHODIMP SetRemoteDescription(int32_t aAction, const char* aSDP);
+
+ void SetRemoteDescription(int32_t aAction, const nsAString& aSDP,
+ ErrorResult& rv) {
+ rv = SetRemoteDescription(aAction, NS_ConvertUTF16toUTF8(aSDP).get());
+ }
+
+ already_AddRefed<dom::Promise> GetStats(dom::MediaStreamTrack* aSelector);
+
+ void GetRemoteStreams(nsTArray<RefPtr<DOMMediaStream>>& aStreamsOut) const;
+
+ NS_IMETHODIMP AddIceCandidate(const char* aCandidate, const char* aMid,
+ const char* aUfrag,
+ const dom::Nullable<unsigned short>& aLevel);
+
+ void AddIceCandidate(const nsAString& aCandidate, const nsAString& aMid,
+ const nsAString& aUfrag,
+ const dom::Nullable<unsigned short>& aLevel,
+ ErrorResult& rv) {
+ rv = AddIceCandidate(NS_ConvertUTF16toUTF8(aCandidate).get(),
+ NS_ConvertUTF16toUTF8(aMid).get(),
+ NS_ConvertUTF16toUTF8(aUfrag).get(), aLevel);
+ }
+
+ void UpdateNetworkState(bool online);
+
+ NS_IMETHODIMP CloseStreams();
+
+ void CloseStreams(ErrorResult& rv) { rv = CloseStreams(); }
+
+ already_AddRefed<dom::RTCRtpTransceiver> AddTransceiver(
+ const dom::RTCRtpTransceiverInit& aInit, const nsAString& aKind,
+ dom::MediaStreamTrack* aSendTrack, bool aAddTrackMagic, ErrorResult& aRv);
+
+ bool CheckNegotiationNeeded();
+ bool CreatedSender(const dom::RTCRtpSender& aSender) const;
+
+ // test-only
+ NS_IMETHODIMP_TO_ERRORRESULT(EnablePacketDump, ErrorResult& rv,
+ unsigned long level, dom::mozPacketDumpType type,
+ bool sending) {
+ rv = EnablePacketDump(level, type, sending);
+ }
+
+ // test-only
+ NS_IMETHODIMP_TO_ERRORRESULT(DisablePacketDump, ErrorResult& rv,
+ unsigned long level, dom::mozPacketDumpType type,
+ bool sending) {
+ rv = DisablePacketDump(level, type, sending);
+ }
+
+ void GetPeerIdentity(nsAString& peerIdentity) {
+ if (mPeerIdentity) {
+ peerIdentity = mPeerIdentity->ToString();
+ return;
+ }
+
+ peerIdentity.SetIsVoid(true);
+ }
+
+ const PeerIdentity* GetPeerIdentity() const { return mPeerIdentity; }
+ NS_IMETHODIMP_TO_ERRORRESULT(SetPeerIdentity, ErrorResult& rv,
+ const nsAString& peerIdentity) {
+ rv = SetPeerIdentity(peerIdentity);
+ }
+
+ const std::string& GetIdAsAscii() const { return mName; }
+
+ void GetId(nsAString& id) { id = NS_ConvertASCIItoUTF16(mName.c_str()); }
+
+ void SetId(const nsAString& id) { mName = NS_ConvertUTF16toUTF8(id).get(); }
+
+ // this method checks to see if we've made a promise to protect media.
+ bool PrivacyRequested() const {
+ return mRequestedPrivacy.valueOr(PrincipalPrivacy::NonPrivate) ==
+ PrincipalPrivacy::Private;
+ }
+
+ NS_IMETHODIMP GetFingerprint(char** fingerprint);
+ void GetFingerprint(nsAString& fingerprint) {
+ char* tmp;
+ nsresult rv = GetFingerprint(&tmp);
+ NS_ENSURE_SUCCESS_VOID(rv);
+ fingerprint.AssignASCII(tmp);
+ delete[] tmp;
+ }
+
+ void GetCurrentLocalDescription(nsAString& aSDP) const;
+ void GetPendingLocalDescription(nsAString& aSDP) const;
+
+ void GetCurrentRemoteDescription(nsAString& aSDP) const;
+ void GetPendingRemoteDescription(nsAString& aSDP) const;
+
+ dom::Nullable<bool> GetCurrentOfferer() const;
+ dom::Nullable<bool> GetPendingOfferer() const;
+
+ NS_IMETHODIMP SignalingState(mozilla::dom::RTCSignalingState* aState);
+
+ mozilla::dom::RTCSignalingState SignalingState() {
+ mozilla::dom::RTCSignalingState state;
+ SignalingState(&state);
+ return state;
+ }
+
+ NS_IMETHODIMP IceConnectionState(mozilla::dom::RTCIceConnectionState* aState);
+
+ mozilla::dom::RTCIceConnectionState IceConnectionState() {
+ mozilla::dom::RTCIceConnectionState state;
+ IceConnectionState(&state);
+ return state;
+ }
+
+ NS_IMETHODIMP IceGatheringState(mozilla::dom::RTCIceGatheringState* aState);
+
+ mozilla::dom::RTCIceGatheringState IceGatheringState() {
+ return mIceGatheringState;
+ }
+
+ NS_IMETHODIMP ConnectionState(mozilla::dom::RTCPeerConnectionState* aState);
+
+ mozilla::dom::RTCPeerConnectionState ConnectionState() {
+ mozilla::dom::RTCPeerConnectionState state;
+ ConnectionState(&state);
+ return state;
+ }
+
+ NS_IMETHODIMP Close();
+
+ void Close(ErrorResult& rv) { rv = Close(); }
+
+ // TODO: Convert this to MOZ_CAN_RUN_SCRIPT (bug 1415230)
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY bool PluginCrash(uint32_t aPluginID,
+ const nsAString& aPluginName);
+
+ NS_IMETHODIMP_TO_ERRORRESULT(SetConfiguration, ErrorResult& rv,
+ const RTCConfiguration& aConfiguration) {
+ rv = SetConfiguration(aConfiguration);
+ }
+
+ dom::RTCSctpTransport* GetSctp() const;
+
+ void RestartIce();
+ void RestartIceNoRenegotiationNeeded();
+
+ void RecordEndOfCallTelemetry();
+
+ nsresult InitializeDataChannel();
+
+ NS_IMETHODIMP_TO_ERRORRESULT_RETREF(nsDOMDataChannel, CreateDataChannel,
+ ErrorResult& rv, const nsAString& aLabel,
+ const nsAString& aProtocol,
+ uint16_t aType, bool outOfOrderAllowed,
+ uint16_t aMaxTime, uint16_t aMaxNum,
+ bool aExternalNegotiated,
+ uint16_t aStream);
+
+ // Base class for chained operations. Necessary right now because some
+ // operations come from JS (in the form of dom::ChainedOperation), and others
+ // come from c++ (dom::ChainedOperation is very unwieldy and arcane to build
+ // in c++). Once we stop using JSImpl, we should be able to simplify this.
+ class Operation : public dom::PromiseNativeHandler {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS(Operation)
+ Operation(PeerConnectionImpl* aPc, ErrorResult& aError);
+ MOZ_CAN_RUN_SCRIPT
+ void Call(ErrorResult& aError);
+ dom::Promise* GetPromise() { return mPromise; }
+ MOZ_CAN_RUN_SCRIPT
+ void ResolvedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override;
+
+ MOZ_CAN_RUN_SCRIPT
+ void RejectedCallback(JSContext* aCx, JS::Handle<JS::Value> aValue,
+ ErrorResult& aRv) override;
+
+ protected:
+ MOZ_CAN_RUN_SCRIPT
+ virtual RefPtr<dom::Promise> CallImpl(ErrorResult& aError) = 0;
+ virtual ~Operation();
+ // This is the promise p from https://w3c.github.io/webrtc-pc/#dfn-chain
+ // This will be a content promise, since we return this to the caller of
+ // Chain.
+ RefPtr<dom::Promise> mPromise;
+ RefPtr<PeerConnectionImpl> mPc;
+ };
+
+ class JSOperation final : public Operation {
+ public:
+ JSOperation(PeerConnectionImpl* aPc, dom::ChainedOperation& aOp,
+ ErrorResult& aError);
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(JSOperation, Operation)
+
+ private:
+ MOZ_CAN_RUN_SCRIPT
+ RefPtr<dom::Promise> CallImpl(ErrorResult& aError) override;
+ ~JSOperation() = default;
+ RefPtr<dom::ChainedOperation> mOperation;
+ };
+
+ MOZ_CAN_RUN_SCRIPT
+ already_AddRefed<dom::Promise> Chain(dom::ChainedOperation& aOperation,
+ ErrorResult& aError);
+ MOZ_CAN_RUN_SCRIPT
+ already_AddRefed<dom::Promise> Chain(const RefPtr<Operation>& aOperation,
+ ErrorResult& aError);
+ already_AddRefed<dom::Promise> MakePromise(ErrorResult& aError) const;
+
+ void UpdateNegotiationNeeded();
+
+ void GetTransceivers(
+ nsTArray<RefPtr<dom::RTCRtpTransceiver>>& aTransceiversOut) {
+ aTransceiversOut = mTransceivers.Clone();
+ }
+
+ // Gets the RTC Signaling State of the JSEP session
+ dom::RTCSignalingState GetSignalingState() const;
+
+ already_AddRefed<dom::Promise> OnSetDescriptionSuccess(
+ dom::RTCSdpType aSdpType, bool aRemote, ErrorResult& aError);
+
+ void OnSetDescriptionError();
+
+ bool IsClosed() const;
+
+ // called when DTLS connects; we only need this once
+ nsresult OnAlpnNegotiated(bool aPrivacyRequested);
+
+ void OnDtlsStateChange(const std::string& aTransportId,
+ TransportLayer::State aState);
+ void UpdateConnectionState();
+ dom::RTCPeerConnectionState GetNewConnectionState() const;
+
+ // initialize telemetry for when calls start
+ void StartCallTelem();
+
+ // Gets all codec stats for all transports, coalesced to transport level.
+ nsTArray<dom::RTCCodecStats> GetCodecStats(DOMHighResTimeStamp aNow);
+
+ RefPtr<dom::RTCStatsReportPromise> GetStats(dom::MediaStreamTrack* aSelector,
+ bool aInternalStats);
+
+ void CollectConduitTelemetryData();
+
+ void OnMediaError(const std::string& aError);
+
+ void DumpPacket_m(size_t level, dom::mozPacketDumpType type, bool sending,
+ UniquePtr<uint8_t[]>& packet, size_t size);
+
+ const dom::RTCStatsTimestampMaker& GetTimestampMaker() const {
+ return mTimestampMaker;
+ }
+
+ // Utility function, given a string pref and an URI, returns whether or not
+ // the URI occurs in the pref. Wildcards are supported (e.g. *.example.com)
+ // and multiple hostnames can be present, separated by commas.
+ static bool HostnameInPref(const char* aPrefList, const nsCString& aHostName);
+
+ void StampTimecard(const char* aEvent);
+
+ bool RelayOnly() const {
+ return mJsConfiguration.mIceTransportPolicy.WasPassed() &&
+ mJsConfiguration.mIceTransportPolicy.Value() ==
+ dom::RTCIceTransportPolicy::Relay;
+ }
+
+ RefPtr<PacketDumper> GetPacketDumper() {
+ if (!mPacketDumper) {
+ mPacketDumper = new PacketDumper(mHandle);
+ }
+
+ return mPacketDumper;
+ }
+
+ nsString GenerateUUID() const {
+ std::string result;
+ if (!mUuidGen->Generate(&result)) {
+ MOZ_CRASH();
+ }
+ return NS_ConvertUTF8toUTF16(result.c_str());
+ }
+
+ bool ShouldAllowOldSetParameters() const { return mAllowOldSetParameters; }
+
+ nsCString GetHostname() const { return mHostname; }
+ nsCString GetEffectiveTLDPlus1() const { return mEffectiveTLDPlus1; }
+
+ void SendWarningToConsole(const nsCString& aWarning);
+
+ const UniquePtr<dom::RTCStatsReportInternal>& GetFinalStats() const {
+ return mFinalStats;
+ }
+
+ void DisableLongTermStats() { mDisableLongTermStats = true; }
+
+ bool LongTermStatsIsDisabled() const { return mDisableLongTermStats; }
+
+ static void GetDefaultVideoCodecs(
+ std::vector<UniquePtr<JsepCodecDescription>>& aSupportedCodecs,
+ bool aUseRtx);
+
+ static void GetDefaultAudioCodecs(
+ std::vector<UniquePtr<JsepCodecDescription>>& aSupportedCodecs);
+
+ static void GetDefaultRtpExtensions(
+ std::vector<RtpExtensionHeader>& aRtpExtensions);
+
+ static void GetCapabilities(const nsAString& aKind,
+ dom::Nullable<dom::RTCRtpCapabilities>& aResult,
+ sdp::Direction aDirection);
+ static void SetupPreferredCodecs(
+ std::vector<UniquePtr<JsepCodecDescription>>& aPreferredCodecs);
+
+ static void SetupPreferredRtpExtensions(
+ std::vector<RtpExtensionHeader>& aPreferredheaders);
+
+ private:
+ virtual ~PeerConnectionImpl();
+ PeerConnectionImpl(const PeerConnectionImpl& rhs);
+ PeerConnectionImpl& operator=(PeerConnectionImpl);
+
+ RefPtr<dom::RTCStatsPromise> GetDataChannelStats(
+ const RefPtr<DataChannelConnection>& aDataChannelConnection,
+ const DOMHighResTimeStamp aTimestamp);
+ nsresult CalculateFingerprint(const std::string& algorithm,
+ std::vector<uint8_t>* fingerprint) const;
+ nsresult ConfigureJsepSessionCodecs();
+
+ NS_IMETHODIMP EnsureDataConnection(uint16_t aLocalPort, uint16_t aNumstreams,
+ uint32_t aMaxMessageSize, bool aMMSSet);
+
+ nsresult CheckApiState(bool assert_ice_ready) const;
+ void StoreFinalStats(UniquePtr<dom::RTCStatsReportInternal>&& report);
+ void CheckThread() const { MOZ_ASSERT(NS_IsMainThread(), "Wrong thread"); }
+
+ // test-only: called from AddRIDExtension and AddRIDFilter
+ // for simulcast mochitests.
+ RefPtr<MediaPipeline> GetMediaPipelineForTrack(
+ dom::MediaStreamTrack& aRecvTrack);
+
+ void CandidateReady(const std::string& candidate,
+ const std::string& transportId, const std::string& ufrag);
+ void SendLocalIceCandidateToContent(uint16_t level, const std::string& mid,
+ const std::string& candidate,
+ const std::string& ufrag);
+
+ nsresult GetDatachannelParameters(uint32_t* channels, uint16_t* localport,
+ uint16_t* remoteport,
+ uint32_t* maxmessagesize, bool* mmsset,
+ std::string* transportId,
+ bool* client) const;
+
+ nsresult AddRtpTransceiverToJsepSession(JsepTransceiver& transceiver);
+
+ void RecordIceRestartStatistics(JsepSdpType type);
+
+ void StoreConfigurationForAboutWebrtc(const RTCConfiguration& aConfig);
+
+ dom::Sequence<dom::RTCSdpParsingErrorInternal> GetLastSdpParsingErrors()
+ const;
+
+ MOZ_CAN_RUN_SCRIPT
+ void RunNextOperation(ErrorResult& aError);
+
+ void SyncToJsep();
+ void SyncFromJsep();
+
+ void DoSetDescriptionSuccessPostProcessing(dom::RTCSdpType aSdpType,
+ bool aRemote,
+ const RefPtr<dom::Promise>& aP);
+
+ // Timecard used to measure processing time. This should be the first class
+ // attribute so that we accurately measure the time required to instantiate
+ // any other attributes of this class.
+ Timecard* mTimeCard;
+
+ // Configuration used to initialize the PeerConnection
+ dom::RTCConfigurationInternal mJsConfiguration;
+
+ mozilla::dom::RTCSignalingState mSignalingState;
+
+ // ICE State
+ mozilla::dom::RTCIceConnectionState mIceConnectionState;
+ mozilla::dom::RTCIceGatheringState mIceGatheringState;
+
+ mozilla::dom::RTCPeerConnectionState mConnectionState;
+
+ RefPtr<PeerConnectionObserver> mPCObserver;
+
+ nsCOMPtr<nsPIDOMWindowInner> mWindow;
+
+ // The SDP sent in from JS
+ std::string mLocalRequestedSDP;
+ std::string mRemoteRequestedSDP;
+ // Only accessed from main
+ mozilla::dom::Sequence<mozilla::dom::RTCSdpHistoryEntryInternal> mSdpHistory;
+ std::string mPendingLocalDescription;
+ std::string mPendingRemoteDescription;
+ std::string mCurrentLocalDescription;
+ std::string mCurrentRemoteDescription;
+ Maybe<bool> mPendingOfferer;
+ Maybe<bool> mCurrentOfferer;
+
+ // DTLS fingerprint
+ std::string mFingerprint;
+ std::string mRemoteFingerprint;
+
+ // identity-related fields
+ // The entity on the other end of the peer-to-peer connection;
+ // void if they are not yet identified, and no identity setting has been set
+ RefPtr<PeerIdentity> mPeerIdentity;
+ // The certificate we are using.
+ RefPtr<mozilla::dom::RTCCertificate> mCertificate;
+ // Whether an app should be prevented from accessing media produced by the PC
+ // If this is true, then media will not be sent until mPeerIdentity matches
+ // local streams PeerIdentity; and remote streams are protected from content
+ //
+ // This can be false if mPeerIdentity is set, in the case where identity is
+ // provided, but the media is not protected from the app on either side
+ Maybe<PrincipalPrivacy> mRequestedPrivacy;
+
+ // A handle to refer to this PC with
+ std::string mHandle;
+
+ // A name for this PC that we are willing to expose to content.
+ std::string mName;
+ nsCString mHostname;
+ nsCString mEffectiveTLDPlus1;
+
+ // The target to run stuff on
+ nsCOMPtr<nsISerialEventTarget> mSTSThread;
+
+ // DataConnection that's used to get all the DataChannels
+ RefPtr<mozilla::DataChannelConnection> mDataConnection;
+ unsigned int mDataChannelsOpened = 0;
+ unsigned int mDataChannelsClosed = 0;
+
+ bool mForceIceTcp;
+ RefPtr<MediaTransportHandler> mTransportHandler;
+
+ // The JSEP negotiation session.
+ mozilla::UniquePtr<PCUuidGenerator> mUuidGen;
+ mozilla::UniquePtr<mozilla::JsepSession> mJsepSession;
+ // There are lots of error cases where we want to abandon an sRD/sLD _after_
+ // it has already been applied to the JSEP engine, and revert back to the
+ // previous state. We also want to ensure that the various modifications
+ // to the JSEP engine are not exposed to JS until the sRD/sLD completes,
+ // which is why we have a new "uncommitted" JSEP engine.
+ mozilla::UniquePtr<mozilla::JsepSession> mUncommittedJsepSession;
+ unsigned long mIceRestartCount;
+ unsigned long mIceRollbackCount;
+
+ // The following are used for Telemetry:
+ bool mCallTelemStarted = false;
+ bool mCallTelemEnded = false;
+
+ // We _could_ make mFinalStatsQuery be an RTCStatsReportPromise, but that
+ // would require RTCStatsReportPromise to no longer be exclusive, which is
+ // a bit of a hassle, and not very performant.
+ RefPtr<GenericNonExclusivePromise> mFinalStatsQuery;
+ UniquePtr<dom::RTCStatsReportInternal> mFinalStats;
+ bool mDisableLongTermStats = false;
+
+ // Start time of ICE.
+ mozilla::TimeStamp mIceStartTime;
+ // Hold PeerConnectionAutoTimer instances for each window.
+ static std::map<uint64_t, PeerConnectionAutoTimer> sCallDurationTimers;
+
+ bool mHaveConfiguredCodecs;
+
+ bool mTrickle;
+
+ bool mPrivateWindow;
+
+ // Whether this PeerConnection is being counted as active by mWindow
+ bool mActiveOnWindow;
+
+ // storage for Telemetry data
+ uint16_t mMaxReceiving[SdpMediaSection::kMediaTypes];
+ uint16_t mMaxSending[SdpMediaSection::kMediaTypes];
+
+ // used to store the raw trickle candidate string for display
+ // on the about:webrtc raw candidates table.
+ std::vector<std::string> mRawTrickledCandidates;
+
+ dom::RTCStatsTimestampMaker mTimestampMaker;
+
+ RefPtr<RTCStatsIdGenerator> mIdGenerator;
+ // Ordinarily, I would use a std::map here, but this used to be a JS Map
+ // which iterates in insertion order, and I want to avoid changing this.
+ nsTArray<RefPtr<DOMMediaStream>> mReceiveStreams;
+
+ DOMMediaStream* GetReceiveStream(const std::string& aId) const;
+ DOMMediaStream* CreateReceiveStream(const std::string& aId);
+
+ void InitLocalAddrs(); // for stun local address IPC request
+ bool ShouldForceProxy() const;
+ std::unique_ptr<NrSocketProxyConfig> GetProxyConfig() const;
+
+ class StunAddrsHandler : public net::StunAddrsListener {
+ public:
+ explicit StunAddrsHandler(PeerConnectionImpl* aPc)
+ : mPcHandle(aPc->GetHandle()) {}
+
+ void OnMDNSQueryComplete(const nsCString& hostname,
+ const Maybe<nsCString>& address) override;
+
+ void OnStunAddrsAvailable(
+ const mozilla::net::NrIceStunAddrArray& addrs) override;
+
+ private:
+ // This class is not cycle-collected, so we must avoid grabbing a strong
+ // reference.
+ const std::string mPcHandle;
+ virtual ~StunAddrsHandler() {}
+ };
+
+ // Manage ICE transports.
+ void UpdateTransport(const JsepTransceiver& aTransceiver, bool aForceIceTcp);
+
+ void GatherIfReady();
+ void FlushIceCtxOperationQueueIfReady();
+ void PerformOrEnqueueIceCtxOperation(nsIRunnable* runnable);
+ nsresult SetTargetForDefaultLocalAddressLookup();
+ void EnsureIceGathering(bool aDefaultRouteOnly, bool aObfuscateHostAddresses);
+
+ bool GetPrefDefaultAddressOnly() const;
+ bool GetPrefObfuscateHostAddresses() const;
+
+ bool IsIceCtxReady() const {
+ return mLocalAddrsRequestState == STUN_ADDR_REQUEST_COMPLETE;
+ }
+
+ // Ensure ICE transports exist that we might need when offer/answer concludes
+ void EnsureTransports(const JsepSession& aSession);
+
+ void UpdateRTCDtlsTransports(bool aMarkAsStable);
+ void RollbackRTCDtlsTransports();
+ void RemoveRTCDtlsTransportsExcept(
+ const std::set<std::string>& aTransportIds);
+
+ // Activate ICE transports at the conclusion of offer/answer,
+ // or when rollback occurs.
+ nsresult UpdateTransports(const JsepSession& aSession,
+ const bool forceIceTcp);
+
+ void ResetStunAddrsForIceRestart() { mStunAddrs.Clear(); }
+
+ // Start ICE checks.
+ void StartIceChecks(const JsepSession& session);
+
+ // Process a trickle ICE candidate.
+ void AddIceCandidate(const std::string& candidate,
+ const std::string& aTransportId,
+ const std::string& aUFrag);
+
+ // Handle complete media pipelines.
+ // This updates codec parameters, starts/stops send/receive, and other
+ // stuff that doesn't necessarily require negotiation. This can be called at
+ // any time, not just when an offer/answer exchange completes.
+ nsresult UpdateMediaPipelines();
+
+ already_AddRefed<dom::RTCRtpTransceiver> CreateTransceiver(
+ const std::string& aId, bool aIsVideo,
+ const dom::RTCRtpTransceiverInit& aInit,
+ dom::MediaStreamTrack* aSendTrack, bool aAddTrackMagic, ErrorResult& aRv);
+
+ std::string GetTransportIdMatchingSendTrack(
+ const dom::MediaStreamTrack& aTrack) const;
+
+ // this determines if any track is peerIdentity constrained
+ bool AnyLocalTrackHasPeerIdentity() const;
+
+ bool AnyCodecHasPluginID(uint64_t aPluginID);
+
+ already_AddRefed<nsIHttpChannelInternal> GetChannel() const;
+
+ void BreakCycles();
+
+ bool HasPendingSetParameters() const;
+ void InvalidateLastReturnedParameters();
+
+ RefPtr<WebrtcCallWrapper> mCall;
+
+ // See Bug 1642419, this can be removed when all sites are working with RTX.
+ bool mRtxIsAllowed = true;
+
+ nsTArray<RefPtr<Operation>> mOperations;
+ bool mChainingOperation = false;
+ bool mUpdateNegotiationNeededFlagOnEmptyChain = false;
+ bool mNegotiationNeeded = false;
+ std::set<std::pair<std::string, std::string>> mLocalIceCredentialsToReplace;
+
+ nsTArray<RefPtr<dom::RTCRtpTransceiver>> mTransceivers;
+ std::map<std::string, RefPtr<dom::RTCDtlsTransport>>
+ mTransportIdToRTCDtlsTransport;
+ RefPtr<dom::RTCSctpTransport> mSctpTransport;
+
+ // Used whenever we need to dispatch a runnable to STS to tweak something
+ // on our ICE ctx, but are not ready to do so at the moment (eg; we are
+ // waiting to get a callback with our http proxy config before we start
+ // gathering or start checking)
+ std::vector<nsCOMPtr<nsIRunnable>> mQueuedIceCtxOperations;
+
+ // Set if prefs dictate that we should force the use of a web proxy.
+ bool mForceProxy = false;
+
+ // Used to cancel incoming stun addrs response
+ RefPtr<net::StunAddrsRequestChild> mStunAddrsRequest;
+
+ enum StunAddrRequestState {
+ STUN_ADDR_REQUEST_NONE,
+ STUN_ADDR_REQUEST_PENDING,
+ STUN_ADDR_REQUEST_COMPLETE
+ };
+ // Used to track the state of the stun addr IPC request
+ StunAddrRequestState mLocalAddrsRequestState = STUN_ADDR_REQUEST_NONE;
+
+ // Used to store the result of the stun addr IPC request
+ nsTArray<NrIceStunAddr> mStunAddrs;
+
+ // Used to ensure the target for default local address lookup is only set
+ // once.
+ bool mTargetForDefaultLocalAddressLookupIsSet = false;
+
+ // Keep track of local hostnames to register. Registration is deferred
+ // until StartIceChecks has run. Accessed on main thread only.
+ std::map<std::string, std::string> mMDNSHostnamesToRegister;
+ bool mCanRegisterMDNSHostnamesDirectly = false;
+
+ // Used to store the mDNS hostnames that we have registered
+ std::set<std::string> mRegisteredMDNSHostnames;
+
+ // web-compat stopgap
+ bool mAllowOldSetParameters = false;
+
+ // Used to store the mDNS hostnames that we have queried
+ struct PendingIceCandidate {
+ std::vector<std::string> mTokenizedCandidate;
+ std::string mTransportId;
+ std::string mUfrag;
+ };
+ std::map<std::string, std::list<PendingIceCandidate>> mQueriedMDNSHostnames;
+
+ // Connecting PCImpl to sigslot is not safe, because sigslot takes strong
+ // references without any reference counting, and JS holds refcounted strong
+ // references to PCImpl (meaning JS can cause PCImpl to be destroyed). This
+ // is not ref-counted (since sigslot holds onto non-refcounted strong refs)
+ // Must be destroyed on STS. Holds a weak reference to PCImpl.
+ class SignalHandler : public sigslot::has_slots<> {
+ public:
+ SignalHandler(PeerConnectionImpl* aPc, MediaTransportHandler* aSource);
+ virtual ~SignalHandler();
+
+ void ConnectSignals();
+
+ // ICE events
+ void IceGatheringStateChange_s(dom::RTCIceGatheringState aState);
+ void IceConnectionStateChange_s(dom::RTCIceConnectionState aState);
+ void OnCandidateFound_s(const std::string& aTransportId,
+ const CandidateInfo& aCandidateInfo);
+ void AlpnNegotiated_s(const std::string& aAlpn, bool aPrivacyRequested);
+ void ConnectionStateChange_s(const std::string& aTransportId,
+ TransportLayer::State aState);
+
+ private:
+ const std::string mHandle;
+ RefPtr<MediaTransportHandler> mSource;
+ RefPtr<nsISerialEventTarget> mSTSThread;
+ };
+
+ mozilla::UniquePtr<SignalHandler> mSignalHandler;
+
+ // Make absolutely sure our refcount does not go to 0 before Close() is called
+ // This is because Close does a stats query, which needs the
+ // PeerConnectionImpl to stick around until the query is done.
+ RefPtr<PeerConnectionImpl> mKungFuDeathGrip;
+ RefPtr<PacketDumper> mPacketDumper;
+
+ public:
+ // these are temporary until the DataChannel Listen/Connect API is removed
+ unsigned short listenPort;
+ unsigned short connectPort;
+ char* connectStr; // XXX ownership/free
+};
+
+// This is what is returned when you acquire on a handle
+class PeerConnectionWrapper {
+ public:
+ explicit PeerConnectionWrapper(const std::string& handle);
+
+ PeerConnectionImpl* impl() { return impl_; }
+
+ private:
+ RefPtr<PeerConnectionImpl> impl_;
+};
+
+} // namespace mozilla
+
+#undef NS_IMETHODIMP_TO_ERRORRESULT
+#undef NS_IMETHODIMP_TO_ERRORRESULT_RETREF
+#endif // _PEER_CONNECTION_IMPL_H_
diff --git a/dom/media/webrtc/jsapi/RTCDTMFSender.cpp b/dom/media/webrtc/jsapi/RTCDTMFSender.cpp
new file mode 100644
index 0000000000..30355aca26
--- /dev/null
+++ b/dom/media/webrtc/jsapi/RTCDTMFSender.cpp
@@ -0,0 +1,159 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "RTCDTMFSender.h"
+#include "libwebrtcglue/MediaConduitInterface.h"
+#include "transport/logging.h"
+#include "RTCRtpTransceiver.h"
+#include "nsITimer.h"
+#include "mozilla/dom/RTCDTMFSenderBinding.h"
+#include "mozilla/dom/RTCDTMFToneChangeEvent.h"
+#include <algorithm>
+#include <bitset>
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(RTCDTMFSender, DOMEventTargetHelper,
+ mTransceiver, mSendTimer)
+
+NS_IMPL_ADDREF_INHERITED(RTCDTMFSender, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(RTCDTMFSender, DOMEventTargetHelper)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(RTCDTMFSender)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsITimerCallback)
+ NS_INTERFACE_MAP_ENTRY(nsINamed)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+LazyLogModule gDtmfLog("RTCDTMFSender");
+
+RTCDTMFSender::RTCDTMFSender(nsPIDOMWindowInner* aWindow,
+ RTCRtpTransceiver* aTransceiver)
+ : DOMEventTargetHelper(aWindow), mTransceiver(aTransceiver) {}
+
+JSObject* RTCDTMFSender::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return RTCDTMFSender_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+static int GetDTMFToneCode(uint16_t c) {
+ const char* DTMF_TONECODES = "0123456789*#ABCD";
+
+ if (c == ',') {
+ // , is a special character indicating a 2 second delay
+ return -1;
+ }
+
+ const char* i = strchr(DTMF_TONECODES, c);
+ MOZ_ASSERT(i);
+ return static_cast<int>(i - DTMF_TONECODES);
+}
+
+static std::bitset<256> GetCharacterBitset(const std::string& aCharsInSet) {
+ std::bitset<256> result;
+ for (auto c : aCharsInSet) {
+ result[c] = true;
+ }
+ return result;
+}
+
+static bool IsUnrecognizedChar(const char c) {
+ static const std::bitset<256> recognized =
+ GetCharacterBitset("0123456789ABCD#*,");
+ return !recognized[c];
+}
+
+void RTCDTMFSender::SetPayloadType(int32_t aPayloadType,
+ int32_t aPayloadFrequency) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mPayloadType = Some(aPayloadType);
+ mPayloadFrequency = Some(aPayloadFrequency);
+}
+
+void RTCDTMFSender::InsertDTMF(const nsAString& aTones, uint32_t aDuration,
+ uint32_t aInterToneGap, ErrorResult& aRv) {
+ if (!mTransceiver->CanSendDTMF()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ std::string utf8Tones = NS_ConvertUTF16toUTF8(aTones).get();
+
+ std::transform(utf8Tones.begin(), utf8Tones.end(), utf8Tones.begin(),
+ [](const char c) { return std::toupper(c); });
+
+ if (std::any_of(utf8Tones.begin(), utf8Tones.end(), IsUnrecognizedChar)) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_CHARACTER_ERR);
+ return;
+ }
+
+ CopyUTF8toUTF16(utf8Tones, mToneBuffer);
+ mDuration = std::clamp(aDuration, 40U, 6000U);
+ mInterToneGap = std::clamp(aInterToneGap, 30U, 6000U);
+
+ if (mToneBuffer.Length()) {
+ StartPlayout(0);
+ }
+}
+
+void RTCDTMFSender::StopPlayout() {
+ if (mSendTimer) {
+ mSendTimer->Cancel();
+ mSendTimer = nullptr;
+ }
+}
+
+void RTCDTMFSender::StartPlayout(uint32_t aDelay) {
+ if (!mSendTimer) {
+ mSendTimer = NS_NewTimer();
+ mSendTimer->InitWithCallback(this, aDelay, nsITimer::TYPE_ONE_SHOT);
+ }
+}
+
+nsresult RTCDTMFSender::Notify(nsITimer* timer) {
+ MOZ_ASSERT(NS_IsMainThread());
+ StopPlayout();
+
+ if (!mTransceiver->IsSending()) {
+ return NS_OK;
+ }
+
+ RTCDTMFToneChangeEventInit init;
+ if (!mToneBuffer.IsEmpty()) {
+ uint16_t toneChar = mToneBuffer.CharAt(0);
+ int tone = GetDTMFToneCode(toneChar);
+
+ init.mTone.Assign(toneChar);
+
+ mToneBuffer.Cut(0, 1);
+
+ if (tone == -1) {
+ StartPlayout(2000);
+ } else {
+ // Reset delay if necessary
+ StartPlayout(mDuration + mInterToneGap);
+ mDtmfEvent.Notify(DtmfEvent(mPayloadType.ref(), mPayloadFrequency.ref(),
+ tone, mDuration));
+ }
+ }
+
+ RefPtr<RTCDTMFToneChangeEvent> event =
+ RTCDTMFToneChangeEvent::Constructor(this, u"tonechange"_ns, init);
+ DispatchTrustedEvent(event);
+
+ return NS_OK;
+}
+
+nsresult RTCDTMFSender::GetName(nsACString& aName) {
+ aName.AssignLiteral("RTCDTMFSender");
+ return NS_OK;
+}
+
+void RTCDTMFSender::GetToneBuffer(nsAString& aOutToneBuffer) {
+ aOutToneBuffer = mToneBuffer;
+}
+
+} // namespace mozilla::dom
+
+#undef LOGTAG
diff --git a/dom/media/webrtc/jsapi/RTCDTMFSender.h b/dom/media/webrtc/jsapi/RTCDTMFSender.h
new file mode 100644
index 0000000000..14be7eb6ee
--- /dev/null
+++ b/dom/media/webrtc/jsapi/RTCDTMFSender.h
@@ -0,0 +1,78 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _RTCDTMFSender_h_
+#define _RTCDTMFSender_h_
+
+#include "MediaEventSource.h"
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/RefPtr.h"
+#include "js/RootingAPI.h"
+#include "nsITimer.h"
+
+class nsPIDOMWindowInner;
+class nsITimer;
+
+namespace mozilla {
+class AudioSessionConduit;
+
+struct DtmfEvent {
+ DtmfEvent(int aPayloadType, int aPayloadFrequency, int aEventCode,
+ int aLengthMs)
+ : mPayloadType(aPayloadType),
+ mPayloadFrequency(aPayloadFrequency),
+ mEventCode(aEventCode),
+ mLengthMs(aLengthMs) {}
+ const int mPayloadType;
+ const int mPayloadFrequency;
+ const int mEventCode;
+ const int mLengthMs;
+};
+
+namespace dom {
+class RTCRtpTransceiver;
+
+class RTCDTMFSender : public DOMEventTargetHelper,
+ public nsITimerCallback,
+ public nsINamed {
+ public:
+ RTCDTMFSender(nsPIDOMWindowInner* aWindow, RTCRtpTransceiver* aTransceiver);
+
+ // nsISupports
+ NS_DECL_NSITIMERCALLBACK
+ NS_DECL_NSINAMED
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(RTCDTMFSender, DOMEventTargetHelper)
+
+ // webidl
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+ void SetPayloadType(int32_t aPayloadType, int32_t aPayloadFrequency);
+ void InsertDTMF(const nsAString& aTones, uint32_t aDuration,
+ uint32_t aInterToneGap, ErrorResult& aRv);
+ void GetToneBuffer(nsAString& aOutToneBuffer);
+ IMPL_EVENT_HANDLER(tonechange)
+
+ void StopPlayout();
+
+ MediaEventSource<DtmfEvent>& OnDtmfEvent() { return mDtmfEvent; }
+
+ private:
+ virtual ~RTCDTMFSender() = default;
+
+ void StartPlayout(uint32_t aDelay);
+
+ RefPtr<RTCRtpTransceiver> mTransceiver;
+ MediaEventProducer<DtmfEvent> mDtmfEvent;
+ Maybe<int32_t> mPayloadType;
+ Maybe<int32_t> mPayloadFrequency;
+ nsString mToneBuffer;
+ uint32_t mDuration = 0;
+ uint32_t mInterToneGap = 0;
+ nsCOMPtr<nsITimer> mSendTimer;
+};
+
+} // namespace dom
+} // namespace mozilla
+#endif // _RTCDTMFSender_h_
diff --git a/dom/media/webrtc/jsapi/RTCDtlsTransport.cpp b/dom/media/webrtc/jsapi/RTCDtlsTransport.cpp
new file mode 100644
index 0000000000..442e9c7a17
--- /dev/null
+++ b/dom/media/webrtc/jsapi/RTCDtlsTransport.cpp
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "RTCDtlsTransport.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/EventBinding.h"
+#include "mozilla/dom/RTCDtlsTransportBinding.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(RTCDtlsTransport, DOMEventTargetHelper)
+
+NS_IMPL_ADDREF_INHERITED(RTCDtlsTransport, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(RTCDtlsTransport, DOMEventTargetHelper)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(RTCDtlsTransport)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+RTCDtlsTransport::RTCDtlsTransport(nsPIDOMWindowInner* aWindow)
+ : DOMEventTargetHelper(aWindow), mState(RTCDtlsTransportState::New) {}
+
+JSObject* RTCDtlsTransport::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return RTCDtlsTransport_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void RTCDtlsTransport::UpdateState(TransportLayer::State aState) {
+ if (mState == RTCDtlsTransportState::Closed) {
+ return;
+ }
+
+ RTCDtlsTransportState newState = mState;
+ switch (aState) {
+ case TransportLayer::TS_NONE:
+ break;
+ case TransportLayer::TS_INIT:
+ break;
+ case TransportLayer::TS_CONNECTING:
+ newState = RTCDtlsTransportState::Connecting;
+ break;
+ case TransportLayer::TS_OPEN:
+ newState = RTCDtlsTransportState::Connected;
+ break;
+ case TransportLayer::TS_CLOSED:
+ newState = RTCDtlsTransportState::Closed;
+ break;
+ case TransportLayer::TS_ERROR:
+ newState = RTCDtlsTransportState::Failed;
+ break;
+ }
+
+ if (newState == mState) {
+ return;
+ }
+
+ mState = newState;
+
+ EventInit init;
+ init.mBubbles = false;
+ init.mCancelable = false;
+
+ RefPtr<Event> event = Event::Constructor(this, u"statechange"_ns, init);
+
+ DispatchTrustedEvent(event);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webrtc/jsapi/RTCDtlsTransport.h b/dom/media/webrtc/jsapi/RTCDtlsTransport.h
new file mode 100644
index 0000000000..74a4b5f618
--- /dev/null
+++ b/dom/media/webrtc/jsapi/RTCDtlsTransport.h
@@ -0,0 +1,43 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _RTCDtlsTransport_h_
+#define _RTCDtlsTransport_h_
+
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/RefPtr.h"
+#include "js/RootingAPI.h"
+#include "transport/transportlayer.h"
+
+class nsPIDOMWindowInner;
+
+namespace mozilla::dom {
+
+enum class RTCDtlsTransportState : uint8_t;
+
+class RTCDtlsTransport : public DOMEventTargetHelper {
+ public:
+ explicit RTCDtlsTransport(nsPIDOMWindowInner* aWindow);
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(RTCDtlsTransport,
+ DOMEventTargetHelper)
+
+ // webidl
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+ IMPL_EVENT_HANDLER(statechange)
+ RTCDtlsTransportState State() const { return mState; }
+
+ void UpdateState(TransportLayer::State aState);
+
+ private:
+ virtual ~RTCDtlsTransport() = default;
+
+ RTCDtlsTransportState mState;
+};
+
+} // namespace mozilla::dom
+#endif // _RTCDtlsTransport_h_
diff --git a/dom/media/webrtc/jsapi/RTCRtpReceiver.cpp b/dom/media/webrtc/jsapi/RTCRtpReceiver.cpp
new file mode 100644
index 0000000000..136aa5142f
--- /dev/null
+++ b/dom/media/webrtc/jsapi/RTCRtpReceiver.cpp
@@ -0,0 +1,942 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "RTCRtpReceiver.h"
+#include "PeerConnectionImpl.h"
+#include "mozilla/dom/RTCRtpCapabilitiesBinding.h"
+#include "transport/logging.h"
+#include "mozilla/dom/MediaStreamTrack.h"
+#include "mozilla/dom/Promise.h"
+#include "nsPIDOMWindow.h"
+#include "PrincipalHandle.h"
+#include "nsIPrincipal.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/NullPrincipal.h"
+#include "MediaTrackGraph.h"
+#include "RemoteTrackSource.h"
+#include "libwebrtcglue/RtpRtcpConfig.h"
+#include "nsString.h"
+#include "mozilla/dom/AudioStreamTrack.h"
+#include "mozilla/dom/VideoStreamTrack.h"
+#include "MediaTransportHandler.h"
+#include "jsep/JsepTransceiver.h"
+#include "mozilla/dom/RTCRtpReceiverBinding.h"
+#include "mozilla/dom/RTCRtpSourcesBinding.h"
+#include "RTCStatsReport.h"
+#include "mozilla/Preferences.h"
+#include "PeerConnectionCtx.h"
+#include "RTCRtpTransceiver.h"
+#include "libwebrtcglue/AudioConduit.h"
+#include "call/call.h"
+
+namespace mozilla::dom {
+
+LazyLogModule gReceiverLog("RTCRtpReceiver");
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(RTCRtpReceiver)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(RTCRtpReceiver)
+ // We do not do anything here, we wait for BreakCycles to be called
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(RTCRtpReceiver)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow, mPc, mTransceiver, mTrack,
+ mTrackSource)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(RTCRtpReceiver)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(RTCRtpReceiver)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(RTCRtpReceiver)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+static PrincipalHandle GetPrincipalHandle(nsPIDOMWindowInner* aWindow,
+ PrincipalPrivacy aPrivacy) {
+ // Set the principal used for creating the tracks. This makes the track
+ // data (audio/video samples) accessible to the receiving page. We're
+ // only certain that privacy hasn't been requested if we're connected.
+ nsCOMPtr<nsIScriptObjectPrincipal> winPrincipal = do_QueryInterface(aWindow);
+ RefPtr<nsIPrincipal> principal = winPrincipal->GetPrincipal();
+ if (NS_WARN_IF(!principal)) {
+ principal = NullPrincipal::CreateWithoutOriginAttributes();
+ } else if (aPrivacy == PrincipalPrivacy::Private) {
+ principal = NullPrincipal::CreateWithInheritedAttributes(principal);
+ }
+ return MakePrincipalHandle(principal);
+}
+
+#define INIT_CANONICAL(name, val) \
+ name(AbstractThread::MainThread(), val, \
+ "RTCRtpReceiver::" #name " (Canonical)")
+
+RTCRtpReceiver::RTCRtpReceiver(
+ nsPIDOMWindowInner* aWindow, PrincipalPrivacy aPrivacy,
+ PeerConnectionImpl* aPc, MediaTransportHandler* aTransportHandler,
+ AbstractThread* aCallThread, nsISerialEventTarget* aStsThread,
+ MediaSessionConduit* aConduit, RTCRtpTransceiver* aTransceiver,
+ const TrackingId& aTrackingId)
+ : mWatchManager(this, AbstractThread::MainThread()),
+ mWindow(aWindow),
+ mPc(aPc),
+ mCallThread(aCallThread),
+ mStsThread(aStsThread),
+ mTransportHandler(aTransportHandler),
+ mTransceiver(aTransceiver),
+ INIT_CANONICAL(mSsrc, 0),
+ INIT_CANONICAL(mVideoRtxSsrc, 0),
+ INIT_CANONICAL(mLocalRtpExtensions, RtpExtList()),
+ INIT_CANONICAL(mAudioCodecs, std::vector<AudioCodecConfig>()),
+ INIT_CANONICAL(mVideoCodecs, std::vector<VideoCodecConfig>()),
+ INIT_CANONICAL(mVideoRtpRtcpConfig, Nothing()),
+ INIT_CANONICAL(mReceiving, false) {
+ PrincipalHandle principalHandle = GetPrincipalHandle(aWindow, aPrivacy);
+ const bool isAudio = aConduit->type() == MediaSessionConduit::AUDIO;
+
+ MediaTrackGraph* graph = MediaTrackGraph::GetInstance(
+ isAudio ? MediaTrackGraph::AUDIO_THREAD_DRIVER
+ : MediaTrackGraph::SYSTEM_THREAD_DRIVER,
+ aWindow, MediaTrackGraph::REQUEST_DEFAULT_SAMPLE_RATE,
+ MediaTrackGraph::DEFAULT_OUTPUT_DEVICE);
+
+ if (isAudio) {
+ auto* source = graph->CreateSourceTrack(MediaSegment::AUDIO);
+ mTrackSource = MakeAndAddRef<RemoteTrackSource>(
+ source, this, principalHandle, u"remote audio"_ns, aTrackingId);
+ mTrack = MakeAndAddRef<AudioStreamTrack>(aWindow, source, mTrackSource);
+ mPipeline = MakeAndAddRef<MediaPipelineReceiveAudio>(
+ mPc->GetHandle(), aTransportHandler, aCallThread, mStsThread.get(),
+ *aConduit->AsAudioSessionConduit(), mTrackSource->Stream(), aTrackingId,
+ principalHandle, aPrivacy);
+ } else {
+ auto* source = graph->CreateSourceTrack(MediaSegment::VIDEO);
+ mTrackSource = MakeAndAddRef<RemoteTrackSource>(
+ source, this, principalHandle, u"remote video"_ns, aTrackingId);
+ mTrack = MakeAndAddRef<VideoStreamTrack>(aWindow, source, mTrackSource);
+ mPipeline = MakeAndAddRef<MediaPipelineReceiveVideo>(
+ mPc->GetHandle(), aTransportHandler, aCallThread, mStsThread.get(),
+ *aConduit->AsVideoSessionConduit(), mTrackSource->Stream(), aTrackingId,
+ principalHandle, aPrivacy);
+ }
+
+ mPipeline->InitControl(this);
+
+ // Spec says remote tracks start out muted.
+ mTrackSource->SetMuted(true);
+
+ // Until Bug 1232234 is fixed, we'll get extra RTCP BYES during renegotiation,
+ // so we'll disable muting on RTCP BYE and timeout for now.
+ if (Preferences::GetBool("media.peerconnection.mute_on_bye_or_timeout",
+ false)) {
+ mRtcpByeListener = aConduit->RtcpByeEvent().Connect(
+ GetMainThreadSerialEventTarget(), this, &RTCRtpReceiver::OnRtcpBye);
+ mRtcpTimeoutListener = aConduit->RtcpTimeoutEvent().Connect(
+ GetMainThreadSerialEventTarget(), this, &RTCRtpReceiver::OnRtcpTimeout);
+ }
+
+ mWatchManager.Watch(mReceiveTrackMute,
+ &RTCRtpReceiver::UpdateReceiveTrackMute);
+}
+
+#undef INIT_CANONICAL
+
+RTCRtpReceiver::~RTCRtpReceiver() { MOZ_ASSERT(!mPipeline); }
+
+JSObject* RTCRtpReceiver::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return RTCRtpReceiver_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+RTCDtlsTransport* RTCRtpReceiver::GetTransport() const {
+ if (!mTransceiver) {
+ return nullptr;
+ }
+ return mTransceiver->GetDtlsTransport();
+}
+
+void RTCRtpReceiver::GetCapabilities(
+ const GlobalObject&, const nsAString& aKind,
+ Nullable<dom::RTCRtpCapabilities>& aResult) {
+ PeerConnectionImpl::GetCapabilities(aKind, aResult, sdp::Direction::kRecv);
+}
+
+already_AddRefed<Promise> RTCRtpReceiver::GetStats(ErrorResult& aError) {
+ nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(mWindow);
+ RefPtr<Promise> promise = Promise::Create(global, aError);
+ if (NS_WARN_IF(aError.Failed())) {
+ return nullptr;
+ }
+
+ if (NS_WARN_IF(!mTransceiver)) {
+ // TODO(bug 1056433): When we stop nulling this out when the PC is closed
+ // (or when the transceiver is stopped), we can remove this code. We
+ // resolve instead of reject in order to make this eventual change in
+ // behavior a little smaller.
+ promise->MaybeResolve(new RTCStatsReport(mWindow));
+ return promise.forget();
+ }
+
+ mTransceiver->ChainToDomPromiseWithCodecStats(GetStatsInternal(), promise);
+ return promise.forget();
+}
+
+nsTArray<RefPtr<RTCStatsPromise>> RTCRtpReceiver::GetStatsInternal(
+ bool aSkipIceStats) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsTArray<RefPtr<RTCStatsPromise>> promises(3);
+
+ if (!mPipeline) {
+ return promises;
+ }
+
+ if (!mHaveStartedReceiving) {
+ return promises;
+ }
+
+ nsString recvTrackId;
+ MOZ_ASSERT(mTrack);
+ if (mTrack) {
+ mTrack->GetId(recvTrackId);
+ }
+
+ {
+ // Add bandwidth estimation stats
+ promises.AppendElement(InvokeAsync(
+ mCallThread, __func__,
+ [conduit = mPipeline->mConduit, recvTrackId]() mutable {
+ auto report = MakeUnique<dom::RTCStatsCollection>();
+ const Maybe<webrtc::Call::Stats> stats = conduit->GetCallStats();
+ stats.apply([&](const auto& aStats) {
+ dom::RTCBandwidthEstimationInternal bw;
+ bw.mTrackIdentifier = recvTrackId;
+ bw.mSendBandwidthBps.Construct(aStats.send_bandwidth_bps / 8);
+ bw.mMaxPaddingBps.Construct(aStats.max_padding_bitrate_bps / 8);
+ bw.mReceiveBandwidthBps.Construct(aStats.recv_bandwidth_bps / 8);
+ bw.mPacerDelayMs.Construct(aStats.pacer_delay_ms);
+ if (aStats.rtt_ms >= 0) {
+ bw.mRttMs.Construct(aStats.rtt_ms);
+ }
+ if (!report->mBandwidthEstimations.AppendElement(std::move(bw),
+ fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ });
+ return RTCStatsPromise::CreateAndResolve(std::move(report), __func__);
+ }));
+ }
+
+ promises.AppendElement(
+ InvokeAsync(
+ mCallThread, __func__,
+ [pipeline = mPipeline, recvTrackId] {
+ auto report = MakeUnique<dom::RTCStatsCollection>();
+ auto asAudio = pipeline->mConduit->AsAudioSessionConduit();
+ auto asVideo = pipeline->mConduit->AsVideoSessionConduit();
+
+ nsString kind = asVideo.isNothing() ? u"audio"_ns : u"video"_ns;
+ nsString idstr = kind + u"_"_ns;
+ idstr.AppendInt(static_cast<uint32_t>(pipeline->Level()));
+
+ Maybe<uint32_t> ssrc = pipeline->mConduit->GetRemoteSSRC();
+
+ // Add frame history
+ asVideo.apply([&](const auto& conduit) {
+ if (conduit->AddFrameHistory(&report->mVideoFrameHistories)) {
+ auto& history = report->mVideoFrameHistories.LastElement();
+ history.mTrackIdentifier = recvTrackId;
+ }
+ });
+
+ // TODO(@@NG):ssrcs handle Conduits having multiple stats at the
+ // same level.
+ // This is pending spec work.
+ // Gather pipeline stats.
+ nsString localId = u"inbound_rtp_"_ns + idstr;
+ nsString remoteId;
+
+ auto constructCommonRemoteOutboundRtpStats =
+ [&](RTCRemoteOutboundRtpStreamStats& aRemote,
+ const DOMHighResTimeStamp& aTimestamp) {
+ remoteId = u"inbound_rtcp_"_ns + idstr;
+ aRemote.mTimestamp.Construct(aTimestamp);
+ aRemote.mId.Construct(remoteId);
+ aRemote.mType.Construct(RTCStatsType::Remote_outbound_rtp);
+ ssrc.apply([&](uint32_t aSsrc) { aRemote.mSsrc = aSsrc; });
+ aRemote.mKind = kind;
+ aRemote.mMediaType.Construct(
+ kind); // mediaType is the old name for kind.
+ aRemote.mLocalId.Construct(localId);
+ };
+
+ auto constructCommonInboundRtpStats =
+ [&](RTCInboundRtpStreamStats& aLocal) {
+ aLocal.mTrackIdentifier = recvTrackId;
+ aLocal.mTimestamp.Construct(
+ pipeline->GetTimestampMaker().GetNow().ToDom());
+ aLocal.mId.Construct(localId);
+ aLocal.mType.Construct(RTCStatsType::Inbound_rtp);
+ ssrc.apply([&](uint32_t aSsrc) { aLocal.mSsrc = aSsrc; });
+ aLocal.mKind = kind;
+ aLocal.mMediaType.Construct(
+ kind); // mediaType is the old name for kind.
+ if (remoteId.Length()) {
+ aLocal.mRemoteId.Construct(remoteId);
+ }
+ };
+
+ asAudio.apply([&](auto& aConduit) {
+ Maybe<webrtc::AudioReceiveStreamInterface::Stats> audioStats =
+ aConduit->GetReceiverStats();
+ if (audioStats.isNothing()) {
+ return;
+ }
+
+ if (!audioStats->last_packet_received_timestamp_ms) {
+ // By spec: "The lifetime of all RTP monitored objects starts
+ // when the RTP stream is first used: When the first RTP packet
+ // is sent or received on the SSRC it represents"
+ return;
+ }
+
+ // First, fill in remote stat with rtcp sender data, if present.
+ if (audioStats->last_sender_report_timestamp_ms) {
+ RTCRemoteOutboundRtpStreamStats remote;
+ constructCommonRemoteOutboundRtpStats(
+ remote,
+ RTCStatsTimestamp::FromNtp(
+ aConduit->GetTimestampMaker(),
+ webrtc::Timestamp::Millis(
+ *audioStats->last_sender_report_timestamp_ms) +
+ webrtc::TimeDelta::Seconds(webrtc::kNtpJan1970))
+ .ToDom());
+ remote.mPacketsSent.Construct(
+ audioStats->sender_reports_packets_sent);
+ remote.mBytesSent.Construct(
+ audioStats->sender_reports_bytes_sent);
+ remote.mRemoteTimestamp.Construct(
+ *audioStats->last_sender_report_remote_timestamp_ms);
+ if (!report->mRemoteOutboundRtpStreamStats.AppendElement(
+ std::move(remote), fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ }
+
+ // Then, fill in local side (with cross-link to remote only if
+ // present)
+ RTCInboundRtpStreamStats local;
+ constructCommonInboundRtpStats(local);
+ local.mJitter.Construct(audioStats->jitter_ms / 1000.0);
+ local.mPacketsLost.Construct(audioStats->packets_lost);
+ local.mPacketsReceived.Construct(audioStats->packets_rcvd);
+ local.mPacketsDiscarded.Construct(audioStats->packets_discarded);
+ local.mBytesReceived.Construct(audioStats->payload_bytes_rcvd);
+ // Always missing from libwebrtc stats
+ // if (audioStats->estimated_playout_ntp_timestamp_ms) {
+ // local.mEstimatedPlayoutTimestamp.Construct(
+ // RTCStatsTimestamp::FromNtp(
+ // aConduit->GetTimestampMaker(),
+ // webrtc::Timestamp::Millis(
+ // *audioStats->estimated_playout_ntp_timestamp_ms))
+ // .ToDom());
+ // }
+ local.mJitterBufferDelay.Construct(
+ audioStats->jitter_buffer_delay_seconds);
+ local.mJitterBufferEmittedCount.Construct(
+ audioStats->jitter_buffer_emitted_count);
+ local.mTotalSamplesReceived.Construct(
+ audioStats->total_samples_received);
+ local.mConcealedSamples.Construct(audioStats->concealed_samples);
+ local.mSilentConcealedSamples.Construct(
+ audioStats->silent_concealed_samples);
+ if (audioStats->last_packet_received_timestamp_ms) {
+ local.mLastPacketReceivedTimestamp.Construct(
+ RTCStatsTimestamp::FromNtp(
+ aConduit->GetTimestampMaker(),
+ webrtc::Timestamp::Millis(
+ *audioStats->last_packet_received_timestamp_ms) +
+ webrtc::TimeDelta::Seconds(webrtc::kNtpJan1970))
+ .ToDom());
+ }
+ local.mHeaderBytesReceived.Construct(
+ audioStats->header_and_padding_bytes_rcvd);
+ local.mFecPacketsReceived.Construct(
+ audioStats->fec_packets_received);
+ local.mFecPacketsDiscarded.Construct(
+ audioStats->fec_packets_discarded);
+ local.mConcealmentEvents.Construct(
+ audioStats->concealment_events);
+
+ local.mInsertedSamplesForDeceleration.Construct(
+ audioStats->inserted_samples_for_deceleration);
+ local.mRemovedSamplesForAcceleration.Construct(
+ audioStats->removed_samples_for_acceleration);
+ if (audioStats->audio_level >= 0 &&
+ audioStats->audio_level <= 32767) {
+ local.mAudioLevel.Construct(audioStats->audio_level / 32767.0);
+ }
+ local.mTotalAudioEnergy.Construct(
+ audioStats->total_output_energy);
+ local.mTotalSamplesDuration.Construct(
+ audioStats->total_output_duration);
+
+ if (!report->mInboundRtpStreamStats.AppendElement(
+ std::move(local), fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ });
+
+ asVideo.apply([&](auto& aConduit) {
+ Maybe<webrtc::VideoReceiveStreamInterface::Stats> videoStats =
+ aConduit->GetReceiverStats();
+ if (videoStats.isNothing()) {
+ return;
+ }
+
+ if (!videoStats->rtp_stats.last_packet_received_timestamp_ms) {
+ // By spec: "The lifetime of all RTP monitored objects starts
+ // when the RTP stream is first used: When the first RTP packet
+ // is sent or received on the SSRC it represents"
+ return;
+ }
+
+ // First, fill in remote stat with rtcp sender data, if present.
+ if (videoStats->rtcp_sender_ntp_timestamp_ms) {
+ RTCRemoteOutboundRtpStreamStats remote;
+ constructCommonRemoteOutboundRtpStats(
+ remote, RTCStatsTimestamp::FromNtp(
+ aConduit->GetTimestampMaker(),
+ webrtc::Timestamp::Millis(
+ videoStats->rtcp_sender_ntp_timestamp_ms))
+ .ToDom());
+ remote.mPacketsSent.Construct(
+ videoStats->rtcp_sender_packets_sent);
+ remote.mBytesSent.Construct(
+ videoStats->rtcp_sender_octets_sent);
+ remote.mRemoteTimestamp.Construct(
+ (webrtc::TimeDelta::Millis(
+ videoStats->rtcp_sender_remote_ntp_timestamp_ms) -
+ webrtc::TimeDelta::Seconds(webrtc::kNtpJan1970))
+ .ms());
+ if (!report->mRemoteOutboundRtpStreamStats.AppendElement(
+ std::move(remote), fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ }
+
+ // Then, fill in local side (with cross-link to remote only if
+ // present)
+ RTCInboundRtpStreamStats local;
+ constructCommonInboundRtpStats(local);
+ local.mJitter.Construct(
+ static_cast<double>(videoStats->rtp_stats.jitter) /
+ webrtc::kVideoPayloadTypeFrequency);
+ local.mPacketsLost.Construct(videoStats->rtp_stats.packets_lost);
+ local.mPacketsReceived.Construct(
+ videoStats->rtp_stats.packet_counter.packets);
+ local.mPacketsDiscarded.Construct(videoStats->packets_discarded);
+ local.mDiscardedPackets.Construct(videoStats->packets_discarded);
+ local.mBytesReceived.Construct(
+ videoStats->rtp_stats.packet_counter.payload_bytes);
+
+ // Fill in packet type statistics
+ local.mNackCount.Construct(
+ videoStats->rtcp_packet_type_counts.nack_packets);
+ local.mFirCount.Construct(
+ videoStats->rtcp_packet_type_counts.fir_packets);
+ local.mPliCount.Construct(
+ videoStats->rtcp_packet_type_counts.pli_packets);
+
+ // Lastly, fill in video decoder stats
+ local.mFramesDecoded.Construct(videoStats->frames_decoded);
+
+ local.mFramesPerSecond.Construct(videoStats->decode_frame_rate);
+ local.mFrameWidth.Construct(videoStats->width);
+ local.mFrameHeight.Construct(videoStats->height);
+ // XXX: key_frames + delta_frames may undercount frames because
+ // they were dropped in FrameBuffer::InsertFrame. (bug 1766553)
+ local.mFramesReceived.Construct(
+ videoStats->frame_counts.key_frames +
+ videoStats->frame_counts.delta_frames);
+ local.mJitterBufferDelay.Construct(
+ videoStats->jitter_buffer_delay_seconds);
+ local.mJitterBufferEmittedCount.Construct(
+ videoStats->jitter_buffer_emitted_count);
+
+ if (videoStats->qp_sum) {
+ local.mQpSum.Construct(videoStats->qp_sum.value());
+ }
+ local.mTotalDecodeTime.Construct(
+ double(videoStats->total_decode_time.ms()) / 1000);
+ local.mTotalInterFrameDelay.Construct(
+ videoStats->total_inter_frame_delay);
+ local.mTotalSquaredInterFrameDelay.Construct(
+ videoStats->total_squared_inter_frame_delay);
+ if (videoStats->rtp_stats.last_packet_received_timestamp_ms) {
+ local.mLastPacketReceivedTimestamp.Construct(
+ RTCStatsTimestamp::FromNtp(
+ aConduit->GetTimestampMaker(),
+ webrtc::Timestamp::Millis(
+ *videoStats->rtp_stats
+ .last_packet_received_timestamp_ms) +
+ webrtc::TimeDelta::Seconds(webrtc::kNtpJan1970))
+ .ToDom());
+ }
+ local.mHeaderBytesReceived.Construct(
+ videoStats->rtp_stats.packet_counter.header_bytes +
+ videoStats->rtp_stats.packet_counter.padding_bytes);
+ local.mTotalProcessingDelay.Construct(
+ videoStats->total_processing_delay.seconds<double>());
+ /*
+ * Potential new stats that are now available upstream
+ .if (videoStats->estimated_playout_ntp_timestamp_ms) {
+ local.mEstimatedPlayoutTimestamp.Construct(
+ RTCStatsTimestamp::FromNtp(
+ aConduit->GetTimestampMaker(),
+ webrtc::Timestamp::Millis(
+ *videoStats->estimated_playout_ntp_timestamp_ms))
+ .ToDom());
+ }
+ */
+ // Not including frames dropped in the rendering pipe, which
+ // is not of webrtc's concern anyway?!
+ local.mFramesDropped.Construct(videoStats->frames_dropped);
+ if (!report->mInboundRtpStreamStats.AppendElement(
+ std::move(local), fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ });
+ return RTCStatsPromise::CreateAndResolve(std::move(report),
+ __func__);
+ })
+ ->Then(
+ mStsThread, __func__,
+ [pipeline = mPipeline](UniquePtr<RTCStatsCollection> aReport) {
+ // Fill in Contributing Source statistics
+ if (!aReport->mInboundRtpStreamStats.IsEmpty() &&
+ aReport->mInboundRtpStreamStats[0].mId.WasPassed()) {
+ pipeline->GetContributingSourceStats(
+ aReport->mInboundRtpStreamStats[0].mId.Value(),
+ aReport->mRtpContributingSourceStats);
+ }
+ return RTCStatsPromise::CreateAndResolve(std::move(aReport),
+ __func__);
+ },
+ [] {
+ MOZ_CRASH("Unexpected reject");
+ return RTCStatsPromise::CreateAndReject(NS_ERROR_UNEXPECTED,
+ __func__);
+ }));
+
+ if (!aSkipIceStats && GetJsepTransceiver().mTransport.mComponents) {
+ promises.AppendElement(mTransportHandler->GetIceStats(
+ GetJsepTransceiver().mTransport.mTransportId,
+ mPipeline->GetTimestampMaker().GetNow().ToDom()));
+ }
+
+ return promises;
+}
+
+void RTCRtpReceiver::SetJitterBufferTarget(
+ const Nullable<DOMHighResTimeStamp>& aTargetMs, ErrorResult& aError) {
+ // Spec says jitter buffer target cannot be negative or larger than 4000
+ // milliseconds and to throw RangeError if it is. If an invalid value is
+ // received we return early to preserve the current JitterBufferTarget
+ // internal slot and jitter buffer values.
+ if (mPipeline && mPipeline->mConduit) {
+ if (!aTargetMs.IsNull() &&
+ (aTargetMs.Value() < 0.0 || aTargetMs.Value() > 4000.0)) {
+ aError.ThrowRangeError<MSG_VALUE_OUT_OF_RANGE>("jitterBufferTarget");
+ return;
+ }
+
+ mJitterBufferTarget.reset();
+
+ if (!aTargetMs.IsNull()) {
+ mJitterBufferTarget = Some(aTargetMs.Value());
+ }
+ // If aJitterBufferTarget is null then we are resetting the jitter buffer so
+ // pass the default target of 0.0.
+ mPipeline->mConduit->SetJitterBufferTarget(
+ mJitterBufferTarget.valueOr(0.0));
+ }
+}
+
+void RTCRtpReceiver::GetContributingSources(
+ nsTArray<RTCRtpContributingSource>& aSources) {
+ // Duplicate code...
+ if (mPipeline && mPipeline->mConduit) {
+ nsTArray<dom::RTCRtpSourceEntry> sources;
+ mPipeline->mConduit->GetRtpSources(sources);
+ sources.RemoveElementsBy([](const dom::RTCRtpSourceEntry& aEntry) {
+ return aEntry.mSourceType != dom::RTCRtpSourceEntryType::Contributing;
+ });
+ aSources.ReplaceElementsAt(0, aSources.Length(), sources.Elements(),
+ sources.Length());
+ }
+}
+
+void RTCRtpReceiver::GetSynchronizationSources(
+ nsTArray<dom::RTCRtpSynchronizationSource>& aSources) {
+ // Duplicate code...
+ if (mPipeline && mPipeline->mConduit) {
+ nsTArray<dom::RTCRtpSourceEntry> sources;
+ mPipeline->mConduit->GetRtpSources(sources);
+ sources.RemoveElementsBy([](const dom::RTCRtpSourceEntry& aEntry) {
+ return aEntry.mSourceType != dom::RTCRtpSourceEntryType::Synchronization;
+ });
+ aSources.ReplaceElementsAt(0, aSources.Length(), sources.Elements(),
+ sources.Length());
+ }
+}
+
+nsPIDOMWindowInner* RTCRtpReceiver::GetParentObject() const { return mWindow; }
+
+void RTCRtpReceiver::Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ mWatchManager.Shutdown();
+ if (mPipeline) {
+ mPipeline->Shutdown();
+ mPipeline = nullptr;
+ }
+ if (mTrackSource) {
+ mTrackSource->Destroy();
+ }
+ mCallThread = nullptr;
+ mRtcpByeListener.DisconnectIfExists();
+ mRtcpTimeoutListener.DisconnectIfExists();
+ mUnmuteListener.DisconnectIfExists();
+}
+
+void RTCRtpReceiver::BreakCycles() {
+ mWindow = nullptr;
+ mPc = nullptr;
+ mTrack = nullptr;
+ mTrackSource = nullptr;
+}
+
+void RTCRtpReceiver::UpdateTransport() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!mHaveSetupTransport) {
+ mPipeline->SetLevel(GetJsepTransceiver().GetLevel());
+ mHaveSetupTransport = true;
+ }
+
+ UniquePtr<MediaPipelineFilter> filter;
+
+ auto const& details = GetJsepTransceiver().mRecvTrack.GetNegotiatedDetails();
+ if (GetJsepTransceiver().HasBundleLevel() && details) {
+ std::vector<webrtc::RtpExtension> extmaps;
+ details->ForEachRTPHeaderExtension(
+ [&extmaps](const SdpExtmapAttributeList::Extmap& extmap) {
+ extmaps.emplace_back(extmap.extensionname, extmap.entry);
+ });
+ filter = MakeUnique<MediaPipelineFilter>(extmaps);
+
+ // Add remote SSRCs so we can distinguish which RTP packets actually
+ // belong to this pipeline (also RTCP sender reports).
+ for (uint32_t ssrc : GetJsepTransceiver().mRecvTrack.GetSsrcs()) {
+ filter->AddRemoteSSRC(ssrc);
+ }
+ for (uint32_t ssrc : GetJsepTransceiver().mRecvTrack.GetRtxSsrcs()) {
+ filter->AddRemoteSSRC(ssrc);
+ }
+ auto mid = Maybe<std::string>();
+ if (GetMid() != "") {
+ mid = Some(GetMid());
+ }
+ filter->SetRemoteMediaStreamId(mid);
+
+ // Add unique payload types as a last-ditch fallback
+ auto uniquePts = GetJsepTransceiver()
+ .mRecvTrack.GetNegotiatedDetails()
+ ->GetUniquePayloadTypes();
+ for (unsigned char& uniquePt : uniquePts) {
+ filter->AddUniquePT(uniquePt);
+ }
+ }
+
+ mPipeline->UpdateTransport_m(GetJsepTransceiver().mTransport.mTransportId,
+ std::move(filter));
+}
+
+void RTCRtpReceiver::UpdateConduit() {
+ if (mPipeline->mConduit->type() == MediaSessionConduit::VIDEO) {
+ UpdateVideoConduit();
+ } else {
+ UpdateAudioConduit();
+ }
+
+ if ((mReceiving = mTransceiver->IsReceiving())) {
+ mHaveStartedReceiving = true;
+ }
+}
+
+void RTCRtpReceiver::UpdateVideoConduit() {
+ RefPtr<VideoSessionConduit> conduit =
+ *mPipeline->mConduit->AsVideoSessionConduit();
+
+ // NOTE(pkerr) - this is new behavior. Needed because the
+ // CreateVideoReceiveStream method of the Call API will assert (in debug)
+ // and fail if a value is not provided for the remote_ssrc that will be used
+ // by the far-end sender.
+ if (!GetJsepTransceiver().mRecvTrack.GetSsrcs().empty()) {
+ MOZ_LOG(gReceiverLog, LogLevel::Debug,
+ ("%s[%s]: %s Setting remote SSRC %u", mPc->GetHandle().c_str(),
+ GetMid().c_str(), __FUNCTION__,
+ GetJsepTransceiver().mRecvTrack.GetSsrcs().front()));
+ uint32_t rtxSsrc =
+ GetJsepTransceiver().mRecvTrack.GetRtxSsrcs().empty()
+ ? 0
+ : GetJsepTransceiver().mRecvTrack.GetRtxSsrcs().front();
+ mSsrc = GetJsepTransceiver().mRecvTrack.GetSsrcs().front();
+ mVideoRtxSsrc = rtxSsrc;
+
+ // TODO (bug 1423041) once we pay attention to receiving MID's in RTP
+ // packets (see bug 1405495) we could make this depending on the presence of
+ // MID in the RTP packets instead of relying on the signaling.
+ // In any case, do not disable SSRC changes if no SSRCs were negotiated
+ if (GetJsepTransceiver().HasBundleLevel() &&
+ (!GetJsepTransceiver().mRecvTrack.GetNegotiatedDetails() ||
+ !GetJsepTransceiver().mRecvTrack.GetNegotiatedDetails()->GetExt(
+ webrtc::RtpExtension::kMidUri))) {
+ mCallThread->Dispatch(
+ NewRunnableMethod("VideoSessionConduit::DisableSsrcChanges", conduit,
+ &VideoSessionConduit::DisableSsrcChanges));
+ }
+ }
+
+ if (GetJsepTransceiver().mRecvTrack.GetNegotiatedDetails() &&
+ GetJsepTransceiver().mRecvTrack.GetActive()) {
+ const auto& details(
+ *GetJsepTransceiver().mRecvTrack.GetNegotiatedDetails());
+
+ {
+ std::vector<webrtc::RtpExtension> extmaps;
+ // @@NG read extmap from track
+ details.ForEachRTPHeaderExtension(
+ [&extmaps](const SdpExtmapAttributeList::Extmap& extmap) {
+ extmaps.emplace_back(extmap.extensionname, extmap.entry);
+ });
+ mLocalRtpExtensions = extmaps;
+ }
+
+ std::vector<VideoCodecConfig> configs;
+ RTCRtpTransceiver::NegotiatedDetailsToVideoCodecConfigs(details, &configs);
+ if (configs.empty()) {
+ // TODO: Are we supposed to plumb this error back to JS? This does not
+ // seem like a failure to set an answer, it just means that codec
+ // negotiation failed. For now, we're just doing the same thing we do
+ // if negotiation as a whole failed.
+ MOZ_LOG(gReceiverLog, LogLevel::Error,
+ ("%s[%s]: %s No video codecs were negotiated (recv).",
+ mPc->GetHandle().c_str(), GetMid().c_str(), __FUNCTION__));
+ return;
+ }
+
+ mVideoCodecs = configs;
+ mVideoRtpRtcpConfig = Some(details.GetRtpRtcpConfig());
+ }
+}
+
+void RTCRtpReceiver::UpdateAudioConduit() {
+ RefPtr<AudioSessionConduit> conduit =
+ *mPipeline->mConduit->AsAudioSessionConduit();
+
+ if (!GetJsepTransceiver().mRecvTrack.GetSsrcs().empty()) {
+ MOZ_LOG(gReceiverLog, LogLevel::Debug,
+ ("%s[%s]: %s Setting remote SSRC %u", mPc->GetHandle().c_str(),
+ GetMid().c_str(), __FUNCTION__,
+ GetJsepTransceiver().mRecvTrack.GetSsrcs().front()));
+ mSsrc = GetJsepTransceiver().mRecvTrack.GetSsrcs().front();
+
+ // TODO (bug 1423041) once we pay attention to receiving MID's in RTP
+ // packets (see bug 1405495) we could make this depending on the presence of
+ // MID in the RTP packets instead of relying on the signaling.
+ // In any case, do not disable SSRC changes if no SSRCs were negotiated
+ if (GetJsepTransceiver().HasBundleLevel() &&
+ (!GetJsepTransceiver().mRecvTrack.GetNegotiatedDetails() ||
+ !GetJsepTransceiver().mRecvTrack.GetNegotiatedDetails()->GetExt(
+ webrtc::RtpExtension::kMidUri))) {
+ mCallThread->Dispatch(
+ NewRunnableMethod("AudioSessionConduit::DisableSsrcChanges", conduit,
+ &AudioSessionConduit::DisableSsrcChanges));
+ }
+ }
+
+ if (GetJsepTransceiver().mRecvTrack.GetNegotiatedDetails() &&
+ GetJsepTransceiver().mRecvTrack.GetActive()) {
+ const auto& details(
+ *GetJsepTransceiver().mRecvTrack.GetNegotiatedDetails());
+ std::vector<AudioCodecConfig> configs;
+ RTCRtpTransceiver::NegotiatedDetailsToAudioCodecConfigs(details, &configs);
+ if (configs.empty()) {
+ // TODO: Are we supposed to plumb this error back to JS? This does not
+ // seem like a failure to set an answer, it just means that codec
+ // negotiation failed. For now, we're just doing the same thing we do
+ // if negotiation as a whole failed.
+ MOZ_LOG(gReceiverLog, LogLevel::Error,
+ ("%s[%s]: %s No audio codecs were negotiated (recv)",
+ mPc->GetHandle().c_str(), GetMid().c_str(), __FUNCTION__));
+ return;
+ }
+
+ // Ensure conduit knows about extensions prior to creating streams
+ {
+ std::vector<webrtc::RtpExtension> extmaps;
+ // @@NG read extmap from track
+ details.ForEachRTPHeaderExtension(
+ [&extmaps](const SdpExtmapAttributeList::Extmap& extmap) {
+ extmaps.emplace_back(extmap.extensionname, extmap.entry);
+ });
+ mLocalRtpExtensions = extmaps;
+ }
+
+ mAudioCodecs = configs;
+ }
+}
+
+void RTCRtpReceiver::Stop() {
+ MOZ_ASSERT(mTransceiver->Stopped());
+ mReceiving = false;
+}
+
+bool RTCRtpReceiver::HasTrack(const dom::MediaStreamTrack* aTrack) const {
+ return !aTrack || (mTrack == aTrack);
+}
+
+void RTCRtpReceiver::SyncFromJsep(const JsepTransceiver& aJsepTransceiver) {
+ if (!mPipeline) {
+ return;
+ }
+
+ // Spec says we set [[Receptive]] to true on sLD(sendrecv/recvonly), and to
+ // false on sRD(recvonly/inactive), sLD(sendonly/inactive), or when stop()
+ // is called.
+ bool wasReceptive = mReceptive;
+ mReceptive = aJsepTransceiver.mRecvTrack.GetReceptive();
+ if (!wasReceptive && mReceptive) {
+ mUnmuteListener = mPipeline->mConduit->RtpPacketEvent().Connect(
+ GetMainThreadSerialEventTarget(), this, &RTCRtpReceiver::OnRtpPacket);
+ } else if (wasReceptive && !mReceptive) {
+ mUnmuteListener.DisconnectIfExists();
+ }
+}
+
+void RTCRtpReceiver::SyncToJsep(JsepTransceiver& aJsepTransceiver) const {}
+
+void RTCRtpReceiver::UpdateStreams(StreamAssociationChanges* aChanges) {
+ // We don't sort and use set_difference, because we need to report the
+ // added/removed streams in the order that they appear in the SDP.
+ std::set<std::string> newIds(
+ GetJsepTransceiver().mRecvTrack.GetStreamIds().begin(),
+ GetJsepTransceiver().mRecvTrack.GetStreamIds().end());
+ MOZ_ASSERT(GetJsepTransceiver().mRecvTrack.GetRemoteSetSendBit() ||
+ newIds.empty());
+ bool needsTrackEvent = false;
+ for (const auto& id : mStreamIds) {
+ if (!newIds.count(id)) {
+ aChanges->mStreamAssociationsRemoved.push_back({mTrack, id});
+ }
+ }
+
+ std::set<std::string> oldIds(mStreamIds.begin(), mStreamIds.end());
+ for (const auto& id : GetJsepTransceiver().mRecvTrack.GetStreamIds()) {
+ if (!oldIds.count(id)) {
+ needsTrackEvent = true;
+ aChanges->mStreamAssociationsAdded.push_back({mTrack, id});
+ }
+ }
+
+ mStreamIds = GetJsepTransceiver().mRecvTrack.GetStreamIds();
+
+ if (mRemoteSetSendBit !=
+ GetJsepTransceiver().mRecvTrack.GetRemoteSetSendBit()) {
+ mRemoteSetSendBit = GetJsepTransceiver().mRecvTrack.GetRemoteSetSendBit();
+ if (mRemoteSetSendBit) {
+ needsTrackEvent = true;
+ } else {
+ aChanges->mReceiversToMute.push_back(this);
+ }
+ }
+
+ if (needsTrackEvent) {
+ aChanges->mTrackEvents.push_back({this, mStreamIds});
+ }
+}
+
+void RTCRtpReceiver::UpdatePrincipalPrivacy(PrincipalPrivacy aPrivacy) {
+ if (!mPipeline) {
+ return;
+ }
+
+ if (aPrivacy != PrincipalPrivacy::Private) {
+ return;
+ }
+
+ mPipeline->SetPrivatePrincipal(GetPrincipalHandle(mWindow, aPrivacy));
+}
+
+// test-only: adds fake CSRCs and audio data
+void RTCRtpReceiver::MozInsertAudioLevelForContributingSource(
+ const uint32_t aSource, const DOMHighResTimeStamp aTimestamp,
+ const uint32_t aRtpTimestamp, const bool aHasLevel, const uint8_t aLevel) {
+ if (!mPipeline || mPipeline->IsVideo() || !mPipeline->mConduit) {
+ return;
+ }
+ mPipeline->mConduit->InsertAudioLevelForContributingSource(
+ aSource, aTimestamp, aRtpTimestamp, aHasLevel, aLevel);
+}
+
+void RTCRtpReceiver::OnRtcpBye() { mReceiveTrackMute = true; }
+
+void RTCRtpReceiver::OnRtcpTimeout() { mReceiveTrackMute = true; }
+
+void RTCRtpReceiver::SetTrackMuteFromRemoteSdp() {
+ MOZ_ASSERT(!mReceptive,
+ "PeerConnectionImpl should have blocked unmute events prior to "
+ "firing mute");
+ mReceiveTrackMute = true;
+ // Set the mute state (and fire the mute event) synchronously. Unmute is
+ // handled asynchronously after receiving RTP packets.
+ UpdateReceiveTrackMute();
+ MOZ_ASSERT(mTrack->Muted(), "Muted state was indeed set synchronously");
+}
+
+void RTCRtpReceiver::OnRtpPacket() {
+ MOZ_ASSERT(mReceptive, "We should not be registered unless this is set!");
+ // We should be registered since we're currently getting a callback.
+ mUnmuteListener.Disconnect();
+ if (mReceptive) {
+ mReceiveTrackMute = false;
+ }
+}
+
+void RTCRtpReceiver::UpdateReceiveTrackMute() {
+ if (!mTrack) {
+ return;
+ }
+ if (!mTrackSource) {
+ return;
+ }
+ // This sets the muted state for mTrack and all its clones.
+ // Idempotent -- only reacts to changes.
+ mTrackSource->SetMuted(mReceiveTrackMute);
+}
+
+std::string RTCRtpReceiver::GetMid() const {
+ return mTransceiver->GetMidAscii();
+}
+
+JsepTransceiver& RTCRtpReceiver::GetJsepTransceiver() {
+ MOZ_ASSERT(mTransceiver);
+ return mTransceiver->GetJsepTransceiver();
+}
+
+const JsepTransceiver& RTCRtpReceiver::GetJsepTransceiver() const {
+ MOZ_ASSERT(mTransceiver);
+ return mTransceiver->GetJsepTransceiver();
+}
+
+} // namespace mozilla::dom
+
+#undef LOGTAG
diff --git a/dom/media/webrtc/jsapi/RTCRtpReceiver.h b/dom/media/webrtc/jsapi/RTCRtpReceiver.h
new file mode 100644
index 0000000000..2c050bceb1
--- /dev/null
+++ b/dom/media/webrtc/jsapi/RTCRtpReceiver.h
@@ -0,0 +1,198 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _RTCRtpReceiver_h_
+#define _RTCRtpReceiver_h_
+
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/StateMirroring.h"
+#include "mozilla/Maybe.h"
+#include "js/RootingAPI.h"
+#include "libwebrtcglue/RtpRtcpConfig.h"
+#include "nsTArray.h"
+#include "mozilla/dom/RTCRtpCapabilitiesBinding.h"
+#include "mozilla/dom/RTCStatsReportBinding.h"
+#include "PerformanceRecorder.h"
+#include "RTCStatsReport.h"
+#include "transportbridge/MediaPipeline.h"
+#include <vector>
+
+class nsPIDOMWindowInner;
+
+namespace mozilla {
+class MediaSessionConduit;
+class MediaTransportHandler;
+class JsepTransceiver;
+class PeerConnectionImpl;
+enum class PrincipalPrivacy : uint8_t;
+class RemoteTrackSource;
+
+namespace dom {
+class MediaStreamTrack;
+class Promise;
+class RTCDtlsTransport;
+struct RTCRtpCapabilities;
+struct RTCRtpContributingSource;
+struct RTCRtpSynchronizationSource;
+class RTCRtpTransceiver;
+
+class RTCRtpReceiver : public nsISupports,
+ public nsWrapperCache,
+ public MediaPipelineReceiveControlInterface {
+ public:
+ RTCRtpReceiver(nsPIDOMWindowInner* aWindow, PrincipalPrivacy aPrivacy,
+ PeerConnectionImpl* aPc,
+ MediaTransportHandler* aTransportHandler,
+ AbstractThread* aCallThread, nsISerialEventTarget* aStsThread,
+ MediaSessionConduit* aConduit, RTCRtpTransceiver* aTransceiver,
+ const TrackingId& aTrackingId);
+
+ // nsISupports
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(RTCRtpReceiver)
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // webidl
+ MediaStreamTrack* Track() const { return mTrack; }
+ RTCDtlsTransport* GetTransport() const;
+ static void GetCapabilities(const GlobalObject&, const nsAString& aKind,
+ Nullable<dom::RTCRtpCapabilities>& aResult);
+ already_AddRefed<Promise> GetStats(ErrorResult& aError);
+ void GetContributingSources(
+ nsTArray<dom::RTCRtpContributingSource>& aSources);
+ void GetSynchronizationSources(
+ nsTArray<dom::RTCRtpSynchronizationSource>& aSources);
+ // test-only: insert fake CSRCs and audio levels for testing
+ void MozInsertAudioLevelForContributingSource(
+ const uint32_t aSource, const DOMHighResTimeStamp aTimestamp,
+ const uint32_t aRtpTimestamp, const bool aHasLevel, const uint8_t aLevel);
+
+ nsPIDOMWindowInner* GetParentObject() const;
+ nsTArray<RefPtr<RTCStatsPromise>> GetStatsInternal(
+ bool aSkipIceStats = false);
+ Nullable<DOMHighResTimeStamp> GetJitterBufferTarget(
+ ErrorResult& aError) const {
+ return mJitterBufferTarget.isSome() ? Nullable(mJitterBufferTarget.value())
+ : Nullable<DOMHighResTimeStamp>();
+ }
+ void SetJitterBufferTarget(const Nullable<DOMHighResTimeStamp>& aTargetMs,
+ ErrorResult& aError);
+
+ void Shutdown();
+ void BreakCycles();
+ // Terminal state, reached through stopping RTCRtpTransceiver.
+ void Stop();
+ bool HasTrack(const dom::MediaStreamTrack* aTrack) const;
+ void SyncToJsep(JsepTransceiver& aJsepTransceiver) const;
+ void SyncFromJsep(const JsepTransceiver& aJsepTransceiver);
+ const std::vector<std::string>& GetStreamIds() const { return mStreamIds; }
+
+ struct StreamAssociation {
+ RefPtr<MediaStreamTrack> mTrack;
+ std::string mStreamId;
+ };
+
+ struct TrackEventInfo {
+ RefPtr<RTCRtpReceiver> mReceiver;
+ std::vector<std::string> mStreamIds;
+ };
+
+ struct StreamAssociationChanges {
+ std::vector<RefPtr<RTCRtpReceiver>> mReceiversToMute;
+ std::vector<StreamAssociation> mStreamAssociationsRemoved;
+ std::vector<StreamAssociation> mStreamAssociationsAdded;
+ std::vector<TrackEventInfo> mTrackEvents;
+ };
+
+ // This is called when we set an answer (ie; when the transport is finalized).
+ void UpdateTransport();
+ void UpdateConduit();
+
+ // This is called when we set a remote description; may be an offer or answer.
+ void UpdateStreams(StreamAssociationChanges* aChanges);
+
+ // Called when the privacy-needed state changes on the fly, as a result of
+ // ALPN negotiation.
+ void UpdatePrincipalPrivacy(PrincipalPrivacy aPrivacy);
+
+ void OnRtcpBye();
+ void OnRtcpTimeout();
+
+ void SetTrackMuteFromRemoteSdp();
+ void OnRtpPacket();
+ void UpdateUnmuteBlockingState();
+ void UpdateReceiveTrackMute();
+
+ AbstractCanonical<Ssrc>* CanonicalSsrc() { return &mSsrc; }
+ AbstractCanonical<Ssrc>* CanonicalVideoRtxSsrc() { return &mVideoRtxSsrc; }
+ AbstractCanonical<RtpExtList>* CanonicalLocalRtpExtensions() {
+ return &mLocalRtpExtensions;
+ }
+
+ AbstractCanonical<std::vector<AudioCodecConfig>>* CanonicalAudioCodecs() {
+ return &mAudioCodecs;
+ }
+
+ AbstractCanonical<std::vector<VideoCodecConfig>>* CanonicalVideoCodecs() {
+ return &mVideoCodecs;
+ }
+ AbstractCanonical<Maybe<RtpRtcpConfig>>* CanonicalVideoRtpRtcpConfig() {
+ return &mVideoRtpRtcpConfig;
+ }
+ AbstractCanonical<bool>* CanonicalReceiving() override { return &mReceiving; }
+
+ private:
+ virtual ~RTCRtpReceiver();
+
+ void UpdateVideoConduit();
+ void UpdateAudioConduit();
+
+ std::string GetMid() const;
+ JsepTransceiver& GetJsepTransceiver();
+ const JsepTransceiver& GetJsepTransceiver() const;
+
+ WatchManager<RTCRtpReceiver> mWatchManager;
+ nsCOMPtr<nsPIDOMWindowInner> mWindow;
+ RefPtr<PeerConnectionImpl> mPc;
+ bool mHaveStartedReceiving = false;
+ bool mHaveSetupTransport = false;
+ RefPtr<AbstractThread> mCallThread;
+ nsCOMPtr<nsISerialEventTarget> mStsThread;
+ RefPtr<dom::MediaStreamTrack> mTrack;
+ RefPtr<RemoteTrackSource> mTrackSource;
+ RefPtr<MediaPipelineReceive> mPipeline;
+ RefPtr<MediaTransportHandler> mTransportHandler;
+ RefPtr<RTCRtpTransceiver> mTransceiver;
+ // This is [[AssociatedRemoteMediaStreams]], basically. We do not keep the
+ // streams themselves here, because that would require this object to know
+ // where the stream list for the whole RTCPeerConnection lives..
+ std::vector<std::string> mStreamIds;
+ bool mRemoteSetSendBit = false;
+ Watchable<bool> mReceiveTrackMute{true, "RTCRtpReceiver::mReceiveTrackMute"};
+ // This corresponds to the [[Receptive]] slot on RTCRtpTransceiver.
+ // Its only purpose is suppressing unmute events if true.
+ bool mReceptive = false;
+ // This is the [[JitterBufferTarget]] internal slot.
+ Maybe<DOMHighResTimeStamp> mJitterBufferTarget;
+
+ MediaEventListener mRtcpByeListener;
+ MediaEventListener mRtcpTimeoutListener;
+ MediaEventListener mUnmuteListener;
+
+ Canonical<Ssrc> mSsrc;
+ Canonical<Ssrc> mVideoRtxSsrc;
+ Canonical<RtpExtList> mLocalRtpExtensions;
+ Canonical<std::vector<AudioCodecConfig>> mAudioCodecs;
+ Canonical<std::vector<VideoCodecConfig>> mVideoCodecs;
+ Canonical<Maybe<RtpRtcpConfig>> mVideoRtpRtcpConfig;
+ Canonical<bool> mReceiving;
+};
+
+} // namespace dom
+} // namespace mozilla
+#endif // _RTCRtpReceiver_h_
diff --git a/dom/media/webrtc/jsapi/RTCRtpSender.cpp b/dom/media/webrtc/jsapi/RTCRtpSender.cpp
new file mode 100644
index 0000000000..568a83e8d1
--- /dev/null
+++ b/dom/media/webrtc/jsapi/RTCRtpSender.cpp
@@ -0,0 +1,1654 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "RTCRtpSender.h"
+#include "transport/logging.h"
+#include "mozilla/dom/MediaStreamTrack.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/glean/GleanMetrics.h"
+#include "nsPIDOMWindow.h"
+#include "nsString.h"
+#include "mozilla/dom/VideoStreamTrack.h"
+#include "jsep/JsepTransceiver.h"
+#include "mozilla/dom/RTCRtpSenderBinding.h"
+#include "RTCStatsReport.h"
+#include "mozilla/Preferences.h"
+#include "RTCRtpTransceiver.h"
+#include "PeerConnectionImpl.h"
+#include "libwebrtcglue/AudioConduit.h"
+#include <vector>
+#include "call/call.h"
+
+namespace mozilla::dom {
+
+LazyLogModule gSenderLog("RTCRtpSender");
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(RTCRtpSender)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(RTCRtpSender)
+ // We do not do anything here, we wait for BreakCycles to be called
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(RTCRtpSender)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow, mPc, mSenderTrack, mTransceiver,
+ mStreams, mDtmf)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(RTCRtpSender)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(RTCRtpSender)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(RTCRtpSender)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+#define INIT_CANONICAL(name, val) \
+ name(AbstractThread::MainThread(), val, "RTCRtpSender::" #name " (Canonical)")
+
+RTCRtpSender::RTCRtpSender(nsPIDOMWindowInner* aWindow, PeerConnectionImpl* aPc,
+ MediaTransportHandler* aTransportHandler,
+ AbstractThread* aCallThread,
+ nsISerialEventTarget* aStsThread,
+ MediaSessionConduit* aConduit,
+ dom::MediaStreamTrack* aTrack,
+ const Sequence<RTCRtpEncodingParameters>& aEncodings,
+ RTCRtpTransceiver* aTransceiver)
+ : mWatchManager(this, AbstractThread::MainThread()),
+ mWindow(aWindow),
+ mPc(aPc),
+ mSenderTrack(aTrack),
+ mTransportHandler(aTransportHandler),
+ mTransceiver(aTransceiver),
+ INIT_CANONICAL(mSsrcs, Ssrcs()),
+ INIT_CANONICAL(mVideoRtxSsrcs, Ssrcs()),
+ INIT_CANONICAL(mLocalRtpExtensions, RtpExtList()),
+ INIT_CANONICAL(mAudioCodec, Nothing()),
+ INIT_CANONICAL(mVideoCodec, Nothing()),
+ INIT_CANONICAL(mVideoRtpRtcpConfig, Nothing()),
+ INIT_CANONICAL(mVideoCodecMode, webrtc::VideoCodecMode::kRealtimeVideo),
+ INIT_CANONICAL(mCname, std::string()),
+ INIT_CANONICAL(mTransmitting, false) {
+ mPipeline = new MediaPipelineTransmit(
+ mPc->GetHandle(), aTransportHandler, aCallThread, aStsThread,
+ aConduit->type() == MediaSessionConduit::VIDEO, aConduit);
+ mPipeline->InitControl(this);
+
+ if (aConduit->type() == MediaSessionConduit::AUDIO) {
+ mDtmf = new RTCDTMFSender(aWindow, mTransceiver);
+ }
+ mPipeline->SetTrack(mSenderTrack);
+
+ mozilla::glean::rtcrtpsender::count.Add(1);
+
+ if (mPc->ShouldAllowOldSetParameters()) {
+ mAllowOldSetParameters = true;
+ mozilla::glean::rtcrtpsender::count_setparameters_compat.Add(1);
+ }
+
+ if (aEncodings.Length()) {
+ // This sender was created by addTransceiver with sendEncodings.
+ mParameters.mEncodings = aEncodings;
+ mSimulcastEnvelopeSet = true;
+ mozilla::glean::rtcrtpsender::used_sendencodings.AddToNumerator(1);
+ } else {
+ // This sender was created by addTrack, sRD(offer), or addTransceiver
+ // without sendEncodings.
+ RTCRtpEncodingParameters defaultEncoding;
+ defaultEncoding.mActive = true;
+ if (aConduit->type() == MediaSessionConduit::VIDEO) {
+ defaultEncoding.mScaleResolutionDownBy.Construct(1.0f);
+ }
+ Unused << mParameters.mEncodings.AppendElement(defaultEncoding, fallible);
+ UpdateRestorableEncodings(mParameters.mEncodings);
+ MaybeGetJsepRids();
+ }
+
+ if (mDtmf) {
+ mWatchManager.Watch(mTransmitting, &RTCRtpSender::UpdateDtmfSender);
+ }
+}
+
+#undef INIT_CANONICAL
+
+RTCRtpSender::~RTCRtpSender() = default;
+
+JSObject* RTCRtpSender::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return RTCRtpSender_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+RTCDtlsTransport* RTCRtpSender::GetTransport() const {
+ if (!mTransceiver) {
+ return nullptr;
+ }
+ return mTransceiver->GetDtlsTransport();
+}
+
+RTCDTMFSender* RTCRtpSender::GetDtmf() const { return mDtmf; }
+
+already_AddRefed<Promise> RTCRtpSender::GetStats(ErrorResult& aError) {
+ RefPtr<Promise> promise = MakePromise(aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+ if (NS_WARN_IF(!mPipeline)) {
+ // TODO(bug 1056433): When we stop nulling this out when the PC is closed
+ // (or when the transceiver is stopped), we can remove this code. We
+ // resolve instead of reject in order to make this eventual change in
+ // behavior a little smaller.
+ promise->MaybeResolve(new RTCStatsReport(mWindow));
+ return promise.forget();
+ }
+
+ if (!mSenderTrack) {
+ promise->MaybeResolve(new RTCStatsReport(mWindow));
+ return promise.forget();
+ }
+
+ mTransceiver->ChainToDomPromiseWithCodecStats(GetStatsInternal(), promise);
+ return promise.forget();
+}
+
+nsTArray<RefPtr<dom::RTCStatsPromise>> RTCRtpSender::GetStatsInternal(
+ bool aSkipIceStats) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsTArray<RefPtr<RTCStatsPromise>> promises(2);
+ if (!mSenderTrack || !mPipeline) {
+ return promises;
+ }
+
+ nsAutoString trackName;
+ if (auto track = mPipeline->GetTrack()) {
+ track->GetId(trackName);
+ }
+
+ {
+ // Add bandwidth estimation stats
+ promises.AppendElement(InvokeAsync(
+ mPipeline->mCallThread, __func__,
+ [conduit = mPipeline->mConduit, trackName]() mutable {
+ auto report = MakeUnique<dom::RTCStatsCollection>();
+ Maybe<webrtc::Call::Stats> stats = conduit->GetCallStats();
+ stats.apply([&](const auto aStats) {
+ dom::RTCBandwidthEstimationInternal bw;
+ bw.mTrackIdentifier = trackName;
+ bw.mSendBandwidthBps.Construct(aStats.send_bandwidth_bps / 8);
+ bw.mMaxPaddingBps.Construct(aStats.max_padding_bitrate_bps / 8);
+ bw.mReceiveBandwidthBps.Construct(aStats.recv_bandwidth_bps / 8);
+ bw.mPacerDelayMs.Construct(aStats.pacer_delay_ms);
+ if (aStats.rtt_ms >= 0) {
+ bw.mRttMs.Construct(aStats.rtt_ms);
+ }
+ if (!report->mBandwidthEstimations.AppendElement(std::move(bw),
+ fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ });
+ return RTCStatsPromise::CreateAndResolve(std::move(report), __func__);
+ }));
+ }
+
+ promises.AppendElement(InvokeAsync(
+ mPipeline->mCallThread, __func__, [pipeline = mPipeline, trackName] {
+ auto report = MakeUnique<dom::RTCStatsCollection>();
+ auto asAudio = pipeline->mConduit->AsAudioSessionConduit();
+ auto asVideo = pipeline->mConduit->AsVideoSessionConduit();
+
+ nsString kind = asVideo.isNothing() ? u"audio"_ns : u"video"_ns;
+ nsString idstr = kind + u"_"_ns;
+ idstr.AppendInt(static_cast<uint32_t>(pipeline->Level()));
+
+ for (uint32_t ssrc : pipeline->mConduit->GetLocalSSRCs()) {
+ nsString localId = u"outbound_rtp_"_ns + idstr + u"_"_ns;
+ localId.AppendInt(ssrc);
+ nsString remoteId;
+ Maybe<uint16_t> base_seq =
+ pipeline->mConduit->RtpSendBaseSeqFor(ssrc);
+
+ auto constructCommonRemoteInboundRtpStats =
+ [&](RTCRemoteInboundRtpStreamStats& aRemote,
+ const webrtc::ReportBlockData& aRtcpData) {
+ remoteId = u"outbound_rtcp_"_ns + idstr + u"_"_ns;
+ remoteId.AppendInt(ssrc);
+ aRemote.mTimestamp.Construct(
+ RTCStatsTimestamp::FromNtp(
+ pipeline->GetTimestampMaker(),
+ webrtc::Timestamp::Micros(
+ aRtcpData.report_block_timestamp_utc_us()) +
+ webrtc::TimeDelta::Seconds(webrtc::kNtpJan1970))
+ .ToDom());
+ aRemote.mId.Construct(remoteId);
+ aRemote.mType.Construct(RTCStatsType::Remote_inbound_rtp);
+ aRemote.mSsrc = ssrc;
+ aRemote.mKind = kind;
+ aRemote.mMediaType.Construct(
+ kind); // mediaType is the old name for kind.
+ aRemote.mLocalId.Construct(localId);
+ if (base_seq) {
+ if (aRtcpData.report_block()
+ .extended_highest_sequence_number < *base_seq) {
+ aRemote.mPacketsReceived.Construct(0);
+ } else {
+ aRemote.mPacketsReceived.Construct(
+ aRtcpData.report_block()
+ .extended_highest_sequence_number -
+ aRtcpData.report_block().packets_lost - *base_seq + 1);
+ }
+ }
+ };
+
+ auto constructCommonOutboundRtpStats =
+ [&](RTCOutboundRtpStreamStats& aLocal) {
+ aLocal.mSsrc = ssrc;
+ aLocal.mTimestamp.Construct(
+ pipeline->GetTimestampMaker().GetNow().ToDom());
+ aLocal.mId.Construct(localId);
+ aLocal.mType.Construct(RTCStatsType::Outbound_rtp);
+ aLocal.mKind = kind;
+ aLocal.mMediaType.Construct(
+ kind); // mediaType is the old name for kind.
+ if (remoteId.Length()) {
+ aLocal.mRemoteId.Construct(remoteId);
+ }
+ };
+
+ asAudio.apply([&](auto& aConduit) {
+ Maybe<webrtc::AudioSendStream::Stats> audioStats =
+ aConduit->GetSenderStats();
+ if (audioStats.isNothing()) {
+ return;
+ }
+
+ if (audioStats->packets_sent == 0) {
+ // By spec: "The lifetime of all RTP monitored objects starts
+ // when the RTP stream is first used: When the first RTP packet
+ // is sent or received on the SSRC it represents"
+ return;
+ }
+
+ // First, fill in remote stat with rtcp receiver data, if present.
+ // ReceiverReports have less information than SenderReports, so fill
+ // in what we can.
+ Maybe<webrtc::ReportBlockData> reportBlockData;
+ {
+ if (const auto remoteSsrc = aConduit->GetRemoteSSRC();
+ remoteSsrc) {
+ for (auto& data : audioStats->report_block_datas) {
+ if (data.report_block().source_ssrc == ssrc &&
+ data.report_block().sender_ssrc == *remoteSsrc) {
+ reportBlockData.emplace(data);
+ break;
+ }
+ }
+ }
+ }
+ reportBlockData.apply([&](auto& aReportBlockData) {
+ RTCRemoteInboundRtpStreamStats remote;
+ constructCommonRemoteInboundRtpStats(remote, aReportBlockData);
+ if (audioStats->jitter_ms >= 0) {
+ remote.mJitter.Construct(audioStats->jitter_ms / 1000.0);
+ }
+ if (audioStats->packets_lost >= 0) {
+ remote.mPacketsLost.Construct(audioStats->packets_lost);
+ }
+ if (audioStats->rtt_ms >= 0) {
+ remote.mRoundTripTime.Construct(
+ static_cast<double>(audioStats->rtt_ms) / 1000.0);
+ }
+ remote.mFractionLost.Construct(audioStats->fraction_lost);
+ remote.mTotalRoundTripTime.Construct(
+ double(aReportBlockData.sum_rtt_ms()) / 1000);
+ remote.mRoundTripTimeMeasurements.Construct(
+ aReportBlockData.num_rtts());
+ if (!report->mRemoteInboundRtpStreamStats.AppendElement(
+ std::move(remote), fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ });
+
+ // Then, fill in local side (with cross-link to remote only if
+ // present)
+ RTCOutboundRtpStreamStats local;
+ constructCommonOutboundRtpStats(local);
+ local.mPacketsSent.Construct(audioStats->packets_sent);
+ local.mBytesSent.Construct(audioStats->payload_bytes_sent);
+ local.mNackCount.Construct(
+ audioStats->rtcp_packet_type_counts.nack_packets);
+ local.mHeaderBytesSent.Construct(
+ audioStats->header_and_padding_bytes_sent);
+ local.mRetransmittedPacketsSent.Construct(
+ audioStats->retransmitted_packets_sent);
+ local.mRetransmittedBytesSent.Construct(
+ audioStats->retransmitted_bytes_sent);
+ /*
+ * Potential new stats that are now available upstream.
+ * Note: when we last tried exposing this we were getting
+ * targetBitrate for audio was ending up as 0. We did not
+ * investigate why.
+ local.mTargetBitrate.Construct(audioStats->target_bitrate_bps);
+ */
+ if (!report->mOutboundRtpStreamStats.AppendElement(std::move(local),
+ fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ });
+
+ asVideo.apply([&](auto& aConduit) {
+ Maybe<webrtc::VideoSendStream::Stats> videoStats =
+ aConduit->GetSenderStats();
+ if (videoStats.isNothing()) {
+ return;
+ }
+
+ Maybe<webrtc::VideoSendStream::StreamStats> streamStats;
+ auto kv = videoStats->substreams.find(ssrc);
+ if (kv != videoStats->substreams.end()) {
+ streamStats = Some(kv->second);
+ }
+
+ if (!streamStats) {
+ // By spec: "The lifetime of all RTP monitored objects starts
+ // when the RTP stream is first used: When the first RTP packet
+ // is sent or received on the SSRC it represents"
+ return;
+ }
+
+ aConduit->GetAssociatedLocalRtxSSRC(ssrc).apply(
+ [&](const auto rtxSsrc) {
+ auto kv = videoStats->substreams.find(rtxSsrc);
+ if (kv != videoStats->substreams.end()) {
+ streamStats->rtp_stats.Add(kv->second.rtp_stats);
+ }
+ });
+
+ if (streamStats->rtp_stats.first_packet_time_ms == -1) {
+ return;
+ }
+
+ // First, fill in remote stat with rtcp receiver data, if present.
+ // ReceiverReports have less information than SenderReports, so fill
+ // in what we can.
+ if (streamStats->report_block_data) {
+ const webrtc::ReportBlockData& rtcpReportData =
+ *streamStats->report_block_data;
+ RTCRemoteInboundRtpStreamStats remote;
+ remote.mJitter.Construct(
+ static_cast<double>(rtcpReportData.report_block().jitter) /
+ webrtc::kVideoPayloadTypeFrequency);
+ remote.mPacketsLost.Construct(
+ rtcpReportData.report_block().packets_lost);
+ if (rtcpReportData.has_rtt()) {
+ remote.mRoundTripTime.Construct(
+ static_cast<double>(rtcpReportData.last_rtt_ms()) / 1000.0);
+ }
+ constructCommonRemoteInboundRtpStats(remote, rtcpReportData);
+ remote.mTotalRoundTripTime.Construct(
+ streamStats->report_block_data->sum_rtt_ms() / 1000.0);
+ remote.mFractionLost.Construct(
+ static_cast<float>(
+ rtcpReportData.report_block().fraction_lost) /
+ (1 << 8));
+ remote.mRoundTripTimeMeasurements.Construct(
+ streamStats->report_block_data->num_rtts());
+ if (!report->mRemoteInboundRtpStreamStats.AppendElement(
+ std::move(remote), fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ }
+
+ // Then, fill in local side (with cross-link to remote only if
+ // present)
+ RTCOutboundRtpStreamStats local;
+ constructCommonOutboundRtpStats(local);
+ local.mPacketsSent.Construct(
+ streamStats->rtp_stats.transmitted.packets);
+ local.mBytesSent.Construct(
+ streamStats->rtp_stats.transmitted.payload_bytes);
+ local.mNackCount.Construct(
+ streamStats->rtcp_packet_type_counts.nack_packets);
+ local.mFirCount.Construct(
+ streamStats->rtcp_packet_type_counts.fir_packets);
+ local.mPliCount.Construct(
+ streamStats->rtcp_packet_type_counts.pli_packets);
+ local.mFramesEncoded.Construct(streamStats->frames_encoded);
+ if (streamStats->qp_sum) {
+ local.mQpSum.Construct(*streamStats->qp_sum);
+ }
+ local.mHeaderBytesSent.Construct(
+ streamStats->rtp_stats.transmitted.header_bytes +
+ streamStats->rtp_stats.transmitted.padding_bytes);
+ local.mRetransmittedPacketsSent.Construct(
+ streamStats->rtp_stats.retransmitted.packets);
+ local.mRetransmittedBytesSent.Construct(
+ streamStats->rtp_stats.retransmitted.payload_bytes);
+ local.mTotalEncodedBytesTarget.Construct(
+ videoStats->total_encoded_bytes_target);
+ local.mFrameWidth.Construct(streamStats->width);
+ local.mFrameHeight.Construct(streamStats->height);
+ local.mFramesSent.Construct(streamStats->frames_encoded);
+ local.mHugeFramesSent.Construct(streamStats->huge_frames_sent);
+ local.mTotalEncodeTime.Construct(
+ double(streamStats->total_encode_time_ms) / 1000.);
+ /*
+ * Potential new stats that are now available upstream.
+ local.mTargetBitrate.Construct(videoStats->target_media_bitrate_bps);
+ */
+ if (!report->mOutboundRtpStreamStats.AppendElement(std::move(local),
+ fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ });
+ }
+
+ auto constructCommonMediaSourceStats =
+ [&](RTCMediaSourceStats& aStats) {
+ nsString id = u"mediasource_"_ns + idstr + trackName;
+ aStats.mTimestamp.Construct(
+ pipeline->GetTimestampMaker().GetNow().ToDom());
+ aStats.mId.Construct(id);
+ aStats.mType.Construct(RTCStatsType::Media_source);
+ aStats.mTrackIdentifier = trackName;
+ aStats.mKind = kind;
+ };
+
+ // TODO(bug 1804678): Use RTCAudioSourceStats/RTCVideoSourceStats
+ RTCMediaSourceStats mediaSourceStats;
+ constructCommonMediaSourceStats(mediaSourceStats);
+ if (!report->mMediaSourceStats.AppendElement(
+ std::move(mediaSourceStats), fallible)) {
+ mozalloc_handle_oom(0);
+ }
+
+ return RTCStatsPromise::CreateAndResolve(std::move(report), __func__);
+ }));
+
+ if (!aSkipIceStats && GetJsepTransceiver().mTransport.mComponents) {
+ promises.AppendElement(mTransportHandler->GetIceStats(
+ GetJsepTransceiver().mTransport.mTransportId,
+ mPipeline->GetTimestampMaker().GetNow().ToDom()));
+ }
+
+ return promises;
+}
+
+void RTCRtpSender::GetCapabilities(const GlobalObject&, const nsAString& aKind,
+ Nullable<dom::RTCRtpCapabilities>& aResult) {
+ PeerConnectionImpl::GetCapabilities(aKind, aResult, sdp::Direction::kSend);
+}
+
+void RTCRtpSender::WarnAboutBadSetParameters(const nsCString& aError) {
+ nsCString warning(
+ "WARNING! Invalid setParameters call detected! The good news? Firefox "
+ "supports sendEncodings in addTransceiver now, so we ask that you switch "
+ "over to using the parameters code you use for other browsers. Thank you "
+ "for your patience and support. The specific error was: ");
+ warning += aError;
+ mPc->SendWarningToConsole(warning);
+}
+
+nsCString RTCRtpSender::GetEffectiveTLDPlus1() const {
+ return mPc->GetEffectiveTLDPlus1();
+}
+
+already_AddRefed<Promise> RTCRtpSender::SetParameters(
+ const dom::RTCRtpSendParameters& aParameters, ErrorResult& aError) {
+ dom::RTCRtpSendParameters paramsCopy(aParameters);
+ // When the setParameters method is called, the user agent MUST run the
+ // following steps:
+ // Let parameters be the method's first argument.
+ // Let sender be the RTCRtpSender object on which setParameters is invoked.
+ // Let transceiver be the RTCRtpTransceiver object associated with sender
+ // (i.e.sender is transceiver.[[Sender]]).
+
+ RefPtr<dom::Promise> p = MakePromise(aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+
+ if (mPc->IsClosed()) {
+ p->MaybeRejectWithInvalidStateError("Peer connection is closed");
+ return p.forget();
+ }
+
+ // If transceiver.[[Stopped]] is true, return a promise rejected with a newly
+ // created InvalidStateError.
+ if (mTransceiver->Stopped()) {
+ p->MaybeRejectWithInvalidStateError("This sender's transceiver is stopped");
+ return p.forget();
+ }
+
+ // If sender.[[LastReturnedParameters]] is null, return a promise rejected
+ // with a newly created InvalidStateError.
+ if (!mLastReturnedParameters.isSome()) {
+ nsCString error(
+ "Cannot call setParameters without first calling getParameters");
+ if (mAllowOldSetParameters) {
+ if (!mHaveWarnedBecauseNoGetParameters) {
+ mHaveWarnedBecauseNoGetParameters = true;
+ mozilla::glean::rtcrtpsender_setparameters::warn_no_getparameters
+ .AddToNumerator(1);
+#ifdef EARLY_BETA_OR_EARLIER
+ mozilla::glean::rtcrtpsender_setparameters::blame_no_getparameters
+ .Get(GetEffectiveTLDPlus1())
+ .Add(1);
+#endif
+ }
+ WarnAboutBadSetParameters(error);
+ } else {
+ if (!mHaveFailedBecauseNoGetParameters) {
+ mHaveFailedBecauseNoGetParameters = true;
+ mozilla::glean::rtcrtpsender_setparameters::fail_no_getparameters
+ .AddToNumerator(1);
+ }
+ p->MaybeRejectWithInvalidStateError(error);
+ return p.forget();
+ }
+ }
+
+ // According to the spec, our consistency checking is based on
+ // [[LastReturnedParameters]], but if we're letting
+ // [[LastReturnedParameters]]==null slide, we still want to do
+ // consistency checking on _something_ so we can warn implementers if they
+ // are messing that up also. Just find something, _anything_, to do that
+ // checking with.
+ // TODO(bug 1803388): Remove this stuff once it is no longer needed.
+ // TODO(bug 1803389): Remove the glean errors once they are no longer needed.
+ Maybe<RTCRtpSendParameters> oldParams;
+ if (mAllowOldSetParameters) {
+ if (mLastReturnedParameters.isSome()) {
+ oldParams = mLastReturnedParameters;
+ } else if (mPendingParameters.isSome()) {
+ oldParams = mPendingParameters;
+ } else {
+ oldParams = Some(mParameters);
+ }
+ MOZ_ASSERT(oldParams.isSome());
+ } else {
+ oldParams = mLastReturnedParameters;
+ }
+ MOZ_ASSERT(oldParams.isSome());
+
+ // Validate parameters by running the following steps:
+ // Let encodings be parameters.encodings.
+ // Let codecs be parameters.codecs.
+ // Let N be the number of RTCRtpEncodingParameters stored in
+ // sender.[[SendEncodings]].
+ // If any of the following conditions are met,
+ // return a promise rejected with a newly created InvalidModificationError:
+
+ bool pendingRidChangeFromCompatMode = false;
+ // encodings.length is different from N.
+ if (paramsCopy.mEncodings.Length() != oldParams->mEncodings.Length()) {
+ nsCString error("Cannot change the number of encodings with setParameters");
+ if (!mAllowOldSetParameters) {
+ if (!mHaveFailedBecauseEncodingCountChange) {
+ mHaveFailedBecauseEncodingCountChange = true;
+ mozilla::glean::rtcrtpsender_setparameters::fail_length_changed
+ .AddToNumerator(1);
+ }
+ p->MaybeRejectWithInvalidModificationError(error);
+ return p.forget();
+ }
+ // Make sure we don't use the old rids in SyncToJsep while we wait for the
+ // queued task below to update mParameters.
+ pendingRidChangeFromCompatMode = true;
+ mSimulcastEnvelopeSet = true;
+ if (!mHaveWarnedBecauseEncodingCountChange) {
+ mHaveWarnedBecauseEncodingCountChange = true;
+ mozilla::glean::rtcrtpsender_setparameters::warn_length_changed
+ .AddToNumerator(1);
+#ifdef EARLY_BETA_OR_EARLIER
+ mozilla::glean::rtcrtpsender_setparameters::blame_length_changed
+ .Get(GetEffectiveTLDPlus1())
+ .Add(1);
+#endif
+ }
+ WarnAboutBadSetParameters(error);
+ } else {
+ // encodings has been re-ordered.
+ for (size_t i = 0; i < paramsCopy.mEncodings.Length(); ++i) {
+ const auto& oldEncoding = oldParams->mEncodings[i];
+ const auto& newEncoding = paramsCopy.mEncodings[i];
+ if (oldEncoding.mRid != newEncoding.mRid) {
+ nsCString error("Cannot change rid, or reorder encodings");
+ if (!mHaveFailedBecauseRidChange) {
+ mHaveFailedBecauseRidChange = true;
+ mozilla::glean::rtcrtpsender_setparameters::fail_rid_changed
+ .AddToNumerator(1);
+ }
+ p->MaybeRejectWithInvalidModificationError(error);
+ return p.forget();
+ }
+ }
+ }
+
+ // TODO(bug 1803388): Handle this in webidl, once we stop allowing the old
+ // setParameters style.
+ if (!paramsCopy.mTransactionId.WasPassed()) {
+ nsCString error("transactionId is not set!");
+ if (!mAllowOldSetParameters) {
+ if (!mHaveFailedBecauseNoTransactionId) {
+ mHaveFailedBecauseNoTransactionId = true;
+ mozilla::glean::rtcrtpsender_setparameters::fail_no_transactionid
+ .AddToNumerator(1);
+ }
+ p->MaybeRejectWithTypeError(error);
+ return p.forget();
+ }
+ if (!mHaveWarnedBecauseNoTransactionId) {
+ mHaveWarnedBecauseNoTransactionId = true;
+ mozilla::glean::rtcrtpsender_setparameters::warn_no_transactionid
+ .AddToNumerator(1);
+#ifdef EARLY_BETA_OR_EARLIER
+ mozilla::glean::rtcrtpsender_setparameters::blame_no_transactionid
+ .Get(GetEffectiveTLDPlus1())
+ .Add(1);
+#endif
+ }
+ WarnAboutBadSetParameters(error);
+ } else if (oldParams->mTransactionId != paramsCopy.mTransactionId) {
+ // Any parameter in parameters is marked as a Read-only parameter (such as
+ // RID) and has a value that is different from the corresponding parameter
+ // value in sender.[[LastReturnedParameters]]. Note that this also applies
+ // to transactionId.
+ nsCString error(
+ "Cannot change transaction id: call getParameters, modify the result, "
+ "and then call setParameters");
+ if (!mAllowOldSetParameters) {
+ if (!mHaveFailedBecauseStaleTransactionId) {
+ mHaveFailedBecauseStaleTransactionId = true;
+ mozilla::glean::rtcrtpsender_setparameters::fail_stale_transactionid
+ .AddToNumerator(1);
+ }
+ p->MaybeRejectWithInvalidModificationError(error);
+ return p.forget();
+ }
+ if (!mHaveWarnedBecauseStaleTransactionId) {
+ mHaveWarnedBecauseStaleTransactionId = true;
+ mozilla::glean::rtcrtpsender_setparameters::warn_stale_transactionid
+ .AddToNumerator(1);
+#ifdef EARLY_BETA_OR_EARLIER
+ mozilla::glean::rtcrtpsender_setparameters::blame_stale_transactionid
+ .Get(GetEffectiveTLDPlus1())
+ .Add(1);
+#endif
+ }
+ WarnAboutBadSetParameters(error);
+ }
+
+ // This could conceivably happen if we are allowing the old setParameters
+ // behavior.
+ if (!paramsCopy.mEncodings.Length()) {
+ nsCString error("Cannot set an empty encodings array");
+ if (!mAllowOldSetParameters) {
+ if (!mHaveFailedBecauseNoEncodings) {
+ mHaveFailedBecauseNoEncodings = true;
+ mozilla::glean::rtcrtpsender_setparameters::fail_no_encodings
+ .AddToNumerator(1);
+ }
+
+ p->MaybeRejectWithInvalidModificationError(error);
+ return p.forget();
+ }
+ // TODO: Add some warning telemetry here
+ WarnAboutBadSetParameters(error);
+ // Just don't do this; it's stupid.
+ paramsCopy.mEncodings = oldParams->mEncodings;
+ }
+
+ // TODO: Verify remaining read-only parameters
+ // headerExtensions (bug 1765851)
+ // rtcp (bug 1765852)
+ // codecs (bug 1534687)
+
+ // CheckAndRectifyEncodings handles the following steps:
+ // If transceiver kind is "audio", remove the scaleResolutionDownBy member
+ // from all encodings that contain one.
+ //
+ // If transceiver kind is "video", and any encoding in encodings contains a
+ // scaleResolutionDownBy member whose value is less than 1.0, return a
+ // promise rejected with a newly created RangeError.
+ //
+ // Verify that each encoding in encodings has a maxFramerate member whose
+ // value is greater than or equal to 0.0. If one of the maxFramerate values
+ // does not meet this requirement, return a promise rejected with a newly
+ // created RangeError.
+ ErrorResult rv;
+ CheckAndRectifyEncodings(paramsCopy.mEncodings, mTransceiver->IsVideo(), rv);
+ if (rv.Failed()) {
+ if (!mHaveFailedBecauseOtherError) {
+ mHaveFailedBecauseOtherError = true;
+ mozilla::glean::rtcrtpsender_setparameters::fail_other.AddToNumerator(1);
+ }
+ p->MaybeReject(std::move(rv));
+ return p.forget();
+ }
+
+ // If transceiver kind is "video", then for each encoding in encodings that
+ // doesn't contain a scaleResolutionDownBy member, add a
+ // scaleResolutionDownBy member with the value 1.0.
+ if (mTransceiver->IsVideo()) {
+ for (auto& encoding : paramsCopy.mEncodings) {
+ if (!encoding.mScaleResolutionDownBy.WasPassed()) {
+ encoding.mScaleResolutionDownBy.Construct(1.0);
+ }
+ }
+ }
+
+ // Let p be a new promise. (see above)
+
+ // In parallel, configure the media stack to use parameters to transmit
+ // sender.[[SenderTrack]].
+ // Right now this is infallible. That may change someday.
+
+ // We need to put this in a member variable, since MaybeUpdateConduit needs it
+ // This also allows PeerConnectionImpl to detect when there is a pending
+ // setParameters, which has implcations for the handling of
+ // setRemoteDescription.
+ mPendingRidChangeFromCompatMode = pendingRidChangeFromCompatMode;
+ mPendingParameters = Some(paramsCopy);
+ uint32_t serialNumber = ++mNumSetParametersCalls;
+ MaybeUpdateConduit();
+
+ // If the media stack is successfully configured with parameters,
+ // queue a task to run the following steps:
+ GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction(
+ __func__,
+ [this, self = RefPtr<RTCRtpSender>(this), p, paramsCopy, serialNumber] {
+ // Set sender.[[LastReturnedParameters]] to null.
+ mLastReturnedParameters = Nothing();
+ // Set sender.[[SendEncodings]] to parameters.encodings.
+ mParameters = paramsCopy;
+ UpdateRestorableEncodings(mParameters.mEncodings);
+ // Only clear mPendingParameters if it matches; there could have been
+ // back-to-back calls to setParameters, and we only want to clear this
+ // if no subsequent setParameters is pending.
+ if (serialNumber == mNumSetParametersCalls) {
+ mPendingParameters = Nothing();
+ // Ok, nothing has called SyncToJsep while this async task was
+ // pending. No need for special handling anymore.
+ mPendingRidChangeFromCompatMode = false;
+ }
+ MOZ_ASSERT(mParameters.mEncodings.Length());
+ // Resolve p with undefined.
+ p->MaybeResolveWithUndefined();
+ }));
+
+ // Return p.
+ return p.forget();
+}
+
+// static
+void RTCRtpSender::CheckAndRectifyEncodings(
+ Sequence<RTCRtpEncodingParameters>& aEncodings, bool aVideo,
+ ErrorResult& aRv) {
+ // If any encoding contains a rid member whose value does not conform to the
+ // grammar requirements specified in Section 10 of [RFC8851], throw a
+ // TypeError.
+ for (const auto& encoding : aEncodings) {
+ if (encoding.mRid.WasPassed()) {
+ std::string utf8Rid = NS_ConvertUTF16toUTF8(encoding.mRid.Value()).get();
+ std::string error;
+ if (!SdpRidAttributeList::CheckRidValidity(utf8Rid, &error)) {
+ aRv.ThrowTypeError(nsCString(error));
+ return;
+ }
+ if (utf8Rid.size() > SdpRidAttributeList::kMaxRidLength) {
+ std::ostringstream ss;
+ ss << "Rid can be at most " << SdpRidAttributeList::kMaxRidLength
+ << " characters long (due to internal limitations)";
+ aRv.ThrowTypeError(nsCString(ss.str()));
+ return;
+ }
+ }
+ }
+
+ if (aEncodings.Length() > 1) {
+ // If some but not all encodings contain a rid member, throw a TypeError.
+ // rid must be set if there is more than one encoding
+ // NOTE: Since rid is read-only, and the number of encodings cannot grow,
+ // this should never happen in setParameters.
+ for (const auto& encoding : aEncodings) {
+ if (!encoding.mRid.WasPassed()) {
+ aRv.ThrowTypeError("Missing rid");
+ return;
+ }
+ }
+
+ // If any encoding contains a rid member whose value is the same as that of
+ // a rid contained in another encoding in sendEncodings, throw a TypeError.
+ // NOTE: Since rid is read-only, and the number of encodings cannot grow,
+ // this should never happen in setParameters.
+ std::set<nsString> uniqueRids;
+ for (const auto& encoding : aEncodings) {
+ if (uniqueRids.count(encoding.mRid.Value())) {
+ aRv.ThrowTypeError("Duplicate rid");
+ return;
+ }
+ uniqueRids.insert(encoding.mRid.Value());
+ }
+ }
+ // TODO: ptime/adaptivePtime validation (bug 1733647)
+
+ // If kind is "audio", remove the scaleResolutionDownBy member from all
+ // encodings that contain one.
+ if (!aVideo) {
+ for (auto& encoding : aEncodings) {
+ if (encoding.mScaleResolutionDownBy.WasPassed()) {
+ encoding.mScaleResolutionDownBy.Reset();
+ }
+ if (encoding.mMaxFramerate.WasPassed()) {
+ encoding.mMaxFramerate.Reset();
+ }
+ }
+ }
+
+ // If any encoding contains a scaleResolutionDownBy member whose value is
+ // less than 1.0, throw a RangeError.
+ for (const auto& encoding : aEncodings) {
+ if (encoding.mScaleResolutionDownBy.WasPassed()) {
+ if (encoding.mScaleResolutionDownBy.Value() < 1.0f) {
+ aRv.ThrowRangeError("scaleResolutionDownBy must be >= 1.0");
+ return;
+ }
+ }
+ }
+
+ // Verify that the value of each maxFramerate member in sendEncodings that is
+ // defined is greater than 0.0. If one of the maxFramerate values does not
+ // meet this requirement, throw a RangeError.
+ for (const auto& encoding : aEncodings) {
+ if (encoding.mMaxFramerate.WasPassed()) {
+ if (encoding.mMaxFramerate.Value() < 0.0f) {
+ aRv.ThrowRangeError("maxFramerate must be non-negative");
+ return;
+ }
+ }
+ }
+}
+
+void RTCRtpSender::GetParameters(RTCRtpSendParameters& aParameters) {
+ MOZ_ASSERT(mParameters.mEncodings.Length());
+ // If sender.[[LastReturnedParameters]] is not null, return
+ // sender.[[LastReturnedParameters]], and abort these steps.
+ if (mLastReturnedParameters.isSome()) {
+ aParameters = *mLastReturnedParameters;
+ return;
+ }
+
+ // Let result be a new RTCRtpSendParameters dictionary constructed as follows:
+
+ // transactionId is set to a new unique identifier
+ aParameters.mTransactionId.Construct(mPc->GenerateUUID());
+
+ // encodings is set to the value of the [[SendEncodings]] internal slot.
+ aParameters.mEncodings = mParameters.mEncodings;
+
+ // The headerExtensions sequence is populated based on the header extensions
+ // that have been negotiated for sending
+ // TODO(bug 1765851): We do not support this yet
+ // aParameters.mHeaderExtensions.Construct();
+
+ // codecs is set to the value of the [[SendCodecs]] internal slot
+ // TODO(bug 1534687): We do not support this yet
+
+ // rtcp.cname is set to the CNAME of the associated RTCPeerConnection.
+ // rtcp.reducedSize is set to true if reduced-size RTCP has been negotiated
+ // for sending, and false otherwise.
+ // TODO(bug 1765852): We do not support this yet
+ aParameters.mRtcp.Construct();
+ aParameters.mRtcp.Value().mCname.Construct();
+ aParameters.mRtcp.Value().mReducedSize.Construct(false);
+ aParameters.mHeaderExtensions.Construct();
+ aParameters.mCodecs.Construct();
+
+ // Set sender.[[LastReturnedParameters]] to result.
+ mLastReturnedParameters = Some(aParameters);
+
+ // Queue a task that sets sender.[[LastReturnedParameters]] to null.
+ GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction(
+ __func__, [this, self = RefPtr<RTCRtpSender>(this)] {
+ mLastReturnedParameters = Nothing();
+ }));
+}
+
+bool operator==(const RTCRtpEncodingParameters& a1,
+ const RTCRtpEncodingParameters& a2) {
+ // webidl does not generate types that are equality comparable
+ return a1.mActive == a2.mActive && a1.mFec == a2.mFec &&
+ a1.mMaxBitrate == a2.mMaxBitrate &&
+ a1.mMaxFramerate == a2.mMaxFramerate && a1.mPriority == a2.mPriority &&
+ a1.mRid == a2.mRid && a1.mRtx == a2.mRtx &&
+ a1.mScaleResolutionDownBy == a2.mScaleResolutionDownBy &&
+ a1.mSsrc == a2.mSsrc;
+}
+
+// static
+void RTCRtpSender::ApplyJsEncodingToConduitEncoding(
+ const RTCRtpEncodingParameters& aJsEncoding,
+ VideoCodecConfig::Encoding* aConduitEncoding) {
+ aConduitEncoding->active = aJsEncoding.mActive;
+ if (aJsEncoding.mMaxBitrate.WasPassed()) {
+ aConduitEncoding->constraints.maxBr = aJsEncoding.mMaxBitrate.Value();
+ }
+ if (aJsEncoding.mMaxFramerate.WasPassed()) {
+ aConduitEncoding->constraints.maxFps =
+ Some(aJsEncoding.mMaxFramerate.Value());
+ }
+ if (aJsEncoding.mScaleResolutionDownBy.WasPassed()) {
+ // Optional does not have a valueOr, despite being based on Maybe
+ // :(
+ aConduitEncoding->constraints.scaleDownBy =
+ aJsEncoding.mScaleResolutionDownBy.Value();
+ } else {
+ aConduitEncoding->constraints.scaleDownBy = 1.0f;
+ }
+}
+
+void RTCRtpSender::UpdateRestorableEncodings(
+ const Sequence<RTCRtpEncodingParameters>& aEncodings) {
+ MOZ_ASSERT(aEncodings.Length());
+
+ if (GetJsepTransceiver().mSendTrack.GetNegotiatedDetails()) {
+ // Once initial negotiation completes, we are no longer allowed to restore
+ // the unicast encoding.
+ mUnicastEncoding.reset();
+ } else if (mParameters.mEncodings.Length() == 1 &&
+ !mParameters.mEncodings[0].mRid.WasPassed()) {
+ // If we have not completed the initial negotiation, and we currently are
+ // ridless unicast, we need to save our unicast encoding in case a
+ // rollback occurs.
+ mUnicastEncoding = Some(mParameters.mEncodings[0]);
+ }
+}
+
+Sequence<RTCRtpEncodingParameters> RTCRtpSender::ToSendEncodings(
+ const std::vector<std::string>& aRids) const {
+ MOZ_ASSERT(!aRids.empty());
+
+ Sequence<RTCRtpEncodingParameters> result;
+ // If sendEncodings is given as input to this algorithm, and is non-empty,
+ // set the [[SendEncodings]] slot to sendEncodings.
+ for (const auto& rid : aRids) {
+ MOZ_ASSERT(!rid.empty());
+ RTCRtpEncodingParameters encoding;
+ encoding.mActive = true;
+ encoding.mRid.Construct(NS_ConvertUTF8toUTF16(rid.c_str()));
+ Unused << result.AppendElement(encoding, fallible);
+ }
+
+ // If sendEncodings is non-empty, set each encoding's scaleResolutionDownBy
+ // to 2^(length of sendEncodings - encoding index - 1).
+ if (mTransceiver->IsVideo()) {
+ double scale = 1.0f;
+ for (auto it = result.rbegin(); it != result.rend(); ++it) {
+ it->mScaleResolutionDownBy.Construct(scale);
+ scale *= 2;
+ }
+ }
+
+ return result;
+}
+
+void RTCRtpSender::MaybeGetJsepRids() {
+ MOZ_ASSERT(!mSimulcastEnvelopeSet);
+ MOZ_ASSERT(mParameters.mEncodings.Length());
+
+ auto jsepRids = GetJsepTransceiver().mSendTrack.GetRids();
+ if (!jsepRids.empty()) {
+ UpdateRestorableEncodings(mParameters.mEncodings);
+ if (jsepRids.size() != 1 || !jsepRids[0].empty()) {
+ // JSEP is using at least one rid. Stomp our single ridless encoding
+ mParameters.mEncodings = ToSendEncodings(jsepRids);
+ }
+ mSimulcastEnvelopeSet = true;
+ mSimulcastEnvelopeSetByJSEP = true;
+ }
+}
+
+Sequence<RTCRtpEncodingParameters> RTCRtpSender::GetMatchingEncodings(
+ const std::vector<std::string>& aRids) const {
+ Sequence<RTCRtpEncodingParameters> result;
+
+ if (!aRids.empty() && !aRids[0].empty()) {
+ // Simulcast, or unicast with rid
+ for (const auto& encoding : mParameters.mEncodings) {
+ for (const auto& rid : aRids) {
+ auto utf16Rid = NS_ConvertUTF8toUTF16(rid.c_str());
+ if (!encoding.mRid.WasPassed() || (utf16Rid == encoding.mRid.Value())) {
+ auto encodingCopy(encoding);
+ if (!encodingCopy.mRid.WasPassed()) {
+ encodingCopy.mRid.Construct(NS_ConvertUTF8toUTF16(rid.c_str()));
+ }
+ Unused << result.AppendElement(encodingCopy, fallible);
+ break;
+ }
+ }
+ }
+ }
+
+ // If we're allowing the old setParameters behavior, we _might_ be able to
+ // get into this situation even if there were rids above. Be extra careful.
+ // Under normal circumstances, this just handles the ridless case.
+ if (!result.Length()) {
+ // Unicast with no specified rid. Restore mUnicastEncoding, if
+ // it exists, otherwise pick the first encoding.
+ if (mUnicastEncoding.isSome()) {
+ Unused << result.AppendElement(*mUnicastEncoding, fallible);
+ } else {
+ Unused << result.AppendElement(mParameters.mEncodings[0], fallible);
+ }
+ }
+
+ return result;
+}
+
+void RTCRtpSender::SetStreams(
+ const Sequence<OwningNonNull<DOMMediaStream>>& aStreams, ErrorResult& aRv) {
+ if (mPc->IsClosed()) {
+ aRv.ThrowInvalidStateError(
+ "Cannot call setStreams if the peer connection is closed");
+ return;
+ }
+
+ SetStreamsImpl(aStreams);
+ mPc->UpdateNegotiationNeeded();
+}
+
+void RTCRtpSender::SetStreamsImpl(
+ const Sequence<OwningNonNull<DOMMediaStream>>& aStreams) {
+ mStreams.Clear();
+ std::set<nsString> ids;
+ for (const auto& stream : aStreams) {
+ nsString id;
+ stream->GetId(id);
+ if (!ids.count(id)) {
+ ids.insert(id);
+ mStreams.AppendElement(stream);
+ }
+ }
+}
+
+void RTCRtpSender::GetStreams(nsTArray<RefPtr<DOMMediaStream>>& aStreams) {
+ aStreams = mStreams.Clone();
+}
+
+class ReplaceTrackOperation final : public PeerConnectionImpl::Operation {
+ public:
+ ReplaceTrackOperation(PeerConnectionImpl* aPc,
+ const RefPtr<RTCRtpTransceiver>& aTransceiver,
+ const RefPtr<MediaStreamTrack>& aTrack,
+ ErrorResult& aError);
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(ReplaceTrackOperation,
+ PeerConnectionImpl::Operation)
+
+ private:
+ MOZ_CAN_RUN_SCRIPT
+ RefPtr<dom::Promise> CallImpl(ErrorResult& aError) override;
+ ~ReplaceTrackOperation() = default;
+ RefPtr<RTCRtpTransceiver> mTransceiver;
+ RefPtr<MediaStreamTrack> mNewTrack;
+};
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(ReplaceTrackOperation,
+ PeerConnectionImpl::Operation, mTransceiver,
+ mNewTrack)
+
+NS_IMPL_ADDREF_INHERITED(ReplaceTrackOperation, PeerConnectionImpl::Operation)
+NS_IMPL_RELEASE_INHERITED(ReplaceTrackOperation, PeerConnectionImpl::Operation)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(ReplaceTrackOperation)
+NS_INTERFACE_MAP_END_INHERITING(PeerConnectionImpl::Operation)
+
+ReplaceTrackOperation::ReplaceTrackOperation(
+ PeerConnectionImpl* aPc, const RefPtr<RTCRtpTransceiver>& aTransceiver,
+ const RefPtr<MediaStreamTrack>& aTrack, ErrorResult& aError)
+ : PeerConnectionImpl::Operation(aPc, aError),
+ mTransceiver(aTransceiver),
+ mNewTrack(aTrack) {}
+
+RefPtr<dom::Promise> ReplaceTrackOperation::CallImpl(ErrorResult& aError) {
+ RefPtr<RTCRtpSender> sender = mTransceiver->Sender();
+ // If transceiver.[[Stopped]] is true, return a promise rejected with a newly
+ // created InvalidStateError.
+ if (mTransceiver->Stopped()) {
+ RefPtr<dom::Promise> error = sender->MakePromise(aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+ MOZ_LOG(gSenderLog, LogLevel::Debug,
+ ("%s Cannot call replaceTrack when transceiver is stopped",
+ __FUNCTION__));
+ error->MaybeRejectWithInvalidStateError(
+ "Cannot call replaceTrack when transceiver is stopped");
+ return error;
+ }
+
+ // Let p be a new promise.
+ RefPtr<dom::Promise> p = sender->MakePromise(aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+
+ if (!sender->SeamlessTrackSwitch(mNewTrack)) {
+ MOZ_LOG(gSenderLog, LogLevel::Info,
+ ("%s Could not seamlessly replace track", __FUNCTION__));
+ p->MaybeRejectWithInvalidModificationError(
+ "Could not seamlessly replace track");
+ return p;
+ }
+
+ // Queue a task that runs the following steps:
+ GetMainThreadSerialEventTarget()->Dispatch(NS_NewRunnableFunction(
+ __func__, [p, sender, track = mNewTrack]() MOZ_CAN_RUN_SCRIPT_BOUNDARY {
+ // If connection.[[IsClosed]] is true, abort these steps.
+ // Set sender.[[SenderTrack]] to withTrack.
+ if (sender->SetSenderTrackWithClosedCheck(track)) {
+ // Resolve p with undefined.
+ p->MaybeResolveWithUndefined();
+ }
+ }));
+
+ // Return p.
+ return p;
+}
+
+already_AddRefed<dom::Promise> RTCRtpSender::ReplaceTrack(
+ dom::MediaStreamTrack* aWithTrack, ErrorResult& aError) {
+ // If withTrack is non-null and withTrack.kind differs from the transceiver
+ // kind of transceiver, return a promise rejected with a newly created
+ // TypeError.
+ if (aWithTrack) {
+ nsString newKind;
+ aWithTrack->GetKind(newKind);
+ nsString oldKind;
+ mTransceiver->GetKind(oldKind);
+ if (newKind != oldKind) {
+ RefPtr<dom::Promise> error = MakePromise(aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+ error->MaybeRejectWithTypeError(
+ "Cannot replaceTrack with a different kind!");
+ return error.forget();
+ }
+ }
+
+ MOZ_LOG(gSenderLog, LogLevel::Debug,
+ ("%s[%s]: %s (%p to %p)", mPc->GetHandle().c_str(), GetMid().c_str(),
+ __FUNCTION__, mSenderTrack.get(), aWithTrack));
+
+ // Return the result of chaining the following steps to connection's
+ // operations chain:
+ RefPtr<PeerConnectionImpl::Operation> op =
+ new ReplaceTrackOperation(mPc, mTransceiver, aWithTrack, aError);
+ if (aError.Failed()) {
+ return nullptr;
+ }
+ // Static analysis forces us to use a temporary.
+ auto pc = mPc;
+ return pc->Chain(op, aError);
+}
+
+nsPIDOMWindowInner* RTCRtpSender::GetParentObject() const { return mWindow; }
+
+already_AddRefed<dom::Promise> RTCRtpSender::MakePromise(
+ ErrorResult& aError) const {
+ return mPc->MakePromise(aError);
+}
+
+bool RTCRtpSender::SeamlessTrackSwitch(
+ const RefPtr<MediaStreamTrack>& aWithTrack) {
+ // We do not actually update mSenderTrack here! Spec says that happens in a
+ // queued task after this is done (this happens in
+ // SetSenderTrackWithClosedCheck).
+
+ mPipeline->SetTrack(aWithTrack);
+
+ MaybeUpdateConduit();
+
+ // There may eventually be cases where a renegotiation is necessary to switch.
+ return true;
+}
+
+void RTCRtpSender::SetTrack(const RefPtr<MediaStreamTrack>& aTrack) {
+ // Used for RTCPeerConnection.removeTrack and RTCPeerConnection.addTrack
+ mSenderTrack = aTrack;
+ SeamlessTrackSwitch(aTrack);
+ if (aTrack) {
+ // RFC says (in the section on remote rollback):
+ // However, an RtpTransceiver MUST NOT be removed if a track was attached
+ // to the RtpTransceiver via the addTrack method.
+ mAddTrackCalled = true;
+ }
+}
+
+bool RTCRtpSender::SetSenderTrackWithClosedCheck(
+ const RefPtr<MediaStreamTrack>& aTrack) {
+ if (!mPc->IsClosed()) {
+ mSenderTrack = aTrack;
+ return true;
+ }
+
+ return false;
+}
+
+void RTCRtpSender::Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ mWatchManager.Shutdown();
+ mPipeline->Shutdown();
+ mPipeline = nullptr;
+}
+
+void RTCRtpSender::BreakCycles() {
+ mWindow = nullptr;
+ mPc = nullptr;
+ mSenderTrack = nullptr;
+ mTransceiver = nullptr;
+ mStreams.Clear();
+ mDtmf = nullptr;
+}
+
+void RTCRtpSender::UpdateTransport() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!mHaveSetupTransport) {
+ mPipeline->SetLevel(GetJsepTransceiver().GetLevel());
+ mHaveSetupTransport = true;
+ }
+
+ mPipeline->UpdateTransport_m(GetJsepTransceiver().mTransport.mTransportId,
+ nullptr);
+}
+
+void RTCRtpSender::MaybeUpdateConduit() {
+ // NOTE(pkerr) - the Call API requires the both local_ssrc and remote_ssrc be
+ // set to a non-zero value or the CreateVideo...Stream call will fail.
+ if (NS_WARN_IF(GetJsepTransceiver().mSendTrack.GetSsrcs().empty())) {
+ MOZ_ASSERT(
+ false,
+ "No local ssrcs! This is a bug in the jsep engine, and should never "
+ "happen!");
+ return;
+ }
+
+ if (!mPipeline) {
+ return;
+ }
+
+ bool wasTransmitting = mTransmitting;
+
+ if (mPipeline->mConduit->type() == MediaSessionConduit::VIDEO) {
+ Maybe<VideoConfig> newConfig = GetNewVideoConfig();
+ if (newConfig.isSome()) {
+ ApplyVideoConfig(*newConfig);
+ }
+ } else {
+ Maybe<AudioConfig> newConfig = GetNewAudioConfig();
+ if (newConfig.isSome()) {
+ ApplyAudioConfig(*newConfig);
+ }
+ }
+
+ if (!mSenderTrack && !wasTransmitting && mTransmitting) {
+ MOZ_LOG(gSenderLog, LogLevel::Debug,
+ ("%s[%s]: %s Starting transmit conduit without send track!",
+ mPc->GetHandle().c_str(), GetMid().c_str(), __FUNCTION__));
+ }
+}
+
+void RTCRtpSender::SyncFromJsep(const JsepTransceiver& aJsepTransceiver) {
+ if (!mSimulcastEnvelopeSet) {
+ // JSEP is establishing the simulcast envelope for the first time, right now
+ // This is the addTrack (or addTransceiver without sendEncodings) case.
+ MaybeGetJsepRids();
+ } else if (!aJsepTransceiver.mSendTrack.GetNegotiatedDetails() ||
+ !aJsepTransceiver.mSendTrack.IsInHaveRemote()) {
+ // Spec says that we do not update our encodings until we're in stable,
+ // _unless_ this is the first negotiation.
+ std::vector<std::string> rids = aJsepTransceiver.mSendTrack.GetRids();
+ if (mSimulcastEnvelopeSetByJSEP && rids.empty()) {
+ // JSEP previously set the simulcast envelope, but now it has no opinion
+ // regarding unicast/simulcast. This can only happen on rollback of the
+ // initial remote offer.
+ mParameters.mEncodings = GetMatchingEncodings(rids);
+ MOZ_ASSERT(mParameters.mEncodings.Length());
+ mSimulcastEnvelopeSetByJSEP = false;
+ mSimulcastEnvelopeSet = false;
+ } else if (!rids.empty()) {
+ // JSEP has an opinion on the simulcast envelope, which trumps anything
+ // we have already.
+ mParameters.mEncodings = GetMatchingEncodings(rids);
+ MOZ_ASSERT(mParameters.mEncodings.Length());
+ }
+ }
+
+ MaybeUpdateConduit();
+}
+
+void RTCRtpSender::SyncToJsep(JsepTransceiver& aJsepTransceiver) const {
+ std::vector<std::string> streamIds;
+ for (const auto& stream : mStreams) {
+ nsString wideStreamId;
+ stream->GetId(wideStreamId);
+ std::string streamId = NS_ConvertUTF16toUTF8(wideStreamId).get();
+ MOZ_ASSERT(!streamId.empty());
+ streamIds.push_back(streamId);
+ }
+
+ aJsepTransceiver.mSendTrack.UpdateStreamIds(streamIds);
+
+ if (mSimulcastEnvelopeSet) {
+ std::vector<std::string> rids;
+ Maybe<RTCRtpSendParameters> parameters;
+ if (mPendingRidChangeFromCompatMode) {
+ // *sigh* If we have just let a setParameters change our rids, but we have
+ // not yet updated mParameters because the queued task hasn't run yet,
+ // we want to set the _new_ rids on the JsepTrack. So, we are forced to
+ // grab them from mPendingParameters.
+ parameters = mPendingParameters;
+ } else {
+ parameters = Some(mParameters);
+ }
+ for (const auto& encoding : parameters->mEncodings) {
+ if (encoding.mRid.WasPassed()) {
+ rids.push_back(NS_ConvertUTF16toUTF8(encoding.mRid.Value()).get());
+ } else {
+ rids.push_back("");
+ }
+ }
+
+ aJsepTransceiver.mSendTrack.SetRids(rids);
+ }
+
+ if (mTransceiver->IsVideo()) {
+ aJsepTransceiver.mSendTrack.SetMaxEncodings(webrtc::kMaxSimulcastStreams);
+ } else {
+ aJsepTransceiver.mSendTrack.SetMaxEncodings(1);
+ }
+
+ if (mAddTrackCalled) {
+ aJsepTransceiver.SetOnlyExistsBecauseOfSetRemote(false);
+ }
+}
+
+Maybe<RTCRtpSender::VideoConfig> RTCRtpSender::GetNewVideoConfig() {
+ // It is possible for SDP to signal that there is a send track, but there not
+ // actually be a send track, according to the specification; all that needs to
+ // happen is for the transceiver to be configured to send...
+ if (!GetJsepTransceiver().mSendTrack.GetNegotiatedDetails()) {
+ return Nothing();
+ }
+
+ VideoConfig oldConfig;
+ oldConfig.mSsrcs = mSsrcs;
+ oldConfig.mLocalRtpExtensions = mLocalRtpExtensions;
+ oldConfig.mCname = mCname;
+ oldConfig.mTransmitting = mTransmitting;
+ oldConfig.mVideoRtxSsrcs = mVideoRtxSsrcs;
+ oldConfig.mVideoCodec = mVideoCodec;
+ oldConfig.mVideoRtpRtcpConfig = mVideoRtpRtcpConfig;
+ oldConfig.mVideoCodecMode = mVideoCodecMode;
+
+ VideoConfig newConfig(oldConfig);
+
+ UpdateBaseConfig(&newConfig);
+
+ newConfig.mVideoRtxSsrcs = GetJsepTransceiver().mSendTrack.GetRtxSsrcs();
+
+ const JsepTrackNegotiatedDetails details(
+ *GetJsepTransceiver().mSendTrack.GetNegotiatedDetails());
+
+ if (mSenderTrack) {
+ RefPtr<mozilla::dom::VideoStreamTrack> videotrack =
+ mSenderTrack->AsVideoStreamTrack();
+
+ if (!videotrack) {
+ MOZ_CRASH(
+ "In ConfigureVideoCodecMode, mSenderTrack is not video! This should "
+ "never happen!");
+ }
+
+ dom::MediaSourceEnum source = videotrack->GetSource().GetMediaSource();
+ switch (source) {
+ case dom::MediaSourceEnum::Browser:
+ case dom::MediaSourceEnum::Screen:
+ case dom::MediaSourceEnum::Window:
+ case dom::MediaSourceEnum::Application:
+ newConfig.mVideoCodecMode = webrtc::VideoCodecMode::kScreensharing;
+ break;
+
+ case dom::MediaSourceEnum::Camera:
+ case dom::MediaSourceEnum::Other:
+ // Other is used by canvas capture, which we treat as realtime video.
+ // This seems debatable, but we've been doing it this way for a long
+ // time, so this is likely fine.
+ newConfig.mVideoCodecMode = webrtc::VideoCodecMode::kRealtimeVideo;
+ break;
+
+ case dom::MediaSourceEnum::Microphone:
+ case dom::MediaSourceEnum::AudioCapture:
+ case dom::MediaSourceEnum::EndGuard_:
+ MOZ_ASSERT(false);
+ break;
+ }
+ }
+
+ std::vector<VideoCodecConfig> configs;
+ RTCRtpTransceiver::NegotiatedDetailsToVideoCodecConfigs(details, &configs);
+
+ if (configs.empty()) {
+ // TODO: Are we supposed to plumb this error back to JS? This does not
+ // seem like a failure to set an answer, it just means that codec
+ // negotiation failed. For now, we're just doing the same thing we do
+ // if negotiation as a whole failed.
+ MOZ_LOG(gSenderLog, LogLevel::Error,
+ ("%s[%s]: %s No video codecs were negotiated (send).",
+ mPc->GetHandle().c_str(), GetMid().c_str(), __FUNCTION__));
+ return Nothing();
+ }
+
+ newConfig.mVideoCodec = Some(configs[0]);
+ // Spec says that we start using new parameters right away, _before_ we
+ // update the parameters that are visible to JS (ie; mParameters).
+ const RTCRtpSendParameters& parameters =
+ mPendingParameters.isSome() ? *mPendingParameters : mParameters;
+ for (VideoCodecConfig::Encoding& conduitEncoding :
+ newConfig.mVideoCodec->mEncodings) {
+ for (const RTCRtpEncodingParameters& jsEncoding : parameters.mEncodings) {
+ std::string rid;
+ if (jsEncoding.mRid.WasPassed()) {
+ rid = NS_ConvertUTF16toUTF8(jsEncoding.mRid.Value()).get();
+ }
+ if (conduitEncoding.rid == rid) {
+ ApplyJsEncodingToConduitEncoding(jsEncoding, &conduitEncoding);
+ break;
+ }
+ }
+ }
+
+ newConfig.mVideoRtpRtcpConfig = Some(details.GetRtpRtcpConfig());
+
+ if (newConfig == oldConfig) {
+ MOZ_LOG(gSenderLog, LogLevel::Debug,
+ ("%s[%s]: %s No change in video config", mPc->GetHandle().c_str(),
+ GetMid().c_str(), __FUNCTION__));
+ return Nothing();
+ }
+
+ if (newConfig.mVideoCodec.isSome()) {
+ MOZ_ASSERT(newConfig.mSsrcs.size() ==
+ newConfig.mVideoCodec->mEncodings.size());
+ }
+ return Some(newConfig);
+}
+
+Maybe<RTCRtpSender::AudioConfig> RTCRtpSender::GetNewAudioConfig() {
+ AudioConfig oldConfig;
+ oldConfig.mSsrcs = mSsrcs;
+ oldConfig.mLocalRtpExtensions = mLocalRtpExtensions;
+ oldConfig.mCname = mCname;
+ oldConfig.mTransmitting = mTransmitting;
+ oldConfig.mAudioCodec = mAudioCodec;
+
+ AudioConfig newConfig(oldConfig);
+
+ UpdateBaseConfig(&newConfig);
+
+ if (GetJsepTransceiver().mSendTrack.GetNegotiatedDetails() &&
+ GetJsepTransceiver().mSendTrack.GetActive()) {
+ const auto& details(
+ *GetJsepTransceiver().mSendTrack.GetNegotiatedDetails());
+
+ std::vector<AudioCodecConfig> configs;
+ RTCRtpTransceiver::NegotiatedDetailsToAudioCodecConfigs(details, &configs);
+ if (configs.empty()) {
+ // TODO: Are we supposed to plumb this error back to JS? This does not
+ // seem like a failure to set an answer, it just means that codec
+ // negotiation failed. For now, we're just doing the same thing we do
+ // if negotiation as a whole failed.
+ MOZ_LOG(gSenderLog, LogLevel::Error,
+ ("%s[%s]: %s No audio codecs were negotiated (send)",
+ mPc->GetHandle().c_str(), GetMid().c_str(), __FUNCTION__));
+ return Nothing();
+ }
+
+ std::vector<AudioCodecConfig> dtmfConfigs;
+ std::copy_if(
+ configs.begin(), configs.end(), std::back_inserter(dtmfConfigs),
+ [](const auto& value) { return value.mName == "telephone-event"; });
+
+ const AudioCodecConfig& sendCodec = configs[0];
+
+ if (!dtmfConfigs.empty()) {
+ // There is at least one telephone-event codec.
+ // We primarily choose the codec whose frequency matches the send codec.
+ // Secondarily we choose the one with the lowest frequency.
+ auto dtmfIterator =
+ std::find_if(dtmfConfigs.begin(), dtmfConfigs.end(),
+ [&sendCodec](const auto& dtmfCodec) {
+ return dtmfCodec.mFreq == sendCodec.mFreq;
+ });
+ if (dtmfIterator == dtmfConfigs.end()) {
+ dtmfIterator = std::min_element(
+ dtmfConfigs.begin(), dtmfConfigs.end(),
+ [](const auto& a, const auto& b) { return a.mFreq < b.mFreq; });
+ }
+ MOZ_ASSERT(dtmfIterator != dtmfConfigs.end());
+ newConfig.mDtmfPt = dtmfIterator->mType;
+ newConfig.mDtmfFreq = dtmfIterator->mFreq;
+ }
+
+ newConfig.mAudioCodec = Some(sendCodec);
+ }
+
+ if (newConfig == oldConfig) {
+ MOZ_LOG(gSenderLog, LogLevel::Debug,
+ ("%s[%s]: %s No change in audio config", mPc->GetHandle().c_str(),
+ GetMid().c_str(), __FUNCTION__));
+ return Nothing();
+ }
+
+ return Some(newConfig);
+}
+
+void RTCRtpSender::UpdateBaseConfig(BaseConfig* aConfig) {
+ aConfig->mSsrcs = GetJsepTransceiver().mSendTrack.GetSsrcs();
+ aConfig->mCname = GetJsepTransceiver().mSendTrack.GetCNAME();
+
+ if (GetJsepTransceiver().mSendTrack.GetNegotiatedDetails() &&
+ GetJsepTransceiver().mSendTrack.GetActive()) {
+ const auto& details(
+ *GetJsepTransceiver().mSendTrack.GetNegotiatedDetails());
+ {
+ std::vector<webrtc::RtpExtension> extmaps;
+ // @@NG read extmap from track
+ details.ForEachRTPHeaderExtension(
+ [&extmaps](const SdpExtmapAttributeList::Extmap& extmap) {
+ extmaps.emplace_back(extmap.extensionname, extmap.entry);
+ });
+ aConfig->mLocalRtpExtensions = extmaps;
+ }
+ }
+ // RTCRtpTransceiver::IsSending is updated after negotiation completes, in a
+ // queued task (which we may be in right now). Don't use
+ // JsepTrack::GetActive, because that updates before the queued task, which
+ // is too early for some of the things we interact with here (eg;
+ // RTCDTMFSender).
+ aConfig->mTransmitting = mTransceiver->IsSending();
+}
+
+void RTCRtpSender::ApplyVideoConfig(const VideoConfig& aConfig) {
+ if (aConfig.mVideoCodec.isSome()) {
+ MOZ_ASSERT(aConfig.mSsrcs.size() == aConfig.mVideoCodec->mEncodings.size());
+ }
+
+ mSsrcs = aConfig.mSsrcs;
+ mCname = aConfig.mCname;
+ mLocalRtpExtensions = aConfig.mLocalRtpExtensions;
+
+ mVideoRtxSsrcs = aConfig.mVideoRtxSsrcs;
+ mVideoCodec = aConfig.mVideoCodec;
+ mVideoRtpRtcpConfig = aConfig.mVideoRtpRtcpConfig;
+ mVideoCodecMode = aConfig.mVideoCodecMode;
+
+ mTransmitting = aConfig.mTransmitting;
+}
+
+void RTCRtpSender::ApplyAudioConfig(const AudioConfig& aConfig) {
+ mTransmitting = false;
+
+ mSsrcs = aConfig.mSsrcs;
+ mCname = aConfig.mCname;
+ mLocalRtpExtensions = aConfig.mLocalRtpExtensions;
+
+ mAudioCodec = aConfig.mAudioCodec;
+
+ if (aConfig.mDtmfPt >= 0) {
+ mDtmf->SetPayloadType(aConfig.mDtmfPt, aConfig.mDtmfFreq);
+ }
+
+ mTransmitting = aConfig.mTransmitting;
+}
+
+void RTCRtpSender::Stop() {
+ MOZ_ASSERT(mTransceiver->Stopped());
+ mTransmitting = false;
+}
+
+bool RTCRtpSender::HasTrack(const dom::MediaStreamTrack* aTrack) const {
+ if (!mSenderTrack) {
+ return false;
+ }
+
+ if (!aTrack) {
+ return true;
+ }
+
+ return mSenderTrack.get() == aTrack;
+}
+
+RefPtr<MediaPipelineTransmit> RTCRtpSender::GetPipeline() const {
+ return mPipeline;
+}
+
+std::string RTCRtpSender::GetMid() const { return mTransceiver->GetMidAscii(); }
+
+JsepTransceiver& RTCRtpSender::GetJsepTransceiver() {
+ return mTransceiver->GetJsepTransceiver();
+}
+
+void RTCRtpSender::UpdateDtmfSender() {
+ if (!mDtmf) {
+ return;
+ }
+
+ if (mTransmitting) {
+ return;
+ }
+
+ mDtmf->StopPlayout();
+}
+
+} // namespace mozilla::dom
+
+#undef LOGTAG
diff --git a/dom/media/webrtc/jsapi/RTCRtpSender.h b/dom/media/webrtc/jsapi/RTCRtpSender.h
new file mode 100644
index 0000000000..0c1282e0db
--- /dev/null
+++ b/dom/media/webrtc/jsapi/RTCRtpSender.h
@@ -0,0 +1,260 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _RTCRtpSender_h_
+#define _RTCRtpSender_h_
+
+#include "nsISupports.h"
+#include "nsWrapperCache.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/StateMirroring.h"
+#include "mozilla/Maybe.h"
+#include "js/RootingAPI.h"
+#include "libwebrtcglue/RtpRtcpConfig.h"
+#include "nsTArray.h"
+#include "mozilla/dom/RTCStatsReportBinding.h"
+#include "mozilla/dom/RTCRtpCapabilitiesBinding.h"
+#include "mozilla/dom/RTCRtpParametersBinding.h"
+#include "RTCStatsReport.h"
+#include "jsep/JsepTrack.h"
+#include "transportbridge/MediaPipeline.h"
+
+class nsPIDOMWindowInner;
+
+namespace mozilla {
+class MediaSessionConduit;
+class MediaTransportHandler;
+class JsepTransceiver;
+class PeerConnectionImpl;
+class DOMMediaStream;
+
+namespace dom {
+class MediaStreamTrack;
+class Promise;
+class RTCDtlsTransport;
+class RTCDTMFSender;
+struct RTCRtpCapabilities;
+class RTCRtpTransceiver;
+
+class RTCRtpSender : public nsISupports,
+ public nsWrapperCache,
+ public MediaPipelineTransmitControlInterface {
+ public:
+ RTCRtpSender(nsPIDOMWindowInner* aWindow, PeerConnectionImpl* aPc,
+ MediaTransportHandler* aTransportHandler,
+ AbstractThread* aCallThread, nsISerialEventTarget* aStsThread,
+ MediaSessionConduit* aConduit, dom::MediaStreamTrack* aTrack,
+ const Sequence<RTCRtpEncodingParameters>& aEncodings,
+ RTCRtpTransceiver* aTransceiver);
+
+ // nsISupports
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(RTCRtpSender)
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ // webidl
+ MediaStreamTrack* GetTrack() const { return mSenderTrack; }
+ RTCDtlsTransport* GetTransport() const;
+ RTCDTMFSender* GetDtmf() const;
+ MOZ_CAN_RUN_SCRIPT
+ already_AddRefed<Promise> ReplaceTrack(MediaStreamTrack* aWithTrack,
+ ErrorResult& aError);
+ already_AddRefed<Promise> GetStats(ErrorResult& aError);
+ static void GetCapabilities(const GlobalObject&, const nsAString& kind,
+ Nullable<dom::RTCRtpCapabilities>& result);
+ already_AddRefed<Promise> SetParameters(
+ const dom::RTCRtpSendParameters& aParameters, ErrorResult& aError);
+ // Not a simple getter, so not const
+ // See https://w3c.github.io/webrtc-pc/#dom-rtcrtpsender-getparameters
+ void GetParameters(RTCRtpSendParameters& aParameters);
+
+ static void CheckAndRectifyEncodings(
+ Sequence<RTCRtpEncodingParameters>& aEncodings, bool aVideo,
+ ErrorResult& aRv);
+
+ nsPIDOMWindowInner* GetParentObject() const;
+ nsTArray<RefPtr<RTCStatsPromise>> GetStatsInternal(
+ bool aSkipIceStats = false);
+
+ void SetStreams(const Sequence<OwningNonNull<DOMMediaStream>>& aStreams,
+ ErrorResult& aRv);
+ // ChromeOnly webidl
+ void GetStreams(nsTArray<RefPtr<DOMMediaStream>>& aStreams);
+ // ChromeOnly webidl
+ void SetStreamsImpl(const Sequence<OwningNonNull<DOMMediaStream>>& aStreams);
+ // ChromeOnly webidl
+ void SetTrack(const RefPtr<MediaStreamTrack>& aTrack);
+ void Shutdown();
+ void BreakCycles();
+ // Terminal state, reached through stopping RTCRtpTransceiver.
+ void Stop();
+ bool HasTrack(const dom::MediaStreamTrack* aTrack) const;
+ bool IsMyPc(const PeerConnectionImpl* aPc) const { return mPc.get() == aPc; }
+ RefPtr<MediaPipelineTransmit> GetPipeline() const;
+ already_AddRefed<dom::Promise> MakePromise(ErrorResult& aError) const;
+ bool SeamlessTrackSwitch(const RefPtr<MediaStreamTrack>& aWithTrack);
+ bool SetSenderTrackWithClosedCheck(const RefPtr<MediaStreamTrack>& aTrack);
+
+ // This is called when we set an answer (ie; when the transport is finalized).
+ void UpdateTransport();
+ void SyncToJsep(JsepTransceiver& aJsepTransceiver) const;
+ void SyncFromJsep(const JsepTransceiver& aJsepTransceiver);
+ void MaybeUpdateConduit();
+
+ AbstractCanonical<Ssrcs>* CanonicalSsrcs() { return &mSsrcs; }
+ AbstractCanonical<Ssrcs>* CanonicalVideoRtxSsrcs() { return &mVideoRtxSsrcs; }
+ AbstractCanonical<RtpExtList>* CanonicalLocalRtpExtensions() {
+ return &mLocalRtpExtensions;
+ }
+
+ AbstractCanonical<Maybe<AudioCodecConfig>>* CanonicalAudioCodec() {
+ return &mAudioCodec;
+ }
+
+ AbstractCanonical<Maybe<VideoCodecConfig>>* CanonicalVideoCodec() {
+ return &mVideoCodec;
+ }
+ AbstractCanonical<Maybe<RtpRtcpConfig>>* CanonicalVideoRtpRtcpConfig() {
+ return &mVideoRtpRtcpConfig;
+ }
+ AbstractCanonical<webrtc::VideoCodecMode>* CanonicalVideoCodecMode() {
+ return &mVideoCodecMode;
+ }
+ AbstractCanonical<std::string>* CanonicalCname() { return &mCname; }
+ AbstractCanonical<bool>* CanonicalTransmitting() override {
+ return &mTransmitting;
+ }
+
+ bool HasPendingSetParameters() const { return mPendingParameters.isSome(); }
+ void InvalidateLastReturnedParameters() {
+ mLastReturnedParameters = Nothing();
+ }
+
+ private:
+ virtual ~RTCRtpSender();
+
+ std::string GetMid() const;
+ JsepTransceiver& GetJsepTransceiver();
+ static void ApplyJsEncodingToConduitEncoding(
+ const RTCRtpEncodingParameters& aJsEncoding,
+ VideoCodecConfig::Encoding* aConduitEncoding);
+ void UpdateRestorableEncodings(
+ const Sequence<RTCRtpEncodingParameters>& aEncodings);
+ Sequence<RTCRtpEncodingParameters> GetMatchingEncodings(
+ const std::vector<std::string>& aRids) const;
+ Sequence<RTCRtpEncodingParameters> ToSendEncodings(
+ const std::vector<std::string>& aRids) const;
+ void MaybeGetJsepRids();
+ void UpdateDtmfSender();
+
+ void WarnAboutBadSetParameters(const nsCString& aError);
+ nsCString GetEffectiveTLDPlus1() const;
+
+ WatchManager<RTCRtpSender> mWatchManager;
+ nsCOMPtr<nsPIDOMWindowInner> mWindow;
+ RefPtr<PeerConnectionImpl> mPc;
+ RefPtr<dom::MediaStreamTrack> mSenderTrack;
+ bool mAddTrackCalled = false;
+ RTCRtpSendParameters mParameters;
+ Maybe<RTCRtpSendParameters> mPendingParameters;
+ uint32_t mNumSetParametersCalls = 0;
+ // When JSEP goes from simulcast to unicast without a rid, and we started out
+ // as unicast without a rid, we are supposed to restore that unicast encoding
+ // from before.
+ Maybe<RTCRtpEncodingParameters> mUnicastEncoding;
+ bool mSimulcastEnvelopeSet = false;
+ bool mSimulcastEnvelopeSetByJSEP = false;
+ bool mPendingRidChangeFromCompatMode = false;
+ Maybe<RTCRtpSendParameters> mLastReturnedParameters;
+ RefPtr<MediaPipelineTransmit> mPipeline;
+ RefPtr<MediaTransportHandler> mTransportHandler;
+ RefPtr<RTCRtpTransceiver> mTransceiver;
+ nsTArray<RefPtr<DOMMediaStream>> mStreams;
+ bool mHaveSetupTransport = false;
+ // TODO(bug 1803388): Remove this stuff once it is no longer needed.
+ bool mAllowOldSetParameters = false;
+
+ // TODO(bug 1803388): Remove the glean warnings once they are no longer needed
+ bool mHaveWarnedBecauseNoGetParameters = false;
+ bool mHaveWarnedBecauseEncodingCountChange = false;
+ bool mHaveWarnedBecauseNoTransactionId = false;
+ bool mHaveWarnedBecauseStaleTransactionId = false;
+ // TODO(bug 1803389): Remove the glean errors once they are no longer needed.
+ bool mHaveFailedBecauseNoGetParameters = false;
+ bool mHaveFailedBecauseEncodingCountChange = false;
+ bool mHaveFailedBecauseRidChange = false;
+ bool mHaveFailedBecauseNoTransactionId = false;
+ bool mHaveFailedBecauseStaleTransactionId = false;
+ bool mHaveFailedBecauseNoEncodings = false;
+ bool mHaveFailedBecauseOtherError = false;
+
+ RefPtr<dom::RTCDTMFSender> mDtmf;
+
+ class BaseConfig {
+ public:
+ // TODO(bug 1744116): Use = default here
+ bool operator==(const BaseConfig& aOther) const {
+ return mSsrcs == aOther.mSsrcs &&
+ mLocalRtpExtensions == aOther.mLocalRtpExtensions &&
+ mCname == aOther.mCname && mTransmitting == aOther.mTransmitting;
+ }
+ Ssrcs mSsrcs;
+ RtpExtList mLocalRtpExtensions;
+ std::string mCname;
+ bool mTransmitting = false;
+ };
+
+ class VideoConfig : public BaseConfig {
+ public:
+ // TODO(bug 1744116): Use = default here
+ bool operator==(const VideoConfig& aOther) const {
+ return BaseConfig::operator==(aOther) &&
+ mVideoRtxSsrcs == aOther.mVideoRtxSsrcs &&
+ mVideoCodec == aOther.mVideoCodec &&
+ mVideoRtpRtcpConfig == aOther.mVideoRtpRtcpConfig &&
+ mVideoCodecMode == aOther.mVideoCodecMode;
+ }
+ Ssrcs mVideoRtxSsrcs;
+ Maybe<VideoCodecConfig> mVideoCodec;
+ Maybe<RtpRtcpConfig> mVideoRtpRtcpConfig;
+ webrtc::VideoCodecMode mVideoCodecMode =
+ webrtc::VideoCodecMode::kRealtimeVideo;
+ };
+
+ class AudioConfig : public BaseConfig {
+ public:
+ // TODO(bug 1744116): Use = default here
+ bool operator==(const AudioConfig& aOther) const {
+ return BaseConfig::operator==(aOther) &&
+ mAudioCodec == aOther.mAudioCodec && mDtmfPt == aOther.mDtmfPt &&
+ mDtmfFreq == aOther.mDtmfFreq;
+ }
+ Maybe<AudioCodecConfig> mAudioCodec;
+ int32_t mDtmfPt = -1;
+ int32_t mDtmfFreq = 0;
+ };
+
+ Maybe<VideoConfig> GetNewVideoConfig();
+ Maybe<AudioConfig> GetNewAudioConfig();
+ void UpdateBaseConfig(BaseConfig* aConfig);
+ void ApplyVideoConfig(const VideoConfig& aConfig);
+ void ApplyAudioConfig(const AudioConfig& aConfig);
+
+ Canonical<Ssrcs> mSsrcs;
+ Canonical<Ssrcs> mVideoRtxSsrcs;
+ Canonical<RtpExtList> mLocalRtpExtensions;
+
+ Canonical<Maybe<AudioCodecConfig>> mAudioCodec;
+ Canonical<Maybe<VideoCodecConfig>> mVideoCodec;
+ Canonical<Maybe<RtpRtcpConfig>> mVideoRtpRtcpConfig;
+ Canonical<webrtc::VideoCodecMode> mVideoCodecMode;
+ Canonical<std::string> mCname;
+ Canonical<bool> mTransmitting;
+};
+
+} // namespace dom
+} // namespace mozilla
+#endif // _RTCRtpSender_h_
diff --git a/dom/media/webrtc/jsapi/RTCRtpTransceiver.cpp b/dom/media/webrtc/jsapi/RTCRtpTransceiver.cpp
new file mode 100644
index 0000000000..bf4f74dd5d
--- /dev/null
+++ b/dom/media/webrtc/jsapi/RTCRtpTransceiver.cpp
@@ -0,0 +1,1080 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "jsapi/RTCRtpTransceiver.h"
+#include "mozilla/UniquePtr.h"
+#include <algorithm>
+#include <string>
+#include <vector>
+#include "libwebrtcglue/AudioConduit.h"
+#include "libwebrtcglue/VideoConduit.h"
+#include "MediaTrackGraph.h"
+#include "transportbridge/MediaPipeline.h"
+#include "transportbridge/MediaPipelineFilter.h"
+#include "jsep/JsepTrack.h"
+#include "sdp/SdpHelper.h"
+#include "MediaTrackGraphImpl.h"
+#include "transport/logging.h"
+#include "MediaEngine.h"
+#include "nsIPrincipal.h"
+#include "MediaSegment.h"
+#include "RemoteTrackSource.h"
+#include "libwebrtcglue/RtpRtcpConfig.h"
+#include "MediaTransportHandler.h"
+#include "mozilla/dom/RTCRtpReceiverBinding.h"
+#include "mozilla/dom/RTCRtpSenderBinding.h"
+#include "mozilla/dom/RTCRtpTransceiverBinding.h"
+#include "mozilla/dom/Promise.h"
+#include "RTCDtlsTransport.h"
+#include "RTCRtpReceiver.h"
+#include "RTCRtpSender.h"
+#include "RTCDTMFSender.h"
+#include "systemservices/MediaUtils.h"
+#include "libwebrtcglue/WebrtcCallWrapper.h"
+#include "libwebrtcglue/WebrtcGmpVideoCodec.h"
+#include "utils/PerformanceRecorder.h"
+
+namespace mozilla {
+
+using namespace dom;
+
+namespace {
+struct ConduitControlState : public AudioConduitControlInterface,
+ public VideoConduitControlInterface {
+ ConduitControlState(RTCRtpTransceiver* aTransceiver, RTCRtpSender* aSender,
+ RTCRtpReceiver* aReceiver)
+ : mTransceiver(new nsMainThreadPtrHolder<RTCRtpTransceiver>(
+ "ConduitControlState::mTransceiver", aTransceiver, false)),
+ mSender(new nsMainThreadPtrHolder<dom::RTCRtpSender>(
+ "ConduitControlState::mSender", aSender, false)),
+ mReceiver(new nsMainThreadPtrHolder<dom::RTCRtpReceiver>(
+ "ConduitControlState::mReceiver", aReceiver, false)) {}
+
+ const nsMainThreadPtrHandle<RTCRtpTransceiver> mTransceiver;
+ const nsMainThreadPtrHandle<RTCRtpSender> mSender;
+ const nsMainThreadPtrHandle<RTCRtpReceiver> mReceiver;
+
+ // MediaConduitControlInterface
+ AbstractCanonical<bool>* CanonicalReceiving() override {
+ return mReceiver->CanonicalReceiving();
+ }
+ AbstractCanonical<bool>* CanonicalTransmitting() override {
+ return mSender->CanonicalTransmitting();
+ }
+ AbstractCanonical<Ssrcs>* CanonicalLocalSsrcs() override {
+ return mSender->CanonicalSsrcs();
+ }
+ AbstractCanonical<std::string>* CanonicalLocalCname() override {
+ return mSender->CanonicalCname();
+ }
+ AbstractCanonical<std::string>* CanonicalMid() override {
+ return mTransceiver->CanonicalMid();
+ }
+ AbstractCanonical<Ssrc>* CanonicalRemoteSsrc() override {
+ return mReceiver->CanonicalSsrc();
+ }
+ AbstractCanonical<std::string>* CanonicalSyncGroup() override {
+ return mTransceiver->CanonicalSyncGroup();
+ }
+ AbstractCanonical<RtpExtList>* CanonicalLocalRecvRtpExtensions() override {
+ return mReceiver->CanonicalLocalRtpExtensions();
+ }
+ AbstractCanonical<RtpExtList>* CanonicalLocalSendRtpExtensions() override {
+ return mSender->CanonicalLocalRtpExtensions();
+ }
+
+ // AudioConduitControlInterface
+ AbstractCanonical<Maybe<AudioCodecConfig>>* CanonicalAudioSendCodec()
+ override {
+ return mSender->CanonicalAudioCodec();
+ }
+ AbstractCanonical<std::vector<AudioCodecConfig>>* CanonicalAudioRecvCodecs()
+ override {
+ return mReceiver->CanonicalAudioCodecs();
+ }
+ MediaEventSource<DtmfEvent>& OnDtmfEvent() override {
+ return mSender->GetDtmf()->OnDtmfEvent();
+ }
+
+ // VideoConduitControlInterface
+ AbstractCanonical<Ssrcs>* CanonicalLocalVideoRtxSsrcs() override {
+ return mSender->CanonicalVideoRtxSsrcs();
+ }
+ AbstractCanonical<Ssrc>* CanonicalRemoteVideoRtxSsrc() override {
+ return mReceiver->CanonicalVideoRtxSsrc();
+ }
+ AbstractCanonical<Maybe<VideoCodecConfig>>* CanonicalVideoSendCodec()
+ override {
+ return mSender->CanonicalVideoCodec();
+ }
+ AbstractCanonical<Maybe<RtpRtcpConfig>>* CanonicalVideoSendRtpRtcpConfig()
+ override {
+ return mSender->CanonicalVideoRtpRtcpConfig();
+ }
+ AbstractCanonical<std::vector<VideoCodecConfig>>* CanonicalVideoRecvCodecs()
+ override {
+ return mReceiver->CanonicalVideoCodecs();
+ }
+ AbstractCanonical<Maybe<RtpRtcpConfig>>* CanonicalVideoRecvRtpRtcpConfig()
+ override {
+ return mReceiver->CanonicalVideoRtpRtcpConfig();
+ }
+ AbstractCanonical<webrtc::VideoCodecMode>* CanonicalVideoCodecMode()
+ override {
+ return mSender->CanonicalVideoCodecMode();
+ }
+};
+} // namespace
+
+MOZ_MTLOG_MODULE("RTCRtpTransceiver")
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(RTCRtpTransceiver)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(RTCRtpTransceiver)
+ if (tmp->mHandlingUnlink) {
+ tmp->BreakCycles();
+ tmp->mHandlingUnlink = false;
+ }
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(RTCRtpTransceiver)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWindow, mPc, mSendTrack, mReceiver,
+ mSender, mDtlsTransport,
+ mLastStableDtlsTransport)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(RTCRtpTransceiver)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(RTCRtpTransceiver)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(RTCRtpTransceiver)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+#define INIT_CANONICAL(name, val) \
+ name(AbstractThread::MainThread(), val, \
+ "RTCRtpTransceiver::" #name " (Canonical)")
+
+RTCRtpTransceiver::RTCRtpTransceiver(
+ nsPIDOMWindowInner* aWindow, bool aPrivacyNeeded, PeerConnectionImpl* aPc,
+ MediaTransportHandler* aTransportHandler, JsepSession* aJsepSession,
+ const std::string& aTransceiverId, bool aIsVideo,
+ nsISerialEventTarget* aStsThread, dom::MediaStreamTrack* aSendTrack,
+ WebrtcCallWrapper* aCallWrapper, RTCStatsIdGenerator* aIdGenerator)
+ : mWindow(aWindow),
+ mPc(aPc),
+ mTransportHandler(aTransportHandler),
+ mTransceiverId(aTransceiverId),
+ mJsepTransceiver(*aJsepSession->GetTransceiver(mTransceiverId)),
+ mStsThread(aStsThread),
+ mCallWrapper(aCallWrapper),
+ mSendTrack(aSendTrack),
+ mIdGenerator(aIdGenerator),
+ mPrincipalPrivacy(aPrivacyNeeded ? PrincipalPrivacy::Private
+ : PrincipalPrivacy::NonPrivate),
+ mIsVideo(aIsVideo),
+ INIT_CANONICAL(mMid, std::string()),
+ INIT_CANONICAL(mSyncGroup, std::string()) {}
+
+#undef INIT_CANONICAL
+
+RTCRtpTransceiver::~RTCRtpTransceiver() = default;
+
+SdpDirectionAttribute::Direction ToSdpDirection(
+ RTCRtpTransceiverDirection aDirection) {
+ switch (aDirection) {
+ case dom::RTCRtpTransceiverDirection::Sendrecv:
+ return SdpDirectionAttribute::Direction::kSendrecv;
+ case dom::RTCRtpTransceiverDirection::Sendonly:
+ return SdpDirectionAttribute::Direction::kSendonly;
+ case dom::RTCRtpTransceiverDirection::Recvonly:
+ return SdpDirectionAttribute::Direction::kRecvonly;
+ case dom::RTCRtpTransceiverDirection::Inactive:
+ return SdpDirectionAttribute::Direction::kInactive;
+ case dom::RTCRtpTransceiverDirection::EndGuard_:;
+ }
+ MOZ_CRASH("Invalid transceiver direction!");
+}
+
+static uint32_t sRemoteSourceId = 0;
+
+// TODO(bug 1401592): Once we implement the sendEncodings stuff, there will
+// need to be validation code in here.
+void RTCRtpTransceiver::Init(const RTCRtpTransceiverInit& aInit,
+ ErrorResult& aRv) {
+ TrackingId trackingId(TrackingId::Source::RTCRtpReceiver, sRemoteSourceId++,
+ TrackingId::TrackAcrossProcesses::Yes);
+ if (IsVideo()) {
+ InitVideo(trackingId);
+ } else {
+ InitAudio();
+ }
+
+ if (!IsValid()) {
+ aRv = NS_ERROR_UNEXPECTED;
+ return;
+ }
+
+ mReceiver = new RTCRtpReceiver(mWindow, mPrincipalPrivacy, mPc,
+ mTransportHandler, mCallWrapper->mCallThread,
+ mStsThread, mConduit, this, trackingId);
+
+ mSender = new RTCRtpSender(mWindow, mPc, mTransportHandler,
+ mCallWrapper->mCallThread, mStsThread, mConduit,
+ mSendTrack, aInit.mSendEncodings, this);
+
+ if (mConduit) {
+ InitConduitControl();
+ }
+
+ mSender->SetStreamsImpl(aInit.mStreams);
+ mDirection = aInit.mDirection;
+}
+
+void RTCRtpTransceiver::SetDtlsTransport(dom::RTCDtlsTransport* aDtlsTransport,
+ bool aStable) {
+ mDtlsTransport = aDtlsTransport;
+ if (aStable) {
+ mLastStableDtlsTransport = mDtlsTransport;
+ }
+}
+
+void RTCRtpTransceiver::RollbackToStableDtlsTransport() {
+ mDtlsTransport = mLastStableDtlsTransport;
+}
+
+void RTCRtpTransceiver::InitAudio() {
+ mConduit = AudioSessionConduit::Create(mCallWrapper, mStsThread);
+
+ if (!mConduit) {
+ MOZ_MTLOG(ML_ERROR, mPc->GetHandle()
+ << "[" << mMid.Ref() << "]: " << __FUNCTION__
+ << ": Failed to create AudioSessionConduit");
+ // TODO(bug 1422897): We need a way to record this when it happens in the
+ // wild.
+ }
+}
+
+void RTCRtpTransceiver::InitVideo(const TrackingId& aRecvTrackingId) {
+ VideoSessionConduit::Options options;
+ options.mVideoLatencyTestEnable =
+ Preferences::GetBool("media.video.test_latency", false);
+ options.mMinBitrate = std::max(
+ 0,
+ Preferences::GetInt("media.peerconnection.video.min_bitrate", 0) * 1000);
+ options.mStartBitrate = std::max(
+ 0, Preferences::GetInt("media.peerconnection.video.start_bitrate", 0) *
+ 1000);
+ options.mPrefMaxBitrate = std::max(
+ 0,
+ Preferences::GetInt("media.peerconnection.video.max_bitrate", 0) * 1000);
+ if (options.mMinBitrate != 0 &&
+ options.mMinBitrate < kViEMinCodecBitrate_bps) {
+ options.mMinBitrate = kViEMinCodecBitrate_bps;
+ }
+ if (options.mStartBitrate < options.mMinBitrate) {
+ options.mStartBitrate = options.mMinBitrate;
+ }
+ if (options.mPrefMaxBitrate &&
+ options.mStartBitrate > options.mPrefMaxBitrate) {
+ options.mStartBitrate = options.mPrefMaxBitrate;
+ }
+ // XXX We'd love if this was a live param for testing adaptation/etc
+ // in automation
+ options.mMinBitrateEstimate =
+ std::max(0, Preferences::GetInt(
+ "media.peerconnection.video.min_bitrate_estimate", 0) *
+ 1000);
+ options.mSpatialLayers = std::max(
+ 1, Preferences::GetInt("media.peerconnection.video.svc.spatial", 0));
+ options.mTemporalLayers = std::max(
+ 1, Preferences::GetInt("media.peerconnection.video.svc.temporal", 0));
+ options.mDenoising =
+ Preferences::GetBool("media.peerconnection.video.denoising", false);
+ options.mLockScaling =
+ Preferences::GetBool("media.peerconnection.video.lock_scaling", false);
+
+ mConduit =
+ VideoSessionConduit::Create(mCallWrapper, mStsThread, std::move(options),
+ mPc->GetHandle(), aRecvTrackingId);
+
+ if (!mConduit) {
+ MOZ_MTLOG(ML_ERROR, mPc->GetHandle()
+ << "[" << mMid.Ref() << "]: " << __FUNCTION__
+ << ": Failed to create VideoSessionConduit");
+ // TODO(bug 1422897): We need a way to record this when it happens in the
+ // wild.
+ }
+}
+
+void RTCRtpTransceiver::InitConduitControl() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mConduit);
+ ConduitControlState control(this, mSender, mReceiver);
+ mCallWrapper->mCallThread->Dispatch(NS_NewRunnableFunction(
+ __func__, [conduit = mConduit, control = std::move(control)]() mutable {
+ conduit->AsVideoSessionConduit().apply(
+ [&](VideoSessionConduit* aConduit) {
+ aConduit->InitControl(&control);
+ });
+ conduit->AsAudioSessionConduit().apply(
+ [&](AudioSessionConduit* aConduit) {
+ aConduit->InitControl(&control);
+ });
+ }));
+}
+
+void RTCRtpTransceiver::Close() {
+ // Called via PCImpl::Close -> PCImpl::CloseInt -> PCImpl::ShutdownMedia ->
+ // PCMedia::SelfDestruct. Satisfies step 7 of
+ // https://w3c.github.io/webrtc-pc/#dom-rtcpeerconnection-close
+ mShutdown = true;
+ if (mDtlsTransport) {
+ mDtlsTransport->UpdateState(TransportLayer::TS_CLOSED);
+ }
+ StopImpl();
+}
+
+void RTCRtpTransceiver::BreakCycles() {
+ mSender->BreakCycles();
+ mReceiver->BreakCycles();
+ mWindow = nullptr;
+ mSendTrack = nullptr;
+ mSender = nullptr;
+ mReceiver = nullptr;
+ mDtlsTransport = nullptr;
+ mLastStableDtlsTransport = nullptr;
+ mPc = nullptr;
+}
+
+// TODO: Only called from one place in PeerConnectionImpl, synchronously, when
+// the JSEP engine has successfully completed an offer/answer exchange. This is
+// a bit squirrely, since identity validation happens asynchronously in
+// PeerConnection.jsm. This probably needs to happen once all the "in parallel"
+// steps have succeeded, but before we queue the task for JS observable state
+// updates.
+nsresult RTCRtpTransceiver::UpdateTransport() {
+ if (!mHasTransport) {
+ return NS_OK;
+ }
+
+ mReceiver->UpdateTransport();
+ mSender->UpdateTransport();
+ return NS_OK;
+}
+
+nsresult RTCRtpTransceiver::UpdateConduit() {
+ if (mStopped) {
+ return NS_OK;
+ }
+
+ mReceiver->UpdateConduit();
+ mSender->MaybeUpdateConduit();
+
+ return NS_OK;
+}
+
+void RTCRtpTransceiver::UpdatePrincipalPrivacy(PrincipalPrivacy aPrivacy) {
+ if (mPrincipalPrivacy == aPrivacy) {
+ return;
+ }
+
+ mPrincipalPrivacy = aPrivacy;
+ mReceiver->UpdatePrincipalPrivacy(mPrincipalPrivacy);
+}
+
+void RTCRtpTransceiver::ResetSync() { mSyncGroup = std::string(); }
+
+// TODO: Only called from one place in PeerConnectionImpl, synchronously, when
+// the JSEP engine has successfully completed an offer/answer exchange. This is
+// a bit squirrely, since identity validation happens asynchronously in
+// PeerConnection.jsm. This probably needs to happen once all the "in parallel"
+// steps have succeeded, but before we queue the task for JS observable state
+// updates.
+nsresult RTCRtpTransceiver::SyncWithMatchingVideoConduits(
+ nsTArray<RefPtr<RTCRtpTransceiver>>& transceivers) {
+ if (mStopped) {
+ return NS_OK;
+ }
+
+ if (IsVideo()) {
+ MOZ_MTLOG(ML_ERROR, mPc->GetHandle()
+ << "[" << mMid.Ref() << "]: " << __FUNCTION__
+ << " called when transceiver is not "
+ "video! This should never happen.");
+ MOZ_CRASH();
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ std::set<std::string> myReceiveStreamIds;
+ myReceiveStreamIds.insert(mReceiver->GetStreamIds().begin(),
+ mReceiver->GetStreamIds().end());
+
+ for (RefPtr<RTCRtpTransceiver>& transceiver : transceivers) {
+ if (!transceiver->IsValid()) {
+ continue;
+ }
+
+ if (!transceiver->IsVideo()) {
+ // |this| is an audio transceiver, so we skip other audio transceivers
+ continue;
+ }
+
+ // Maybe could make this more efficient by cacheing this set, but probably
+ // not worth it.
+ for (const std::string& streamId :
+ transceiver->Receiver()->GetStreamIds()) {
+ if (myReceiveStreamIds.count(streamId)) {
+ // Ok, we have one video, one non-video - cross the streams!
+ mSyncGroup = streamId;
+ transceiver->mSyncGroup = streamId;
+
+ MOZ_MTLOG(ML_DEBUG, mPc->GetHandle()
+ << "[" << mMid.Ref() << "]: " << __FUNCTION__
+ << " Syncing " << mConduit.get() << " to "
+ << transceiver->mConduit.get());
+
+ // The sync code in call.cc only permits sync between audio stream and
+ // one video stream. They take the first match, so there's no point in
+ // continuing here. If we want to change the default, we should sort
+ // video streams here and only call SetSyncGroup on the chosen stream.
+ break;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+bool RTCRtpTransceiver::ConduitHasPluginID(uint64_t aPluginID) {
+ return mConduit && mConduit->HasCodecPluginID(aPluginID);
+}
+
+void RTCRtpTransceiver::SyncFromJsep(const JsepSession& aSession) {
+ MOZ_MTLOG(ML_DEBUG, mPc->GetHandle()
+ << "[" << mMid.Ref() << "]: " << __FUNCTION__
+ << " Syncing from JSEP transceiver");
+ if (mShutdown) {
+ // Shutdown_m has already been called, probably due to pc.close(). Just
+ // nod and smile.
+ return;
+ }
+
+ mJsepTransceiver = *aSession.GetTransceiver(mTransceiverId);
+
+ // Transceivers can stop due to JSEP negotiation, so we need to check that
+ if (mJsepTransceiver.IsStopped()) {
+ StopImpl();
+ }
+
+ mReceiver->SyncFromJsep(mJsepTransceiver);
+ mSender->SyncFromJsep(mJsepTransceiver);
+
+ // mid from JSEP
+ if (mJsepTransceiver.IsAssociated()) {
+ mMid = mJsepTransceiver.GetMid();
+ } else {
+ mMid = std::string();
+ }
+
+ // currentDirection from JSEP, but not if "this transceiver has never been
+ // represented in an offer/answer exchange"
+ if (mJsepTransceiver.HasLevel() && mJsepTransceiver.IsNegotiated()) {
+ if (mJsepTransceiver.mRecvTrack.GetActive()) {
+ if (mJsepTransceiver.mSendTrack.GetActive()) {
+ mCurrentDirection.SetValue(dom::RTCRtpTransceiverDirection::Sendrecv);
+ mHasBeenUsedToSend = true;
+ } else {
+ mCurrentDirection.SetValue(dom::RTCRtpTransceiverDirection::Recvonly);
+ }
+ } else {
+ if (mJsepTransceiver.mSendTrack.GetActive()) {
+ mCurrentDirection.SetValue(dom::RTCRtpTransceiverDirection::Sendonly);
+ mHasBeenUsedToSend = true;
+ } else {
+ mCurrentDirection.SetValue(dom::RTCRtpTransceiverDirection::Inactive);
+ }
+ }
+ }
+
+ mShouldRemove = mJsepTransceiver.IsRemoved();
+ mHasTransport = mJsepTransceiver.HasLevel() && !mJsepTransceiver.IsStopped();
+}
+
+void RTCRtpTransceiver::SyncToJsep(JsepSession& aSession) const {
+ MOZ_MTLOG(ML_DEBUG, mPc->GetHandle()
+ << "[" << mMid.Ref() << "]: " << __FUNCTION__
+ << " Syncing to JSEP transceiver");
+
+ aSession.ApplyToTransceiver(
+ mTransceiverId, [this, self = RefPtr<const RTCRtpTransceiver>(this)](
+ JsepTransceiver& aTransceiver) {
+ mReceiver->SyncToJsep(aTransceiver);
+ mSender->SyncToJsep(aTransceiver);
+ aTransceiver.mJsDirection = ToSdpDirection(mDirection);
+ if (mStopped) {
+ aTransceiver.Stop();
+ }
+ });
+}
+
+void RTCRtpTransceiver::GetKind(nsAString& aKind) const {
+ // The transceiver kind of an RTCRtpTransceiver is defined by the kind of the
+ // associated RTCRtpReceiver's MediaStreamTrack object.
+ MOZ_ASSERT(mReceiver && mReceiver->Track());
+ mReceiver->Track()->GetKind(aKind);
+}
+
+void RTCRtpTransceiver::GetMid(nsAString& aMid) const {
+ if (!mMid.Ref().empty()) {
+ aMid = NS_ConvertUTF8toUTF16(mMid.Ref());
+ } else {
+ aMid.SetIsVoid(true);
+ }
+}
+
+std::string RTCRtpTransceiver::GetMidAscii() const {
+ if (mMid.Ref().empty()) {
+ return std::string();
+ }
+
+ return mMid.Ref();
+}
+
+void RTCRtpTransceiver::SetDirection(RTCRtpTransceiverDirection aDirection,
+ ErrorResult& aRv) {
+ if (mStopped) {
+ aRv.ThrowInvalidStateError("Transceiver is stopped!");
+ return;
+ }
+
+ if (aDirection == mDirection) {
+ return;
+ }
+
+ SetDirectionInternal(aDirection);
+
+ mPc->UpdateNegotiationNeeded();
+}
+
+void RTCRtpTransceiver::SetDirectionInternal(
+ RTCRtpTransceiverDirection aDirection) {
+ // We do not update the direction on the JsepTransceiver until sync
+ mDirection = aDirection;
+}
+
+bool RTCRtpTransceiver::ShouldRemove() const { return mShouldRemove; }
+
+bool RTCRtpTransceiver::CanSendDTMF() const {
+ // Spec says: "If connection's RTCPeerConnectionState is not "connected"
+ // return false." We don't support that right now. This is supposed to be
+ // true once ICE is complete, and _all_ DTLS handshakes are also complete. We
+ // don't really have access to the state of _all_ of our DTLS states either.
+ // Our pipeline _does_ know whether SRTP/SRTCP is ready, which happens
+ // immediately after our transport finishes DTLS (unless there was an error),
+ // so this is pretty close.
+ // TODO (bug 1265827): Base this on RTCPeerConnectionState instead.
+ // TODO (bug 1623193): Tighten this up
+ if (!IsSending() || !mSender->GetTrack()) {
+ return false;
+ }
+
+ // Ok, it looks like the connection is up and sending. Did we negotiate
+ // telephone-event?
+ const JsepTrackNegotiatedDetails* details =
+ mJsepTransceiver.mSendTrack.GetNegotiatedDetails();
+ if (NS_WARN_IF(!details || !details->GetEncodingCount())) {
+ // What?
+ return false;
+ }
+
+ for (size_t i = 0; i < details->GetEncodingCount(); ++i) {
+ const auto& encoding = details->GetEncoding(i);
+ for (const auto& codec : encoding.GetCodecs()) {
+ if (codec->mName == "telephone-event") {
+ return true;
+ }
+ }
+ }
+
+ return false;
+}
+
+JSObject* RTCRtpTransceiver::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return dom::RTCRtpTransceiver_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsPIDOMWindowInner* RTCRtpTransceiver::GetParentObject() const {
+ return mWindow;
+}
+
+static void JsepCodecDescToAudioCodecConfig(
+ const JsepAudioCodecDescription& aCodec, Maybe<AudioCodecConfig>* aConfig) {
+ uint16_t pt;
+
+ // TODO(bug 1761272): Getting the pt for a JsepAudioCodecDescription should be
+ // infallible.
+ if (NS_WARN_IF(!aCodec.GetPtAsInt(&pt))) {
+ MOZ_MTLOG(ML_ERROR, "Invalid payload type: " << aCodec.mDefaultPt);
+ MOZ_ASSERT(false);
+ return;
+ }
+
+ // libwebrtc crashes if we attempt to configure a mono recv codec
+ bool sendMono = aCodec.mForceMono && aCodec.mDirection == sdp::kSend;
+
+ *aConfig = Some(AudioCodecConfig(
+ pt, aCodec.mName, static_cast<int>(aCodec.mClock),
+ sendMono ? 1 : static_cast<int>(aCodec.mChannels), aCodec.mFECEnabled));
+ (*aConfig)->mMaxPlaybackRate = static_cast<int>(aCodec.mMaxPlaybackRate);
+ (*aConfig)->mDtmfEnabled = aCodec.mDtmfEnabled;
+ (*aConfig)->mDTXEnabled = aCodec.mDTXEnabled;
+ (*aConfig)->mMaxAverageBitrate = aCodec.mMaxAverageBitrate;
+ (*aConfig)->mFrameSizeMs = aCodec.mFrameSizeMs;
+ (*aConfig)->mMinFrameSizeMs = aCodec.mMinFrameSizeMs;
+ (*aConfig)->mMaxFrameSizeMs = aCodec.mMaxFrameSizeMs;
+ (*aConfig)->mCbrEnabled = aCodec.mCbrEnabled;
+}
+
+// TODO: This and the next function probably should move to JsepTransceiver
+Maybe<const std::vector<UniquePtr<JsepCodecDescription>>&>
+RTCRtpTransceiver::GetNegotiatedSendCodecs() const {
+ if (!mJsepTransceiver.mSendTrack.GetActive()) {
+ return Nothing();
+ }
+
+ const auto* details = mJsepTransceiver.mSendTrack.GetNegotiatedDetails();
+ if (!details) {
+ return Nothing();
+ }
+
+ if (details->GetEncodingCount() == 0) {
+ return Nothing();
+ }
+
+ return SomeRef(details->GetEncoding(0).GetCodecs());
+}
+
+Maybe<const std::vector<UniquePtr<JsepCodecDescription>>&>
+RTCRtpTransceiver::GetNegotiatedRecvCodecs() const {
+ if (!mJsepTransceiver.mRecvTrack.GetActive()) {
+ return Nothing();
+ }
+
+ const auto* details = mJsepTransceiver.mRecvTrack.GetNegotiatedDetails();
+ if (!details) {
+ return Nothing();
+ }
+
+ if (details->GetEncodingCount() == 0) {
+ return Nothing();
+ }
+
+ return SomeRef(details->GetEncoding(0).GetCodecs());
+}
+
+// TODO: Maybe move this someplace else?
+/*static*/
+void RTCRtpTransceiver::NegotiatedDetailsToAudioCodecConfigs(
+ const JsepTrackNegotiatedDetails& aDetails,
+ std::vector<AudioCodecConfig>* aConfigs) {
+ Maybe<AudioCodecConfig> telephoneEvent;
+
+ if (aDetails.GetEncodingCount()) {
+ for (const auto& codec : aDetails.GetEncoding(0).GetCodecs()) {
+ if (NS_WARN_IF(codec->Type() != SdpMediaSection::kAudio)) {
+ MOZ_ASSERT(false, "Codec is not audio! This is a JSEP bug.");
+ return;
+ }
+ Maybe<AudioCodecConfig> config;
+ const JsepAudioCodecDescription& audio =
+ static_cast<const JsepAudioCodecDescription&>(*codec);
+ JsepCodecDescToAudioCodecConfig(audio, &config);
+ if (config->mName == "telephone-event") {
+ telephoneEvent = std::move(config);
+ } else {
+ aConfigs->push_back(std::move(*config));
+ }
+ }
+ }
+
+ // Put telephone event at the back, because webrtc.org crashes if we don't
+ // If we need to do even more sorting, we should use std::sort.
+ if (telephoneEvent) {
+ aConfigs->push_back(std::move(*telephoneEvent));
+ }
+}
+
+auto RTCRtpTransceiver::GetActivePayloadTypes() const
+ -> RefPtr<ActivePayloadTypesPromise> {
+ if (!mConduit) {
+ return ActivePayloadTypesPromise::CreateAndResolve(PayloadTypes(),
+ __func__);
+ }
+
+ if (!mCallWrapper) {
+ return ActivePayloadTypesPromise::CreateAndResolve(PayloadTypes(),
+ __func__);
+ }
+
+ return InvokeAsync(mCallWrapper->mCallThread, __func__,
+ [conduit = mConduit]() {
+ PayloadTypes pts;
+ pts.mSendPayloadType = conduit->ActiveSendPayloadType();
+ pts.mRecvPayloadType = conduit->ActiveRecvPayloadType();
+ return ActivePayloadTypesPromise::CreateAndResolve(
+ std::move(pts), __func__);
+ });
+}
+
+static void JsepCodecDescToVideoCodecConfig(
+ const JsepVideoCodecDescription& aCodec, Maybe<VideoCodecConfig>* aConfig) {
+ uint16_t pt;
+
+ // TODO(bug 1761272): Getting the pt for a JsepVideoCodecDescription should be
+ // infallible.
+ if (NS_WARN_IF(!aCodec.GetPtAsInt(&pt))) {
+ MOZ_MTLOG(ML_ERROR, "Invalid payload type: " << aCodec.mDefaultPt);
+ MOZ_ASSERT(false);
+ return;
+ }
+
+ UniquePtr<VideoCodecConfigH264> h264Config;
+
+ if (aCodec.mName == "H264") {
+ h264Config = MakeUnique<VideoCodecConfigH264>();
+ size_t spropSize = sizeof(h264Config->sprop_parameter_sets);
+ strncpy(h264Config->sprop_parameter_sets,
+ aCodec.mSpropParameterSets.c_str(), spropSize);
+ h264Config->sprop_parameter_sets[spropSize - 1] = '\0';
+ h264Config->packetization_mode =
+ static_cast<int>(aCodec.mPacketizationMode);
+ h264Config->profile_level_id = static_cast<int>(aCodec.mProfileLevelId);
+ h264Config->tias_bw = 0; // TODO(bug 1403206)
+ }
+
+ *aConfig = Some(VideoCodecConfig(pt, aCodec.mName, aCodec.mConstraints,
+ h264Config.get()));
+
+ (*aConfig)->mAckFbTypes = aCodec.mAckFbTypes;
+ (*aConfig)->mNackFbTypes = aCodec.mNackFbTypes;
+ (*aConfig)->mCcmFbTypes = aCodec.mCcmFbTypes;
+ (*aConfig)->mRembFbSet = aCodec.RtcpFbRembIsSet();
+ (*aConfig)->mFECFbSet = aCodec.mFECEnabled;
+ (*aConfig)->mTransportCCFbSet = aCodec.RtcpFbTransportCCIsSet();
+ if (aCodec.mFECEnabled) {
+ uint16_t pt;
+ if (SdpHelper::GetPtAsInt(aCodec.mREDPayloadType, &pt)) {
+ (*aConfig)->mREDPayloadType = pt;
+ }
+ if (SdpHelper::GetPtAsInt(aCodec.mULPFECPayloadType, &pt)) {
+ (*aConfig)->mULPFECPayloadType = pt;
+ }
+ }
+ if (aCodec.mRtxEnabled) {
+ uint16_t pt;
+ if (SdpHelper::GetPtAsInt(aCodec.mRtxPayloadType, &pt)) {
+ (*aConfig)->mRTXPayloadType = pt;
+ }
+ }
+}
+
+// TODO: Maybe move this someplace else?
+/*static*/
+void RTCRtpTransceiver::NegotiatedDetailsToVideoCodecConfigs(
+ const JsepTrackNegotiatedDetails& aDetails,
+ std::vector<VideoCodecConfig>* aConfigs) {
+ if (aDetails.GetEncodingCount()) {
+ for (const auto& codec : aDetails.GetEncoding(0).GetCodecs()) {
+ if (NS_WARN_IF(codec->Type() != SdpMediaSection::kVideo)) {
+ MOZ_ASSERT(false, "Codec is not video! This is a JSEP bug.");
+ return;
+ }
+ Maybe<VideoCodecConfig> config;
+ const JsepVideoCodecDescription& video =
+ static_cast<const JsepVideoCodecDescription&>(*codec);
+
+ JsepCodecDescToVideoCodecConfig(video, &config);
+
+ config->mTias = aDetails.GetTias();
+
+ for (size_t i = 0; i < aDetails.GetEncodingCount(); ++i) {
+ const JsepTrackEncoding& jsepEncoding(aDetails.GetEncoding(i));
+ if (jsepEncoding.HasFormat(video.mDefaultPt)) {
+ VideoCodecConfig::Encoding encoding;
+ encoding.rid = jsepEncoding.mRid;
+ config->mEncodings.push_back(encoding);
+ }
+ }
+
+ aConfigs->push_back(std::move(*config));
+ }
+ }
+}
+
+void RTCRtpTransceiver::Stop(ErrorResult& aRv) {
+ if (mPc->IsClosed()) {
+ aRv.ThrowInvalidStateError("Peer connection is closed");
+ return;
+ }
+
+ StopImpl();
+ mPc->UpdateNegotiationNeeded();
+}
+
+void RTCRtpTransceiver::StopImpl() {
+ if (mStopped) {
+ return;
+ }
+
+ if (mCallWrapper) {
+ auto conduit = std::move(mConduit);
+ (conduit ? conduit->Shutdown()
+ : GenericPromise::CreateAndResolve(true, __func__))
+ ->Then(GetMainThreadSerialEventTarget(), __func__,
+ [sender = mSender, receiver = mReceiver]() mutable {
+ // Shutdown pipelines when conduits are guaranteed shut down,
+ // so that all packets sent from conduits can be delivered.
+ sender->Shutdown();
+ receiver->Shutdown();
+ });
+ mCallWrapper = nullptr;
+ }
+ mStopped = true;
+ mCurrentDirection.SetNull();
+
+ mSender->Stop();
+ mReceiver->Stop();
+
+ auto self = nsMainThreadPtrHandle<RTCRtpTransceiver>(
+ new nsMainThreadPtrHolder<RTCRtpTransceiver>(
+ "RTCRtpTransceiver::StopImpl::self", this, false));
+ mStsThread->Dispatch(NS_NewRunnableFunction(
+ __func__, [self] { self->mTransportHandler = nullptr; }));
+}
+
+bool RTCRtpTransceiver::IsVideo() const { return mIsVideo; }
+
+bool RTCRtpTransceiver::IsSending() const {
+ return mCurrentDirection == Nullable(RTCRtpTransceiverDirection::Sendonly) ||
+ mCurrentDirection == Nullable(RTCRtpTransceiverDirection::Sendrecv);
+}
+
+bool RTCRtpTransceiver::IsReceiving() const {
+ return mCurrentDirection == Nullable(RTCRtpTransceiverDirection::Recvonly) ||
+ mCurrentDirection == Nullable(RTCRtpTransceiverDirection::Sendrecv);
+}
+
+void RTCRtpTransceiver::ChainToDomPromiseWithCodecStats(
+ nsTArray<RefPtr<RTCStatsPromise>> aStats,
+ const RefPtr<dom::Promise>& aDomPromise) {
+ nsTArray<RTCCodecStats> codecStats =
+ mPc->GetCodecStats(mPc->GetTimestampMaker().GetNow().ToDom());
+
+ AutoTArray<
+ std::tuple<RTCRtpTransceiver*, RefPtr<RTCStatsPromise::AllPromiseType>>,
+ 1>
+ statsPromises;
+ statsPromises.AppendElement(std::make_tuple(
+ this, RTCStatsPromise::All(GetMainThreadSerialEventTarget(), aStats)));
+
+ ApplyCodecStats(std::move(codecStats), std::move(statsPromises))
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [aDomPromise, window = mWindow,
+ idGen = mIdGenerator](UniquePtr<RTCStatsCollection> aStats) mutable {
+ // Rewrite ids and merge stats collections into the final report.
+ AutoTArray<UniquePtr<RTCStatsCollection>, 1> stats;
+ stats.AppendElement(std::move(aStats));
+
+ RTCStatsCollection opaqueStats;
+ idGen->RewriteIds(std::move(stats), &opaqueStats);
+
+ RefPtr<RTCStatsReport> report(new RTCStatsReport(window));
+ report->Incorporate(opaqueStats);
+
+ aDomPromise->MaybeResolve(std::move(report));
+ },
+ [aDomPromise](nsresult aError) {
+ aDomPromise->MaybeReject(NS_ERROR_FAILURE);
+ });
+}
+
+RefPtr<RTCStatsPromise> RTCRtpTransceiver::ApplyCodecStats(
+ nsTArray<RTCCodecStats> aCodecStats,
+ nsTArray<
+ std::tuple<RTCRtpTransceiver*, RefPtr<RTCStatsPromise::AllPromiseType>>>
+ aTransceiverStatsPromises) {
+ MOZ_ASSERT(NS_IsMainThread());
+ // The process here is roughly:
+ // - Gather all inputs to the codec filtering process, including:
+ // - Each transceiver's transportIds
+ // - Each transceiver's active payload types (resolved)
+ // - Each transceiver's resolved stats
+ //
+ // Waiting (async) for multiple promises of different types is not supported
+ // by the MozPromise API (bug 1752318), so we are a bit finicky here. We
+ // create media::Refcountables of the types we want to resolve, and let
+ // these be shared across Then-functions through RefPtrs.
+ //
+ // - For each active payload type in a transceiver:
+ // - Register the codec stats for this payload type and transport if we
+ // haven't already done so
+ // - If it was a send payload type, assign the codec stats id for this
+ // payload type and transport to the transceiver's outbound-rtp and
+ // remote-inbound-rtp stats as codecId
+ // - If it was a recv payload type, assign the codec stats id for this
+ // payload type and transport to the transceiver's inbound-rtp and
+ // remote-outbound-rtp stats as codecId
+ //
+ // - Flatten all transceiver stats collections into one, and set the
+ // registered codec stats on it
+
+ // Wrap codec stats in a Refcountable<> to allow sharing across promise
+ // handlers.
+ auto codecStats = MakeRefPtr<media::Refcountable<nsTArray<RTCCodecStats>>>();
+ *codecStats = std::move(aCodecStats);
+
+ struct IdComparator {
+ bool operator()(const RTCCodecStats& aA, const RTCCodecStats& aB) const {
+ return aA.mId.Value() < aB.mId.Value();
+ }
+ };
+
+ // Stores distinct codec stats by id; to avoid dupes within a transport.
+ auto finalCodecStats =
+ MakeRefPtr<media::Refcountable<std::set<RTCCodecStats, IdComparator>>>();
+
+ // All the transceiver rtp stream stats in a single array. These stats will,
+ // when resolved, contain codecIds.
+ nsTArray<RefPtr<RTCStatsPromise>> promises(
+ aTransceiverStatsPromises.Length());
+
+ for (const auto& [transceiver, allPromise] : aTransceiverStatsPromises) {
+ // Per transceiver, gather up what we need to assign codecId to this
+ // transceiver's rtp stream stats. Register codec stats while we're at it.
+ auto payloadTypes =
+ MakeRefPtr<media::Refcountable<RTCRtpTransceiver::PayloadTypes>>();
+ promises.AppendElement(
+ transceiver->GetActivePayloadTypes()
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [payloadTypes, allPromise = allPromise](
+ RTCRtpTransceiver::PayloadTypes aPayloadTypes) {
+ // Forward active payload types to the next Then-handler.
+ *payloadTypes = std::move(aPayloadTypes);
+ return allPromise;
+ },
+ [] {
+ MOZ_CRASH("Unexpected reject");
+ return RTCStatsPromise::AllPromiseType::CreateAndReject(
+ NS_ERROR_UNEXPECTED, __func__);
+ })
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [codecStats, finalCodecStats, payloadTypes,
+ transportId =
+ NS_ConvertASCIItoUTF16(transceiver->GetTransportId())](
+ nsTArray<UniquePtr<RTCStatsCollection>>
+ aTransceiverStats) mutable {
+ // We have all the data we need to register codec stats and
+ // assign codecIds for this transceiver's rtp stream stats.
+
+ auto report = MakeUnique<RTCStatsCollection>();
+ FlattenStats(std::move(aTransceiverStats), report.get());
+
+ // Find the codec stats we are looking for, based on the
+ // transportId and the active payload types.
+ Maybe<RTCCodecStats&> sendCodec;
+ Maybe<RTCCodecStats&> recvCodec;
+ for (auto& codec : *codecStats) {
+ if (payloadTypes->mSendPayloadType.isSome() ==
+ sendCodec.isSome() &&
+ payloadTypes->mRecvPayloadType.isSome() ==
+ recvCodec.isSome()) {
+ // We have found all the codec stats we were looking for.
+ break;
+ }
+ if (codec.mTransportId != transportId) {
+ continue;
+ }
+ if (payloadTypes->mSendPayloadType &&
+ *payloadTypes->mSendPayloadType ==
+ static_cast<int>(codec.mPayloadType) &&
+ (!codec.mCodecType.WasPassed() ||
+ codec.mCodecType.Value() == RTCCodecType::Encode)) {
+ MOZ_ASSERT(!sendCodec,
+ "At most one send codec stat per transceiver");
+ sendCodec = SomeRef(codec);
+ }
+ if (payloadTypes->mRecvPayloadType &&
+ *payloadTypes->mRecvPayloadType ==
+ static_cast<int>(codec.mPayloadType) &&
+ (!codec.mCodecType.WasPassed() ||
+ codec.mCodecType.Value() == RTCCodecType::Decode)) {
+ MOZ_ASSERT(!recvCodec,
+ "At most one recv codec stat per transceiver");
+ recvCodec = SomeRef(codec);
+ }
+ }
+
+ // Register and assign codecIds for the found codec stats.
+ if (sendCodec) {
+ finalCodecStats->insert(*sendCodec);
+ for (auto& stat : report->mOutboundRtpStreamStats) {
+ stat.mCodecId.Construct(sendCodec->mId.Value());
+ }
+ for (auto& stat : report->mRemoteInboundRtpStreamStats) {
+ stat.mCodecId.Construct(sendCodec->mId.Value());
+ }
+ }
+ if (recvCodec) {
+ finalCodecStats->insert(*recvCodec);
+ for (auto& stat : report->mInboundRtpStreamStats) {
+ stat.mCodecId.Construct(recvCodec->mId.Value());
+ }
+ for (auto& stat : report->mRemoteOutboundRtpStreamStats) {
+ stat.mCodecId.Construct(recvCodec->mId.Value());
+ }
+ }
+
+ return RTCStatsPromise::CreateAndResolve(std::move(report),
+ __func__);
+ },
+ [] {
+ MOZ_CRASH("Unexpected reject");
+ return RTCStatsPromise::CreateAndReject(NS_ERROR_UNEXPECTED,
+ __func__);
+ }));
+ }
+
+ return RTCStatsPromise::All(GetMainThreadSerialEventTarget(), promises)
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [finalCodecStats = std::move(finalCodecStats)](
+ nsTArray<UniquePtr<RTCStatsCollection>> aStats) mutable {
+ auto finalStats = MakeUnique<RTCStatsCollection>();
+ FlattenStats(std::move(aStats), finalStats.get());
+ MOZ_ASSERT(finalStats->mCodecStats.IsEmpty());
+ if (!finalStats->mCodecStats.SetCapacity(finalCodecStats->size(),
+ fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ while (!finalCodecStats->empty()) {
+ auto node = finalCodecStats->extract(finalCodecStats->begin());
+ if (!finalStats->mCodecStats.AppendElement(
+ std::move(node.value()), fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ }
+ return RTCStatsPromise::CreateAndResolve(std::move(finalStats),
+ __func__);
+ },
+ [] {
+ MOZ_CRASH("Unexpected reject");
+ return RTCStatsPromise::CreateAndReject(NS_ERROR_UNEXPECTED,
+ __func__);
+ });
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/jsapi/RTCRtpTransceiver.h b/dom/media/webrtc/jsapi/RTCRtpTransceiver.h
new file mode 100644
index 0000000000..4830e465a0
--- /dev/null
+++ b/dom/media/webrtc/jsapi/RTCRtpTransceiver.h
@@ -0,0 +1,243 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef _TRANSCEIVERIMPL_H_
+#define _TRANSCEIVERIMPL_H_
+
+#include <string>
+#include "mozilla/StateMirroring.h"
+#include "mozilla/RefPtr.h"
+#include "nsCOMPtr.h"
+#include "nsISerialEventTarget.h"
+#include "nsTArray.h"
+#include "mozilla/dom/MediaStreamTrack.h"
+#include "ErrorList.h"
+#include "jsep/JsepSession.h"
+#include "transport/transportlayer.h" // For TransportLayer::State
+#include "mozilla/dom/RTCRtpTransceiverBinding.h"
+#include "RTCStatsReport.h"
+
+class nsIPrincipal;
+
+namespace mozilla {
+class PeerIdentity;
+class MediaSessionConduit;
+class VideoSessionConduit;
+class AudioSessionConduit;
+struct AudioCodecConfig;
+class VideoCodecConfig; // Why is this a class, but AudioCodecConfig a struct?
+class MediaPipelineTransmit;
+class MediaPipeline;
+class MediaPipelineFilter;
+class MediaTransportHandler;
+class RTCStatsIdGenerator;
+class WebrtcCallWrapper;
+class JsepTrackNegotiatedDetails;
+class PeerConnectionImpl;
+enum class PrincipalPrivacy : uint8_t;
+
+namespace dom {
+class RTCDtlsTransport;
+class RTCDTMFSender;
+class RTCRtpTransceiver;
+struct RTCRtpSourceEntry;
+class RTCRtpReceiver;
+class RTCRtpSender;
+
+/**
+ * This is what ties all the various pieces that make up a transceiver
+ * together. This includes:
+ * MediaStreamTrack for rendering and capture
+ * MediaTransportHandler for RTP transmission/reception
+ * Audio/VideoConduit for feeding RTP/RTCP into webrtc.org for decoding, and
+ * feeding audio/video frames into webrtc.org for encoding into RTP/RTCP.
+ */
+class RTCRtpTransceiver : public nsISupports, public nsWrapperCache {
+ public:
+ /**
+ * |aSendTrack| might or might not be set.
+ */
+ RTCRtpTransceiver(
+ nsPIDOMWindowInner* aWindow, bool aPrivacyNeeded, PeerConnectionImpl* aPc,
+ MediaTransportHandler* aTransportHandler, JsepSession* aJsepSession,
+ const std::string& aTransceiverId, bool aIsVideo,
+ nsISerialEventTarget* aStsThread, MediaStreamTrack* aSendTrack,
+ WebrtcCallWrapper* aCallWrapper, RTCStatsIdGenerator* aIdGenerator);
+
+ void Init(const RTCRtpTransceiverInit& aInit, ErrorResult& aRv);
+
+ bool IsValid() const { return !!mConduit; }
+
+ nsresult UpdateTransport();
+
+ nsresult UpdateConduit();
+
+ void UpdatePrincipalPrivacy(PrincipalPrivacy aPrivacy);
+
+ void ResetSync();
+
+ nsresult SyncWithMatchingVideoConduits(
+ nsTArray<RefPtr<RTCRtpTransceiver>>& transceivers);
+
+ void Close();
+
+ void BreakCycles();
+
+ bool ConduitHasPluginID(uint64_t aPluginID);
+
+ // for webidl
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+ nsPIDOMWindowInner* GetParentObject() const;
+ RTCRtpReceiver* Receiver() const { return mReceiver; }
+ RTCRtpSender* Sender() const { return mSender; }
+ RTCDtlsTransport* GetDtlsTransport() const { return mDtlsTransport; }
+ void GetKind(nsAString& aKind) const;
+ void GetMid(nsAString& aMid) const;
+ RTCRtpTransceiverDirection Direction() const { return mDirection; }
+ void SetDirection(RTCRtpTransceiverDirection aDirection, ErrorResult& aRv);
+ Nullable<RTCRtpTransceiverDirection> GetCurrentDirection() {
+ return mCurrentDirection;
+ }
+ void Stop(ErrorResult& aRv);
+ void SetDirectionInternal(RTCRtpTransceiverDirection aDirection);
+ bool HasBeenUsedToSend() const { return mHasBeenUsedToSend; }
+
+ bool CanSendDTMF() const;
+ bool Stopped() const { return mStopped; }
+ void SyncToJsep(JsepSession& aSession) const;
+ void SyncFromJsep(const JsepSession& aSession);
+ std::string GetMidAscii() const;
+
+ void SetDtlsTransport(RTCDtlsTransport* aDtlsTransport, bool aStable);
+ void RollbackToStableDtlsTransport();
+
+ std::string GetTransportId() const {
+ return mJsepTransceiver.mTransport.mTransportId;
+ }
+
+ JsepTransceiver& GetJsepTransceiver() { return mJsepTransceiver; }
+
+ bool IsVideo() const;
+
+ bool IsSending() const;
+
+ bool IsReceiving() const;
+
+ bool ShouldRemove() const;
+
+ Maybe<const std::vector<UniquePtr<JsepCodecDescription>>&>
+ GetNegotiatedSendCodecs() const;
+
+ Maybe<const std::vector<UniquePtr<JsepCodecDescription>>&>
+ GetNegotiatedRecvCodecs() const;
+
+ struct PayloadTypes {
+ Maybe<int> mSendPayloadType;
+ Maybe<int> mRecvPayloadType;
+ };
+ using ActivePayloadTypesPromise = MozPromise<PayloadTypes, nsresult, true>;
+ RefPtr<ActivePayloadTypesPromise> GetActivePayloadTypes() const;
+
+ MediaSessionConduit* GetConduit() const { return mConduit; }
+
+ const std::string& GetJsepTransceiverId() const { return mTransceiverId; }
+
+ void SetRemovedFromPc() { mHandlingUnlink = true; }
+
+ // nsISupports
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(RTCRtpTransceiver)
+
+ static void NegotiatedDetailsToAudioCodecConfigs(
+ const JsepTrackNegotiatedDetails& aDetails,
+ std::vector<AudioCodecConfig>* aConfigs);
+
+ static void NegotiatedDetailsToVideoCodecConfigs(
+ const JsepTrackNegotiatedDetails& aDetails,
+ std::vector<VideoCodecConfig>* aConfigs);
+
+ /* Returns a promise that will contain the stats in aStats, along with the
+ * codec stats (which is a PC-wide thing) */
+ void ChainToDomPromiseWithCodecStats(nsTArray<RefPtr<RTCStatsPromise>> aStats,
+ const RefPtr<Promise>& aDomPromise);
+
+ /**
+ * Takes a set of codec stats (per-peerconnection) and a set of
+ * transceiver/transceiver-stats-promise tuples. Filters out all referenced
+ * codec stats based on the transceiver's transport and rtp stream stats.
+ * Finally returns the flattened stats containing the filtered codec stats and
+ * all given per-transceiver-stats.
+ */
+ static RefPtr<RTCStatsPromise> ApplyCodecStats(
+ nsTArray<RTCCodecStats> aCodecStats,
+ nsTArray<std::tuple<RTCRtpTransceiver*,
+ RefPtr<RTCStatsPromise::AllPromiseType>>>
+ aTransceiverStatsPromises);
+
+ AbstractCanonical<std::string>* CanonicalMid() { return &mMid; }
+ AbstractCanonical<std::string>* CanonicalSyncGroup() { return &mSyncGroup; }
+
+ private:
+ virtual ~RTCRtpTransceiver();
+ void InitAudio();
+ void InitVideo(const TrackingId& aRecvTrackingId);
+ void InitConduitControl();
+ void StopImpl();
+
+ nsCOMPtr<nsPIDOMWindowInner> mWindow;
+ RefPtr<PeerConnectionImpl> mPc;
+ RefPtr<MediaTransportHandler> mTransportHandler;
+ const std::string mTransceiverId;
+ // Copy of latest from the JSEP engine.
+ JsepTransceiver mJsepTransceiver;
+ nsCOMPtr<nsISerialEventTarget> mStsThread;
+ // state for webrtc.org that is shared between all transceivers
+ RefPtr<WebrtcCallWrapper> mCallWrapper;
+ RefPtr<MediaStreamTrack> mSendTrack;
+ RefPtr<RTCStatsIdGenerator> mIdGenerator;
+ RefPtr<MediaSessionConduit> mConduit;
+ // The spec says both RTCRtpReceiver and RTCRtpSender have a slot for
+ // an RTCDtlsTransport. They are always the same, so we'll store it
+ // here.
+ RefPtr<RTCDtlsTransport> mDtlsTransport;
+ // The spec says both RTCRtpReceiver and RTCRtpSender have a slot for
+ // a last stable state RTCDtlsTransport. They are always the same, so
+ // we'll store it here.
+ RefPtr<RTCDtlsTransport> mLastStableDtlsTransport;
+ RefPtr<RTCRtpReceiver> mReceiver;
+ RefPtr<RTCRtpSender> mSender;
+ RTCRtpTransceiverDirection mDirection = RTCRtpTransceiverDirection::Sendrecv;
+ Nullable<RTCRtpTransceiverDirection> mCurrentDirection;
+ bool mStopped = false;
+ bool mShutdown = false;
+ bool mHasBeenUsedToSend = false;
+ PrincipalPrivacy mPrincipalPrivacy;
+ bool mShouldRemove = false;
+ bool mHasTransport = false;
+ bool mIsVideo;
+ // This is really nasty. Most of the time, PeerConnectionImpl needs to be in
+ // charge of unlinking each RTCRtpTransceiver, because it needs to perform
+ // stats queries on its way out, which requires all of the RTCRtpTransceivers
+ // (and their transitive dependencies) to stick around until those stats
+ // queries are finished. However, when an RTCRtpTransceiver is removed from
+ // the PeerConnectionImpl due to negotiation, the PeerConnectionImpl
+ // releases its reference, which means the PeerConnectionImpl cannot be in
+ // charge of the unlink anymore. We cannot do the unlink when this reference
+ // is released either, because RTCRtpTransceiver might have some work it needs
+ // to do first. Also, JS may be maintaining a reference to the
+ // RTCRtpTransceiver (or one of its dependencies), which means it must remain
+ // fully functional after it is removed (meaning it cannot release any of its
+ // dependencies, or vice versa).
+ bool mHandlingUnlink = false;
+ std::string mTransportId;
+
+ Canonical<std::string> mMid;
+ Canonical<std::string> mSyncGroup;
+};
+
+} // namespace dom
+
+} // namespace mozilla
+
+#endif // _TRANSCEIVERIMPL_H_
diff --git a/dom/media/webrtc/jsapi/RTCSctpTransport.cpp b/dom/media/webrtc/jsapi/RTCSctpTransport.cpp
new file mode 100644
index 0000000000..63424968ae
--- /dev/null
+++ b/dom/media/webrtc/jsapi/RTCSctpTransport.cpp
@@ -0,0 +1,53 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "RTCSctpTransport.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/EventBinding.h"
+#include "mozilla/dom/RTCSctpTransportBinding.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(RTCSctpTransport, DOMEventTargetHelper,
+ mDtlsTransport)
+
+NS_IMPL_ADDREF_INHERITED(RTCSctpTransport, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(RTCSctpTransport, DOMEventTargetHelper)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(RTCSctpTransport)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+RTCSctpTransport::RTCSctpTransport(nsPIDOMWindowInner* aWindow,
+ RTCDtlsTransport& aDtlsTransport,
+ double aMaxMessageSize,
+ const Nullable<uint16_t>& aMaxChannels)
+ : DOMEventTargetHelper(aWindow),
+ mState(RTCSctpTransportState::Connecting),
+ mDtlsTransport(&aDtlsTransport),
+ mMaxMessageSize(aMaxMessageSize),
+ mMaxChannels(aMaxChannels) {}
+
+JSObject* RTCSctpTransport::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return RTCSctpTransport_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void RTCSctpTransport::UpdateState(RTCSctpTransportState aState) {
+ if (mState == RTCSctpTransportState::Closed || mState == aState) {
+ return;
+ }
+
+ mState = aState;
+
+ EventInit init;
+ init.mBubbles = false;
+ init.mCancelable = false;
+
+ RefPtr<Event> event = Event::Constructor(this, u"statechange"_ns, init);
+
+ DispatchTrustedEvent(event);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webrtc/jsapi/RTCSctpTransport.h b/dom/media/webrtc/jsapi/RTCSctpTransport.h
new file mode 100644
index 0000000000..8e21db5e73
--- /dev/null
+++ b/dom/media/webrtc/jsapi/RTCSctpTransport.h
@@ -0,0 +1,65 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _RTCSctpTransport_h_
+#define _RTCSctpTransport_h_
+
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/RefPtr.h"
+#include "js/RootingAPI.h"
+#include "RTCDtlsTransport.h"
+
+class nsPIDOMWindowInner;
+
+namespace mozilla::dom {
+
+enum class RTCSctpTransportState : uint8_t;
+
+class RTCSctpTransport : public DOMEventTargetHelper {
+ public:
+ explicit RTCSctpTransport(nsPIDOMWindowInner* aWindow,
+ RTCDtlsTransport& aDtlsTransport,
+ double aMaxMessageSize,
+ const Nullable<uint16_t>& aMaxChannels);
+
+ // nsISupports
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(RTCSctpTransport,
+ DOMEventTargetHelper)
+
+ // webidl
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+ IMPL_EVENT_HANDLER(statechange)
+
+ RTCDtlsTransport* Transport() const { return mDtlsTransport; }
+ RTCSctpTransportState State() const { return mState; }
+ double MaxMessageSize() const { return mMaxMessageSize; }
+ Nullable<uint16_t> GetMaxChannels() const { return mMaxChannels; }
+
+ void SetTransport(RTCDtlsTransport& aTransport) {
+ mDtlsTransport = &aTransport;
+ }
+
+ void SetMaxMessageSize(double aMaxMessageSize) {
+ mMaxMessageSize = aMaxMessageSize;
+ }
+
+ void SetMaxChannels(const Nullable<uint16_t>& aMaxChannels) {
+ mMaxChannels = aMaxChannels;
+ }
+
+ void UpdateState(RTCSctpTransportState aState);
+
+ private:
+ virtual ~RTCSctpTransport() = default;
+
+ RTCSctpTransportState mState;
+ RefPtr<RTCDtlsTransport> mDtlsTransport;
+ double mMaxMessageSize;
+ Nullable<uint16_t> mMaxChannels;
+};
+
+} // namespace mozilla::dom
+#endif // _RTCSctpTransport_h_
diff --git a/dom/media/webrtc/jsapi/RTCStatsIdGenerator.cpp b/dom/media/webrtc/jsapi/RTCStatsIdGenerator.cpp
new file mode 100644
index 0000000000..8b0462e223
--- /dev/null
+++ b/dom/media/webrtc/jsapi/RTCStatsIdGenerator.cpp
@@ -0,0 +1,90 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+
+#include "RTCStatsIdGenerator.h"
+
+#include <iostream>
+
+#include "mozilla/RandomNum.h"
+#include "RTCStatsReport.h"
+#include "WebrtcGlobal.h"
+
+namespace mozilla {
+
+RTCStatsIdGenerator::RTCStatsIdGenerator()
+ : mSalt(RandomUint64().valueOr(0xa5a5a5a5)), mCounter(0) {}
+
+void RTCStatsIdGenerator::RewriteIds(
+ nsTArray<UniquePtr<dom::RTCStatsCollection>> aFromStats,
+ dom::RTCStatsCollection* aIntoReport) {
+ // Rewrite an Optional id
+ auto rewriteId = [&](dom::Optional<nsString>& id) {
+ if (id.WasPassed()) {
+ id.Value() = Id(id.Value());
+ }
+ };
+
+ auto rewriteIds = [&](auto& aList, auto... aParam) {
+ for (auto& stat : aList) {
+ (rewriteId(stat.*aParam), ...);
+ }
+ };
+
+ // Involves a lot of copying, since webidl dictionaries don't have
+ // move semantics. Oh well.
+
+ // Create a temporary to avoid double-rewriting any stats already in
+ // aIntoReport.
+ auto stats = MakeUnique<dom::RTCStatsCollection>();
+ dom::FlattenStats(std::move(aFromStats), stats.get());
+
+ using S = dom::RTCStats;
+ using ICPS = dom::RTCIceCandidatePairStats;
+ using RSS = dom::RTCRtpStreamStats;
+ using IRSS = dom::RTCInboundRtpStreamStats;
+ using ORSS = dom::RTCOutboundRtpStreamStats;
+ using RIRSS = dom::RTCRemoteInboundRtpStreamStats;
+ using RORSS = dom::RTCRemoteOutboundRtpStreamStats;
+
+ rewriteIds(stats->mIceCandidatePairStats, &S::mId, &ICPS::mLocalCandidateId,
+ &ICPS::mRemoteCandidateId);
+ rewriteIds(stats->mIceCandidateStats, &S::mId);
+ rewriteIds(stats->mInboundRtpStreamStats, &S::mId, &IRSS::mRemoteId,
+ &RSS::mCodecId);
+ rewriteIds(stats->mOutboundRtpStreamStats, &S::mId, &ORSS::mRemoteId,
+ &RSS::mCodecId);
+ rewriteIds(stats->mRemoteInboundRtpStreamStats, &S::mId, &RIRSS::mLocalId,
+ &RSS::mCodecId);
+ rewriteIds(stats->mRemoteOutboundRtpStreamStats, &S::mId, &RORSS::mLocalId,
+ &RSS::mCodecId);
+ rewriteIds(stats->mCodecStats, &S::mId);
+ rewriteIds(stats->mRtpContributingSourceStats, &S::mId);
+ rewriteIds(stats->mTrickledIceCandidateStats, &S::mId);
+ rewriteIds(stats->mDataChannelStats, &S::mId);
+
+ dom::MergeStats(std::move(stats), aIntoReport);
+}
+
+nsString RTCStatsIdGenerator::Id(const nsString& aKey) {
+ if (!aKey.Length()) {
+ MOZ_ASSERT(aKey.Length(), "Stats IDs should never be empty.");
+ return aKey;
+ }
+ if (mAllocated.find(aKey) == mAllocated.end()) {
+ mAllocated[aKey] = Generate();
+ }
+ return mAllocated[aKey];
+}
+
+nsString RTCStatsIdGenerator::Generate() {
+ auto random = RandomUint64().valueOr(0x1a22);
+ auto idNum = static_cast<uint32_t>(mSalt ^ ((mCounter++ << 16) | random));
+ nsString id;
+ id.AppendInt(idNum, 16); // Append as hex
+ return id;
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/jsapi/RTCStatsIdGenerator.h b/dom/media/webrtc/jsapi/RTCStatsIdGenerator.h
new file mode 100644
index 0000000000..1391c029ad
--- /dev/null
+++ b/dom/media/webrtc/jsapi/RTCStatsIdGenerator.h
@@ -0,0 +1,42 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+
+#ifndef _RTCSTATSIDGENERATOR_H_
+#define _RTCSTATSIDGENERATOR_H_
+
+#include <map>
+
+#include "mozilla/Atomics.h"
+#include "mozilla/UniquePtr.h"
+#include "nsISupportsImpl.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+namespace dom {
+struct RTCStatsCollection;
+} // namespace dom
+
+class RTCStatsIdGenerator {
+ public:
+ RTCStatsIdGenerator();
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RTCStatsIdGenerator);
+
+ void RewriteIds(nsTArray<UniquePtr<dom::RTCStatsCollection>> aFromStats,
+ dom::RTCStatsCollection* aIntoReport);
+
+ private:
+ virtual ~RTCStatsIdGenerator(){};
+ nsString Id(const nsString& aKey);
+ nsString Generate();
+
+ const uint64_t mSalt;
+ uint64_t mCounter;
+ std::map<nsString, nsString> mAllocated;
+};
+
+} // namespace mozilla
+#endif
diff --git a/dom/media/webrtc/jsapi/RTCStatsReport.cpp b/dom/media/webrtc/jsapi/RTCStatsReport.cpp
new file mode 100644
index 0000000000..9f39eb3865
--- /dev/null
+++ b/dom/media/webrtc/jsapi/RTCStatsReport.cpp
@@ -0,0 +1,213 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#include "RTCStatsReport.h"
+#include "libwebrtcglue/SystemTime.h"
+#include "mozilla/dom/Performance.h"
+#include "nsRFPService.h"
+#include "WebrtcGlobal.h"
+
+namespace mozilla::dom {
+
+RTCStatsTimestampState::RTCStatsTimestampState()
+ : mRandomTimelineSeed(0),
+ mStartDomRealtime(WebrtcSystemTimeBase()),
+ mStartRealtime(
+ WebrtcSystemTime() -
+ webrtc::TimeDelta::Micros(
+ (TimeStamp::Now() - mStartDomRealtime).ToMicroseconds())),
+ mRTPCallerType(RTPCallerType::Normal),
+ mStartWallClockRaw(
+ PerformanceService::GetOrCreate()->TimeOrigin(mStartDomRealtime)) {}
+
+RTCStatsTimestampState::RTCStatsTimestampState(Performance& aPerformance)
+ : mRandomTimelineSeed(aPerformance.GetRandomTimelineSeed()),
+ mStartDomRealtime(aPerformance.CreationTimeStamp()),
+ mStartRealtime(
+ WebrtcSystemTime() -
+ webrtc::TimeDelta::Micros(
+ (TimeStamp::Now() - mStartDomRealtime).ToMicroseconds())),
+ mRTPCallerType(aPerformance.GetRTPCallerType()),
+ mStartWallClockRaw(
+ PerformanceService::GetOrCreate()->TimeOrigin(mStartDomRealtime)) {}
+
+TimeStamp RTCStatsTimestamp::ToMozTime() const { return mMozTime; }
+
+webrtc::Timestamp RTCStatsTimestamp::ToRealtime() const {
+ return ToDomRealtime() +
+ webrtc::TimeDelta::Micros(mState.mStartRealtime.us());
+}
+
+webrtc::Timestamp RTCStatsTimestamp::To1Jan1970() const {
+ return ToDomRealtime() + webrtc::TimeDelta::Millis(mState.mStartWallClockRaw);
+}
+
+webrtc::Timestamp RTCStatsTimestamp::ToNtp() const {
+ return To1Jan1970() + webrtc::TimeDelta::Seconds(webrtc::kNtpJan1970);
+}
+
+webrtc::Timestamp RTCStatsTimestamp::ToDomRealtime() const {
+ return webrtc::Timestamp::Micros(
+ (mMozTime - mState.mStartDomRealtime).ToMicroseconds());
+}
+
+DOMHighResTimeStamp RTCStatsTimestamp::ToDom() const {
+ // webrtc-pc says to use performance.timeOrigin + performance.now(), but
+ // keeping a Performance object around is difficult because it is
+ // main-thread-only. So, we perform the same calculation here. Note that this
+ // can be very different from the current wall-clock time because of changes
+ // to the wall clock, or monotonic clock drift over long periods of time.
+ // We are very careful to do exactly what Performance does, to avoid timestamp
+ // discrepancies.
+
+ DOMHighResTimeStamp realtime = ToDomRealtime().ms<double>();
+ // mRandomTimelineSeed is not set in the unit-tests.
+ if (mState.mRandomTimelineSeed) {
+ realtime = nsRFPService::ReduceTimePrecisionAsMSecs(
+ realtime, mState.mRandomTimelineSeed, mState.mRTPCallerType);
+ }
+
+ // Ugh. Performance::TimeOrigin is not constant, which means we need to
+ // emulate this weird behavior so our time stamps are consistent with JS
+ // timeOrigin. This is based on the code here:
+ // https://searchfox.org/mozilla-central/rev/
+ // 053826b10f838f77c27507e5efecc96e34718541/dom/performance/Performance.cpp#111-117
+ DOMHighResTimeStamp start = nsRFPService::ReduceTimePrecisionAsMSecs(
+ mState.mStartWallClockRaw, 0, mState.mRTPCallerType);
+
+ return start + realtime;
+}
+
+/* static */ RTCStatsTimestamp RTCStatsTimestamp::FromMozTime(
+ const RTCStatsTimestampMaker& aMaker, TimeStamp aMozTime) {
+ return RTCStatsTimestamp(aMaker.mState, aMozTime);
+}
+
+/* static */ RTCStatsTimestamp RTCStatsTimestamp::FromRealtime(
+ const RTCStatsTimestampMaker& aMaker, webrtc::Timestamp aRealtime) {
+ return FromDomRealtime(
+ aMaker,
+ aRealtime - webrtc::TimeDelta::Micros(aMaker.mState.mStartRealtime.us()));
+}
+
+/* static */ RTCStatsTimestamp RTCStatsTimestamp::From1Jan1970(
+ const RTCStatsTimestampMaker& aMaker, webrtc::Timestamp a1Jan1970) {
+ const auto& state = aMaker.mState;
+ return FromDomRealtime(
+ aMaker, a1Jan1970 - webrtc::TimeDelta::Millis(state.mStartWallClockRaw));
+}
+
+/* static */ RTCStatsTimestamp RTCStatsTimestamp::FromNtp(
+ const RTCStatsTimestampMaker& aMaker, webrtc::Timestamp aNtpTime) {
+ const auto& state = aMaker.mState;
+ const auto domRealtime = aNtpTime -
+ webrtc::TimeDelta::Seconds(webrtc::kNtpJan1970) -
+ webrtc::TimeDelta::Millis(state.mStartWallClockRaw);
+ // Ntp times exposed by libwebrtc to stats are always **rounded** to
+ // milliseconds. That means they can jump up to half a millisecond into the
+ // future. We compensate for that here so that things seem consistent to js.
+ return FromDomRealtime(aMaker, domRealtime - webrtc::TimeDelta::Micros(500));
+}
+
+/* static */ RTCStatsTimestamp RTCStatsTimestamp::FromDomRealtime(
+ const RTCStatsTimestampMaker& aMaker, webrtc::Timestamp aDomRealtime) {
+ return RTCStatsTimestamp(aMaker.mState, aMaker.mState.mStartDomRealtime +
+ TimeDuration::FromMicroseconds(
+ aDomRealtime.us<double>()));
+}
+
+RTCStatsTimestamp::RTCStatsTimestamp(RTCStatsTimestampState aState,
+ TimeStamp aMozTime)
+ : mState(aState), mMozTime(aMozTime) {}
+
+RTCStatsTimestampMaker::RTCStatsTimestampMaker(RTCStatsTimestampState aState)
+ : mState(aState) {}
+
+/* static */
+RTCStatsTimestampMaker RTCStatsTimestampMaker::Create(
+ nsPIDOMWindowInner* aWindow /* = nullptr */) {
+ if (!aWindow) {
+ return RTCStatsTimestampMaker(RTCStatsTimestampState());
+ }
+ if (Performance* p = aWindow->GetPerformance()) {
+ return RTCStatsTimestampMaker(RTCStatsTimestampState(*p));
+ }
+ return RTCStatsTimestampMaker(RTCStatsTimestampState());
+}
+
+RTCStatsTimestamp RTCStatsTimestampMaker::GetNow() const {
+ return RTCStatsTimestamp::FromMozTime(*this, TimeStamp::Now());
+}
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(RTCStatsReport, mParent)
+
+RTCStatsReport::RTCStatsReport(nsPIDOMWindowInner* aParent)
+ : mParent(aParent) {}
+
+/*static*/
+already_AddRefed<RTCStatsReport> RTCStatsReport::Constructor(
+ const GlobalObject& aGlobal) {
+ nsCOMPtr<nsPIDOMWindowInner> window(
+ do_QueryInterface(aGlobal.GetAsSupports()));
+ RefPtr<RTCStatsReport> report(new RTCStatsReport(window));
+ return report.forget();
+}
+
+JSObject* RTCStatsReport::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return RTCStatsReport_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void RTCStatsReport::Incorporate(RTCStatsCollection& aStats) {
+ ForAllPublicRTCStatsCollectionMembers(
+ aStats, [&](auto... aMember) { (SetRTCStats(aMember), ...); });
+}
+
+void RTCStatsReport::Set(const nsAString& aKey, JS::Handle<JSObject*> aValue,
+ ErrorResult& aRv) {
+ RTCStatsReport_Binding::MaplikeHelpers::Set(this, aKey, aValue, aRv);
+}
+
+namespace {
+template <size_t I, typename... Ts>
+bool MoveInto(std::tuple<Ts...>& aFrom, std::tuple<Ts*...>& aInto) {
+ return std::get<I>(aInto)->AppendElements(std::move(std::get<I>(aFrom)),
+ fallible);
+}
+
+template <size_t... Is, typename... Ts>
+bool MoveInto(std::tuple<Ts...>&& aFrom, std::tuple<Ts*...>& aInto,
+ std::index_sequence<Is...>) {
+ return (... && MoveInto<Is>(aFrom, aInto));
+}
+
+template <typename... Ts>
+bool MoveInto(std::tuple<Ts...>&& aFrom, std::tuple<Ts*...>& aInto) {
+ return MoveInto(std::move(aFrom), aInto, std::index_sequence_for<Ts...>());
+}
+} // namespace
+
+void MergeStats(UniquePtr<RTCStatsCollection> aFromStats,
+ RTCStatsCollection* aIntoStats) {
+ auto fromTuple = ForAllRTCStatsCollectionMembers(
+ *aFromStats,
+ [&](auto&... aMember) { return std::make_tuple(std::move(aMember)...); });
+ auto intoTuple = ForAllRTCStatsCollectionMembers(
+ *aIntoStats,
+ [&](auto&... aMember) { return std::make_tuple(&aMember...); });
+ if (!MoveInto(std::move(fromTuple), intoTuple)) {
+ mozalloc_handle_oom(0);
+ }
+}
+
+void FlattenStats(nsTArray<UniquePtr<RTCStatsCollection>> aFromStats,
+ RTCStatsCollection* aIntoStats) {
+ for (auto& stats : aFromStats) {
+ MergeStats(std::move(stats), aIntoStats);
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webrtc/jsapi/RTCStatsReport.h b/dom/media/webrtc/jsapi/RTCStatsReport.h
new file mode 100644
index 0000000000..97bf3daa52
--- /dev/null
+++ b/dom/media/webrtc/jsapi/RTCStatsReport.h
@@ -0,0 +1,205 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#ifndef RTCStatsReport_h_
+#define RTCStatsReport_h_
+
+#include "api/units/timestamp.h" // webrtc::Timestamp
+#include "js/RootingAPI.h" // JS::Rooted
+#include "js/Value.h"
+#include "mozilla/dom/AutoEntryScript.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/dom/PerformanceService.h"
+#include "mozilla/dom/RTCStatsReportBinding.h" // RTCStatsCollection
+#include "mozilla/dom/ToJSValue.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/UniquePtr.h"
+#include "nsCOMPtr.h"
+#include "nsIGlobalObject.h"
+#include "nsPIDOMWindow.h" // nsPIDOMWindowInner
+#include "nsContentUtils.h"
+#include "nsWrapperCache.h"
+#include "prtime.h" // PR_Now
+
+namespace mozilla {
+
+extern TimeStamp WebrtcSystemTimeBase();
+
+namespace dom {
+
+/**
+ * Keeps the state needed to convert RTCStatsTimestamps.
+ */
+struct RTCStatsTimestampState {
+ RTCStatsTimestampState();
+ explicit RTCStatsTimestampState(Performance& aPerformance);
+
+ RTCStatsTimestampState(const RTCStatsTimestampState&) = default;
+
+ // These members are sampled when a non-copy constructor is called.
+
+ // Performance's random timeline seed.
+ const uint64_t mRandomTimelineSeed;
+ // TimeStamp::Now() when the members were sampled. This is equivalent to time
+ // 0 in DomRealtime.
+ const TimeStamp mStartDomRealtime;
+ // WebrtcSystemTime() when the members were sampled. This represents the same
+ // point in time as mStartDomRealtime, but as a webrtc timestamp.
+ const webrtc::Timestamp mStartRealtime;
+ // Performance's RTPCallerType.
+ const RTPCallerType mRTPCallerType;
+ // Performance.timeOrigin for mStartDomRealtime when the members were sampled.
+ const DOMHighResTimeStamp mStartWallClockRaw;
+};
+
+/**
+ * Classes that facilitate creating timestamps for webrtc stats by mimicking
+ * dom::Performance, as well as getting and converting timestamps for libwebrtc
+ * and our integration with it.
+ *
+ * They use the same clock to avoid drift and inconsistencies, base on
+ * mozilla::TimeStamp, and convert to and from these time bases:
+ * - Moz : Monotonic, unspecified (but constant) and inaccessible epoch,
+ * as implemented by mozilla::TimeStamp
+ * - Realtime : Monotonic, unspecified (but constant) epoch.
+ * - 1Jan1970 : Monotonic, unix epoch (00:00:00 UTC on 1 January 1970).
+ * - Ntp : Monotonic, ntp epoch (00:00:00 UTC on 1 January 1900).
+ * - Dom : Monotonic, milliseconds since unix epoch, as the timestamps
+ * defined by webrtc-pc. Corresponds to Performance.timeOrigin +
+ * Performance.now(). Has reduced precision.
+ * - DomRealtime: Like Dom, but with full precision.
+ * - WallClock : Non-monotonic, unix epoch. Not used here since it is
+ * non-monotonic and cannot be correlated to the other time
+ * bases.
+ */
+class RTCStatsTimestampMaker;
+class RTCStatsTimestamp {
+ public:
+ TimeStamp ToMozTime() const;
+ webrtc::Timestamp ToRealtime() const;
+ webrtc::Timestamp To1Jan1970() const;
+ webrtc::Timestamp ToNtp() const;
+ webrtc::Timestamp ToDomRealtime() const;
+ DOMHighResTimeStamp ToDom() const;
+
+ static RTCStatsTimestamp FromMozTime(const RTCStatsTimestampMaker& aMaker,
+ TimeStamp aMozTime);
+ static RTCStatsTimestamp FromRealtime(const RTCStatsTimestampMaker& aMaker,
+ webrtc::Timestamp aRealtime);
+ static RTCStatsTimestamp From1Jan1970(const RTCStatsTimestampMaker& aMaker,
+ webrtc::Timestamp aRealtime);
+ static RTCStatsTimestamp FromNtp(const RTCStatsTimestampMaker& aMaker,
+ webrtc::Timestamp aRealtime);
+ static RTCStatsTimestamp FromDomRealtime(const RTCStatsTimestampMaker& aMaker,
+ webrtc::Timestamp aDomRealtime);
+ // There is on purpose no conversion functions from DOMHighResTimeStamp
+ // because of the loss in precision of a floating point to integer conversion.
+
+ private:
+ RTCStatsTimestamp(RTCStatsTimestampState aState, TimeStamp aMozTime);
+
+ const RTCStatsTimestampState mState;
+ const TimeStamp mMozTime;
+};
+
+class RTCStatsTimestampMaker {
+ public:
+ static RTCStatsTimestampMaker Create(nsPIDOMWindowInner* aWindow = nullptr);
+
+ RTCStatsTimestamp GetNow() const;
+
+ const RTCStatsTimestampState mState;
+
+ private:
+ explicit RTCStatsTimestampMaker(RTCStatsTimestampState aState);
+};
+
+// TODO(bug 1588303): If we ever get move semantics for webidl dictionaries, we
+// can stop wrapping these in UniquePtr, which will allow us to simplify code
+// in several places.
+typedef MozPromise<UniquePtr<RTCStatsCollection>, nsresult, true>
+ RTCStatsPromise;
+
+typedef MozPromise<UniquePtr<RTCStatsReportInternal>, nsresult, true>
+ RTCStatsReportPromise;
+
+class RTCStatsReport final : public nsWrapperCache {
+ public:
+ NS_INLINE_DECL_CYCLE_COLLECTING_NATIVE_REFCOUNTING(RTCStatsReport)
+ NS_DECL_CYCLE_COLLECTION_NATIVE_WRAPPERCACHE_CLASS(RTCStatsReport)
+
+ explicit RTCStatsReport(nsPIDOMWindowInner* aParent);
+
+ // TODO(bug 1586109): Remove this once we no longer have to create empty
+ // RTCStatsReports from JS.
+ static already_AddRefed<RTCStatsReport> Constructor(
+ const GlobalObject& aGlobal);
+
+ void Incorporate(RTCStatsCollection& aStats);
+
+ nsPIDOMWindowInner* GetParentObject() const { return mParent; }
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ private:
+ ~RTCStatsReport() = default;
+ void Set(const nsAString& aKey, JS::Handle<JSObject*> aValue,
+ ErrorResult& aRv);
+
+ template <typename T>
+ nsresult SetRTCStats(Sequence<T>& aValues) {
+ for (T& value : aValues) {
+ nsresult rv = SetRTCStats(value);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+ return NS_OK;
+ }
+
+ // We cannot just declare this as SetRTCStats(RTCStats&), because the
+ // conversion function that ToJSValue uses is non-virtual.
+ template <typename T>
+ nsresult SetRTCStats(T& aValue) {
+ static_assert(std::is_base_of<RTCStats, T>::value,
+ "SetRTCStats is for setting RTCStats only");
+
+ if (!aValue.mId.WasPassed()) {
+ return NS_OK;
+ }
+
+ const nsString key(aValue.mId.Value());
+
+ // Cargo-culted from dom::Promise; converts aValue to a JSObject
+ AutoEntryScript aes(mParent->AsGlobal()->GetGlobalJSObject(),
+ "RTCStatsReport::SetRTCStats");
+ JSContext* cx = aes.cx();
+ JS::Rooted<JS::Value> val(cx);
+ if (!ToJSValue(cx, std::forward<T>(aValue), &val)) {
+ return NS_ERROR_FAILURE;
+ }
+ JS::Rooted<JSObject*> jsObject(cx, &val.toObject());
+
+ ErrorResult rv;
+ Set(key, jsObject, rv);
+ return rv.StealNSResult();
+ }
+
+ nsCOMPtr<nsPIDOMWindowInner> mParent;
+};
+
+void MergeStats(UniquePtr<dom::RTCStatsCollection> aFromStats,
+ dom::RTCStatsCollection* aIntoStats);
+
+void FlattenStats(nsTArray<UniquePtr<dom::RTCStatsCollection>> aFromStats,
+ dom::RTCStatsCollection* aIntoStats);
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // RTCStatsReport_h_
diff --git a/dom/media/webrtc/jsapi/RemoteTrackSource.cpp b/dom/media/webrtc/jsapi/RemoteTrackSource.cpp
new file mode 100644
index 0000000000..41c679e431
--- /dev/null
+++ b/dom/media/webrtc/jsapi/RemoteTrackSource.cpp
@@ -0,0 +1,73 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "RemoteTrackSource.h"
+
+#include "MediaStreamError.h"
+#include "MediaTrackGraph.h"
+#include "RTCRtpReceiver.h"
+
+namespace mozilla {
+
+NS_IMPL_ADDREF_INHERITED(RemoteTrackSource, dom::MediaStreamTrackSource)
+NS_IMPL_RELEASE_INHERITED(RemoteTrackSource, dom::MediaStreamTrackSource)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(RemoteTrackSource)
+NS_INTERFACE_MAP_END_INHERITING(dom::MediaStreamTrackSource)
+NS_IMPL_CYCLE_COLLECTION_CLASS(RemoteTrackSource)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(RemoteTrackSource,
+ dom::MediaStreamTrackSource)
+ tmp->Destroy();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mReceiver)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(RemoteTrackSource,
+ dom::MediaStreamTrackSource)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mReceiver)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+RemoteTrackSource::RemoteTrackSource(SourceMediaTrack* aStream,
+ dom::RTCRtpReceiver* aReceiver,
+ nsIPrincipal* aPrincipal,
+ const nsString& aLabel,
+ TrackingId aTrackingId)
+ : dom::MediaStreamTrackSource(aPrincipal, aLabel, std::move(aTrackingId)),
+ mStream(aStream),
+ mReceiver(aReceiver) {}
+
+RemoteTrackSource::~RemoteTrackSource() { Destroy(); }
+
+void RemoteTrackSource::Destroy() {
+ if (mStream) {
+ MOZ_ASSERT(!mStream->IsDestroyed());
+ mStream->End();
+ mStream->Destroy();
+ mStream = nullptr;
+
+ GetMainThreadSerialEventTarget()->Dispatch(NewRunnableMethod(
+ "RemoteTrackSource::ForceEnded", this, &RemoteTrackSource::ForceEnded));
+ }
+}
+
+auto RemoteTrackSource::ApplyConstraints(
+ const dom::MediaTrackConstraints& aConstraints, dom::CallerType aCallerType)
+ -> RefPtr<ApplyConstraintsPromise> {
+ return ApplyConstraintsPromise::CreateAndReject(
+ MakeRefPtr<MediaMgrError>(
+ dom::MediaStreamError::Name::OverconstrainedError, ""),
+ __func__);
+}
+
+void RemoteTrackSource::SetPrincipal(nsIPrincipal* aPrincipal) {
+ mPrincipal = aPrincipal;
+ PrincipalChanged();
+}
+
+void RemoteTrackSource::SetMuted(bool aMuted) { MutedChanged(aMuted); }
+
+void RemoteTrackSource::ForceEnded() { OverrideEnded(); }
+
+SourceMediaTrack* RemoteTrackSource::Stream() const { return mStream; }
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/jsapi/RemoteTrackSource.h b/dom/media/webrtc/jsapi/RemoteTrackSource.h
new file mode 100644
index 0000000000..be730f0030
--- /dev/null
+++ b/dom/media/webrtc/jsapi/RemoteTrackSource.h
@@ -0,0 +1,64 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_WEBRTC_JSAPI_REMOTETRACKSOURCE_H_
+#define DOM_MEDIA_WEBRTC_JSAPI_REMOTETRACKSOURCE_H_
+
+#include "MediaStreamTrack.h"
+
+namespace mozilla {
+
+namespace dom {
+class RTCRtpReceiver;
+}
+
+class SourceMediaTrack;
+
+class RemoteTrackSource : public dom::MediaStreamTrackSource {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(RemoteTrackSource,
+ dom::MediaStreamTrackSource)
+
+ RemoteTrackSource(SourceMediaTrack* aStream, dom::RTCRtpReceiver* aReceiver,
+ nsIPrincipal* aPrincipal, const nsString& aLabel,
+ TrackingId aTrackingId);
+
+ void Destroy() override;
+
+ dom::MediaSourceEnum GetMediaSource() const override {
+ return dom::MediaSourceEnum::Other;
+ }
+
+ RefPtr<ApplyConstraintsPromise> ApplyConstraints(
+ const dom::MediaTrackConstraints& aConstraints,
+ dom::CallerType aCallerType) override;
+
+ void Stop() override {
+ // XXX (Bug 1314270): Implement rejection logic if necessary when we have
+ // clarity in the spec.
+ }
+
+ void Disable() override {}
+
+ void Enable() override {}
+
+ void SetPrincipal(nsIPrincipal* aPrincipal);
+ void SetMuted(bool aMuted);
+ void ForceEnded();
+
+ SourceMediaTrack* Stream() const;
+
+ private:
+ virtual ~RemoteTrackSource();
+
+ RefPtr<SourceMediaTrack> mStream;
+ RefPtr<dom::RTCRtpReceiver> mReceiver;
+};
+
+} // namespace mozilla
+
+#endif // DOM_MEDIA_WEBRTC_JSAPI_REMOTETRACKSOURCE_H_
diff --git a/dom/media/webrtc/jsapi/WebrtcGlobalChild.h b/dom/media/webrtc/jsapi/WebrtcGlobalChild.h
new file mode 100644
index 0000000000..147b4b44f0
--- /dev/null
+++ b/dom/media/webrtc/jsapi/WebrtcGlobalChild.h
@@ -0,0 +1,42 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _WEBRTC_GLOBAL_CHILD_H_
+#define _WEBRTC_GLOBAL_CHILD_H_
+
+#include "mozilla/dom/PWebrtcGlobalChild.h"
+
+namespace mozilla::dom {
+
+class WebrtcGlobalChild : public PWebrtcGlobalChild {
+ friend class ContentChild;
+
+ bool mShutdown;
+
+ MOZ_IMPLICIT WebrtcGlobalChild();
+ virtual void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ virtual mozilla::ipc::IPCResult RecvGetStats(
+ const nsAString& aPcIdFilter, GetStatsResolver&& aResolve) override;
+ virtual mozilla::ipc::IPCResult RecvClearStats() override;
+ // MOZ_CAN_RUN_SCRIPT_BOUNDARY because we can't do MOZ_CAN_RUN_SCRIPT in
+ // ipdl-generated things yet.
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY
+ virtual mozilla::ipc::IPCResult RecvGetLog(
+ GetLogResolver&& aResolve) override;
+ virtual mozilla::ipc::IPCResult RecvClearLog() override;
+ virtual mozilla::ipc::IPCResult RecvSetAecLogging(
+ const bool& aEnable) override;
+ virtual mozilla::ipc::IPCResult RecvSetDebugMode(const int& aLevel) override;
+
+ static WebrtcGlobalChild* GetOrSet(const Maybe<WebrtcGlobalChild*>& aChild);
+
+ public:
+ virtual ~WebrtcGlobalChild();
+ static WebrtcGlobalChild* Get();
+};
+
+} // namespace mozilla::dom
+
+#endif // _WEBRTC_GLOBAL_CHILD_H_
diff --git a/dom/media/webrtc/jsapi/WebrtcGlobalInformation.cpp b/dom/media/webrtc/jsapi/WebrtcGlobalInformation.cpp
new file mode 100644
index 0000000000..7d0a9e64b1
--- /dev/null
+++ b/dom/media/webrtc/jsapi/WebrtcGlobalInformation.cpp
@@ -0,0 +1,829 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WebrtcGlobalInformation.h"
+#include "WebrtcGlobalStatsHistory.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/dom/PWebrtcGlobal.h"
+#include "mozilla/dom/PWebrtcGlobalChild.h"
+#include "mozilla/media/webrtc/WebrtcGlobal.h"
+#include "WebrtcGlobalChild.h"
+#include "WebrtcGlobalParent.h"
+
+#include <algorithm>
+#include <vector>
+#include <type_traits>
+
+#include "mozilla/dom/WebrtcGlobalInformationBinding.h"
+#include "mozilla/dom/RTCStatsReportBinding.h" // for RTCStatsReportInternal
+#include "mozilla/dom/ContentChild.h"
+
+#include "nsISupports.h"
+#include "nsITimer.h"
+#include "nsLiteralString.h"
+#include "nsNetCID.h" // NS_SOCKETTRANSPORTSERVICE_CONTRACTID
+#include "nsServiceManagerUtils.h" // do_GetService
+#include "nsXULAppAPI.h"
+#include "mozilla/ErrorResult.h"
+#include "nsProxyRelease.h" // nsMainThreadPtrHolder
+#include "mozilla/Telemetry.h"
+#include "mozilla/Unused.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/ClearOnShutdown.h"
+
+#include "common/browser_logging/WebRtcLog.h"
+#include "nsString.h"
+#include "transport/runnable_utils.h"
+#include "MediaTransportHandler.h"
+#include "PeerConnectionCtx.h"
+#include "PeerConnectionImpl.h"
+
+#ifdef XP_WIN
+# include <process.h>
+#endif
+
+namespace mozilla::dom {
+
+using StatsRequestCallback =
+ nsMainThreadPtrHandle<WebrtcGlobalStatisticsCallback>;
+
+using LogRequestCallback = nsMainThreadPtrHandle<WebrtcGlobalLoggingCallback>;
+
+class WebrtcContentParents {
+ public:
+ static WebrtcGlobalParent* Alloc();
+ static void Dealloc(WebrtcGlobalParent* aParent);
+ static bool Empty() { return sContentParents.empty(); }
+ static const std::vector<RefPtr<WebrtcGlobalParent>>& GetAll() {
+ return sContentParents;
+ }
+
+ WebrtcContentParents() = delete;
+ WebrtcContentParents(const WebrtcContentParents&) = delete;
+ WebrtcContentParents& operator=(const WebrtcContentParents&) = delete;
+
+ private:
+ static std::vector<RefPtr<WebrtcGlobalParent>> sContentParents;
+};
+
+std::vector<RefPtr<WebrtcGlobalParent>> WebrtcContentParents::sContentParents;
+
+WebrtcGlobalParent* WebrtcContentParents::Alloc() {
+ RefPtr<WebrtcGlobalParent> cp = new WebrtcGlobalParent;
+ sContentParents.push_back(cp);
+ return cp.get();
+}
+
+void WebrtcContentParents::Dealloc(WebrtcGlobalParent* aParent) {
+ if (aParent) {
+ aParent->mShutdown = true;
+ auto cp =
+ std::find(sContentParents.begin(), sContentParents.end(), aParent);
+ if (cp != sContentParents.end()) {
+ sContentParents.erase(cp);
+ }
+ }
+}
+
+static PeerConnectionCtx* GetPeerConnectionCtx() {
+ if (PeerConnectionCtx::isActive()) {
+ MOZ_ASSERT(PeerConnectionCtx::GetInstance());
+ return PeerConnectionCtx::GetInstance();
+ }
+ return nullptr;
+}
+
+static nsTArray<dom::RTCStatsReportInternal>& GetWebrtcGlobalStatsStash() {
+ static StaticAutoPtr<nsTArray<dom::RTCStatsReportInternal>> sStash;
+ if (!sStash) {
+ sStash = new nsTArray<dom::RTCStatsReportInternal>();
+ ClearOnShutdown(&sStash);
+ }
+ return *sStash;
+}
+
+static RefPtr<PWebrtcGlobalParent::GetStatsPromise>
+GetStatsPromiseForThisProcess(const nsAString& aPcIdFilter) {
+ nsTArray<RefPtr<dom::RTCStatsReportPromise>> promises;
+
+ std::set<nsString> pcids;
+ if (auto* ctx = GetPeerConnectionCtx()) {
+ // Grab stats for PCs that still exist
+ ctx->ForEachPeerConnection([&](PeerConnectionImpl* aPc) {
+ if (!aPcIdFilter.IsEmpty() &&
+ !aPcIdFilter.EqualsASCII(aPc->GetIdAsAscii().c_str())) {
+ return;
+ }
+ if (!aPc->IsClosed() || !aPc->LongTermStatsIsDisabled()) {
+ nsString id;
+ aPc->GetId(id);
+ pcids.insert(id);
+ promises.AppendElement(aPc->GetStats(nullptr, true));
+ }
+ });
+
+ // Grab previously stashed stats, if they aren't dupes, and ensure they
+ // are marked closed. (In a content process, this should already have
+ // happened, but in the parent process, the stash will contain the last
+ // observed stats from the content processes. From the perspective of the
+ // parent process, these are assumed closed unless we see new stats from the
+ // content process that say otherwise.)
+ for (auto& report : GetWebrtcGlobalStatsStash()) {
+ report.mClosed = true;
+ if ((aPcIdFilter.IsEmpty() || aPcIdFilter == report.mPcid) &&
+ !pcids.count(report.mPcid)) {
+ promises.AppendElement(dom::RTCStatsReportPromise::CreateAndResolve(
+ MakeUnique<dom::RTCStatsReportInternal>(report), __func__));
+ }
+ }
+ }
+
+ auto UnwrapUniquePtrs = [](dom::RTCStatsReportPromise::AllSettledPromiseType::
+ ResolveOrRejectValue&& aResult) {
+ nsTArray<dom::RTCStatsReportInternal> reports;
+ MOZ_RELEASE_ASSERT(aResult.IsResolve(), "AllSettled should never reject!");
+ for (auto& reportResult : aResult.ResolveValue()) {
+ if (reportResult.IsResolve()) {
+ reports.AppendElement(*reportResult.ResolveValue());
+ }
+ }
+ return PWebrtcGlobalParent::GetStatsPromise::CreateAndResolve(
+ std::move(reports), __func__);
+ };
+
+ return dom::RTCStatsReportPromise::AllSettled(
+ GetMainThreadSerialEventTarget(), promises)
+ ->Then(GetMainThreadSerialEventTarget(), __func__,
+ std::move(UnwrapUniquePtrs));
+}
+
+static std::map<int32_t, dom::Sequence<nsString>>& GetWebrtcGlobalLogStash() {
+ static StaticAutoPtr<std::map<int32_t, dom::Sequence<nsString>>> sStash;
+ if (!sStash) {
+ sStash = new std::map<int32_t, dom::Sequence<nsString>>();
+ ClearOnShutdown(&sStash);
+ }
+ return *sStash;
+}
+
+static void ClearLongTermStats() {
+ if (!NS_IsMainThread()) {
+ MOZ_ASSERT(NS_IsMainThread());
+ return;
+ }
+
+ GetWebrtcGlobalStatsStash().Clear();
+ if (XRE_IsParentProcess()) {
+ WebrtcGlobalStatsHistory::Clear();
+ }
+ if (auto* ctx = GetPeerConnectionCtx()) {
+ ctx->ClearClosedStats();
+ }
+}
+
+void WebrtcGlobalInformation::ClearAllStats(const GlobalObject& aGlobal) {
+ if (!NS_IsMainThread()) {
+ return;
+ }
+
+ // Chrome-only API
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ if (!WebrtcContentParents::Empty()) {
+ // Pass on the request to any content process based PeerConnections.
+ for (const auto& cp : WebrtcContentParents::GetAll()) {
+ Unused << cp->SendClearStats();
+ }
+ }
+
+ // Flush the history for the chrome process
+ ClearLongTermStats();
+}
+
+void WebrtcGlobalInformation::GetStatsHistoryPcIds(
+ const GlobalObject& aGlobal,
+ WebrtcGlobalStatisticsHistoryPcIdsCallback& aPcIdsCallback,
+ ErrorResult& aRv) {
+ if (!NS_IsMainThread()) {
+ aRv.Throw(NS_ERROR_NOT_SAME_THREAD);
+ return;
+ }
+
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ IgnoredErrorResult rv;
+ aPcIdsCallback.Call(WebrtcGlobalStatsHistory::PcIds(), rv);
+ aRv = NS_OK;
+}
+
+void WebrtcGlobalInformation::GetStatsHistorySince(
+ const GlobalObject& aGlobal,
+ WebrtcGlobalStatisticsHistoryCallback& aStatsCallback,
+ const nsAString& pcIdFilter, const Optional<DOMHighResTimeStamp>& aAfter,
+ const Optional<DOMHighResTimeStamp>& aSdpAfter, ErrorResult& aRv) {
+ if (!NS_IsMainThread()) {
+ aRv.Throw(NS_ERROR_NOT_SAME_THREAD);
+ return;
+ }
+
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ WebrtcGlobalStatisticsReport history;
+
+ auto statsAfter = aAfter.WasPassed() ? Some(aAfter.Value()) : Nothing();
+ auto sdpAfter = aSdpAfter.WasPassed() ? Some(aSdpAfter.Value()) : Nothing();
+
+ WebrtcGlobalStatsHistory::GetHistory(pcIdFilter).apply([&](auto& hist) {
+ if (!history.mReports.AppendElements(hist->Since(statsAfter), fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ if (!history.mSdpHistories.AppendElement(hist->SdpSince(sdpAfter),
+ fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ });
+
+ IgnoredErrorResult rv;
+ aStatsCallback.Call(history, rv);
+ aRv = NS_OK;
+}
+
+using StatsPromiseArray =
+ nsTArray<RefPtr<PWebrtcGlobalParent::GetStatsPromise>>;
+
+void WebrtcGlobalInformation::GatherHistory() {
+ const nsString emptyFilter;
+ if (!NS_IsMainThread()) {
+ MOZ_ASSERT(NS_IsMainThread());
+ return;
+ }
+
+ MOZ_ASSERT(XRE_IsParentProcess());
+ using StatsPromise = PWebrtcGlobalParent::GetStatsPromise;
+ auto resolveThenAppendStatsHistory = [](RefPtr<StatsPromise>&& promise) {
+ auto AppendStatsHistory = [](StatsPromise::ResolveOrRejectValue&& result) {
+ if (result.IsReject()) {
+ return;
+ }
+ for (const auto& report : result.ResolveValue()) {
+ WebrtcGlobalStatsHistory::Record(
+ MakeUnique<RTCStatsReportInternal>(report));
+ }
+ };
+ promise->Then(GetMainThreadSerialEventTarget(), __func__,
+ std::move(AppendStatsHistory));
+ };
+ for (const auto& cp : WebrtcContentParents::GetAll()) {
+ resolveThenAppendStatsHistory(cp->SendGetStats(emptyFilter));
+ }
+ resolveThenAppendStatsHistory(GetStatsPromiseForThisProcess(emptyFilter));
+}
+
+void WebrtcGlobalInformation::GetAllStats(
+ const GlobalObject& aGlobal, WebrtcGlobalStatisticsCallback& aStatsCallback,
+ const Optional<nsAString>& aPcIdFilter, ErrorResult& aRv) {
+ if (!NS_IsMainThread()) {
+ aRv.Throw(NS_ERROR_NOT_SAME_THREAD);
+ return;
+ }
+
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ StatsPromiseArray statsPromises;
+
+ nsString filter;
+ if (aPcIdFilter.WasPassed()) {
+ filter = aPcIdFilter.Value();
+ }
+
+ for (const auto& cp : WebrtcContentParents::GetAll()) {
+ statsPromises.AppendElement(cp->SendGetStats(filter));
+ }
+
+ // Stats from this (the parent) process. How long do we keep supporting this?
+ statsPromises.AppendElement(GetStatsPromiseForThisProcess(filter));
+
+ // CallbackObject does not support threadsafe refcounting, and must be
+ // used and destroyed on main.
+ StatsRequestCallback callbackHandle(
+ new nsMainThreadPtrHolder<WebrtcGlobalStatisticsCallback>(
+ "WebrtcGlobalStatisticsCallback", &aStatsCallback));
+
+ auto FlattenThenStashThenCallback =
+ [callbackHandle,
+ filter](PWebrtcGlobalParent::GetStatsPromise::AllSettledPromiseType::
+ ResolveOrRejectValue&& aResult) MOZ_CAN_RUN_SCRIPT_BOUNDARY {
+ std::set<nsString> pcids;
+ WebrtcGlobalStatisticsReport flattened;
+ MOZ_RELEASE_ASSERT(aResult.IsResolve(),
+ "AllSettled should never reject!");
+ // Flatten stats from content processes and parent process.
+ // The stats from the parent process (which will come last) might
+ // contain some stale content-process stats, so skip those.
+ for (auto& processResult : aResult.ResolveValue()) {
+ // TODO: Report rejection on individual content processes someday?
+ if (processResult.IsResolve()) {
+ for (auto& pcStats : processResult.ResolveValue()) {
+ if (!pcids.count(pcStats.mPcid)) {
+ pcids.insert(pcStats.mPcid);
+ if (!flattened.mReports.AppendElement(std::move(pcStats),
+ fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ }
+ }
+ }
+ }
+
+ if (filter.IsEmpty()) {
+ // Unfiltered is simple; the flattened result becomes the new stash.
+ GetWebrtcGlobalStatsStash() = flattened.mReports;
+ } else if (!flattened.mReports.IsEmpty()) {
+ // Update our stash with the single result.
+ MOZ_ASSERT(flattened.mReports.Length() == 1);
+ StashStats(flattened.mReports[0]);
+ }
+
+ IgnoredErrorResult rv;
+ callbackHandle->Call(flattened, rv);
+ };
+
+ PWebrtcGlobalParent::GetStatsPromise::AllSettled(
+ GetMainThreadSerialEventTarget(), statsPromises)
+ ->Then(GetMainThreadSerialEventTarget(), __func__,
+ std::move(FlattenThenStashThenCallback));
+
+ aRv = NS_OK;
+}
+
+static RefPtr<PWebrtcGlobalParent::GetLogPromise> GetLogPromise() {
+ PeerConnectionCtx* ctx = GetPeerConnectionCtx();
+ if (!ctx) {
+ // This process has never created a PeerConnection, so no ICE logging.
+ return PWebrtcGlobalParent::GetLogPromise::CreateAndResolve(
+ Sequence<nsString>(), __func__);
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsISerialEventTarget> stsThread =
+ do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv);
+
+ if (NS_WARN_IF(NS_FAILED(rv) || !stsThread)) {
+ return PWebrtcGlobalParent::GetLogPromise::CreateAndResolve(
+ Sequence<nsString>(), __func__);
+ }
+
+ RefPtr<MediaTransportHandler> transportHandler = ctx->GetTransportHandler();
+
+ auto AddMarkers =
+ [](MediaTransportHandler::IceLogPromise::ResolveOrRejectValue&& aValue) {
+ nsString pid;
+ pid.AppendInt(getpid());
+ Sequence<nsString> logs;
+ if (aValue.IsResolve() && !aValue.ResolveValue().IsEmpty()) {
+ bool ok = logs.AppendElement(
+ u"+++++++ BEGIN (process id "_ns + pid + u") ++++++++"_ns,
+ fallible);
+ ok &=
+ !!logs.AppendElements(std::move(aValue.ResolveValue()), fallible);
+ ok &= !!logs.AppendElement(
+ u"+++++++ END (process id "_ns + pid + u") ++++++++"_ns,
+ fallible);
+ if (!ok) {
+ mozalloc_handle_oom(0);
+ }
+ }
+ return PWebrtcGlobalParent::GetLogPromise::CreateAndResolve(
+ std::move(logs), __func__);
+ };
+
+ return transportHandler->GetIceLog(nsCString())
+ ->Then(GetMainThreadSerialEventTarget(), __func__, std::move(AddMarkers));
+}
+
+static nsresult RunLogClear() {
+ PeerConnectionCtx* ctx = GetPeerConnectionCtx();
+ if (!ctx) {
+ // This process has never created a PeerConnection, so no ICE logging.
+ return NS_OK;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsISerialEventTarget> stsThread =
+ do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (!stsThread) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<MediaTransportHandler> transportHandler = ctx->GetTransportHandler();
+
+ return RUN_ON_THREAD(
+ stsThread,
+ WrapRunnable(transportHandler, &MediaTransportHandler::ClearIceLog),
+ NS_DISPATCH_NORMAL);
+}
+
+void WebrtcGlobalInformation::ClearLogging(const GlobalObject& aGlobal) {
+ if (!NS_IsMainThread()) {
+ return;
+ }
+
+ // Chrome-only API
+ MOZ_ASSERT(XRE_IsParentProcess());
+ GetWebrtcGlobalLogStash().clear();
+
+ if (!WebrtcContentParents::Empty()) {
+ // Clear content process signaling logs
+ for (const auto& cp : WebrtcContentParents::GetAll()) {
+ Unused << cp->SendClearLog();
+ }
+ }
+
+ // Clear chrome process signaling logs
+ Unused << RunLogClear();
+}
+
+static RefPtr<GenericPromise> UpdateLogStash() {
+ nsTArray<RefPtr<GenericPromise>> logPromises;
+ MOZ_ASSERT(XRE_IsParentProcess());
+ for (const auto& cp : WebrtcContentParents::GetAll()) {
+ auto StashLog =
+ [id = cp->Id() * 2 /* Make sure 1 isn't used */](
+ PWebrtcGlobalParent::GetLogPromise::ResolveOrRejectValue&& aValue) {
+ if (aValue.IsResolve() && !aValue.ResolveValue().IsEmpty()) {
+ GetWebrtcGlobalLogStash()[id] = aValue.ResolveValue();
+ }
+ return GenericPromise::CreateAndResolve(true, __func__);
+ };
+ logPromises.AppendElement(cp->SendGetLog()->Then(
+ GetMainThreadSerialEventTarget(), __func__, std::move(StashLog)));
+ }
+
+ // Get ICE logging for this (the parent) process. How long do we support this?
+ logPromises.AppendElement(GetLogPromise()->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [](PWebrtcGlobalParent::GetLogPromise::ResolveOrRejectValue&& aValue) {
+ if (aValue.IsResolve()) {
+ GetWebrtcGlobalLogStash()[1] = aValue.ResolveValue();
+ }
+ return GenericPromise::CreateAndResolve(true, __func__);
+ }));
+
+ return GenericPromise::AllSettled(GetMainThreadSerialEventTarget(),
+ logPromises)
+ ->Then(GetMainThreadSerialEventTarget(), __func__,
+ [](GenericPromise::AllSettledPromiseType::ResolveOrRejectValue&&
+ aValue) {
+ // We don't care about the value, since we're just going to copy
+ // what is in the stash. This ignores failures too, which is what
+ // we want.
+ return GenericPromise::CreateAndResolve(true, __func__);
+ });
+}
+
+void WebrtcGlobalInformation::GetLogging(
+ const GlobalObject& aGlobal, const nsAString& aPattern,
+ WebrtcGlobalLoggingCallback& aLoggingCallback, ErrorResult& aRv) {
+ if (!NS_IsMainThread()) {
+ aRv.Throw(NS_ERROR_NOT_SAME_THREAD);
+ return;
+ }
+
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ nsAutoString pattern(aPattern);
+
+ // CallbackObject does not support threadsafe refcounting, and must be
+ // destroyed on main.
+ LogRequestCallback callbackHandle(
+ new nsMainThreadPtrHolder<WebrtcGlobalLoggingCallback>(
+ "WebrtcGlobalLoggingCallback", &aLoggingCallback));
+
+ auto FilterThenCallback =
+ [pattern, callbackHandle](GenericPromise::ResolveOrRejectValue&& aValue)
+ MOZ_CAN_RUN_SCRIPT_BOUNDARY {
+ dom::Sequence<nsString> flattened;
+ for (const auto& [id, log] : GetWebrtcGlobalLogStash()) {
+ (void)id;
+ for (const auto& line : log) {
+ if (pattern.IsEmpty() || (line.Find(pattern) != kNotFound)) {
+ if (!flattened.AppendElement(line, fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ }
+ }
+ }
+ IgnoredErrorResult rv;
+ callbackHandle->Call(flattened, rv);
+ };
+
+ UpdateLogStash()->Then(GetMainThreadSerialEventTarget(), __func__,
+ std::move(FilterThenCallback));
+ aRv = NS_OK;
+}
+
+static int32_t sLastSetLevel = 0;
+static bool sLastAECDebug = false;
+static Maybe<nsCString> sAecDebugLogDir;
+
+void WebrtcGlobalInformation::SetDebugLevel(const GlobalObject& aGlobal,
+ int32_t aLevel) {
+ if (aLevel) {
+ StartWebRtcLog(mozilla::LogLevel(aLevel));
+ } else {
+ StopWebRtcLog();
+ }
+ sLastSetLevel = aLevel;
+
+ for (const auto& cp : WebrtcContentParents::GetAll()) {
+ Unused << cp->SendSetDebugMode(aLevel);
+ }
+}
+
+int32_t WebrtcGlobalInformation::DebugLevel(const GlobalObject& aGlobal) {
+ return sLastSetLevel;
+}
+
+void WebrtcGlobalInformation::SetAecDebug(const GlobalObject& aGlobal,
+ bool aEnable) {
+ if (aEnable) {
+ sAecDebugLogDir = Some(StartAecLog());
+ } else {
+ StopAecLog();
+ }
+
+ sLastAECDebug = aEnable;
+
+ for (const auto& cp : WebrtcContentParents::GetAll()) {
+ Unused << cp->SendSetAecLogging(aEnable);
+ }
+}
+
+bool WebrtcGlobalInformation::AecDebug(const GlobalObject& aGlobal) {
+ return sLastAECDebug;
+}
+
+void WebrtcGlobalInformation::GetAecDebugLogDir(const GlobalObject& aGlobal,
+ nsAString& aDir) {
+ aDir = NS_ConvertASCIItoUTF16(sAecDebugLogDir.valueOr(""_ns));
+}
+
+/*static*/
+void WebrtcGlobalInformation::StashStats(
+ const dom::RTCStatsReportInternal& aReport) {
+ // Remove previous report, if present
+ // TODO: Make this a map instead of an array?
+ for (size_t i = 0; i < GetWebrtcGlobalStatsStash().Length();) {
+ auto& pcStats = GetWebrtcGlobalStatsStash()[i];
+ if (pcStats.mPcid == aReport.mPcid) {
+ GetWebrtcGlobalStatsStash().RemoveElementAt(i);
+ break;
+ }
+ ++i;
+ }
+ // Stash final stats
+ GetWebrtcGlobalStatsStash().AppendElement(aReport);
+}
+
+void WebrtcGlobalInformation::AdjustTimerReferences(
+ PcTrackingUpdate&& aUpdate) {
+ static StaticRefPtr<nsITimer> sHistoryTimer;
+ static StaticAutoPtr<nsTHashSet<nsString>> sPcids;
+
+ MOZ_ASSERT(NS_IsMainThread());
+
+ auto HandleAdd = [&](nsString&& aPcid, bool aIsLongTermStatsDisabled) {
+ if (!sPcids) {
+ sPcids = new nsTHashSet<nsString>();
+ ClearOnShutdown(&sPcids);
+ }
+ sPcids->EnsureInserted(aPcid);
+ // Reserve a stats history
+ WebrtcGlobalStatsHistory::InitHistory(nsString(aPcid),
+ aIsLongTermStatsDisabled);
+ if (!sHistoryTimer) {
+ sHistoryTimer = NS_NewTimer(GetMainThreadSerialEventTarget());
+ if (sHistoryTimer) {
+ sHistoryTimer->InitWithNamedFuncCallback(
+ [](nsITimer* aTimer, void* aClosure) {
+ if (WebrtcGlobalStatsHistory::Pref::Enabled()) {
+ WebrtcGlobalInformation::GatherHistory();
+ }
+ },
+ nullptr, WebrtcGlobalStatsHistory::Pref::PollIntervalMs(),
+ nsITimer::TYPE_REPEATING_SLACK,
+ "WebrtcGlobalInformation::GatherHistory");
+ }
+ ClearOnShutdown(&sHistoryTimer);
+ }
+ };
+
+ auto HandleRemove = [&](const nsString& aRemoved) {
+ WebrtcGlobalStatsHistory::CloseHistory(nsString(aRemoved));
+ if (!sPcids || !sPcids->Count()) {
+ return;
+ }
+ if (!sPcids->Contains(aRemoved)) {
+ return;
+ }
+ sPcids->Remove(aRemoved);
+ if (!sPcids->Count() && sHistoryTimer) {
+ sHistoryTimer->Cancel();
+ sHistoryTimer = nullptr;
+ }
+ };
+
+ switch (aUpdate.Type()) {
+ case PcTrackingUpdate::Type::Add: {
+ HandleAdd(std::move(aUpdate.mPcid),
+ aUpdate.mLongTermStatsDisabled.valueOrFrom([&]() {
+ MOZ_ASSERT(aUpdate.mLongTermStatsDisabled.isNothing());
+ return true;
+ }));
+ return;
+ }
+ case PcTrackingUpdate::Type::Remove: {
+ HandleRemove(aUpdate.mPcid);
+ return;
+ }
+ default: {
+ MOZ_ASSERT(false, "Invalid PcCount operation");
+ }
+ }
+}
+
+WebrtcGlobalParent* WebrtcGlobalParent::Alloc() {
+ return WebrtcContentParents::Alloc();
+}
+
+bool WebrtcGlobalParent::Dealloc(WebrtcGlobalParent* aActor) {
+ WebrtcContentParents::Dealloc(aActor);
+ return true;
+}
+
+void WebrtcGlobalParent::ActorDestroy(ActorDestroyReason aWhy) {
+ mShutdown = true;
+ for (const auto& pcId : mPcids) {
+ using Update = WebrtcGlobalInformation::PcTrackingUpdate;
+ auto update = Update::Remove(nsString(pcId));
+ WebrtcGlobalInformation::PeerConnectionTracking(update);
+ }
+}
+
+mozilla::ipc::IPCResult WebrtcGlobalParent::Recv__delete__() {
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebrtcGlobalParent::RecvPeerConnectionCreated(
+ const nsAString& aPcId, const bool& aIsLongTermStatsDisabled) {
+ if (mShutdown) {
+ return IPC_OK();
+ }
+ mPcids.EnsureInserted(aPcId);
+ using Update = WebrtcGlobalInformation::PcTrackingUpdate;
+ auto update = Update::Add(nsString(aPcId), aIsLongTermStatsDisabled);
+ WebrtcGlobalInformation::PeerConnectionTracking(update);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebrtcGlobalParent::RecvPeerConnectionDestroyed(
+ const nsAString& aPcId) {
+ mPcids.EnsureRemoved(aPcId);
+ using Update = WebrtcGlobalInformation::PcTrackingUpdate;
+ auto update = Update::Remove(nsString(aPcId));
+ WebrtcGlobalStatsHistory::CloseHistory(aPcId);
+ WebrtcGlobalInformation::PeerConnectionTracking(update);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebrtcGlobalParent::RecvPeerConnectionFinalStats(
+ const RTCStatsReportInternal& aFinalStats) {
+ auto finalStats = MakeUnique<RTCStatsReportInternal>(aFinalStats);
+ WebrtcGlobalStatsHistory::Record(std::move(finalStats));
+ WebrtcGlobalStatsHistory::CloseHistory(aFinalStats.mPcid);
+ return IPC_OK();
+}
+
+MOZ_IMPLICIT WebrtcGlobalParent::WebrtcGlobalParent() : mShutdown(false) {
+ MOZ_COUNT_CTOR(WebrtcGlobalParent);
+}
+
+MOZ_IMPLICIT WebrtcGlobalParent::~WebrtcGlobalParent() {
+ MOZ_COUNT_DTOR(WebrtcGlobalParent);
+}
+
+mozilla::ipc::IPCResult WebrtcGlobalChild::RecvGetStats(
+ const nsAString& aPcIdFilter, GetStatsResolver&& aResolve) {
+ if (!mShutdown) {
+ GetStatsPromiseForThisProcess(aPcIdFilter)
+ ->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [resolve = std::move(aResolve)](
+ nsTArray<dom::RTCStatsReportInternal>&& aReports) {
+ resolve(std::move(aReports));
+ },
+ []() { MOZ_CRASH(); });
+ return IPC_OK();
+ }
+
+ aResolve(nsTArray<RTCStatsReportInternal>());
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebrtcGlobalChild::RecvClearStats() {
+ if (mShutdown) {
+ return IPC_OK();
+ }
+
+ ClearLongTermStats();
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebrtcGlobalChild::RecvGetLog(
+ GetLogResolver&& aResolve) {
+ if (mShutdown) {
+ aResolve(Sequence<nsString>());
+ return IPC_OK();
+ }
+
+ GetLogPromise()->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [aResolve = std::move(aResolve)](
+ PWebrtcGlobalParent::GetLogPromise::ResolveOrRejectValue&& aValue) {
+ if (aValue.IsResolve()) {
+ aResolve(aValue.ResolveValue());
+ } else {
+ aResolve(Sequence<nsString>());
+ }
+ });
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebrtcGlobalChild::RecvClearLog() {
+ if (mShutdown) {
+ return IPC_OK();
+ }
+
+ RunLogClear();
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebrtcGlobalChild::RecvSetAecLogging(
+ const bool& aEnable) {
+ if (!mShutdown) {
+ if (aEnable) {
+ StartAecLog();
+ } else {
+ StopAecLog();
+ }
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebrtcGlobalChild::RecvSetDebugMode(const int& aLevel) {
+ if (!mShutdown) {
+ if (aLevel) {
+ StartWebRtcLog(mozilla::LogLevel(aLevel));
+ } else {
+ StopWebRtcLog();
+ }
+ }
+ return IPC_OK();
+}
+
+WebrtcGlobalChild* WebrtcGlobalChild::GetOrSet(
+ const Maybe<WebrtcGlobalChild*>& aChild) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(XRE_IsContentProcess());
+ static WebrtcGlobalChild* sChild;
+ if (!sChild && !aChild) {
+ sChild = static_cast<WebrtcGlobalChild*>(
+ ContentChild::GetSingleton()->SendPWebrtcGlobalConstructor());
+ }
+ aChild.apply([](auto* child) { sChild = child; });
+ return sChild;
+}
+
+WebrtcGlobalChild* WebrtcGlobalChild::Get() { return GetOrSet(Nothing()); }
+
+void WebrtcGlobalChild::ActorDestroy(ActorDestroyReason aWhy) {
+ mShutdown = true;
+}
+
+MOZ_IMPLICIT WebrtcGlobalChild::WebrtcGlobalChild() : mShutdown(false) {
+ MOZ_COUNT_CTOR(WebrtcGlobalChild);
+}
+
+MOZ_IMPLICIT WebrtcGlobalChild::~WebrtcGlobalChild() {
+ MOZ_COUNT_DTOR(WebrtcGlobalChild);
+ GetOrSet(Some(nullptr));
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webrtc/jsapi/WebrtcGlobalInformation.h b/dom/media/webrtc/jsapi/WebrtcGlobalInformation.h
new file mode 100644
index 0000000000..d43013bb43
--- /dev/null
+++ b/dom/media/webrtc/jsapi/WebrtcGlobalInformation.h
@@ -0,0 +1,102 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _WEBRTC_GLOBAL_INFORMATION_H_
+#define _WEBRTC_GLOBAL_INFORMATION_H_
+
+#include <tuple>
+#include "mozilla/dom/WebrtcGlobalInformationBinding.h"
+#include "nsString.h"
+#include "mozilla/dom/BindingDeclarations.h" // for Optional
+#include "nsDOMNavigationTiming.h"
+#include "WebrtcGlobalStatsHistory.h"
+
+namespace mozilla {
+class PeerConnectionImpl;
+class ErrorResult;
+
+namespace dom {
+
+class GlobalObject;
+class WebrtcGlobalStatisticsCallback;
+class WebrtcGlobalStatisticsHistoryPcIdsCallback;
+class WebrtcGlobalLoggingCallback;
+struct RTCStatsReportInternal;
+
+class WebrtcGlobalInformation {
+ public:
+ MOZ_CAN_RUN_SCRIPT
+ static void GetAllStats(const GlobalObject& aGlobal,
+ WebrtcGlobalStatisticsCallback& aStatsCallback,
+ const Optional<nsAString>& aPcIdFilter,
+ ErrorResult& aRv);
+
+ MOZ_CAN_RUN_SCRIPT
+ static void GetStatsHistoryPcIds(
+ const GlobalObject& aGlobal,
+ WebrtcGlobalStatisticsHistoryPcIdsCallback& aPcIdsCallback,
+ ErrorResult& aRv);
+
+ MOZ_CAN_RUN_SCRIPT
+ static void GetStatsHistorySince(
+ const GlobalObject& aGlobal,
+ WebrtcGlobalStatisticsHistoryCallback& aStatsCallback,
+ const nsAString& aPcIdFilter, const Optional<DOMHighResTimeStamp>& aAfter,
+ const Optional<DOMHighResTimeStamp>& aSdpAfter, ErrorResult& aRv);
+
+ static void GatherHistory();
+
+ static void ClearAllStats(const GlobalObject& aGlobal);
+
+ MOZ_CAN_RUN_SCRIPT
+ static void GetLogging(const GlobalObject& aGlobal, const nsAString& aPattern,
+ WebrtcGlobalLoggingCallback& aLoggingCallback,
+ ErrorResult& aRv);
+
+ static void ClearLogging(const GlobalObject& aGlobal);
+
+ static void SetDebugLevel(const GlobalObject& aGlobal, int32_t aLevel);
+ static int32_t DebugLevel(const GlobalObject& aGlobal);
+
+ static void SetAecDebug(const GlobalObject& aGlobal, bool aEnable);
+ static bool AecDebug(const GlobalObject& aGlobal);
+ static void GetAecDebugLogDir(const GlobalObject& aGlobal, nsAString& aDir);
+
+ static void StashStats(const RTCStatsReportInternal& aReport);
+
+ WebrtcGlobalInformation() = delete;
+ WebrtcGlobalInformation(const WebrtcGlobalInformation& aOrig) = delete;
+ WebrtcGlobalInformation& operator=(const WebrtcGlobalInformation& aRhs) =
+ delete;
+
+ struct PcTrackingUpdate {
+ static PcTrackingUpdate Add(const nsString& aPcid,
+ const bool& aLongTermStatsDisabled) {
+ return PcTrackingUpdate{aPcid, Some(aLongTermStatsDisabled)};
+ }
+ static PcTrackingUpdate Remove(const nsString& aPcid) {
+ return PcTrackingUpdate{aPcid, Nothing()};
+ }
+ nsString mPcid;
+ Maybe<bool> mLongTermStatsDisabled;
+ enum class Type {
+ Add,
+ Remove,
+ };
+ Type Type() const {
+ return mLongTermStatsDisabled ? Type::Add : Type::Remove;
+ }
+ };
+ static void PeerConnectionTracking(PcTrackingUpdate& aUpdate) {
+ AdjustTimerReferences(std::move(aUpdate));
+ }
+
+ private:
+ static void AdjustTimerReferences(PcTrackingUpdate&& aUpdate);
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // _WEBRTC_GLOBAL_INFORMATION_H_
diff --git a/dom/media/webrtc/jsapi/WebrtcGlobalParent.h b/dom/media/webrtc/jsapi/WebrtcGlobalParent.h
new file mode 100644
index 0000000000..8372c4182f
--- /dev/null
+++ b/dom/media/webrtc/jsapi/WebrtcGlobalParent.h
@@ -0,0 +1,52 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _WEBRTC_GLOBAL_PARENT_H_
+#define _WEBRTC_GLOBAL_PARENT_H_
+
+#include "mozilla/dom/PWebrtcGlobalParent.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "nsISupportsImpl.h"
+
+namespace mozilla::dom {
+
+class WebrtcParents;
+
+class WebrtcGlobalParent : public PWebrtcGlobalParent {
+ friend class ContentParent;
+ friend class WebrtcGlobalInformation;
+ friend class WebrtcContentParents;
+
+ bool mShutdown;
+ nsTHashSet<nsString> mPcids;
+
+ MOZ_IMPLICIT WebrtcGlobalParent();
+
+ static WebrtcGlobalParent* Alloc();
+ static bool Dealloc(WebrtcGlobalParent* aActor);
+
+ virtual void ActorDestroy(ActorDestroyReason aWhy) override;
+ virtual mozilla::ipc::IPCResult Recv__delete__() override;
+ // Notification that a PeerConnection exists, and stats polling can begin
+ // if it hasn't already begun due to a previously created PeerConnection.
+ virtual mozilla::ipc::IPCResult RecvPeerConnectionCreated(
+ const nsAString& aPcId, const bool& aIsLongTermStatsDisabled) override;
+ // Notification that a PeerConnection no longer exists, and stats polling
+ // can end if there are no other PeerConnections.
+ virtual mozilla::ipc::IPCResult RecvPeerConnectionDestroyed(
+ const nsAString& aPcid) override;
+ // Ditto but we have final stats
+ virtual mozilla::ipc::IPCResult RecvPeerConnectionFinalStats(
+ const RTCStatsReportInternal& aFinalStats) override;
+ virtual ~WebrtcGlobalParent();
+
+ public:
+ NS_INLINE_DECL_REFCOUNTING(WebrtcGlobalParent)
+
+ bool IsActive() { return !mShutdown; }
+};
+
+} // namespace mozilla::dom
+
+#endif // _WEBRTC_GLOBAL_PARENT_H_
diff --git a/dom/media/webrtc/jsapi/WebrtcGlobalStatsHistory.cpp b/dom/media/webrtc/jsapi/WebrtcGlobalStatsHistory.cpp
new file mode 100644
index 0000000000..1d576b5dca
--- /dev/null
+++ b/dom/media/webrtc/jsapi/WebrtcGlobalStatsHistory.cpp
@@ -0,0 +1,282 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WebrtcGlobalStatsHistory.h"
+#include <memory>
+
+#include "domstubs.h"
+#include "mozilla/LinkedList.h"
+#include "mozilla/dom/BindingUtils.h"
+#include "mozilla/dom/RTCStatsReportBinding.h" // for RTCStatsReportInternal
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/fallible.h"
+#include "mozilla/mozalloc_oom.h"
+#include "nsDOMNavigationTiming.h"
+
+namespace mozilla::dom {
+
+constexpr auto SEC_TO_MS(const DOMHighResTimeStamp sec) -> DOMHighResTimeStamp {
+ return sec * 1000.0;
+}
+
+constexpr auto MIN_TO_MS(const DOMHighResTimeStamp min) -> DOMHighResTimeStamp {
+ return SEC_TO_MS(min * 60.0);
+}
+
+// Prefs
+auto WebrtcGlobalStatsHistory::Pref::Enabled() -> bool {
+ return mozilla::StaticPrefs::media_aboutwebrtc_hist_enabled();
+}
+
+auto WebrtcGlobalStatsHistory::Pref::PollIntervalMs() -> uint32_t {
+ return mozilla::StaticPrefs::media_aboutwebrtc_hist_poll_interval_ms();
+}
+
+auto WebrtcGlobalStatsHistory::Pref::StorageWindowS() -> uint32_t {
+ return mozilla::StaticPrefs::media_aboutwebrtc_hist_storage_window_s();
+}
+
+auto WebrtcGlobalStatsHistory::Pref::PruneAfterM() -> uint32_t {
+ return mozilla::StaticPrefs::media_aboutwebrtc_hist_prune_after_m();
+}
+
+auto WebrtcGlobalStatsHistory::Pref::ClosedStatsToRetain() -> uint32_t {
+ return mozilla::StaticPrefs::media_aboutwebrtc_hist_closed_stats_to_retain();
+}
+
+auto WebrtcGlobalStatsHistory::Get() -> WebrtcGlobalStatsHistory::StatsMap& {
+ static StaticAutoPtr<StatsMap> sHist;
+ if (!sHist) {
+ sHist = new StatsMap();
+ ClearOnShutdown(&sHist);
+ }
+ return *sHist;
+}
+
+auto WebrtcGlobalStatsHistory::Entry::ReportElement::Timestamp() const
+ -> DOMHighResTimeStamp {
+ return report->mTimestamp;
+}
+
+auto WebrtcGlobalStatsHistory::Entry::SdpElement::Timestamp() const
+ -> DOMHighResTimeStamp {
+ return sdp.mTimestamp;
+}
+
+auto WebrtcGlobalStatsHistory::Entry::MakeReportElement(
+ UniquePtr<RTCStatsReportInternal> aReport)
+ -> WebrtcGlobalStatsHistory::Entry::ReportElement* {
+ auto* elem = new ReportElement();
+ elem->report = std::move(aReport);
+ // We don't want to store a copy of the SDP history with each stats entry.
+ // SDP History is stored seperately, see MakeSdpElements.
+ elem->report->mSdpHistory.Clear();
+ return elem;
+}
+
+auto WebrtcGlobalStatsHistory::Entry::MakeSdpElementsSince(
+ Sequence<RTCSdpHistoryEntryInternal>&& aSdpHistory,
+ const Maybe<DOMHighResTimeStamp>& aSdpAfter)
+ -> AutoCleanLinkedList<WebrtcGlobalStatsHistory::Entry::SdpElement> {
+ AutoCleanLinkedList<WebrtcGlobalStatsHistory::Entry::SdpElement> result;
+ for (auto& sdpHist : aSdpHistory) {
+ if (!aSdpAfter || aSdpAfter.value() < sdpHist.mTimestamp) {
+ auto* element = new SdpElement();
+ element->sdp = sdpHist;
+ result.insertBack(element);
+ }
+ }
+ return result;
+}
+
+template <typename T>
+auto FindFirstEntryAfter(const T* first,
+ const Maybe<DOMHighResTimeStamp>& aAfter) -> const T* {
+ const auto* current = first;
+ while (aAfter && current && current->Timestamp() <= aAfter.value()) {
+ current = current->getNext();
+ }
+ return current;
+}
+
+template <typename T>
+auto CountElementsToEndInclusive(const LinkedListElement<T>* elem) -> size_t {
+ size_t count = 0;
+ const auto* cursor = elem;
+ while (cursor && cursor->isInList()) {
+ count++;
+ cursor = cursor->getNext();
+ }
+ return count;
+}
+
+auto WebrtcGlobalStatsHistory::Entry::Since(
+ const Maybe<DOMHighResTimeStamp>& aAfter) const
+ -> nsTArray<RTCStatsReportInternal> {
+ nsTArray<RTCStatsReportInternal> results;
+ const auto* cursor = FindFirstEntryAfter(mReports.getFirst(), aAfter);
+ const auto count = CountElementsToEndInclusive(cursor);
+ if (!results.SetCapacity(count, fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ while (cursor) {
+ results.AppendElement(RTCStatsReportInternal(*cursor->report));
+ cursor = cursor->getNext();
+ }
+ return results;
+}
+
+auto WebrtcGlobalStatsHistory::Entry::SdpSince(
+ const Maybe<DOMHighResTimeStamp>& aAfter) const -> RTCSdpHistoryInternal {
+ RTCSdpHistoryInternal results;
+ results.mPcid = mPcid;
+ // If no timestamp was passed copy the entire history
+ const auto* cursor = FindFirstEntryAfter(mSdp.getFirst(), aAfter);
+ const auto count = CountElementsToEndInclusive(cursor);
+ if (!results.mSdpHistory.SetCapacity(count, fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ while (cursor) {
+ if (!results.mSdpHistory.AppendElement(
+ RTCSdpHistoryEntryInternal(cursor->sdp), fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ cursor = cursor->getNext();
+ }
+ return results;
+}
+
+auto WebrtcGlobalStatsHistory::Entry::Prune(const DOMHighResTimeStamp aBefore)
+ -> void {
+ // Clear everything in the case that we don't keep stats
+ if (mIsLongTermStatsDisabled) {
+ mReports.clear();
+ }
+ // Clear everything before the cutoff
+ for (auto* element = mReports.getFirst();
+ element && element->report->mTimestamp < aBefore;
+ element = mReports.getFirst()) {
+ delete mReports.popFirst();
+ }
+ // I don't think we should prune SDPs but if we did it would look like this:
+ // Note: we always keep the most recent SDP
+}
+
+auto WebrtcGlobalStatsHistory::InitHistory(const nsAString& aPcId,
+ const bool aIsLongTermStatsDisabled)
+ -> void {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ if (WebrtcGlobalStatsHistory::Get().MaybeGet(aPcId)) {
+ return;
+ }
+ WebrtcGlobalStatsHistory::Get().GetOrInsertNew(aPcId, nsString(aPcId),
+ aIsLongTermStatsDisabled);
+};
+
+auto WebrtcGlobalStatsHistory::Record(UniquePtr<RTCStatsReportInternal> aReport)
+ -> void {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ // Use the report timestamp as "now" for determining time depth
+ // based pruning.
+ const auto now = aReport->mTimestamp;
+ const auto earliest = now - SEC_TO_MS(Pref::StorageWindowS());
+
+ // Store closed state before moving the report
+ const auto closed = aReport->mClosed;
+ const auto pcId = aReport->mPcid;
+
+ auto history = WebrtcGlobalStatsHistory::GetHistory(aReport->mPcid);
+ if (history && Pref::Enabled()) {
+ auto entry = history.value();
+ // Remove expired entries
+ entry->Prune(earliest);
+ // Find new SDP entries
+ auto sdpAfter = Maybe<DOMHighResTimeStamp>(Nothing());
+ if (auto* lastSdp = entry->mSdp.getLast(); lastSdp) {
+ sdpAfter = Some(lastSdp->Timestamp());
+ }
+ entry->mSdp.extendBack(
+ Entry::MakeSdpElementsSince(std::move(aReport->mSdpHistory), sdpAfter));
+ // Reports must be in ascending order by mTimestamp
+ const auto* latest = entry->mReports.getLast();
+ // Maintain sorted order
+ if (!latest || latest->report->mTimestamp < aReport->mTimestamp) {
+ entry->mReports.insertBack(Entry::MakeReportElement(std::move(aReport)));
+ }
+ }
+ // Close the history if needed
+ if (closed) {
+ CloseHistory(pcId);
+ }
+}
+
+auto WebrtcGlobalStatsHistory::CloseHistory(const nsAString& aPcId) -> void {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ auto maybeHist = WebrtcGlobalStatsHistory::Get().MaybeGet(aPcId);
+ if (!maybeHist) {
+ return;
+ }
+ {
+ auto&& hist = maybeHist.value();
+ hist->mIsClosed = true;
+
+ if (hist->mIsLongTermStatsDisabled) {
+ WebrtcGlobalStatsHistory::Get().Remove(aPcId);
+ return;
+ }
+ }
+ size_t remainingClosedStatsToRetain =
+ WebrtcGlobalStatsHistory::Pref::ClosedStatsToRetain();
+ WebrtcGlobalStatsHistory::Get().RemoveIf([&](auto& iter) {
+ auto& entry = iter.Data();
+ if (!entry->mIsClosed) {
+ return false;
+ }
+ if (entry->mIsLongTermStatsDisabled) {
+ return true;
+ }
+ if (remainingClosedStatsToRetain > 0) {
+ remainingClosedStatsToRetain -= 1;
+ return false;
+ }
+ return true;
+ });
+}
+
+auto WebrtcGlobalStatsHistory::Clear() -> void {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ WebrtcGlobalStatsHistory::Get().RemoveIf([](auto& aIter) {
+ // First clear all the closed histories.
+ if (aIter.Data()->mIsClosed) {
+ return true;
+ }
+ // For all remaining histories clear their stored reports
+ aIter.Data()->mReports.clear();
+ // As an optimization we don't clear the SDP, because that would
+ // be reconstitued in the very next stats gathering polling period.
+ // Those are potentially large allocations which we can skip.
+ return false;
+ });
+}
+
+auto WebrtcGlobalStatsHistory::PcIds() -> dom::Sequence<nsString> {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ dom::Sequence<nsString> pcIds;
+ for (const auto& pcId : WebrtcGlobalStatsHistory::Get().Keys()) {
+ if (!pcIds.AppendElement(pcId, fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ }
+ return pcIds;
+}
+
+auto WebrtcGlobalStatsHistory::GetHistory(const nsAString& aPcId)
+ -> Maybe<RefPtr<Entry> > {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ const auto pcid = NS_ConvertUTF16toUTF8(aPcId);
+
+ return WebrtcGlobalStatsHistory::Get().MaybeGet(aPcId);
+}
+} // namespace mozilla::dom
diff --git a/dom/media/webrtc/jsapi/WebrtcGlobalStatsHistory.h b/dom/media/webrtc/jsapi/WebrtcGlobalStatsHistory.h
new file mode 100644
index 0000000000..aa820e787d
--- /dev/null
+++ b/dom/media/webrtc/jsapi/WebrtcGlobalStatsHistory.h
@@ -0,0 +1,88 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#pragma once
+
+#include <memory>
+#include "domstubs.h"
+#include "mozilla/LinkedList.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/DOMString.h"
+#include "mozilla/dom/RTCStatsReportBinding.h"
+#include "nsDOMNavigationTiming.h"
+#include "nsHashKeys.h"
+#include "nsTHashMap.h"
+
+namespace mozilla::dom {
+class WebrtcGlobalStatisticsHistoryCallback;
+struct RTCStatsReportInternal;
+
+struct WebrtcGlobalStatsHistory {
+ // History preferences
+ struct Pref {
+ static auto Enabled() -> bool;
+ static auto PollIntervalMs() -> uint32_t;
+ static auto StorageWindowS() -> uint32_t;
+ static auto PruneAfterM() -> uint32_t;
+ static auto ClosedStatsToRetain() -> uint32_t;
+ Pref() = delete;
+ ~Pref() = delete;
+ };
+
+ struct Entry {
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(Entry)
+ // We need to wrap the report in an element
+ struct ReportElement : public LinkedListElement<ReportElement> {
+ UniquePtr<RTCStatsReportInternal> report;
+ auto Timestamp() const -> DOMHighResTimeStamp;
+ virtual ~ReportElement() = default;
+ };
+ // And likewise for the SDP history
+ struct SdpElement : public LinkedListElement<SdpElement> {
+ RTCSdpHistoryEntryInternal sdp;
+ auto Timestamp() const -> DOMHighResTimeStamp;
+ virtual ~SdpElement() = default;
+ };
+
+ explicit Entry(const nsString& aPcid, const bool aIsLongTermStatsDisabled)
+ : mPcid(aPcid), mIsLongTermStatsDisabled(aIsLongTermStatsDisabled) {}
+
+ nsString mPcid;
+ AutoCleanLinkedList<ReportElement> mReports;
+ AutoCleanLinkedList<SdpElement> mSdp;
+ bool mIsLongTermStatsDisabled;
+ bool mIsClosed = false;
+
+ auto Since(const Maybe<DOMHighResTimeStamp>& aAfter) const
+ -> nsTArray<RTCStatsReportInternal>;
+ auto SdpSince(const Maybe<DOMHighResTimeStamp>& aAfter) const
+ -> RTCSdpHistoryInternal;
+
+ static auto MakeReportElement(UniquePtr<RTCStatsReportInternal> aReport)
+ -> ReportElement*;
+ static auto MakeSdpElementsSince(
+ Sequence<RTCSdpHistoryEntryInternal>&& aSdpHistory,
+ const Maybe<DOMHighResTimeStamp>& aSdpAfter)
+ -> AutoCleanLinkedList<SdpElement>;
+ auto Prune(const DOMHighResTimeStamp aBefore) -> void;
+
+ private:
+ virtual ~Entry() = default;
+ };
+ using StatsMap = nsTHashMap<nsStringHashKey, RefPtr<Entry> >;
+ static auto InitHistory(const nsAString& aPcId,
+ const bool aIsLongTermStatsDisabled) -> void;
+ static auto Record(UniquePtr<RTCStatsReportInternal> aReport) -> void;
+ static auto CloseHistory(const nsAString& aPcId) -> void;
+ static auto GetHistory(const nsAString& aPcId) -> Maybe<RefPtr<Entry> >;
+ static auto Clear() -> void;
+ static auto PcIds() -> dom::Sequence<nsString>;
+
+ WebrtcGlobalStatsHistory() = delete;
+
+ private:
+ static auto Get() -> StatsMap&;
+};
+} // namespace mozilla::dom
diff --git a/dom/media/webrtc/jsapi/moz.build b/dom/media/webrtc/jsapi/moz.build
new file mode 100644
index 0000000000..2c1dbe79f1
--- /dev/null
+++ b/dom/media/webrtc/jsapi/moz.build
@@ -0,0 +1,51 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+include("/dom/media/webrtc/third_party_build/webrtc.mozbuild")
+
+LOCAL_INCLUDES += [
+ "!/ipc/ipdl/_ipdlheaders",
+ "/dom/base",
+ "/dom/media",
+ "/dom/media/webrtc",
+ "/ipc/chromium/src",
+ "/media/webrtc",
+ "/netwerk/dns", # For nsDNSService2.h
+ "/third_party/libsrtp/src/include",
+ "/third_party/libwebrtc",
+ "/third_party/libwebrtc/third_party/abseil-cpp",
+]
+
+UNIFIED_SOURCES += [
+ "MediaTransportHandler.cpp",
+ "MediaTransportHandlerIPC.cpp",
+ "MediaTransportParent.cpp",
+ "PacketDumper.cpp",
+ "PeerConnectionCtx.cpp",
+ "PeerConnectionImpl.cpp",
+ "RemoteTrackSource.cpp",
+ "RTCDtlsTransport.cpp",
+ "RTCDTMFSender.cpp",
+ "RTCRtpReceiver.cpp",
+ "RTCRtpSender.cpp",
+ "RTCRtpTransceiver.cpp",
+ "RTCSctpTransport.cpp",
+ "RTCStatsIdGenerator.cpp",
+ "RTCStatsReport.cpp",
+ "WebrtcGlobalInformation.cpp",
+ "WebrtcGlobalStatsHistory.cpp",
+]
+
+EXPORTS.mozilla.dom += [
+ "RTCDtlsTransport.h",
+ "RTCDTMFSender.h",
+ "RTCRtpReceiver.h",
+ "RTCRtpSender.h",
+ "RTCRtpTransceiver.h",
+ "RTCSctpTransport.h",
+ "RTCStatsReport.h",
+]
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/webrtc/jsep/JsepCodecDescription.h b/dom/media/webrtc/jsep/JsepCodecDescription.h
new file mode 100644
index 0000000000..5336182b77
--- /dev/null
+++ b/dom/media/webrtc/jsep/JsepCodecDescription.h
@@ -0,0 +1,1196 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _JSEPCODECDESCRIPTION_H_
+#define _JSEPCODECDESCRIPTION_H_
+
+#include <cmath>
+#include <string>
+#include "sdp/SdpMediaSection.h"
+#include "sdp/SdpHelper.h"
+#include "nsCRT.h"
+#include "nsString.h"
+#include "mozilla/net/DataChannelProtocol.h"
+#include "mozilla/Preferences.h"
+
+namespace mozilla {
+
+#define JSEP_CODEC_CLONE(T) \
+ virtual JsepCodecDescription* Clone() const override { return new T(*this); }
+
+// A single entry in our list of known codecs.
+class JsepCodecDescription {
+ public:
+ JsepCodecDescription(const std::string& defaultPt, const std::string& name,
+ uint32_t clock, uint32_t channels, bool enabled)
+ : mDefaultPt(defaultPt),
+ mName(name),
+ mClock(clock),
+ mChannels(channels),
+ mEnabled(enabled),
+ mStronglyPreferred(false),
+ mDirection(sdp::kSend) {}
+ virtual ~JsepCodecDescription() {}
+
+ virtual SdpMediaSection::MediaType Type() const = 0;
+
+ virtual JsepCodecDescription* Clone() const = 0;
+
+ bool GetPtAsInt(uint16_t* ptOutparam) const {
+ return SdpHelper::GetPtAsInt(mDefaultPt, ptOutparam);
+ }
+
+ // The id used for codec stats, to uniquely identify this codec configuration
+ // within a transport.
+ const nsString& StatsId() const {
+ if (!mStatsId) {
+ mStatsId.emplace();
+ mStatsId->AppendPrintf(
+ "_%s_%s/%s_%u_%u_%s", mDefaultPt.c_str(),
+ Type() == SdpMediaSection::kVideo ? "video" : "audio", mName.c_str(),
+ mClock, mChannels, mSdpFmtpLine ? mSdpFmtpLine->c_str() : "nothing");
+ }
+ return *mStatsId;
+ }
+
+ virtual bool Matches(const std::string& fmt,
+ const SdpMediaSection& remoteMsection) const {
+ // note: fmt here is remote fmt (to go with remoteMsection)
+ if (Type() != remoteMsection.GetMediaType()) {
+ return false;
+ }
+
+ const SdpRtpmapAttributeList::Rtpmap* entry(remoteMsection.FindRtpmap(fmt));
+
+ if (entry) {
+ if (!nsCRT::strcasecmp(mName.c_str(), entry->name.c_str()) &&
+ (mClock == entry->clock) && (mChannels == entry->channels)) {
+ return ParametersMatch(fmt, remoteMsection);
+ }
+ } else if (!fmt.compare("9") && mName == "G722") {
+ return true;
+ } else if (!fmt.compare("0") && mName == "PCMU") {
+ return true;
+ } else if (!fmt.compare("8") && mName == "PCMA") {
+ return true;
+ }
+ return false;
+ }
+
+ virtual bool ParametersMatch(const std::string& fmt,
+ const SdpMediaSection& remoteMsection) const {
+ return true;
+ }
+
+ Maybe<std::string> GetMatchingFormat(
+ const SdpMediaSection& remoteMsection) const {
+ for (const auto& fmt : remoteMsection.GetFormats()) {
+ if (Matches(fmt, remoteMsection)) {
+ return Some(fmt);
+ }
+ }
+ return Nothing();
+ }
+
+ virtual bool Negotiate(const std::string& pt,
+ const SdpMediaSection& remoteMsection,
+ bool remoteIsOffer,
+ Maybe<const SdpMediaSection&> localMsection) {
+ // Configuration might change. Invalidate the stats id.
+ mStatsId = Nothing();
+ if (mDirection == sdp::kSend || remoteIsOffer) {
+ mDefaultPt = pt;
+ }
+ if (localMsection) {
+ // Offer/answer is concluding. Update the sdpFmtpLine.
+ MOZ_ASSERT(mDirection == sdp::kSend || mDirection == sdp::kRecv);
+ const SdpMediaSection& msection =
+ mDirection == sdp::kSend ? remoteMsection : *localMsection;
+ UpdateSdpFmtpLine(ToMaybeRef(msection.FindFmtp(mDefaultPt)));
+ }
+ return true;
+ }
+
+ virtual void ApplyConfigToFmtp(
+ UniquePtr<SdpFmtpAttributeList::Parameters>& aFmtp) const = 0;
+
+ virtual void AddToMediaSection(SdpMediaSection& msection) const {
+ if (mEnabled && msection.GetMediaType() == Type()) {
+ if (mDirection == sdp::kRecv) {
+ msection.AddCodec(mDefaultPt, mName, mClock, mChannels);
+ }
+
+ AddParametersToMSection(msection);
+ }
+ }
+
+ virtual void AddParametersToMSection(SdpMediaSection& msection) const {}
+
+ virtual void EnsureNoDuplicatePayloadTypes(std::set<std::string>& aUsedPts) {
+ mEnabled = EnsurePayloadTypeNotDuplicate(aUsedPts, mDefaultPt);
+ }
+
+ bool EnsurePayloadTypeNotDuplicate(std::set<std::string>& aUsedPts,
+ std::string& aPtToCheck) {
+ if (!mEnabled) {
+ return false;
+ }
+
+ if (!aUsedPts.count(aPtToCheck)) {
+ aUsedPts.insert(aPtToCheck);
+ return true;
+ }
+
+ // |codec| cannot use its current payload type. Try to find another.
+ for (uint16_t freePt = 96; freePt <= 127; ++freePt) {
+ // Not super efficient, but readability is probably more important.
+ std::ostringstream os;
+ os << freePt;
+ std::string freePtAsString = os.str();
+
+ if (!aUsedPts.count(freePtAsString)) {
+ aUsedPts.insert(freePtAsString);
+ aPtToCheck = freePtAsString;
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ // TODO Bug 1751671: Take a verbatim fmtp line (std::string or eq.) instead
+ // of fmtp parameters that have to be (re-)serialized.
+ void UpdateSdpFmtpLine(
+ const Maybe<const SdpFmtpAttributeList::Parameters&> aParams) {
+ mSdpFmtpLine = aParams.map([](const auto& aFmtp) {
+ std::stringstream ss;
+ aFmtp.Serialize(ss);
+ return ss.str();
+ });
+ }
+
+ std::string mDefaultPt;
+ std::string mName;
+ Maybe<std::string> mSdpFmtpLine;
+ mutable Maybe<nsString> mStatsId;
+ uint32_t mClock;
+ uint32_t mChannels;
+ bool mEnabled;
+ bool mStronglyPreferred;
+ sdp::Direction mDirection;
+ // Will hold constraints from both fmtp and rid
+ EncodingConstraints mConstraints;
+};
+
+class JsepAudioCodecDescription : public JsepCodecDescription {
+ public:
+ JsepAudioCodecDescription(const std::string& defaultPt,
+ const std::string& name, uint32_t clock,
+ uint32_t channels, bool enabled = true)
+ : JsepCodecDescription(defaultPt, name, clock, channels, enabled),
+ mMaxPlaybackRate(0),
+ mForceMono(false),
+ mFECEnabled(false),
+ mDtmfEnabled(false),
+ mMaxAverageBitrate(0),
+ mDTXEnabled(false),
+ mFrameSizeMs(0),
+ mMinFrameSizeMs(0),
+ mMaxFrameSizeMs(0),
+ mCbrEnabled(false) {}
+
+ static constexpr SdpMediaSection::MediaType type = SdpMediaSection::kAudio;
+
+ SdpMediaSection::MediaType Type() const override { return type; }
+
+ JSEP_CODEC_CLONE(JsepAudioCodecDescription)
+
+ static UniquePtr<JsepAudioCodecDescription> CreateDefaultOpus() {
+ // Per jmspeex on IRC:
+ // For 32KHz sampling, 28 is ok, 32 is good, 40 should be really good
+ // quality. Note that 1-2Kbps will be wasted on a stereo Opus channel
+ // with mono input compared to configuring it for mono.
+ // If we reduce bitrate enough Opus will low-pass us; 16000 will kill a
+ // 9KHz tone. This should be adaptive when we're at the low-end of video
+ // bandwidth (say <100Kbps), and if we're audio-only, down to 8 or
+ // 12Kbps.
+ return MakeUnique<JsepAudioCodecDescription>("109", "opus", 48000, 2);
+ }
+
+ static UniquePtr<JsepAudioCodecDescription> CreateDefaultG722() {
+ return MakeUnique<JsepAudioCodecDescription>("9", "G722", 8000, 1);
+ }
+
+ static UniquePtr<JsepAudioCodecDescription> CreateDefaultPCMU() {
+ return MakeUnique<JsepAudioCodecDescription>("0", "PCMU", 8000, 1);
+ }
+
+ static UniquePtr<JsepAudioCodecDescription> CreateDefaultPCMA() {
+ return MakeUnique<JsepAudioCodecDescription>("8", "PCMA", 8000, 1);
+ }
+
+ static UniquePtr<JsepAudioCodecDescription> CreateDefaultTelephoneEvent() {
+ return MakeUnique<JsepAudioCodecDescription>("101", "telephone-event", 8000,
+ 1);
+ }
+
+ SdpFmtpAttributeList::OpusParameters GetOpusParameters(
+ const std::string& pt, const SdpMediaSection& msection) const {
+ // Will contain defaults if nothing else
+ SdpFmtpAttributeList::OpusParameters result;
+ auto* params = msection.FindFmtp(pt);
+
+ if (params && params->codec_type == SdpRtpmapAttributeList::kOpus) {
+ result =
+ static_cast<const SdpFmtpAttributeList::OpusParameters&>(*params);
+ }
+
+ return result;
+ }
+
+ SdpFmtpAttributeList::TelephoneEventParameters GetTelephoneEventParameters(
+ const std::string& pt, const SdpMediaSection& msection) const {
+ // Will contain defaults if nothing else
+ SdpFmtpAttributeList::TelephoneEventParameters result;
+ auto* params = msection.FindFmtp(pt);
+
+ if (params &&
+ params->codec_type == SdpRtpmapAttributeList::kTelephoneEvent) {
+ result =
+ static_cast<const SdpFmtpAttributeList::TelephoneEventParameters&>(
+ *params);
+ }
+
+ return result;
+ }
+
+ void AddParametersToMSection(SdpMediaSection& msection) const override {
+ if (mDirection == sdp::kSend) {
+ return;
+ }
+
+ if (mName == "opus") {
+ UniquePtr<SdpFmtpAttributeList::Parameters> opusParams =
+ MakeUnique<SdpFmtpAttributeList::OpusParameters>(
+ GetOpusParameters(mDefaultPt, msection));
+
+ ApplyConfigToFmtp(opusParams);
+
+ msection.SetFmtp(SdpFmtpAttributeList::Fmtp(mDefaultPt, *opusParams));
+ } else if (mName == "telephone-event") {
+ // add the default dtmf tones
+ SdpFmtpAttributeList::TelephoneEventParameters teParams(
+ GetTelephoneEventParameters(mDefaultPt, msection));
+ msection.SetFmtp(SdpFmtpAttributeList::Fmtp(mDefaultPt, teParams));
+ }
+ }
+
+ bool Negotiate(const std::string& pt, const SdpMediaSection& remoteMsection,
+ bool remoteIsOffer,
+ Maybe<const SdpMediaSection&> localMsection) override {
+ JsepCodecDescription::Negotiate(pt, remoteMsection, remoteIsOffer,
+ localMsection);
+ if (mName == "opus" && mDirection == sdp::kSend) {
+ SdpFmtpAttributeList::OpusParameters opusParams(
+ GetOpusParameters(mDefaultPt, remoteMsection));
+
+ mMaxPlaybackRate = opusParams.maxplaybackrate;
+ mForceMono = !opusParams.stereo;
+ // draft-ietf-rtcweb-fec-03.txt section 4.2 says support for FEC
+ // at the received side is declarative and can be negotiated
+ // separately for either media direction.
+ mFECEnabled = opusParams.useInBandFec;
+ if ((opusParams.maxAverageBitrate >= 6000) &&
+ (opusParams.maxAverageBitrate <= 510000)) {
+ mMaxAverageBitrate = opusParams.maxAverageBitrate;
+ }
+ mDTXEnabled = opusParams.useDTX;
+ if (remoteMsection.GetAttributeList().HasAttribute(
+ SdpAttribute::kPtimeAttribute)) {
+ mFrameSizeMs = remoteMsection.GetAttributeList().GetPtime();
+ } else {
+ mFrameSizeMs = opusParams.frameSizeMs;
+ }
+ mMinFrameSizeMs = opusParams.minFrameSizeMs;
+ if (remoteMsection.GetAttributeList().HasAttribute(
+ SdpAttribute::kMaxptimeAttribute)) {
+ mMaxFrameSizeMs = remoteMsection.GetAttributeList().GetMaxptime();
+ } else {
+ mMaxFrameSizeMs = opusParams.maxFrameSizeMs;
+ }
+ mCbrEnabled = opusParams.useCbr;
+ }
+
+ return true;
+ }
+
+ void ApplyConfigToFmtp(
+ UniquePtr<SdpFmtpAttributeList::Parameters>& aFmtp) const override {
+ if (mName == "opus") {
+ SdpFmtpAttributeList::OpusParameters opusParams;
+ if (aFmtp) {
+ MOZ_RELEASE_ASSERT(aFmtp->codec_type == SdpRtpmapAttributeList::kOpus);
+ opusParams =
+ static_cast<const SdpFmtpAttributeList::OpusParameters&>(*aFmtp);
+ opusParams.useInBandFec = mFECEnabled ? 1 : 0;
+ } else {
+ // If we weren't passed a fmtp to use then show we can do in band FEC
+ // for getCapabilities queries.
+ opusParams.useInBandFec = 1;
+ }
+ if (mMaxPlaybackRate) {
+ opusParams.maxplaybackrate = mMaxPlaybackRate;
+ }
+ opusParams.maxAverageBitrate = mMaxAverageBitrate;
+
+ if (mChannels == 2 &&
+ !Preferences::GetBool("media.peerconnection.sdp.disable_stereo_fmtp",
+ false) &&
+ !mForceMono) {
+ // We prefer to receive stereo, if available.
+ opusParams.stereo = 1;
+ }
+ opusParams.useDTX = mDTXEnabled;
+ opusParams.frameSizeMs = mFrameSizeMs;
+ opusParams.minFrameSizeMs = mMinFrameSizeMs;
+ opusParams.maxFrameSizeMs = mMaxFrameSizeMs;
+ opusParams.useCbr = mCbrEnabled;
+ aFmtp.reset(opusParams.Clone());
+ }
+ };
+
+ uint32_t mMaxPlaybackRate;
+ bool mForceMono;
+ bool mFECEnabled;
+ bool mDtmfEnabled;
+ uint32_t mMaxAverageBitrate;
+ bool mDTXEnabled;
+ uint32_t mFrameSizeMs;
+ uint32_t mMinFrameSizeMs;
+ uint32_t mMaxFrameSizeMs;
+ bool mCbrEnabled;
+};
+
+class JsepVideoCodecDescription : public JsepCodecDescription {
+ public:
+ JsepVideoCodecDescription(const std::string& defaultPt,
+ const std::string& name, uint32_t clock,
+ bool enabled = true)
+ : JsepCodecDescription(defaultPt, name, clock, 0, enabled),
+ mTmmbrEnabled(false),
+ mRembEnabled(false),
+ mFECEnabled(false),
+ mTransportCCEnabled(false),
+ mRtxEnabled(false),
+ mProfileLevelId(0),
+ mPacketizationMode(0) {
+ // Add supported rtcp-fb types
+ mNackFbTypes.push_back("");
+ mNackFbTypes.push_back(SdpRtcpFbAttributeList::pli);
+ mCcmFbTypes.push_back(SdpRtcpFbAttributeList::fir);
+ }
+
+ static constexpr SdpMediaSection::MediaType type = SdpMediaSection::kVideo;
+
+ SdpMediaSection::MediaType Type() const override { return type; }
+
+ static UniquePtr<JsepVideoCodecDescription> CreateDefaultVP8(bool aUseRtx) {
+ auto codec = MakeUnique<JsepVideoCodecDescription>("120", "VP8", 90000);
+ // Defaults for mandatory params
+ codec->mConstraints.maxFs = 12288; // Enough for 2048x1536
+ codec->mConstraints.maxFps = Some(60);
+ if (aUseRtx) {
+ codec->EnableRtx("124");
+ }
+ return codec;
+ }
+
+ static UniquePtr<JsepVideoCodecDescription> CreateDefaultVP9(bool aUseRtx) {
+ auto codec = MakeUnique<JsepVideoCodecDescription>("121", "VP9", 90000);
+ // Defaults for mandatory params
+ codec->mConstraints.maxFs = 12288; // Enough for 2048x1536
+ codec->mConstraints.maxFps = Some(60);
+ if (aUseRtx) {
+ codec->EnableRtx("125");
+ }
+ return codec;
+ }
+
+ static UniquePtr<JsepVideoCodecDescription> CreateDefaultH264_0(
+ bool aUseRtx) {
+ auto codec = MakeUnique<JsepVideoCodecDescription>("97", "H264", 90000);
+ codec->mPacketizationMode = 0;
+ // Defaults for mandatory params
+ codec->mProfileLevelId = 0x42E00D;
+ if (aUseRtx) {
+ codec->EnableRtx("98");
+ }
+ return codec;
+ }
+
+ static UniquePtr<JsepVideoCodecDescription> CreateDefaultH264_1(
+ bool aUseRtx) {
+ auto codec = MakeUnique<JsepVideoCodecDescription>("126", "H264", 90000);
+ codec->mPacketizationMode = 1;
+ // Defaults for mandatory params
+ codec->mProfileLevelId = 0x42E00D;
+ if (aUseRtx) {
+ codec->EnableRtx("127");
+ }
+ return codec;
+ }
+
+ static UniquePtr<JsepVideoCodecDescription> CreateDefaultUlpFec() {
+ return MakeUnique<JsepVideoCodecDescription>(
+ "123", // payload type
+ "ulpfec", // codec name
+ 90000 // clock rate (match other video codecs)
+ );
+ }
+
+ static UniquePtr<JsepVideoCodecDescription> CreateDefaultRed() {
+ return MakeUnique<JsepVideoCodecDescription>(
+ "122", // payload type
+ "red", // codec name
+ 90000 // clock rate (match other video codecs)
+ );
+ }
+
+ void ApplyConfigToFmtp(
+ UniquePtr<SdpFmtpAttributeList::Parameters>& aFmtp) const override {
+ if (mName == "H264") {
+ SdpFmtpAttributeList::H264Parameters h264Params;
+ if (aFmtp) {
+ MOZ_RELEASE_ASSERT(aFmtp->codec_type == SdpRtpmapAttributeList::kH264);
+ h264Params =
+ static_cast<const SdpFmtpAttributeList::H264Parameters&>(*aFmtp);
+ }
+
+ if (mDirection == sdp::kSend) {
+ if (!h264Params.level_asymmetry_allowed) {
+ // First time the fmtp has been set; set just in case this is for a
+ // sendonly m-line, since even though we aren't receiving the level
+ // negotiation still needs to happen (sigh).
+ h264Params.profile_level_id = mProfileLevelId;
+ }
+ } else {
+ // Parameters that only apply to what we receive
+ h264Params.max_mbps = mConstraints.maxMbps;
+ h264Params.max_fs = mConstraints.maxFs;
+ h264Params.max_cpb = mConstraints.maxCpb;
+ h264Params.max_dpb = mConstraints.maxDpb;
+ h264Params.max_br = mConstraints.maxBr;
+ strncpy(h264Params.sprop_parameter_sets, mSpropParameterSets.c_str(),
+ sizeof(h264Params.sprop_parameter_sets) - 1);
+ h264Params.profile_level_id = mProfileLevelId;
+ }
+
+ // Parameters that apply to both the send and recv directions
+ h264Params.packetization_mode = mPacketizationMode;
+ // Hard-coded, may need to change someday?
+ h264Params.level_asymmetry_allowed = true;
+
+ // Parameters that apply to both the send and recv directions
+ h264Params.packetization_mode = mPacketizationMode;
+ // Hard-coded, may need to change someday?
+ h264Params.level_asymmetry_allowed = true;
+ aFmtp.reset(h264Params.Clone());
+ } else if (mName == "VP8" || mName == "VP9") {
+ SdpRtpmapAttributeList::CodecType type =
+ mName == "VP8" ? SdpRtpmapAttributeList::CodecType::kVP8
+ : SdpRtpmapAttributeList::CodecType::kVP9;
+ auto vp8Params = SdpFmtpAttributeList::VP8Parameters(type);
+
+ if (aFmtp) {
+ MOZ_RELEASE_ASSERT(aFmtp->codec_type == type);
+ vp8Params =
+ static_cast<const SdpFmtpAttributeList::VP8Parameters&>(*aFmtp);
+ }
+ // VP8 and VP9 share the same SDP parameters thus far
+ vp8Params.max_fs = mConstraints.maxFs;
+ if (mConstraints.maxFps.isSome()) {
+ vp8Params.max_fr =
+ static_cast<unsigned int>(std::round(*mConstraints.maxFps));
+ } else {
+ vp8Params.max_fr = 60;
+ }
+ aFmtp.reset(vp8Params.Clone());
+ }
+ }
+
+ virtual void EnableTmmbr() {
+ // EnableTmmbr can be called multiple times due to multiple calls to
+ // PeerConnectionImpl::ConfigureJsepSessionCodecs
+ if (!mTmmbrEnabled) {
+ mTmmbrEnabled = true;
+ mCcmFbTypes.push_back(SdpRtcpFbAttributeList::tmmbr);
+ }
+ }
+
+ virtual void EnableRemb() {
+ // EnableRemb can be called multiple times due to multiple calls to
+ // PeerConnectionImpl::ConfigureJsepSessionCodecs
+ if (!mRembEnabled) {
+ mRembEnabled = true;
+ mOtherFbTypes.push_back({"", SdpRtcpFbAttributeList::kRemb, "", ""});
+ }
+ }
+
+ virtual void EnableFec(std::string redPayloadType,
+ std::string ulpfecPayloadType) {
+ // Enabling FEC for video works a little differently than enabling
+ // REMB or TMMBR. Support for FEC is indicated by the presence of
+ // particular codes (red and ulpfec) instead of using rtcpfb
+ // attributes on a given codec. There is no rtcpfb to push for FEC
+ // as can be seen above when REMB or TMMBR are enabled.
+
+ // Ensure we have valid payload types. This returns zero on failure, which
+ // is a valid payload type.
+ uint16_t redPt, ulpfecPt;
+ if (!SdpHelper::GetPtAsInt(redPayloadType, &redPt) ||
+ !SdpHelper::GetPtAsInt(ulpfecPayloadType, &ulpfecPt)) {
+ return;
+ }
+
+ mFECEnabled = true;
+ mREDPayloadType = redPayloadType;
+ mULPFECPayloadType = ulpfecPayloadType;
+ }
+
+ virtual void EnableTransportCC() {
+ if (!mTransportCCEnabled) {
+ mTransportCCEnabled = true;
+ mOtherFbTypes.push_back(
+ {"", SdpRtcpFbAttributeList::kTransportCC, "", ""});
+ }
+ }
+
+ void EnableRtx(const std::string& rtxPayloadType) {
+ mRtxEnabled = true;
+ mRtxPayloadType = rtxPayloadType;
+ }
+
+ void AddParametersToMSection(SdpMediaSection& msection) const override {
+ AddFmtpsToMSection(msection);
+ AddRtcpFbsToMSection(msection);
+ }
+
+ void AddFmtpsToMSection(SdpMediaSection& msection) const {
+ if (mName == "H264") {
+ UniquePtr<SdpFmtpAttributeList::Parameters> h264Params =
+ MakeUnique<SdpFmtpAttributeList::H264Parameters>(
+ GetH264Parameters(mDefaultPt, msection));
+
+ ApplyConfigToFmtp(h264Params);
+
+ msection.SetFmtp(SdpFmtpAttributeList::Fmtp(mDefaultPt, *h264Params));
+ } else if (mName == "red" && !mRedundantEncodings.empty()) {
+ SdpFmtpAttributeList::RedParameters redParams(
+ GetRedParameters(mDefaultPt, msection));
+ redParams.encodings = mRedundantEncodings;
+ msection.SetFmtp(SdpFmtpAttributeList::Fmtp(mDefaultPt, redParams));
+ } else if (mName == "VP8" || mName == "VP9") {
+ if (mDirection == sdp::kRecv) {
+ // VP8 and VP9 share the same SDP parameters thus far
+ UniquePtr<SdpFmtpAttributeList::Parameters> vp8Params =
+ MakeUnique<SdpFmtpAttributeList::VP8Parameters>(
+ GetVP8Parameters(mDefaultPt, msection));
+ ApplyConfigToFmtp(vp8Params);
+ msection.SetFmtp(SdpFmtpAttributeList::Fmtp(mDefaultPt, *vp8Params));
+ }
+ }
+
+ if (mRtxEnabled && mDirection == sdp::kRecv) {
+ SdpFmtpAttributeList::RtxParameters params(
+ GetRtxParameters(mDefaultPt, msection));
+ uint16_t apt;
+ if (SdpHelper::GetPtAsInt(mDefaultPt, &apt)) {
+ if (apt <= 127) {
+ msection.AddCodec(mRtxPayloadType, "rtx", mClock, mChannels);
+
+ params.apt = apt;
+ msection.SetFmtp(SdpFmtpAttributeList::Fmtp(mRtxPayloadType, params));
+ }
+ }
+ }
+ }
+
+ void AddRtcpFbsToMSection(SdpMediaSection& msection) const {
+ SdpRtcpFbAttributeList rtcpfbs(msection.GetRtcpFbs());
+ for (const auto& rtcpfb : rtcpfbs.mFeedbacks) {
+ if (rtcpfb.pt == mDefaultPt) {
+ // Already set by the codec for the other direction.
+ return;
+ }
+ }
+
+ for (const std::string& type : mAckFbTypes) {
+ rtcpfbs.PushEntry(mDefaultPt, SdpRtcpFbAttributeList::kAck, type);
+ }
+ for (const std::string& type : mNackFbTypes) {
+ rtcpfbs.PushEntry(mDefaultPt, SdpRtcpFbAttributeList::kNack, type);
+ }
+ for (const std::string& type : mCcmFbTypes) {
+ rtcpfbs.PushEntry(mDefaultPt, SdpRtcpFbAttributeList::kCcm, type);
+ }
+ for (const auto& fb : mOtherFbTypes) {
+ rtcpfbs.PushEntry(mDefaultPt, fb.type, fb.parameter, fb.extra);
+ }
+
+ msection.SetRtcpFbs(rtcpfbs);
+ }
+
+ SdpFmtpAttributeList::H264Parameters GetH264Parameters(
+ const std::string& pt, const SdpMediaSection& msection) const {
+ // Will contain defaults if nothing else
+ SdpFmtpAttributeList::H264Parameters result;
+ auto* params = msection.FindFmtp(pt);
+
+ if (params && params->codec_type == SdpRtpmapAttributeList::kH264) {
+ result =
+ static_cast<const SdpFmtpAttributeList::H264Parameters&>(*params);
+ }
+
+ return result;
+ }
+
+ SdpFmtpAttributeList::RedParameters GetRedParameters(
+ const std::string& pt, const SdpMediaSection& msection) const {
+ SdpFmtpAttributeList::RedParameters result;
+ auto* params = msection.FindFmtp(pt);
+
+ if (params && params->codec_type == SdpRtpmapAttributeList::kRed) {
+ result = static_cast<const SdpFmtpAttributeList::RedParameters&>(*params);
+ }
+
+ return result;
+ }
+
+ SdpFmtpAttributeList::RtxParameters GetRtxParameters(
+ const std::string& pt, const SdpMediaSection& msection) const {
+ SdpFmtpAttributeList::RtxParameters result;
+ const auto* params = msection.FindFmtp(pt);
+
+ if (params && params->codec_type == SdpRtpmapAttributeList::kRtx) {
+ result = static_cast<const SdpFmtpAttributeList::RtxParameters&>(*params);
+ }
+
+ return result;
+ }
+
+ Maybe<std::string> GetRtxPtByApt(const std::string& apt,
+ const SdpMediaSection& msection) const {
+ Maybe<std::string> result;
+ uint16_t aptAsInt;
+ if (!SdpHelper::GetPtAsInt(apt, &aptAsInt)) {
+ return result;
+ }
+
+ const SdpAttributeList& attrs = msection.GetAttributeList();
+ if (attrs.HasAttribute(SdpAttribute::kFmtpAttribute)) {
+ for (const auto& fmtpAttr : attrs.GetFmtp().mFmtps) {
+ if (fmtpAttr.parameters) {
+ auto* params = fmtpAttr.parameters.get();
+ if (params && params->codec_type == SdpRtpmapAttributeList::kRtx) {
+ const SdpFmtpAttributeList::RtxParameters* rtxParams =
+ static_cast<const SdpFmtpAttributeList::RtxParameters*>(params);
+ if (rtxParams->apt == aptAsInt) {
+ result = Some(fmtpAttr.format);
+ break;
+ }
+ }
+ }
+ }
+ }
+ return result;
+ }
+
+ SdpFmtpAttributeList::VP8Parameters GetVP8Parameters(
+ const std::string& pt, const SdpMediaSection& msection) const {
+ SdpRtpmapAttributeList::CodecType expectedType(
+ mName == "VP8" ? SdpRtpmapAttributeList::kVP8
+ : SdpRtpmapAttributeList::kVP9);
+
+ // Will contain defaults if nothing else
+ SdpFmtpAttributeList::VP8Parameters result(expectedType);
+ auto* params = msection.FindFmtp(pt);
+
+ if (params && params->codec_type == expectedType) {
+ result = static_cast<const SdpFmtpAttributeList::VP8Parameters&>(*params);
+ }
+
+ return result;
+ }
+
+ void NegotiateRtcpFb(const SdpMediaSection& remoteMsection,
+ SdpRtcpFbAttributeList::Type type,
+ std::vector<std::string>* supportedTypes) {
+ Maybe<std::string> remoteFmt = GetMatchingFormat(remoteMsection);
+ if (!remoteFmt) {
+ return;
+ }
+ std::vector<std::string> temp;
+ for (auto& subType : *supportedTypes) {
+ if (remoteMsection.HasRtcpFb(*remoteFmt, type, subType)) {
+ temp.push_back(subType);
+ }
+ }
+ *supportedTypes = temp;
+ }
+
+ void NegotiateRtcpFb(
+ const SdpMediaSection& remoteMsection,
+ std::vector<SdpRtcpFbAttributeList::Feedback>* supportedFbs) {
+ Maybe<std::string> remoteFmt = GetMatchingFormat(remoteMsection);
+ if (!remoteFmt) {
+ return;
+ }
+ std::vector<SdpRtcpFbAttributeList::Feedback> temp;
+ for (auto& fb : *supportedFbs) {
+ if (remoteMsection.HasRtcpFb(*remoteFmt, fb.type, fb.parameter)) {
+ temp.push_back(fb);
+ }
+ }
+ *supportedFbs = temp;
+ }
+
+ void NegotiateRtcpFb(const SdpMediaSection& remote) {
+ // Removes rtcp-fb types that the other side doesn't support
+ NegotiateRtcpFb(remote, SdpRtcpFbAttributeList::kAck, &mAckFbTypes);
+ NegotiateRtcpFb(remote, SdpRtcpFbAttributeList::kNack, &mNackFbTypes);
+ NegotiateRtcpFb(remote, SdpRtcpFbAttributeList::kCcm, &mCcmFbTypes);
+ NegotiateRtcpFb(remote, &mOtherFbTypes);
+ }
+
+ virtual bool Negotiate(const std::string& pt,
+ const SdpMediaSection& remoteMsection,
+ bool remoteIsOffer,
+ Maybe<const SdpMediaSection&> localMsection) override {
+ JsepCodecDescription::Negotiate(pt, remoteMsection, remoteIsOffer,
+ localMsection);
+ if (mName == "H264") {
+ SdpFmtpAttributeList::H264Parameters h264Params(
+ GetH264Parameters(mDefaultPt, remoteMsection));
+
+ // Level is negotiated symmetrically if level asymmetry is disallowed
+ if (!h264Params.level_asymmetry_allowed) {
+ SetSaneH264Level(std::min(GetSaneH264Level(h264Params.profile_level_id),
+ GetSaneH264Level(mProfileLevelId)),
+ &mProfileLevelId);
+ }
+
+ if (mDirection == sdp::kSend) {
+ // Remote values of these apply only to the send codec.
+ mConstraints.maxFs = h264Params.max_fs;
+ mConstraints.maxMbps = h264Params.max_mbps;
+ mConstraints.maxCpb = h264Params.max_cpb;
+ mConstraints.maxDpb = h264Params.max_dpb;
+ mConstraints.maxBr = h264Params.max_br;
+ mSpropParameterSets = h264Params.sprop_parameter_sets;
+ // Only do this if we didn't symmetrically negotiate above
+ if (h264Params.level_asymmetry_allowed) {
+ SetSaneH264Level(GetSaneH264Level(h264Params.profile_level_id),
+ &mProfileLevelId);
+ }
+ } else {
+ // TODO(bug 1143709): max-recv-level support
+ }
+ } else if (mName == "red") {
+ SdpFmtpAttributeList::RedParameters redParams(
+ GetRedParameters(mDefaultPt, remoteMsection));
+ mRedundantEncodings = redParams.encodings;
+ } else if (mName == "VP8" || mName == "VP9") {
+ if (mDirection == sdp::kSend) {
+ SdpFmtpAttributeList::VP8Parameters vp8Params(
+ GetVP8Parameters(mDefaultPt, remoteMsection));
+
+ mConstraints.maxFs = vp8Params.max_fs;
+ // Right now, we treat max-fr=0 (or the absence of max-fr) as no limit.
+ // We will eventually want to stop doing this (bug 1762600).
+ if (vp8Params.max_fr) {
+ mConstraints.maxFps = Some(vp8Params.max_fr);
+ }
+ }
+ }
+
+ if (mRtxEnabled && (mDirection == sdp::kSend || remoteIsOffer)) {
+ Maybe<std::string> rtxPt = GetRtxPtByApt(mDefaultPt, remoteMsection);
+ if (rtxPt.isSome()) {
+ EnableRtx(*rtxPt);
+ } else {
+ mRtxEnabled = false;
+ mRtxPayloadType = "";
+ }
+ }
+
+ NegotiateRtcpFb(remoteMsection);
+ return true;
+ }
+
+ // Maps the not-so-sane encoding of H264 level into something that is
+ // ordered in the way one would expect
+ // 1b is 0xAB, everything else is the level left-shifted one half-byte
+ // (eg; 1.0 is 0xA0, 1.1 is 0xB0, 3.1 is 0x1F0)
+ static uint32_t GetSaneH264Level(uint32_t profileLevelId) {
+ uint32_t profileIdc = (profileLevelId >> 16);
+
+ if (profileIdc == 0x42 || profileIdc == 0x4D || profileIdc == 0x58) {
+ if ((profileLevelId & 0x10FF) == 0x100B) {
+ // Level 1b
+ return 0xAB;
+ }
+ }
+
+ uint32_t level = profileLevelId & 0xFF;
+
+ if (level == 0x09) {
+ // Another way to encode level 1b
+ return 0xAB;
+ }
+
+ return level << 4;
+ }
+
+ static void SetSaneH264Level(uint32_t level, uint32_t* profileLevelId) {
+ uint32_t profileIdc = (*profileLevelId >> 16);
+ uint32_t levelMask = 0xFF;
+
+ if (profileIdc == 0x42 || profileIdc == 0x4d || profileIdc == 0x58) {
+ levelMask = 0x10FF;
+ if (level == 0xAB) {
+ // Level 1b
+ level = 0x100B;
+ } else {
+ // Not 1b, just shift
+ level = level >> 4;
+ }
+ } else if (level == 0xAB) {
+ // Another way to encode 1b
+ level = 0x09;
+ } else {
+ // Not 1b, just shift
+ level = level >> 4;
+ }
+
+ *profileLevelId = (*profileLevelId & ~levelMask) | level;
+ }
+
+ enum Subprofile {
+ kH264ConstrainedBaseline,
+ kH264Baseline,
+ kH264Main,
+ kH264Extended,
+ kH264High,
+ kH264High10,
+ kH264High42,
+ kH264High44,
+ kH264High10I,
+ kH264High42I,
+ kH264High44I,
+ kH264CALVC44,
+ kH264UnknownSubprofile
+ };
+
+ static Subprofile GetSubprofile(uint32_t profileLevelId) {
+ // Based on Table 5 from RFC 6184:
+ // Profile profile_idc profile-iop
+ // (hexadecimal) (binary)
+
+ // CB 42 (B) x1xx0000
+ // same as: 4D (M) 1xxx0000
+ // same as: 58 (E) 11xx0000
+ // B 42 (B) x0xx0000
+ // same as: 58 (E) 10xx0000
+ // M 4D (M) 0x0x0000
+ // E 58 00xx0000
+ // H 64 00000000
+ // H10 6E 00000000
+ // H42 7A 00000000
+ // H44 F4 00000000
+ // H10I 6E 00010000
+ // H42I 7A 00010000
+ // H44I F4 00010000
+ // C44I 2C 00010000
+
+ if ((profileLevelId & 0xFF4F00) == 0x424000) {
+ // 01001111 (mask, 0x4F)
+ // x1xx0000 (from table)
+ // 01000000 (expected value, 0x40)
+ return kH264ConstrainedBaseline;
+ }
+
+ if ((profileLevelId & 0xFF8F00) == 0x4D8000) {
+ // 10001111 (mask, 0x8F)
+ // 1xxx0000 (from table)
+ // 10000000 (expected value, 0x80)
+ return kH264ConstrainedBaseline;
+ }
+
+ if ((profileLevelId & 0xFFCF00) == 0x58C000) {
+ // 11001111 (mask, 0xCF)
+ // 11xx0000 (from table)
+ // 11000000 (expected value, 0xC0)
+ return kH264ConstrainedBaseline;
+ }
+
+ if ((profileLevelId & 0xFF4F00) == 0x420000) {
+ // 01001111 (mask, 0x4F)
+ // x0xx0000 (from table)
+ // 00000000 (expected value)
+ return kH264Baseline;
+ }
+
+ if ((profileLevelId & 0xFFCF00) == 0x588000) {
+ // 11001111 (mask, 0xCF)
+ // 10xx0000 (from table)
+ // 10000000 (expected value, 0x80)
+ return kH264Baseline;
+ }
+
+ if ((profileLevelId & 0xFFAF00) == 0x4D0000) {
+ // 10101111 (mask, 0xAF)
+ // 0x0x0000 (from table)
+ // 00000000 (expected value)
+ return kH264Main;
+ }
+
+ if ((profileLevelId & 0xFF0000) == 0x580000) {
+ // 11001111 (mask, 0xCF)
+ // 00xx0000 (from table)
+ // 00000000 (expected value)
+ return kH264Extended;
+ }
+
+ if ((profileLevelId & 0xFFFF00) == 0x640000) {
+ return kH264High;
+ }
+
+ if ((profileLevelId & 0xFFFF00) == 0x6E0000) {
+ return kH264High10;
+ }
+
+ if ((profileLevelId & 0xFFFF00) == 0x7A0000) {
+ return kH264High42;
+ }
+
+ if ((profileLevelId & 0xFFFF00) == 0xF40000) {
+ return kH264High44;
+ }
+
+ if ((profileLevelId & 0xFFFF00) == 0x6E1000) {
+ return kH264High10I;
+ }
+
+ if ((profileLevelId & 0xFFFF00) == 0x7A1000) {
+ return kH264High42I;
+ }
+
+ if ((profileLevelId & 0xFFFF00) == 0xF41000) {
+ return kH264High44I;
+ }
+
+ if ((profileLevelId & 0xFFFF00) == 0x2C1000) {
+ return kH264CALVC44;
+ }
+
+ return kH264UnknownSubprofile;
+ }
+
+ virtual bool ParametersMatch(
+ const std::string& fmt,
+ const SdpMediaSection& remoteMsection) const override {
+ if (mName == "H264") {
+ SdpFmtpAttributeList::H264Parameters h264Params(
+ GetH264Parameters(fmt, remoteMsection));
+
+ if (h264Params.packetization_mode != mPacketizationMode) {
+ return false;
+ }
+
+ if (GetSubprofile(h264Params.profile_level_id) !=
+ GetSubprofile(mProfileLevelId)) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ virtual bool RtcpFbRembIsSet() const {
+ for (const auto& fb : mOtherFbTypes) {
+ if (fb.type == SdpRtcpFbAttributeList::kRemb) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ virtual bool RtcpFbTransportCCIsSet() const {
+ for (const auto& fb : mOtherFbTypes) {
+ if (fb.type == SdpRtcpFbAttributeList::kTransportCC) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ virtual void UpdateRedundantEncodings(
+ const std::vector<UniquePtr<JsepCodecDescription>>& codecs) {
+ for (const auto& codec : codecs) {
+ if (codec->Type() == type && codec->mEnabled && codec->mName != "red") {
+ uint16_t pt;
+ if (!SdpHelper::GetPtAsInt(codec->mDefaultPt, &pt)) {
+ continue;
+ }
+ mRedundantEncodings.push_back(pt);
+ }
+ }
+ }
+
+ void EnsureNoDuplicatePayloadTypes(std::set<std::string>& aUsedPts) override {
+ JsepCodecDescription::EnsureNoDuplicatePayloadTypes(aUsedPts);
+ if (mFECEnabled) {
+ mFECEnabled = EnsurePayloadTypeNotDuplicate(aUsedPts, mREDPayloadType) &&
+ EnsurePayloadTypeNotDuplicate(aUsedPts, mULPFECPayloadType);
+ }
+ if (mRtxEnabled) {
+ mRtxEnabled = EnsurePayloadTypeNotDuplicate(aUsedPts, mRtxPayloadType);
+ }
+ }
+
+ JSEP_CODEC_CLONE(JsepVideoCodecDescription)
+
+ std::vector<std::string> mAckFbTypes;
+ std::vector<std::string> mNackFbTypes;
+ std::vector<std::string> mCcmFbTypes;
+ std::vector<SdpRtcpFbAttributeList::Feedback> mOtherFbTypes;
+ bool mTmmbrEnabled;
+ bool mRembEnabled;
+ bool mFECEnabled;
+ bool mTransportCCEnabled;
+ bool mRtxEnabled;
+ std::string mREDPayloadType;
+ std::string mULPFECPayloadType;
+ std::string mRtxPayloadType;
+ std::vector<uint8_t> mRedundantEncodings;
+
+ // H264-specific stuff
+ uint32_t mProfileLevelId;
+ uint32_t mPacketizationMode;
+ std::string mSpropParameterSets;
+};
+
+class JsepApplicationCodecDescription : public JsepCodecDescription {
+ // This is the new draft-21 implementation
+ public:
+ JsepApplicationCodecDescription(const std::string& name, uint16_t channels,
+ uint16_t localPort,
+ uint32_t localMaxMessageSize,
+ bool enabled = true)
+ : JsepCodecDescription("", name, 0, channels, enabled),
+ mLocalPort(localPort),
+ mLocalMaxMessageSize(localMaxMessageSize),
+ mRemotePort(0),
+ mRemoteMaxMessageSize(0),
+ mRemoteMMSSet(false) {}
+
+ static constexpr SdpMediaSection::MediaType type =
+ SdpMediaSection::kApplication;
+
+ SdpMediaSection::MediaType Type() const override { return type; }
+
+ JSEP_CODEC_CLONE(JsepApplicationCodecDescription)
+
+ static UniquePtr<JsepApplicationCodecDescription> CreateDefault() {
+ return MakeUnique<JsepApplicationCodecDescription>(
+ "webrtc-datachannel", WEBRTC_DATACHANNEL_STREAMS_DEFAULT,
+ WEBRTC_DATACHANNEL_PORT_DEFAULT,
+ WEBRTC_DATACHANNEL_MAX_MESSAGE_SIZE_LOCAL);
+ }
+
+ // Override, uses sctpport or sctpmap instead of rtpmap
+ virtual bool Matches(const std::string& fmt,
+ const SdpMediaSection& remoteMsection) const override {
+ if (type != remoteMsection.GetMediaType()) {
+ return false;
+ }
+
+ int sctp_port = remoteMsection.GetSctpPort();
+ bool fmt_matches =
+ nsCRT::strcasecmp(mName.c_str(),
+ remoteMsection.GetFormats()[0].c_str()) == 0;
+ if (sctp_port && fmt_matches) {
+ // New sctp draft 21 format
+ return true;
+ }
+
+ const SdpSctpmapAttributeList::Sctpmap* sctp_map(
+ remoteMsection.GetSctpmap());
+ if (sctp_map) {
+ // Old sctp draft 05 format
+ return nsCRT::strcasecmp(mName.c_str(), sctp_map->name.c_str()) == 0;
+ }
+
+ return false;
+ }
+
+ virtual void AddToMediaSection(SdpMediaSection& msection) const override {
+ if (mEnabled && msection.GetMediaType() == type) {
+ if (mDirection == sdp::kRecv) {
+ msection.AddDataChannel(mName, mLocalPort, mChannels,
+ mLocalMaxMessageSize);
+ }
+
+ AddParametersToMSection(msection);
+ }
+ }
+
+ bool Negotiate(const std::string& pt, const SdpMediaSection& remoteMsection,
+ bool remoteIsOffer,
+ Maybe<const SdpMediaSection&> localMsection) override {
+ JsepCodecDescription::Negotiate(pt, remoteMsection, remoteIsOffer,
+ localMsection);
+
+ uint32_t message_size;
+ mRemoteMMSSet = remoteMsection.GetMaxMessageSize(&message_size);
+ if (mRemoteMMSSet) {
+ mRemoteMaxMessageSize = message_size;
+ } else {
+ mRemoteMaxMessageSize =
+ WEBRTC_DATACHANNEL_MAX_MESSAGE_SIZE_REMOTE_DEFAULT;
+ }
+
+ int sctp_port = remoteMsection.GetSctpPort();
+ if (sctp_port) {
+ mRemotePort = sctp_port;
+ return true;
+ }
+
+ const SdpSctpmapAttributeList::Sctpmap* sctp_map(
+ remoteMsection.GetSctpmap());
+ if (sctp_map) {
+ mRemotePort = std::stoi(sctp_map->pt);
+ return true;
+ }
+
+ return false;
+ }
+
+ // We only support one datachannel per m-section
+ void EnsureNoDuplicatePayloadTypes(std::set<std::string>& aUsedPts) override {
+ }
+
+ void ApplyConfigToFmtp(
+ UniquePtr<SdpFmtpAttributeList::Parameters>& aFmtp) const override{};
+
+ uint16_t mLocalPort;
+ uint32_t mLocalMaxMessageSize;
+ uint16_t mRemotePort;
+ uint32_t mRemoteMaxMessageSize;
+ bool mRemoteMMSSet;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/jsep/JsepSession.h b/dom/media/webrtc/jsep/JsepSession.h
new file mode 100644
index 0000000000..bf68da900b
--- /dev/null
+++ b/dom/media/webrtc/jsep/JsepSession.h
@@ -0,0 +1,287 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _JSEPSESSION_H_
+#define _JSEPSESSION_H_
+
+#include <map>
+#include <string>
+#include <vector>
+#include "mozilla/Attributes.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "nsError.h"
+
+#include "jsep/JsepTransport.h"
+#include "sdp/Sdp.h"
+
+#include "jsep/JsepTransceiver.h"
+
+#include "mozilla/dom/PeerConnectionObserverEnumsBinding.h"
+
+namespace mozilla {
+
+// Forward declarations
+class JsepCodecDescription;
+
+enum JsepSignalingState {
+ kJsepStateStable,
+ kJsepStateHaveLocalOffer,
+ kJsepStateHaveRemoteOffer,
+ kJsepStateHaveLocalPranswer,
+ kJsepStateHaveRemotePranswer,
+ kJsepStateClosed
+};
+
+enum JsepSdpType {
+ kJsepSdpOffer,
+ kJsepSdpAnswer,
+ kJsepSdpPranswer,
+ kJsepSdpRollback
+};
+
+enum JsepDescriptionPendingOrCurrent {
+ kJsepDescriptionCurrent,
+ kJsepDescriptionPending,
+ kJsepDescriptionPendingOrCurrent
+};
+
+struct JsepOAOptions {};
+struct JsepOfferOptions : public JsepOAOptions {
+ Maybe<size_t> mOfferToReceiveAudio;
+ Maybe<size_t> mOfferToReceiveVideo;
+ Maybe<bool> mIceRestart; // currently ignored by JsepSession
+};
+struct JsepAnswerOptions : public JsepOAOptions {};
+
+enum JsepBundlePolicy { kBundleBalanced, kBundleMaxCompat, kBundleMaxBundle };
+
+enum JsepMediaType { kNone = 0, kAudio, kVideo, kAudioVideo };
+
+struct JsepExtmapMediaType {
+ JsepMediaType mMediaType;
+ SdpExtmapAttributeList::Extmap mExtmap;
+};
+
+class JsepSession {
+ public:
+ explicit JsepSession(const std::string& name)
+ : mName(name), mState(kJsepStateStable), mNegotiations(0) {}
+ virtual ~JsepSession() {}
+
+ virtual JsepSession* Clone() const = 0;
+
+ virtual nsresult Init() = 0;
+
+ // Accessors for basic properties.
+ virtual const std::string& GetName() const { return mName; }
+ virtual JsepSignalingState GetState() const { return mState; }
+ virtual uint32_t GetNegotiations() const { return mNegotiations; }
+
+ // Set up the ICE And DTLS data.
+ virtual nsresult SetBundlePolicy(JsepBundlePolicy policy) = 0;
+ virtual bool RemoteIsIceLite() const = 0;
+ virtual std::vector<std::string> GetIceOptions() const = 0;
+
+ virtual nsresult AddDtlsFingerprint(const std::string& algorithm,
+ const std::vector<uint8_t>& value) = 0;
+
+ virtual nsresult AddRtpExtension(
+ JsepMediaType mediaType, const std::string& extensionName,
+ SdpDirectionAttribute::Direction direction) = 0;
+ virtual nsresult AddAudioRtpExtension(
+ const std::string& extensionName,
+ SdpDirectionAttribute::Direction direction) = 0;
+ virtual nsresult AddVideoRtpExtension(
+ const std::string& extensionName,
+ SdpDirectionAttribute::Direction direction) = 0;
+ virtual nsresult AddAudioVideoRtpExtension(
+ const std::string& extensionName,
+ SdpDirectionAttribute::Direction direction) = 0;
+
+ // Kinda gross to be locking down the data structure type like this, but
+ // returning by value is problematic due to the lack of stl move semantics in
+ // our build config, since we can't use UniquePtr in the container. The
+ // alternative is writing a raft of accessor functions that allow arbitrary
+ // manipulation (which will be unwieldy), or allowing functors to be injected
+ // that manipulate the data structure (still pretty unwieldy).
+ virtual std::vector<UniquePtr<JsepCodecDescription>>& Codecs() = 0;
+
+ template <class UnaryFunction>
+ void ForEachCodec(UnaryFunction& function) {
+ std::for_each(Codecs().begin(), Codecs().end(), function);
+ for (auto& transceiver : GetTransceivers()) {
+ transceiver.mSendTrack.ForEachCodec(function);
+ transceiver.mRecvTrack.ForEachCodec(function);
+ }
+ }
+
+ template <class BinaryPredicate>
+ void SortCodecs(BinaryPredicate& sorter) {
+ std::stable_sort(Codecs().begin(), Codecs().end(), sorter);
+ for (auto& transceiver : GetTransceivers()) {
+ transceiver.mSendTrack.SortCodecs(sorter);
+ transceiver.mRecvTrack.SortCodecs(sorter);
+ }
+ }
+
+ // Would be nice to have this return a Maybe containing the return of
+ // |aFunction|, but Maybe cannot contain a void.
+ template <typename UnaryFunction>
+ bool ApplyToTransceiver(const std::string& aId, UnaryFunction&& aFunction) {
+ for (auto& transceiver : GetTransceivers()) {
+ if (transceiver.GetUuid() == aId) {
+ std::forward<UnaryFunction>(aFunction)(transceiver);
+ return true;
+ }
+ }
+ return false;
+ }
+
+ template <typename UnaryFunction>
+ void ForEachTransceiver(UnaryFunction&& aFunction) {
+ for (auto& transceiver : GetTransceivers()) {
+ std::forward<UnaryFunction>(aFunction)(transceiver);
+ }
+ }
+
+ template <typename UnaryFunction>
+ void ForEachTransceiver(UnaryFunction&& aFunction) const {
+ for (const auto& transceiver : GetTransceivers()) {
+ std::forward<UnaryFunction>(aFunction)(transceiver);
+ }
+ }
+
+ Maybe<const JsepTransceiver> GetTransceiver(const std::string& aId) const {
+ for (const auto& transceiver : GetTransceivers()) {
+ if (transceiver.GetUuid() == aId) {
+ return Some(transceiver);
+ }
+ }
+ return Nothing();
+ }
+
+ template <typename MatchFunction>
+ Maybe<const JsepTransceiver> FindTransceiver(MatchFunction&& aFunc) const {
+ for (const auto& transceiver : GetTransceivers()) {
+ if (std::forward<MatchFunction>(aFunc)(transceiver)) {
+ return Some(transceiver);
+ }
+ }
+ return Nothing();
+ }
+
+ bool SetTransceiver(const JsepTransceiver& aNew) {
+ return ApplyToTransceiver(aNew.GetUuid(),
+ [aNew](JsepTransceiver& aOld) { aOld = aNew; });
+ }
+
+ virtual void AddTransceiver(const JsepTransceiver& transceiver) = 0;
+
+ class Result {
+ public:
+ Result() = default;
+ MOZ_IMPLICIT Result(dom::PCError aError) : mError(Some(aError)) {}
+ // TODO(bug 1527916): Need c'tor and members for handling RTCError.
+ Maybe<dom::PCError> mError;
+ };
+
+ // Basic JSEP operations.
+ virtual Result CreateOffer(const JsepOfferOptions& options,
+ std::string* offer) = 0;
+ virtual Result CreateAnswer(const JsepAnswerOptions& options,
+ std::string* answer) = 0;
+ virtual std::string GetLocalDescription(
+ JsepDescriptionPendingOrCurrent type) const = 0;
+ virtual std::string GetRemoteDescription(
+ JsepDescriptionPendingOrCurrent type) const = 0;
+ virtual Result SetLocalDescription(JsepSdpType type,
+ const std::string& sdp) = 0;
+ virtual Result SetRemoteDescription(JsepSdpType type,
+ const std::string& sdp) = 0;
+ virtual Result AddRemoteIceCandidate(const std::string& candidate,
+ const std::string& mid,
+ const Maybe<uint16_t>& level,
+ const std::string& ufrag,
+ std::string* transportId) = 0;
+ virtual nsresult AddLocalIceCandidate(const std::string& candidate,
+ const std::string& transportId,
+ const std::string& ufrag,
+ uint16_t* level, std::string* mid,
+ bool* skipped) = 0;
+ virtual nsresult UpdateDefaultCandidate(
+ const std::string& defaultCandidateAddr, uint16_t defaultCandidatePort,
+ const std::string& defaultRtcpCandidateAddr,
+ uint16_t defaultRtcpCandidatePort, const std::string& transportId) = 0;
+ virtual nsresult Close() = 0;
+
+ // ICE controlling or controlled
+ virtual bool IsIceControlling() const = 0;
+ virtual Maybe<bool> IsPendingOfferer() const = 0;
+ virtual Maybe<bool> IsCurrentOfferer() const = 0;
+ virtual bool IsIceRestarting() const = 0;
+ virtual std::set<std::pair<std::string, std::string>> GetLocalIceCredentials()
+ const = 0;
+
+ virtual const std::string GetLastError() const { return "Error"; }
+
+ virtual const std::vector<std::pair<size_t, std::string>>&
+ GetLastSdpParsingErrors() const = 0;
+
+ static const char* GetStateStr(JsepSignalingState state) {
+ static const char* states[] = {"stable",
+ "have-local-offer",
+ "have-remote-offer",
+ "have-local-pranswer",
+ "have-remote-pranswer",
+ "closed"};
+
+ return states[state];
+ }
+
+ virtual bool CheckNegotiationNeeded() const = 0;
+
+ void CountTracksAndDatachannels(
+ uint16_t (&receiving)[SdpMediaSection::kMediaTypes],
+ uint16_t (&sending)[SdpMediaSection::kMediaTypes]) const {
+ memset(receiving, 0, sizeof(receiving));
+ memset(sending, 0, sizeof(sending));
+
+ for (const auto& transceiver : GetTransceivers()) {
+ if (transceiver.mRecvTrack.GetActive() ||
+ transceiver.GetMediaType() == SdpMediaSection::kApplication) {
+ receiving[transceiver.mRecvTrack.GetMediaType()]++;
+ }
+
+ if (transceiver.mSendTrack.GetActive() ||
+ transceiver.GetMediaType() == SdpMediaSection::kApplication) {
+ sending[transceiver.mSendTrack.GetMediaType()]++;
+ }
+ }
+ }
+
+ virtual void SetDefaultCodecs(
+ const std::vector<UniquePtr<JsepCodecDescription>>& aPreferredCodecs) = 0;
+
+ // See Bug 1642419, this can be removed when all sites are working with RTX.
+ void SetRtxIsAllowed(bool aRtxIsAllowed) { mRtxIsAllowed = aRtxIsAllowed; }
+
+ protected:
+ friend class JsepSessionTest;
+ // Returns transceivers in the order they were added.
+ virtual std::vector<JsepTransceiver>& GetTransceivers() = 0;
+ virtual const std::vector<JsepTransceiver>& GetTransceivers() const = 0;
+
+ const std::string mName;
+ JsepSignalingState mState;
+ uint32_t mNegotiations;
+
+ // See Bug 1642419, this can be removed when all sites are working with RTX.
+ bool mRtxIsAllowed = true;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/jsep/JsepSessionImpl.cpp b/dom/media/webrtc/jsep/JsepSessionImpl.cpp
new file mode 100644
index 0000000000..bb792e7764
--- /dev/null
+++ b/dom/media/webrtc/jsep/JsepSessionImpl.cpp
@@ -0,0 +1,2457 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "jsep/JsepSessionImpl.h"
+
+#include <stdlib.h>
+
+#include <bitset>
+#include <iterator>
+#include <set>
+#include <string>
+#include <utility>
+
+#include "transport/logging.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/net/DataChannelProtocol.h"
+#include "nsDebug.h"
+#include "nspr.h"
+#include "nss.h"
+#include "pk11pub.h"
+
+#include "api/rtp_parameters.h"
+
+#include "jsep/JsepTrack.h"
+#include "jsep/JsepTransport.h"
+#include "sdp/HybridSdpParser.h"
+#include "sdp/SipccSdp.h"
+
+namespace mozilla {
+
+MOZ_MTLOG_MODULE("jsep")
+
+#define JSEP_SET_ERROR(error) \
+ do { \
+ std::ostringstream os; \
+ os << error; \
+ mLastError = os.str(); \
+ MOZ_MTLOG(ML_ERROR, "[" << mName << "]: " << mLastError); \
+ } while (0);
+
+static std::bitset<128> GetForbiddenSdpPayloadTypes() {
+ std::bitset<128> forbidden(0);
+ forbidden[1] = true;
+ forbidden[2] = true;
+ forbidden[19] = true;
+ for (uint16_t i = 64; i < 96; ++i) {
+ forbidden[i] = true;
+ }
+ return forbidden;
+}
+
+static std::string GetRandomHex(size_t words) {
+ std::ostringstream os;
+
+ for (size_t i = 0; i < words; ++i) {
+ uint32_t rand;
+ SECStatus rv = PK11_GenerateRandom(reinterpret_cast<unsigned char*>(&rand),
+ sizeof(rand));
+ if (rv != SECSuccess) {
+ MOZ_CRASH();
+ return "";
+ }
+
+ os << std::hex << std::setfill('0') << std::setw(8) << rand;
+ }
+ return os.str();
+}
+
+JsepSessionImpl::JsepSessionImpl(const JsepSessionImpl& aOrig)
+ : JsepSession(aOrig),
+ JsepSessionCopyableStuff(aOrig),
+ mUuidGen(aOrig.mUuidGen->Clone()),
+ mGeneratedOffer(aOrig.mGeneratedOffer ? aOrig.mGeneratedOffer->Clone()
+ : nullptr),
+ mGeneratedAnswer(aOrig.mGeneratedAnswer ? aOrig.mGeneratedAnswer->Clone()
+ : nullptr),
+ mCurrentLocalDescription(aOrig.mCurrentLocalDescription
+ ? aOrig.mCurrentLocalDescription->Clone()
+ : nullptr),
+ mCurrentRemoteDescription(aOrig.mCurrentRemoteDescription
+ ? aOrig.mCurrentRemoteDescription->Clone()
+ : nullptr),
+ mPendingLocalDescription(aOrig.mPendingLocalDescription
+ ? aOrig.mPendingLocalDescription->Clone()
+ : nullptr),
+ mPendingRemoteDescription(aOrig.mPendingRemoteDescription
+ ? aOrig.mPendingRemoteDescription->Clone()
+ : nullptr),
+ mSdpHelper(&mLastError),
+ mParser(new HybridSdpParser()) {
+ for (const auto& codec : aOrig.mSupportedCodecs) {
+ mSupportedCodecs.emplace_back(codec->Clone());
+ }
+}
+
+nsresult JsepSessionImpl::Init() {
+ mLastError.clear();
+
+ MOZ_ASSERT(!mSessionId, "Init called more than once");
+
+ nsresult rv = SetupIds();
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mEncodeTrackId =
+ Preferences::GetBool("media.peerconnection.sdp.encode_track_id", true);
+
+ mIceUfrag = GetRandomHex(1);
+ mIcePwd = GetRandomHex(4);
+ return NS_OK;
+}
+
+static void GetIceCredentials(
+ const Sdp& aSdp,
+ std::set<std::pair<std::string, std::string>>* aCredentials) {
+ for (size_t i = 0; i < aSdp.GetMediaSectionCount(); ++i) {
+ const SdpAttributeList& attrs = aSdp.GetMediaSection(i).GetAttributeList();
+ if (attrs.HasAttribute(SdpAttribute::kIceUfragAttribute) &&
+ attrs.HasAttribute(SdpAttribute::kIcePwdAttribute)) {
+ aCredentials->insert(
+ std::make_pair(attrs.GetIceUfrag(), attrs.GetIcePwd()));
+ }
+ }
+}
+
+std::set<std::pair<std::string, std::string>>
+JsepSessionImpl::GetLocalIceCredentials() const {
+ std::set<std::pair<std::string, std::string>> result;
+ if (mCurrentLocalDescription) {
+ GetIceCredentials(*mCurrentLocalDescription, &result);
+ }
+ if (mPendingLocalDescription) {
+ GetIceCredentials(*mPendingLocalDescription, &result);
+ }
+ return result;
+}
+
+void JsepSessionImpl::AddTransceiver(const JsepTransceiver& aTransceiver) {
+ mLastError.clear();
+ MOZ_MTLOG(ML_DEBUG,
+ "[" << mName << "]: Adding transceiver " << aTransceiver.GetUuid());
+#ifdef DEBUG
+ if (aTransceiver.GetMediaType() == SdpMediaSection::kApplication) {
+ // Make sure we don't add more than one DataChannel transceiver
+ for (const auto& transceiver : mTransceivers) {
+ MOZ_ASSERT(transceiver.GetMediaType() != SdpMediaSection::kApplication);
+ }
+ }
+#endif
+ mTransceivers.push_back(aTransceiver);
+ InitTransceiver(mTransceivers.back());
+}
+
+void JsepSessionImpl::InitTransceiver(JsepTransceiver& aTransceiver) {
+ mLastError.clear();
+
+ if (aTransceiver.GetMediaType() != SdpMediaSection::kApplication) {
+ // Make sure we have an ssrc. Might already be set.
+ aTransceiver.mSendTrack.EnsureSsrcs(mSsrcGenerator, 1U);
+ aTransceiver.mSendTrack.SetCNAME(mCNAME);
+
+ // Make sure we have identifiers for send track, just in case.
+ // (man I hate this)
+ if (mEncodeTrackId) {
+ aTransceiver.mSendTrack.SetTrackId(aTransceiver.GetUuid());
+ }
+ } else {
+ // Datachannel transceivers should always be sendrecv. Just set it instead
+ // of asserting.
+ aTransceiver.mJsDirection = SdpDirectionAttribute::kSendrecv;
+ }
+
+ aTransceiver.mSendTrack.PopulateCodecs(mSupportedCodecs);
+ aTransceiver.mRecvTrack.PopulateCodecs(mSupportedCodecs);
+ // We do not set mLevel yet, we do that either on createOffer, or setRemote
+}
+
+nsresult JsepSessionImpl::SetBundlePolicy(JsepBundlePolicy policy) {
+ mLastError.clear();
+
+ if (mBundlePolicy == policy) {
+ return NS_OK;
+ }
+
+ if (mCurrentLocalDescription) {
+ JSEP_SET_ERROR(
+ "Changing the bundle policy is only supported before the "
+ "first SetLocalDescription.");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ mBundlePolicy = policy;
+ return NS_OK;
+}
+
+nsresult JsepSessionImpl::AddDtlsFingerprint(
+ const std::string& algorithm, const std::vector<uint8_t>& value) {
+ mLastError.clear();
+ JsepDtlsFingerprint fp;
+
+ fp.mAlgorithm = algorithm;
+ fp.mValue = value;
+
+ mDtlsFingerprints.push_back(fp);
+
+ return NS_OK;
+}
+
+nsresult JsepSessionImpl::AddRtpExtension(
+ JsepMediaType mediaType, const std::string& extensionName,
+ SdpDirectionAttribute::Direction direction) {
+ mLastError.clear();
+
+ for (auto& ext : mRtpExtensions) {
+ if (ext.mExtmap.direction == direction &&
+ ext.mExtmap.extensionname == extensionName) {
+ if (ext.mMediaType != mediaType) {
+ ext.mMediaType = JsepMediaType::kAudioVideo;
+ }
+ return NS_OK;
+ }
+ }
+
+ uint16_t freeEntry = GetNeverUsedExtmapEntry();
+
+ if (freeEntry == 0) {
+ return NS_ERROR_FAILURE;
+ }
+
+ JsepExtmapMediaType extMediaType = {
+ mediaType,
+ {freeEntry, direction,
+ // do we want to specify direction?
+ direction != SdpDirectionAttribute::kSendrecv, extensionName, ""}};
+
+ mRtpExtensions.push_back(extMediaType);
+ return NS_OK;
+}
+
+nsresult JsepSessionImpl::AddAudioRtpExtension(
+ const std::string& extensionName,
+ SdpDirectionAttribute::Direction direction) {
+ return AddRtpExtension(JsepMediaType::kAudio, extensionName, direction);
+}
+
+nsresult JsepSessionImpl::AddVideoRtpExtension(
+ const std::string& extensionName,
+ SdpDirectionAttribute::Direction direction) {
+ return AddRtpExtension(JsepMediaType::kVideo, extensionName, direction);
+}
+
+nsresult JsepSessionImpl::AddAudioVideoRtpExtension(
+ const std::string& extensionName,
+ SdpDirectionAttribute::Direction direction) {
+ return AddRtpExtension(JsepMediaType::kAudioVideo, extensionName, direction);
+}
+
+nsresult JsepSessionImpl::CreateOfferMsection(const JsepOfferOptions& options,
+ JsepTransceiver& transceiver,
+ Sdp* local) {
+ SdpMediaSection::Protocol protocol(
+ SdpHelper::GetProtocolForMediaType(transceiver.GetMediaType()));
+
+ const Sdp* answer(GetAnswer());
+ const SdpMediaSection* lastAnswerMsection = nullptr;
+
+ if (answer &&
+ (local->GetMediaSectionCount() < answer->GetMediaSectionCount())) {
+ lastAnswerMsection =
+ &answer->GetMediaSection(local->GetMediaSectionCount());
+ // Use the protocol the answer used, even if it is not what we would have
+ // used.
+ protocol = lastAnswerMsection->GetProtocol();
+ }
+
+ SdpMediaSection* msection = &local->AddMediaSection(
+ transceiver.GetMediaType(), transceiver.mJsDirection, 0, protocol,
+ sdp::kIPv4, "0.0.0.0");
+
+ // Some of this stuff (eg; mid) sticks around even if disabled
+ if (lastAnswerMsection) {
+ MOZ_ASSERT(lastAnswerMsection->GetMediaType() ==
+ transceiver.GetMediaType());
+ nsresult rv = mSdpHelper.CopyStickyParams(*lastAnswerMsection, msection);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ if (transceiver.IsStopped()) {
+ SdpHelper::DisableMsection(local, msection);
+ return NS_OK;
+ }
+
+ msection->SetPort(9);
+
+ // We don't do this in AddTransportAttributes because that is also used for
+ // making answers, and we don't want to unconditionally set rtcp-mux or
+ // rtcp-rsize there.
+ if (mSdpHelper.HasRtcp(msection->GetProtocol())) {
+ // Set RTCP-MUX.
+ msection->GetAttributeList().SetAttribute(
+ new SdpFlagAttribute(SdpAttribute::kRtcpMuxAttribute));
+ // Set RTCP-RSIZE
+ if (msection->GetMediaType() == SdpMediaSection::MediaType::kVideo &&
+ Preferences::GetBool("media.navigator.video.offer_rtcp_rsize", false)) {
+ msection->GetAttributeList().SetAttribute(
+ new SdpFlagAttribute(SdpAttribute::kRtcpRsizeAttribute));
+ }
+ }
+
+ nsresult rv = AddTransportAttributes(msection, SdpSetupAttribute::kActpass);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ transceiver.mSendTrack.AddToOffer(mSsrcGenerator, msection);
+ transceiver.mRecvTrack.AddToOffer(mSsrcGenerator, msection);
+
+ AddExtmap(msection);
+
+ std::string mid;
+ // We do not set the mid on the transceiver, that happens when a description
+ // is set.
+ if (transceiver.IsAssociated()) {
+ mid = transceiver.GetMid();
+ } else {
+ mid = GetNewMid();
+ }
+
+ msection->GetAttributeList().SetAttribute(
+ new SdpStringAttribute(SdpAttribute::kMidAttribute, mid));
+
+ return NS_OK;
+}
+
+void JsepSessionImpl::SetupBundle(Sdp* sdp) const {
+ std::vector<std::string> mids;
+ std::set<SdpMediaSection::MediaType> observedTypes;
+
+ // This has the effect of changing the bundle level if the first m-section
+ // goes from disabled to enabled. This is kinda inefficient.
+
+ for (size_t i = 0; i < sdp->GetMediaSectionCount(); ++i) {
+ auto& attrs = sdp->GetMediaSection(i).GetAttributeList();
+ if ((sdp->GetMediaSection(i).GetPort() != 0) &&
+ attrs.HasAttribute(SdpAttribute::kMidAttribute)) {
+ bool useBundleOnly = false;
+ switch (mBundlePolicy) {
+ case kBundleMaxCompat:
+ // We don't use bundle-only for max-compat
+ break;
+ case kBundleBalanced:
+ // balanced means we use bundle-only on everything but the first
+ // m-section of a given type
+ if (observedTypes.count(sdp->GetMediaSection(i).GetMediaType())) {
+ useBundleOnly = true;
+ }
+ observedTypes.insert(sdp->GetMediaSection(i).GetMediaType());
+ break;
+ case kBundleMaxBundle:
+ // max-bundle means we use bundle-only on everything but the first
+ // m-section
+ useBundleOnly = !mids.empty();
+ break;
+ }
+
+ if (useBundleOnly) {
+ attrs.SetAttribute(
+ new SdpFlagAttribute(SdpAttribute::kBundleOnlyAttribute));
+ // Set port to 0 for sections with bundle-only attribute. (mjf)
+ sdp->GetMediaSection(i).SetPort(0);
+ }
+
+ mids.push_back(attrs.GetMid());
+ }
+ }
+
+ if (!mids.empty()) {
+ UniquePtr<SdpGroupAttributeList> groupAttr(new SdpGroupAttributeList);
+ groupAttr->PushEntry(SdpGroupAttributeList::kBundle, mids);
+ sdp->GetAttributeList().SetAttribute(groupAttr.release());
+ }
+}
+
+JsepSession::Result JsepSessionImpl::CreateOffer(
+ const JsepOfferOptions& options, std::string* offer) {
+ mLastError.clear();
+
+ if (mState != kJsepStateStable && mState != kJsepStateHaveLocalOffer) {
+ JSEP_SET_ERROR("Cannot create offer in state " << GetStateStr(mState));
+ // Spec doesn't seem to say this is an error. It probably should.
+ return dom::PCError::InvalidStateError;
+ }
+
+ // This is one of those places where CreateOffer sets some state.
+ SetIceRestarting(options.mIceRestart.isSome() && *(options.mIceRestart));
+
+ UniquePtr<Sdp> sdp;
+
+ // Make the basic SDP that is common to offer/answer.
+ nsresult rv = CreateGenericSDP(&sdp);
+ NS_ENSURE_SUCCESS(rv, dom::PCError::OperationError);
+
+ for (size_t level = 0;
+ Maybe<JsepTransceiver> transceiver = GetTransceiverForLocal(level);
+ ++level) {
+ rv = CreateOfferMsection(options, *transceiver, sdp.get());
+ NS_ENSURE_SUCCESS(rv, dom::PCError::OperationError);
+ SetTransceiver(*transceiver);
+ }
+
+ SetupBundle(sdp.get());
+
+ if (mCurrentLocalDescription) {
+ rv = CopyPreviousTransportParams(*GetAnswer(), *mCurrentLocalDescription,
+ *sdp, sdp.get());
+ NS_ENSURE_SUCCESS(rv, dom::PCError::OperationError);
+ }
+
+ *offer = sdp->ToString();
+ mGeneratedOffer = std::move(sdp);
+ ++mSessionVersion;
+ MOZ_MTLOG(ML_DEBUG, "[" << mName << "]: CreateOffer \nSDP=\n" << *offer);
+
+ return Result();
+}
+
+std::string JsepSessionImpl::GetLocalDescription(
+ JsepDescriptionPendingOrCurrent type) const {
+ std::ostringstream os;
+ mozilla::Sdp* sdp = GetParsedLocalDescription(type);
+ if (sdp) {
+ sdp->Serialize(os);
+ }
+ return os.str();
+}
+
+std::string JsepSessionImpl::GetRemoteDescription(
+ JsepDescriptionPendingOrCurrent type) const {
+ std::ostringstream os;
+ mozilla::Sdp* sdp = GetParsedRemoteDescription(type);
+ if (sdp) {
+ sdp->Serialize(os);
+ }
+ return os.str();
+}
+
+void JsepSessionImpl::AddExtmap(SdpMediaSection* msection) {
+ auto extensions = GetRtpExtensions(*msection);
+
+ if (!extensions.empty()) {
+ SdpExtmapAttributeList* extmap = new SdpExtmapAttributeList;
+ extmap->mExtmaps = extensions;
+ msection->GetAttributeList().SetAttribute(extmap);
+ }
+}
+
+std::vector<SdpExtmapAttributeList::Extmap> JsepSessionImpl::GetRtpExtensions(
+ const SdpMediaSection& msection) {
+ std::vector<SdpExtmapAttributeList::Extmap> result;
+ JsepMediaType mediaType = JsepMediaType::kNone;
+ switch (msection.GetMediaType()) {
+ case SdpMediaSection::kAudio:
+ mediaType = JsepMediaType::kAudio;
+ break;
+ case SdpMediaSection::kVideo:
+ mediaType = JsepMediaType::kVideo;
+ if (msection.GetAttributeList().HasAttribute(
+ SdpAttribute::kRidAttribute)) {
+ // We need RID support
+ // TODO: Would it be worth checking that the direction is sane?
+ AddVideoRtpExtension(webrtc::RtpExtension::kRidUri,
+ SdpDirectionAttribute::kSendonly);
+
+ if (mRtxIsAllowed &&
+ Preferences::GetBool("media.peerconnection.video.use_rtx", false)) {
+ AddVideoRtpExtension(webrtc::RtpExtension::kRepairedRidUri,
+ SdpDirectionAttribute::kSendonly);
+ }
+ }
+ break;
+ default:;
+ }
+ if (mediaType != JsepMediaType::kNone) {
+ for (auto ext = mRtpExtensions.begin(); ext != mRtpExtensions.end();
+ ++ext) {
+ if (ext->mMediaType == mediaType ||
+ ext->mMediaType == JsepMediaType::kAudioVideo) {
+ result.push_back(ext->mExtmap);
+ }
+ }
+ }
+ return result;
+}
+
+std::string JsepSessionImpl::GetNewMid() {
+ std::string mid;
+
+ do {
+ std::ostringstream osMid;
+ osMid << mMidCounter++;
+ mid = osMid.str();
+ } while (mUsedMids.count(mid));
+
+ mUsedMids.insert(mid);
+ return mid;
+}
+
+void JsepSessionImpl::AddCommonExtmaps(const SdpMediaSection& remoteMsection,
+ SdpMediaSection* msection) {
+ auto negotiatedRtpExtensions = GetRtpExtensions(*msection);
+ mSdpHelper.NegotiateAndAddExtmaps(remoteMsection, negotiatedRtpExtensions,
+ msection);
+}
+
+uint16_t JsepSessionImpl::GetNeverUsedExtmapEntry() {
+ uint16_t result = 1;
+
+ // Walk the set in order, and return the first "hole" we find
+ for (const auto used : mExtmapEntriesEverUsed) {
+ if (result != used) {
+ MOZ_ASSERT(result < used);
+ break;
+ }
+
+ // RFC 5285 says entries >= 4096 are used in offers to force the answerer
+ // to pick, so we do not want to actually use these
+ if (used == 4095) {
+ JSEP_SET_ERROR(
+ "Too many rtp extensions have been added. "
+ "That's 4095. Who _does_ that?");
+ return 0;
+ }
+
+ result = used + 1;
+ }
+
+ mExtmapEntriesEverUsed.insert(result);
+ return result;
+}
+
+JsepSession::Result JsepSessionImpl::CreateAnswer(
+ const JsepAnswerOptions& options, std::string* answer) {
+ mLastError.clear();
+
+ if (mState != kJsepStateHaveRemoteOffer) {
+ JSEP_SET_ERROR("Cannot create answer in state " << GetStateStr(mState));
+ return dom::PCError::InvalidStateError;
+ }
+
+ UniquePtr<Sdp> sdp;
+
+ // Make the basic SDP that is common to offer/answer.
+ nsresult rv = CreateGenericSDP(&sdp);
+ NS_ENSURE_SUCCESS(rv, dom::PCError::OperationError);
+
+ const Sdp& offer = *mPendingRemoteDescription;
+
+ // Copy the bundle groups into our answer
+ UniquePtr<SdpGroupAttributeList> groupAttr(new SdpGroupAttributeList);
+ mSdpHelper.GetBundleGroups(offer, &groupAttr->mGroups);
+ sdp->GetAttributeList().SetAttribute(groupAttr.release());
+
+ for (size_t i = 0; i < offer.GetMediaSectionCount(); ++i) {
+ // The transceivers are already in place, due to setRemote
+ Maybe<JsepTransceiver> transceiver(GetTransceiverForLevel(i));
+ if (!transceiver) {
+ JSEP_SET_ERROR("No transceiver for level " << i);
+ MOZ_ASSERT(false);
+ return dom::PCError::OperationError;
+ }
+ rv = CreateAnswerMsection(options, *transceiver, offer.GetMediaSection(i),
+ sdp.get());
+ NS_ENSURE_SUCCESS(rv, dom::PCError::OperationError);
+ SetTransceiver(*transceiver);
+ }
+
+ // Ensure that each bundle-group starts with a mid that has a transport, in
+ // case we've disabled what the offerer wanted to use. If the group doesn't
+ // contain anything that has a transport, remove it.
+ groupAttr.reset(new SdpGroupAttributeList);
+ std::vector<SdpGroupAttributeList::Group> bundleGroups;
+ mSdpHelper.GetBundleGroups(*sdp, &bundleGroups);
+ for (auto& group : bundleGroups) {
+ for (auto& mid : group.tags) {
+ const SdpMediaSection* msection =
+ mSdpHelper.FindMsectionByMid(offer, mid);
+
+ if (msection && !msection->GetAttributeList().HasAttribute(
+ SdpAttribute::kBundleOnlyAttribute)) {
+ std::swap(group.tags[0], mid);
+ groupAttr->mGroups.push_back(group);
+ break;
+ }
+ }
+ }
+ sdp->GetAttributeList().SetAttribute(groupAttr.release());
+
+ if (mCurrentLocalDescription) {
+ // per discussion with bwc, 3rd parm here should be offer, not *sdp. (mjf)
+ rv = CopyPreviousTransportParams(*GetAnswer(), *mCurrentRemoteDescription,
+ offer, sdp.get());
+ NS_ENSURE_SUCCESS(rv, dom::PCError::OperationError);
+ }
+
+ *answer = sdp->ToString();
+ mGeneratedAnswer = std::move(sdp);
+ ++mSessionVersion;
+ MOZ_MTLOG(ML_DEBUG, "[" << mName << "]: CreateAnswer \nSDP=\n" << *answer);
+
+ return Result();
+}
+
+nsresult JsepSessionImpl::CreateAnswerMsection(
+ const JsepAnswerOptions& options, JsepTransceiver& transceiver,
+ const SdpMediaSection& remoteMsection, Sdp* sdp) {
+ MOZ_ASSERT(transceiver.GetMediaType() == remoteMsection.GetMediaType());
+ SdpDirectionAttribute::Direction direction =
+ reverse(remoteMsection.GetDirection()) & transceiver.mJsDirection;
+ SdpMediaSection& msection =
+ sdp->AddMediaSection(remoteMsection.GetMediaType(), direction, 9,
+ remoteMsection.GetProtocol(), sdp::kIPv4, "0.0.0.0");
+
+ nsresult rv = mSdpHelper.CopyStickyParams(remoteMsection, &msection);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (mSdpHelper.MsectionIsDisabled(remoteMsection) ||
+ // JS might have stopped this
+ transceiver.IsStopped()) {
+ SdpHelper::DisableMsection(sdp, &msection);
+ return NS_OK;
+ }
+
+ MOZ_ASSERT(transceiver.IsAssociated());
+ if (msection.GetAttributeList().GetMid().empty()) {
+ msection.GetAttributeList().SetAttribute(new SdpStringAttribute(
+ SdpAttribute::kMidAttribute, transceiver.GetMid()));
+ }
+
+ MOZ_ASSERT(transceiver.GetMid() == msection.GetAttributeList().GetMid());
+
+ SdpSetupAttribute::Role role;
+ if (transceiver.mTransport.mDtls && !IsIceRestarting()) {
+ role = (transceiver.mTransport.mDtls->mRole ==
+ JsepDtlsTransport::kJsepDtlsClient)
+ ? SdpSetupAttribute::kActive
+ : SdpSetupAttribute::kPassive;
+ } else {
+ rv = DetermineAnswererSetupRole(remoteMsection, &role);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+
+ rv = AddTransportAttributes(&msection, role);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ transceiver.mSendTrack.AddToAnswer(remoteMsection, mSsrcGenerator, &msection);
+ transceiver.mRecvTrack.AddToAnswer(remoteMsection, mSsrcGenerator, &msection);
+
+ // Add extmap attributes. This logic will probably be moved to the track,
+ // since it can be specified on a per-sender basis in JS.
+ // We will need some validation to ensure that the ids are identical for
+ // RTP streams that are bundled together, though (bug 1406529).
+ AddCommonExtmaps(remoteMsection, &msection);
+
+ if (msection.GetFormats().empty()) {
+ // Could not negotiate anything. Disable m-section.
+ SdpHelper::DisableMsection(sdp, &msection);
+ }
+
+ return NS_OK;
+}
+
+nsresult JsepSessionImpl::DetermineAnswererSetupRole(
+ const SdpMediaSection& remoteMsection, SdpSetupAttribute::Role* rolep) {
+ // Determine the role.
+ // RFC 5763 says:
+ //
+ // The endpoint MUST use the setup attribute defined in [RFC4145].
+ // The endpoint that is the offerer MUST use the setup attribute
+ // value of setup:actpass and be prepared to receive a client_hello
+ // before it receives the answer. The answerer MUST use either a
+ // setup attribute value of setup:active or setup:passive. Note that
+ // if the answerer uses setup:passive, then the DTLS handshake will
+ // not begin until the answerer is received, which adds additional
+ // latency. setup:active allows the answer and the DTLS handshake to
+ // occur in parallel. Thus, setup:active is RECOMMENDED. Whichever
+ // party is active MUST initiate a DTLS handshake by sending a
+ // ClientHello over each flow (host/port quartet).
+ //
+ // We default to assuming that the offerer is passive and we are active.
+ SdpSetupAttribute::Role role = SdpSetupAttribute::kActive;
+
+ if (remoteMsection.GetAttributeList().HasAttribute(
+ SdpAttribute::kSetupAttribute)) {
+ switch (remoteMsection.GetAttributeList().GetSetup().mRole) {
+ case SdpSetupAttribute::kActive:
+ role = SdpSetupAttribute::kPassive;
+ break;
+ case SdpSetupAttribute::kPassive:
+ case SdpSetupAttribute::kActpass:
+ role = SdpSetupAttribute::kActive;
+ break;
+ case SdpSetupAttribute::kHoldconn:
+ // This should have been caught by ParseSdp
+ MOZ_ASSERT(false);
+ JSEP_SET_ERROR(
+ "The other side used an illegal setup attribute"
+ " (\"holdconn\").");
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+
+ *rolep = role;
+ return NS_OK;
+}
+
+JsepSession::Result JsepSessionImpl::SetLocalDescription(
+ JsepSdpType type, const std::string& constSdp) {
+ mLastError.clear();
+ std::string sdp = constSdp;
+
+ MOZ_MTLOG(ML_DEBUG, "[" << mName << "]: SetLocalDescription type=" << type
+ << "\nSDP=\n"
+ << sdp);
+
+ switch (type) {
+ case kJsepSdpOffer:
+ if (!mGeneratedOffer) {
+ JSEP_SET_ERROR(
+ "Cannot set local offer when createOffer has not been called.");
+ return dom::PCError::InvalidModificationError;
+ }
+ if (sdp.empty()) {
+ sdp = mGeneratedOffer->ToString();
+ }
+ if (mState == kJsepStateHaveLocalOffer) {
+ // Rollback previous offer before applying the new one.
+ SetLocalDescription(kJsepSdpRollback, "");
+ MOZ_ASSERT(mState == kJsepStateStable);
+ }
+ break;
+ case kJsepSdpAnswer:
+ case kJsepSdpPranswer:
+ if (!mGeneratedAnswer) {
+ JSEP_SET_ERROR(
+ "Cannot set local answer when createAnswer has not been called.");
+ return dom::PCError::InvalidModificationError;
+ }
+ if (sdp.empty()) {
+ sdp = mGeneratedAnswer->ToString();
+ }
+ break;
+ case kJsepSdpRollback:
+ if (mState != kJsepStateHaveLocalOffer) {
+ JSEP_SET_ERROR("Cannot rollback local description in "
+ << GetStateStr(mState));
+ // Currently, spec allows this in any state except stable, and
+ // sRD(rollback) and sLD(rollback) do exactly the same thing.
+ return dom::PCError::InvalidStateError;
+ }
+
+ mPendingLocalDescription.reset();
+ SetState(kJsepStateStable);
+ RollbackLocalOffer();
+ return Result();
+ }
+
+ switch (mState) {
+ case kJsepStateStable:
+ if (type != kJsepSdpOffer) {
+ JSEP_SET_ERROR("Cannot set local answer in state "
+ << GetStateStr(mState));
+ return dom::PCError::InvalidStateError;
+ }
+ break;
+ case kJsepStateHaveRemoteOffer:
+ if (type != kJsepSdpAnswer && type != kJsepSdpPranswer) {
+ JSEP_SET_ERROR("Cannot set local offer in state "
+ << GetStateStr(mState));
+ return dom::PCError::InvalidStateError;
+ }
+ break;
+ default:
+ JSEP_SET_ERROR("Cannot set local offer or answer in state "
+ << GetStateStr(mState));
+ return dom::PCError::InvalidStateError;
+ }
+
+ UniquePtr<Sdp> parsed;
+ nsresult rv = ParseSdp(sdp, &parsed);
+ // Needs to be RTCError with sdp-syntax-error
+ NS_ENSURE_SUCCESS(rv, dom::PCError::OperationError);
+
+ // Check that content hasn't done anything unsupported with the SDP
+ rv = ValidateLocalDescription(*parsed, type);
+ NS_ENSURE_SUCCESS(rv, dom::PCError::InvalidModificationError);
+
+ switch (type) {
+ case kJsepSdpOffer:
+ rv = ValidateOffer(*parsed);
+ break;
+ case kJsepSdpAnswer:
+ case kJsepSdpPranswer:
+ rv = ValidateAnswer(*mPendingRemoteDescription, *parsed);
+ break;
+ case kJsepSdpRollback:
+ MOZ_CRASH(); // Handled above
+ }
+ NS_ENSURE_SUCCESS(rv, dom::PCError::InvalidAccessError);
+
+ if (type == kJsepSdpOffer) {
+ // Save in case we need to rollback
+ mOldTransceivers = mTransceivers;
+ }
+
+ SdpHelper::BundledMids bundledMids;
+ rv = mSdpHelper.GetBundledMids(*parsed, &bundledMids);
+ NS_ENSURE_SUCCESS(rv, dom::PCError::OperationError);
+
+ SdpHelper::BundledMids remoteBundledMids;
+ if (type != kJsepSdpOffer) {
+ rv = mSdpHelper.GetBundledMids(*mPendingRemoteDescription,
+ &remoteBundledMids);
+ NS_ENSURE_SUCCESS(rv, dom::PCError::OperationError);
+ }
+
+ for (size_t i = 0; i < parsed->GetMediaSectionCount(); ++i) {
+ Maybe<JsepTransceiver> transceiver(GetTransceiverForLevel(i));
+ if (!transceiver) {
+ MOZ_ASSERT(false);
+ JSEP_SET_ERROR("No transceiver for level " << i);
+ return dom::PCError::OperationError;
+ }
+
+ const auto& msection = parsed->GetMediaSection(i);
+ transceiver->Associate(msection.GetAttributeList().GetMid());
+ transceiver->mRecvTrack.RecvTrackSetLocal(msection);
+
+ if (mSdpHelper.MsectionIsDisabled(msection)) {
+ transceiver->mTransport.Close();
+ SetTransceiver(*transceiver);
+ continue;
+ }
+
+ bool hasOwnTransport = mSdpHelper.OwnsTransport(
+ msection, bundledMids,
+ (type == kJsepSdpOffer) ? sdp::kOffer : sdp::kAnswer);
+ if (type != kJsepSdpOffer) {
+ const auto& remoteMsection =
+ mPendingRemoteDescription->GetMediaSection(i);
+ // Don't allow the answer to override what the offer allowed for
+ hasOwnTransport &= mSdpHelper.OwnsTransport(
+ remoteMsection, remoteBundledMids, sdp::kOffer);
+ }
+
+ if (hasOwnTransport) {
+ EnsureHasOwnTransport(parsed->GetMediaSection(i), *transceiver);
+ }
+
+ if (type == kJsepSdpOffer) {
+ if (!hasOwnTransport) {
+ auto it = bundledMids.find(transceiver->GetMid());
+ if (it != bundledMids.end()) {
+ transceiver->SetBundleLevel(it->second->GetLevel());
+ }
+ }
+ } else {
+ auto it = remoteBundledMids.find(transceiver->GetMid());
+ if (it != remoteBundledMids.end()) {
+ transceiver->SetBundleLevel(it->second->GetLevel());
+ }
+ }
+ SetTransceiver(*transceiver);
+ }
+
+ CopyBundleTransports();
+
+ switch (type) {
+ case kJsepSdpOffer:
+ rv = SetLocalDescriptionOffer(std::move(parsed));
+ break;
+ case kJsepSdpAnswer:
+ case kJsepSdpPranswer:
+ rv = SetLocalDescriptionAnswer(type, std::move(parsed));
+ break;
+ case kJsepSdpRollback:
+ MOZ_CRASH(); // Handled above
+ }
+
+ NS_ENSURE_SUCCESS(rv, dom::PCError::OperationError);
+ return Result();
+}
+
+nsresult JsepSessionImpl::SetLocalDescriptionOffer(UniquePtr<Sdp> offer) {
+ MOZ_ASSERT(mState == kJsepStateStable);
+ mPendingLocalDescription = std::move(offer);
+ mIsPendingOfferer = Some(true);
+ SetState(kJsepStateHaveLocalOffer);
+ return NS_OK;
+}
+
+nsresult JsepSessionImpl::SetLocalDescriptionAnswer(JsepSdpType type,
+ UniquePtr<Sdp> answer) {
+ MOZ_ASSERT(mState == kJsepStateHaveRemoteOffer);
+ mPendingLocalDescription = std::move(answer);
+
+ nsresult rv = HandleNegotiatedSession(mPendingLocalDescription,
+ mPendingRemoteDescription);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mCurrentRemoteDescription = std::move(mPendingRemoteDescription);
+ mCurrentLocalDescription = std::move(mPendingLocalDescription);
+ MOZ_ASSERT(mIsPendingOfferer.isSome() && !*mIsPendingOfferer);
+ mIsPendingOfferer.reset();
+ mIsCurrentOfferer = Some(false);
+
+ SetState(kJsepStateStable);
+ return NS_OK;
+}
+
+JsepSession::Result JsepSessionImpl::SetRemoteDescription(
+ JsepSdpType type, const std::string& sdp) {
+ mLastError.clear();
+
+ MOZ_MTLOG(ML_DEBUG, "[" << mName << "]: SetRemoteDescription type=" << type
+ << "\nSDP=\n"
+ << sdp);
+
+ if (mState == kJsepStateHaveRemoteOffer && type == kJsepSdpOffer) {
+ // Rollback previous offer before applying the new one.
+ SetRemoteDescription(kJsepSdpRollback, "");
+ MOZ_ASSERT(mState == kJsepStateStable);
+ }
+
+ if (type == kJsepSdpRollback) {
+ if (mState != kJsepStateHaveRemoteOffer) {
+ JSEP_SET_ERROR("Cannot rollback remote description in "
+ << GetStateStr(mState));
+ return dom::PCError::InvalidStateError;
+ }
+
+ mPendingRemoteDescription.reset();
+ SetState(kJsepStateStable);
+ RollbackRemoteOffer();
+
+ return Result();
+ }
+
+ switch (mState) {
+ case kJsepStateStable:
+ if (type != kJsepSdpOffer) {
+ JSEP_SET_ERROR("Cannot set remote answer in state "
+ << GetStateStr(mState));
+ return dom::PCError::InvalidStateError;
+ }
+ break;
+ case kJsepStateHaveLocalOffer:
+ case kJsepStateHaveRemotePranswer:
+ if (type != kJsepSdpAnswer && type != kJsepSdpPranswer) {
+ JSEP_SET_ERROR("Cannot set remote offer in state "
+ << GetStateStr(mState));
+ return dom::PCError::InvalidStateError;
+ }
+ break;
+ default:
+ JSEP_SET_ERROR("Cannot set remote offer or answer in current state "
+ << GetStateStr(mState));
+ return dom::PCError::InvalidStateError;
+ }
+
+ // Parse.
+ UniquePtr<Sdp> parsed;
+ nsresult rv = ParseSdp(sdp, &parsed);
+ // Needs to be RTCError with sdp-syntax-error
+ NS_ENSURE_SUCCESS(rv, dom::PCError::OperationError);
+
+ rv = ValidateRemoteDescription(*parsed);
+ NS_ENSURE_SUCCESS(rv, dom::PCError::InvalidAccessError);
+
+ switch (type) {
+ case kJsepSdpOffer:
+ rv = ValidateOffer(*parsed);
+ break;
+ case kJsepSdpAnswer:
+ case kJsepSdpPranswer:
+ rv = ValidateAnswer(*mPendingLocalDescription, *parsed);
+ break;
+ case kJsepSdpRollback:
+ MOZ_CRASH(); // Handled above
+ }
+ NS_ENSURE_SUCCESS(rv, dom::PCError::InvalidAccessError);
+
+ bool iceLite =
+ parsed->GetAttributeList().HasAttribute(SdpAttribute::kIceLiteAttribute);
+
+ // check for mismatch ufrag/pwd indicating ice restart
+ // can't just check the first one because it might be disabled
+ bool iceRestarting = false;
+ if (mCurrentRemoteDescription.get()) {
+ for (size_t i = 0; !iceRestarting &&
+ i < mCurrentRemoteDescription->GetMediaSectionCount();
+ ++i) {
+ const SdpMediaSection& newMsection = parsed->GetMediaSection(i);
+ const SdpMediaSection& oldMsection =
+ mCurrentRemoteDescription->GetMediaSection(i);
+
+ if (mSdpHelper.MsectionIsDisabled(newMsection) ||
+ mSdpHelper.MsectionIsDisabled(oldMsection)) {
+ continue;
+ }
+
+ iceRestarting = mSdpHelper.IceCredentialsDiffer(newMsection, oldMsection);
+ }
+ }
+
+ std::vector<std::string> iceOptions;
+ if (parsed->GetAttributeList().HasAttribute(
+ SdpAttribute::kIceOptionsAttribute)) {
+ iceOptions = parsed->GetAttributeList().GetIceOptions().mValues;
+ }
+
+ if (type == kJsepSdpOffer) {
+ // Save in case we need to rollback.
+ mOldTransceivers = mTransceivers;
+ for (auto& transceiver : mTransceivers) {
+ if (!transceiver.IsNegotiated()) {
+ // We chose a level for this transceiver, but never negotiated it.
+ // Discard this state.
+ transceiver.ClearLevel();
+ }
+ }
+ }
+
+ // TODO(bug 1095780): Note that we create remote tracks even when
+ // They contain only codecs we can't negotiate or other craziness.
+ rv = UpdateTransceiversFromRemoteDescription(*parsed);
+ NS_ENSURE_SUCCESS(rv, dom::PCError::OperationError);
+
+ for (size_t i = 0; i < parsed->GetMediaSectionCount(); ++i) {
+ MOZ_ASSERT(GetTransceiverForLevel(i));
+ }
+
+ switch (type) {
+ case kJsepSdpOffer:
+ rv = SetRemoteDescriptionOffer(std::move(parsed));
+ break;
+ case kJsepSdpAnswer:
+ case kJsepSdpPranswer:
+ rv = SetRemoteDescriptionAnswer(type, std::move(parsed));
+ break;
+ case kJsepSdpRollback:
+ MOZ_CRASH(); // Handled above
+ }
+
+ NS_ENSURE_SUCCESS(rv, dom::PCError::OperationError);
+
+ mRemoteIsIceLite = iceLite;
+ mIceOptions = iceOptions;
+ SetIceRestarting(iceRestarting);
+ return Result();
+}
+
+nsresult JsepSessionImpl::HandleNegotiatedSession(
+ const UniquePtr<Sdp>& local, const UniquePtr<Sdp>& remote) {
+ // local ufrag/pwd has been negotiated; we will never go back to the old ones
+ mOldIceUfrag.clear();
+ mOldIcePwd.clear();
+
+ bool remoteIceLite =
+ remote->GetAttributeList().HasAttribute(SdpAttribute::kIceLiteAttribute);
+
+ mIceControlling = remoteIceLite || *mIsPendingOfferer;
+
+ const Sdp& answer = *mIsPendingOfferer ? *remote : *local;
+
+ SdpHelper::BundledMids bundledMids;
+ nsresult rv = mSdpHelper.GetBundledMids(answer, &bundledMids);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // First, set the bundle level on the transceivers
+ for (auto& [mid, transportOwner] : bundledMids) {
+ Maybe<JsepTransceiver> bundledTransceiver = GetTransceiverForMid(mid);
+ if (!bundledTransceiver) {
+ JSEP_SET_ERROR("No transceiver for bundled mid " << mid);
+ return NS_ERROR_INVALID_ARG;
+ }
+ bundledTransceiver->SetBundleLevel(transportOwner->GetLevel());
+ SetTransceiver(*bundledTransceiver);
+ }
+
+ // Now walk through the m-sections, perform negotiation, and update the
+ // transceivers.
+ for (size_t i = 0; i < local->GetMediaSectionCount(); ++i) {
+ Maybe<JsepTransceiver> transceiver(GetTransceiverForLevel(i));
+ if (!transceiver) {
+ MOZ_ASSERT(false);
+ JSEP_SET_ERROR("No transceiver for level " << i);
+ return NS_ERROR_FAILURE;
+ }
+
+ // Skip disabled m-sections.
+ if (answer.GetMediaSection(i).GetPort() == 0) {
+ transceiver->mTransport.Close();
+ transceiver->Stop();
+ transceiver->Disassociate();
+ transceiver->ClearBundleLevel();
+ transceiver->mSendTrack.SetActive(false);
+ transceiver->mRecvTrack.SetActive(false);
+ transceiver->SetCanRecycle();
+ SetTransceiver(*transceiver);
+ // Do not clear mLevel yet! That will happen on the next negotiation.
+ continue;
+ }
+
+ rv = MakeNegotiatedTransceiver(remote->GetMediaSection(i),
+ local->GetMediaSection(i), *transceiver);
+ NS_ENSURE_SUCCESS(rv, rv);
+ SetTransceiver(*transceiver);
+ }
+
+ CopyBundleTransports();
+
+ std::vector<JsepTrack*> remoteTracks;
+ for (auto& transceiver : mTransceivers) {
+ remoteTracks.push_back(&transceiver.mRecvTrack);
+ }
+ JsepTrack::SetUniquePayloadTypes(remoteTracks);
+
+ mNegotiations++;
+
+ mGeneratedAnswer.reset();
+ mGeneratedOffer.reset();
+
+ return NS_OK;
+}
+
+nsresult JsepSessionImpl::MakeNegotiatedTransceiver(
+ const SdpMediaSection& remote, const SdpMediaSection& local,
+ JsepTransceiver& transceiver) {
+ const SdpMediaSection& answer = *mIsPendingOfferer ? remote : local;
+
+ bool sending = false;
+ bool receiving = false;
+
+ // JS could stop the transceiver after the answer was created.
+ if (!transceiver.IsStopped()) {
+ if (*mIsPendingOfferer) {
+ receiving = answer.IsSending();
+ sending = answer.IsReceiving();
+ } else {
+ sending = answer.IsSending();
+ receiving = answer.IsReceiving();
+ }
+ }
+
+ MOZ_MTLOG(ML_DEBUG, "[" << mName << "]: Negotiated m= line"
+ << " index=" << local.GetLevel() << " type="
+ << local.GetMediaType() << " sending=" << sending
+ << " receiving=" << receiving);
+
+ transceiver.SetNegotiated();
+
+ // Ensure that this is finalized in case we need to copy it below
+ nsresult rv =
+ FinalizeTransport(remote.GetAttributeList(), answer.GetAttributeList(),
+ &transceiver.mTransport);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ transceiver.mSendTrack.SetActive(sending);
+ rv = transceiver.mSendTrack.Negotiate(answer, remote, local);
+ if (NS_FAILED(rv)) {
+ JSEP_SET_ERROR("Answer had no codecs in common with offer in m-section "
+ << local.GetLevel());
+ return rv;
+ }
+
+ JsepTrack& recvTrack = transceiver.mRecvTrack;
+ recvTrack.SetActive(receiving);
+ rv = recvTrack.Negotiate(answer, remote, local);
+ if (NS_FAILED(rv)) {
+ JSEP_SET_ERROR("Answer had no codecs in common with offer in m-section "
+ << local.GetLevel());
+ return rv;
+ }
+
+ if (transceiver.HasBundleLevel() && recvTrack.GetSsrcs().empty() &&
+ recvTrack.GetMediaType() != SdpMediaSection::kApplication) {
+ // TODO(bug 1105005): Once we have urn:ietf:params:rtp-hdrext:sdes:mid
+ // support, we should only fire this warning if that extension was not
+ // negotiated.
+ MOZ_MTLOG(ML_ERROR, "[" << mName
+ << "]: Bundled m-section has no ssrc "
+ "attributes. This may cause media packets to be "
+ "dropped.");
+ }
+
+ if (transceiver.mTransport.mComponents == 2) {
+ // RTCP MUX or not.
+ // TODO(bug 1095743): verify that the PTs are consistent with mux.
+ MOZ_MTLOG(ML_DEBUG, "[" << mName << "]: RTCP-MUX is off");
+ }
+
+ if (answer.GetAttributeList().HasAttribute(SdpAttribute::kExtmapAttribute)) {
+ const auto extmaps = answer.GetAttributeList().GetExtmap().mExtmaps;
+ for (const auto& negotiatedExtension : extmaps) {
+ if (negotiatedExtension.entry == 0) {
+ MOZ_ASSERT(false, "This should have been caught sooner");
+ continue;
+ }
+
+ mExtmapEntriesEverNegotiated[negotiatedExtension.entry] =
+ negotiatedExtension.extensionname;
+
+ for (auto& originalExtension : mRtpExtensions) {
+ if (negotiatedExtension.extensionname ==
+ originalExtension.mExtmap.extensionname) {
+ // Update extmap to match what was negotiated
+ originalExtension.mExtmap.entry = negotiatedExtension.entry;
+ mExtmapEntriesEverUsed.insert(negotiatedExtension.entry);
+ } else if (originalExtension.mExtmap.entry ==
+ negotiatedExtension.entry) {
+ // If this extmap entry was claimed for a different extension, update
+ // it to a new value so we don't end up with a duplicate.
+ originalExtension.mExtmap.entry = GetNeverUsedExtmapEntry();
+ }
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+void JsepSessionImpl::EnsureHasOwnTransport(const SdpMediaSection& msection,
+ JsepTransceiver& transceiver) {
+ JsepTransport& transport = transceiver.mTransport;
+
+ if (!transceiver.HasOwnTransport()) {
+ // Transceiver didn't own this transport last time, it won't now either
+ transport.Close();
+ }
+
+ transport.mLocalUfrag = msection.GetAttributeList().GetIceUfrag();
+ transport.mLocalPwd = msection.GetAttributeList().GetIcePwd();
+
+ transceiver.ClearBundleLevel();
+
+ if (!transport.mComponents) {
+ if (mSdpHelper.HasRtcp(msection.GetProtocol())) {
+ transport.mComponents = 2;
+ } else {
+ transport.mComponents = 1;
+ }
+ }
+
+ if (transport.mTransportId.empty()) {
+ // TODO: Once we use different ICE ufrag/pass for each m-section, we can
+ // use that here.
+ std::ostringstream os;
+ os << "transport_" << mTransportIdCounter++;
+ transport.mTransportId = os.str();
+ }
+}
+
+void JsepSessionImpl::CopyBundleTransports() {
+ for (auto& transceiver : mTransceivers) {
+ if (transceiver.HasBundleLevel()) {
+ MOZ_MTLOG(ML_DEBUG,
+ "[" << mName << "] Transceiver " << transceiver.GetLevel()
+ << " is in a bundle; transceiver "
+ << transceiver.BundleLevel() << " owns the transport.");
+ Maybe<const JsepTransceiver> transportOwner =
+ GetTransceiverForLevel(transceiver.BundleLevel());
+ MOZ_ASSERT(transportOwner);
+ if (transportOwner) {
+ transceiver.mTransport = transportOwner->mTransport;
+ }
+ } else if (transceiver.HasLevel()) {
+ MOZ_MTLOG(ML_DEBUG, "[" << mName << "] Transceiver "
+ << transceiver.GetLevel()
+ << " is not necessarily in a bundle.");
+ }
+ if (transceiver.HasLevel()) {
+ MOZ_MTLOG(ML_DEBUG,
+ "[" << mName << "] Transceiver " << transceiver.GetLevel()
+ << " transport-id: " << transceiver.mTransport.mTransportId
+ << " components: " << transceiver.mTransport.mComponents);
+ }
+ }
+}
+
+nsresult JsepSessionImpl::FinalizeTransport(const SdpAttributeList& remote,
+ const SdpAttributeList& answer,
+ JsepTransport* transport) const {
+ if (!transport->mComponents) {
+ return NS_OK;
+ }
+
+ if (!transport->mIce || transport->mIce->mUfrag != remote.GetIceUfrag() ||
+ transport->mIce->mPwd != remote.GetIcePwd()) {
+ UniquePtr<JsepIceTransport> ice = MakeUnique<JsepIceTransport>();
+ transport->mDtls = nullptr;
+
+ // We do sanity-checking for these in ParseSdp
+ ice->mUfrag = remote.GetIceUfrag();
+ ice->mPwd = remote.GetIcePwd();
+ transport->mIce = std::move(ice);
+ }
+
+ if (remote.HasAttribute(SdpAttribute::kCandidateAttribute)) {
+ transport->mIce->mCandidates = remote.GetCandidate();
+ }
+
+ if (!transport->mDtls) {
+ // RFC 5763 says:
+ //
+ // The endpoint MUST use the setup attribute defined in [RFC4145].
+ // The endpoint that is the offerer MUST use the setup attribute
+ // value of setup:actpass and be prepared to receive a client_hello
+ // before it receives the answer. The answerer MUST use either a
+ // setup attribute value of setup:active or setup:passive. Note that
+ // if the answerer uses setup:passive, then the DTLS handshake will
+ // not begin until the answerer is received, which adds additional
+ // latency. setup:active allows the answer and the DTLS handshake to
+ // occur in parallel. Thus, setup:active is RECOMMENDED. Whichever
+ // party is active MUST initiate a DTLS handshake by sending a
+ // ClientHello over each flow (host/port quartet).
+ UniquePtr<JsepDtlsTransport> dtls = MakeUnique<JsepDtlsTransport>();
+ dtls->mFingerprints = remote.GetFingerprint();
+ if (!answer.HasAttribute(mozilla::SdpAttribute::kSetupAttribute)) {
+ dtls->mRole = *mIsPendingOfferer ? JsepDtlsTransport::kJsepDtlsServer
+ : JsepDtlsTransport::kJsepDtlsClient;
+ } else {
+ if (*mIsPendingOfferer) {
+ dtls->mRole = (answer.GetSetup().mRole == SdpSetupAttribute::kActive)
+ ? JsepDtlsTransport::kJsepDtlsServer
+ : JsepDtlsTransport::kJsepDtlsClient;
+ } else {
+ dtls->mRole = (answer.GetSetup().mRole == SdpSetupAttribute::kActive)
+ ? JsepDtlsTransport::kJsepDtlsClient
+ : JsepDtlsTransport::kJsepDtlsServer;
+ }
+ }
+
+ transport->mDtls = std::move(dtls);
+ }
+
+ if (answer.HasAttribute(SdpAttribute::kRtcpMuxAttribute)) {
+ transport->mComponents = 1;
+ }
+
+ return NS_OK;
+}
+
+nsresult JsepSessionImpl::AddTransportAttributes(
+ SdpMediaSection* msection, SdpSetupAttribute::Role dtlsRole) {
+ if (mIceUfrag.empty() || mIcePwd.empty()) {
+ JSEP_SET_ERROR("Missing ICE ufrag or password");
+ return NS_ERROR_FAILURE;
+ }
+
+ SdpAttributeList& attrList = msection->GetAttributeList();
+ attrList.SetAttribute(
+ new SdpStringAttribute(SdpAttribute::kIceUfragAttribute, mIceUfrag));
+ attrList.SetAttribute(
+ new SdpStringAttribute(SdpAttribute::kIcePwdAttribute, mIcePwd));
+
+ msection->GetAttributeList().SetAttribute(new SdpSetupAttribute(dtlsRole));
+
+ return NS_OK;
+}
+
+nsresult JsepSessionImpl::CopyPreviousTransportParams(
+ const Sdp& oldAnswer, const Sdp& offerersPreviousSdp, const Sdp& newOffer,
+ Sdp* newLocal) {
+ for (size_t i = 0; i < oldAnswer.GetMediaSectionCount(); ++i) {
+ if (!mSdpHelper.MsectionIsDisabled(newLocal->GetMediaSection(i)) &&
+ mSdpHelper.AreOldTransportParamsValid(oldAnswer, offerersPreviousSdp,
+ newOffer, i)) {
+ // If newLocal is an offer, this will be the number of components we used
+ // last time, and if it is an answer, this will be the number of
+ // components we've decided we're using now.
+ Maybe<const JsepTransceiver> transceiver(GetTransceiverForLevel(i));
+ if (!transceiver) {
+ MOZ_ASSERT(false);
+ JSEP_SET_ERROR("No transceiver for level " << i);
+ return NS_ERROR_FAILURE;
+ }
+ size_t numComponents = transceiver->mTransport.mComponents;
+ nsresult rv = mSdpHelper.CopyTransportParams(
+ numComponents, mCurrentLocalDescription->GetMediaSection(i),
+ &newLocal->GetMediaSection(i));
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult JsepSessionImpl::ParseSdp(const std::string& sdp,
+ UniquePtr<Sdp>* parsedp) {
+ auto results = mParser->Parse(sdp);
+ auto parsed = std::move(results->Sdp());
+ mLastSdpParsingErrors = results->Errors();
+ if (!parsed) {
+ std::string error = results->ParserName() + " Failed to parse SDP: ";
+ mSdpHelper.AppendSdpParseErrors(mLastSdpParsingErrors, &error);
+ JSEP_SET_ERROR(error);
+ return NS_ERROR_INVALID_ARG;
+ }
+ // Verify that the JSEP rules for all SDP are followed
+ for (size_t i = 0; i < parsed->GetMediaSectionCount(); ++i) {
+ if (mSdpHelper.MsectionIsDisabled(parsed->GetMediaSection(i))) {
+ // Disabled, let this stuff slide.
+ continue;
+ }
+
+ const SdpMediaSection& msection(parsed->GetMediaSection(i));
+ auto& mediaAttrs = msection.GetAttributeList();
+
+ if (mediaAttrs.HasAttribute(SdpAttribute::kMidAttribute) &&
+ mediaAttrs.GetMid().length() > 16) {
+ JSEP_SET_ERROR(
+ "Invalid description, mid length greater than 16 "
+ "unsupported until 2-byte rtp header extensions are "
+ "supported in webrtc.org");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (mediaAttrs.HasAttribute(SdpAttribute::kExtmapAttribute)) {
+ std::set<uint16_t> extIds;
+ for (const auto& ext : mediaAttrs.GetExtmap().mExtmaps) {
+ uint16_t id = ext.entry;
+
+ if (id < 1 || id > 14) {
+ JSEP_SET_ERROR("Description contains invalid extension id "
+ << id << " on level " << i
+ << " which is unsupported until 2-byte rtp"
+ " header extensions are supported in webrtc.org");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (extIds.find(id) != extIds.end()) {
+ JSEP_SET_ERROR("Description contains duplicate extension id "
+ << id << " on level " << i);
+ return NS_ERROR_INVALID_ARG;
+ }
+ extIds.insert(id);
+ }
+ }
+
+ static const std::bitset<128> forbidden = GetForbiddenSdpPayloadTypes();
+ if (msection.GetMediaType() == SdpMediaSection::kAudio ||
+ msection.GetMediaType() == SdpMediaSection::kVideo) {
+ // Sanity-check that payload type can work with RTP
+ for (const std::string& fmt : msection.GetFormats()) {
+ uint16_t payloadType;
+ if (!SdpHelper::GetPtAsInt(fmt, &payloadType)) {
+ JSEP_SET_ERROR("Payload type \""
+ << fmt << "\" is not a 16-bit unsigned int at level "
+ << i);
+ return NS_ERROR_INVALID_ARG;
+ }
+ if (payloadType > 127) {
+ JSEP_SET_ERROR("audio/video payload type \""
+ << fmt << "\" is too large at level " << i);
+ return NS_ERROR_INVALID_ARG;
+ }
+ if (forbidden.test(payloadType)) {
+ JSEP_SET_ERROR("Illegal audio/video payload type \""
+ << fmt << "\" at level " << i);
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+ }
+ }
+
+ *parsedp = std::move(parsed);
+ return NS_OK;
+}
+
+nsresult JsepSessionImpl::SetRemoteDescriptionOffer(UniquePtr<Sdp> offer) {
+ MOZ_ASSERT(mState == kJsepStateStable);
+
+ mPendingRemoteDescription = std::move(offer);
+ mIsPendingOfferer = Some(false);
+
+ SetState(kJsepStateHaveRemoteOffer);
+ return NS_OK;
+}
+
+nsresult JsepSessionImpl::SetRemoteDescriptionAnswer(JsepSdpType type,
+ UniquePtr<Sdp> answer) {
+ MOZ_ASSERT(mState == kJsepStateHaveLocalOffer ||
+ mState == kJsepStateHaveRemotePranswer);
+
+ mPendingRemoteDescription = std::move(answer);
+
+ nsresult rv = HandleNegotiatedSession(mPendingLocalDescription,
+ mPendingRemoteDescription);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ mCurrentRemoteDescription = std::move(mPendingRemoteDescription);
+ mCurrentLocalDescription = std::move(mPendingLocalDescription);
+ MOZ_ASSERT(mIsPendingOfferer.isSome() && *mIsPendingOfferer);
+ mIsPendingOfferer.reset();
+ mIsCurrentOfferer = Some(true);
+
+ SetState(kJsepStateStable);
+ return NS_OK;
+}
+
+Maybe<JsepTransceiver> JsepSessionImpl::GetTransceiverForLevel(
+ size_t level) const {
+ return FindTransceiver([level](const JsepTransceiver& transceiver) {
+ return transceiver.HasLevel() && (transceiver.GetLevel() == level);
+ });
+}
+
+Maybe<JsepTransceiver> JsepSessionImpl::GetTransceiverForMid(
+ const std::string& mid) const {
+ return FindTransceiver([mid](const JsepTransceiver& transceiver) {
+ return transceiver.IsAssociated() && (transceiver.GetMid() == mid);
+ });
+}
+
+Maybe<JsepTransceiver> JsepSessionImpl::GetTransceiverForLocal(size_t level) {
+ if (Maybe<JsepTransceiver> transceiver = GetTransceiverForLevel(level)) {
+ if (transceiver->CanRecycle() &&
+ transceiver->GetMediaType() != SdpMediaSection::kApplication) {
+ // Attempt to recycle. If this fails, the old transceiver stays put.
+ transceiver->Disassociate();
+ Maybe<JsepTransceiver> newTransceiver =
+ FindUnassociatedTransceiver(transceiver->GetMediaType(), false);
+ if (newTransceiver) {
+ newTransceiver->SetLevel(level);
+ transceiver->ClearLevel();
+ transceiver->mSendTrack.ClearRids();
+ SetTransceiver(*newTransceiver);
+ SetTransceiver(*transceiver);
+ return newTransceiver;
+ }
+ }
+
+ SetTransceiver(*transceiver);
+ return transceiver;
+ }
+
+ // There is no transceiver for |level| right now.
+
+ // Look for an RTP transceiver
+ for (auto& transceiver : mTransceivers) {
+ if (transceiver.GetMediaType() != SdpMediaSection::kApplication &&
+ !transceiver.IsStopped() && !transceiver.HasLevel()) {
+ transceiver.SetLevel(level);
+ return Some(transceiver);
+ }
+ }
+
+ // Ok, look for a datachannel
+ for (auto& transceiver : mTransceivers) {
+ if (!transceiver.IsStopped() && !transceiver.HasLevel()) {
+ transceiver.SetLevel(level);
+ return Some(transceiver);
+ }
+ }
+
+ return Nothing();
+}
+
+Maybe<JsepTransceiver> JsepSessionImpl::GetTransceiverForRemote(
+ const SdpMediaSection& msection) {
+ size_t level = msection.GetLevel();
+ Maybe<JsepTransceiver> transceiver = GetTransceiverForLevel(level);
+ if (transceiver) {
+ if (!transceiver->CanRecycle()) {
+ return transceiver;
+ }
+ transceiver->Disassociate();
+ transceiver->ClearLevel();
+ transceiver->mSendTrack.ClearRids();
+ SetTransceiver(*transceiver);
+ }
+
+ // No transceiver for |level|
+ transceiver = FindUnassociatedTransceiver(msection.GetMediaType(), true);
+ if (transceiver) {
+ transceiver->SetLevel(level);
+ SetTransceiver(*transceiver);
+ return transceiver;
+ }
+
+ // Make a new transceiver
+ JsepTransceiver newTransceiver(msection.GetMediaType(), *mUuidGen,
+ SdpDirectionAttribute::kRecvonly);
+ newTransceiver.SetLevel(level);
+ newTransceiver.SetOnlyExistsBecauseOfSetRemote(true);
+ AddTransceiver(newTransceiver);
+ return Some(mTransceivers.back());
+}
+
+Maybe<JsepTransceiver> JsepSessionImpl::GetTransceiverWithTransport(
+ const std::string& transportId) const {
+ for (const auto& transceiver : mTransceivers) {
+ if (transceiver.HasOwnTransport() &&
+ (transceiver.mTransport.mTransportId == transportId)) {
+ MOZ_ASSERT(transceiver.HasLevel(),
+ "Transceiver has a transport, but no level!");
+ return Some(transceiver);
+ }
+ }
+
+ return Nothing();
+}
+
+nsresult JsepSessionImpl::UpdateTransceiversFromRemoteDescription(
+ const Sdp& remote) {
+ // Iterate over the sdp, updating remote tracks as we go
+ for (size_t i = 0; i < remote.GetMediaSectionCount(); ++i) {
+ const SdpMediaSection& msection = remote.GetMediaSection(i);
+
+ Maybe<JsepTransceiver> transceiver(GetTransceiverForRemote(msection));
+ if (!transceiver) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!mSdpHelper.MsectionIsDisabled(msection)) {
+ if (msection.GetAttributeList().HasAttribute(
+ SdpAttribute::kMidAttribute)) {
+ transceiver->Associate(msection.GetAttributeList().GetMid());
+ }
+ if (!transceiver->IsAssociated()) {
+ transceiver->Associate(GetNewMid());
+ } else {
+ mUsedMids.insert(transceiver->GetMid());
+ }
+ } else {
+ transceiver->mTransport.Close();
+ transceiver->Disassociate();
+ // This cannot be rolled back.
+ transceiver->Stop();
+ SetTransceiver(*transceiver);
+ continue;
+ }
+
+ if (msection.GetMediaType() == SdpMediaSection::MediaType::kApplication) {
+ SetTransceiver(*transceiver);
+ continue;
+ }
+
+ transceiver->mSendTrack.SendTrackSetRemote(mSsrcGenerator, msection);
+
+ // Interop workaround for endpoints that don't support msid.
+ // Ensures that there is a default stream id set, provided the remote is
+ // sending.
+ // TODO(bug 1426005): Remove this, or at least move it to JsepTrack.
+ transceiver->mRecvTrack.UpdateStreamIds({mDefaultRemoteStreamId});
+
+ // This will process a=msid if present, or clear the stream ids if the
+ // msection is not sending. If the msection is sending, and there are no
+ // a=msid, the previously set default will stay.
+ transceiver->mRecvTrack.RecvTrackSetRemote(remote, msection);
+ SetTransceiver(*transceiver);
+ }
+
+ return NS_OK;
+}
+
+Maybe<JsepTransceiver> JsepSessionImpl::FindUnassociatedTransceiver(
+ SdpMediaSection::MediaType type, bool magic) {
+ // Look through transceivers that are not mapped to an m-section
+ for (auto& transceiver : mTransceivers) {
+ if (type == SdpMediaSection::kApplication &&
+ type == transceiver.GetMediaType()) {
+ transceiver.RestartDatachannelTransceiver();
+ return Some(transceiver);
+ }
+ if (!transceiver.IsStopped() && !transceiver.HasLevel() &&
+ (!magic || transceiver.HasAddTrackMagic()) &&
+ (transceiver.GetMediaType() == type)) {
+ return Some(transceiver);
+ }
+ }
+
+ return Nothing();
+}
+
+void JsepSessionImpl::RollbackLocalOffer() {
+ for (size_t i = 0; i < mTransceivers.size(); ++i) {
+ auto& transceiver = mTransceivers[i];
+ if (mOldTransceivers.size() > i) {
+ transceiver.Rollback(mOldTransceivers[i], false);
+ mOldTransceivers[i] = transceiver;
+ continue;
+ }
+
+ JsepTransceiver temp(transceiver.GetMediaType(), *mUuidGen);
+ InitTransceiver(temp);
+ transceiver.Rollback(temp, false);
+ mOldTransceivers.push_back(transceiver);
+ }
+
+ mTransceivers = std::move(mOldTransceivers);
+}
+
+void JsepSessionImpl::RollbackRemoteOffer() {
+ for (size_t i = 0; i < mTransceivers.size(); ++i) {
+ auto& transceiver = mTransceivers[i];
+ if (mOldTransceivers.size() > i) {
+ // Some stuff cannot be rolled back. Save this information.
+ transceiver.Rollback(mOldTransceivers[i], true);
+ mOldTransceivers[i] = transceiver;
+ continue;
+ }
+
+ // New transceiver!
+ // We rollback even for transceivers we will remove, just to ensure we end
+ // up at the starting state.
+ JsepTransceiver temp(transceiver.GetMediaType(), *mUuidGen);
+ InitTransceiver(temp);
+ transceiver.Rollback(temp, true);
+
+ if (transceiver.OnlyExistsBecauseOfSetRemote()) {
+ transceiver.Stop();
+ transceiver.SetRemoved();
+ }
+ mOldTransceivers.push_back(transceiver);
+ }
+
+ mTransceivers = std::move(mOldTransceivers);
+}
+
+nsresult JsepSessionImpl::ValidateLocalDescription(const Sdp& description,
+ JsepSdpType type) {
+ Sdp* generated = nullptr;
+ // TODO(bug 1095226): Better checking.
+ if (type == kJsepSdpOffer) {
+ generated = mGeneratedOffer.get();
+ } else {
+ generated = mGeneratedAnswer.get();
+ }
+
+ if (!generated) {
+ JSEP_SET_ERROR(
+ "Calling SetLocal without first calling CreateOffer/Answer"
+ " is not supported.");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (description.GetMediaSectionCount() != generated->GetMediaSectionCount()) {
+ JSEP_SET_ERROR("Changing the number of m-sections is not allowed");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ for (size_t i = 0; i < description.GetMediaSectionCount(); ++i) {
+ auto& origMsection = generated->GetMediaSection(i);
+ auto& finalMsection = description.GetMediaSection(i);
+ if (origMsection.GetMediaType() != finalMsection.GetMediaType()) {
+ JSEP_SET_ERROR("Changing the media-type of m-sections is not allowed");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // These will be present in reoffer
+ if (!mCurrentLocalDescription) {
+ if (finalMsection.GetAttributeList().HasAttribute(
+ SdpAttribute::kCandidateAttribute)) {
+ JSEP_SET_ERROR("Adding your own candidate attributes is not supported");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (finalMsection.GetAttributeList().HasAttribute(
+ SdpAttribute::kEndOfCandidatesAttribute)) {
+ JSEP_SET_ERROR("Why are you trying to set a=end-of-candidates?");
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+
+ if (mSdpHelper.MsectionIsDisabled(finalMsection)) {
+ continue;
+ }
+
+ if (!finalMsection.GetAttributeList().HasAttribute(
+ SdpAttribute::kMidAttribute)) {
+ JSEP_SET_ERROR("Local descriptions must have a=mid attributes.");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (finalMsection.GetAttributeList().GetMid() !=
+ origMsection.GetAttributeList().GetMid()) {
+ JSEP_SET_ERROR("Changing the mid of m-sections is not allowed.");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // TODO(bug 1095218): Check msid
+ // TODO(bug 1095226): Check ice-ufrag and ice-pwd
+ // TODO(bug 1095226): Check fingerprints
+ // TODO(bug 1095226): Check payload types (at least ensure that payload
+ // types we don't actually support weren't added)
+ // TODO(bug 1095226): Check ice-options?
+ }
+
+ if (description.GetAttributeList().HasAttribute(
+ SdpAttribute::kIceLiteAttribute)) {
+ JSEP_SET_ERROR("Running ICE in lite mode is unsupported");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ return NS_OK;
+}
+
+nsresult JsepSessionImpl::ValidateRemoteDescription(const Sdp& description) {
+ if (!mCurrentLocalDescription) {
+ // Initial offer; nothing to validate besides the stuff in ParseSdp
+ return NS_OK;
+ }
+
+ if (mCurrentLocalDescription->GetMediaSectionCount() >
+ description.GetMediaSectionCount()) {
+ JSEP_SET_ERROR(
+ "New remote description has fewer m-sections than the "
+ "previous remote description.");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ for (size_t i = 0; i < description.GetMediaSectionCount(); ++i) {
+ const SdpAttributeList& attrs =
+ description.GetMediaSection(i).GetAttributeList();
+
+ if (attrs.HasAttribute(SdpAttribute::kExtmapAttribute)) {
+ for (const auto& ext : attrs.GetExtmap().mExtmaps) {
+ if (mExtmapEntriesEverNegotiated.count(ext.entry) &&
+ mExtmapEntriesEverNegotiated[ext.entry] != ext.extensionname) {
+ JSEP_SET_ERROR(
+ "Remote description attempted to remap RTP extension id "
+ << ext.entry << " from "
+ << mExtmapEntriesEverNegotiated[ext.entry] << " to "
+ << ext.extensionname);
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+ }
+ }
+
+ if (!mCurrentRemoteDescription) {
+ // No further checking for initial answers
+ return NS_OK;
+ }
+
+ // These are solely to check that bundle is valid
+ SdpHelper::BundledMids bundledMids;
+ nsresult rv = GetNegotiatedBundledMids(&bundledMids);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ SdpHelper::BundledMids newBundledMids;
+ rv = mSdpHelper.GetBundledMids(description, &newBundledMids);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // check for partial ice restart, which is not supported
+ Maybe<bool> iceCredsDiffer;
+ for (size_t i = 0; i < mCurrentRemoteDescription->GetMediaSectionCount();
+ ++i) {
+ const SdpMediaSection& newMsection = description.GetMediaSection(i);
+ const SdpMediaSection& oldMsection =
+ mCurrentRemoteDescription->GetMediaSection(i);
+
+ if (mSdpHelper.MsectionIsDisabled(newMsection) ||
+ mSdpHelper.MsectionIsDisabled(oldMsection)) {
+ continue;
+ }
+
+ if (oldMsection.GetMediaType() != newMsection.GetMediaType()) {
+ JSEP_SET_ERROR("Remote description changes the media type of m-line "
+ << i);
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ bool differ = mSdpHelper.IceCredentialsDiffer(newMsection, oldMsection);
+
+ if (mIsPendingOfferer.isSome() && *mIsPendingOfferer && differ &&
+ !IsIceRestarting()) {
+ JSEP_SET_ERROR(
+ "Remote description indicates ICE restart but offer did not "
+ "request ICE restart (new remote description changes either "
+ "the ice-ufrag or ice-pwd)");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // Detect whether all the creds are the same or all are different
+ if (!iceCredsDiffer.isSome()) {
+ // for the first msection capture whether creds are different or same
+ iceCredsDiffer = mozilla::Some(differ);
+ } else if (iceCredsDiffer.isSome() && *iceCredsDiffer != differ) {
+ // subsequent msections must match the first sections
+ JSEP_SET_ERROR(
+ "Partial ICE restart is unsupported at this time "
+ "(new remote description changes either the ice-ufrag "
+ "or ice-pwd on fewer than all msections)");
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult JsepSessionImpl::ValidateOffer(const Sdp& offer) {
+ return mSdpHelper.ValidateTransportAttributes(offer, sdp::kOffer);
+}
+
+nsresult JsepSessionImpl::ValidateAnswer(const Sdp& offer, const Sdp& answer) {
+ if (offer.GetMediaSectionCount() != answer.GetMediaSectionCount()) {
+ JSEP_SET_ERROR("Offer and answer have different number of m-lines "
+ << "(" << offer.GetMediaSectionCount() << " vs "
+ << answer.GetMediaSectionCount() << ")");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ nsresult rv = mSdpHelper.ValidateTransportAttributes(answer, sdp::kAnswer);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (size_t i = 0; i < offer.GetMediaSectionCount(); ++i) {
+ const SdpMediaSection& offerMsection = offer.GetMediaSection(i);
+ const SdpMediaSection& answerMsection = answer.GetMediaSection(i);
+
+ if (offerMsection.GetMediaType() != answerMsection.GetMediaType()) {
+ JSEP_SET_ERROR("Answer and offer have different media types at m-line "
+ << i);
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (mSdpHelper.MsectionIsDisabled(answerMsection)) {
+ continue;
+ }
+
+ if (mSdpHelper.MsectionIsDisabled(offerMsection)) {
+ JSEP_SET_ERROR(
+ "Answer tried to enable an m-section that was disabled in the offer");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (!offerMsection.IsSending() && answerMsection.IsReceiving()) {
+ JSEP_SET_ERROR("Answer tried to set recv when offer did not set send");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (!offerMsection.IsReceiving() && answerMsection.IsSending()) {
+ JSEP_SET_ERROR("Answer tried to set send when offer did not set recv");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ const SdpAttributeList& answerAttrs(answerMsection.GetAttributeList());
+ const SdpAttributeList& offerAttrs(offerMsection.GetAttributeList());
+ if (answerAttrs.HasAttribute(SdpAttribute::kMidAttribute) &&
+ offerAttrs.HasAttribute(SdpAttribute::kMidAttribute) &&
+ offerAttrs.GetMid() != answerAttrs.GetMid()) {
+ JSEP_SET_ERROR("Answer changes mid for level, was \'"
+ << offerMsection.GetAttributeList().GetMid()
+ << "\', now \'"
+ << answerMsection.GetAttributeList().GetMid() << "\'");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // Sanity check extmap
+ if (answerAttrs.HasAttribute(SdpAttribute::kExtmapAttribute)) {
+ if (!offerAttrs.HasAttribute(SdpAttribute::kExtmapAttribute)) {
+ JSEP_SET_ERROR("Answer adds extmap attributes to level " << i);
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ for (const auto& ansExt : answerAttrs.GetExtmap().mExtmaps) {
+ bool found = false;
+ for (const auto& offExt : offerAttrs.GetExtmap().mExtmaps) {
+ if (ansExt.extensionname == offExt.extensionname) {
+ if ((ansExt.direction & reverse(offExt.direction)) !=
+ ansExt.direction) {
+ // FIXME we do not return an error here, because Chrome up to
+ // version 57 is actually tripping over this if they are the
+ // answerer. See bug 1355010 for details.
+ MOZ_MTLOG(ML_WARNING,
+ "[" << mName
+ << "]: Answer has inconsistent"
+ " direction on extmap attribute at level "
+ << i << " (" << ansExt.extensionname
+ << "). Offer had " << offExt.direction
+ << ", answer had " << ansExt.direction << ".");
+ // return NS_ERROR_INVALID_ARG;
+ }
+
+ if (offExt.entry < 4096 && (offExt.entry != ansExt.entry)) {
+ JSEP_SET_ERROR("Answer changed id for extmap attribute at level "
+ << i << " (" << offExt.extensionname << ") from "
+ << offExt.entry << " to " << ansExt.entry << ".");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (ansExt.entry >= 4096) {
+ JSEP_SET_ERROR("Answer used an invalid id ("
+ << ansExt.entry
+ << ") for extmap attribute at level " << i << " ("
+ << ansExt.extensionname << ").");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ JSEP_SET_ERROR("Answer has extmap "
+ << ansExt.extensionname
+ << " at "
+ "level "
+ << i << " that was not present in offer.");
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult JsepSessionImpl::CreateGenericSDP(UniquePtr<Sdp>* sdpp) {
+ // draft-ietf-rtcweb-jsep-08 Section 5.2.1:
+ // o The second SDP line MUST be an "o=" line, as specified in
+ // [RFC4566], Section 5.2. The value of the <username> field SHOULD
+ // be "-". The value of the <sess-id> field SHOULD be a
+ // cryptographically random number. To ensure uniqueness, this
+ // number SHOULD be at least 64 bits long. The value of the <sess-
+ // version> field SHOULD be zero. The value of the <nettype>
+ // <addrtype> <unicast-address> tuple SHOULD be set to a non-
+ // meaningful address, such as IN IP4 0.0.0.0, to prevent leaking the
+ // local address in this field. As mentioned in [RFC4566], the
+ // entire o= line needs to be unique, but selecting a random number
+ // for <sess-id> is sufficient to accomplish this.
+ //
+ // Historical note: we used to report the actual version number here, after
+ // "SDPARTA-", but that becomes a problem starting with version 100, since
+ // some services parse 100 as "10" and give us legacy/broken behavior. So
+ // we're freezing the version number at 99.0 in this string.
+ auto origin = SdpOrigin("mozilla...THIS_IS_SDPARTA-99.0", mSessionId,
+ mSessionVersion, sdp::kIPv4, "0.0.0.0");
+
+ UniquePtr<Sdp> sdp = MakeUnique<SipccSdp>(origin);
+
+ if (mDtlsFingerprints.empty()) {
+ JSEP_SET_ERROR("Missing DTLS fingerprint");
+ return NS_ERROR_FAILURE;
+ }
+
+ UniquePtr<SdpFingerprintAttributeList> fpl =
+ MakeUnique<SdpFingerprintAttributeList>();
+ for (auto& dtlsFingerprint : mDtlsFingerprints) {
+ fpl->PushEntry(dtlsFingerprint.mAlgorithm, dtlsFingerprint.mValue);
+ }
+ sdp->GetAttributeList().SetAttribute(fpl.release());
+
+ auto* iceOpts = new SdpOptionsAttribute(SdpAttribute::kIceOptionsAttribute);
+ iceOpts->PushEntry("trickle");
+ sdp->GetAttributeList().SetAttribute(iceOpts);
+
+ // This assumes content doesn't add a bunch of msid attributes with a
+ // different semantic in mind.
+ std::vector<std::string> msids;
+ msids.push_back("*");
+ mSdpHelper.SetupMsidSemantic(msids, sdp.get());
+
+ *sdpp = std::move(sdp);
+ return NS_OK;
+}
+
+nsresult JsepSessionImpl::SetupIds() {
+ SECStatus rv = PK11_GenerateRandom(
+ reinterpret_cast<unsigned char*>(&mSessionId), sizeof(mSessionId));
+ // RFC 3264 says that session-ids MUST be representable as a _signed_
+ // 64 bit number, meaning the MSB cannot be set.
+ mSessionId = mSessionId >> 1;
+ if (rv != SECSuccess) {
+ JSEP_SET_ERROR("Failed to generate session id: " << rv);
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!mUuidGen->Generate(&mDefaultRemoteStreamId)) {
+ JSEP_SET_ERROR("Failed to generate default uuid for streams");
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!mUuidGen->Generate(&mCNAME)) {
+ JSEP_SET_ERROR("Failed to generate CNAME");
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+void JsepSessionImpl::SetDefaultCodecs(
+ const std::vector<UniquePtr<JsepCodecDescription>>& aPreferredCodecs) {
+ mSupportedCodecs.clear();
+
+ for (const auto& codec : aPreferredCodecs) {
+ mSupportedCodecs.emplace_back(codec->Clone());
+ }
+}
+
+void JsepSessionImpl::SetState(JsepSignalingState state) {
+ if (state == mState) return;
+
+ MOZ_MTLOG(ML_NOTICE, "[" << mName << "]: " << GetStateStr(mState) << " -> "
+ << GetStateStr(state));
+ mState = state;
+}
+
+JsepSession::Result JsepSessionImpl::AddRemoteIceCandidate(
+ const std::string& candidate, const std::string& mid,
+ const Maybe<uint16_t>& level, const std::string& ufrag,
+ std::string* transportId) {
+ mLastError.clear();
+ if (!mCurrentRemoteDescription && !mPendingRemoteDescription) {
+ JSEP_SET_ERROR("Cannot add ICE candidate when there is no remote SDP");
+ return dom::PCError::InvalidStateError;
+ }
+
+ if (mid.empty() && !level.isSome() && candidate.empty()) {
+ // Set end-of-candidates on SDP
+ if (mCurrentRemoteDescription) {
+ nsresult rv = mSdpHelper.SetIceGatheringComplete(
+ mCurrentRemoteDescription.get(), ufrag);
+ NS_ENSURE_SUCCESS(rv, dom::PCError::OperationError);
+ }
+
+ if (mPendingRemoteDescription) {
+ // If we had an error when adding the candidate to the current
+ // description, we stomp them here. This is deliberate.
+ nsresult rv = mSdpHelper.SetIceGatheringComplete(
+ mPendingRemoteDescription.get(), ufrag);
+ NS_ENSURE_SUCCESS(rv, dom::PCError::OperationError);
+ }
+ return Result();
+ }
+
+ Maybe<JsepTransceiver> transceiver;
+ if (!mid.empty()) {
+ transceiver = GetTransceiverForMid(mid);
+ } else if (level.isSome()) {
+ transceiver = GetTransceiverForLevel(level.value());
+ }
+
+ if (!transceiver) {
+ JSEP_SET_ERROR("Cannot set ICE candidate for level="
+ << level << " mid=" << mid << ": No such transceiver.");
+ return dom::PCError::OperationError;
+ }
+
+ if (level.isSome() && transceiver->GetLevel() != level.value()) {
+ MOZ_MTLOG(ML_WARNING, "Mismatch between mid and level - \""
+ << mid << "\" is not the mid for level "
+ << level);
+ }
+
+ *transportId = transceiver->mTransport.mTransportId;
+ nsresult rv = NS_ERROR_UNEXPECTED;
+ if (mCurrentRemoteDescription) {
+ rv =
+ mSdpHelper.AddCandidateToSdp(mCurrentRemoteDescription.get(), candidate,
+ transceiver->GetLevel(), ufrag);
+ }
+
+ if (mPendingRemoteDescription) {
+ // If we had an error when adding the candidate to the current description,
+ // we stomp them here. This is deliberate.
+ rv =
+ mSdpHelper.AddCandidateToSdp(mPendingRemoteDescription.get(), candidate,
+ transceiver->GetLevel(), ufrag);
+ }
+
+ NS_ENSURE_SUCCESS(rv, dom::PCError::OperationError);
+ return Result();
+}
+
+nsresult JsepSessionImpl::AddLocalIceCandidate(const std::string& candidate,
+ const std::string& transportId,
+ const std::string& ufrag,
+ uint16_t* level,
+ std::string* mid,
+ bool* skipped) {
+ mLastError.clear();
+ *skipped = true;
+ if (!mCurrentLocalDescription && !mPendingLocalDescription) {
+ JSEP_SET_ERROR("Cannot add ICE candidate when there is no local SDP");
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ Maybe<const JsepTransceiver> transceiver =
+ GetTransceiverWithTransport(transportId);
+ if (!transceiver || !transceiver->IsAssociated()) {
+ // mainly here to make some testing less complicated, but also just in case
+ return NS_OK;
+ }
+
+ *level = transceiver->GetLevel();
+ *mid = transceiver->GetMid();
+
+ nsresult rv = NS_ERROR_INVALID_ARG;
+ if (mCurrentLocalDescription) {
+ rv = mSdpHelper.AddCandidateToSdp(mCurrentLocalDescription.get(), candidate,
+ *level, ufrag);
+ }
+
+ if (mPendingLocalDescription) {
+ // If we had an error when adding the candidate to the current description,
+ // we stomp them here. This is deliberate.
+ rv = mSdpHelper.AddCandidateToSdp(mPendingLocalDescription.get(), candidate,
+ *level, ufrag);
+ }
+
+ *skipped = false;
+ return rv;
+}
+
+nsresult JsepSessionImpl::UpdateDefaultCandidate(
+ const std::string& defaultCandidateAddr, uint16_t defaultCandidatePort,
+ const std::string& defaultRtcpCandidateAddr,
+ uint16_t defaultRtcpCandidatePort, const std::string& transportId) {
+ mLastError.clear();
+
+ mozilla::Sdp* sdp =
+ GetParsedLocalDescription(kJsepDescriptionPendingOrCurrent);
+
+ if (!sdp) {
+ JSEP_SET_ERROR("Cannot add ICE candidate in state " << GetStateStr(mState));
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ for (const auto& transceiver : mTransceivers) {
+ // We set the default address for bundled m-sections, but not candidate
+ // attributes. Ugh.
+ if (transceiver.mTransport.mTransportId == transportId) {
+ MOZ_ASSERT(transceiver.HasLevel(),
+ "Transceiver has a transport, but no level! "
+ "This should never happen.");
+ std::string defaultRtcpCandidateAddrCopy(defaultRtcpCandidateAddr);
+ if (mState == kJsepStateStable) {
+ if (transceiver.mTransport.mComponents == 1) {
+ // We know we're doing rtcp-mux by now. Don't create an rtcp attr.
+ defaultRtcpCandidateAddrCopy = "";
+ defaultRtcpCandidatePort = 0;
+ }
+ }
+
+ size_t level = transceiver.GetLevel();
+ if (level >= sdp->GetMediaSectionCount()) {
+ MOZ_ASSERT(false, "Transceiver's level is too large!");
+ JSEP_SET_ERROR("Transceiver's level is too large!");
+ return NS_ERROR_FAILURE;
+ }
+
+ auto& msection = sdp->GetMediaSection(level);
+
+ // Do not add default candidate to a bundle-only m-section, sinice that
+ // might confuse endpoints that do not support bundle-only.
+ if (!msection.GetAttributeList().HasAttribute(
+ SdpAttribute::kBundleOnlyAttribute)) {
+ mSdpHelper.SetDefaultAddresses(
+ defaultCandidateAddr, defaultCandidatePort,
+ defaultRtcpCandidateAddrCopy, defaultRtcpCandidatePort, &msection);
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult JsepSessionImpl::GetNegotiatedBundledMids(
+ SdpHelper::BundledMids* bundledMids) {
+ const Sdp* answerSdp = GetAnswer();
+
+ if (!answerSdp) {
+ return NS_OK;
+ }
+
+ return mSdpHelper.GetBundledMids(*answerSdp, bundledMids);
+}
+
+mozilla::Sdp* JsepSessionImpl::GetParsedLocalDescription(
+ JsepDescriptionPendingOrCurrent type) const {
+ if (type == kJsepDescriptionPending) {
+ return mPendingLocalDescription.get();
+ } else if (mPendingLocalDescription &&
+ type == kJsepDescriptionPendingOrCurrent) {
+ return mPendingLocalDescription.get();
+ }
+ return mCurrentLocalDescription.get();
+}
+
+mozilla::Sdp* JsepSessionImpl::GetParsedRemoteDescription(
+ JsepDescriptionPendingOrCurrent type) const {
+ if (type == kJsepDescriptionPending) {
+ return mPendingRemoteDescription.get();
+ } else if (mPendingRemoteDescription &&
+ type == kJsepDescriptionPendingOrCurrent) {
+ return mPendingRemoteDescription.get();
+ }
+ return mCurrentRemoteDescription.get();
+}
+
+const Sdp* JsepSessionImpl::GetAnswer() const {
+ return (mIsCurrentOfferer.isSome() && *mIsCurrentOfferer)
+ ? mCurrentRemoteDescription.get()
+ : mCurrentLocalDescription.get();
+}
+
+void JsepSessionImpl::SetIceRestarting(bool restarting) {
+ if (restarting) {
+ // not restarting -> restarting
+ if (!IsIceRestarting()) {
+ // We don't set this more than once, so the old ufrag/pwd is preserved
+ // even if we CreateOffer({iceRestart:true}) multiple times in a row.
+ mOldIceUfrag = mIceUfrag;
+ mOldIcePwd = mIcePwd;
+ }
+ mIceUfrag = GetRandomHex(1);
+ mIcePwd = GetRandomHex(4);
+ } else if (IsIceRestarting()) {
+ // restarting -> not restarting, restore old ufrag/pwd
+ mIceUfrag = mOldIceUfrag;
+ mIcePwd = mOldIcePwd;
+ mOldIceUfrag.clear();
+ mOldIcePwd.clear();
+ }
+}
+
+nsresult JsepSessionImpl::Close() {
+ mLastError.clear();
+ SetState(kJsepStateClosed);
+ return NS_OK;
+}
+
+const std::string JsepSessionImpl::GetLastError() const { return mLastError; }
+
+const std::vector<std::pair<size_t, std::string>>&
+JsepSessionImpl::GetLastSdpParsingErrors() const {
+ return mLastSdpParsingErrors;
+}
+
+bool JsepSessionImpl::CheckNegotiationNeeded() const {
+ MOZ_ASSERT(mState == kJsepStateStable);
+
+ for (const auto& transceiver : mTransceivers) {
+ if (transceiver.IsStopped()) {
+ if (transceiver.IsAssociated()) {
+ MOZ_MTLOG(ML_DEBUG, "[" << mName
+ << "]: Negotiation needed because of "
+ "stopped transceiver that still has a mid.");
+ return true;
+ }
+ continue;
+ }
+
+ if (!transceiver.IsAssociated()) {
+ MOZ_MTLOG(ML_DEBUG, "[" << mName
+ << "]: Negotiation needed because of "
+ "unassociated (but not stopped) transceiver.");
+ return true;
+ }
+
+ if (!mCurrentLocalDescription || !mCurrentRemoteDescription) {
+ MOZ_CRASH(
+ "Transceivers should not be associated if we're in stable "
+ "before the first negotiation.");
+ continue;
+ }
+
+ if (!transceiver.HasLevel()) {
+ MOZ_CRASH("Associated transceivers should always have a level.");
+ continue;
+ }
+
+ if (transceiver.GetMediaType() == SdpMediaSection::kApplication) {
+ continue;
+ }
+
+ size_t level = transceiver.GetLevel();
+ if (NS_WARN_IF(mCurrentLocalDescription->GetMediaSectionCount() <= level) ||
+ NS_WARN_IF(mCurrentRemoteDescription->GetMediaSectionCount() <=
+ level)) {
+ MOZ_ASSERT(false);
+ continue;
+ }
+
+ const SdpMediaSection& local =
+ mCurrentLocalDescription->GetMediaSection(level);
+ const SdpMediaSection& remote =
+ mCurrentRemoteDescription->GetMediaSection(level);
+
+ if (transceiver.mJsDirection & sdp::kSend) {
+ std::vector<std::string> sdpMsids;
+ if (local.GetAttributeList().HasAttribute(SdpAttribute::kMsidAttribute)) {
+ for (const auto& msidAttr : local.GetAttributeList().GetMsid().mMsids) {
+ if (msidAttr.identifier != "-") {
+ sdpMsids.push_back(msidAttr.identifier);
+ }
+ }
+ }
+ std::sort(sdpMsids.begin(), sdpMsids.end());
+
+ std::vector<std::string> jsepMsids;
+ for (const auto& jsepMsid : transceiver.mSendTrack.GetStreamIds()) {
+ jsepMsids.push_back(jsepMsid);
+ }
+ std::sort(jsepMsids.begin(), jsepMsids.end());
+
+ if (!std::equal(sdpMsids.begin(), sdpMsids.end(), jsepMsids.begin(),
+ jsepMsids.end())) {
+ MOZ_MTLOG(ML_DEBUG,
+ "[" << mName
+ << "]: Negotiation needed because transceiver "
+ "is sending, and the local SDP has different "
+ "msids than the send track");
+ MOZ_MTLOG(ML_DEBUG, "[" << mName << "]: SDP msids = [");
+ for (const auto& msid : sdpMsids) {
+ MOZ_MTLOG(ML_DEBUG, msid << ", ");
+ }
+ MOZ_MTLOG(ML_DEBUG, "]");
+ MOZ_MTLOG(ML_DEBUG, "[" << mName << "]: JSEP msids = [");
+ for (const auto& msid : jsepMsids) {
+ MOZ_MTLOG(ML_DEBUG, msid << ", ");
+ }
+ MOZ_MTLOG(ML_DEBUG, "]");
+ return true;
+ }
+ }
+
+ if (mIsCurrentOfferer.isSome() && *mIsCurrentOfferer) {
+ if ((local.GetDirection() != transceiver.mJsDirection) &&
+ reverse(remote.GetDirection()) != transceiver.mJsDirection) {
+ MOZ_MTLOG(ML_DEBUG, "[" << mName
+ << "]: Negotiation needed because "
+ "the direction on our offer, and the remote "
+ "answer, does not "
+ "match the direction on a transceiver.");
+ return true;
+ }
+ } else if (local.GetDirection() !=
+ (transceiver.mJsDirection & reverse(remote.GetDirection()))) {
+ MOZ_MTLOG(
+ ML_DEBUG,
+ "[" << mName
+ << "]: Negotiation needed because "
+ "the direction on our answer doesn't match the direction on a "
+ "transceiver, even though the remote offer would have allowed "
+ "it.");
+ return true;
+ }
+ }
+
+ return false;
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/jsep/JsepSessionImpl.h b/dom/media/webrtc/jsep/JsepSessionImpl.h
new file mode 100644
index 0000000000..1cdbba9cfc
--- /dev/null
+++ b/dom/media/webrtc/jsep/JsepSessionImpl.h
@@ -0,0 +1,286 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _JSEPSESSIONIMPL_H_
+#define _JSEPSESSIONIMPL_H_
+
+#include <map>
+#include <set>
+#include <string>
+#include <vector>
+
+#include "jsep/JsepCodecDescription.h"
+#include "jsep/JsepSession.h"
+#include "jsep/JsepTrack.h"
+#include "jsep/JsepTransceiver.h"
+#include "jsep/SsrcGenerator.h"
+#include "sdp/HybridSdpParser.h"
+#include "sdp/SdpHelper.h"
+
+namespace mozilla {
+
+// JsepSessionImpl members that have default copy c'tors, to simplify the
+// implementation of the copy c'tor for JsepSessionImpl
+class JsepSessionCopyableStuff {
+ protected:
+ struct JsepDtlsFingerprint {
+ std::string mAlgorithm;
+ std::vector<uint8_t> mValue;
+ };
+
+ Maybe<bool> mIsPendingOfferer;
+ Maybe<bool> mIsCurrentOfferer;
+ bool mIceControlling = false;
+ std::string mIceUfrag;
+ std::string mIcePwd;
+ std::string mOldIceUfrag;
+ std::string mOldIcePwd;
+ bool mRemoteIsIceLite = false;
+ std::vector<std::string> mIceOptions;
+ JsepBundlePolicy mBundlePolicy = kBundleBalanced;
+ std::vector<JsepDtlsFingerprint> mDtlsFingerprints;
+ uint64_t mSessionId = 0;
+ uint64_t mSessionVersion = 0;
+ size_t mMidCounter = 0;
+ std::set<std::string> mUsedMids;
+ size_t mTransportIdCounter = 0;
+ std::vector<JsepExtmapMediaType> mRtpExtensions;
+ std::set<uint16_t> mExtmapEntriesEverUsed;
+ std::map<uint16_t, std::string> mExtmapEntriesEverNegotiated;
+ std::string mDefaultRemoteStreamId;
+ std::string mCNAME;
+ // Used to prevent duplicate local SSRCs. Not used to prevent local/remote or
+ // remote-only duplication, which will be important for EKT but not now.
+ std::set<uint32_t> mSsrcs;
+ std::string mLastError;
+ std::vector<std::pair<size_t, std::string>> mLastSdpParsingErrors;
+ bool mEncodeTrackId = true;
+ SsrcGenerator mSsrcGenerator;
+ // !!!NOT INDEXED BY LEVEL!!! The level mapping is done with
+ // JsepTransceiver::mLevel. The keys are UUIDs.
+ std::vector<JsepTransceiver> mTransceivers;
+ // So we can rollback. Not as simple as just going back to the old, though...
+ std::vector<JsepTransceiver> mOldTransceivers;
+};
+
+class JsepSessionImpl : public JsepSession, public JsepSessionCopyableStuff {
+ public:
+ JsepSessionImpl(const std::string& name, UniquePtr<JsepUuidGenerator> uuidgen)
+ : JsepSession(name),
+ mUuidGen(std::move(uuidgen)),
+ mSdpHelper(&mLastError),
+ mParser(new HybridSdpParser()) {}
+
+ JsepSessionImpl(const JsepSessionImpl& aOrig);
+
+ JsepSession* Clone() const override { return new JsepSessionImpl(*this); }
+
+ // Implement JsepSession methods.
+ virtual nsresult Init() override;
+
+ nsresult SetBundlePolicy(JsepBundlePolicy policy) override;
+
+ virtual bool RemoteIsIceLite() const override { return mRemoteIsIceLite; }
+
+ virtual std::vector<std::string> GetIceOptions() const override {
+ return mIceOptions;
+ }
+
+ virtual nsresult AddDtlsFingerprint(
+ const std::string& algorithm, const std::vector<uint8_t>& value) override;
+
+ virtual nsresult AddRtpExtension(
+ JsepMediaType mediaType, const std::string& extensionName,
+ SdpDirectionAttribute::Direction direction) override;
+ virtual nsresult AddAudioRtpExtension(
+ const std::string& extensionName,
+ SdpDirectionAttribute::Direction direction =
+ SdpDirectionAttribute::Direction::kSendrecv) override;
+
+ virtual nsresult AddVideoRtpExtension(
+ const std::string& extensionName,
+ SdpDirectionAttribute::Direction direction =
+ SdpDirectionAttribute::Direction::kSendrecv) override;
+
+ virtual nsresult AddAudioVideoRtpExtension(
+ const std::string& extensionName,
+ SdpDirectionAttribute::Direction direction =
+ SdpDirectionAttribute::Direction::kSendrecv) override;
+
+ virtual std::vector<UniquePtr<JsepCodecDescription>>& Codecs() override {
+ return mSupportedCodecs;
+ }
+
+ virtual Result CreateOffer(const JsepOfferOptions& options,
+ std::string* offer) override;
+
+ virtual Result CreateAnswer(const JsepAnswerOptions& options,
+ std::string* answer) override;
+
+ virtual std::string GetLocalDescription(
+ JsepDescriptionPendingOrCurrent type) const override;
+
+ virtual std::string GetRemoteDescription(
+ JsepDescriptionPendingOrCurrent type) const override;
+
+ virtual Result SetLocalDescription(JsepSdpType type,
+ const std::string& sdp) override;
+
+ virtual Result SetRemoteDescription(JsepSdpType type,
+ const std::string& sdp) override;
+
+ virtual Result AddRemoteIceCandidate(const std::string& candidate,
+ const std::string& mid,
+ const Maybe<uint16_t>& level,
+ const std::string& ufrag,
+ std::string* transportId) override;
+
+ virtual nsresult AddLocalIceCandidate(const std::string& candidate,
+ const std::string& transportId,
+ const std::string& ufrag,
+ uint16_t* level, std::string* mid,
+ bool* skipped) override;
+
+ virtual nsresult UpdateDefaultCandidate(
+ const std::string& defaultCandidateAddr, uint16_t defaultCandidatePort,
+ const std::string& defaultRtcpCandidateAddr,
+ uint16_t defaultRtcpCandidatePort,
+ const std::string& transportId) override;
+
+ virtual nsresult Close() override;
+
+ virtual const std::string GetLastError() const override;
+
+ virtual const std::vector<std::pair<size_t, std::string>>&
+ GetLastSdpParsingErrors() const override;
+
+ virtual bool IsIceControlling() const override { return mIceControlling; }
+
+ virtual Maybe<bool> IsPendingOfferer() const override {
+ return mIsPendingOfferer;
+ }
+
+ virtual Maybe<bool> IsCurrentOfferer() const override {
+ return mIsCurrentOfferer;
+ }
+
+ virtual bool IsIceRestarting() const override {
+ return !mOldIceUfrag.empty();
+ }
+
+ virtual std::set<std::pair<std::string, std::string>> GetLocalIceCredentials()
+ const override;
+
+ virtual void AddTransceiver(const JsepTransceiver& transceiver) override;
+
+ virtual bool CheckNegotiationNeeded() const override;
+
+ virtual void SetDefaultCodecs(
+ const std::vector<UniquePtr<JsepCodecDescription>>& aPreferredCodecs)
+ override;
+
+ private:
+ friend class JsepSessionTest;
+ virtual const std::vector<JsepTransceiver>& GetTransceivers() const override {
+ return mTransceivers;
+ }
+
+ virtual std::vector<JsepTransceiver>& GetTransceivers() override {
+ return mTransceivers;
+ }
+
+ // Non-const so it can set mLastError
+ nsresult CreateGenericSDP(UniquePtr<Sdp>* sdp);
+ void AddExtmap(SdpMediaSection* msection);
+ std::vector<SdpExtmapAttributeList::Extmap> GetRtpExtensions(
+ const SdpMediaSection& msection);
+ std::string GetNewMid();
+
+ void AddCommonExtmaps(const SdpMediaSection& remoteMsection,
+ SdpMediaSection* msection);
+ uint16_t GetNeverUsedExtmapEntry();
+ nsresult SetupIds();
+ void SetState(JsepSignalingState state);
+ // Non-const so it can set mLastError
+ nsresult ParseSdp(const std::string& sdp, UniquePtr<Sdp>* parsedp);
+ nsresult SetLocalDescriptionOffer(UniquePtr<Sdp> offer);
+ nsresult SetLocalDescriptionAnswer(JsepSdpType type, UniquePtr<Sdp> answer);
+ nsresult SetRemoteDescriptionOffer(UniquePtr<Sdp> offer);
+ nsresult SetRemoteDescriptionAnswer(JsepSdpType type, UniquePtr<Sdp> answer);
+ nsresult ValidateLocalDescription(const Sdp& description, JsepSdpType type);
+ nsresult ValidateRemoteDescription(const Sdp& description);
+ nsresult ValidateOffer(const Sdp& offer);
+ nsresult ValidateAnswer(const Sdp& offer, const Sdp& answer);
+ nsresult UpdateTransceiversFromRemoteDescription(const Sdp& remote);
+ Maybe<JsepTransceiver> GetTransceiverForLevel(size_t level) const;
+ Maybe<JsepTransceiver> GetTransceiverForMid(const std::string& mid) const;
+ Maybe<JsepTransceiver> GetTransceiverForLocal(size_t level);
+ Maybe<JsepTransceiver> GetTransceiverForRemote(
+ const SdpMediaSection& msection);
+ Maybe<JsepTransceiver> GetTransceiverWithTransport(
+ const std::string& transportId) const;
+ // The w3c and IETF specs have a lot of "magical" behavior that happens when
+ // addTrack is used. This was a deliberate design choice. Sadface.
+ Maybe<JsepTransceiver> FindUnassociatedTransceiver(
+ SdpMediaSection::MediaType type, bool magic);
+ // Called for rollback of local description
+ void RollbackLocalOffer();
+ // Called for rollback of remote description
+ void RollbackRemoteOffer();
+ nsresult HandleNegotiatedSession(const UniquePtr<Sdp>& local,
+ const UniquePtr<Sdp>& remote);
+ nsresult AddTransportAttributes(SdpMediaSection* msection,
+ SdpSetupAttribute::Role dtlsRole);
+ nsresult CopyPreviousTransportParams(const Sdp& oldAnswer,
+ const Sdp& offerersPreviousSdp,
+ const Sdp& newOffer, Sdp* newLocal);
+ void EnsureMsid(Sdp* remote);
+ void SetupBundle(Sdp* sdp) const;
+ nsresult CreateOfferMsection(const JsepOfferOptions& options,
+ JsepTransceiver& transceiver, Sdp* local);
+ nsresult CreateAnswerMsection(const JsepAnswerOptions& options,
+ JsepTransceiver& transceiver,
+ const SdpMediaSection& remoteMsection,
+ Sdp* sdp);
+ nsresult DetermineAnswererSetupRole(const SdpMediaSection& remoteMsection,
+ SdpSetupAttribute::Role* rolep);
+ nsresult MakeNegotiatedTransceiver(const SdpMediaSection& remote,
+ const SdpMediaSection& local,
+ JsepTransceiver& transceiverOut);
+ void EnsureHasOwnTransport(const SdpMediaSection& msection,
+ JsepTransceiver& transceiver);
+ void CopyBundleTransports();
+
+ nsresult FinalizeTransport(const SdpAttributeList& remote,
+ const SdpAttributeList& answer,
+ JsepTransport* transport) const;
+
+ nsresult GetNegotiatedBundledMids(SdpHelper::BundledMids* bundledMids);
+
+ nsresult EnableOfferMsection(SdpMediaSection* msection);
+
+ mozilla::Sdp* GetParsedLocalDescription(
+ JsepDescriptionPendingOrCurrent type) const;
+ mozilla::Sdp* GetParsedRemoteDescription(
+ JsepDescriptionPendingOrCurrent type) const;
+ const Sdp* GetAnswer() const;
+ void SetIceRestarting(bool restarting);
+
+ void InitTransceiver(JsepTransceiver& aTransceiver);
+
+ UniquePtr<JsepUuidGenerator> mUuidGen;
+ UniquePtr<Sdp> mGeneratedOffer; // Created but not set.
+ UniquePtr<Sdp> mGeneratedAnswer; // Created but not set.
+ UniquePtr<Sdp> mCurrentLocalDescription;
+ UniquePtr<Sdp> mCurrentRemoteDescription;
+ UniquePtr<Sdp> mPendingLocalDescription;
+ UniquePtr<Sdp> mPendingRemoteDescription;
+ std::vector<UniquePtr<JsepCodecDescription>> mSupportedCodecs;
+ SdpHelper mSdpHelper;
+ UniquePtr<SdpParser> mParser;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/jsep/JsepTrack.cpp b/dom/media/webrtc/jsep/JsepTrack.cpp
new file mode 100644
index 0000000000..f60e4ce0d7
--- /dev/null
+++ b/dom/media/webrtc/jsep/JsepTrack.cpp
@@ -0,0 +1,670 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "jsep/JsepTrack.h"
+#include "jsep/JsepCodecDescription.h"
+#include "jsep/JsepTrackEncoding.h"
+
+#include <algorithm>
+
+namespace mozilla {
+void JsepTrack::GetNegotiatedPayloadTypes(
+ std::vector<uint16_t>* payloadTypes) const {
+ if (!mNegotiatedDetails) {
+ return;
+ }
+
+ for (const auto& encoding : mNegotiatedDetails->mEncodings) {
+ GetPayloadTypes(encoding->GetCodecs(), payloadTypes);
+ }
+
+ // Prune out dupes
+ std::sort(payloadTypes->begin(), payloadTypes->end());
+ auto newEnd = std::unique(payloadTypes->begin(), payloadTypes->end());
+ payloadTypes->erase(newEnd, payloadTypes->end());
+}
+
+/* static */
+void JsepTrack::GetPayloadTypes(
+ const std::vector<UniquePtr<JsepCodecDescription>>& codecs,
+ std::vector<uint16_t>* payloadTypes) {
+ for (const auto& codec : codecs) {
+ uint16_t pt;
+ if (!codec->GetPtAsInt(&pt)) {
+ MOZ_ASSERT(false);
+ continue;
+ }
+ payloadTypes->push_back(pt);
+ }
+}
+
+void JsepTrack::EnsureNoDuplicatePayloadTypes(
+ std::vector<UniquePtr<JsepCodecDescription>>* codecs) {
+ std::set<std::string> uniquePayloadTypes;
+ for (auto& codec : *codecs) {
+ codec->EnsureNoDuplicatePayloadTypes(uniquePayloadTypes);
+ }
+}
+
+void JsepTrack::EnsureSsrcs(SsrcGenerator& ssrcGenerator, size_t aNumber) {
+ while (mSsrcs.size() < aNumber) {
+ uint32_t ssrc, rtxSsrc;
+ if (!ssrcGenerator.GenerateSsrc(&ssrc) ||
+ !ssrcGenerator.GenerateSsrc(&rtxSsrc)) {
+ return;
+ }
+ mSsrcs.push_back(ssrc);
+ mSsrcToRtxSsrc[ssrc] = rtxSsrc;
+ MOZ_ASSERT(mSsrcs.size() == mSsrcToRtxSsrc.size());
+ }
+}
+
+void JsepTrack::PopulateCodecs(
+ const std::vector<UniquePtr<JsepCodecDescription>>& prototype) {
+ mPrototypeCodecs.clear();
+ for (const auto& prototypeCodec : prototype) {
+ if (prototypeCodec->Type() == mType) {
+ mPrototypeCodecs.emplace_back(prototypeCodec->Clone());
+ mPrototypeCodecs.back()->mDirection = mDirection;
+ }
+ }
+
+ EnsureNoDuplicatePayloadTypes(&mPrototypeCodecs);
+}
+
+void JsepTrack::AddToOffer(SsrcGenerator& ssrcGenerator,
+ SdpMediaSection* offer) {
+ AddToMsection(mPrototypeCodecs, offer);
+
+ if (mDirection == sdp::kSend) {
+ std::vector<std::string> rids;
+ if (offer->IsSending()) {
+ rids = mRids;
+ }
+
+ AddToMsection(rids, sdp::kSend, ssrcGenerator,
+ IsRtxEnabled(mPrototypeCodecs), offer);
+ }
+}
+
+void JsepTrack::AddToAnswer(const SdpMediaSection& offer,
+ SsrcGenerator& ssrcGenerator,
+ SdpMediaSection* answer) {
+ // We do not modify mPrototypeCodecs here, since we're only creating an
+ // answer. Once offer/answer concludes, we will update mPrototypeCodecs.
+ std::vector<UniquePtr<JsepCodecDescription>> codecs =
+ NegotiateCodecs(offer, true, Nothing());
+ if (codecs.empty()) {
+ return;
+ }
+
+ AddToMsection(codecs, answer);
+
+ if (mDirection == sdp::kSend) {
+ AddToMsection(mRids, sdp::kSend, ssrcGenerator, IsRtxEnabled(codecs),
+ answer);
+ }
+}
+
+void JsepTrack::SetRids(const std::vector<std::string>& aRids) {
+ MOZ_ASSERT(!aRids.empty());
+ if (!mRids.empty()) {
+ return;
+ }
+ mRids = aRids;
+}
+
+void JsepTrack::SetMaxEncodings(size_t aMax) {
+ mMaxEncodings = aMax;
+ if (mRids.size() > mMaxEncodings) {
+ mRids.resize(mMaxEncodings);
+ }
+}
+
+void JsepTrack::RecvTrackSetRemote(const Sdp& aSdp,
+ const SdpMediaSection& aMsection) {
+ mInHaveRemote = true;
+ MOZ_ASSERT(mDirection == sdp::kRecv);
+ MOZ_ASSERT(aMsection.GetMediaType() !=
+ SdpMediaSection::MediaType::kApplication);
+ std::string error;
+ SdpHelper helper(&error);
+
+ mRemoteSetSendBit = aMsection.IsSending();
+ if (!mRemoteSetSendBit) {
+ mReceptive = false;
+ }
+
+ if (aMsection.IsSending()) {
+ (void)helper.GetIdsFromMsid(aSdp, aMsection, &mStreamIds);
+ } else {
+ mStreamIds.clear();
+ }
+
+ // We do this whether or not the track is active
+ SetCNAME(helper.GetCNAME(aMsection));
+ mSsrcs.clear();
+ if (aMsection.GetAttributeList().HasAttribute(SdpAttribute::kSsrcAttribute)) {
+ for (const auto& ssrcAttr : aMsection.GetAttributeList().GetSsrc().mSsrcs) {
+ mSsrcs.push_back(ssrcAttr.ssrc);
+ }
+ }
+
+ // Use FID ssrc-group to associate rtx ssrcs with "regular" ssrcs. Despite
+ // not being part of RFC 4588, this is how rtx is negotiated by libwebrtc
+ // and jitsi.
+ mSsrcToRtxSsrc.clear();
+ if (aMsection.GetAttributeList().HasAttribute(
+ SdpAttribute::kSsrcGroupAttribute)) {
+ for (const auto& group :
+ aMsection.GetAttributeList().GetSsrcGroup().mSsrcGroups) {
+ if (group.semantics == SdpSsrcGroupAttributeList::kFid &&
+ group.ssrcs.size() == 2) {
+ // Ensure we have a "regular" ssrc for each rtx ssrc.
+ if (std::find(mSsrcs.begin(), mSsrcs.end(), group.ssrcs[0]) !=
+ mSsrcs.end()) {
+ mSsrcToRtxSsrc[group.ssrcs[0]] = group.ssrcs[1];
+
+ // Remove rtx ssrcs from mSsrcs
+ auto res = std::remove_if(
+ mSsrcs.begin(), mSsrcs.end(),
+ [group](uint32_t ssrc) { return ssrc == group.ssrcs[1]; });
+ mSsrcs.erase(res, mSsrcs.end());
+ }
+ }
+ }
+ }
+}
+
+void JsepTrack::RecvTrackSetLocal(const SdpMediaSection& aMsection) {
+ MOZ_ASSERT(mDirection == sdp::kRecv);
+
+ // TODO: Should more stuff live in here? Anything that needs to happen when we
+ // decide we're ready to receive packets should probably go in here.
+ mReceptive = aMsection.IsReceiving();
+}
+
+void JsepTrack::SendTrackSetRemote(SsrcGenerator& aSsrcGenerator,
+ const SdpMediaSection& aRemoteMsection) {
+ mInHaveRemote = true;
+ if (mType == SdpMediaSection::kApplication) {
+ return;
+ }
+
+ std::vector<SdpRidAttributeList::Rid> rids;
+
+ // TODO: Current language in webrtc-pc is completely broken, and so I will
+ // not be quoting it here.
+ if ((mType == SdpMediaSection::kVideo) &&
+ aRemoteMsection.GetAttributeList().HasAttribute(
+ SdpAttribute::kSimulcastAttribute)) {
+ // Note: webrtc-pc does not appear to support the full IETF simulcast
+ // spec. In particular, the IETF simulcast spec supports requesting
+ // multiple different sets of encodings. For example, "a=simulcast:send
+ // 1,2;3,4;5,6" means that there are three simulcast streams, the first of
+ // which can use either rid 1 or 2 (but not both), the second of which can
+ // use rid 3 or 4 (but not both), and the third of which can use rid 5 or
+ // 6 (but not both). webrtc-pc does not support this either/or stuff for
+ // rid; each simulcast stream gets exactly one rid.
+ // Also, webrtc-pc does not support the '~' pause syntax at all
+ // See https://github.com/w3c/webrtc-pc/issues/2769
+ GetRids(aRemoteMsection, sdp::kRecv, &rids);
+ }
+
+ if (mRids.empty()) {
+ // Initial configuration
+ for (const auto& ridAttr : rids) {
+ // TODO: Spec might change, making a length > 16 invalid SDP.
+ std::string dummy;
+ if (SdpRidAttributeList::CheckRidValidity(ridAttr.id, &dummy) &&
+ ridAttr.id.size() <= SdpRidAttributeList::kMaxRidLength) {
+ mRids.push_back(ridAttr.id);
+ }
+ }
+ if (mRids.size() > mMaxEncodings) {
+ mRids.resize(mMaxEncodings);
+ }
+ } else {
+ // JSEP is allowed to remove or reorder rids. RTCRtpSender won't pay
+ // attention to reordering.
+ std::vector<std::string> newRids;
+ for (const auto& ridAttr : rids) {
+ for (const auto& oldRid : mRids) {
+ if (oldRid == ridAttr.id) {
+ newRids.push_back(oldRid);
+ break;
+ }
+ }
+ }
+ mRids = std::move(newRids);
+ }
+
+ if (mRids.empty()) {
+ mRids.push_back("");
+ }
+
+ UpdateSsrcs(aSsrcGenerator, mRids.size());
+}
+
+void JsepTrack::AddToMsection(
+ const std::vector<UniquePtr<JsepCodecDescription>>& codecs,
+ SdpMediaSection* msection) const {
+ MOZ_ASSERT(msection->GetMediaType() == mType);
+ MOZ_ASSERT(!codecs.empty());
+
+ for (const auto& codec : codecs) {
+ codec->AddToMediaSection(*msection);
+ }
+
+ if ((mDirection == sdp::kSend) && (mType != SdpMediaSection::kApplication) &&
+ msection->IsSending()) {
+ if (mStreamIds.empty()) {
+ msection->AddMsid("-", mTrackId);
+ } else {
+ for (const std::string& streamId : mStreamIds) {
+ msection->AddMsid(streamId, mTrackId);
+ }
+ }
+ }
+}
+
+void JsepTrack::UpdateSsrcs(SsrcGenerator& ssrcGenerator, size_t encodings) {
+ MOZ_ASSERT(mDirection == sdp::kSend);
+ MOZ_ASSERT(mType != SdpMediaSection::kApplication);
+ size_t numSsrcs = std::max<size_t>(encodings, 1U);
+
+ EnsureSsrcs(ssrcGenerator, numSsrcs);
+ PruneSsrcs(numSsrcs);
+ if (mNegotiatedDetails && mNegotiatedDetails->GetEncodingCount() > numSsrcs) {
+ mNegotiatedDetails->TruncateEncodings(numSsrcs);
+ }
+
+ MOZ_ASSERT(!mSsrcs.empty());
+}
+
+void JsepTrack::PruneSsrcs(size_t aNumSsrcs) {
+ mSsrcs.resize(aNumSsrcs);
+
+ // We might have duplicate entries in mSsrcs, so we need to resize first and
+ // then remove ummatched rtx ssrcs.
+ auto itor = mSsrcToRtxSsrc.begin();
+ while (itor != mSsrcToRtxSsrc.end()) {
+ if (std::find(mSsrcs.begin(), mSsrcs.end(), itor->first) == mSsrcs.end()) {
+ itor = mSsrcToRtxSsrc.erase(itor);
+ } else {
+ ++itor;
+ }
+ }
+}
+
+bool JsepTrack::IsRtxEnabled(
+ const std::vector<UniquePtr<JsepCodecDescription>>& codecs) const {
+ for (const auto& codec : codecs) {
+ if (codec->Type() == SdpMediaSection::kVideo &&
+ static_cast<const JsepVideoCodecDescription*>(codec.get())
+ ->mRtxEnabled) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void JsepTrack::AddToMsection(const std::vector<std::string>& aRids,
+ sdp::Direction direction,
+ SsrcGenerator& ssrcGenerator, bool rtxEnabled,
+ SdpMediaSection* msection) {
+ if (aRids.size() > 1) {
+ UniquePtr<SdpSimulcastAttribute> simulcast(new SdpSimulcastAttribute);
+ UniquePtr<SdpRidAttributeList> ridAttrs(new SdpRidAttributeList);
+ for (const std::string& rid : aRids) {
+ SdpRidAttributeList::Rid ridAttr;
+ ridAttr.id = rid;
+ ridAttr.direction = direction;
+ ridAttrs->mRids.push_back(ridAttr);
+
+ SdpSimulcastAttribute::Version version;
+ version.choices.push_back(SdpSimulcastAttribute::Encoding(rid, false));
+ if (direction == sdp::kSend) {
+ simulcast->sendVersions.push_back(version);
+ } else {
+ simulcast->recvVersions.push_back(version);
+ }
+ }
+
+ msection->GetAttributeList().SetAttribute(simulcast.release());
+ msection->GetAttributeList().SetAttribute(ridAttrs.release());
+ }
+
+ bool requireRtxSsrcs = rtxEnabled && msection->IsSending();
+
+ if (mType != SdpMediaSection::kApplication && mDirection == sdp::kSend) {
+ UpdateSsrcs(ssrcGenerator, aRids.size());
+
+ if (requireRtxSsrcs) {
+ MOZ_ASSERT(mSsrcs.size() == mSsrcToRtxSsrc.size());
+ std::vector<uint32_t> allSsrcs;
+ UniquePtr<SdpSsrcGroupAttributeList> group(new SdpSsrcGroupAttributeList);
+ for (const auto& ssrc : mSsrcs) {
+ const auto rtxSsrc = mSsrcToRtxSsrc[ssrc];
+ allSsrcs.push_back(ssrc);
+ allSsrcs.push_back(rtxSsrc);
+ group->PushEntry(SdpSsrcGroupAttributeList::kFid, {ssrc, rtxSsrc});
+ }
+ msection->SetSsrcs(allSsrcs, mCNAME);
+ msection->GetAttributeList().SetAttribute(group.release());
+ } else {
+ msection->SetSsrcs(mSsrcs, mCNAME);
+ }
+ }
+}
+
+void JsepTrack::GetRids(const SdpMediaSection& msection,
+ sdp::Direction direction,
+ std::vector<SdpRidAttributeList::Rid>* rids) const {
+ rids->clear();
+ if (!msection.GetAttributeList().HasAttribute(
+ SdpAttribute::kSimulcastAttribute)) {
+ return;
+ }
+
+ const SdpSimulcastAttribute& simulcast(
+ msection.GetAttributeList().GetSimulcast());
+
+ const SdpSimulcastAttribute::Versions* versions = nullptr;
+ switch (direction) {
+ case sdp::kSend:
+ versions = &simulcast.sendVersions;
+ break;
+ case sdp::kRecv:
+ versions = &simulcast.recvVersions;
+ break;
+ }
+
+ if (!versions->IsSet()) {
+ return;
+ }
+
+ // RFC 8853 does not seem to forbid duplicate rids in a simulcast attribute.
+ // So, while this is obviously silly, we should be prepared for it and
+ // ignore those duplicate rids.
+ std::set<std::string> uniqueRids;
+ for (const SdpSimulcastAttribute::Version& version : *versions) {
+ if (!version.choices.empty() && !uniqueRids.count(version.choices[0].rid)) {
+ // We validate that rids are present (and sane) elsewhere.
+ rids->push_back(*msection.FindRid(version.choices[0].rid));
+ uniqueRids.insert(version.choices[0].rid);
+ }
+ }
+}
+
+void JsepTrack::CreateEncodings(
+ const SdpMediaSection& remote,
+ const std::vector<UniquePtr<JsepCodecDescription>>& negotiatedCodecs,
+ JsepTrackNegotiatedDetails* negotiatedDetails) {
+ negotiatedDetails->mTias = remote.GetBandwidth("TIAS");
+
+ webrtc::RtcpMode rtcpMode = webrtc::RtcpMode::kCompound;
+ // rtcp-rsize (video only)
+ if (remote.GetMediaType() == SdpMediaSection::kVideo &&
+ remote.GetAttributeList().HasAttribute(
+ SdpAttribute::kRtcpRsizeAttribute)) {
+ rtcpMode = webrtc::RtcpMode::kReducedSize;
+ }
+ negotiatedDetails->mRtpRtcpConf = RtpRtcpConfig(rtcpMode);
+
+ // TODO add support for b=AS if TIAS is not set (bug 976521)
+
+ if (mRids.empty()) {
+ mRids.push_back("");
+ }
+
+ size_t numEncodings = mRids.size();
+
+ // Drop SSRCs if fewer RIDs were offered than we have encodings
+ if (mSsrcs.size() > numEncodings) {
+ PruneSsrcs(numEncodings);
+ }
+
+ // For each stream make sure we have an encoding, and configure
+ // that encoding appropriately.
+ for (size_t i = 0; i < numEncodings; ++i) {
+ UniquePtr<JsepTrackEncoding> encoding(new JsepTrackEncoding);
+ if (mRids.size() > i) {
+ encoding->mRid = mRids[i];
+ }
+ for (const auto& codec : negotiatedCodecs) {
+ encoding->AddCodec(*codec);
+ }
+ negotiatedDetails->mEncodings.push_back(std::move(encoding));
+ }
+}
+
+std::vector<UniquePtr<JsepCodecDescription>> JsepTrack::GetCodecClones() const {
+ std::vector<UniquePtr<JsepCodecDescription>> clones;
+ for (const auto& codec : mPrototypeCodecs) {
+ clones.emplace_back(codec->Clone());
+ }
+ return clones;
+}
+
+static bool CompareCodec(const UniquePtr<JsepCodecDescription>& lhs,
+ const UniquePtr<JsepCodecDescription>& rhs) {
+ return lhs->mStronglyPreferred && !rhs->mStronglyPreferred;
+}
+
+std::vector<UniquePtr<JsepCodecDescription>> JsepTrack::NegotiateCodecs(
+ const SdpMediaSection& remote, bool remoteIsOffer,
+ Maybe<const SdpMediaSection&> local) {
+ std::vector<UniquePtr<JsepCodecDescription>> negotiatedCodecs;
+ std::vector<UniquePtr<JsepCodecDescription>> newPrototypeCodecs;
+
+ // Outer loop establishes the remote side's preference
+ for (const std::string& fmt : remote.GetFormats()) {
+ for (auto& codec : mPrototypeCodecs) {
+ if (!codec || !codec->mEnabled || !codec->Matches(fmt, remote)) {
+ continue;
+ }
+
+ // First codec of ours that matches. See if we can negotiate it.
+ UniquePtr<JsepCodecDescription> clone(codec->Clone());
+ if (clone->Negotiate(fmt, remote, remoteIsOffer, local)) {
+ // If negotiation succeeded, remember the payload type the other side
+ // used for reoffers.
+ codec->mDefaultPt = fmt;
+
+ // Remember whether we negotiated rtx and the associated pt for later.
+ if (codec->Type() == SdpMediaSection::kVideo) {
+ JsepVideoCodecDescription* videoCodec =
+ static_cast<JsepVideoCodecDescription*>(codec.get());
+ JsepVideoCodecDescription* cloneVideoCodec =
+ static_cast<JsepVideoCodecDescription*>(clone.get());
+ bool useRtx =
+ mRtxIsAllowed &&
+ Preferences::GetBool("media.peerconnection.video.use_rtx", false);
+ videoCodec->mRtxEnabled = useRtx && cloneVideoCodec->mRtxEnabled;
+ videoCodec->mRtxPayloadType = cloneVideoCodec->mRtxPayloadType;
+ }
+
+ // Moves the codec out of mPrototypeCodecs, leaving an empty
+ // UniquePtr, so we don't use it again. Also causes successfully
+ // negotiated codecs to be placed up front in the future.
+ newPrototypeCodecs.emplace_back(std::move(codec));
+ negotiatedCodecs.emplace_back(std::move(clone));
+ break;
+ }
+ }
+ }
+
+ // newPrototypeCodecs contains just the negotiated stuff so far. Add the rest.
+ for (auto& codec : mPrototypeCodecs) {
+ if (codec) {
+ newPrototypeCodecs.emplace_back(std::move(codec));
+ }
+ }
+
+ // Negotiated stuff is up front, so it will take precedence when ensuring
+ // there are no duplicate payload types.
+ EnsureNoDuplicatePayloadTypes(&newPrototypeCodecs);
+ std::swap(newPrototypeCodecs, mPrototypeCodecs);
+
+ // Find the (potential) red codec and ulpfec codec or telephone-event
+ JsepVideoCodecDescription* red = nullptr;
+ JsepVideoCodecDescription* ulpfec = nullptr;
+ JsepAudioCodecDescription* dtmf = nullptr;
+ // We can safely cast here since JsepTrack has a MediaType and only codecs
+ // that match that MediaType (kAudio or kVideo) are added.
+ for (auto& codec : negotiatedCodecs) {
+ if (codec->mName == "red") {
+ red = static_cast<JsepVideoCodecDescription*>(codec.get());
+ } else if (codec->mName == "ulpfec") {
+ ulpfec = static_cast<JsepVideoCodecDescription*>(codec.get());
+ } else if (codec->mName == "telephone-event") {
+ dtmf = static_cast<JsepAudioCodecDescription*>(codec.get());
+ }
+ }
+ // if we have a red codec remove redundant encodings that don't exist
+ if (red) {
+ // Since we could have an externally specified redundant endcodings
+ // list, we shouldn't simply rebuild the redundant encodings list
+ // based on the current list of codecs.
+ std::vector<uint8_t> unnegotiatedEncodings;
+ std::swap(unnegotiatedEncodings, red->mRedundantEncodings);
+ for (auto redundantPt : unnegotiatedEncodings) {
+ std::string pt = std::to_string(redundantPt);
+ for (const auto& codec : negotiatedCodecs) {
+ if (pt == codec->mDefaultPt) {
+ red->mRedundantEncodings.push_back(redundantPt);
+ break;
+ }
+ }
+ }
+ }
+ // Video FEC is indicated by the existence of the red and ulpfec
+ // codecs and not an attribute on the particular video codec (like in
+ // a rtcpfb attr). If we see both red and ulpfec codecs, we enable FEC
+ // on all the other codecs.
+ if (red && ulpfec) {
+ for (auto& codec : negotiatedCodecs) {
+ if (codec->mName != "red" && codec->mName != "ulpfec") {
+ JsepVideoCodecDescription* videoCodec =
+ static_cast<JsepVideoCodecDescription*>(codec.get());
+ videoCodec->EnableFec(red->mDefaultPt, ulpfec->mDefaultPt);
+ }
+ }
+ }
+
+ // Dtmf support is indicated by the existence of the telephone-event
+ // codec, and not an attribute on the particular audio codec (like in a
+ // rtcpfb attr). If we see the telephone-event codec, we enabled dtmf
+ // support on all the other audio codecs.
+ if (dtmf) {
+ for (auto& codec : negotiatedCodecs) {
+ JsepAudioCodecDescription* audioCodec =
+ static_cast<JsepAudioCodecDescription*>(codec.get());
+ audioCodec->mDtmfEnabled = true;
+ }
+ }
+
+ // Make sure strongly preferred codecs are up front, overriding the remote
+ // side's preference.
+ std::stable_sort(negotiatedCodecs.begin(), negotiatedCodecs.end(),
+ CompareCodec);
+
+ if (!red) {
+ // No red, remove ulpfec
+ negotiatedCodecs.erase(
+ std::remove_if(negotiatedCodecs.begin(), negotiatedCodecs.end(),
+ [ulpfec](const UniquePtr<JsepCodecDescription>& codec) {
+ return codec.get() == ulpfec;
+ }),
+ negotiatedCodecs.end());
+ // Make sure there's no dangling ptr here
+ ulpfec = nullptr;
+ }
+
+ return negotiatedCodecs;
+}
+
+nsresult JsepTrack::Negotiate(const SdpMediaSection& answer,
+ const SdpMediaSection& remote,
+ const SdpMediaSection& local) {
+ std::vector<UniquePtr<JsepCodecDescription>> negotiatedCodecs =
+ NegotiateCodecs(remote, &answer != &remote, SomeRef(local));
+
+ if (negotiatedCodecs.empty()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ UniquePtr<JsepTrackNegotiatedDetails> negotiatedDetails =
+ MakeUnique<JsepTrackNegotiatedDetails>();
+
+ CreateEncodings(remote, negotiatedCodecs, negotiatedDetails.get());
+
+ if (answer.GetAttributeList().HasAttribute(SdpAttribute::kExtmapAttribute)) {
+ for (auto& extmapAttr : answer.GetAttributeList().GetExtmap().mExtmaps) {
+ SdpDirectionAttribute::Direction direction = extmapAttr.direction;
+ if (&remote == &answer) {
+ // Answer is remote, we need to flip this.
+ direction = reverse(direction);
+ }
+
+ if (direction & mDirection) {
+ negotiatedDetails->mExtmap[extmapAttr.extensionname] = extmapAttr;
+ }
+ }
+ }
+
+ mInHaveRemote = false;
+ mNegotiatedDetails = std::move(negotiatedDetails);
+ return NS_OK;
+}
+
+// When doing bundle, if all else fails we can try to figure out which m-line a
+// given RTP packet belongs to by looking at the payload type field. This only
+// works, however, if that payload type appeared in only one m-section.
+// We figure that out here.
+/* static */
+void JsepTrack::SetUniquePayloadTypes(std::vector<JsepTrack*>& tracks) {
+ // Maps to track details if no other track contains the payload type,
+ // otherwise maps to nullptr.
+ std::map<uint16_t, JsepTrackNegotiatedDetails*> payloadTypeToDetailsMap;
+
+ for (JsepTrack* track : tracks) {
+ if (track->GetMediaType() == SdpMediaSection::kApplication) {
+ continue;
+ }
+
+ auto* details = track->GetNegotiatedDetails();
+ if (!details) {
+ // Can happen if negotiation fails on a track
+ continue;
+ }
+
+ std::vector<uint16_t> payloadTypesForTrack;
+ track->GetNegotiatedPayloadTypes(&payloadTypesForTrack);
+
+ for (uint16_t pt : payloadTypesForTrack) {
+ if (payloadTypeToDetailsMap.count(pt)) {
+ // Found in more than one track, not unique
+ payloadTypeToDetailsMap[pt] = nullptr;
+ } else {
+ payloadTypeToDetailsMap[pt] = details;
+ }
+ }
+ }
+
+ for (auto ptAndDetails : payloadTypeToDetailsMap) {
+ uint16_t uniquePt = ptAndDetails.first;
+ MOZ_ASSERT(uniquePt <= UINT8_MAX);
+ auto trackDetails = ptAndDetails.second;
+
+ if (trackDetails) {
+ trackDetails->mUniquePayloadTypes.push_back(
+ static_cast<uint8_t>(uniquePt));
+ }
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/jsep/JsepTrack.h b/dom/media/webrtc/jsep/JsepTrack.h
new file mode 100644
index 0000000000..70583ccb71
--- /dev/null
+++ b/dom/media/webrtc/jsep/JsepTrack.h
@@ -0,0 +1,305 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _JSEPTRACK_H_
+#define _JSEPTRACK_H_
+
+#include <functional>
+#include <algorithm>
+#include <string>
+#include <map>
+#include <set>
+#include <vector>
+
+#include <mozilla/UniquePtr.h>
+#include "mozilla/Preferences.h"
+#include "nsError.h"
+
+#include "jsep/JsepTrackEncoding.h"
+#include "jsep/SsrcGenerator.h"
+#include "sdp/Sdp.h"
+#include "sdp/SdpAttribute.h"
+#include "sdp/SdpMediaSection.h"
+#include "libwebrtcglue/RtpRtcpConfig.h"
+namespace mozilla {
+
+class JsepTrackNegotiatedDetails {
+ public:
+ JsepTrackNegotiatedDetails()
+ : mTias(0), mRtpRtcpConf(webrtc::RtcpMode::kCompound) {}
+
+ JsepTrackNegotiatedDetails(const JsepTrackNegotiatedDetails& orig)
+ : mExtmap(orig.mExtmap),
+ mUniquePayloadTypes(orig.mUniquePayloadTypes),
+ mTias(orig.mTias),
+ mRtpRtcpConf(orig.mRtpRtcpConf) {
+ for (const auto& encoding : orig.mEncodings) {
+ mEncodings.emplace_back(new JsepTrackEncoding(*encoding));
+ }
+ }
+
+ size_t GetEncodingCount() const { return mEncodings.size(); }
+
+ const JsepTrackEncoding& GetEncoding(size_t index) const {
+ MOZ_RELEASE_ASSERT(index < mEncodings.size());
+ return *mEncodings[index];
+ }
+
+ void TruncateEncodings(size_t aSize) {
+ if (mEncodings.size() < aSize) {
+ MOZ_ASSERT(false);
+ return;
+ }
+ mEncodings.resize(aSize);
+ }
+
+ const SdpExtmapAttributeList::Extmap* GetExt(
+ const std::string& ext_name) const {
+ auto it = mExtmap.find(ext_name);
+ if (it != mExtmap.end()) {
+ return &it->second;
+ }
+ return nullptr;
+ }
+
+ void ForEachRTPHeaderExtension(
+ const std::function<void(const SdpExtmapAttributeList::Extmap& extmap)>&
+ fn) const {
+ for (auto entry : mExtmap) {
+ fn(entry.second);
+ }
+ }
+
+ std::vector<uint8_t> GetUniquePayloadTypes() const {
+ return mUniquePayloadTypes;
+ }
+
+ uint32_t GetTias() const { return mTias; }
+
+ RtpRtcpConfig GetRtpRtcpConfig() const { return mRtpRtcpConf; }
+
+ private:
+ friend class JsepTrack;
+
+ std::map<std::string, SdpExtmapAttributeList::Extmap> mExtmap;
+ std::vector<uint8_t> mUniquePayloadTypes;
+ std::vector<UniquePtr<JsepTrackEncoding>> mEncodings;
+ uint32_t mTias; // bits per second
+ RtpRtcpConfig mRtpRtcpConf;
+};
+
+class JsepTrack {
+ public:
+ JsepTrack(mozilla::SdpMediaSection::MediaType type, sdp::Direction direction)
+ : mType(type),
+ mDirection(direction),
+ mActive(false),
+ mRemoteSetSendBit(false) {}
+
+ virtual ~JsepTrack() {}
+
+ void UpdateStreamIds(const std::vector<std::string>& streamIds) {
+ mStreamIds = streamIds;
+ }
+
+ void SetTrackId(const std::string& aTrackId) { mTrackId = aTrackId; }
+
+ void ClearStreamIds() { mStreamIds.clear(); }
+
+ void RecvTrackSetRemote(const Sdp& aSdp, const SdpMediaSection& aMsection);
+ void RecvTrackSetLocal(const SdpMediaSection& aMsection);
+
+ // This is called whenever a remote description is set; we do not wait for
+ // offer/answer to complete, since there's nothing to actually negotiate here.
+ void SendTrackSetRemote(SsrcGenerator& aSsrcGenerator,
+ const SdpMediaSection& aRemoteMsection);
+
+ JsepTrack(const JsepTrack& orig) { *this = orig; }
+
+ JsepTrack(JsepTrack&& orig) = default;
+ JsepTrack& operator=(JsepTrack&& rhs) = default;
+
+ JsepTrack& operator=(const JsepTrack& rhs) {
+ if (this != &rhs) {
+ mType = rhs.mType;
+ mStreamIds = rhs.mStreamIds;
+ mTrackId = rhs.mTrackId;
+ mCNAME = rhs.mCNAME;
+ mDirection = rhs.mDirection;
+ mRids = rhs.mRids;
+ mSsrcs = rhs.mSsrcs;
+ mSsrcToRtxSsrc = rhs.mSsrcToRtxSsrc;
+ mActive = rhs.mActive;
+ mRemoteSetSendBit = rhs.mRemoteSetSendBit;
+ mReceptive = rhs.mReceptive;
+ mMaxEncodings = rhs.mMaxEncodings;
+ mInHaveRemote = rhs.mInHaveRemote;
+ mRtxIsAllowed = rhs.mRtxIsAllowed;
+
+ mPrototypeCodecs.clear();
+ for (const auto& codec : rhs.mPrototypeCodecs) {
+ mPrototypeCodecs.emplace_back(codec->Clone());
+ }
+ if (rhs.mNegotiatedDetails) {
+ mNegotiatedDetails.reset(
+ new JsepTrackNegotiatedDetails(*rhs.mNegotiatedDetails));
+ }
+ }
+ return *this;
+ }
+
+ virtual mozilla::SdpMediaSection::MediaType GetMediaType() const {
+ return mType;
+ }
+
+ virtual const std::vector<std::string>& GetStreamIds() const {
+ return mStreamIds;
+ }
+
+ virtual const std::string& GetCNAME() const { return mCNAME; }
+
+ virtual void SetCNAME(const std::string& cname) { mCNAME = cname; }
+
+ virtual sdp::Direction GetDirection() const { return mDirection; }
+
+ virtual const std::vector<uint32_t>& GetSsrcs() const { return mSsrcs; }
+
+ virtual std::vector<uint32_t> GetRtxSsrcs() const {
+ std::vector<uint32_t> result;
+ if (mRtxIsAllowed &&
+ Preferences::GetBool("media.peerconnection.video.use_rtx", false)) {
+ std::for_each(
+ mSsrcToRtxSsrc.begin(), mSsrcToRtxSsrc.end(),
+ [&result](const auto& pair) { result.push_back(pair.second); });
+ }
+ return result;
+ }
+
+ virtual void EnsureSsrcs(SsrcGenerator& ssrcGenerator, size_t aNumber);
+
+ bool GetActive() const { return mActive; }
+
+ void SetActive(bool active) { mActive = active; }
+
+ bool GetRemoteSetSendBit() const { return mRemoteSetSendBit; }
+ bool GetReceptive() const { return mReceptive; }
+
+ virtual void PopulateCodecs(
+ const std::vector<UniquePtr<JsepCodecDescription>>& prototype);
+
+ template <class UnaryFunction>
+ void ForEachCodec(UnaryFunction func) {
+ std::for_each(mPrototypeCodecs.begin(), mPrototypeCodecs.end(), func);
+ }
+
+ template <class BinaryPredicate>
+ void SortCodecs(BinaryPredicate sorter) {
+ std::stable_sort(mPrototypeCodecs.begin(), mPrototypeCodecs.end(), sorter);
+ }
+
+ // These two are non-const because this is where ssrcs are chosen.
+ virtual void AddToOffer(SsrcGenerator& ssrcGenerator, SdpMediaSection* offer);
+ virtual void AddToAnswer(const SdpMediaSection& offer,
+ SsrcGenerator& ssrcGenerator,
+ SdpMediaSection* answer);
+
+ virtual nsresult Negotiate(const SdpMediaSection& answer,
+ const SdpMediaSection& remote,
+ const SdpMediaSection& local);
+ static void SetUniquePayloadTypes(std::vector<JsepTrack*>& tracks);
+ virtual void GetNegotiatedPayloadTypes(
+ std::vector<uint16_t>* payloadTypes) const;
+
+ // This will be set when negotiation is carried out.
+ virtual const JsepTrackNegotiatedDetails* GetNegotiatedDetails() const {
+ if (mNegotiatedDetails) {
+ return mNegotiatedDetails.get();
+ }
+ return nullptr;
+ }
+
+ virtual JsepTrackNegotiatedDetails* GetNegotiatedDetails() {
+ if (mNegotiatedDetails) {
+ return mNegotiatedDetails.get();
+ }
+ return nullptr;
+ }
+
+ virtual void ClearNegotiatedDetails() { mNegotiatedDetails.reset(); }
+
+ void SetRids(const std::vector<std::string>& aRids);
+ void ClearRids() { mRids.clear(); }
+ const std::vector<std::string>& GetRids() const { return mRids; }
+
+ void AddToMsection(const std::vector<std::string>& aRids,
+ sdp::Direction direction, SsrcGenerator& ssrcGenerator,
+ bool rtxEnabled, SdpMediaSection* msection);
+
+ // See Bug 1642419, this can be removed when all sites are working with RTX.
+ void SetRtxIsAllowed(bool aRtxIsAllowed) { mRtxIsAllowed = aRtxIsAllowed; }
+
+ void SetMaxEncodings(size_t aMax);
+ bool IsInHaveRemote() const { return mInHaveRemote; }
+
+ private:
+ std::vector<UniquePtr<JsepCodecDescription>> GetCodecClones() const;
+ static void EnsureNoDuplicatePayloadTypes(
+ std::vector<UniquePtr<JsepCodecDescription>>* codecs);
+ static void GetPayloadTypes(
+ const std::vector<UniquePtr<JsepCodecDescription>>& codecs,
+ std::vector<uint16_t>* pts);
+ void AddToMsection(const std::vector<UniquePtr<JsepCodecDescription>>& codecs,
+ SdpMediaSection* msection) const;
+ void GetRids(const SdpMediaSection& msection, sdp::Direction direction,
+ std::vector<SdpRidAttributeList::Rid>* rids) const;
+ void CreateEncodings(
+ const SdpMediaSection& remote,
+ const std::vector<UniquePtr<JsepCodecDescription>>& negotiatedCodecs,
+ JsepTrackNegotiatedDetails* details);
+
+ virtual std::vector<UniquePtr<JsepCodecDescription>> NegotiateCodecs(
+ const SdpMediaSection& remote, bool remoteIsOffer,
+ Maybe<const SdpMediaSection&> local);
+
+ void UpdateSsrcs(SsrcGenerator& ssrcGenerator, size_t encodings);
+ void PruneSsrcs(size_t aNumSsrcs);
+ bool IsRtxEnabled(
+ const std::vector<UniquePtr<JsepCodecDescription>>& codecs) const;
+
+ mozilla::SdpMediaSection::MediaType mType;
+ // These are the ids that everyone outside of JsepSession care about
+ std::vector<std::string> mStreamIds;
+ std::string mTrackId;
+ std::string mCNAME;
+ sdp::Direction mDirection;
+ std::vector<UniquePtr<JsepCodecDescription>> mPrototypeCodecs;
+ // List of rids. May be initially populated from JS, or from a remote SDP.
+ // Can be updated by remote SDP. If no negotiation has taken place at all,
+ // this will be empty. If negotiation has taken place, but no simulcast
+ // attr was negotiated, this will contain the empty string as a single
+ // element. If a simulcast attribute was negotiated, this will contain the
+ // negotiated rids.
+ std::vector<std::string> mRids;
+ UniquePtr<JsepTrackNegotiatedDetails> mNegotiatedDetails;
+ std::vector<uint32_t> mSsrcs;
+ std::map<uint32_t, uint32_t> mSsrcToRtxSsrc;
+ bool mActive;
+ bool mRemoteSetSendBit;
+ // This is used to drive RTCRtpTransceiver.[[Receptive]]. Basically, this
+ // denotes whether we are prepared to receive RTP. When we apply a local
+ // description with the recv bit set, this is set to true, even if we have
+ // not seen the remote description yet. If we apply either a local or remote
+ // description without the recv bit set (from our perspective), this is set
+ // to false.
+ bool mReceptive = false;
+ size_t mMaxEncodings = 3;
+ bool mInHaveRemote = false;
+
+ // See Bug 1642419, this can be removed when all sites are working with RTX.
+ bool mRtxIsAllowed = true;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/jsep/JsepTrackEncoding.h b/dom/media/webrtc/jsep/JsepTrackEncoding.h
new file mode 100644
index 0000000000..c17dd8534e
--- /dev/null
+++ b/dom/media/webrtc/jsep/JsepTrackEncoding.h
@@ -0,0 +1,62 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _JESPTRACKENCODING_H_
+#define _JESPTRACKENCODING_H_
+
+#include "jsep/JsepCodecDescription.h"
+#include "common/EncodingConstraints.h"
+
+#include <vector>
+
+namespace mozilla {
+// Represents a single encoding of a media track. When simulcast is used, there
+// may be multiple. Each encoding may have some constraints (imposed by JS), and
+// may be able to use any one of multiple codecs (JsepCodecDescription) at any
+// given time.
+class JsepTrackEncoding {
+ public:
+ JsepTrackEncoding() = default;
+ JsepTrackEncoding(const JsepTrackEncoding& orig) { *this = orig; }
+
+ JsepTrackEncoding(JsepTrackEncoding&& aOrig) = default;
+
+ JsepTrackEncoding& operator=(const JsepTrackEncoding& aRhs) {
+ if (this != &aRhs) {
+ mRid = aRhs.mRid;
+ mCodecs.clear();
+ for (const auto& codec : aRhs.mCodecs) {
+ mCodecs.emplace_back(codec->Clone());
+ }
+ }
+ return *this;
+ }
+
+ const std::vector<UniquePtr<JsepCodecDescription>>& GetCodecs() const {
+ return mCodecs;
+ }
+
+ void AddCodec(const JsepCodecDescription& codec) {
+ mCodecs.emplace_back(codec.Clone());
+ }
+
+ bool HasFormat(const std::string& format) const {
+ for (const auto& codec : mCodecs) {
+ if (codec->mDefaultPt == format) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ std::string mRid;
+
+ private:
+ std::vector<UniquePtr<JsepCodecDescription>> mCodecs;
+};
+} // namespace mozilla
+
+#endif // _JESPTRACKENCODING_H_
diff --git a/dom/media/webrtc/jsep/JsepTransceiver.h b/dom/media/webrtc/jsep/JsepTransceiver.h
new file mode 100644
index 0000000000..45c9b85120
--- /dev/null
+++ b/dom/media/webrtc/jsep/JsepTransceiver.h
@@ -0,0 +1,220 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _JSEPTRANSCEIVER_H_
+#define _JSEPTRANSCEIVER_H_
+
+#include <string>
+
+#include "sdp/SdpAttribute.h"
+#include "sdp/SdpMediaSection.h"
+#include "sdp/Sdp.h"
+#include "jsep/JsepTransport.h"
+#include "jsep/JsepTrack.h"
+
+#include "nsError.h"
+
+namespace mozilla {
+
+class JsepUuidGenerator {
+ public:
+ virtual ~JsepUuidGenerator() = default;
+ virtual bool Generate(std::string* id) = 0;
+ virtual JsepUuidGenerator* Clone() const = 0;
+};
+
+class JsepTransceiver {
+ public:
+ explicit JsepTransceiver(SdpMediaSection::MediaType type,
+ JsepUuidGenerator& aUuidGen,
+ SdpDirectionAttribute::Direction jsDirection =
+ SdpDirectionAttribute::kSendrecv)
+ : mJsDirection(jsDirection),
+ mSendTrack(type, sdp::kSend),
+ mRecvTrack(type, sdp::kRecv),
+ mLevel(SIZE_MAX),
+ mBundleLevel(SIZE_MAX),
+ mAddTrackMagic(false),
+ mOnlyExistsBecauseOfSetRemote(false),
+ mStopped(false),
+ mRemoved(false),
+ mNegotiated(false),
+ mCanRecycle(false) {
+ if (!aUuidGen.Generate(&mUuid)) {
+ MOZ_CRASH();
+ }
+ }
+
+ JsepTransceiver(const JsepTransceiver& orig) = default;
+ JsepTransceiver(JsepTransceiver&& orig) = default;
+ JsepTransceiver& operator=(const JsepTransceiver& aRhs) = default;
+ JsepTransceiver& operator=(JsepTransceiver&& aRhs) = default;
+
+ ~JsepTransceiver() = default;
+
+ void Rollback(JsepTransceiver& oldTransceiver, bool aRemote) {
+ MOZ_ASSERT(oldTransceiver.GetMediaType() == GetMediaType());
+ MOZ_ASSERT(!oldTransceiver.IsNegotiated() || !oldTransceiver.HasLevel() ||
+ !HasLevel() || oldTransceiver.GetLevel() == GetLevel());
+ mTransport = oldTransceiver.mTransport;
+ if (aRemote) {
+ mLevel = oldTransceiver.mLevel;
+ mBundleLevel = oldTransceiver.mBundleLevel;
+ mSendTrack = oldTransceiver.mSendTrack;
+ }
+ mRecvTrack = oldTransceiver.mRecvTrack;
+
+ // Don't allow rollback to re-associate a transceiver.
+ if (!oldTransceiver.IsAssociated()) {
+ Disassociate();
+ }
+ }
+
+ bool IsAssociated() const { return !mMid.empty(); }
+
+ const std::string& GetMid() const {
+ MOZ_ASSERT(IsAssociated());
+ return mMid;
+ }
+
+ void Associate(const std::string& mid) {
+ MOZ_ASSERT(HasLevel());
+ mMid = mid;
+ }
+
+ void Disassociate() { mMid.clear(); }
+
+ bool HasLevel() const { return mLevel != SIZE_MAX; }
+
+ void SetLevel(size_t level) {
+ MOZ_ASSERT(level != SIZE_MAX);
+ MOZ_ASSERT(!HasLevel());
+ MOZ_ASSERT(!IsStopped());
+
+ mLevel = level;
+ }
+
+ void ClearLevel() {
+ MOZ_ASSERT(!IsAssociated());
+ mLevel = SIZE_MAX;
+ mBundleLevel = SIZE_MAX;
+ }
+
+ size_t GetLevel() const {
+ MOZ_ASSERT(HasLevel());
+ return mLevel;
+ }
+
+ void Stop() { mStopped = true; }
+
+ bool IsStopped() const { return mStopped; }
+
+ void RestartDatachannelTransceiver() {
+ MOZ_RELEASE_ASSERT(GetMediaType() == SdpMediaSection::kApplication);
+ mStopped = false;
+ mCanRecycle = false;
+ }
+
+ void SetRemoved() { mRemoved = true; }
+
+ bool IsRemoved() const { return mRemoved; }
+
+ bool HasBundleLevel() const { return mBundleLevel != SIZE_MAX; }
+
+ size_t BundleLevel() const {
+ MOZ_ASSERT(HasBundleLevel());
+ return mBundleLevel;
+ }
+
+ void SetBundleLevel(size_t aBundleLevel) {
+ MOZ_ASSERT(aBundleLevel != SIZE_MAX);
+ mBundleLevel = aBundleLevel;
+ }
+
+ void ClearBundleLevel() { mBundleLevel = SIZE_MAX; }
+
+ size_t GetTransportLevel() const {
+ MOZ_ASSERT(HasLevel());
+ if (HasBundleLevel()) {
+ return BundleLevel();
+ }
+ return GetLevel();
+ }
+
+ void SetAddTrackMagic() { mAddTrackMagic = true; }
+
+ bool HasAddTrackMagic() const { return mAddTrackMagic; }
+
+ void SetOnlyExistsBecauseOfSetRemote(bool aValue) {
+ mOnlyExistsBecauseOfSetRemote = aValue;
+ }
+
+ bool OnlyExistsBecauseOfSetRemote() const {
+ return mOnlyExistsBecauseOfSetRemote;
+ }
+
+ void SetNegotiated() {
+ MOZ_ASSERT(IsAssociated());
+ MOZ_ASSERT(HasLevel());
+ mNegotiated = true;
+ }
+
+ bool IsNegotiated() const { return mNegotiated; }
+
+ void SetCanRecycle() { mCanRecycle = true; }
+
+ bool CanRecycle() const { return mCanRecycle; }
+
+ const std::string& GetUuid() const { return mUuid; }
+
+ // Convenience function
+ SdpMediaSection::MediaType GetMediaType() const {
+ MOZ_ASSERT(mRecvTrack.GetMediaType() == mSendTrack.GetMediaType());
+ return mRecvTrack.GetMediaType();
+ }
+
+ bool HasTransport() const { return mTransport.mComponents != 0; }
+
+ bool HasOwnTransport() const {
+ if (HasTransport() &&
+ (!HasBundleLevel() || (GetLevel() == BundleLevel()))) {
+ return true;
+ }
+ return false;
+ }
+
+ // See Bug 1642419, this can be removed when all sites are working with RTX.
+ void SetRtxIsAllowed(bool aRtxIsAllowed) {
+ mSendTrack.SetRtxIsAllowed(aRtxIsAllowed);
+ mRecvTrack.SetRtxIsAllowed(aRtxIsAllowed);
+ }
+
+ // This is the direction JS wants. It might not actually happen.
+ SdpDirectionAttribute::Direction mJsDirection;
+
+ JsepTrack mSendTrack;
+ JsepTrack mRecvTrack;
+ JsepTransport mTransport;
+
+ private:
+ std::string mUuid;
+ // Stuff that is not negotiated
+ std::string mMid;
+ size_t mLevel; // SIZE_MAX if no level
+ // Is this track pair sharing a transport with another?
+ size_t mBundleLevel; // SIZE_MAX if no bundle level
+ // The w3c and IETF specs have a lot of "magical" behavior that happens
+ // when addTrack is used to create a transceiver. This was a deliberate
+ // design choice. Sadface.
+ bool mAddTrackMagic;
+ bool mOnlyExistsBecauseOfSetRemote;
+ bool mStopped;
+ bool mRemoved;
+ bool mNegotiated;
+ bool mCanRecycle;
+};
+
+} // namespace mozilla
+
+#endif // _JSEPTRANSCEIVER_H_
diff --git a/dom/media/webrtc/jsep/JsepTransport.h b/dom/media/webrtc/jsep/JsepTransport.h
new file mode 100644
index 0000000000..67690b1fd4
--- /dev/null
+++ b/dom/media/webrtc/jsep/JsepTransport.h
@@ -0,0 +1,102 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _JSEPTRANSPORT_H_
+#define _JSEPTRANSPORT_H_
+
+#include <string>
+#include <vector>
+
+#include <mozilla/RefPtr.h>
+#include <mozilla/UniquePtr.h>
+#include "nsISupportsImpl.h"
+
+#include "sdp/SdpAttribute.h"
+
+namespace mozilla {
+
+class JsepDtlsTransport {
+ public:
+ JsepDtlsTransport() : mRole(kJsepDtlsInvalidRole) {}
+
+ virtual ~JsepDtlsTransport() {}
+
+ enum Role { kJsepDtlsClient, kJsepDtlsServer, kJsepDtlsInvalidRole };
+
+ virtual const SdpFingerprintAttributeList& GetFingerprints() const {
+ return mFingerprints;
+ }
+
+ virtual Role GetRole() const { return mRole; }
+
+ private:
+ friend class JsepSessionImpl;
+
+ SdpFingerprintAttributeList mFingerprints;
+ Role mRole;
+};
+
+class JsepIceTransport {
+ public:
+ JsepIceTransport() {}
+
+ virtual ~JsepIceTransport() {}
+
+ const std::string& GetUfrag() const { return mUfrag; }
+ const std::string& GetPassword() const { return mPwd; }
+ const std::vector<std::string>& GetCandidates() const { return mCandidates; }
+
+ private:
+ friend class JsepSessionImpl;
+
+ std::string mUfrag;
+ std::string mPwd;
+ std::vector<std::string> mCandidates;
+};
+
+class JsepTransport {
+ public:
+ JsepTransport() : mComponents(0) {}
+
+ JsepTransport(const JsepTransport& orig) { *this = orig; }
+
+ ~JsepTransport() {}
+
+ JsepTransport& operator=(const JsepTransport& orig) {
+ if (this != &orig) {
+ mIce.reset(orig.mIce ? new JsepIceTransport(*orig.mIce) : nullptr);
+ mDtls.reset(orig.mDtls ? new JsepDtlsTransport(*orig.mDtls) : nullptr);
+ mTransportId = orig.mTransportId;
+ mComponents = orig.mComponents;
+ mLocalUfrag = orig.mLocalUfrag;
+ mLocalPwd = orig.mLocalPwd;
+ }
+ return *this;
+ }
+
+ void Close() {
+ mComponents = 0;
+ mTransportId.clear();
+ mIce.reset();
+ mDtls.reset();
+ mLocalUfrag.clear();
+ mLocalPwd.clear();
+ }
+
+ // Unique identifier for this transport within this call. Group?
+ std::string mTransportId;
+
+ // ICE stuff.
+ UniquePtr<JsepIceTransport> mIce;
+ UniquePtr<JsepDtlsTransport> mDtls;
+
+ // Number of required components.
+ size_t mComponents;
+ std::string mLocalUfrag;
+ std::string mLocalPwd;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/jsep/SsrcGenerator.cpp b/dom/media/webrtc/jsep/SsrcGenerator.cpp
new file mode 100644
index 0000000000..50025007d1
--- /dev/null
+++ b/dom/media/webrtc/jsep/SsrcGenerator.cpp
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "jsep/SsrcGenerator.h"
+#include "pk11pub.h"
+
+namespace mozilla {
+
+bool SsrcGenerator::GenerateSsrc(uint32_t* ssrc) {
+ do {
+ SECStatus rv = PK11_GenerateRandom(reinterpret_cast<unsigned char*>(ssrc),
+ sizeof(uint32_t));
+ if (rv != SECSuccess) {
+ return false;
+ }
+ } while (mSsrcs.count(*ssrc));
+ mSsrcs.insert(*ssrc);
+
+ return true;
+}
+} // namespace mozilla
diff --git a/dom/media/webrtc/jsep/SsrcGenerator.h b/dom/media/webrtc/jsep/SsrcGenerator.h
new file mode 100644
index 0000000000..b106358f32
--- /dev/null
+++ b/dom/media/webrtc/jsep/SsrcGenerator.h
@@ -0,0 +1,20 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _SSRCGENERATOR_H_
+#define _SSRCGENERATOR_H_
+
+#include <set>
+
+namespace mozilla {
+class SsrcGenerator {
+ public:
+ bool GenerateSsrc(uint32_t* ssrc);
+
+ private:
+ std::set<uint32_t> mSsrcs;
+};
+} // namespace mozilla
+
+#endif // _SSRCGENERATOR_H_
diff --git a/dom/media/webrtc/jsep/moz.build b/dom/media/webrtc/jsep/moz.build
new file mode 100644
index 0000000000..750ac76a30
--- /dev/null
+++ b/dom/media/webrtc/jsep/moz.build
@@ -0,0 +1,18 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+include("/dom/media/webrtc/third_party_build/webrtc.mozbuild")
+
+LOCAL_INCLUDES += [
+ "/dom/media/webrtc",
+ "/media/webrtc",
+ "/third_party/libwebrtc",
+ "/third_party/libwebrtc/third_party/abseil-cpp",
+ "/third_party/sipcc",
+]
+
+UNIFIED_SOURCES += ["JsepSessionImpl.cpp", "JsepTrack.cpp", "SsrcGenerator.cpp"]
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/webrtc/libwebrtcglue/AudioConduit.cpp b/dom/media/webrtc/libwebrtcglue/AudioConduit.cpp
new file mode 100644
index 0000000000..d8492f779a
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/AudioConduit.cpp
@@ -0,0 +1,975 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioConduit.h"
+
+#include "common/browser_logging/CSFLog.h"
+#include "MediaConduitControl.h"
+#include "mozilla/media/MediaUtils.h"
+#include "mozilla/Telemetry.h"
+#include "transport/runnable_utils.h"
+#include "transport/SrtpFlow.h" // For SRTP_MAX_EXPANSION
+#include "WebrtcCallWrapper.h"
+
+// libwebrtc includes
+#include "api/audio_codecs/builtin_audio_encoder_factory.h"
+#include "audio/audio_receive_stream.h"
+#include "media/base/media_constants.h"
+
+// for ntohs
+#ifdef HAVE_NETINET_IN_H
+# include <netinet/in.h>
+#elif defined XP_WIN
+# include <winsock2.h>
+#endif
+
+#ifdef MOZ_WIDGET_ANDROID
+# include "AndroidBridge.h"
+#endif
+
+namespace mozilla {
+
+namespace {
+
+static const char* acLogTag = "WebrtcAudioSessionConduit";
+#ifdef LOGTAG
+# undef LOGTAG
+#endif
+#define LOGTAG acLogTag
+
+using namespace cricket;
+using LocalDirection = MediaSessionConduitLocalDirection;
+
+const char kCodecParamCbr[] = "cbr";
+
+} // namespace
+
+/**
+ * Factory Method for AudioConduit
+ */
+RefPtr<AudioSessionConduit> AudioSessionConduit::Create(
+ RefPtr<WebrtcCallWrapper> aCall,
+ nsCOMPtr<nsISerialEventTarget> aStsThread) {
+ CSFLogDebug(LOGTAG, "%s ", __FUNCTION__);
+ MOZ_ASSERT(NS_IsMainThread());
+
+ return MakeRefPtr<WebrtcAudioConduit>(std::move(aCall),
+ std::move(aStsThread));
+}
+
+#define INIT_MIRROR(name, val) \
+ name(aCallThread, val, "WebrtcAudioConduit::Control::" #name " (Mirror)")
+WebrtcAudioConduit::Control::Control(const RefPtr<AbstractThread>& aCallThread)
+ : INIT_MIRROR(mReceiving, false),
+ INIT_MIRROR(mTransmitting, false),
+ INIT_MIRROR(mLocalSsrcs, Ssrcs()),
+ INIT_MIRROR(mLocalCname, std::string()),
+ INIT_MIRROR(mMid, std::string()),
+ INIT_MIRROR(mRemoteSsrc, 0),
+ INIT_MIRROR(mSyncGroup, std::string()),
+ INIT_MIRROR(mLocalRecvRtpExtensions, RtpExtList()),
+ INIT_MIRROR(mLocalSendRtpExtensions, RtpExtList()),
+ INIT_MIRROR(mSendCodec, Nothing()),
+ INIT_MIRROR(mRecvCodecs, std::vector<AudioCodecConfig>()) {}
+#undef INIT_MIRROR
+
+RefPtr<GenericPromise> WebrtcAudioConduit::Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ return InvokeAsync(mCallThread, "WebrtcAudioConduit::Shutdown (main thread)",
+ [this, self = RefPtr<WebrtcAudioConduit>(this)] {
+ mControl.mReceiving.DisconnectIfConnected();
+ mControl.mTransmitting.DisconnectIfConnected();
+ mControl.mLocalSsrcs.DisconnectIfConnected();
+ mControl.mLocalCname.DisconnectIfConnected();
+ mControl.mMid.DisconnectIfConnected();
+ mControl.mRemoteSsrc.DisconnectIfConnected();
+ mControl.mSyncGroup.DisconnectIfConnected();
+ mControl.mLocalRecvRtpExtensions.DisconnectIfConnected();
+ mControl.mLocalSendRtpExtensions.DisconnectIfConnected();
+ mControl.mSendCodec.DisconnectIfConnected();
+ mControl.mRecvCodecs.DisconnectIfConnected();
+ mControl.mOnDtmfEventListener.DisconnectIfExists();
+ mWatchManager.Shutdown();
+
+ {
+ AutoWriteLock lock(mLock);
+ DeleteSendStream();
+ DeleteRecvStream();
+ }
+
+ return GenericPromise::CreateAndResolve(
+ true, "WebrtcAudioConduit::Shutdown (call thread)");
+ });
+}
+
+WebrtcAudioConduit::WebrtcAudioConduit(
+ RefPtr<WebrtcCallWrapper> aCall, nsCOMPtr<nsISerialEventTarget> aStsThread)
+ : mCall(std::move(aCall)),
+ mSendTransport(this),
+ mRecvTransport(this),
+ mRecvStreamConfig(),
+ mRecvStream(nullptr),
+ mSendStreamConfig(&mSendTransport),
+ mSendStream(nullptr),
+ mSendStreamRunning(false),
+ mRecvStreamRunning(false),
+ mDtmfEnabled(false),
+ mLock("WebrtcAudioConduit::mLock"),
+ mCallThread(std::move(mCall->mCallThread)),
+ mStsThread(std::move(aStsThread)),
+ mControl(mCall->mCallThread),
+ mWatchManager(this, mCall->mCallThread) {
+ mRecvStreamConfig.rtcp_send_transport = &mRecvTransport;
+ mRecvStreamConfig.rtp.rtcp_event_observer = this;
+}
+
+/**
+ * Destruction defines for our super-classes
+ */
+WebrtcAudioConduit::~WebrtcAudioConduit() {
+ CSFLogDebug(LOGTAG, "%s ", __FUNCTION__);
+ MOZ_ASSERT(!mSendStream && !mRecvStream,
+ "Call DeleteStreams prior to ~WebrtcAudioConduit.");
+}
+
+#define CONNECT(aCanonical, aMirror) \
+ do { \
+ (aMirror).Connect(aCanonical); \
+ mWatchManager.Watch(aMirror, &WebrtcAudioConduit::OnControlConfigChange); \
+ } while (0)
+
+void WebrtcAudioConduit::InitControl(AudioConduitControlInterface* aControl) {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+
+ CONNECT(aControl->CanonicalReceiving(), mControl.mReceiving);
+ CONNECT(aControl->CanonicalTransmitting(), mControl.mTransmitting);
+ CONNECT(aControl->CanonicalLocalSsrcs(), mControl.mLocalSsrcs);
+ CONNECT(aControl->CanonicalLocalCname(), mControl.mLocalCname);
+ CONNECT(aControl->CanonicalMid(), mControl.mMid);
+ CONNECT(aControl->CanonicalRemoteSsrc(), mControl.mRemoteSsrc);
+ CONNECT(aControl->CanonicalSyncGroup(), mControl.mSyncGroup);
+ CONNECT(aControl->CanonicalLocalRecvRtpExtensions(),
+ mControl.mLocalRecvRtpExtensions);
+ CONNECT(aControl->CanonicalLocalSendRtpExtensions(),
+ mControl.mLocalSendRtpExtensions);
+ CONNECT(aControl->CanonicalAudioSendCodec(), mControl.mSendCodec);
+ CONNECT(aControl->CanonicalAudioRecvCodecs(), mControl.mRecvCodecs);
+ mControl.mOnDtmfEventListener = aControl->OnDtmfEvent().Connect(
+ mCall->mCallThread, this, &WebrtcAudioConduit::OnDtmfEvent);
+}
+
+#undef CONNECT
+
+void WebrtcAudioConduit::OnDtmfEvent(const DtmfEvent& aEvent) {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ MOZ_ASSERT(mSendStream);
+ MOZ_ASSERT(mDtmfEnabled);
+ mSendStream->SendTelephoneEvent(aEvent.mPayloadType, aEvent.mPayloadFrequency,
+ aEvent.mEventCode, aEvent.mLengthMs);
+}
+
+void WebrtcAudioConduit::OnControlConfigChange() {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+
+ bool recvStreamReconfigureNeeded = false;
+ bool sendStreamReconfigureNeeded = false;
+ bool recvStreamRecreationNeeded = false;
+ bool sendStreamRecreationNeeded = false;
+
+ if (!mControl.mLocalSsrcs.Ref().empty()) {
+ if (mControl.mLocalSsrcs.Ref()[0] != mSendStreamConfig.rtp.ssrc) {
+ sendStreamRecreationNeeded = true;
+
+ // For now...
+ recvStreamRecreationNeeded = true;
+ }
+ mRecvStreamConfig.rtp.local_ssrc = mControl.mLocalSsrcs.Ref()[0];
+ mSendStreamConfig.rtp.ssrc = mControl.mLocalSsrcs.Ref()[0];
+
+ // In the future we can do this instead of recreating the recv stream:
+ // if (mRecvStream) {
+ // mCall->Call()->OnLocalSsrcUpdated(mRecvStream,
+ // mControl.mLocalSsrcs.Ref()[0]);
+ // }
+ }
+
+ if (mControl.mLocalCname.Ref() != mSendStreamConfig.rtp.c_name) {
+ mSendStreamConfig.rtp.c_name = mControl.mLocalCname.Ref();
+ sendStreamReconfigureNeeded = true;
+ }
+
+ if (mControl.mMid.Ref() != mSendStreamConfig.rtp.mid) {
+ mSendStreamConfig.rtp.mid = mControl.mMid.Ref();
+ sendStreamReconfigureNeeded = true;
+ }
+
+ if (mControl.mRemoteSsrc.Ref() != mControl.mConfiguredRemoteSsrc) {
+ mRecvStreamConfig.rtp.remote_ssrc = mControl.mConfiguredRemoteSsrc =
+ mControl.mRemoteSsrc.Ref();
+ recvStreamRecreationNeeded = true;
+ }
+
+ if (mControl.mSyncGroup.Ref() != mRecvStreamConfig.sync_group) {
+ mRecvStreamConfig.sync_group = mControl.mSyncGroup.Ref();
+ // For now...
+ recvStreamRecreationNeeded = true;
+ // In the future we can do this instead of recreating the recv stream:
+ // if (mRecvStream) {
+ // mCall->Call()->OnUpdateSyncGroup(mRecvStream,
+ // mRecvStreamConfig.sync_group);
+ // }
+ }
+
+ if (auto filteredExtensions = FilterExtensions(
+ LocalDirection::kRecv, mControl.mLocalRecvRtpExtensions);
+ filteredExtensions != mRecvStreamConfig.rtp.extensions) {
+ mRecvStreamConfig.rtp.extensions = std::move(filteredExtensions);
+ // For now...
+ recvStreamRecreationNeeded = true;
+ // In the future we can do this instead of recreating the recv stream:
+ // if (mRecvStream) {
+ // mRecvStream->SetRtpExtensions(mRecvStreamConfig.rtp.extensions);
+ //}
+ }
+
+ if (auto filteredExtensions = FilterExtensions(
+ LocalDirection::kSend, mControl.mLocalSendRtpExtensions);
+ filteredExtensions != mSendStreamConfig.rtp.extensions) {
+ // At the very least, we need a reconfigure. Recreation needed if the
+ // extmap for any extension has changed, but not for adding/removing
+ // extensions.
+ sendStreamReconfigureNeeded = true;
+
+ for (const auto& newExt : filteredExtensions) {
+ if (sendStreamRecreationNeeded) {
+ break;
+ }
+ for (const auto& oldExt : mSendStreamConfig.rtp.extensions) {
+ if (newExt.uri == oldExt.uri) {
+ if (newExt.id != oldExt.id) {
+ sendStreamRecreationNeeded = true;
+ }
+ // We're done handling newExt, one way or another
+ break;
+ }
+ }
+ }
+
+ mSendStreamConfig.rtp.extensions = std::move(filteredExtensions);
+ }
+
+ mControl.mSendCodec.Ref().apply([&](const auto& aConfig) {
+ if (mControl.mConfiguredSendCodec != mControl.mSendCodec.Ref()) {
+ mControl.mConfiguredSendCodec = mControl.mSendCodec;
+ if (ValidateCodecConfig(aConfig, true) == kMediaConduitNoError) {
+ mSendStreamConfig.encoder_factory =
+ webrtc::CreateBuiltinAudioEncoderFactory();
+
+ webrtc::AudioSendStream::Config::SendCodecSpec spec(
+ aConfig.mType, CodecConfigToLibwebrtcFormat(aConfig));
+ mSendStreamConfig.send_codec_spec = spec;
+
+ mDtmfEnabled = aConfig.mDtmfEnabled;
+ sendStreamReconfigureNeeded = true;
+ }
+ }
+ });
+
+ if (mControl.mConfiguredRecvCodecs != mControl.mRecvCodecs.Ref()) {
+ mControl.mConfiguredRecvCodecs = mControl.mRecvCodecs;
+ mRecvStreamConfig.decoder_factory = mCall->mAudioDecoderFactory;
+ mRecvStreamConfig.decoder_map.clear();
+
+ for (const auto& codec : mControl.mRecvCodecs.Ref()) {
+ if (ValidateCodecConfig(codec, false) != kMediaConduitNoError) {
+ continue;
+ }
+ mRecvStreamConfig.decoder_map.emplace(
+ codec.mType, CodecConfigToLibwebrtcFormat(codec));
+ }
+
+ recvStreamReconfigureNeeded = true;
+ }
+
+ if (!recvStreamReconfigureNeeded && !sendStreamReconfigureNeeded &&
+ !recvStreamRecreationNeeded && !sendStreamRecreationNeeded &&
+ mControl.mReceiving == mRecvStreamRunning &&
+ mControl.mTransmitting == mSendStreamRunning) {
+ // No changes applied -- no need to lock.
+ return;
+ }
+
+ if (recvStreamRecreationNeeded) {
+ recvStreamReconfigureNeeded = false;
+ }
+ if (sendStreamRecreationNeeded) {
+ sendStreamReconfigureNeeded = false;
+ }
+
+ {
+ AutoWriteLock lock(mLock);
+ // Recreate/Stop/Start streams as needed.
+ if (recvStreamRecreationNeeded) {
+ DeleteRecvStream();
+ }
+ if (mControl.mReceiving) {
+ CreateRecvStream();
+ }
+ if (sendStreamRecreationNeeded) {
+ DeleteSendStream();
+ }
+ if (mControl.mTransmitting) {
+ CreateSendStream();
+ }
+ }
+
+ // We make sure to not hold the lock while stopping/starting/reconfiguring
+ // streams, so as to not cause deadlocks. These methods can cause our platform
+ // codecs to dispatch sync runnables to main, and main may grab the lock.
+
+ if (mRecvStream && recvStreamReconfigureNeeded) {
+ MOZ_ASSERT(!recvStreamRecreationNeeded);
+ mRecvStream->SetDecoderMap(mRecvStreamConfig.decoder_map);
+ }
+
+ if (mSendStream && sendStreamReconfigureNeeded) {
+ MOZ_ASSERT(!sendStreamRecreationNeeded);
+ // TODO: Pass a callback here, so we can react to RTCErrors thrown by
+ // libwebrtc.
+ mSendStream->Reconfigure(mSendStreamConfig, nullptr);
+ }
+
+ if (!mControl.mReceiving) {
+ StopReceiving();
+ }
+ if (!mControl.mTransmitting) {
+ StopTransmitting();
+ }
+
+ if (mControl.mReceiving) {
+ StartReceiving();
+ }
+ if (mControl.mTransmitting) {
+ StartTransmitting();
+ }
+}
+
+std::vector<uint32_t> WebrtcAudioConduit::GetLocalSSRCs() const {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ return std::vector<uint32_t>(1, mRecvStreamConfig.rtp.local_ssrc);
+}
+
+bool WebrtcAudioConduit::OverrideRemoteSSRC(uint32_t aSsrc) {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+
+ if (mRecvStreamConfig.rtp.remote_ssrc == aSsrc) {
+ return true;
+ }
+ mRecvStreamConfig.rtp.remote_ssrc = aSsrc;
+
+ const bool wasReceiving = mRecvStreamRunning;
+ const bool hadRecvStream = mRecvStream;
+
+ StopReceiving();
+
+ if (hadRecvStream) {
+ AutoWriteLock lock(mLock);
+ DeleteRecvStream();
+ CreateRecvStream();
+ }
+
+ if (wasReceiving) {
+ StartReceiving();
+ }
+ return true;
+}
+
+Maybe<Ssrc> WebrtcAudioConduit::GetRemoteSSRC() const {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ // libwebrtc uses 0 to mean a lack of SSRC. That is not to spec.
+ return mRecvStreamConfig.rtp.remote_ssrc == 0
+ ? Nothing()
+ : Some(mRecvStreamConfig.rtp.remote_ssrc);
+}
+
+Maybe<webrtc::AudioReceiveStreamInterface::Stats>
+WebrtcAudioConduit::GetReceiverStats() const {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ if (!mRecvStream) {
+ return Nothing();
+ }
+ return Some(mRecvStream->GetStats());
+}
+
+Maybe<webrtc::AudioSendStream::Stats> WebrtcAudioConduit::GetSenderStats()
+ const {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ if (!mSendStream) {
+ return Nothing();
+ }
+ return Some(mSendStream->GetStats());
+}
+
+Maybe<webrtc::CallBasicStats> WebrtcAudioConduit::GetCallStats() const {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ if (!mCall->Call()) {
+ return Nothing();
+ }
+ return Some(mCall->Call()->GetStats());
+}
+
+void WebrtcAudioConduit::OnRtcpBye() { mRtcpByeEvent.Notify(); }
+
+void WebrtcAudioConduit::OnRtcpTimeout() { mRtcpTimeoutEvent.Notify(); }
+
+void WebrtcAudioConduit::SetTransportActive(bool aActive) {
+ MOZ_ASSERT(mStsThread->IsOnCurrentThread());
+ if (mTransportActive == aActive) {
+ return;
+ }
+
+ // If false, This stops us from sending
+ mTransportActive = aActive;
+
+ // We queue this because there might be notifications to these listeners
+ // pending, and we don't want to drop them by letting this jump ahead of
+ // those notifications. We move the listeners into the lambda in case the
+ // transport comes back up before we disconnect them. (The Connect calls
+ // happen in MediaPipeline)
+ // We retain a strong reference to ourself, because the listeners are holding
+ // a non-refcounted reference to us, and moving them into the lambda could
+ // conceivably allow them to outlive us.
+ if (!aActive) {
+ MOZ_ALWAYS_SUCCEEDS(mCallThread->Dispatch(NS_NewRunnableFunction(
+ __func__,
+ [self = RefPtr<WebrtcAudioConduit>(this),
+ recvRtpListener = std::move(mReceiverRtpEventListener),
+ recvRtcpListener = std::move(mReceiverRtcpEventListener),
+ sendRtcpListener = std::move(mSenderRtcpEventListener)]() mutable {
+ recvRtpListener.DisconnectIfExists();
+ recvRtcpListener.DisconnectIfExists();
+ sendRtcpListener.DisconnectIfExists();
+ })));
+ }
+}
+
+// AudioSessionConduit Implementation
+MediaConduitErrorCode WebrtcAudioConduit::SendAudioFrame(
+ std::unique_ptr<webrtc::AudioFrame> frame) {
+ CSFLogDebug(LOGTAG, "%s ", __FUNCTION__);
+ // Following checks need to be performed
+ // 1. Non null audio buffer pointer, and
+ // 2. Valid sample rate, and
+ // 3. Appropriate Sample Length for 10 ms audio-frame. This represents the
+ // block size used upstream for processing.
+ // Ex: for 16000 sample rate , valid block-length is 160.
+ // Similarly for 32000 sample rate, valid block length is 320.
+
+ if (!frame->data() ||
+ (IsSamplingFreqSupported(frame->sample_rate_hz()) == false) ||
+ ((frame->samples_per_channel() % (frame->sample_rate_hz() / 100) != 0))) {
+ CSFLogError(LOGTAG, "%s Invalid Parameters ", __FUNCTION__);
+ MOZ_ASSERT(PR_FALSE);
+ return kMediaConduitMalformedArgument;
+ }
+
+ // This is the AudioProxyThread, blocking it for a bit is fine.
+ AutoReadLock lock(mLock);
+ if (!mSendStreamRunning) {
+ CSFLogError(LOGTAG, "%s Engine not transmitting ", __FUNCTION__);
+ return kMediaConduitSessionNotInited;
+ }
+
+ mSendStream->SendAudioData(std::move(frame));
+ return kMediaConduitNoError;
+}
+
+MediaConduitErrorCode WebrtcAudioConduit::GetAudioFrame(
+ int32_t samplingFreqHz, webrtc::AudioFrame* frame) {
+ CSFLogDebug(LOGTAG, "%s ", __FUNCTION__);
+
+ // validate params
+ if (!frame) {
+ CSFLogError(LOGTAG, "%s Null Audio Buffer Pointer", __FUNCTION__);
+ MOZ_ASSERT(PR_FALSE);
+ return kMediaConduitMalformedArgument;
+ }
+
+ // Validate sample length
+ if (GetNum10msSamplesForFrequency(samplingFreqHz) == 0) {
+ CSFLogError(LOGTAG, "%s Invalid Sampling Frequency ", __FUNCTION__);
+ MOZ_ASSERT(PR_FALSE);
+ return kMediaConduitMalformedArgument;
+ }
+
+ // If the lock is taken, skip this chunk to avoid blocking the audio thread.
+ AutoTryReadLock tryLock(mLock);
+ if (!tryLock) {
+ CSFLogError(LOGTAG, "%s Conduit going through negotiation ", __FUNCTION__);
+ return kMediaConduitPlayoutError;
+ }
+
+ // Conduit should have reception enabled before we ask for decoded
+ // samples
+ if (!mRecvStreamRunning) {
+ CSFLogError(LOGTAG, "%s Engine not Receiving ", __FUNCTION__);
+ return kMediaConduitSessionNotInited;
+ }
+
+ // Unfortunate to have to cast to an internal class, but that looks like the
+ // only way short of interfacing with a layer above (which mixes all streams,
+ // which we don't want) or a layer below (which we try to avoid because it is
+ // less stable).
+ auto info = static_cast<webrtc::AudioReceiveStreamImpl*>(mRecvStream)
+ ->GetAudioFrameWithInfo(samplingFreqHz, frame);
+
+ if (info == webrtc::AudioMixer::Source::AudioFrameInfo::kError) {
+ CSFLogError(LOGTAG, "%s Getting audio frame failed", __FUNCTION__);
+ return kMediaConduitPlayoutError;
+ }
+
+ CSFLogDebug(LOGTAG, "%s Got %zu channels of %zu samples", __FUNCTION__,
+ frame->num_channels(), frame->samples_per_channel());
+ return kMediaConduitNoError;
+}
+
+// Transport Layer Callbacks
+void WebrtcAudioConduit::OnRtpReceived(webrtc::RtpPacketReceived&& aPacket,
+ webrtc::RTPHeader&& aHeader) {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+
+ if (mAllowSsrcChange && mRecvStreamConfig.rtp.remote_ssrc != aHeader.ssrc) {
+ CSFLogDebug(LOGTAG, "%s: switching from SSRC %u to %u", __FUNCTION__,
+ mRecvStreamConfig.rtp.remote_ssrc, aHeader.ssrc);
+ OverrideRemoteSSRC(aHeader.ssrc);
+ }
+
+ CSFLogVerbose(LOGTAG, "%s: seq# %u, Len %zu, SSRC %u (0x%x) ", __FUNCTION__,
+ aPacket.SequenceNumber(), aPacket.size(), aPacket.Ssrc(),
+ aPacket.Ssrc());
+
+ mRtpPacketEvent.Notify();
+ if (mCall->Call()) {
+ mCall->Call()->Receiver()->DeliverRtpPacket(
+ webrtc::MediaType::AUDIO, std::move(aPacket),
+ [self = RefPtr<WebrtcAudioConduit>(this)](
+ const webrtc::RtpPacketReceived& packet) {
+ CSFLogVerbose(
+ LOGTAG,
+ "AudioConduit %p: failed demuxing packet, ssrc: %u seq: %u",
+ self.get(), packet.Ssrc(), packet.SequenceNumber());
+ return false;
+ });
+ }
+}
+
+void WebrtcAudioConduit::OnRtcpReceived(MediaPacket&& aPacket) {
+ CSFLogDebug(LOGTAG, "%s", __FUNCTION__);
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+
+ if (mCall->Call()) {
+ mCall->Call()->Receiver()->DeliverRtcpPacket(
+ rtc::CopyOnWriteBuffer(aPacket.data(), aPacket.len()));
+ }
+}
+
+Maybe<uint16_t> WebrtcAudioConduit::RtpSendBaseSeqFor(uint32_t aSsrc) const {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ auto it = mRtpSendBaseSeqs.find(aSsrc);
+ if (it == mRtpSendBaseSeqs.end()) {
+ return Nothing();
+ }
+ return Some(it->second);
+}
+
+const dom::RTCStatsTimestampMaker& WebrtcAudioConduit::GetTimestampMaker()
+ const {
+ return mCall->GetTimestampMaker();
+}
+
+void WebrtcAudioConduit::StopTransmitting() {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ MOZ_ASSERT(!mLock.LockedForWritingByCurrentThread());
+
+ if (!mSendStreamRunning) {
+ return;
+ }
+
+ if (mSendStream) {
+ CSFLogDebug(LOGTAG, "%s Stopping send stream", __FUNCTION__);
+ mSendStream->Stop();
+ }
+
+ mSendStreamRunning = false;
+}
+
+void WebrtcAudioConduit::StartTransmitting() {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ MOZ_ASSERT(mSendStream);
+ MOZ_ASSERT(!mLock.LockedForWritingByCurrentThread());
+
+ if (mSendStreamRunning) {
+ return;
+ }
+
+ CSFLogDebug(LOGTAG, "%s Starting send stream", __FUNCTION__);
+
+ mCall->Call()->SignalChannelNetworkState(webrtc::MediaType::AUDIO,
+ webrtc::kNetworkUp);
+ mSendStream->Start();
+ mSendStreamRunning = true;
+}
+
+void WebrtcAudioConduit::StopReceiving() {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ MOZ_ASSERT(!mLock.LockedForWritingByCurrentThread());
+
+ if (!mRecvStreamRunning) {
+ return;
+ }
+
+ if (mRecvStream) {
+ CSFLogDebug(LOGTAG, "%s Stopping recv stream", __FUNCTION__);
+ mRecvStream->Stop();
+ }
+
+ mRecvStreamRunning = false;
+}
+
+void WebrtcAudioConduit::StartReceiving() {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ MOZ_ASSERT(mRecvStream);
+ MOZ_ASSERT(!mLock.LockedForWritingByCurrentThread());
+
+ if (mRecvStreamRunning) {
+ return;
+ }
+
+ CSFLogDebug(LOGTAG, "%s Starting receive stream (SSRC %u (0x%x))",
+ __FUNCTION__, mRecvStreamConfig.rtp.remote_ssrc,
+ mRecvStreamConfig.rtp.remote_ssrc);
+
+ mCall->Call()->SignalChannelNetworkState(webrtc::MediaType::AUDIO,
+ webrtc::kNetworkUp);
+ mRecvStream->Start();
+ mRecvStreamRunning = true;
+}
+
+bool WebrtcAudioConduit::SendRtp(const uint8_t* aData, size_t aLength,
+ const webrtc::PacketOptions& aOptions) {
+ MOZ_ASSERT(aLength >= 12);
+ const uint16_t seqno = ntohs(*((uint16_t*)&aData[2]));
+ const uint32_t ssrc = ntohl(*((uint32_t*)&aData[8]));
+
+ CSFLogVerbose(
+ LOGTAG,
+ "AudioConduit %p: Sending RTP Packet seq# %u, len %zu, SSRC %u (0x%x)",
+ this, seqno, aLength, ssrc, ssrc);
+
+ if (!mTransportActive) {
+ CSFLogError(LOGTAG, "AudioConduit %p: RTP Packet Send Failed ", this);
+ return false;
+ }
+
+ MediaPacket packet;
+ packet.Copy(aData, aLength, aLength + SRTP_MAX_EXPANSION);
+ packet.SetType(MediaPacket::RTP);
+ mSenderRtpSendEvent.Notify(std::move(packet));
+
+ // Parse the sequence number of the first rtp packet as base_seq.
+ const auto inserted = mRtpSendBaseSeqs_n.insert({ssrc, seqno}).second;
+
+ if (inserted || aOptions.packet_id >= 0) {
+ int64_t now_ms = PR_Now() / 1000;
+ MOZ_ALWAYS_SUCCEEDS(mCallThread->Dispatch(NS_NewRunnableFunction(
+ __func__, [this, self = RefPtr<WebrtcAudioConduit>(this),
+ packet_id = aOptions.packet_id, now_ms, ssrc, seqno] {
+ mRtpSendBaseSeqs.insert({ssrc, seqno});
+ if (packet_id >= 0) {
+ if (mCall->Call()) {
+ // TODO: This notification should ideally happen after the
+ // transport layer has sent the packet on the wire.
+ mCall->Call()->OnSentPacket({packet_id, now_ms});
+ }
+ }
+ })));
+ }
+ return true;
+}
+
+bool WebrtcAudioConduit::SendSenderRtcp(const uint8_t* aData, size_t aLength) {
+ CSFLogVerbose(
+ LOGTAG,
+ "AudioConduit %p: Sending RTCP SR Packet, len %zu, SSRC %u (0x%x)", this,
+ aLength, (uint32_t)ntohl(*((uint32_t*)&aData[4])),
+ (uint32_t)ntohl(*((uint32_t*)&aData[4])));
+
+ if (!mTransportActive) {
+ CSFLogError(LOGTAG, "%s RTCP SR Packet Send Failed ", __FUNCTION__);
+ return false;
+ }
+
+ MediaPacket packet;
+ packet.Copy(aData, aLength, aLength + SRTP_MAX_EXPANSION);
+ packet.SetType(MediaPacket::RTCP);
+ mSenderRtcpSendEvent.Notify(std::move(packet));
+ return true;
+}
+
+bool WebrtcAudioConduit::SendReceiverRtcp(const uint8_t* aData,
+ size_t aLength) {
+ CSFLogVerbose(
+ LOGTAG,
+ "AudioConduit %p: Sending RTCP RR Packet, len %zu, SSRC %u (0x%x)", this,
+ aLength, (uint32_t)ntohl(*((uint32_t*)&aData[4])),
+ (uint32_t)ntohl(*((uint32_t*)&aData[4])));
+
+ if (!mTransportActive) {
+ CSFLogError(LOGTAG, "AudioConduit %p: RTCP RR Packet Send Failed", this);
+ return false;
+ }
+
+ MediaPacket packet;
+ packet.Copy(aData, aLength, aLength + SRTP_MAX_EXPANSION);
+ packet.SetType(MediaPacket::RTCP);
+ mReceiverRtcpSendEvent.Notify(std::move(packet));
+ return true;
+}
+
+/**
+ * Supported Sampling Frequencies.
+ */
+bool WebrtcAudioConduit::IsSamplingFreqSupported(int freq) const {
+ return GetNum10msSamplesForFrequency(freq) != 0;
+}
+
+std::vector<webrtc::RtpSource> WebrtcAudioConduit::GetUpstreamRtpSources()
+ const {
+ MOZ_ASSERT(NS_IsMainThread());
+ std::vector<webrtc::RtpSource> sources;
+ {
+ AutoReadLock lock(mLock);
+ if (mRecvStream) {
+ sources = mRecvStream->GetSources();
+ }
+ }
+ return sources;
+}
+
+/* Return block-length of 10 ms audio frame in number of samples */
+unsigned int WebrtcAudioConduit::GetNum10msSamplesForFrequency(
+ int samplingFreqHz) const {
+ switch (samplingFreqHz) {
+ case 16000:
+ return 160; // 160 samples
+ case 32000:
+ return 320; // 320 samples
+ case 44100:
+ return 441; // 441 samples
+ case 48000:
+ return 480; // 480 samples
+ default:
+ return 0; // invalid or unsupported
+ }
+}
+
+/**
+ * Perform validation on the codecConfig to be applied.
+ * Verifies if the codec is already applied.
+ */
+MediaConduitErrorCode WebrtcAudioConduit::ValidateCodecConfig(
+ const AudioCodecConfig& codecInfo, bool send) {
+ if (codecInfo.mName.empty()) {
+ CSFLogError(LOGTAG, "%s Empty Payload Name ", __FUNCTION__);
+ return kMediaConduitMalformedArgument;
+ }
+
+ // Only mono or stereo channels supported
+ if ((codecInfo.mChannels != 1) && (codecInfo.mChannels != 2)) {
+ CSFLogError(LOGTAG, "%s Channel Unsupported ", __FUNCTION__);
+ return kMediaConduitMalformedArgument;
+ }
+
+ return kMediaConduitNoError;
+}
+
+RtpExtList WebrtcAudioConduit::FilterExtensions(LocalDirection aDirection,
+ const RtpExtList& aExtensions) {
+ const bool isSend = aDirection == LocalDirection::kSend;
+ RtpExtList filteredExtensions;
+
+ for (const auto& extension : aExtensions) {
+ // ssrc-audio-level RTP header extension
+ if (extension.uri == webrtc::RtpExtension::kAudioLevelUri) {
+ filteredExtensions.push_back(
+ webrtc::RtpExtension(extension.uri, extension.id));
+ }
+
+ // csrc-audio-level RTP header extension
+ if (extension.uri == webrtc::RtpExtension::kCsrcAudioLevelsUri) {
+ if (isSend) {
+ continue;
+ }
+ filteredExtensions.push_back(
+ webrtc::RtpExtension(extension.uri, extension.id));
+ }
+
+ // MID RTP header extension
+ if (extension.uri == webrtc::RtpExtension::kMidUri) {
+ if (!isSend) {
+ // TODO: recv mid support, see also bug 1727211
+ continue;
+ }
+ filteredExtensions.push_back(
+ webrtc::RtpExtension(extension.uri, extension.id));
+ }
+ }
+
+ return filteredExtensions;
+}
+
+webrtc::SdpAudioFormat WebrtcAudioConduit::CodecConfigToLibwebrtcFormat(
+ const AudioCodecConfig& aConfig) {
+ webrtc::SdpAudioFormat::Parameters parameters;
+ if (aConfig.mName == kOpusCodecName) {
+ if (aConfig.mChannels == 2) {
+ parameters[kCodecParamStereo] = kParamValueTrue;
+ }
+ if (aConfig.mFECEnabled) {
+ parameters[kCodecParamUseInbandFec] = kParamValueTrue;
+ }
+ if (aConfig.mDTXEnabled) {
+ parameters[kCodecParamUseDtx] = kParamValueTrue;
+ }
+ if (aConfig.mMaxPlaybackRate) {
+ parameters[kCodecParamMaxPlaybackRate] =
+ std::to_string(aConfig.mMaxPlaybackRate);
+ }
+ if (aConfig.mMaxAverageBitrate) {
+ parameters[kCodecParamMaxAverageBitrate] =
+ std::to_string(aConfig.mMaxAverageBitrate);
+ }
+ if (aConfig.mFrameSizeMs) {
+ parameters[kCodecParamPTime] = std::to_string(aConfig.mFrameSizeMs);
+ }
+ if (aConfig.mMinFrameSizeMs) {
+ parameters[kCodecParamMinPTime] = std::to_string(aConfig.mMinFrameSizeMs);
+ }
+ if (aConfig.mMaxFrameSizeMs) {
+ parameters[kCodecParamMaxPTime] = std::to_string(aConfig.mMaxFrameSizeMs);
+ }
+ if (aConfig.mCbrEnabled) {
+ parameters[kCodecParamCbr] = kParamValueTrue;
+ }
+ }
+
+ return webrtc::SdpAudioFormat(aConfig.mName, aConfig.mFreq, aConfig.mChannels,
+ parameters);
+}
+
+void WebrtcAudioConduit::DeleteSendStream() {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ MOZ_ASSERT(mLock.LockedForWritingByCurrentThread());
+
+ if (!mSendStream) {
+ return;
+ }
+
+ mCall->Call()->DestroyAudioSendStream(mSendStream);
+ mSendStreamRunning = false;
+ mSendStream = nullptr;
+
+ // Reset base_seqs in case ssrcs get re-used.
+ mRtpSendBaseSeqs.clear();
+}
+
+void WebrtcAudioConduit::CreateSendStream() {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ MOZ_ASSERT(mLock.LockedForWritingByCurrentThread());
+
+ if (mSendStream) {
+ return;
+ }
+
+ mSendStream = mCall->Call()->CreateAudioSendStream(mSendStreamConfig);
+}
+
+void WebrtcAudioConduit::DeleteRecvStream() {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ MOZ_ASSERT(mLock.LockedForWritingByCurrentThread());
+
+ if (!mRecvStream) {
+ return;
+ }
+
+ mCall->Call()->DestroyAudioReceiveStream(mRecvStream);
+ mRecvStreamRunning = false;
+ mRecvStream = nullptr;
+}
+
+void WebrtcAudioConduit::CreateRecvStream() {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ MOZ_ASSERT(mLock.LockedForWritingByCurrentThread());
+
+ if (mRecvStream) {
+ return;
+ }
+
+ mRecvStream = mCall->Call()->CreateAudioReceiveStream(mRecvStreamConfig);
+ // Ensure that we set the jitter buffer target on this stream.
+ mRecvStream->SetBaseMinimumPlayoutDelayMs(mJitterBufferTargetMs);
+}
+
+void WebrtcAudioConduit::SetJitterBufferTarget(DOMHighResTimeStamp aTargetMs) {
+ MOZ_RELEASE_ASSERT(aTargetMs <= std::numeric_limits<uint16_t>::max());
+ MOZ_RELEASE_ASSERT(aTargetMs >= 0);
+
+ MOZ_ALWAYS_SUCCEEDS(mCallThread->Dispatch(NS_NewRunnableFunction(
+ __func__,
+ [this, self = RefPtr<WebrtcAudioConduit>(this), targetMs = aTargetMs] {
+ mJitterBufferTargetMs = static_cast<uint16_t>(targetMs);
+ if (mRecvStream) {
+ mRecvStream->SetBaseMinimumPlayoutDelayMs(targetMs);
+ }
+ })));
+}
+
+void WebrtcAudioConduit::DeliverPacket(rtc::CopyOnWriteBuffer packet,
+ PacketType type) {
+ // Currently unused.
+ MOZ_ASSERT(false);
+}
+
+Maybe<int> WebrtcAudioConduit::ActiveSendPayloadType() const {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+
+ auto stats = GetSenderStats();
+ if (!stats) {
+ return Nothing();
+ }
+
+ if (!stats->codec_payload_type) {
+ return Nothing();
+ }
+
+ return Some(*stats->codec_payload_type);
+}
+
+Maybe<int> WebrtcAudioConduit::ActiveRecvPayloadType() const {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+
+ auto stats = GetReceiverStats();
+ if (!stats) {
+ return Nothing();
+ }
+
+ if (!stats->codec_payload_type) {
+ return Nothing();
+ }
+
+ return Some(*stats->codec_payload_type);
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/libwebrtcglue/AudioConduit.h b/dom/media/webrtc/libwebrtcglue/AudioConduit.h
new file mode 100644
index 0000000000..e8de331e12
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/AudioConduit.h
@@ -0,0 +1,303 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef AUDIO_SESSION_H_
+#define AUDIO_SESSION_H_
+
+#include "mozilla/Attributes.h"
+#include "mozilla/ReentrantMonitor.h"
+#include "mozilla/RWLock.h"
+#include "mozilla/StateMirroring.h"
+#include "mozilla/TimeStamp.h"
+
+#include "MediaConduitInterface.h"
+#include "common/MediaEngineWrapper.h"
+
+/**
+ * This file hosts several structures identifying different aspects of a RTP
+ * Session.
+ */
+namespace mozilla {
+
+struct DtmfEvent;
+
+/**
+ * Concrete class for Audio session. Hooks up
+ * - media-source and target to external transport
+ */
+class WebrtcAudioConduit : public AudioSessionConduit,
+ public webrtc::RtcpEventObserver {
+ public:
+ Maybe<int> ActiveSendPayloadType() const override;
+ Maybe<int> ActiveRecvPayloadType() const override;
+
+ void OnRtpReceived(webrtc::RtpPacketReceived&& aPacket,
+ webrtc::RTPHeader&& aHeader);
+ void OnRtcpReceived(MediaPacket&& aPacket);
+
+ void OnRtcpBye() override;
+ void OnRtcpTimeout() override;
+
+ void SetTransportActive(bool aActive) override;
+
+ MediaEventSourceExc<MediaPacket>& SenderRtpSendEvent() override {
+ return mSenderRtpSendEvent;
+ }
+ MediaEventSourceExc<MediaPacket>& SenderRtcpSendEvent() override {
+ return mSenderRtcpSendEvent;
+ }
+ MediaEventSourceExc<MediaPacket>& ReceiverRtcpSendEvent() override {
+ return mReceiverRtcpSendEvent;
+ }
+ void ConnectReceiverRtpEvent(
+ MediaEventSourceExc<webrtc::RtpPacketReceived, webrtc::RTPHeader>& aEvent)
+ override {
+ mReceiverRtpEventListener =
+ aEvent.Connect(mCallThread, this, &WebrtcAudioConduit::OnRtpReceived);
+ }
+ void ConnectReceiverRtcpEvent(
+ MediaEventSourceExc<MediaPacket>& aEvent) override {
+ mReceiverRtcpEventListener =
+ aEvent.Connect(mCallThread, this, &WebrtcAudioConduit::OnRtcpReceived);
+ }
+ void ConnectSenderRtcpEvent(
+ MediaEventSourceExc<MediaPacket>& aEvent) override {
+ mSenderRtcpEventListener =
+ aEvent.Connect(mCallThread, this, &WebrtcAudioConduit::OnRtcpReceived);
+ }
+
+ Maybe<uint16_t> RtpSendBaseSeqFor(uint32_t aSsrc) const override;
+
+ const dom::RTCStatsTimestampMaker& GetTimestampMaker() const override;
+
+ void StopTransmitting();
+ void StartTransmitting();
+ void StopReceiving();
+ void StartReceiving();
+
+ /**
+ * Function to deliver externally captured audio sample for encoding and
+ * transport
+ * @param frame [in]: AudioFrame in upstream's format for forwarding to the
+ * send stream. Ownership is passed along.
+ * NOTE: ConfigureSendMediaCodec() SHOULD be called before this function can
+ * be invoked. This ensures the inserted audio-samples can be transmitted by
+ * the conduit.
+ */
+ MediaConduitErrorCode SendAudioFrame(
+ std::unique_ptr<webrtc::AudioFrame> frame) override;
+
+ /**
+ * Function to grab a decoded audio-sample from the media engine for
+ * rendering / playout of length 10 milliseconds.
+ *
+ * @param samplingFreqHz [in]: Frequency of the sampling for playback in
+ * Hertz (16000, 32000,..)
+ * @param frame [in/out]: Pointer to an AudioFrame to which audio data will be
+ * copied
+ * NOTE: This function should be invoked every 10 milliseconds for the best
+ * performance
+ * NOTE: ConfigureRecvMediaCodec() SHOULD be called before this function can
+ * be invoked
+ * This ensures the decoded samples are ready for reading and playout is
+ * enabled.
+ */
+ MediaConduitErrorCode GetAudioFrame(int32_t samplingFreqHz,
+ webrtc::AudioFrame* frame) override;
+
+ bool SendRtp(const uint8_t* aData, size_t aLength,
+ const webrtc::PacketOptions& aOptions) override;
+ bool SendSenderRtcp(const uint8_t* aData, size_t aLength) override;
+ bool SendReceiverRtcp(const uint8_t* aData, size_t aLength) override;
+
+ bool HasCodecPluginID(uint64_t aPluginID) const override { return false; }
+
+ void SetJitterBufferTarget(DOMHighResTimeStamp aTargetMs) override;
+
+ void DeliverPacket(rtc::CopyOnWriteBuffer packet, PacketType type) override;
+
+ RefPtr<GenericPromise> Shutdown() override;
+
+ WebrtcAudioConduit(RefPtr<WebrtcCallWrapper> aCall,
+ nsCOMPtr<nsISerialEventTarget> aStsThread);
+
+ virtual ~WebrtcAudioConduit();
+
+ // Call thread.
+ void InitControl(AudioConduitControlInterface* aControl) override;
+
+ // Handle a DTMF event from mControl.mOnDtmfEventListener.
+ void OnDtmfEvent(const DtmfEvent& aEvent);
+
+ // Called when a parameter in mControl has changed. Call thread.
+ void OnControlConfigChange();
+
+ Ssrcs GetLocalSSRCs() const override;
+ Maybe<Ssrc> GetRemoteSSRC() const override;
+
+ void DisableSsrcChanges() override {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ mAllowSsrcChange = false;
+ }
+
+ private:
+ /**
+ * Override the remote ssrc configured on mRecvStreamConfig.
+ *
+ * Recreates and restarts the recv stream if needed. The overriden value is
+ * overwritten the next time the mControl.mRemoteSsrc mirror changes value.
+ *
+ * Call thread only.
+ */
+ bool OverrideRemoteSSRC(uint32_t aSsrc);
+
+ public:
+ void UnsetRemoteSSRC(uint32_t aSsrc) override {}
+
+ Maybe<webrtc::AudioReceiveStreamInterface::Stats> GetReceiverStats()
+ const override;
+ Maybe<webrtc::AudioSendStream::Stats> GetSenderStats() const override;
+ Maybe<webrtc::CallBasicStats> GetCallStats() const override;
+
+ bool IsSamplingFreqSupported(int freq) const override;
+
+ MediaEventSource<void>& RtcpByeEvent() override { return mRtcpByeEvent; }
+ MediaEventSource<void>& RtcpTimeoutEvent() override {
+ return mRtcpTimeoutEvent;
+ }
+ MediaEventSource<void>& RtpPacketEvent() override { return mRtpPacketEvent; }
+
+ std::vector<webrtc::RtpSource> GetUpstreamRtpSources() const override;
+
+ private:
+ WebrtcAudioConduit(const WebrtcAudioConduit& other) = delete;
+ void operator=(const WebrtcAudioConduit& other) = delete;
+
+ // Generate block size in sample length for a given sampling frequency
+ unsigned int GetNum10msSamplesForFrequency(int samplingFreqHz) const;
+
+ // Checks the codec to be applied
+ static MediaConduitErrorCode ValidateCodecConfig(
+ const AudioCodecConfig& codecInfo, bool send);
+ /**
+ * Of all extensions in aExtensions, returns a list of supported extensions.
+ */
+ static RtpExtList FilterExtensions(
+ MediaSessionConduitLocalDirection aDirection,
+ const RtpExtList& aExtensions);
+ static webrtc::SdpAudioFormat CodecConfigToLibwebrtcFormat(
+ const AudioCodecConfig& aConfig);
+
+ void CreateSendStream();
+ void DeleteSendStream();
+ void CreateRecvStream();
+ void DeleteRecvStream();
+
+ // Are SSRC changes without signaling allowed or not.
+ // Call thread only.
+ bool mAllowSsrcChange = true;
+
+ // Const so can be accessed on any thread. Most methods are called on the Call
+ // thread.
+ const RefPtr<WebrtcCallWrapper> mCall;
+
+ // Set up in the ctor and then not touched. Called through by the streams on
+ // any thread.
+ WebrtcSendTransport mSendTransport;
+ WebrtcReceiveTransport mRecvTransport;
+
+ // Accessed only on the Call thread.
+ webrtc::AudioReceiveStreamInterface::Config mRecvStreamConfig;
+
+ // Written only on the Call thread. Guarded by mLock, except for reads on the
+ // Call thread.
+ webrtc::AudioReceiveStreamInterface* mRecvStream;
+
+ // Accessed only on the Call thread.
+ webrtc::AudioSendStream::Config mSendStreamConfig;
+
+ // Written only on the Call thread. Guarded by mLock, except for reads on the
+ // Call thread.
+ webrtc::AudioSendStream* mSendStream;
+
+ // If true => mSendStream started and not stopped
+ // Written only on the Call thread.
+ Atomic<bool> mSendStreamRunning;
+ // If true => mRecvStream started and not stopped
+ // Written only on the Call thread.
+ Atomic<bool> mRecvStreamRunning;
+
+ // Accessed only on the Call thread.
+ bool mDtmfEnabled;
+
+ mutable RWLock mLock MOZ_UNANNOTATED;
+
+ // Call worker thread. All access to mCall->Call() happens here.
+ const RefPtr<AbstractThread> mCallThread;
+
+ // Socket transport service thread. Any thread.
+ const nsCOMPtr<nsISerialEventTarget> mStsThread;
+
+ // Target jitter buffer to be applied to the receive stream in milliseconds.
+ uint16_t mJitterBufferTargetMs = 0;
+
+ struct Control {
+ // Mirrors and events that map to AudioConduitControlInterface for control.
+ // Call thread only.
+ Mirror<bool> mReceiving;
+ Mirror<bool> mTransmitting;
+ Mirror<Ssrcs> mLocalSsrcs;
+ Mirror<std::string> mLocalCname;
+ Mirror<std::string> mMid;
+ Mirror<Ssrc> mRemoteSsrc;
+ Mirror<std::string> mSyncGroup;
+ Mirror<RtpExtList> mLocalRecvRtpExtensions;
+ Mirror<RtpExtList> mLocalSendRtpExtensions;
+ Mirror<Maybe<AudioCodecConfig>> mSendCodec;
+ Mirror<std::vector<AudioCodecConfig>> mRecvCodecs;
+ MediaEventListener mOnDtmfEventListener;
+
+ // For caching mRemoteSsrc, since another caller may change the remote ssrc
+ // in the stream config directly.
+ Ssrc mConfiguredRemoteSsrc = 0;
+ // For tracking changes to mSendCodec.
+ Maybe<AudioCodecConfig> mConfiguredSendCodec;
+ // For tracking changes to mRecvCodecs.
+ std::vector<AudioCodecConfig> mConfiguredRecvCodecs;
+
+ Control() = delete;
+ explicit Control(const RefPtr<AbstractThread>& aCallThread);
+ } mControl;
+
+ // WatchManager allowing Mirrors to trigger functions that will update the
+ // webrtc.org configuration.
+ WatchManager<WebrtcAudioConduit> mWatchManager;
+
+ // Accessed from mStsThread. Last successfully polled RTT
+ Maybe<DOMHighResTimeStamp> mRttSec;
+
+ // Call thread only. ssrc -> base_seq
+ std::map<uint32_t, uint16_t> mRtpSendBaseSeqs;
+ // libwebrtc network thread only. ssrc -> base_seq.
+ // To track changes needed to mRtpSendBaseSeqs.
+ std::map<uint32_t, uint16_t> mRtpSendBaseSeqs_n;
+
+ // Thread safe
+ Atomic<bool> mTransportActive = Atomic<bool>(false);
+ MediaEventProducer<void> mRtcpByeEvent;
+ MediaEventProducer<void> mRtcpTimeoutEvent;
+ MediaEventProducer<void> mRtpPacketEvent;
+ MediaEventProducerExc<MediaPacket> mSenderRtpSendEvent;
+ MediaEventProducerExc<MediaPacket> mSenderRtcpSendEvent;
+ MediaEventProducerExc<MediaPacket> mReceiverRtcpSendEvent;
+
+ // Assigned and revoked on mStsThread. Listeners for receiving packets.
+ MediaEventListener mSenderRtcpEventListener; // Rtp-transmitting pipeline
+ MediaEventListener mReceiverRtcpEventListener; // Rtp-receiving pipeline
+ MediaEventListener mReceiverRtpEventListener; // Rtp-receiving pipeline
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/libwebrtcglue/CallWorkerThread.h b/dom/media/webrtc/libwebrtcglue/CallWorkerThread.h
new file mode 100644
index 0000000000..12d21fbee4
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/CallWorkerThread.h
@@ -0,0 +1,116 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_WEBRTC_LIBWEBRTCGLUE_CALLWORKERTHREAD_H_
+#define DOM_MEDIA_WEBRTC_LIBWEBRTCGLUE_CALLWORKERTHREAD_H_
+
+#include "mozilla/AbstractThread.h"
+#include "nsIDirectTaskDispatcher.h"
+#include "TaskQueueWrapper.h"
+
+namespace mozilla {
+
+// Implements AbstractThread for running things on the webrtc TaskQueue.
+// Webrtc TaskQueues are not refcounted so cannot implement AbstractThread
+// directly.
+class CallWorkerThread final : public AbstractThread,
+ public nsIDirectTaskDispatcher {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIDIRECTTASKDISPATCHER
+
+ explicit CallWorkerThread(
+ UniquePtr<TaskQueueWrapper<DeletionPolicy::NonBlocking>> aWebrtcTaskQueue)
+ : AbstractThread(aWebrtcTaskQueue->mTaskQueue->SupportsTailDispatch()),
+ mWebrtcTaskQueue(std::move(aWebrtcTaskQueue)) {}
+
+ // AbstractThread overrides
+ nsresult Dispatch(already_AddRefed<nsIRunnable> aRunnable,
+ DispatchReason aReason) override;
+ bool IsCurrentThreadIn() const override;
+ TaskDispatcher& TailDispatcher() override;
+ nsIEventTarget* AsEventTarget() override;
+ NS_IMETHOD
+ DelayedDispatch(already_AddRefed<nsIRunnable> aEvent,
+ uint32_t aDelayMs) override;
+
+ NS_IMETHOD RegisterShutdownTask(nsITargetShutdownTask* aTask) override;
+ NS_IMETHOD UnregisterShutdownTask(nsITargetShutdownTask* aTask) override;
+
+ const UniquePtr<TaskQueueWrapper<DeletionPolicy::NonBlocking>>
+ mWebrtcTaskQueue;
+
+ protected:
+ ~CallWorkerThread() = default;
+};
+
+NS_IMPL_ISUPPORTS(CallWorkerThread, nsIDirectTaskDispatcher,
+ nsISerialEventTarget, nsIEventTarget);
+
+//-----------------------------------------------------------------------------
+// AbstractThread
+//-----------------------------------------------------------------------------
+
+nsresult CallWorkerThread::Dispatch(already_AddRefed<nsIRunnable> aRunnable,
+ DispatchReason aReason) {
+ RefPtr<nsIRunnable> runnable = aRunnable;
+ return mWebrtcTaskQueue->mTaskQueue->Dispatch(
+ mWebrtcTaskQueue->CreateTaskRunner(std::move(runnable)), aReason);
+}
+
+bool CallWorkerThread::IsCurrentThreadIn() const {
+ return mWebrtcTaskQueue->mTaskQueue->IsOnCurrentThreadInfallible() &&
+ mWebrtcTaskQueue->IsCurrent();
+}
+
+TaskDispatcher& CallWorkerThread::TailDispatcher() {
+ return mWebrtcTaskQueue->mTaskQueue->TailDispatcher();
+}
+
+nsIEventTarget* CallWorkerThread::AsEventTarget() {
+ return mWebrtcTaskQueue->mTaskQueue->AsEventTarget();
+}
+
+NS_IMETHODIMP
+CallWorkerThread::DelayedDispatch(already_AddRefed<nsIRunnable> aEvent,
+ uint32_t aDelayMs) {
+ RefPtr<nsIRunnable> event = aEvent;
+ return mWebrtcTaskQueue->mTaskQueue->DelayedDispatch(
+ mWebrtcTaskQueue->CreateTaskRunner(std::move(event)), aDelayMs);
+}
+
+NS_IMETHODIMP CallWorkerThread::RegisterShutdownTask(
+ nsITargetShutdownTask* aTask) {
+ return mWebrtcTaskQueue->mTaskQueue->RegisterShutdownTask(aTask);
+}
+
+NS_IMETHODIMP CallWorkerThread::UnregisterShutdownTask(
+ nsITargetShutdownTask* aTask) {
+ return mWebrtcTaskQueue->mTaskQueue->UnregisterShutdownTask(aTask);
+}
+
+//-----------------------------------------------------------------------------
+// nsIDirectTaskDispatcher
+//-----------------------------------------------------------------------------
+
+NS_IMETHODIMP
+CallWorkerThread::DispatchDirectTask(already_AddRefed<nsIRunnable> aEvent) {
+ nsCOMPtr<nsIRunnable> event = aEvent;
+ return mWebrtcTaskQueue->mTaskQueue->DispatchDirectTask(
+ mWebrtcTaskQueue->CreateTaskRunner(std::move(event)));
+}
+
+NS_IMETHODIMP CallWorkerThread::DrainDirectTasks() {
+ return mWebrtcTaskQueue->mTaskQueue->DrainDirectTasks();
+}
+
+NS_IMETHODIMP CallWorkerThread::HaveDirectTasks(bool* aValue) {
+ return mWebrtcTaskQueue->mTaskQueue->HaveDirectTasks(aValue);
+}
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/libwebrtcglue/CodecConfig.h b/dom/media/webrtc/libwebrtcglue/CodecConfig.h
new file mode 100644
index 0000000000..023ea98783
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/CodecConfig.h
@@ -0,0 +1,237 @@
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef CODEC_CONFIG_H_
+#define CODEC_CONFIG_H_
+
+#include <string>
+#include <vector>
+
+#include "common/EncodingConstraints.h"
+
+namespace mozilla {
+
+/**
+ * Minimalistic Audio Codec Config Params
+ */
+struct AudioCodecConfig {
+ /*
+ * The data-types for these properties mimic the
+ * corresponding webrtc::CodecInst data-types.
+ */
+ int mType;
+ std::string mName;
+ int mFreq;
+ int mChannels;
+
+ bool mFECEnabled;
+ bool mDtmfEnabled;
+ uint32_t mFrameSizeMs;
+ uint32_t mMaxFrameSizeMs;
+ uint32_t mMinFrameSizeMs;
+
+ // OPUS-specific
+ bool mDTXEnabled;
+ uint32_t mMaxAverageBitrate;
+ int mMaxPlaybackRate;
+ bool mCbrEnabled;
+
+ AudioCodecConfig(int type, std::string name, int freq, int channels,
+ bool FECEnabled)
+ : mType(type),
+ mName(name),
+ mFreq(freq),
+ mChannels(channels),
+ mFECEnabled(FECEnabled),
+ mDtmfEnabled(false),
+ mFrameSizeMs(0),
+ mMaxFrameSizeMs(0),
+ mMinFrameSizeMs(0),
+ mDTXEnabled(false),
+ mMaxAverageBitrate(0),
+ mMaxPlaybackRate(0),
+ mCbrEnabled(false) {}
+
+ bool operator==(const AudioCodecConfig& aOther) const {
+ return mType == aOther.mType && mName == aOther.mName &&
+ mFreq == aOther.mFreq && mChannels == aOther.mChannels &&
+ mFECEnabled == aOther.mFECEnabled &&
+ mDtmfEnabled == aOther.mDtmfEnabled &&
+ mFrameSizeMs == aOther.mFrameSizeMs &&
+ mMaxFrameSizeMs == aOther.mMaxFrameSizeMs &&
+ mMinFrameSizeMs == aOther.mMinFrameSizeMs &&
+ mDTXEnabled == aOther.mDTXEnabled &&
+ mMaxAverageBitrate == aOther.mMaxAverageBitrate &&
+ mMaxPlaybackRate == aOther.mMaxPlaybackRate &&
+ mCbrEnabled == aOther.mCbrEnabled;
+ }
+};
+
+/*
+ * Minimalistic video codec configuration
+ * More to be added later depending on the use-case
+ */
+
+#define MAX_SPROP_LEN 128
+
+// used for holding SDP negotiation results
+struct VideoCodecConfigH264 {
+ char sprop_parameter_sets[MAX_SPROP_LEN];
+ int packetization_mode;
+ int profile_level_id;
+ int tias_bw;
+
+ bool operator==(const VideoCodecConfigH264& aOther) const {
+ return strncmp(sprop_parameter_sets, aOther.sprop_parameter_sets,
+ MAX_SPROP_LEN) == 0 &&
+ packetization_mode == aOther.packetization_mode &&
+ profile_level_id == aOther.profile_level_id &&
+ tias_bw == aOther.tias_bw;
+ }
+};
+
+// class so the std::strings can get freed more easily/reliably
+class VideoCodecConfig {
+ public:
+ /*
+ * The data-types for these properties mimic the
+ * corresponding webrtc::VideoCodec data-types.
+ */
+ int mType; // payload type
+ std::string mName;
+
+ std::vector<std::string> mAckFbTypes;
+ std::vector<std::string> mNackFbTypes;
+ std::vector<std::string> mCcmFbTypes;
+ // Don't pass mOtherFbTypes from JsepVideoCodecDescription because we'd have
+ // to drag SdpRtcpFbAttributeList::Feedback along too.
+ bool mRembFbSet;
+ bool mFECFbSet;
+ bool mTransportCCFbSet;
+
+ int mULPFECPayloadType;
+ int mREDPayloadType;
+ int mREDRTXPayloadType;
+ int mRTXPayloadType;
+
+ uint32_t mTias;
+ EncodingConstraints mEncodingConstraints;
+ struct Encoding {
+ std::string rid;
+ EncodingConstraints constraints;
+ bool active = true;
+ // TODO(bug 1744116): Use = default here
+ bool operator==(const Encoding& aOther) const {
+ return rid == aOther.rid && constraints == aOther.constraints &&
+ active == aOther.active;
+ }
+ };
+ std::vector<Encoding> mEncodings;
+ std::string mSpropParameterSets;
+ uint8_t mProfile;
+ uint8_t mConstraints;
+ uint8_t mLevel;
+ uint8_t mPacketizationMode;
+ // TODO: add external negotiated SPS/PPS
+
+ // TODO(bug 1744116): Use = default here
+ bool operator==(const VideoCodecConfig& aRhs) const {
+ return mType == aRhs.mType && mName == aRhs.mName &&
+ mAckFbTypes == aRhs.mAckFbTypes &&
+ mNackFbTypes == aRhs.mNackFbTypes &&
+ mCcmFbTypes == aRhs.mCcmFbTypes && mRembFbSet == aRhs.mRembFbSet &&
+ mFECFbSet == aRhs.mFECFbSet &&
+ mTransportCCFbSet == aRhs.mTransportCCFbSet &&
+ mULPFECPayloadType == aRhs.mULPFECPayloadType &&
+ mREDPayloadType == aRhs.mREDPayloadType &&
+ mREDRTXPayloadType == aRhs.mREDRTXPayloadType &&
+ mRTXPayloadType == aRhs.mRTXPayloadType && mTias == aRhs.mTias &&
+ mEncodingConstraints == aRhs.mEncodingConstraints &&
+ mEncodings == aRhs.mEncodings &&
+ mSpropParameterSets == aRhs.mSpropParameterSets &&
+ mProfile == aRhs.mProfile && mConstraints == aRhs.mConstraints &&
+ mLevel == aRhs.mLevel &&
+ mPacketizationMode == aRhs.mPacketizationMode;
+ }
+
+ VideoCodecConfig(int type, std::string name,
+ const EncodingConstraints& constraints,
+ const struct VideoCodecConfigH264* h264 = nullptr)
+ : mType(type),
+ mName(name),
+ mRembFbSet(false),
+ mFECFbSet(false),
+ mTransportCCFbSet(false),
+ mULPFECPayloadType(-1),
+ mREDPayloadType(-1),
+ mREDRTXPayloadType(-1),
+ mRTXPayloadType(-1),
+ mTias(0),
+ mEncodingConstraints(constraints),
+ mProfile(0x42),
+ mConstraints(0xE0),
+ mLevel(0x0C),
+ mPacketizationMode(1) {
+ if (h264) {
+ mProfile = (h264->profile_level_id & 0x00FF0000) >> 16;
+ mConstraints = (h264->profile_level_id & 0x0000FF00) >> 8;
+ mLevel = (h264->profile_level_id & 0x000000FF);
+ mPacketizationMode = h264->packetization_mode;
+ mSpropParameterSets = h264->sprop_parameter_sets;
+ }
+ }
+
+ bool ResolutionEquals(const VideoCodecConfig& aConfig) const {
+ if (mEncodings.size() != aConfig.mEncodings.size()) {
+ return false;
+ }
+ for (size_t i = 0; i < mEncodings.size(); ++i) {
+ if (!mEncodings[i].constraints.ResolutionEquals(
+ aConfig.mEncodings[i].constraints)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ // Nothing seems to use this right now. Do we intend to support this
+ // someday?
+ bool RtcpFbAckIsSet(const std::string& type) const {
+ for (auto i = mAckFbTypes.begin(); i != mAckFbTypes.end(); ++i) {
+ if (*i == type) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ bool RtcpFbNackIsSet(const std::string& type) const {
+ for (auto i = mNackFbTypes.begin(); i != mNackFbTypes.end(); ++i) {
+ if (*i == type) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ bool RtcpFbCcmIsSet(const std::string& type) const {
+ for (auto i = mCcmFbTypes.begin(); i != mCcmFbTypes.end(); ++i) {
+ if (*i == type) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ bool RtcpFbRembIsSet() const { return mRembFbSet; }
+
+ bool RtcpFbFECIsSet() const { return mFECFbSet; }
+
+ bool RtcpFbTransportCCIsSet() const { return mTransportCCFbSet; }
+
+ bool RtxPayloadTypeIsSet() const { return mRTXPayloadType != -1; }
+};
+} // namespace mozilla
+#endif
diff --git a/dom/media/webrtc/libwebrtcglue/GmpVideoCodec.cpp b/dom/media/webrtc/libwebrtcglue/GmpVideoCodec.cpp
new file mode 100644
index 0000000000..ccadd846e2
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/GmpVideoCodec.cpp
@@ -0,0 +1,22 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WebrtcGmpVideoCodec.h"
+#include "GmpVideoCodec.h"
+
+namespace mozilla {
+
+WebrtcVideoEncoder* GmpVideoCodec::CreateEncoder(
+ const webrtc::SdpVideoFormat& aFormat, std::string aPCHandle) {
+ return new WebrtcVideoEncoderProxy(
+ new WebrtcGmpVideoEncoder(aFormat, std::move(aPCHandle)));
+}
+
+WebrtcVideoDecoder* GmpVideoCodec::CreateDecoder(std::string aPCHandle,
+ TrackingId aTrackingId) {
+ return new WebrtcVideoDecoderProxy(std::move(aPCHandle),
+ std::move(aTrackingId));
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/libwebrtcglue/GmpVideoCodec.h b/dom/media/webrtc/libwebrtcglue/GmpVideoCodec.h
new file mode 100644
index 0000000000..caf125c809
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/GmpVideoCodec.h
@@ -0,0 +1,27 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GMPVIDEOCODEC_H_
+#define GMPVIDEOCODEC_H_
+
+#include <string>
+
+#include "PerformanceRecorder.h"
+
+namespace mozilla {
+
+class WebrtcVideoDecoder;
+class WebrtcVideoEncoder;
+
+class GmpVideoCodec {
+ public:
+ static WebrtcVideoEncoder* CreateEncoder(
+ const webrtc::SdpVideoFormat& aFormat, std::string aPCHandle);
+ static WebrtcVideoDecoder* CreateDecoder(std::string aPCHandle,
+ TrackingId aTrackingId);
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/libwebrtcglue/MediaConduitControl.h b/dom/media/webrtc/libwebrtcglue/MediaConduitControl.h
new file mode 100644
index 0000000000..a860fab146
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/MediaConduitControl.h
@@ -0,0 +1,77 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_WEBRTC_LIBWEBRTCGLUE_MEDIACONDUITCONTROL_H_
+#define DOM_MEDIA_WEBRTC_LIBWEBRTCGLUE_MEDIACONDUITCONTROL_H_
+
+#include "jsapi/RTCDTMFSender.h" // For DtmfEvent
+#include "mozilla/StateMirroring.h"
+#include "RtpRtcpConfig.h"
+#include <vector>
+#include <string>
+#include "mozilla/Maybe.h"
+#include "CodecConfig.h" // For Audio/VideoCodecConfig
+#include "api/rtp_parameters.h" // For webrtc::RtpExtension
+#include "api/video_codecs/video_codec.h" // For webrtc::VideoCodecMode
+
+namespace mozilla {
+
+using RtpExtList = std::vector<webrtc::RtpExtension>;
+using Ssrc = uint32_t;
+using Ssrcs = std::vector<uint32_t>;
+
+/**
+ * These are the interfaces used to control the async conduits. Some parameters
+ * are common, and some are tied to the conduit type. See
+ * MediaSessionConduit::InitConduitControl for how they are used.
+ *
+ * Put simply, the implementer of the interfaces below may set its canonicals on
+ * any thread, and the conduits will react to those changes accordingly, on
+ * their dedicated worker thread. One instance of these interfaces could control
+ * multiple conduits as each canonical can connect to any number of mirrors.
+ */
+
+class MediaConduitControlInterface {
+ public:
+ virtual AbstractCanonical<bool>* CanonicalReceiving() = 0;
+ virtual AbstractCanonical<bool>* CanonicalTransmitting() = 0;
+ virtual AbstractCanonical<Ssrcs>* CanonicalLocalSsrcs() = 0;
+ virtual AbstractCanonical<std::string>* CanonicalLocalCname() = 0;
+ virtual AbstractCanonical<std::string>* CanonicalMid() = 0;
+ virtual AbstractCanonical<Ssrc>* CanonicalRemoteSsrc() = 0;
+ virtual AbstractCanonical<std::string>* CanonicalSyncGroup() = 0;
+ virtual AbstractCanonical<RtpExtList>* CanonicalLocalRecvRtpExtensions() = 0;
+ virtual AbstractCanonical<RtpExtList>* CanonicalLocalSendRtpExtensions() = 0;
+};
+
+class AudioConduitControlInterface : public MediaConduitControlInterface {
+ public:
+ virtual AbstractCanonical<Maybe<AudioCodecConfig>>*
+ CanonicalAudioSendCodec() = 0;
+ virtual AbstractCanonical<std::vector<AudioCodecConfig>>*
+ CanonicalAudioRecvCodecs() = 0;
+ virtual MediaEventSource<DtmfEvent>& OnDtmfEvent() = 0;
+};
+
+class VideoConduitControlInterface : public MediaConduitControlInterface {
+ public:
+ virtual AbstractCanonical<Ssrcs>* CanonicalLocalVideoRtxSsrcs() = 0;
+ virtual AbstractCanonical<Ssrc>* CanonicalRemoteVideoRtxSsrc() = 0;
+ virtual AbstractCanonical<Maybe<VideoCodecConfig>>*
+ CanonicalVideoSendCodec() = 0;
+ virtual AbstractCanonical<Maybe<RtpRtcpConfig>>*
+ CanonicalVideoSendRtpRtcpConfig() = 0;
+ virtual AbstractCanonical<std::vector<VideoCodecConfig>>*
+ CanonicalVideoRecvCodecs() = 0;
+ virtual AbstractCanonical<Maybe<RtpRtcpConfig>>*
+ CanonicalVideoRecvRtpRtcpConfig() = 0;
+ virtual AbstractCanonical<webrtc::VideoCodecMode>*
+ CanonicalVideoCodecMode() = 0;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/libwebrtcglue/MediaConduitErrors.h b/dom/media/webrtc/libwebrtcglue/MediaConduitErrors.h
new file mode 100644
index 0000000000..34487d77a0
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/MediaConduitErrors.h
@@ -0,0 +1,46 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MEDIA_SESSION_ERRORS_H_
+#define MEDIA_SESSION_ERRORS_H_
+
+namespace mozilla {
+enum MediaConduitErrorCode {
+ kMediaConduitNoError = 0, // 0 for Success,greater than 0 imples error
+ kMediaConduitSessionNotInited =
+ 10100, // Session not initialized.10100 serves as
+ // base for the conduit errors
+ kMediaConduitMalformedArgument, // Malformed input to Conduit API
+ kMediaConduitCaptureError, // WebRTC capture APIs failed
+ kMediaConduitInvalidSendCodec, // Wrong Send codec
+ kMediaConduitInvalidReceiveCodec, // Wrong Recv Codec
+ kMediaConduitCodecInUse, // Already applied Codec
+ kMediaConduitInvalidRenderer, // Null or Wrong Renderer object
+ kMediaConduitRendererFail, // Add Render called multiple times
+ kMediaConduitSendingAlready, // Engine already trasmitting
+ kMediaConduitReceivingAlready, // Engine already receiving
+ kMediaConduitTransportRegistrationFail, // Null or wrong transport interface
+ kMediaConduitInvalidTransport, // Null or wrong transport interface
+ kMediaConduitChannelError, // Configuration Error
+ kMediaConduitSocketError, // Media Engine transport socket error
+ kMediaConduitRTPRTCPModuleError, // Couldn't start RTP/RTCP processing
+ kMediaConduitRTPProcessingFailed, // Processing incoming RTP frame failed
+ kMediaConduitUnknownError, // More information can be found in logs
+ kMediaConduitExternalRecordingError, // Couldn't start external recording
+ kMediaConduitRecordingError, // Runtime recording error
+ kMediaConduitExternalPlayoutError, // Couldn't start external playout
+ kMediaConduitPlayoutError, // Runtime playout error
+ kMediaConduitMTUError, // Can't set MTU
+ kMediaConduitRTCPStatusError, // Can't set RTCP mode
+ kMediaConduitKeyFrameRequestError, // Can't set KeyFrameRequest mode
+ kMediaConduitNACKStatusError, // Can't set NACK mode
+ kMediaConduitTMMBRStatusError, // Can't set TMMBR mode
+ kMediaConduitFECStatusError, // Can't set FEC mode
+ kMediaConduitHybridNACKFECStatusError, // Can't set Hybrid NACK / FEC mode
+ kMediaConduitVideoSendStreamError // WebRTC video send stream failure
+};
+
+}
+
+#endif
diff --git a/dom/media/webrtc/libwebrtcglue/MediaConduitInterface.cpp b/dom/media/webrtc/libwebrtcglue/MediaConduitInterface.cpp
new file mode 100644
index 0000000000..28079b2478
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/MediaConduitInterface.cpp
@@ -0,0 +1,151 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaConduitInterface.h"
+
+#include "nsTArray.h"
+#include "mozilla/Assertions.h"
+#include "MainThreadUtils.h"
+#include "SystemTime.h"
+
+#include "system_wrappers/include/clock.h"
+
+namespace mozilla {
+
+void MediaSessionConduit::GetRtpSources(
+ nsTArray<dom::RTCRtpSourceEntry>& outSources) const {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mSourcesUpdateNeeded) {
+ UpdateRtpSources(GetUpstreamRtpSources());
+ OnSourcesUpdated();
+ }
+ outSources.Clear();
+ for (auto& [key, entry] : mSourcesCache) {
+ (void)key;
+ outSources.AppendElement(entry);
+ }
+
+ struct TimestampComparator {
+ bool LessThan(const dom::RTCRtpSourceEntry& aLhs,
+ const dom::RTCRtpSourceEntry& aRhs) const {
+ // Sort descending!
+ return aLhs.mTimestamp > aRhs.mTimestamp;
+ }
+
+ bool Equals(const dom::RTCRtpSourceEntry& aLhs,
+ const dom::RTCRtpSourceEntry& aRhs) const {
+ return aLhs.mTimestamp == aRhs.mTimestamp;
+ }
+ };
+
+ // *sigh* We have to re-sort this by JS timestamp; we can run into cases
+ // where the libwebrtc timestamps are not in exactly the same order as JS
+ // timestamps due to clock differences (wibbly-wobbly, timey-wimey stuff)
+ outSources.Sort(TimestampComparator());
+}
+
+static double rtpToDomAudioLevel(uint8_t aAudioLevel) {
+ if (aAudioLevel == 127) {
+ // Spec indicates that a value of 127 should be set to 0
+ return 0;
+ }
+
+ // All other values are calculated as 10^(-rfc_level/20)
+ return std::pow(10, -aAudioLevel / 20.0);
+}
+
+void MediaSessionConduit::UpdateRtpSources(
+ const std::vector<webrtc::RtpSource>& aSources) const {
+ MOZ_ASSERT(NS_IsMainThread());
+ // Empty out the cache; we'll copy things back as needed
+ auto cache = std::move(mSourcesCache);
+
+ for (const auto& source : aSources) {
+ SourceKey key(source);
+ auto it = cache.find(key);
+ if (it != cache.end()) {
+ // This source entry was already in the cache, and should continue to be
+ // present in exactly the same form as before. This means we do _not_
+ // want to perform the timestamp adjustment again, since it might yield a
+ // slightly different result. This is why we copy this entry from the old
+ // cache instead of simply rebuilding it, and is also why we key the
+ // cache based on timestamp (keying the cache based on timestamp also
+ // gets us the ordering we want, conveniently).
+ mSourcesCache[key] = it->second;
+ continue;
+ }
+
+ // This is something we did not already have in the cache.
+ dom::RTCRtpSourceEntry domEntry;
+ domEntry.mSource = source.source_id();
+ switch (source.source_type()) {
+ case webrtc::RtpSourceType::SSRC:
+ domEntry.mSourceType = dom::RTCRtpSourceEntryType::Synchronization;
+ break;
+ case webrtc::RtpSourceType::CSRC:
+ domEntry.mSourceType = dom::RTCRtpSourceEntryType::Contributing;
+ break;
+ default:
+ MOZ_CRASH("Unexpected RTCRtpSourceEntryType");
+ }
+
+ if (source.audio_level()) {
+ domEntry.mAudioLevel.Construct(rtpToDomAudioLevel(*source.audio_level()));
+ }
+
+ // These timestamps are always **rounded** to milliseconds. That means they
+ // can jump up to half a millisecond into the future. We compensate for that
+ // here so that things seem consistent to js.
+ domEntry.mTimestamp = dom::RTCStatsTimestamp::FromRealtime(
+ GetTimestampMaker(),
+ webrtc::Timestamp::Millis(source.timestamp_ms()) -
+ webrtc::TimeDelta::Micros(500))
+ .ToDom();
+ domEntry.mRtpTimestamp = source.rtp_timestamp();
+ mSourcesCache[key] = domEntry;
+ }
+}
+
+void MediaSessionConduit::OnSourcesUpdated() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mSourcesUpdateNeeded);
+ mSourcesUpdateNeeded = false;
+ // Reset the updateNeeded flag and clear the cache in a direct task, i.e.,
+ // as soon as the current task has finished.
+ AbstractThread::GetCurrent()->TailDispatcher().AddDirectTask(
+ NS_NewRunnableFunction(
+ __func__, [this, self = RefPtr<const MediaSessionConduit>(this)] {
+ mSourcesUpdateNeeded = true;
+ mSourcesCache.clear();
+ }));
+}
+
+void MediaSessionConduit::InsertAudioLevelForContributingSource(
+ const uint32_t aCsrcSource, const int64_t aTimestamp,
+ const uint32_t aRtpTimestamp, const bool aHasAudioLevel,
+ const uint8_t aAudioLevel) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mSourcesUpdateNeeded) {
+ OnSourcesUpdated();
+ }
+
+ dom::RTCRtpSourceEntry domEntry;
+ domEntry.mSource = aCsrcSource;
+ domEntry.mSourceType = dom::RTCRtpSourceEntryType::Contributing;
+ domEntry.mTimestamp = aTimestamp;
+ domEntry.mRtpTimestamp = aRtpTimestamp;
+ if (aHasAudioLevel) {
+ domEntry.mAudioLevel.Construct(rtpToDomAudioLevel(aAudioLevel));
+ }
+
+ auto now = GetTimestampMaker().GetNow();
+ webrtc::Timestamp convertedTimestamp =
+ now.ToRealtime() - webrtc::TimeDelta::Millis(now.ToDom() - aTimestamp);
+
+ SourceKey key(convertedTimestamp.ms<uint32_t>(), aCsrcSource);
+ mSourcesCache[key] = domEntry;
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/libwebrtcglue/MediaConduitInterface.h b/dom/media/webrtc/libwebrtcglue/MediaConduitInterface.h
new file mode 100644
index 0000000000..86e148dc5c
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/MediaConduitInterface.h
@@ -0,0 +1,495 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MEDIA_CONDUIT_ABSTRACTION_
+#define MEDIA_CONDUIT_ABSTRACTION_
+
+#include <vector>
+#include <functional>
+#include <map>
+
+#include "CodecConfig.h"
+#include "ImageContainer.h"
+#include "jsapi/RTCStatsReport.h"
+#include "MediaConduitErrors.h"
+#include "mozilla/media/MediaUtils.h"
+#include "mozilla/MozPromise.h"
+#include "WebrtcVideoCodecFactory.h"
+#include "nsTArray.h"
+#include "mozilla/dom/RTCRtpSourcesBinding.h"
+#include "PerformanceRecorder.h"
+#include "transport/mediapacket.h"
+#include "MediaConduitControl.h"
+
+// libwebrtc includes
+#include "api/audio/audio_frame.h"
+#include "api/call/transport.h"
+#include "api/rtp_headers.h"
+#include "api/rtp_parameters.h"
+#include "api/transport/rtp/rtp_source.h"
+#include "api/video/video_frame_buffer.h"
+#include "call/audio_receive_stream.h"
+#include "call/audio_send_stream.h"
+#include "call/call_basic_stats.h"
+#include "call/video_receive_stream.h"
+#include "call/video_send_stream.h"
+#include "rtc_base/copy_on_write_buffer.h"
+
+namespace webrtc {
+class RtpPacketReceived;
+class VideoFrame;
+} // namespace webrtc
+
+namespace mozilla {
+namespace dom {
+struct RTCRtpSourceEntry;
+}
+
+namespace dom {
+struct RTCRtpSourceEntry;
+}
+
+enum class MediaSessionConduitLocalDirection : int { kSend, kRecv };
+
+class VideoSessionConduit;
+class AudioSessionConduit;
+class WebrtcCallWrapper;
+
+/**
+ * 1. Abstract renderer for video data
+ * 2. This class acts as abstract interface between the video-engine and
+ * video-engine agnostic renderer implementation.
+ * 3. Concrete implementation of this interface is responsible for
+ * processing and/or rendering the obtained raw video frame to appropriate
+ * output , say, <video>
+ */
+class VideoRenderer {
+ protected:
+ virtual ~VideoRenderer() {}
+
+ public:
+ /**
+ * Callback Function reportng any change in the video-frame dimensions
+ * @param width: current width of the video @ decoder
+ * @param height: current height of the video @ decoder
+ */
+ virtual void FrameSizeChange(unsigned int width, unsigned int height) = 0;
+
+ /**
+ * Callback Function reporting decoded frame for processing.
+ * @param buffer: reference to decoded video frame
+ * @param buffer_size: size of the decoded frame
+ * @param time_stamp: Decoder timestamp, typically 90KHz as per RTP
+ * @render_time: Wall-clock time at the decoder for synchronization
+ * purposes in milliseconds
+ * NOTE: If decoded video frame is passed through buffer , it is the
+ * responsibility of the concrete implementations of this class to own copy
+ * of the frame if needed for time longer than scope of this callback.
+ * Such implementations should be quick in processing the frames and return
+ * immediately.
+ */
+ virtual void RenderVideoFrame(const webrtc::VideoFrameBuffer& buffer,
+ uint32_t time_stamp, int64_t render_time) = 0;
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(VideoRenderer)
+};
+
+/**
+ * Generic Interface for representing Audio/Video Session
+ * MediaSession conduit is identified by 2 main components
+ * 1. Attached Transport Interface (through events) for inbound and outbound RTP
+ * transport
+ * 2. Attached Renderer Interface for rendering media data off the network
+ * This class hides specifics of Media-Engine implementation from the consumers
+ * of this interface.
+ * Also provides codec configuration API for the media sent and recevied
+ */
+class MediaSessionConduit {
+ protected:
+ virtual ~MediaSessionConduit() {}
+
+ public:
+ enum Type { AUDIO, VIDEO };
+ enum class PacketType { RTP, RTCP };
+
+ static std::string LocalDirectionToString(
+ const MediaSessionConduitLocalDirection aDirection) {
+ return aDirection == MediaSessionConduitLocalDirection::kSend ? "send"
+ : "receive";
+ }
+
+ virtual Type type() const = 0;
+
+ // Call thread only
+ virtual Maybe<int> ActiveSendPayloadType() const = 0;
+ virtual Maybe<int> ActiveRecvPayloadType() const = 0;
+
+ // Whether transport is currently sending and receiving packets
+ virtual void SetTransportActive(bool aActive) = 0;
+
+ // Sending packets
+ virtual MediaEventSourceExc<MediaPacket>& SenderRtpSendEvent() = 0;
+ virtual MediaEventSourceExc<MediaPacket>& SenderRtcpSendEvent() = 0;
+ virtual MediaEventSourceExc<MediaPacket>& ReceiverRtcpSendEvent() = 0;
+
+ // Receiving packets...
+ // from an rtp-receiving pipeline
+ virtual void ConnectReceiverRtpEvent(
+ MediaEventSourceExc<webrtc::RtpPacketReceived, webrtc::RTPHeader>&
+ aEvent) = 0;
+ // from an rtp-receiving pipeline
+ virtual void ConnectReceiverRtcpEvent(
+ MediaEventSourceExc<MediaPacket>& aEvent) = 0;
+ // from an rtp-transmitting pipeline
+ virtual void ConnectSenderRtcpEvent(
+ MediaEventSourceExc<MediaPacket>& aEvent) = 0;
+
+ // Sts thread only.
+ virtual Maybe<uint16_t> RtpSendBaseSeqFor(uint32_t aSsrc) const = 0;
+
+ // Any thread.
+ virtual const dom::RTCStatsTimestampMaker& GetTimestampMaker() const = 0;
+
+ virtual Ssrcs GetLocalSSRCs() const = 0;
+
+ virtual Maybe<Ssrc> GetRemoteSSRC() const = 0;
+ virtual void UnsetRemoteSSRC(Ssrc aSsrc) = 0;
+
+ virtual void DisableSsrcChanges() = 0;
+
+ virtual bool HasCodecPluginID(uint64_t aPluginID) const = 0;
+
+ // Stuff for driving mute/unmute events
+ virtual MediaEventSource<void>& RtcpByeEvent() = 0;
+ virtual MediaEventSource<void>& RtcpTimeoutEvent() = 0;
+ virtual MediaEventSource<void>& RtpPacketEvent() = 0;
+
+ virtual bool SendRtp(const uint8_t* aData, size_t aLength,
+ const webrtc::PacketOptions& aOptions) = 0;
+ virtual bool SendSenderRtcp(const uint8_t* aData, size_t aLength) = 0;
+ virtual bool SendReceiverRtcp(const uint8_t* aData, size_t aLength) = 0;
+
+ virtual void DeliverPacket(rtc::CopyOnWriteBuffer packet,
+ PacketType type) = 0;
+
+ virtual RefPtr<GenericPromise> Shutdown() = 0;
+
+ virtual Maybe<RefPtr<AudioSessionConduit>> AsAudioSessionConduit() = 0;
+ virtual Maybe<RefPtr<VideoSessionConduit>> AsVideoSessionConduit() = 0;
+
+ virtual Maybe<webrtc::CallBasicStats> GetCallStats() const = 0;
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaSessionConduit)
+
+ void GetRtpSources(nsTArray<dom::RTCRtpSourceEntry>& outSources) const;
+
+ virtual void SetJitterBufferTarget(DOMHighResTimeStamp aTargetMs) = 0;
+
+ // test-only: inserts fake CSRCs and audio level data.
+ // NB: fake data is only valid during the current main thread task.
+ void InsertAudioLevelForContributingSource(const uint32_t aCsrcSource,
+ const int64_t aTimestamp,
+ const uint32_t aRtpTimestamp,
+ const bool aHasAudioLevel,
+ const uint8_t aAudioLevel);
+
+ protected:
+ virtual std::vector<webrtc::RtpSource> GetUpstreamRtpSources() const = 0;
+
+ private:
+ void UpdateRtpSources(const std::vector<webrtc::RtpSource>& aSources) const;
+
+ // Marks the cache as having been updated in the current task, and keeps it
+ // stable until the current task is finished.
+ void OnSourcesUpdated() const;
+
+ // Accessed only on main thread. This exists for a couple of reasons:
+ // 1. The webrtc spec says that source stats are updated using a queued task;
+ // libwebrtc's internal representation of these stats is updated without
+ // any task queueing, which means we need a mainthread-only cache.
+ // 2. libwebrtc uses its own clock that is not consistent with the one we
+ // need to use for stats (the so-called JS timestamps), which means we need
+ // to adjust the timestamps. Since timestamp adjustment is inexact and will
+ // not necessarily yield exactly the same result if performed again later, we
+ // need to avoid performing it more than once for each entry, which means we
+ // need to remember both the JS timestamp (in dom::RTCRtpSourceEntry) and the
+ // libwebrtc timestamp (in SourceKey::mLibwebrtcTimestampMs).
+ class SourceKey {
+ public:
+ explicit SourceKey(const webrtc::RtpSource& aSource)
+ : SourceKey(aSource.timestamp_ms(), aSource.source_id()) {}
+
+ SourceKey(uint32_t aTimestamp, uint32_t aSrc)
+ : mLibwebrtcTimestampMs(aTimestamp), mSrc(aSrc) {}
+
+ // TODO: Once we support = default for this in our toolchain, do so
+ auto operator>(const SourceKey& aRhs) const {
+ if (mLibwebrtcTimestampMs == aRhs.mLibwebrtcTimestampMs) {
+ return mSrc > aRhs.mSrc;
+ }
+ return mLibwebrtcTimestampMs > aRhs.mLibwebrtcTimestampMs;
+ }
+
+ private:
+ uint32_t mLibwebrtcTimestampMs;
+ uint32_t mSrc;
+ };
+ mutable std::map<SourceKey, dom::RTCRtpSourceEntry, std::greater<SourceKey>>
+ mSourcesCache;
+ // Accessed only on main thread. A flag saying whether mSourcesCache needs
+ // updating. Ensures that get*Sources() appear stable from javascript
+ // throughout a main thread task, even though we don't follow the spec to the
+ // letter (dispatch a task to update the sources).
+ mutable bool mSourcesUpdateNeeded = true;
+};
+
+class WebrtcSendTransport : public webrtc::Transport {
+ // WeakRef to the owning conduit
+ MediaSessionConduit* mConduit;
+
+ public:
+ explicit WebrtcSendTransport(MediaSessionConduit* aConduit)
+ : mConduit(aConduit) {}
+ bool SendRtp(const uint8_t* aPacket, size_t aLength,
+ const webrtc::PacketOptions& aOptions) override {
+ return mConduit->SendRtp(aPacket, aLength, aOptions);
+ }
+ bool SendRtcp(const uint8_t* aPacket, size_t aLength) override {
+ return mConduit->SendSenderRtcp(aPacket, aLength);
+ }
+};
+
+class WebrtcReceiveTransport : public webrtc::Transport {
+ // WeakRef to the owning conduit
+ MediaSessionConduit* mConduit;
+
+ public:
+ explicit WebrtcReceiveTransport(MediaSessionConduit* aConduit)
+ : mConduit(aConduit) {}
+ bool SendRtp(const uint8_t* aPacket, size_t aLength,
+ const webrtc::PacketOptions& aOptions) override {
+ MOZ_CRASH("Unexpected RTP packet");
+ }
+ bool SendRtcp(const uint8_t* aPacket, size_t aLength) override {
+ return mConduit->SendReceiverRtcp(aPacket, aLength);
+ }
+};
+
+// Abstract base classes for external encoder/decoder.
+
+// Interface to help signal PluginIDs
+class CodecPluginID {
+ public:
+ virtual MediaEventSource<uint64_t>* InitPluginEvent() { return nullptr; }
+ virtual MediaEventSource<uint64_t>* ReleasePluginEvent() { return nullptr; }
+ virtual ~CodecPluginID() {}
+};
+
+class VideoEncoder : public CodecPluginID {
+ public:
+ virtual ~VideoEncoder() {}
+};
+
+class VideoDecoder : public CodecPluginID {
+ public:
+ virtual ~VideoDecoder() {}
+};
+
+/**
+ * MediaSessionConduit for video
+ * Refer to the comments on MediaSessionConduit above for overall
+ * information
+ */
+class VideoSessionConduit : public MediaSessionConduit {
+ public:
+ struct Options {
+ bool mVideoLatencyTestEnable = false;
+ // All in bps.
+ int mMinBitrate = 0;
+ int mStartBitrate = 0;
+ int mPrefMaxBitrate = 0;
+ int mMinBitrateEstimate = 0;
+ bool mDenoising = false;
+ bool mLockScaling = false;
+ uint8_t mSpatialLayers = 1;
+ uint8_t mTemporalLayers = 1;
+ };
+
+ /**
+ * Factory function to create and initialize a Video Conduit Session
+ * @param webrtc::Call instance shared by paired audio and video
+ * media conduits
+ * @param aOptions are a number of options, typically from prefs, used to
+ * configure the created VideoConduit.
+ * @param aPCHandle is a string representing the RTCPeerConnection that is
+ * creating this VideoConduit. This is used when reporting GMP plugin
+ * crashes.
+ * @result Concrete VideoSessionConduitObject or nullptr in the case
+ * of failure
+ */
+ static RefPtr<VideoSessionConduit> Create(
+ RefPtr<WebrtcCallWrapper> aCall,
+ nsCOMPtr<nsISerialEventTarget> aStsThread, Options aOptions,
+ std::string aPCHandle, const TrackingId& aRecvTrackingId);
+
+ enum FrameRequestType {
+ FrameRequestNone,
+ FrameRequestFir,
+ FrameRequestPli,
+ FrameRequestUnknown
+ };
+
+ VideoSessionConduit()
+ : mFrameRequestMethod(FrameRequestNone),
+ mUsingNackBasic(false),
+ mUsingTmmbr(false),
+ mUsingFEC(false) {}
+
+ virtual ~VideoSessionConduit() {}
+
+ Type type() const override { return VIDEO; }
+
+ Maybe<RefPtr<AudioSessionConduit>> AsAudioSessionConduit() override {
+ return Nothing();
+ }
+
+ Maybe<RefPtr<VideoSessionConduit>> AsVideoSessionConduit() override {
+ return Some(RefPtr<VideoSessionConduit>(this));
+ }
+
+ /**
+ * Hooks up mControl Mirrors with aControl Canonicals, and sets up
+ * mWatchManager to react on Mirror changes.
+ */
+ virtual void InitControl(VideoConduitControlInterface* aControl) = 0;
+
+ /**
+ * Function to attach Renderer end-point of the Media-Video conduit.
+ * @param aRenderer : Reference to the concrete Video renderer implementation
+ * Note: Multiple invocations of this API shall remove an existing renderer
+ * and attaches the new to the Conduit.
+ */
+ virtual MediaConduitErrorCode AttachRenderer(
+ RefPtr<mozilla::VideoRenderer> aRenderer) = 0;
+ virtual void DetachRenderer() = 0;
+
+ /**
+ * Function to deliver a capture video frame for encoding and transport.
+ * If the frame's timestamp is 0, it will be automatcally generated.
+ *
+ * NOTE: ConfigureSendMediaCodec() must be called before this function can
+ * be invoked. This ensures the inserted video-frames can be
+ * transmitted by the conduit.
+ */
+ virtual MediaConduitErrorCode SendVideoFrame(webrtc::VideoFrame aFrame) = 0;
+
+ /**
+ * These methods allow unit tests to double-check that the
+ * rtcp-fb settings are as expected.
+ */
+ FrameRequestType FrameRequestMethod() const { return mFrameRequestMethod; }
+
+ bool UsingNackBasic() const { return mUsingNackBasic; }
+
+ bool UsingTmmbr() const { return mUsingTmmbr; }
+
+ bool UsingFEC() const { return mUsingFEC; }
+
+ virtual Maybe<webrtc::VideoReceiveStreamInterface::Stats> GetReceiverStats()
+ const = 0;
+ virtual Maybe<webrtc::VideoSendStream::Stats> GetSenderStats() const = 0;
+
+ virtual void CollectTelemetryData() = 0;
+
+ virtual bool AddFrameHistory(
+ dom::Sequence<dom::RTCVideoFrameHistoryInternal>* outHistories) const = 0;
+
+ virtual Maybe<Ssrc> GetAssociatedLocalRtxSSRC(Ssrc aSsrc) const = 0;
+
+ protected:
+ /* RTCP feedback settings, for unit testing purposes */
+ FrameRequestType mFrameRequestMethod;
+ bool mUsingNackBasic;
+ bool mUsingTmmbr;
+ bool mUsingFEC;
+};
+
+/**
+ * MediaSessionConduit for audio
+ * Refer to the comments on MediaSessionConduit above for overall
+ * information
+ */
+class AudioSessionConduit : public MediaSessionConduit {
+ public:
+ /**
+ * Factory function to create and initialize an Audio Conduit Session
+ * @param webrtc::Call instance shared by paired audio and video
+ * media conduits
+ * @result Concrete AudioSessionConduitObject or nullptr in the case
+ * of failure
+ */
+ static RefPtr<AudioSessionConduit> Create(
+ RefPtr<WebrtcCallWrapper> aCall,
+ nsCOMPtr<nsISerialEventTarget> aStsThread);
+
+ virtual ~AudioSessionConduit() {}
+
+ Type type() const override { return AUDIO; }
+
+ Maybe<RefPtr<AudioSessionConduit>> AsAudioSessionConduit() override {
+ return Some(this);
+ }
+
+ Maybe<RefPtr<VideoSessionConduit>> AsVideoSessionConduit() override {
+ return Nothing();
+ }
+
+ /**
+ * Hooks up mControl Mirrors with aControl Canonicals, and sets up
+ * mWatchManager to react on Mirror changes.
+ */
+ virtual void InitControl(AudioConduitControlInterface* aControl) = 0;
+
+ /**
+ * Function to deliver externally captured audio sample for encoding and
+ * transport
+ * @param frame [in]: AudioFrame in upstream's format for forwarding to the
+ * send stream. Ownership is passed along.
+ * NOTE: ConfigureSendMediaCodec() SHOULD be called before this function can
+ * be invoked. This ensures the inserted audio-samples can be transmitted by
+ * the conduit.
+ */
+ virtual MediaConduitErrorCode SendAudioFrame(
+ std::unique_ptr<webrtc::AudioFrame> frame) = 0;
+
+ /**
+ * Function to grab a decoded audio-sample from the media engine for
+ * rendering / playout of length 10 milliseconds.
+ *
+ * @param samplingFreqHz [in]: Frequency of the sampling for playback in
+ * Hertz (16000, 32000,..)
+ * @param frame [in/out]: Pointer to an AudioFrame to which audio data will be
+ * copied
+ * NOTE: This function should be invoked every 10 milliseconds for the best
+ * performance
+ * NOTE: ConfigureRecvMediaCodec() SHOULD be called before this function can
+ * be invoked
+ * This ensures the decoded samples are ready for reading and playout is
+ * enabled.
+ */
+ virtual MediaConduitErrorCode GetAudioFrame(int32_t samplingFreqHz,
+ webrtc::AudioFrame* frame) = 0;
+
+ /**
+ * Checks if given sampling frequency is supported
+ * @param freq: Sampling rate (in Hz) to check
+ */
+ virtual bool IsSamplingFreqSupported(int freq) const = 0;
+
+ virtual Maybe<webrtc::AudioReceiveStreamInterface::Stats> GetReceiverStats()
+ const = 0;
+ virtual Maybe<webrtc::AudioSendStream::Stats> GetSenderStats() const = 0;
+};
+} // namespace mozilla
+#endif
diff --git a/dom/media/webrtc/libwebrtcglue/MediaDataCodec.cpp b/dom/media/webrtc/libwebrtcglue/MediaDataCodec.cpp
new file mode 100644
index 0000000000..895fcdc432
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/MediaDataCodec.cpp
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaDataCodec.h"
+
+#include "PDMFactory.h"
+#include "WebrtcGmpVideoCodec.h"
+#include "WebrtcMediaDataDecoderCodec.h"
+#include "WebrtcMediaDataEncoderCodec.h"
+#include "mozilla/StaticPrefs_media.h"
+
+namespace mozilla {
+
+/* static */
+WebrtcVideoEncoder* MediaDataCodec::CreateEncoder(
+ const webrtc::SdpVideoFormat& aFormat) {
+ if (!StaticPrefs::media_webrtc_platformencoder()) {
+ return nullptr;
+ }
+ if (!WebrtcMediaDataEncoder::CanCreate(
+ webrtc::PayloadStringToCodecType(aFormat.name))) {
+ return nullptr;
+ }
+
+ return new WebrtcVideoEncoderProxy(new WebrtcMediaDataEncoder(aFormat));
+}
+
+/* static */
+WebrtcVideoDecoder* MediaDataCodec::CreateDecoder(
+ webrtc::VideoCodecType aCodecType, TrackingId aTrackingId) {
+ switch (aCodecType) {
+ case webrtc::VideoCodecType::kVideoCodecVP8:
+ case webrtc::VideoCodecType::kVideoCodecVP9:
+ if (!StaticPrefs::media_navigator_mediadatadecoder_vpx_enabled()) {
+ return nullptr;
+ }
+ break;
+ case webrtc::VideoCodecType::kVideoCodecH264:
+ if (!StaticPrefs::media_navigator_mediadatadecoder_h264_enabled()) {
+ return nullptr;
+ }
+ break;
+ default:
+ return nullptr;
+ }
+
+ nsAutoCString codec;
+ switch (aCodecType) {
+ case webrtc::VideoCodecType::kVideoCodecVP8:
+ codec = "video/vp8";
+ break;
+ case webrtc::VideoCodecType::kVideoCodecVP9:
+ codec = "video/vp9";
+ break;
+ case webrtc::VideoCodecType::kVideoCodecH264:
+ codec = "video/avc";
+ break;
+ default:
+ return nullptr;
+ }
+ RefPtr<PDMFactory> pdm = new PDMFactory();
+ if (pdm->SupportsMimeType(codec) == media::DecodeSupport::Unsupported) {
+ return nullptr;
+ }
+
+ return new WebrtcMediaDataDecoder(codec, aTrackingId);
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/libwebrtcglue/MediaDataCodec.h b/dom/media/webrtc/libwebrtcglue/MediaDataCodec.h
new file mode 100644
index 0000000000..b885d6ae0c
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/MediaDataCodec.h
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MEDIA_DATA_CODEC_H_
+#define MEDIA_DATA_CODEC_H_
+
+#include "MediaConduitInterface.h"
+
+namespace mozilla {
+
+class WebrtcVideoDecoder;
+class WebrtcVideoEncoder;
+class MediaDataCodec {
+ public:
+ /**
+ * Create encoder object for codec format |aFormat|. Return |nullptr| when
+ * failed.
+ */
+ static WebrtcVideoEncoder* CreateEncoder(
+ const webrtc::SdpVideoFormat& aFormat);
+
+ /**
+ * Create decoder object for codec type |aCodecType|. Return |nullptr| when
+ * failed.
+ */
+ static WebrtcVideoDecoder* CreateDecoder(webrtc::VideoCodecType aCodecType,
+ TrackingId aTrackingId);
+};
+} // namespace mozilla
+
+#endif // MEDIA_DATA_CODEC_H_
diff --git a/dom/media/webrtc/libwebrtcglue/RtpRtcpConfig.h b/dom/media/webrtc/libwebrtcglue/RtpRtcpConfig.h
new file mode 100644
index 0000000000..03a774ec3b
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/RtpRtcpConfig.h
@@ -0,0 +1,24 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef __RTPRTCP_CONFIG_H__
+#define __RTPRTCP_CONFIG_H__
+#include "api/rtp_headers.h"
+
+namespace mozilla {
+class RtpRtcpConfig {
+ public:
+ RtpRtcpConfig() = delete;
+ explicit RtpRtcpConfig(const webrtc::RtcpMode aMode) : mRtcpMode(aMode) {}
+ webrtc::RtcpMode GetRtcpMode() const { return mRtcpMode; }
+
+ bool operator==(const RtpRtcpConfig& aOther) const {
+ return mRtcpMode == aOther.mRtcpMode;
+ }
+
+ private:
+ webrtc::RtcpMode mRtcpMode;
+};
+} // namespace mozilla
+#endif
diff --git a/dom/media/webrtc/libwebrtcglue/RunningStat.h b/dom/media/webrtc/libwebrtcglue/RunningStat.h
new file mode 100644
index 0000000000..7a0e88f193
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/RunningStat.h
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+/* Adapted from "Accurately computing running variance - John D. Cook"
+ http://www.johndcook.com/standard_deviation.html */
+
+#ifndef RUNNING_STAT_H_
+#define RUNNING_STAT_H_
+#include <math.h>
+
+namespace mozilla {
+
+class RunningStat {
+ public:
+ RunningStat() : mN(0), mOldM(0.0), mNewM(0.0), mOldS(0.0), mNewS(0.0) {}
+
+ void Clear() { mN = 0; }
+
+ void Push(double x) {
+ mN++;
+
+ // See Knuth TAOCP vol 2, 3rd edition, page 232
+ if (mN == 1) {
+ mOldM = mNewM = x;
+ mOldS = 0.0;
+ } else {
+ mNewM = mOldM + (x - mOldM) / mN;
+ mNewS = mOldS + (x - mOldM) * (x - mNewM);
+
+ // set up for next iteration
+ mOldM = mNewM;
+ mOldS = mNewS;
+ }
+ }
+
+ int NumDataValues() const { return mN; }
+
+ double Mean() const { return (mN > 0) ? mNewM : 0.0; }
+
+ double Variance() const { return (mN > 1) ? mNewS / (mN - 1) : 0.0; }
+
+ double StandardDeviation() const { return sqrt(Variance()); }
+
+ private:
+ int mN;
+ double mOldM, mNewM, mOldS, mNewS;
+};
+} // namespace mozilla
+#endif // RUNNING_STAT_H_
diff --git a/dom/media/webrtc/libwebrtcglue/SystemTime.cpp b/dom/media/webrtc/libwebrtcglue/SystemTime.cpp
new file mode 100644
index 0000000000..bba7fd788e
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/SystemTime.cpp
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SystemTime.h"
+
+#include "TimeUnits.h"
+
+namespace mozilla {
+
+// webrtc::Timestamp may not be negative. `now-base` for the first call to
+// WebrtcSystemTime() is always 0, which makes it impossible for libwebrtc
+// code to calculate a timestamp older than the first one returned. This
+// offset makes sure the clock starts at a value equivalent to roughly 4.5h.
+static constexpr webrtc::TimeDelta kWebrtcTimeOffset =
+ webrtc::TimeDelta::Micros(0x10000000);
+
+RTCStatsTimestampMakerRealtimeClock::RTCStatsTimestampMakerRealtimeClock(
+ const dom::RTCStatsTimestampMaker& aTimestampMaker)
+ : mTimestampMaker(aTimestampMaker) {}
+
+webrtc::Timestamp RTCStatsTimestampMakerRealtimeClock::CurrentTime() {
+ return mTimestampMaker.GetNow().ToRealtime();
+}
+
+webrtc::NtpTime RTCStatsTimestampMakerRealtimeClock::ConvertTimestampToNtpTime(
+ webrtc::Timestamp aRealtime) {
+ return CreateNtp(
+ dom::RTCStatsTimestamp::FromRealtime(mTimestampMaker, aRealtime).ToNtp());
+}
+
+TimeStamp WebrtcSystemTimeBase() {
+ static TimeStamp now = TimeStamp::Now();
+ return now;
+}
+
+webrtc::Timestamp WebrtcSystemTime() {
+ const TimeStamp base = WebrtcSystemTimeBase();
+ const TimeStamp now = TimeStamp::Now();
+ return webrtc::Timestamp::Micros((now - base).ToMicroseconds()) +
+ kWebrtcTimeOffset;
+}
+
+webrtc::NtpTime CreateNtp(webrtc::Timestamp aTime) {
+ const int64_t timeNtpUs = aTime.us();
+ const uint32_t seconds = static_cast<uint32_t>(timeNtpUs / USECS_PER_S);
+
+ constexpr int64_t fractionsPerSec = 1LL << 32;
+ const int64_t fractionsUs = timeNtpUs % USECS_PER_S;
+ const uint32_t fractions = (fractionsUs * fractionsPerSec) / USECS_PER_S;
+
+ return webrtc::NtpTime(seconds, fractions);
+}
+} // namespace mozilla
+
+namespace rtc {
+int64_t SystemTimeNanos() { return mozilla::WebrtcSystemTime().us() * 1000; }
+} // namespace rtc
diff --git a/dom/media/webrtc/libwebrtcglue/SystemTime.h b/dom/media/webrtc/libwebrtcglue/SystemTime.h
new file mode 100644
index 0000000000..5c042e689f
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/SystemTime.h
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_WEBRTC_LIBWEBRTCGLUE_SYSTEMTIMENANOS_H_
+#define DOM_MEDIA_WEBRTC_LIBWEBRTCGLUE_SYSTEMTIMENANOS_H_
+
+#include "jsapi/RTCStatsReport.h"
+#include "mozilla/TimeStamp.h"
+#include "system_wrappers/include/clock.h"
+
+namespace mozilla {
+class RTCStatsTimestampMakerRealtimeClock : public webrtc::Clock {
+ public:
+ explicit RTCStatsTimestampMakerRealtimeClock(
+ const dom::RTCStatsTimestampMaker& aTimestampMaker);
+
+ webrtc::Timestamp CurrentTime() override;
+
+ // Upstream, this method depend on rtc::TimeUTCMicros for converting the
+ // monotonic system clock to Ntp, if only for the first call when deciding the
+ // Ntp offset.
+ // We override this to be able to use our own clock instead of
+ // rtc::TimeUTCMicros for ntp timestamps.
+ webrtc::NtpTime ConvertTimestampToNtpTime(
+ webrtc::Timestamp aRealtime) override;
+
+ const dom::RTCStatsTimestampMaker mTimestampMaker;
+};
+
+// The time base used for WebrtcSystemTime(). Completely arbitrary. Constant.
+TimeStamp WebrtcSystemTimeBase();
+
+// The returned timestamp denotes the monotonic time passed since
+// WebrtcSystemTimeBase(). Libwebrtc uses this to track how time advances from a
+// specific point in time. It adds an offset to make the timestamps absolute.
+webrtc::Timestamp WebrtcSystemTime();
+
+webrtc::NtpTime CreateNtp(webrtc::Timestamp aTime);
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/libwebrtcglue/TaskQueueWrapper.h b/dom/media/webrtc/libwebrtcglue/TaskQueueWrapper.h
new file mode 100644
index 0000000000..da11bfdf8b
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/TaskQueueWrapper.h
@@ -0,0 +1,182 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_WEBRTC_LIBWEBRTCGLUE_TASKQUEUEWRAPPER_H_
+#define DOM_MEDIA_WEBRTC_LIBWEBRTCGLUE_TASKQUEUEWRAPPER_H_
+
+#include "api/task_queue/task_queue_factory.h"
+#include "mozilla/DataMutex.h"
+#include "mozilla/RecursiveMutex.h"
+#include "mozilla/ProfilerRunnable.h"
+#include "mozilla/TaskQueue.h"
+#include "VideoUtils.h"
+#include "mozilla/media/MediaUtils.h" // For media::Await
+
+namespace mozilla {
+
+enum class DeletionPolicy : uint8_t { Blocking, NonBlocking };
+
+/**
+ * A wrapper around Mozilla TaskQueues in the shape of a libwebrtc TaskQueue.
+ *
+ * Allows libwebrtc to use Mozilla threads where tooling, e.g. profiling, is set
+ * up and just works.
+ *
+ * Mozilla APIs like Runnables, MozPromise, etc. can also be used with the
+ * wrapped TaskQueue to run things on the right thread when interacting with
+ * libwebrtc.
+ */
+template <DeletionPolicy Deletion>
+class TaskQueueWrapper : public webrtc::TaskQueueBase {
+ public:
+ TaskQueueWrapper(RefPtr<TaskQueue> aTaskQueue, nsCString aName)
+ : mTaskQueue(std::move(aTaskQueue)), mName(std::move(aName)) {}
+ ~TaskQueueWrapper() = default;
+
+ void Delete() override {
+ {
+ // Scope this to make sure it does not race against the promise chain we
+ // set up below.
+ auto hasShutdown = mHasShutdown.Lock();
+ *hasShutdown = true;
+ }
+
+ MOZ_RELEASE_ASSERT(Deletion == DeletionPolicy::NonBlocking ||
+ !mTaskQueue->IsOnCurrentThread());
+
+ nsCOMPtr<nsISerialEventTarget> backgroundTaskQueue;
+ NS_CreateBackgroundTaskQueue(__func__, getter_AddRefs(backgroundTaskQueue));
+ if (NS_WARN_IF(!backgroundTaskQueue)) {
+ // Ok... that's pretty broken. Try main instead.
+ MOZ_ASSERT(false);
+ backgroundTaskQueue = GetMainThreadSerialEventTarget();
+ }
+
+ RefPtr<GenericPromise> shutdownPromise = mTaskQueue->BeginShutdown()->Then(
+ backgroundTaskQueue, __func__, [this] {
+ // Wait until shutdown is complete, then delete for real. Although we
+ // prevent queued tasks from executing with mHasShutdown, that is a
+ // member variable, which means we still need to ensure that the
+ // queue is done executing tasks before destroying it.
+ delete this;
+ return GenericPromise::CreateAndResolve(true, __func__);
+ });
+ if constexpr (Deletion == DeletionPolicy::Blocking) {
+ media::Await(backgroundTaskQueue.forget(), shutdownPromise);
+ } else {
+ Unused << shutdownPromise;
+ }
+ }
+
+ already_AddRefed<Runnable> CreateTaskRunner(
+ absl::AnyInvocable<void() &&> aTask) {
+ return NS_NewRunnableFunction(
+ "TaskQueueWrapper::CreateTaskRunner",
+ [this, task = std::move(aTask),
+ name = nsPrintfCString("TQ %s: webrtc::QueuedTask",
+ mName.get())]() mutable {
+ CurrentTaskQueueSetter current(this);
+ auto hasShutdown = mHasShutdown.Lock();
+ if (*hasShutdown) {
+ return;
+ }
+ AUTO_PROFILE_FOLLOWING_RUNNABLE(name);
+ std::move(task)();
+ });
+ }
+
+ already_AddRefed<Runnable> CreateTaskRunner(nsCOMPtr<nsIRunnable> aRunnable) {
+ return NS_NewRunnableFunction(
+ "TaskQueueWrapper::CreateTaskRunner",
+ [this, runnable = std::move(aRunnable)]() mutable {
+ CurrentTaskQueueSetter current(this);
+ auto hasShutdown = mHasShutdown.Lock();
+ if (*hasShutdown) {
+ return;
+ }
+ AUTO_PROFILE_FOLLOWING_RUNNABLE(runnable);
+ runnable->Run();
+ });
+ }
+
+ void PostTask(absl::AnyInvocable<void() &&> aTask) override {
+ MOZ_ALWAYS_SUCCEEDS(
+ mTaskQueue->Dispatch(CreateTaskRunner(std::move(aTask))));
+ }
+
+ void PostDelayedTask(absl::AnyInvocable<void() &&> aTask,
+ webrtc::TimeDelta aDelay) override {
+ if (aDelay.ms() == 0) {
+ // AbstractThread::DelayedDispatch doesn't support delay 0
+ PostTask(std::move(aTask));
+ return;
+ }
+ MOZ_ALWAYS_SUCCEEDS(mTaskQueue->DelayedDispatch(
+ CreateTaskRunner(std::move(aTask)), aDelay.ms()));
+ }
+
+ void PostDelayedHighPrecisionTask(absl::AnyInvocable<void() &&> aTask,
+ webrtc::TimeDelta aDelay) override {
+ PostDelayedTask(std::move(aTask), aDelay);
+ }
+
+ const RefPtr<TaskQueue> mTaskQueue;
+ const nsCString mName;
+
+ // This is a recursive mutex because a TaskRunner holding this mutex while
+ // running its runnable may end up running other - tail dispatched - runnables
+ // too, and they'll again try to grab the mutex.
+ // The mutex must be held while running the runnable since otherwise there'd
+ // be a race between shutting down the underlying task queue and the runnable
+ // dispatching to that task queue (and we assert it succeeds in e.g.,
+ // PostTask()).
+ DataMutexBase<bool, RecursiveMutex> mHasShutdown{
+ false, "TaskQueueWrapper::mHasShutdown"};
+};
+
+template <DeletionPolicy Deletion>
+class DefaultDelete<TaskQueueWrapper<Deletion>>
+ : public webrtc::TaskQueueDeleter {
+ public:
+ void operator()(TaskQueueWrapper<Deletion>* aPtr) const {
+ webrtc::TaskQueueDeleter::operator()(aPtr);
+ }
+};
+
+class SharedThreadPoolWebRtcTaskQueueFactory : public webrtc::TaskQueueFactory {
+ public:
+ SharedThreadPoolWebRtcTaskQueueFactory() {}
+
+ template <DeletionPolicy Deletion>
+ UniquePtr<TaskQueueWrapper<Deletion>> CreateTaskQueueWrapper(
+ absl::string_view aName, bool aSupportTailDispatch, Priority aPriority,
+ MediaThreadType aThreadType = MediaThreadType::WEBRTC_WORKER) const {
+ // XXX Do something with aPriority
+ nsCString name(aName.data(), aName.size());
+ auto taskQueue = TaskQueue::Create(GetMediaThreadPool(aThreadType),
+ name.get(), aSupportTailDispatch);
+ return MakeUnique<TaskQueueWrapper<Deletion>>(std::move(taskQueue),
+ std::move(name));
+ }
+
+ std::unique_ptr<webrtc::TaskQueueBase, webrtc::TaskQueueDeleter>
+ CreateTaskQueue(absl::string_view aName, Priority aPriority) const override {
+ // libwebrtc will dispatch some tasks sync, i.e., block the origin thread
+ // until they've run, and that doesn't play nice with tail dispatching since
+ // there will never be a tail.
+ // DeletionPolicy::Blocking because this is for libwebrtc use and that's
+ // what they expect.
+ constexpr bool supportTailDispatch = false;
+ return std::unique_ptr<webrtc::TaskQueueBase, webrtc::TaskQueueDeleter>(
+ CreateTaskQueueWrapper<DeletionPolicy::Blocking>(
+ std::move(aName), supportTailDispatch, aPriority)
+ .release(),
+ webrtc::TaskQueueDeleter());
+ }
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/libwebrtcglue/VideoConduit.cpp b/dom/media/webrtc/libwebrtcglue/VideoConduit.cpp
new file mode 100644
index 0000000000..f99f818d48
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/VideoConduit.cpp
@@ -0,0 +1,1902 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "VideoConduit.h"
+
+#include <algorithm>
+#include <cinttypes>
+#include <cmath>
+
+#include "common/browser_logging/CSFLog.h"
+#include "common/YuvStamper.h"
+#include "GmpVideoCodec.h"
+#include "MediaConduitControl.h"
+#include "MediaDataCodec.h"
+#include "mozilla/dom/RTCRtpSourcesBinding.h"
+#include "mozilla/media/MediaUtils.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/TemplateLib.h"
+#include "nsIGfxInfo.h"
+#include "nsIPrefBranch.h"
+#include "nsIPrefService.h"
+#include "nsServiceManagerUtils.h"
+#include "RtpRtcpConfig.h"
+#include "transport/SrtpFlow.h" // For SRTP_MAX_EXPANSION
+#include "Tracing.h"
+#include "VideoStreamFactory.h"
+#include "WebrtcCallWrapper.h"
+#include "WebrtcGmpVideoCodec.h"
+
+// libwebrtc includes
+#include "api/transport/bitrate_settings.h"
+#include "api/video_codecs/h264_profile_level_id.h"
+#include "api/video_codecs/sdp_video_format.h"
+#include "api/video_codecs/video_codec.h"
+#include "media/base/media_constants.h"
+#include "media/engine/encoder_simulcast_proxy.h"
+#include "modules/rtp_rtcp/include/rtp_rtcp_defines.h"
+#include "modules/video_coding/codecs/vp8/include/vp8.h"
+#include "modules/video_coding/codecs/vp9/include/vp9.h"
+
+#ifdef MOZ_WIDGET_ANDROID
+# include "VideoEngine.h"
+#endif
+
+// for ntohs
+#ifdef _MSC_VER
+# include "Winsock2.h"
+#else
+# include <netinet/in.h>
+#endif
+
+#define INVALID_RTP_PAYLOAD 255 // valid payload types are 0 to 127
+
+namespace mozilla {
+
+namespace {
+
+const char* vcLogTag = "WebrtcVideoSessionConduit";
+#ifdef LOGTAG
+# undef LOGTAG
+#endif
+#define LOGTAG vcLogTag
+
+using namespace cricket;
+using LocalDirection = MediaSessionConduitLocalDirection;
+
+const int kNullPayloadType = -1;
+const char kRtcpFbCcmParamTmmbr[] = "tmmbr";
+
+// The number of frame buffers WebrtcVideoConduit may create before returning
+// errors.
+// Sometimes these are released synchronously but they can be forwarded all the
+// way to the encoder for asynchronous encoding. With a pool size of 5,
+// we allow 1 buffer for the current conversion, and 4 buffers to be queued at
+// the encoder.
+#define SCALER_BUFFER_POOL_SIZE 5
+
+// The pixel alignment to use for the highest resolution layer when simulcast
+// is active and one or more layers are being scaled.
+#define SIMULCAST_RESOLUTION_ALIGNMENT 16
+
+template <class t>
+void ConstrainPreservingAspectRatioExact(uint32_t max_fs, t* width, t* height) {
+ // We could try to pick a better starting divisor, but it won't make any real
+ // performance difference.
+ for (size_t d = 1; d < std::min(*width, *height); ++d) {
+ if ((*width % d) || (*height % d)) {
+ continue; // Not divisible
+ }
+
+ if (((*width) * (*height)) / (d * d) <= max_fs) {
+ *width /= d;
+ *height /= d;
+ return;
+ }
+ }
+
+ *width = 0;
+ *height = 0;
+}
+
+/**
+ * Perform validation on the codecConfig to be applied
+ */
+MediaConduitErrorCode ValidateCodecConfig(const VideoCodecConfig& codecInfo) {
+ if (codecInfo.mName.empty()) {
+ CSFLogError(LOGTAG, "%s Empty Payload Name ", __FUNCTION__);
+ return kMediaConduitMalformedArgument;
+ }
+
+ return kMediaConduitNoError;
+}
+
+webrtc::VideoCodecType SupportedCodecType(webrtc::VideoCodecType aType) {
+ switch (aType) {
+ case webrtc::VideoCodecType::kVideoCodecVP8:
+ case webrtc::VideoCodecType::kVideoCodecVP9:
+ case webrtc::VideoCodecType::kVideoCodecH264:
+ return aType;
+ default:
+ return webrtc::VideoCodecType::kVideoCodecGeneric;
+ }
+ // NOTREACHED
+}
+
+// Call thread only.
+rtc::scoped_refptr<webrtc::VideoEncoderConfig::EncoderSpecificSettings>
+ConfigureVideoEncoderSettings(const VideoCodecConfig& aConfig,
+ const WebrtcVideoConduit* aConduit,
+ webrtc::SdpVideoFormat::Parameters& aParameters) {
+ bool is_screencast =
+ aConduit->CodecMode() == webrtc::VideoCodecMode::kScreensharing;
+ // No automatic resizing when using simulcast or screencast.
+ bool automatic_resize = !is_screencast && aConfig.mEncodings.size() <= 1;
+ bool denoising;
+ bool codec_default_denoising = false;
+ if (is_screencast) {
+ denoising = false;
+ } else {
+ // Use codec default if video_noise_reduction is unset.
+ denoising = aConduit->Denoising();
+ codec_default_denoising = !denoising;
+ }
+
+ if (aConfig.mName == kH264CodecName) {
+ aParameters[kH264FmtpPacketizationMode] =
+ std::to_string(aConfig.mPacketizationMode);
+ {
+ std::stringstream ss;
+ ss << std::hex << std::setfill('0');
+ ss << std::setw(2) << static_cast<uint32_t>(aConfig.mProfile);
+ ss << std::setw(2) << static_cast<uint32_t>(aConfig.mConstraints);
+ ss << std::setw(2) << static_cast<uint32_t>(aConfig.mLevel);
+ std::string profileLevelId = ss.str();
+ auto parsedProfileLevelId =
+ webrtc::ParseH264ProfileLevelId(profileLevelId.c_str());
+ MOZ_DIAGNOSTIC_ASSERT(parsedProfileLevelId);
+ if (parsedProfileLevelId) {
+ aParameters[kH264FmtpProfileLevelId] = profileLevelId;
+ }
+ }
+ aParameters[kH264FmtpSpropParameterSets] = aConfig.mSpropParameterSets;
+ }
+ if (aConfig.mName == kVp8CodecName) {
+ webrtc::VideoCodecVP8 vp8_settings =
+ webrtc::VideoEncoder::GetDefaultVp8Settings();
+ vp8_settings.automaticResizeOn = automatic_resize;
+ // VP8 denoising is enabled by default.
+ vp8_settings.denoisingOn = codec_default_denoising ? true : denoising;
+ return rtc::scoped_refptr<
+ webrtc::VideoEncoderConfig::EncoderSpecificSettings>(
+ new rtc::RefCountedObject<
+ webrtc::VideoEncoderConfig::Vp8EncoderSpecificSettings>(
+ vp8_settings));
+ }
+ if (aConfig.mName == kVp9CodecName) {
+ webrtc::VideoCodecVP9 vp9_settings =
+ webrtc::VideoEncoder::GetDefaultVp9Settings();
+ if (!is_screencast) {
+ // Always configure only 1 spatial layer for screencapture as libwebrtc
+ // has some special requirements when SVC is active. For non-screencapture
+ // the spatial layers are experimentally configurable via a pref.
+ vp9_settings.numberOfSpatialLayers = aConduit->SpatialLayers();
+ }
+ // VP9 denoising is disabled by default.
+ vp9_settings.denoisingOn = codec_default_denoising ? false : denoising;
+ return rtc::scoped_refptr<
+ webrtc::VideoEncoderConfig::EncoderSpecificSettings>(
+ new rtc::RefCountedObject<
+ webrtc::VideoEncoderConfig::Vp9EncoderSpecificSettings>(
+ vp9_settings));
+ }
+ return nullptr;
+}
+
+uint32_t GenerateRandomSSRC() {
+ uint32_t ssrc;
+ do {
+ SECStatus rv = PK11_GenerateRandom(reinterpret_cast<unsigned char*>(&ssrc),
+ sizeof(ssrc));
+ MOZ_RELEASE_ASSERT(rv == SECSuccess);
+ } while (ssrc == 0); // webrtc.org code has fits if you select an SSRC of 0
+
+ return ssrc;
+}
+
+// TODO: Make this a defaulted operator when we have c++20 (bug 1731036).
+bool operator==(const rtc::VideoSinkWants& aThis,
+ const rtc::VideoSinkWants& aOther) {
+ // This would have to be expanded should we make use of more members of
+ // rtc::VideoSinkWants.
+ return aThis.max_pixel_count == aOther.max_pixel_count &&
+ aThis.max_framerate_fps == aOther.max_framerate_fps &&
+ aThis.resolution_alignment == aOther.resolution_alignment;
+}
+
+// TODO: Make this a defaulted operator when we have c++20 (bug 1731036).
+bool operator!=(const rtc::VideoSinkWants& aThis,
+ const rtc::VideoSinkWants& aOther) {
+ return !(aThis == aOther);
+}
+
+// TODO: Make this a defaulted operator when we have c++20 (bug 1731036).
+bool operator!=(
+ const webrtc::VideoReceiveStreamInterface::Config::Rtp& aThis,
+ const webrtc::VideoReceiveStreamInterface::Config::Rtp& aOther) {
+ return aThis.remote_ssrc != aOther.remote_ssrc ||
+ aThis.local_ssrc != aOther.local_ssrc ||
+ aThis.rtcp_mode != aOther.rtcp_mode ||
+ aThis.rtcp_xr.receiver_reference_time_report !=
+ aOther.rtcp_xr.receiver_reference_time_report ||
+ aThis.remb != aOther.remb || aThis.tmmbr != aOther.tmmbr ||
+ aThis.keyframe_method != aOther.keyframe_method ||
+ aThis.lntf.enabled != aOther.lntf.enabled ||
+ aThis.nack.rtp_history_ms != aOther.nack.rtp_history_ms ||
+ aThis.ulpfec_payload_type != aOther.ulpfec_payload_type ||
+ aThis.red_payload_type != aOther.red_payload_type ||
+ aThis.rtx_ssrc != aOther.rtx_ssrc ||
+ aThis.protected_by_flexfec != aOther.protected_by_flexfec ||
+ aThis.rtx_associated_payload_types !=
+ aOther.rtx_associated_payload_types ||
+ aThis.raw_payload_types != aOther.raw_payload_types ||
+ aThis.extensions != aOther.extensions;
+}
+
+#ifdef DEBUG
+// TODO: Make this a defaulted operator when we have c++20 (bug 1731036).
+bool operator==(
+ const webrtc::VideoReceiveStreamInterface::Config::Rtp& aThis,
+ const webrtc::VideoReceiveStreamInterface::Config::Rtp& aOther) {
+ return !(aThis != aOther);
+}
+#endif
+
+// TODO: Make this a defaulted operator when we have c++20 (bug 1731036).
+bool operator!=(const webrtc::RtpConfig& aThis,
+ const webrtc::RtpConfig& aOther) {
+ return aThis.ssrcs != aOther.ssrcs || aThis.rids != aOther.rids ||
+ aThis.mid != aOther.mid || aThis.rtcp_mode != aOther.rtcp_mode ||
+ aThis.max_packet_size != aOther.max_packet_size ||
+ aThis.extmap_allow_mixed != aOther.extmap_allow_mixed ||
+ aThis.extensions != aOther.extensions ||
+ aThis.payload_name != aOther.payload_name ||
+ aThis.payload_type != aOther.payload_type ||
+ aThis.raw_payload != aOther.raw_payload ||
+ aThis.lntf.enabled != aOther.lntf.enabled ||
+ aThis.nack.rtp_history_ms != aOther.nack.rtp_history_ms ||
+ !(aThis.ulpfec == aOther.ulpfec) ||
+ aThis.flexfec.payload_type != aOther.flexfec.payload_type ||
+ aThis.flexfec.ssrc != aOther.flexfec.ssrc ||
+ aThis.flexfec.protected_media_ssrcs !=
+ aOther.flexfec.protected_media_ssrcs ||
+ aThis.rtx.ssrcs != aOther.rtx.ssrcs ||
+ aThis.rtx.payload_type != aOther.rtx.payload_type ||
+ aThis.c_name != aOther.c_name;
+}
+
+#ifdef DEBUG
+// TODO: Make this a defaulted operator when we have c++20 (bug 1731036).
+bool operator==(const webrtc::RtpConfig& aThis,
+ const webrtc::RtpConfig& aOther) {
+ return !(aThis != aOther);
+}
+#endif
+
+} // namespace
+
+/**
+ * Factory Method for VideoConduit
+ */
+RefPtr<VideoSessionConduit> VideoSessionConduit::Create(
+ RefPtr<WebrtcCallWrapper> aCall, nsCOMPtr<nsISerialEventTarget> aStsThread,
+ Options aOptions, std::string aPCHandle,
+ const TrackingId& aRecvTrackingId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aCall, "missing required parameter: aCall");
+ CSFLogVerbose(LOGTAG, "%s", __FUNCTION__);
+
+ if (!aCall) {
+ return nullptr;
+ }
+
+ auto obj = MakeRefPtr<WebrtcVideoConduit>(
+ std::move(aCall), std::move(aStsThread), std::move(aOptions),
+ std::move(aPCHandle), aRecvTrackingId);
+ if (obj->Init() != kMediaConduitNoError) {
+ CSFLogError(LOGTAG, "%s VideoConduit Init Failed ", __FUNCTION__);
+ return nullptr;
+ }
+ CSFLogVerbose(LOGTAG, "%s Successfully created VideoConduit ", __FUNCTION__);
+ return obj.forget();
+}
+
+#define INIT_MIRROR(name, val) \
+ name(aCallThread, val, "WebrtcVideoConduit::Control::" #name " (Mirror)")
+WebrtcVideoConduit::Control::Control(const RefPtr<AbstractThread>& aCallThread)
+ : INIT_MIRROR(mReceiving, false),
+ INIT_MIRROR(mTransmitting, false),
+ INIT_MIRROR(mLocalSsrcs, Ssrcs()),
+ INIT_MIRROR(mLocalRtxSsrcs, Ssrcs()),
+ INIT_MIRROR(mLocalCname, std::string()),
+ INIT_MIRROR(mMid, std::string()),
+ INIT_MIRROR(mRemoteSsrc, 0),
+ INIT_MIRROR(mRemoteRtxSsrc, 0),
+ INIT_MIRROR(mSyncGroup, std::string()),
+ INIT_MIRROR(mLocalRecvRtpExtensions, RtpExtList()),
+ INIT_MIRROR(mLocalSendRtpExtensions, RtpExtList()),
+ INIT_MIRROR(mSendCodec, Nothing()),
+ INIT_MIRROR(mSendRtpRtcpConfig, Nothing()),
+ INIT_MIRROR(mRecvCodecs, std::vector<VideoCodecConfig>()),
+ INIT_MIRROR(mRecvRtpRtcpConfig, Nothing()),
+ INIT_MIRROR(mCodecMode, webrtc::VideoCodecMode::kRealtimeVideo) {}
+#undef INIT_MIRROR
+
+WebrtcVideoConduit::WebrtcVideoConduit(
+ RefPtr<WebrtcCallWrapper> aCall, nsCOMPtr<nsISerialEventTarget> aStsThread,
+ Options aOptions, std::string aPCHandle, const TrackingId& aRecvTrackingId)
+ : mRendererMonitor("WebrtcVideoConduit::mRendererMonitor"),
+ mCallThread(aCall->mCallThread),
+ mStsThread(std::move(aStsThread)),
+ mControl(aCall->mCallThread),
+ mWatchManager(this, aCall->mCallThread),
+ mMutex("WebrtcVideoConduit::mMutex"),
+ mDecoderFactory(MakeUnique<WebrtcVideoDecoderFactory>(
+ mCallThread.get(), aPCHandle, aRecvTrackingId)),
+ mEncoderFactory(MakeUnique<WebrtcVideoEncoderFactory>(
+ mCallThread.get(), std::move(aPCHandle))),
+ mBufferPool(false, SCALER_BUFFER_POOL_SIZE),
+ mEngineTransmitting(false),
+ mEngineReceiving(false),
+ mVideoLatencyTestEnable(aOptions.mVideoLatencyTestEnable),
+ mMinBitrate(aOptions.mMinBitrate),
+ mStartBitrate(aOptions.mStartBitrate),
+ mPrefMaxBitrate(aOptions.mPrefMaxBitrate),
+ mMinBitrateEstimate(aOptions.mMinBitrateEstimate),
+ mDenoising(aOptions.mDenoising),
+ mLockScaling(aOptions.mLockScaling),
+ mSpatialLayers(aOptions.mSpatialLayers),
+ mTemporalLayers(aOptions.mTemporalLayers),
+ mCall(std::move(aCall)),
+ mSendTransport(this),
+ mRecvTransport(this),
+ mSendStreamConfig(&mSendTransport),
+ mVideoStreamFactory("WebrtcVideoConduit::mVideoStreamFactory"),
+ mRecvStreamConfig(&mRecvTransport) {
+ mRecvStreamConfig.rtp.rtcp_event_observer = this;
+}
+
+WebrtcVideoConduit::~WebrtcVideoConduit() {
+ CSFLogDebug(LOGTAG, "%s ", __FUNCTION__);
+
+ MOZ_ASSERT(!mSendStream && !mRecvStream,
+ "Call DeleteStreams prior to ~WebrtcVideoConduit.");
+}
+
+#define CONNECT(aCanonical, aMirror) \
+ do { \
+ (aMirror).Connect(aCanonical); \
+ mWatchManager.Watch(aMirror, &WebrtcVideoConduit::OnControlConfigChange); \
+ } while (0)
+
+void WebrtcVideoConduit::InitControl(VideoConduitControlInterface* aControl) {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+
+ CONNECT(aControl->CanonicalReceiving(), mControl.mReceiving);
+ CONNECT(aControl->CanonicalTransmitting(), mControl.mTransmitting);
+ CONNECT(aControl->CanonicalLocalSsrcs(), mControl.mLocalSsrcs);
+ CONNECT(aControl->CanonicalLocalVideoRtxSsrcs(), mControl.mLocalRtxSsrcs);
+ CONNECT(aControl->CanonicalLocalCname(), mControl.mLocalCname);
+ CONNECT(aControl->CanonicalMid(), mControl.mMid);
+ CONNECT(aControl->CanonicalRemoteSsrc(), mControl.mRemoteSsrc);
+ CONNECT(aControl->CanonicalRemoteVideoRtxSsrc(), mControl.mRemoteRtxSsrc);
+ CONNECT(aControl->CanonicalSyncGroup(), mControl.mSyncGroup);
+ CONNECT(aControl->CanonicalLocalRecvRtpExtensions(),
+ mControl.mLocalRecvRtpExtensions);
+ CONNECT(aControl->CanonicalLocalSendRtpExtensions(),
+ mControl.mLocalSendRtpExtensions);
+ CONNECT(aControl->CanonicalVideoSendCodec(), mControl.mSendCodec);
+ CONNECT(aControl->CanonicalVideoSendRtpRtcpConfig(),
+ mControl.mSendRtpRtcpConfig);
+ CONNECT(aControl->CanonicalVideoRecvCodecs(), mControl.mRecvCodecs);
+ CONNECT(aControl->CanonicalVideoRecvRtpRtcpConfig(),
+ mControl.mRecvRtpRtcpConfig);
+ CONNECT(aControl->CanonicalVideoCodecMode(), mControl.mCodecMode);
+}
+
+#undef CONNECT
+
+void WebrtcVideoConduit::OnControlConfigChange() {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+
+ bool encoderReconfigureNeeded = false;
+ bool remoteSsrcUpdateNeeded = false;
+ bool sendStreamRecreationNeeded = false;
+
+ if (mControl.mRemoteSsrc.Ref() != mControl.mConfiguredRemoteSsrc) {
+ mControl.mConfiguredRemoteSsrc = mControl.mRemoteSsrc;
+ remoteSsrcUpdateNeeded = true;
+ }
+
+ if (mControl.mRemoteRtxSsrc.Ref() != mControl.mConfiguredRemoteRtxSsrc) {
+ mControl.mConfiguredRemoteRtxSsrc = mControl.mRemoteRtxSsrc;
+ remoteSsrcUpdateNeeded = true;
+ }
+
+ if (mControl.mSyncGroup.Ref() != mRecvStreamConfig.sync_group) {
+ mRecvStreamConfig.sync_group = mControl.mSyncGroup;
+ }
+
+ if (mControl.mLocalRecvRtpExtensions.Ref() !=
+ mRecvStreamConfig.rtp.extensions) {
+ mRecvStreamConfig.rtp.extensions = mControl.mLocalRecvRtpExtensions;
+ }
+
+ if (const auto [codecConfigList, rtpRtcpConfig] = std::make_pair(
+ mControl.mRecvCodecs.Ref(), mControl.mRecvRtpRtcpConfig.Ref());
+ !codecConfigList.empty() && rtpRtcpConfig.isSome() &&
+ (codecConfigList != mControl.mConfiguredRecvCodecs ||
+ rtpRtcpConfig != mControl.mConfiguredRecvRtpRtcpConfig)) {
+ mControl.mConfiguredRecvCodecs = codecConfigList;
+ mControl.mConfiguredRecvRtpRtcpConfig = rtpRtcpConfig;
+
+ webrtc::VideoReceiveStreamInterface::Config::Rtp newRtp(
+ mRecvStreamConfig.rtp);
+ MOZ_ASSERT(newRtp == mRecvStreamConfig.rtp);
+ newRtp.rtx_associated_payload_types.clear();
+ newRtp.rtcp_mode = rtpRtcpConfig->GetRtcpMode();
+ newRtp.nack.rtp_history_ms = 0;
+ newRtp.remb = false;
+ newRtp.tmmbr = false;
+ newRtp.keyframe_method = webrtc::KeyFrameReqMethod::kNone;
+ newRtp.ulpfec_payload_type = kNullPayloadType;
+ newRtp.red_payload_type = kNullPayloadType;
+ bool use_fec = false;
+ bool configuredH264 = false;
+ std::vector<webrtc::VideoReceiveStreamInterface::Decoder> recv_codecs;
+
+ // Try Applying the codecs in the list
+ // we treat as success if at least one codec was applied and reception was
+ // started successfully.
+ for (const auto& codec_config : codecConfigList) {
+ if (auto condError = ValidateCodecConfig(codec_config);
+ condError != kMediaConduitNoError) {
+ CSFLogError(LOGTAG, "Invalid recv codec config for %s decoder: %i",
+ codec_config.mName.c_str(), condError);
+ continue;
+ }
+
+ if (codec_config.mName == kH264CodecName) {
+ // TODO(bug 1200768): We can only handle configuring one recv H264 codec
+ if (configuredH264) {
+ continue;
+ }
+ configuredH264 = true;
+ }
+
+ if (codec_config.mName == kUlpfecCodecName) {
+ newRtp.ulpfec_payload_type = codec_config.mType;
+ continue;
+ }
+
+ if (codec_config.mName == kRedCodecName) {
+ newRtp.red_payload_type = codec_config.mType;
+ continue;
+ }
+
+ if (SupportedCodecType(
+ webrtc::PayloadStringToCodecType(codec_config.mName)) ==
+ webrtc::VideoCodecType::kVideoCodecGeneric) {
+ CSFLogError(LOGTAG, "%s Unknown decoder type: %s", __FUNCTION__,
+ codec_config.mName.c_str());
+ continue;
+ }
+
+ // Check for the keyframe request type: PLI is preferred over FIR, and FIR
+ // is preferred over none.
+ if (codec_config.RtcpFbNackIsSet(kRtcpFbNackParamPli)) {
+ newRtp.keyframe_method = webrtc::KeyFrameReqMethod::kPliRtcp;
+ } else if (newRtp.keyframe_method !=
+ webrtc::KeyFrameReqMethod::kPliRtcp &&
+ codec_config.RtcpFbCcmIsSet(kRtcpFbCcmParamFir)) {
+ newRtp.keyframe_method = webrtc::KeyFrameReqMethod::kFirRtcp;
+ }
+
+ // What if codec A has Nack and REMB, and codec B has TMMBR, and codec C
+ // has none? In practice, that's not a useful configuration, and
+ // VideoReceiveStream::Config can't represent that, so simply union the
+ // (boolean) settings
+ if (codec_config.RtcpFbNackIsSet(kParamValueEmpty)) {
+ newRtp.nack.rtp_history_ms = 1000;
+ }
+ newRtp.tmmbr |= codec_config.RtcpFbCcmIsSet(kRtcpFbCcmParamTmmbr);
+ newRtp.remb |= codec_config.RtcpFbRembIsSet();
+ use_fec |= codec_config.RtcpFbFECIsSet();
+
+ if (codec_config.RtxPayloadTypeIsSet()) {
+ newRtp.rtx_associated_payload_types[codec_config.mRTXPayloadType] =
+ codec_config.mType;
+ }
+
+ auto& decoder = recv_codecs.emplace_back();
+ decoder.video_format = webrtc::SdpVideoFormat(codec_config.mName);
+ decoder.payload_type = codec_config.mType;
+ }
+
+ if (!use_fec) {
+ // Reset to defaults
+ newRtp.ulpfec_payload_type = kNullPayloadType;
+ newRtp.red_payload_type = kNullPayloadType;
+ }
+
+ // TODO: This would be simpler, but for some reason gives
+ // "error: invalid operands to binary expression
+ // ('webrtc::VideoReceiveStreamInterface::Decoder' and
+ // 'webrtc::VideoReceiveStreamInterface::Decoder')"
+ // if (recv_codecs != mRecvStreamConfig.decoders) {
+ if (!std::equal(recv_codecs.begin(), recv_codecs.end(),
+ mRecvStreamConfig.decoders.begin(),
+ mRecvStreamConfig.decoders.end(),
+ [](const auto& aLeft, const auto& aRight) {
+ return aLeft == aRight;
+ })) {
+ if (recv_codecs.empty()) {
+ CSFLogError(LOGTAG, "%s Found no valid receive codecs", __FUNCTION__);
+ }
+ mRecvStreamConfig.decoders = std::move(recv_codecs);
+ }
+
+ if (mRecvStreamConfig.rtp != newRtp) {
+ mRecvStreamConfig.rtp = newRtp;
+ }
+ }
+
+ {
+ // mSendStreamConfig and other members need the lock
+ MutexAutoLock lock(mMutex);
+ if (mControl.mLocalSsrcs.Ref() != mSendStreamConfig.rtp.ssrcs) {
+ mSendStreamConfig.rtp.ssrcs = mControl.mLocalSsrcs;
+ sendStreamRecreationNeeded = true;
+
+ const uint32_t localSsrc = mSendStreamConfig.rtp.ssrcs.empty()
+ ? 0
+ : mSendStreamConfig.rtp.ssrcs.front();
+ if (localSsrc != mRecvStreamConfig.rtp.local_ssrc) {
+ mRecvStreamConfig.rtp.local_ssrc = localSsrc;
+ }
+ }
+
+ {
+ Ssrcs localRtxSsrcs = mControl.mLocalRtxSsrcs.Ref();
+ if (!mControl.mSendCodec.Ref()
+ .map([](const auto& aCodec) {
+ return aCodec.RtxPayloadTypeIsSet();
+ })
+ .valueOr(false)) {
+ localRtxSsrcs.clear();
+ }
+ if (localRtxSsrcs != mSendStreamConfig.rtp.rtx.ssrcs) {
+ mSendStreamConfig.rtp.rtx.ssrcs = localRtxSsrcs;
+ sendStreamRecreationNeeded = true;
+ }
+ }
+
+ if (mControl.mLocalCname.Ref() != mSendStreamConfig.rtp.c_name) {
+ mSendStreamConfig.rtp.c_name = mControl.mLocalCname;
+ sendStreamRecreationNeeded = true;
+ }
+
+ if (mControl.mMid.Ref() != mSendStreamConfig.rtp.mid) {
+ mSendStreamConfig.rtp.mid = mControl.mMid;
+ sendStreamRecreationNeeded = true;
+ }
+
+ if (mControl.mLocalSendRtpExtensions.Ref() !=
+ mSendStreamConfig.rtp.extensions) {
+ mSendStreamConfig.rtp.extensions = mControl.mLocalSendRtpExtensions;
+ sendStreamRecreationNeeded = true;
+ }
+
+ if (const auto [codecConfig, rtpRtcpConfig] = std::make_pair(
+ mControl.mSendCodec.Ref(), mControl.mSendRtpRtcpConfig.Ref());
+ codecConfig.isSome() && rtpRtcpConfig.isSome() &&
+ (codecConfig != mControl.mConfiguredSendCodec ||
+ rtpRtcpConfig != mControl.mConfiguredSendRtpRtcpConfig)) {
+ CSFLogDebug(LOGTAG, "Configuring codec %s", codecConfig->mName.c_str());
+ mControl.mConfiguredSendCodec = codecConfig;
+ mControl.mConfiguredSendRtpRtcpConfig = rtpRtcpConfig;
+
+ if (ValidateCodecConfig(*codecConfig) == kMediaConduitNoError) {
+ encoderReconfigureNeeded = true;
+
+ mCurSendCodecConfig = codecConfig;
+
+ size_t streamCount = std::min(codecConfig->mEncodings.size(),
+ (size_t)webrtc::kMaxSimulcastStreams);
+ size_t highestResolutionIndex = 0;
+ for (size_t i = 1; i < streamCount; ++i) {
+ if (codecConfig->mEncodings[i].constraints.scaleDownBy <
+ codecConfig->mEncodings[highestResolutionIndex]
+ .constraints.scaleDownBy) {
+ highestResolutionIndex = i;
+ }
+ }
+ MOZ_RELEASE_ASSERT(streamCount >= 1,
+ "streamCount should be at least one");
+
+ CSFLogDebug(LOGTAG,
+ "Updating send codec for VideoConduit:%p stream count:%zu",
+ this, streamCount);
+
+ // So we can comply with b=TIAS/b=AS/maxbr=X when input resolution
+ // changes
+ MOZ_ASSERT(codecConfig->mTias < INT_MAX);
+ mNegotiatedMaxBitrate = static_cast<int>(codecConfig->mTias);
+
+ if (mLastWidth == 0 && mMinBitrateEstimate != 0) {
+ // Only do this at the start; use "have we sent a frame" as a
+ // reasonable stand-in. min <= start <= max (but all three parameters
+ // are optional)
+ webrtc::BitrateSettings settings;
+ settings.min_bitrate_bps = mMinBitrateEstimate;
+ settings.start_bitrate_bps = mMinBitrateEstimate;
+ mCall->Call()->SetClientBitratePreferences(settings);
+ }
+
+ // XXX parse the encoded SPS/PPS data and set
+ // spsData/spsLen/ppsData/ppsLen
+ mEncoderConfig.video_format =
+ webrtc::SdpVideoFormat(codecConfig->mName);
+ mEncoderConfig.encoder_specific_settings =
+ ConfigureVideoEncoderSettings(
+ *codecConfig, this, mEncoderConfig.video_format.parameters);
+
+ mEncoderConfig.codec_type = SupportedCodecType(
+ webrtc::PayloadStringToCodecType(codecConfig->mName));
+ MOZ_RELEASE_ASSERT(mEncoderConfig.codec_type !=
+ webrtc::VideoCodecType::kVideoCodecGeneric);
+
+ mEncoderConfig.content_type =
+ mControl.mCodecMode.Ref() == webrtc::VideoCodecMode::kRealtimeVideo
+ ? webrtc::VideoEncoderConfig::ContentType::kRealtimeVideo
+ : webrtc::VideoEncoderConfig::ContentType::kScreen;
+
+ mEncoderConfig.frame_drop_enabled =
+ mControl.mCodecMode.Ref() != webrtc::VideoCodecMode::kScreensharing;
+
+ mEncoderConfig.min_transmit_bitrate_bps = mMinBitrate;
+
+ // Set the max bitrate, defaulting to 10Mbps, checking:
+ // - pref
+ // - b=TIAS
+ // - codec constraints
+ // - encoding parameter if there's a single stream
+ int maxBps = KBPS(10000);
+ maxBps = MinIgnoreZero(maxBps, mPrefMaxBitrate);
+ maxBps = MinIgnoreZero(maxBps, mNegotiatedMaxBitrate);
+ maxBps = MinIgnoreZero(
+ maxBps, static_cast<int>(codecConfig->mEncodingConstraints.maxBr));
+ if (codecConfig->mEncodings.size() == 1) {
+ maxBps = MinIgnoreZero(
+ maxBps,
+ static_cast<int>(codecConfig->mEncodings[0].constraints.maxBr));
+ }
+ mEncoderConfig.max_bitrate_bps = maxBps;
+
+ // TODO this is for webrtc-priority, but needs plumbing bits
+ mEncoderConfig.bitrate_priority = 1.0;
+
+ // Expected max number of encodings
+ mEncoderConfig.number_of_streams = streamCount;
+
+ // libwebrtc disables this by default.
+ mSendStreamConfig.suspend_below_min_bitrate = false;
+
+ webrtc::RtpConfig newRtp = mSendStreamConfig.rtp;
+ MOZ_ASSERT(newRtp == mSendStreamConfig.rtp);
+ newRtp.payload_name = codecConfig->mName;
+ newRtp.payload_type = codecConfig->mType;
+ newRtp.rtcp_mode = rtpRtcpConfig->GetRtcpMode();
+ newRtp.max_packet_size = kVideoMtu;
+ newRtp.rtx.payload_type = codecConfig->RtxPayloadTypeIsSet()
+ ? codecConfig->mRTXPayloadType
+ : kNullPayloadType;
+
+ {
+ // See Bug 1297058, enabling FEC when basic NACK is to be enabled in
+ // H.264 is problematic
+ const bool useFECDefaults =
+ !codecConfig->RtcpFbFECIsSet() ||
+ (codecConfig->mName == kH264CodecName &&
+ codecConfig->RtcpFbNackIsSet(kParamValueEmpty));
+ newRtp.ulpfec.ulpfec_payload_type =
+ useFECDefaults ? kNullPayloadType
+ : codecConfig->mULPFECPayloadType;
+ newRtp.ulpfec.red_payload_type =
+ useFECDefaults ? kNullPayloadType : codecConfig->mREDPayloadType;
+ newRtp.ulpfec.red_rtx_payload_type =
+ useFECDefaults ? kNullPayloadType
+ : codecConfig->mREDRTXPayloadType;
+ }
+
+ newRtp.nack.rtp_history_ms =
+ codecConfig->RtcpFbNackIsSet(kParamValueEmpty) ? 1000 : 0;
+
+ {
+ newRtp.rids.clear();
+ bool has_rid = false;
+ for (size_t idx = 0; idx < streamCount; idx++) {
+ const auto& encoding = codecConfig->mEncodings[idx];
+ if (encoding.rid[0]) {
+ has_rid = true;
+ break;
+ }
+ }
+ if (has_rid) {
+ for (size_t idx = streamCount; idx > 0; idx--) {
+ const auto& encoding = codecConfig->mEncodings[idx - 1];
+ newRtp.rids.push_back(encoding.rid);
+ }
+ }
+ }
+ if (mSendStreamConfig.rtp != newRtp) {
+ mSendStreamConfig.rtp = newRtp;
+ sendStreamRecreationNeeded = true;
+ }
+
+ mEncoderConfig.video_stream_factory = CreateVideoStreamFactory();
+ }
+ }
+
+ {
+ const auto& mode = mControl.mCodecMode.Ref();
+ MOZ_ASSERT(mode == webrtc::VideoCodecMode::kRealtimeVideo ||
+ mode == webrtc::VideoCodecMode::kScreensharing);
+
+ auto contentType =
+ mode == webrtc::VideoCodecMode::kRealtimeVideo
+ ? webrtc::VideoEncoderConfig::ContentType::kRealtimeVideo
+ : webrtc::VideoEncoderConfig::ContentType::kScreen;
+
+ if (contentType != mEncoderConfig.content_type) {
+ mEncoderConfig.video_stream_factory = CreateVideoStreamFactory();
+ encoderReconfigureNeeded = true;
+ }
+ }
+
+ if (remoteSsrcUpdateNeeded) {
+ SetRemoteSSRCConfig(mControl.mConfiguredRemoteSsrc,
+ mControl.mConfiguredRemoteRtxSsrc);
+ }
+
+ // Handle un-signalled SSRCs by creating random ones and then when they
+ // actually get set, we'll destroy and recreate.
+ if (mControl.mReceiving || mControl.mTransmitting) {
+ const auto remoteSsrc = mRecvStreamConfig.rtp.remote_ssrc;
+ const auto localSsrc = mRecvStreamConfig.rtp.local_ssrc;
+ const auto localSsrcs = mSendStreamConfig.rtp.ssrcs;
+ EnsureLocalSSRC();
+ if (mControl.mReceiving) {
+ EnsureRemoteSSRC();
+ }
+ if (localSsrc != mRecvStreamConfig.rtp.local_ssrc ||
+ remoteSsrc != mRecvStreamConfig.rtp.remote_ssrc) {
+ }
+ if (localSsrcs != mSendStreamConfig.rtp.ssrcs) {
+ sendStreamRecreationNeeded = true;
+ }
+ }
+
+ // Recreate receiving streams
+ if (mControl.mReceiving) {
+ DeleteRecvStream();
+ CreateRecvStream();
+ }
+ if (sendStreamRecreationNeeded) {
+ DeleteSendStream();
+ }
+ if (mControl.mTransmitting) {
+ CreateSendStream();
+ }
+ }
+
+ // We make sure to not hold the lock while stopping/starting/reconfiguring
+ // streams, so as to not cause deadlocks. These methods can cause our platform
+ // codecs to dispatch sync runnables to main, and main may grab the lock.
+
+ if (mSendStream && encoderReconfigureNeeded) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ mSendStreamConfig.rtp.ssrcs.size() == mEncoderConfig.number_of_streams,
+ "Each video substream must have a corresponding ssrc.");
+ mSendStream->ReconfigureVideoEncoder(mEncoderConfig.Copy());
+ }
+
+ if (!mControl.mReceiving) {
+ StopReceiving();
+ }
+ if (!mControl.mTransmitting) {
+ StopTransmitting();
+ }
+
+ if (mControl.mReceiving) {
+ StartReceiving();
+ }
+ if (mControl.mTransmitting) {
+ StartTransmitting();
+ }
+}
+
+std::vector<unsigned int> WebrtcVideoConduit::GetLocalSSRCs() const {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ return mSendStreamConfig.rtp.ssrcs;
+}
+
+Maybe<Ssrc> WebrtcVideoConduit::GetAssociatedLocalRtxSSRC(Ssrc aSsrc) const {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ for (size_t i = 0; i < mSendStreamConfig.rtp.ssrcs.size() &&
+ i < mSendStreamConfig.rtp.rtx.ssrcs.size();
+ ++i) {
+ if (mSendStreamConfig.rtp.ssrcs[i] == aSsrc) {
+ return Some(mSendStreamConfig.rtp.rtx.ssrcs[i]);
+ }
+ }
+ return Nothing();
+}
+
+void WebrtcVideoConduit::DeleteSendStream() {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ mMutex.AssertCurrentThreadOwns();
+
+ if (!mSendStream) {
+ return;
+ }
+
+ mCall->Call()->DestroyVideoSendStream(mSendStream);
+ mEngineTransmitting = false;
+ mSendStream = nullptr;
+
+ // Reset base_seqs in case ssrcs get re-used.
+ mRtpSendBaseSeqs.clear();
+}
+
+void WebrtcVideoConduit::CreateSendStream() {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ mMutex.AssertCurrentThreadOwns();
+
+ if (mSendStream) {
+ return;
+ }
+
+ nsAutoString codecName;
+ codecName.AssignASCII(mSendStreamConfig.rtp.payload_name.c_str());
+ Telemetry::ScalarAdd(Telemetry::ScalarID::WEBRTC_VIDEO_SEND_CODEC_USED,
+ codecName, 1);
+
+ mSendStreamConfig.encoder_settings.encoder_factory = mEncoderFactory.get();
+ mSendStreamConfig.encoder_settings.bitrate_allocator_factory =
+ mCall->mVideoBitrateAllocatorFactory.get();
+
+ MOZ_DIAGNOSTIC_ASSERT(
+ mSendStreamConfig.rtp.ssrcs.size() == mEncoderConfig.number_of_streams,
+ "Each video substream must have a corresponding ssrc.");
+
+ mSendStream = mCall->Call()->CreateVideoSendStream(mSendStreamConfig.Copy(),
+ mEncoderConfig.Copy());
+
+ mSendStream->SetSource(this, webrtc::DegradationPreference::BALANCED);
+}
+
+void WebrtcVideoConduit::DeleteRecvStream() {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ mMutex.AssertCurrentThreadOwns();
+
+ if (!mRecvStream) {
+ return;
+ }
+
+ mCall->Call()->DestroyVideoReceiveStream(mRecvStream);
+ mEngineReceiving = false;
+ mRecvStream = nullptr;
+}
+
+void WebrtcVideoConduit::CreateRecvStream() {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ mMutex.AssertCurrentThreadOwns();
+
+ if (mRecvStream) {
+ return;
+ }
+
+ mRecvStreamConfig.renderer = this;
+
+ for (auto& decoder : mRecvStreamConfig.decoders) {
+ nsAutoString codecName;
+ codecName.AssignASCII(decoder.video_format.name.c_str());
+ Telemetry::ScalarAdd(Telemetry::ScalarID::WEBRTC_VIDEO_RECV_CODEC_USED,
+ codecName, 1);
+ }
+
+ mRecvStreamConfig.decoder_factory = mDecoderFactory.get();
+
+ mRecvStream =
+ mCall->Call()->CreateVideoReceiveStream(mRecvStreamConfig.Copy());
+ // Ensure that we set the jitter buffer target on this stream.
+ mRecvStream->SetBaseMinimumPlayoutDelayMs(mJitterBufferTargetMs);
+
+ CSFLogDebug(LOGTAG, "Created VideoReceiveStream %p for SSRC %u (0x%x)",
+ mRecvStream, mRecvStreamConfig.rtp.remote_ssrc,
+ mRecvStreamConfig.rtp.remote_ssrc);
+}
+
+void WebrtcVideoConduit::NotifyUnsetCurrentRemoteSSRC() {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ CSFLogDebug(LOGTAG, "%s (%p): Unsetting SSRC %u in other conduits",
+ __FUNCTION__, this, mRecvStreamConfig.rtp.remote_ssrc);
+ mCall->UnregisterConduit(this);
+ mCall->UnsetRemoteSSRC(mRecvStreamConfig.rtp.remote_ssrc);
+ mCall->RegisterConduit(this);
+}
+
+void WebrtcVideoConduit::SetRemoteSSRCConfig(uint32_t aSsrc,
+ uint32_t aRtxSsrc) {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+
+ CSFLogDebug(LOGTAG, "%s: SSRC %u (0x%x)", __FUNCTION__, aSsrc, aSsrc);
+
+ if (mRecvStreamConfig.rtp.remote_ssrc != aSsrc) {
+ nsCOMPtr<nsIDirectTaskDispatcher> dtd = do_QueryInterface(mCallThread);
+ MOZ_ALWAYS_SUCCEEDS(dtd->DispatchDirectTask(NewRunnableMethod(
+ "WebrtcVideoConduit::NotifyUnsetCurrentRemoteSSRC", this,
+ &WebrtcVideoConduit::NotifyUnsetCurrentRemoteSSRC)));
+ }
+
+ mRecvSSRC = mRecvStreamConfig.rtp.remote_ssrc = aSsrc;
+ mRecvStreamConfig.rtp.rtx_ssrc = aRtxSsrc;
+}
+
+void WebrtcVideoConduit::SetRemoteSSRCAndRestartAsNeeded(uint32_t aSsrc,
+ uint32_t aRtxSsrc) {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+
+ if (mRecvStreamConfig.rtp.remote_ssrc == aSsrc &&
+ mRecvStreamConfig.rtp.rtx_ssrc == aRtxSsrc) {
+ return;
+ }
+
+ SetRemoteSSRCConfig(aSsrc, aRtxSsrc);
+
+ const bool wasReceiving = mEngineReceiving;
+ const bool hadRecvStream = mRecvStream;
+
+ StopReceiving();
+
+ if (hadRecvStream) {
+ MutexAutoLock lock(mMutex);
+ DeleteRecvStream();
+ CreateRecvStream();
+ }
+
+ if (wasReceiving) {
+ StartReceiving();
+ }
+}
+
+void WebrtcVideoConduit::EnsureRemoteSSRC() {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ mMutex.AssertCurrentThreadOwns();
+
+ const auto& ssrcs = mSendStreamConfig.rtp.ssrcs;
+ if (mRecvStreamConfig.rtp.remote_ssrc != 0 &&
+ std::find(ssrcs.begin(), ssrcs.end(),
+ mRecvStreamConfig.rtp.remote_ssrc) == ssrcs.end()) {
+ return;
+ }
+
+ uint32_t ssrc;
+ do {
+ ssrc = GenerateRandomSSRC();
+ } while (
+ NS_WARN_IF(std::find(ssrcs.begin(), ssrcs.end(), ssrc) != ssrcs.end()));
+ CSFLogDebug(LOGTAG, "VideoConduit %p: Generated remote SSRC %u", this, ssrc);
+ SetRemoteSSRCConfig(ssrc, 0);
+}
+
+void WebrtcVideoConduit::EnsureLocalSSRC() {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ mMutex.AssertCurrentThreadOwns();
+
+ auto& ssrcs = mSendStreamConfig.rtp.ssrcs;
+ if (ssrcs.empty()) {
+ ssrcs.push_back(GenerateRandomSSRC());
+ }
+
+ // Reverse-iterating here so that the first dupe in `ssrcs` always wins.
+ for (auto& ssrc : Reversed(ssrcs)) {
+ if (ssrc != 0 && ssrc != mRecvStreamConfig.rtp.remote_ssrc &&
+ std::count(ssrcs.begin(), ssrcs.end(), ssrc) == 1) {
+ continue;
+ }
+ do {
+ ssrc = GenerateRandomSSRC();
+ } while (NS_WARN_IF(ssrc == mRecvStreamConfig.rtp.remote_ssrc) ||
+ NS_WARN_IF(std::count(ssrcs.begin(), ssrcs.end(), ssrc) > 1));
+ CSFLogDebug(LOGTAG, "%s (%p): Generated local SSRC %u", __FUNCTION__, this,
+ ssrc);
+ }
+ mRecvStreamConfig.rtp.local_ssrc = ssrcs[0];
+}
+
+void WebrtcVideoConduit::UnsetRemoteSSRC(uint32_t aSsrc) {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ mMutex.AssertNotCurrentThreadOwns();
+
+ if (mRecvStreamConfig.rtp.remote_ssrc != aSsrc &&
+ mRecvStreamConfig.rtp.rtx_ssrc != aSsrc) {
+ return;
+ }
+
+ const auto& ssrcs = mSendStreamConfig.rtp.ssrcs;
+ uint32_t our_ssrc = 0;
+ do {
+ our_ssrc = GenerateRandomSSRC();
+ } while (NS_WARN_IF(our_ssrc == aSsrc) ||
+ NS_WARN_IF(std::find(ssrcs.begin(), ssrcs.end(), our_ssrc) !=
+ ssrcs.end()));
+
+ CSFLogDebug(LOGTAG, "%s (%p): Generated remote SSRC %u", __FUNCTION__, this,
+ our_ssrc);
+
+ // There is a (tiny) chance that this new random ssrc will collide with some
+ // other conduit's remote ssrc, in which case that conduit will choose a new
+ // one.
+ SetRemoteSSRCAndRestartAsNeeded(our_ssrc, 0);
+}
+
+/*static*/
+unsigned WebrtcVideoConduit::ToLibwebrtcMaxFramerate(
+ const Maybe<double>& aMaxFramerate) {
+ Maybe<unsigned> negotiatedMaxFps;
+ if (aMaxFramerate.isSome()) {
+ // libwebrtc does not handle non-integer max framerate.
+ unsigned integerMaxFps = static_cast<unsigned>(std::round(*aMaxFramerate));
+ // libwebrtc crashes with a max framerate of 0, even though the
+ // spec says this is valid. For now, we treat this as no limit.
+ if (integerMaxFps) {
+ negotiatedMaxFps = Some(integerMaxFps);
+ }
+ }
+ // We do not use DEFAULT_VIDEO_MAX_FRAMERATE here; that is used at the very
+ // end in VideoStreamFactory, once codec-wide and per-encoding limits are
+ // known.
+ return negotiatedMaxFps.refOr(std::numeric_limits<unsigned int>::max());
+}
+
+Maybe<Ssrc> WebrtcVideoConduit::GetRemoteSSRC() const {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ // libwebrtc uses 0 to mean a lack of SSRC. That is not to spec.
+ return mRecvStreamConfig.rtp.remote_ssrc == 0
+ ? Nothing()
+ : Some(mRecvStreamConfig.rtp.remote_ssrc);
+}
+
+Maybe<webrtc::VideoReceiveStreamInterface::Stats>
+WebrtcVideoConduit::GetReceiverStats() const {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ if (!mRecvStream) {
+ return Nothing();
+ }
+ return Some(mRecvStream->GetStats());
+}
+
+Maybe<webrtc::VideoSendStream::Stats> WebrtcVideoConduit::GetSenderStats()
+ const {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ if (!mSendStream) {
+ return Nothing();
+ }
+ return Some(mSendStream->GetStats());
+}
+
+Maybe<webrtc::Call::Stats> WebrtcVideoConduit::GetCallStats() const {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ if (!mCall->Call()) {
+ return Nothing();
+ }
+ return Some(mCall->Call()->GetStats());
+}
+
+MediaConduitErrorCode WebrtcVideoConduit::Init() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ CSFLogDebug(LOGTAG, "%s this=%p", __FUNCTION__, this);
+
+#ifdef MOZ_WIDGET_ANDROID
+ if (mozilla::camera::VideoEngine::SetAndroidObjects() != 0) {
+ CSFLogError(LOGTAG, "%s: could not set Android objects", __FUNCTION__);
+ return kMediaConduitSessionNotInited;
+ }
+#endif // MOZ_WIDGET_ANDROID
+
+ mSendPluginCreated = mEncoderFactory->CreatedGmpPluginEvent().Connect(
+ GetMainThreadSerialEventTarget(),
+ [self = detail::RawPtr(this)](uint64_t aPluginID) {
+ self.get()->mSendCodecPluginIDs.AppendElement(aPluginID);
+ });
+ mSendPluginReleased = mEncoderFactory->ReleasedGmpPluginEvent().Connect(
+ GetMainThreadSerialEventTarget(),
+ [self = detail::RawPtr(this)](uint64_t aPluginID) {
+ self.get()->mSendCodecPluginIDs.RemoveElement(aPluginID);
+ });
+ mRecvPluginCreated = mDecoderFactory->CreatedGmpPluginEvent().Connect(
+ GetMainThreadSerialEventTarget(),
+ [self = detail::RawPtr(this)](uint64_t aPluginID) {
+ self.get()->mRecvCodecPluginIDs.AppendElement(aPluginID);
+ });
+ mRecvPluginReleased = mDecoderFactory->ReleasedGmpPluginEvent().Connect(
+ GetMainThreadSerialEventTarget(),
+ [self = detail::RawPtr(this)](uint64_t aPluginID) {
+ self.get()->mRecvCodecPluginIDs.RemoveElement(aPluginID);
+ });
+
+ MOZ_ALWAYS_SUCCEEDS(mCallThread->Dispatch(NS_NewRunnableFunction(
+ __func__, [this, self = RefPtr<WebrtcVideoConduit>(this)] {
+ mCall->RegisterConduit(this);
+ })));
+
+ CSFLogDebug(LOGTAG, "%s Initialization Done", __FUNCTION__);
+ return kMediaConduitNoError;
+}
+
+RefPtr<GenericPromise> WebrtcVideoConduit::Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mSendPluginCreated.DisconnectIfExists();
+ mSendPluginReleased.DisconnectIfExists();
+ mRecvPluginCreated.DisconnectIfExists();
+ mRecvPluginReleased.DisconnectIfExists();
+
+ return InvokeAsync(
+ mCallThread, __func__, [this, self = RefPtr<WebrtcVideoConduit>(this)] {
+ using namespace Telemetry;
+ if (mSendBitrate.NumDataValues() > 0) {
+ Accumulate(WEBRTC_VIDEO_ENCODER_BITRATE_AVG_PER_CALL_KBPS,
+ static_cast<unsigned>(mSendBitrate.Mean() / 1000));
+ Accumulate(
+ WEBRTC_VIDEO_ENCODER_BITRATE_STD_DEV_PER_CALL_KBPS,
+ static_cast<unsigned>(mSendBitrate.StandardDeviation() / 1000));
+ mSendBitrate.Clear();
+ }
+ if (mSendFramerate.NumDataValues() > 0) {
+ Accumulate(WEBRTC_VIDEO_ENCODER_FRAMERATE_AVG_PER_CALL,
+ static_cast<unsigned>(mSendFramerate.Mean()));
+ Accumulate(
+ WEBRTC_VIDEO_ENCODER_FRAMERATE_10X_STD_DEV_PER_CALL,
+ static_cast<unsigned>(mSendFramerate.StandardDeviation() * 10));
+ mSendFramerate.Clear();
+ }
+
+ if (mRecvBitrate.NumDataValues() > 0) {
+ Accumulate(WEBRTC_VIDEO_DECODER_BITRATE_AVG_PER_CALL_KBPS,
+ static_cast<unsigned>(mRecvBitrate.Mean() / 1000));
+ Accumulate(
+ WEBRTC_VIDEO_DECODER_BITRATE_STD_DEV_PER_CALL_KBPS,
+ static_cast<unsigned>(mRecvBitrate.StandardDeviation() / 1000));
+ mRecvBitrate.Clear();
+ }
+ if (mRecvFramerate.NumDataValues() > 0) {
+ Accumulate(WEBRTC_VIDEO_DECODER_FRAMERATE_AVG_PER_CALL,
+ static_cast<unsigned>(mRecvFramerate.Mean()));
+ Accumulate(
+ WEBRTC_VIDEO_DECODER_FRAMERATE_10X_STD_DEV_PER_CALL,
+ static_cast<unsigned>(mRecvFramerate.StandardDeviation() * 10));
+ mRecvFramerate.Clear();
+ }
+
+ mControl.mReceiving.DisconnectIfConnected();
+ mControl.mTransmitting.DisconnectIfConnected();
+ mControl.mLocalSsrcs.DisconnectIfConnected();
+ mControl.mLocalRtxSsrcs.DisconnectIfConnected();
+ mControl.mLocalCname.DisconnectIfConnected();
+ mControl.mMid.DisconnectIfConnected();
+ mControl.mRemoteSsrc.DisconnectIfConnected();
+ mControl.mRemoteRtxSsrc.DisconnectIfConnected();
+ mControl.mSyncGroup.DisconnectIfConnected();
+ mControl.mLocalRecvRtpExtensions.DisconnectIfConnected();
+ mControl.mLocalSendRtpExtensions.DisconnectIfConnected();
+ mControl.mSendCodec.DisconnectIfConnected();
+ mControl.mSendRtpRtcpConfig.DisconnectIfConnected();
+ mControl.mRecvCodecs.DisconnectIfConnected();
+ mControl.mRecvRtpRtcpConfig.DisconnectIfConnected();
+ mControl.mCodecMode.DisconnectIfConnected();
+ mWatchManager.Shutdown();
+
+ mCall->UnregisterConduit(this);
+ mDecoderFactory->DisconnectAll();
+ mEncoderFactory->DisconnectAll();
+ {
+ MutexAutoLock lock(mMutex);
+ DeleteSendStream();
+ DeleteRecvStream();
+ }
+
+ return GenericPromise::CreateAndResolve(true, __func__);
+ });
+}
+
+webrtc::VideoCodecMode WebrtcVideoConduit::CodecMode() const {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ return mControl.mCodecMode;
+}
+
+MediaConduitErrorCode WebrtcVideoConduit::AttachRenderer(
+ RefPtr<mozilla::VideoRenderer> aVideoRenderer) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ CSFLogDebug(LOGTAG, "%s", __FUNCTION__);
+
+ // null renderer
+ if (!aVideoRenderer) {
+ CSFLogError(LOGTAG, "%s NULL Renderer", __FUNCTION__);
+ MOZ_ASSERT(false);
+ return kMediaConduitInvalidRenderer;
+ }
+
+ // This function is called only from main, so we only need to protect against
+ // modifying mRenderer while any webrtc.org code is trying to use it.
+ {
+ ReentrantMonitorAutoEnter enter(mRendererMonitor);
+ mRenderer = aVideoRenderer;
+ // Make sure the renderer knows the resolution
+ mRenderer->FrameSizeChange(mReceivingWidth, mReceivingHeight);
+ }
+
+ return kMediaConduitNoError;
+}
+
+void WebrtcVideoConduit::DetachRenderer() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ ReentrantMonitorAutoEnter enter(mRendererMonitor);
+ if (mRenderer) {
+ mRenderer = nullptr;
+ }
+}
+
+rtc::RefCountedObject<mozilla::VideoStreamFactory>*
+WebrtcVideoConduit::CreateVideoStreamFactory() {
+ auto videoStreamFactory = mVideoStreamFactory.Lock();
+ *videoStreamFactory = new rtc::RefCountedObject<VideoStreamFactory>(
+ *mCurSendCodecConfig, mControl.mCodecMode, mMinBitrate, mStartBitrate,
+ mPrefMaxBitrate, mNegotiatedMaxBitrate, mVideoBroadcaster.wants(),
+ mLockScaling);
+ return videoStreamFactory->get();
+}
+
+void WebrtcVideoConduit::AddOrUpdateSink(
+ rtc::VideoSinkInterface<webrtc::VideoFrame>* sink,
+ const rtc::VideoSinkWants& wants) {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ if (!mRegisteredSinks.Contains(sink)) {
+ mRegisteredSinks.AppendElement(sink);
+ }
+ auto oldWants = mVideoBroadcaster.wants();
+ mVideoBroadcaster.AddOrUpdateSink(sink, wants);
+ if (oldWants != mVideoBroadcaster.wants()) {
+ mEncoderConfig.video_stream_factory = CreateVideoStreamFactory();
+ mSendStream->ReconfigureVideoEncoder(mEncoderConfig.Copy());
+ }
+}
+
+void WebrtcVideoConduit::RemoveSink(
+ rtc::VideoSinkInterface<webrtc::VideoFrame>* sink) {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+
+ mRegisteredSinks.RemoveElement(sink);
+ auto oldWants = mVideoBroadcaster.wants();
+ mVideoBroadcaster.RemoveSink(sink);
+ if (oldWants != mVideoBroadcaster.wants()) {
+ mEncoderConfig.video_stream_factory = CreateVideoStreamFactory();
+ mSendStream->ReconfigureVideoEncoder(mEncoderConfig.Copy());
+ }
+}
+
+MediaConduitErrorCode WebrtcVideoConduit::SendVideoFrame(
+ webrtc::VideoFrame aFrame) {
+ // XXX Google uses a "timestamp_aligner" to translate timestamps from the
+ // camera via TranslateTimestamp(); we should look at doing the same. This
+ // avoids sampling error when capturing frames, but google had to deal with
+ // some broken cameras, include Logitech c920's IIRC.
+
+ {
+ MutexAutoLock lock(mMutex);
+ if (mSendStreamConfig.rtp.ssrcs.empty()) {
+ CSFLogVerbose(LOGTAG, "WebrtcVideoConduit %p %s No SSRC set", this,
+ __FUNCTION__);
+ return kMediaConduitNoError;
+ }
+ if (!mCurSendCodecConfig) {
+ CSFLogVerbose(LOGTAG, "WebrtcVideoConduit %p %s No send codec set", this,
+ __FUNCTION__);
+ return kMediaConduitNoError;
+ }
+
+ // Workaround for bug in libwebrtc where all encodings are transmitted
+ // if they are all inactive.
+ bool anyActive = false;
+ for (const auto& encoding : mCurSendCodecConfig->mEncodings) {
+ if (encoding.active) {
+ anyActive = true;
+ break;
+ }
+ }
+ if (!anyActive) {
+ CSFLogVerbose(LOGTAG, "WebrtcVideoConduit %p %s No active encodings",
+ this, __FUNCTION__);
+ return kMediaConduitNoError;
+ }
+
+ CSFLogVerbose(LOGTAG, "WebrtcVideoConduit %p %s (send SSRC %u (0x%x))",
+ this, __FUNCTION__, mSendStreamConfig.rtp.ssrcs.front(),
+ mSendStreamConfig.rtp.ssrcs.front());
+
+ if (aFrame.width() != mLastWidth || aFrame.height() != mLastHeight) {
+ // See if we need to recalculate what we're sending.
+ CSFLogVerbose(LOGTAG, "%s: call SelectSendResolution with %ux%u",
+ __FUNCTION__, aFrame.width(), aFrame.height());
+ MOZ_ASSERT(aFrame.width() != 0 && aFrame.height() != 0);
+ // Note coverity will flag this since it thinks they can be 0
+ MOZ_ASSERT(mCurSendCodecConfig);
+
+ mLastWidth = aFrame.width();
+ mLastHeight = aFrame.height();
+ }
+
+ // adapt input video to wants of sink
+ if (!mVideoBroadcaster.frame_wanted()) {
+ return kMediaConduitNoError;
+ }
+
+ // Check if we need to drop this frame to meet a requested FPS
+ auto videoStreamFactory = mVideoStreamFactory.Lock();
+ auto& videoStreamFactoryRef = videoStreamFactory.ref();
+ if (videoStreamFactoryRef->ShouldDropFrame(aFrame)) {
+ return kMediaConduitNoError;
+ }
+ }
+
+ // If we have zero width or height, drop the frame here. Attempting to send
+ // it will cause all sorts of problems in the webrtc.org code.
+ if (aFrame.width() == 0 || aFrame.height() == 0) {
+ return kMediaConduitNoError;
+ }
+
+ rtc::scoped_refptr<webrtc::VideoFrameBuffer> buffer =
+ aFrame.video_frame_buffer();
+
+ MOZ_ASSERT(!aFrame.color_space(), "Unexpected use of color space");
+ MOZ_ASSERT(!aFrame.has_update_rect(), "Unexpected use of update rect");
+
+#ifdef MOZ_REAL_TIME_TRACING
+ if (profiler_is_active()) {
+ MutexAutoLock lock(mMutex);
+ nsAutoCStringN<256> ssrcsCommaSeparated;
+ bool first = true;
+ for (auto ssrc : mSendStreamConfig.rtp.ssrcs) {
+ if (!first) {
+ ssrcsCommaSeparated.AppendASCII(", ");
+ } else {
+ first = false;
+ }
+ ssrcsCommaSeparated.AppendInt(ssrc);
+ }
+ // The first frame has a delta of zero.
+ uint64_t timestampDelta =
+ mLastTimestampSendUs.isSome()
+ ? aFrame.timestamp_us() - mLastTimestampSendUs.value()
+ : 0;
+ mLastTimestampSendUs = Some(aFrame.timestamp_us());
+ TRACE_COMMENT("VideoConduit::SendVideoFrame", "t-delta=%.1fms, ssrcs=%s",
+ timestampDelta / 1000.f, ssrcsCommaSeparated.get());
+ }
+#endif
+
+ mVideoBroadcaster.OnFrame(aFrame);
+
+ return kMediaConduitNoError;
+}
+
+// Transport Layer Callbacks
+
+void WebrtcVideoConduit::DeliverPacket(rtc::CopyOnWriteBuffer packet,
+ PacketType type) {
+ // Currently unused.
+ MOZ_ASSERT(false);
+}
+
+void WebrtcVideoConduit::OnRtpReceived(webrtc::RtpPacketReceived&& aPacket,
+ webrtc::RTPHeader&& aHeader) {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+
+ mRemoteSendSSRC = aHeader.ssrc;
+
+ if (mAllowSsrcChange || mRecvStreamConfig.rtp.remote_ssrc == 0) {
+ bool switchRequired = mRecvStreamConfig.rtp.remote_ssrc != aHeader.ssrc;
+ if (switchRequired) {
+ // Handle the unknown ssrc (and ssrc-not-signaled case).
+
+ // We need to check that the newly received ssrc is not already
+ // associated with ulpfec or rtx. This is how webrtc.org handles
+ // things, see https://codereview.webrtc.org/1226093002.
+ const webrtc::VideoReceiveStreamInterface::Config::Rtp& rtp =
+ mRecvStreamConfig.rtp;
+ switchRequired =
+ rtp.rtx_associated_payload_types.find(aHeader.payloadType) ==
+ rtp.rtx_associated_payload_types.end() &&
+ rtp.ulpfec_payload_type != aHeader.payloadType;
+ }
+
+ if (switchRequired) {
+ CSFLogInfo(LOGTAG, "VideoConduit %p: Switching remote SSRC from %u to %u",
+ this, mRecvStreamConfig.rtp.remote_ssrc, aHeader.ssrc);
+ SetRemoteSSRCAndRestartAsNeeded(aHeader.ssrc, 0);
+ }
+ }
+
+ CSFLogVerbose(LOGTAG, "%s: seq# %u, Len %zu, SSRC %u (0x%x) ", __FUNCTION__,
+ aPacket.SequenceNumber(), aPacket.size(), aPacket.Ssrc(),
+ aPacket.Ssrc());
+
+ mRtpPacketEvent.Notify();
+ if (mCall->Call()) {
+ mCall->Call()->Receiver()->DeliverRtpPacket(
+ webrtc::MediaType::VIDEO, std::move(aPacket),
+ [self = RefPtr<WebrtcVideoConduit>(this)](
+ const webrtc::RtpPacketReceived& packet) {
+ CSFLogVerbose(
+ LOGTAG,
+ "VideoConduit %p: failed demuxing packet, ssrc: %u seq: %u",
+ self.get(), packet.Ssrc(), packet.SequenceNumber());
+ return false;
+ });
+ }
+}
+
+void WebrtcVideoConduit::OnRtcpReceived(MediaPacket&& aPacket) {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+
+ CSFLogVerbose(LOGTAG, "VideoConduit %p: Received RTCP Packet, len %zu ", this,
+ aPacket.len());
+
+ if (mCall->Call()) {
+ mCall->Call()->Receiver()->DeliverRtcpPacket(
+ rtc::CopyOnWriteBuffer(aPacket.data(), aPacket.len()));
+ }
+}
+
+Maybe<uint16_t> WebrtcVideoConduit::RtpSendBaseSeqFor(uint32_t aSsrc) const {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ auto it = mRtpSendBaseSeqs.find(aSsrc);
+ if (it == mRtpSendBaseSeqs.end()) {
+ return Nothing();
+ }
+ return Some(it->second);
+}
+
+const dom::RTCStatsTimestampMaker& WebrtcVideoConduit::GetTimestampMaker()
+ const {
+ return mCall->GetTimestampMaker();
+}
+
+void WebrtcVideoConduit::StopTransmitting() {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ mMutex.AssertNotCurrentThreadOwns();
+
+ if (!mEngineTransmitting) {
+ return;
+ }
+
+ if (mSendStream) {
+ CSFLogDebug(LOGTAG, "%s Stopping send stream", __FUNCTION__);
+ mSendStream->Stop();
+ }
+
+ mEngineTransmitting = false;
+}
+
+void WebrtcVideoConduit::StartTransmitting() {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ MOZ_ASSERT(mSendStream);
+ mMutex.AssertNotCurrentThreadOwns();
+
+ if (mEngineTransmitting) {
+ return;
+ }
+
+ CSFLogDebug(LOGTAG, "%s Starting send stream", __FUNCTION__);
+
+ mSendStream->Start();
+ // XXX File a bug to consider hooking this up to the state of mtransport
+ mCall->Call()->SignalChannelNetworkState(webrtc::MediaType::VIDEO,
+ webrtc::kNetworkUp);
+ mEngineTransmitting = true;
+}
+
+void WebrtcVideoConduit::StopReceiving() {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ mMutex.AssertNotCurrentThreadOwns();
+
+ // Are we receiving already? If so, stop receiving and playout
+ // since we can't apply new recv codec when the engine is playing.
+ if (!mEngineReceiving) {
+ return;
+ }
+
+ if (mRecvStream) {
+ CSFLogDebug(LOGTAG, "%s Stopping receive stream", __FUNCTION__);
+ mRecvStream->Stop();
+ }
+
+ mEngineReceiving = false;
+}
+
+void WebrtcVideoConduit::StartReceiving() {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ MOZ_ASSERT(mRecvStream);
+ mMutex.AssertNotCurrentThreadOwns();
+
+ if (mEngineReceiving) {
+ return;
+ }
+
+ CSFLogDebug(LOGTAG, "%s Starting receive stream (SSRC %u (0x%x))",
+ __FUNCTION__, mRecvStreamConfig.rtp.remote_ssrc,
+ mRecvStreamConfig.rtp.remote_ssrc);
+ // Start Receiving on the video engine
+ mRecvStream->Start();
+
+ // XXX File a bug to consider hooking this up to the state of mtransport
+ mCall->Call()->SignalChannelNetworkState(webrtc::MediaType::VIDEO,
+ webrtc::kNetworkUp);
+ mEngineReceiving = true;
+}
+
+bool WebrtcVideoConduit::SendRtp(const uint8_t* aData, size_t aLength,
+ const webrtc::PacketOptions& aOptions) {
+ MOZ_ASSERT(aLength >= 12);
+ const uint16_t seqno = ntohs(*((uint16_t*)&aData[2]));
+ const uint32_t ssrc = ntohl(*((uint32_t*)&aData[8]));
+
+ CSFLogVerbose(
+ LOGTAG,
+ "VideoConduit %p: Sending RTP Packet seq# %u, len %zu, SSRC %u (0x%x)",
+ this, seqno, aLength, ssrc, ssrc);
+
+ if (!mTransportActive) {
+ CSFLogError(LOGTAG, "VideoConduit %p: RTP Packet Send Failed", this);
+ return false;
+ }
+
+ MediaPacket packet;
+ packet.Copy(aData, aLength, aLength + SRTP_MAX_EXPANSION);
+ packet.SetType(MediaPacket::RTP);
+ mSenderRtpSendEvent.Notify(std::move(packet));
+
+ // Parse the sequence number of the first rtp packet as base_seq.
+ const auto inserted = mRtpSendBaseSeqs_n.insert({ssrc, seqno}).second;
+
+ if (inserted || aOptions.packet_id >= 0) {
+ int64_t now_ms = PR_Now() / 1000;
+ MOZ_ALWAYS_SUCCEEDS(mCallThread->Dispatch(NS_NewRunnableFunction(
+ __func__, [this, self = RefPtr<WebrtcVideoConduit>(this),
+ packet_id = aOptions.packet_id, now_ms, ssrc, seqno] {
+ mRtpSendBaseSeqs.insert({ssrc, seqno});
+ if (packet_id >= 0) {
+ if (mCall->Call()) {
+ // TODO: This notification should ideally happen after the
+ // transport layer has sent the packet on the wire.
+ mCall->Call()->OnSentPacket({packet_id, now_ms});
+ }
+ }
+ })));
+ }
+ return true;
+}
+
+bool WebrtcVideoConduit::SendSenderRtcp(const uint8_t* aData, size_t aLength) {
+ CSFLogVerbose(
+ LOGTAG,
+ "VideoConduit %p: Sending RTCP SR Packet, len %zu, SSRC %u (0x%x)", this,
+ aLength, (uint32_t)ntohl(*((uint32_t*)&aData[4])),
+ (uint32_t)ntohl(*((uint32_t*)&aData[4])));
+
+ if (!mTransportActive) {
+ CSFLogError(LOGTAG, "VideoConduit %p: RTCP SR Packet Send Failed", this);
+ return false;
+ }
+
+ MediaPacket packet;
+ packet.Copy(aData, aLength, aLength + SRTP_MAX_EXPANSION);
+ packet.SetType(MediaPacket::RTCP);
+ mSenderRtcpSendEvent.Notify(std::move(packet));
+ return true;
+}
+
+bool WebrtcVideoConduit::SendReceiverRtcp(const uint8_t* aData,
+ size_t aLength) {
+ CSFLogVerbose(
+ LOGTAG,
+ "VideoConduit %p: Sending RTCP RR Packet, len %zu, SSRC %u (0x%x)", this,
+ aLength, (uint32_t)ntohl(*((uint32_t*)&aData[4])),
+ (uint32_t)ntohl(*((uint32_t*)&aData[4])));
+
+ if (!mTransportActive) {
+ CSFLogError(LOGTAG, "VideoConduit %p: RTCP RR Packet Send Failed", this);
+ return false;
+ }
+
+ MediaPacket packet;
+ packet.Copy(aData, aLength, aLength + SRTP_MAX_EXPANSION);
+ packet.SetType(MediaPacket::RTCP);
+ mReceiverRtcpSendEvent.Notify(std::move(packet));
+ return true;
+}
+
+void WebrtcVideoConduit::OnFrame(const webrtc::VideoFrame& video_frame) {
+ const uint32_t localRecvSsrc = mRecvSSRC;
+ const uint32_t remoteSendSsrc = mRemoteSendSSRC;
+
+ CSFLogVerbose(
+ LOGTAG,
+ "VideoConduit %p: Rendering frame, Remote SSRC %u (0x%x), size %ux%u",
+ this, static_cast<uint32_t>(remoteSendSsrc),
+ static_cast<uint32_t>(remoteSendSsrc), video_frame.width(),
+ video_frame.height());
+ ReentrantMonitorAutoEnter enter(mRendererMonitor);
+
+ if (!mRenderer) {
+ CSFLogError(LOGTAG, "VideoConduit %p: Cannot render frame, no renderer",
+ this);
+ return;
+ }
+
+ bool needsNewHistoryElement = mReceivedFrameHistory.mEntries.IsEmpty();
+
+ if (mReceivingWidth != video_frame.width() ||
+ mReceivingHeight != video_frame.height()) {
+ mReceivingWidth = video_frame.width();
+ mReceivingHeight = video_frame.height();
+ mRenderer->FrameSizeChange(mReceivingWidth, mReceivingHeight);
+ needsNewHistoryElement = true;
+ }
+
+ if (!needsNewHistoryElement) {
+ auto& currentEntry = mReceivedFrameHistory.mEntries.LastElement();
+ needsNewHistoryElement =
+ currentEntry.mRotationAngle !=
+ static_cast<unsigned long>(video_frame.rotation()) ||
+ currentEntry.mLocalSsrc != localRecvSsrc ||
+ currentEntry.mRemoteSsrc != remoteSendSsrc;
+ }
+
+ // Record frame history
+ const auto historyNow = mCall->GetTimestampMaker().GetNow().ToDom();
+ if (needsNewHistoryElement) {
+ dom::RTCVideoFrameHistoryEntryInternal frameHistoryElement;
+ frameHistoryElement.mConsecutiveFrames = 0;
+ frameHistoryElement.mWidth = video_frame.width();
+ frameHistoryElement.mHeight = video_frame.height();
+ frameHistoryElement.mRotationAngle =
+ static_cast<unsigned long>(video_frame.rotation());
+ frameHistoryElement.mFirstFrameTimestamp = historyNow;
+ frameHistoryElement.mLocalSsrc = localRecvSsrc;
+ frameHistoryElement.mRemoteSsrc = remoteSendSsrc;
+ if (!mReceivedFrameHistory.mEntries.AppendElement(frameHistoryElement,
+ fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ }
+ auto& currentEntry = mReceivedFrameHistory.mEntries.LastElement();
+
+ currentEntry.mConsecutiveFrames++;
+ currentEntry.mLastFrameTimestamp = historyNow;
+ // Attempt to retrieve an timestamp encoded in the image pixels if enabled.
+ if (mVideoLatencyTestEnable && mReceivingWidth && mReceivingHeight) {
+ uint64_t now = PR_Now();
+ uint64_t timestamp = 0;
+ uint8_t* data = const_cast<uint8_t*>(
+ video_frame.video_frame_buffer()->GetI420()->DataY());
+ bool ok = YuvStamper::Decode(
+ mReceivingWidth, mReceivingHeight, mReceivingWidth, data,
+ reinterpret_cast<unsigned char*>(&timestamp), sizeof(timestamp), 0, 0);
+ if (ok) {
+ VideoLatencyUpdate(now - timestamp);
+ }
+ }
+#ifdef MOZ_REAL_TIME_TRACING
+ if (profiler_is_active()) {
+ MutexAutoLock lock(mMutex);
+ // The first frame has a delta of zero.
+ uint32_t rtpTimestamp = video_frame.timestamp();
+ uint32_t timestampDelta =
+ mLastRTPTimestampReceive.isSome()
+ ? rtpTimestamp - mLastRTPTimestampReceive.value()
+ : 0;
+ mLastRTPTimestampReceive = Some(rtpTimestamp);
+ TRACE_COMMENT("VideoConduit::OnFrame", "t-delta=%.1fms, ssrc=%u",
+ timestampDelta * 1000.f / webrtc::kVideoPayloadTypeFrequency,
+ localRecvSsrc);
+ }
+#endif
+
+ mRenderer->RenderVideoFrame(*video_frame.video_frame_buffer(),
+ video_frame.timestamp(),
+ video_frame.render_time_ms());
+}
+
+bool WebrtcVideoConduit::AddFrameHistory(
+ dom::Sequence<dom::RTCVideoFrameHistoryInternal>* outHistories) const {
+ ReentrantMonitorAutoEnter enter(mRendererMonitor);
+ if (!outHistories->AppendElement(mReceivedFrameHistory, fallible)) {
+ mozalloc_handle_oom(0);
+ return false;
+ }
+ return true;
+}
+
+void WebrtcVideoConduit::SetJitterBufferTarget(DOMHighResTimeStamp aTargetMs) {
+ MOZ_RELEASE_ASSERT(aTargetMs <= std::numeric_limits<uint16_t>::max());
+ MOZ_RELEASE_ASSERT(aTargetMs >= 0);
+
+ MOZ_ALWAYS_SUCCEEDS(mCallThread->Dispatch(NS_NewRunnableFunction(
+ __func__,
+ [this, self = RefPtr<WebrtcVideoConduit>(this), targetMs = aTargetMs] {
+ mJitterBufferTargetMs = static_cast<uint16_t>(targetMs);
+ if (mRecvStream) {
+ mRecvStream->SetBaseMinimumPlayoutDelayMs(targetMs);
+ }
+ })));
+}
+
+void WebrtcVideoConduit::DumpCodecDB() const {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+
+ for (const auto& entry : mControl.mConfiguredRecvCodecs) {
+ CSFLogDebug(LOGTAG, "Payload Name: %s", entry.mName.c_str());
+ CSFLogDebug(LOGTAG, "Payload Type: %d", entry.mType);
+ CSFLogDebug(LOGTAG, "Payload Max Frame Size: %d",
+ entry.mEncodingConstraints.maxFs);
+ if (entry.mEncodingConstraints.maxFps.isSome()) {
+ CSFLogDebug(LOGTAG, "Payload Max Frame Rate: %f",
+ *entry.mEncodingConstraints.maxFps);
+ }
+ }
+}
+
+void WebrtcVideoConduit::VideoLatencyUpdate(uint64_t aNewSample) {
+ mRendererMonitor.AssertCurrentThreadIn();
+
+ mVideoLatencyAvg =
+ (sRoundingPadding * aNewSample + sAlphaNum * mVideoLatencyAvg) /
+ sAlphaDen;
+}
+
+uint64_t WebrtcVideoConduit::MozVideoLatencyAvg() {
+ mRendererMonitor.AssertCurrentThreadIn();
+
+ return mVideoLatencyAvg / sRoundingPadding;
+}
+
+void WebrtcVideoConduit::CollectTelemetryData() {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+
+ if (mEngineTransmitting) {
+ webrtc::VideoSendStream::Stats stats = mSendStream->GetStats();
+ mSendBitrate.Push(stats.media_bitrate_bps);
+ mSendFramerate.Push(stats.encode_frame_rate);
+ }
+ if (mEngineReceiving) {
+ webrtc::VideoReceiveStreamInterface::Stats stats = mRecvStream->GetStats();
+ mRecvBitrate.Push(stats.total_bitrate_bps);
+ mRecvFramerate.Push(stats.decode_frame_rate);
+ }
+}
+
+void WebrtcVideoConduit::OnRtcpBye() { mRtcpByeEvent.Notify(); }
+
+void WebrtcVideoConduit::OnRtcpTimeout() { mRtcpTimeoutEvent.Notify(); }
+
+void WebrtcVideoConduit::SetTransportActive(bool aActive) {
+ MOZ_ASSERT(mStsThread->IsOnCurrentThread());
+ if (mTransportActive == aActive) {
+ return;
+ }
+
+ // If false, This stops us from sending
+ mTransportActive = aActive;
+
+ // We queue this because there might be notifications to these listeners
+ // pending, and we don't want to drop them by letting this jump ahead of
+ // those notifications. We move the listeners into the lambda in case the
+ // transport comes back up before we disconnect them. (The Connect calls
+ // happen in MediaPipeline)
+ // We retain a strong reference to ourself, because the listeners are holding
+ // a non-refcounted reference to us, and moving them into the lambda could
+ // conceivably allow them to outlive us.
+ if (!aActive) {
+ MOZ_ALWAYS_SUCCEEDS(mCallThread->Dispatch(NS_NewRunnableFunction(
+ __func__,
+ [self = RefPtr<WebrtcVideoConduit>(this),
+ recvRtpListener = std::move(mReceiverRtpEventListener),
+ recvRtcpListener = std::move(mReceiverRtcpEventListener),
+ sendRtcpListener = std::move(mSenderRtcpEventListener)]() mutable {
+ recvRtpListener.DisconnectIfExists();
+ recvRtcpListener.DisconnectIfExists();
+ sendRtcpListener.DisconnectIfExists();
+ })));
+ }
+}
+
+std::vector<webrtc::RtpSource> WebrtcVideoConduit::GetUpstreamRtpSources()
+ const {
+ MOZ_ASSERT(NS_IsMainThread());
+ std::vector<webrtc::RtpSource> sources;
+ {
+ MutexAutoLock lock(mMutex);
+ if (mRecvStream) {
+ sources = mRecvStream->GetSources();
+ }
+ }
+ return sources;
+}
+
+bool WebrtcVideoConduit::HasCodecPluginID(uint64_t aPluginID) const {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ return mSendCodecPluginIDs.Contains(aPluginID) ||
+ mRecvCodecPluginIDs.Contains(aPluginID);
+}
+
+bool WebrtcVideoConduit::HasH264Hardware() {
+ nsCOMPtr<nsIGfxInfo> gfxInfo = do_GetService("@mozilla.org/gfx/info;1");
+ if (!gfxInfo) {
+ return false;
+ }
+ int32_t status;
+ nsCString discardFailureId;
+ return NS_SUCCEEDED(gfxInfo->GetFeatureStatus(
+ nsIGfxInfo::FEATURE_WEBRTC_HW_ACCELERATION_H264, discardFailureId,
+ &status)) &&
+ status == nsIGfxInfo::FEATURE_STATUS_OK;
+}
+
+Maybe<int> WebrtcVideoConduit::ActiveSendPayloadType() const {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+
+ if (!mSendStream) {
+ return Nothing();
+ }
+
+ if (mSendStreamConfig.rtp.payload_type == -1) {
+ return Nothing();
+ }
+
+ return Some(mSendStreamConfig.rtp.payload_type);
+}
+
+Maybe<int> WebrtcVideoConduit::ActiveRecvPayloadType() const {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+
+ auto stats = GetReceiverStats();
+ if (!stats) {
+ return Nothing();
+ }
+
+ if (stats->current_payload_type == -1) {
+ return Nothing();
+ }
+
+ return Some(stats->current_payload_type);
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/libwebrtcglue/VideoConduit.h b/dom/media/webrtc/libwebrtcglue/VideoConduit.h
new file mode 100644
index 0000000000..f46f1f962c
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/VideoConduit.h
@@ -0,0 +1,494 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef VIDEO_SESSION_H_
+#define VIDEO_SESSION_H_
+
+#include "mozilla/Atomics.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/DataMutex.h"
+#include "mozilla/ReentrantMonitor.h"
+#include "mozilla/SharedThreadPool.h"
+#include "mozilla/StateMirroring.h"
+#include "mozilla/UniquePtr.h"
+#include "nsITimer.h"
+
+#include "MediaConduitInterface.h"
+#include "common/MediaEngineWrapper.h"
+#include "RtpRtcpConfig.h"
+#include "RunningStat.h"
+#include "transport/runnable_utils.h"
+
+// conflicts with #include of scoped_ptr.h
+#undef FF
+// Video Engine Includes
+#include "api/video_codecs/video_decoder.h"
+#include "api/video_codecs/video_encoder.h"
+#include "api/video_codecs/sdp_video_format.h"
+#include "call/call_basic_stats.h"
+#include "common_video/include/video_frame_buffer_pool.h"
+#include "media/base/video_broadcaster.h"
+#include <functional>
+#include <memory>
+/** This file hosts several structures identifying different aspects
+ * of a RTP Session.
+ */
+
+namespace mozilla {
+
+// Convert (SI) kilobits/sec to (SI) bits/sec
+#define KBPS(kbps) kbps * 1000
+
+const int kViEMinCodecBitrate_bps = KBPS(30);
+const unsigned int kVideoMtu = 1200;
+const int kQpMax = 56;
+
+template <typename T>
+T MinIgnoreZero(const T& a, const T& b) {
+ return std::min(a ? a : b, b ? b : a);
+}
+
+class VideoStreamFactory;
+class WebrtcAudioConduit;
+
+// Interface of external video encoder for WebRTC.
+class WebrtcVideoEncoder : public VideoEncoder, public webrtc::VideoEncoder {};
+
+// Interface of external video decoder for WebRTC.
+class WebrtcVideoDecoder : public VideoDecoder, public webrtc::VideoDecoder {};
+
+/**
+ * Concrete class for Video session. Hooks up
+ * - media-source and target to external transport
+ */
+class WebrtcVideoConduit
+ : public VideoSessionConduit,
+ public webrtc::RtcpEventObserver,
+ public rtc::VideoSinkInterface<webrtc::VideoFrame>,
+ public rtc::VideoSourceInterface<webrtc::VideoFrame> {
+ public:
+ // Returns true when both encoder and decoder are HW accelerated.
+ static bool HasH264Hardware();
+
+ Maybe<int> ActiveSendPayloadType() const override;
+ Maybe<int> ActiveRecvPayloadType() const override;
+
+ /**
+ * Function to attach Renderer end-point for the Media-Video conduit.
+ * @param aRenderer : Reference to the concrete mozilla Video renderer
+ * implementation Note: Multiple invocations of this API shall remove an
+ * existing renderer and attaches the new to the Conduit.
+ */
+ MediaConduitErrorCode AttachRenderer(
+ RefPtr<mozilla::VideoRenderer> aVideoRenderer) override;
+ void DetachRenderer() override;
+
+ Maybe<uint16_t> RtpSendBaseSeqFor(uint32_t aSsrc) const override;
+
+ const dom::RTCStatsTimestampMaker& GetTimestampMaker() const override;
+
+ void StopTransmitting();
+ void StartTransmitting();
+ void StopReceiving();
+ void StartReceiving();
+
+ /**
+ * Function to deliver a capture video frame for encoding and transport.
+ * If the frame's timestamp is 0, it will be automatically generated.
+ *
+ * NOTE: ConfigureSendMediaCodec() must be called before this function can
+ * be invoked. This ensures the inserted video-frames can be
+ * transmitted by the conduit.
+ */
+ MediaConduitErrorCode SendVideoFrame(webrtc::VideoFrame aFrame) override;
+
+ bool SendRtp(const uint8_t* aData, size_t aLength,
+ const webrtc::PacketOptions& aOptions) override;
+ bool SendSenderRtcp(const uint8_t* aData, size_t aLength) override;
+ bool SendReceiverRtcp(const uint8_t* aData, size_t aLength) override;
+
+ /*
+ * webrtc:VideoSinkInterface implementation
+ * -------------------------------
+ */
+ void OnFrame(const webrtc::VideoFrame& frame) override;
+
+ /*
+ * webrtc:VideoSourceInterface implementation
+ * -------------------------------
+ */
+ void AddOrUpdateSink(rtc::VideoSinkInterface<webrtc::VideoFrame>* sink,
+ const rtc::VideoSinkWants& wants) override;
+ void RemoveSink(rtc::VideoSinkInterface<webrtc::VideoFrame>* sink) override;
+
+ bool HasCodecPluginID(uint64_t aPluginID) const override;
+
+ RefPtr<GenericPromise> Shutdown() override;
+
+ bool Denoising() const { return mDenoising; }
+
+ uint8_t SpatialLayers() const { return mSpatialLayers; }
+
+ uint8_t TemporalLayers() const { return mTemporalLayers; }
+
+ webrtc::VideoCodecMode CodecMode() const;
+
+ WebrtcVideoConduit(RefPtr<WebrtcCallWrapper> aCall,
+ nsCOMPtr<nsISerialEventTarget> aStsThread,
+ Options aOptions, std::string aPCHandle,
+ const TrackingId& aRecvTrackingId);
+ virtual ~WebrtcVideoConduit();
+
+ // Call thread.
+ void InitControl(VideoConduitControlInterface* aControl) override;
+
+ // Called when a parameter in mControl has changed. Call thread.
+ void OnControlConfigChange();
+
+ // Necessary Init steps on main thread.
+ MediaConduitErrorCode Init();
+
+ Ssrcs GetLocalSSRCs() const override;
+ Maybe<Ssrc> GetAssociatedLocalRtxSSRC(Ssrc aSsrc) const override;
+ Maybe<Ssrc> GetRemoteSSRC() const override;
+
+ // Call thread.
+ void UnsetRemoteSSRC(uint32_t aSsrc) override;
+
+ static unsigned ToLibwebrtcMaxFramerate(const Maybe<double>& aMaxFramerate);
+
+ private:
+ void NotifyUnsetCurrentRemoteSSRC();
+ void SetRemoteSSRCConfig(uint32_t aSsrc, uint32_t aRtxSsrc);
+ void SetRemoteSSRCAndRestartAsNeeded(uint32_t aSsrc, uint32_t aRtxSsrc);
+ rtc::RefCountedObject<mozilla::VideoStreamFactory>*
+ CreateVideoStreamFactory();
+
+ public:
+ // Creating a recv stream or a send stream requires a local ssrc to be
+ // configured. This method will generate one if needed.
+ void EnsureLocalSSRC();
+ // Creating a recv stream requires a remote ssrc to be configured. This method
+ // will generate one if needed.
+ void EnsureRemoteSSRC();
+
+ Maybe<webrtc::VideoReceiveStreamInterface::Stats> GetReceiverStats()
+ const override;
+ Maybe<webrtc::VideoSendStream::Stats> GetSenderStats() const override;
+ Maybe<webrtc::CallBasicStats> GetCallStats() const override;
+
+ bool AddFrameHistory(dom::Sequence<dom::RTCVideoFrameHistoryInternal>*
+ outHistories) const override;
+
+ void SetJitterBufferTarget(DOMHighResTimeStamp aTargetMs) override;
+
+ uint64_t MozVideoLatencyAvg();
+
+ void DisableSsrcChanges() override {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ mAllowSsrcChange = false;
+ }
+
+ void CollectTelemetryData() override;
+
+ void OnRtpReceived(webrtc::RtpPacketReceived&& aPacket,
+ webrtc::RTPHeader&& aHeader);
+ void OnRtcpReceived(MediaPacket&& aPacket);
+
+ void OnRtcpBye() override;
+ void OnRtcpTimeout() override;
+
+ void SetTransportActive(bool aActive) override;
+
+ MediaEventSourceExc<MediaPacket>& SenderRtpSendEvent() override {
+ return mSenderRtpSendEvent;
+ }
+ MediaEventSourceExc<MediaPacket>& SenderRtcpSendEvent() override {
+ return mSenderRtcpSendEvent;
+ }
+ MediaEventSourceExc<MediaPacket>& ReceiverRtcpSendEvent() override {
+ return mReceiverRtcpSendEvent;
+ }
+ void ConnectReceiverRtpEvent(
+ MediaEventSourceExc<webrtc::RtpPacketReceived, webrtc::RTPHeader>& aEvent)
+ override {
+ mReceiverRtpEventListener =
+ aEvent.Connect(mCallThread, this, &WebrtcVideoConduit::OnRtpReceived);
+ }
+ void ConnectReceiverRtcpEvent(
+ MediaEventSourceExc<MediaPacket>& aEvent) override {
+ mReceiverRtcpEventListener =
+ aEvent.Connect(mCallThread, this, &WebrtcVideoConduit::OnRtcpReceived);
+ }
+ void ConnectSenderRtcpEvent(
+ MediaEventSourceExc<MediaPacket>& aEvent) override {
+ mSenderRtcpEventListener =
+ aEvent.Connect(mCallThread, this, &WebrtcVideoConduit::OnRtcpReceived);
+ }
+
+ std::vector<webrtc::RtpSource> GetUpstreamRtpSources() const override;
+
+ private:
+ // Don't allow copying/assigning.
+ WebrtcVideoConduit(const WebrtcVideoConduit&) = delete;
+ void operator=(const WebrtcVideoConduit&) = delete;
+
+ // Utility function to dump recv codec database
+ void DumpCodecDB() const;
+
+ // Video Latency Test averaging filter
+ void VideoLatencyUpdate(uint64_t aNewSample);
+
+ void CreateSendStream();
+ void DeleteSendStream();
+ void CreateRecvStream();
+ void DeleteRecvStream();
+
+ void DeliverPacket(rtc::CopyOnWriteBuffer packet, PacketType type) override;
+
+ MediaEventSource<void>& RtcpByeEvent() override { return mRtcpByeEvent; }
+ MediaEventSource<void>& RtcpTimeoutEvent() override {
+ return mRtcpTimeoutEvent;
+ }
+ MediaEventSource<void>& RtpPacketEvent() override { return mRtpPacketEvent; }
+
+ bool RequiresNewSendStream(const VideoCodecConfig& newConfig) const;
+
+ mutable mozilla::ReentrantMonitor mRendererMonitor MOZ_UNANNOTATED;
+
+ // Accessed on any thread under mRendererMonitor.
+ RefPtr<mozilla::VideoRenderer> mRenderer;
+
+ // Accessed on any thread under mRendererMonitor.
+ unsigned short mReceivingWidth = 0;
+
+ // Accessed on any thread under mRendererMonitor.
+ unsigned short mReceivingHeight = 0;
+
+ // Call worker thread. All access to mCall->Call() happens here.
+ const nsCOMPtr<nsISerialEventTarget> mCallThread;
+
+ // Socket transport service thread that runs stats queries against us. Any
+ // thread.
+ const nsCOMPtr<nsISerialEventTarget> mStsThread;
+
+ // Thread on which we are fed video frames. Set lazily on first call to
+ // SendVideoFrame().
+ nsCOMPtr<nsISerialEventTarget> mFrameSendingThread;
+
+ struct Control {
+ // Mirrors that map to VideoConduitControlInterface for control. Call thread
+ // only.
+ Mirror<bool> mReceiving;
+ Mirror<bool> mTransmitting;
+ Mirror<Ssrcs> mLocalSsrcs;
+ Mirror<Ssrcs> mLocalRtxSsrcs;
+ Mirror<std::string> mLocalCname;
+ Mirror<std::string> mMid;
+ Mirror<Ssrc> mRemoteSsrc;
+ Mirror<Ssrc> mRemoteRtxSsrc;
+ Mirror<std::string> mSyncGroup;
+ Mirror<RtpExtList> mLocalRecvRtpExtensions;
+ Mirror<RtpExtList> mLocalSendRtpExtensions;
+ Mirror<Maybe<VideoCodecConfig>> mSendCodec;
+ Mirror<Maybe<RtpRtcpConfig>> mSendRtpRtcpConfig;
+ Mirror<std::vector<VideoCodecConfig>> mRecvCodecs;
+ Mirror<Maybe<RtpRtcpConfig>> mRecvRtpRtcpConfig;
+ Mirror<webrtc::VideoCodecMode> mCodecMode;
+
+ // For caching mRemoteSsrc and mRemoteRtxSsrc, since another caller may
+ // change the remote ssrc in the stream config directly.
+ Ssrc mConfiguredRemoteSsrc = 0;
+ Ssrc mConfiguredRemoteRtxSsrc = 0;
+ // For tracking changes to mSendCodec and mSendRtpRtcpConfig.
+ Maybe<VideoCodecConfig> mConfiguredSendCodec;
+ Maybe<RtpRtcpConfig> mConfiguredSendRtpRtcpConfig;
+ // For tracking changes to mRecvCodecs and mRecvRtpRtcpConfig.
+ std::vector<VideoCodecConfig> mConfiguredRecvCodecs;
+ Maybe<RtpRtcpConfig> mConfiguredRecvRtpRtcpConfig;
+
+ Control() = delete;
+ explicit Control(const RefPtr<AbstractThread>& aCallThread);
+ } mControl;
+
+ // WatchManager allowing Mirrors and other watch targets to trigger functions
+ // that will update the webrtc.org configuration.
+ WatchManager<WebrtcVideoConduit> mWatchManager;
+
+ mutable Mutex mMutex MOZ_UNANNOTATED;
+
+ // Decoder factory used by mRecvStream when it needs new decoders. This is
+ // not shared broader like some state in the WebrtcCallWrapper because it
+ // handles CodecPluginID plumbing tied to this VideoConduit.
+ const UniquePtr<WebrtcVideoDecoderFactory> mDecoderFactory;
+
+ // Encoder factory used by mSendStream when it needs new encoders. This is
+ // not shared broader like some state in the WebrtcCallWrapper because it
+ // handles CodecPluginID plumbing tied to this VideoConduit.
+ const UniquePtr<WebrtcVideoEncoderFactory> mEncoderFactory;
+
+ // Our own record of the sinks added to mVideoBroadcaster so we can support
+ // dispatching updates to sinks from off-Call-thread. Call thread only.
+ AutoTArray<rtc::VideoSinkInterface<webrtc::VideoFrame>*, 1> mRegisteredSinks;
+
+ // Broadcaster that distributes our frames to all registered sinks.
+ // Threadsafe.
+ rtc::VideoBroadcaster mVideoBroadcaster;
+
+ // Buffer pool used for scaling frames.
+ // Accessed on the frame-feeding thread only.
+ webrtc::VideoFrameBufferPool mBufferPool;
+
+ // Engine state we are concerned with. Written on the Call thread and read
+ // anywhere.
+ mozilla::Atomic<bool>
+ mEngineTransmitting; // If true ==> Transmit Subsystem is up and running
+ mozilla::Atomic<bool>
+ mEngineReceiving; // if true ==> Receive Subsystem up and running
+
+ // Written only on the Call thread. Guarded by mMutex, except for reads on the
+ // Call thread.
+ Maybe<VideoCodecConfig> mCurSendCodecConfig;
+
+ // Bookkeeping of stats for telemetry. Call thread only.
+ RunningStat mSendFramerate;
+ RunningStat mSendBitrate;
+ RunningStat mRecvFramerate;
+ RunningStat mRecvBitrate;
+
+ // Must call webrtc::Call::DestroyVideoReceive/SendStream to delete this.
+ // Written only on the Call thread. Guarded by mMutex, except for reads on the
+ // Call thread.
+ webrtc::VideoReceiveStreamInterface* mRecvStream = nullptr;
+
+ // Must call webrtc::Call::DestroyVideoReceive/SendStream to delete this.
+ webrtc::VideoSendStream* mSendStream = nullptr;
+
+ // Written on the frame feeding thread.
+ // Guarded by mMutex, except for reads on the frame feeding thread.
+ unsigned short mLastWidth = 0;
+
+ // Written on the frame feeding thread.
+ // Guarded by mMutex, except for reads on the frame feeding thread.
+ unsigned short mLastHeight = 0;
+
+ // Written on the frame feeding thread, the timestamp of the last frame on the
+ // send side, in microseconds. This is a local timestamp using the system
+ // clock with a unspecified epoch (Like mozilla::TimeStamp).
+ // Guarded by mMutex.
+ Maybe<uint64_t> mLastTimestampSendUs;
+
+ // Written on the frame receive thread, the rtp timestamp of the last frame
+ // on the receive side, in 90kHz base. This comes from the RTP packet.
+ // Guarded by mMutex.
+ Maybe<uint32_t> mLastRTPTimestampReceive;
+
+ // Accessed from any thread under mRendererMonitor.
+ uint64_t mVideoLatencyAvg = 0;
+
+ const bool mVideoLatencyTestEnable;
+
+ // All in bps.
+ const int mMinBitrate;
+ const int mStartBitrate;
+ const int mPrefMaxBitrate;
+ const int mMinBitrateEstimate;
+
+ // Max bitrate in bps as provided by negotiation. Call thread only.
+ int mNegotiatedMaxBitrate = 0;
+
+ // Set to true to force denoising on.
+ const bool mDenoising;
+
+ // Set to true to ignore sink wants (scaling due to bwe and cpu usage).
+ const bool mLockScaling;
+
+ const uint8_t mSpatialLayers;
+ const uint8_t mTemporalLayers;
+
+ static const unsigned int sAlphaNum = 7;
+ static const unsigned int sAlphaDen = 8;
+ static const unsigned int sRoundingPadding = 1024;
+
+ // Target jitter buffer to be applied to the receive stream in milliseconds.
+ uint16_t mJitterBufferTargetMs = 0;
+
+ // WEBRTC.ORG Call API
+ // Const so can be accessed on any thread. All methods are called on the Call
+ // thread.
+ const RefPtr<WebrtcCallWrapper> mCall;
+
+ // Set up in the ctor and then not touched. Called through by the streams on
+ // any thread. Safe since we own and control the lifetime of the streams.
+ WebrtcSendTransport mSendTransport;
+ WebrtcReceiveTransport mRecvTransport;
+
+ // Written only on the Call thread. Guarded by mMutex, except for reads on the
+ // Call thread. Typical non-Call thread access is on the frame delivery
+ // thread.
+ webrtc::VideoSendStream::Config mSendStreamConfig;
+
+ // Call thread only.
+ webrtc::VideoEncoderConfig mEncoderConfig;
+
+ // Written only on the Call thread. Guarded by mMutex, except for reads on the
+ // Call thread. Calls can happen under mMutex on any thread.
+ DataMutex<RefPtr<rtc::RefCountedObject<VideoStreamFactory>>>
+ mVideoStreamFactory;
+
+ // Call thread only.
+ webrtc::VideoReceiveStreamInterface::Config mRecvStreamConfig;
+
+ // Are SSRC changes without signaling allowed or not.
+ // Call thread only.
+ bool mAllowSsrcChange = true;
+
+ // Accessed during configuration/signaling (Call thread), and on the frame
+ // delivery thread for frame history tracking. Set only on the Call thread.
+ Atomic<uint32_t> mRecvSSRC =
+ Atomic<uint32_t>(0); // this can change during a stream!
+
+ // Accessed from both the STS and frame delivery thread for frame history
+ // tracking. Set when receiving packets.
+ Atomic<uint32_t> mRemoteSendSSRC =
+ Atomic<uint32_t>(0); // this can change during a stream!
+
+ // Main thread only
+ nsTArray<uint64_t> mSendCodecPluginIDs;
+ // Main thread only
+ nsTArray<uint64_t> mRecvCodecPluginIDs;
+
+ // Main thread only
+ MediaEventListener mSendPluginCreated;
+ MediaEventListener mSendPluginReleased;
+ MediaEventListener mRecvPluginCreated;
+ MediaEventListener mRecvPluginReleased;
+
+ // Call thread only. ssrc -> base_seq
+ std::map<uint32_t, uint16_t> mRtpSendBaseSeqs;
+ // libwebrtc network thread only. ssrc -> base_seq.
+ // To track changes needed to mRtpSendBaseSeqs.
+ std::map<uint32_t, uint16_t> mRtpSendBaseSeqs_n;
+
+ // Tracking the attributes of received frames over time
+ // Protected by mRendererMonitor
+ dom::RTCVideoFrameHistoryInternal mReceivedFrameHistory;
+
+ // Thread safe
+ Atomic<bool> mTransportActive = Atomic<bool>(false);
+ MediaEventProducer<void> mRtcpByeEvent;
+ MediaEventProducer<void> mRtcpTimeoutEvent;
+ MediaEventProducer<void> mRtpPacketEvent;
+ MediaEventProducerExc<MediaPacket> mSenderRtpSendEvent;
+ MediaEventProducerExc<MediaPacket> mSenderRtcpSendEvent;
+ MediaEventProducerExc<MediaPacket> mReceiverRtcpSendEvent;
+
+ // Assigned and revoked on mStsThread. Listeners for receiving packets.
+ MediaEventListener mSenderRtcpEventListener; // Rtp-transmitting pipeline
+ MediaEventListener mReceiverRtcpEventListener; // Rtp-receiving pipeline
+ MediaEventListener mReceiverRtpEventListener; // Rtp-receiving pipeline
+};
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/libwebrtcglue/VideoStreamFactory.cpp b/dom/media/webrtc/libwebrtcglue/VideoStreamFactory.cpp
new file mode 100644
index 0000000000..9782f8760b
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/VideoStreamFactory.cpp
@@ -0,0 +1,387 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#include "VideoStreamFactory.h"
+
+#include "common/browser_logging/CSFLog.h"
+#include "nsThreadUtils.h"
+#include "VideoConduit.h"
+
+template <class t>
+void ConstrainPreservingAspectRatio(uint16_t aMaxWidth, uint16_t aMaxHeight,
+ t* aWidth, t* aHeight) {
+ if (((*aWidth) <= aMaxWidth) && ((*aHeight) <= aMaxHeight)) {
+ return;
+ }
+
+ if ((*aWidth) * aMaxHeight > aMaxWidth * (*aHeight)) {
+ (*aHeight) = aMaxWidth * (*aHeight) / (*aWidth);
+ (*aWidth) = aMaxWidth;
+ } else {
+ (*aWidth) = aMaxHeight * (*aWidth) / (*aHeight);
+ (*aHeight) = aMaxHeight;
+ }
+}
+
+namespace mozilla {
+
+#ifdef LOGTAG
+# undef LOGTAG
+#endif
+#define LOGTAG "WebrtcVideoSessionConduit"
+
+#define DEFAULT_VIDEO_MAX_FRAMERATE 30u
+
+#define MB_OF(w, h) \
+ ((unsigned int)((((w + 15) >> 4)) * ((unsigned int)((h + 15) >> 4))))
+// For now, try to set the max rates well above the knee in the curve.
+// Chosen somewhat arbitrarily; it's hard to find good data oriented for
+// realtime interactive/talking-head recording. These rates assume
+// 30fps.
+
+// XXX Populate this based on a pref (which we should consider sorting because
+// people won't assume they need to).
+static VideoStreamFactory::ResolutionAndBitrateLimits
+ kResolutionAndBitrateLimits[] = {
+ // clang-format off
+ {MB_OF(1920, 1200), KBPS(1500), KBPS(2000), KBPS(10000)}, // >HD (3K, 4K, etc)
+ {MB_OF(1280, 720), KBPS(1200), KBPS(1500), KBPS(5000)}, // HD ~1080-1200
+ {MB_OF(800, 480), KBPS(200), KBPS(800), KBPS(2500)}, // HD ~720
+ {MB_OF(480, 270), KBPS(150), KBPS(500), KBPS(2000)}, // WVGA
+ {tl::Max<MB_OF(400, 240), MB_OF(352, 288)>::value, KBPS(125), KBPS(300), KBPS(1300)}, // VGA
+ {MB_OF(176, 144), KBPS(100), KBPS(150), KBPS(500)}, // WQVGA, CIF
+ {0 , KBPS(40), KBPS(80), KBPS(250)} // QCIF and below
+ // clang-format on
+};
+
+auto VideoStreamFactory::GetLimitsFor(unsigned int aWidth, unsigned int aHeight,
+ int aCapBps /* = 0 */)
+ -> ResolutionAndBitrateLimits {
+ // max bandwidth should be proportional (not linearly!) to resolution, and
+ // proportional (perhaps linearly, or close) to current frame rate.
+ int fs = MB_OF(aWidth, aHeight);
+
+ for (const auto& resAndLimits : kResolutionAndBitrateLimits) {
+ if (fs > resAndLimits.resolution_in_mb &&
+ // pick the highest range where at least start rate is within cap
+ // (or if we're at the end of the array).
+ (aCapBps == 0 || resAndLimits.start_bitrate_bps <= aCapBps ||
+ resAndLimits.resolution_in_mb == 0)) {
+ return resAndLimits;
+ }
+ }
+
+ MOZ_CRASH("Loop should have handled fallback");
+}
+
+/**
+ * Function to set the encoding bitrate limits based on incoming frame size and
+ * rate
+ * @param width, height: dimensions of the frame
+ * @param min: minimum bitrate in bps
+ * @param start: bitrate in bps that the encoder should start with
+ * @param cap: user-enforced max bitrate, or 0
+ * @param pref_cap: cap enforced by prefs
+ * @param negotiated_cap: cap negotiated through SDP
+ * @param aVideoStream stream to apply bitrates to
+ */
+static void SelectBitrates(unsigned short width, unsigned short height, int min,
+ int start, int cap, int pref_cap, int negotiated_cap,
+ webrtc::VideoStream& aVideoStream) {
+ int& out_min = aVideoStream.min_bitrate_bps;
+ int& out_start = aVideoStream.target_bitrate_bps;
+ int& out_max = aVideoStream.max_bitrate_bps;
+
+ VideoStreamFactory::ResolutionAndBitrateLimits resAndLimits =
+ VideoStreamFactory::GetLimitsFor(width, height);
+ out_min = MinIgnoreZero(resAndLimits.min_bitrate_bps, cap);
+ out_start = MinIgnoreZero(resAndLimits.start_bitrate_bps, cap);
+ out_max = MinIgnoreZero(resAndLimits.max_bitrate_bps, cap);
+
+ // Note: negotiated_cap is the max transport bitrate - it applies to
+ // a single codec encoding, but should also apply to the sum of all
+ // simulcast layers in this encoding! So sum(layers.maxBitrate) <=
+ // negotiated_cap
+ // Note that out_max already has had pref_cap applied to it
+ out_max = MinIgnoreZero(negotiated_cap, out_max);
+ out_min = std::min(out_min, out_max);
+ out_start = std::min(out_start, out_max);
+
+ if (min && min > out_min) {
+ out_min = min;
+ }
+ // If we try to set a minimum bitrate that is too low, ViE will reject it.
+ out_min = std::max(kViEMinCodecBitrate_bps, out_min);
+ out_max = std::max(kViEMinCodecBitrate_bps, out_max);
+ if (start && start > out_start) {
+ out_start = start;
+ }
+
+ // Ensure that min <= start <= max
+ if (out_min > out_max) {
+ out_min = out_max;
+ }
+ out_start = std::min(out_max, std::max(out_start, out_min));
+
+ MOZ_ASSERT(pref_cap == 0 || out_max <= pref_cap);
+}
+
+std::vector<webrtc::VideoStream> VideoStreamFactory::CreateEncoderStreams(
+ int aWidth, int aHeight, const webrtc::VideoEncoderConfig& aConfig) {
+ // We only allow one layer when screensharing
+ const size_t streamCount =
+ mCodecMode == webrtc::VideoCodecMode::kScreensharing
+ ? 1
+ : aConfig.number_of_streams;
+
+ MOZ_RELEASE_ASSERT(streamCount >= 1, "Should request at least one stream");
+
+ std::vector<webrtc::VideoStream> streams;
+ streams.reserve(streamCount);
+
+ {
+ auto frameRateController = mFramerateController.Lock();
+ frameRateController->Reset();
+ }
+
+ for (int idx = streamCount - 1; idx >= 0; --idx) {
+ webrtc::VideoStream video_stream;
+ auto& encoding = mCodecConfig.mEncodings[idx];
+ video_stream.active = encoding.active;
+ MOZ_ASSERT(encoding.constraints.scaleDownBy >= 1.0);
+
+ gfx::IntSize newSize(0, 0);
+
+ if (aWidth && aHeight) {
+ auto maxPixelCount = mLockScaling ? 0U : mWants.max_pixel_count;
+ newSize = CalculateScaledResolution(
+ aWidth, aHeight, encoding.constraints.scaleDownBy, maxPixelCount);
+ }
+
+ if (newSize.width == 0 || newSize.height == 0) {
+ CSFLogInfo(LOGTAG,
+ "%s Stream with RID %s ignored because of no resolution.",
+ __FUNCTION__, encoding.rid.c_str());
+ continue;
+ }
+
+ uint16_t max_width = mCodecConfig.mEncodingConstraints.maxWidth;
+ uint16_t max_height = mCodecConfig.mEncodingConstraints.maxHeight;
+ if (max_width || max_height) {
+ max_width = max_width ? max_width : UINT16_MAX;
+ max_height = max_height ? max_height : UINT16_MAX;
+ ConstrainPreservingAspectRatio(max_width, max_height, &newSize.width,
+ &newSize.height);
+ }
+
+ MOZ_ASSERT(newSize.width > 0);
+ MOZ_ASSERT(newSize.height > 0);
+ video_stream.width = newSize.width;
+ video_stream.height = newSize.height;
+ SelectMaxFramerateForAllStreams(newSize.width, newSize.height);
+
+ CSFLogInfo(LOGTAG, "%s Input frame %ux%u, RID %s scaling to %zux%zu",
+ __FUNCTION__, aWidth, aHeight, encoding.rid.c_str(),
+ video_stream.width, video_stream.height);
+
+ // mMaxFramerateForAllStreams is based on codec-wide stuff like fmtp, and
+ // hard-coded limits based on the source resolution.
+ // mCodecConfig.mEncodingConstraints.maxFps does not take the hard-coded
+ // limits into account, so we have mMaxFramerateForAllStreams which
+ // incorporates those. Per-encoding max framerate is based on parameters
+ // from JS, and maybe rid
+ unsigned int max_framerate = SelectFrameRate(
+ mMaxFramerateForAllStreams, video_stream.width, video_stream.height);
+ max_framerate = std::min(WebrtcVideoConduit::ToLibwebrtcMaxFramerate(
+ encoding.constraints.maxFps),
+ max_framerate);
+ if (max_framerate >= std::numeric_limits<int>::max()) {
+ // If nothing has specified any kind of limit (uncommon), pick something
+ // reasonable.
+ max_framerate = DEFAULT_VIDEO_MAX_FRAMERATE;
+ }
+ video_stream.max_framerate = static_cast<int>(max_framerate);
+ CSFLogInfo(LOGTAG, "%s Stream with RID %s maxFps=%d (global max fps = %u)",
+ __FUNCTION__, encoding.rid.c_str(), video_stream.max_framerate,
+ (unsigned)mMaxFramerateForAllStreams);
+
+ SelectBitrates(video_stream.width, video_stream.height, mMinBitrate,
+ mStartBitrate, encoding.constraints.maxBr, mPrefMaxBitrate,
+ mNegotiatedMaxBitrate, video_stream);
+
+ video_stream.bitrate_priority = aConfig.bitrate_priority;
+ video_stream.max_qp = kQpMax;
+
+ if (streamCount > 1) {
+ if (mCodecMode == webrtc::VideoCodecMode::kScreensharing) {
+ video_stream.num_temporal_layers = 1;
+ } else {
+ video_stream.num_temporal_layers = 2;
+ }
+ // XXX Bug 1390215 investigate using more of
+ // simulcast.cc:GetSimulcastConfig() or our own algorithm to replace it
+ }
+
+ if (mCodecConfig.mName == "H264") {
+ if (mCodecConfig.mEncodingConstraints.maxMbps > 0) {
+ // Not supported yet!
+ CSFLogError(LOGTAG, "%s H.264 max_mbps not supported yet",
+ __FUNCTION__);
+ }
+ }
+ streams.push_back(video_stream);
+ }
+
+ MOZ_RELEASE_ASSERT(streams.size(), "Should configure at least one stream");
+ return streams;
+}
+
+gfx::IntSize VideoStreamFactory::CalculateScaledResolution(
+ int aWidth, int aHeight, double aScaleDownByResolution,
+ unsigned int aMaxPixelCount) {
+ // If any adjustments like scaleResolutionDownBy or maxFS are being given
+ // we want to choose a height and width here to provide for more variety
+ // in possible resolutions.
+ int width = aWidth;
+ int height = aHeight;
+
+ if (aScaleDownByResolution > 1) {
+ width = static_cast<int>(aWidth / aScaleDownByResolution);
+ height = static_cast<int>(aHeight / aScaleDownByResolution);
+ }
+
+ // Check if we still need to adjust resolution down more due to other
+ // constraints.
+ if (mCodecConfig.mEncodingConstraints.maxFs > 0 || aMaxPixelCount > 0) {
+ auto currentFs = static_cast<unsigned int>(width * height);
+ auto maxFs =
+ (mCodecConfig.mEncodingConstraints.maxFs > 0 && aMaxPixelCount > 0)
+ ? std::min((mCodecConfig.mEncodingConstraints.maxFs * 16 * 16),
+ aMaxPixelCount)
+ : std::max((mCodecConfig.mEncodingConstraints.maxFs * 16 * 16),
+ aMaxPixelCount);
+
+ // If our currentFs is greater than maxFs we calculate a width and height
+ // that will get as close as possible to maxFs and try to maintain aspect
+ // ratio.
+ if (currentFs > maxFs) {
+ if (aWidth > aHeight) { // Landscape
+ auto aspectRatio = static_cast<double>(aWidth) / aHeight;
+
+ height = static_cast<int>(std::sqrt(maxFs / aspectRatio));
+ width = static_cast<int>(height * aspectRatio);
+ } else { // Portrait
+ auto aspectRatio = static_cast<double>(aHeight) / aWidth;
+
+ width = static_cast<int>(std::sqrt(maxFs / aspectRatio));
+ height = static_cast<int>(width * aspectRatio);
+ }
+ }
+ }
+
+ // Simplest possible adaptation to resolution alignment.
+ width -= width % mWants.resolution_alignment;
+ height -= height % mWants.resolution_alignment;
+
+ // Dont scale below our minimum value to prevent problems.
+ const int minSize = 1;
+ if (width < minSize || height < minSize) {
+ width = minSize;
+ height = minSize;
+ }
+
+ return gfx::IntSize(width, height);
+}
+
+void VideoStreamFactory::SelectMaxFramerateForAllStreams(
+ unsigned short aWidth, unsigned short aHeight) {
+ int max_fs = std::numeric_limits<int>::max();
+ if (!mLockScaling) {
+ max_fs = mWants.max_pixel_count;
+ }
+ // Limit resolution to max-fs
+ if (mCodecConfig.mEncodingConstraints.maxFs) {
+ // max-fs is in macroblocks, convert to pixels
+ max_fs = std::min(
+ max_fs,
+ static_cast<int>(mCodecConfig.mEncodingConstraints.maxFs * (16 * 16)));
+ }
+
+ unsigned int framerate_all_streams =
+ SelectFrameRate(mMaxFramerateForAllStreams, aWidth, aHeight);
+ unsigned int maxFrameRate = mMaxFramerateForAllStreams;
+ if (mMaxFramerateForAllStreams != framerate_all_streams) {
+ CSFLogDebug(LOGTAG, "%s: framerate changing to %u (from %u)", __FUNCTION__,
+ framerate_all_streams, maxFrameRate);
+ mMaxFramerateForAllStreams = framerate_all_streams;
+ }
+
+ int framerate_with_wants;
+ if (framerate_all_streams > std::numeric_limits<int>::max()) {
+ framerate_with_wants = std::numeric_limits<int>::max();
+ } else {
+ framerate_with_wants = static_cast<int>(framerate_all_streams);
+ }
+
+ framerate_with_wants =
+ std::min(framerate_with_wants, mWants.max_framerate_fps);
+ CSFLogDebug(LOGTAG,
+ "%s: Calling OnOutputFormatRequest, max_fs=%d, max_fps=%d",
+ __FUNCTION__, max_fs, framerate_with_wants);
+ auto frameRateController = mFramerateController.Lock();
+ frameRateController->SetMaxFramerate(framerate_with_wants);
+}
+
+unsigned int VideoStreamFactory::SelectFrameRate(
+ unsigned int aOldFramerate, unsigned short aSendingWidth,
+ unsigned short aSendingHeight) {
+ unsigned int new_framerate = aOldFramerate;
+
+ // Limit frame rate based on max-mbps
+ if (mCodecConfig.mEncodingConstraints.maxMbps) {
+ unsigned int cur_fs, mb_width, mb_height;
+
+ mb_width = (aSendingWidth + 15) >> 4;
+ mb_height = (aSendingHeight + 15) >> 4;
+
+ cur_fs = mb_width * mb_height;
+ if (cur_fs > 0) { // in case no frames have been sent
+ new_framerate = mCodecConfig.mEncodingConstraints.maxMbps / cur_fs;
+ }
+ }
+
+ new_framerate =
+ std::min(new_framerate, WebrtcVideoConduit::ToLibwebrtcMaxFramerate(
+ mCodecConfig.mEncodingConstraints.maxFps));
+ return new_framerate;
+}
+
+bool VideoStreamFactory::ShouldDropFrame(const webrtc::VideoFrame& aFrame) {
+ bool hasNonZeroLayer = false;
+ {
+ const size_t streamCount =
+ mCodecMode == webrtc::VideoCodecMode::kScreensharing
+ ? 1
+ : mCodecConfig.mEncodings.size();
+ for (int idx = streamCount - 1; idx >= 0; --idx) {
+ const auto& encoding = mCodecConfig.mEncodings[idx];
+ if (aFrame.width() / encoding.constraints.scaleDownBy >= 1.0 &&
+ aFrame.height() / encoding.constraints.scaleDownBy >= 1.0) {
+ hasNonZeroLayer = true;
+ break;
+ }
+ }
+ }
+ if (!hasNonZeroLayer) {
+ return true;
+ }
+
+ auto frameRateController = mFramerateController.Lock();
+ return frameRateController->ShouldDropFrame(aFrame.timestamp_us() *
+ rtc::kNumNanosecsPerMicrosec);
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/libwebrtcglue/VideoStreamFactory.h b/dom/media/webrtc/libwebrtcglue/VideoStreamFactory.h
new file mode 100644
index 0000000000..832d1e9399
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/VideoStreamFactory.h
@@ -0,0 +1,132 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#ifndef VideoStreamFactory_h
+#define VideoStreamFactory_h
+
+#include "CodecConfig.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/DataMutex.h"
+#include "mozilla/gfx/Point.h"
+#include "mozilla/UniquePtr.h"
+#include "api/video/video_source_interface.h"
+#include "common_video/framerate_controller.h"
+#include "rtc_base/time_utils.h"
+#include "video/config/video_encoder_config.h"
+
+namespace webrtc {
+class VideoFrame;
+}
+
+namespace mozilla {
+
+// Factory class for VideoStreams... vie_encoder.cc will call this to
+// reconfigure.
+class VideoStreamFactory
+ : public webrtc::VideoEncoderConfig::VideoStreamFactoryInterface {
+ public:
+ struct ResolutionAndBitrateLimits {
+ int resolution_in_mb;
+ int min_bitrate_bps;
+ int start_bitrate_bps;
+ int max_bitrate_bps;
+ };
+
+ static ResolutionAndBitrateLimits GetLimitsFor(unsigned int aWidth,
+ unsigned int aHeight,
+ int aCapBps = 0);
+
+ VideoStreamFactory(VideoCodecConfig aConfig,
+ webrtc::VideoCodecMode aCodecMode, int aMinBitrate,
+ int aStartBitrate, int aPrefMaxBitrate,
+ int aNegotiatedMaxBitrate,
+ const rtc::VideoSinkWants& aWants, bool aLockScaling)
+ : mCodecMode(aCodecMode),
+ mMaxFramerateForAllStreams(std::numeric_limits<unsigned int>::max()),
+ mCodecConfig(std::forward<VideoCodecConfig>(aConfig)),
+ mMinBitrate(aMinBitrate),
+ mStartBitrate(aStartBitrate),
+ mPrefMaxBitrate(aPrefMaxBitrate),
+ mNegotiatedMaxBitrate(aNegotiatedMaxBitrate),
+ mFramerateController("VideoStreamFactory::mFramerateController"),
+ mWants(aWants),
+ mLockScaling(aLockScaling) {}
+
+ // This gets called off-main thread and may hold internal webrtc.org
+ // locks. May *NOT* lock the conduit's mutex, to avoid deadlocks.
+ std::vector<webrtc::VideoStream> CreateEncoderStreams(
+ int aWidth, int aHeight,
+ const webrtc::VideoEncoderConfig& aConfig) override;
+ /**
+ * Function to select and change the encoding resolution based on incoming
+ * frame size and current available bandwidth.
+ * @param width, height: dimensions of the frame
+ */
+ void SelectMaxFramerateForAllStreams(unsigned short aWidth,
+ unsigned short aHeight);
+
+ /**
+ * Function to determine if the frame should be dropped based on the given
+ * frame's resolution (combined with the factory's scaleResolutionDownBy) or
+ * timestamp.
+ * @param aFrame frame to be evaluated.
+ * @return true if frame should be dropped, false otehrwise.
+ */
+ bool ShouldDropFrame(const webrtc::VideoFrame& aFrame);
+
+ private:
+ /**
+ * Function to calculate a scaled down width and height based on
+ * scaleDownByResolution, maxFS, and max pixel count settings.
+ * @param aWidth current frame width
+ * @param aHeight current frame height
+ * @param aScaleDownByResolution value to scale width and height down by.
+ * @param aMaxPixelCount maximum number of pixels wanted in a frame.
+ * @return a gfx:IntSize containing width and height to use. These may match
+ * the aWidth and aHeight passed in if no scaling was needed.
+ */
+ gfx::IntSize CalculateScaledResolution(int aWidth, int aHeight,
+ double aScaleDownByResolution,
+ unsigned int aMaxPixelCount);
+
+ /**
+ * Function to select and change the encoding frame rate based on incoming
+ * frame rate, current frame size and max-mbps setting.
+ * @param aOldFramerate current framerate
+ * @param aSendingWidth width of frames being sent
+ * @param aSendingHeight height of frames being sent
+ * @return new framerate meeting max-mbps requriements based on frame size
+ */
+ unsigned int SelectFrameRate(unsigned int aOldFramerate,
+ unsigned short aSendingWidth,
+ unsigned short aSendingHeight);
+
+ // Used to limit number of streams for screensharing.
+ Atomic<webrtc::VideoCodecMode> mCodecMode;
+
+ // The framerate we're currently sending at.
+ Atomic<unsigned int> mMaxFramerateForAllStreams;
+
+ // The current send codec config, containing simulcast layer configs.
+ const VideoCodecConfig mCodecConfig;
+
+ // Bitrate limits in bps.
+ const int mMinBitrate = 0;
+ const int mStartBitrate = 0;
+ const int mPrefMaxBitrate = 0;
+ const int mNegotiatedMaxBitrate = 0;
+
+ // DatamMutex used as object is mutated from a libwebrtc thread and
+ // a seperate thread used to pass video frames to libwebrtc.
+ DataMutex<webrtc::FramerateController> mFramerateController;
+
+ const rtc::VideoSinkWants mWants;
+ const bool mLockScaling;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/libwebrtcglue/WebrtcCallWrapper.cpp b/dom/media/webrtc/libwebrtcglue/WebrtcCallWrapper.cpp
new file mode 100644
index 0000000000..7d4547ddb6
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/WebrtcCallWrapper.cpp
@@ -0,0 +1,105 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WebrtcCallWrapper.h"
+
+#include "jsapi/PeerConnectionCtx.h"
+#include "MediaConduitInterface.h"
+#include "TaskQueueWrapper.h"
+
+// libwebrtc includes
+#include "call/rtp_transport_controller_send_factory.h"
+
+namespace mozilla {
+
+/* static */ RefPtr<WebrtcCallWrapper> WebrtcCallWrapper::Create(
+ const dom::RTCStatsTimestampMaker& aTimestampMaker,
+ UniquePtr<media::ShutdownBlockingTicket> aShutdownTicket,
+ const RefPtr<SharedWebrtcState>& aSharedState) {
+ auto eventLog = MakeUnique<webrtc::RtcEventLogNull>();
+ auto taskQueueFactory = MakeUnique<SharedThreadPoolWebRtcTaskQueueFactory>();
+ auto videoBitrateAllocatorFactory =
+ WrapUnique(webrtc::CreateBuiltinVideoBitrateAllocatorFactory().release());
+ RefPtr<WebrtcCallWrapper> wrapper = new WebrtcCallWrapper(
+ aSharedState, std::move(videoBitrateAllocatorFactory),
+ std::move(eventLog), std::move(taskQueueFactory), aTimestampMaker,
+ std::move(aShutdownTicket));
+
+ wrapper->mCallThread->Dispatch(
+ NS_NewRunnableFunction(__func__, [wrapper, aSharedState] {
+ webrtc::Call::Config config(wrapper->mEventLog.get());
+ config.audio_state =
+ webrtc::AudioState::Create(aSharedState->mAudioStateConfig);
+ config.task_queue_factory = wrapper->mTaskQueueFactory.get();
+ config.trials = aSharedState->mTrials.get();
+ wrapper->SetCall(WrapUnique(webrtc::Call::Create(
+ config, &wrapper->mClock,
+ webrtc::RtpTransportControllerSendFactory().Create(
+ config.ExtractTransportConfig(), &wrapper->mClock))));
+ }));
+
+ return wrapper;
+}
+
+void WebrtcCallWrapper::SetCall(UniquePtr<webrtc::Call> aCall) {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ MOZ_ASSERT(!mCall);
+ mCall = std::move(aCall);
+}
+
+webrtc::Call* WebrtcCallWrapper::Call() const {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ return mCall.get();
+}
+
+void WebrtcCallWrapper::UnsetRemoteSSRC(uint32_t aSsrc) {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ for (const auto& conduit : mConduits) {
+ conduit->UnsetRemoteSSRC(aSsrc);
+ }
+}
+
+void WebrtcCallWrapper::RegisterConduit(MediaSessionConduit* conduit) {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ mConduits.insert(conduit);
+}
+
+void WebrtcCallWrapper::UnregisterConduit(MediaSessionConduit* conduit) {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ mConduits.erase(conduit);
+}
+
+void WebrtcCallWrapper::Destroy() {
+ MOZ_ASSERT(mCallThread->IsOnCurrentThread());
+ mCall = nullptr;
+ mShutdownTicket = nullptr;
+}
+
+const dom::RTCStatsTimestampMaker& WebrtcCallWrapper::GetTimestampMaker()
+ const {
+ return mClock.mTimestampMaker;
+}
+
+WebrtcCallWrapper::~WebrtcCallWrapper() { MOZ_ASSERT(!mCall); }
+
+WebrtcCallWrapper::WebrtcCallWrapper(
+ RefPtr<SharedWebrtcState> aSharedState,
+ UniquePtr<webrtc::VideoBitrateAllocatorFactory>
+ aVideoBitrateAllocatorFactory,
+ UniquePtr<webrtc::RtcEventLog> aEventLog,
+ UniquePtr<webrtc::TaskQueueFactory> aTaskQueueFactory,
+ const dom::RTCStatsTimestampMaker& aTimestampMaker,
+ UniquePtr<media::ShutdownBlockingTicket> aShutdownTicket)
+ : mSharedState(std::move(aSharedState)),
+ mClock(aTimestampMaker),
+ mShutdownTicket(std::move(aShutdownTicket)),
+ mCallThread(mSharedState->mCallWorkerThread),
+ mAudioDecoderFactory(mSharedState->mAudioDecoderFactory),
+ mVideoBitrateAllocatorFactory(std::move(aVideoBitrateAllocatorFactory)),
+ mEventLog(std::move(aEventLog)),
+ mTaskQueueFactory(std::move(aTaskQueueFactory)) {}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/libwebrtcglue/WebrtcCallWrapper.h b/dom/media/webrtc/libwebrtcglue/WebrtcCallWrapper.h
new file mode 100644
index 0000000000..17054c5f75
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/WebrtcCallWrapper.h
@@ -0,0 +1,114 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_WEBRTC_LIBWEBRTCGLUE_WEBRTCCALLWRAPPER_H_
+#define DOM_MEDIA_WEBRTC_LIBWEBRTCGLUE_WEBRTCCALLWRAPPER_H_
+
+#include <set>
+
+#include "domstubs.h"
+#include "jsapi/RTCStatsReport.h"
+#include "nsISupportsImpl.h"
+#include "SystemTime.h"
+
+// libwebrtc includes
+#include "api/video/builtin_video_bitrate_allocator_factory.h"
+#include "call/call.h"
+#include "call/call_config.h"
+
+namespace mozilla {
+class AbstractThread;
+class MediaSessionConduit;
+class SharedWebrtcState;
+
+namespace media {
+class ShutdownBlockingTicket;
+}
+
+// Wrap the webrtc.org Call class adding mozilla add/ref support.
+class WebrtcCallWrapper {
+ public:
+ typedef webrtc::CallConfig Config;
+
+ static RefPtr<WebrtcCallWrapper> Create(
+ const dom::RTCStatsTimestampMaker& aTimestampMaker,
+ UniquePtr<media::ShutdownBlockingTicket> aShutdownTicket,
+ const RefPtr<SharedWebrtcState>& aSharedState);
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WebrtcCallWrapper)
+
+ // Don't allow copying/assigning.
+ WebrtcCallWrapper(const WebrtcCallWrapper&) = delete;
+ void operator=(const WebrtcCallWrapper&) = delete;
+
+ void SetCall(UniquePtr<webrtc::Call> aCall);
+
+ webrtc::Call* Call() const;
+
+ void UnsetRemoteSSRC(uint32_t aSsrc);
+
+ // Idempotent.
+ void RegisterConduit(MediaSessionConduit* conduit);
+
+ // Idempotent.
+ void UnregisterConduit(MediaSessionConduit* conduit);
+
+ // Allow destroying the Call instance on the Call worker thread.
+ //
+ // Note that shutdown is blocked until the Call instance is destroyed.
+ //
+ // This CallWrapper is designed to be sharable, and is held by several objects
+ // that are cycle-collectable. TaskQueueWrapper that the Call instances use
+ // for worker threads are based off SharedThreadPools, and will block
+ // xpcom-shutdown-threads until destroyed. The Call instance however will hold
+ // on to its worker threads until destruction.
+ //
+ // If the last ref to this CallWrapper is held to cycle collector shutdown we
+ // end up in a deadlock where cycle collector shutdown is required to destroy
+ // the SharedThreadPool that is blocking xpcom-shutdown-threads from finishing
+ // and triggering cycle collector shutdown.
+ //
+ // It would be nice to have the invariant where this class is immutable to the
+ // degree that mCall is const, but given the above that is not possible.
+ void Destroy();
+
+ const dom::RTCStatsTimestampMaker& GetTimestampMaker() const;
+
+ protected:
+ virtual ~WebrtcCallWrapper();
+
+ WebrtcCallWrapper(RefPtr<SharedWebrtcState> aSharedState,
+ UniquePtr<webrtc::VideoBitrateAllocatorFactory>
+ aVideoBitrateAllocatorFactory,
+ UniquePtr<webrtc::RtcEventLog> aEventLog,
+ UniquePtr<webrtc::TaskQueueFactory> aTaskQueueFactory,
+ const dom::RTCStatsTimestampMaker& aTimestampMaker,
+ UniquePtr<media::ShutdownBlockingTicket> aShutdownTicket);
+
+ const RefPtr<SharedWebrtcState> mSharedState;
+
+ // Allows conduits to know about one another, to avoid remote SSRC
+ // collisions.
+ std::set<RefPtr<MediaSessionConduit>> mConduits;
+ RTCStatsTimestampMakerRealtimeClock mClock;
+ UniquePtr<media::ShutdownBlockingTicket> mShutdownTicket;
+
+ public:
+ const RefPtr<AbstractThread> mCallThread;
+ const RefPtr<webrtc::AudioDecoderFactory> mAudioDecoderFactory;
+ const UniquePtr<webrtc::VideoBitrateAllocatorFactory>
+ mVideoBitrateAllocatorFactory;
+ const UniquePtr<webrtc::RtcEventLog> mEventLog;
+ const UniquePtr<webrtc::TaskQueueFactory> mTaskQueueFactory;
+
+ protected:
+ // Call worker thread only.
+ UniquePtr<webrtc::Call> mCall;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/libwebrtcglue/WebrtcGmpVideoCodec.cpp b/dom/media/webrtc/libwebrtcglue/WebrtcGmpVideoCodec.cpp
new file mode 100644
index 0000000000..50e39b22d0
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/WebrtcGmpVideoCodec.cpp
@@ -0,0 +1,1043 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WebrtcGmpVideoCodec.h"
+
+#include <utility>
+#include <vector>
+
+#include "GMPLog.h"
+#include "MainThreadUtils.h"
+#include "VideoConduit.h"
+#include "gmp-video-frame-encoded.h"
+#include "gmp-video-frame-i420.h"
+#include "mozilla/CheckedInt.h"
+#include "mozilla/EndianUtils.h"
+#include "mozilla/IntegerPrintfMacros.h"
+#include "mozilla/SyncRunnable.h"
+#include "nsServiceManagerUtils.h"
+#include "transport/runnable_utils.h"
+#include "api/video/video_frame_type.h"
+#include "common_video/include/video_frame_buffer.h"
+#include "media/base/media_constants.h"
+// #include "rtc_base/bind.h"
+
+namespace mozilla {
+
+// QP scaling thresholds.
+static const int kLowH264QpThreshold = 24;
+static const int kHighH264QpThreshold = 37;
+
+// Encoder.
+WebrtcGmpVideoEncoder::WebrtcGmpVideoEncoder(
+ const webrtc::SdpVideoFormat& aFormat, std::string aPCHandle)
+ : mGMP(nullptr),
+ mInitting(false),
+ mHost(nullptr),
+ mMaxPayloadSize(0),
+ mFormatParams(aFormat.parameters),
+ mCallbackMutex("WebrtcGmpVideoEncoder encoded callback mutex"),
+ mCallback(nullptr),
+ mPCHandle(std::move(aPCHandle)),
+ mInputImageMap("WebrtcGmpVideoEncoder::mInputImageMap") {
+ mCodecParams.mGMPApiVersion = 0;
+ mCodecParams.mCodecType = kGMPVideoCodecInvalid;
+ mCodecParams.mPLType = 0;
+ mCodecParams.mWidth = 0;
+ mCodecParams.mHeight = 0;
+ mCodecParams.mStartBitrate = 0;
+ mCodecParams.mMaxBitrate = 0;
+ mCodecParams.mMinBitrate = 0;
+ mCodecParams.mMaxFramerate = 0;
+ mCodecParams.mFrameDroppingOn = false;
+ mCodecParams.mKeyFrameInterval = 0;
+ mCodecParams.mQPMax = 0;
+ mCodecParams.mNumberOfSimulcastStreams = 0;
+ mCodecParams.mMode = kGMPCodecModeInvalid;
+ mCodecParams.mLogLevel = GetGMPLibraryLogLevel();
+ MOZ_ASSERT(!mPCHandle.empty());
+}
+
+WebrtcGmpVideoEncoder::~WebrtcGmpVideoEncoder() {
+ // We should not have been destroyed if we never closed our GMP
+ MOZ_ASSERT(!mGMP);
+}
+
+static int WebrtcFrameTypeToGmpFrameType(webrtc::VideoFrameType aIn,
+ GMPVideoFrameType* aOut) {
+ MOZ_ASSERT(aOut);
+ switch (aIn) {
+ case webrtc::VideoFrameType::kVideoFrameKey:
+ *aOut = kGMPKeyFrame;
+ break;
+ case webrtc::VideoFrameType::kVideoFrameDelta:
+ *aOut = kGMPDeltaFrame;
+ break;
+ case webrtc::VideoFrameType::kEmptyFrame:
+ *aOut = kGMPSkipFrame;
+ break;
+ default:
+ MOZ_CRASH("Unexpected webrtc::FrameType");
+ }
+
+ return WEBRTC_VIDEO_CODEC_OK;
+}
+
+static int GmpFrameTypeToWebrtcFrameType(GMPVideoFrameType aIn,
+ webrtc::VideoFrameType* aOut) {
+ MOZ_ASSERT(aOut);
+ switch (aIn) {
+ case kGMPKeyFrame:
+ *aOut = webrtc::VideoFrameType::kVideoFrameKey;
+ break;
+ case kGMPDeltaFrame:
+ *aOut = webrtc::VideoFrameType::kVideoFrameDelta;
+ break;
+ case kGMPSkipFrame:
+ *aOut = webrtc::VideoFrameType::kEmptyFrame;
+ break;
+ default:
+ MOZ_CRASH("Unexpected GMPVideoFrameType");
+ }
+
+ return WEBRTC_VIDEO_CODEC_OK;
+}
+
+static int SizeNumBytes(GMPBufferType aBufferType) {
+ switch (aBufferType) {
+ case GMP_BufferSingle:
+ return 0;
+ case GMP_BufferLength8:
+ return 1;
+ case GMP_BufferLength16:
+ return 2;
+ case GMP_BufferLength24:
+ return 3;
+ case GMP_BufferLength32:
+ return 4;
+ default:
+ MOZ_CRASH("Unexpected buffer type");
+ }
+}
+
+int32_t WebrtcGmpVideoEncoder::InitEncode(
+ const webrtc::VideoCodec* aCodecSettings,
+ const webrtc::VideoEncoder::Settings& aSettings) {
+ if (!mMPS) {
+ mMPS = do_GetService("@mozilla.org/gecko-media-plugin-service;1");
+ }
+ MOZ_ASSERT(mMPS);
+
+ if (!mGMPThread) {
+ if (NS_WARN_IF(NS_FAILED(mMPS->GetThread(getter_AddRefs(mGMPThread))))) {
+ return WEBRTC_VIDEO_CODEC_ERROR;
+ }
+ }
+
+ MOZ_ASSERT(aCodecSettings->numberOfSimulcastStreams == 1,
+ "Simulcast not implemented for GMP-H264");
+
+ // Bug XXXXXX: transfer settings from codecSettings to codec.
+ GMPVideoCodec codecParams;
+ memset(&codecParams, 0, sizeof(codecParams));
+
+ codecParams.mGMPApiVersion = kGMPVersion34;
+ codecParams.mLogLevel = GetGMPLibraryLogLevel();
+ codecParams.mStartBitrate = aCodecSettings->startBitrate;
+ codecParams.mMinBitrate = aCodecSettings->minBitrate;
+ codecParams.mMaxBitrate = aCodecSettings->maxBitrate;
+ codecParams.mMaxFramerate = aCodecSettings->maxFramerate;
+
+ memset(&mCodecSpecificInfo.codecSpecific, 0,
+ sizeof(mCodecSpecificInfo.codecSpecific));
+ mCodecSpecificInfo.codecType = webrtc::kVideoCodecH264;
+ mCodecSpecificInfo.codecSpecific.H264.packetization_mode =
+ mFormatParams.count(cricket::kH264FmtpPacketizationMode) == 1 &&
+ mFormatParams.at(cricket::kH264FmtpPacketizationMode) == "1"
+ ? webrtc::H264PacketizationMode::NonInterleaved
+ : webrtc::H264PacketizationMode::SingleNalUnit;
+
+ uint32_t maxPayloadSize = aSettings.max_payload_size;
+ if (mCodecSpecificInfo.codecSpecific.H264.packetization_mode ==
+ webrtc::H264PacketizationMode::NonInterleaved) {
+ maxPayloadSize = 0; // No limit, use FUAs
+ }
+
+ if (aCodecSettings->mode == webrtc::VideoCodecMode::kScreensharing) {
+ codecParams.mMode = kGMPScreensharing;
+ } else {
+ codecParams.mMode = kGMPRealtimeVideo;
+ }
+
+ codecParams.mWidth = aCodecSettings->width;
+ codecParams.mHeight = aCodecSettings->height;
+
+ RefPtr<GmpInitDoneRunnable> initDone(new GmpInitDoneRunnable(mPCHandle));
+ mGMPThread->Dispatch(
+ WrapRunnableNM(WebrtcGmpVideoEncoder::InitEncode_g,
+ RefPtr<WebrtcGmpVideoEncoder>(this), codecParams,
+ aSettings.number_of_cores, maxPayloadSize, initDone),
+ NS_DISPATCH_NORMAL);
+
+ // Since init of the GMP encoder is a multi-step async dispatch (including
+ // dispatches to main), and since this function is invoked on main, there's
+ // no safe way to block until this init is done. If an error occurs, we'll
+ // handle it later.
+ return WEBRTC_VIDEO_CODEC_OK;
+}
+
+/* static */
+void WebrtcGmpVideoEncoder::InitEncode_g(
+ const RefPtr<WebrtcGmpVideoEncoder>& aThis,
+ const GMPVideoCodec& aCodecParams, int32_t aNumberOfCores,
+ uint32_t aMaxPayloadSize, const RefPtr<GmpInitDoneRunnable>& aInitDone) {
+ nsTArray<nsCString> tags;
+ tags.AppendElement("h264"_ns);
+ UniquePtr<GetGMPVideoEncoderCallback> callback(
+ new InitDoneCallback(aThis, aInitDone, aCodecParams));
+ aThis->mInitting = true;
+ aThis->mMaxPayloadSize = aMaxPayloadSize;
+ nsresult rv = aThis->mMPS->GetGMPVideoEncoder(nullptr, &tags, ""_ns,
+ std::move(callback));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ GMP_LOG_DEBUG("GMP Encode: GetGMPVideoEncoder failed");
+ aThis->Close_g();
+ aInitDone->Dispatch(WEBRTC_VIDEO_CODEC_ERROR,
+ "GMP Encode: GetGMPVideoEncoder failed");
+ }
+}
+
+int32_t WebrtcGmpVideoEncoder::GmpInitDone(GMPVideoEncoderProxy* aGMP,
+ GMPVideoHost* aHost,
+ std::string* aErrorOut) {
+ if (!mInitting || !aGMP || !aHost) {
+ *aErrorOut =
+ "GMP Encode: Either init was aborted, "
+ "or init failed to supply either a GMP Encoder or GMP host.";
+ if (aGMP) {
+ // This could destroy us, since aGMP may be the last thing holding a ref
+ // Return immediately.
+ aGMP->Close();
+ }
+ return WEBRTC_VIDEO_CODEC_ERROR;
+ }
+
+ mInitting = false;
+
+ if (mGMP && mGMP != aGMP) {
+ Close_g();
+ }
+
+ mGMP = aGMP;
+ mHost = aHost;
+ mCachedPluginId = Some(mGMP->GetPluginId());
+ mInitPluginEvent.Notify(*mCachedPluginId);
+ return WEBRTC_VIDEO_CODEC_OK;
+}
+
+int32_t WebrtcGmpVideoEncoder::GmpInitDone(GMPVideoEncoderProxy* aGMP,
+ GMPVideoHost* aHost,
+ const GMPVideoCodec& aCodecParams,
+ std::string* aErrorOut) {
+ int32_t r = GmpInitDone(aGMP, aHost, aErrorOut);
+ if (r != WEBRTC_VIDEO_CODEC_OK) {
+ // We might have been destroyed if GmpInitDone failed.
+ // Return immediately.
+ return r;
+ }
+ mCodecParams = aCodecParams;
+ return InitEncoderForSize(aCodecParams.mWidth, aCodecParams.mHeight,
+ aErrorOut);
+}
+
+void WebrtcGmpVideoEncoder::Close_g() {
+ GMPVideoEncoderProxy* gmp(mGMP);
+ mGMP = nullptr;
+ mHost = nullptr;
+ mInitting = false;
+
+ if (mCachedPluginId) {
+ mReleasePluginEvent.Notify(*mCachedPluginId);
+ }
+ mCachedPluginId = Nothing();
+
+ if (gmp) {
+ // Do this last, since this could cause us to be destroyed
+ gmp->Close();
+ }
+}
+
+int32_t WebrtcGmpVideoEncoder::InitEncoderForSize(unsigned short aWidth,
+ unsigned short aHeight,
+ std::string* aErrorOut) {
+ mCodecParams.mWidth = aWidth;
+ mCodecParams.mHeight = aHeight;
+ // Pass dummy codecSpecific data for now...
+ nsTArray<uint8_t> codecSpecific;
+
+ GMPErr err =
+ mGMP->InitEncode(mCodecParams, codecSpecific, this, 1, mMaxPayloadSize);
+ if (err != GMPNoErr) {
+ *aErrorOut = "GMP Encode: InitEncode failed";
+ return WEBRTC_VIDEO_CODEC_ERROR;
+ }
+
+ return WEBRTC_VIDEO_CODEC_OK;
+}
+
+int32_t WebrtcGmpVideoEncoder::Encode(
+ const webrtc::VideoFrame& aInputImage,
+ const std::vector<webrtc::VideoFrameType>* aFrameTypes) {
+ MOZ_ASSERT(aInputImage.width() >= 0 && aInputImage.height() >= 0);
+ if (!aFrameTypes) {
+ return WEBRTC_VIDEO_CODEC_ERROR;
+ }
+
+ // It is safe to copy aInputImage here because the frame buffer is held by
+ // a refptr.
+ mGMPThread->Dispatch(WrapRunnableNM(&WebrtcGmpVideoEncoder::Encode_g,
+ RefPtr<WebrtcGmpVideoEncoder>(this),
+ aInputImage, *aFrameTypes),
+ NS_DISPATCH_NORMAL);
+
+ return WEBRTC_VIDEO_CODEC_OK;
+}
+
+void WebrtcGmpVideoEncoder::RegetEncoderForResolutionChange(
+ uint32_t aWidth, uint32_t aHeight,
+ const RefPtr<GmpInitDoneRunnable>& aInitDone) {
+ Close_g();
+
+ UniquePtr<GetGMPVideoEncoderCallback> callback(
+ new InitDoneForResolutionChangeCallback(this, aInitDone, aWidth,
+ aHeight));
+
+ // OpenH264 codec (at least) can't handle dynamic input resolution changes
+ // re-init the plugin when the resolution changes
+ // XXX allow codec to indicate it doesn't need re-init!
+ nsTArray<nsCString> tags;
+ tags.AppendElement("h264"_ns);
+ mInitting = true;
+ if (NS_WARN_IF(NS_FAILED(mMPS->GetGMPVideoEncoder(nullptr, &tags, ""_ns,
+ std::move(callback))))) {
+ aInitDone->Dispatch(WEBRTC_VIDEO_CODEC_ERROR,
+ "GMP Encode: GetGMPVideoEncoder failed");
+ }
+}
+
+void WebrtcGmpVideoEncoder::Encode_g(
+ const RefPtr<WebrtcGmpVideoEncoder>& aEncoder,
+ webrtc::VideoFrame aInputImage,
+ std::vector<webrtc::VideoFrameType> aFrameTypes) {
+ if (!aEncoder->mGMP) {
+ // destroyed via Terminate(), failed to init, or just not initted yet
+ GMP_LOG_DEBUG("GMP Encode: not initted yet");
+ return;
+ }
+ MOZ_ASSERT(aEncoder->mHost);
+
+ if (static_cast<uint32_t>(aInputImage.width()) !=
+ aEncoder->mCodecParams.mWidth ||
+ static_cast<uint32_t>(aInputImage.height()) !=
+ aEncoder->mCodecParams.mHeight) {
+ GMP_LOG_DEBUG("GMP Encode: resolution change from %ux%u to %dx%d",
+ aEncoder->mCodecParams.mWidth, aEncoder->mCodecParams.mHeight,
+ aInputImage.width(), aInputImage.height());
+
+ RefPtr<GmpInitDoneRunnable> initDone(
+ new GmpInitDoneRunnable(aEncoder->mPCHandle));
+ aEncoder->RegetEncoderForResolutionChange(aInputImage.width(),
+ aInputImage.height(), initDone);
+ if (!aEncoder->mGMP) {
+ // We needed to go async to re-get the encoder. Bail.
+ return;
+ }
+ }
+
+ GMPVideoFrame* ftmp = nullptr;
+ GMPErr err = aEncoder->mHost->CreateFrame(kGMPI420VideoFrame, &ftmp);
+ if (err != GMPNoErr) {
+ GMP_LOG_DEBUG("GMP Encode: failed to create frame on host");
+ return;
+ }
+ GMPUniquePtr<GMPVideoi420Frame> frame(static_cast<GMPVideoi420Frame*>(ftmp));
+ const webrtc::I420BufferInterface* input_image =
+ aInputImage.video_frame_buffer()->GetI420();
+ // check for overflow of stride * height
+ CheckedInt32 ysize =
+ CheckedInt32(input_image->StrideY()) * input_image->height();
+ MOZ_RELEASE_ASSERT(ysize.isValid());
+ // I will assume that if that doesn't overflow, the others case - YUV
+ // 4:2:0 has U/V widths <= Y, even with alignment issues.
+ err = frame->CreateFrame(
+ ysize.value(), input_image->DataY(),
+ input_image->StrideU() * ((input_image->height() + 1) / 2),
+ input_image->DataU(),
+ input_image->StrideV() * ((input_image->height() + 1) / 2),
+ input_image->DataV(), input_image->width(), input_image->height(),
+ input_image->StrideY(), input_image->StrideU(), input_image->StrideV());
+ if (err != GMPNoErr) {
+ GMP_LOG_DEBUG("GMP Encode: failed to create frame");
+ return;
+ }
+ frame->SetTimestamp((aInputImage.timestamp() * 1000ll) /
+ 90); // note: rounds down!
+ // frame->SetDuration(1000000ll/30); // XXX base duration on measured current
+ // FPS - or don't bother
+
+ // Bug XXXXXX: Set codecSpecific info
+ GMPCodecSpecificInfo info;
+ memset(&info, 0, sizeof(info));
+ info.mCodecType = kGMPVideoCodecH264;
+ nsTArray<uint8_t> codecSpecificInfo;
+ codecSpecificInfo.AppendElements((uint8_t*)&info,
+ sizeof(GMPCodecSpecificInfo));
+
+ nsTArray<GMPVideoFrameType> gmp_frame_types;
+ for (auto it = aFrameTypes.begin(); it != aFrameTypes.end(); ++it) {
+ GMPVideoFrameType ft;
+
+ int32_t ret = WebrtcFrameTypeToGmpFrameType(*it, &ft);
+ if (ret != WEBRTC_VIDEO_CODEC_OK) {
+ GMP_LOG_DEBUG(
+ "GMP Encode: failed to map webrtc frame type to gmp frame type");
+ return;
+ }
+
+ gmp_frame_types.AppendElement(ft);
+ }
+
+ {
+ auto inputImageMap = aEncoder->mInputImageMap.Lock();
+ DebugOnly<bool> inserted = false;
+ std::tie(std::ignore, inserted) = inputImageMap->insert(
+ {frame->Timestamp(), {aInputImage.timestamp_us()}});
+ MOZ_ASSERT(inserted, "Duplicate timestamp");
+ }
+
+ GMP_LOG_DEBUG("GMP Encode: %" PRIu64, (frame->Timestamp()));
+ err = aEncoder->mGMP->Encode(std::move(frame), codecSpecificInfo,
+ gmp_frame_types);
+ if (err != GMPNoErr) {
+ GMP_LOG_DEBUG("GMP Encode: failed to encode frame");
+ }
+}
+
+int32_t WebrtcGmpVideoEncoder::RegisterEncodeCompleteCallback(
+ webrtc::EncodedImageCallback* aCallback) {
+ MutexAutoLock lock(mCallbackMutex);
+ mCallback = aCallback;
+
+ return WEBRTC_VIDEO_CODEC_OK;
+}
+
+/* static */
+void WebrtcGmpVideoEncoder::ReleaseGmp_g(
+ const RefPtr<WebrtcGmpVideoEncoder>& aEncoder) {
+ aEncoder->Close_g();
+}
+
+int32_t WebrtcGmpVideoEncoder::Shutdown() {
+ GMP_LOG_DEBUG("GMP Released:");
+ RegisterEncodeCompleteCallback(nullptr);
+ if (mGMPThread) {
+ mGMPThread->Dispatch(WrapRunnableNM(&WebrtcGmpVideoEncoder::ReleaseGmp_g,
+ RefPtr<WebrtcGmpVideoEncoder>(this)),
+ NS_DISPATCH_NORMAL);
+ }
+ return WEBRTC_VIDEO_CODEC_OK;
+}
+
+int32_t WebrtcGmpVideoEncoder::SetRates(
+ const webrtc::VideoEncoder::RateControlParameters& aParameters) {
+ MOZ_ASSERT(mGMPThread);
+ MOZ_ASSERT(aParameters.bitrate.IsSpatialLayerUsed(0));
+ MOZ_ASSERT(!aParameters.bitrate.HasBitrate(0, 1),
+ "No simulcast support for H264");
+ MOZ_ASSERT(!aParameters.bitrate.IsSpatialLayerUsed(1),
+ "No simulcast support for H264");
+ mGMPThread->Dispatch(
+ WrapRunnableNM(&WebrtcGmpVideoEncoder::SetRates_g,
+ RefPtr<WebrtcGmpVideoEncoder>(this),
+ aParameters.bitrate.GetBitrate(0, 0) / 1000,
+ aParameters.framerate_fps > 0.0
+ ? Some(aParameters.framerate_fps)
+ : Nothing()),
+ NS_DISPATCH_NORMAL);
+
+ return WEBRTC_VIDEO_CODEC_OK;
+}
+
+WebrtcVideoEncoder::EncoderInfo WebrtcGmpVideoEncoder::GetEncoderInfo() const {
+ WebrtcVideoEncoder::EncoderInfo info;
+ info.supports_native_handle = false;
+ info.implementation_name = "GMPOpenH264";
+ info.scaling_settings = WebrtcVideoEncoder::ScalingSettings(
+ kLowH264QpThreshold, kHighH264QpThreshold);
+ info.is_hardware_accelerated = false;
+ info.supports_simulcast = false;
+ return info;
+}
+
+/* static */
+int32_t WebrtcGmpVideoEncoder::SetRates_g(RefPtr<WebrtcGmpVideoEncoder> aThis,
+ uint32_t aNewBitRateKbps,
+ Maybe<double> aFrameRate) {
+ if (!aThis->mGMP) {
+ // destroyed via Terminate()
+ return WEBRTC_VIDEO_CODEC_ERROR;
+ }
+
+ GMPErr err = aThis->mGMP->SetRates(
+ aNewBitRateKbps, aFrameRate
+ .map([](double aFr) {
+ // Avoid rounding to 0
+ return std::max(1U, static_cast<uint32_t>(aFr));
+ })
+ .valueOr(aThis->mCodecParams.mMaxFramerate));
+ if (err != GMPNoErr) {
+ return WEBRTC_VIDEO_CODEC_ERROR;
+ }
+
+ return WEBRTC_VIDEO_CODEC_OK;
+}
+
+// GMPVideoEncoderCallback virtual functions.
+void WebrtcGmpVideoEncoder::Terminated() {
+ GMP_LOG_DEBUG("GMP Encoder Terminated: %p", (void*)this);
+
+ GMPVideoEncoderProxy* gmp(mGMP);
+ mGMP = nullptr;
+ mHost = nullptr;
+ mInitting = false;
+
+ if (gmp) {
+ // Do this last, since this could cause us to be destroyed
+ gmp->Close();
+ }
+
+ // Could now notify that it's dead
+}
+
+void WebrtcGmpVideoEncoder::Encoded(
+ GMPVideoEncodedFrame* aEncodedFrame,
+ const nsTArray<uint8_t>& aCodecSpecificInfo) {
+ webrtc::Timestamp capture_time = webrtc::Timestamp::Micros(0);
+ {
+ auto inputImageMap = mInputImageMap.Lock();
+ auto handle = inputImageMap->extract(aEncodedFrame->TimeStamp());
+ MOZ_ASSERT(handle);
+ if (handle) {
+ capture_time = webrtc::Timestamp::Micros(handle.mapped().timestamp_us);
+ }
+ }
+
+ MutexAutoLock lock(mCallbackMutex);
+ if (!mCallback) {
+ return;
+ }
+
+ webrtc::VideoFrameType ft;
+ GmpFrameTypeToWebrtcFrameType(aEncodedFrame->FrameType(), &ft);
+ uint32_t timestamp = (aEncodedFrame->TimeStamp() * 90ll + 999) / 1000;
+
+ GMP_LOG_DEBUG("GMP Encoded: %" PRIu64 ", type %d, len %d",
+ aEncodedFrame->TimeStamp(), aEncodedFrame->BufferType(),
+ aEncodedFrame->Size());
+
+ if (!aEncodedFrame->Buffer()) {
+ GMP_LOG_ERROR("GMP plugin returned null buffer");
+ return;
+ }
+
+ // Libwebrtc's RtpPacketizerH264 expects a 3- or 4-byte NALU start sequence
+ // before the start of the NALU payload. {0,0,1} or {0,0,0,1}. We set this
+ // in-place. Any other length of the length field we reject.
+
+ const int sizeNumBytes = SizeNumBytes(aEncodedFrame->BufferType());
+ uint32_t unitOffset = 0;
+ uint32_t unitSize = 0;
+ // Make sure we don't read past the end of the buffer getting the size
+ while (unitOffset + sizeNumBytes < aEncodedFrame->Size()) {
+ uint8_t* unitBuffer = aEncodedFrame->Buffer() + unitOffset;
+ switch (aEncodedFrame->BufferType()) {
+ case GMP_BufferLength24: {
+#if MOZ_LITTLE_ENDIAN()
+ unitSize = (static_cast<uint32_t>(*unitBuffer)) |
+ (static_cast<uint32_t>(*(unitBuffer + 1)) << 8) |
+ (static_cast<uint32_t>(*(unitBuffer + 2)) << 16);
+#else
+ unitSize = (static_cast<uint32_t>(*unitBuffer) << 16) |
+ (static_cast<uint32_t>(*(unitBuffer + 1)) << 8) |
+ (static_cast<uint32_t>(*(unitBuffer + 2)));
+#endif
+ const uint8_t startSequence[] = {0, 0, 1};
+ if (memcmp(unitBuffer, startSequence, 3) == 0) {
+ // This is a bug in OpenH264 where it misses to convert the NALU start
+ // sequence to the NALU size per the GMP protocol. We mitigate this by
+ // letting it through as this is what libwebrtc already expects and
+ // scans for.
+ unitSize = aEncodedFrame->Size() - 3;
+ break;
+ }
+ memcpy(unitBuffer, startSequence, 3);
+ break;
+ }
+ case GMP_BufferLength32: {
+#if MOZ_LITTLE_ENDIAN()
+ unitSize = LittleEndian::readUint32(unitBuffer);
+#else
+ unitSize = BigEndian::readUint32(unitBuffer);
+#endif
+ const uint8_t startSequence[] = {0, 0, 0, 1};
+ if (memcmp(unitBuffer, startSequence, 4) == 0) {
+ // This is a bug in OpenH264 where it misses to convert the NALU start
+ // sequence to the NALU size per the GMP protocol. We mitigate this by
+ // letting it through as this is what libwebrtc already expects and
+ // scans for.
+ unitSize = aEncodedFrame->Size() - 4;
+ break;
+ }
+ memcpy(unitBuffer, startSequence, 4);
+ break;
+ }
+ default:
+ GMP_LOG_ERROR("GMP plugin returned type we cannot handle (%d)",
+ aEncodedFrame->BufferType());
+ return;
+ }
+
+ MOZ_ASSERT(unitSize != 0);
+ MOZ_ASSERT(unitOffset + sizeNumBytes + unitSize <= aEncodedFrame->Size());
+ if (unitSize == 0 ||
+ unitOffset + sizeNumBytes + unitSize > aEncodedFrame->Size()) {
+ // XXX Should we kill the plugin for returning extra bytes? Probably
+ GMP_LOG_ERROR(
+ "GMP plugin returned badly formatted encoded data: "
+ "unitOffset=%u, sizeNumBytes=%d, unitSize=%u, size=%u",
+ unitOffset, sizeNumBytes, unitSize, aEncodedFrame->Size());
+ return;
+ }
+
+ unitOffset += sizeNumBytes + unitSize;
+ }
+
+ if (unitOffset != aEncodedFrame->Size()) {
+ // At most 3 bytes can be left over, depending on buffertype
+ GMP_LOG_DEBUG("GMP plugin returned %u extra bytes",
+ aEncodedFrame->Size() - unitOffset);
+ }
+
+ webrtc::EncodedImage unit;
+ unit.SetEncodedData(webrtc::EncodedImageBuffer::Create(
+ aEncodedFrame->Buffer(), aEncodedFrame->Size()));
+ unit._frameType = ft;
+ unit.SetTimestamp(timestamp);
+ unit.capture_time_ms_ = capture_time.ms();
+ unit._encodedWidth = aEncodedFrame->EncodedWidth();
+ unit._encodedHeight = aEncodedFrame->EncodedHeight();
+
+ // Parse QP.
+ mH264BitstreamParser.ParseBitstream(unit);
+ unit.qp_ = mH264BitstreamParser.GetLastSliceQp().value_or(-1);
+
+ // TODO: Currently the OpenH264 codec does not preserve any codec
+ // specific info passed into it and just returns default values.
+ // If this changes in the future, it would be nice to get rid of
+ // mCodecSpecificInfo.
+ mCallback->OnEncodedImage(unit, &mCodecSpecificInfo);
+}
+
+// Decoder.
+WebrtcGmpVideoDecoder::WebrtcGmpVideoDecoder(std::string aPCHandle,
+ TrackingId aTrackingId)
+ : mGMP(nullptr),
+ mInitting(false),
+ mHost(nullptr),
+ mCallbackMutex("WebrtcGmpVideoDecoder decoded callback mutex"),
+ mCallback(nullptr),
+ mDecoderStatus(GMPNoErr),
+ mPCHandle(std::move(aPCHandle)),
+ mTrackingId(std::move(aTrackingId)) {
+ MOZ_ASSERT(!mPCHandle.empty());
+}
+
+WebrtcGmpVideoDecoder::~WebrtcGmpVideoDecoder() {
+ // We should not have been destroyed if we never closed our GMP
+ MOZ_ASSERT(!mGMP);
+}
+
+bool WebrtcGmpVideoDecoder::Configure(
+ const webrtc::VideoDecoder::Settings& settings) {
+ if (!mMPS) {
+ mMPS = do_GetService("@mozilla.org/gecko-media-plugin-service;1");
+ }
+ MOZ_ASSERT(mMPS);
+
+ if (!mGMPThread) {
+ if (NS_WARN_IF(NS_FAILED(mMPS->GetThread(getter_AddRefs(mGMPThread))))) {
+ return false;
+ }
+ }
+
+ RefPtr<GmpInitDoneRunnable> initDone(new GmpInitDoneRunnable(mPCHandle));
+ mGMPThread->Dispatch(
+ WrapRunnableNM(&WebrtcGmpVideoDecoder::Configure_g,
+ RefPtr<WebrtcGmpVideoDecoder>(this), settings, initDone),
+ NS_DISPATCH_NORMAL);
+
+ return true;
+}
+
+/* static */
+void WebrtcGmpVideoDecoder::Configure_g(
+ const RefPtr<WebrtcGmpVideoDecoder>& aThis,
+ const webrtc::VideoDecoder::Settings& settings, // unused
+ const RefPtr<GmpInitDoneRunnable>& aInitDone) {
+ nsTArray<nsCString> tags;
+ tags.AppendElement("h264"_ns);
+ UniquePtr<GetGMPVideoDecoderCallback> callback(
+ new InitDoneCallback(aThis, aInitDone));
+ aThis->mInitting = true;
+ nsresult rv = aThis->mMPS->GetGMPVideoDecoder(nullptr, &tags, ""_ns,
+ std::move(callback));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ GMP_LOG_DEBUG("GMP Decode: GetGMPVideoDecoder failed");
+ aThis->Close_g();
+ aInitDone->Dispatch(WEBRTC_VIDEO_CODEC_ERROR,
+ "GMP Decode: GetGMPVideoDecoder failed.");
+ }
+}
+
+int32_t WebrtcGmpVideoDecoder::GmpInitDone(GMPVideoDecoderProxy* aGMP,
+ GMPVideoHost* aHost,
+ std::string* aErrorOut) {
+ if (!mInitting || !aGMP || !aHost) {
+ *aErrorOut =
+ "GMP Decode: Either init was aborted, "
+ "or init failed to supply either a GMP decoder or GMP host.";
+ if (aGMP) {
+ // This could destroy us, since aGMP may be the last thing holding a ref
+ // Return immediately.
+ aGMP->Close();
+ }
+ return WEBRTC_VIDEO_CODEC_ERROR;
+ }
+
+ mInitting = false;
+
+ if (mGMP && mGMP != aGMP) {
+ Close_g();
+ }
+
+ mGMP = aGMP;
+ mHost = aHost;
+ mCachedPluginId = Some(mGMP->GetPluginId());
+ mInitPluginEvent.Notify(*mCachedPluginId);
+ // Bug XXXXXX: transfer settings from codecSettings to codec.
+ GMPVideoCodec codec;
+ memset(&codec, 0, sizeof(codec));
+ codec.mGMPApiVersion = kGMPVersion34;
+ codec.mLogLevel = GetGMPLibraryLogLevel();
+
+ // XXX this is currently a hack
+ // GMPVideoCodecUnion codecSpecific;
+ // memset(&codecSpecific, 0, sizeof(codecSpecific));
+ nsTArray<uint8_t> codecSpecific;
+ nsresult rv = mGMP->InitDecode(codec, codecSpecific, this, 1);
+ if (NS_FAILED(rv)) {
+ *aErrorOut = "GMP Decode: InitDecode failed";
+ mQueuedFrames.Clear();
+ return WEBRTC_VIDEO_CODEC_ERROR;
+ }
+
+ // now release any frames that got queued waiting for InitDone
+ if (!mQueuedFrames.IsEmpty()) {
+ // So we're safe to call Decode_g(), which asserts it's empty
+ nsTArray<UniquePtr<GMPDecodeData>> temp = std::move(mQueuedFrames);
+ for (auto& queued : temp) {
+ Decode_g(RefPtr<WebrtcGmpVideoDecoder>(this), std::move(queued));
+ }
+ }
+
+ // This is an ugly solution to asynchronous decoding errors
+ // from Decode_g() not being returned to the synchronous Decode() method.
+ // If we don't return an error code at this point, our caller ultimately won't
+ // know to request a PLI and the video stream will remain frozen unless an IDR
+ // happens to arrive for other reasons. Bug 1492852 tracks implementing a
+ // proper solution.
+ if (mDecoderStatus != GMPNoErr) {
+ GMP_LOG_ERROR("%s: Decoder status is bad (%u)!", __PRETTY_FUNCTION__,
+ static_cast<unsigned>(mDecoderStatus));
+ return WEBRTC_VIDEO_CODEC_ERROR;
+ }
+
+ return WEBRTC_VIDEO_CODEC_OK;
+}
+
+void WebrtcGmpVideoDecoder::Close_g() {
+ GMPVideoDecoderProxy* gmp(mGMP);
+ mGMP = nullptr;
+ mHost = nullptr;
+ mInitting = false;
+
+ if (mCachedPluginId) {
+ mReleasePluginEvent.Notify(*mCachedPluginId);
+ }
+ mCachedPluginId = Nothing();
+
+ if (gmp) {
+ // Do this last, since this could cause us to be destroyed
+ gmp->Close();
+ }
+}
+
+int32_t WebrtcGmpVideoDecoder::Decode(const webrtc::EncodedImage& aInputImage,
+ bool aMissingFrames,
+ int64_t aRenderTimeMs) {
+ MOZ_ASSERT(mGMPThread);
+ MOZ_ASSERT(!NS_IsMainThread());
+ if (!aInputImage.size()) {
+ return WEBRTC_VIDEO_CODEC_ERROR;
+ }
+
+ MediaInfoFlag flag = MediaInfoFlag::None;
+ flag |= (aInputImage._frameType == webrtc::VideoFrameType::kVideoFrameKey
+ ? MediaInfoFlag::KeyFrame
+ : MediaInfoFlag::NonKeyFrame);
+ flag |= MediaInfoFlag::SoftwareDecoding;
+ flag |= MediaInfoFlag::VIDEO_H264;
+ mPerformanceRecorder.Start((aInputImage.Timestamp() * 1000ll) / 90,
+ "WebrtcGmpVideoDecoder"_ns, mTrackingId, flag);
+
+ // This is an ugly solution to asynchronous decoding errors
+ // from Decode_g() not being returned to the synchronous Decode() method.
+ // If we don't return an error code at this point, our caller ultimately won't
+ // know to request a PLI and the video stream will remain frozen unless an IDR
+ // happens to arrive for other reasons. Bug 1492852 tracks implementing a
+ // proper solution.
+ auto decodeData =
+ MakeUnique<GMPDecodeData>(aInputImage, aMissingFrames, aRenderTimeMs);
+
+ mGMPThread->Dispatch(WrapRunnableNM(&WebrtcGmpVideoDecoder::Decode_g,
+ RefPtr<WebrtcGmpVideoDecoder>(this),
+ std::move(decodeData)),
+ NS_DISPATCH_NORMAL);
+
+ if (mDecoderStatus != GMPNoErr) {
+ GMP_LOG_ERROR("%s: Decoder status is bad (%u)!", __PRETTY_FUNCTION__,
+ static_cast<unsigned>(mDecoderStatus));
+ return WEBRTC_VIDEO_CODEC_ERROR;
+ }
+
+ return WEBRTC_VIDEO_CODEC_OK;
+}
+
+/* static */
+void WebrtcGmpVideoDecoder::Decode_g(const RefPtr<WebrtcGmpVideoDecoder>& aThis,
+ UniquePtr<GMPDecodeData>&& aDecodeData) {
+ if (!aThis->mGMP) {
+ if (aThis->mInitting) {
+ // InitDone hasn't been called yet (race)
+ aThis->mQueuedFrames.AppendElement(std::move(aDecodeData));
+ return;
+ }
+ // destroyed via Terminate(), failed to init, or just not initted yet
+ GMP_LOG_DEBUG("GMP Decode: not initted yet");
+
+ aThis->mDecoderStatus = GMPDecodeErr;
+ return;
+ }
+
+ MOZ_ASSERT(aThis->mQueuedFrames.IsEmpty());
+ MOZ_ASSERT(aThis->mHost);
+
+ GMPVideoFrame* ftmp = nullptr;
+ GMPErr err = aThis->mHost->CreateFrame(kGMPEncodedVideoFrame, &ftmp);
+ if (err != GMPNoErr) {
+ GMP_LOG_ERROR("%s: CreateFrame failed (%u)!", __PRETTY_FUNCTION__,
+ static_cast<unsigned>(err));
+ aThis->mDecoderStatus = err;
+ return;
+ }
+
+ GMPUniquePtr<GMPVideoEncodedFrame> frame(
+ static_cast<GMPVideoEncodedFrame*>(ftmp));
+ err = frame->CreateEmptyFrame(aDecodeData->mImage.size());
+ if (err != GMPNoErr) {
+ GMP_LOG_ERROR("%s: CreateEmptyFrame failed (%u)!", __PRETTY_FUNCTION__,
+ static_cast<unsigned>(err));
+ aThis->mDecoderStatus = err;
+ return;
+ }
+
+ // XXX At this point, we only will get mode1 data (a single length and a
+ // buffer) Session_info.cc/etc code needs to change to support mode 0.
+ *(reinterpret_cast<uint32_t*>(frame->Buffer())) = frame->Size();
+
+ // XXX It'd be wonderful not to have to memcpy the encoded data!
+ memcpy(frame->Buffer() + 4, aDecodeData->mImage.data() + 4,
+ frame->Size() - 4);
+
+ frame->SetEncodedWidth(aDecodeData->mImage._encodedWidth);
+ frame->SetEncodedHeight(aDecodeData->mImage._encodedHeight);
+ frame->SetTimeStamp((aDecodeData->mImage.Timestamp() * 1000ll) /
+ 90); // rounds down
+ frame->SetCompleteFrame(
+ true); // upstream no longer deals with incomplete frames
+ frame->SetBufferType(GMP_BufferLength32);
+
+ GMPVideoFrameType ft;
+ int32_t ret =
+ WebrtcFrameTypeToGmpFrameType(aDecodeData->mImage._frameType, &ft);
+ if (ret != WEBRTC_VIDEO_CODEC_OK) {
+ GMP_LOG_ERROR("%s: WebrtcFrameTypeToGmpFrameType failed (%u)!",
+ __PRETTY_FUNCTION__, static_cast<unsigned>(ret));
+ aThis->mDecoderStatus = GMPDecodeErr;
+ return;
+ }
+
+ // Bug XXXXXX: Set codecSpecific info
+ GMPCodecSpecificInfo info;
+ memset(&info, 0, sizeof(info));
+ info.mCodecType = kGMPVideoCodecH264;
+ info.mCodecSpecific.mH264.mSimulcastIdx = 0;
+ nsTArray<uint8_t> codecSpecificInfo;
+ codecSpecificInfo.AppendElements((uint8_t*)&info,
+ sizeof(GMPCodecSpecificInfo));
+
+ GMP_LOG_DEBUG("GMP Decode: %" PRIu64 ", len %zu%s", frame->TimeStamp(),
+ aDecodeData->mImage.size(),
+ ft == kGMPKeyFrame ? ", KeyFrame" : "");
+
+ nsresult rv =
+ aThis->mGMP->Decode(std::move(frame), aDecodeData->mMissingFrames,
+ codecSpecificInfo, aDecodeData->mRenderTimeMs);
+ if (NS_FAILED(rv)) {
+ GMP_LOG_ERROR("%s: Decode failed (rv=%u)!", __PRETTY_FUNCTION__,
+ static_cast<unsigned>(rv));
+ aThis->mDecoderStatus = GMPDecodeErr;
+ return;
+ }
+
+ aThis->mDecoderStatus = GMPNoErr;
+}
+
+int32_t WebrtcGmpVideoDecoder::RegisterDecodeCompleteCallback(
+ webrtc::DecodedImageCallback* aCallback) {
+ MutexAutoLock lock(mCallbackMutex);
+ mCallback = aCallback;
+
+ return WEBRTC_VIDEO_CODEC_OK;
+}
+
+/* static */
+void WebrtcGmpVideoDecoder::ReleaseGmp_g(
+ const RefPtr<WebrtcGmpVideoDecoder>& aDecoder) {
+ aDecoder->Close_g();
+}
+
+int32_t WebrtcGmpVideoDecoder::ReleaseGmp() {
+ GMP_LOG_DEBUG("GMP Released:");
+ RegisterDecodeCompleteCallback(nullptr);
+
+ if (mGMPThread) {
+ mGMPThread->Dispatch(WrapRunnableNM(&WebrtcGmpVideoDecoder::ReleaseGmp_g,
+ RefPtr<WebrtcGmpVideoDecoder>(this)),
+ NS_DISPATCH_NORMAL);
+ }
+ return WEBRTC_VIDEO_CODEC_OK;
+}
+
+void WebrtcGmpVideoDecoder::Terminated() {
+ GMP_LOG_DEBUG("GMP Decoder Terminated: %p", (void*)this);
+
+ GMPVideoDecoderProxy* gmp(mGMP);
+ mGMP = nullptr;
+ mHost = nullptr;
+ mInitting = false;
+
+ if (gmp) {
+ // Do this last, since this could cause us to be destroyed
+ gmp->Close();
+ }
+
+ // Could now notify that it's dead
+}
+
+void WebrtcGmpVideoDecoder::Decoded(GMPVideoi420Frame* aDecodedFrame) {
+ // we have two choices here: wrap the frame with a callback that frees
+ // the data later (risking running out of shmems), or copy the data out
+ // always. Also, we can only Destroy() the frame on the gmp thread, so
+ // copying is simplest if expensive.
+ // I420 size including rounding...
+ CheckedInt32 length =
+ (CheckedInt32(aDecodedFrame->Stride(kGMPYPlane)) *
+ aDecodedFrame->Height()) +
+ (aDecodedFrame->Stride(kGMPVPlane) + aDecodedFrame->Stride(kGMPUPlane)) *
+ ((aDecodedFrame->Height() + 1) / 2);
+ int32_t size = length.value();
+ MOZ_RELEASE_ASSERT(length.isValid() && size > 0);
+
+ // Don't use MakeUniqueFallible here, because UniquePtr isn't copyable, and
+ // the closure below in WrapI420Buffer uses std::function which _is_ copyable.
+ // We'll alloc the buffer here, so we preserve the "fallible" nature, and
+ // then hand a shared_ptr, which is copyable, to WrapI420Buffer.
+ auto falliblebuffer = new (std::nothrow) uint8_t[size];
+ if (falliblebuffer) {
+ auto buffer = std::shared_ptr<uint8_t>(falliblebuffer);
+
+ // This is 3 separate buffers currently anyways, no use in trying to
+ // see if we can use a single memcpy.
+ uint8_t* buffer_y = buffer.get();
+ memcpy(buffer_y, aDecodedFrame->Buffer(kGMPYPlane),
+ aDecodedFrame->Stride(kGMPYPlane) * aDecodedFrame->Height());
+ // Should this be aligned, making it non-contiguous? Assume no, this is
+ // already factored into the strides.
+ uint8_t* buffer_u =
+ buffer_y + aDecodedFrame->Stride(kGMPYPlane) * aDecodedFrame->Height();
+ memcpy(buffer_u, aDecodedFrame->Buffer(kGMPUPlane),
+ aDecodedFrame->Stride(kGMPUPlane) *
+ ((aDecodedFrame->Height() + 1) / 2));
+ uint8_t* buffer_v = buffer_u + aDecodedFrame->Stride(kGMPUPlane) *
+ ((aDecodedFrame->Height() + 1) / 2);
+ memcpy(buffer_v, aDecodedFrame->Buffer(kGMPVPlane),
+ aDecodedFrame->Stride(kGMPVPlane) *
+ ((aDecodedFrame->Height() + 1) / 2));
+
+ MutexAutoLock lock(mCallbackMutex);
+ if (mCallback) {
+ // Note: the last parameter to WrapI420Buffer is named no_longer_used,
+ // but is currently called in the destructor of WrappedYuvBuffer when
+ // the buffer is "no_longer_used".
+ rtc::scoped_refptr<webrtc::I420BufferInterface> video_frame_buffer =
+ webrtc::WrapI420Buffer(
+ aDecodedFrame->Width(), aDecodedFrame->Height(), buffer_y,
+ aDecodedFrame->Stride(kGMPYPlane), buffer_u,
+ aDecodedFrame->Stride(kGMPUPlane), buffer_v,
+ aDecodedFrame->Stride(kGMPVPlane), [buffer] {});
+
+ GMP_LOG_DEBUG("GMP Decoded: %" PRIu64, aDecodedFrame->Timestamp());
+ auto videoFrame =
+ webrtc::VideoFrame::Builder()
+ .set_video_frame_buffer(video_frame_buffer)
+ .set_timestamp_rtp(
+ // round up
+ (aDecodedFrame->UpdatedTimestamp() * 90ll + 999) / 1000)
+ .build();
+ mPerformanceRecorder.Record(
+ static_cast<int64_t>(aDecodedFrame->Timestamp()),
+ [&](DecodeStage& aStage) {
+ aStage.SetImageFormat(DecodeStage::YUV420P);
+ aStage.SetResolution(aDecodedFrame->Width(),
+ aDecodedFrame->Height());
+ aStage.SetColorDepth(gfx::ColorDepth::COLOR_8);
+ });
+ mCallback->Decoded(videoFrame);
+ }
+ }
+ aDecodedFrame->Destroy();
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/libwebrtcglue/WebrtcGmpVideoCodec.h b/dom/media/webrtc/libwebrtcglue/WebrtcGmpVideoCodec.h
new file mode 100644
index 0000000000..ef3e1bd5af
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/WebrtcGmpVideoCodec.h
@@ -0,0 +1,507 @@
+/*
+ * Copyright (c) 2012, The WebRTC project authors. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *
+ * * Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in
+ * the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the name of Google nor the names of its contributors may
+ * be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#ifndef WEBRTCGMPVIDEOCODEC_H_
+#define WEBRTCGMPVIDEOCODEC_H_
+
+#include <queue>
+#include <string>
+
+#include "nsThreadUtils.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/Telemetry.h"
+
+#include "mozIGeckoMediaPluginService.h"
+#include "MediaConduitInterface.h"
+#include "AudioConduit.h"
+#include "PerformanceRecorder.h"
+#include "VideoConduit.h"
+#include "api/video/video_frame_type.h"
+#include "modules/video_coding/include/video_codec_interface.h"
+#include "common_video/h264/h264_bitstream_parser.h"
+
+#include "gmp-video-host.h"
+#include "GMPVideoDecoderProxy.h"
+#include "GMPVideoEncoderProxy.h"
+
+#include "jsapi/PeerConnectionImpl.h"
+
+namespace mozilla {
+
+class GmpInitDoneRunnable : public Runnable {
+ public:
+ explicit GmpInitDoneRunnable(std::string aPCHandle)
+ : Runnable("GmpInitDoneRunnable"),
+ mResult(WEBRTC_VIDEO_CODEC_OK),
+ mPCHandle(std::move(aPCHandle)) {}
+
+ NS_IMETHOD Run() override {
+ Telemetry::Accumulate(Telemetry::WEBRTC_GMP_INIT_SUCCESS,
+ mResult == WEBRTC_VIDEO_CODEC_OK);
+ if (mResult == WEBRTC_VIDEO_CODEC_OK) {
+ // Might be useful to notify the PeerConnection about successful init
+ // someday.
+ return NS_OK;
+ }
+
+ PeerConnectionWrapper wrapper(mPCHandle);
+ if (wrapper.impl()) {
+ wrapper.impl()->OnMediaError(mError);
+ }
+ return NS_OK;
+ }
+
+ void Dispatch(int32_t aResult, const std::string& aError = "") {
+ mResult = aResult;
+ mError = aError;
+ nsCOMPtr<nsIThread> mainThread(do_GetMainThread());
+ if (mainThread) {
+ // For some reason, the compiler on CI is treating |this| as a const
+ // pointer, despite the fact that we're in a non-const function. And,
+ // interestingly enough, correcting this doesn't require a const_cast.
+ mainThread->Dispatch(do_AddRef(static_cast<nsIRunnable*>(this)),
+ NS_DISPATCH_NORMAL);
+ }
+ }
+
+ int32_t Result() { return mResult; }
+
+ private:
+ int32_t mResult;
+ const std::string mPCHandle;
+ std::string mError;
+};
+
+// Hold a frame for later decode
+class GMPDecodeData {
+ public:
+ GMPDecodeData(const webrtc::EncodedImage& aInputImage, bool aMissingFrames,
+ int64_t aRenderTimeMs)
+ : mImage(aInputImage),
+ mMissingFrames(aMissingFrames),
+ mRenderTimeMs(aRenderTimeMs) {
+ // We want to use this for queuing, and the calling code recycles the
+ // buffer on return from Decode()
+ MOZ_RELEASE_ASSERT(aInputImage.size() <
+ (std::numeric_limits<size_t>::max() >> 1));
+ }
+
+ ~GMPDecodeData() = default;
+
+ const webrtc::EncodedImage mImage;
+ const bool mMissingFrames;
+ const int64_t mRenderTimeMs;
+};
+
+class RefCountedWebrtcVideoEncoder {
+ public:
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+ // Implement sort of WebrtcVideoEncoder interface and support refcounting.
+ // (We cannot use |Release|, since that's needed for nsRefPtr)
+ virtual int32_t InitEncode(
+ const webrtc::VideoCodec* aCodecSettings,
+ const webrtc::VideoEncoder::Settings& aSettings) = 0;
+
+ virtual int32_t Encode(
+ const webrtc::VideoFrame& aInputImage,
+ const std::vector<webrtc::VideoFrameType>* aFrameTypes) = 0;
+
+ virtual int32_t RegisterEncodeCompleteCallback(
+ webrtc::EncodedImageCallback* aCallback) = 0;
+
+ virtual int32_t Shutdown() = 0;
+
+ virtual int32_t SetRates(
+ const webrtc::VideoEncoder::RateControlParameters& aParameters) = 0;
+
+ virtual MediaEventSource<uint64_t>* InitPluginEvent() = 0;
+
+ virtual MediaEventSource<uint64_t>* ReleasePluginEvent() = 0;
+
+ virtual WebrtcVideoEncoder::EncoderInfo GetEncoderInfo() const = 0;
+
+ protected:
+ virtual ~RefCountedWebrtcVideoEncoder() = default;
+};
+
+class WebrtcGmpVideoEncoder : public GMPVideoEncoderCallbackProxy,
+ public RefCountedWebrtcVideoEncoder {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WebrtcGmpVideoEncoder, final);
+
+ WebrtcGmpVideoEncoder(const webrtc::SdpVideoFormat& aFormat,
+ std::string aPCHandle);
+
+ // Implement VideoEncoder interface, sort of.
+ // (We cannot use |Release|, since that's needed for nsRefPtr)
+ int32_t InitEncode(const webrtc::VideoCodec* aCodecSettings,
+ const webrtc::VideoEncoder::Settings& aSettings) override;
+
+ int32_t Encode(
+ const webrtc::VideoFrame& aInputImage,
+ const std::vector<webrtc::VideoFrameType>* aFrameTypes) override;
+
+ int32_t RegisterEncodeCompleteCallback(
+ webrtc::EncodedImageCallback* aCallback) override;
+
+ int32_t Shutdown() override;
+
+ int32_t SetRates(
+ const webrtc::VideoEncoder::RateControlParameters& aParameters) override;
+
+ WebrtcVideoEncoder::EncoderInfo GetEncoderInfo() const override;
+
+ MediaEventSource<uint64_t>* InitPluginEvent() override {
+ return &mInitPluginEvent;
+ }
+
+ MediaEventSource<uint64_t>* ReleasePluginEvent() override {
+ return &mReleasePluginEvent;
+ }
+
+ // GMPVideoEncoderCallback virtual functions.
+ virtual void Terminated() override;
+
+ virtual void Encoded(GMPVideoEncodedFrame* aEncodedFrame,
+ const nsTArray<uint8_t>& aCodecSpecificInfo) override;
+
+ virtual void Error(GMPErr aError) override {}
+
+ private:
+ virtual ~WebrtcGmpVideoEncoder();
+
+ static void InitEncode_g(const RefPtr<WebrtcGmpVideoEncoder>& aThis,
+ const GMPVideoCodec& aCodecParams,
+ int32_t aNumberOfCores, uint32_t aMaxPayloadSize,
+ const RefPtr<GmpInitDoneRunnable>& aInitDone);
+ int32_t GmpInitDone(GMPVideoEncoderProxy* aGMP, GMPVideoHost* aHost,
+ const GMPVideoCodec& aCodecParams,
+ std::string* aErrorOut);
+ int32_t GmpInitDone(GMPVideoEncoderProxy* aGMP, GMPVideoHost* aHost,
+ std::string* aErrorOut);
+ int32_t InitEncoderForSize(unsigned short aWidth, unsigned short aHeight,
+ std::string* aErrorOut);
+ static void ReleaseGmp_g(const RefPtr<WebrtcGmpVideoEncoder>& aEncoder);
+ void Close_g();
+
+ class InitDoneCallback : public GetGMPVideoEncoderCallback {
+ public:
+ InitDoneCallback(const RefPtr<WebrtcGmpVideoEncoder>& aEncoder,
+ const RefPtr<GmpInitDoneRunnable>& aInitDone,
+ const GMPVideoCodec& aCodecParams)
+ : mEncoder(aEncoder),
+ mInitDone(aInitDone),
+ mCodecParams(aCodecParams) {}
+
+ virtual void Done(GMPVideoEncoderProxy* aGMP,
+ GMPVideoHost* aHost) override {
+ std::string errorOut;
+ int32_t result =
+ mEncoder->GmpInitDone(aGMP, aHost, mCodecParams, &errorOut);
+
+ mInitDone->Dispatch(result, errorOut);
+ }
+
+ private:
+ const RefPtr<WebrtcGmpVideoEncoder> mEncoder;
+ const RefPtr<GmpInitDoneRunnable> mInitDone;
+ const GMPVideoCodec mCodecParams;
+ };
+
+ static void Encode_g(const RefPtr<WebrtcGmpVideoEncoder>& aEncoder,
+ webrtc::VideoFrame aInputImage,
+ std::vector<webrtc::VideoFrameType> aFrameTypes);
+ void RegetEncoderForResolutionChange(
+ uint32_t aWidth, uint32_t aHeight,
+ const RefPtr<GmpInitDoneRunnable>& aInitDone);
+
+ class InitDoneForResolutionChangeCallback
+ : public GetGMPVideoEncoderCallback {
+ public:
+ InitDoneForResolutionChangeCallback(
+ const RefPtr<WebrtcGmpVideoEncoder>& aEncoder,
+ const RefPtr<GmpInitDoneRunnable>& aInitDone, uint32_t aWidth,
+ uint32_t aHeight)
+ : mEncoder(aEncoder),
+ mInitDone(aInitDone),
+ mWidth(aWidth),
+ mHeight(aHeight) {}
+
+ virtual void Done(GMPVideoEncoderProxy* aGMP,
+ GMPVideoHost* aHost) override {
+ std::string errorOut;
+ int32_t result = mEncoder->GmpInitDone(aGMP, aHost, &errorOut);
+ if (result != WEBRTC_VIDEO_CODEC_OK) {
+ mInitDone->Dispatch(result, errorOut);
+ return;
+ }
+
+ result = mEncoder->InitEncoderForSize(mWidth, mHeight, &errorOut);
+ mInitDone->Dispatch(result, errorOut);
+ }
+
+ private:
+ const RefPtr<WebrtcGmpVideoEncoder> mEncoder;
+ const RefPtr<GmpInitDoneRunnable> mInitDone;
+ const uint32_t mWidth;
+ const uint32_t mHeight;
+ };
+
+ static int32_t SetRates_g(RefPtr<WebrtcGmpVideoEncoder> aThis,
+ uint32_t aNewBitRateKbps, Maybe<double> aFrameRate);
+
+ nsCOMPtr<mozIGeckoMediaPluginService> mMPS;
+ nsCOMPtr<nsIThread> mGMPThread;
+ GMPVideoEncoderProxy* mGMP;
+ // Used to handle a race where Release() is called while init is in progress
+ bool mInitting;
+ GMPVideoHost* mHost;
+ GMPVideoCodec mCodecParams;
+ uint32_t mMaxPayloadSize;
+ const webrtc::SdpVideoFormat::Parameters mFormatParams;
+ webrtc::CodecSpecificInfo mCodecSpecificInfo;
+ webrtc::H264BitstreamParser mH264BitstreamParser;
+ // Protects mCallback
+ Mutex mCallbackMutex MOZ_UNANNOTATED;
+ webrtc::EncodedImageCallback* mCallback;
+ Maybe<uint64_t> mCachedPluginId;
+ const std::string mPCHandle;
+
+ struct InputImageData {
+ int64_t timestamp_us;
+ };
+ // Map rtp time -> input image data
+ DataMutex<std::map<uint32_t, InputImageData>> mInputImageMap;
+
+ MediaEventProducer<uint64_t> mInitPluginEvent;
+ MediaEventProducer<uint64_t> mReleasePluginEvent;
+};
+
+// Basically a strong ref to a RefCountedWebrtcVideoEncoder, that also
+// translates from Release() to RefCountedWebrtcVideoEncoder::Shutdown(),
+// since we need RefCountedWebrtcVideoEncoder::Release() for managing the
+// refcount. The webrtc.org code gets one of these, so it doesn't unilaterally
+// delete the "real" encoder.
+class WebrtcVideoEncoderProxy : public WebrtcVideoEncoder {
+ public:
+ explicit WebrtcVideoEncoderProxy(
+ RefPtr<RefCountedWebrtcVideoEncoder> aEncoder)
+ : mEncoderImpl(std::move(aEncoder)) {}
+
+ virtual ~WebrtcVideoEncoderProxy() {
+ RegisterEncodeCompleteCallback(nullptr);
+ }
+
+ MediaEventSource<uint64_t>* InitPluginEvent() override {
+ return mEncoderImpl->InitPluginEvent();
+ }
+
+ MediaEventSource<uint64_t>* ReleasePluginEvent() override {
+ return mEncoderImpl->ReleasePluginEvent();
+ }
+
+ int32_t InitEncode(const webrtc::VideoCodec* aCodecSettings,
+ const WebrtcVideoEncoder::Settings& aSettings) override {
+ return mEncoderImpl->InitEncode(aCodecSettings, aSettings);
+ }
+
+ int32_t Encode(
+ const webrtc::VideoFrame& aInputImage,
+ const std::vector<webrtc::VideoFrameType>* aFrameTypes) override {
+ return mEncoderImpl->Encode(aInputImage, aFrameTypes);
+ }
+
+ int32_t RegisterEncodeCompleteCallback(
+ webrtc::EncodedImageCallback* aCallback) override {
+ return mEncoderImpl->RegisterEncodeCompleteCallback(aCallback);
+ }
+
+ int32_t Release() override { return mEncoderImpl->Shutdown(); }
+
+ void SetRates(const RateControlParameters& aParameters) override {
+ mEncoderImpl->SetRates(aParameters);
+ }
+
+ EncoderInfo GetEncoderInfo() const override {
+ return mEncoderImpl->GetEncoderInfo();
+ }
+
+ private:
+ const RefPtr<RefCountedWebrtcVideoEncoder> mEncoderImpl;
+};
+
+class WebrtcGmpVideoDecoder : public GMPVideoDecoderCallbackProxy {
+ public:
+ WebrtcGmpVideoDecoder(std::string aPCHandle, TrackingId aTrackingId);
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WebrtcGmpVideoDecoder, final);
+
+ // Implement VideoEncoder interface, sort of.
+ // (We cannot use |Release|, since that's needed for nsRefPtr)
+ virtual bool Configure(const webrtc::VideoDecoder::Settings& settings);
+ virtual int32_t Decode(const webrtc::EncodedImage& aInputImage,
+ bool aMissingFrames, int64_t aRenderTimeMs);
+ virtual int32_t RegisterDecodeCompleteCallback(
+ webrtc::DecodedImageCallback* aCallback);
+
+ virtual int32_t ReleaseGmp();
+
+ MediaEventSource<uint64_t>* InitPluginEvent() { return &mInitPluginEvent; }
+
+ MediaEventSource<uint64_t>* ReleasePluginEvent() {
+ return &mReleasePluginEvent;
+ }
+
+ // GMPVideoDecoderCallbackProxy
+ virtual void Terminated() override;
+
+ virtual void Decoded(GMPVideoi420Frame* aDecodedFrame) override;
+
+ virtual void ReceivedDecodedReferenceFrame(
+ const uint64_t aPictureId) override {
+ MOZ_CRASH();
+ }
+
+ virtual void ReceivedDecodedFrame(const uint64_t aPictureId) override {
+ MOZ_CRASH();
+ }
+
+ virtual void InputDataExhausted() override {}
+
+ virtual void DrainComplete() override {}
+
+ virtual void ResetComplete() override {}
+
+ virtual void Error(GMPErr aError) override { mDecoderStatus = aError; }
+
+ private:
+ virtual ~WebrtcGmpVideoDecoder();
+
+ static void Configure_g(const RefPtr<WebrtcGmpVideoDecoder>& aThis,
+ const webrtc::VideoDecoder::Settings& settings,
+ const RefPtr<GmpInitDoneRunnable>& aInitDone);
+ int32_t GmpInitDone(GMPVideoDecoderProxy* aGMP, GMPVideoHost* aHost,
+ std::string* aErrorOut);
+ static void ReleaseGmp_g(const RefPtr<WebrtcGmpVideoDecoder>& aDecoder);
+ void Close_g();
+
+ class InitDoneCallback : public GetGMPVideoDecoderCallback {
+ public:
+ explicit InitDoneCallback(const RefPtr<WebrtcGmpVideoDecoder>& aDecoder,
+ const RefPtr<GmpInitDoneRunnable>& aInitDone)
+ : mDecoder(aDecoder), mInitDone(aInitDone) {}
+
+ virtual void Done(GMPVideoDecoderProxy* aGMP,
+ GMPVideoHost* aHost) override {
+ std::string errorOut;
+ int32_t result = mDecoder->GmpInitDone(aGMP, aHost, &errorOut);
+
+ mInitDone->Dispatch(result, errorOut);
+ }
+
+ private:
+ const RefPtr<WebrtcGmpVideoDecoder> mDecoder;
+ const RefPtr<GmpInitDoneRunnable> mInitDone;
+ };
+
+ static void Decode_g(const RefPtr<WebrtcGmpVideoDecoder>& aThis,
+ UniquePtr<GMPDecodeData>&& aDecodeData);
+
+ nsCOMPtr<mozIGeckoMediaPluginService> mMPS;
+ nsCOMPtr<nsIThread> mGMPThread;
+ GMPVideoDecoderProxy* mGMP; // Addref is held for us
+ // Used to handle a race where Release() is called while init is in progress
+ bool mInitting;
+ // Frames queued for decode while mInitting is true
+ nsTArray<UniquePtr<GMPDecodeData>> mQueuedFrames;
+ GMPVideoHost* mHost;
+ // Protects mCallback
+ Mutex mCallbackMutex MOZ_UNANNOTATED;
+ webrtc::DecodedImageCallback* mCallback;
+ Maybe<uint64_t> mCachedPluginId;
+ Atomic<GMPErr, ReleaseAcquire> mDecoderStatus;
+ const std::string mPCHandle;
+ const TrackingId mTrackingId;
+ PerformanceRecorderMulti<DecodeStage> mPerformanceRecorder;
+
+ MediaEventProducer<uint64_t> mInitPluginEvent;
+ MediaEventProducer<uint64_t> mReleasePluginEvent;
+};
+
+// Basically a strong ref to a WebrtcGmpVideoDecoder, that also translates
+// from Release() to WebrtcGmpVideoDecoder::ReleaseGmp(), since we need
+// WebrtcGmpVideoDecoder::Release() for managing the refcount.
+// The webrtc.org code gets one of these, so it doesn't unilaterally delete
+// the "real" encoder.
+class WebrtcVideoDecoderProxy : public WebrtcVideoDecoder {
+ public:
+ explicit WebrtcVideoDecoderProxy(std::string aPCHandle,
+ TrackingId aTrackingId)
+ : mDecoderImpl(new WebrtcGmpVideoDecoder(std::move(aPCHandle),
+ std::move(aTrackingId))) {}
+
+ virtual ~WebrtcVideoDecoderProxy() {
+ RegisterDecodeCompleteCallback(nullptr);
+ }
+
+ MediaEventSource<uint64_t>* InitPluginEvent() override {
+ return mDecoderImpl->InitPluginEvent();
+ }
+
+ MediaEventSource<uint64_t>* ReleasePluginEvent() override {
+ return mDecoderImpl->ReleasePluginEvent();
+ }
+
+ bool Configure(const Settings& settings) override {
+ return mDecoderImpl->Configure(settings);
+ }
+
+ int32_t Decode(const webrtc::EncodedImage& aInputImage, bool aMissingFrames,
+ int64_t aRenderTimeMs) override {
+ return mDecoderImpl->Decode(aInputImage, aMissingFrames, aRenderTimeMs);
+ }
+
+ int32_t RegisterDecodeCompleteCallback(
+ webrtc::DecodedImageCallback* aCallback) override {
+ return mDecoderImpl->RegisterDecodeCompleteCallback(aCallback);
+ }
+
+ int32_t Release() override { return mDecoderImpl->ReleaseGmp(); }
+
+ private:
+ const RefPtr<WebrtcGmpVideoDecoder> mDecoderImpl;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/libwebrtcglue/WebrtcImageBuffer.h b/dom/media/webrtc/libwebrtcglue/WebrtcImageBuffer.h
new file mode 100644
index 0000000000..305f4df577
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/WebrtcImageBuffer.h
@@ -0,0 +1,53 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef WebrtcImageBuffer_h__
+#define WebrtcImageBuffer_h__
+
+#include "common_video/include/video_frame_buffer.h"
+
+namespace mozilla {
+namespace layers {
+class Image;
+}
+
+class ImageBuffer : public webrtc::VideoFrameBuffer {
+ public:
+ explicit ImageBuffer(RefPtr<layers::Image>&& aImage)
+ : mImage(std::move(aImage)) {}
+
+ rtc::scoped_refptr<webrtc::I420BufferInterface> ToI420() override {
+ RefPtr<layers::PlanarYCbCrImage> image = mImage->AsPlanarYCbCrImage();
+ MOZ_ASSERT(image);
+ if (!image) {
+ // TODO. YUV420 ReadBack, Image only provides a RGB readback.
+ return nullptr;
+ }
+ const layers::PlanarYCbCrData* data = image->GetData();
+ rtc::scoped_refptr<webrtc::I420BufferInterface> buf =
+ webrtc::WrapI420Buffer(
+ data->mPictureRect.width, data->mPictureRect.height,
+ data->mYChannel, data->mYStride, data->mCbChannel,
+ data->mCbCrStride, data->mCrChannel, data->mCbCrStride,
+ [image] { /* keep reference alive*/ });
+ return buf;
+ }
+
+ Type type() const override { return Type::kNative; }
+
+ int width() const override { return mImage->GetSize().width; }
+
+ int height() const override { return mImage->GetSize().height; }
+
+ RefPtr<layers::Image> GetNativeImage() const { return mImage; }
+
+ private:
+ const RefPtr<layers::Image> mImage;
+};
+
+} // namespace mozilla
+
+#endif // WebrtcImageBuffer_h__
diff --git a/dom/media/webrtc/libwebrtcglue/WebrtcMediaDataDecoderCodec.cpp b/dom/media/webrtc/libwebrtcglue/WebrtcMediaDataDecoderCodec.cpp
new file mode 100644
index 0000000000..f132e6a124
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/WebrtcMediaDataDecoderCodec.cpp
@@ -0,0 +1,209 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WebrtcMediaDataDecoderCodec.h"
+
+#include "ImageContainer.h"
+#include "MediaDataDecoderProxy.h"
+#include "PDMFactory.h"
+#include "VideoUtils.h"
+#include "mozilla/layers/ImageBridgeChild.h"
+#include "mozilla/media/MediaUtils.h"
+#include "mozilla/StaticPrefs_media.h"
+
+namespace mozilla {
+
+WebrtcMediaDataDecoder::WebrtcMediaDataDecoder(nsACString& aCodecMimeType,
+ TrackingId aTrackingId)
+ : mThreadPool(GetMediaThreadPool(MediaThreadType::SUPERVISOR)),
+ mTaskQueue(TaskQueue::Create(do_AddRef(mThreadPool),
+ "WebrtcMediaDataDecoder::mTaskQueue")),
+ mImageContainer(MakeAndAddRef<layers::ImageContainer>(
+ layers::ImageContainer::ASYNCHRONOUS)),
+ mFactory(new PDMFactory()),
+ mTrackType(TrackInfo::kUndefinedTrack),
+ mCodecType(aCodecMimeType),
+ mTrackingId(std::move(aTrackingId)) {}
+
+WebrtcMediaDataDecoder::~WebrtcMediaDataDecoder() {}
+
+bool WebrtcMediaDataDecoder::Configure(
+ const webrtc::VideoDecoder::Settings& settings) {
+ nsCString codec;
+ mTrackType = TrackInfo::kVideoTrack;
+ mInfo = VideoInfo(settings.max_render_resolution().Width(),
+ settings.max_render_resolution().Height());
+ mInfo.mMimeType = mCodecType;
+
+#ifdef MOZ_WIDGET_GTK
+ if (mInfo.mMimeType.EqualsLiteral("video/vp8") &&
+ !StaticPrefs::media_navigator_mediadatadecoder_vp8_hardware_enabled()) {
+ mDisabledHardwareAcceleration = true;
+ }
+#endif
+
+ return WEBRTC_VIDEO_CODEC_OK == CreateDecoder();
+}
+
+int32_t WebrtcMediaDataDecoder::Decode(const webrtc::EncodedImage& aInputImage,
+ bool aMissingFrames,
+ int64_t aRenderTimeMs) {
+ if (!mCallback || !mDecoder) {
+ return WEBRTC_VIDEO_CODEC_UNINITIALIZED;
+ }
+
+ if (!aInputImage.data() || !aInputImage.size()) {
+ return WEBRTC_VIDEO_CODEC_ERR_PARAMETER;
+ }
+
+ // Always start with a complete key frame.
+ if (mNeedKeyframe) {
+ if (aInputImage._frameType != webrtc::VideoFrameType::kVideoFrameKey)
+ return WEBRTC_VIDEO_CODEC_ERROR;
+ // We have a key frame - is it complete?
+ mNeedKeyframe = false;
+ }
+
+ auto disabledHardwareAcceleration =
+ MakeScopeExit([&] { mDisabledHardwareAcceleration = true; });
+
+ RefPtr<MediaRawData> compressedFrame =
+ new MediaRawData(aInputImage.data(), aInputImage.size());
+ if (!compressedFrame->Data()) {
+ return WEBRTC_VIDEO_CODEC_MEMORY;
+ }
+
+ compressedFrame->mTime =
+ media::TimeUnit::FromMicroseconds(aInputImage.Timestamp());
+ compressedFrame->mTimecode =
+ media::TimeUnit::FromMicroseconds(aRenderTimeMs * 1000);
+ compressedFrame->mKeyframe =
+ aInputImage._frameType == webrtc::VideoFrameType::kVideoFrameKey;
+ {
+ media::Await(
+ do_AddRef(mThreadPool), mDecoder->Decode(compressedFrame),
+ [&](const MediaDataDecoder::DecodedData& aResults) {
+ mResults = aResults.Clone();
+ mError = NS_OK;
+ },
+ [&](const MediaResult& aError) { mError = aError; });
+
+ for (auto& frame : mResults) {
+ MOZ_ASSERT(frame->mType == MediaData::Type::VIDEO_DATA);
+ RefPtr<VideoData> video = frame->As<VideoData>();
+ MOZ_ASSERT(video);
+ if (!video->mImage) {
+ // Nothing to display.
+ continue;
+ }
+ rtc::scoped_refptr<ImageBuffer> image(
+ new rtc::RefCountedObject<ImageBuffer>(std::move(video->mImage)));
+
+ auto videoFrame = webrtc::VideoFrame::Builder()
+ .set_video_frame_buffer(image)
+ .set_timestamp_rtp(aInputImage.Timestamp())
+ .set_rotation(aInputImage.rotation_)
+ .build();
+ mCallback->Decoded(videoFrame);
+ }
+ mResults.Clear();
+ }
+
+ if (NS_FAILED(mError) && mError != NS_ERROR_DOM_MEDIA_CANCELED) {
+ CreateDecoder();
+ return WEBRTC_VIDEO_CODEC_ERROR;
+ }
+
+ if (NS_FAILED(mError)) {
+ return WEBRTC_VIDEO_CODEC_ERROR;
+ }
+
+ disabledHardwareAcceleration.release();
+ return WEBRTC_VIDEO_CODEC_OK;
+}
+
+int32_t WebrtcMediaDataDecoder::RegisterDecodeCompleteCallback(
+ webrtc::DecodedImageCallback* aCallback) {
+ mCallback = aCallback;
+ return WEBRTC_VIDEO_CODEC_OK;
+}
+
+int32_t WebrtcMediaDataDecoder::Release() {
+ if (mDecoder) {
+ RefPtr<MediaDataDecoder> decoder = std::move(mDecoder);
+ decoder->Flush()->Then(mTaskQueue, __func__,
+ [decoder]() { decoder->Shutdown(); });
+ }
+
+ mNeedKeyframe = true;
+ mError = NS_OK;
+
+ return WEBRTC_VIDEO_CODEC_OK;
+}
+
+bool WebrtcMediaDataDecoder::OnTaskQueue() const {
+ return mTaskQueue->IsOnCurrentThread();
+}
+
+int32_t WebrtcMediaDataDecoder::CreateDecoder() {
+ RefPtr<layers::KnowsCompositor> knowsCompositor =
+ layers::ImageBridgeChild::GetSingleton();
+
+ if (mDecoder) {
+ Release();
+ }
+
+ RefPtr<TaskQueue> tq =
+ TaskQueue::Create(GetMediaThreadPool(MediaThreadType::PLATFORM_DECODER),
+ "webrtc decode TaskQueue");
+ RefPtr<MediaDataDecoder> decoder;
+
+ media::Await(do_AddRef(mThreadPool), InvokeAsync(tq, __func__, [&] {
+ RefPtr<GenericPromise> p =
+ mFactory
+ ->CreateDecoder(
+ {mInfo,
+ CreateDecoderParams::OptionSet(
+ CreateDecoderParams::Option::LowLatency,
+ CreateDecoderParams::Option::FullH264Parsing,
+ CreateDecoderParams::Option::
+ ErrorIfNoInitializationData,
+ mDisabledHardwareAcceleration
+ ? CreateDecoderParams::Option::
+ HardwareDecoderNotAllowed
+ : CreateDecoderParams::Option::Default),
+ mTrackType, mImageContainer, knowsCompositor,
+ Some(mTrackingId)})
+ ->Then(
+ tq, __func__,
+ [&](RefPtr<MediaDataDecoder>&& aDecoder) {
+ decoder = std::move(aDecoder);
+ return GenericPromise::CreateAndResolve(
+ true, __func__);
+ },
+ [](const MediaResult& aResult) {
+ return GenericPromise::CreateAndReject(
+ NS_ERROR_DOM_MEDIA_FATAL_ERR, __func__);
+ });
+ return p;
+ }));
+
+ if (!decoder) {
+ return WEBRTC_VIDEO_CODEC_ERROR;
+ }
+
+ // We need to wrap our decoder in a MediaDataDecoderProxy so that it always
+ // run on an nsISerialEventTarget (which the webrtc code doesn't do)
+ mDecoder = new MediaDataDecoderProxy(decoder.forget(), tq.forget());
+
+ media::Await(
+ do_AddRef(mThreadPool), mDecoder->Init(),
+ [&](TrackInfo::TrackType) { mError = NS_OK; },
+ [&](const MediaResult& aError) { mError = aError; });
+
+ return NS_SUCCEEDED(mError) ? WEBRTC_VIDEO_CODEC_OK
+ : WEBRTC_VIDEO_CODEC_ERROR;
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/libwebrtcglue/WebrtcMediaDataDecoderCodec.h b/dom/media/webrtc/libwebrtcglue/WebrtcMediaDataDecoderCodec.h
new file mode 100644
index 0000000000..ccb54c692b
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/WebrtcMediaDataDecoderCodec.h
@@ -0,0 +1,70 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef WebrtcMediaDataDecoderCodec_h__
+#define WebrtcMediaDataDecoderCodec_h__
+
+#include "MediaConduitInterface.h"
+#include "MediaInfo.h"
+#include "MediaResult.h"
+#include "PlatformDecoderModule.h"
+#include "VideoConduit.h"
+#include "WebrtcImageBuffer.h"
+#include "common_video/include/video_frame_buffer.h"
+#include "modules/video_coding/include/video_codec_interface.h"
+
+namespace webrtc {
+class DecodedImageCallback;
+}
+namespace mozilla {
+namespace layers {
+class Image;
+class ImageContainer;
+} // namespace layers
+
+class PDMFactory;
+class SharedThreadPool;
+class TaskQueue;
+
+class WebrtcMediaDataDecoder : public WebrtcVideoDecoder {
+ public:
+ WebrtcMediaDataDecoder(nsACString& aCodecMimeType, TrackingId aTrackingId);
+
+ bool Configure(const webrtc::VideoDecoder::Settings& settings) override;
+
+ int32_t Decode(const webrtc::EncodedImage& inputImage, bool missingFrames,
+ int64_t renderTimeMs = -1) override;
+
+ int32_t RegisterDecodeCompleteCallback(
+ webrtc::DecodedImageCallback* callback) override;
+
+ int32_t Release() override;
+
+ private:
+ ~WebrtcMediaDataDecoder();
+ void QueueFrame(MediaRawData* aFrame);
+ bool OnTaskQueue() const;
+ int32_t CreateDecoder();
+
+ const RefPtr<SharedThreadPool> mThreadPool;
+ const RefPtr<TaskQueue> mTaskQueue;
+ const RefPtr<layers::ImageContainer> mImageContainer;
+ const RefPtr<PDMFactory> mFactory;
+ RefPtr<MediaDataDecoder> mDecoder;
+ webrtc::DecodedImageCallback* mCallback = nullptr;
+ VideoInfo mInfo;
+ TrackInfo::TrackType mTrackType;
+ bool mNeedKeyframe = true;
+ MozPromiseRequestHolder<MediaDataDecoder::DecodePromise> mDecodeRequest;
+
+ MediaResult mError = NS_OK;
+ MediaDataDecoder::DecodedData mResults;
+ const nsCString mCodecType;
+ bool mDisabledHardwareAcceleration = false;
+ const TrackingId mTrackingId;
+};
+
+} // namespace mozilla
+
+#endif // WebrtcMediaDataDecoderCodec_h__
diff --git a/dom/media/webrtc/libwebrtcglue/WebrtcMediaDataEncoderCodec.cpp b/dom/media/webrtc/libwebrtcglue/WebrtcMediaDataEncoderCodec.cpp
new file mode 100644
index 0000000000..332d49b4e5
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/WebrtcMediaDataEncoderCodec.cpp
@@ -0,0 +1,518 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WebrtcMediaDataEncoderCodec.h"
+
+#include "AnnexB.h"
+#include "api/video_codecs/h264_profile_level_id.h"
+#include "ImageContainer.h"
+#include "media/base/media_constants.h"
+#include "MediaData.h"
+#include "modules/video_coding/utility/vp8_header_parser.h"
+#include "modules/video_coding/utility/vp9_uncompressed_header_parser.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Span.h"
+#include "mozilla/gfx/Point.h"
+#include "mozilla/media/MediaUtils.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "PEMFactory.h"
+#include "system_wrappers/include/clock.h"
+#include "VideoUtils.h"
+
+namespace mozilla {
+
+extern LazyLogModule sPEMLog;
+
+#undef LOG
+#define LOG(msg, ...) \
+ MOZ_LOG(sPEMLog, LogLevel::Debug, \
+ ("WebrtcMediaDataEncoder=%p, " msg, this, ##__VA_ARGS__))
+
+#undef LOG_V
+#define LOG_V(msg, ...) \
+ MOZ_LOG(sPEMLog, LogLevel::Verbose, \
+ ("WebrtcMediaDataEncoder=%p, " msg, this, ##__VA_ARGS__))
+
+using namespace media;
+using namespace layers;
+using MimeTypeResult = Maybe<nsLiteralCString>;
+
+static MimeTypeResult ConvertWebrtcCodecTypeToMimeType(
+ const webrtc::VideoCodecType& aType) {
+ switch (aType) {
+ case webrtc::VideoCodecType::kVideoCodecVP8:
+ return Some("video/vp8"_ns);
+ case webrtc::VideoCodecType::kVideoCodecVP9:
+ return Some("video/vp9"_ns);
+ case webrtc::VideoCodecType::kVideoCodecH264:
+ return Some("video/avc"_ns);
+ default:
+ MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE("Unsupported codec type");
+ }
+ return Nothing();
+}
+
+bool WebrtcMediaDataEncoder::CanCreate(
+ const webrtc::VideoCodecType aCodecType) {
+ auto factory = MakeRefPtr<PEMFactory>();
+ MimeTypeResult mimeType = ConvertWebrtcCodecTypeToMimeType(aCodecType);
+ return mimeType ? factory->SupportsMimeType(mimeType.ref()) : false;
+}
+
+static const char* PacketModeStr(const webrtc::CodecSpecificInfo& aInfo) {
+ MOZ_ASSERT(aInfo.codecType != webrtc::VideoCodecType::kVideoCodecGeneric);
+
+ if (aInfo.codecType != webrtc::VideoCodecType::kVideoCodecH264) {
+ return "N/A";
+ }
+ switch (aInfo.codecSpecific.H264.packetization_mode) {
+ case webrtc::H264PacketizationMode::SingleNalUnit:
+ return "SingleNalUnit";
+ case webrtc::H264PacketizationMode::NonInterleaved:
+ return "NonInterleaved";
+ default:
+ return "Unknown";
+ }
+}
+
+static MediaDataEncoder::H264Specific::ProfileLevel ConvertProfileLevel(
+ const webrtc::SdpVideoFormat::Parameters& aParameters) {
+ const absl::optional<webrtc::H264ProfileLevelId> profileLevel =
+ webrtc::ParseSdpForH264ProfileLevelId(aParameters);
+ if (profileLevel &&
+ (profileLevel->profile == webrtc::H264Profile::kProfileBaseline ||
+ profileLevel->profile ==
+ webrtc::H264Profile::kProfileConstrainedBaseline)) {
+ return MediaDataEncoder::H264Specific::ProfileLevel::BaselineAutoLevel;
+ }
+ return MediaDataEncoder::H264Specific::ProfileLevel::MainAutoLevel;
+}
+
+static MediaDataEncoder::VPXSpecific::Complexity MapComplexity(
+ webrtc::VideoCodecComplexity aComplexity) {
+ switch (aComplexity) {
+ case webrtc::VideoCodecComplexity::kComplexityNormal:
+ return MediaDataEncoder::VPXSpecific::Complexity::Normal;
+ case webrtc::VideoCodecComplexity::kComplexityHigh:
+ return MediaDataEncoder::VPXSpecific::Complexity::High;
+ case webrtc::VideoCodecComplexity::kComplexityHigher:
+ return MediaDataEncoder::VPXSpecific::Complexity::Higher;
+ case webrtc::VideoCodecComplexity::kComplexityMax:
+ return MediaDataEncoder::VPXSpecific::Complexity::Max;
+ default:
+ MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE("Bad complexity value");
+ }
+}
+
+WebrtcMediaDataEncoder::WebrtcMediaDataEncoder(
+ const webrtc::SdpVideoFormat& aFormat)
+ : mTaskQueue(
+ TaskQueue::Create(GetMediaThreadPool(MediaThreadType::SUPERVISOR),
+ "WebrtcMediaDataEncoder::mTaskQueue")),
+ mFactory(new PEMFactory()),
+ mCallbackMutex("WebrtcMediaDataEncoderCodec encoded callback mutex"),
+ mFormatParams(aFormat.parameters),
+ // Use the same lower and upper bound as h264_video_toolbox_encoder which
+ // is an encoder from webrtc's upstream codebase.
+ // 0.5 is set as a mininum to prevent overcompensating for large temporary
+ // overshoots. We don't want to degrade video quality too badly.
+ // 0.95 is set to prevent oscillations. When a lower bitrate is set on the
+ // encoder than previously set, its output seems to have a brief period of
+ // drastically reduced bitrate, so we want to avoid that. In steady state
+ // conditions, 0.95 seems to give us better overall bitrate over long
+ // periods of time.
+ mBitrateAdjuster(0.5, 0.95) {
+ PodZero(&mCodecSpecific.codecSpecific);
+}
+
+WebrtcMediaDataEncoder::~WebrtcMediaDataEncoder() = default;
+
+static void InitCodecSpecficInfo(
+ webrtc::CodecSpecificInfo& aInfo, const webrtc::VideoCodec* aCodecSettings,
+ const webrtc::SdpVideoFormat::Parameters& aParameters) {
+ MOZ_ASSERT(aCodecSettings);
+
+ aInfo.codecType = aCodecSettings->codecType;
+ switch (aCodecSettings->codecType) {
+ case webrtc::VideoCodecType::kVideoCodecH264: {
+ aInfo.codecSpecific.H264.packetization_mode =
+ aParameters.count(cricket::kH264FmtpPacketizationMode) == 1 &&
+ aParameters.at(cricket::kH264FmtpPacketizationMode) == "1"
+ ? webrtc::H264PacketizationMode::NonInterleaved
+ : webrtc::H264PacketizationMode::SingleNalUnit;
+ break;
+ }
+ case webrtc::VideoCodecType::kVideoCodecVP9: {
+ MOZ_ASSERT(aCodecSettings->VP9().numberOfSpatialLayers == 1);
+ aInfo.codecSpecific.VP9.flexible_mode =
+ aCodecSettings->VP9().flexibleMode;
+ aInfo.codecSpecific.VP9.first_frame_in_picture = true;
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+int32_t WebrtcMediaDataEncoder::InitEncode(
+ const webrtc::VideoCodec* aCodecSettings,
+ const webrtc::VideoEncoder::Settings& aSettings) {
+ MOZ_ASSERT(aCodecSettings);
+
+ if (aCodecSettings->numberOfSimulcastStreams > 1) {
+ LOG("Only one stream is supported. Falling back to simulcast adaptor");
+ return WEBRTC_VIDEO_CODEC_ERR_SIMULCAST_PARAMETERS_NOT_SUPPORTED;
+ }
+
+ // TODO: enable max output size setting when supported.
+ if (aCodecSettings->codecType == webrtc::VideoCodecType::kVideoCodecH264 &&
+ !(mFormatParams.count(cricket::kH264FmtpPacketizationMode) == 1 &&
+ mFormatParams.at(cricket::kH264FmtpPacketizationMode) == "1")) {
+ LOG("Some platform encoders don't support setting max output size."
+ " Falling back to SW");
+ return WEBRTC_VIDEO_CODEC_FALLBACK_SOFTWARE;
+ }
+
+ if (mEncoder) {
+ // Clean existing encoder.
+ Shutdown();
+ }
+
+ RefPtr<MediaDataEncoder> encoder = CreateEncoder(aCodecSettings);
+ if (!encoder) {
+ LOG("Fail to create encoder. Falling back to SW");
+ return WEBRTC_VIDEO_CODEC_FALLBACK_SOFTWARE;
+ }
+
+ InitCodecSpecficInfo(mCodecSpecific, aCodecSettings, mFormatParams);
+ LOG("Init encode, mimeType %s, mode %s", mInfo.mMimeType.get(),
+ PacketModeStr(mCodecSpecific));
+ if (!media::Await(do_AddRef(mTaskQueue), encoder->Init()).IsResolve()) {
+ LOG("Fail to init encoder. Falling back to SW");
+ return WEBRTC_VIDEO_CODEC_FALLBACK_SOFTWARE;
+ }
+ mEncoder = std::move(encoder);
+ return WEBRTC_VIDEO_CODEC_OK;
+}
+
+bool WebrtcMediaDataEncoder::SetupConfig(
+ const webrtc::VideoCodec* aCodecSettings) {
+ MimeTypeResult mimeType =
+ ConvertWebrtcCodecTypeToMimeType(aCodecSettings->codecType);
+ if (!mimeType) {
+ LOG("Get incorrect mime type");
+ return false;
+ }
+ mInfo = VideoInfo(aCodecSettings->width, aCodecSettings->height);
+ mInfo.mMimeType = mimeType.extract();
+ mMaxFrameRate = aCodecSettings->maxFramerate;
+ // Those bitrates in codec setting are all kbps, so we have to covert them to
+ // bps.
+ mMaxBitrateBps = aCodecSettings->maxBitrate * 1000;
+ mMinBitrateBps = aCodecSettings->minBitrate * 1000;
+ mBitrateAdjuster.SetTargetBitrateBps(aCodecSettings->startBitrate * 1000);
+ return true;
+}
+
+already_AddRefed<MediaDataEncoder> WebrtcMediaDataEncoder::CreateEncoder(
+ const webrtc::VideoCodec* aCodecSettings) {
+ if (!SetupConfig(aCodecSettings)) {
+ return nullptr;
+ }
+ const bool swOnly = StaticPrefs::media_webrtc_platformencoder_sw_only();
+ LOG("Request platform encoder for %s, bitRate=%u bps, frameRate=%u"
+ ", sw-only=%d",
+ mInfo.mMimeType.get(), mBitrateAdjuster.GetTargetBitrateBps(),
+ aCodecSettings->maxFramerate, swOnly);
+
+ size_t keyframeInterval = 1;
+ switch (aCodecSettings->codecType) {
+ case webrtc::VideoCodecType::kVideoCodecH264: {
+ keyframeInterval = aCodecSettings->H264().keyFrameInterval;
+ break;
+ }
+ case webrtc::VideoCodecType::kVideoCodecVP8: {
+ keyframeInterval = aCodecSettings->VP8().keyFrameInterval;
+ break;
+ }
+ case webrtc::VideoCodecType::kVideoCodecVP9: {
+ keyframeInterval = aCodecSettings->VP9().keyFrameInterval;
+ break;
+ }
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unsupported codec type");
+ return nullptr;
+ }
+ CreateEncoderParams params(
+ mInfo, MediaDataEncoder::Usage::Realtime,
+ TaskQueue::Create(GetMediaThreadPool(MediaThreadType::PLATFORM_ENCODER),
+ "WebrtcMediaDataEncoder::mEncoder"),
+ MediaDataEncoder::PixelFormat::YUV420P, aCodecSettings->maxFramerate,
+ keyframeInterval, mBitrateAdjuster.GetTargetBitrateBps());
+ switch (aCodecSettings->codecType) {
+ case webrtc::VideoCodecType::kVideoCodecH264: {
+ params.SetCodecSpecific(
+ MediaDataEncoder::H264Specific(ConvertProfileLevel(mFormatParams)));
+ break;
+ }
+ case webrtc::VideoCodecType::kVideoCodecVP8: {
+ const webrtc::VideoCodecVP8& vp8 = aCodecSettings->VP8();
+ const webrtc::VideoCodecComplexity complexity =
+ aCodecSettings->GetVideoEncoderComplexity();
+ const bool frameDropEnabled = aCodecSettings->GetFrameDropEnabled();
+ params.SetCodecSpecific(MediaDataEncoder::VPXSpecific::VP8(
+ MapComplexity(complexity), false, vp8.numberOfTemporalLayers,
+ vp8.denoisingOn, vp8.automaticResizeOn, frameDropEnabled));
+ break;
+ }
+ case webrtc::VideoCodecType::kVideoCodecVP9: {
+ const webrtc::VideoCodecVP9& vp9 = aCodecSettings->VP9();
+ const webrtc::VideoCodecComplexity complexity =
+ aCodecSettings->GetVideoEncoderComplexity();
+ const bool frameDropEnabled = aCodecSettings->GetFrameDropEnabled();
+ params.SetCodecSpecific(MediaDataEncoder::VPXSpecific::VP9(
+ MapComplexity(complexity), false, vp9.numberOfTemporalLayers,
+ vp9.denoisingOn, vp9.automaticResizeOn, frameDropEnabled,
+ vp9.adaptiveQpMode, vp9.numberOfSpatialLayers, vp9.flexibleMode));
+ break;
+ }
+ default:
+ MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE("Unsupported codec type");
+ }
+ return mFactory->CreateEncoder(params, swOnly);
+}
+
+WebrtcVideoEncoder::EncoderInfo WebrtcMediaDataEncoder::GetEncoderInfo() const {
+ WebrtcVideoEncoder::EncoderInfo info;
+ info.supports_native_handle = false;
+ info.implementation_name = "MediaDataEncoder";
+ info.is_hardware_accelerated = false;
+ info.supports_simulcast = false;
+
+#ifdef MOZ_WIDGET_ANDROID
+ // Assume MediaDataEncoder is used mainly for hardware encoding. 16-alignment
+ // seems required on Android. This could be improved by querying the
+ // underlying encoder.
+ info.requested_resolution_alignment = 16;
+ info.apply_alignment_to_all_simulcast_layers = true;
+#endif
+ return info;
+}
+
+int32_t WebrtcMediaDataEncoder::RegisterEncodeCompleteCallback(
+ webrtc::EncodedImageCallback* aCallback) {
+ MutexAutoLock lock(mCallbackMutex);
+ mCallback = aCallback;
+ return WEBRTC_VIDEO_CODEC_OK;
+}
+
+int32_t WebrtcMediaDataEncoder::Shutdown() {
+ LOG("Release encoder");
+ {
+ MutexAutoLock lock(mCallbackMutex);
+ mCallback = nullptr;
+ mError = NS_OK;
+ }
+ if (mEncoder) {
+ media::Await(do_AddRef(mTaskQueue), mEncoder->Shutdown());
+ mEncoder = nullptr;
+ }
+ return WEBRTC_VIDEO_CODEC_OK;
+}
+
+static already_AddRefed<VideoData> CreateVideoDataFromWebrtcVideoFrame(
+ const webrtc::VideoFrame& aFrame, const bool aIsKeyFrame,
+ const TimeUnit aDuration) {
+ MOZ_ASSERT(aFrame.video_frame_buffer()->type() ==
+ webrtc::VideoFrameBuffer::Type::kI420,
+ "Only support YUV420!");
+ const webrtc::I420BufferInterface* i420 =
+ aFrame.video_frame_buffer()->GetI420();
+
+ PlanarYCbCrData yCbCrData;
+ yCbCrData.mYChannel = const_cast<uint8_t*>(i420->DataY());
+ yCbCrData.mYStride = i420->StrideY();
+ yCbCrData.mCbChannel = const_cast<uint8_t*>(i420->DataU());
+ yCbCrData.mCrChannel = const_cast<uint8_t*>(i420->DataV());
+ MOZ_ASSERT(i420->StrideU() == i420->StrideV());
+ yCbCrData.mCbCrStride = i420->StrideU();
+ yCbCrData.mPictureRect = gfx::IntRect(0, 0, i420->width(), i420->height());
+ yCbCrData.mChromaSubsampling = gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT;
+
+ RefPtr<PlanarYCbCrImage> image =
+ new RecyclingPlanarYCbCrImage(new BufferRecycleBin());
+ image->CopyData(yCbCrData);
+
+ // Although webrtc::VideoFrame::timestamp_rtp_ will likely be deprecated,
+ // webrtc::EncodedImage and the VPx encoders still use it in the imported
+ // version of libwebrtc. Not using the same timestamp values generates
+ // discontinuous time and confuses the video receiver when switching from
+ // platform to libwebrtc encoder.
+ TimeUnit timestamp =
+ media::TimeUnit(aFrame.timestamp(), cricket::kVideoCodecClockrate);
+ return VideoData::CreateFromImage(image->GetSize(), 0, timestamp, aDuration,
+ image, aIsKeyFrame, timestamp);
+}
+
+static void UpdateCodecSpecificInfo(webrtc::CodecSpecificInfo& aInfo,
+ const gfx::IntSize& aSize,
+ const bool aIsKeyframe) {
+ switch (aInfo.codecType) {
+ case webrtc::VideoCodecType::kVideoCodecVP8: {
+ // See webrtc::VP8EncoderImpl::PopulateCodecSpecific().
+ webrtc::CodecSpecificInfoVP8& vp8 = aInfo.codecSpecific.VP8;
+ vp8.keyIdx = webrtc::kNoKeyIdx;
+ // Cannot be 100% sure unless parsing significant portion of the
+ // bitstream. Treat all frames as referenced just to be safe.
+ vp8.nonReference = false;
+ // One temporal layer only.
+ vp8.temporalIdx = webrtc::kNoTemporalIdx;
+ vp8.layerSync = false;
+ break;
+ }
+ case webrtc::VideoCodecType::kVideoCodecVP9: {
+ // See webrtc::VP9EncoderImpl::PopulateCodecSpecific().
+ webrtc::CodecSpecificInfoVP9& vp9 = aInfo.codecSpecific.VP9;
+ vp9.inter_pic_predicted = !aIsKeyframe;
+ vp9.ss_data_available = aIsKeyframe && !vp9.flexible_mode;
+ // One temporal & spatial layer only.
+ vp9.temporal_idx = webrtc::kNoTemporalIdx;
+ vp9.temporal_up_switch = false;
+ vp9.num_spatial_layers = 1;
+ vp9.end_of_picture = true;
+ vp9.gof_idx = webrtc::kNoGofIdx;
+ vp9.width[0] = aSize.width;
+ vp9.height[0] = aSize.height;
+ break;
+ }
+ default:
+ break;
+ }
+}
+
+static void GetVPXQp(const webrtc::VideoCodecType aType,
+ webrtc::EncodedImage& aImage) {
+ switch (aType) {
+ case webrtc::VideoCodecType::kVideoCodecVP8:
+ webrtc::vp8::GetQp(aImage.data(), aImage.size(), &(aImage.qp_));
+ break;
+ case webrtc::VideoCodecType::kVideoCodecVP9:
+ webrtc::vp9::GetQp(aImage.data(), aImage.size(), &(aImage.qp_));
+ break;
+ default:
+ break;
+ }
+}
+
+int32_t WebrtcMediaDataEncoder::Encode(
+ const webrtc::VideoFrame& aInputFrame,
+ const std::vector<webrtc::VideoFrameType>* aFrameTypes) {
+ if (!aInputFrame.size() || !aInputFrame.video_frame_buffer() ||
+ aFrameTypes->empty()) {
+ return WEBRTC_VIDEO_CODEC_ERR_PARAMETER;
+ }
+
+ if (!mEncoder) {
+ return WEBRTC_VIDEO_CODEC_UNINITIALIZED;
+ }
+ {
+ MutexAutoLock lock(mCallbackMutex);
+ if (!mCallback) {
+ return WEBRTC_VIDEO_CODEC_UNINITIALIZED;
+ }
+ if (NS_FAILED(mError)) {
+ return WEBRTC_VIDEO_CODEC_ERROR;
+ }
+ }
+
+ LOG_V("Encode frame, type %d size %u", static_cast<int>((*aFrameTypes)[0]),
+ aInputFrame.size());
+ MOZ_ASSERT(aInputFrame.video_frame_buffer()->type() ==
+ webrtc::VideoFrameBuffer::Type::kI420);
+ RefPtr<VideoData> data = CreateVideoDataFromWebrtcVideoFrame(
+ aInputFrame, (*aFrameTypes)[0] == webrtc::VideoFrameType::kVideoFrameKey,
+ TimeUnit::FromSeconds(1.0 / mMaxFrameRate));
+ const gfx::IntSize displaySize = data->mDisplay;
+
+ mEncoder->Encode(data)->Then(
+ mTaskQueue, __func__,
+ [self = RefPtr<WebrtcMediaDataEncoder>(this), this,
+ displaySize](MediaDataEncoder::EncodedData aFrames) {
+ LOG_V("Received encoded frame, nums %zu width %d height %d",
+ aFrames.Length(), displaySize.width, displaySize.height);
+ for (auto& frame : aFrames) {
+ MutexAutoLock lock(mCallbackMutex);
+ if (!mCallback) {
+ break;
+ }
+ webrtc::EncodedImage image;
+ image.SetEncodedData(
+ webrtc::EncodedImageBuffer::Create(frame->Data(), frame->Size()));
+ image._encodedWidth = displaySize.width;
+ image._encodedHeight = displaySize.height;
+ CheckedInt64 time =
+ TimeUnitToFrames(frame->mTime, cricket::kVideoCodecClockrate);
+ if (!time.isValid()) {
+ self->mError = MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
+ "invalid timestamp from encoder");
+ break;
+ }
+ image.SetTimestamp(time.value());
+ image._frameType = frame->mKeyframe
+ ? webrtc::VideoFrameType::kVideoFrameKey
+ : webrtc::VideoFrameType::kVideoFrameDelta;
+ GetVPXQp(mCodecSpecific.codecType, image);
+ UpdateCodecSpecificInfo(mCodecSpecific, displaySize,
+ frame->mKeyframe);
+
+ LOG_V("Send encoded image");
+ self->mCallback->OnEncodedImage(image, &mCodecSpecific);
+ self->mBitrateAdjuster.Update(image.size());
+ }
+ },
+ [self = RefPtr<WebrtcMediaDataEncoder>(this)](const MediaResult aError) {
+ self->mError = aError;
+ });
+ return WEBRTC_VIDEO_CODEC_OK;
+}
+
+int32_t WebrtcMediaDataEncoder::SetRates(
+ const webrtc::VideoEncoder::RateControlParameters& aParameters) {
+ if (!aParameters.bitrate.HasBitrate(0, 0)) {
+ LOG("%s: no bitrate value to set.", __func__);
+ return WEBRTC_VIDEO_CODEC_ERR_PARAMETER;
+ }
+ MOZ_ASSERT(aParameters.bitrate.IsSpatialLayerUsed(0));
+ MOZ_ASSERT(!aParameters.bitrate.IsSpatialLayerUsed(1),
+ "No simulcast support for platform encoder");
+
+ const uint32_t newBitrateBps = aParameters.bitrate.GetBitrate(0, 0);
+ if (newBitrateBps < mMinBitrateBps || newBitrateBps > mMaxBitrateBps) {
+ LOG("%s: bitrate value out of range.", __func__);
+ return WEBRTC_VIDEO_CODEC_ERR_PARAMETER;
+ }
+
+ // We have already been in this bitrate.
+ if (mBitrateAdjuster.GetAdjustedBitrateBps() == newBitrateBps) {
+ return WEBRTC_VIDEO_CODEC_OK;
+ }
+
+ if (!mEncoder) {
+ return WEBRTC_VIDEO_CODEC_UNINITIALIZED;
+ }
+ {
+ MutexAutoLock lock(mCallbackMutex);
+ if (NS_FAILED(mError)) {
+ return WEBRTC_VIDEO_CODEC_ERROR;
+ }
+ }
+ mBitrateAdjuster.SetTargetBitrateBps(newBitrateBps);
+ LOG("Set bitrate %u bps, minBitrate %u bps, maxBitrate %u bps", newBitrateBps,
+ mMinBitrateBps, mMaxBitrateBps);
+ auto rv =
+ media::Await(do_AddRef(mTaskQueue), mEncoder->SetBitrate(newBitrateBps));
+ return rv.IsResolve() ? WEBRTC_VIDEO_CODEC_OK : WEBRTC_VIDEO_CODEC_ERROR;
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/libwebrtcglue/WebrtcMediaDataEncoderCodec.h b/dom/media/webrtc/libwebrtcglue/WebrtcMediaDataEncoderCodec.h
new file mode 100644
index 0000000000..bc462cc382
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/WebrtcMediaDataEncoderCodec.h
@@ -0,0 +1,78 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef WebrtcMediaDataEncoderCodec_h__
+#define WebrtcMediaDataEncoderCodec_h__
+
+#include "MediaConduitInterface.h"
+#include "MediaInfo.h"
+#include "MediaResult.h"
+#include "PlatformEncoderModule.h"
+#include "WebrtcGmpVideoCodec.h"
+#include "common_video/include/bitrate_adjuster.h"
+#include "modules/video_coding/include/video_codec_interface.h"
+
+namespace mozilla {
+
+class MediaData;
+class PEMFactory;
+class SharedThreadPool;
+class TaskQueue;
+
+class WebrtcMediaDataEncoder : public RefCountedWebrtcVideoEncoder {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WebrtcMediaDataEncoder, final);
+
+ static bool CanCreate(const webrtc::VideoCodecType aCodecType);
+
+ explicit WebrtcMediaDataEncoder(const webrtc::SdpVideoFormat& aFormat);
+
+ int32_t InitEncode(const webrtc::VideoCodec* aCodecSettings,
+ const webrtc::VideoEncoder::Settings& aSettings) override;
+
+ int32_t RegisterEncodeCompleteCallback(
+ webrtc::EncodedImageCallback* aCallback) override;
+
+ int32_t Shutdown() override;
+
+ int32_t Encode(
+ const webrtc::VideoFrame& aFrame,
+ const std::vector<webrtc::VideoFrameType>* aFrameTypes) override;
+
+ int32_t SetRates(
+ const webrtc::VideoEncoder::RateControlParameters& aParameters) override;
+
+ WebrtcVideoEncoder::EncoderInfo GetEncoderInfo() const override;
+ MediaEventSource<uint64_t>* InitPluginEvent() override { return nullptr; }
+
+ MediaEventSource<uint64_t>* ReleasePluginEvent() override { return nullptr; }
+
+ private:
+ virtual ~WebrtcMediaDataEncoder();
+
+ bool SetupConfig(const webrtc::VideoCodec* aCodecSettings);
+ already_AddRefed<MediaDataEncoder> CreateEncoder(
+ const webrtc::VideoCodec* aCodecSettings);
+ bool InitEncoder();
+
+ const RefPtr<TaskQueue> mTaskQueue;
+ const RefPtr<PEMFactory> mFactory;
+ RefPtr<MediaDataEncoder> mEncoder;
+
+ Mutex mCallbackMutex MOZ_UNANNOTATED; // Protects mCallback and mError.
+ webrtc::EncodedImageCallback* mCallback = nullptr;
+ MediaResult mError = NS_OK;
+
+ VideoInfo mInfo;
+ webrtc::SdpVideoFormat::Parameters mFormatParams;
+ webrtc::CodecSpecificInfo mCodecSpecific;
+ webrtc::BitrateAdjuster mBitrateAdjuster;
+ uint32_t mMaxFrameRate;
+ uint32_t mMinBitrateBps;
+ uint32_t mMaxBitrateBps;
+};
+
+} // namespace mozilla
+
+#endif // WebrtcMediaDataEncoderCodec_h__
diff --git a/dom/media/webrtc/libwebrtcglue/WebrtcVideoCodecFactory.cpp b/dom/media/webrtc/libwebrtcglue/WebrtcVideoCodecFactory.cpp
new file mode 100644
index 0000000000..6acec07ea3
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/WebrtcVideoCodecFactory.cpp
@@ -0,0 +1,139 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WebrtcVideoCodecFactory.h"
+
+#include "GmpVideoCodec.h"
+#include "MediaDataCodec.h"
+#include "VideoConduit.h"
+#include "mozilla/StaticPrefs_media.h"
+
+// libwebrtc includes
+#include "api/rtp_headers.h"
+#include "api/video_codecs/video_codec.h"
+#include "api/video_codecs/video_encoder_software_fallback_wrapper.h"
+#include "media/engine/encoder_simulcast_proxy.h"
+#include "modules/video_coding/codecs/vp8/include/vp8.h"
+#include "modules/video_coding/codecs/vp9/include/vp9.h"
+
+namespace mozilla {
+
+std::unique_ptr<webrtc::VideoDecoder>
+WebrtcVideoDecoderFactory::CreateVideoDecoder(
+ const webrtc::SdpVideoFormat& aFormat) {
+ std::unique_ptr<webrtc::VideoDecoder> decoder;
+ auto type = webrtc::PayloadStringToCodecType(aFormat.name);
+
+ // Attempt to create a decoder using MediaDataDecoder.
+ decoder.reset(MediaDataCodec::CreateDecoder(type, mTrackingId));
+ if (decoder) {
+ return decoder;
+ }
+
+ switch (type) {
+ case webrtc::VideoCodecType::kVideoCodecH264: {
+ // Get an external decoder
+ auto gmpDecoder =
+ WrapUnique(GmpVideoCodec::CreateDecoder(mPCHandle, mTrackingId));
+ mCreatedGmpPluginEvent.Forward(*gmpDecoder->InitPluginEvent());
+ mReleasedGmpPluginEvent.Forward(*gmpDecoder->ReleasePluginEvent());
+ decoder.reset(gmpDecoder.release());
+ break;
+ }
+
+ // Use libvpx decoders as fallbacks.
+ case webrtc::VideoCodecType::kVideoCodecVP8:
+ if (!decoder) {
+ decoder = webrtc::VP8Decoder::Create();
+ }
+ break;
+ case webrtc::VideoCodecType::kVideoCodecVP9:
+ decoder = webrtc::VP9Decoder::Create();
+ break;
+
+ default:
+ break;
+ }
+
+ return decoder;
+}
+
+std::unique_ptr<webrtc::VideoEncoder>
+WebrtcVideoEncoderFactory::CreateVideoEncoder(
+ const webrtc::SdpVideoFormat& aFormat) {
+ if (!mInternalFactory->Supports(aFormat)) {
+ return nullptr;
+ }
+ auto type = webrtc::PayloadStringToCodecType(aFormat.name);
+ switch (type) {
+ case webrtc::VideoCodecType::kVideoCodecVP8:
+ // XXX We might be able to use the simulcast proxy for more codecs, but
+ // that requires testing.
+ return std::make_unique<webrtc::EncoderSimulcastProxy>(
+ mInternalFactory.get(), aFormat);
+ default:
+ return mInternalFactory->CreateVideoEncoder(aFormat);
+ }
+}
+
+bool WebrtcVideoEncoderFactory::InternalFactory::Supports(
+ const webrtc::SdpVideoFormat& aFormat) {
+ switch (webrtc::PayloadStringToCodecType(aFormat.name)) {
+ case webrtc::VideoCodecType::kVideoCodecVP8:
+ case webrtc::VideoCodecType::kVideoCodecVP9:
+ case webrtc::VideoCodecType::kVideoCodecH264:
+ return true;
+ default:
+ return false;
+ }
+}
+
+std::unique_ptr<webrtc::VideoEncoder>
+WebrtcVideoEncoderFactory::InternalFactory::CreateVideoEncoder(
+ const webrtc::SdpVideoFormat& aFormat) {
+ MOZ_ASSERT(Supports(aFormat));
+
+ std::unique_ptr<webrtc::VideoEncoder> platformEncoder;
+ platformEncoder.reset(MediaDataCodec::CreateEncoder(aFormat));
+ const bool fallback = StaticPrefs::media_webrtc_software_encoder_fallback();
+ if (!fallback && platformEncoder) {
+ return platformEncoder;
+ }
+
+ std::unique_ptr<webrtc::VideoEncoder> encoder;
+ switch (webrtc::PayloadStringToCodecType(aFormat.name)) {
+ case webrtc::VideoCodecType::kVideoCodecH264: {
+ // get an external encoder
+ auto gmpEncoder =
+ WrapUnique(GmpVideoCodec::CreateEncoder(aFormat, mPCHandle));
+ mCreatedGmpPluginEvent.Forward(*gmpEncoder->InitPluginEvent());
+ mReleasedGmpPluginEvent.Forward(*gmpEncoder->ReleasePluginEvent());
+ encoder.reset(gmpEncoder.release());
+ break;
+ }
+ // libvpx fallbacks.
+ case webrtc::VideoCodecType::kVideoCodecVP8:
+ if (!encoder) {
+ encoder = webrtc::VP8Encoder::Create();
+ }
+ break;
+ case webrtc::VideoCodecType::kVideoCodecVP9:
+ encoder = webrtc::VP9Encoder::Create();
+ break;
+
+ default:
+ break;
+ }
+ if (fallback && encoder && platformEncoder) {
+ return webrtc::CreateVideoEncoderSoftwareFallbackWrapper(
+ std::move(encoder), std::move(platformEncoder), false);
+ }
+ if (platformEncoder) {
+ return platformEncoder;
+ }
+ return encoder;
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/libwebrtcglue/WebrtcVideoCodecFactory.h b/dom/media/webrtc/libwebrtcglue/WebrtcVideoCodecFactory.h
new file mode 100644
index 0000000000..ef5765043f
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/WebrtcVideoCodecFactory.h
@@ -0,0 +1,124 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-*/
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DOM_MEDIA_WEBRTC_LIBWEBRTCGLUE_WEBRTCVIDEOCODECFACTORY_H_
+#define DOM_MEDIA_WEBRTC_LIBWEBRTCGLUE_WEBRTCVIDEOCODECFACTORY_H_
+
+#include "api/video_codecs/video_decoder_factory.h"
+#include "api/video_codecs/video_encoder_factory.h"
+#include "MediaEventSource.h"
+#include "PerformanceRecorder.h"
+
+namespace mozilla {
+class GmpPluginNotifierInterface {
+ virtual void DisconnectAll() = 0;
+ virtual MediaEventSource<uint64_t>& CreatedGmpPluginEvent() = 0;
+ virtual MediaEventSource<uint64_t>& ReleasedGmpPluginEvent() = 0;
+};
+
+class GmpPluginNotifier : public GmpPluginNotifierInterface {
+ public:
+ explicit GmpPluginNotifier(nsCOMPtr<nsISerialEventTarget> aOwningThread)
+ : mOwningThread(std::move(aOwningThread)),
+ mCreatedGmpPluginEvent(mOwningThread),
+ mReleasedGmpPluginEvent(mOwningThread) {}
+
+ ~GmpPluginNotifier() = default;
+
+ void DisconnectAll() override {
+ MOZ_ASSERT(mOwningThread->IsOnCurrentThread());
+ mCreatedGmpPluginEvent.DisconnectAll();
+ mReleasedGmpPluginEvent.DisconnectAll();
+ }
+
+ MediaEventSource<uint64_t>& CreatedGmpPluginEvent() override {
+ return mCreatedGmpPluginEvent;
+ }
+
+ MediaEventSource<uint64_t>& ReleasedGmpPluginEvent() override {
+ return mReleasedGmpPluginEvent;
+ }
+
+ protected:
+ const nsCOMPtr<nsISerialEventTarget> mOwningThread;
+ MediaEventForwarder<uint64_t> mCreatedGmpPluginEvent;
+ MediaEventForwarder<uint64_t> mReleasedGmpPluginEvent;
+};
+
+class WebrtcVideoDecoderFactory : public GmpPluginNotifier,
+ public webrtc::VideoDecoderFactory {
+ public:
+ WebrtcVideoDecoderFactory(nsCOMPtr<nsISerialEventTarget> aOwningThread,
+ std::string aPCHandle, TrackingId aTrackingId)
+ : GmpPluginNotifier(std::move(aOwningThread)),
+ mPCHandle(std::move(aPCHandle)),
+ mTrackingId(std::move(aTrackingId)) {}
+
+ std::vector<webrtc::SdpVideoFormat> GetSupportedFormats() const override {
+ MOZ_CRASH("Unexpected call");
+ return std::vector<webrtc::SdpVideoFormat>();
+ }
+
+ std::unique_ptr<webrtc::VideoDecoder> CreateVideoDecoder(
+ const webrtc::SdpVideoFormat& aFormat) override;
+
+ private:
+ const std::string mPCHandle;
+ const TrackingId mTrackingId;
+};
+
+class WebrtcVideoEncoderFactory : public GmpPluginNotifierInterface,
+ public webrtc::VideoEncoderFactory {
+ class InternalFactory : public GmpPluginNotifier,
+ public webrtc::VideoEncoderFactory {
+ public:
+ InternalFactory(nsCOMPtr<nsISerialEventTarget> aOwningThread,
+ std::string aPCHandle)
+ : GmpPluginNotifier(std::move(aOwningThread)),
+ mPCHandle(std::move(aPCHandle)) {}
+
+ std::vector<webrtc::SdpVideoFormat> GetSupportedFormats() const override {
+ MOZ_CRASH("Unexpected call");
+ return std::vector<webrtc::SdpVideoFormat>();
+ }
+
+ std::unique_ptr<webrtc::VideoEncoder> CreateVideoEncoder(
+ const webrtc::SdpVideoFormat& aFormat) override;
+
+ bool Supports(const webrtc::SdpVideoFormat& aFormat);
+
+ private:
+ const std::string mPCHandle;
+ };
+
+ public:
+ explicit WebrtcVideoEncoderFactory(
+ nsCOMPtr<nsISerialEventTarget> aOwningThread, std::string aPCHandle)
+ : mInternalFactory(MakeUnique<InternalFactory>(std::move(aOwningThread),
+ std::move(aPCHandle))) {}
+
+ std::vector<webrtc::SdpVideoFormat> GetSupportedFormats() const override {
+ MOZ_CRASH("Unexpected call");
+ return std::vector<webrtc::SdpVideoFormat>();
+ }
+
+ std::unique_ptr<webrtc::VideoEncoder> CreateVideoEncoder(
+ const webrtc::SdpVideoFormat& aFormat) override;
+
+ void DisconnectAll() override { mInternalFactory->DisconnectAll(); }
+
+ MediaEventSource<uint64_t>& CreatedGmpPluginEvent() override {
+ return mInternalFactory->CreatedGmpPluginEvent();
+ }
+ MediaEventSource<uint64_t>& ReleasedGmpPluginEvent() override {
+ return mInternalFactory->ReleasedGmpPluginEvent();
+ }
+
+ private:
+ const UniquePtr<InternalFactory> mInternalFactory;
+};
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/libwebrtcglue/moz.build b/dom/media/webrtc/libwebrtcglue/moz.build
new file mode 100644
index 0000000000..62bac2ffe6
--- /dev/null
+++ b/dom/media/webrtc/libwebrtcglue/moz.build
@@ -0,0 +1,35 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+include("/dom/media/webrtc/third_party_build/webrtc.mozbuild")
+
+LOCAL_INCLUDES += [
+ "!/ipc/ipdl/_ipdlheaders",
+ "/dom/media/gmp", # for GMPLog.h,
+ "/dom/media/webrtc",
+ "/ipc/chromium/src",
+ "/media/libyuv/libyuv/include",
+ "/media/webrtc",
+ "/third_party/libsrtp/src/include",
+ "/third_party/libwebrtc",
+ "/third_party/libwebrtc/third_party/abseil-cpp",
+]
+
+UNIFIED_SOURCES += [
+ "AudioConduit.cpp",
+ "GmpVideoCodec.cpp",
+ "MediaConduitInterface.cpp",
+ "MediaDataCodec.cpp",
+ "SystemTime.cpp",
+ "VideoConduit.cpp",
+ "VideoStreamFactory.cpp",
+ "WebrtcCallWrapper.cpp",
+ "WebrtcGmpVideoCodec.cpp",
+ "WebrtcMediaDataDecoderCodec.cpp",
+ "WebrtcMediaDataEncoderCodec.cpp",
+ "WebrtcVideoCodecFactory.cpp",
+]
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/webrtc/metrics.yaml b/dom/media/webrtc/metrics.yaml
new file mode 100644
index 0000000000..77d9112543
--- /dev/null
+++ b/dom/media/webrtc/metrics.yaml
@@ -0,0 +1,358 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Adding a new metric? We have docs for that!
+# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html
+
+---
+$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
+$tags:
+ - 'Core :: WebRTC'
+
+rtcrtpsender:
+ count:
+ type: counter
+ description: >
+ The number of RTCRtpSenders created.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1401592
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1832459
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1401592
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1832459
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - webrtc-telemetry-alerts@mozilla.com
+ expires: 120
+
+ count_setparameters_compat:
+ type: counter
+ description: >
+ The number of RTCRtpSenders created that use the compatibility mode for
+ setParameters.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1401592
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1832459
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1401592
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1832459
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - webrtc-telemetry-alerts@mozilla.com
+ expires: 120
+
+ used_sendencodings:
+ type: rate
+ description: >
+ The proportion of RTCRtpSenders that were created by an addTransceivers
+ call that was passed a sendEncodings.
+ denominator_metric: rtcrtpsender.count
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1401592
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1832459
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1401592
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1832459
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - webrtc-telemetry-alerts@mozilla.com
+ expires: 120
+
+rtcrtpsender.setparameters:
+ warn_no_getparameters:
+ type: rate
+ description: >
+ The proportion of RTCRtpSenders configured with the setParameters compat
+ mode that have warned at least once about a setParameters call because
+ [[LastReturnedParameters]] was not set. (ie; there was not a recent
+ enough call to getParameters)
+ denominator_metric: rtcrtpsender.count_setparameters_compat
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1401592
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1832459
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1401592
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1832459
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - webrtc-telemetry-alerts@mozilla.com
+ expires: 120
+
+ blame_no_getparameters:
+ type: labeled_counter
+ description: >
+ The number of RTCRtpSenders that have warned at least once about
+ a `setParameters` call because `[[LastReturnedParameters]]` was not set,
+ broken down by the eTLD+1 of the site. (ie; there was not a recent
+ enough call to `getParameters`) Collected only on EARLY_BETA_OR_EARLIER.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1831343
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1831343
+ data_sensitivity:
+ - web_activity
+ notification_emails:
+ - webrtc-telemetry-alerts@mozilla.com
+ expires: 120
+
+ warn_length_changed:
+ type: rate
+ description: >
+ The proportion of RTCRtpSenders configured with the setParameters compat
+ mode that have warned at least once about a setParameters call that
+ attempted to change the number of encodings.
+ denominator_metric: rtcrtpsender.count_setparameters_compat
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1401592
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1832459
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1401592
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1832459
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - webrtc-telemetry-alerts@mozilla.com
+ expires: 120
+
+ blame_length_changed:
+ type: labeled_counter
+ description: >
+ The number of RTCRtpSenders that have warned at least once about a
+ `setParameters` call that attempted to change the number of encodings,
+ broken down by the eTLD+1 of the site. Collected only on
+ EARLY_BETA_OR_EARLIER.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1831343
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1831343
+ data_sensitivity:
+ - web_activity
+ notification_emails:
+ - webrtc-telemetry-alerts@mozilla.com
+ expires: 120
+
+ warn_rid_changed:
+ type: rate
+ description: >
+ The proportion of RTCRtpSenders configured with the setParameters compat
+ mode that have warned at least once about a setParameters call that
+ attempted to change the rid on an encoding (note that we only check this
+ if the encoding count did not change, see warn_length_changed).
+ denominator_metric: rtcrtpsender.count_setparameters_compat
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1401592
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1401592
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - webrtc-telemetry-alerts@mozilla.com
+ expires: 116
+
+ warn_no_transactionid:
+ type: rate
+ description: >
+ The proportion of RTCRtpSenders configured with the setParameters compat
+ mode that have warned at least once about a setParameters call that did
+ not set the transactionId field.
+ denominator_metric: rtcrtpsender.count_setparameters_compat
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1401592
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1832459
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1401592
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1832459
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - webrtc-telemetry-alerts@mozilla.com
+ expires: 120
+
+ blame_no_transactionid:
+ type: labeled_counter
+ description: >
+ The number of RTCRtpSenders that have warned at least once about a
+ `setParameters` call that did not set the transactionId field, broken down
+ by the eTLD+1 of the site. Collected only on EARLY_BETA_OR_EARLIER.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1831343
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1831343
+ data_sensitivity:
+ - web_activity
+ notification_emails:
+ - webrtc-telemetry-alerts@mozilla.com
+ expires: 120
+
+ warn_stale_transactionid:
+ type: rate
+ description: >
+ The proportion of RTCRtpSenders configured with the setParameters compat
+ mode that have warned at least once about a setParameters call that used
+ a stale transaction id.
+ denominator_metric: rtcrtpsender.count_setparameters_compat
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1401592
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1832459
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1401592
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1832459
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - webrtc-telemetry-alerts@mozilla.com
+ expires: 120
+
+ blame_stale_transactionid:
+ type: labeled_counter
+ description: >
+ The number of RTCRtpSenders that have warned at least once about a
+ `setParameters` call that used a stale transaction id, broken down by the
+ eTLD+1 of the site. Collected only on EARLY_BETA_OR_EARLIER.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1831343
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1831343
+ data_sensitivity:
+ - web_activity
+ notification_emails:
+ - webrtc-telemetry-alerts@mozilla.com
+ expires: 120
+
+ fail_length_changed:
+ type: rate
+ description: >
+ The proportion of RTCRtpSenders that have thrown an error at least once
+ about a setParameters call that attempted to change the number of
+ encodings.
+ denominator_metric: rtcrtpsender.count
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1401592
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1832459
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1401592
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1832459
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - webrtc-telemetry-alerts@mozilla.com
+ expires: 120
+
+ fail_rid_changed:
+ type: rate
+ description: >
+ The proportion of RTCRtpSenders that have thrown an error at least once
+ about a setParameters call that attempted to change the rid on an
+ encoding (note that we only check this if the encoding count did not
+ change, see fail_length_changed).
+ denominator_metric: rtcrtpsender.count
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1401592
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1832459
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1401592
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1832459
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - webrtc-telemetry-alerts@mozilla.com
+ expires: 120
+
+ fail_no_getparameters:
+ type: rate
+ description: >
+ The proportion of RTCRtpSenders that have thrown an error at least once
+ about a setParameters call because [[LastReturnedParameters]] was not set.
+ (ie; there was not a recent enough call to getParameters)
+ denominator_metric: rtcrtpsender.count
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1401592
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1832459
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1401592
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1832459
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - webrtc-telemetry-alerts@mozilla.com
+ expires: 120
+
+ fail_no_transactionid:
+ type: rate
+ description: >
+ The proportion of RTCRtpSenders that have thrown an error at least once
+ about a setParameters call that did not set the transactionId field.
+ denominator_metric: rtcrtpsender.count
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1401592
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1832459
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1401592
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1832459
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - webrtc-telemetry-alerts@mozilla.com
+ expires: 120
+
+ fail_stale_transactionid:
+ type: rate
+ description: >
+ The proportion of RTCRtpSenders that have thrown an error at least once
+ about a setParameters call that used a stale transaction id.
+ denominator_metric: rtcrtpsender.count
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1401592
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1832459
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1401592
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1832459
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - webrtc-telemetry-alerts@mozilla.com
+ expires: 120
+
+ fail_no_encodings:
+ type: rate
+ description: >
+ The proportion of RTCRtpSenders configured with the setParameters compat
+ mode that have thrown an error at least once about a setParameters call
+ that had no encodings (we do not measure this against the general
+ population of RTCRtpSenders, since without the compat mode this failure
+ is never observed, because it fails the length change check).
+ denominator_metric: rtcrtpsender.count_setparameters_compat
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1401592
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1832459
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1401592
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1832459
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - webrtc-telemetry-alerts@mozilla.com
+ expires: 120
+
+ fail_other:
+ type: rate
+ description: >
+ The proportion of RTCRtpSenders that have thrown an error at least once
+ about a setParameters call that had no encodings.
+ denominator_metric: rtcrtpsender.count
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1401592
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1832459
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1401592
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1832459
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - webrtc-telemetry-alerts@mozilla.com
+ expires: 120
diff --git a/dom/media/webrtc/moz.build b/dom/media/webrtc/moz.build
new file mode 100644
index 0000000000..cb8e363faa
--- /dev/null
+++ b/dom/media/webrtc/moz.build
@@ -0,0 +1,132 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+include("/dom/media/webrtc/third_party_build/webrtc.mozbuild")
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "WebRTC: Audio/Video")
+
+with Files("PeerIdentity.*"):
+ BUG_COMPONENT = ("Core", "WebRTC: Signaling")
+
+with Files("common/**"):
+ BUG_COMPONENT = ("Core", "WebRTC: Signaling")
+with Files("jsep/**"):
+ BUG_COMPONENT = ("Core", "WebRTC: Signaling")
+with Files("libwebrtcglue/**"):
+ BUG_COMPONENT = ("Core", "WebRTC: Signaling")
+with Files("transportbridge/**"):
+ BUG_COMPONENT = ("Core", "WebRTC: Signaling")
+with Files("jsapi/**"):
+ BUG_COMPONENT = ("Core", "WebRTC: Signaling")
+with Files("sdp/**"):
+ BUG_COMPONENT = ("Core", "WebRTC: Signaling")
+with Files("third_party_build/**"):
+ BUG_COMPONENT = ("Core", "WebRTC")
+
+
+EXPORTS += [
+ "CubebDeviceEnumerator.h",
+ "MediaEngine.h",
+ "MediaEngineFake.h",
+ "MediaEnginePrefs.h",
+ "MediaEngineSource.h",
+ "MediaTrackConstraints.h",
+ "SineWaveGenerator.h",
+]
+
+SOURCES += [
+ "CubebDeviceEnumerator.cpp",
+]
+
+if CONFIG["MOZ_WEBRTC"]:
+ EXPORTS += [
+ "MediaEngineRemoteVideoSource.h",
+ "MediaEngineWebRTC.h",
+ "MediaEngineWebRTCAudio.h",
+ ]
+ EXPORTS.mozilla.dom += ["RTCIdentityProviderRegistrar.h"]
+ UNIFIED_SOURCES += [
+ "MediaEngineRemoteVideoSource.cpp",
+ "MediaEngineWebRTCAudio.cpp",
+ "RTCCertificate.cpp",
+ "RTCIdentityProviderRegistrar.cpp",
+ ]
+ # MediaEngineWebRTC.cpp needs to be built separately.
+ SOURCES += [
+ "MediaEngineWebRTC.cpp",
+ ]
+ PREPROCESSED_IPDL_SOURCES += [
+ "PWebrtcGlobal.ipdl",
+ ]
+ LOCAL_INCLUDES += [
+ "..",
+ "/dom/base",
+ "/dom/media",
+ "/dom/media/webrtc/common",
+ "/dom/media/webrtc/common/browser_logging",
+ "/media/libyuv/libyuv/include",
+ "/third_party/libwebrtc",
+ "/third_party/libwebrtc/third_party/abseil-cpp",
+ ]
+
+if CONFIG["MOZ_WEBRTC_SIGNALING"]:
+ DIRS += [
+ "common",
+ "jsapi",
+ "jsep",
+ "libwebrtcglue",
+ "sdp",
+ "transportbridge",
+ "/third_party/libwebrtc",
+ ]
+
+ if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
+ DIRS += [
+ "/third_party/drm/",
+ "/third_party/drm/libdrm",
+ "/third_party/gbm/",
+ "/third_party/gbm/libgbm",
+ "/third_party/libepoxy/",
+ "/third_party/pipewire/libpipewire",
+ ]
+
+ # Avoid warnings from third-party code that we can not modify.
+ if CONFIG["CC_TYPE"] == "clang-cl":
+ CXXFLAGS += ["-Wno-invalid-source-encoding"]
+
+
+PREPROCESSED_IPDL_SOURCES += [
+ "PMediaTransport.ipdl",
+]
+
+UNIFIED_SOURCES += [
+ "MediaEngineFake.cpp",
+ "MediaEngineSource.cpp",
+ "MediaTrackConstraints.cpp",
+ "PeerIdentity.cpp",
+]
+
+EXPORTS.mozilla += [
+ "PeerIdentity.h",
+]
+EXPORTS.mozilla.dom += [
+ "RTCCertificate.h",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+# Suppress some GCC/clang warnings being treated as errors:
+# - about attributes on forward declarations for types that are already
+# defined, which complains about important MOZ_EXPORT attributes for
+# android API types
+CXXFLAGS += [
+ "-Wno-error=attributes",
+]
+
+FINAL_LIBRARY = "xul"
+
+# Add libFuzzer configuration directives
+include("/tools/fuzzing/libfuzzer-config.mozbuild")
diff --git a/dom/media/webrtc/sdp/HybridSdpParser.cpp b/dom/media/webrtc/sdp/HybridSdpParser.cpp
new file mode 100644
index 0000000000..d1f2d5c058
--- /dev/null
+++ b/dom/media/webrtc/sdp/HybridSdpParser.cpp
@@ -0,0 +1,88 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "sdp/HybridSdpParser.h"
+#include "sdp/SdpLog.h"
+#include "sdp/SdpPref.h"
+#include "sdp/SdpTelemetry.h"
+#include "sdp/SipccSdpParser.h"
+#include "sdp/RsdparsaSdpParser.h"
+#include "sdp/ParsingResultComparer.h"
+
+#include "mozilla/Logging.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Telemetry.h"
+
+#include <unordered_map>
+
+namespace mozilla {
+
+using mozilla::LogLevel;
+
+const std::string& HybridSdpParser::ParserName() {
+ const static std::string PARSER_NAME = "hybrid";
+ return PARSER_NAME;
+}
+
+HybridSdpParser::HybridSdpParser()
+ : mStrictSuccess(SdpPref::StrictSuccess()),
+ mPrimary(SdpPref::Primary()),
+ mSecondary(SdpPref::Secondary()),
+ mFailover(SdpPref::Failover()) {
+ MOZ_ASSERT(!(mSecondary && mFailover),
+ "Can not have both a secondary and failover parser!");
+ MOZ_LOG(SdpLog, LogLevel::Info,
+ ("Primary SDP Parser: %s", mPrimary->Name().c_str()));
+ mSecondary.apply([](auto& parser) {
+ MOZ_LOG(SdpLog, LogLevel::Info,
+ ("Secondary SDP Logger: %s", parser->Name().c_str()));
+ });
+ mFailover.apply([](auto& parser) {
+ MOZ_LOG(SdpLog, LogLevel::Info,
+ ("Failover SDP Logger: %s", parser->Name().c_str()));
+ });
+}
+
+auto HybridSdpParser::Parse(const std::string& aText)
+ -> UniquePtr<SdpParser::Results> {
+ using Results = UniquePtr<SdpParser::Results>;
+ using Role = SdpTelemetry::Roles;
+ using Mode = SdpPref::AlternateParseModes;
+
+ Mode mode = Mode::Never;
+ auto results = mPrimary->Parse(aText);
+
+ auto successful = [&](Results& aRes) -> bool {
+ // In strict mode any reported error counts as failure
+ if (mStrictSuccess) {
+ return aRes->Ok();
+ }
+ return aRes->Sdp() != nullptr;
+ };
+ // Pass results on for comparison and return A if it was a success and B
+ // otherwise.
+ auto compare = [&](Results&& aResB) -> Results {
+ SdpTelemetry::RecordParse(aResB, mode, Role::Secondary);
+ ParsingResultComparer::Compare(results, aResB, aText, mode);
+ return std::move(successful(results) ? results : aResB);
+ };
+ // Run secondary parser, if there is one, and update selected results.
+ mSecondary.apply([&](auto& sec) {
+ mode = Mode::Parallel;
+ results = compare(std::move(sec->Parse(aText)));
+ });
+ // Run failover parser, if there is one, and update selected results.
+ mFailover.apply([&](auto& failover) { // Only run if primary parser failed
+ mode = Mode::Failover;
+ if (!successful(results)) {
+ results = compare(std::move(failover->Parse(aText)));
+ }
+ });
+
+ SdpTelemetry::RecordParse(results, mode, Role::Primary);
+ return results;
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/sdp/HybridSdpParser.h b/dom/media/webrtc/sdp/HybridSdpParser.h
new file mode 100644
index 0000000000..2b0ef399a5
--- /dev/null
+++ b/dom/media/webrtc/sdp/HybridSdpParser.h
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _HYBRIDSDPPARSER_H_
+#define _HYBRIDSDPPARSER_H_
+
+#include "sdp/SdpParser.h"
+#include "sdp/SdpTelemetry.h"
+
+namespace mozilla {
+
+// This shim parser delegates parsing to WEbRTC-SDP and SIPCC, based on
+// preference flags. Additionally it handles collecting telemetry and fallback
+// behavior between the parsers.
+class HybridSdpParser : public SdpParser {
+ static const std::string& ParserName();
+
+ public:
+ HybridSdpParser();
+ virtual ~HybridSdpParser() = default;
+
+ auto Name() const -> const std::string& override { return ParserName(); }
+ auto Parse(const std::string& aText)
+ -> UniquePtr<SdpParser::Results> override;
+
+ private:
+ const bool mStrictSuccess;
+ const UniquePtr<SdpParser> mPrimary;
+ const Maybe<UniquePtr<SdpParser>> mSecondary;
+ const Maybe<UniquePtr<SdpParser>> mFailover;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/sdp/ParsingResultComparer.cpp b/dom/media/webrtc/sdp/ParsingResultComparer.cpp
new file mode 100644
index 0000000000..8592fa52b3
--- /dev/null
+++ b/dom/media/webrtc/sdp/ParsingResultComparer.cpp
@@ -0,0 +1,331 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "sdp/Sdp.h"
+#include "sdp/ParsingResultComparer.h"
+#include "sdp/SipccSdpParser.h"
+#include "sdp/RsdparsaSdpParser.h"
+#include "sdp/SdpTelemetry.h"
+
+#include <string>
+#include <ostream>
+#include <regex>
+
+#include "mozilla/Assertions.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/Logging.h"
+
+using mozilla::LogLevel;
+static mozilla::LazyLogModule sSdpDiffLogger("sdpdiff_logger");
+
+#define LOGD(msg) MOZ_LOG(sSdpDiffLogger, LogLevel::Debug, msg)
+#define LOGE(msg) MOZ_LOG(sSdpDiffLogger, LogLevel::Error, msg)
+
+#define LOG_EXPECT(result, expect, msg) \
+ { \
+ if (((expect) == SdpComparisonResult::Equal) == (result)) { \
+ LOGD(msg); \
+ } else { \
+ LOGE(("UNEXPECTED COMPARISON RESULT: vvvvvv")); \
+ LOGE(msg); \
+ } \
+ }
+
+namespace mozilla {
+
+using AttributeType = SdpAttribute::AttributeType;
+
+template <typename T>
+std::string ToString(const T& serializable) {
+ std::ostringstream os;
+
+ os << serializable;
+ return os.str();
+}
+bool ParsingResultComparer::Compare(const Results& aResA, const Results& aResB,
+ const std::string& aOriginalSdp,
+ const SdpPref::AlternateParseModes& aMode) {
+ MOZ_ASSERT(aResA, "aResA must not be a nullptr");
+ MOZ_ASSERT(aResB, "aResB must not be a nullptr");
+ MOZ_ASSERT(aResA->ParserName() != aResB->ParserName(),
+ "aResA and aResB must be from different parsers");
+ SdpTelemetry::RecordCompare(aResA, aResB, aMode);
+
+ ParsingResultComparer comparer;
+ if (!aResA->Sdp() || !aResB->Sdp()) {
+ return !aResA->Sdp() && !aResB->Sdp();
+ }
+ if (SipccSdpParser::IsNamed(aResA->ParserName())) {
+ MOZ_ASSERT(RsdparsaSdpParser::IsNamed(aResB->ParserName()));
+ return comparer.Compare(*aResB->Sdp(), *aResA->Sdp(), aOriginalSdp,
+ SdpComparisonResult::Equal);
+ }
+ MOZ_ASSERT(SipccSdpParser::IsNamed(aResB->ParserName()));
+ MOZ_ASSERT(RsdparsaSdpParser::IsNamed(aResA->ParserName()));
+ return comparer.Compare(*aResA->Sdp(), *aResB->Sdp(), aOriginalSdp,
+ SdpComparisonResult::Equal);
+}
+
+bool ParsingResultComparer::Compare(const Sdp& rsdparsaSdp, const Sdp& sipccSdp,
+ const std::string& originalSdp,
+ const SdpComparisonResult expect) {
+ mOriginalSdp = originalSdp;
+ const std::string sipccSdpStr = sipccSdp.ToString();
+ const std::string rsdparsaSdpStr = rsdparsaSdp.ToString();
+
+ bool result = rsdparsaSdpStr == sipccSdpStr;
+ LOG_EXPECT(result, expect, ("The original sdp: \n%s", mOriginalSdp.c_str()));
+ if (result) {
+ Telemetry::ScalarAdd(Telemetry::ScalarID::WEBRTC_SDP_PARSER_DIFF,
+ u"serialization_is_equal"_ns, 1);
+ LOG_EXPECT(result, expect, ("Serialization is equal"));
+ return result;
+ }
+ // Do a deep comparison
+ result = true;
+
+ Telemetry::ScalarAdd(Telemetry::ScalarID::WEBRTC_SDP_PARSER_DIFF,
+ u"serialization_is_not_equal"_ns, 1);
+ LOG_EXPECT(result, expect,
+ ("Serialization is not equal\n"
+ " --- Sipcc SDP ---\n"
+ "%s\n"
+ "--- Rsdparsa SDP ---\n"
+ "%s\n",
+ sipccSdpStr.c_str(), rsdparsaSdpStr.c_str()));
+
+ const std::string rsdparsaOriginStr = ToString(rsdparsaSdp.GetOrigin());
+ const std::string sipccOriginStr = ToString(sipccSdp.GetOrigin());
+
+ // Compare the session level
+ if (rsdparsaOriginStr != sipccOriginStr) {
+ Telemetry::ScalarAdd(Telemetry::ScalarID::WEBRTC_SDP_PARSER_DIFF, u"o="_ns,
+ 1);
+ result = false;
+ LOG_EXPECT(result, expect,
+ ("origin is not equal\nrust origin: %s\nsipcc origin: %s",
+ rsdparsaOriginStr.c_str(), sipccOriginStr.c_str()));
+ }
+
+ if (MOZ_LOG_TEST(sSdpDiffLogger, LogLevel::Debug)) {
+ const auto rust_sess_attr_count = rsdparsaSdp.GetAttributeList().Count();
+ const auto sipcc_sess_attr_count = sipccSdp.GetAttributeList().Count();
+
+ if (rust_sess_attr_count != sipcc_sess_attr_count) {
+ LOG_EXPECT(false, expect,
+ ("Session level attribute count is NOT equal, rsdparsa: %u, "
+ "sipcc: %u\n",
+ rust_sess_attr_count, sipcc_sess_attr_count));
+ }
+ }
+
+ result &= CompareAttrLists(rsdparsaSdp.GetAttributeList(),
+ sipccSdp.GetAttributeList(), -1);
+
+ const uint32_t sipccMediaSecCount =
+ static_cast<uint32_t>(sipccSdp.GetMediaSectionCount());
+ const uint32_t rsdparsaMediaSecCount =
+ static_cast<uint32_t>(rsdparsaSdp.GetMediaSectionCount());
+
+ if (sipccMediaSecCount != rsdparsaMediaSecCount) {
+ result = false;
+ Telemetry::ScalarAdd(Telemetry::ScalarID::WEBRTC_SDP_PARSER_DIFF,
+ u"inequal_msec_count"_ns, 1);
+ LOG_EXPECT(result, expect,
+ ("Media section count is NOT equal, rsdparsa: %d, sipcc: %d \n",
+ rsdparsaMediaSecCount, sipccMediaSecCount));
+ }
+
+ for (size_t i = 0; i < std::min(sipccMediaSecCount, rsdparsaMediaSecCount);
+ i++) {
+ result &= CompareMediaSections(rsdparsaSdp.GetMediaSection(i),
+ sipccSdp.GetMediaSection(i));
+ }
+
+ return result;
+}
+
+bool ParsingResultComparer::CompareMediaSections(
+ const SdpMediaSection& rustMediaSection,
+ const SdpMediaSection& sipccMediaSection,
+ const SdpComparisonResult expect) const {
+ bool result = true;
+ auto trackMediaLineMismatch = [&result, &expect](
+ auto rustValue, auto sipccValue,
+ const nsString& valueDescription) {
+ result = false;
+ nsString typeStr = u"m="_ns;
+ typeStr += valueDescription;
+ Telemetry::ScalarAdd(Telemetry::ScalarID::WEBRTC_SDP_PARSER_DIFF, typeStr,
+ 1);
+ LOG_EXPECT(result, expect,
+ ("The media line values %s are not equal\n"
+ "rsdparsa value: %s\n"
+ "sipcc value: %s\n",
+ NS_LossyConvertUTF16toASCII(valueDescription).get(),
+ ToString(rustValue).c_str(), ToString(sipccValue).c_str()));
+ };
+
+ auto compareMediaLineValue = [trackMediaLineMismatch](
+ auto rustValue, auto sipccValue,
+ const nsString& valueDescription) {
+ if (rustValue != sipccValue) {
+ trackMediaLineMismatch(rustValue, sipccValue, valueDescription);
+ }
+ };
+
+ auto compareSimpleMediaLineValue =
+ [&rustMediaSection, &sipccMediaSection, compareMediaLineValue](
+ auto valGetFuncPtr, const nsString& valueDescription) {
+ compareMediaLineValue((rustMediaSection.*valGetFuncPtr)(),
+ (sipccMediaSection.*valGetFuncPtr)(),
+ valueDescription);
+ };
+
+ compareSimpleMediaLineValue(&SdpMediaSection::GetMediaType, u"media_type"_ns);
+ compareSimpleMediaLineValue(&SdpMediaSection::GetPort, u"port"_ns);
+ compareSimpleMediaLineValue(&SdpMediaSection::GetPortCount, u"port_count"_ns);
+ compareSimpleMediaLineValue(&SdpMediaSection::GetProtocol, u"protocol"_ns);
+ compareSimpleMediaLineValue(&SdpMediaSection::IsReceiving,
+ u"is_receiving"_ns);
+ compareSimpleMediaLineValue(&SdpMediaSection::IsSending, u"is_sending"_ns);
+ compareSimpleMediaLineValue(&SdpMediaSection::GetDirection, u"direction"_ns);
+ compareSimpleMediaLineValue(&SdpMediaSection::GetLevel, u"level"_ns);
+
+ compareMediaLineValue(ToString(rustMediaSection.GetConnection()),
+ ToString(sipccMediaSection.GetConnection()),
+ u"connection"_ns);
+
+ result &= CompareAttrLists(rustMediaSection.GetAttributeList(),
+ sipccMediaSection.GetAttributeList(),
+ static_cast<int>(rustMediaSection.GetLevel()));
+ return result;
+}
+
+bool ParsingResultComparer::CompareAttrLists(
+ const SdpAttributeList& rustAttrlist, const SdpAttributeList& sipccAttrlist,
+ int level, const SdpComparisonResult expect) const {
+ bool result = true;
+
+ for (size_t i = AttributeType::kFirstAttribute;
+ i <= static_cast<size_t>(AttributeType::kLastAttribute); i++) {
+ const AttributeType type = static_cast<AttributeType>(i);
+ std::string attrStr;
+ if (type != AttributeType::kDirectionAttribute) {
+ attrStr = "a=" + SdpAttribute::GetAttributeTypeString(type);
+ } else {
+ attrStr = "a=_direction_attribute_";
+ }
+
+ if (sipccAttrlist.HasAttribute(type, false)) {
+ auto sipccAttrStr = ToString(*sipccAttrlist.GetAttribute(type, false));
+
+ if (!rustAttrlist.HasAttribute(type, false)) {
+ result = false;
+ nsString typeStr;
+ typeStr.AssignASCII(attrStr.c_str());
+ typeStr += u"_missing"_ns;
+ Telemetry::ScalarAdd(Telemetry::ScalarID::WEBRTC_SDP_PARSER_DIFF,
+ typeStr, 1);
+ LOG_EXPECT(result, expect,
+ ("Rust is missing the attribute: %s\n", attrStr.c_str()));
+ LOG_EXPECT(result, expect,
+ ("Rust is missing: %s\n", sipccAttrStr.c_str()));
+
+ continue;
+ }
+
+ auto rustAttrStr = ToString(*rustAttrlist.GetAttribute(type, false));
+
+ if (rustAttrStr != sipccAttrStr) {
+ if (type == AttributeType::kFmtpAttribute) {
+ if (rustAttrlist.GetFmtp() == sipccAttrlist.GetFmtp()) {
+ continue;
+ }
+ }
+
+ std::string originalAttrStr = GetAttributeLines(attrStr, level);
+ if (rustAttrStr != originalAttrStr) {
+ result = false;
+ nsString typeStr;
+ typeStr.AssignASCII(attrStr.c_str());
+ typeStr += u"_inequal"_ns;
+ Telemetry::ScalarAdd(Telemetry::ScalarID::WEBRTC_SDP_PARSER_DIFF,
+ typeStr, 1);
+ LOG_EXPECT(result, expect,
+ ("%s is neither equal to sipcc nor to the orginal sdp\n"
+ "--------------rsdparsa attribute---------------\n"
+ "%s"
+ "--------------sipcc attribute---------------\n"
+ "%s"
+ "--------------original attribute---------------\n"
+ "%s\n",
+ attrStr.c_str(), rustAttrStr.c_str(),
+ sipccAttrStr.c_str(), originalAttrStr.c_str()));
+ } else {
+ LOG_EXPECT(
+ result, expect,
+ ("But the rust serialization is equal to the orignal sdp\n"));
+ }
+ }
+ } else {
+ if (rustAttrlist.HasAttribute(type, false)) {
+ nsString typeStr;
+ typeStr.AssignASCII(attrStr.c_str());
+ typeStr += u"_unexpected"_ns;
+ Telemetry::ScalarAdd(Telemetry::ScalarID::WEBRTC_SDP_PARSER_DIFF,
+ typeStr, 1);
+ }
+ }
+ }
+
+ return result;
+}
+
+std::vector<std::string> SplitLines(const std::string& sdp) {
+ std::stringstream ss(sdp);
+ std::string to;
+ std::vector<std::string> lines;
+
+ while (std::getline(ss, to, '\n')) {
+ lines.push_back(to);
+ }
+
+ return lines;
+}
+
+std::string ParsingResultComparer::GetAttributeLines(
+ const std::string& attrType, int level) const {
+ std::vector<std::string> lines = SplitLines(mOriginalSdp);
+ std::string attrToFind = attrType + ":";
+ std::string attrLines;
+ int currentLevel = -1;
+ // Filters rtcp-fb lines that contain "x-..." types
+ // This is because every SDP from Edge contains these rtcp-fb x- types
+ // for example: a=rtcp-fb:121 x-foo
+ std::regex customRtcpFbLines(R"(a\=rtcp\-fb\:(\d+|\*).* x\-.*)");
+
+ for (auto& line : lines) {
+ if (line.find("m=") == 0) {
+ if (level > currentLevel) {
+ attrLines.clear();
+ currentLevel++;
+ } else {
+ break;
+ }
+ } else if (line.find(attrToFind) == 0) {
+ if (std::regex_match(line, customRtcpFbLines)) {
+ continue;
+ }
+
+ attrLines += (line + '\n');
+ }
+ }
+
+ return attrLines;
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/sdp/ParsingResultComparer.h b/dom/media/webrtc/sdp/ParsingResultComparer.h
new file mode 100644
index 0000000000..5a1c2ac635
--- /dev/null
+++ b/dom/media/webrtc/sdp/ParsingResultComparer.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _PARSINGRESULTCOMPARER_H_
+#define _PARSINGRESULTCOMPARER_H_
+
+#include "sdp/SdpParser.h"
+#include "sdp/SdpPref.h"
+
+#include <string>
+
+namespace mozilla {
+
+class Sdp;
+class SdpMediaSection;
+class SdpAttributeList;
+
+enum class SdpComparisonResult {
+ Inequal = false,
+ Equal = true,
+};
+
+class ParsingResultComparer {
+ public:
+ using Results = UniquePtr<SdpParser::Results>;
+
+ ParsingResultComparer() = default;
+
+ static bool Compare(const Results& aResA, const Results& aResB,
+ const std::string& aOrignalSdp,
+ const SdpPref::AlternateParseModes& aMode);
+ bool Compare(const Sdp& rsdparsaSdp, const Sdp& sipccSdp,
+ const std::string& aOriginalSdp,
+ const SdpComparisonResult expect = SdpComparisonResult::Equal);
+ bool CompareMediaSections(
+ const SdpMediaSection& rustMediaSection,
+ const SdpMediaSection& sipccMediaSection,
+ const SdpComparisonResult expect = SdpComparisonResult::Equal) const;
+ bool CompareAttrLists(
+ const SdpAttributeList& rustAttrlist,
+ const SdpAttributeList& sipccAttrlist, int level,
+ const SdpComparisonResult expect = SdpComparisonResult::Equal) const;
+ void TrackRustParsingFailed(size_t sipccErrorCount) const;
+ void TrackSipccParsingFailed(size_t rustErrorCount) const;
+
+ private:
+ std::string mOriginalSdp;
+
+ std::string GetAttributeLines(const std::string& attrType, int level) const;
+};
+
+} // namespace mozilla
+
+#endif // _PARSINGRESULTCOMPARER_H_
diff --git a/dom/media/webrtc/sdp/RsdparsaSdp.cpp b/dom/media/webrtc/sdp/RsdparsaSdp.cpp
new file mode 100644
index 0000000000..0e653dc17e
--- /dev/null
+++ b/dom/media/webrtc/sdp/RsdparsaSdp.cpp
@@ -0,0 +1,126 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "sdp/RsdparsaSdp.h"
+
+#include <cstdlib>
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Assertions.h"
+#include "nsError.h"
+#include <iostream>
+
+#include "sdp/RsdparsaSdpInc.h"
+#include "sdp/RsdparsaSdpMediaSection.h"
+
+#ifdef CRLF
+# undef CRLF
+#endif
+#define CRLF "\r\n"
+
+namespace mozilla {
+
+RsdparsaSdp::RsdparsaSdp(RsdparsaSessionHandle session, const SdpOrigin& origin)
+ : mSession(std::move(session)), mOrigin(origin) {
+ RsdparsaSessionHandle attributeSession(sdp_new_reference(mSession.get()));
+ mAttributeList.reset(
+ new RsdparsaSdpAttributeList(std::move(attributeSession)));
+
+ size_t section_count = sdp_media_section_count(mSession.get());
+ for (size_t level = 0; level < section_count; level++) {
+ RustMediaSection* mediaSection =
+ sdp_get_media_section(mSession.get(), level);
+ if (!mediaSection) {
+ MOZ_ASSERT(false,
+ "sdp_get_media_section failed because level was out of"
+ " bounds, but we did a bounds check!");
+ break;
+ }
+ RsdparsaSessionHandle newSession(sdp_new_reference(mSession.get()));
+ RsdparsaSdpMediaSection* sdpMediaSection;
+ sdpMediaSection = new RsdparsaSdpMediaSection(
+ level, std::move(newSession), mediaSection, mAttributeList.get());
+ mMediaSections.emplace_back(sdpMediaSection);
+ }
+}
+
+RsdparsaSdp::RsdparsaSdp(const RsdparsaSdp& aOrig)
+ : RsdparsaSdp(RsdparsaSessionHandle(create_sdp_clone(aOrig.mSession.get())),
+ aOrig.mOrigin) {}
+
+Sdp* RsdparsaSdp::Clone() const { return new RsdparsaSdp(*this); }
+
+const SdpOrigin& RsdparsaSdp::GetOrigin() const { return mOrigin; }
+
+uint32_t RsdparsaSdp::GetBandwidth(const std::string& type) const {
+ return get_sdp_bandwidth(mSession.get(), type.c_str());
+}
+
+const SdpMediaSection& RsdparsaSdp::GetMediaSection(size_t level) const {
+ if (level > mMediaSections.size()) {
+ MOZ_CRASH();
+ }
+ return *mMediaSections[level];
+}
+
+SdpMediaSection& RsdparsaSdp::GetMediaSection(size_t level) {
+ if (level > mMediaSections.size()) {
+ MOZ_CRASH();
+ }
+ return *mMediaSections[level];
+}
+
+SdpMediaSection& RsdparsaSdp::AddMediaSection(
+ SdpMediaSection::MediaType mediaType, SdpDirectionAttribute::Direction dir,
+ uint16_t port, SdpMediaSection::Protocol protocol, sdp::AddrType addrType,
+ const std::string& addr) {
+ StringView rustAddr{addr.c_str(), addr.size()};
+ auto nr = sdp_add_media_section(mSession.get(), mediaType, dir, port,
+ protocol, addrType, rustAddr);
+
+ if (NS_SUCCEEDED(nr)) {
+ size_t level = mMediaSections.size();
+ RsdparsaSessionHandle newSessHandle(sdp_new_reference(mSession.get()));
+
+ auto rustMediaSection = sdp_get_media_section(mSession.get(), level);
+ auto mediaSection =
+ new RsdparsaSdpMediaSection(level, std::move(newSessHandle),
+ rustMediaSection, mAttributeList.get());
+ mMediaSections.emplace_back(mediaSection);
+
+ return *mediaSection;
+ } else {
+ // Return the last media section if the construction of this one fails
+ return GetMediaSection(mMediaSections.size() - 1);
+ }
+}
+
+void RsdparsaSdp::Serialize(std::ostream& os) const {
+ os << "v=0" << CRLF << mOrigin << "s=-" << CRLF;
+
+ // We don't support creating i=, u=, e=, p=
+ // We don't generate c= at the session level (only in media)
+
+ BandwidthVec* bwVec = sdp_get_session_bandwidth_vec(mSession.get());
+ char* bwString = sdp_serialize_bandwidth(bwVec);
+ if (bwString) {
+ os << bwString;
+ sdp_free_string(bwString);
+ }
+
+ os << "t=0 0" << CRLF;
+
+ // We don't support r= or z=
+
+ // attributes
+ os << *mAttributeList;
+
+ // media sections
+ for (const auto& msection : mMediaSections) {
+ os << *msection;
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/sdp/RsdparsaSdp.h b/dom/media/webrtc/sdp/RsdparsaSdp.h
new file mode 100644
index 0000000000..4942abb574
--- /dev/null
+++ b/dom/media/webrtc/sdp/RsdparsaSdp.h
@@ -0,0 +1,72 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _RSDPARSA_SDP_H_
+#define _RSDPARSA_SDP_H_
+
+#include "mozilla/UniquePtr.h"
+
+#include "sdp/Sdp.h"
+
+#include "sdp/RsdparsaSdpMediaSection.h"
+#include "sdp/RsdparsaSdpAttributeList.h"
+#include "sdp/RsdparsaSdpInc.h"
+#include "sdp/RsdparsaSdpGlue.h"
+
+namespace mozilla {
+
+class RsdparsaSdpParser;
+class SdpParser;
+
+class RsdparsaSdp final : public Sdp {
+ friend class RsdparsaSdpParser;
+
+ public:
+ explicit RsdparsaSdp(RsdparsaSessionHandle session, const SdpOrigin& origin);
+
+ Sdp* Clone() const override;
+
+ const SdpOrigin& GetOrigin() const override;
+
+ // Note: connection information is always retrieved from media sections
+ uint32_t GetBandwidth(const std::string& type) const override;
+
+ size_t GetMediaSectionCount() const override {
+ return sdp_media_section_count(mSession.get());
+ }
+
+ const SdpAttributeList& GetAttributeList() const override {
+ return *mAttributeList;
+ }
+
+ SdpAttributeList& GetAttributeList() override { return *mAttributeList; }
+
+ const SdpMediaSection& GetMediaSection(size_t level) const override;
+
+ SdpMediaSection& GetMediaSection(size_t level) override;
+
+ SdpMediaSection& AddMediaSection(SdpMediaSection::MediaType media,
+ SdpDirectionAttribute::Direction dir,
+ uint16_t port,
+ SdpMediaSection::Protocol proto,
+ sdp::AddrType addrType,
+ const std::string& addr) override;
+
+ void Serialize(std::ostream&) const override;
+
+ private:
+ RsdparsaSdp() : mOrigin("", 0, 0, sdp::kIPv4, "") {}
+ RsdparsaSdp(const RsdparsaSdp& aOrig);
+
+ RsdparsaSessionHandle mSession;
+ SdpOrigin mOrigin;
+ UniquePtr<RsdparsaSdpAttributeList> mAttributeList;
+ std::vector<UniquePtr<RsdparsaSdpMediaSection>> mMediaSections;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/sdp/RsdparsaSdpAttributeList.cpp b/dom/media/webrtc/sdp/RsdparsaSdpAttributeList.cpp
new file mode 100644
index 0000000000..0b76757c44
--- /dev/null
+++ b/dom/media/webrtc/sdp/RsdparsaSdpAttributeList.cpp
@@ -0,0 +1,1301 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsCRT.h"
+
+#include "sdp/RsdparsaSdpAttributeList.h"
+#include "sdp/RsdparsaSdpInc.h"
+#include "sdp/RsdparsaSdpGlue.h"
+
+#include <ostream>
+#include "mozilla/Assertions.h"
+
+#include <limits>
+
+namespace mozilla {
+
+const std::string RsdparsaSdpAttributeList::kEmptyString = "";
+
+RsdparsaSdpAttributeList::~RsdparsaSdpAttributeList() {
+ for (size_t i = 0; i < kNumAttributeTypes; ++i) {
+ delete mAttributes[i];
+ }
+}
+
+bool RsdparsaSdpAttributeList::HasAttribute(AttributeType type,
+ bool sessionFallback) const {
+ return !!GetAttribute(type, sessionFallback);
+}
+
+const SdpAttribute* RsdparsaSdpAttributeList::GetAttribute(
+ AttributeType type, bool sessionFallback) const {
+ const SdpAttribute* value = mAttributes[static_cast<size_t>(type)];
+ // Only do fallback when the attribute can appear at both the media and
+ // session level
+ if (!value && !AtSessionLevel() && sessionFallback &&
+ SdpAttribute::IsAllowedAtSessionLevel(type) &&
+ SdpAttribute::IsAllowedAtMediaLevel(type)) {
+ return mSessionAttributes->GetAttribute(type, false);
+ }
+ return value;
+}
+
+void RsdparsaSdpAttributeList::RemoveAttribute(AttributeType type) {
+ delete mAttributes[static_cast<size_t>(type)];
+ mAttributes[static_cast<size_t>(type)] = nullptr;
+}
+
+void RsdparsaSdpAttributeList::Clear() {
+ for (size_t i = 0; i < kNumAttributeTypes; ++i) {
+ RemoveAttribute(static_cast<AttributeType>(i));
+ }
+}
+
+uint32_t RsdparsaSdpAttributeList::Count() const {
+ uint32_t count = 0;
+ for (size_t i = 0; i < kNumAttributeTypes; ++i) {
+ if (mAttributes[i]) {
+ count++;
+ }
+ }
+ return count;
+}
+
+void RsdparsaSdpAttributeList::SetAttribute(SdpAttribute* attr) {
+ if (!IsAllowedHere(attr->GetType())) {
+ MOZ_ASSERT(false, "This type of attribute is not allowed here");
+ delete attr;
+ return;
+ }
+ RemoveAttribute(attr->GetType());
+ mAttributes[attr->GetType()] = attr;
+}
+
+const std::vector<std::string>& RsdparsaSdpAttributeList::GetCandidate() const {
+ if (!HasAttribute(SdpAttribute::kCandidateAttribute)) {
+ MOZ_CRASH();
+ }
+
+ return static_cast<const SdpMultiStringAttribute*>(
+ GetAttribute(SdpAttribute::kCandidateAttribute))
+ ->mValues;
+}
+
+const SdpConnectionAttribute& RsdparsaSdpAttributeList::GetConnection() const {
+ if (!HasAttribute(SdpAttribute::kConnectionAttribute)) {
+ MOZ_CRASH();
+ }
+
+ return *static_cast<const SdpConnectionAttribute*>(
+ GetAttribute(SdpAttribute::kConnectionAttribute));
+}
+
+SdpDirectionAttribute::Direction RsdparsaSdpAttributeList::GetDirection()
+ const {
+ if (!HasAttribute(SdpAttribute::kDirectionAttribute)) {
+ MOZ_CRASH();
+ }
+
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kDirectionAttribute);
+ return static_cast<const SdpDirectionAttribute*>(attr)->mValue;
+}
+
+const SdpDtlsMessageAttribute& RsdparsaSdpAttributeList::GetDtlsMessage()
+ const {
+ if (!HasAttribute(SdpAttribute::kDtlsMessageAttribute)) {
+ MOZ_CRASH();
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kDtlsMessageAttribute);
+ return *static_cast<const SdpDtlsMessageAttribute*>(attr);
+}
+
+const SdpExtmapAttributeList& RsdparsaSdpAttributeList::GetExtmap() const {
+ if (!HasAttribute(SdpAttribute::kExtmapAttribute)) {
+ MOZ_CRASH();
+ }
+
+ return *static_cast<const SdpExtmapAttributeList*>(
+ GetAttribute(SdpAttribute::kExtmapAttribute));
+}
+
+const SdpFingerprintAttributeList& RsdparsaSdpAttributeList::GetFingerprint()
+ const {
+ if (!HasAttribute(SdpAttribute::kFingerprintAttribute)) {
+ MOZ_CRASH();
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kFingerprintAttribute);
+ return *static_cast<const SdpFingerprintAttributeList*>(attr);
+}
+
+const SdpFmtpAttributeList& RsdparsaSdpAttributeList::GetFmtp() const {
+ if (!HasAttribute(SdpAttribute::kFmtpAttribute)) {
+ MOZ_CRASH();
+ }
+
+ return *static_cast<const SdpFmtpAttributeList*>(
+ GetAttribute(SdpAttribute::kFmtpAttribute));
+}
+
+const SdpGroupAttributeList& RsdparsaSdpAttributeList::GetGroup() const {
+ if (!HasAttribute(SdpAttribute::kGroupAttribute)) {
+ MOZ_CRASH();
+ }
+
+ return *static_cast<const SdpGroupAttributeList*>(
+ GetAttribute(SdpAttribute::kGroupAttribute));
+}
+
+const SdpOptionsAttribute& RsdparsaSdpAttributeList::GetIceOptions() const {
+ if (!HasAttribute(SdpAttribute::kIceOptionsAttribute)) {
+ MOZ_CRASH();
+ }
+
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kIceOptionsAttribute);
+ return *static_cast<const SdpOptionsAttribute*>(attr);
+}
+
+const std::string& RsdparsaSdpAttributeList::GetIcePwd() const {
+ if (!HasAttribute(SdpAttribute::kIcePwdAttribute)) {
+ return kEmptyString;
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kIcePwdAttribute);
+ return static_cast<const SdpStringAttribute*>(attr)->mValue;
+}
+
+const std::string& RsdparsaSdpAttributeList::GetIceUfrag() const {
+ if (!HasAttribute(SdpAttribute::kIceUfragAttribute)) {
+ return kEmptyString;
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kIceUfragAttribute);
+ return static_cast<const SdpStringAttribute*>(attr)->mValue;
+}
+
+const std::string& RsdparsaSdpAttributeList::GetIdentity() const {
+ if (!HasAttribute(SdpAttribute::kIdentityAttribute)) {
+ return kEmptyString;
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kIdentityAttribute);
+ return static_cast<const SdpStringAttribute*>(attr)->mValue;
+}
+
+const SdpImageattrAttributeList& RsdparsaSdpAttributeList::GetImageattr()
+ const {
+ if (!HasAttribute(SdpAttribute::kImageattrAttribute)) {
+ MOZ_CRASH();
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kImageattrAttribute);
+ return *static_cast<const SdpImageattrAttributeList*>(attr);
+}
+
+const SdpSimulcastAttribute& RsdparsaSdpAttributeList::GetSimulcast() const {
+ if (!HasAttribute(SdpAttribute::kSimulcastAttribute)) {
+ MOZ_CRASH();
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kSimulcastAttribute);
+ return *static_cast<const SdpSimulcastAttribute*>(attr);
+}
+
+const std::string& RsdparsaSdpAttributeList::GetLabel() const {
+ if (!HasAttribute(SdpAttribute::kLabelAttribute)) {
+ return kEmptyString;
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kLabelAttribute);
+ return static_cast<const SdpStringAttribute*>(attr)->mValue;
+}
+
+uint32_t RsdparsaSdpAttributeList::GetMaxptime() const {
+ if (!HasAttribute(SdpAttribute::kMaxptimeAttribute)) {
+ MOZ_CRASH();
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kMaxptimeAttribute);
+ return static_cast<const SdpNumberAttribute*>(attr)->mValue;
+}
+
+const std::string& RsdparsaSdpAttributeList::GetMid() const {
+ if (!HasAttribute(SdpAttribute::kMidAttribute)) {
+ return kEmptyString;
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kMidAttribute);
+ return static_cast<const SdpStringAttribute*>(attr)->mValue;
+}
+
+const SdpMsidAttributeList& RsdparsaSdpAttributeList::GetMsid() const {
+ if (!HasAttribute(SdpAttribute::kMsidAttribute)) {
+ MOZ_CRASH();
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kMsidAttribute);
+ return *static_cast<const SdpMsidAttributeList*>(attr);
+}
+
+const SdpMsidSemanticAttributeList& RsdparsaSdpAttributeList::GetMsidSemantic()
+ const {
+ if (!HasAttribute(SdpAttribute::kMsidSemanticAttribute)) {
+ MOZ_CRASH();
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kMsidSemanticAttribute);
+ return *static_cast<const SdpMsidSemanticAttributeList*>(attr);
+}
+
+const SdpRidAttributeList& RsdparsaSdpAttributeList::GetRid() const {
+ if (!HasAttribute(SdpAttribute::kRidAttribute)) {
+ MOZ_CRASH();
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kRidAttribute);
+ return *static_cast<const SdpRidAttributeList*>(attr);
+}
+
+uint32_t RsdparsaSdpAttributeList::GetPtime() const {
+ if (!HasAttribute(SdpAttribute::kPtimeAttribute)) {
+ MOZ_CRASH();
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kPtimeAttribute);
+ return static_cast<const SdpNumberAttribute*>(attr)->mValue;
+}
+
+const SdpRtcpAttribute& RsdparsaSdpAttributeList::GetRtcp() const {
+ if (!HasAttribute(SdpAttribute::kRtcpAttribute)) {
+ MOZ_CRASH();
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kRtcpAttribute);
+ return *static_cast<const SdpRtcpAttribute*>(attr);
+}
+
+const SdpRtcpFbAttributeList& RsdparsaSdpAttributeList::GetRtcpFb() const {
+ if (!HasAttribute(SdpAttribute::kRtcpFbAttribute)) {
+ MOZ_CRASH();
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kRtcpFbAttribute);
+ return *static_cast<const SdpRtcpFbAttributeList*>(attr);
+}
+
+const SdpRemoteCandidatesAttribute&
+RsdparsaSdpAttributeList::GetRemoteCandidates() const {
+ MOZ_CRASH("Not yet implemented");
+}
+
+const SdpRtpmapAttributeList& RsdparsaSdpAttributeList::GetRtpmap() const {
+ if (!HasAttribute(SdpAttribute::kRtpmapAttribute)) {
+ MOZ_CRASH();
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kRtpmapAttribute);
+ return *static_cast<const SdpRtpmapAttributeList*>(attr);
+}
+
+const SdpSctpmapAttributeList& RsdparsaSdpAttributeList::GetSctpmap() const {
+ if (!HasAttribute(SdpAttribute::kSctpmapAttribute)) {
+ MOZ_CRASH();
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kSctpmapAttribute);
+ return *static_cast<const SdpSctpmapAttributeList*>(attr);
+}
+
+uint32_t RsdparsaSdpAttributeList::GetSctpPort() const {
+ if (!HasAttribute(SdpAttribute::kSctpPortAttribute)) {
+ MOZ_CRASH();
+ }
+
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kSctpPortAttribute);
+ return static_cast<const SdpNumberAttribute*>(attr)->mValue;
+}
+
+uint32_t RsdparsaSdpAttributeList::GetMaxMessageSize() const {
+ if (!HasAttribute(SdpAttribute::kMaxMessageSizeAttribute)) {
+ MOZ_CRASH();
+ }
+
+ const SdpAttribute* attr =
+ GetAttribute(SdpAttribute::kMaxMessageSizeAttribute);
+ return static_cast<const SdpNumberAttribute*>(attr)->mValue;
+}
+
+const SdpSetupAttribute& RsdparsaSdpAttributeList::GetSetup() const {
+ if (!HasAttribute(SdpAttribute::kSetupAttribute)) {
+ MOZ_CRASH();
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kSetupAttribute);
+ return *static_cast<const SdpSetupAttribute*>(attr);
+}
+
+const SdpSsrcAttributeList& RsdparsaSdpAttributeList::GetSsrc() const {
+ if (!HasAttribute(SdpAttribute::kSsrcAttribute)) {
+ MOZ_CRASH();
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kSsrcAttribute);
+ return *static_cast<const SdpSsrcAttributeList*>(attr);
+}
+
+const SdpSsrcGroupAttributeList& RsdparsaSdpAttributeList::GetSsrcGroup()
+ const {
+ if (!HasAttribute(SdpAttribute::kSsrcGroupAttribute)) {
+ MOZ_CRASH();
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kSsrcGroupAttribute);
+ return *static_cast<const SdpSsrcGroupAttributeList*>(attr);
+}
+
+void RsdparsaSdpAttributeList::LoadAttribute(RustAttributeList* attributeList,
+ AttributeType type) {
+ if (!mAttributes[type]) {
+ switch (type) {
+ case SdpAttribute::kIceUfragAttribute:
+ LoadIceUfrag(attributeList);
+ return;
+ case SdpAttribute::kIcePwdAttribute:
+ LoadIcePwd(attributeList);
+ return;
+ case SdpAttribute::kIceOptionsAttribute:
+ LoadIceOptions(attributeList);
+ return;
+ case SdpAttribute::kDtlsMessageAttribute:
+ LoadDtlsMessage(attributeList);
+ return;
+ case SdpAttribute::kFingerprintAttribute:
+ LoadFingerprint(attributeList);
+ return;
+ case SdpAttribute::kIdentityAttribute:
+ LoadIdentity(attributeList);
+ return;
+ case SdpAttribute::kSetupAttribute:
+ LoadSetup(attributeList);
+ return;
+ case SdpAttribute::kSsrcAttribute:
+ LoadSsrc(attributeList);
+ return;
+ case SdpAttribute::kRtpmapAttribute:
+ LoadRtpmap(attributeList);
+ return;
+ case SdpAttribute::kFmtpAttribute:
+ LoadFmtp(attributeList);
+ return;
+ case SdpAttribute::kPtimeAttribute:
+ LoadPtime(attributeList);
+ return;
+ case SdpAttribute::kIceLiteAttribute:
+ case SdpAttribute::kRtcpMuxAttribute:
+ case SdpAttribute::kRtcpRsizeAttribute:
+ case SdpAttribute::kBundleOnlyAttribute:
+ case SdpAttribute::kEndOfCandidatesAttribute:
+ LoadFlags(attributeList);
+ return;
+ case SdpAttribute::kMaxMessageSizeAttribute:
+ LoadMaxMessageSize(attributeList);
+ return;
+ case SdpAttribute::kMidAttribute:
+ LoadMid(attributeList);
+ return;
+ case SdpAttribute::kMsidAttribute:
+ LoadMsid(attributeList);
+ return;
+ case SdpAttribute::kMsidSemanticAttribute:
+ LoadMsidSemantics(attributeList);
+ return;
+ case SdpAttribute::kGroupAttribute:
+ LoadGroup(attributeList);
+ return;
+ case SdpAttribute::kRtcpAttribute:
+ LoadRtcp(attributeList);
+ return;
+ case SdpAttribute::kRtcpFbAttribute:
+ LoadRtcpFb(attributeList);
+ return;
+ case SdpAttribute::kImageattrAttribute:
+ LoadImageattr(attributeList);
+ return;
+ case SdpAttribute::kSctpmapAttribute:
+ LoadSctpmaps(attributeList);
+ return;
+ case SdpAttribute::kDirectionAttribute:
+ LoadDirection(attributeList);
+ return;
+ case SdpAttribute::kRemoteCandidatesAttribute:
+ LoadRemoteCandidates(attributeList);
+ return;
+ case SdpAttribute::kRidAttribute:
+ LoadRids(attributeList);
+ return;
+ case SdpAttribute::kSctpPortAttribute:
+ LoadSctpPort(attributeList);
+ return;
+ case SdpAttribute::kExtmapAttribute:
+ LoadExtmap(attributeList);
+ return;
+ case SdpAttribute::kSimulcastAttribute:
+ LoadSimulcast(attributeList);
+ return;
+ case SdpAttribute::kMaxptimeAttribute:
+ LoadMaxPtime(attributeList);
+ return;
+ case SdpAttribute::kCandidateAttribute:
+ LoadCandidate(attributeList);
+ return;
+ case SdpAttribute::kSsrcGroupAttribute:
+ LoadSsrcGroup(attributeList);
+ return;
+ case SdpAttribute::kConnectionAttribute:
+ case SdpAttribute::kIceMismatchAttribute:
+ case SdpAttribute::kLabelAttribute:
+ // These attributes are unused
+ return;
+ }
+ }
+}
+
+void RsdparsaSdpAttributeList::LoadAll(RustAttributeList* attributeList) {
+ for (int i = SdpAttribute::kFirstAttribute; i <= SdpAttribute::kLastAttribute;
+ i++) {
+ LoadAttribute(attributeList, static_cast<SdpAttribute::AttributeType>(i));
+ }
+}
+
+void RsdparsaSdpAttributeList::LoadIceUfrag(RustAttributeList* attributeList) {
+ StringView ufragStr;
+ nsresult nr = sdp_get_iceufrag(attributeList, &ufragStr);
+ if (NS_SUCCEEDED(nr)) {
+ std::string iceufrag = convertStringView(ufragStr);
+ SetAttribute(
+ new SdpStringAttribute(SdpAttribute::kIceUfragAttribute, iceufrag));
+ }
+}
+
+void RsdparsaSdpAttributeList::LoadIcePwd(RustAttributeList* attributeList) {
+ StringView pwdStr;
+ nsresult nr = sdp_get_icepwd(attributeList, &pwdStr);
+ if (NS_SUCCEEDED(nr)) {
+ std::string icePwd = convertStringView(pwdStr);
+ SetAttribute(
+ new SdpStringAttribute(SdpAttribute::kIcePwdAttribute, icePwd));
+ }
+}
+
+void RsdparsaSdpAttributeList::LoadIdentity(RustAttributeList* attributeList) {
+ StringView identityStr;
+ nsresult nr = sdp_get_identity(attributeList, &identityStr);
+ if (NS_SUCCEEDED(nr)) {
+ std::string identity = convertStringView(identityStr);
+ SetAttribute(
+ new SdpStringAttribute(SdpAttribute::kIdentityAttribute, identity));
+ }
+}
+
+void RsdparsaSdpAttributeList::LoadIceOptions(
+ RustAttributeList* attributeList) {
+ StringVec* options;
+ nsresult nr = sdp_get_iceoptions(attributeList, &options);
+ if (NS_SUCCEEDED(nr)) {
+ std::vector<std::string> optionsVec;
+ auto optionsAttr =
+ MakeUnique<SdpOptionsAttribute>(SdpAttribute::kIceOptionsAttribute);
+ for (size_t i = 0; i < string_vec_len(options); i++) {
+ StringView optionStr;
+ string_vec_get_view(options, i, &optionStr);
+ optionsAttr->PushEntry(convertStringView(optionStr));
+ }
+ SetAttribute(optionsAttr.release());
+ }
+}
+
+void RsdparsaSdpAttributeList::LoadFingerprint(
+ RustAttributeList* attributeList) {
+ size_t nFp = sdp_get_fingerprint_count(attributeList);
+ if (nFp == 0) {
+ return;
+ }
+ auto rustFingerprints = MakeUnique<RustSdpAttributeFingerprint[]>(nFp);
+ sdp_get_fingerprints(attributeList, nFp, rustFingerprints.get());
+ auto fingerprints = MakeUnique<SdpFingerprintAttributeList>();
+ for (size_t i = 0; i < nFp; i++) {
+ const RustSdpAttributeFingerprint& fingerprint = rustFingerprints[i];
+ std::string algorithm;
+ switch (fingerprint.hashAlgorithm) {
+ case RustSdpAttributeFingerprintHashAlgorithm::kSha1:
+ algorithm = "sha-1";
+ break;
+ case RustSdpAttributeFingerprintHashAlgorithm::kSha224:
+ algorithm = "sha-224";
+ break;
+ case RustSdpAttributeFingerprintHashAlgorithm::kSha256:
+ algorithm = "sha-256";
+ break;
+ case RustSdpAttributeFingerprintHashAlgorithm::kSha384:
+ algorithm = "sha-384";
+ break;
+ case RustSdpAttributeFingerprintHashAlgorithm::kSha512:
+ algorithm = "sha-512";
+ break;
+ }
+
+ std::vector<uint8_t> fingerprintBytes =
+ convertU8Vec(fingerprint.fingerprint);
+
+ fingerprints->PushEntry(algorithm, fingerprintBytes);
+ }
+ SetAttribute(fingerprints.release());
+}
+
+void RsdparsaSdpAttributeList::LoadDtlsMessage(
+ RustAttributeList* attributeList) {
+ RustSdpAttributeDtlsMessage rustDtlsMessage;
+ nsresult nr = sdp_get_dtls_message(attributeList, &rustDtlsMessage);
+ if (NS_SUCCEEDED(nr)) {
+ SdpDtlsMessageAttribute::Role role;
+ if (rustDtlsMessage.role == RustSdpAttributeDtlsMessageType::kClient) {
+ role = SdpDtlsMessageAttribute::kClient;
+ } else {
+ role = SdpDtlsMessageAttribute::kServer;
+ }
+
+ std::string value = convertStringView(rustDtlsMessage.value);
+
+ SetAttribute(new SdpDtlsMessageAttribute(role, value));
+ }
+}
+
+void RsdparsaSdpAttributeList::LoadSetup(RustAttributeList* attributeList) {
+ RustSdpSetup rustSetup;
+ nsresult nr = sdp_get_setup(attributeList, &rustSetup);
+ if (NS_SUCCEEDED(nr)) {
+ SdpSetupAttribute::Role setupEnum;
+ switch (rustSetup) {
+ case RustSdpSetup::kRustActive:
+ setupEnum = SdpSetupAttribute::kActive;
+ break;
+ case RustSdpSetup::kRustActpass:
+ setupEnum = SdpSetupAttribute::kActpass;
+ break;
+ case RustSdpSetup::kRustHoldconn:
+ setupEnum = SdpSetupAttribute::kHoldconn;
+ break;
+ case RustSdpSetup::kRustPassive:
+ setupEnum = SdpSetupAttribute::kPassive;
+ break;
+ }
+ SetAttribute(new SdpSetupAttribute(setupEnum));
+ }
+}
+
+void RsdparsaSdpAttributeList::LoadSsrc(RustAttributeList* attributeList) {
+ size_t numSsrc = sdp_get_ssrc_count(attributeList);
+ if (numSsrc == 0) {
+ return;
+ }
+ auto rustSsrcs = MakeUnique<RustSdpAttributeSsrc[]>(numSsrc);
+ sdp_get_ssrcs(attributeList, numSsrc, rustSsrcs.get());
+ auto ssrcs = MakeUnique<SdpSsrcAttributeList>();
+ for (size_t i = 0; i < numSsrc; i++) {
+ RustSdpAttributeSsrc& ssrc = rustSsrcs[i];
+ std::string attribute = convertStringView(ssrc.attribute);
+ std::string value = convertStringView(ssrc.value);
+ if (value.length() == 0) {
+ ssrcs->PushEntry(ssrc.id, attribute);
+ } else {
+ ssrcs->PushEntry(ssrc.id, attribute + ":" + value);
+ }
+ }
+ SetAttribute(ssrcs.release());
+}
+
+void RsdparsaSdpAttributeList::LoadSsrcGroup(RustAttributeList* attributeList) {
+ size_t numSsrcGroups = sdp_get_ssrc_group_count(attributeList);
+ if (numSsrcGroups == 0) {
+ return;
+ }
+ auto rustSsrcGroups = MakeUnique<RustSdpAttributeSsrcGroup[]>(numSsrcGroups);
+ sdp_get_ssrc_groups(attributeList, numSsrcGroups, rustSsrcGroups.get());
+ auto ssrcGroups = MakeUnique<SdpSsrcGroupAttributeList>();
+ for (size_t i = 0; i < numSsrcGroups; i++) {
+ RustSdpAttributeSsrcGroup& ssrcGroup = rustSsrcGroups[i];
+ SdpSsrcGroupAttributeList::Semantics semantic;
+ switch (ssrcGroup.semantic) {
+ case RustSdpAttributeSsrcGroupSemantic ::kRustDup:
+ semantic = SdpSsrcGroupAttributeList::kDup;
+ break;
+ case RustSdpAttributeSsrcGroupSemantic ::kRustFec:
+ semantic = SdpSsrcGroupAttributeList::kFec;
+ break;
+ case RustSdpAttributeSsrcGroupSemantic ::kRustFecFr:
+ semantic = SdpSsrcGroupAttributeList::kFecFr;
+ break;
+ case RustSdpAttributeSsrcGroupSemantic ::kRustFid:
+ semantic = SdpSsrcGroupAttributeList::kFid;
+ break;
+ case RustSdpAttributeSsrcGroupSemantic ::kRustSim:
+ semantic = SdpSsrcGroupAttributeList::kSim;
+ break;
+ }
+ std::vector<uint32_t> ssrcs;
+ for (size_t i = 0; i < ssrc_vec_len(ssrcGroup.ssrcs); ++i) {
+ uint32_t ssrc;
+ ssrc_vec_get_id(ssrcGroup.ssrcs, i, &ssrc);
+ ssrcs.push_back(ssrc);
+ }
+ ssrcGroups->PushEntry(semantic, ssrcs);
+ }
+ SetAttribute(ssrcGroups.release());
+}
+
+struct FmtDefaults {
+ uint32_t minimumChannels = 0;
+};
+
+std::tuple<SdpRtpmapAttributeList::CodecType, FmtDefaults> strToCodecType(
+ const std::string& name) {
+ auto codec = SdpRtpmapAttributeList::kOtherCodec;
+ FmtDefaults defaults = {0}; // This is tracked to match SIPCC behavior only
+ if (!nsCRT::strcasecmp(name.c_str(), "opus")) {
+ codec = SdpRtpmapAttributeList::kOpus;
+ defaults = {0};
+ } else if (!nsCRT::strcasecmp(name.c_str(), "G722")) {
+ codec = SdpRtpmapAttributeList::kG722;
+ defaults = {1};
+ } else if (!nsCRT::strcasecmp(name.c_str(), "PCMU")) {
+ codec = SdpRtpmapAttributeList::kPCMU;
+ defaults = {1};
+ } else if (!nsCRT::strcasecmp(name.c_str(), "PCMA")) {
+ codec = SdpRtpmapAttributeList::kPCMA;
+ defaults = {1};
+ } else if (!nsCRT::strcasecmp(name.c_str(), "VP8")) {
+ codec = SdpRtpmapAttributeList::kVP8;
+ defaults = {0};
+ } else if (!nsCRT::strcasecmp(name.c_str(), "VP9")) {
+ codec = SdpRtpmapAttributeList::kVP9;
+ defaults = {0};
+ } else if (!nsCRT::strcasecmp(name.c_str(), "iLBC")) {
+ codec = SdpRtpmapAttributeList::kiLBC;
+ defaults = {1};
+ } else if (!nsCRT::strcasecmp(name.c_str(), "iSAC")) {
+ codec = SdpRtpmapAttributeList::kiSAC;
+ defaults = {1};
+ } else if (!nsCRT::strcasecmp(name.c_str(), "H264")) {
+ codec = SdpRtpmapAttributeList::kH264;
+ defaults = {0};
+ } else if (!nsCRT::strcasecmp(name.c_str(), "red")) {
+ codec = SdpRtpmapAttributeList::kRed;
+ defaults = {0};
+ } else if (!nsCRT::strcasecmp(name.c_str(), "ulpfec")) {
+ codec = SdpRtpmapAttributeList::kUlpfec;
+ defaults = {0};
+ } else if (!nsCRT::strcasecmp(name.c_str(), "telephone-event")) {
+ codec = SdpRtpmapAttributeList::kTelephoneEvent;
+ defaults = {1};
+ } else if (!nsCRT::strcasecmp(name.c_str(), "rtx")) {
+ codec = SdpRtpmapAttributeList::kRtx;
+ defaults = {0};
+ }
+ return std::make_tuple(codec, defaults);
+}
+
+void RsdparsaSdpAttributeList::LoadRtpmap(RustAttributeList* attributeList) {
+ size_t numRtpmap = sdp_get_rtpmap_count(attributeList);
+ if (numRtpmap == 0) {
+ return;
+ }
+ auto rustRtpmaps = MakeUnique<RustSdpAttributeRtpmap[]>(numRtpmap);
+ sdp_get_rtpmaps(attributeList, numRtpmap, rustRtpmaps.get());
+ auto rtpmapList = MakeUnique<SdpRtpmapAttributeList>();
+ for (size_t i = 0; i < numRtpmap; i++) {
+ RustSdpAttributeRtpmap& rtpmap = rustRtpmaps[i];
+ std::string payloadType = std::to_string(rtpmap.payloadType);
+ std::string name = convertStringView(rtpmap.codecName);
+ auto [codec, defaults] = strToCodecType(name);
+ uint32_t channels = rtpmap.channels;
+ if (channels == 0) {
+ channels = defaults.minimumChannels;
+ }
+ rtpmapList->PushEntry(payloadType, codec, name, rtpmap.frequency, channels);
+ }
+ SetAttribute(rtpmapList.release());
+}
+
+void RsdparsaSdpAttributeList::LoadFmtp(RustAttributeList* attributeList) {
+ size_t numFmtp = sdp_get_fmtp_count(attributeList);
+ if (numFmtp == 0) {
+ return;
+ }
+ auto rustFmtps = MakeUnique<RustSdpAttributeFmtp[]>(numFmtp);
+ size_t numValidFmtp = sdp_get_fmtp(attributeList, numFmtp, rustFmtps.get());
+ auto fmtpList = MakeUnique<SdpFmtpAttributeList>();
+ for (size_t i = 0; i < numValidFmtp; i++) {
+ const RustSdpAttributeFmtp& fmtp = rustFmtps[i];
+ uint8_t payloadType = fmtp.payloadType;
+ std::string codecName = convertStringView(fmtp.codecName);
+ const RustSdpAttributeFmtpParameters& rustFmtpParameters = fmtp.parameters;
+
+ UniquePtr<SdpFmtpAttributeList::Parameters> fmtpParameters;
+
+ // use the upper case version of the codec name
+ std::transform(codecName.begin(), codecName.end(), codecName.begin(),
+ ::toupper);
+
+ if (codecName == "H264") {
+ SdpFmtpAttributeList::H264Parameters h264Parameters;
+
+ h264Parameters.packetization_mode = rustFmtpParameters.packetization_mode;
+ h264Parameters.level_asymmetry_allowed =
+ rustFmtpParameters.level_asymmetry_allowed;
+ h264Parameters.profile_level_id = rustFmtpParameters.profile_level_id;
+ h264Parameters.max_mbps = rustFmtpParameters.max_mbps;
+ h264Parameters.max_fs = rustFmtpParameters.max_fs;
+ h264Parameters.max_cpb = rustFmtpParameters.max_cpb;
+ h264Parameters.max_dpb = rustFmtpParameters.max_dpb;
+ h264Parameters.max_br = rustFmtpParameters.max_br;
+
+ // TODO(bug 1466859): Support sprop-parameter-sets
+
+ fmtpParameters.reset(
+ new SdpFmtpAttributeList::H264Parameters(std::move(h264Parameters)));
+ } else if (codecName == "OPUS") {
+ SdpFmtpAttributeList::OpusParameters opusParameters;
+
+ opusParameters.maxplaybackrate = rustFmtpParameters.maxplaybackrate;
+ opusParameters.maxAverageBitrate = rustFmtpParameters.maxaveragebitrate;
+ opusParameters.useDTX = rustFmtpParameters.usedtx;
+ opusParameters.stereo = rustFmtpParameters.stereo;
+ opusParameters.useInBandFec = rustFmtpParameters.useinbandfec;
+ opusParameters.frameSizeMs = rustFmtpParameters.ptime;
+ opusParameters.minFrameSizeMs = rustFmtpParameters.minptime;
+ opusParameters.maxFrameSizeMs = rustFmtpParameters.maxptime;
+ opusParameters.useCbr = rustFmtpParameters.cbr;
+
+ fmtpParameters.reset(
+ new SdpFmtpAttributeList::OpusParameters(std::move(opusParameters)));
+ } else if ((codecName == "VP8") || (codecName == "VP9")) {
+ SdpFmtpAttributeList::VP8Parameters vp8Parameters(
+ codecName == "VP8" ? SdpRtpmapAttributeList::kVP8
+ : SdpRtpmapAttributeList::kVP9);
+
+ vp8Parameters.max_fs = rustFmtpParameters.max_fs;
+ vp8Parameters.max_fr = rustFmtpParameters.max_fr;
+
+ fmtpParameters.reset(
+ new SdpFmtpAttributeList::VP8Parameters(std::move(vp8Parameters)));
+ } else if (codecName == "TELEPHONE-EVENT") {
+ SdpFmtpAttributeList::TelephoneEventParameters telephoneEventParameters;
+
+ telephoneEventParameters.dtmfTones =
+ convertStringView(rustFmtpParameters.dtmf_tones);
+
+ fmtpParameters.reset(new SdpFmtpAttributeList::TelephoneEventParameters(
+ std::move(telephoneEventParameters)));
+ } else if (codecName == "RED") {
+ SdpFmtpAttributeList::RedParameters redParameters;
+
+ redParameters.encodings = convertU8Vec(rustFmtpParameters.encodings);
+
+ fmtpParameters.reset(
+ new SdpFmtpAttributeList::RedParameters(std::move(redParameters)));
+ } else if (codecName == "RTX") {
+ SdpFmtpAttributeList::RtxParameters rtxParameters;
+
+ rtxParameters.apt = rustFmtpParameters.rtx.apt;
+ if (rustFmtpParameters.rtx.has_rtx_time) {
+ rtxParameters.rtx_time = Some(rustFmtpParameters.rtx.rtx_time);
+ }
+
+ fmtpParameters.reset(
+ new SdpFmtpAttributeList::RtxParameters(rtxParameters));
+ } else {
+ // The parameter set is unknown so skip it
+ continue;
+ }
+ fmtpList->PushEntry(std::to_string(payloadType), *fmtpParameters);
+ }
+ SetAttribute(fmtpList.release());
+}
+
+void RsdparsaSdpAttributeList::LoadPtime(RustAttributeList* attributeList) {
+ int64_t ptime = sdp_get_ptime(attributeList);
+ if (ptime >= 0) {
+ SetAttribute(new SdpNumberAttribute(SdpAttribute::kPtimeAttribute,
+ static_cast<uint32_t>(ptime)));
+ }
+}
+
+void RsdparsaSdpAttributeList::LoadFlags(RustAttributeList* attributeList) {
+ RustSdpAttributeFlags flags = sdp_get_attribute_flags(attributeList);
+ if (flags.iceLite) {
+ SetAttribute(new SdpFlagAttribute(SdpAttribute::kIceLiteAttribute));
+ }
+ if (flags.rtcpMux) {
+ SetAttribute(new SdpFlagAttribute(SdpAttribute::kRtcpMuxAttribute));
+ }
+ if (flags.rtcpRsize) {
+ SetAttribute(new SdpFlagAttribute(SdpAttribute::kRtcpRsizeAttribute));
+ }
+ if (flags.bundleOnly) {
+ SetAttribute(new SdpFlagAttribute(SdpAttribute::kBundleOnlyAttribute));
+ }
+ if (flags.endOfCandidates) {
+ SetAttribute(new SdpFlagAttribute(SdpAttribute::kEndOfCandidatesAttribute));
+ }
+}
+
+void RsdparsaSdpAttributeList::LoadMaxMessageSize(
+ RustAttributeList* attributeList) {
+ int64_t max_msg_size = sdp_get_max_msg_size(attributeList);
+ if (max_msg_size >= 0) {
+ SetAttribute(new SdpNumberAttribute(SdpAttribute::kMaxMessageSizeAttribute,
+ static_cast<uint32_t>(max_msg_size)));
+ }
+}
+
+void RsdparsaSdpAttributeList::LoadMid(RustAttributeList* attributeList) {
+ StringView rustMid;
+ if (NS_SUCCEEDED(sdp_get_mid(attributeList, &rustMid))) {
+ std::string mid = convertStringView(rustMid);
+ SetAttribute(new SdpStringAttribute(SdpAttribute::kMidAttribute, mid));
+ }
+}
+
+void RsdparsaSdpAttributeList::LoadMsid(RustAttributeList* attributeList) {
+ size_t numMsid = sdp_get_msid_count(attributeList);
+ if (numMsid == 0) {
+ return;
+ }
+ auto rustMsid = MakeUnique<RustSdpAttributeMsid[]>(numMsid);
+ sdp_get_msids(attributeList, numMsid, rustMsid.get());
+ auto msids = MakeUnique<SdpMsidAttributeList>();
+ for (size_t i = 0; i < numMsid; i++) {
+ RustSdpAttributeMsid& msid = rustMsid[i];
+ std::string id = convertStringView(msid.id);
+ std::string appdata = convertStringView(msid.appdata);
+ msids->PushEntry(id, appdata);
+ }
+ SetAttribute(msids.release());
+}
+
+void RsdparsaSdpAttributeList::LoadMsidSemantics(
+ RustAttributeList* attributeList) {
+ size_t numMsidSemantic = sdp_get_msid_semantic_count(attributeList);
+ if (numMsidSemantic == 0) {
+ return;
+ }
+ auto rustMsidSemantics =
+ MakeUnique<RustSdpAttributeMsidSemantic[]>(numMsidSemantic);
+ sdp_get_msid_semantics(attributeList, numMsidSemantic,
+ rustMsidSemantics.get());
+ auto msidSemantics = MakeUnique<SdpMsidSemanticAttributeList>();
+ for (size_t i = 0; i < numMsidSemantic; i++) {
+ RustSdpAttributeMsidSemantic& rustMsidSemantic = rustMsidSemantics[i];
+ std::string semantic = convertStringView(rustMsidSemantic.semantic);
+ std::vector<std::string> msids = convertStringVec(rustMsidSemantic.msids);
+ msidSemantics->PushEntry(semantic, msids);
+ }
+ SetAttribute(msidSemantics.release());
+}
+
+void RsdparsaSdpAttributeList::LoadGroup(RustAttributeList* attributeList) {
+ size_t numGroup = sdp_get_group_count(attributeList);
+ if (numGroup == 0) {
+ return;
+ }
+ auto rustGroups = MakeUnique<RustSdpAttributeGroup[]>(numGroup);
+ sdp_get_groups(attributeList, numGroup, rustGroups.get());
+ auto groups = MakeUnique<SdpGroupAttributeList>();
+ for (size_t i = 0; i < numGroup; i++) {
+ RustSdpAttributeGroup& group = rustGroups[i];
+ SdpGroupAttributeList::Semantics semantic;
+ switch (group.semantic) {
+ case RustSdpAttributeGroupSemantic ::kRustLipSynchronization:
+ semantic = SdpGroupAttributeList::kLs;
+ break;
+ case RustSdpAttributeGroupSemantic ::kRustFlowIdentification:
+ semantic = SdpGroupAttributeList::kFid;
+ break;
+ case RustSdpAttributeGroupSemantic ::kRustSingleReservationFlow:
+ semantic = SdpGroupAttributeList::kSrf;
+ break;
+ case RustSdpAttributeGroupSemantic ::kRustAlternateNetworkAddressType:
+ semantic = SdpGroupAttributeList::kAnat;
+ break;
+ case RustSdpAttributeGroupSemantic ::kRustForwardErrorCorrection:
+ semantic = SdpGroupAttributeList::kFec;
+ break;
+ case RustSdpAttributeGroupSemantic ::kRustDecodingDependency:
+ semantic = SdpGroupAttributeList::kDdp;
+ break;
+ case RustSdpAttributeGroupSemantic ::kRustBundle:
+ semantic = SdpGroupAttributeList::kBundle;
+ break;
+ }
+ std::vector<std::string> tags = convertStringVec(group.tags);
+ groups->PushEntry(semantic, tags);
+ }
+ SetAttribute(groups.release());
+}
+
+void RsdparsaSdpAttributeList::LoadRtcp(RustAttributeList* attributeList) {
+ RustSdpAttributeRtcp rtcp;
+ if (NS_SUCCEEDED(sdp_get_rtcp(attributeList, &rtcp))) {
+ if (rtcp.has_address) {
+ auto address = convertExplicitlyTypedAddress(&rtcp.unicastAddr);
+ SetAttribute(new SdpRtcpAttribute(rtcp.port, sdp::kInternet,
+ address.first, address.second));
+ } else {
+ SetAttribute(new SdpRtcpAttribute(rtcp.port));
+ }
+ }
+}
+
+void RsdparsaSdpAttributeList::LoadRtcpFb(RustAttributeList* attributeList) {
+ auto rtcpfbsCount = sdp_get_rtcpfb_count(attributeList);
+ if (!rtcpfbsCount) {
+ return;
+ }
+
+ auto rustRtcpfbs = MakeUnique<RustSdpAttributeRtcpFb[]>(rtcpfbsCount);
+ sdp_get_rtcpfbs(attributeList, rtcpfbsCount, rustRtcpfbs.get());
+
+ auto rtcpfbList = MakeUnique<SdpRtcpFbAttributeList>();
+ for (size_t i = 0; i < rtcpfbsCount; i++) {
+ RustSdpAttributeRtcpFb& rtcpfb = rustRtcpfbs[i];
+ uint32_t payloadTypeU32 = rtcpfb.payloadType;
+
+ std::stringstream ss;
+ if (payloadTypeU32 == std::numeric_limits<uint32_t>::max()) {
+ ss << "*";
+ } else {
+ ss << payloadTypeU32;
+ }
+
+ uint32_t feedbackType = rtcpfb.feedbackType;
+ std::string parameter = convertStringView(rtcpfb.parameter);
+ std::string extra = convertStringView(rtcpfb.extra);
+
+ rtcpfbList->PushEntry(
+ ss.str(), static_cast<SdpRtcpFbAttributeList::Type>(feedbackType),
+ parameter, extra);
+ }
+
+ SetAttribute(rtcpfbList.release());
+}
+
+SdpSimulcastAttribute::Versions LoadSimulcastVersions(
+ const RustSdpAttributeSimulcastVersionVec* rustVersionList) {
+ size_t rustVersionCount = sdp_simulcast_get_version_count(rustVersionList);
+ auto rustVersionArray =
+ MakeUnique<RustSdpAttributeSimulcastVersion[]>(rustVersionCount);
+ sdp_simulcast_get_versions(rustVersionList, rustVersionCount,
+ rustVersionArray.get());
+
+ SdpSimulcastAttribute::Versions versions;
+
+ for (size_t i = 0; i < rustVersionCount; i++) {
+ const RustSdpAttributeSimulcastVersion& rustVersion = rustVersionArray[i];
+ size_t rustIdCount = sdp_simulcast_get_ids_count(rustVersion.ids);
+ if (!rustIdCount) {
+ continue;
+ }
+
+ SdpSimulcastAttribute::Version version;
+ auto rustIdArray = MakeUnique<RustSdpAttributeSimulcastId[]>(rustIdCount);
+ sdp_simulcast_get_ids(rustVersion.ids, rustIdCount, rustIdArray.get());
+
+ for (size_t j = 0; j < rustIdCount; j++) {
+ const RustSdpAttributeSimulcastId& rustId = rustIdArray[j];
+ std::string id = convertStringView(rustId.id);
+ // TODO: Bug 1225877. Added support for 'paused'-state
+ version.choices.push_back(
+ SdpSimulcastAttribute::Encoding(id, rustId.paused));
+ }
+
+ versions.push_back(version);
+ }
+
+ return versions;
+}
+
+void RsdparsaSdpAttributeList::LoadSimulcast(RustAttributeList* attributeList) {
+ RustSdpAttributeSimulcast rustSimulcast;
+ if (NS_SUCCEEDED(sdp_get_simulcast(attributeList, &rustSimulcast))) {
+ auto simulcast = MakeUnique<SdpSimulcastAttribute>();
+
+ simulcast->sendVersions = LoadSimulcastVersions(rustSimulcast.send);
+ simulcast->recvVersions = LoadSimulcastVersions(rustSimulcast.recv);
+
+ SetAttribute(simulcast.release());
+ }
+}
+
+SdpImageattrAttributeList::XYRange LoadImageattrXYRange(
+ const RustSdpAttributeImageAttrXYRange& rustXYRange) {
+ SdpImageattrAttributeList::XYRange xyRange;
+
+ if (!rustXYRange.discrete_values) {
+ xyRange.min = rustXYRange.min;
+ xyRange.max = rustXYRange.max;
+ xyRange.step = rustXYRange.step;
+
+ } else {
+ xyRange.discreteValues = convertU32Vec(rustXYRange.discrete_values);
+ }
+
+ return xyRange;
+}
+
+std::vector<SdpImageattrAttributeList::Set> LoadImageattrSets(
+ const RustSdpAttributeImageAttrSetVec* rustSets) {
+ std::vector<SdpImageattrAttributeList::Set> sets;
+
+ size_t rustSetCount = sdp_imageattr_get_set_count(rustSets);
+ if (!rustSetCount) {
+ return sets;
+ }
+
+ auto rustSetArray = MakeUnique<RustSdpAttributeImageAttrSet[]>(rustSetCount);
+ sdp_imageattr_get_sets(rustSets, rustSetCount, rustSetArray.get());
+
+ for (size_t i = 0; i < rustSetCount; i++) {
+ const RustSdpAttributeImageAttrSet& rustSet = rustSetArray[i];
+ SdpImageattrAttributeList::Set set;
+
+ set.xRange = LoadImageattrXYRange(rustSet.x);
+ set.yRange = LoadImageattrXYRange(rustSet.y);
+
+ if (rustSet.has_sar) {
+ if (!rustSet.sar.discrete_values) {
+ set.sRange.min = rustSet.sar.min;
+ set.sRange.max = rustSet.sar.max;
+ } else {
+ set.sRange.discreteValues = convertF32Vec(rustSet.sar.discrete_values);
+ }
+ }
+
+ if (rustSet.has_par) {
+ set.pRange.min = rustSet.par.min;
+ set.pRange.max = rustSet.par.max;
+ }
+
+ set.qValue = rustSet.q;
+
+ sets.push_back(set);
+ }
+
+ return sets;
+}
+
+void RsdparsaSdpAttributeList::LoadImageattr(RustAttributeList* attributeList) {
+ size_t numImageattrs = sdp_get_imageattr_count(attributeList);
+ if (numImageattrs == 0) {
+ return;
+ }
+ auto rustImageattrs = MakeUnique<RustSdpAttributeImageAttr[]>(numImageattrs);
+ sdp_get_imageattrs(attributeList, numImageattrs, rustImageattrs.get());
+ auto imageattrList = MakeUnique<SdpImageattrAttributeList>();
+ for (size_t i = 0; i < numImageattrs; i++) {
+ const RustSdpAttributeImageAttr& rustImageAttr = rustImageattrs[i];
+
+ SdpImageattrAttributeList::Imageattr imageAttr;
+
+ if (rustImageAttr.payloadType != std::numeric_limits<uint32_t>::max()) {
+ imageAttr.pt = Some(rustImageAttr.payloadType);
+ }
+
+ if (rustImageAttr.send.sets) {
+ imageAttr.sendSets = LoadImageattrSets(rustImageAttr.send.sets);
+ } else {
+ imageAttr.sendAll = true;
+ }
+
+ if (rustImageAttr.recv.sets) {
+ imageAttr.recvSets = LoadImageattrSets(rustImageAttr.recv.sets);
+ } else {
+ imageAttr.recvAll = true;
+ }
+
+ imageattrList->mImageattrs.push_back(imageAttr);
+ }
+ SetAttribute(imageattrList.release());
+}
+
+void RsdparsaSdpAttributeList::LoadSctpmaps(RustAttributeList* attributeList) {
+ size_t numSctpmaps = sdp_get_sctpmap_count(attributeList);
+ if (numSctpmaps == 0) {
+ return;
+ }
+ auto rustSctpmaps = MakeUnique<RustSdpAttributeSctpmap[]>(numSctpmaps);
+ sdp_get_sctpmaps(attributeList, numSctpmaps, rustSctpmaps.get());
+ auto sctpmapList = MakeUnique<SdpSctpmapAttributeList>();
+ for (size_t i = 0; i < numSctpmaps; i++) {
+ RustSdpAttributeSctpmap& sctpmap = rustSctpmaps[i];
+ sctpmapList->PushEntry(std::to_string(sctpmap.port), "webrtc-datachannel",
+ sctpmap.channels);
+ }
+ SetAttribute(sctpmapList.release());
+}
+
+void RsdparsaSdpAttributeList::LoadDirection(RustAttributeList* attributeList) {
+ SdpDirectionAttribute::Direction dir;
+ RustDirection rustDir = sdp_get_direction(attributeList);
+ switch (rustDir) {
+ case RustDirection::kRustRecvonly:
+ dir = SdpDirectionAttribute::kRecvonly;
+ break;
+ case RustDirection::kRustSendonly:
+ dir = SdpDirectionAttribute::kSendonly;
+ break;
+ case RustDirection::kRustSendrecv:
+ dir = SdpDirectionAttribute::kSendrecv;
+ break;
+ case RustDirection::kRustInactive:
+ dir = SdpDirectionAttribute::kInactive;
+ break;
+ }
+ SetAttribute(new SdpDirectionAttribute(dir));
+}
+
+void RsdparsaSdpAttributeList::LoadRemoteCandidates(
+ RustAttributeList* attributeList) {
+ size_t nC = sdp_get_remote_candidate_count(attributeList);
+ if (nC == 0) {
+ return;
+ }
+ auto rustCandidates = MakeUnique<RustSdpAttributeRemoteCandidate[]>(nC);
+ sdp_get_remote_candidates(attributeList, nC, rustCandidates.get());
+ std::vector<SdpRemoteCandidatesAttribute::Candidate> candidates;
+ for (size_t i = 0; i < nC; i++) {
+ RustSdpAttributeRemoteCandidate& rustCandidate = rustCandidates[i];
+ SdpRemoteCandidatesAttribute::Candidate candidate;
+ candidate.port = rustCandidate.port;
+ candidate.id = std::to_string(rustCandidate.component);
+ candidate.address = convertAddress(&rustCandidate.address);
+ candidates.push_back(candidate);
+ }
+ SdpRemoteCandidatesAttribute* candidatesList;
+ candidatesList = new SdpRemoteCandidatesAttribute(candidates);
+ SetAttribute(candidatesList);
+}
+
+void RsdparsaSdpAttributeList::LoadRids(RustAttributeList* attributeList) {
+ size_t numRids = sdp_get_rid_count(attributeList);
+ if (numRids == 0) {
+ return;
+ }
+
+ auto rustRids = MakeUnique<RustSdpAttributeRid[]>(numRids);
+ sdp_get_rids(attributeList, numRids, rustRids.get());
+
+ auto ridList = MakeUnique<SdpRidAttributeList>();
+ for (size_t i = 0; i < numRids; i++) {
+ const RustSdpAttributeRid& rid = rustRids[i];
+
+ std::string id = convertStringView(rid.id);
+ auto direction = static_cast<sdp::Direction>(rid.direction);
+ std::vector<uint16_t> formats = convertU16Vec(rid.formats);
+
+ EncodingConstraints parameters;
+ parameters.maxWidth = rid.params.max_width;
+ parameters.maxHeight = rid.params.max_height;
+ // Right now, we treat max-fps=0 and the absence of max-fps as no limit.
+ // We will eventually want to treat max-fps=0 as 0 frames per second, and
+ // the absence of max-fps as no limit (bug 1762632).
+ if (rid.params.max_fps) {
+ parameters.maxFps = Some(rid.params.max_fps);
+ }
+ parameters.maxFs = rid.params.max_fs;
+ parameters.maxBr = rid.params.max_br;
+ parameters.maxPps = rid.params.max_pps;
+
+ std::vector<std::string> depends = convertStringVec(rid.depends);
+
+ ridList->PushEntry(id, direction, formats, parameters, depends);
+ }
+
+ SetAttribute(ridList.release());
+}
+
+void RsdparsaSdpAttributeList::LoadSctpPort(RustAttributeList* attributeList) {
+ int64_t port = sdp_get_sctp_port(attributeList);
+ if (port >= 0) {
+ SetAttribute(new SdpNumberAttribute(SdpAttribute::kSctpPortAttribute,
+ static_cast<uint32_t>(port)));
+ }
+}
+
+void RsdparsaSdpAttributeList::LoadExtmap(RustAttributeList* attributeList) {
+ size_t numExtmap = sdp_get_extmap_count(attributeList);
+ if (numExtmap == 0) {
+ return;
+ }
+ auto rustExtmaps = MakeUnique<RustSdpAttributeExtmap[]>(numExtmap);
+ sdp_get_extmaps(attributeList, numExtmap, rustExtmaps.get());
+ auto extmaps = MakeUnique<SdpExtmapAttributeList>();
+ for (size_t i = 0; i < numExtmap; i++) {
+ RustSdpAttributeExtmap& rustExtmap = rustExtmaps[i];
+ std::string name = convertStringView(rustExtmap.url);
+ SdpDirectionAttribute::Direction direction;
+ bool directionSpecified = rustExtmap.direction_specified;
+ switch (rustExtmap.direction) {
+ case RustDirection::kRustRecvonly:
+ direction = SdpDirectionAttribute::kRecvonly;
+ break;
+ case RustDirection::kRustSendonly:
+ direction = SdpDirectionAttribute::kSendonly;
+ break;
+ case RustDirection::kRustSendrecv:
+ direction = SdpDirectionAttribute::kSendrecv;
+ break;
+ case RustDirection::kRustInactive:
+ direction = SdpDirectionAttribute::kInactive;
+ break;
+ }
+ std::string extensionAttributes;
+ extensionAttributes = convertStringView(rustExtmap.extensionAttributes);
+ extmaps->PushEntry((uint16_t)rustExtmap.id, direction, directionSpecified,
+ name, extensionAttributes);
+ }
+ SetAttribute(extmaps.release());
+}
+
+void RsdparsaSdpAttributeList::LoadMaxPtime(RustAttributeList* attributeList) {
+ uint64_t maxPtime = 0;
+ nsresult nr = sdp_get_maxptime(attributeList, &maxPtime);
+ if (NS_SUCCEEDED(nr)) {
+ SetAttribute(
+ new SdpNumberAttribute(SdpAttribute::kMaxptimeAttribute, maxPtime));
+ }
+}
+
+void RsdparsaSdpAttributeList::LoadCandidate(RustAttributeList* attributeList) {
+ size_t candidatesCount = sdp_get_candidate_count(attributeList);
+ if (!candidatesCount) {
+ return;
+ }
+
+ StringVec* rustCandidatesStrings;
+ sdp_get_candidates(attributeList, candidatesCount, &rustCandidatesStrings);
+
+ std::vector<std::string> candidatesStrings =
+ convertStringVec(rustCandidatesStrings);
+ free_boxed_string_vec(rustCandidatesStrings);
+
+ auto candidates =
+ MakeUnique<SdpMultiStringAttribute>(SdpAttribute::kCandidateAttribute);
+ candidates->mValues = candidatesStrings;
+
+ SetAttribute(candidates.release());
+}
+
+bool RsdparsaSdpAttributeList::IsAllowedHere(SdpAttribute::AttributeType type) {
+ if (AtSessionLevel() && !SdpAttribute::IsAllowedAtSessionLevel(type)) {
+ return false;
+ }
+
+ if (!AtSessionLevel() && !SdpAttribute::IsAllowedAtMediaLevel(type)) {
+ return false;
+ }
+
+ return true;
+}
+
+void RsdparsaSdpAttributeList::Serialize(std::ostream& os) const {
+ for (size_t i = 0; i < kNumAttributeTypes; ++i) {
+ if (mAttributes[i]) {
+ os << *mAttributes[i];
+ }
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/sdp/RsdparsaSdpAttributeList.h b/dom/media/webrtc/sdp/RsdparsaSdpAttributeList.h
new file mode 100644
index 0000000000..812f15e112
--- /dev/null
+++ b/dom/media/webrtc/sdp/RsdparsaSdpAttributeList.h
@@ -0,0 +1,157 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _RSDPARSA_SDP_ATTRIBUTE_LIST_H_
+#define _RSDPARSA_SDP_ATTRIBUTE_LIST_H_
+
+#include "sdp/RsdparsaSdpGlue.h"
+#include "sdp/RsdparsaSdpInc.h"
+#include "sdp/SdpAttributeList.h"
+
+namespace mozilla {
+
+class RsdparsaSdp;
+class RsdparsaSdpMediaSection;
+class SdpParser;
+
+class RsdparsaSdpAttributeList : public SdpAttributeList {
+ friend class RsdparsaSdpMediaSection;
+ friend class RsdparsaSdp;
+
+ public:
+ // Make sure we don't hide the default arg thunks
+ using SdpAttributeList::GetAttribute;
+ using SdpAttributeList::HasAttribute;
+
+ bool HasAttribute(AttributeType type, bool sessionFallback) const override;
+ const SdpAttribute* GetAttribute(AttributeType type,
+ bool sessionFallback) const override;
+ void SetAttribute(SdpAttribute* attr) override;
+ void RemoveAttribute(AttributeType type) override;
+ void Clear() override;
+ uint32_t Count() const override;
+
+ const SdpConnectionAttribute& GetConnection() const override;
+ const SdpFingerprintAttributeList& GetFingerprint() const override;
+ const SdpGroupAttributeList& GetGroup() const override;
+ const SdpOptionsAttribute& GetIceOptions() const override;
+ const SdpRtcpAttribute& GetRtcp() const override;
+ const SdpRemoteCandidatesAttribute& GetRemoteCandidates() const override;
+ const SdpSetupAttribute& GetSetup() const override;
+ const SdpSsrcAttributeList& GetSsrc() const override;
+ const SdpSsrcGroupAttributeList& GetSsrcGroup() const override;
+ const SdpDtlsMessageAttribute& GetDtlsMessage() const override;
+
+ // These attributes can appear multiple times, so the returned
+ // classes actually represent a collection of values.
+ const std::vector<std::string>& GetCandidate() const override;
+ const SdpExtmapAttributeList& GetExtmap() const override;
+ const SdpFmtpAttributeList& GetFmtp() const override;
+ const SdpImageattrAttributeList& GetImageattr() const override;
+ const SdpSimulcastAttribute& GetSimulcast() const override;
+ const SdpMsidAttributeList& GetMsid() const override;
+ const SdpMsidSemanticAttributeList& GetMsidSemantic() const override;
+ const SdpRidAttributeList& GetRid() const override;
+ const SdpRtcpFbAttributeList& GetRtcpFb() const override;
+ const SdpRtpmapAttributeList& GetRtpmap() const override;
+ const SdpSctpmapAttributeList& GetSctpmap() const override;
+
+ // These attributes are effectively simple types, so we'll make life
+ // easy by just returning their value.
+ uint32_t GetSctpPort() const override;
+ uint32_t GetMaxMessageSize() const override;
+ const std::string& GetIcePwd() const override;
+ const std::string& GetIceUfrag() const override;
+ const std::string& GetIdentity() const override;
+ const std::string& GetLabel() const override;
+ unsigned int GetMaxptime() const override;
+ const std::string& GetMid() const override;
+ unsigned int GetPtime() const override;
+
+ SdpDirectionAttribute::Direction GetDirection() const override;
+
+ void Serialize(std::ostream&) const override;
+
+ virtual ~RsdparsaSdpAttributeList();
+
+ private:
+ explicit RsdparsaSdpAttributeList(RsdparsaSessionHandle session)
+ : mSession(std::move(session)),
+ mSessionAttributes(nullptr),
+ mIsVideo(false),
+ mAttributes() {
+ RustAttributeList* attributes = get_sdp_session_attributes(mSession.get());
+ LoadAll(attributes);
+ }
+
+ RsdparsaSdpAttributeList(RsdparsaSessionHandle session,
+ const RustMediaSection* const msection,
+ const RsdparsaSdpAttributeList* sessionAttributes)
+ : mSession(std::move(session)),
+ mSessionAttributes(sessionAttributes),
+ mAttributes() {
+ mIsVideo =
+ sdp_rust_get_media_type(msection) == RustSdpMediaValue::kRustVideo;
+ RustAttributeList* attributes = sdp_get_media_attribute_list(msection);
+ LoadAll(attributes);
+ }
+
+ static const std::string kEmptyString;
+ static const size_t kNumAttributeTypes = SdpAttribute::kLastAttribute + 1;
+
+ const RsdparsaSessionHandle mSession;
+ const RsdparsaSdpAttributeList* mSessionAttributes;
+ bool mIsVideo;
+
+ bool AtSessionLevel() const { return !mSessionAttributes; }
+
+ bool IsAllowedHere(SdpAttribute::AttributeType type);
+ void LoadAll(RustAttributeList* attributeList);
+ void LoadAttribute(RustAttributeList* attributeList, AttributeType type);
+ void LoadIceUfrag(RustAttributeList* attributeList);
+ void LoadIcePwd(RustAttributeList* attributeList);
+ void LoadIdentity(RustAttributeList* attributeList);
+ void LoadIceOptions(RustAttributeList* attributeList);
+ void LoadFingerprint(RustAttributeList* attributeList);
+ void LoadDtlsMessage(RustAttributeList* attributeList);
+ void LoadSetup(RustAttributeList* attributeList);
+ void LoadSsrc(RustAttributeList* attributeList);
+ void LoadSsrcGroup(RustAttributeList* attributeList);
+ void LoadRtpmap(RustAttributeList* attributeList);
+ void LoadFmtp(RustAttributeList* attributeList);
+ void LoadPtime(RustAttributeList* attributeList);
+ void LoadFlags(RustAttributeList* attributeList);
+ void LoadMaxMessageSize(RustAttributeList* attributeList);
+ void LoadMid(RustAttributeList* attributeList);
+ void LoadMsid(RustAttributeList* attributeList);
+ void LoadMsidSemantics(RustAttributeList* attributeList);
+ void LoadGroup(RustAttributeList* attributeList);
+ void LoadRtcp(RustAttributeList* attributeList);
+ void LoadRtcpFb(RustAttributeList* attributeList);
+ void LoadSctpPort(RustAttributeList* attributeList);
+ void LoadSimulcast(RustAttributeList* attributeList);
+ void LoadImageattr(RustAttributeList* attributeList);
+ void LoadSctpmaps(RustAttributeList* attributeList);
+ void LoadDirection(RustAttributeList* attributeList);
+ void LoadRemoteCandidates(RustAttributeList* attributeList);
+ void LoadRids(RustAttributeList* attributeList);
+ void LoadExtmap(RustAttributeList* attributeList);
+ void LoadMaxPtime(RustAttributeList* attributeList);
+ void LoadCandidate(RustAttributeList* attributeList);
+
+ void WarnAboutMisplacedAttribute(SdpAttribute::AttributeType type,
+ uint32_t lineNumber, SdpParser& errorHolder);
+
+ SdpAttribute* mAttributes[kNumAttributeTypes];
+
+ RsdparsaSdpAttributeList(const RsdparsaSdpAttributeList& orig) = delete;
+ RsdparsaSdpAttributeList& operator=(const RsdparsaSdpAttributeList& rhs) =
+ delete;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/sdp/RsdparsaSdpGlue.cpp b/dom/media/webrtc/sdp/RsdparsaSdpGlue.cpp
new file mode 100644
index 0000000000..01a1a1d817
--- /dev/null
+++ b/dom/media/webrtc/sdp/RsdparsaSdpGlue.cpp
@@ -0,0 +1,106 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include <string>
+#include <cstdint>
+
+#include "sdp/RsdparsaSdpInc.h"
+#include "sdp/RsdparsaSdpGlue.h"
+namespace mozilla {
+
+std::string convertStringView(StringView str) {
+ if (nullptr == str.buf) {
+ return std::string();
+ } else {
+ return std::string(str.buf, str.len);
+ }
+}
+
+std::vector<std::string> convertStringVec(StringVec* vec) {
+ std::vector<std::string> ret;
+ size_t len = string_vec_len(vec);
+ for (size_t i = 0; i < len; i++) {
+ StringView view;
+ string_vec_get_view(vec, i, &view);
+ ret.push_back(convertStringView(view));
+ }
+ return ret;
+}
+
+sdp::AddrType convertAddressType(RustSdpAddressType addrType) {
+ switch (addrType) {
+ case RustSdpAddressType::kRustAddrIp4:
+ return sdp::kIPv4;
+ case RustSdpAddressType::kRustAddrIp6:
+ return sdp::kIPv6;
+ }
+
+ MOZ_CRASH("unknown address type");
+}
+
+std::string convertAddress(RustAddress* address) {
+ return address->isFqdn ? convertStringView(address->fqdn)
+ : std::string(address->ipAddress);
+}
+
+std::pair<sdp::AddrType, std::string> convertExplicitlyTypedAddress(
+ RustExplicitlyTypedAddress* address) {
+ return std::make_pair(convertAddressType(address->addressType),
+ convertAddress(&address->address));
+}
+
+std::vector<uint8_t> convertU8Vec(U8Vec* vec) {
+ std::vector<std::uint8_t> ret;
+
+ size_t len = u8_vec_len(vec);
+ for (size_t i = 0; i < len; i++) {
+ uint8_t byte;
+ u8_vec_get(vec, i, &byte);
+ ret.push_back(byte);
+ }
+
+ return ret;
+}
+
+std::vector<uint16_t> convertU16Vec(U16Vec* vec) {
+ std::vector<std::uint16_t> ret;
+
+ size_t len = u16_vec_len(vec);
+ for (size_t i = 0; i < len; i++) {
+ uint16_t word;
+ u16_vec_get(vec, i, &word);
+ ret.push_back(word);
+ }
+
+ return ret;
+}
+
+std::vector<uint32_t> convertU32Vec(U32Vec* vec) {
+ std::vector<std::uint32_t> ret;
+
+ size_t len = u32_vec_len(vec);
+ for (size_t i = 0; i < len; i++) {
+ uint32_t num;
+ u32_vec_get(vec, i, &num);
+ ret.push_back(num);
+ }
+
+ return ret;
+}
+
+std::vector<float> convertF32Vec(F32Vec* vec) {
+ std::vector<float> ret;
+
+ size_t len = f32_vec_len(vec);
+ for (size_t i = 0; i < len; i++) {
+ float flt;
+ f32_vec_get(vec, i, &flt);
+ ret.push_back(flt);
+ }
+
+ return ret;
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/sdp/RsdparsaSdpGlue.h b/dom/media/webrtc/sdp/RsdparsaSdpGlue.h
new file mode 100644
index 0000000000..8ed275be75
--- /dev/null
+++ b/dom/media/webrtc/sdp/RsdparsaSdpGlue.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef _RUSTSDPGLUE_H_
+#define _RUSTSDPGLUE_H_
+
+#include <string>
+#include <vector>
+#include <utility>
+#include "SdpEnum.h"
+#include "mozilla/UniquePtr.h"
+#include "sdp/RsdparsaSdpInc.h"
+
+namespace mozilla {
+
+struct FreeRustSdpSession {
+ void operator()(RustSdpSession* aSess) { sdp_free_session(aSess); }
+};
+
+typedef UniquePtr<RustSdpSession, FreeRustSdpSession> RsdparsaSessionHandle;
+
+std::string convertStringView(StringView str);
+std::vector<std::string> convertStringVec(StringVec* vec);
+std::string convertAddress(RustAddress* address);
+std::pair<sdp::AddrType, std::string> convertExplicitlyTypedAddress(
+ RustExplicitlyTypedAddress* addr);
+std::vector<uint8_t> convertU8Vec(U8Vec* vec);
+std::vector<uint16_t> convertU16Vec(U16Vec* vec);
+std::vector<uint32_t> convertU32Vec(U32Vec* vec);
+std::vector<float> convertF32Vec(F32Vec* vec);
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/sdp/RsdparsaSdpInc.h b/dom/media/webrtc/sdp/RsdparsaSdpInc.h
new file mode 100644
index 0000000000..1237d2fdad
--- /dev/null
+++ b/dom/media/webrtc/sdp/RsdparsaSdpInc.h
@@ -0,0 +1,510 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef _RUSTSDPINC_H_
+#define _RUSTSDPINC_H_
+
+#include "nsError.h"
+#include "mozilla/Maybe.h"
+
+#include <stdint.h>
+#include <stdbool.h>
+
+struct BandwidthVec;
+struct RustSdpSession;
+struct RustSdpError;
+struct RustMediaSection;
+struct RustAttributeList;
+struct StringVec;
+struct U8Vec;
+struct U32Vec;
+struct U16Vec;
+struct F32Vec;
+struct SsrcVec;
+struct RustHeapString;
+
+enum class RustSdpAddressType { kRustAddrIp4, kRustAddrIp6 };
+
+struct StringView {
+ const char* buf;
+ size_t len;
+};
+
+struct RustAddress {
+ char ipAddress[50];
+ StringView fqdn;
+ bool isFqdn;
+};
+
+struct RustExplicitlyTypedAddress {
+ RustSdpAddressType addressType;
+ RustAddress address;
+};
+
+struct RustSdpConnection {
+ RustExplicitlyTypedAddress addr;
+ uint8_t ttl;
+ uint64_t amount;
+};
+
+struct RustSdpOrigin {
+ StringView username;
+ uint64_t sessionId;
+ uint64_t sessionVersion;
+ RustExplicitlyTypedAddress addr; // TODO address
+};
+
+enum class RustSdpMediaValue { kRustAudio, kRustVideo, kRustApplication };
+
+enum class RustSdpProtocolValue {
+ kRustRtpSavpf,
+ kRustUdpTlsRtpSavp,
+ kRustTcpDtlsRtpSavp,
+ kRustUdpTlsRtpSavpf,
+ kRustTcpDtlsRtpSavpf,
+ kRustDtlsSctp,
+ kRustUdpDtlsSctp,
+ kRustTcpDtlsSctp,
+ kRustRtpAvp,
+ kRustRtpAvpf,
+ kRustRtpSavp,
+};
+
+enum class RustSdpFormatType { kRustIntegers, kRustStrings };
+
+enum class RustSdpAttributeFingerprintHashAlgorithm : uint16_t {
+ kSha1,
+ kSha224,
+ kSha256,
+ kSha384,
+ kSha512,
+};
+
+struct RustSdpAttributeFingerprint {
+ RustSdpAttributeFingerprintHashAlgorithm hashAlgorithm;
+ U8Vec* fingerprint;
+};
+
+enum class RustSdpSetup {
+ kRustActive,
+ kRustActpass,
+ kRustHoldconn,
+ kRustPassive
+};
+
+enum class RustSdpAttributeDtlsMessageType : uint8_t {
+ kClient,
+ kServer,
+};
+
+struct RustSdpAttributeDtlsMessage {
+ RustSdpAttributeDtlsMessageType role;
+ StringView value;
+};
+
+struct RustSdpAttributeSsrc {
+ uint32_t id;
+ StringView attribute;
+ StringView value;
+};
+
+enum class RustSdpAttributeSsrcGroupSemantic {
+ kRustDup,
+ kRustFid,
+ kRustFec,
+ kRustFecFr,
+ kRustSim,
+};
+
+struct RustSdpAttributeSsrcGroup {
+ RustSdpAttributeSsrcGroupSemantic semantic;
+ SsrcVec* ssrcs;
+};
+
+struct RustSdpAttributeRtpmap {
+ uint8_t payloadType;
+ StringView codecName;
+ uint32_t frequency;
+ uint32_t channels;
+};
+
+struct RustSdpAttributeRtcpFb {
+ uint32_t payloadType;
+ uint32_t feedbackType;
+ StringView parameter;
+ StringView extra;
+};
+
+struct RustSdpAttributeRidParameters {
+ uint32_t max_width;
+ uint32_t max_height;
+ uint32_t max_fps;
+ uint32_t max_fs;
+ uint32_t max_br;
+ uint32_t max_pps;
+ StringVec* unknown;
+};
+
+struct RustSdpAttributeRid {
+ StringView id;
+ uint32_t direction;
+ U16Vec* formats;
+ RustSdpAttributeRidParameters params;
+ StringVec* depends;
+};
+
+struct RustSdpAttributeImageAttrXYRange {
+ uint32_t min;
+ uint32_t max;
+ uint32_t step;
+ U32Vec* discrete_values;
+};
+
+struct RustSdpAttributeImageAttrSRange {
+ float min;
+ float max;
+ F32Vec* discrete_values;
+};
+
+struct RustSdpAttributeImageAttrPRange {
+ float min;
+ float max;
+};
+
+struct RustSdpAttributeImageAttrSet {
+ RustSdpAttributeImageAttrXYRange x;
+ RustSdpAttributeImageAttrXYRange y;
+ bool has_sar;
+ RustSdpAttributeImageAttrSRange sar;
+ bool has_par;
+ RustSdpAttributeImageAttrPRange par;
+ float q;
+};
+
+struct RustSdpAttributeImageAttrSetVec;
+struct RustSdpAttributeImageAttrSetList {
+ RustSdpAttributeImageAttrSetVec* sets;
+};
+
+struct RustSdpAttributeImageAttr {
+ uint32_t payloadType;
+ RustSdpAttributeImageAttrSetList send;
+ RustSdpAttributeImageAttrSetList recv;
+};
+
+struct RustRtxFmtpParameters {
+ uint8_t apt;
+ bool has_rtx_time;
+ uint32_t rtx_time;
+};
+
+struct RustSdpAttributeFmtpParameters {
+ // H264
+ uint32_t packetization_mode;
+ bool level_asymmetry_allowed;
+ uint32_t profile_level_id;
+ uint32_t max_fs;
+ uint32_t max_cpb;
+ uint32_t max_dpb;
+ uint32_t max_br;
+ uint32_t max_mbps;
+
+ // VP8 and VP9
+ // max_fs, already defined in H264
+ uint32_t max_fr;
+
+ // Opus
+ uint32_t maxplaybackrate;
+ uint32_t maxaveragebitrate;
+ bool usedtx;
+ bool stereo;
+ bool useinbandfec;
+ bool cbr;
+ uint32_t ptime;
+ uint32_t minptime;
+ uint32_t maxptime;
+
+ // telephone-event
+ StringView dtmf_tones;
+
+ // RTX
+ RustRtxFmtpParameters rtx;
+
+ // Red codecs
+ U8Vec* encodings;
+
+ // Unknown
+ StringVec* unknown_tokens;
+};
+
+struct RustSdpAttributeFmtp {
+ uint8_t payloadType;
+ StringView codecName;
+ RustSdpAttributeFmtpParameters parameters;
+};
+
+struct RustSdpAttributeFlags {
+ bool iceLite;
+ bool rtcpMux;
+ bool rtcpRsize;
+ bool bundleOnly;
+ bool endOfCandidates;
+};
+
+struct RustSdpAttributeMsid {
+ StringView id;
+ StringView appdata;
+};
+
+struct RustSdpAttributeMsidSemantic {
+ StringView semantic;
+ StringVec* msids;
+};
+
+enum class RustSdpAttributeGroupSemantic {
+ kRustLipSynchronization,
+ kRustFlowIdentification,
+ kRustSingleReservationFlow,
+ kRustAlternateNetworkAddressType,
+ kRustForwardErrorCorrection,
+ kRustDecodingDependency,
+ kRustBundle,
+};
+
+struct RustSdpAttributeGroup {
+ RustSdpAttributeGroupSemantic semantic;
+ StringVec* tags;
+};
+
+struct RustSdpAttributeRtcp {
+ uint32_t port;
+ RustExplicitlyTypedAddress unicastAddr;
+ bool has_address;
+};
+
+struct RustSdpAttributeSctpmap {
+ uint32_t port;
+ uint32_t channels;
+};
+
+struct RustSdpAttributeSimulcastId {
+ StringView id;
+ bool paused;
+};
+
+struct RustSdpAttributeSimulcastIdVec;
+struct RustSdpAttributeSimulcastVersion {
+ RustSdpAttributeSimulcastIdVec* ids;
+};
+
+struct RustSdpAttributeSimulcastVersionVec;
+struct RustSdpAttributeSimulcast {
+ RustSdpAttributeSimulcastVersionVec* send;
+ RustSdpAttributeSimulcastVersionVec* recv;
+};
+
+enum class RustDirection {
+ kRustRecvonly,
+ kRustSendonly,
+ kRustSendrecv,
+ kRustInactive
+};
+
+struct RustSdpAttributeRemoteCandidate {
+ uint32_t component;
+ RustAddress address;
+ uint32_t port;
+};
+
+struct RustSdpAttributeExtmap {
+ uint16_t id;
+ bool direction_specified;
+ RustDirection direction;
+ StringView url;
+ StringView extensionAttributes;
+};
+
+extern "C" {
+
+size_t string_vec_len(const StringVec* vec);
+nsresult string_vec_get_view(const StringVec* vec, size_t index,
+ StringView* str);
+nsresult free_boxed_string_vec(StringVec* vec);
+
+size_t f32_vec_len(const F32Vec* vec);
+nsresult f32_vec_get(const F32Vec* vec, size_t index, float* ret);
+
+size_t u32_vec_len(const U32Vec* vec);
+nsresult u32_vec_get(const U32Vec* vec, size_t index, uint32_t* ret);
+
+size_t u16_vec_len(const U16Vec* vec);
+nsresult u16_vec_get(const U16Vec* vec, size_t index, uint16_t* ret);
+
+size_t u8_vec_len(const U8Vec* vec);
+nsresult u8_vec_get(const U8Vec* vec, size_t index, uint8_t* ret);
+
+size_t ssrc_vec_len(const SsrcVec* vec);
+nsresult ssrc_vec_get_id(const SsrcVec* vec, size_t index, uint32_t* ret);
+
+void sdp_free_string(char* string);
+
+nsresult parse_sdp(StringView sdp, bool fail_on_warning, RustSdpSession** ret,
+ RustSdpError** err);
+RustSdpSession* sdp_new_reference(RustSdpSession* aSess);
+RustSdpSession* create_sdp_clone(const RustSdpSession* aSess);
+void sdp_free_session(RustSdpSession* ret);
+size_t sdp_get_error_line_num(const RustSdpError* err);
+char* sdp_get_error_message(const RustSdpError* err);
+void sdp_free_error_message(char* message);
+void sdp_free_error(RustSdpError* err);
+
+RustSdpOrigin sdp_get_origin(const RustSdpSession* aSess);
+
+uint32_t get_sdp_bandwidth(const RustSdpSession* aSess,
+ const char* aBandwidthType);
+BandwidthVec* sdp_get_session_bandwidth_vec(const RustSdpSession* aSess);
+BandwidthVec* sdp_get_media_bandwidth_vec(const RustMediaSection* aMediaSec);
+char* sdp_serialize_bandwidth(const BandwidthVec* bandwidths);
+bool sdp_session_has_connection(const RustSdpSession* aSess);
+nsresult sdp_get_session_connection(const RustSdpSession* aSess,
+ RustSdpConnection* ret);
+RustAttributeList* get_sdp_session_attributes(const RustSdpSession* aSess);
+
+size_t sdp_media_section_count(const RustSdpSession* aSess);
+RustMediaSection* sdp_get_media_section(const RustSdpSession* aSess,
+ size_t aLevel);
+nsresult sdp_add_media_section(RustSdpSession* aSess, uint32_t aMediaType,
+ uint32_t aDirection, uint16_t aPort,
+ uint32_t aProtocol, uint32_t aAddrType,
+ StringView aAddr);
+RustSdpMediaValue sdp_rust_get_media_type(const RustMediaSection* aMediaSec);
+RustSdpProtocolValue sdp_get_media_protocol(const RustMediaSection* aMediaSec);
+RustSdpFormatType sdp_get_format_type(const RustMediaSection* aMediaSec);
+StringVec* sdp_get_format_string_vec(const RustMediaSection* aMediaSec);
+U32Vec* sdp_get_format_u32_vec(const RustMediaSection* aMediaSec);
+void sdp_set_media_port(const RustMediaSection* aMediaSec, uint32_t aPort);
+uint32_t sdp_get_media_port(const RustMediaSection* aMediaSec);
+uint32_t sdp_get_media_port_count(const RustMediaSection* aMediaSec);
+uint32_t sdp_get_media_bandwidth(const RustMediaSection* aMediaSec,
+ const char* aBandwidthType);
+bool sdp_media_has_connection(const RustMediaSection* aMediaSec);
+nsresult sdp_get_media_connection(const RustMediaSection* aMediaSec,
+ RustSdpConnection* ret);
+
+RustAttributeList* sdp_get_media_attribute_list(
+ const RustMediaSection* aMediaSec);
+
+nsresult sdp_media_add_codec(const RustMediaSection* aMediaSec, uint8_t aPT,
+ StringView aCodecName, uint32_t aClockrate,
+ uint16_t channels);
+void sdp_media_clear_codecs(const RustMediaSection* aMediaSec);
+nsresult sdp_media_add_datachannel(const RustMediaSection* aMediaSec,
+ StringView aName, uint16_t aPort,
+ uint16_t streams, uint32_t aMessageSize);
+
+nsresult sdp_get_iceufrag(const RustAttributeList* aList, StringView* ret);
+nsresult sdp_get_icepwd(const RustAttributeList* aList, StringView* ret);
+nsresult sdp_get_identity(const RustAttributeList* aList, StringView* ret);
+nsresult sdp_get_iceoptions(const RustAttributeList* aList, StringVec** ret);
+
+nsresult sdp_get_dtls_message(const RustAttributeList* aList,
+ RustSdpAttributeDtlsMessage* ret);
+
+size_t sdp_get_fingerprint_count(const RustAttributeList* aList);
+void sdp_get_fingerprints(const RustAttributeList* aList, size_t listSize,
+ RustSdpAttributeFingerprint* ret);
+
+nsresult sdp_get_setup(const RustAttributeList* aList, RustSdpSetup* ret);
+
+size_t sdp_get_ssrc_count(const RustAttributeList* aList);
+void sdp_get_ssrcs(const RustAttributeList* aList, size_t listSize,
+ RustSdpAttributeSsrc* ret);
+
+size_t sdp_get_ssrc_group_count(const RustAttributeList* aList);
+void sdp_get_ssrc_groups(const RustAttributeList* aList, size_t listSize,
+ RustSdpAttributeSsrcGroup* ret);
+
+size_t sdp_get_rtpmap_count(const RustAttributeList* aList);
+void sdp_get_rtpmaps(const RustAttributeList* aList, size_t listSize,
+ RustSdpAttributeRtpmap* ret);
+
+size_t sdp_get_fmtp_count(const RustAttributeList* aList);
+size_t sdp_get_fmtp(const RustAttributeList* aList, size_t listSize,
+ RustSdpAttributeFmtp* ret);
+
+int64_t sdp_get_ptime(const RustAttributeList* aList);
+int64_t sdp_get_max_msg_size(const RustAttributeList* aList);
+int64_t sdp_get_sctp_port(const RustAttributeList* aList);
+nsresult sdp_get_maxptime(const RustAttributeList* aList, uint64_t* aMaxPtime);
+
+RustSdpAttributeFlags sdp_get_attribute_flags(const RustAttributeList* aList);
+
+nsresult sdp_get_mid(const RustAttributeList* aList, StringView* ret);
+
+size_t sdp_get_msid_count(const RustAttributeList* aList);
+void sdp_get_msids(const RustAttributeList* aList, size_t listSize,
+ RustSdpAttributeMsid* ret);
+
+size_t sdp_get_msid_semantic_count(RustAttributeList* aList);
+void sdp_get_msid_semantics(const RustAttributeList* aList, size_t listSize,
+ RustSdpAttributeMsidSemantic* ret);
+
+size_t sdp_get_group_count(const RustAttributeList* aList);
+void sdp_get_groups(const RustAttributeList* aList, size_t listSize,
+ RustSdpAttributeGroup* ret);
+
+nsresult sdp_get_rtcp(const RustAttributeList* aList,
+ RustSdpAttributeRtcp* ret);
+
+size_t sdp_get_rtcpfb_count(const RustAttributeList* aList);
+void sdp_get_rtcpfbs(const RustAttributeList* aList, size_t listSize,
+ RustSdpAttributeRtcpFb* ret);
+
+size_t sdp_get_imageattr_count(const RustAttributeList* aList);
+void sdp_get_imageattrs(const RustAttributeList* aList, size_t listSize,
+ RustSdpAttributeImageAttr* ret);
+
+size_t sdp_imageattr_get_set_count(const RustSdpAttributeImageAttrSetVec* sets);
+void sdp_imageattr_get_sets(const RustSdpAttributeImageAttrSetVec* sets,
+ size_t listSize, RustSdpAttributeImageAttrSet* ret);
+
+size_t sdp_get_sctpmap_count(const RustAttributeList* aList);
+void sdp_get_sctpmaps(const RustAttributeList* aList, size_t listSize,
+ RustSdpAttributeSctpmap* ret);
+
+nsresult sdp_get_simulcast(const RustAttributeList* aList,
+ RustSdpAttributeSimulcast* ret);
+
+size_t sdp_simulcast_get_version_count(
+ const RustSdpAttributeSimulcastVersionVec* aVersionList);
+void sdp_simulcast_get_versions(
+ const RustSdpAttributeSimulcastVersionVec* aversionList, size_t listSize,
+ RustSdpAttributeSimulcastVersion* ret);
+
+size_t sdp_simulcast_get_ids_count(const RustSdpAttributeSimulcastIdVec* aAlts);
+void sdp_simulcast_get_ids(const RustSdpAttributeSimulcastIdVec* aAlts,
+ size_t listSize, RustSdpAttributeSimulcastId* ret);
+
+RustDirection sdp_get_direction(const RustAttributeList* aList);
+
+size_t sdp_get_remote_candidate_count(const RustAttributeList* aList);
+void sdp_get_remote_candidates(const RustAttributeList* aList, size_t listSize,
+ RustSdpAttributeRemoteCandidate* ret);
+
+size_t sdp_get_candidate_count(const RustAttributeList* aList);
+void sdp_get_candidates(const RustAttributeList* aLisst, size_t listSize,
+ StringVec** ret);
+
+size_t sdp_get_rid_count(const RustAttributeList* aList);
+void sdp_get_rids(const RustAttributeList* aList, size_t listSize,
+ RustSdpAttributeRid* ret);
+
+size_t sdp_get_extmap_count(const RustAttributeList* aList);
+void sdp_get_extmaps(const RustAttributeList* aList, size_t listSize,
+ RustSdpAttributeExtmap* ret);
+
+} // extern "C"
+
+#endif
diff --git a/dom/media/webrtc/sdp/RsdparsaSdpMediaSection.cpp b/dom/media/webrtc/sdp/RsdparsaSdpMediaSection.cpp
new file mode 100644
index 0000000000..25084db2ad
--- /dev/null
+++ b/dom/media/webrtc/sdp/RsdparsaSdpMediaSection.cpp
@@ -0,0 +1,253 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "sdp/SdpMediaSection.h"
+#include "sdp/RsdparsaSdpMediaSection.h"
+
+#include "sdp/RsdparsaSdpGlue.h"
+#include "sdp/RsdparsaSdpInc.h"
+
+#include <ostream>
+
+#ifdef CRLF
+# undef CRLF
+#endif
+#define CRLF "\r\n"
+
+namespace mozilla {
+
+RsdparsaSdpMediaSection::RsdparsaSdpMediaSection(
+ size_t level, RsdparsaSessionHandle session,
+ const RustMediaSection* const section,
+ const RsdparsaSdpAttributeList* sessionLevel)
+ : SdpMediaSection(level), mSession(std::move(session)), mSection(section) {
+ switch (sdp_rust_get_media_type(section)) {
+ case RustSdpMediaValue::kRustAudio:
+ mMediaType = kAudio;
+ break;
+ case RustSdpMediaValue::kRustVideo:
+ mMediaType = kVideo;
+ break;
+ case RustSdpMediaValue::kRustApplication:
+ mMediaType = kApplication;
+ break;
+ }
+
+ RsdparsaSessionHandle attributeSession(sdp_new_reference(mSession.get()));
+ mAttributeList.reset(new RsdparsaSdpAttributeList(std::move(attributeSession),
+ section, sessionLevel));
+
+ LoadFormats();
+ LoadConnection();
+}
+
+unsigned int RsdparsaSdpMediaSection::GetPort() const {
+ return sdp_get_media_port(mSection);
+}
+
+void RsdparsaSdpMediaSection::SetPort(unsigned int port) {
+ sdp_set_media_port(mSection, port);
+}
+
+unsigned int RsdparsaSdpMediaSection::GetPortCount() const {
+ return sdp_get_media_port_count(mSection);
+}
+
+SdpMediaSection::Protocol RsdparsaSdpMediaSection::GetProtocol() const {
+ switch (sdp_get_media_protocol(mSection)) {
+ case RustSdpProtocolValue::kRustRtpSavpf:
+ return kRtpSavpf;
+ case RustSdpProtocolValue::kRustUdpTlsRtpSavp:
+ return kUdpTlsRtpSavp;
+ case RustSdpProtocolValue::kRustTcpDtlsRtpSavp:
+ return kTcpDtlsRtpSavp;
+ case RustSdpProtocolValue::kRustUdpTlsRtpSavpf:
+ return kUdpTlsRtpSavpf;
+ case RustSdpProtocolValue::kRustTcpDtlsRtpSavpf:
+ return kTcpDtlsRtpSavpf;
+ case RustSdpProtocolValue::kRustDtlsSctp:
+ return kDtlsSctp;
+ case RustSdpProtocolValue::kRustUdpDtlsSctp:
+ return kUdpDtlsSctp;
+ case RustSdpProtocolValue::kRustTcpDtlsSctp:
+ return kTcpDtlsSctp;
+ case RustSdpProtocolValue::kRustRtpAvp:
+ return kRtpAvp;
+ case RustSdpProtocolValue::kRustRtpAvpf:
+ return kRtpAvpf;
+ case RustSdpProtocolValue::kRustRtpSavp:
+ return kRtpSavp;
+ }
+ MOZ_CRASH("invalid media protocol");
+}
+
+const SdpConnection& RsdparsaSdpMediaSection::GetConnection() const {
+ MOZ_ASSERT(mConnection);
+ return *mConnection;
+}
+
+SdpConnection& RsdparsaSdpMediaSection::GetConnection() {
+ MOZ_ASSERT(mConnection);
+ return *mConnection;
+}
+
+uint32_t RsdparsaSdpMediaSection::GetBandwidth(const std::string& type) const {
+ return sdp_get_media_bandwidth(mSection, type.c_str());
+}
+
+const std::vector<std::string>& RsdparsaSdpMediaSection::GetFormats() const {
+ return mFormats;
+}
+
+const SdpAttributeList& RsdparsaSdpMediaSection::GetAttributeList() const {
+ return *mAttributeList;
+}
+
+SdpAttributeList& RsdparsaSdpMediaSection::GetAttributeList() {
+ return *mAttributeList;
+}
+
+SdpDirectionAttribute RsdparsaSdpMediaSection::GetDirectionAttribute() const {
+ return SdpDirectionAttribute(mAttributeList->GetDirection());
+}
+
+void RsdparsaSdpMediaSection::AddCodec(const std::string& pt,
+ const std::string& name,
+ uint32_t clockrate, uint16_t channels) {
+ StringView rustName{name.c_str(), name.size()};
+
+ // call the rust interface
+ auto nr = sdp_media_add_codec(mSection, std::stoul(pt), rustName, clockrate,
+ channels);
+
+ if (NS_SUCCEEDED(nr)) {
+ // If the rust call was successful, adjust the shadow C++ structures
+ mFormats.push_back(pt);
+
+ // Add a rtpmap in mAttributeList
+ SdpRtpmapAttributeList* rtpmap = new SdpRtpmapAttributeList();
+ if (mAttributeList->HasAttribute(SdpAttribute::kRtpmapAttribute)) {
+ const SdpRtpmapAttributeList& old = mAttributeList->GetRtpmap();
+ for (auto it = old.mRtpmaps.begin(); it != old.mRtpmaps.end(); ++it) {
+ rtpmap->mRtpmaps.push_back(*it);
+ }
+ }
+
+ SdpRtpmapAttributeList::CodecType codec =
+ SdpRtpmapAttributeList::kOtherCodec;
+ if (name == "opus") {
+ codec = SdpRtpmapAttributeList::kOpus;
+ } else if (name == "VP8") {
+ codec = SdpRtpmapAttributeList::kVP8;
+ } else if (name == "VP9") {
+ codec = SdpRtpmapAttributeList::kVP9;
+ } else if (name == "H264") {
+ codec = SdpRtpmapAttributeList::kH264;
+ }
+
+ rtpmap->PushEntry(pt, codec, name, clockrate, channels);
+ mAttributeList->SetAttribute(rtpmap);
+ }
+}
+
+void RsdparsaSdpMediaSection::ClearCodecs() {
+ // Clear the codecs in rust
+ sdp_media_clear_codecs(mSection);
+
+ mFormats.clear();
+ mAttributeList->RemoveAttribute(SdpAttribute::kRtpmapAttribute);
+ mAttributeList->RemoveAttribute(SdpAttribute::kFmtpAttribute);
+ mAttributeList->RemoveAttribute(SdpAttribute::kSctpmapAttribute);
+ mAttributeList->RemoveAttribute(SdpAttribute::kRtcpFbAttribute);
+}
+
+void RsdparsaSdpMediaSection::AddDataChannel(const std::string& name,
+ uint16_t port, uint16_t streams,
+ uint32_t message_size) {
+ StringView rustName{name.c_str(), name.size()};
+ auto nr = sdp_media_add_datachannel(mSection, rustName, port, streams,
+ message_size);
+ if (NS_SUCCEEDED(nr)) {
+ // Update the formats
+ mFormats.clear();
+ LoadFormats();
+
+ // Update the attribute list
+ RsdparsaSessionHandle sessHandle(sdp_new_reference(mSession.get()));
+ auto sessAttributes = mAttributeList->mSessionAttributes;
+ mAttributeList.reset(new RsdparsaSdpAttributeList(
+ std::move(sessHandle), mSection, sessAttributes));
+ }
+}
+
+void RsdparsaSdpMediaSection::Serialize(std::ostream& os) const {
+ os << "m=" << mMediaType << " " << GetPort();
+ if (GetPortCount()) {
+ os << "/" << GetPortCount();
+ }
+ os << " " << GetProtocol();
+ for (auto i = mFormats.begin(); i != mFormats.end(); ++i) {
+ os << " " << (*i);
+ }
+ os << CRLF;
+
+ // We dont do i=
+
+ if (mConnection) {
+ os << *mConnection;
+ }
+
+ BandwidthVec* bwVec = sdp_get_media_bandwidth_vec(mSection);
+ char* bwString = sdp_serialize_bandwidth(bwVec);
+ if (bwString) {
+ os << bwString;
+ sdp_free_string(bwString);
+ }
+
+ // We dont do k= because they're evil
+
+ os << *mAttributeList;
+}
+
+void RsdparsaSdpMediaSection::LoadFormats() {
+ RustSdpFormatType formatType = sdp_get_format_type(mSection);
+ if (formatType == RustSdpFormatType::kRustIntegers) {
+ U32Vec* vec = sdp_get_format_u32_vec(mSection);
+ size_t len = u32_vec_len(vec);
+ for (size_t i = 0; i < len; i++) {
+ uint32_t val;
+ u32_vec_get(vec, i, &val);
+ mFormats.push_back(std::to_string(val));
+ }
+ } else {
+ StringVec* vec = sdp_get_format_string_vec(mSection);
+ mFormats = convertStringVec(vec);
+ }
+}
+
+UniquePtr<SdpConnection> convertRustConnection(RustSdpConnection conn) {
+ auto address = convertExplicitlyTypedAddress(&conn.addr);
+ return MakeUnique<SdpConnection>(address.first, address.second, conn.ttl,
+ conn.amount);
+}
+
+void RsdparsaSdpMediaSection::LoadConnection() {
+ RustSdpConnection conn;
+ nsresult nr;
+ if (sdp_media_has_connection(mSection)) {
+ nr = sdp_get_media_connection(mSection, &conn);
+ if (NS_SUCCEEDED(nr)) {
+ mConnection = convertRustConnection(conn);
+ }
+ } else if (sdp_session_has_connection(mSession.get())) {
+ nr = sdp_get_session_connection(mSession.get(), &conn);
+ if (NS_SUCCEEDED(nr)) {
+ mConnection = convertRustConnection(conn);
+ }
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/sdp/RsdparsaSdpMediaSection.h b/dom/media/webrtc/sdp/RsdparsaSdpMediaSection.h
new file mode 100644
index 0000000000..3c193bd99c
--- /dev/null
+++ b/dom/media/webrtc/sdp/RsdparsaSdpMediaSection.h
@@ -0,0 +1,71 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _RUSTSDPMEDIASECTION_H_
+#define _RUSTSDPMEDIASECTION_H_
+
+#include "mozilla/UniquePtr.h"
+#include "sdp/RsdparsaSdpInc.h"
+#include "sdp/RsdparsaSdpGlue.h"
+#include "sdp/SdpMediaSection.h"
+#include "sdp/RsdparsaSdpAttributeList.h"
+
+namespace mozilla {
+
+class RsdparsaSdp;
+class SdpParser;
+
+class RsdparsaSdpMediaSection final : public SdpMediaSection {
+ friend class RsdparsaSdp;
+
+ public:
+ ~RsdparsaSdpMediaSection() {}
+
+ MediaType GetMediaType() const override { return mMediaType; }
+
+ unsigned int GetPort() const override;
+ void SetPort(unsigned int port) override;
+ unsigned int GetPortCount() const override;
+ Protocol GetProtocol() const override;
+ const SdpConnection& GetConnection() const override;
+ SdpConnection& GetConnection() override;
+ uint32_t GetBandwidth(const std::string& type) const override;
+ const std::vector<std::string>& GetFormats() const override;
+
+ const SdpAttributeList& GetAttributeList() const override;
+ SdpAttributeList& GetAttributeList() override;
+ SdpDirectionAttribute GetDirectionAttribute() const override;
+
+ void AddCodec(const std::string& pt, const std::string& name,
+ uint32_t clockrate, uint16_t channels) override;
+ void ClearCodecs() override;
+
+ void AddDataChannel(const std::string& name, uint16_t port, uint16_t streams,
+ uint32_t message_size) override;
+
+ void Serialize(std::ostream&) const override;
+
+ private:
+ RsdparsaSdpMediaSection(size_t level, RsdparsaSessionHandle session,
+ const RustMediaSection* const section,
+ const RsdparsaSdpAttributeList* sessionLevel);
+
+ void LoadFormats();
+ void LoadConnection();
+
+ RsdparsaSessionHandle mSession;
+ const RustMediaSection* mSection;
+
+ MediaType mMediaType;
+ std::vector<std::string> mFormats;
+
+ UniquePtr<SdpConnection> mConnection;
+
+ UniquePtr<RsdparsaSdpAttributeList> mAttributeList;
+};
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/sdp/RsdparsaSdpParser.cpp b/dom/media/webrtc/sdp/RsdparsaSdpParser.cpp
new file mode 100644
index 0000000000..b955bcd46b
--- /dev/null
+++ b/dom/media/webrtc/sdp/RsdparsaSdpParser.cpp
@@ -0,0 +1,73 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsError.h"
+
+#include "mozilla/UniquePtr.h"
+
+#include "sdp/Sdp.h"
+#include "sdp/RsdparsaSdp.h"
+#include "sdp/RsdparsaSdpParser.h"
+#include "sdp/RsdparsaSdpInc.h"
+#include "sdp/RsdparsaSdpGlue.h"
+
+namespace mozilla {
+
+const std::string& RsdparsaSdpParser::ParserName() {
+ static const std::string& WEBRTC_SDP_NAME = "WEBRTCSDP";
+ return WEBRTC_SDP_NAME;
+}
+
+UniquePtr<SdpParser::Results> RsdparsaSdpParser::Parse(
+ const std::string& aText) {
+ UniquePtr<SdpParser::InternalResults> results(
+ new SdpParser::InternalResults(Name()));
+ RustSdpSession* result = nullptr;
+ RustSdpError* err = nullptr;
+ StringView sdpTextView{aText.c_str(), aText.length()};
+ nsresult rv = parse_sdp(sdpTextView, false, &result, &err);
+ if (rv != NS_OK) {
+ size_t line = sdp_get_error_line_num(err);
+ char* cString = sdp_get_error_message(err);
+ if (cString) {
+ std::string errMsg(cString);
+ sdp_free_error_message(cString);
+ sdp_free_error(err);
+ results->AddParseError(line, errMsg);
+ } else {
+ results->AddParseError(line, "Unable to retreive parse error.");
+ }
+ return results;
+ }
+
+ if (err) {
+ size_t line = sdp_get_error_line_num(err);
+ char* cString = sdp_get_error_message(err);
+ if (cString) {
+ std::string warningMsg(cString);
+ results->AddParseWarning(line, warningMsg);
+ sdp_free_error_message(cString);
+ sdp_free_error(err);
+ } else {
+ results->AddParseWarning(line, "Unable to retreive parse warning.");
+ }
+ }
+
+ RsdparsaSessionHandle uniqueResult(result);
+ RustSdpOrigin rustOrigin = sdp_get_origin(uniqueResult.get());
+ auto address = convertExplicitlyTypedAddress(&rustOrigin.addr);
+ SdpOrigin origin(convertStringView(rustOrigin.username), rustOrigin.sessionId,
+ rustOrigin.sessionVersion, address.first, address.second);
+
+ results->SetSdp(MakeUnique<RsdparsaSdp>(std::move(uniqueResult), origin));
+ return results;
+}
+
+bool RsdparsaSdpParser::IsNamed(const std::string& aName) {
+ return aName == ParserName();
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/sdp/RsdparsaSdpParser.h b/dom/media/webrtc/sdp/RsdparsaSdpParser.h
new file mode 100644
index 0000000000..443aa46557
--- /dev/null
+++ b/dom/media/webrtc/sdp/RsdparsaSdpParser.h
@@ -0,0 +1,34 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _RUSTSDPPARSER_H_
+#define _RUSTSDPPARSER_H_
+
+#include <string>
+
+#include "mozilla/UniquePtr.h"
+
+#include "sdp/SdpParser.h"
+
+namespace mozilla {
+
+class RsdparsaSdpParser final : public SdpParser {
+ static const std::string& ParserName();
+
+ public:
+ RsdparsaSdpParser() = default;
+ virtual ~RsdparsaSdpParser() = default;
+
+ const std::string& Name() const override { return ParserName(); }
+
+ UniquePtr<SdpParser::Results> Parse(const std::string& text) override;
+
+ static bool IsNamed(const std::string& aName);
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/sdp/Sdp.h b/dom/media/webrtc/sdp/Sdp.h
new file mode 100644
index 0000000000..7576cac46f
--- /dev/null
+++ b/dom/media/webrtc/sdp/Sdp.h
@@ -0,0 +1,166 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+
+ ,-----. ,--. ,--.
+ ' .--./ ,--,--.,--.,--.,-' '-.`--' ,---. ,--,--,
+ | | ' ,-. || || |'-. .-',--.| .-. || `
+ ' '--'\\ '-' |' '' ' | | | |' '-' '| || |
+ `-----' `--`--' `----' `--' `--' `---' `--''--'
+
+ :+o+-
+ -dNNNNNd.
+ yNNNNNNNs
+ :mNNNNNm-
+ `/sso/``-://-
+ .:+sydNNNNNNms: `://`
+ `-/+shmNNNNNNNNNNNNNNNms- :mNNNm/
+ `-/oydmNNNNNNNNNNNNNNNNNNNNNNNNdo- +NNNNNN+
+ .shmNNNNNNNNNNNmdyo/:dNNNNNNNNNNNNNNNNdo. `sNNNNNm+
+ hNNNNNNNNmhs+:-` .dNNNNNNNNNNNNNNNNNNNNh+-` `hNNNNNm:
+ -yddyo/:. -dNNNNm::ymNNNNNNNNNNNNNNNmdy+/dNNNNNd.
+ :mNNNNd. `/ymNNNNNNNNNNNNNNNNNNNNNNh`
+ +NNNNNh` `+hNNNNNNNNNNNNNNNNNNNs
+ sNNNNNy` .yNNNNNm`-/oymNNNm+
+ `yNNNNNo oNNNNNm` `-.
+ .dNNNNm/ oNNNNNm`
+ oNNNNm: +NNNNNm`
+ `+yho. +NNNNNm`
+ +NNNNNNs.
+ `yNNNNNNmy-
+ -smNNNNNNh:
+ .smNNNNNNh/
+ `omNNNNNNd:
+ `+dNNNNNd
+ ````......```` /hmdy-
+ `.:/+osyhddmNNMMMMMMMMMMMMMMMMMMMMNNmddhyso+/:.`
+ `-+shmNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNmhs+-`
+ -smMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMds-
+ hMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMh
+ yMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMs
+ .ohNMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMNh+.
+ ./oydmMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMmhyo:.
+ `.:/+osyyhddmmNNMMMMMMMMMMMMMMNNmmddhyyso+/:.`
+
+ ,--------.,--. ,--. ,--.
+ '--. .--'| ,---. `--' ,---. | | ,---.
+ | | | .-. |,--.( .-' | |( .-'
+ | | | | | || |.-' `) | |.-' `)
+ `--' `--' `--'`--'`----' `--'`----'
+ ,--.
+ ,---. ,------. ,------. ,--. | |
+ ' .-' | .-. \ | .--. ' ,--,--.,--.--.,-' '-. ,--,--.| |
+ `. `-. | | \ :| '--' |' ,-. || .--''-. .-'' ,-. || |
+ .-' || '--' /| | --' \ '-' || | | | \ '-' |`--'
+ `-----' `-------' `--' `--`--'`--' `--' `--`--'.--.
+ '__'
+*/
+
+#ifndef _SDP_H_
+#define _SDP_H_
+
+#include <ostream>
+#include <vector>
+#include <sstream>
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Maybe.h"
+#include "sdp/SdpMediaSection.h"
+#include "sdp/SdpAttributeList.h"
+#include "sdp/SdpEnum.h"
+
+namespace mozilla {
+
+class SdpOrigin;
+class SdpEncryptionKey;
+class SdpMediaSection;
+
+/**
+ * Base class for an SDP
+ */
+class Sdp {
+ public:
+ Sdp() = default;
+ virtual ~Sdp() = default;
+
+ virtual Sdp* Clone() const = 0;
+
+ virtual const SdpOrigin& GetOrigin() const = 0;
+ // Note: connection information is always retrieved from media sections
+ virtual uint32_t GetBandwidth(const std::string& type) const = 0;
+
+ virtual const SdpAttributeList& GetAttributeList() const = 0;
+ virtual SdpAttributeList& GetAttributeList() = 0;
+
+ virtual size_t GetMediaSectionCount() const = 0;
+ virtual const SdpMediaSection& GetMediaSection(size_t level) const = 0;
+ virtual SdpMediaSection& GetMediaSection(size_t level) = 0;
+
+ virtual SdpMediaSection& AddMediaSection(SdpMediaSection::MediaType media,
+ SdpDirectionAttribute::Direction dir,
+ uint16_t port,
+ SdpMediaSection::Protocol proto,
+ sdp::AddrType addrType,
+ const std::string& addr) = 0;
+
+ virtual void Serialize(std::ostream&) const = 0;
+
+ std::string ToString() const;
+};
+
+inline std::ostream& operator<<(std::ostream& os, const Sdp& sdp) {
+ sdp.Serialize(os);
+ return os;
+}
+
+inline std::string Sdp::ToString() const {
+ std::stringstream s;
+ s << *this;
+ return s.str();
+}
+
+class SdpOrigin {
+ public:
+ SdpOrigin(const std::string& username, uint64_t sessId, uint64_t sessVer,
+ sdp::AddrType addrType, const std::string& addr)
+ : mUsername(username),
+ mSessionId(sessId),
+ mSessionVersion(sessVer),
+ mAddrType(addrType),
+ mAddress(addr) {}
+
+ const std::string& GetUsername() const { return mUsername; }
+
+ uint64_t GetSessionId() const { return mSessionId; }
+
+ uint64_t GetSessionVersion() const { return mSessionVersion; }
+
+ sdp::AddrType GetAddrType() const { return mAddrType; }
+
+ const std::string& GetAddress() const { return mAddress; }
+
+ void Serialize(std::ostream& os) const {
+ sdp::NetType netType = sdp::kInternet;
+ os << "o=" << mUsername << " " << mSessionId << " " << mSessionVersion
+ << " " << netType << " " << mAddrType << " " << mAddress << "\r\n";
+ }
+
+ private:
+ std::string mUsername;
+ uint64_t mSessionId;
+ uint64_t mSessionVersion;
+ sdp::AddrType mAddrType;
+ std::string mAddress;
+};
+
+inline std::ostream& operator<<(std::ostream& os, const SdpOrigin& origin) {
+ origin.Serialize(os);
+ return os;
+}
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/sdp/SdpAttribute.cpp b/dom/media/webrtc/sdp/SdpAttribute.cpp
new file mode 100644
index 0000000000..cacb25e6b1
--- /dev/null
+++ b/dom/media/webrtc/sdp/SdpAttribute.cpp
@@ -0,0 +1,1562 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "sdp/SdpAttribute.h"
+#include "sdp/SdpHelper.h"
+#include <iomanip>
+#include <bitset>
+
+#ifdef CRLF
+# undef CRLF
+#endif
+#define CRLF "\r\n"
+
+namespace mozilla {
+
+static unsigned char PeekChar(std::istream& is, std::string* error) {
+ int next = is.peek();
+ if (next == EOF) {
+ *error = "Truncated";
+ return 0;
+ }
+
+ return next;
+}
+
+static std::string ParseToken(std::istream& is, const std::string& delims,
+ std::string* error) {
+ std::string token;
+ while (is) {
+ unsigned char c = PeekChar(is, error);
+ if (!c || (delims.find(c) != std::string::npos)) {
+ break;
+ }
+ token.push_back(std::tolower(is.get()));
+ }
+ return token;
+}
+
+static bool SkipChar(std::istream& is, unsigned char c, std::string* error) {
+ if (PeekChar(is, error) != c) {
+ *error = "Expected \'";
+ error->push_back(c);
+ error->push_back('\'');
+ return false;
+ }
+
+ is.get();
+ return true;
+}
+
+void SdpConnectionAttribute::Serialize(std::ostream& os) const {
+ os << "a=" << mType << ":" << mValue << CRLF;
+}
+
+void SdpDirectionAttribute::Serialize(std::ostream& os) const {
+ os << "a=" << mValue << CRLF;
+}
+
+void SdpDtlsMessageAttribute::Serialize(std::ostream& os) const {
+ os << "a=" << mType << ":" << mRole << " " << mValue << CRLF;
+}
+
+bool SdpDtlsMessageAttribute::Parse(std::istream& is, std::string* error) {
+ std::string roleToken = ParseToken(is, " ", error);
+ if (roleToken == "server") {
+ mRole = kServer;
+ } else if (roleToken == "client") {
+ mRole = kClient;
+ } else {
+ *error = "Invalid dtls-message role; must be either client or server";
+ return false;
+ }
+
+ is >> std::ws;
+
+ std::string s(std::istreambuf_iterator<char>(is), {});
+ mValue = s;
+
+ return true;
+}
+
+void SdpExtmapAttributeList::Serialize(std::ostream& os) const {
+ for (auto i = mExtmaps.begin(); i != mExtmaps.end(); ++i) {
+ os << "a=" << mType << ":" << i->entry;
+ if (i->direction_specified) {
+ os << "/" << i->direction;
+ }
+ os << " " << i->extensionname;
+ if (i->extensionattributes.length()) {
+ os << " " << i->extensionattributes;
+ }
+ os << CRLF;
+ }
+}
+
+void SdpFingerprintAttributeList::Serialize(std::ostream& os) const {
+ for (auto i = mFingerprints.begin(); i != mFingerprints.end(); ++i) {
+ os << "a=" << mType << ":" << i->hashFunc << " "
+ << FormatFingerprint(i->fingerprint) << CRLF;
+ }
+}
+
+// Format the fingerprint in RFC 4572 Section 5 attribute format
+std::string SdpFingerprintAttributeList::FormatFingerprint(
+ const std::vector<uint8_t>& fp) {
+ if (fp.empty()) {
+ MOZ_ASSERT(false, "Cannot format an empty fingerprint.");
+ return "";
+ }
+
+ std::ostringstream os;
+ for (auto i = fp.begin(); i != fp.end(); ++i) {
+ os << ":" << std::hex << std::uppercase << std::setw(2) << std::setfill('0')
+ << static_cast<uint32_t>(*i);
+ }
+ return os.str().substr(1);
+}
+
+static uint8_t FromUppercaseHex(char ch) {
+ if ((ch >= '0') && (ch <= '9')) {
+ return ch - '0';
+ }
+ if ((ch >= 'A') && (ch <= 'F')) {
+ return ch - 'A' + 10;
+ }
+ return 16; // invalid
+}
+
+// Parse the fingerprint from RFC 4572 Section 5 attribute format
+std::vector<uint8_t> SdpFingerprintAttributeList::ParseFingerprint(
+ const std::string& str) {
+ size_t targetSize = (str.length() + 1) / 3;
+ std::vector<uint8_t> fp(targetSize);
+ size_t fpIndex = 0;
+
+ if (str.length() % 3 != 2) {
+ fp.clear();
+ return fp;
+ }
+
+ for (size_t i = 0; i < str.length(); i += 3) {
+ uint8_t high = FromUppercaseHex(str[i]);
+ uint8_t low = FromUppercaseHex(str[i + 1]);
+ if (high > 0xf || low > 0xf ||
+ (i + 2 < str.length() && str[i + 2] != ':')) {
+ fp.clear(); // error
+ return fp;
+ }
+ fp[fpIndex++] = high << 4 | low;
+ }
+ return fp;
+}
+
+bool SdpFmtpAttributeList::operator==(const SdpFmtpAttributeList& other) const {
+ return mFmtps == other.mFmtps;
+}
+
+void SdpFmtpAttributeList::Serialize(std::ostream& os) const {
+ for (auto i = mFmtps.begin(); i != mFmtps.end(); ++i) {
+ if (i->parameters) {
+ os << "a=" << mType << ":" << i->format << " ";
+ i->parameters->Serialize(os);
+ os << CRLF;
+ }
+ }
+}
+
+void SdpGroupAttributeList::Serialize(std::ostream& os) const {
+ for (auto i = mGroups.begin(); i != mGroups.end(); ++i) {
+ os << "a=" << mType << ":" << i->semantics;
+ for (auto j = i->tags.begin(); j != i->tags.end(); ++j) {
+ os << " " << (*j);
+ }
+ os << CRLF;
+ }
+}
+
+// We're just using an SdpStringAttribute for this right now
+#if 0
+void SdpIdentityAttribute::Serialize(std::ostream& os) const
+{
+ os << "a=" << mType << ":" << mAssertion;
+ for (auto i = mExtensions.begin(); i != mExtensions.end(); i++) {
+ os << (i == mExtensions.begin() ? " " : ";") << (*i);
+ }
+ os << CRLF;
+}
+#endif
+
+// Class to help with omitting a leading delimiter for the first item in a list
+class SkipFirstDelimiter {
+ public:
+ explicit SkipFirstDelimiter(const std::string& delim)
+ : mDelim(delim), mFirst(true) {}
+
+ std::ostream& print(std::ostream& os) {
+ if (!mFirst) {
+ os << mDelim;
+ }
+ mFirst = false;
+ return os;
+ }
+
+ private:
+ std::string mDelim;
+ bool mFirst;
+};
+
+static std::ostream& operator<<(std::ostream& os, SkipFirstDelimiter& delim) {
+ return delim.print(os);
+}
+
+void SdpImageattrAttributeList::XYRange::Serialize(std::ostream& os) const {
+ if (discreteValues.empty()) {
+ os << "[" << min << ":";
+ if (step != 1) {
+ os << step << ":";
+ }
+ os << max << "]";
+ } else if (discreteValues.size() == 1) {
+ os << discreteValues.front();
+ } else {
+ os << "[";
+ SkipFirstDelimiter comma(",");
+ for (auto value : discreteValues) {
+ os << comma << value;
+ }
+ os << "]";
+ }
+}
+
+template <typename T>
+bool GetUnsigned(std::istream& is, T min, T max, T* value, std::string* error) {
+ if (PeekChar(is, error) == '-') {
+ *error = "Value is less than 0";
+ return false;
+ }
+
+ is >> std::noskipws >> *value;
+
+ if (is.fail()) {
+ *error = "Malformed";
+ return false;
+ }
+
+ if (*value < min) {
+ *error = "Value too small";
+ return false;
+ }
+
+ if (*value > max) {
+ *error = "Value too large";
+ return false;
+ }
+
+ return true;
+}
+
+static bool GetXYValue(std::istream& is, uint32_t* value, std::string* error) {
+ return GetUnsigned<uint32_t>(is, 1, 999999, value, error);
+}
+
+bool SdpImageattrAttributeList::XYRange::ParseDiscreteValues(
+ std::istream& is, std::string* error) {
+ do {
+ uint32_t value;
+ if (!GetXYValue(is, &value, error)) {
+ return false;
+ }
+ discreteValues.push_back(value);
+ } while (SkipChar(is, ',', error));
+
+ return SkipChar(is, ']', error);
+}
+
+bool SdpImageattrAttributeList::XYRange::ParseAfterMin(std::istream& is,
+ std::string* error) {
+ // We have already parsed "[320:", and now expect another uint
+ uint32_t value;
+ if (!GetXYValue(is, &value, error)) {
+ return false;
+ }
+
+ if (SkipChar(is, ':', error)) {
+ // Range with step eg [320:16:640]
+ step = value;
+ // Now |value| should be the max
+ if (!GetXYValue(is, &value, error)) {
+ return false;
+ }
+ }
+
+ max = value;
+ if (min >= max) {
+ *error = "Min is not smaller than max";
+ return false;
+ }
+
+ return SkipChar(is, ']', error);
+}
+
+bool SdpImageattrAttributeList::XYRange::ParseAfterBracket(std::istream& is,
+ std::string* error) {
+ // Either a range, or a list of discrete values
+ // [320:640], [320:16:640], or [320,640]
+ uint32_t value;
+ if (!GetXYValue(is, &value, error)) {
+ return false;
+ }
+
+ if (SkipChar(is, ':', error)) {
+ // Range - [640:480] or [640:16:480]
+ min = value;
+ return ParseAfterMin(is, error);
+ }
+
+ if (SkipChar(is, ',', error)) {
+ discreteValues.push_back(value);
+ return ParseDiscreteValues(is, error);
+ }
+
+ *error = "Expected \':\' or \',\'";
+ return false;
+}
+
+bool SdpImageattrAttributeList::XYRange::Parse(std::istream& is,
+ std::string* error) {
+ if (SkipChar(is, '[', error)) {
+ return ParseAfterBracket(is, error);
+ }
+
+ // Single discrete value
+ uint32_t value;
+ if (!GetXYValue(is, &value, error)) {
+ return false;
+ }
+ discreteValues.push_back(value);
+
+ return true;
+}
+
+static bool GetSPValue(std::istream& is, float* value, std::string* error) {
+ return GetUnsigned<float>(is, 0.1f, 9.9999f, value, error);
+}
+
+static bool GetQValue(std::istream& is, float* value, std::string* error) {
+ return GetUnsigned<float>(is, 0.0f, 1.0f, value, error);
+}
+
+bool SdpImageattrAttributeList::SRange::ParseDiscreteValues(
+ std::istream& is, std::string* error) {
+ do {
+ float value;
+ if (!GetSPValue(is, &value, error)) {
+ return false;
+ }
+ discreteValues.push_back(value);
+ } while (SkipChar(is, ',', error));
+
+ return SkipChar(is, ']', error);
+}
+
+bool SdpImageattrAttributeList::SRange::ParseAfterMin(std::istream& is,
+ std::string* error) {
+ if (!GetSPValue(is, &max, error)) {
+ return false;
+ }
+
+ if (min >= max) {
+ *error = "Min is not smaller than max";
+ return false;
+ }
+
+ return SkipChar(is, ']', error);
+}
+
+bool SdpImageattrAttributeList::SRange::ParseAfterBracket(std::istream& is,
+ std::string* error) {
+ // Either a range, or a list of discrete values
+ float value;
+ if (!GetSPValue(is, &value, error)) {
+ return false;
+ }
+
+ if (SkipChar(is, '-', error)) {
+ min = value;
+ return ParseAfterMin(is, error);
+ }
+
+ if (SkipChar(is, ',', error)) {
+ discreteValues.push_back(value);
+ return ParseDiscreteValues(is, error);
+ }
+
+ *error = "Expected either \'-\' or \',\'";
+ return false;
+}
+
+bool SdpImageattrAttributeList::SRange::Parse(std::istream& is,
+ std::string* error) {
+ if (SkipChar(is, '[', error)) {
+ return ParseAfterBracket(is, error);
+ }
+
+ // Single discrete value
+ float value;
+ if (!GetSPValue(is, &value, error)) {
+ return false;
+ }
+ discreteValues.push_back(value);
+ return true;
+}
+
+bool SdpImageattrAttributeList::PRange::Parse(std::istream& is,
+ std::string* error) {
+ if (!SkipChar(is, '[', error)) {
+ return false;
+ }
+
+ if (!GetSPValue(is, &min, error)) {
+ return false;
+ }
+
+ if (!SkipChar(is, '-', error)) {
+ return false;
+ }
+
+ if (!GetSPValue(is, &max, error)) {
+ return false;
+ }
+
+ if (min >= max) {
+ *error = "min must be smaller than max";
+ return false;
+ }
+
+ if (!SkipChar(is, ']', error)) {
+ return false;
+ }
+ return true;
+}
+
+void SdpImageattrAttributeList::SRange::Serialize(std::ostream& os) const {
+ os << std::setprecision(4) << std::fixed;
+ if (discreteValues.empty()) {
+ os << "[" << min << "-" << max << "]";
+ } else if (discreteValues.size() == 1) {
+ os << discreteValues.front();
+ } else {
+ os << "[";
+ SkipFirstDelimiter comma(",");
+ for (auto value : discreteValues) {
+ os << comma << value;
+ }
+ os << "]";
+ }
+}
+
+void SdpImageattrAttributeList::PRange::Serialize(std::ostream& os) const {
+ os << std::setprecision(4) << std::fixed;
+ os << "[" << min << "-" << max << "]";
+}
+
+static std::string ParseKey(std::istream& is, std::string* error) {
+ std::string token = ParseToken(is, "=", error);
+ if (!SkipChar(is, '=', error)) {
+ return "";
+ }
+ return token;
+}
+
+static bool SkipBraces(std::istream& is, std::string* error) {
+ if (PeekChar(is, error) != '[') {
+ *error = "Expected \'[\'";
+ return false;
+ }
+
+ size_t braceCount = 0;
+ do {
+ switch (PeekChar(is, error)) {
+ case '[':
+ ++braceCount;
+ break;
+ case ']':
+ --braceCount;
+ break;
+ default:
+ break;
+ }
+ is.get();
+ } while (braceCount && is);
+
+ if (!is) {
+ *error = "Expected closing brace";
+ return false;
+ }
+
+ return true;
+}
+
+// Assumptions:
+// 1. If the value contains '[' or ']', they are balanced.
+// 2. The value contains no ',' outside of brackets.
+static bool SkipValue(std::istream& is, std::string* error) {
+ while (is) {
+ switch (PeekChar(is, error)) {
+ case ',':
+ case ']':
+ return true;
+ case '[':
+ if (!SkipBraces(is, error)) {
+ return false;
+ }
+ break;
+ default:
+ is.get();
+ }
+ }
+
+ *error = "No closing \']\' on set";
+ return false;
+}
+
+bool SdpImageattrAttributeList::Set::Parse(std::istream& is,
+ std::string* error) {
+ if (!SkipChar(is, '[', error)) {
+ return false;
+ }
+
+ if (ParseKey(is, error) != "x") {
+ *error = "Expected x=";
+ return false;
+ }
+
+ if (!xRange.Parse(is, error)) {
+ return false;
+ }
+
+ if (!SkipChar(is, ',', error)) {
+ return false;
+ }
+
+ if (ParseKey(is, error) != "y") {
+ *error = "Expected y=";
+ return false;
+ }
+
+ if (!yRange.Parse(is, error)) {
+ return false;
+ }
+
+ qValue = 0.5f; // default
+
+ bool gotSar = false;
+ bool gotPar = false;
+ bool gotQ = false;
+
+ while (SkipChar(is, ',', error)) {
+ std::string key = ParseKey(is, error);
+ if (key.empty()) {
+ *error = "Expected key-value";
+ return false;
+ }
+
+ if (key == "sar") {
+ if (gotSar) {
+ *error = "Extra sar parameter";
+ return false;
+ }
+ gotSar = true;
+ if (!sRange.Parse(is, error)) {
+ return false;
+ }
+ } else if (key == "par") {
+ if (gotPar) {
+ *error = "Extra par parameter";
+ return false;
+ }
+ gotPar = true;
+ if (!pRange.Parse(is, error)) {
+ return false;
+ }
+ } else if (key == "q") {
+ if (gotQ) {
+ *error = "Extra q parameter";
+ return false;
+ }
+ gotQ = true;
+ if (!GetQValue(is, &qValue, error)) {
+ return false;
+ }
+ } else {
+ if (!SkipValue(is, error)) {
+ return false;
+ }
+ }
+ }
+
+ return SkipChar(is, ']', error);
+}
+
+void SdpImageattrAttributeList::Set::Serialize(std::ostream& os) const {
+ os << "[x=";
+ xRange.Serialize(os);
+ os << ",y=";
+ yRange.Serialize(os);
+ if (sRange.IsSet()) {
+ os << ",sar=";
+ sRange.Serialize(os);
+ }
+ if (pRange.IsSet()) {
+ os << ",par=";
+ pRange.Serialize(os);
+ }
+ if (qValue >= 0) {
+ os << std::setprecision(2) << std::fixed << ",q=" << qValue;
+ }
+ os << "]";
+}
+
+bool SdpImageattrAttributeList::Imageattr::ParseSets(std::istream& is,
+ std::string* error) {
+ std::string type = ParseToken(is, " \t", error);
+
+ bool* isAll = nullptr;
+ std::vector<Set>* sets = nullptr;
+
+ if (type == "send") {
+ isAll = &sendAll;
+ sets = &sendSets;
+ } else if (type == "recv") {
+ isAll = &recvAll;
+ sets = &recvSets;
+ } else {
+ *error = "Unknown type, must be either send or recv";
+ return false;
+ }
+
+ if (*isAll || !sets->empty()) {
+ *error = "Multiple send or recv set lists";
+ return false;
+ }
+
+ is >> std::ws;
+ if (SkipChar(is, '*', error)) {
+ *isAll = true;
+ return true;
+ }
+
+ do {
+ Set set;
+ if (!set.Parse(is, error)) {
+ return false;
+ }
+
+ sets->push_back(set);
+ is >> std::ws;
+ } while (PeekChar(is, error) == '[');
+
+ return true;
+}
+
+bool SdpImageattrAttributeList::Imageattr::Parse(std::istream& is,
+ std::string* error) {
+ if (!SkipChar(is, '*', error)) {
+ uint16_t value;
+ if (!GetUnsigned<uint16_t>(is, 0, UINT16_MAX, &value, error)) {
+ return false;
+ }
+ pt = Some(value);
+ }
+
+ is >> std::ws;
+ if (!ParseSets(is, error)) {
+ return false;
+ }
+
+ // There might be a second one
+ is >> std::ws;
+ if (is.eof()) {
+ return true;
+ }
+
+ if (!ParseSets(is, error)) {
+ return false;
+ }
+
+ is >> std::ws;
+ if (!is.eof()) {
+ *error = "Trailing characters";
+ return false;
+ }
+
+ return true;
+}
+
+void SdpImageattrAttributeList::Imageattr::Serialize(std::ostream& os) const {
+ if (pt.isSome()) {
+ os << *pt;
+ } else {
+ os << "*";
+ }
+
+ if (sendAll) {
+ os << " send *";
+ } else if (!sendSets.empty()) {
+ os << " send";
+ for (auto& set : sendSets) {
+ os << " ";
+ set.Serialize(os);
+ }
+ }
+
+ if (recvAll) {
+ os << " recv *";
+ } else if (!recvSets.empty()) {
+ os << " recv";
+ for (auto& set : recvSets) {
+ os << " ";
+ set.Serialize(os);
+ }
+ }
+}
+
+void SdpImageattrAttributeList::Serialize(std::ostream& os) const {
+ for (auto& imageattr : mImageattrs) {
+ os << "a=" << mType << ":";
+ imageattr.Serialize(os);
+ os << CRLF;
+ }
+}
+
+bool SdpImageattrAttributeList::PushEntry(const std::string& raw,
+ std::string* error,
+ size_t* errorPos) {
+ std::istringstream is(raw);
+
+ Imageattr imageattr;
+ if (!imageattr.Parse(is, error)) {
+ is.clear();
+ *errorPos = is.tellg();
+ return false;
+ }
+
+ mImageattrs.push_back(imageattr);
+ return true;
+}
+
+void SdpMsidAttributeList::Serialize(std::ostream& os) const {
+ for (auto i = mMsids.begin(); i != mMsids.end(); ++i) {
+ os << "a=" << mType << ":" << i->identifier;
+ if (i->appdata.length()) {
+ os << " " << i->appdata;
+ }
+ os << CRLF;
+ }
+}
+
+void SdpMsidSemanticAttributeList::Serialize(std::ostream& os) const {
+ for (auto i = mMsidSemantics.begin(); i != mMsidSemantics.end(); ++i) {
+ os << "a=" << mType << ":" << i->semantic;
+ for (auto j = i->msids.begin(); j != i->msids.end(); ++j) {
+ os << " " << *j;
+ }
+ os << CRLF;
+ }
+}
+
+void SdpRemoteCandidatesAttribute::Serialize(std::ostream& os) const {
+ if (mCandidates.empty()) {
+ return;
+ }
+
+ os << "a=" << mType;
+ for (auto i = mCandidates.begin(); i != mCandidates.end(); i++) {
+ os << (i == mCandidates.begin() ? ":" : " ") << i->id << " " << i->address
+ << " " << i->port;
+ }
+ os << CRLF;
+}
+
+// Remove this function. See Bug 1469702
+bool SdpRidAttributeList::Rid::ParseParameters(std::istream& is,
+ std::string* error) {
+ if (!PeekChar(is, error)) {
+ // No parameters
+ return true;
+ }
+
+ do {
+ is >> std::ws;
+ std::string key = ParseKey(is, error);
+ if (key.empty()) {
+ return false; // Illegal trailing cruft
+ }
+
+ // This allows pt= to appear anywhere, instead of only at the beginning, but
+ // this ends up being significantly less code.
+ if (key == "pt") {
+ if (!ParseFormats(is, error)) {
+ return false;
+ }
+ } else if (key == "max-width") {
+ if (!GetUnsigned<uint32_t>(is, 0, UINT32_MAX, &constraints.maxWidth,
+ error)) {
+ return false;
+ }
+ } else if (key == "max-height") {
+ if (!GetUnsigned<uint32_t>(is, 0, UINT32_MAX, &constraints.maxHeight,
+ error)) {
+ return false;
+ }
+ } else if (key == "max-fps") {
+ uint32_t maxFps;
+ if (!GetUnsigned<uint32_t>(is, 0, UINT32_MAX, &maxFps, error)) {
+ return false;
+ }
+ constraints.maxFps = Some(maxFps);
+ } else if (key == "max-fs") {
+ if (!GetUnsigned<uint32_t>(is, 0, UINT32_MAX, &constraints.maxFs,
+ error)) {
+ return false;
+ }
+ } else if (key == "max-br") {
+ if (!GetUnsigned<uint32_t>(is, 0, UINT32_MAX, &constraints.maxBr,
+ error)) {
+ return false;
+ }
+ } else if (key == "max-pps") {
+ if (!GetUnsigned<uint32_t>(is, 0, UINT32_MAX, &constraints.maxPps,
+ error)) {
+ return false;
+ }
+ } else if (key == "depend") {
+ if (!ParseDepend(is, error)) {
+ return false;
+ }
+ } else {
+ (void)ParseToken(is, ";", error);
+ }
+ } while (SkipChar(is, ';', error));
+ return true;
+}
+
+// Remove this function. See Bug 1469702
+bool SdpRidAttributeList::Rid::ParseDepend(std::istream& is,
+ std::string* error) {
+ do {
+ std::string id = ParseToken(is, ",;", error);
+ if (id.empty()) {
+ return false;
+ }
+ dependIds.push_back(id);
+ } while (SkipChar(is, ',', error));
+
+ return true;
+}
+
+// Remove this function. See Bug 1469702
+bool SdpRidAttributeList::Rid::ParseFormats(std::istream& is,
+ std::string* error) {
+ do {
+ uint16_t fmt;
+ if (!GetUnsigned<uint16_t>(is, 0, 127, &fmt, error)) {
+ return false;
+ }
+ formats.push_back(fmt);
+ } while (SkipChar(is, ',', error));
+
+ return true;
+}
+
+void SdpRidAttributeList::Rid::SerializeParameters(std::ostream& os) const {
+ if (!HasParameters()) {
+ return;
+ }
+
+ os << " ";
+
+ SkipFirstDelimiter semic(";");
+
+ if (!formats.empty()) {
+ os << semic << "pt=";
+ SkipFirstDelimiter comma(",");
+ for (uint16_t fmt : formats) {
+ os << comma << fmt;
+ }
+ }
+
+ if (constraints.maxWidth) {
+ os << semic << "max-width=" << constraints.maxWidth;
+ }
+
+ if (constraints.maxHeight) {
+ os << semic << "max-height=" << constraints.maxHeight;
+ }
+
+ if (constraints.maxFps) {
+ os << semic << "max-fps=" << constraints.maxFps;
+ }
+
+ if (constraints.maxFs) {
+ os << semic << "max-fs=" << constraints.maxFs;
+ }
+
+ if (constraints.maxBr) {
+ os << semic << "max-br=" << constraints.maxBr;
+ }
+
+ if (constraints.maxPps) {
+ os << semic << "max-pps=" << constraints.maxPps;
+ }
+
+ if (!dependIds.empty()) {
+ os << semic << "depend=";
+ SkipFirstDelimiter comma(",");
+ for (const std::string& id : dependIds) {
+ os << comma << id;
+ }
+ }
+}
+
+// Remove this function. See Bug 1469702
+bool SdpRidAttributeList::Rid::Parse(std::istream& is, std::string* error) {
+ id = ParseToken(is, " ", error);
+ if (!CheckRidValidity(id, error)) {
+ return false;
+ }
+
+ is >> std::ws;
+ std::string directionToken = ParseToken(is, " ", error);
+ if (directionToken == "send") {
+ direction = sdp::kSend;
+ } else if (directionToken == "recv") {
+ direction = sdp::kRecv;
+ } else {
+ *error = "Invalid direction, must be either send or recv";
+ return false;
+ }
+
+ return ParseParameters(is, error);
+}
+
+static std::bitset<256> GetAllowedRidCharacters() {
+ // From RFC 8851:
+ // rid-id = 1*(alpha-numeric / "-" / "_")
+ std::bitset<256> result;
+ for (unsigned char c = 'a'; c <= 'z'; ++c) {
+ result.set(c);
+ }
+ for (unsigned char c = 'A'; c <= 'Z'; ++c) {
+ result.set(c);
+ }
+ for (unsigned char c = '0'; c <= '9'; ++c) {
+ result.set(c);
+ }
+ // NOTE: RFC 8851 says these are allowed, but RFC 8852 says they are not
+ // https://www.rfc-editor.org/errata/eid7132
+ // result.set('-');
+ // result.set('_');
+ return result;
+}
+
+/* static */
+bool SdpRidAttributeList::CheckRidValidity(const std::string& aRid,
+ std::string* aError) {
+ if (aRid.empty()) {
+ *aError = "Rid must be non-empty (according to RFC 8851)";
+ return false;
+ }
+
+ // We need to check against a maximum length, but that is nowhere
+ // specified in webrtc-pc right now.
+ if (aRid.size() > 255) {
+ *aError = "Rid can be at most 255 characters long (according to RFC 8852)";
+ return false;
+ }
+
+ // TODO: Right now, if the rid is longer than kMaxRidLength, we don't treat it
+ // as a parse error, since the grammar does not have this restriction.
+ // Instead, our JSEP code ignores rids that exceed this limit. However, there
+ // is a possibility that the IETF grammar (in RFC 8852) will change the limit
+ // from 255 to 16, in which case we will need to revise this code.
+
+ static const std::bitset<256> allowed = GetAllowedRidCharacters();
+ for (unsigned char c : aRid) {
+ if (!allowed[c]) {
+ *aError =
+ "Rid can contain only alphanumeric characters (according to RFC "
+ "8852)";
+ return false;
+ }
+ }
+
+ return true;
+}
+
+// This can be overridden if necessary
+size_t SdpRidAttributeList::kMaxRidLength = 255;
+
+void SdpRidAttributeList::Rid::Serialize(std::ostream& os) const {
+ os << id << " " << direction;
+ SerializeParameters(os);
+}
+
+bool SdpRidAttributeList::Rid::HasFormat(const std::string& format) const {
+ if (formats.empty()) {
+ return true;
+ }
+
+ uint16_t formatAsInt;
+ if (!SdpHelper::GetPtAsInt(format, &formatAsInt)) {
+ return false;
+ }
+
+ return (std::find(formats.begin(), formats.end(), formatAsInt) !=
+ formats.end());
+}
+
+void SdpRidAttributeList::Serialize(std::ostream& os) const {
+ for (const Rid& rid : mRids) {
+ os << "a=" << mType << ":";
+ rid.Serialize(os);
+ os << CRLF;
+ }
+}
+
+// Remove this function. See Bug 1469702
+bool SdpRidAttributeList::PushEntry(const std::string& raw, std::string* error,
+ size_t* errorPos) {
+ std::istringstream is(raw);
+
+ Rid rid;
+ if (!rid.Parse(is, error)) {
+ is.clear();
+ *errorPos = is.tellg();
+ return false;
+ }
+
+ mRids.push_back(rid);
+ return true;
+}
+
+void SdpRidAttributeList::PushEntry(const std::string& id, sdp::Direction dir,
+ const std::vector<uint16_t>& formats,
+ const EncodingConstraints& constraints,
+ const std::vector<std::string>& dependIds) {
+ SdpRidAttributeList::Rid rid;
+
+ rid.id = id;
+ rid.direction = dir;
+ rid.formats = formats;
+ rid.constraints = constraints;
+ rid.dependIds = dependIds;
+
+ mRids.push_back(std::move(rid));
+}
+
+void SdpRtcpAttribute::Serialize(std::ostream& os) const {
+ os << "a=" << mType << ":" << mPort;
+ if (!mAddress.empty()) {
+ os << " " << mNetType << " " << mAddrType << " " << mAddress;
+ }
+ os << CRLF;
+}
+
+const char* SdpRtcpFbAttributeList::pli = "pli";
+const char* SdpRtcpFbAttributeList::sli = "sli";
+const char* SdpRtcpFbAttributeList::rpsi = "rpsi";
+const char* SdpRtcpFbAttributeList::app = "app";
+
+const char* SdpRtcpFbAttributeList::fir = "fir";
+const char* SdpRtcpFbAttributeList::tmmbr = "tmmbr";
+const char* SdpRtcpFbAttributeList::tstr = "tstr";
+const char* SdpRtcpFbAttributeList::vbcm = "vbcm";
+
+void SdpRtcpFbAttributeList::Serialize(std::ostream& os) const {
+ for (auto i = mFeedbacks.begin(); i != mFeedbacks.end(); ++i) {
+ os << "a=" << mType << ":" << i->pt << " " << i->type;
+ if (i->parameter.length()) {
+ os << " " << i->parameter;
+ if (i->extra.length()) {
+ os << " " << i->extra;
+ }
+ }
+ os << CRLF;
+ }
+}
+
+static bool ShouldSerializeChannels(SdpRtpmapAttributeList::CodecType type) {
+ switch (type) {
+ case SdpRtpmapAttributeList::kOpus:
+ case SdpRtpmapAttributeList::kG722:
+ return true;
+ case SdpRtpmapAttributeList::kPCMU:
+ case SdpRtpmapAttributeList::kPCMA:
+ case SdpRtpmapAttributeList::kVP8:
+ case SdpRtpmapAttributeList::kVP9:
+ case SdpRtpmapAttributeList::kiLBC:
+ case SdpRtpmapAttributeList::kiSAC:
+ case SdpRtpmapAttributeList::kH264:
+ case SdpRtpmapAttributeList::kRed:
+ case SdpRtpmapAttributeList::kUlpfec:
+ case SdpRtpmapAttributeList::kTelephoneEvent:
+ case SdpRtpmapAttributeList::kRtx:
+ return false;
+ case SdpRtpmapAttributeList::kOtherCodec:
+ return true;
+ }
+ MOZ_CRASH();
+}
+
+void SdpRtpmapAttributeList::Serialize(std::ostream& os) const {
+ for (auto i = mRtpmaps.begin(); i != mRtpmaps.end(); ++i) {
+ os << "a=" << mType << ":" << i->pt << " " << i->name << "/" << i->clock;
+ if (i->channels && ShouldSerializeChannels(i->codec)) {
+ os << "/" << i->channels;
+ }
+ os << CRLF;
+ }
+}
+
+void SdpSctpmapAttributeList::Serialize(std::ostream& os) const {
+ for (auto i = mSctpmaps.begin(); i != mSctpmaps.end(); ++i) {
+ os << "a=" << mType << ":" << i->pt << " " << i->name << " " << i->streams
+ << CRLF;
+ }
+}
+
+void SdpSetupAttribute::Serialize(std::ostream& os) const {
+ os << "a=" << mType << ":" << mRole << CRLF;
+}
+
+void SdpSimulcastAttribute::Version::Serialize(std::ostream& os) const {
+ SkipFirstDelimiter comma(",");
+ for (const Encoding& choice : choices) {
+ os << comma;
+ if (choice.paused) {
+ os << '~';
+ }
+ os << choice.rid;
+ }
+}
+
+bool SdpSimulcastAttribute::Version::Parse(std::istream& is,
+ std::string* error) {
+ do {
+ bool paused = SkipChar(is, '~', error);
+ std::string value = ParseToken(is, ",; ", error);
+ if (value.empty()) {
+ *error = "Missing rid";
+ return false;
+ }
+ if (!SdpRidAttributeList::CheckRidValidity(value, error)) {
+ return false;
+ }
+ choices.push_back(Encoding(value, paused));
+ } while (SkipChar(is, ',', error));
+
+ return true;
+}
+
+void SdpSimulcastAttribute::Versions::Serialize(std::ostream& os) const {
+ SkipFirstDelimiter semic(";");
+ for (const Version& version : *this) {
+ if (!version.IsSet()) {
+ continue;
+ }
+ os << semic;
+ version.Serialize(os);
+ }
+}
+
+bool SdpSimulcastAttribute::Versions::Parse(std::istream& is,
+ std::string* error) {
+ do {
+ Version version;
+ if (!version.Parse(is, error)) {
+ return false;
+ }
+ push_back(version);
+ } while (SkipChar(is, ';', error));
+
+ return true;
+}
+
+void SdpSimulcastAttribute::Serialize(std::ostream& os) const {
+ MOZ_ASSERT(sendVersions.IsSet() || recvVersions.IsSet());
+
+ os << "a=" << mType << ":";
+
+ if (sendVersions.IsSet()) {
+ os << "send ";
+ sendVersions.Serialize(os);
+ }
+
+ if (recvVersions.IsSet()) {
+ if (sendVersions.IsSet()) {
+ os << " ";
+ }
+ os << "recv ";
+ recvVersions.Serialize(os);
+ }
+
+ os << CRLF;
+}
+
+bool SdpSimulcastAttribute::Parse(std::istream& is, std::string* error) {
+ bool gotRecv = false;
+ bool gotSend = false;
+
+ while (true) {
+ is >> std::ws;
+ std::string token = ParseToken(is, " \t", error);
+ if (token.empty()) {
+ break;
+ }
+
+ if (token == "send") {
+ if (gotSend) {
+ *error = "Already got a send list";
+ return false;
+ }
+ gotSend = true;
+
+ is >> std::ws;
+ if (!sendVersions.Parse(is, error)) {
+ return false;
+ }
+ } else if (token == "recv") {
+ if (gotRecv) {
+ *error = "Already got a recv list";
+ return false;
+ }
+ gotRecv = true;
+
+ is >> std::ws;
+ if (!recvVersions.Parse(is, error)) {
+ return false;
+ }
+ } else {
+ *error = "Type must be either 'send' or 'recv'";
+ return false;
+ }
+ }
+
+ if (!gotSend && !gotRecv) {
+ *error = "Empty simulcast attribute";
+ return false;
+ }
+
+ return true;
+}
+
+void SdpSsrcAttributeList::Serialize(std::ostream& os) const {
+ for (auto i = mSsrcs.begin(); i != mSsrcs.end(); ++i) {
+ os << "a=" << mType << ":" << i->ssrc << " " << i->attribute << CRLF;
+ }
+}
+
+void SdpSsrcGroupAttributeList::Serialize(std::ostream& os) const {
+ for (auto i = mSsrcGroups.begin(); i != mSsrcGroups.end(); ++i) {
+ os << "a=" << mType << ":" << i->semantics;
+ for (auto j = i->ssrcs.begin(); j != i->ssrcs.end(); ++j) {
+ os << " " << (*j);
+ }
+ os << CRLF;
+ }
+}
+
+void SdpMultiStringAttribute::Serialize(std::ostream& os) const {
+ for (auto i = mValues.begin(); i != mValues.end(); ++i) {
+ os << "a=" << mType << ":" << *i << CRLF;
+ }
+}
+
+void SdpOptionsAttribute::Serialize(std::ostream& os) const {
+ if (mValues.empty()) {
+ return;
+ }
+
+ os << "a=" << mType << ":";
+
+ for (auto i = mValues.begin(); i != mValues.end(); ++i) {
+ if (i != mValues.begin()) {
+ os << " ";
+ }
+ os << *i;
+ }
+ os << CRLF;
+}
+
+void SdpOptionsAttribute::Load(const std::string& value) {
+ size_t start = 0;
+ size_t end = value.find(' ');
+ while (end != std::string::npos) {
+ PushEntry(value.substr(start, end));
+ start = end + 1;
+ end = value.find(' ', start);
+ }
+ PushEntry(value.substr(start));
+}
+
+void SdpFlagAttribute::Serialize(std::ostream& os) const {
+ os << "a=" << mType << CRLF;
+}
+
+void SdpStringAttribute::Serialize(std::ostream& os) const {
+ os << "a=" << mType << ":" << mValue << CRLF;
+}
+
+void SdpNumberAttribute::Serialize(std::ostream& os) const {
+ os << "a=" << mType << ":" << mValue << CRLF;
+}
+
+bool SdpAttribute::IsAllowedAtMediaLevel(AttributeType type) {
+ switch (type) {
+ case kBundleOnlyAttribute:
+ return true;
+ case kCandidateAttribute:
+ return true;
+ case kConnectionAttribute:
+ return true;
+ case kDirectionAttribute:
+ return true;
+ case kDtlsMessageAttribute:
+ return false;
+ case kEndOfCandidatesAttribute:
+ return true;
+ case kExtmapAttribute:
+ return true;
+ case kFingerprintAttribute:
+ return true;
+ case kFmtpAttribute:
+ return true;
+ case kGroupAttribute:
+ return false;
+ case kIceLiteAttribute:
+ return false;
+ case kIceMismatchAttribute:
+ return true;
+ // RFC 5245 says this is session-level only, but
+ // draft-ietf-mmusic-ice-sip-sdp-03 updates this to allow at the media
+ // level.
+ case kIceOptionsAttribute:
+ return true;
+ case kIcePwdAttribute:
+ return true;
+ case kIceUfragAttribute:
+ return true;
+ case kIdentityAttribute:
+ return false;
+ case kImageattrAttribute:
+ return true;
+ case kLabelAttribute:
+ return true;
+ case kMaxptimeAttribute:
+ return true;
+ case kMidAttribute:
+ return true;
+ case kMsidAttribute:
+ return true;
+ case kMsidSemanticAttribute:
+ return false;
+ case kPtimeAttribute:
+ return true;
+ case kRemoteCandidatesAttribute:
+ return true;
+ case kRidAttribute:
+ return true;
+ case kRtcpAttribute:
+ return true;
+ case kRtcpFbAttribute:
+ return true;
+ case kRtcpMuxAttribute:
+ return true;
+ case kRtcpRsizeAttribute:
+ return true;
+ case kRtpmapAttribute:
+ return true;
+ case kSctpmapAttribute:
+ return true;
+ case kSetupAttribute:
+ return true;
+ case kSimulcastAttribute:
+ return true;
+ case kSsrcAttribute:
+ return true;
+ case kSsrcGroupAttribute:
+ return true;
+ case kSctpPortAttribute:
+ return true;
+ case kMaxMessageSizeAttribute:
+ return true;
+ }
+ MOZ_CRASH("Unknown attribute type");
+}
+
+bool SdpAttribute::IsAllowedAtSessionLevel(AttributeType type) {
+ switch (type) {
+ case kBundleOnlyAttribute:
+ return false;
+ case kCandidateAttribute:
+ return false;
+ case kConnectionAttribute:
+ return true;
+ case kDirectionAttribute:
+ return true;
+ case kDtlsMessageAttribute:
+ return true;
+ case kEndOfCandidatesAttribute:
+ return true;
+ case kExtmapAttribute:
+ return true;
+ case kFingerprintAttribute:
+ return true;
+ case kFmtpAttribute:
+ return false;
+ case kGroupAttribute:
+ return true;
+ case kIceLiteAttribute:
+ return true;
+ case kIceMismatchAttribute:
+ return false;
+ case kIceOptionsAttribute:
+ return true;
+ case kIcePwdAttribute:
+ return true;
+ case kIceUfragAttribute:
+ return true;
+ case kIdentityAttribute:
+ return true;
+ case kImageattrAttribute:
+ return false;
+ case kLabelAttribute:
+ return false;
+ case kMaxptimeAttribute:
+ return false;
+ case kMidAttribute:
+ return false;
+ case kMsidSemanticAttribute:
+ return true;
+ case kMsidAttribute:
+ return false;
+ case kPtimeAttribute:
+ return false;
+ case kRemoteCandidatesAttribute:
+ return false;
+ case kRidAttribute:
+ return false;
+ case kRtcpAttribute:
+ return false;
+ case kRtcpFbAttribute:
+ return false;
+ case kRtcpMuxAttribute:
+ return false;
+ case kRtcpRsizeAttribute:
+ return false;
+ case kRtpmapAttribute:
+ return false;
+ case kSctpmapAttribute:
+ return false;
+ case kSetupAttribute:
+ return true;
+ case kSimulcastAttribute:
+ return false;
+ case kSsrcAttribute:
+ return false;
+ case kSsrcGroupAttribute:
+ return false;
+ case kSctpPortAttribute:
+ return false;
+ case kMaxMessageSizeAttribute:
+ return false;
+ }
+ MOZ_CRASH("Unknown attribute type");
+}
+
+const std::string SdpAttribute::GetAttributeTypeString(AttributeType type) {
+ switch (type) {
+ case kBundleOnlyAttribute:
+ return "bundle-only";
+ case kCandidateAttribute:
+ return "candidate";
+ case kConnectionAttribute:
+ return "connection";
+ case kDtlsMessageAttribute:
+ return "dtls-message";
+ case kEndOfCandidatesAttribute:
+ return "end-of-candidates";
+ case kExtmapAttribute:
+ return "extmap";
+ case kFingerprintAttribute:
+ return "fingerprint";
+ case kFmtpAttribute:
+ return "fmtp";
+ case kGroupAttribute:
+ return "group";
+ case kIceLiteAttribute:
+ return "ice-lite";
+ case kIceMismatchAttribute:
+ return "ice-mismatch";
+ case kIceOptionsAttribute:
+ return "ice-options";
+ case kIcePwdAttribute:
+ return "ice-pwd";
+ case kIceUfragAttribute:
+ return "ice-ufrag";
+ case kIdentityAttribute:
+ return "identity";
+ case kImageattrAttribute:
+ return "imageattr";
+ case kLabelAttribute:
+ return "label";
+ case kMaxptimeAttribute:
+ return "maxptime";
+ case kMidAttribute:
+ return "mid";
+ case kMsidAttribute:
+ return "msid";
+ case kMsidSemanticAttribute:
+ return "msid-semantic";
+ case kPtimeAttribute:
+ return "ptime";
+ case kRemoteCandidatesAttribute:
+ return "remote-candidates";
+ case kRidAttribute:
+ return "rid";
+ case kRtcpAttribute:
+ return "rtcp";
+ case kRtcpFbAttribute:
+ return "rtcp-fb";
+ case kRtcpMuxAttribute:
+ return "rtcp-mux";
+ case kRtcpRsizeAttribute:
+ return "rtcp-rsize";
+ case kRtpmapAttribute:
+ return "rtpmap";
+ case kSctpmapAttribute:
+ return "sctpmap";
+ case kSetupAttribute:
+ return "setup";
+ case kSimulcastAttribute:
+ return "simulcast";
+ case kSsrcAttribute:
+ return "ssrc";
+ case kSsrcGroupAttribute:
+ return "ssrc-group";
+ case kSctpPortAttribute:
+ return "sctp-port";
+ case kMaxMessageSizeAttribute:
+ return "max-message-size";
+ case kDirectionAttribute:
+ MOZ_CRASH("kDirectionAttribute not valid here");
+ }
+ MOZ_CRASH("Unknown attribute type");
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/sdp/SdpAttribute.h b/dom/media/webrtc/sdp/SdpAttribute.h
new file mode 100644
index 0000000000..6af497ac18
--- /dev/null
+++ b/dom/media/webrtc/sdp/SdpAttribute.h
@@ -0,0 +1,1907 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _SDPATTRIBUTE_H_
+#define _SDPATTRIBUTE_H_
+
+#include <algorithm>
+#include <cctype>
+#include <vector>
+#include <ostream>
+#include <sstream>
+#include <cstring>
+#include <iomanip>
+#include <string>
+
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Maybe.h"
+
+#include "sdp/SdpEnum.h"
+#include "common/EncodingConstraints.h"
+
+namespace mozilla {
+
+/**
+ * Base class for SDP attributes
+ */
+class SdpAttribute {
+ public:
+ enum AttributeType {
+ kFirstAttribute = 0,
+ kBundleOnlyAttribute = 0,
+ kCandidateAttribute,
+ kConnectionAttribute,
+ kDirectionAttribute,
+ kDtlsMessageAttribute,
+ kEndOfCandidatesAttribute,
+ kExtmapAttribute,
+ kFingerprintAttribute,
+ kFmtpAttribute,
+ kGroupAttribute,
+ kIceLiteAttribute,
+ kIceMismatchAttribute,
+ kIceOptionsAttribute,
+ kIcePwdAttribute,
+ kIceUfragAttribute,
+ kIdentityAttribute,
+ kImageattrAttribute,
+ kLabelAttribute,
+ kMaxptimeAttribute,
+ kMidAttribute,
+ kMsidAttribute,
+ kMsidSemanticAttribute,
+ kPtimeAttribute,
+ kRemoteCandidatesAttribute,
+ kRidAttribute,
+ kRtcpAttribute,
+ kRtcpFbAttribute,
+ kRtcpMuxAttribute,
+ kRtcpRsizeAttribute,
+ kRtpmapAttribute,
+ kSctpmapAttribute,
+ kSetupAttribute,
+ kSimulcastAttribute,
+ kSsrcAttribute,
+ kSsrcGroupAttribute,
+ kSctpPortAttribute,
+ kMaxMessageSizeAttribute,
+ kLastAttribute = kMaxMessageSizeAttribute
+ };
+
+ explicit SdpAttribute(AttributeType type) : mType(type) {}
+ virtual ~SdpAttribute() {}
+
+ virtual SdpAttribute* Clone() const = 0;
+
+ AttributeType GetType() const { return mType; }
+
+ virtual void Serialize(std::ostream&) const = 0;
+
+ static bool IsAllowedAtSessionLevel(AttributeType type);
+ static bool IsAllowedAtMediaLevel(AttributeType type);
+ static const std::string GetAttributeTypeString(AttributeType type);
+
+ protected:
+ AttributeType mType;
+};
+
+inline std::ostream& operator<<(std::ostream& os, const SdpAttribute& attr) {
+ attr.Serialize(os);
+ return os;
+}
+
+inline std::ostream& operator<<(std::ostream& os,
+ const SdpAttribute::AttributeType type) {
+ os << SdpAttribute::GetAttributeTypeString(type);
+ return os;
+}
+
+///////////////////////////////////////////////////////////////////////////
+// a=candidate, RFC5245
+//-------------------------------------------------------------------------
+//
+// candidate-attribute = "candidate" ":" foundation SP component-id SP
+// transport SP
+// priority SP
+// connection-address SP ;from RFC 4566
+// port ;port from RFC 4566
+// SP cand-type
+// [SP rel-addr]
+// [SP rel-port]
+// *(SP extension-att-name SP
+// extension-att-value)
+// foundation = 1*32ice-char
+// component-id = 1*5DIGIT
+// transport = "UDP" / transport-extension
+// transport-extension = token ; from RFC 3261
+// priority = 1*10DIGIT
+// cand-type = "typ" SP candidate-types
+// candidate-types = "host" / "srflx" / "prflx" / "relay" / token
+// rel-addr = "raddr" SP connection-address
+// rel-port = "rport" SP port
+// extension-att-name = byte-string ;from RFC 4566
+// extension-att-value = byte-string
+// ice-char = ALPHA / DIGIT / "+" / "/"
+
+// We use a SdpMultiStringAttribute for candidates
+
+///////////////////////////////////////////////////////////////////////////
+// a=connection, RFC4145
+//-------------------------------------------------------------------------
+// connection-attr = "a=connection:" conn-value
+// conn-value = "new" / "existing"
+class SdpConnectionAttribute : public SdpAttribute {
+ public:
+ enum ConnValue { kNew, kExisting };
+
+ explicit SdpConnectionAttribute(SdpConnectionAttribute::ConnValue value)
+ : SdpAttribute(kConnectionAttribute), mValue(value) {}
+
+ SdpAttribute* Clone() const override {
+ return new SdpConnectionAttribute(*this);
+ }
+
+ virtual void Serialize(std::ostream& os) const override;
+
+ ConnValue mValue;
+};
+
+inline std::ostream& operator<<(std::ostream& os,
+ SdpConnectionAttribute::ConnValue c) {
+ switch (c) {
+ case SdpConnectionAttribute::kNew:
+ os << "new";
+ break;
+ case SdpConnectionAttribute::kExisting:
+ os << "existing";
+ break;
+ default:
+ MOZ_ASSERT(false);
+ os << "?";
+ }
+ return os;
+}
+
+///////////////////////////////////////////////////////////////////////////
+// a=sendrecv / a=sendonly / a=recvonly / a=inactive, RFC 4566
+//-------------------------------------------------------------------------
+class SdpDirectionAttribute : public SdpAttribute {
+ public:
+ enum Direction {
+ kInactive = 0,
+ kSendonly = sdp::kSend,
+ kRecvonly = sdp::kRecv,
+ kSendrecv = sdp::kSend | sdp::kRecv
+ };
+
+ explicit SdpDirectionAttribute(Direction value)
+ : SdpAttribute(kDirectionAttribute), mValue(value) {}
+
+ SdpAttribute* Clone() const override {
+ return new SdpDirectionAttribute(*this);
+ }
+
+ virtual void Serialize(std::ostream& os) const override;
+
+ Direction mValue;
+};
+
+inline std::ostream& operator<<(std::ostream& os,
+ SdpDirectionAttribute::Direction d) {
+ switch (d) {
+ case SdpDirectionAttribute::kSendonly:
+ os << "sendonly";
+ break;
+ case SdpDirectionAttribute::kRecvonly:
+ os << "recvonly";
+ break;
+ case SdpDirectionAttribute::kSendrecv:
+ os << "sendrecv";
+ break;
+ case SdpDirectionAttribute::kInactive:
+ os << "inactive";
+ break;
+ default:
+ MOZ_ASSERT(false);
+ os << "?";
+ }
+ return os;
+}
+
+inline SdpDirectionAttribute::Direction reverse(
+ SdpDirectionAttribute::Direction d) {
+ switch (d) {
+ case SdpDirectionAttribute::Direction::kInactive:
+ return SdpDirectionAttribute::Direction::kInactive;
+ case SdpDirectionAttribute::Direction::kSendonly:
+ return SdpDirectionAttribute::Direction::kRecvonly;
+ case SdpDirectionAttribute::Direction::kRecvonly:
+ return SdpDirectionAttribute::Direction::kSendonly;
+ case SdpDirectionAttribute::Direction::kSendrecv:
+ return SdpDirectionAttribute::Direction::kSendrecv;
+ }
+ MOZ_MAKE_COMPILER_ASSUME_IS_UNREACHABLE("Invalid direction!");
+ MOZ_RELEASE_ASSERT(false);
+}
+
+inline SdpDirectionAttribute::Direction operator|(
+ SdpDirectionAttribute::Direction d1, SdpDirectionAttribute::Direction d2) {
+ return (SdpDirectionAttribute::Direction)((unsigned)d1 | (unsigned)d2);
+}
+
+inline SdpDirectionAttribute::Direction operator&(
+ SdpDirectionAttribute::Direction d1, SdpDirectionAttribute::Direction d2) {
+ return (SdpDirectionAttribute::Direction)((unsigned)d1 & (unsigned)d2);
+}
+
+inline SdpDirectionAttribute::Direction operator|=(
+ SdpDirectionAttribute::Direction& d1, SdpDirectionAttribute::Direction d2) {
+ d1 = d1 | d2;
+ return d1;
+}
+
+inline SdpDirectionAttribute::Direction operator&=(
+ SdpDirectionAttribute::Direction& d1, SdpDirectionAttribute::Direction d2) {
+ d1 = d1 & d2;
+ return d1;
+}
+
+///////////////////////////////////////////////////////////////////////////
+// a=dtls-message, draft-rescorla-dtls-in-sdp
+//-------------------------------------------------------------------------
+// attribute =/ dtls-message-attribute
+//
+// dtls-message-attribute = "dtls-message" ":" role SP value
+//
+// role = "client" / "server"
+//
+// value = 1*(ALPHA / DIGIT / "+" / "/" / "=" )
+// ; base64 encoded message
+class SdpDtlsMessageAttribute : public SdpAttribute {
+ public:
+ enum Role { kClient, kServer };
+
+ explicit SdpDtlsMessageAttribute(Role role, const std::string& value)
+ : SdpAttribute(kDtlsMessageAttribute), mRole(role), mValue(value) {}
+
+ // TODO: remove this, Bug 1469702
+ explicit SdpDtlsMessageAttribute(const std::string& unparsed)
+ : SdpAttribute(kDtlsMessageAttribute), mRole(kClient) {
+ std::istringstream is(unparsed);
+ std::string error;
+ // We're not really worried about errors here if we don't parse;
+ // this attribute is a pure optimization.
+ Parse(is, &error);
+ }
+
+ SdpAttribute* Clone() const override {
+ return new SdpDtlsMessageAttribute(*this);
+ }
+
+ virtual void Serialize(std::ostream& os) const override;
+
+ // TODO: remove this, Bug 1469702
+ bool Parse(std::istream& is, std::string* error);
+
+ Role mRole;
+ std::string mValue;
+};
+
+inline std::ostream& operator<<(std::ostream& os,
+ SdpDtlsMessageAttribute::Role r) {
+ switch (r) {
+ case SdpDtlsMessageAttribute::kClient:
+ os << "client";
+ break;
+ case SdpDtlsMessageAttribute::kServer:
+ os << "server";
+ break;
+ default:
+ MOZ_ASSERT(false);
+ os << "?";
+ }
+ return os;
+}
+
+///////////////////////////////////////////////////////////////////////////
+// a=extmap, RFC5285
+//-------------------------------------------------------------------------
+// RFC5285
+// extmap = mapentry SP extensionname [SP extensionattributes]
+//
+// extensionname = URI
+//
+// direction = "sendonly" / "recvonly" / "sendrecv" / "inactive"
+//
+// mapentry = "extmap:" 1*5DIGIT ["/" direction]
+//
+// extensionattributes = byte-string
+//
+// URI = <Defined in RFC 3986>
+//
+// byte-string = <Defined in RFC 4566>
+//
+// SP = <Defined in RFC 5234>
+//
+// DIGIT = <Defined in RFC 5234>
+class SdpExtmapAttributeList : public SdpAttribute {
+ public:
+ SdpExtmapAttributeList() : SdpAttribute(kExtmapAttribute) {}
+
+ struct Extmap {
+ uint16_t entry;
+ SdpDirectionAttribute::Direction direction;
+ bool direction_specified;
+ std::string extensionname;
+ std::string extensionattributes;
+ };
+
+ void PushEntry(uint16_t entry, SdpDirectionAttribute::Direction direction,
+ bool direction_specified, const std::string& extensionname,
+ const std::string& extensionattributes = "") {
+ Extmap value = {entry, direction, direction_specified, extensionname,
+ extensionattributes};
+ mExtmaps.push_back(value);
+ }
+
+ SdpAttribute* Clone() const override {
+ return new SdpExtmapAttributeList(*this);
+ }
+
+ virtual void Serialize(std::ostream& os) const override;
+
+ std::vector<Extmap> mExtmaps;
+};
+
+///////////////////////////////////////////////////////////////////////////
+// a=fingerprint, RFC4572
+//-------------------------------------------------------------------------
+// fingerprint-attribute = "fingerprint" ":" hash-func SP fingerprint
+//
+// hash-func = "sha-1" / "sha-224" / "sha-256" /
+// "sha-384" / "sha-512" /
+// "md5" / "md2" / token
+// ; Additional hash functions can only come
+// ; from updates to RFC 3279
+//
+// fingerprint = 2UHEX *(":" 2UHEX)
+// ; Each byte in upper-case hex, separated
+// ; by colons.
+//
+// UHEX = DIGIT / %x41-46 ; A-F uppercase
+class SdpFingerprintAttributeList : public SdpAttribute {
+ public:
+ SdpFingerprintAttributeList() : SdpAttribute(kFingerprintAttribute) {}
+
+ enum HashAlgorithm {
+ kSha1,
+ kSha224,
+ kSha256,
+ kSha384,
+ kSha512,
+ kMd5,
+ kMd2,
+ kUnknownAlgorithm
+ };
+
+ struct Fingerprint {
+ HashAlgorithm hashFunc;
+ std::vector<uint8_t> fingerprint;
+ };
+
+ // For use by application programmers. Enforces that it's a known and
+ // reasonable algorithm.
+ void PushEntry(std::string algorithm_str,
+ const std::vector<uint8_t>& fingerprint,
+ bool enforcePlausible = true) {
+ std::transform(algorithm_str.begin(), algorithm_str.end(),
+ algorithm_str.begin(), ::tolower);
+
+ SdpFingerprintAttributeList::HashAlgorithm algorithm =
+ SdpFingerprintAttributeList::kUnknownAlgorithm;
+
+ if (algorithm_str == "sha-1") {
+ algorithm = SdpFingerprintAttributeList::kSha1;
+ } else if (algorithm_str == "sha-224") {
+ algorithm = SdpFingerprintAttributeList::kSha224;
+ } else if (algorithm_str == "sha-256") {
+ algorithm = SdpFingerprintAttributeList::kSha256;
+ } else if (algorithm_str == "sha-384") {
+ algorithm = SdpFingerprintAttributeList::kSha384;
+ } else if (algorithm_str == "sha-512") {
+ algorithm = SdpFingerprintAttributeList::kSha512;
+ } else if (algorithm_str == "md5") {
+ algorithm = SdpFingerprintAttributeList::kMd5;
+ } else if (algorithm_str == "md2") {
+ algorithm = SdpFingerprintAttributeList::kMd2;
+ }
+
+ if ((algorithm == SdpFingerprintAttributeList::kUnknownAlgorithm) ||
+ fingerprint.empty()) {
+ if (enforcePlausible) {
+ MOZ_ASSERT(false, "Unknown fingerprint algorithm");
+ } else {
+ return;
+ }
+ }
+
+ PushEntry(algorithm, fingerprint);
+ }
+
+ void PushEntry(HashAlgorithm hashFunc,
+ const std::vector<uint8_t>& fingerprint) {
+ Fingerprint value = {hashFunc, fingerprint};
+ mFingerprints.push_back(value);
+ }
+
+ SdpAttribute* Clone() const override {
+ return new SdpFingerprintAttributeList(*this);
+ }
+
+ virtual void Serialize(std::ostream& os) const override;
+
+ std::vector<Fingerprint> mFingerprints;
+
+ static std::string FormatFingerprint(const std::vector<uint8_t>& fp);
+ static std::vector<uint8_t> ParseFingerprint(const std::string& str);
+};
+
+inline std::ostream& operator<<(std::ostream& os,
+ SdpFingerprintAttributeList::HashAlgorithm a) {
+ switch (a) {
+ case SdpFingerprintAttributeList::kSha1:
+ os << "sha-1";
+ break;
+ case SdpFingerprintAttributeList::kSha224:
+ os << "sha-224";
+ break;
+ case SdpFingerprintAttributeList::kSha256:
+ os << "sha-256";
+ break;
+ case SdpFingerprintAttributeList::kSha384:
+ os << "sha-384";
+ break;
+ case SdpFingerprintAttributeList::kSha512:
+ os << "sha-512";
+ break;
+ case SdpFingerprintAttributeList::kMd5:
+ os << "md5";
+ break;
+ case SdpFingerprintAttributeList::kMd2:
+ os << "md2";
+ break;
+ default:
+ MOZ_ASSERT(false);
+ os << "?";
+ }
+ return os;
+}
+
+///////////////////////////////////////////////////////////////////////////
+// a=group, RFC5888
+//-------------------------------------------------------------------------
+// group-attribute = "a=group:" semantics
+// *(SP identification-tag)
+// semantics = "LS" / "FID" / semantics-extension
+// semantics-extension = token
+// identification-tag = token
+class SdpGroupAttributeList : public SdpAttribute {
+ public:
+ SdpGroupAttributeList() : SdpAttribute(kGroupAttribute) {}
+
+ enum Semantics {
+ kLs, // RFC5888
+ kFid, // RFC5888
+ kSrf, // RFC3524
+ kAnat, // RFC4091
+ kFec, // RFC5956
+ kFecFr, // RFC5956
+ kCs, // draft-mehta-rmt-flute-sdp-05
+ kDdp, // RFC5583
+ kDup, // RFC7104
+ kBundle // draft-ietf-mmusic-bundle
+ };
+
+ struct Group {
+ Semantics semantics;
+ std::vector<std::string> tags;
+ };
+
+ void PushEntry(Semantics semantics, const std::vector<std::string>& tags) {
+ Group value = {semantics, tags};
+ mGroups.push_back(value);
+ }
+
+ void RemoveMid(const std::string& mid) {
+ for (auto i = mGroups.begin(); i != mGroups.end();) {
+ auto tag = std::find(i->tags.begin(), i->tags.end(), mid);
+ if (tag != i->tags.end()) {
+ i->tags.erase(tag);
+ }
+
+ if (i->tags.empty()) {
+ i = mGroups.erase(i);
+ } else {
+ ++i;
+ }
+ }
+ }
+
+ SdpAttribute* Clone() const override {
+ return new SdpGroupAttributeList(*this);
+ }
+
+ virtual void Serialize(std::ostream& os) const override;
+
+ std::vector<Group> mGroups;
+};
+
+inline std::ostream& operator<<(std::ostream& os,
+ SdpGroupAttributeList::Semantics s) {
+ switch (s) {
+ case SdpGroupAttributeList::kLs:
+ os << "LS";
+ break;
+ case SdpGroupAttributeList::kFid:
+ os << "FID";
+ break;
+ case SdpGroupAttributeList::kSrf:
+ os << "SRF";
+ break;
+ case SdpGroupAttributeList::kAnat:
+ os << "ANAT";
+ break;
+ case SdpGroupAttributeList::kFec:
+ os << "FEC";
+ break;
+ case SdpGroupAttributeList::kFecFr:
+ os << "FEC-FR";
+ break;
+ case SdpGroupAttributeList::kCs:
+ os << "CS";
+ break;
+ case SdpGroupAttributeList::kDdp:
+ os << "DDP";
+ break;
+ case SdpGroupAttributeList::kDup:
+ os << "DUP";
+ break;
+ case SdpGroupAttributeList::kBundle:
+ os << "BUNDLE";
+ break;
+ default:
+ MOZ_ASSERT(false);
+ os << "?";
+ }
+ return os;
+}
+
+///////////////////////////////////////////////////////////////////////////
+// a=identity, draft-ietf-rtcweb-security-arch
+//-------------------------------------------------------------------------
+// identity-attribute = "identity:" identity-assertion
+// [ SP identity-extension
+// *(";" [ SP ] identity-extension) ]
+// identity-assertion = base64
+// base64 = 1*(ALPHA / DIGIT / "+" / "/" / "=" )
+// identity-extension = extension-att-name [ "=" extension-att-value ]
+// extension-att-name = token
+// extension-att-value = 1*(%x01-09 / %x0b-0c / %x0e-3a / %x3c-ff)
+// ; byte-string from [RFC4566] omitting ";"
+
+// We're just using an SdpStringAttribute for this right now
+#if 0
+class SdpIdentityAttribute : public SdpAttribute
+{
+public:
+ explicit SdpIdentityAttribute(const std::string &assertion,
+ const std::vector<std::string> &extensions =
+ std::vector<std::string>()) :
+ SdpAttribute(kIdentityAttribute),
+ mAssertion(assertion),
+ mExtensions(extensions) {}
+
+ virtual void Serialize(std::ostream& os) const override;
+
+ std::string mAssertion;
+ std::vector<std::string> mExtensions;
+}
+#endif
+
+///////////////////////////////////////////////////////////////////////////
+// a=imageattr, RFC6236
+//-------------------------------------------------------------------------
+// image-attr = "imageattr:" PT 1*2( 1*WSP ( "send" / "recv" )
+// 1*WSP attr-list )
+// PT = 1*DIGIT / "*"
+// attr-list = ( set *(1*WSP set) ) / "*"
+// ; WSP and DIGIT defined in [RFC5234]
+//
+// set= "[" "x=" xyrange "," "y=" xyrange *( "," key-value ) "]"
+// ; x is the horizontal image size range (pixel count)
+// ; y is the vertical image size range (pixel count)
+//
+// key-value = ( "sar=" srange )
+// / ( "par=" prange )
+// / ( "q=" qvalue )
+// ; Key-value MAY be extended with other keyword
+// ; parameters.
+// ; At most, one instance each of sar, par, or q
+// ; is allowed in a set.
+// ;
+// ; sar (sample aspect ratio) is the sample aspect ratio
+// ; associated with the set (optional, MAY be ignored)
+// ; par (picture aspect ratio) is the allowed
+// ; ratio between the display's x and y physical
+// ; size (optional)
+// ; q (optional, range [0.0..1.0], default value 0.5)
+// ; is the preference for the given set,
+// ; a higher value means a higher preference
+//
+// onetonine = "1" / "2" / "3" / "4" / "5" / "6" / "7" / "8" / "9"
+// ; Digit between 1 and 9
+// xyvalue = onetonine *5DIGIT
+// ; Digit between 1 and 9 that is
+// ; followed by 0 to 5 other digits
+// step = xyvalue
+// xyrange = ( "[" xyvalue ":" [ step ":" ] xyvalue "]" )
+// ; Range between a lower and an upper value
+// ; with an optional step, default step = 1
+// ; The rightmost occurrence of xyvalue MUST have a
+// ; higher value than the leftmost occurrence.
+// / ( "[" xyvalue 1*( "," xyvalue ) "]" )
+// ; Discrete values separated by ','
+// / ( xyvalue )
+// ; A single value
+// spvalue = ( "0" "." onetonine *3DIGIT )
+// ; Values between 0.1000 and 0.9999
+// / ( onetonine "." 1*4DIGIT )
+// ; Values between 1.0000 and 9.9999
+// srange = ( "[" spvalue 1*( "," spvalue ) "]" )
+// ; Discrete values separated by ','.
+// ; Each occurrence of spvalue MUST be
+// ; greater than the previous occurrence.
+// / ( "[" spvalue "-" spvalue "]" )
+// ; Range between a lower and an upper level (inclusive)
+// ; The second occurrence of spvalue MUST have a higher
+// ; value than the first
+// / ( spvalue )
+// ; A single value
+//
+// prange = ( "[" spvalue "-" spvalue "]" )
+// ; Range between a lower and an upper level (inclusive)
+// ; The second occurrence of spvalue MUST have a higher
+// ; value than the first
+//
+// qvalue = ( "0" "." 1*2DIGIT )
+// / ( "1" "." 1*2("0") )
+// ; Values between 0.00 and 1.00
+//
+// XXX TBD -- We don't use this yet, and it's a project unto itself.
+//
+
+class SdpImageattrAttributeList : public SdpAttribute {
+ public:
+ SdpImageattrAttributeList() : SdpAttribute(kImageattrAttribute) {}
+
+ class XYRange {
+ public:
+ XYRange() : min(0), max(0), step(1) {}
+ void Serialize(std::ostream& os) const;
+ // TODO: Remove this Bug 1469702
+ bool Parse(std::istream& is, std::string* error);
+ // TODO: Remove this Bug 1469702
+ bool ParseAfterBracket(std::istream& is, std::string* error);
+ // TODO: Remove this Bug 1469702
+ bool ParseAfterMin(std::istream& is, std::string* error);
+ // TODO: Remove this Bug 1469702
+ bool ParseDiscreteValues(std::istream& is, std::string* error);
+ std::vector<uint32_t> discreteValues;
+ // min/max are used iff discreteValues is empty
+ uint32_t min;
+ uint32_t max;
+ uint32_t step;
+ };
+
+ class SRange {
+ public:
+ SRange() : min(0), max(0) {}
+ void Serialize(std::ostream& os) const;
+ // TODO: Remove this Bug 1469702
+ bool Parse(std::istream& is, std::string* error);
+ // TODO: Remove this Bug 1469702
+ bool ParseAfterBracket(std::istream& is, std::string* error);
+ // TODO: Remove this Bug 1469702
+ bool ParseAfterMin(std::istream& is, std::string* error);
+ // TODO: Remove this Bug 1469702
+ bool ParseDiscreteValues(std::istream& is, std::string* error);
+ bool IsSet() const { return !discreteValues.empty() || (min && max); }
+ std::vector<float> discreteValues;
+ // min/max are used iff discreteValues is empty
+ float min;
+ float max;
+ };
+
+ class PRange {
+ public:
+ PRange() : min(0), max(0) {}
+ void Serialize(std::ostream& os) const;
+ // TODO: Remove this Bug 1469702
+ bool Parse(std::istream& is, std::string* error);
+ bool IsSet() const { return min && max; }
+ float min;
+ float max;
+ };
+
+ class Set {
+ public:
+ Set() : qValue(-1) {}
+ void Serialize(std::ostream& os) const;
+ // TODO: Remove this Bug 1469702
+ bool Parse(std::istream& is, std::string* error);
+ XYRange xRange;
+ XYRange yRange;
+ SRange sRange;
+ PRange pRange;
+ float qValue;
+ };
+
+ class Imageattr {
+ public:
+ Imageattr() : pt(), sendAll(false), recvAll(false) {}
+ void Serialize(std::ostream& os) const;
+ // TODO: Remove this Bug 1469702
+ bool Parse(std::istream& is, std::string* error);
+ // TODO: Remove this Bug 1469702
+ bool ParseSets(std::istream& is, std::string* error);
+ // If not set, this means all payload types
+ Maybe<uint16_t> pt;
+ bool sendAll;
+ std::vector<Set> sendSets;
+ bool recvAll;
+ std::vector<Set> recvSets;
+ };
+
+ SdpAttribute* Clone() const override {
+ return new SdpImageattrAttributeList(*this);
+ }
+
+ virtual void Serialize(std::ostream& os) const override;
+
+ // TODO: Remove this Bug 1469702
+ bool PushEntry(const std::string& raw, std::string* error, size_t* errorPos);
+
+ std::vector<Imageattr> mImageattrs;
+};
+
+///////////////////////////////////////////////////////////////////////////
+// a=msid, draft-ietf-mmusic-msid
+//-------------------------------------------------------------------------
+// msid-attr = "msid:" identifier [ SP appdata ]
+// identifier = 1*64token-char ; see RFC 4566
+// appdata = 1*64token-char ; see RFC 4566
+class SdpMsidAttributeList : public SdpAttribute {
+ public:
+ SdpMsidAttributeList() : SdpAttribute(kMsidAttribute) {}
+
+ struct Msid {
+ std::string identifier;
+ std::string appdata;
+ };
+
+ void PushEntry(const std::string& identifier,
+ const std::string& appdata = "") {
+ Msid value = {identifier, appdata};
+ mMsids.push_back(value);
+ }
+
+ SdpAttribute* Clone() const override {
+ return new SdpMsidAttributeList(*this);
+ }
+
+ virtual void Serialize(std::ostream& os) const override;
+
+ std::vector<Msid> mMsids;
+};
+
+///////////////////////////////////////////////////////////////////////////
+// a=msid-semantic, draft-ietf-mmusic-msid
+//-------------------------------------------------------------------------
+// msid-semantic-attr = "msid-semantic:" msid-semantic msid-list
+// msid-semantic = token ; see RFC 4566
+// msid-list = *(" " msid-id) / " *"
+class SdpMsidSemanticAttributeList : public SdpAttribute {
+ public:
+ SdpMsidSemanticAttributeList() : SdpAttribute(kMsidSemanticAttribute) {}
+
+ struct MsidSemantic {
+ // TODO: Once we have some more of these, we might want to make an enum
+ std::string semantic;
+ std::vector<std::string> msids;
+ };
+
+ void PushEntry(const std::string& semantic,
+ const std::vector<std::string>& msids) {
+ MsidSemantic value = {semantic, msids};
+ mMsidSemantics.push_back(value);
+ }
+
+ SdpAttribute* Clone() const override {
+ return new SdpMsidSemanticAttributeList(*this);
+ }
+
+ virtual void Serialize(std::ostream& os) const override;
+
+ std::vector<MsidSemantic> mMsidSemantics;
+};
+
+///////////////////////////////////////////////////////////////////////////
+// a=remote-candiate, RFC5245
+//-------------------------------------------------------------------------
+// remote-candidate-att = "remote-candidates" ":" remote-candidate
+// 0*(SP remote-candidate)
+// remote-candidate = component-ID SP connection-address SP port
+class SdpRemoteCandidatesAttribute : public SdpAttribute {
+ public:
+ struct Candidate {
+ std::string id;
+ std::string address;
+ uint16_t port;
+ };
+
+ explicit SdpRemoteCandidatesAttribute(
+ const std::vector<Candidate>& candidates)
+ : SdpAttribute(kRemoteCandidatesAttribute), mCandidates(candidates) {}
+
+ SdpAttribute* Clone() const override {
+ return new SdpRemoteCandidatesAttribute(*this);
+ }
+
+ virtual void Serialize(std::ostream& os) const override;
+
+ std::vector<Candidate> mCandidates;
+};
+
+/*
+a=rid, draft-pthatcher-mmusic-rid-01
+
+ rid-syntax = "a=rid:" rid-identifier SP rid-dir
+ [ rid-pt-param-list / rid-param-list ]
+
+ rid-identifier = 1*(alpha-numeric / "-" / "_")
+
+ rid-dir = "send" / "recv"
+
+ rid-pt-param-list = SP rid-fmt-list *(";" rid-param)
+
+ rid-param-list = SP rid-param *(";" rid-param)
+
+ rid-fmt-list = "pt=" fmt *( "," fmt )
+ ; fmt defined in {{RFC4566}}
+
+ rid-param = rid-width-param
+ / rid-height-param
+ / rid-fps-param
+ / rid-fs-param
+ / rid-br-param
+ / rid-pps-param
+ / rid-depend-param
+ / rid-param-other
+
+ rid-width-param = "max-width" [ "=" int-param-val ]
+
+ rid-height-param = "max-height" [ "=" int-param-val ]
+
+ rid-fps-param = "max-fps" [ "=" int-param-val ]
+
+ rid-fs-param = "max-fs" [ "=" int-param-val ]
+
+ rid-br-param = "max-br" [ "=" int-param-val ]
+
+ rid-pps-param = "max-pps" [ "=" int-param-val ]
+
+ rid-depend-param = "depend=" rid-list
+
+ rid-param-other = 1*(alpha-numeric / "-") [ "=" param-val ]
+
+ rid-list = rid-identifier *( "," rid-identifier )
+
+ int-param-val = 1*DIGIT
+
+ param-val = *( %x20-58 / %x60-7E )
+ ; Any printable character except semicolon
+*/
+class SdpRidAttributeList : public SdpAttribute {
+ public:
+ explicit SdpRidAttributeList() : SdpAttribute(kRidAttribute) {}
+
+ struct Rid {
+ Rid() : direction(sdp::kSend) {}
+
+ // Remove this function. See Bug 1469702
+ bool Parse(std::istream& is, std::string* error);
+ // Remove this function. See Bug 1469702
+ bool ParseParameters(std::istream& is, std::string* error);
+ // Remove this function. See Bug 1469702
+ bool ParseDepend(std::istream& is, std::string* error);
+ // Remove this function. See Bug 1469702
+ bool ParseFormats(std::istream& is, std::string* error);
+
+ void Serialize(std::ostream& os) const;
+ void SerializeParameters(std::ostream& os) const;
+ bool HasFormat(const std::string& format) const;
+ bool HasParameters() const {
+ return !formats.empty() || constraints.maxWidth ||
+ constraints.maxHeight || constraints.maxFps || constraints.maxFs ||
+ constraints.maxBr || constraints.maxPps || !dependIds.empty();
+ }
+
+ std::string id;
+ sdp::Direction direction;
+ std::vector<uint16_t> formats; // Empty implies all
+ EncodingConstraints constraints;
+ std::vector<std::string> dependIds;
+ };
+
+ SdpAttribute* Clone() const override {
+ return new SdpRidAttributeList(*this);
+ }
+
+ static bool CheckRidValidity(const std::string& aRid, std::string* aError);
+ static size_t kMaxRidLength;
+
+ virtual void Serialize(std::ostream& os) const override;
+
+ // Remove this function. See Bug 1469702
+ bool PushEntry(const std::string& raw, std::string* error, size_t* errorPos);
+
+ void PushEntry(const std::string& id, sdp::Direction dir,
+ const std::vector<uint16_t>& formats,
+ const EncodingConstraints& constraints,
+ const std::vector<std::string>& dependIds);
+
+ std::vector<Rid> mRids;
+};
+
+///////////////////////////////////////////////////////////////////////////
+// a=rtcp, RFC3605
+//-------------------------------------------------------------------------
+// rtcp-attribute = "a=rtcp:" port [nettype space addrtype space
+// connection-address] CRLF
+class SdpRtcpAttribute : public SdpAttribute {
+ public:
+ explicit SdpRtcpAttribute(uint16_t port)
+ : SdpAttribute(kRtcpAttribute),
+ mPort(port),
+ mNetType(sdp::kNetTypeNone),
+ mAddrType(sdp::kAddrTypeNone) {}
+
+ SdpRtcpAttribute(uint16_t port, sdp::NetType netType, sdp::AddrType addrType,
+ const std::string& address)
+ : SdpAttribute(kRtcpAttribute),
+ mPort(port),
+ mNetType(netType),
+ mAddrType(addrType),
+ mAddress(address) {
+ MOZ_ASSERT(netType != sdp::kNetTypeNone);
+ MOZ_ASSERT(addrType != sdp::kAddrTypeNone);
+ MOZ_ASSERT(!address.empty());
+ }
+
+ SdpAttribute* Clone() const override { return new SdpRtcpAttribute(*this); }
+
+ virtual void Serialize(std::ostream& os) const override;
+
+ uint16_t mPort;
+ sdp::NetType mNetType;
+ sdp::AddrType mAddrType;
+ std::string mAddress;
+};
+
+///////////////////////////////////////////////////////////////////////////
+// a=rtcp-fb, RFC4585
+//-------------------------------------------------------------------------
+// rtcp-fb-syntax = "a=rtcp-fb:" rtcp-fb-pt SP rtcp-fb-val CRLF
+//
+// rtcp-fb-pt = "*" ; wildcard: applies to all formats
+// / fmt ; as defined in SDP spec
+//
+// rtcp-fb-val = "ack" rtcp-fb-ack-param
+// / "nack" rtcp-fb-nack-param
+// / "trr-int" SP 1*DIGIT
+// / rtcp-fb-id rtcp-fb-param
+//
+// rtcp-fb-id = 1*(alpha-numeric / "-" / "_")
+//
+// rtcp-fb-param = SP "app" [SP byte-string]
+// / SP token [SP byte-string]
+// / ; empty
+//
+// rtcp-fb-ack-param = SP "rpsi"
+// / SP "app" [SP byte-string]
+// / SP token [SP byte-string]
+// / ; empty
+//
+// rtcp-fb-nack-param = SP "pli"
+// / SP "sli"
+// / SP "rpsi"
+// / SP "app" [SP byte-string]
+// / SP token [SP byte-string]
+// / ; empty
+//
+class SdpRtcpFbAttributeList : public SdpAttribute {
+ public:
+ SdpRtcpFbAttributeList() : SdpAttribute(kRtcpFbAttribute) {}
+
+ enum Type { kAck, kApp, kCcm, kNack, kTrrInt, kRemb, kTransportCC };
+
+ static const char* pli;
+ static const char* sli;
+ static const char* rpsi;
+ static const char* app;
+
+ static const char* fir;
+ static const char* tmmbr;
+ static const char* tstr;
+ static const char* vbcm;
+
+ struct Feedback {
+ std::string pt;
+ Type type;
+ std::string parameter;
+ std::string extra;
+ // TODO(bug 1744307): Use =default here once it is supported
+ bool operator==(const Feedback& aOther) const {
+ return pt == aOther.pt && type == aOther.type &&
+ parameter == aOther.parameter && extra == aOther.extra;
+ }
+ };
+
+ void PushEntry(const std::string& pt, Type type,
+ const std::string& parameter = "",
+ const std::string& extra = "") {
+ Feedback value = {pt, type, parameter, extra};
+ mFeedbacks.push_back(value);
+ }
+
+ SdpAttribute* Clone() const override {
+ return new SdpRtcpFbAttributeList(*this);
+ }
+
+ virtual void Serialize(std::ostream& os) const override;
+
+ std::vector<Feedback> mFeedbacks;
+};
+
+inline std::ostream& operator<<(std::ostream& os,
+ SdpRtcpFbAttributeList::Type type) {
+ switch (type) {
+ case SdpRtcpFbAttributeList::kAck:
+ os << "ack";
+ break;
+ case SdpRtcpFbAttributeList::kApp:
+ os << "app";
+ break;
+ case SdpRtcpFbAttributeList::kCcm:
+ os << "ccm";
+ break;
+ case SdpRtcpFbAttributeList::kNack:
+ os << "nack";
+ break;
+ case SdpRtcpFbAttributeList::kTrrInt:
+ os << "trr-int";
+ break;
+ case SdpRtcpFbAttributeList::kRemb:
+ os << "goog-remb";
+ break;
+ case SdpRtcpFbAttributeList::kTransportCC:
+ os << "transport-cc";
+ break;
+ default:
+ MOZ_ASSERT(false);
+ os << "?";
+ }
+ return os;
+}
+
+///////////////////////////////////////////////////////////////////////////
+// a=rtpmap, RFC4566
+//-------------------------------------------------------------------------
+// a=rtpmap:<payload type> <encoding name>/<clock rate> [/<encoding parameters>]
+class SdpRtpmapAttributeList : public SdpAttribute {
+ public:
+ SdpRtpmapAttributeList() : SdpAttribute(kRtpmapAttribute) {}
+
+ // Minimal set to get going
+ enum CodecType {
+ kOpus,
+ kG722,
+ kPCMU,
+ kPCMA,
+ kVP8,
+ kVP9,
+ kiLBC,
+ kiSAC,
+ kH264,
+ kRed,
+ kUlpfec,
+ kTelephoneEvent,
+ kRtx,
+ kOtherCodec
+ };
+
+ struct Rtpmap {
+ std::string pt;
+ CodecType codec;
+ std::string name;
+ uint32_t clock;
+ // Technically, this could mean something else in the future.
+ // In practice, that's probably not going to happen.
+ uint32_t channels;
+ };
+
+ void PushEntry(const std::string& pt, CodecType codec,
+ const std::string& name, uint32_t clock,
+ uint32_t channels = 0) {
+ Rtpmap value = {pt, codec, name, clock, channels};
+ mRtpmaps.push_back(value);
+ }
+
+ SdpAttribute* Clone() const override {
+ return new SdpRtpmapAttributeList(*this);
+ }
+
+ virtual void Serialize(std::ostream& os) const override;
+
+ bool HasEntry(const std::string& pt) const {
+ for (auto it = mRtpmaps.begin(); it != mRtpmaps.end(); ++it) {
+ if (it->pt == pt) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ const Rtpmap& GetEntry(const std::string& pt) const {
+ for (auto it = mRtpmaps.begin(); it != mRtpmaps.end(); ++it) {
+ if (it->pt == pt) {
+ return *it;
+ }
+ }
+ MOZ_CRASH();
+ }
+
+ std::vector<Rtpmap> mRtpmaps;
+};
+
+inline std::ostream& operator<<(std::ostream& os,
+ SdpRtpmapAttributeList::CodecType c) {
+ switch (c) {
+ case SdpRtpmapAttributeList::kOpus:
+ os << "opus";
+ break;
+ case SdpRtpmapAttributeList::kG722:
+ os << "G722";
+ break;
+ case SdpRtpmapAttributeList::kPCMU:
+ os << "PCMU";
+ break;
+ case SdpRtpmapAttributeList::kPCMA:
+ os << "PCMA";
+ break;
+ case SdpRtpmapAttributeList::kVP8:
+ os << "VP8";
+ break;
+ case SdpRtpmapAttributeList::kVP9:
+ os << "VP9";
+ break;
+ case SdpRtpmapAttributeList::kiLBC:
+ os << "iLBC";
+ break;
+ case SdpRtpmapAttributeList::kiSAC:
+ os << "iSAC";
+ break;
+ case SdpRtpmapAttributeList::kH264:
+ os << "H264";
+ break;
+ case SdpRtpmapAttributeList::kRed:
+ os << "red";
+ break;
+ case SdpRtpmapAttributeList::kUlpfec:
+ os << "ulpfec";
+ break;
+ case SdpRtpmapAttributeList::kTelephoneEvent:
+ os << "telephone-event";
+ break;
+ case SdpRtpmapAttributeList::kRtx:
+ os << "rtx";
+ break;
+ default:
+ MOZ_ASSERT(false);
+ os << "?";
+ }
+ return os;
+}
+
+///////////////////////////////////////////////////////////////////////////
+// a=fmtp, RFC4566, RFC5576
+//-------------------------------------------------------------------------
+// a=fmtp:<format> <format specific parameters>
+//
+class SdpFmtpAttributeList : public SdpAttribute {
+ public:
+ SdpFmtpAttributeList() : SdpAttribute(kFmtpAttribute) {}
+
+ // Base class for format parameters
+ class Parameters {
+ public:
+ explicit Parameters(SdpRtpmapAttributeList::CodecType aCodec)
+ : codec_type(aCodec) {}
+
+ virtual ~Parameters() {}
+ virtual Parameters* Clone() const = 0;
+ virtual void Serialize(std::ostream& os) const = 0;
+ virtual bool CompareEq(const Parameters& other) const = 0;
+
+ bool operator==(const Parameters& other) const {
+ return codec_type == other.codec_type && CompareEq(other);
+ }
+
+ SdpRtpmapAttributeList::CodecType codec_type;
+ };
+
+ class RedParameters : public Parameters {
+ public:
+ RedParameters() : Parameters(SdpRtpmapAttributeList::kRed) {}
+
+ virtual Parameters* Clone() const override {
+ return new RedParameters(*this);
+ }
+
+ virtual void Serialize(std::ostream& os) const override {
+ for (size_t i = 0; i < encodings.size(); ++i) {
+ os << (i != 0 ? "/" : "") << std::to_string(encodings[i]);
+ }
+ }
+
+ virtual bool CompareEq(const Parameters& other) const override {
+ return encodings == static_cast<const RedParameters&>(other).encodings;
+ }
+
+ std::vector<uint8_t> encodings;
+ };
+
+ class RtxParameters : public Parameters {
+ public:
+ uint8_t apt = 255; // Valid payload types are 0 - 127, use 255 to represent
+ // unset value.
+ Maybe<uint32_t> rtx_time;
+
+ RtxParameters() : Parameters(SdpRtpmapAttributeList::kRtx) {}
+
+ virtual ~RtxParameters() {}
+
+ virtual Parameters* Clone() const override {
+ return new RtxParameters(*this);
+ }
+
+ virtual void Serialize(std::ostream& os) const override {
+ if (apt <= 127) {
+ os << "apt=" << static_cast<uint32_t>(apt);
+ rtx_time.apply([&](const auto& time) { os << ";rtx-time=" << time; });
+ }
+ }
+
+ virtual bool CompareEq(const Parameters& aOther) const override {
+ if (aOther.codec_type != codec_type) {
+ return false;
+ }
+ auto other = static_cast<const RtxParameters&>(aOther);
+ return other.apt == apt && other.rtx_time == rtx_time;
+ }
+ };
+
+ class H264Parameters : public Parameters {
+ public:
+ static const uint32_t kDefaultProfileLevelId = 0x420010;
+
+ H264Parameters()
+ : Parameters(SdpRtpmapAttributeList::kH264),
+ packetization_mode(0),
+ level_asymmetry_allowed(false),
+ profile_level_id(kDefaultProfileLevelId),
+ max_mbps(0),
+ max_fs(0),
+ max_cpb(0),
+ max_dpb(0),
+ max_br(0) {
+ memset(sprop_parameter_sets, 0, sizeof(sprop_parameter_sets));
+ }
+
+ virtual Parameters* Clone() const override {
+ return new H264Parameters(*this);
+ }
+
+ virtual void Serialize(std::ostream& os) const override {
+ // Note: don't move this, since having an unconditional param up top
+ // lets us avoid a whole bunch of conditional streaming of ';' below
+ os << "profile-level-id=" << std::hex << std::setfill('0') << std::setw(6)
+ << profile_level_id << std::dec << std::setfill(' ');
+
+ os << ";level-asymmetry-allowed=" << (level_asymmetry_allowed ? 1 : 0);
+
+ if (strlen(sprop_parameter_sets)) {
+ os << ";sprop-parameter-sets=" << sprop_parameter_sets;
+ }
+
+ if (packetization_mode != 0) {
+ os << ";packetization-mode=" << packetization_mode;
+ }
+
+ if (max_mbps != 0) {
+ os << ";max-mbps=" << max_mbps;
+ }
+
+ if (max_fs != 0) {
+ os << ";max-fs=" << max_fs;
+ }
+
+ if (max_cpb != 0) {
+ os << ";max-cpb=" << max_cpb;
+ }
+
+ if (max_dpb != 0) {
+ os << ";max-dpb=" << max_dpb;
+ }
+
+ if (max_br != 0) {
+ os << ";max-br=" << max_br;
+ }
+ }
+
+ virtual bool CompareEq(const Parameters& other) const override {
+ const auto& otherH264 = static_cast<const H264Parameters&>(other);
+
+ // sprop is not comapred here as it does not get parsed in the rsdparsa
+ return packetization_mode == otherH264.packetization_mode &&
+ level_asymmetry_allowed == otherH264.level_asymmetry_allowed &&
+ profile_level_id == otherH264.profile_level_id &&
+ max_mbps == otherH264.max_mbps && max_fs == otherH264.max_fs &&
+ max_cpb == otherH264.max_cpb && max_dpb == otherH264.max_dpb &&
+ max_br == otherH264.max_br;
+ }
+
+ static const size_t max_sprop_len = 128;
+ char sprop_parameter_sets[max_sprop_len];
+ unsigned int packetization_mode;
+ bool level_asymmetry_allowed;
+ unsigned int profile_level_id;
+ unsigned int max_mbps;
+ unsigned int max_fs;
+ unsigned int max_cpb;
+ unsigned int max_dpb;
+ unsigned int max_br;
+ };
+
+ // Also used for VP9 since they share parameters
+ class VP8Parameters : public Parameters {
+ public:
+ explicit VP8Parameters(SdpRtpmapAttributeList::CodecType type)
+ : Parameters(type), max_fs(0), max_fr(0) {}
+
+ virtual Parameters* Clone() const override {
+ return new VP8Parameters(*this);
+ }
+
+ virtual void Serialize(std::ostream& os) const override {
+ // draft-ietf-payload-vp8-11 says these are mandatory, upper layer
+ // needs to ensure they're set properly.
+ os << "max-fs=" << max_fs;
+ os << ";max-fr=" << max_fr;
+ }
+
+ virtual bool CompareEq(const Parameters& other) const override {
+ const auto& otherVP8 = static_cast<const VP8Parameters&>(other);
+
+ return max_fs == otherVP8.max_fs && max_fr == otherVP8.max_fr;
+ }
+
+ unsigned int max_fs;
+ unsigned int max_fr;
+ };
+
+ class OpusParameters : public Parameters {
+ public:
+ enum {
+ kDefaultMaxPlaybackRate = 48000,
+ kDefaultStereo = 0,
+ kDefaultUseInBandFec = 0,
+ kDefaultMaxAverageBitrate = 0,
+ kDefaultUseDTX = 0,
+ kDefaultFrameSize = 0,
+ kDefaultMinFrameSize = 0,
+ kDefaultMaxFrameSize = 0,
+ kDefaultUseCbr = 0
+ };
+ OpusParameters()
+ : Parameters(SdpRtpmapAttributeList::kOpus),
+ maxplaybackrate(kDefaultMaxPlaybackRate),
+ stereo(kDefaultStereo),
+ useInBandFec(kDefaultUseInBandFec),
+ maxAverageBitrate(kDefaultMaxAverageBitrate),
+ useDTX(kDefaultUseDTX),
+ frameSizeMs(kDefaultFrameSize),
+ minFrameSizeMs(kDefaultMinFrameSize),
+ maxFrameSizeMs(kDefaultMaxFrameSize),
+ useCbr(kDefaultUseCbr) {}
+
+ Parameters* Clone() const override { return new OpusParameters(*this); }
+
+ void Serialize(std::ostream& os) const override {
+ os << "maxplaybackrate=" << maxplaybackrate << ";stereo=" << stereo
+ << ";useinbandfec=" << useInBandFec;
+
+ if (useDTX) {
+ os << ";usedtx=1";
+ }
+ if (maxAverageBitrate) {
+ os << ";maxaveragebitrate=" << maxAverageBitrate;
+ }
+ if (frameSizeMs) {
+ os << ";ptime=" << frameSizeMs;
+ }
+ if (minFrameSizeMs) {
+ os << ";minptime=" << minFrameSizeMs;
+ }
+ if (maxFrameSizeMs) {
+ os << ";maxptime=" << maxFrameSizeMs;
+ }
+ if (useCbr) {
+ os << ";cbr=1";
+ }
+ }
+
+ virtual bool CompareEq(const Parameters& other) const override {
+ const auto& otherOpus = static_cast<const OpusParameters&>(other);
+
+ bool maxplaybackrateIsEq = (maxplaybackrate == otherOpus.maxplaybackrate);
+
+ // This is due to a bug in sipcc that causes maxplaybackrate to
+ // always be 0 if it appears in the fmtp
+ if (((maxplaybackrate == 0) && (otherOpus.maxplaybackrate != 0)) ||
+ ((maxplaybackrate != 0) && (otherOpus.maxplaybackrate == 0))) {
+ maxplaybackrateIsEq = true;
+ }
+
+ return maxplaybackrateIsEq && stereo == otherOpus.stereo &&
+ useInBandFec == otherOpus.useInBandFec &&
+ maxAverageBitrate == otherOpus.maxAverageBitrate &&
+ useDTX == otherOpus.useDTX &&
+ frameSizeMs == otherOpus.frameSizeMs &&
+ minFrameSizeMs == otherOpus.minFrameSizeMs &&
+ maxFrameSizeMs == otherOpus.maxFrameSizeMs &&
+ useCbr == otherOpus.useCbr;
+ }
+
+ unsigned int maxplaybackrate;
+ unsigned int stereo;
+ unsigned int useInBandFec;
+ uint32_t maxAverageBitrate;
+ bool useDTX;
+ uint32_t frameSizeMs;
+ uint32_t minFrameSizeMs;
+ uint32_t maxFrameSizeMs;
+ bool useCbr;
+ };
+
+ class TelephoneEventParameters : public Parameters {
+ public:
+ TelephoneEventParameters()
+ : Parameters(SdpRtpmapAttributeList::kTelephoneEvent),
+ dtmfTones("0-15") {}
+
+ virtual Parameters* Clone() const override {
+ return new TelephoneEventParameters(*this);
+ }
+
+ void Serialize(std::ostream& os) const override { os << dtmfTones; }
+
+ virtual bool CompareEq(const Parameters& other) const override {
+ return dtmfTones ==
+ static_cast<const TelephoneEventParameters&>(other).dtmfTones;
+ }
+
+ std::string dtmfTones;
+ };
+
+ class Fmtp {
+ public:
+ Fmtp(const std::string& aFormat, const Parameters& aParameters)
+ : format(aFormat), parameters(aParameters.Clone()) {}
+
+ // TODO: Rip all of this out when we have move semantics in the stl.
+ Fmtp(const Fmtp& orig) { *this = orig; }
+
+ Fmtp& operator=(const Fmtp& rhs) {
+ if (this != &rhs) {
+ format = rhs.format;
+ parameters.reset(rhs.parameters ? rhs.parameters->Clone() : nullptr);
+ }
+ return *this;
+ }
+
+ bool operator==(const Fmtp& other) const {
+ return format == other.format && *parameters == *other.parameters;
+ }
+
+ // The contract around these is as follows:
+ // * |parameters| is only set if we recognized the media type and had
+ // a subclass of Parameters to represent that type of parameters
+ // * |parameters| is a best-effort representation; it might be missing
+ // stuff
+ // * Parameters::codec_type tells you the concrete class, eg
+ // kH264 -> H264Parameters
+ std::string format;
+ UniquePtr<Parameters> parameters;
+ };
+
+ bool operator==(const SdpFmtpAttributeList& other) const;
+
+ SdpAttribute* Clone() const override {
+ return new SdpFmtpAttributeList(*this);
+ }
+
+ virtual void Serialize(std::ostream& os) const override;
+
+ void PushEntry(const std::string& format, const Parameters& parameters) {
+ mFmtps.push_back(Fmtp(format, parameters));
+ }
+
+ std::vector<Fmtp> mFmtps;
+};
+
+///////////////////////////////////////////////////////////////////////////
+// a=sctpmap, draft-ietf-mmusic-sctp-sdp-05
+//-------------------------------------------------------------------------
+// sctpmap-attr = "a=sctpmap:" sctpmap-number media-subtypes
+// [streams]
+// sctpmap-number = 1*DIGIT
+// protocol = labelstring
+// labelstring = text
+// text = byte-string
+// streams = 1*DIGIT
+//
+// We're going to pretend that there are spaces where they make sense.
+class SdpSctpmapAttributeList : public SdpAttribute {
+ public:
+ SdpSctpmapAttributeList() : SdpAttribute(kSctpmapAttribute) {}
+
+ struct Sctpmap {
+ std::string pt;
+ std::string name;
+ uint32_t streams;
+ };
+
+ void PushEntry(const std::string& pt, const std::string& name,
+ uint32_t streams = 0) {
+ Sctpmap value = {pt, name, streams};
+ mSctpmaps.push_back(value);
+ }
+
+ SdpAttribute* Clone() const override {
+ return new SdpSctpmapAttributeList(*this);
+ }
+
+ virtual void Serialize(std::ostream& os) const override;
+
+ bool HasEntry(const std::string& pt) const {
+ for (auto it = mSctpmaps.begin(); it != mSctpmaps.end(); ++it) {
+ if (it->pt == pt) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ const Sctpmap& GetFirstEntry() const { return mSctpmaps[0]; }
+
+ std::vector<Sctpmap> mSctpmaps;
+};
+
+///////////////////////////////////////////////////////////////////////////
+// a=setup, RFC4145
+//-------------------------------------------------------------------------
+// setup-attr = "a=setup:" role
+// role = "active" / "passive" / "actpass" / "holdconn"
+class SdpSetupAttribute : public SdpAttribute {
+ public:
+ enum Role { kActive, kPassive, kActpass, kHoldconn };
+
+ explicit SdpSetupAttribute(Role role)
+ : SdpAttribute(kSetupAttribute), mRole(role) {}
+
+ SdpAttribute* Clone() const override { return new SdpSetupAttribute(*this); }
+
+ virtual void Serialize(std::ostream& os) const override;
+
+ Role mRole;
+};
+
+inline std::ostream& operator<<(std::ostream& os, SdpSetupAttribute::Role r) {
+ switch (r) {
+ case SdpSetupAttribute::kActive:
+ os << "active";
+ break;
+ case SdpSetupAttribute::kPassive:
+ os << "passive";
+ break;
+ case SdpSetupAttribute::kActpass:
+ os << "actpass";
+ break;
+ case SdpSetupAttribute::kHoldconn:
+ os << "holdconn";
+ break;
+ default:
+ MOZ_ASSERT(false);
+ os << "?";
+ }
+ return os;
+}
+
+// Old draft-04
+// sc-attr = "a=simulcast:" 1*2( WSP sc-str-list ) [WSP sc-pause-list]
+// sc-str-list = sc-dir WSP sc-id-type "=" sc-alt-list *( ";" sc-alt-list )
+// sc-pause-list = "paused=" sc-alt-list
+// sc-dir = "send" / "recv"
+// sc-id-type = "pt" / "rid" / token
+// sc-alt-list = sc-id *( "," sc-id )
+// sc-id = fmt / rid-identifier / token
+// ; WSP defined in [RFC5234]
+// ; fmt, token defined in [RFC4566]
+// ; rid-identifier defined in [I-D.pthatcher-mmusic-rid]
+//
+// New draft 14, need to parse this for now, will eventually emit it
+// sc-value = ( sc-send [SP sc-recv] ) / ( sc-recv [SP sc-send] )
+// sc-send = %s"send" SP sc-str-list
+// sc-recv = %s"recv" SP sc-str-list
+// sc-str-list = sc-alt-list *( ";" sc-alt-list )
+// sc-alt-list = sc-id *( "," sc-id )
+// sc-id-paused = "~"
+// sc-id = [sc-id-paused] rid-id
+// ; SP defined in [RFC5234]
+// ; rid-id defined in [I-D.ietf-mmusic-rid]
+
+class SdpSimulcastAttribute : public SdpAttribute {
+ public:
+ SdpSimulcastAttribute() : SdpAttribute(kSimulcastAttribute) {}
+
+ SdpAttribute* Clone() const override {
+ return new SdpSimulcastAttribute(*this);
+ }
+
+ void Serialize(std::ostream& os) const override;
+ bool Parse(std::istream& is, std::string* error);
+
+ class Encoding {
+ public:
+ Encoding(const std::string& aRid, bool aPaused)
+ : rid(aRid), paused(aPaused) {}
+ std::string rid;
+ bool paused = false;
+ };
+
+ class Version {
+ public:
+ void Serialize(std::ostream& os) const;
+ bool IsSet() const { return !choices.empty(); }
+ bool Parse(std::istream& is, std::string* error);
+
+ std::vector<Encoding> choices;
+ };
+
+ class Versions : public std::vector<Version> {
+ public:
+ void Serialize(std::ostream& os) const;
+ bool IsSet() const {
+ if (empty()) {
+ return false;
+ }
+
+ for (const Version& version : *this) {
+ if (version.IsSet()) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ bool Parse(std::istream& is, std::string* error);
+ };
+
+ Versions sendVersions;
+ Versions recvVersions;
+};
+
+///////////////////////////////////////////////////////////////////////////
+// a=ssrc, RFC5576
+//-------------------------------------------------------------------------
+// ssrc-attr = "ssrc:" ssrc-id SP attribute
+// ; The base definition of "attribute" is in RFC 4566.
+// ; (It is the content of "a=" lines.)
+//
+// ssrc-id = integer ; 0 .. 2**32 - 1
+//-------------------------------------------------------------------------
+// TODO -- In the future, it might be nice if we ran a parse on the
+// attribute section of this so that we could interpret it semantically.
+// For WebRTC, the key use case for a=ssrc is assocaiting SSRCs with
+// media sections, and we're not really going to care about the attribute
+// itself. So we're just going to store it as a string for the time being.
+// Issue 187.
+class SdpSsrcAttributeList : public SdpAttribute {
+ public:
+ SdpSsrcAttributeList() : SdpAttribute(kSsrcAttribute) {}
+
+ struct Ssrc {
+ uint32_t ssrc;
+ std::string attribute;
+ };
+
+ void PushEntry(uint32_t ssrc, const std::string& attribute) {
+ Ssrc value = {ssrc, attribute};
+ mSsrcs.push_back(value);
+ }
+
+ SdpAttribute* Clone() const override {
+ return new SdpSsrcAttributeList(*this);
+ }
+
+ virtual void Serialize(std::ostream& os) const override;
+
+ std::vector<Ssrc> mSsrcs;
+};
+
+///////////////////////////////////////////////////////////////////////////
+// a=ssrc-group, RFC5576
+//-------------------------------------------------------------------------
+// ssrc-group-attr = "ssrc-group:" semantics *(SP ssrc-id)
+//
+// semantics = "FEC" / "FID" / token
+//
+// ssrc-id = integer ; 0 .. 2**32 - 1
+class SdpSsrcGroupAttributeList : public SdpAttribute {
+ public:
+ enum Semantics {
+ kFec, // RFC5576
+ kFid, // RFC5576
+ kFecFr, // RFC5956
+ kDup, // RFC7104
+ kSim // non-standard, used by hangouts
+ };
+
+ struct SsrcGroup {
+ Semantics semantics;
+ std::vector<uint32_t> ssrcs;
+ };
+
+ SdpSsrcGroupAttributeList() : SdpAttribute(kSsrcGroupAttribute) {}
+
+ void PushEntry(Semantics semantics, const std::vector<uint32_t>& ssrcs) {
+ SsrcGroup value = {semantics, ssrcs};
+ mSsrcGroups.push_back(value);
+ }
+
+ SdpAttribute* Clone() const override {
+ return new SdpSsrcGroupAttributeList(*this);
+ }
+
+ virtual void Serialize(std::ostream& os) const override;
+
+ std::vector<SsrcGroup> mSsrcGroups;
+};
+
+inline std::ostream& operator<<(std::ostream& os,
+ SdpSsrcGroupAttributeList::Semantics s) {
+ switch (s) {
+ case SdpSsrcGroupAttributeList::kFec:
+ os << "FEC";
+ break;
+ case SdpSsrcGroupAttributeList::kFid:
+ os << "FID";
+ break;
+ case SdpSsrcGroupAttributeList::kFecFr:
+ os << "FEC-FR";
+ break;
+ case SdpSsrcGroupAttributeList::kDup:
+ os << "DUP";
+ break;
+ case SdpSsrcGroupAttributeList::kSim:
+ os << "SIM";
+ break;
+ default:
+ MOZ_ASSERT(false);
+ os << "?";
+ }
+ return os;
+}
+
+///////////////////////////////////////////////////////////////////////////
+class SdpMultiStringAttribute : public SdpAttribute {
+ public:
+ explicit SdpMultiStringAttribute(AttributeType type) : SdpAttribute(type) {}
+
+ void PushEntry(const std::string& entry) { mValues.push_back(entry); }
+
+ SdpAttribute* Clone() const override {
+ return new SdpMultiStringAttribute(*this);
+ }
+
+ virtual void Serialize(std::ostream& os) const override;
+
+ std::vector<std::string> mValues;
+};
+
+// otherwise identical to SdpMultiStringAttribute, this is used for
+// ice-options and other places where the value is serialized onto
+// a single line with space separating tokens
+class SdpOptionsAttribute : public SdpAttribute {
+ public:
+ explicit SdpOptionsAttribute(AttributeType type) : SdpAttribute(type) {}
+
+ void PushEntry(const std::string& entry) { mValues.push_back(entry); }
+
+ void Load(const std::string& value);
+
+ SdpAttribute* Clone() const override {
+ return new SdpOptionsAttribute(*this);
+ }
+
+ virtual void Serialize(std::ostream& os) const override;
+
+ std::vector<std::string> mValues;
+};
+
+// Used for attributes that take no value (eg; a=ice-lite)
+class SdpFlagAttribute : public SdpAttribute {
+ public:
+ explicit SdpFlagAttribute(AttributeType type) : SdpAttribute(type) {}
+
+ SdpAttribute* Clone() const override { return new SdpFlagAttribute(*this); }
+
+ virtual void Serialize(std::ostream& os) const override;
+};
+
+// Used for any other kind of single-valued attribute not otherwise specialized
+class SdpStringAttribute : public SdpAttribute {
+ public:
+ explicit SdpStringAttribute(AttributeType type, const std::string& value)
+ : SdpAttribute(type), mValue(value) {}
+
+ SdpAttribute* Clone() const override { return new SdpStringAttribute(*this); }
+
+ virtual void Serialize(std::ostream& os) const override;
+
+ std::string mValue;
+};
+
+// Used for any purely (non-negative) numeric attribute
+class SdpNumberAttribute : public SdpAttribute {
+ public:
+ explicit SdpNumberAttribute(AttributeType type, uint32_t value = 0)
+ : SdpAttribute(type), mValue(value) {}
+
+ SdpAttribute* Clone() const override { return new SdpNumberAttribute(*this); }
+
+ virtual void Serialize(std::ostream& os) const override;
+
+ uint32_t mValue;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/sdp/SdpAttributeList.h b/dom/media/webrtc/sdp/SdpAttributeList.h
new file mode 100644
index 0000000000..23088dc967
--- /dev/null
+++ b/dom/media/webrtc/sdp/SdpAttributeList.h
@@ -0,0 +1,90 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _SDPATTRIBUTELIST_H_
+#define _SDPATTRIBUTELIST_H_
+
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Attributes.h"
+
+#include "sdp/SdpAttribute.h"
+
+namespace mozilla {
+
+class SdpAttributeList {
+ public:
+ virtual ~SdpAttributeList() {}
+ typedef SdpAttribute::AttributeType AttributeType;
+
+ // Avoid default params on virtual functions
+ bool HasAttribute(AttributeType type) const {
+ return HasAttribute(type, true);
+ }
+
+ const SdpAttribute* GetAttribute(AttributeType type) const {
+ return GetAttribute(type, true);
+ }
+
+ virtual bool HasAttribute(AttributeType type, bool sessionFallback) const = 0;
+ virtual const SdpAttribute* GetAttribute(AttributeType type,
+ bool sessionFallback) const = 0;
+ // The setter takes an attribute of any type, and takes ownership
+ virtual void SetAttribute(SdpAttribute* attr) = 0;
+ virtual void RemoveAttribute(AttributeType type) = 0;
+ virtual void Clear() = 0;
+ virtual uint32_t Count() const = 0;
+
+ virtual const SdpConnectionAttribute& GetConnection() const = 0;
+ virtual const SdpOptionsAttribute& GetIceOptions() const = 0;
+ virtual const SdpRtcpAttribute& GetRtcp() const = 0;
+ virtual const SdpRemoteCandidatesAttribute& GetRemoteCandidates() const = 0;
+ virtual const SdpSetupAttribute& GetSetup() const = 0;
+ virtual const SdpDtlsMessageAttribute& GetDtlsMessage() const = 0;
+
+ // These attributes can appear multiple times, so the returned
+ // classes actually represent a collection of values.
+ virtual const std::vector<std::string>& GetCandidate() const = 0;
+ virtual const SdpExtmapAttributeList& GetExtmap() const = 0;
+ virtual const SdpFingerprintAttributeList& GetFingerprint() const = 0;
+ virtual const SdpFmtpAttributeList& GetFmtp() const = 0;
+ virtual const SdpGroupAttributeList& GetGroup() const = 0;
+ virtual const SdpImageattrAttributeList& GetImageattr() const = 0;
+ virtual const SdpSimulcastAttribute& GetSimulcast() const = 0;
+ virtual const SdpMsidAttributeList& GetMsid() const = 0;
+ virtual const SdpMsidSemanticAttributeList& GetMsidSemantic() const = 0;
+ virtual const SdpRidAttributeList& GetRid() const = 0;
+ virtual const SdpRtcpFbAttributeList& GetRtcpFb() const = 0;
+ virtual const SdpRtpmapAttributeList& GetRtpmap() const = 0;
+ virtual const SdpSctpmapAttributeList& GetSctpmap() const = 0;
+ virtual uint32_t GetSctpPort() const = 0;
+ virtual uint32_t GetMaxMessageSize() const = 0;
+ virtual const SdpSsrcAttributeList& GetSsrc() const = 0;
+ virtual const SdpSsrcGroupAttributeList& GetSsrcGroup() const = 0;
+
+ // These attributes are effectively simple types, so we'll make life
+ // easy by just returning their value.
+ virtual const std::string& GetIcePwd() const = 0;
+ virtual const std::string& GetIceUfrag() const = 0;
+ virtual const std::string& GetIdentity() const = 0;
+ virtual const std::string& GetLabel() const = 0;
+ virtual unsigned int GetMaxptime() const = 0;
+ virtual const std::string& GetMid() const = 0;
+ virtual unsigned int GetPtime() const = 0;
+
+ // This is "special", because it's multiple things
+ virtual SdpDirectionAttribute::Direction GetDirection() const = 0;
+
+ virtual void Serialize(std::ostream&) const = 0;
+};
+
+inline std::ostream& operator<<(std::ostream& os, const SdpAttributeList& al) {
+ al.Serialize(os);
+ return os;
+}
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/sdp/SdpEnum.h b/dom/media/webrtc/sdp/SdpEnum.h
new file mode 100644
index 0000000000..c5d67352e0
--- /dev/null
+++ b/dom/media/webrtc/sdp/SdpEnum.h
@@ -0,0 +1,64 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _SDPENUM_H_
+#define _SDPENUM_H_
+
+#include <ostream>
+
+#include "mozilla/Assertions.h"
+
+namespace mozilla::sdp {
+
+enum NetType { kNetTypeNone, kInternet };
+
+inline std::ostream& operator<<(std::ostream& os, sdp::NetType t) {
+ switch (t) {
+ case sdp::kNetTypeNone:
+ MOZ_ASSERT(false);
+ return os << "NONE";
+ case sdp::kInternet:
+ return os << "IN";
+ }
+ MOZ_CRASH("Unknown NetType");
+}
+
+enum AddrType { kAddrTypeNone, kIPv4, kIPv6 };
+
+inline std::ostream& operator<<(std::ostream& os, sdp::AddrType t) {
+ switch (t) {
+ case sdp::kAddrTypeNone:
+ MOZ_ASSERT(false);
+ return os << "NONE";
+ case sdp::kIPv4:
+ return os << "IP4";
+ case sdp::kIPv6:
+ return os << "IP6";
+ }
+ MOZ_CRASH("Unknown AddrType");
+}
+
+enum Direction {
+ // Start at 1 so these can be used as flags
+ kSend = 1,
+ kRecv = 2
+};
+
+inline std::ostream& operator<<(std::ostream& os, sdp::Direction d) {
+ switch (d) {
+ case sdp::kSend:
+ return os << "send";
+ case sdp::kRecv:
+ return os << "recv";
+ }
+ MOZ_CRASH("Unknown Direction");
+}
+
+enum SdpType { kOffer, kAnswer };
+
+} // namespace mozilla::sdp
+
+#endif
diff --git a/dom/media/webrtc/sdp/SdpHelper.cpp b/dom/media/webrtc/sdp/SdpHelper.cpp
new file mode 100644
index 0000000000..d24a7d199d
--- /dev/null
+++ b/dom/media/webrtc/sdp/SdpHelper.cpp
@@ -0,0 +1,801 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "sdp/SdpHelper.h"
+
+#include "sdp/Sdp.h"
+#include "sdp/SdpMediaSection.h"
+#include "transport/logging.h"
+
+#include "nsDebug.h"
+#include "nsError.h"
+#include "prprf.h"
+
+#include <string.h>
+#include <set>
+
+namespace mozilla {
+MOZ_MTLOG_MODULE("sdp")
+
+#define SDP_SET_ERROR(error) \
+ do { \
+ std::ostringstream os; \
+ os << error; \
+ mLastError = os.str(); \
+ MOZ_MTLOG(ML_ERROR, mLastError); \
+ } while (0);
+
+nsresult SdpHelper::CopyTransportParams(size_t numComponents,
+ const SdpMediaSection& oldLocal,
+ SdpMediaSection* newLocal) {
+ const SdpAttributeList& oldLocalAttrs = oldLocal.GetAttributeList();
+ // Copy over m-section details
+ if (!oldLocalAttrs.HasAttribute(SdpAttribute::kBundleOnlyAttribute)) {
+ // Do not copy port 0 from an offer with a=bundle-only; this could cause
+ // an answer msection to be erroneously rejected.
+ newLocal->SetPort(oldLocal.GetPort());
+ }
+ newLocal->GetConnection() = oldLocal.GetConnection();
+
+ SdpAttributeList& newLocalAttrs = newLocal->GetAttributeList();
+
+ // Now we copy over attributes that won't be added by the usual logic
+ if (oldLocalAttrs.HasAttribute(SdpAttribute::kCandidateAttribute) &&
+ numComponents) {
+ UniquePtr<SdpMultiStringAttribute> candidateAttrs(
+ new SdpMultiStringAttribute(SdpAttribute::kCandidateAttribute));
+ for (const std::string& candidate : oldLocalAttrs.GetCandidate()) {
+ size_t component;
+ nsresult rv = GetComponent(candidate, &component);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (numComponents >= component) {
+ candidateAttrs->mValues.push_back(candidate);
+ }
+ }
+ if (!candidateAttrs->mValues.empty()) {
+ newLocalAttrs.SetAttribute(candidateAttrs.release());
+ }
+ }
+
+ if (oldLocalAttrs.HasAttribute(SdpAttribute::kEndOfCandidatesAttribute)) {
+ newLocalAttrs.SetAttribute(
+ new SdpFlagAttribute(SdpAttribute::kEndOfCandidatesAttribute));
+ }
+
+ if (numComponents == 2 &&
+ oldLocalAttrs.HasAttribute(SdpAttribute::kRtcpAttribute)) {
+ // copy rtcp attribute if we had one that we are using
+ newLocalAttrs.SetAttribute(new SdpRtcpAttribute(oldLocalAttrs.GetRtcp()));
+ }
+
+ return NS_OK;
+}
+
+bool SdpHelper::AreOldTransportParamsValid(const Sdp& oldAnswer,
+ const Sdp& offerersPreviousSdp,
+ const Sdp& newOffer, size_t level) {
+ if (MsectionIsDisabled(oldAnswer.GetMediaSection(level)) ||
+ MsectionIsDisabled(newOffer.GetMediaSection(level))) {
+ // Obvious
+ return false;
+ }
+
+ if (!OwnsTransport(oldAnswer, level, sdp::kAnswer)) {
+ // The transport attributes on this m-section were thrown away, because it
+ // was bundled.
+ return false;
+ }
+
+ if (!OwnsTransport(newOffer, level, sdp::kOffer)) {
+ return false;
+ }
+
+ if (IceCredentialsDiffer(newOffer.GetMediaSection(level),
+ offerersPreviousSdp.GetMediaSection(level))) {
+ return false;
+ }
+
+ return true;
+}
+
+bool SdpHelper::IceCredentialsDiffer(const SdpMediaSection& msection1,
+ const SdpMediaSection& msection2) {
+ const SdpAttributeList& attrs1(msection1.GetAttributeList());
+ const SdpAttributeList& attrs2(msection2.GetAttributeList());
+
+ if ((attrs1.GetIceUfrag() != attrs2.GetIceUfrag()) ||
+ (attrs1.GetIcePwd() != attrs2.GetIcePwd())) {
+ return true;
+ }
+
+ return false;
+}
+
+nsresult SdpHelper::GetComponent(const std::string& candidate,
+ size_t* component) {
+ unsigned int temp;
+ int32_t result = PR_sscanf(candidate.c_str(), "%*s %u", &temp);
+ if (result == 1) {
+ *component = temp;
+ return NS_OK;
+ }
+ SDP_SET_ERROR("Malformed ICE candidate: " << candidate);
+ return NS_ERROR_INVALID_ARG;
+}
+
+bool SdpHelper::MsectionIsDisabled(const SdpMediaSection& msection) const {
+ return !msection.GetPort() && !msection.GetAttributeList().HasAttribute(
+ SdpAttribute::kBundleOnlyAttribute);
+}
+
+void SdpHelper::DisableMsection(Sdp* sdp, SdpMediaSection* msection) {
+ std::string mid;
+
+ // Make sure to remove the mid from any group attributes
+ if (msection->GetAttributeList().HasAttribute(SdpAttribute::kMidAttribute)) {
+ mid = msection->GetAttributeList().GetMid();
+ if (sdp->GetAttributeList().HasAttribute(SdpAttribute::kGroupAttribute)) {
+ UniquePtr<SdpGroupAttributeList> newGroupAttr(
+ new SdpGroupAttributeList(sdp->GetAttributeList().GetGroup()));
+ newGroupAttr->RemoveMid(mid);
+ sdp->GetAttributeList().SetAttribute(newGroupAttr.release());
+ }
+ }
+
+ // Clear out attributes.
+ msection->GetAttributeList().Clear();
+
+ auto* direction = new SdpDirectionAttribute(SdpDirectionAttribute::kInactive);
+ msection->GetAttributeList().SetAttribute(direction);
+ msection->SetPort(0);
+
+ // maintain the mid for easier identification on other side
+ if (!mid.empty()) {
+ msection->GetAttributeList().SetAttribute(
+ new SdpStringAttribute(SdpAttribute::kMidAttribute, mid));
+ }
+
+ msection->ClearCodecs();
+
+ auto mediaType = msection->GetMediaType();
+ switch (mediaType) {
+ case SdpMediaSection::kAudio:
+ msection->AddCodec("0", "PCMU", 8000, 1);
+ break;
+ case SdpMediaSection::kVideo:
+ msection->AddCodec("120", "VP8", 90000, 1);
+ break;
+ case SdpMediaSection::kApplication:
+ msection->AddDataChannel("webrtc-datachannel", 0, 0, 0);
+ break;
+ default:
+ // We need to have something here to fit the grammar, this seems safe
+ // and 19 is a reserved payload type which should not be used by anyone.
+ msection->AddCodec("19", "reserved", 8000, 1);
+ }
+}
+
+void SdpHelper::GetBundleGroups(
+ const Sdp& sdp,
+ std::vector<SdpGroupAttributeList::Group>* bundleGroups) const {
+ if (sdp.GetAttributeList().HasAttribute(SdpAttribute::kGroupAttribute)) {
+ for (auto& group : sdp.GetAttributeList().GetGroup().mGroups) {
+ if (group.semantics == SdpGroupAttributeList::kBundle) {
+ bundleGroups->push_back(group);
+ }
+ }
+ }
+}
+
+nsresult SdpHelper::GetBundledMids(const Sdp& sdp, BundledMids* bundledMids) {
+ std::vector<SdpGroupAttributeList::Group> bundleGroups;
+ GetBundleGroups(sdp, &bundleGroups);
+
+ for (SdpGroupAttributeList::Group& group : bundleGroups) {
+ if (group.tags.empty()) {
+ continue;
+ }
+
+ const SdpMediaSection* msection(FindMsectionByMid(sdp, group.tags[0]));
+
+ if (!msection) {
+ SDP_SET_ERROR(
+ "mid specified for bundle transport in group attribute"
+ " does not exist in the SDP. (mid="
+ << group.tags[0] << ")");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (MsectionIsDisabled(*msection)) {
+ SDP_SET_ERROR(
+ "mid specified for bundle transport in group attribute"
+ " points at a disabled m-section. (mid="
+ << group.tags[0] << ")");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ for (const std::string& mid : group.tags) {
+ if (bundledMids->count(mid)) {
+ SDP_SET_ERROR("mid \'" << mid
+ << "\' appears more than once in a "
+ "BUNDLE group");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ (*bundledMids)[mid] = msection;
+ }
+ }
+
+ return NS_OK;
+}
+
+bool SdpHelper::OwnsTransport(const Sdp& sdp, uint16_t level,
+ sdp::SdpType type) {
+ auto& msection = sdp.GetMediaSection(level);
+
+ BundledMids bundledMids;
+ nsresult rv = GetBundledMids(sdp, &bundledMids);
+ if (NS_FAILED(rv)) {
+ // Should have been caught sooner.
+ MOZ_ASSERT(false);
+ return true;
+ }
+
+ return OwnsTransport(msection, bundledMids, type);
+}
+
+bool SdpHelper::OwnsTransport(const SdpMediaSection& msection,
+ const BundledMids& bundledMids,
+ sdp::SdpType type) {
+ if (MsectionIsDisabled(msection)) {
+ return false;
+ }
+
+ if (!msection.GetAttributeList().HasAttribute(SdpAttribute::kMidAttribute)) {
+ // No mid, definitely no bundle for this m-section
+ return true;
+ }
+ std::string mid(msection.GetAttributeList().GetMid());
+ if (type != sdp::kOffer || msection.GetAttributeList().HasAttribute(
+ SdpAttribute::kBundleOnlyAttribute)) {
+ // If this is an answer, or this m-section is marked bundle-only, the group
+ // attribute is authoritative. Otherwise, we aren't sure.
+ if (bundledMids.count(mid) && &msection != bundledMids.at(mid)) {
+ // mid is bundled, and isn't the bundle m-section
+ return false;
+ }
+ }
+
+ return true;
+}
+
+nsresult SdpHelper::GetMidFromLevel(const Sdp& sdp, uint16_t level,
+ std::string* mid) {
+ if (level >= sdp.GetMediaSectionCount()) {
+ SDP_SET_ERROR("Index " << level << " out of range");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ const SdpMediaSection& msection = sdp.GetMediaSection(level);
+ const SdpAttributeList& attrList = msection.GetAttributeList();
+
+ // grab the mid and set the outparam
+ if (attrList.HasAttribute(SdpAttribute::kMidAttribute)) {
+ *mid = attrList.GetMid();
+ }
+
+ return NS_OK;
+}
+
+nsresult SdpHelper::AddCandidateToSdp(Sdp* sdp,
+ const std::string& candidateUntrimmed,
+ uint16_t level,
+ const std::string& ufrag) {
+ if (level >= sdp->GetMediaSectionCount()) {
+ SDP_SET_ERROR("Index " << level << " out of range");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ SdpMediaSection& msection = sdp->GetMediaSection(level);
+ SdpAttributeList& attrList = msection.GetAttributeList();
+
+ if (!ufrag.empty()) {
+ if (!attrList.HasAttribute(SdpAttribute::kIceUfragAttribute) ||
+ attrList.GetIceUfrag() != ufrag) {
+ SDP_SET_ERROR("Unknown ufrag (" << ufrag << ")");
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+
+ if (candidateUntrimmed.empty()) {
+ SetIceGatheringComplete(sdp, level, ufrag);
+ return NS_OK;
+ }
+
+ // Trim off '[a=]candidate:'
+ size_t begin = candidateUntrimmed.find(':');
+ if (begin == std::string::npos) {
+ SDP_SET_ERROR("Invalid candidate, no ':' (" << candidateUntrimmed << ")");
+ return NS_ERROR_INVALID_ARG;
+ }
+ ++begin;
+
+ std::string candidate = candidateUntrimmed.substr(begin);
+
+ UniquePtr<SdpMultiStringAttribute> candidates;
+ if (!attrList.HasAttribute(SdpAttribute::kCandidateAttribute)) {
+ // Create new
+ candidates.reset(
+ new SdpMultiStringAttribute(SdpAttribute::kCandidateAttribute));
+ } else {
+ // Copy existing
+ candidates.reset(new SdpMultiStringAttribute(
+ *static_cast<const SdpMultiStringAttribute*>(
+ attrList.GetAttribute(SdpAttribute::kCandidateAttribute))));
+ }
+ candidates->PushEntry(candidate);
+ attrList.SetAttribute(candidates.release());
+
+ return NS_OK;
+}
+
+nsresult SdpHelper::SetIceGatheringComplete(Sdp* sdp,
+ const std::string& ufrag) {
+ for (uint16_t i = 0; i < sdp->GetMediaSectionCount(); ++i) {
+ nsresult rv = SetIceGatheringComplete(sdp, i, ufrag);
+ NS_ENSURE_SUCCESS(rv, rv);
+ }
+ return NS_OK;
+}
+
+nsresult SdpHelper::SetIceGatheringComplete(Sdp* sdp, uint16_t level,
+ const std::string& ufrag) {
+ if (level >= sdp->GetMediaSectionCount()) {
+ SDP_SET_ERROR("Index " << level << " out of range");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ SdpMediaSection& msection = sdp->GetMediaSection(level);
+ SdpAttributeList& attrList = msection.GetAttributeList();
+
+ if (!ufrag.empty()) {
+ if (!attrList.HasAttribute(SdpAttribute::kIceUfragAttribute) ||
+ attrList.GetIceUfrag() != ufrag) {
+ SDP_SET_ERROR("Unknown ufrag (" << ufrag << ")");
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+
+ attrList.SetAttribute(
+ new SdpFlagAttribute(SdpAttribute::kEndOfCandidatesAttribute));
+ // Remove trickle-ice option
+ attrList.RemoveAttribute(SdpAttribute::kIceOptionsAttribute);
+ return NS_OK;
+}
+
+void SdpHelper::SetDefaultAddresses(const std::string& defaultCandidateAddr,
+ uint16_t defaultCandidatePort,
+ const std::string& defaultRtcpCandidateAddr,
+ uint16_t defaultRtcpCandidatePort,
+ SdpMediaSection* msection) {
+ SdpAttributeList& attrList = msection->GetAttributeList();
+
+ msection->GetConnection().SetAddress(defaultCandidateAddr);
+ msection->SetPort(defaultCandidatePort);
+ if (!defaultRtcpCandidateAddr.empty()) {
+ sdp::AddrType ipVersion = sdp::kIPv4;
+ if (defaultRtcpCandidateAddr.find(':') != std::string::npos) {
+ ipVersion = sdp::kIPv6;
+ }
+ attrList.SetAttribute(new SdpRtcpAttribute(defaultRtcpCandidatePort,
+ sdp::kInternet, ipVersion,
+ defaultRtcpCandidateAddr));
+ }
+}
+
+nsresult SdpHelper::GetIdsFromMsid(const Sdp& sdp,
+ const SdpMediaSection& msection,
+ std::vector<std::string>* streamIds) {
+ std::vector<SdpMsidAttributeList::Msid> allMsids;
+ nsresult rv = GetMsids(msection, &allMsids);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (allMsids.empty()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ streamIds->clear();
+ for (const auto& msid : allMsids) {
+ // "-" means no stream, see draft-ietf-mmusic-msid
+ // Remove duplicates, but leave order the same
+ if (msid.identifier != "-" &&
+ !std::count(streamIds->begin(), streamIds->end(), msid.identifier)) {
+ streamIds->push_back(msid.identifier);
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult SdpHelper::GetMsids(const SdpMediaSection& msection,
+ std::vector<SdpMsidAttributeList::Msid>* msids) {
+ if (msection.GetAttributeList().HasAttribute(SdpAttribute::kMsidAttribute)) {
+ *msids = msection.GetAttributeList().GetMsid().mMsids;
+ return NS_OK;
+ }
+
+ // If there are no a=msid, can we find msids in ssrc attributes?
+ // (Chrome does not put plain-old msid attributes in its SDP)
+ if (msection.GetAttributeList().HasAttribute(SdpAttribute::kSsrcAttribute)) {
+ auto& ssrcs = msection.GetAttributeList().GetSsrc().mSsrcs;
+
+ for (auto i = ssrcs.begin(); i != ssrcs.end(); ++i) {
+ if (i->attribute.find("msid:") == 0) {
+ std::string streamId;
+ std::string trackId;
+ nsresult rv = ParseMsid(i->attribute, &streamId, &trackId);
+ NS_ENSURE_SUCCESS(rv, rv);
+ msids->push_back({streamId, trackId});
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult SdpHelper::ParseMsid(const std::string& msidAttribute,
+ std::string* streamId, std::string* trackId) {
+ // Would be nice if SdpSsrcAttributeList could parse out the contained
+ // attribute, but at least the parse here is simple.
+ // We are being very forgiving here wrt whitespace; tabs are not actually
+ // allowed, nor is leading/trailing whitespace.
+ size_t streamIdStart = msidAttribute.find_first_not_of(" \t", 5);
+ // We do not assume the appdata token is here, since this is not
+ // necessarily a webrtc msid
+ if (streamIdStart == std::string::npos) {
+ SDP_SET_ERROR("Malformed source-level msid attribute: " << msidAttribute);
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ size_t streamIdEnd = msidAttribute.find_first_of(" \t", streamIdStart);
+ if (streamIdEnd == std::string::npos) {
+ streamIdEnd = msidAttribute.size();
+ }
+
+ size_t trackIdStart = msidAttribute.find_first_not_of(" \t", streamIdEnd);
+ if (trackIdStart == std::string::npos) {
+ trackIdStart = msidAttribute.size();
+ }
+
+ size_t trackIdEnd = msidAttribute.find_first_of(" \t", trackIdStart);
+ if (trackIdEnd == std::string::npos) {
+ trackIdEnd = msidAttribute.size();
+ }
+
+ size_t streamIdSize = streamIdEnd - streamIdStart;
+ size_t trackIdSize = trackIdEnd - trackIdStart;
+
+ *streamId = msidAttribute.substr(streamIdStart, streamIdSize);
+ *trackId = msidAttribute.substr(trackIdStart, trackIdSize);
+ return NS_OK;
+}
+
+void SdpHelper::SetupMsidSemantic(const std::vector<std::string>& msids,
+ Sdp* sdp) const {
+ if (!msids.empty()) {
+ UniquePtr<SdpMsidSemanticAttributeList> msidSemantics(
+ new SdpMsidSemanticAttributeList);
+ msidSemantics->PushEntry("WMS", msids);
+ sdp->GetAttributeList().SetAttribute(msidSemantics.release());
+ }
+}
+
+std::string SdpHelper::GetCNAME(const SdpMediaSection& msection) const {
+ if (msection.GetAttributeList().HasAttribute(SdpAttribute::kSsrcAttribute)) {
+ auto& ssrcs = msection.GetAttributeList().GetSsrc().mSsrcs;
+ for (auto i = ssrcs.begin(); i != ssrcs.end(); ++i) {
+ if (i->attribute.find("cname:") == 0) {
+ return i->attribute.substr(6);
+ }
+ }
+ }
+ return "";
+}
+
+const SdpMediaSection* SdpHelper::FindMsectionByMid(
+ const Sdp& sdp, const std::string& mid) const {
+ for (size_t i = 0; i < sdp.GetMediaSectionCount(); ++i) {
+ auto& attrs = sdp.GetMediaSection(i).GetAttributeList();
+ if (attrs.HasAttribute(SdpAttribute::kMidAttribute) &&
+ attrs.GetMid() == mid) {
+ return &sdp.GetMediaSection(i);
+ }
+ }
+ return nullptr;
+}
+
+SdpMediaSection* SdpHelper::FindMsectionByMid(Sdp& sdp,
+ const std::string& mid) const {
+ for (size_t i = 0; i < sdp.GetMediaSectionCount(); ++i) {
+ auto& attrs = sdp.GetMediaSection(i).GetAttributeList();
+ if (attrs.HasAttribute(SdpAttribute::kMidAttribute) &&
+ attrs.GetMid() == mid) {
+ return &sdp.GetMediaSection(i);
+ }
+ }
+ return nullptr;
+}
+
+nsresult SdpHelper::CopyStickyParams(const SdpMediaSection& source,
+ SdpMediaSection* dest) {
+ auto& sourceAttrs = source.GetAttributeList();
+ auto& destAttrs = dest->GetAttributeList();
+
+ // There's no reason to renegotiate rtcp-mux
+ if (sourceAttrs.HasAttribute(SdpAttribute::kRtcpMuxAttribute)) {
+ destAttrs.SetAttribute(
+ new SdpFlagAttribute(SdpAttribute::kRtcpMuxAttribute));
+ }
+
+ // mid should stay the same
+ if (sourceAttrs.HasAttribute(SdpAttribute::kMidAttribute)) {
+ destAttrs.SetAttribute(new SdpStringAttribute(SdpAttribute::kMidAttribute,
+ sourceAttrs.GetMid()));
+ }
+
+ // Keep RTCP mode setting
+ if (sourceAttrs.HasAttribute(SdpAttribute::kRtcpRsizeAttribute) &&
+ source.GetMediaType() == SdpMediaSection::kVideo) {
+ destAttrs.SetAttribute(
+ new SdpFlagAttribute(SdpAttribute::kRtcpRsizeAttribute));
+ }
+
+ return NS_OK;
+}
+
+bool SdpHelper::HasRtcp(SdpMediaSection::Protocol proto) const {
+ switch (proto) {
+ case SdpMediaSection::kRtpAvpf:
+ case SdpMediaSection::kDccpRtpAvpf:
+ case SdpMediaSection::kDccpRtpSavpf:
+ case SdpMediaSection::kRtpSavpf:
+ case SdpMediaSection::kUdpTlsRtpSavpf:
+ case SdpMediaSection::kTcpDtlsRtpSavpf:
+ case SdpMediaSection::kDccpTlsRtpSavpf:
+ return true;
+ case SdpMediaSection::kRtpAvp:
+ case SdpMediaSection::kUdp:
+ case SdpMediaSection::kVat:
+ case SdpMediaSection::kRtp:
+ case SdpMediaSection::kUdptl:
+ case SdpMediaSection::kTcp:
+ case SdpMediaSection::kTcpRtpAvp:
+ case SdpMediaSection::kRtpSavp:
+ case SdpMediaSection::kTcpBfcp:
+ case SdpMediaSection::kTcpTlsBfcp:
+ case SdpMediaSection::kTcpTls:
+ case SdpMediaSection::kFluteUdp:
+ case SdpMediaSection::kTcpMsrp:
+ case SdpMediaSection::kTcpTlsMsrp:
+ case SdpMediaSection::kDccp:
+ case SdpMediaSection::kDccpRtpAvp:
+ case SdpMediaSection::kDccpRtpSavp:
+ case SdpMediaSection::kUdpTlsRtpSavp:
+ case SdpMediaSection::kTcpDtlsRtpSavp:
+ case SdpMediaSection::kDccpTlsRtpSavp:
+ case SdpMediaSection::kUdpMbmsFecRtpAvp:
+ case SdpMediaSection::kUdpMbmsFecRtpSavp:
+ case SdpMediaSection::kUdpMbmsRepair:
+ case SdpMediaSection::kFecUdp:
+ case SdpMediaSection::kUdpFec:
+ case SdpMediaSection::kTcpMrcpv2:
+ case SdpMediaSection::kTcpTlsMrcpv2:
+ case SdpMediaSection::kPstn:
+ case SdpMediaSection::kUdpTlsUdptl:
+ case SdpMediaSection::kSctp:
+ case SdpMediaSection::kDtlsSctp:
+ case SdpMediaSection::kUdpDtlsSctp:
+ case SdpMediaSection::kTcpDtlsSctp:
+ return false;
+ }
+ MOZ_CRASH("Unknown protocol, probably corruption.");
+}
+
+SdpMediaSection::Protocol SdpHelper::GetProtocolForMediaType(
+ SdpMediaSection::MediaType type) {
+ if (type == SdpMediaSection::kApplication) {
+ return SdpMediaSection::kUdpDtlsSctp;
+ }
+
+ return SdpMediaSection::kUdpTlsRtpSavpf;
+}
+
+void SdpHelper::AppendSdpParseErrors(
+ const std::vector<std::pair<size_t, std::string> >& aErrors,
+ std::string* aErrorString) {
+ std::ostringstream os;
+ for (auto i = aErrors.begin(); i != aErrors.end(); ++i) {
+ os << "SDP Parse Error on line " << i->first << ": " + i->second
+ << std::endl;
+ }
+ *aErrorString += os.str();
+}
+
+/* static */
+bool SdpHelper::GetPtAsInt(const std::string& ptString, uint16_t* ptOutparam) {
+ char* end;
+ unsigned long pt = strtoul(ptString.c_str(), &end, 10);
+ size_t length = static_cast<size_t>(end - ptString.c_str());
+ if ((pt > UINT16_MAX) || (length != ptString.size())) {
+ return false;
+ }
+ *ptOutparam = pt;
+ return true;
+}
+
+void SdpHelper::NegotiateAndAddExtmaps(
+ const SdpMediaSection& remoteMsection,
+ std::vector<SdpExtmapAttributeList::Extmap>& localExtensions,
+ SdpMediaSection* localMsection) {
+ if (!remoteMsection.GetAttributeList().HasAttribute(
+ SdpAttribute::kExtmapAttribute)) {
+ return;
+ }
+
+ UniquePtr<SdpExtmapAttributeList> localExtmap(new SdpExtmapAttributeList);
+ auto& theirExtmap = remoteMsection.GetAttributeList().GetExtmap().mExtmaps;
+ for (const auto& theirExt : theirExtmap) {
+ for (auto& ourExt : localExtensions) {
+ if (theirExt.entry == 0) {
+ // 0 is invalid, ignore it
+ continue;
+ }
+
+ if (theirExt.extensionname != ourExt.extensionname) {
+ continue;
+ }
+
+ ourExt.direction = reverse(theirExt.direction) & ourExt.direction;
+ if (ourExt.direction == SdpDirectionAttribute::Direction::kInactive) {
+ continue;
+ }
+
+ // RFC 5285 says that ids >= 4096 can be used by the offerer to
+ // force the answerer to pick, otherwise the value in the offer is
+ // used.
+ if (theirExt.entry < 4096) {
+ ourExt.entry = theirExt.entry;
+ }
+
+ localExtmap->mExtmaps.push_back(ourExt);
+ }
+ }
+
+ if (!localExtmap->mExtmaps.empty()) {
+ localMsection->GetAttributeList().SetAttribute(localExtmap.release());
+ }
+}
+
+static bool AttributeListMatch(const SdpAttributeList& list1,
+ const SdpAttributeList& list2) {
+ // TODO: Consider adding telemetry in this function to record which
+ // attributes don't match. See Bug 1432955.
+ for (int i = SdpAttribute::kFirstAttribute; i <= SdpAttribute::kLastAttribute;
+ i++) {
+ auto attributeType = static_cast<SdpAttribute::AttributeType>(i);
+ // TODO: We should do more thorough checking here, e.g. serialize and
+ // compare strings. See Bug 1439690.
+ if (list1.HasAttribute(attributeType, false) !=
+ list2.HasAttribute(attributeType, false)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+static bool MediaSectionMatch(const SdpMediaSection& mediaSection1,
+ const SdpMediaSection& mediaSection2) {
+ // TODO: We should do more thorough checking in this function.
+ // See Bug 1439690.
+ if (!AttributeListMatch(mediaSection1.GetAttributeList(),
+ mediaSection2.GetAttributeList())) {
+ return false;
+ }
+ if (mediaSection1.GetPort() != mediaSection2.GetPort()) {
+ return false;
+ }
+ const std::vector<std::string>& formats1 = mediaSection1.GetFormats();
+ const std::vector<std::string>& formats2 = mediaSection2.GetFormats();
+ auto formats1Set = std::set<std::string>(formats1.begin(), formats1.end());
+ auto formats2Set = std::set<std::string>(formats2.begin(), formats2.end());
+ if (formats1Set != formats2Set) {
+ return false;
+ }
+ return true;
+}
+
+bool SdpHelper::SdpMatch(const Sdp& sdp1, const Sdp& sdp2) {
+ if (sdp1.GetMediaSectionCount() != sdp2.GetMediaSectionCount()) {
+ return false;
+ }
+ if (!AttributeListMatch(sdp1.GetAttributeList(), sdp2.GetAttributeList())) {
+ return false;
+ }
+ for (size_t i = 0; i < sdp1.GetMediaSectionCount(); i++) {
+ const SdpMediaSection& mediaSection1 = sdp1.GetMediaSection(i);
+ const SdpMediaSection& mediaSection2 = sdp2.GetMediaSection(i);
+ if (!MediaSectionMatch(mediaSection1, mediaSection2)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+nsresult SdpHelper::ValidateTransportAttributes(const Sdp& aSdp,
+ sdp::SdpType aType) {
+ BundledMids bundledMids;
+ nsresult rv = GetBundledMids(aSdp, &bundledMids);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ for (size_t level = 0; level < aSdp.GetMediaSectionCount(); ++level) {
+ const auto& msection = aSdp.GetMediaSection(level);
+ if (OwnsTransport(msection, bundledMids, aType)) {
+ const auto& mediaAttrs = msection.GetAttributeList();
+ if (mediaAttrs.GetIceUfrag().empty()) {
+ SDP_SET_ERROR("Invalid description, no ice-ufrag attribute at level "
+ << level);
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (mediaAttrs.GetIcePwd().empty()) {
+ SDP_SET_ERROR("Invalid description, no ice-pwd attribute at level "
+ << level);
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (!mediaAttrs.HasAttribute(SdpAttribute::kFingerprintAttribute)) {
+ SDP_SET_ERROR("Invalid description, no fingerprint attribute at level "
+ << level);
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ const SdpFingerprintAttributeList& fingerprints(
+ mediaAttrs.GetFingerprint());
+ if (fingerprints.mFingerprints.empty()) {
+ SDP_SET_ERROR(
+ "Invalid description, no supported fingerprint algorithms present "
+ "at level "
+ << level);
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (mediaAttrs.HasAttribute(SdpAttribute::kSetupAttribute, true)) {
+ if (mediaAttrs.GetSetup().mRole == SdpSetupAttribute::kHoldconn) {
+ SDP_SET_ERROR(
+ "Invalid description, illegal setup attribute \"holdconn\" "
+ "at level "
+ << level);
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (aType == sdp::kAnswer &&
+ mediaAttrs.GetSetup().mRole == SdpSetupAttribute::kActpass) {
+ SDP_SET_ERROR(
+ "Invalid answer, illegal setup attribute \"actpass\" at level "
+ << level);
+ return NS_ERROR_INVALID_ARG;
+ }
+ } else if (aType == sdp::kOffer) {
+ SDP_SET_ERROR("Invalid offer, no setup attribute at level " << level);
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/sdp/SdpHelper.h b/dom/media/webrtc/sdp/SdpHelper.h
new file mode 100644
index 0000000000..3c65d01442
--- /dev/null
+++ b/dom/media/webrtc/sdp/SdpHelper.h
@@ -0,0 +1,109 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _SDPHELPER_H_
+#define _SDPHELPER_H_
+
+#include "nsError.h"
+
+#include "sdp/SdpMediaSection.h"
+#include "sdp/SdpAttribute.h"
+
+#include "transport/m_cpp_utils.h"
+
+#include <string>
+#include <map>
+#include <vector>
+
+namespace mozilla {
+class SdpMediaSection;
+class Sdp;
+
+class SdpHelper {
+ public:
+ // Takes a std::string* into which error strings will be written for the
+ // lifetime of the SdpHelper.
+ explicit SdpHelper(std::string* errorDest) : mLastError(*errorDest) {}
+ ~SdpHelper() {}
+
+ nsresult GetComponent(const std::string& candidate, size_t* component);
+ nsresult CopyTransportParams(size_t numComponents,
+ const SdpMediaSection& source,
+ SdpMediaSection* dest);
+ bool AreOldTransportParamsValid(const Sdp& oldAnswer,
+ const Sdp& offerersPreviousSdp,
+ const Sdp& newOffer, size_t level);
+ bool IceCredentialsDiffer(const SdpMediaSection& msection1,
+ const SdpMediaSection& msection2);
+
+ bool MsectionIsDisabled(const SdpMediaSection& msection) const;
+ static void DisableMsection(Sdp* sdp, SdpMediaSection* msection);
+
+ // Maps each mid to the m-section that owns its bundle transport.
+ // Mids that do not appear in an a=group:BUNDLE do not appear here.
+ typedef std::map<std::string, const SdpMediaSection*> BundledMids;
+
+ nsresult GetBundledMids(const Sdp& sdp, BundledMids* bundledMids);
+
+ bool OwnsTransport(const Sdp& localSdp, uint16_t level, sdp::SdpType type);
+ bool OwnsTransport(const SdpMediaSection& msection,
+ const BundledMids& bundledMids, sdp::SdpType type);
+ void GetBundleGroups(const Sdp& sdp,
+ std::vector<SdpGroupAttributeList::Group>* groups) const;
+
+ nsresult GetMidFromLevel(const Sdp& sdp, uint16_t level, std::string* mid);
+ nsresult GetIdsFromMsid(const Sdp& sdp, const SdpMediaSection& msection,
+ std::vector<std::string>* streamId);
+ nsresult GetMsids(const SdpMediaSection& msection,
+ std::vector<SdpMsidAttributeList::Msid>* msids);
+ nsresult ParseMsid(const std::string& msidAttribute, std::string* streamId,
+ std::string* trackId);
+ nsresult AddCandidateToSdp(Sdp* sdp, const std::string& candidate,
+ uint16_t level, const std::string& ufrag);
+ nsresult SetIceGatheringComplete(Sdp* sdp, const std::string& ufrag);
+ nsresult SetIceGatheringComplete(Sdp* sdp, uint16_t level,
+ const std::string& ufrag);
+ void SetDefaultAddresses(const std::string& defaultCandidateAddr,
+ uint16_t defaultCandidatePort,
+ const std::string& defaultRtcpCandidateAddr,
+ uint16_t defaultRtcpCandidatePort,
+ SdpMediaSection* msection);
+ void SetupMsidSemantic(const std::vector<std::string>& msids, Sdp* sdp) const;
+
+ std::string GetCNAME(const SdpMediaSection& msection) const;
+
+ SdpMediaSection* FindMsectionByMid(Sdp& sdp, const std::string& mid) const;
+
+ const SdpMediaSection* FindMsectionByMid(const Sdp& sdp,
+ const std::string& mid) const;
+
+ nsresult CopyStickyParams(const SdpMediaSection& source,
+ SdpMediaSection* dest);
+ bool HasRtcp(SdpMediaSection::Protocol proto) const;
+ static SdpMediaSection::Protocol GetProtocolForMediaType(
+ SdpMediaSection::MediaType type);
+ void AppendSdpParseErrors(
+ const std::vector<std::pair<size_t, std::string> >& aErrors,
+ std::string* aErrorString);
+
+ static bool GetPtAsInt(const std::string& ptString, uint16_t* ptOutparam);
+
+ void NegotiateAndAddExtmaps(
+ const SdpMediaSection& remoteMsection,
+ std::vector<SdpExtmapAttributeList::Extmap>& localExtensions,
+ SdpMediaSection* localMsection);
+
+ bool SdpMatch(const Sdp& sdp1, const Sdp& sdp2);
+ nsresult ValidateTransportAttributes(const Sdp& aSdp, sdp::SdpType aType);
+
+ private:
+ std::string& mLastError;
+
+ DISALLOW_COPY_ASSIGN(SdpHelper);
+};
+} // namespace mozilla
+
+#endif // _SDPHELPER_H_
diff --git a/dom/media/webrtc/sdp/SdpLog.cpp b/dom/media/webrtc/sdp/SdpLog.cpp
new file mode 100644
index 0000000000..a841ac7412
--- /dev/null
+++ b/dom/media/webrtc/sdp/SdpLog.cpp
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include <type_traits>
+
+#include "sdp/SdpLog.h"
+#include "common/browser_logging/CSFLog.h"
+
+namespace mozilla {
+LazyLogModule SdpLog("sdp");
+} // namespace mozilla
+
+// For compile time enum comparison
+template <typename E, typename F>
+constexpr bool compareEnum(E e, F f) {
+ return static_cast<typename std::underlying_type<E>::type>(e) ==
+ static_cast<typename std::underlying_type<F>::type>(f);
+}
+
+CSFLogLevel SDPToCSFLogLevel(const SDPLogLevel priority) {
+ static_assert(compareEnum(SDP_LOG_ERROR, CSF_LOG_ERROR));
+ static_assert(compareEnum(SDP_LOG_WARNING, CSF_LOG_WARNING));
+ static_assert(compareEnum(SDP_LOG_INFO, CSF_LOG_INFO));
+ static_assert(compareEnum(SDP_LOG_DEBUG, CSF_LOG_DEBUG));
+ static_assert(compareEnum(SDP_LOG_VERBOSE, CSF_LOG_VERBOSE));
+
+ // Check that all SDP_LOG_* cases are covered. It compiles to nothing.
+ switch (priority) {
+ case SDP_LOG_ERROR:
+ case SDP_LOG_WARNING:
+ case SDP_LOG_INFO:
+ case SDP_LOG_DEBUG:
+ case SDP_LOG_VERBOSE:
+ break;
+ }
+
+ // Ditto for CSF_LOG_*
+ switch (static_cast<CSFLogLevel>(priority)) {
+ case CSF_LOG_ERROR:
+ case CSF_LOG_WARNING:
+ case CSF_LOG_INFO:
+ case CSF_LOG_DEBUG:
+ case CSF_LOG_VERBOSE:
+ break;
+ }
+
+ return static_cast<CSFLogLevel>(priority);
+}
+
+void SDPLog(SDPLogLevel priority, const char* sourceFile, int sourceLine,
+ const char* tag, const char* format, ...) {
+ va_list ap;
+ va_start(ap, format);
+ CSFLogV(SDPToCSFLogLevel(priority), sourceFile, sourceLine, tag, format, ap);
+ va_end(ap);
+}
+
+void SDPLogV(SDPLogLevel priority, const char* sourceFile, int sourceLine,
+ const char* tag, const char* format, va_list args) {
+ CSFLogV(SDPToCSFLogLevel(priority), sourceFile, sourceLine, tag, format,
+ args);
+}
+
+int SDPLogTestLevel(SDPLogLevel priority) {
+ return CSFLogTestLevel(SDPToCSFLogLevel(priority));
+}
diff --git a/dom/media/webrtc/sdp/SdpLog.h b/dom/media/webrtc/sdp/SdpLog.h
new file mode 100644
index 0000000000..63bceff889
--- /dev/null
+++ b/dom/media/webrtc/sdp/SdpLog.h
@@ -0,0 +1,17 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _SDP_LOG_H_
+#define _SDP_LOG_H_
+
+#include "mozilla/Logging.h"
+#include "sdp_log.h"
+
+namespace mozilla {
+extern mozilla::LazyLogModule SdpLog;
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/sdp/SdpMediaSection.cpp b/dom/media/webrtc/sdp/SdpMediaSection.cpp
new file mode 100644
index 0000000000..dd8f6e54fe
--- /dev/null
+++ b/dom/media/webrtc/sdp/SdpMediaSection.cpp
@@ -0,0 +1,197 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "sdp/SdpMediaSection.h"
+
+namespace mozilla {
+const SdpFmtpAttributeList::Parameters* SdpMediaSection::FindFmtp(
+ const std::string& pt) const {
+ const SdpAttributeList& attrs = GetAttributeList();
+
+ if (attrs.HasAttribute(SdpAttribute::kFmtpAttribute)) {
+ for (auto& fmtpAttr : attrs.GetFmtp().mFmtps) {
+ if (fmtpAttr.format == pt && fmtpAttr.parameters) {
+ return fmtpAttr.parameters.get();
+ }
+ }
+ }
+ return nullptr;
+}
+
+void SdpMediaSection::SetFmtp(const SdpFmtpAttributeList::Fmtp& fmtpToSet) {
+ UniquePtr<SdpFmtpAttributeList> fmtps(new SdpFmtpAttributeList);
+
+ if (GetAttributeList().HasAttribute(SdpAttribute::kFmtpAttribute)) {
+ *fmtps = GetAttributeList().GetFmtp();
+ }
+
+ bool found = false;
+ for (SdpFmtpAttributeList::Fmtp& fmtp : fmtps->mFmtps) {
+ if (fmtp.format == fmtpToSet.format) {
+ fmtp = fmtpToSet;
+ found = true;
+ }
+ }
+
+ if (!found) {
+ fmtps->mFmtps.push_back(fmtpToSet);
+ }
+
+ GetAttributeList().SetAttribute(fmtps.release());
+}
+
+void SdpMediaSection::RemoveFmtp(const std::string& pt) {
+ UniquePtr<SdpFmtpAttributeList> fmtps(new SdpFmtpAttributeList);
+
+ SdpAttributeList& attrList = GetAttributeList();
+ if (attrList.HasAttribute(SdpAttribute::kFmtpAttribute)) {
+ *fmtps = attrList.GetFmtp();
+ }
+
+ for (size_t i = 0; i < fmtps->mFmtps.size(); ++i) {
+ if (pt == fmtps->mFmtps[i].format) {
+ fmtps->mFmtps.erase(fmtps->mFmtps.begin() + i);
+ break;
+ }
+ }
+
+ attrList.SetAttribute(fmtps.release());
+}
+
+const SdpRtpmapAttributeList::Rtpmap* SdpMediaSection::FindRtpmap(
+ const std::string& pt) const {
+ auto& attrs = GetAttributeList();
+ if (!attrs.HasAttribute(SdpAttribute::kRtpmapAttribute)) {
+ return nullptr;
+ }
+
+ const SdpRtpmapAttributeList& rtpmap = attrs.GetRtpmap();
+ if (!rtpmap.HasEntry(pt)) {
+ return nullptr;
+ }
+
+ return &rtpmap.GetEntry(pt);
+}
+
+const SdpSctpmapAttributeList::Sctpmap* SdpMediaSection::GetSctpmap() const {
+ auto& attrs = GetAttributeList();
+ if (!attrs.HasAttribute(SdpAttribute::kSctpmapAttribute)) {
+ return nullptr;
+ }
+
+ const SdpSctpmapAttributeList& sctpmap = attrs.GetSctpmap();
+ if (sctpmap.mSctpmaps.empty()) {
+ return nullptr;
+ }
+
+ return &sctpmap.GetFirstEntry();
+}
+
+uint32_t SdpMediaSection::GetSctpPort() const {
+ auto& attrs = GetAttributeList();
+ if (!attrs.HasAttribute(SdpAttribute::kSctpPortAttribute)) {
+ return 0;
+ }
+
+ return attrs.GetSctpPort();
+}
+
+bool SdpMediaSection::GetMaxMessageSize(uint32_t* size) const {
+ *size = 0;
+
+ auto& attrs = GetAttributeList();
+ if (!attrs.HasAttribute(SdpAttribute::kMaxMessageSizeAttribute)) {
+ return false;
+ }
+
+ *size = attrs.GetMaxMessageSize();
+ return true;
+}
+
+bool SdpMediaSection::HasRtcpFb(const std::string& pt,
+ SdpRtcpFbAttributeList::Type type,
+ const std::string& subType) const {
+ const SdpAttributeList& attrs(GetAttributeList());
+
+ if (!attrs.HasAttribute(SdpAttribute::kRtcpFbAttribute)) {
+ return false;
+ }
+
+ for (auto& rtcpfb : attrs.GetRtcpFb().mFeedbacks) {
+ if (rtcpfb.type == type) {
+ if (rtcpfb.pt == "*" || rtcpfb.pt == pt) {
+ if (rtcpfb.parameter == subType) {
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+}
+
+SdpRtcpFbAttributeList SdpMediaSection::GetRtcpFbs() const {
+ SdpRtcpFbAttributeList result;
+ if (GetAttributeList().HasAttribute(SdpAttribute::kRtcpFbAttribute)) {
+ result = GetAttributeList().GetRtcpFb();
+ }
+ return result;
+}
+
+void SdpMediaSection::SetRtcpFbs(const SdpRtcpFbAttributeList& rtcpfbs) {
+ if (rtcpfbs.mFeedbacks.empty()) {
+ GetAttributeList().RemoveAttribute(SdpAttribute::kRtcpFbAttribute);
+ return;
+ }
+
+ GetAttributeList().SetAttribute(new SdpRtcpFbAttributeList(rtcpfbs));
+}
+
+void SdpMediaSection::SetSsrcs(const std::vector<uint32_t>& ssrcs,
+ const std::string& cname) {
+ if (ssrcs.empty()) {
+ GetAttributeList().RemoveAttribute(SdpAttribute::kSsrcAttribute);
+ return;
+ }
+
+ UniquePtr<SdpSsrcAttributeList> ssrcAttr(new SdpSsrcAttributeList);
+ for (auto ssrc : ssrcs) {
+ // When using ssrc attributes, we are required to at least have a cname.
+ // (See https://tools.ietf.org/html/rfc5576#section-6.1)
+ std::string cnameAttr("cname:");
+ cnameAttr += cname;
+ ssrcAttr->PushEntry(ssrc, cnameAttr);
+ }
+
+ GetAttributeList().SetAttribute(ssrcAttr.release());
+}
+
+void SdpMediaSection::AddMsid(const std::string& id,
+ const std::string& appdata) {
+ UniquePtr<SdpMsidAttributeList> msids(new SdpMsidAttributeList);
+ if (GetAttributeList().HasAttribute(SdpAttribute::kMsidAttribute)) {
+ msids->mMsids = GetAttributeList().GetMsid().mMsids;
+ }
+ msids->PushEntry(id, appdata);
+ GetAttributeList().SetAttribute(msids.release());
+}
+
+const SdpRidAttributeList::Rid* SdpMediaSection::FindRid(
+ const std::string& id) const {
+ if (!GetAttributeList().HasAttribute(SdpAttribute::kRidAttribute)) {
+ return nullptr;
+ }
+
+ for (const auto& rid : GetAttributeList().GetRid().mRids) {
+ if (rid.id == id) {
+ return &rid;
+ }
+ }
+
+ return nullptr;
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/sdp/SdpMediaSection.h b/dom/media/webrtc/sdp/SdpMediaSection.h
new file mode 100644
index 0000000000..a205551a1d
--- /dev/null
+++ b/dom/media/webrtc/sdp/SdpMediaSection.h
@@ -0,0 +1,317 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _SDPMEDIASECTION_H_
+#define _SDPMEDIASECTION_H_
+
+#include "mozilla/Maybe.h"
+#include "sdp/SdpEnum.h"
+#include "sdp/SdpAttributeList.h"
+#include <string>
+#include <vector>
+#include <sstream>
+
+namespace mozilla {
+
+class SdpAttributeList;
+
+class SdpConnection;
+
+class SdpMediaSection {
+ public:
+ enum MediaType { kAudio, kVideo, kText, kApplication, kMessage };
+ // don't add to enum to avoid warnings about unhandled enum values
+ static const size_t kMediaTypes = static_cast<size_t>(kMessage) + 1;
+
+ enum Protocol {
+ kRtpAvp, // RTP/AVP [RFC4566]
+ kUdp, // udp [RFC4566]
+ kVat, // vat [historic]
+ kRtp, // rtp [historic]
+ kUdptl, // udptl [ITU-T]
+ kTcp, // TCP [RFC4145]
+ kRtpAvpf, // RTP/AVPF [RFC4585]
+ kTcpRtpAvp, // TCP/RTP/AVP [RFC4571]
+ kRtpSavp, // RTP/SAVP [RFC3711]
+ kTcpBfcp, // TCP/BFCP [RFC4583]
+ kTcpTlsBfcp, // TCP/TLS/BFCP [RFC4583]
+ kTcpTls, // TCP/TLS [RFC4572]
+ kFluteUdp, // FLUTE/UDP [RFC-mehta-rmt-flute-sdp-05]
+ kTcpMsrp, // TCP/MSRP [RFC4975]
+ kTcpTlsMsrp, // TCP/TLS/MSRP [RFC4975]
+ kDccp, // DCCP [RFC5762]
+ kDccpRtpAvp, // DCCP/RTP/AVP [RFC5762]
+ kDccpRtpSavp, // DCCP/RTP/SAVP [RFC5762]
+ kDccpRtpAvpf, // DCCP/RTP/AVPF [RFC5762]
+ kDccpRtpSavpf, // DCCP/RTP/SAVPF [RFC5762]
+ kRtpSavpf, // RTP/SAVPF [RFC5124]
+ kUdpTlsRtpSavp, // UDP/TLS/RTP/SAVP [RFC5764]
+ kTcpDtlsRtpSavp, // TCP/DTLS/RTP/SAVP [RFC7850]
+ kDccpTlsRtpSavp, // DCCP/TLS/RTP/SAVP [RFC5764]
+ kUdpTlsRtpSavpf, // UDP/TLS/RTP/SAVPF [RFC5764]
+ kTcpDtlsRtpSavpf, // TCP/DTLS/RTP/SAVPF [RFC7850]
+ kDccpTlsRtpSavpf, // DCCP/TLS/RTP/SAVPF [RFC5764]
+ kUdpMbmsFecRtpAvp, // UDP/MBMS-FEC/RTP/AVP [RFC6064]
+ kUdpMbmsFecRtpSavp, // UDP/MBMS-FEC/RTP/SAVP [RFC6064]
+ kUdpMbmsRepair, // UDP/MBMS-REPAIR [RFC6064]
+ kFecUdp, // FEC/UDP [RFC6364]
+ kUdpFec, // UDP/FEC [RFC6364]
+ kTcpMrcpv2, // TCP/MRCPv2 [RFC6787]
+ kTcpTlsMrcpv2, // TCP/TLS/MRCPv2 [RFC6787]
+ kPstn, // PSTN [RFC7195]
+ kUdpTlsUdptl, // UDP/TLS/UDPTL [RFC7345]
+ kSctp, // SCTP [draft-ietf-mmusic-sctp-sdp-07]
+ kDtlsSctp, // DTLS/SCTP [draft-ietf-mmusic-sctp-sdp-07]
+ kUdpDtlsSctp, // UDP/DTLS/SCTP [draft-ietf-mmusic-sctp-sdp-21]
+ kTcpDtlsSctp // TCP/DTLS/SCTP [draft-ietf-mmusic-sctp-sdp-21]
+ };
+
+ explicit SdpMediaSection(size_t level) : mLevel(level) {}
+
+ virtual MediaType GetMediaType() const = 0;
+ virtual unsigned int GetPort() const = 0;
+ virtual void SetPort(unsigned int port) = 0;
+ virtual unsigned int GetPortCount() const = 0;
+ virtual Protocol GetProtocol() const = 0;
+ virtual const SdpConnection& GetConnection() const = 0;
+ virtual SdpConnection& GetConnection() = 0;
+ virtual uint32_t GetBandwidth(const std::string& type) const = 0;
+ virtual const std::vector<std::string>& GetFormats() const = 0;
+
+ std::vector<std::string> GetFormatsForSimulcastVersion(
+ size_t simulcastVersion, bool send, bool recv) const;
+ virtual const SdpAttributeList& GetAttributeList() const = 0;
+ virtual SdpAttributeList& GetAttributeList() = 0;
+
+ virtual SdpDirectionAttribute GetDirectionAttribute() const = 0;
+
+ virtual void Serialize(std::ostream&) const = 0;
+
+ virtual void AddCodec(const std::string& pt, const std::string& name,
+ uint32_t clockrate, uint16_t channels) = 0;
+ virtual void ClearCodecs() = 0;
+
+ virtual void AddDataChannel(const std::string& name, uint16_t port,
+ uint16_t streams, uint32_t message_size) = 0;
+
+ size_t GetLevel() const { return mLevel; }
+
+ inline bool IsReceiving() const { return GetDirection() & sdp::kRecv; }
+
+ inline bool IsSending() const { return GetDirection() & sdp::kSend; }
+
+ inline void SetReceiving(bool receiving) {
+ auto direction = GetDirection();
+ if (direction & sdp::kSend) {
+ SetDirection(receiving ? SdpDirectionAttribute::kSendrecv
+ : SdpDirectionAttribute::kSendonly);
+ } else {
+ SetDirection(receiving ? SdpDirectionAttribute::kRecvonly
+ : SdpDirectionAttribute::kInactive);
+ }
+ }
+
+ inline void SetSending(bool sending) {
+ auto direction = GetDirection();
+ if (direction & sdp::kRecv) {
+ SetDirection(sending ? SdpDirectionAttribute::kSendrecv
+ : SdpDirectionAttribute::kRecvonly);
+ } else {
+ SetDirection(sending ? SdpDirectionAttribute::kSendonly
+ : SdpDirectionAttribute::kInactive);
+ }
+ }
+
+ inline void SetDirection(SdpDirectionAttribute::Direction direction) {
+ GetAttributeList().SetAttribute(new SdpDirectionAttribute(direction));
+ }
+
+ inline SdpDirectionAttribute::Direction GetDirection() const {
+ return GetDirectionAttribute().mValue;
+ }
+
+ const SdpFmtpAttributeList::Parameters* FindFmtp(const std::string& pt) const;
+ void SetFmtp(const SdpFmtpAttributeList::Fmtp& fmtp);
+ void RemoveFmtp(const std::string& pt);
+ const SdpRtpmapAttributeList::Rtpmap* FindRtpmap(const std::string& pt) const;
+ const SdpSctpmapAttributeList::Sctpmap* GetSctpmap() const;
+ uint32_t GetSctpPort() const;
+ bool GetMaxMessageSize(uint32_t* size) const;
+ bool HasRtcpFb(const std::string& pt, SdpRtcpFbAttributeList::Type type,
+ const std::string& subType) const;
+ SdpRtcpFbAttributeList GetRtcpFbs() const;
+ void SetRtcpFbs(const SdpRtcpFbAttributeList& rtcpfbs);
+ bool HasFormat(const std::string& format) const {
+ return std::find(GetFormats().begin(), GetFormats().end(), format) !=
+ GetFormats().end();
+ }
+ void SetSsrcs(const std::vector<uint32_t>& ssrcs, const std::string& cname);
+ void AddMsid(const std::string& id, const std::string& appdata);
+ const SdpRidAttributeList::Rid* FindRid(const std::string& id) const;
+
+ private:
+ size_t mLevel;
+};
+
+inline std::ostream& operator<<(std::ostream& os, const SdpMediaSection& ms) {
+ ms.Serialize(os);
+ return os;
+}
+
+inline std::ostream& operator<<(std::ostream& os,
+ SdpMediaSection::MediaType t) {
+ switch (t) {
+ case SdpMediaSection::kAudio:
+ return os << "audio";
+ case SdpMediaSection::kVideo:
+ return os << "video";
+ case SdpMediaSection::kText:
+ return os << "text";
+ case SdpMediaSection::kApplication:
+ return os << "application";
+ case SdpMediaSection::kMessage:
+ return os << "message";
+ }
+ MOZ_ASSERT(false, "Unknown MediaType");
+ return os << "?";
+}
+
+inline std::ostream& operator<<(std::ostream& os, SdpMediaSection::Protocol p) {
+ switch (p) {
+ case SdpMediaSection::kRtpAvp:
+ return os << "RTP/AVP";
+ case SdpMediaSection::kUdp:
+ return os << "udp";
+ case SdpMediaSection::kVat:
+ return os << "vat";
+ case SdpMediaSection::kRtp:
+ return os << "rtp";
+ case SdpMediaSection::kUdptl:
+ return os << "udptl";
+ case SdpMediaSection::kTcp:
+ return os << "TCP";
+ case SdpMediaSection::kRtpAvpf:
+ return os << "RTP/AVPF";
+ case SdpMediaSection::kTcpRtpAvp:
+ return os << "TCP/RTP/AVP";
+ case SdpMediaSection::kRtpSavp:
+ return os << "RTP/SAVP";
+ case SdpMediaSection::kTcpBfcp:
+ return os << "TCP/BFCP";
+ case SdpMediaSection::kTcpTlsBfcp:
+ return os << "TCP/TLS/BFCP";
+ case SdpMediaSection::kTcpTls:
+ return os << "TCP/TLS";
+ case SdpMediaSection::kFluteUdp:
+ return os << "FLUTE/UDP";
+ case SdpMediaSection::kTcpMsrp:
+ return os << "TCP/MSRP";
+ case SdpMediaSection::kTcpTlsMsrp:
+ return os << "TCP/TLS/MSRP";
+ case SdpMediaSection::kDccp:
+ return os << "DCCP";
+ case SdpMediaSection::kDccpRtpAvp:
+ return os << "DCCP/RTP/AVP";
+ case SdpMediaSection::kDccpRtpSavp:
+ return os << "DCCP/RTP/SAVP";
+ case SdpMediaSection::kDccpRtpAvpf:
+ return os << "DCCP/RTP/AVPF";
+ case SdpMediaSection::kDccpRtpSavpf:
+ return os << "DCCP/RTP/SAVPF";
+ case SdpMediaSection::kRtpSavpf:
+ return os << "RTP/SAVPF";
+ case SdpMediaSection::kUdpTlsRtpSavp:
+ return os << "UDP/TLS/RTP/SAVP";
+ case SdpMediaSection::kTcpDtlsRtpSavp:
+ return os << "TCP/DTLS/RTP/SAVP";
+ case SdpMediaSection::kDccpTlsRtpSavp:
+ return os << "DCCP/TLS/RTP/SAVP";
+ case SdpMediaSection::kUdpTlsRtpSavpf:
+ return os << "UDP/TLS/RTP/SAVPF";
+ case SdpMediaSection::kTcpDtlsRtpSavpf:
+ return os << "TCP/DTLS/RTP/SAVPF";
+ case SdpMediaSection::kDccpTlsRtpSavpf:
+ return os << "DCCP/TLS/RTP/SAVPF";
+ case SdpMediaSection::kUdpMbmsFecRtpAvp:
+ return os << "UDP/MBMS-FEC/RTP/AVP";
+ case SdpMediaSection::kUdpMbmsFecRtpSavp:
+ return os << "UDP/MBMS-FEC/RTP/SAVP";
+ case SdpMediaSection::kUdpMbmsRepair:
+ return os << "UDP/MBMS-REPAIR";
+ case SdpMediaSection::kFecUdp:
+ return os << "FEC/UDP";
+ case SdpMediaSection::kUdpFec:
+ return os << "UDP/FEC";
+ case SdpMediaSection::kTcpMrcpv2:
+ return os << "TCP/MRCPv2";
+ case SdpMediaSection::kTcpTlsMrcpv2:
+ return os << "TCP/TLS/MRCPv2";
+ case SdpMediaSection::kPstn:
+ return os << "PSTN";
+ case SdpMediaSection::kUdpTlsUdptl:
+ return os << "UDP/TLS/UDPTL";
+ case SdpMediaSection::kSctp:
+ return os << "SCTP";
+ case SdpMediaSection::kDtlsSctp:
+ return os << "DTLS/SCTP";
+ case SdpMediaSection::kUdpDtlsSctp:
+ return os << "UDP/DTLS/SCTP";
+ case SdpMediaSection::kTcpDtlsSctp:
+ return os << "TCP/DTLS/SCTP";
+ }
+ MOZ_ASSERT(false, "Unknown Protocol");
+ return os << "?";
+}
+
+class SdpConnection {
+ public:
+ SdpConnection(sdp::AddrType addrType, std::string addr, uint8_t ttl = 0,
+ uint32_t count = 0)
+ : mAddrType(addrType), mAddr(addr), mTtl(ttl), mCount(count) {}
+ ~SdpConnection() {}
+
+ sdp::AddrType GetAddrType() const { return mAddrType; }
+ const std::string& GetAddress() const { return mAddr; }
+ void SetAddress(const std::string& address) {
+ mAddr = address;
+ if (mAddr.find(':') != std::string::npos) {
+ mAddrType = sdp::kIPv6;
+ } else {
+ mAddrType = sdp::kIPv4;
+ }
+ }
+ uint8_t GetTtl() const { return mTtl; }
+ uint32_t GetCount() const { return mCount; }
+
+ void Serialize(std::ostream& os) const {
+ sdp::NetType netType = sdp::kInternet;
+
+ os << "c=" << netType << " " << mAddrType << " " << mAddr;
+
+ if (mTtl) {
+ os << "/" << static_cast<uint32_t>(mTtl);
+ if (mCount) {
+ os << "/" << mCount;
+ }
+ }
+ os << "\r\n";
+ }
+
+ private:
+ sdp::AddrType mAddrType;
+ std::string mAddr;
+ uint8_t mTtl; // 0-255; 0 when unset
+ uint32_t mCount; // 0 when unset
+};
+
+inline std::ostream& operator<<(std::ostream& os, const SdpConnection& c) {
+ c.Serialize(os);
+ return os;
+}
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/sdp/SdpParser.h b/dom/media/webrtc/sdp/SdpParser.h
new file mode 100644
index 0000000000..4b23e93705
--- /dev/null
+++ b/dom/media/webrtc/sdp/SdpParser.h
@@ -0,0 +1,81 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _SDPPARSER_H_
+#define _SDPPARSER_H_
+
+#include <vector>
+#include <string>
+#include "sdp/Sdp.h"
+#include "sdp/SdpLog.h"
+
+#include "mozilla/Telemetry.h"
+
+namespace mozilla {
+
+class SdpParser {
+ public:
+ SdpParser() = default;
+ virtual ~SdpParser() = default;
+
+ class Results {
+ public:
+ typedef std::pair<size_t, std::string> Anomaly;
+ typedef std::vector<Anomaly> AnomalyVec;
+ virtual ~Results() = default;
+ UniquePtr<mozilla::Sdp>& Sdp() { return mSdp; }
+ AnomalyVec& Errors() { return mErrors; }
+ AnomalyVec& Warnings() { return mWarnings; }
+ virtual const std::string& ParserName() const = 0;
+ bool Ok() const { return mErrors.empty(); }
+
+ protected:
+ UniquePtr<mozilla::Sdp> mSdp;
+ AnomalyVec mErrors;
+ AnomalyVec mWarnings;
+ };
+
+ // The name of the parser implementation
+ virtual const std::string& Name() const = 0;
+
+ /**
+ * This parses the provided text into an SDP object.
+ * This returns a nullptr-valued pointer if things go poorly.
+ */
+ virtual UniquePtr<SdpParser::Results> Parse(const std::string& aText) = 0;
+
+ class InternalResults : public Results {
+ public:
+ explicit InternalResults(const std::string& aParserName)
+ : mParserName(aParserName) {}
+ virtual ~InternalResults() = default;
+
+ void SetSdp(UniquePtr<mozilla::Sdp>&& aSdp) { mSdp = std::move(aSdp); }
+
+ void AddParseError(size_t line, const std::string& message) {
+ MOZ_LOG(SdpLog, LogLevel::Error,
+ ("%s: parser error %s, at line %zu", mParserName.c_str(),
+ message.c_str(), line));
+ mErrors.push_back(std::make_pair(line, message));
+ }
+
+ void AddParseWarning(size_t line, const std::string& message) {
+ MOZ_LOG(SdpLog, LogLevel::Warning,
+ ("%s: parser warning %s, at line %zu", mParserName.c_str(),
+ message.c_str(), line));
+ mWarnings.push_back(std::make_pair(line, message));
+ }
+
+ const std::string& ParserName() const override { return mParserName; }
+
+ private:
+ const std::string mParserName;
+ };
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/sdp/SdpPref.cpp b/dom/media/webrtc/sdp/SdpPref.cpp
new file mode 100644
index 0000000000..d0fc03c4d0
--- /dev/null
+++ b/dom/media/webrtc/sdp/SdpPref.cpp
@@ -0,0 +1,107 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "sdp/SdpPref.h"
+#include "sdp/RsdparsaSdpParser.h"
+#include "sdp/SipccSdpParser.h"
+
+namespace mozilla {
+
+const std::string SdpPref::PRIMARY_PREF = "media.peerconnection.sdp.parser";
+const std::string SdpPref::ALTERNATE_PREF =
+ "media.peerconnection.sdp.alternate_parse_mode";
+const std::string SdpPref::STRICT_SUCCESS_PREF =
+ "media.peerconnection.sdp.strict_success";
+const std::string SdpPref::DEFAULT = "default";
+
+auto SdpPref::ToString(const Parsers& aParser) -> std::string {
+ switch (aParser) {
+ case Parsers::Sipcc:
+ return "sipcc";
+ case Parsers::WebRtcSdp:
+ return "webrtc-sdp";
+ };
+ MOZ_CRASH("ALL Parsers CASES ARE NOT COVERED");
+ return "";
+}
+
+auto SdpPref::ToString(const AlternateParseModes& aMode) -> std::string {
+ switch (aMode) {
+ case AlternateParseModes::Parallel:
+ return "parallel";
+ case AlternateParseModes::Failover:
+ return "failover";
+ case AlternateParseModes::Never:
+ return "never";
+ };
+ MOZ_CRASH("ALL AlternateParseModes CASES ARE NOT COVERED");
+ return "";
+}
+
+auto SdpPref::Parser() -> Parsers {
+ static const auto values = std::unordered_map<std::string, Parsers>{
+ {"sipcc", Parsers::Sipcc},
+ {"webrtc-sdp", Parsers::WebRtcSdp},
+ {DEFAULT, Parsers::Sipcc},
+ };
+ return Pref(PRIMARY_PREF, values);
+}
+
+auto SdpPref::AlternateParseMode() -> AlternateParseModes {
+ static const auto values =
+ std::unordered_map<std::string, AlternateParseModes>{
+ {"parallel", AlternateParseModes::Parallel},
+ {"failover", AlternateParseModes::Failover},
+ {"never", AlternateParseModes::Never},
+ {DEFAULT, AlternateParseModes::Parallel},
+ };
+ return Pref(ALTERNATE_PREF, values);
+}
+
+auto SdpPref::Primary() -> UniquePtr<SdpParser> {
+ switch (Parser()) {
+ case Parsers::Sipcc:
+ return UniquePtr<SdpParser>(new SipccSdpParser());
+ case Parsers::WebRtcSdp:
+ return UniquePtr<SdpParser>(new RsdparsaSdpParser());
+ }
+ MOZ_CRASH("ALL Parsers CASES ARE NOT COVERED");
+ return nullptr;
+}
+
+auto SdpPref::Secondary() -> Maybe<UniquePtr<SdpParser>> {
+ if (AlternateParseMode() != AlternateParseModes::Parallel) {
+ return Nothing();
+ }
+ switch (Parser()) { // Choose whatever the primary parser isn't
+ case Parsers::Sipcc:
+ return Some(UniquePtr<SdpParser>(new RsdparsaSdpParser()));
+ case Parsers::WebRtcSdp:
+ return Some(UniquePtr<SdpParser>(new SipccSdpParser()));
+ }
+ MOZ_CRASH("ALL Parsers CASES ARE NOT COVERED");
+ return Nothing();
+}
+
+auto SdpPref::Failover() -> Maybe<UniquePtr<SdpParser>> {
+ if (AlternateParseMode() != AlternateParseModes::Failover) {
+ return Nothing();
+ }
+ switch (Parser()) {
+ case Parsers::Sipcc:
+ return Some(UniquePtr<SdpParser>(new RsdparsaSdpParser()));
+ case Parsers::WebRtcSdp:
+ return Some(UniquePtr<SdpParser>(new SipccSdpParser()));
+ }
+ MOZ_CRASH("ALL Parsers CASES ARE NOT COVERED");
+ return Nothing();
+}
+
+auto SdpPref::StrictSuccess() -> bool {
+ return Preferences::GetBool(STRICT_SUCCESS_PREF.c_str(), false);
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/sdp/SdpPref.h b/dom/media/webrtc/sdp/SdpPref.h
new file mode 100644
index 0000000000..5f24fb12a7
--- /dev/null
+++ b/dom/media/webrtc/sdp/SdpPref.h
@@ -0,0 +1,82 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _SDPPREF_H_
+#define _SDPPREF_H_
+
+#include "mozilla/Maybe.h"
+#include "mozilla/Preferences.h"
+
+#include <string>
+#include <unordered_map>
+
+namespace mozilla {
+
+class SdpParser;
+
+// Interprets about:config SDP parsing preferences
+class SdpPref {
+ private:
+ static const std::string PRIMARY_PREF;
+ static const std::string ALTERNATE_PREF;
+ static const std::string STRICT_SUCCESS_PREF;
+ static const std::string DEFAULT;
+
+ public:
+ // Supported Parsers
+ enum class Parsers {
+ Sipcc,
+ WebRtcSdp,
+ };
+ static auto ToString(const Parsers& aParser) -> std::string;
+
+ // How is the alternate used
+ enum class AlternateParseModes {
+ Parallel, // Alternate is always run, if A succedes it is used, otherwise B
+ // is used
+ Failover, // Alternate is only run on failure of the primary to parse
+ Never, // Alternate is never run; this is effectively a kill switch
+ };
+ static auto ToString(const AlternateParseModes& aMode) -> std::string;
+
+ private:
+ // Finds the mapping between a pref string and pref value, if none exists the
+ // default is used
+ template <class T>
+ static auto Pref(const std::string& aPrefName,
+ const std::unordered_map<std::string, T>& aMap) -> T {
+ MOZ_ASSERT(aMap.find(DEFAULT) != aMap.end());
+
+ nsCString value;
+ if (NS_FAILED(Preferences::GetCString(aPrefName.c_str(), value))) {
+ return aMap.at(DEFAULT);
+ }
+ const auto found = aMap.find(value.get());
+ if (found != aMap.end()) {
+ return found->second;
+ }
+ return aMap.at(DEFAULT);
+ }
+ // The value of the parser pref
+ static auto Parser() -> Parsers;
+
+ // The value of the alternate parse mode pref
+ static auto AlternateParseMode() -> AlternateParseModes;
+
+ public:
+ // Do non-fatal parsing errors count as failure
+ static auto StrictSuccess() -> bool;
+ // Functions to create the primary, secondary and failover parsers.
+
+ // Reads about:config to choose the primary Parser
+ static auto Primary() -> UniquePtr<SdpParser>;
+ static auto Secondary() -> Maybe<UniquePtr<SdpParser>>;
+ static auto Failover() -> Maybe<UniquePtr<SdpParser>>;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/sdp/SdpTelemetry.cpp b/dom/media/webrtc/sdp/SdpTelemetry.cpp
new file mode 100644
index 0000000000..7b3a6cec18
--- /dev/null
+++ b/dom/media/webrtc/sdp/SdpTelemetry.cpp
@@ -0,0 +1,63 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "sdp/SdpTelemetry.h"
+
+#include "mozilla/Telemetry.h"
+
+namespace mozilla {
+
+auto SdpTelemetry::RecordParse(const SdpTelemetry::Results& aResult,
+ const SdpTelemetry::Modes& aMode,
+ const SdpTelemetry::Roles& aRole) -> void {
+ Telemetry::ScalarAdd(Telemetry::ScalarID::WEBRTC_SDP_PARSER_DIFF,
+ BucketNameFragment(aResult, aMode, aRole), 1);
+}
+
+auto SdpTelemetry::RecordCompare(const SdpTelemetry::Results& aFirst,
+ const SdpTelemetry::Results& aSecond,
+ const SdpTelemetry::Modes& aMode) -> void {
+ const nsAutoString bucket =
+ BucketNameFragment(aFirst, aMode, Roles::Primary) +
+ NS_ConvertASCIItoUTF16("__") +
+ BucketNameFragment(aSecond, aMode, Roles::Secondary);
+ Telemetry::ScalarAdd(Telemetry::ScalarID::WEBRTC_SDP_PARSER_DIFF, bucket, 1);
+}
+
+auto SdpTelemetry::BucketNameFragment(const SdpTelemetry::Results& aResult,
+ const SdpTelemetry::Modes& aMode,
+ const SdpTelemetry::Roles& aRole)
+ -> nsAutoString {
+ auto mode = [&]() -> std::string {
+ switch (aMode) {
+ case Modes::Parallel:
+ return "parallel";
+ case Modes::Failover:
+ return "failover";
+ case Modes::Never:
+ return "standalone";
+ }
+ MOZ_CRASH("Unknown SDP Parse Mode!");
+ };
+ auto role = [&]() -> std::string {
+ switch (aRole) {
+ case Roles::Primary:
+ return "primary";
+ case Roles::Secondary:
+ return "secondary";
+ }
+ MOZ_CRASH("Unknown SDP Parse Role!");
+ };
+ auto success = [&]() -> std::string {
+ return aResult->Ok() ? "success" : "failure";
+ };
+ nsAutoString name;
+ name.AssignASCII(nsCString(aResult->ParserName() + "_" + mode() + "_" +
+ role() + "_" + success()));
+ return name;
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/sdp/SdpTelemetry.h b/dom/media/webrtc/sdp/SdpTelemetry.h
new file mode 100644
index 0000000000..ce308a04ec
--- /dev/null
+++ b/dom/media/webrtc/sdp/SdpTelemetry.h
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _SDPTELEMETRY_H_
+#define _SDPTELEMETRY_H_
+
+#include "sdp/SdpParser.h"
+#include "sdp/SdpPref.h"
+
+namespace mozilla {
+
+class SdpTelemetry {
+ public:
+ SdpTelemetry() = delete;
+
+ using Results = UniquePtr<SdpParser::Results>;
+ using Modes = SdpPref::AlternateParseModes;
+
+ enum class Roles {
+ Primary,
+ Secondary,
+ };
+
+ static auto RecordParse(const Results& aResults, const Modes& aMode,
+ const Roles& aRole) -> void;
+
+ static auto RecordSecondaryParse(const Results& aResult, const Modes& aMode)
+ -> void;
+
+ static auto RecordCompare(const Results& aFirst, const Results& aSecond,
+ const Modes& aMode) -> void;
+
+ private:
+ static auto BucketNameFragment(const Results& aResult, const Modes& aModes,
+ const Roles& aRoles) -> nsAutoString;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/sdp/SipccSdp.cpp b/dom/media/webrtc/sdp/SipccSdp.cpp
new file mode 100644
index 0000000000..7e36f9e930
--- /dev/null
+++ b/dom/media/webrtc/sdp/SipccSdp.cpp
@@ -0,0 +1,173 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "sdp/SipccSdp.h"
+
+#include <cstdlib>
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Assertions.h"
+#include "sdp/SdpParser.h"
+
+#ifdef CRLF
+# undef CRLF
+#endif
+#define CRLF "\r\n"
+
+namespace mozilla {
+
+SipccSdp::SipccSdp(const SipccSdp& aOrig)
+ : mOrigin(aOrig.mOrigin),
+ mBandwidths(aOrig.mBandwidths),
+ mAttributeList(aOrig.mAttributeList, nullptr) {
+ for (const auto& msection : aOrig.mMediaSections) {
+ mMediaSections.emplace_back(
+ new SipccSdpMediaSection(*msection, &mAttributeList));
+ }
+}
+
+Sdp* SipccSdp::Clone() const { return new SipccSdp(*this); }
+
+const SdpOrigin& SipccSdp::GetOrigin() const { return mOrigin; }
+
+uint32_t SipccSdp::GetBandwidth(const std::string& type) const {
+ auto found = mBandwidths.find(type);
+ if (found == mBandwidths.end()) {
+ return 0;
+ }
+ return found->second;
+}
+
+const SdpMediaSection& SipccSdp::GetMediaSection(size_t level) const {
+ if (level > mMediaSections.size()) {
+ MOZ_CRASH();
+ }
+ return *mMediaSections[level];
+}
+
+SdpMediaSection& SipccSdp::GetMediaSection(size_t level) {
+ if (level > mMediaSections.size()) {
+ MOZ_CRASH();
+ }
+ return *mMediaSections[level];
+}
+
+SdpMediaSection& SipccSdp::AddMediaSection(SdpMediaSection::MediaType mediaType,
+ SdpDirectionAttribute::Direction dir,
+ uint16_t port,
+ SdpMediaSection::Protocol protocol,
+ sdp::AddrType addrType,
+ const std::string& addr) {
+ size_t level = mMediaSections.size();
+ SipccSdpMediaSection* media =
+ new SipccSdpMediaSection(level, &mAttributeList);
+ media->mMediaType = mediaType;
+ media->mPort = port;
+ media->mPortCount = 0;
+ media->mProtocol = protocol;
+ media->mConnection = MakeUnique<SdpConnection>(addrType, addr);
+ media->GetAttributeList().SetAttribute(new SdpDirectionAttribute(dir));
+ mMediaSections.emplace_back(media);
+ return *media;
+}
+
+bool SipccSdp::LoadOrigin(sdp_t* sdp, InternalResults& results) {
+ std::string username = sdp_get_owner_username(sdp);
+ uint64_t sessId = strtoull(sdp_get_owner_sessionid(sdp), nullptr, 10);
+ uint64_t sessVer = strtoull(sdp_get_owner_version(sdp), nullptr, 10);
+
+ sdp_nettype_e type = sdp_get_owner_network_type(sdp);
+ if (type != SDP_NT_INTERNET) {
+ results.AddParseError(2, "Unsupported network type");
+ return false;
+ }
+
+ sdp::AddrType addrType;
+ switch (sdp_get_owner_address_type(sdp)) {
+ case SDP_AT_IP4:
+ addrType = sdp::kIPv4;
+ break;
+ case SDP_AT_IP6:
+ addrType = sdp::kIPv6;
+ break;
+ default:
+ results.AddParseError(2, "Unsupported address type");
+ return false;
+ }
+
+ std::string address = sdp_get_owner_address(sdp);
+ mOrigin = SdpOrigin(username, sessId, sessVer, addrType, address);
+ return true;
+}
+
+bool SipccSdp::Load(sdp_t* sdp, InternalResults& results) {
+ // Believe it or not, SDP_SESSION_LEVEL is 0xFFFF
+ if (!mAttributeList.Load(sdp, SDP_SESSION_LEVEL, results)) {
+ return false;
+ }
+
+ if (!LoadOrigin(sdp, results)) {
+ return false;
+ }
+
+ if (!mBandwidths.Load(sdp, SDP_SESSION_LEVEL, results)) {
+ return false;
+ }
+
+ for (int i = 0; i < sdp_get_num_media_lines(sdp); ++i) {
+ // note that we pass a "level" here that is one higher
+ // sipcc counts media sections from 1, using 0xFFFF as the "session"
+ UniquePtr<SipccSdpMediaSection> section(
+ new SipccSdpMediaSection(i, &mAttributeList));
+ if (!section->Load(sdp, i + 1, results)) {
+ return false;
+ }
+ mMediaSections.push_back(std::move(section));
+ }
+ return true;
+}
+
+void SipccSdp::Serialize(std::ostream& os) const {
+ os << "v=0" << CRLF << mOrigin << "s=-" << CRLF;
+
+ // We don't support creating i=, u=, e=, p=
+ // We don't generate c= at the session level (only in media)
+
+ mBandwidths.Serialize(os);
+ os << "t=0 0" << CRLF;
+
+ // We don't support r= or z=
+
+ // attributes
+ os << mAttributeList;
+
+ // media sections
+ for (const auto& msection : mMediaSections) {
+ os << *msection;
+ }
+}
+
+bool SipccSdpBandwidths::Load(sdp_t* sdp, uint16_t level,
+ InternalResults& results) {
+ size_t count = sdp_get_num_bw_lines(sdp, level);
+ for (size_t i = 1; i <= count; ++i) {
+ sdp_bw_modifier_e bwtype = sdp_get_bw_modifier(sdp, level, i);
+ uint32_t bandwidth = sdp_get_bw_value(sdp, level, i);
+ if (bwtype != SDP_BW_MODIFIER_UNSUPPORTED) {
+ const char* typeName = sdp_get_bw_modifier_name(bwtype);
+ (*this)[typeName] = bandwidth;
+ }
+ }
+
+ return true;
+}
+
+void SipccSdpBandwidths::Serialize(std::ostream& os) const {
+ for (auto i = begin(); i != end(); ++i) {
+ os << "b=" << i->first << ":" << i->second << CRLF;
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/sdp/SipccSdp.h b/dom/media/webrtc/sdp/SipccSdp.h
new file mode 100644
index 0000000000..4915821cee
--- /dev/null
+++ b/dom/media/webrtc/sdp/SipccSdp.h
@@ -0,0 +1,82 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _SIPCCSDP_H_
+#define _SIPCCSDP_H_
+
+#include <map>
+#include <vector>
+#include "mozilla/Attributes.h"
+
+#include "sdp/Sdp.h"
+#include "sdp/SdpParser.h"
+#include "sdp/SipccSdpMediaSection.h"
+#include "sdp/SipccSdpAttributeList.h"
+extern "C" {
+#include "sipcc_sdp.h"
+}
+
+namespace mozilla {
+
+class SipccSdpParser;
+
+class SipccSdp final : public Sdp {
+ friend class SipccSdpParser;
+
+ public:
+ explicit SipccSdp(const SdpOrigin& origin)
+ : mOrigin(origin), mAttributeList(nullptr) {}
+ SipccSdp(const SipccSdp& aOrig);
+
+ virtual Sdp* Clone() const override;
+
+ virtual const SdpOrigin& GetOrigin() const override;
+
+ // Note: connection information is always retrieved from media sections
+ virtual uint32_t GetBandwidth(const std::string& type) const override;
+
+ virtual size_t GetMediaSectionCount() const override {
+ return mMediaSections.size();
+ }
+
+ virtual const SdpAttributeList& GetAttributeList() const override {
+ return mAttributeList;
+ }
+
+ virtual SdpAttributeList& GetAttributeList() override {
+ return mAttributeList;
+ }
+
+ virtual const SdpMediaSection& GetMediaSection(size_t level) const override;
+
+ virtual SdpMediaSection& GetMediaSection(size_t level) override;
+
+ virtual SdpMediaSection& AddMediaSection(SdpMediaSection::MediaType media,
+ SdpDirectionAttribute::Direction dir,
+ uint16_t port,
+ SdpMediaSection::Protocol proto,
+ sdp::AddrType addrType,
+ const std::string& addr) override;
+
+ virtual void Serialize(std::ostream&) const override;
+
+ private:
+ using InternalResults = SdpParser::InternalResults;
+
+ SipccSdp() : mOrigin("", 0, 0, sdp::kIPv4, ""), mAttributeList(nullptr) {}
+
+ bool Load(sdp_t* sdp, InternalResults& results);
+ bool LoadOrigin(sdp_t* sdp, InternalResults& results);
+
+ SdpOrigin mOrigin;
+ SipccSdpBandwidths mBandwidths;
+ SipccSdpAttributeList mAttributeList;
+ std::vector<UniquePtr<SipccSdpMediaSection>> mMediaSections;
+};
+
+} // namespace mozilla
+
+#endif // _sdp_h_
diff --git a/dom/media/webrtc/sdp/SipccSdpAttributeList.cpp b/dom/media/webrtc/sdp/SipccSdpAttributeList.cpp
new file mode 100644
index 0000000000..15572b6dd3
--- /dev/null
+++ b/dom/media/webrtc/sdp/SipccSdpAttributeList.cpp
@@ -0,0 +1,1386 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "sdp/SipccSdpAttributeList.h"
+
+#include <ostream>
+#include "mozilla/Assertions.h"
+
+extern "C" {
+#include "sdp_private.h"
+}
+
+namespace mozilla {
+
+using InternalResults = SdpParser::InternalResults;
+
+/* static */
+const std::string SipccSdpAttributeList::kEmptyString = "";
+
+SipccSdpAttributeList::SipccSdpAttributeList(
+ const SipccSdpAttributeList* sessionLevel)
+ : mSessionLevel(sessionLevel) {
+ memset(&mAttributes, 0, sizeof(mAttributes));
+}
+
+SipccSdpAttributeList::SipccSdpAttributeList(
+ const SipccSdpAttributeList& aOrig,
+ const SipccSdpAttributeList* sessionLevel)
+ : SipccSdpAttributeList(sessionLevel) {
+ for (size_t i = 0; i < kNumAttributeTypes; ++i) {
+ if (aOrig.mAttributes[i]) {
+ mAttributes[i] = aOrig.mAttributes[i]->Clone();
+ }
+ }
+}
+
+SipccSdpAttributeList::~SipccSdpAttributeList() {
+ for (size_t i = 0; i < kNumAttributeTypes; ++i) {
+ delete mAttributes[i];
+ }
+}
+
+bool SipccSdpAttributeList::HasAttribute(AttributeType type,
+ bool sessionFallback) const {
+ return !!GetAttribute(type, sessionFallback);
+}
+
+const SdpAttribute* SipccSdpAttributeList::GetAttribute(
+ AttributeType type, bool sessionFallback) const {
+ const SdpAttribute* value = mAttributes[static_cast<size_t>(type)];
+ // Only do fallback when the attribute can appear at both the media and
+ // session level
+ if (!value && !AtSessionLevel() && sessionFallback &&
+ SdpAttribute::IsAllowedAtSessionLevel(type) &&
+ SdpAttribute::IsAllowedAtMediaLevel(type)) {
+ return mSessionLevel->GetAttribute(type, false);
+ }
+ return value;
+}
+
+void SipccSdpAttributeList::RemoveAttribute(AttributeType type) {
+ delete mAttributes[static_cast<size_t>(type)];
+ mAttributes[static_cast<size_t>(type)] = nullptr;
+}
+
+void SipccSdpAttributeList::Clear() {
+ for (size_t i = 0; i < kNumAttributeTypes; ++i) {
+ RemoveAttribute(static_cast<AttributeType>(i));
+ }
+}
+
+uint32_t SipccSdpAttributeList::Count() const {
+ uint32_t count = 0;
+ for (size_t i = 0; i < kNumAttributeTypes; ++i) {
+ if (mAttributes[i]) {
+ count++;
+ }
+ }
+ return count;
+}
+
+void SipccSdpAttributeList::SetAttribute(SdpAttribute* attr) {
+ if (!IsAllowedHere(attr->GetType())) {
+ MOZ_ASSERT(false, "This type of attribute is not allowed here");
+ return;
+ }
+ RemoveAttribute(attr->GetType());
+ mAttributes[attr->GetType()] = attr;
+}
+
+void SipccSdpAttributeList::LoadSimpleString(sdp_t* sdp, uint16_t level,
+ sdp_attr_e attr,
+ AttributeType targetType,
+ InternalResults& results) {
+ const char* value = sdp_attr_get_simple_string(sdp, attr, level, 0, 1);
+ if (value) {
+ if (!IsAllowedHere(targetType)) {
+ uint32_t lineNumber = sdp_attr_line_number(sdp, attr, level, 0, 1);
+ WarnAboutMisplacedAttribute(targetType, lineNumber, results);
+ } else {
+ SetAttribute(new SdpStringAttribute(targetType, std::string(value)));
+ }
+ }
+}
+
+void SipccSdpAttributeList::LoadSimpleStrings(sdp_t* sdp, uint16_t level,
+ InternalResults& results) {
+ LoadSimpleString(sdp, level, SDP_ATTR_MID, SdpAttribute::kMidAttribute,
+ results);
+ LoadSimpleString(sdp, level, SDP_ATTR_LABEL, SdpAttribute::kLabelAttribute,
+ results);
+}
+
+void SipccSdpAttributeList::LoadSimpleNumber(sdp_t* sdp, uint16_t level,
+ sdp_attr_e attr,
+ AttributeType targetType,
+ InternalResults& results) {
+ if (sdp_attr_valid(sdp, attr, level, 0, 1)) {
+ if (!IsAllowedHere(targetType)) {
+ uint32_t lineNumber = sdp_attr_line_number(sdp, attr, level, 0, 1);
+ WarnAboutMisplacedAttribute(targetType, lineNumber, results);
+ } else {
+ uint32_t value = sdp_attr_get_simple_u32(sdp, attr, level, 0, 1);
+ SetAttribute(new SdpNumberAttribute(targetType, value));
+ }
+ }
+}
+
+void SipccSdpAttributeList::LoadSimpleNumbers(sdp_t* sdp, uint16_t level,
+ InternalResults& results) {
+ LoadSimpleNumber(sdp, level, SDP_ATTR_PTIME, SdpAttribute::kPtimeAttribute,
+ results);
+ LoadSimpleNumber(sdp, level, SDP_ATTR_MAXPTIME,
+ SdpAttribute::kMaxptimeAttribute, results);
+ LoadSimpleNumber(sdp, level, SDP_ATTR_SCTPPORT,
+ SdpAttribute::kSctpPortAttribute, results);
+ LoadSimpleNumber(sdp, level, SDP_ATTR_MAXMESSAGESIZE,
+ SdpAttribute::kMaxMessageSizeAttribute, results);
+}
+
+void SipccSdpAttributeList::LoadFlags(sdp_t* sdp, uint16_t level) {
+ if (AtSessionLevel()) {
+ if (sdp_attr_valid(sdp, SDP_ATTR_ICE_LITE, level, 0, 1)) {
+ SetAttribute(new SdpFlagAttribute(SdpAttribute::kIceLiteAttribute));
+ }
+ } else { // media-level
+ if (sdp_attr_valid(sdp, SDP_ATTR_RTCP_MUX, level, 0, 1)) {
+ SetAttribute(new SdpFlagAttribute(SdpAttribute::kRtcpMuxAttribute));
+ }
+ if (sdp_attr_valid(sdp, SDP_ATTR_END_OF_CANDIDATES, level, 0, 1)) {
+ SetAttribute(
+ new SdpFlagAttribute(SdpAttribute::kEndOfCandidatesAttribute));
+ }
+ if (sdp_attr_valid(sdp, SDP_ATTR_BUNDLE_ONLY, level, 0, 1)) {
+ SetAttribute(new SdpFlagAttribute(SdpAttribute::kBundleOnlyAttribute));
+ }
+ if (sdp_attr_valid(sdp, SDP_ATTR_RTCP_RSIZE, level, 0, 1))
+ SetAttribute(new SdpFlagAttribute(SdpAttribute::kRtcpRsizeAttribute));
+ }
+}
+
+static void ConvertDirection(sdp_direction_e sipcc_direction,
+ SdpDirectionAttribute::Direction* dir_outparam) {
+ switch (sipcc_direction) {
+ case SDP_DIRECTION_SENDRECV:
+ *dir_outparam = SdpDirectionAttribute::kSendrecv;
+ return;
+ case SDP_DIRECTION_SENDONLY:
+ *dir_outparam = SdpDirectionAttribute::kSendonly;
+ return;
+ case SDP_DIRECTION_RECVONLY:
+ *dir_outparam = SdpDirectionAttribute::kRecvonly;
+ return;
+ case SDP_DIRECTION_INACTIVE:
+ *dir_outparam = SdpDirectionAttribute::kInactive;
+ return;
+ case SDP_MAX_QOS_DIRECTIONS:
+ // Nothing actually sets this value.
+ // Fall through to MOZ_CRASH below.
+ {}
+ }
+
+ MOZ_CRASH("Invalid direction from sipcc; this is probably corruption");
+}
+
+void SipccSdpAttributeList::LoadDirection(sdp_t* sdp, uint16_t level,
+ InternalResults& results) {
+ SdpDirectionAttribute::Direction dir;
+ ConvertDirection(sdp_get_media_direction(sdp, level, 0), &dir);
+ SetAttribute(new SdpDirectionAttribute(dir));
+}
+
+void SipccSdpAttributeList::LoadIceAttributes(sdp_t* sdp, uint16_t level) {
+ char* value;
+ sdp_result_e sdpres =
+ sdp_attr_get_ice_attribute(sdp, level, 0, SDP_ATTR_ICE_UFRAG, 1, &value);
+ if (sdpres == SDP_SUCCESS) {
+ SetAttribute(new SdpStringAttribute(SdpAttribute::kIceUfragAttribute,
+ std::string(value)));
+ }
+ sdpres =
+ sdp_attr_get_ice_attribute(sdp, level, 0, SDP_ATTR_ICE_PWD, 1, &value);
+ if (sdpres == SDP_SUCCESS) {
+ SetAttribute(new SdpStringAttribute(SdpAttribute::kIcePwdAttribute,
+ std::string(value)));
+ }
+
+ const char* iceOptVal =
+ sdp_attr_get_simple_string(sdp, SDP_ATTR_ICE_OPTIONS, level, 0, 1);
+ if (iceOptVal) {
+ auto* iceOptions =
+ new SdpOptionsAttribute(SdpAttribute::kIceOptionsAttribute);
+ iceOptions->Load(iceOptVal);
+ SetAttribute(iceOptions);
+ }
+}
+
+bool SipccSdpAttributeList::LoadFingerprint(sdp_t* sdp, uint16_t level,
+ InternalResults& results) {
+ char* value;
+ UniquePtr<SdpFingerprintAttributeList> fingerprintAttrs;
+
+ for (uint16_t i = 1; i < UINT16_MAX; ++i) {
+ sdp_result_e result = sdp_attr_get_dtls_fingerprint_attribute(
+ sdp, level, 0, SDP_ATTR_DTLS_FINGERPRINT, i, &value);
+
+ if (result != SDP_SUCCESS) {
+ break;
+ }
+
+ std::string fingerprintAttr(value);
+ uint32_t lineNumber =
+ sdp_attr_line_number(sdp, SDP_ATTR_DTLS_FINGERPRINT, level, 0, i);
+
+ // sipcc does not expose parse code for this
+ size_t start = fingerprintAttr.find_first_not_of(" \t");
+ if (start == std::string::npos) {
+ results.AddParseError(lineNumber, "Empty fingerprint attribute");
+ return false;
+ }
+
+ size_t end = fingerprintAttr.find_first_of(" \t", start);
+ if (end == std::string::npos) {
+ // One token, no trailing ws
+ results.AddParseError(lineNumber,
+ "Only one token in fingerprint attribute");
+ return false;
+ }
+
+ std::string algorithmToken(fingerprintAttr.substr(start, end - start));
+
+ start = fingerprintAttr.find_first_not_of(" \t", end);
+ if (start == std::string::npos) {
+ // One token, trailing ws
+ results.AddParseError(lineNumber,
+ "Only one token in fingerprint attribute");
+ return false;
+ }
+
+ std::string fingerprintToken(fingerprintAttr.substr(start));
+
+ std::vector<uint8_t> fingerprint =
+ SdpFingerprintAttributeList::ParseFingerprint(fingerprintToken);
+ if (fingerprint.empty()) {
+ results.AddParseError(lineNumber, "Malformed fingerprint token");
+ return false;
+ }
+
+ if (!fingerprintAttrs) {
+ fingerprintAttrs.reset(new SdpFingerprintAttributeList);
+ }
+
+ // Don't assert on unknown algorithm, just skip
+ fingerprintAttrs->PushEntry(algorithmToken, fingerprint, false);
+ }
+
+ if (fingerprintAttrs) {
+ SetAttribute(fingerprintAttrs.release());
+ }
+
+ return true;
+}
+
+void SipccSdpAttributeList::LoadCandidate(sdp_t* sdp, uint16_t level) {
+ char* value;
+ auto candidates =
+ MakeUnique<SdpMultiStringAttribute>(SdpAttribute::kCandidateAttribute);
+
+ for (uint16_t i = 1; i < UINT16_MAX; ++i) {
+ sdp_result_e result = sdp_attr_get_ice_attribute(
+ sdp, level, 0, SDP_ATTR_ICE_CANDIDATE, i, &value);
+
+ if (result != SDP_SUCCESS) {
+ break;
+ }
+
+ candidates->mValues.push_back(value);
+ }
+
+ if (!candidates->mValues.empty()) {
+ SetAttribute(candidates.release());
+ }
+}
+
+bool SipccSdpAttributeList::LoadSctpmap(sdp_t* sdp, uint16_t level,
+ InternalResults& results) {
+ auto sctpmap = MakeUnique<SdpSctpmapAttributeList>();
+ for (uint16_t i = 0; i < UINT16_MAX; ++i) {
+ sdp_attr_t* attr = sdp_find_attr(sdp, level, 0, SDP_ATTR_SCTPMAP, i + 1);
+
+ if (!attr) {
+ break;
+ }
+
+ // Yeah, this is a little weird, but for now we'll just store this as a
+ // payload type.
+ uint16_t payloadType = attr->attr.sctpmap.port;
+ uint16_t streams = attr->attr.sctpmap.streams;
+ const char* name = attr->attr.sctpmap.protocol;
+
+ std::ostringstream osPayloadType;
+ osPayloadType << payloadType;
+ sctpmap->PushEntry(osPayloadType.str(), name, streams);
+ }
+
+ if (!sctpmap->mSctpmaps.empty()) {
+ SetAttribute(sctpmap.release());
+ }
+
+ return true;
+}
+
+SdpRtpmapAttributeList::CodecType SipccSdpAttributeList::GetCodecType(
+ rtp_ptype type) {
+ switch (type) {
+ case RTP_PCMU:
+ return SdpRtpmapAttributeList::kPCMU;
+ case RTP_PCMA:
+ return SdpRtpmapAttributeList::kPCMA;
+ case RTP_G722:
+ return SdpRtpmapAttributeList::kG722;
+ case RTP_H264_P0:
+ case RTP_H264_P1:
+ return SdpRtpmapAttributeList::kH264;
+ case RTP_OPUS:
+ return SdpRtpmapAttributeList::kOpus;
+ case RTP_VP8:
+ return SdpRtpmapAttributeList::kVP8;
+ case RTP_VP9:
+ return SdpRtpmapAttributeList::kVP9;
+ case RTP_RED:
+ return SdpRtpmapAttributeList::kRed;
+ case RTP_ULPFEC:
+ return SdpRtpmapAttributeList::kUlpfec;
+ case RTP_RTX:
+ return SdpRtpmapAttributeList::kRtx;
+ case RTP_TELEPHONE_EVENT:
+ return SdpRtpmapAttributeList::kTelephoneEvent;
+ case RTP_NONE:
+ // Happens when sipcc doesn't know how to translate to the enum
+ case RTP_CELP:
+ case RTP_G726:
+ case RTP_GSM:
+ case RTP_G723:
+ case RTP_DVI4:
+ case RTP_DVI4_II:
+ case RTP_LPC:
+ case RTP_G728:
+ case RTP_G729:
+ case RTP_JPEG:
+ case RTP_NV:
+ case RTP_H261:
+ case RTP_L16:
+ case RTP_H263:
+ case RTP_ILBC:
+ case RTP_I420:
+ return SdpRtpmapAttributeList::kOtherCodec;
+ }
+ MOZ_CRASH("Invalid codec type from sipcc. Probably corruption.");
+}
+
+bool SipccSdpAttributeList::LoadRtpmap(sdp_t* sdp, uint16_t level,
+ InternalResults& results) {
+ auto rtpmap = MakeUnique<SdpRtpmapAttributeList>();
+ uint16_t count;
+ sdp_result_e result =
+ sdp_attr_num_instances(sdp, level, 0, SDP_ATTR_RTPMAP, &count);
+ if (result != SDP_SUCCESS) {
+ MOZ_ASSERT(false, "Unable to get rtpmap size");
+ results.AddParseError(sdp_get_media_line_number(sdp, level),
+ "Unable to get rtpmap size");
+ return false;
+ }
+ for (uint16_t i = 0; i < count; ++i) {
+ uint16_t pt = sdp_attr_get_rtpmap_payload_type(sdp, level, 0, i + 1);
+ const char* ccName = sdp_attr_get_rtpmap_encname(sdp, level, 0, i + 1);
+
+ if (!ccName) {
+ // Probably no rtpmap attribute for a pt in an m-line
+ results.AddParseError(sdp_get_media_line_number(sdp, level),
+ "No rtpmap attribute for payload type");
+ continue;
+ }
+
+ std::string name(ccName);
+
+ SdpRtpmapAttributeList::CodecType codec =
+ GetCodecType(sdp_get_known_payload_type(sdp, level, pt));
+
+ uint32_t clock = sdp_attr_get_rtpmap_clockrate(sdp, level, 0, i + 1);
+ uint16_t channels = 0;
+
+ // sipcc gives us a channels value of "1" for video
+ if (sdp_get_media_type(sdp, level) == SDP_MEDIA_AUDIO) {
+ channels = sdp_attr_get_rtpmap_num_chan(sdp, level, 0, i + 1);
+ }
+
+ std::ostringstream osPayloadType;
+ osPayloadType << pt;
+ rtpmap->PushEntry(osPayloadType.str(), codec, name, clock, channels);
+ }
+
+ if (!rtpmap->mRtpmaps.empty()) {
+ SetAttribute(rtpmap.release());
+ }
+
+ return true;
+}
+
+void SipccSdpAttributeList::LoadSetup(sdp_t* sdp, uint16_t level) {
+ sdp_setup_type_e setupType;
+ auto sdpres = sdp_attr_get_setup_attribute(sdp, level, 0, 1, &setupType);
+
+ if (sdpres != SDP_SUCCESS) {
+ return;
+ }
+
+ switch (setupType) {
+ case SDP_SETUP_ACTIVE:
+ SetAttribute(new SdpSetupAttribute(SdpSetupAttribute::kActive));
+ return;
+ case SDP_SETUP_PASSIVE:
+ SetAttribute(new SdpSetupAttribute(SdpSetupAttribute::kPassive));
+ return;
+ case SDP_SETUP_ACTPASS:
+ SetAttribute(new SdpSetupAttribute(SdpSetupAttribute::kActpass));
+ return;
+ case SDP_SETUP_HOLDCONN:
+ SetAttribute(new SdpSetupAttribute(SdpSetupAttribute::kHoldconn));
+ return;
+ case SDP_SETUP_UNKNOWN:
+ return;
+ case SDP_SETUP_NOT_FOUND:
+ case SDP_MAX_SETUP:
+ // There is no code that will set these.
+ // Fall through to MOZ_CRASH() below.
+ {}
+ }
+
+ MOZ_CRASH("Invalid setup type from sipcc. This is probably corruption.");
+}
+
+void SipccSdpAttributeList::LoadSsrc(sdp_t* sdp, uint16_t level) {
+ auto ssrcs = MakeUnique<SdpSsrcAttributeList>();
+
+ for (uint16_t i = 1; i < UINT16_MAX; ++i) {
+ sdp_attr_t* attr = sdp_find_attr(sdp, level, 0, SDP_ATTR_SSRC, i);
+
+ if (!attr) {
+ break;
+ }
+
+ sdp_ssrc_t* ssrc = &(attr->attr.ssrc);
+ ssrcs->PushEntry(ssrc->ssrc, ssrc->attribute);
+ }
+
+ if (!ssrcs->mSsrcs.empty()) {
+ SetAttribute(ssrcs.release());
+ }
+}
+
+void SipccSdpAttributeList::LoadSsrcGroup(sdp_t* sdp, uint16_t level) {
+ auto ssrcGroups = MakeUnique<SdpSsrcGroupAttributeList>();
+
+ for (uint16_t i = 1; i < UINT16_MAX; ++i) {
+ sdp_attr_t* attr = sdp_find_attr(sdp, level, 0, SDP_ATTR_SSRC_GROUP, i);
+
+ if (!attr) {
+ break;
+ }
+
+ sdp_ssrc_group_t* ssrc_group = &(attr->attr.ssrc_group);
+
+ SdpSsrcGroupAttributeList::Semantics semantic;
+ switch (ssrc_group->semantic) {
+ case SDP_SSRC_GROUP_ATTR_FEC:
+ semantic = SdpSsrcGroupAttributeList::kFec;
+ break;
+ case SDP_SSRC_GROUP_ATTR_FID:
+ semantic = SdpSsrcGroupAttributeList::kFid;
+ break;
+ case SDP_SSRC_GROUP_ATTR_FECFR:
+ semantic = SdpSsrcGroupAttributeList::kFecFr;
+ break;
+ case SDP_SSRC_GROUP_ATTR_DUP:
+ semantic = SdpSsrcGroupAttributeList::kDup;
+ break;
+ case SDP_SSRC_GROUP_ATTR_SIM:
+ semantic = SdpSsrcGroupAttributeList::kSim;
+ break;
+ case SDP_MAX_SSRC_GROUP_ATTR_VAL:
+ continue;
+ case SDP_SSRC_GROUP_ATTR_UNSUPPORTED:
+ continue;
+ }
+
+ std::vector<uint32_t> ssrcs;
+ ssrcs.reserve(ssrc_group->num_ssrcs);
+ for (int i = 0; i < ssrc_group->num_ssrcs; ++i) {
+ ssrcs.push_back(ssrc_group->ssrcs[i]);
+ }
+
+ ssrcGroups->PushEntry(semantic, ssrcs);
+ }
+
+ if (!ssrcGroups->mSsrcGroups.empty()) {
+ SetAttribute(ssrcGroups.release());
+ }
+}
+
+bool SipccSdpAttributeList::LoadImageattr(sdp_t* sdp, uint16_t level,
+ InternalResults& results) {
+ UniquePtr<SdpImageattrAttributeList> imageattrs(
+ new SdpImageattrAttributeList);
+
+ for (uint16_t i = 1; i < UINT16_MAX; ++i) {
+ const char* imageattrRaw =
+ sdp_attr_get_simple_string(sdp, SDP_ATTR_IMAGEATTR, level, 0, i);
+ if (!imageattrRaw) {
+ break;
+ }
+
+ std::string error;
+ size_t errorPos;
+ if (!imageattrs->PushEntry(imageattrRaw, &error, &errorPos)) {
+ std::ostringstream fullError;
+ fullError << error << " at column " << errorPos;
+ results.AddParseError(
+ sdp_attr_line_number(sdp, SDP_ATTR_IMAGEATTR, level, 0, i),
+ fullError.str());
+ return false;
+ }
+ }
+
+ if (!imageattrs->mImageattrs.empty()) {
+ SetAttribute(imageattrs.release());
+ }
+ return true;
+}
+
+bool SipccSdpAttributeList::LoadSimulcast(sdp_t* sdp, uint16_t level,
+ InternalResults& results) {
+ const char* simulcastRaw =
+ sdp_attr_get_simple_string(sdp, SDP_ATTR_SIMULCAST, level, 0, 1);
+ if (!simulcastRaw) {
+ return true;
+ }
+
+ UniquePtr<SdpSimulcastAttribute> simulcast(new SdpSimulcastAttribute);
+
+ std::istringstream is(simulcastRaw);
+ std::string error;
+ if (!simulcast->Parse(is, &error)) {
+ std::ostringstream fullError;
+ fullError << error << " at column " << is.tellg();
+ results.AddParseError(
+ sdp_attr_line_number(sdp, SDP_ATTR_SIMULCAST, level, 0, 1),
+ fullError.str());
+ return false;
+ }
+
+ SetAttribute(simulcast.release());
+ return true;
+}
+
+bool SipccSdpAttributeList::LoadGroups(sdp_t* sdp, uint16_t level,
+ InternalResults& results) {
+ uint16_t attrCount = 0;
+ if (sdp_attr_num_instances(sdp, level, 0, SDP_ATTR_GROUP, &attrCount) !=
+ SDP_SUCCESS) {
+ MOZ_ASSERT(false, "Could not get count of group attributes");
+ results.AddParseError(0, "Could not get count of group attributes");
+ return false;
+ }
+
+ UniquePtr<SdpGroupAttributeList> groups = MakeUnique<SdpGroupAttributeList>();
+ for (uint16_t attr = 1; attr <= attrCount; ++attr) {
+ SdpGroupAttributeList::Semantics semantics;
+ std::vector<std::string> tags;
+
+ switch (sdp_get_group_attr(sdp, level, 0, attr)) {
+ case SDP_GROUP_ATTR_FID:
+ semantics = SdpGroupAttributeList::kFid;
+ break;
+ case SDP_GROUP_ATTR_LS:
+ semantics = SdpGroupAttributeList::kLs;
+ break;
+ case SDP_GROUP_ATTR_ANAT:
+ semantics = SdpGroupAttributeList::kAnat;
+ break;
+ case SDP_GROUP_ATTR_BUNDLE:
+ semantics = SdpGroupAttributeList::kBundle;
+ break;
+ default:
+ continue;
+ }
+
+ uint16_t idCount = sdp_get_group_num_id(sdp, level, 0, attr);
+ for (uint16_t id = 1; id <= idCount; ++id) {
+ const char* idStr = sdp_get_group_id(sdp, level, 0, attr, id);
+ if (!idStr) {
+ std::ostringstream os;
+ os << "bad a=group identifier at " << (attr - 1) << ", " << (id - 1);
+ results.AddParseError(0, os.str());
+ return false;
+ }
+ tags.push_back(std::string(idStr));
+ }
+ groups->PushEntry(semantics, tags);
+ }
+
+ if (!groups->mGroups.empty()) {
+ SetAttribute(groups.release());
+ }
+
+ return true;
+}
+
+bool SipccSdpAttributeList::LoadMsidSemantics(sdp_t* sdp, uint16_t level,
+ InternalResults& results) {
+ auto msidSemantics = MakeUnique<SdpMsidSemanticAttributeList>();
+
+ for (uint16_t i = 1; i < UINT16_MAX; ++i) {
+ sdp_attr_t* attr = sdp_find_attr(sdp, level, 0, SDP_ATTR_MSID_SEMANTIC, i);
+
+ if (!attr) {
+ break;
+ }
+
+ sdp_msid_semantic_t* msid_semantic = &(attr->attr.msid_semantic);
+ std::vector<std::string> msids;
+ for (size_t i = 0; i < SDP_MAX_MEDIA_STREAMS; ++i) {
+ if (!msid_semantic->msids[i]) {
+ break;
+ }
+
+ msids.push_back(msid_semantic->msids[i]);
+ }
+
+ msidSemantics->PushEntry(msid_semantic->semantic, msids);
+ }
+
+ if (!msidSemantics->mMsidSemantics.empty()) {
+ SetAttribute(msidSemantics.release());
+ }
+ return true;
+}
+
+void SipccSdpAttributeList::LoadIdentity(sdp_t* sdp, uint16_t level) {
+ const char* val =
+ sdp_attr_get_long_string(sdp, SDP_ATTR_IDENTITY, level, 0, 1);
+ if (val) {
+ SetAttribute(new SdpStringAttribute(SdpAttribute::kIdentityAttribute,
+ std::string(val)));
+ }
+}
+
+void SipccSdpAttributeList::LoadDtlsMessage(sdp_t* sdp, uint16_t level) {
+ const char* val =
+ sdp_attr_get_long_string(sdp, SDP_ATTR_DTLS_MESSAGE, level, 0, 1);
+ if (val) {
+ // sipcc does not expose parse code for this, so we use a SDParta-provided
+ // parser
+ std::string strval(val);
+ SetAttribute(new SdpDtlsMessageAttribute(strval));
+ }
+}
+
+void SipccSdpAttributeList::LoadFmtp(sdp_t* sdp, uint16_t level) {
+ auto fmtps = MakeUnique<SdpFmtpAttributeList>();
+
+ for (uint16_t i = 1; i < UINT16_MAX; ++i) {
+ sdp_attr_t* attr = sdp_find_attr(sdp, level, 0, SDP_ATTR_FMTP, i);
+
+ if (!attr) {
+ break;
+ }
+
+ sdp_fmtp_t* fmtp = &(attr->attr.fmtp);
+
+ // Get the payload type
+ std::stringstream osPayloadType;
+ // payload_num is the number in the fmtp attribute, verbatim
+ osPayloadType << fmtp->payload_num;
+
+ // Get parsed form of parameters, if supported
+ UniquePtr<SdpFmtpAttributeList::Parameters> parameters;
+
+ rtp_ptype codec = sdp_get_known_payload_type(sdp, level, fmtp->payload_num);
+
+ switch (codec) {
+ case RTP_H264_P0:
+ case RTP_H264_P1: {
+ SdpFmtpAttributeList::H264Parameters* h264Parameters(
+ new SdpFmtpAttributeList::H264Parameters);
+
+ sstrncpy(h264Parameters->sprop_parameter_sets, fmtp->parameter_sets,
+ sizeof(h264Parameters->sprop_parameter_sets));
+
+ h264Parameters->level_asymmetry_allowed =
+ !!(fmtp->level_asymmetry_allowed);
+
+ h264Parameters->packetization_mode = fmtp->packetization_mode;
+ sscanf(fmtp->profile_level_id, "%x", &h264Parameters->profile_level_id);
+ h264Parameters->max_mbps = fmtp->max_mbps;
+ h264Parameters->max_fs = fmtp->max_fs;
+ h264Parameters->max_cpb = fmtp->max_cpb;
+ h264Parameters->max_dpb = fmtp->max_dpb;
+ h264Parameters->max_br = fmtp->max_br;
+
+ parameters.reset(h264Parameters);
+ } break;
+ case RTP_VP9: {
+ SdpFmtpAttributeList::VP8Parameters* vp9Parameters(
+ new SdpFmtpAttributeList::VP8Parameters(
+ SdpRtpmapAttributeList::kVP9));
+
+ vp9Parameters->max_fs = fmtp->max_fs;
+ vp9Parameters->max_fr = fmtp->max_fr;
+
+ parameters.reset(vp9Parameters);
+ } break;
+ case RTP_VP8: {
+ SdpFmtpAttributeList::VP8Parameters* vp8Parameters(
+ new SdpFmtpAttributeList::VP8Parameters(
+ SdpRtpmapAttributeList::kVP8));
+
+ vp8Parameters->max_fs = fmtp->max_fs;
+ vp8Parameters->max_fr = fmtp->max_fr;
+
+ parameters.reset(vp8Parameters);
+ } break;
+ case RTP_RED: {
+ SdpFmtpAttributeList::RedParameters* redParameters(
+ new SdpFmtpAttributeList::RedParameters);
+ for (int i = 0; i < SDP_FMTP_MAX_REDUNDANT_ENCODINGS &&
+ fmtp->redundant_encodings[i];
+ ++i) {
+ redParameters->encodings.push_back(fmtp->redundant_encodings[i]);
+ }
+
+ parameters.reset(redParameters);
+ } break;
+ case RTP_OPUS: {
+ SdpFmtpAttributeList::OpusParameters* opusParameters(
+ new SdpFmtpAttributeList::OpusParameters);
+ opusParameters->maxplaybackrate = fmtp->maxplaybackrate;
+ opusParameters->stereo = fmtp->stereo;
+ opusParameters->useInBandFec = fmtp->useinbandfec;
+ opusParameters->maxAverageBitrate = fmtp->maxaveragebitrate;
+ opusParameters->useDTX = fmtp->usedtx;
+ parameters.reset(opusParameters);
+ } break;
+ case RTP_TELEPHONE_EVENT: {
+ SdpFmtpAttributeList::TelephoneEventParameters* teParameters(
+ new SdpFmtpAttributeList::TelephoneEventParameters);
+ if (strlen(fmtp->dtmf_tones) > 0) {
+ teParameters->dtmfTones = fmtp->dtmf_tones;
+ }
+ parameters.reset(teParameters);
+ } break;
+ case RTP_RTX: {
+ SdpFmtpAttributeList::RtxParameters* rtxParameters(
+ new SdpFmtpAttributeList::RtxParameters);
+ rtxParameters->apt = fmtp->apt;
+ if (fmtp->has_rtx_time == TRUE) {
+ rtxParameters->rtx_time = Some(fmtp->rtx_time);
+ }
+ parameters.reset(rtxParameters);
+ } break;
+ default: {
+ }
+ }
+
+ if (parameters) {
+ fmtps->PushEntry(osPayloadType.str(), *parameters);
+ }
+ }
+
+ if (!fmtps->mFmtps.empty()) {
+ SetAttribute(fmtps.release());
+ }
+}
+
+void SipccSdpAttributeList::LoadMsids(sdp_t* sdp, uint16_t level,
+ InternalResults& results) {
+ uint16_t attrCount = 0;
+ if (sdp_attr_num_instances(sdp, level, 0, SDP_ATTR_MSID, &attrCount) !=
+ SDP_SUCCESS) {
+ MOZ_ASSERT(false, "Unable to get count of msid attributes");
+ results.AddParseError(0, "Unable to get count of msid attributes");
+ return;
+ }
+ auto msids = MakeUnique<SdpMsidAttributeList>();
+ for (uint16_t i = 1; i <= attrCount; ++i) {
+ uint32_t lineNumber = sdp_attr_line_number(sdp, SDP_ATTR_MSID, level, 0, i);
+
+ const char* identifier = sdp_attr_get_msid_identifier(sdp, level, 0, i);
+ if (!identifier) {
+ results.AddParseError(lineNumber, "msid attribute with bad identity");
+ continue;
+ }
+
+ const char* appdata = sdp_attr_get_msid_appdata(sdp, level, 0, i);
+ if (!appdata) {
+ results.AddParseError(lineNumber, "msid attribute with bad appdata");
+ continue;
+ }
+
+ msids->PushEntry(identifier, appdata);
+ }
+
+ if (!msids->mMsids.empty()) {
+ SetAttribute(msids.release());
+ }
+}
+
+bool SipccSdpAttributeList::LoadRid(sdp_t* sdp, uint16_t level,
+ InternalResults& results) {
+ UniquePtr<SdpRidAttributeList> rids(new SdpRidAttributeList);
+
+ for (uint16_t i = 1; i < UINT16_MAX; ++i) {
+ const char* ridRaw =
+ sdp_attr_get_simple_string(sdp, SDP_ATTR_RID, level, 0, i);
+ if (!ridRaw) {
+ break;
+ }
+
+ std::string error;
+ size_t errorPos;
+ if (!rids->PushEntry(ridRaw, &error, &errorPos)) {
+ std::ostringstream fullError;
+ fullError << error << " at column " << errorPos;
+ results.AddParseError(
+ sdp_attr_line_number(sdp, SDP_ATTR_RID, level, 0, i),
+ fullError.str());
+ return false;
+ }
+ }
+
+ if (!rids->mRids.empty()) {
+ SetAttribute(rids.release());
+ }
+ return true;
+}
+
+void SipccSdpAttributeList::LoadExtmap(sdp_t* sdp, uint16_t level,
+ InternalResults& results) {
+ auto extmaps = MakeUnique<SdpExtmapAttributeList>();
+
+ for (uint16_t i = 1; i < UINT16_MAX; ++i) {
+ sdp_attr_t* attr = sdp_find_attr(sdp, level, 0, SDP_ATTR_EXTMAP, i);
+
+ if (!attr) {
+ break;
+ }
+
+ sdp_extmap_t* extmap = &(attr->attr.extmap);
+
+ SdpDirectionAttribute::Direction dir = SdpDirectionAttribute::kSendrecv;
+
+ if (extmap->media_direction_specified) {
+ ConvertDirection(extmap->media_direction, &dir);
+ }
+
+ extmaps->PushEntry(extmap->id, dir, extmap->media_direction_specified,
+ extmap->uri, extmap->extension_attributes);
+ }
+
+ if (!extmaps->mExtmaps.empty()) {
+ if (!AtSessionLevel() &&
+ mSessionLevel->HasAttribute(SdpAttribute::kExtmapAttribute)) {
+ uint32_t lineNumber =
+ sdp_attr_line_number(sdp, SDP_ATTR_EXTMAP, level, 0, 1);
+ results.AddParseError(
+ lineNumber, "extmap attributes in both session and media level");
+ }
+ SetAttribute(extmaps.release());
+ }
+}
+
+void SipccSdpAttributeList::LoadRtcpFb(sdp_t* sdp, uint16_t level,
+ InternalResults& results) {
+ auto rtcpfbs = MakeUnique<SdpRtcpFbAttributeList>();
+
+ for (uint16_t i = 1; i < UINT16_MAX; ++i) {
+ sdp_attr_t* attr = sdp_find_attr(sdp, level, 0, SDP_ATTR_RTCP_FB, i);
+
+ if (!attr) {
+ break;
+ }
+
+ sdp_fmtp_fb_t* rtcpfb = &attr->attr.rtcp_fb;
+
+ SdpRtcpFbAttributeList::Type type;
+ std::string parameter;
+
+ // Set type and parameter
+ switch (rtcpfb->feedback_type) {
+ case SDP_RTCP_FB_ACK:
+ type = SdpRtcpFbAttributeList::kAck;
+ switch (rtcpfb->param.ack) {
+ // TODO: sipcc doesn't seem to support ack with no following token.
+ // Issue 189.
+ case SDP_RTCP_FB_ACK_RPSI:
+ parameter = SdpRtcpFbAttributeList::rpsi;
+ break;
+ case SDP_RTCP_FB_ACK_APP:
+ parameter = SdpRtcpFbAttributeList::app;
+ break;
+ default:
+ // Type we don't care about, ignore.
+ continue;
+ }
+ break;
+ case SDP_RTCP_FB_CCM:
+ type = SdpRtcpFbAttributeList::kCcm;
+ switch (rtcpfb->param.ccm) {
+ case SDP_RTCP_FB_CCM_FIR:
+ parameter = SdpRtcpFbAttributeList::fir;
+ break;
+ case SDP_RTCP_FB_CCM_TMMBR:
+ parameter = SdpRtcpFbAttributeList::tmmbr;
+ break;
+ case SDP_RTCP_FB_CCM_TSTR:
+ parameter = SdpRtcpFbAttributeList::tstr;
+ break;
+ case SDP_RTCP_FB_CCM_VBCM:
+ parameter = SdpRtcpFbAttributeList::vbcm;
+ break;
+ default:
+ // Type we don't care about, ignore.
+ continue;
+ }
+ break;
+ case SDP_RTCP_FB_NACK:
+ type = SdpRtcpFbAttributeList::kNack;
+ switch (rtcpfb->param.nack) {
+ case SDP_RTCP_FB_NACK_BASIC:
+ break;
+ case SDP_RTCP_FB_NACK_SLI:
+ parameter = SdpRtcpFbAttributeList::sli;
+ break;
+ case SDP_RTCP_FB_NACK_PLI:
+ parameter = SdpRtcpFbAttributeList::pli;
+ break;
+ case SDP_RTCP_FB_NACK_RPSI:
+ parameter = SdpRtcpFbAttributeList::rpsi;
+ break;
+ case SDP_RTCP_FB_NACK_APP:
+ parameter = SdpRtcpFbAttributeList::app;
+ break;
+ default:
+ // Type we don't care about, ignore.
+ continue;
+ }
+ break;
+ case SDP_RTCP_FB_TRR_INT: {
+ type = SdpRtcpFbAttributeList::kTrrInt;
+ std::ostringstream os;
+ os << rtcpfb->param.trr_int;
+ parameter = os.str();
+ } break;
+ case SDP_RTCP_FB_REMB: {
+ type = SdpRtcpFbAttributeList::kRemb;
+ } break;
+ case SDP_RTCP_FB_TRANSPORT_CC: {
+ type = SdpRtcpFbAttributeList::kTransportCC;
+ } break;
+ default:
+ // Type we don't care about, ignore.
+ continue;
+ }
+
+ std::stringstream osPayloadType;
+ if (rtcpfb->payload_num == UINT16_MAX) {
+ osPayloadType << "*";
+ } else {
+ osPayloadType << rtcpfb->payload_num;
+ }
+
+ std::string pt(osPayloadType.str());
+ std::string extra(rtcpfb->extra);
+
+ rtcpfbs->PushEntry(pt, type, parameter, extra);
+ }
+
+ if (!rtcpfbs->mFeedbacks.empty()) {
+ SetAttribute(rtcpfbs.release());
+ }
+}
+
+void SipccSdpAttributeList::LoadRtcp(sdp_t* sdp, uint16_t level,
+ InternalResults& results) {
+ sdp_attr_t* attr = sdp_find_attr(sdp, level, 0, SDP_ATTR_RTCP, 1);
+
+ if (!attr) {
+ return;
+ }
+
+ sdp_rtcp_t* rtcp = &attr->attr.rtcp;
+
+ if (rtcp->nettype != SDP_NT_INTERNET) {
+ return;
+ }
+
+ if (rtcp->addrtype != SDP_AT_IP4 && rtcp->addrtype != SDP_AT_IP6) {
+ return;
+ }
+
+ if (!strlen(rtcp->addr)) {
+ SetAttribute(new SdpRtcpAttribute(rtcp->port));
+ } else {
+ SetAttribute(new SdpRtcpAttribute(
+ rtcp->port, sdp::kInternet,
+ rtcp->addrtype == SDP_AT_IP4 ? sdp::kIPv4 : sdp::kIPv6, rtcp->addr));
+ }
+}
+
+bool SipccSdpAttributeList::Load(sdp_t* sdp, uint16_t level,
+ InternalResults& results) {
+ LoadSimpleStrings(sdp, level, results);
+ LoadSimpleNumbers(sdp, level, results);
+ LoadFlags(sdp, level);
+ LoadDirection(sdp, level, results);
+
+ if (AtSessionLevel()) {
+ if (!LoadGroups(sdp, level, results)) {
+ return false;
+ }
+
+ if (!LoadMsidSemantics(sdp, level, results)) {
+ return false;
+ }
+
+ LoadIdentity(sdp, level);
+ LoadDtlsMessage(sdp, level);
+ } else {
+ sdp_media_e mtype = sdp_get_media_type(sdp, level);
+ if (mtype == SDP_MEDIA_APPLICATION) {
+ LoadSctpmap(sdp, level, results);
+ } else {
+ if (!LoadRtpmap(sdp, level, results)) {
+ return false;
+ }
+ }
+ LoadCandidate(sdp, level);
+ LoadFmtp(sdp, level);
+ LoadMsids(sdp, level, results);
+ LoadRtcpFb(sdp, level, results);
+ LoadRtcp(sdp, level, results);
+ LoadSsrc(sdp, level);
+ LoadSsrcGroup(sdp, level);
+ if (!LoadImageattr(sdp, level, results)) {
+ return false;
+ }
+ if (!LoadSimulcast(sdp, level, results)) {
+ return false;
+ }
+ if (!LoadRid(sdp, level, results)) {
+ return false;
+ }
+ }
+
+ LoadIceAttributes(sdp, level);
+ if (!LoadFingerprint(sdp, level, results)) {
+ return false;
+ }
+ LoadSetup(sdp, level);
+ LoadExtmap(sdp, level, results);
+
+ return true;
+}
+
+bool SipccSdpAttributeList::IsAllowedHere(
+ SdpAttribute::AttributeType type) const {
+ if (AtSessionLevel() && !SdpAttribute::IsAllowedAtSessionLevel(type)) {
+ return false;
+ }
+
+ if (!AtSessionLevel() && !SdpAttribute::IsAllowedAtMediaLevel(type)) {
+ return false;
+ }
+
+ return true;
+}
+
+void SipccSdpAttributeList::WarnAboutMisplacedAttribute(
+ SdpAttribute::AttributeType type, uint32_t lineNumber,
+ InternalResults& results) {
+ std::string warning = SdpAttribute::GetAttributeTypeString(type) +
+ (AtSessionLevel() ? " at session level. Ignoring."
+ : " at media level. Ignoring.");
+ results.AddParseError(lineNumber, warning);
+}
+
+const std::vector<std::string>& SipccSdpAttributeList::GetCandidate() const {
+ if (!HasAttribute(SdpAttribute::kCandidateAttribute)) {
+ MOZ_CRASH();
+ }
+
+ return static_cast<const SdpMultiStringAttribute*>(
+ GetAttribute(SdpAttribute::kCandidateAttribute))
+ ->mValues;
+}
+
+const SdpConnectionAttribute& SipccSdpAttributeList::GetConnection() const {
+ if (!HasAttribute(SdpAttribute::kConnectionAttribute)) {
+ MOZ_CRASH();
+ }
+
+ return *static_cast<const SdpConnectionAttribute*>(
+ GetAttribute(SdpAttribute::kConnectionAttribute));
+}
+
+SdpDirectionAttribute::Direction SipccSdpAttributeList::GetDirection() const {
+ if (!HasAttribute(SdpAttribute::kDirectionAttribute)) {
+ MOZ_CRASH();
+ }
+
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kDirectionAttribute);
+ return static_cast<const SdpDirectionAttribute*>(attr)->mValue;
+}
+
+const SdpDtlsMessageAttribute& SipccSdpAttributeList::GetDtlsMessage() const {
+ if (!HasAttribute(SdpAttribute::kDtlsMessageAttribute)) {
+ MOZ_CRASH();
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kDtlsMessageAttribute);
+ return *static_cast<const SdpDtlsMessageAttribute*>(attr);
+}
+
+const SdpExtmapAttributeList& SipccSdpAttributeList::GetExtmap() const {
+ if (!HasAttribute(SdpAttribute::kExtmapAttribute)) {
+ MOZ_CRASH();
+ }
+
+ return *static_cast<const SdpExtmapAttributeList*>(
+ GetAttribute(SdpAttribute::kExtmapAttribute));
+}
+
+const SdpFingerprintAttributeList& SipccSdpAttributeList::GetFingerprint()
+ const {
+ if (!HasAttribute(SdpAttribute::kFingerprintAttribute)) {
+ MOZ_CRASH();
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kFingerprintAttribute);
+ return *static_cast<const SdpFingerprintAttributeList*>(attr);
+}
+
+const SdpFmtpAttributeList& SipccSdpAttributeList::GetFmtp() const {
+ if (!HasAttribute(SdpAttribute::kFmtpAttribute)) {
+ MOZ_CRASH();
+ }
+
+ return *static_cast<const SdpFmtpAttributeList*>(
+ GetAttribute(SdpAttribute::kFmtpAttribute));
+}
+
+const SdpGroupAttributeList& SipccSdpAttributeList::GetGroup() const {
+ if (!HasAttribute(SdpAttribute::kGroupAttribute)) {
+ MOZ_CRASH();
+ }
+
+ return *static_cast<const SdpGroupAttributeList*>(
+ GetAttribute(SdpAttribute::kGroupAttribute));
+}
+
+const SdpOptionsAttribute& SipccSdpAttributeList::GetIceOptions() const {
+ if (!HasAttribute(SdpAttribute::kIceOptionsAttribute)) {
+ MOZ_CRASH();
+ }
+
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kIceOptionsAttribute);
+ return *static_cast<const SdpOptionsAttribute*>(attr);
+}
+
+const std::string& SipccSdpAttributeList::GetIcePwd() const {
+ if (!HasAttribute(SdpAttribute::kIcePwdAttribute)) {
+ return kEmptyString;
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kIcePwdAttribute);
+ return static_cast<const SdpStringAttribute*>(attr)->mValue;
+}
+
+const std::string& SipccSdpAttributeList::GetIceUfrag() const {
+ if (!HasAttribute(SdpAttribute::kIceUfragAttribute)) {
+ return kEmptyString;
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kIceUfragAttribute);
+ return static_cast<const SdpStringAttribute*>(attr)->mValue;
+}
+
+const std::string& SipccSdpAttributeList::GetIdentity() const {
+ if (!HasAttribute(SdpAttribute::kIdentityAttribute)) {
+ return kEmptyString;
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kIdentityAttribute);
+ return static_cast<const SdpStringAttribute*>(attr)->mValue;
+}
+
+const SdpImageattrAttributeList& SipccSdpAttributeList::GetImageattr() const {
+ if (!HasAttribute(SdpAttribute::kImageattrAttribute)) {
+ MOZ_CRASH();
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kImageattrAttribute);
+ return *static_cast<const SdpImageattrAttributeList*>(attr);
+}
+
+const SdpSimulcastAttribute& SipccSdpAttributeList::GetSimulcast() const {
+ if (!HasAttribute(SdpAttribute::kSimulcastAttribute)) {
+ MOZ_CRASH();
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kSimulcastAttribute);
+ return *static_cast<const SdpSimulcastAttribute*>(attr);
+}
+
+const std::string& SipccSdpAttributeList::GetLabel() const {
+ if (!HasAttribute(SdpAttribute::kLabelAttribute)) {
+ return kEmptyString;
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kLabelAttribute);
+ return static_cast<const SdpStringAttribute*>(attr)->mValue;
+}
+
+uint32_t SipccSdpAttributeList::GetMaxptime() const {
+ if (!HasAttribute(SdpAttribute::kMaxptimeAttribute)) {
+ MOZ_CRASH();
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kMaxptimeAttribute);
+ return static_cast<const SdpNumberAttribute*>(attr)->mValue;
+}
+
+const std::string& SipccSdpAttributeList::GetMid() const {
+ if (!HasAttribute(SdpAttribute::kMidAttribute)) {
+ return kEmptyString;
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kMidAttribute);
+ return static_cast<const SdpStringAttribute*>(attr)->mValue;
+}
+
+const SdpMsidAttributeList& SipccSdpAttributeList::GetMsid() const {
+ if (!HasAttribute(SdpAttribute::kMsidAttribute)) {
+ MOZ_CRASH();
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kMsidAttribute);
+ return *static_cast<const SdpMsidAttributeList*>(attr);
+}
+
+const SdpMsidSemanticAttributeList& SipccSdpAttributeList::GetMsidSemantic()
+ const {
+ if (!HasAttribute(SdpAttribute::kMsidSemanticAttribute)) {
+ MOZ_CRASH();
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kMsidSemanticAttribute);
+ return *static_cast<const SdpMsidSemanticAttributeList*>(attr);
+}
+
+const SdpRidAttributeList& SipccSdpAttributeList::GetRid() const {
+ if (!HasAttribute(SdpAttribute::kRidAttribute)) {
+ MOZ_CRASH();
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kRidAttribute);
+ return *static_cast<const SdpRidAttributeList*>(attr);
+}
+
+uint32_t SipccSdpAttributeList::GetPtime() const {
+ if (!HasAttribute(SdpAttribute::kPtimeAttribute)) {
+ MOZ_CRASH();
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kPtimeAttribute);
+ return static_cast<const SdpNumberAttribute*>(attr)->mValue;
+}
+
+const SdpRtcpAttribute& SipccSdpAttributeList::GetRtcp() const {
+ if (!HasAttribute(SdpAttribute::kRtcpAttribute)) {
+ MOZ_CRASH();
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kRtcpAttribute);
+ return *static_cast<const SdpRtcpAttribute*>(attr);
+}
+
+const SdpRtcpFbAttributeList& SipccSdpAttributeList::GetRtcpFb() const {
+ if (!HasAttribute(SdpAttribute::kRtcpFbAttribute)) {
+ MOZ_CRASH();
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kRtcpFbAttribute);
+ return *static_cast<const SdpRtcpFbAttributeList*>(attr);
+}
+
+const SdpRemoteCandidatesAttribute& SipccSdpAttributeList::GetRemoteCandidates()
+ const {
+ MOZ_CRASH("Not yet implemented");
+}
+
+const SdpRtpmapAttributeList& SipccSdpAttributeList::GetRtpmap() const {
+ if (!HasAttribute(SdpAttribute::kRtpmapAttribute)) {
+ MOZ_CRASH();
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kRtpmapAttribute);
+ return *static_cast<const SdpRtpmapAttributeList*>(attr);
+}
+
+const SdpSctpmapAttributeList& SipccSdpAttributeList::GetSctpmap() const {
+ if (!HasAttribute(SdpAttribute::kSctpmapAttribute)) {
+ MOZ_CRASH();
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kSctpmapAttribute);
+ return *static_cast<const SdpSctpmapAttributeList*>(attr);
+}
+
+uint32_t SipccSdpAttributeList::GetSctpPort() const {
+ if (!HasAttribute(SdpAttribute::kSctpPortAttribute)) {
+ MOZ_CRASH();
+ }
+
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kSctpPortAttribute);
+ return static_cast<const SdpNumberAttribute*>(attr)->mValue;
+}
+
+uint32_t SipccSdpAttributeList::GetMaxMessageSize() const {
+ if (!HasAttribute(SdpAttribute::kMaxMessageSizeAttribute)) {
+ MOZ_CRASH();
+ }
+
+ const SdpAttribute* attr =
+ GetAttribute(SdpAttribute::kMaxMessageSizeAttribute);
+ return static_cast<const SdpNumberAttribute*>(attr)->mValue;
+}
+
+const SdpSetupAttribute& SipccSdpAttributeList::GetSetup() const {
+ if (!HasAttribute(SdpAttribute::kSetupAttribute)) {
+ MOZ_CRASH();
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kSetupAttribute);
+ return *static_cast<const SdpSetupAttribute*>(attr);
+}
+
+const SdpSsrcAttributeList& SipccSdpAttributeList::GetSsrc() const {
+ if (!HasAttribute(SdpAttribute::kSsrcAttribute)) {
+ MOZ_CRASH();
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kSsrcAttribute);
+ return *static_cast<const SdpSsrcAttributeList*>(attr);
+}
+
+const SdpSsrcGroupAttributeList& SipccSdpAttributeList::GetSsrcGroup() const {
+ if (!HasAttribute(SdpAttribute::kSsrcGroupAttribute)) {
+ MOZ_CRASH();
+ }
+ const SdpAttribute* attr = GetAttribute(SdpAttribute::kSsrcGroupAttribute);
+ return *static_cast<const SdpSsrcGroupAttributeList*>(attr);
+}
+
+void SipccSdpAttributeList::Serialize(std::ostream& os) const {
+ for (size_t i = 0; i < kNumAttributeTypes; ++i) {
+ if (mAttributes[i]) {
+ os << *mAttributes[i];
+ }
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/sdp/SipccSdpAttributeList.h b/dom/media/webrtc/sdp/SipccSdpAttributeList.h
new file mode 100644
index 0000000000..4499e84bb9
--- /dev/null
+++ b/dom/media/webrtc/sdp/SipccSdpAttributeList.h
@@ -0,0 +1,145 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _SIPCCSDPATTRIBUTELIST_H_
+#define _SIPCCSDPATTRIBUTELIST_H_
+
+#include "sdp/SdpParser.h"
+#include "sdp/SdpAttributeList.h"
+
+extern "C" {
+#include "sipcc_sdp.h"
+}
+
+namespace mozilla {
+
+class SipccSdp;
+class SipccSdpMediaSection;
+
+class SipccSdpAttributeList : public SdpAttributeList {
+ friend class SipccSdpMediaSection;
+ friend class SipccSdp;
+
+ public:
+ // Make sure we don't hide the default arg thunks
+ using SdpAttributeList::GetAttribute;
+ using SdpAttributeList::HasAttribute;
+
+ virtual bool HasAttribute(AttributeType type,
+ bool sessionFallback) const override;
+ virtual const SdpAttribute* GetAttribute(AttributeType type,
+ bool sessionFallback) const override;
+ virtual void SetAttribute(SdpAttribute* attr) override;
+ virtual void RemoveAttribute(AttributeType type) override;
+ virtual void Clear() override;
+ virtual uint32_t Count() const override;
+
+ virtual const SdpConnectionAttribute& GetConnection() const override;
+ virtual const SdpFingerprintAttributeList& GetFingerprint() const override;
+ virtual const SdpGroupAttributeList& GetGroup() const override;
+ virtual const SdpOptionsAttribute& GetIceOptions() const override;
+ virtual const SdpRtcpAttribute& GetRtcp() const override;
+ virtual const SdpRemoteCandidatesAttribute& GetRemoteCandidates()
+ const override;
+ virtual const SdpSetupAttribute& GetSetup() const override;
+ virtual const SdpSsrcAttributeList& GetSsrc() const override;
+ virtual const SdpSsrcGroupAttributeList& GetSsrcGroup() const override;
+ virtual const SdpDtlsMessageAttribute& GetDtlsMessage() const override;
+
+ // These attributes can appear multiple times, so the returned
+ // classes actually represent a collection of values.
+ virtual const std::vector<std::string>& GetCandidate() const override;
+ virtual const SdpExtmapAttributeList& GetExtmap() const override;
+ virtual const SdpFmtpAttributeList& GetFmtp() const override;
+ virtual const SdpImageattrAttributeList& GetImageattr() const override;
+ const SdpSimulcastAttribute& GetSimulcast() const override;
+ virtual const SdpMsidAttributeList& GetMsid() const override;
+ virtual const SdpMsidSemanticAttributeList& GetMsidSemantic() const override;
+ const SdpRidAttributeList& GetRid() const override;
+ virtual const SdpRtcpFbAttributeList& GetRtcpFb() const override;
+ virtual const SdpRtpmapAttributeList& GetRtpmap() const override;
+ virtual const SdpSctpmapAttributeList& GetSctpmap() const override;
+ virtual uint32_t GetSctpPort() const override;
+ virtual uint32_t GetMaxMessageSize() const override;
+
+ // These attributes are effectively simple types, so we'll make life
+ // easy by just returning their value.
+ virtual const std::string& GetIcePwd() const override;
+ virtual const std::string& GetIceUfrag() const override;
+ virtual const std::string& GetIdentity() const override;
+ virtual const std::string& GetLabel() const override;
+ virtual unsigned int GetMaxptime() const override;
+ virtual const std::string& GetMid() const override;
+ virtual unsigned int GetPtime() const override;
+
+ virtual SdpDirectionAttribute::Direction GetDirection() const override;
+
+ virtual void Serialize(std::ostream&) const override;
+
+ virtual ~SipccSdpAttributeList();
+
+ private:
+ static const std::string kEmptyString;
+ static const size_t kNumAttributeTypes = SdpAttribute::kLastAttribute + 1;
+
+ // Pass a session-level attribute list if constructing a media-level one,
+ // otherwise pass nullptr
+ explicit SipccSdpAttributeList(const SipccSdpAttributeList* sessionLevel);
+
+ // Copy c'tor, sort of
+ SipccSdpAttributeList(const SipccSdpAttributeList& aOrig,
+ const SipccSdpAttributeList* sessionLevel);
+
+ using InternalResults = SdpParser::InternalResults;
+
+ bool Load(sdp_t* sdp, uint16_t level, InternalResults& results);
+ void LoadSimpleStrings(sdp_t* sdp, uint16_t level, InternalResults& results);
+ void LoadSimpleString(sdp_t* sdp, uint16_t level, sdp_attr_e attr,
+ AttributeType targetType, InternalResults& results);
+ void LoadSimpleNumbers(sdp_t* sdp, uint16_t level, InternalResults& results);
+ void LoadSimpleNumber(sdp_t* sdp, uint16_t level, sdp_attr_e attr,
+ AttributeType targetType, InternalResults& results);
+ void LoadFlags(sdp_t* sdp, uint16_t level);
+ void LoadDirection(sdp_t* sdp, uint16_t level, InternalResults& results);
+ bool LoadRtpmap(sdp_t* sdp, uint16_t level, InternalResults& results);
+ bool LoadSctpmap(sdp_t* sdp, uint16_t level, InternalResults& results);
+ void LoadIceAttributes(sdp_t* sdp, uint16_t level);
+ bool LoadFingerprint(sdp_t* sdp, uint16_t level, InternalResults& results);
+ void LoadCandidate(sdp_t* sdp, uint16_t level);
+ void LoadSetup(sdp_t* sdp, uint16_t level);
+ void LoadSsrc(sdp_t* sdp, uint16_t level);
+ void LoadSsrcGroup(sdp_t* sdp, uint16_t level);
+ bool LoadImageattr(sdp_t* sdp, uint16_t level, InternalResults& results);
+ bool LoadSimulcast(sdp_t* sdp, uint16_t level, InternalResults& results);
+ bool LoadGroups(sdp_t* sdp, uint16_t level, InternalResults& results);
+ bool LoadMsidSemantics(sdp_t* sdp, uint16_t level, InternalResults& results);
+ void LoadIdentity(sdp_t* sdp, uint16_t level);
+ void LoadDtlsMessage(sdp_t* sdp, uint16_t level);
+ void LoadFmtp(sdp_t* sdp, uint16_t level);
+ void LoadMsids(sdp_t* sdp, uint16_t level, InternalResults& results);
+ bool LoadRid(sdp_t* sdp, uint16_t level, InternalResults& results);
+ void LoadExtmap(sdp_t* sdp, uint16_t level, InternalResults& results);
+ void LoadRtcpFb(sdp_t* sdp, uint16_t level, InternalResults& results);
+ void LoadRtcp(sdp_t* sdp, uint16_t level, InternalResults& results);
+ static SdpRtpmapAttributeList::CodecType GetCodecType(rtp_ptype type);
+
+ bool AtSessionLevel() const { return !mSessionLevel; }
+ bool IsAllowedHere(SdpAttribute::AttributeType type) const;
+ void WarnAboutMisplacedAttribute(SdpAttribute::AttributeType type,
+ uint32_t lineNumber,
+ InternalResults& results);
+
+ const SipccSdpAttributeList* mSessionLevel;
+
+ SdpAttribute* mAttributes[kNumAttributeTypes];
+
+ SipccSdpAttributeList(const SipccSdpAttributeList& orig) = delete;
+ SipccSdpAttributeList& operator=(const SipccSdpAttributeList& rhs) = delete;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/sdp/SipccSdpMediaSection.cpp b/dom/media/webrtc/sdp/SipccSdpMediaSection.cpp
new file mode 100644
index 0000000000..4304ca541e
--- /dev/null
+++ b/dom/media/webrtc/sdp/SipccSdpMediaSection.cpp
@@ -0,0 +1,401 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "sdp/SipccSdpMediaSection.h"
+
+#include <ostream>
+#include "sdp/SdpParser.h"
+
+extern "C" {
+#include "sipcc_sdp.h"
+}
+
+#ifdef CRLF
+# undef CRLF
+#endif
+#define CRLF "\r\n"
+
+namespace mozilla {
+
+SipccSdpMediaSection::SipccSdpMediaSection(
+ const SipccSdpMediaSection& aOrig,
+ const SipccSdpAttributeList* sessionLevel)
+ : SdpMediaSection(aOrig),
+ mMediaType(aOrig.mMediaType),
+ mPort(aOrig.mPort),
+ mPortCount(aOrig.mPortCount),
+ mProtocol(aOrig.mProtocol),
+ mFormats(aOrig.mFormats),
+ mConnection(new SdpConnection(*aOrig.mConnection)),
+ mBandwidths(aOrig.mBandwidths),
+ mAttributeList(aOrig.mAttributeList, sessionLevel) {}
+
+unsigned int SipccSdpMediaSection::GetPort() const { return mPort; }
+
+void SipccSdpMediaSection::SetPort(unsigned int port) { mPort = port; }
+
+unsigned int SipccSdpMediaSection::GetPortCount() const { return mPortCount; }
+
+SdpMediaSection::Protocol SipccSdpMediaSection::GetProtocol() const {
+ return mProtocol;
+}
+
+const SdpConnection& SipccSdpMediaSection::GetConnection() const {
+ return *mConnection;
+}
+
+SdpConnection& SipccSdpMediaSection::GetConnection() { return *mConnection; }
+
+uint32_t SipccSdpMediaSection::GetBandwidth(const std::string& type) const {
+ auto found = mBandwidths.find(type);
+ if (found == mBandwidths.end()) {
+ return 0;
+ }
+ return found->second;
+}
+
+const std::vector<std::string>& SipccSdpMediaSection::GetFormats() const {
+ return mFormats;
+}
+
+const SdpAttributeList& SipccSdpMediaSection::GetAttributeList() const {
+ return mAttributeList;
+}
+
+SdpAttributeList& SipccSdpMediaSection::GetAttributeList() {
+ return mAttributeList;
+}
+
+SdpDirectionAttribute SipccSdpMediaSection::GetDirectionAttribute() const {
+ return SdpDirectionAttribute(mAttributeList.GetDirection());
+}
+
+bool SipccSdpMediaSection::Load(sdp_t* sdp, uint16_t level,
+ InternalResults& results) {
+ switch (sdp_get_media_type(sdp, level)) {
+ case SDP_MEDIA_AUDIO:
+ mMediaType = kAudio;
+ break;
+ case SDP_MEDIA_VIDEO:
+ mMediaType = kVideo;
+ break;
+ case SDP_MEDIA_APPLICATION:
+ mMediaType = kApplication;
+ break;
+ case SDP_MEDIA_TEXT:
+ mMediaType = kText;
+ break;
+
+ default:
+ results.AddParseError(sdp_get_media_line_number(sdp, level),
+ "Unsupported media section type");
+ return false;
+ }
+
+ mPort = sdp_get_media_portnum(sdp, level);
+ int32_t pc = sdp_get_media_portcount(sdp, level);
+ if (pc == SDP_INVALID_VALUE) {
+ // SDP_INVALID_VALUE (ie; -2) is used when there is no port count. :(
+ mPortCount = 0;
+ } else if (pc > static_cast<int32_t>(UINT16_MAX) || pc < 0) {
+ results.AddParseError(sdp_get_media_line_number(sdp, level),
+ "Invalid port count");
+ return false;
+ } else {
+ mPortCount = pc;
+ }
+
+ if (!LoadProtocol(sdp, level, results)) {
+ return false;
+ }
+
+ if (!LoadFormats(sdp, level, results)) {
+ return false;
+ }
+
+ if (!mAttributeList.Load(sdp, level, results)) {
+ return false;
+ }
+
+ if (!ValidateSimulcast(sdp, level, results)) {
+ return false;
+ }
+
+ if (!mBandwidths.Load(sdp, level, results)) {
+ return false;
+ }
+
+ return LoadConnection(sdp, level, results);
+}
+
+bool SipccSdpMediaSection::LoadProtocol(sdp_t* sdp, uint16_t level,
+ InternalResults& results) {
+ switch (sdp_get_media_transport(sdp, level)) {
+ case SDP_TRANSPORT_RTPAVP:
+ mProtocol = kRtpAvp;
+ break;
+ case SDP_TRANSPORT_RTPSAVP:
+ mProtocol = kRtpSavp;
+ break;
+ case SDP_TRANSPORT_RTPAVPF:
+ mProtocol = kRtpAvpf;
+ break;
+ case SDP_TRANSPORT_RTPSAVPF:
+ mProtocol = kRtpSavpf;
+ break;
+ case SDP_TRANSPORT_UDPTLSRTPSAVP:
+ mProtocol = kUdpTlsRtpSavp;
+ break;
+ case SDP_TRANSPORT_UDPTLSRTPSAVPF:
+ mProtocol = kUdpTlsRtpSavpf;
+ break;
+ case SDP_TRANSPORT_TCPDTLSRTPSAVP:
+ mProtocol = kTcpDtlsRtpSavp;
+ break;
+ case SDP_TRANSPORT_TCPDTLSRTPSAVPF:
+ mProtocol = kTcpDtlsRtpSavpf;
+ break;
+ case SDP_TRANSPORT_DTLSSCTP:
+ mProtocol = kDtlsSctp;
+ break;
+ case SDP_TRANSPORT_UDPDTLSSCTP:
+ mProtocol = kUdpDtlsSctp;
+ break;
+ case SDP_TRANSPORT_TCPDTLSSCTP:
+ mProtocol = kTcpDtlsSctp;
+ break;
+
+ default:
+ results.AddParseError(sdp_get_media_line_number(sdp, level),
+ "Unsupported media transport type");
+ return false;
+ }
+ return true;
+}
+
+bool SipccSdpMediaSection::LoadFormats(sdp_t* sdp, uint16_t level,
+ InternalResults& results) {
+ sdp_media_e mtype = sdp_get_media_type(sdp, level);
+
+ if (mtype == SDP_MEDIA_APPLICATION) {
+ sdp_transport_e ttype = sdp_get_media_transport(sdp, level);
+ if ((ttype == SDP_TRANSPORT_UDPDTLSSCTP) ||
+ (ttype == SDP_TRANSPORT_TCPDTLSSCTP)) {
+ if (sdp_get_media_sctp_fmt(sdp, level) ==
+ SDP_SCTP_MEDIA_FMT_WEBRTC_DATACHANNEL) {
+ mFormats.push_back("webrtc-datachannel");
+ }
+ } else {
+ uint32_t ptype = sdp_get_media_sctp_port(sdp, level);
+ std::ostringstream osPayloadType;
+ osPayloadType << ptype;
+ mFormats.push_back(osPayloadType.str());
+ }
+ } else if (mtype == SDP_MEDIA_AUDIO || mtype == SDP_MEDIA_VIDEO) {
+ uint16_t count = sdp_get_media_num_payload_types(sdp, level);
+ for (uint16_t i = 0; i < count; ++i) {
+ sdp_payload_ind_e indicator; // we ignore this, which is fine
+ uint32_t ptype =
+ sdp_get_media_payload_type(sdp, level, i + 1, &indicator);
+
+ if (GET_DYN_PAYLOAD_TYPE_VALUE(ptype) > UINT8_MAX) {
+ results.AddParseError(sdp_get_media_line_number(sdp, level),
+ "Format is too large");
+ return false;
+ }
+
+ std::ostringstream osPayloadType;
+ // sipcc stores payload types in a funny way. When sipcc and the SDP it
+ // parsed differ on what payload type number should be used for a given
+ // codec, sipcc's value goes in the lower byte, and the SDP's value in
+ // the upper byte. When they do not differ, only the lower byte is used.
+ // We want what was in the SDP, verbatim.
+ osPayloadType << GET_DYN_PAYLOAD_TYPE_VALUE(ptype);
+ mFormats.push_back(osPayloadType.str());
+ }
+ }
+
+ return true;
+}
+
+bool SipccSdpMediaSection::ValidateSimulcast(sdp_t* sdp, uint16_t level,
+ InternalResults& results) const {
+ if (!GetAttributeList().HasAttribute(SdpAttribute::kSimulcastAttribute)) {
+ return true;
+ }
+
+ const SdpSimulcastAttribute& simulcast(GetAttributeList().GetSimulcast());
+ if (!ValidateSimulcastVersions(sdp, level, simulcast.sendVersions, sdp::kSend,
+ results)) {
+ return false;
+ }
+ if (!ValidateSimulcastVersions(sdp, level, simulcast.recvVersions, sdp::kRecv,
+ results)) {
+ return false;
+ }
+ return true;
+}
+
+bool SipccSdpMediaSection::ValidateSimulcastVersions(
+ sdp_t* sdp, uint16_t level, const SdpSimulcastAttribute::Versions& versions,
+ sdp::Direction direction, InternalResults& results) const {
+ for (const SdpSimulcastAttribute::Version& version : versions) {
+ for (const SdpSimulcastAttribute::Encoding& encoding : version.choices) {
+ const SdpRidAttributeList::Rid* ridAttr = FindRid(encoding.rid);
+ if (!ridAttr || (ridAttr->direction != direction)) {
+ std::ostringstream os;
+ os << "No rid attribute for \'" << encoding.rid << "\'";
+ results.AddParseError(sdp_get_media_line_number(sdp, level), os.str());
+ results.AddParseError(sdp_get_media_line_number(sdp, level), os.str());
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+bool SipccSdpMediaSection::LoadConnection(sdp_t* sdp, uint16_t level,
+ InternalResults& results) {
+ if (!sdp_connection_valid(sdp, level)) {
+ level = SDP_SESSION_LEVEL;
+ if (!sdp_connection_valid(sdp, level)) {
+ results.AddParseError(sdp_get_media_line_number(sdp, level),
+ "Missing c= line");
+ return false;
+ }
+ }
+
+ sdp_nettype_e type = sdp_get_conn_nettype(sdp, level);
+ if (type != SDP_NT_INTERNET) {
+ results.AddParseError(sdp_get_media_line_number(sdp, level),
+ "Unsupported network type");
+ return false;
+ }
+
+ sdp::AddrType addrType;
+ switch (sdp_get_conn_addrtype(sdp, level)) {
+ case SDP_AT_IP4:
+ addrType = sdp::kIPv4;
+ break;
+ case SDP_AT_IP6:
+ addrType = sdp::kIPv6;
+ break;
+ default:
+ results.AddParseError(sdp_get_media_line_number(sdp, level),
+ "Unsupported address type");
+ return false;
+ }
+
+ std::string address = sdp_get_conn_address(sdp, level);
+ int16_t ttl = static_cast<uint16_t>(sdp_get_mcast_ttl(sdp, level));
+ if (ttl < 0) {
+ ttl = 0;
+ }
+ int32_t numAddr =
+ static_cast<uint32_t>(sdp_get_mcast_num_of_addresses(sdp, level));
+ if (numAddr < 0) {
+ numAddr = 0;
+ }
+ mConnection = MakeUnique<SdpConnection>(addrType, address, ttl, numAddr);
+ return true;
+}
+
+void SipccSdpMediaSection::AddCodec(const std::string& pt,
+ const std::string& name, uint32_t clockrate,
+ uint16_t channels) {
+ mFormats.push_back(pt);
+
+ SdpRtpmapAttributeList* rtpmap = new SdpRtpmapAttributeList();
+ if (mAttributeList.HasAttribute(SdpAttribute::kRtpmapAttribute)) {
+ const SdpRtpmapAttributeList& old = mAttributeList.GetRtpmap();
+ for (auto it = old.mRtpmaps.begin(); it != old.mRtpmaps.end(); ++it) {
+ rtpmap->mRtpmaps.push_back(*it);
+ }
+ }
+ SdpRtpmapAttributeList::CodecType codec = SdpRtpmapAttributeList::kOtherCodec;
+ if (name == "opus") {
+ codec = SdpRtpmapAttributeList::kOpus;
+ } else if (name == "G722") {
+ codec = SdpRtpmapAttributeList::kG722;
+ } else if (name == "PCMU") {
+ codec = SdpRtpmapAttributeList::kPCMU;
+ } else if (name == "PCMA") {
+ codec = SdpRtpmapAttributeList::kPCMA;
+ } else if (name == "VP8") {
+ codec = SdpRtpmapAttributeList::kVP8;
+ } else if (name == "VP9") {
+ codec = SdpRtpmapAttributeList::kVP9;
+ } else if (name == "H264") {
+ codec = SdpRtpmapAttributeList::kH264;
+ }
+
+ rtpmap->PushEntry(pt, codec, name, clockrate, channels);
+ mAttributeList.SetAttribute(rtpmap);
+}
+
+void SipccSdpMediaSection::ClearCodecs() {
+ mFormats.clear();
+ mAttributeList.RemoveAttribute(SdpAttribute::kRtpmapAttribute);
+ mAttributeList.RemoveAttribute(SdpAttribute::kFmtpAttribute);
+ mAttributeList.RemoveAttribute(SdpAttribute::kSctpmapAttribute);
+ mAttributeList.RemoveAttribute(SdpAttribute::kRtcpFbAttribute);
+}
+
+void SipccSdpMediaSection::AddDataChannel(const std::string& name,
+ uint16_t port, uint16_t streams,
+ uint32_t message_size) {
+ // Only one allowed, for now. This may change as the specs (and deployments)
+ // evolve.
+ mFormats.clear();
+ if ((mProtocol == kUdpDtlsSctp) || (mProtocol == kTcpDtlsSctp)) {
+ // new data channel format according to draft 21
+ mFormats.push_back(name);
+ mAttributeList.SetAttribute(
+ new SdpNumberAttribute(SdpAttribute::kSctpPortAttribute, port));
+ if (message_size) {
+ mAttributeList.SetAttribute(new SdpNumberAttribute(
+ SdpAttribute::kMaxMessageSizeAttribute, message_size));
+ }
+ } else {
+ // old data channels format according to draft 05
+ std::string port_str = std::to_string(port);
+ mFormats.push_back(port_str);
+ SdpSctpmapAttributeList* sctpmap = new SdpSctpmapAttributeList();
+ sctpmap->PushEntry(port_str, name, streams);
+ mAttributeList.SetAttribute(sctpmap);
+ if (message_size) {
+ // This is a workaround to allow detecting Firefox's w/o EOR support
+ mAttributeList.SetAttribute(new SdpNumberAttribute(
+ SdpAttribute::kMaxMessageSizeAttribute, message_size));
+ }
+ }
+}
+
+void SipccSdpMediaSection::Serialize(std::ostream& os) const {
+ os << "m=" << mMediaType << " " << mPort;
+ if (mPortCount) {
+ os << "/" << mPortCount;
+ }
+ os << " " << mProtocol;
+ for (auto i = mFormats.begin(); i != mFormats.end(); ++i) {
+ os << " " << (*i);
+ }
+ os << CRLF;
+
+ // We dont do i=
+
+ if (mConnection) {
+ os << *mConnection;
+ }
+
+ mBandwidths.Serialize(os);
+
+ // We dont do k= because they're evil
+
+ os << mAttributeList;
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/sdp/SipccSdpMediaSection.h b/dom/media/webrtc/sdp/SipccSdpMediaSection.h
new file mode 100644
index 0000000000..c9abb4d33c
--- /dev/null
+++ b/dom/media/webrtc/sdp/SipccSdpMediaSection.h
@@ -0,0 +1,101 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _SIPCCSDPMEDIASECTION_H_
+#define _SIPCCSDPMEDIASECTION_H_
+
+#include "mozilla/Attributes.h"
+#include "mozilla/UniquePtr.h"
+#include "sdp/SdpMediaSection.h"
+#include "sdp/SipccSdpAttributeList.h"
+
+#include <map>
+
+extern "C" {
+#include "sipcc_sdp.h"
+}
+
+namespace mozilla {
+
+class SipccSdp;
+class SdpParser;
+
+using InternalResults = SdpParser::InternalResults;
+
+class SipccSdpBandwidths final : public std::map<std::string, uint32_t> {
+ public:
+ bool Load(sdp_t* sdp, uint16_t level, InternalResults& results);
+ void Serialize(std::ostream& os) const;
+};
+
+class SipccSdpMediaSection final : public SdpMediaSection {
+ friend class SipccSdp;
+
+ public:
+ ~SipccSdpMediaSection() {}
+
+ virtual MediaType GetMediaType() const override { return mMediaType; }
+
+ virtual unsigned int GetPort() const override;
+ virtual void SetPort(unsigned int port) override;
+ virtual unsigned int GetPortCount() const override;
+ virtual Protocol GetProtocol() const override;
+ virtual const SdpConnection& GetConnection() const override;
+ virtual SdpConnection& GetConnection() override;
+ virtual uint32_t GetBandwidth(const std::string& type) const override;
+ virtual const std::vector<std::string>& GetFormats() const override;
+
+ virtual const SdpAttributeList& GetAttributeList() const override;
+ virtual SdpAttributeList& GetAttributeList() override;
+ virtual SdpDirectionAttribute GetDirectionAttribute() const override;
+
+ virtual void AddCodec(const std::string& pt, const std::string& name,
+ uint32_t clockrate, uint16_t channels) override;
+ virtual void ClearCodecs() override;
+
+ virtual void AddDataChannel(const std::string& name, uint16_t port,
+ uint16_t streams, uint32_t message_size) override;
+
+ virtual void Serialize(std::ostream&) const override;
+
+ private:
+ SipccSdpMediaSection(size_t level, const SipccSdpAttributeList* sessionLevel)
+ : SdpMediaSection(level),
+ mMediaType(static_cast<MediaType>(0)),
+ mPort(0),
+ mPortCount(0),
+ mProtocol(static_cast<Protocol>(0)),
+ mAttributeList(sessionLevel) {}
+
+ SipccSdpMediaSection(const SipccSdpMediaSection& aOrig,
+ const SipccSdpAttributeList* sessionLevel);
+
+ bool Load(sdp_t* sdp, uint16_t level, InternalResults& results);
+ bool LoadConnection(sdp_t* sdp, uint16_t level, InternalResults& results);
+ bool LoadProtocol(sdp_t* sdp, uint16_t level, InternalResults& results);
+ bool LoadFormats(sdp_t* sdp, uint16_t level, InternalResults& results);
+ bool ValidateSimulcast(sdp_t* sdp, uint16_t level,
+ InternalResults& results) const;
+ bool ValidateSimulcastVersions(
+ sdp_t* sdp, uint16_t level,
+ const SdpSimulcastAttribute::Versions& versions, sdp::Direction direction,
+ InternalResults& results) const;
+
+ // the following values are cached on first get
+ MediaType mMediaType;
+ uint16_t mPort;
+ uint16_t mPortCount;
+ Protocol mProtocol;
+ std::vector<std::string> mFormats;
+
+ UniquePtr<SdpConnection> mConnection;
+ SipccSdpBandwidths mBandwidths;
+
+ SipccSdpAttributeList mAttributeList;
+};
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/sdp/SipccSdpParser.cpp b/dom/media/webrtc/sdp/SipccSdpParser.cpp
new file mode 100644
index 0000000000..901efd041f
--- /dev/null
+++ b/dom/media/webrtc/sdp/SipccSdpParser.cpp
@@ -0,0 +1,88 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "sdp/SipccSdpParser.h"
+#include "sdp/SipccSdp.h"
+
+#include <utility>
+extern "C" {
+#include "sipcc_sdp.h"
+}
+
+namespace mozilla {
+
+extern "C" {
+
+void sipcc_sdp_parser_results_handler(void* context, uint32_t line,
+ const char* message) {
+ auto* results = static_cast<UniquePtr<InternalResults>*>(context);
+ std::string err(message);
+ (*results)->AddParseError(line, err);
+}
+
+} // extern "C"
+
+const std::string& SipccSdpParser::ParserName() {
+ static const std::string SIPCC_NAME = "SIPCC";
+ return SIPCC_NAME;
+}
+
+UniquePtr<SdpParser::Results> SipccSdpParser::Parse(const std::string& aText) {
+ UniquePtr<InternalResults> results(new InternalResults(Name()));
+ sdp_conf_options_t* sipcc_config = sdp_init_config();
+ if (!sipcc_config) {
+ return UniquePtr<SdpParser::Results>();
+ }
+
+ sdp_nettype_supported(sipcc_config, SDP_NT_INTERNET, true);
+ sdp_addrtype_supported(sipcc_config, SDP_AT_IP4, true);
+ sdp_addrtype_supported(sipcc_config, SDP_AT_IP6, true);
+ sdp_transport_supported(sipcc_config, SDP_TRANSPORT_RTPAVP, true);
+ sdp_transport_supported(sipcc_config, SDP_TRANSPORT_RTPAVPF, true);
+ sdp_transport_supported(sipcc_config, SDP_TRANSPORT_RTPSAVP, true);
+ sdp_transport_supported(sipcc_config, SDP_TRANSPORT_RTPSAVPF, true);
+ sdp_transport_supported(sipcc_config, SDP_TRANSPORT_UDPTLSRTPSAVP, true);
+ sdp_transport_supported(sipcc_config, SDP_TRANSPORT_UDPTLSRTPSAVPF, true);
+ sdp_transport_supported(sipcc_config, SDP_TRANSPORT_TCPDTLSRTPSAVP, true);
+ sdp_transport_supported(sipcc_config, SDP_TRANSPORT_TCPDTLSRTPSAVPF, true);
+ sdp_transport_supported(sipcc_config, SDP_TRANSPORT_DTLSSCTP, true);
+ sdp_transport_supported(sipcc_config, SDP_TRANSPORT_UDPDTLSSCTP, true);
+ sdp_transport_supported(sipcc_config, SDP_TRANSPORT_TCPDTLSSCTP, true);
+ sdp_require_session_name(sipcc_config, false);
+
+ sdp_config_set_error_handler(sipcc_config, &sipcc_sdp_parser_results_handler,
+ &results);
+
+ // Takes ownership of |sipcc_config| iff it succeeds
+ sdp_t* sdp = sdp_init_description(sipcc_config);
+ if (!sdp) {
+ sdp_free_config(sipcc_config);
+ return results;
+ }
+
+ const char* rawString = aText.c_str();
+ sdp_result_e sdpres = sdp_parse(sdp, rawString, aText.length());
+ if (sdpres != SDP_SUCCESS) {
+ sdp_free_description(sdp);
+ return results;
+ }
+
+ UniquePtr<SipccSdp> sipccSdp(new SipccSdp);
+
+ bool success = sipccSdp->Load(sdp, *results);
+ sdp_free_description(sdp);
+ if (success) {
+ results->SetSdp(UniquePtr<mozilla::Sdp>(std::move(sipccSdp)));
+ }
+
+ return results;
+}
+
+bool SipccSdpParser::IsNamed(const std::string& aName) {
+ return aName == ParserName();
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/sdp/SipccSdpParser.h b/dom/media/webrtc/sdp/SipccSdpParser.h
new file mode 100644
index 0000000000..dc9c1ea02d
--- /dev/null
+++ b/dom/media/webrtc/sdp/SipccSdpParser.h
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _SIPCCSDPPARSER_H_
+#define _SIPCCSDPPARSER_H_
+
+#include <string>
+
+#include "mozilla/UniquePtr.h"
+
+#include "sdp/Sdp.h"
+#include "sdp/SdpParser.h"
+
+namespace mozilla {
+
+class SipccSdpParser final : public SdpParser {
+ static const std::string& ParserName();
+
+ public:
+ SipccSdpParser() = default;
+ virtual ~SipccSdpParser() = default;
+
+ const std::string& Name() const override { return ParserName(); }
+
+ UniquePtr<SdpParser::Results> Parse(const std::string& aText) override;
+
+ static bool IsNamed(const std::string& aName);
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/sdp/moz.build b/dom/media/webrtc/sdp/moz.build
new file mode 100644
index 0000000000..675009acd0
--- /dev/null
+++ b/dom/media/webrtc/sdp/moz.build
@@ -0,0 +1,48 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+if CONFIG["OS_TARGET"] == "WINNT":
+ DEFINES["SIP_OS_WINDOWS"] = True
+elif CONFIG["OS_TARGET"] == "Darwin":
+ DEFINES["SIP_OS_OSX"] = True
+else:
+ DEFINES["SIP_OS_LINUX"] = True
+
+# Add libFuzzer configuration directives
+include("/tools/fuzzing/libfuzzer-config.mozbuild")
+
+LOCAL_INCLUDES += [
+ "/dom/media/webrtc",
+ "/media/webrtc",
+ "/third_party/sipcc",
+]
+
+UNIFIED_SOURCES += [
+ "HybridSdpParser.cpp",
+ "ParsingResultComparer.cpp",
+ "SdpAttribute.cpp",
+ "SdpHelper.cpp",
+ "SdpLog.cpp",
+ "SdpMediaSection.cpp",
+ "SdpPref.cpp",
+ "SdpTelemetry.cpp",
+ "SipccSdp.cpp",
+ "SipccSdpAttributeList.cpp",
+ "SipccSdpMediaSection.cpp",
+ "SipccSdpParser.cpp",
+]
+
+SOURCES += [
+ # Building these as part of the unified build leads to multiply defined
+ # symbols on windows.
+ "RsdparsaSdp.cpp",
+ "RsdparsaSdpAttributeList.cpp",
+ "RsdparsaSdpGlue.cpp",
+ "RsdparsaSdpMediaSection.cpp",
+ "RsdparsaSdpParser.cpp",
+]
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/webrtc/sdp/rsdparsa_capi/Cargo.toml b/dom/media/webrtc/sdp/rsdparsa_capi/Cargo.toml
new file mode 100644
index 0000000000..a0aeff086f
--- /dev/null
+++ b/dom/media/webrtc/sdp/rsdparsa_capi/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "rsdparsa_capi"
+version = "0.1.0"
+authors = ["Paul Ellenbogen <pe5@cs.princeton.edu>",
+ "Nils Ohlmeier <github@ohlmeier.org>"]
+license = "MPL-2.0"
+
+[dependencies]
+libc = "^0.2.0"
+log = "0.4"
+rsdparsa = {package = "webrtc-sdp", version = "0.3.10"}
+nserror = { path = "../../../../../xpcom/rust/nserror" }
diff --git a/dom/media/webrtc/sdp/rsdparsa_capi/src/attribute.rs b/dom/media/webrtc/sdp/rsdparsa_capi/src/attribute.rs
new file mode 100644
index 0000000000..82483f151f
--- /dev/null
+++ b/dom/media/webrtc/sdp/rsdparsa_capi/src/attribute.rs
@@ -0,0 +1,1472 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use libc::{c_float, size_t};
+use std::ptr;
+use std::slice;
+
+use nserror::{nsresult, NS_ERROR_INVALID_ARG, NS_OK};
+use rsdparsa::attribute_type::*;
+use rsdparsa::SdpSession;
+
+use network::{RustAddress, RustExplicitlyTypedAddress};
+use types::StringView;
+
+#[no_mangle]
+pub unsafe extern "C" fn num_attributes(session: *const SdpSession) -> u32 {
+ (*session).attribute.len() as u32
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn get_attribute_ptr(
+ session: *const SdpSession,
+ index: u32,
+ ret: *mut *const SdpAttribute,
+) -> nsresult {
+ match (*session).attribute.get(index as usize) {
+ Some(attribute) => {
+ *ret = attribute as *const SdpAttribute;
+ NS_OK
+ }
+ None => NS_ERROR_INVALID_ARG,
+ }
+}
+
+fn count_attribute(attributes: &[SdpAttribute], search: SdpAttributeType) -> usize {
+ let mut count = 0;
+ for attribute in (*attributes).iter() {
+ if SdpAttributeType::from(attribute) == search {
+ count += 1;
+ }
+ }
+ count
+}
+
+fn argsearch(attributes: &[SdpAttribute], attribute_type: SdpAttributeType) -> Option<usize> {
+ for (i, attribute) in (*attributes).iter().enumerate() {
+ if SdpAttributeType::from(attribute) == attribute_type {
+ return Some(i);
+ }
+ }
+ None
+}
+
+pub unsafe fn has_attribute(
+ attributes: *const Vec<SdpAttribute>,
+ attribute_type: SdpAttributeType,
+) -> bool {
+ argsearch((*attributes).as_slice(), attribute_type).is_some()
+}
+
+fn get_attribute(
+ attributes: &[SdpAttribute],
+ attribute_type: SdpAttributeType,
+) -> Option<&SdpAttribute> {
+ argsearch(attributes, attribute_type).map(|i| &attributes[i])
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub enum RustSdpAttributeDtlsMessageType {
+ Client,
+ Server,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct RustSdpAttributeDtlsMessage {
+ pub role: u8,
+ pub value: StringView,
+}
+
+impl<'a> From<&'a SdpAttributeDtlsMessage> for RustSdpAttributeDtlsMessage {
+ fn from(other: &SdpAttributeDtlsMessage) -> Self {
+ match other {
+ &SdpAttributeDtlsMessage::Client(ref x) => RustSdpAttributeDtlsMessage {
+ role: RustSdpAttributeDtlsMessageType::Client as u8,
+ value: StringView::from(x.as_str()),
+ },
+ &SdpAttributeDtlsMessage::Server(ref x) => RustSdpAttributeDtlsMessage {
+ role: RustSdpAttributeDtlsMessageType::Server as u8,
+ value: StringView::from(x.as_str()),
+ },
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_dtls_message(
+ attributes: *const Vec<SdpAttribute>,
+ ret: *mut RustSdpAttributeDtlsMessage,
+) -> nsresult {
+ let attr = get_attribute((*attributes).as_slice(), SdpAttributeType::DtlsMessage);
+ if let Some(&SdpAttribute::DtlsMessage(ref dtls_message)) = attr {
+ *ret = RustSdpAttributeDtlsMessage::from(dtls_message);
+ return NS_OK;
+ }
+ NS_ERROR_INVALID_ARG
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_iceufrag(
+ attributes: *const Vec<SdpAttribute>,
+ ret: *mut StringView,
+) -> nsresult {
+ let attr = get_attribute((*attributes).as_slice(), SdpAttributeType::IceUfrag);
+ if let Some(&SdpAttribute::IceUfrag(ref string)) = attr {
+ *ret = StringView::from(string.as_str());
+ return NS_OK;
+ }
+ NS_ERROR_INVALID_ARG
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_icepwd(
+ attributes: *const Vec<SdpAttribute>,
+ ret: *mut StringView,
+) -> nsresult {
+ let attr = get_attribute((*attributes).as_slice(), SdpAttributeType::IcePwd);
+ if let Some(&SdpAttribute::IcePwd(ref string)) = attr {
+ *ret = StringView::from(string.as_str());
+ return NS_OK;
+ }
+ NS_ERROR_INVALID_ARG
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_identity(
+ attributes: *const Vec<SdpAttribute>,
+ ret: *mut StringView,
+) -> nsresult {
+ let attr = get_attribute((*attributes).as_slice(), SdpAttributeType::Identity);
+ if let Some(&SdpAttribute::Identity(ref string)) = attr {
+ *ret = StringView::from(string.as_str());
+ return NS_OK;
+ }
+ NS_ERROR_INVALID_ARG
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_iceoptions(
+ attributes: *const Vec<SdpAttribute>,
+ ret: *mut *const Vec<String>,
+) -> nsresult {
+ let attr = get_attribute((*attributes).as_slice(), SdpAttributeType::IceOptions);
+ if let Some(&SdpAttribute::IceOptions(ref options)) = attr {
+ *ret = options;
+ return NS_OK;
+ }
+ NS_ERROR_INVALID_ARG
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_maxptime(
+ attributes: *const Vec<SdpAttribute>,
+ ret: *mut u64,
+) -> nsresult {
+ let attr = get_attribute((*attributes).as_slice(), SdpAttributeType::MaxPtime);
+ if let Some(&SdpAttribute::MaxPtime(ref max_ptime)) = attr {
+ *ret = *max_ptime;
+ return NS_OK;
+ }
+ NS_ERROR_INVALID_ARG
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct RustSdpAttributeFingerprint {
+ hash_algorithm: u16,
+ fingerprint: *const Vec<u8>,
+}
+
+impl<'a> From<&'a SdpAttributeFingerprint> for RustSdpAttributeFingerprint {
+ fn from(other: &SdpAttributeFingerprint) -> Self {
+ RustSdpAttributeFingerprint {
+ hash_algorithm: other.hash_algorithm as u16,
+ fingerprint: &other.fingerprint,
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_fingerprint_count(attributes: *const Vec<SdpAttribute>) -> size_t {
+ count_attribute((*attributes).as_slice(), SdpAttributeType::Fingerprint)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_fingerprints(
+ attributes: *const Vec<SdpAttribute>,
+ ret_size: size_t,
+ ret_fingerprints: *mut RustSdpAttributeFingerprint,
+) {
+ let attrs: Vec<_> = (*attributes)
+ .iter()
+ .filter_map(|x| {
+ if let SdpAttribute::Fingerprint(ref data) = *x {
+ Some(RustSdpAttributeFingerprint::from(data))
+ } else {
+ None
+ }
+ })
+ .collect();
+ let fingerprints = slice::from_raw_parts_mut(ret_fingerprints, ret_size);
+ fingerprints.copy_from_slice(attrs.as_slice());
+}
+
+#[repr(C)]
+#[derive(Clone)]
+pub enum RustSdpAttributeSetup {
+ Active,
+ Actpass,
+ Holdconn,
+ Passive,
+}
+
+impl<'a> From<&'a SdpAttributeSetup> for RustSdpAttributeSetup {
+ fn from(other: &SdpAttributeSetup) -> Self {
+ match *other {
+ SdpAttributeSetup::Active => RustSdpAttributeSetup::Active,
+ SdpAttributeSetup::Actpass => RustSdpAttributeSetup::Actpass,
+ SdpAttributeSetup::Holdconn => RustSdpAttributeSetup::Holdconn,
+ SdpAttributeSetup::Passive => RustSdpAttributeSetup::Passive,
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_setup(
+ attributes: *const Vec<SdpAttribute>,
+ ret: *mut RustSdpAttributeSetup,
+) -> nsresult {
+ let attr = get_attribute((*attributes).as_slice(), SdpAttributeType::Setup);
+ if let Some(&SdpAttribute::Setup(ref setup)) = attr {
+ *ret = RustSdpAttributeSetup::from(setup);
+ return NS_OK;
+ }
+ NS_ERROR_INVALID_ARG
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct RustSdpAttributeSsrc {
+ pub id: u32,
+ pub attribute: StringView,
+ pub value: StringView,
+}
+
+impl<'a> From<&'a SdpAttributeSsrc> for RustSdpAttributeSsrc {
+ fn from(other: &SdpAttributeSsrc) -> Self {
+ RustSdpAttributeSsrc {
+ id: other.id,
+ attribute: StringView::from(&other.attribute),
+ value: StringView::from(&other.value),
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_ssrc_count(attributes: *const Vec<SdpAttribute>) -> size_t {
+ count_attribute((*attributes).as_slice(), SdpAttributeType::Ssrc)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_ssrcs(
+ attributes: *const Vec<SdpAttribute>,
+ ret_size: size_t,
+ ret_ssrcs: *mut RustSdpAttributeSsrc,
+) {
+ let attrs: Vec<_> = (*attributes)
+ .iter()
+ .filter_map(|x| {
+ if let SdpAttribute::Ssrc(ref data) = *x {
+ Some(RustSdpAttributeSsrc::from(data))
+ } else {
+ None
+ }
+ })
+ .collect();
+ let ssrcs = slice::from_raw_parts_mut(ret_ssrcs, ret_size);
+ ssrcs.copy_from_slice(attrs.as_slice());
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub enum RustSdpSsrcGroupSemantic {
+ Duplication,
+ FlowIdentification,
+ ForwardErrorCorrection,
+ ForwardErrorCorrectionFr,
+ SIM,
+}
+
+impl<'a> From<&'a SdpSsrcGroupSemantic> for RustSdpSsrcGroupSemantic {
+ fn from(other: &SdpSsrcGroupSemantic) -> Self {
+ match *other {
+ SdpSsrcGroupSemantic::Duplication => RustSdpSsrcGroupSemantic::Duplication,
+ SdpSsrcGroupSemantic::FlowIdentification => {
+ RustSdpSsrcGroupSemantic::FlowIdentification
+ }
+ SdpSsrcGroupSemantic::ForwardErrorCorrection => {
+ RustSdpSsrcGroupSemantic::ForwardErrorCorrection
+ }
+ SdpSsrcGroupSemantic::ForwardErrorCorrectionFr => {
+ RustSdpSsrcGroupSemantic::ForwardErrorCorrectionFr
+ }
+ SdpSsrcGroupSemantic::Sim => RustSdpSsrcGroupSemantic::SIM,
+ }
+ }
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct RustSdpSsrcGroup {
+ pub semantic: RustSdpSsrcGroupSemantic,
+ pub ssrcs: *const Vec<SdpAttributeSsrc>,
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_ssrc_group_count(attributes: *const Vec<SdpAttribute>) -> size_t {
+ count_attribute((*attributes).as_slice(), SdpAttributeType::SsrcGroup)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_ssrc_groups(
+ attributes: *const Vec<SdpAttribute>,
+ ret_size: size_t,
+ ret_ssrc_groups: *mut RustSdpSsrcGroup,
+) {
+ let attrs: Vec<_> = (*attributes)
+ .iter()
+ .filter_map(|x| {
+ if let SdpAttribute::SsrcGroup(ref semantic, ref ssrcs) = *x {
+ Some(RustSdpSsrcGroup {
+ semantic: RustSdpSsrcGroupSemantic::from(semantic),
+ ssrcs: ssrcs,
+ })
+ } else {
+ None
+ }
+ })
+ .collect();
+ let ssrc_groups = slice::from_raw_parts_mut(ret_ssrc_groups, ret_size);
+ ssrc_groups.copy_from_slice(attrs.as_slice());
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct RustSdpAttributeRtpmap {
+ pub payload_type: u8,
+ pub codec_name: StringView,
+ pub frequency: u32,
+ pub channels: u32,
+}
+
+impl<'a> From<&'a SdpAttributeRtpmap> for RustSdpAttributeRtpmap {
+ fn from(other: &SdpAttributeRtpmap) -> Self {
+ RustSdpAttributeRtpmap {
+ payload_type: other.payload_type as u8,
+ codec_name: StringView::from(other.codec_name.as_str()),
+ frequency: other.frequency as u32,
+ channels: other.channels.unwrap_or(0),
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_rtpmap_count(attributes: *const Vec<SdpAttribute>) -> size_t {
+ count_attribute((*attributes).as_slice(), SdpAttributeType::Rtpmap)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_rtpmaps(
+ attributes: *const Vec<SdpAttribute>,
+ ret_size: size_t,
+ ret_rtpmaps: *mut RustSdpAttributeRtpmap,
+) {
+ let attrs: Vec<_> = (*attributes)
+ .iter()
+ .filter_map(|x| {
+ if let SdpAttribute::Rtpmap(ref data) = *x {
+ Some(RustSdpAttributeRtpmap::from(data))
+ } else {
+ None
+ }
+ })
+ .collect();
+ let rtpmaps = slice::from_raw_parts_mut(ret_rtpmaps, ret_size);
+ rtpmaps.copy_from_slice(attrs.as_slice());
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct RustRtxFmtpParameters {
+ pub apt: u8,
+ pub has_rtx_time: bool,
+ pub rtx_time: u32,
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct RustSdpAttributeFmtpParameters {
+ // H264
+ pub packetization_mode: u32,
+ pub level_asymmetry_allowed: bool,
+ pub profile_level_id: u32,
+ pub max_fs: u32,
+ pub max_cpb: u32,
+ pub max_dpb: u32,
+ pub max_br: u32,
+ pub max_mbps: u32,
+
+ // VP8 and VP9
+ // max_fs, already defined in H264
+ pub max_fr: u32,
+
+ // Opus
+ pub maxplaybackrate: u32,
+ pub maxaveragebitrate: u32,
+ pub usedtx: bool,
+ pub stereo: bool,
+ pub useinbandfec: bool,
+ pub cbr: bool,
+ pub ptime: u32,
+ pub minptime: u32,
+ pub maxptime: u32,
+
+ // telephone-event
+ pub dtmf_tones: StringView,
+
+ // RTX
+ pub rtx: RustRtxFmtpParameters,
+
+ // Red
+ pub encodings: *const Vec<u8>,
+
+ // Unknown
+ pub unknown_tokens: *const Vec<String>,
+}
+
+impl<'a> From<&'a SdpAttributeFmtpParameters> for RustSdpAttributeFmtpParameters {
+ fn from(other: &SdpAttributeFmtpParameters) -> Self {
+ let rtx = if let Some(rtx) = other.rtx {
+ RustRtxFmtpParameters {
+ apt: rtx.apt,
+ has_rtx_time: rtx.rtx_time.is_some(),
+ rtx_time: rtx.rtx_time.unwrap_or(0),
+ }
+ } else {
+ RustRtxFmtpParameters {
+ apt: 0,
+ has_rtx_time: false,
+ rtx_time: 0,
+ }
+ };
+
+ RustSdpAttributeFmtpParameters {
+ packetization_mode: other.packetization_mode,
+ level_asymmetry_allowed: other.level_asymmetry_allowed,
+ profile_level_id: other.profile_level_id,
+ max_fs: other.max_fs,
+ max_cpb: other.max_cpb,
+ max_dpb: other.max_dpb,
+ max_br: other.max_br,
+ max_mbps: other.max_mbps,
+ usedtx: other.usedtx,
+ stereo: other.stereo,
+ useinbandfec: other.useinbandfec,
+ cbr: other.cbr,
+ max_fr: other.max_fr,
+ maxplaybackrate: other.maxplaybackrate,
+ maxaveragebitrate: other.maxaveragebitrate,
+ ptime: other.ptime,
+ minptime: other.minptime,
+ maxptime: other.maxptime,
+ dtmf_tones: StringView::from(other.dtmf_tones.as_str()),
+ rtx,
+ encodings: &other.encodings,
+ unknown_tokens: &other.unknown_tokens,
+ }
+ }
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct RustSdpAttributeFmtp {
+ pub payload_type: u8,
+ pub codec_name: StringView,
+ pub parameters: RustSdpAttributeFmtpParameters,
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_fmtp_count(attributes: *const Vec<SdpAttribute>) -> size_t {
+ count_attribute((*attributes).as_slice(), SdpAttributeType::Fmtp)
+}
+
+fn find_payload_type(attributes: &[SdpAttribute], payload_type: u8) -> Option<&SdpAttributeRtpmap> {
+ attributes
+ .iter()
+ .filter_map(|x| {
+ if let SdpAttribute::Rtpmap(ref data) = *x {
+ if data.payload_type == payload_type {
+ Some(data)
+ } else {
+ None
+ }
+ } else {
+ None
+ }
+ })
+ .next()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_fmtp(
+ attributes: *const Vec<SdpAttribute>,
+ ret_size: size_t,
+ ret_fmtp: *mut RustSdpAttributeFmtp,
+) -> size_t {
+ let fmtps = (*attributes).iter().filter_map(|x| {
+ if let SdpAttribute::Fmtp(ref data) = *x {
+ Some(data)
+ } else {
+ None
+ }
+ });
+ let mut rust_fmtps = Vec::new();
+ for fmtp in fmtps {
+ if let Some(rtpmap) = find_payload_type((*attributes).as_slice(), fmtp.payload_type) {
+ rust_fmtps.push(RustSdpAttributeFmtp {
+ payload_type: fmtp.payload_type as u8,
+ codec_name: StringView::from(rtpmap.codec_name.as_str()),
+ parameters: RustSdpAttributeFmtpParameters::from(&fmtp.parameters),
+ });
+ }
+ }
+ let fmtps = if ret_size <= rust_fmtps.len() {
+ slice::from_raw_parts_mut(ret_fmtp, ret_size)
+ } else {
+ slice::from_raw_parts_mut(ret_fmtp, rust_fmtps.len())
+ };
+ fmtps.copy_from_slice(rust_fmtps.as_slice());
+ fmtps.len()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_ptime(attributes: *const Vec<SdpAttribute>) -> i64 {
+ for attribute in (*attributes).iter() {
+ if let SdpAttribute::Ptime(time) = *attribute {
+ return time as i64;
+ }
+ }
+ -1
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_max_msg_size(attributes: *const Vec<SdpAttribute>) -> i64 {
+ for attribute in (*attributes).iter() {
+ if let SdpAttribute::MaxMessageSize(max_msg_size) = *attribute {
+ return max_msg_size as i64;
+ }
+ }
+ -1
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_sctp_port(attributes: *const Vec<SdpAttribute>) -> i64 {
+ for attribute in (*attributes).iter() {
+ if let SdpAttribute::SctpPort(port) = *attribute {
+ return port as i64;
+ }
+ }
+ -1
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct RustSdpAttributeFlags {
+ pub ice_lite: bool,
+ pub rtcp_mux: bool,
+ pub rtcp_rsize: bool,
+ pub bundle_only: bool,
+ pub end_of_candidates: bool,
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_attribute_flags(
+ attributes: *const Vec<SdpAttribute>,
+) -> RustSdpAttributeFlags {
+ let mut ret = RustSdpAttributeFlags {
+ ice_lite: false,
+ rtcp_mux: false,
+ rtcp_rsize: false,
+ bundle_only: false,
+ end_of_candidates: false,
+ };
+ for attribute in (*attributes).iter() {
+ if let SdpAttribute::IceLite = *attribute {
+ ret.ice_lite = true;
+ } else if let SdpAttribute::RtcpMux = *attribute {
+ ret.rtcp_mux = true;
+ } else if let SdpAttribute::RtcpRsize = *attribute {
+ ret.rtcp_rsize = true;
+ } else if let SdpAttribute::BundleOnly = *attribute {
+ ret.bundle_only = true;
+ } else if let SdpAttribute::EndOfCandidates = *attribute {
+ ret.end_of_candidates = true;
+ }
+ }
+ ret
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_mid(
+ attributes: *const Vec<SdpAttribute>,
+ ret: *mut StringView,
+) -> nsresult {
+ for attribute in (*attributes).iter() {
+ if let SdpAttribute::Mid(ref data) = *attribute {
+ *ret = StringView::from(data.as_str());
+ return NS_OK;
+ }
+ }
+ NS_ERROR_INVALID_ARG
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct RustSdpAttributeMsid {
+ id: StringView,
+ appdata: StringView,
+}
+
+impl<'a> From<&'a SdpAttributeMsid> for RustSdpAttributeMsid {
+ fn from(other: &SdpAttributeMsid) -> Self {
+ RustSdpAttributeMsid {
+ id: StringView::from(other.id.as_str()),
+ appdata: StringView::from(&other.appdata),
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_msid_count(attributes: *const Vec<SdpAttribute>) -> size_t {
+ count_attribute((*attributes).as_slice(), SdpAttributeType::Msid)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_msids(
+ attributes: *const Vec<SdpAttribute>,
+ ret_size: size_t,
+ ret_msids: *mut RustSdpAttributeMsid,
+) {
+ let attrs: Vec<_> = (*attributes)
+ .iter()
+ .filter_map(|x| {
+ if let SdpAttribute::Msid(ref data) = *x {
+ Some(RustSdpAttributeMsid::from(data))
+ } else {
+ None
+ }
+ })
+ .collect();
+ let msids = slice::from_raw_parts_mut(ret_msids, ret_size);
+ msids.copy_from_slice(attrs.as_slice());
+}
+
+// TODO: Finish msid attributes once parsing is changed upstream.
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct RustSdpAttributeMsidSemantic {
+ pub semantic: StringView,
+ pub msids: *const Vec<String>,
+}
+
+impl<'a> From<&'a SdpAttributeMsidSemantic> for RustSdpAttributeMsidSemantic {
+ fn from(other: &SdpAttributeMsidSemantic) -> Self {
+ RustSdpAttributeMsidSemantic {
+ semantic: StringView::from(other.semantic.as_str()),
+ msids: &other.msids,
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_msid_semantic_count(
+ attributes: *const Vec<SdpAttribute>,
+) -> size_t {
+ count_attribute((*attributes).as_slice(), SdpAttributeType::MsidSemantic)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_msid_semantics(
+ attributes: *const Vec<SdpAttribute>,
+ ret_size: size_t,
+ ret_msid_semantics: *mut RustSdpAttributeMsidSemantic,
+) {
+ let attrs: Vec<_> = (*attributes)
+ .iter()
+ .filter_map(|x| {
+ if let SdpAttribute::MsidSemantic(ref data) = *x {
+ Some(RustSdpAttributeMsidSemantic::from(data))
+ } else {
+ None
+ }
+ })
+ .collect();
+ let msid_semantics = slice::from_raw_parts_mut(ret_msid_semantics, ret_size);
+ msid_semantics.copy_from_slice(attrs.as_slice());
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub enum RustSdpAttributeGroupSemantic {
+ LipSynchronization,
+ FlowIdentification,
+ SingleReservationFlow,
+ AlternateNetworkAddressType,
+ ForwardErrorCorrection,
+ DecodingDependency,
+ Bundle,
+}
+
+impl<'a> From<&'a SdpAttributeGroupSemantic> for RustSdpAttributeGroupSemantic {
+ fn from(other: &SdpAttributeGroupSemantic) -> Self {
+ match *other {
+ SdpAttributeGroupSemantic::LipSynchronization => {
+ RustSdpAttributeGroupSemantic::LipSynchronization
+ }
+ SdpAttributeGroupSemantic::FlowIdentification => {
+ RustSdpAttributeGroupSemantic::FlowIdentification
+ }
+ SdpAttributeGroupSemantic::SingleReservationFlow => {
+ RustSdpAttributeGroupSemantic::SingleReservationFlow
+ }
+ SdpAttributeGroupSemantic::AlternateNetworkAddressType => {
+ RustSdpAttributeGroupSemantic::AlternateNetworkAddressType
+ }
+ SdpAttributeGroupSemantic::ForwardErrorCorrection => {
+ RustSdpAttributeGroupSemantic::ForwardErrorCorrection
+ }
+ SdpAttributeGroupSemantic::DecodingDependency => {
+ RustSdpAttributeGroupSemantic::DecodingDependency
+ }
+ SdpAttributeGroupSemantic::Bundle => RustSdpAttributeGroupSemantic::Bundle,
+ }
+ }
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct RustSdpAttributeGroup {
+ pub semantic: RustSdpAttributeGroupSemantic,
+ pub tags: *const Vec<String>,
+}
+
+impl<'a> From<&'a SdpAttributeGroup> for RustSdpAttributeGroup {
+ fn from(other: &SdpAttributeGroup) -> Self {
+ RustSdpAttributeGroup {
+ semantic: RustSdpAttributeGroupSemantic::from(&other.semantics),
+ tags: &other.tags,
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_group_count(attributes: *const Vec<SdpAttribute>) -> size_t {
+ count_attribute((*attributes).as_slice(), SdpAttributeType::Group)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_groups(
+ attributes: *const Vec<SdpAttribute>,
+ ret_size: size_t,
+ ret_groups: *mut RustSdpAttributeGroup,
+) {
+ let attrs: Vec<_> = (*attributes)
+ .iter()
+ .filter_map(|x| {
+ if let SdpAttribute::Group(ref data) = *x {
+ Some(RustSdpAttributeGroup::from(data))
+ } else {
+ None
+ }
+ })
+ .collect();
+ let groups = slice::from_raw_parts_mut(ret_groups, ret_size);
+ groups.copy_from_slice(attrs.as_slice());
+}
+
+#[repr(C)]
+pub struct RustSdpAttributeRtcp {
+ pub port: u32,
+ pub unicast_addr: RustExplicitlyTypedAddress,
+ pub has_address: bool,
+}
+
+impl<'a> From<&'a SdpAttributeRtcp> for RustSdpAttributeRtcp {
+ fn from(other: &SdpAttributeRtcp) -> Self {
+ match other.unicast_addr {
+ Some(ref address) => RustSdpAttributeRtcp {
+ port: other.port as u32,
+ unicast_addr: address.into(),
+ has_address: true,
+ },
+ None => RustSdpAttributeRtcp {
+ port: other.port as u32,
+ unicast_addr: RustExplicitlyTypedAddress::default(),
+ has_address: false,
+ },
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_rtcp(
+ attributes: *const Vec<SdpAttribute>,
+ ret: *mut RustSdpAttributeRtcp,
+) -> nsresult {
+ let attr = get_attribute((*attributes).as_slice(), SdpAttributeType::Rtcp);
+ if let Some(&SdpAttribute::Rtcp(ref data)) = attr {
+ *ret = RustSdpAttributeRtcp::from(data);
+ return NS_OK;
+ }
+ NS_ERROR_INVALID_ARG
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct RustSdpAttributeRtcpFb {
+ pub payload_type: u32,
+ pub feedback_type: u32,
+ pub parameter: StringView,
+ pub extra: StringView,
+}
+
+impl<'a> From<&'a SdpAttributeRtcpFb> for RustSdpAttributeRtcpFb {
+ fn from(other: &SdpAttributeRtcpFb) -> Self {
+ RustSdpAttributeRtcpFb {
+ payload_type: match other.payload_type {
+ SdpAttributePayloadType::Wildcard => u32::max_value(),
+ SdpAttributePayloadType::PayloadType(x) => x as u32,
+ },
+ feedback_type: other.feedback_type.clone() as u32,
+ parameter: StringView::from(other.parameter.as_str()),
+ extra: StringView::from(other.extra.as_str()),
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_rtcpfb_count(attributes: *const Vec<SdpAttribute>) -> size_t {
+ count_attribute((*attributes).as_slice(), SdpAttributeType::Rtcpfb)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_rtcpfbs(
+ attributes: *const Vec<SdpAttribute>,
+ ret_size: size_t,
+ ret_rtcpfbs: *mut RustSdpAttributeRtcpFb,
+) {
+ let attrs: Vec<_> = (*attributes)
+ .iter()
+ .filter_map(|x| {
+ if let SdpAttribute::Rtcpfb(ref data) = *x {
+ Some(RustSdpAttributeRtcpFb::from(data))
+ } else {
+ None
+ }
+ })
+ .collect();
+ let rtcpfbs = slice::from_raw_parts_mut(ret_rtcpfbs, ret_size);
+ rtcpfbs.clone_from_slice(attrs.as_slice());
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct RustSdpAttributeImageAttrXyRange {
+ // range
+ pub min: u32,
+ pub max: u32,
+ pub step: u32,
+
+ // discrete values
+ pub discrete_values: *const Vec<u32>,
+}
+
+impl<'a> From<&'a SdpAttributeImageAttrXyRange> for RustSdpAttributeImageAttrXyRange {
+ fn from(other: &SdpAttributeImageAttrXyRange) -> Self {
+ match other {
+ &SdpAttributeImageAttrXyRange::Range(min, max, step) => {
+ RustSdpAttributeImageAttrXyRange {
+ min,
+ max,
+ step: step.unwrap_or(1),
+ discrete_values: ptr::null(),
+ }
+ }
+ &SdpAttributeImageAttrXyRange::DiscreteValues(ref discrete_values) => {
+ RustSdpAttributeImageAttrXyRange {
+ min: 0,
+ max: 1,
+ step: 1,
+ discrete_values,
+ }
+ }
+ }
+ }
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct RustSdpAttributeImageAttrSRange {
+ // range
+ pub min: c_float,
+ pub max: c_float,
+
+ // discrete values
+ pub discrete_values: *const Vec<c_float>,
+}
+
+impl<'a> From<&'a SdpAttributeImageAttrSRange> for RustSdpAttributeImageAttrSRange {
+ fn from(other: &SdpAttributeImageAttrSRange) -> Self {
+ match other {
+ &SdpAttributeImageAttrSRange::Range(min, max) => RustSdpAttributeImageAttrSRange {
+ min,
+ max,
+ discrete_values: ptr::null(),
+ },
+ &SdpAttributeImageAttrSRange::DiscreteValues(ref discrete_values) => {
+ RustSdpAttributeImageAttrSRange {
+ min: 0.0,
+ max: 1.0,
+ discrete_values,
+ }
+ }
+ }
+ }
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct RustSdpAttributeImageAttrPRange {
+ pub min: c_float,
+ pub max: c_float,
+}
+
+impl<'a> From<&'a SdpAttributeImageAttrPRange> for RustSdpAttributeImageAttrPRange {
+ fn from(other: &SdpAttributeImageAttrPRange) -> Self {
+ RustSdpAttributeImageAttrPRange {
+ min: other.min,
+ max: other.max,
+ }
+ }
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct RustSdpAttributeImageAttrSet {
+ pub x: RustSdpAttributeImageAttrXyRange,
+ pub y: RustSdpAttributeImageAttrXyRange,
+
+ pub has_sar: bool,
+ pub sar: RustSdpAttributeImageAttrSRange,
+
+ pub has_par: bool,
+ pub par: RustSdpAttributeImageAttrPRange,
+
+ pub q: c_float,
+}
+
+impl<'a> From<&'a SdpAttributeImageAttrSet> for RustSdpAttributeImageAttrSet {
+ fn from(other: &SdpAttributeImageAttrSet) -> Self {
+ RustSdpAttributeImageAttrSet {
+ x: RustSdpAttributeImageAttrXyRange::from(&other.x),
+ y: RustSdpAttributeImageAttrXyRange::from(&other.y),
+
+ has_sar: other.sar.is_some(),
+ sar: match other.sar {
+ Some(ref x) => RustSdpAttributeImageAttrSRange::from(x),
+ // This is just any valid value accepted by rust,
+ // it might as well by uninitilized
+ None => RustSdpAttributeImageAttrSRange::from(
+ &SdpAttributeImageAttrSRange::DiscreteValues(vec![]),
+ ),
+ },
+
+ has_par: other.par.is_some(),
+ par: match other.par {
+ Some(ref x) => RustSdpAttributeImageAttrPRange::from(x),
+ // This is just any valid value accepted by rust,
+ // it might as well by uninitilized
+ None => RustSdpAttributeImageAttrPRange { min: 0.0, max: 1.0 },
+ },
+
+ q: other.q.unwrap_or(0.5),
+ }
+ }
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct RustSdpAttributeImageAttrSetList {
+ pub sets: *const Vec<SdpAttributeImageAttrSet>,
+}
+
+impl<'a> From<&'a SdpAttributeImageAttrSetList> for RustSdpAttributeImageAttrSetList {
+ fn from(other: &SdpAttributeImageAttrSetList) -> Self {
+ match other {
+ &SdpAttributeImageAttrSetList::Wildcard => {
+ RustSdpAttributeImageAttrSetList { sets: ptr::null() }
+ }
+ &SdpAttributeImageAttrSetList::Sets(ref sets) => {
+ RustSdpAttributeImageAttrSetList { sets: sets }
+ }
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_imageattr_get_set_count(
+ sets: *const Vec<SdpAttributeImageAttrSet>,
+) -> size_t {
+ (*sets).len()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_imageattr_get_sets(
+ sets: *const Vec<SdpAttributeImageAttrSet>,
+ ret_size: size_t,
+ ret: *mut RustSdpAttributeImageAttrSet,
+) {
+ let rust_sets: Vec<_> = (*sets)
+ .iter()
+ .map(RustSdpAttributeImageAttrSet::from)
+ .collect();
+ let sets = slice::from_raw_parts_mut(ret, ret_size);
+ sets.clone_from_slice(rust_sets.as_slice());
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct RustSdpAttributeImageAttr {
+ pub pt: u32,
+ pub send: RustSdpAttributeImageAttrSetList,
+ pub recv: RustSdpAttributeImageAttrSetList,
+}
+
+impl<'a> From<&'a SdpAttributeImageAttr> for RustSdpAttributeImageAttr {
+ fn from(other: &SdpAttributeImageAttr) -> Self {
+ RustSdpAttributeImageAttr {
+ pt: match other.pt {
+ SdpAttributePayloadType::Wildcard => u32::max_value(),
+ SdpAttributePayloadType::PayloadType(x) => x as u32,
+ },
+ send: RustSdpAttributeImageAttrSetList::from(&other.send),
+ recv: RustSdpAttributeImageAttrSetList::from(&other.recv),
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_imageattr_count(attributes: *const Vec<SdpAttribute>) -> size_t {
+ count_attribute((*attributes).as_slice(), SdpAttributeType::ImageAttr)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_imageattrs(
+ attributes: *const Vec<SdpAttribute>,
+ ret_size: size_t,
+ ret_attrs: *mut RustSdpAttributeImageAttr,
+) {
+ let attrs: Vec<_> = (*attributes)
+ .iter()
+ .filter_map(|x| {
+ if let SdpAttribute::ImageAttr(ref data) = *x {
+ Some(RustSdpAttributeImageAttr::from(data))
+ } else {
+ None
+ }
+ })
+ .collect();
+ let imageattrs = slice::from_raw_parts_mut(ret_attrs, ret_size);
+ imageattrs.copy_from_slice(attrs.as_slice());
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct RustSdpAttributeSctpmap {
+ pub port: u32,
+ pub channels: u32,
+}
+
+impl<'a> From<&'a SdpAttributeSctpmap> for RustSdpAttributeSctpmap {
+ fn from(other: &SdpAttributeSctpmap) -> Self {
+ RustSdpAttributeSctpmap {
+ port: other.port as u32,
+ channels: other.channels,
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_sctpmap_count(attributes: *const Vec<SdpAttribute>) -> size_t {
+ count_attribute((*attributes).as_slice(), SdpAttributeType::Sctpmap)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_sctpmaps(
+ attributes: *const Vec<SdpAttribute>,
+ ret_size: size_t,
+ ret_sctpmaps: *mut RustSdpAttributeSctpmap,
+) {
+ let attrs: Vec<_> = (*attributes)
+ .iter()
+ .filter_map(|x| {
+ if let SdpAttribute::Sctpmap(ref data) = *x {
+ Some(RustSdpAttributeSctpmap::from(data))
+ } else {
+ None
+ }
+ })
+ .collect();
+ let sctpmaps = slice::from_raw_parts_mut(ret_sctpmaps, ret_size);
+ sctpmaps.copy_from_slice(attrs.as_slice());
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct RustSdpAttributeSimulcastId {
+ pub id: StringView,
+ pub paused: bool,
+}
+
+impl<'a> From<&'a SdpAttributeSimulcastId> for RustSdpAttributeSimulcastId {
+ fn from(other: &SdpAttributeSimulcastId) -> Self {
+ RustSdpAttributeSimulcastId {
+ id: StringView::from(other.id.as_str()),
+ paused: other.paused,
+ }
+ }
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct RustSdpAttributeSimulcastVersion {
+ pub ids: *const Vec<SdpAttributeSimulcastId>,
+}
+
+impl<'a> From<&'a SdpAttributeSimulcastVersion> for RustSdpAttributeSimulcastVersion {
+ fn from(other: &SdpAttributeSimulcastVersion) -> Self {
+ RustSdpAttributeSimulcastVersion { ids: &other.ids }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_simulcast_get_ids_count(
+ ids: *const Vec<SdpAttributeSimulcastId>,
+) -> size_t {
+ (*ids).len()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_simulcast_get_ids(
+ ids: *const Vec<SdpAttributeSimulcastId>,
+ ret_size: size_t,
+ ret: *mut RustSdpAttributeSimulcastId,
+) {
+ let rust_ids: Vec<_> = (*ids)
+ .iter()
+ .map(RustSdpAttributeSimulcastId::from)
+ .collect();
+ let ids = slice::from_raw_parts_mut(ret, ret_size);
+ ids.clone_from_slice(rust_ids.as_slice());
+}
+
+#[repr(C)]
+pub struct RustSdpAttributeSimulcast {
+ pub send: *const Vec<SdpAttributeSimulcastVersion>,
+ pub receive: *const Vec<SdpAttributeSimulcastVersion>,
+}
+
+impl<'a> From<&'a SdpAttributeSimulcast> for RustSdpAttributeSimulcast {
+ fn from(other: &SdpAttributeSimulcast) -> Self {
+ RustSdpAttributeSimulcast {
+ send: &other.send,
+ receive: &other.receive,
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_simulcast_get_version_count(
+ version_list: *const Vec<SdpAttributeSimulcastVersion>,
+) -> size_t {
+ (*version_list).len()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_simulcast_get_versions(
+ version_list: *const Vec<SdpAttributeSimulcastVersion>,
+ ret_size: size_t,
+ ret: *mut RustSdpAttributeSimulcastVersion,
+) {
+ let rust_versions_list: Vec<_> = (*version_list)
+ .iter()
+ .map(RustSdpAttributeSimulcastVersion::from)
+ .collect();
+ let versions = slice::from_raw_parts_mut(ret, ret_size);
+ versions.clone_from_slice(rust_versions_list.as_slice())
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_simulcast(
+ attributes: *const Vec<SdpAttribute>,
+ ret: *mut RustSdpAttributeSimulcast,
+) -> nsresult {
+ let attr = get_attribute((*attributes).as_slice(), SdpAttributeType::Simulcast);
+ if let Some(&SdpAttribute::Simulcast(ref data)) = attr {
+ *ret = RustSdpAttributeSimulcast::from(data);
+ return NS_OK;
+ }
+ NS_ERROR_INVALID_ARG
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub enum RustDirection {
+ Recvonly,
+ Sendonly,
+ Sendrecv,
+ Inactive,
+}
+
+impl<'a> From<&'a Option<SdpAttributeDirection>> for RustDirection {
+ fn from(other: &Option<SdpAttributeDirection>) -> Self {
+ match *other {
+ Some(ref direction) => match *direction {
+ SdpAttributeDirection::Recvonly => RustDirection::Recvonly,
+ SdpAttributeDirection::Sendonly => RustDirection::Sendonly,
+ SdpAttributeDirection::Sendrecv => RustDirection::Sendrecv,
+ },
+ None => RustDirection::Inactive,
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_direction(attributes: *const Vec<SdpAttribute>) -> RustDirection {
+ for attribute in (*attributes).iter() {
+ match *attribute {
+ SdpAttribute::Recvonly => {
+ return RustDirection::Recvonly;
+ }
+ SdpAttribute::Sendonly => {
+ return RustDirection::Sendonly;
+ }
+ SdpAttribute::Sendrecv => {
+ return RustDirection::Sendrecv;
+ }
+ SdpAttribute::Inactive => {
+ return RustDirection::Inactive;
+ }
+ _ => (),
+ }
+ }
+ RustDirection::Sendrecv
+}
+
+#[repr(C)]
+pub struct RustSdpAttributeRemoteCandidate {
+ pub component: u32,
+ pub address: RustAddress,
+ pub port: u32,
+}
+
+impl<'a> From<&'a SdpAttributeRemoteCandidate> for RustSdpAttributeRemoteCandidate {
+ fn from(other: &SdpAttributeRemoteCandidate) -> Self {
+ RustSdpAttributeRemoteCandidate {
+ component: other.component,
+ address: RustAddress::from(&other.address),
+ port: other.port,
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_remote_candidate_count(
+ attributes: *const Vec<SdpAttribute>,
+) -> size_t {
+ count_attribute((*attributes).as_slice(), SdpAttributeType::RemoteCandidate)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_remote_candidates(
+ attributes: *const Vec<SdpAttribute>,
+ ret_size: size_t,
+ ret_candidates: *mut RustSdpAttributeRemoteCandidate,
+) {
+ let attrs = (*attributes).iter().filter_map(|x| {
+ if let SdpAttribute::RemoteCandidate(ref data) = *x {
+ Some(RustSdpAttributeRemoteCandidate::from(data))
+ } else {
+ None
+ }
+ });
+ let candidates = slice::from_raw_parts_mut(ret_candidates, ret_size);
+ for (source, destination) in attrs.zip(candidates) {
+ *destination = source
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_candidate_count(attributes: *const Vec<SdpAttribute>) -> size_t {
+ count_attribute((*attributes).as_slice(), SdpAttributeType::Candidate)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_candidates(
+ attributes: *const Vec<SdpAttribute>,
+ _ret_size: size_t,
+ ret: *mut *const Vec<String>,
+) {
+ let attr_strings: Vec<String> = (*attributes)
+ .iter()
+ .filter_map(|x| {
+ if let SdpAttribute::Candidate(ref attr) = *x {
+ // The serialized attribute starts with "candidate:...", this needs to be removed
+ Some(attr.to_string())
+ } else {
+ None
+ }
+ })
+ .collect();
+
+ *ret = Box::into_raw(Box::from(attr_strings));
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct RustSdpAttributeRidParameters {
+ pub max_width: u32,
+ pub max_height: u32,
+ pub max_fps: u32,
+ pub max_fs: u32,
+ pub max_br: u32,
+ pub max_pps: u32,
+ pub unknown: *const Vec<String>,
+}
+
+impl<'a> From<&'a SdpAttributeRidParameters> for RustSdpAttributeRidParameters {
+ fn from(other: &SdpAttributeRidParameters) -> Self {
+ RustSdpAttributeRidParameters {
+ max_width: other.max_width,
+ max_height: other.max_height,
+ max_fps: other.max_fps,
+ max_fs: other.max_fs,
+ max_br: other.max_br,
+ max_pps: other.max_pps,
+
+ unknown: &other.unknown,
+ }
+ }
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct RustSdpAttributeRid {
+ pub id: StringView,
+ pub direction: u32,
+ pub formats: *const Vec<u16>,
+ pub params: RustSdpAttributeRidParameters,
+ pub depends: *const Vec<String>,
+}
+
+impl<'a> From<&'a SdpAttributeRid> for RustSdpAttributeRid {
+ fn from(other: &SdpAttributeRid) -> Self {
+ RustSdpAttributeRid {
+ id: StringView::from(other.id.as_str()),
+ direction: other.direction.clone() as u32,
+ formats: &other.formats,
+ params: RustSdpAttributeRidParameters::from(&other.params),
+ depends: &other.depends,
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_rid_count(attributes: *const Vec<SdpAttribute>) -> size_t {
+ count_attribute((*attributes).as_slice(), SdpAttributeType::Rid)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_rids(
+ attributes: *const Vec<SdpAttribute>,
+ ret_size: size_t,
+ ret_rids: *mut RustSdpAttributeRid,
+) {
+ let attrs: Vec<_> = (*attributes)
+ .iter()
+ .filter_map(|x| {
+ if let SdpAttribute::Rid(ref data) = *x {
+ Some(RustSdpAttributeRid::from(data))
+ } else {
+ None
+ }
+ })
+ .collect();
+ let rids = slice::from_raw_parts_mut(ret_rids, ret_size);
+ rids.clone_from_slice(attrs.as_slice());
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct RustSdpAttributeExtmap {
+ pub id: u16,
+ pub direction_specified: bool,
+ pub direction: RustDirection,
+ pub url: StringView,
+ pub extension_attributes: StringView,
+}
+
+impl<'a> From<&'a SdpAttributeExtmap> for RustSdpAttributeExtmap {
+ fn from(other: &SdpAttributeExtmap) -> Self {
+ let dir = if other.direction.is_some() {
+ RustDirection::from(&other.direction)
+ } else {
+ RustDirection::from(&Some(SdpAttributeDirection::Sendrecv))
+ };
+ RustSdpAttributeExtmap {
+ id: other.id as u16,
+ direction_specified: other.direction.is_some(),
+ direction: dir,
+ url: StringView::from(other.url.as_str()),
+ extension_attributes: StringView::from(&other.extension_attributes),
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_extmap_count(attributes: *const Vec<SdpAttribute>) -> size_t {
+ count_attribute((*attributes).as_slice(), SdpAttributeType::Extmap)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_extmaps(
+ attributes: *const Vec<SdpAttribute>,
+ ret_size: size_t,
+ ret_rids: *mut RustSdpAttributeExtmap,
+) {
+ let attrs: Vec<_> = (*attributes)
+ .iter()
+ .filter_map(|x| {
+ if let SdpAttribute::Extmap(ref data) = *x {
+ Some(RustSdpAttributeExtmap::from(data))
+ } else {
+ None
+ }
+ })
+ .collect();
+ let extmaps = slice::from_raw_parts_mut(ret_rids, ret_size);
+ extmaps.copy_from_slice(attrs.as_slice());
+}
diff --git a/dom/media/webrtc/sdp/rsdparsa_capi/src/lib.rs b/dom/media/webrtc/sdp/rsdparsa_capi/src/lib.rs
new file mode 100644
index 0000000000..20a13900a2
--- /dev/null
+++ b/dom/media/webrtc/sdp/rsdparsa_capi/src/lib.rs
@@ -0,0 +1,298 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate libc;
+extern crate nserror;
+extern crate rsdparsa;
+
+use std::ffi::CString;
+use std::os::raw::c_char;
+use std::ptr;
+
+use libc::size_t;
+
+use std::rc::Rc;
+
+use std::convert::{TryFrom, TryInto};
+
+use nserror::{nsresult, NS_ERROR_INVALID_ARG, NS_OK};
+use rsdparsa::address::ExplicitlyTypedAddress;
+use rsdparsa::anonymizer::{AnonymizingClone, StatefulSdpAnonymizer};
+use rsdparsa::attribute_type::SdpAttribute;
+use rsdparsa::error::SdpParserError;
+use rsdparsa::media_type::{SdpMediaValue, SdpProtocolValue};
+use rsdparsa::{SdpBandwidth, SdpSession, SdpTiming};
+
+#[macro_use]
+extern crate log;
+
+pub mod attribute;
+pub mod media_section;
+pub mod network;
+pub mod types;
+
+use network::{
+ get_bandwidth, origin_view_helper, RustAddressType, RustSdpConnection, RustSdpOrigin,
+};
+pub use types::{StringView, NULL_STRING};
+
+#[no_mangle]
+pub unsafe extern "C" fn parse_sdp(
+ sdp: StringView,
+ fail_on_warning: bool,
+ session: *mut *const SdpSession,
+ parser_error: *mut *const SdpParserError,
+) -> nsresult {
+ let sdp_str: String = match sdp.try_into() {
+ Ok(string) => string,
+ Err(boxed_error) => {
+ *session = ptr::null();
+ *parser_error = Box::into_raw(Box::new(SdpParserError::Sequence {
+ message: format!("{}", boxed_error),
+ line_number: 0,
+ }));
+ return NS_ERROR_INVALID_ARG;
+ }
+ };
+
+ let parser_result = rsdparsa::parse_sdp(&sdp_str, fail_on_warning);
+ match parser_result {
+ Ok(mut parsed) => {
+ *parser_error = match parsed.warnings.len() {
+ 0 => ptr::null(),
+ _ => Box::into_raw(Box::new(parsed.warnings.remove(0))),
+ };
+ *session = Rc::into_raw(Rc::new(parsed));
+ NS_OK
+ }
+ Err(e) => {
+ *session = ptr::null();
+ error!("Error parsing SDP in rust: {}", e);
+ *parser_error = Box::into_raw(Box::new(e));
+ NS_ERROR_INVALID_ARG
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn create_anonymized_sdp_clone(
+ session: *const SdpSession,
+) -> *const SdpSession {
+ Rc::into_raw(Rc::new(
+ (*session).masked_clone(&mut StatefulSdpAnonymizer::new()),
+ ))
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn create_sdp_clone(session: *const SdpSession) -> *const SdpSession {
+ Rc::into_raw(Rc::new((*session).clone()))
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_free_session(sdp_ptr: *mut SdpSession) {
+ let sdp = Rc::from_raw(sdp_ptr);
+ drop(sdp);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_new_reference(session: *mut SdpSession) -> *const SdpSession {
+ let original = Rc::from_raw(session);
+ let ret = Rc::into_raw(Rc::clone(&original));
+ Rc::into_raw(original); // So the original reference doesn't get dropped
+ ret
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_error_line_num(parser_error: *mut SdpParserError) -> size_t {
+ match *parser_error {
+ SdpParserError::Line { line_number, .. }
+ | SdpParserError::Unsupported { line_number, .. }
+ | SdpParserError::Sequence { line_number, .. } => line_number,
+ }
+}
+
+#[no_mangle]
+// Callee must check that a nullptr is not returned
+pub unsafe extern "C" fn sdp_get_error_message(parser_error: *mut SdpParserError) -> *mut c_char {
+ let message = format!("{}", *parser_error);
+ return match CString::new(message.as_str()) {
+ Ok(c_char_ptr) => c_char_ptr.into_raw(),
+ Err(_) => 0 as *mut c_char,
+ };
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_free_error_message(message: *mut c_char) {
+ if message != 0 as *mut c_char {
+ let _tmp = CString::from_raw(message);
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_free_error(parser_error: *mut SdpParserError) {
+ let e = Box::from_raw(parser_error);
+ drop(e);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn get_version(session: *const SdpSession) -> u64 {
+ (*session).get_version()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_origin(session: *const SdpSession) -> RustSdpOrigin {
+ origin_view_helper((*session).get_origin())
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn session_view(session: *const SdpSession) -> StringView {
+ StringView::from((*session).get_session())
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_session_has_connection(session: *const SdpSession) -> bool {
+ (*session).connection.is_some()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_session_connection(
+ session: *const SdpSession,
+ connection: *mut RustSdpConnection,
+) -> nsresult {
+ match (*session).connection {
+ Some(ref c) => {
+ *connection = RustSdpConnection::from(c);
+ NS_OK
+ }
+ None => NS_ERROR_INVALID_ARG,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_add_media_section(
+ session: *mut SdpSession,
+ media_type: u32,
+ direction: u32,
+ port: u16,
+ protocol: u32,
+ addr_type: u32,
+ address: StringView,
+) -> nsresult {
+ let addr_type = match RustAddressType::try_from(addr_type) {
+ Ok(a) => a.into(),
+ Err(e) => {
+ return e;
+ }
+ };
+ let address_string: String = match address.try_into() {
+ Ok(x) => x,
+ Err(boxed_error) => {
+ error!("Error while parsing string, description: {}", boxed_error);
+ return NS_ERROR_INVALID_ARG;
+ }
+ };
+ let address = match ExplicitlyTypedAddress::try_from((addr_type, address_string.as_str())) {
+ Ok(a) => a,
+ Err(_) => {
+ return NS_ERROR_INVALID_ARG;
+ }
+ };
+
+ let media_type = match media_type {
+ 0 => SdpMediaValue::Audio, // MediaType::kAudio
+ 1 => SdpMediaValue::Video, // MediaType::kVideo
+ 3 => SdpMediaValue::Application, // MediaType::kApplication
+ _ => {
+ return NS_ERROR_INVALID_ARG;
+ }
+ };
+ let protocol = match protocol {
+ 20 => SdpProtocolValue::RtpSavpf, // Protocol::kRtpSavpf
+ 21 => SdpProtocolValue::UdpTlsRtpSavp, // Protocol::kUdpTlsRtpSavp
+ 22 => SdpProtocolValue::TcpDtlsRtpSavp, // Protocol::kTcpDtlsRtpSavp
+ 24 => SdpProtocolValue::UdpTlsRtpSavpf, // Protocol::kUdpTlsRtpSavpf
+ 25 => SdpProtocolValue::TcpDtlsRtpSavpf, // Protocol::kTcpTlsRtpSavpf
+ 37 => SdpProtocolValue::DtlsSctp, // Protocol::kDtlsSctp
+ 38 => SdpProtocolValue::UdpDtlsSctp, // Protocol::kUdpDtlsSctp
+ 39 => SdpProtocolValue::TcpDtlsSctp, // Protocol::kTcpDtlsSctp
+ _ => {
+ return NS_ERROR_INVALID_ARG;
+ }
+ };
+ let direction = match direction {
+ 1 => SdpAttribute::Sendonly,
+ 2 => SdpAttribute::Recvonly,
+ 3 => SdpAttribute::Sendrecv,
+ _ => {
+ return NS_ERROR_INVALID_ARG;
+ }
+ };
+
+ match (*session).add_media(media_type, direction, port as u32, protocol, address) {
+ Ok(_) => NS_OK,
+ Err(_) => NS_ERROR_INVALID_ARG,
+ }
+}
+
+#[repr(C)]
+#[derive(Clone)]
+pub struct RustSdpTiming {
+ pub start: u64,
+ pub stop: u64,
+}
+
+impl<'a> From<&'a SdpTiming> for RustSdpTiming {
+ fn from(timing: &SdpTiming) -> Self {
+ RustSdpTiming {
+ start: timing.start,
+ stop: timing.stop,
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_session_has_timing(session: *const SdpSession) -> bool {
+ (*session).timing.is_some()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_session_timing(
+ session: *const SdpSession,
+ timing: *mut RustSdpTiming,
+) -> nsresult {
+ match (*session).timing {
+ Some(ref t) => {
+ *timing = RustSdpTiming::from(t);
+ NS_OK
+ }
+ None => NS_ERROR_INVALID_ARG,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_media_section_count(session: *const SdpSession) -> size_t {
+ (*session).media.len()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn get_sdp_bandwidth(
+ session: *const SdpSession,
+ bandwidth_type: *const c_char,
+) -> u32 {
+ get_bandwidth(&(*session).bandwidth, bandwidth_type)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_session_bandwidth_vec(
+ session: *const SdpSession,
+) -> *const Vec<SdpBandwidth> {
+ &(*session).bandwidth
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn get_sdp_session_attributes(
+ session: *const SdpSession,
+) -> *const Vec<SdpAttribute> {
+ &(*session).attribute
+}
diff --git a/dom/media/webrtc/sdp/rsdparsa_capi/src/media_section.rs b/dom/media/webrtc/sdp/rsdparsa_capi/src/media_section.rs
new file mode 100644
index 0000000000..8429ab2815
--- /dev/null
+++ b/dom/media/webrtc/sdp/rsdparsa_capi/src/media_section.rs
@@ -0,0 +1,233 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use std::convert::TryInto;
+use std::os::raw::c_char;
+use std::ptr;
+
+use libc::size_t;
+
+use nserror::{nsresult, NS_ERROR_INVALID_ARG, NS_OK};
+use rsdparsa::attribute_type::{SdpAttribute, SdpAttributeRtpmap};
+use rsdparsa::media_type::{SdpFormatList, SdpMedia, SdpMediaValue, SdpProtocolValue};
+use rsdparsa::{SdpBandwidth, SdpSession};
+
+use network::{get_bandwidth, RustSdpConnection};
+use types::StringView;
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_media_section(
+ session: *const SdpSession,
+ index: size_t,
+) -> *const SdpMedia {
+ return match (*session).media.get(index) {
+ Some(m) => m,
+ None => ptr::null(),
+ };
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub enum RustSdpMediaValue {
+ Audio,
+ Video,
+ Application,
+}
+
+impl<'a> From<&'a SdpMediaValue> for RustSdpMediaValue {
+ fn from(val: &SdpMediaValue) -> Self {
+ match *val {
+ SdpMediaValue::Audio => RustSdpMediaValue::Audio,
+ SdpMediaValue::Video => RustSdpMediaValue::Video,
+ SdpMediaValue::Application => RustSdpMediaValue::Application,
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_rust_get_media_type(sdp_media: *const SdpMedia) -> RustSdpMediaValue {
+ RustSdpMediaValue::from((*sdp_media).get_type())
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub enum RustSdpProtocolValue {
+ RtpSavpf,
+ UdpTlsRtpSavp,
+ TcpDtlsRtpSavp,
+ UdpTlsRtpSavpf,
+ TcpDtlsRtpSavpf,
+ DtlsSctp,
+ UdpDtlsSctp,
+ TcpDtlsSctp,
+ RtpAvp,
+ RtpAvpf,
+ RtpSavp,
+}
+
+impl<'a> From<&'a SdpProtocolValue> for RustSdpProtocolValue {
+ fn from(val: &SdpProtocolValue) -> Self {
+ match *val {
+ SdpProtocolValue::RtpSavpf => RustSdpProtocolValue::RtpSavpf,
+ SdpProtocolValue::UdpTlsRtpSavp => RustSdpProtocolValue::UdpTlsRtpSavp,
+ SdpProtocolValue::TcpDtlsRtpSavp => RustSdpProtocolValue::TcpDtlsRtpSavp,
+ SdpProtocolValue::UdpTlsRtpSavpf => RustSdpProtocolValue::UdpTlsRtpSavpf,
+ SdpProtocolValue::TcpDtlsRtpSavpf => RustSdpProtocolValue::TcpDtlsRtpSavpf,
+ SdpProtocolValue::DtlsSctp => RustSdpProtocolValue::DtlsSctp,
+ SdpProtocolValue::UdpDtlsSctp => RustSdpProtocolValue::UdpDtlsSctp,
+ SdpProtocolValue::TcpDtlsSctp => RustSdpProtocolValue::TcpDtlsSctp,
+ SdpProtocolValue::RtpAvp => RustSdpProtocolValue::RtpAvp,
+ SdpProtocolValue::RtpAvpf => RustSdpProtocolValue::RtpAvpf,
+ SdpProtocolValue::RtpSavp => RustSdpProtocolValue::RtpSavp,
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_media_protocol(
+ sdp_media: *const SdpMedia,
+) -> RustSdpProtocolValue {
+ RustSdpProtocolValue::from((*sdp_media).get_proto())
+}
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub enum RustSdpFormatType {
+ Integers,
+ Strings,
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_format_type(sdp_media: *const SdpMedia) -> RustSdpFormatType {
+ match *(*sdp_media).get_formats() {
+ SdpFormatList::Integers(_) => RustSdpFormatType::Integers,
+ SdpFormatList::Strings(_) => RustSdpFormatType::Strings,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_format_string_vec(
+ sdp_media: *const SdpMedia,
+) -> *const Vec<String> {
+ if let SdpFormatList::Strings(ref formats) = *(*sdp_media).get_formats() {
+ formats
+ } else {
+ ptr::null()
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_format_u32_vec(sdp_media: *const SdpMedia) -> *const Vec<u32> {
+ if let SdpFormatList::Integers(ref formats) = *(*sdp_media).get_formats() {
+ formats
+ } else {
+ ptr::null()
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_set_media_port(sdp_media: *mut SdpMedia, port: u32) {
+ (*sdp_media).set_port(port);
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_media_port(sdp_media: *const SdpMedia) -> u32 {
+ (*sdp_media).get_port()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_media_port_count(sdp_media: *const SdpMedia) -> u32 {
+ (*sdp_media).get_port_count()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_media_bandwidth(
+ sdp_media: *const SdpMedia,
+ bandwidth_type: *const c_char,
+) -> u32 {
+ get_bandwidth((*sdp_media).get_bandwidth(), bandwidth_type)
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_media_bandwidth_vec(
+ sdp_media: *const SdpMedia,
+) -> *const Vec<SdpBandwidth> {
+ (*sdp_media).get_bandwidth()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_media_has_connection(sdp_media: *const SdpMedia) -> bool {
+ (*sdp_media).get_connection().is_some()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_media_connection(
+ sdp_media: *const SdpMedia,
+ ret: *mut RustSdpConnection,
+) -> nsresult {
+ if let &Some(ref connection) = (*sdp_media).get_connection() {
+ *ret = RustSdpConnection::from(connection);
+ return NS_OK;
+ }
+ NS_ERROR_INVALID_ARG
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_get_media_attribute_list(
+ sdp_media: *const SdpMedia,
+) -> *const Vec<SdpAttribute> {
+ (*sdp_media).get_attributes()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_media_clear_codecs(sdp_media: *mut SdpMedia) {
+ (*sdp_media).remove_codecs()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_media_add_codec(
+ sdp_media: *mut SdpMedia,
+ pt: u8,
+ codec_name: StringView,
+ clockrate: u32,
+ channels: u16,
+) -> nsresult {
+ let rtpmap = SdpAttributeRtpmap {
+ payload_type: pt,
+ codec_name: match codec_name.try_into() {
+ Ok(x) => x,
+ Err(boxed_error) => {
+ error!("Error while parsing string, description: {}", boxed_error);
+ return NS_ERROR_INVALID_ARG;
+ }
+ },
+ frequency: clockrate,
+ channels: Some(channels as u32),
+ };
+
+ match (*sdp_media).add_codec(rtpmap) {
+ Ok(_) => NS_OK,
+ Err(_) => NS_ERROR_INVALID_ARG,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_media_add_datachannel(
+ sdp_media: *mut SdpMedia,
+ name: StringView,
+ port: u16,
+ streams: u16,
+ message_size: u32,
+) -> nsresult {
+ let name_str = match name.try_into() {
+ Ok(x) => x,
+ Err(_) => {
+ return NS_ERROR_INVALID_ARG;
+ }
+ };
+ match (*sdp_media).add_datachannel(name_str, port, streams, message_size) {
+ Ok(_) => NS_OK,
+ Err(_) => NS_ERROR_INVALID_ARG,
+ }
+}
diff --git a/dom/media/webrtc/sdp/rsdparsa_capi/src/network.rs b/dom/media/webrtc/sdp/rsdparsa_capi/src/network.rs
new file mode 100644
index 0000000000..970f7c8dec
--- /dev/null
+++ b/dom/media/webrtc/sdp/rsdparsa_capi/src/network.rs
@@ -0,0 +1,266 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+extern crate nserror;
+
+use std::ffi::{CStr, CString};
+use std::net::IpAddr;
+use std::os::raw::c_char;
+
+use rsdparsa::address::{Address, AddressType, AddressTyped, ExplicitlyTypedAddress};
+use rsdparsa::{SdpBandwidth, SdpConnection, SdpOrigin};
+use std::convert::TryFrom;
+use types::{StringView, NULL_STRING};
+
+#[repr(C)]
+#[derive(Clone, Copy, PartialEq)]
+pub enum RustAddressType {
+ IP4,
+ IP6,
+}
+
+impl TryFrom<u32> for RustAddressType {
+ type Error = nserror::nsresult;
+ fn try_from(address_type: u32) -> Result<Self, Self::Error> {
+ match address_type {
+ 1 => Ok(RustAddressType::IP4),
+ 2 => Ok(RustAddressType::IP6),
+ _ => Err(nserror::NS_ERROR_INVALID_ARG),
+ }
+ }
+}
+
+impl From<AddressType> for RustAddressType {
+ fn from(address_type: AddressType) -> Self {
+ match address_type {
+ AddressType::IpV4 => RustAddressType::IP4,
+ AddressType::IpV6 => RustAddressType::IP6,
+ }
+ }
+}
+
+impl Into<AddressType> for RustAddressType {
+ fn into(self) -> AddressType {
+ match self {
+ RustAddressType::IP4 => AddressType::IpV4,
+ RustAddressType::IP6 => AddressType::IpV6,
+ }
+ }
+}
+
+impl<'a> From<&'a IpAddr> for RustAddressType {
+ fn from(addr: &IpAddr) -> RustAddressType {
+ addr.address_type().into()
+ }
+}
+
+pub fn get_octets(addr: &IpAddr) -> [u8; 16] {
+ let mut octets = [0; 16];
+ match *addr {
+ IpAddr::V4(v4_addr) => {
+ let v4_octets = v4_addr.octets();
+ (&mut octets[0..4]).copy_from_slice(&v4_octets);
+ }
+ IpAddr::V6(v6_addr) => {
+ let v6_octets = v6_addr.octets();
+ octets.copy_from_slice(&v6_octets);
+ }
+ }
+ octets
+}
+
+#[repr(C)]
+pub struct RustAddress {
+ ip_address: [u8; 50],
+ fqdn: StringView,
+ is_fqdn: bool,
+}
+
+impl<'a> From<&'a Address> for RustAddress {
+ fn from(address: &Address) -> Self {
+ match address {
+ Address::Ip(ip) => Self::from(ip),
+ Address::Fqdn(fqdn) => Self {
+ ip_address: [0; 50],
+ fqdn: fqdn.as_str().into(),
+ is_fqdn: true,
+ },
+ }
+ }
+}
+
+impl<'a> From<&'a IpAddr> for RustAddress {
+ fn from(addr: &IpAddr) -> Self {
+ let mut c_addr = [0; 50];
+ let str_addr = format!("{}", addr);
+ let str_bytes = str_addr.as_bytes();
+ if str_bytes.len() < 50 {
+ c_addr[..str_bytes.len()].copy_from_slice(&str_bytes);
+ }
+ Self {
+ ip_address: c_addr,
+ fqdn: NULL_STRING,
+ is_fqdn: false,
+ }
+ }
+}
+
+#[repr(C)]
+pub struct RustExplicitlyTypedAddress {
+ address_type: RustAddressType,
+ address: RustAddress,
+}
+
+impl Default for RustExplicitlyTypedAddress {
+ fn default() -> Self {
+ Self {
+ address_type: RustAddressType::IP4,
+ address: RustAddress {
+ ip_address: [0; 50],
+ fqdn: NULL_STRING,
+ is_fqdn: false,
+ },
+ }
+ }
+}
+
+impl<'a> From<&'a ExplicitlyTypedAddress> for RustExplicitlyTypedAddress {
+ fn from(address: &ExplicitlyTypedAddress) -> Self {
+ match address {
+ ExplicitlyTypedAddress::Fqdn { domain, .. } => Self {
+ address_type: address.address_type().into(),
+ address: RustAddress {
+ ip_address: [0; 50],
+ fqdn: StringView::from(domain.as_str()),
+ is_fqdn: true,
+ },
+ },
+ ExplicitlyTypedAddress::Ip(ip_address) => Self {
+ address_type: ip_address.address_type().into(),
+ address: ip_address.into(),
+ },
+ }
+ }
+}
+
+// TODO @@NG remove
+impl<'a> From<&'a Option<IpAddr>> for RustExplicitlyTypedAddress {
+ fn from(addr: &Option<IpAddr>) -> Self {
+ match *addr {
+ Some(ref x) => Self {
+ address_type: RustAddressType::from(x.address_type()),
+ address: RustAddress::from(x),
+ },
+ None => Self::default(),
+ }
+ }
+}
+
+#[repr(C)]
+pub struct RustSdpConnection {
+ pub addr: RustExplicitlyTypedAddress,
+ pub ttl: u8,
+ pub amount: u64,
+}
+
+impl<'a> From<&'a SdpConnection> for RustSdpConnection {
+ fn from(sdp_connection: &SdpConnection) -> Self {
+ let ttl = match sdp_connection.ttl {
+ Some(x) => x as u8,
+ None => 0,
+ };
+ let amount = match sdp_connection.amount {
+ Some(x) => x as u64,
+ None => 0,
+ };
+ RustSdpConnection {
+ addr: RustExplicitlyTypedAddress::from(&sdp_connection.address),
+ ttl: ttl,
+ amount: amount,
+ }
+ }
+}
+
+#[repr(C)]
+pub struct RustSdpOrigin {
+ username: StringView,
+ session_id: u64,
+ session_version: u64,
+ addr: RustExplicitlyTypedAddress,
+}
+
+fn bandwidth_match(str_bw: &str, enum_bw: &SdpBandwidth) -> bool {
+ match *enum_bw {
+ SdpBandwidth::As(_) => str_bw == "AS",
+ SdpBandwidth::Ct(_) => str_bw == "CT",
+ SdpBandwidth::Tias(_) => str_bw == "TIAS",
+ SdpBandwidth::Unknown(ref type_name, _) => str_bw == type_name,
+ }
+}
+
+fn bandwidth_value(bandwidth: &SdpBandwidth) -> u32 {
+ match *bandwidth {
+ SdpBandwidth::As(x) | SdpBandwidth::Ct(x) | SdpBandwidth::Tias(x) => x,
+ SdpBandwidth::Unknown(_, _) => 0,
+ }
+}
+
+pub unsafe fn get_bandwidth(bandwidths: &Vec<SdpBandwidth>, bandwidth_type: *const c_char) -> u32 {
+ let bw_type = match CStr::from_ptr(bandwidth_type).to_str() {
+ Ok(string) => string,
+ Err(_) => return 0,
+ };
+ for bandwidth in bandwidths.iter() {
+ if bandwidth_match(bw_type, bandwidth) {
+ return bandwidth_value(bandwidth);
+ }
+ }
+ 0
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_serialize_bandwidth(bw: *const Vec<SdpBandwidth>) -> *mut c_char {
+ let mut builder = String::new();
+ for bandwidth in (*bw).iter() {
+ match *bandwidth {
+ SdpBandwidth::As(val) => {
+ builder.push_str("b=AS:");
+ builder.push_str(&val.to_string());
+ builder.push_str("\r\n");
+ }
+ SdpBandwidth::Ct(val) => {
+ builder.push_str("b=CT:");
+ builder.push_str(&val.to_string());
+ builder.push_str("\r\n");
+ }
+ SdpBandwidth::Tias(val) => {
+ builder.push_str("b=TIAS:");
+ builder.push_str(&val.to_string());
+ builder.push_str("\r\n");
+ }
+ SdpBandwidth::Unknown(ref name, val) => {
+ builder.push_str("b=");
+ builder.push_str(name.as_str());
+ builder.push(':');
+ builder.push_str(&val.to_string());
+ builder.push_str("\r\n");
+ }
+ }
+ }
+ CString::from_vec_unchecked(builder.into_bytes()).into_raw()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn sdp_free_string(s: *mut c_char) {
+ drop(CString::from_raw(s));
+}
+
+pub unsafe fn origin_view_helper(origin: &SdpOrigin) -> RustSdpOrigin {
+ RustSdpOrigin {
+ username: StringView::from(origin.username.as_str()),
+ session_id: origin.session_id,
+ session_version: origin.session_version,
+ addr: RustExplicitlyTypedAddress::from(&origin.unicast_addr),
+ }
+}
diff --git a/dom/media/webrtc/sdp/rsdparsa_capi/src/types.rs b/dom/media/webrtc/sdp/rsdparsa_capi/src/types.rs
new file mode 100644
index 0000000000..2522c8333d
--- /dev/null
+++ b/dom/media/webrtc/sdp/rsdparsa_capi/src/types.rs
@@ -0,0 +1,199 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use libc::size_t;
+use std::boxed::Box;
+use std::convert::TryInto;
+use std::error::Error;
+use std::ffi::CStr;
+use std::{slice, str};
+
+use nserror::{nsresult, NS_ERROR_INVALID_ARG, NS_OK};
+
+use rsdparsa::attribute_type::SdpAttributeSsrc;
+
+#[repr(C)]
+#[derive(Clone, Copy)]
+pub struct StringView {
+ buffer: *const u8,
+ len: size_t,
+}
+
+pub const NULL_STRING: StringView = StringView {
+ buffer: 0 as *const u8,
+ len: 0,
+};
+
+impl<'a> From<&'a str> for StringView {
+ fn from(input: &str) -> StringView {
+ StringView {
+ buffer: input.as_ptr(),
+ len: input.len(),
+ }
+ }
+}
+
+impl TryInto<String> for StringView {
+ type Error = Box<dyn Error>;
+ fn try_into(self) -> Result<String, Box<dyn Error>> {
+ // This block must be unsafe as it converts a StringView, most likly provided from the
+ // C++ code, into a rust String and thus needs to operate with raw pointers.
+ let string_slice: &[u8];
+ unsafe {
+ // Add one to the length as the length passed in the StringView is the length of
+ // the string and is missing the null terminator
+ string_slice = slice::from_raw_parts(self.buffer, self.len + 1 as usize);
+ }
+
+ let c_str = match CStr::from_bytes_with_nul(string_slice) {
+ Ok(string) => string,
+ Err(x) => {
+ return Err(Box::new(x));
+ }
+ };
+
+ let str_slice: &str = match str::from_utf8(c_str.to_bytes()) {
+ Ok(string) => string,
+ Err(x) => {
+ return Err(Box::new(x));
+ }
+ };
+
+ Ok(str_slice.to_string())
+ }
+}
+
+impl<'a, T: AsRef<str>> From<&'a Option<T>> for StringView {
+ fn from(input: &Option<T>) -> StringView {
+ match *input {
+ Some(ref x) => StringView {
+ buffer: x.as_ref().as_ptr(),
+ len: x.as_ref().len(),
+ },
+ None => NULL_STRING,
+ }
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn string_vec_len(vec: *const Vec<String>) -> size_t {
+ (*vec).len() as size_t
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn string_vec_get_view(
+ vec: *const Vec<String>,
+ index: size_t,
+ ret: *mut StringView,
+) -> nsresult {
+ match (*vec).get(index) {
+ Some(ref string) => {
+ *ret = StringView::from(string.as_str());
+ NS_OK
+ }
+ None => NS_ERROR_INVALID_ARG,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn free_boxed_string_vec(ptr: *mut Vec<String>) -> nsresult {
+ drop(Box::from_raw(ptr));
+ NS_OK
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn f32_vec_len(vec: *const Vec<f32>) -> size_t {
+ (*vec).len()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn f32_vec_get(
+ vec: *const Vec<f32>,
+ index: size_t,
+ ret: *mut f32,
+) -> nsresult {
+ match (*vec).get(index) {
+ Some(val) => {
+ *ret = *val;
+ NS_OK
+ }
+ None => NS_ERROR_INVALID_ARG,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn u32_vec_len(vec: *const Vec<u32>) -> size_t {
+ (*vec).len()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn u32_vec_get(
+ vec: *const Vec<u32>,
+ index: size_t,
+ ret: *mut u32,
+) -> nsresult {
+ match (*vec).get(index) {
+ Some(val) => {
+ *ret = *val;
+ NS_OK
+ }
+ None => NS_ERROR_INVALID_ARG,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn u16_vec_len(vec: *const Vec<u16>) -> size_t {
+ (*vec).len()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn u16_vec_get(
+ vec: *const Vec<u16>,
+ index: size_t,
+ ret: *mut u16,
+) -> nsresult {
+ match (*vec).get(index) {
+ Some(val) => {
+ *ret = *val;
+ NS_OK
+ }
+ None => NS_ERROR_INVALID_ARG,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn u8_vec_len(vec: *const Vec<u8>) -> size_t {
+ (*vec).len()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn u8_vec_get(vec: *const Vec<u8>, index: size_t, ret: *mut u8) -> nsresult {
+ match (*vec).get(index) {
+ Some(val) => {
+ *ret = *val;
+ NS_OK
+ }
+ None => NS_ERROR_INVALID_ARG,
+ }
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn ssrc_vec_len(vec: *const Vec<SdpAttributeSsrc>) -> size_t {
+ (*vec).len()
+}
+
+#[no_mangle]
+pub unsafe extern "C" fn ssrc_vec_get_id(
+ vec: *const Vec<SdpAttributeSsrc>,
+ index: size_t,
+ ret: *mut u32,
+) -> nsresult {
+ match (*vec).get(index) {
+ Some(val) => {
+ *ret = val.id;
+ NS_OK
+ }
+ None => NS_ERROR_INVALID_ARG,
+ }
+}
diff --git a/dom/media/webrtc/tests/crashtests/1770075.html b/dom/media/webrtc/tests/crashtests/1770075.html
new file mode 100644
index 0000000000..4d451216bd
--- /dev/null
+++ b/dom/media/webrtc/tests/crashtests/1770075.html
@@ -0,0 +1,8 @@
+<script>
+window.addEventListener('load', () => {
+ let a = new RTCPeerConnection({}, {})
+ a.createOffer({'offerToReceiveVideo': true})
+ let b = new WeakRef(a.getTransceivers()[0])
+ setTimeout("self.close()", 200)
+})
+</script>
diff --git a/dom/media/webrtc/tests/crashtests/1789908.html b/dom/media/webrtc/tests/crashtests/1789908.html
new file mode 100644
index 0000000000..3d58d3dc6b
--- /dev/null
+++ b/dom/media/webrtc/tests/crashtests/1789908.html
@@ -0,0 +1,25 @@
+<script>
+window.addEventListener('load', () => {
+ const sdp = `v=0
+o=mozilla...THIS_IS_SDPARTA-99.0 4978061689314146455 0 IN IP4 0.0.0.0
+s=-
+t=0 0
+a=fingerprint:sha-256 1D:E5:0C:97:18:43:38:3D:FF:7D:6A:BF:E3:AC:CA:70:AB:53:5A:35:95:92:4F:98:86:61:CA:5D:D5:9D:5E:41
+a=group:BUNDLE 0
+a=ice-options:trickle
+a=msid-semantic:WMS *
+m=video 9 UDP/TLS/RTP/SAVPF 120
+c=IN IP4 0.0.0.0
+a=fmtp:120 max-fs=12288;max-fr=60
+a=ice-pwd:c3a5e05023a8c38f671aef91ed1802d6
+a=ice-ufrag:91e4526d
+a=setup:actpass
+
+a=rtpmap:120 VP8/90000
+`;
+
+ let a = new RTCPeerConnection()
+ a.setRemoteDescription({sdp, type: "offer"});
+ setTimeout("self.close()", 200)
+})
+</script>
diff --git a/dom/media/webrtc/tests/crashtests/1799168.html b/dom/media/webrtc/tests/crashtests/1799168.html
new file mode 100644
index 0000000000..6c5c9db237
--- /dev/null
+++ b/dom/media/webrtc/tests/crashtests/1799168.html
@@ -0,0 +1,16 @@
+<script>
+window.addEventListener('load', async () => {
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+ offerer.addTransceiver('audio');
+ await offerer.setLocalDescription();
+ await answerer.setRemoteDescription(offerer.localDescription);
+ const answer = await answerer.createAnswer();
+ await offerer.setRemoteDescription(answer);
+ // relay candidate with TCP!
+ const candidate = 'candidate:3 1 tcp 18087935 20.253.151.225 3478 typ relay raddr 10.0.48.153 rport 3478 tcptype passive';
+ await offerer.addIceCandidate({candidate, sdpMLineIndex: 0});
+ await new Promise(r => setTimeout(r, 2000));
+ self.close();
+})
+</script>
diff --git a/dom/media/webrtc/tests/crashtests/1816708.html b/dom/media/webrtc/tests/crashtests/1816708.html
new file mode 100644
index 0000000000..c7ba824041
--- /dev/null
+++ b/dom/media/webrtc/tests/crashtests/1816708.html
@@ -0,0 +1,21 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<script>
+document.addEventListener('DOMContentLoaded', async () => {
+ const peer = new RTCPeerConnection()
+ const stream = await navigator.mediaDevices.getUserMedia({
+ video: true,
+ audio: true,
+ fake: true,
+ peerIdentity: 'name',
+ })
+ stream.getTracks().forEach((track) => peer.addTrack(track, stream))
+ const offer = await peer.createOffer({})
+ await peer.setLocalDescription(offer)
+ await peer.setRemoteDescription(offer)
+ document.documentElement.removeAttribute("class");
+})
+</script>
+</head>
+</html>
diff --git a/dom/media/webrtc/tests/crashtests/1821477.html b/dom/media/webrtc/tests/crashtests/1821477.html
new file mode 100644
index 0000000000..c37bd6bd02
--- /dev/null
+++ b/dom/media/webrtc/tests/crashtests/1821477.html
@@ -0,0 +1,16 @@
+<html class="reftest-wait">
+<script>
+document.addEventListener("DOMContentLoaded", async () => {
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ try {
+ (await navigator.mediaDevices.getDisplayMedia({
+ "video": {
+ "frameRate": 2147483647,
+ },
+ })).stop();
+ } finally {
+ document.documentElement.removeAttribute("class");
+ }
+});
+</script>
+</html>
diff --git a/dom/media/webrtc/tests/crashtests/crashtests.list b/dom/media/webrtc/tests/crashtests/crashtests.list
new file mode 100644
index 0000000000..64434b28e2
--- /dev/null
+++ b/dom/media/webrtc/tests/crashtests/crashtests.list
@@ -0,0 +1,7 @@
+defaults pref(media.navigator.permission.disabled,true) pref(media.devices.insecure.enabled,true) pref(media.getusermedia.insecure.enabled,true)
+
+load 1770075.html
+load 1789908.html
+load 1799168.html
+load 1816708.html
+skip-if(/^Windows\x20NT\x206\.1/.test(http.oscpu)) load 1821477.html
diff --git a/dom/media/webrtc/tests/fuzztests/moz.build b/dom/media/webrtc/tests/fuzztests/moz.build
new file mode 100644
index 0000000000..fef388e6c9
--- /dev/null
+++ b/dom/media/webrtc/tests/fuzztests/moz.build
@@ -0,0 +1,22 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Library("FuzzingSdp")
+
+LOCAL_INCLUDES += [
+ "/dom/media/webrtc",
+ "/ipc/chromium/src",
+ "/media/webrtc",
+]
+
+# Add libFuzzer configuration directives
+include("/tools/fuzzing/libfuzzer-config.mozbuild")
+
+SOURCES += [
+ "sdp_parser_libfuzz.cpp",
+]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/dom/media/webrtc/tests/fuzztests/sdp_parser_libfuzz.cpp b/dom/media/webrtc/tests/fuzztests/sdp_parser_libfuzz.cpp
new file mode 100644
index 0000000000..3451d6fd21
--- /dev/null
+++ b/dom/media/webrtc/tests/fuzztests/sdp_parser_libfuzz.cpp
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <string>
+
+#include "gtest/gtest.h"
+
+#include "FuzzingInterface.h"
+
+#include "sdp/SipccSdpParser.h"
+
+using namespace mozilla;
+
+static mozilla::UniquePtr<SdpParser::Results> sdpPtr;
+static SipccSdpParser mParser;
+
+int FuzzingInitSdpParser(int* argc, char*** argv) { return 0; }
+
+static int RunSdpParserFuzzing(const uint8_t* data, size_t size) {
+ std::string message(reinterpret_cast<const char*>(data), size);
+
+ sdpPtr = mParser.Parse(message);
+
+ return 0;
+}
+
+MOZ_FUZZING_INTERFACE_RAW(FuzzingInitSdpParser, RunSdpParserFuzzing, SdpParser);
diff --git a/dom/media/webrtc/tests/mochitests/NetworkPreparationChromeScript.js b/dom/media/webrtc/tests/mochitests/NetworkPreparationChromeScript.js
new file mode 100644
index 0000000000..d3872f1519
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/NetworkPreparationChromeScript.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var browser = Services.wm.getMostRecentWindow("navigator:browser");
+var connection = browser.navigator.mozMobileConnections[0];
+
+// provide a fake APN and enable data connection.
+// enable 3G radio
+function enableRadio() {
+ if (connection.radioState !== "enabled") {
+ connection.setRadioEnabled(true);
+ }
+}
+
+// disable 3G radio
+function disableRadio() {
+ if (connection.radioState === "enabled") {
+ connection.setRadioEnabled(false);
+ }
+}
+
+addMessageListener("prepare-network", function (message) {
+ connection.addEventListener("datachange", function onDataChange() {
+ if (connection.data.connected) {
+ connection.removeEventListener("datachange", onDataChange);
+ Services.prefs.setIntPref("network.proxy.type", 2);
+ sendAsyncMessage("network-ready", true);
+ }
+ });
+
+ enableRadio();
+});
+
+addMessageListener("network-cleanup", function (message) {
+ connection.addEventListener("datachange", function onDataChange() {
+ if (!connection.data.connected) {
+ connection.removeEventListener("datachange", onDataChange);
+ Services.prefs.setIntPref("network.proxy.type", 2);
+ sendAsyncMessage("network-disabled", true);
+ }
+ });
+ disableRadio();
+});
diff --git a/dom/media/webrtc/tests/mochitests/addTurnsSelfsignedCert.js b/dom/media/webrtc/tests/mochitests/addTurnsSelfsignedCert.js
new file mode 100644
index 0000000000..1e8be3a397
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/addTurnsSelfsignedCert.js
@@ -0,0 +1,32 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* eslint-env mozilla/chrome-script */
+
+"use strict";
+
+// This is only usable from the parent process, even for doing simple stuff like
+// serializing a cert.
+var gCertMaker = Cc["@mozilla.org/security/x509certdb;1"].getService(
+ Ci.nsIX509CertDB
+);
+
+var gCertOverrides = Cc["@mozilla.org/security/certoverride;1"].getService(
+ Ci.nsICertOverrideService
+);
+
+addMessageListener("add-turns-certs", certs => {
+ var port = 5349;
+ certs.forEach(certDescription => {
+ var cert = gCertMaker.constructX509FromBase64(certDescription.cert);
+ gCertOverrides.rememberValidityOverride(
+ certDescription.hostname,
+ port,
+ {},
+ cert,
+ false
+ );
+ });
+ sendAsyncMessage("certs-added");
+});
diff --git a/dom/media/webrtc/tests/mochitests/blacksilence.js b/dom/media/webrtc/tests/mochitests/blacksilence.js
new file mode 100644
index 0000000000..5ea35f8a7f
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/blacksilence.js
@@ -0,0 +1,134 @@
+(function (global) {
+ "use strict";
+
+ // an invertible check on the condition.
+ // if the constraint is applied, then the check is direct
+ // if not applied, then the result should be reversed
+ function check(constraintApplied, condition, message) {
+ var good = constraintApplied ? condition : !condition;
+ message =
+ (constraintApplied ? "with" : "without") +
+ " constraint: should " +
+ (constraintApplied ? "" : "not ") +
+ message +
+ " = " +
+ (good ? "OK" : "waiting...");
+ info(message);
+ return good;
+ }
+
+ function mkElement(type) {
+ // This makes an unattached element.
+ // It's not rendered to save the cycles that costs on b2g emulator
+ // and it gets dropped (and GC'd) when the test is done.
+ var e = document.createElement(type);
+ e.width = 32;
+ e.height = 24;
+ document.getElementById("display").appendChild(e);
+ return e;
+ }
+
+ // Runs checkFunc until it reports success.
+ // This is kludgy, but you have to wait for media to start flowing, and it
+ // can't be any old media, it has to include real data, for which we have no
+ // reliable signals to use as a trigger.
+ function periodicCheck(checkFunc) {
+ var resolve;
+ var done = false;
+ // This returns a function so that we create 10 closures in the loop, not
+ // one; and so that the timers don't all start straight away
+ var waitAndCheck = counter => () => {
+ if (done) {
+ return Promise.resolve();
+ }
+ return new Promise(r => setTimeout(r, 200 << counter)).then(() => {
+ if (checkFunc()) {
+ done = true;
+ resolve();
+ }
+ });
+ };
+
+ var chain = Promise.resolve();
+ for (var i = 0; i < 10; ++i) {
+ chain = chain.then(waitAndCheck(i));
+ }
+ return new Promise(r => (resolve = r));
+ }
+
+ function isSilence(audioData) {
+ var silence = true;
+ for (var i = 0; i < audioData.length; ++i) {
+ if (audioData[i] !== 128) {
+ silence = false;
+ }
+ }
+ return silence;
+ }
+
+ function checkAudio(constraintApplied, stream) {
+ var audio = mkElement("audio");
+ audio.srcObject = stream;
+ audio.play();
+
+ var context = new AudioContext();
+ var source = context.createMediaStreamSource(stream);
+ var analyser = context.createAnalyser();
+ source.connect(analyser);
+ analyser.connect(context.destination);
+
+ return periodicCheck(() => {
+ var sampleCount = analyser.frequencyBinCount;
+ info("got some audio samples: " + sampleCount);
+ var buffer = new Uint8Array(sampleCount);
+ analyser.getByteTimeDomainData(buffer);
+
+ var silent = check(
+ constraintApplied,
+ isSilence(buffer),
+ "be silence for audio"
+ );
+ return sampleCount > 0 && silent;
+ }).then(() => {
+ source.disconnect();
+ analyser.disconnect();
+ audio.pause();
+ ok(true, "audio is " + (constraintApplied ? "" : "not ") + "silent");
+ });
+ }
+
+ function checkVideo(constraintApplied, stream) {
+ var video = mkElement("video");
+ video.srcObject = stream;
+ video.play();
+
+ return periodicCheck(() => {
+ try {
+ var canvas = mkElement("canvas");
+ var ctx = canvas.getContext("2d");
+ // Have to guard drawImage with the try as well, due to bug 879717. If
+ // we get an error, this round fails, but that failure is usually just
+ // transitory.
+ ctx.drawImage(video, 0, 0);
+ ctx.getImageData(0, 0, 1, 1);
+ return check(
+ constraintApplied,
+ false,
+ "throw on getImageData for video"
+ );
+ } catch (e) {
+ return check(
+ constraintApplied,
+ e.name === "SecurityError",
+ "get a security error: " + e.name
+ );
+ }
+ }).then(() => {
+ video.pause();
+ ok(true, "video is " + (constraintApplied ? "" : "not ") + "protected");
+ });
+ }
+
+ global.audioIsSilence = checkAudio;
+ global.videoIsBlack = checkVideo;
+})(this);
diff --git a/dom/media/webrtc/tests/mochitests/dataChannel.js b/dom/media/webrtc/tests/mochitests/dataChannel.js
new file mode 100644
index 0000000000..eac52f96ab
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/dataChannel.js
@@ -0,0 +1,352 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Returns the contents of a blob as text
+ *
+ * @param {Blob} blob
+ The blob to retrieve the contents from
+ */
+function getBlobContent(blob) {
+ return new Promise(resolve => {
+ var reader = new FileReader();
+ // Listen for 'onloadend' which will always be called after a success or failure
+ reader.onloadend = event => resolve(event.target.result);
+ reader.readAsText(blob);
+ });
+}
+
+var commandsCreateDataChannel = [
+ function PC_REMOTE_EXPECT_DATA_CHANNEL(test) {
+ test.pcRemote.expectDataChannel();
+ },
+
+ function PC_LOCAL_CREATE_DATA_CHANNEL(test) {
+ var channel = test.pcLocal.createDataChannel({});
+ is(channel.binaryType, "blob", channel + " is of binary type 'blob'");
+
+ is(
+ test.pcLocal.signalingState,
+ STABLE,
+ "Create datachannel does not change signaling state"
+ );
+ return test.pcLocal.observedNegotiationNeeded;
+ },
+];
+
+var commandsWaitForDataChannel = [
+ function PC_LOCAL_VERIFY_DATA_CHANNEL_STATE(test) {
+ return test.pcLocal.dataChannels[0].opened;
+ },
+
+ function PC_REMOTE_VERIFY_DATA_CHANNEL_STATE(test) {
+ return test.pcRemote.nextDataChannel.then(channel => channel.opened);
+ },
+];
+
+var commandsCheckDataChannel = [
+ function SEND_MESSAGE(test) {
+ var message = "Lorem ipsum dolor sit amet";
+
+ info("Sending message:" + message);
+ return test.send(message).then(result => {
+ is(
+ result.data,
+ message,
+ "Message correctly transmitted from pcLocal to pcRemote."
+ );
+ });
+ },
+
+ function SEND_BLOB(test) {
+ var contents = "At vero eos et accusam et justo duo dolores et ea rebum.";
+ var blob = new Blob([contents], { type: "text/plain" });
+
+ info("Sending blob");
+ return test
+ .send(blob)
+ .then(result => {
+ ok(result.data instanceof Blob, "Received data is of instance Blob");
+ is(result.data.size, blob.size, "Received data has the correct size.");
+
+ return getBlobContent(result.data);
+ })
+ .then(recv_contents =>
+ is(recv_contents, contents, "Received data has the correct content.")
+ );
+ },
+
+ function CREATE_SECOND_DATA_CHANNEL(test) {
+ return test.createDataChannel({}).then(result => {
+ is(
+ result.remote.binaryType,
+ "blob",
+ "remote data channel is of binary type 'blob'"
+ );
+ });
+ },
+
+ function SEND_MESSAGE_THROUGH_LAST_OPENED_CHANNEL(test) {
+ var channels = test.pcRemote.dataChannels;
+ var message = "I am the Omega";
+
+ info("Sending message:" + message);
+ return test.send(message).then(result => {
+ is(
+ channels.indexOf(result.channel),
+ channels.length - 1,
+ "Last channel used"
+ );
+ is(result.data, message, "Received message has the correct content.");
+ });
+ },
+
+ function SEND_MESSAGE_THROUGH_FIRST_CHANNEL(test) {
+ var message = "Message through 1st channel";
+ var options = {
+ sourceChannel: test.pcLocal.dataChannels[0],
+ targetChannel: test.pcRemote.dataChannels[0],
+ };
+
+ info("Sending message:" + message);
+ return test.send(message, options).then(result => {
+ is(
+ test.pcRemote.dataChannels.indexOf(result.channel),
+ 0,
+ "1st channel used"
+ );
+ is(result.data, message, "Received message has the correct content.");
+ });
+ },
+
+ function SEND_MESSAGE_BACK_THROUGH_FIRST_CHANNEL(test) {
+ var message = "Return a message also through 1st channel";
+ var options = {
+ sourceChannel: test.pcRemote.dataChannels[0],
+ targetChannel: test.pcLocal.dataChannels[0],
+ };
+
+ info("Sending message:" + message);
+ return test.send(message, options).then(result => {
+ is(
+ test.pcLocal.dataChannels.indexOf(result.channel),
+ 0,
+ "1st channel used"
+ );
+ is(result.data, message, "Return message has the correct content.");
+ });
+ },
+
+ function CREATE_NEGOTIATED_DATA_CHANNEL_MAX_RETRANSMITS(test) {
+ var options = {
+ negotiated: true,
+ id: 5,
+ protocol: "foo/bar",
+ ordered: false,
+ maxRetransmits: 500,
+ };
+ return test.createDataChannel(options).then(result => {
+ is(
+ result.local.binaryType,
+ "blob",
+ result.remote + " is of binary type 'blob'"
+ );
+ is(
+ result.local.id,
+ options.id,
+ result.local + " id is:" + result.local.id
+ );
+ is(
+ result.local.protocol,
+ options.protocol,
+ result.local + " protocol is:" + result.local.protocol
+ );
+ is(
+ result.local.reliable,
+ false,
+ result.local + " reliable is:" + result.local.reliable
+ );
+ is(
+ result.local.ordered,
+ options.ordered,
+ result.local + " ordered is:" + result.local.ordered
+ );
+ is(
+ result.local.maxRetransmits,
+ options.maxRetransmits,
+ result.local + " maxRetransmits is:" + result.local.maxRetransmits
+ );
+ is(
+ result.local.maxPacketLifeTime,
+ null,
+ result.local + " maxPacketLifeTime is:" + result.local.maxPacketLifeTime
+ );
+
+ is(
+ result.remote.binaryType,
+ "blob",
+ result.remote + " is of binary type 'blob'"
+ );
+ is(
+ result.remote.id,
+ options.id,
+ result.remote + " id is:" + result.remote.id
+ );
+ is(
+ result.remote.protocol,
+ options.protocol,
+ result.remote + " protocol is:" + result.remote.protocol
+ );
+ is(
+ result.remote.reliable,
+ false,
+ result.remote + " reliable is:" + result.remote.reliable
+ );
+ is(
+ result.remote.ordered,
+ options.ordered,
+ result.remote + " ordered is:" + result.remote.ordered
+ );
+ is(
+ result.remote.maxRetransmits,
+ options.maxRetransmits,
+ result.remote + " maxRetransmits is:" + result.remote.maxRetransmits
+ );
+ is(
+ result.remote.maxPacketLifeTime,
+ null,
+ result.remote +
+ " maxPacketLifeTime is:" +
+ result.remote.maxPacketLifeTime
+ );
+ });
+ },
+
+ function SEND_MESSAGE_THROUGH_LAST_OPENED_CHANNEL2(test) {
+ var channels = test.pcRemote.dataChannels;
+ var message = "I am the walrus; Goo goo g'joob";
+
+ info("Sending message:" + message);
+ return test.send(message).then(result => {
+ is(
+ channels.indexOf(result.channel),
+ channels.length - 1,
+ "Last channel used"
+ );
+ is(result.data, message, "Received message has the correct content.");
+ });
+ },
+
+ function CREATE_NEGOTIATED_DATA_CHANNEL_MAX_PACKET_LIFE_TIME(test) {
+ var options = {
+ ordered: false,
+ maxPacketLifeTime: 10,
+ };
+ return test.createDataChannel(options).then(result => {
+ is(
+ result.local.binaryType,
+ "blob",
+ result.local + " is of binary type 'blob'"
+ );
+ is(
+ result.local.protocol,
+ "",
+ result.local + " protocol is:" + result.local.protocol
+ );
+ is(
+ result.local.reliable,
+ false,
+ result.local + " reliable is:" + result.local.reliable
+ );
+ is(
+ result.local.ordered,
+ options.ordered,
+ result.local + " ordered is:" + result.local.ordered
+ );
+ is(
+ result.local.maxRetransmits,
+ null,
+ result.local + " maxRetransmits is:" + result.local.maxRetransmits
+ );
+ is(
+ result.local.maxPacketLifeTime,
+ options.maxPacketLifeTime,
+ result.local + " maxPacketLifeTime is:" + result.local.maxPacketLifeTime
+ );
+
+ is(
+ result.remote.binaryType,
+ "blob",
+ result.remote + " is of binary type 'blob'"
+ );
+ is(
+ result.remote.protocol,
+ "",
+ result.remote + " protocol is:" + result.remote.protocol
+ );
+ is(
+ result.remote.reliable,
+ false,
+ result.remote + " reliable is:" + result.remote.reliable
+ );
+ is(
+ result.remote.ordered,
+ options.ordered,
+ result.remote + " ordered is:" + result.remote.ordered
+ );
+ is(
+ result.remote.maxRetransmits,
+ null,
+ result.remote + " maxRetransmits is:" + result.remote.maxRetransmits
+ );
+ is(
+ result.remote.maxPacketLifeTime,
+ options.maxPacketLifeTime,
+ result.remote +
+ " maxPacketLifeTime is:" +
+ result.remote.maxPacketLifeTime
+ );
+ });
+ },
+
+ function SEND_MESSAGE_THROUGH_LAST_OPENED_CHANNEL3(test) {
+ var channels = test.pcRemote.dataChannels;
+ var message = "Nice to see you working maxPacketLifeTime";
+
+ info("Sending message:" + message);
+ return test.send(message).then(result => {
+ is(
+ channels.indexOf(result.channel),
+ channels.length - 1,
+ "Last channel used"
+ );
+ is(result.data, message, "Received message has the correct content.");
+ });
+ },
+];
+
+var commandsCheckLargeXfer = [
+ function SEND_BIG_BUFFER(test) {
+ var size = 2 * 1024 * 1024; // SCTP internal buffer is now 1MB, so use 2MB to ensure the buffer gets full
+ var buffer = new ArrayBuffer(size);
+ // note: type received is always blob for binary data
+ var options = {};
+ options.bufferedAmountLowThreshold = 64 * 1024;
+ info("Sending arraybuffer");
+ return test.send(buffer, options).then(result => {
+ ok(result.data instanceof Blob, "Received data is of instance Blob");
+ is(result.data.size, size, "Received data has the correct size.");
+ });
+ },
+];
+
+function addInitialDataChannel(chain) {
+ chain.insertBefore("PC_LOCAL_CREATE_OFFER", commandsCreateDataChannel);
+ chain.insertBefore(
+ "PC_LOCAL_WAIT_FOR_MEDIA_FLOW",
+ commandsWaitForDataChannel
+ );
+ chain.removeAfter("PC_REMOTE_CHECK_ICE_CONNECTIONS");
+ chain.append(commandsCheckDataChannel);
+}
diff --git a/dom/media/webrtc/tests/mochitests/head.js b/dom/media/webrtc/tests/mochitests/head.js
new file mode 100644
index 0000000000..7c9f6d52de
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/head.js
@@ -0,0 +1,1445 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var Cc = SpecialPowers.Cc;
+var Ci = SpecialPowers.Ci;
+
+// Specifies if we want fake audio streams for this run
+let WANT_FAKE_AUDIO = true;
+// Specifies if we want fake video streams for this run
+let WANT_FAKE_VIDEO = true;
+let TEST_AUDIO_FREQ = 1000;
+
+/**
+ * Reads the current values of preferences affecting fake and loopback devices
+ * and sets the WANT_FAKE_AUDIO and WANT_FAKE_VIDEO gloabals appropriately.
+ */
+function updateConfigFromFakeAndLoopbackPrefs() {
+ let audioDevice = SpecialPowers.getCharPref("media.audio_loopback_dev", "");
+ if (audioDevice) {
+ WANT_FAKE_AUDIO = false;
+ dump("TEST DEVICES: Got loopback audio: " + audioDevice + "\n");
+ } else {
+ WANT_FAKE_AUDIO = true;
+ dump(
+ "TEST DEVICES: No test device found in media.audio_loopback_dev, using fake audio streams.\n"
+ );
+ }
+ let videoDevice = SpecialPowers.getCharPref("media.video_loopback_dev", "");
+ if (videoDevice) {
+ WANT_FAKE_VIDEO = false;
+ dump("TEST DEVICES: Got loopback video: " + videoDevice + "\n");
+ } else {
+ WANT_FAKE_VIDEO = true;
+ dump(
+ "TEST DEVICES: No test device found in media.video_loopback_dev, using fake video streams.\n"
+ );
+ }
+}
+
+updateConfigFromFakeAndLoopbackPrefs();
+
+/**
+ * Global flag to skip LoopbackTone
+ */
+let DISABLE_LOOPBACK_TONE = false;
+/**
+ * Helper class to setup a sine tone of a given frequency.
+ */
+class LoopbackTone {
+ constructor(audioContext, frequency) {
+ if (!audioContext) {
+ throw new Error("You must provide a valid AudioContext");
+ }
+ this.oscNode = audioContext.createOscillator();
+ var gainNode = audioContext.createGain();
+ gainNode.gain.value = 0.5;
+ this.oscNode.connect(gainNode);
+ gainNode.connect(audioContext.destination);
+ this.changeFrequency(frequency);
+ }
+
+ // Method should be used when WANT_FAKE_AUDIO is false.
+ start() {
+ if (!this.oscNode) {
+ throw new Error("Attempt to start a stopped LoopbackTone");
+ }
+ info(`Start loopback tone at ${this.oscNode.frequency.value}`);
+ this.oscNode.start();
+ }
+
+ // Change the frequency of the tone. It can be used after start.
+ // Frequency will change on the fly. No need to stop and create a new instance.
+ changeFrequency(frequency) {
+ if (!this.oscNode) {
+ throw new Error("Attempt to change frequency on a stopped LoopbackTone");
+ }
+ this.oscNode.frequency.value = frequency;
+ }
+
+ stop() {
+ if (!this.oscNode) {
+ throw new Error("Attempt to stop a stopped LoopbackTone");
+ }
+ this.oscNode.stop();
+ this.oscNode = null;
+ }
+}
+// Object that holds the default loopback tone.
+var DefaultLoopbackTone = null;
+
+/**
+ * This class provides helpers around analysing the audio content in a stream
+ * using WebAudio AnalyserNodes.
+ *
+ * @constructor
+ * @param {object} stream
+ * A MediaStream object whose audio track we shall analyse.
+ */
+function AudioStreamAnalyser(ac, stream) {
+ this.audioContext = ac;
+ this.stream = stream;
+ this.sourceNodes = [];
+ this.analyser = this.audioContext.createAnalyser();
+ // Setting values lower than default for speedier testing on emulators
+ this.analyser.smoothingTimeConstant = 0.2;
+ this.analyser.fftSize = 1024;
+ this.connectTrack = t => {
+ let source = this.audioContext.createMediaStreamSource(
+ new MediaStream([t])
+ );
+ this.sourceNodes.push(source);
+ source.connect(this.analyser);
+ };
+ this.stream.getAudioTracks().forEach(t => this.connectTrack(t));
+ this.onaddtrack = ev => this.connectTrack(ev.track);
+ this.stream.addEventListener("addtrack", this.onaddtrack);
+ this.data = new Uint8Array(this.analyser.frequencyBinCount);
+}
+
+AudioStreamAnalyser.prototype = {
+ /**
+ * Get an array of frequency domain data for our stream's audio track.
+ *
+ * @returns {array} A Uint8Array containing the frequency domain data.
+ */
+ getByteFrequencyData() {
+ this.analyser.getByteFrequencyData(this.data);
+ return this.data;
+ },
+
+ /**
+ * Append a canvas to the DOM where the frequency data are drawn.
+ * Useful to debug tests.
+ */
+ enableDebugCanvas() {
+ var cvs = (this.debugCanvas = document.createElement("canvas"));
+ const content = document.getElementById("content");
+ content.insertBefore(cvs, content.children[0]);
+
+ // Easy: 1px per bin
+ cvs.width = this.analyser.frequencyBinCount;
+ cvs.height = 128;
+ cvs.style.border = "1px solid red";
+
+ var c = cvs.getContext("2d");
+ c.fillStyle = "black";
+
+ var self = this;
+ function render() {
+ c.clearRect(0, 0, cvs.width, cvs.height);
+ var array = self.getByteFrequencyData();
+ for (var i = 0; i < array.length; i++) {
+ c.fillRect(i, cvs.height - array[i] / 2, 1, cvs.height);
+ }
+ if (!cvs.stopDrawing) {
+ requestAnimationFrame(render);
+ }
+ }
+ requestAnimationFrame(render);
+ },
+
+ /**
+ * Stop drawing of and remove the debug canvas from the DOM if it was
+ * previously added.
+ */
+ disableDebugCanvas() {
+ if (!this.debugCanvas || !this.debugCanvas.parentElement) {
+ return;
+ }
+
+ this.debugCanvas.stopDrawing = true;
+ this.debugCanvas.parentElement.removeChild(this.debugCanvas);
+ },
+
+ /**
+ * Disconnects the input stream from our internal analyser node.
+ * Call this to reduce main thread processing, mostly necessary on slow
+ * devices.
+ */
+ disconnect() {
+ this.disableDebugCanvas();
+ this.sourceNodes.forEach(n => n.disconnect());
+ this.sourceNodes = [];
+ this.stream.removeEventListener("addtrack", this.onaddtrack);
+ },
+
+ /**
+ * Return a Promise, that will be resolved when the function passed as
+ * argument, when called, returns true (meaning the analysis was a
+ * success). The promise is rejected if the cancel promise resolves first.
+ *
+ * @param {function} analysisFunction
+ * A function that performs an analysis, and resolves with true if the
+ * analysis was a success (i.e. it found what it was looking for)
+ * @param {promise} cancel
+ * A promise that on resolving will reject the promise we returned.
+ */
+ async waitForAnalysisSuccess(
+ analysisFunction,
+ cancel = wait(60000, new Error("Audio analysis timed out"))
+ ) {
+ let aborted = false;
+ cancel.then(() => (aborted = true));
+
+ // We need to give the Analyser some time to start gathering data.
+ await wait(200);
+
+ do {
+ await new Promise(resolve => requestAnimationFrame(resolve));
+ if (aborted) {
+ throw await cancel;
+ }
+ } while (!analysisFunction(this.getByteFrequencyData()));
+ },
+
+ /**
+ * Return the FFT bin index for a given frequency.
+ *
+ * @param {double} frequency
+ * The frequency for whicht to return the bin number.
+ * @returns {integer} the index of the bin in the FFT array.
+ */
+ binIndexForFrequency(frequency) {
+ return (
+ 1 +
+ Math.round(
+ (frequency * this.analyser.fftSize) / this.audioContext.sampleRate
+ )
+ );
+ },
+
+ /**
+ * Reverse operation, get the frequency for a bin index.
+ *
+ * @param {integer} index an index in an FFT array
+ * @returns {double} the frequency for this bin
+ */
+ frequencyForBinIndex(index) {
+ return ((index - 1) * this.audioContext.sampleRate) / this.analyser.fftSize;
+ },
+};
+
+/**
+ * Creates a MediaStream with an audio track containing a sine tone at the
+ * given frequency.
+ *
+ * @param {AudioContext} ac
+ * AudioContext in which to create the OscillatorNode backing the stream
+ * @param {double} frequency
+ * The frequency in Hz of the generated sine tone
+ * @returns {MediaStream} the MediaStream containing sine tone audio track
+ */
+function createOscillatorStream(ac, frequency) {
+ var osc = ac.createOscillator();
+ osc.frequency.value = frequency;
+
+ var oscDest = ac.createMediaStreamDestination();
+ osc.connect(oscDest);
+ osc.start();
+ return oscDest.stream;
+}
+
+/**
+ * Create the necessary HTML elements for head and body as used by Mochitests
+ *
+ * @param {object} meta
+ * Meta information of the test
+ * @param {string} meta.title
+ * Description of the test
+ * @param {string} [meta.bug]
+ * Bug the test was created for
+ * @param {boolean} [meta.visible=false]
+ * Visibility of the media elements
+ */
+function realCreateHTML(meta) {
+ var test = document.getElementById("test");
+
+ // Create the head content
+ var elem = document.createElement("meta");
+ elem.setAttribute("charset", "utf-8");
+ document.head.appendChild(elem);
+
+ var title = document.createElement("title");
+ title.textContent = meta.title;
+ document.head.appendChild(title);
+
+ // Create the body content
+ var anchor = document.createElement("a");
+ anchor.textContent = meta.title;
+ if (meta.bug) {
+ anchor.setAttribute(
+ "href",
+ "https://bugzilla.mozilla.org/show_bug.cgi?id=" + meta.bug
+ );
+ } else {
+ anchor.setAttribute("target", "_blank");
+ }
+
+ document.body.insertBefore(anchor, test);
+
+ var display = document.createElement("p");
+ display.setAttribute("id", "display");
+ document.body.insertBefore(display, test);
+
+ var content = document.createElement("div");
+ content.setAttribute("id", "content");
+ content.style.display = meta.visible ? "block" : "none";
+ document.body.appendChild(content);
+}
+
+/**
+ * Creates an element of the given type, assigns the given id, sets the controls
+ * and autoplay attributes and adds it to the content node.
+ *
+ * @param {string} type
+ * Defining if we should create an "audio" or "video" element
+ * @param {string} id
+ * A string to use as the element id.
+ */
+function createMediaElement(type, id) {
+ const element = document.createElement(type);
+ element.setAttribute("id", id);
+ element.setAttribute("height", 100);
+ element.setAttribute("width", 150);
+ element.setAttribute("controls", "controls");
+ element.setAttribute("autoplay", "autoplay");
+ element.setAttribute("muted", "muted");
+ element.muted = true;
+ document.getElementById("content").appendChild(element);
+
+ return element;
+}
+
+/**
+ * Returns an existing element for the given track with the given idPrefix,
+ * as it was added by createMediaElementForTrack().
+ *
+ * @param {MediaStreamTrack} track
+ * Track used as the element's source.
+ * @param {string} idPrefix
+ * A string to use as the element id. The track id will also be appended.
+ */
+function getMediaElementForTrack(track, idPrefix) {
+ return document.getElementById(idPrefix + "_" + track.id);
+}
+
+/**
+ * Create a media element with a track as source and attach it to the content
+ * node.
+ *
+ * @param {MediaStreamTrack} track
+ * Track for use as source.
+ * @param {string} idPrefix
+ * A string to use as the element id. The track id will also be appended.
+ * @return {HTMLMediaElement} The created HTML media element
+ */
+function createMediaElementForTrack(track, idPrefix) {
+ const id = idPrefix + "_" + track.id;
+ const element = createMediaElement(track.kind, id);
+ element.srcObject = new MediaStream([track]);
+
+ return element;
+}
+
+/**
+ * Wrapper function for mediaDevices.getUserMedia used by some tests. Whether
+ * to use fake devices or not is now determined in pref further below instead.
+ *
+ * @param {Dictionary} constraints
+ * The constraints for this mozGetUserMedia callback
+ */
+function getUserMedia(constraints) {
+ // Tests may have changed the values of prefs, so recheck
+ updateConfigFromFakeAndLoopbackPrefs();
+ if (
+ !WANT_FAKE_AUDIO &&
+ !constraints.fake &&
+ constraints.audio &&
+ !DISABLE_LOOPBACK_TONE
+ ) {
+ // Loopback device is configured, start the default loopback tone
+ if (!DefaultLoopbackTone) {
+ TEST_AUDIO_FREQ = 440;
+ DefaultLoopbackTone = new LoopbackTone(
+ new AudioContext(),
+ TEST_AUDIO_FREQ
+ );
+ DefaultLoopbackTone.start();
+ }
+ // Disable input processing mode when it's not explicity enabled.
+ // This is to avoid distortion of the loopback tone
+ constraints.audio = Object.assign(
+ {},
+ { autoGainControl: false },
+ { echoCancellation: false },
+ { noiseSuppression: false },
+ constraints.audio
+ );
+ } else {
+ // Fake device configured, ensure our test freq is correct.
+ TEST_AUDIO_FREQ = 1000;
+ }
+ info("Call getUserMedia for " + JSON.stringify(constraints));
+ return navigator.mediaDevices
+ .getUserMedia(constraints)
+ .then(stream => (checkMediaStreamTracks(constraints, stream), stream));
+}
+
+// These are the promises we use to track that the prerequisites for the test
+// are in place before running it.
+var setTestOptions;
+var testConfigured = new Promise(r => (setTestOptions = r));
+
+function pushPrefs(...p) {
+ return SpecialPowers.pushPrefEnv({ set: p });
+}
+
+async function withPrefs(prefs, func) {
+ await SpecialPowers.pushPrefEnv({ set: prefs });
+ try {
+ return await func();
+ } finally {
+ await SpecialPowers.popPrefEnv();
+ }
+}
+
+function setupEnvironment() {
+ var defaultMochitestPrefs = {
+ set: [
+ ["media.peerconnection.enabled", true],
+ ["media.peerconnection.identity.enabled", true],
+ ["media.peerconnection.identity.timeout", 120000],
+ ["media.peerconnection.ice.stun_client_maximum_transmits", 14],
+ ["media.peerconnection.ice.trickle_grace_period", 30000],
+ ["media.navigator.permission.disabled", true],
+ // If either fake audio or video is desired we enable fake streams.
+ // If loopback devices are set they will be chosen instead of fakes in gecko.
+ ["media.navigator.streams.fake", WANT_FAKE_AUDIO || WANT_FAKE_VIDEO],
+ ["media.getusermedia.audiocapture.enabled", true],
+ ["media.getusermedia.screensharing.enabled", true],
+ ["media.getusermedia.window.focus_source.enabled", false],
+ ["media.recorder.audio_node.enabled", true],
+ ["media.peerconnection.ice.obfuscate_host_addresses", false],
+ ["media.peerconnection.nat_simulator.filtering_type", ""],
+ ["media.peerconnection.nat_simulator.mapping_type", ""],
+ ["media.peerconnection.nat_simulator.block_tcp", false],
+ ["media.peerconnection.nat_simulator.block_udp", false],
+ ["media.peerconnection.nat_simulator.redirect_address", ""],
+ ["media.peerconnection.nat_simulator.redirect_targets", ""],
+ ],
+ };
+
+ if (navigator.userAgent.includes("Android")) {
+ defaultMochitestPrefs.set.push(
+ ["media.navigator.video.default_width", 320],
+ ["media.navigator.video.default_height", 240],
+ ["media.navigator.video.max_fr", 10],
+ ["media.autoplay.default", Ci.nsIAutoplay.ALLOWED]
+ );
+ }
+
+ // Platform codec prefs should be matched because fake H.264 GMP codec doesn't
+ // produce/consume real bitstreams. [TODO] remove after bug 1509012 is fixed.
+ const platformEncoderEnabled = SpecialPowers.getBoolPref(
+ "media.webrtc.platformencoder"
+ );
+ defaultMochitestPrefs.set.push([
+ "media.navigator.mediadatadecoder_h264_enabled",
+ platformEncoderEnabled,
+ ]);
+
+ // Running as a Mochitest.
+ SimpleTest.requestFlakyTimeout("WebRTC inherently depends on timeouts");
+ window.finish = () => SimpleTest.finish();
+ SpecialPowers.pushPrefEnv(defaultMochitestPrefs, setTestOptions);
+
+ // We don't care about waiting for this to complete, we just want to ensure
+ // that we don't build up a huge backlog of GC work.
+ SpecialPowers.exactGC();
+}
+
+// [TODO] remove after bug 1509012 is fixed.
+async function matchPlatformH264CodecPrefs() {
+ const hasHW264 =
+ SpecialPowers.getBoolPref("media.webrtc.platformencoder") &&
+ !SpecialPowers.getBoolPref("media.webrtc.platformencoder.sw_only") &&
+ (navigator.userAgent.includes("Android") ||
+ navigator.userAgent.includes("Mac OS X"));
+
+ await pushPrefs(
+ ["media.webrtc.platformencoder", hasHW264],
+ ["media.navigator.mediadatadecoder_h264_enabled", hasHW264]
+ );
+}
+
+async function runTestWhenReady(testFunc) {
+ setupEnvironment();
+ const options = await testConfigured;
+ try {
+ await testFunc(options);
+ } catch (e) {
+ ok(
+ false,
+ `Error executing test: ${e}
+${e.stack ? e.stack : ""}`
+ );
+ } finally {
+ SimpleTest.finish();
+ }
+}
+
+/**
+ * Checks that the media stream tracks have the expected amount of tracks
+ * with the correct attributes based on the type and constraints given.
+ *
+ * @param {Object} constraints specifies whether the stream should have
+ * audio, video, or both
+ * @param {String} type the type of media stream tracks being checked
+ * @param {sequence<MediaStreamTrack>} mediaStreamTracks the media stream
+ * tracks being checked
+ */
+function checkMediaStreamTracksByType(constraints, type, mediaStreamTracks) {
+ if (constraints[type]) {
+ is(mediaStreamTracks.length, 1, "One " + type + " track shall be present");
+
+ if (mediaStreamTracks.length) {
+ is(mediaStreamTracks[0].kind, type, "Track kind should be " + type);
+ ok(mediaStreamTracks[0].id, "Track id should be defined");
+ ok(!mediaStreamTracks[0].muted, "Track should not be muted");
+ }
+ } else {
+ is(mediaStreamTracks.length, 0, "No " + type + " tracks shall be present");
+ }
+}
+
+/**
+ * Check that the given media stream contains the expected media stream
+ * tracks given the associated audio & video constraints provided.
+ *
+ * @param {Object} constraints specifies whether the stream should have
+ * audio, video, or both
+ * @param {MediaStream} mediaStream the media stream being checked
+ */
+function checkMediaStreamTracks(constraints, mediaStream) {
+ checkMediaStreamTracksByType(
+ constraints,
+ "audio",
+ mediaStream.getAudioTracks()
+ );
+ checkMediaStreamTracksByType(
+ constraints,
+ "video",
+ mediaStream.getVideoTracks()
+ );
+}
+
+/**
+ * Check that a media stream contains exactly a set of media stream tracks.
+ *
+ * @param {MediaStream} mediaStream the media stream being checked
+ * @param {Array} tracks the tracks that should exist in mediaStream
+ * @param {String} [message] an optional message to pass to asserts
+ */
+function checkMediaStreamContains(mediaStream, tracks, message) {
+ message = message ? message + ": " : "";
+ tracks.forEach(t =>
+ ok(
+ mediaStream.getTrackById(t.id),
+ message + "MediaStream " + mediaStream.id + " contains track " + t.id
+ )
+ );
+ is(
+ mediaStream.getTracks().length,
+ tracks.length,
+ message + "MediaStream " + mediaStream.id + " contains no extra tracks"
+ );
+}
+
+function checkMediaStreamCloneAgainstOriginal(clone, original) {
+ isnot(clone.id.length, 0, "Stream clone should have an id string");
+ isnot(clone, original, "Stream clone should be different from the original");
+ isnot(
+ clone.id,
+ original.id,
+ "Stream clone's id should be different from the original's"
+ );
+ is(
+ clone.getAudioTracks().length,
+ original.getAudioTracks().length,
+ "All audio tracks should get cloned"
+ );
+ is(
+ clone.getVideoTracks().length,
+ original.getVideoTracks().length,
+ "All video tracks should get cloned"
+ );
+ is(clone.active, original.active, "Active state should be preserved");
+ original
+ .getTracks()
+ .forEach(t =>
+ ok(!clone.getTrackById(t.id), "The clone's tracks should be originals")
+ );
+}
+
+function checkMediaStreamTrackCloneAgainstOriginal(clone, original) {
+ isnot(clone.id.length, 0, "Track clone should have an id string");
+ isnot(clone, original, "Track clone should be different from the original");
+ isnot(
+ clone.id,
+ original.id,
+ "Track clone's id should be different from the original's"
+ );
+ is(
+ clone.kind,
+ original.kind,
+ "Track clone's kind should be same as the original's"
+ );
+ is(
+ clone.enabled,
+ original.enabled,
+ "Track clone's kind should be same as the original's"
+ );
+ is(
+ clone.readyState,
+ original.readyState,
+ "Track clone's readyState should be same as the original's"
+ );
+ is(
+ clone.muted,
+ original.muted,
+ "Track clone's muted state should be same as the original's"
+ );
+}
+
+/*** Utility methods */
+
+/** The dreadful setTimeout, use sparingly */
+function wait(time, message) {
+ return new Promise(r => setTimeout(() => r(message), time));
+}
+
+/** The even more dreadful setInterval, use even more sparingly */
+function waitUntil(func, time) {
+ return new Promise(resolve => {
+ var interval = setInterval(() => {
+ if (func()) {
+ clearInterval(interval);
+ resolve();
+ }
+ }, time || 200);
+ });
+}
+
+/** Time out while waiting for a promise to get resolved or rejected. */
+var timeout = (promise, time, msg) =>
+ Promise.race([
+ promise,
+ wait(time).then(() => Promise.reject(new Error(msg))),
+ ]);
+
+/** Adds a |finally| function to a promise whose argument is invoked whether the
+ * promise is resolved or rejected, and that does not interfere with chaining.*/
+var addFinallyToPromise = promise => {
+ promise.finally = func => {
+ return promise.then(
+ result => {
+ func();
+ return Promise.resolve(result);
+ },
+ error => {
+ func();
+ return Promise.reject(error);
+ }
+ );
+ };
+ return promise;
+};
+
+/** Use event listener to call passed-in function on fire until it returns true */
+var listenUntil = (target, eventName, onFire) => {
+ return new Promise(resolve =>
+ target.addEventListener(eventName, function callback(event) {
+ var result = onFire(event);
+ if (result) {
+ target.removeEventListener(eventName, callback);
+ resolve(result);
+ }
+ })
+ );
+};
+
+/* Test that a function throws the right error */
+function mustThrowWith(msg, reason, f) {
+ try {
+ f();
+ ok(false, msg + " must throw");
+ } catch (e) {
+ is(e.name, reason, msg + " must throw: " + e.message);
+ }
+}
+
+/* Get a dummy audio track */
+function getSilentTrack() {
+ let ctx = new AudioContext(),
+ oscillator = ctx.createOscillator();
+ let dst = oscillator.connect(ctx.createMediaStreamDestination());
+ oscillator.start();
+ return Object.assign(dst.stream.getAudioTracks()[0], { enabled: false });
+}
+
+function getBlackTrack({ width = 640, height = 480 } = {}) {
+ let canvas = Object.assign(document.createElement("canvas"), {
+ width,
+ height,
+ });
+ canvas.getContext("2d").fillRect(0, 0, width, height);
+ let stream = canvas.captureStream();
+ return Object.assign(stream.getVideoTracks()[0], { enabled: false });
+}
+
+/*** Test control flow methods */
+
+/**
+ * Generates a callback function fired only under unexpected circumstances
+ * while running the tests. The generated function kills off the test as well
+ * gracefully.
+ *
+ * @param {String} [message]
+ * An optional message to show if no object gets passed into the
+ * generated callback method.
+ */
+function generateErrorCallback(message) {
+ var stack = new Error().stack.split("\n");
+ stack.shift(); // Don't include this instantiation frame
+
+ /**
+ * @param {object} aObj
+ * The object fired back from the callback
+ */
+ return aObj => {
+ if (aObj) {
+ if (aObj.name && aObj.message) {
+ ok(
+ false,
+ "Unexpected callback for '" +
+ aObj.name +
+ "' with message = '" +
+ aObj.message +
+ "' at " +
+ JSON.stringify(stack)
+ );
+ } else {
+ ok(
+ false,
+ "Unexpected callback with = '" +
+ aObj +
+ "' at: " +
+ JSON.stringify(stack)
+ );
+ }
+ } else {
+ ok(
+ false,
+ "Unexpected callback with message = '" +
+ message +
+ "' at: " +
+ JSON.stringify(stack)
+ );
+ }
+ throw new Error("Unexpected callback");
+ };
+}
+
+var unexpectedEventArrived;
+var rejectOnUnexpectedEvent = new Promise((x, reject) => {
+ unexpectedEventArrived = reject;
+});
+
+/**
+ * Generates a callback function fired only for unexpected events happening.
+ *
+ * @param {String} description
+ Description of the object for which the event has been fired
+ * @param {String} eventName
+ Name of the unexpected event
+ */
+function unexpectedEvent(message, eventName) {
+ var stack = new Error().stack.split("\n");
+ stack.shift(); // Don't include this instantiation frame
+
+ return e => {
+ var details =
+ "Unexpected event '" +
+ eventName +
+ "' fired with message = '" +
+ message +
+ "' at: " +
+ JSON.stringify(stack);
+ ok(false, details);
+ unexpectedEventArrived(new Error(details));
+ };
+}
+
+/**
+ * Implements the one-shot event pattern used throughout. Each of the 'onxxx'
+ * attributes on the wrappers can be set with a custom handler. Prior to the
+ * handler being set, if the event fires, it causes the test execution to halt.
+ * That handler is used exactly once, after which the original, error-generating
+ * handler is re-installed. Thus, each event handler is used at most once.
+ *
+ * @param {object} wrapper
+ * The wrapper on which the psuedo-handler is installed
+ * @param {object} obj
+ * The real source of events
+ * @param {string} event
+ * The name of the event
+ */
+function createOneShotEventWrapper(wrapper, obj, event) {
+ var onx = "on" + event;
+ var unexpected = unexpectedEvent(wrapper, event);
+ wrapper[onx] = unexpected;
+ obj[onx] = e => {
+ info(wrapper + ': "on' + event + '" event fired');
+ e.wrapper = wrapper;
+ wrapper[onx](e);
+ wrapper[onx] = unexpected;
+ };
+}
+
+/**
+ * Returns a promise that resolves when `target` has raised an event with the
+ * given name the given number of times. Cancel the returned promise by passing
+ * in a `cancel` promise and resolving it.
+ *
+ * @param {object} target
+ * The target on which the event should occur.
+ * @param {string} name
+ * The name of the event that should occur.
+ * @param {integer} count
+ * Optional number of times the event should be raised before resolving.
+ * @param {promise} cancel
+ * Optional promise that on resolving rejects the returned promise,
+ * so we can avoid logging results after a test has finished.
+ * @returns {promise} A promise that resolves to the last of the seen events.
+ */
+function haveEvents(target, name, count, cancel) {
+ var listener;
+ var counter = count || 1;
+ return Promise.race([
+ (cancel || new Promise(() => {})).then(e => Promise.reject(e)),
+ new Promise(resolve =>
+ target.addEventListener(
+ name,
+ (listener = e => --counter < 1 && resolve(e))
+ )
+ ),
+ ]).then(e => (target.removeEventListener(name, listener), e));
+}
+
+/**
+ * Returns a promise that resolves when `target` has raised an event with the
+ * given name. Cancel the returned promise by passing in a `cancel` promise and
+ * resolving it.
+ *
+ * @param {object} target
+ * The target on which the event should occur.
+ * @param {string} name
+ * The name of the event that should occur.
+ * @param {promise} cancel
+ * Optional promise that on resolving rejects the returned promise,
+ * so we can avoid logging results after a test has finished.
+ * @returns {promise} A promise that resolves to the seen event.
+ */
+function haveEvent(target, name, cancel) {
+ return haveEvents(target, name, 1, cancel);
+}
+
+/**
+ * Returns a promise that resolves if the target has not seen the given event
+ * after one crank (or until the given timeoutPromise resolves) of the event
+ * loop.
+ *
+ * @param {object} target
+ * The target on which the event should not occur.
+ * @param {string} name
+ * The name of the event that should not occur.
+ * @param {promise} timeoutPromise
+ * Optional promise defining how long we should wait before resolving.
+ * @returns {promise} A promise that is rejected if we see the given event, or
+ * resolves after a timeout otherwise.
+ */
+function haveNoEvent(target, name, timeoutPromise) {
+ return haveEvent(target, name, timeoutPromise || wait(0)).then(
+ () => Promise.reject(new Error("Too many " + name + " events")),
+ () => {}
+ );
+}
+
+/**
+ * Returns a promise that resolves after the target has seen the given number
+ * of events but no such event in a following crank of the event loop.
+ *
+ * @param {object} target
+ * The target on which the events should occur.
+ * @param {string} name
+ * The name of the event that should occur.
+ * @param {integer} count
+ * Optional number of times the event should be raised before resolving.
+ * @param {promise} cancel
+ * Optional promise that on resolving rejects the returned promise,
+ * so we can avoid logging results after a test has finished.
+ * @returns {promise} A promise that resolves to the last of the seen events.
+ */
+function haveEventsButNoMore(target, name, count, cancel) {
+ return haveEvents(target, name, count, cancel).then(e =>
+ haveNoEvent(target, name).then(() => e)
+ );
+}
+
+/*
+ * Resolves the returned promise with an object with usage and reportCount
+ * properties. `usage` is in the same units as reported by the reporter for
+ * `path`.
+ */
+const collectMemoryUsage = async path => {
+ const MemoryReporterManager = Cc[
+ "@mozilla.org/memory-reporter-manager;1"
+ ].getService(Ci.nsIMemoryReporterManager);
+
+ let usage = 0;
+ let reportCount = 0;
+ await new Promise(resolve =>
+ MemoryReporterManager.getReports(
+ (aProcess, aPath, aKind, aUnits, aAmount, aDesc) => {
+ if (aPath != path) {
+ return;
+ }
+ ++reportCount;
+ usage += aAmount;
+ },
+ null,
+ resolve,
+ null,
+ /* anonymized = */ false
+ )
+ );
+ return { usage, reportCount };
+};
+
+// Some DNS helper functions
+const dnsLookup = async hostname => {
+ // Convenience API for various networking related stuff. _Almost_ convenient
+ // enough.
+ const neckoDashboard = SpecialPowers.Cc[
+ "@mozilla.org/network/dashboard;1"
+ ].getService(Ci.nsIDashboard);
+
+ const results = await new Promise(r => {
+ neckoDashboard.requestDNSLookup(hostname, results => {
+ r(SpecialPowers.wrap(results));
+ });
+ });
+
+ // |address| is an array-like dictionary (ie; keys are all integers).
+ // We convert to an array to make it less unwieldy.
+ const addresses = [...results.address];
+ info(`DNS results for ${hostname}: ${JSON.stringify(addresses)}`);
+ return addresses;
+};
+
+const dnsLookupV4 = async hostname => {
+ const addresses = await dnsLookup(hostname);
+ return addresses.filter(address => !address.includes(":"));
+};
+
+const dnsLookupV6 = async hostname => {
+ const addresses = await dnsLookup(hostname);
+ return addresses.filter(address => address.includes(":"));
+};
+
+const getTurnHostname = turnUrl => {
+ const urlNoParams = turnUrl.split("?")[0];
+ // Strip off scheme
+ const hostAndMaybePort = urlNoParams.split(":", 2)[1];
+ if (hostAndMaybePort[0] == "[") {
+ // IPV6 literal, strip out '[', and split at closing ']'
+ return hostAndMaybePort.substring(1).split("]")[0];
+ }
+ return hostAndMaybePort.split(":")[0];
+};
+
+// Yo dawg I heard you like yo dawg I heard you like Proxies
+// Example: let value = await GleanTest.category.metric.testGetValue();
+// For labeled metrics:
+// let value = await GleanTest.category.metric["label"].testGetValue();
+// Please don't try to use the string "testGetValue" as a label.
+const GleanTest = new Proxy(
+ {},
+ {
+ get(target, categoryName, receiver) {
+ return new Proxy(
+ {},
+ {
+ get(target, metricName, receiver) {
+ return new Proxy(
+ {
+ async testGetValue() {
+ return SpecialPowers.spawnChrome(
+ [categoryName, metricName],
+ async (categoryName, metricName) => {
+ await Services.fog.testFlushAllChildren();
+ const window = this.browsingContext.topChromeWindow;
+ return window.Glean[categoryName][
+ metricName
+ ].testGetValue();
+ }
+ );
+ },
+ },
+ {
+ get(target, prop, receiver) {
+ // The only prop that will be there is testGetValue, but we
+ // might add more later.
+ if (prop in target) {
+ return target[prop];
+ }
+
+ // |prop| must be a label?
+ const label = prop;
+ return {
+ async testGetValue() {
+ return SpecialPowers.spawnChrome(
+ [categoryName, metricName, label],
+ async (categoryName, metricName, label) => {
+ await Services.fog.testFlushAllChildren();
+ const window = this.browsingContext.topChromeWindow;
+ return window.Glean[categoryName][metricName][
+ label
+ ].testGetValue();
+ }
+ );
+ },
+ };
+ },
+ }
+ );
+ },
+ }
+ );
+ },
+ }
+);
+
+/**
+ * This class executes a series of functions in a continuous sequence.
+ * Promise-bearing functions are executed after the previous promise completes.
+ *
+ * @constructor
+ * @param {object} framework
+ * A back reference to the framework which makes use of the class. It is
+ * passed to each command callback.
+ * @param {function[]} commandList
+ * Commands to set during initialization
+ */
+function CommandChain(framework, commandList) {
+ this._framework = framework;
+ this.commands = commandList || [];
+}
+
+CommandChain.prototype = {
+ /**
+ * Start the command chain. This returns a promise that always resolves
+ * cleanly (this catches errors and fails the test case).
+ */
+ execute() {
+ return this.commands
+ .reduce((prev, next, i) => {
+ if (typeof next !== "function" || !next.name) {
+ throw new Error("registered non-function" + next);
+ }
+
+ return prev.then(() => {
+ info("Run step " + (i + 1) + ": " + next.name);
+ return Promise.race([next(this._framework), rejectOnUnexpectedEvent]);
+ });
+ }, Promise.resolve())
+ .catch(e =>
+ ok(
+ false,
+ "Error in test execution: " +
+ e +
+ (typeof e.stack === "string"
+ ? " " + e.stack.split("\n").join(" ... ")
+ : "")
+ )
+ );
+ },
+
+ /**
+ * Add new commands to the end of the chain
+ */
+ append(commands) {
+ this.commands = this.commands.concat(commands);
+ },
+
+ /**
+ * Returns the index of the specified command in the chain.
+ * @param {occurrence} Optional param specifying which occurrence to match,
+ * with 0 representing the first occurrence.
+ */
+ indexOf(functionOrName, occurrence) {
+ occurrence = occurrence || 0;
+ return this.commands.findIndex(func => {
+ if (typeof functionOrName === "string") {
+ if (func.name !== functionOrName) {
+ return false;
+ }
+ } else if (func !== functionOrName) {
+ return false;
+ }
+ if (occurrence) {
+ --occurrence;
+ return false;
+ }
+ return true;
+ });
+ },
+
+ mustHaveIndexOf(functionOrName, occurrence) {
+ var index = this.indexOf(functionOrName, occurrence);
+ if (index == -1) {
+ throw new Error("Unknown test: " + functionOrName);
+ }
+ return index;
+ },
+
+ /**
+ * Inserts the new commands after the specified command.
+ */
+ insertAfter(functionOrName, commands, all, occurrence) {
+ this._insertHelper(functionOrName, commands, 1, all, occurrence);
+ },
+
+ /**
+ * Inserts the new commands after every occurrence of the specified command
+ */
+ insertAfterEach(functionOrName, commands) {
+ this._insertHelper(functionOrName, commands, 1, true);
+ },
+
+ /**
+ * Inserts the new commands before the specified command.
+ */
+ insertBefore(functionOrName, commands, all, occurrence) {
+ this._insertHelper(functionOrName, commands, 0, all, occurrence);
+ },
+
+ _insertHelper(functionOrName, commands, delta, all, occurrence) {
+ occurrence = occurrence || 0;
+ for (
+ var index = this.mustHaveIndexOf(functionOrName, occurrence);
+ index !== -1;
+ index = this.indexOf(functionOrName, ++occurrence)
+ ) {
+ this.commands = [].concat(
+ this.commands.slice(0, index + delta),
+ commands,
+ this.commands.slice(index + delta)
+ );
+ if (!all) {
+ break;
+ }
+ }
+ },
+
+ /**
+ * Removes the specified command, returns what was removed.
+ */
+ remove(functionOrName, occurrence) {
+ return this.commands.splice(
+ this.mustHaveIndexOf(functionOrName, occurrence),
+ 1
+ );
+ },
+
+ /**
+ * Removes all commands after the specified one, returns what was removed.
+ */
+ removeAfter(functionOrName, occurrence) {
+ return this.commands.splice(
+ this.mustHaveIndexOf(functionOrName, occurrence) + 1
+ );
+ },
+
+ /**
+ * Removes all commands before the specified one, returns what was removed.
+ */
+ removeBefore(functionOrName, occurrence) {
+ return this.commands.splice(
+ 0,
+ this.mustHaveIndexOf(functionOrName, occurrence)
+ );
+ },
+
+ /**
+ * Replaces a single command, returns what was removed.
+ */
+ replace(functionOrName, commands) {
+ this.insertBefore(functionOrName, commands);
+ return this.remove(functionOrName);
+ },
+
+ /**
+ * Replaces all commands after the specified one, returns what was removed.
+ */
+ replaceAfter(functionOrName, commands, occurrence) {
+ var oldCommands = this.removeAfter(functionOrName, occurrence);
+ this.append(commands);
+ return oldCommands;
+ },
+
+ /**
+ * Replaces all commands before the specified one, returns what was removed.
+ */
+ replaceBefore(functionOrName, commands) {
+ var oldCommands = this.removeBefore(functionOrName);
+ this.insertBefore(functionOrName, commands);
+ return oldCommands;
+ },
+
+ /**
+ * Remove all commands whose name match the specified regex.
+ */
+ filterOut(id_match) {
+ this.commands = this.commands.filter(c => !id_match.test(c.name));
+ },
+};
+
+function AudioStreamHelper() {
+ this._context = new AudioContext();
+}
+
+AudioStreamHelper.prototype = {
+ checkAudio(stream, analyser, fun) {
+ /*
+ analyser.enableDebugCanvas();
+ return analyser.waitForAnalysisSuccess(fun)
+ .then(() => analyser.disableDebugCanvas());
+ */
+ return analyser.waitForAnalysisSuccess(fun);
+ },
+
+ checkAudioFlowing(stream) {
+ var analyser = new AudioStreamAnalyser(this._context, stream);
+ var freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ);
+ return this.checkAudio(stream, analyser, array => array[freq] > 200);
+ },
+
+ checkAudioNotFlowing(stream) {
+ var analyser = new AudioStreamAnalyser(this._context, stream);
+ var freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ);
+ return this.checkAudio(stream, analyser, array => array[freq] < 50);
+ },
+};
+
+class VideoFrameEmitter {
+ constructor(color1, color2, width, height) {
+ if (!width) {
+ width = 50;
+ }
+ if (!height) {
+ height = width;
+ }
+ this._helper = new CaptureStreamTestHelper2D(width, height);
+ this._canvas = this._helper.createAndAppendElement(
+ "canvas",
+ "source_canvas"
+ );
+ this._canvas.width = width;
+ this._canvas.height = height;
+ this._color1 = color1 ? color1 : this._helper.green;
+ this._color2 = color2 ? color2 : this._helper.red;
+ // Make sure this is initted
+ this._helper.drawColor(this._canvas, this._color1);
+ this._stream = this._canvas.captureStream();
+ this._started = false;
+ }
+
+ stream() {
+ return this._stream;
+ }
+
+ helper() {
+ return this._helper;
+ }
+
+ colors(color1, color2) {
+ this._color1 = color1 ? color1 : this._helper.green;
+ this._color2 = color2 ? color2 : this._helper.red;
+ try {
+ this._helper.drawColor(this._canvas, this._color1);
+ } catch (e) {
+ // ignore; stream might have shut down
+ }
+ }
+
+ size(width, height) {
+ this._canvas.width = width;
+ this._canvas.height = height;
+ }
+
+ start() {
+ if (this._started) {
+ info("*** emitter already started");
+ return;
+ }
+
+ let i = 0;
+ this._started = true;
+ this._intervalId = setInterval(() => {
+ try {
+ this._helper.drawColor(this._canvas, i ? this._color1 : this._color2);
+ i = 1 - i;
+ } catch (e) {
+ // ignore; stream might have shut down, and we don't bother clearing
+ // the setInterval.
+ }
+ }, 500);
+ }
+
+ stop() {
+ if (this._started) {
+ clearInterval(this._intervalId);
+ this._started = false;
+ }
+ }
+}
+
+class VideoStreamHelper {
+ constructor() {
+ this._helper = new CaptureStreamTestHelper2D(50, 50);
+ }
+
+ async checkHasFrame(video, { offsetX, offsetY, threshold } = {}) {
+ const h = this._helper;
+ await h.waitForPixel(
+ video,
+ px => {
+ let result = h.isOpaquePixelNot(px, h.black, threshold);
+ info(
+ "Checking that we have a frame, got [" +
+ Array.from(px) +
+ "]. Ref=[" +
+ Array.from(h.black.data) +
+ "]. Threshold=" +
+ threshold +
+ ". Pass=" +
+ result
+ );
+ return result;
+ },
+ { offsetX, offsetY }
+ );
+ }
+
+ async checkVideoPlaying(
+ video,
+ { offsetX = 10, offsetY = 10, threshold = 16 } = {}
+ ) {
+ const h = this._helper;
+ await this.checkHasFrame(video, { offsetX, offsetY, threshold });
+ let startPixel = {
+ data: h.getPixel(video, offsetX, offsetY),
+ name: "startcolor",
+ };
+ await h.waitForPixel(
+ video,
+ px => {
+ let result = h.isPixelNot(px, startPixel, threshold);
+ info(
+ "Checking playing, [" +
+ Array.from(px) +
+ "] vs [" +
+ Array.from(startPixel.data) +
+ "]. Threshold=" +
+ threshold +
+ " Pass=" +
+ result
+ );
+ return result;
+ },
+ { offsetX, offsetY }
+ );
+ }
+
+ async checkVideoPaused(
+ video,
+ { offsetX = 10, offsetY = 10, threshold = 16, time = 5000 } = {}
+ ) {
+ const h = this._helper;
+ await this.checkHasFrame(video, { offsetX, offsetY, threshold });
+ let startPixel = {
+ data: h.getPixel(video, offsetX, offsetY),
+ name: "startcolor",
+ };
+ try {
+ await h.waitForPixel(
+ video,
+ px => {
+ let result = h.isOpaquePixelNot(px, startPixel, threshold);
+ info(
+ "Checking paused, [" +
+ Array.from(px) +
+ "] vs [" +
+ Array.from(startPixel.data) +
+ "]. Threshold=" +
+ threshold +
+ " Pass=" +
+ result
+ );
+ return result;
+ },
+ { offsetX, offsetY, cancel: wait(time, "timeout") }
+ );
+ ok(false, "Frame changed within " + time / 1000 + " seconds");
+ } catch (e) {
+ is(
+ e,
+ "timeout",
+ "Frame shouldn't change for " + time / 1000 + " seconds"
+ );
+ }
+ }
+}
+
+(function () {
+ var el = document.createElement("link");
+ el.rel = "stylesheet";
+ el.type = "text/css";
+ el.href = "/tests/SimpleTest/test.css";
+ document.head.appendChild(el);
+})();
diff --git a/dom/media/webrtc/tests/mochitests/helpers_from_wpt/sdp.js b/dom/media/webrtc/tests/mochitests/helpers_from_wpt/sdp.js
new file mode 100644
index 0000000000..6460f64a44
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/helpers_from_wpt/sdp.js
@@ -0,0 +1,889 @@
+/* eslint-env node */
+"use strict";
+
+// SDP helpers.
+var SDPUtils = {};
+
+// Generate an alphanumeric identifier for cname or mids.
+// TODO: use UUIDs instead? https://gist.github.com/jed/982883
+SDPUtils.generateIdentifier = function() {
+ return Math.random()
+ .toString(36)
+ .substr(2, 10);
+};
+
+// The RTCP CNAME used by all peerconnections from the same JS.
+SDPUtils.localCName = SDPUtils.generateIdentifier();
+
+// Splits SDP into lines, dealing with both CRLF and LF.
+SDPUtils.splitLines = function(blob) {
+ return blob
+ .trim()
+ .split("\n")
+ .map(function(line) {
+ return line.trim();
+ });
+};
+// Splits SDP into sessionpart and mediasections. Ensures CRLF.
+SDPUtils.splitSections = function(blob) {
+ var parts = blob.split("\nm=");
+ return parts.map(function(part, index) {
+ return (index > 0 ? "m=" + part : part).trim() + "\r\n";
+ });
+};
+
+// returns the session description.
+SDPUtils.getDescription = function(blob) {
+ var sections = SDPUtils.splitSections(blob);
+ return sections && sections[0];
+};
+
+// returns the individual media sections.
+SDPUtils.getMediaSections = function(blob) {
+ var sections = SDPUtils.splitSections(blob);
+ sections.shift();
+ return sections;
+};
+
+// Returns lines that start with a certain prefix.
+SDPUtils.matchPrefix = function(blob, prefix) {
+ return SDPUtils.splitLines(blob).filter(function(line) {
+ return line.indexOf(prefix) === 0;
+ });
+};
+
+// Parses an ICE candidate line. Sample input:
+// candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8
+// rport 55996"
+SDPUtils.parseCandidate = function(line) {
+ var parts;
+ // Parse both variants.
+ if (line.indexOf("a=candidate:") === 0) {
+ parts = line.substring(12).split(" ");
+ } else {
+ parts = line.substring(10).split(" ");
+ }
+
+ var candidate = {
+ foundation: parts[0],
+ component: parseInt(parts[1], 10),
+ protocol: parts[2].toLowerCase(),
+ priority: parseInt(parts[3], 10),
+ ip: parts[4],
+ address: parts[4], // address is an alias for ip.
+ port: parseInt(parts[5], 10),
+ // skip parts[6] == 'typ'
+ type: parts[7],
+ };
+
+ for (var i = 8; i < parts.length; i += 2) {
+ switch (parts[i]) {
+ case "raddr":
+ candidate.relatedAddress = parts[i + 1];
+ break;
+ case "rport":
+ candidate.relatedPort = parseInt(parts[i + 1], 10);
+ break;
+ case "tcptype":
+ candidate.tcpType = parts[i + 1];
+ break;
+ case "ufrag":
+ candidate.ufrag = parts[i + 1]; // for backward compability.
+ candidate.usernameFragment = parts[i + 1];
+ break;
+ default:
+ // extension handling, in particular ufrag
+ candidate[parts[i]] = parts[i + 1];
+ break;
+ }
+ }
+ return candidate;
+};
+
+// Translates a candidate object into SDP candidate attribute.
+SDPUtils.writeCandidate = function(candidate) {
+ var sdp = [];
+ sdp.push(candidate.foundation);
+ sdp.push(candidate.component);
+ sdp.push(candidate.protocol.toUpperCase());
+ sdp.push(candidate.priority);
+ sdp.push(candidate.address || candidate.ip);
+ sdp.push(candidate.port);
+
+ var type = candidate.type;
+ sdp.push("typ");
+ sdp.push(type);
+ if (type !== "host" && candidate.relatedAddress && candidate.relatedPort) {
+ sdp.push("raddr");
+ sdp.push(candidate.relatedAddress);
+ sdp.push("rport");
+ sdp.push(candidate.relatedPort);
+ }
+ if (candidate.tcpType && candidate.protocol.toLowerCase() === "tcp") {
+ sdp.push("tcptype");
+ sdp.push(candidate.tcpType);
+ }
+ if (candidate.usernameFragment || candidate.ufrag) {
+ sdp.push("ufrag");
+ sdp.push(candidate.usernameFragment || candidate.ufrag);
+ }
+ return "candidate:" + sdp.join(" ");
+};
+
+// Parses an ice-options line, returns an array of option tags.
+// a=ice-options:foo bar
+SDPUtils.parseIceOptions = function(line) {
+ return line.substr(14).split(" ");
+};
+
+// Parses an rtpmap line, returns RTCRtpCoddecParameters. Sample input:
+// a=rtpmap:111 opus/48000/2
+SDPUtils.parseRtpMap = function(line) {
+ var parts = line.substr(9).split(" ");
+ var parsed = {
+ payloadType: parseInt(parts.shift(), 10), // was: id
+ };
+
+ parts = parts[0].split("/");
+
+ parsed.name = parts[0];
+ parsed.clockRate = parseInt(parts[1], 10); // was: clockrate
+ parsed.channels = parts.length === 3 ? parseInt(parts[2], 10) : 1;
+ // legacy alias, got renamed back to channels in ORTC.
+ parsed.numChannels = parsed.channels;
+ return parsed;
+};
+
+// Generate an a=rtpmap line from RTCRtpCodecCapability or
+// RTCRtpCodecParameters.
+SDPUtils.writeRtpMap = function(codec) {
+ var pt = codec.payloadType;
+ if (codec.preferredPayloadType !== undefined) {
+ pt = codec.preferredPayloadType;
+ }
+ var channels = codec.channels || codec.numChannels || 1;
+ return (
+ "a=rtpmap:" +
+ pt +
+ " " +
+ codec.name +
+ "/" +
+ codec.clockRate +
+ (channels !== 1 ? "/" + channels : "") +
+ "\r\n"
+ );
+};
+
+// Parses an a=extmap line (headerextension from RFC 5285). Sample input:
+// a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
+// a=extmap:2/sendonly urn:ietf:params:rtp-hdrext:toffset
+SDPUtils.parseExtmap = function(line) {
+ var parts = line.substr(9).split(" ");
+ return {
+ id: parseInt(parts[0], 10),
+ direction: parts[0].indexOf("/") > 0 ? parts[0].split("/")[1] : "sendrecv",
+ uri: parts[1],
+ };
+};
+
+// Generates a=extmap line from RTCRtpHeaderExtensionParameters or
+// RTCRtpHeaderExtension.
+SDPUtils.writeExtmap = function(headerExtension) {
+ return (
+ "a=extmap:" +
+ (headerExtension.id || headerExtension.preferredId) +
+ (headerExtension.direction && headerExtension.direction !== "sendrecv"
+ ? "/" + headerExtension.direction
+ : "") +
+ " " +
+ headerExtension.uri +
+ "\r\n"
+ );
+};
+
+// Parses an ftmp line, returns dictionary. Sample input:
+// a=fmtp:96 vbr=on;cng=on
+// Also deals with vbr=on; cng=on
+SDPUtils.parseFmtp = function(line) {
+ var parsed = {};
+ var kv;
+ var parts = line.substr(line.indexOf(" ") + 1).split(";");
+ for (var j = 0; j < parts.length; j++) {
+ kv = parts[j].trim().split("=");
+ parsed[kv[0].trim()] = kv[1];
+ }
+ return parsed;
+};
+
+// Generates an a=ftmp line from RTCRtpCodecCapability or RTCRtpCodecParameters.
+SDPUtils.writeFmtp = function(codec) {
+ var line = "";
+ var pt = codec.payloadType;
+ if (codec.preferredPayloadType !== undefined) {
+ pt = codec.preferredPayloadType;
+ }
+ if (codec.parameters && Object.keys(codec.parameters).length) {
+ var params = [];
+ Object.keys(codec.parameters).forEach(function(param) {
+ if (codec.parameters[param]) {
+ params.push(param + "=" + codec.parameters[param]);
+ } else {
+ params.push(param);
+ }
+ });
+ line += "a=fmtp:" + pt + " " + params.join(";") + "\r\n";
+ }
+ return line;
+};
+
+// Parses an rtcp-fb line, returns RTCPRtcpFeedback object. Sample input:
+// a=rtcp-fb:98 nack rpsi
+SDPUtils.parseRtcpFb = function(line) {
+ var parts = line.substr(line.indexOf(" ") + 1).split(" ");
+ return {
+ type: parts.shift(),
+ parameter: parts.join(" "),
+ };
+};
+// Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters.
+SDPUtils.writeRtcpFb = function(codec) {
+ var lines = "";
+ var pt = codec.payloadType;
+ if (codec.preferredPayloadType !== undefined) {
+ pt = codec.preferredPayloadType;
+ }
+ if (codec.rtcpFeedback && codec.rtcpFeedback.length) {
+ // FIXME: special handling for trr-int?
+ codec.rtcpFeedback.forEach(function(fb) {
+ lines +=
+ "a=rtcp-fb:" +
+ pt +
+ " " +
+ fb.type +
+ (fb.parameter && fb.parameter.length ? " " + fb.parameter : "") +
+ "\r\n";
+ });
+ }
+ return lines;
+};
+
+// Parses an RFC 5576 ssrc media attribute. Sample input:
+// a=ssrc:3735928559 cname:something
+SDPUtils.parseSsrcMedia = function(line) {
+ var sp = line.indexOf(" ");
+ var parts = {
+ ssrc: parseInt(line.substr(7, sp - 7), 10),
+ };
+ var colon = line.indexOf(":", sp);
+ if (colon > -1) {
+ parts.attribute = line.substr(sp + 1, colon - sp - 1);
+ parts.value = line.substr(colon + 1);
+ } else {
+ parts.attribute = line.substr(sp + 1);
+ }
+ return parts;
+};
+
+SDPUtils.parseSsrcGroup = function(line) {
+ var parts = line.substr(13).split(" ");
+ return {
+ semantics: parts.shift(),
+ ssrcs: parts.map(function(ssrc) {
+ return parseInt(ssrc, 10);
+ }),
+ };
+};
+
+// Extracts the MID (RFC 5888) from a media section.
+// returns the MID or undefined if no mid line was found.
+SDPUtils.getMid = function(mediaSection) {
+ var mid = SDPUtils.matchPrefix(mediaSection, "a=mid:")[0];
+ if (mid) {
+ return mid.substr(6);
+ }
+};
+
+SDPUtils.parseFingerprint = function(line) {
+ var parts = line.substr(14).split(" ");
+ return {
+ algorithm: parts[0].toLowerCase(), // algorithm is case-sensitive in Edge.
+ value: parts[1],
+ };
+};
+
+// Extracts DTLS parameters from SDP media section or sessionpart.
+// FIXME: for consistency with other functions this should only
+// get the fingerprint line as input. See also getIceParameters.
+SDPUtils.getDtlsParameters = function(mediaSection, sessionpart) {
+ var lines = SDPUtils.matchPrefix(
+ mediaSection + sessionpart,
+ "a=fingerprint:"
+ );
+ // Note: a=setup line is ignored since we use the 'auto' role.
+ // Note2: 'algorithm' is not case sensitive except in Edge.
+ return {
+ role: "auto",
+ fingerprints: lines.map(SDPUtils.parseFingerprint),
+ };
+};
+
+// Serializes DTLS parameters to SDP.
+SDPUtils.writeDtlsParameters = function(params, setupType) {
+ var sdp = "a=setup:" + setupType + "\r\n";
+ params.fingerprints.forEach(function(fp) {
+ sdp += "a=fingerprint:" + fp.algorithm + " " + fp.value + "\r\n";
+ });
+ return sdp;
+};
+
+// Parses a=crypto lines into
+// https://rawgit.com/aboba/edgertc/master/msortc-rs4.html#dictionary-rtcsrtpsdesparameters-members
+SDPUtils.parseCryptoLine = function(line) {
+ var parts = line.substr(9).split(" ");
+ return {
+ tag: parseInt(parts[0], 10),
+ cryptoSuite: parts[1],
+ keyParams: parts[2],
+ sessionParams: parts.slice(3),
+ };
+};
+
+SDPUtils.writeCryptoLine = function(parameters) {
+ return (
+ "a=crypto:" +
+ parameters.tag +
+ " " +
+ parameters.cryptoSuite +
+ " " +
+ (typeof parameters.keyParams === "object"
+ ? SDPUtils.writeCryptoKeyParams(parameters.keyParams)
+ : parameters.keyParams) +
+ (parameters.sessionParams ? " " + parameters.sessionParams.join(" ") : "") +
+ "\r\n"
+ );
+};
+
+// Parses the crypto key parameters into
+// https://rawgit.com/aboba/edgertc/master/msortc-rs4.html#rtcsrtpkeyparam*
+SDPUtils.parseCryptoKeyParams = function(keyParams) {
+ if (keyParams.indexOf("inline:") !== 0) {
+ return null;
+ }
+ var parts = keyParams.substr(7).split("|");
+ return {
+ keyMethod: "inline",
+ keySalt: parts[0],
+ lifeTime: parts[1],
+ mkiValue: parts[2] ? parts[2].split(":")[0] : undefined,
+ mkiLength: parts[2] ? parts[2].split(":")[1] : undefined,
+ };
+};
+
+SDPUtils.writeCryptoKeyParams = function(keyParams) {
+ return (
+ keyParams.keyMethod +
+ ":" +
+ keyParams.keySalt +
+ (keyParams.lifeTime ? "|" + keyParams.lifeTime : "") +
+ (keyParams.mkiValue && keyParams.mkiLength
+ ? "|" + keyParams.mkiValue + ":" + keyParams.mkiLength
+ : "")
+ );
+};
+
+// Extracts all SDES paramters.
+SDPUtils.getCryptoParameters = function(mediaSection, sessionpart) {
+ var lines = SDPUtils.matchPrefix(mediaSection + sessionpart, "a=crypto:");
+ return lines.map(SDPUtils.parseCryptoLine);
+};
+
+// Parses ICE information from SDP media section or sessionpart.
+// FIXME: for consistency with other functions this should only
+// get the ice-ufrag and ice-pwd lines as input.
+SDPUtils.getIceParameters = function(mediaSection, sessionpart) {
+ var ufrag = SDPUtils.matchPrefix(
+ mediaSection + sessionpart,
+ "a=ice-ufrag:"
+ )[0];
+ var pwd = SDPUtils.matchPrefix(mediaSection + sessionpart, "a=ice-pwd:")[0];
+ if (!(ufrag && pwd)) {
+ return null;
+ }
+ return {
+ usernameFragment: ufrag.substr(12),
+ password: pwd.substr(10),
+ };
+};
+
+// Serializes ICE parameters to SDP.
+SDPUtils.writeIceParameters = function(params) {
+ return (
+ "a=ice-ufrag:" +
+ params.usernameFragment +
+ "\r\n" +
+ "a=ice-pwd:" +
+ params.password +
+ "\r\n"
+ );
+};
+
+// Parses the SDP media section and returns RTCRtpParameters.
+SDPUtils.parseRtpParameters = function(mediaSection) {
+ var description = {
+ codecs: [],
+ headerExtensions: [],
+ fecMechanisms: [],
+ rtcp: [],
+ };
+ var lines = SDPUtils.splitLines(mediaSection);
+ var mline = lines[0].split(" ");
+ for (var i = 3; i < mline.length; i++) {
+ // find all codecs from mline[3..]
+ var pt = mline[i];
+ var rtpmapline = SDPUtils.matchPrefix(
+ mediaSection,
+ "a=rtpmap:" + pt + " "
+ )[0];
+ if (rtpmapline) {
+ var codec = SDPUtils.parseRtpMap(rtpmapline);
+ var fmtps = SDPUtils.matchPrefix(mediaSection, "a=fmtp:" + pt + " ");
+ // Only the first a=fmtp:<pt> is considered.
+ codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {};
+ codec.rtcpFeedback = SDPUtils.matchPrefix(
+ mediaSection,
+ "a=rtcp-fb:" + pt + " "
+ ).map(SDPUtils.parseRtcpFb);
+ description.codecs.push(codec);
+ // parse FEC mechanisms from rtpmap lines.
+ switch (codec.name.toUpperCase()) {
+ case "RED":
+ case "ULPFEC":
+ description.fecMechanisms.push(codec.name.toUpperCase());
+ break;
+ default:
+ // only RED and ULPFEC are recognized as FEC mechanisms.
+ break;
+ }
+ }
+ }
+ SDPUtils.matchPrefix(mediaSection, "a=extmap:").forEach(function(line) {
+ description.headerExtensions.push(SDPUtils.parseExtmap(line));
+ });
+ // FIXME: parse rtcp.
+ return description;
+};
+
+// Generates parts of the SDP media section describing the capabilities /
+// parameters.
+SDPUtils.writeRtpDescription = function(kind, caps) {
+ var sdp = "";
+
+ // Build the mline.
+ sdp += "m=" + kind + " ";
+ sdp += caps.codecs.length > 0 ? "9" : "0"; // reject if no codecs.
+ sdp += " UDP/TLS/RTP/SAVPF ";
+ sdp +=
+ caps.codecs
+ .map(function(codec) {
+ if (codec.preferredPayloadType !== undefined) {
+ return codec.preferredPayloadType;
+ }
+ return codec.payloadType;
+ })
+ .join(" ") + "\r\n";
+
+ sdp += "c=IN IP4 0.0.0.0\r\n";
+ sdp += "a=rtcp:9 IN IP4 0.0.0.0\r\n";
+
+ // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb.
+ caps.codecs.forEach(function(codec) {
+ sdp += SDPUtils.writeRtpMap(codec);
+ sdp += SDPUtils.writeFmtp(codec);
+ sdp += SDPUtils.writeRtcpFb(codec);
+ });
+ var maxptime = 0;
+ caps.codecs.forEach(function(codec) {
+ if (codec.maxptime > maxptime) {
+ maxptime = codec.maxptime;
+ }
+ });
+ if (maxptime > 0) {
+ sdp += "a=maxptime:" + maxptime + "\r\n";
+ }
+ sdp += "a=rtcp-mux\r\n";
+
+ if (caps.headerExtensions) {
+ caps.headerExtensions.forEach(function(extension) {
+ sdp += SDPUtils.writeExtmap(extension);
+ });
+ }
+ // FIXME: write fecMechanisms.
+ return sdp;
+};
+
+// Parses the SDP media section and returns an array of
+// RTCRtpEncodingParameters.
+SDPUtils.parseRtpEncodingParameters = function(mediaSection) {
+ var encodingParameters = [];
+ var description = SDPUtils.parseRtpParameters(mediaSection);
+ var hasRed = description.fecMechanisms.indexOf("RED") !== -1;
+ var hasUlpfec = description.fecMechanisms.indexOf("ULPFEC") !== -1;
+
+ // filter a=ssrc:... cname:, ignore PlanB-msid
+ var ssrcs = SDPUtils.matchPrefix(mediaSection, "a=ssrc:")
+ .map(function(line) {
+ return SDPUtils.parseSsrcMedia(line);
+ })
+ .filter(function(parts) {
+ return parts.attribute === "cname";
+ });
+ var primarySsrc = ssrcs.length > 0 && ssrcs[0].ssrc;
+ var secondarySsrc;
+
+ var flows = SDPUtils.matchPrefix(mediaSection, "a=ssrc-group:FID").map(
+ function(line) {
+ var parts = line.substr(17).split(" ");
+ return parts.map(function(part) {
+ return parseInt(part, 10);
+ });
+ }
+ );
+ if (flows.length > 0 && flows[0].length > 1 && flows[0][0] === primarySsrc) {
+ secondarySsrc = flows[0][1];
+ }
+
+ description.codecs.forEach(function(codec) {
+ if (codec.name.toUpperCase() === "RTX" && codec.parameters.apt) {
+ var encParam = {
+ ssrc: primarySsrc,
+ codecPayloadType: parseInt(codec.parameters.apt, 10),
+ };
+ if (primarySsrc && secondarySsrc) {
+ encParam.rtx = { ssrc: secondarySsrc };
+ }
+ encodingParameters.push(encParam);
+ if (hasRed) {
+ encParam = JSON.parse(JSON.stringify(encParam));
+ encParam.fec = {
+ ssrc: primarySsrc,
+ mechanism: hasUlpfec ? "red+ulpfec" : "red",
+ };
+ encodingParameters.push(encParam);
+ }
+ }
+ });
+ if (encodingParameters.length === 0 && primarySsrc) {
+ encodingParameters.push({
+ ssrc: primarySsrc,
+ });
+ }
+
+ // we support both b=AS and b=TIAS but interpret AS as TIAS.
+ var bandwidth = SDPUtils.matchPrefix(mediaSection, "b=");
+ if (bandwidth.length) {
+ if (bandwidth[0].indexOf("b=TIAS:") === 0) {
+ bandwidth = parseInt(bandwidth[0].substr(7), 10);
+ } else if (bandwidth[0].indexOf("b=AS:") === 0) {
+ // use formula from JSEP to convert b=AS to TIAS value.
+ bandwidth =
+ parseInt(bandwidth[0].substr(5), 10) * 1000 * 0.95 - 50 * 40 * 8;
+ } else {
+ bandwidth = undefined;
+ }
+ encodingParameters.forEach(function(params) {
+ params.maxBitrate = bandwidth;
+ });
+ }
+ return encodingParameters;
+};
+
+// parses http://draft.ortc.org/#rtcrtcpparameters*
+SDPUtils.parseRtcpParameters = function(mediaSection) {
+ var rtcpParameters = {};
+
+ // Gets the first SSRC. Note tha with RTX there might be multiple
+ // SSRCs.
+ var remoteSsrc = SDPUtils.matchPrefix(mediaSection, "a=ssrc:")
+ .map(function(line) {
+ return SDPUtils.parseSsrcMedia(line);
+ })
+ .filter(function(obj) {
+ return obj.attribute === "cname";
+ })[0];
+ if (remoteSsrc) {
+ rtcpParameters.cname = remoteSsrc.value;
+ rtcpParameters.ssrc = remoteSsrc.ssrc;
+ }
+
+ // Edge uses the compound attribute instead of reducedSize
+ // compound is !reducedSize
+ var rsize = SDPUtils.matchPrefix(mediaSection, "a=rtcp-rsize");
+ rtcpParameters.reducedSize = rsize.length > 0;
+ rtcpParameters.compound = rsize.length === 0;
+
+ // parses the rtcp-mux attrіbute.
+ // Note that Edge does not support unmuxed RTCP.
+ var mux = SDPUtils.matchPrefix(mediaSection, "a=rtcp-mux");
+ rtcpParameters.mux = mux.length > 0;
+
+ return rtcpParameters;
+};
+
+// parses either a=msid: or a=ssrc:... msid lines and returns
+// the id of the MediaStream and MediaStreamTrack.
+SDPUtils.parseMsid = function(mediaSection) {
+ var parts;
+ var spec = SDPUtils.matchPrefix(mediaSection, "a=msid:");
+ if (spec.length === 1) {
+ parts = spec[0].substr(7).split(" ");
+ return { stream: parts[0], track: parts[1] };
+ }
+ var planB = SDPUtils.matchPrefix(mediaSection, "a=ssrc:")
+ .map(function(line) {
+ return SDPUtils.parseSsrcMedia(line);
+ })
+ .filter(function(msidParts) {
+ return msidParts.attribute === "msid";
+ });
+ if (planB.length > 0) {
+ parts = planB[0].value.split(" ");
+ return { stream: parts[0], track: parts[1] };
+ }
+};
+
+// SCTP
+// parses draft-ietf-mmusic-sctp-sdp-26 first and falls back
+// to draft-ietf-mmusic-sctp-sdp-05
+SDPUtils.parseSctpDescription = function(mediaSection) {
+ var mline = SDPUtils.parseMLine(mediaSection);
+ var maxSizeLine = SDPUtils.matchPrefix(mediaSection, "a=max-message-size:");
+ var maxMessageSize;
+ if (maxSizeLine.length > 0) {
+ maxMessageSize = parseInt(maxSizeLine[0].substr(19), 10);
+ }
+ if (isNaN(maxMessageSize)) {
+ maxMessageSize = 65536;
+ }
+ var sctpPort = SDPUtils.matchPrefix(mediaSection, "a=sctp-port:");
+ if (sctpPort.length > 0) {
+ return {
+ port: parseInt(sctpPort[0].substr(12), 10),
+ protocol: mline.fmt,
+ maxMessageSize,
+ };
+ }
+ var sctpMapLines = SDPUtils.matchPrefix(mediaSection, "a=sctpmap:");
+ if (sctpMapLines.length > 0) {
+ var parts = SDPUtils.matchPrefix(mediaSection, "a=sctpmap:")[0]
+ .substr(10)
+ .split(" ");
+ return {
+ port: parseInt(parts[0], 10),
+ protocol: parts[1],
+ maxMessageSize,
+ };
+ }
+};
+
+// SCTP
+// outputs the draft-ietf-mmusic-sctp-sdp-26 version that all browsers
+// support by now receiving in this format, unless we originally parsed
+// as the draft-ietf-mmusic-sctp-sdp-05 format (indicated by the m-line
+// protocol of DTLS/SCTP -- without UDP/ or TCP/)
+SDPUtils.writeSctpDescription = function(media, sctp) {
+ var output = [];
+ if (media.protocol !== "DTLS/SCTP") {
+ output = [
+ "m=" + media.kind + " 9 " + media.protocol + " " + sctp.protocol + "\r\n",
+ "c=IN IP4 0.0.0.0\r\n",
+ "a=sctp-port:" + sctp.port + "\r\n",
+ ];
+ } else {
+ output = [
+ "m=" + media.kind + " 9 " + media.protocol + " " + sctp.port + "\r\n",
+ "c=IN IP4 0.0.0.0\r\n",
+ "a=sctpmap:" + sctp.port + " " + sctp.protocol + " 65535\r\n",
+ ];
+ }
+ if (sctp.maxMessageSize !== undefined) {
+ output.push("a=max-message-size:" + sctp.maxMessageSize + "\r\n");
+ }
+ return output.join("");
+};
+
+// Generate a session ID for SDP.
+// https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-20#section-5.2.1
+// recommends using a cryptographically random +ve 64-bit value
+// but right now this should be acceptable and within the right range
+SDPUtils.generateSessionId = function() {
+ return Math.floor((Math.random() * 4294967296) + 1);
+};
+
+// Write boilder plate for start of SDP
+// sessId argument is optional - if not supplied it will
+// be generated randomly
+// sessVersion is optional and defaults to 2
+// sessUser is optional and defaults to 'thisisadapterortc'
+SDPUtils.writeSessionBoilerplate = function(sessId, sessVer, sessUser) {
+ var sessionId;
+ var version = sessVer !== undefined ? sessVer : 2;
+ if (sessId) {
+ sessionId = sessId;
+ } else {
+ sessionId = SDPUtils.generateSessionId();
+ }
+ var user = sessUser || "thisisadapterortc";
+ // FIXME: sess-id should be an NTP timestamp.
+ return (
+ "v=0\r\n" +
+ "o=" +
+ user +
+ " " +
+ sessionId +
+ " " +
+ version +
+ " IN IP4 127.0.0.1\r\n" +
+ "s=-\r\n" +
+ "t=0 0\r\n"
+ );
+};
+
+SDPUtils.writeMediaSection = function(transceiver, caps, type, stream) {
+ var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps);
+
+ // Map ICE parameters (ufrag, pwd) to SDP.
+ sdp += SDPUtils.writeIceParameters(
+ transceiver.iceGatherer.getLocalParameters()
+ );
+
+ // Map DTLS parameters to SDP.
+ sdp += SDPUtils.writeDtlsParameters(
+ transceiver.dtlsTransport.getLocalParameters(),
+ type === "offer" ? "actpass" : "active"
+ );
+
+ sdp += "a=mid:" + transceiver.mid + "\r\n";
+
+ if (transceiver.direction) {
+ sdp += "a=" + transceiver.direction + "\r\n";
+ } else if (transceiver.rtpSender && transceiver.rtpReceiver) {
+ sdp += "a=sendrecv\r\n";
+ } else if (transceiver.rtpSender) {
+ sdp += "a=sendonly\r\n";
+ } else if (transceiver.rtpReceiver) {
+ sdp += "a=recvonly\r\n";
+ } else {
+ sdp += "a=inactive\r\n";
+ }
+
+ if (transceiver.rtpSender) {
+ // spec.
+ var msid =
+ "msid:" + stream.id + " " + transceiver.rtpSender.track.id + "\r\n";
+ sdp += "a=" + msid;
+
+ // for Chrome.
+ sdp += "a=ssrc:" + transceiver.sendEncodingParameters[0].ssrc + " " + msid;
+ if (transceiver.sendEncodingParameters[0].rtx) {
+ sdp +=
+ "a=ssrc:" + transceiver.sendEncodingParameters[0].rtx.ssrc + " " + msid;
+ sdp +=
+ "a=ssrc-group:FID " +
+ transceiver.sendEncodingParameters[0].ssrc +
+ " " +
+ transceiver.sendEncodingParameters[0].rtx.ssrc +
+ "\r\n";
+ }
+ }
+ // FIXME: this should be written by writeRtpDescription.
+ sdp +=
+ "a=ssrc:" +
+ transceiver.sendEncodingParameters[0].ssrc +
+ " cname:" +
+ SDPUtils.localCName +
+ "\r\n";
+ if (transceiver.rtpSender && transceiver.sendEncodingParameters[0].rtx) {
+ sdp +=
+ "a=ssrc:" +
+ transceiver.sendEncodingParameters[0].rtx.ssrc +
+ " cname:" +
+ SDPUtils.localCName +
+ "\r\n";
+ }
+ return sdp;
+};
+
+// Gets the direction from the mediaSection or the sessionpart.
+SDPUtils.getDirection = function(mediaSection, sessionpart) {
+ // Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv.
+ var lines = SDPUtils.splitLines(mediaSection);
+ for (var i = 0; i < lines.length; i++) {
+ switch (lines[i]) {
+ case "a=sendrecv":
+ case "a=sendonly":
+ case "a=recvonly":
+ case "a=inactive":
+ return lines[i].substr(2);
+ default:
+ // FIXME: What should happen here?
+ }
+ }
+ if (sessionpart) {
+ return SDPUtils.getDirection(sessionpart);
+ }
+ return "sendrecv";
+};
+
+SDPUtils.getKind = function(mediaSection) {
+ var lines = SDPUtils.splitLines(mediaSection);
+ var mline = lines[0].split(" ");
+ return mline[0].substr(2);
+};
+
+SDPUtils.isRejected = function(mediaSection) {
+ return mediaSection.split(" ", 2)[1] === "0";
+};
+
+SDPUtils.parseMLine = function(mediaSection) {
+ var lines = SDPUtils.splitLines(mediaSection);
+ var parts = lines[0].substr(2).split(" ");
+ return {
+ kind: parts[0],
+ port: parseInt(parts[1], 10),
+ protocol: parts[2],
+ fmt: parts.slice(3).join(" "),
+ };
+};
+
+SDPUtils.parseOLine = function(mediaSection) {
+ var line = SDPUtils.matchPrefix(mediaSection, "o=")[0];
+ var parts = line.substr(2).split(" ");
+ return {
+ username: parts[0],
+ sessionId: parts[1],
+ sessionVersion: parseInt(parts[2], 10),
+ netType: parts[3],
+ addressType: parts[4],
+ address: parts[5],
+ };
+};
+
+// a very naive interpretation of a valid SDP.
+SDPUtils.isValidSDP = function(blob) {
+ if (typeof blob !== "string" || blob.length === 0) {
+ return false;
+ }
+ var lines = SDPUtils.splitLines(blob);
+ for (var i = 0; i < lines.length; i++) {
+ if (lines[i].length < 2 || lines[i].charAt(1) !== "=") {
+ return false;
+ }
+ // TODO: check the modifier a bit more.
+ }
+ return true;
+};
+
+// Expose public methods.
+if (typeof module === "object") {
+ module.exports = SDPUtils;
+}
diff --git a/dom/media/webrtc/tests/mochitests/iceTestUtils.js b/dom/media/webrtc/tests/mochitests/iceTestUtils.js
new file mode 100644
index 0000000000..d4d1f5c4b4
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/iceTestUtils.js
@@ -0,0 +1,302 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// This is mostly so test_peerConnection_gatherWithStun300.html and
+// test_peerConnection_gatherWithStun300IPv6 can share this code. I would have
+// put the ipv6 test code in the same file, but our ipv6 tester support is
+// inconsistent enough that we need to be able to track the ipv6 test
+// separately.
+
+async function findStatsRelayCandidates(pc, protocol) {
+ const stats = await pc.getStats();
+ return [...stats.values()].filter(
+ v =>
+ v.type == "local-candidate" &&
+ v.candidateType == "relay" &&
+ v.relayProtocol == protocol
+ );
+}
+
+// Trickles candidates if pcDst is set, and resolves the candidate list
+async function trickleIce(pc, pcDst) {
+ const candidates = [],
+ addCandidatePromises = [];
+ while (true) {
+ const { candidate } = await new Promise(r =>
+ pc.addEventListener("icecandidate", r, { once: true })
+ );
+ if (!candidate) {
+ break;
+ }
+ candidates.push(candidate);
+ if (pcDst) {
+ addCandidatePromises.push(pcDst.addIceCandidate(candidate));
+ }
+ }
+ await Promise.all(addCandidatePromises);
+ return candidates;
+}
+
+async function gather(pc) {
+ if (pc.signalingState == "stable") {
+ await pc.setLocalDescription(
+ await pc.createOffer({ offerToReceiveAudio: true })
+ );
+ } else if (pc.signalingState == "have-remote-offer") {
+ await pc.setLocalDescription();
+ }
+
+ return trickleIce(pc);
+}
+
+async function gatherWithTimeout(pc, timeout, context) {
+ const throwOnTimeout = async () => {
+ await wait(timeout);
+ throw new Error(
+ `Gathering did not complete within ${timeout} ms with ${context}`
+ );
+ };
+
+ return Promise.race([gather(pc), throwOnTimeout()]);
+}
+
+async function iceConnected(pc) {
+ return new Promise((resolve, reject) => {
+ pc.addEventListener("iceconnectionstatechange", () => {
+ if (["connected", "completed"].includes(pc.iceConnectionState)) {
+ resolve();
+ } else if (pc.iceConnectionState == "failed") {
+ reject(new Error(`ICE failed`));
+ }
+ });
+ });
+}
+
+// Set up trickle, but does not wait for it to complete. Can be used by itself
+// in cases where we do not expect any new candidates, but want to still set up
+// the signal handling in case new candidates _do_ show up.
+async function connectNoTrickleWait(offerer, answerer, timeout, context) {
+ return connect(offerer, answerer, timeout, context, true);
+}
+
+async function connect(
+ offerer,
+ answerer,
+ timeout,
+ context,
+ noTrickleWait = false
+) {
+ const trickle1 = trickleIce(offerer, answerer);
+ const trickle2 = trickleIce(answerer, offerer);
+ try {
+ const offer = await offerer.createOffer({ offerToReceiveAudio: true });
+ await offerer.setLocalDescription(offer);
+ await answerer.setRemoteDescription(offer);
+ const answer = await answerer.createAnswer();
+ await Promise.all([
+ offerer.setRemoteDescription(answer),
+ answerer.setLocalDescription(answer),
+ ]);
+
+ const throwOnTimeout = async () => {
+ if (timeout) {
+ await wait(timeout);
+ throw new Error(
+ `ICE did not complete within ${timeout} ms with ${context}`
+ );
+ }
+ };
+
+ await Promise.race([
+ Promise.all([iceConnected(offerer), iceConnected(answerer)]),
+ throwOnTimeout(timeout, context),
+ ]);
+ } finally {
+ if (!noTrickleWait) {
+ // TODO(bug 1751509): For now, we need to let gathering finish before we
+ // proceed, because there are races in ICE restart wrt gathering state.
+ await Promise.all([trickle1, trickle2]);
+ }
+ }
+}
+
+function isV6HostCandidate(candidate) {
+ const fields = candidate.candidate.split(" ");
+ const type = fields[7];
+ const ipAddress = fields[4];
+ return type == "host" && ipAddress.includes(":");
+}
+
+async function ipv6Supported() {
+ const pc = new RTCPeerConnection();
+ const candidates = await gatherWithTimeout(pc, 8000);
+ info(`baseline candidates: ${JSON.stringify(candidates)}`);
+ pc.close();
+ return candidates.some(isV6HostCandidate);
+}
+
+function makeContextString(iceServers) {
+ const currentRedirectAddress = SpecialPowers.getCharPref(
+ "media.peerconnection.nat_simulator.redirect_address",
+ ""
+ );
+ const currentRedirectTargets = SpecialPowers.getCharPref(
+ "media.peerconnection.nat_simulator.redirect_targets",
+ ""
+ );
+ return `redirect rule: ${currentRedirectAddress}=>${currentRedirectTargets} iceServers: ${JSON.stringify(
+ iceServers
+ )}`;
+}
+
+async function checkSrflx(iceServers) {
+ const context = makeContextString(iceServers);
+ info(`checkSrflx ${context}`);
+ const pc = new RTCPeerConnection({
+ iceServers,
+ bundlePolicy: "max-bundle", // Avoids extra candidates
+ });
+ const candidates = await gatherWithTimeout(pc, 8000, context);
+ const srflxCandidates = candidates.filter(c => c.candidate.includes("srflx"));
+ info(`candidates: ${JSON.stringify(srflxCandidates)}`);
+ // TODO(bug 1339203): Once we support rtcpMuxPolicy, set it to "require" to
+ // result in a single srflx candidate
+ is(
+ srflxCandidates.length,
+ 2,
+ `Should have two srflx candidates with ${context}`
+ );
+ pc.close();
+}
+
+async function checkNoSrflx(iceServers) {
+ const context = makeContextString(iceServers);
+ info(`checkNoSrflx ${context}`);
+ const pc = new RTCPeerConnection({
+ iceServers,
+ bundlePolicy: "max-bundle", // Avoids extra candidates
+ });
+ const candidates = await gatherWithTimeout(pc, 8000, context);
+ const srflxCandidates = candidates.filter(c => c.candidate.includes("srflx"));
+ info(`candidates: ${JSON.stringify(srflxCandidates)}`);
+ is(
+ srflxCandidates.length,
+ 0,
+ `Should have no srflx candidates with ${context}`
+ );
+ pc.close();
+}
+
+async function checkRelayUdp(iceServers) {
+ const context = makeContextString(iceServers);
+ info(`checkRelayUdp ${context}`);
+ const pc = new RTCPeerConnection({
+ iceServers,
+ bundlePolicy: "max-bundle", // Avoids extra candidates
+ });
+ const candidates = await gatherWithTimeout(pc, 8000, context);
+ const relayCandidates = candidates.filter(c => c.candidate.includes("relay"));
+ info(`candidates: ${JSON.stringify(relayCandidates)}`);
+ // TODO(bug 1339203): Once we support rtcpMuxPolicy, set it to "require" to
+ // result in a single relay candidate
+ is(
+ relayCandidates.length,
+ 2,
+ `Should have two relay candidates with ${context}`
+ );
+ // It would be nice if RTCIceCandidate had a field telling us what the
+ // "related protocol" is (similar to relatedAddress and relatedPort).
+ // Because there is no such thing, we need to go through the stats API,
+ // which _does_ have that information.
+ is(
+ (await findStatsRelayCandidates(pc, "tcp")).length,
+ 0,
+ `No TCP relay candidates should be present with ${context}`
+ );
+ pc.close();
+}
+
+async function checkRelayTcp(iceServers) {
+ const context = makeContextString(iceServers);
+ info(`checkRelayTcp ${context}`);
+ const pc = new RTCPeerConnection({
+ iceServers,
+ bundlePolicy: "max-bundle", // Avoids extra candidates
+ });
+ const candidates = await gatherWithTimeout(pc, 8000, context);
+ const relayCandidates = candidates.filter(c => c.candidate.includes("relay"));
+ info(`candidates: ${JSON.stringify(relayCandidates)}`);
+ // TODO(bug 1339203): Once we support rtcpMuxPolicy, set it to "require" to
+ // result in a single relay candidate
+ is(
+ relayCandidates.length,
+ 2,
+ `Should have two relay candidates with ${context}`
+ );
+ // It would be nice if RTCIceCandidate had a field telling us what the
+ // "related protocol" is (similar to relatedAddress and relatedPort).
+ // Because there is no such thing, we need to go through the stats API,
+ // which _does_ have that information.
+ is(
+ (await findStatsRelayCandidates(pc, "udp")).length,
+ 0,
+ `No UDP relay candidates should be present with ${context}`
+ );
+ pc.close();
+}
+
+async function checkRelayUdpTcp(iceServers) {
+ const context = makeContextString(iceServers);
+ info(`checkRelayUdpTcp ${context}`);
+ const pc = new RTCPeerConnection({
+ iceServers,
+ bundlePolicy: "max-bundle", // Avoids extra candidates
+ });
+ const candidates = await gatherWithTimeout(pc, 8000, context);
+ const relayCandidates = candidates.filter(c => c.candidate.includes("relay"));
+ info(`candidates: ${JSON.stringify(relayCandidates)}`);
+ // TODO(bug 1339203): Once we support rtcpMuxPolicy, set it to "require" to
+ // result in a single relay candidate each for UDP and TCP
+ is(
+ relayCandidates.length,
+ 4,
+ `Should have two relay candidates for each protocol with ${context}`
+ );
+ // It would be nice if RTCIceCandidate had a field telling us what the
+ // "related protocol" is (similar to relatedAddress and relatedPort).
+ // Because there is no such thing, we need to go through the stats API,
+ // which _does_ have that information.
+ is(
+ (await findStatsRelayCandidates(pc, "udp")).length,
+ 2,
+ `Two UDP relay candidates should be present with ${context}`
+ );
+ // TODO(bug 1705563): This is 1 because of bug 1705563
+ is(
+ (await findStatsRelayCandidates(pc, "tcp")).length,
+ 1,
+ `One TCP relay candidates should be present with ${context}`
+ );
+ pc.close();
+}
+
+async function checkNoRelay(iceServers) {
+ const context = makeContextString(iceServers);
+ info(`checkNoRelay ${context}`);
+ const pc = new RTCPeerConnection({
+ iceServers,
+ bundlePolicy: "max-bundle", // Avoids extra candidates
+ });
+ const candidates = await gatherWithTimeout(pc, 8000, context);
+ const relayCandidates = candidates.filter(c => c.candidate.includes("relay"));
+ info(`candidates: ${JSON.stringify(relayCandidates)}`);
+ is(
+ relayCandidates.length,
+ 0,
+ `Should have no relay candidates with ${context}`
+ );
+ pc.close();
+}
diff --git a/dom/media/webrtc/tests/mochitests/identity/identityPcTest.js b/dom/media/webrtc/tests/mochitests/identity/identityPcTest.js
new file mode 100644
index 0000000000..1381873f9d
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/identityPcTest.js
@@ -0,0 +1,79 @@
+function identityPcTest(remoteOptions) {
+ var user = "someone";
+ var domain1 = "test1.example.com";
+ var domain2 = "test2.example.com";
+ var id1 = user + "@" + domain1;
+ var id2 = user + "@" + domain2;
+
+ test = new PeerConnectionTest({
+ config_local: {
+ peerIdentity: id2,
+ },
+ config_remote: {
+ peerIdentity: id1,
+ },
+ });
+ test.setMediaConstraints(
+ [
+ {
+ audio: true,
+ video: true,
+ peerIdentity: id2,
+ },
+ ],
+ [
+ remoteOptions || {
+ audio: true,
+ video: true,
+ peerIdentity: id1,
+ },
+ ]
+ );
+ test.pcLocal.setIdentityProvider("test1.example.com", { protocol: "idp.js" });
+ test.pcRemote.setIdentityProvider("test2.example.com", {
+ protocol: "idp.js",
+ });
+ test.chain.append([
+ function PEER_IDENTITY_IS_SET_CORRECTLY(test) {
+ // no need to wait to check identity in this case,
+ // setRemoteDescription should wait for the IdP to complete
+ function checkIdentity(pc, pfx, idp, name) {
+ return pc.peerIdentity.then(peerInfo => {
+ is(peerInfo.idp, idp, pfx + "IdP check");
+ is(peerInfo.name, name + "@" + idp, pfx + "identity check");
+ });
+ }
+
+ return Promise.all([
+ checkIdentity(
+ test.pcLocal._pc,
+ "local: ",
+ "test2.example.com",
+ "someone"
+ ),
+ checkIdentity(
+ test.pcRemote._pc,
+ "remote: ",
+ "test1.example.com",
+ "someone"
+ ),
+ ]);
+ },
+
+ function REMOTE_STREAMS_ARE_RESTRICTED(test) {
+ var remoteStream = test.pcLocal._pc.getRemoteStreams()[0];
+ for (const track of remoteStream.getTracks()) {
+ mustThrowWith(
+ `Freshly received ${track.kind} track with peerIdentity`,
+ "SecurityError",
+ () => new MediaRecorder(new MediaStream([track])).start()
+ );
+ }
+ return Promise.all([
+ audioIsSilence(true, remoteStream),
+ videoIsBlack(true, remoteStream),
+ ]);
+ },
+ ]);
+ return test.run();
+}
diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-bad.js b/dom/media/webrtc/tests/mochitests/identity/idp-bad.js
new file mode 100644
index 0000000000..86e1cb7a34
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/idp-bad.js
@@ -0,0 +1 @@
+<This isn't valid JS>
diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-min.js b/dom/media/webrtc/tests/mochitests/identity/idp-min.js
new file mode 100644
index 0000000000..a4b2c55cee
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/idp-min.js
@@ -0,0 +1,24 @@
+(function (global) {
+ "use strict";
+ // A minimal implementation of the interface.
+ // Though this isn't particularly functional.
+ // This is needed so that we can have a "working" IdP served
+ // from two different locations in the tree.
+ global.rtcIdentityProvider.register({
+ generateAssertion(payload, origin, usernameHint) {
+ dump("idp: generateAssertion(" + payload + ")\n");
+ return Promise.resolve({
+ idp: { domain: "example.com", protocol: "idp.js" },
+ assertion: "bogus",
+ });
+ },
+
+ validateAssertion(assertion, origin) {
+ dump("idp: validateAssertion(" + assertion + ")\n");
+ return Promise.resolve({
+ identity: "user@example.com",
+ contents: "bogus",
+ });
+ },
+ });
+})(this);
diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http-trick.js b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http-trick.js
new file mode 100644
index 0000000000..75390cbf4f
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http-trick.js
@@ -0,0 +1,3 @@
+(function () {
+ dump("ERROR\n");
+})();
diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http-trick.js^headers^ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http-trick.js^headers^
new file mode 100644
index 0000000000..b3a2afd90a
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http-trick.js^headers^
@@ -0,0 +1,2 @@
+HTTP 301 Moved Permanently
+Location: http://example.com/.well-known/idp-proxy/idp-redirect-https.js
diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http.js b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http.js
new file mode 100644
index 0000000000..75390cbf4f
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http.js
@@ -0,0 +1,3 @@
+(function () {
+ dump("ERROR\n");
+})();
diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http.js^headers^ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http.js^headers^
new file mode 100644
index 0000000000..d2380984e7
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-http.js^headers^
@@ -0,0 +1,2 @@
+HTTP 301 Moved Permanently
+Location: http://example.com/.well-known/idp-proxy/idp.js
diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-double.js b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-double.js
new file mode 100644
index 0000000000..75390cbf4f
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-double.js
@@ -0,0 +1,3 @@
+(function () {
+ dump("ERROR\n");
+})();
diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-double.js^headers^ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-double.js^headers^
new file mode 100644
index 0000000000..3fb8a35ae7
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-double.js^headers^
@@ -0,0 +1,2 @@
+HTTP 301 Moved Permanently
+Location: https://example.com/.well-known/idp-proxy/idp-redirect-https.js
diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-odd-path.js b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-odd-path.js
new file mode 100644
index 0000000000..75390cbf4f
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-odd-path.js
@@ -0,0 +1,3 @@
+(function () {
+ dump("ERROR\n");
+})();
diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-odd-path.js^headers^ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-odd-path.js^headers^
new file mode 100644
index 0000000000..6e2931eda9
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https-odd-path.js^headers^
@@ -0,0 +1,2 @@
+HTTP 301 Moved Permanently
+Location: https://example.com/.well-known/idp-min.js
diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https.js b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https.js
new file mode 100644
index 0000000000..75390cbf4f
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https.js
@@ -0,0 +1,3 @@
+(function () {
+ dump("ERROR\n");
+})();
diff --git a/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https.js^headers^ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https.js^headers^
new file mode 100644
index 0000000000..77d56ac442
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/idp-redirect-https.js^headers^
@@ -0,0 +1,2 @@
+HTTP 301 Moved Permanently
+Location: https://example.com/.well-known/idp-proxy/idp.js
diff --git a/dom/media/webrtc/tests/mochitests/identity/idp.js b/dom/media/webrtc/tests/mochitests/identity/idp.js
new file mode 100644
index 0000000000..557740657f
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/idp.js
@@ -0,0 +1,119 @@
+(function (global) {
+ "use strict";
+
+ // rather than create a million different IdP configurations and litter the
+ // world with files all containing near-identical code, let's use the hash/URL
+ // fragment as a way of generating instructions for the IdP
+ var instructions = global.location.hash.replace("#", "").split(":");
+ function is(target) {
+ return function (instruction) {
+ return instruction === target;
+ };
+ }
+
+ function IDPJS() {
+ this.domain = global.location.host;
+ var path = global.location.pathname;
+ this.protocol =
+ path.substring(path.lastIndexOf("/") + 1) + global.location.hash;
+ this.id = crypto.getRandomValues(new Uint8Array(10)).join(".");
+ }
+
+ IDPJS.prototype = {
+ getLogin() {
+ return fetch(
+ "https://example.com/.well-known/idp-proxy/idp.sjs?" + this.id
+ ).then(response => response.status === 200);
+ },
+ checkLogin(result) {
+ return this.getLogin().then(loggedIn => {
+ if (loggedIn) {
+ return result;
+ }
+ return Promise.reject({
+ name: "IdpLoginError",
+ loginUrl:
+ "https://example.com/.well-known/idp-proxy/login.html#" + this.id,
+ });
+ });
+ },
+
+ borkResult(result) {
+ if (instructions.some(is("throw"))) {
+ throw new Error("Throwing!");
+ }
+ if (instructions.some(is("fail"))) {
+ return Promise.reject(new Error("Failing!"));
+ }
+ if (instructions.some(is("login"))) {
+ return this.checkLogin(result);
+ }
+ if (instructions.some(is("hang"))) {
+ return new Promise(r => {});
+ }
+ dump("idp: result=" + JSON.stringify(result) + "\n");
+ return Promise.resolve(result);
+ },
+
+ _selectUsername(usernameHint) {
+ dump("_selectUsername: usernameHint(" + usernameHint + ")\n");
+ var username = "someone@" + this.domain;
+ if (usernameHint) {
+ var at = usernameHint.indexOf("@");
+ if (at < 0) {
+ username = usernameHint + "@" + this.domain;
+ } else if (usernameHint.substring(at + 1) === this.domain) {
+ username = usernameHint;
+ }
+ }
+ return username;
+ },
+
+ generateAssertion(payload, origin, options) {
+ dump(
+ "idp: generateAssertion(" +
+ payload +
+ ", " +
+ origin +
+ ", " +
+ JSON.stringify(options) +
+ ")\n"
+ );
+ var idpDetails = {
+ domain: this.domain,
+ protocol: this.protocol,
+ };
+ if (instructions.some(is("bad-assert"))) {
+ idpDetails = {};
+ }
+ return this.borkResult({
+ idp: idpDetails,
+ assertion: JSON.stringify({
+ username: this._selectUsername(options.usernameHint),
+ contents: payload,
+ }),
+ });
+ },
+
+ validateAssertion(assertion, origin) {
+ dump("idp: validateAssertion(" + assertion + ")\n");
+ var assertion = JSON.parse(assertion);
+ if (instructions.some(is("bad-validate"))) {
+ assertion.contents = {};
+ }
+ return this.borkResult({
+ identity: assertion.username,
+ contents: assertion.contents,
+ });
+ },
+ };
+
+ if (!instructions.some(is("not_ready"))) {
+ dump("registering idp.js" + global.location.hash + "\n");
+ var idp = new IDPJS();
+ global.rtcIdentityProvider.register({
+ generateAssertion: idp.generateAssertion.bind(idp),
+ validateAssertion: idp.validateAssertion.bind(idp),
+ });
+ }
+})(this);
diff --git a/dom/media/webrtc/tests/mochitests/identity/idp.sjs b/dom/media/webrtc/tests/mochitests/identity/idp.sjs
new file mode 100644
index 0000000000..e1a245be78
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/idp.sjs
@@ -0,0 +1,18 @@
+function handleRequest(request, response) {
+ var key = "/.well-known/idp-proxy/" + request.queryString;
+ dump(getState(key) + "\n");
+ if (request.method === "GET") {
+ if (getState(key)) {
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ } else {
+ response.setStatusLine(request.httpVersion, 404, "Not Found");
+ }
+ } else if (request.method === "PUT") {
+ setState(key, "OK");
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ } else {
+ response.setStatusLine(request.httpVersion, 406, "Method Not Allowed");
+ }
+ response.setHeader("Content-Type", "text/plain;charset=UTF-8");
+ response.write("OK");
+}
diff --git a/dom/media/webrtc/tests/mochitests/identity/login.html b/dom/media/webrtc/tests/mochitests/identity/login.html
new file mode 100644
index 0000000000..eafba22f2d
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/login.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Identity Provider Login</title>
+ <script type="application/javascript">
+ window.onload = () => {
+ var xhr = new XMLHttpRequest();
+ xhr.open("PUT", "https://example.com/.well-known/idp-proxy/idp.sjs?" +
+ window.location.hash.replace('#', ''));
+ xhr.onload = () => {
+ var isFramed = (window !== window.top);
+ var parent = isFramed ? window.parent : window.opener;
+ // Using '*' is cheating, but that's OK.
+ parent.postMessage('LOGINDONE', '*');
+ var done = document.createElement('div');
+
+ done.textContent = 'Done';
+ document.body.appendChild(done);
+
+ if (!isFramed) {
+ window.close();
+ }
+ };
+ xhr.send();
+ };
+ </script>
+</head>
+<body>
+ <div>Logging in...</div>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/identity/mochitest.ini b/dom/media/webrtc/tests/mochitests/identity/mochitest.ini
new file mode 100644
index 0000000000..7a60cc6c6e
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/mochitest.ini
@@ -0,0 +1,47 @@
+[DEFAULT]
+subsuite = media
+skip-if = (os == 'linux' && !debug)
+support-files =
+ /.well-known/idp-proxy/idp.js
+ identityPcTest.js
+ !/dom/media/webrtc/tests/mochitests/blacksilence.js
+ !/dom/media/webrtc/tests/mochitests/dataChannel.js
+ !/dom/media/webrtc/tests/mochitests/head.js
+ !/dom/media/webrtc/tests/mochitests/network.js
+ !/dom/media/webrtc/tests/mochitests/pc.js
+ !/dom/media/webrtc/tests/mochitests/sdpUtils.js
+ !/dom/media/webrtc/tests/mochitests/templates.js
+ !/dom/media/webrtc/tests/mochitests/turnConfig.js
+tags = mtg
+
+[test_fingerprints.html]
+scheme=https
+[test_getIdentityAssertion.html]
+[test_idpproxy.html]
+support-files =
+ /.well-known/idp-proxy/idp-redirect-http.js
+ /.well-known/idp-proxy/idp-redirect-http.js^headers^
+ /.well-known/idp-proxy/idp-redirect-http-trick.js
+ /.well-known/idp-proxy/idp-redirect-http-trick.js^headers^
+ /.well-known/idp-proxy/idp-redirect-https.js
+ /.well-known/idp-proxy/idp-redirect-https.js^headers^
+ /.well-known/idp-proxy/idp-redirect-https-double.js
+ /.well-known/idp-proxy/idp-redirect-https-double.js^headers^
+ /.well-known/idp-proxy/idp-redirect-https-odd-path.js
+ /.well-known/idp-proxy/idp-redirect-https-odd-path.js^headers^
+ /.well-known/idp-min.js
+ /.well-known/idp-proxy/idp-bad.js
+[test_loginNeeded.html]
+support-files =
+ /.well-known/idp-proxy/login.html
+ /.well-known/idp-proxy/idp.sjs
+[test_peerConnection_asymmetricIsolation.html]
+scheme=https
+skip-if = os == 'android'
+[test_peerConnection_peerIdentity.html]
+scheme=https
+skip-if = os == 'android'
+[test_setIdentityProvider.html]
+scheme=https
+[test_setIdentityProviderWithErrors.html]
+scheme=https
diff --git a/dom/media/webrtc/tests/mochitests/identity/test_fingerprints.html b/dom/media/webrtc/tests/mochitests/identity/test_fingerprints.html
new file mode 100644
index 0000000000..0a7f0a2033
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/test_fingerprints.html
@@ -0,0 +1,91 @@
+<html>
+<head>
+<meta charset="utf-8" />
+<script type="application/javascript">var scriptRelativePath = "../";</script>
+<script type="application/javascript" src="../pc.js"></script>
+</head>
+<body>
+<script class="testbody" type="application/javascript">
+createHTML({ title: "Test multiple identity fingerprints", bug: "1005152" });
+
+// here we call the identity provider directly
+async function getIdentityAssertion(fingerprint) {
+ const { IdpSandbox } = SpecialPowers.ChromeUtils.import(
+ 'resource://gre/modules/media/IdpSandbox.jsm'
+ );
+ const sandbox = new IdpSandbox('example.com', 'idp.js', window);
+ const idp = SpecialPowers.wrap(await sandbox.start());
+ const assertion = SpecialPowers.wrap(await
+ idp.generateAssertion(JSON.stringify({ fingerprint }),
+ 'https://example.com',
+ {}));
+ const assertionString = btoa(JSON.stringify(assertion));
+ sandbox.stop();
+ return assertionString;
+}
+
+// This takes a real fingerprint and makes some extra bad ones.
+function makeFingerprints(algorithm, digest) {
+ const fingerprints = [];
+ fingerprints.push({ algorithm, digest });
+ for (var i = 0; i < 3; ++i) {
+ fingerprints.push({
+ algorithm,
+ digest: digest.replace(/:./g, ':' + i.toString(16))
+ });
+ }
+ return fingerprints;
+}
+
+const fingerprintRegex = /^a=fingerprint:(\S+) (\S+)/m;
+const identityRegex = /^a=identity:(\S+)/m;
+
+function fingerprintSdp(fingerprints) {
+ return fingerprints.map(fp => 'a=fInGeRpRiNt:' + fp.algorithm +
+ ' ' + fp.digest + '\n').join('');
+}
+
+// Firefox only uses a single fingerprint.
+// That doesn't mean we have it create SDP that describes two.
+// This function synthesizes that SDP and tries to set it.
+
+runNetworkTest(async () => {
+ // this one fails setRemoteDescription if the identity is not good
+ const pcStrict = new RTCPeerConnection({ peerIdentity: 'someone@example.com'});
+ // this one will be manually tweaked to have two fingerprints
+ const pcDouble = new RTCPeerConnection({});
+
+ const stream = await getUserMedia({ video: true });
+ ok(stream, 'Got test stream');
+ const [track] = stream.getTracks();
+ pcDouble.addTrack(track, stream);
+ try {
+ const offer = await pcDouble.createOffer();
+ ok(offer, 'Got offer');
+ const match = offer.sdp.match(fingerprintRegex);
+ if (!match) {
+ throw new Error('No fingerprint in offer SDP');
+ }
+ const fingerprints = makeFingerprints(match[1], match[2]);
+ const assertion = await getIdentityAssertion(fingerprints);
+ ok(assertion, 'Should have assertion');
+
+ const sdp = offer.sdp.slice(0, match.index) +
+ 'a=identity:' + assertion + '\n' +
+ fingerprintSdp(fingerprints.slice(1)) +
+ offer.sdp.slice(match.index);
+
+ await pcStrict.setRemoteDescription({ type: 'offer', sdp });
+ ok(true, 'Modified fingerprints were accepted');
+ } catch (error) {
+ const e = SpecialPowers.wrap(error);
+ ok(false, 'error in test: ' +
+ (e.message ? (e.message + '\n' + e.stack) : e));
+ }
+ pcStrict.close();
+ pcDouble.close();
+ track.stop();
+});
+</script>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/identity/test_getIdentityAssertion.html b/dom/media/webrtc/tests/mochitests/identity/test_getIdentityAssertion.html
new file mode 100644
index 0000000000..47e1cb1df6
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/test_getIdentityAssertion.html
@@ -0,0 +1,101 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript">var scriptRelativePath = "../";</script>
+ <script type="application/javascript" src="../pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: "getIdentityAssertion Tests",
+ bug: "942367"
+ });
+
+function checkIdentity(assertion, identity) {
+ // here we dig into the payload, which means we need to know something
+ // about how the IdP actually works (not good in general, but OK here)
+ var assertion = JSON.parse(atob(assertion)).assertion;
+ var user = JSON.parse(assertion).username;
+ is(user, identity, 'id should be "' + identity + '" is "' + user + '"');
+}
+
+function getAssertion(t, instructions, userHint) {
+ dump('instructions: ' + instructions + '\n');
+ dump('userHint: ' + userHint + '\n');
+ t.pcLocal.setIdentityProvider('example.com',
+ { protocol: 'idp.js' + instructions,
+ usernameHint: userHint });
+ return t.pcLocal._pc.getIdentityAssertion();
+}
+
+var test;
+function theTest() {
+ test = new PeerConnectionTest();
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.chain.removeAfter('PC_REMOTE_CHECK_INITIAL_SIGNALINGSTATE');
+ test.chain.append([
+ function PC_LOCAL_IDENTITY_ASSERTION_FAILS_WITHOUT_PROVIDER(t) {
+ return t.pcLocal._pc.getIdentityAssertion()
+ .then(a => ok(false, 'should fail without provider'),
+ e => ok(e, 'should fail without provider'));
+ },
+
+ function PC_LOCAL_IDENTITY_ASSERTION_FAILS_WITH_BAD_PROVIDER(t) {
+ t.pcLocal._pc.setIdentityProvider('example.com',
+ { protocol: 'idp-bad.js',
+ usernameHint: '' });
+ return t.pcLocal._pc.getIdentityAssertion()
+ .then(a => ok(false, 'should fail with bad provider'),
+ e => {
+ is(e.name, 'IdpError', 'should fail with bad provider');
+ ok(e.message, 'should include a nice message');
+ });
+ },
+
+ function PC_LOCAL_GET_TWO_ASSERTIONS(t) {
+ return Promise.all([
+ getAssertion(t, ''),
+ getAssertion(t, '')
+ ]).then(assertions => {
+ is(assertions.length, 2, "Two assertions generated");
+ assertions.forEach(a => checkIdentity(a, 'someone@example.com'));
+ });
+ },
+
+ function PC_LOCAL_IDP_FAILS(t) {
+ return getAssertion(t, '#fail')
+ .then(a => ok(false, '#fail should not get an identity result'),
+ e => is(e.name, 'IdpError', '#fail should cause rejection'));
+ },
+
+ function PC_LOCAL_IDP_LOGIN_ERROR(t) {
+ return getAssertion(t, '#login')
+ .then(a => ok(false, '#login should not work'),
+ e => {
+ is(e.name, 'IdpLoginError', 'name is IdpLoginError');
+ is(t.pcLocal._pc.idpLoginUrl.split('#')[0],
+ 'https://example.com/.well-known/idp-proxy/login.html',
+ 'got the right login URL from the IdP');
+ });
+ },
+
+ function PC_LOCAL_IDP_NOT_READY(t) {
+ return getAssertion(t, '#not_ready')
+ .then(a => ok(false, '#not_ready should not get an identity result'),
+ e => is(e.name, 'IdpError', '#not_ready should cause rejection'));
+ },
+
+ function PC_LOCAL_ASSERTION_WITH_SPECIFIC_NAME(t) {
+ return getAssertion(t, '', 'user@example.com')
+ .then(a => checkIdentity(a, 'user@example.com'));
+ }
+ ]);
+ return test.run();
+}
+runNetworkTest(theTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/identity/test_idpproxy.html b/dom/media/webrtc/tests/mochitests/identity/test_idpproxy.html
new file mode 100644
index 0000000000..065501b8a4
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/test_idpproxy.html
@@ -0,0 +1,178 @@
+<html>
+<head>
+<meta charset="utf-8" />
+<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+<script src="/tests/SimpleTest/SimpleTest.js"></script>
+</head>
+<body>
+ <script class="testbody" type="application/javascript">
+"use strict";
+var { IdpSandbox } = SpecialPowers.ChromeUtils.import(
+ "resource://gre/modules/media/IdpSandbox.jsm"
+);
+var dummyPayload = JSON.stringify({
+ this: 'is',
+ a: ['stu', 6],
+ obj: null
+});
+
+function test_domain_sandbox() {
+ var diabolical = {
+ toString() {
+ return 'example.com/path';
+ }
+ };
+ var domains = [ 'ex/foo', 'user@ex', 'user:pass@ex', 'ex#foo', 'ex?foo',
+ '', 12, null, diabolical, true ];
+ domains.forEach(function(domain) {
+ try {
+ var idp = new IdpSandbox(domain, undefined, window);
+ ok(false, 'IdpSandbox allowed a bad domain: ' + domain);
+ } catch (e) {
+ var str = (typeof domain === 'string') ? domain : typeof domain;
+ ok(true, 'Evil domain "' + str + '" raises exception');
+ }
+ });
+}
+
+function test_protocol_sandbox() {
+ var protos = [ '../evil/proto', '..%2Fevil%2Fproto',
+ '\\evil', '%5cevil', 12, true, {} ];
+ protos.forEach(function(proto) {
+ try {
+ var idp = new IdpSandbox('example.com', proto, window);
+ ok(false, 'IdpSandbox allowed a bad protocol: ' + proto);
+ } catch (e) {
+ var str = (typeof proto === 'string') ? proto : typeof proto;
+ ok(true, 'Evil protocol "' + proto + '" raises exception');
+ }
+ });
+}
+
+function idpName(hash) {
+ return 'idp.js' + (hash ? ('#' + hash) : '');
+}
+
+function makeSandbox(js) {
+ var name = js || idpName();
+ info('Creating a sandbox for the protocol: ' + name);
+ var sandbox = new IdpSandbox('example.com', name, window);
+ return sandbox.start().then(idp => SpecialPowers.wrap(idp));
+}
+
+function test_generate_assertion() {
+ return makeSandbox()
+ .then(idp => idp.generateAssertion(dummyPayload,
+ 'https://example.net',
+ {}))
+ .then(response => {
+ response = SpecialPowers.wrap(response);
+ is(response.idp.domain, 'example.com', 'domain is correct');
+ is(response.idp.protocol, 'idp.js', 'protocol is correct');
+ ok(typeof response.assertion === 'string', 'assertion is present');
+ });
+}
+
+// test that the test IdP can eat its own dogfood; which is the only way to test
+// validateAssertion, since that consumes the output of generateAssertion (in
+// theory, generateAssertion could identify a different IdP domain).
+
+function test_validate_assertion() {
+ return makeSandbox()
+ .then(idp => idp.generateAssertion(dummyPayload,
+ 'https://example.net',
+ { usernameHint: 'user' }))
+ .then(assertion => {
+ var wrapped = SpecialPowers.wrap(assertion);
+ return makeSandbox()
+ .then(idp => idp.validateAssertion(wrapped.assertion,
+ 'https://example.net'));
+ }).then(response => {
+ response = SpecialPowers.wrap(response);
+ is(response.identity, 'user@example.com');
+ is(response.contents, dummyPayload);
+ });
+}
+
+// We don't want to test the #bad or the #hang instructions,
+// errors of the sort those generate aren't handled by the sandbox code.
+function test_assertion_failure(reason) {
+ return () => {
+ return makeSandbox(idpName(reason))
+ .then(idp => idp.generateAssertion('hello', 'example.net', {}))
+ .then(r => ok(false, 'should not succeed on ' + reason),
+ e => ok(true, 'failed correctly on ' + reason));
+ };
+}
+
+function test_load_failure() {
+ return makeSandbox('non-existent-file')
+ .then(() => ok(false, 'Should fail to load non-existent file'),
+ e => ok(e, 'Should fail to load non-existent file'));
+}
+
+function test_redirect_ok(from) {
+ return () => {
+ return makeSandbox(from)
+ .then(idp => idp.generateAssertion('hello', 'example.net'))
+ .then(r => ok(SpecialPowers.wrap(r).assertion,
+ 'Redirect to https should be OK'));
+ };
+}
+
+function test_redirect_fail(from) {
+ return () => {
+ return makeSandbox(from)
+ .then(() => ok(false, 'Redirect to https should fail'),
+ e => ok(e, 'Redirect to https should fail'));
+ };
+}
+
+function test_bad_js() {
+ return makeSandbox('idp-bad.js')
+ .then(() => ok(false, 'Bad JS should not load'),
+ e => ok(e, 'Bad JS should not load'));
+}
+
+function run_all_tests() {
+ [
+ test_domain_sandbox,
+ test_protocol_sandbox,
+ test_generate_assertion,
+ test_validate_assertion,
+
+ // fail of the IdP fails
+ test_assertion_failure('fail'),
+ // fail if the IdP throws
+ test_assertion_failure('throw'),
+ // fail if the IdP is not ready
+ test_assertion_failure('not_ready'),
+
+ test_load_failure(),
+ // Test a redirect to an HTTPS origin, which should be OK
+ test_redirect_ok('idp-redirect-https.js'),
+ // Two redirects is fine too
+ test_redirect_ok('idp-redirect-https-double.js'),
+ // A secure redirect to a path other than /.well-known/idp-proxy/* should
+ // also work fine.
+ test_redirect_ok('idp-redirect-https-odd-path.js'),
+ // A redirect to HTTP is not-cool
+ test_redirect_fail('idp-redirect-http.js'),
+ // Also catch tricks like https->http->https
+ test_redirect_fail('idp-redirect-http-trick.js'),
+
+ test_bad_js
+ ].reduce((p, test) => {
+ return p.then(test)
+ .catch(e => ok(false, test.name + ' failed: ' +
+ SpecialPowers.wrap(e).message + '\n' +
+ SpecialPowers.wrap(e).stack));
+ }, Promise.resolve())
+ .then(() => SimpleTest.finish());
+}
+
+SimpleTest.waitForExplicitFinish();
+run_all_tests();
+</script>
+ </body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/identity/test_loginNeeded.html b/dom/media/webrtc/tests/mochitests/identity/test_loginNeeded.html
new file mode 100644
index 0000000000..550dc20d92
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/test_loginNeeded.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript">var scriptRelativePath = "../";</script>
+ <script type="application/javascript" src="../pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: 'RTCPeerConnection identity with login',
+ bug: '1153314'
+ });
+
+function waitForLoginDone() {
+ return new Promise(resolve => {
+ window.addEventListener('message', function listener(e) {
+ is(e.origin, 'https://example.com', 'got the right message origin');
+ is(e.data, 'LOGINDONE', 'got the right message');
+ window.removeEventListener('message', listener);
+ resolve();
+ });
+ });
+}
+
+function checkLogin(t, name, onLoginNeeded) {
+ t.pcLocal.setIdentityProvider('example.com',
+ { protocol: 'idp.js#login:' + name });
+ return t.pcLocal._pc.getIdentityAssertion()
+ .then(a => ok(false, 'should request login'),
+ e => {
+ is(e.name, 'IdpLoginError', 'name is IdpLoginError');
+ is(t.pcLocal._pc.idpLoginUrl.split('#')[0],
+ 'https://example.com/.well-known/idp-proxy/login.html',
+ 'got the right login URL from the IdP');
+ return t.pcLocal._pc.idpLoginUrl;
+ })
+ .then(onLoginNeeded)
+ .then(waitForLoginDone)
+ .then(() => t.pcLocal._pc.getIdentityAssertion())
+ .then(a => ok(a, 'got assertion'));
+}
+
+function theTest() {
+ var test = new PeerConnectionTest();
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.chain.removeAfter('PC_REMOTE_CHECK_INITIAL_SIGNALINGSTATE');
+ test.chain.append([
+ function PC_LOCAL_IDENTITY_ASSERTION_WITH_IFRAME_LOGIN(t) {
+ return checkLogin(t, 'iframe', loginUrl => {
+ var iframe = document.createElement('iframe');
+ iframe.setAttribute('src', loginUrl);
+ iframe.frameBorder = 0;
+ iframe.width = 400;
+ iframe.height = 60;
+ document.getElementById('display').appendChild(iframe);
+ });
+ },
+ function PC_LOCAL_IDENTITY_ASSERTION_WITH_WINDOW_LOGIN(t) {
+ return checkLogin(t, 'openwin', loginUrl => {
+ window.open(loginUrl, 'login', 'width=400,height=60');
+ });
+ }
+ ]);
+ return test.run();
+}
+runNetworkTest(theTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/identity/test_peerConnection_asymmetricIsolation.html b/dom/media/webrtc/tests/mochitests/identity/test_peerConnection_asymmetricIsolation.html
new file mode 100644
index 0000000000..65a2fc5392
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/test_peerConnection_asymmetricIsolation.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript">var scriptRelativePath = "../";</script>
+ <script type="application/javascript" src="../pc.js"></script>
+ <script type="application/javascript" src="../blacksilence.js"></script>
+ <script type="application/javascript" src="identityPcTest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ title: "Non-isolated media entering an isolated session becomes isolated",
+ bug: "996238"
+});
+
+function theTest() {
+ // Override the remote media capture options to remove isolation for the
+ // remote party; the test verifies that the media it receives on the local
+ // side is isolated anyway.
+ return identityPcTest({
+ audio: true,
+ video: true
+ });
+}
+runNetworkTest(theTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/identity/test_peerConnection_peerIdentity.html b/dom/media/webrtc/tests/mochitests/identity/test_peerConnection_peerIdentity.html
new file mode 100644
index 0000000000..a8116cc451
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/test_peerConnection_peerIdentity.html
@@ -0,0 +1,21 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript">var scriptRelativePath = "../";</script>
+ <script type="application/javascript" src="../pc.js"></script>
+ <script type="application/javascript" src="../blacksilence.js"></script>
+ <script type="application/javascript" src="identityPcTest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ title: "setIdentityProvider leads to peerIdentity and assertions in SDP",
+ bug: "942367"
+});
+
+runNetworkTest(identityPcTest);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/identity/test_setIdentityProvider.html b/dom/media/webrtc/tests/mochitests/identity/test_setIdentityProvider.html
new file mode 100644
index 0000000000..ac7cba6a5e
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/test_setIdentityProvider.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript">var scriptRelativePath = "../";</script>
+ <script type="application/javascript" src="../pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: "setIdentityProvider leads to peerIdentity and assertions in SDP",
+ bug: "942367"
+ });
+
+function checkIdentity(peer, prefix, idp, name) {
+ prefix = prefix + ": ";
+ return peer._pc.peerIdentity.then(peerIdentity => {
+ ok(peerIdentity, prefix + "peerIdentity is set");
+ is(peerIdentity.idp, idp, prefix + "IdP is correct");
+ is(peerIdentity.name, name + "@" + idp, prefix + "identity is correct");
+ });
+}
+
+function theTest() {
+ var test = new PeerConnectionTest();
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.pcLocal.setIdentityProvider("test1.example.com",
+ { protocol: "idp.js",
+ usernameHint: "someone" });
+ test.pcRemote.setIdentityProvider("test2.example.com",
+ { protocol: "idp.js",
+ usernameHinte: "someone"});
+
+ test.chain.append([
+ function PC_LOCAL_PEER_IDENTITY_IS_SET_CORRECTLY(test) {
+ return checkIdentity(test.pcLocal, "local", "test2.example.com", "someone");
+ },
+ function PC_REMOTE_PEER_IDENTITY_IS_SET_CORRECTLY(test) {
+ return checkIdentity(test.pcRemote, "remote", "test1.example.com", "someone");
+ },
+
+ function OFFER_AND_ANSWER_INCLUDES_IDENTITY(test) {
+ ok(test.originalOffer.sdp.includes("a=identity"), "a=identity is in the offer SDP");
+ ok(test.originalAnswer.sdp.includes("a=identity"), "a=identity is in the answer SDP");
+ },
+
+ function PC_LOCAL_DESCRIPTIONS_CONTAIN_IDENTITY(test) {
+ ok(test.pcLocal.localDescription.sdp.includes("a=identity"),
+ "a=identity is in the local copy of the offer");
+ ok(test.pcLocal.remoteDescription.sdp.includes("a=identity"),
+ "a=identity is in the local copy of the answer");
+ },
+ function PC_REMOTE_DESCRIPTIONS_CONTAIN_IDENTITY(test) {
+ ok(test.pcRemote.localDescription.sdp.includes("a=identity"),
+ "a=identity is in the remote copy of the offer");
+ ok(test.pcRemote.remoteDescription.sdp.includes("a=identity"),
+ "a=identity is in the remote copy of the answer");
+ }
+ ]);
+ return test.run();
+}
+runNetworkTest(theTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/identity/test_setIdentityProviderWithErrors.html b/dom/media/webrtc/tests/mochitests/identity/test_setIdentityProviderWithErrors.html
new file mode 100644
index 0000000000..ce6832d1e6
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/identity/test_setIdentityProviderWithErrors.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript">var scriptRelativePath = "../";</script>
+ <script type="application/javascript" src="../pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+'use strict';
+ createHTML({
+ title: "Identity Provider returning errors is handled correctly",
+ bug: "942367"
+ });
+
+runNetworkTest(function () {
+ var test = new PeerConnectionTest();
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ // No IdP for local.
+ // Remote generates a bad assertion, but that only fails to validate
+ test.pcRemote.setIdentityProvider('example.com',
+ { protocol: 'idp.js#bad-validate',
+ usernameHint: 'nobody' });
+
+ // Save the peerIdentity promises now, since when they reject they are
+ // replaced and we expect them to be rejected this time
+ var peerIdentityLocal = test.pcLocal._pc.peerIdentity;
+ var peerIdentityRemote = test.pcRemote._pc.peerIdentity;
+
+ test.chain.append([
+ function ONLY_REMOTE_SDP_INCLUDES_IDENTITY_ASSERTION(t) {
+ ok(!t.originalOffer.sdp.includes('a=identity'),
+ 'a=identity not contained in the offer SDP');
+ ok(t.originalAnswer.sdp.includes('a=identity'),
+ 'a=identity is contained in the answer SDP');
+ },
+ function PEER_IDENTITY_IS_EMPTY(t) {
+ // we are only waiting for the local side to complete
+ // an error on the remote side is immediately fatal though
+ return Promise.race([
+ peerIdentityLocal.then(
+ () => ok(false, t.pcLocal + ' incorrectly received valid peer identity'),
+ e => ok(e, t.pcLocal + ' correctly failed to validate peer identity')),
+ peerIdentityRemote.then(
+ () => ok(false, t.pcRemote + ' incorrecly received a valid peer identity'),
+ e => ok(false, t.pcRemote + ' incorrectly rejected peer identity'))
+ ]);
+ }
+ ]);
+
+ return test.run();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/mediaStreamPlayback.js b/dom/media/webrtc/tests/mochitests/mediaStreamPlayback.js
new file mode 100644
index 0000000000..44c1c78ea0
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/mediaStreamPlayback.js
@@ -0,0 +1,241 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const ENDED_TIMEOUT_LENGTH = 30000;
+
+/* The time we wait depends primarily on the canplaythrough event firing
+ * Note: this needs to be at least 30s because the
+ * B2G emulator in VMs is really slow. */
+const VERIFYPLAYING_TIMEOUT_LENGTH = 60000;
+
+/**
+ * This class manages playback of a HTMLMediaElement with a MediaStream.
+ * When constructed by a caller, an object instance is created with
+ * a media element and a media stream object.
+ *
+ * @param {HTMLMediaElement} mediaElement the media element for playback
+ * @param {MediaStream} mediaStream the media stream used in
+ * the mediaElement for playback
+ */
+function MediaStreamPlayback(mediaElement, mediaStream) {
+ this.mediaElement = mediaElement;
+ this.mediaStream = mediaStream;
+}
+
+MediaStreamPlayback.prototype = {
+ /**
+ * Starts media element with a media stream, runs it until a canplaythrough
+ * and timeupdate event fires, and calls stop() on all its tracks.
+ *
+ * @param {Boolean} isResume specifies if this media element is being resumed
+ * from a previous run
+ */
+ playMedia(isResume) {
+ this.startMedia(isResume);
+ return this.verifyPlaying()
+ .then(() => this.stopTracksForStreamInMediaPlayback())
+ .then(() => this.detachFromMediaElement());
+ },
+
+ /**
+ * Stops the local media stream's tracks while it's currently in playback in
+ * a media element.
+ *
+ * Precondition: The media stream and element should both be actively
+ * being played. All the stream's tracks must be local.
+ */
+ stopTracksForStreamInMediaPlayback() {
+ var elem = this.mediaElement;
+ return Promise.all([
+ haveEvent(
+ elem,
+ "ended",
+ wait(ENDED_TIMEOUT_LENGTH, new Error("Timeout"))
+ ),
+ ...this.mediaStream
+ .getTracks()
+ .map(t => (t.stop(), haveNoEvent(t, "ended"))),
+ ]);
+ },
+
+ /**
+ * Starts media with a media stream, runs it until a canplaythrough and
+ * timeupdate event fires, and detaches from the element without stopping media.
+ *
+ * @param {Boolean} isResume specifies if this media element is being resumed
+ * from a previous run
+ */
+ playMediaWithoutStoppingTracks(isResume) {
+ this.startMedia(isResume);
+ return this.verifyPlaying().then(() => this.detachFromMediaElement());
+ },
+
+ /**
+ * Starts the media with the associated stream.
+ *
+ * @param {Boolean} isResume specifies if the media element playback
+ * is being resumed from a previous run
+ */
+ startMedia(isResume) {
+ // If we're playing media element for the first time, check that time is zero.
+ if (!isResume) {
+ is(
+ this.mediaElement.currentTime,
+ 0,
+ "Before starting the media element, currentTime = 0"
+ );
+ }
+ this.canPlayThroughFired = listenUntil(
+ this.mediaElement,
+ "canplaythrough",
+ () => true
+ );
+
+ // Hooks up the media stream to the media element and starts playing it
+ this.mediaElement.srcObject = this.mediaStream;
+ this.mediaElement.play();
+ },
+
+ /**
+ * Verifies that media is playing.
+ */
+ verifyPlaying() {
+ var lastElementTime = this.mediaElement.currentTime;
+
+ var mediaTimeProgressed = listenUntil(
+ this.mediaElement,
+ "timeupdate",
+ () => this.mediaElement.currentTime > lastElementTime
+ );
+
+ return timeout(
+ Promise.all([this.canPlayThroughFired, mediaTimeProgressed]),
+ VERIFYPLAYING_TIMEOUT_LENGTH,
+ "verifyPlaying timed out"
+ ).then(() => {
+ is(this.mediaElement.paused, false, "Media element should be playing");
+ is(
+ this.mediaElement.duration,
+ Number.POSITIVE_INFINITY,
+ "Duration should be infinity"
+ );
+
+ // When the media element is playing with a real-time stream, we
+ // constantly switch between having data to play vs. queuing up data,
+ // so we can only check that the ready state is one of those two values
+ ok(
+ this.mediaElement.readyState === HTMLMediaElement.HAVE_ENOUGH_DATA ||
+ this.mediaElement.readyState === HTMLMediaElement.HAVE_CURRENT_DATA,
+ "Ready state shall be HAVE_ENOUGH_DATA or HAVE_CURRENT_DATA"
+ );
+
+ is(this.mediaElement.seekable.length, 0, "Seekable length shall be zero");
+ is(this.mediaElement.buffered.length, 0, "Buffered length shall be zero");
+
+ is(
+ this.mediaElement.seeking,
+ false,
+ "MediaElement is not seekable with MediaStream"
+ );
+ ok(
+ isNaN(this.mediaElement.startOffsetTime),
+ "Start offset time shall not be a number"
+ );
+ is(
+ this.mediaElement.defaultPlaybackRate,
+ 1,
+ "DefaultPlaybackRate should be 1"
+ );
+ is(this.mediaElement.playbackRate, 1, "PlaybackRate should be 1");
+ is(this.mediaElement.preload, "none", 'Preload should be "none"');
+ is(this.mediaElement.src, "", "No src should be defined");
+ is(
+ this.mediaElement.currentSrc,
+ "",
+ "Current src should still be an empty string"
+ );
+ });
+ },
+
+ /**
+ * Detaches from the element without stopping the media.
+ *
+ * Precondition: The media stream and element should both be actively
+ * being played.
+ */
+ detachFromMediaElement() {
+ this.mediaElement.pause();
+ this.mediaElement.srcObject = null;
+ },
+};
+
+// haxx to prevent SimpleTest from failing at window.onload
+function addLoadEvent() {}
+
+/* import-globals-from /testing/mochitest/tests/SimpleTest/SimpleTest.js */
+/* import-globals-from head.js */
+const scriptsReady = Promise.all(
+ ["/tests/SimpleTest/SimpleTest.js", "head.js"].map(script => {
+ const el = document.createElement("script");
+ el.src = script;
+ document.head.appendChild(el);
+ return new Promise(r => (el.onload = r));
+ })
+);
+
+function createHTML(options) {
+ return scriptsReady.then(() => realCreateHTML(options));
+}
+
+async function runTest(testFunction) {
+ await Promise.all([
+ scriptsReady,
+ SpecialPowers.pushPrefEnv({
+ set: [["media.navigator.permission.fake", true]],
+ }),
+ ]);
+ await runTestWhenReady(async (...args) => {
+ await testFunction(...args);
+ await noGum();
+ });
+}
+
+// noGum - Helper to detect whether active guM tracks still exist.
+//
+// Note it relies on the permissions system to detect active tracks, so it won't
+// catch getUserMedia use while media.navigator.permission.disabled is true
+// (which is common in automation), UNLESS we set
+// media.navigator.permission.fake to true also, like runTest() does above.
+async function noGum() {
+ if (!navigator.mediaDevices) {
+ // No mediaDevices, then gUM cannot have been called either.
+ return;
+ }
+ const mediaManagerService = Cc[
+ "@mozilla.org/mediaManagerService;1"
+ ].getService(Ci.nsIMediaManagerService);
+
+ const hasCamera = {};
+ const hasMicrophone = {};
+ mediaManagerService.mediaCaptureWindowState(
+ window,
+ hasCamera,
+ hasMicrophone,
+ {},
+ {},
+ {},
+ {},
+ false
+ );
+ is(
+ hasCamera.value,
+ mediaManagerService.STATE_NOCAPTURE,
+ "Test must leave no active camera gUM tracks behind."
+ );
+ is(
+ hasMicrophone.value,
+ mediaManagerService.STATE_NOCAPTURE,
+ "Test must leave no active microphone gUM tracks behind."
+ );
+}
diff --git a/dom/media/webrtc/tests/mochitests/mochitest.ini b/dom/media/webrtc/tests/mochitests/mochitest.ini
new file mode 100644
index 0000000000..d1a9800984
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/mochitest.ini
@@ -0,0 +1,65 @@
+[DEFAULT]
+tags = mtg webrtc
+subsuite = media
+scheme = https
+support-files =
+ head.js
+ dataChannel.js
+ mediaStreamPlayback.js
+ network.js
+ nonTrickleIce.js
+ pc.js
+ stats.js
+ templates.js
+ test_enumerateDevices_iframe.html
+ test_enumerateDevices_iframe_pre_gum.html
+ test_getUserMedia_permission_iframe.html
+ NetworkPreparationChromeScript.js
+ blacksilence.js
+ turnConfig.js
+ sdpUtils.js
+ addTurnsSelfsignedCert.js
+ parser_rtp.js
+ peerconnection_audio_forced_sample_rate.js
+ iceTestUtils.js
+ simulcast.js
+ helpers_from_wpt/sdp.js
+ !/dom/canvas/test/captureStream_common.js
+ !/dom/canvas/test/webgl-mochitest/webgl-util.js
+ !/dom/media/test/manifest.js
+ !/dom/media/test/seek.webm
+ !/dom/media/test/gizmo.mp4
+ !/docshell/test/navigation/blank.html
+prefs =
+ focusmanager.testmode=true # emulate focus
+ privacy.partition.network_state=false
+ network.proxy.allow_hijacking_localhost=true
+ media.devices.enumerate.legacy.enabled=false
+
+[test_1488832.html]
+skip-if =
+ os == 'linux' # Bug 1714410
+[test_1717318.html]
+[test_a_noOp.html]
+scheme=http
+[test_enumerateDevices.html]
+[test_enumerateDevices_getUserMediaFake.html]
+[test_enumerateDevices_legacy.html]
+[test_enumerateDevices_navigation.html]
+skip-if = true # Disabled because it is a racy test and causes timeouts, see bug 1650932
+[test_fingerprinting_resistance.html]
+skip-if =
+ os == "linux" && asan # Bug 1646309 - low frequency intermittent
+[test_forceSampleRate.html]
+scheme=http
+[test_groupId.html]
+[test_multi_mics.html]
+skip-if = os == 'android'
+[test_ondevicechange.html]
+run-sequentially = sets prefs that may disrupt other tests
+[test_setSinkId.html]
+skip-if =
+ os != 'linux' # the only platform with real devices
+[test_setSinkId_default_addTrack.html]
+[test_setSinkId_preMutedElement.html]
+[test_unfocused_pref.html]
diff --git a/dom/media/webrtc/tests/mochitests/mochitest_datachannel.ini b/dom/media/webrtc/tests/mochitests/mochitest_datachannel.ini
new file mode 100644
index 0000000000..881421af0e
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/mochitest_datachannel.ini
@@ -0,0 +1,52 @@
+[DEFAULT]
+tags = mtg webrtc
+subsuite = media
+scheme = https
+support-files =
+ head.js
+ dataChannel.js
+ mediaStreamPlayback.js
+ network.js
+ nonTrickleIce.js
+ pc.js
+ stats.js
+ templates.js
+ test_enumerateDevices_iframe.html
+ test_getUserMedia_permission_iframe.html
+ NetworkPreparationChromeScript.js
+ blacksilence.js
+ turnConfig.js
+ sdpUtils.js
+ addTurnsSelfsignedCert.js
+ parser_rtp.js
+ peerconnection_audio_forced_sample_rate.js
+ iceTestUtils.js
+ simulcast.js
+ helpers_from_wpt/sdp.js
+ !/dom/canvas/test/captureStream_common.js
+ !/dom/canvas/test/webgl-mochitest/webgl-util.js
+ !/dom/media/test/manifest.js
+ !/dom/media/test/seek.webm
+ !/dom/media/test/gizmo.mp4
+ !/docshell/test/navigation/blank.html
+prefs =
+ focusmanager.testmode=true # emulate focus
+ privacy.partition.network_state=false
+ network.proxy.allow_hijacking_localhost=true
+
+[test_dataChannel_basicAudio.html]
+[test_dataChannel_basicAudioVideo.html]
+[test_dataChannel_basicAudioVideoCombined.html]
+[test_dataChannel_basicAudioVideoNoBundle.html]
+[test_dataChannel_basicDataOnly.html]
+[test_dataChannel_basicVideo.html]
+[test_dataChannel_bug1013809.html]
+[test_dataChannel_dataOnlyBufferedAmountLow.html]
+scheme=http
+[test_dataChannel_dtlsVersions.html]
+[test_dataChannel_hostnameObfuscation.html]
+scheme=http
+[test_dataChannel_noOffer.html]
+scheme=http
+[test_dataChannel_stats.html]
+scheme=http
diff --git a/dom/media/webrtc/tests/mochitests/mochitest_getusermedia.ini b/dom/media/webrtc/tests/mochitests/mochitest_getusermedia.ini
new file mode 100644
index 0000000000..02c8272b5b
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/mochitest_getusermedia.ini
@@ -0,0 +1,105 @@
+[DEFAULT]
+tags = mtg webrtc
+subsuite = media
+scheme = https
+support-files =
+ head.js
+ dataChannel.js
+ mediaStreamPlayback.js
+ network.js
+ nonTrickleIce.js
+ pc.js
+ stats.js
+ templates.js
+ test_enumerateDevices_iframe.html
+ test_getUserMedia_permission_iframe.html
+ NetworkPreparationChromeScript.js
+ blacksilence.js
+ turnConfig.js
+ sdpUtils.js
+ addTurnsSelfsignedCert.js
+ parser_rtp.js
+ peerconnection_audio_forced_sample_rate.js
+ iceTestUtils.js
+ simulcast.js
+ helpers_from_wpt/sdp.js
+ !/dom/canvas/test/captureStream_common.js
+ !/dom/canvas/test/webgl-mochitest/webgl-util.js
+ !/dom/media/test/manifest.js
+ !/dom/media/test/seek.webm
+ !/dom/media/test/gizmo.mp4
+ !/docshell/test/navigation/blank.html
+prefs =
+ focusmanager.testmode=true # emulate focus
+ privacy.partition.network_state=false
+ network.proxy.allow_hijacking_localhost=true
+ media.devices.enumerate.legacy.enabled=false
+
+[test_defaultAudioConstraints.html]
+skip-if = os == 'mac'
+ os == 'win'
+ toolkit == 'android' # Bug 1404995, no loopback devices on some platforms
+[test_getUserMedia_GC_MediaStream.html]
+[test_getUserMedia_active_autoplay.html]
+[test_getUserMedia_addTrackRemoveTrack.html]
+[test_getUserMedia_addtrack_removetrack_events.html]
+[test_getUserMedia_audioCapture.html]
+skip-if = toolkit == 'android'
+ (os == "win" && processor == "aarch64") # android(Bug 1189784, timeouts on 4.3 emulator), android(Bug 1264333), aarch64 due to 1538359
+[test_getUserMedia_audioConstraints.html]
+skip-if = os == 'mac'
+ os == 'win'
+ toolkit == 'android' # Bug 1404995, no loopback devices on some platforms
+[test_getUserMedia_audioConstraints_concurrentIframes.html]
+skip-if = os == 'mac'
+ os == 'win'
+ toolkit == 'android'
+ (os == 'linux' && debug) # Bug 1404995, no loopback devices on some platforms # Bug 1481101
+ os == "linux" && !debug && !fission # bug 1645930, lower frequency intermittent
+[test_getUserMedia_audioConstraints_concurrentStreams.html]
+skip-if = os == 'mac'
+ os == 'win'
+ toolkit == 'android' # Bug 1404995, no loopback devices on some platforms
+[test_getUserMedia_basicAudio.html]
+[test_getUserMedia_basicAudio_loopback.html]
+skip-if = os == 'mac'
+ os == 'win'
+ toolkit == 'android' # Bug 1404995, no loopback devices on some platforms
+[test_getUserMedia_basicScreenshare.html]
+skip-if =
+ toolkit == 'android' # no screenshare on android
+ apple_silicon # bug 1707742
+[test_getUserMedia_basicTabshare.html]
+skip-if =
+ toolkit == 'android' # no windowshare on android
+[test_getUserMedia_basicVideo.html]
+[test_getUserMedia_basicVideoAudio.html]
+[test_getUserMedia_basicVideo_playAfterLoadedmetadata.html]
+[test_getUserMedia_basicWindowshare.html]
+skip-if = toolkit == 'android' # no windowshare on android
+[test_getUserMedia_bug1223696.html]
+[test_getUserMedia_callbacks.html]
+[test_getUserMedia_constraints.html]
+[test_getUserMedia_cubebDisabled.html]
+[test_getUserMedia_cubebDisabledFakeStreams.html]
+[test_getUserMedia_getTrackById.html]
+[test_getUserMedia_gumWithinGum.html]
+[test_getUserMedia_loadedmetadata.html]
+[test_getUserMedia_mediaElementCapture_audio.html]
+[test_getUserMedia_mediaElementCapture_tracks.html]
+[test_getUserMedia_mediaElementCapture_video.html]
+[test_getUserMedia_mediaStreamClone.html]
+[test_getUserMedia_mediaStreamConstructors.html]
+[test_getUserMedia_mediaStreamTrackClone.html]
+[test_getUserMedia_nonDefaultRate.html]
+[test_getUserMedia_peerIdentity.html]
+[test_getUserMedia_permission.html]
+[test_getUserMedia_playAudioTwice.html]
+[test_getUserMedia_playVideoAudioTwice.html]
+[test_getUserMedia_playVideoTwice.html]
+[test_getUserMedia_scarySources.html]
+skip-if = toolkit == 'android' # no screenshare or windowshare on android
+[test_getUserMedia_spinEventLoop.html]
+[test_getUserMedia_trackCloneCleanup.html]
+[test_getUserMedia_trackEnded.html]
+
diff --git a/dom/media/webrtc/tests/mochitests/mochitest_peerconnection.ini b/dom/media/webrtc/tests/mochitests/mochitest_peerconnection.ini
new file mode 100644
index 0000000000..37fb551435
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/mochitest_peerconnection.ini
@@ -0,0 +1,311 @@
+[DEFAULT]
+tags = mtg webrtc
+subsuite = media
+scheme = https
+support-files =
+ head.js
+ dataChannel.js
+ mediaStreamPlayback.js
+ network.js
+ nonTrickleIce.js
+ pc.js
+ stats.js
+ templates.js
+ test_enumerateDevices_iframe.html
+ test_getUserMedia_permission_iframe.html
+ NetworkPreparationChromeScript.js
+ blacksilence.js
+ turnConfig.js
+ sdpUtils.js
+ addTurnsSelfsignedCert.js
+ parser_rtp.js
+ peerconnection_audio_forced_sample_rate.js
+ iceTestUtils.js
+ simulcast.js
+ helpers_from_wpt/sdp.js
+ !/dom/canvas/test/captureStream_common.js
+ !/dom/canvas/test/webgl-mochitest/webgl-util.js
+ !/dom/media/test/manifest.js
+ !/dom/media/test/seek.webm
+ !/dom/media/test/gizmo.mp4
+ !/docshell/test/navigation/blank.html
+prefs =
+ focusmanager.testmode=true # emulate focus
+ privacy.partition.network_state=false
+ network.proxy.allow_hijacking_localhost=true
+ media.devices.enumerate.legacy.enabled=false
+
+[test_peerConnection_addAudioTrackToExistingVideoStream.html]
+[test_peerConnection_addDataChannel.html]
+[test_peerConnection_addDataChannelNoBundle.html]
+[test_peerConnection_addSecondAudioStream.html]
+[test_peerConnection_addSecondAudioStreamNoBundle.html]
+[test_peerConnection_addSecondVideoStream.html]
+[test_peerConnection_addSecondVideoStreamNoBundle.html]
+[test_peerConnection_addtrack_removetrack_events.html]
+[test_peerConnection_answererAddSecondAudioStream.html]
+[test_peerConnection_audioChannels.html]
+[test_peerConnection_audioCodecs.html]
+[test_peerConnection_audioContributingSources.html]
+[test_peerConnection_audioRenegotiationInactiveAnswer.html]
+[test_peerConnection_audioSynchronizationSources.html]
+[test_peerConnection_audioSynchronizationSourcesUnidirectional.html]
+[test_peerConnection_basicAudio.html]
+[test_peerConnection_basicAudioDynamicPtMissingRtpmap.html]
+[test_peerConnection_basicAudioNATRelay.html]
+skip-if =
+ toolkit == 'android' # websockets don't work on android (bug 1266217)
+scheme=http
+[test_peerConnection_basicAudioNATRelayTCP.html]
+skip-if =
+ toolkit == 'android' # websockets don't work on android (bug 1266217)
+ (os == 'win' && os_version == '6.1') # WinError 10048
+scheme=http
+[test_peerConnection_basicAudioNATRelayTCPWithStun300.html]
+skip-if =
+ toolkit == 'android' # websockets don't work on android (bug 1266217)
+ (os == 'win' && os_version == '6.1') # WinError 10048
+scheme=http
+[test_peerConnection_basicAudioNATRelayTLS.html]
+skip-if =
+ toolkit == 'android' # websockets don't work on android (bug 1266217)
+ (os == 'win' && os_version == '6.1') # WinError 10048
+scheme=http
+[test_peerConnection_basicAudioNATRelayWithStun300.html]
+skip-if =
+ toolkit == 'android' # websockets don't work on android (bug 1266217)
+ (os == 'win' && os_version == '6.1') # WinError 10048
+scheme=http
+[test_peerConnection_basicAudioNATSrflx.html]
+skip-if =
+ toolkit == 'android' # websockets don't work on android (bug 1266217)
+ (os == 'win' && os_version == '6.1') # WinError 10048
+scheme=http
+[test_peerConnection_basicAudioNoisyUDPBlock.html]
+skip-if =
+ toolkit == 'android' # websockets don't work on android (bug 1266217)
+ (os == 'win' && os_version == '6.1') # WinError 10048
+scheme=http
+[test_peerConnection_basicAudioPcmaPcmuOnly.html]
+[test_peerConnection_basicAudioRelayPolicy.html]
+skip-if =
+ toolkit == 'android' # websockets don't work on android (bug 1266217)
+ (os == 'win' && os_version == '6.1') # WinError 10048
+scheme=http
+[test_peerConnection_basicAudioRequireEOC.html]
+[test_peerConnection_basicAudioVerifyRtpHeaderExtensions.html]
+[test_peerConnection_basicAudioVideo.html]
+[test_peerConnection_basicAudioVideoCombined.html]
+[test_peerConnection_basicAudioVideoNoBundle.html]
+[test_peerConnection_basicAudioVideoNoBundleNoRtcpMux.html]
+[test_peerConnection_basicAudioVideoNoRtcpMux.html]
+[test_peerConnection_basicAudioVideoTransceivers.html]
+[test_peerConnection_basicAudioVideoVerifyExtmap.html]
+[test_peerConnection_basicAudioVideoVerifyExtmapSendonly.html]
+[test_peerConnection_basicAudioVideoVerifyTooLongMidFails.html]
+[test_peerConnection_basicAudio_forced_higher_rate.html]
+[test_peerConnection_basicAudio_forced_lower_rate.html]
+[test_peerConnection_basicH264Video.html]
+skip-if =
+ toolkit == 'android' && is_emulator # Bug 1355786, No h264 support on android emulator
+[test_peerConnection_basicScreenshare.html]
+skip-if = toolkit == 'android' # no screenshare on android
+[test_peerConnection_basicVideo.html]
+[test_peerConnection_basicVideoVerifyRtpHeaderExtensions.html]
+[test_peerConnection_basicWindowshare.html]
+skip-if = toolkit == 'android' # no screenshare on android
+[test_peerConnection_bug1013809.html]
+[test_peerConnection_bug1042791.html]
+skip-if = (toolkit == 'android' && is_emulator) # Bug 1355786, No h264 support on android emulator
+[test_peerConnection_bug1227781.html]
+scheme=http
+[test_peerConnection_bug1512281.html]
+fail-if = 1
+[test_peerConnection_bug1773067.html]
+[test_peerConnection_bug822674.html]
+scheme=http
+[test_peerConnection_bug825703.html]
+scheme=http
+[test_peerConnection_bug827843.html]
+[test_peerConnection_bug834153.html]
+scheme=http
+[test_peerConnection_callbacks.html]
+[test_peerConnection_captureStream_canvas_2d.html]
+scheme=http
+[test_peerConnection_captureStream_canvas_2d_noSSRC.html]
+scheme=http
+[test_peerConnection_captureStream_canvas_webgl.html]
+scheme=http
+[test_peerConnection_capturedVideo.html]
+tags=capturestream
+skip-if = toolkit == 'android' # android(Bug 1189784, timeouts on 4.3 emulator), Bug 1264340
+[test_peerConnection_certificates.html]
+[test_peerConnection_checkPacketDumpHook.html]
+[test_peerConnection_close.html]
+scheme=http
+[test_peerConnection_closeDuringIce.html]
+[test_peerConnection_codecNegotiationFailure.html]
+[test_peerConnection_constructedStream.html]
+[test_peerConnection_disabledVideoPreNegotiation.html]
+[test_peerConnection_encodingsNegotiation.html]
+[test_peerConnection_errorCallbacks.html]
+scheme=http
+[test_peerConnection_extmapRenegotiation.html]
+[test_peerConnection_forwarding_basicAudioVideoCombined.html]
+skip-if = toolkit == 'android' # Bug 1189784
+[test_peerConnection_gatherWithSetConfiguration.html]
+skip-if =
+ toolkit == 'android' # websockets don't work on android (bug 1266217)
+ (os == 'win' && os_version == '6.1') # WinError 10048
+scheme=http
+[test_peerConnection_gatherWithStun300.html]
+skip-if =
+ toolkit == 'android' # websockets don't work on android (bug 1266217)
+ (os == 'win' && os_version == '6.1') # WinError 10048
+scheme=http
+[test_peerConnection_gatherWithStun300IPv6.html]
+skip-if =
+ toolkit == 'android' # websockets don't work on android (bug 1266217)
+ os == 'mac' # no ipv6 support on OS X testers (bug 1710706)
+ os == 'win' # no ipv6 support on windows testers (bug 1710706)
+scheme=http
+[test_peerConnection_glean.html]
+[test_peerConnection_iceFailure.html]
+skip-if = true # (Bug 1180388 for win, mac and linux), android(Bug 1189784), Bug 1180388
+scheme=http
+[test_peerConnection_insertDTMF.html]
+[test_peerConnection_localReofferRollback.html]
+[test_peerConnection_localRollback.html]
+[test_peerConnection_maxFsConstraint.html]
+[test_peerConnection_multiple_captureStream_canvas_2d.html]
+scheme=http
+[test_peerConnection_noTrickleAnswer.html]
+[test_peerConnection_noTrickleOffer.html]
+[test_peerConnection_noTrickleOfferAnswer.html]
+[test_peerConnection_nonDefaultRate.html]
+[test_peerConnection_offerRequiresReceiveAudio.html]
+[test_peerConnection_offerRequiresReceiveVideo.html]
+[test_peerConnection_offerRequiresReceiveVideoAudio.html]
+[test_peerConnection_portRestrictions.html]
+[test_peerConnection_promiseSendOnly.html]
+[test_peerConnection_recordReceiveTrack.html]
+[test_peerConnection_relayOnly.html]
+disabled=bug 1612063 # test is racy
+[test_peerConnection_remoteReofferRollback.html]
+[test_peerConnection_remoteRollback.html]
+[test_peerConnection_removeAudioTrack.html]
+[test_peerConnection_removeThenAddAudioTrack.html]
+[test_peerConnection_removeThenAddAudioTrackNoBundle.html]
+[test_peerConnection_removeThenAddVideoTrack.html]
+[test_peerConnection_removeThenAddVideoTrackNoBundle.html]
+[test_peerConnection_removeVideoTrack.html]
+[test_peerConnection_renderAfterRenegotiation.html]
+scheme=http
+[test_peerConnection_replaceNullTrackThenRenegotiateAudio.html]
+[test_peerConnection_replaceNullTrackThenRenegotiateVideo.html]
+[test_peerConnection_replaceTrack.html]
+[test_peerConnection_replaceTrack_camera.html]
+skip-if = toolkit == 'android' # Bug 1614460
+[test_peerConnection_replaceTrack_disabled.html]
+skip-if =
+ toolkit == 'android' # Bug 1614460
+[test_peerConnection_replaceTrack_microphone.html]
+[test_peerConnection_replaceVideoThenRenegotiate.html]
+[test_peerConnection_restartIce.html]
+[test_peerConnection_restartIceBadAnswer.html]
+[test_peerConnection_restartIceLocalAndRemoteRollback.html]
+[test_peerConnection_restartIceLocalAndRemoteRollbackNoSubsequentRestart.html]
+[test_peerConnection_restartIceLocalRollback.html]
+[test_peerConnection_restartIceLocalRollbackNoSubsequentRestart.html]
+[test_peerConnection_restartIceNoBundle.html]
+[test_peerConnection_restartIceNoBundleNoRtcpMux.html]
+[test_peerConnection_restartIceNoRtcpMux.html]
+[test_peerConnection_restrictBandwidthTargetBitrate.html]
+[test_peerConnection_restrictBandwidthWithTias.html]
+[test_peerConnection_rtcp_rsize.html]
+[test_peerConnection_scaleResolution.html]
+[test_peerConnection_scaleResolution_oldSetParameters.html]
+[test_peerConnection_sender_and_receiver_stats.html]
+[test_peerConnection_setLocalAnswerInHaveLocalOffer.html]
+[test_peerConnection_setLocalAnswerInStable.html]
+[test_peerConnection_setLocalOfferInHaveRemoteOffer.html]
+[test_peerConnection_setParameters.html]
+[test_peerConnection_setParameters_maxFramerate.html]
+[test_peerConnection_setParameters_maxFramerate_oldSetParameters.html]
+[test_peerConnection_setParameters_oldSetParameters.html]
+[test_peerConnection_setParameters_scaleResolutionDownBy.html]
+skip-if = (os == 'win' && processor == 'aarch64') # aarch64 due to bug 1537567
+[test_peerConnection_setParameters_scaleResolutionDownBy_oldSetParameters.html]
+skip-if = (os == 'win' && processor == 'aarch64') # aarch64 due to bug 1537567
+[test_peerConnection_setRemoteAnswerInHaveRemoteOffer.html]
+[test_peerConnection_setRemoteAnswerInStable.html]
+[test_peerConnection_setRemoteOfferInHaveLocalOffer.html]
+[test_peerConnection_simulcastAnswer.html]
+skip-if = toolkit == 'android' # no simulcast support on android
+[test_peerConnection_simulcastAnswer_lowResFirst.html]
+skip-if = toolkit == 'android' # no simulcast support on android
+[test_peerConnection_simulcastAnswer_lowResFirst_oldSetParameters.html]
+skip-if = toolkit == 'android' # no simulcast support on android
+[test_peerConnection_simulcastAnswer_oldSetParameters.html]
+skip-if = toolkit == 'android' # no simulcast support on android
+[test_peerConnection_simulcastOddResolution.html]
+skip-if = toolkit == 'android' # no simulcast support on android
+[test_peerConnection_simulcastOddResolution_oldSetParameters.html]
+skip-if = toolkit == 'android' # no simulcast support on android
+[test_peerConnection_simulcastOffer.html]
+skip-if = toolkit == 'android' # no simulcast support on android
+[test_peerConnection_simulcastOffer_lowResFirst.html]
+skip-if = toolkit == 'android' # no simulcast support on android
+[test_peerConnection_simulcastOffer_lowResFirst_oldSetParameters.html]
+skip-if = toolkit == 'android' # no simulcast support on android
+[test_peerConnection_simulcastOffer_oldSetParameters.html]
+skip-if = toolkit == 'android' # no simulcast support on android
+[test_peerConnection_stats.html]
+[test_peerConnection_stats_jitter.html]
+skip-if = tsan # Bug 1672590, TSan is just too slow to pass this test
+[test_peerConnection_stats_oneway.html]
+[test_peerConnection_stats_relayProtocol.html]
+skip-if =
+ toolkit == 'android' # android(Bug 1189784, timeouts on 4.3 emulator, Bug 1373858, Bug 1521117)
+ socketprocess_e10s
+ (os == 'win' && os_version == '6.1') # WinError 10048
+scheme=http
+[test_peerConnection_stereoFmtpPref.html]
+[test_peerConnection_syncSetDescription.html]
+[test_peerConnection_telephoneEventFirst.html]
+[test_peerConnection_threeUnbundledConnections.html]
+[test_peerConnection_throwInCallbacks.html]
+[test_peerConnection_toJSON.html]
+scheme=http
+[test_peerConnection_trackDisabling.html]
+skip-if = toolkit == 'android' # Bug 1614460
+[test_peerConnection_trackDisabling_clones.html]
+[test_peerConnection_trackless_sender_stats.html]
+[test_peerConnection_twoAudioStreams.html]
+[test_peerConnection_twoAudioTracksInOneStream.html]
+[test_peerConnection_twoAudioVideoStreams.html]
+[test_peerConnection_twoAudioVideoStreamsCombined.html]
+skip-if = (toolkit == 'android') || (os == 'linux' && asan) # android(Bug 1189784), Bug 1480942 for Linux asan
+[test_peerConnection_twoAudioVideoStreamsCombinedNoBundle.html]
+skip-if =
+ (toolkit == 'android') # Bug 1189784
+ (os == 'linux' && asan) # Bug 1480942
+ (os == 'win' && processor == 'aarch64') # Bug 1777081
+[test_peerConnection_twoVideoStreams.html]
+[test_peerConnection_twoVideoTracksInOneStream.html]
+[test_peerConnection_verifyAudioAfterRenegotiation.html]
+skip-if =
+ os == "android" && processor == "x86_64" && !debug # Bug 1783287
+[test_peerConnection_verifyDescriptions.html]
+[test_peerConnection_verifyVideoAfterRenegotiation.html]
+[test_peerConnection_videoCodecs.html]
+skip-if =
+ toolkit == 'android' # android(Bug 1614460)
+ win10_2004 && !debug # Bug 1777082
+[test_peerConnection_videoRenegotiationInactiveAnswer.html]
+[test_peerConnection_webAudio.html]
+tags = webaudio webrtc
+scheme=http
+[test_selftest.html]
+# Bug 1227781: Crash with bogus TURN server.
+scheme=http
diff --git a/dom/media/webrtc/tests/mochitests/network.js b/dom/media/webrtc/tests/mochitests/network.js
new file mode 100644
index 0000000000..223721b111
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/network.js
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * A stub function for preparing the network if needed
+ *
+ */
+async function startNetworkAndTest() {}
+
+/**
+ * A stub function to shutdown the network if needed
+ */
+async function networkTestFinished() {}
diff --git a/dom/media/webrtc/tests/mochitests/nonTrickleIce.js b/dom/media/webrtc/tests/mochitests/nonTrickleIce.js
new file mode 100644
index 0000000000..9361944791
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/nonTrickleIce.js
@@ -0,0 +1,97 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function removeTrickleOption(desc) {
+ var sdp = desc.sdp.replace(/\r\na=ice-options:trickle\r\n/, "\r\n");
+ return new RTCSessionDescription({ type: desc.type, sdp });
+}
+
+function makeOffererNonTrickle(chain) {
+ chain.replace("PC_LOCAL_SETUP_ICE_HANDLER", [
+ function PC_LOCAL_SETUP_NOTRICKLE_ICE_HANDLER(test) {
+ // We need to install this callback before calling setLocalDescription
+ // otherwise we might miss callbacks
+ test.pcLocal.setupIceCandidateHandler(test, () => {});
+ // We ignore ICE candidates because we want the full offer
+ },
+ ]);
+ chain.replace("PC_REMOTE_GET_OFFER", [
+ function PC_REMOTE_GET_FULL_OFFER(test) {
+ return test.pcLocal.endOfTrickleIce.then(() => {
+ test._local_offer = removeTrickleOption(test.pcLocal.localDescription);
+ test._offer_constraints = test.pcLocal.constraints;
+ test._offer_options = test.pcLocal.offerOptions;
+ });
+ },
+ ]);
+ chain.insertAfter("PC_REMOTE_SANE_REMOTE_SDP", [
+ function PC_REMOTE_REQUIRE_REMOTE_SDP_CANDIDATES(test) {
+ info(
+ "test.pcLocal.localDescription.sdp: " +
+ JSON.stringify(test.pcLocal.localDescription.sdp)
+ );
+ info("test._local_offer.sdp" + JSON.stringify(test._local_offer.sdp));
+ is(
+ test.pcRemote._pc.canTrickleIceCandidates,
+ false,
+ "Remote thinks that trickle isn't supported"
+ );
+ ok(!test.localRequiresTrickleIce, "Local does NOT require trickle");
+ ok(
+ test._local_offer.sdp.includes("a=candidate"),
+ "offer has ICE candidates"
+ );
+ ok(
+ test._local_offer.sdp.includes("a=end-of-candidates"),
+ "offer has end-of-candidates"
+ );
+ },
+ ]);
+ chain.remove("PC_REMOTE_CHECK_CAN_TRICKLE_SYNC");
+}
+
+function makeAnswererNonTrickle(chain) {
+ chain.replace("PC_REMOTE_SETUP_ICE_HANDLER", [
+ function PC_REMOTE_SETUP_NOTRICKLE_ICE_HANDLER(test) {
+ // We need to install this callback before calling setLocalDescription
+ // otherwise we might miss callbacks
+ test.pcRemote.setupIceCandidateHandler(test, () => {});
+ // We ignore ICE candidates because we want the full offer
+ },
+ ]);
+ chain.replace("PC_LOCAL_GET_ANSWER", [
+ function PC_LOCAL_GET_FULL_ANSWER(test) {
+ return test.pcRemote.endOfTrickleIce.then(() => {
+ test._remote_answer = removeTrickleOption(
+ test.pcRemote.localDescription
+ );
+ test._answer_constraints = test.pcRemote.constraints;
+ });
+ },
+ ]);
+ chain.insertAfter("PC_LOCAL_SANE_REMOTE_SDP", [
+ function PC_LOCAL_REQUIRE_REMOTE_SDP_CANDIDATES(test) {
+ info(
+ "test.pcRemote.localDescription.sdp: " +
+ JSON.stringify(test.pcRemote.localDescription.sdp)
+ );
+ info("test._remote_answer.sdp" + JSON.stringify(test._remote_answer.sdp));
+ is(
+ test.pcLocal._pc.canTrickleIceCandidates,
+ false,
+ "Local thinks that trickle isn't supported"
+ );
+ ok(!test.remoteRequiresTrickleIce, "Remote does NOT require trickle");
+ ok(
+ test._remote_answer.sdp.includes("a=candidate"),
+ "answer has ICE candidates"
+ );
+ ok(
+ test._remote_answer.sdp.includes("a=end-of-candidates"),
+ "answer has end-of-candidates"
+ );
+ },
+ ]);
+ chain.remove("PC_LOCAL_CHECK_CAN_TRICKLE_SYNC");
+}
diff --git a/dom/media/webrtc/tests/mochitests/parser_rtp.js b/dom/media/webrtc/tests/mochitests/parser_rtp.js
new file mode 100644
index 0000000000..2275c1f787
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/parser_rtp.js
@@ -0,0 +1,131 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/*
+ * Parses an RTP packet
+ * @param buffer an ArrayBuffer that contains the packet
+ * @return { type: "rtp", header: {...}, payload: a DataView }
+ */
+var ParseRtpPacket = buffer => {
+ // DataView.getFooInt returns big endian numbers by default
+ let view = new DataView(buffer);
+
+ // Standard Header Fields
+ // https://tools.ietf.org/html/rfc3550#section-5.1
+ // 0 1 2 3
+ // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ // |V=2|P|X| CC |M| PT | sequence number |
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ // | timestamp |
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ // | synchronization source (SSRC) identifier |
+ // +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+
+ // | contributing source (CSRC) identifiers |
+ // | .... |
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+
+ let header = {};
+ let offset = 0;
+ // Note that incrementing the offset happens as close to reading the data as
+ // possible. This simplifies ensuring that the number of read bytes and the
+ // offset increment match. Data may be manipulated between when the offset is
+ // incremented and before the next read.
+ let byte = view.getUint8(offset);
+ offset++;
+ // Version 2 Bit
+ header.version = (0xc0 & byte) >> 6;
+ // Padding 1 Bit
+ header.padding = (0x30 & byte) >> 5;
+ // Extension 1 Bit
+ header.extensionsPresent = (0x10 & byte) >> 4 == 1;
+ // CSRC count 4 Bit
+ header.csrcCount = 0xf & byte;
+
+ byte = view.getUint8(offset);
+ offset++;
+ // Marker 1 Bit
+ header.marker = (0x80 & byte) >> 7;
+ // Payload Type 7 Bit
+ header.payloadType = 0x7f & byte;
+ // Sequence Number 16 Bit
+ header.sequenceNumber = view.getUint16(offset);
+ offset += 2;
+ // Timestamp 32 Bit
+ header.timestamp = view.getUint32(offset);
+ offset += 4;
+ // SSRC 32 Bit
+ header.ssrc = view.getUint32(offset);
+ offset += 4;
+
+ // CSRC 32 Bit
+ header.csrcs = [];
+ for (let c = 0; c < header.csrcCount; c++) {
+ header.csrcs.push(view.getUint32(offset));
+ offset += 4;
+ }
+
+ // Extensions
+ header.extensions = [];
+ header.extensionPaddingBytes = 0;
+ header.extensionsTotalLength = 0;
+ if (header.extensionsPresent) {
+ // https://tools.ietf.org/html/rfc3550#section-5.3.1
+ // 0 1 2 3
+ // 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ // | defined by profile | length |
+ // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ // | header extension |
+ // | .... |
+ let addExtension = (id, len) =>
+ header.extensions.push({
+ id,
+ data: new DataView(buffer, offset, len),
+ });
+ let extensionId = view.getUint16(offset);
+ offset += 2;
+ // len is in 32 bit units, not bytes
+ header.extensionsTotalLength = view.getUint16(offset) * 4;
+ offset += 2;
+ // Check for https://tools.ietf.org/html/rfc5285
+ if (extensionId != 0xbede) {
+ // No rfc5285
+ addExtension(extensionId, header.extensionsTotalLength);
+ offset += header.extensionsTotalLength;
+ } else {
+ let expectedEnd = offset + header.extensionsTotalLength;
+ while (offset < expectedEnd) {
+ // We only support "one-byte" extension headers ATM
+ // https://tools.ietf.org/html/rfc5285#section-4.2
+ // 0
+ // 0 1 2 3 4 5 6 7
+ // +-+-+-+-+-+-+-+-+
+ // | ID | len |
+ // +-+-+-+-+-+-+-+-+
+ byte = view.getUint8(offset);
+ offset++;
+ // Check for padding which can occur between extensions or at the end
+ if (byte == 0) {
+ header.extensionPaddingBytes++;
+ continue;
+ }
+ let id = (byte & 0xf0) >> 4;
+ // Check for the FORBIDDEN id (15), dun dun dun
+ if (id == 15) {
+ // Ignore bytes until until the end of extensions
+ offset = expectedEnd;
+ break;
+ }
+ // the length of the extention is len + 1
+ let len = (byte & 0x0f) + 1;
+ addExtension(id, len);
+ offset += len;
+ }
+ }
+ }
+ return { type: "rtp", header, payload: new DataView(buffer, offset) };
+};
diff --git a/dom/media/webrtc/tests/mochitests/pc.js b/dom/media/webrtc/tests/mochitests/pc.js
new file mode 100644
index 0000000000..36a923fbed
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/pc.js
@@ -0,0 +1,2495 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const LOOPBACK_ADDR = "127.0.0.";
+
+const iceStateTransitions = {
+ new: ["checking", "closed"], //Note: 'failed' might need to added here
+ // even though it is not in the standard
+ checking: ["new", "connected", "failed", "closed"], //Note: do we need to
+ // allow 'completed' in
+ // here as well?
+ connected: ["new", "completed", "disconnected", "closed"],
+ completed: ["new", "disconnected", "closed"],
+ disconnected: ["new", "connected", "completed", "failed", "closed"],
+ failed: ["new", "disconnected", "closed"],
+ closed: [],
+};
+
+const signalingStateTransitions = {
+ stable: ["have-local-offer", "have-remote-offer", "closed"],
+ "have-local-offer": [
+ "have-remote-pranswer",
+ "stable",
+ "closed",
+ "have-local-offer",
+ ],
+ "have-remote-pranswer": ["stable", "closed", "have-remote-pranswer"],
+ "have-remote-offer": [
+ "have-local-pranswer",
+ "stable",
+ "closed",
+ "have-remote-offer",
+ ],
+ "have-local-pranswer": ["stable", "closed", "have-local-pranswer"],
+ closed: [],
+};
+
+var makeDefaultCommands = () => {
+ return [].concat(
+ commandsPeerConnectionInitial,
+ commandsGetUserMedia,
+ commandsPeerConnectionOfferAnswer
+ );
+};
+
+/**
+ * This class handles tests for peer connections.
+ *
+ * @constructor
+ * @param {object} [options={}]
+ * Optional options for the peer connection test
+ * @param {object} [options.commands=commandsPeerConnection]
+ * Commands to run for the test
+ * @param {bool} [options.is_local=true]
+ * true if this test should run the tests for the "local" side.
+ * @param {bool} [options.is_remote=true]
+ * true if this test should run the tests for the "remote" side.
+ * @param {object} [options.config_local=undefined]
+ * Configuration for the local peer connection instance
+ * @param {object} [options.config_remote=undefined]
+ * Configuration for the remote peer connection instance. If not defined
+ * the configuration from the local instance will be used
+ */
+function PeerConnectionTest(options) {
+ // If no options are specified make it an empty object
+ options = options || {};
+ options.commands = options.commands || makeDefaultCommands();
+ options.is_local = "is_local" in options ? options.is_local : true;
+ options.is_remote = "is_remote" in options ? options.is_remote : true;
+
+ options.h264 = "h264" in options ? options.h264 : false;
+ options.bundle = "bundle" in options ? options.bundle : true;
+ options.rtcpmux = "rtcpmux" in options ? options.rtcpmux : true;
+ options.opus = "opus" in options ? options.opus : true;
+ options.ssrc = "ssrc" in options ? options.ssrc : true;
+
+ options.config_local = options.config_local || {};
+ options.config_remote = options.config_remote || {};
+
+ if (!options.bundle) {
+ // Make sure neither end tries to use bundle-only!
+ options.config_local.bundlePolicy = "max-compat";
+ options.config_remote.bundlePolicy = "max-compat";
+ }
+
+ if (iceServersArray.length) {
+ if (!options.turn_disabled_local && !options.config_local.iceServers) {
+ options.config_local.iceServers = iceServersArray;
+ }
+ if (!options.turn_disabled_remote && !options.config_remote.iceServers) {
+ options.config_remote.iceServers = iceServersArray;
+ }
+ } else if (typeof turnServers !== "undefined") {
+ if (!options.turn_disabled_local && turnServers.local) {
+ if (!options.config_local.hasOwnProperty("iceServers")) {
+ options.config_local.iceServers = turnServers.local.iceServers;
+ }
+ }
+ if (!options.turn_disabled_remote && turnServers.remote) {
+ if (!options.config_remote.hasOwnProperty("iceServers")) {
+ options.config_remote.iceServers = turnServers.remote.iceServers;
+ }
+ }
+ }
+
+ if (options.is_local) {
+ this.pcLocal = new PeerConnectionWrapper("pcLocal", options.config_local);
+ } else {
+ this.pcLocal = null;
+ }
+
+ if (options.is_remote) {
+ this.pcRemote = new PeerConnectionWrapper(
+ "pcRemote",
+ options.config_remote || options.config_local
+ );
+ } else {
+ this.pcRemote = null;
+ }
+
+ // Create command chain instance and assign default commands
+ this.chain = new CommandChain(this, options.commands);
+
+ this.testOptions = options;
+}
+
+/** TODO: consider removing this dependency on timeouts */
+function timerGuard(p, time, message) {
+ return Promise.race([
+ p,
+ wait(time).then(() => {
+ throw new Error("timeout after " + time / 1000 + "s: " + message);
+ }),
+ ]);
+}
+
+/**
+ * Closes the peer connection if it is active
+ */
+PeerConnectionTest.prototype.closePC = function () {
+ info("Closing peer connections");
+
+ var closeIt = pc => {
+ if (!pc || pc.signalingState === "closed") {
+ return Promise.resolve();
+ }
+
+ var promise = Promise.all([
+ Promise.all(
+ pc._pc
+ .getReceivers()
+ .filter(receiver => receiver.track.readyState == "live")
+ .map(receiver => {
+ info(
+ "Waiting for track " +
+ receiver.track.id +
+ " (" +
+ receiver.track.kind +
+ ") to end."
+ );
+ return haveEvent(receiver.track, "ended", wait(50000)).then(
+ event => {
+ is(
+ event.target,
+ receiver.track,
+ "Event target should be the correct track"
+ );
+ info(pc + " ended fired for track " + receiver.track.id);
+ },
+ e =>
+ e
+ ? Promise.reject(e)
+ : ok(
+ false,
+ "ended never fired for track " + receiver.track.id
+ )
+ );
+ })
+ ),
+ ]);
+ pc.close();
+ return promise;
+ };
+
+ return timerGuard(
+ Promise.all([closeIt(this.pcLocal), closeIt(this.pcRemote)]),
+ 60000,
+ "failed to close peer connection"
+ );
+};
+
+/**
+ * Close the open data channels, followed by the underlying peer connection
+ */
+PeerConnectionTest.prototype.close = function () {
+ var allChannels = (this.pcLocal || this.pcRemote).dataChannels;
+ return timerGuard(
+ Promise.all(allChannels.map((channel, i) => this.closeDataChannels(i))),
+ 120000,
+ "failed to close data channels"
+ ).then(() => this.closePC());
+};
+
+/**
+ * Close the specified data channels
+ *
+ * @param {Number} index
+ * Index of the data channels to close on both sides
+ */
+PeerConnectionTest.prototype.closeDataChannels = function (index) {
+ info("closeDataChannels called with index: " + index);
+ var localChannel = null;
+ if (this.pcLocal) {
+ localChannel = this.pcLocal.dataChannels[index];
+ }
+ var remoteChannel = null;
+ if (this.pcRemote) {
+ remoteChannel = this.pcRemote.dataChannels[index];
+ }
+
+ // We need to setup all the close listeners before calling close
+ var setupClosePromise = channel => {
+ if (!channel) {
+ return Promise.resolve();
+ }
+ return new Promise(resolve => {
+ channel.onclose = () => {
+ is(
+ channel.readyState,
+ "closed",
+ name + " channel " + index + " closed"
+ );
+ resolve();
+ };
+ });
+ };
+
+ // make sure to setup close listeners before triggering any actions
+ var allClosed = Promise.all([
+ setupClosePromise(localChannel),
+ setupClosePromise(remoteChannel),
+ ]);
+ var complete = timerGuard(
+ allClosed,
+ 120000,
+ "failed to close data channel pair"
+ );
+
+ // triggering close on one side should suffice
+ if (remoteChannel) {
+ remoteChannel.close();
+ } else if (localChannel) {
+ localChannel.close();
+ }
+
+ return complete;
+};
+
+/**
+ * Send data (message or blob) to the other peer
+ *
+ * @param {String|Blob} data
+ * Data to send to the other peer. For Blobs the MIME type will be lost.
+ * @param {Object} [options={ }]
+ * Options to specify the data channels to be used
+ * @param {DataChannelWrapper} [options.sourceChannel=pcLocal.dataChannels[length - 1]]
+ * Data channel to use for sending the message
+ * @param {DataChannelWrapper} [options.targetChannel=pcRemote.dataChannels[length - 1]]
+ * Data channel to use for receiving the message
+ */
+PeerConnectionTest.prototype.send = async function (data, options) {
+ options = options || {};
+ const source =
+ options.sourceChannel ||
+ this.pcLocal.dataChannels[this.pcLocal.dataChannels.length - 1];
+ const target =
+ options.targetChannel ||
+ this.pcRemote.dataChannels[this.pcRemote.dataChannels.length - 1];
+ source.bufferedAmountLowThreshold = options.bufferedAmountLowThreshold || 0;
+
+ const getSizeInBytes = d => {
+ if (d instanceof Blob) {
+ return d.size;
+ } else if (d instanceof ArrayBuffer) {
+ return d.byteLength;
+ } else if (d instanceof String || typeof d === "string") {
+ return new TextEncoder().encode(d).length;
+ } else {
+ ok(false);
+ }
+ };
+
+ const expectedSizeInBytes = getSizeInBytes(data);
+ const bufferedAmount = source.bufferedAmount;
+
+ source.send(data);
+ is(
+ source.bufferedAmount,
+ expectedSizeInBytes + bufferedAmount,
+ `Buffered amount should be ${expectedSizeInBytes}`
+ );
+
+ await new Promise(resolve => (source.onbufferedamountlow = resolve));
+
+ return new Promise(resolve => {
+ // Register event handler for the target channel
+ target.onmessage = e => {
+ is(
+ getSizeInBytes(e.data),
+ expectedSizeInBytes,
+ `Expected to receive the same number of bytes as we sent (${expectedSizeInBytes})`
+ );
+ resolve({ channel: target, data: e.data });
+ };
+ });
+};
+
+/**
+ * Create a data channel
+ *
+ * @param {Dict} options
+ * Options for the data channel (see nsIPeerConnection)
+ */
+PeerConnectionTest.prototype.createDataChannel = function (options) {
+ var remotePromise;
+ if (!options.negotiated) {
+ this.pcRemote.expectDataChannel("pcRemote expected data channel");
+ remotePromise = this.pcRemote.nextDataChannel;
+ }
+
+ // Create the datachannel
+ var localChannel = this.pcLocal.createDataChannel(options);
+ var localPromise = localChannel.opened;
+
+ if (options.negotiated) {
+ remotePromise = localPromise.then(localChannel => {
+ // externally negotiated - we need to open from both ends
+ options.id = options.id || channel.id; // allow for no id on options
+ var remoteChannel = this.pcRemote.createDataChannel(options);
+ return remoteChannel.opened;
+ });
+ }
+
+ // pcRemote.observedNegotiationNeeded might be undefined if
+ // !options.negotiated, which means we just wait on pcLocal
+ return Promise.all([
+ this.pcLocal.observedNegotiationNeeded,
+ this.pcRemote.observedNegotiationNeeded,
+ ]).then(() => {
+ return Promise.all([localPromise, remotePromise]).then(result => {
+ return { local: result[0], remote: result[1] };
+ });
+ });
+};
+
+/**
+ * Creates an answer for the specified peer connection instance
+ * and automatically handles the failure case.
+ *
+ * @param {PeerConnectionWrapper} peer
+ * The peer connection wrapper to run the command on
+ */
+PeerConnectionTest.prototype.createAnswer = function (peer) {
+ return peer.createAnswer().then(answer => {
+ // make a copy so this does not get updated with ICE candidates
+ this.originalAnswer = new RTCSessionDescription(
+ JSON.parse(JSON.stringify(answer))
+ );
+ return answer;
+ });
+};
+
+/**
+ * Creates an offer for the specified peer connection instance
+ * and automatically handles the failure case.
+ *
+ * @param {PeerConnectionWrapper} peer
+ * The peer connection wrapper to run the command on
+ */
+PeerConnectionTest.prototype.createOffer = function (peer) {
+ return peer.createOffer().then(offer => {
+ // make a copy so this does not get updated with ICE candidates
+ this.originalOffer = new RTCSessionDescription(
+ JSON.parse(JSON.stringify(offer))
+ );
+ return offer;
+ });
+};
+
+/**
+ * Sets the local description for the specified peer connection instance
+ * and automatically handles the failure case.
+ *
+ * @param {PeerConnectionWrapper} peer
+ The peer connection wrapper to run the command on
+ * @param {RTCSessionDescriptionInit} desc
+ * Session description for the local description request
+ */
+PeerConnectionTest.prototype.setLocalDescription = function (
+ peer,
+ desc,
+ stateExpected
+) {
+ var eventFired = new Promise(resolve => {
+ peer.onsignalingstatechange = e => {
+ info(peer + ": 'signalingstatechange' event received");
+ var state = e.target.signalingState;
+ if (stateExpected === state) {
+ peer.setLocalDescStableEventDate = new Date();
+ resolve();
+ } else {
+ ok(
+ false,
+ "This event has either already fired or there has been a " +
+ "mismatch between event received " +
+ state +
+ " and event expected " +
+ stateExpected
+ );
+ }
+ };
+ });
+
+ var stateChanged = peer.setLocalDescription(desc).then(() => {
+ peer.setLocalDescDate = new Date();
+ });
+
+ peer.endOfTrickleSdp = peer.endOfTrickleIce
+ .then(() => {
+ return peer._pc.localDescription;
+ })
+ .catch(e => ok(false, "Sending EOC message failed: " + e));
+
+ return Promise.all([eventFired, stateChanged]);
+};
+
+/**
+ * Sets the media constraints for both peer connection instances.
+ *
+ * @param {object} constraintsLocal
+ * Media constrains for the local peer connection instance
+ * @param constraintsRemote
+ */
+PeerConnectionTest.prototype.setMediaConstraints = function (
+ constraintsLocal,
+ constraintsRemote
+) {
+ if (this.pcLocal) {
+ this.pcLocal.constraints = constraintsLocal;
+ }
+ if (this.pcRemote) {
+ this.pcRemote.constraints = constraintsRemote;
+ }
+};
+
+/**
+ * Sets the media options used on a createOffer call in the test.
+ *
+ * @param {object} options the media constraints to use on createOffer
+ */
+PeerConnectionTest.prototype.setOfferOptions = function (options) {
+ if (this.pcLocal) {
+ this.pcLocal.offerOptions = options;
+ }
+};
+
+/**
+ * Sets the remote description for the specified peer connection instance
+ * and automatically handles the failure case.
+ *
+ * @param {PeerConnectionWrapper} peer
+ The peer connection wrapper to run the command on
+ * @param {RTCSessionDescriptionInit} desc
+ * Session description for the remote description request
+ */
+PeerConnectionTest.prototype.setRemoteDescription = function (
+ peer,
+ desc,
+ stateExpected
+) {
+ var eventFired = new Promise(resolve => {
+ peer.onsignalingstatechange = e => {
+ info(peer + ": 'signalingstatechange' event received");
+ var state = e.target.signalingState;
+ if (stateExpected === state) {
+ peer.setRemoteDescStableEventDate = new Date();
+ resolve();
+ } else {
+ ok(
+ false,
+ "This event has either already fired or there has been a " +
+ "mismatch between event received " +
+ state +
+ " and event expected " +
+ stateExpected
+ );
+ }
+ };
+ });
+
+ var stateChanged = peer.setRemoteDescription(desc).then(() => {
+ peer.setRemoteDescDate = new Date();
+ peer.checkMediaTracks();
+ });
+
+ return Promise.all([eventFired, stateChanged]);
+};
+
+/**
+ * Adds and removes steps to/from the execution chain based on the configured
+ * testOptions.
+ */
+PeerConnectionTest.prototype.updateChainSteps = function () {
+ if (this.testOptions.h264) {
+ this.chain.insertAfterEach("PC_LOCAL_CREATE_OFFER", [
+ PC_LOCAL_REMOVE_ALL_BUT_H264_FROM_OFFER,
+ ]);
+ }
+ if (!this.testOptions.bundle) {
+ this.chain.insertAfterEach("PC_LOCAL_CREATE_OFFER", [
+ PC_LOCAL_REMOVE_BUNDLE_FROM_OFFER,
+ ]);
+ }
+ if (!this.testOptions.rtcpmux) {
+ this.chain.insertAfterEach("PC_LOCAL_CREATE_OFFER", [
+ PC_LOCAL_REMOVE_RTCPMUX_FROM_OFFER,
+ ]);
+ }
+ if (!this.testOptions.ssrc) {
+ this.chain.insertAfterEach("PC_LOCAL_CREATE_OFFER", [
+ PC_LOCAL_REMOVE_SSRC_FROM_OFFER,
+ ]);
+ this.chain.insertAfterEach("PC_REMOTE_CREATE_ANSWER", [
+ PC_REMOTE_REMOVE_SSRC_FROM_ANSWER,
+ ]);
+ }
+ if (!this.testOptions.is_local) {
+ this.chain.filterOut(/^PC_LOCAL/);
+ }
+ if (!this.testOptions.is_remote) {
+ this.chain.filterOut(/^PC_REMOTE/);
+ }
+};
+
+/**
+ * Start running the tests as assigned to the command chain.
+ */
+PeerConnectionTest.prototype.run = async function () {
+ /* We have to modify the chain here to allow tests which modify the default
+ * test chain instantiating a PeerConnectionTest() */
+ this.updateChainSteps();
+ try {
+ await this.chain.execute();
+ await this.close();
+ } catch (e) {
+ const stack =
+ typeof e.stack === "string"
+ ? ` ${e.stack.split("\n").join(" ... ")}`
+ : "";
+ ok(false, `Error in test execution: ${e} (${stack})`);
+ }
+};
+
+/**
+ * Routes ice candidates from one PCW to the other PCW
+ */
+PeerConnectionTest.prototype.iceCandidateHandler = function (
+ caller,
+ candidate
+) {
+ info("Received: " + JSON.stringify(candidate) + " from " + caller);
+
+ var target = null;
+ if (caller.includes("pcLocal")) {
+ if (this.pcRemote) {
+ target = this.pcRemote;
+ }
+ } else if (caller.includes("pcRemote")) {
+ if (this.pcLocal) {
+ target = this.pcLocal;
+ }
+ } else {
+ ok(false, "received event from unknown caller: " + caller);
+ return;
+ }
+
+ if (target) {
+ target.storeOrAddIceCandidate(candidate);
+ } else {
+ info("sending ice candidate to signaling server");
+ send_message({ type: "ice_candidate", ice_candidate: candidate });
+ }
+};
+
+/**
+ * Installs a polling function for the socket.io client to read
+ * all messages from the chat room into a message queue.
+ */
+PeerConnectionTest.prototype.setupSignalingClient = function () {
+ this.signalingMessageQueue = [];
+ this.signalingCallbacks = {};
+ this.signalingLoopRun = true;
+
+ var queueMessage = message => {
+ info("Received signaling message: " + JSON.stringify(message));
+ var fired = false;
+ Object.keys(this.signalingCallbacks).forEach(name => {
+ if (name === message.type) {
+ info("Invoking callback for message type: " + name);
+ this.signalingCallbacks[name](message);
+ fired = true;
+ }
+ });
+ if (!fired) {
+ this.signalingMessageQueue.push(message);
+ info(
+ "signalingMessageQueue.length: " + this.signalingMessageQueue.length
+ );
+ }
+ if (this.signalingLoopRun) {
+ wait_for_message().then(queueMessage);
+ } else {
+ info("Exiting signaling message event loop");
+ }
+ };
+ wait_for_message().then(queueMessage);
+};
+
+/**
+ * Sets a flag to stop reading further messages from the chat room.
+ */
+PeerConnectionTest.prototype.signalingMessagesFinished = function () {
+ this.signalingLoopRun = false;
+};
+
+/**
+ * Register a callback function to deliver messages from the chat room
+ * directly instead of storing them in the message queue.
+ *
+ * @param {string} messageType
+ * For which message types should the callback get invoked.
+ *
+ * @param {function} onMessage
+ * The function which gets invoked if a message of the messageType
+ * has been received from the chat room.
+ */
+PeerConnectionTest.prototype.registerSignalingCallback = function (
+ messageType,
+ onMessage
+) {
+ this.signalingCallbacks[messageType] = onMessage;
+};
+
+/**
+ * Searches the message queue for the first message of a given type
+ * and invokes the given callback function, or registers the callback
+ * function for future messages if the queue contains no such message.
+ *
+ * @param {string} messageType
+ * The type of message to search and register for.
+ */
+PeerConnectionTest.prototype.getSignalingMessage = function (messageType) {
+ var i = this.signalingMessageQueue.findIndex(m => m.type === messageType);
+ if (i >= 0) {
+ info(
+ "invoking callback on message " +
+ i +
+ " from message queue, for message type:" +
+ messageType
+ );
+ return Promise.resolve(this.signalingMessageQueue.splice(i, 1)[0]);
+ }
+ return new Promise(resolve =>
+ this.registerSignalingCallback(messageType, resolve)
+ );
+};
+
+/**
+ * This class acts as a wrapper around a DataChannel instance.
+ *
+ * @param dataChannel
+ * @param peerConnectionWrapper
+ * @constructor
+ */
+function DataChannelWrapper(dataChannel, peerConnectionWrapper) {
+ this._channel = dataChannel;
+ this._pc = peerConnectionWrapper;
+
+ info("Creating " + this);
+
+ /**
+ * Setup appropriate callbacks
+ */
+ createOneShotEventWrapper(this, this._channel, "close");
+ createOneShotEventWrapper(this, this._channel, "error");
+ createOneShotEventWrapper(this, this._channel, "message");
+ createOneShotEventWrapper(this, this._channel, "bufferedamountlow");
+
+ this.opened = timerGuard(
+ new Promise(resolve => {
+ this._channel.onopen = () => {
+ this._channel.onopen = unexpectedEvent(this, "onopen");
+ is(this.readyState, "open", "data channel is 'open' after 'onopen'");
+ resolve(this);
+ };
+ }),
+ 180000,
+ "channel didn't open in time"
+ );
+}
+
+DataChannelWrapper.prototype = {
+ /**
+ * Returns the binary type of the channel
+ *
+ * @returns {String} The binary type
+ */
+ get binaryType() {
+ return this._channel.binaryType;
+ },
+
+ /**
+ * Sets the binary type of the channel
+ *
+ * @param {String} type
+ * The new binary type of the channel
+ */
+ set binaryType(type) {
+ this._channel.binaryType = type;
+ },
+
+ /**
+ * Returns the label of the underlying data channel
+ *
+ * @returns {String} The label
+ */
+ get label() {
+ return this._channel.label;
+ },
+
+ /**
+ * Returns the protocol of the underlying data channel
+ *
+ * @returns {String} The protocol
+ */
+ get protocol() {
+ return this._channel.protocol;
+ },
+
+ /**
+ * Returns the id of the underlying data channel
+ *
+ * @returns {number} The stream id
+ */
+ get id() {
+ return this._channel.id;
+ },
+
+ /**
+ * Returns the reliable state of the underlying data channel
+ *
+ * @returns {bool} The stream's reliable state
+ */
+ get reliable() {
+ return this._channel.reliable;
+ },
+
+ /**
+ * Returns the ordered attribute of the data channel
+ *
+ * @returns {bool} The ordered attribute
+ */
+ get ordered() {
+ return this._channel.ordered;
+ },
+
+ /**
+ * Returns the maxPacketLifeTime attribute of the data channel
+ *
+ * @returns {number} The maxPacketLifeTime attribute
+ */
+ get maxPacketLifeTime() {
+ return this._channel.maxPacketLifeTime;
+ },
+
+ /**
+ * Returns the maxRetransmits attribute of the data channel
+ *
+ * @returns {number} The maxRetransmits attribute
+ */
+ get maxRetransmits() {
+ return this._channel.maxRetransmits;
+ },
+
+ /**
+ * Returns the readyState bit of the data channel
+ *
+ * @returns {String} The state of the channel
+ */
+ get readyState() {
+ return this._channel.readyState;
+ },
+
+ get bufferedAmount() {
+ return this._channel.bufferedAmount;
+ },
+
+ /**
+ * Sets the bufferlowthreshold of the channel
+ *
+ * @param {integer} amoutn
+ * The new threshold for the chanel
+ */
+ set bufferedAmountLowThreshold(amount) {
+ this._channel.bufferedAmountLowThreshold = amount;
+ },
+
+ /**
+ * Close the data channel
+ */
+ close() {
+ info(this + ": Closing channel");
+ this._channel.close();
+ },
+
+ /**
+ * Send data through the data channel
+ *
+ * @param {String|Object} data
+ * Data which has to be sent through the data channel
+ */
+ send(data) {
+ info(this + ": Sending data '" + data + "'");
+ this._channel.send(data);
+ },
+
+ /**
+ * Returns the string representation of the class
+ *
+ * @returns {String} The string representation
+ */
+ toString() {
+ return (
+ "DataChannelWrapper (" + this._pc.label + "_" + this._channel.label + ")"
+ );
+ },
+};
+
+/**
+ * This class acts as a wrapper around a PeerConnection instance.
+ *
+ * @constructor
+ * @param {string} label
+ * Description for the peer connection instance
+ * @param {object} configuration
+ * Configuration for the peer connection instance
+ */
+function PeerConnectionWrapper(label, configuration) {
+ this.configuration = configuration;
+ if (configuration && configuration.label_suffix) {
+ label = label + "_" + configuration.label_suffix;
+ }
+ this.label = label;
+
+ this.constraints = [];
+ this.offerOptions = {};
+
+ this.dataChannels = [];
+
+ this._local_ice_candidates = [];
+ this._remote_ice_candidates = [];
+ this.localRequiresTrickleIce = false;
+ this.remoteRequiresTrickleIce = false;
+ this.localMediaElements = [];
+ this.remoteMediaElements = [];
+ this.audioElementsOnly = false;
+
+ this._sendStreams = [];
+
+ this.expectedLocalTrackInfo = [];
+ this.remoteStreamsByTrackId = new Map();
+
+ this.disableRtpCountChecking = false;
+
+ this.iceConnectedResolve;
+ this.iceConnectedReject;
+ this.iceConnected = new Promise((resolve, reject) => {
+ this.iceConnectedResolve = resolve;
+ this.iceConnectedReject = reject;
+ });
+ this.iceCheckingRestartExpected = false;
+ this.iceCheckingIceRollbackExpected = false;
+
+ info("Creating " + this);
+ this._pc = new RTCPeerConnection(this.configuration);
+
+ /**
+ * Setup callback handlers
+ */
+ // This allows test to register their own callbacks for ICE connection state changes
+ this.ice_connection_callbacks = {};
+
+ this._pc.oniceconnectionstatechange = e => {
+ isnot(
+ typeof this._pc.iceConnectionState,
+ "undefined",
+ "iceConnectionState should not be undefined"
+ );
+ var iceState = this._pc.iceConnectionState;
+ info(
+ this + ": oniceconnectionstatechange fired, new state is: " + iceState
+ );
+ Object.keys(this.ice_connection_callbacks).forEach(name => {
+ this.ice_connection_callbacks[name]();
+ });
+ if (iceState === "connected") {
+ this.iceConnectedResolve();
+ } else if (iceState === "failed") {
+ this.iceConnectedReject(new Error("ICE failed"));
+ }
+ };
+
+ this._pc.onicegatheringstatechange = e => {
+ isnot(
+ typeof this._pc.iceGatheringState,
+ "undefined",
+ "iceGetheringState should not be undefined"
+ );
+ var gatheringState = this._pc.iceGatheringState;
+ info(
+ this +
+ ": onicegatheringstatechange fired, new state is: " +
+ gatheringState
+ );
+ };
+
+ createOneShotEventWrapper(this, this._pc, "datachannel");
+ this._pc.addEventListener("datachannel", e => {
+ var wrapper = new DataChannelWrapper(e.channel, this);
+ this.dataChannels.push(wrapper);
+ });
+
+ createOneShotEventWrapper(this, this._pc, "signalingstatechange");
+ createOneShotEventWrapper(this, this._pc, "negotiationneeded");
+}
+
+PeerConnectionWrapper.prototype = {
+ /**
+ * Returns the senders
+ *
+ * @returns {sequence<RTCRtpSender>} the senders
+ */
+ getSenders() {
+ return this._pc.getSenders();
+ },
+
+ /**
+ * Returns the getters
+ *
+ * @returns {sequence<RTCRtpReceiver>} the receivers
+ */
+ getReceivers() {
+ return this._pc.getReceivers();
+ },
+
+ /**
+ * Returns the local description.
+ *
+ * @returns {object} The local description
+ */
+ get localDescription() {
+ return this._pc.localDescription;
+ },
+
+ /**
+ * Returns the remote description.
+ *
+ * @returns {object} The remote description
+ */
+ get remoteDescription() {
+ return this._pc.remoteDescription;
+ },
+
+ /**
+ * Returns the signaling state.
+ *
+ * @returns {object} The local description
+ */
+ get signalingState() {
+ return this._pc.signalingState;
+ },
+ /**
+ * Returns the ICE connection state.
+ *
+ * @returns {object} The local description
+ */
+ get iceConnectionState() {
+ return this._pc.iceConnectionState;
+ },
+
+ setIdentityProvider(provider, options) {
+ this._pc.setIdentityProvider(provider, options);
+ },
+
+ elementPrefix: direction => {
+ return [this.label, direction].join("_");
+ },
+
+ getMediaElementForTrack(track, direction) {
+ var prefix = this.elementPrefix(direction);
+ return getMediaElementForTrack(track, prefix);
+ },
+
+ createMediaElementForTrack(track, direction) {
+ var prefix = this.elementPrefix(direction);
+ return createMediaElementForTrack(track, prefix);
+ },
+
+ ensureMediaElement(track, direction) {
+ var prefix = this.elementPrefix(direction);
+ var element = this.getMediaElementForTrack(track, direction);
+ if (!element) {
+ element = this.createMediaElementForTrack(track, direction);
+ if (direction == "local") {
+ this.localMediaElements.push(element);
+ } else if (direction == "remote") {
+ this.remoteMediaElements.push(element);
+ }
+ }
+
+ // We do this regardless, because sometimes we end up with a new stream with
+ // an old id (ie; the rollback tests cause the same stream to be added
+ // twice)
+ element.srcObject = new MediaStream([track]);
+ element.play();
+ },
+
+ addSendStream(stream) {
+ // The PeerConnection will not necessarily know about this stream
+ // automatically, because replaceTrack is not told about any streams the
+ // new track might be associated with. Only content really knows.
+ this._sendStreams.push(stream);
+ },
+
+ getStreamForSendTrack(track) {
+ return this._sendStreams.find(str => str.getTrackById(track.id));
+ },
+
+ getStreamForRecvTrack(track) {
+ return this._pc.getRemoteStreams().find(s => !!s.getTrackById(track.id));
+ },
+
+ /**
+ * Attaches a local track to this RTCPeerConnection using
+ * RTCPeerConnection.addTrack().
+ *
+ * Also creates a media element playing a MediaStream containing all
+ * tracks that have been added to `stream` using `attachLocalTrack()`.
+ *
+ * @param {MediaStreamTrack} track
+ * MediaStreamTrack to handle
+ * @param {MediaStream} stream
+ * MediaStream to use as container for `track` on remote side
+ */
+ attachLocalTrack(track, stream) {
+ info("Got a local " + track.kind + " track");
+
+ this.expectNegotiationNeeded();
+ var sender = this._pc.addTrack(track, stream);
+ is(sender.track, track, "addTrack returns sender");
+ is(
+ this._pc.getSenders().pop(),
+ sender,
+ "Sender should be the last element in getSenders()"
+ );
+
+ ok(track.id, "track has id");
+ ok(track.kind, "track has kind");
+ ok(stream.id, "stream has id");
+ this.expectedLocalTrackInfo.push({ track, sender, streamId: stream.id });
+ this.addSendStream(stream);
+
+ // This will create one media element per track, which might not be how
+ // we set up things with the RTCPeerConnection. It's the only way
+ // we can ensure all sent tracks are flowing however.
+ this.ensureMediaElement(track, "local");
+
+ return this.observedNegotiationNeeded;
+ },
+
+ /**
+ * Callback when we get local media. Also an appropriate HTML media element
+ * will be created and added to the content node.
+ *
+ * @param {MediaStream} stream
+ * Media stream to handle
+ */
+ attachLocalStream(stream, useAddTransceiver) {
+ info("Got local media stream: (" + stream.id + ")");
+
+ this.expectNegotiationNeeded();
+ if (useAddTransceiver) {
+ info("Using addTransceiver (on PC).");
+ stream.getTracks().forEach(track => {
+ var transceiver = this._pc.addTransceiver(track, { streams: [stream] });
+ is(transceiver.sender.track, track, "addTransceiver returns sender");
+ });
+ }
+ // In order to test both the addStream and addTrack APIs, we do half one
+ // way, half the other, at random.
+ else if (Math.random() < 0.5) {
+ info("Using addStream.");
+ this._pc.addStream(stream);
+ ok(
+ this._pc
+ .getSenders()
+ .find(sender => sender.track == stream.getTracks()[0]),
+ "addStream returns sender"
+ );
+ } else {
+ info("Using addTrack (on PC).");
+ stream.getTracks().forEach(track => {
+ var sender = this._pc.addTrack(track, stream);
+ is(sender.track, track, "addTrack returns sender");
+ });
+ }
+
+ this.addSendStream(stream);
+
+ stream.getTracks().forEach(track => {
+ ok(track.id, "track has id");
+ ok(track.kind, "track has kind");
+ const sender = this._pc.getSenders().find(s => s.track == track);
+ ok(sender, "track has a sender");
+ this.expectedLocalTrackInfo.push({ track, sender, streamId: stream.id });
+ this.ensureMediaElement(track, "local");
+ });
+
+ return this.observedNegotiationNeeded;
+ },
+
+ removeSender(index) {
+ var sender = this._pc.getSenders()[index];
+ this.expectedLocalTrackInfo = this.expectedLocalTrackInfo.filter(
+ i => i.sender != sender
+ );
+ this.expectNegotiationNeeded();
+ this._pc.removeTrack(sender);
+ return this.observedNegotiationNeeded;
+ },
+
+ senderReplaceTrack(sender, withTrack, stream) {
+ const info = this.expectedLocalTrackInfo.find(i => i.sender == sender);
+ if (!info) {
+ return; // replaceTrack on a null track, probably
+ }
+ info.track = withTrack;
+ this.addSendStream(stream);
+ this.ensureMediaElement(withTrack, "local");
+ return sender.replaceTrack(withTrack);
+ },
+
+ async getUserMedia(constraints) {
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ var stream = await getUserMedia(constraints);
+ if (constraints.audio) {
+ stream.getAudioTracks().forEach(track => {
+ info(
+ this +
+ " gUM local stream " +
+ stream.id +
+ " with audio track " +
+ track.id
+ );
+ });
+ }
+ if (constraints.video) {
+ stream.getVideoTracks().forEach(track => {
+ info(
+ this +
+ " gUM local stream " +
+ stream.id +
+ " with video track " +
+ track.id
+ );
+ });
+ }
+ return stream;
+ },
+
+ /**
+ * Requests all the media streams as specified in the constrains property.
+ *
+ * @param {array} constraintsList
+ * Array of constraints for GUM calls
+ */
+ getAllUserMedia(constraintsList) {
+ if (constraintsList.length === 0) {
+ info("Skipping GUM: no UserMedia requested");
+ return Promise.resolve();
+ }
+
+ info("Get " + constraintsList.length + " local streams");
+ return Promise.all(
+ constraintsList.map(constraints => this.getUserMedia(constraints))
+ );
+ },
+
+ async getAllUserMediaAndAddStreams(constraintsList) {
+ var streams = await this.getAllUserMedia(constraintsList);
+ if (!streams) {
+ return;
+ }
+ return Promise.all(streams.map(stream => this.attachLocalStream(stream)));
+ },
+
+ async getAllUserMediaAndAddTransceivers(constraintsList) {
+ var streams = await this.getAllUserMedia(constraintsList);
+ if (!streams) {
+ return;
+ }
+ return Promise.all(
+ streams.map(stream => this.attachLocalStream(stream, true))
+ );
+ },
+
+ /**
+ * Create a new data channel instance. Also creates a promise called
+ * `this.nextDataChannel` that resolves when the next data channel arrives.
+ */
+ expectDataChannel(message) {
+ this.nextDataChannel = new Promise(resolve => {
+ this.ondatachannel = e => {
+ ok(e.channel, message);
+ is(
+ e.channel.readyState,
+ "open",
+ "data channel in 'open' after 'ondatachannel'"
+ );
+ resolve(e.channel);
+ };
+ });
+ },
+
+ /**
+ * Create a new data channel instance
+ *
+ * @param {Object} options
+ * Options which get forwarded to nsIPeerConnection.createDataChannel
+ * @returns {DataChannelWrapper} The created data channel
+ */
+ createDataChannel(options) {
+ var label = "channel_" + this.dataChannels.length;
+ info(this + ": Create data channel '" + label);
+
+ if (!this.dataChannels.length) {
+ this.expectNegotiationNeeded();
+ }
+ var channel = this._pc.createDataChannel(label, options);
+ is(channel.readyState, "connecting", "initial readyState is 'connecting'");
+ var wrapper = new DataChannelWrapper(channel, this);
+ this.dataChannels.push(wrapper);
+ return wrapper;
+ },
+
+ /**
+ * Creates an offer and automatically handles the failure case.
+ */
+ createOffer() {
+ return this._pc.createOffer(this.offerOptions).then(offer => {
+ info("Got offer: " + JSON.stringify(offer));
+ // note: this might get updated through ICE gathering
+ this._latest_offer = offer;
+ return offer;
+ });
+ },
+
+ /**
+ * Creates an answer and automatically handles the failure case.
+ */
+ createAnswer() {
+ return this._pc.createAnswer().then(answer => {
+ info(this + ": Got answer: " + JSON.stringify(answer));
+ this._last_answer = answer;
+ return answer;
+ });
+ },
+
+ /**
+ * Sets the local description and automatically handles the failure case.
+ *
+ * @param {object} desc
+ * RTCSessionDescriptionInit for the local description request
+ */
+ setLocalDescription(desc) {
+ this.observedNegotiationNeeded = undefined;
+ return this._pc.setLocalDescription(desc).then(() => {
+ info(this + ": Successfully set the local description");
+ });
+ },
+
+ /**
+ * Tries to set the local description and expect failure. Automatically
+ * causes the test case to fail if the call succeeds.
+ *
+ * @param {object} desc
+ * RTCSessionDescriptionInit for the local description request
+ * @returns {Promise}
+ * A promise that resolves to the expected error
+ */
+ setLocalDescriptionAndFail(desc) {
+ return this._pc
+ .setLocalDescription(desc)
+ .then(
+ generateErrorCallback("setLocalDescription should have failed."),
+ err => {
+ info(this + ": As expected, failed to set the local description");
+ return err;
+ }
+ );
+ },
+
+ /**
+ * Sets the remote description and automatically handles the failure case.
+ *
+ * @param {object} desc
+ * RTCSessionDescriptionInit for the remote description request
+ */
+ setRemoteDescription(desc) {
+ this.observedNegotiationNeeded = undefined;
+ // This has to be done before calling sRD, otherwise a candidate in flight
+ // could end up in the PC's operations queue before sRD resolves.
+ if (desc.type == "rollback") {
+ this.holdIceCandidates = new Promise(
+ r => (this.releaseIceCandidates = r)
+ );
+ }
+ return this._pc.setRemoteDescription(desc).then(() => {
+ info(this + ": Successfully set remote description");
+ if (desc.type != "rollback") {
+ this.releaseIceCandidates();
+ }
+ });
+ },
+
+ /**
+ * Tries to set the remote description and expect failure. Automatically
+ * causes the test case to fail if the call succeeds.
+ *
+ * @param {object} desc
+ * RTCSessionDescriptionInit for the remote description request
+ * @returns {Promise}
+ * a promise that resolve to the returned error
+ */
+ setRemoteDescriptionAndFail(desc) {
+ return this._pc
+ .setRemoteDescription(desc)
+ .then(
+ generateErrorCallback("setRemoteDescription should have failed."),
+ err => {
+ info(this + ": As expected, failed to set the remote description");
+ return err;
+ }
+ );
+ },
+
+ /**
+ * Registers a callback for the signaling state change and
+ * appends the new state to an array for logging it later.
+ */
+ logSignalingState() {
+ this.signalingStateLog = [this._pc.signalingState];
+ this._pc.addEventListener("signalingstatechange", e => {
+ var newstate = this._pc.signalingState;
+ var oldstate = this.signalingStateLog[this.signalingStateLog.length - 1];
+ if (Object.keys(signalingStateTransitions).includes(oldstate)) {
+ ok(
+ signalingStateTransitions[oldstate].includes(newstate),
+ this +
+ ": legal signaling state transition from " +
+ oldstate +
+ " to " +
+ newstate
+ );
+ } else {
+ ok(
+ false,
+ this +
+ ": old signaling state " +
+ oldstate +
+ " missing in signaling transition array"
+ );
+ }
+ this.signalingStateLog.push(newstate);
+ });
+ },
+
+ isTrackOnPC(track) {
+ return !!this.getStreamForRecvTrack(track);
+ },
+
+ allExpectedTracksAreObserved(expected, observed) {
+ return Object.keys(expected).every(trackId => observed[trackId]);
+ },
+
+ setupStreamEventHandlers(stream) {
+ const myTrackIds = new Set(stream.getTracks().map(t => t.id));
+
+ stream.addEventListener("addtrack", ({ track }) => {
+ ok(
+ !myTrackIds.has(track.id),
+ "Duplicate addtrack callback: " +
+ `stream id=${stream.id} track id=${track.id}`
+ );
+ myTrackIds.add(track.id);
+ // addtrack events happen before track events, so the track callback hasn't
+ // heard about this yet.
+ let streams = this.remoteStreamsByTrackId.get(track.id);
+ ok(
+ !streams || !streams.has(stream.id),
+ `In addtrack for stream id=${stream.id}` +
+ `there should not have been a track event for track id=${track.id} ` +
+ " containing this stream yet."
+ );
+ ok(
+ stream.getTracks().includes(track),
+ "In addtrack, stream id=" +
+ `${stream.id} should already contain track id=${track.id}`
+ );
+ });
+
+ stream.addEventListener("removetrack", ({ track }) => {
+ ok(
+ myTrackIds.has(track.id),
+ "Duplicate removetrack callback: " +
+ `stream id=${stream.id} track id=${track.id}`
+ );
+ myTrackIds.delete(track.id);
+ // Also remove the association from remoteStreamsByTrackId
+ const streams = this.remoteStreamsByTrackId.get(track.id);
+ ok(
+ streams,
+ `In removetrack for stream id=${stream.id}, track id=` +
+ `${track.id} should have had a track callback for the stream.`
+ );
+ streams.delete(stream.id);
+ ok(
+ !stream.getTracks().includes(track),
+ "In removetrack, stream id=" +
+ `${stream.id} should not contain track id=${track.id}`
+ );
+ });
+ },
+
+ setupTrackEventHandler() {
+ this._pc.addEventListener("track", ({ track, streams }) => {
+ info(`${this}: 'ontrack' event fired for ${track.id}`);
+ ok(this.isTrackOnPC(track), `Found track ${track.id}`);
+
+ let gratuitousEvent = true;
+ let streamsContainingTrack = this.remoteStreamsByTrackId.get(track.id);
+ if (!streamsContainingTrack) {
+ gratuitousEvent = false; // Told us about a new track
+ this.remoteStreamsByTrackId.set(track.id, new Set());
+ streamsContainingTrack = this.remoteStreamsByTrackId.get(track.id);
+ }
+
+ for (const stream of streams) {
+ ok(
+ stream.getTracks().includes(track),
+ `In track event, track id=${track.id}` +
+ ` should already be in stream id=${stream.id}`
+ );
+
+ if (!streamsContainingTrack.has(stream.id)) {
+ gratuitousEvent = false; // Told us about a new stream
+ streamsContainingTrack.add(stream.id);
+ this.setupStreamEventHandlers(stream);
+ }
+ }
+
+ ok(!gratuitousEvent, "track event told us something new");
+
+ // So far, we've verified consistency between the current state of the
+ // streams, addtrack/removetrack events on the streams, and track events
+ // on the peerconnection. We have also verified that we have not gotten
+ // any gratuitous events. We have not done anything to verify that the
+ // current state of affairs matches what we were expecting it to.
+
+ this.ensureMediaElement(track, "remote");
+ });
+ },
+
+ /**
+ * Either adds a given ICE candidate right away or stores it to be added
+ * later, depending on the state of the PeerConnection.
+ *
+ * @param {object} candidate
+ * The RTCIceCandidate to be added or stored
+ */
+ storeOrAddIceCandidate(candidate) {
+ this._remote_ice_candidates.push(candidate);
+ if (this.signalingState === "closed") {
+ info("Received ICE candidate for closed PeerConnection - discarding");
+ return;
+ }
+ this.holdIceCandidates
+ .then(() => {
+ info(this + ": adding ICE candidate " + JSON.stringify(candidate));
+ return this._pc.addIceCandidate(candidate);
+ })
+ .then(() => ok(true, this + " successfully added an ICE candidate"))
+ .catch(e =>
+ // The onicecandidate callback runs independent of the test steps
+ // and therefore errors thrown from in there don't get caught by the
+ // race of the Promises around our test steps.
+ // Note: as long as we are queuing ICE candidates until the success
+ // of sRD() this should never ever happen.
+ ok(false, this + " adding ICE candidate failed with: " + e.message)
+ );
+ },
+
+ /**
+ * Registers a callback for the ICE connection state change and
+ * appends the new state to an array for logging it later.
+ */
+ logIceConnectionState() {
+ this.iceConnectionLog = [this._pc.iceConnectionState];
+ this.ice_connection_callbacks.logIceStatus = () => {
+ var newstate = this._pc.iceConnectionState;
+ var oldstate = this.iceConnectionLog[this.iceConnectionLog.length - 1];
+ if (Object.keys(iceStateTransitions).includes(oldstate)) {
+ if (this.iceCheckingRestartExpected) {
+ is(
+ newstate,
+ "checking",
+ "iceconnectionstate event '" +
+ newstate +
+ "' matches expected state 'checking'"
+ );
+ this.iceCheckingRestartExpected = false;
+ } else if (this.iceCheckingIceRollbackExpected) {
+ is(
+ newstate,
+ "connected",
+ "iceconnectionstate event '" +
+ newstate +
+ "' matches expected state 'connected'"
+ );
+ this.iceCheckingIceRollbackExpected = false;
+ } else {
+ ok(
+ iceStateTransitions[oldstate].includes(newstate),
+ this +
+ ": legal ICE state transition from " +
+ oldstate +
+ " to " +
+ newstate
+ );
+ }
+ } else {
+ ok(
+ false,
+ this +
+ ": old ICE state " +
+ oldstate +
+ " missing in ICE transition array"
+ );
+ }
+ this.iceConnectionLog.push(newstate);
+ };
+ },
+
+ /**
+ * Resets the ICE connected Promise and allows ICE connection state monitoring
+ * to go backwards to 'checking'.
+ */
+ expectIceChecking() {
+ this.iceCheckingRestartExpected = true;
+ this.iceConnected = new Promise((resolve, reject) => {
+ this.iceConnectedResolve = resolve;
+ this.iceConnectedReject = reject;
+ });
+ },
+
+ /**
+ * Waits for ICE to either connect or fail.
+ *
+ * @returns {Promise}
+ * resolves when connected, rejects on failure
+ */
+ waitForIceConnected() {
+ return this.iceConnected;
+ },
+
+ /**
+ * Setup a onicecandidate handler
+ *
+ * @param {object} test
+ * A PeerConnectionTest object to which the ice candidates gets
+ * forwarded.
+ */
+ setupIceCandidateHandler(test, candidateHandler) {
+ candidateHandler = candidateHandler || test.iceCandidateHandler.bind(test);
+
+ var resolveEndOfTrickle;
+ this.endOfTrickleIce = new Promise(r => (resolveEndOfTrickle = r));
+ this.holdIceCandidates = new Promise(r => (this.releaseIceCandidates = r));
+
+ this._pc.onicecandidate = anEvent => {
+ if (!anEvent.candidate) {
+ this._pc.onicecandidate = () =>
+ ok(
+ false,
+ this.label + " received ICE candidate after end of trickle"
+ );
+ info(this.label + ": received end of trickle ICE event");
+ ok(
+ this._pc.iceGatheringState === "complete",
+ "ICE gathering state has reached complete"
+ );
+ resolveEndOfTrickle(this.label);
+ return;
+ }
+
+ info(
+ this.label + ": iceCandidate = " + JSON.stringify(anEvent.candidate)
+ );
+ ok(anEvent.candidate.sdpMid.length, "SDP mid not empty");
+ ok(
+ anEvent.candidate.usernameFragment.length,
+ "usernameFragment not empty"
+ );
+
+ ok(
+ typeof anEvent.candidate.sdpMLineIndex === "number",
+ "SDP MLine Index needs to exist"
+ );
+ this._local_ice_candidates.push(anEvent.candidate);
+ candidateHandler(this.label, anEvent.candidate);
+ };
+ },
+
+ checkLocalMediaTracks() {
+ info(
+ `${this}: Checking local tracks ${JSON.stringify(
+ this.expectedLocalTrackInfo
+ )}`
+ );
+ const sendersWithTrack = this._pc.getSenders().filter(({ track }) => track);
+ is(
+ sendersWithTrack.length,
+ this.expectedLocalTrackInfo.length,
+ "The number of senders with a track should be equal to the number of " +
+ "expected local tracks."
+ );
+
+ // expectedLocalTrackInfo is in the same order that the tracks were added, and
+ // so should the output of getSenders.
+ this.expectedLocalTrackInfo.forEach((info, i) => {
+ const sender = sendersWithTrack[i];
+ is(sender, info.sender, `Sender ${i} should match`);
+ is(sender.track, info.track, `Track ${i} should match`);
+ });
+ },
+
+ /**
+ * Checks that we are getting the media tracks we expect.
+ */
+ checkMediaTracks() {
+ this.checkLocalMediaTracks();
+ },
+
+ checkLocalMsids() {
+ const sdp = this.localDescription.sdp;
+ const msections = sdputils.getMSections(sdp);
+ const expectedStreamIdCounts = new Map();
+ for (const { track, sender, streamId } of this.expectedLocalTrackInfo) {
+ const transceiver = this._pc
+ .getTransceivers()
+ .find(t => t.sender == sender);
+ ok(transceiver, "There should be a transceiver for each sender");
+ if (transceiver.mid) {
+ const midFinder = new RegExp(`^a=mid:${transceiver.mid}$`, "m");
+ const msection = msections.find(m => m.match(midFinder));
+ ok(
+ msection,
+ `There should be a media section for mid = ${transceiver.mid}`
+ );
+ ok(
+ msection.startsWith(`m=${track.kind}`),
+ `Media section should be of type ${track.kind}`
+ );
+ const msidFinder = new RegExp(`^a=msid:${streamId} \\S+$`, "m");
+ ok(
+ msection.match(msidFinder),
+ `Should find a=msid:${streamId} in media section` +
+ " (with any track id for now)"
+ );
+ const count = expectedStreamIdCounts.get(streamId) || 0;
+ expectedStreamIdCounts.set(streamId, count + 1);
+ }
+ }
+
+ // Check for any unexpected msids.
+ const allMsids = sdp.match(new RegExp("^a=msid:\\S+", "mg"));
+ if (!allMsids) {
+ return;
+ }
+ const allStreamIds = allMsids.map(msidAttr =>
+ msidAttr.replace("a=msid:", "")
+ );
+ allStreamIds.forEach(id => {
+ const count = expectedStreamIdCounts.get(id);
+ ok(count, `Unexpected stream id ${id} found in local description.`);
+ if (count) {
+ expectedStreamIdCounts.set(id, count - 1);
+ }
+ });
+ },
+
+ /**
+ * Check that media flow is present for the given media element by checking
+ * that it reaches ready state HAVE_ENOUGH_DATA and progresses time further
+ * than the start of the check.
+ *
+ * This ensures, that the stream being played is producing
+ * data and, in case it contains a video track, that at least one video frame
+ * has been displayed.
+ *
+ * @param {HTMLMediaElement} track
+ * The media element to check
+ * @returns {Promise}
+ * A promise that resolves when media data is flowing.
+ */
+ waitForMediaElementFlow(element) {
+ info("Checking data flow for element: " + element.id);
+ is(
+ element.ended,
+ !element.srcObject.active,
+ "Element ended should be the inverse of the MediaStream's active state"
+ );
+ if (element.ended) {
+ is(
+ element.readyState,
+ element.HAVE_CURRENT_DATA,
+ "Element " + element.id + " is ended and should have had data"
+ );
+ return Promise.resolve();
+ }
+
+ const haveEnoughData = (
+ element.readyState == element.HAVE_ENOUGH_DATA
+ ? Promise.resolve()
+ : haveEvent(
+ element,
+ "canplay",
+ wait(60000, new Error("Timeout for element " + element.id))
+ )
+ ).then(_ => info("Element " + element.id + " has enough data."));
+
+ const startTime = element.currentTime;
+ const timeProgressed = timeout(
+ listenUntil(element, "timeupdate", _ => element.currentTime > startTime),
+ 60000,
+ "Element " + element.id + " should progress currentTime"
+ ).then();
+
+ return Promise.all([haveEnoughData, timeProgressed]);
+ },
+
+ /**
+ * Wait for RTP packet flow for the given MediaStreamTrack.
+ *
+ * @param {object} track
+ * A MediaStreamTrack to wait for data flow on.
+ * @returns {Promise}
+ * Returns a promise which yields a StatsReport object with RTP stats.
+ */
+ async _waitForRtpFlow(target, rtpType) {
+ const { track } = target;
+ info(`_waitForRtpFlow(${track.id}, ${rtpType})`);
+ const packets = `packets${rtpType == "outbound-rtp" ? "Sent" : "Received"}`;
+
+ const retryInterval = 500; // Time between stats checks
+ const timeout = 30000; // Timeout in ms
+ const retries = timeout / retryInterval;
+
+ for (let i = 0; i < retries; i++) {
+ info(`Checking ${rtpType} for ${track.kind} track ${track.id} try ${i}`);
+ for (const rtp of (await target.getStats()).values()) {
+ if (rtp.type != rtpType) {
+ continue;
+ }
+ if (rtp.kind != track.kind) {
+ continue;
+ }
+
+ const numPackets = rtp[packets];
+ info(`Track ${track.id} has ${numPackets} ${packets}.`);
+ if (!numPackets) {
+ continue;
+ }
+
+ ok(true, `RTP flowing for ${track.kind} track ${track.id}`);
+ return;
+ }
+ await wait(retryInterval);
+ }
+ throw new Error(
+ `Checking stats for track ${track.id} timed out after ${timeout} ms`
+ );
+ },
+
+ /**
+ * Wait for inbound RTP packet flow for the given MediaStreamTrack.
+ *
+ * @param {object} receiver
+ * An RTCRtpReceiver to wait for data flow on.
+ * @returns {Promise}
+ * Returns a promise that resolves once data is flowing.
+ */
+ async waitForInboundRtpFlow(receiver) {
+ return this._waitForRtpFlow(receiver, "inbound-rtp");
+ },
+
+ /**
+ * Wait for outbound RTP packet flow for the given MediaStreamTrack.
+ *
+ * @param {object} sender
+ * An RTCRtpSender to wait for data flow on.
+ * @returns {Promise}
+ * Returns a promise that resolves once data is flowing.
+ */
+ async waitForOutboundRtpFlow(sender) {
+ return this._waitForRtpFlow(sender, "outbound-rtp");
+ },
+
+ getExpectedActiveReceivers() {
+ return this._pc
+ .getTransceivers()
+ .filter(
+ t =>
+ !t.stopped &&
+ t.currentDirection &&
+ t.currentDirection != "inactive" &&
+ t.currentDirection != "sendonly"
+ )
+ .filter(({ receiver }) => receiver.track)
+ .map(({ mid, currentDirection, receiver }) => {
+ info(
+ `Found transceiver that should be receiving RTP: mid=${mid}` +
+ ` currentDirection=${currentDirection}` +
+ ` kind=${receiver.track.kind} track-id=${receiver.track.id}`
+ );
+ return receiver;
+ });
+ },
+
+ getExpectedSenders() {
+ return this._pc.getSenders().filter(({ track }) => track);
+ },
+
+ /**
+ * Wait for presence of video flow on all media elements and rtp flow on
+ * all sending and receiving track involved in this test.
+ *
+ * @returns {Promise}
+ * A promise that resolves when media flows for all elements and tracks
+ */
+ waitForMediaFlow() {
+ const receivers = this.getExpectedActiveReceivers();
+ return Promise.all([
+ ...this.localMediaElements.map(el => this.waitForMediaElementFlow(el)),
+ ...this.remoteMediaElements
+ .filter(({ srcObject }) =>
+ receivers.some(({ track }) =>
+ srcObject.getTracks().some(t => t == track)
+ )
+ )
+ .map(el => this.waitForMediaElementFlow(el)),
+ ...receivers.map(receiver => this.waitForInboundRtpFlow(receiver)),
+ ...this.getExpectedSenders().map(sender =>
+ this.waitForOutboundRtpFlow(sender)
+ ),
+ ]);
+ },
+
+ /**
+ * Check that correct audio (typically a flat tone) is flowing to this
+ * PeerConnection for each transceiver that should be receiving. Uses
+ * WebAudio AnalyserNodes to compare input and output audio data in the
+ * frequency domain.
+ *
+ * @param {object} from
+ * A PeerConnectionWrapper whose audio RTPSender we use as source for
+ * the audio flow check.
+ * @returns {Promise}
+ * A promise that resolves when we're receiving the tone/s from |from|.
+ */
+ async checkReceivingToneFrom(
+ audiocontext,
+ from,
+ cancel = wait(60000, new Error("Tone not detected"))
+ ) {
+ let localTransceivers = this._pc
+ .getTransceivers()
+ .filter(t => t.mid)
+ .filter(t => t.receiver.track.kind == "audio")
+ .sort((t1, t2) => t1.mid < t2.mid);
+ let remoteTransceivers = from._pc
+ .getTransceivers()
+ .filter(t => t.mid)
+ .filter(t => t.receiver.track.kind == "audio")
+ .sort((t1, t2) => t1.mid < t2.mid);
+
+ is(
+ localTransceivers.length,
+ remoteTransceivers.length,
+ "Same number of associated audio transceivers on remote and local."
+ );
+
+ for (let i = 0; i < localTransceivers.length; i++) {
+ is(
+ localTransceivers[i].mid,
+ remoteTransceivers[i].mid,
+ "Transceivers at index " + i + " have the same mid."
+ );
+
+ if (!remoteTransceivers[i].sender.track) {
+ continue;
+ }
+
+ if (
+ remoteTransceivers[i].currentDirection == "recvonly" ||
+ remoteTransceivers[i].currentDirection == "inactive"
+ ) {
+ continue;
+ }
+
+ let sendTrack = remoteTransceivers[i].sender.track;
+ let inputElem = from.getMediaElementForTrack(sendTrack, "local");
+ ok(
+ inputElem,
+ "Remote wrapper should have a media element for track id " +
+ sendTrack.id
+ );
+ let inputAudioStream = from.getStreamForSendTrack(sendTrack);
+ ok(
+ inputAudioStream,
+ "Remote wrapper should have a stream for track id " + sendTrack.id
+ );
+ let inputAnalyser = new AudioStreamAnalyser(
+ audiocontext,
+ inputAudioStream
+ );
+
+ let recvTrack = localTransceivers[i].receiver.track;
+ let outputAudioStream = this.getStreamForRecvTrack(recvTrack);
+ ok(
+ outputAudioStream,
+ "Local wrapper should have a stream for track id " + recvTrack.id
+ );
+ let outputAnalyser = new AudioStreamAnalyser(
+ audiocontext,
+ outputAudioStream
+ );
+
+ let error = null;
+ cancel.then(e => (error = e));
+
+ let indexOfMax = data =>
+ data.reduce((max, val, i) => (val >= data[max] ? i : max), 0);
+
+ await outputAnalyser.waitForAnalysisSuccess(() => {
+ if (error) {
+ throw error;
+ }
+
+ let inputData = inputAnalyser.getByteFrequencyData();
+ let outputData = outputAnalyser.getByteFrequencyData();
+
+ let inputMax = indexOfMax(inputData);
+ let outputMax = indexOfMax(outputData);
+ info(
+ `Comparing maxima; input[${inputMax}] = ${inputData[inputMax]},` +
+ ` output[${outputMax}] = ${outputData[outputMax]}`
+ );
+ if (!inputData[inputMax] || !outputData[outputMax]) {
+ return false;
+ }
+
+ // When the input and output maxima are within reasonable distance (2% of
+ // total length, which means ~10 for length 512) from each other, we can
+ // be sure that the input tone has made it through the peer connection.
+ info(`input data length: ${inputData.length}`);
+ return Math.abs(inputMax - outputMax) < inputData.length * 0.02;
+ });
+ }
+ },
+
+ /**
+ * Check that stats are present by checking for known stats.
+ */
+ async getStats(selector) {
+ const stats = await this._pc.getStats(selector);
+ const dict = {};
+ for (const [k, v] of stats.entries()) {
+ dict[k] = v;
+ }
+ info(`${this}: Got stats: ${JSON.stringify(dict)}`);
+ return stats;
+ },
+
+ /**
+ * Checks that we are getting the media streams we expect.
+ *
+ * @param {object} stats
+ * The stats to check from this PeerConnectionWrapper
+ */
+ checkStats(stats) {
+ const isRemote = ({ type }) =>
+ ["remote-outbound-rtp", "remote-inbound-rtp"].includes(type);
+ var counters = {};
+ for (let [key, res] of stats) {
+ info("Checking stats for " + key + " : " + res);
+ // validate stats
+ ok(res.id == key, "Coherent stats id");
+ const now = performance.timeOrigin + performance.now();
+ const minimum = performance.timeOrigin;
+ const type = isRemote(res) ? "rtcp" : "rtp";
+ ok(
+ res.timestamp >= minimum,
+ `Valid ${type} timestamp ${res.timestamp} >= ${minimum} (
+ ${res.timestamp - minimum} ms)`
+ );
+ ok(
+ res.timestamp <= now,
+ `Valid ${type} timestamp ${res.timestamp} <= ${now} (
+ ${res.timestamp - now} ms)`
+ );
+ if (isRemote(res)) {
+ continue;
+ }
+ counters[res.type] = (counters[res.type] || 0) + 1;
+
+ switch (res.type) {
+ case "inbound-rtp":
+ case "outbound-rtp":
+ {
+ // Inbound tracks won't have an ssrc if RTP is not flowing.
+ // (eg; negotiated inactive)
+ ok(
+ res.ssrc || res.type == "inbound-rtp",
+ "Outbound RTP stats has an ssrc."
+ );
+
+ if (res.ssrc) {
+ // ssrc is a 32 bit number returned as an unsigned long
+ ok(!/[^0-9]/.test(`${res.ssrc}`), "SSRC is numeric");
+ ok(parseInt(res.ssrc) < Math.pow(2, 32), "SSRC is within limits");
+ }
+
+ if (res.type == "outbound-rtp") {
+ ok(res.packetsSent !== undefined, "Rtp packetsSent");
+ // We assume minimum payload to be 1 byte (guess from RFC 3550)
+ ok(res.bytesSent >= res.packetsSent, "Rtp bytesSent");
+ } else {
+ ok(res.packetsReceived !== undefined, "Rtp packetsReceived");
+ ok(res.bytesReceived >= res.packetsReceived, "Rtp bytesReceived");
+ }
+ if (res.remoteId) {
+ var rem = stats.get(res.remoteId);
+ ok(isRemote(rem), "Remote is rtcp");
+ ok(rem.localId == res.id, "Remote backlink match");
+ if (res.type == "outbound-rtp") {
+ ok(rem.type == "remote-inbound-rtp", "Rtcp is inbound");
+ if (rem.packetsLost) {
+ ok(
+ rem.packetsLost >= 0,
+ "Rtcp packetsLost " + rem.packetsLost + " >= 0"
+ );
+ ok(
+ rem.packetsLost < 1000,
+ "Rtcp packetsLost " + rem.packetsLost + " < 1000"
+ );
+ }
+ if (!this.disableRtpCountChecking) {
+ // no guarantee which one is newer!
+ // Note: this must change when we add a timestamp field to remote RTCP reports
+ // and make rem.timestamp be the reception time
+ if (res.timestamp < rem.timestamp) {
+ info(
+ "REVERSED timestamps: rec:" +
+ rem.packetsReceived +
+ " time:" +
+ rem.timestamp +
+ " sent:" +
+ res.packetsSent +
+ " time:" +
+ res.timestamp
+ );
+ }
+ }
+ if (rem.jitter) {
+ ok(rem.jitter >= 0, "Rtcp jitter " + rem.jitter + " >= 0");
+ ok(rem.jitter < 5, "Rtcp jitter " + rem.jitter + " < 5 sec");
+ }
+ if (rem.roundTripTime) {
+ ok(
+ rem.roundTripTime >= 0,
+ "Rtcp rtt " + rem.roundTripTime + " >= 0"
+ );
+ ok(
+ rem.roundTripTime < 60,
+ "Rtcp rtt " + rem.roundTripTime + " < 1 min"
+ );
+ }
+ } else {
+ ok(rem.type == "remote-outbound-rtp", "Rtcp is outbound");
+ ok(rem.packetsSent !== undefined, "Rtcp packetsSent");
+ ok(rem.bytesSent !== undefined, "Rtcp bytesSent");
+ }
+ ok(rem.ssrc == res.ssrc, "Remote ssrc match");
+ } else {
+ info("No rtcp info received yet");
+ }
+ }
+ break;
+ }
+ }
+
+ var nin = this._pc.getTransceivers().filter(t => {
+ return (
+ !t.stopped &&
+ t.currentDirection != "inactive" &&
+ t.currentDirection != "sendonly"
+ );
+ }).length;
+ const nout = Object.keys(this.expectedLocalTrackInfo).length;
+ var ndata = this.dataChannels.length;
+
+ // TODO(Bug 957145): Restore stronger inbound-rtp test once Bug 948249 is fixed
+ //is((counters["inbound-rtp"] || 0), nin, "Have " + nin + " inbound-rtp stat(s)");
+ ok(
+ (counters["inbound-rtp"] || 0) >= nin,
+ "Have at least " + nin + " inbound-rtp stat(s) *"
+ );
+
+ is(
+ counters["outbound-rtp"] || 0,
+ nout,
+ "Have " + nout + " outbound-rtp stat(s)"
+ );
+
+ var numLocalCandidates = counters["local-candidate"] || 0;
+ var numRemoteCandidates = counters["remote-candidate"] || 0;
+ // If there are no tracks, there will be no stats either.
+ if (nin + nout + ndata > 0) {
+ ok(numLocalCandidates, "Have local-candidate stat(s)");
+ ok(numRemoteCandidates, "Have remote-candidate stat(s)");
+ } else {
+ is(numLocalCandidates, 0, "Have no local-candidate stats");
+ is(numRemoteCandidates, 0, "Have no remote-candidate stats");
+ }
+ },
+
+ /**
+ * Compares the Ice server configured for this PeerConnectionWrapper
+ * with the ICE candidates received in the RTCP stats.
+ *
+ * @param {object} stats
+ * The stats to be verified for relayed vs. direct connection.
+ */
+ checkStatsIceConnectionType(stats, expectedLocalCandidateType) {
+ let lId;
+ let rId;
+ for (let stat of stats.values()) {
+ if (stat.type == "candidate-pair" && stat.selected) {
+ lId = stat.localCandidateId;
+ rId = stat.remoteCandidateId;
+ break;
+ }
+ }
+ isnot(
+ lId,
+ undefined,
+ "Got local candidate ID " + lId + " for selected pair"
+ );
+ isnot(
+ rId,
+ undefined,
+ "Got remote candidate ID " + rId + " for selected pair"
+ );
+ let lCand = stats.get(lId);
+ let rCand = stats.get(rId);
+ if (!lCand || !rCand) {
+ ok(
+ false,
+ "failed to find candidatepair IDs or stats for local: " +
+ lId +
+ " remote: " +
+ rId
+ );
+ return;
+ }
+
+ info(
+ "checkStatsIceConnectionType verifying: local=" +
+ JSON.stringify(lCand) +
+ " remote=" +
+ JSON.stringify(rCand)
+ );
+ expectedLocalCandidateType = expectedLocalCandidateType || "host";
+ var candidateType = lCand.candidateType;
+ if (lCand.relayProtocol === "tcp" && candidateType === "relay") {
+ candidateType = "relay-tcp";
+ }
+
+ if (lCand.relayProtocol === "tls" && candidateType === "relay") {
+ candidateType = "relay-tls";
+ }
+
+ if (expectedLocalCandidateType === "srflx" && candidateType === "prflx") {
+ // Be forgiving of prflx when expecting srflx, since that can happen due
+ // to timing.
+ candidateType = "srflx";
+ }
+
+ is(
+ candidateType,
+ expectedLocalCandidateType,
+ "Local candidate type is what we expected for selected pair"
+ );
+ },
+
+ /**
+ * Compares amount of established ICE connection according to ICE candidate
+ * pairs in the stats reporting with the expected amount of connection based
+ * on the constraints.
+ *
+ * @param {object} stats
+ * The stats to check for ICE candidate pairs
+ * @param {object} testOptions
+ * The test options object from the PeerConnectionTest
+ */
+ checkStatsIceConnections(stats, testOptions) {
+ var numIceConnections = 0;
+ stats.forEach(stat => {
+ if (stat.type === "candidate-pair" && stat.selected) {
+ numIceConnections += 1;
+ }
+ });
+ info("ICE connections according to stats: " + numIceConnections);
+ isnot(
+ numIceConnections,
+ 0,
+ "Number of ICE connections according to stats is not zero"
+ );
+ if (testOptions.bundle) {
+ if (testOptions.rtcpmux) {
+ is(numIceConnections, 1, "stats reports exactly 1 ICE connection");
+ } else {
+ is(
+ numIceConnections,
+ 2,
+ "stats report exactly 2 ICE connections for media and RTCP"
+ );
+ }
+ } else {
+ var numAudioTransceivers = this._pc
+ .getTransceivers()
+ .filter(transceiver => {
+ return (
+ !transceiver.stopped && transceiver.receiver.track.kind == "audio"
+ );
+ }).length;
+
+ var numVideoTransceivers = this._pc
+ .getTransceivers()
+ .filter(transceiver => {
+ return (
+ !transceiver.stopped && transceiver.receiver.track.kind == "video"
+ );
+ }).length;
+
+ var numExpectedTransports = numAudioTransceivers + numVideoTransceivers;
+ if (!testOptions.rtcpmux) {
+ numExpectedTransports *= 2;
+ }
+
+ if (this.dataChannels.length) {
+ ++numExpectedTransports;
+ }
+
+ info(
+ "expected audio + video + data transports: " + numExpectedTransports
+ );
+ is(
+ numIceConnections,
+ numExpectedTransports,
+ "stats ICE connections matches expected A/V transports"
+ );
+ }
+ },
+
+ expectNegotiationNeeded() {
+ if (!this.observedNegotiationNeeded) {
+ this.observedNegotiationNeeded = new Promise(resolve => {
+ this.onnegotiationneeded = resolve;
+ });
+ }
+ },
+
+ /**
+ * Property-matching function for finding a certain stat in passed-in stats
+ *
+ * @param {object} stats
+ * The stats to check from this PeerConnectionWrapper
+ * @param {object} props
+ * The properties to look for
+ * @returns {boolean} Whether an entry containing all match-props was found.
+ */
+ hasStat(stats, props) {
+ for (let res of stats.values()) {
+ var match = true;
+ for (let prop in props) {
+ if (res[prop] !== props[prop]) {
+ match = false;
+ break;
+ }
+ }
+ if (match) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Closes the connection
+ */
+ close() {
+ this._pc.close();
+ this.localMediaElements.forEach(e => e.pause());
+ info(this + ": Closed connection.");
+ },
+
+ /**
+ * Returns the string representation of the class
+ *
+ * @returns {String} The string representation
+ */
+ toString() {
+ return "PeerConnectionWrapper (" + this.label + ")";
+ },
+};
+
+// haxx to prevent SimpleTest from failing at window.onload
+function addLoadEvent() {}
+
+function loadScript(...scripts) {
+ return Promise.all(
+ scripts.map(script => {
+ var el = document.createElement("script");
+ if (typeof scriptRelativePath === "string" && script.charAt(0) !== "/") {
+ script = scriptRelativePath + script;
+ }
+ el.src = script;
+ document.head.appendChild(el);
+ return new Promise(r => {
+ el.onload = r;
+ el.onerror = r;
+ });
+ })
+ );
+}
+
+// Ensure SimpleTest.js is loaded before other scripts.
+/* import-globals-from /testing/mochitest/tests/SimpleTest/SimpleTest.js */
+/* import-globals-from head.js */
+/* import-globals-from templates.js */
+/* import-globals-from turnConfig.js */
+/* import-globals-from dataChannel.js */
+/* import-globals-from network.js */
+/* import-globals-from sdpUtils.js */
+
+var scriptsReady = loadScript("/tests/SimpleTest/SimpleTest.js").then(() => {
+ return loadScript(
+ "head.js",
+ "templates.js",
+ "turnConfig.js",
+ "dataChannel.js",
+ "network.js",
+ "sdpUtils.js"
+ );
+});
+
+function createHTML(options) {
+ return scriptsReady.then(() => realCreateHTML(options));
+}
+
+var iceServerWebsocket;
+var iceServersArray = [];
+
+var addTurnsSelfsignedCerts = () => {
+ var gUrl = SimpleTest.getTestFileURL("addTurnsSelfsignedCert.js");
+ var gScript = SpecialPowers.loadChromeScript(gUrl);
+ var certs = [];
+ // If the ICE server is running TURNS, and includes a "cert" attribute in
+ // its JSON, we set up an override that will forgive things like
+ // self-signed for it.
+ iceServersArray.forEach(iceServer => {
+ if (iceServer.hasOwnProperty("cert")) {
+ iceServer.urls.forEach(url => {
+ if (url.startsWith("turns:")) {
+ // Assumes no port or params!
+ certs.push({ cert: iceServer.cert, hostname: url.substr(6) });
+ }
+ });
+ }
+ });
+
+ return new Promise((resolve, reject) => {
+ gScript.addMessageListener("certs-added", () => {
+ resolve();
+ });
+
+ gScript.sendAsyncMessage("add-turns-certs", certs);
+ });
+};
+
+var setupIceServerConfig = useIceServer => {
+ // We disable ICE support for HTTP proxy when using a TURN server, because
+ // mochitest uses a fake HTTP proxy to serve content, which will eat our STUN
+ // packets for TURN TCP.
+ var enableHttpProxy = enable =>
+ SpecialPowers.pushPrefEnv({
+ set: [["media.peerconnection.disable_http_proxy", !enable]],
+ });
+
+ var spawnIceServer = () =>
+ new Promise((resolve, reject) => {
+ iceServerWebsocket = new WebSocket("ws://localhost:8191/");
+ iceServerWebsocket.onopen = event => {
+ info("websocket/process bridge open, starting ICE Server...");
+ iceServerWebsocket.send("iceserver");
+ };
+
+ iceServerWebsocket.onmessage = event => {
+ // The first message will contain the iceServers configuration, subsequent
+ // messages are just logging.
+ info("ICE Server: " + event.data);
+ resolve(event.data);
+ };
+
+ iceServerWebsocket.onerror = () => {
+ reject("ICE Server error: Is the ICE server websocket up?");
+ };
+
+ iceServerWebsocket.onclose = () => {
+ info("ICE Server websocket closed");
+ reject("ICE Server gone before getting configuration");
+ };
+ });
+
+ if (!useIceServer) {
+ info("Skipping ICE Server for this test");
+ return enableHttpProxy(true);
+ }
+
+ return enableHttpProxy(false)
+ .then(spawnIceServer)
+ .then(iceServersStr => {
+ iceServersArray = JSON.parse(iceServersStr);
+ })
+ .then(addTurnsSelfsignedCerts);
+};
+
+async function runNetworkTest(testFunction, fixtureOptions = {}) {
+ let { AppConstants } = SpecialPowers.ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+ );
+
+ await scriptsReady;
+ await runTestWhenReady(async options => {
+ await startNetworkAndTest();
+ await setupIceServerConfig(fixtureOptions.useIceServer);
+ await testFunction(options);
+ await networkTestFinished();
+ });
+}
diff --git a/dom/media/webrtc/tests/mochitests/peerconnection_audio_forced_sample_rate.js b/dom/media/webrtc/tests/mochitests/peerconnection_audio_forced_sample_rate.js
new file mode 100644
index 0000000000..d0c647be0d
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/peerconnection_audio_forced_sample_rate.js
@@ -0,0 +1,32 @@
+// This function takes a sample-rate, and tests that audio flows correctly when
+// the sampling-rate at which the MTG runs is not one of the sampling-rates that
+// the MediaPipeline can work with.
+// It is in a separate file because we have an MTG per document, and we want to
+// test multiple sample-rates, so we include it in multiple HTML mochitest
+// files.
+async function test_peerconnection_audio_forced_sample_rate(forcedSampleRate) {
+ await scriptsReady;
+ await pushPrefs(["media.cubeb.force_sample_rate", forcedSampleRate]);
+ await runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ const ac = new AudioContext();
+ test.setMediaConstraints([{ audio: true }], []);
+ test.chain.replace("PC_LOCAL_GUM", [
+ function PC_LOCAL_WEBAUDIO_SOURCE(test) {
+ const oscillator = ac.createOscillator();
+ oscillator.type = "sine";
+ oscillator.frequency.value = 700;
+ oscillator.start();
+ const dest = ac.createMediaStreamDestination();
+ oscillator.connect(dest);
+ test.pcLocal.attachLocalStream(dest.stream);
+ },
+ ]);
+ test.chain.append([
+ function CHECK_REMOTE_AUDIO_FLOW(test) {
+ return test.pcRemote.checkReceivingToneFrom(ac, test.pcLocal);
+ },
+ ]);
+ return test.run();
+ });
+}
diff --git a/dom/media/webrtc/tests/mochitests/sdpUtils.js b/dom/media/webrtc/tests/mochitests/sdpUtils.js
new file mode 100644
index 0000000000..51cae10dba
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/sdpUtils.js
@@ -0,0 +1,398 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var sdputils = {
+ // Finds the codec id / payload type given a codec format
+ // (e.g., "VP8", "VP9/90000"). `offset` tells us which one to use in case of
+ // multiple matches.
+ findCodecId(sdp, format, offset = 0) {
+ let regex = new RegExp("rtpmap:([0-9]+) " + format, "gi");
+ let match;
+ for (let i = 0; i <= offset; ++i) {
+ match = regex.exec(sdp);
+ if (!match) {
+ throw new Error(
+ "Couldn't find offset " +
+ i +
+ " of codec " +
+ format +
+ " while looking for offset " +
+ offset +
+ " in sdp:\n" +
+ sdp
+ );
+ }
+ }
+ // match[0] is the full matched string
+ // match[1] is the first parenthesis group
+ return match[1];
+ },
+
+ // Returns a list of all payload types, excluding rtx, in an sdp.
+ getPayloadTypes(sdp) {
+ const regex = /^a=rtpmap:([0-9]+) (?:(?!rtx).)*$/gim;
+ const pts = [];
+ for (const [line, pt] of sdp.matchAll(regex)) {
+ pts.push(pt);
+ }
+ return pts;
+ },
+
+ // Finds all the extmap ids in the given sdp. Note that this does NOT
+ // consider m-sections, so a more generic version would need to
+ // look at each m-section separately.
+ findExtmapIds(sdp) {
+ var sdpExtmapIds = [];
+ extmapRegEx = /^a=extmap:([0-9+])/gm;
+ // must call exec on the regex to get each match in the string
+ while ((searchResults = extmapRegEx.exec(sdp)) !== null) {
+ // returned array has the matched text as the first item,
+ // and then one item for each capturing parenthesis that
+ // matched containing the text that was captured.
+ sdpExtmapIds.push(searchResults[1]);
+ }
+ return sdpExtmapIds;
+ },
+
+ findExtmapIdsUrnsDirections(sdp) {
+ var sdpExtmap = [];
+ extmapRegEx = /^a=extmap:([0-9+])([A-Za-z/]*) ([A-Za-z0-9_:\-\/\.]+)/gm;
+ // must call exec on the regex to get each match in the string
+ while ((searchResults = extmapRegEx.exec(sdp)) !== null) {
+ // returned array has the matched text as the first item,
+ // and then one item for each capturing parenthesis that
+ // matched containing the text that was captured.
+ var idUrn = [];
+ idUrn.push(searchResults[1]);
+ idUrn.push(searchResults[3]);
+ idUrn.push(searchResults[2].slice(1));
+ sdpExtmap.push(idUrn);
+ }
+ return sdpExtmap;
+ },
+
+ verify_unique_extmap_ids(sdp) {
+ const sdpExtmapIds = sdputils.findExtmapIdsUrnsDirections(sdp);
+
+ return sdpExtmapIds.reduce(function (result, item, index) {
+ const [id, urn, dir] = item;
+ ok(
+ !(id in result) || (result[id][0] === urn && result[id][1] === dir),
+ "ID " + id + " is unique ID for " + urn + " and direction " + dir
+ );
+ result[id] = [urn, dir];
+ return result;
+ }, {});
+ },
+
+ getMSections(sdp) {
+ return sdp
+ .split(new RegExp("^m=", "gm"))
+ .slice(1)
+ .map(s => "m=" + s);
+ },
+
+ getAudioMSections(sdp) {
+ return this.getMSections(sdp).filter(section =>
+ section.startsWith("m=audio")
+ );
+ },
+
+ getVideoMSections(sdp) {
+ return this.getMSections(sdp).filter(section =>
+ section.startsWith("m=video")
+ );
+ },
+
+ checkSdpAfterEndOfTrickle(description, testOptions, label) {
+ info("EOC-SDP: " + JSON.stringify(description));
+
+ const checkForTransportAttributes = msection => {
+ info("Checking msection: " + msection);
+ ok(
+ msection.includes("a=end-of-candidates"),
+ label + ": SDP contains end-of-candidates"
+ );
+
+ if (!msection.startsWith("m=application")) {
+ if (testOptions.rtcpmux) {
+ ok(
+ msection.includes("a=rtcp-mux"),
+ label + ": SDP contains rtcp-mux"
+ );
+ } else {
+ ok(msection.includes("a=rtcp:"), label + ": SDP contains rtcp port");
+ }
+ }
+ };
+
+ const hasOwnTransport = msection => {
+ const port0Check = new RegExp(/^m=\S+ 0 /).exec(msection);
+ if (port0Check) {
+ return false;
+ }
+ const midMatch = new RegExp(/\r\na=mid:(\S+)/).exec(msection);
+ if (!midMatch) {
+ return true;
+ }
+ const mid = midMatch[1];
+ const bundleGroupMatch = new RegExp(
+ "\\r\\na=group:BUNDLE \\S.* " + mid + "\\s+"
+ ).exec(description.sdp);
+ return bundleGroupMatch == null;
+ };
+
+ const msectionsWithOwnTransports = this.getMSections(
+ description.sdp
+ ).filter(hasOwnTransport);
+
+ ok(
+ msectionsWithOwnTransports.length,
+ "SDP should contain at least one msection with a transport"
+ );
+ msectionsWithOwnTransports.forEach(checkForTransportAttributes);
+
+ if (testOptions.ssrc) {
+ ok(description.sdp.includes("a=ssrc"), label + ": SDP contains a=ssrc");
+ } else {
+ ok(
+ !description.sdp.includes("a=ssrc"),
+ label + ": SDP does not contain a=ssrc"
+ );
+ }
+ },
+
+ // Note, we don't bother removing the fmtp lines, which makes a good test
+ // for some SDP parsing issues.
+ removeCodec(sdp, codec) {
+ var updated_sdp = sdp.replace(
+ new RegExp("a=rtpmap:" + codec + ".*\\/90000\\r\\n", ""),
+ ""
+ );
+ updated_sdp = updated_sdp.replace(
+ new RegExp("(RTP\\/SAVPF.*)( " + codec + ")(.*\\r\\n)", ""),
+ "$1$3"
+ );
+ updated_sdp = updated_sdp.replace(
+ new RegExp("a=rtcp-fb:" + codec + " nack\\r\\n", ""),
+ ""
+ );
+ updated_sdp = updated_sdp.replace(
+ new RegExp("a=rtcp-fb:" + codec + " nack pli\\r\\n", ""),
+ ""
+ );
+ updated_sdp = updated_sdp.replace(
+ new RegExp("a=rtcp-fb:" + codec + " ccm fir\\r\\n", ""),
+ ""
+ );
+ return updated_sdp;
+ },
+
+ removeAllButPayloadType(sdp, pt) {
+ return sdp.replace(
+ new RegExp("m=(\\w+ \\w+) UDP/TLS/RTP/SAVPF .*" + pt + ".*\\r\\n", "gi"),
+ "m=$1 UDP/TLS/RTP/SAVPF " + pt + "\r\n"
+ );
+ },
+
+ removeRtpMapForPayloadType(sdp, pt) {
+ return sdp.replace(new RegExp("a=rtpmap:" + pt + ".*\\r\\n", "gi"), "");
+ },
+
+ removeRtcpMux(sdp) {
+ return sdp.replace(/a=rtcp-mux\r\n/g, "");
+ },
+
+ removeSSRCs(sdp) {
+ return sdp.replace(/a=ssrc.*\r\n/g, "");
+ },
+
+ removeBundle(sdp) {
+ return sdp.replace(/a=group:BUNDLE .*\r\n/g, "");
+ },
+
+ reduceAudioMLineToPcmuPcma(sdp) {
+ return sdp.replace(
+ /m=audio .*\r\n/g,
+ "m=audio 9 UDP/TLS/RTP/SAVPF 0 8\r\n"
+ );
+ },
+
+ setAllMsectionsInactive(sdp) {
+ return sdp
+ .replace(/\r\na=sendrecv/g, "\r\na=inactive")
+ .replace(/\r\na=sendonly/g, "\r\na=inactive")
+ .replace(/\r\na=recvonly/g, "\r\na=inactive");
+ },
+
+ removeAllRtpMaps(sdp) {
+ return sdp.replace(/a=rtpmap:.*\r\n/g, "");
+ },
+
+ reduceAudioMLineToDynamicPtAndOpus(sdp) {
+ return sdp.replace(
+ /m=audio .*\r\n/g,
+ "m=audio 9 UDP/TLS/RTP/SAVPF 101 109\r\n"
+ );
+ },
+
+ addTiasBps(sdp, bps) {
+ return sdp.replace(/c=IN (.*)\r\n/g, "c=IN $1\r\nb=TIAS:" + bps + "\r\n");
+ },
+
+ removeSimulcastProperties(sdp) {
+ return sdp
+ .replace(/a=simulcast:.*\r\n/g, "")
+ .replace(/a=rid:.*\r\n/g, "")
+ .replace(
+ /a=extmap:[^\s]* urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id.*\r\n/g,
+ ""
+ )
+ .replace(
+ /a=extmap:[^\s]* urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id.*\r\n/g,
+ ""
+ );
+ },
+
+ transferSimulcastProperties(offer_sdp, answer_sdp) {
+ if (!offer_sdp.includes("a=simulcast:")) {
+ return answer_sdp;
+ }
+ ok(
+ offer_sdp.includes("a=simulcast:send "),
+ "Offer contains simulcast attribute"
+ );
+ var o_simul = offer_sdp.match(/simulcast:send (.*)([\n$])*/i);
+ var new_answer_sdp = answer_sdp + "a=simulcast:recv " + o_simul[1] + "\r\n";
+ ok(offer_sdp.includes("a=rid:"), "Offer contains RID attribute");
+ var o_rids = offer_sdp.match(/a=rid:(.*)/gi);
+ o_rids.forEach(o_rid => {
+ new_answer_sdp = new_answer_sdp + o_rid.replace(/send/, "recv") + "\r\n";
+ });
+ var extmap_id = offer_sdp.match(
+ "a=extmap:([0-9+])/sendonly urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id"
+ );
+ ok(extmap_id != null, "Offer contains RID RTP header extension");
+ new_answer_sdp =
+ new_answer_sdp +
+ "a=extmap:" +
+ extmap_id[1] +
+ "/recvonly urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\n";
+ var extmap_id = offer_sdp.match(
+ "a=extmap:([0-9+])/sendonly urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id"
+ );
+ if (extmap_id != null) {
+ new_answer_sdp =
+ new_answer_sdp +
+ "a=extmap:" +
+ extmap_id[1] +
+ "/recvonly urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\n";
+ }
+
+ return new_answer_sdp;
+ },
+
+ verifySdp(
+ desc,
+ expectedType,
+ offerConstraintsList,
+ offerOptions,
+ testOptions
+ ) {
+ info("Examining this SessionDescription: " + JSON.stringify(desc));
+ info("offerConstraintsList: " + JSON.stringify(offerConstraintsList));
+ info("offerOptions: " + JSON.stringify(offerOptions));
+ ok(desc, "SessionDescription is not null");
+ is(desc.type, expectedType, "SessionDescription type is " + expectedType);
+ ok(desc.sdp.length > 10, "SessionDescription body length is plausible");
+ ok(desc.sdp.includes("a=ice-ufrag"), "ICE username is present in SDP");
+ ok(desc.sdp.includes("a=ice-pwd"), "ICE password is present in SDP");
+ ok(desc.sdp.includes("a=fingerprint"), "ICE fingerprint is present in SDP");
+ //TODO: update this for loopback support bug 1027350
+ ok(
+ !desc.sdp.includes(LOOPBACK_ADDR),
+ "loopback interface is absent from SDP"
+ );
+ var requiresTrickleIce = !desc.sdp.includes("a=candidate");
+ if (requiresTrickleIce) {
+ info("No ICE candidate in SDP -> requiring trickle ICE");
+ } else {
+ info("at least one ICE candidate is present in SDP");
+ }
+
+ //TODO: how can we check for absence/presence of m=application?
+
+ var audioTracks =
+ sdputils.countTracksInConstraint("audio", offerConstraintsList) ||
+ (offerOptions && offerOptions.offerToReceiveAudio ? 1 : 0);
+
+ info("expected audio tracks: " + audioTracks);
+ if (audioTracks == 0) {
+ ok(!desc.sdp.includes("m=audio"), "audio m-line is absent from SDP");
+ } else {
+ ok(desc.sdp.includes("m=audio"), "audio m-line is present in SDP");
+ is(
+ testOptions.opus,
+ desc.sdp.includes("a=rtpmap:109 opus/48000/2"),
+ "OPUS codec is present in SDP"
+ );
+ //TODO: ideally the rtcp-mux should be for the m=audio, and not just
+ // anywhere in the SDP (JS SDP parser bug 1045429)
+ is(
+ testOptions.rtcpmux,
+ desc.sdp.includes("a=rtcp-mux"),
+ "RTCP Mux is offered in SDP"
+ );
+ }
+
+ var videoTracks =
+ sdputils.countTracksInConstraint("video", offerConstraintsList) ||
+ (offerOptions && offerOptions.offerToReceiveVideo ? 1 : 0);
+
+ info("expected video tracks: " + videoTracks);
+ if (videoTracks == 0) {
+ ok(!desc.sdp.includes("m=video"), "video m-line is absent from SDP");
+ } else {
+ ok(desc.sdp.includes("m=video"), "video m-line is present in SDP");
+ if (testOptions.h264) {
+ ok(
+ desc.sdp.includes("a=rtpmap:126 H264/90000") ||
+ desc.sdp.includes("a=rtpmap:97 H264/90000"),
+ "H.264 codec is present in SDP"
+ );
+ } else {
+ ok(
+ desc.sdp.includes("a=rtpmap:120 VP8/90000") ||
+ desc.sdp.includes("a=rtpmap:121 VP9/90000"),
+ "VP8 or VP9 codec is present in SDP"
+ );
+ }
+ is(
+ testOptions.rtcpmux,
+ desc.sdp.includes("a=rtcp-mux"),
+ "RTCP Mux is offered in SDP"
+ );
+ is(
+ testOptions.ssrc,
+ desc.sdp.includes("a=ssrc"),
+ "a=ssrc signaled in SDP"
+ );
+ }
+
+ return requiresTrickleIce;
+ },
+
+ /**
+ * Counts the amount of audio tracks in a given media constraint.
+ *
+ * @param constraints
+ * The contraint to be examined.
+ */
+ countTracksInConstraint(type, constraints) {
+ if (!Array.isArray(constraints)) {
+ return 0;
+ }
+ return constraints.reduce((sum, c) => sum + (c[type] ? 1 : 0), 0);
+ },
+};
diff --git a/dom/media/webrtc/tests/mochitests/simulcast.js b/dom/media/webrtc/tests/mochitests/simulcast.js
new file mode 100644
index 0000000000..0af36478c4
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/simulcast.js
@@ -0,0 +1,232 @@
+"use strict";
+/* Helper functions to munge SDP and split the sending track into
+ * separate tracks on the receiving end. This can be done in a number
+ * of ways, the one used here uses the fact that the MID and RID header
+ * extensions which are used for packet routing share the same wire
+ * format. The receiver interprets the rids from the sender as mids
+ * which allows receiving the different spatial resolutions on separate
+ * m-lines and tracks.
+ */
+
+// Borrowed from wpt, with some dependencies removed.
+
+const ridExtensions = [
+ "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id",
+ "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id",
+];
+
+function ridToMid(description, rids) {
+ const sections = SDPUtils.splitSections(description.sdp);
+ const dtls = SDPUtils.getDtlsParameters(sections[1], sections[0]);
+ const ice = SDPUtils.getIceParameters(sections[1], sections[0]);
+ const rtpParameters = SDPUtils.parseRtpParameters(sections[1]);
+ const setupValue = description.sdp.match(/a=setup:(.*)/)[1];
+ const directionValue =
+ description.sdp.match(/a=sendrecv|a=sendonly|a=recvonly|a=inactive/) ||
+ "a=sendrecv";
+ const mline = SDPUtils.parseMLine(sections[1]);
+
+ // Skip mid extension; we are replacing it with the rid extmap
+ rtpParameters.headerExtensions = rtpParameters.headerExtensions.filter(
+ ext => ext.uri != "urn:ietf:params:rtp-hdrext:sdes:mid"
+ );
+
+ for (const ext of rtpParameters.headerExtensions) {
+ if (ext.uri == "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id") {
+ ext.uri = "urn:ietf:params:rtp-hdrext:sdes:mid";
+ }
+ }
+
+ // Filter rtx as we have no way to (re)interpret rrid.
+ // Not doing this makes probing use RTX, it's not understood and ramp-up is slower.
+ rtpParameters.codecs = rtpParameters.codecs.filter(
+ c => c.name.toUpperCase() !== "RTX"
+ );
+
+ if (!rids) {
+ rids = Array.from(description.sdp.matchAll(/a=rid:(.*) send/g)).map(
+ r => r[1]
+ );
+ }
+
+ let sdp =
+ SDPUtils.writeSessionBoilerplate() +
+ SDPUtils.writeDtlsParameters(dtls, setupValue) +
+ SDPUtils.writeIceParameters(ice) +
+ "a=group:BUNDLE " +
+ rids.join(" ") +
+ "\r\n";
+ const baseRtpDescription = SDPUtils.writeRtpDescription(
+ mline.kind,
+ rtpParameters
+ );
+ for (const rid of rids) {
+ sdp +=
+ baseRtpDescription +
+ "a=mid:" +
+ rid +
+ "\r\n" +
+ "a=msid:rid-" +
+ rid +
+ " rid-" +
+ rid +
+ "\r\n";
+ sdp += directionValue + "\r\n";
+ }
+ return sdp;
+}
+
+function midToRid(description, localDescription, rids) {
+ const sections = SDPUtils.splitSections(description.sdp);
+ const dtls = SDPUtils.getDtlsParameters(sections[1], sections[0]);
+ const ice = SDPUtils.getIceParameters(sections[1], sections[0]);
+ const rtpParameters = SDPUtils.parseRtpParameters(sections[1]);
+ const setupValue = description.sdp.match(/a=setup:(.*)/)[1];
+ const directionValue =
+ description.sdp.match(/a=sendrecv|a=sendonly|a=recvonly|a=inactive/) ||
+ "a=sendrecv";
+ const mline = SDPUtils.parseMLine(sections[1]);
+
+ // Skip rid extensions; we are replacing them with the mid extmap
+ rtpParameters.headerExtensions = rtpParameters.headerExtensions.filter(
+ ext => !ridExtensions.includes(ext.uri)
+ );
+
+ for (const ext of rtpParameters.headerExtensions) {
+ if (ext.uri == "urn:ietf:params:rtp-hdrext:sdes:mid") {
+ ext.uri = "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id";
+ }
+ }
+
+ const localMid = localDescription
+ ? SDPUtils.getMid(SDPUtils.splitSections(localDescription.sdp)[1])
+ : "0";
+
+ if (!rids) {
+ rids = [];
+ for (let i = 1; i < sections.length; i++) {
+ rids.push(SDPUtils.getMid(sections[i]));
+ }
+ }
+
+ let sdp =
+ SDPUtils.writeSessionBoilerplate() +
+ SDPUtils.writeDtlsParameters(dtls, setupValue) +
+ SDPUtils.writeIceParameters(ice) +
+ "a=group:BUNDLE " +
+ localMid +
+ "\r\n";
+ sdp += SDPUtils.writeRtpDescription(mline.kind, rtpParameters);
+ // Although we are converting mids to rids, we still need a mid.
+ // The first one will be consistent with trickle ICE candidates.
+ sdp += "a=mid:" + localMid + "\r\n";
+ sdp += directionValue + "\r\n";
+
+ for (const rid of rids) {
+ const stringrid = String(rid); // allow integers
+ const choices = stringrid.split(",");
+ choices.forEach(choice => {
+ sdp += "a=rid:" + choice + " recv\r\n";
+ });
+ }
+ if (rids.length) {
+ sdp += "a=simulcast:recv " + rids.join(";") + "\r\n";
+ }
+
+ return sdp;
+}
+
+async function doOfferToSendSimulcast(offerer, answerer) {
+ await offerer.setLocalDescription();
+
+ // Is this a renegotiation? If so, we cannot remove (or reorder!) any mids,
+ // even if some rids have been removed or reordered.
+ let mids = [];
+ if (answerer.localDescription) {
+ // Renegotiation. Mids must be the same as before, because renegotiation
+ // can never remove or reorder mids, nor can it expand the simulcast
+ // envelope.
+ mids = [...answerer.localDescription.sdp.matchAll(/a=mid:(.*)/g)].map(
+ e => e[1]
+ );
+ } else {
+ // First negotiation; the mids will be exactly the same as the rids
+ const simulcastAttr = offerer.localDescription.sdp.match(
+ /a=simulcast:send (.*)/
+ );
+ if (simulcastAttr) {
+ mids = simulcastAttr[1].split(";");
+ }
+ }
+
+ const nonSimulcastOffer = ridToMid(offerer.localDescription, mids);
+ await answerer.setRemoteDescription({
+ type: "offer",
+ sdp: nonSimulcastOffer,
+ });
+}
+
+async function doAnswerToRecvSimulcast(offerer, answerer, rids) {
+ await answerer.setLocalDescription();
+ const simulcastAnswer = midToRid(
+ answerer.localDescription,
+ offerer.localDescription,
+ rids
+ );
+ await offerer.setRemoteDescription({ type: "answer", sdp: simulcastAnswer });
+}
+
+async function doOfferToRecvSimulcast(offerer, answerer, rids) {
+ await offerer.setLocalDescription();
+ const simulcastOffer = midToRid(
+ offerer.localDescription,
+ answerer.localDescription,
+ rids
+ );
+ await answerer.setRemoteDescription({ type: "offer", sdp: simulcastOffer });
+}
+
+async function doAnswerToSendSimulcast(offerer, answerer) {
+ await answerer.setLocalDescription();
+
+ // See which mids the offerer had; it will barf if we remove or reorder them
+ const mids = [...offerer.localDescription.sdp.matchAll(/a=mid:(.*)/g)].map(
+ e => e[1]
+ );
+
+ const nonSimulcastAnswer = ridToMid(answerer.localDescription, mids);
+ await offerer.setRemoteDescription({
+ type: "answer",
+ sdp: nonSimulcastAnswer,
+ });
+}
+
+async function doOfferToSendSimulcastAndAnswer(offerer, answerer, rids) {
+ await doOfferToSendSimulcast(offerer, answerer);
+ await doAnswerToRecvSimulcast(offerer, answerer, rids);
+}
+
+async function doOfferToRecvSimulcastAndAnswer(offerer, answerer, rids) {
+ await doOfferToRecvSimulcast(offerer, answerer, rids);
+ await doAnswerToSendSimulcast(offerer, answerer);
+}
+
+// This would be useful for cases other than simulcast, but we do not use it
+// anywhere else right now, nor do we have a place for wpt-friendly helpers at
+// the moment.
+function createPlaybackElement(track) {
+ const elem = document.createElement(track.kind);
+ elem.autoplay = true;
+ elem.srcObject = new MediaStream([track]);
+ elem.id = track.id;
+ return elem;
+}
+
+async function getPlaybackWithLoadedMetadata(track) {
+ const elem = createPlaybackElement(track);
+ return new Promise(resolve => {
+ elem.addEventListener("loadedmetadata", () => {
+ resolve(elem);
+ });
+ });
+}
diff --git a/dom/media/webrtc/tests/mochitests/stats.js b/dom/media/webrtc/tests/mochitests/stats.js
new file mode 100644
index 0000000000..475f8eeca9
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/stats.js
@@ -0,0 +1,1596 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const statsExpectedByType = {
+ "inbound-rtp": {
+ expected: [
+ "trackIdentifier",
+ "id",
+ "timestamp",
+ "type",
+ "ssrc",
+ "mediaType",
+ "kind",
+ "codecId",
+ "packetsReceived",
+ "packetsLost",
+ "packetsDiscarded",
+ "bytesReceived",
+ "jitter",
+ "lastPacketReceivedTimestamp",
+ "headerBytesReceived",
+ // Always missing from libwebrtc stats
+ // "estimatedPlayoutTimestamp",
+ "jitterBufferDelay",
+ "jitterBufferEmittedCount",
+ ],
+ optional: ["remoteId", "nackCount", "qpSum"],
+ localVideoOnly: [
+ "firCount",
+ "pliCount",
+ "framesDecoded",
+ "framesDropped",
+ "discardedPackets",
+ "framesPerSecond",
+ "frameWidth",
+ "frameHeight",
+ "framesReceived",
+ "totalDecodeTime",
+ "totalInterFrameDelay",
+ "totalProcessingDelay",
+ "totalSquaredInterFrameDelay",
+ ],
+ localAudioOnly: [
+ "totalSamplesReceived",
+ // libwebrtc doesn't seem to do FEC for video
+ "fecPacketsReceived",
+ "fecPacketsDiscarded",
+ "concealedSamples",
+ "silentConcealedSamples",
+ "concealmentEvents",
+ "insertedSamplesForDeceleration",
+ "removedSamplesForAcceleration",
+ "audioLevel",
+ "totalAudioEnergy",
+ "totalSamplesDuration",
+ ],
+ unimplemented: [
+ "mediaTrackId",
+ "transportId",
+ "associateStatsId",
+ "sliCount",
+ "packetsRepaired",
+ "fractionLost",
+ "burstPacketsLost",
+ "burstLossCount",
+ "burstDiscardCount",
+ "gapDiscardRate",
+ "gapLossRate",
+ ],
+ deprecated: ["mozRtt", "isRemote"],
+ },
+ "outbound-rtp": {
+ expected: [
+ "id",
+ "timestamp",
+ "type",
+ "ssrc",
+ "mediaType",
+ "kind",
+ "codecId",
+ "packetsSent",
+ "bytesSent",
+ "remoteId",
+ "headerBytesSent",
+ "retransmittedPacketsSent",
+ "retransmittedBytesSent",
+ ],
+ optional: ["nackCount", "qpSum"],
+ localAudioOnly: [],
+ localVideoOnly: [
+ "framesEncoded",
+ "firCount",
+ "pliCount",
+ "frameWidth",
+ "frameHeight",
+ "framesSent",
+ "hugeFramesSent",
+ "totalEncodeTime",
+ "totalEncodedBytesTarget",
+ ],
+ unimplemented: ["mediaTrackId", "transportId", "sliCount", "targetBitrate"],
+ deprecated: ["isRemote"],
+ },
+ "remote-inbound-rtp": {
+ expected: [
+ "id",
+ "timestamp",
+ "type",
+ "ssrc",
+ "mediaType",
+ "kind",
+ "codecId",
+ "packetsLost",
+ "jitter",
+ "localId",
+ "totalRoundTripTime",
+ "fractionLost",
+ "roundTripTimeMeasurements",
+ ],
+ optional: ["roundTripTime", "nackCount", "packetsReceived"],
+ unimplemented: [
+ "mediaTrackId",
+ "transportId",
+ "packetsDiscarded",
+ "associateStatsId",
+ "sliCount",
+ "packetsRepaired",
+ "burstPacketsLost",
+ "burstLossCount",
+ "burstDiscardCount",
+ "gapDiscardRate",
+ "gapLossRate",
+ ],
+ deprecated: ["mozRtt", "isRemote"],
+ },
+ "remote-outbound-rtp": {
+ expected: [
+ "id",
+ "timestamp",
+ "type",
+ "ssrc",
+ "mediaType",
+ "kind",
+ "codecId",
+ "packetsSent",
+ "bytesSent",
+ "localId",
+ "remoteTimestamp",
+ ],
+ optional: ["nackCount"],
+ unimplemented: ["mediaTrackId", "transportId", "sliCount", "targetBitrate"],
+ deprecated: ["isRemote"],
+ },
+ "media-source": {
+ expected: ["id", "timestamp", "type", "trackIdentifier", "kind"],
+ unimplemented: [
+ "audioLevel",
+ "totalAudioEnergy",
+ "totalSamplesDuration",
+ "echoReturnLoss",
+ "echoReturnLossEnhancement",
+ "droppedSamplesDuration",
+ "droppedSamplesEvents",
+ "totalCaptureDelay",
+ "totalSamplesCaptured",
+ "width",
+ "height",
+ "frames",
+ "framesPerSecond",
+ ],
+ optional: [],
+ deprecated: [],
+ },
+ csrc: { skip: true },
+ codec: {
+ expected: [
+ "timestamp",
+ "type",
+ "id",
+ "payloadType",
+ "transportId",
+ "mimeType",
+ "clockRate",
+ "sdpFmtpLine",
+ ],
+ optional: ["codecType", "channels"],
+ unimplemented: [],
+ deprecated: [],
+ },
+ "peer-connection": { skip: true },
+ "data-channel": { skip: true },
+ track: { skip: true },
+ transport: { skip: true },
+ "candidate-pair": {
+ expected: [
+ "id",
+ "timestamp",
+ "type",
+ "transportId",
+ "localCandidateId",
+ "remoteCandidateId",
+ "state",
+ "priority",
+ "nominated",
+ "writable",
+ "readable",
+ "bytesSent",
+ "bytesReceived",
+ "lastPacketSentTimestamp",
+ "lastPacketReceivedTimestamp",
+ ],
+ optional: ["selected"],
+ unimplemented: [
+ "totalRoundTripTime",
+ "currentRoundTripTime",
+ "availableOutgoingBitrate",
+ "availableIncomingBitrate",
+ "requestsReceived",
+ "requestsSent",
+ "responsesReceived",
+ "responsesSent",
+ "retransmissionsReceived",
+ "retransmissionsSent",
+ "consentRequestsSent",
+ ],
+ deprecated: [],
+ },
+ "local-candidate": {
+ expected: [
+ "id",
+ "timestamp",
+ "type",
+ "address",
+ "protocol",
+ "port",
+ "candidateType",
+ "priority",
+ ],
+ optional: ["relayProtocol", "proxied"],
+ unimplemented: ["networkType", "url", "transportId"],
+ deprecated: [
+ "candidateId",
+ "portNumber",
+ "ipAddress",
+ "componentId",
+ "mozLocalTransport",
+ "transport",
+ ],
+ },
+ "remote-candidate": {
+ expected: [
+ "id",
+ "timestamp",
+ "type",
+ "address",
+ "protocol",
+ "port",
+ "candidateType",
+ "priority",
+ ],
+ optional: ["relayProtocol", "proxied"],
+ unimplemented: ["networkType", "url", "transportId"],
+ deprecated: [
+ "candidateId",
+ "portNumber",
+ "ipAddress",
+ "componentId",
+ "mozLocalTransport",
+ "transport",
+ ],
+ },
+ certificate: { skip: true },
+};
+
+["in", "out"].forEach(pre => {
+ let s = statsExpectedByType[pre + "bound-rtp"];
+ s.optional = [...s.optional, ...s.localVideoOnly, ...s.localAudioOnly];
+});
+
+//
+// Checks that the fields in a report conform to the expectations in
+// statExpectedByType
+//
+function checkExpectedFields(report) {
+ report.forEach(stat => {
+ let expectations = statsExpectedByType[stat.type];
+ ok(expectations, "Stats type " + stat.type + " was expected");
+ // If the type is not expected or if it is flagged for skipping continue to
+ // the next
+ if (!expectations || expectations.skip) {
+ return;
+ }
+ // Check that all required fields exist
+ expectations.expected.forEach(field => {
+ ok(
+ field in stat,
+ "Expected stat field " + stat.type + "." + field + " exists"
+ );
+ });
+ // Check that each field is either expected or optional
+ let allowed = [...expectations.expected, ...expectations.optional];
+ Object.keys(stat).forEach(field => {
+ ok(
+ allowed.includes(field),
+ "Stat field " +
+ stat.type +
+ "." +
+ field +
+ ` is allowed. ${JSON.stringify(stat)}`
+ );
+ });
+
+ //
+ // Ensure that unimplemented fields are not implemented
+ // note: if a field is implemented it should be moved to expected or
+ // optional.
+ //
+ expectations.unimplemented.forEach(field => {
+ ok(
+ !Object.keys(stat).includes(field),
+ "Unimplemented field " + stat.type + "." + field + " does not exist."
+ );
+ });
+
+ //
+ // Ensure that all deprecated fields are not present
+ //
+ expectations.deprecated.forEach(field => {
+ ok(
+ !Object.keys(stat).includes(field),
+ "Deprecated field " + stat.type + "." + field + " does not exist."
+ );
+ });
+ });
+}
+
+function pedanticChecks(report) {
+ // Check that report is only-maplike
+ [...report.keys()].forEach(key =>
+ is(
+ report[key],
+ undefined,
+ `Report is not dictionary like, it lacks a property for key ${key}`
+ )
+ );
+ report.forEach((statObj, mapKey) => {
+ info(`"${mapKey} = ${JSON.stringify(statObj, null, 2)}`);
+ });
+ // eslint-disable-next-line complexity
+ report.forEach((statObj, mapKey) => {
+ let tested = {};
+ // Record what fields get tested.
+ // To access a field foo without marking it as tested use stat.inner.foo
+ let stat = new Proxy(statObj, {
+ get(stat, key) {
+ if (key == "inner") {
+ return stat;
+ }
+ tested[key] = true;
+ return stat[key];
+ },
+ });
+
+ let expectations = statsExpectedByType[stat.type];
+
+ if (expectations.skip) {
+ return;
+ }
+
+ // All stats share the following attributes inherited from RTCStats
+ is(stat.id, mapKey, stat.type + ".id is the same as the report key.");
+
+ // timestamp
+ ok(stat.timestamp >= 0, stat.type + ".timestamp is not less than 0");
+ // If the timebase for the timestamp is not properly set the timestamp
+ // will appear relative to the year 1970; Bug 1495446
+ const date = new Date(stat.timestamp);
+ ok(
+ date.getFullYear() > 1970,
+ `${stat.type}.timestamp is relative to current time, date=${date}`
+ );
+ //
+ // RTCStreamStats attributes with common behavior
+ //
+ // inbound-rtp, outbound-rtp, remote-inbound-rtp, remote-outbound-rtp
+ // inherit from RTCStreamStats
+ if (
+ [
+ "inbound-rtp",
+ "outbound-rtp",
+ "remote-inbound-rtp",
+ "remote-outbound-rtp",
+ ].includes(stat.type)
+ ) {
+ const isRemote = stat.type.startsWith("remote-");
+ //
+ // Common RTCStreamStats fields
+ //
+
+ // SSRC
+ ok(stat.ssrc, stat.type + ".ssrc has a value");
+
+ // kind
+ ok(
+ ["audio", "video"].includes(stat.kind),
+ stat.type + ".kind is 'audio' or 'video'"
+ );
+
+ // mediaType, renamed to kind but remains for backward compability.
+ ok(
+ ["audio", "video"].includes(stat.mediaType),
+ stat.type + ".mediaType is 'audio' or 'video'"
+ );
+
+ ok(stat.kind == stat.mediaType, "kind equals legacy mediaType");
+
+ // codecId
+ ok(stat.codecId, `${stat.type}.codecId has a value`);
+ ok(report.has(stat.codecId), `codecId ${stat.codecId} exists in report`);
+ is(
+ report.get(stat.codecId).type,
+ "codec",
+ `codecId ${stat.codecId} in report is codec type`
+ );
+ is(
+ report.get(stat.codecId).mimeType.slice(0, 5),
+ stat.kind,
+ `codecId ${stat.codecId} in report is for a mimeType of the same ` +
+ `media type as the referencing rtp stream stat`
+ );
+
+ if (isRemote) {
+ // local id
+ if (stat.localId) {
+ ok(
+ report.has(stat.localId),
+ `localId ${stat.localId} exists in report.`
+ );
+ is(
+ report.get(stat.localId).ssrc,
+ stat.ssrc,
+ "remote ssrc and local ssrc match."
+ );
+ is(
+ report.get(stat.localId).remoteId,
+ stat.id,
+ "local object has remote object as it's own remote object."
+ );
+ }
+ } else {
+ // remote id
+ if (stat.remoteId) {
+ ok(
+ report.has(stat.remoteId),
+ `remoteId ${stat.remoteId} exists in report.`
+ );
+ is(
+ report.get(stat.remoteId).ssrc,
+ stat.ssrc,
+ "remote ssrc and local ssrc match."
+ );
+ is(
+ report.get(stat.remoteId).localId,
+ stat.id,
+ "remote object has local object as it's own local object."
+ );
+ }
+ }
+
+ // nackCount
+ if (stat.nackCount) {
+ ok(
+ stat.nackCount >= 0,
+ `${stat.type}.nackCount is sane (${stat.kind}).`
+ );
+ }
+
+ if (!isRemote && stat.inner.kind == "video") {
+ // firCount
+ ok(
+ stat.firCount >= 0 && stat.firCount < 100,
+ `${stat.type}.firCount is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.firCount}`
+ );
+
+ // pliCount
+ ok(
+ stat.pliCount >= 0 && stat.pliCount < 200,
+ `${stat.type}.pliCount is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.pliCount}`
+ );
+
+ // qpSum
+ if (stat.qpSum !== undefined) {
+ ok(
+ stat.qpSum > 0,
+ `${stat.type}.qpSum is at least 0 ` +
+ `${stat.kind} test. value=${stat.qpSum}`
+ );
+ }
+ } else {
+ is(
+ stat.qpSum,
+ undefined,
+ `${stat.type}.qpSum does not exist when stat.kind != video`
+ );
+ }
+ }
+
+ if (stat.type == "inbound-rtp") {
+ //
+ // Required fields
+ //
+
+ // trackIdentifier
+ is(typeof stat.trackIdentifier, "string");
+ isnot(stat.trackIdentifier, "");
+
+ // packetsReceived
+ ok(
+ stat.packetsReceived >= 0 && stat.packetsReceived < 10 ** 5,
+ `${stat.type}.packetsReceived is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.packetsReceived}`
+ );
+
+ // packetsDiscarded
+ ok(
+ stat.packetsDiscarded >= 0 && stat.packetsDiscarded < 100,
+ `${stat.type}.packetsDiscarded is sane number for a short test. ` +
+ `value=${stat.packetsDiscarded}`
+ );
+ // bytesReceived
+ ok(
+ stat.bytesReceived >= 0 && stat.bytesReceived < 10 ** 9, // Not a magic number, just a guess
+ `${stat.type}.bytesReceived is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.bytesReceived}`
+ );
+
+ // packetsLost
+ ok(
+ stat.packetsLost < 100,
+ `${stat.type}.packetsLost is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.packetsLost}`
+ );
+
+ // This should be much lower for audio, TODO: Bug 1330575
+ let expectedJitter = stat.kind == "video" ? 0.5 : 1;
+ // jitter
+ ok(
+ stat.jitter < expectedJitter,
+ `${stat.type}.jitter is sane number for a ${stat.kind} ` +
+ `local only test. value=${stat.jitter}`
+ );
+
+ // lastPacketReceivedTimestamp
+ ok(
+ stat.lastPacketReceivedTimestamp !== undefined,
+ `${stat.type}.lastPacketReceivedTimestamp has a value`
+ );
+
+ // headerBytesReceived
+ ok(
+ stat.headerBytesReceived >= 0 && stat.headerBytesReceived < 50000,
+ `${stat.type}.headerBytesReceived is sane for a short test. ` +
+ `value=${stat.headerBytesReceived}`
+ );
+
+ // Always missing from libwebrtc stats
+ // estimatedPlayoutTimestamp
+ // ok(
+ // stat.estimatedPlayoutTimestamp !== undefined,
+ // `${stat.type}.estimatedPlayoutTimestamp has a value`
+ // );
+
+ // jitterBufferEmittedCount
+ let expectedJitterBufferEmmitedCount = stat.kind == "video" ? 7 : 1000;
+ ok(
+ stat.jitterBufferEmittedCount > expectedJitterBufferEmmitedCount,
+ `${stat.type}.jitterBufferEmittedCount is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.jitterBufferEmittedCount}`
+ );
+
+ // jitterBufferDelay
+ let avgJitterBufferDelay =
+ stat.jitterBufferDelay / stat.jitterBufferEmittedCount;
+ ok(
+ avgJitterBufferDelay > 0.01 && avgJitterBufferDelay < 10,
+ `${stat.type}.jitterBufferDelay is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.jitterBufferDelay}/${stat.jitterBufferEmittedCount}=${avgJitterBufferDelay}`
+ );
+
+ //
+ // Optional fields
+ //
+
+ //
+ // Local audio only stats
+ //
+ if (stat.inner.kind != "audio") {
+ expectations.localAudioOnly.forEach(field => {
+ ok(
+ stat[field] === undefined,
+ `${stat.type} does not have field ${field}` +
+ ` when kind is not 'audio'`
+ );
+ });
+ } else {
+ expectations.localAudioOnly.forEach(field => {
+ ok(
+ stat.inner[field] !== undefined,
+ stat.type + " has field " + field + " when kind is video"
+ );
+ });
+ // totalSamplesReceived
+ ok(
+ stat.totalSamplesReceived > 1000,
+ `${stat.type}.totalSamplesReceived is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.totalSamplesReceived}`
+ );
+
+ // fecPacketsReceived
+ ok(
+ stat.fecPacketsReceived >= 0 && stat.fecPacketsReceived < 10 ** 5,
+ `${stat.type}.fecPacketsReceived is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.fecPacketsReceived}`
+ );
+
+ // fecPacketsDiscarded
+ ok(
+ stat.fecPacketsDiscarded >= 0 && stat.fecPacketsDiscarded < 100,
+ `${stat.type}.fecPacketsDiscarded is sane number for a short test. ` +
+ `value=${stat.fecPacketsDiscarded}`
+ );
+ // concealedSamples
+ ok(
+ stat.concealedSamples >= 0 &&
+ stat.concealedSamples <= stat.totalSamplesReceived,
+ `${stat.type}.concealedSamples is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.concealedSamples}`
+ );
+
+ // silentConcealedSamples
+ ok(
+ stat.silentConcealedSamples >= 0 &&
+ stat.silentConcealedSamples <= stat.concealedSamples,
+ `${stat.type}.silentConcealedSamples is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.silentConcealedSamples}`
+ );
+
+ // concealmentEvents
+ ok(
+ stat.concealmentEvents >= 0 &&
+ stat.concealmentEvents <= stat.packetsReceived,
+ `${stat.type}.concealmentEvents is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.concealmentEvents}`
+ );
+
+ // insertedSamplesForDeceleration
+ ok(
+ stat.insertedSamplesForDeceleration >= 0 &&
+ stat.insertedSamplesForDeceleration <= stat.totalSamplesReceived,
+ `${stat.type}.insertedSamplesForDeceleration is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.insertedSamplesForDeceleration}`
+ );
+
+ // removedSamplesForAcceleration
+ ok(
+ stat.removedSamplesForAcceleration >= 0 &&
+ stat.removedSamplesForAcceleration <= stat.totalSamplesReceived,
+ `${stat.type}.removedSamplesForAcceleration is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.removedSamplesForAcceleration}`
+ );
+
+ // audioLevel
+ ok(
+ stat.audioLevel >= 0 && stat.audioLevel <= 128,
+ `${stat.type}.bytesReceived is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.audioLevel}`
+ );
+
+ // totalAudioEnergy
+ ok(
+ stat.totalAudioEnergy >= 0 && stat.totalAudioEnergy <= 128,
+ `${stat.type}.totalAudioEnergy is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.totalAudioEnergy}`
+ );
+
+ // totalSamplesDuration
+ ok(
+ stat.totalSamplesDuration >= 0 && stat.totalSamplesDuration <= 300,
+ `${stat.type}.totalSamplesDuration is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.totalSamplesDuration}`
+ );
+ }
+
+ //
+ // Local video only stats
+ //
+ if (stat.inner.kind != "video") {
+ expectations.localVideoOnly.forEach(field => {
+ ok(
+ stat[field] === undefined,
+ `${stat.type} does not have field ${field}` +
+ ` when kind is not 'video'`
+ );
+ });
+ } else {
+ expectations.localVideoOnly.forEach(field => {
+ ok(
+ stat.inner[field] !== undefined,
+ stat.type + " has field " + field + " when kind is video"
+ );
+ });
+ // discardedPackets
+ ok(
+ stat.discardedPackets < 100,
+ `${stat.type}.discardedPackets is a sane number for a short test. ` +
+ `value=${stat.discardedPackets}`
+ );
+ // framesPerSecond
+ ok(
+ stat.framesPerSecond > 0 && stat.framesPerSecond < 70,
+ `${stat.type}.framesPerSecond is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.framesPerSecond}`
+ );
+
+ // framesDecoded
+ ok(
+ stat.framesDecoded > 0 && stat.framesDecoded < 1000000,
+ `${stat.type}.framesDecoded is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.framesDecoded}`
+ );
+
+ // framesDropped
+ ok(
+ stat.framesDropped >= 0 && stat.framesDropped < 100,
+ `${stat.type}.framesDropped is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.framesDropped}`
+ );
+
+ // frameWidth
+ ok(
+ stat.frameWidth > 0 && stat.frameWidth < 100000,
+ `${stat.type}.frameWidth is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.framesSent}`
+ );
+
+ // frameHeight
+ ok(
+ stat.frameHeight > 0 && stat.frameHeight < 100000,
+ `${stat.type}.frameHeight is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.frameHeight}`
+ );
+
+ // totalDecodeTime
+ ok(
+ stat.totalDecodeTime >= 0 && stat.totalDecodeTime < 300,
+ `${stat.type}.totalDecodeTime is sane for a short test. ` +
+ `value=${stat.totalDecodeTime}`
+ );
+
+ // totalProcessingDelay
+ ok(
+ stat.totalProcessingDelay < 100,
+ `${stat.type}.totalProcessingDelay is sane number for a short test ` +
+ `local only test. value=${stat.totalProcessingDelay}`
+ );
+
+ // totalInterFrameDelay
+ ok(
+ stat.totalInterFrameDelay >= 0 && stat.totalInterFrameDelay < 100,
+ `${stat.type}.totalInterFrameDelay is sane for a short test. ` +
+ `value=${stat.totalInterFrameDelay}`
+ );
+
+ // totalSquaredInterFrameDelay
+ ok(
+ stat.totalSquaredInterFrameDelay >= 0 &&
+ stat.totalSquaredInterFrameDelay < 100,
+ `${stat.type}.totalSquaredInterFrameDelay is sane for a short test. ` +
+ `value=${stat.totalSquaredInterFrameDelay}`
+ );
+
+ // framesReceived
+ ok(
+ stat.framesReceived >= 0 && stat.framesReceived < 100000,
+ `${stat.type}.framesReceived is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.framesReceived}`
+ );
+ }
+ } else if (stat.type == "remote-inbound-rtp") {
+ // roundTripTime
+ ok(
+ stat.roundTripTime >= 0,
+ `${stat.type}.roundTripTime is sane with` +
+ `value of: ${stat.roundTripTime} (${stat.kind})`
+ );
+ //
+ // Required fields
+ //
+
+ // packetsLost
+ ok(
+ stat.packetsLost < 100,
+ `${stat.type}.packetsLost is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.packetsLost}`
+ );
+
+ // jitter
+ ok(
+ stat.jitter >= 0,
+ `${stat.type}.jitter is sane number (${stat.kind}). ` +
+ `value=${stat.jitter}`
+ );
+
+ //
+ // Optional fields
+ //
+
+ // packetsReceived
+ if (stat.packetsReceived) {
+ ok(
+ stat.packetsReceived >= 0 && stat.packetsReceived < 10 ** 5,
+ `${stat.type}.packetsReceived is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.packetsReceived}`
+ );
+ }
+
+ // totalRoundTripTime
+ ok(
+ stat.totalRoundTripTime < 50000,
+ `${stat.type}.totalRoundTripTime is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.totalRoundTripTime}`
+ );
+
+ // fractionLost
+ ok(
+ stat.fractionLost < 0.2,
+ `${stat.type}.fractionLost is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.fractionLost}`
+ );
+
+ // roundTripTimeMeasurements
+ ok(
+ stat.roundTripTimeMeasurements >= 1 &&
+ stat.roundTripTimeMeasurements < 500,
+ `${stat.type}.roundTripTimeMeasurements is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.roundTripTimeMeasurements}`
+ );
+ } else if (stat.type == "outbound-rtp") {
+ //
+ // Required fields
+ //
+
+ // packetsSent
+ ok(
+ stat.packetsSent > 0 && stat.packetsSent < 10000,
+ `${stat.type}.packetsSent is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.packetsSent}`
+ );
+
+ // bytesSent
+ const audio1Min = 16000 * 60; // 128kbps
+ const video1Min = 250000 * 60; // 2Mbps
+ ok(
+ stat.bytesSent > 0 &&
+ stat.bytesSent < (stat.kind == "video" ? video1Min : audio1Min),
+ `${stat.type}.bytesSent is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.bytesSent}`
+ );
+
+ // headerBytesSent
+ ok(
+ stat.headerBytesSent > 0 &&
+ stat.headerBytesSent < (stat.kind == "video" ? video1Min : audio1Min),
+ `${stat.type}.headerBytesSent is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.headerBytesSent}`
+ );
+
+ // retransmittedPacketsSent
+ ok(
+ stat.retransmittedPacketsSent >= 0 &&
+ stat.retransmittedPacketsSent <
+ (stat.kind == "video" ? video1Min : audio1Min),
+ `${stat.type}.retransmittedPacketsSent is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.retransmittedPacketsSent}`
+ );
+
+ // retransmittedBytesSent
+ ok(
+ stat.retransmittedBytesSent >= 0 &&
+ stat.retransmittedBytesSent <
+ (stat.kind == "video" ? video1Min : audio1Min),
+ `${stat.type}.retransmittedBytesSent is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.retransmittedBytesSent}`
+ );
+
+ //
+ // Optional fields
+ //
+
+ // qpSum
+ // This is supported for all of our vpx codecs (on the encode side, see
+ // bug 1519590)
+ const mimeType = report.get(stat.codecId).mimeType;
+ if (mimeType.includes("VP")) {
+ ok(
+ stat.qpSum >= 0,
+ `${stat.type}.qpSum is a sane number (${stat.kind}) ` +
+ `for ${report.get(stat.codecId).mimeType}. value=${stat.qpSum}`
+ );
+ } else if (mimeType.includes("H264")) {
+ // OpenH264 encoder records QP so we check for either condition.
+ if (!stat.qpSum && !("qpSum" in stat)) {
+ ok(
+ !stat.qpSum && !("qpSum" in stat),
+ `${stat.type}.qpSum absent for ${report.get(stat.codecId).mimeType}`
+ );
+ } else {
+ ok(
+ stat.qpSum >= 0,
+ `${stat.type}.qpSum is a sane number (${stat.kind}) ` +
+ `for ${report.get(stat.codecId).mimeType}. value=${stat.qpSum}`
+ );
+ }
+ } else {
+ ok(
+ !stat.qpSum && !("qpSum" in stat),
+ `${stat.type}.qpSum absent for ${report.get(stat.codecId).mimeType}`
+ );
+ }
+
+ //
+ // Local video only stats
+ //
+ if (stat.inner.kind != "video") {
+ expectations.localVideoOnly.forEach(field => {
+ ok(
+ stat[field] === undefined,
+ `${stat.type} does not have field ` +
+ `${field} when kind is not 'video'`
+ );
+ });
+ } else {
+ expectations.localVideoOnly.forEach(field => {
+ ok(
+ stat.inner[field] !== undefined,
+ `${stat.type} has field ` +
+ `${field} when kind is video and isRemote is false`
+ );
+ });
+
+ // framesEncoded
+ ok(
+ stat.framesEncoded >= 0 && stat.framesEncoded < 100000,
+ `${stat.type}.framesEncoded is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.framesEncoded}`
+ );
+
+ // frameWidth
+ ok(
+ stat.frameWidth >= 0 && stat.frameWidth < 100000,
+ `${stat.type}.frameWidth is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.frameWidth}`
+ );
+
+ // frameHeight
+ ok(
+ stat.frameHeight >= 0 && stat.frameHeight < 100000,
+ `${stat.type}.frameHeight is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.frameHeight}`
+ );
+
+ // framesSent
+ ok(
+ stat.framesSent >= 0 && stat.framesSent < 100000,
+ `${stat.type}.framesSent is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.framesSent}`
+ );
+
+ // hugeFramesSent
+ ok(
+ stat.hugeFramesSent >= 0 && stat.hugeFramesSent < 100000,
+ `${stat.type}.hugeFramesSent is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.hugeFramesSent}`
+ );
+
+ // totalEncodeTime
+ ok(
+ stat.totalEncodeTime >= 0,
+ `${stat.type}.totalEncodeTime is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.totalEncodeTime}`
+ );
+
+ // totalEncodedBytesTarget
+ ok(
+ stat.totalEncodedBytesTarget > 1000,
+ `${stat.type}.totalEncodedBytesTarget is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.totalEncodedBytesTarget}`
+ );
+ }
+ } else if (stat.type == "remote-outbound-rtp") {
+ //
+ // Required fields
+ //
+
+ // packetsSent
+ ok(
+ stat.packetsSent > 0 && stat.packetsSent < 10000,
+ `${stat.type}.packetsSent is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.packetsSent}`
+ );
+
+ // bytesSent
+ const audio1Min = 16000 * 60; // 128kbps
+ const video1Min = 250000 * 60; // 2Mbps
+ ok(
+ stat.bytesSent > 0 &&
+ stat.bytesSent < (stat.kind == "video" ? video1Min : audio1Min),
+ `${stat.type}.bytesSent is a sane number for a short ` +
+ `${stat.kind} test. value=${stat.bytesSent}`
+ );
+
+ ok(
+ stat.remoteTimestamp !== undefined,
+ `${stat.type}.remoteTimestamp ` + `is not undefined (${stat.kind})`
+ );
+ const ageSeconds = (stat.timestamp - stat.remoteTimestamp) / 1000;
+ // remoteTimestamp is exact (so it can be mapped to a packet), whereas
+ // timestamp has reduced precision. It is possible that
+ // remoteTimestamp occurs a millisecond into the future from
+ // timestamp. We also subtract half a millisecond when reducing
+ // precision on libwebrtc timestamps, to counteract the potential
+ // rounding up that libwebrtc may do since it tends to round its
+ // internal timestamps to whole milliseconds. In the worst case
+ // remoteTimestamp may therefore occur 2 milliseconds ahead of
+ // timestamp.
+ ok(
+ ageSeconds >= -0.002 && ageSeconds < 30,
+ `${stat.type}.remoteTimestamp is on the same timeline as ` +
+ `${stat.type}.timestamp, and no older than 30 seconds. ` +
+ `difference=${ageSeconds}s`
+ );
+ } else if (stat.type == "media-source") {
+ // trackIdentifier
+ is(typeof stat.trackIdentifier, "string");
+ isnot(stat.trackIdentifier, "");
+
+ // kind
+ is(typeof stat.kind, "string");
+ ok(stat.kind == "audio" || stat.kind == "video");
+ } else if (stat.type == "codec") {
+ //
+ // Required fields
+ //
+
+ // mimeType & payloadType
+ switch (stat.mimeType) {
+ case "audio/opus":
+ is(stat.payloadType, 109, "codec.payloadType for opus");
+ break;
+ case "video/VP8":
+ is(stat.payloadType, 120, "codec.payloadType for VP8");
+ break;
+ case "video/VP9":
+ is(stat.payloadType, 121, "codec.payloadType for VP9");
+ break;
+ case "video/H264":
+ ok(
+ stat.payloadType == 97 || stat.payloadType == 126,
+ `codec.payloadType for H264 was ${stat.payloadType}, exp. 97 or 126`
+ );
+ break;
+ default:
+ ok(
+ false,
+ `Unexpected codec.mimeType ${stat.mimeType} for payloadType ` +
+ `${stat.payloadType}`
+ );
+ break;
+ }
+
+ // transportId
+ // (no transport stats yet)
+ ok(stat.transportId, "codec.transportId is set");
+
+ // clockRate
+ if (stat.mimeType.startsWith("audio")) {
+ is(stat.clockRate, 48000, "codec.clockRate for audio/opus");
+ } else if (stat.mimeType.startsWith("video")) {
+ is(stat.clockRate, 90000, "codec.clockRate for video");
+ }
+
+ // sdpFmtpLine
+ // (not technically mandated by spec, but expected here)
+ ok(stat.sdpFmtpLine, "codec.sdpFmtpLine is set");
+ const opusParams = [
+ "maxplaybackrate",
+ "maxaveragebitrate",
+ "usedtx",
+ "stereo",
+ "useinbandfec",
+ "cbr",
+ "ptime",
+ "minptime",
+ "maxptime",
+ ];
+ const vpxParams = ["max-fs", "max-fr"];
+ const h264Params = [
+ "packetization-mode",
+ "level-asymmetry-allowed",
+ "profile-level-id",
+ "max-fs",
+ "max-cpb",
+ "max-dpb",
+ "max-br",
+ "max-mbps",
+ ];
+ for (const param of stat.sdpFmtpLine.split(";")) {
+ const [key, value] = param.split("=");
+ if (stat.payloadType == 109) {
+ ok(
+ opusParams.includes(key),
+ `codec.sdpFmtpLine param ${key}=${value} for opus`
+ );
+ } else if (stat.payloadType == 120 || stat.payloadType == 121) {
+ ok(
+ vpxParams.includes(key),
+ `codec.sdpFmtpLine param ${key}=${value} for VPx`
+ );
+ } else if (stat.payloadType == 97 || stat.payloadType == 126) {
+ ok(
+ h264Params.includes(key),
+ `codec.sdpFmtpLine param ${key}=${value} for H264`
+ );
+ if (key == "packetization-mode") {
+ if (stat.payloadType == 97) {
+ is(value, "0", "codec.sdpFmtpLine: H264 (97) packetization-mode");
+ } else if (stat.payloadType == 126) {
+ is(
+ value,
+ "1",
+ "codec.sdpFmtpLine: H264 (126) packetization-mode"
+ );
+ }
+ }
+ if (key == "profile-level-id") {
+ is(value, "42e01f", "codec.sdpFmtpLine: H264 profile-level-id");
+ }
+ }
+ }
+
+ //
+ // Optional fields
+ //
+
+ // codecType
+ ok(
+ !Object.keys(stat).includes("codecType") ||
+ stat.codecType == "encode" ||
+ stat.codecType == "decode",
+ "codec.codecType (${codec.codecType}) is an expected value or absent"
+ );
+ let numRecvStreams = 0;
+ let numSendStreams = 0;
+ const counts = {
+ "inbound-rtp": 0,
+ "outbound-rtp": 0,
+ "remote-inbound-rtp": 0,
+ "remote-outbound-rtp": 0,
+ };
+ const [kind] = stat.mimeType.split("/");
+ report.forEach(other => {
+ if (other.type == "inbound-rtp" && other.kind == kind) {
+ numRecvStreams += 1;
+ } else if (other.type == "outbound-rtp" && other.kind == kind) {
+ numSendStreams += 1;
+ }
+ if (other.codecId == stat.id) {
+ counts[other.type] += 1;
+ }
+ });
+ const expectedCounts = {
+ encode: {
+ "inbound-rtp": 0,
+ "outbound-rtp": numSendStreams,
+ "remote-inbound-rtp": numSendStreams,
+ "remote-outbound-rtp": 0,
+ },
+ decode: {
+ "inbound-rtp": numRecvStreams,
+ "outbound-rtp": 0,
+ "remote-inbound-rtp": 0,
+ "remote-outbound-rtp": numRecvStreams,
+ },
+ absent: {
+ "inbound-rtp": numRecvStreams,
+ "outbound-rtp": numSendStreams,
+ "remote-inbound-rtp": numSendStreams,
+ "remote-outbound-rtp": numRecvStreams,
+ },
+ };
+ // Note that the logic above assumes at most one sender and at most one
+ // receiver was used to generate this stats report. If more senders or
+ // receivers are present, they'd be referring to not only this codec stat,
+ // skewing `numSendStreams` and `numRecvStreams` above.
+ // This could be fixed when we support `senderId` and `receiverId` in
+ // RTCOutboundRtpStreamStats and RTCInboundRtpStreamStats respectively.
+ for (const [key, value] of Object.entries(counts)) {
+ is(
+ value,
+ expectedCounts[stat.codecType || "absent"][key],
+ `codec.codecType ${stat.codecType || "absent"} ref from ${key} stat`
+ );
+ }
+
+ // channels
+ if (stat.mimeType.startsWith("audio")) {
+ ok(stat.channels, "codec.channels should exist for audio");
+ if (stat.channels) {
+ if (stat.sdpFmtpLine.includes("stereo=1")) {
+ is(stat.channels, 2, "codec.channels for stereo audio");
+ } else {
+ is(stat.channels, 1, "codec.channels for mono audio");
+ }
+ }
+ } else {
+ ok(!stat.channels, "codec.channels should not exist for video");
+ }
+ } else if (stat.type == "candidate-pair") {
+ info("candidate-pair is: " + JSON.stringify(stat));
+ //
+ // Required fields
+ //
+
+ // transportId
+ ok(
+ stat.transportId,
+ `${stat.type}.transportId has a value. value=` +
+ `${stat.transportId} (${stat.kind})`
+ );
+
+ // localCandidateId
+ ok(
+ stat.localCandidateId,
+ `${stat.type}.localCandidateId has a value. value=` +
+ `${stat.localCandidateId} (${stat.kind})`
+ );
+
+ // remoteCandidateId
+ ok(
+ stat.remoteCandidateId,
+ `${stat.type}.remoteCandidateId has a value. value=` +
+ `${stat.remoteCandidateId} (${stat.kind})`
+ );
+
+ // priority
+ ok(
+ stat.priority,
+ `${stat.type}.priority has a value. value=` +
+ `${stat.priority} (${stat.kind})`
+ );
+
+ // readable
+ ok(
+ stat.readable,
+ `${stat.type}.readable is true. value=${stat.readable} ` +
+ `(${stat.kind})`
+ );
+
+ // writable
+ ok(
+ stat.writable,
+ `${stat.type}.writable is true. value=${stat.writable} ` +
+ `(${stat.kind})`
+ );
+
+ // state
+ if (
+ stat.state == "succeeded" &&
+ stat.selected !== undefined &&
+ stat.selected
+ ) {
+ info("candidate-pair state is succeeded and selected is true");
+ // nominated
+ ok(
+ stat.nominated,
+ `${stat.type}.nominated is true. value=${stat.nominated} ` +
+ `(${stat.kind})`
+ );
+
+ // bytesSent
+ ok(
+ stat.bytesSent > 1000,
+ `${stat.type}.bytesSent is a sane number (>1,000) for a short ` +
+ `${stat.kind} test. value=${stat.bytesSent}`
+ );
+
+ // bytesReceived
+ ok(
+ stat.bytesReceived > 500,
+ `${stat.type}.bytesReceived is a sane number (>500) for a short ` +
+ `${stat.kind} test. value=${stat.bytesReceived}`
+ );
+
+ // lastPacketSentTimestamp
+ ok(
+ stat.lastPacketSentTimestamp,
+ `${stat.type}.lastPacketSentTimestamp has a value. value=` +
+ `${stat.lastPacketSentTimestamp} (${stat.kind})`
+ );
+
+ // lastPacketReceivedTimestamp
+ ok(
+ stat.lastPacketReceivedTimestamp,
+ `${stat.type}.lastPacketReceivedTimestamp has a value. value=` +
+ `${stat.lastPacketReceivedTimestamp} (${stat.kind})`
+ );
+ } else {
+ info("candidate-pair is _not_ both state == succeeded and selected");
+ // nominated
+ ok(
+ stat.nominated !== undefined,
+ `${stat.type}.nominated exists. value=${stat.nominated} ` +
+ `(${stat.kind})`
+ );
+ ok(
+ stat.bytesSent !== undefined,
+ `${stat.type}.bytesSent exists. value=${stat.bytesSent} ` +
+ `(${stat.kind})`
+ );
+ ok(
+ stat.bytesReceived !== undefined,
+ `${stat.type}.bytesReceived exists. value=${stat.bytesReceived} ` +
+ `(${stat.kind})`
+ );
+ ok(
+ stat.lastPacketSentTimestamp !== undefined,
+ `${stat.type}.lastPacketSentTimestamp exists. value=` +
+ `${stat.lastPacketSentTimestamp} (${stat.kind})`
+ );
+ ok(
+ stat.lastPacketReceivedTimestamp !== undefined,
+ `${stat.type}.lastPacketReceivedTimestamp exists. value=` +
+ `${stat.lastPacketReceivedTimestamp} (${stat.kind})`
+ );
+ }
+
+ //
+ // Optional fields
+ //
+ // selected
+ ok(
+ stat.selected === undefined ||
+ (stat.state == "succeeded" && stat.selected) ||
+ !stat.selected,
+ `${stat.type}.selected is undefined, true when state is succeeded, ` +
+ `or false. value=${stat.selected} (${stat.kind})`
+ );
+ } else if (
+ stat.type == "local-candidate" ||
+ stat.type == "remote-candidate"
+ ) {
+ info(`candidate is ${JSON.stringify(stat)}`);
+
+ // address
+ ok(
+ stat.address,
+ `${stat.type} has address. value=${stat.address} ` + `(${stat.kind})`
+ );
+
+ // protocol
+ ok(
+ stat.protocol,
+ `${stat.type} has protocol. value=${stat.protocol} ` + `(${stat.kind})`
+ );
+
+ // port
+ ok(
+ stat.port >= 0,
+ `${stat.type} has port >= 0. value=${stat.port} ` + `(${stat.kind})`
+ );
+ ok(
+ stat.port <= 65535,
+ `${stat.type} has port <= 65535. value=${stat.port} ` + `(${stat.kind})`
+ );
+
+ // candidateType
+ ok(
+ stat.candidateType,
+ `${stat.type} has candidateType. value=${stat.candidateType} ` +
+ `(${stat.kind})`
+ );
+
+ // priority
+ ok(
+ stat.priority > 0 && stat.priority < 2 ** 32 - 1,
+ `${stat.type} has priority between 1 and 2^32 - 1 inc. ` +
+ `value=${stat.priority} (${stat.kind})`
+ );
+
+ // relayProtocol
+ if (stat.type == "local-candidate" && stat.candidateType == "relay") {
+ ok(
+ stat.relayProtocol,
+ `relay ${stat.type} has relayProtocol. value=${stat.relayProtocol} ` +
+ `(${stat.kind})`
+ );
+ } else {
+ is(
+ stat.relayProtocol,
+ undefined,
+ `relayProtocol is undefined for candidates that are not relay and ` +
+ `local. value=${stat.relayProtocol} (${stat.kind})`
+ );
+ }
+
+ // proxied
+ if (stat.proxied) {
+ ok(
+ stat.proxied == "proxied" || stat.proxied == "non-proxied",
+ `${stat.type} has proxied. value=${stat.proxied} (${stat.kind})`
+ );
+ }
+ }
+
+ //
+ // Ensure everything was tested
+ //
+ [...expectations.expected, ...expectations.optional].forEach(field => {
+ ok(
+ Object.keys(tested).includes(field),
+ `${stat.type}.${field} was tested.`
+ );
+ });
+ });
+}
+
+function dumpStats(stats) {
+ const dict = {};
+ for (const [k, v] of stats.entries()) {
+ dict[k] = v;
+ }
+ info(`Got stats: ${JSON.stringify(dict)}`);
+}
+
+async function waitForSyncedRtcp(pc) {
+ // Ensures that RTCP is present
+ let ensureSyncedRtcp = async () => {
+ let report = await pc.getStats();
+ for (const v of report.values()) {
+ if (v.type.endsWith("bound-rtp") && !(v.remoteId || v.localId)) {
+ info(`${v.id} is missing remoteId or localId: ${JSON.stringify(v)}`);
+ return null;
+ }
+ if (v.type == "remote-inbound-rtp" && v.roundTripTime === undefined) {
+ info(`${v.id} is missing roundTripTime: ${JSON.stringify(v)}`);
+ return null;
+ }
+ }
+ return report;
+ };
+ // Returns true if there is proof in aStats of rtcp flow for all remote stats
+ // objects, compared to baseStats.
+ const hasAllRtcpUpdated = (baseStats, stats) => {
+ let hasRtcpStats = false;
+ for (const v of stats.values()) {
+ if (v.type == "remote-outbound-rtp") {
+ hasRtcpStats = true;
+ if (!v.remoteTimestamp) {
+ // `remoteTimestamp` is 0 or not present.
+ return false;
+ }
+ if (v.remoteTimestamp <= baseStats.get(v.id)?.remoteTimestamp) {
+ // `remoteTimestamp` has not advanced further than the base stats,
+ // i.e., no new sender report has been received.
+ return false;
+ }
+ } else if (v.type == "remote-inbound-rtp") {
+ hasRtcpStats = true;
+ // The ideal thing here would be to check `reportsReceived`, but it's
+ // not yet implemented.
+ if (!v.packetsReceived) {
+ // `packetsReceived` is 0 or not present.
+ return false;
+ }
+ if (v.packetsReceived <= baseStats.get(v.id)?.packetsReceived) {
+ // `packetsReceived` has not advanced further than the base stats,
+ // i.e., no new receiver report has been received.
+ return false;
+ }
+ }
+ }
+ return hasRtcpStats;
+ };
+ let attempts = 0;
+ const baseStats = await pc.getStats();
+ // Time-units are MS
+ const waitPeriod = 100;
+ const maxTime = 20000;
+ for (let totalTime = maxTime; totalTime > 0; totalTime -= waitPeriod) {
+ try {
+ let syncedStats = await ensureSyncedRtcp();
+ if (syncedStats && hasAllRtcpUpdated(baseStats, syncedStats)) {
+ dumpStats(syncedStats);
+ return syncedStats;
+ }
+ } catch (e) {
+ info(e);
+ info(e.stack);
+ throw e;
+ }
+ attempts += 1;
+ info(`waitForSyncedRtcp: no sync on attempt ${attempts}, retrying.`);
+ await wait(waitPeriod);
+ }
+ throw Error(
+ "Waiting for synced RTCP timed out after at least " + maxTime + "ms"
+ );
+}
+
+function checkSenderStats(senderStats, streamCount) {
+ const outboundRtpReports = [];
+ const remoteInboundRtpReports = [];
+ for (const v of senderStats.values()) {
+ if (v.type == "outbound-rtp") {
+ outboundRtpReports.push(v);
+ } else if (v.type == "remote-inbound-rtp") {
+ remoteInboundRtpReports.push(v);
+ }
+ }
+ is(
+ outboundRtpReports.length,
+ streamCount,
+ `Sender with ${streamCount} simulcast streams has ${streamCount} outbound-rtp reports`
+ );
+ is(
+ remoteInboundRtpReports.length,
+ streamCount,
+ `Sender with ${streamCount} simulcast streams has ${streamCount} remote-inbound-rtp reports`
+ );
+ for (const outboundRtpReport of outboundRtpReports) {
+ is(
+ outboundRtpReports.filter(r => r.ssrc == outboundRtpReport.ssrc).length,
+ 1,
+ "Simulcast send track SSRCs are distinct"
+ );
+ const remoteReports = remoteInboundRtpReports.filter(
+ r => r.id == outboundRtpReport.remoteId
+ );
+ is(
+ remoteReports.length,
+ 1,
+ "Simulcast send tracks have exactly one remote counterpart"
+ );
+ const remoteInboundRtpReport = remoteReports[0];
+ is(
+ outboundRtpReport.ssrc,
+ remoteInboundRtpReport.ssrc,
+ "SSRC matches for outbound-rtp and remote-inbound-rtp"
+ );
+ }
+}
+
+function PC_LOCAL_TEST_LOCAL_STATS(test) {
+ return waitForSyncedRtcp(test.pcLocal._pc).then(stats => {
+ checkExpectedFields(stats);
+ pedanticChecks(stats);
+ return Promise.all([
+ test.pcLocal._pc.getSenders().map(async s => {
+ checkSenderStats(
+ await s.getStats(),
+ Math.max(1, s.getParameters()?.encodings?.length ?? 0)
+ );
+ }),
+ ]);
+ });
+}
+
+function PC_REMOTE_TEST_REMOTE_STATS(test) {
+ return waitForSyncedRtcp(test.pcRemote._pc).then(stats => {
+ checkExpectedFields(stats);
+ pedanticChecks(stats);
+ return Promise.all([
+ test.pcRemote._pc.getSenders().map(async s => {
+ checkSenderStats(
+ await s.getStats(),
+ s.track ? Math.max(1, s.getParameters()?.encodings?.length ?? 0) : 0
+ );
+ }),
+ ]);
+ });
+}
diff --git a/dom/media/webrtc/tests/mochitests/templates.js b/dom/media/webrtc/tests/mochitests/templates.js
new file mode 100644
index 0000000000..6b7750fd2c
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/templates.js
@@ -0,0 +1,615 @@
+/**
+ * Default list of commands to execute for a PeerConnection test.
+ */
+
+const STABLE = "stable";
+const HAVE_LOCAL_OFFER = "have-local-offer";
+const HAVE_REMOTE_OFFER = "have-remote-offer";
+const CLOSED = "closed";
+
+const ICE_NEW = "new";
+const GATH_NEW = "new";
+const GATH_GATH = "gathering";
+const GATH_COMPLETE = "complete";
+
+function deltaSeconds(date1, date2) {
+ return (date2.getTime() - date1.getTime()) / 1000;
+}
+
+function dumpSdp(test) {
+ if (typeof test._local_offer !== "undefined") {
+ dump("ERROR: SDP offer: " + test._local_offer.sdp.replace(/[\r]/g, ""));
+ }
+ if (typeof test._remote_answer !== "undefined") {
+ dump("ERROR: SDP answer: " + test._remote_answer.sdp.replace(/[\r]/g, ""));
+ }
+
+ if (
+ test.pcLocal &&
+ typeof test.pcLocal._local_ice_candidates !== "undefined"
+ ) {
+ dump(
+ "pcLocal._local_ice_candidates: " +
+ JSON.stringify(test.pcLocal._local_ice_candidates) +
+ "\n"
+ );
+ dump(
+ "pcLocal._remote_ice_candidates: " +
+ JSON.stringify(test.pcLocal._remote_ice_candidates) +
+ "\n"
+ );
+ dump(
+ "pcLocal._ice_candidates_to_add: " +
+ JSON.stringify(test.pcLocal._ice_candidates_to_add) +
+ "\n"
+ );
+ }
+ if (
+ test.pcRemote &&
+ typeof test.pcRemote._local_ice_candidates !== "undefined"
+ ) {
+ dump(
+ "pcRemote._local_ice_candidates: " +
+ JSON.stringify(test.pcRemote._local_ice_candidates) +
+ "\n"
+ );
+ dump(
+ "pcRemote._remote_ice_candidates: " +
+ JSON.stringify(test.pcRemote._remote_ice_candidates) +
+ "\n"
+ );
+ dump(
+ "pcRemote._ice_candidates_to_add: " +
+ JSON.stringify(test.pcRemote._ice_candidates_to_add) +
+ "\n"
+ );
+ }
+
+ if (test.pcLocal && typeof test.pcLocal.iceConnectionLog !== "undefined") {
+ dump(
+ "pcLocal ICE connection state log: " +
+ test.pcLocal.iceConnectionLog +
+ "\n"
+ );
+ }
+ if (test.pcRemote && typeof test.pcRemote.iceConnectionLog !== "undefined") {
+ dump(
+ "pcRemote ICE connection state log: " +
+ test.pcRemote.iceConnectionLog +
+ "\n"
+ );
+ }
+
+ if (
+ test.pcLocal &&
+ test.pcRemote &&
+ typeof test.pcLocal.setRemoteDescDate !== "undefined" &&
+ typeof test.pcRemote.setLocalDescDate !== "undefined"
+ ) {
+ var delta = deltaSeconds(
+ test.pcLocal.setRemoteDescDate,
+ test.pcRemote.setLocalDescDate
+ );
+ dump(
+ "Delay between pcLocal.setRemote <-> pcRemote.setLocal: " + delta + "\n"
+ );
+ }
+ if (
+ test.pcLocal &&
+ test.pcRemote &&
+ typeof test.pcLocal.setRemoteDescDate !== "undefined" &&
+ typeof test.pcLocal.setRemoteDescStableEventDate !== "undefined"
+ ) {
+ var delta = deltaSeconds(
+ test.pcLocal.setRemoteDescDate,
+ test.pcLocal.setRemoteDescStableEventDate
+ );
+ dump(
+ "Delay between pcLocal.setRemote <-> pcLocal.signalingStateStable: " +
+ delta +
+ "\n"
+ );
+ }
+ if (
+ test.pcLocal &&
+ test.pcRemote &&
+ typeof test.pcRemote.setLocalDescDate !== "undefined" &&
+ typeof test.pcRemote.setLocalDescStableEventDate !== "undefined"
+ ) {
+ var delta = deltaSeconds(
+ test.pcRemote.setLocalDescDate,
+ test.pcRemote.setLocalDescStableEventDate
+ );
+ dump(
+ "Delay between pcRemote.setLocal <-> pcRemote.signalingStateStable: " +
+ delta +
+ "\n"
+ );
+ }
+}
+
+// We need to verify that at least one candidate has been (or will be) gathered.
+function waitForAnIceCandidate(pc) {
+ return new Promise(resolve => {
+ if (!pc.localRequiresTrickleIce || pc._local_ice_candidates.length) {
+ resolve();
+ } else {
+ // In some circumstances, especially when both PCs are on the same
+ // browser, even though we are connected, the connection can be
+ // established without receiving a single candidate from one or other
+ // peer. So we wait for at least one...
+ pc._pc.addEventListener("icecandidate", resolve);
+ }
+ }).then(() => {
+ ok(
+ pc._local_ice_candidates.length,
+ pc + " received local trickle ICE candidates"
+ );
+ isnot(
+ pc._pc.iceGatheringState,
+ GATH_NEW,
+ pc + " ICE gathering state is not 'new'"
+ );
+ });
+}
+
+async function checkTrackStats(pc, track, outbound) {
+ const audio = track.kind == "audio";
+ const msg =
+ `${pc} stats ${outbound ? "outbound " : "inbound "}` +
+ `${audio ? "audio" : "video"} rtp track id ${track.id}`;
+ const stats = await pc.getStats(track);
+ ok(
+ pc.hasStat(stats, {
+ type: outbound ? "outbound-rtp" : "inbound-rtp",
+ kind: audio ? "audio" : "video",
+ }),
+ `${msg} - found expected stats`
+ );
+ ok(
+ !pc.hasStat(stats, {
+ type: outbound ? "inbound-rtp" : "outbound-rtp",
+ }),
+ `${msg} - did not find extra stats with wrong direction`
+ );
+ ok(
+ !pc.hasStat(stats, {
+ kind: audio ? "video" : "audio",
+ }),
+ `${msg} - did not find extra stats with wrong media type`
+ );
+}
+
+function checkAllTrackStats(pc) {
+ return Promise.all([
+ ...pc
+ .getExpectedActiveReceivers()
+ .map(({ track }) => checkTrackStats(pc, track, false)),
+ ...pc
+ .getExpectedSenders()
+ .map(({ track }) => checkTrackStats(pc, track, true)),
+ ]);
+}
+
+// Commands run once at the beginning of each test, even when performing a
+// renegotiation test.
+var commandsPeerConnectionInitial = [
+ function PC_LOCAL_SETUP_ICE_LOGGER(test) {
+ test.pcLocal.logIceConnectionState();
+ },
+
+ function PC_REMOTE_SETUP_ICE_LOGGER(test) {
+ test.pcRemote.logIceConnectionState();
+ },
+
+ function PC_LOCAL_SETUP_SIGNALING_LOGGER(test) {
+ test.pcLocal.logSignalingState();
+ },
+
+ function PC_REMOTE_SETUP_SIGNALING_LOGGER(test) {
+ test.pcRemote.logSignalingState();
+ },
+
+ function PC_LOCAL_SETUP_TRACK_HANDLER(test) {
+ test.pcLocal.setupTrackEventHandler();
+ },
+
+ function PC_REMOTE_SETUP_TRACK_HANDLER(test) {
+ test.pcRemote.setupTrackEventHandler();
+ },
+
+ function PC_LOCAL_CHECK_INITIAL_SIGNALINGSTATE(test) {
+ is(
+ test.pcLocal.signalingState,
+ STABLE,
+ "Initial local signalingState is 'stable'"
+ );
+ },
+
+ function PC_REMOTE_CHECK_INITIAL_SIGNALINGSTATE(test) {
+ is(
+ test.pcRemote.signalingState,
+ STABLE,
+ "Initial remote signalingState is 'stable'"
+ );
+ },
+
+ function PC_LOCAL_CHECK_INITIAL_ICE_STATE(test) {
+ is(
+ test.pcLocal.iceConnectionState,
+ ICE_NEW,
+ "Initial local ICE connection state is 'new'"
+ );
+ },
+
+ function PC_REMOTE_CHECK_INITIAL_ICE_STATE(test) {
+ is(
+ test.pcRemote.iceConnectionState,
+ ICE_NEW,
+ "Initial remote ICE connection state is 'new'"
+ );
+ },
+
+ function PC_LOCAL_CHECK_INITIAL_CAN_TRICKLE_SYNC(test) {
+ is(
+ test.pcLocal._pc.canTrickleIceCandidates,
+ null,
+ "Local trickle status should start out unknown"
+ );
+ },
+
+ function PC_REMOTE_CHECK_INITIAL_CAN_TRICKLE_SYNC(test) {
+ is(
+ test.pcRemote._pc.canTrickleIceCandidates,
+ null,
+ "Remote trickle status should start out unknown"
+ );
+ },
+];
+
+var commandsGetUserMedia = [
+ function PC_LOCAL_GUM(test) {
+ return test.pcLocal.getAllUserMediaAndAddStreams(test.pcLocal.constraints);
+ },
+
+ function PC_REMOTE_GUM(test) {
+ return test.pcRemote.getAllUserMediaAndAddStreams(
+ test.pcRemote.constraints
+ );
+ },
+];
+
+var commandsPeerConnectionOfferAnswer = [
+ function PC_LOCAL_SETUP_ICE_HANDLER(test) {
+ test.pcLocal.setupIceCandidateHandler(test);
+ },
+
+ function PC_REMOTE_SETUP_ICE_HANDLER(test) {
+ test.pcRemote.setupIceCandidateHandler(test);
+ },
+
+ function PC_LOCAL_CREATE_OFFER(test) {
+ return test.createOffer(test.pcLocal).then(offer => {
+ is(
+ test.pcLocal.signalingState,
+ STABLE,
+ "Local create offer does not change signaling state"
+ );
+ });
+ },
+
+ function PC_LOCAL_SET_LOCAL_DESCRIPTION(test) {
+ return test
+ .setLocalDescription(test.pcLocal, test.originalOffer, HAVE_LOCAL_OFFER)
+ .then(() => {
+ is(
+ test.pcLocal.signalingState,
+ HAVE_LOCAL_OFFER,
+ "signalingState after local setLocalDescription is 'have-local-offer'"
+ );
+ });
+ },
+
+ function PC_REMOTE_GET_OFFER(test) {
+ test._local_offer = test.originalOffer;
+ test._offer_constraints = test.pcLocal.constraints;
+ test._offer_options = test.pcLocal.offerOptions;
+ return Promise.resolve();
+ },
+
+ function PC_REMOTE_SET_REMOTE_DESCRIPTION(test) {
+ return test
+ .setRemoteDescription(test.pcRemote, test._local_offer, HAVE_REMOTE_OFFER)
+ .then(() => {
+ is(
+ test.pcRemote.signalingState,
+ HAVE_REMOTE_OFFER,
+ "signalingState after remote setRemoteDescription is 'have-remote-offer'"
+ );
+ });
+ },
+
+ function PC_REMOTE_CHECK_CAN_TRICKLE_SYNC(test) {
+ is(
+ test.pcRemote._pc.canTrickleIceCandidates,
+ true,
+ "Remote thinks that local can trickle"
+ );
+ },
+
+ function PC_LOCAL_SANE_LOCAL_SDP(test) {
+ test.pcLocal.localRequiresTrickleIce = sdputils.verifySdp(
+ test._local_offer,
+ "offer",
+ test._offer_constraints,
+ test._offer_options,
+ test.testOptions
+ );
+ },
+
+ function PC_REMOTE_SANE_REMOTE_SDP(test) {
+ test.pcRemote.remoteRequiresTrickleIce = sdputils.verifySdp(
+ test._local_offer,
+ "offer",
+ test._offer_constraints,
+ test._offer_options,
+ test.testOptions
+ );
+ },
+
+ function PC_REMOTE_CREATE_ANSWER(test) {
+ return test.createAnswer(test.pcRemote).then(answer => {
+ is(
+ test.pcRemote.signalingState,
+ HAVE_REMOTE_OFFER,
+ "Remote createAnswer does not change signaling state"
+ );
+ });
+ },
+
+ function PC_REMOTE_SET_LOCAL_DESCRIPTION(test) {
+ return test
+ .setLocalDescription(test.pcRemote, test.originalAnswer, STABLE)
+ .then(() => {
+ is(
+ test.pcRemote.signalingState,
+ STABLE,
+ "signalingState after remote setLocalDescription is 'stable'"
+ );
+ });
+ },
+
+ function PC_LOCAL_GET_ANSWER(test) {
+ test._remote_answer = test.originalAnswer;
+ test._answer_constraints = test.pcRemote.constraints;
+ return Promise.resolve();
+ },
+
+ function PC_LOCAL_SET_REMOTE_DESCRIPTION(test) {
+ return test
+ .setRemoteDescription(test.pcLocal, test._remote_answer, STABLE)
+ .then(() => {
+ is(
+ test.pcLocal.signalingState,
+ STABLE,
+ "signalingState after local setRemoteDescription is 'stable'"
+ );
+ });
+ },
+
+ function PC_REMOTE_SANE_LOCAL_SDP(test) {
+ test.pcRemote.localRequiresTrickleIce = sdputils.verifySdp(
+ test._remote_answer,
+ "answer",
+ test._offer_constraints,
+ test._offer_options,
+ test.testOptions
+ );
+ },
+ function PC_LOCAL_SANE_REMOTE_SDP(test) {
+ test.pcLocal.remoteRequiresTrickleIce = sdputils.verifySdp(
+ test._remote_answer,
+ "answer",
+ test._offer_constraints,
+ test._offer_options,
+ test.testOptions
+ );
+ },
+
+ function PC_LOCAL_CHECK_CAN_TRICKLE_SYNC(test) {
+ is(
+ test.pcLocal._pc.canTrickleIceCandidates,
+ true,
+ "Local thinks that remote can trickle"
+ );
+ },
+
+ function PC_LOCAL_WAIT_FOR_ICE_CONNECTED(test) {
+ return test.pcLocal.waitForIceConnected().then(() => {
+ info(
+ test.pcLocal +
+ ": ICE connection state log: " +
+ test.pcLocal.iceConnectionLog
+ );
+ });
+ },
+
+ function PC_REMOTE_WAIT_FOR_ICE_CONNECTED(test) {
+ return test.pcRemote.waitForIceConnected().then(() => {
+ info(
+ test.pcRemote +
+ ": ICE connection state log: " +
+ test.pcRemote.iceConnectionLog
+ );
+ });
+ },
+
+ function PC_LOCAL_VERIFY_ICE_GATHERING(test) {
+ return waitForAnIceCandidate(test.pcLocal);
+ },
+
+ function PC_REMOTE_VERIFY_ICE_GATHERING(test) {
+ return waitForAnIceCandidate(test.pcRemote);
+ },
+
+ function PC_LOCAL_WAIT_FOR_MEDIA_FLOW(test) {
+ return test.pcLocal.waitForMediaFlow();
+ },
+
+ function PC_REMOTE_WAIT_FOR_MEDIA_FLOW(test) {
+ return test.pcRemote.waitForMediaFlow();
+ },
+
+ function PC_LOCAL_CHECK_STATS(test) {
+ return test.pcLocal.getStats().then(stats => {
+ test.pcLocal.checkStats(stats);
+ });
+ },
+
+ function PC_REMOTE_CHECK_STATS(test) {
+ return test.pcRemote.getStats().then(stats => {
+ test.pcRemote.checkStats(stats);
+ });
+ },
+
+ function PC_LOCAL_CHECK_ICE_CONNECTION_TYPE(test) {
+ return test.pcLocal.getStats().then(stats => {
+ test.pcLocal.checkStatsIceConnectionType(
+ stats,
+ test.testOptions.expectedLocalCandidateType
+ );
+ });
+ },
+
+ function PC_REMOTE_CHECK_ICE_CONNECTION_TYPE(test) {
+ return test.pcRemote.getStats().then(stats => {
+ test.pcRemote.checkStatsIceConnectionType(
+ stats,
+ test.testOptions.expectedRemoteCandidateType
+ );
+ });
+ },
+
+ function PC_LOCAL_CHECK_ICE_CONNECTIONS(test) {
+ return test.pcLocal.getStats().then(stats => {
+ test.pcLocal.checkStatsIceConnections(stats, test.testOptions);
+ });
+ },
+
+ function PC_REMOTE_CHECK_ICE_CONNECTIONS(test) {
+ return test.pcRemote.getStats().then(stats => {
+ test.pcRemote.checkStatsIceConnections(stats, test.testOptions);
+ });
+ },
+
+ function PC_LOCAL_CHECK_MSID(test) {
+ return test.pcLocal.checkLocalMsids();
+ },
+ function PC_REMOTE_CHECK_MSID(test) {
+ return test.pcRemote.checkLocalMsids();
+ },
+
+ function PC_LOCAL_CHECK_TRACK_STATS(test) {
+ return checkAllTrackStats(test.pcLocal);
+ },
+ function PC_REMOTE_CHECK_TRACK_STATS(test) {
+ return checkAllTrackStats(test.pcRemote);
+ },
+ function PC_LOCAL_VERIFY_SDP_AFTER_END_OF_TRICKLE(test) {
+ if (test.pcLocal.endOfTrickleSdp) {
+ /* In case the endOfTrickleSdp promise is resolved already it will win the
+ * race because it gets evaluated first. But if endOfTrickleSdp is still
+ * pending the rejection will win the race. */
+ return Promise.race([
+ test.pcLocal.endOfTrickleSdp,
+ Promise.reject("No SDP"),
+ ]).then(
+ sdp =>
+ sdputils.checkSdpAfterEndOfTrickle(
+ sdp,
+ test.testOptions,
+ test.pcLocal.label
+ ),
+ () =>
+ info(
+ "pcLocal: Gathering is not complete yet, skipping post-gathering SDP check"
+ )
+ );
+ }
+ },
+ function PC_REMOTE_VERIFY_SDP_AFTER_END_OF_TRICKLE(test) {
+ if (test.pcRemote.endOfTrickleSdp) {
+ /* In case the endOfTrickleSdp promise is resolved already it will win the
+ * race because it gets evaluated first. But if endOfTrickleSdp is still
+ * pending the rejection will win the race. */
+ return Promise.race([
+ test.pcRemote.endOfTrickleSdp,
+ Promise.reject("No SDP"),
+ ]).then(
+ sdp =>
+ sdputils.checkSdpAfterEndOfTrickle(
+ sdp,
+ test.testOptions,
+ test.pcRemote.label
+ ),
+ () =>
+ info(
+ "pcRemote: Gathering is not complete yet, skipping post-gathering SDP check"
+ )
+ );
+ }
+ },
+];
+
+function PC_LOCAL_REMOVE_ALL_BUT_H264_FROM_OFFER(test) {
+ isnot(
+ test.originalOffer.sdp.search("H264/90000"),
+ -1,
+ "H.264 should be present in the SDP offer"
+ );
+ test.originalOffer.sdp = sdputils.removeCodec(
+ sdputils.removeCodec(
+ sdputils.removeCodec(test.originalOffer.sdp, 120),
+ 121,
+ 97
+ )
+ );
+ info("Updated H264 only offer: " + JSON.stringify(test.originalOffer));
+}
+
+function PC_LOCAL_REMOVE_BUNDLE_FROM_OFFER(test) {
+ test.originalOffer.sdp = sdputils.removeBundle(test.originalOffer.sdp);
+ info("Updated no bundle offer: " + JSON.stringify(test.originalOffer));
+}
+
+function PC_LOCAL_REMOVE_RTCPMUX_FROM_OFFER(test) {
+ test.originalOffer.sdp = sdputils.removeRtcpMux(test.originalOffer.sdp);
+ info("Updated no RTCP-Mux offer: " + JSON.stringify(test.originalOffer));
+}
+
+function PC_LOCAL_REMOVE_SSRC_FROM_OFFER(test) {
+ test.originalOffer.sdp = sdputils.removeSSRCs(test.originalOffer.sdp);
+ info("Updated no SSRCs offer: " + JSON.stringify(test.originalOffer));
+}
+
+function PC_REMOTE_REMOVE_SSRC_FROM_ANSWER(test) {
+ test.originalAnswer.sdp = sdputils.removeSSRCs(test.originalAnswer.sdp);
+ info("Updated no SSRCs answer: " + JSON.stringify(test.originalAnswer));
+}
+
+var addRenegotiation = (chain, commands, checks) => {
+ chain.append(commands);
+ chain.append(commandsPeerConnectionOfferAnswer);
+ if (checks) {
+ chain.append(checks);
+ }
+};
+
+var addRenegotiationAnswerer = (chain, commands, checks) => {
+ chain.append(function SWAP_PC_LOCAL_PC_REMOTE(test) {
+ var temp = test.pcLocal;
+ test.pcLocal = test.pcRemote;
+ test.pcRemote = temp;
+ });
+ addRenegotiation(chain, commands, checks);
+};
diff --git a/dom/media/webrtc/tests/mochitests/test_1488832.html b/dom/media/webrtc/tests/mochitests/test_1488832.html
new file mode 100644
index 0000000000..8798994b24
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_1488832.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<iframe id="testframe"></iframe>
+<script>
+"use strict";
+
+createHTML({
+ title: "gUM shutdown race",
+ bug: "1488832"
+});
+
+runTest(async () => {
+ testframe.srcdoc = `
+ <html>
+ <head>
+ <script>
+ function start() {
+ for (let i = 0; i < 16; i++) {
+ window.navigator.mediaDevices.getUserMedia({video: true})
+ setTimeout('location.reload()', 100)
+ }
+ }
+ document.addEventListener('DOMContentLoaded', start)
+ </` + `script>
+ </head>
+ </html>`;
+
+ await wait(10000);
+ testframe.srcdoc = "";
+});
+</script>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_1717318.html b/dom/media/webrtc/tests/mochitests/test_1717318.html
new file mode 100644
index 0000000000..425bd29e7e
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_1717318.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>PC construct with no global object (bug 1717318)</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+"use strict";
+
+// nsIArray is not special here, it could be pretty much any interface.
+// We do this outside the try block just in case someday the interface is
+// removed.
+const dummyInterface = SpecialPowers.Components.interfaces.nsIArray;
+ok(dummyInterface, "nsIArray should exist");
+try {
+ // Just don't crash.
+ SpecialPowers.Components.classes["@mozilla.org/peerconnection;1"]
+ .createInstance(dummyInterface);
+} catch (e) {}
+
+</script>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_a_noOp.html b/dom/media/webrtc/tests/mochitests/test_a_noOp.html
new file mode 100644
index 0000000000..971f5d7666
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_a_noOp.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1264772
+-->
+<head>
+ <title>Test for Bug 1264772</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1264772">Mozilla Bug 1264772</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+/** Test for Bug 1264772 **/
+// The WebRTC tests seem to have more problems with intermittents (at
+// least on Android) if they run first in a test run. This is a dummy test
+// to ensure that the browser is ready prior to running any actual WebRTC
+// tests.
+//
+// Note: mochitests are run in alphabetical order, so it is not sufficient
+// for this test to appear first in the manifest.
+ok(true, "test passed");
+
+</script>
+</pre>
+</body>
diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudio.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudio.html
new file mode 100644
index 0000000000..06ca9562ad
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudio.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "796895",
+ title: "Basic data channel audio connection"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ addInitialDataChannel(test.chain);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideo.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideo.html
new file mode 100644
index 0000000000..ea534ca2e7
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideo.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "796891",
+ title: "Basic data channel audio/video connection"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ addInitialDataChannel(test.chain);
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideoCombined.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideoCombined.html
new file mode 100644
index 0000000000..d5409986ec
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideoCombined.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "796891",
+ title: "Basic data channel audio/video connection"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ addInitialDataChannel(test.chain);
+ test.setMediaConstraints([{audio: true, video: true}],
+ [{audio: true, video: true}]);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideoNoBundle.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideoNoBundle.html
new file mode 100644
index 0000000000..7dc22d86ad
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicAudioVideoNoBundle.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1016476",
+ title: "Basic data channel audio/video connection without bundle"
+ });
+
+var test;
+runNetworkTest(function (options) {
+ options = options || { };
+ options.bundle = false;
+ test = new PeerConnectionTest(options);
+ addInitialDataChannel(test.chain);
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+ return test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_basicDataOnly.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicDataOnly.html
new file mode 100644
index 0000000000..98e72f7a21
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicDataOnly.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "796894",
+ title: "Basic datachannel only connection"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ addInitialDataChannel(test.chain);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_basicVideo.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicVideo.html
new file mode 100644
index 0000000000..90f2d7caff
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_basicVideo.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "796889",
+ title: "Basic data channel video connection"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ addInitialDataChannel(test.chain);
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_bug1013809.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_bug1013809.html
new file mode 100644
index 0000000000..e36caebab4
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_bug1013809.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "796895",
+ title: "Basic data channel audio connection"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ addInitialDataChannel(test.chain);
+ var sld = test.chain.remove("PC_REMOTE_SET_LOCAL_DESCRIPTION");
+ test.chain.insertAfter("PC_LOCAL_SET_REMOTE_DESCRIPTION", sld);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_dataOnlyBufferedAmountLow.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_dataOnlyBufferedAmountLow.html
new file mode 100644
index 0000000000..26767e0865
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_dataOnlyBufferedAmountLow.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1051685",
+ title: "Verify bufferedAmountLowThreshold works"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ addInitialDataChannel(test.chain);
+ test.chain.insertAfter('PC_REMOTE_CHECK_ICE_CONNECTIONS', commandsCheckLargeXfer);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_dtlsVersions.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_dtlsVersions.html
new file mode 100644
index 0000000000..6f0cbc5d3d
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_dtlsVersions.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1284103",
+ title: "Test basic data channel audio connection for supported DTLS versions"
+ });
+
+ async function testDtlsVersion(options, version) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.peerconnection.dtls.version.min", version],
+ ["media.peerconnection.dtls.version.max", version]
+ ]
+ });
+
+ const test = new PeerConnectionTest(options);
+ addInitialDataChannel(test.chain);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+
+ await test.run();
+ }
+
+ runNetworkTest(async (options) => {
+ // 770 = DTLS 1.0, 771 = DTLS 1.2, 772 = DTLS 1.3
+ for (var version = 770; version <= 772; version++) {
+ await testDtlsVersion(options, version);
+ }
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_hostnameObfuscation.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_hostnameObfuscation.html
new file mode 100644
index 0000000000..d0790fb9c9
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_hostnameObfuscation.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1592620",
+ title: "Blocklist to disable hostname obfuscation"
+ });
+
+ async function testBlocklist(options, blocklistEntry, shouldBeObfuscated) {
+ let test = new PeerConnectionTest(options);
+ addInitialDataChannel(test.chain);
+
+ if (blocklistEntry !== null) {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["media.peerconnection.ice.obfuscate_host_addresses.blocklist",
+ blocklistEntry]
+ ]
+ });
+ }
+
+ test.chain.insertAfter('PC_LOCAL_WAIT_FOR_ICE_CONNECTED', [
+ async function CHECK_LOCAL_CANDIDATES() {
+ const stats = await test.pcLocal.getStats();
+ stats.forEach(s => {
+ if (s.type === 'local-candidate') {
+ if (shouldBeObfuscated) {
+ ok(s.address.includes(".local"), "address should be obfuscated");
+ } else {
+ ok(!s.address.includes(".local"), "address should not be obfuscated");
+ }
+ }
+ });
+ }]);
+
+ await test.run();
+ }
+
+ runNetworkTest(async (options) => {
+ await SpecialPowers.pushPrefEnv({
+ set: [["media.peerconnection.ice.obfuscate_host_addresses", true]]
+ });
+ await testBlocklist(options, null, true);
+ await testBlocklist(options, "", true);
+ await testBlocklist(options, "example.com", true);
+ await testBlocklist(options, "mochi.test", false);
+ await testBlocklist(options, "example.com,mochi.test", false);
+ await testBlocklist(options, "*.test", false);
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_noOffer.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_noOffer.html
new file mode 100644
index 0000000000..a6e9fa5214
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_noOffer.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "856319",
+ title: "Don't offer m=application unless createDataChannel is called first"
+ });
+
+ runNetworkTest(async function () {
+ const pc = new RTCPeerConnection();
+
+ // necessary to circumvent bug 864109
+ const options = { offerToReceiveAudio: true };
+
+ const errorCallback = generateErrorCallback();
+ try {
+ const offer = await pc.createOffer(options);
+ ok(!offer.sdp.includes("m=application"),
+ "m=application is not contained in the SDP");
+ } catch(e) {
+ errorCallback(e);
+ }
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_dataChannel_stats.html b/dom/media/webrtc/tests/mochitests/test_dataChannel_stats.html
new file mode 100644
index 0000000000..4498e2d23a
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_dataChannel_stats.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1218356",
+ title: "DataChannel stats"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ test.chain.remove('PC_LOCAL_CHECK_STATS');
+ test.chain.remove('PC_REMOTE_CHECK_STATS');
+ addInitialDataChannel(test.chain);
+ test.chain.removeAfter("PC_REMOTE_CHECK_ICE_CONNECTIONS");
+ test.chain.insertAfter("PC_REMOTE_CHECK_ICE_CONNECTIONS",
+ async function TEST_DATA_CHANNEL_STATS(test) {
+ const channel = test.pcLocal.dataChannels[0];
+ test.pcRemote.dataChannels[0].onbufferedamountlow = () => {};
+ test.pcRemote.dataChannels[0].send(`Sending Message`);
+ channel.onbufferedamountlow = () => {};
+ const event = await new Promise( r => channel.onmessage = r);
+ info(`Received message: "${event.data}"`);
+ const report = await test.pcLocal.getStats();
+ info(`Received Stats ${JSON.stringify([...report.values()], null, 2)}\n`);
+ const stats = [...report.values()].find(block => block.type == "data-channel");
+ info(`DataChannel stats ${JSON.stringify(stats, null, 2)}`);
+ is(stats.label, channel.label, 'DataChannel stats has correct label');
+ is(stats.protocol, channel.protocol,
+ 'DataChannel stats has correct protocol');
+ is(stats.dataChannelIdentifier, channel.id,
+ 'DataChannel stats has correct dataChannelIdentifier');
+ is(stats.state, channel.readyState, 'DataChannel has correct state');
+ is(stats.bytesReceived, 15, 'DataChannel has correct bytesReceived');
+ is(stats.bytesSent, 0, 'DataChannel has correct bytesSent');
+ is(stats.messagesReceived, 1,
+ 'DataChannel has correct messagesReceived');
+ is(stats.messagesSent, 0, 'DataChannel has correct messagesSent');
+ });
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_defaultAudioConstraints.html b/dom/media/webrtc/tests/mochitests/test_defaultAudioConstraints.html
new file mode 100644
index 0000000000..8e0db48fff
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_defaultAudioConstraints.html
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+"use strict";
+
+createHTML({
+ title: "Test that the audio constraints that observe at the audio constraints we expect.",
+ bug: "1509842"
+});
+
+runTest(async () => {
+ // We need a real device to get a MediaEngine supporting constraints
+ let audioDevice = SpecialPowers.getCharPref("media.audio_loopback_dev", "");
+ if (!audioDevice) {
+ todo(false, "No device set by framework. Try --use-test-media-devices");
+ return;
+ }
+
+ // Get a gUM track with the default settings, check that they are what we
+ // expect.
+ let stream = await navigator.mediaDevices.getUserMedia({ audio: true });
+ let track = stream.getAudioTracks()[0];
+ let defaultSettings = track.getSettings();
+
+ is(defaultSettings.echoCancellation, true,
+ "Echo cancellation should be ON by default.");
+ is(defaultSettings.noiseSuppression, true,
+ "Noise suppression should be ON by default.");
+ is(defaultSettings.autoGainControl, true,
+ "Automatic gain control should be ON by default.");
+
+ track.stop();
+
+ // This is UA-dependant, and belongs in a Mochitest, not in a WPT.
+ // When a gUM track has been requested with `echoCancellation` OFF, check that
+ // `noiseSuppression` and `autoGainControl` are off as well.
+ stream =
+ await navigator.mediaDevices.getUserMedia({audio:{echoCancellation: false}});
+ track = stream.getAudioTracks()[0];
+ defaultSettings = track.getSettings();
+
+ is(defaultSettings.echoCancellation, false,
+ "Echo cancellation should be OFF when requested.");
+ is(defaultSettings.noiseSuppression, false,
+ `Noise suppression should be OFF when echoCancellation is the only
+ constraint and is OFF.`);
+ is(defaultSettings.autoGainControl, false,
+ `Automatic gain control should be OFF when echoCancellation is the only
+ constraint and is OFF.`);
+
+ track.stop();
+
+ // When a gUM track has been requested with `echoCancellation` OFF, check that
+ // `noiseSuppression` and `autoGainControl` are not OFF as well if another
+ // constraint has been specified.
+ stream =
+ await navigator.mediaDevices.getUserMedia({audio:{echoCancellation: false,
+ autoGainControl: true}});
+ track = stream.getAudioTracks()[0];
+ defaultSettings = track.getSettings();
+
+ is(defaultSettings.echoCancellation, false,
+ "Echo cancellation should be OFF when requested.");
+ is(defaultSettings.noiseSuppression, false,
+ `Noise suppression should be OFF when echoCancellation is OFF and another
+ constraint has been specified.`);
+ is(defaultSettings.autoGainControl, true,
+ "Auto gain control should be ON when requested.");
+
+ track.stop();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_enumerateDevices.html b/dom/media/webrtc/tests/mochitests/test_enumerateDevices.html
new file mode 100644
index 0000000000..48bec0006a
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_enumerateDevices.html
@@ -0,0 +1,141 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({ title: "Run enumerateDevices code", bug: "1046245" });
+/**
+ Tests covering enumerateDevices API and deviceId constraint. Exercise code.
+*/
+
+async function mustSucceedWithStream(msg, f) {
+ try {
+ const stream = await f();
+ for (const track of stream.getTracks()) {
+ track.stop();
+ }
+ ok(true, msg + " must succeed");
+ } catch (e) {
+ is(e.name, null, msg + " must succeed: " + e.message);
+ }
+}
+
+async function mustFailWith(msg, reason, constraint, f) {
+ try {
+ await f();
+ ok(false, msg + " must fail");
+ } catch(e) {
+ is(e.name, reason, msg + " must fail: " + e.message);
+ if (constraint) {
+ is(e.constraint, constraint, msg + " must fail w/correct constraint.");
+ }
+ }
+}
+
+const gUM = c => navigator.mediaDevices.getUserMedia(c);
+
+const kinds = ["videoinput", "audioinput", "audiooutput"];
+
+function validateDevice({kind, label, deviceId, groupId}) {
+ ok(kinds.includes(kind), "Known device kind");
+ is(deviceId.length, 44, "deviceId length id as expected for Firefox");
+ ok(label.length !== undefined, "Device label: " + label);
+ isnot(groupId, "", "groupId must be present.");
+}
+
+runTest(async () => {
+ await pushPrefs(["media.navigator.streams.fake", true]);
+
+ // Validate enumerated devices after gUM.
+ for (const track of (await gUM({video: true, audio: true})).getTracks()) {
+ track.stop();
+ }
+
+ let devices = await navigator.mediaDevices.enumerateDevices();
+ ok(devices.length, "At least one device found");
+ const jsoned = JSON.parse(JSON.stringify(devices));
+ is(jsoned[0].kind, devices[0].kind, "kind survived serializer");
+ is(jsoned[0].deviceId, devices[0].deviceId, "deviceId survived serializer");
+ for (const device of devices) {
+ validateDevice(device);
+ if (device.kind == "audiooutput") continue;
+ // Test deviceId constraint
+ let deviceId = device.deviceId;
+ let constraints = (device.kind == "videoinput") ? { video: { deviceId } }
+ : { audio: { deviceId } };
+ for (const track of (await gUM(constraints)).getTracks()) {
+ is(typeof(track.label), "string", "Track label is a string");
+ is(track.label, device.label, "Track label is the device label");
+ track.stop();
+ }
+ }
+
+ const unknownId = "unknown9qHr8B0JIbcHlbl9xR+jMbZZ8WyoPfpCXPfc=";
+
+ // Check deviceId failure paths for video.
+
+ await mustSucceedWithStream("unknown plain deviceId on video",
+ () => gUM({ video: { deviceId: unknownId } }));
+ await mustSucceedWithStream("unknown plain deviceId on audio",
+ () => gUM({ audio: { deviceId: unknownId } }));
+ await mustFailWith("unknown exact deviceId on video",
+ "OverconstrainedError", "deviceId",
+ () => gUM({ video: { deviceId: { exact: unknownId } } }));
+ await mustFailWith("unknown exact deviceId on audio",
+ "OverconstrainedError", "deviceId",
+ () => gUM({ audio: { deviceId: { exact: unknownId } } }));
+
+ // Check that deviceIds are stable for same origin and differ across origins.
+
+ const path = "/tests/dom/media/webrtc/tests/mochitests/test_enumerateDevices_iframe.html";
+ const origins = ["https://example.com", "https://test1.example.com"];
+ info(window.location);
+
+ const haveDevicesMap = new Promise(resolve => {
+ const map = new Map();
+ window.addEventListener("message", ({origin, data}) => {
+ ok(origins.includes(origin), "Got message from expected origin");
+ map.set(origin, JSON.parse(data));
+ if (map.size < origins.length) return;
+ resolve(map);
+ });
+ });
+
+ await Promise.all(origins.map(origin => {
+ const iframe = document.createElement("iframe");
+ iframe.src = origin + path;
+ iframe.allow = "camera;microphone;speaker-selection";
+ info(iframe.src);
+ document.documentElement.appendChild(iframe);
+ return new Promise(resolve => iframe.onload = resolve);
+ }));
+ let devicesMap = await haveDevicesMap;
+ let [sameOriginDevices, differentOriginDevices] = origins.map(o => devicesMap.get(o));
+
+ is(sameOriginDevices.length, devices.length, "same origin same devices");
+ is(differentOriginDevices.length, devices.length, "cross origin same devices");
+ [...sameOriginDevices, ...differentOriginDevices].forEach(d => validateDevice(d));
+
+ for (const device of sameOriginDevices) {
+ ok(devices.find(d => d.deviceId == device.deviceId),
+ "Same origin deviceId for " + device.label + " must match");
+ }
+ for (const device of differentOriginDevices) {
+ ok(!devices.find(d => d.deviceId == device.deviceId),
+ "Different origin deviceId for " + device.label + " must be different");
+ }
+
+ // Check the special case of no devices found.
+ await pushPrefs(["media.navigator.streams.fake", false],
+ ["media.audio_loopback_dev", "none"],
+ ["media.video_loopback_dev", "none"]);
+ devices = await navigator.mediaDevices.enumerateDevices();
+ is(devices.length, 0, "No devices");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_enumerateDevices_getUserMediaFake.html b/dom/media/webrtc/tests/mochitests/test_enumerateDevices_getUserMediaFake.html
new file mode 100644
index 0000000000..7952bcba1b
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_enumerateDevices_getUserMediaFake.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+ <script>
+"use strict";
+
+createHTML({
+ title: "Test labeled devices or speakers aren't exposed in enumerateDevices() after fake getUserMedia()",
+ bug: "1743524"
+});
+
+runTest(async () => {
+ await pushPrefs(
+ ["media.setsinkid.enabled", true],
+ // This test uses real devices because fake devices are not grouped with
+ // audiooutput devices.
+ ["media.navigator.streams.fake", false]);
+ const devices = navigator.mediaDevices;
+ {
+ // `fake:true` means that getUserMedia() resolves without any permission
+ // check, and so this should not be sufficient to expose real device info.
+ const stream = await devices.getUserMedia({ audio: true, fake: true });
+ stream.getTracks()[0].stop();
+ const list = await devices.enumerateDevices();
+ const labeledDevices = list.filter(({label}) => label != "");
+ is(labeledDevices.length, 0, "must be zero labeled devices after fake gUM");
+ const outputDevices = list.filter(({kind}) => kind == "audiooutput");
+ is(outputDevices.length, 0, "must be zero output devices after fake gUM");
+ }
+ {
+ // Check without `fake:true` to verify assumptions about existing devices.
+ let stream;
+ try {
+ stream = await devices.getUserMedia({ audio: true });
+ stream.getTracks()[0].stop();
+ } catch (e) {
+ if (e.name == "NotFoundError" &&
+ navigator.userAgent.includes("Mac OS X")) {
+ todo(false, "Expecting no real audioinput device on Mac test machines");
+ return;
+ }
+ throw e;
+ }
+ {
+ const list = await devices.enumerateDevices();
+ const audioDevices = list.filter(({kind}) => kind.includes("audio"));
+ ok(audioDevices.length, "have audio devices after real gUM");
+ const unlabeledAudioDevices = audioDevices.filter(({label}) => !label);
+ is(unlabeledAudioDevices.length, 0,
+ "must be zero unlabeled audio devices after real gUM");
+
+ const outputDevices = list.filter(({kind}) => kind == "audiooutput");
+ isnot(outputDevices.length, 0, "have output devices after real gUM");
+ }
+ }
+});
+ </script>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_enumerateDevices_iframe.html b/dom/media/webrtc/tests/mochitests/test_enumerateDevices_iframe.html
new file mode 100644
index 0000000000..beea3a4f97
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_enumerateDevices_iframe.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+<pre id="test">
+<script type="application/javascript">
+/**
+ Runs inside iframe in test_enumerateDevices.html.
+*/
+
+const pushPrefs = (...p) => SpecialPowers.pushPrefEnv({set: p});
+const gUM = c => navigator.mediaDevices.getUserMedia(c);
+
+(async () => {
+ await pushPrefs(["media.navigator.streams.fake", true]);
+
+ // Validate enumerated devices after gUM.
+ for (const track of (await gUM({video: true, audio: true})).getTracks()) {
+ track.stop();
+ }
+
+ const devices = await navigator.mediaDevices.enumerateDevices();
+ parent.postMessage(JSON.stringify(devices), "https://example.com:443");
+
+})().catch(e => setTimeout(() => { throw e; }));
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_enumerateDevices_iframe_pre_gum.html b/dom/media/webrtc/tests/mochitests/test_enumerateDevices_iframe_pre_gum.html
new file mode 100644
index 0000000000..f2dc2d1f65
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_enumerateDevices_iframe_pre_gum.html
@@ -0,0 +1,22 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+<pre id="test">
+<script type="application/javascript">
+/**
+ Runs inside iframe in test_enumerateDevices_legacy.html.
+*/
+
+const pushPrefs = (...p) => SpecialPowers.pushPrefEnv({set: p});
+
+(async () => {
+ await pushPrefs(["media.navigator.streams.fake", true]);
+
+ const devices = await navigator.mediaDevices.enumerateDevices();
+ parent.postMessage(JSON.stringify(devices), "https://example.com:443");
+
+})().catch(e => setTimeout(() => { throw e; }));
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_enumerateDevices_legacy.html b/dom/media/webrtc/tests/mochitests/test_enumerateDevices_legacy.html
new file mode 100644
index 0000000000..c599f2b599
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_enumerateDevices_legacy.html
@@ -0,0 +1,147 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({ title: "Run enumerateDevices code", bug: "1046245" });
+/**
+ This is a modified copy of test_enumerateDevices.html testing the
+ enumerateDevices() legacy version and deviceId constraint.
+*/
+
+async function mustSucceedWithStream(msg, f) {
+ try {
+ const stream = await f();
+ for (const track of stream.getTracks()) {
+ track.stop();
+ }
+ ok(true, msg + " must succeed");
+ } catch (e) {
+ is(e.name, null, msg + " must succeed: " + e.message);
+ }
+}
+
+async function mustFailWith(msg, reason, constraint, f) {
+ try {
+ await f();
+ ok(false, msg + " must fail");
+ } catch(e) {
+ is(e.name, reason, msg + " must fail: " + e.message);
+ if (constraint) {
+ is(e.constraint, constraint, msg + " must fail w/correct constraint.");
+ }
+ }
+}
+
+const gUM = c => navigator.mediaDevices.getUserMedia(c);
+
+const kinds = ["videoinput", "audioinput", "audiooutput"];
+
+function validateDevice({kind, label, deviceId, groupId}) {
+ ok(kinds.includes(kind), "Known device kind");
+ is(deviceId.length, 44, "deviceId length id as expected for Firefox");
+ ok(label.length !== undefined, "Device label: " + label);
+ isnot(groupId, "", "groupId must be present.");
+}
+
+runTest(async () => {
+ await pushPrefs(["media.navigator.streams.fake", true],
+ ["media.devices.enumerate.legacy.enabled", true]);
+
+ // Validate enumerated devices before gUM (legacy).
+
+ let devices = await navigator.mediaDevices.enumerateDevices();
+ ok(devices.length, "At least one device found");
+ const jsoned = JSON.parse(JSON.stringify(devices));
+ is(jsoned[0].kind, devices[0].kind, "kind survived serializer");
+ is(jsoned[0].deviceId, devices[0].deviceId, "deviceId survived serializer");
+ for (const device of devices) {
+ validateDevice(device);
+ if (device.kind == "audiooutput") continue;
+ is(device.label, "", "Device label is empty");
+ // Test deviceId constraint
+ let deviceId = device.deviceId;
+ let constraints = (device.kind == "videoinput") ? { video: { deviceId } }
+ : { audio: { deviceId } };
+ let namedDevices;
+ for (const track of (await gUM(constraints)).getTracks()) {
+ is(typeof(track.label), "string", "Track label is a string");
+ isnot(track.label.length, 0, "Track label is not empty");
+ if (!namedDevices) {
+ namedDevices = await navigator.mediaDevices.enumerateDevices();
+ }
+ const namedDevice = namedDevices.find(d => d.deviceId == device.deviceId);
+ is(track.label, namedDevice.label, "Track label is the device label");
+ track.stop();
+ }
+ }
+
+ const unknownId = "unknown9qHr8B0JIbcHlbl9xR+jMbZZ8WyoPfpCXPfc=";
+
+ // Check deviceId failure paths for video.
+
+ await mustSucceedWithStream("unknown plain deviceId on video",
+ () => gUM({ video: { deviceId: unknownId } }));
+ await mustSucceedWithStream("unknown plain deviceId on audio",
+ () => gUM({ audio: { deviceId: unknownId } }));
+ await mustFailWith("unknown exact deviceId on video",
+ "OverconstrainedError", "deviceId",
+ () => gUM({ video: { deviceId: { exact: unknownId } } }));
+ await mustFailWith("unknown exact deviceId on audio",
+ "OverconstrainedError", "deviceId",
+ () => gUM({ audio: { deviceId: { exact: unknownId } } }));
+
+ // Check that deviceIds are stable for same origin and differ across origins.
+
+ const path = "/tests/dom/media/webrtc/tests/mochitests/test_enumerateDevices_iframe_pre_gum.html";
+ const origins = ["https://example.com", "https://test1.example.com"];
+ info(window.location);
+
+ const haveDevicesMap = new Promise(resolve => {
+ const map = new Map();
+ window.addEventListener("message", ({origin, data}) => {
+ ok(origins.includes(origin), "Got message from expected origin");
+ map.set(origin, JSON.parse(data));
+ if (map.size < origins.length) return;
+ resolve(map);
+ });
+ });
+
+ await Promise.all(origins.map(origin => {
+ const iframe = document.createElement("iframe");
+ iframe.src = origin + path;
+ iframe.allow = "camera;microphone;speaker-selection";
+ info(iframe.src);
+ document.documentElement.appendChild(iframe);
+ return new Promise(resolve => iframe.onload = resolve);
+ }));
+ let devicesMap = await haveDevicesMap;
+ let [sameOriginDevices, differentOriginDevices] = origins.map(o => devicesMap.get(o));
+
+ is(sameOriginDevices.length, devices.length, "same origin same devices");
+ is(differentOriginDevices.length, devices.length, "cross origin same devices");
+ [...sameOriginDevices, ...differentOriginDevices].forEach(d => validateDevice(d));
+
+ for (const device of sameOriginDevices) {
+ ok(devices.find(d => d.deviceId == device.deviceId),
+ "Same origin deviceId for " + device.label + " must match");
+ }
+ for (const device of differentOriginDevices) {
+ ok(!devices.find(d => d.deviceId == device.deviceId),
+ "Different origin deviceId for " + device.label + " must be different");
+ }
+
+ // Check the special case of no devices found.
+ await pushPrefs(["media.navigator.streams.fake", false],
+ ["media.audio_loopback_dev", "none"],
+ ["media.video_loopback_dev", "none"]);
+ devices = await navigator.mediaDevices.enumerateDevices();
+ is(devices.length, 0, "No devices");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_enumerateDevices_navigation.html b/dom/media/webrtc/tests/mochitests/test_enumerateDevices_navigation.html
new file mode 100644
index 0000000000..bf7650223f
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_enumerateDevices_navigation.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<iframe id="iframe" srcdoc="<script>
+ window.enumerateDevices = () =>
+ navigator.mediaDevices.enumerateDevices();
+ </script>"
+ width="100%" height="50%" frameborder="1">
+</iframe>
+<pre id="test">
+<script type="application/javascript">
+createHTML({ title: "Suspend enumerateDevices code ", bug: "1479840" });
+/**
+ This test covers the case that the enumerateDevices method is suspended by
+ navigating away the current window. In order to implement that the enumeration
+ is executed in an iframe which is cleared before the enumeration has been resolved
+*/
+
+runTest(async () => {
+ // Run enumerate devices and mesure the time it will take.
+ const start = new Date().getTime();
+ try {
+ await iframe.contentWindow.enumerateDevices();
+ } catch (e) {
+ info("Failed to enumerate devices, error: " + e);
+ }
+ const elapsed = new Date().getTime() - start;
+
+ // Run again and navigate away. Expected to remain pending.
+ let p = iframe.contentWindow.enumerateDevices()
+ p.then( devices => {
+ ok(false, "Enumerate devices promise resolved unexpectedly, found " + devices.length + " devices.");
+ })
+ .catch ( error => {
+ ok(false, "Enumerate devices promise rejected unexpectedly: " + error);
+ });
+ iframe.srcdoc = "";
+
+ // Wait enough time.
+ try {
+ await timeout(p, 5 * elapsed, "timeout");
+ ok(false, "Enumerate devices promise resolved unexpectedly");
+ } catch (e) {
+ is(e.message, "timeout", "We should time out without enumerateDevices rejecting");
+ }
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_fingerprinting_resistance.html b/dom/media/webrtc/tests/mochitests/test_fingerprinting_resistance.html
new file mode 100644
index 0000000000..7e9cd5a219
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_fingerprinting_resistance.html
@@ -0,0 +1,112 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="utf-8">
+<script src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<script>
+/* global SimpleTest SpecialPowers */
+
+async function testEnumerateDevices(expectDevices) {
+ let devices = await navigator.mediaDevices.enumerateDevices();
+ if (!expectDevices) {
+ SimpleTest.is(devices.length, 0, "testEnumerateDevices: No devices");
+ return;
+ }
+ let cams = devices.filter((device) => device.kind == "videoinput");
+ let mics = devices.filter((device) => device.kind == "audioinput");
+ SimpleTest.ok((cams.length == 1) && (mics.length == 1),
+ "testEnumerateDevices: a microphone and a camera");
+}
+
+async function testGetUserMedia(expectDevices) {
+ const constraints = [
+ {audio: true},
+ {video: true},
+ {audio: true, video: true},
+ {video: {width: {min: 1e9}}}, // impossible
+ {audio: {channelCount: {exact: 1e3}}}, // impossible
+ ];
+ for (let constraint of constraints) {
+ let message = "getUserMedia(" + JSON.stringify(constraint) + ")";
+ try {
+ let stream = await navigator.mediaDevices.getUserMedia(constraint);
+ SimpleTest.ok(expectDevices, message + " resolved");
+ if (!expectDevices) {
+ continue;
+ }
+
+ // We only do testGetUserMedia(true) when privacy.resistFingerprinting
+ // is true, test if MediaStreamTrack.label is spoofed.
+ for (let track of stream.getTracks()) {
+ switch (track.kind) {
+ case "audio":
+ SimpleTest.is(track.label, "Internal Microphone", "AudioStreamTrack.label");
+ break;
+ case "video":
+ SimpleTest.is(track.label, "Internal Camera", "VideoStreamTrack.label");
+ break;
+ default:
+ SimpleTest.ok(false, "Unknown kind: " + track.kind);
+ break;
+ }
+ track.stop();
+ }
+ } catch (e) {
+ if (!expectDevices) {
+ SimpleTest.is(e.name, "NotAllowedError", message + " throws NotAllowedError");
+ } else {
+ SimpleTest.ok(false, message + " failed: " + e);
+ }
+ }
+ }
+}
+
+async function testDevices() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.resistFingerprinting", true],
+ ["media.navigator.streams.fake", true]
+ ]
+ });
+ await testEnumerateDevices(true); // should list a microphone and a camera
+ await testGetUserMedia(true); // should get audio and video streams
+}
+
+async function testNoDevices() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.resistFingerprinting", false],
+ ["media.navigator.permission.device", false],
+ ["media.navigator.streams.fake", false],
+ ["media.audio_loopback_dev", "foo"],
+ ["media.video_loopback_dev", "bar"]
+ ]
+ });
+ await testEnumerateDevices(false); // should list nothing
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["privacy.resistFingerprinting", true]
+ ]
+ });
+ await testEnumerateDevices(true); // should list a microphone and a camera
+ await testGetUserMedia(false); // should reject with NotAllowedError
+}
+
+createHTML({
+ title: "Neutralize the threat of fingerprinting of media devices API when 'privacy.resistFingerprinting' is true",
+ bug: "1372073"
+});
+
+runTest(async () => {
+ // Make sure enumerateDevices and getUserMedia work when
+ // privacy.resistFingerprinting is true.
+ await testDevices();
+
+ // Test that absence of devices can't be detected.
+ await testNoDevices();
+});
+</script>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_forceSampleRate.html b/dom/media/webrtc/tests/mochitests/test_forceSampleRate.html
new file mode 100644
index 0000000000..c5a9820aaa
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_forceSampleRate.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test the pref media.cubeb.force_sample_rate</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+const WEIRD_SAMPLE_RATE = 44101;
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({"set": [
+ ["media.cubeb.force_sample_rate", WEIRD_SAMPLE_RATE]
+]}).then(function() {
+ var ac = new AudioContext();
+ is(ac.sampleRate, WEIRD_SAMPLE_RATE, "Forced sample-rate set successfully.");
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_GC_MediaStream.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_GC_MediaStream.html
new file mode 100644
index 0000000000..5aa0e64947
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_GC_MediaStream.html
@@ -0,0 +1,59 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ "use strict";
+
+ createHTML({
+ title: "MediaStreams can be garbage collected",
+ bug: "1407542"
+ });
+
+ let SpecialStream = SpecialPowers.wrap(MediaStream);
+
+ async function testGC(stream, numCopies, copy) {
+ let startStreams = await SpecialStream.countUnderlyingStreams();
+
+ let copies = new Array(numCopies).fill(0).map(() => copy(stream));
+ ok(await SpecialStream.countUnderlyingStreams() > startStreams,
+ "MediaStreamTrack constructor creates more underlying streams");
+
+ copies = [];
+ await new Promise(r => SpecialPowers.exactGC(r));
+ is(await SpecialStream.countUnderlyingStreams(), startStreams,
+ "MediaStreamTracks should have been collected");
+ }
+
+ runTest(async () => {
+ // We do not need LoopbackTone because it is not used
+ // and creates extra streams that affect the result
+ DISABLE_LOOPBACK_TONE = true;
+
+ let gUMStream = await getUserMedia({video: true});
+ info("Testing GC of track-array constructor with cloned tracks");
+ await testGC(gUMStream, 10, s => new MediaStream(s.getTracks().map(t => t.clone())));
+
+ info("Testing GC of empty constructor plus addTrack with cloned tracks");
+ await testGC(gUMStream, 10, s => {
+ let s2 = new MediaStream();
+ s.getTracks().forEach(t => s2.addTrack(t.clone()));
+ return s2;
+ });
+
+ info("Testing GC of cloned stream");
+ await testGC(gUMStream, 10, s => s.clone());
+
+ info("Testing GC of gUM stream");
+ gUMStream = null;
+ await new Promise(r => SpecialPowers.exactGC(r));
+ is(await SpecialStream.countUnderlyingStreams(), 0,
+ "Original gUM stream should be collectable");
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_active_autoplay.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_active_autoplay.html
new file mode 100644
index 0000000000..c1a39cdd4c
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_active_autoplay.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<video id="testAutoplay" autoplay></video>
+<script type="application/javascript">
+"use strict";
+
+const video = document.getElementById("testAutoplay");
+var stream;
+var otherVideoTrack;
+var otherAudioTrack;
+
+createHTML({
+ title: "MediaStream can be autoplayed in media element after going inactive and then active",
+ bug: "1208316"
+});
+
+runTest(() => getUserMedia({audio: true, video: true}).then(s => {
+ stream = s;
+ otherVideoTrack = stream.getVideoTracks()[0].clone();
+ otherAudioTrack = stream.getAudioTracks()[0].clone();
+
+ video.srcObject = stream;
+ return haveEvent(video, "playing", wait(5000, new Error("Timeout")));
+})
+.then(() => {
+ ok(!video.ended, "Video element should be playing after adding a gUM stream");
+ stream.getTracks().forEach(t => t.stop());
+ return haveEvent(video, "ended", wait(5000, new Error("Timeout")));
+})
+.then(() => {
+ ok(video.ended, "Video element should be ended");
+ stream.addTrack(otherVideoTrack);
+ return haveEvent(video, "playing", wait(5000, new Error("Timeout")));
+})
+.then(() => {
+ ok(!video.ended, "Video element should be playing after adding a video track");
+ stream.getTracks().forEach(t => t.stop());
+ return haveEvent(video, "ended", wait(5000, new Error("Timeout")));
+})
+.then(() => {
+ ok(video.ended, "Video element should be ended");
+ stream.addTrack(otherAudioTrack);
+ return haveEvent(video, "playing", wait(5000, new Error("Timeout")));
+})
+.then(() => {
+ ok(!video.ended, "Video element should be playing after adding a audio track");
+ stream.getTracks().forEach(t => t.stop());
+ return haveEvent(video, "ended", wait(5000, new Error("Timeout")));
+})
+.then(() => {
+ ok(video.ended, "Video element should be ended");
+}));
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_addTrackRemoveTrack.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_addTrackRemoveTrack.html
new file mode 100644
index 0000000000..27dad2519f
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_addTrackRemoveTrack.html
@@ -0,0 +1,169 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ "use strict";
+
+ createHTML({
+ title: "MediaStream's addTrack() and removeTrack() with getUserMedia streams Test",
+ bug: "1103188"
+ });
+
+ runTest(() => Promise.resolve()
+ .then(() => getUserMedia({audio: true})).then(stream =>
+ getUserMedia({video: true}).then(otherStream => {
+ info("Test addTrack()ing a video track to an audio-only gUM stream");
+ var track = stream.getTracks()[0];
+ var otherTrack = otherStream.getTracks()[0];
+
+ stream.addTrack(track);
+ checkMediaStreamContains(stream, [track], "Re-added audio");
+
+ stream.addTrack(otherTrack);
+ checkMediaStreamContains(stream, [track, otherTrack], "Added video");
+
+ var testElem = createMediaElement('video', 'testAddTrackAudioVideo');
+ var playback = new MediaStreamPlayback(testElem, stream);
+ return playback.playMedia(false);
+ }))
+ .then(() => getUserMedia({video: true})).then(stream =>
+ getUserMedia({video: true}).then(otherStream => {
+ info("Test addTrack()ing a video track to a video-only gUM stream");
+ var track = stream.getTracks()[0];
+ var otherTrack = otherStream.getTracks()[0];
+
+ stream.addTrack(track);
+ checkMediaStreamContains(stream, [track], "Re-added video");
+
+ stream.addTrack(otherTrack);
+ checkMediaStreamContains(stream, [track, otherTrack], "Added video");
+
+ var test = createMediaElement('video', 'testAddTrackDoubleVideo');
+ var playback = new MediaStreamPlayback(test, stream);
+ return playback.playMedia(false);
+ }))
+ .then(() => getUserMedia({video: true})).then(stream =>
+ getUserMedia({video: true}).then(otherStream => {
+ info("Test removeTrack() existing and added video tracks from a video-only gUM stream");
+ var track = stream.getTracks()[0];
+ var otherTrack = otherStream.getTracks()[0];
+
+ stream.removeTrack(otherTrack);
+ checkMediaStreamContains(stream, [track], "Removed non-existing video");
+
+ stream.addTrack(otherTrack);
+ checkMediaStreamContains(stream, [track, otherTrack], "Added video");
+
+ stream.removeTrack(otherTrack);
+ checkMediaStreamContains(stream, [track], "Removed added video");
+
+ stream.removeTrack(otherTrack);
+ checkMediaStreamContains(stream, [track], "Re-removed added video");
+
+ stream.removeTrack(track);
+ checkMediaStreamContains(stream, [], "Removed original video");
+
+ var elem = createMediaElement('video', 'testRemoveAllVideo');
+ var loadeddata = false;
+ elem.onloadeddata = () => { loadeddata = true; elem.onloadeddata = null; };
+ elem.srcObject = stream;
+ elem.play();
+ return wait(500).then(() => {
+ ok(!loadeddata, "Stream without tracks shall not raise 'loadeddata' on media element");
+ elem.pause();
+ elem.srcObject = null;
+ })
+ .then(() => {
+ stream.addTrack(track);
+ checkMediaStreamContains(stream, [track], "Re-added added-then-removed track");
+ var playback = new MediaStreamPlayback(elem, stream);
+ return playback.playMedia(false);
+ })
+ .then(() => otherTrack.stop());
+ }))
+ .then(() => getUserMedia({ audio: true })).then(audioStream =>
+ getUserMedia({ video: true }).then(videoStream => {
+ info("Test adding track and removing the original");
+ var audioTrack = audioStream.getTracks()[0];
+ var videoTrack = videoStream.getTracks()[0];
+ videoStream.removeTrack(videoTrack);
+ audioStream.addTrack(videoTrack);
+
+ checkMediaStreamContains(videoStream, [], "1, Removed original track");
+ checkMediaStreamContains(audioStream, [audioTrack, videoTrack],
+ "2, Added external track");
+
+ var elem = createMediaElement('video', 'testAddRemoveOriginalTrackVideo');
+ var playback = new MediaStreamPlayback(elem, audioStream);
+ return playback.playMedia(false);
+ }))
+ .then(() => getUserMedia({ audio: true, video: true })).then(stream => {
+ info("Test removing stopped tracks");
+ stream.getTracks().forEach(t => {
+ t.stop();
+ stream.removeTrack(t);
+ });
+ checkMediaStreamContains(stream, [], "Removed stopped tracks");
+ })
+ .then(() => {
+ var ac = new AudioContext();
+
+ var osc1k = createOscillatorStream(ac, 1000);
+ var audioTrack1k = osc1k.getTracks()[0];
+
+ var osc5k = createOscillatorStream(ac, 5000);
+ var audioTrack5k = osc5k.getTracks()[0];
+
+ var osc10k = createOscillatorStream(ac, 10000);
+ var audioTrack10k = osc10k.getTracks()[0];
+
+ var stream = osc1k;
+ return Promise.resolve().then(() => {
+ info("Analysing audio output with original 1k track");
+ var analyser = new AudioStreamAnalyser(ac, stream);
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(1000)] > 200 &&
+ array[analyser.binIndexForFrequency(5000)] < 50 &&
+ array[analyser.binIndexForFrequency(10000)] < 50);
+ }).then(() => {
+ info("Analysing audio output with removed original 1k track and added 5k track");
+ stream.removeTrack(audioTrack1k);
+ stream.addTrack(audioTrack5k);
+ var analyser = new AudioStreamAnalyser(ac, stream);
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(1000)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] > 200 &&
+ array[analyser.binIndexForFrequency(10000)] < 50);
+ }).then(() => {
+ info("Analysing audio output with removed 5k track and added 10k track");
+ stream.removeTrack(audioTrack5k);
+ stream.addTrack(audioTrack10k);
+ var analyser = new AudioStreamAnalyser(ac, stream);
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(1000)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] < 50 &&
+ array[analyser.binIndexForFrequency(10000)] > 200);
+ }).then(() => {
+ info("Analysing audio output with re-added 1k, 5k and added 10k tracks");
+ stream.addTrack(audioTrack1k);
+ stream.addTrack(audioTrack5k);
+ var analyser = new AudioStreamAnalyser(ac, stream);
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(1000)] > 200 &&
+ array[analyser.binIndexForFrequency(2500)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] > 200 &&
+ array[analyser.binIndexForFrequency(7500)] < 50 &&
+ array[analyser.binIndexForFrequency(10000)] > 200 &&
+ array[analyser.binIndexForFrequency(11000)] < 50);
+ });
+ }));
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_addtrack_removetrack_events.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_addtrack_removetrack_events.html
new file mode 100644
index 0000000000..833653ebb2
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_addtrack_removetrack_events.html
@@ -0,0 +1,110 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+"use strict";
+
+createHTML({
+ title: "MediaStream's 'addtrack' and 'removetrack' events shouldn't fire on manual operations",
+ bug: "1208328"
+});
+
+var spinEventLoop = () => new Promise(r => setTimeout(r, 0));
+
+var stream;
+var clone;
+var newStream;
+var tracks = [];
+
+var addTrack = track => {
+ info("Adding track " + track.id);
+ stream.addTrack(track);
+};
+var removeTrack = track => {
+ info("Removing track " + track.id);
+ stream.removeTrack(track);
+};
+var stopTrack = track => {
+ if (track.readyState == "live") {
+ info("Stopping track " + track.id);
+ }
+ track.stop();
+};
+
+runTest(() => getUserMedia({audio: true, video: true})
+ .then(s => {
+ stream = s;
+ clone = s.clone();
+ stream.addEventListener("addtrack", function onAddtrack(event) {
+ ok(false, "addtrack fired unexpectedly for track " + event.track.id);
+ });
+ stream.addEventListener("removetrack", function onRemovetrack(event) {
+ ok(false, "removetrack fired unexpectedly for track " + event.track.id);
+ });
+
+ return getUserMedia({audio: true, video: true});
+ })
+ .then(s => {
+ newStream = s;
+ info("Stopping an original track");
+ stopTrack(stream.getTracks()[0]);
+
+ return spinEventLoop();
+ })
+ .then(() => {
+ info("Removing original tracks");
+ stream.getTracks().forEach(t => (stream.removeTrack(t), tracks.push(t)));
+
+ return spinEventLoop();
+ })
+ .then(() => {
+ info("Adding other gUM tracks");
+ newStream.getTracks().forEach(t => addTrack(t))
+
+ return spinEventLoop();
+ })
+ .then(() => {
+ info("Adding cloned tracks");
+ let clone = stream.clone();
+ clone.getTracks().forEach(t => addTrack(t));
+
+ return spinEventLoop();
+ })
+ .then(() => {
+ info("Removing a clone");
+ removeTrack(clone.getTracks()[0]);
+
+ return spinEventLoop();
+ })
+ .then(() => {
+ info("Stopping clones");
+ clone.getTracks().forEach(t => stopTrack(t));
+
+ return spinEventLoop();
+ })
+ .then(() => {
+ info("Stopping originals");
+ stream.getTracks().forEach(t => stopTrack(t));
+ tracks.forEach(t => stopTrack(t));
+
+ return spinEventLoop();
+ })
+ .then(() => {
+ info("Removing remaining tracks");
+ stream.getTracks().forEach(t => removeTrack(t));
+
+ return spinEventLoop();
+ })
+ .then(() => {
+ // Test MediaStreamTrackEvent required args here.
+ mustThrowWith("MediaStreamTrackEvent without required args",
+ "TypeError", () => new MediaStreamTrackEvent("addtrack", {}));
+ }));
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioCapture.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioCapture.html
new file mode 100644
index 0000000000..2cc649a321
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioCapture.html
@@ -0,0 +1,104 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test AudioCapture </title>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script>
+
+(async () => {
+ // Get an opus file containing a sine wave at maximum amplitude, of duration
+ // `lengthSeconds`, and of frequency `frequency`.
+ async function getSineWaveFile(frequency, lengthSeconds) {
+ const off = new OfflineAudioContext(1, lengthSeconds * 48000, 48000);
+ const osc = off.createOscillator();
+ const rec = new MediaRecorder(off.destination,
+ {mimeType: "audio/ogg; codecs=opus"});
+ osc.frequency.value = frequency;
+ osc.connect(off.destination);
+ osc.start();
+ rec.start();
+ off.startRendering();
+ const {data} = await new Promise(r => rec.ondataavailable = r);
+ return data;
+ }
+
+ await createHTML({
+ bug: "1156472",
+ title: "Test AudioCapture with regular HTMLMediaElement, AudioContext, " +
+ "and HTMLMediaElement playing a MediaStream",
+ visible: true
+ });
+
+ await runTestWhenReady(async () => {
+ /**
+ * Get two HTMLMediaElements:
+ * - One playing a sine tone from a blob (of an opus file created on the fly)
+ * - One being the output for an AudioContext's OscillatorNode, connected to
+ * a MediaSourceDestinationNode.
+ *
+ * Also, use the AudioContext playing through its AudioDestinationNode another
+ * tone, using another OscillatorNode.
+ *
+ * Capture the output of the document, feed that back into the AudioContext,
+ * with an AnalyserNode, and check the frequency content to make sure we
+ * have recorded the three sources.
+ *
+ * The three sine tones have frequencies far apart from each other, so that we
+ * can check that the spectrum of the capture stream contains three
+ * components with a high magnitude.
+ */
+ const wavtone = createMediaElement("audio", "WaveTone");
+ const acTone = createMediaElement("audio", "audioContextTone");
+ const ac = new AudioContext();
+
+ const oscThroughMediaElement = ac.createOscillator();
+ oscThroughMediaElement.frequency.value = 1000;
+ const oscThroughAudioDestinationNode = ac.createOscillator();
+ oscThroughAudioDestinationNode.frequency.value = 5000;
+ const msDest = ac.createMediaStreamDestination();
+
+ oscThroughMediaElement.connect(msDest);
+ oscThroughAudioDestinationNode.connect(ac.destination);
+
+ acTone.srcObject = msDest.stream;
+
+ const blob = await getSineWaveFile(10000, 10);
+ wavtone.src = URL.createObjectURL(blob);
+ oscThroughMediaElement.start();
+ oscThroughAudioDestinationNode.start();
+ wavtone.loop = true;
+ wavtone.play();
+ acTone.play();
+
+ const constraints = {audio: {mediaSource: "audioCapture"}};
+
+ const stream = await getUserMedia(constraints);
+ try {
+ const analyser = new AudioStreamAnalyser(ac, stream);
+ analyser.enableDebugCanvas();
+ await analyser.waitForAnalysisSuccess(array => {
+ // We want to find three frequency components here, around 1000, 5000
+ // and 10000Hz. Frequency are logarithmic. Also make sure we have low
+ // energy in between, not just a flat white noise.
+ return (array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(1000)] > 200 &&
+ array[analyser.binIndexForFrequency(2500)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] > 200 &&
+ array[analyser.binIndexForFrequency(7500)] < 50 &&
+ array[analyser.binIndexForFrequency(10000)] > 200);
+ });
+ } finally {
+ for (let t of stream.getTracks()) {
+ t.stop();
+ }
+ ac.close();
+ }
+ });
+})();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints.html
new file mode 100644
index 0000000000..162e83063a
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints.html
@@ -0,0 +1,93 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+"use strict";
+
+createHTML({
+ title: "Test that microphone getSettings report correct settings after applyConstraints",
+ bug: "1447982",
+});
+
+function testTrackAgainstAudioConstraints(track, audioConstraints) {
+ let constraints = track.getConstraints();
+ is(constraints.autoGainControl, audioConstraints.autoGainControl,
+ "Should report correct autoGainControl constraint");
+ is(constraints.echoCancellation, audioConstraints.echoCancellation,
+ "Should report correct echoCancellation constraint");
+ is(constraints.noiseSuppression, audioConstraints.noiseSuppression,
+ "Should report correct noiseSuppression constraint");
+
+ let settings = track.getSettings();
+ is(settings.autoGainControl, audioConstraints.autoGainControl,
+ "Should report correct autoGainControl setting");
+ is(settings.echoCancellation, audioConstraints.echoCancellation,
+ "Should report correct echoCancellation setting");
+ is(settings.noiseSuppression, audioConstraints.noiseSuppression,
+ "Should report correct noiseSuppression setting");
+}
+
+async function testAudioConstraints(track, audioConstraints) {
+ // We applyConstraints() first and do a fresh gUM later, to avoid
+ // testing multiple concurrent captures at different settings.
+
+ info(`Testing applying constraints ${JSON.stringify(audioConstraints)} ` +
+ `to track with settings ${JSON.stringify(track.getSettings())}`);
+ await track.applyConstraints(audioConstraints);
+ testTrackAgainstAudioConstraints(track, audioConstraints);
+
+ info("Testing fresh gUM request with audio constraints " +
+ JSON.stringify(audioConstraints));
+ let stream = await getUserMedia({audio: audioConstraints});
+ testTrackAgainstAudioConstraints(stream.getTracks()[0], audioConstraints);
+ stream.getTracks().forEach(t => t.stop());
+}
+
+runTest(async () => {
+ let audioDevice = SpecialPowers.getCharPref("media.audio_loopback_dev", "");
+ if (!audioDevice) {
+ ok(false, "No device set by framework. Try --use-test-media-devices");
+ return;
+ }
+
+ let supportedConstraints = navigator.mediaDevices.getSupportedConstraints();
+ is(supportedConstraints.autoGainControl, true,
+ "autoGainControl constraint should be supported");
+ is(supportedConstraints.echoCancellation, true,
+ "echoCancellation constraint should be supported");
+ is(supportedConstraints.noiseSuppression, true,
+ "noiseSuppression constraint should be supported");
+
+ let egn = (e, g, n) => ({
+ echoCancellation: e,
+ autoGainControl: g,
+ noiseSuppression: n
+ });
+
+ let stream = await getUserMedia({
+ audio: egn(true, true, true),
+ });
+ let track = stream.getTracks()[0];
+ let audioConstraintsToTest = [
+ egn(false, true, true),
+ egn(true, false, true),
+ egn(true, true, false),
+ egn(false, false, true),
+ egn(false, true, false),
+ egn(true, false, false),
+ egn(false, false, false),
+ egn(true, true, true),
+ ];
+ for (let audioConstraints of audioConstraintsToTest) {
+ await testAudioConstraints(track, audioConstraints);
+ }
+ track.stop();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints_concurrentIframes.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints_concurrentIframes.html
new file mode 100644
index 0000000000..d07dbc41f1
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints_concurrentIframes.html
@@ -0,0 +1,157 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ title: "getUserMedia in multiple iframes with different constraints",
+ bug: "1404977"
+});
+/**
+ * Verify that we can successfully call getUserMedia for the same device in
+ * multiple iframes concurrently. This is checked by creating a number of
+ * iframes and performing a separate getUserMedia call in each. We verify the
+ * stream returned by that call has the same constraints as requested both
+ * immediately after the call and after all gUM calls have been made. The test
+ * then verifies the streams can be played.
+ */
+runTest(async function() {
+ // Compare constraints and return a string with the differences in
+ // echoCancellation, autoGainControl, and noiseSuppression. The string
+ // will be empty if there are no differences.
+ function getConstraintDifferenceString(constraints, otherConstraints) {
+ let diffString = "";
+ if (constraints.echoCancellation != otherConstraints.echoCancellation) {
+ diffString += "echoCancellation different: " +
+ `${constraints.echoCancellation} != ${otherConstraints.echoCancellation}, `;
+ }
+ if (constraints.autoGainControl != otherConstraints.autoGainControl) {
+ diffString += "autoGainControl different: " +
+ `${constraints.autoGainControl} != ${otherConstraints.autoGainControl}, `;
+ }
+ if (constraints.noiseSuppression != otherConstraints.noiseSuppression) {
+ diffString += "noiseSuppression different: " +
+ `${constraints.noiseSuppression} != ${otherConstraints.noiseSuppression}, `;
+ }
+ // Replace trailing comma and space if any
+ return diffString.replace(/, $/, "");
+ }
+
+ // We need a real device to get a MediaEngine supporting constraints
+ let audioDevice = SpecialPowers.getCharPref("media.audio_loopback_dev", "");
+ if (!audioDevice) {
+ todo(false, "No device set by framework. Try --use-test-media-devices");
+ return;
+ }
+
+ let egn = (e, g, n) => ({
+ echoCancellation: e,
+ autoGainControl: g,
+ noiseSuppression: n
+ });
+
+ let allConstraintCombinations = [
+ egn(false, false, false),
+ egn(true, false, false),
+ egn(false, true, false),
+ egn(false, false, true),
+ egn(true, true, false),
+ egn(true, false, true),
+ egn(false, true, true),
+ egn(true, true, true),
+ ];
+
+ // TODO: We would like to be able to perform an arbitrary number of gUM calls
+ // at once, but issues with pulse and audio IPC mean on some systems we're
+ // limited to as few as 2 concurrent calls. To avoid issues we chunk test runs
+ // to only two calls at a time. The while, splice and GC lines can be removed,
+ // the extra scope removed and allConstraintCombinations can be renamed to
+ // constraintCombinations once this issue is resolved. See bug 1480489.
+ while (allConstraintCombinations.length) {
+ {
+ let constraintCombinations = allConstraintCombinations.splice(0, 2);
+ // Array to store objects that associate information used in our test such as
+ // constraints, iframes, gum streams, and various promises.
+ let testCases = [];
+
+ for (let constraints of constraintCombinations) {
+ let testCase = {requestedConstraints: constraints};
+ // Provide an id for logging, labeling related elements.
+ testCase.id = `testCase.` +
+ `e=${constraints.echoCancellation}.` +
+ `g=${constraints.noiseSuppression}.` +
+ `n=${constraints.noiseSuppression}`;
+ testCases.push(testCase);
+ testCase.iframe = document.createElement("iframe");
+ testCase.iframeLoadedPromise = new Promise((resolve, reject) => {
+ testCase.iframe.onload = () => { resolve(); };
+ });
+ document.body.appendChild(testCase.iframe);
+ }
+ is(testCases.length,
+ constraintCombinations.length,
+ "Should have created a testcase for each constraint");
+
+ // Wait for all iframes to be loaded
+ await Promise.all(testCases.map(tc => tc.iframeLoadedPromise));
+
+ // Start a tone at our top level page so the gUM calls will record something
+ // should we wish to verify their recording in future.
+ let tone = new LoopbackTone(new AudioContext, TEST_AUDIO_FREQ);
+ tone.start();
+
+ // One by one see if we can grab a gUM stream per iframe
+ for (let testCase of testCases) {
+ // Use normal gUM rather than our test helper as the test harness was
+ // not made to be used inside iframes.
+ testCase.gumStream =
+ await testCase.iframe.contentWindow.navigator.mediaDevices.getUserMedia({audio: testCase.requestedConstraints})
+ .catch(e => Promise.reject(`getUserMedia calls should not fail! Failed at ${testCase.id} with: ${e}!`));
+ let differenceString = getConstraintDifferenceString(
+ testCase.requestedConstraints,
+ testCase.gumStream.getAudioTracks()[0].getSettings());
+ ok(!differenceString,
+ `gUM stream for ${testCase.id} should have the same constraints as were ` +
+ `requested from gUM. Differences: ${differenceString}`);
+ }
+
+ // Once all streams are collected, make sure the constraints haven't been
+ // mutated by another gUM call.
+ for (let testCase of testCases) {
+ let differenceString = getConstraintDifferenceString(
+ testCase.requestedConstraints,
+ testCase.gumStream.getAudioTracks()[0].getSettings());
+ ok(!differenceString,
+ `gUM stream for ${testCase.id} should not have had constraints altered after ` +
+ `all gUM calls are done. Differences: ${differenceString}`);
+ }
+
+ // We do not currently have tests to verify the behaviour of the different
+ // constraints. Once we do we should do further verification here. See
+ // bug 1406372, bug 1406376, and bug 1406377.
+
+ for (let testCase of testCases) {
+ let testAudio = createMediaElement("audio", `testAudio.${testCase.id}`);
+ let playback = new MediaStreamPlayback(testAudio, testCase.gumStream);
+ await playback.playMediaWithoutStoppingTracks(false);
+ }
+
+ // Stop the tracks for each stream, we left them running above via
+ // playMediaWithoutStoppingTracks to make sure they can play concurrently.
+ for (let testCase of testCases) {
+ testCase.gumStream.getTracks().map(t => t.stop());
+ document.body.removeChild(testCase.iframe);
+ }
+
+ tone.stop();
+ }
+ await new Promise(r => SpecialPowers.exactGC(r));
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints_concurrentStreams.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints_concurrentStreams.html
new file mode 100644
index 0000000000..f5b5e784ea
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_audioConstraints_concurrentStreams.html
@@ -0,0 +1,123 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ title: "getUserMedia multiple times, concurrently, and with different constraints",
+ bug: "1404977"
+});
+/**
+ * Verify that we can successfully call getUserMedia multiple times for the
+ * same device, concurrently. This is checked by calling getUserMedia a number
+ * of times with different constraints. We verify that the stream returned by
+ * that call has the same constraints as requested both immediately after the
+ * call and after all gUM calls have been made. The test then verifies the
+ * streams can be played.
+ */
+runTest(async function() {
+ // Compare constraints and return a string with the differences in
+ // echoCancellation, autoGainControl, and noiseSuppression. The string
+ // will be empty if there are no differences.
+ function getConstraintDifferenceString(constraints, otherConstraints) {
+ let diffString = "";
+ if (constraints.echoCancellation != otherConstraints.echoCancellation) {
+ diffString += "echoCancellation different: " +
+ `${constraints.echoCancellation} != ${otherConstraints.echoCancellation}, `;
+ }
+ if (constraints.autoGainControl != otherConstraints.autoGainControl) {
+ diffString += "autoGainControl different: " +
+ `${constraints.autoGainControl} != ${otherConstraints.autoGainControl}, `;
+ }
+ if (constraints.noiseSuppression != otherConstraints.noiseSuppression) {
+ diffString += "noiseSuppression different: " +
+ `${constraints.noiseSuppression} != ${otherConstraints.noiseSuppression}, `;
+ }
+ // Replace trailing comma and space if any
+ return diffString.replace(/, $/, "");
+ }
+
+ // We need a real device to get a MediaEngine supporting constraints
+ let audioDevice = SpecialPowers.getCharPref("media.audio_loopback_dev", "");
+ if (!audioDevice) {
+ todo(false, "No device set by framework. Try --use-test-media-devices");
+ return;
+ }
+
+ let egn = (e, g, n) => ({
+ echoCancellation: e,
+ autoGainControl: g,
+ noiseSuppression: n
+ });
+
+ let constraintCombinations = [
+ egn(false, false, false),
+ egn(true, false, false),
+ egn(false, true, false),
+ egn(false, false, true),
+ egn(true, true, false),
+ egn(true, false, true),
+ egn(false, true, true),
+ egn(true, true, true),
+ ];
+
+ // Array to store objects that associate information used in our test such as
+ // constraints, gum streams, and various promises.
+ let testCases = [];
+
+ for (let constraints of constraintCombinations) {
+ let testCase = {requestedConstraints: constraints};
+ // Provide an id for logging, labeling related elements.
+ testCase.id = `testCase.` +
+ `e=${constraints.echoCancellation}.` +
+ `g=${constraints.noiseSuppression}.` +
+ `n=${constraints.noiseSuppression}`;
+ testCases.push(testCase);
+ testCase.gumStream =
+ await getUserMedia({audio: testCase.requestedConstraints})
+ .catch(e => Promise.reject(`getUserMedia calls should not fail! Failed at ${testCase.id} with: ${e}!`));
+ let differenceString = getConstraintDifferenceString(
+ testCase.requestedConstraints,
+ testCase.gumStream.getAudioTracks()[0].getSettings());
+ ok(!differenceString,
+ `gUM stream for ${testCase.id} should have the same constraints as were ` +
+ `requested from gUM. Differences: ${differenceString}`);
+ }
+ is(testCases.length,
+ constraintCombinations.length,
+ "Should have a stream for each constraint");
+
+ // Once all streams are collected, make sure the constraints haven't been
+ // mutated by another gUM call.
+ for (let testCase of testCases) {
+ let differenceString = getConstraintDifferenceString(
+ testCase.requestedConstraints,
+ testCase.gumStream.getAudioTracks()[0].getSettings());
+ ok(!differenceString,
+ `gUM stream for ${testCase.id} should not have had constraints altered after ` +
+ `all gUM calls are done. Differences: ${differenceString}`);
+ }
+
+ // We do not currently have tests to verify the behaviour of the different
+ // constraints. Once we do we should do further verificaiton here. See
+ // bug 1406372, bug 1406376, and bug 1406377.
+
+ for (let testCase of testCases) {
+ let testAudio = createMediaElement("audio", `testAudio.${testCase.id}`);
+ let playback = new MediaStreamPlayback(testAudio, testCase.gumStream);
+ await playback.playMediaWithoutStoppingTracks(false);
+ }
+
+ // Stop the tracks for each stream, we left them running above via
+ // playMediaWithoutStoppingTracks to make sure they can play concurrently.
+ for (let testCase of testCases) {
+ testCase.gumStream.getTracks().map(t => t.stop());
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicAudio.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicAudio.html
new file mode 100644
index 0000000000..b4775b4244
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicAudio.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({ title: "getUserMedia Basic Audio Test", bug: "781534" });
+ /**
+ * Run a test to verify that we can complete a start and stop media playback
+ * cycle for an audio MediaStream on an audio HTMLMediaElement.
+ */
+ runTest(function () {
+ var testAudio = createMediaElement('audio', 'testAudio');
+ var constraints = {audio: true};
+
+ return getUserMedia(constraints).then(stream => {
+ var playback = new MediaStreamPlayback(testAudio, stream);
+ return playback.playMedia(false);
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicAudio_loopback.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicAudio_loopback.html
new file mode 100644
index 0000000000..10bf669c00
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicAudio_loopback.html
@@ -0,0 +1,99 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script>
+ createHTML({
+ title: "getUserMedia Basic Audio Test Loopback",
+ bug: "1406350",
+ visible: true
+ });
+
+ /**
+ * Run a test to verify the use of LoopbackTone as audio input.
+ */
+ runTest(async () => {
+ if (!SpecialPowers.getCharPref("media.audio_loopback_dev", "")) {
+ todo(false, "No loopback device set by framework. Try --use-test-media-devices");
+ return;
+ }
+
+ // At this point DefaultLoopbackTone has been instantiated
+ // automatically on frequency TEST_AUDIO_FREQ (440 Hz). Verify
+ // that a tone is detected on that frequency.
+ info("Capturing at default frequency");
+ const stream = await getUserMedia({audio: true});
+
+ try {
+ const audioContext = new AudioContext();
+ const analyser = new AudioStreamAnalyser(audioContext, stream);
+ analyser.enableDebugCanvas();
+ await analyser.waitForAnalysisSuccess(array => {
+ // High energy on 1000 Hz low energy around that
+ const freg_50Hz = array[analyser.binIndexForFrequency(50)];
+ const freq = array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)];
+ const freq_2000Hz = array[analyser.binIndexForFrequency(2000)];
+
+ info("Analysing audio frequency - low:target:high = "
+ + freg_50Hz + ':' + freq + ':' + freq_2000Hz);
+ return freg_50Hz < 50 && freq > 200 && freq_2000Hz < 50;
+ });
+
+ // Use the LoopbackTone API to change the frequency of the default tone.
+ // Verify that a tone is detected on the new frequency (800 Hz).
+ info("Change loopback tone frequency");
+ DefaultLoopbackTone.changeFrequency(800);
+ await analyser.waitForAnalysisSuccess(array => {
+ const freg_50Hz = array[analyser.binIndexForFrequency(50)];
+ const freq = array[analyser.binIndexForFrequency(800)];
+ const freq_2000Hz = array[analyser.binIndexForFrequency(2000)];
+
+ info("Analysing audio frequency - low:target:high = "
+ + freg_50Hz + ':' + freq + ':' + freq_2000Hz);
+ return freg_50Hz < 50 && freq > 200 && freq_2000Hz < 50;
+ });
+
+ // Create a second tone at a different frequency.
+ // Verify that both tones are detected.
+ info("Multiple loopback tones");
+ DefaultLoopbackTone.changeFrequency(TEST_AUDIO_FREQ);
+ const second_tone = new LoopbackTone(audioContext, 2000);
+ second_tone.start();
+ await analyser.waitForAnalysisSuccess(array => {
+ const freg_50Hz = array[analyser.binIndexForFrequency(50)];
+ const freq = array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)];
+ const freq_2000Hz = array[analyser.binIndexForFrequency(2000)];
+ const freq_4000Hz = array[analyser.binIndexForFrequency(4000)];
+
+ info("Analysing audio frequency - low:target1:target2:high = "
+ + freg_50Hz + ':' + freq + ':' + freq_2000Hz + ':' + freq_4000Hz);
+ return freg_50Hz < 50 && freq > 200 && freq_2000Hz > 200 && freq_4000Hz < 50;
+ });
+
+ // Stop all tones and verify that there is no audio on the given frequencies.
+ info("Stop all loopback tones");
+ DefaultLoopbackTone.stop();
+ second_tone.stop()
+ await analyser.waitForAnalysisSuccess(array => {
+ const freg_50Hz = array[analyser.binIndexForFrequency(50)];
+ const freq = array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)];
+ const freq_2000Hz = array[analyser.binIndexForFrequency(2000)];
+
+ info("Analysing audio frequency - low:target:high = "
+ + freg_50Hz + ':' + freq + ':' + freq_2000Hz);
+ return freg_50Hz < 50 && freq < 50 && freq_2000Hz < 50;
+ });
+ } finally {
+ for (let t of stream.getTracks()) {
+ t.stop();
+ }
+ }
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicScreenshare.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicScreenshare.html
new file mode 100644
index 0000000000..cc73de77da
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicScreenshare.html
@@ -0,0 +1,260 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: "getUserMedia Basic Screenshare Test",
+ bug: "1211656",
+ visible: true,
+ });
+
+ const {AppConstants} =
+ SpecialPowers.ChromeUtils.import("resource://gre/modules/AppConstants.jsm");
+
+ // Since the MacOS backend captures in the wrong rgb color profile we need
+ // large thresholds there, and they vary greatly by color. We define
+ // thresholds per platform and color here to still allow the test to run.
+ // Since the colors used (red, green, blue, white) consist only of "pure"
+ // components (0 or 255 for each component), the high thresholds on Mac will
+ // still be able to catch an error where the image is corrupt, or if frames
+ // don't flow.
+ const thresholds = {
+ // macos: captures in the display rgb color profile, which we treat as
+ // sRGB, which is most likely wrong. These thresholds are needed currently
+ // in CI. See bug 1827606.
+ "macosx": { "red": 120, "green": 135, "blue": 35, "white": 10 },
+ // windows: rounding errors in 1) conversion to I420 (the capture),
+ // 2) downscaling, 3) conversion to RGB (for rendering).
+ "win": { "red": 5, "green": 5, "blue": 10, "white": 5 },
+ // linux: rounding errors in 1) conversion to I420 (the capture),
+ // 2) downscaling, 3) conversion to RGB (for rendering).
+ "linux": { "red": 5, "green": 5, "blue": 10, "white": 5 },
+ // android: we don't have a screen capture backend.
+ "android": { "red": 0, "green": 0, "blue": 0, "white": 0 },
+ // other: here just because it's supported by AppConstants.platform.
+ "other": { "red": 0, "green": 0, "blue": 0, "white": 0 },
+ };
+
+ const verifyScreenshare =
+ async (video, helper, upleft, upright, downleft, downright) => {
+ if (video.readyState < video.HAVE_CURRENT_DATA) {
+ info("Waiting for data");
+ await new Promise(r => video.onloadeddata = r);
+ }
+
+ // We assume video size will not change. Offsets help to account for a
+ // square fullscreen-canvas, while the screen is rectangular.
+ const offsetX = Math.max(0, video.videoWidth - video.videoHeight) / 2;
+ const offsetY = Math.max(0, video.videoHeight - video.videoWidth) / 2;
+
+ const verifyAround = async (internalX, internalY, color) => {
+ // Pick a couple of samples around a coordinate to check for a color.
+ // We check multiple rows and columns, to avoid most artifact issues.
+ let areaSamples = [
+ {dx: 0, dy: 0},
+ {dx: 1, dy: 3},
+ {dx: 8, dy: 5},
+ ];
+ const threshold = thresholds[AppConstants.platform][color.name];
+ for (let {dx, dy} of areaSamples) {
+ const x = offsetX + dx + internalX;
+ const y = offsetY + dy + internalY;
+ info(`Checking pixel (${[x,y]}) of total resolution `
+ + `${video.videoWidth}x${video.videoHeight} against ${color.name}.`);
+ let lastPixel = [-1, -1, -1, -1];
+ await helper.waitForPixel(video, px => {
+ lastPixel = Array.from(px);
+ return helper.isPixel(px, color, threshold);
+ }, {
+ offsetX: x,
+ offsetY: y,
+ cancel: wait(30000).then(_ =>
+ new Error(`Checking ${[x,y]} against ${color.name} timed out. ` +
+ `Got [${lastPixel}]. Threshold ${threshold}.`)),
+ });
+ ok(true, `Pixel (${[x,y]}) passed. Got [${lastPixel}].`);
+ }
+ };
+
+ const screenSizeSq = Math.min(video.videoWidth, video.videoHeight);
+
+ info("Waiting for upper left quadrant to become " + upleft.name);
+ await verifyAround(screenSizeSq / 4, screenSizeSq / 4, upleft);
+
+ info("Waiting for upper right quadrant to become " + upright.name);
+ await verifyAround(screenSizeSq * 3 / 4, screenSizeSq / 4, upright);
+
+ info("Waiting for lower left quadrant to become " + downleft.name);
+ await verifyAround(screenSizeSq / 4, screenSizeSq * 3 / 4, downleft);
+
+ info("Waiting for lower right quadrant to become " + downright.name);
+ await verifyAround(screenSizeSq * 3 / 4, screenSizeSq * 3 / 4, downright);
+ };
+
+ /**
+ * Run a test to verify that we can complete a start and stop media playback
+ * cycle for a screenshare MediaStream on a video HTMLMediaElement.
+ */
+ runTest(async function () {
+ await pushPrefs(
+ ["full-screen-api.enabled", true],
+ ["full-screen-api.allow-trusted-requests-only", false],
+ ["full-screen-api.transition-duration.enter", "0 0"],
+ ["full-screen-api.transition-duration.leave", "0 0"],
+ );
+
+ // Improve real estate for screenshots
+ const test = document.getElementById("test");
+ test.setAttribute("style", "height:0;margin:0;");
+ const display = document.getElementById("display");
+ display.setAttribute("style", "margin:0;");
+ const testVideo = createMediaElement('video', 'testVideo');
+ testVideo.removeAttribute("width");
+ testVideo.removeAttribute("height");
+ testVideo.setAttribute("style", "max-height:240px;");
+
+ const canvas = document.createElement("canvas");
+ canvas.width = canvas.height = 20;
+ document.getElementById("content").appendChild(canvas);
+ const draw = ([upleft, upright, downleft, downright]) => {
+ helper.drawColor(canvas, helper[upleft], {offsetX: 0, offsetY: 0});
+ helper.drawColor(canvas, helper[upright], {offsetX: 10, offsetY: 0});
+ helper.drawColor(canvas, helper[downleft], {offsetX: 0, offsetY: 10});
+ helper.drawColor(canvas, helper[downright], {offsetX: 10, offsetY: 10});
+ };
+ const helper = new CaptureStreamTestHelper2D(1, 1);
+
+ const doVerify = async (stream, [upleft, upright, downleft, downright]) => {
+ // Reset from potential earlier verification runs.
+ testVideo.srcObject = null;
+ const playback = new MediaStreamPlayback(testVideo, stream);
+ playback.startMedia();
+ await playback.verifyPlaying();
+ const settings = stream.getTracks()[0].getSettings();
+ is(settings.width, testVideo.videoWidth,
+ "Width setting should match video width");
+ is(settings.height, testVideo.videoHeight,
+ "Height setting should match video height");
+ await SpecialPowers.wrap(canvas).requestFullscreen();
+ try {
+ await verifyScreenshare(testVideo, helper, helper[upleft], helper[upright],
+ helper[downleft], helper[downright]);
+ } finally {
+ await playback.stopTracksForStreamInMediaPlayback();
+ await SpecialPowers.wrap(document).exitFullscreen();
+ // We wait a bit extra here to make sure we have completely left
+ // fullscreen when the --screenshot-on-fail screenshot is captured.
+ await wait(300);
+ }
+ };
+
+ info("Testing screenshare without constraints");
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ let stream = await getUserMedia({video: {mediaSource: "screen"}});
+ let settings = stream.getTracks()[0].getSettings();
+ ok(settings.width <= 8192,
+ `Width setting ${settings.width} should be set after gUM (or 0 per bug 1453247)`);
+ ok(settings.height <= 8192,
+ `Height setting ${settings.height} should be set after gUM (or 0 per bug 1453247)`);
+ let colors = ["red", "blue", "green", "white"];
+ draw(colors);
+ await doVerify(stream, colors);
+ const screenWidth = testVideo.videoWidth;
+ const screenHeight = testVideo.videoHeight;
+
+ info("Testing screenshare with size and framerate constraints");
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ for (const track of stream.getTracks()) {
+ track.stop();
+ }
+ stream = await getUserMedia({
+ video: {
+ mediaSource: 'screen',
+ width: {
+ min: '10',
+ max: '100'
+ },
+ height: {
+ min: '10',
+ max: '100'
+ },
+ frameRate: {
+ min: '10',
+ max: '15'
+ },
+ },
+ });
+ settings = stream.getTracks()[0].getSettings();
+ ok(settings.width == 0 || (settings.width >= 10 && settings.width <= 100),
+ `Width setting ${settings.width} should be correct after gUM (or 0 per bug 1453247)`);
+ ok(settings.height == 0 || (settings.height >= 10 && settings.height <= 100),
+ `Height setting ${settings.height} should be correct after gUM (or 0 per bug 1453247)`);
+ colors = ["green", "red", "white", "blue"];
+ draw(colors);
+ const streamClone = stream.clone();
+ await doVerify(streamClone, colors);
+ settings = stream.getTracks()[0].getSettings();
+ ok(settings.width >= 10 && settings.width <= 100,
+ `Width setting ${settings.width} should be within constraints`);
+ ok(settings.height >= 10 && settings.height <= 100,
+ `Height setting ${settings.height} should be within constraints`);
+ is(settings.width, testVideo.videoWidth,
+ "Width setting should match video width");
+ is(settings.height, testVideo.videoHeight,
+ "Height setting should match video height");
+ let expectedHeight = (screenHeight * settings.width) / screenWidth;
+ ok(Math.abs(expectedHeight - settings.height) <= 1,
+ "Aspect ratio after constrained gUM should be close enough");
+
+ info("Testing modifying screenshare with applyConstraints");
+ testVideo.srcObject = stream;
+ testVideo.play();
+ await new Promise(r => testVideo.onloadeddata = r);
+ const resize = haveEvent(
+ testVideo, "resize", wait(5000, new Error("Timeout waiting for resize")));
+ await stream.getVideoTracks()[0].applyConstraints({
+ mediaSource: 'screen',
+ width: 200,
+ height: 200,
+ frameRate: {
+ min: '5',
+ max: '10'
+ }
+ });
+ // getSettings() should report correct size as soon as applyConstraints()
+ // resolves - bug 1453259. Until fixed, check that we at least report
+ // something sane.
+ const newSettings = stream.getTracks()[0].getSettings();
+ ok(newSettings.width > settings.width && newSettings.width < screenWidth,
+ `Width setting ${newSettings.width} should have increased after applyConstraints`);
+ ok(newSettings.height > settings.height && newSettings.height < screenHeight,
+ `Height setting ${newSettings.height} should have increased after applyConstraints`);
+ await resize;
+ settings = stream.getTracks()[0].getSettings();
+ ok(settings.width > 100 && settings.width < screenWidth,
+ `Width setting ${settings.width} should have increased after first frame after applyConstraints`);
+ ok(settings.height > 100 && settings.height < screenHeight,
+ `Height setting ${settings.height} should have increased after first frame after applyConstraints`);
+ is(settings.width, testVideo.videoWidth,
+ "Width setting should match video width");
+ is(settings.height, testVideo.videoHeight,
+ "Height setting should match video height");
+ expectedHeight = (screenHeight * settings.width) / screenWidth;
+ ok(Math.abs(expectedHeight - settings.height) <= 1,
+ "Aspect ratio after applying constraints should be close enough");
+ colors = ["white", "green", "blue", "red"];
+ draw(colors);
+ await doVerify(stream, colors);
+ for (const track of [...stream.getTracks(), ...streamClone.getTracks()]) {
+ track.stop();
+ }
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicTabshare.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicTabshare.html
new file mode 100644
index 0000000000..635cf387d4
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicTabshare.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: "getUserMedia Basic Tabshare Test",
+ bug: "1193075"
+ });
+ /**
+ * Run a test to verify that we can complete a start and stop media playback
+ * cycle for a tabshare MediaStream on a video HTMLMediaElement.
+ *
+ * Additionally, exercise applyConstraints code for tabshare viewport offset.
+ */
+ runTest(function () {
+ var testVideo = createMediaElement('video', 'testVideo');
+
+ return Promise.resolve()
+ .then(() => pushPrefs(["media.getusermedia.browser.enabled", true]))
+ .then(() => {
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ return getUserMedia({
+ video: { mediaSource: "browser",
+ scrollWithPage: true },
+ });
+ })
+ .then(stream => {
+ var playback = new MediaStreamPlayback(testVideo, stream);
+ return playback.playMedia(false);
+ })
+ .then(() => getUserMedia({
+ video: {
+ mediaSource: "browser",
+ viewportOffsetX: 0,
+ viewportOffsetY: 0,
+ viewportWidth: 100,
+ viewportHeight: 100
+ },
+ }))
+ .then(stream => {
+ var playback = new MediaStreamPlayback(testVideo, stream);
+ playback.startMedia(false);
+ return playback.verifyPlaying()
+ .then(() => Promise.all([
+ () => testVideo.srcObject.getVideoTracks()[0].applyConstraints({
+ mediaSource: "browser",
+ viewportOffsetX: 10,
+ viewportOffsetY: 50,
+ viewportWidth: 90,
+ viewportHeight: 50
+ }),
+ () => listenUntil(testVideo, "resize", () => true)
+ ]))
+ .then(() => playback.verifyPlaying()) // still playing
+ .then(() => playback.stopTracksForStreamInMediaPlayback())
+ .then(() => playback.detachFromMediaElement());
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideo.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideo.html
new file mode 100644
index 0000000000..786d9f2e4b
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideo.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: "getUserMedia Basic Video Test",
+ bug: "781534"
+ });
+ /**
+ * Run a test to verify that we can complete a start and stop media playback
+ * cycle for an video MediaStream on a video HTMLMediaElement.
+ */
+ runTest(function () {
+ var testVideo = createMediaElement('video', 'testVideo');
+ var constraints = {video: true};
+
+ return getUserMedia(constraints).then(stream => {
+ var playback = new MediaStreamPlayback(testVideo, stream);
+ return playback.playMedia(false);
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideoAudio.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideoAudio.html
new file mode 100644
index 0000000000..5218bf7301
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideoAudio.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: "getUserMedia Basic Video & Audio Test",
+ bug: "781534"
+ });
+ /**
+ * Run a test to verify that we can complete a start and stop media playback
+ * cycle for a video and audio MediaStream on a video HTMLMediaElement.
+ */
+ runTest(function () {
+ var testVideoAudio = createMediaElement('video', 'testVideoAudio');
+ var constraints = {video: true, audio: true};
+
+ return getUserMedia(constraints).then(stream => {
+ var playback = new MediaStreamPlayback(testVideoAudio, stream);
+ return playback.playMedia(false);
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideo_playAfterLoadedmetadata.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideo_playAfterLoadedmetadata.html
new file mode 100644
index 0000000000..fbab1b4357
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicVideo_playAfterLoadedmetadata.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: "getUserMedia Basic Video shall receive 'loadedmetadata' without play()ing",
+ bug: "1149494"
+ });
+ /**
+ * Run a test to verify that we will always get 'loadedmetadata' from a video
+ * HTMLMediaElement playing a gUM MediaStream.
+ */
+ runTest(() => {
+ var testVideo = createMediaElement('video', 'testVideo');
+ var constraints = {video: true};
+
+ return getUserMedia(constraints).then(stream => {
+ var playback = new MediaStreamPlayback(testVideo, stream);
+ var video = playback.mediaElement;
+
+ video.srcObject = stream;
+ return new Promise(resolve => {
+ ok(playback.mediaElement.paused,
+ "Media element should be paused before play()ing");
+ video.addEventListener('loadedmetadata', function() {
+ ok(video.videoWidth > 0, "Expected nonzero video width");
+ ok(video.videoHeight > 0, "Expected nonzero video width");
+ resolve();
+ });
+ })
+ .then(() => stream.getTracks().forEach(t => t.stop()));
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicWindowshare.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicWindowshare.html
new file mode 100644
index 0000000000..7b27944bdc
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_basicWindowshare.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: "getUserMedia Basic Windowshare Test",
+ bug: "1038926"
+ });
+ /**
+ * Run a test to verify that we can complete a start and stop media playback
+ * cycle for an screenshare MediaStream on a video HTMLMediaElement.
+ */
+ runTest(async function () {
+ const testVideo = createMediaElement('video', 'testVideo');
+ const constraints = {
+ video: { mediaSource: "window" },
+ };
+
+ try {
+ await getUserMedia(constraints);
+ ok(false, "Should require user gesture");
+ } catch (e) {
+ is(e.name, "InvalidStateError");
+ }
+
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ const stream = await getUserMedia(constraints);
+ const playback = new MediaStreamPlayback(testVideo, stream);
+ return playback.playMedia(false);
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_bug1223696.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_bug1223696.html
new file mode 100644
index 0000000000..6af7b69d70
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_bug1223696.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ "use strict";
+
+ createHTML({
+ title: "Testing that removeTrack+addTrack of video tracks still render the correct track in a media element",
+ bug: "1223696",
+ visible: true
+ });
+
+ runTest(async function() {
+ const stream = await getUserMedia({audio:true, video: true});
+ info("Test addTrack()ing a video track to an audio-only gUM stream");
+
+ const video = createMediaElement("video", "test_video_track");
+ video.srcObject = stream;
+ video.play();
+
+ await haveEvent(video, "loadeddata", wait(5000, new Error("Timeout")));
+ info("loadeddata");
+
+ const removedTrack = stream.getVideoTracks()[0];
+ stream.removeTrack(removedTrack);
+
+ const h = new CaptureStreamTestHelper2D();
+ const emitter = new VideoFrameEmitter(h.grey, h.grey);
+ emitter.start();
+
+ stream.addTrack(emitter.stream().getVideoTracks()[0]);
+
+ checkMediaStreamContains(stream, [stream.getAudioTracks()[0],
+ emitter.stream().getVideoTracks()[0]]);
+
+ await h.pixelMustBecome(video, h.grey, {
+ threshold: 5,
+ infoString: "The canvas track should be rendered by the media element",
+ });
+
+ emitter.stop();
+ for (const t of [removedTrack, ...stream.getAudioTracks()]) {
+ t.stop();
+ }
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_callbacks.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_callbacks.html
new file mode 100644
index 0000000000..14c6cc7e7f
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_callbacks.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: "navigator.mozGetUserMedia Callback Test",
+ bug: "1119593"
+ });
+ /**
+ * Check that the old fashioned callback-based function works.
+ */
+ runTest(function () {
+ var testAudio = createMediaElement('audio', 'testAudio');
+ var constraints = {audio: true};
+
+ SimpleTest.waitForExplicitFinish();
+ return new Promise(resolve =>
+ navigator.mozGetUserMedia(constraints, stream => {
+ checkMediaStreamTracks(constraints, stream);
+
+ var playback = new MediaStreamPlayback(testAudio, stream);
+ return playback.playMedia(false)
+ .then(() => resolve(), generateErrorCallback());
+ }, generateErrorCallback())
+ );
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_constraints.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_constraints.html
new file mode 100644
index 0000000000..d6439ce9d6
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_constraints.html
@@ -0,0 +1,166 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script src="mediaStreamPlayback.js"></script>
+ <script src="constraints.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({ title: "Test getUserMedia constraints", bug: "882145" });
+/**
+ Tests covering gUM constraints API for audio, video and fake video. Exercise
+ successful parsing code and ensure that unknown required constraints and
+ overconstraining cases produce appropriate errors.
+*/
+var tests = [
+ // Each test here tests a different constraint or codepath.
+ { message: "unknown required constraint on video ignored",
+ constraints: { video: { somethingUnknown: { exact: 0 } } },
+ error: null },
+ { message: "unknown required constraint on audio ignored",
+ constraints: { audio: { somethingUnknown: { exact: 0 } } },
+ error: null },
+ { message: "audio overconstrained by facingMode ignored",
+ constraints: { audio: { facingMode: { exact: 'left' } } },
+ error: null },
+ { message: "full screensharing requires permission",
+ constraints: { video: { mediaSource: 'screen' } },
+ error: "NotAllowedError" },
+ { message: "application screensharing no longer exists",
+ constraints: { video: { mediaSource: 'application' } },
+ error: "OverconstrainedError" },
+ { message: "window screensharing requires permission",
+ constraints: { video: { mediaSource: 'window' } },
+ error: "NotAllowedError" },
+ { message: "browser screensharing requires permission",
+ constraints: { video: { mediaSource: 'browser' } },
+ error: "NotAllowedError" },
+ { message: "unknown mediaSource in video fails",
+ constraints: { video: { mediaSource: 'uncle' } },
+ error: "OverconstrainedError",
+ constraint: "mediaSource" },
+ { message: "unknown mediaSource in audio fails",
+ constraints: { audio: { mediaSource: 'uncle' } },
+ error: "OverconstrainedError",
+ constraint: "mediaSource" },
+ { message: "emtpy constraint fails",
+ constraints: { },
+ error: "TypeError" },
+ { message: "Triggering mock failure in default video device fails",
+ constraints: { video: { deviceId: 'bad device' } },
+ error: "NotReadableError" },
+ { message: "Triggering mock failure in default audio device fails",
+ constraints: { audio: { deviceId: 'bad device' } },
+ error: "NotReadableError" },
+ { message: "Success-path: optional video facingMode + audio ignoring facingMode",
+ constraints: { audio: { mediaSource: 'microphone',
+ facingMode: 'left',
+ foo: 0,
+ advanced: [{ facingMode: 'environment' },
+ { facingMode: 'user' },
+ { bar: 0 }] },
+ video: { mediaSource: 'camera',
+ foo: 0,
+ advanced: [{ facingMode: 'environment' },
+ { facingMode: ['user'] },
+ { facingMode: ['left', 'right', 'user'] },
+ { bar: 0 }] } },
+ error: null },
+ { message: "legacy facingMode ignored",
+ constraints: { video: { mandatory: { facingMode: 'left' } } },
+ error: null },
+];
+
+var mustSupport = [
+ 'width', 'height', 'frameRate', 'facingMode', 'deviceId', 'groupId',
+ 'echoCancellation', 'noiseSuppression', 'autoGainControl', 'channelCount',
+
+ // Yet to add:
+ // 'aspectRatio', 'volume', 'sampleRate', 'sampleSize', 'latency'
+
+ // http://fluffy.github.io/w3c-screen-share/#screen-based-video-constraints
+ // OBE by http://w3c.github.io/mediacapture-screen-share
+ 'mediaSource',
+
+ // Experimental https://bugzilla.mozilla.org/show_bug.cgi?id=1131568#c3
+ 'browserWindow', 'scrollWithPage',
+ 'viewportOffsetX', 'viewportOffsetY', 'viewportWidth', 'viewportHeight',
+];
+
+var mustFailWith = (msg, reason, constraint, f) =>
+ f().then(() => ok(false, msg + " must fail"), e => {
+ is(e.name, reason, msg + " must fail: " + e.message);
+ if (constraint !== undefined) {
+ is(e.constraint, constraint, msg + " must fail w/correct constraint.");
+ }
+ });
+
+/**
+ * Starts the test run by running through each constraint
+ * test by verifying that the right resolution and rejection is fired.
+ */
+
+runTest(() => pushPrefs(
+ // This test expects fake devices, particularly for the 'triggering mock
+ // failure *' steps. So explicitly disable loopback and setup fakes
+ ['media.audio_loopback_dev', ''],
+ ['media.video_loopback_dev', ''],
+ ['media.navigator.streams.fake', true]
+ )
+ .then(() => {
+ // Check supported constraints first.
+ var dict = navigator.mediaDevices.getSupportedConstraints();
+ var supported = Object.keys(dict);
+
+ mustSupport.forEach(key => ok(supported.includes(key) && dict[key],
+ "Supports " + key));
+
+ var unexpected = supported.filter(key => !mustSupport.includes(key));
+ is(unexpected.length, 0,
+ "Unanticipated support (please update test): " + unexpected);
+ })
+ .then(() => pushPrefs(["media.getusermedia.browser.enabled", false],
+ ["media.getusermedia.screensharing.enabled", false]))
+ .then(() => tests.reduce((p, test) => p.then(
+ () => {
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ return getUserMedia(test.constraints);
+ })
+ .then(stream => {
+ is(null, test.error, test.message);
+ stream.getTracks().forEach(t => t.stop());
+ }, e => {
+ is(e.name, test.error, test.message + ": " + e.message);
+ if (test.constraint) {
+ is(e.constraint, test.constraint,
+ test.message + " w/correct constraint.");
+ }
+ }), Promise.resolve()))
+ .then(() => getUserMedia({video: true, audio: true}))
+ .then(stream => stream.getVideoTracks()[0].applyConstraints({ width: 320 })
+ .then(() => stream.getAudioTracks()[0].applyConstraints({ }))
+ .then(() => {
+ stream.getTracks().forEach(track => track.stop());
+ ok(true, "applyConstraints code exercised");
+ }))
+ // TODO: Test outcome once fake devices support constraints (Bug 1088621)
+ .then(() => mustFailWith("applyConstraints fails on non-Gum tracks",
+ "OverconstrainedError", "",
+ () => (new AudioContext())
+ .createMediaStreamDestination().stream
+ .getAudioTracks()[0].applyConstraints()))
+ .then(() => mustFailWith(
+ "getUserMedia with unsatisfied required constraint",
+ "OverconstrainedError", "deviceId",
+ () => getUserMedia({ audio: true,
+ video: { deviceId: { exact: "unheardof" } } })))
+ .then(() => mustFailWith(
+ "getUserMedia with unsatisfied required constraint array",
+ "OverconstrainedError", "deviceId",
+ () => getUserMedia({ audio: true,
+ video: { deviceId: { exact: ["a", "b"] } } }))));
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_cubebDisabled.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_cubebDisabled.html
new file mode 100644
index 0000000000..54142aeb77
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_cubebDisabled.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: "getUserMedia with Cubeb Disabled Test",
+ bug: "1443525"
+ });
+ /**
+ * Run a test to verify we fail gracefully if we cannot fetch a cubeb context
+ * during a gUM call.
+ */
+ runTest(async function () {
+ info("Get user media with cubeb disabled starting");
+ // Push prefs to ensure no cubeb context and no fake streams.
+ await pushPrefs(["media.cubeb.force_null_context", true],
+ ["media.navigator.permission.device", false],
+ ["media.navigator.streams.fake", false]);
+
+ // Request audio only, to avoid cams
+ let constraints = {audio: true, video: false};
+ let stream;
+ try {
+ stream = await getUserMedia(constraints);
+ } catch (e) {
+ // We've got no audio backend, so we expect gUM to fail.
+ ok(e.name == "NotFoundError", "Expected NotFoundError due to no audio tracks!");
+ return;
+ }
+ // If we're not on android we should not have gotten a stream without a cubeb context!
+ ok(false, "getUserMedia not expected to succeed when cubeb is disabled, but it did!");
+ });
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_cubebDisabledFakeStreams.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_cubebDisabledFakeStreams.html
new file mode 100644
index 0000000000..f8150cc4c1
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_cubebDisabledFakeStreams.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: "getUserMedia fake stream with Cubeb Disabled Test",
+ bug: "1443525"
+ });
+ /**
+ * Run a test to verify we can still return a fake stream even if we cannot
+ * get a cubeb context. See also Bug 1434477
+ */
+ runTest(async function () {
+ info("Get user media with cubeb disabled and fake tracks starting");
+ // Push prefs to ensure no cubeb context and fake streams
+ await pushPrefs(["media.cubeb.force_null_context", true],
+ ["media.navigator.streams.fake", true],
+ ['media.audio_loopback_dev', '']);
+ let testAudio = createMediaElement('audio', 'testAudio');
+ // Request audio only, to avoid cams
+ let constraints = {audio: true, video: false};
+ let stream;
+ try {
+ stream = await getUserMedia(constraints);
+ } catch (e) {
+ // We've got no audio backend, so we expect gUM to fail
+ ok(false, `Did not expect to fail, but got ${e}`);
+ return;
+ }
+ ok(stream, "getUserMedia should get a stream!");
+ let playback = new MediaStreamPlayback(testAudio, stream);
+ return playback.playMedia(false);
+ });
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_getTrackById.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_getTrackById.html
new file mode 100644
index 0000000000..161bf631e3
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_getTrackById.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: "Basic getTrackById test of gUM stream",
+ bug: "1208390",
+ });
+
+ runTest(() => {
+ var constraints = {audio: true, video: true};
+ return getUserMedia(constraints).then(stream => {
+ is(stream.getTrackById(""), null,
+ "getTrackById of non-matching string should return null");
+
+ let audioTrack = stream.getAudioTracks()[0];
+ is(stream.getTrackById(audioTrack.id), audioTrack,
+ "getTrackById with matching id should return the track");
+
+ let videoTrack = stream.getVideoTracks()[0];
+ is(stream.getTrackById(videoTrack.id), videoTrack,
+ "getTrackById with matching id should return the track");
+
+ stream.removeTrack(audioTrack);
+ is(stream.getTrackById(audioTrack.id), null,
+ "getTrackById with id of removed track should return null");
+
+ let newStream = new MediaStream();
+ is(newStream.getTrackById(videoTrack.id), null,
+ "getTrackById with id of track in other stream should return null");
+
+ newStream.addTrack(audioTrack);
+ is(newStream.getTrackById(audioTrack.id), audioTrack,
+ "getTrackByid with matching id should return the track");
+
+ newStream.addTrack(videoTrack);
+ is(newStream.getTrackById(videoTrack.id), videoTrack,
+ "getTrackByid with matching id should return the track");
+ [audioTrack, videoTrack].forEach(t => t.stop());
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_gumWithinGum.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_gumWithinGum.html
new file mode 100644
index 0000000000..86a7aa5606
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_gumWithinGum.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({title: "getUserMedia within getUserMedia", bug: "822109" });
+ /**
+ * Run a test that we can complete a playback cycle for a video,
+ * then upon completion, do a playback cycle with audio, such that
+ * the audio gum call happens within the video gum call.
+ */
+ runTest(function () {
+ return getUserMedia({video: true})
+ .then(videoStream => {
+ var testVideo = createMediaElement('video', 'testVideo');
+ var videoPlayback = new MediaStreamPlayback(testVideo,
+ videoStream);
+
+ return videoPlayback.playMediaWithoutStoppingTracks(false)
+ .then(() => getUserMedia({audio: true}))
+ .then(audioStream => {
+ var testAudio = createMediaElement('audio', 'testAudio');
+ var audioPlayback = new MediaStreamPlayback(testAudio,
+ audioStream);
+
+ return audioPlayback.playMedia(false);
+ })
+ .then(() => videoStream.getTracks().forEach(t => t.stop()));
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_loadedmetadata.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_loadedmetadata.html
new file mode 100644
index 0000000000..d6efac4650
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_loadedmetadata.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: "getUserMedia in media element should have video dimensions on loadedmetadata",
+ bug: "1240478"
+ });
+ /**
+ * Tests that assigning a stream to a media element results in the
+ * "loadedmetadata" event without having to play() the media element.
+ *
+ * Also makes sure that the video size has been set on "loadedmetadata".
+ */
+ runTest(function () {
+ var v = document.createElement("video");
+ document.body.appendChild(v);
+ v.preload = "metadata";
+
+ var constraints = {video: true, audio: true};
+ return getUserMedia(constraints).then(stream => new Promise(resolve => {
+ v.srcObject = stream;
+ v.onloadedmetadata = () => {
+ isnot(v.videoWidth, 0, "videoWidth shall be set on 'loadedmetadata'");
+ isnot(v.videoHeight, 0, "videoHeight shall be set on 'loadedmetadata'");
+ resolve();
+ };
+ })
+ .then(() => stream.getTracks().forEach(t => t.stop())));
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_audio.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_audio.html
new file mode 100644
index 0000000000..3b9e00896c
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_audio.html
@@ -0,0 +1,116 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script>
+
+createHTML({
+ bug: "1259788",
+ title: "Test CaptureStream audio content on HTMLMediaElement playing a gUM MediaStream",
+ visible: true
+});
+
+var audioContext;
+var gUMAudioElement;
+var analyser;
+runTest(() => getUserMedia({audio: { echoCancellation: false }})
+ .then(stream => {
+ gUMAudioElement = createMediaElement("audio", "gUMAudio");
+ gUMAudioElement.srcObject = stream;
+
+ audioContext = new AudioContext();
+ info("Capturing");
+
+ analyser = new AudioStreamAnalyser(audioContext,
+ gUMAudioElement.mozCaptureStream());
+ analyser.enableDebugCanvas();
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)] > 200 &&
+ array[analyser.binIndexForFrequency(2500)] < 50);
+ })
+ .then(() => {
+ info("Audio flowing. Pausing.");
+ gUMAudioElement.pause();
+
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)] < 50 &&
+ array[analyser.binIndexForFrequency(2500)] < 50);
+ })
+ .then(() => {
+ info("Audio stopped flowing. Playing.");
+ gUMAudioElement.play();
+
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)] > 200 &&
+ array[analyser.binIndexForFrequency(2500)] < 50);
+ })
+ .then(() => {
+ info("Audio flowing. Removing source.");
+ var stream = gUMAudioElement.srcObject;
+ gUMAudioElement.srcObject = null;
+
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)] < 50 &&
+ array[analyser.binIndexForFrequency(2500)] < 50)
+ .then(() => stream);
+ })
+ .then(stream => {
+ info("Audio stopped flowing. Setting source.");
+ gUMAudioElement.srcObject = stream;
+
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)] > 200 &&
+ array[analyser.binIndexForFrequency(2500)] < 50);
+ })
+ .then(() => {
+ info("Audio flowing from new source. Adding a track.");
+ let oscillator = audioContext.createOscillator();
+ oscillator.type = 'sine';
+ oscillator.frequency.value = 2000;
+ oscillator.start();
+
+ let oscOut = audioContext.createMediaStreamDestination();
+ oscillator.connect(oscOut);
+
+ gUMAudioElement.srcObject.addTrack(oscOut.stream.getTracks()[0]);
+
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)] > 200 &&
+ array[analyser.binIndexForFrequency(1500)] < 50 &&
+ array[analyser.binIndexForFrequency(2000)] > 200 &&
+ array[analyser.binIndexForFrequency(2500)] < 50);
+ })
+ .then(() => {
+ info("Audio flowing from new track. Removing a track.");
+
+ const gUMTrack = gUMAudioElement.srcObject.getTracks()[0];
+ gUMAudioElement.srcObject.removeTrack(gUMTrack);
+
+ is(gUMAudioElement.srcObject.getTracks().length, 1,
+ "A track should have been removed");
+
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(TEST_AUDIO_FREQ)] < 50 &&
+ array[analyser.binIndexForFrequency(1500)] < 50 &&
+ array[analyser.binIndexForFrequency(2000)] > 200 &&
+ array[analyser.binIndexForFrequency(2500)] < 50)
+ .then(() => [gUMTrack, ...gUMAudioElement.srcObject.getTracks()]
+ .forEach(t => t.stop()));
+ })
+ .then(() => ok(true, "Test passed."))
+ .catch(e => ok(false, "Test failed: " + e + (e.stack ? "\n" + e.stack : ""))));
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_tracks.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_tracks.html
new file mode 100644
index 0000000000..a747e75de9
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_tracks.html
@@ -0,0 +1,179 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script>
+
+createHTML({
+ bug: "1259788",
+ title: "Test CaptureStream track output on HTMLMediaElement playing a gUM MediaStream",
+ visible: true
+});
+
+let audioElement;
+let audioCaptureStream;
+let videoElement;
+let videoCaptureStream;
+let untilEndedElement;
+let streamUntilEnded;
+const tracks = [];
+runTest(async () => {
+ try {
+ let stream = await getUserMedia({audio: true, video: true});
+ // We need to test with multiple tracks. We add an extra of each kind.
+ for (const track of stream.getTracks()) {
+ stream.addTrack(track.clone());
+ }
+
+ audioElement = createMediaElement("audio", "gUMAudio");
+ audioElement.srcObject = stream;
+
+ await haveEvent(audioElement, "loadedmetadata", wait(50000, new Error("Timeout")));
+
+ info("Capturing audio element (loadedmetadata -> captureStream)");
+ audioCaptureStream = audioElement.mozCaptureStream();
+
+ is(audioCaptureStream.getAudioTracks().length, 2,
+ "audio element should capture two audio tracks");
+ is(audioCaptureStream.getVideoTracks().length, 0,
+ "audio element should not capture any video tracks");
+
+ await haveNoEvent(audioCaptureStream, "addtrack");
+
+ videoElement = createMediaElement("video", "gUMVideo");
+
+ info("Capturing video element (captureStream -> loadedmetadata)");
+ videoCaptureStream = videoElement.mozCaptureStream();
+ videoElement.srcObject = audioElement.srcObject.clone();
+
+ is(videoCaptureStream.getTracks().length, 0,
+ "video element should have no tracks before metadata known");
+
+ await haveEventsButNoMore(
+ videoCaptureStream, "addtrack", 3, wait(50000, new Error("No event")));
+
+ is(videoCaptureStream.getAudioTracks().length, 2,
+ "video element should capture two audio tracks");
+ is(videoCaptureStream.getVideoTracks().length, 1,
+ "video element should capture one video track at most");
+
+ info("Testing dynamically adding audio track to audio element");
+ audioElement.srcObject.addTrack(
+ audioElement.srcObject.getAudioTracks()[0].clone());
+ await haveEventsButNoMore(
+ audioCaptureStream, "addtrack", 1, wait(50000, new Error("No event")));
+
+ is(audioCaptureStream.getAudioTracks().length, 3,
+ "Audio element should have three audio tracks captured.");
+
+ info("Testing dynamically adding video track to audio element");
+ audioElement.srcObject.addTrack(
+ audioElement.srcObject.getVideoTracks()[0].clone());
+ await haveNoEvent(audioCaptureStream, "addtrack");
+
+ is(audioCaptureStream.getVideoTracks().length, 0,
+ "Audio element should have no video tracks captured.");
+
+ info("Testing dynamically adding audio track to video element");
+ videoElement.srcObject.addTrack(
+ videoElement.srcObject.getAudioTracks()[0].clone());
+ await haveEventsButNoMore(
+ videoCaptureStream, "addtrack", 1, wait(50000, new Error("Timeout")));
+
+ is(videoCaptureStream.getAudioTracks().length, 3,
+ "Captured video stream should have three audio tracks captured.");
+
+ info("Testing dynamically adding video track to video element");
+ videoElement.srcObject.addTrack(
+ videoElement.srcObject.getVideoTracks()[0].clone());
+ await haveNoEvent(videoCaptureStream, "addtrack");
+
+ is(videoCaptureStream.getVideoTracks().length, 1,
+ "Captured video stream should have at most one video tracks captured.");
+
+ info("Testing track removal.");
+ tracks.push(...videoElement.srcObject.getTracks());
+ for (const track of videoElement.srcObject.getVideoTracks().reverse()) {
+ videoElement.srcObject.removeTrack(track);
+ }
+ is(videoCaptureStream.getVideoTracks().length, 1,
+ "Captured video should have still have one video track.");
+
+ await haveEvent(videoCaptureStream.getVideoTracks()[0], "ended",
+ wait(50000, new Error("Timeout")));
+ await haveEvent(videoCaptureStream, "removetrack",
+ wait(50000, new Error("Timeout")));
+
+ is(videoCaptureStream.getVideoTracks().length, 0,
+ "Captured video stream should have no video tracks after removal.");
+
+
+ info("Testing source reset.");
+ stream = await getUserMedia({audio: true, video: true});
+ videoElement.srcObject = stream;
+ for (const track of videoCaptureStream.getTracks()) {
+ await Promise.race(videoCaptureStream.getTracks().map(
+ t => haveEvent(t, "ended", wait(50000, new Error("Timeout"))))
+ );
+ await haveEvent(videoCaptureStream, "removetrack", wait(50000, new Error("Timeout")));
+ }
+ await haveEventsButNoMore(
+ videoCaptureStream, "addtrack", 2, wait(50000, new Error("Timeout")));
+ is(videoCaptureStream.getAudioTracks().length, 1,
+ "Captured video stream should have one audio track");
+
+ is(videoCaptureStream.getVideoTracks().length, 1,
+ "Captured video stream should have one video track");
+
+ info("Testing CaptureStreamUntilEnded");
+ untilEndedElement =
+ createMediaElement("video", "gUMVideoUntilEnded");
+ untilEndedElement.srcObject = audioElement.srcObject;
+
+ await haveEvent(untilEndedElement, "loadedmetadata",
+ wait(50000, new Error("Timeout")));
+
+ streamUntilEnded = untilEndedElement.mozCaptureStreamUntilEnded();
+
+ is(streamUntilEnded.getAudioTracks().length, 3,
+ "video element should capture all 3 audio tracks until ended");
+ is(streamUntilEnded.getVideoTracks().length, 1,
+ "video element should capture only 1 video track until ended");
+
+ for (const track of untilEndedElement.srcObject.getTracks()) {
+ track.stop();
+ }
+
+ await haveEvent(untilEndedElement, "ended", wait(50000, new Error("Timeout")));
+ for (const track of streamUntilEnded.getTracks()) {
+ await Promise.race(streamUntilEnded.getTracks().map(
+ t => haveEvent(t, "ended", wait(50000, new Error("Timeout"))))
+ );
+ await haveEvent(streamUntilEnded, "removetrack", wait(50000, new Error("Timeout")));
+ }
+
+ info("Element and tracks ended. Ensuring that new tracks aren't created.");
+ untilEndedElement.srcObject = videoElement.srcObject;
+ await haveEventsButNoMore(
+ untilEndedElement, "loadedmetadata", 1, wait(50000, new Error("Timeout")));
+
+ is(streamUntilEnded.getTracks().length, 0, "Should have no tracks");
+ } catch(e) {
+ ok(false, "Test failed: " + e + (e && e.stack ? "\n" + e.stack : ""));
+ } finally {
+ if (videoElement) {
+ tracks.push(...videoElement.srcObject.getTracks());
+ }
+ for(const track of tracks) {
+ track.stop();
+ }
+ }
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_video.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_video.html
new file mode 100644
index 0000000000..d177e93bfb
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaElementCapture_video.html
@@ -0,0 +1,91 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script>
+
+createHTML({
+ bug: "1259788",
+ title: "Test CaptureStream video content on HTMLMediaElement playing a gUM MediaStream",
+ visible: true
+});
+
+var gUMVideoElement;
+var captureStreamElement;
+
+const pausedTimeout = 1000;
+let h;
+
+runTest(async () => {
+ try {
+ await pushPrefs(
+ // This test expects fake video devices, as it expects captured frames to
+ // shift over time, which is not currently provided by loopback devices
+ ['media.video_loopback_dev', ''],
+ ['media.navigator.streams.fake', true]);
+
+ let stream = await getUserMedia({video: true});
+ h = new VideoStreamHelper();
+ gUMVideoElement =
+ createMediaElement("video", "gUMVideo");
+ gUMVideoElement.srcObject = stream;
+ gUMVideoElement.play();
+
+ info("Capturing");
+ captureStreamElement =
+ createMediaElement("video", "captureStream");
+ captureStreamElement.srcObject = gUMVideoElement.mozCaptureStream();
+ captureStreamElement.play();
+
+ await h.checkVideoPlaying(captureStreamElement);
+
+ // Adding a dummy audio track to the stream will keep a consuming media
+ // element from ending.
+ // We could also solve it by repeatedly play()ing or autoplay, but then we
+ // wouldn't be sure the media element stopped rendering video because it
+ // went to the ended state or because there were no frames for the track.
+ let osc = createOscillatorStream(new AudioContext(), 1000);
+ captureStreamElement.srcObject.addTrack(osc.getTracks()[0]);
+
+ info("Video flowing. Pausing.");
+ gUMVideoElement.pause();
+ await h.checkVideoPaused(captureStreamElement, { time: pausedTimeout });
+
+ info("Video stopped flowing. Playing.");
+ gUMVideoElement.play();
+ await h.checkVideoPlaying(captureStreamElement);
+
+ info("Video flowing. Removing source.");
+ stream = gUMVideoElement.srcObject;
+ gUMVideoElement.srcObject = null;
+ await h.checkVideoPaused(captureStreamElement, { time: pausedTimeout });
+
+ info("Video stopped flowing. Setting source.");
+ gUMVideoElement.srcObject = stream;
+ await h.checkVideoPlaying(captureStreamElement);
+
+ info("Video flowing. Changing source by track manipulation. Remove first.");
+ let track = gUMVideoElement.srcObject.getTracks()[0];
+ gUMVideoElement.srcObject.removeTrack(track);
+ await h.checkVideoPaused(captureStreamElement, { time: pausedTimeout });
+
+ info("Video paused. Changing source by track manipulation. Add first.");
+ gUMVideoElement.srcObject.addTrack(track);
+ gUMVideoElement.play();
+ await h.checkVideoPlaying(captureStreamElement);
+
+ gUMVideoElement.srcObject.getTracks().forEach(t => t.stop());
+ ok(true, "Test passed.");
+ } catch (e) {
+ ok(false, "Test failed: " + e + (e.stack ? "\n" + e.stack : ""));
+ }
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamClone.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamClone.html
new file mode 100644
index 0000000000..029ce77dd0
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamClone.html
@@ -0,0 +1,258 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+"use strict";
+
+createHTML({
+ title: "MediaStream.clone()",
+ bug: "1208371"
+});
+
+runTest(async () => {
+ await pushPrefs(
+ ["media.getusermedia.camera.stop_on_disable.enabled", true],
+ ["media.getusermedia.camera.stop_on_disable.delay_ms", 0],
+ ["media.getusermedia.microphone.stop_on_disable.enabled", true],
+ ["media.getusermedia.microphone.stop_on_disable.delay_ms", 0]);
+
+ let gUMStream = await getUserMedia({audio: true, video: true});
+ {
+ info("Test clone()ing an audio/video gUM stream");
+ let clone = gUMStream.clone();
+
+ checkMediaStreamCloneAgainstOriginal(clone, gUMStream);
+ checkMediaStreamTrackCloneAgainstOriginal(clone.getAudioTracks()[0],
+ gUMStream.getAudioTracks()[0]);
+ checkMediaStreamTrackCloneAgainstOriginal(clone.getVideoTracks()[0],
+ gUMStream.getVideoTracks()[0]);
+
+ isnot(clone.id.length, 0, "Stream clone should have an id string");
+ isnot(clone.getAudioTracks()[0].id.length, 0,
+ "Audio track clone should have an id string");
+ isnot(clone.getVideoTracks()[0].id.length, 0,
+ "Audio track clone should have an id string");
+
+ info("Playing from track clones");
+ let test = createMediaElement('video', 'testClonePlayback');
+ let playback = new MediaStreamPlayback(test, clone);
+ await playback.playMedia(false);
+ }
+
+ {
+ info("Test addTrack()ing a video track to a stream without affecting its clone");
+ let stream = new MediaStream(gUMStream.getVideoTracks());
+ let otherStream = await getUserMedia({video: true});
+ let track = stream.getTracks()[0];
+ let otherTrack = otherStream.getTracks()[0];
+
+ let streamClone = stream.clone();
+ let trackClone = streamClone.getTracks()[0];
+ checkMediaStreamContains(streamClone, [trackClone], "Initial clone");
+
+ stream.addTrack(otherTrack);
+ checkMediaStreamContains(stream, [track, otherTrack],
+ "Added video to original");
+ checkMediaStreamContains(streamClone, [trackClone],
+ "Clone not affected");
+
+ stream.removeTrack(track);
+ streamClone.addTrack(track);
+ checkMediaStreamContains(streamClone, [trackClone, track],
+ "Added video to clone");
+ checkMediaStreamContains(stream, [otherTrack],
+ "Original not affected");
+
+ // Not part of streamClone. Does not get stopped by the playback test.
+ otherTrack.stop();
+
+ let test = createMediaElement('video', 'testClonePlayback');
+ let playback = new MediaStreamPlayback(test, streamClone);
+ await playback.playMedia(false);
+ }
+
+ {
+ info("Test cloning a stream into inception");
+ let stream = gUMStream.clone()
+ let clone = stream;
+ let clones = Array(10).fill().map(() => clone = clone.clone());
+ let inceptionClone = clones.pop();
+ checkMediaStreamCloneAgainstOriginal(inceptionClone, stream);
+ stream.getTracks().forEach(t => (stream.removeTrack(t),
+ inceptionClone.addTrack(t)));
+ is(inceptionClone.getAudioTracks().length, 2,
+ "The inception clone should contain the original audio track and a track clone");
+ is(inceptionClone.getVideoTracks().length, 2,
+ "The inception clone should contain the original video track and a track clone");
+
+ let test = createMediaElement('video', 'testClonePlayback');
+ let playback = new MediaStreamPlayback(test, inceptionClone);
+ await playback.playMedia(false);
+ clones.forEach(c => c.getTracks().forEach(t => t.stop()));
+ stream.getTracks().forEach(t => t.stop());
+ }
+
+ {
+ info("Test adding tracks from many stream clones to the original stream");
+ let stream = gUMStream.clone();
+
+ const LOOPS = 3;
+ for (let i = 0; i < LOOPS; i++) {
+ stream.clone().getTracks().forEach(t => stream.addTrack(t));
+ }
+ is(stream.getAudioTracks().length, Math.pow(2, LOOPS),
+ "The original track should contain the original audio track and all the audio clones");
+ is(stream.getVideoTracks().length, Math.pow(2, LOOPS),
+ "The original track should contain the original video track and all the video clones");
+ stream.getTracks().forEach(t1 => is(stream.getTracks()
+ .filter(t2 => t1.id == t2.id)
+ .length,
+ 1, "Each track should be unique"));
+
+ let test = createMediaElement('video', 'testClonePlayback');
+ let playback = new MediaStreamPlayback(test, stream);
+ await playback.playMedia(false);
+ }
+
+ {
+ info("Testing audio content routing with MediaStream.clone()");
+ let ac = new AudioContext();
+
+ let osc1kOriginal = createOscillatorStream(ac, 1000);
+ let audioTrack1kOriginal = osc1kOriginal.getTracks()[0];
+ let audioTrack1kClone = osc1kOriginal.clone().getTracks()[0];
+
+ let osc5kOriginal = createOscillatorStream(ac, 5000);
+ let audioTrack5kOriginal = osc5kOriginal.getTracks()[0];
+ let audioTrack5kClone = osc5kOriginal.clone().getTracks()[0];
+
+ info("Analysing audio output of original stream (1k + 5k)");
+ let stream = new MediaStream();
+ stream.addTrack(audioTrack1kOriginal);
+ stream.addTrack(audioTrack5kOriginal);
+
+ let analyser = new AudioStreamAnalyser(ac, stream);
+ await analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(1000)] > 200 &&
+ array[analyser.binIndexForFrequency(3000)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] > 200 &&
+ array[analyser.binIndexForFrequency(10000)] < 50);
+
+ info("Waiting for original tracks to stop");
+ stream.getTracks().forEach(t => t.stop());
+ await analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ // WebAudioDestination streams do not handle stop()
+ // XXX Should they? Plan to resolve that in bug 1208384.
+ // array[analyser.binIndexForFrequency(1000)] < 50 &&
+ array[analyser.binIndexForFrequency(3000)] < 50 &&
+ // array[analyser.binIndexForFrequency(5000)] < 50 &&
+ array[analyser.binIndexForFrequency(10000)] < 50);
+ analyser.disconnect();
+
+ info("Analysing audio output of stream clone (1k + 5k)");
+ stream = new MediaStream();
+ stream.addTrack(audioTrack1kClone);
+ stream.addTrack(audioTrack5kClone);
+
+ analyser = new AudioStreamAnalyser(ac, stream);
+ await analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(1000)] > 200 &&
+ array[analyser.binIndexForFrequency(3000)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] > 200 &&
+ array[analyser.binIndexForFrequency(10000)] < 50);
+ analyser.disconnect();
+
+ info("Analysing audio output of clone of clone (1k + 5k)");
+ stream = new MediaStream([audioTrack1kClone, audioTrack5kClone]).clone();
+
+ analyser = new AudioStreamAnalyser(ac, stream);
+ await analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(1000)] > 200 &&
+ array[analyser.binIndexForFrequency(3000)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] > 200 &&
+ array[analyser.binIndexForFrequency(10000)] < 50);
+ analyser.disconnect();
+
+ info("Analysing audio output of clone() + addTrack()ed tracks (1k + 5k)");
+ stream = new MediaStream(new MediaStream([ audioTrack1kClone
+ , audioTrack5kClone
+ ]).clone().getTracks());
+
+ analyser = new AudioStreamAnalyser(ac, stream);
+ await analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(1000)] > 200 &&
+ array[analyser.binIndexForFrequency(3000)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] > 200 &&
+ array[analyser.binIndexForFrequency(10000)] < 50);
+ analyser.disconnect();
+
+ info("Analysing audio output of clone()d tracks in original stream (1k) " +
+ "and clone()d tracks in stream clone (5k)");
+ stream = new MediaStream([audioTrack1kClone, audioTrack5kClone]);
+ let streamClone = stream.clone();
+
+ stream.getTracks().forEach(t => stream.removeTrack(t));
+ stream.addTrack(streamClone.getTracks()[0]);
+ streamClone.removeTrack(streamClone.getTracks()[0]);
+
+ analyser = new AudioStreamAnalyser(ac, stream);
+ await analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(1000)] > 200 &&
+ array[analyser.binIndexForFrequency(3000)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] < 50);
+ analyser.disconnect();
+
+ let cloneAnalyser = new AudioStreamAnalyser(ac, streamClone);
+ await cloneAnalyser.waitForAnalysisSuccess(array =>
+ array[cloneAnalyser.binIndexForFrequency(1000)] < 50 &&
+ array[cloneAnalyser.binIndexForFrequency(3000)] < 50 &&
+ array[cloneAnalyser.binIndexForFrequency(5000)] > 200 &&
+ array[cloneAnalyser.binIndexForFrequency(10000)] < 50);
+ cloneAnalyser.disconnect();
+
+ info("Analysing audio output enabled and disabled tracks that don't affect each other");
+ stream = new MediaStream([audioTrack1kClone, audioTrack5kClone]);
+ let clone = stream.clone();
+
+ stream.getTracks()[0].enabled = true;
+ stream.getTracks()[1].enabled = false;
+
+ clone.getTracks()[0].enabled = false;
+ clone.getTracks()[1].enabled = true;
+
+ analyser = new AudioStreamAnalyser(ac, stream);
+ await analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(1000)] > 200 &&
+ array[analyser.binIndexForFrequency(3000)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] < 50);
+ analyser.disconnect();
+
+ cloneAnalyser = new AudioStreamAnalyser(ac, clone);
+ await cloneAnalyser.waitForAnalysisSuccess(array =>
+ array[cloneAnalyser.binIndexForFrequency(1000)] < 50 &&
+ array[cloneAnalyser.binIndexForFrequency(3000)] < 50 &&
+ array[cloneAnalyser.binIndexForFrequency(5000)] > 200 &&
+ array[cloneAnalyser.binIndexForFrequency(10000)] < 50);
+ cloneAnalyser.disconnect();
+
+ // Restore original tracks
+ stream.getTracks().forEach(t => t.enabled = true);
+ }
+
+ gUMStream.getTracks().forEach(t => t.stop());
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamConstructors.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamConstructors.html
new file mode 100644
index 0000000000..4ea6e3f444
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamConstructors.html
@@ -0,0 +1,171 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ "use strict";
+
+ createHTML({
+ title: "MediaStream constructors with getUserMedia streams Test",
+ bug: "1070216"
+ });
+
+ var audioContext = new AudioContext();
+ var videoElement;
+
+ runTest(() => Promise.resolve()
+ .then(() => videoElement = createMediaElement('video', 'constructorsTest'))
+ .then(() => getUserMedia({video: true})).then(gUMStream => {
+ info("Test default constructor with video");
+ ok(gUMStream.active, "gUMStream with one track should be active");
+ var track = gUMStream.getTracks()[0];
+
+ var stream = new MediaStream();
+ ok(!stream.active, "New MediaStream should be inactive");
+ checkMediaStreamContains(stream, [], "Default constructed stream");
+
+ stream.addTrack(track);
+ ok(stream.active, "MediaStream should be active after adding a track");
+ checkMediaStreamContains(stream, [track], "Added video track");
+
+ var playback = new MediaStreamPlayback(videoElement, stream);
+ return playback.playMedia(false).then(() => {
+ ok(!gUMStream.active, "gUMStream should be inactive after stopping");
+ ok(!stream.active, "stream with stopped tracks should be inactive");
+ });
+ })
+ .then(() => getUserMedia({video: true})).then(gUMStream => {
+ info("Test copy constructor with gUM stream");
+ ok(gUMStream.active, "gUMStream with one track should be active");
+ var track = gUMStream.getTracks()[0];
+
+ var stream = new MediaStream(gUMStream);
+ ok(stream.active, "List constructed MediaStream should be active");
+ checkMediaStreamContains(stream, [track], "Copy constructed video track");
+
+ var playback = new MediaStreamPlayback(videoElement, stream);
+ return playback.playMedia(false).then(() => {
+ ok(!gUMStream.active, "gUMStream should be inactive after stopping");
+ ok(!stream.active, "stream with stopped tracks should be inactive");
+ });
+ })
+ .then(() => getUserMedia({video: true})).then(gUMStream => {
+ info("Test list constructor with empty list");
+ ok(gUMStream.active, "gUMStream with one track should be active");
+ var track = gUMStream.getTracks()[0];
+
+ var stream = new MediaStream([]);
+ ok(!stream.active, "Empty-list constructed MediaStream should be inactive");
+ checkMediaStreamContains(stream, [], "Empty-list constructed stream");
+
+ stream.addTrack(track);
+ ok(stream.active, "MediaStream should be active after adding a track");
+ checkMediaStreamContains(stream, [track], "Added video track");
+
+ var playback = new MediaStreamPlayback(videoElement, stream);
+ return playback.playMedia(false).then(() => {
+ ok(!gUMStream.active, "gUMStream should be inactive after stopping");
+ ok(!stream.active, "stream with stopped tracks should be inactive");
+ });
+ })
+ .then(() => getUserMedia({audio: true, video: true})).then(gUMStream => {
+ info("Test list constructor with a gUM audio/video stream");
+ ok(gUMStream.active, "gUMStream with two tracks should be active");
+ var audioTrack = gUMStream.getAudioTracks()[0];
+ var videoTrack = gUMStream.getVideoTracks()[0];
+
+ var stream = new MediaStream([audioTrack, videoTrack]);
+ ok(stream.active, "List constructed MediaStream should be active");
+ checkMediaStreamContains(stream, [audioTrack, videoTrack],
+ "List constructed audio and video tracks");
+
+ var playback = new MediaStreamPlayback(videoElement, stream);
+ return playback.playMedia(false).then(() => {
+ ok(!gUMStream.active, "gUMStream should be inactive after stopping");
+ ok(!stream.active, "stream with stopped tracks should be inactive");
+ });
+ })
+ .then(() => getUserMedia({video: true})).then(gUMStream => {
+ info("Test list constructor with gUM-video and WebAudio tracks");
+ ok(gUMStream.active, "gUMStream with one track should be active");
+ var audioStream = createOscillatorStream(audioContext, 2000);
+ ok(audioStream.active, "WebAudio stream should be active");
+
+ var audioTrack = audioStream.getTracks()[0];
+ var videoTrack = gUMStream.getTracks()[0];
+
+ var stream = new MediaStream([audioTrack, videoTrack]);
+ ok(stream.active, "List constructed MediaStream should be active");
+ checkMediaStreamContains(stream, [audioTrack, videoTrack],
+ "List constructed WebAudio and gUM-video tracks");
+
+ var playback = new MediaStreamPlayback(videoElement, stream);
+ return playback.playMedia(false).then(() => {
+ gUMStream.getTracks().forEach(t => t.stop());
+ ok(!gUMStream.active, "gUMStream should be inactive after stopping");
+ ok(!stream.active, "stream with stopped tracks should be inactive");
+ });
+ })
+ .then(() => {
+ var osc1k = createOscillatorStream(audioContext, 1000);
+ var audioTrack1k = osc1k.getTracks()[0];
+
+ var osc5k = createOscillatorStream(audioContext, 5000);
+ var audioTrack5k = osc5k.getTracks()[0];
+
+ var osc10k = createOscillatorStream(audioContext, 10000);
+ var audioTrack10k = osc10k.getTracks()[0];
+
+ return Promise.resolve().then(() => {
+ info("Analysing audio output with empty default constructed stream");
+ var stream = new MediaStream();
+ var analyser = new AudioStreamAnalyser(audioContext, stream);
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(1000)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] < 50 &&
+ array[analyser.binIndexForFrequency(10000)] < 50)
+ .then(() => analyser.disconnect());
+ }).then(() => {
+ info("Analysing audio output with copy constructed 5k stream");
+ var stream = new MediaStream(osc5k);
+ is(stream.active, osc5k.active,
+ "Copy constructed MediaStream should preserve active state");
+ var analyser = new AudioStreamAnalyser(audioContext, stream);
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(1000)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] > 200 &&
+ array[analyser.binIndexForFrequency(10000)] < 50)
+ .then(() => analyser.disconnect());
+ }).then(() => {
+ info("Analysing audio output with empty-list constructed stream");
+ var stream = new MediaStream([]);
+ var analyser = new AudioStreamAnalyser(audioContext, stream);
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(1000)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] < 50 &&
+ array[analyser.binIndexForFrequency(10000)] < 50)
+ .then(() => analyser.disconnect());
+ }).then(() => {
+ info("Analysing audio output with list constructed 1k, 5k and 10k tracks");
+ var stream = new MediaStream([audioTrack1k, audioTrack5k, audioTrack10k]);
+ ok(stream.active,
+ "List constructed MediaStream from WebAudio should be active");
+ var analyser = new AudioStreamAnalyser(audioContext, stream);
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(1000)] > 200 &&
+ array[analyser.binIndexForFrequency(2500)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] > 200 &&
+ array[analyser.binIndexForFrequency(7500)] < 50 &&
+ array[analyser.binIndexForFrequency(10000)] > 200 &&
+ array[analyser.binIndexForFrequency(11000)] < 50)
+ .then(() => analyser.disconnect());
+ });
+ }));
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamTrackClone.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamTrackClone.html
new file mode 100644
index 0000000000..e5e0764427
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_mediaStreamTrackClone.html
@@ -0,0 +1,170 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ "use strict";
+
+ createHTML({
+ title: "MediaStreamTrack.clone()",
+ bug: "1208371"
+ });
+
+ var testSingleTrackClonePlayback = constraints =>
+ getUserMedia(constraints).then(stream => {
+ info("Test clone()ing an " + constraints + " gUM track");
+ var track = stream.getTracks()[0];
+ var clone = track.clone();
+
+ checkMediaStreamTrackCloneAgainstOriginal(clone, track);
+
+ info("Stopping original track");
+ track.stop();
+
+ info("Creating new stream for clone");
+ var cloneStream = new MediaStream([clone]);
+ checkMediaStreamContains(cloneStream, [clone]);
+
+ info("Testing playback of track clone");
+ var test = createMediaElement('video', 'testClonePlayback');
+ var playback = new MediaStreamPlayback(test, cloneStream);
+ return playback.playMedia(false);
+ });
+
+ runTest(() => Promise.resolve()
+ .then(() => testSingleTrackClonePlayback({audio: true}))
+ .then(() => testSingleTrackClonePlayback({video: true}))
+ .then(() => getUserMedia({video: true})).then(stream => {
+ info("Test cloning a track into inception");
+ var track = stream.getTracks()[0];
+ var clone = track;
+ var clones = Array(10).fill().map(() => clone = clone.clone());
+ var inceptionClone = clones.pop();
+ checkMediaStreamTrackCloneAgainstOriginal(inceptionClone, track);
+
+ var cloneStream = new MediaStream();
+ cloneStream.addTrack(inceptionClone);
+
+ // cloneStream is now essentially the same as stream.clone();
+ checkMediaStreamCloneAgainstOriginal(cloneStream, stream);
+
+ var test = createMediaElement('video', 'testClonePlayback');
+ var playback = new MediaStreamPlayback(test, cloneStream);
+ return playback.playMedia(false).then(() => {
+ info("Testing that clones of ended tracks are ended");
+ cloneStream.clone().getTracks().forEach(t =>
+ is(t.readyState, "ended", "Track " + t.id + " should be ended"));
+ })
+ .then(() => {
+ clones.forEach(t => t.stop());
+ track.stop();
+ });
+ })
+ .then(() => getUserMedia({audio: true, video: true})).then(stream => {
+ info("Test adding many track clones to the original stream");
+
+ const LOOPS = 3;
+ for (var i = 0; i < LOOPS; i++) {
+ stream.getTracks().forEach(t => stream.addTrack(t.clone()));
+ }
+ is(stream.getVideoTracks().length, Math.pow(2, LOOPS),
+ "The original track should contain the original video track and all the video clones");
+ stream.getTracks().forEach(t1 => is(stream.getTracks()
+ .filter(t2 => t1.id == t2.id)
+ .length,
+ 1, "Each track should be unique"));
+
+ var test = createMediaElement('video', 'testClonePlayback');
+ var playback = new MediaStreamPlayback(test, stream);
+ return playback.playMedia(false);
+ })
+ .then(() => {
+ info("Testing audio content routing with MediaStreamTrack.clone()");
+ var ac = new AudioContext();
+
+ var osc1kOriginal = createOscillatorStream(ac, 1000);
+ var audioTrack1kOriginal = osc1kOriginal.getTracks()[0];
+ var audioTrack1kClone = audioTrack1kOriginal.clone();
+
+ var osc5kOriginal = createOscillatorStream(ac, 5000);
+ var audioTrack5kOriginal = osc5kOriginal.getTracks()[0];
+ var audioTrack5kClone = audioTrack5kOriginal.clone();
+
+ return Promise.resolve().then(() => {
+ info("Analysing audio output enabled and disabled tracks that don't affect each other");
+ audioTrack1kOriginal.enabled = true;
+ audioTrack5kOriginal.enabled = false;
+
+ audioTrack1kClone.enabled = false;
+ audioTrack5kClone.enabled = true;
+
+ var analyser =
+ new AudioStreamAnalyser(ac, new MediaStream([audioTrack1kOriginal,
+ audioTrack5kOriginal]));
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(1000)] > 200 &&
+ array[analyser.binIndexForFrequency(3000)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] < 50)
+ .then(() => analyser.disconnect())
+ .then(() => {
+ var cloneAnalyser =
+ new AudioStreamAnalyser(ac, new MediaStream([audioTrack1kClone,
+ audioTrack5kClone]));
+ return cloneAnalyser.waitForAnalysisSuccess(array =>
+ array[cloneAnalyser.binIndexForFrequency(1000)] < 50 &&
+ array[cloneAnalyser.binIndexForFrequency(3000)] < 50 &&
+ array[cloneAnalyser.binIndexForFrequency(5000)] > 200 &&
+ array[cloneAnalyser.binIndexForFrequency(10000)] < 50)
+ .then(() => cloneAnalyser.disconnect());
+ })
+ // Restore original tracks
+ .then(() => [audioTrack1kOriginal,
+ audioTrack5kOriginal,
+ audioTrack1kClone,
+ audioTrack5kClone].forEach(t => t.enabled = true));
+ }).then(() => {
+ info("Analysing audio output of 1k original and 5k clone.");
+ var stream = new MediaStream();
+ stream.addTrack(audioTrack1kOriginal);
+ stream.addTrack(audioTrack5kClone);
+
+ var analyser = new AudioStreamAnalyser(ac, stream);
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(1000)] > 200 &&
+ array[analyser.binIndexForFrequency(3000)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] > 200 &&
+ array[analyser.binIndexForFrequency(10000)] < 50)
+ .then(() => {
+ info("Waiting for tracks to stop");
+ stream.getTracks().forEach(t => t.stop());
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(1000)] < 50 &&
+ array[analyser.binIndexForFrequency(3000)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] < 50 &&
+ array[analyser.binIndexForFrequency(10000)] < 50);
+ }).then(() => analyser.disconnect());
+ }).then(() => {
+ info("Analysing audio output of clones of clones (1kx2 + 5kx4)");
+ var stream = new MediaStream([audioTrack1kClone.clone(),
+ audioTrack5kOriginal.clone().clone().clone().clone()]);
+
+ var analyser = new AudioStreamAnalyser(ac, stream);
+ return analyser.waitForAnalysisSuccess(array =>
+ array[analyser.binIndexForFrequency(50)] < 50 &&
+ array[analyser.binIndexForFrequency(1000)] > 200 &&
+ array[analyser.binIndexForFrequency(3000)] < 50 &&
+ array[analyser.binIndexForFrequency(5000)] > 200 &&
+ array[analyser.binIndexForFrequency(10000)] < 50)
+ .then(() => analyser.disconnect());
+ });
+ }));
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_nonDefaultRate.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_nonDefaultRate.html
new file mode 100644
index 0000000000..8a6ac8c62b
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_nonDefaultRate.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: "getUserMedia feed to a graph with non default rate",
+ bug: "1387454",
+ });
+
+ /**
+ * Run a test to verify that when we use the streem from a gUM to an AudioContext
+ * with non default rate the connection fails. (gUM is always on default rate).
+ */
+ runTest(async () => {
+ // Since we do not examine the stream we do not need loopback.
+ DISABLE_LOOPBACK_TONE = true;
+ const stream = await getUserMedia({audio: true});
+ const nonDefaultRate = 32000;
+ const ac = new AudioContext({sampleRate: nonDefaultRate});
+ mustThrowWith(
+ "Connect stream with graph of different sample rate",
+ "NotSupportedError", () => {
+ ac.createMediaStreamSource(stream);
+ }
+ );
+ for (let t of stream.getTracks()) {
+ t.stop();
+ }
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_peerIdentity.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_peerIdentity.html
new file mode 100644
index 0000000000..c4dfb9acb8
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_peerIdentity.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+ <script type="application/javascript" src="blacksilence.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ title: "Test getUserMedia peerIdentity Constraint",
+ bug: "942367"
+});
+async function theTest() {
+ async function testPeerIdentityConstraint(withConstraint) {
+ const config = { audio: true, video: true };
+ if (withConstraint) {
+ config.peerIdentity = 'user@example.com';
+ }
+ info('getting media with constraints: ' + JSON.stringify(config));
+ const stream = await getUserMedia(config);
+ for (const track of stream.getTracks()) {
+ const recorder = new MediaRecorder(new MediaStream([track]));
+ try {
+ recorder.start();
+ ok(!withConstraint,
+ `gUM ${track.kind} track without peerIdentity must not throw`);
+ recorder.stop();
+ } catch(e) {
+ ok(withConstraint,
+ `gUM ${track.kind} track with peerIdentity must throw`);
+ }
+ }
+ await Promise.all([
+ audioIsSilence(withConstraint, stream),
+ videoIsBlack(withConstraint, stream),
+ ]);
+ stream.getTracks().forEach(t => t.stop());
+ };
+
+ // both without and with the constraint
+ await testPeerIdentityConstraint(false);
+ await testPeerIdentityConstraint(true);
+}
+
+runTest(theTest);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_permission.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_permission.html
new file mode 100644
index 0000000000..cd02c7326c
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_permission.html
@@ -0,0 +1,104 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({ title: "Test getUserMedia in iframes", bug: "1371741" });
+/**
+ Tests covering enumerateDevices API and deviceId constraint. Exercise code.
+*/
+
+// Call gUM in iframe.
+async function iframeGum(dict, iframe = document.createElement("iframe")) {
+ Object.assign(iframe, dict);
+ if (dict.src) {
+ info(`<iframe src="${dict.src}" sandbox="${dict.sandbox}">`);
+ } else {
+ info(`<iframe srcdoc sandbox="${dict.sandbox}">`);
+ }
+ document.documentElement.appendChild(iframe);
+
+ const once = (t, msg) => new Promise(r => t.addEventListener(msg, r, { once: true }));
+ const haveMessage = once(window, "message");
+ await new Promise(resolve => iframe.onload = resolve);
+ return (await haveMessage).data;
+};
+
+runTest(async () => {
+ const path = "/tests/dom/media/webrtc/tests/mochitests/test_getUserMedia_permission_iframe.html";
+
+ async function sourceFn() {
+ try {
+ const gUM = c => navigator.mediaDevices.getUserMedia(c);
+ let message;
+ let stream;
+ try {
+ stream = await gUM({ video: true });
+ message = 'success';
+ } catch(e) {
+ message = e.name;
+ }
+ parent.postMessage(message, 'https://example.com:443');
+
+ if (message == "success") {
+ stream.getTracks().forEach(track => track.stop());
+ }
+ } catch (e) {
+ setTimeout(() => { throw e; });
+ }
+ }
+
+ const source = `<html\><script\>(${sourceFn.toString()})()</script\></html\>`;
+
+ // Test gUM in sandboxed vs. regular iframe.
+
+ for (const origin of [window.location.origin, "https://test1.example.com"]) {
+ const src = origin + path;
+ is(await iframeGum({ src, sandbox: "allow-scripts" }),
+ "NotAllowedError", "gUM fails in sandboxed iframe " + origin);
+ }
+ is(await iframeGum({
+ src: path,
+ sandbox: "allow-scripts allow-same-origin",
+ }),
+ "success", "gUM works in regular same-origin iframe");
+ is(await iframeGum({
+ src: `https://test1.example.com${path}`,
+ sandbox: "allow-scripts allow-same-origin",
+ }),
+ "NotAllowedError", "gUM fails in regular cross-origin iframe");
+
+ // Test gUM in sandboxed vs regular srcdoc iframe
+
+ const iframeSrcdoc = document.createElement("iframe");
+ iframeSrcdoc.srcdoc = source;
+ is(await iframeGum({ sandbox: "allow-scripts" }, iframeSrcdoc),
+ "NotAllowedError", "gUM fails in sandboxed srcdoc iframe");
+ is(await iframeGum({ sandbox: "allow-scripts allow-same-origin" }, iframeSrcdoc),
+ "success", "gUM works in regular srcdoc iframe");
+
+ // Test gUM in sandboxed vs regular blob iframe
+
+ const blob = new Blob([source], {type : "text/html"});
+ let src = URL.createObjectURL(blob);
+ is(await iframeGum({ src, sandbox: "allow-scripts" }),
+ "NotAllowedError", "gUM fails in sandboxed blob iframe");
+ is(await iframeGum({ src, sandbox: "allow-scripts allow-same-origin"}),
+ "success", "gUM works in regular blob iframe");
+ URL.revokeObjectURL(src);
+
+ // data iframes always have null-principals
+
+ src = `data:text/html;base64,${btoa(source)}`;
+ is(await iframeGum({ src, sandbox: "allow-scripts" }),
+ "NotAllowedError", "gUM fails in sandboxed data iframe");
+ is(await iframeGum({ src, sandbox: "allow-scripts allow-same-origin"}),
+ "NotAllowedError", "gUM fails in regular data iframe");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_permission_iframe.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_permission_iframe.html
new file mode 100644
index 0000000000..732c2cf98c
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_permission_iframe.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+<pre id="test">
+<script type="application/javascript">
+/**
+ Runs inside iframe in test_getUserMedia_permission.html.
+*/
+
+const gUM = c => navigator.mediaDevices.getUserMedia(c);
+
+(async () => {
+ let message;
+ let stream;
+ try {
+ stream = await gUM({ video: true });
+ message = "success";
+ } catch(e) {
+ message = e.name;
+ }
+ parent.postMessage(message, "https://example.com:443");
+
+ if (message == "success") {
+ stream.getTracks().forEach(track => track.stop());
+ }
+})().catch(e => setTimeout(() => { throw e; }));
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_playAudioTwice.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_playAudioTwice.html
new file mode 100644
index 0000000000..30d168bf38
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_playAudioTwice.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({title: "getUserMedia Play Audio Twice", bug: "822109" });
+ /**
+ * Run a test that we can complete an audio playback cycle twice in a row.
+ */
+ runTest(function () {
+ return getUserMedia({audio: true}).then(audioStream => {
+ var testAudio = createMediaElement('audio', 'testAudio');
+ var playback = new MediaStreamPlayback(testAudio, audioStream);
+
+ return playback.playMediaWithoutStoppingTracks(false)
+ .then(() => playback.playMedia(true));
+ });
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_playVideoAudioTwice.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_playVideoAudioTwice.html
new file mode 100644
index 0000000000..7b5e6effd1
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_playVideoAudioTwice.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({title: "getUserMedia Play Video and Audio Twice", bug: "822109" });
+ /**
+ * Run a test that we can complete a video playback cycle twice in a row.
+ */
+ runTest(function () {
+ return getUserMedia({video: true, audio: true}).then(stream => {
+ var testVideo = createMediaElement('video', 'testVideo');
+ var playback = new MediaStreamPlayback(testVideo, stream);
+
+ return playback.playMediaWithoutStoppingTracks(false)
+ .then(() => playback.playMedia(true));
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_playVideoTwice.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_playVideoTwice.html
new file mode 100644
index 0000000000..2890f45eab
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_playVideoTwice.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({ title: "getUserMedia Play Video Twice", bug: "822109" });
+ /**
+ * Run a test that we can complete a video playback cycle twice in a row.
+ */
+ runTest(function () {
+ return getUserMedia({video: true}).then(stream => {
+ var testVideo = createMediaElement('video', 'testVideo');
+ var streamPlayback = new MediaStreamPlayback(testVideo, stream);
+
+ return streamPlayback.playMediaWithoutStoppingTracks(false)
+ .then(() => streamPlayback.playMedia(true));
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_scarySources.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_scarySources.html
new file mode 100644
index 0000000000..782110823e
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_scarySources.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+
+createHTML({title: "Detect screensharing sources that are firefox", bug: "1311048"});
+
+const Services = SpecialPowers.Services;
+
+let observe = topic => new Promise(r => Services.obs.addObserver(function o(...args) {
+ Services.obs.removeObserver(o, topic);
+ r(args.map(x => SpecialPowers.wrap(x)));
+}, topic));
+
+let getDevices = async constraints => {
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ let [{ windowID, innerWindowID, callID, devices }] = await Promise.race([
+ getUserMedia(constraints),
+ observe("getUserMedia:request")
+ ]);
+ let window = Services.wm.getOuterWindowWithId(windowID);
+ return devices.map(SpecialPowers.wrapCallback(d => d.QueryInterface(Ci.nsIMediaDevice)));
+};
+
+runTest(async () => {
+ await pushPrefs(["media.navigator.permission.disabled", true],
+ ["media.navigator.permission.force", true]);
+ let devices = await getDevices({video: { mediaSource: "window" }});
+ ok(devices.length, "Found one or more windows.");
+ devices = Array.prototype.filter.call(devices, d => d.scary);
+ ok(devices.length, "Found one or more scary windows (our own counts).");
+ devices = devices.filter(d => d.rawName.includes("MochiTest"));
+ ok(devices.length,
+ "Our own window is among the scary: "
+ + devices.map(d => `"${d.rawName}"`));
+
+ devices = await getDevices({video: { mediaSource: "screen" }});
+ let numScreens = devices.length;
+ ok(numScreens, "Found one or more screens.");
+ devices = Array.prototype.filter.call(devices, d => d.scary);
+ is(devices.length, numScreens, "All screens are scary.");
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_spinEventLoop.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_spinEventLoop.html
new file mode 100644
index 0000000000..ae691785f5
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_spinEventLoop.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({ title: "getUserMedia Basic Audio Test", bug: "1208656" });
+ /**
+ * Run a test to verify that we can spin the event loop from within a mozGUM callback.
+ */
+ runTest(() => {
+ var testAudio = createMediaElement('audio', 'testAudio');
+ return new Promise((resolve, reject) => {
+ navigator.mozGetUserMedia({ audio: true }, stream => {
+ SpecialPowers.spinEventLoop(window);
+ ok(true, "Didn't crash");
+ stream.getTracks().forEach(t => t.stop());
+ resolve();
+ }, () => {});
+ });
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_trackCloneCleanup.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_trackCloneCleanup.html
new file mode 100644
index 0000000000..60077ec73b
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_trackCloneCleanup.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ "use strict";
+
+ createHTML({
+ title: "Stopping a MediaStreamTrack and its clones should deallocate the device",
+ bug: "1294605"
+ });
+
+ runTest(async () => {
+ await pushPrefs(["media.navigator.permission.fake", true]);
+ const stream = await getUserMedia({audio: true, video: true, fake: true});
+ const clone = stream.clone();
+ stream.getTracks().forEach(t => t.stop());
+ stream.clone().getTracks().forEach(t => stream.addTrack(t));
+ is(stream.getTracks().filter(t => t.readyState == "live").length, 0,
+ "Cloning ended tracks should make them ended");
+ [...stream.getTracks(), ...clone.getTracks()].forEach(t => t.stop());
+
+ // Bug 1295352: better to be explicit about noGum here wrt future refactoring.
+ return noGum();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_getUserMedia_trackEnded.html b/dom/media/webrtc/tests/mochitests/test_getUserMedia_trackEnded.html
new file mode 100644
index 0000000000..b275f4555f
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_getUserMedia_trackEnded.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<iframe id="iframe" onload="iframeLoaded()" srcdoc="
+ <script type='application/javascript'>
+ document.gUM = (constraints, success, failure) =>
+ navigator.mediaDevices.getUserMedia(constraints).then(success, failure);
+ </script>">
+</iframe>
+<script type="application/javascript">
+ "use strict";
+ let iframeLoadedPromise = {};
+ iframeLoadedPromise.promise = new Promise(r => {
+ iframeLoadedPromise.resolve = r;
+ });;
+ function iframeLoaded() {
+ iframeLoadedPromise.resolve();
+ }
+
+ createHTML({
+ title: "getUserMedia MediaStreamTrack 'ended' event on navigating",
+ bug: "1208373",
+ });
+
+ runTest(async () => {
+ await iframeLoadedPromise.promise;
+ let iframe = document.getElementById("iframe");
+ let stream;
+ // We're passing callbacks into a method in the iframe here, because
+ // a Promise created in the iframe is unusable after the iframe has
+ // navigated away (see bug 1269400 for details).
+ return new Promise((resolve, reject) =>
+ iframe.contentDocument.gUM({audio: true, video: true}, resolve, reject))
+ .then(s => {
+ // We're cloning a stream containing identical tracks (an original
+ // and its clone) to test that ended works both for originals
+ // clones when they're both owned by the same MediaStream.
+ // (Bug 1274221)
+ stream = new MediaStream([].concat(s.getTracks(), s.getTracks())
+ .map(t => t.clone())).clone();
+ var allTracksEnded = Promise.all(stream.getTracks().map(t => {
+ info("Set up ended handler for track " + t.id);
+ return haveEvent(t, "ended", wait(50000))
+ .then(event => {
+ info("ended handler invoked for track " + t.id);
+ is(event.target, t, "Target should be correct");
+ }, e => e ? Promise.reject(e)
+ : ok(false, "ended event never raised for track " + t.id));
+ }));
+ stream.getTracks().forEach(t =>
+ is(t.readyState, "live",
+ "Non-ended track should have readyState 'live'"));
+ iframe.srcdoc = "";
+ info("iframe has been reset. Waiting for tracks to end.");
+ return allTracksEnded;
+ })
+ .then(() => stream.getTracks().forEach(t =>
+ is(t.readyState, "ended",
+ "Ended track should have readyState 'ended'")));
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_groupId.html b/dom/media/webrtc/tests/mochitests/test_groupId.html
new file mode 100644
index 0000000000..f2aefe5e80
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_groupId.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({ title: "Test group id of MediaDeviceInfo", bug: "1213453" });
+
+async function getDefaultDevices() {
+ const devices = await navigator.mediaDevices.enumerateDevices();
+ is(devices.length, 2, "Two fake devices found.");
+
+ devices.forEach(d => isnot(d.groupId, "", "GroupId is included in every device"));
+
+ const videos = devices.filter(d => d.kind == "videoinput");
+ is(videos.length, 1, "One video device found.");
+ const audios = devices.filter(d => d.kind == "audioinput");
+ is(audios.length, 1, "One microphone device found.");
+
+ return {audio: audios[0], video: videos[0]};
+}
+
+runTest(async () => {
+ // Force fake devices in order to be able to change camera name by pref.
+ await pushPrefs(["media.navigator.streams.fake", true],
+ ["media.audio_loopback_dev", ""],
+ ["media.video_loopback_dev", ""]);
+
+ const afterGum = await navigator.mediaDevices.getUserMedia({
+ video: true, audio: true
+ });
+ afterGum.getTracks().forEach(track => track.stop());
+
+ let {audio, video} = await getDefaultDevices();
+
+ /* The low level method to correlate groupIds is by device names.
+ * Use a similar comparison here to verify that it works.
+ * Multiple devices of the same device name are not expected in
+ * automation. */
+ isnot(audio.label, video.label, "Audio label differs from video");
+ isnot(audio.groupId, video.groupId, "Not the same groupIds");
+ // Change video name to match.
+ await pushPrefs(["media.getusermedia.fake-camera-name", audio.label]);
+ ({audio, video} = await getDefaultDevices());
+ is(audio.label, video.label, "Audio label matches video");
+ is(audio.groupId, video.groupId, "GroupIds should be the same");
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_multi_mics.html b/dom/media/webrtc/tests/mochitests/test_multi_mics.html
new file mode 100644
index 0000000000..95bcbfd3e4
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_multi_mics.html
@@ -0,0 +1,66 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+"use strict";
+
+createHTML({
+ title: "Test the ability of opening multiple microphones via gUM",
+ bug: "1238038",
+});
+
+runTest(async () => {
+ // Ensure we use the real microphones by disabling loopback devices and fake devices.
+ await pushPrefs(["media.audio_loopback_dev", ""], ["media.navigator.streams.fake", false]);
+
+ try {
+ let devices = await navigator.mediaDevices.enumerateDevices();
+ // Create constraints
+ let constraints = [];
+ devices.forEach((device) => {
+ if (device.kind === "audioinput") {
+ constraints.push({
+ audio: { deviceId: { exact: device.deviceId } },
+ });
+ }
+ });
+ if (constraints.length >= 2) {
+ // Create constraints
+ let constraints = [];
+ devices.forEach((device) => {
+ if (device.kind === "audioinput") {
+ constraints.push({
+ audio: { deviceId: { exact: device.deviceId } },
+ });
+ }
+ });
+ // Open microphones by the constraints
+ let mediaStreams = [];
+ for (let c of constraints) {
+ let stream = await navigator.mediaDevices.getUserMedia(c);
+ dump("MediaStream: " + stream.id + " for device: " + c.audio.deviceId.exact + " is created\n");
+ mediaStreams.push(stream);
+ }
+ // Close microphones
+ for (let stream of mediaStreams) {
+ for (let track of stream.getTracks()) {
+ track.stop();
+ }
+ dump("Stop all tracks in MediaStream: " + stream.id + "\n");
+ }
+ mediaStreams = [];
+ } else {
+ dump("Skip test since we need at least two microphones\n");
+ }
+ } catch (e) {
+ ok(false, e.name + ": " + e.message);
+ }
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_ondevicechange.html b/dom/media/webrtc/tests/mochitests/test_ondevicechange.html
new file mode 100644
index 0000000000..4358d9d748
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_ondevicechange.html
@@ -0,0 +1,180 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<script type="application/javascript">
+"use strict";
+
+createHTML({
+ title: "ondevicechange tests",
+ bug: "1152383"
+});
+
+async function resolveOnEvent(target, name) {
+ return new Promise(r => target.addEventListener(name, r, {once: true}));
+}
+let eventCount = 0;
+async function triggerVideoDevicechange() {
+ ++eventCount;
+ // "media.getusermedia.fake-camera-name" specifies the name of the single
+ // fake video camera.
+ // Changing the pref imitates replacing one device with another.
+ return pushPrefs(["media.getusermedia.fake-camera-name",
+ `devicechange ${eventCount}`])
+}
+function addIframe() {
+ const iframe = document.createElement("iframe");
+ // Workaround for bug 1743933
+ iframe.loadPromise = resolveOnEvent(iframe, "load");
+ document.documentElement.appendChild(iframe);
+ return iframe;
+}
+
+runTest(async () => {
+ // A toplevel Window and an iframe Windows are compared for devicechange
+ // events.
+ const iframe1 = addIframe();
+ const iframe2 = addIframe();
+ await Promise.all([
+ iframe1.loadPromise,
+ iframe2.loadPromise,
+ pushPrefs(
+ // Use the fake video backend to trigger devicechange events.
+ ["media.navigator.streams.fake", true],
+ // Loopback would override fake.
+ ["media.video_loopback_dev", ""],
+ // Make fake devices count as real, permission-wise, or devicechange
+ // events won't be exposed
+ ["media.navigator.permission.fake", true],
+ // For gUM.
+ ["media.navigator.permission.disabled", true]
+ ),
+ ]);
+ const topDevices = navigator.mediaDevices;
+ const frame1Devices = iframe1.contentWindow.navigator.mediaDevices;
+ const frame2Devices = iframe2.contentWindow.navigator.mediaDevices;
+ // Initialization of MediaDevices::mLastPhysicalDevices is triggered when
+ // ondevicechange is set but tests "media.getusermedia.fake-camera-name"
+ // asynchronously. Wait for getUserMedia() completion to ensure that the
+ // pref has been read before doDevicechanges() changes it.
+ frame1Devices.ondevicechange = () => {};
+ const topEventPromise = resolveOnEvent(topDevices, "devicechange");
+ const frame2EventPromise = resolveOnEvent(frame2Devices, "devicechange");
+ (await frame1Devices.getUserMedia({video: true})).getTracks()[0].stop();
+
+ await Promise.all([
+ resolveOnEvent(frame1Devices, "devicechange"),
+ triggerVideoDevicechange(),
+ ]);
+ ok(true,
+ "devicechange event is fired when gUM has been in use");
+ // The number of devices has not changed. Race a settled Promise to check
+ // that no devicechange event has been received in frame2.
+ const racer = {};
+ is(await Promise.race([frame2EventPromise, racer]), racer,
+ "devicechange event is NOT fired in iframe2 for replaced device when " +
+ "gUM has NOT been in use");
+ // getUserMedia() is invoked on frame2Devices after a first device list
+ // change but before returning to the previous state, in order to test that
+ // the device set is compared with the set after previous device list
+ // changes regardless of whether a "devicechange" event was previously
+ // dispatched.
+ (await frame2Devices.getUserMedia({video: true})).getTracks()[0].stop();
+ // Revert device list change.
+ await Promise.all([
+ resolveOnEvent(frame1Devices, "devicechange"),
+ resolveOnEvent(frame2Devices, "devicechange"),
+ SpecialPowers.popPrefEnv(),
+ ]);
+ ok(true,
+ "devicechange event is fired on return to previous list " +
+ "after gUM has been is use");
+
+ const frame1EventPromise1 = resolveOnEvent(frame1Devices, "devicechange");
+ while (true) {
+ const racePromise = Promise.race([
+ frame1EventPromise1,
+ // 100ms is half the coalescing time in MediaManager::DeviceListChanged().
+ wait(100, {type: "wait done"}),
+ ]);
+ await triggerVideoDevicechange();
+ if ((await racePromise).type == "devicechange") {
+ ok(true,
+ "devicechange event is fired even when hardware changes continue");
+ break;
+ }
+ }
+
+ is(await Promise.race([topEventPromise, racer]), racer,
+ "devicechange event is NOT fired for device replacements when " +
+ "gUM has NOT been in use");
+
+ if (navigator.userAgent.includes("Android")) {
+ todo(false, "test assumes Firefox-for-Desktop specific API and behavior");
+ return;
+ }
+ // Open a new tab, which is expected to receive focus and hide the first tab.
+ const tab = window.open();
+ SimpleTest.registerCleanupFunction(() => tab.close());
+ await Promise.all([
+ resolveOnEvent(document, 'visibilitychange'),
+ resolveOnEvent(tab, 'focus'),
+ ]);
+ ok(tab.document.hasFocus(), "tab.document.hasFocus()");
+ await Promise.all([
+ resolveOnEvent(tab, 'blur'),
+ SpecialPowers.spawnChrome([], function focusUrlBar() {
+ this.browsingContext.topChromeWindow.gURLBar.focus();
+ }),
+ ]);
+ ok(!tab.document.hasFocus(), "!tab.document.hasFocus()");
+ is(document.visibilityState, 'hidden', 'visibilityState')
+ const frame1EventPromise2 = resolveOnEvent(frame1Devices, "devicechange");
+ const tabDevices = tab.navigator.mediaDevices;
+ tabDevices.ondevicechange = () => {};
+ const tabStream = await tabDevices.getUserMedia({video: true});
+ // Trigger and await two devicechanges on tabDevices to wait long enough to
+ // provide that a devicechange on another MediaDevices would be received.
+ for (let i = 0; i < 2; ++i) {
+ await Promise.all([
+ resolveOnEvent(tabDevices, "devicechange"),
+ triggerVideoDevicechange(),
+ ]);
+ };
+ is(await Promise.race([frame1EventPromise2, racer]), racer,
+ "devicechange event is NOT fired while tab is in background");
+ tab.close();
+ await resolveOnEvent(document, 'visibilitychange');
+ is(document.visibilityState, 'visible', 'visibilityState')
+ await frame1EventPromise2;
+ ok(true, "devicechange event IS fired when tab returns to foreground");
+
+ const audioLoopbackDev =
+ SpecialPowers.getCharPref("media.audio_loopback_dev", "");
+ if (!navigator.userAgent.includes("Linux")) {
+ todo_isnot(audioLoopbackDev, "", "audio_loopback_dev");
+ return;
+ }
+ isnot(audioLoopbackDev, "", "audio_loopback_dev");
+ await Promise.all([
+ resolveOnEvent(topDevices, "devicechange"),
+ pushPrefs(["media.audio_loopback_dev", "none"]),
+ ]);
+ ok(true,
+ "devicechange event IS fired when last audio device is removed and " +
+ "gUM has NOT been in use");
+ await Promise.all([
+ resolveOnEvent(topDevices, "devicechange"),
+ pushPrefs(["media.audio_loopback_dev", audioLoopbackDev]),
+ ]);
+ ok(true,
+ "devicechange event IS fired when first audio device is added and " +
+ "gUM has NOT been in use");
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_addAudioTrackToExistingVideoStream.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_addAudioTrackToExistingVideoStream.html
new file mode 100644
index 0000000000..b09d7ffeb5
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_addAudioTrackToExistingVideoStream.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1246310",
+ title: "Renegotiation: add audio track to existing video-only stream",
+ });
+
+ runNetworkTest(function (options) {
+ SimpleTest.requestCompleteLog();
+ const test = new PeerConnectionTest(options);
+ test.chain.replace("PC_LOCAL_GUM",
+ [
+ function PC_LOCAL_GUM_ATTACH_VIDEO_ONLY(test) {
+ var localConstraints = {audio: true, video: true};
+ test.setMediaConstraints([{video: true}], []);
+ return getUserMedia(localConstraints)
+ .then(s => test.originalGumStream = s)
+ .then(() => is(test.originalGumStream.getAudioTracks().length, 1,
+ "Should have 1 audio track"))
+ .then(() => is(test.originalGumStream.getVideoTracks().length, 1,
+ "Should have 1 video track"))
+ .then(() => test.pcLocal.attachLocalTrack(
+ test.originalGumStream.getVideoTracks()[0],
+ test.originalGumStream));
+ },
+ ]
+ );
+ addRenegotiation(test.chain,
+ [
+ function PC_LOCAL_ATTACH_SECOND_TRACK_AUDIO(test) {
+ test.setMediaConstraints([{audio: true, video: true}], []);
+ return test.pcLocal.attachLocalTrack(
+ test.originalGumStream.getAudioTracks()[0],
+ test.originalGumStream);
+ },
+ ],
+ [
+ function PC_CHECK_REMOTE_AUDIO_FLOW(test) {
+ return test.pcRemote.checkReceivingToneFrom(new AudioContext(), test.pcLocal);
+ }
+ ]
+ );
+
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_addDataChannel.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_addDataChannel.html
new file mode 100644
index 0000000000..c7536214e5
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_addDataChannel.html
@@ -0,0 +1,33 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1017888",
+ title: "Renegotiation: add DataChannel"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ addRenegotiation(test.chain,
+ commandsCreateDataChannel,
+ commandsCheckDataChannel);
+
+ // Insert before the second PC_LOCAL_WAIT_FOR_MEDIA_FLOW
+ test.chain.insertBefore('PC_LOCAL_WAIT_FOR_MEDIA_FLOW',
+ commandsWaitForDataChannel,
+ false,
+ 1);
+
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_addDataChannelNoBundle.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_addDataChannelNoBundle.html
new file mode 100644
index 0000000000..6ad754336c
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_addDataChannelNoBundle.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1017888",
+ title: "Renegotiation: add DataChannel"
+ });
+
+ runNetworkTest(function (options) {
+ options = options || { };
+ options.bundle = false;
+ const test = new PeerConnectionTest(options);
+ addRenegotiation(test.chain,
+ commandsCreateDataChannel.concat(
+ [
+ function PC_LOCAL_EXPECT_ICE_CHECKING(test) {
+ test.pcLocal.expectIceChecking();
+ },
+ function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+ test.pcRemote.expectIceChecking();
+ },
+ ]
+ ),
+ commandsCheckDataChannel);
+
+ // Insert before the second PC_LOCAL_WAIT_FOR_MEDIA_FLOW
+ test.chain.insertBefore('PC_LOCAL_WAIT_FOR_MEDIA_FLOW',
+ commandsWaitForDataChannel,
+ false,
+ 1);
+
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondAudioStream.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondAudioStream.html
new file mode 100644
index 0000000000..61a0250887
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondAudioStream.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1017888",
+ title: "Renegotiation: add second audio stream"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ addRenegotiation(test.chain,
+ [
+ function PC_LOCAL_ADD_SECOND_STREAM(test) {
+ test.setMediaConstraints([{audio: true}, {audio: true}],
+ [{audio: true}]);
+ return test.pcLocal.getAllUserMediaAndAddStreams([{audio: true}]);
+ },
+ ],
+ [
+ function PC_REMOTE_CHECK_ADDED_TRACK(test) {
+ // We test both tracks to avoid an ordering problem
+ is(test.pcRemote._pc.getReceivers().length, 2,
+ "pcRemote should have two receivers");
+ return Promise.all(test.pcRemote._pc.getReceivers().map(r => {
+ const analyser = new AudioStreamAnalyser(
+ new AudioContext(), new MediaStream([r.track]));
+ const freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ);
+ return analyser.waitForAnalysisSuccess(arr => arr[freq] > 200);
+ }));
+ },
+ ]
+ );
+
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondAudioStreamNoBundle.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondAudioStreamNoBundle.html
new file mode 100644
index 0000000000..32d0564717
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondAudioStreamNoBundle.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1017888",
+ title: "Renegotiation: add second audio stream, no bundle"
+ });
+
+ runNetworkTest(function (options = {}) {
+ options.bundle = false;
+ const test = new PeerConnectionTest(options);
+ addRenegotiation(test.chain,
+ [
+ function PC_LOCAL_ADD_SECOND_STREAM(test) {
+ test.setMediaConstraints([{audio: true}, {audio: true}],
+ [{audio: true}]);
+ // Since this is a NoBundle variant, adding a track will cause us to
+ // go back to checking.
+ test.pcLocal.expectIceChecking();
+ return test.pcLocal.getAllUserMediaAndAddStreams([{audio: true}]);
+ },
+ function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+ test.pcRemote.expectIceChecking();
+ },
+ ],
+ [
+ function PC_REMOTE_CHECK_ADDED_TRACK(test) {
+ // We test both tracks to avoid an ordering problem
+ is(test.pcRemote._pc.getReceivers().length, 2,
+ "pcRemote should have two receivers");
+ return Promise.all(test.pcRemote._pc.getReceivers().map(r => {
+ const analyser = new AudioStreamAnalyser(
+ new AudioContext(), new MediaStream([r.track]));
+ const freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ);
+ return analyser.waitForAnalysisSuccess(arr => arr[freq] > 200);
+ }));
+ },
+ ]
+ );
+
+ // TODO(bug 1093835): figure out how to verify if media flows through the new stream
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondVideoStream.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondVideoStream.html
new file mode 100644
index 0000000000..1565958d01
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondVideoStream.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1017888",
+ title: "Renegotiation: add second video stream"
+ });
+
+ runNetworkTest(async function (options) {
+ // Use fake video here since the native fake device on linux doesn't
+ // change color as needed by checkVideoPlaying() below.
+ await pushPrefs(
+ ['media.video_loopback_dev', ''],
+ ['media.navigator.streams.fake', true]);
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ const test = new PeerConnectionTest(options);
+ addRenegotiation(test.chain,
+ [
+ function PC_LOCAL_ADD_SECOND_STREAM(test) {
+ test.setMediaConstraints([{video: true}, {video: true}],
+ [{video: true}]);
+ return test.pcLocal.getAllUserMediaAndAddStreams([{video: true}]);
+ },
+ ],
+ [
+ function PC_REMOTE_CHECK_VIDEO_FLOW(test) {
+ const h = new VideoStreamHelper();
+ is(test.pcRemote.remoteMediaElements.length, 2,
+ "Should have two remote media elements after renegotiation");
+ return Promise.all(test.pcRemote.remoteMediaElements.map(video =>
+ h.checkVideoPlaying(video)));
+ },
+ ]
+ );
+
+ test.setMediaConstraints([{video: true, fake: true}], [{video: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondVideoStreamNoBundle.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondVideoStreamNoBundle.html
new file mode 100644
index 0000000000..2857100998
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_addSecondVideoStreamNoBundle.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1017888",
+ title: "Renegotiation: add second video stream, no bundle"
+ });
+
+ runNetworkTest(async function (options = {}) {
+ // Use fake video here since the native fake device on linux doesn't
+ // change color as needed by checkVideoPlaying() below.
+ await pushPrefs(
+ ['media.video_loopback_dev', ''],
+ ['media.navigator.streams.fake', true]);
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ options.bundle = false;
+ const test = new PeerConnectionTest(options);
+ addRenegotiation(test.chain,
+ [
+ function PC_LOCAL_ADD_SECOND_STREAM(test) {
+ test.setMediaConstraints([{video: true}, {video: true}],
+ [{video: true}]);
+ // Since this is a NoBundle variant, adding a track will cause us to
+ // go back to checking.
+ test.pcLocal.expectIceChecking();
+ return test.pcLocal.getAllUserMediaAndAddStreams([{video: true}]);
+ },
+ function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+ test.pcRemote.expectIceChecking();
+ },
+ ],
+ [
+ function PC_REMOTE_CHECK_VIDEO_FLOW(test) {
+ const h = new VideoStreamHelper();
+ is(test.pcRemote.remoteMediaElements.length, 2,
+ "Should have two remote media elements after renegotiation");
+ return Promise.all(test.pcRemote.remoteMediaElements.map(video =>
+ h.checkVideoPlaying(video)));
+ },
+ ]
+ );
+
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+ await test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_addtrack_removetrack_events.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_addtrack_removetrack_events.html
new file mode 100644
index 0000000000..ff9ca9a772
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_addtrack_removetrack_events.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+"use strict";
+
+createHTML({
+ title: "MediaStream's 'addtrack' and 'removetrack' events with gUM",
+ bug: "1208328"
+});
+
+runNetworkTest(function (options) {
+ let test = new PeerConnectionTest(options);
+ let eventsPromise;
+ addRenegotiation(test.chain,
+ [
+ function PC_LOCAL_SWAP_VIDEO_TRACKS(test) {
+ return getUserMedia({video: true}).then(stream => {
+ var videoTransceiver = test.pcLocal._pc.getTransceivers()[1];
+ is(videoTransceiver.currentDirection, "sendonly",
+ "Video transceiver's current direction is sendonly");
+ is(videoTransceiver.direction, "sendrecv",
+ "Video transceiver's desired direction is sendrecv");
+
+ const localStream = test.pcLocal._pc.getLocalStreams()[0];
+ ok(localStream, "Should have local stream");
+
+ const remoteStream = test.pcRemote._pc.getRemoteStreams()[0];
+ ok(remoteStream, "Should have remote stream");
+
+ const newTrack = stream.getTracks()[0];
+
+ const videoSenderIndex =
+ test.pcLocal._pc.getSenders().findIndex(s => s.track.kind == "video");
+ isnot(videoSenderIndex, -1, "Should have video sender");
+
+ test.pcLocal.removeSender(videoSenderIndex);
+ is(videoTransceiver.direction, "recvonly",
+ "Video transceiver should be recvonly after removeTrack");
+ test.pcLocal.attachLocalTrack(stream.getTracks()[0], localStream);
+ is(videoTransceiver.direction, "recvonly",
+ "Video transceiver should be recvonly after addTrack");
+
+ eventsPromise = haveEvent(remoteStream, "addtrack",
+ wait(50000, new Error("No addtrack event for " + newTrack.id)))
+ .then(trackEvent => {
+ ok(trackEvent instanceof MediaStreamTrackEvent,
+ "Expected event to be instance of MediaStreamTrackEvent");
+ is(trackEvent.type, "addtrack",
+ "Expected addtrack event type");
+ is(trackEvent.track.readyState, "live",
+ "added track should be live");
+ })
+ .then(() => haveNoEvent(remoteStream, "addtrack"));
+ });
+ },
+ ],
+ [
+ function PC_REMOTE_CHECK_EVENTS(test) {
+ return eventsPromise;
+ },
+ ]
+ );
+
+ test.setMediaConstraints([{audio: true, video: true}], []);
+ return test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_answererAddSecondAudioStream.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_answererAddSecondAudioStream.html
new file mode 100644
index 0000000000..d9b01bf722
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_answererAddSecondAudioStream.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1017888",
+ title: "Renegotiation: answerer adds second audio stream"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ addRenegotiationAnswerer(test.chain,
+ [
+ function PC_LOCAL_ADD_SECOND_STREAM(test) {
+ test.setMediaConstraints([{audio: true}, {audio: true}],
+ [{audio: true}]);
+ return test.pcLocal.getAllUserMediaAndAddStreams([{audio: true}]);
+ },
+ ]
+ );
+
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_audioChannels.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioChannels.html
new file mode 100644
index 0000000000..f6e77f8271
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioChannels.html
@@ -0,0 +1,102 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+
+createHTML({
+ bug: "1765005",
+ title: "Verify audio channel limits for each negotiated audio codec",
+});
+
+const matchesChannels = (sdp, codec, channels) =>
+ !!sdp.match(new RegExp(`a=rtpmap:\\d* ${codec}\/\\d*\/${channels}\r\n`, "g")) ||
+ (channels <= 1 &&
+ !!sdp.match(new RegExp(`a=rtpmap:\\d* ${codec}\/\\d*\r\n`, "g")));
+
+async function testAudioChannels(track, codec, channels, accepted, expectedChannels) {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ try {
+ pc1.addTrack(track);
+ pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate);
+ pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate);
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ let {type, sdp} = await pc2.createAnswer();
+ sdp = sdp.replace(new RegExp(`a=rtpmap:(\\d*) ${codec}\/(\\d*)\/?\\d*\r\n`, "g"),
+ `a=rtpmap:$1 ${codec}/$2/${channels}\r\n`);
+ const payloadType = Number(sdputils.findCodecId(sdp, codec));
+ sdp = sdputils.removeAllButPayloadType(sdp, payloadType);
+ ok(matchesChannels(sdp, codec, channels), "control");
+ await pc2.setLocalDescription({type, sdp});
+ is(matchesChannels(pc2.localDescription.sdp, codec, channels),
+ accepted,
+ `test pc2.localDescription`);
+ try {
+ await pc1.setRemoteDescription(pc2.localDescription);
+ ok(expectedChannels, "SRD should succeed iff we're expecting channels");
+ const [receiver] = pc2.getReceivers();
+ await new Promise(r => receiver.track.onunmute = r);
+ let stats = await receiver.getStats();
+ let inboundStat = [...stats.values()].find(({type}) => type == "inbound-rtp");
+ if (!inboundStat) {
+ info("work around bug 1765215"); // TODO bug 1765215
+ await new Promise(r => setTimeout(r, 200));
+ stats = await receiver.getStats();
+ inboundStat = [...stats.values()].find(({type}) => type == "inbound-rtp");
+ }
+ ok(inboundStat, "has inbound-rtp stats after track unmute (w/workaround)");
+ const codecStat = stats.get(inboundStat.codecId);
+ ok(codecStat.mimeType.includes(codec), "correct codec");
+ is(codecStat.payloadType, payloadType, "correct payloadType");
+ is(codecStat.channels, expectedChannels, "expected channels");
+ } catch (e) {
+ ok(!expectedChannels, "SRD should fail iff we're not expecting channels");
+ }
+ } finally {
+ pc1.close();
+ pc2.close();
+ }
+}
+
+runNetworkTest(async () => {
+ const [track] = (await navigator.mediaDevices.getUserMedia({audio: true}))
+ .getAudioTracks();
+ try {
+ for (let [codec, channels, accepted, expectedChannels] of [
+ ["opus", 2, true, 2],
+ ["opus", 1, true, 0],
+ ["opus", 1000, true, 0],
+ ["G722", 1, true, 1],
+ ["G722", 2, true, 0],
+ ["G722", 1000, true, 0],
+ ["PCMU", 1, true, 1],
+ ["PCMU", 2, false, 1],
+ ["PCMU", 1000, false, 1],
+ ["PCMA", 1, true, 1],
+ ["PCMA", 2, false, 1],
+ ["PCMA", 1000, false, 1]
+ ]) {
+ const testName = `${codec} with ${channels} channel(s) is ` +
+ `${accepted? "accepted" : "ignored"} and produces ` +
+ `${expectedChannels || "no"} channels`;
+ try {
+ info(`Testing that ${testName}`);
+ await testAudioChannels(track, codec, channels, accepted, expectedChannels);
+ } catch (e) {
+ ok(false, `Error testing that ${testName}: ${e}\n${e.stack}`);
+ }
+ }
+ } finally {
+ track.stop();
+ }
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_audioCodecs.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioCodecs.html
new file mode 100644
index 0000000000..8874436e3b
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioCodecs.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1395853",
+ title: "Verify audio content over WebRTC for every audio codec",
+ });
+
+ // We match the format member against the sdp to figure out the payload type,
+ // So all other present codecs can be removed.
+ const codecs = [ "opus", "G722", "PCMU", "PCMA" ];
+
+ async function testAudioCodec(options = {}, codec) {
+ // sdputils checks for opus as part of its sdp sanity test
+ options.opus = codec == "opus";
+
+ let test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}], []);
+
+ test.chain.insertBefore("PC_LOCAL_SET_LOCAL_DESCRIPTION", [
+ function PC_LOCAL_FILTER_OUT_CODECS() {
+ let otherCodec = codecs.find(c => c != codec);
+ let otherId = sdputils.findCodecId(test.originalOffer.sdp, otherCodec);
+
+ let id = sdputils.findCodecId(test.originalOffer.sdp, codec);
+ test.originalOffer.sdp =
+ sdputils.removeAllButPayloadType(test.originalOffer.sdp, id);
+
+ ok(!test.originalOffer.sdp.match(new RegExp(`m=.*UDP/TLS/RTP/SAVPF.* ${otherId}[^0-9]`, "gi")),
+ `Other codec ${otherId} should be removed after filtering`);
+ ok(test.originalOffer.sdp.match(new RegExp(`m=.*UDP/TLS/RTP/SAVPF.* ${id}[^0-9]`, "gi")),
+ `Tested codec ${id} should remain after filtering`);
+
+ for (let c of codecs.filter(c => c != codec)) {
+ // Remove rtpmaps for the other codecs so sdp sanity tests pass.
+ let id = sdputils.findCodecId(test.originalOffer.sdp, c);
+ test.originalOffer.sdp =
+ sdputils.removeRtpMapForPayloadType(test.originalOffer.sdp, id);
+ }
+
+ ok(!test.originalOffer.sdp.match(new RegExp(`a=rtpmap:${otherId}.*\\r\\n`, "gi")),
+ `Rtpmap of other codec ${otherId} should be removed after filtering`);
+ ok(test.originalOffer.sdp.match(new RegExp(`a=rtpmap:${id}.*\\r\\n`, "gi")),
+ `Rtpmap of tested codec should remain after filtering`);
+ },
+ ]);
+
+ test.chain.append([
+ async function CHECK_AUDIO_FLOW() {
+ try {
+ await test.pcRemote.checkReceivingToneFrom(new AudioContext(), test.pcLocal);
+ ok(true, "input and output audio data matches");
+ } catch(e) {
+ ok(false, `No audio flow: ${e}`);
+ }
+ },
+ ]);
+
+ await test.run();
+ }
+
+ runNetworkTest(async (options) => {
+ for (let codec of codecs) {
+ info(`Testing audio for codec ${codec}`);
+ try {
+ await testAudioCodec(options, codec);
+ } catch(e) {
+ ok(false, `Error in test for codec ${codec}: ${e}\n${e.stack}`);
+ }
+ info(`Tested audio for codec ${codec}`);
+ }
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_audioContributingSources.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioContributingSources.html
new file mode 100644
index 0000000000..333b40a888
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioContributingSources.html
@@ -0,0 +1,144 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1363667",
+ title: "Test audio receiver getContributingSources"
+ });
+
+ // test_peerConnection_audioSynchronizationSources.html tests
+ // much of the functionality of getContributingSources as the implementation
+ // is shared.
+ var testGetContributingSources = async (test) => {
+ const remoteReceiver = test.pcRemote.getReceivers()[0];
+ const localReceiver = test.pcLocal.getReceivers()[0];
+
+ // Check that getContributingSources is empty as there is no MCU
+ is(remoteReceiver.getContributingSources().length, 0,
+ "remote contributing sources is empty");
+ is(localReceiver.getContributingSources().length, 0,
+ "local contributing sources is empty");
+ // Wait for the next JS event loop iteration, to clear the cache
+ await Promise.resolve().then();
+ // Insert new entries as if there were an MCU
+ const csrc0 = 124756;
+ const timestamp0 = performance.now() + performance.timeOrigin;
+ const rtpTimestamp0 = 11111;
+ const hasAudioLevel0 = true;
+ // Audio level as expected to be received in RTP
+ const audioLevel0 = 34;
+ // Audio level as expected to be returned
+ const expectedAudioLevel0 = 10 ** (-audioLevel0 / 20);
+
+ SpecialPowers.wrap(remoteReceiver).mozInsertAudioLevelForContributingSource(
+ csrc0,
+ timestamp0,
+ rtpTimestamp0,
+ hasAudioLevel0,
+ audioLevel0);
+
+ const csrc1 = 5786;
+ const timestamp1 = timestamp0 - 200;
+ const rtpTimestamp1 = 22222;
+ const hasAudioLevel1 = false;
+ const audioLevel1 = 0;
+
+ SpecialPowers.wrap(remoteReceiver).mozInsertAudioLevelForContributingSource(
+ csrc1,
+ timestamp1,
+ rtpTimestamp1,
+ hasAudioLevel1,
+ audioLevel1);
+
+ const csrc2 = 93487;
+ const timestamp2 = timestamp0 - 200;
+ const rtpTimestamp2 = 333333;
+ const hasAudioLevel2 = true;
+ const audioLevel2 = 127;
+
+ SpecialPowers.wrap(remoteReceiver).mozInsertAudioLevelForContributingSource(
+ csrc2,
+ timestamp2,
+ rtpTimestamp2,
+ hasAudioLevel2,
+ audioLevel2);
+
+ const contributingSources = remoteReceiver.getContributingSources();
+ is(contributingSources.length, 3,
+ "Expected number of contributing sources");
+
+ // Check that both inserted were returned
+ const source0 = contributingSources.find(c => c.source == csrc0);
+ ok(source0, "first csrc was found");
+
+ const source1 = contributingSources.find(c => c.source == csrc1);
+ ok(source1, "second csrsc was found");
+
+ // Add a small margin of error in the timestamps
+ const compareTimestamps = (ts1, ts2) => Math.abs(ts1 - ts2) < 100;
+
+ // Check the CSRC with audioLevel
+ const isWithinErr = Math.abs(source0.audioLevel - expectedAudioLevel0)
+ < expectedAudioLevel0 / 50;
+ ok(isWithinErr,
+ `Contributing source has correct audio level. (${source0.audioLevel})`);
+ ok(compareTimestamps(source0.timestamp, timestamp0),
+ `Contributing source has correct timestamp (got ${source0.timestamp}), expected ${timestamp0}`);
+ is(source0.rtpTimestamp, rtpTimestamp0,
+ `Contributing source has correct RTP timestamp (${source0.rtpTimestamp}`);
+ // Check the CSRC without audioLevel
+ is(source1.audioLevel, undefined,
+ `Contributing source has no audio level. (${source1.audioLevel})`);
+ ok(compareTimestamps(source1.timestamp, timestamp1),
+ `Contributing source has correct timestamp (got ${source1.timestamp}, expected ${timestamp1})`);
+ is(source1.rtpTimestamp, rtpTimestamp1,
+ `Contributing source has correct RTP timestamp (${source1.rtpTimestamp}`);
+ // Check that a received RTP audio level 127 is exactly 0
+ const source2 = contributingSources.find(c => c.source == csrc2);
+ ok(source2, "third csrc was found");
+ is(source2.audioLevel, 0,
+ `Contributing source has audio level of 0 when RTP audio level is 127`);
+ // Check caching
+ is(JSON.stringify(contributingSources),
+ JSON.stringify(remoteReceiver.getContributingSources()),
+ "getContributingSources is cached");
+ // Check that sources are sorted in descending order by time stamp
+ const timestamp3 = performance.now() + performance.timeOrigin;
+ const rtpTimestamp3 = 44444;
+ // Larger offsets are further back in time
+ const testOffsets = [3, 7, 5, 6, 1, 4];
+ for (const offset of testOffsets) {
+ SpecialPowers.wrap(localReceiver).mozInsertAudioLevelForContributingSource(
+ offset, // Using offset for SSRC for convenience
+ timestamp3 - offset,
+ rtpTimestamp3,
+ true,
+ offset);
+ }
+ const sources = localReceiver.getContributingSources();
+ const sourceOffsets = sources.map(s => s.source);
+ is(JSON.stringify(sourceOffsets),
+ JSON.stringify([...testOffsets].sort((a, b) => a - b)),
+ `Contributing sources are sorted in descending order by timestamp:`
+ + ` ${JSON.stringify(sources)}`);
+ };
+
+ var test;
+ runNetworkTest(async function(options) {
+ test = new PeerConnectionTest(options);
+ test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW",
+ [testGetContributingSources]);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.pcLocal.audioElementsOnly = true;
+ await pushPrefs(["privacy.reduceTimerPrecision", false]);
+ await test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_audioRenegotiationInactiveAnswer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioRenegotiationInactiveAnswer.html
new file mode 100644
index 0000000000..6d3a23b57a
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioRenegotiationInactiveAnswer.html
@@ -0,0 +1,69 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="sdpUtils.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1213773",
+ title: "Renegotiation: answerer uses a=inactive for audio"
+ });
+
+ runNetworkTest(function (options) {
+ const helper = new AudioStreamHelper();
+
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}], []);
+ let haveFirstUnmuteEvent;
+
+ test.chain.insertBefore("PC_REMOTE_SET_LOCAL_DESCRIPTION", [
+ function PC_REMOTE_SETUP_ONUNMUTE_1() {
+ haveFirstUnmuteEvent = haveEvent(test.pcRemote._pc.getReceivers()[0].track, "unmute");
+ }
+ ]);
+
+ test.chain.append([
+ function PC_REMOTE_CHECK_AUDIO_UNMUTED() {
+ return haveFirstUnmuteEvent;
+ },
+ function PC_REMOTE_CHECK_AUDIO_FLOWING() {
+ return helper.checkAudioFlowing(test.pcRemote._pc.getRemoteStreams()[0]);
+ }
+ ]);
+
+ addRenegotiation(test.chain, []);
+
+ test.chain.insertAfter("PC_LOCAL_GET_ANSWER", [
+ function PC_LOCAL_REWRITE_REMOTE_SDP_INACTIVE(test) {
+ test._remote_answer.sdp =
+ sdputils.setAllMsectionsInactive(test._remote_answer.sdp);
+ }
+ ], false, 1);
+
+ test.chain.append([
+ function PC_REMOTE_CHECK_AUDIO_NOT_FLOWING() {
+ return helper.checkAudioNotFlowing(test.pcRemote._pc.getRemoteStreams()[0]);
+ }
+ ]);
+
+ test.chain.remove("PC_REMOTE_CHECK_STATS", 1);
+ test.chain.remove("PC_LOCAL_CHECK_STATS", 1);
+ test.chain.remove("PC_REMOTE_WAIT_FOR_MEDIA_FLOW", 1);
+
+ addRenegotiation(test.chain, []);
+
+ test.chain.append([
+ function PC_REMOTE_CHECK_AUDIO_FLOWING_2() {
+ return helper.checkAudioFlowing(test.pcRemote._pc.getRemoteStreams()[0]);
+ }
+ ]);
+
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_audioSynchronizationSources.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioSynchronizationSources.html
new file mode 100644
index 0000000000..32603b2e40
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioSynchronizationSources.html
@@ -0,0 +1,95 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1363667",
+ title: "Test audio receiver getSynchronizationSources"
+ });
+
+ var waitForSyncSources = async (test) => {
+ let receivers = [...test.pcRemote.getReceivers(),
+ ...test.pcLocal.getReceivers()];
+ is(receivers.length, 2, "Expected number of receivers");
+ // Wait for sync sources
+ while (true) {
+ if (receivers[0].getSynchronizationSources().length &&
+ receivers[1].getSynchronizationSources().length) {
+ break;
+ }
+ await wait(250);
+ }
+ };
+
+ var testGetSynchronizationSources = async (test) => {
+ await waitForSyncSources(test);
+ let receivers = [...test.pcRemote.getReceivers(),
+ ...test.pcLocal.getReceivers()];
+ is(receivers.length, 2,
+ `Expected number of receivers is 2. (${receivers.length})`);
+ for (let recv of receivers) {
+ let syncSources = recv.getSynchronizationSources();
+ ok(syncSources,
+ "Receiver has Synchronization sources " + JSON.stringify(syncSources));
+ is(syncSources.length, 1, "Each receiver has only a single sync source");
+ let source = recv.getSynchronizationSources()[0];
+ ok(source.audioLevel !== null,
+ `Synchronization source has audio level. (${source.audioLevel})`);
+ ok(source.audioLevel >= 0.0,
+ `Synchronization source audio level >= 0.0 (${source.audioLevel})`);
+ ok(source.audioLevel <= 1.0,
+ `Synchronization source audio level <= 1.0 (${source.audioLevel})`);
+ ok(source.timestamp,
+ `Synchronization source has timestamp (${source.timestamp})`);
+ const ageSeconds =
+ (window.performance.now() + window.performance.timeOrigin -
+ source.timestamp) / 1000;
+ ok(ageSeconds >= 0,
+ `Synchronization source timestamp is in the past`);
+ ok(ageSeconds < 2.5,
+ `Synchronization source timestamp is close to now`);
+ is(source.voiceActivityFlag, undefined,
+ "Synchronization source unsupported voiceActivity is undefined");
+ }
+ };
+
+ var testSynchronizationSourceCached = async (test) => {
+ await waitForSyncSources(test);
+ let receivers = [...test.pcRemote.getReceivers(),
+ ...test.pcLocal.getReceivers()];
+ is(receivers.length, 2,
+ `Expected number of receivers is 2. (${receivers.length})`);
+ let sourceSets = [[],[]];
+ for (let sourceSet of sourceSets) {
+ for (let recv of receivers) {
+ let sources = recv.getSynchronizationSources();
+ is(sources.length, 1,
+ `Expected number of sources is 1. (${sources.length})`);
+ sourceSet.push(sources);
+ }
+ // Busy wait 1s before trying again
+ let endTime = performance.now() + 1000;
+ while (performance.now() < endTime) {};
+ }
+ is(JSON.stringify(sourceSets[0]), JSON.stringify(sourceSets[1]),
+ "Subsequent getSynchronizationSources calls are cached.");
+ };
+
+ var test;
+ runNetworkTest(function(options) {
+ test = new PeerConnectionTest(options);
+ test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW",
+ [testGetSynchronizationSources,
+ testSynchronizationSourceCached]);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.pcLocal.audioElementsOnly = true;
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_audioSynchronizationSourcesUnidirectional.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioSynchronizationSourcesUnidirectional.html
new file mode 100644
index 0000000000..6d66614e91
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_audioSynchronizationSourcesUnidirectional.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1439001",
+ title: "Test audio unidirectional getSynchronizationSources"
+ });
+
+ var waitForSyncSources = async (test) => {
+ let receiver = test.pcRemote.getReceivers()[0];
+ ok(receiver, "Remote has a receiver");
+ // Wait for remote sync source
+ while (!receiver.getSynchronizationSources().length) {
+ await wait(250);
+ }
+ is(receiver.getSynchronizationSources().length, 1,
+ "Remote receiver has a synchronization source");
+ // Make sure local has no sync source
+ is(test.pcLocal.getReceivers()[0].getSynchronizationSources().length, 0,
+ "Local receiver has no synchronization source");
+ };
+ /*
+ * Test to make sure that in unidirectional calls, the receiving end has
+ * synchronization sources with audio levels, and the sending end has none.
+ */
+ var testGetSynchronizationSourcesUnidirectional = async (test) => {
+ await waitForSyncSources(test);
+ let receiver = test.pcRemote.getReceivers()[0];
+ let syncSources = receiver.getSynchronizationSources();
+ ok(syncSources.length,
+ "Receiver has Synchronization sources " + JSON.stringify(syncSources));
+ is(syncSources.length, 1, "Receiver has only a single sync source");
+ let syncSource = syncSources[0];
+ ok(syncSource.audioLevel !== undefined, "SynchronizationSource has audioLevel");
+ };
+
+ var test;
+ runNetworkTest(function(options) {
+ test = new PeerConnectionTest(options);
+ test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW",
+ [testGetSynchronizationSourcesUnidirectional]);
+ test.setMediaConstraints([{audio: true}], []);
+ test.pcLocal.audioElementsOnly = true;
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio.html
new file mode 100644
index 0000000000..5fd10a67f9
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "796892",
+ title: "Basic audio-only peer connection"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ // pc.js uses video elements by default, we want to test audio elements here
+ test.pcLocal.audioElementsOnly = true;
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioDynamicPtMissingRtpmap.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioDynamicPtMissingRtpmap.html
new file mode 100644
index 0000000000..a076bf80f1
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioDynamicPtMissingRtpmap.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1246011",
+ title: "Offer with dynamic PT but missing rtpmap"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ options = options || { };
+ // we want Opus to get selected and 101 to be ignored
+ options.opus = true;
+ test = new PeerConnectionTest(options);
+ test.chain.insertBefore("PC_REMOTE_GET_OFFER", [
+ function PC_LOCAL_REDUCE_MLINE_REMOVE_RTPMAPS(test) {
+ test.originalOffer.sdp =
+ sdputils.reduceAudioMLineToDynamicPtAndOpus(test.originalOffer.sdp);
+ test.originalOffer.sdp =
+ sdputils.removeAllRtpMaps(test.originalOffer.sdp);
+ test.originalOffer.sdp = test.originalOffer.sdp + "a=rtpmap:109 opus/48000/2\r\n";
+ info("SDP with dyn PT and no Rtpmap: " + JSON.stringify(test.originalOffer));
+ }
+ ]);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelay.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelay.html
new file mode 100644
index 0000000000..180abc075a
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelay.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="nonTrickleIce.js"></script>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1231975",
+ title: "Basic audio-only peer connection with port dependent NAT, for verifying UDP relay"
+});
+
+// This test uses the NAT simulator, which doesn't work in https, so we turn
+// on getUserMedia in http, which requires a reload.
+if (!("mediaDevices" in navigator)) {
+ SpecialPowers.pushPrefEnv({set: [['media.devices.insecure.enabled', true]]},
+ () => location.reload());
+} else {
+ runNetworkTest(async (options = {}) => {
+ await pushPrefs(
+ ['media.peerconnection.ice.obfuscate_host_addresses', false],
+ ['media.peerconnection.nat_simulator.filtering_type', 'PORT_DEPENDENT'],
+ ['media.peerconnection.nat_simulator.mapping_type', 'PORT_DEPENDENT'],
+ ['media.peerconnection.nat_simulator.block_tcp', true],
+ ['media.getusermedia.insecure.enabled', true]);
+ options.expectedLocalCandidateType = "srflx";
+ options.expectedRemoteCandidateType = "relay";
+ // If both have TURN, it is a toss-up which one will end up using a
+ // relay.
+ options.turn_disabled_local = true;
+ const test = new PeerConnectionTest(options);
+ // Make sure we don't end up choosing the wrong thing due to delays in
+ // trickle. Once we are willing to accept trickle after ICE success, we
+ // can maybe wait a bit to allow things to stabilize.
+ // TODO(bug 1238249)
+ makeOffererNonTrickle(test.chain);
+ makeAnswererNonTrickle(test.chain);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ await test.run();
+ }, { useIceServer: true });
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTCP.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTCP.html
new file mode 100644
index 0000000000..7bb51764bd
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTCP.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1231975",
+ title: "Basic audio-only peer connection with port dependent NAT that blocks UDP"
+});
+
+// This test uses the NAT simulator, which doesn't work in https, so we turn
+// on getUserMedia in http, which requires a reload.
+if (!("mediaDevices" in navigator)) {
+ SpecialPowers.pushPrefEnv({set: [['media.devices.insecure.enabled', true]]},
+ () => location.reload());
+} else {
+ runNetworkTest(async (options = {}) => {
+ await pushPrefs(
+ ['media.peerconnection.ice.obfuscate_host_addresses', false],
+ ['media.peerconnection.nat_simulator.filtering_type', 'PORT_DEPENDENT'],
+ ['media.peerconnection.nat_simulator.mapping_type', 'PORT_DEPENDENT'],
+ ['media.peerconnection.nat_simulator.block_udp', true],
+ ['media.peerconnection.nat_simulator.block_tcp', false],
+ ['media.peerconnection.nat_simulator.block_tls', true],
+ ['media.peerconnection.ice.loopback', true],
+ ['media.getusermedia.insecure.enabled', true]);
+ options.expectedLocalCandidateType = "relay-tcp";
+ options.expectedRemoteCandidateType = "relay-tcp";
+ // No reason to wait for gathering to complete like the other NAT tests,
+ // since relayed-tcp is the only thing that can work.
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ await test.run();
+ }, { useIceServer: true });
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTCPWithStun300.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTCPWithStun300.html
new file mode 100644
index 0000000000..43ea6aaea7
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTCPWithStun300.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="nonTrickleIce.js"></script>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "857668",
+ title: "Basic audio-only peer connection with UDP-blocking NAT, for verifying TCP relay with STUN 300 responses"
+});
+
+// This test uses the NAT simulator, which doesn't work in https, so we turn
+// on getUserMedia in http, which requires a reload.
+if (!("mediaDevices" in navigator)) {
+ SpecialPowers.pushPrefEnv({set: [['media.devices.insecure.enabled', true]]},
+ () => location.reload());
+} else {
+ runNetworkTest(async (options = {}) => {
+ await pushPrefs(
+ ['media.peerconnection.ice.obfuscate_host_addresses', false],
+ ['media.peerconnection.nat_simulator.filtering_type', 'ENDPOINT_INDEPENDENT'],
+ ['media.peerconnection.nat_simulator.mapping_type', 'ENDPOINT_INDEPENDENT'],
+ ['media.peerconnection.nat_simulator.block_udp', true],
+ ['media.peerconnection.nat_simulator.block_tcp', false],
+ ['media.peerconnection.ice.loopback', true],
+ ['media.getusermedia.insecure.enabled', true]);
+ options.expectedLocalCandidateType = "relay-tcp";
+ options.expectedRemoteCandidateType = "relay-tcp";
+ const turnServer = iceServersArray.find(server => "username" in server);
+ const turnRedirectPort = turnServer.turn_redirect_port;
+ const turnHostname = getTurnHostname(turnServer.urls[0]);
+ turnServer.urls = [`turn:${turnHostname}:${turnRedirectPort}?transport=tcp`];
+ // Override turn servers so we can test simulated redirects
+ options.config_local = {iceServers: [turnServer]};
+ options.config_remote = {iceServers: [turnServer]};
+ const test = new PeerConnectionTest(options);
+ // Make sure we don't end up choosing the wrong thing due to delays in
+ // trickle. Once we are willing to accept trickle after ICE success, we
+ // can maybe wait a bit to allow things to stabilize.
+ // TODO(bug 1238249)
+ makeOffererNonTrickle(test.chain);
+ makeAnswererNonTrickle(test.chain);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ await test.run();
+ await SpecialPowers.popPrefEnv();
+ }, { useIceServer: true });
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTLS.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTLS.html
new file mode 100644
index 0000000000..7446401f87
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayTLS.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1231975",
+ title: "Basic audio-only peer connection with port dependent NAT that blocks STUN"
+});
+
+// This test uses the NAT simulator, which doesn't work in https, so we turn
+// on getUserMedia in http, which requires a reload.
+if (!("mediaDevices" in navigator)) {
+ SpecialPowers.pushPrefEnv({set: [['media.devices.insecure.enabled', true]]},
+ () => location.reload());
+} else {
+ runNetworkTest(async (options = {}) => {
+ await pushPrefs(
+ ['media.peerconnection.ice.obfuscate_host_addresses', false],
+ ['media.peerconnection.nat_simulator.filtering_type', 'PORT_DEPENDENT'],
+ ['media.peerconnection.nat_simulator.mapping_type', 'PORT_DEPENDENT'],
+ ['media.peerconnection.nat_simulator.block_udp', true],
+ ['media.peerconnection.nat_simulator.block_tcp', true],
+ ['media.peerconnection.ice.loopback', true],
+ ['media.getusermedia.insecure.enabled', true]);
+ options.expectedLocalCandidateType = "relay-tls";
+ options.expectedRemoteCandidateType = "relay-tls";
+ // No reason to wait for gathering to complete like the other NAT tests,
+ // since relayed-tcp is the only thing that can work.
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ await test.run();
+ }, { useIceServer: true });
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayWithStun300.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayWithStun300.html
new file mode 100644
index 0000000000..286e67bc2f
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATRelayWithStun300.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="nonTrickleIce.js"></script>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "857668",
+ title: "Basic audio-only peer connection with port dependent NAT, for verifying UDP relay with STUN 300 responses"
+});
+
+// This test uses the NAT simulator, which doesn't work in https, so we turn
+// on getUserMedia in http, which requires a reload.
+if (!("mediaDevices" in navigator)) {
+ SpecialPowers.pushPrefEnv({set: [['media.devices.insecure.enabled', true]]},
+ () => location.reload());
+} else {
+ runNetworkTest(async (options = {}) => {
+ await pushPrefs(
+ ['media.peerconnection.ice.obfuscate_host_addresses', false],
+ ['media.peerconnection.nat_simulator.filtering_type', 'PORT_DEPENDENT'],
+ ['media.peerconnection.nat_simulator.mapping_type', 'PORT_DEPENDENT'],
+ ['media.peerconnection.nat_simulator.block_tcp', true],
+ ['media.getusermedia.insecure.enabled', true]);
+ options.expectedLocalCandidateType = "srflx";
+ options.expectedRemoteCandidateType = "relay";
+ const turnServer = iceServersArray.find(server => "username" in server);
+ const turnRedirectPort = turnServer.turn_redirect_port;
+ const turnHostname = getTurnHostname(turnServer.urls[0]);
+ turnServer.urls = [`turn:${turnHostname}:${turnRedirectPort}`];
+ // Override turn servers so we can test redirects
+ options.config_remote = {iceServers: [turnServer]};
+ // If both have TURN, it is a toss-up which one will end up using a
+ // relay, so we disable TURN for one side.
+ options.turn_disabled_local = true;
+ const test = new PeerConnectionTest(options);
+ // Make sure we don't end up choosing the wrong thing due to delays in
+ // trickle. Once we are willing to accept trickle after ICE success, we
+ // can maybe wait a bit to allow things to stabilize.
+ // TODO(bug 1238249)
+ makeOffererNonTrickle(test.chain);
+ makeAnswererNonTrickle(test.chain);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ await test.run();
+ }, { useIceServer: true });
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATSrflx.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATSrflx.html
new file mode 100644
index 0000000000..78fa8bcb2c
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNATSrflx.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="nonTrickleIce.js"></script>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1231975",
+ title: "Basic audio-only peer connection with endpoint independent NAT, for verifying UDP srflx"
+});
+
+// This test uses the NAT simulator, which doesn't work in https, so we turn
+// on getUserMedia in http, which requires a reload.
+if (!("mediaDevices" in navigator)) {
+ SpecialPowers.pushPrefEnv({set: [['media.devices.insecure.enabled', true]]},
+ () => location.reload());
+} else {
+ runNetworkTest(async (options = {}) => {
+ await pushPrefs(
+ ['media.peerconnection.ice.obfuscate_host_addresses', false],
+ ['media.peerconnection.nat_simulator.filtering_type', 'ENDPOINT_INDEPENDENT'],
+ ['media.peerconnection.nat_simulator.mapping_type', 'ENDPOINT_INDEPENDENT'],
+ ['media.peerconnection.nat_simulator.block_tcp', true],
+ ['media.getusermedia.insecure.enabled', true]);
+ options.expectedLocalCandidateType = "srflx";
+ options.expectedRemoteCandidateType = "srflx";
+ const test = new PeerConnectionTest(options);
+ // Make sure we don't end up choosing the wrong thing due to delays in
+ // trickle. Once we are willing to accept trickle after ICE success, we
+ // can maybe wait a bit to allow things to stabilize.
+ // TODO(bug 1238249)
+ makeOffererNonTrickle(test.chain);
+ makeAnswererNonTrickle(test.chain);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ await test.run();
+ }, { useIceServer: true });
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNoisyUDPBlock.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNoisyUDPBlock.html
new file mode 100644
index 0000000000..297121cd94
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioNoisyUDPBlock.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1231975",
+ title: "Basic audio-only peer connection where UDP sockets return errors on send"
+});
+
+// This test uses the NAT simulator, which doesn't work in https, so we turn
+// on getUserMedia in http, which requires a reload.
+if (!("mediaDevices" in navigator)) {
+ SpecialPowers.pushPrefEnv({set: [['media.devices.insecure.enabled', true]]},
+ () => location.reload());
+} else {
+ runNetworkTest(async (options = {}) => {
+ await pushPrefs(
+ ['media.peerconnection.ice.obfuscate_host_addresses', false],
+ ['media.peerconnection.nat_simulator.filtering_type', 'PORT_DEPENDENT'],
+ ['media.peerconnection.nat_simulator.mapping_type', 'PORT_DEPENDENT'],
+ ['media.peerconnection.nat_simulator.block_udp', true],
+ ['media.peerconnection.nat_simulator.error_code_for_drop', 3 /*R_INTERNAL*/],
+ ['media.peerconnection.nat_simulator.block_tls', true],
+ ['media.getusermedia.insecure.enabled', true]);
+ options.expectedLocalCandidateType = "relay-tcp";
+ options.expectedRemoteCandidateType = "relay-tcp";
+ // No reason to wait for gathering to complete like the other NAT tests,
+ // since relayed-tcp is the only thing that can work.
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ await test.run();
+ }, { useIceServer: true });
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioPcmaPcmuOnly.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioPcmaPcmuOnly.html
new file mode 100644
index 0000000000..f0fe721b8e
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioPcmaPcmuOnly.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1221837",
+ title: "Only offer PCMA and PMCU in mline (no rtpmaps)"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ options = options || { };
+ options.opus = false;
+ test = new PeerConnectionTest(options);
+ test.chain.insertBefore("PC_REMOTE_GET_OFFER", [
+ function PC_LOCAL_REDUCE_MLINE_REMOVE_RTPMAPS(test) {
+ test.originalOffer.sdp =
+ sdputils.reduceAudioMLineToPcmuPcma(test.originalOffer.sdp);
+ test.originalOffer.sdp =
+ sdputils.removeAllRtpMaps(test.originalOffer.sdp);
+ info("SDP without Rtpmaps: " + JSON.stringify(test.originalOffer));
+ }
+ ]);
+ test.chain.insertAfter("PC_REMOTE_SANE_LOCAL_SDP", [
+ function PC_REMOTE_VERIFY_PCMU(test) {
+ ok(test._remote_answer.sdp.includes("a=rtpmap:0 PCMU/8000"), "PCMU codec is present in SDP");
+ }
+ ]);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioRelayPolicy.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioRelayPolicy.html
new file mode 100644
index 0000000000..ced57ff8a3
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioRelayPolicy.html
@@ -0,0 +1,83 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1663746",
+ title: "Basic tests for relay ice policy"
+});
+
+runNetworkTest(async () => {
+ await pushPrefs(
+ // Enable mDNS, since there are some checks we want to run with that
+ ['media.peerconnection.ice.obfuscate_host_addresses', true]);
+
+ const offerer = new RTCPeerConnection({iceServers: iceServersArray, iceTransportPolicy: 'relay'});
+ const answerer = new RTCPeerConnection({iceServers: iceServersArray});
+
+ offerer.onicecandidate = e => {
+ if (e.candidate) {
+ ok(!e.candidate.candidate.includes(' host '), 'IceTransportPolicy \"relay\" should prevent the advertisement of host candidates');
+ ok(!e.candidate.candidate.includes(' srflx '), 'IceTransportPolicy \"relay\" should prevent the advertisement of srflx candidates');
+ }
+ answerer.addIceCandidate(e.candidate);
+ };
+
+ answerer.onicecandidate = e => {
+ if (e.candidate && e.candidate.candidate.includes(' host ')) {
+ ok(e.candidate.candidate.includes('.local'), 'When obfuscate_host_addresses is true, we expect host candidates to use mDNS');
+ }
+ offerer.addIceCandidate(e.candidate);
+ };
+
+ const offererConnected = new Promise(r => {
+ offerer.oniceconnectionstatechange = () => {
+ if (offerer.iceConnectionState == 'connected') {
+ r();
+ }
+ };
+ });
+
+ const answererConnected = new Promise(r => {
+ answerer.oniceconnectionstatechange = () => {
+ if (answerer.iceConnectionState == 'connected') {
+ r();
+ }
+ };
+ });
+
+ const offer = await offerer.createOffer({offerToReceiveAudio: true});
+ await Promise.all([offerer.setLocalDescription(offer), answerer.setRemoteDescription(offer)]);
+ const answer = await answerer.createAnswer();
+ await Promise.all([answerer.setLocalDescription(answer), offerer.setRemoteDescription(answer)]);
+
+ info('Waiting for ICE to connect');
+ await Promise.all([offererConnected, answererConnected]);
+
+ const offererStats = await offerer.getStats();
+ const localCandidates = [...offererStats.values()].filter(stat => stat.type == 'local-candidate');
+ const remoteCandidates = [...offererStats.values()].filter(stat => stat.type == 'remote-candidate');
+ isnot(localCandidates, []);
+ isnot(remoteCandidates, []);
+
+ const localNonRelayCandidates =
+ localCandidates.filter(cand => cand.candidateType != 'relay');
+ is(localNonRelayCandidates.length, 0, `There should only be local relay candidates, because we are using the "relay" IceTransportPolicy, but we got ${JSON.stringify(localNonRelayCandidates)}`);
+
+ const remoteHostCandidates =
+ remoteCandidates.filter(cand => cand.candidateType == 'host');
+ is(remoteHostCandidates.length, 0, `There should be no remote host candidates in the stats, because mDNS resolution should have been disabled by the "relay" IceTransportPolicy, but we got ${JSON.stringify(remoteHostCandidates)}`);
+
+ offerer.close();
+ answerer.close();
+
+ await SpecialPowers.popPrefEnv();
+}, { useIceServer: true });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioRequireEOC.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioRequireEOC.html
new file mode 100644
index 0000000000..afad4550d4
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioRequireEOC.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1167443",
+ title: "Basic audio-only peer connection which waits for end-of-candidates"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ test.chain.replace("PC_LOCAL_VERIFY_SDP_AFTER_END_OF_TRICKLE", [
+ function PC_LOCAL_REQUIRE_SDP_AFTER_END_OF_TRICKLE(test) {
+ return test.pcLocal.endOfTrickleSdp.then(sdp =>
+ sdputils.checkSdpAfterEndOfTrickle(sdp, test.testOptions, test.pcLocal.label));
+ }
+ ]);
+ test.chain.replace("PC_REMOTE_VERIFY_SDP_AFTER_END_OF_TRICKLE", [
+ function PC_REMOTE_REQUIRE_SDP_AFTER_END_OF_TRICKLE(test) {
+ return test.pcRemote.endOfTrickleSdp.then(sdp =>
+ sdputils.checkSdpAfterEndOfTrickle(sdp, test.testOptions, test.pcRemote.label));
+ }
+ ]);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVerifyRtpHeaderExtensions.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVerifyRtpHeaderExtensions.html
new file mode 100644
index 0000000000..f28a990bd2
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVerifyRtpHeaderExtensions.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="parser_rtp.js"></script>
+ <script type="application/javascript" src="sdpUtils.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1416932",
+ title: "Basic audio-only peer connection and verify rtp header extensions"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ // pc.js uses video elements by default, we want to test audio elements here
+ test.pcLocal.audioElementsOnly = true;
+
+ let getRtpPacket = (pc) => {
+ // we only examine received packets
+ let sending = false;
+ pc.mozEnablePacketDump(0, "rtp", sending);
+ return new Promise((res, rej) =>
+ pc.mozSetPacketCallback((...args) => {
+ res([...args]);
+ pc.mozSetPacketCallback(() => {});
+ pc.mozDisablePacketDump(0, "rtp", sending);
+ })
+ );
+ }
+
+ const pc = SpecialPowers.wrap(test.pcRemote._pc);
+ const haveFirstPacket = getRtpPacket(pc);
+
+ test.chain.insertBefore('PC_REMOTE_WAIT_FOR_MEDIA_FLOW', [
+ async function PC_REMOTE_CHECK_RTP_HEADER_EXTS_AGAINST_SDP() {
+
+ const sdpExtmapIds = sdputils.findExtmapIds(test.originalAnswer.sdp);
+
+ const [level, type, sending, data] = await haveFirstPacket;
+ const extensions = ParseRtpPacket(data).header.extensions;
+
+ // make sure we got the same number of rtp header extensions in
+ // the received packet as were negotiated in the sdp. Then
+ // check to make sure each of the received extension ids were in
+ // the sdp.
+ is(sdpExtmapIds.length, extensions.length, "number of received ids match sdp ids");
+ // note, we are comparing a number (from the parsed rtp packet)
+ // and a string (from the answer sdp)
+ ok(extensions.every((ext) => sdpExtmapIds.includes(""+ext.id)), "extension id arrays equivalent");
+ }
+ ]);
+
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideo.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideo.html
new file mode 100644
index 0000000000..c2c2d43f09
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideo.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "796890",
+ title: "Basic audio/video (separate) peer connection"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoCombined.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoCombined.html
new file mode 100644
index 0000000000..02a561f9b8
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoCombined.html
@@ -0,0 +1,24 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "796890",
+ title: "Basic audio/video (combined) peer connection"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true, video: true}],
+ [{audio: true, video: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoBundle.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoBundle.html
new file mode 100644
index 0000000000..cae7f6617f
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoBundle.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1016476",
+ title: "Basic audio/video peer connection with no Bundle"
+ });
+
+ runNetworkTest(options => {
+ options = options || { };
+ options.bundle = false;
+ var test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoBundleNoRtcpMux.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoBundleNoRtcpMux.html
new file mode 100644
index 0000000000..49b0136752
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoBundleNoRtcpMux.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1167443",
+ title: "Basic audio & video call with disabled bundle and disabled RTCP-Mux"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ options = options || { };
+ options.bundle = false;
+ options.rtcpmux = false;
+ test = new PeerConnectionTest(options);
+ test.chain.replace("PC_LOCAL_VERIFY_SDP_AFTER_END_OF_TRICKLE", [
+ function PC_LOCAL_REQUIRE_SDP_AFTER_END_OF_TRICKLE(test) {
+ return test.pcLocal.endOfTrickleSdp .then(sdp =>
+ sdputils.checkSdpAfterEndOfTrickle(sdp, test.testOptions, test.pcLocal.label));
+ }
+ ]);
+ test.chain.replace("PC_REMOTE_VERIFY_SDP_AFTER_END_OF_TRICKLE", [
+ function PC_REMOTE_REQUIRE_SDP_AFTER_END_OF_TRICKLE(test) {
+ return test.pcRemote.endOfTrickleSdp .then(sdp =>
+ sdputils.checkSdpAfterEndOfTrickle(sdp, test.testOptions, test.pcRemote.label));
+ }
+ ]);
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoRtcpMux.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoRtcpMux.html
new file mode 100644
index 0000000000..48524604ba
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoNoRtcpMux.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1167443",
+ title: "Basic audio & video call with disabled RTCP-Mux"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ options = options || { };
+ options.rtcpmux = false;
+ test = new PeerConnectionTest(options);
+ test.chain.replace("PC_LOCAL_VERIFY_SDP_AFTER_END_OF_TRICKLE", [
+ function PC_LOCAL_REQUIRE_SDP_AFTER_END_OF_TRICKLE(test) {
+ return test.pcLocal.endOfTrickleSdp .then(sdp =>
+ sdputils.checkSdpAfterEndOfTrickle(sdp, test.testOptions, test.pcLocal.label));
+ }
+ ]);
+ test.chain.replace("PC_REMOTE_VERIFY_SDP_AFTER_END_OF_TRICKLE", [
+ function PC_REMOTE_REQUIRE_SDP_AFTER_END_OF_TRICKLE(test) {
+ return test.pcRemote.endOfTrickleSdp .then(sdp =>
+ sdputils.checkSdpAfterEndOfTrickle(sdp, test.testOptions, test.pcRemote.label));
+ }
+ ]);
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoTransceivers.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoTransceivers.html
new file mode 100644
index 0000000000..181d089d26
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoTransceivers.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1290948",
+ title: "Basic audio/video with addTransceiver"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+ test.chain.replace("PC_LOCAL_GUM",
+ [
+ function PC_LOCAL_GUM_TRANSCEIVERS(test) {
+ return test.pcLocal.getAllUserMediaAndAddTransceivers(test.pcLocal.constraints);
+ }
+ ]);
+
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyExtmap.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyExtmap.html
new file mode 100644
index 0000000000..e3da00bfa5
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyExtmap.html
@@ -0,0 +1,97 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1406529",
+ title: "Verify SDP extmap attribute for sendrecv connection"
+ });
+
+ var test;
+ runNetworkTest(async function (options) {
+ await pushPrefs(["media.navigator.video.use_transport_cc", true]);
+
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+
+ test.chain.insertAfter('PC_LOCAL_SET_LOCAL_DESCRIPTION', [
+ async function PC_LOCAL_CHECK_SDP_OFFER_EXTMAP() {
+ sdputils.verify_unique_extmap_ids(test.originalOffer.sdp);
+
+ const audio = sdputils.findExtmapIdsUrnsDirections(
+ sdputils.getAudioMSections(test.originalOffer.sdp));
+ const expected_audio = [
+ /* Please modify this list when you add or remove RTP header
+ extensions. */
+ ["1", "urn:ietf:params:rtp-hdrext:ssrc-audio-level", ""],
+ ["2", "urn:ietf:params:rtp-hdrext:csrc-audio-level", "recvonly"],
+ ["3", "urn:ietf:params:rtp-hdrext:sdes:mid", ""],
+ ];
+ // *Ugh* ...
+ ok(JSON.stringify(audio) ===
+ JSON.stringify(expected_audio),
+ "List of offer audio URNs meets expected values");
+
+ const video = sdputils.findExtmapIdsUrnsDirections(
+ sdputils.getVideoMSections(test.originalOffer.sdp));
+ const expected_video = [
+ /* Please modify this list when you add or remove RTP header
+ extensions. */
+ ["3", "urn:ietf:params:rtp-hdrext:sdes:mid", ""],
+ ["4", "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time", ""],
+ ["5", "urn:ietf:params:rtp-hdrext:toffset", ""],
+ ["6", "http://www.webrtc.org/experiments/rtp-hdrext/playout-delay", "recvonly"],
+ ["7", "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", ""],
+ ];
+ // *Ugh* ...
+ ok(JSON.stringify(video) ===
+ JSON.stringify(expected_video),
+ "List of offer video URNs meets expected values");
+ }
+ ]);
+
+ test.chain.removeAfter('PC_REMOTE_SET_LOCAL_DESCRIPTION');
+ test.chain.append([
+ async function PC_REMOTE_CHECK_SDP_ANSWER_EXTMAP() {
+ sdputils.verify_unique_extmap_ids(test.originalAnswer.sdp);
+
+ const audio = sdputils.findExtmapIdsUrnsDirections(
+ sdputils.getAudioMSections(test.originalAnswer.sdp));
+ const expected_audio = [
+ /* Please modify this list when you add or remove RTP header
+ extensions. */
+ ["1", "urn:ietf:params:rtp-hdrext:ssrc-audio-level",""],
+ ["3", "urn:ietf:params:rtp-hdrext:sdes:mid",""],
+ ];
+ // *Ugh* ...
+ ok(JSON.stringify(audio) ===
+ JSON.stringify(expected_audio),
+ "List of answer audio URNs meets expected values");
+
+ const video = sdputils.findExtmapIdsUrnsDirections(
+ sdputils.getVideoMSections(test.originalAnswer.sdp));
+ const expected_video = [
+ /* Please modify this list when you add or remove RTP header
+ extensions. */
+ ["3", "urn:ietf:params:rtp-hdrext:sdes:mid",""],
+ ["4", "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time",""],
+ ["5", "urn:ietf:params:rtp-hdrext:toffset",""],
+ ["7", "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", ""],
+ ];
+ ok(JSON.stringify(video) ===
+ JSON.stringify(expected_video),
+ "List of answer video URNs meets expected values");
+ }
+ ]);
+
+ await test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyExtmapSendonly.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyExtmapSendonly.html
new file mode 100644
index 0000000000..6cbc9e4c00
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyExtmapSendonly.html
@@ -0,0 +1,97 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1406529",
+ title: "Verify SDP extmap attribute for sendonly connection"
+ });
+
+ var test;
+ runNetworkTest(async function (options) {
+ await pushPrefs(["media.navigator.video.use_transport_cc", true]);
+
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ []);
+
+ test.chain.insertAfter('PC_LOCAL_SET_LOCAL_DESCRIPTION', [
+ async function PC_LOCAL_CHECK_SDP_OFFER_EXTMAP() {
+ sdputils.verify_unique_extmap_ids(test.originalOffer.sdp);
+
+ const audio = sdputils.findExtmapIdsUrnsDirections(
+ sdputils.getAudioMSections(test.originalOffer.sdp));
+ const expected_audio = [
+ /* Please modify this list when you add or remove RTP header
+ extensions. */
+ ["1", "urn:ietf:params:rtp-hdrext:ssrc-audio-level", ""],
+ ["2", "urn:ietf:params:rtp-hdrext:csrc-audio-level", "recvonly"],
+ ["3", "urn:ietf:params:rtp-hdrext:sdes:mid", ""],
+ ];
+ // *Ugh* ...
+ ok(JSON.stringify(audio) ===
+ JSON.stringify(expected_audio),
+ "List of offer audio URNs meets expected values");
+
+ const video = sdputils.findExtmapIdsUrnsDirections(
+ sdputils.getVideoMSections(test.originalOffer.sdp));
+ const expected_video = [
+ /* Please modify this list when you add or remove RTP header
+ extensions. */
+ ["3", "urn:ietf:params:rtp-hdrext:sdes:mid", ""],
+ ["4", "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time", ""],
+ ["5", "urn:ietf:params:rtp-hdrext:toffset", ""],
+ ["6", "http://www.webrtc.org/experiments/rtp-hdrext/playout-delay", "recvonly"],
+ ["7", "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", ""],
+ ];
+ // *Ugh* ...
+ ok(JSON.stringify(video) ===
+ JSON.stringify(expected_video),
+ "List of offer video URNs meets expected values");
+ }
+ ]);
+
+ test.chain.removeAfter('PC_REMOTE_SET_LOCAL_DESCRIPTION');
+ test.chain.append([
+ async function PC_REMOTE_CHECK_SDP_ANSWER_EXTMAP() {
+ sdputils.verify_unique_extmap_ids(test.originalAnswer.sdp);
+
+ const audio = sdputils.findExtmapIdsUrnsDirections(
+ sdputils.getAudioMSections(test.originalAnswer.sdp));
+ const expected_audio = [
+ /* Please modify this list when you add or remove RTP header
+ extensions. */
+ ["1", "urn:ietf:params:rtp-hdrext:ssrc-audio-level",""],
+ ["3", "urn:ietf:params:rtp-hdrext:sdes:mid",""],
+ ];
+ // *Ugh* ...
+ ok(JSON.stringify(audio) ===
+ JSON.stringify(expected_audio),
+ "List of answer audio URNs meets expected values");
+
+ const video = sdputils.findExtmapIdsUrnsDirections(
+ sdputils.getVideoMSections(test.originalAnswer.sdp));
+ const expected_video = [
+ /* Please modify this list when you add or remove RTP header
+ extensions. */
+ ["3", "urn:ietf:params:rtp-hdrext:sdes:mid",""],
+ ["4", "http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time",""],
+ ["5", "urn:ietf:params:rtp-hdrext:toffset",""],
+ ["7", "http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01", ""],
+ ];
+ ok(JSON.stringify(video) ===
+ JSON.stringify(expected_video),
+ "List of answer video URNs meets expected values");
+ }
+ ]);
+
+ await test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyTooLongMidFails.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyTooLongMidFails.html
new file mode 100644
index 0000000000..70d27b48c6
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudioVideoVerifyTooLongMidFails.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1427009",
+ title: "Test mid longer than 16 characters fails"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ options = options || { };
+ options.bundle = false;
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+
+ test.chain.replaceAfter("PC_LOCAL_CREATE_OFFER",
+ [
+ function PC_LOCAL_MUNGE_OFFER_SDP(test) {
+ test.originalOffer.sdp =
+ test.originalOffer.sdp.replace(/a=mid:.*\r\n/g,
+ "a=mid:really_long_mid_over_16_chars\r\n");
+ },
+ function PC_LOCAL_EXPECT_SET_LOCAL_DESCRIPTION_FAIL(test) {
+ return test.setLocalDescription(test.pcLocal,
+ test.originalOffer,
+ HAVE_LOCAL_OFFER)
+ .then(() => ok(false, "setLocalDescription must fail"),
+ // This needs to be RTCError once we support it, and once we
+ // stop allowing any modification, InvalidModificationError
+ e => is(e.name, "OperationError",
+ "setLocalDescription must fail and did"));
+ }
+ ], 0 // first occurance
+ );
+
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio_forced_higher_rate.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio_forced_higher_rate.html
new file mode 100644
index 0000000000..95bfb06514
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio_forced_higher_rate.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="peerconnection_audio_forced_sample_rate.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1437366",
+ title: "Basic audio-only peer connection, with the MTG running at a rate not supported by the MediaPipeline (49000Hz)"
+});
+
+test_peerconnection_audio_forced_sample_rate(49000);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio_forced_lower_rate.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio_forced_lower_rate.html
new file mode 100644
index 0000000000..aab9778971
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicAudio_forced_lower_rate.html
@@ -0,0 +1,19 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="peerconnection_audio_forced_sample_rate.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1437366",
+ title: "Basic audio-only peer connection, with the MTG running at a rate not supported by the MediaPipeline (24000Hz)"
+});
+
+test_peerconnection_audio_forced_sample_rate(24000);
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicH264Video.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicH264Video.html
new file mode 100644
index 0000000000..072c35da39
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicH264Video.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1040346",
+ title: "Basic H.264 GMP video-only peer connection"
+ });
+
+ var test;
+ runNetworkTest(async function (options) {
+ matchPlatformH264CodecPrefs();
+ options = options || { };
+ options.h264 = true;
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicScreenshare.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicScreenshare.html
new file mode 100644
index 0000000000..93148ac5fd
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicScreenshare.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1039666",
+ title: "Basic screenshare-only peer connection"
+ });
+
+ async function supportedVideoPayloadTypes() {
+ const pc = new RTCPeerConnection();
+ const offer = await pc.createOffer({offerToReceiveVideo: true});
+ return sdputils.getPayloadTypes(offer.sdp);
+ }
+
+ async function testScreenshare(payloadType) {
+ const options = {};
+ options.h264 = payloadType == 97 || payloadType == 126;
+ const test = new PeerConnectionTest(options);
+ const constraints = {
+ video: { mediaSource: "screen" },
+ };
+ test.setMediaConstraints([constraints], []);
+ test.chain.insertAfterEach("PC_LOCAL_CREATE_OFFER", [
+ function PC_LOCAL_ISOLATE_CODEC() {
+ info(`Forcing payload type ${payloadType}. Note that other associated ` +
+ `payload types, like RTX, are removed too.`);
+ test.originalOffer.sdp =
+ sdputils.removeAllButPayloadType(test.originalOffer.sdp, payloadType);
+ },
+ ]);
+ await test.run();
+ }
+
+ runNetworkTest(async () => {
+ await matchPlatformH264CodecPrefs();
+ const pts = await supportedVideoPayloadTypes();
+ ok(pts.includes("120"), "VP8 is supported");
+ ok(pts.includes("121"), "VP9 is supported");
+ if (pts.length > 2) {
+ is(pts.length, 4, "Expected VP8, VP9 and two variants of H264");
+ ok(pts.includes("97"), "H264 with no packetization-mode is supported");
+ ok(pts.includes("126"), "H264 with packetization-mode=1 is supported");
+ }
+ for (const pt of pts) {
+ await testScreenshare(pt);
+ }
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicVideo.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicVideo.html
new file mode 100644
index 0000000000..4a0655d696
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicVideo.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "796888",
+ title: "Basic video-only peer connection"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicVideoVerifyRtpHeaderExtensions.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicVideoVerifyRtpHeaderExtensions.html
new file mode 100644
index 0000000000..7874e52a10
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicVideoVerifyRtpHeaderExtensions.html
@@ -0,0 +1,82 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="parser_rtp.js"></script>
+ <script type="application/javascript" src="sdpUtils.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1416932",
+ title: "Basic video-only peer connection and verify rtp header extensions"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+
+ let getRtpPacketWithExtension = (pc, extension) => {
+ // we only examine received packets
+ let sending = false;
+ pc.mozEnablePacketDump(0, "rtp", sending);
+ return new Promise((res, rej) =>
+ pc.mozSetPacketCallback((...args) => {
+ const packet = ParseRtpPacket(args[3]);
+ info(`midId = ${extension} packet = ${JSON.stringify(packet, null, 2)}`);
+ if (packet.header.extensions.find(e => e.id == extension) !== undefined) {
+ res(packet);
+ pc.mozSetPacketCallback(() => {});
+ pc.mozDisablePacketDump(0, "rtp", sending);
+ }
+ })
+ );
+ }
+
+ let havePacketWithMid;
+ let sdpExtmaps;
+
+ // MID can stop being sent when acked causing failures if packets are checked later.
+ // Starting packet sniffer before PC_LOCAL_SET_REMOTE_DESCRIPTION to be ready
+ // to inspect packets ahead of any packets arriving.
+ test.chain.insertBefore('PC_LOCAL_SET_REMOTE_DESCRIPTION', [
+ function PC_REMOTE_FIND_RTP_PACKETS_WITH_MIDID() {
+
+ sdpExtmaps = sdputils.findExtmapIdsUrnsDirections(test.originalAnswer.sdp);
+ const [midId] = sdpExtmaps.find(([, urn]) => urn == "urn:ietf:params:rtp-hdrext:sdes:mid");
+ const pc = SpecialPowers.wrap(test.pcRemote._pc);
+ havePacketWithMid = getRtpPacketWithExtension(pc, midId);
+ }
+ ]);
+
+ test.chain.insertBefore('PC_REMOTE_WAIT_FOR_MEDIA_FLOW', [
+ async function PC_REMOTE_CHECK_RTP_HEADER_EXTS_AGAINST_SDP() {
+
+ const sdpExtmapIds = sdpExtmaps.map(e => e[0]);
+ const packet = await havePacketWithMid;
+ const extIds = packet.header.extensions.map(e => `${e.id}`);
+ // make sure we got the same number of rtp header extensions in
+ // the received packet as were negotiated in the sdp. Then
+ // check to make sure each of the received extension ids were in
+ // the sdp.
+ is(sdpExtmapIds.length, extIds.length,
+ `number of sdp ids match received ids ` +
+ `${JSON.stringify(sdpExtmapIds)} == ${JSON.stringify(extIds)}\n` +
+ `sdp = ${test.originalAnswer.sdp}\n` +
+ `packet = ${JSON.stringify(packet, null, 2)}`);
+ // note, we are comparing a number (from the parsed rtp packet)
+ // and a string (from the answer sdp)
+ ok(extIds.every(id => sdpExtmapIds.includes(id)) &&
+ sdpExtmapIds.every(id => extIds.includes(id)),
+ `extension id arrays equivalent`);
+ }
+ ]);
+
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_basicWindowshare.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicWindowshare.html
new file mode 100644
index 0000000000..1cfb0797db
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_basicWindowshare.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1038926",
+ title: "Basic windowshare-only peer connection"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ const constraints = {
+ video: { mediaSource: "window" },
+ };
+ test.setMediaConstraints([constraints], []);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1013809.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1013809.html
new file mode 100644
index 0000000000..a8c7004793
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1013809.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1013809",
+ title: "Audio-only peer connection with swapped setLocal and setRemote steps"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ var sld = test.chain.remove("PC_REMOTE_SET_LOCAL_DESCRIPTION");
+ test.chain.insertAfter("PC_LOCAL_SET_REMOTE_DESCRIPTION", sld);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1042791.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1042791.html
new file mode 100644
index 0000000000..a84dcf9d09
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1042791.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1040346",
+ title: "Basic H.264 GMP video-only peer connection"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ options = options || { };
+ options.h264 = true;
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+ test.chain.removeAfter("PC_LOCAL_CREATE_OFFER");
+
+ test.chain.append([
+ function PC_LOCAL_VERIFY_H264_OFFER(test) {
+ ok(!test.pcLocal._latest_offer.sdp.toLowerCase().includes("profile-level-id=0x42e0"),
+ "H264 offer does not contain profile-level-id=0x42e0");
+ ok(test.pcLocal._latest_offer.sdp.toLowerCase().includes("profile-level-id=42e0"),
+ "H264 offer contains profile-level-id=42e0");
+ }
+ ]);
+
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1227781.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1227781.html
new file mode 100644
index 0000000000..41e4aec457
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1227781.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1227781",
+ title: "Test with invalid TURN server"
+ });
+
+ const turnConfig = {
+ iceServers: [
+ {
+ username: "mozilla",
+ credential: "mozilla",
+ url: "turn:test@10.0.0.1",
+ },
+ ],
+ };
+ runNetworkTest(function (options) {
+ let exception = false;
+ try {
+ new RTCPeerConnection(turnConfig);
+ } catch (e) {
+ info(e);
+ exception = true;
+ }
+ is(exception, true, "Exception fired");
+ ok("Success");
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1512281.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1512281.html
new file mode 100644
index 0000000000..e6451becea
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1512281.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1512281",
+ title: "Test that RTCP sender and receiver stats are not swapped"
+ });
+
+const ensure_missing_rtcp = async stats => {
+ const rtcp_stats = [...stats.values()].filter(
+ s => s.type.endsWith("bound-rtp") &&
+ s.isRemote == true).map(s => JSON.stringify(s))
+ is(rtcp_stats, [],
+ "There are no RTCP stats when RTCP reception is turned off");
+};
+
+const PC_LOCAL_TEST_FOR_MISSING_RTCP = async test =>
+ await ensure_missing_rtcp(await test.pcLocal.getStats());
+
+const PC_REMOTE_TEST_FOR_MISSING_RTCP = async test =>
+ await ensure_missing_rtcp(await test.pcRemote.getStats());
+
+runNetworkTest(async options => {
+ await pushPrefs(["media.webrtc.net.force_disable_rtcp_reception", true]);
+
+ const test = new PeerConnectionTest(options);
+
+ test.chain.insertAfter("PC_LOCAL_WAIT_FOR_MEDIA_FLOW",
+ [PC_LOCAL_TEST_FOR_MISSING_RTCP]);
+
+ test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW",
+ [PC_REMOTE_TEST_FOR_MISSING_RTCP]);
+
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+ await test.run();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1773067.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1773067.html
new file mode 100644
index 0000000000..9e6d79a107
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug1773067.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1773067",
+ title: "getStats on a closed peer connection should fail, not hang, " +
+ " until bug 1056433 is fixed"
+ });
+
+ // TODO: Bug 1056433 removes the need for this test
+ runNetworkTest(async function () {
+ let errorName;
+ try {
+ const pc = new RTCPeerConnection();
+ pc.close();
+ await pc.getStats();
+ } catch(e) {
+ errorName = e.name;
+ }
+ is(errorName,
+ "InvalidStateError",
+ "getStats on closed peer connection fails instead of hanging");
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_bug822674.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug822674.html
new file mode 100644
index 0000000000..fceb2c2a1d
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug822674.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "822674",
+ title: "RTCPeerConnection isn't a true javascript object as it should be"
+ });
+
+ runNetworkTest(function () {
+ var pc = new RTCPeerConnection();
+
+ pc.thereIsNeverGoingToBeAPropertyWithThisNameOnThisInterface = 1;
+ is(pc.thereIsNeverGoingToBeAPropertyWithThisNameOnThisInterface, 1,
+ "Can set expandos on an RTCPeerConnection");
+
+ pc = null;
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_bug825703.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug825703.html
new file mode 100644
index 0000000000..5cd168af8a
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug825703.html
@@ -0,0 +1,140 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "825703",
+ title: "RTCConfiguration valid/invalid permutations"
+ });
+
+// ^^^ Don't insert data above this line without adjusting line number below!
+var lineNumberAndFunction = {
+// <--- 16 is the line this must be.
+ line: 17, func: () => new RTCPeerConnection().onaddstream = () => {}
+};
+
+var makePC = (config, expected_error) => {
+ var exception;
+ try {
+ new RTCPeerConnection(config).close();
+ } catch (e) {
+ exception = e;
+ }
+ is((exception? exception.name : "success"), expected_error || "success",
+ "RTCPeerConnection(" + JSON.stringify(config) + ")");
+};
+
+// The order of properties in objects is not guaranteed in JavaScript, so this
+// transform produces json-comparable dictionaries. The resulting copy is only
+// meant to be used in comparisons (e.g. array-ness is not preserved).
+
+var toComparable = o =>
+ (typeof o != 'object' || !o)? o : Object.keys(o).sort().reduce((co, key) => {
+ co[key] = toComparable(o[key]);
+ return co;
+}, {});
+
+// This is a test of the iceServers parsing code + readable errors
+runNetworkTest(() => {
+ var exception = null;
+
+ try {
+ new RTCPeerConnection().close();
+ } catch (e) {
+ exception = e;
+ }
+ ok(!exception, "RTCPeerConnection() succeeds");
+ exception = null;
+
+ // Some overlap still with WPT RTCConfiguration-iceServers.html
+
+ makePC({ iceServers: [
+ { urls:"stun:127.0.0.1" },
+ { urls:"stun:localhost", foo:"" },
+ { urls: ["stun:127.0.0.1", "stun:localhost"] },
+ { urls:"stuns:localhost", foo:"" },
+ { urls:"turn:[::1]:3478", username:"p", credential:"p" },
+ { urls:"turn:[::1]:3478", username:"", credential:"" },
+ { urls:"turns:[::1]:3478", username:"", credential:"" },
+ { urls:"turn:localhost:3478?transport=udp", username:"p", credential:"p" },
+ { urls: ["turn:[::1]:3478", "turn:localhost"], username:"p", credential:"p" },
+ { urls:"turns:localhost:3478?transport=udp", username:"p", credential:"p" },
+ { url:"stun:localhost", foo:"" },
+ { url:"turn:localhost", username:"p", credential:"p" }
+ ]});
+
+ makePC({ iceServers: [{ urls:"http:0.0.0.0" }] }, "SyntaxError");
+
+ try {
+ new RTCPeerConnection({ iceServers: [{ url:"http:0.0.0.0" }] }).close();
+ } catch (e) {
+ ok(e.message.indexOf("http") > 0,
+ "RTCPeerConnection() constructor has readable exceptions");
+ }
+
+ // Test getConfiguration
+ const config = {
+ bundlePolicy: "max-bundle",
+ iceTransportPolicy: "relay",
+ peerIdentity: null,
+ certificates: [],
+ iceServers: [
+ { urls: ["stun:127.0.0.1", "stun:localhost"], credentialType:"password" },
+ { urls: ["turn:[::1]:3478"], username:"p", credential:"p", credentialType:"password" },
+ ],
+ };
+ // Make sure sdpSemantics is not exposed in getConfiguration
+ const configWithExtraProps = Object.assign({},
+ config,
+ {sdpSemantics: "plan-b"});
+ ok("sdpSemantics" in configWithExtraProps, "sdpSemantics control");
+
+ const pc = new RTCPeerConnection(configWithExtraProps);
+ is(JSON.stringify(toComparable(pc.getConfiguration())),
+ JSON.stringify(toComparable(config)), "getConfiguration");
+ pc.close();
+
+ var push = prefs => SpecialPowers.pushPrefEnv(prefs);
+
+ return Promise.resolve()
+ // This set of tests are setting the about:config User preferences for default
+ // ice servers and checking the outputs when RTCPeerConnection() is
+ // invoked. See Bug 1167922 for more information.
+ .then(() => push({ set: [['media.peerconnection.default_iceservers', ""]] })
+ .then(() => makePC())
+ .then(() => push({ set: [['media.peerconnection.default_iceservers', "k"]] }))
+ .then(() => makePC())
+ .then(() => push({ set: [['media.peerconnection.default_iceservers', "[{\"urls\": [\"stun:stun.services.mozilla.com\"]}]"]] }))
+ .then(() => makePC()))
+ // This set of tests check that warnings work. See Bug 1254839 for more.
+ .then(() => {
+ let promise = new Promise(resolve => {
+ SpecialPowers.registerConsoleListener(msg => {
+ if (msg.message.includes("onaddstream")) {
+ SpecialPowers.postConsoleSentinel();
+ resolve(msg.message);
+ }
+ });
+ });
+ lineNumberAndFunction.func();
+ return promise;
+ }).then(warning => {
+ is(warning.split('"')[1],
+ "WebRTC: onaddstream is deprecated! Use peerConnection.ontrack instead.",
+ "warning logged");
+ var remainder = warning.split('"').slice(2).join('"');
+ info(remainder);
+ ok(remainder.includes('file: "' + window.location + '"'),
+ "warning has this file");
+ ok(remainder.includes('line: ' + lineNumberAndFunction.line),
+ "warning has correct line number");
+ });
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_bug827843.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug827843.html
new file mode 100644
index 0000000000..06cfde9e5d
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug827843.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "827843",
+ title: "Ensure that localDescription and remoteDescription are null after close"
+ });
+
+var steps = [
+ function CHECK_SDP_ON_CLOSED_PC(test) {
+ var description;
+ var exception = null;
+
+ test.pcLocal.close();
+
+ try { description = test.pcLocal.localDescription; } catch (e) { exception = e; }
+ ok(exception, "Attempt to access localDescription of pcLocal after close throws exception");
+ exception = null;
+
+ try { description = test.pcLocal.remoteDescription; } catch (e) { exception = e; }
+ ok(exception, "Attempt to access remoteDescription of pcLocal after close throws exception");
+ exception = null;
+
+ test.pcRemote.close();
+
+ try { description = test.pcRemote.localDescription; } catch (e) { exception = e; }
+ ok(exception, "Attempt to access localDescription of pcRemote after close throws exception");
+ exception = null;
+
+ try { description = test.pcRemote.remoteDescription; } catch (e) { exception = e; }
+ ok(exception, "Attempt to access remoteDescription of pcRemote after close throws exception");
+ }
+];
+
+var test;
+runNetworkTest(() => {
+ test = new PeerConnectionTest();
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.chain.append(steps);
+ return test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_bug834153.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug834153.html
new file mode 100644
index 0000000000..6d8ca2a7ce
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_bug834153.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "834153",
+ title: "Queue CreateAnswer in PeerConnection.js"
+ });
+
+ runNetworkTest(function () {
+ var pc1 = new RTCPeerConnection();
+ var pc2 = new RTCPeerConnection();
+
+ return pc1.createOffer({ offerToReceiveAudio: true }).then(offer => {
+ // The whole point of this test is not to wait for the
+ // setRemoteDescription call to succesfully complete, so we
+ // don't wait for it to succeed.
+ pc2.setRemoteDescription(offer);
+ return pc2.createAnswer();
+ })
+ .then(answer => is(answer.type, "answer", "CreateAnswer created an answer"))
+ .catch(reason => ok(false, reason.message))
+ .then(() => {
+ pc1.close();
+ pc2.close();
+ })
+ .catch(reason => ok(false, reason.message));
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_callbacks.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_callbacks.html
new file mode 100644
index 0000000000..4c890e4400
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_callbacks.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: "PeerConnection using callback functions",
+ bug: "1119593",
+ visible: true
+ });
+
+// This still aggressively uses promises, but it is testing that the callback functions
+// are properly in place.
+
+// wrapper that turns a callback-based function call into a promise
+function pcall(o, f, beforeArg) {
+ return new Promise((resolve, reject) => {
+ var args = [resolve, reject];
+ if (typeof beforeArg !== 'undefined') {
+ args.unshift(beforeArg);
+ }
+ info('Calling ' + f.name);
+ f.apply(o, args);
+ });
+}
+
+var pc1 = new RTCPeerConnection();
+var pc2 = new RTCPeerConnection();
+
+var pc2_haveRemoteOffer = new Promise(resolve => {
+ pc2.onsignalingstatechange =
+ e => (e.target.signalingState == "have-remote-offer") && resolve();
+});
+var pc1_stable = new Promise(resolve => {
+ pc1.onsignalingstatechange =
+ e => (e.target.signalingState == "stable") && resolve();
+});
+
+pc1.onicecandidate = e => {
+ pc2_haveRemoteOffer
+ .then(() => !e.candidate || pcall(pc2, pc2.addIceCandidate, e.candidate))
+ .catch(generateErrorCallback());
+};
+pc2.onicecandidate = e => {
+ pc1_stable
+ .then(() => !e.candidate || pcall(pc1, pc1.addIceCandidate, e.candidate))
+ .catch(generateErrorCallback());
+};
+
+var v1, v2;
+var delivered = new Promise(resolve => {
+ pc2.onaddstream = e => {
+ v2.srcObject = e.stream;
+ resolve(e.stream);
+ };
+});
+
+runNetworkTest(function() {
+ v1 = createMediaElement('video', 'v1');
+ v2 = createMediaElement('video', 'v2');
+ var canPlayThrough = new Promise(resolve => v2.canplaythrough = resolve);
+ is(v2.currentTime, 0, "v2.currentTime is zero at outset");
+
+ // not testing legacy gUM here
+ return navigator.mediaDevices.getUserMedia({ video: true, audio: true })
+ .then(stream => pc1.addStream(v1.srcObject = stream))
+ .then(() => pcall(pc1, pc1.createOffer))
+ .then(offer => pcall(pc1, pc1.setLocalDescription, offer))
+ .then(() => pcall(pc2, pc2.setRemoteDescription, pc1.localDescription))
+ .then(() => pcall(pc2, pc2.createAnswer))
+ .then(answer => pcall(pc2, pc2.setLocalDescription, answer))
+ .then(() => pcall(pc1, pc1.setRemoteDescription, pc2.localDescription))
+ .then(() => delivered)
+ // .then(() => canPlayThrough) // why doesn't this fire?
+ .then(() => waitUntil(() => v2.currentTime > 0))
+ .then(() => ok(v2.currentTime > 0, "v2.currentTime is moving (" + v2.currentTime + ")"))
+ .then(() => ok(true, "Connected."))
+ .then(() => { v1.pause(); v2.pause(); });
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_2d.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_2d.html
new file mode 100644
index 0000000000..db3a735008
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_2d.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1032848",
+ title: "Canvas(2D)::CaptureStream as video-only input to peerconnection",
+ visible: true
+});
+
+runNetworkTest(async () => {
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ var test = new PeerConnectionTest();
+ var mediaElement;
+ var h = new CaptureStreamTestHelper2D();
+ var canvas = document.createElement('canvas');
+ var stream;
+ canvas.id = 'source_canvas';
+ canvas.width = canvas.height = 16;
+ document.getElementById('content').appendChild(canvas);
+
+ test.setMediaConstraints([{video: true}], []);
+ test.chain.replace("PC_LOCAL_GUM", [
+ function PC_LOCAL_CANVAS_CAPTURESTREAM(test) {
+ h.drawColor(canvas, h.green);
+ stream = canvas.captureStream(0);
+ test.pcLocal.attachLocalStream(stream);
+ stream.requestFrame();
+ var i = 0;
+ return setInterval(function() {
+ try {
+ info("draw " + i ? "green" : "red");
+ h.drawColor(canvas, i ? h.green : h.red);
+ i = 1 - i;
+ stream.requestFrame();
+ } catch (e) {
+ // ignore; stream might have shut down, and we don't bother clearing
+ // the setInterval.
+ }
+ }, 500);
+ }
+ ]);
+ test.chain.append([
+ function PC_REMOTE_WAIT_FOR_REMOTE_GREEN() {
+ mediaElement = test.pcRemote.remoteMediaElements[0];
+ ok(!!mediaElement, "Should have remote video element for pcRemote");
+ return h.pixelMustBecome(mediaElement, h.green, {
+ threshold: 128,
+ infoString: "pcRemote's remote should become green",
+ });
+ },
+ function PC_LOCAL_DRAW_LOCAL_RED() {
+ // After requesting a frame it will be captured at the time of next render.
+ // Next render will happen at next stable state, at the earliest,
+ // i.e., this order of `requestFrame(); draw();` should work.
+ stream.requestFrame();
+ h.drawColor(canvas, h.red);
+ },
+ function PC_REMOTE_WAIT_FOR_REMOTE_RED() {
+ return h.pixelMustBecome(mediaElement, h.red, {
+ threshold: 128,
+ infoString: "pcRemote's remote should become red",
+ });
+ }
+ ]);
+ await test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_2d_noSSRC.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_2d_noSSRC.html
new file mode 100644
index 0000000000..e33a7e8886
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_2d_noSSRC.html
@@ -0,0 +1,83 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ title: "Canvas(2D)::CaptureStream as video-only input to peerconnection with no a=ssrc",
+ visible: true
+});
+
+var test;
+runNetworkTest(async (options) => {
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ options = options || { };
+ options.ssrc = false;
+ test = new PeerConnectionTest(options);
+ var mediaElement;
+ var h = new CaptureStreamTestHelper2D();
+ var canvas = document.createElement('canvas');
+ var stream;
+ canvas.id = 'source_canvas';
+ canvas.width = canvas.height = 16;
+ document.getElementById('content').appendChild(canvas);
+
+ test.setMediaConstraints([{video: true}], []);
+ test.chain.replace("PC_LOCAL_GUM", [
+ function PC_LOCAL_CANVAS_CAPTURESTREAM(test) {
+ h.drawColor(canvas, h.green);
+ stream = canvas.captureStream(0);
+ test.pcLocal.attachLocalStream(stream);
+ stream.requestFrame();
+ var i = 0;
+ return setInterval(function() {
+ try {
+ info("draw " + i ? "green" : "red");
+ h.drawColor(canvas, i ? h.green : h.red);
+ i = 1 - i;
+ stream.requestFrame();
+ } catch (e) {
+ // ignore; stream might have shut down, and we don't bother clearing
+ // the setInterval.
+ }
+ }, 500);
+ }
+ ]);
+ test.chain.append([
+ function PC_REMOTE_WAIT_FOR_REMOTE_GREEN() {
+ mediaElement = test.pcRemote.remoteMediaElements[0];
+ ok(!!mediaElement, "Should have remote video element for pcRemote");
+ return h.pixelMustBecome(mediaElement, h.green, {
+ threshold: 128,
+ infoString: "pcRemote's remote should become green",
+ });
+ },
+ function PC_LOCAL_DRAW_LOCAL_RED() {
+ // After requesting a frame it will be captured at the time of next render.
+ // Next render will happen at next stable state, at the earliest,
+ // i.e., this order of `requestFrame(); draw();` should work.
+ stream.requestFrame();
+ h.drawColor(canvas, h.red);
+ },
+ function PC_REMOTE_WAIT_FOR_REMOTE_RED() {
+ return h.pixelMustBecome(mediaElement, h.red, {
+ threshold: 128,
+ infoString: "pcRemote's remote should become red",
+ });
+ }
+ ]);
+ await test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_webgl.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_webgl.html
new file mode 100644
index 0000000000..167379fb37
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_captureStream_canvas_webgl.html
@@ -0,0 +1,130 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/webgl-mochitest/webgl-util.js"></script>
+</head>
+<body>
+<pre id="test">
+<script id="v-shader" type="x-shader/x-vertex">
+ attribute vec2 aPosition;
+ void main() {
+ gl_Position = vec4(aPosition, 0, 1);
+}
+</script>
+<script id="f-shader" type="x-shader/x-fragment">
+ precision mediump float;
+ uniform vec4 uColor;
+ void main() { gl_FragColor = uColor; }
+</script>
+<script type="application/javascript">
+createHTML({
+ bug: "1032848",
+ title: "Canvas(WebGL)::CaptureStream as video-only input to peerconnection"
+});
+
+runNetworkTest(async () => {
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ var test = new PeerConnectionTest();
+ var vremote;
+ var h = new CaptureStreamTestHelperWebGL();
+ var canvas = document.createElement('canvas');
+ canvas.id = 'source_canvas';
+ canvas.width = canvas.height = 16;
+ canvas.style.display = 'none';
+ document.getElementById('content').appendChild(canvas);
+
+ var gl = canvas.getContext('webgl');
+ if (!gl) {
+ todo(false, "WebGL unavailable.");
+ networkTestFinished();
+ return;
+ }
+
+ test.setMediaConstraints([{video: true}], []);
+ test.chain.replace("PC_LOCAL_GUM", [
+ function WEBGL_SETUP(test) {
+ var program = WebGLUtil.createProgramByIds(gl, 'v-shader', 'f-shader');
+
+ if (!program) {
+ ok(false, "Program should link");
+ return Promise.reject("Program should link");
+ }
+ gl.useProgram(program);
+
+ var uColorLocation = gl.getUniformLocation(program, "uColor");
+ h.setFragmentColorLocation(uColorLocation);
+
+ var squareBuffer = gl.createBuffer();
+ gl.bindBuffer(gl.ARRAY_BUFFER, squareBuffer);
+
+ var vertices = [ 0, 0,
+ -1, 0,
+ 0, 1,
+ -1, 1 ];
+ gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(vertices), gl.STATIC_DRAW);
+ squareBuffer.itemSize = 2;
+ squareBuffer.numItems = 4;
+
+ program.aPosition = gl.getAttribLocation(program, "aPosition");
+ gl.enableVertexAttribArray(program.aPosition);
+ gl.vertexAttribPointer(program.aPosition, squareBuffer.itemSize, gl.FLOAT, false, 0, 0);
+ },
+ function PC_LOCAL_CANVAS_CAPTURESTREAM(test) {
+ h.drawColor(canvas, h.green);
+ test.pcLocal.canvasStream = canvas.captureStream(0.0);
+ is(test.pcLocal.canvasStream.canvas, canvas, "Canvas attribute is correct");
+ test.pcLocal.attachLocalStream(test.pcLocal.canvasStream);
+ var i = 0;
+ return setInterval(function() {
+ try {
+ info("draw " + i ? "green" : "red");
+ h.drawColor(canvas, i ? h.green : h.red);
+ i = 1 - i;
+ test.pcLocal.canvasStream.requestFrame();
+ } catch (e) {
+ // ignore; stream might have shut down, and we don't bother clearing
+ // the setInterval.
+ }
+ }, 500);
+ }
+ ]);
+ test.chain.append([
+ function FIND_REMOTE_VIDEO() {
+ vremote = test.pcRemote.remoteMediaElements[0];
+ ok(!!vremote, "Should have remote video element for pcRemote");
+ },
+ function WAIT_FOR_REMOTE_GREEN() {
+ return h.pixelMustBecome(vremote, h.green, {
+ threshold: 128,
+ infoString: "pcRemote's remote should become green",
+ });
+ },
+ function REQUEST_FRAME(test) {
+ // After requesting a frame it will be captured at the time of next render.
+ // Next render will happen at next stable state, at the earliest,
+ // i.e., this order of `requestFrame(); draw();` should work.
+ test.pcLocal.canvasStream.requestFrame();
+ },
+ function DRAW_LOCAL_RED() {
+ h.drawColor(canvas, h.red);
+ },
+ function WAIT_FOR_REMOTE_RED() {
+ return h.pixelMustBecome(vremote, h.red, {
+ threshold: 128,
+ infoString: "pcRemote's remote should become red",
+ });
+ }
+ ]);
+ await test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_capturedVideo.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_capturedVideo.html
new file mode 100644
index 0000000000..f6f48ba429
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_capturedVideo.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script src="pc.js"></script>
+ <script src="../../../test/manifest.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+(async () => {
+ await createHTML({
+ bug: "1081409",
+ title: "Captured video-only over peer connection",
+ visible: true
+ });
+
+ // Run tests in sequence for log readability.
+ PARALLEL_TESTS = 1;
+ const manager = new MediaTestManager;
+
+ async function startTest(media, token) {
+ manager.started(token);
+ info(`Starting test for ${media.name}`);
+ const video = document.createElement('video');
+ video.id = "id_" + media.name;
+ video.width = 160;
+ video.height = 120;
+ video.muted = true;
+ video.controls = true;
+ video.preload = "metadata";
+ video.src = "../../../test/" + media.name;
+
+ document.getElementById("content").appendChild(video);
+
+ const onerror = new Promise(r => video.onerror = r).then(_ =>
+ new Error(`${media.name} failed in playback. code=${video.error.code}`));
+
+ await Promise.race([
+ new Promise(res => video.onloadedmetadata = res),
+ onerror,
+ ]);
+ onerror.catch(e => ok(false, e));
+ setupEnvironment();
+ await testConfigured;
+ const stream = video.mozCaptureStream();
+ const test = new PeerConnectionTest(
+ {
+ config_local: { label_suffix: media.name },
+ config_remote: { label_suffix: media.name },
+ }
+ );
+ test.setOfferOptions(
+ {
+ offerToReceiveVideo: false,
+ offerToReceiveAudio: false,
+ }
+ );
+ const hasVideo = !!stream.getVideoTracks().length;
+ const hasAudio = !!stream.getAudioTracks().length;
+ test.setMediaConstraints([{ video: hasVideo, audio: hasAudio }], []);
+ test.chain.replace("PC_LOCAL_GUM", [
+ function PC_LOCAL_CAPTUREVIDEO(test) {
+ test.pcLocal.attachLocalStream(stream);
+ },
+ ]);
+ test.chain.insertBefore("PC_LOCAL_WAIT_FOR_MEDIA_FLOW", [
+ function PC_LOCAL_START_MEDIA(test) {
+ video.play();
+ },
+ ]);
+ await test.run();
+ removeNodeAndSource(video);
+ manager.finished(token);
+ }
+
+ manager.runTests(getPlayableVideos(gLongerTests), startTest);
+})();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_certificates.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_certificates.html
new file mode 100644
index 0000000000..561f285f60
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_certificates.html
@@ -0,0 +1,185 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1172785",
+ title: "Certificate management"
+ });
+
+ function badCertificate(config, expectedError, message) {
+ return RTCPeerConnection.generateCertificate(config)
+ .then(() => ok(false, message),
+ e => is(e.name, expectedError, message));
+ }
+
+ // Checks a handful of obviously bad options to RTCCertificate.create(). Most
+ // of the checking is done by the WebCrypto code underpinning this, hence the
+ // baffling error codes, but a sanity check is still in order.
+ function checkBadParameters() {
+ return Promise.all([
+ badCertificate({
+ name: "RSASSA-PKCS1-v1_5",
+ hash: "SHA-256",
+ modulusLength: 1023,
+ publicExponent: new Uint8Array([1, 0, 1])
+ }, "NotSupportedError", "1023-bit is too small to succeed"),
+
+ badCertificate({
+ name: "RSASSA-PKCS1-v1_5",
+ hash: "SHA-384",
+ modulusLength: 2048,
+ publicExponent: new Uint8Array([1, 0, 1])
+ }, "NotSupportedError", "SHA-384 isn't supported yet"),
+
+ // A SyntaxError happens in the "generate key operation" step, but
+ // webrtc-pc does not say to reject the promise if this step fails.
+ // It does say to throw NotSupportedError if we have passed "an
+ // algorithm that the user agent cannot or will not use to generate a
+ // certificate".
+ badCertificate({
+ name: "ECDH",
+ namedCurve: "P-256"
+ }, "NotSupportedError", "ECDH is rejected because the usage is neither \"deriveKey\" or \"deriveBits\""),
+
+ badCertificate({
+ name: "not a valid algorithm"
+ }, "NotSupportedError", "not a valid algorithm"),
+
+ badCertificate("ECDSA", "NotSupportedError", "a bare name is not enough"),
+
+ badCertificate({
+ name: "ECDSA",
+ namedCurve: "not a curve"
+ }, "NotSupportedError", "ECDSA with an unknown curve")
+ ]);
+ }
+
+ function createDB() {
+ var openDB = indexedDB.open("genericstore");
+ openDB.onupgradeneeded = e => {
+ var db = e.target.result;
+ db.createObjectStore("data");
+ };
+ return new Promise(resolve => {
+ openDB.onsuccess = e => resolve(e.target.result);
+ });
+ }
+
+ function resultPromise(tx, op) {
+ return new Promise((resolve, reject) => {
+ op.onsuccess = e => resolve(e.target.result);
+ op.onerror = () => reject(op.error);
+ tx.onabort = () => reject(tx.error);
+ });
+ }
+
+ function store(db, value) {
+ var tx = db.transaction("data", "readwrite");
+ var store = tx.objectStore("data");
+ return resultPromise(tx, store.put(value, "value"));
+ }
+
+ function retrieve(db) {
+ var tx = db.transaction("data", "readonly");
+ var store = tx.objectStore("data");
+ return resultPromise(tx, store.get("value"));
+ }
+
+ // Creates a database, stores a value, retrieves it.
+ function storeAndRetrieve(value) {
+ return createDB().then(db => {
+ return store(db, value)
+ .then(() => retrieve(db))
+ .then(retrieved => {
+ db.close();
+ return retrieved;
+ });
+ });
+ }
+
+ var test;
+ runNetworkTest(function (options) {
+ var expiredCert;
+ return Promise.resolve()
+ .then(() => RTCPeerConnection.generateCertificate({
+ name: "ECDSA",
+ namedCurve: "P-256",
+ expires: 1 // smallest possible expiration window
+ }))
+ .then(cert => {
+ ok(!isNaN(cert.expires), 'cert has expiration time');
+ info('Expires at ' + new Date(cert.expires));
+ expiredCert = cert;
+ })
+
+ .then(() => checkBadParameters())
+
+ .then(() => {
+ var delay = expiredCert.expires - Date.now();
+ // Hopefully this delay is never needed.
+ if (delay > 0) {
+ return new Promise(r => setTimeout(r, delay));
+ }
+ })
+ .then(() => {
+ ok(expiredCert.expires <= Date.now(), 'Cert should be at or past expiration');
+ try {
+ new RTCPeerConnection({ certificates: [expiredCert] });
+ ok(false, 'Constructing peer connection with an expired cert is not allowed');
+ } catch(e) {
+ is(e.name, 'InvalidAccessError',
+ 'Constructing peer connection with an expired certs is not allowed');
+ }
+ })
+
+ .then(() => Promise.all([
+ RTCPeerConnection.generateCertificate({
+ name: "ECDSA",
+ namedCurve: "P-256"
+ }),
+ RTCPeerConnection.generateCertificate({
+ name: "RSASSA-PKCS1-v1_5",
+ hash: "SHA-256",
+ modulusLength: 2048,
+ publicExponent: new Uint8Array([1, 0, 1])
+ })
+ ]))
+
+ // A round trip through indexedDB should not do anything.
+ .then(storeAndRetrieve)
+ .then(certs => {
+ try {
+ new RTCPeerConnection({ certificates: certs });
+ ok(false, 'Constructing peer connection with multiple certs is not allowed');
+ } catch(e) {
+ is(e.name, 'NotSupportedError',
+ 'Constructing peer connection with multiple certs is not allowed');
+ }
+ return certs;
+ })
+ .then(certs => {
+ test = new PeerConnectionTest({
+ config_local: {
+ certificates: [certs[0]]
+ },
+ config_remote: {
+ certificates: [certs[1]]
+ }
+ });
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ })
+ .catch(e => {
+ console.log('test failure', e);
+ ok(false, 'test failed: ' + e);
+ });
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_checkPacketDumpHook.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_checkPacketDumpHook.html
new file mode 100644
index 0000000000..248e102dd2
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_checkPacketDumpHook.html
@@ -0,0 +1,107 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1377299",
+ title: "Check that packet dump hooks generate callbacks"
+ });
+
+ function waitForPacket(pc, checkFunction) {
+ return new Promise(resolve => {
+ function onPacket(level, type, sending, packet) {
+ if (checkFunction(level, type, sending, packet)) {
+ SpecialPowers.wrap(pc).mozSetPacketCallback(() => {});
+ resolve();
+ }
+ }
+
+ SpecialPowers.wrap(pc).mozSetPacketCallback(onPacket);
+ }
+ );
+ }
+
+ async function waitForSendPacket(pc, type, level) {
+ await SpecialPowers.wrap(pc).mozEnablePacketDump(level, type, true);
+ await timeout(
+ waitForPacket(pc, (obsLevel, obsType, sending) => {
+ is(obsLevel, level, "Level for packet is " + level);
+ is(obsType, type, "Type for packet is " + type);
+ ok(sending, "This is a send packet");
+ return true;
+ }),
+ 10000, "Timeout waiting for " + type + " send packet on level " + level);
+ await SpecialPowers.wrap(pc).mozDisablePacketDump(level, type, true);
+ }
+
+ async function waitForRecvPacket(pc, type, level) {
+ await SpecialPowers.wrap(pc).mozEnablePacketDump(level, type, false);
+ await timeout(
+ waitForPacket(pc, (obsLevel, obsType, sending) => {
+ is(obsLevel, level, "Level for packet is " + level);
+ is(obsType, type, "Type for packet is " + type);
+ ok(!sending, "This is a recv packet");
+ return true;
+ }),
+ 10000, "Timeout waiting for " + type + " recv packet on level " + level);
+ await SpecialPowers.wrap(pc).mozDisablePacketDump(level, type, false);
+ }
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true, video: true}],
+ [{audio: true, video: true}]);
+ // pc.js uses video elements by default, we want to test audio elements here
+ test.pcLocal.audioElementsOnly = true;
+
+ test.chain.insertBefore('PC_LOCAL_WAIT_FOR_MEDIA_FLOW',[
+ async function PC_LOCAL_CHECK_PACKET_DUMP_HOOKS() {
+ await waitForRecvPacket(test.pcLocal._pc, "rtp", 0);
+ await waitForRecvPacket(test.pcLocal._pc, "rtcp", 0);
+ await waitForRecvPacket(test.pcLocal._pc, "srtp", 0);
+ await waitForRecvPacket(test.pcLocal._pc, "srtcp", 0);
+ await waitForSendPacket(test.pcLocal._pc, "rtp", 0);
+ await waitForSendPacket(test.pcLocal._pc, "rtcp", 0);
+ await waitForSendPacket(test.pcLocal._pc, "srtp", 0);
+ await waitForSendPacket(test.pcLocal._pc, "srtcp", 0);
+
+ await waitForRecvPacket(test.pcLocal._pc, "rtp", 1);
+ await waitForRecvPacket(test.pcLocal._pc, "rtcp", 1);
+ await waitForRecvPacket(test.pcLocal._pc, "srtp", 1);
+ await waitForRecvPacket(test.pcLocal._pc, "srtcp", 1);
+ await waitForSendPacket(test.pcLocal._pc, "rtp", 1);
+ await waitForSendPacket(test.pcLocal._pc, "rtcp", 1);
+ await waitForSendPacket(test.pcLocal._pc, "srtp", 1);
+ await waitForSendPacket(test.pcLocal._pc, "srtcp", 1);
+ },
+ async function PC_REMOTE_CHECK_PACKET_DUMP_HOOKS() {
+ await waitForRecvPacket(test.pcRemote._pc, "rtp", 0);
+ await waitForRecvPacket(test.pcRemote._pc, "rtcp", 0);
+ await waitForRecvPacket(test.pcRemote._pc, "srtp", 0);
+ await waitForRecvPacket(test.pcRemote._pc, "srtcp", 0);
+ await waitForSendPacket(test.pcRemote._pc, "rtp", 0);
+ await waitForSendPacket(test.pcRemote._pc, "rtcp", 0);
+ await waitForSendPacket(test.pcRemote._pc, "srtp", 0);
+ await waitForSendPacket(test.pcRemote._pc, "srtcp", 0);
+
+ await waitForRecvPacket(test.pcRemote._pc, "rtp", 1);
+ await waitForRecvPacket(test.pcRemote._pc, "rtcp", 1);
+ await waitForRecvPacket(test.pcRemote._pc, "srtp", 1);
+ await waitForRecvPacket(test.pcRemote._pc, "srtcp", 1);
+ await waitForSendPacket(test.pcRemote._pc, "rtp", 1);
+ await waitForSendPacket(test.pcRemote._pc, "rtcp", 1);
+ await waitForSendPacket(test.pcRemote._pc, "srtp", 1);
+ await waitForSendPacket(test.pcRemote._pc, "srtcp", 1);
+ }
+ ]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_close.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_close.html
new file mode 100644
index 0000000000..3edf677203
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_close.html
@@ -0,0 +1,134 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "991877",
+ title: "Basic RTCPeerConnection.close() tests"
+ });
+
+ runNetworkTest(function () {
+ var pc = new RTCPeerConnection();
+ var sender = pc.addTrack(getSilentTrack(), new MediaStream());
+ var exception = null;
+ var eTimeout = null;
+
+ // everything should be in initial state
+ is(pc.signalingState, "stable", "Initial signalingState is 'stable'");
+ is(pc.iceConnectionState, "new", "Initial iceConnectionState is 'new'");
+ is(pc.iceGatheringState, "new", "Initial iceGatheringState is 'new'");
+
+ var finish;
+ var finished = new Promise(resolve => finish = resolve);
+
+ var mustNotSettle = (p, ms, msg) => Promise.race([
+ p.then(() => ok(false, msg + " must not settle"),
+ e => ok(false, msg + " must not settle. Got " + e.name)),
+ wait(ms).then(() => ok(true, msg + " must not settle"))
+ ]);
+
+ var silence = mustNotSettle(pc.createOffer(), 1000,
+ "createOffer immediately followed by close");
+ try {
+ pc.close();
+ } catch (e) {
+ exception = e;
+ }
+ is(exception, null, "closing the connection raises no exception");
+ is(pc.signalingState, "closed", "Final signalingState is 'closed'");
+ is(pc.iceConnectionState, "closed", "Final iceConnectionState is 'closed'");
+
+ // test that pc is really closed (and doesn't crash, bug 1259728)
+ try {
+ pc.getLocalStreams();
+ } catch (e) {
+ exception = e;
+ }
+ is(exception && exception.name, "InvalidStateError",
+ "pc.getLocalStreams should throw when closed");
+ exception = null;
+
+ try {
+ pc.close();
+ } catch (e) {
+ exception = e;
+ }
+ is(exception, null, "A second close() should not raise an exception");
+ is(pc.signalingState, "closed", "Final signalingState stays at 'closed'");
+ is(pc.iceConnectionState, "closed", "Final iceConnectionState stays at 'closed'");
+
+ // Due to a limitation in our WebIDL compiler that prevents overloads with
+ // both Promise and non-Promise return types, legacy APIs with callbacks
+ // are unable to continue to throw exceptions. Luckily the spec uses
+ // exceptions solely for "programming errors" so this should not hinder
+ // working code from working, which is the point of the legacy API. All
+ // new code should use the promise API.
+ //
+ // The legacy methods that no longer throw on programming errors like
+ // "invalid-on-close" are:
+ // - createOffer
+ // - createAnswer
+ // - setLocalDescription
+ // - setRemoteDescription
+ // - addIceCandidate
+ // - getStats
+ //
+ // These legacy methods fire the error callback instead. This is not
+ // entirely to spec but is better than ignoring programming errors.
+
+ var offer = new RTCSessionDescription({ sdp: "sdp", type: "offer" });
+ var answer = new RTCSessionDescription({ sdp: "sdp", type: "answer" });
+ var candidate = new RTCIceCandidate({ candidate: "dummy",
+ sdpMid: "test",
+ sdpMLineIndex: 3 });
+
+ var doesFail = (p, msg) => p.then(generateErrorCallback(msg),
+ r => is(r.name, "InvalidStateError", msg));
+ Promise.all([
+ [pc.createOffer(), "createOffer"],
+ [pc.createOffer({offerToReceiveAudio: true}), "createOffer({offerToReceiveAudio: true})"],
+ [pc.createOffer({offerToReceiveAudio: false}), "createOffer({offerToReceiveAudio: false})"],
+ [pc.createOffer({offerToReceiveVideo: true}), "createOffer({offerToReceiveVideo: true})"],
+ [pc.createOffer({offerToReceiveVideo: false}), "createOffer({offerToReceiveVideo: false})"],
+ [pc.createAnswer(), "createAnswer"],
+ [pc.setLocalDescription(offer), "setLocalDescription"],
+ [pc.setRemoteDescription(answer), "setRemoteDescription"],
+ [pc.addIceCandidate(candidate), "addIceCandidate"],
+ [new Promise((y, n) => pc.createOffer(y, n)), "Legacy createOffer"],
+ [new Promise((y, n) => pc.createAnswer(y, n)), "Legacy createAnswer"],
+ [new Promise((y, n) => pc.setLocalDescription(offer, y, n)), "Legacy setLocalDescription"],
+ [new Promise((y, n) => pc.setRemoteDescription(answer, y, n)), "Legacy setRemoteDescription"],
+ [new Promise((y, n) => pc.addIceCandidate(candidate, y, n)), "Legacy addIceCandidate"],
+ [sender.replaceTrack(getSilentTrack()), "replaceTrack"],
+ ].map(([p, name]) => doesFail(p, name + " fails on close")))
+ .catch(reason => ok(false, "unexpected failure: " + reason))
+ .then(finish);
+
+ // Other methods are unaffected.
+
+ SimpleTest.doesThrow(function() {
+ pc.updateIce("Invalid RTC Configuration")},
+ "updateIce() on closed PC raised expected exception");
+
+ SimpleTest.doesThrow(function() {
+ pc.addStream("Invalid Media Stream")},
+ "addStream() on closed PC raised expected exception");
+
+ SimpleTest.doesThrow(function() {
+ pc.createDataChannel({})},
+ "createDataChannel() on closed PC raised expected exception");
+
+ SimpleTest.doesThrow(function() {
+ pc.setIdentityProvider("Invalid Provider")},
+ "setIdentityProvider() on closed PC raised expected exception");
+
+ return Promise.all([finished, silence]);
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_closeDuringIce.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_closeDuringIce.html
new file mode 100644
index 0000000000..db3a2922d5
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_closeDuringIce.html
@@ -0,0 +1,79 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1087629",
+ title: "Close PCs during ICE connectivity check"
+ });
+
+// Test closeDuringIce to simulate problems during peer connections
+
+
+function PC_LOCAL_SETUP_NULL_ICE_HANDLER(test) {
+ test.pcLocal.setupIceCandidateHandler(test, function() {}, function () {});
+}
+function PC_REMOTE_SETUP_NULL_ICE_HANDLER(test) {
+ test.pcRemote.setupIceCandidateHandler(test, function() {}, function () {});
+}
+function PC_REMOTE_ADD_FAKE_ICE_CANDIDATE(test) {
+ var cand = {"candidate":"candidate:0 1 UDP 2130379007 192.0.2.1 12345 typ host","sdpMid":"","sdpMLineIndex":0};
+ test.pcRemote.storeOrAddIceCandidate(cand);
+ info(test.pcRemote + " Stored fake candidate: " + JSON.stringify(cand));
+}
+function PC_LOCAL_ADD_FAKE_ICE_CANDIDATE(test) {
+ var cand = {"candidate":"candidate:0 1 UDP 2130379007 192.0.2.2 56789 typ host","sdpMid":"","sdpMLineIndex":0};
+ test.pcLocal.storeOrAddIceCandidate(cand);
+ info(test.pcLocal + " Stored fake candidate: " + JSON.stringify(cand));
+}
+function PC_LOCAL_CLOSE_DURING_ICE(test) {
+ return test.pcLocal.iceChecking.then(() => {
+ test.pcLocal.onsignalingstatechange = function () {};
+ test.pcLocal.close();
+ });
+}
+function PC_REMOTE_CLOSE_DURING_ICE(test) {
+ return test.pcRemote.iceChecking.then(() => {
+ test.pcRemote.onsignalingstatechange = function () {};
+ test.pcRemote.close();
+ });
+}
+function PC_LOCAL_WAIT_FOR_ICE_CHECKING(test) {
+ var resolveIceChecking;
+ test.pcLocal.iceChecking = new Promise(r => resolveIceChecking = r);
+ test.pcLocal.ice_connection_callbacks.checkIceStatus = () => {
+ if (test.pcLocal._pc.iceConnectionState === "checking") {
+ resolveIceChecking();
+ }
+ }
+}
+function PC_REMOTE_WAIT_FOR_ICE_CHECKING(test) {
+ var resolveIceChecking;
+ test.pcRemote.iceChecking = new Promise(r => resolveIceChecking = r);
+ test.pcRemote.ice_connection_callbacks.checkIceStatus = () => {
+ if (test.pcRemote._pc.iceConnectionState === "checking") {
+ resolveIceChecking();
+ }
+ }
+}
+
+runNetworkTest(() => {
+ var test = new PeerConnectionTest();
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.chain.replace("PC_LOCAL_SETUP_ICE_HANDLER", PC_LOCAL_SETUP_NULL_ICE_HANDLER);
+ test.chain.replace("PC_REMOTE_SETUP_ICE_HANDLER", PC_REMOTE_SETUP_NULL_ICE_HANDLER);
+ test.chain.insertAfter("PC_REMOTE_SETUP_NULL_ICE_HANDLER", PC_LOCAL_WAIT_FOR_ICE_CHECKING);
+ test.chain.insertAfter("PC_LOCAL_WAIT_FOR_ICE_CHECKING", PC_REMOTE_WAIT_FOR_ICE_CHECKING);
+ test.chain.removeAfter("PC_LOCAL_SET_REMOTE_DESCRIPTION");
+ test.chain.append([PC_REMOTE_ADD_FAKE_ICE_CANDIDATE, PC_LOCAL_ADD_FAKE_ICE_CANDIDATE,
+ PC_LOCAL_CLOSE_DURING_ICE, PC_REMOTE_CLOSE_DURING_ICE]);
+ return test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_codecNegotiationFailure.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_codecNegotiationFailure.html
new file mode 100644
index 0000000000..819e13fe1b
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_codecNegotiationFailure.html
@@ -0,0 +1,111 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="iceTestUtils.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1683934",
+ title: "RTCPeerConnection check codec negotiation failure"
+ });
+
+ function makeWeirdCodecs(sdp) {
+ return sdp
+ .replaceAll('VP8', 'VEEEEEEEEP8')
+ .replaceAll('VP9', 'VEEEEEEEEP9')
+ .replaceAll('H264', 'HERP264');
+ }
+
+ const tests = [
+ async function offererWeirdCodecs() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ const offer = await pc1.createOffer();
+ offer.sdp = makeWeirdCodecs(offer.sdp);
+ // It is not an error to receive an offer with no codecs we support
+ await pc2.setRemoteDescription(offer);
+ await pc2.setLocalDescription();
+ await wait(2000);
+ },
+
+ async function answererWeirdCodecs() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ const answer = await pc2.createAnswer();
+ answer.sdp = makeWeirdCodecs(answer.sdp);
+ try {
+ await pc1.setRemoteDescription(answer);
+ ok(false, "Should have thrown");
+ } catch (e) {
+ ok(true, "Should have thrown");
+ }
+ },
+
+ async function reoffererWeirdCodecs() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await connect(pc1, pc2, 32000, "Initial connection");
+
+ const offer = await pc1.createOffer();
+ offer.sdp = makeWeirdCodecs(offer.sdp);
+ // It is not an error to receive an offer with no codecs we support
+ await pc2.setRemoteDescription(offer);
+ await pc2.setLocalDescription();
+ await wait(2000);
+ },
+
+ async function reanswererWeirdCodecs() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await connect(pc1, pc2, 32000, "Initial connection");
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ const answer = await pc2.createAnswer();
+ answer.sdp = makeWeirdCodecs(answer.sdp);
+ try {
+ await pc1.setRemoteDescription(answer);
+ ok(false, "Should have thrown");
+ } catch (e) {
+ ok(true, "Should have thrown");
+ }
+ },
+
+ ];
+
+ runNetworkTest(async () => {
+ for (const test of tests) {
+ info(`Running test: ${test.name}`);
+ await test();
+ info(`Done running test: ${test.name}`);
+ }
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_constructedStream.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_constructedStream.html
new file mode 100644
index 0000000000..8431b7534e
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_constructedStream.html
@@ -0,0 +1,67 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1271669",
+ title: "Test that pc.addTrack() accepts any MediaStream",
+ visible: true
+});
+
+runNetworkTest(() => {
+ var test = new PeerConnectionTest();
+ var constructedStream;
+ var dummyStream = new MediaStream();
+ var dummyStreamTracks = [];
+
+ test.setMediaConstraints([ {audio: true, video: true}
+ , {audio: true}
+ , {video: true}
+ ], []);
+ test.chain.replace("PC_LOCAL_GUM", [
+ function PC_LOCAL_GUM_CONSTRUCTED_STREAM(test) {
+ return getUserMedia(test.pcLocal.constraints[0]).then(stream => {
+ constructedStream = new MediaStream(stream.getTracks());
+ test.pcLocal.attachLocalStream(constructedStream);
+ });
+ },
+ function PC_LOCAL_GUM_DUMMY_STREAM(test) {
+ return getUserMedia(test.pcLocal.constraints[1])
+ .then(stream => dummyStreamTracks.push(...stream.getTracks()))
+ .then(() => getUserMedia(test.pcLocal.constraints[2]))
+ .then(stream => dummyStreamTracks.push(...stream.getTracks()))
+ .then(() => dummyStreamTracks.forEach(t =>
+ test.pcLocal.attachLocalTrack(t, dummyStream)));
+ },
+ ]);
+
+ let checkSentTracksReceived = (sentStreamId, sentTracks) => {
+ let receivedStream =
+ test.pcRemote._pc.getRemoteStreams().find(s => s.id == sentStreamId);
+ ok(receivedStream, "We should receive a stream with with the sent stream's id (" + sentStreamId + ")");
+ if (!receivedStream) {
+ return;
+ }
+
+ is(receivedStream.getTracks().length, sentTracks.length,
+ "Should receive same number of tracks as were sent");
+ };
+
+ test.chain.append([
+ function PC_REMOTE_CHECK_RECEIVED_CONSTRUCTED_STREAM() {
+ checkSentTracksReceived(constructedStream.id, constructedStream.getTracks());
+ },
+ function PC_REMOTE_CHECK_RECEIVED_DUMMY_STREAM() {
+ checkSentTracksReceived(dummyStream.id, dummyStreamTracks);
+ },
+ ]);
+ return test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_disabledVideoPreNegotiation.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_disabledVideoPreNegotiation.html
new file mode 100644
index 0000000000..4c06de792e
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_disabledVideoPreNegotiation.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1570673",
+ title: "Sending an initially disabled video track should be playable remotely",
+ visible: true,
+ });
+
+ var test;
+ runNetworkTest(async (options) => {
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{video: true}], []);
+ test.chain.insertAfter("PC_LOCAL_GUM", function PC_LOCAL_DISABLE_VIDEO() {
+ for (const {track} of test.pcLocal._pc.getSenders()) {
+ if (track.kind == "video") {
+ track.enabled = false;
+ }
+ }
+ });
+ test.chain.append(async function PC_REMOTE_RECEIVING_BLACK() {
+ const v = test.pcRemote.remoteMediaElements[0];
+ is(v.readyState, v.HAVE_ENOUGH_DATA, "video element should be playing");
+ const h = new CaptureStreamTestHelper2D();
+ await h.waitForPixel(test.pcRemote.remoteMediaElements[0],
+ px => h.isPixel(px, h.black, 128));
+ });
+ await test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_encodingsNegotiation.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_encodingsNegotiation.html
new file mode 100644
index 0000000000..f46d7eb0d2
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_encodingsNegotiation.html
@@ -0,0 +1,85 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="simulcast.js"></script>
+ <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1401592",
+ title: "Simulcast negotiation tests",
+ visible: true
+});
+
+// simulcast negotiation is mostly tested in wpt, but we test a few
+// implementation-specific things here.
+const tests = [
+ async function checkVideoEncodingLimit() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await doOfferToRecvSimulcast(pc2, pc1, ["1", "2", "3", "4"]);
+
+ const {encodings} = sender.getParameters();
+ const rids = encodings.map(({rid}) => rid);
+ isDeeply(rids, ["1", "2", "3"]);
+
+ pc1.close();
+ pc2.close();
+ stream.getTracks().forEach(track => track.stop());
+ },
+
+ // wpt currently does not assume support for 3 encodings, which limits the
+ // effectiveness of its powers-of-2 test (since it can test only for 1 and 2)
+ async function checkScaleResolutionDownByAutoFillPowersOf2() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await doOfferToRecvSimulcast(pc2, pc1, ["1", "2", "3"]);
+
+ const {encodings} = sender.getParameters();
+ const scaleValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy);
+ isDeeply(scaleValues, [4, 2, 1]);
+ },
+
+ async function checkLibwebrtcRidLengthLimit() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await doOfferToRecvSimulcast(pc2, pc1, ["foo", "wibblywobblyjeremybearimy"]);
+ const {encodings} = sender.getParameters();
+ const rids = encodings.map(({rid}) => rid);
+ isDeeply(rids, ["foo"]);
+
+ pc1.close();
+ pc2.close();
+ stream.getTracks().forEach(track => track.stop());
+ },
+];
+
+runNetworkTest(async () => {
+ for (const test of tests) {
+ info(`Running test: ${test.name}`);
+ await test();
+ info(`Done running test: ${test.name}`);
+ }
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_errorCallbacks.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_errorCallbacks.html
new file mode 100644
index 0000000000..851a256509
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_errorCallbacks.html
@@ -0,0 +1,55 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "834270",
+ title: "Align PeerConnection error handling with WebRTC specification"
+ });
+
+ function validateReason(reason) {
+ ok(reason.name.length, "Reason name = " + reason.name);
+ ok(reason.message.length, "Reason message = " + reason.message);
+ };
+
+ function testCreateAnswerError() {
+ var pc = new RTCPeerConnection();
+ info ("Testing createAnswer error");
+ return pc.createAnswer()
+ .then(generateErrorCallback("createAnswer before offer should fail"),
+ validateReason);
+ };
+
+ function testSetLocalDescriptionError() {
+ var pc = new RTCPeerConnection();
+ info ("Testing setLocalDescription error");
+ return pc.setLocalDescription({ sdp: "Picklechips!", type: "offer" })
+ .then(generateErrorCallback("setLocalDescription with nonsense SDP should fail"),
+ validateReason);
+ };
+
+ function testSetRemoteDescriptionError() {
+ var pc = new RTCPeerConnection();
+ info ("Testing setRemoteDescription error");
+ return pc.setRemoteDescription({ sdp: "Who?", type: "offer" })
+ .then(generateErrorCallback("setRemoteDescription with nonsense SDP should fail"),
+ validateReason);
+ };
+
+ // No test for createOffer errors -- there's nothing we can do at this
+ // level to evoke an error in createOffer.
+
+ runNetworkTest(function () {
+ return testCreateAnswerError()
+ .then(testSetLocalDescriptionError)
+ .then(testSetRemoteDescriptionError);
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_extmapRenegotiation.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_extmapRenegotiation.html
new file mode 100644
index 0000000000..78c6bb986c
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_extmapRenegotiation.html
@@ -0,0 +1,325 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="iceTestUtils.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1799932",
+ title: "RTCPeerConnection check renegotiation of extmap"
+ });
+
+ function setExtmap(sdp, uri, id) {
+ const regex = new RegExp(`a=extmap:[0-9]+(\/[a-z]+)? ${uri}`, 'g');
+ if (id) {
+ return sdp.replaceAll(regex, `a=extmap:${id}$1 ${uri}`);
+ } else {
+ return sdp.replaceAll(regex, `a=unknownattr`);
+ }
+ }
+
+ function getExtmap(sdp, uri) {
+ const regex = new RegExp(`a=extmap:([0-9]+)(\/[a-z]+)? ${uri}`);
+ return sdp.match(regex)[1];
+ }
+
+ function replaceExtUri(sdp, oldUri, newUri) {
+ const regex = new RegExp(`(a=extmap:[0-9]+\/[a-z]+)? ${oldUri}`, 'g');
+ return sdp.replaceAll(regex, `$1 ${newUri}`);
+ }
+
+ const tests = [
+ async function checkAudioMidChange() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({audio: true});
+ pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await connect(pc1, pc2, 32000, "Initial connection");
+
+ // Sadly, there's no way to tell the offerer to change the extmap. Other
+ // types of endpoint could conceivably do this, so we at least don't want
+ // to crash.
+ // TODO: Would be nice to be able to test this with an endpoint that
+ // actually changes the ids it uses.
+ const reoffer = await pc1.createOffer();
+ reoffer.sdp = setExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid", 14);
+ info(`New reoffer: ${reoffer.sdp}`);
+ await pc2.setRemoteDescription(reoffer);
+ await pc2.setLocalDescription();
+ await wait(2000);
+ },
+
+ async function checkVideoMidChange() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await connect(pc1, pc2, 32000, "Initial connection");
+
+ // Sadly, there's no way to tell the offerer to change the extmap. Other
+ // types of endpoint could conceivably do this, so we at least don't want
+ // to crash.
+ // TODO: Would be nice to be able to test this with an endpoint that
+ // actually changes the ids it uses.
+ const reoffer = await pc1.createOffer();
+ reoffer.sdp = setExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid", 14);
+ info(`New reoffer: ${reoffer.sdp}`);
+ await pc2.setRemoteDescription(reoffer);
+ await pc2.setLocalDescription();
+ await wait(2000);
+ },
+
+ async function checkAudioMidSwap() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({audio: true});
+ pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await connect(pc1, pc2, 32000, "Initial connection");
+
+ // Sadly, there's no way to tell the offerer to change the extmap. Other
+ // types of endpoint could conceivably do this, so we at least don't want
+ // to crash.
+ // TODO: Would be nice to be able to test this with an endpoint that
+ // actually changes the ids it uses.
+ const reoffer = await pc1.createOffer();
+ const midId = getExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid");
+ const ssrcLevelId = getExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level");
+ reoffer.sdp = setExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid", ssrcLevelId);
+ reoffer.sdp = setExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", midId);
+ info(`New reoffer: ${reoffer.sdp}`);
+ try {
+ await pc2.setRemoteDescription(reoffer);
+ ok(false, "sRD should fail when it attempts extension id remapping");
+ } catch (e) {
+ ok(true, "sRD should fail when it attempts extension id remapping");
+ }
+ },
+
+ async function checkVideoMidSwap() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await connect(pc1, pc2, 32000, "Initial connection");
+
+ // Sadly, there's no way to tell the offerer to change the extmap. Other
+ // types of endpoint could conceivably do this, so we at least don't want
+ // to crash.
+ // TODO: Would be nice to be able to test this with an endpoint that
+ // actually changes the ids it uses.
+ const reoffer = await pc1.createOffer();
+ const midId = getExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid");
+ const toffsetId = getExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:toffset");
+ reoffer.sdp = setExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid", toffsetId);
+ reoffer.sdp = setExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:toffset", midId);
+ info(`New reoffer: ${reoffer.sdp}`);
+ try {
+ await pc2.setRemoteDescription(reoffer);
+ ok(false, "sRD should fail when it attempts extension id remapping");
+ } catch (e) {
+ ok(true, "sRD should fail when it attempts extension id remapping");
+ }
+ },
+
+ async function checkAudioIdReuse() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({audio: true});
+ pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await connect(pc1, pc2, 32000, "Initial connection");
+
+ // Sadly, there's no way to tell the offerer to change the extmap. Other
+ // types of endpoint could conceivably do this, so we at least don't want
+ // to crash.
+ // TODO: Would be nice to be able to test this with an endpoint that
+ // actually changes the ids it uses.
+ const reoffer = await pc1.createOffer();
+ // Change uri, but not the id, so the id now refers to foo.
+ reoffer.sdp = replaceExtUri(reoffer.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", "foo");
+ info(`New reoffer: ${reoffer.sdp}`);
+ try {
+ await pc2.setRemoteDescription(reoffer);
+ ok(false, "sRD should fail when it attempts extension id remapping");
+ } catch (e) {
+ ok(true, "sRD should fail when it attempts extension id remapping");
+ }
+ },
+
+ async function checkVideoIdReuse() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await connect(pc1, pc2, 32000, "Initial connection");
+
+ // Sadly, there's no way to tell the offerer to change the extmap. Other
+ // types of endpoint could conceivably do this, so we at least don't want
+ // to crash.
+ // TODO: Would be nice to be able to test this with an endpoint that
+ // actually changes the ids it uses.
+ const reoffer = await pc1.createOffer();
+ // Change uri, but not the id, so the id now refers to foo.
+ reoffer.sdp = replaceExtUri(reoffer.sdp, "urn:ietf:params:rtp-hdrext:toffset", "foo");
+ info(`New reoffer: ${reoffer.sdp}`);
+ try {
+ await pc2.setRemoteDescription(reoffer);
+ ok(false, "sRD should fail when it attempts extension id remapping");
+ } catch (e) {
+ ok(true, "sRD should fail when it attempts extension id remapping");
+ }
+ },
+
+ // What happens when remote answer uses an extmap id, and then a remote
+ // reoffer tries to use the same id for something else?
+ async function checkAudioIdReuseOffererThenAnswerer() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({audio: true});
+ pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ await connect(pc1, pc2, 32000, "Initial connection");
+
+ const reoffer = await pc2.createOffer();
+ // Change uri, but not the id, so the id now refers to foo.
+ reoffer.sdp = replaceExtUri(reoffer.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", "foo");
+ info(`New reoffer: ${reoffer.sdp}`);
+ try {
+ await pc1.setRemoteDescription(reoffer);
+ ok(false, "sRD should fail when it attempts extension id remapping");
+ } catch (e) {
+ ok(true, "sRD should fail when it attempts extension id remapping");
+ }
+ },
+
+ // What happens when a remote offer uses a different extmap id than the
+ // default? Does the answerer remember the new id in reoffers?
+ async function checkAudioIdReuseOffererThenAnswerer() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({audio: true});
+ pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ // Negotiate, but change id for ssrc-audio-level to something pc2 would
+ // not typically use.
+ await pc1.setLocalDescription();
+ const mungedOffer = setExtmap(pc1.localDescription.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", 12);
+ await pc2.setRemoteDescription({sdp: mungedOffer, type: "offer"});
+ await pc2.setLocalDescription();
+
+ const reoffer = await pc2.createOffer();
+ is(getExtmap(reoffer.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level"), "12");
+ },
+
+ async function checkAudioUnnegotiatedIdReuse1() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({audio: true});
+ pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ // Negotiate, but remove ssrc-audio-level from answer
+ await pc1.setLocalDescription();
+ const levelId = getExtmap(pc1.localDescription.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level");
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ const answerNoExt = setExtmap(pc2.localDescription.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", undefined);
+ await pc1.setRemoteDescription({sdp: answerNoExt, type: "answer"});
+
+ // Renegotiate, and use the id that offerer used for ssrc-audio-level for
+ // something different (while making sure we don't use it twice)
+ await pc2.setLocalDescription();
+ const mungedReoffer = setExtmap(pc2.localDescription.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid", levelId);
+ const twiceMungedReoffer = setExtmap(mungedReoffer, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", undefined);
+ await pc1.setRemoteDescription({sdp: twiceMungedReoffer, type: "offer"});
+ },
+
+ async function checkAudioUnnegotiatedIdReuse2() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({audio: true});
+ pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ // Negotiate, but remove ssrc-audio-level from offer. pc2 has never seen
+ // |levelId| in extmap yet, but internally probably wants to use that for
+ // ssrc-audio-level
+ await pc1.setLocalDescription();
+ const levelId = getExtmap(pc1.localDescription.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level");
+ const offerNoExt = setExtmap(pc1.localDescription.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", undefined);
+ await pc2.setRemoteDescription({sdp: offerNoExt, type: "offer"});
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+
+ // Renegotiate, but use |levelId| for something other than
+ // ssrc-audio-level. pc2 should not throw.
+ await pc1.setLocalDescription();
+ const mungedReoffer = setExtmap(pc1.localDescription.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid", levelId);
+ const twiceMungedReoffer = setExtmap(mungedReoffer, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", undefined);
+ await pc2.setRemoteDescription({sdp: twiceMungedReoffer, type: "offer"});
+ },
+
+ async function checkAudioUnnegotiatedIdReuse3() {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const stream = await navigator.mediaDevices.getUserMedia({audio: true});
+ pc1.addTrack(stream.getTracks()[0]);
+ pc2.addTrack(stream.getTracks()[0]);
+
+ // Negotiate, but replace ssrc-audio-level with something pc2 won't
+ // support in offer.
+ await pc1.setLocalDescription();
+ const levelId = getExtmap(pc1.localDescription.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level");
+ const mungedOffer = replaceExtUri(pc1.localDescription.sdp, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", "fooba");
+ await pc2.setRemoteDescription({sdp: mungedOffer, type: "offer"});
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+
+ // Renegotiate, and use levelId for something pc2 _will_ support.
+ await pc1.setLocalDescription();
+ const mungedReoffer = setExtmap(pc1.localDescription.sdp, "urn:ietf:params:rtp-hdrext:sdes:mid", levelId);
+ const twiceMungedReoffer = setExtmap(mungedReoffer, "urn:ietf:params:rtp-hdrext:ssrc-audio-level", undefined);
+ await pc2.setRemoteDescription({sdp: twiceMungedReoffer, type: "offer"});
+ },
+
+ ];
+
+ runNetworkTest(async () => {
+ for (const test of tests) {
+ info(`Running test: ${test.name}`);
+ await test();
+ info(`Done running test: ${test.name}`);
+ }
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_forwarding_basicAudioVideoCombined.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_forwarding_basicAudioVideoCombined.html
new file mode 100644
index 0000000000..84b53a123b
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_forwarding_basicAudioVideoCombined.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "931903",
+ title: "Forwarding a stream from a combined audio/video peerconnection to another"
+ });
+
+runNetworkTest(function() {
+ var gumTest = new PeerConnectionTest();
+
+ var forwardingOptions = { config_local: { label_suffix: "forwarded" },
+ config_remote: { label_suffix: "forwarded" } };
+ var forwardingTest = new PeerConnectionTest(forwardingOptions);
+
+ gumTest.setMediaConstraints([{audio: true, video: true}], []);
+ forwardingTest.setMediaConstraints([{audio: true, video: true}], []);
+ forwardingTest.chain.replace("PC_LOCAL_GUM", [
+ function PC_FORWARDING_CAPTUREVIDEO(test) {
+ var streams = gumTest.pcRemote._pc.getRemoteStreams();
+ is(streams.length, 1, "One stream to forward");
+ is(streams[0].getTracks().length, 2, "Forwarded stream has 2 tracks");
+ forwardingTest.pcLocal.attachLocalStream(streams[0]);
+ return Promise.resolve();
+ }
+ ]);
+ gumTest.chain.removeAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW");
+ return gumTest.chain.execute()
+ .then(() => forwardingTest.chain.execute())
+ .then(() => gumTest.close())
+ .then(() => forwardingTest.close());
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithSetConfiguration.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithSetConfiguration.html
new file mode 100644
index 0000000000..6710e628aa
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithSetConfiguration.html
@@ -0,0 +1,450 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="iceTestUtils.js"></script>
+ <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script></head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1253706",
+ title: "Test ICE gathering when setConfiguration is used to change the ICE config"
+ });
+
+ const tests = [
+ async function baselineV4Cases() {
+ await checkSrflx([{urls:[`stun:${turnAddressV4}`]}]);
+ await checkRelayUdp([{urls:[`turn:${turnAddressV4}`], username, credential}]);
+ await checkRelayTcp([{urls:[`turn:${turnAddressV4}?transport=tcp`], username, credential}]);
+ await checkRelayUdpTcp([{urls:[`turn:${turnAddressV4}`, `turn:${turnAddressV4}?transport=tcp`], username, credential}]);
+ await checkNoSrflx();
+ await checkNoRelay();
+ },
+
+ async function addStunServerBeforeOffer() {
+ const pc = new RTCPeerConnection();
+ try {
+ pc.setConfiguration({iceServers: [{urls:[`stun:${turnAddressV4}`]}]});
+ const candidates = await gatherWithTimeout(pc, 32000, `just a stun server`);
+ ok(candidates.some(c => c.candidate.includes("srflx")), "Should get at least one srflx candidate");
+ ok(!candidates.some(c => c.candidate.includes("relay")), "Should not get any relay candidates");
+ } finally {
+ pc.close();
+ }
+ },
+
+ async function addTurnServerBeforeOffer() {
+ const pc = new RTCPeerConnection();
+ try {
+ pc.setConfiguration({iceServers: [{urls:[`turn:${turnAddressV4}`], username, credential}]});
+ const candidates = await gatherWithTimeout(pc, 32000, `a turn (udp) server`);
+ ok(candidates.some(c => c.candidate.includes("srflx")), "Should get at least one srflx candidate");
+ ok(candidates.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate");
+ } finally {
+ pc.close();
+ }
+ },
+
+ async function addTurnTcpServerBeforeOffer() {
+ const pc = new RTCPeerConnection();
+ try {
+ pc.setConfiguration({iceServers: [{urls:[`turn:${turnAddressV4}?transport=tcp`], username, credential}]});
+ const candidates = await gatherWithTimeout(pc, 32000, `a turn (tcp) server`);
+ ok(!candidates.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates");
+ ok(candidates.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate");
+ } finally {
+ pc.close();
+ }
+ },
+
+ async function addStunServerAfterOffer() {
+ const pc = new RTCPeerConnection();
+ try {
+ const candidates1 = await gatherWithTimeout(pc, 32000, `no ICE servers`);
+ ok(!candidates1.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates");
+ ok(!candidates1.some(c => c.candidate.includes("relay")), "Should not get any relay candidates");
+ await pc.setLocalDescription({type: "rollback"});
+
+ pc.setConfiguration({iceServers: [{urls:[`stun:${turnAddressV4}`]}]});
+ const candidates2 = await gatherWithTimeout(pc, 32000, `just a stun server`);
+ ok(candidates2.some(c => c.candidate.includes("srflx")), "Should get at least one srflx candidate");
+ ok(!candidates2.some(c => c.candidate.includes("relay")), "Should not get any relay candidates");
+ } finally {
+ pc.close();
+ }
+ },
+
+ async function addTurnServerAfterOffer() {
+ const pc = new RTCPeerConnection();
+ try {
+ const candidates1 = await gatherWithTimeout(pc, 32000, `no ICE servers`);
+ ok(!candidates1.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates");
+ ok(!candidates1.some(c => c.candidate.includes("relay")), "Should not get any relay candidates");
+ await pc.setLocalDescription({type: "rollback"});
+
+ pc.setConfiguration({iceServers: [{urls:[`turn:${turnAddressV4}`], username, credential}]});
+ const candidates2 = await gatherWithTimeout(pc, 32000, `a turn (udp) server`);
+ ok(candidates2.some(c => c.candidate.includes("srflx")), "Should get at least one srflx candidate");
+ ok(candidates2.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate");
+ } finally {
+ pc.close();
+ }
+ },
+
+ async function addTurnTcpServerAfterOffer() {
+ const pc = new RTCPeerConnection();
+ try {
+ const candidates1 = await gatherWithTimeout(pc, 32000, `no ICE servers`);
+ ok(!candidates1.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates");
+ ok(!candidates1.some(c => c.candidate.includes("relay")), "Should not get any relay candidates");
+ await pc.setLocalDescription({type: "rollback"});
+
+ pc.setConfiguration({iceServers: [{urls:[`turn:${turnAddressV4}?transport=tcp`], username, credential}]});
+ const candidates2 = await gatherWithTimeout(pc, 32000, `a turn (tcp) server`);
+ ok(!candidates2.some(c => c.candidate.includes("srflx")), "Should get no srflx candidates");
+ ok(candidates2.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate");
+ } finally {
+ pc.close();
+ }
+ },
+
+ async function removeStunServerBeforeOffer() {
+ const pc = new RTCPeerConnection({iceServers: [{urls:[`stun:${turnAddressV4}`]}]});
+ try {
+ pc.setConfiguration({});
+ const candidates = await gatherWithTimeout(pc, 32000, `no ICE servers`);
+ ok(!candidates.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates");
+ ok(!candidates.some(c => c.candidate.includes("relay")), "Should not get any relay candidates");
+ } finally {
+ pc.close();
+ }
+ },
+
+ async function removeTurnServerBeforeOffer() {
+ const pc = new RTCPeerConnection({iceServers: [{urls:[`turn:${turnAddressV4}`], username, credential}]});
+ try {
+ pc.setConfiguration({});
+ const candidates = await gatherWithTimeout(pc, 32000, `no ICE servers`);
+ ok(!candidates.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates");
+ ok(!candidates.some(c => c.candidate.includes("relay")), "Should not get any relay candidates");
+ } finally {
+ pc.close();
+ }
+ },
+
+ async function removeTurnTcpServerBeforeOffer() {
+ const pc = new RTCPeerConnection({iceServers: [{urls:[`turn:${turnAddressV4}?transport=tcp`], username, credential}]});
+ try {
+ pc.setConfiguration({});
+ const candidates = await gatherWithTimeout(pc, 32000, `no ICE servers`);
+ ok(!candidates.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates");
+ ok(!candidates.some(c => c.candidate.includes("relay")), "Should not get any relay candidates");
+ } finally {
+ pc.close();
+ }
+ },
+
+ async function removeStunServerAfterOffer() {
+ const pc = new RTCPeerConnection({iceServers: [{urls:[`stun:${turnAddressV4}`]}]});
+ try {
+ const candidates1 = await gatherWithTimeout(pc, 32000, `just a stun server`);
+ ok(candidates1.some(c => c.candidate.includes("srflx")), "Should get at least one srflx candidate");
+ ok(!candidates1.some(c => c.candidate.includes("relay")), "Should not get any relay candidates");
+ await pc.setLocalDescription({type: "rollback"});
+
+ pc.setConfiguration({});
+ const candidates2 = await gatherWithTimeout(pc, 32000, `no ICE servers`);
+ ok(!candidates2.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates");
+ ok(!candidates2.some(c => c.candidate.includes("relay")), "Should not get any relay candidates");
+ } finally {
+ pc.close();
+ }
+ },
+
+ async function removeTurnServerAfterOffer() {
+ const pc = new RTCPeerConnection({iceServers: [{urls:[`turn:${turnAddressV4}`], username, credential}]});
+ try {
+ const candidates1 = await gatherWithTimeout(pc, 32000, `a turn (udp) server`);
+ ok(candidates1.some(c => c.candidate.includes("srflx")), "Should get at least one srflx candidate");
+ ok(candidates1.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate");
+ await pc.setLocalDescription({type: "rollback"});
+
+ pc.setConfiguration({});
+ const candidates2 = await gatherWithTimeout(pc, 32000, `no ICE servers`);
+ ok(!candidates2.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates");
+ ok(!candidates2.some(c => c.candidate.includes("relay")), "Should not get any relay candidates");
+ } finally {
+ pc.close();
+ }
+ },
+
+ async function removeTurnTcpServerAfterOffer() {
+ const pc = new RTCPeerConnection({iceServers: [{urls:[`turn:${turnAddressV4}?transport=tcp`], username, credential}]});
+ try {
+ const candidates1 = await gatherWithTimeout(pc, 32000, `a turn (tcp) server`);
+ ok(!candidates1.some(c => c.candidate.includes("srflx")), "Should get no srflx candidates");
+ ok(candidates1.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate");
+ await pc.setLocalDescription({type: "rollback"});
+
+ pc.setConfiguration({});
+ const candidates2 = await gatherWithTimeout(pc, 32000, `no ICE servers`);
+ ok(!candidates2.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates");
+ ok(!candidates2.some(c => c.candidate.includes("relay")), "Should not get any relay candidates");
+ } finally {
+ pc.close();
+ }
+ },
+
+ async function addStunServerAfterNegotiation() {
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection({iceServers: [{urls:[`stun:${turnAddressV4}`]}]});
+ try {
+ const candidatePromise = trickleIce(offerer);
+ await connect(offerer, answerer, 32000, `no ICE servers`);
+ const candidates = await candidatePromise;
+ const ufrags = Array.from(new Set(candidates.map(c => c.usernameFragment)));
+ is(ufrags.length, 1, "Should have one ufrag in candidate set");
+
+ offerer.setConfiguration({iceServers: [{urls:[`stun:${turnAddressV4}`]}]});
+ const candidates2 = await gatherWithTimeout(offerer, 32000, `just a stun server`);
+ ok(candidates2.some(c => c.candidate.includes("srflx")), "Should get at least one srflx candidate");
+ ok(!candidates2.some(c => c.candidate.includes("relay")), "Should not get any relay candidates");
+ const ufrags2 = Array.from(new Set(candidates2.map(c => c.usernameFragment)));
+ is(ufrags2.length, 1, "Should have one ufrag in candidate set");
+ isnot(ufrags[0], ufrags2[0], "ufrag should change, because setConfiguration should have triggered an ICE restart");
+ } finally {
+ offerer.close();
+ answerer.close();
+ }
+ },
+
+ async function addTurnServerAfterNegotiation() {
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection({iceServers: [{urls:[`stun:${turnAddressV4}`]}]});
+ try {
+ const candidatePromise = trickleIce(offerer);
+ await connect(offerer, answerer, 32000, `no ICE servers`);
+ const candidates = await candidatePromise;
+ const ufrags = Array.from(new Set(candidates.map(c => c.usernameFragment)));
+ is(ufrags.length, 1, "Should have one ufrag in candidate set");
+
+ offerer.setConfiguration({iceServers: [{urls:[`turn:${turnAddressV4}`], username, credential}]});
+ const candidates2 = await gatherWithTimeout(offerer, 32000, `a turn (udp) server`);
+ ok(candidates2.some(c => c.candidate.includes("srflx")), "Should get at least one srflx candidate");
+ ok(candidates2.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate");
+ const ufrags2 = Array.from(new Set(candidates2.map(c => c.usernameFragment)));
+ is(ufrags2.length, 1, "Should have one ufrag in candidate set");
+ isnot(ufrags[0], ufrags2[0], "ufrag should change, because setConfiguration should have triggered an ICE restart");
+ } finally {
+ offerer.close();
+ answerer.close();
+ }
+ },
+
+ async function addTurnTcpServerAfterNegotiation() {
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection({iceServers: [{urls:[`stun:${turnAddressV4}`]}]});
+ try {
+ const candidatePromise = trickleIce(offerer);
+ await connect(offerer, answerer, 32000, `no ICE servers`);
+ const candidates = await candidatePromise;
+ const ufrags = Array.from(new Set(candidates.map(c => c.usernameFragment)));
+ is(ufrags.length, 1, "Should have one ufrag in candidate set");
+
+ offerer.setConfiguration({iceServers: [{urls:[`turn:${turnAddressV4}?transport=tcp`], username, credential}]});
+ const candidates2 = await gatherWithTimeout(offerer, 32000, `a turn (tcp) server`);
+ ok(!candidates2.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates");
+ ok(candidates2.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate");
+ const ufrags2 = Array.from(new Set(candidates2.map(c => c.usernameFragment)));
+ is(ufrags2.length, 1, "Should have one ufrag in candidate set");
+ isnot(ufrags[0], ufrags2[0], "ufrag should change, because setConfiguration should have triggered an ICE restart");
+ } finally {
+ offerer.close();
+ answerer.close();
+ }
+ },
+
+ async function addStunServerBeforeCreateAnswer() {
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+
+ try {
+ await answerer.setRemoteDescription(await offerer.createOffer({offerToReceiveAudio: true}));
+
+ answerer.setConfiguration({iceServers: [{urls:[`stun:${turnAddressV4}`]}]});
+ const candidates = await gatherWithTimeout(answerer, 32000, `just a stun server`);
+ ok(candidates.some(c => c.candidate.includes("srflx")), "Should get at least one srflx candidate");
+ ok(!candidates.some(c => c.candidate.includes("relay")), "Should not get any relay candidates");
+ } finally {
+ offerer.close();
+ answerer.close();
+ }
+ },
+
+ async function addTurnServerBeforeCreateAnswer() {
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+
+ try {
+ await answerer.setRemoteDescription(await offerer.createOffer({offerToReceiveAudio: true}));
+
+ answerer.setConfiguration({iceServers: [{urls:[`turn:${turnAddressV4}`], username, credential}]});
+ const candidates = await gatherWithTimeout(answerer, 32000, `a turn (udp) server`);
+ ok(candidates.some(c => c.candidate.includes("srflx")), "Should get at least one srflx candidate");
+ ok(candidates.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate");
+ } finally {
+ offerer.close();
+ answerer.close();
+ }
+ },
+
+ async function addTurnTcpServerBeforeCreateAnswer() {
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+
+ try {
+ await answerer.setRemoteDescription(await offerer.createOffer({offerToReceiveAudio: true}));
+
+ answerer.setConfiguration({iceServers: [{urls:[`turn:${turnAddressV4}?transport=tcp`], username, credential}]});
+ const candidates = await gatherWithTimeout(answerer, 32000, `a turn (tcp) server`);
+ ok(!candidates.some(c => c.candidate.includes("srflx")), "Should not get any srflx candidates");
+ ok(candidates.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate");
+ } finally {
+ offerer.close();
+ answerer.close();
+ }
+ },
+
+ async function relayPolicyPreventsSrflx() {
+ const pc = new RTCPeerConnection();
+ try {
+ pc.setConfiguration({iceServers: [{urls:[`turn:${turnAddressV4}`], username, credential}], iceTransportPolicy: "relay"});
+ const candidates = await gatherWithTimeout(pc, 32000, `a turn (udp) server`);
+ ok(!candidates.some(c => c.candidate.includes("srflx")), "Should not get a srflx candidate");
+ ok(candidates.some(c => c.candidate.includes("relay")), "Should get at least one relay candidate");
+ } finally {
+ pc.close();
+ }
+ },
+
+ async function addOffererStunServerAllowsIceToConnect() {
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+
+ try {
+ try {
+ // Both ends are behind a simulated endpoint-independent NAT, which
+ // requires at least one side to have a srflx candidate to work.
+ await connect(offerer, answerer, 2000, `no ICE servers`);
+ ok(false, "ICE should either have failed, or timed out!");
+ } catch (e) {
+ if (!(e instanceof Error)) throw e;
+ ok(true, "ICE should either have failed, or timed out!");
+ }
+
+ offerer.setConfiguration({iceServers: [{urls:[`stun:${turnAddressV4}`]}]});
+ await connect(offerer, answerer, 32000, `just a STUN server`);
+ } finally {
+ offerer.close();
+ answerer.close();
+ }
+ },
+
+ async function addAnswererStunServerDoesNotAllowIceToConnect() {
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+
+ try {
+ try {
+ // Both ends are behind a simulated endpoint-independent NAT, which
+ // requires at least one side to have a srflx candidate to work.
+ await connect(offerer, answerer, 2000, `no ICE servers`);
+ ok(false, "ICE should either have failed, or timed out!");
+ } catch (e) {
+ if (!(e instanceof Error)) throw e;
+ ok(true, "ICE should either have failed, or timed out!");
+ }
+
+ // This _won't_ help, because the answerer does not get to decide to
+ // trigger an ICE restart.
+ answerer.setConfiguration({iceServers: [{urls:[`stun:${turnAddressV4}`]}]});
+ try {
+ await connectNoTrickleWait(offerer, answerer, 2000, `no ICE servers`);
+ ok(false, "ICE should either have failed, or timed out!");
+ } catch (e) {
+ if (!(e instanceof Error)) throw e;
+ ok(true, "ICE should either have failed, or timed out!");
+ }
+ } finally {
+ offerer.close();
+ answerer.close();
+ }
+ },
+
+ async function addOffererTurnServerAllowsIceToConnect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.filtering_type', 'PORT_DEPENDENT'],
+ ['media.peerconnection.nat_simulator.mapping_type', 'PORT_DEPENDENT']);
+
+ const offerer = new RTCPeerConnection({iceServers: [{urls:[`stun:${turnAddressV4}`]}]});
+ const answerer = new RTCPeerConnection({iceServers: [{urls:[`stun:${turnAddressV4}`]}]});
+
+ try {
+ try {
+ // Both ends are behind a simulated port-dependent NAT, which
+ // requires at least one side to have a relay candidate to work.
+ await connect(offerer, answerer, 2000, `just a STUN server`);
+ ok(false, "ICE should either have failed, or timed out!");
+ } catch (e) {
+ if (!(e instanceof Error)) throw e;
+ ok(true, "ICE should either have failed, or timed out!");
+ }
+
+ offerer.setConfiguration({iceServers: [{urls:[`turn:${turnAddressV4}`], username, credential}]});
+ await connect(offerer, answerer, 32000, `a TURN (udp) server`);
+ } finally {
+ offerer.close();
+ answerer.close();
+ await SpecialPowers.popPrefEnv();
+ }
+ },
+
+ ];
+
+ runNetworkTest(async () => {
+ const turnServer = iceServersArray.find(server => "username" in server);
+ username = turnServer.username;
+ credential = turnServer.credential;
+ // Just use the first url. It might make sense to look for TURNS first,
+ // since that will always use a hostname, but on CI we don't have TURNS
+ // support anyway (see bug 1323439).
+ const turnHostname = getTurnHostname(turnServer.urls[0]);
+ turnAddressV4 = await dnsLookupV4(turnHostname);
+
+ await pushPrefs(
+ ['media.peerconnection.ice.obfuscate_host_addresses', false],
+ ['media.peerconnection.nat_simulator.filtering_type', 'ENDPOINT_INDEPENDENT'],
+ ['media.peerconnection.nat_simulator.mapping_type', 'ENDPOINT_INDEPENDENT'],
+ ['media.peerconnection.ice.loopback', true],
+ ['media.getusermedia.insecure.enabled', true]);
+
+ for (const test of tests) {
+ info(`Running test: ${test.name}`);
+ try {
+ await test();
+ } catch (e) {
+ ok(false, `Caught ${e.name}: ${e.message} ${e.stack}`);
+ }
+ info(`Done running test: ${test.name}`);
+ // Make sure we don't build up a pile of GC work, and also get PCImpl to
+ // print their timecards.
+ await new Promise(r => SpecialPowers.exactGC(r));
+ }
+
+ await SpecialPowers.popPrefEnv();
+ }, { useIceServer: true });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithStun300.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithStun300.html
new file mode 100644
index 0000000000..50bc4a6553
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithStun300.html
@@ -0,0 +1,269 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="iceTestUtils.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "857668",
+ title: "RTCPeerConnection check STUN gathering with STUN/300 responses"
+ });
+
+ /* This is pretty hairy, so some background:
+ * Spec is here: https://datatracker.ietf.org/doc/html/rfc8489#section-10
+ * STUN/300 responses allow a server to redirect STUN requests to one or
+ more other servers, as ALTERNATE-SERVER attributes.
+ * The server specifies the IP address, IP version, and port for each
+ ALTERNATE-SERVER.
+ * The spec allows multiple rounds of redirects, and requires the client to
+ remember the servers it has already tried to avoid redirect loops.
+ * For TURNS, the TURN server can also supply an ALTERNATE-DOMAIN attribute,
+ which the client MUST use for the TLS handshake on the new target. The
+ client does _not_ use this as an FQDN; it always uses the address in the
+ ALTERNATE-SERVER. ALTERNATE-DOMAIN is meaningless in the non-TLS case.
+ * STUN/300 with ALTERNATE-SERVER is only defined for the TURN Allocate
+ message type (at least in the context of ICE). Clients are supposed to
+ treat STUN/300 as an unrecoverable error in all other cases. The TURN spec
+ does _not_ spell out how a client should handle multiple ALTERNATE-SERVERs.
+ We just take the first one that we have not already tried, and that is the
+ same IP version that we started with. This is because switching the IP
+ version is problematic for ICE.
+ * The test TURN server opens extra ports that will respond with redirects to
+ the _real_ ports, but the address remains the same. This is because we
+ cannot know ahead of time whether the machine we're running on has more
+ than one IP address of each version. This means the test TURN server is not
+ useful for testing cases where the address changes. Also, the test TURN
+ server does not currently know how to respond with multiple
+ ALTERNATE-SERVERs.
+ * To test cases where the _address_ changes, we instead use a feature in the
+ NAT simulator to respond with fake redirects when the destination address
+ matches an address that we configure with a pref. This feature can add
+ multiple ALTERNATE-SERVERs.
+ * The test TURN server's STUN/300 responses have a proper MESSAGE-INTEGRITY,
+ but the NAT simulator's do _not_. For now, we want both cases to work,
+ because some servers respond with STUN/300 without including
+ MESSAGE-INTEGRITY. This is a spec violation, even though the spec
+ contradicts itself in non-normative language elsewhere.
+ * Right now, neither the NAT simulator nor the test TURN server support
+ ALTERNATE-DOMAIN.
+ */
+
+ // These are the ports the test TURN server will respond with redirects on.
+ // The test TURN server tells us what these are in the JSON it spits out when
+ // we start it.
+ let turnRedirectPort;
+ let turnsRedirectPort;
+
+ // These are the addresses that we will configure the NAT simulator to
+ // redirect to. We do DNS lookups of the host in iceServersArray (provided
+ // by the test TURN server), and put the results here. On some platforms this
+ // will be 127.0.0.1 and ::1, but on others we may use a real address.
+ let redirectTargetV4;
+
+ // Test TURN server tells us these in the JSON it spits out when we start it
+ let username;
+ let credential;
+
+ // This is the address we will configure the NAT simulator to respond with
+ // redirects for. We use an address from TEST-NET since it is really unlikely
+ // we'll see that on a real machine, and also because we do not have
+ // special-case code in nICEr for TEST-NET (like we do for link-local, for
+ // example).
+ const redirectAddressV4 = '198.51.100.1';
+
+ const tests = [
+ async function baselineV4Cases() {
+ await checkSrflx([{urls:[`stun:${redirectTargetV4}`]}]);
+ await checkRelayUdp([{urls:[`turn:${redirectTargetV4}`], username, credential}]);
+ await checkRelayTcp([{urls:[`turn:${redirectTargetV4}?transport=tcp`], username, credential}]);
+ await checkRelayUdpTcp([{urls:[`turn:${redirectTargetV4}`, `turn:${redirectTargetV4}?transport=tcp`], username, credential}]);
+ },
+
+ async function stunV4Redirect() {
+ // This test uses the test TURN server, because nICEr drops responses
+ // without MESSAGE-INTEGRITY on the floor _unless_ they are a STUN/300 to
+ // an Allocate request. If we tried to use the NAT simulator for this, we
+ // would have to wait for nICEr to time out, since the NAT simulator does
+ // not know how to do MESSAGE-INTEGRITY.
+ await checkNoSrflx(
+ [{urls:[`stun:${redirectTargetV4}:${turnRedirectPort}`]}]);
+ },
+
+ async function turnV4UdpPortRedirect() {
+ await checkRelayUdp([{urls:[`turn:${redirectTargetV4}:${turnRedirectPort}`], username, credential}]);
+ },
+
+ async function turnV4TcpPortRedirect() {
+ await checkRelayTcp([{urls:[`turn:${redirectTargetV4}:${turnRedirectPort}?transport=tcp`], username, credential}]);
+ },
+
+ async function turnV4UdpTcpPortRedirect() {
+ await checkRelayUdpTcp([{urls:[`turn:${redirectTargetV4}:${turnRedirectPort}`, `turn:${redirectTargetV4}:${turnRedirectPort}?transport=tcp`], username, credential}]);
+ },
+
+ async function turnV4UdpAddressRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV4}`]);
+ await checkRelayUdp([{urls:[`turn:${redirectAddressV4}`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV4TcpAddressRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV4}`]);
+ await checkRelayTcp([{urls:[`turn:${redirectAddressV4}?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV4UdpTcpAddressRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV4}`]);
+ await checkRelayUdpTcp([{urls:[`turn:${redirectAddressV4}`, `turn:${redirectAddressV4}?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV4UdpEmptyRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', '']);
+ await checkNoRelay([{urls:[`turn:${redirectAddressV4}`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV4TcpEmptyRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', '']);
+ await checkNoRelay([{urls:[`turn:${redirectAddressV4}?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV4UdpTcpEmptyRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', '']);
+ await checkNoRelay([{urls:[`turn:${redirectAddressV4}`, `turn:${redirectAddressV4}?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV4UdpAddressAndPortRedirect() {
+ // This should result in two rounds of redirection; the first is by
+ // address, the second is by port.
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV4}`]);
+ await checkRelayUdp([{urls:[`turn:${redirectAddressV4}:${turnRedirectPort}`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV4TcpAddressAndPortRedirect() {
+ // This should result in two rounds of redirection; the first is by
+ // address, the second is by port.
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV4}`]);
+ await checkRelayTcp([{urls:[`turn:${redirectAddressV4}:${turnRedirectPort}?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV4UdpTcpAddressAndPortRedirect() {
+ // This should result in two rounds of redirection; the first is by
+ // address, the second is by port.
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV4}`]);
+ await checkRelayUdpTcp([{urls:[`turn:${redirectAddressV4}:${turnRedirectPort}`, `turn:${redirectAddressV4}:${turnRedirectPort}?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV4UdpRedirectLoop() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV4}`]);
+ // If we don't detect the loop, gathering will not finish
+ await checkNoRelay([{urls:[`turn:${redirectAddressV4}`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV4TcpRedirectLoop() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV4}`]);
+ // If we don't detect the loop, gathering will not finish
+ await checkNoRelay([{urls:[`turn:${redirectAddressV4}?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV4UdpTcpRedirectLoop() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV4}`]);
+ // If we don't detect the loop, gathering will not finish
+ await checkNoRelay([{urls:[`turn:${redirectAddressV4}`, `turn:${redirectAddressV4}?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV4UdpMultipleAddressRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV4},${redirectTargetV4}`]);
+ await checkRelayUdp([{urls:[`turn:${redirectAddressV4}`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV4TcpMultipleAddressRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV4},${redirectTargetV4}`]);
+ await checkRelayTcp([{urls:[`turn:${redirectAddressV4}?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV4UdpTcpMultipleAddressRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV4}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV4},${redirectTargetV4}`]);
+ await checkRelayUdpTcp([{urls:[`turn:${redirectAddressV4}`, `turn:${redirectAddressV4}?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+ ];
+
+ runNetworkTest(async () => {
+ const turnServer = iceServersArray.find(server => "username" in server);
+ username = turnServer.username;
+ credential = turnServer.credential;
+ // Special props, non-standard
+ turnRedirectPort = turnServer.turn_redirect_port;
+ turnsRedirectPort = turnServer.turns_redirect_port;
+ // Just use the first url. It might make sense to look for TURNS first,
+ // since that will always use a hostname, but on CI we don't have TURNS
+ // support anyway (see bug 1323439).
+ const turnHostname = getTurnHostname(turnServer.urls[0]);
+ redirectTargetV4 = await dnsLookupV4(turnHostname);
+
+ await pushPrefs(
+ ['media.peerconnection.ice.obfuscate_host_addresses', false],
+ ['media.peerconnection.nat_simulator.filtering_type', 'ENDPOINT_INDEPENDENT'],
+ ['media.peerconnection.nat_simulator.mapping_type', 'ENDPOINT_INDEPENDENT'],
+ ['media.peerconnection.ice.loopback', true],
+ ['media.getusermedia.insecure.enabled', true]);
+
+ for (const test of tests) {
+ info(`Running test: ${test.name}`);
+ await test();
+ info(`Done running test: ${test.name}`);
+ }
+
+ await SpecialPowers.popPrefEnv();
+ }, { useIceServer: true });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithStun300IPv6.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithStun300IPv6.html
new file mode 100644
index 0000000000..16f8f39978
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_gatherWithStun300IPv6.html
@@ -0,0 +1,283 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="iceTestUtils.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "857668",
+ title: "RTCPeerConnection check STUN gathering with STUN/300 responses (IPv6)"
+ });
+
+ /* This is pretty hairy, so some background:
+ * Spec is here: https://datatracker.ietf.org/doc/html/rfc8489#section-10
+ * STUN/300 responses allow a server to redirect STUN requests to one or
+ more other servers, as ALTERNATE-SERVER attributes.
+ * The server specifies the IP address, IP version, and port for each
+ ALTERNATE-SERVER.
+ * The spec allows multiple rounds of redirects, and requires the client to
+ remember the servers it has already tried to avoid redirect loops.
+ * For TURNS, the TURN server can also supply an ALTERNATE-DOMAIN attribute,
+ which the client MUST use for the TLS handshake on the new target. The
+ client does _not_ use this as an FQDN; it always uses the address in the
+ ALTERNATE-SERVER. ALTERNATE-DOMAIN is meaningless in the non-TLS case.
+ * STUN/300 with ALTERNATE-SERVER is only defined for the TURN Allocate
+ message type (at least in the context of ICE). Clients are supposed to
+ treat STUN/300 as an unrecoverable error in all other cases. The TURN spec
+ does _not_ spell out how a client should handle multiple ALTERNATE-SERVERs.
+ We just take the first one that we have not already tried, and that is the
+ same IP version that we started with. This is because switching the IP
+ version is problematic for ICE.
+ * The test TURN server opens extra ports that will respond with redirects to
+ the _real_ ports, but the address remains the same. This is because we
+ cannot know ahead of time whether the machine we're running on has more
+ than one IP address of each version. This means the test TURN server is not
+ useful for testing cases where the address changes. Also, the test TURN
+ server does not currently know how to respond with multiple
+ ALTERNATE-SERVERs.
+ * To test cases where the _address_ changes, we instead use a feature in the
+ NAT simulator to respond with fake redirects when the destination address
+ matches an address that we configure with a pref. This feature can add
+ multiple ALTERNATE-SERVERs.
+ * The test TURN server's STUN/300 responses have a proper MESSAGE-INTEGRITY,
+ but the NAT simulator's do _not_. For now, we want both cases to work,
+ because some servers respond with STUN/300 without including
+ MESSAGE-INTEGRITY. This is a spec violation, even though the spec
+ contradicts itself in non-normative language elsewhere.
+ * Right now, neither the NAT simulator nor the test TURN server support
+ ALTERNATE-DOMAIN.
+ */
+
+ // These are the ports the test TURN server will respond with redirects on.
+ // The test TURN server tells us what these are in the JSON it spits out when
+ // we start it.
+ let turnRedirectPort;
+ let turnsRedirectPort;
+
+ // These are the addresses that we will configure the NAT simulator to
+ // redirect to. We do DNS lookups of the host in iceServersArray (provided
+ // by the test TURN server), and put the results here. On some platforms this
+ // will be 127.0.0.1 and ::1, but on others we may use a real address.
+ let redirectTargetV6;
+
+ // Test TURN server tells us these in the JSON it spits out when we start it
+ let username;
+ let credential;
+
+ // This is the address we will configure the NAT simulator to respond with
+ // redirects for. We use an address from TEST-NET since it is really unlikely
+ // we'll see that on a real machine, and also because we do not have
+ // special-case code in nICEr for TEST-NET (like we do for link-local, for
+ // example).
+ const redirectAddressV6 = '::ffff:198.51.100.1';
+
+ const tests = [
+ async function baselineV6Cases() {
+ await checkSrflx([{urls:[`stun:[${redirectTargetV6}]`]}]);
+ await checkRelayUdp([{urls:[`turn:[${redirectTargetV6}]`], username, credential}]);
+ await checkRelayTcp([{urls:[`turn:[${redirectTargetV6}]?transport=tcp`], username, credential}]);
+ await checkRelayUdpTcp([{urls:[`turn:[${redirectTargetV6}]`, `turn:[${redirectTargetV6}]?transport=tcp`], username, credential}]);
+ },
+
+ async function stunV6Redirect() {
+ // This test uses the test TURN server, because nICEr drops responses
+ // without MESSAGE-INTEGRITY on the floor _unless_ they are a STUN/300 to
+ // an Allocate request. If we tried to use the NAT simulator for this, we
+ // would have to wait for nICEr to time out, since the NAT simulator does
+ // not know how to do MESSAGE-INTEGRITY.
+ await checkNoSrflx(
+ [{urls:[`stun:[${redirectTargetV6}]:${turnRedirectPort}`]}]);
+ },
+
+ async function turnV6UdpPortRedirect() {
+ await checkRelayUdp([{urls:[`turn:[${redirectTargetV6}]:${turnRedirectPort}`], username, credential}]);
+ },
+
+ async function turnV6TcpPortRedirect() {
+ await checkRelayTcp([{urls:[`turn:[${redirectTargetV6}]:${turnRedirectPort}?transport=tcp`], username, credential}]);
+ },
+
+ async function turnV6UdpTcpPortRedirect() {
+ await checkRelayUdpTcp([{urls:[`turn:[${redirectTargetV6}]:${turnRedirectPort}`, `turn:[${redirectTargetV6}]:${turnRedirectPort}?transport=tcp`], username, credential}]);
+ },
+
+ async function turnV6UdpAddressRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV6}`]);
+ await checkRelayUdp([{urls:[`turn:[${redirectAddressV6}]`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV6TcpAddressRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV6}`]);
+ await checkRelayTcp([{urls:[`turn:[${redirectAddressV6}]?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV6UdpTcpAddressRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV6}`]);
+ await checkRelayUdpTcp([{urls:[`turn:[${redirectAddressV6}]`, `turn:[${redirectAddressV6}]?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV6UdpEmptyRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', '']);
+ await checkNoRelay([{urls:[`turn:[${redirectAddressV6}]`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV6TcpEmptyRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', '']);
+ await checkNoRelay([{urls:[`turn:[${redirectAddressV6}]?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV6UdpTcpEmptyRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', '']);
+ await checkNoRelay([{urls:[`turn:[${redirectAddressV6}]`, `turn:[${redirectAddressV6}]?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV6UdpAddressAndPortRedirect() {
+ // This should result in two rounds of redirection; the first is by
+ // address, the second is by port.
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV6}`]);
+ await checkRelayUdp([{urls:[`turn:[${redirectAddressV6}]:${turnRedirectPort}`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV6TcpAddressAndPortRedirect() {
+ // This should result in two rounds of redirection; the first is by
+ // address, the second is by port.
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV6}`]);
+ await checkRelayTcp([{urls:[`turn:[${redirectAddressV6}]:${turnRedirectPort}?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV6UdpTcpAddressAndPortRedirect() {
+ // This should result in two rounds of redirection; the first is by
+ // address, the second is by port.
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectTargetV6}`]);
+ await checkRelayUdpTcp([{urls:[`turn:[${redirectAddressV6}]:${turnRedirectPort}`, `turn:[${redirectAddressV6}]:${turnRedirectPort}?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV6UdpRedirectLoop() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV6}`]);
+ // If we don't detect the loop, gathering will not finish
+ await checkNoRelay([{urls:[`turn:[${redirectAddressV6}]`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV6TcpRedirectLoop() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV6}`]);
+ // If we don't detect the loop, gathering will not finish
+ await checkNoRelay([{urls:[`turn:[${redirectAddressV6}]?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV6UdpTcpRedirectLoop() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV6}`]);
+ // If we don't detect the loop, gathering will not finish
+ await checkNoRelay([{urls:[`turn:[${redirectAddressV6}]`, `turn:[${redirectAddressV6}]?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV6UdpMultipleAddressRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV6},${redirectTargetV6}`]);
+ await checkRelayUdp([{urls:[`turn:[${redirectAddressV6}]`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV6TcpMultipleAddressRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV6},${redirectTargetV6}`]);
+ await checkRelayTcp([{urls:[`turn:[${redirectAddressV6}]?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+
+ async function turnV6UdpTcpMultipleAddressRedirect() {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.redirect_address', `${redirectAddressV6}`],
+ ['media.peerconnection.nat_simulator.redirect_targets', `${redirectAddressV6},${redirectTargetV6}`]);
+ await checkRelayUdpTcp([{urls:[`turn:[${redirectAddressV6}]`, `turn:[${redirectAddressV6}]?transport=tcp`], username, credential}]);
+ await SpecialPowers.popPrefEnv();
+ },
+ ];
+
+ runNetworkTest(async () => {
+ const turnServer = iceServersArray.find(server => "username" in server);
+ username = turnServer.username;
+ credential = turnServer.credential;
+ // Special props, non-standard
+ turnRedirectPort = turnServer.turn_redirect_port;
+ turnsRedirectPort = turnServer.turns_redirect_port;
+ // Just use the first url. It might make sense to look for TURNS first,
+ // since that will always use a hostname, but on CI we don't have TURNS
+ // support anyway (see bug 1323439).
+ const turnHostname = getTurnHostname(turnServer.urls[0]);
+
+ await pushPrefs(
+ ['media.peerconnection.ice.obfuscate_host_addresses', false],
+ ['media.peerconnection.nat_simulator.filtering_type', 'ENDPOINT_INDEPENDENT'],
+ ['media.peerconnection.nat_simulator.mapping_type', 'ENDPOINT_INDEPENDENT'],
+ ['media.peerconnection.ice.loopback', true],
+ ['media.getusermedia.insecure.enabled', true]);
+
+ if (await ipv6Supported()) {
+ redirectTargetV6 = await dnsLookupV6(turnHostname);
+ if (redirectTargetV6 == '' && turnHostname == 'localhost') {
+ // Our testers don't seem to have IPv6 DNS resolution for localhost
+ // set up...
+ redirectTargetV6 = '::1';
+ }
+
+ if (redirectTargetV6 != '') {
+ for (const test of tests) {
+ info(`Running test: ${test.name}`);
+ await test();
+ info(`Done running test: ${test.name}`);
+ }
+ } else {
+ ok(false, `This machine has an IPv6 address, but ${turnHostname} does not resolve to an IPv6 address`);
+ }
+ } else {
+ ok(false, 'This machine appears to not have an IPv6 address');
+ }
+
+ await SpecialPowers.popPrefEnv();
+ }, { useIceServer: true });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_glean.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_glean.html
new file mode 100644
index 0000000000..d5d6026ee5
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_glean.html
@@ -0,0 +1,488 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1401592",
+ title: "Test that glean is recording stats as expected",
+ visible: true
+});
+
+const { AppConstants } = SpecialPowers.ChromeUtils.import(
+ "resource://gre/modules/AppConstants.jsm"
+);
+
+async function getAllWarningRates() {
+ return {
+ warnNoGetparameters:
+ await GleanTest.rtcrtpsenderSetparameters.warnNoGetparameters.testGetValue(),
+ warnLengthChanged:
+ await GleanTest.rtcrtpsenderSetparameters.warnLengthChanged.testGetValue(),
+ warnRidChanged:
+ await GleanTest.rtcrtpsenderSetparameters.warnRidChanged.testGetValue(),
+ warnNoTransactionid:
+ await GleanTest.rtcrtpsenderSetparameters.warnNoTransactionid.testGetValue(),
+ warnStaleTransactionid:
+ await GleanTest.rtcrtpsenderSetparameters.warnStaleTransactionid.testGetValue(),
+ };
+}
+
+const tests = [
+ async function checkRTCRtpSenderCount() {
+ const pc = new RTCPeerConnection();
+ const oldCount = await GleanTest.rtcrtpsender.count.testGetValue() ?? 0;
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
+ });
+ const countDiff = await GleanTest.rtcrtpsender.count.testGetValue() - oldCount;
+ is(countDiff, 1, "Glean should have recorded the creation of a single RTCRtpSender");
+ },
+
+ async function checkRTCRtpSenderSetParametersCompatCount() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", true]);
+ const pc = new RTCPeerConnection();
+ const oldCount = await GleanTest.rtcrtpsender.countSetparametersCompat.testGetValue() ?? 0;
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
+ });
+ const countDiff = await GleanTest.rtcrtpsender.countSetparametersCompat.testGetValue() - oldCount;
+ is(countDiff, 1, "Glean should have recorded the creation of a single RTCRtpSender that uses the setParameters compat mode");
+ },
+
+ async function checkSendEncodings() {
+ const pc = new RTCPeerConnection();
+ const oldRate = await GleanTest.rtcrtpsender.usedSendencodings.testGetValue();
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
+ });
+ const newRate = await GleanTest.rtcrtpsender.usedSendencodings.testGetValue();
+ is(newRate.denominator, oldRate.denominator + 1, "Glean should have recorded the creation of a single RTCRtpSender");
+ is(newRate.numerator, oldRate.numerator + 1, "Glean should have recorded the use of sendEncodings");
+ },
+
+ async function checkAddTransceiverNoSendEncodings() {
+ const pc = new RTCPeerConnection();
+ const oldRate = await GleanTest.rtcrtpsender.usedSendencodings.testGetValue();
+ const {sender} = pc.addTransceiver('video');
+ const newRate = await GleanTest.rtcrtpsender.usedSendencodings.testGetValue();
+ is(newRate.denominator, oldRate.denominator + 1, "Glean should have recorded the creation of a single RTCRtpSender");
+ is(newRate.numerator, oldRate.numerator, "Glean should not have recorded a use of sendEncodings");
+ },
+
+ async function checkAddTrack() {
+ const pc = new RTCPeerConnection();
+ const oldRate = await GleanTest.rtcrtpsender.usedSendencodings.testGetValue();
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ const sender = pc.addTrack(stream.getTracks()[0]);
+ const newRate = await GleanTest.rtcrtpsender.usedSendencodings.testGetValue();
+ is(newRate.denominator, oldRate.denominator + 1, "Glean should have recorded the creation of a single RTCRtpSender");
+ is(newRate.numerator, oldRate.numerator, "Glean should not have recorded a use of sendEncodings");
+ },
+
+ async function checkGoodSetParametersCompatMode() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", true]);
+ const pc = new RTCPeerConnection();
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
+ });
+ const oldWarningRates = await getAllWarningRates();
+ await sender.setParameters(sender.getParameters());
+ const newWarningRates = await getAllWarningRates();
+ isDeeply(oldWarningRates, newWarningRates);
+ },
+
+ async function checkBadSetParametersNoGetParametersWarning() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", true]);
+ const pc = new RTCPeerConnection();
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
+ });
+
+ let oldRate = await GleanTest.rtcrtpsenderSetparameters.warnNoGetparameters.testGetValue();
+ let oldBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameNoGetparameters["example.com"].testGetValue() || 0;
+
+ await sender.setParameters({encodings: [{rid: "0"},{rid: "1"},{rid: "2"}]});
+
+ let newRate = await GleanTest.rtcrtpsenderSetparameters.warnNoGetparameters.testGetValue();
+ let newBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameNoGetparameters["example.com"].testGetValue() || 0;
+
+ is(newRate.numerator, oldRate.numerator + 1, "Glean should have recorded a warning in setParameters due to lack of a getParameters call");
+
+ if (AppConstants.EARLY_BETA_OR_EARLIER) {
+ is(newBlameCount, oldBlameCount + 1, "Glean should have recorded that example.com encountered this warning");
+ } else {
+ is(newBlameCount, oldBlameCount, "Glean should not have recorded that example.com encountered this warning, because we're running this test on a stable channel");
+ }
+
+ oldRate = newRate;
+ oldBlameCount = newBlameCount;
+
+ // Glean should only record the warning once per sender!
+ await sender.setParameters({encodings: [{rid: "0"},{rid: "1"},{rid: "2"}]});
+
+ newRate = await GleanTest.rtcrtpsenderSetparameters.warnNoGetparameters.testGetValue();
+ newBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameNoGetparameters["example.com"].testGetValue() || 0;
+
+ is(newRate.numerator, oldRate.numerator, "Glean should not have recorded another warning in setParameters due to lack of a getParameters call");
+ is(newBlameCount, oldBlameCount, "Glean should not have recorded that example.com encountered this warning a second time, since this is the same sender");
+ },
+
+ async function checkBadSetParametersLengthChangedWarning() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", true]);
+ const pc = new RTCPeerConnection();
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
+ });
+
+ let oldRate = await GleanTest.rtcrtpsenderSetparameters.warnLengthChanged.testGetValue();
+ let oldBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameLengthChanged["example.com"].testGetValue() || 0;
+
+ let params = sender.getParameters();
+ params.encodings.pop();
+ await sender.setParameters(params);
+
+ let newRate = await GleanTest.rtcrtpsenderSetparameters.warnLengthChanged.testGetValue();
+ let newBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameLengthChanged["example.com"].testGetValue() || 0;
+
+ is(newRate.numerator, oldRate.numerator + 1, "Glean should have recorded a warning due to a length change in encodings");
+
+ if (AppConstants.EARLY_BETA_OR_EARLIER) {
+ is(newBlameCount, oldBlameCount + 1, "Glean should have recorded that example.com encountered this warning");
+ } else {
+ is(newBlameCount, oldBlameCount, "Glean should not have recorded that example.com encountered this warning, because we're running this test on a stable channel");
+ }
+
+ oldRate = newRate;
+ oldBlameCount = newBlameCount;
+
+ // Glean should only record the warning once per sender!
+ params = sender.getParameters();
+ params.encodings.pop();
+ await sender.setParameters(params);
+
+ newRate = await GleanTest.rtcrtpsenderSetparameters.warnLengthChanged.testGetValue();
+ newBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameLengthChanged["example.com"].testGetValue() || 0;
+
+ is(newRate.numerator, oldRate.numerator, "Glean should not have recorded another warning due to a length change in encodings");
+ is(newBlameCount, oldBlameCount, "Glean should not have recorded that example.com encountered this warning a second time, since this is the same sender");
+ },
+
+ async function checkBadSetParametersRidChangedWarning() {
+ // This pref does not let rid change errors slide anymore
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", true]);
+ const pc = new RTCPeerConnection();
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
+ });
+
+ let oldRate = await GleanTest.rtcrtpsenderSetparameters.failRidChanged.testGetValue();
+ let oldWarnRate = await GleanTest.rtcrtpsenderSetparameters.warnRidChanged.testGetValue();
+
+ let params = sender.getParameters();
+ params.encodings[1].rid = "foo";
+ try {
+ await sender.setParameters(params);
+ } catch (e) {
+ }
+ let newRate = await GleanTest.rtcrtpsenderSetparameters.failRidChanged.testGetValue();
+ let newWarnRate = await GleanTest.rtcrtpsenderSetparameters.warnRidChanged.testGetValue();
+ is(newRate.numerator, oldRate.numerator + 1, "Glean should have recorded an error due to a rid change in encodings");
+ is(newWarnRate.numerator, oldWarnRate.numerator, "Glean should not have recorded a warning due to a rid change in encodings");
+
+ // Glean should only record the error once per sender!
+ params = sender.getParameters();
+ params.encodings[1].rid = "bar";
+ oldRate = newRate;
+ try {
+ await sender.setParameters(params);
+ } catch (e) {
+ }
+ newRate = await GleanTest.rtcrtpsenderSetparameters.failRidChanged.testGetValue();
+ newWarnRate = await GleanTest.rtcrtpsenderSetparameters.warnRidChanged.testGetValue();
+ is(newRate.numerator, oldRate.numerator, "Glean should not have recorded another error due to a rid change in encodings");
+ is(newWarnRate.numerator, oldWarnRate.numerator, "Glean should not have recorded a warning due to a rid change in encodings");
+ },
+
+ async function checkBadSetParametersNoTransactionIdWarning() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", true]);
+ const pc = new RTCPeerConnection();
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
+ });
+
+ let oldRate = await GleanTest.rtcrtpsenderSetparameters.warnNoTransactionid.testGetValue();
+ let oldBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameNoTransactionid["example.com"].testGetValue() || 0;
+
+ await sender.setParameters({encodings: [{rid: "0"},{rid: "1"},{rid: "2"}]});
+
+ let newRate = await GleanTest.rtcrtpsenderSetparameters.warnNoTransactionid.testGetValue();
+ let newBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameNoTransactionid["example.com"].testGetValue() || 0;
+
+ is(newRate.numerator, oldRate.numerator + 1, "Glean should have recorded a warning due to missing transactionId in setParameters");
+
+ if (AppConstants.EARLY_BETA_OR_EARLIER) {
+ is(newBlameCount, oldBlameCount + 1, "Glean should have recorded that example.com encountered this warning");
+ } else {
+ is(newBlameCount, oldBlameCount, "Glean should not have recorded that example.com encountered this warning, because we're running this test on a stable channel");
+ }
+
+ oldRate = newRate;
+ oldBlameCount = newBlameCount;
+
+ // Glean should only record the warning once per sender!
+ await sender.setParameters({encodings: [{rid: "0"},{rid: "1"},{rid: "2"}]});
+
+ newRate = await GleanTest.rtcrtpsenderSetparameters.warnNoTransactionid.testGetValue();
+ newBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameNoTransactionid["example.com"].testGetValue() || 0;
+
+ is(newRate.numerator, oldRate.numerator, "Glean should not have recorded another warning due to missing transactionId in setParameters");
+ is(newBlameCount, oldBlameCount, "Glean should not have recorded that example.com encountered this warning a second time, since this is the same sender");
+ },
+
+ async function checkBadSetParametersStaleTransactionIdWarning() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", true]);
+ const pc = new RTCPeerConnection();
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
+ });
+
+ let oldRate = await GleanTest.rtcrtpsenderSetparameters.warnStaleTransactionid.testGetValue();
+ let oldBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameStaleTransactionid["example.com"].testGetValue() || 0;
+
+ let params = sender.getParameters();
+ // Cause transactionId to be stale
+ await pc.createOffer();
+ // ...but make sure there is a recent getParameters call
+ sender.getParameters();
+ await sender.setParameters(params);
+
+ let newRate = await GleanTest.rtcrtpsenderSetparameters.warnStaleTransactionid.testGetValue();
+ let newBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameStaleTransactionid["example.com"].testGetValue() || 0;
+
+ is(newRate.numerator, oldRate.numerator + 1, "Glean should have recorded a warning due to stale transactionId in setParameters");
+
+ if (AppConstants.EARLY_BETA_OR_EARLIER) {
+ is(newBlameCount, oldBlameCount + 1, "Glean should have recorded that example.com encountered this warning");
+ } else {
+ is(newBlameCount, oldBlameCount, "Glean should not have recorded that example.com encountered this warning, because we're running this test on a stable channel");
+ }
+
+ oldRate = newRate;
+ oldBlameCount = newBlameCount;
+
+ // Glean should only record the warning once per sender!
+ params = sender.getParameters();
+ // Cause transactionId to be stale
+ await pc.createOffer();
+ // ...but make sure there is a recent getParameters call
+ sender.getParameters();
+ await sender.setParameters(params);
+
+ newRate = await GleanTest.rtcrtpsenderSetparameters.warnStaleTransactionid.testGetValue();
+ newBlameCount = await GleanTest.rtcrtpsenderSetparameters.blameStaleTransactionid["example.com"].testGetValue() || 0;
+
+ is(newRate.numerator, oldRate.numerator, "Glean should not have recorded another warning due to stale transactionId in setParameters");
+ is(newBlameCount, oldBlameCount, "Glean should not have recorded that example.com encountered this warning a second time, since this is the same sender");
+ },
+
+ async function checkBadSetParametersLengthChangedError() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", false]);
+ const pc = new RTCPeerConnection();
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
+ });
+ let oldRate = await GleanTest.rtcrtpsenderSetparameters.failLengthChanged.testGetValue();
+ let params = sender.getParameters();
+ params.encodings.pop();
+ try {
+ await sender.setParameters(params);
+ } catch(e) {
+ }
+ let newRate = await GleanTest.rtcrtpsenderSetparameters.failLengthChanged.testGetValue();
+ is(newRate.numerator, oldRate.numerator + 1, "Glean should have recorded an error due to a length change in encodings");
+
+ // Glean should only record the error once per sender!
+ params = sender.getParameters();
+ params.encodings.pop();
+ oldRate = newRate;
+ try {
+ await sender.setParameters(params);
+ } catch (e) {
+ }
+ newRate = await GleanTest.rtcrtpsenderSetparameters.failLengthChanged.testGetValue();
+ is(newRate.numerator, oldRate.numerator, "Glean should not have recorded another error due to a length change in encodings");
+ },
+
+ async function checkBadSetParametersRidChangedError() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", false]);
+ const pc = new RTCPeerConnection();
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
+ });
+ let oldRate = await GleanTest.rtcrtpsenderSetparameters.failRidChanged.testGetValue();
+ let params = sender.getParameters();
+ params.encodings[1].rid = "foo";
+ try {
+ await sender.setParameters(params);
+ } catch (e) {
+ }
+ let newRate = await GleanTest.rtcrtpsenderSetparameters.failRidChanged.testGetValue();
+ is(newRate.numerator, oldRate.numerator + 1, "Glean should have recorded an error due to a rid change in encodings");
+
+ // Glean should only record the error once per sender!
+ params = sender.getParameters();
+ params.encodings[1].rid = "bar";
+ oldRate = newRate;
+ try {
+ await sender.setParameters(params);
+ } catch (e) {
+ }
+ newRate = await GleanTest.rtcrtpsenderSetparameters.failRidChanged.testGetValue();
+ is(newRate.numerator, oldRate.numerator, "Glean should not have recorded another error due to a rid change in encodings");
+ },
+
+ async function checkBadSetParametersNoGetParametersError() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", false]);
+ const pc = new RTCPeerConnection();
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
+ });
+ let oldRate = await GleanTest.rtcrtpsenderSetparameters.failNoGetparameters.testGetValue();
+ try {
+ await sender.setParameters({encodings: [{rid: "0"},{rid: "1"},{rid: "2"}]});
+ } catch (e) {
+ }
+ let newRate = await GleanTest.rtcrtpsenderSetparameters.failNoGetparameters.testGetValue();
+ is(newRate.numerator, oldRate.numerator + 1, "Glean should have recorded an error in setParameters due to lack of a getParameters call");
+
+ // Glean should only record the error once per sender!
+ oldRate = newRate;
+ try {
+ await sender.setParameters({encodings: [{rid: "0"},{rid: "1"},{rid: "2"}]});
+ } catch (e) {
+ }
+ newRate = await GleanTest.rtcrtpsenderSetparameters.failNoGetparameters.testGetValue();
+ is(newRate.numerator, oldRate.numerator, "Glean should not have recorded another error in setParameters due to lack of a getParameters call");
+ },
+
+ async function checkBadSetParametersStaleTransactionIdError() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", false]);
+ const pc = new RTCPeerConnection();
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
+ });
+ let oldRate = await GleanTest.rtcrtpsenderSetparameters.failStaleTransactionid.testGetValue();
+ let params = sender.getParameters();
+ // Cause transactionId to be stale
+ await pc.createOffer();
+ // ...but make sure there is a recent getParameters call
+ sender.getParameters();
+ try {
+ await sender.setParameters(params);
+ } catch (e) {
+ }
+ let newRate = await GleanTest.rtcrtpsenderSetparameters.failStaleTransactionid.testGetValue();
+ is(newRate.numerator, oldRate.numerator + 1, "Glean should have recorded an error due to stale transactionId in setParameters");
+
+ // Glean should only record the error once per sender!
+ oldRate = newRate;
+ params = sender.getParameters();
+ // Cause transactionId to be stale
+ await pc.createOffer();
+ // ...but make sure there is a recent getParameters call
+ sender.getParameters();
+ try {
+ await sender.setParameters(params);
+ } catch (e) {
+ }
+ newRate = await GleanTest.rtcrtpsenderSetparameters.failStaleTransactionid.testGetValue();
+ is(newRate.numerator, oldRate.numerator, "Glean should not have recorded another error due to stale transactionId in setParameters");
+ },
+
+ async function checkBadSetParametersNoEncodingsError() {
+ // If we do not allow the old setParameters, this will fail the length check
+ // instead.
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", true]);
+ const pc = new RTCPeerConnection();
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
+ });
+ let oldRate = await GleanTest.rtcrtpsenderSetparameters.failNoEncodings.testGetValue();
+ let params = sender.getParameters();
+ params.encodings = [];
+ try {
+ await sender.setParameters(params);
+ } catch (e) {
+ }
+ let newRate = await GleanTest.rtcrtpsenderSetparameters.failNoEncodings.testGetValue();
+ is(newRate.numerator, oldRate.numerator, "Glean should not have recorded an error due to empty encodings in setParameters");
+
+ // Glean should only record the error once per sender!
+ oldRate = newRate;
+ params = sender.getParameters();
+ params.encodings = [];
+ try {
+ await sender.setParameters(params);
+ } catch (e) {
+ }
+ newRate = await GleanTest.rtcrtpsenderSetparameters.failNoEncodings.testGetValue();
+ is(newRate.numerator, oldRate.numerator, "Glean should not have recorded an error due empty encodings in setParameters");
+ },
+
+ async function checkBadSetParametersOtherError() {
+ const pc = new RTCPeerConnection();
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
+ });
+ let oldRate = await GleanTest.rtcrtpsenderSetparameters.failOther.testGetValue();
+ let params = sender.getParameters();
+ params.encodings[0].scaleResolutionDownBy = 0.5;
+ try {
+ await sender.setParameters(params);
+ } catch (e) {
+ }
+ let newRate = await GleanTest.rtcrtpsenderSetparameters.failOther.testGetValue();
+ is(newRate.numerator, oldRate.numerator + 1, "Glean should have recorded an error due to some other failure");
+
+ // Glean should only record the error once per sender!
+ oldRate = newRate;
+ params = sender.getParameters();
+ params.encodings[0].scaleResolutionDownBy = 0.5;
+ try {
+ await sender.setParameters(params);
+ } catch (e) {
+ }
+ newRate = await GleanTest.rtcrtpsenderSetparameters.failOther.testGetValue();
+ is(newRate.numerator, oldRate.numerator, "Glean should not have recorded another error due to some other failure");
+ },
+
+];
+
+runNetworkTest(async () => {
+ for (const test of tests) {
+ info(`Running test: ${test.name}`);
+ await test();
+ info(`Done running test: ${test.name}`);
+ }
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_iceFailure.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_iceFailure.html
new file mode 100644
index 0000000000..1b82473997
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_iceFailure.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1087629",
+ title: "Wait for ICE failure"
+ });
+
+// Test iceFailure
+
+function PC_LOCAL_SETUP_NULL_ICE_HANDLER(test) {
+ test.pcLocal.setupIceCandidateHandler(test, function() {}, function () {});
+}
+function PC_REMOTE_SETUP_NULL_ICE_HANDLER(test) {
+ test.pcRemote.setupIceCandidateHandler(test, function() {}, function () {});
+}
+function PC_REMOTE_ADD_FAKE_ICE_CANDIDATE(test) {
+ var cand = {"candidate":"candidate:0 1 UDP 2130379007 192.0.2.1 12345 typ host","sdpMid":"","sdpMLineIndex":0};
+ test.pcRemote.storeOrAddIceCandidate(cand);
+ info(test.pcRemote + " Stored fake candidate: " + JSON.stringify(cand));
+}
+function PC_LOCAL_ADD_FAKE_ICE_CANDIDATE(test) {
+ var cand = {"candidate":"candidate:0 1 UDP 2130379007 192.0.2.2 56789 typ host","sdpMid":"","sdpMLineIndex":0};
+ test.pcLocal.storeOrAddIceCandidate(cand);
+ info(test.pcLocal + " Stored fake candidate: " + JSON.stringify(cand));
+}
+function PC_LOCAL_WAIT_FOR_ICE_FAILURE(test) {
+ return test.pcLocal.iceFailed.then(() => {
+ ok(true, this.pcLocal + " Ice Failure Reached.");
+ });
+}
+function PC_REMOTE_WAIT_FOR_ICE_FAILURE(test) {
+ return test.pcRemote.iceFailed.then(() => {
+ ok(true, this.pcRemote + " Ice Failure Reached.");
+ });
+}
+function PC_LOCAL_WAIT_FOR_ICE_FAILED(test) {
+ var resolveIceFailed;
+ test.pcLocal.iceFailed = new Promise(r => resolveIceFailed = r);
+ test.pcLocal.ice_connection_callbacks.checkIceStatus = () => {
+ if (test.pcLocal._pc.iceConnectionState === "failed") {
+ resolveIceFailed();
+ }
+ }
+}
+function PC_REMOTE_WAIT_FOR_ICE_FAILED(test) {
+ var resolveIceFailed;
+ test.pcRemote.iceFailed = new Promise(r => resolveIceFailed = r);
+ test.pcRemote.ice_connection_callbacks.checkIceStatus = () => {
+ if (test.pcRemote._pc.iceConnectionState === "failed") {
+ resolveIceFailed();
+ }
+ }
+}
+
+runNetworkTest(async () => {
+ await pushPrefs(
+ ['media.peerconnection.ice.stun_client_maximum_transmits', 3],
+ ['media.peerconnection.ice.trickle_grace_period', 3000],
+ );
+ var test = new PeerConnectionTest();
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.chain.replace("PC_LOCAL_SETUP_ICE_HANDLER", PC_LOCAL_SETUP_NULL_ICE_HANDLER);
+ test.chain.replace("PC_REMOTE_SETUP_ICE_HANDLER", PC_REMOTE_SETUP_NULL_ICE_HANDLER);
+ test.chain.insertAfter("PC_REMOTE_SETUP_NULL_ICE_HANDLER", PC_LOCAL_WAIT_FOR_ICE_FAILED);
+ test.chain.insertAfter("PC_LOCAL_WAIT_FOR_ICE_FAILED", PC_REMOTE_WAIT_FOR_ICE_FAILED);
+ test.chain.removeAfter("PC_LOCAL_SET_REMOTE_DESCRIPTION");
+ test.chain.append([
+ PC_REMOTE_ADD_FAKE_ICE_CANDIDATE,
+ PC_LOCAL_ADD_FAKE_ICE_CANDIDATE,
+ PC_LOCAL_WAIT_FOR_ICE_FAILURE,
+ PC_REMOTE_WAIT_FOR_ICE_FAILURE
+ ]);
+ await test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_insertDTMF.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_insertDTMF.html
new file mode 100644
index 0000000000..ca8b866a6d
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_insertDTMF.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1291715",
+ title: "Test insertDTMF on sender",
+ visible: true
+});
+
+function insertdtmftest(pc) {
+ ok(pc.getSenders().length, "have senders");
+ var sender = pc.getSenders()[0];
+ ok(sender.dtmf, "sender has dtmf object");
+
+ ok(sender.dtmf.toneBuffer === "", "sender should start with empty tonebuffer");
+
+ // These will trigger assertions on debug builds if we do not enforce the
+ // specified minimums and maximums for duration and interToneGap.
+ sender.dtmf.insertDTMF("A", 10);
+ sender.dtmf.insertDTMF("A", 10000);
+ sender.dtmf.insertDTMF("A", 70, 10);
+
+ var threw = false;
+ try {
+ sender.dtmf.insertDTMF("bad tones");
+ } catch (ex) {
+ threw = true;
+ is(ex.code, DOMException.INVALID_CHARACTER_ERR, "Expected InvalidCharacterError");
+ }
+ ok(threw, "Expected exception");
+
+ sender.dtmf.insertDTMF("A");
+ sender.dtmf.insertDTMF("B");
+ ok(!sender.dtmf.toneBuffer.includes("A"), "calling insertDTMF should replace current characters");
+
+ sender.dtmf.insertDTMF("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
+ ok(sender.dtmf.toneBuffer.includes("A"), "lowercase characters should be normalized");
+
+ pc.removeTrack(sender);
+ threw = false;
+ try {
+ sender.dtmf.insertDTMF("AAA");
+ } catch (ex) {
+ threw = true;
+ is(ex.code, DOMException.INVALID_STATE_ERR, "Expected InvalidStateError");
+ }
+ ok(threw, "Expected exception");
+}
+
+runNetworkTest(() => {
+ test = new PeerConnectionTest();
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.chain.removeAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW");
+
+ // Test sender dtmf.
+ test.chain.append([
+ function PC_LOCAL_INSERT_DTMF(test) {
+ // We want to call removeTrack
+ test.pcLocal.expectNegotiationNeeded();
+ return insertdtmftest(test.pcLocal._pc);
+ }
+ ]);
+
+ return pushPrefs(['media.peerconnection.dtmf.enabled', true])
+ .then(() => test.run());
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_localReofferRollback.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_localReofferRollback.html
new file mode 100644
index 0000000000..16406ece6e
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_localReofferRollback.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "952145",
+ title: "Rollback local reoffer"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ addRenegotiation(test.chain, [
+ function PC_LOCAL_ADD_SECOND_STREAM(test) {
+ test.setMediaConstraints([{audio: true}, {audio: true}],
+ [{audio: true}]);
+ return test.pcLocal.getAllUserMediaAndAddStreams([{audio: true}]);
+ },
+
+ function PC_LOCAL_CREATE_AND_SET_OFFER(test) {
+ return test.createOffer(test.pcLocal).then(offer => {
+ return test.setLocalDescription(test.pcLocal, offer, HAVE_LOCAL_OFFER);
+ });
+ },
+
+ function PC_LOCAL_ROLLBACK(test) {
+ // the negotiationNeeded slot should have been true both before and
+ // after this SLD, so the event should fire again.
+ test.pcLocal.expectNegotiationNeeded();
+ return test.setLocalDescription(test.pcLocal,
+ { type: "rollback", sdp: "" },
+ STABLE);
+ },
+ ]);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_localRollback.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_localRollback.html
new file mode 100644
index 0000000000..5bdc8cc029
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_localRollback.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "952145",
+ title: "Rollback local offer"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.chain.insertBefore('PC_LOCAL_CREATE_OFFER', [
+ function PC_REMOTE_CREATE_AND_SET_OFFER(test) {
+ return test.createOffer(test.pcRemote).then(offer => {
+ return test.setLocalDescription(test.pcRemote, offer, HAVE_LOCAL_OFFER);
+ });
+ },
+
+ function PC_REMOTE_ROLLBACK(test) {
+ // the negotiationNeeded slot should have been true both before and
+ // after this SLD, so the event should fire again.
+ test.pcRemote.expectNegotiationNeeded();
+ return test.setLocalDescription(test.pcRemote,
+ { type: "rollback", sdp: "" },
+ STABLE);
+ },
+
+ // Rolling back should shut down gathering
+ function PC_REMOTE_WAIT_FOR_END_OF_TRICKLE(test) {
+ return test.pcRemote.endOfTrickleIce;
+ },
+
+ function PC_REMOTE_SETUP_ICE_HANDLER(test) {
+ test.pcRemote.setupIceCandidateHandler(test);
+ },
+ ]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_maxFsConstraint.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_maxFsConstraint.html
new file mode 100644
index 0000000000..a2f2555020
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_maxFsConstraint.html
@@ -0,0 +1,112 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1393687",
+ title: "Enforce max-fs constraint on a PeerConnection",
+ visible: true
+ });
+
+ var mustRejectWith = (msg, reason, f) =>
+ f().then(() => ok(false, msg),
+ e => is(e.name, reason, msg));
+
+ var removeAllButCodec = (d, codec) =>
+ (d.sdp = d.sdp.replace(/m=video (\w) UDP\/TLS\/RTP\/SAVPF \w.*\r\n/,
+ "m=video $1 UDP/TLS/RTP/SAVPF " + codec + "\r\n"), d);
+
+ var mungeSDP = (d, forceH264) => {
+ if (forceH264) {
+ removeAllButCodec(d, 126);
+ d.sdp = d.sdp.replace(/a=fmtp:126 (.*);packetization-mode=1/, "a=fmtp:126 $1;packetization-mode=1;max-fs=100");
+ } else {
+ d.sdp = d.sdp.replace(/max-fs=\d+/, "max-fs=100");
+ }
+ return d;
+ };
+
+ const checkForH264Support = async () => {
+ const pc = new RTCPeerConnection();
+ const offer = await pc.createOffer({offerToReceiveVideo: true});
+ return offer.sdp.match(/a=rtpmap:[1-9][0-9]* H264/);
+ };
+
+ let resolutionAlignment = 1;
+
+ function testScale(codec) {
+ var v1 = createMediaElement('video', 'v1');
+ var v2 = createMediaElement('video', 'v2');
+
+ var pc1 = new RTCPeerConnection();
+ var pc2 = new RTCPeerConnection();
+
+ var add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ pc1.onicecandidate = e => add(pc2, e.candidate, generateErrorCallback());
+ pc2.onicecandidate = e => add(pc1, e.candidate, generateErrorCallback());
+
+ info("testing max-fs with" + codec);
+
+ pc1.onnegotiationneeded = e =>
+ pc1.createOffer()
+ .then(d => pc1.setLocalDescription(mungeSDP(d, codec == "H264")))
+ .then(() => pc2.setRemoteDescription(pc1.localDescription))
+ .then(() => pc2.createAnswer()).then(d => pc2.setLocalDescription(mungeSDP(d, codec =="H264")))
+ .then(() => pc1.setRemoteDescription(pc2.localDescription))
+ .catch(generateErrorCallback());
+
+ pc2.ontrack = e => {
+ v2.srcObject = e.streams[0];
+ };
+
+ var stream;
+
+ return navigator.mediaDevices.getUserMedia({ video: true })
+ .then(s => {
+ stream = s;
+ v1.srcObject = stream;
+ let track = stream.getVideoTracks()[0];
+ let sender = pc1.addTrack(track, stream);
+ is(v2.currentTime, 0, "v2.currentTime is zero at outset");
+ })
+ .then(() => wait(5000))
+ .then(() => {
+ if (v2.videoWidth == 0 && v2.videoHeight == 0) {
+ info("Skipping test, insufficient time for video to start.");
+ } else {
+ const expectedWidth = 184 - 184 % resolutionAlignment;
+ const expectedHeight = 138 - 138 % resolutionAlignment;
+ is(v2.videoWidth, expectedWidth,
+ `sink width should be ${expectedWidth} for ${codec}`);
+ is(v2.videoHeight, expectedHeight,
+ `sink height should be ${expectedHeight} for ${codec}`);
+ }})
+ .then(() => {
+ stream.getTracks().forEach(track => track.stop());
+ v1.srcObject = v2.srcObject = null;
+ }).catch(generateErrorCallback());
+ }
+
+ runNetworkTest(async () => {
+ await pushPrefs(['media.peerconnection.video.lock_scaling', true]);
+ if (await checkForH264Support()) {
+ if (navigator.userAgent.includes("Android")) {
+ // Android only has a hw encoder for h264
+ resolutionAlignment = 16;
+ }
+ await matchPlatformH264CodecPrefs();
+ await testScale("H264");
+ }
+
+ // Disable h264 hardware support, to ensure it is not prioritized over VP8
+ await pushPrefs(["media.webrtc.hw.h264.enabled", false]);
+ await testScale("VP8");
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_multiple_captureStream_canvas_2d.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_multiple_captureStream_canvas_2d.html
new file mode 100644
index 0000000000..9ad25e7852
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_multiple_captureStream_canvas_2d.html
@@ -0,0 +1,115 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1166832",
+ title: "Canvas(2D)::Multiple CaptureStream as video-only input to peerconnection",
+ visible: true
+});
+
+/**
+ * Test to verify using multiple capture streams concurrently.
+ */
+runNetworkTest(async () => {
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false]);
+ await pushPrefs(["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ var test = new PeerConnectionTest();
+ var h = new CaptureStreamTestHelper2D(50, 50);
+
+ var vremote1;
+ var stream1;
+ var canvas1 = h.createAndAppendElement('canvas', 'source_canvas1');
+
+ var vremote2;
+ var stream2;
+ var canvas2 = h.createAndAppendElement('canvas', 'source_canvas2');
+
+ const threshold = 128;
+
+ test.setMediaConstraints([{video: true}, {video: true}], []);
+ test.chain.replace("PC_LOCAL_GUM", [
+ function PC_LOCAL_CANVAS_CAPTURESTREAM(test) {
+ h.drawColor(canvas1, h.green);
+ h.drawColor(canvas2, h.blue);
+ stream1 = canvas1.captureStream(0); // fps = 0 to capture single frame
+ test.pcLocal.attachLocalStream(stream1);
+ stream2 = canvas2.captureStream(0); // fps = 0 to capture single frame
+ test.pcLocal.attachLocalStream(stream2);
+ var i = 0;
+ return setInterval(function() {
+ try {
+ info("draw " + i ? "green" : "red/blue");
+ h.drawColor(canvas1, i ? h.green : h.red);
+ h.drawColor(canvas2, i ? h.green : h.blue);
+ i = 1 - i;
+ stream1.requestFrame();
+ stream2.requestFrame();
+ } catch (e) {
+ // ignore; stream might have shut down, and we don't bother clearing
+ // the setInterval.
+ }
+ }, 500);
+ }
+ ]);
+
+ test.chain.append([
+ function CHECK_REMOTE_VIDEO() {
+ is(test.pcRemote.remoteMediaElements.length, 2, "pcRemote Should have 2 remote media elements");
+ vremote1 = test.pcRemote.remoteMediaElements[0];
+ vremote2 = test.pcRemote.remoteMediaElements[1];
+
+ // since we don't know which remote video is created first, we don't know
+ // which should be blue or red, but this will make sure that one is
+ // green and one is blue
+ return Promise.race([
+ Promise.all([
+ h.pixelMustBecome(vremote1, h.red, {
+ threshold,
+ infoString: "pcRemote's remote1 should become red",
+ }),
+ h.pixelMustBecome(vremote2, h.blue, {
+ threshold,
+ infoString: "pcRemote's remote2 should become blue",
+ }),
+ ]),
+ Promise.all([
+ h.pixelMustBecome(vremote2, h.red, {
+ threshold,
+ infoString: "pcRemote's remote2 should become red",
+ }),
+ h.pixelMustBecome(vremote1, h.blue, {
+ threshold,
+ infoString: "pcRemote's remote1 should become blue",
+ }),
+ ])
+ ]);
+ },
+ function WAIT_FOR_REMOTE_BOTH_GREEN() {
+ return Promise.all([
+ h.pixelMustBecome(vremote1, h.green, {
+ threshold,
+ infoString: "pcRemote's remote1 should become green",
+ }),
+ h.pixelMustBecome(vremote2, h.green, {
+ threshold,
+ infoString: "pcRemote's remote2 should become green",
+ }),
+ ])
+ },
+ ]);
+ await test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleAnswer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleAnswer.html
new file mode 100644
index 0000000000..7e3fd78430
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleAnswer.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="nonTrickleIce.js"></script>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1060102",
+ title: "Basic audio only SDP answer without trickle ICE"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ makeAnswererNonTrickle(test.chain);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleOffer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleOffer.html
new file mode 100644
index 0000000000..12b2a95596
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleOffer.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="nonTrickleIce.js"></script>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1060102",
+ title: "Basic audio only SDP offer without trickle ICE"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ makeOffererNonTrickle(test.chain);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleOfferAnswer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleOfferAnswer.html
new file mode 100644
index 0000000000..554750e975
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_noTrickleOfferAnswer.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="nonTrickleIce.js"></script>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1060102",
+ title: "Basic audio only SDP offer and answer without trickle ICE"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ makeOffererNonTrickle(test.chain);
+ makeAnswererNonTrickle(test.chain);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_nonDefaultRate.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_nonDefaultRate.html
new file mode 100644
index 0000000000..ad9414cef2
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_nonDefaultRate.html
@@ -0,0 +1,200 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({ title: "PeerConnection feed to a graph with non default rate", bug: "1387454" });
+ /**
+ * Run a test to verify that when we use the streem with nonDefault rate to/from a PC
+ * the connection fails. (PC is always on default rate).
+ */
+
+ let pc1;
+ let pc2;
+
+ const offerOptions = {
+ offerToReceiveAudio: 1,
+ };
+
+ function getName(pc) {
+ return (pc === pc1) ? 'pc1' : 'pc2';
+ }
+
+ function getOtherPc(pc) {
+ return (pc === pc1) ? pc2 : pc1;
+ }
+
+ function onAddIceCandidateSuccess(pc) {
+ ok(true, getName(pc) + ' addIceCandidate success');
+ }
+
+ function onAddIceCandidateError(pc, error) {
+ ok(false, getName(pc) + ' failed to add ICE Candidate: ' + error.toString());
+ }
+
+ function onIceCandidate(pc, event, done) {
+ if (!event.candidate) {
+ ok(pc.iceGatheringState === 'complete', getName(pc) + " ICE gathering state has reached complete");
+ done();
+ return;
+ }
+ getOtherPc(pc).addIceCandidate(event.candidate)
+ .then(() => {
+ onAddIceCandidateSuccess(pc);
+ },
+ (err) => {
+ onAddIceCandidateError(pc, err);
+ });
+ info(getName(pc) + ' ICE candidate: ' + event.candidate.candidate);
+ }
+
+ function onIceStateChange(pc, event) {
+ if (pc) {
+ info(getName(pc) + ' ICE state: ' + pc.iceConnectionState);
+ info('ICE state change event: ', event);
+ }
+ }
+
+ function onCreateOfferSuccess(desc) {
+ info('Offer from pc1\n' + desc.sdp);
+ info('pc1 setLocalDescription start');
+
+ pc1.setLocalDescription(desc)
+ .then(() => {
+ onSetLocalSuccess(pc1);
+ },
+ onSetSessionDescriptionError);
+
+ info('pc2 setRemoteDescription start');
+ pc2.setRemoteDescription(desc).then(() => {
+ onSetRemoteSuccess(pc2);
+ },
+ onSetSessionDescriptionError);
+
+ info('pc2 createAnswer start');
+
+ // Since the 'remote' side has no media stream we need
+ // to pass in the right constraints in order for it to
+ // accept the incoming offer of audio and video.
+ pc2.createAnswer()
+ .then(onCreateAnswerSuccess, onCreateSessionDescriptionError);
+ }
+
+ function onSetSessionDescriptionError(error) {
+ ok(false, 'Failed to set session description: ' + error.toString());
+ }
+
+ function onSetLocalSuccess(pc) {
+ ok(true, getName(pc) + ' setLocalDescription complete');
+ }
+
+ function onCreateSessionDescriptionError(error) {
+ ok(false, 'Failed to create session description: ' + error.toString());
+ }
+
+ function onSetRemoteSuccess(pc) {
+ ok(true, getName(pc) + ' setRemoteDescription complete');
+ }
+
+ function onCreateAnswerSuccess(desc) {
+ info('Answer from pc2:\n' + desc.sdp);
+ info('pc2 setLocalDescription start');
+ pc2.setLocalDescription(desc).then(() => {
+ onSetLocalSuccess(pc2);
+ },
+ onSetSessionDescriptionError);
+ info('pc1 setRemoteDescription start');
+ pc1.setRemoteDescription(desc).then(() => {
+ onSetRemoteSuccess(pc1);
+ },
+ onSetSessionDescriptionError);
+ }
+
+ async function getRemoteStream(localStream) {
+ info("got local stream")
+ const audioTracks = localStream.getAudioTracks();
+
+ const servers = null;
+
+ pc1 = new RTCPeerConnection(servers);
+ info('Created local peer connection object pc1');
+ const iceComplete1 = new Promise((resolve, reject) => {
+ pc1.onicecandidate = (e) => {
+ onIceCandidate(pc1, e, resolve);
+ };
+ });
+
+ pc2 = new RTCPeerConnection(servers);
+ info('Created remote peer connection object pc2');
+ const iceComplete2 = new Promise((resolve, reject) => {
+ pc2.onicecandidate = (e) => {
+ onIceCandidate(pc2, e, resolve);
+ };
+ });
+
+ pc1.oniceconnectionstatechange = (e) => {
+ onIceStateChange(pc1, e);
+ };
+ pc2.oniceconnectionstatechange = (e) => {
+ onIceStateChange(pc2, e);
+ };
+
+ const remoteStreamPromise = new Promise((resolve, reject) => {
+ pc2.ontrack = (e) => {
+ info('pc2 received remote stream ' + e.streams[0]);
+ resolve(e.streams[0]);
+ };
+ });
+
+ localStream.getTracks().forEach((track) => {
+ pc1.addTrack(track, localStream);
+ });
+ info('Added local stream to pc1');
+
+ info('pc1 createOffer start');
+ pc1.createOffer(offerOptions)
+ .then(onCreateOfferSuccess,onCreateSessionDescriptionError);
+
+ let promise_arr = await Promise.all([remoteStreamPromise, iceComplete1, iceComplete2]);
+ return promise_arr[0];
+ }
+
+ runTest(async () => {
+ // Local stream operates at non default rate (32000)
+ const nonDefaultRate = 32000;
+ const nonDefault_ctx = new AudioContext({sampleRate: nonDefaultRate});
+ oscillator = nonDefault_ctx.createOscillator();
+ const dest = nonDefault_ctx.createMediaStreamDestination();
+ oscillator.connect(dest);
+ oscillator.start();
+
+ // Wait for remote stream
+ const remoteStream = await getRemoteStream(dest.stream)
+ ok(true, 'Got remote stream ' + remoteStream);
+
+ // remoteStream now comes from PC so operates at default
+ // rates. Verify that by adding to a default context
+ const ac = new AudioContext;
+ const source_default_rate = ac.createMediaStreamSource(remoteStream);
+
+ // Now try to add the remoteStream on a non default context
+ mustThrowWith(
+ "Connect stream with graph of different sample rate",
+ "NotSupportedError", () => {
+ nonDefault_ctx.createMediaStreamSource(remoteStream);
+ }
+ );
+
+ // Close peer connections to make sure we don't get error:
+ // "logged result after SimpleTest.finish(): pc1 addIceCandidate success"
+ // See Bug 1626814.
+ pc1.close();
+ pc2.close();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveAudio.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveAudio.html
new file mode 100644
index 0000000000..1f936714f1
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveAudio.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "850275",
+ title: "Simple offer media constraint test with audio"
+ });
+
+ runNetworkTest(function() {
+ var test = new PeerConnectionTest();
+ test.setMediaConstraints([], [{audio: true}]);
+ test.setOfferOptions({ offerToReceiveAudio: true });
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveVideo.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveVideo.html
new file mode 100644
index 0000000000..c5afbb5c1f
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveVideo.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "850275",
+ title: "Simple offer media constraint test with video"
+ });
+
+ runNetworkTest(function() {
+ var test = new PeerConnectionTest();
+ test.setMediaConstraints([], [{video: true}]);
+ test.setOfferOptions({ offerToReceiveVideo: true });
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveVideoAudio.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveVideoAudio.html
new file mode 100644
index 0000000000..d7bc29c6d3
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_offerRequiresReceiveVideoAudio.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "850275",
+ title: "Simple offer media constraint test with video/audio"
+ });
+
+ runNetworkTest(function() {
+ var test = new PeerConnectionTest();
+ test.setMediaConstraints([], [{audio: true, video: true}]);
+ test.setOfferOptions({ offerToReceiveVideo: true, offerToReceiveAudio: true });
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_portRestrictions.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_portRestrictions.html
new file mode 100644
index 0000000000..7cd695ff54
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_portRestrictions.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1677046",
+ title: "RTCPeerConnection check restricted ports"
+ });
+
+var makePC = (config, expected_error) => {
+ var exception;
+ try {
+ new RTCPeerConnection(config).close();
+ } catch (e) {
+ exception = e;
+ }
+ is((exception? exception.name : "success"), expected_error || "success",
+ "RTCPeerConnection(" + JSON.stringify(config) + ")");
+};
+
+// This is a test of the iceServers parsing code + readable errors
+runNetworkTest(() => {
+ var exception = null;
+
+ // check various ports on the blocklist
+ makePC({ iceServers: [
+ { urls:"turn:[::1]:6666", username:"p", credential:"p" }] }, "NS_ERROR_UNEXPECTED");
+ makePC({ iceServers: [
+ { urls:"turns:localhost:6667?transport=udp", username:"p", credential:"p" }] },
+ "NS_ERROR_UNEXPECTED");
+ makePC({ iceServers: [
+ { urls:"stun:localhost:21", foo:"" }] }, "NS_ERROR_UNEXPECTED");
+ makePC({ iceServers: [
+ { urls:"stun:[::1]:22", foo:"" }] }, "NS_ERROR_UNEXPECTED");
+ makePC({ iceServers: [
+ { urls:"turn:localhost:5060", username:"p", credential:"p" }] },
+ "NS_ERROR_UNEXPECTED");
+
+ // check various ports on the good list for webrtc (or default port)
+ makePC({ iceServers: [
+ { urls:"turn:[::1]:53", username:"p", credential:"p" },
+ { urls:"turn:[::1]:5349", username:"p", credential:"p" },
+ { urls:"turn:[::1]:3478", username:"p", credential:"p" },
+ { urls:"turn:[::1]", username:"p", credential:"p" },
+ { urls:"turn:localhost:53?transport=udp", username:"p", credential:"p" },
+ { urls:"turn:localhost:3478?transport=udp", username:"p", credential:"p" },
+ { urls:"turn:localhost:53?transport=tcp", username:"p", credential:"p" },
+ { urls:"turn:localhost:3478?transport=tcp", username:"p", credential:"p" },
+ { urls:"turns:localhost:3478?transport=udp", username:"p", credential:"p" },
+ { urls:"stun:localhost", foo:"" }
+ ]});
+
+ // not in the known good ports and not on the generic block list
+ makePC({ iceServers: [{ urls:"turn:localhost:6664", username:"p", credential:"p" }] });
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_promiseSendOnly.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_promiseSendOnly.html
new file mode 100644
index 0000000000..a3fbb5753c
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_promiseSendOnly.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1091898",
+ title: "PeerConnection with promises (sendonly)",
+ visible: true
+ });
+
+ var pc1 = new RTCPeerConnection();
+ var pc2 = new RTCPeerConnection();
+
+ var add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ pc1.onicecandidate = e => add(pc2, e.candidate, generateErrorCallback());
+ pc2.onicecandidate = e => add(pc1, e.candidate, generateErrorCallback());
+
+ var v1, v2;
+ var delivered = new Promise(resolve => pc2.ontrack = e => {
+ // Test RTCTrackEvent here.
+ ok(e.streams.length, "has streams");
+ ok(e.streams[0].getTrackById(e.track.id), "has track");
+ ok(pc2.getReceivers().some(receiver => receiver == e.receiver), "has receiver");
+ if (e.streams[0].getTracks().length == 2) {
+ // Test RTCTrackEvent required args here.
+ mustThrowWith("RTCTrackEvent wo/required args",
+ "TypeError", () => new RTCTrackEvent("track", {}));
+ v2.srcObject = e.streams[0];
+ resolve();
+ }
+ });
+
+ runNetworkTest(function() {
+ v1 = createMediaElement('video', 'v1');
+ v2 = createMediaElement('video', 'v2');
+ var canPlayThrough = new Promise(resolve => v2.canplaythrough = e => resolve());
+
+ is(v2.currentTime, 0, "v2.currentTime is zero at outset");
+
+ return navigator.mediaDevices.getUserMedia({ video: true, audio: true })
+ .then(stream => (v1.srcObject = stream).getTracks().forEach(t => pc1.addTrack(t, stream)))
+ .then(() => pc1.createOffer({})) // check that createOffer accepts arg.
+ .then(offer => pc1.setLocalDescription(offer))
+ .then(() => pc2.setRemoteDescription(pc1.localDescription))
+ .then(() => pc2.createAnswer({})) // check that createAnswer accepts arg.
+ .then(answer => pc2.setLocalDescription(answer))
+ .then(() => pc1.setRemoteDescription(pc2.localDescription))
+ .then(() => delivered)
+// .then(() => canPlayThrough) // why doesn't this fire?
+ .then(() => waitUntil(() => v2.currentTime > 0))
+ .then(() => ok(v2.currentTime > 0, "v2.currentTime is moving (" + v2.currentTime + ")"))
+ .then(() => ok(true, "Connected."));
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_recordReceiveTrack.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_recordReceiveTrack.html
new file mode 100644
index 0000000000..d5cb91b048
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_recordReceiveTrack.html
@@ -0,0 +1,101 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<script src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script>
+createHTML({
+ bug: "1212237",
+ title: "Recording a fresh receive track should not throw",
+ visible: true,
+});
+
+/**
+ * Called when a fresh track is available, and tests that the track can be
+ * recorded until it ends without any thrown errors or fired error events.
+ */
+let generation = 0;
+async function testTrackAccessible(track) {
+ const id = ++generation;
+ info(`Testing accessibility for ${track.kind} track ${id}`);
+ const recorder = new MediaRecorder(new MediaStream([track]));
+ recorder.start();
+ let haveError = new Promise((_, rej) => recorder.onerror = e => rej(e.error));
+ await Promise.race([
+ new Promise(r => recorder.onstart = r),
+ haveError,
+ ]);
+ info(`Recording of ${track.kind} track ${id} started`);
+
+ const {data} = await Promise.race([
+ new Promise(r => recorder.ondataavailable = r),
+ haveError,
+ ]);
+ info(`Recording of ${track.kind} track ${id} finished at size ${data.size}`);
+
+ await Promise.race([
+ new Promise(r => recorder.onstop = r),
+ haveError,
+ ]);
+ info(`Recording of ${track.kind} track ${id} stopped`);
+
+ const element = createMediaElement(track.kind, `recording_${track.id}`);
+ const url = URL.createObjectURL(data);
+ try {
+ element.src = url;
+ element.preload = "metadata";
+ haveError = new Promise(
+ (_, rej) => element.onerror = e => rej(element.error));
+ await Promise.race([
+ new Promise(r => element.onloadeddata = r),
+ haveError,
+ ]);
+ info(`Playback of recording of ${track.kind} track ${id} loaded data`);
+
+ element.play();
+ await Promise.race([
+ new Promise(r => element.onended = r),
+ haveError,
+ ]);
+ info(`Playback of recording of ${track.kind} track ${id} ended`);
+ } finally {
+ URL.revokeObjectURL(data);
+ }
+}
+
+runNetworkTest(async options => {
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{video: true}], [{audio: true}]);
+ test.setOfferOptions({offerToReceiveAudio: true});
+ const freshVideoTrackIsAccessible = new Promise(
+ r => test.pcRemote._pc.addEventListener("track", r, {once: true})
+ ).then(({track}) => testTrackAccessible(track));
+ const freshAudioTrackIsAccessible = new Promise(
+ r => test.pcLocal._pc.addEventListener("track", r, {once: true})
+ ).then(({track}) => testTrackAccessible(track));
+ test.chain.append([
+ function PC_CLOSE_TO_END_TRACKS() {
+ return test.close();
+ },
+ async function FRESH_VIDEO_TRACK_IS_ACCESSIBLE() {
+ await freshVideoTrackIsAccessible;
+ ok(true, "A freshly received video track is accessible by MediaRecorder");
+ },
+ async function FRESH_AUDIO_TRACK_IS_ACCESSIBLE() {
+ await freshAudioTrackIsAccessible;
+ ok(true, "A freshly received audio track is accessible by MediaRecorder");
+ },
+ ]);
+ await test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_relayOnly.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_relayOnly.html
new file mode 100644
index 0000000000..3b07783c04
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_relayOnly.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1187775",
+ title: "peer connection ICE fails on relay-only without TURN"
+});
+
+function PC_LOCAL_NO_CANDIDATES(test) {
+ var isnt = can => is(can, null, "No candidates: " + JSON.stringify(can));
+ test.pcLocal._pc.addEventListener("icecandidate", e => isnt(e.candidate));
+}
+
+function PC_BOTH_WAIT_FOR_ICE_FAILED(test) {
+ var isFail = (f, reason, msg) =>
+ f().then(() => { throw new Error(msg + " must fail"); },
+ e => is(e.message, reason, msg + " must fail with: " + e.message));
+
+ return Promise.all([
+ isFail(() => test.pcLocal.waitForIceConnected(), "ICE failed", "Local ICE"),
+ isFail(() => test.pcRemote.waitForIceConnected(), "ICE failed", "Remote ICE")
+ ])
+ .then(() => ok(true, "ICE on both sides must fail."));
+}
+
+runNetworkTest(async options => {
+ await pushPrefs(
+ ['media.peerconnection.ice.stun_client_maximum_transmits', 3],
+ ['media.peerconnection.ice.trickle_grace_period', 5000]
+ );
+ options = options || {};
+ options.config_local = options.config_local || {};
+ const servers = options.config_local.iceServers || [];
+ // remove any turn servers
+ options.config_local.iceServers = servers.filter(server =>
+ server.urls.every(u => !u.toLowerCase().startsWith('turn')));
+
+ // Here's the setting we're testing. Comment out and this test should fail:
+ options.config_local.iceTransportPolicy = "relay";
+
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+ test.chain.remove("PC_LOCAL_SETUP_ICE_LOGGER"); // Needed to suppress failing
+ test.chain.remove("PC_REMOTE_SETUP_ICE_LOGGER"); // on ICE-failure.
+ test.chain.insertAfter("PC_LOCAL_SETUP_ICE_HANDLER", PC_LOCAL_NO_CANDIDATES);
+ test.chain.replace("PC_LOCAL_WAIT_FOR_ICE_CONNECTED", PC_BOTH_WAIT_FOR_ICE_FAILED);
+ test.chain.removeAfter("PC_BOTH_WAIT_FOR_ICE_FAILED");
+ await test.run();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_remoteReofferRollback.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_remoteReofferRollback.html
new file mode 100644
index 0000000000..80aa30beaa
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_remoteReofferRollback.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "952145",
+ title: "Rollback remote reoffer"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ addRenegotiation(test.chain,
+ [
+ function PC_LOCAL_ADD_SECOND_STREAM(test) {
+ test.setMediaConstraints([{audio: true}, {audio: true}],
+ [{audio: true}]);
+ return test.pcLocal.getAllUserMediaAndAddStreams([{audio: true}]);
+ },
+ ]
+ );
+ test.chain.replaceAfter('PC_REMOTE_SET_REMOTE_DESCRIPTION',
+ [
+ function PC_REMOTE_ROLLBACK(test) {
+ return test.setRemoteDescription(test.pcRemote, { type: "rollback" },
+ STABLE);
+ },
+
+ function PC_LOCAL_ROLLBACK(test) {
+ // We haven't negotiated the new stream yet.
+ test.pcLocal.expectNegotiationNeeded();
+ return test.setLocalDescription(
+ test.pcLocal,
+ new RTCSessionDescription({ type: "rollback", sdp: ""}),
+ STABLE);
+ },
+ ],
+ 1 // Second PC_REMOTE_SET_REMOTE_DESCRIPTION
+ );
+ test.chain.append(commandsPeerConnectionOfferAnswer);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_remoteRollback.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_remoteRollback.html
new file mode 100644
index 0000000000..827646b0de
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_remoteRollback.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "952145",
+ title: "Rollback remote offer"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.chain.removeAfter('PC_REMOTE_CHECK_CAN_TRICKLE_SYNC');
+ test.chain.append([
+ function PC_REMOTE_ROLLBACK(test) {
+ // We still haven't negotiated the tracks
+ test.pcRemote.expectNegotiationNeeded();
+ return test.setRemoteDescription(test.pcRemote, { type: "rollback" },
+ STABLE);
+ },
+
+ function PC_REMOTE_CHECK_CAN_TRICKLE_REVERT_SYNC(test) {
+ is(test.pcRemote._pc.canTrickleIceCandidates, null,
+ "Remote canTrickleIceCandidates is reverted to null");
+ },
+
+ function PC_LOCAL_ROLLBACK(test) {
+ // We still haven't negotiated the tracks
+ test.pcLocal.expectNegotiationNeeded();
+ return test.setLocalDescription(
+ test.pcLocal,
+ new RTCSessionDescription({ type: "rollback", sdp: ""}),
+ STABLE);
+ },
+
+ // Rolling back should shut down gathering
+ function PC_LOCAL_WAIT_FOR_END_OF_TRICKLE(test) {
+ return test.pcLocal.endOfTrickleIce;
+ },
+ ]);
+ test.chain.append(commandsPeerConnectionOfferAnswer);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_removeAudioTrack.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeAudioTrack.html
new file mode 100644
index 0000000000..e1e99b38c9
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeAudioTrack.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1017888",
+ title: "Renegotiation: remove audio track"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ let receivedTrack, analyser, freq;
+ addRenegotiation(test.chain,
+ [
+ function PC_REMOTE_SETUP_ANALYSER(test) {
+ is(test.pcRemote._pc.getReceivers().length, 1,
+ "pcRemote should have one receiver before renegotiation");
+
+ receivedTrack = test.pcRemote._pc.getReceivers()[0].track;
+ is(receivedTrack.readyState, "live",
+ "The received track should be live");
+
+ analyser = new AudioStreamAnalyser(
+ new AudioContext(), new MediaStream([receivedTrack]));
+ freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ);
+
+ return analyser.waitForAnalysisSuccess(arr => arr[freq] > 200);
+ },
+ function PC_LOCAL_REMOVE_AUDIO_TRACK(test) {
+ test.setOfferOptions({ offerToReceiveAudio: true });
+ return test.pcLocal.removeSender(0);
+ },
+ ],
+ [
+ function PC_REMOTE_CHECK_FLOW_STOPPED(test) {
+ // Simply removing a track is not enough to cause it to be
+ // signaled as ended. Spec may change though.
+ // TODO: One last check of the spec is in order
+ is(receivedTrack.readyState, "live",
+ "The received track should not have ended");
+
+ return analyser.waitForAnalysisSuccess(arr => arr[freq] < 50);
+ },
+ ]
+ );
+
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddAudioTrack.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddAudioTrack.html
new file mode 100644
index 0000000000..28b76e3b43
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddAudioTrack.html
@@ -0,0 +1,87 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1017888",
+ title: "Renegotiation: remove then add audio track"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ let originalTrack;
+ let haveMuteEvent = new Promise(() => {});
+ let haveUnmuteEvent = new Promise(() => {});
+ addRenegotiation(test.chain,
+ [
+ function PC_REMOTE_FIND_RECEIVER(test) {
+ is(test.pcRemote._pc.getReceivers().length, 1,
+ "pcRemote should have one receiver");
+ originalTrack = test.pcRemote._pc.getReceivers()[0].track;
+ },
+ function PC_LOCAL_REMOVE_AUDIO_TRACK(test) {
+ return test.pcLocal.removeSender(0);
+ },
+ function PC_LOCAL_ADD_AUDIO_TRACK(test) {
+ // The new track's pipeline will start with a packet count of
+ // 0, but the remote side will keep its old pipeline and packet
+ // count.
+ test.pcLocal.disableRtpCountChecking = true;
+ return test.pcLocal.getAllUserMediaAndAddStreams([{audio: true}]);
+ },
+ ],
+ [
+ function PC_REMOTE_WAIT_FOR_UNMUTE() {
+ return haveUnmuteEvent;
+ },
+ function PC_REMOTE_CHECK_ADDED_TRACK(test) {
+ is(test.pcRemote._pc.getTransceivers().length, 2,
+ "pcRemote should have two transceivers");
+ const track = test.pcRemote._pc.getTransceivers()[1].receiver.track;
+
+ const analyser = new AudioStreamAnalyser(
+ new AudioContext(), new MediaStream([track]));
+ const freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ);
+ return analyser.waitForAnalysisSuccess(arr => arr[freq] > 200);
+ },
+ function PC_REMOTE_WAIT_FOR_MUTE() {
+ return haveMuteEvent;
+ },
+ function PC_REMOTE_CHECK_REMOVED_TRACK(test) {
+ is(test.pcRemote._pc.getTransceivers().length, 2,
+ "pcRemote should have two transceivers");
+ const track = test.pcRemote._pc.getTransceivers()[0].receiver.track;
+
+ const analyser = new AudioStreamAnalyser(
+ new AudioContext(), new MediaStream([track]));
+ const freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ);
+ return analyser.waitForAnalysisSuccess(arr => arr[freq] < 50);
+ }
+ ]
+ );
+
+ // The first track should mute when the connection is closed.
+ test.chain.insertBefore("PC_REMOTE_SET_REMOTE_DESCRIPTION", [
+ function PC_REMOTE_SETUP_ONMUTE(test) {
+ haveMuteEvent = haveEvent(test.pcRemote._pc.getReceivers()[0].track, "mute");
+ }
+ ]);
+
+ // Second negotiation should cause the second track to unmute.
+ test.chain.insertAfter("PC_REMOTE_SET_REMOTE_DESCRIPTION", [
+ function PC_REMOTE_SETUP_ONUNMUTE(test) {
+ haveUnmuteEvent = haveEvent(test.pcRemote._pc.getReceivers()[1].track, "unmute");
+ }
+ ], false, 1);
+
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddAudioTrackNoBundle.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddAudioTrackNoBundle.html
new file mode 100644
index 0000000000..cff424e12c
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddAudioTrackNoBundle.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1017888",
+ title: "Renegotiation: remove then add audio track"
+ });
+
+ runNetworkTest(function (options) {
+ options = options || { };
+ options.bundle = false;
+ const test = new PeerConnectionTest(options);
+ let originalTrack;
+ addRenegotiation(test.chain,
+ [
+ function PC_REMOTE_FIND_RECEIVER(test) {
+ is(test.pcRemote._pc.getReceivers().length, 1,
+ "pcRemote should have one receiver");
+ originalTrack = test.pcRemote._pc.getReceivers()[0].track;
+ },
+ function PC_LOCAL_REMOVE_AUDIO_TRACK(test) {
+ // The new track's pipeline will start with a packet count of
+ // 0, but the remote side will keep its old pipeline and packet
+ // count.
+ test.pcLocal.disableRtpCountChecking = true;
+ return test.pcLocal.removeSender(0);
+ },
+ function PC_LOCAL_ADD_AUDIO_TRACK(test) {
+ return test.pcLocal.getAllUserMediaAndAddStreams([{audio: true}]);
+ },
+ function PC_LOCAL_EXPECT_ICE_CHECKING(test) {
+ test.pcLocal.expectIceChecking();
+ },
+ function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+ test.pcRemote.expectIceChecking();
+ },
+ ],
+ [
+ function PC_REMOTE_CHECK_ADDED_TRACK(test) {
+ is(test.pcRemote._pc.getTransceivers().length, 2,
+ "pcRemote should have two transceivers");
+ const track = test.pcRemote._pc.getTransceivers()[1].receiver.track;
+
+ const analyser = new AudioStreamAnalyser(
+ new AudioContext(), new MediaStream([track]));
+ const freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ);
+ return analyser.waitForAnalysisSuccess(arr => arr[freq] > 200);
+ },
+ function PC_REMOTE_CHECK_REMOVED_TRACK(test) {
+ is(test.pcRemote._pc.getTransceivers().length, 2,
+ "pcRemote should have two transceivers");
+ const track = test.pcRemote._pc.getTransceivers()[0].receiver.track;
+
+ const analyser = new AudioStreamAnalyser(
+ new AudioContext(), new MediaStream([track]));
+ const freq = analyser.binIndexForFrequency(TEST_AUDIO_FREQ);
+ return analyser.waitForAnalysisSuccess(arr => arr[freq] < 50);
+ }
+ ]
+ );
+
+ test.chain.insertAfterEach('PC_LOCAL_CREATE_OFFER',
+ PC_LOCAL_REMOVE_BUNDLE_FROM_OFFER);
+
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddVideoTrack.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddVideoTrack.html
new file mode 100644
index 0000000000..b1be690e5b
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddVideoTrack.html
@@ -0,0 +1,98 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1017888",
+ title: "Renegotiation: remove then add video track"
+ });
+
+ runNetworkTest(async function (options) {
+ // Use fake video here since the native fake device on linux doesn't
+ // change color as needed by checkVideoPlaying() below.
+ await pushPrefs(
+ ['media.video_loopback_dev', ''],
+ ['media.navigator.streams.fake', true]);
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ const test = new PeerConnectionTest(options);
+ const helper = new VideoStreamHelper();
+ var originalTrack;
+ let haveMuteEvent = new Promise(() => {});
+ let haveUnmuteEvent = new Promise(() => {});
+ addRenegotiation(test.chain,
+ [
+ function PC_REMOTE_FIND_RECEIVER(test) {
+ is(test.pcRemote._pc.getReceivers().length, 1,
+ "pcRemote should have one receiver");
+ originalTrack = test.pcRemote._pc.getReceivers()[0].track;
+ },
+ function PC_LOCAL_REMOVE_VIDEO_TRACK(test) {
+ // The new track's pipeline will start with a packet count of
+ // 0, but the remote side will keep its old pipeline and packet
+ // count.
+ test.pcLocal.disableRtpCountChecking = true;
+ return test.pcLocal.removeSender(0);
+ },
+ function PC_LOCAL_ADD_VIDEO_TRACK(test) {
+ return test.pcLocal.getAllUserMediaAndAddStreams([{video: true}]);
+ },
+ ],
+ [
+ function PC_REMOTE_WAIT_FOR_UNMUTE() {
+ return haveUnmuteEvent;
+ },
+ function PC_REMOTE_CHECK_ADDED_TRACK(test) {
+ is(test.pcRemote._pc.getTransceivers().length, 2,
+ "pcRemote should have two transceivers");
+ const track = test.pcRemote._pc.getTransceivers()[1].receiver.track;
+
+ const vAdded = test.pcRemote.remoteMediaElements.find(
+ elem => elem.id.includes(track.id));
+ return helper.checkVideoPlaying(vAdded);
+ },
+ function PC_REMOTE_WAIT_FOR_MUTE() {
+ return haveMuteEvent;
+ },
+ function PC_REMOTE_CHECK_REMOVED_TRACK(test) {
+ is(test.pcRemote._pc.getTransceivers().length, 2,
+ "pcRemote should have two transceivers");
+ const track = test.pcRemote._pc.getTransceivers()[0].receiver.track;
+
+ const vAdded = test.pcRemote.remoteMediaElements.find(
+ elem => elem.id.includes(track.id));
+ return helper.checkVideoPaused(vAdded, 10, 10, 16, 5000);
+ }
+ ]
+ );
+
+ // The first track should mute when the connection is closed.
+ test.chain.insertBefore("PC_REMOTE_SET_REMOTE_DESCRIPTION", [
+ function PC_REMOTE_SETUP_ONMUTE(test) {
+ haveMuteEvent = haveEvent(test.pcRemote._pc.getReceivers()[0].track, "mute");
+ }
+ ]);
+
+ // Second negotiation should cause the second track to unmute.
+ test.chain.insertAfter("PC_REMOTE_SET_REMOTE_DESCRIPTION", [
+ function PC_REMOTE_SETUP_ONUNMUTE(test) {
+ haveUnmuteEvent = haveEvent(test.pcRemote._pc.getReceivers()[1].track, "unmute");
+ }
+ ], false, 1);
+
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+ await test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddVideoTrackNoBundle.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddVideoTrackNoBundle.html
new file mode 100644
index 0000000000..dcaf7943e2
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeThenAddVideoTrackNoBundle.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1017888",
+ title: "Renegotiation: remove then add video track, no bundle"
+ });
+
+ runNetworkTest(async function (options) {
+ // Use fake video here since the native fake device on linux doesn't
+ // change color as needed by checkVideoPlaying() below.
+ await pushPrefs(
+ ['media.video_loopback_dev', ''],
+ ['media.navigator.streams.fake', true]);
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ options = options || { };
+ options.bundle = false;
+ const test = new PeerConnectionTest(options);
+ const helper = new VideoStreamHelper();
+ var originalTrack;
+ addRenegotiation(test.chain,
+ [
+ function PC_REMOTE_FIND_RECEIVER(test) {
+ is(test.pcRemote._pc.getReceivers().length, 1,
+ "pcRemote should have one receiver");
+ originalTrack = test.pcRemote._pc.getReceivers()[0].track;
+ },
+ function PC_LOCAL_REMOVE_VIDEO_TRACK(test) {
+ // The new track's pipeline will start with a packet count of
+ // 0, but the remote side will keep its old pipeline and packet
+ // count.
+ test.pcLocal.disableRtpCountChecking = true;
+ return test.pcLocal.removeSender(0);
+ },
+ function PC_LOCAL_ADD_VIDEO_TRACK(test) {
+ // Use fake:true here since the native fake device on linux doesn't
+ // change color as needed by checkVideoPlaying() below.
+ return test.pcLocal.getAllUserMediaAndAddStreams([{video: true}]);
+ },
+ function PC_LOCAL_EXPECT_ICE_CHECKING(test) {
+ test.pcLocal.expectIceChecking();
+ },
+ function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+ test.pcRemote.expectIceChecking();
+ },
+ ],
+ [
+ function PC_REMOTE_CHECK_ADDED_TRACK(test) {
+ is(test.pcRemote._pc.getTransceivers().length, 2,
+ "pcRemote should have two transceivers");
+ const track = test.pcRemote._pc.getTransceivers()[1].receiver.track;
+
+ const vAdded = test.pcRemote.remoteMediaElements.find(
+ elem => elem.id.includes(track.id));
+ return helper.checkVideoPlaying(vAdded);
+ },
+ function PC_REMOTE_CHECK_REMOVED_TRACK(test) {
+ is(test.pcRemote._pc.getTransceivers().length, 2,
+ "pcRemote should have two transceivers");
+ const track = test.pcRemote._pc.getTransceivers()[0].receiver.track;
+
+ const vAdded = test.pcRemote.remoteMediaElements.find(
+ elem => elem.id.includes(track.id));
+ return helper.checkVideoPaused(vAdded, 10, 10, 16, 5000);
+ },
+ ]
+ );
+
+ test.chain.insertAfterEach('PC_LOCAL_CREATE_OFFER',
+ PC_LOCAL_REMOVE_BUNDLE_FROM_OFFER);
+
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+ await test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_removeVideoTrack.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeVideoTrack.html
new file mode 100644
index 0000000000..4c4e7905e1
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_removeVideoTrack.html
@@ -0,0 +1,64 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1017888",
+ title: "Renegotiation: remove video track"
+ });
+
+ runNetworkTest(async (options) => {
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ const test = new PeerConnectionTest(options);
+ let receivedTrack, element;
+ addRenegotiation(test.chain,
+ [
+ function PC_REMOTE_SETUP_HELPER(test) {
+ is(test.pcRemote._pc.getReceivers().length, 1,
+ "pcRemote should have one receiver before renegotiation");
+
+ receivedTrack = test.pcRemote._pc.getReceivers()[0].track;
+ is(receivedTrack.readyState, "live",
+ "The received track should be live");
+
+ element = createMediaElement("video", "pcRemoteReceivedVideo");
+ element.srcObject = new MediaStream([receivedTrack]);
+ return haveEvent(element, "loadeddata");
+ },
+ function PC_LOCAL_REMOVE_VIDEO_TRACK(test) {
+ test.setOfferOptions({ offerToReceiveVideo: true });
+ test.setMediaConstraints([], [{video: true}]);
+ return test.pcLocal.removeSender(0);
+ },
+ ],
+ [
+ function PC_REMOTE_CHECK_FLOW_STOPPED(test) {
+ is(test.pcRemote._pc.getTransceivers().length, 1,
+ "pcRemote should have one transceiver");
+ const track = test.pcRemote._pc.getTransceivers()[0].receiver.track;
+
+ const vAdded = test.pcRemote.remoteMediaElements.find(
+ elem => elem.id.includes(track.id));
+ const helper = new VideoStreamHelper();
+ return helper.checkVideoPaused(vAdded, 10, 10, 16, 5000);
+ },
+ ]
+ );
+
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+ await test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_renderAfterRenegotiation.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_renderAfterRenegotiation.html
new file mode 100644
index 0000000000..c8091d7a9e
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_renderAfterRenegotiation.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1273652",
+ title: "Video receiver still renders after renegotiation",
+ visible: true
+ });
+
+ var pc1 = new RTCPeerConnection();
+ var pc2 = new RTCPeerConnection();
+
+ var add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ pc1.onicecandidate = e => add(pc2, e.candidate, generateErrorCallback());
+ pc2.onicecandidate = e => add(pc1, e.candidate, generateErrorCallback());
+
+ var v1, v2;
+ var delivered = new Promise(resolve => pc2.ontrack = e => {
+ // Test RTCTrackEvent here.
+ ok(e.streams.length, "has streams");
+ ok(e.streams[0].getTrackById(e.track.id), "has track");
+ ok(pc2.getReceivers().some(receiver => receiver == e.receiver), "has receiver");
+ if (e.streams[0].getTracks().length == 1) {
+ // Test RTCTrackEvent required args here.
+ mustThrowWith("RTCTrackEvent wo/required args",
+ "TypeError", () => new RTCTrackEvent("track", {}));
+ v2.srcObject = e.streams[0];
+ resolve();
+ }
+ });
+
+ runNetworkTest(async () => {
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false]);
+ await pushPrefs(["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ v2 = createMediaElement('video', 'v2');
+ is(v2.currentTime, 0, "v2.currentTime is zero at outset");
+
+ const emitter = new VideoFrameEmitter(CaptureStreamTestHelper.prototype.blue,
+ CaptureStreamTestHelper.prototype.green,
+ 16, 16);
+ emitter.start();
+ emitter.stream().getTracks().forEach(t => pc1.addTrack(t, emitter.stream()));
+ let h = emitter.helper();
+
+ let offer = await pc1.createOffer({});
+ await pc1.setLocalDescription(offer);
+ await pc2.setRemoteDescription(pc1.localDescription);
+ // check that createAnswer accepts arg.
+ let answer = await pc2.createAnswer({});
+ await pc2.setLocalDescription(answer);
+ await pc1.setRemoteDescription(pc2.localDescription);
+
+ // re-negotiate to trigger the race condition in the jitter buffer
+ offer = await pc1.createOffer({}); // check that createOffer accepts arg.
+ await pc1.setLocalDescription(offer);
+ await pc2.setRemoteDescription(pc1.localDescription);
+ answer = await pc2.createAnswer({});
+ await pc2.setLocalDescription(answer);
+ await pc1.setRemoteDescription(pc2.localDescription);
+ await delivered;
+
+ // now verify that actually something gets rendered into the remote video
+ // element.
+ await h.pixelMustBecome(v2, h.blue, {
+ threshold: 128,
+ infoString: "pcRemote's video should become blue",
+ });
+ // This will verify that new changes to the canvas propagate through
+ // the peerconnection
+ emitter.colors(h.red, h.green)
+ await h.pixelMustBecome(v2, h.red, {
+ threshold: 128,
+ infoString: "pcRemote's video should become red",
+ });
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceNullTrackThenRenegotiateAudio.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceNullTrackThenRenegotiateAudio.html
new file mode 100644
index 0000000000..2253b87672
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceNullTrackThenRenegotiateAudio.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1763832",
+ title: "Renegotiation (audio): Start with no track and recvonly, then replace and set direction to sendrecv, then renegotiate"
+ });
+
+ runNetworkTest(async () => {
+ await pushPrefs(
+ ['media.audio_loopback_dev', ''],
+ ['media.navigator.streams.fake', true]);
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+ const transceiverSend = offerer.addTransceiver('audio', {direction: 'recvonly'});
+
+ const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback());
+ answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback());
+
+ await offerer.setLocalDescription();
+ await answerer.setRemoteDescription(offerer.localDescription);
+ await answerer.setLocalDescription();
+ await offerer.setRemoteDescription(answerer.localDescription);
+
+ // add audio with replaceTrack, set send bit, and renegotiate
+ const stream = await navigator.mediaDevices.getUserMedia({audio: true});
+ const [track] = stream.getAudioTracks();
+ transceiverSend.sender.replaceTrack(track);
+ transceiverSend.direction = "sendrecv";
+ const remoteStreamAvailable = new Promise(r => {
+ answerer.ontrack = ({track}) => r(new MediaStream([track]));
+ });
+
+ await offerer.setLocalDescription();
+ await answerer.setRemoteDescription(offerer.localDescription);
+ await answerer.setLocalDescription();
+ await offerer.setRemoteDescription(answerer.localDescription);
+
+ const remoteStream = await remoteStreamAvailable;
+ const h = new AudioStreamHelper();
+ await h.checkAudioFlowing(remoteStream);
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceNullTrackThenRenegotiateVideo.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceNullTrackThenRenegotiateVideo.html
new file mode 100644
index 0000000000..d7bd6d8a37
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceNullTrackThenRenegotiateVideo.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <script type="application/javascript" src="simulcast.js"></script></head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1763832",
+ title: "Renegotiation (video): Start with no track and recvonly, then replace and set direction to sendrecv, then renegotiate"
+ });
+
+ runNetworkTest(async () => {
+ await pushPrefs(
+ ['media.video_loopback_dev', ''],
+ ['media.navigator.streams.fake', true]);
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+ const transceiverSend = offerer.addTransceiver('video', {direction: 'recvonly'});
+
+ const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback());
+ answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback());
+
+ await offerer.setLocalDescription();
+ await answerer.setRemoteDescription(offerer.localDescription);
+ await answerer.setLocalDescription();
+ await offerer.setRemoteDescription(answerer.localDescription);
+
+ // add video with replaceTrack, set send bit, and renegotiate
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ const [track] = stream.getVideoTracks();
+ transceiverSend.sender.replaceTrack(track);
+ transceiverSend.direction = "sendrecv";
+ const metadataToBeLoaded = [];
+ answerer.ontrack = (e) => {
+ metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track));
+ };
+
+ await offerer.setLocalDescription();
+ await answerer.setRemoteDescription(offerer.localDescription);
+ await answerer.setLocalDescription();
+ await offerer.setRemoteDescription(answerer.localDescription);
+
+ const elems = await Promise.all(metadataToBeLoaded);
+ is(elems.length, 1, "Should have one video element");
+
+ const helper = new VideoStreamHelper();
+ await helper.checkVideoPlaying(elems[0]);
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack.html
new file mode 100644
index 0000000000..9befc5c564
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack.html
@@ -0,0 +1,187 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1032839",
+ title: "Replace video and audio (with WebAudio) tracks",
+ visible: true
+ });
+
+ function allLocalStreamsHaveSender(pc) {
+ return pc.getLocalStreams()
+ .every(s => s.getTracks() // Every local stream,
+ .some(t => pc.getSenders() // should have some track,
+ .some(sn => sn.track == t))) // that's being sent over |pc|.
+ }
+
+ function allRemoteStreamsHaveReceiver(pc) {
+ return pc.getRemoteStreams()
+ .every(s => s.getTracks() // Every remote stream,
+ .some(t => pc.getReceivers() // should have some track,
+ .some(sn => sn.track == t))) // that's being received over |pc|.
+ }
+
+ function replacetest(wrapper) {
+ var pc = wrapper._pc;
+ var oldSenderCount = pc.getSenders().length;
+ var sender = pc.getSenders().find(sn => sn.track.kind == "video");
+ var oldTrack = sender.track;
+ ok(sender, "We have a sender for video");
+ ok(allLocalStreamsHaveSender(pc),
+ "Shouldn't have any local streams without a corresponding sender");
+ ok(allRemoteStreamsHaveReceiver(pc),
+ "Shouldn't have any remote streams without a corresponding receiver");
+
+ var newTrack;
+ var audiotrack;
+ return getUserMedia({video:true, audio:true})
+ .then(newStream => {
+ window.grip = newStream;
+ newTrack = newStream.getVideoTracks()[0];
+ audiotrack = newStream.getAudioTracks()[0];
+ isnot(newTrack, sender.track, "replacing with a different track");
+ ok(!pc.getLocalStreams().some(s => s == newStream),
+ "from a different stream");
+ // Use wrapper function, since it updates expected tracks
+ return wrapper.senderReplaceTrack(sender, newTrack, newStream);
+ })
+ .then(() => {
+ is(pc.getSenders().length, oldSenderCount, "same sender count");
+ is(sender.track, newTrack, "sender.track has been replaced");
+ ok(!pc.getSenders().map(sn => sn.track).some(t => t == oldTrack),
+ "old track not among senders");
+ // Spec does not say we add this new track to any stream
+ ok(!pc.getLocalStreams().some(s => s.getTracks()
+ .some(t => t == sender.track)),
+ "track does not exist among pc's local streams");
+ return sender.replaceTrack(audiotrack)
+ .then(() => ok(false, "replacing with different kind should fail"),
+ e => is(e.name, "TypeError",
+ "replacing with different kind should fail"));
+ });
+ }
+
+ runNetworkTest(function () {
+ test = new PeerConnectionTest();
+ test.audioCtx = new AudioContext();
+ test.setMediaConstraints([{video: true, audio: true}], [{video: true}]);
+ test.chain.removeAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW");
+
+ // Test replaceTrack on pcRemote separately since it's video only.
+ test.chain.append([
+ function PC_REMOTE_VIDEOONLY_REPLACE_VIDEOTRACK(test) {
+ return replacetest(test.pcRemote);
+ },
+ function PC_LOCAL_NEW_VIDEOTRACK_WAIT_FOR_MEDIA_FLOW(test) {
+ return test.pcLocal.waitForMediaFlow();
+ }
+ ]);
+
+ // Replace video twice on pcLocal to make sure it still works
+ // (does audio twice too, but hey)
+ test.chain.append([
+ function PC_LOCAL_AUDIOVIDEO_REPLACE_VIDEOTRACK_1(test) {
+ return replacetest(test.pcLocal);
+ },
+ function PC_REMOTE_NEW_VIDEOTRACK_WAIT_FOR_MEDIA_FLOW_1(test) {
+ return test.pcRemote.waitForMediaFlow();
+ },
+ function PC_LOCAL_AUDIOVIDEO_REPLACE_VIDEOTRACK_2(test) {
+ return replacetest(test.pcLocal);
+ },
+ function PC_REMOTE_NEW_VIDEOTRACK_WAIT_FOR_MEDIA_FLOW_2(test) {
+ return test.pcRemote.waitForMediaFlow();
+ }
+ ]);
+
+ test.chain.append([
+ function PC_LOCAL_AUDIOVIDEO_REPLACE_VIDEOTRACK_WITHSAME(test) {
+ var pc = test.pcLocal._pc;
+ var sender = pc.getSenders().find(sn => sn.track.kind == "video");
+ ok(sender, "should still have a sender of video");
+ return sender.replaceTrack(sender.track)
+ .then(() => ok(true, "replacing with itself should succeed"));
+ },
+ function PC_REMOTE_NEW_SAME_VIDEOTRACK_WAIT_FOR_MEDIA_FLOW(test) {
+ return test.pcRemote.waitForMediaFlow();
+ }
+ ]);
+
+ // Replace the gUM audio track on pcLocal with a WebAudio track.
+ test.chain.append([
+ function PC_LOCAL_AUDIOVIDEO_REPLACE_AUDIOTRACK_WEBAUDIO(test) {
+ var pc = test.pcLocal._pc;
+ var sender = pc.getSenders().find(sn => sn.track.kind == "audio");
+ ok(sender, "track has a sender");
+ var oldSenderCount = pc.getSenders().length;
+ var oldTrack = sender.track;
+
+ var sourceNode = test.audioCtx.createOscillator();
+ sourceNode.type = 'sine';
+ // We need a frequency not too close to the fake audio track
+ // (440Hz for loopback devices, 1kHz for fake tracks).
+ sourceNode.frequency.value = 2000;
+ sourceNode.start();
+
+ var destNode = test.audioCtx.createMediaStreamDestination();
+ sourceNode.connect(destNode);
+ var newTrack = destNode.stream.getAudioTracks()[0];
+
+ return test.pcLocal.senderReplaceTrack(
+ sender, newTrack, destNode.stream)
+ .then(() => {
+ is(pc.getSenders().length, oldSenderCount, "same sender count");
+ ok(!pc.getSenders().some(sn => sn.track == oldTrack),
+ "Replaced track should be removed from senders");
+ // TODO: Should PC remove local streams when there are no senders
+ // associated with it? getLocalStreams() isn't in the spec anymore,
+ // so I guess it is pretty arbitrary?
+ is(sender.track, newTrack, "sender.track has been replaced");
+ // Spec does not say we add this new track to any stream
+ ok(!pc.getLocalStreams().some(s => s.getTracks()
+ .some(t => t == sender.track)),
+ "track exists among pc's local streams");
+ });
+ }
+ ]);
+ test.chain.append([
+ function PC_LOCAL_CHECK_WEBAUDIO_FLOW_PRESENT(test) {
+ return test.pcRemote.checkReceivingToneFrom(test.audioCtx, test.pcLocal);
+ }
+ ]);
+ test.chain.append([
+ function PC_LOCAL_INVALID_ADD_VIDEOTRACKS(test) {
+ let videoTransceivers = test.pcLocal._pc.getTransceivers()
+ .filter(transceiver => {
+ return !transceiver.stopped &&
+ transceiver.receiver.track.kind == "video" &&
+ transceiver.sender.track;
+ });
+
+ ok(videoTransceivers.length,
+ "There is at least one non-stopped video transceiver with a track.");
+
+ videoTransceivers.forEach(transceiver => {
+ var stream = test.pcLocal._pc.getLocalStreams()[0];;
+ var track = transceiver.sender.track;
+ try {
+ test.pcLocal._pc.addTrack(track, stream);
+ ok(false, "addTrack existing track should fail");
+ } catch (e) {
+ is(e.name, "InvalidAccessError",
+ "addTrack existing track should fail");
+ }
+ });
+ }
+ ]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_camera.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_camera.html
new file mode 100644
index 0000000000..356517e79f
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_camera.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<video id="video" width="160" height="120" autoplay></video>
+
+<script type="application/javascript">
+ createHTML({
+ bug: "1709481",
+ title: "replaceTrack (null -> camera) test",
+ visible: true
+ });
+
+ runNetworkTest(async () => {
+ // Make sure we use the fake video device, and not loopback
+ await pushPrefs(
+ ['media.video_loopback_dev', ''],
+ ['media.navigator.streams.fake', true]);
+ const pc1 = new RTCPeerConnection(), pc2 = new RTCPeerConnection();
+ pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate);
+ pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate);
+ pc2.ontrack = ({track}) => video.srcObject = new MediaStream([track]);
+ pc1.addTransceiver("audio");
+ const tc1 = pc1.addTransceiver("video");
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ const [track] = stream.getVideoTracks();
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+ await wait(100);
+ await tc1.sender.replaceTrack(track);
+ const h = new VideoStreamHelper();
+ await h.checkVideoPlaying(video);
+ pc1.close();
+ pc2.close();
+ await SpecialPowers.popPrefEnv();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_disabled.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_disabled.html
new file mode 100644
index 0000000000..11b2762d96
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_disabled.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+<script src="pc.js"></script>
+<script src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script>
+createHTML({
+ bug: "1576771",
+ title: "Replace a disabled video track with an enabled one",
+ visible: true,
+});
+
+runNetworkTest(() => {
+ const helper = new CaptureStreamTestHelper2D(240, 160);
+ const emitter = new VideoFrameEmitter(helper.green, helper.green, 240, 160);
+ const test = new PeerConnectionTest();
+ test.setMediaConstraints([{video: true}], []);
+ test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW", [
+ function PC_LOCAL_DISABLE_SENDTRACK(test) {
+ test.pcLocal._pc.getSenders()[0].track.enabled = false;
+ },
+ function PC_REMOTE_WAIT_FOR_BLACK(test) {
+ return helper.pixelMustBecome(
+ test.pcRemote.remoteMediaElements[0], helper.black, {
+ threshold: 128,
+ infoString: "Remote disabled track becomes black",
+ cancel: wait(10000).then(
+ () => new Error("Timeout waiting for black"))});
+ },
+ function PC_LOCAL_REPLACETRACK_WITH_ENABLED_TRACK(test) {
+ emitter.start();
+ test.pcLocal._pc.getSenders()[0].replaceTrack(
+ emitter.stream().getTracks()[0]);
+ },
+ ]);
+ test.chain.append([
+ function PC_REMOTE_WAIT_FOR_GREEN(test) {
+ return helper.pixelMustBecome(
+ test.pcRemote.remoteMediaElements[0], helper.green, {
+ threshold: 128,
+ infoString: "Remote disabled track becomes green",
+ cancel: wait(10000).then(
+ () => new Error("Timeout waiting for green"))});
+ },
+ function CLEANUP(test) {
+ emitter.stop();
+ for (const track of emitter.stream().getTracks()) {
+ track.stop();
+ }
+ },
+ ]);
+ return test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_microphone.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_microphone.html
new file mode 100644
index 0000000000..5886caf6a4
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceTrack_microphone.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script type="application/javascript">
+ createHTML({
+ bug: "1709481",
+ title: "replaceTrack (null -> microphone) test",
+ visible: true
+ });
+
+ runNetworkTest(async () => {
+ // Make sure we use the fake audio device, and not loopback
+ await pushPrefs(
+ ['media.audio_loopback_dev', ''],
+ ['media.navigator.streams.fake', true]);
+ const pc1 = new RTCPeerConnection(), pc2 = new RTCPeerConnection();
+ pc1.onicecandidate = e => pc2.addIceCandidate(e.candidate);
+ pc2.onicecandidate = e => pc1.addIceCandidate(e.candidate);
+ let remoteStream;
+ pc2.ontrack = ({track}) => remoteStream = new MediaStream([track]);
+ const tc1 = pc1.addTransceiver("audio");
+ const stream = await navigator.mediaDevices.getUserMedia({audio: true});
+ const [track] = stream.getAudioTracks();
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+ await wait(100);
+ await tc1.sender.replaceTrack(track);
+ const h = new AudioStreamHelper();
+ await h.checkAudioFlowing(remoteStream);
+ pc1.close();
+ pc2.close();
+ await SpecialPowers.popPrefEnv();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceVideoThenRenegotiate.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceVideoThenRenegotiate.html
new file mode 100644
index 0000000000..070cb42fcb
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_replaceVideoThenRenegotiate.html
@@ -0,0 +1,74 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1017888",
+ title: "Renegotiation: replaceTrack followed by adding a second video stream"
+ });
+
+ runNetworkTest(async (options) => {
+ await pushPrefs(['media.peerconnection.video.min_bitrate_estimate', 180*1000]);
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{video:true}], [{video:true}]);
+ const helper = new VideoStreamHelper();
+ const emitter1 = new VideoFrameEmitter(CaptureStreamTestHelper.prototype.red,
+ CaptureStreamTestHelper.prototype.green);
+ const emitter2 = new VideoFrameEmitter(CaptureStreamTestHelper.prototype.blue,
+ CaptureStreamTestHelper.prototype.grey);
+ test.chain.replace("PC_LOCAL_GUM", [
+ function PC_LOCAL_ADDTRACK(test) {
+ test.pcLocal.attachLocalStream(emitter1.stream());
+ emitter1.start();
+ },
+ ]);
+ addRenegotiation(test.chain,
+ [
+ function PC_LOCAL_REPLACE_VIDEO_TRACK_THEN_ADD_SECOND_STREAM(test) {
+ emitter1.stop();
+ emitter2.start();
+ const newstream = emitter2.stream();
+ const newtrack = newstream.getVideoTracks()[0];
+ var sender = test.pcLocal._pc.getSenders()[0];
+ return test.pcLocal.senderReplaceTrack(sender, newtrack, newstream)
+ .then(() => {
+ test.setMediaConstraints([{video: true}, {video: true}],
+ [{video: true}]);
+ });
+ },
+ ],
+ [
+ function PC_REMOTE_CHECK_ORIGINAL_TRACK_NOT_ENDED(test) {
+ is(test.pcRemote._pc.getTransceivers().length, 1,
+ "pcRemote should have one transceiver");
+ const track = test.pcRemote._pc.getTransceivers()[0].receiver.track;
+
+ const vremote = test.pcRemote.remoteMediaElements.find(
+ elem => elem.id.includes(track.id));
+ if (!vremote) {
+ return Promise.reject(new Error("Couldn't find video element"));
+ }
+ ok(!vremote.ended, "Original track should not have ended after renegotiation (replaceTrack is not signalled!)");
+ return helper.checkVideoPlaying(vremote);
+ }
+ ]
+ );
+
+ await test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIce.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIce.html
new file mode 100644
index 0000000000..d94bb084b7
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIce.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "906986",
+ title: "Renegotiation: restart ice"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+
+ addRenegotiation(test.chain,
+ [
+ // causes a full, normal ice restart
+ function PC_LOCAL_SET_OFFER_OPTION(test) {
+ test.setOfferOptions({ iceRestart: true });
+ },
+ function PC_LOCAL_EXPECT_ICE_CHECKING(test) {
+ test.pcLocal.expectIceChecking();
+ },
+ function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+ test.pcRemote.expectIceChecking();
+ }
+ ]
+ );
+
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceBadAnswer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceBadAnswer.html
new file mode 100644
index 0000000000..b71001b0db
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceBadAnswer.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1413709",
+ title: "Renegotiation: bad answer ICE credentials"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+
+ addRenegotiation(test.chain,
+ [
+ function PC_LOCAL_ADD_SECOND_STREAM(test) {
+ test.setMediaConstraints([{audio: true}],
+ []);
+ return test.pcLocal.getAllUserMedia([{audio: true}]);
+ },
+ ]
+ );
+
+ // If the offerer hasn't indicated ICE restart, then an answer
+ // arriving during renegotiation that has modified ICE credentials
+ // should cause an error
+ test.chain.replaceAfter("PC_LOCAL_GET_ANSWER",
+ [
+ function PC_LOCAL_REWRITE_REMOTE_SDP_ICE_CREDS(test) {
+ test._remote_answer.sdp =
+ test._remote_answer.sdp.replace(/a=ice-pwd:.*\r\n/g,
+ "a=ice-pwd:bad-pwd\r\n")
+ .replace(/a=ice-ufrag:.*\r\n/g,
+ "a=ice-ufrag:bad-ufrag\r\n");
+ },
+
+ function PC_LOCAL_EXPECT_SET_REMOTE_DESCRIPTION_FAIL(test) {
+ return test.setRemoteDescription(test.pcLocal,
+ test._remote_answer,
+ STABLE)
+ .then(() => ok(false, "setRemoteDescription must fail"),
+ e => is(e.name, "InvalidAccessError",
+ "setRemoteDescription must fail and did"));
+ }
+ ], 1 // replace after the second PC_LOCAL_GET_ANSWER
+ );
+
+ test.setMediaConstraints([{audio: true}], []);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalAndRemoteRollback.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalAndRemoteRollback.html
new file mode 100644
index 0000000000..6bbf9440fc
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalAndRemoteRollback.html
@@ -0,0 +1,82 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "906986",
+ title: "Renegotiation: restart ice, local and remote rollback"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+
+ addRenegotiation(test.chain,
+ [
+ async function PC_LOCAL_SETUP_ICE_HANDLER(test) {
+ await test.pcLocal.endOfTrickleIce;
+ test.pcLocal.setupIceCandidateHandler(test);
+ },
+
+ // causes a full, normal ice restart
+ function PC_LOCAL_SET_OFFER_OPTION(test) {
+ test.setOfferOptions({ iceRestart: true });
+ }
+ ]
+ );
+
+ test.chain.replaceAfter('PC_REMOTE_CREATE_ANSWER',
+ [
+ function PC_LOCAL_EXPECT_ICE_CONNECTED(test) {
+ test.pcLocal.iceCheckingIceRollbackExpected = true;
+ },
+
+ function PC_REMOTE_ROLLBACK(test) {
+ return test.setRemoteDescription(test.pcRemote, { type: "rollback" },
+ STABLE);
+ },
+
+ async function PC_LOCAL_ROLLBACK(test) {
+ await test.pcLocal.endOfTrickleIce;
+ // We haven't negotiated the new stream yet.
+ test.pcLocal.expectNegotiationNeeded();
+ return test.setLocalDescription(
+ test.pcLocal,
+ new RTCSessionDescription({ type: "rollback", sdp: ""}),
+ STABLE);
+ },
+
+ // Rolling back should shut down gathering for the offerer,
+ // but because the answerer never set a local description, no ICE
+ // gathering has happened yet, so there's no changes to ICE gathering
+ // state
+ function PC_LOCAL_WAIT_FOR_END_OF_TRICKLE(test) {
+ return test.pcLocal.endOfTrickleIce;
+ },
+
+ function PC_LOCAL_EXPECT_ICE_CHECKING(test) {
+ test.pcLocal.expectIceChecking();
+ },
+ function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+ test.pcRemote.expectIceChecking();
+ }
+ ],
+ 1 // Replaces after second PC_REMOTE_CREATE_ANSWER
+ );
+ test.chain.append(commandsPeerConnectionOfferAnswer);
+
+ // for now, only use one stream, because rollback doesn't seem to
+ // like multiple streams. See bug 1259465.
+ test.setMediaConstraints([{audio: true}],
+ [{audio: true}]);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalAndRemoteRollbackNoSubsequentRestart.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalAndRemoteRollbackNoSubsequentRestart.html
new file mode 100644
index 0000000000..37b0fc68fc
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalAndRemoteRollbackNoSubsequentRestart.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "906986",
+ title: "Renegotiation: restart ice, local and remote rollback, without a subsequent ICE restart"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+
+ addRenegotiation(test.chain,
+ [
+ function PC_LOCAL_SETUP_ICE_HANDLER(test) {
+ test.pcLocal.setupIceCandidateHandler(test);
+ },
+ function PC_REMOTE_SETUP_ICE_HANDLER(test) {
+ test.pcRemote.setupIceCandidateHandler(test);
+ },
+
+ // causes a full, normal ice restart
+ function PC_LOCAL_SET_OFFER_OPTION(test) {
+ test.setOfferOptions({ iceRestart: true });
+ }
+ ]
+ );
+
+ test.chain.replaceAfter('PC_REMOTE_CREATE_ANSWER',
+ [
+ function PC_REMOTE_ROLLBACK(test) {
+ return test.setRemoteDescription(test.pcRemote, { type: "rollback" },
+ STABLE);
+ },
+
+ async function PC_LOCAL_ROLLBACK(test) {
+ await test.pcLocal.endOfTrickleIce;
+ // We haven't negotiated the new stream yet.
+ test.pcLocal.expectNegotiationNeeded();
+ return test.setLocalDescription(
+ test.pcLocal,
+ new RTCSessionDescription({ type: "rollback", sdp: ""}),
+ STABLE);
+ },
+
+ // Rolling back should shut down gathering for the offerer,
+ // but because the answerer never set a local description, no ICE
+ // gathering has happened yet, so there's no changes to ICE gathering
+ // state
+ function PC_LOCAL_WAIT_FOR_END_OF_TRICKLE(test) {
+ return test.pcLocal.endOfTrickleIce;
+ },
+
+ function PC_LOCAL_SET_OFFER_OPTION(test) {
+ test.setOfferOptions({ iceRestart: false });
+ }
+ ],
+ 1 // Replaces after second PC_REMOTE_CREATE_ANSWER
+ );
+ test.chain.append(commandsPeerConnectionOfferAnswer);
+
+ // for now, only use one stream, because rollback doesn't seem to
+ // like multiple streams. See bug 1259465.
+ test.setMediaConstraints([{audio: true}],
+ [{audio: true}]);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalRollback.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalRollback.html
new file mode 100644
index 0000000000..f5f9a1f220
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalRollback.html
@@ -0,0 +1,76 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "906986",
+ title: "Renegotiation: restart ice, local rollback"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+
+ addRenegotiation(test.chain,
+ [
+ // causes a full, normal ice restart
+ function PC_LOCAL_SET_OFFER_OPTION(test) {
+ test.setOfferOptions({ iceRestart: true });
+ },
+ // causes an ice restart and then rolls it back
+ // (does not result in sending an offer)
+ function PC_LOCAL_SETUP_ICE_HANDLER(test) {
+ test.pcLocal.setupIceCandidateHandler(test, () => {});
+ },
+ function PC_LOCAL_CREATE_AND_SET_OFFER(test) {
+ return test.createOffer(test.pcLocal).then(offer => {
+ return test.setLocalDescription(test.pcLocal,
+ offer,
+ HAVE_LOCAL_OFFER);
+ });
+ },
+ function PC_LOCAL_EXPECT_ICE_CONNECTED(test) {
+ test.pcLocal.iceCheckingIceRollbackExpected = true;
+ },
+ function PC_LOCAL_WAIT_FOR_GATHERING(test) {
+ return new Promise(r => {
+ test.pcLocal._pc.addEventListener("icegatheringstatechange", () => {
+ if (test.pcLocal._pc.iceGatheringState == "gathering") {
+ r();
+ }
+ });
+ });
+ },
+ function PC_LOCAL_ROLLBACK(test) {
+ return test.setLocalDescription(test.pcLocal,
+ { type: "rollback", sdp: ""},
+ STABLE);
+ },
+ // Rolling back should shut down gathering
+ function PC_LOCAL_WAIT_FOR_END_OF_TRICKLE(test) {
+ return test.pcLocal.endOfTrickleIce;
+ },
+ function PC_LOCAL_EXPECT_ICE_CHECKING(test) {
+ test.pcLocal.expectIceChecking();
+ },
+ function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+ test.pcRemote.expectIceChecking();
+ }
+ ]
+ );
+
+ // for now, only use one stream, because rollback doesn't seem to
+ // like multiple streams. See bug 1259465.
+ test.setMediaConstraints([{audio: true}],
+ [{audio: true}]);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalRollbackNoSubsequentRestart.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalRollbackNoSubsequentRestart.html
new file mode 100644
index 0000000000..8e27864aae
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceLocalRollbackNoSubsequentRestart.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "906986",
+ title: "Renegotiation: restart ice, local rollback, then renegotiation without ICE restart"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+
+ addRenegotiation(test.chain,
+ [
+ // causes a full, normal ice restart
+ function PC_LOCAL_SET_OFFER_OPTION(test) {
+ test.setOfferOptions({ iceRestart: true });
+ },
+ // causes an ice restart and then rolls it back
+ // (does not result in sending an offer)
+ function PC_LOCAL_SETUP_ICE_HANDLER(test) {
+ test.pcLocal.setupIceCandidateHandler(test, () => {});
+ },
+ function PC_LOCAL_CREATE_AND_SET_OFFER(test) {
+ return test.createOffer(test.pcLocal).then(offer => {
+ return test.setLocalDescription(test.pcLocal,
+ offer,
+ HAVE_LOCAL_OFFER);
+ });
+ },
+ function PC_LOCAL_WAIT_FOR_END_OF_TRICKLE(test) {
+ return test.pcLocal.endOfTrickleIce;
+ },
+ function PC_LOCAL_ROLLBACK(test) {
+ return test.setLocalDescription(test.pcLocal,
+ { type: "rollback", sdp: ""},
+ STABLE);
+ },
+ function PC_LOCAL_SET_OFFER_OPTION(test) {
+ test.setOfferOptions({ iceRestart: false });
+ }
+ ]
+ );
+
+ // for now, only use one stream, because rollback doesn't seem to
+ // like multiple streams. See bug 1259465.
+ test.setMediaConstraints([{audio: true}],
+ [{audio: true}]);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoBundle.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoBundle.html
new file mode 100644
index 0000000000..134fa97cc0
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoBundle.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "906986",
+ title: "Renegotiation: restart ice, no bundle"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ options = options || { };
+ options.bundle = false;
+ test = new PeerConnectionTest(options);
+
+ addRenegotiation(test.chain,
+ [
+ // causes a full, normal ice restart
+ function PC_LOCAL_SET_OFFER_OPTION(test) {
+ test.setOfferOptions({ iceRestart: true });
+ },
+ function PC_LOCAL_EXPECT_ICE_CHECKING(test) {
+ test.pcLocal.expectIceChecking();
+ },
+ function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+ test.pcRemote.expectIceChecking();
+ }
+ ]
+ );
+
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoBundleNoRtcpMux.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoBundleNoRtcpMux.html
new file mode 100644
index 0000000000..06a3a3c980
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoBundleNoRtcpMux.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "906986",
+ title: "Renegotiation: restart ice, no bundle and disabled RTCP-Mux"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ options = options || { };
+ options.bundle = false;
+ options.rtcpmux = false;
+ test = new PeerConnectionTest(options);
+
+ addRenegotiation(test.chain,
+ [
+ // causes a full, normal ice restart
+ function PC_LOCAL_SET_OFFER_OPTION(test) {
+ test.setOfferOptions({ iceRestart: true });
+ },
+ function PC_LOCAL_EXPECT_ICE_CHECKING(test) {
+ test.pcLocal.expectIceChecking();
+ },
+ function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+ test.pcRemote.expectIceChecking();
+ }
+ ]
+ );
+
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoRtcpMux.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoRtcpMux.html
new file mode 100644
index 0000000000..5d4780211a
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restartIceNoRtcpMux.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "906986",
+ title: "Renegotiation: restart ice, with disabled RTCP-Mux"
+ });
+
+ var test;
+ runNetworkTest(function (options) {
+ options = options || { };
+ options.rtcpmux = false;
+ test = new PeerConnectionTest(options);
+
+ addRenegotiation(test.chain,
+ [
+ // causes a full, normal ice restart
+ function PC_LOCAL_SET_OFFER_OPTION(test) {
+ test.setOfferOptions({ iceRestart: true });
+ },
+ function PC_LOCAL_EXPECT_ICE_CHECKING(test) {
+ test.pcLocal.expectIceChecking();
+ },
+ function PC_REMOTE_EXPECT_ICE_CHECKING(test) {
+ test.pcRemote.expectIceChecking();
+ }
+ ]
+ );
+
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+ return test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restrictBandwidthTargetBitrate.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restrictBandwidthTargetBitrate.html
new file mode 100644
index 0000000000..ff9fb1fc22
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restrictBandwidthTargetBitrate.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "Bug 1404250",
+ title: "Extremely bitrate restricted video-only peer connection"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+ test.chain.insertAfter('PC_REMOTE_GET_OFFER', [
+ function PC_REMOTE_ADD_TIAS(test) {
+ test._local_offer.sdp = sdputils.addTiasBps(
+ test._local_offer.sdp, 25000);
+ info("Offer with TIAS: " + JSON.stringify(test._local_offer));
+ }
+ ]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_restrictBandwidthWithTias.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_restrictBandwidthWithTias.html
new file mode 100644
index 0000000000..85b831e9a8
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_restrictBandwidthWithTias.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1359854",
+ title: "500Kb restricted video-only peer connection"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+ test.chain.insertAfter('PC_REMOTE_GET_OFFER', [
+ function PC_REMOTE_ADD_TIAS(test) {
+ test._local_offer.sdp = sdputils.addTiasBps(
+ test._local_offer.sdp, 250000);
+ info("Offer with TIAS: " + JSON.stringify(test._local_offer));
+ }
+ ]);
+ // TODO it would be nice to verify the used bandwidth
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_rtcp_rsize.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_rtcp_rsize.html
new file mode 100644
index 0000000000..25270984ea
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_rtcp_rsize.html
@@ -0,0 +1,81 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+ <script type="application/javascript" src="sdpUtils.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1279153",
+ title: "rtcp-rsize",
+ visible: true
+ });
+
+ // 0) Use webrtc-sdp
+ // 1) ADD RTCP-RISZE to all video m-sections
+ // 2) Check for RTCP-RSIZE in ANSWER
+ // 3) Wait for media to flow
+ // 4) Wait for RTCP stats
+
+ runNetworkTest(async function (options) {
+ const test = new PeerConnectionTest(options);
+
+ let mSectionsAltered = 0;
+
+ test.chain.insertAfter("PC_LOCAL_CREATE_OFFER", [
+ function PC_LOCAL_ADD_RTCP_RSIZE(test) {
+ const lines = test.originalOffer.sdp.split("\r\n");
+ info(`SDP before rtcp-rsize: ${lines.join('\n')}`);
+ // Insert an rtcp-rsize for each m section
+ const rsizeAdded = lines.flatMap(line => {
+ if (line.startsWith("m=video")) {
+ mSectionsAltered = mSectionsAltered + 1;
+ return [line, "a=rtcp-rsize"];
+ }
+ return [line];
+ });
+ test.originalOffer.sdp = rsizeAdded.join("\r\n");
+ info(`SDP with rtcp-rsize: ${rsizeAdded.join("\n")}`);
+ is(mSectionsAltered, 1, "We only altered 1 msection")
+ }]);
+
+ // Check that the rtcp-rsize makes into the answer
+ test.chain.insertAfter("PC_LOCAL_SET_REMOTE_DESCRIPTION", [
+ function PC_LOCAL_CHECK_RTCP_RSIZE(test) {
+ const msections = sdputils.getMSections(test.pcLocal._pc.currentRemoteDescription.sdp);
+ var alteredMSectionsFound = 0;
+ for (msection of msections) {
+ if (msection.startsWith("m=video")) {
+ ok(msection.includes("\r\na=rtcp-rsize\r\n"), "video m-section includes RTCP-RSIZE");
+ alteredMSectionsFound = alteredMSectionsFound + 1;
+ } else {
+ ok(!msection.includes("\r\na=rtcp-rsize\r\n"), "audio m-section does not include RTCP-RSIZE");
+ }
+ }
+ is(alteredMSectionsFound, mSectionsAltered, "correct number of msections found");
+ }
+ ]);
+
+ // Make sure that we are still getting RTCP stats
+ test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW",
+ async function PC_LOCAL_AND_REMOTE_CHECK_FOR_RTCP_STATS(test) {
+ await Promise.all([
+ waitForSyncedRtcp(test.pcLocal._pc),
+ waitForSyncedRtcp(test.pcRemote._pc),
+ ]);
+ // The work is done by waitForSyncedRtcp which will throw if
+ // RTCP stats are not received.
+ info("RTCP stats received!");
+ },
+ );
+ test.setMediaConstraints([{audio: true}, {video: true}], []);
+ await test.run();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_scaleResolution.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_scaleResolution.html
new file mode 100644
index 0000000000..4be6873fa6
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_scaleResolution.html
@@ -0,0 +1,119 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1244913",
+ title: "Scale resolution down on a PeerConnection",
+ visible: true
+ });
+
+ async function checkForH264Support() {
+ const pc = new RTCPeerConnection();
+ const offer = await pc.createOffer({offerToReceiveVideo: true});
+ return offer.sdp.match(/a=rtpmap:[1-9][0-9]* H264/);
+ }
+
+ let resolutionAlignment = 1;
+
+ var mustRejectWith = (msg, reason, f) =>
+ f().then(() => ok(false, msg),
+ e => is(e.name, reason, msg));
+
+ async function testScale(codec) {
+ var pc1 = new RTCPeerConnection();
+ var pc2 = new RTCPeerConnection();
+
+ var add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ pc1.onicecandidate = e => add(pc2, e.candidate, generateErrorCallback());
+ pc2.onicecandidate = e => add(pc1, e.candidate, generateErrorCallback());
+
+ info("testing scaling with " + codec);
+
+ let stream = await navigator.mediaDevices.getUserMedia({ video: true });
+
+ var v1 = createMediaElement('video', 'v1');
+ var v2 = createMediaElement('video', 'v2');
+
+ var ontrackfired = new Promise(resolve => pc2.ontrack = e => resolve(e));
+ var v2loadedmetadata = new Promise(resolve => v2.onloadedmetadata = resolve);
+
+ is(v2.currentTime, 0, "v2.currentTime is zero at outset");
+
+ v1.srcObject = stream;
+ var sender = pc1.addTrack(stream.getVideoTracks()[0], stream);
+ let parameters = sender.getParameters();
+ is(parameters.encodings.length, 1, "Default number of encodings should be 1");
+ parameters.encodings[0].scaleResolutionDownBy = 0.5;
+
+ await mustRejectWith(
+ "Invalid scaleResolutionDownBy must reject", "RangeError",
+ () => sender.setParameters(parameters)
+ );
+
+ parameters = sender.getParameters();
+ parameters.encodings[0].scaleResolutionDownBy = 2;
+ parameters.encodings[0].maxBitrate = 60000;
+
+ await sender.setParameters(parameters);
+
+ parameters = sender.getParameters();
+ is(parameters.encodings[0].scaleResolutionDownBy, 2, "Should be able to set scaleResolutionDownBy");
+ is(parameters.encodings[0].maxBitrate, 60000, "Should be able to set maxBitrate");
+
+ let offer = await pc1.createOffer();
+ if (codec == "VP8") {
+ offer.sdp = sdputils.removeAllButPayloadType(offer.sdp, 126);
+ }
+ await pc1.setLocalDescription(offer);
+ await pc2.setRemoteDescription(pc1.localDescription);
+
+ let answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+ await pc1.setRemoteDescription(pc2.localDescription);
+ let trackevent = await ontrackfired;
+
+ v2.srcObject = trackevent.streams[0];
+
+ await v2loadedmetadata;
+
+ await waitUntil(() => v2.currentTime > 0);
+ ok(v2.currentTime > 0, "v2.currentTime is moving (" + v2.currentTime + ")");
+
+ ok(v1.videoWidth > 0, "source width is positive");
+ ok(v1.videoHeight > 0, "source height is positive");
+ const expectedWidth =
+ v1.videoWidth / 2 - (v1.videoWidth / 2 % resolutionAlignment);
+ const expectedHeight =
+ v1.videoHeight / 2 - (v1.videoHeight / 2 % resolutionAlignment);
+ is(v2.videoWidth, expectedWidth,
+ "sink is half the width of the source");
+ is(v2.videoHeight, expectedHeight,
+ "sink is half the height of the source");
+ stream.getTracks().forEach(track => track.stop());
+ v1.srcObject = v2.srcObject = null;
+ pc1.close()
+ pc2.close()
+ }
+
+ runNetworkTest(async () => {
+ await matchPlatformH264CodecPrefs();
+ const hasH264 = await checkForH264Support();
+ if (hasH264 && navigator.userAgent.includes("Android")) {
+ // Android only has a hw encoder for h264
+ resolutionAlignment = 16;
+ }
+ await pushPrefs(['media.peerconnection.video.lock_scaling', true]);
+ await testScale("VP8");
+ if (hasH264) {
+ await testScale("H264");
+ }
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_scaleResolution_oldSetParameters.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_scaleResolution_oldSetParameters.html
new file mode 100644
index 0000000000..85a989ba32
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_scaleResolution_oldSetParameters.html
@@ -0,0 +1,122 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1244913",
+ title: "Scale resolution down on a PeerConnection",
+ visible: true
+ });
+
+ async function checkForH264Support() {
+ const pc = new RTCPeerConnection();
+ const offer = await pc.createOffer({offerToReceiveVideo: true});
+ return offer.sdp.match(/a=rtpmap:[1-9][0-9]* H264/);
+ }
+
+ let resolutionAlignment = 1;
+
+ var mustRejectWith = (msg, reason, f) =>
+ f().then(() => ok(false, msg),
+ e => is(e.name, reason, msg));
+
+ async function testScale(codec) {
+ var pc1 = new RTCPeerConnection();
+ var pc2 = new RTCPeerConnection();
+
+ var add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ pc1.onicecandidate = e => add(pc2, e.candidate, generateErrorCallback());
+ pc2.onicecandidate = e => add(pc1, e.candidate, generateErrorCallback());
+
+ info("testing scaling with " + codec);
+
+ let stream = await navigator.mediaDevices.getUserMedia({ video: true });
+
+ var v1 = createMediaElement('video', 'v1');
+ var v2 = createMediaElement('video', 'v2');
+
+ var ontrackfired = new Promise(resolve => pc2.ontrack = e => resolve(e));
+ var v2loadedmetadata = new Promise(resolve => v2.onloadedmetadata = resolve);
+
+ is(v2.currentTime, 0, "v2.currentTime is zero at outset");
+
+ v1.srcObject = stream;
+ var sender = pc1.addTrack(stream.getVideoTracks()[0], stream);
+
+ const otherErrorStart = await GleanTest.rtcrtpsenderSetparameters.failOther.testGetValue();
+ const noTransactionIdWarningStart = await GleanTest.rtcrtpsenderSetparameters.warnNoTransactionid.testGetValue();
+
+ await mustRejectWith(
+ "Invalid scaleResolutionDownBy must reject", "RangeError",
+ () => sender.setParameters(
+ { encodings:[{ scaleResolutionDownBy: 0.5 } ] })
+ );
+
+ const otherErrorEnd = await GleanTest.rtcrtpsenderSetparameters.failOther.testGetValue();
+ const noTransactionIdWarningEnd = await GleanTest.rtcrtpsenderSetparameters.warnNoTransactionid.testGetValue();
+
+ // Make sure Glean is recording these statistics
+ is(otherErrorEnd.denominator, otherErrorStart.denominator, "No new RTCRtpSenders were created during this time");
+ is(otherErrorEnd.numerator, otherErrorStart.numerator + 1, "RTCRtpSender.setParameters reported a failure via Glean");
+ is(noTransactionIdWarningEnd.denominator, noTransactionIdWarningStart.denominator, "No new RTCRtpSenders were created during this time");
+ is(noTransactionIdWarningEnd.numerator, noTransactionIdWarningStart.numerator + 1, "Glean should have recorded a warning due to missing transactionId");
+
+ await sender.setParameters({ encodings: [{ maxBitrate: 60000,
+ scaleResolutionDownBy: 2 }] });
+
+ let offer = await pc1.createOffer();
+ if (codec == "VP8") {
+ offer.sdp = sdputils.removeAllButPayloadType(offer.sdp, 126);
+ }
+ await pc1.setLocalDescription(offer);
+ await pc2.setRemoteDescription(pc1.localDescription);
+
+ let answer = await pc2.createAnswer();
+ await pc2.setLocalDescription(answer);
+ await pc1.setRemoteDescription(pc2.localDescription);
+ let trackevent = await ontrackfired;
+
+ v2.srcObject = trackevent.streams[0];
+
+ await v2loadedmetadata;
+
+ await waitUntil(() => v2.currentTime > 0);
+ ok(v2.currentTime > 0, "v2.currentTime is moving (" + v2.currentTime + ")");
+
+ ok(v1.videoWidth > 0, "source width is positive");
+ ok(v1.videoHeight > 0, "source height is positive");
+ const expectedWidth =
+ v1.videoWidth / 2 - (v1.videoWidth / 2 % resolutionAlignment);
+ const expectedHeight =
+ v1.videoHeight / 2 - (v1.videoHeight / 2 % resolutionAlignment);
+ is(v2.videoWidth, expectedWidth,
+ "sink is half the width of the source");
+ is(v2.videoHeight, expectedHeight,
+ "sink is half the height of the source");
+ stream.getTracks().forEach(track => track.stop());
+ v1.srcObject = v2.srcObject = null;
+ pc1.close()
+ pc2.close()
+ }
+
+ runNetworkTest(async () => {
+ await matchPlatformH264CodecPrefs();
+ const hasH264 = await checkForH264Support();
+ if (hasH264 && navigator.userAgent.includes("Android")) {
+ // Android only has a hw encoder for h264
+ resolutionAlignment = 16;
+ }
+ await pushPrefs(['media.peerconnection.video.lock_scaling', true]);
+ await testScale("VP8");
+ if (hasH264) {
+ await testScale("H264");
+ }
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_sender_and_receiver_stats.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_sender_and_receiver_stats.html
new file mode 100644
index 0000000000..72749e8c50
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_sender_and_receiver_stats.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1355220",
+ title: "RTCRtpSender.getStats() and RTCRtpReceiver.getStats()",
+ visible: true
+ });
+
+ const checkStats = (sndReport, rcvReport, kind) => {
+ ok(sndReport instanceof window.RTCStatsReport, "sender stats are a RTCStatsReport");
+ ok(rcvReport instanceof window.RTCStatsReport, "receiver stats are a RTCStatsReport");
+ // Returns SSRCs and performs some checks
+ let getSsrcs = (report, kind) => {
+ return [...report.values()]
+ .filter(stat => stat.type.endsWith("bound-rtp")).map(stat =>{
+ isnot(parseInt(stat.id, 16), NaN,
+ `id ${stat.id} is an opaque (hex) number`);
+ is(stat.kind, kind, "kind of " + stat.id
+ + " is expected type " + kind);
+ return stat.ssrc;
+ }).sort().join("|");
+ };
+ let sndSsrcs = getSsrcs(sndReport, kind);
+ let rcvSsrcs = getSsrcs(rcvReport, kind);
+ ok(sndSsrcs, "sender SSRCs is not empty");
+ ok(rcvSsrcs, "receiver SSRCs is not empty");
+ is(sndSsrcs, rcvSsrcs, "sender SSRCs match receiver SSRCs");
+ };
+
+ // This MUST be run after PC_*_WAIT_FOR_MEDIA_FLOW to ensure that we have RTP
+ // before checking for RTCP.
+ // It will throw UnsyncedRtcpError if it times out waiting for sync.
+
+ runNetworkTest(function (options) {
+ test = new PeerConnectionTest(options);
+ test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW",
+ async function PC_LOCAL_AND_REMOTE_CHECK_SENDER_RECEIVER_STATS(test) {
+ await Promise.all([
+ waitForSyncedRtcp(test.pcLocal._pc),
+ waitForSyncedRtcp(test.pcRemote._pc),
+ ]);
+ let senders = test.pcLocal.getSenders();
+ let receivers = test.pcRemote.getReceivers();
+ is(senders.length, 2, "Have exactly two senders.");
+ is(receivers.length, 2, "Have exactly two receivers.");
+ for(let kind of ["audio", "video"]) {
+ let senderStats =
+ await senders.find(s => s.track.kind == kind).getStats();
+ is(senders.filter(s => s.track.kind == kind).length, 1,
+ "Exactly 1 sender of kind " + kind);
+ let receiverStats =
+ await receivers.find(r => r.track.kind == kind).getStats();
+ is(receivers.filter(r => r.track.kind == kind).length, 1,
+ "Exactly 1 receiver of kind " + kind);
+
+ checkStats(senderStats, receiverStats, kind);
+ }
+ }
+ );
+ test.setMediaConstraints([{audio: true}, {video: true}], []);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalAnswerInHaveLocalOffer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalAnswerInHaveLocalOffer.html
new file mode 100644
index 0000000000..07cdd7d6bd
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalAnswerInHaveLocalOffer.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "784519",
+ title: "setLocalDescription (answer) in 'have-local-offer'"
+ });
+
+runNetworkTest(function () {
+ const test = new PeerConnectionTest();
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.chain.removeAfter("PC_LOCAL_SET_LOCAL_DESCRIPTION");
+
+ test.chain.append([
+ function PC_LOCAL_SET_LOCAL_ANSWER(test) {
+ test.pcLocal._latest_offer.type = "answer";
+ return test.pcLocal.setLocalDescriptionAndFail(test.pcLocal._latest_offer)
+ .then(err => {
+ is(err.name, "InvalidModificationError", "Error is InvalidModificationError");
+ });
+ }
+ ]);
+
+ return test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalAnswerInStable.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalAnswerInStable.html
new file mode 100644
index 0000000000..e57c0640f4
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalAnswerInStable.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "784519",
+ title: "setLocalDescription (answer) in 'stable'"
+ });
+
+runNetworkTest(function () {
+ const test = new PeerConnectionTest();
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.chain.removeAfter("PC_LOCAL_CREATE_OFFER");
+
+ test.chain.append([
+ function PC_LOCAL_SET_LOCAL_ANSWER(test) {
+ test.pcLocal._latest_offer.type = "answer";
+ return test.pcLocal.setLocalDescriptionAndFail(test.pcLocal._latest_offer)
+ .then(err => {
+ is(err.name, "InvalidModificationError", "Error is InvalidModificationError");
+ });
+ }
+ ]);
+
+ return test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalOfferInHaveRemoteOffer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalOfferInHaveRemoteOffer.html
new file mode 100644
index 0000000000..bd98a83635
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setLocalOfferInHaveRemoteOffer.html
@@ -0,0 +1,31 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "784519",
+ title: "setLocalDescription (offer) in 'have-remote-offer'"
+ });
+
+runNetworkTest(function () {
+ const test = new PeerConnectionTest();
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.chain.removeAfter("PC_REMOTE_SET_REMOTE_DESCRIPTION");
+
+ test.chain.append([
+ async function PC_REMOTE_SET_LOCAL_OFFER(test) {
+ const err = await test.pcRemote.setLocalDescriptionAndFail(test.pcLocal._latest_offer);
+ is(err.name, "InvalidModificationError", "Error is InvalidModificationError");
+ }
+ ]);
+
+ return test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters.html
new file mode 100644
index 0000000000..5df97e39f5
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters.html
@@ -0,0 +1,470 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1230184",
+ title: "Set parameters on sender",
+ visible: true
+});
+
+const simulcastOffer = `v=0
+o=- 3840232462471583827 0 IN IP4 127.0.0.1
+s=-
+t=0 0
+a=group:BUNDLE 0
+a=msid-semantic: WMS
+m=video 9 UDP/TLS/RTP/SAVPF 96
+c=IN IP4 0.0.0.0
+a=rtcp:9 IN IP4 0.0.0.0
+a=ice-ufrag:Li6+
+a=ice-pwd:3C05CTZBRQVmGCAq7hVasHlT
+a=ice-options:trickle
+a=fingerprint:sha-256 5B:D3:8E:66:0E:7D:D3:F3:8E:E6:80:28:19:FC:55:AD:58:5D:B9:3D:A8:DE:45:4A:E7:87:02:F8:3C:0B:3B:B3
+a=setup:actpass
+a=mid:0
+a=extmap:4 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id
+a=recvonly
+a=rtcp-mux
+a=rtpmap:96 VP8/90000
+a=rtcp-fb:96 goog-remb
+a=rtcp-fb:96 transport-cc
+a=rtcp-fb:96 ccm fir
+a=rid:foo recv
+a=rid:bar recv
+a=simulcast:recv foo;bar
+`;
+
+function buildMaximumSendEncodings() {
+ const sendEncodings = [];
+ while (true) {
+ // isDeeply does not see identical string primitives and String objects
+ // as the same, so we make this a string primitive.
+ sendEncodings.push({rid: `${sendEncodings.length}`});
+ const pc = new RTCPeerConnection();
+ const {sender} = pc.addTransceiver('video', {sendEncodings});
+ const {encodings} = sender.getParameters();
+ if (encodings.length < sendEncodings.length) {
+ sendEncodings.pop();
+ return sendEncodings;
+ }
+ }
+}
+
+async function queueAWebrtcTask() {
+ const pc = new RTCPeerConnection();
+ pc.addTransceiver('audio');
+ await new Promise(r => pc.onnegotiationneeded = r);
+ pc.close();
+}
+
+// setParameters is mostly tested in wpt, but we test a few
+// implementation-specific things here. Other mochitests check whether the
+// set parameters actually have the desired effect on the media streams.
+const tests = [
+
+ // wpt currently does not assume support for 3 encodings, which limits the
+ // effectiveness of its powers-of-2 test (since it can test only for 1 and 2)
+ async function checkScaleResolutionDownByAutoFillPowersOf2() {
+ const pc = new RTCPeerConnection();
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [{rid: "0"},{rid: "1"},{rid: "2"}]
+ });
+ const {encodings} = sender.getParameters();
+ const scaleValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy);
+ isDeeply(scaleValues, [4, 2, 1]);
+ },
+
+ // wpt currently does not assume support for 3 encodings, which limits the
+ // effectiveness of its fill-with-1 test
+ async function checkScaleResolutionDownByAutoFillWith1() {
+ const pc = new RTCPeerConnection();
+ const {sender} = pc.addTransceiver('video', {
+ sendEncodings: [
+ {rid: "0"},{rid: "1", scaleResolutionDownBy: 3},{rid: "2"}
+ ]
+ });
+ const {encodings} = sender.getParameters();
+ const scaleValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy);
+ isDeeply(scaleValues, [1, 3, 1]);
+ },
+
+ async function checkVideoEncodingLimit() {
+ const pc = new RTCPeerConnection();
+ const maxSendEncodings = buildMaximumSendEncodings();
+ const sendEncodings = maxSendEncodings.concat({rid: "a"});
+ const {sender} = pc.addTransceiver('video', {sendEncodings});
+ const {encodings} = sender.getParameters();
+
+ const rids = encodings.map(({rid}) => rid);
+ const expectedRids = maxSendEncodings.map(({rid}) => rid);
+ isDeeply(rids, expectedRids);
+
+ const scaleValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy);
+ const expectedScaleValues = [];
+ let scale = 1;
+ while (expectedScaleValues.length < maxSendEncodings.length) {
+ expectedScaleValues.push(scale);
+ scale *= 2;
+ }
+ isDeeply(scaleValues, expectedScaleValues.reverse());
+ },
+
+ async function checkScaleDownByInTrimmedEncoding() {
+ const pc = new RTCPeerConnection();
+ const maxSendEncodings = buildMaximumSendEncodings();
+ const sendEncodings = maxSendEncodings.concat({rid: "a", scaleResolutionDownBy: 3});
+ const {sender} = pc.addTransceiver('video', {sendEncodings});
+ const {encodings} = sender.getParameters();
+ const rids = encodings.map(({rid}) => rid);
+ const expectedRids = maxSendEncodings.map(({rid}) => rid);
+ isDeeply(rids, expectedRids);
+ const scaleValues = encodings.map(({scaleResolutionDownBy}) => scaleResolutionDownBy);
+ const expectedScaleValues = maxSendEncodings.map(() => 1);
+ isDeeply(scaleValues, expectedScaleValues);
+ },
+
+ async function checkLibwebrtcRidLengthLimit() {
+ const pc = new RTCPeerConnection();
+ try {
+ pc.addTransceiver('video', {
+ sendEncodings: [{rid: "wibblywobblyjeremybearimy"}]}
+ );
+ ok(false, "Rid should be too long for libwebrtc!");
+ } catch (e) {
+ is(e.name, "TypeError",
+ "Rid that is too long for libwebrtc should result in a TypeError");
+ }
+ },
+
+ async function checkErrorsInTrimmedEncodings() {
+ const pc = new RTCPeerConnection();
+ const maxSendEncodings = buildMaximumSendEncodings();
+ try {
+ const sendEncodings = maxSendEncodings.concat({rid: "foo-bar"});
+ pc.addTransceiver('video', { sendEncodings });
+ ok(false, "Should throw due to invalid rid characters");
+ } catch (e) {
+ is(e.name, "TypeError")
+ }
+ try {
+ const sendEncodings = maxSendEncodings.concat({rid: "wibblywobblyjeremybearimy"});
+ pc.addTransceiver('video', { sendEncodings });
+ ok(false, "Should throw because rid too long");
+ } catch (e) {
+ is(e.name, "TypeError")
+ }
+ try {
+ const sendEncodings = maxSendEncodings.concat({scaleResolutionDownBy: 2});
+ pc.addTransceiver('video', { sendEncodings });
+ ok(false, "Should throw due to missing rid");
+ } catch (e) {
+ is(e.name, "TypeError")
+ }
+ try {
+ const sendEncodings = maxSendEncodings.concat(maxSendEncodings[0]);
+ pc.addTransceiver('video', { sendEncodings });
+ ok(false, "Should throw due to duplicate rid");
+ } catch (e) {
+ is(e.name, "TypeError")
+ }
+ try {
+ const sendEncodings = maxSendEncodings.concat({rid: maxSendEncodings.length, scaleResolutionDownBy: 0});
+ pc.addTransceiver('video', { sendEncodings });
+ ok(false, "Should throw due to invalid scaleResolutionDownBy");
+ } catch (e) {
+ is(e.name, "RangeError")
+ }
+ try {
+ const sendEncodings = maxSendEncodings.concat({rid: maxSendEncodings.length, maxFramerate: -1});
+ pc.addTransceiver('video', { sendEncodings });
+ ok(false, "Should throw due to invalid maxFramerate");
+ } catch (e) {
+ is(e.name, "RangeError")
+ }
+ },
+
+ async function checkCompatModeUnicastSetParametersAllowsSimulcastOffer() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", true]);
+ const pc1 = new RTCPeerConnection();
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ const parameters = sender.getParameters();
+ parameters.encodings[0].scaleResolutionDownBy = 3.0;
+ await sender.setParameters(parameters);
+
+ await pc1.setRemoteDescription({type: "offer", sdp: simulcastOffer});
+
+ const {encodings} = sender.getParameters();
+ const rids = encodings.map(({rid}) => rid);
+ isDeeply(rids, ["foo", "bar"]);
+ is(encodings[0].scaleResolutionDownBy, 2.0);
+ is(encodings[1].scaleResolutionDownBy, 1.0);
+ },
+
+ async function checkCompatModeUnicastSetParametersInterruptAllowsSimulcastOffer() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", true]);
+ const pc1 = new RTCPeerConnection();
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ const parameters = sender.getParameters();
+ parameters.encodings[0].scaleResolutionDownBy = 3.0;
+
+ const offerDone = pc1.setRemoteDescription({type: "offer", sdp: simulcastOffer});
+ await sender.setParameters(parameters);
+ await offerDone;
+
+ const {encodings} = sender.getParameters();
+ const rids = encodings.map(({rid}) => rid);
+ isDeeply(rids, ["foo", "bar"]);
+ is(encodings[0].scaleResolutionDownBy, 2.0);
+ is(encodings[1].scaleResolutionDownBy, 1.0);
+ },
+
+ async function checkCompatModeSimulcastSetParametersSetsSimulcastEnvelope() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", true]);
+ const pc1 = new RTCPeerConnection();
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ const parameters = sender.getParameters();
+ parameters.encodings[0].rid = "1";
+ parameters.encodings.push({rid: "2"});
+ await sender.setParameters(parameters);
+
+ await pc1.setRemoteDescription({type: "offer", sdp: simulcastOffer});
+
+ const {encodings} = sender.getParameters();
+ const rids = encodings.map(({rid}) => rid);
+ // No overlap in rids -> unicast
+ isDeeply(rids, [undefined]);
+ },
+
+ async function checkCompatModeSimulcastSetParametersRacesLocalUnicastOffer() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", true]);
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ // unicast offer
+ const offer = await pc1.createOffer();
+ const aTask = queueAWebrtcTask();
+ const sldPromise = pc1.setLocalDescription(offer);
+
+ // Right now, we have aTask queued. The success task for sLD is not queued
+ // yet, because Firefox performs the initial steps on the microtask queue,
+ // which we have not allowed to run yet. Awaiting aTask will first clear
+ // the microtask queue, then run the task queue until aTask is finished.
+ // That _should_ result in the success task for sLD(offer) being queued.
+ await aTask;
+
+ const parameters = sender.getParameters();
+ parameters.encodings[0].rid = "foo";
+ parameters.encodings.push({rid: "bar"});
+ // simulcast setParameters; the task to update [[SendEncodings]] should be
+ // queued after the success task for sLD(offer)
+ await sender.setParameters(parameters);
+ await sldPromise;
+
+ const {encodings} = sender.getParameters();
+ const rids = encodings.map(({rid}) => rid);
+ // Compat mode lets this slide, but won't try to negotiate it since we've
+ // already applied a unicast local offer.
+ isDeeply(rids, ["foo", "bar"]);
+
+ // Let negotiation finish, so we can generate a new offer
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+
+ const reoffer = await pc1.createOffer();
+ ok(!reoffer.sdp.includes("a=simulcast"), "reoffer should be unicast");
+ },
+
+ async function checkCompatModeSimulcastSetParametersRacesRemoteOffer() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", true]);
+ const pc1 = new RTCPeerConnection();
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ const parameters = sender.getParameters();
+ parameters.encodings[0].rid = "foo";
+ parameters.encodings.push({rid: "bar"});
+ const p = sender.setParameters(parameters);
+ await pc1.setRemoteDescription({type: "offer", sdp: simulcastOffer});
+ await p;
+ const answer = await pc1.createAnswer();
+
+ const {encodings} = sender.getParameters();
+ const rids = encodings.map(({rid}) => rid);
+ isDeeply(rids, ["foo", "bar"]);
+ ok(answer.sdp.includes("a=simulcast:send foo;bar"), "answer should be simulcast");
+ },
+
+ async function checkCompatModeSimulcastSetParametersRacesLocalAnswer() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", true]);
+ // We do an initial negotiation, and while the local answer is pending,
+ // perform a setParameters on a not-yet-negotiated video sender. The intent
+ // here is to have the success task for sLD(answer) run while the
+ // setParameters is pending.
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const audioStream = await navigator.mediaDevices.getUserMedia({audio: true});
+ // We use this later on, but set it up now so we don't inadvertently
+ // crank the event loop more than we intend below.
+ const videoStream = await navigator.mediaDevices.getUserMedia({video: true});
+ pc2.addTrack(audioStream.getTracks()[0]);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+ const answer = await pc1.createAnswer();
+ const aTask = queueAWebrtcTask();
+ const sldPromise = pc1.setLocalDescription(answer);
+
+ // Right now, we have aTask queued. The success task for sLD is not queued
+ // yet, because Firefox performs the initial steps on the microtask queue,
+ // which we have not allowed to run yet. Awaiting aTask will first clear
+ // the microtask queue, then run the task queue until aTask is finished.
+ // That _should_ result in the success task for sLD(answer) being queued.
+ await aTask;
+
+ // The success task for sLD(answer) should be queued now. Don't relinquish
+ // the event loop!
+
+ // New sender that has nothing to do with the negotiation in progress.
+ const sender = pc1.addTrack(videoStream.getTracks()[0]);
+ const parameters = sender.getParameters();
+ parameters.encodings[0].rid = "foo";
+ parameters.encodings.push({rid: "bar"});
+
+ // We have not relinquished the event loop, so the sLD(answer) should still
+ // be queued. The task that updates [[SendEncodings]] (from setParameters)
+ // should be queued behind it. Let them both run.
+ await sender.setParameters(parameters);
+ await sldPromise;
+
+ const offer = await pc1.createOffer();
+
+ const {encodings} = sender.getParameters();
+ const rids = encodings.map(({rid}) => rid);
+ isDeeply(rids, ["foo", "bar"]);
+ ok(offer.sdp.includes("a=simulcast:send foo;bar"), "offer should be simulcast");
+ },
+
+ async function checkCompatModeSimulcastSetParametersRacesRemoteAnswer() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", true]);
+ // We do an initial negotiation, and while the remote answer is pending,
+ // perform a setParameters on a not-yet-negotiated video sender. The intent
+ // here is to have the success task for sRD(answer) run while the
+ // setParameters is pending.
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ const audioStream = await navigator.mediaDevices.getUserMedia({audio: true});
+ // We use this later on, but set it up now so we don't inadvertently
+ // crank the event loop more than we intend below.
+ const videoStream = await navigator.mediaDevices.getUserMedia({video: true});
+ pc1.addTrack(audioStream.getTracks()[0]);
+ await pc1.setLocalDescription();
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ const aTask = queueAWebrtcTask();
+ const srdPromise = pc1.setRemoteDescription(pc2.localDescription);
+
+ // Right now, we have aTask queued. The success task for sRD is not queued
+ // yet, because Firefox performs the initial steps on the microtask queue,
+ // which we have not allowed to run yet. Awaiting aTask will first clear
+ // the microtask queue, then run the task queue until aTask is finished.
+ // That _should_ result in the success task for sRD(answer) being queued.
+ await aTask;
+
+ // The success task for sRD(answer) should be queued now. Don't relinquish
+ // the event loop!
+
+ const sender = pc1.addTrack(videoStream.getTracks()[0]);
+ const parameters = sender.getParameters();
+ parameters.encodings[0].rid = "foo";
+ parameters.encodings.push({rid: "bar"});
+
+ // We have not relinquished the event loop, so the sRD(answer) should still
+ // be queued. The task that updates [[SendEncodings]] (from setParameters)
+ // should be queued behind it. Let them both run.
+ await sender.setParameters(parameters);
+ await srdPromise;
+
+ const offer = await pc1.createOffer();
+
+ const {encodings} = sender.getParameters();
+ const rids = encodings.map(({rid}) => rid);
+ isDeeply(rids, ["foo", "bar"]);
+ ok(offer.sdp.includes("a=simulcast:send foo;bar"), "offer should be simulcast");
+ },
+
+ async function checkCompatModeSimulcastRidlessSetParametersRacesLocalOffer() {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", true]);
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ const stream = await navigator.mediaDevices.getUserMedia({video: true});
+ const sender = pc1.addTrack(stream.getTracks()[0]);
+ // unicast offer
+ const aTask = queueAWebrtcTask();
+ const sldPromise = pc1.setLocalDescription();
+
+ // Right now, we have aTask queued. The success task for sLD is not queued
+ // yet, because Firefox performs the initial steps on the microtask queue,
+ // which we have not allowed to run yet. Awaiting aTask will first clear
+ // the microtask queue, then run the task queue until aTask is finished.
+ // That _should_ result in the success task for sLD(offer) being queued.
+ await aTask;
+
+ // simulcast setParameters; the task to update [[SendEncodings]] should be
+ // queued after the success task for sLD(offer)
+ try {
+ await sender.setParameters({"encodings": [{}, {}]});
+ ok(false, "setParameters with two ridless encodings should fail");
+ } catch (e) {
+ ok(true, "setParameters with two ridless encodings should fail");
+ }
+ await sldPromise;
+
+ const {encodings} = sender.getParameters();
+ const rids = encodings.map(({rid}) => rid);
+ // Compat mode lets this slide, but won't try to negotiate it since we've
+ // already applied a unicast local offer.
+ isDeeply(rids, [undefined]);
+
+ // Let negotiation finish, so we can generate a new offer
+ await pc2.setRemoteDescription(pc1.localDescription);
+ await pc2.setLocalDescription();
+ await pc1.setRemoteDescription(pc2.localDescription);
+
+ const reoffer = await pc1.createOffer();
+ ok(!reoffer.sdp.includes("a=simulcast"), "reoffer should be unicast");
+ },
+
+];
+
+runNetworkTest(async () => {
+ await pushPrefs(
+ ["media.peerconnection.allow_old_setParameters", false]);
+ for (const test of tests) {
+ info(`Running test: ${test.name}`);
+ await test();
+ info(`Done running test: ${test.name}`);
+ }
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_maxFramerate.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_maxFramerate.html
new file mode 100644
index 0000000000..8047719775
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_maxFramerate.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1611957",
+ title: "Live-updating maxFramerate"
+});
+
+let sender, receiver;
+
+async function checkMaxFrameRate(rate) {
+ const parameters = sender.getParameters();
+ parameters.encodings[0].maxFramerate = rate;
+ await sender.setParameters(parameters);
+ await wait(2000);
+ const stats = Array.from((await receiver.getStats()).values());
+ const inboundRtp = stats.find(stat => stat.type == "inbound-rtp");
+ info(`inbound-rtp stats: ${JSON.stringify(inboundRtp)}`);
+ const fps = inboundRtp.framesPerSecond;
+ ok(fps <= (rate * 1.1) + 1,
+ `fps is an appropriate value (${fps}) for rate (${rate})`);
+}
+
+runNetworkTest(async function (options) {
+ let test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{video: true}], []);
+ test.chain.append([
+ function CHECK_PRECONDITIONS() {
+ is(test.pcLocal._pc.getSenders().length, 1,
+ "Should have 1 local sender");
+ is(test.pcRemote._pc.getReceivers().length, 1,
+ "Should have 1 remote receiver");
+
+ sender = test.pcLocal._pc.getSenders()[0];
+ receiver = test.pcRemote._pc.getReceivers()[0];
+ },
+ function PC_LOCAL_SET_MAX_FRAMERATE_2() {
+ return checkMaxFrameRate(2);
+ },
+ function PC_LOCAL_SET_MAX_FRAMERATE_4() {
+ return checkMaxFrameRate(4);
+ },
+ function PC_LOCAL_SET_MAX_FRAMERATE_15() {
+ return checkMaxFrameRate(15);
+ },
+ function PC_LOCAL_SET_MAX_FRAMERATE_8() {
+ return checkMaxFrameRate(8);
+ },
+ function PC_LOCAL_SET_MAX_FRAMERATE_1() {
+ return checkMaxFrameRate(1);
+ },
+ ]);
+ await test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_maxFramerate_oldSetParameters.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_maxFramerate_oldSetParameters.html
new file mode 100644
index 0000000000..9c68a31c0a
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_maxFramerate_oldSetParameters.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1611957",
+ title: "Live-updating maxFramerate"
+});
+
+let sender, receiver;
+
+async function checkMaxFrameRate(rate) {
+ sender.setParameters({ encodings: [{ maxFramerate: rate }] });
+ await wait(2000);
+ const stats = Array.from((await receiver.getStats()).values());
+ const inboundRtp = stats.find(stat => stat.type == "inbound-rtp");
+ info(`inbound-rtp stats: ${JSON.stringify(inboundRtp)}`);
+ const fps = inboundRtp.framesPerSecond;
+ ok(fps <= (rate * 1.1) + 1, `fps is an appropriate value (${fps}) for rate (${rate})`);
+}
+
+runNetworkTest(async function (options) {
+ let test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{video: true}], []);
+ test.chain.append([
+ function CHECK_PRECONDITIONS() {
+ is(test.pcLocal._pc.getSenders().length, 1,
+ "Should have 1 local sender");
+ is(test.pcRemote._pc.getReceivers().length, 1,
+ "Should have 1 remote receiver");
+
+ sender = test.pcLocal._pc.getSenders()[0];
+ receiver = test.pcRemote._pc.getReceivers()[0];
+ },
+ function PC_LOCAL_SET_MAX_FRAMERATE_2() {
+ return checkMaxFrameRate(2);
+ },
+ function PC_LOCAL_SET_MAX_FRAMERATE_4() {
+ return checkMaxFrameRate(4);
+ },
+ function PC_LOCAL_SET_MAX_FRAMERATE_15() {
+ return checkMaxFrameRate(15);
+ },
+ function PC_LOCAL_SET_MAX_FRAMERATE_8() {
+ return checkMaxFrameRate(8);
+ },
+ function PC_LOCAL_SET_MAX_FRAMERATE_1() {
+ return checkMaxFrameRate(1);
+ },
+ ]);
+ await test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_oldSetParameters.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_oldSetParameters.html
new file mode 100644
index 0000000000..2b55ec46e6
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_oldSetParameters.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1230184",
+ title: "Set parameters on sender",
+ visible: true
+});
+
+function parameterstest(pc) {
+ ok(pc.getSenders().length, "have senders");
+ var sender = pc.getSenders()[0];
+
+ var testParameters = (params, errorName, errorMsg) => {
+ info("Trying to set " + JSON.stringify(params));
+
+ var validateParameters = (a, b) => {
+ var validateEncoding = (a, b) => {
+ is(a.rid, b.rid, "same rid");
+ is(a.maxBitrate, b.maxBitrate, "same maxBitrate");
+ is(a.maxFramerate, b.maxFramerate, "same maxFramerate");
+ is(a.scaleResolutionDownBy, b.scaleResolutionDownBy,
+ "same scaleResolutionDownBy");
+ };
+ is(a.encodings.length, (b.encodings || []).length, "same encodings");
+ a.encodings.forEach((en, i) => validateEncoding(en, b.encodings[i]));
+ };
+
+ var before = JSON.stringify(sender.getParameters());
+ isnot(JSON.stringify(params), before, "starting condition");
+
+ var p = sender.setParameters(params)
+ .then(() => {
+ isnot(JSON.stringify(sender.getParameters()), before, "parameters changed");
+ validateParameters(sender.getParameters(), params);
+ is(null, errorName || null, "is success expected");
+ }, e => {
+ is(e.name, errorName, "correct error name");
+ is(e.message, errorMsg, "correct error message");
+ });
+ is(JSON.stringify(sender.getParameters()), before, "parameters not set yet");
+ return p;
+ };
+
+ return [
+ [{ encodings: [ { rid: "foo", maxBitrate: 40000, scaleResolutionDownBy: 2 },
+ { rid: "bar", maxBitrate: 10000, scaleResolutionDownBy: 4 }]
+ }],
+ [{ encodings: [{ maxBitrate: 10000, scaleResolutionDownBy: 4 }]}],
+ [{ encodings: [{ maxFramerate: 0.0, scaleResolutionDownBy: 1 }]}],
+ [{ encodings: [{ maxFramerate: 30.5, scaleResolutionDownBy: 1 }]}],
+ [{ encodings: [{ maxFramerate: -1, scaleResolutionDownBy: 1 }]}, "RangeError", "maxFramerate must be non-negative"],
+ [{ encodings: [{ maxBitrate: 40000 },
+ { rid: "bar", maxBitrate: 10000 }] }, "TypeError", "Missing rid"],
+ [{ encodings: [{ rid: "foo", maxBitrate: 40000 },
+ { rid: "bar", maxBitrate: 10000 },
+ { rid: "bar", maxBitrate: 20000 }] }, "TypeError", "Duplicate rid"],
+ [{}, "TypeError", `RTCRtpSender.setParameters: Missing required 'encodings' member of RTCRtpSendParameters.`]
+ ].reduce((p, args) => p.then(() => testParameters.apply(this, args)),
+ Promise.resolve());
+}
+
+runNetworkTest(() => {
+ const test = new PeerConnectionTest();
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+ test.chain.removeAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW");
+
+ // Test sender parameters.
+ test.chain.append([
+ function PC_LOCAL_SET_PARAMETERS(test) {
+ return parameterstest(test.pcLocal._pc);
+ }
+ ]);
+
+ return test.run();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_scaleResolutionDownBy.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_scaleResolutionDownBy.html
new file mode 100644
index 0000000000..d1275d6523
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_scaleResolutionDownBy.html
@@ -0,0 +1,98 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1253499",
+ title: "Live-updating scaleResolutionDownBy"
+});
+
+async function checkForH264Support() {
+ const pc = new RTCPeerConnection();
+ const offer = await pc.createOffer({offerToReceiveVideo: true});
+ return offer.sdp.match(/a=rtpmap:[1-9][0-9]* H264/);
+}
+
+let sender, localElem, remoteElem;
+let originalWidth, originalHeight;
+let resolutionAlignment = 1;
+
+async function checkScaleDownBy(scale) {
+ const parameters = sender.getParameters();
+ parameters.encodings[0].scaleResolutionDownBy = scale;
+ await sender.setParameters(parameters);
+ await haveEvent(remoteElem, "resize", wait(5000, new Error("Timeout")));
+
+ // Find the expected resolution. Internally we floor the exact scaling, then
+ // shrink each dimension to the alignment requested by the encoder.
+ let expectedWidth =
+ originalWidth / scale - (originalWidth / scale % resolutionAlignment);
+ let expectedHeight =
+ originalHeight / scale - (originalHeight / scale % resolutionAlignment);
+
+ is(remoteElem.videoWidth, expectedWidth,
+ `Width should have scaled down by ${scale}`);
+ is(remoteElem.videoHeight, expectedHeight,
+ `Height should have scaled down by ${scale}`);
+}
+
+runNetworkTest(async function (options) {
+ await pushPrefs(['media.peerconnection.video.lock_scaling', true]);
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ if (await checkForH264Support()) {
+ // Android only has h264 in hw, so now we know it will use vp8 in hw too.
+ resolutionAlignment = 16;
+ }
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ let test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{video: true}], []);
+ test.chain.append([
+ function CHECK_PRECONDITIONS() {
+ is(test.pcLocal._pc.getSenders().length, 1,
+ "Should have 1 local sender");
+ is(test.pcLocal.localMediaElements.length, 1,
+ "Should have 1 local sending media element");
+ is(test.pcRemote.remoteMediaElements.length, 1,
+ "Should have 1 remote media element");
+
+ sender = test.pcLocal._pc.getSenders()[0];
+ localElem = test.pcLocal.localMediaElements[0];
+ remoteElem = test.pcRemote.remoteMediaElements[0];
+
+ remoteElem.addEventListener("resize", () =>
+ info(`Video resized to ${remoteElem.videoWidth}x${remoteElem.videoHeight}`));
+
+ originalWidth = localElem.videoWidth;
+ originalHeight = localElem.videoHeight;
+ info(`Original width is ${originalWidth}`);
+ },
+ function PC_LOCAL_SCALEDOWNBY_2() {
+ return checkScaleDownBy(2);
+ },
+ function PC_LOCAL_SCALEDOWNBY_4() {
+ return checkScaleDownBy(4);
+ },
+ function PC_LOCAL_SCALEDOWNBY_15() {
+ return checkScaleDownBy(15);
+ },
+ function PC_LOCAL_SCALEDOWNBY_8() {
+ return checkScaleDownBy(8);
+ },
+ function PC_LOCAL_SCALEDOWNBY_1() {
+ return checkScaleDownBy(1);
+ },
+ ]);
+ await test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_scaleResolutionDownBy_oldSetParameters.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_scaleResolutionDownBy_oldSetParameters.html
new file mode 100644
index 0000000000..4d515bd5c1
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setParameters_scaleResolutionDownBy_oldSetParameters.html
@@ -0,0 +1,96 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1253499",
+ title: "Live-updating scaleResolutionDownBy"
+});
+
+async function checkForH264Support() {
+ const pc = new RTCPeerConnection();
+ const offer = await pc.createOffer({offerToReceiveVideo: true});
+ return offer.sdp.match(/a=rtpmap:[1-9][0-9]* H264/);
+}
+
+let sender, localElem, remoteElem;
+let originalWidth, originalHeight;
+let resolutionAlignment = 1;
+
+async function checkScaleDownBy(scale) {
+ sender.setParameters({ encodings: [{ scaleResolutionDownBy: scale }] });
+ await haveEvent(remoteElem, "resize", wait(5000, new Error("Timeout")));
+
+ // Find the expected resolution. Internally we floor the exact scaling, then
+ // shrink each dimension to the alignment requested by the encoder.
+ let expectedWidth =
+ originalWidth / scale - (originalWidth / scale % resolutionAlignment);
+ let expectedHeight =
+ originalHeight / scale - (originalHeight / scale % resolutionAlignment);
+
+ is(remoteElem.videoWidth, expectedWidth,
+ `Width should have scaled down by ${scale}`);
+ is(remoteElem.videoHeight, expectedHeight,
+ `Height should have scaled down by ${scale}`);
+}
+
+runNetworkTest(async function (options) {
+ await pushPrefs(['media.peerconnection.video.lock_scaling', true]);
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ if (await checkForH264Support()) {
+ // Android only has h264 in hw, so now we know it will use vp8 in hw too.
+ resolutionAlignment = 16;
+ }
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ let test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{video: true}], []);
+ test.chain.append([
+ function CHECK_PRECONDITIONS() {
+ is(test.pcLocal._pc.getSenders().length, 1,
+ "Should have 1 local sender");
+ is(test.pcLocal.localMediaElements.length, 1,
+ "Should have 1 local sending media element");
+ is(test.pcRemote.remoteMediaElements.length, 1,
+ "Should have 1 remote media element");
+
+ sender = test.pcLocal._pc.getSenders()[0];
+ localElem = test.pcLocal.localMediaElements[0];
+ remoteElem = test.pcRemote.remoteMediaElements[0];
+
+ remoteElem.addEventListener("resize", () =>
+ info(`Video resized to ${remoteElem.videoWidth}x${remoteElem.videoHeight}`));
+
+ originalWidth = localElem.videoWidth;
+ originalHeight = localElem.videoHeight;
+ info(`Original width is ${originalWidth}`);
+ },
+ function PC_LOCAL_SCALEDOWNBY_2() {
+ return checkScaleDownBy(2);
+ },
+ function PC_LOCAL_SCALEDOWNBY_4() {
+ return checkScaleDownBy(4);
+ },
+ function PC_LOCAL_SCALEDOWNBY_15() {
+ return checkScaleDownBy(15);
+ },
+ function PC_LOCAL_SCALEDOWNBY_8() {
+ return checkScaleDownBy(8);
+ },
+ function PC_LOCAL_SCALEDOWNBY_1() {
+ return checkScaleDownBy(1);
+ },
+ ]);
+ await test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteAnswerInHaveRemoteOffer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteAnswerInHaveRemoteOffer.html
new file mode 100644
index 0000000000..1912835160
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteAnswerInHaveRemoteOffer.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "784519",
+ title: "setRemoteDescription (answer) in 'have-remote-offer'"
+ });
+
+runNetworkTest(function () {
+ const test = new PeerConnectionTest();
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.chain.removeAfter("PC_REMOTE_SET_REMOTE_DESCRIPTION");
+
+ test.chain.append([
+ function PC_REMOTE_SET_REMOTE_ANSWER(test) {
+ test.pcLocal._latest_offer.type = "answer";
+ test.pcRemote._pc.setRemoteDescription(test.pcLocal._latest_offer)
+ .then(generateErrorCallback('setRemoteDescription should fail'),
+ err =>
+ is(err.name, "InvalidStateError", "Error is InvalidStateError"));
+ }
+ ]);
+
+ return test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteAnswerInStable.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteAnswerInStable.html
new file mode 100644
index 0000000000..6208fdea3e
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteAnswerInStable.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "784519",
+ title: "setRemoteDescription (answer) in 'stable'"
+ });
+
+runNetworkTest(function () {
+ const test = new PeerConnectionTest();
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.chain.removeAfter("PC_LOCAL_CREATE_OFFER");
+
+ test.chain.append([
+ function PC_LOCAL_SET_REMOTE_ANSWER(test) {
+ test.pcLocal._latest_offer.type = "answer";
+ test.pcLocal._pc.setRemoteDescription(test.pcLocal._latest_offer)
+ .then(generateErrorCallback('setRemoteDescription should fail'),
+ err =>
+ is(err.name, "InvalidStateError", "Error is InvalidStateError"));
+ }
+ ]);
+
+ return test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteOfferInHaveLocalOffer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteOfferInHaveLocalOffer.html
new file mode 100644
index 0000000000..20236f442c
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_setRemoteOfferInHaveLocalOffer.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "784519",
+ title: "setRemoteDescription (offer) in 'have-local-offer'"
+ });
+
+runNetworkTest(function () {
+ const test = new PeerConnectionTest();
+ test.setMediaConstraints([{audio: true}], [{audio: true}]);
+ test.chain.removeAfter("PC_LOCAL_SET_LOCAL_DESCRIPTION");
+
+ test.chain.append([
+ async function PC_LOCAL_SET_REMOTE_OFFER(test) {
+ const p = test.pcLocal._pc.setRemoteDescription(test.pcLocal._latest_offer);
+ await new Promise(r => test.pcLocal.onsignalingstatechange = r);
+ is(test.pcLocal._pc.signalingState, 'stable', 'should fire stable');
+ await new Promise(r => test.pcLocal.onsignalingstatechange = r);
+ is(test.pcLocal._pc.signalingState, 'have-remote-offer',
+ 'should fire have-remote-offer');
+ await p;
+ ok(true, 'setRemoteDescription should succeed');
+ }
+ ]);
+
+ return test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer.html
new file mode 100644
index 0000000000..ba75c72022
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer.html
@@ -0,0 +1,121 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script>
+ <script type="application/javascript" src="simulcast.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1231507",
+ title: "Basic video-only peer connection with Simulcast answer",
+ visible: true
+ });
+
+ runNetworkTest(async () => {
+ await pushPrefs(
+ // 180Kbps was determined empirically, set well-higher than
+ // the 80Kbps+overhead needed for the two simulcast streams.
+ // 100Kbps was apparently too low.
+ ['media.peerconnection.video.min_bitrate_estimate', 180*1000]);
+
+
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+
+ const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback());
+ answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback());
+
+ const metadataToBeLoaded = [];
+ offerer.ontrack = (e) => {
+ metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track));
+ };
+
+ // Two recv transceivers, one for each simulcast stream
+ offerer.addTransceiver('video', { direction: 'recvonly' });
+ offerer.addTransceiver('video', { direction: 'recvonly' });
+
+ const offer = await offerer.createOffer();
+
+ const mungedOffer = midToRid(offer);
+ info(`Transformed recv offer to simulcast: ${offer.sdp} to ${mungedOffer}`);
+
+ await answerer.setRemoteDescription({type: 'offer', sdp: mungedOffer});
+
+ // One send transceiver, that will be used to send both simulcast streams
+ const emitter = new VideoFrameEmitter();
+ const videoStream = emitter.stream();
+ const sender = answerer.addTrack(videoStream.getVideoTracks()[0], videoStream);
+ let parameters = sender.getParameters();
+ is(parameters.encodings.length, 2);
+ is(answerer.getSenders().length, 1);
+ emitter.start();
+
+ await offerer.setLocalDescription(offer);
+
+ const rids = offerer.getTransceivers().map(t => t.mid);
+ is(rids.length, 2, 'Should have 2 mids in offer');
+ ok(rids[0] != '', 'First mid should be non-empty');
+ ok(rids[1] != '', 'Second mid should be non-empty');
+ info(`rids: ${JSON.stringify(rids)}`);
+
+ parameters = sender.getParameters();
+ info(`parameters: ${JSON.stringify(parameters)}`);
+ const observedRids = parameters.encodings.map(({rid}) => rid);
+ isDeeply(observedRids, rids);
+ parameters.encodings[0].maxBitrate = 40000;
+ parameters.encodings[0].scaleResolutionDownBy = 1;
+ parameters.encodings[1].maxBitrate = 40000;
+ parameters.encodings[1].scaleResolutionDownBy = 2;
+ await sender.setParameters(parameters);
+
+ const answer = await answerer.createAnswer();
+
+ const mungedAnswer = ridToMid(answer);
+ info(`Transformed send simulcast answer to multiple m-sections: ${answer.sdp} to ${mungedAnswer}`);
+ await offerer.setRemoteDescription({type: 'answer', sdp: mungedAnswer});
+ await answerer.setLocalDescription(answer);
+
+ is(metadataToBeLoaded.length, 2, 'Offerer should have gotten 2 ontrack events');
+ info('Waiting for 2 loadedmetadata events');
+ const videoElems = await Promise.all(metadataToBeLoaded);
+
+ const statsReady =
+ Promise.all([waitForSyncedRtcp(offerer), waitForSyncedRtcp(answerer)]);
+
+ const helper = new VideoStreamHelper();
+ info('Waiting for first video element to start playing');
+ await helper.checkVideoPlaying(videoElems[0]);
+ info('Waiting for second video element to start playing');
+ await helper.checkVideoPlaying(videoElems[1]);
+
+ is(videoElems[0].videoWidth, 50,
+ "sink is same width as source, modulo our cropping algorithm");
+ is(videoElems[0].videoHeight, 50,
+ "sink is same height as source, modulo our cropping algorithm");
+ is(videoElems[1].videoWidth, 25,
+ "sink is 1/2 width of source, modulo our cropping algorithm");
+ is(videoElems[1].videoHeight, 25,
+ "sink is 1/2 height of source, modulo our cropping algorithm");
+
+ await statsReady;
+ info("Stats ready");
+ const senderStats = await sender.getStats();
+ checkSenderStats(senderStats, 2);
+ checkExpectedFields(senderStats);
+ pedanticChecks(senderStats);
+
+ emitter.stop();
+ videoStream.getVideoTracks()[0].stop();
+ offerer.close();
+ answerer.close();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_lowResFirst.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_lowResFirst.html
new file mode 100644
index 0000000000..00c6e4ad3a
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_lowResFirst.html
@@ -0,0 +1,113 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script>
+ <script type="application/javascript" src="simulcast.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1231507",
+ title: "Basic video-only peer connection with Simulcast answer, first rid has lowest resolution",
+ visible: true
+ });
+
+ runNetworkTest(async () => {
+ await pushPrefs(
+ // 180Kbps was determined empirically, set well-higher than
+ // the 80Kbps+overhead needed for the two simulcast streams.
+ // 100Kbps was apparently too low.
+ ['media.peerconnection.video.min_bitrate_estimate', 180*1000]);
+
+
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+
+ const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback());
+ answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback());
+
+ const metadataToBeLoaded = [];
+ offerer.ontrack = (e) => {
+ metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track));
+ };
+
+ // Two recv transceivers, one for each simulcast stream
+ offerer.addTransceiver('video', { direction: 'recvonly' });
+ offerer.addTransceiver('video', { direction: 'recvonly' });
+
+ // One send transceiver, that will be used to send both simulcast streams
+ const emitter = new VideoFrameEmitter();
+ const videoStream = emitter.stream();
+ answerer.addTrack(videoStream.getVideoTracks()[0], videoStream);
+ emitter.start();
+
+ const offer = await offerer.createOffer();
+
+ const mungedOffer = midToRid(offer);
+ info(`Transformed recv offer to simulcast: ${offer.sdp} to ${mungedOffer}`);
+
+ await answerer.setRemoteDescription({type: 'offer', sdp: mungedOffer});
+ await offerer.setLocalDescription(offer);
+
+ const rids = offerer.getTransceivers().map(t => t.mid);
+ is(rids.length, 2, 'Should have 2 mids in offer');
+ ok(rids[0] != '', 'First mid should be non-empty');
+ ok(rids[1] != '', 'Second mid should be non-empty');
+ info(`rids: ${JSON.stringify(rids)}`);
+
+ const sender = answerer.getSenders()[0];
+ const parameters = sender.getParameters();
+ parameters.encodings[0].maxBitrate = 40000;
+ parameters.encodings[0].scaleResolutionDownBy = 2;
+ parameters.encodings[1].maxBitrate = 40000;
+ await sender.setParameters(parameters);
+
+ const answer = await answerer.createAnswer();
+
+ const mungedAnswer = ridToMid(answer);
+ info(`Transformed send simulcast answer to multiple m-sections: ${answer.sdp} to ${mungedAnswer}`);
+ await offerer.setRemoteDescription({type: 'answer', sdp: mungedAnswer});
+ await answerer.setLocalDescription(answer);
+
+ is(metadataToBeLoaded.length, 2, 'Offerer should have gotten 2 ontrack events');
+ info('Waiting for 2 loadedmetadata events');
+ const videoElems = await Promise.all(metadataToBeLoaded);
+
+ const statsReady =
+ Promise.all([waitForSyncedRtcp(offerer), waitForSyncedRtcp(answerer)]);
+
+ const helper = new VideoStreamHelper();
+ info('Waiting for first video element to start playing');
+ await helper.checkVideoPlaying(videoElems[0]);
+ info('Waiting for second video element to start playing');
+ await helper.checkVideoPlaying(videoElems[1]);
+
+ is(videoElems[1].videoWidth, 50,
+ "sink is same width as source, modulo our cropping algorithm");
+ is(videoElems[1].videoHeight, 50,
+ "sink is same height as source, modulo our cropping algorithm");
+ is(videoElems[0].videoWidth, 25,
+ "sink is 1/2 width of source, modulo our cropping algorithm");
+ is(videoElems[0].videoHeight, 25,
+ "sink is 1/2 height of source, modulo our cropping algorithm");
+
+ await statsReady;
+ const senderStats = await sender.getStats();
+ checkSenderStats(senderStats, 2);
+ checkExpectedFields(senderStats);
+ pedanticChecks(senderStats);
+
+ emitter.stop();
+ videoStream.getVideoTracks()[0].stop();
+ offerer.close();
+ answerer.close();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_lowResFirst_oldSetParameters.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_lowResFirst_oldSetParameters.html
new file mode 100644
index 0000000000..c2aafc4575
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_lowResFirst_oldSetParameters.html
@@ -0,0 +1,115 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script>
+ <script type="application/javascript" src="simulcast.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1231507",
+ title: "Basic video-only peer connection with Simulcast answer, first rid has lowest resolution",
+ visible: true
+ });
+
+ runNetworkTest(async () => {
+ await pushPrefs(
+ ['media.peerconnection.simulcast', true],
+ // 180Kbps was determined empirically, set well-higher than
+ // the 80Kbps+overhead needed for the two simulcast streams.
+ // 100Kbps was apparently too low.
+ ['media.peerconnection.video.min_bitrate_estimate', 180*1000]);
+
+
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+
+ const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback());
+ answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback());
+
+ const metadataToBeLoaded = [];
+ offerer.ontrack = (e) => {
+ metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track));
+ };
+
+ // Two recv transceivers, one for each simulcast stream
+ offerer.addTransceiver('video', { direction: 'recvonly' });
+ offerer.addTransceiver('video', { direction: 'recvonly' });
+
+ // One send transceiver, that will be used to send both simulcast streams
+ const emitter = new VideoFrameEmitter();
+ const videoStream = emitter.stream();
+ answerer.addTrack(videoStream.getVideoTracks()[0], videoStream);
+ emitter.start();
+
+ const offer = await offerer.createOffer();
+
+ const mungedOffer = midToRid(offer);
+ info(`Transformed recv offer to simulcast: ${offer.sdp} to ${mungedOffer}`);
+
+ await answerer.setRemoteDescription({type: "offer", sdp: mungedOffer});
+ await offerer.setLocalDescription(offer);
+
+ const rids = offerer.getTransceivers().map(t => t.mid);
+ is(rids.length, 2, 'Should have 2 mids in offer');
+ ok(rids[0] != '', 'First mid should be non-empty');
+ ok(rids[1] != '', 'Second mid should be non-empty');
+ info(`rids: ${JSON.stringify(rids)}`);
+
+ const sender = answerer.getSenders()[0];
+ sender.setParameters({
+ encodings: [
+ { rid: rids[0], maxBitrate: 40000, scaleResolutionDownBy: 2 },
+ { rid: rids[1], maxBitrate: 40000 }
+ ]
+ });
+
+ const answer = await answerer.createAnswer();
+
+ const mungedAnswer = ridToMid(answer);
+ info(`Transformed send simulcast answer to multiple m-sections: ${answer.sdp} to ${mungedAnswer}`);
+ await offerer.setRemoteDescription({type: "answer", sdp: mungedAnswer});
+ await answerer.setLocalDescription(answer);
+
+ is(metadataToBeLoaded.length, 2, 'Offerer should have gotten 2 ontrack events');
+ info('Waiting for 2 loadedmetadata events');
+ const videoElems = await Promise.all(metadataToBeLoaded);
+
+ const statsReady =
+ Promise.all([waitForSyncedRtcp(offerer), waitForSyncedRtcp(answerer)]);
+
+ const helper = new VideoStreamHelper();
+ info('Waiting for first video element to start playing');
+ await helper.checkVideoPlaying(videoElems[0]);
+ info('Waiting for second video element to start playing');
+ await helper.checkVideoPlaying(videoElems[1]);
+
+ is(videoElems[1].videoWidth, 50,
+ "sink is same width as source, modulo our cropping algorithm");
+ is(videoElems[1].videoHeight, 50,
+ "sink is same height as source, modulo our cropping algorithm");
+ is(videoElems[0].videoWidth, 25,
+ "sink is 1/2 width of source, modulo our cropping algorithm");
+ is(videoElems[0].videoHeight, 25,
+ "sink is 1/2 height of source, modulo our cropping algorithm");
+
+ await statsReady;
+ const senderStats = await sender.getStats();
+ checkSenderStats(senderStats, 2);
+ checkExpectedFields(senderStats);
+ pedanticChecks(senderStats);
+
+ emitter.stop();
+ videoStream.getVideoTracks()[0].stop();
+ offerer.close();
+ answerer.close();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_oldSetParameters.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_oldSetParameters.html
new file mode 100644
index 0000000000..bc0b9f71cc
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastAnswer_oldSetParameters.html
@@ -0,0 +1,115 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script>
+ <script type="application/javascript" src="simulcast.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1231507",
+ title: "Basic video-only peer connection with Simulcast answer",
+ visible: true
+ });
+
+ runNetworkTest(async () => {
+ await pushPrefs(
+ ['media.peerconnection.simulcast', true],
+ // 180Kbps was determined empirically, set well-higher than
+ // the 80Kbps+overhead needed for the two simulcast streams.
+ // 100Kbps was apparently too low.
+ ['media.peerconnection.video.min_bitrate_estimate', 180*1000]);
+
+
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+
+ const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback());
+ answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback());
+
+ const metadataToBeLoaded = [];
+ offerer.ontrack = (e) => {
+ metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track));
+ };
+
+ // Two recv transceivers, one for each simulcast stream
+ offerer.addTransceiver('video', { direction: 'recvonly' });
+ offerer.addTransceiver('video', { direction: 'recvonly' });
+
+ // One send transceiver, that will be used to send both simulcast streams
+ const emitter = new VideoFrameEmitter();
+ const videoStream = emitter.stream();
+ answerer.addTrack(videoStream.getVideoTracks()[0], videoStream);
+ emitter.start();
+
+ const offer = await offerer.createOffer();
+
+ const mungedOffer = midToRid(offer);
+ info(`Transformed recv offer to simulcast: ${offer.sdp} to ${mungedOffer}`);
+
+ await answerer.setRemoteDescription({type: "offer", sdp: mungedOffer});
+ await offerer.setLocalDescription(offer);
+
+ const rids = offerer.getTransceivers().map(t => t.mid);
+ is(rids.length, 2, 'Should have 2 mids in offer');
+ ok(rids[0] != '', 'First mid should be non-empty');
+ ok(rids[1] != '', 'Second mid should be non-empty');
+ info(`rids: ${JSON.stringify(rids)}`);
+
+ const sender = answerer.getSenders()[0];
+ await sender.setParameters({
+ encodings: [
+ { rid: rids[0], maxBitrate: 40000 },
+ { rid: rids[1], maxBitrate: 40000, scaleResolutionDownBy: 2 }
+ ]
+ });
+
+ const answer = await answerer.createAnswer();
+
+ const mungedAnswer = ridToMid(answer);
+ info(`Transformed send simulcast answer to multiple m-sections: ${answer.sdp} to ${mungedAnswer}`);
+ await offerer.setRemoteDescription({type: "answer", sdp: mungedAnswer});
+ await answerer.setLocalDescription(answer);
+
+ is(metadataToBeLoaded.length, 2, 'Offerer should have gotten 2 ontrack events');
+ info('Waiting for 2 loadedmetadata events');
+ const videoElems = await Promise.all(metadataToBeLoaded);
+
+ const statsReady =
+ Promise.all([waitForSyncedRtcp(offerer), waitForSyncedRtcp(answerer)]);
+
+ const helper = new VideoStreamHelper();
+ info('Waiting for first video element to start playing');
+ await helper.checkVideoPlaying(videoElems[0]);
+ info('Waiting for second video element to start playing');
+ await helper.checkVideoPlaying(videoElems[1]);
+
+ is(videoElems[0].videoWidth, 50,
+ "sink is same width as source, modulo our cropping algorithm");
+ is(videoElems[0].videoHeight, 50,
+ "sink is same height as source, modulo our cropping algorithm");
+ is(videoElems[1].videoWidth, 25,
+ "sink is 1/2 width of source, modulo our cropping algorithm");
+ is(videoElems[1].videoHeight, 25,
+ "sink is 1/2 height of source, modulo our cropping algorithm");
+
+ await statsReady;
+ const senderStats = await sender.getStats();
+ checkSenderStats(senderStats, 2);
+ checkExpectedFields(senderStats);
+ pedanticChecks(senderStats);
+
+ emitter.stop();
+ videoStream.getVideoTracks()[0].stop();
+ offerer.close();
+ answerer.close();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOddResolution.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOddResolution.html
new file mode 100644
index 0000000000..c380b34f1a
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOddResolution.html
@@ -0,0 +1,183 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script>
+ <script type="application/javascript" src="simulcast.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1432793",
+ title: "Simulcast with odd resolution",
+ visible: true
+ });
+
+ runNetworkTest(async () => {
+ const helper = new VideoStreamHelper();
+ const emitter = new VideoFrameEmitter(helper.green, helper.red, 705, 528);
+
+ async function checkVideoElement(senderElement, receiverElement, encoding) {
+ info(`Waiting for receiver video element ${encoding.rid} to start playing`);
+ await helper.checkVideoPlaying(receiverElement);
+ const srcWidth = senderElement.videoWidth;
+ const srcHeight = senderElement.videoHeight;
+ info(`Source resolution is ${srcWidth}x${srcHeight}`);
+
+ const scaleDownBy = encoding.scaleResolutionDownBy;
+ const expectedWidth = srcWidth / scaleDownBy;
+ const expectedHeight = srcHeight / scaleDownBy;
+ const margin = srcWidth * 0.1;
+ const width = receiverElement.videoWidth;
+ const height = receiverElement.videoHeight;
+ const rid = encoding.rid;
+ ok(width >= expectedWidth - margin && width <= expectedWidth + margin,
+ `Width ${width} should be within 10% of ${expectedWidth} for rid '${rid}'`);
+ ok(height >= expectedHeight - margin && height <= expectedHeight + margin,
+ `Height ${height} should be within 10% of ${expectedHeight} for rid '${rid}'`);
+ }
+
+ async function checkVideoElements(senderElement, receiverElements, encodings) {
+ is(receiverElements.length, encodings.length, 'Number of video elements should match number of encodings');
+ info('Waiting for sender video element to start playing');
+ await helper.checkVideoPlaying(senderElement);
+ for (let i = 0; i < encodings.length; i++) {
+ await checkVideoElement(senderElement, receiverElements[i], encodings[i]);
+ }
+ }
+
+ const sendEncodings = [{ rid: "0", maxBitrate: 40000, scaleResolutionDownBy: 1.9 },
+ { rid: "1", maxBitrate: 40000, scaleResolutionDownBy: 3.5 },
+ { rid: "2", maxBitrate: 40000, scaleResolutionDownBy: 17.8 }];
+
+ async function checkSenderStats(sender) {
+ const senderStats = await sender.getStats();
+ checkSenderStats(senderStats, sendEncodings.length);
+ checkExpectedFields(senderStats);
+ pedanticChecks(senderStats);
+ }
+
+ async function waitForResizeEvents(elements) {
+ return Promise.all(elements.map(elem => haveEvent(elem, 'resize')));
+ }
+
+ await pushPrefs(
+ // 180Kbps was determined empirically, set well-higher than
+ // the 80Kbps+overhead needed for the two simulcast streams.
+ // 100Kbps was apparently too low.
+ ['media.peerconnection.video.min_bitrate_estimate', 180*1000]);
+
+
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+
+ const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback());
+ answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback());
+
+ const metadataToBeLoaded = [];
+ answerer.ontrack = (e) => {
+ metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track));
+ };
+
+ // One send transceiver, that will be used to send both simulcast streams
+ const videoStream = emitter.stream();
+ offerer.addTransceiver(videoStream.getVideoTracks()[0], {sendEncodings});
+ const senderElement = document.createElement('video');
+ senderElement.autoplay = true;
+ senderElement.srcObject = videoStream;
+ senderElement.id = videoStream.id
+
+ const sender = offerer.getSenders()[0];
+ let parameters = sender.getParameters();
+ is(parameters.encodings[0].maxBitrate, sendEncodings[0].maxBitrate);
+ isfuzzy(parameters.encodings[0].scaleResolutionDownBy,
+ sendEncodings[0].scaleResolutionDownBy, 0.01);
+ is(parameters.encodings[1].maxBitrate, sendEncodings[1].maxBitrate);
+ isfuzzy(parameters.encodings[1].scaleResolutionDownBy,
+ sendEncodings[1].scaleResolutionDownBy, 0.01);
+ is(parameters.encodings[2].maxBitrate, sendEncodings[2].maxBitrate);
+ isfuzzy(parameters.encodings[2].scaleResolutionDownBy,
+ sendEncodings[2].scaleResolutionDownBy, 0.01);
+
+ const offer = await offerer.createOffer();
+
+ const mungedOffer = ridToMid(offer);
+ info(`Transformed send simulcast offer to multiple m-sections: ${offer.sdp} to ${mungedOffer}`);
+
+ await answerer.setRemoteDescription({type: 'offer', sdp: mungedOffer});
+ await offerer.setLocalDescription(offer);
+
+ const rids = answerer.getTransceivers().map(t => t.mid);
+ is(rids.length, 3, 'Should have 3 mids in offer');
+ ok(rids[0], 'First mid should be non-empty');
+ ok(rids[1], 'Second mid should be non-empty');
+ ok(rids[2], 'Third mid should be non-empty');
+ info(`rids: ${JSON.stringify(rids)}`);
+
+ const answer = await answerer.createAnswer();
+
+ const mungedAnswer = midToRid(answer);
+ info(`Transformed recv answer to simulcast: ${answer.sdp} to ${mungedAnswer}`);
+ await offerer.setRemoteDescription({type: 'answer', sdp: mungedAnswer});
+ await answerer.setLocalDescription(answer);
+
+ is(metadataToBeLoaded.length, 3, 'Offerer should have gotten 3 ontrack events');
+ emitter.start();
+ info('Waiting for 3 loadedmetadata events');
+ const videoElems = await Promise.all(metadataToBeLoaded);
+ await checkVideoElements(senderElement, videoElems, parameters.encodings);
+ emitter.stop();
+
+ await Promise.all([waitForSyncedRtcp(offerer), waitForSyncedRtcp(answerer)]);
+
+ info(`Changing source resolution to 1280x720`);
+ emitter.size(1280, 720);
+ emitter.start();
+ await waitForResizeEvents([senderElement, ...videoElems]);
+ await checkVideoElements(senderElement, videoElems, parameters.encodings);
+ await checkSenderStats(sender);
+
+ parameters = sender.getParameters();
+ parameters.encodings[0].scaleResolutionDownBy = 1;
+ parameters.encodings[1].scaleResolutionDownBy = 2;
+ parameters.encodings[2].scaleResolutionDownBy = 3;
+ info(`Changing encodings to ${JSON.stringify(parameters.encodings)}`);
+ await sender.setParameters(parameters);
+ await waitForResizeEvents(videoElems);
+ await checkVideoElements(senderElement, videoElems, parameters.encodings);
+ await checkSenderStats(sender);
+
+ parameters = sender.getParameters();
+ parameters.encodings[0].scaleResolutionDownBy = 6;
+ parameters.encodings[1].scaleResolutionDownBy = 5;
+ parameters.encodings[2].scaleResolutionDownBy = 4;
+ info(`Changing encodings to ${JSON.stringify(parameters.encodings)}`);
+ await sender.setParameters(parameters);
+ await waitForResizeEvents(videoElems);
+ await checkVideoElements(senderElement, videoElems, parameters.encodings);
+ await checkSenderStats(sender);
+
+ parameters = sender.getParameters();
+ parameters.encodings[0].scaleResolutionDownBy = 4;
+ parameters.encodings[1].scaleResolutionDownBy = 1;
+ parameters.encodings[2].scaleResolutionDownBy = 2;
+ info(`Changing encodings to ${JSON.stringify(parameters.encodings)}`);
+ await sender.setParameters(parameters);
+ await waitForResizeEvents(videoElems);
+ await checkVideoElements(senderElement, videoElems, parameters.encodings);
+ await checkSenderStats(sender);
+
+ emitter.stop();
+ videoStream.getVideoTracks()[0].stop();
+ offerer.close();
+ answerer.close();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOddResolution_oldSetParameters.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOddResolution_oldSetParameters.html
new file mode 100644
index 0000000000..0f6d3c8520
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOddResolution_oldSetParameters.html
@@ -0,0 +1,172 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script>
+ <script type="application/javascript" src="simulcast.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1432793",
+ title: "Simulcast with odd resolution",
+ visible: true
+ });
+
+ runNetworkTest(async () => {
+ const helper = new VideoStreamHelper();
+ const emitter = new VideoFrameEmitter(helper.green, helper.red, 705, 528);
+
+ async function checkVideoElement(senderElement, receiverElement, encoding) {
+ info(`Waiting for receiver video element ${encoding.rid} to start playing`);
+ await helper.checkVideoPlaying(receiverElement);
+ const srcWidth = senderElement.videoWidth;
+ const srcHeight = senderElement.videoHeight;
+ info(`Source resolution is ${srcWidth}x${srcHeight}`);
+
+ const scaleDownBy = encoding.scaleResolutionDownBy;
+ const expectedWidth = srcWidth / scaleDownBy;
+ const expectedHeight = srcHeight / scaleDownBy;
+ const margin = srcWidth * 0.1;
+ const width = receiverElement.videoWidth;
+ const height = receiverElement.videoHeight;
+ const rid = encoding.rid;
+ ok(width >= expectedWidth - margin && width <= expectedWidth + margin,
+ `Width ${width} should be within 10% of ${expectedWidth} for rid '${rid}'`);
+ ok(height >= expectedHeight - margin && height <= expectedHeight + margin,
+ `Height ${height} should be within 10% of ${expectedHeight} for rid '${rid}'`);
+ }
+
+ async function checkVideoElements(senderElement, receiverElements, encodings) {
+ is(receiverElements.length, encodings.length, 'Number of video elements should match number of encodings');
+ info('Waiting for sender video element to start playing');
+ await helper.checkVideoPlaying(senderElement);
+ for (let i = 0; i < encodings.length; i++) {
+ await checkVideoElement(senderElement, receiverElements[i], encodings[i]);
+ }
+ }
+
+ async function checkSenderStats(sender) {
+ const senderStats = await sender.getStats();
+ checkSenderStats(senderStats, encodings.length);
+ checkExpectedFields(senderStats);
+ pedanticChecks(senderStats);
+ }
+
+ async function waitForResizeEvents(elements) {
+ return Promise.all(elements.map(elem => haveEvent(elem, 'resize')));
+ }
+
+ const encodings = [{ rid: "0", maxBitrate: 40000, scaleResolutionDownBy: 1.9 },
+ { rid: "1", maxBitrate: 40000, scaleResolutionDownBy: 3.5 },
+ { rid: "2", maxBitrate: 40000, scaleResolutionDownBy: 17.8 }];
+
+ await pushPrefs(
+ ['media.peerconnection.simulcast', true],
+ // 180Kbps was determined empirically, set well-higher than
+ // the 80Kbps+overhead needed for the two simulcast streams.
+ // 100Kbps was apparently too low.
+ ['media.peerconnection.video.min_bitrate_estimate', 180*1000]);
+
+
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+
+ const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback());
+ answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback());
+
+ const metadataToBeLoaded = [];
+ answerer.ontrack = (e) => {
+ metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track));
+ };
+
+ // One send transceiver, that will be used to send both simulcast streams
+ const videoStream = emitter.stream();
+ offerer.addTrack(videoStream.getVideoTracks()[0], videoStream);
+ const senderElement = document.createElement('video');
+ senderElement.autoplay = true;
+ senderElement.srcObject = videoStream;
+ senderElement.id = videoStream.id
+
+ const sender = offerer.getSenders()[0];
+ sender.setParameters({encodings});
+
+ const offer = await offerer.createOffer();
+
+ const mungedOffer = ridToMid(offer);
+ info(`Transformed send simulcast offer to multiple m-sections: ${offer.sdp} to ${mungedOffer}`);
+
+ await answerer.setRemoteDescription({type: "offer", sdp: mungedOffer});
+ await offerer.setLocalDescription(offer);
+
+ const rids = answerer.getTransceivers().map(t => t.mid);
+ is(rids.length, 3, 'Should have 3 mids in offer');
+ ok(rids[0], 'First mid should be non-empty');
+ ok(rids[1], 'Second mid should be non-empty');
+ ok(rids[2], 'Third mid should be non-empty');
+ info(`rids: ${JSON.stringify(rids)}`);
+
+ const answer = await answerer.createAnswer();
+
+ const mungedAnswer = midToRid(answer);
+ info(`Transformed recv answer to simulcast: ${answer.sdp} to ${mungedAnswer}`);
+ await offerer.setRemoteDescription({type: "answer", sdp: mungedAnswer});
+ await answerer.setLocalDescription(answer);
+
+ is(metadataToBeLoaded.length, 3, 'Offerer should have gotten 3 ontrack events');
+ emitter.start();
+ info('Waiting for 3 loadedmetadata events');
+ const videoElems = await Promise.all(metadataToBeLoaded);
+ await checkVideoElements(senderElement, videoElems, encodings);
+ emitter.stop();
+
+ await Promise.all([waitForSyncedRtcp(offerer), waitForSyncedRtcp(answerer)]);
+
+ info(`Changing source resolution to 1280x720`);
+ emitter.size(1280, 720);
+ emitter.start();
+ await waitForResizeEvents([senderElement, ...videoElems]);
+ await checkVideoElements(senderElement, videoElems, encodings);
+ await checkSenderStats(sender);
+
+ encodings[0].scaleResolutionDownBy = 1;
+ encodings[1].scaleResolutionDownBy = 2;
+ encodings[2].scaleResolutionDownBy = 3;
+ info(`Changing encodings to ${JSON.stringify(encodings)}`);
+ await sender.setParameters({encodings});
+ await waitForResizeEvents(videoElems);
+ await checkVideoElements(senderElement, videoElems, encodings);
+ await checkSenderStats(sender);
+
+ encodings[0].scaleResolutionDownBy = 6;
+ encodings[1].scaleResolutionDownBy = 5;
+ encodings[2].scaleResolutionDownBy = 4;
+ info(`Changing encodings to ${JSON.stringify(encodings)}`);
+ await sender.setParameters({encodings});
+ await waitForResizeEvents(videoElems);
+ await checkVideoElements(senderElement, videoElems, encodings);
+ await checkSenderStats(sender);
+
+ encodings[0].scaleResolutionDownBy = 4;
+ encodings[1].scaleResolutionDownBy = 1;
+ encodings[2].scaleResolutionDownBy = 2;
+ info(`Changing encodings to ${JSON.stringify(encodings)}`);
+ await sender.setParameters({encodings});
+ await waitForResizeEvents(videoElems);
+ await checkVideoElements(senderElement, videoElems, encodings);
+ await checkSenderStats(sender);
+
+ emitter.stop();
+ videoStream.getVideoTracks()[0].stop();
+ offerer.close();
+ answerer.close();
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer.html
new file mode 100644
index 0000000000..cb7c13a0d1
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer.html
@@ -0,0 +1,109 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="parser_rtp.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script>
+ <script type="application/javascript" src="simulcast.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1231507",
+ title: "Basic video-only peer connection with Simulcast offer",
+ visible: true
+ });
+
+ runNetworkTest(async () => {
+ await pushPrefs(
+ // 180Kbps was determined empirically, set well-higher than
+ // the 80Kbps+overhead needed for the two simulcast streams.
+ // 100Kbps was apparently too low.
+ ['media.peerconnection.video.min_bitrate_estimate', 180*1000]);
+
+
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+
+ const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback());
+ answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback());
+
+ const metadataToBeLoaded = [];
+ answerer.ontrack = (e) => {
+ metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track));
+ };
+
+ // One send transceiver, that will be used to send both simulcast streams
+ const emitter = new VideoFrameEmitter();
+ const videoStream = emitter.stream();
+ const sendEncodings = [
+ { rid: '0', maxBitrate: 40000 },
+ { rid: '1', maxBitrate: 40000, scaleResolutionDownBy: 2 }
+ ];
+ offerer.addTransceiver(videoStream.getVideoTracks()[0], {sendEncodings});
+ emitter.start();
+
+ const sender = offerer.getSenders()[0];
+
+ const offer = await offerer.createOffer();
+
+ const mungedOffer = ridToMid(offer);
+ info(`Transformed send simulcast offer to multiple m-sections: ${offer.sdp} to ${mungedOffer}`);
+
+ await answerer.setRemoteDescription({type: 'offer', sdp: mungedOffer});
+ await offerer.setLocalDescription(offer);
+
+ const rids = answerer.getTransceivers().map(t => t.mid);
+ is(rids.length, 2, 'Should have 2 mids in offer');
+ ok(rids[0] != '', 'First mid should be non-empty');
+ ok(rids[1] != '', 'Second mid should be non-empty');
+ info(`rids: ${JSON.stringify(rids)}`);
+
+ const answer = await answerer.createAnswer();
+
+ const mungedAnswer = midToRid(answer);
+ info(`Transformed recv answer to simulcast: ${answer.sdp} to ${mungedAnswer}`);
+ await offerer.setRemoteDescription({type: 'answer', sdp: mungedAnswer});
+ await answerer.setLocalDescription(answer);
+
+ is(metadataToBeLoaded.length, 2, 'Offerer should have gotten 2 ontrack events');
+ info('Waiting for 2 loadedmetadata events');
+ const videoElems = await Promise.all(metadataToBeLoaded);
+
+ const statsReady =
+ Promise.all([waitForSyncedRtcp(offerer), waitForSyncedRtcp(answerer)]);
+
+ const helper = new VideoStreamHelper();
+ info('Waiting for first video element to start playing');
+ await helper.checkVideoPlaying(videoElems[0]);
+ info('Waiting for second video element to start playing');
+ await helper.checkVideoPlaying(videoElems[1]);
+
+ is(videoElems[0].videoWidth, 50,
+ "sink is same width as source, modulo our cropping algorithm");
+ is(videoElems[0].videoHeight, 50,
+ "sink is same height as source, modulo our cropping algorithm");
+ is(videoElems[1].videoWidth, 25,
+ "sink is 1/2 width of source, modulo our cropping algorithm");
+ is(videoElems[1].videoHeight, 25,
+ "sink is 1/2 height of source, modulo our cropping algorithm");
+
+ await statsReady;
+ const senderStats = await sender.getStats();
+ checkSenderStats(senderStats, 2);
+ checkExpectedFields(senderStats);
+ pedanticChecks(senderStats);
+
+ emitter.stop();
+ videoStream.getVideoTracks()[0].stop();
+ offerer.close();
+ answerer.close();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_lowResFirst.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_lowResFirst.html
new file mode 100644
index 0000000000..93141311f1
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_lowResFirst.html
@@ -0,0 +1,109 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="parser_rtp.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script>
+ <script type="application/javascript" src="simulcast.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1231507",
+ title: "Basic video-only peer connection with Simulcast offer, first rid has lowest resolution",
+ visible: true
+ });
+
+ runNetworkTest(async () => {
+ await pushPrefs(
+ // 180Kbps was determined empirically, set well-higher than
+ // the 80Kbps+overhead needed for the two simulcast streams.
+ // 100Kbps was apparently too low.
+ ['media.peerconnection.video.min_bitrate_estimate', 180*1000]);
+
+
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+
+ const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback());
+ answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback());
+
+ const metadataToBeLoaded = [];
+ answerer.ontrack = (e) => {
+ metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track));
+ };
+
+ // One send transceiver, that will be used to send both simulcast streams
+ const emitter = new VideoFrameEmitter();
+ const videoStream = emitter.stream();
+ const sendEncodings = [
+ { rid: '0', maxBitrate: 40000, scaleResolutionDownBy: 2 },
+ { rid: '1', maxBitrate: 40000 }
+ ];
+ offerer.addTransceiver(videoStream.getVideoTracks()[0], {sendEncodings});
+ emitter.start();
+
+ const sender = offerer.getSenders()[0];
+
+ const offer = await offerer.createOffer();
+
+ const mungedOffer = ridToMid(offer);
+ info(`Transformed send simulcast offer to multiple m-sections: ${offer.sdp} to ${mungedOffer}`);
+
+ await answerer.setRemoteDescription({type: 'offer', sdp: mungedOffer});
+ await offerer.setLocalDescription(offer);
+
+ const rids = answerer.getTransceivers().map(t => t.mid);
+ is(rids.length, 2, 'Should have 2 mids in offer');
+ ok(rids[0] != '', 'First mid should be non-empty');
+ ok(rids[1] != '', 'Second mid should be non-empty');
+ info(`rids: ${JSON.stringify(rids)}`);
+
+ const answer = await answerer.createAnswer();
+
+ const mungedAnswer = midToRid(answer);
+ info(`Transformed recv answer to simulcast: ${answer.sdp} to ${mungedAnswer}`);
+ await offerer.setRemoteDescription({type: 'answer', sdp: mungedAnswer});
+ await answerer.setLocalDescription(answer);
+
+ is(metadataToBeLoaded.length, 2, 'Offerer should have gotten 2 ontrack events');
+ info('Waiting for 2 loadedmetadata events');
+ const videoElems = await Promise.all(metadataToBeLoaded);
+
+ const statsReady =
+ Promise.all([waitForSyncedRtcp(offerer), waitForSyncedRtcp(answerer)]);
+
+ const helper = new VideoStreamHelper();
+ info('Waiting for first video element to start playing');
+ await helper.checkVideoPlaying(videoElems[0]);
+ info('Waiting for second video element to start playing');
+ await helper.checkVideoPlaying(videoElems[1]);
+
+ is(videoElems[1].videoWidth, 50,
+ "sink is same width as source, modulo our cropping algorithm");
+ is(videoElems[1].videoHeight, 50,
+ "sink is same height as source, modulo our cropping algorithm");
+ is(videoElems[0].videoWidth, 25,
+ "sink is 1/2 width of source, modulo our cropping algorithm");
+ is(videoElems[0].videoHeight, 25,
+ "sink is 1/2 height of source, modulo our cropping algorithm");
+
+ await statsReady;
+ const senderStats = await sender.getStats();
+ checkSenderStats(senderStats, 2);
+ checkExpectedFields(senderStats);
+ pedanticChecks(senderStats);
+
+ emitter.stop();
+ videoStream.getVideoTracks()[0].stop();
+ offerer.close();
+ answerer.close();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_lowResFirst_oldSetParameters.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_lowResFirst_oldSetParameters.html
new file mode 100644
index 0000000000..73e2d38eb2
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_lowResFirst_oldSetParameters.html
@@ -0,0 +1,112 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="parser_rtp.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script>
+ <script type="application/javascript" src="simulcast.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1231507",
+ title: "Basic video-only peer connection with Simulcast offer, first rid has lowest resolution",
+ visible: true
+ });
+
+ runNetworkTest(async () => {
+ await pushPrefs(
+ ['media.peerconnection.simulcast', true],
+ // 180Kbps was determined empirically, set well-higher than
+ // the 80Kbps+overhead needed for the two simulcast streams.
+ // 100Kbps was apparently too low.
+ ['media.peerconnection.video.min_bitrate_estimate', 180*1000]);
+
+
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+
+ const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback());
+ answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback());
+
+ const metadataToBeLoaded = [];
+ answerer.ontrack = (e) => {
+ metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track));
+ };
+
+ // One send transceiver, that will be used to send both simulcast streams
+ const emitter = new VideoFrameEmitter();
+ const videoStream = emitter.stream();
+ offerer.addTrack(videoStream.getVideoTracks()[0], videoStream);
+ emitter.start();
+
+ const sender = offerer.getSenders()[0];
+ sender.setParameters({
+ encodings: [
+ { rid: '0', maxBitrate: 40000, scaleResolutionDownBy: 2 },
+ { rid: '1', maxBitrate: 40000 }
+ ]
+ });
+
+ const offer = await offerer.createOffer();
+
+ const mungedOffer = ridToMid(offer);
+ info(`Transformed send simulcast offer to multiple m-sections: ${offer.sdp} to ${mungedOffer}`);
+
+ await answerer.setRemoteDescription({type: 'offer', sdp: mungedOffer});
+ await offerer.setLocalDescription(offer);
+
+ const rids = answerer.getTransceivers().map(t => t.mid);
+ is(rids.length, 2, 'Should have 2 mids in offer');
+ ok(rids[0] != '', 'First mid should be non-empty');
+ ok(rids[1] != '', 'Second mid should be non-empty');
+ info(`rids: ${JSON.stringify(rids)}`);
+
+ const answer = await answerer.createAnswer();
+
+ const mungedAnswer = midToRid(answer);
+ info(`Transformed recv answer to simulcast: ${answer.sdp} to ${mungedAnswer}`);
+ await offerer.setRemoteDescription({type: 'answer', sdp: mungedAnswer});
+ await answerer.setLocalDescription(answer);
+
+ is(metadataToBeLoaded.length, 2, 'Offerer should have gotten 2 ontrack events');
+ info('Waiting for 2 loadedmetadata events');
+ const videoElems = await Promise.all(metadataToBeLoaded);
+
+ const statsReady =
+ Promise.all([waitForSyncedRtcp(offerer), waitForSyncedRtcp(answerer)]);
+
+ const helper = new VideoStreamHelper();
+ info('Waiting for first video element to start playing');
+ await helper.checkVideoPlaying(videoElems[0]);
+ info('Waiting for second video element to start playing');
+ await helper.checkVideoPlaying(videoElems[1]);
+
+ is(videoElems[1].videoWidth, 50,
+ "sink is same width as source, modulo our cropping algorithm");
+ is(videoElems[1].videoHeight, 50,
+ "sink is same height as source, modulo our cropping algorithm");
+ is(videoElems[0].videoWidth, 25,
+ "sink is 1/2 width of source, modulo our cropping algorithm");
+ is(videoElems[0].videoHeight, 25,
+ "sink is 1/2 height of source, modulo our cropping algorithm");
+
+ await statsReady;
+ const senderStats = await sender.getStats();
+ checkSenderStats(senderStats, 2);
+ checkExpectedFields(senderStats);
+ pedanticChecks(senderStats);
+
+ emitter.stop();
+ videoStream.getVideoTracks()[0].stop();
+ offerer.close();
+ answerer.close();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_oldSetParameters.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_oldSetParameters.html
new file mode 100644
index 0000000000..551273af5e
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_simulcastOffer_oldSetParameters.html
@@ -0,0 +1,112 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="parser_rtp.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+ <script type="application/javascript" src="helpers_from_wpt/sdp.js"></script>
+ <script type="application/javascript" src="simulcast.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1231507",
+ title: "Basic video-only peer connection with Simulcast offer",
+ visible: true
+ });
+
+ runNetworkTest(async () => {
+ await pushPrefs(
+ ['media.peerconnection.simulcast', true],
+ // 180Kbps was determined empirically, set well-higher than
+ // the 80Kbps+overhead needed for the two simulcast streams.
+ // 100Kbps was apparently too low.
+ ['media.peerconnection.video.min_bitrate_estimate', 180*1000]);
+
+
+ const offerer = new RTCPeerConnection();
+ const answerer = new RTCPeerConnection();
+
+ const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ offerer.onicecandidate = e => add(answerer, e.candidate, generateErrorCallback());
+ answerer.onicecandidate = e => add(offerer, e.candidate, generateErrorCallback());
+
+ const metadataToBeLoaded = [];
+ answerer.ontrack = (e) => {
+ metadataToBeLoaded.push(getPlaybackWithLoadedMetadata(e.track));
+ };
+
+ // One send transceiver, that will be used to send both simulcast streams
+ const emitter = new VideoFrameEmitter();
+ const videoStream = emitter.stream();
+ offerer.addTrack(videoStream.getVideoTracks()[0], videoStream);
+ emitter.start();
+
+ const sender = offerer.getSenders()[0];
+ sender.setParameters({
+ encodings: [
+ { rid: '0', maxBitrate: 40000 },
+ { rid: '1', maxBitrate: 40000, scaleResolutionDownBy: 2 }
+ ]
+ });
+
+ const offer = await offerer.createOffer();
+
+ const mungedOffer = ridToMid(offer);
+ info(`Transformed send simulcast offer to multiple m-sections: ${offer.sdp} to ${mungedOffer}`);
+
+ await answerer.setRemoteDescription({type: "offer", sdp: mungedOffer});
+ await offerer.setLocalDescription(offer);
+
+ const rids = answerer.getTransceivers().map(t => t.mid);
+ is(rids.length, 2, 'Should have 2 mids in offer');
+ ok(rids[0] != '', 'First mid should be non-empty');
+ ok(rids[1] != '', 'Second mid should be non-empty');
+ info(`rids: ${JSON.stringify(rids)}`);
+
+ const answer = await answerer.createAnswer();
+
+ const mungedAnswer = midToRid(answer);
+ info(`Transformed recv answer to simulcast: ${answer.sdp} to ${mungedAnswer}`);
+ await offerer.setRemoteDescription({type: "answer", sdp: mungedAnswer});
+ await answerer.setLocalDescription(answer);
+
+ is(metadataToBeLoaded.length, 2, 'Offerer should have gotten 2 ontrack events');
+ info('Waiting for 2 loadedmetadata events');
+ const videoElems = await Promise.all(metadataToBeLoaded);
+
+ const statsReady =
+ Promise.all([waitForSyncedRtcp(offerer), waitForSyncedRtcp(answerer)]);
+
+ const helper = new VideoStreamHelper();
+ info('Waiting for first video element to start playing');
+ await helper.checkVideoPlaying(videoElems[0]);
+ info('Waiting for second video element to start playing');
+ await helper.checkVideoPlaying(videoElems[1]);
+
+ is(videoElems[0].videoWidth, 50,
+ "sink is same width as source, modulo our cropping algorithm");
+ is(videoElems[0].videoHeight, 50,
+ "sink is same height as source, modulo our cropping algorithm");
+ is(videoElems[1].videoWidth, 25,
+ "sink is 1/2 width of source, modulo our cropping algorithm");
+ is(videoElems[1].videoHeight, 25,
+ "sink is 1/2 height of source, modulo our cropping algorithm");
+
+ await statsReady;
+ const senderStats = await sender.getStats();
+ checkSenderStats(senderStats, 2);
+ checkExpectedFields(senderStats);
+ pedanticChecks(senderStats);
+
+ emitter.stop();
+ videoStream.getVideoTracks()[0].stop();
+ offerer.close();
+ answerer.close();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_stats.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_stats.html
new file mode 100644
index 0000000000..2ef98dc9c8
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_stats.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1337525",
+ title: "webRtc Stats composition and sanity"
+});
+
+runNetworkTest(async function (options) {
+ // We don't know how to get QP value when using Android system codecs.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ // For accurate comparisons of `remoteTimestamp` (not using reduced precision)
+ // to `timestamp` (using reduced precision).
+ await pushPrefs(["privacy.resistFingerprinting.reduceTimerPrecision.jitter",
+ false]);
+
+ const test = new PeerConnectionTest(options);
+
+ test.chain.insertAfter("PC_LOCAL_WAIT_FOR_MEDIA_FLOW",
+ [PC_LOCAL_TEST_LOCAL_STATS]);
+
+ test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW",
+ [PC_REMOTE_TEST_REMOTE_STATS]);
+
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+ await test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_stats_jitter.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_stats_jitter.html
new file mode 100644
index 0000000000..6e1ef698b4
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_stats_jitter.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1672590",
+ title: "Jitter sanity check"
+ });
+
+const checkJitter = stats => {
+ stats.forEach((stat, mapKey) => {
+ if (stat.type == "remote-inbound-rtp") {
+ // This should be much lower for audio, TODO: Bug 1330575
+ const expectedJitter = stat.kind == "video" ? 0.5 : 1;
+
+ ok(stat.jitter < expectedJitter,
+ stat.type + ".jitter is sane number for a local only test. value="
+ + stat.jitter);
+ }
+ });
+};
+
+const PC_LOCAL_TEST_LOCAL_JITTER = async test => {
+ checkJitter(await waitForSyncedRtcp(test.pcLocal._pc));
+}
+
+const PC_REMOTE_TEST_REMOTE_JITTER = async test => {
+ checkJitter(await waitForSyncedRtcp(test.pcRemote._pc));
+}
+
+runNetworkTest(async function (options) {
+ // We don't know how to get QP value when using Android system codecs.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ const test = new PeerConnectionTest(options);
+
+ test.chain.insertAfter("PC_LOCAL_WAIT_FOR_MEDIA_FLOW",
+ [PC_LOCAL_TEST_LOCAL_JITTER]);
+
+ test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW",
+ [PC_REMOTE_TEST_REMOTE_JITTER]);
+
+ test.setMediaConstraints([{audio: true}, {video: true}],
+ [{audio: true}, {video: true}]);
+ await test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_stats_oneway.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_stats_oneway.html
new file mode 100644
index 0000000000..02ace530a9
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_stats_oneway.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1225722",
+ title: "WebRTC Stats composition and sanity for a one-way peer connection"
+});
+
+runNetworkTest(async function (options) {
+ // We don't know how to get QP value when using Android system codecs.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ // For accurate comparisons of `remoteTimestamp` (not using reduced precision)
+ // to `timestamp` (using reduced precision).
+ await pushPrefs(["privacy.resistFingerprinting.reduceTimerPrecision.jitter",
+ false]);
+
+ const test = new PeerConnectionTest(options);
+
+ test.chain.insertAfter("PC_LOCAL_WAIT_FOR_MEDIA_FLOW",
+ [PC_LOCAL_TEST_LOCAL_STATS]);
+
+ test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW",
+ [PC_REMOTE_TEST_REMOTE_STATS]);
+
+ const testOneWayStats = (stats, codecType) => {
+ const codecs = [];
+ stats.forEach(stat => {
+ if (stat.type == "codec") {
+ codecs.push(stat);
+ is(stat.codecType, codecType, "One-way codec has specific codecType");
+ }
+ });
+ is(codecs.length, 2, "One audio and one video codec");
+ if (codecs.length == 2) {
+ isnot(codecs[0].mimeType.slice(0, 5), codecs[1].mimeType.slice(0, 5),
+ "Different media type for audio vs video mime types");
+ }
+ };
+
+ test.chain.append([
+ async function PC_LOCAL_TEST_CODECTYPE_ENCODE(test) {
+ testOneWayStats(await test.pcLocal._pc.getStats(), "encode");
+ },
+ async function PC_REMOTE_TEST_CODECTYPE_DECODE(test) {
+ testOneWayStats(await test.pcRemote._pc.getStats(), "decode");
+ },
+ ]);
+
+ test.setMediaConstraints([{audio: true}, {video: true}], []);
+ await test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_stats_relayProtocol.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_stats_relayProtocol.html
new file mode 100644
index 0000000000..cdc328fd2b
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_stats_relayProtocol.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="nonTrickleIce.js"></script>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1435789",
+ title: "WebRTC local-candidate relayProtocol stats attribute"
+});
+
+// This test uses the NAT simulator in order to get srflx candidates.
+// It doesn't work in https, so we turn on getUserMedia in http, which requires
+// a reload.
+if (!("mediaDevices" in navigator)) {
+ SpecialPowers.pushPrefEnv({set: [['media.devices.insecure.enabled', true]]},
+ () => location.reload());
+} else {
+ runNetworkTest(async (options = {}) => {
+ await pushPrefs(
+ ['media.peerconnection.nat_simulator.filtering_type', 'ENDPOINT_INDEPENDENT'],
+ ['media.peerconnection.nat_simulator.mapping_type', 'ENDPOINT_INDEPENDENT'],
+ ['media.getusermedia.insecure.enabled', true]);
+ const test = new PeerConnectionTest(options);
+ makeOffererNonTrickle(test.chain);
+ makeAnswererNonTrickle(test.chain);
+
+ test.chain.removeAfter("PC_LOCAL_WAIT_FOR_MEDIA_FLOW");
+ test.chain.append([PC_LOCAL_TEST_LOCAL_STATS_RELAYCANDIDATE]);
+
+ test.setMediaConstraints([{ audio: true }], [{ audio: true }]);
+ await test.run();
+ }, { useIceServer: true });
+}
+
+const PC_LOCAL_TEST_LOCAL_STATS_RELAYCANDIDATE = test => {
+ return test.pcLocal.getStats().then(stats => {
+ let haveRelayProtocol = {};
+ for (let [k, v] of stats) {
+ if (v.type == "local-candidate") {
+ haveRelayProtocol[v.candidateType + "-" + v.relayProtocol] = v.relayProtocol;
+ }
+ }
+ is(haveRelayProtocol["host-undefined"], undefined, "relayProtocol not set for host candidates");
+ is(haveRelayProtocol["srflx-undefined"], undefined, "relayProtocol not set for server reflexive candidates");
+ ok(haveRelayProtocol["relay-udp"], "Has UDP relay candidate");
+ ok(haveRelayProtocol["relay-tcp"], "Has TCP relay candidate");
+ ok(haveRelayProtocol["relay-tls"], "Has TLS relay candidate");
+ is(Object.keys(haveRelayProtocol).length, 5, "All candidate types are accounted for");
+ });
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_stereoFmtpPref.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_stereoFmtpPref.html
new file mode 100644
index 0000000000..ab7811fe82
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_stereoFmtpPref.html
@@ -0,0 +1,61 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1793776",
+ title: "Test that media.peerconnection.sdp.disable_stereo_fmtp works"
+ });
+
+ const tests = [
+ async function testStereo() {
+ const offerer = new RTCPeerConnection();
+ offerer.addTransceiver('audio');
+ const answerer = new RTCPeerConnection();
+ await offerer.setLocalDescription();
+ ok(offerer.localDescription.sdp.includes('stereo=1'),
+ 'Offer uses stereo=1 when media.peerconnection.sdp.disable_stereo_fmtp is not set');
+ await answerer.setRemoteDescription(offerer.localDescription);
+ const {sdp} = await answerer.createAnswer();
+ ok(sdp.includes('stereo=1'), 'Answer uses stereo=1 when media.peerconnection.sdp.disable_stereo_fmtp is not set');
+ },
+
+ async function testNoStereo() {
+ await pushPrefs(
+ ['media.peerconnection.sdp.disable_stereo_fmtp', true]);
+
+ const offerer = new RTCPeerConnection();
+ offerer.addTransceiver('audio');
+ const answerer = new RTCPeerConnection();
+ await offerer.setLocalDescription();
+ ok(offerer.localDescription.sdp.includes('stereo=0'),
+ 'Offer uses stereo=0 when media.peerconnection.sdp.disable_stereo_fmtp is set');
+ await answerer.setRemoteDescription(offerer.localDescription);
+ const {sdp} = await answerer.createAnswer();
+ ok(sdp.includes('stereo=0'), 'Answer uses stereo=0 when media.peerconnection.sdp.disable_stereo_fmtp is set');
+ },
+ ];
+
+ runNetworkTest(async () => {
+ for (const test of tests) {
+ info(`Running test: ${test.name}`);
+ try {
+ await test();
+ } catch (e) {
+ ok(false, `Caught ${e.name}: ${e.message} ${e.stack}`);
+ }
+ info(`Done running test: ${test.name}`);
+ // Make sure we don't build up a pile of GC work, and also get PCImpl to
+ // print their timecards.
+ await new Promise(r => SpecialPowers.exactGC(r));
+ }
+
+ await SpecialPowers.popPrefEnv();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_syncSetDescription.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_syncSetDescription.html
new file mode 100644
index 0000000000..98f0de1b4a
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_syncSetDescription.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1063971",
+ title: "Legacy sync setDescription calls",
+ visible: true
+ });
+
+// Test setDescription without callbacks, which many webrtc examples still do
+
+function PC_LOCAL_SET_LOCAL_DESCRIPTION_SYNC(test) {
+ test.pcLocal.onsignalingstatechange = function() {};
+ test.pcLocal._pc.setLocalDescription(test.originalOffer);
+}
+
+function PC_REMOTE_SET_REMOTE_DESCRIPTION_SYNC(test) {
+ test.pcRemote.onsignalingstatechange = function() {};
+ test.pcRemote._pc.setRemoteDescription(test._local_offer,
+ test.pcRemote.releaseIceCandidates,
+ generateErrorCallback("pcRemote._pc.setRemoteDescription() sync failed"));
+}
+function PC_REMOTE_SET_LOCAL_DESCRIPTION_SYNC(test) {
+ test.pcRemote.onsignalingstatechange = function() {};
+ test.pcRemote._pc.setLocalDescription(test.originalAnswer);
+}
+function PC_LOCAL_SET_REMOTE_DESCRIPTION_SYNC(test) {
+ test.pcLocal.onsignalingstatechange = function() {};
+ test.pcLocal._pc.setRemoteDescription(test._remote_answer,
+ test.pcLocal.releaseIceCandidates,
+ generateErrorCallback("pcLocal._pc.setRemoteDescription() sync failed"));
+}
+
+runNetworkTest(() => {
+ const test = new PeerConnectionTest();
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+ test.chain.replace("PC_LOCAL_SET_LOCAL_DESCRIPTION", PC_LOCAL_SET_LOCAL_DESCRIPTION_SYNC);
+ test.chain.replace("PC_REMOTE_SET_REMOTE_DESCRIPTION", PC_REMOTE_SET_REMOTE_DESCRIPTION_SYNC);
+ test.chain.remove("PC_REMOTE_CHECK_CAN_TRICKLE_SYNC");
+ test.chain.replace("PC_REMOTE_SET_LOCAL_DESCRIPTION", PC_REMOTE_SET_LOCAL_DESCRIPTION_SYNC);
+ test.chain.replace("PC_LOCAL_SET_REMOTE_DESCRIPTION", PC_LOCAL_SET_REMOTE_DESCRIPTION_SYNC);
+ test.chain.remove("PC_LOCAL_CHECK_CAN_TRICKLE_SYNC");
+ return test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_telephoneEventFirst.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_telephoneEventFirst.html
new file mode 100644
index 0000000000..bde51c1fd0
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_telephoneEventFirst.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ title: "RTCPeerConnection with telephone-event codec first in SDP",
+ bug: "1581898",
+ visible: true
+});
+
+const test = async () => {
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ const stream = await navigator.mediaDevices.getUserMedia({audio:true});
+ pc1.addTrack(stream.getAudioTracks()[0], stream);
+ pc2.addTrack(stream.getAudioTracks()[0], stream);
+
+ const offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer);
+
+ const regex = /^(m=audio \d+ [^ ]+) (.*) 101(.*)$/m;
+
+ // Rewrite offer so payload type 101 comes first
+ offer.sdp = offer.sdp.replace(regex, '$1 101 $2 $3');
+
+ ok(offer.sdp.match(/^m=audio \d+ [^ ]+ 101 /m),
+ "Payload type 101 should be first on the m-line");
+
+ await pc2.setRemoteDescription(offer);
+ const answer = await pc2.createAnswer();
+
+ pc1.onicecandidate = e => { pc2.addIceCandidate(e.candidate); }
+ pc2.onicecandidate = e => { pc1.addIceCandidate(e.candidate); }
+
+ await pc1.setRemoteDescription(answer);
+ await pc2.setLocalDescription(answer);
+ await new Promise(resolve => {
+ pc1.oniceconnectionstatechange = e => {
+ if (pc1.iceConnectionState == "connected") {
+ resolve();
+ }
+ };
+ });
+ await wait(1000);
+};
+
+runNetworkTest(test);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_threeUnbundledConnections.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_threeUnbundledConnections.html
new file mode 100644
index 0000000000..75f0d12463
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_threeUnbundledConnections.html
@@ -0,0 +1,134 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1342579",
+ title: "Unbundled PC connects to two different PCs",
+ visible: true
+ });
+
+ const fakeFingerPrint = "a=fingerprint:sha-256 11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11:11";
+
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+ const pc3 = new RTCPeerConnection();
+
+ const add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ pc1.onicecandidate = e => {
+ if (e.candidate) {
+ if (e.candidate.sdpMid === "1") {
+ add(pc2, e.candidate, generateErrorCallback())
+ } else {
+ add(pc3, e.candidate, generateErrorCallback())
+ }
+ }
+ };
+ pc2.onicecandidate = e => add(pc1, e.candidate, generateErrorCallback());
+ pc3.onicecandidate = e => add(pc1, e.candidate, generateErrorCallback());
+
+ let ice1Finished, ice2Finished, ice3Finished;
+ const ice1Done = new Promise(r => ice1Finished = r);
+ const ice2Done = new Promise(r => ice2Finished = r);
+ const ice3Done = new Promise(r => ice3Finished = r);
+
+ const icsc = (pc, str, resolve) => {
+ const state = pc.iceConnectionState;
+ info(str + " ICE connection state is: " + state);
+ if (state == "connected") {
+ ok(true, str + " ICE connected");
+ resolve();
+ } else if (state == "failed") {
+ ok(false, str + " ICE failed")
+ resolve();
+ }
+ };
+
+ pc1.oniceconnectionstatechange = e => icsc(pc1, "PC1", ice1Finished);
+ pc2.oniceconnectionstatechange = e => icsc(pc2, "PC2", ice2Finished);
+ pc3.oniceconnectionstatechange = e => icsc(pc3, "PC3", ice3Finished);
+
+
+ function combineAnswer(origAnswer, answer) {
+ const sdplines = origAnswer.sdp.split('\r\n');
+ const fpIndex = sdplines.findIndex(l => l.match('^a=fingerprint'));
+ const FP = sdplines[fpIndex];
+ const audioIndex = sdplines.findIndex(l => l.match(/^m=audio [1-9]/));
+ const videoIndex = sdplines.findIndex(l => l.match(/^m=video [1-9]/));
+ if (audioIndex > -1) {
+ var ss = sdplines.slice(0, audioIndex);
+ ss.splice(fpIndex, 1);
+ answer.sessionSection = ss;
+ const rejectedVideoIndex = sdplines.findIndex(l => l.match('m=video 0'));
+ var ams = sdplines.slice(audioIndex, rejectedVideoIndex);
+ ams.push(FP);
+ ams.push(fakeFingerPrint);
+ answer.audioMsection = ams;
+ }
+ if (videoIndex > -1) {
+ var vms = sdplines.slice(videoIndex, sdplines.length -1);
+ vms.push(fakeFingerPrint);
+ vms.push(FP);
+ answer.videoMsection = vms;
+ }
+ return answer;
+ }
+
+runNetworkTest(async () => {
+ const v1 = createMediaElement('video', 'v1');
+ const v2 = createMediaElement('video', 'v2');
+ const v3 = createMediaElement('video', 'v3');
+
+ const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
+ (v1.srcObject = stream).getTracks().forEach(t => pc1.addTrack(t, stream));
+
+ const stream2 = await navigator.mediaDevices.getUserMedia({ video: true });
+ (v2.srcObject = stream2).getTracks().forEach(t => pc2.addTrack(t, stream2));
+
+ const stream3 = await navigator.mediaDevices.getUserMedia({ audio: true });
+ (v3.srcObject = stream3).getTracks().forEach(t => pc3.addTrack(t, stream3));
+
+ const offer = await pc1.createOffer();
+ await pc1.setLocalDescription(offer);
+
+ //info("Original OFFER: " + JSON.stringify(offer));
+ offer.sdp = sdputils.removeBundle(offer.sdp);
+ //info("OFFER w/o BUNDLE: " + JSON.stringify(offer));
+ const offerAudio = new RTCSessionDescription(JSON.parse(JSON.stringify(offer)));
+ offerAudio.sdp = offerAudio.sdp.replace('m=video 9', 'm=video 0');
+ //info("offerAudio: " + JSON.stringify(offerAudio));
+ const offerVideo = new RTCSessionDescription(JSON.parse(JSON.stringify(offer)));
+ offerVideo.sdp = offerVideo.sdp.replace('m=audio 9', 'm=audio 0');
+ //info("offerVideo: " + JSON.stringify(offerVideo));
+
+ // We need to do these in parallel, otherwise pc1 will start firing
+ // icecandidate events before pc3 is ready.
+ await Promise.all([pc2.setRemoteDescription(offerVideo), pc3.setRemoteDescription(offerAudio)]);
+
+ const answerVideo = await pc2.createAnswer();
+ const answerAudio = await pc3.createAnswer();
+
+ const answer = combineAnswer(answerAudio, combineAnswer(answerVideo, {}));
+ const fakeAnswer = answer.sessionSection.concat(answer.audioMsection, answer.videoMsection).join('\r\n');
+ info("ANSWER: " + fakeAnswer);
+
+ // We want to do these in parallel, because if we do them seqentially, by the
+ // time pc3.sLD completes pc2 could have fired icecandidate events, when we
+ // haven't called pc1.sRD yet.
+ await Promise.all(
+ [pc2.setLocalDescription(answerVideo),
+ pc3.setLocalDescription(answerAudio),
+ pc1.setRemoteDescription({type: 'answer', sdp: fakeAnswer})]);
+
+ await Promise.all([ice1Done, ice2Done, ice3Done]);
+
+ ok(true, "Connected.");
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_throwInCallbacks.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_throwInCallbacks.html
new file mode 100644
index 0000000000..5a3872c120
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_throwInCallbacks.html
@@ -0,0 +1,83 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "857765",
+ title: "Throw in PeerConnection callbacks"
+ });
+
+runNetworkTest(function () {
+ let finish;
+ const onfinished = new Promise(r => finish = async () => {
+ window.onerror = oldOnError;
+ is(error_count, 7, "Seven expected errors verified.");
+ r();
+ });
+
+ function getFail() {
+ return err => {
+ window.onerror = oldOnError;
+ generateErrorCallback()(err);
+ };
+ }
+
+ let error_count = 0;
+ let oldOnError = window.onerror;
+ window.onerror = (errorMsg, url, lineNumber) => {
+ if (!errorMsg.includes("Expected")) {
+ getFail()(errorMsg);
+ }
+ error_count += 1;
+ info("onerror " + error_count + ": " + errorMsg);
+ if (error_count == 7) {
+ finish();
+ }
+ throw new Error("window.onerror may throw");
+ return false;
+ }
+
+ let pc0, pc1, pc2;
+ // Test failure callbacks (limited to 1 for now)
+ pc0 = new RTCPeerConnection();
+ pc0.close();
+ pc0.createOffer(getFail(), function(err) {
+ pc1 = new RTCPeerConnection();
+ pc2 = new RTCPeerConnection();
+
+ // Test success callbacks (happy path)
+ navigator.mozGetUserMedia({video:true}, function(video1) {
+ pc1.addStream(video1);
+ pc1.createOffer(function(offer) {
+ pc1.setLocalDescription(offer, function() {
+ pc2.setRemoteDescription(offer, function() {
+ pc2.createAnswer(function(answer) {
+ pc2.setLocalDescription(answer, function() {
+ pc1.setRemoteDescription(answer, function() {
+ throw new Error("Expected");
+ }, getFail());
+ throw new Error("Expected");
+ }, getFail());
+ throw new Error("Expected");
+ }, getFail());
+ throw new Error("Expected");
+ }, getFail());
+ throw new Error("Expected");
+ }, getFail());
+ throw new Error("Expected");
+ }, getFail());
+ }, getFail());
+ throw new Error("Expected");
+ });
+
+ return onfinished;
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_toJSON.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_toJSON.html
new file mode 100644
index 0000000000..96c2c42b78
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_toJSON.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "928304",
+ title: "test toJSON() on RTCSessionDescription and RTCIceCandidate"
+ });
+
+ runNetworkTest(function () {
+ /** Test for Bug 872377 **/
+
+ var rtcSession = new RTCSessionDescription({ sdp: "Picklechips!",
+ type: "offer" });
+ var jsonCopy = JSON.parse(JSON.stringify(rtcSession));
+ for (key in rtcSession) {
+ if (typeof(rtcSession[key]) == "function") continue;
+ is(rtcSession[key], jsonCopy[key], "key " + key + " should match.");
+ }
+
+ /** Test for Bug 928304 **/
+
+ var rtcIceCandidate = new RTCIceCandidate({ candidate: "dummy",
+ sdpMid: "test",
+ sdpMLineIndex: 3 });
+ jsonCopy = JSON.parse(JSON.stringify(rtcIceCandidate));
+ for (key in rtcIceCandidate) {
+ if (typeof(rtcIceCandidate[key]) == "function") continue;
+ is(rtcIceCandidate[key], jsonCopy[key], "key " + key + " should match.");
+ }
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_trackDisabling.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_trackDisabling.html
new file mode 100644
index 0000000000..73323cf007
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_trackDisabling.html
@@ -0,0 +1,108 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1219711",
+ title: "Disabling locally should be reflected remotely"
+});
+
+runNetworkTest(async () => {
+ const test = new PeerConnectionTest();
+
+ await pushPrefs(
+ ["media.getusermedia.camera.stop_on_disable.enabled", true],
+ ["media.getusermedia.camera.stop_on_disable.delay_ms", 0],
+ ["media.getusermedia.microphone.stop_on_disable.enabled", true],
+ ["media.getusermedia.microphone.stop_on_disable.delay_ms", 0],
+ // Always use fake tracks since we depend on video to be somewhat green and
+ // audio to have a large 1000Hz component (or 440Hz if using fake devices).
+ ['media.audio_loopback_dev', ''],
+ ['media.video_loopback_dev', ''],
+ ['media.navigator.streams.fake', true]);
+
+ test.setMediaConstraints([{audio: true, video: true}], []);
+ test.chain.append([
+ function CHECK_ASSUMPTIONS() {
+ is(test.pcLocal.localMediaElements.length, 2,
+ "pcLocal should have one media element");
+ is(test.pcRemote.remoteMediaElements.length, 2,
+ "pcRemote should have one media element");
+ is(test.pcLocal._pc.getLocalStreams().length, 1,
+ "pcLocal should have one stream");
+ is(test.pcRemote._pc.getRemoteStreams().length, 1,
+ "pcRemote should have one stream");
+ },
+ async function CHECK_VIDEO() {
+ const h = new CaptureStreamTestHelper2D();
+ const localVideo = test.pcLocal.localMediaElements
+ .find(e => e instanceof HTMLVideoElement);
+ const remoteVideo = test.pcRemote.remoteMediaElements
+ .find(e => e instanceof HTMLVideoElement);
+ // We check a pixel somewhere away from the top left corner since
+ // MediaEngineFake puts semi-transparent time indicators there.
+ const offsetX = 50;
+ const offsetY = 50;
+ const threshold = 128;
+
+ // We're regarding black as disabled here, and we're setting the alpha
+ // channel of the pixel to 255 to disregard alpha when testing.
+ const checkVideoEnabled = video => h.waitForPixel(video,
+ px => (px[3] = 255, h.isPixelNot(px, h.black, threshold)),
+ { offsetX, offsetY }
+ );
+ const checkVideoDisabled = video => h.waitForPixel(video,
+ px => (px[3] = 255, h.isPixel(px, h.black, threshold)),
+ { offsetX, offsetY }
+ );
+
+ info("Checking local video enabled");
+ await checkVideoEnabled(localVideo);
+ info("Checking remote video enabled");
+ await checkVideoEnabled(remoteVideo);
+
+ info("Disabling original");
+ test.pcLocal._pc.getLocalStreams()[0].getVideoTracks()[0].enabled = false;
+
+ info("Checking local video disabled");
+ await checkVideoDisabled(localVideo);
+ info("Checking remote video disabled");
+ await checkVideoDisabled(remoteVideo);
+ },
+ async function CHECK_AUDIO() {
+ const ac = new AudioContext();
+ const localAnalyser = new AudioStreamAnalyser(ac, test.pcLocal._pc.getLocalStreams()[0]);
+ const remoteAnalyser = new AudioStreamAnalyser(ac, test.pcRemote._pc.getRemoteStreams()[0]);
+
+ const checkAudio = (analyser, fun) => analyser.waitForAnalysisSuccess(fun);
+
+ const freq = localAnalyser.binIndexForFrequency(TEST_AUDIO_FREQ);
+ const checkAudioEnabled = analyser =>
+ checkAudio(analyser, array => array[freq] > 200);
+ const checkAudioDisabled = analyser =>
+ checkAudio(analyser, array => array[freq] < 50);
+
+ info("Checking local audio enabled");
+ await checkAudioEnabled(localAnalyser);
+ info("Checking remote audio enabled");
+ await checkAudioEnabled(remoteAnalyser);
+
+ test.pcLocal._pc.getLocalStreams()[0].getAudioTracks()[0].enabled = false;
+
+ info("Checking local audio disabled");
+ await checkAudioDisabled(localAnalyser);
+ info("Checking remote audio disabled");
+ await checkAudioDisabled(remoteAnalyser);
+ },
+ ]);
+ await test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_trackDisabling_clones.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_trackDisabling_clones.html
new file mode 100644
index 0000000000..ae7647fa1a
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_trackDisabling_clones.html
@@ -0,0 +1,162 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1219711",
+ title: "Disabling locally should be reflected remotely, individually for clones"
+});
+
+runNetworkTest(async () => {
+ var test = new PeerConnectionTest();
+
+ await pushPrefs(
+ ["media.getusermedia.camera.stop_on_disable.enabled", true],
+ ["media.getusermedia.camera.stop_on_disable.delay_ms", 0],
+ ["media.getusermedia.microphone.stop_on_disable.enabled", true],
+ ["media.getusermedia.microphone.stop_on_disable.delay_ms", 0],
+ // Always use fake tracks since we depend on audio to have a large 1000Hz
+ // component.
+ ['media.audio_loopback_dev', ''],
+ ['media.navigator.streams.fake', true]);
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ var originalStream;
+ var localVideoOriginal;
+
+ test.setMediaConstraints([{audio: true, video: true}], []);
+ test.chain.replace("PC_LOCAL_GUM", [
+ function PC_LOCAL_GUM_CLONE() {
+ return getUserMedia(test.pcLocal.constraints[0]).then(stream => {
+ originalStream = stream;
+ localVideoOriginal =
+ createMediaElement("video", "local-original");
+ localVideoOriginal.srcObject = stream;
+ test.pcLocal.attachLocalStream(originalStream.clone());
+ });
+ }
+ ]);
+ test.chain.append([
+ function CHECK_ASSUMPTIONS() {
+ is(test.pcLocal.localMediaElements.length, 2,
+ "pcLocal should have one media element");
+ is(test.pcRemote.remoteMediaElements.length, 2,
+ "pcRemote should have one media element");
+ is(test.pcLocal._pc.getLocalStreams().length, 1,
+ "pcLocal should have one stream");
+ is(test.pcRemote._pc.getRemoteStreams().length, 1,
+ "pcRemote should have one stream");
+ },
+ async function CHECK_VIDEO() {
+ info("Checking video");
+ var h = new CaptureStreamTestHelper2D();
+ var localVideoClone = test.pcLocal.localMediaElements
+ .find(e => e instanceof HTMLVideoElement);
+ var remoteVideoClone = test.pcRemote.remoteMediaElements
+ .find(e => e instanceof HTMLVideoElement);
+
+ // We check a pixel somewhere away from the top left corner since
+ // MediaEngineFake puts semi-transparent time indicators there.
+ const offsetX = 50;
+ const offsetY = 50;
+ const threshold = 128;
+ const remoteDisabledColor = h.black;
+
+ // We're regarding black as disabled here, and we're setting the alpha
+ // channel of the pixel to 255 to disregard alpha when testing.
+ var checkVideoEnabled = video => h.waitForPixel(video,
+ px => (px[3] = 255, h.isPixelNot(px, h.black, threshold)),
+ { offsetX, offsetY }
+ );
+ var checkVideoDisabled = video => h.waitForPixel(video,
+ px => (px[3] = 255, h.isPixel(px, h.black, threshold)),
+ { offsetX, offsetY }
+ );
+
+ info("Checking local original enabled");
+ await checkVideoEnabled(localVideoOriginal);
+ info("Checking local clone enabled");
+ await checkVideoEnabled(localVideoClone);
+ info("Checking remote clone enabled");
+ await checkVideoEnabled(remoteVideoClone);
+
+ info("Disabling original");
+ originalStream.getVideoTracks()[0].enabled = false;
+
+ info("Checking local original disabled");
+ await checkVideoDisabled(localVideoOriginal);
+ info("Checking local clone enabled");
+ await checkVideoEnabled(localVideoClone);
+ info("Checking remote clone enabled");
+ await checkVideoEnabled(remoteVideoClone);
+
+ info("Re-enabling original; disabling clone");
+ originalStream.getVideoTracks()[0].enabled = true;
+ test.pcLocal._pc.getLocalStreams()[0].getVideoTracks()[0].enabled = false;
+
+ info("Checking local original enabled");
+ await checkVideoEnabled(localVideoOriginal);
+ info("Checking local clone disabled");
+ await checkVideoDisabled(localVideoClone);
+ info("Checking remote clone disabled");
+ await checkVideoDisabled(remoteVideoClone);
+ },
+ async function CHECK_AUDIO() {
+ info("Checking audio");
+ var ac = new AudioContext();
+ var localAnalyserOriginal = new AudioStreamAnalyser(ac, originalStream);
+ var localAnalyserClone =
+ new AudioStreamAnalyser(ac, test.pcLocal._pc.getLocalStreams()[0]);
+ var remoteAnalyserClone =
+ new AudioStreamAnalyser(ac, test.pcRemote._pc.getRemoteStreams()[0]);
+
+ var freq = localAnalyserOriginal.binIndexForFrequency(TEST_AUDIO_FREQ);
+ var checkAudioEnabled = analyser =>
+ analyser.waitForAnalysisSuccess(array => array[freq] > 200);
+ var checkAudioDisabled = analyser =>
+ analyser.waitForAnalysisSuccess(array => array[freq] < 50);
+
+ info("Checking local original enabled");
+ await checkAudioEnabled(localAnalyserOriginal);
+ info("Checking local clone enabled");
+ await checkAudioEnabled(localAnalyserClone);
+ info("Checking remote clone enabled");
+ await checkAudioEnabled(remoteAnalyserClone);
+
+ info("Disabling original");
+ originalStream.getAudioTracks()[0].enabled = false;
+
+ info("Checking local original disabled");
+ await checkAudioDisabled(localAnalyserOriginal);
+ info("Checking local clone enabled");
+ await checkAudioEnabled(localAnalyserClone);
+ info("Checking remote clone enabled");
+ await checkAudioEnabled(remoteAnalyserClone);
+
+ info("Re-enabling original; disabling clone");
+ originalStream.getAudioTracks()[0].enabled = true;
+ test.pcLocal._pc.getLocalStreams()[0].getAudioTracks()[0].enabled = false;
+
+ info("Checking local original enabled");
+ await checkAudioEnabled(localAnalyserOriginal);
+ info("Checking local clone disabled");
+ await checkAudioDisabled(localAnalyserClone);
+ info("Checking remote clone disabled");
+ await checkAudioDisabled(remoteAnalyserClone);
+ },
+ ]);
+ await test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_trackless_sender_stats.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_trackless_sender_stats.html
new file mode 100644
index 0000000000..f0356f5655
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_trackless_sender_stats.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1452673",
+ title: "Trackless RTCRtpSender.getStats()",
+ visible: true
+ });
+
+ // Calling getstats() on a trackless RTCRtpSender should yield an empty
+ // stats report. When track stats are added in the future, the stats
+ // for the removed tracks should continue to appear.
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ test.chain.removeAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW");
+ test.chain.append(
+ async function PC_LOCAL_AND_REMOTE_TRACKLESS_SENDER_STATS(test) {
+ await Promise.all([
+ waitForSyncedRtcp(test.pcLocal._pc),
+ waitForSyncedRtcp(test.pcRemote._pc),
+ ]);
+ let senders = test.pcLocal.getSenders();
+ let receivers = test.pcRemote.getReceivers();
+ is(senders.length, 2, "Have exactly two senders.");
+ is(receivers.length, 2, "Have exactly two receivers.");
+ for(let kind of ["audio", "video"]) {
+ is(senders.filter(s => s.track.kind == kind).length, 1,
+ "Exactly 1 sender of kind " + kind);
+ is(receivers.filter(r => r.track.kind == kind).length, 1,
+ "Exactly 1 receiver of kind " + kind);
+ }
+ // Remove tracks from senders
+ for (const sender of senders) {
+ await sender.replaceTrack(null);
+ is(sender.track, null, "Sender track removed");
+ let stats = await sender.getStats();
+ ok(stats instanceof window.RTCStatsReport, "Stats is instance of RTCStatsReport");
+ // Number of stats in the report. This should be 0.
+ is(stats.size, 0, "Trackless sender stats report is empty");
+ }
+ }
+ );
+ test.setMediaConstraints([{audio: true}, {video: true}], []);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioStreams.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioStreams.html
new file mode 100644
index 0000000000..7ea18ab3dd
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioStreams.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1091242",
+ title: "Multistream: Two audio streams"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}, {audio: true}],
+ [{audio: true}, {audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioTracksInOneStream.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioTracksInOneStream.html
new file mode 100644
index 0000000000..99d4ad625a
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioTracksInOneStream.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1145407",
+ title: "Multistream: Two audio tracks in one stream"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ test.chain.insertAfter("PC_REMOTE_GET_OFFER", [
+ function PC_REMOTE_OVERRIDE_STREAM_IDS_IN_OFFER(test) {
+ test._local_offer.sdp = test._local_offer.sdp.replace(
+ /a=msid:[^\s]*/g,
+ "a=msid:foo");
+ }
+ ]);
+ test.chain.insertAfter("PC_LOCAL_GET_ANSWER", [
+ function PC_LOCAL_OVERRIDE_STREAM_IDS_IN_ANSWER(test) {
+ test._remote_answer.sdp = test._remote_answer.sdp.replace(
+ /a=msid:[^\s]*/g,
+ "a=msid:foo");
+ }
+ ]);
+ test.setMediaConstraints([{audio: true}, {audio: true}],
+ [{audio: true}, {audio: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreams.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreams.html
new file mode 100644
index 0000000000..5f4bd463d4
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreams.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+
+ createHTML({
+ bug: "1091242",
+ title: "Multistream: Two audio streams, two video streams"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true}, {video: true}, {audio: true},
+ {video: true}],
+ [{audio: true}, {video: true}, {audio: true},
+ {video: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreamsCombined.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreamsCombined.html
new file mode 100644
index 0000000000..fcc9c6c8fa
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreamsCombined.html
@@ -0,0 +1,70 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+
+ createHTML({
+ bug: "1091242",
+ title: "Multistream: Two audio/video streams"
+ });
+
+ runNetworkTest(async (options) => {
+ // Disable platform encodre for SW MFT encoder causes some stats
+ // exceeding the test thresholds.
+ // E.g. inbound-rtp.packetsDiscarded value=118 >= 100.
+ await matchPlatformH264CodecPrefs();
+
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{audio: true, video: true},
+ {audio: true, video: true}],
+ [{audio: true, video: true},
+ {audio: true, video: true}]);
+
+ // Test stats, including coalescing of codec stats.
+ test.chain.insertAfter("PC_LOCAL_WAIT_FOR_MEDIA_FLOW",
+ [PC_LOCAL_TEST_LOCAL_STATS]);
+
+ test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW",
+ [PC_REMOTE_TEST_REMOTE_STATS]);
+
+ const testCoalescedCodecStats = stats => {
+ is([...stats.values()].filter(({type}) => type.endsWith("rtp")).length,
+ 16,
+ "Expected: 4 outbound, 4 remote-inbound, 4 inbound, 4 remote-inbound");
+ const codecs = [...stats.values()]
+ .filter(({type}) => type == "codec")
+ .sort((a, b) => a.mimeType > b.mimeType);
+ is(codecs.length, 2, "Should have registered two codecs (coalesced)");
+ is(new Set(codecs.map(({transportId}) => transportId)).size, 1,
+ "Should have registered only one transport with BUNDLE");
+ const codecTypes = new Set(codecs.map(({codecType}) => codecType));
+ is(codecTypes.size, 1,
+ "Should have identical encode and decode configurations (and stats)");
+ is(codecTypes[0], undefined,
+ "Should have identical encode and decode configurations (and stats)");
+ is(codecs[0].mimeType.slice(0, 5), "audio",
+ "Should have registered an audio codec");
+ is(codecs[1].mimeType.slice(0, 5), "video",
+ "Should have registered a video codec");
+ };
+
+ test.chain.append([
+ async function PC_LOCAL_TEST_COALESCED_CODEC_STATS() {
+ testCoalescedCodecStats(await test.pcLocal._pc.getStats());
+ },
+ async function PC_REMOTE_TEST_COALESCED_CODEC_STATS() {
+ testCoalescedCodecStats(await test.pcRemote._pc.getStats());
+ },
+ ]);
+
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreamsCombinedNoBundle.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreamsCombinedNoBundle.html
new file mode 100644
index 0000000000..8b825db617
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoAudioVideoStreamsCombinedNoBundle.html
@@ -0,0 +1,107 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1225722",
+ title: "Multistream: Two audio/video streams without BUNDLE"
+});
+
+runNetworkTest(async (options = {}) => {
+ // Disable platform encodre for SW MFT encoder causes some stats
+ // exceeding the test thresholds.
+ // E.g. inbound-rtp.packetsDiscarded value=118 >= 100.
+ await matchPlatformH264CodecPrefs();
+
+ options.bundle = false;
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints(
+ [{audio: true, video: true}, {audio: true, video: true}],
+ [{audio: true, video: true}, {audio: true, video: true}]
+ );
+
+ // Test stats, including that codec stats do not coalesce without BUNDLE.
+ const testNonBundledStats = async pc => {
+ // This is basically PC_*_TEST_*_STATS fleshed out, but uses
+ // sender/receiver.getStats instead of pc.getStats, since the codec stats
+ // code assumes at most one sender and at most one receiver.
+ await waitForSyncedRtcp(pc);
+ const senderPromises = pc.getSenders().map(obj => obj.getStats());
+ const receiverPromises = pc.getReceivers().map(obj => obj.getStats());
+ const senderStats = await Promise.all(senderPromises);
+ const receiverStats = await Promise.all(receiverPromises);
+ for (const stats of [...senderStats, ...receiverStats]) {
+ checkExpectedFields(stats);
+ pedanticChecks(stats);
+ }
+ for (const stats of senderStats) {
+ checkSenderStats(stats, 1);
+ }
+ };
+
+ test.chain.insertAfter("PC_LOCAL_WAIT_FOR_MEDIA_FLOW", [
+ async function PC_LOCAL_TEST_LOCAL_NONBUNDLED_STATS(test) {
+ await testNonBundledStats(test.pcLocal._pc);
+ },
+ ]);
+
+ test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW", [
+ async function PC_REMOTE_TEST_LOCAL_NONBUNDLED_STATS(test) {
+ await testNonBundledStats(test.pcRemote._pc);
+ },
+ ]);
+
+ const testNonCoalescedCodecStats = stats => {
+ const codecs = [...stats.values()]
+ .filter(({type}) => type == "codec");
+ is([...stats.values()].filter(({type}) => type.endsWith("rtp")).length, 16,
+ "Expected: 4 outbound, 4 remote-inbound, 4 inbound, 4 remote-inbound");
+ const codecTypes = new Set(codecs.map(({codecType}) => codecType));
+ is(codecTypes.size, 1,
+ "Should have identical encode and decode configurations (and stats)");
+ is(codecTypes[0], undefined,
+ "Should have identical encode and decode configurations (and stats)");
+ const transportIds = new Set(codecs.map(({transportId}) => transportId));
+ is(transportIds.size, 4,
+ "Should have registered four transports for two sendrecv streams");
+ for (const transportId of transportIds) {
+ is(codecs.filter(c => c.transportId == transportId).length, 1,
+ "Should have registered one codec per transport without BUNDLE");
+ }
+ for (const prefix of ["audio", "video"]) {
+ const prefixed = codecs.filter(c => c.mimeType.startsWith(prefix));
+ is(prefixed.length, 2, `Should have registered two ${prefix} codecs`);
+ if (prefixed.length == 2) {
+ is(prefixed[0].payloadType, prefixed[1].payloadType,
+ "same payloadType");
+ isnot(prefixed[0].transportId, prefixed[1].transportId,
+ "different transportIds");
+ is(prefixed[0].mimeType, prefixed[1].mimeType, "same mimeType");
+ is(prefixed[0].clockRate, prefixed[1].clockRate, "same clockRate");
+ is(prefixed[0].channels, prefixed[1].channels, "same channels");
+ is(prefixed[0].sdpFmtpLine, prefixed[1].sdpFmtpLine,
+ "same sdpFmtpLine");
+ }
+ }
+ };
+
+ test.chain.append([
+ async function PC_LOCAL_TEST_NON_COALESCED_CODEC_STATS() {
+ testNonCoalescedCodecStats(await test.pcLocal._pc.getStats());
+ },
+ async function PC_REMOTE_TEST_NON_COALESCED_CODEC_STATS() {
+ testNonCoalescedCodecStats(await test.pcRemote._pc.getStats());
+ },
+ ]);
+
+ return test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_twoVideoStreams.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoVideoStreams.html
new file mode 100644
index 0000000000..0ab180cc55
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoVideoStreams.html
@@ -0,0 +1,23 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1091242",
+ title: "Multistream: Two video streams"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{video: true}, {video: true}],
+ [{video: true}, {video: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_twoVideoTracksInOneStream.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoVideoTracksInOneStream.html
new file mode 100644
index 0000000000..4eaf8b3f48
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_twoVideoTracksInOneStream.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1145407",
+ title: "Multistream: Two video tracks in offerer stream"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ test.chain.insertAfter("PC_REMOTE_GET_OFFER", [
+ function PC_REMOTE_OVERRIDE_STREAM_IDS_IN_OFFER(test) {
+ test._local_offer.sdp = test._local_offer.sdp.replace(
+ /a=msid:[^\s]*/g,
+ "a=msid:foo");
+ }
+ ]);
+ test.chain.insertAfter("PC_LOCAL_GET_ANSWER", [
+ function PC_LOCAL_OVERRIDE_STREAM_IDS_IN_ANSWER(test) {
+ test._remote_answer.sdp = test._remote_answer.sdp.replace(
+ /a=msid:[^\s]*/g,
+ "a=msid:foo");
+ }
+ ]);
+ test.setMediaConstraints([{video: true}, {video: true}],
+ [{video: true}, {video: true}]);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_verifyAudioAfterRenegotiation.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_verifyAudioAfterRenegotiation.html
new file mode 100644
index 0000000000..86ef6d4678
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_verifyAudioAfterRenegotiation.html
@@ -0,0 +1,99 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1166832",
+ title: "Renegotiation: verify audio after renegotiation"
+ });
+
+ runNetworkTest(function (options) {
+ const test = new PeerConnectionTest(options);
+ const helper = new AudioStreamHelper();
+
+ test.chain.append([
+ function CHECK_ASSUMPTIONS() {
+ is(test.pcLocal.localMediaElements.length, 1,
+ "pcLocal should have one media element");
+ is(test.pcRemote.remoteMediaElements.length, 1,
+ "pcRemote should have one media element");
+ is(test.pcLocal._pc.getLocalStreams().length, 1,
+ "pcLocal should have one stream");
+ is(test.pcRemote._pc.getRemoteStreams().length, 1,
+ "pcRemote should have one stream");
+ },
+ function CHECK_AUDIO() {
+ return Promise.resolve()
+ .then(() => info("Checking local audio enabled"))
+ .then(() => helper.checkAudioFlowing(test.pcLocal._pc.getLocalStreams()[0]))
+ .then(() => info("Checking remote audio enabled"))
+ .then(() => helper.checkAudioFlowing(test.pcRemote._pc.getRemoteStreams()[0]))
+
+ .then(() => test.pcLocal._pc.getLocalStreams()[0].getAudioTracks()[0].enabled = false)
+
+ .then(() => info("Checking local audio disabled"))
+ .then(() => helper.checkAudioNotFlowing(test.pcLocal._pc.getLocalStreams()[0]))
+ .then(() => info("Checking remote audio disabled"))
+ .then(() => helper.checkAudioNotFlowing(test.pcRemote._pc.getRemoteStreams()[0]))
+ }
+ ]);
+
+ addRenegotiation(test.chain,
+ [
+ function PC_LOCAL_ADD_SECOND_STREAM(test) {
+ test.setMediaConstraints([{audio: true}],
+ []);
+ return test.pcLocal.getAllUserMediaAndAddStreams([{audio: true}]);
+ },
+ ]
+ );
+
+ test.chain.append([
+ function CHECK_ASSUMPTIONS2() {
+ is(test.pcLocal.localMediaElements.length, 2,
+ "pcLocal should have two media elements");
+ is(test.pcRemote.remoteMediaElements.length, 2,
+ "pcRemote should have two media elements");
+ is(test.pcLocal._pc.getLocalStreams().length, 2,
+ "pcLocal should have two streams");
+ is(test.pcRemote._pc.getRemoteStreams().length, 2,
+ "pcRemote should have two streams");
+ },
+ function RE_CHECK_AUDIO() {
+ return Promise.resolve()
+ .then(() => info("Checking local audio enabled"))
+ .then(() => helper.checkAudioNotFlowing(test.pcLocal._pc.getLocalStreams()[0]))
+ .then(() => info("Checking remote audio enabled"))
+ .then(() => helper.checkAudioNotFlowing(test.pcRemote._pc.getRemoteStreams()[0]))
+
+ .then(() => info("Checking local2 audio enabled"))
+ .then(() => helper.checkAudioFlowing(test.pcLocal._pc.getLocalStreams()[1]))
+ .then(() => info("Checking remote2 audio enabled"))
+ .then(() => helper.checkAudioFlowing(test.pcRemote._pc.getRemoteStreams()[1]))
+
+ .then(() => test.pcLocal._pc.getLocalStreams()[1].getAudioTracks()[0].enabled = false)
+ .then(() => test.pcLocal._pc.getLocalStreams()[0].getAudioTracks()[0].enabled = true)
+
+ .then(() => info("Checking local2 audio disabled"))
+ .then(() => helper.checkAudioNotFlowing(test.pcLocal._pc.getLocalStreams()[1]))
+ .then(() => info("Checking remote2 audio disabled"))
+ .then(() => helper.checkAudioNotFlowing(test.pcRemote._pc.getRemoteStreams()[1]))
+
+ .then(() => info("Checking local audio enabled"))
+ .then(() => helper.checkAudioFlowing(test.pcLocal._pc.getLocalStreams()[0]))
+ .then(() => info("Checking remote audio enabled"))
+ .then(() => helper.checkAudioFlowing(test.pcRemote._pc.getRemoteStreams()[0]))
+ }
+ ]);
+
+ test.setMediaConstraints([{audio: true}], []);
+ return test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_verifyDescriptions.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_verifyDescriptions.html
new file mode 100644
index 0000000000..f685f7c99a
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_verifyDescriptions.html
@@ -0,0 +1,58 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1264479",
+ title: "PeerConnection verify current and pending descriptions"
+ });
+
+ const pc1 = new RTCPeerConnection();
+ const pc2 = new RTCPeerConnection();
+
+ var add = (pc, can, failed) => can && pc.addIceCandidate(can).catch(failed);
+ pc1.onicecandidate = e => add(pc2, e.candidate, generateErrorCallback());
+ pc2.onicecandidate = e => add(pc1, e.candidate, generateErrorCallback());
+
+
+ runNetworkTest(function() {
+ const v1 = createMediaElement('video', 'v1');
+ const v2 = createMediaElement('video', 'v2');
+
+ return navigator.mediaDevices.getUserMedia({ video: true, audio: true })
+ .then(stream => (v1.srcObject = stream).getTracks().forEach(t => pc1.addTrack(t, stream)))
+ .then(() => pc1.createOffer({})) // check that createOffer accepts arg.
+ .then(offer => pc1.setLocalDescription(offer))
+ .then(() => {
+ ok(!pc1.currentLocalDescription, "pc1 currentLocalDescription is empty");
+ ok(pc1.pendingLocalDescription, "pc1 pendingLocalDescription is set");
+ ok(pc1.localDescription, "pc1 localDescription is set");
+ })
+ .then(() => pc2.setRemoteDescription(pc1.localDescription))
+ .then(() => {
+ ok(!pc2.currentRemoteDescription, "pc2 currentRemoteDescription is empty");
+ ok(pc2.pendingRemoteDescription, "pc2 pendingRemoteDescription is set");
+ ok(pc2.remoteDescription, "pc2 remoteDescription is set");
+ })
+ .then(() => pc2.createAnswer({})) // check that createAnswer accepts arg.
+ .then(answer => pc2.setLocalDescription(answer))
+ .then(() => {
+ ok(pc2.currentLocalDescription, "pc2 currentLocalDescription is set");
+ ok(!pc2.pendingLocalDescription, "pc2 pendingLocalDescription is empty");
+ ok(pc2.localDescription, "pc2 localDescription is set");
+ })
+ .then(() => pc1.setRemoteDescription(pc2.localDescription))
+ .then(() => {
+ ok(pc1.currentRemoteDescription, "pc1 currentRemoteDescription is set");
+ ok(!pc1.pendingRemoteDescription, "pc1 pendingRemoteDescription is empty");
+ ok(pc1.remoteDescription, "pc1 remoteDescription is set");
+ });
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_verifyVideoAfterRenegotiation.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_verifyVideoAfterRenegotiation.html
new file mode 100644
index 0000000000..8d4155ddff
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_verifyVideoAfterRenegotiation.html
@@ -0,0 +1,123 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1166832",
+ title: "Renegotiation: verify video after renegotiation"
+ });
+
+runNetworkTest(async () => {
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ const test = new PeerConnectionTest();
+
+ const h1 = new CaptureStreamTestHelper2D(50, 50);
+ const canvas1 = h1.createAndAppendElement('canvas', 'source_canvas1');
+ let stream1;
+ let vremote1;
+
+ const h2 = new CaptureStreamTestHelper2D(50, 50);
+ let canvas2;
+ let stream2;
+ let vremote2;
+
+ test.setMediaConstraints([{video: true}], []);
+ test.chain.replace("PC_LOCAL_GUM", [
+ function DRAW_INITIAL_LOCAL_GREEN(test) {
+ h1.drawColor(canvas1, h1.green);
+ },
+ function PC_LOCAL_CANVAS_CAPTURESTREAM(test) {
+ stream1 = canvas1.captureStream(0);
+ test.pcLocal.attachLocalStream(stream1);
+ let i = 0;
+ return setInterval(function() {
+ try {
+ info("draw " + i ? "green" : "red");
+ h1.drawColor(canvas1, i ? h1.green : h1.red);
+ i = 1 - i;
+ stream1.requestFrame();
+ if (stream2 != null) {
+ h2.drawColor(canvas2, i ? h2.green : h2.blue);
+ stream2.requestFrame();
+ }
+ } catch (e) {
+ // ignore; stream might have shut down, and we don't bother clearing
+ // the setInterval.
+ }
+ }, 500);
+ }
+ ]);
+
+ test.chain.append([
+ function FIND_REMOTE_VIDEO() {
+ vremote1 = test.pcRemote.remoteMediaElements[0];
+ ok(!!vremote1, "Should have remote video element for pcRemote");
+ },
+ function WAIT_FOR_REMOTE_GREEN() {
+ return h1.pixelMustBecome(vremote1, h1.green, {
+ threshold: 128,
+ infoString: "pcRemote's remote should become green",
+ });
+ },
+ function WAIT_FOR_REMOTE_RED() {
+ return h1.pixelMustBecome(vremote1, h1.red, {
+ threshold: 128,
+ infoString: "pcRemote's remote should become red",
+ });
+ }
+ ]);
+
+ addRenegotiation(test.chain,
+ [
+ function PC_LOCAL_ADD_SECOND_STREAM(test) {
+ canvas2 = h2.createAndAppendElement('canvas', 'source_canvas2');
+ h2.drawColor(canvas2, h2.blue);
+ stream2 = canvas2.captureStream(0);
+
+ // can't use test.pcLocal.getAllUserMediaAndAddStreams([{video: true}]);
+ // because it doesn't let us substitute the capture stream
+ test.pcLocal.attachLocalStream(stream2);
+ }
+ ]
+ );
+
+ test.chain.append([
+ function FIND_REMOTE2_VIDEO() {
+ vremote2 = test.pcRemote.remoteMediaElements[1];
+ ok(!!vremote2, "Should have remote2 video element for pcRemote");
+ },
+ function WAIT_FOR_REMOTE2_BLUE() {
+ return h2.pixelMustBecome(vremote2, h2.blue, {
+ threshold: 128,
+ infoString: "pcRemote's remote2 should become blue",
+ });
+ },
+ function DRAW_NEW_LOCAL_GREEN(test) {
+ stream1.requestFrame();
+ h1.drawColor(canvas1, h1.green);
+ },
+ function WAIT_FOR_REMOTE1_GREEN() {
+ return h1.pixelMustBecome(vremote1, h1.green, {
+ threshold: 128,
+ infoString: "pcRemote's remote1 should become green",
+ });
+ }
+ ]);
+
+ await test.run();
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_videoCodecs.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_videoCodecs.html
new file mode 100644
index 0000000000..7a245b5d8c
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_videoCodecs.html
@@ -0,0 +1,142 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="stats.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1395853",
+ title: "Verify video content over WebRTC for every video codec",
+ });
+
+ async function testVideoCodec(options = {}, codec) {
+ const test = new PeerConnectionTest(options);
+ test.setMediaConstraints([{video: true}], []);
+
+ let payloadType;
+ test.chain.insertBefore("PC_LOCAL_SET_LOCAL_DESCRIPTION", [
+ function PC_LOCAL_FILTER_OUT_CODECS() {
+ const otherCodec = codecs.find(c => c != codec);
+ const otherId = sdputils.findCodecId(test.originalOffer.sdp, otherCodec.name, otherCodec.offset);
+ const otherRtpmapMatcher = new RegExp(`a=rtpmap:${otherId}.*\\r\\n`, "gi");
+
+ const id = sdputils.findCodecId(test.originalOffer.sdp, codec.name, codec.offset);
+ payloadType = Number(id);
+ if (codec.offset) {
+ isnot(id, sdputils.findCodecId(test.originalOffer.sdp, codec.name, 0),
+ "Different offsets should return different payload types");
+ }
+ test.originalOffer.sdp =
+ sdputils.removeAllButPayloadType(test.originalOffer.sdp, id);
+
+ ok(!test.originalOffer.sdp.match(new RegExp(`m=.*UDP/TLS/RTP/SAVPF.* ${otherId}[^0-9]`, "gi")),
+ `Other codec ${otherId} should be removed after filtering`);
+ ok(test.originalOffer.sdp.match(new RegExp(`m=.*UDP/TLS/RTP/SAVPF.* ${id}[^0-9]`, "gi")),
+ `Tested codec ${id} should remain after filtering`);
+
+ // We only set it now, or the framework would remove non-H264 codecs
+ // for us.
+ options.h264 = codec.name == "H264";
+ },
+ ]);
+
+ test.chain.insertAfter("PC_LOCAL_WAIT_FOR_MEDIA_FLOW",
+ [PC_LOCAL_TEST_LOCAL_STATS]);
+
+ test.chain.insertAfter("PC_REMOTE_WAIT_FOR_MEDIA_FLOW",
+ [PC_REMOTE_TEST_REMOTE_STATS]);
+
+ test.chain.append([
+ async function PC_LOCAL_TEST_CODEC() {
+ const stats = await test.pcLocal._pc.getStats();
+ let codecCount = 0;
+ stats.forEach(stat => {
+ if (stat.type == "codec") {
+ is(codecCount++, 0, "expected only one encode codec stat");
+ is(stat.payloadType, payloadType, "payloadType as expected");
+ is(stat.mimeType, `video/${codec.name}`, "mimeType as expected");
+ is(stat.codecType, "encode", "codecType as expected");
+ }
+ });
+ },
+ async function PC_REMOTE_TEST_CODEC() {
+ const stats = await test.pcRemote._pc.getStats();
+ let codecCount = 0;
+ stats.forEach(stat => {
+ if (stat.type == "codec") {
+ is(codecCount++, 0, "expected only one decode codec stat");
+ is(stat.payloadType, payloadType, "payloadType as expected");
+ is(stat.mimeType, `video/${codec.name}`, "mimeType as expected");
+ is(stat.codecType, "decode", "codecType as expected");
+ }
+ });
+ },
+ async function CHECK_VIDEO_FLOW() {
+ try {
+ const h = new VideoStreamHelper();
+ await h.checkVideoPlaying(
+ test.pcRemote.remoteMediaElements[0],
+ 10, 10, 128);
+ ok(true, `Got video flow for codec ${codec.name}, offset ${codec.offset}`);
+ } catch(e) {
+ ok(false, `No video flow for codec ${codec.name}, offset ${codec.offset}: ${e}`);
+ }
+ },
+ ]);
+
+ await test.run();
+ }
+
+ // We match the name against the sdp to figure out the payload type,
+ // so all other present codecs can be removed.
+ // Use `offset` when there are multiple instances of a codec expected in an sdp.
+ const codecs = [
+ { name: "VP8" },
+ { name: "VP9" },
+ { name: "H264" },
+ { name: "H264", offset: 1 },
+ ];
+
+ runNetworkTest(async (options) => {
+ // This test expects the video being captured will change color. Use fake
+ // video device as loopback does not currently change.
+ await pushPrefs(
+ ['media.video_loopback_dev', ''],
+ ['media.navigator.streams.fake', true]);
+ for (let codec of codecs) {
+ info(`Testing video for codec ${codec.name} offset ${codec.offset}`);
+ try {
+ let enc = SpecialPowers.getBoolPref('media.webrtc.platformencoder');
+ let dec = SpecialPowers.getBoolPref('media.navigator.mediadatadecoder_h264_enabled');
+ if (codec.name == "H264") {
+ await matchPlatformH264CodecPrefs();
+ if (codec.offset == 1) {
+ // Force fake GMP codec for H.264 mode 0 because not all platforms
+ // support slice size control. Re-enable it after
+ // a. SW encoder fallback support (bug 1726617), and
+ // b. returning valid bitstream from fake GMP encoder (bug 1509012).
+ await pushPrefs(
+ ['media.webrtc.platformencoder', false],
+ ['media.navigator.mediadatadecoder_h264_enabled', false],
+ );
+ }
+ }
+ await testVideoCodec(options, codec);
+ await pushPrefs(
+ ['media.webrtc.platformencoder', enc],
+ ['media.navigator.mediadatadecoder_h264_enabled', dec],
+ );
+ } catch(e) {
+ ok(false, `Error in test for codec ${codec.name}: ${e}\n${e.stack}`);
+ }
+ info(`Tested video for codec ${codec.name}`);
+ }
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_videoRenegotiationInactiveAnswer.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_videoRenegotiationInactiveAnswer.html
new file mode 100644
index 0000000000..b77633493d
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_videoRenegotiationInactiveAnswer.html
@@ -0,0 +1,95 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+ <script type="application/javascript" src="sdpUtils.js"></script>
+ <script type="application/javascript" src="/tests/dom/canvas/test/captureStream_common.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ bug: "1213773",
+ title: "Renegotiation: answerer uses a=inactive for video"
+ });
+
+ runNetworkTest(async (options) => {
+ // [TODO] re-enable HW decoder after bug 1526207 is fixed.
+ if (navigator.userAgent.includes("Android")) {
+ await pushPrefs(["media.navigator.mediadatadecoder_vpx_enabled", false],
+ ["media.webrtc.hw.h264.enabled", false]);
+ }
+
+ const emitter = new VideoFrameEmitter();
+ const helper = new VideoStreamHelper();
+
+ const test = new PeerConnectionTest(options);
+
+ test.chain.replace("PC_LOCAL_GUM", [
+ function PC_LOCAL_CANVAS_CAPTURESTREAM(test) {
+ test.pcLocal.attachLocalStream(emitter.stream());
+ emitter.start();
+ }
+ ]);
+
+ var haveFirstUnmuteEvent;
+
+ test.chain.insertBefore("PC_REMOTE_SET_LOCAL_DESCRIPTION", [
+ function PC_REMOTE_SETUP_ONUNMUTE_1() {
+ haveFirstUnmuteEvent = haveEvent(test.pcRemote._pc.getReceivers()[0].track, "unmute");
+ }
+ ]);
+
+ test.chain.append([
+ function PC_REMOTE_CHECK_VIDEO_UNMUTED() {
+ return haveFirstUnmuteEvent;
+ },
+ function PC_REMOTE_WAIT_FOR_FRAMES() {
+ var vremote = test.pcRemote.remoteMediaElements[0];
+ ok(vremote, "Should have remote video element for pcRemote");
+ return addFinallyToPromise(helper.checkVideoPlaying(vremote))
+ .finally(() => emitter.stop());
+ }
+ ]);
+
+ addRenegotiation(test.chain, []);
+
+ test.chain.insertAfter("PC_LOCAL_GET_ANSWER", [
+ function PC_LOCAL_REWRITE_REMOTE_SDP_INACTIVE(test) {
+ test._remote_answer.sdp =
+ sdputils.setAllMsectionsInactive(test._remote_answer.sdp);
+ }
+ ], false, 1);
+
+ test.chain.append([
+ function PC_REMOTE_ENSURE_NO_FRAMES() {
+ var vremote = test.pcRemote.remoteMediaElements[0];
+ ok(vremote, "Should have remote video element for pcRemote");
+ emitter.start();
+ return addFinallyToPromise(helper.checkVideoPaused(vremote))
+ .finally(() => emitter.stop());
+ },
+ ]);
+
+ test.chain.remove("PC_REMOTE_CHECK_STATS", 1);
+ test.chain.remove("PC_LOCAL_CHECK_STATS", 1);
+
+ addRenegotiation(test.chain, []);
+
+ test.chain.append([
+ function PC_REMOTE_WAIT_FOR_FRAMES_2() {
+ var vremote = test.pcRemote.remoteMediaElements[0];
+ ok(vremote, "Should have remote video element for pcRemote");
+ emitter.start();
+ return addFinallyToPromise(helper.checkVideoPlaying(vremote))
+ .finally(() => emitter.stop());
+ }
+ ]);
+
+ test.setMediaConstraints([{video: true}], []);
+ await test.run();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_peerConnection_webAudio.html b/dom/media/webrtc/tests/mochitests/test_peerConnection_webAudio.html
new file mode 100644
index 0000000000..1d695ecbfa
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_peerConnection_webAudio.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ bug: "1081819",
+ title: "WebAudio on both input and output side of peerconnection"
+});
+
+// This tests WebAudio (a 700Hz OscillatorNode) as input to a PeerConnection.
+// It also tests that a PeerConnection works as input to WebAudio as the remote
+// stream is connected to an AnalyserNode and compared to the source node.
+
+runNetworkTest(function() {
+ const test = new PeerConnectionTest();
+ test.audioContext = new AudioContext();
+ test.setMediaConstraints([{audio: true}], []);
+ test.chain.replace("PC_LOCAL_GUM", [
+ function PC_LOCAL_WEBAUDIO_SOURCE(test) {
+ const oscillator = test.audioContext.createOscillator();
+ oscillator.type = 'sine';
+ oscillator.frequency.value = 700;
+ oscillator.start();
+ const dest = test.audioContext.createMediaStreamDestination();
+ oscillator.connect(dest);
+ test.pcLocal.attachLocalStream(dest.stream);
+ }
+ ]);
+ test.chain.append([
+ function CHECK_AUDIO_FLOW(test) {
+ return test.pcRemote.checkReceivingToneFrom(test.audioContext, test.pcLocal);
+ }
+ ]);
+ return test.run();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_selftest.html b/dom/media/webrtc/tests/mochitests/test_selftest.html
new file mode 100644
index 0000000000..3f1ce1402d
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_selftest.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="pc.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+ createHTML({
+ title: "Self-test of harness functions",
+ visible: true
+ });
+
+function TEST(test) {}
+
+var catcher = func => {
+ try {
+ func();
+ return null;
+ } catch (e) {
+ return e.message;
+ }
+};
+
+runNetworkTest(() => {
+ var test = new PeerConnectionTest();
+ test.setMediaConstraints([{video: true}], [{video: true}]);
+ is(catcher(() => test.chain.replace("PC_LOCAL_SET_LOCAL_DESCRIPTION", TEST)),
+ null, "test.chain.replace works");
+ is(catcher(() => test.chain.replace("FOO", TEST)),
+ "Unknown test: FOO", "test.chain.replace catches typos");
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_setSinkId.html b/dom/media/webrtc/tests/mochitests/test_setSinkId.html
new file mode 100644
index 0000000000..0d85114a0e
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_setSinkId.html
@@ -0,0 +1,83 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script>
+ createHTML({
+ title: "SetSinkId in HTMLMediaElement",
+ bug: "934425",
+ });
+
+ const memoryReportPath = 'explicit/media/media-manager-aggregates';
+
+ /**
+ * Run a test to verify set sink id in audio element.
+ */
+ runTest(async () => {
+ await pushPrefs(["media.setsinkid.enabled", true]);
+
+ if (!SpecialPowers.getCharPref("media.audio_loopback_dev", "")) {
+ ok(false, "No loopback device set by framework. Try --use-test-media-devices");
+ return;
+ }
+
+ // Expose an audio output device.
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ await navigator.mediaDevices.selectAudioOutput();
+
+ const allDevices = await navigator.mediaDevices.enumerateDevices();
+ const audioDevices = allDevices.filter(({kind}) => kind == 'audiooutput');
+ is(audioDevices.length, 1, "Number of output devices found");
+
+ const audio = createMediaElement("audio", "audio");
+ document.body.appendChild(audio);
+
+ is(audio.sinkId, "", "Initial value is empty string");
+
+ const p = audio.setSinkId(audioDevices[0].deviceId);
+ is(audio.sinkId, "", "Value is unchanged upon function return");
+ is(await p, undefined, "promise resolves with undefined");
+ is(audio.sinkId, audioDevices[0].deviceId, `Sink device is set, id: ${audio.sinkId}`);
+
+ await audio.setSinkId(audioDevices[0].deviceId);
+ ok(true, `Sink device is set for 2nd time for the same id: ${audio.sinkId}`);
+
+ try {
+ await audio.setSinkId("dummy sink id");
+ ok(false, "Never enter here, this must fail");
+ } catch (error) {
+ ok(true, `Set sink id expected to fail: ${error}`);
+ is(error.name, "NotFoundError", "Verify correct error");
+ }
+
+ const {usage: usage1} =
+ await collectMemoryUsage(memoryReportPath); // Provided by head.js
+
+ ok(usage1 > 0, "MediaManager memory usage should be non-zero to store \
+device ids after enumerateDevices");
+
+ const p2 = audio.setSinkId("");
+ is(audio.sinkId, audioDevices[0].deviceId,
+ 'sinkId after setSinkId("") return');
+ is(await p2, undefined,
+ "promise resolution value when sinkId parameter is empty");
+ is(audio.sinkId, "", 'sinkId after setSinkId("") resolution');
+
+ await audio.setSinkId(audioDevices[0].deviceId);
+
+ const {usage: usage2, reportCount} =
+ await collectMemoryUsage(memoryReportPath);
+ is(reportCount, 1,
+ 'Expect only one MediaManager to report in content processes.');
+ is(usage2, usage1, "MediaManager memory usage should return to previous \
+value after promise resolution");
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_setSinkId_default_addTrack.html b/dom/media/webrtc/tests/mochitests/test_setSinkId_default_addTrack.html
new file mode 100644
index 0000000000..64db4cad7c
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_setSinkId_default_addTrack.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script type="application/javascript" src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+
+<script>
+ createHTML({
+ title: "HTMLMediaElement.setSinkId with default device and adding a track",
+ bug: "1661649",
+ });
+
+ /**
+ * Run a test to verify set sink id in audio element.
+ */
+ runTest(async () => {
+ await pushPrefs(["media.setsinkid.enabled", true]);
+
+ // Expose an audio output device.
+ SpecialPowers.wrap(document).notifyUserGestureActivation();
+ await navigator.mediaDevices.selectAudioOutput();
+
+ const allDevices = await navigator.mediaDevices.enumerateDevices();
+ const audioDevices = allDevices.filter(({kind}) => kind == 'audiooutput');
+ info(`Found ${audioDevices.length} output devices`);
+ isnot(audioDevices.length, 0, "Found output devices");
+
+ const audio = createMediaElement("audio", "audio");
+ document.body.appendChild(audio);
+
+ audio.srcObject = await navigator.mediaDevices.getUserMedia({audio: true});
+ audio.play();
+
+ await audio.setSinkId(audioDevices[0].deviceId);
+ await audio.setSinkId("");
+ is(audio.sinkId, "", "sinkId restored to default");
+
+ audio.srcObject.addTrack((await navigator.mediaDevices.getUserMedia({audio: true})).getTracks()[0]);
+
+ await wait(0);
+
+ for (let t of audio.srcObject.getTracks()) {
+ t.stop();
+ }
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_setSinkId_preMutedElement.html b/dom/media/webrtc/tests/mochitests/test_setSinkId_preMutedElement.html
new file mode 100644
index 0000000000..fb65c3312f
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_setSinkId_preMutedElement.html
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <script src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<pre id="test">
+<script type="application/javascript">
+createHTML({
+ title: "Test changing sink and muting before the MediaStream is set",
+ bug: "1651049",
+ visible: true
+});
+
+let getOutputDeviceId = async () => {
+ let devices = await navigator.mediaDevices.enumerateDevices();
+ let audios = devices.filter(d => d.kind == "audiooutput");
+ ok(audios.length, "One or more output devices found.");
+ return audios[0].deviceId;
+}
+
+let verifyAudioTone = async (ac, stream, freq) => {
+ const toneAnalyser = new AudioStreamAnalyser(ac, stream);
+ return toneAnalyser.waitForAnalysisSuccess(array => {
+ const lowerFreq = freq / 2;
+ const upperFreq = freq + 1000;
+ const lowerMag = array[toneAnalyser.binIndexForFrequency(lowerFreq)];
+ const freqMag = array[toneAnalyser.binIndexForFrequency(freq)];
+ const upperMag = array[toneAnalyser.binIndexForFrequency(upperFreq)];
+ info("Audio tone expected. "
+ + lowerFreq + ": " + lowerMag + ", "
+ + freq + ": " + freqMag + ", "
+ + upperFreq + ": " + upperMag);
+ return lowerMag < 50 && freqMag > 200 && upperMag < 50;
+ });
+}
+
+let verifyNoAudioTone = async (ac, stream, freq) => {
+ const toneAnalyser = new AudioStreamAnalyser(ac, stream);
+ // repeat check 100 times to make sure that it is muted.
+ let retryCnt = 0;
+ return toneAnalyser.waitForAnalysisSuccess(array => {
+ const lowerFreq = freq / 2;
+ const upperFreq = freq + 1000;
+ const lowerMag = array[toneAnalyser.binIndexForFrequency(lowerFreq)];
+ const freqMag = array[toneAnalyser.binIndexForFrequency(freq)];
+ const upperMag = array[toneAnalyser.binIndexForFrequency(upperFreq)];
+ info("No audio tone expected. "
+ + lowerFreq + ": " + lowerMag + ", "
+ + freq + ": " + freqMag + ", "
+ + upperFreq + ": " + upperMag);
+ return lowerMag == 0 && freqMag == 0 && upperMag == 0 && ++retryCnt == 100;
+ });
+}
+
+runTest(async () => {
+ let audioDevice = SpecialPowers.getCharPref("media.audio_loopback_dev", "");
+ if (!audioDevice) {
+ todo(false, "No loopback device set by framework. Try --use-test-media-devices");
+ return;
+ }
+
+ await pushPrefs(["media.setsinkid.enabled", true]);
+
+ // Implicitly expose the loopback device sink by opening the source in the
+ // same group.
+ const verifyStream = await getUserMedia({audio: true});
+ // We gonna test our tone, stop the auto created one.
+ DefaultLoopbackTone.stop();
+
+ let sinkId = await getOutputDeviceId();
+ isnot(sinkId, "", "SinkId is not null");
+
+ let audioElement = createMediaElement('audio', 'audioElement');
+ audioElement.muted = true;
+ await audioElement.setSinkId(sinkId);
+ isnot(audioElement.sinkId, "", "sinkId property of the element is not null");
+
+ // The test stream is a sine tone of 1000 Hz
+ let ac = new AudioContext();
+ const frequency = 2000;
+ let stream = createOscillatorStream(ac, frequency);
+ await verifyAudioTone(ac, stream, frequency);
+
+ audioElement.srcObject = stream;
+ audioElement.play();
+
+ // Verify the silent output using the loopback device.
+ await verifyNoAudioTone(ac, verifyStream, frequency);
+ info("output is muted");
+
+ // Clean up
+ audioElement.pause();
+ audioElement.srcObject = null;
+ verifyStream.getTracks()[0].stop();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/test_unfocused_pref.html b/dom/media/webrtc/tests/mochitests/test_unfocused_pref.html
new file mode 100644
index 0000000000..22df020f7c
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/test_unfocused_pref.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script src="mediaStreamPlayback.js"></script>
+</head>
+<body>
+<script>
+"use strict";
+
+createHTML({
+ // This pref exists only for a partner testing framework without WebDriver
+ // switch-to-window nor SpecialPowers to set the active window.
+ // Prefer "focusmanager.testmode".
+ title: "Test media.devices.unfocused.enabled",
+ bug: "1740824"
+});
+
+const blank_url = "/tests/docshell/test/navigation/blank.html";
+
+async function resolveOnEvent(target, name) {
+ return new Promise(r => target.addEventListener(name, r, {once: true}));
+}
+
+runTest(async () => {
+ ok(document.hasFocus(), "This test expects initial focus on the document.");
+ // 'resizable' is requested for a separate OS window on relevant platforms
+ // so that this test tests OS focus changes rather than document visibility.
+ const other = window.open(blank_url, "", "resizable");
+ SimpleTest.registerCleanupFunction(() => {
+ other.close();
+ return SimpleTest.promiseFocus(window);
+ });
+ await Promise.all([
+ resolveOnEvent(window, 'blur'),
+ SimpleTest.promiseFocus(other),
+ pushPrefs(["media.devices.unfocused.enabled", true]),
+ ]);
+ ok(!document.hasFocus(), "!document.hasFocus()");
+ await navigator.mediaDevices.enumerateDevices();
+ ok(true, "enumerateDevices() completes without focus.");
+ // The focus requirement with media.devices.unfocused.enabled false
+ // (default) is tested in
+ // testing/web-platform/mozilla/tests/mediacapture-streams/enumerateDevices-without-focus.https.html
+});
+
+</script>
+</body>
+</html>
diff --git a/dom/media/webrtc/tests/mochitests/turnConfig.js b/dom/media/webrtc/tests/mochitests/turnConfig.js
new file mode 100644
index 0000000000..1267de4ec5
--- /dev/null
+++ b/dom/media/webrtc/tests/mochitests/turnConfig.js
@@ -0,0 +1,16 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* An example of how to specify two TURN server configs:
+ *
+ * Note: If turn URL uses FQDN rather then an IP address the TURN relay
+ * verification step in checkStatsIceConnectionType might fail.
+ *
+ * var turnServers = {
+ * local: { iceServers: [{"username":"mozilla","credential":"mozilla","url":"turn:10.0.0.1"}] },
+ * remote: { iceServers: [{"username":"firefox","credential":"firefox","url":"turn:10.0.0.2"}] }
+ * };
+ */
+
+var turnServers = {};
diff --git a/dom/media/webrtc/third_party_build/README.md b/dom/media/webrtc/third_party_build/README.md
new file mode 100644
index 0000000000..9151445c87
--- /dev/null
+++ b/dom/media/webrtc/third_party_build/README.md
@@ -0,0 +1,17 @@
+# Vendoring libwebrtc and the fast-forward process
+
+Most of the important information about this process is contained on the fast-forward
+automation wiki page
+[here](https://wiki.mozilla.org/Media/WebRTC/libwebrtc_Update_Process/automation_plan).
+
+To skip the history and details and go directly to starting the libwebrtc fast-foward
+process, go to the
+[Operation Checklist](https://wiki.mozilla.org/Media/WebRTC/libwebrtc_Update_Process/automation_plan#Operation_Checklist).
+
+# Fixing errors reported in scripts
+
+In most cases, the scripts report errors including suggestions on how to resolve the
+issue. If you're seeing an error message referring you to this README.md file, the
+likely issue is that you're missing environment variables that should be set in a
+config_env file in .moz-fast-forward. The default for that file can be found at
+dom/media/webrtc/third_party_build/default_config_env.
diff --git a/dom/media/webrtc/third_party_build/build_no_op_commits.sh b/dom/media/webrtc/third_party_build/build_no_op_commits.sh
new file mode 100644
index 0000000000..eff61a5eb5
--- /dev/null
+++ b/dom/media/webrtc/third_party_build/build_no_op_commits.sh
@@ -0,0 +1,126 @@
+#!/bin/bash
+
+function show_error_msg()
+{
+ echo "*** ERROR *** $? line $1 $0 did not complete successfully!"
+ echo "$ERROR_HELP"
+}
+ERROR_HELP=""
+
+# Print an Error message if `set -eE` causes the script to exit due to a failed command
+trap 'show_error_msg $LINENO' ERR
+
+source dom/media/webrtc/third_party_build/use_config_env.sh
+
+echo "MOZ_LIBWEBRTC_SRC: $MOZ_LIBWEBRTC_SRC"
+
+# After this point:
+# * eE: All commands should succeed.
+# * u: All variables should be defined before use.
+# * o pipefail: All stages of all pipes should succeed.
+set -eEuo pipefail
+
+CURRENT_DIR=`pwd`
+cd $MOZ_LIBWEBRTC_SRC
+
+MANUAL_INTERVENTION_COMMIT_FILE="$TMP_DIR/manual_commits.txt"
+rm -f $MANUAL_INTERVENTION_COMMIT_FILE
+
+# Find the common commit between our previous work branch and trunk
+CURRENT_RELEASE_BASE=`git merge-base branch-heads/$MOZ_PRIOR_UPSTREAM_BRANCH_HEAD_NUM master`
+
+# Write no-op files for the cherry-picked release branch commits. For more
+# details on what this is doing, see make_upstream_revert_noop.sh.
+COMMITS=`git log -r $CURRENT_RELEASE_BASE..branch-heads/$MOZ_PRIOR_UPSTREAM_BRANCH_HEAD_NUM --format='%h'`
+for commit in $COMMITS; do
+
+ echo "Processing release branch commit $commit for no-op handling"
+
+ # Don't process the commit if the commit message is missing the customary
+ # line that shows which upstream commit is being cherry-picked.
+ CNT=`git show $commit | grep "cherry picked from commit" | wc -l | tr -d " " || true`
+ if [ $CNT != 1 ]; then
+ # record the commit to list at the end of this script as
+ # 'needing intervention'
+ echo " no cherry-pick info found, skipping commit $commit"
+ echo "$commit" >> $MANUAL_INTERVENTION_COMMIT_FILE
+ continue
+ fi
+
+ CHERRY_PICK_COMMIT=`git show $commit | grep "cherry picked from commit" | tr -d "()" | awk '{ print $5; }'`
+ SHORT_SHA=`git show --name-only $CHERRY_PICK_COMMIT --format='%h' | head -1`
+ echo " commit $commit cherry-picks $SHORT_SHA"
+
+ echo "We already cherry-picked this when we vendored $commit." \
+ > $STATE_DIR/$SHORT_SHA.no-op-cherry-pick-msg
+
+done
+
+# This section checks for commits that may have been cherry-picked in more
+# than one release branch.
+TARGET_RELEASE_BASE=`git merge-base $MOZ_TARGET_UPSTREAM_BRANCH_HEAD master`
+NEW_COMMITS=`git log -r $TARGET_RELEASE_BASE..$MOZ_TARGET_UPSTREAM_BRANCH_HEAD --format='%h'`
+
+# Convert the files that we've already generated for no-op detection into
+# something that we can use as a regular expression for searching.
+KNOWN_NO_OP_COMMITS=`cd $STATE_DIR ; \
+ ls *.no-op-cherry-pick-msg \
+ | sed 's/\.no-op-cherry-pick-msg//' \
+ | paste -sd '|' /dev/stdin`
+
+for commit in $NEW_COMMITS; do
+
+ echo "Processing next release branch commit $commit for no-op handling"
+
+ # Don't process the commit if the commit message is missing the customary
+ # line that shows which upstream commit is being cherry-picked.
+ CNT=`git show $commit | grep "cherry picked from commit" | wc -l | tr -d " " || true`
+ if [ $CNT != 1 ]; then
+ # record the commit to list at the end of this script as
+ # 'needing intervention'
+ echo " no cherry-pick info found, skipping commit $commit"
+ echo "$commit" >> $MANUAL_INTERVENTION_COMMIT_FILE
+ continue
+ fi
+
+ CHERRY_PICK_COMMIT=`git show $commit | grep "cherry picked from commit" | tr -d "()" | awk '{ print $5; }'`
+ SHORT_SHA=`git show --name-only $CHERRY_PICK_COMMIT --format='%h' | head -1`
+
+ # The trick here is that we only want to include no-op processing for the
+ # commits that appear both here _and_ in the previous release's cherry-pick
+ # commits. We check the known list of no-op commits to see if it was
+ # cherry picked in the previous release branch and then create another
+ # file for the new release branch commit that will ultimately be a no-op.
+ if [[ "$SHORT_SHA" =~ ^($KNOWN_NO_OP_COMMITS)$ ]]; then
+ echo " commit $commit cherry-picks $SHORT_SHA"
+ cp $STATE_DIR/$SHORT_SHA.no-op-cherry-pick-msg $STATE_DIR/$commit.no-op-cherry-pick-msg
+ fi
+
+done
+
+if [ ! -f $MANUAL_INTERVENTION_COMMIT_FILE ]; then
+ echo "No commits require manual intervention"
+ exit
+fi
+
+echo $"
+Each of the following commits requires manual intervention to
+verify the source of the cherry-pick or there may be errors
+reported during the fast-forward processing. Without this
+intervention, the common symptom is that the vendored commit
+file count (0) will not match the upstream commit file count.
+"
+
+for commit in `cat $MANUAL_INTERVENTION_COMMIT_FILE`; do
+ SUMMARY=`git show --oneline --name-only $commit | head -1`
+ echo " '$SUMMARY'"
+done
+
+echo $"
+To manually create the no-op tracking files needed,
+run the following command for each commit in question:
+ ( export UPSTREAM_COMMIT=\"{sha-of-upstream-commit}\" ; \\
+ export PICKED_COMMIT=\"{sha-of-already-used-commit}\" ; \\
+ echo \"We already cherry-picked this when we vendored \$PICKED_COMMIT.\" \\
+ > $STATE_DIR/\$UPSTREAM_COMMIT.no-op-cherry-pick-msg )
+"
diff --git a/dom/media/webrtc/third_party_build/commit-build-file-changes.sh b/dom/media/webrtc/third_party_build/commit-build-file-changes.sh
new file mode 100644
index 0000000000..2b4c791fca
--- /dev/null
+++ b/dom/media/webrtc/third_party_build/commit-build-file-changes.sh
@@ -0,0 +1,47 @@
+#!/bin/bash
+
+function show_error_msg()
+{
+ echo "*** ERROR *** $? line $1 $0 did not complete successfully!"
+ echo "$ERROR_HELP"
+}
+ERROR_HELP=""
+
+# Print an Error message if `set -eE` causes the script to exit due to a failed command
+trap 'show_error_msg $LINENO' ERR
+
+# All commands should be printed as they are executed
+# set -x
+
+# After this point:
+# * eE: All commands should succede.
+# * u: All variables should be defined before use.
+# * o pipefail: All stages of all pipes should succede.
+set -eEuo pipefail
+
+MOZ_BUILD_CHANGE_CNT=`hg status third_party/libwebrtc | wc -l | tr -d " "`
+echo "MOZ_BUILD_CHANGE_CNT: $MOZ_BUILD_CHANGE_CNT"
+if [ "x$MOZ_BUILD_CHANGE_CNT" != "x0" ]; then
+ CURRENT_COMMIT_SHA=`hg id -i | sed 's/+//'`
+ COMMIT_DESC=`hg --config alias.log=log log -T '{desc|firstline}' -r $CURRENT_COMMIT_SHA`
+
+ # since we have build file changes, touch the CLOBBER file
+ cat CLOBBER | egrep "^#|^$" > CLOBBER.new
+ mv CLOBBER.new CLOBBER
+ echo "Modified build files in third_party/libwebrtc - $COMMIT_DESC" >> CLOBBER
+
+ ADD_CNT=`hg status -nu third_party/libwebrtc | wc -l | tr -d " "`
+ DEL_CNT=`hg status -nd third_party/libwebrtc | wc -l | tr -d " "`
+ if [ "x$ADD_CNT" != "x0" ]; then
+ hg status -nu third_party/libwebrtc | xargs hg add
+ fi
+ if [ "x$DEL_CNT" != "x0" ]; then
+ hg status -nd third_party/libwebrtc | xargs hg rm
+ fi
+
+ hg commit -m \
+ "$COMMIT_DESC - moz.build file updates" \
+ third_party/libwebrtc CLOBBER
+fi
+
+echo "Done in $0"
diff --git a/dom/media/webrtc/third_party_build/default_config_env b/dom/media/webrtc/third_party_build/default_config_env
new file mode 100644
index 0000000000..a50694f0e3
--- /dev/null
+++ b/dom/media/webrtc/third_party_build/default_config_env
@@ -0,0 +1,42 @@
+#!/bin/bash
+
+# Edit {path-to} to match the location of your copy of Mozilla's
+# fork of libwebrtc (at https://github.com/mozilla/libwebrtc).
+export MOZ_LIBWEBRTC_SRC=$STATE_DIR/moz-libwebrtc
+
+# Fast-forwarding each Chromium version of libwebrtc should be done
+# under a separate bugzilla bug. This bug number is used when crafting
+# the commit summary as each upstream commit is vendored into the
+# mercurial repository. The bug used for the v106 fast-forward was
+# 1800920.
+export MOZ_FASTFORWARD_BUG="1833237"
+
+# MOZ_NEXT_LIBWEBRTC_MILESTONE and MOZ_NEXT_FIREFOX_REL_TARGET are
+# not used during fast-forward processing, but facilitate generating this
+# default config. To generate an default config for the next update, run
+# bash dom/media/webrtc/third_party_build/update_default_config_env.sh
+export MOZ_NEXT_LIBWEBRTC_MILESTONE=112
+export MOZ_NEXT_FIREFOX_REL_TARGET=116
+
+# For Chromium release branches, see:
+# https://chromiumdash.appspot.com/branches
+
+# Chromium's v111 release branch was 5563. This is used to pre-stack
+# the previous release branch's commits onto the appropriate base commit
+# (the first common commit between trunk and the release branch).
+export MOZ_PRIOR_UPSTREAM_BRANCH_HEAD_NUM="5563"
+
+# New target release branch for v112 is branch-heads/5615. This is used
+# to calculate the next upstream commit.
+export MOZ_TARGET_UPSTREAM_BRANCH_HEAD="branch-heads/5615"
+
+# For local development 'mozpatches' is fine for a branch name, but when
+# pushing the patch stack to github, it should be named something like
+# 'moz-mods-chr112-for-rel116'.
+export MOZ_LIBWEBRTC_BRANCH="mozpatches"
+
+# After elm has been merged to mozilla-central, the patch stack in
+# moz-libwebrtc should be pushed to github. The script
+# push_official_branch.sh uses this branch name when pushing to the
+# public repo.
+export MOZ_LIBWEBRTC_OFFICIAL_BRANCH="moz-mods-chr112-for-rel116"
diff --git a/dom/media/webrtc/third_party_build/detect_upstream_revert.sh b/dom/media/webrtc/third_party_build/detect_upstream_revert.sh
new file mode 100644
index 0000000000..2dc868952d
--- /dev/null
+++ b/dom/media/webrtc/third_party_build/detect_upstream_revert.sh
@@ -0,0 +1,93 @@
+#!/bin/bash
+
+function show_error_msg()
+{
+ echo "*** ERROR *** $? line $1 $0 did not complete successfully!"
+ echo "$ERROR_HELP"
+}
+ERROR_HELP=""
+
+# Print an Error message if `set -eE` causes the script to exit due to a failed command
+trap 'show_error_msg $LINENO' ERR
+
+source dom/media/webrtc/third_party_build/use_config_env.sh
+
+# If DEBUG_GEN is set all commands should be printed as they are executed
+if [ ! "x$DEBUG_GEN" = "x" ]; then
+ set -x
+fi
+
+if [ "x$MOZ_LIBWEBRTC_SRC" = "x" ]; then
+ echo "MOZ_LIBWEBRTC_SRC is not defined, see README.md"
+ exit
+fi
+
+if [ -d $MOZ_LIBWEBRTC_SRC ]; then
+ echo "MOZ_LIBWEBRTC_SRC is $MOZ_LIBWEBRTC_SRC"
+else
+ echo "Path $MOZ_LIBWEBRTC_SRC is not found, see README.md"
+ exit
+fi
+
+if [ "x$MOZ_LIBWEBRTC_BRANCH" = "x" ]; then
+ echo "MOZ_LIBWEBRTC_BRANCH is not defined, see README.md"
+ exit
+fi
+
+if [ "x$AUTO_FIX_REVERT_AS_NOOP" = "x" ]; then
+ AUTO_FIX_REVERT_AS_NOOP="0"
+fi
+
+find_base_commit
+find_next_commit
+
+MOZ_LIBWEBRTC_COMMIT_MSG=`cd $MOZ_LIBWEBRTC_SRC ; \
+git show --name-only --oneline $MOZ_LIBWEBRTC_NEXT_BASE \
+ | head -1 | sed 's/[^ ]* //'`
+
+echo "MOZ_LIBWEBRTC_BASE: $MOZ_LIBWEBRTC_BASE"
+echo "MOZ_LIBWEBRTC_NEXT_BASE: $MOZ_LIBWEBRTC_NEXT_BASE"
+echo "MOZ_LIBWEBRTC_COMMIT_MSG: $MOZ_LIBWEBRTC_COMMIT_MSG"
+export MOZ_LIBWEBRTC_REVERT_SHA=`cd $MOZ_LIBWEBRTC_SRC ; \
+git log --oneline -r $MOZ_LIBWEBRTC_BASE..$MOZ_TARGET_UPSTREAM_BRANCH_HEAD \
+ | grep -F "Revert \"$MOZ_LIBWEBRTC_COMMIT_MSG" \
+ | tail -1 | awk '{print $1;}' || true`
+
+echo "MOZ_LIBWEBRTC_REVERT_SHA: $MOZ_LIBWEBRTC_REVERT_SHA"
+
+if [ "x$MOZ_LIBWEBRTC_REVERT_SHA" == "x" ]; then
+ echo "no revert commit detected"
+ exit
+fi
+
+if [ "x$AUTO_FIX_REVERT_AS_NOOP" = "x1" ]; then
+ echo "AUTO_FIX_REVERT_AS_NOOP detected, fixing land/revert pair automatically"
+ bash $SCRIPT_DIR/make_upstream_revert_noop.sh
+ exit
+fi
+
+echo $"
+The next upstream commit has a corresponding future \"Revert\" commit.
+
+There are 2 common ways forward in this situation:
+1. If you're relatively certain there will not be rebase conflicts in the
+ github repo ($MOZ_LIBWEBRTC_SRC), simply run:
+ SKIP_NEXT_REVERT_CHK=1 bash $SCRIPT_DIR/loop-ff.sh
+
+2. The surer method for no rebase conflicts is to cherry-pick both the
+ next commit, and the commit that reverts the next commit onto the
+ bottom of our patch stack in github. This pushes the likely rebase
+ conflict into the future when the upstream fix is relanded, but
+ ensures we only have to deal with the conflict once. The following
+ commands will add the necessary commits to the bottom of our patch
+ stack in github, and leave indicator files in the home directory that
+ help loop-ff know when to invoke special no-op commit handling:
+
+ MOZ_LIBWEBRTC_BASE=$MOZ_LIBWEBRTC_BASE \\
+ MOZ_LIBWEBRTC_NEXT_BASE=$MOZ_LIBWEBRTC_NEXT_BASE \\
+ MOZ_LIBWEBRTC_REVERT_SHA=$MOZ_LIBWEBRTC_REVERT_SHA \\
+ bash $SCRIPT_DIR/make_upstream_revert_noop.sh
+
+ SKIP_NEXT_REVERT_CHK=1 bash $SCRIPT_DIR/loop-ff.sh
+"
+exit 1
diff --git a/dom/media/webrtc/third_party_build/elm_arcconfig.patch b/dom/media/webrtc/third_party_build/elm_arcconfig.patch
new file mode 100644
index 0000000000..46adb9c6f1
--- /dev/null
+++ b/dom/media/webrtc/third_party_build/elm_arcconfig.patch
@@ -0,0 +1,10 @@
+diff --git a/.arcconfig b/.arcconfig
+--- a/.arcconfig
++++ b/.arcconfig
+@@ -1,5 +1,5 @@
+ {
+ "phabricator.uri" : "https://phabricator.services.mozilla.com/",
+- "repository.callsign": "MOZILLACENTRAL",
++ "repository.callsign": "ELM",
+ "history.immutable": false
+ }
diff --git a/dom/media/webrtc/third_party_build/elm_rebase.sh b/dom/media/webrtc/third_party_build/elm_rebase.sh
new file mode 100644
index 0000000000..4d53373d35
--- /dev/null
+++ b/dom/media/webrtc/third_party_build/elm_rebase.sh
@@ -0,0 +1,247 @@
+#!/bin/bash
+
+# This script exists to help with the rebase process on elm. It rebases
+# each patch individually to make it easier to fix rebase conflicts
+# without jeopardizing earlier, sucessfully rebased commits. In order to
+# limit rebase conflicts around generated moz.build files, it regenerates
+# moz.build file commits. It also ensures any commits with 'FLOAT' in the
+# commit summary are pushed to the top of the fast-forward stack to help
+# the sheriffs more easily merge our commit stack from elm to moz-central.
+#
+# Occasionally, there will be upstream vendored commits that break the
+# build file generation with follow on commits that fix that error. In
+# order to allow the rebase process to work more smoothly, it is possible
+# to annotate a commit with the string '(skip-generation)' and normal
+# build file generation (detected with changes to BUILD.gn files) is
+# disabled for that commit. The script outputs instructions for handling
+# this situation.
+#
+# Note: the very first rebase operation will require some manual
+# intervention. The user will need to provide, at minimum, the commit that
+# corresponds to moz-central upon which the fast-forward stack is based.
+# It may also be necessary to provide the first commit of the
+# fast-forward stack. Example:
+# MOZ_BOTTOM_FF=30f0afb7e4c5 \
+# MOZ_CURRENT_CENTRAL=cad1bd47c273 \
+# bash dom/media/webrtc/third_party_build/elm_rebase.sh
+#
+# Assumes the top of the fast-forward stack to rebase is the current revision,
+# ".".
+
+function show_error_msg()
+{
+ echo "*** ERROR *** $? line $1 $0 did not complete successfully!"
+ echo "$ERROR_HELP"
+}
+ERROR_HELP=""
+
+# Print an Error message if `set -eE` causes the script to exit due to a failed command
+trap 'show_error_msg $LINENO' ERR
+
+source dom/media/webrtc/third_party_build/use_config_env.sh
+
+GENERATION_ERROR=$"
+Generating build files has failed. The most common reason for this
+failure is that the current commit has an upcoming '(fix-xxxxxx)' commit
+that will then allow the build file generation to complete. If the
+current situation seems to fit that pattern, adding a line with
+'(skip-generation)' to the commit message will ensure that future rebase
+operations do not attempt to generate build files for this commit. It may
+be as simple as running the following commands:
+ HGPLAIN=1 hg log -T '{desc}' -r tip > $TMP_DIR/commit_message.txt
+ ed -s $TMP_DIR/commit_message.txt <<< $'3i\n(skip-generation)\n\n.\nw\nq'
+ hg commit --amend -l $TMP_DIR/commit_message.txt
+ bash $0
+"
+COMMIT_LIST_FILE=$TMP_DIR/rebase-commit-list.txt
+export HGPLAIN=1
+
+# After this point:
+# * eE: All commands should succeed.
+# * o pipefail: All stages of all pipes should succeed.
+set -eEo pipefail
+
+if [ -f $STATE_DIR/rebase_resume_state ]; then
+ source $STATE_DIR/rebase_resume_state
+else
+
+ if [ "x" == "x$MOZ_TOP_FF" ]; then
+ MOZ_TOP_FF=`hg log -r . -T"{node|short}"`
+
+ ERROR_HELP=$"
+The topmost commit to be rebased is not in the public phase. Should it be
+pushed to elm first? If this is intentional, please rerun the command and pass
+it in explicitly:
+ MOZ_TOP_FF=$MOZ_TOP_FF bash $0
+"
+ if [[ $(hg phase -r .) != *public ]]; then
+ echo "$ERROR_HELP"
+ exit 1
+ fi
+ ERROR_HELP=""
+
+ ERROR_HELP=$"
+The topmost commit to be rebased is public but has descendants. If those
+descendants should not be rebased, please rerun the command and pass the commit
+in explicitly:
+ MOZ_TOP_FF=$MOZ_TOP_FF bash $0
+"
+ if [ "x" != "x$(hg log -r 'descendants(.) and !.' -T'{node|short}')" ]; then
+ echo "$ERROR_HELP"
+ exit 1
+ fi
+ ERROR_HELP=""
+ fi
+
+ ERROR_HELP=$"
+An error here is likely because no revision for central is found.
+One possible reason for this is this is your first rebase operation.
+To 'bootstrap' the first rebase operation, please find the
+moz-central commit that the vendoring commits is based on, and
+rerun the command:
+ MOZ_CURRENT_CENTRAL={central-sha} bash $0
+
+You may also need to provide the bottom commit of the fast-forward
+stack. The bottom commit means the commit following central. This
+could be the sha of the .arcconfig commit if it is the bottom commit.
+That command looks like:
+ MOZ_BOTTOM_FF={base-sha} MOZ_CURRENT_CENTRAL={central-sha} bash $0
+"
+ if [ "x" == "x$MOZ_CURRENT_CENTRAL" ]; then
+ MOZ_CURRENT_CENTRAL=`hg log -r central -T"{node|short}"`
+ fi
+ if [ "x" == "x$MOZ_BOTTOM_FF" ]; then
+ MOZ_BOTTOM_FF=`hg log -r $MOZ_CURRENT_CENTRAL~-1 -T"{node|short}"`
+ fi
+ ERROR_HELP=""
+
+ if [ "x" == "x$MOZ_BOTTOM_FF" ]; then
+ echo "No value found for the bottom commit of the fast-forward commit stack."
+ exit 1
+ fi
+
+ # After this point:
+ # * eE: All commands should succeed.
+ # * u: All variables should be defined before use.
+ # * o pipefail: All stages of all pipes should succeed.
+ set -eEuo pipefail
+
+ hg pull central
+ MOZ_NEW_CENTRAL=`hg log -r central -T"{node|short}"`
+
+ echo "moz-central in elm is currently $MOZ_CURRENT_CENTRAL"
+ echo "bottom of fast-foward tree is $MOZ_BOTTOM_FF"
+ echo "top of fast-forward tree (webrtc-fast-forward) is $MOZ_TOP_FF"
+ echo "new target for elm rebase $MOZ_NEW_CENTRAL (tip of moz-central)"
+
+ hg log -T '{rev}:{node|short} {desc|firstline}\n' \
+ -r $MOZ_BOTTOM_FF::$MOZ_TOP_FF > $COMMIT_LIST_FILE
+
+ # move all FLOAT lines to end of file, and delete the "empty" tilde line
+ # line at the beginning
+ ed -s $COMMIT_LIST_FILE <<< $'g/- FLOAT -/m$\ng/^~$/d\nw\nq'
+
+ MOZ_BOOKMARK=`date "+webrtc-fast-forward-%Y-%m-%d--%H-%M"`
+ hg bookmark -r elm $MOZ_BOOKMARK
+
+ hg update $MOZ_NEW_CENTRAL
+
+ # pre-work is complete, let's write out a temporary config file that allows
+ # us to resume
+ echo $"export MOZ_CURRENT_CENTRAL=$MOZ_CURRENT_CENTRAL
+export MOZ_BOTTOM_FF=$MOZ_BOTTOM_FF
+export MOZ_TOP_FF=$MOZ_TOP_FF
+export MOZ_NEW_CENTRAL=$MOZ_NEW_CENTRAL
+export MOZ_BOOKMARK=$MOZ_BOOKMARK
+" > $STATE_DIR/rebase_resume_state
+fi # if [ -f $STATE_DIR/rebase_resume_state ]; then ; else
+
+# grab all commits
+COMMITS=`cat $COMMIT_LIST_FILE | awk '{print $1;}'`
+
+echo -n "Commits: "
+for commit in $COMMITS; do
+echo -n "$commit "
+done
+echo ""
+
+for commit in $COMMITS; do
+ echo "Processing $commit"
+ FULL_COMMIT_LINE=`head -1 $COMMIT_LIST_FILE`
+
+ function remove_commit () {
+ echo "Removing from list '$FULL_COMMIT_LINE'"
+ ed -s $COMMIT_LIST_FILE <<< $'1d\nw\nq'
+ }
+
+ IS_BUILD_COMMIT=`hg log -T '{desc|firstline}' -r $commit \
+ | grep "file updates" | wc -l | tr -d " " || true`
+ echo "IS_BUILD_COMMIT: $IS_BUILD_COMMIT"
+ if [ "x$IS_BUILD_COMMIT" != "x0" ]; then
+ echo "Skipping $commit:"
+ hg log -T '{desc|firstline}' -r $commit
+ remove_commit
+ continue
+ fi
+
+ IS_SKIP_GEN_COMMIT=`hg log --verbose \
+ -r $commit \
+ | grep "skip-generation" | wc -l | tr -d " " || true`
+ echo "IS_SKIP_GEN_COMMIT: $IS_SKIP_GEN_COMMIT"
+
+ echo "Generate patch for: $commit"
+ hg export -r $commit > $TMP_DIR/rebase.patch
+
+ echo "Import patch for $commit"
+ hg import $TMP_DIR/rebase.patch || \
+ ( hg log -T '{desc}' -r $commit > $TMP_DIR/rebase_commit_message.txt ; \
+ remove_commit ; \
+ echo "Error importing: '$FULL_COMMIT_LINE'" ; \
+ echo "Please fix import errors, then:" ; \
+ echo " hg commit -l $TMP_DIR/rebase_commit_message.txt" ; \
+ echo " bash $0" ; \
+ exit 1 )
+
+ remove_commit
+
+ if [ "x$IS_SKIP_GEN_COMMIT" != "x0" ]; then
+ echo "Skipping build generation for $commit"
+ continue
+ fi
+
+ MODIFIED_BUILD_RELATED_FILE_CNT=`hg diff -c tip --stat \
+ --include 'third_party/libwebrtc/**BUILD.gn' \
+ --include 'third_party/libwebrtc/webrtc.gni' \
+ --include 'dom/media/webrtc/third_party_build/gn-configs/webrtc.json' \
+ | wc -l | tr -d " "`
+ echo "MODIFIED_BUILD_RELATED_FILE_CNT: $MODIFIED_BUILD_RELATED_FILE_CNT"
+ if [ "x$MODIFIED_BUILD_RELATED_FILE_CNT" != "x0" ]; then
+ echo "Regenerate build files"
+ ./mach python python/mozbuild/mozbuild/gn_processor.py \
+ dom/media/webrtc/third_party_build/gn-configs/webrtc.json || \
+ ( echo "$GENERATION_ERROR" ; exit 1 )
+
+ MOZ_BUILD_CHANGE_CNT=`hg status third_party/libwebrtc \
+ --include 'third_party/libwebrtc/**moz.build' | wc -l | tr -d " "`
+ if [ "x$MOZ_BUILD_CHANGE_CNT" != "x0" ]; then
+ bash dom/media/webrtc/third_party_build/commit-build-file-changes.sh
+ NEWEST_COMMIT=`hg log -T '{desc|firstline}' -r tip`
+ echo "NEWEST_COMMIT: $NEWEST_COMMIT"
+ echo "NEWEST_COMMIT: $NEWEST_COMMIT" >> $LOG_DIR/rebase-build-changes-commits.log
+ fi
+ echo "Done generating build files"
+ fi
+
+ echo "Done processing $commit"
+done
+
+rm $STATE_DIR/rebase_resume_state
+
+REMAINING_STEPS=$"
+The rebase process is complete. The following steps must be completed manually:
+ ./mach bootstrap --application=browser --no-system-changes
+ ./mach build
+ hg push -r tip --force
+ hg push -B $MOZ_BOOKMARK
+"
+echo "$REMAINING_STEPS"
diff --git a/dom/media/webrtc/third_party_build/extract-for-git.py b/dom/media/webrtc/third_party_build/extract-for-git.py
new file mode 100644
index 0000000000..d2701d7dff
--- /dev/null
+++ b/dom/media/webrtc/third_party_build/extract-for-git.py
@@ -0,0 +1,145 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+import argparse
+import os
+import re
+import subprocess
+
+# This script extracts commits that touch third party webrtc code so they can
+# be imported into Git. It filters out commits that are not part of upstream
+# code and rewrites the paths to match upstream. Finally, the commits are
+# combined into a mailbox file that can be applied with `git am`.
+LIBWEBRTC_DIR = "third_party/libwebrtc"
+
+
+def build_commit_list(revset, env):
+ """Build commit list from the specified revset.
+
+ The revset can be a single revision, e.g. 52bb9bb94661, or a range,
+ e.g. 8c08a5bb8a99::52bb9bb94661, or any other valid revset
+ (check hg help revset). Only commits that touch libwebrtc are included.
+ """
+ res = subprocess.run(
+ ["hg", "log", "-r", revset, "-M", "--template", "{node}\n", LIBWEBRTC_DIR],
+ capture_output=True,
+ text=True,
+ env=env,
+ )
+ return [line.strip() for line in res.stdout.strip().split("\n")]
+
+
+def extract_author_date(sha1, env):
+ res = subprocess.run(
+ ["hg", "log", "-r", sha1, "--template", "{author}|{date|isodate}"],
+ capture_output=True,
+ text=True,
+ env=env,
+ )
+ return res.stdout.split("|")
+
+
+def extract_description(sha1, env):
+ res = subprocess.run(
+ ["hg", "log", "-r", sha1, "--template", "{desc}"],
+ capture_output=True,
+ text=True,
+ env=env,
+ )
+ return res.stdout
+
+
+def extract_commit(sha1, env):
+ res = subprocess.run(
+ ["hg", "log", "-r", sha1, "-pg", "--template", "\n"],
+ capture_output=True,
+ text=True,
+ env=env,
+ )
+ return res.stdout
+
+
+def filter_nonwebrtc(commit):
+ filtered = []
+ skipping = False
+ for line in commit.split("\n"):
+ # Extract only patches affecting libwebrtc, but avoid commits that
+ # touch build, which is tracked by a separate repo, or that affect
+ # moz.build files which are code generated.
+ if (
+ line.startswith("diff --git a/" + LIBWEBRTC_DIR)
+ and not line.startswith("diff --git a/" + LIBWEBRTC_DIR + "/build")
+ and not line.startswith("diff --git a/" + LIBWEBRTC_DIR + "/third_party")
+ and not line.startswith(
+ "diff --git a/" + LIBWEBRTC_DIR + "/moz-patch-stack"
+ )
+ and not line.endswith("moz.build")
+ ):
+ skipping = False
+ elif line.startswith("diff --git"):
+ skipping = True
+
+ if not skipping:
+ filtered.append(line)
+ return "\n".join(filtered)
+
+
+def fixup_paths(commit):
+ # make sure we only rewrite paths in the diff-related or rename lines
+ commit = re.sub(
+ f"^rename (from|to) {LIBWEBRTC_DIR}/", "rename \\1 ", commit, flags=re.MULTILINE
+ )
+ return re.sub(f"( [ab])/{LIBWEBRTC_DIR}/", "\\1/", commit)
+
+
+def write_as_mbox(sha1, author, date, description, commit, ofile):
+ # Use same magic date as git format-patch
+ ofile.write("From {} Mon Sep 17 00:00:00 2001\n".format(sha1))
+ ofile.write("From: {}\n".format(author))
+ ofile.write("Date: {}\n".format(date))
+ description = description.split("\n")
+ ofile.write("Subject: {}\n".format(description[0]))
+ ofile.write("\n".join(description[1:]))
+ ofile.write(
+ "\nMercurial Revision: https://hg.mozilla.org/mozilla-central/rev/{}\n".format(
+ sha1
+ )
+ )
+ ofile.write(commit)
+ ofile.write("\n")
+ ofile.write("\n")
+
+
+if __name__ == "__main__":
+ commits = []
+ parser = argparse.ArgumentParser(
+ description="Format commits for upstream libwebrtc"
+ )
+ parser.add_argument(
+ "revsets", metavar="revset", type=str, nargs="+", help="A revset to process"
+ )
+ parser.add_argument(
+ "--target", choices=("libwebrtc", "build", "third_party"), default="libwebrtc"
+ )
+ args = parser.parse_args()
+
+ if args.target != "libwebrtc":
+ LIBWEBRTC_DIR = os.path.join(LIBWEBRTC_DIR, args.target)
+
+ # must run 'hg' with HGPLAIN=1 to ensure aliases don't interfere with
+ # command output.
+ env = os.environ.copy()
+ env["HGPLAIN"] = "1"
+
+ for revset in args.revsets:
+ commits.extend(build_commit_list(revset, env))
+
+ with open("mailbox.patch", "w") as ofile:
+ for sha1 in commits:
+ author, date = extract_author_date(sha1, env)
+ description = extract_description(sha1, env)
+ filtered_commit = filter_nonwebrtc(extract_commit(sha1, env))
+ if len(filtered_commit) == 0:
+ continue
+ fixedup_commit = fixup_paths(filtered_commit)
+ write_as_mbox(sha1, author, date, description, fixedup_commit, ofile)
diff --git a/dom/media/webrtc/third_party_build/fast-forward-libwebrtc.sh b/dom/media/webrtc/third_party_build/fast-forward-libwebrtc.sh
new file mode 100644
index 0000000000..9ef40a41ac
--- /dev/null
+++ b/dom/media/webrtc/third_party_build/fast-forward-libwebrtc.sh
@@ -0,0 +1,256 @@
+#!/bin/bash
+
+function show_error_msg()
+{
+ echo "*** ERROR *** $? line $1 $0 did not complete successfully!"
+ echo "$ERROR_HELP"
+}
+ERROR_HELP=""
+
+# Print an Error message if `set -eE` causes the script to exit due to a failed command
+trap 'show_error_msg $LINENO' ERR
+
+source dom/media/webrtc/third_party_build/use_config_env.sh
+
+# If DEBUG_GEN is set all commands should be printed as they are executed
+if [ ! "x$DEBUG_GEN" = "x" ]; then
+ set -x
+fi
+
+if [ "x$MOZ_LIBWEBRTC_SRC" = "x" ]; then
+ echo "MOZ_LIBWEBRTC_SRC is not defined, see README.md"
+ exit
+fi
+
+if [ -d $MOZ_LIBWEBRTC_SRC ]; then
+ echo "MOZ_LIBWEBRTC_SRC is $MOZ_LIBWEBRTC_SRC"
+else
+ echo "Path $MOZ_LIBWEBRTC_SRC is not found, see README.md"
+ exit
+fi
+
+if [ "x$MOZ_LIBWEBRTC_BRANCH" = "x" ]; then
+ echo "MOZ_LIBWEBRTC_BRANCH is not defined, see README.md"
+ exit
+fi
+
+if [ "x$HANDLE_NOOP_COMMIT" = "x" ]; then
+ HANDLE_NOOP_COMMIT=""
+fi
+
+RESUME=""
+if [ -f $STATE_DIR/resume_state ]; then
+ RESUME=`tail -1 $STATE_DIR/resume_state`
+fi
+
+GIT_IS_REBASING=`cd $MOZ_LIBWEBRTC_SRC && git status | grep "interactive rebase in progress" | wc -l | tr -d " " || true`
+if [ "x$GIT_IS_REBASING" != "x0" ]; then
+ echo "There is currently a git rebase operation in progress at $MOZ_LIBWEBRTC_SRC."
+ echo "Please resolve the rebase before attempting to continue the fast-forward"
+ echo "operation."
+ exit 1
+fi
+
+if [ "x$RESUME" = "x" ]; then
+ SKIP_TO="run"
+ # Check for modified files and abort if present.
+ MODIFIED_FILES=`hg status --exclude "third_party/libwebrtc/**.orig" third_party/libwebrtc`
+ if [ "x$MODIFIED_FILES" = "x" ]; then
+ # Completely clean the mercurial checkout before proceeding
+ hg update -C -r .
+ hg purge
+ else
+ echo "There are modified files in the checkout. Cowardly aborting!"
+ echo "$MODIFIED_FILES"
+ exit 1
+ fi
+else
+ SKIP_TO=$RESUME
+ hg revert -C third_party/libwebrtc/README.moz-ff-commit &> /dev/null
+fi
+
+find_base_commit
+find_next_commit
+
+echo "looking for $STATE_DIR/$MOZ_LIBWEBRTC_NEXT_BASE.no-op-cherry-pick-msg"
+if [ -f $STATE_DIR/$MOZ_LIBWEBRTC_NEXT_BASE.no-op-cherry-pick-msg ]; then
+ echo "***"
+ echo "*** detected special commit msg, setting HANDLE_NOOP_COMMIT"
+ echo "***"
+ HANDLE_NOOP_COMMIT="1"
+fi
+
+UPSTREAM_ADDED_FILES=""
+
+# Grab the filtered changes from git based on what we vendor.
+FILTERED_GIT_CHANGES=`./mach python $SCRIPT_DIR/filter_git_changes.py \
+ --repo-path $MOZ_LIBWEBRTC_SRC --commit-sha $MOZ_LIBWEBRTC_NEXT_BASE`
+
+# After this point:
+# * eE: All commands should succeed.
+# * u: All variables should be defined before use.
+# * o pipefail: All stages of all pipes should succeed.
+set -eEuo pipefail
+
+echo " MOZ_LIBWEBRTC_BASE: $MOZ_LIBWEBRTC_BASE"
+echo "MOZ_LIBWEBRTC_NEXT_BASE: $MOZ_LIBWEBRTC_NEXT_BASE"
+echo "HANDLE_NOOP_COMMIT: $HANDLE_NOOP_COMMIT"
+echo " RESUME: $RESUME"
+echo "SKIP_TO: $SKIP_TO"
+
+echo "-------"
+echo "------- Write cmd-line to third_party/libwebrtc/README.moz-ff-commit"
+echo "-------"
+echo "# MOZ_LIBWEBRTC_SRC=$MOZ_LIBWEBRTC_SRC MOZ_LIBWEBRTC_BRANCH=$MOZ_LIBWEBRTC_BRANCH bash $0" \
+ >> third_party/libwebrtc/README.moz-ff-commit
+
+echo "-------"
+echo "------- Write new-base to last line of third_party/libwebrtc/README.moz-ff-commit"
+echo "-------"
+echo "# base of lastest vendoring" >> third_party/libwebrtc/README.moz-ff-commit
+echo "$MOZ_LIBWEBRTC_NEXT_BASE" >> third_party/libwebrtc/README.moz-ff-commit
+
+REBASE_HELP=$"
+The rebase operation onto $MOZ_LIBWEBRTC_NEXT_BASE has failed. Please
+resolve all the rebase conflicts. To fix this issue, you will need to
+jump to the github repo at $MOZ_LIBWEBRTC_SRC .
+When the github rebase is complete, re-run the script to resume the
+fast-forward process.
+"
+#"rebase_mozlibwebrtc_stack help for rebase failure"
+function rebase_mozlibwebrtc_stack {
+ echo "-------"
+ echo "------- Rebase $MOZ_LIBWEBRTC_BRANCH to $MOZ_LIBWEBRTC_NEXT_BASE"
+ echo "-------"
+ ERROR_HELP=$REBASE_HELP
+ ( cd $MOZ_LIBWEBRTC_SRC && \
+ git checkout -q $MOZ_LIBWEBRTC_BRANCH && \
+ git rebase $MOZ_LIBWEBRTC_NEXT_BASE \
+ &> $LOG_DIR/log-rebase-moz-libwebrtc.txt \
+ )
+ ERROR_HELP=""
+}
+
+function vendor_off_next_commit {
+ echo "-------"
+ echo "------- Vendor $MOZ_LIBWEBRTC_BRANCH from $MOZ_LIBWEBRTC_SRC"
+ echo "-------"
+ ./mach python $SCRIPT_DIR/vendor-libwebrtc.py \
+ --from-local $MOZ_LIBWEBRTC_SRC \
+ --commit $MOZ_LIBWEBRTC_BRANCH \
+ libwebrtc
+}
+
+# The vendoring script (called above in vendor_off_next_commit) replaces
+# the entire third_party/libwebrtc directory, which effectively removes
+# all the generated moz.build files. It is easier (less error prone),
+# to revert only those missing moz.build files rather than attempt to
+# rebuild them because the rebuild may need json updates to work properly.
+function regen_mozbuild_files {
+ echo "-------"
+ echo "------- Restore moz.build files from repo"
+ echo "-------"
+ hg revert --include "third_party/libwebrtc/**moz.build" \
+ third_party/libwebrtc &> $LOG_DIR/log-regen-mozbuild-files.txt
+}
+
+function add_new_upstream_files {
+ if [ "x$HANDLE_NOOP_COMMIT" == "x1" ]; then
+ return
+ fi
+ UPSTREAM_ADDED_FILES=`echo "$FILTERED_GIT_CHANGES" | grep "^A" \
+ | awk '{print $2;}' || true`
+ if [ "x$UPSTREAM_ADDED_FILES" != "x" ]; then
+ echo "-------"
+ echo "------- Add new upstream files"
+ echo "-------"
+ (cd third_party/libwebrtc && hg add $UPSTREAM_ADDED_FILES)
+ echo "$UPSTREAM_ADDED_FILES" &> $LOG_DIR/log-new-upstream-files.txt
+ fi
+}
+
+function remove_deleted_upstream_files {
+ if [ "x$HANDLE_NOOP_COMMIT" == "x1" ]; then
+ return
+ fi
+ UPSTREAM_DELETED_FILES=`echo "$FILTERED_GIT_CHANGES" | grep "^D" \
+ | awk '{print $2;}' || true`
+ if [ "x$UPSTREAM_DELETED_FILES" != "x" ]; then
+ echo "-------"
+ echo "------- Remove deleted upstream files"
+ echo "-------"
+ (cd third_party/libwebrtc && hg rm $UPSTREAM_DELETED_FILES)
+ echo "$UPSTREAM_DELETED_FILES" &> $LOG_DIR/log-deleted-upstream-files.txt
+ fi
+}
+
+function handle_renamed_upstream_files {
+ if [ "x$HANDLE_NOOP_COMMIT" == "x1" ]; then
+ return
+ fi
+ UPSTREAM_RENAMED_FILES=`echo "$FILTERED_GIT_CHANGES" | grep "^R" \
+ | awk '{print $2 " " $3;}' || true`
+ if [ "x$UPSTREAM_RENAMED_FILES" != "x" ]; then
+ echo "-------"
+ echo "------- Handle renamed upstream files"
+ echo "-------"
+ (cd third_party/libwebrtc && echo "$UPSTREAM_RENAMED_FILES" | while read line; do hg rename --after $line; done)
+ echo "$UPSTREAM_RENAMED_FILES" &> $LOG_DIR/log-renamed-upstream-files.txt
+ fi
+}
+
+if [ $SKIP_TO = "run" ]; then
+ echo "resume2" > $STATE_DIR/resume_state
+ rebase_mozlibwebrtc_stack;
+fi
+
+if [ $SKIP_TO = "resume2" ]; then SKIP_TO="run"; fi
+if [ $SKIP_TO = "run" ]; then
+ echo "resume3" > $STATE_DIR/resume_state
+ vendor_off_next_commit;
+fi
+
+if [ $SKIP_TO = "resume3" ]; then SKIP_TO="run"; fi
+if [ $SKIP_TO = "run" ]; then
+ echo "resume4" > $STATE_DIR/resume_state
+ regen_mozbuild_files;
+fi
+
+if [ $SKIP_TO = "resume4" ]; then SKIP_TO="run"; fi
+if [ $SKIP_TO = "run" ]; then
+ echo "resume5" > $STATE_DIR/resume_state
+ remove_deleted_upstream_files;
+fi
+
+if [ $SKIP_TO = "resume5" ]; then SKIP_TO="run"; fi
+if [ $SKIP_TO = "run" ]; then
+ echo "resume6" > $STATE_DIR/resume_state
+ add_new_upstream_files;
+fi
+
+if [ $SKIP_TO = "resume6" ]; then SKIP_TO="run"; fi
+if [ $SKIP_TO = "run" ]; then
+ echo "resume7" > $STATE_DIR/resume_state
+ handle_renamed_upstream_files;
+fi
+
+echo "" > $STATE_DIR/resume_state
+echo "-------"
+echo "------- Commit vendored changes from $MOZ_LIBWEBRTC_NEXT_BASE"
+echo "-------"
+UPSTREAM_SHA=`cd $MOZ_LIBWEBRTC_SRC && \
+ git show --name-only $MOZ_LIBWEBRTC_NEXT_BASE \
+ | grep "^commit " | awk '{ print $NF }'`
+echo "Bug $MOZ_FASTFORWARD_BUG - Vendor libwebrtc from $MOZ_LIBWEBRTC_NEXT_BASE" \
+ > $TMP_DIR/commit_msg.txt
+echo "" >> $TMP_DIR/commit_msg.txt
+if [ -f $STATE_DIR/$MOZ_LIBWEBRTC_NEXT_BASE.no-op-cherry-pick-msg ]; then
+ cat $STATE_DIR/$MOZ_LIBWEBRTC_NEXT_BASE.no-op-cherry-pick-msg >> $TMP_DIR/commit_msg.txt
+ echo "" >> $TMP_DIR/commit_msg.txt
+ rm $STATE_DIR/$MOZ_LIBWEBRTC_NEXT_BASE.no-op-cherry-pick-msg
+fi
+echo "Upstream commit: https://webrtc.googlesource.com/src/+/$UPSTREAM_SHA" >> $TMP_DIR/commit_msg.txt
+(cd $MOZ_LIBWEBRTC_SRC && \
+git show --name-only $MOZ_LIBWEBRTC_NEXT_BASE | grep "^ ") >> $TMP_DIR/commit_msg.txt
+
+hg commit -l $TMP_DIR/commit_msg.txt third_party/libwebrtc
diff --git a/dom/media/webrtc/third_party_build/fetch_github_repo.py b/dom/media/webrtc/third_party_build/fetch_github_repo.py
new file mode 100644
index 0000000000..bfa40f5c7b
--- /dev/null
+++ b/dom/media/webrtc/third_party_build/fetch_github_repo.py
@@ -0,0 +1,122 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+import argparse
+import os
+import re
+import shutil
+
+from run_operations import run_git, run_shell
+
+# This script fetches the moz-libwebrtc github repro with the expected
+# upstream remote and branch-heads setup. This is used by both the
+# prep_repo.sh script as well as the restore_patch_stack.py script.
+#
+# For speed and conservation of network resources, after fetching all
+# the data, a tar of the repo is made and used if available.
+
+
+def fetch_repo(github_path, force_fetch, tar_path):
+ capture_output = False
+
+ # check for pre-existing repo - make sure we force the removal
+ if force_fetch and os.path.exists(github_path):
+ print("Removing existing repo: {}".format(github_path))
+ shutil.rmtree(github_path)
+
+ # clone https://github.com/mozilla/libwebrtc
+ if not os.path.exists(github_path):
+ # check for pre-existing tar, use it if we have it
+ if os.path.exists(tar_path):
+ print("Using tar file to reconstitute repo")
+ cmd = "cd {} ; tar --extract --gunzip --file={}".format(
+ os.path.dirname(github_path), os.path.basename(tar_path)
+ )
+ run_shell(cmd, capture_output)
+ else:
+ print("Cloning github repo")
+ run_shell(
+ "git clone https://github.com/mozilla/libwebrtc {}".format(github_path),
+ capture_output,
+ )
+
+ # setup upstream (https://webrtc.googlesource.com/src)
+ stdout_lines = run_git("git config --local --list", github_path)
+ stdout_lines = [
+ path for path in stdout_lines if re.findall("^remote.upstream.url.*", path)
+ ]
+ if len(stdout_lines) == 0:
+ print("Fetching upstream")
+ run_git("git checkout master", github_path)
+ run_git(
+ "git remote add upstream https://webrtc.googlesource.com/src", github_path
+ )
+ run_git("git fetch upstream", github_path)
+ run_git("git merge upstream/master", github_path)
+ else:
+ print(
+ "Upstream remote (https://webrtc.googlesource.com/src) already configured"
+ )
+
+ # setup upstream branch-heads
+ stdout_lines = run_git(
+ "git config --local --get-all remote.upstream.fetch", github_path
+ )
+ if len(stdout_lines) == 1:
+ print("Fetching upstream branch-heads")
+ run_git(
+ "git config --local --add remote.upstream.fetch +refs/branch-heads/*:refs/remotes/branch-heads/*",
+ github_path,
+ )
+ run_git("git fetch upstream", github_path)
+ else:
+ print("Upstream remote branch-heads already configured")
+
+ # do a sanity fetch in case this was not a freshly cloned copy of the
+ # repo, meaning it may not have all the mozilla branches present.
+ run_git("git fetch --all", github_path)
+
+ # create tar to avoid time refetching
+ if not os.path.exists(tar_path):
+ print("Creating tar file for quicker restore")
+ cmd = "cd {} ; tar --create --gzip --file={} {}".format(
+ os.path.dirname(github_path),
+ os.path.basename(tar_path),
+ os.path.basename(github_path),
+ )
+ run_shell(cmd, capture_output)
+
+
+if __name__ == "__main__":
+ default_state_dir = ".moz-fast-forward"
+ default_tar_name = "moz-libwebrtc.tar.gz"
+
+ parser = argparse.ArgumentParser(
+ description="Restore moz-libwebrtc github patch stack"
+ )
+ parser.add_argument(
+ "--repo-path",
+ required=True,
+ help="path to libwebrtc repo",
+ )
+ parser.add_argument(
+ "--force-fetch",
+ action="store_true",
+ default=False,
+ help="force rebuild an existing repo directory",
+ )
+ parser.add_argument(
+ "--tar-name",
+ default=default_tar_name,
+ help="name of tar file (defaults to {})".format(default_tar_name),
+ )
+ parser.add_argument(
+ "--state-path",
+ default=default_state_dir,
+ help="path to state directory (defaults to {})".format(default_state_dir),
+ )
+ args = parser.parse_args()
+
+ fetch_repo(
+ args.repo_path, args.force_fetch, os.path.join(args.state_path, args.tar_name)
+ )
diff --git a/dom/media/webrtc/third_party_build/filter_git_changes.py b/dom/media/webrtc/third_party_build/filter_git_changes.py
new file mode 100644
index 0000000000..f8964e145b
--- /dev/null
+++ b/dom/media/webrtc/third_party_build/filter_git_changes.py
@@ -0,0 +1,72 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+import argparse
+import importlib
+import re
+import subprocess
+import sys
+
+sys.path.insert(0, "./dom/media/webrtc/third_party_build")
+vendor_libwebrtc = importlib.import_module("vendor-libwebrtc")
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(
+ description="Get relevant change count from an upstream git commit"
+ )
+ parser.add_argument(
+ "--repo-path",
+ required=True,
+ help="path to libwebrtc repo",
+ )
+ parser.add_argument("--commit-sha", required=True, help="sha of commit to examine")
+ parser.add_argument("--diff-filter", choices=("A", "D", "R"))
+ args = parser.parse_args()
+
+ command = [
+ "git",
+ "show",
+ "--oneline",
+ "--name-status",
+ "--pretty=format:",
+ None if not args.diff_filter else "--diff-filter={}".format(args.diff_filter),
+ args.commit_sha,
+ ]
+ # strip possible empty elements from command list
+ command = [x for x in command if x is not None]
+
+ # Get the list of changes in the upstream commit.
+ res = subprocess.run(
+ command,
+ capture_output=True,
+ text=True,
+ cwd=args.repo_path,
+ )
+ if res.returncode != 0:
+ sys.exit("error: {}".format(res.stderr.strip()))
+
+ changed_files = [line.strip() for line in res.stdout.strip().split("\n")]
+ changed_files = [line for line in changed_files if line != ""]
+
+ # Fetch the list of excludes and includes used in the vendoring script.
+ exclude_list = vendor_libwebrtc.get_excluded_paths()
+ include_list = vendor_libwebrtc.get_included_path_overrides()
+
+ # First, search for changes in files that are specifically included.
+ # Do this first, because some of these files might be filtered out
+ # by the exclude list.
+ regex_includes = "|".join(["^.\t{}".format(i) for i in include_list])
+ included_files = [
+ path for path in changed_files if re.findall(regex_includes, path)
+ ]
+
+ # Convert the exclude list to a regex string.
+ regex_excludes = "|".join(["^.\t{}".format(i) for i in exclude_list])
+
+ # Filter out the excluded files/paths.
+ files_not_excluded = [
+ path for path in changed_files if not re.findall(regex_excludes, path)
+ ]
+
+ for path in included_files + files_not_excluded:
+ print(path)
diff --git a/dom/media/webrtc/third_party_build/gn-configs/README.md b/dom/media/webrtc/third_party_build/gn-configs/README.md
new file mode 100644
index 0000000000..111d2d022e
--- /dev/null
+++ b/dom/media/webrtc/third_party_build/gn-configs/README.md
@@ -0,0 +1,16 @@
+# Generate new gn json files and moz.build files for building libwebrtc in our tree
+
+/!\ This is only supported on Linux and macOS. If you are on Windows, you can run
+the script under [WSL](https://docs.microsoft.com/en-us/windows/wsl/install).
+
+1. The script should be run from the top directory of our firefox tree.
+
+ ```
+ ./mach python python/mozbuild/mozbuild/gn_processor.py dom/media/webrtc/third_party_build/gn-configs/webrtc.json
+ ```
+
+2. Checkin all the generated/modified files and try your build!
+
+# Adding new configurations to the build
+
+Edit the `main` function in the `python/mozbuild/mozbuild/gn_processor.py` file.
diff --git a/dom/media/webrtc/third_party_build/gn-configs/webrtc.json b/dom/media/webrtc/third_party_build/gn-configs/webrtc.json
new file mode 100644
index 0000000000..9744558025
--- /dev/null
+++ b/dom/media/webrtc/third_party_build/gn-configs/webrtc.json
@@ -0,0 +1,83 @@
+{
+ "target_dir": "third_party/libwebrtc",
+ "gn_target": "//:webrtc",
+ "gn_sandbox_variables": {
+ "COMPILE_FLAGS": {
+ "WARNINGS_AS_ERRORS": []
+ },
+ "FINAL_LIBRARY": "webrtc"
+ },
+ "mozilla_flags": ["-fobjc-arc", "-mavx2", "-mfma", "-mfpu=neon", "-msse2"],
+ "write_mozbuild_variables": {
+ "INCLUDE_TK_CFLAGS_DIRS": [
+ "third_party/libwebrtc/modules/desktop_capture/desktop_capture_gn",
+ "third_party/libwebrtc/modules/portal/portal_gn"
+ ]
+ },
+ "non_unified_sources": [
+ "third_party/libwebrtc/api/audio_codecs/opus/audio_encoder_opus_config.cc",
+ "third_party/libwebrtc/api/video/i210_buffer.cc",
+ "third_party/libwebrtc/api/video/i410_buffer.cc",
+ "third_party/libwebrtc/api/video/i422_buffer.cc",
+ "third_party/libwebrtc/api/video/i444_buffer.cc",
+ "third_party/libwebrtc/audio/channel_send_frame_transformer_delegate.cc",
+ "third_party/libwebrtc/common_audio/vad/vad_core.c",
+ "third_party/libwebrtc/common_audio/vad/webrtc_vad.c",
+ "third_party/libwebrtc/common_audio/signal_processing/resample_by_2_mips.c",
+ "third_party/libwebrtc/modules/audio_coding/codecs/isac/fix/source/decode_plc.c",
+ "third_party/libwebrtc/modules/audio_coding/codecs/isac/fix/source/lpc_masking_model.c",
+ "third_party/libwebrtc/modules/audio_coding/codecs/isac/fix/source/pitch_filter.c",
+ "third_party/libwebrtc/modules/audio_coding/codecs/isac/fix/source/pitch_filter_c.c",
+ "third_party/libwebrtc/modules/audio_coding/neteq/audio_vector.cc",
+ "third_party/libwebrtc/modules/audio_coding/neteq/underrun_optimizer.cc",
+ "third_party/libwebrtc/modules/audio_device/android/audio_manager.cc",
+ "third_party/libwebrtc/modules/audio_device/android/audio_record_jni.cc",
+ "third_party/libwebrtc/modules/audio_device/android/audio_track_jni.cc",
+ "third_party/libwebrtc/modules/audio_device/android/opensles_player.cc",
+ "third_party/libwebrtc/modules/audio_device/linux/audio_device_pulse_linux.cc",
+ "third_party/libwebrtc/modules/audio_device/linux/audio_mixer_manager_pulse_linux.cc",
+ "third_party/libwebrtc/modules/audio_device/win/audio_device_core_win.cc",
+ "third_party/libwebrtc/modules/audio_processing/aecm/aecm_core.cc",
+ "third_party/libwebrtc/modules/audio_processing/aecm/aecm_core_c.cc",
+ "third_party/libwebrtc/modules/audio_processing/aecm/aecm_core_mips.cc",
+ "third_party/libwebrtc/modules/audio_processing/aecm/aecm_core_neon.cc",
+ "third_party/libwebrtc/modules/audio_processing/aecm/echo_control_mobile.cc",
+ "third_party/libwebrtc/modules/audio_processing/echo_control_mobile_impl.cc",
+ "third_party/libwebrtc/modules/audio_processing/echo_detector/normalized_covariance_estimator.cc",
+ "third_party/libwebrtc/modules/audio_processing/gain_control_impl.cc",
+ "third_party/libwebrtc/modules/audio_processing/rms_level.cc",
+ "third_party/libwebrtc/modules/congestion_controller/goog_cc/loss_based_bandwidth_estimation.cc",
+ "third_party/libwebrtc/modules/congestion_controller/goog_cc/send_side_bandwidth_estimation.cc",
+ "third_party/libwebrtc/modules/desktop_capture/fallback_desktop_capturer_wrapper.cc",
+ "third_party/libwebrtc/modules/desktop_capture/linux/wayland/base_capturer_pipewire.cc",
+ "third_party/libwebrtc/modules/desktop_capture/linux/wayland/moz_base_capturer_pipewire.cc",
+ "third_party/libwebrtc/modules/desktop_capture/mouse_cursor_monitor_linux.cc",
+ "third_party/libwebrtc/modules/desktop_capture/win/screen_capturer_win_gdi.cc",
+ "third_party/libwebrtc/modules/pacing/prioritized_packet_queue.cc",
+ "third_party/libwebrtc/modules/remote_bitrate_estimator/remote_bitrate_estimator_single_stream.cc",
+ "third_party/libwebrtc/modules/rtp_rtcp/source/flexfec_receiver.cc",
+ "third_party/libwebrtc/modules/rtp_rtcp/source/rtcp_packet/tmmbn.cc",
+ "third_party/libwebrtc/modules/rtp_rtcp/source/rtcp_packet/tmmbr.cc",
+ "third_party/libwebrtc/modules/rtp_rtcp/source/rtcp_sender.cc",
+ "third_party/libwebrtc/modules/rtp_rtcp/source/rtp_format_vp8.cc",
+ "third_party/libwebrtc/modules/rtp_rtcp/source/rtp_rtcp_impl2.cc",
+ "third_party/libwebrtc/modules/rtp_rtcp/source/rtp_sender_egress.cc",
+ "third_party/libwebrtc/modules/rtp_rtcp/source/rtp_sender_video.cc",
+ "third_party/libwebrtc/modules/rtp_rtcp/source/rtp_util.cc",
+ "third_party/libwebrtc/modules/rtp_rtcp/source/ulpfec_generator.cc",
+ "third_party/libwebrtc/modules/rtp_rtcp/source/video_rtp_depacketizer_vp9.cc",
+ "third_party/libwebrtc/modules/third_party/g722/g722_encode.c",
+ "third_party/libwebrtc/modules/video_capture/windows/device_info_ds.cc",
+ "third_party/libwebrtc/modules/video_capture/windows/help_functions_ds.cc",
+ "third_party/libwebrtc/modules/video_capture/windows/sink_filter_ds.cc",
+ "third_party/libwebrtc/modules/video_coding/codecs/vp8/screenshare_layers.cc",
+ "third_party/libwebrtc/modules/video_coding/svc/scalability_structure_key_svc.cc",
+ "third_party/libwebrtc/modules/video_coding/svc/scalability_structure_simulcast.cc",
+ "third_party/libwebrtc/rtc_base/win/hstring.cc",
+ "third_party/libwebrtc/third_party/abseil-cpp/absl/strings/numbers.cc",
+ "third_party/libwebrtc/third_party/abseil-cpp/absl/synchronization/blocking_counter.cc",
+ "third_party/libwebrtc/third_party/abseil-cpp/absl/time/internal/cctz/src/time_zone_posix.cc",
+ "third_party/libwebrtc/third_party/abseil-cpp/absl/time/time.cc",
+ "third_party/libwebrtc/video/rtp_video_stream_receiver2.cc"
+ ]
+}
diff --git a/dom/media/webrtc/third_party_build/lookup_branch_head.py b/dom/media/webrtc/third_party_build/lookup_branch_head.py
new file mode 100644
index 0000000000..afd0e6c791
--- /dev/null
+++ b/dom/media/webrtc/third_party_build/lookup_branch_head.py
@@ -0,0 +1,98 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+import argparse
+import json
+import os
+import pathlib
+import sys
+import urllib.request
+
+# default cache file location in STATE_DIR location
+default_cache_path = ".moz-fast-forward/milestone.cache"
+
+
+def fetch_branch_head_dict():
+ milestone_url = (
+ "https://chromiumdash.appspot.com/fetch_milestones?only_branched=true"
+ )
+ uf = urllib.request.urlopen(milestone_url)
+ html = uf.read()
+ milestone_dict = json.loads(html)
+
+ # There is more information in the json dictionary, but we only care
+ # about the milestone (version) to branch "name" (webrtc_branch)
+ # info. For example:
+ # v106 -> 5249 (which translates to branch-heads/5249)
+ # v107 -> 5304 (which translates to branch-heads/5304)
+ #
+ # As returned from web query, milestones are integers and branch
+ # "names" are strings.
+ new_dict = {}
+ for row in milestone_dict:
+ new_dict[row["milestone"]] = row["webrtc_branch"]
+
+ return new_dict
+
+
+def read_dict_from_cache(cache_path):
+ if cache_path is not None and os.path.exists(cache_path):
+ with open(cache_path, "r") as ifile:
+ return json.loads(ifile.read(), object_hook=jsonKeys2int)
+ return {}
+
+
+def write_dict_to_cache(cache_path, milestones):
+ with open(cache_path, "w") as ofile:
+ ofile.write(json.dumps(milestones))
+
+
+def get_branch_head(milestone, cache_path=default_cache_path):
+ milestones = read_dict_from_cache(cache_path)
+
+ # if the cache didn't exist or is stale, try to fetch using a web query
+ if milestone not in milestones:
+ try:
+ milestones = fetch_branch_head_dict()
+ write_dict_to_cache(cache_path, milestones)
+ except Exception:
+ pass
+
+ if milestone in milestones:
+ return milestones[milestone]
+ return None
+
+
+# From https://stackoverflow.com/questions/1450957/pythons-json-module-converts-int-dictionary-keys-to-strings
+def jsonKeys2int(x):
+ if isinstance(x, dict):
+ return {int(k): v for k, v in x.items()}
+ return x
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(
+ description="Get libwebrtc branch-head for given chromium milestone"
+ )
+ parser.add_argument(
+ "milestone", type=int, help="integer chromium milestone (example: 106)"
+ )
+ parser.add_argument("-v", "--verbose", action="store_true")
+ parser.add_argument("-c", "--cache", type=pathlib.Path, help="path to cache file")
+ args = parser.parse_args()
+
+ # if the user provided a cache path use it, otherwise use the default
+ local_cache_path = args.cache or default_cache_path
+
+ branch_head = get_branch_head(args.milestone, local_cache_path)
+ if branch_head is None:
+ sys.exit("error: chromium milestone '{}' is not found.".format(args.milestone))
+
+ if args.verbose:
+ print(
+ "chromium milestone {} uses branch-heads/{}".format(
+ args.milestone, branch_head
+ )
+ )
+ else:
+ print(branch_head)
diff --git a/dom/media/webrtc/third_party_build/loop-ff.sh b/dom/media/webrtc/third_party_build/loop-ff.sh
new file mode 100644
index 0000000000..72a8d88091
--- /dev/null
+++ b/dom/media/webrtc/third_party_build/loop-ff.sh
@@ -0,0 +1,236 @@
+#!/bin/bash
+
+source dom/media/webrtc/third_party_build/use_config_env.sh
+
+# file for logging loop script output
+LOOP_OUTPUT_LOG=$LOG_DIR/log-loop-ff.txt
+
+function echo_log()
+{
+ echo "===loop-ff=== $@" 2>&1| tee -a $LOOP_OUTPUT_LOG
+}
+
+function show_error_msg()
+{
+ echo_log "*** ERROR *** $? line $1 $0 did not complete successfully!"
+ echo_log "$ERROR_HELP"
+}
+ERROR_HELP=""
+
+# Print an Error message if `set -eE` causes the script to exit due to a failed command
+trap 'show_error_msg $LINENO' ERR
+
+# If DEBUG_LOOP_FF is set all commands should be printed as they are executed
+if [ ! "x$DEBUG_LOOP_FF" = "x" ]; then
+ set -x
+fi
+
+if [ "x$MOZ_LIBWEBRTC_SRC" = "x" ]; then
+ echo "MOZ_LIBWEBRTC_SRC is not defined, see README.md"
+ exit
+fi
+
+if [ ! -d $MOZ_LIBWEBRTC_SRC ]; then
+ echo "Path $MOZ_LIBWEBRTC_SRC is not found, see README.md"
+ exit
+fi
+
+if [ "x$MOZ_LIBWEBRTC_BRANCH" = "x" ]; then
+ echo "MOZ_LIBWEBRTC_BRANCH is not defined, see README.md"
+ exit
+fi
+
+if [ "x$MOZ_STOP_AFTER_COMMIT" = "x" ]; then
+ MOZ_STOP_AFTER_COMMIT=`cd $MOZ_LIBWEBRTC_SRC ; git show $MOZ_TARGET_UPSTREAM_BRANCH_HEAD --format='%h' --name-only | head -1`
+ echo "No MOZ_STOP_AFTER_COMMIT variable defined - stopping at $MOZ_TARGET_UPSTREAM_BRANCH_HEAD"
+fi
+
+if [ "x$MOZ_ADVANCE_ONE_COMMIT" = "x" ]; then
+ MOZ_ADVANCE_ONE_COMMIT=""
+fi
+
+if [ "x$SKIP_NEXT_REVERT_CHK" = "x" ]; then
+ SKIP_NEXT_REVERT_CHK="0"
+fi
+
+MOZ_CHANGED=0
+GIT_CHANGED=0
+HANDLE_NOOP_COMMIT=""
+
+# After this point:
+# * eE: All commands should succeed.
+# * u: All variables should be defined before use.
+# * o pipefail: All stages of all pipes should succeed.
+set -eEuo pipefail
+
+# start a new log with every run of this script
+rm -f $LOOP_OUTPUT_LOG
+# make sure third_party/libwebrtc/README.moz-ff-commit is the committed version
+# so we properly determine MOZ_LIBWEBRTC_BASE and MOZ_LIBWEBRTC_NEXT_BASE
+# in the loop below
+hg revert -C third_party/libwebrtc/README.moz-ff-commit &> /dev/null
+
+# check for a resume situation from fast-forward-libwebrtc.sh
+RESUME=""
+if [ -f $STATE_DIR/resume_state ]; then
+ RESUME=`tail -1 $STATE_DIR/resume_state`
+fi
+
+ERROR_HELP=$"
+It appears that initial vendoring verification has failed.
+- If you have never run the fast-forward process before, you may need to
+ prepare the github repository by running prep_repo.sh.
+- If you have previously run loop-ff.sh successfully, there may be a new
+ change to third_party/libwebrtc that should be extracted and added to
+ the patch stack in github. It may be as easy as running:
+ ./mach python $SCRIPT_DIR/extract-for-git.py tip::tip
+ mv mailbox.patch $MOZ_LIBWEBRTC_SRC
+ (cd $MOZ_LIBWEBRTC_SRC && \\
+ git am mailbox.patch)
+"
+# if we're not in the resume situation from fast-forward-libwebrtc.sh
+if [ "x$RESUME" = "x" ]; then
+ # start off by verifying the vendoring process to make sure no changes have
+ # been added to elm to fix bugs.
+ echo_log "Verifying vendoring..."
+ bash $SCRIPT_DIR/verify_vendoring.sh
+ echo_log "Done verifying vendoring."
+fi
+ERROR_HELP=""
+
+for (( ; ; )); do
+
+find_base_commit
+find_next_commit
+
+if [ $MOZ_LIBWEBRTC_BASE == $MOZ_LIBWEBRTC_NEXT_BASE ]; then
+ echo_log "Processing complete, already at upstream $MOZ_LIBWEBRTC_BASE"
+ exit
+fi
+
+echo_log "==================="
+
+COMMITS_REMAINING=`cd $MOZ_LIBWEBRTC_SRC ; \
+ git log --oneline $MOZ_LIBWEBRTC_BASE..$MOZ_TARGET_UPSTREAM_BRANCH_HEAD \
+ | wc -l | tr -d " "`
+echo_log "Commits remaining: $COMMITS_REMAINING"
+
+ERROR_HELP=$"Some portion of the detection and/or fixing of upstream revert commits
+has failed. Please fix the state of the git hub repo at: $MOZ_LIBWEBRTC_SRC.
+When fixed, please resume this script with the following command:
+ SKIP_NEXT_REVERT_CHK=1 bash $SCRIPT_DIR/loop-ff.sh
+"
+if [ "x$SKIP_NEXT_REVERT_CHK" == "x0" ]; then
+ echo_log "Check for upcoming revert commit"
+ AUTO_FIX_REVERT_AS_NOOP=1 bash $SCRIPT_DIR/detect_upstream_revert.sh \
+ 2>&1| tee -a $LOOP_OUTPUT_LOG
+fi
+SKIP_NEXT_REVERT_CHK="0"
+ERROR_HELP=""
+
+echo_log "Looking for $STATE_DIR/$MOZ_LIBWEBRTC_NEXT_BASE.no-op-cherry-pick-msg"
+if [ -f $STATE_DIR/$MOZ_LIBWEBRTC_NEXT_BASE.no-op-cherry-pick-msg ]; then
+ echo_log "Detected special commit msg, setting HANDLE_NOOP_COMMIT=1"
+ HANDLE_NOOP_COMMIT="1"
+fi
+
+echo_log "Moving from moz-libwebrtc commit $MOZ_LIBWEBRTC_BASE to $MOZ_LIBWEBRTC_NEXT_BASE"
+bash $SCRIPT_DIR/fast-forward-libwebrtc.sh 2>&1| tee -a $LOOP_OUTPUT_LOG
+
+MOZ_CHANGED=`hg diff -c tip --stat \
+ | egrep -ve "README.moz-ff-commit|README.mozilla|files changed," \
+ | wc -l | tr -d " " || true`
+GIT_CHANGED=`./mach python $SCRIPT_DIR/filter_git_changes.py \
+ --repo-path $MOZ_LIBWEBRTC_SRC --commit-sha $MOZ_LIBWEBRTC_NEXT_BASE \
+ | wc -l | tr -d " "`
+FILE_CNT_MISMATCH_MSG=$"
+The number of files changed in the upstream commit ($GIT_CHANGED) does
+not match the number of files changed in the local Mozilla repo
+commit ($MOZ_CHANGED). This may indicate a mismatch between the vendoring
+script and this script, or it could be a true error in the import
+processing. Once the issue has been resolved, the following steps
+remain for this commit:
+ # generate moz.build files (may not be necessary)
+ ./mach python python/mozbuild/mozbuild/gn_processor.py \\
+ $SCRIPT_DIR/gn-configs/webrtc.json
+ # commit the updated moz.build files with the appropriate commit msg
+ bash $SCRIPT_DIR/commit-build-file-changes.sh
+ # do a (hopefully) quick test build
+ ./mach build
+"
+echo_log "Verify number of files changed MOZ($MOZ_CHANGED) GIT($GIT_CHANGED)"
+if [ "x$HANDLE_NOOP_COMMIT" == "x1" ]; then
+ echo_log "NO-OP commit detected, we expect file changed counts to differ"
+elif [ $MOZ_CHANGED -ne $GIT_CHANGED ]; then
+ echo_log "MOZ_CHANGED $MOZ_CHANGED should equal GIT_CHANGED $GIT_CHANGED"
+ echo "$FILE_CNT_MISMATCH_MSG"
+ exit 1
+fi
+HANDLE_NOOP_COMMIT=""
+
+# save the current patch stack in case we need to reconstitute it later
+./mach python $SCRIPT_DIR/save_patch_stack.py \
+ --repo-path $MOZ_LIBWEBRTC_SRC \
+ --branch $MOZ_LIBWEBRTC_BRANCH \
+ --patch-path "third_party/libwebrtc/moz-patch-stack" \
+ --state-path $STATE_DIR \
+ --target-branch-head $MOZ_TARGET_UPSTREAM_BRANCH_HEAD \
+ 2>&1| tee -a $LOOP_OUTPUT_LOG
+
+MODIFIED_BUILD_RELATED_FILE_CNT=`hg diff -c tip --stat \
+ --include 'third_party/libwebrtc/**BUILD.gn' \
+ --include 'third_party/libwebrtc/webrtc.gni' \
+ | grep -v "files changed" \
+ | wc -l | tr -d " " || true`
+ERROR_HELP=$"
+Generating build files has failed. This likely means changes to one or more
+BUILD.gn files are required. Commit those changes following the instructions
+in https://wiki.mozilla.org/Media/WebRTC/libwebrtc_Update_Process#Operational_notes
+Then complete these steps:
+ # generate moz.build files (may not be necessary)
+ ./mach python python/mozbuild/mozbuild/gn_processor.py \\
+ $SCRIPT_DIR/gn-configs/webrtc.json
+ # commit the updated moz.build files with the appropriate commit msg
+ bash $SCRIPT_DIR/commit-build-file-changes.sh
+ # do a (hopefully) quick test build
+ ./mach build
+After a successful build, you may resume this script.
+"
+echo_log "Modified BUILD.gn (or webrtc.gni) files: $MODIFIED_BUILD_RELATED_FILE_CNT"
+if [ "x$MODIFIED_BUILD_RELATED_FILE_CNT" != "x0" ]; then
+ echo_log "Regenerate build files"
+ ./mach python python/mozbuild/mozbuild/gn_processor.py \
+ $SCRIPT_DIR/gn-configs/webrtc.json 2>&1| tee -a $LOOP_OUTPUT_LOG
+
+ MOZ_BUILD_CHANGE_CNT=`hg status third_party/libwebrtc \
+ --include 'third_party/libwebrtc/**moz.build' | wc -l | tr -d " "`
+ if [ "x$MOZ_BUILD_CHANGE_CNT" != "x0" ]; then
+ echo_log "Detected modified moz.build files, commiting"
+ fi
+
+ bash $SCRIPT_DIR/commit-build-file-changes.sh 2>&1| tee -a $LOOP_OUTPUT_LOG
+fi
+ERROR_HELP=""
+
+ERROR_HELP=$"
+The test build has failed. Most likely this is due to an upstream api change that
+must be reflected in Mozilla code outside of the third_party/libwebrtc directory.
+"
+echo_log "Test build"
+./mach build 2>&1| tee -a $LOOP_OUTPUT_LOG
+ERROR_HELP=""
+
+if [ ! "x$MOZ_STOP_AFTER_COMMIT" = "x" ]; then
+if [ $MOZ_LIBWEBRTC_NEXT_BASE = $MOZ_STOP_AFTER_COMMIT ]; then
+ break
+fi
+fi
+
+if [ ! "x$MOZ_ADVANCE_ONE_COMMIT" = "x" ]; then
+ echo_log "Done advancing one commit."
+ exit
+fi
+
+done
+
+echo_log "Completed fast-foward to $MOZ_STOP_AFTER_COMMIT"
diff --git a/dom/media/webrtc/third_party_build/make_upstream_revert_noop.sh b/dom/media/webrtc/third_party_build/make_upstream_revert_noop.sh
new file mode 100755
index 0000000000..9f5fdd7b82
--- /dev/null
+++ b/dom/media/webrtc/third_party_build/make_upstream_revert_noop.sh
@@ -0,0 +1,101 @@
+#!/bin/bash
+
+# This script takes the current base sha, the next base sha, and the sha
+# of the commit that reverts the next base as determined by
+# detect_upstream_revert.sh and "inserts" two commits at the bottom of the
+# moz_libwebrtc GitHub patch stack. The two commits are exact copies of
+# the upcoming commit and its corresponding revert commit, with commit
+# messages indicating they are temporary commits. Additionally, 2 files
+# are written that act as markers for the fast-forward script and contain
+# supplemental commit message text.
+#
+# When the fast-forward script runs, it will rebase onto the next base
+# sha. Since we have a corresponding, identical temp commit at the bottom
+# of our patch stack, the temp commit will be absorbed as unnecessary.
+# Since the patch stack now has the temp revert commit at the bottom, this
+# results in a “no-op” commit. The marker file indicates that specially
+# handling should occur in the fast-forward-libwebrtc.sh and loop-ff.sh
+# scripts. This special handling includes adding the supplemental commit
+# text that explains why the commit is a no-op (or empty) commit and
+# skipping the verification of the number of files changed between our
+# mercurial repository and the GitHub repository.
+
+function show_error_msg()
+{
+ echo "*** ERROR *** $? line $1 $0 did not complete successfully!"
+ echo "$ERROR_HELP"
+}
+ERROR_HELP=""
+
+# Print an Error message if `set -eE` causes the script to exit due to a failed command
+trap 'show_error_msg $LINENO' ERR
+
+source dom/media/webrtc/third_party_build/use_config_env.sh
+
+# If DEBUG_GEN is set all commands should be printed as they are executed
+if [ ! "x$DEBUG_GEN" = "x" ]; then
+ set -x
+fi
+
+if [ "x$MOZ_LIBWEBRTC_SRC" = "x" ]; then
+ echo "MOZ_LIBWEBRTC_SRC is not defined, see README.md"
+ exit
+fi
+
+if [ -d $MOZ_LIBWEBRTC_SRC ]; then
+ echo "MOZ_LIBWEBRTC_SRC is $MOZ_LIBWEBRTC_SRC"
+else
+ echo "Path $MOZ_LIBWEBRTC_SRC is not found, see README.md"
+ exit
+fi
+
+if [ "x$MOZ_LIBWEBRTC_BRANCH" = "x" ]; then
+ echo "MOZ_LIBWEBRTC_BRANCH is not defined, see README.md"
+ exit
+fi
+
+# After this point:
+# * eE: All commands should succeed.
+# * u: All variables should be defined before use.
+# * o pipefail: All stages of all pipes should succeed.
+set -eEuo pipefail
+
+find_base_commit
+find_next_commit
+echo "MOZ_LIBWEBRTC_BASE: $MOZ_LIBWEBRTC_BASE"
+echo "MOZ_LIBWEBRTC_NEXT_BASE: $MOZ_LIBWEBRTC_NEXT_BASE"
+echo "MOZ_LIBWEBRTC_REVERT_SHA: $MOZ_LIBWEBRTC_REVERT_SHA"
+
+# These files serve dual purposes:
+# 1) They serve as marker/indicator files to loop-ff.sh to
+# know to process the commit differently, accounting for
+# the no-op nature of the commit and it's corresponding
+# revert commit.
+# 2) The contain supplemental commit message text to explain
+# why the commits are essentially empty.
+# They are written first on the off chance that the rebase
+# operation below fails and requires manual intervention,
+# thus avoiding the operator of these scripts to remember to
+# generate these two files.
+echo $"Essentially a no-op since we're going to see this change
+reverted when we vendor in $MOZ_LIBWEBRTC_REVERT_SHA." \
+> $STATE_DIR/$MOZ_LIBWEBRTC_NEXT_BASE.no-op-cherry-pick-msg
+
+echo "We already cherry-picked this when we vendored $MOZ_LIBWEBRTC_NEXT_BASE." \
+> $STATE_DIR/$MOZ_LIBWEBRTC_REVERT_SHA.no-op-cherry-pick-msg
+
+cd $MOZ_LIBWEBRTC_SRC
+rm -f $TMP_DIR/*.patch $TMP_DIR/*.patch.bak
+git checkout -b moz-cherry-pick $MOZ_LIBWEBRTC_BASE
+git format-patch -o $TMP_DIR -k --start-number 1 \
+ $MOZ_LIBWEBRTC_NEXT_BASE^..$MOZ_LIBWEBRTC_NEXT_BASE
+git format-patch -o $TMP_DIR -k --start-number 2 \
+ $MOZ_LIBWEBRTC_REVERT_SHA^..$MOZ_LIBWEBRTC_REVERT_SHA
+sed -i.bak -e "/^Subject: / s/$/ ($MOZ_LIBWEBRTC_NEXT_BASE)/" $TMP_DIR/0001*.patch
+sed -i.bak -e "/^Subject: / s/$/ ($MOZ_LIBWEBRTC_REVERT_SHA)/" $TMP_DIR/0002*.patch
+sed -i.bak -e 's/^Subject: /Subject: (tmp-cherry-pick) /' $TMP_DIR/*.patch
+git am $TMP_DIR/*.patch
+git checkout $MOZ_LIBWEBRTC_BRANCH
+git rebase moz-cherry-pick
+git branch -d moz-cherry-pick
+
diff --git a/dom/media/webrtc/third_party_build/pre-warmed-milestone.cache b/dom/media/webrtc/third_party_build/pre-warmed-milestone.cache
new file mode 100644
index 0000000000..826268db47
--- /dev/null
+++ b/dom/media/webrtc/third_party_build/pre-warmed-milestone.cache
@@ -0,0 +1 @@
+{"109": "5414", "108": "5359", "107": "5304", "106": "5249", "105": "5195"}
diff --git a/dom/media/webrtc/third_party_build/prep_repo.sh b/dom/media/webrtc/third_party_build/prep_repo.sh
new file mode 100644
index 0000000000..9f22f63253
--- /dev/null
+++ b/dom/media/webrtc/third_party_build/prep_repo.sh
@@ -0,0 +1,93 @@
+#!/bin/bash
+
+function show_error_msg()
+{
+ echo "*** ERROR *** $? line $1 $0 did not complete successfully!"
+ echo "$ERROR_HELP"
+}
+ERROR_HELP=""
+
+# Print an Error message if `set -eE` causes the script to exit due to a failed command
+trap 'show_error_msg $LINENO' ERR
+
+source dom/media/webrtc/third_party_build/use_config_env.sh
+
+echo "MOZ_LIBWEBRTC_SRC: $MOZ_LIBWEBRTC_SRC"
+echo "MOZ_LIBWEBRTC_BRANCH: $MOZ_LIBWEBRTC_BRANCH"
+echo "MOZ_FASTFORWARD_BUG: $MOZ_FASTFORWARD_BUG"
+
+# After this point:
+# * eE: All commands should succeed.
+# * u: All variables should be defined before use.
+# * o pipefail: All stages of all pipes should succeed.
+set -eEuo pipefail
+
+# wipe resume_state for new run
+rm -f $STATE_DIR/resume_state
+
+# If there is no cache file for the branch-head lookups done in
+# update_default_config.sh, go ahead and copy our small pre-warmed
+# version.
+if [ ! -f $STATE_DIR/milestone.cache ]; then
+ cp $SCRIPT_DIR/pre-warmed-milestone.cache $STATE_DIR/milestone.cache
+fi
+
+# fetch the github repro
+./mach python $SCRIPT_DIR/fetch_github_repo.py \
+ --repo-path $MOZ_LIBWEBRTC_SRC \
+ --state-path $STATE_DIR
+
+CURRENT_DIR=`pwd`
+cd $MOZ_LIBWEBRTC_SRC
+
+# clear any possible previous patches
+rm -f *.patch
+
+# create a new work branch and "export" a new patch stack to rebase
+# find the common commit between our upstream release branch and trunk
+CHERRY_PICK_BASE=`git merge-base branch-heads/$MOZ_PRIOR_UPSTREAM_BRANCH_HEAD_NUM master`
+echo "common commit: $CHERRY_PICK_BASE"
+
+# create a new branch at the common commit and checkout the new branch
+ERROR_HELP=$"
+Unable to create branch '$MOZ_LIBWEBRTC_BRANCH'. This probably means
+that prep_repo.sh is being called on a repo that already has a patch
+stack in progress. If you're sure you want to do this, the following
+commands will allow the process to continue:
+ ( cd $MOZ_LIBWEBRTC_SRC && \\
+ git checkout $MOZ_LIBWEBRTC_BRANCH && \\
+ git checkout -b $MOZ_LIBWEBRTC_BRANCH-old && \\
+ git branch -D $MOZ_LIBWEBRTC_BRANCH ) && \\
+ bash $0
+"
+git branch $MOZ_LIBWEBRTC_BRANCH $CHERRY_PICK_BASE
+ERROR_HELP=""
+git checkout $MOZ_LIBWEBRTC_BRANCH
+
+# make sure we're starting with a clean tmp directory
+rm -f $TMP_DIR/*.patch $TMP_DIR/*.patch.bak
+
+# grab the patches for all the commits in chrome's release branch for libwebrtc
+git format-patch -o $TMP_DIR -k $CHERRY_PICK_BASE..branch-heads/$MOZ_PRIOR_UPSTREAM_BRANCH_HEAD_NUM
+# tweak the release branch commit summaries to show they were cherry picked
+sed -i.bak -e "/^Subject: / s/^Subject: /Subject: (cherry-pick-branch-heads\/$MOZ_PRIOR_UPSTREAM_BRANCH_HEAD_NUM) /" $TMP_DIR/*.patch
+git am $TMP_DIR/*.patch # applies to branch mozpatches
+rm $TMP_DIR/*.patch $TMP_DIR/*.patch.bak
+
+# we don't use restore_patch_stack.py here because it would overwrite the patches
+# from the previous release branch we just added in the above step.
+
+# grab all the moz patches and apply
+git am $CURRENT_DIR/third_party/libwebrtc/moz-patch-stack/*.patch
+
+cd $CURRENT_DIR
+
+# cp all the no-op files to STATE_DIR
+NO_OP_FILE_COUNT=`ls third_party/libwebrtc/moz-patch-stack \
+ | grep "no-op-cherry-pick-msg" | wc -l | tr -d " " || true`
+if [ "x$NO_OP_FILE_COUNT" != "x0" ]; then
+ cp $CURRENT_DIR/third_party/libwebrtc/moz-patch-stack/*.no-op-cherry-pick-msg \
+ $STATE_DIR
+fi
+
+bash $SCRIPT_DIR/verify_vendoring.sh || true
diff --git a/dom/media/webrtc/third_party_build/push_official_branch.sh b/dom/media/webrtc/third_party_build/push_official_branch.sh
new file mode 100644
index 0000000000..e7ed17df2b
--- /dev/null
+++ b/dom/media/webrtc/third_party_build/push_official_branch.sh
@@ -0,0 +1,50 @@
+#!/bin/bash
+
+# This script is a simple helper script that creates a branch name
+# in moz-libwebrtc and pushes it to origin. You will need permissions
+# to the mozilla fork of libwebrtc in order to complete this operation.
+# The repo is: https://github.com/mozilla/libwebrtc/
+#
+# Note: this should only be run after elm has been merged to mozilla-central
+
+function show_error_msg()
+{
+ echo "*** ERROR *** $? line $1 $0 did not complete successfully!"
+ echo "$ERROR_HELP"
+}
+ERROR_HELP=""
+
+# Print an Error message if `set -eE` causes the script to exit due to a failed command
+trap 'show_error_msg $LINENO' ERR
+
+source dom/media/webrtc/third_party_build/use_config_env.sh
+
+# After this point:
+# * eE: All commands should succeed.
+# * o pipefail: All stages of all pipes should succeed.
+set -eEo pipefail
+
+cd $MOZ_LIBWEBRTC_SRC
+
+git fetch
+
+if git show-ref --quiet refs/remotes/origin/$MOZ_LIBWEBRTC_OFFICIAL_BRANCH; then
+ echo "Branch '$MOZ_LIBWEBRTC_OFFICIAL_BRANCH' already exists remotely. Nothing to do."
+
+ echo "Please ensure this branch information can be found on Bug $MOZ_FASTFORWARD_BUG"
+ echo "https://github.com/mozilla/libwebrtc/tree/$MOZ_LIBWEBRTC_OFFICIAL_BRANCH"
+ exit 0
+fi
+
+if git show-ref --quiet refs/heads/$MOZ_LIBWEBRTC_OFFICIAL_BRANCH; then
+ echo "Branch '$MOZ_LIBWEBRTC_OFFICIAL_BRANCH' already exists locally. No need to create it."
+else
+ echo "Creating branch '$MOZ_LIBWEBRTC_OFFICIAL_BRANCH'"
+ git branch $MOZ_LIBWEBRTC_OFFICIAL_BRANCH
+fi
+
+echo "Pushing the branch to https://github.com/mozilla/libwebrtc"
+git push origin $MOZ_LIBWEBRTC_OFFICIAL_BRANCH
+
+echo "Please add this new branch information to Bug $MOZ_FASTFORWARD_BUG"
+echo "https://github.com/mozilla/libwebrtc/tree/$MOZ_LIBWEBRTC_OFFICIAL_BRANCH"
diff --git a/dom/media/webrtc/third_party_build/restore_elm_arcconfig.py b/dom/media/webrtc/third_party_build/restore_elm_arcconfig.py
new file mode 100644
index 0000000000..00c0bd7309
--- /dev/null
+++ b/dom/media/webrtc/third_party_build/restore_elm_arcconfig.py
@@ -0,0 +1,27 @@
+#!/bin/env python
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+from subprocess import run
+
+# This script sets the Arcanist configuration for the elm repo. This script
+# should be run after each repository reset.
+#
+# Usage: from the root of the repo `./mach python dom/media/webrtc/third_party_build/restore_elm_arcconfig.py`
+#
+
+ret = run(
+ [
+ "hg",
+ "import",
+ "-m",
+ "Bug 1729988 - FLOAT - REPO-elm - update .arcconfig repo callsign r=bgrins",
+ "dom/media/webrtc/third_party_build/elm_arcconfig.patch",
+ ]
+).returncode
+if ret != 0:
+ raise Exception(f"Failed to add FLOATing arcconfig patch for ELM: { ret }")
+else:
+ print("ELM .arcconfig restored. Please push this change to ELM")
diff --git a/dom/media/webrtc/third_party_build/restore_patch_stack.py b/dom/media/webrtc/third_party_build/restore_patch_stack.py
new file mode 100644
index 0000000000..6db3a64192
--- /dev/null
+++ b/dom/media/webrtc/third_party_build/restore_patch_stack.py
@@ -0,0 +1,107 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+import argparse
+import os
+import re
+import shutil
+
+from fetch_github_repo import fetch_repo
+from run_operations import run_git, run_shell
+
+# This script restores the mozilla patch stack and no-op commit tracking
+# files. In the case of repo corruption or a mistake made during
+# various rebase conflict resolution operations, the patch-stack can be
+# restored rather than requiring the user to restart the fast-forward
+# process from the beginning.
+
+
+def get_last_line(file_path):
+ # technique from https://stackoverflow.com/questions/46258499/how-to-read-the-last-line-of-a-file-in-python
+ with open(file_path, "rb") as f:
+ try: # catch OSError in case of a one line file
+ f.seek(-2, os.SEEK_END)
+ while f.read(1) != b"\n":
+ f.seek(-2, os.SEEK_CUR)
+ except OSError:
+ f.seek(0)
+ return f.readline().decode().strip()
+
+
+def restore_patch_stack(
+ github_path, github_branch, patch_directory, state_directory, tar_name
+):
+ # first, refetch the repo (hopefully utilizing the tarfile for speed) so
+ # the patches apply cleanly
+ fetch_repo(github_path, True, os.path.join(state_directory, tar_name))
+
+ # remove any stale no-op-cherry-pick-msg files in state_directory
+ run_shell("rm {}/*.no-op-cherry-pick-msg || true".format(state_directory))
+
+ # lookup latest vendored commit from third_party/libwebrtc/README.moz-ff-commit
+ file = os.path.abspath("third_party/libwebrtc/README.moz-ff-commit")
+ last_vendored_commit = get_last_line(file)
+
+ # checkout the previous vendored commit with proper branch name
+ cmd = "git checkout -b {} {}".format(github_branch, last_vendored_commit)
+ run_git(cmd, github_path)
+
+ # restore the patches to moz-libwebrtc repo, use run_shell instead of
+ # run_hg to allow filepath wildcard
+ print("Restoring patch stack")
+ run_shell("cd {} && git am {}/*.patch".format(github_path, patch_directory))
+
+ # it is also helpful to restore the no-op-cherry-pick-msg files to
+ # the state directory so that if we're restoring a patch-stack we
+ # also restore the possibly consumed no-op tracking files.
+ no_op_files = [
+ path
+ for path in os.listdir(patch_directory)
+ if re.findall(".*no-op-cherry-pick-msg$", path)
+ ]
+ for file in no_op_files:
+ shutil.copy(os.path.join(patch_directory, file), state_directory)
+
+
+if __name__ == "__main__":
+ default_patch_dir = "third_party/libwebrtc/moz-patch-stack"
+ default_state_dir = ".moz-fast-forward"
+ default_tar_name = "moz-libwebrtc.tar.gz"
+
+ parser = argparse.ArgumentParser(
+ description="Restore moz-libwebrtc github patch stack"
+ )
+ parser.add_argument(
+ "--repo-path",
+ required=True,
+ help="path to libwebrtc repo",
+ )
+ parser.add_argument(
+ "--branch",
+ default="mozpatches",
+ help="moz-libwebrtc branch (defaults to mozpatches)",
+ )
+ parser.add_argument(
+ "--patch-path",
+ default=default_patch_dir,
+ help="path to save patches (defaults to {})".format(default_patch_dir),
+ )
+ parser.add_argument(
+ "--tar-name",
+ default=default_tar_name,
+ help="name of tar file (defaults to {})".format(default_tar_name),
+ )
+ parser.add_argument(
+ "--state-path",
+ default=default_state_dir,
+ help="path to state directory (defaults to {})".format(default_state_dir),
+ )
+ args = parser.parse_args()
+
+ restore_patch_stack(
+ args.repo_path,
+ args.branch,
+ os.path.abspath(args.patch_path),
+ args.state_path,
+ args.tar_name,
+ )
diff --git a/dom/media/webrtc/third_party_build/run_operations.py b/dom/media/webrtc/third_party_build/run_operations.py
new file mode 100644
index 0000000000..794ea268d8
--- /dev/null
+++ b/dom/media/webrtc/third_party_build/run_operations.py
@@ -0,0 +1,78 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+import os
+import subprocess
+import sys
+
+# This is a collection of helper functions that use the subprocess.run
+# command to execute commands. In the future, if we have cases where
+# we find common python functionality in utilizing the data returned
+# from these functions, that functionality can live here for easy reuse
+# between other python scripts in dom/media/webrtc/third_party_build.
+
+
+# must run 'hg' with HGPLAIN=1 to ensure aliases don't interfere with
+# command output.
+env = os.environ.copy()
+env["HGPLAIN"] = "1"
+
+
+def run_hg(cmd):
+ cmd_list = cmd.split(" ")
+ res = subprocess.run(
+ cmd_list,
+ capture_output=True,
+ text=True,
+ env=env,
+ )
+ if res.returncode != 0:
+ print(
+ "Hit return code {} running '{}'. Aborting.".format(res.returncode, cmd),
+ file=sys.stderr,
+ )
+ print(res.stderr)
+ sys.exit(1)
+ stdout = res.stdout.strip()
+ return [] if len(stdout) == 0 else stdout.split("\n")
+
+
+def run_git(cmd, working_dir):
+ cmd_list = cmd.split(" ")
+ res = subprocess.run(
+ cmd_list,
+ capture_output=True,
+ text=True,
+ cwd=working_dir,
+ )
+ if res.returncode != 0:
+ print(
+ "Hit return code {} running '{}'. Aborting.".format(res.returncode, cmd),
+ file=sys.stderr,
+ )
+ print(res.stderr)
+ sys.exit(1)
+ stdout = res.stdout.strip()
+ return [] if len(stdout) == 0 else stdout.split("\n")
+
+
+def run_shell(cmd, capture_output=True):
+ res = subprocess.run(
+ cmd,
+ shell=True,
+ capture_output=capture_output,
+ text=True,
+ )
+ if res.returncode != 0:
+ print(
+ "Hit return code {} running '{}'. Aborting.".format(res.returncode, cmd),
+ file=sys.stderr,
+ )
+ print(res.stderr)
+ sys.exit(1)
+ output_lines = []
+ if capture_output:
+ stdout = res.stdout.strip()
+ output_lines = [] if len(stdout) == 0 else stdout.split("\n")
+
+ return output_lines
diff --git a/dom/media/webrtc/third_party_build/save_patch_stack.py b/dom/media/webrtc/third_party_build/save_patch_stack.py
new file mode 100644
index 0000000000..11c007109c
--- /dev/null
+++ b/dom/media/webrtc/third_party_build/save_patch_stack.py
@@ -0,0 +1,142 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+import argparse
+import os
+import re
+import shutil
+
+from run_operations import run_git, run_hg, run_shell
+
+# This script saves the mozilla patch stack and no-op commit tracking
+# files. This makes our fast-forward process much more resilient by
+# saving the intermediate state after each upstream commit is processed.
+
+
+def save_patch_stack(
+ github_path,
+ github_branch,
+ patch_directory,
+ state_directory,
+ target_branch_head,
+ bug_number,
+):
+ # remove the current patch files
+ files_to_remove = os.listdir(patch_directory)
+ for file in files_to_remove:
+ os.remove(os.path.join(patch_directory, file))
+
+ # find the base of the patch stack
+ cmd = "git merge-base {} {}".format(github_branch, target_branch_head)
+ stdout_lines = run_git(cmd, github_path)
+ merge_base = stdout_lines[0]
+
+ # grab patch stack
+ cmd = "git format-patch --keep-subject --output-directory {} {}..{}".format(
+ patch_directory, merge_base, github_branch
+ )
+ run_git(cmd, github_path)
+
+ # remove the commit summary from the file name
+ patches_to_rename = os.listdir(patch_directory)
+ for file in patches_to_rename:
+ shortened_name = re.sub("^(\d\d\d\d)-.*\.patch", "\\1.patch", file)
+ os.rename(
+ os.path.join(patch_directory, file),
+ os.path.join(patch_directory, shortened_name),
+ )
+
+ # remove the unhelpful first line of the patch files that only
+ # causes diff churn. For reasons why we can't skip creating backup
+ # files during the in-place editing, see:
+ # https://stackoverflow.com/questions/5694228/sed-in-place-flag-that-works-both-on-mac-bsd-and-linux
+ run_shell("sed -i'.bak' -e '1d' {}/*.patch".format(patch_directory))
+ run_shell("rm {}/*.patch.bak".format(patch_directory))
+
+ # it is also helpful to save the no-op-cherry-pick-msg files from
+ # the state directory so that if we're restoring a patch-stack we
+ # also restore the possibly consumed no-op tracking files.
+ no_op_files = [
+ path
+ for path in os.listdir(state_directory)
+ if re.findall(".*no-op-cherry-pick-msg$", path)
+ ]
+ for file in no_op_files:
+ shutil.copy(os.path.join(state_directory, file), patch_directory)
+
+ # get missing files (that should be marked removed)
+ cmd = "hg status --no-status --deleted {}".format(patch_directory)
+ stdout_lines = run_hg(cmd)
+ if len(stdout_lines) != 0:
+ cmd = "hg rm {}".format(" ".join(stdout_lines))
+ run_hg(cmd)
+
+ # get unknown files (that should be marked added)
+ cmd = "hg status --no-status --unknown {}".format(patch_directory)
+ stdout_lines = run_hg(cmd)
+ if len(stdout_lines) != 0:
+ cmd = "hg add {}".format(" ".join(stdout_lines))
+ run_hg(cmd)
+
+ # if any files are marked for add/remove/modify, commit them
+ cmd = "hg status --added --removed --modified {}".format(patch_directory)
+ stdout_lines = run_hg(cmd)
+ if (len(stdout_lines)) != 0:
+ print("Updating {} files in {}".format(len(stdout_lines), patch_directory))
+ if bug_number is None:
+ run_hg("hg amend")
+ else:
+ run_shell(
+ "hg commit --message 'Bug {} - updated libwebrtc patch stack'".format(
+ bug_number
+ )
+ )
+
+
+if __name__ == "__main__":
+ default_patch_dir = "third_party/libwebrtc/moz-patch-stack"
+ default_state_dir = ".moz-fast-forward"
+
+ parser = argparse.ArgumentParser(
+ description="Save moz-libwebrtc github patch stack"
+ )
+ parser.add_argument(
+ "--repo-path",
+ required=True,
+ help="path to libwebrtc repo",
+ )
+ parser.add_argument(
+ "--branch",
+ default="mozpatches",
+ help="moz-libwebrtc branch (defaults to mozpatches)",
+ )
+ parser.add_argument(
+ "--patch-path",
+ default=default_patch_dir,
+ help="path to save patches (defaults to {})".format(default_patch_dir),
+ )
+ parser.add_argument(
+ "--state-path",
+ default=default_state_dir,
+ help="path to state directory (defaults to {})".format(default_state_dir),
+ )
+ parser.add_argument(
+ "--target-branch-head",
+ required=True,
+ help="target branch head for fast-forward, should match MOZ_TARGET_UPSTREAM_BRANCH_HEAD in config_env",
+ )
+ parser.add_argument(
+ "--separate-commit-bug-number",
+ type=int,
+ help="integer Bugzilla number (example: 1800920), if provided will write patch stack as separate commit",
+ )
+ args = parser.parse_args()
+
+ save_patch_stack(
+ args.repo_path,
+ args.branch,
+ os.path.abspath(args.patch_path),
+ args.state_path,
+ args.target_branch_head,
+ args.separate_commit_bug_number,
+ )
diff --git a/dom/media/webrtc/third_party_build/update_default_config.sh b/dom/media/webrtc/third_party_build/update_default_config.sh
new file mode 100644
index 0000000000..f4020f9b45
--- /dev/null
+++ b/dom/media/webrtc/third_party_build/update_default_config.sh
@@ -0,0 +1,46 @@
+#!/bin/bash
+
+function show_error_msg()
+{
+ echo "*** ERROR *** $? line $1 $0 did not complete successfully!"
+ echo "$ERROR_HELP"
+}
+ERROR_HELP=""
+
+# Print an Error message if `set -eE` causes the script to exit due to a failed command
+trap 'show_error_msg $LINENO' ERR
+
+if [ "x" = "x$NEW_BUG_NUMBER" ]; then
+ echo "NEW_BUG_NUMBER is not defined. You should probably have a new bug"
+ echo "number defined for the next fast-forward update. Then do:"
+ echo " NEW_BUG_NUMBER={new-bug-number} bash $0"
+ exit
+fi
+
+source dom/media/webrtc/third_party_build/use_config_env.sh
+
+if [ "x$MOZ_NEXT_LIBWEBRTC_MILESTONE" = "x" ]; then
+ echo "MOZ_NEXT_LIBWEBRTC_MILESTONE is not defined, see README.md"
+ exit
+fi
+
+if [ "x$MOZ_NEXT_FIREFOX_REL_TARGET" = "x" ]; then
+ echo "MOZ_NEXT_FIREFOX_REL_TARGET is not defined, see README.md"
+ exit
+fi
+
+
+# After this point:
+# * eE: All commands should succeed.
+# * u: All variables should be defined before use.
+# * o pipefail: All stages of all pipes should succeed.
+set -eEuo pipefail
+
+ERROR_HELP=$"
+An error has occurred running $SCRIPT_DIR/write_default_config.py
+"
+./mach python $SCRIPT_DIR/write_default_config.py \
+ --bug-number $NEW_BUG_NUMBER \
+ --milestone $MOZ_NEXT_LIBWEBRTC_MILESTONE \
+ --release-target $MOZ_NEXT_FIREFOX_REL_TARGET \
+ > $SCRIPT_DIR/default_config_env
diff --git a/dom/media/webrtc/third_party_build/use_config_env.sh b/dom/media/webrtc/third_party_build/use_config_env.sh
new file mode 100644
index 0000000000..bd78dc7e0e
--- /dev/null
+++ b/dom/media/webrtc/third_party_build/use_config_env.sh
@@ -0,0 +1,89 @@
+#!/bin/bash
+
+# Assume that if STATE_DIR is already defined, we do not need to
+# execute this file again.
+if [ "x$STATE_DIR" != "x" ]; then
+ # no need to run script since we've already run
+ return
+fi
+
+export SCRIPT_DIR="dom/media/webrtc/third_party_build"
+# first, make sure we're running from the top of moz-central repo
+if [ ! -d $SCRIPT_DIR ]; then
+ echo "Error: unable to find directory $SCRIPT_DIR"
+ exit 1
+fi
+
+# Should we tie the location of the STATE_DIR to the path
+# in MOZ_CONFIG_PATH? Probably.
+export STATE_DIR=`pwd`/.moz-fast-forward
+export LOG_DIR=$STATE_DIR/logs
+export TMP_DIR=$STATE_DIR/tmp
+
+if [ ! -d $STATE_DIR ]; then
+ echo "Creating missing $STATE_DIR"
+ mkdir -p $STATE_DIR
+ if [ ! -d $STATE_DIR ]; then
+ echo "error: unable to find (or create) $STATE_DIR"
+ exit 1
+ fi
+fi
+echo "Using STATE_DIR=$STATE_DIR"
+
+if [ ! -d $LOG_DIR ]; then
+ echo "Creating missing $LOG_DIR"
+ mkdir -p $LOG_DIR
+ if [ ! -d $LOG_DIR ]; then
+ echo "error: unable to find (or create) $LOG_DIR"
+ exit 1
+ fi
+fi
+echo "Using LOG_DIR=$LOG_DIR"
+
+if [ ! -d $TMP_DIR ]; then
+ echo "Creating missing $TMP_DIR"
+ mkdir -p $TMP_DIR
+ if [ ! -d $TMP_DIR ]; then
+ echo "error: unable to find (or create) $TMP_DIR"
+ exit 1
+ fi
+fi
+echo "Using TMP_DIR=$TMP_DIR"
+
+# Allow user to override default path to config_env
+if [ "x$MOZ_CONFIG_PATH" = "x" ]; then
+ MOZ_CONFIG_PATH=$STATE_DIR/config_env
+ echo "Using default MOZ_CONFIG_PATH=$MOZ_CONFIG_PATH"
+fi
+
+if [ ! -f $MOZ_CONFIG_PATH ]; then
+ echo "Creating default config file at $MOZ_CONFIG_PATH"
+ cp $SCRIPT_DIR/default_config_env $MOZ_CONFIG_PATH
+fi
+source $MOZ_CONFIG_PATH
+
+
+function find_base_commit()
+{
+ # read the last line of README.moz-ff-commit to retrieve our current base
+ # commit in moz-libwebrtc
+ MOZ_LIBWEBRTC_BASE=`tail -1 third_party/libwebrtc/README.moz-ff-commit`
+ echo "prelim MOZ_LIBWEBRTC_BASE: $MOZ_LIBWEBRTC_BASE"
+ # if we've advanced into a chrome release branch, we need to adjust the
+ # MOZ_LIBWEBRTC_BASE to the last common commit so we can now advance up
+ # the trunk commits.
+ MOZ_LIBWEBRTC_BASE=`cd $MOZ_LIBWEBRTC_SRC ; git merge-base $MOZ_LIBWEBRTC_BASE $MOZ_TARGET_UPSTREAM_BRANCH_HEAD`
+ # now make it a short hash
+ MOZ_LIBWEBRTC_BASE=`cd $MOZ_LIBWEBRTC_SRC ; git rev-parse --short $MOZ_LIBWEBRTC_BASE`
+ echo "adjusted MOZ_LIBWEBRTC_BASE: $MOZ_LIBWEBRTC_BASE"
+}
+export -f find_base_commit
+
+function find_next_commit()
+{
+ # identify the next commit above our current base commit
+ MOZ_LIBWEBRTC_NEXT_BASE=`cd $MOZ_LIBWEBRTC_SRC ; \
+ git log --oneline --ancestry-path $MOZ_LIBWEBRTC_BASE^..$MOZ_TARGET_UPSTREAM_BRANCH_HEAD \
+ | tail -2 | head -1 | awk '{print $1;}'`
+}
+export -f find_next_commit
diff --git a/dom/media/webrtc/third_party_build/vendor-libwebrtc.py b/dom/media/webrtc/third_party_build/vendor-libwebrtc.py
new file mode 100644
index 0000000000..99723849c7
--- /dev/null
+++ b/dom/media/webrtc/third_party_build/vendor-libwebrtc.py
@@ -0,0 +1,419 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+import argparse
+import datetime
+import os
+import shutil
+import stat
+import subprocess
+import sys
+import tarfile
+
+import requests
+
+THIRDPARTY_USED_IN_FIREFOX = [
+ "abseil-cpp",
+ "google_benchmark",
+ "pffft",
+ "rnnoise",
+]
+
+LIBWEBRTC_DIR = os.path.normpath("third_party/libwebrtc")
+
+
+def get_excluded_paths():
+ return [
+ ".clang-format",
+ ".git-blame-ignore-revs",
+ ".gitignore",
+ ".vpython",
+ "CODE_OF_CONDUCT.md",
+ "ENG_REVIEW_OWNERS",
+ "PRESUBMIT.py",
+ "README.chromium",
+ "WATCHLISTS",
+ "codereview.settings",
+ "license_template.txt",
+ "native-api.md",
+ "presubmit_test.py",
+ "presubmit_test_mocks.py",
+ "pylintrc",
+ # Only the camera code under sdk/android/api/org/webrtc is used, so
+ # we remove sdk/android and add back the specific files we want.
+ "sdk/android",
+ ]
+
+
+# Paths in this list are included even if their parent directory is
+# excluded in get_excluded_paths()
+def get_included_path_overrides():
+ return [
+ "sdk/android/src/java/org/webrtc/NativeLibrary.java",
+ "sdk/android/src/java/org/webrtc/FramerateBitrateAdjuster.java",
+ "sdk/android/src/java/org/webrtc/MediaCodecVideoDecoderFactory.java",
+ "sdk/android/src/java/org/webrtc/BitrateAdjuster.java",
+ "sdk/android/src/java/org/webrtc/MediaCodecWrapperFactory.java",
+ "sdk/android/src/java/org/webrtc/WebRtcClassLoader.java",
+ "sdk/android/src/java/org/webrtc/audio/WebRtcAudioRecord.java",
+ "sdk/android/src/java/org/webrtc/audio/WebRtcAudioTrack.java",
+ "sdk/android/src/java/org/webrtc/audio/WebRtcAudioManager.java",
+ "sdk/android/src/java/org/webrtc/audio/LowLatencyAudioBufferManager.java",
+ "sdk/android/src/java/org/webrtc/audio/WebRtcAudioUtils.java",
+ "sdk/android/src/java/org/webrtc/audio/WebRtcAudioEffects.java",
+ "sdk/android/src/java/org/webrtc/audio/VolumeLogger.java",
+ "sdk/android/src/java/org/webrtc/NativeCapturerObserver.java",
+ "sdk/android/src/java/org/webrtc/MediaCodecWrapper.java",
+ "sdk/android/src/java/org/webrtc/CalledByNative.java",
+ "sdk/android/src/java/org/webrtc/Histogram.java",
+ "sdk/android/src/java/org/webrtc/EglBase10Impl.java",
+ "sdk/android/src/java/org/webrtc/EglBase14Impl.java",
+ "sdk/android/src/java/org/webrtc/MediaCodecWrapperFactoryImpl.java",
+ "sdk/android/src/java/org/webrtc/AndroidVideoDecoder.java",
+ "sdk/android/src/java/org/webrtc/BaseBitrateAdjuster.java",
+ "sdk/android/src/java/org/webrtc/HardwareVideoEncoder.java",
+ "sdk/android/src/java/org/webrtc/VideoCodecMimeType.java",
+ "sdk/android/src/java/org/webrtc/NativeAndroidVideoTrackSource.java",
+ "sdk/android/src/java/org/webrtc/VideoDecoderWrapper.java",
+ "sdk/android/src/java/org/webrtc/JNILogging.java",
+ "sdk/android/src/java/org/webrtc/CameraCapturer.java",
+ "sdk/android/src/java/org/webrtc/CameraSession.java",
+ "sdk/android/src/java/org/webrtc/H264Utils.java",
+ "sdk/android/src/java/org/webrtc/Empty.java",
+ "sdk/android/src/java/org/webrtc/DynamicBitrateAdjuster.java",
+ "sdk/android/src/java/org/webrtc/Camera1Session.java",
+ "sdk/android/src/java/org/webrtc/JniCommon.java",
+ "sdk/android/src/java/org/webrtc/NV12Buffer.java",
+ "sdk/android/src/java/org/webrtc/WrappedNativeI420Buffer.java",
+ "sdk/android/src/java/org/webrtc/GlGenericDrawer.java",
+ "sdk/android/src/java/org/webrtc/RefCountDelegate.java",
+ "sdk/android/src/java/org/webrtc/Camera2Session.java",
+ "sdk/android/src/java/org/webrtc/MediaCodecUtils.java",
+ "sdk/android/src/java/org/webrtc/CalledByNativeUnchecked.java",
+ "sdk/android/src/java/org/webrtc/VideoEncoderWrapper.java",
+ "sdk/android/src/java/org/webrtc/NV21Buffer.java",
+ "sdk/android/api/org/webrtc/RendererCommon.java",
+ "sdk/android/api/org/webrtc/YuvHelper.java",
+ "sdk/android/api/org/webrtc/LibvpxVp9Encoder.java",
+ "sdk/android/api/org/webrtc/Metrics.java",
+ "sdk/android/api/org/webrtc/CryptoOptions.java",
+ "sdk/android/api/org/webrtc/MediaConstraints.java",
+ "sdk/android/api/org/webrtc/YuvConverter.java",
+ "sdk/android/api/org/webrtc/JavaI420Buffer.java",
+ "sdk/android/api/org/webrtc/VideoDecoder.java",
+ "sdk/android/api/org/webrtc/WrappedNativeVideoDecoder.java",
+ "sdk/android/api/org/webrtc/Camera2Enumerator.java",
+ "sdk/android/api/org/webrtc/SurfaceTextureHelper.java",
+ "sdk/android/api/org/webrtc/EglBase10.java",
+ "sdk/android/api/org/webrtc/DataChannel.java",
+ "sdk/android/api/org/webrtc/audio/JavaAudioDeviceModule.java",
+ "sdk/android/api/org/webrtc/audio/AudioDeviceModule.java",
+ "sdk/android/api/org/webrtc/audio/LegacyAudioDeviceModule.java",
+ "sdk/android/api/org/webrtc/SessionDescription.java",
+ "sdk/android/api/org/webrtc/GlUtil.java",
+ "sdk/android/api/org/webrtc/VideoSource.java",
+ "sdk/android/api/org/webrtc/AudioTrack.java",
+ "sdk/android/api/org/webrtc/EglRenderer.java",
+ "sdk/android/api/org/webrtc/VideoEncoder.java",
+ "sdk/android/api/org/webrtc/VideoCapturer.java",
+ "sdk/android/api/org/webrtc/SoftwareVideoDecoderFactory.java",
+ "sdk/android/api/org/webrtc/AudioSource.java",
+ "sdk/android/api/org/webrtc/GlRectDrawer.java",
+ "sdk/android/api/org/webrtc/StatsReport.java",
+ "sdk/android/api/org/webrtc/CameraVideoCapturer.java",
+ "sdk/android/api/org/webrtc/NetEqFactoryFactory.java",
+ "sdk/android/api/org/webrtc/AudioProcessingFactory.java",
+ "sdk/android/api/org/webrtc/Camera2Capturer.java",
+ "sdk/android/api/org/webrtc/ScreenCapturerAndroid.java",
+ "sdk/android/api/org/webrtc/RefCounted.java",
+ "sdk/android/api/org/webrtc/VideoEncoderFallback.java",
+ "sdk/android/api/org/webrtc/AudioEncoderFactoryFactory.java",
+ "sdk/android/api/org/webrtc/EglBase14.java",
+ "sdk/android/api/org/webrtc/SoftwareVideoEncoderFactory.java",
+ "sdk/android/api/org/webrtc/VideoEncoderFactory.java",
+ "sdk/android/api/org/webrtc/StatsObserver.java",
+ "sdk/android/api/org/webrtc/PlatformSoftwareVideoDecoderFactory.java",
+ "sdk/android/api/org/webrtc/Camera1Capturer.java",
+ "sdk/android/api/org/webrtc/AddIceObserver.java",
+ "sdk/android/api/org/webrtc/SurfaceViewRenderer.java",
+ "sdk/android/api/org/webrtc/CameraEnumerator.java",
+ "sdk/android/api/org/webrtc/CameraEnumerationAndroid.java",
+ "sdk/android/api/org/webrtc/VideoDecoderFallback.java",
+ "sdk/android/api/org/webrtc/FileVideoCapturer.java",
+ "sdk/android/api/org/webrtc/NativeLibraryLoader.java",
+ "sdk/android/api/org/webrtc/Camera1Enumerator.java",
+ "sdk/android/api/org/webrtc/NativePeerConnectionFactory.java",
+ "sdk/android/api/org/webrtc/LibaomAv1Encoder.java",
+ "sdk/android/api/org/webrtc/BuiltinAudioEncoderFactoryFactory.java",
+ "sdk/android/api/org/webrtc/AudioDecoderFactoryFactory.java",
+ "sdk/android/api/org/webrtc/FecControllerFactoryFactoryInterface.java",
+ "sdk/android/api/org/webrtc/VideoFrameBufferType.java",
+ "sdk/android/api/org/webrtc/SdpObserver.java",
+ "sdk/android/api/org/webrtc/Predicate.java",
+ "sdk/android/api/org/webrtc/VideoFileRenderer.java",
+ "sdk/android/api/org/webrtc/WrappedNativeVideoEncoder.java",
+ "sdk/android/api/org/webrtc/LibvpxVp8Encoder.java",
+ "sdk/android/api/org/webrtc/DtmfSender.java",
+ "sdk/android/api/org/webrtc/VideoTrack.java",
+ "sdk/android/api/org/webrtc/LibvpxVp8Decoder.java",
+ "sdk/android/api/org/webrtc/GlShader.java",
+ "sdk/android/api/org/webrtc/FrameEncryptor.java",
+ "sdk/android/api/org/webrtc/EglBase.java",
+ "sdk/android/api/org/webrtc/VideoProcessor.java",
+ "sdk/android/api/org/webrtc/SSLCertificateVerifier.java",
+ "sdk/android/api/org/webrtc/VideoSink.java",
+ "sdk/android/api/org/webrtc/MediaSource.java",
+ "sdk/android/api/org/webrtc/DefaultVideoDecoderFactory.java",
+ "sdk/android/api/org/webrtc/VideoCodecInfo.java",
+ "sdk/android/api/org/webrtc/FrameDecryptor.java",
+ "sdk/android/api/org/webrtc/VideoDecoderFactory.java",
+ "sdk/android/api/org/webrtc/TextureBufferImpl.java",
+ "sdk/android/api/org/webrtc/VideoFrame.java",
+ "sdk/android/api/org/webrtc/IceCandidateErrorEvent.java",
+ "sdk/android/api/org/webrtc/CapturerObserver.java",
+ "sdk/android/api/org/webrtc/MediaStreamTrack.java",
+ "sdk/android/api/org/webrtc/GlTextureFrameBuffer.java",
+ "sdk/android/api/org/webrtc/TurnCustomizer.java",
+ "sdk/android/api/org/webrtc/TimestampAligner.java",
+ "sdk/android/api/org/webrtc/BuiltinAudioDecoderFactoryFactory.java",
+ "sdk/android/api/org/webrtc/LibvpxVp9Decoder.java",
+ "sdk/android/api/org/webrtc/SurfaceEglRenderer.java",
+ "sdk/android/api/org/webrtc/HardwareVideoDecoderFactory.java",
+ "sdk/android/api/org/webrtc/VideoCodecStatus.java",
+ "sdk/android/api/org/webrtc/Dav1dDecoder.java",
+ "sdk/android/api/org/webrtc/VideoFrameDrawer.java",
+ "sdk/android/api/org/webrtc/CallSessionFileRotatingLogSink.java",
+ "sdk/android/api/org/webrtc/EncodedImage.java",
+ ]
+
+
+def make_github_url(repo, commit):
+ if not repo.endswith("/"):
+ repo += "/"
+ return repo + "archive/" + commit + ".tar.gz"
+
+
+def make_googlesource_url(target, commit):
+ if target == "libwebrtc":
+ return "https://webrtc.googlesource.com/src.git/+archive/" + commit + ".tar.gz"
+ elif target == "build":
+ return (
+ "https://chromium.googlesource.com/chromium/src/build/+archive/"
+ + commit
+ + ".tar.gz"
+ )
+ elif target == "third_party":
+ return (
+ "https://chromium.googlesource.com/chromium/src/third_party/+archive/"
+ + commit
+ + ".tar.gz"
+ )
+
+
+def fetch(target, url):
+ print("Fetching commit from {}".format(url))
+ req = requests.get(url)
+ if req.status_code == 200:
+ with open(target + ".tar.gz", "wb") as f:
+ f.write(req.content)
+ else:
+ print(
+ "Hit status code {} fetching commit. Aborting.".format(req.status_code),
+ file=sys.stderr,
+ )
+ sys.exit(1)
+ with open(os.path.join(LIBWEBRTC_DIR, "README.mozilla"), "a") as f:
+ # write the the command line used
+ f.write("# ./mach python {}\n".format(" ".join(sys.argv[0:])))
+ f.write(
+ "{} updated from commit {} on {}.\n".format(
+ target, url, datetime.datetime.utcnow().isoformat()
+ )
+ )
+
+
+def fetch_local(target, path, commit):
+ target_archive = target + ".tar.gz"
+ cp = subprocess.run(["git", "archive", "-o", target_archive, commit], cwd=path)
+ if cp.returncode != 0:
+ print(
+ "Hit return code {} fetching commit. Aborting.".format(cp.returncode),
+ file=sys.stderr,
+ )
+ sys.exit(1)
+
+ with open(os.path.join(LIBWEBRTC_DIR, "README.mozilla"), "a") as f:
+ # write the the command line used
+ f.write("# ./mach python {}\n".format(" ".join(sys.argv[0:])))
+ f.write(
+ "{} updated from {} commit {} on {}.\n".format(
+ target, path, commit, datetime.datetime.utcnow().isoformat()
+ )
+ )
+ shutil.move(os.path.join(path, target_archive), target_archive)
+
+
+def validate_tar_member(member, path):
+ def _is_within_directory(directory, target):
+ real_directory = os.path.realpath(directory)
+ real_target = os.path.realpath(target)
+ prefix = os.path.commonprefix([real_directory, real_target])
+ return prefix == real_directory
+
+ member_path = os.path.join(path, member.name)
+ if not _is_within_directory(path, member_path):
+ raise Exception("Attempted path traversal in tar file: " + member.name)
+ if member.issym():
+ link_path = os.path.join(os.path.dirname(member_path), member.linkname)
+ if not _is_within_directory(path, link_path):
+ raise Exception("Attempted link path traversal in tar file: " + member.name)
+ if member.mode & (stat.S_ISUID | stat.S_ISGID):
+ raise Exception("Attempted setuid or setgid in tar file: " + member.name)
+
+
+def safe_extract(tar, path=".", *, numeric_owner=False):
+ def _files(tar, path):
+ for member in tar:
+ validate_tar_member(member, path)
+ yield member
+
+ tar.extractall(path, members=_files(tar, path), numeric_owner=numeric_owner)
+
+
+def unpack(target):
+ target_archive = target + ".tar.gz"
+ target_path = "tmp-" + target
+ try:
+ shutil.rmtree(target_path)
+ except FileNotFoundError:
+ pass
+ with tarfile.open(target_archive) as t:
+ safe_extract(t, path=target_path)
+
+ if target == "libwebrtc":
+ # use the top level directories from the tarfile and
+ # delete those directories in LIBWEBRTC_DIR
+ libwebrtc_used_in_firefox = os.listdir(target_path)
+ for path in libwebrtc_used_in_firefox:
+ try:
+ shutil.rmtree(os.path.join(LIBWEBRTC_DIR, path))
+ except FileNotFoundError:
+ pass
+ except NotADirectoryError:
+ pass
+
+ unused_libwebrtc_in_firefox = get_excluded_paths()
+ forced_used_in_firefox = get_included_path_overrides()
+
+ # adjust target_path if GitHub packaging is involved
+ if not os.path.exists(os.path.join(target_path, libwebrtc_used_in_firefox[0])):
+ # GitHub packs everything inside a separate directory
+ target_path = os.path.join(target_path, os.listdir(target_path)[0])
+
+ # remove any entries found in unused_libwebrtc_in_firefox from the
+ # tarfile
+ for path in unused_libwebrtc_in_firefox:
+ if os.path.isdir(os.path.join(target_path, path)):
+ shutil.rmtree(os.path.join(target_path, path))
+ else:
+ os.remove(os.path.join(target_path, path))
+
+ # move remaining top level entries from the tarfile to LIBWEBRTC_DIR
+ for path in os.listdir(target_path):
+ shutil.move(
+ os.path.join(target_path, path), os.path.join(LIBWEBRTC_DIR, path)
+ )
+
+ # An easy, but inefficient way to accomplish including specific
+ # files from directories otherwise removed. Re-extract the tar
+ # file, and only copy over the exact files requested.
+ shutil.rmtree(target_path)
+ with tarfile.open(target_archive) as t:
+ safe_extract(t, path=target_path)
+
+ # Copy the force included files. Note: the instinctual action
+ # is to do this prior to removing the excluded paths to avoid
+ # reextracting the tar file. However, this causes errors due to
+ # pre-existing paths when other directories are moved out of the
+ # tar file in the "move all the top level entries from the
+ # tarfile" phase above.
+ for path in forced_used_in_firefox:
+ dest_path = os.path.join(LIBWEBRTC_DIR, path)
+ dir_path = os.path.dirname(dest_path)
+ if not os.path.exists(dir_path):
+ os.makedirs(dir_path)
+ shutil.move(os.path.join(target_path, path), dest_path)
+ elif target == "build":
+ try:
+ shutil.rmtree(os.path.join(LIBWEBRTC_DIR, "build"))
+ except FileNotFoundError:
+ pass
+ os.makedirs(os.path.join(LIBWEBRTC_DIR, "build"))
+
+ if os.path.exists(os.path.join(target_path, "linux")):
+ for path in os.listdir(target_path):
+ shutil.move(
+ os.path.join(target_path, path),
+ os.path.join(LIBWEBRTC_DIR, "build", path),
+ )
+ else:
+ # GitHub packs everything inside a separate directory
+ target_path = os.path.join(target_path, os.listdir(target_path)[0])
+ for path in os.listdir(target_path):
+ shutil.move(
+ os.path.join(target_path, path),
+ os.path.join(LIBWEBRTC_DIR, "build", path),
+ )
+ elif target == "third_party":
+ try:
+ shutil.rmtree(os.path.join(LIBWEBRTC_DIR, "third_party"))
+ except FileNotFoundError:
+ pass
+ except NotADirectoryError:
+ pass
+
+ if os.path.exists(os.path.join(target_path, THIRDPARTY_USED_IN_FIREFOX[0])):
+ for path in THIRDPARTY_USED_IN_FIREFOX:
+ shutil.move(
+ os.path.join(target_path, path),
+ os.path.join(LIBWEBRTC_DIR, "third_party", path),
+ )
+ else:
+ # GitHub packs everything inside a separate directory
+ target_path = os.path.join(target_path, os.listdir(target_path)[0])
+ for path in THIRDPARTY_USED_IN_FIREFOX:
+ shutil.move(
+ os.path.join(target_path, path),
+ os.path.join(LIBWEBRTC_DIR, "third_party", path),
+ )
+
+
+def cleanup(target):
+ os.remove(target + ".tar.gz")
+ shutil.rmtree("tmp-" + target)
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(description="Update libwebrtc")
+ parser.add_argument("target", choices=("libwebrtc", "build", "third_party"))
+ group = parser.add_mutually_exclusive_group(required=True)
+ group.add_argument("--from-github", type=str)
+ group.add_argument("--from-googlesource", action="store_true", default=False)
+ group.add_argument("--from-local", type=str)
+ parser.add_argument("--commit", type=str, default="master")
+ parser.add_argument("--skip-fetch", action="store_true", default=False)
+ parser.add_argument("--skip-cleanup", action="store_true", default=False)
+ args = parser.parse_args()
+
+ os.makedirs(LIBWEBRTC_DIR, exist_ok=True)
+
+ if not args.skip_fetch:
+ if args.from_github:
+ fetch(args.target, make_github_url(args.from_github, args.commit))
+ elif args.from_googlesource:
+ fetch(args.target, make_googlesource_url(args.target, args.commit))
+ elif args.from_local:
+ fetch_local(args.target, args.from_local, args.commit)
+ unpack(args.target)
+ if not args.skip_cleanup:
+ cleanup(args.target)
diff --git a/dom/media/webrtc/third_party_build/verify_vendoring.sh b/dom/media/webrtc/third_party_build/verify_vendoring.sh
new file mode 100644
index 0000000000..3d2c161075
--- /dev/null
+++ b/dom/media/webrtc/third_party_build/verify_vendoring.sh
@@ -0,0 +1,55 @@
+#!/bin/bash
+
+function show_error_msg()
+{
+ echo "*** ERROR *** $? line $1 $0 did not complete successfully!"
+ echo "$ERROR_HELP"
+}
+ERROR_HELP=""
+
+# Print an Error message if `set -eE` causes the script to exit due to a failed command
+trap 'show_error_msg $LINENO' ERR
+
+source dom/media/webrtc/third_party_build/use_config_env.sh
+
+echo "MOZ_LIBWEBRTC_SRC: $MOZ_LIBWEBRTC_SRC"
+echo "MOZ_LIBWEBRTC_BRANCH: $MOZ_LIBWEBRTC_BRANCH"
+echo "MOZ_FASTFORWARD_BUG: $MOZ_FASTFORWARD_BUG"
+
+# After this point:
+# * eE: All commands should succeed.
+# * u: All variables should be defined before use.
+# * o pipefail: All stages of all pipes should succeed.
+set -eEuo pipefail
+
+./mach python $SCRIPT_DIR/vendor-libwebrtc.py \
+ --from-local $MOZ_LIBWEBRTC_SRC \
+ --commit $MOZ_LIBWEBRTC_BRANCH \
+ libwebrtc
+
+hg revert -q \
+ --include "third_party/libwebrtc/**moz.build" \
+ --include "third_party/libwebrtc/README.mozilla" \
+ third_party/libwebrtc
+
+FILE_CHANGE_CNT=`hg status third_party/libwebrtc | wc -l | tr -d " "`
+if [ "x$FILE_CHANGE_CNT" != "x0" ]; then
+ echo "***"
+ echo "There are changes after vendoring - running extract-for-git.py"
+ echo "is recommended. First, find the mercurial commit after the"
+ echo "previous fast-forward landing. The commands you want will look"
+ echo "something like:"
+ echo " ./mach python $SCRIPT_DIR/extract-for-git.py {after-ff-commit}::{tip-of-central}"
+ echo " mv mailbox.patch $MOZ_LIBWEBRTC_SRC"
+ echo " (cd $MOZ_LIBWEBRTC_SRC && \\"
+ echo " git am mailbox.patch)"
+ echo ""
+ echo "After adding the new changes from moz-central to the moz-libwebrtc"
+ echo "patch stack, you may re-run this command to verify vendoring:"
+ echo " bash $0"
+
+ exit 1
+fi
+
+
+echo "Done - vendoring has been verified."
diff --git a/dom/media/webrtc/third_party_build/webrtc.mozbuild b/dom/media/webrtc/third_party_build/webrtc.mozbuild
new file mode 100644
index 0000000000..30169c36c2
--- /dev/null
+++ b/dom/media/webrtc/third_party_build/webrtc.mozbuild
@@ -0,0 +1,40 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# This file mimics the defines used by any headers in libwebrtc that get
+# included in Mozilla code.
+# We currently audit these by hand. Searching for the defines upstream is a good
+# start:
+# https://source.chromium.org/search?q=usage:%23if.*def.*%5B%5E_%5D$%20AND%20path:%5C.h%20AND%20path:%5Ethird_party%2Fwebrtc&ss=chromium
+
+if CONFIG['MOZ_WEBRTC']:
+ DEFINES['HAVE_UINT64_T'] = True
+ DEFINES['WEBRTC_MOZILLA_BUILD'] = True
+ DEFINES['RTC_ENABLE_VP9'] = True
+
+ if CONFIG['OS_TARGET'] != 'WINNT':
+ DEFINES['WEBRTC_POSIX'] = True
+ DEFINES['WEBRTC_BUILD_LIBEVENT'] = True
+
+ if CONFIG['OS_TARGET'] == 'Linux':
+ DEFINES['WEBRTC_LINUX'] = True
+ elif CONFIG['OS_TARGET'] == 'Darwin':
+ DEFINES['WEBRTC_MAC'] = True
+ elif CONFIG['OS_TARGET'] == 'WINNT':
+ DEFINES['WEBRTC_WIN'] = True
+ DEFINES['RTC_ENABLE_WIN_WGC'] = False
+ DEFINES['HAVE_WINSOCK2_H'] = True
+ elif CONFIG['OS_TARGET'] in ('DragonFly', 'FreeBSD', 'NetBSD', 'OpenBSD'):
+ DEFINES['WEBRTC_BSD'] = True
+ elif CONFIG['OS_TARGET'] == 'Android':
+ DEFINES['WEBRTC_LINUX'] = True
+ DEFINES['WEBRTC_ANDROID'] = True
+
+ if CONFIG['MOZ_X11']:
+ DEFINES['WEBRTC_USE_X11'] = True
+
+ if CONFIG["MOZ_WIDGET_TOOLKIT"] == "gtk":
+ DEFINES['WEBRTC_USE_PIPEWIRE'] = True
diff --git a/dom/media/webrtc/third_party_build/write_default_config.py b/dom/media/webrtc/third_party_build/write_default_config.py
new file mode 100644
index 0000000000..204789e052
--- /dev/null
+++ b/dom/media/webrtc/third_party_build/write_default_config.py
@@ -0,0 +1,104 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+import argparse
+import sys
+from string import Template
+
+sys.path.insert(0, "./dom/media/webrtc/third_party_build")
+import lookup_branch_head
+
+text = """#!/bin/bash
+
+# Edit {path-to} to match the location of your copy of Mozilla's
+# fork of libwebrtc (at https://github.com/mozilla/libwebrtc).
+export MOZ_LIBWEBRTC_SRC=$$STATE_DIR/moz-libwebrtc
+
+# Fast-forwarding each Chromium version of libwebrtc should be done
+# under a separate bugzilla bug. This bug number is used when crafting
+# the commit summary as each upstream commit is vendored into the
+# mercurial repository. The bug used for the v106 fast-forward was
+# 1800920.
+export MOZ_FASTFORWARD_BUG="$bugnum"
+
+# MOZ_NEXT_LIBWEBRTC_MILESTONE and MOZ_NEXT_FIREFOX_REL_TARGET are
+# not used during fast-forward processing, but facilitate generating this
+# default config. To generate an default config for the next update, run
+# bash dom/media/webrtc/third_party_build/update_default_config_env.sh
+export MOZ_NEXT_LIBWEBRTC_MILESTONE=$m2
+export MOZ_NEXT_FIREFOX_REL_TARGET=$t2
+
+# For Chromium release branches, see:
+# https://chromiumdash.appspot.com/branches
+
+# Chromium's v$m1 release branch was $bh1. This is used to pre-stack
+# the previous release branch's commits onto the appropriate base commit
+# (the first common commit between trunk and the release branch).
+export MOZ_PRIOR_UPSTREAM_BRANCH_HEAD_NUM="$bh1"
+
+# New target release branch for v$m2 is branch-heads/$bh2. This is used
+# to calculate the next upstream commit.
+export MOZ_TARGET_UPSTREAM_BRANCH_HEAD="branch-heads/$bh2"
+
+# For local development 'mozpatches' is fine for a branch name, but when
+# pushing the patch stack to github, it should be named something like
+# 'moz-mods-chr$m2-for-rel$t2'.
+export MOZ_LIBWEBRTC_BRANCH="mozpatches"
+
+# After elm has been merged to mozilla-central, the patch stack in
+# moz-libwebrtc should be pushed to github. The script
+# push_official_branch.sh uses this branch name when pushing to the
+# public repo.
+export MOZ_LIBWEBRTC_OFFICIAL_BRANCH="moz-mods-chr$m2-for-rel$t2"
+"""
+
+
+def build_default_config_env(bug_number, milestone, target):
+ prior_branch_head = lookup_branch_head.get_branch_head(milestone)
+ if prior_branch_head is None:
+ sys.exit("error: chromium milestone '{}' is not found.".format(milestone))
+ new_branch_head = lookup_branch_head.get_branch_head(milestone + 1)
+ if new_branch_head is None:
+ sys.exit(
+ "error: next chromium milestone '{}' is not found.".format(milestone + 1)
+ )
+
+ s = Template(text)
+ return s.substitute(
+ bugnum=bug_number,
+ m1=milestone,
+ m2=milestone + 1,
+ t2=target + 1,
+ bh1=prior_branch_head,
+ bh2=new_branch_head,
+ )
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(
+ description="Updates the default_config_env file for new release/milestone"
+ )
+ parser.add_argument(
+ "--bug-number",
+ required=True,
+ type=int,
+ help="integer Bugzilla number (example: 1800920)",
+ )
+ parser.add_argument(
+ "--milestone",
+ required=True,
+ type=int,
+ help="integer chromium milestone (example: 106)",
+ )
+ parser.add_argument(
+ "--release-target",
+ required=True,
+ type=int,
+ help="integer firefox release (example: 110)",
+ )
+ args = parser.parse_args()
+
+ print(
+ build_default_config_env(args.bug_number, args.milestone, args.release_target),
+ end="",
+ )
diff --git a/dom/media/webrtc/transport/README b/dom/media/webrtc/transport/README
new file mode 100644
index 0000000000..3630b2aa1f
--- /dev/null
+++ b/dom/media/webrtc/transport/README
@@ -0,0 +1,45 @@
+This is a generic media transport system for WebRTC.
+
+The basic model is that you have a TransportFlow which contains a
+series of TransportLayers, each of which gets an opportunity to
+manipulate data up and down the stack (think SysV STREAMS or a
+standard networking stack). You can also address individual
+sublayers to manipulate them or to bypass reading and writing
+at an upper layer; WebRTC uses this to implement DTLS-SRTP.
+
+
+DATAFLOW MODEL
+Unlike the existing nsSocket I/O system, this is a push rather
+than a pull system. Clients of the interface do writes downward
+with SendPacket() and receive notification of incoming packets
+via callbacks registed via sigslot.h. It is the responsibility
+of the bottom layer (or any other layer which needs to reference
+external events) to arrange for that somehow; typically by
+using nsITimer or the SocketTansportService.
+
+This sort of push model is a much better fit for the demands
+of WebRTC, expecially because ICE contexts span multiple
+network transports.
+
+
+THREADING MODEL
+There are no thread locks. It is the responsibility of the caller to
+arrange that any given TransportLayer/TransportFlow is only
+manipulated in one thread at once. One good way to do this is to run
+everything on the STS thread. Many of the existing layer implementations
+(TransportLayerIce, TransportLayerLoopback) already run on STS so in those
+cases you must run on STS, though you can do setup on the main thread and
+then activate them on the STS.
+
+
+EXISTING TRANSPORT LAYERS
+The following transport layers are currently implemented:
+
+* DTLS -- a wrapper around NSS's DTLS [RFC 6347] stack
+* ICE -- a wrapper around the nICEr ICE [RFC 5245] stack.
+* Loopback -- a loopback IO mechanism
+* Logging -- a passthrough that just logs its data
+
+The last two are primarily for debugging.
+
+
diff --git a/dom/media/webrtc/transport/SrtpFlow.cpp b/dom/media/webrtc/transport/SrtpFlow.cpp
new file mode 100644
index 0000000000..827d1a0f6d
--- /dev/null
+++ b/dom/media/webrtc/transport/SrtpFlow.cpp
@@ -0,0 +1,259 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+#include "logging.h"
+#include "SrtpFlow.h"
+
+#include "srtp.h"
+
+#include "transportlayerdtls.h"
+
+#include "mozilla/RefPtr.h"
+
+using namespace mozilla;
+
+namespace mozilla {
+
+MOZ_MTLOG_MODULE("mtransport")
+bool SrtpFlow::initialized; // Static
+
+SrtpFlow::~SrtpFlow() {
+ if (session_) {
+ srtp_dealloc(session_);
+ }
+}
+
+unsigned int SrtpFlow::KeySize(int cipher_suite) {
+ srtp_profile_t profile = static_cast<srtp_profile_t>(cipher_suite);
+ return srtp_profile_get_master_key_length(profile);
+}
+
+unsigned int SrtpFlow::SaltSize(int cipher_suite) {
+ srtp_profile_t profile = static_cast<srtp_profile_t>(cipher_suite);
+ return srtp_profile_get_master_salt_length(profile);
+}
+
+RefPtr<SrtpFlow> SrtpFlow::Create(int cipher_suite, bool inbound,
+ const void* key, size_t key_len) {
+ nsresult res = Init();
+ if (!NS_SUCCEEDED(res)) return nullptr;
+
+ RefPtr<SrtpFlow> flow = new SrtpFlow();
+
+ if (!key) {
+ MOZ_MTLOG(ML_ERROR, "Null SRTP key specified");
+ return nullptr;
+ }
+
+ if ((key_len > SRTP_MAX_KEY_LENGTH) || (key_len < SRTP_MIN_KEY_LENGTH)) {
+ MOZ_ASSERT(false, "Invalid SRTP key length");
+ return nullptr;
+ }
+
+ srtp_policy_t policy;
+ memset(&policy, 0, sizeof(srtp_policy_t));
+
+ // Note that we set the same cipher suite for RTP and RTCP
+ // since any flow can only have one cipher suite with DTLS-SRTP
+ switch (cipher_suite) {
+ case kDtlsSrtpAeadAes256Gcm:
+ MOZ_MTLOG(ML_DEBUG, "Setting SRTP cipher suite SRTP_AEAD_AES_256_GCM");
+ srtp_crypto_policy_set_aes_gcm_256_16_auth(&policy.rtp);
+ srtp_crypto_policy_set_aes_gcm_256_16_auth(&policy.rtcp);
+ break;
+ case kDtlsSrtpAeadAes128Gcm:
+ MOZ_MTLOG(ML_DEBUG, "Setting SRTP cipher suite SRTP_AEAD_AES_128_GCM");
+ srtp_crypto_policy_set_aes_gcm_128_16_auth(&policy.rtp);
+ srtp_crypto_policy_set_aes_gcm_128_16_auth(&policy.rtcp);
+ break;
+ case kDtlsSrtpAes128CmHmacSha1_80:
+ MOZ_MTLOG(ML_DEBUG,
+ "Setting SRTP cipher suite SRTP_AES128_CM_HMAC_SHA1_80");
+ srtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&policy.rtp);
+ srtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&policy.rtcp);
+ break;
+ case kDtlsSrtpAes128CmHmacSha1_32:
+ MOZ_MTLOG(ML_DEBUG,
+ "Setting SRTP cipher suite SRTP_AES128_CM_HMAC_SHA1_32");
+ srtp_crypto_policy_set_aes_cm_128_hmac_sha1_32(&policy.rtp);
+ srtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(
+ &policy.rtcp); // 80-bit per RFC 5764
+ break; // S 4.1.2.
+ default:
+ MOZ_MTLOG(ML_ERROR, "Request to set unknown SRTP cipher suite");
+ return nullptr;
+ }
+ // This key is copied into the srtp_t object, so we don't
+ // need to keep it.
+ policy.key =
+ const_cast<unsigned char*>(static_cast<const unsigned char*>(key));
+ policy.ssrc.type = inbound ? ssrc_any_inbound : ssrc_any_outbound;
+ policy.ssrc.value = 0;
+ policy.window_size =
+ 1024; // Use the Chrome value. Needs to be revisited. Default is 128
+ policy.allow_repeat_tx = 1; // Use Chrome value; needed for NACK mode to work
+ policy.next = nullptr;
+
+ // Now make the session
+ srtp_err_status_t r = srtp_create(&flow->session_, &policy);
+ if (r != srtp_err_status_ok) {
+ MOZ_MTLOG(ML_ERROR, "Error creating srtp session");
+ return nullptr;
+ }
+
+ return flow;
+}
+
+nsresult SrtpFlow::CheckInputs(bool protect, void* in, int in_len, int max_len,
+ int* out_len) {
+ MOZ_ASSERT(in);
+ if (!in) {
+ MOZ_MTLOG(ML_ERROR, "NULL input value");
+ return NS_ERROR_NULL_POINTER;
+ }
+
+ if (in_len < 0) {
+ MOZ_MTLOG(ML_ERROR, "Input length is negative");
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ if (max_len < 0) {
+ MOZ_MTLOG(ML_ERROR, "Max output length is negative");
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ if (protect) {
+ if ((max_len < SRTP_MAX_EXPANSION) ||
+ ((max_len - SRTP_MAX_EXPANSION) < in_len)) {
+ MOZ_MTLOG(ML_ERROR, "Output too short");
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+ } else {
+ if (in_len > max_len) {
+ MOZ_MTLOG(ML_ERROR, "Output too short");
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult SrtpFlow::ProtectRtp(void* in, int in_len, int max_len, int* out_len) {
+ nsresult res = CheckInputs(true, in, in_len, max_len, out_len);
+ if (NS_FAILED(res)) return res;
+
+ int len = in_len;
+ srtp_err_status_t r = srtp_protect(session_, in, &len);
+
+ if (r != srtp_err_status_ok) {
+ MOZ_MTLOG(ML_ERROR, "Error protecting SRTP packet");
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_ASSERT(len <= max_len);
+ *out_len = len;
+
+ MOZ_MTLOG(ML_DEBUG,
+ "Successfully protected an SRTP packet of len " << *out_len);
+
+ return NS_OK;
+}
+
+nsresult SrtpFlow::UnprotectRtp(void* in, int in_len, int max_len,
+ int* out_len) {
+ nsresult res = CheckInputs(false, in, in_len, max_len, out_len);
+ if (NS_FAILED(res)) return res;
+
+ int len = in_len;
+ srtp_err_status_t r = srtp_unprotect(session_, in, &len);
+
+ if (r != srtp_err_status_ok) {
+ MOZ_MTLOG(ML_ERROR, "Error unprotecting SRTP packet error=" << (int)r);
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_ASSERT(len <= max_len);
+ *out_len = len;
+
+ MOZ_MTLOG(ML_DEBUG,
+ "Successfully unprotected an SRTP packet of len " << *out_len);
+
+ return NS_OK;
+}
+
+nsresult SrtpFlow::ProtectRtcp(void* in, int in_len, int max_len,
+ int* out_len) {
+ nsresult res = CheckInputs(true, in, in_len, max_len, out_len);
+ if (NS_FAILED(res)) return res;
+
+ int len = in_len;
+ srtp_err_status_t r = srtp_protect_rtcp(session_, in, &len);
+
+ if (r != srtp_err_status_ok) {
+ MOZ_MTLOG(ML_ERROR, "Error protecting SRTCP packet");
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_ASSERT(len <= max_len);
+ *out_len = len;
+
+ MOZ_MTLOG(ML_DEBUG,
+ "Successfully protected an SRTCP packet of len " << *out_len);
+
+ return NS_OK;
+}
+
+nsresult SrtpFlow::UnprotectRtcp(void* in, int in_len, int max_len,
+ int* out_len) {
+ nsresult res = CheckInputs(false, in, in_len, max_len, out_len);
+ if (NS_FAILED(res)) return res;
+
+ int len = in_len;
+ srtp_err_status_t r = srtp_unprotect_rtcp(session_, in, &len);
+
+ if (r != srtp_err_status_ok) {
+ MOZ_MTLOG(ML_ERROR, "Error unprotecting SRTCP packet error=" << (int)r);
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_ASSERT(len <= max_len);
+ *out_len = len;
+
+ MOZ_MTLOG(ML_DEBUG,
+ "Successfully unprotected an SRTCP packet of len " << *out_len);
+
+ return NS_OK;
+}
+
+// Statics
+void SrtpFlow::srtp_event_handler(srtp_event_data_t* data) {
+ // TODO(ekr@rtfm.com): Implement this
+ MOZ_CRASH();
+}
+
+nsresult SrtpFlow::Init() {
+ if (!initialized) {
+ srtp_err_status_t r = srtp_init();
+ if (r != srtp_err_status_ok) {
+ MOZ_MTLOG(ML_ERROR, "Could not initialize SRTP");
+ MOZ_ASSERT(PR_FALSE);
+ return NS_ERROR_FAILURE;
+ }
+
+ r = srtp_install_event_handler(&SrtpFlow::srtp_event_handler);
+ if (r != srtp_err_status_ok) {
+ MOZ_MTLOG(ML_ERROR, "Could not install SRTP event handler");
+ MOZ_ASSERT(PR_FALSE);
+ return NS_ERROR_FAILURE;
+ }
+
+ initialized = true;
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/transport/SrtpFlow.h b/dom/media/webrtc/transport/SrtpFlow.h
new file mode 100644
index 0000000000..92fbfcf1a5
--- /dev/null
+++ b/dom/media/webrtc/transport/SrtpFlow.h
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+#ifndef srtpflow_h__
+#define srtpflow_h__
+
+#include "mozilla/RefPtr.h"
+#include "nsISupportsImpl.h"
+#include "srtp.h"
+
+namespace mozilla {
+
+#define SRTP_ICM_MASTER_KEY_LENGTH 16
+#define SRTP_ICM_MASTER_SALT_LENGTH 14
+#define SRTP_ICM_MAX_MASTER_LENGTH \
+ (SRTP_ICM_MASTER_KEY_LENGTH + SRTP_ICM_MASTER_SALT_LENGTH)
+
+#define SRTP_GCM_MASTER_KEY_MIN_LENGTH 16
+#define SRTP_GCM_MASTER_KEY_MAX_LENGTH 32
+#define SRTP_GCM_MASTER_SALT_LENGTH 12
+
+#define SRTP_GCM_MIN_MASTER_LENGTH \
+ (SRTP_GCM_MASTER_KEY_MIN_LENGTH + SRTP_GCM_MASTER_SALT_LENGTH)
+#define SRTP_GCM_MAX_MASTER_LENGTH \
+ (SRTP_GCM_MASTER_KEY_MAX_LENGTH + SRTP_GCM_MASTER_SALT_LENGTH)
+
+#define SRTP_MIN_KEY_LENGTH SRTP_GCM_MIN_MASTER_LENGTH
+#define SRTP_MAX_KEY_LENGTH SRTP_GCM_MAX_MASTER_LENGTH
+
+// SRTCP requires an auth tag *plus* a 4-byte index-plus-'E'-bit value (see
+// RFC 3711)
+#define SRTP_MAX_EXPANSION (SRTP_MAX_TRAILER_LEN + 4)
+
+class SrtpFlow {
+ ~SrtpFlow();
+
+ public:
+ static unsigned int KeySize(int cipher_suite);
+ static unsigned int SaltSize(int cipher_suite);
+
+ static RefPtr<SrtpFlow> Create(int cipher_suite, bool inbound,
+ const void* key, size_t key_len);
+
+ nsresult ProtectRtp(void* in, int in_len, int max_len, int* out_len);
+ nsresult UnprotectRtp(void* in, int in_len, int max_len, int* out_len);
+ nsresult ProtectRtcp(void* in, int in_len, int max_len, int* out_len);
+ nsresult UnprotectRtcp(void* in, int in_len, int max_len, int* out_len);
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SrtpFlow)
+
+ static void srtp_event_handler(srtp_event_data_t* data);
+
+ private:
+ SrtpFlow() : session_(nullptr) {}
+
+ nsresult CheckInputs(bool protect, void* in, int in_len, int max_len,
+ int* out_len);
+
+ static nsresult Init();
+ static bool initialized; // Was libsrtp initialized? Only happens once.
+
+ srtp_t session_;
+};
+
+} // namespace mozilla
+#endif
diff --git a/dom/media/webrtc/transport/WebrtcTCPSocketWrapper.cpp b/dom/media/webrtc/transport/WebrtcTCPSocketWrapper.cpp
new file mode 100644
index 0000000000..2bff019878
--- /dev/null
+++ b/dom/media/webrtc/transport/WebrtcTCPSocketWrapper.cpp
@@ -0,0 +1,123 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WebrtcTCPSocketWrapper.h"
+
+#include "mozilla/net/WebrtcTCPSocketChild.h"
+
+#include "nsNetCID.h"
+#include "nsProxyRelease.h"
+#include "nsServiceManagerUtils.h"
+
+#include "nr_socket_proxy_config.h"
+
+namespace mozilla::net {
+
+using std::shared_ptr;
+
+WebrtcTCPSocketWrapper::WebrtcTCPSocketWrapper(
+ WebrtcTCPSocketCallback* aCallbacks)
+ : mProxyCallbacks(aCallbacks),
+ mWebrtcTCPSocket(nullptr),
+ mMainThread(nullptr),
+ mSocketThread(nullptr) {
+ mMainThread = GetMainThreadSerialEventTarget();
+ mSocketThread = do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID);
+ MOZ_RELEASE_ASSERT(mMainThread, "no main thread");
+ MOZ_RELEASE_ASSERT(mSocketThread, "no socket thread");
+}
+
+WebrtcTCPSocketWrapper::~WebrtcTCPSocketWrapper() {
+ MOZ_ASSERT(!mWebrtcTCPSocket, "webrtc TCP socket non-null");
+
+ // If we're never opened then we never get an OnClose from our parent process.
+ // We need to release our callbacks here safely.
+ NS_ProxyRelease("WebrtcTCPSocketWrapper::CleanUpCallbacks", mSocketThread,
+ mProxyCallbacks.forget());
+}
+
+void WebrtcTCPSocketWrapper::AsyncOpen(
+ const nsCString& aHost, const int& aPort, const nsCString& aLocalAddress,
+ const int& aLocalPort, bool aUseTls,
+ const shared_ptr<NrSocketProxyConfig>& aConfig) {
+ if (!NS_IsMainThread()) {
+ MOZ_ALWAYS_SUCCEEDS(mMainThread->Dispatch(
+ NewRunnableMethod<const nsCString, const int, const nsCString,
+ const int, bool,
+ const shared_ptr<NrSocketProxyConfig>>(
+ "WebrtcTCPSocketWrapper::AsyncOpen", this,
+ &WebrtcTCPSocketWrapper::AsyncOpen, aHost, aPort, aLocalAddress,
+ aLocalPort, aUseTls, aConfig)));
+ return;
+ }
+
+ MOZ_ASSERT(!mWebrtcTCPSocket, "wrapper already open");
+ mWebrtcTCPSocket = new WebrtcTCPSocketChild(this);
+ mWebrtcTCPSocket->AsyncOpen(aHost, aPort, aLocalAddress, aLocalPort, aUseTls,
+ aConfig);
+}
+
+void WebrtcTCPSocketWrapper::SendWrite(nsTArray<uint8_t>&& aReadData) {
+ if (!NS_IsMainThread()) {
+ MOZ_ALWAYS_SUCCEEDS(
+ mMainThread->Dispatch(NewRunnableMethod<nsTArray<uint8_t>&&>(
+ "WebrtcTCPSocketWrapper::SendWrite", this,
+ &WebrtcTCPSocketWrapper::SendWrite, std::move(aReadData))));
+ return;
+ }
+
+ MOZ_ASSERT(mWebrtcTCPSocket, "webrtc TCP socket should be non-null");
+ mWebrtcTCPSocket->SendWrite(aReadData);
+}
+
+void WebrtcTCPSocketWrapper::Close() {
+ if (!NS_IsMainThread()) {
+ MOZ_ALWAYS_SUCCEEDS(mMainThread->Dispatch(
+ NewRunnableMethod("WebrtcTCPSocketWrapper::Close", this,
+ &WebrtcTCPSocketWrapper::Close)));
+ return;
+ }
+
+ // We're only open if we have a channel. Also Close() should be idempotent.
+ if (mWebrtcTCPSocket) {
+ RefPtr<WebrtcTCPSocketChild> child = mWebrtcTCPSocket;
+ mWebrtcTCPSocket = nullptr;
+ child->SendClose();
+ }
+}
+
+void WebrtcTCPSocketWrapper::OnClose(nsresult aReason) {
+ MOZ_ASSERT(NS_IsMainThread(), "not on main thread");
+ MOZ_ASSERT(mProxyCallbacks, "webrtc TCP callbacks should be non-null");
+
+ MOZ_ALWAYS_SUCCEEDS(mSocketThread->Dispatch(NewRunnableMethod<nsresult>(
+ "WebrtcTCPSocketWrapper::OnClose", mProxyCallbacks,
+ &WebrtcTCPSocketCallback::OnClose, aReason)));
+
+ NS_ProxyRelease("WebrtcTCPSocketWrapper::CleanUpCallbacks", mSocketThread,
+ mProxyCallbacks.forget());
+}
+
+void WebrtcTCPSocketWrapper::OnRead(nsTArray<uint8_t>&& aReadData) {
+ MOZ_ASSERT(NS_IsMainThread(), "not on main thread");
+ MOZ_ASSERT(mProxyCallbacks, "webrtc TCP callbacks should be non-null");
+
+ MOZ_ALWAYS_SUCCEEDS(
+ mSocketThread->Dispatch(NewRunnableMethod<nsTArray<uint8_t>&&>(
+ "WebrtcTCPSocketWrapper::OnRead", mProxyCallbacks,
+ &WebrtcTCPSocketCallback::OnRead, std::move(aReadData))));
+}
+
+void WebrtcTCPSocketWrapper::OnConnected(const nsACString& aProxyType) {
+ MOZ_ASSERT(NS_IsMainThread(), "not on main thread");
+ MOZ_ASSERT(mProxyCallbacks, "webrtc TCP callbacks should be non-null");
+
+ MOZ_ALWAYS_SUCCEEDS(mSocketThread->Dispatch(NewRunnableMethod<nsCString>(
+ "WebrtcTCPSocketWrapper::OnConnected", mProxyCallbacks,
+ &WebrtcTCPSocketCallback::OnConnected, aProxyType)));
+}
+
+} // namespace mozilla::net
diff --git a/dom/media/webrtc/transport/WebrtcTCPSocketWrapper.h b/dom/media/webrtc/transport/WebrtcTCPSocketWrapper.h
new file mode 100644
index 0000000000..c0775ee9aa
--- /dev/null
+++ b/dom/media/webrtc/transport/WebrtcTCPSocketWrapper.h
@@ -0,0 +1,69 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef webrtc_tcp_socket_wrapper__
+#define webrtc_tcp_socket_wrapper__
+
+#include <memory>
+
+#include "nsCOMPtr.h"
+#include "nsTArray.h"
+
+#include "mozilla/net/WebrtcTCPSocketCallback.h"
+
+class nsIEventTarget;
+
+namespace mozilla {
+
+class NrSocketProxyConfig;
+
+namespace net {
+
+class WebrtcTCPSocketChild;
+
+/**
+ * WebrtcTCPSocketWrapper is a protector class for mtransport and IPDL.
+ * mtransport and IPDL cannot include headers from each other due to conflicting
+ * typedefs. Also it helps users by dispatching calls to the appropriate thread
+ * based on mtransport's and IPDL's threading requirements.
+ *
+ * WebrtcTCPSocketWrapper is only used in the child process.
+ * WebrtcTCPSocketWrapper does not dispatch for the parent process.
+ * WebrtcTCPSocketCallback calls are dispatched to the STS thread.
+ * IPDL calls are dispatched to the main thread.
+ */
+class WebrtcTCPSocketWrapper : public WebrtcTCPSocketCallback {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WebrtcTCPSocketWrapper, override)
+
+ explicit WebrtcTCPSocketWrapper(WebrtcTCPSocketCallback* aCallbacks);
+
+ virtual void AsyncOpen(const nsCString& aHost, const int& aPort,
+ const nsCString& aLocalAddress, const int& aLocalPort,
+ bool aUseTls,
+ const std::shared_ptr<NrSocketProxyConfig>& aConfig);
+ virtual void SendWrite(nsTArray<uint8_t>&& aReadData);
+ virtual void Close();
+
+ // WebrtcTCPSocketCallback
+ virtual void OnClose(nsresult aReason) override;
+ virtual void OnConnected(const nsACString& aProxyType) override;
+ virtual void OnRead(nsTArray<uint8_t>&& aReadData) override;
+
+ protected:
+ RefPtr<WebrtcTCPSocketCallback> mProxyCallbacks;
+ RefPtr<WebrtcTCPSocketChild> mWebrtcTCPSocket;
+
+ nsCOMPtr<nsIEventTarget> mMainThread;
+ nsCOMPtr<nsIEventTarget> mSocketThread;
+
+ virtual ~WebrtcTCPSocketWrapper();
+};
+
+} // namespace net
+} // namespace mozilla
+
+#endif // webrtc_tcp_socket_wrapper__
diff --git a/dom/media/webrtc/transport/build/moz.build b/dom/media/webrtc/transport/build/moz.build
new file mode 100644
index 0000000000..7349ee5d71
--- /dev/null
+++ b/dom/media/webrtc/transport/build/moz.build
@@ -0,0 +1,44 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+EXPORTS.transport += [
+ "../dtlsidentity.h",
+ "../m_cpp_utils.h",
+ "../mediapacket.h",
+ "../nr_socket_proxy_config.h",
+ "../nricectx.h",
+ "../nricemediastream.h",
+ "../nriceresolverfake.h",
+ "../nricestunaddr.h",
+ "../rlogconnector.h",
+ "../runnable_utils.h",
+ "../sigslot.h",
+ "../simpletokenbucket.h",
+ "../SrtpFlow.h",
+ "../stun_socket_filter.h",
+ "../transportflow.h",
+ "../transportlayer.h",
+ "../transportlayerdtls.h",
+ "../transportlayerice.h",
+ "../transportlayerlog.h",
+ "../transportlayerloopback.h",
+ "../transportlayersrtp.h",
+]
+
+include("../common.build")
+
+# Add libFuzzer configuration directives
+include("/tools/fuzzing/libfuzzer-config.mozbuild")
+
+# These files cannot be built in unified mode because of the redefinition of
+# getLogModule, UNIMPLEMENTED, nr_socket_long_term_violation_time,
+# nr_socket_short_term_violation_time, nrappkit/IPDL typedef conflicts in
+# PBrowserOrId and WebrtcTCPSocketChild.
+SOURCES += transport_cppsrcs
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/webrtc/transport/common.build b/dom/media/webrtc/transport/common.build
new file mode 100644
index 0000000000..b907b57ee8
--- /dev/null
+++ b/dom/media/webrtc/transport/common.build
@@ -0,0 +1,94 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+transport_lcppsrcs = [
+ "dtlsidentity.cpp",
+ "mediapacket.cpp",
+ "nr_socket_proxy_config.cpp",
+ "nr_socket_prsock.cpp",
+ "nr_socket_tcp.cpp",
+ "nr_timer.cpp",
+ "nricectx.cpp",
+ "nricemediastream.cpp",
+ "nriceresolver.cpp",
+ "nriceresolverfake.cpp",
+ "nricestunaddr.cpp",
+ "nrinterfaceprioritizer.cpp",
+ "rlogconnector.cpp",
+ "simpletokenbucket.cpp",
+ "SrtpFlow.cpp",
+ "stun_socket_filter.cpp",
+ "test_nr_socket.cpp",
+ "transportflow.cpp",
+ "transportlayer.cpp",
+ "transportlayerdtls.cpp",
+ "transportlayerice.cpp",
+ "transportlayerlog.cpp",
+ "transportlayerloopback.cpp",
+ "transportlayersrtp.cpp",
+ "WebrtcTCPSocketWrapper.cpp",
+]
+
+transport_cppsrcs = [
+ "/dom/media/webrtc/transport/%s" % s for s in sorted(transport_lcppsrcs)
+]
+
+LOCAL_INCLUDES += [
+ "/dom/media/webrtc/transport/",
+ "/dom/media/webrtc/transport/third_party/",
+ "/dom/media/webrtc/transport/third_party/nICEr/src/crypto",
+ "/dom/media/webrtc/transport/third_party/nICEr/src/ice",
+ "/dom/media/webrtc/transport/third_party/nICEr/src/net",
+ "/dom/media/webrtc/transport/third_party/nICEr/src/stun",
+ "/dom/media/webrtc/transport/third_party/nICEr/src/util",
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/event",
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/log",
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/plugin",
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/port/generic/include",
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/registry",
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/share",
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/stats",
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr",
+ "/third_party/libsrtp/src/crypto/include",
+ "/third_party/libsrtp/src/include",
+]
+
+if CONFIG["OS_TARGET"] in ["Darwin", "DragonFly", "FreeBSD", "NetBSD", "OpenBSD"]:
+ if CONFIG["OS_TARGET"] == "Darwin":
+ DEFINES["DARWIN"] = True
+ else:
+ DEFINES["BSD"] = True
+ LOCAL_INCLUDES += [
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/port/darwin/include",
+ ]
+elif CONFIG["OS_TARGET"] == "Linux":
+ DEFINES["LINUX"] = True
+ LOCAL_INCLUDES += [
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/port/linux/include",
+ ]
+elif CONFIG["OS_TARGET"] == "Android":
+ DEFINES["LINUX"] = True
+ DEFINES["ANDROID"] = True
+ LOCAL_INCLUDES += [
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/port/android/include",
+ ]
+elif CONFIG["OS_TARGET"] == "WINNT":
+ DEFINES["WIN"] = True
+ # for stun.h
+ DEFINES["WIN32"] = True
+ LOCAL_INCLUDES += [
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/port/win32/include",
+ ]
+
+for var in ("HAVE_STRDUP", "NR_SOCKET_IS_VOID_PTR"):
+ DEFINES[var] = True
+
+DEFINES["R_DEFINED_INT2"] = "int16_t"
+DEFINES["R_DEFINED_UINT2"] = "uint16_t"
+DEFINES["R_DEFINED_INT4"] = "int32_t"
+DEFINES["R_DEFINED_UINT4"] = "uint32_t"
+DEFINES["R_DEFINED_INT8"] = "int64_t"
+DEFINES["R_DEFINED_UINT8"] = "uint64_t"
diff --git a/dom/media/webrtc/transport/dtlsidentity.cpp b/dom/media/webrtc/transport/dtlsidentity.cpp
new file mode 100644
index 0000000000..c466ce7d88
--- /dev/null
+++ b/dom/media/webrtc/transport/dtlsidentity.cpp
@@ -0,0 +1,331 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "dtlsidentity.h"
+
+#include "cert.h"
+#include "cryptohi.h"
+#include "keyhi.h"
+#include "nsError.h"
+#include "pk11pub.h"
+#include "sechash.h"
+#include "mozpkix/nss_scoped_ptrs.h"
+#include "secerr.h"
+#include "sslerr.h"
+
+#include "mozilla/Sprintf.h"
+
+namespace mozilla {
+
+SECItem* WrapPrivateKeyInfoWithEmptyPassword(
+ SECKEYPrivateKey* pk) /* encrypt this private key */
+{
+ if (!pk) {
+ PR_SetError(SEC_ERROR_INVALID_ARGS, 0);
+ return nullptr;
+ }
+
+ UniquePK11SlotInfo slot(PK11_GetInternalSlot());
+ if (!slot) {
+ return nullptr;
+ }
+
+ // For private keys, NSS cannot export anything other than RSA, but we need EC
+ // also. So, we use the private key encryption function to serialize instead,
+ // using a hard-coded dummy password; this is not intended to provide any
+ // additional security, it just works around a limitation in NSS.
+ SECItem dummyPassword = {siBuffer, nullptr, 0};
+ UniqueSECKEYEncryptedPrivateKeyInfo epki(PK11_ExportEncryptedPrivKeyInfo(
+ slot.get(), SEC_OID_AES_128_CBC, &dummyPassword, pk, 1, nullptr));
+
+ if (!epki) {
+ return nullptr;
+ }
+
+ return SEC_ASN1EncodeItem(
+ nullptr, nullptr, epki.get(),
+ NSS_Get_SECKEY_EncryptedPrivateKeyInfoTemplate(nullptr, false));
+}
+
+SECStatus UnwrapPrivateKeyInfoWithEmptyPassword(
+ SECItem* derPKI, const UniqueCERTCertificate& aCert,
+ SECKEYPrivateKey** privk) {
+ if (!derPKI || !aCert || !privk) {
+ PR_SetError(SEC_ERROR_INVALID_ARGS, 0);
+ return SECFailure;
+ }
+
+ UniqueSECKEYPublicKey publicKey(CERT_ExtractPublicKey(aCert.get()));
+ // This is a pointer to data inside publicKey
+ SECItem* publicValue = nullptr;
+ switch (publicKey->keyType) {
+ case dsaKey:
+ publicValue = &publicKey->u.dsa.publicValue;
+ break;
+ case dhKey:
+ publicValue = &publicKey->u.dh.publicValue;
+ break;
+ case rsaKey:
+ publicValue = &publicKey->u.rsa.modulus;
+ break;
+ case ecKey:
+ publicValue = &publicKey->u.ec.publicValue;
+ break;
+ default:
+ MOZ_ASSERT(false);
+ PR_SetError(SSL_ERROR_BAD_CERTIFICATE, 0);
+ return SECFailure;
+ }
+
+ UniquePK11SlotInfo slot(PK11_GetInternalSlot());
+ if (!slot) {
+ return SECFailure;
+ }
+
+ UniquePLArenaPool temparena(PORT_NewArena(DER_DEFAULT_CHUNKSIZE));
+ if (!temparena) {
+ return SECFailure;
+ }
+
+ SECKEYEncryptedPrivateKeyInfo* epki =
+ PORT_ArenaZNew(temparena.get(), SECKEYEncryptedPrivateKeyInfo);
+ if (!epki) {
+ return SECFailure;
+ }
+
+ SECStatus rv = SEC_ASN1DecodeItem(
+ temparena.get(), epki,
+ NSS_Get_SECKEY_EncryptedPrivateKeyInfoTemplate(nullptr, false), derPKI);
+ if (rv != SECSuccess) {
+ // If SEC_ASN1DecodeItem fails, we cannot assume anything about the
+ // validity of the data in epki. The best we can do is free the arena
+ // and return.
+ return rv;
+ }
+
+ // See comment in WrapPrivateKeyInfoWithEmptyPassword about this
+ // dummy password stuff.
+ SECItem dummyPassword = {siBuffer, nullptr, 0};
+ return PK11_ImportEncryptedPrivateKeyInfoAndReturnKey(
+ slot.get(), epki, &dummyPassword, nullptr, publicValue, false, false,
+ publicKey->keyType, KU_ALL, privk, nullptr);
+}
+
+nsresult DtlsIdentity::Serialize(nsTArray<uint8_t>* aKeyDer,
+ nsTArray<uint8_t>* aCertDer) {
+ ScopedSECItem derPki(WrapPrivateKeyInfoWithEmptyPassword(private_key_.get()));
+ if (!derPki) {
+ return NS_ERROR_FAILURE;
+ }
+
+ aKeyDer->AppendElements(derPki->data, derPki->len);
+ aCertDer->AppendElements(cert_->derCert.data, cert_->derCert.len);
+ return NS_OK;
+}
+
+/* static */
+RefPtr<DtlsIdentity> DtlsIdentity::Deserialize(
+ const nsTArray<uint8_t>& aKeyDer, const nsTArray<uint8_t>& aCertDer,
+ SSLKEAType authType) {
+ SECItem certDer = {siBuffer, const_cast<uint8_t*>(aCertDer.Elements()),
+ static_cast<unsigned int>(aCertDer.Length())};
+ UniqueCERTCertificate cert(CERT_NewTempCertificate(
+ CERT_GetDefaultCertDB(), &certDer, nullptr, true, true));
+
+ SECItem derPKI = {siBuffer, const_cast<uint8_t*>(aKeyDer.Elements()),
+ static_cast<unsigned int>(aKeyDer.Length())};
+
+ SECKEYPrivateKey* privateKey;
+ if (UnwrapPrivateKeyInfoWithEmptyPassword(&derPKI, cert, &privateKey) !=
+ SECSuccess) {
+ MOZ_ASSERT(false);
+ return nullptr;
+ }
+
+ return new DtlsIdentity(UniqueSECKEYPrivateKey(privateKey), std::move(cert),
+ authType);
+}
+
+RefPtr<DtlsIdentity> DtlsIdentity::Generate() {
+ UniquePK11SlotInfo slot(PK11_GetInternalSlot());
+ if (!slot) {
+ return nullptr;
+ }
+
+ uint8_t random_name[16];
+
+ SECStatus rv =
+ PK11_GenerateRandomOnSlot(slot.get(), random_name, sizeof(random_name));
+ if (rv != SECSuccess) return nullptr;
+
+ std::string name;
+ char chunk[3];
+ for (unsigned char r_name : random_name) {
+ SprintfLiteral(chunk, "%.2x", r_name);
+ name += chunk;
+ }
+
+ std::string subject_name_string = "CN=" + name;
+ UniqueCERTName subject_name(CERT_AsciiToName(subject_name_string.c_str()));
+ if (!subject_name) {
+ return nullptr;
+ }
+
+ unsigned char paramBuf[12]; // OIDs are small
+ SECItem ecdsaParams = {siBuffer, paramBuf, sizeof(paramBuf)};
+ SECOidData* oidData = SECOID_FindOIDByTag(SEC_OID_SECG_EC_SECP256R1);
+ if (!oidData || (oidData->oid.len > (sizeof(paramBuf) - 2))) {
+ return nullptr;
+ }
+ ecdsaParams.data[0] = SEC_ASN1_OBJECT_ID;
+ ecdsaParams.data[1] = oidData->oid.len;
+ memcpy(ecdsaParams.data + 2, oidData->oid.data, oidData->oid.len);
+ ecdsaParams.len = oidData->oid.len + 2;
+
+ SECKEYPublicKey* pubkey;
+ UniqueSECKEYPrivateKey private_key(
+ PK11_GenerateKeyPair(slot.get(), CKM_EC_KEY_PAIR_GEN, &ecdsaParams,
+ &pubkey, PR_FALSE, PR_TRUE, nullptr));
+ if (private_key == nullptr) return nullptr;
+ UniqueSECKEYPublicKey public_key(pubkey);
+ pubkey = nullptr;
+
+ UniqueCERTSubjectPublicKeyInfo spki(
+ SECKEY_CreateSubjectPublicKeyInfo(public_key.get()));
+ if (!spki) {
+ return nullptr;
+ }
+
+ UniqueCERTCertificateRequest certreq(
+ CERT_CreateCertificateRequest(subject_name.get(), spki.get(), nullptr));
+ if (!certreq) {
+ return nullptr;
+ }
+
+ // From 1 day before todayto 30 days after.
+ // This is a sort of arbitrary range designed to be valid
+ // now with some slack in case the other side expects
+ // some before expiry.
+ //
+ // Note: explicit casts necessary to avoid
+ // warning C4307: '*' : integral constant overflow
+ static const PRTime oneDay = PRTime(PR_USEC_PER_SEC) * PRTime(60) // sec
+ * PRTime(60) // min
+ * PRTime(24); // hours
+ PRTime now = PR_Now();
+ PRTime notBefore = now - oneDay;
+ PRTime notAfter = now + (PRTime(30) * oneDay);
+
+ UniqueCERTValidity validity(CERT_CreateValidity(notBefore, notAfter));
+ if (!validity) {
+ return nullptr;
+ }
+
+ unsigned long serial;
+ // Note: This serial in principle could collide, but it's unlikely
+ rv = PK11_GenerateRandomOnSlot(
+ slot.get(), reinterpret_cast<unsigned char*>(&serial), sizeof(serial));
+ if (rv != SECSuccess) {
+ return nullptr;
+ }
+
+ // NB: CERTCertificates created with CERT_CreateCertificate are not safe to
+ // use with other NSS functions like CERT_DupCertificate.
+ // The strategy here is to create a tbsCertificate ("to-be-signed
+ // certificate"), encode it, and sign it, resulting in a signed DER
+ // certificate that can be decoded into a CERTCertificate.
+ UniqueCERTCertificate tbsCertificate(CERT_CreateCertificate(
+ serial, subject_name.get(), validity.get(), certreq.get()));
+ if (!tbsCertificate) {
+ return nullptr;
+ }
+
+ PLArenaPool* arena = tbsCertificate->arena;
+
+ rv = SECOID_SetAlgorithmID(arena, &tbsCertificate->signature,
+ SEC_OID_ANSIX962_ECDSA_SHA256_SIGNATURE, nullptr);
+ if (rv != SECSuccess) return nullptr;
+
+ // Set version to X509v3.
+ *(tbsCertificate->version.data) = SEC_CERTIFICATE_VERSION_3;
+ tbsCertificate->version.len = 1;
+
+ SECItem innerDER;
+ innerDER.len = 0;
+ innerDER.data = nullptr;
+
+ if (!SEC_ASN1EncodeItem(arena, &innerDER, tbsCertificate.get(),
+ SEC_ASN1_GET(CERT_CertificateTemplate))) {
+ return nullptr;
+ }
+
+ SECItem* certDer = PORT_ArenaZNew(arena, SECItem);
+ if (!certDer) {
+ return nullptr;
+ }
+
+ rv = SEC_DerSignData(arena, certDer, innerDER.data, innerDER.len,
+ private_key.get(),
+ SEC_OID_ANSIX962_ECDSA_SHA256_SIGNATURE);
+ if (rv != SECSuccess) {
+ return nullptr;
+ }
+
+ UniqueCERTCertificate certificate(CERT_NewTempCertificate(
+ CERT_GetDefaultCertDB(), certDer, nullptr, false, true));
+
+ return new DtlsIdentity(std::move(private_key), std::move(certificate),
+ ssl_kea_ecdh);
+}
+
+const std::string DtlsIdentity::DEFAULT_HASH_ALGORITHM = "sha-256";
+
+nsresult DtlsIdentity::ComputeFingerprint(DtlsDigest* digest) const {
+ const UniqueCERTCertificate& c = cert();
+ MOZ_ASSERT(c);
+
+ return ComputeFingerprint(c, digest);
+}
+
+nsresult DtlsIdentity::ComputeFingerprint(const UniqueCERTCertificate& cert,
+ DtlsDigest* digest) {
+ MOZ_ASSERT(cert);
+
+ HASH_HashType ht;
+
+ if (digest->algorithm_ == "sha-1") {
+ ht = HASH_AlgSHA1;
+ } else if (digest->algorithm_ == "sha-224") {
+ ht = HASH_AlgSHA224;
+ } else if (digest->algorithm_ == "sha-256") {
+ ht = HASH_AlgSHA256;
+ } else if (digest->algorithm_ == "sha-384") {
+ ht = HASH_AlgSHA384;
+ } else if (digest->algorithm_ == "sha-512") {
+ ht = HASH_AlgSHA512;
+ } else {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ const SECHashObject* ho = HASH_GetHashObject(ht);
+ MOZ_ASSERT(ho);
+ if (!ho) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ MOZ_ASSERT(ho->length >= 20); // Double check
+ digest->value_.resize(ho->length);
+
+ SECStatus rv = HASH_HashBuf(ho->type, digest->value_.data(),
+ cert->derCert.data, cert->derCert.len);
+ if (rv != SECSuccess) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/transport/dtlsidentity.h b/dom/media/webrtc/transport/dtlsidentity.h
new file mode 100644
index 0000000000..b4f7686618
--- /dev/null
+++ b/dom/media/webrtc/transport/dtlsidentity.h
@@ -0,0 +1,101 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef dtls_identity_h__
+#define dtls_identity_h__
+
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "ScopedNSSTypes.h"
+#include "m_cpp_utils.h"
+#include "mozilla/RefPtr.h"
+#include "nsISupportsImpl.h"
+#include "nsTArray.h"
+#include "sslt.h"
+
+// All code in this module requires NSS to be live.
+// Callers must initialize NSS and implement the nsNSSShutdownObject
+// protocol.
+namespace mozilla {
+
+class DtlsDigest {
+ public:
+ const static size_t kMaxDtlsDigestLength = HASH_LENGTH_MAX;
+ DtlsDigest() = default;
+ explicit DtlsDigest(const std::string& algorithm) : algorithm_(algorithm) {}
+ DtlsDigest(const std::string& algorithm, const std::vector<uint8_t>& value)
+ : algorithm_(algorithm), value_(value) {
+ MOZ_ASSERT(value.size() <= kMaxDtlsDigestLength);
+ }
+ ~DtlsDigest() = default;
+
+ bool operator!=(const DtlsDigest& rhs) const { return !operator==(rhs); }
+
+ bool operator==(const DtlsDigest& rhs) const {
+ if (algorithm_ != rhs.algorithm_) {
+ return false;
+ }
+
+ return value_ == rhs.value_;
+ }
+
+ std::string algorithm_;
+ std::vector<uint8_t> value_;
+};
+
+typedef std::vector<DtlsDigest> DtlsDigestList;
+
+class DtlsIdentity final {
+ public:
+ // This constructor takes ownership of privkey and cert.
+ DtlsIdentity(UniqueSECKEYPrivateKey privkey, UniqueCERTCertificate cert,
+ SSLKEAType authType)
+ : private_key_(std::move(privkey)),
+ cert_(std::move(cert)),
+ auth_type_(authType) {}
+
+ // Allows serialization/deserialization; cannot write IPC serialization code
+ // directly for DtlsIdentity, since IPC-able types need to be constructable
+ // on the stack.
+ nsresult Serialize(nsTArray<uint8_t>* aKeyDer, nsTArray<uint8_t>* aCertDer);
+ static RefPtr<DtlsIdentity> Deserialize(const nsTArray<uint8_t>& aKeyDer,
+ const nsTArray<uint8_t>& aCertDer,
+ SSLKEAType authType);
+
+ // This is only for use in tests, or for external linkage. It makes a (bad)
+ // instance of this class.
+ static RefPtr<DtlsIdentity> Generate();
+
+ // These don't create copies or transfer ownership. If you want these to live
+ // on, make a copy.
+ const UniqueCERTCertificate& cert() const { return cert_; }
+ const UniqueSECKEYPrivateKey& privkey() const { return private_key_; }
+ // Note: this uses SSLKEAType because that is what the libssl API requires.
+ // This is a giant confusing mess, but libssl indexes certificates based on a
+ // key exchange type, not authentication type (as you might have reasonably
+ // expected).
+ SSLKEAType auth_type() const { return auth_type_; }
+
+ nsresult ComputeFingerprint(DtlsDigest* digest) const;
+ static nsresult ComputeFingerprint(const UniqueCERTCertificate& cert,
+ DtlsDigest* digest);
+
+ static const std::string DEFAULT_HASH_ALGORITHM;
+ enum { HASH_ALGORITHM_MAX_LENGTH = 64 };
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DtlsIdentity)
+
+ private:
+ ~DtlsIdentity() = default;
+ DISALLOW_COPY_ASSIGN(DtlsIdentity);
+
+ UniqueSECKEYPrivateKey private_key_;
+ UniqueCERTCertificate cert_;
+ SSLKEAType auth_type_;
+};
+} // namespace mozilla
+#endif
diff --git a/dom/media/webrtc/transport/fuzztest/moz.build b/dom/media/webrtc/transport/fuzztest/moz.build
new file mode 100644
index 0000000000..f22a5a702b
--- /dev/null
+++ b/dom/media/webrtc/transport/fuzztest/moz.build
@@ -0,0 +1,31 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Library("FuzzingStun")
+
+DEFINES["HAVE_STRDUP"] = True
+
+LOCAL_INCLUDES += [
+ "/dom/media/webrtc/transport/third_party/nICEr/src/net",
+ "/dom/media/webrtc/transport/third_party/nICEr/src/stun",
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/event",
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/log",
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/plugin",
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/port/darwin/include",
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/share",
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/stats",
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr",
+ "/ipc/chromium/src",
+]
+
+# Add libFuzzer configuration directives
+include("/tools/fuzzing/libfuzzer-config.mozbuild")
+
+SOURCES += [
+ "stun_parser_libfuzz.cpp",
+]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/dom/media/webrtc/transport/fuzztest/stun_parser_libfuzz.cpp b/dom/media/webrtc/transport/fuzztest/stun_parser_libfuzz.cpp
new file mode 100644
index 0000000000..73bae5024c
--- /dev/null
+++ b/dom/media/webrtc/transport/fuzztest/stun_parser_libfuzz.cpp
@@ -0,0 +1,35 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <string>
+
+#include "gtest/gtest.h"
+
+#include "FuzzingInterface.h"
+
+extern "C" {
+#include <csi_platform.h>
+#include "stun_msg.h"
+#include "stun_codec.h"
+}
+
+int FuzzingInitStunParser(int* argc, char*** argv) { return 0; }
+
+static int RunStunParserFuzzing(const uint8_t* data, size_t size) {
+ nr_stun_message* req = 0;
+
+ UCHAR* mes = (UCHAR*)data;
+
+ if (!nr_stun_message_create2(&req, mes, size)) {
+ nr_stun_decode_message(req, nullptr, nullptr);
+ nr_stun_message_destroy(&req);
+ }
+
+ return 0;
+}
+
+MOZ_FUZZING_INTERFACE_RAW(FuzzingInitStunParser, RunStunParserFuzzing,
+ StunParser);
diff --git a/dom/media/webrtc/transport/ipc/NrIceStunAddrMessageUtils.h b/dom/media/webrtc/transport/ipc/NrIceStunAddrMessageUtils.h
new file mode 100644
index 0000000000..34ec46726e
--- /dev/null
+++ b/dom/media/webrtc/transport/ipc/NrIceStunAddrMessageUtils.h
@@ -0,0 +1,54 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_net_NrIceStunAddrMessageUtils_h
+#define mozilla_net_NrIceStunAddrMessageUtils_h
+
+// forward declare NrIceStunAddr for --disable-webrtc builds where
+// the header will not be available.
+namespace mozilla {
+class NrIceStunAddr;
+} // namespace mozilla
+
+#include "ipc/IPCMessageUtils.h"
+#ifdef MOZ_WEBRTC
+# include "transport/nricestunaddr.h"
+#endif
+
+namespace IPC {
+
+template <>
+struct ParamTraits<mozilla::NrIceStunAddr> {
+ static void Write(MessageWriter* aWriter,
+ const mozilla::NrIceStunAddr& aParam) {
+#ifdef MOZ_WEBRTC
+ const size_t bufSize = aParam.SerializationBufferSize();
+ char* buffer = new char[bufSize];
+ aParam.Serialize(buffer, bufSize);
+ aWriter->WriteBytes((void*)buffer, bufSize);
+ delete[] buffer;
+#endif
+ }
+
+ static bool Read(MessageReader* aReader, mozilla::NrIceStunAddr* aResult) {
+#ifdef MOZ_WEBRTC
+ const size_t bufSize = aResult->SerializationBufferSize();
+ char* buffer = new char[bufSize];
+ bool result = aReader->ReadBytesInto((void*)buffer, bufSize);
+
+ if (result) {
+ result = result && (NS_OK == aResult->Deserialize(buffer, bufSize));
+ }
+ delete[] buffer;
+
+ return result;
+#else
+ return false;
+#endif
+ }
+};
+
+} // namespace IPC
+
+#endif // mozilla_net_NrIceStunAddrMessageUtils_h
diff --git a/dom/media/webrtc/transport/ipc/PStunAddrsParams.h b/dom/media/webrtc/transport/ipc/PStunAddrsParams.h
new file mode 100644
index 0000000000..315925609d
--- /dev/null
+++ b/dom/media/webrtc/transport/ipc/PStunAddrsParams.h
@@ -0,0 +1,33 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef PStunAddrsParams_h
+#define PStunAddrsParams_h
+
+#include "mozilla/Maybe.h"
+#include "nsTArray.h"
+
+#ifdef MOZ_WEBRTC
+# include "transport/nricestunaddr.h"
+#endif
+
+namespace mozilla {
+namespace net {
+
+// Need to define typedef in .h file--can't seem to in ipdl.h file?
+#ifdef MOZ_WEBRTC
+typedef nsTArray<NrIceStunAddr> NrIceStunAddrArray;
+#else
+// a "dummy" typedef for --disabled-webrtc builds when the definition
+// for NrIceStunAddr is not available (otherwise we get complaints
+// about missing definitions for contructor and destructor)
+typedef nsTArray<int> NrIceStunAddrArray;
+#endif
+
+typedef Maybe<nsCString> MaybeNsCString;
+
+} // namespace net
+} // namespace mozilla
+
+#endif // PStunAddrsParams_h
diff --git a/dom/media/webrtc/transport/ipc/PStunAddrsRequest.ipdl b/dom/media/webrtc/transport/ipc/PStunAddrsRequest.ipdl
new file mode 100644
index 0000000000..c775f73f99
--- /dev/null
+++ b/dom/media/webrtc/transport/ipc/PStunAddrsRequest.ipdl
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PNecko;
+
+using mozilla::net::NrIceStunAddrArray from "mozilla/net/PStunAddrsParams.h";
+using mozilla::net::MaybeNsCString from "mozilla/net/PStunAddrsParams.h";
+
+include "mozilla/net/NrIceStunAddrMessageUtils.h";
+
+namespace mozilla {
+namespace net {
+
+[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual]
+async protocol PStunAddrsRequest
+{
+ manager PNecko;
+
+parent:
+ async GetStunAddrs();
+
+ async RegisterMDNSHostname(nsCString hostname, nsCString address);
+ async QueryMDNSHostname(nsCString hostname);
+ async UnregisterMDNSHostname(nsCString hostname);
+
+ async __delete__();
+
+child:
+ async OnMDNSQueryComplete(nsCString hostname, MaybeNsCString address);
+ async OnStunAddrsAvailable(NrIceStunAddrArray iceStunAddrs);
+};
+
+} // namespace net
+} // namespace mozilla
diff --git a/dom/media/webrtc/transport/ipc/PWebrtcTCPSocket.ipdl b/dom/media/webrtc/transport/ipc/PWebrtcTCPSocket.ipdl
new file mode 100644
index 0000000000..3bc2a89828
--- /dev/null
+++ b/dom/media/webrtc/transport/ipc/PWebrtcTCPSocket.ipdl
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PNecko;
+include protocol PSocketProcess;
+
+include WebrtcProxyConfig;
+
+using mozilla::dom::TabId from "mozilla/dom/ipc/IdType.h";
+
+namespace mozilla {
+namespace net {
+
+[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual]
+async protocol PWebrtcTCPSocket
+{
+ manager PNecko or PSocketProcess;
+
+parent:
+ async AsyncOpen(nsCString aHost,
+ int32_t aPort,
+ nsCString aLocalAddress,
+ int32_t aLocalPort,
+ bool aUseTls,
+ WebrtcProxyConfig? aProxyConfig);
+ async Write(uint8_t[] aWriteData);
+ async Close();
+
+child:
+ async OnClose(nsresult aReason);
+ async OnConnected(nsCString aProxyType);
+ async OnRead(uint8_t[] aReadData);
+
+ async __delete__();
+};
+
+} // namespace net
+} // namespace mozilla
diff --git a/dom/media/webrtc/transport/ipc/StunAddrsRequestChild.cpp b/dom/media/webrtc/transport/ipc/StunAddrsRequestChild.cpp
new file mode 100644
index 0000000000..057fcbd330
--- /dev/null
+++ b/dom/media/webrtc/transport/ipc/StunAddrsRequestChild.cpp
@@ -0,0 +1,45 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "StunAddrsRequestChild.h"
+
+#include "mozilla/net/NeckoChild.h"
+#include "nsISerialEventTarget.h"
+
+using namespace mozilla::ipc;
+
+namespace mozilla::net {
+
+StunAddrsRequestChild::StunAddrsRequestChild(StunAddrsListener* listener)
+ : mListener(listener) {
+ gNeckoChild->SendPStunAddrsRequestConstructor(this);
+ // IPDL holds a reference until IPDL channel gets destroyed
+ AddIPDLReference();
+}
+
+mozilla::ipc::IPCResult StunAddrsRequestChild::RecvOnMDNSQueryComplete(
+ const nsACString& hostname, const Maybe<nsCString>& address) {
+ if (mListener) {
+ mListener->OnMDNSQueryComplete(PromiseFlatCString(hostname), address);
+ }
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult StunAddrsRequestChild::RecvOnStunAddrsAvailable(
+ const NrIceStunAddrArray& addrs) {
+ if (mListener) {
+ mListener->OnStunAddrsAvailable(addrs);
+ }
+ return IPC_OK();
+}
+
+void StunAddrsRequestChild::Cancel() { mListener = nullptr; }
+
+NS_IMPL_ADDREF(StunAddrsRequestChild)
+NS_IMPL_RELEASE(StunAddrsRequestChild)
+
+NS_IMPL_ADDREF(StunAddrsListener)
+NS_IMPL_RELEASE(StunAddrsListener)
+
+} // namespace mozilla::net
diff --git a/dom/media/webrtc/transport/ipc/StunAddrsRequestChild.h b/dom/media/webrtc/transport/ipc/StunAddrsRequestChild.h
new file mode 100644
index 0000000000..f487f52baf
--- /dev/null
+++ b/dom/media/webrtc/transport/ipc/StunAddrsRequestChild.h
@@ -0,0 +1,64 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_net_StunAddrsRequestChild_h
+#define mozilla_net_StunAddrsRequestChild_h
+
+#include "mozilla/net/PStunAddrsRequestChild.h"
+
+class nsISerialEventTarget;
+
+namespace mozilla::net {
+
+class StunAddrsListener {
+ public:
+ virtual void OnMDNSQueryComplete(const nsCString& hostname,
+ const Maybe<nsCString>& address) = 0;
+ virtual void OnStunAddrsAvailable(const NrIceStunAddrArray& addrs) = 0;
+
+ NS_IMETHOD_(MozExternalRefCountType) AddRef();
+ NS_IMETHOD_(MozExternalRefCountType) Release();
+
+ protected:
+ virtual ~StunAddrsListener() = default;
+
+ ThreadSafeAutoRefCnt mRefCnt;
+ NS_DECL_OWNINGTHREAD
+};
+
+class StunAddrsRequestChild final : public PStunAddrsRequestChild {
+ friend class PStunAddrsRequestChild;
+
+ public:
+ explicit StunAddrsRequestChild(StunAddrsListener* listener);
+
+ NS_IMETHOD_(MozExternalRefCountType) AddRef();
+ NS_IMETHOD_(MozExternalRefCountType) Release();
+
+ // Not sure why AddIPDLReference & ReleaseIPDLReference don't come
+ // from PStunAddrsRequestChild since the IPC plumbing seem to
+ // expect this.
+ void AddIPDLReference() { AddRef(); }
+ void ReleaseIPDLReference() { Release(); }
+
+ void Cancel();
+
+ protected:
+ virtual ~StunAddrsRequestChild() = default;
+
+ virtual mozilla::ipc::IPCResult RecvOnMDNSQueryComplete(
+ const nsACString& aHostname, const Maybe<nsCString>& aAddress) override;
+
+ virtual mozilla::ipc::IPCResult RecvOnStunAddrsAvailable(
+ const NrIceStunAddrArray& addrs) override;
+
+ RefPtr<StunAddrsListener> mListener;
+
+ ThreadSafeAutoRefCnt mRefCnt;
+ NS_DECL_OWNINGTHREAD
+};
+
+} // namespace mozilla::net
+
+#endif // mozilla_net_StunAddrsRequestChild_h
diff --git a/dom/media/webrtc/transport/ipc/StunAddrsRequestParent.cpp b/dom/media/webrtc/transport/ipc/StunAddrsRequestParent.cpp
new file mode 100644
index 0000000000..23ef6dea73
--- /dev/null
+++ b/dom/media/webrtc/transport/ipc/StunAddrsRequestParent.cpp
@@ -0,0 +1,262 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "StunAddrsRequestParent.h"
+
+#include "../runnable_utils.h"
+#include "mozilla/StaticPtr.h"
+#include "nsIThread.h"
+#include "nsNetUtil.h"
+
+#include "transport/nricectx.h"
+#include "transport/nricemediastream.h" // needed only for including nricectx.h
+#include "transport/nricestunaddr.h"
+
+#include "../mdns_service/mdns_service.h"
+
+extern "C" {
+#include "local_addr.h"
+}
+
+using namespace mozilla::ipc;
+
+namespace mozilla::net {
+
+static void mdns_service_resolved(void* cb, const char* hostname,
+ const char* addr) {
+ StunAddrsRequestParent* self = static_cast<StunAddrsRequestParent*>(cb);
+ self->OnQueryComplete(nsCString(hostname), Some(nsCString(addr)));
+}
+
+void mdns_service_timedout(void* cb, const char* hostname) {
+ StunAddrsRequestParent* self = static_cast<StunAddrsRequestParent*>(cb);
+ self->OnQueryComplete(nsCString(hostname), Nothing());
+}
+
+StunAddrsRequestParent::StunAddrsRequestParent() : mIPCClosed(false) {
+ NS_GetMainThread(getter_AddRefs(mMainThread));
+
+ nsresult res;
+ mSTSThread = do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &res);
+ MOZ_ASSERT(mSTSThread);
+}
+
+StunAddrsRequestParent::~StunAddrsRequestParent() {
+ ASSERT_ON_THREAD(mMainThread);
+}
+
+mozilla::ipc::IPCResult StunAddrsRequestParent::RecvGetStunAddrs() {
+ ASSERT_ON_THREAD(mMainThread);
+
+ if (mIPCClosed) {
+ return IPC_OK();
+ }
+
+ RUN_ON_THREAD(mSTSThread,
+ WrapRunnable(RefPtr<StunAddrsRequestParent>(this),
+ &StunAddrsRequestParent::GetStunAddrs_s),
+ NS_DISPATCH_NORMAL);
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult StunAddrsRequestParent::RecvRegisterMDNSHostname(
+ const nsACString& aHostname, const nsACString& aAddress) {
+ ASSERT_ON_THREAD(mMainThread);
+
+ if (mIPCClosed) {
+ return IPC_OK();
+ }
+
+ if (mSharedMDNSService) {
+ mSharedMDNSService->RegisterHostname(aHostname.BeginReading(),
+ aAddress.BeginReading());
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult StunAddrsRequestParent::RecvQueryMDNSHostname(
+ const nsACString& aHostname) {
+ ASSERT_ON_THREAD(mMainThread);
+
+ if (mIPCClosed) {
+ return IPC_OK();
+ }
+
+ if (mSharedMDNSService) {
+ mSharedMDNSService->QueryHostname(this, aHostname.BeginReading());
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult StunAddrsRequestParent::RecvUnregisterMDNSHostname(
+ const nsACString& aHostname) {
+ ASSERT_ON_THREAD(mMainThread);
+
+ if (mIPCClosed) {
+ return IPC_OK();
+ }
+
+ if (mSharedMDNSService) {
+ mSharedMDNSService->UnregisterHostname(aHostname.BeginReading());
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult StunAddrsRequestParent::Recv__delete__() {
+ // see note below in ActorDestroy
+ mIPCClosed = true;
+ return IPC_OK();
+}
+
+void StunAddrsRequestParent::OnQueryComplete(const nsACString& hostname,
+ const Maybe<nsCString>& address) {
+ RUN_ON_THREAD(mMainThread,
+ WrapRunnable(RefPtr<StunAddrsRequestParent>(this),
+ &StunAddrsRequestParent::OnQueryComplete_m,
+ nsCString(hostname), address),
+ NS_DISPATCH_NORMAL);
+}
+
+void StunAddrsRequestParent::ActorDestroy(ActorDestroyReason why) {
+ // We may still have refcount>0 if we haven't made it through
+ // GetStunAddrs_s and SendStunAddrs_m yet, but child process
+ // has crashed. We must not send any more msgs to child, or
+ // IPDL will kill chrome process, too.
+ mIPCClosed = true;
+
+ // We need to stop the mDNS service here to ensure that we don't
+ // end up with any messages queued for the main thread after the
+ // destructors run. Because of Bug 1569311, all of the
+ // StunAddrsRequestParent instances end up being destroyed one
+ // after the other, so it is ok to free the shared service when
+ // the first one is destroyed rather than waiting for the last one.
+ // If this behaviour changes, we would potentially end up starting
+ // and stopping instances repeatedly and should add a refcount and
+ // a way of cancelling pending queries to avoid churn in that case.
+ if (mSharedMDNSService) {
+ mSharedMDNSService = nullptr;
+ }
+}
+
+void StunAddrsRequestParent::GetStunAddrs_s() {
+ ASSERT_ON_THREAD(mSTSThread);
+
+ // get the stun addresses while on STS thread
+ NrIceStunAddrArray addrs = NrIceCtx::GetStunAddrs();
+
+ if (mIPCClosed) {
+ return;
+ }
+
+ // in order to return the result over IPC, we need to be on main thread
+ RUN_ON_THREAD(
+ mMainThread,
+ WrapRunnable(RefPtr<StunAddrsRequestParent>(this),
+ &StunAddrsRequestParent::SendStunAddrs_m, std::move(addrs)),
+ NS_DISPATCH_NORMAL);
+}
+
+void StunAddrsRequestParent::SendStunAddrs_m(const NrIceStunAddrArray& addrs) {
+ ASSERT_ON_THREAD(mMainThread);
+
+ if (mIPCClosed) {
+ // nothing to do: child probably crashed
+ return;
+ }
+
+ // This means that the mDNS service will continue running until shutdown
+ // once started. The StunAddrsRequestParent destructor does not run until
+ // shutdown anyway (see Bug 1569311), so there is not much we can do about
+ // this here. One option would be to add a check if there are no hostnames
+ // registered after UnregisterHostname is called, and if so, stop the mDNS
+ // service at that time (see Bug 1569955.)
+ if (!mSharedMDNSService) {
+ std::ostringstream o;
+ char buffer[16];
+ for (auto& addr : addrs) {
+ if (addr.localAddr().addr.ip_version == NR_IPV4 &&
+ !nr_transport_addr_is_loopback(&addr.localAddr().addr)) {
+ nr_transport_addr_get_addrstring(&addr.localAddr().addr, buffer, 16);
+ o << buffer << ";";
+ }
+ }
+ std::string addrstring = o.str();
+ if (!addrstring.empty()) {
+ mSharedMDNSService = new MDNSServiceWrapper(addrstring);
+ }
+ }
+
+ // send the new addresses back to the child
+ Unused << SendOnStunAddrsAvailable(addrs);
+}
+
+void StunAddrsRequestParent::OnQueryComplete_m(
+ const nsACString& hostname, const Maybe<nsCString>& address) {
+ ASSERT_ON_THREAD(mMainThread);
+
+ if (mIPCClosed) {
+ // nothing to do: child probably crashed
+ return;
+ }
+
+ // send the hostname and address back to the child
+ Unused << SendOnMDNSQueryComplete(hostname, address);
+}
+
+StaticRefPtr<StunAddrsRequestParent::MDNSServiceWrapper>
+ StunAddrsRequestParent::mSharedMDNSService;
+
+NS_IMPL_ADDREF(StunAddrsRequestParent)
+NS_IMPL_RELEASE(StunAddrsRequestParent)
+
+StunAddrsRequestParent::MDNSServiceWrapper::MDNSServiceWrapper(
+ const std::string& ifaddr)
+ : ifaddr(ifaddr) {}
+
+void StunAddrsRequestParent::MDNSServiceWrapper::RegisterHostname(
+ const char* hostname, const char* address) {
+ StartIfRequired();
+ if (mMDNSService) {
+ mdns_service_register_hostname(mMDNSService, hostname, address);
+ }
+}
+
+void StunAddrsRequestParent::MDNSServiceWrapper::QueryHostname(
+ void* data, const char* hostname) {
+ StartIfRequired();
+ if (mMDNSService) {
+ mdns_service_query_hostname(mMDNSService, data, mdns_service_resolved,
+ mdns_service_timedout, hostname);
+ }
+}
+
+void StunAddrsRequestParent::MDNSServiceWrapper::UnregisterHostname(
+ const char* hostname) {
+ StartIfRequired();
+ if (mMDNSService) {
+ mdns_service_unregister_hostname(mMDNSService, hostname);
+ }
+}
+
+StunAddrsRequestParent::MDNSServiceWrapper::~MDNSServiceWrapper() {
+ if (mMDNSService) {
+ mdns_service_stop(mMDNSService);
+ mMDNSService = nullptr;
+ }
+}
+
+void StunAddrsRequestParent::MDNSServiceWrapper::StartIfRequired() {
+ if (!mMDNSService) {
+ mMDNSService = mdns_service_start(ifaddr.c_str());
+ }
+}
+
+NS_IMPL_ADDREF(StunAddrsRequestParent::MDNSServiceWrapper)
+NS_IMPL_RELEASE(StunAddrsRequestParent::MDNSServiceWrapper)
+
+} // namespace mozilla::net
diff --git a/dom/media/webrtc/transport/ipc/StunAddrsRequestParent.h b/dom/media/webrtc/transport/ipc/StunAddrsRequestParent.h
new file mode 100644
index 0000000000..33e71abbc7
--- /dev/null
+++ b/dom/media/webrtc/transport/ipc/StunAddrsRequestParent.h
@@ -0,0 +1,82 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_net_StunAddrsRequestParent_h
+#define mozilla_net_StunAddrsRequestParent_h
+
+#include "mozilla/net/PStunAddrsRequestParent.h"
+
+struct MDNSService;
+
+namespace mozilla::net {
+
+class StunAddrsRequestParent : public PStunAddrsRequestParent {
+ friend class PStunAddrsRequestParent;
+
+ public:
+ StunAddrsRequestParent();
+
+ NS_IMETHOD_(MozExternalRefCountType) AddRef();
+ NS_IMETHOD_(MozExternalRefCountType) Release();
+
+ mozilla::ipc::IPCResult Recv__delete__() override;
+
+ void OnQueryComplete(const nsACString& hostname,
+ const Maybe<nsCString>& address);
+
+ protected:
+ virtual ~StunAddrsRequestParent();
+
+ virtual mozilla::ipc::IPCResult RecvGetStunAddrs() override;
+ virtual mozilla::ipc::IPCResult RecvRegisterMDNSHostname(
+ const nsACString& hostname, const nsACString& address) override;
+ virtual mozilla::ipc::IPCResult RecvQueryMDNSHostname(
+ const nsACString& hostname) override;
+ virtual mozilla::ipc::IPCResult RecvUnregisterMDNSHostname(
+ const nsACString& hostname) override;
+ virtual void ActorDestroy(ActorDestroyReason why) override;
+
+ nsCOMPtr<nsIThread> mMainThread;
+ nsCOMPtr<nsISerialEventTarget> mSTSThread;
+
+ void GetStunAddrs_s();
+ void SendStunAddrs_m(const NrIceStunAddrArray& addrs);
+
+ void OnQueryComplete_m(const nsACString& hostname,
+ const Maybe<nsCString>& address);
+
+ ThreadSafeAutoRefCnt mRefCnt;
+ NS_DECL_OWNINGTHREAD
+
+ private:
+ bool mIPCClosed; // true if IPDL channel has been closed (child crash)
+
+ class MDNSServiceWrapper {
+ public:
+ explicit MDNSServiceWrapper(const std::string& ifaddr);
+ void RegisterHostname(const char* hostname, const char* address);
+ void QueryHostname(void* data, const char* hostname);
+ void UnregisterHostname(const char* hostname);
+
+ NS_IMETHOD_(MozExternalRefCountType) AddRef();
+ NS_IMETHOD_(MozExternalRefCountType) Release();
+
+ protected:
+ ThreadSafeAutoRefCnt mRefCnt;
+ NS_DECL_OWNINGTHREAD
+
+ private:
+ virtual ~MDNSServiceWrapper();
+ void StartIfRequired();
+
+ std::string ifaddr;
+ MDNSService* mMDNSService = nullptr;
+ };
+
+ static StaticRefPtr<MDNSServiceWrapper> mSharedMDNSService;
+};
+
+} // namespace mozilla::net
+
+#endif // mozilla_net_StunAddrsRequestParent_h
diff --git a/dom/media/webrtc/transport/ipc/WebrtcProxyConfig.ipdlh b/dom/media/webrtc/transport/ipc/WebrtcProxyConfig.ipdlh
new file mode 100644
index 0000000000..e93e82a6a3
--- /dev/null
+++ b/dom/media/webrtc/transport/ipc/WebrtcProxyConfig.ipdlh
@@ -0,0 +1,23 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+using mozilla::dom::TabId from "mozilla/dom/ipc/IdType.h";
+include NeckoChannelParams;
+
+namespace mozilla {
+namespace net {
+
+struct WebrtcProxyConfig {
+ TabId tabId;
+ nsCString alpn;
+ LoadInfoArgs loadInfoArgs;
+ bool forceProxy;
+};
+
+} // namespace net
+} // namespace mozilla
+
diff --git a/dom/media/webrtc/transport/ipc/WebrtcTCPSocket.cpp b/dom/media/webrtc/transport/ipc/WebrtcTCPSocket.cpp
new file mode 100644
index 0000000000..447b4cc741
--- /dev/null
+++ b/dom/media/webrtc/transport/ipc/WebrtcTCPSocket.cpp
@@ -0,0 +1,785 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WebrtcTCPSocket.h"
+
+#include "nsHttpChannel.h"
+#include "nsIChannel.h"
+#include "nsIClassOfService.h"
+#include "nsIContentPolicy.h"
+#include "nsIIOService.h"
+#include "nsILoadInfo.h"
+#include "nsIProtocolProxyService.h"
+#include "nsIURIMutator.h"
+#include "nsICookieJarSettings.h"
+#include "nsProxyRelease.h"
+#include "nsString.h"
+#include "mozilla/dom/ContentProcessManager.h"
+#include "mozilla/dom/BrowserParent.h"
+#include "nsISocketTransportService.h"
+#include "nsICancelable.h"
+#include "nsSocketTransportService2.h"
+
+#include "WebrtcTCPSocketCallback.h"
+#include "WebrtcTCPSocketLog.h"
+
+namespace mozilla::net {
+
+class WebrtcTCPData {
+ public:
+ explicit WebrtcTCPData(nsTArray<uint8_t>&& aData) : mData(std::move(aData)) {
+ MOZ_COUNT_CTOR(WebrtcTCPData);
+ }
+
+ MOZ_COUNTED_DTOR(WebrtcTCPData)
+
+ const nsTArray<uint8_t>& GetData() const { return mData; }
+
+ private:
+ nsTArray<uint8_t> mData;
+};
+
+NS_IMPL_ISUPPORTS(WebrtcTCPSocket, nsIAuthPromptProvider,
+ nsIHttpUpgradeListener, nsIInputStreamCallback,
+ nsIInterfaceRequestor, nsIOutputStreamCallback,
+ nsIRequestObserver, nsIStreamListener,
+ nsIProtocolProxyCallback)
+
+WebrtcTCPSocket::WebrtcTCPSocket(WebrtcTCPSocketCallback* aCallbacks)
+ : mProxyCallbacks(aCallbacks),
+ mClosed(false),
+ mOpened(false),
+ mWriteOffset(0),
+ mAuthProvider(nullptr),
+ mTransport(nullptr),
+ mSocketIn(nullptr),
+ mSocketOut(nullptr) {
+ LOG(("WebrtcTCPSocket::WebrtcTCPSocket %p\n", this));
+ mMainThread = GetMainThreadSerialEventTarget();
+ mSocketThread = do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID);
+ MOZ_RELEASE_ASSERT(mMainThread, "no main thread");
+ MOZ_RELEASE_ASSERT(mSocketThread, "no socket thread");
+}
+
+WebrtcTCPSocket::~WebrtcTCPSocket() {
+ LOG(("WebrtcTCPSocket::~WebrtcTCPSocket %p\n", this));
+
+ NS_ProxyRelease("WebrtcTCPSocket::CleanUpAuthProvider", mMainThread,
+ mAuthProvider.forget());
+}
+
+void WebrtcTCPSocket::SetTabId(dom::TabId aTabId) {
+ MOZ_ASSERT(NS_IsMainThread());
+ dom::ContentProcessManager* cpm = dom::ContentProcessManager::GetSingleton();
+ if (cpm) {
+ dom::ContentParentId cpId = cpm->GetTabProcessId(aTabId);
+ mAuthProvider = cpm->GetBrowserParentByProcessAndTabId(cpId, aTabId);
+ }
+}
+
+nsresult WebrtcTCPSocket::Write(nsTArray<uint8_t>&& aWriteData) {
+ LOG(("WebrtcTCPSocket::Write %p\n", this));
+ MOZ_ASSERT(NS_IsMainThread());
+ nsresult rv = mSocketThread->Dispatch(NewRunnableMethod<nsTArray<uint8_t>&&>(
+ "WebrtcTCPSocket::Write", this, &WebrtcTCPSocket::EnqueueWrite_s,
+ std::move(aWriteData)));
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to dispatch to STS");
+
+ return rv;
+}
+
+nsresult WebrtcTCPSocket::Close() {
+ LOG(("WebrtcTCPSocket::Close %p\n", this));
+
+ CloseWithReason(NS_OK);
+
+ return NS_OK;
+}
+
+void WebrtcTCPSocket::CloseWithReason(nsresult aReason) {
+ LOG(("WebrtcTCPSocket::CloseWithReason %p reason=%u\n", this,
+ static_cast<uint32_t>(aReason)));
+
+ if (!OnSocketThread()) {
+ MOZ_ASSERT(NS_IsMainThread(), "not on main thread");
+
+ // Let's pretend we got an open even if we didn't to prevent an Open later.
+ mOpened = true;
+
+ DebugOnly<nsresult> rv =
+ mSocketThread->Dispatch(NewRunnableMethod<nsresult>(
+ "WebrtcTCPSocket::CloseWithReason", this,
+ &WebrtcTCPSocket::CloseWithReason, aReason));
+
+ // This was MOZ_ALWAYS_SUCCEEDS, but that now uses NS_WARNING_ASSERTION.
+ // In order to convert this back to MOZ_ALWAYS_SUCCEEDS we would need
+ // OnSocketThread to return true if we're shutting down and doing the
+ // "running all of STS's queued events on main" thing.
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to dispatch to STS");
+
+ return;
+ }
+
+ if (mClosed) {
+ return;
+ }
+
+ mClosed = true;
+
+ if (mSocketIn) {
+ mSocketIn->AsyncWait(nullptr, 0, 0, nullptr);
+ mSocketIn = nullptr;
+ }
+
+ if (mSocketOut) {
+ mSocketOut->AsyncWait(nullptr, 0, 0, nullptr);
+ mSocketOut = nullptr;
+ }
+
+ if (mTransport) {
+ mTransport->Close(NS_BASE_STREAM_CLOSED);
+ mTransport = nullptr;
+ }
+
+ NS_ProxyRelease("WebrtcTCPSocket::CleanUpAuthProvider", mMainThread,
+ mAuthProvider.forget());
+ InvokeOnClose(aReason);
+}
+
+nsresult WebrtcTCPSocket::Open(
+ const nsACString& aHost, const int& aPort, const nsACString& aLocalAddress,
+ const int& aLocalPort, bool aUseTls,
+ const Maybe<net::WebrtcProxyConfig>& aProxyConfig) {
+ LOG(("WebrtcTCPSocket::Open %p remote-host=%s local-addr=%s local-port=%d",
+ this, PromiseFlatCString(aHost).get(),
+ PromiseFlatCString(aLocalAddress).get(), aLocalPort));
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NS_WARN_IF(mOpened)) {
+ LOG(("WebrtcTCPSocket %p: TCP socket already open\n", this));
+ CloseWithReason(NS_ERROR_FAILURE);
+ return NS_ERROR_FAILURE;
+ }
+
+ mOpened = true;
+ const nsLiteralCString schemePrefix = aUseTls ? "https://"_ns : "http://"_ns;
+ nsAutoCString spec(schemePrefix);
+
+ bool ipv6Literal = aHost.Find(":") != kNotFound;
+ if (ipv6Literal) {
+ spec += "[";
+ spec += aHost;
+ spec += "]";
+ } else {
+ spec += aHost;
+ }
+
+ nsresult rv = NS_MutateURI(NS_STANDARDURLMUTATOR_CONTRACTID)
+ .SetSpec(spec)
+ .SetPort(aPort)
+ .Finalize(mURI);
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ CloseWithReason(NS_ERROR_FAILURE);
+ return NS_ERROR_FAILURE;
+ }
+
+ mTls = aUseTls;
+ mLocalAddress = aLocalAddress;
+ mLocalPort = aLocalPort;
+ mProxyConfig = aProxyConfig;
+
+ if (!mProxyConfig.isSome()) {
+ OpenWithoutHttpProxy(nullptr);
+ return NS_OK;
+ }
+
+ // We need to figure out whether a proxy needs to be used for mURI before
+ // we can start on establishing a connection.
+ rv = DoProxyConfigLookup();
+
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ CloseWithReason(rv);
+ }
+
+ return rv;
+}
+
+nsresult WebrtcTCPSocket::DoProxyConfigLookup() {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsresult rv;
+ nsCOMPtr<nsIProtocolProxyService> pps =
+ do_GetService(NS_PROTOCOLPROXYSERVICE_CONTRACTID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ nsCOMPtr<nsIChannel> channel;
+ rv = NS_NewChannel(getter_AddRefs(channel), mURI,
+ nsContentUtils::GetSystemPrincipal(),
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_OTHER);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = pps->AsyncResolve(channel,
+ nsIProtocolProxyService::RESOLVE_PREFER_HTTPS_PROXY |
+ nsIProtocolProxyService::RESOLVE_ALWAYS_TUNNEL,
+ this, nullptr, getter_AddRefs(mProxyRequest));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ // We pick back up in OnProxyAvailable
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP WebrtcTCPSocket::OnProxyAvailable(nsICancelable* aRequest,
+ nsIChannel* aChannel,
+ nsIProxyInfo* aProxyinfo,
+ nsresult aResult) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mProxyRequest = nullptr;
+
+ if (NS_SUCCEEDED(aResult) && aProxyinfo) {
+ nsresult rv = aProxyinfo->GetType(mProxyType);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ CloseWithReason(rv);
+ return rv;
+ }
+
+ if (mProxyType == "http" || mProxyType == "https") {
+ rv = OpenWithHttpProxy();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ CloseWithReason(rv);
+ }
+ return rv;
+ }
+
+ if (mProxyType == "socks" || mProxyType == "socks4" ||
+ mProxyType == "direct") {
+ OpenWithoutHttpProxy(aProxyinfo);
+ return NS_OK;
+ }
+ }
+
+ OpenWithoutHttpProxy(nullptr);
+
+ return NS_OK;
+}
+
+void WebrtcTCPSocket::OpenWithoutHttpProxy(nsIProxyInfo* aSocksProxyInfo) {
+ if (!OnSocketThread()) {
+ DebugOnly<nsresult> rv =
+ mSocketThread->Dispatch(NewRunnableMethod<nsCOMPtr<nsIProxyInfo>>(
+ "WebrtcTCPSocket::OpenWithoutHttpProxy", this,
+ &WebrtcTCPSocket::OpenWithoutHttpProxy, aSocksProxyInfo));
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to dispatch to STS");
+ return;
+ }
+
+ LOG(("WebrtcTCPSocket::OpenWithoutHttpProxy %p\n", this));
+
+ if (mClosed) {
+ return;
+ }
+
+ if (NS_WARN_IF(mProxyConfig.isSome() && mProxyConfig->forceProxy() &&
+ !aSocksProxyInfo)) {
+ CloseWithReason(NS_ERROR_FAILURE);
+ return;
+ }
+
+ nsCString host;
+ int32_t port;
+
+ nsresult rv = mURI->GetHost(host);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ CloseWithReason(rv);
+ return;
+ }
+
+ rv = mURI->GetPort(&port);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ CloseWithReason(rv);
+ return;
+ }
+
+ AutoTArray<nsCString, 1> socketTypes;
+ if (mTls) {
+ socketTypes.AppendElement("ssl"_ns);
+ }
+
+ nsCOMPtr<nsISocketTransportService> sts =
+ do_GetService("@mozilla.org/network/socket-transport-service;1");
+ rv = sts->CreateTransport(socketTypes, host, port, aSocksProxyInfo, nullptr,
+ getter_AddRefs(mTransport));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ CloseWithReason(rv);
+ return;
+ }
+
+ mTransport->SetReuseAddrPort(true);
+
+ PRNetAddr prAddr;
+ if (NS_WARN_IF(PR_SUCCESS !=
+ PR_InitializeNetAddr(PR_IpAddrAny, mLocalPort, &prAddr))) {
+ CloseWithReason(NS_ERROR_FAILURE);
+ return;
+ }
+
+ if (NS_WARN_IF(PR_SUCCESS !=
+ PR_StringToNetAddr(mLocalAddress.BeginReading(), &prAddr))) {
+ CloseWithReason(NS_ERROR_FAILURE);
+ return;
+ }
+
+ mozilla::net::NetAddr addr(&prAddr);
+ rv = mTransport->Bind(&addr);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ CloseWithReason(rv);
+ return;
+ }
+
+ // Binding to a V4 address is not sufficient to cause this socket to use
+ // V4, and the same goes for V6. So, we disable as needed here.
+ uint32_t flags = 0;
+ if (addr.raw.family == AF_INET) {
+ flags |= nsISocketTransport::DISABLE_IPV6;
+ } else if (addr.raw.family == AF_INET6) {
+ flags |= nsISocketTransport::DISABLE_IPV4;
+ } else {
+ MOZ_CRASH();
+ }
+
+ mTransport->SetConnectionFlags(flags);
+
+ nsCOMPtr<nsIInputStream> socketIn;
+ rv = mTransport->OpenInputStream(0, 0, 0, getter_AddRefs(socketIn));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ CloseWithReason(rv);
+ return;
+ }
+ mSocketIn = do_QueryInterface(socketIn);
+ if (NS_WARN_IF(!mSocketIn)) {
+ CloseWithReason(NS_ERROR_NULL_POINTER);
+ return;
+ }
+
+ nsCOMPtr<nsIOutputStream> socketOut;
+ rv = mTransport->OpenOutputStream(nsITransport::OPEN_UNBUFFERED, 0, 0,
+ getter_AddRefs(socketOut));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ CloseWithReason(rv);
+ return;
+ }
+ mSocketOut = do_QueryInterface(socketOut);
+ if (NS_WARN_IF(!mSocketOut)) {
+ CloseWithReason(NS_ERROR_NULL_POINTER);
+ return;
+ }
+
+ FinishOpen();
+}
+
+nsresult WebrtcTCPSocket::OpenWithHttpProxy() {
+ MOZ_ASSERT(NS_IsMainThread(), "not on main thread");
+ LOG(("WebrtcTCPSocket::OpenWithHttpProxy %p\n", this));
+ nsresult rv;
+ nsCOMPtr<nsIIOService> ioService;
+ ioService = do_GetService(NS_IOSERVICE_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) {
+ LOG(("WebrtcTCPSocket %p: io service missing\n", this));
+ return rv;
+ }
+
+ nsCOMPtr<nsILoadInfo> loadInfo;
+ Maybe<net::LoadInfoArgs> loadInfoArgs = Some(mProxyConfig->loadInfoArgs());
+
+ // FIXME: We don't know the remote type of the process which provided these
+ // LoadInfoArgs. Pass in `NOT_REMOTE_TYPE` as the origin process to blindly
+ // accept whatever value was passed by the other side for now, as we aren't
+ // using it for security checks here.
+ // If this code ever starts checking the triggering remote type, this needs to
+ // be changed.
+ rv = ipc::LoadInfoArgsToLoadInfo(loadInfoArgs, NOT_REMOTE_TYPE,
+ getter_AddRefs(loadInfo));
+ if (NS_FAILED(rv)) {
+ LOG(("WebrtcTCPSocket %p: could not init load info\n", this));
+ return rv;
+ }
+
+ // -need to always tunnel since we're using a proxy
+ // -there shouldn't be an opportunity to send cookies, but explicitly disallow
+ // them anyway.
+ // -the previous proxy tunnel didn't support redirects e.g. 307. don't need to
+ // introduce new behavior. can't follow redirects on connect anyway.
+ nsCOMPtr<nsIChannel> localChannel;
+ rv = ioService->NewChannelFromURIWithProxyFlags(
+ mURI, nullptr,
+ // Proxy flags are overridden by SetConnectOnly()
+ 0, loadInfo->LoadingNode(), loadInfo->GetLoadingPrincipal(),
+ loadInfo->TriggeringPrincipal(),
+ nsILoadInfo::SEC_COOKIES_OMIT |
+ // We need this flag to allow loads from any origin since this channel
+ // is being used to CONNECT to an HTTP proxy.
+ nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_SEC_CONTEXT_IS_NULL,
+ nsIContentPolicy::TYPE_PROXIED_WEBRTC_MEDIA,
+ getter_AddRefs(localChannel));
+ if (NS_FAILED(rv)) {
+ LOG(("WebrtcTCPSocket %p: bad open channel\n", this));
+ return rv;
+ }
+
+ nsCOMPtr<nsILoadInfo> channelLoadInfo = localChannel->LoadInfo();
+ nsCOMPtr<nsICookieJarSettings> cookieJarSettings;
+ loadInfo->GetCookieJarSettings(getter_AddRefs(cookieJarSettings));
+ channelLoadInfo->SetCookieJarSettings(cookieJarSettings);
+
+ RefPtr<nsHttpChannel> httpChannel;
+ CallQueryInterface(localChannel, httpChannel.StartAssignment());
+
+ if (!httpChannel) {
+ LOG(("WebrtcTCPSocket %p: not an http channel\n", this));
+ return NS_ERROR_FAILURE;
+ }
+
+ httpChannel->SetNotificationCallbacks(this);
+
+ // don't block webrtc proxy setup with other requests
+ // often more than one of these channels will be created all at once by ICE
+ nsCOMPtr<nsIClassOfService> cos = do_QueryInterface(localChannel);
+ if (cos) {
+ cos->AddClassFlags(nsIClassOfService::Unblocked |
+ nsIClassOfService::DontThrottle);
+ } else {
+ LOG(("WebrtcTCPSocket %p: could not set class of service\n", this));
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = httpChannel->HTTPUpgrade(mProxyConfig->alpn(), this);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+ rv = httpChannel->SetConnectOnly();
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ rv = httpChannel->AsyncOpen(this);
+
+ if (NS_FAILED(rv)) {
+ LOG(("WebrtcTCPSocket %p: cannot async open\n", this));
+ return rv;
+ }
+
+ // This picks back up in OnTransportAvailable once we have connected to the
+ // proxy, and performed the http upgrade to switch the proxy into passthrough
+ // mode.
+
+ return NS_OK;
+}
+
+void WebrtcTCPSocket::EnqueueWrite_s(nsTArray<uint8_t>&& aWriteData) {
+ LOG(("WebrtcTCPSocket::EnqueueWrite %p\n", this));
+ MOZ_ASSERT(OnSocketThread(), "not on socket thread");
+
+ if (mClosed) {
+ return;
+ }
+
+ mWriteQueue.emplace_back(std::move(aWriteData));
+
+ if (mSocketOut) {
+ mSocketOut->AsyncWait(this, 0, 0, nullptr);
+ }
+}
+
+void WebrtcTCPSocket::InvokeOnClose(nsresult aReason) {
+ LOG(("WebrtcTCPSocket::InvokeOnClose %p\n", this));
+
+ if (!NS_IsMainThread()) {
+ MOZ_ALWAYS_SUCCEEDS(mMainThread->Dispatch(
+ NewRunnableMethod<nsresult>("WebrtcTCPSocket::InvokeOnClose", this,
+ &WebrtcTCPSocket::InvokeOnClose, aReason)));
+ return;
+ }
+
+ MOZ_ASSERT(mProxyCallbacks, "webrtc TCP callback should be non-null");
+
+ if (mProxyRequest) {
+ mProxyRequest->Cancel(aReason);
+ mProxyRequest = nullptr;
+ }
+
+ mProxyCallbacks->OnClose(aReason);
+ mProxyCallbacks = nullptr;
+}
+
+void WebrtcTCPSocket::InvokeOnConnected() {
+ LOG(("WebrtcTCPSocket::InvokeOnConnected %p\n", this));
+
+ if (!NS_IsMainThread()) {
+ MOZ_ALWAYS_SUCCEEDS(mMainThread->Dispatch(
+ NewRunnableMethod("WebrtcTCPSocket::InvokeOnConnected", this,
+ &WebrtcTCPSocket::InvokeOnConnected)));
+ return;
+ }
+
+ MOZ_ASSERT(mProxyCallbacks, "webrtc TCP callback should be non-null");
+
+ mProxyCallbacks->OnConnected(mProxyType);
+}
+
+void WebrtcTCPSocket::InvokeOnRead(nsTArray<uint8_t>&& aReadData) {
+ LOG(("WebrtcTCPSocket::InvokeOnRead %p count=%zu\n", this,
+ aReadData.Length()));
+
+ if (!NS_IsMainThread()) {
+ MOZ_ALWAYS_SUCCEEDS(
+ mMainThread->Dispatch(NewRunnableMethod<nsTArray<uint8_t>&&>(
+ "WebrtcTCPSocket::InvokeOnRead", this,
+ &WebrtcTCPSocket::InvokeOnRead, std::move(aReadData))));
+ return;
+ }
+
+ MOZ_ASSERT(mProxyCallbacks, "webrtc TCP callback should be non-null");
+
+ mProxyCallbacks->OnRead(std::move(aReadData));
+}
+
+// nsIHttpUpgradeListener
+NS_IMETHODIMP
+WebrtcTCPSocket::OnTransportAvailable(nsISocketTransport* aTransport,
+ nsIAsyncInputStream* aSocketIn,
+ nsIAsyncOutputStream* aSocketOut) {
+ // This is called only in the http proxy case, once we have connected to the
+ // http proxy and performed the http upgrade to switch it over to passthrough
+ // mode. That process is started async by OpenWithHttpProxy.
+ LOG(("WebrtcTCPSocket::OnTransportAvailable %p\n", this));
+ MOZ_ASSERT(OnSocketThread(), "not on socket thread");
+ MOZ_ASSERT(!mTransport,
+ "already called transport available on webrtc TCP socket");
+
+ // Cancel any pending callbacks. The caller doesn't always cancel these
+ // awaits. We need to make sure they don't get them.
+ aSocketIn->AsyncWait(nullptr, 0, 0, nullptr);
+ aSocketOut->AsyncWait(nullptr, 0, 0, nullptr);
+
+ if (mClosed) {
+ LOG(("WebrtcTCPSocket::OnTransportAvailable %p closed\n", this));
+ return NS_OK;
+ }
+
+ mTransport = aTransport;
+ mSocketIn = aSocketIn;
+ mSocketOut = aSocketOut;
+
+ // pulled from nr_socket_prsock.cpp
+ uint32_t minBufferSize = 256 * 1024;
+ nsresult rv = mTransport->SetSendBufferSize(minBufferSize);
+ if (NS_FAILED(rv)) {
+ LOG(("WebrtcProxyChannel::OnTransportAvailable %p send failed\n", this));
+ CloseWithReason(rv);
+ return rv;
+ }
+ rv = mTransport->SetRecvBufferSize(minBufferSize);
+ if (NS_FAILED(rv)) {
+ LOG(("WebrtcProxyChannel::OnTransportAvailable %p recv failed\n", this));
+ CloseWithReason(rv);
+ return rv;
+ }
+
+ FinishOpen();
+ return NS_OK;
+}
+
+void WebrtcTCPSocket::FinishOpen() {
+ MOZ_ASSERT(OnSocketThread());
+ // mTransport, mSocketIn, and mSocketOut are all set. We may have set them in
+ // OnTransportAvailable (in the http/https proxy case), or in
+ // OpenWithoutHttpProxy. From here on out, this class functions the same for
+ // these two cases.
+
+ mSocketIn->AsyncWait(this, 0, 0, nullptr);
+
+ InvokeOnConnected();
+}
+
+NS_IMETHODIMP
+WebrtcTCPSocket::OnUpgradeFailed(nsresult aErrorCode) {
+ LOG(("WebrtcTCPSocket::OnUpgradeFailed %p\n", this));
+ MOZ_ASSERT(OnSocketThread(), "not on socket thread");
+ MOZ_ASSERT(!mTransport,
+ "already called transport available on webrtc TCP socket");
+
+ if (mClosed) {
+ LOG(("WebrtcTCPSocket::OnUpgradeFailed %p closed\n", this));
+ return NS_OK;
+ }
+
+ CloseWithReason(aErrorCode);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WebrtcTCPSocket::OnWebSocketConnectionAvailable(
+ WebSocketConnectionBase* aConnection) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+}
+
+// nsIRequestObserver (from nsIStreamListener)
+NS_IMETHODIMP
+WebrtcTCPSocket::OnStartRequest(nsIRequest* aRequest) {
+ LOG(("WebrtcTCPSocket::OnStartRequest %p\n", this));
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WebrtcTCPSocket::OnStopRequest(nsIRequest* aRequest, nsresult aStatusCode) {
+ LOG(("WebrtcTCPSocket::OnStopRequest %p status=%u\n", this,
+ static_cast<uint32_t>(aStatusCode)));
+
+ // see nsHttpChannel::ProcessFailedProxyConnect for most error codes
+ if (NS_FAILED(aStatusCode)) {
+ CloseWithReason(aStatusCode);
+ return aStatusCode;
+ }
+
+ return NS_OK;
+}
+
+// nsIStreamListener
+NS_IMETHODIMP
+WebrtcTCPSocket::OnDataAvailable(nsIRequest* aRequest,
+ nsIInputStream* aInputStream, uint64_t aOffset,
+ uint32_t aCount) {
+ LOG(("WebrtcTCPSocket::OnDataAvailable %p count=%u\n", this, aCount));
+ MOZ_ASSERT(0, "unreachable data available");
+ return NS_OK;
+}
+
+// nsIInputStreamCallback
+NS_IMETHODIMP
+WebrtcTCPSocket::OnInputStreamReady(nsIAsyncInputStream* in) {
+ LOG(("WebrtcTCPSocket::OnInputStreamReady %p unwritten=%zu\n", this,
+ CountUnwrittenBytes()));
+ MOZ_ASSERT(OnSocketThread(), "not on socket thread");
+ MOZ_ASSERT(!mClosed, "webrtc TCP socket closed");
+ MOZ_ASSERT(mTransport, "webrtc TCP socket not connected");
+ MOZ_ASSERT(mSocketIn == in, "wrong input stream");
+
+ char buffer[9216];
+ uint32_t remainingCapacity = sizeof(buffer);
+ uint32_t read = 0;
+
+ while (remainingCapacity > 0) {
+ uint32_t count = 0;
+ nsresult rv = mSocketIn->Read(buffer + read, remainingCapacity, &count);
+ if (rv == NS_BASE_STREAM_WOULD_BLOCK) {
+ break;
+ }
+
+ if (NS_FAILED(rv)) {
+ LOG(("WebrtcTCPSocket::OnInputStreamReady %p failed %u\n", this,
+ static_cast<uint32_t>(rv)));
+ CloseWithReason(rv);
+ return rv;
+ }
+
+ // base stream closed
+ if (count == 0) {
+ LOG(("WebrtcTCPSocket::OnInputStreamReady %p connection closed\n", this));
+ CloseWithReason(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+
+ remainingCapacity -= count;
+ read += count;
+ }
+
+ if (read > 0) {
+ nsTArray<uint8_t> array(read);
+ array.AppendElements(buffer, read);
+
+ InvokeOnRead(std::move(array));
+ }
+
+ mSocketIn->AsyncWait(this, 0, 0, nullptr);
+
+ return NS_OK;
+}
+
+// nsIOutputStreamCallback
+NS_IMETHODIMP
+WebrtcTCPSocket::OnOutputStreamReady(nsIAsyncOutputStream* out) {
+ LOG(("WebrtcTCPSocket::OnOutputStreamReady %p unwritten=%zu\n", this,
+ CountUnwrittenBytes()));
+ MOZ_ASSERT(OnSocketThread(), "not on socket thread");
+ MOZ_ASSERT(!mClosed, "webrtc TCP socket closed");
+ MOZ_ASSERT(mTransport, "webrtc TCP socket not connected");
+ MOZ_ASSERT(mSocketOut == out, "wrong output stream");
+
+ while (!mWriteQueue.empty()) {
+ const WebrtcTCPData& data = mWriteQueue.front();
+
+ char* buffer = reinterpret_cast<char*>(
+ const_cast<uint8_t*>(data.GetData().Elements())) +
+ mWriteOffset;
+ uint32_t toWrite = data.GetData().Length() - mWriteOffset;
+
+ uint32_t wrote = 0;
+ nsresult rv = mSocketOut->Write(buffer, toWrite, &wrote);
+ if (rv == NS_BASE_STREAM_WOULD_BLOCK) {
+ mSocketOut->AsyncWait(this, 0, 0, nullptr);
+ return NS_OK;
+ }
+
+ if (NS_FAILED(rv)) {
+ LOG(("WebrtcTCPSocket::OnOutputStreamReady %p failed %u\n", this,
+ static_cast<uint32_t>(rv)));
+ CloseWithReason(rv);
+ return NS_OK;
+ }
+
+ mWriteOffset += wrote;
+
+ if (toWrite == wrote) {
+ mWriteOffset = 0;
+ mWriteQueue.pop_front();
+ }
+ }
+
+ return NS_OK;
+}
+
+// nsIInterfaceRequestor
+NS_IMETHODIMP
+WebrtcTCPSocket::GetInterface(const nsIID& iid, void** result) {
+ LOG(("WebrtcTCPSocket::GetInterface %p\n", this));
+
+ return QueryInterface(iid, result);
+}
+
+size_t WebrtcTCPSocket::CountUnwrittenBytes() const {
+ size_t count = 0;
+
+ for (const WebrtcTCPData& data : mWriteQueue) {
+ count += data.GetData().Length();
+ }
+
+ MOZ_ASSERT(count >= mWriteOffset, "offset exceeds write buffer length");
+
+ count -= mWriteOffset;
+
+ return count;
+}
+
+} // namespace mozilla::net
diff --git a/dom/media/webrtc/transport/ipc/WebrtcTCPSocket.h b/dom/media/webrtc/transport/ipc/WebrtcTCPSocket.h
new file mode 100644
index 0000000000..632ba47d32
--- /dev/null
+++ b/dom/media/webrtc/transport/ipc/WebrtcTCPSocket.h
@@ -0,0 +1,104 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef webrtc_tcp_socket_h__
+#define webrtc_tcp_socket_h__
+
+#include <list>
+
+#include "nsCOMPtr.h"
+#include "nsIAsyncInputStream.h"
+#include "nsIAsyncOutputStream.h"
+#include "nsIAuthPromptProvider.h"
+#include "nsIHttpChannelInternal.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsIStreamListener.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+#include "nsIProtocolProxyCallback.h"
+#include "mozilla/net/WebrtcProxyConfig.h"
+
+class nsISocketTransport;
+
+namespace mozilla::net {
+
+class WebrtcTCPSocketCallback;
+class WebrtcTCPData;
+
+class WebrtcTCPSocket : public nsIHttpUpgradeListener,
+ public nsIStreamListener,
+ public nsIInputStreamCallback,
+ public nsIOutputStreamCallback,
+ public nsIInterfaceRequestor,
+ public nsIAuthPromptProvider,
+ public nsIProtocolProxyCallback {
+ public:
+ NS_DECL_NSIHTTPUPGRADELISTENER
+ NS_DECL_NSIINPUTSTREAMCALLBACK
+ NS_DECL_NSIINTERFACEREQUESTOR
+ NS_DECL_NSIOUTPUTSTREAMCALLBACK
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_FORWARD_SAFE_NSIAUTHPROMPTPROVIDER(mAuthProvider)
+ NS_DECL_NSIPROTOCOLPROXYCALLBACK
+
+ explicit WebrtcTCPSocket(WebrtcTCPSocketCallback* aCallbacks);
+
+ void SetTabId(dom::TabId aTabId);
+ nsresult Open(const nsACString& aHost, const int& aPort,
+ const nsACString& aLocalAddress, const int& aLocalPort,
+ bool aUseTls,
+ const Maybe<net::WebrtcProxyConfig>& aProxyConfig);
+ nsresult Write(nsTArray<uint8_t>&& aBytes);
+ nsresult Close();
+
+ size_t CountUnwrittenBytes() const;
+
+ protected:
+ virtual ~WebrtcTCPSocket();
+
+ // protected for gtests
+ virtual void InvokeOnClose(nsresult aReason);
+ virtual void InvokeOnConnected();
+ virtual void InvokeOnRead(nsTArray<uint8_t>&& aReadData);
+
+ RefPtr<WebrtcTCPSocketCallback> mProxyCallbacks;
+
+ private:
+ bool mClosed;
+ bool mOpened;
+ nsCOMPtr<nsIURI> mURI;
+ bool mTls = false;
+ Maybe<WebrtcProxyConfig> mProxyConfig;
+ nsCString mLocalAddress;
+ uint16_t mLocalPort = 0;
+ nsCString mProxyType;
+
+ nsresult DoProxyConfigLookup();
+ nsresult OpenWithHttpProxy();
+ void OpenWithoutHttpProxy(nsIProxyInfo* aSocksProxyInfo);
+ void FinishOpen();
+ void EnqueueWrite_s(nsTArray<uint8_t>&& aWriteData);
+
+ void CloseWithReason(nsresult aReason);
+
+ size_t mWriteOffset;
+ std::list<WebrtcTCPData> mWriteQueue;
+ nsCOMPtr<nsIAuthPromptProvider> mAuthProvider;
+
+ // Indicates that the channel is CONNECTed
+ nsCOMPtr<nsISocketTransport> mTransport;
+ nsCOMPtr<nsIAsyncInputStream> mSocketIn;
+ nsCOMPtr<nsIAsyncOutputStream> mSocketOut;
+ nsCOMPtr<nsIEventTarget> mMainThread;
+ nsCOMPtr<nsIEventTarget> mSocketThread;
+ nsCOMPtr<nsICancelable> mProxyRequest;
+};
+
+} // namespace mozilla::net
+
+#endif // webrtc_tcp_socket_h__
diff --git a/dom/media/webrtc/transport/ipc/WebrtcTCPSocketCallback.h b/dom/media/webrtc/transport/ipc/WebrtcTCPSocketCallback.h
new file mode 100644
index 0000000000..1929e55ac2
--- /dev/null
+++ b/dom/media/webrtc/transport/ipc/WebrtcTCPSocketCallback.h
@@ -0,0 +1,28 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef webrtc_tcp_socket_callback_h__
+#define webrtc_tcp_socket_callback_h__
+
+#include "nsTArray.h"
+
+namespace mozilla::net {
+
+class WebrtcTCPSocketCallback {
+ public:
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+ virtual void OnClose(nsresult aReason) = 0;
+ virtual void OnConnected(const nsACString& aProxyType) = 0;
+ virtual void OnRead(nsTArray<uint8_t>&& aReadData) = 0;
+
+ protected:
+ virtual ~WebrtcTCPSocketCallback() = default;
+};
+
+} // namespace mozilla::net
+
+#endif // webrtc_tcp_socket_callback_h__
diff --git a/dom/media/webrtc/transport/ipc/WebrtcTCPSocketChild.cpp b/dom/media/webrtc/transport/ipc/WebrtcTCPSocketChild.cpp
new file mode 100644
index 0000000000..52d1ba8ab2
--- /dev/null
+++ b/dom/media/webrtc/transport/ipc/WebrtcTCPSocketChild.cpp
@@ -0,0 +1,96 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WebrtcTCPSocketChild.h"
+
+#include "mozilla/net/NeckoChild.h"
+#include "mozilla/net/SocketProcessChild.h"
+
+#include "LoadInfo.h"
+
+#include "WebrtcTCPSocketLog.h"
+#include "WebrtcTCPSocketCallback.h"
+
+using namespace mozilla::ipc;
+
+namespace mozilla::net {
+
+mozilla::ipc::IPCResult WebrtcTCPSocketChild::RecvOnClose(
+ const nsresult& aReason) {
+ LOG(("WebrtcTCPSocketChild::RecvOnClose %p\n", this));
+
+ MOZ_ASSERT(mProxyCallbacks, "webrtc TCP callbacks should be non-null");
+ mProxyCallbacks->OnClose(aReason);
+ mProxyCallbacks = nullptr;
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebrtcTCPSocketChild::RecvOnConnected(
+ const nsACString& aProxyType) {
+ LOG(("WebrtcTCPSocketChild::RecvOnConnected %p\n", this));
+
+ MOZ_ASSERT(mProxyCallbacks, "webrtc TCP callbacks should be non-null");
+ mProxyCallbacks->OnConnected(aProxyType);
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebrtcTCPSocketChild::RecvOnRead(
+ nsTArray<uint8_t>&& aReadData) {
+ LOG(("WebrtcTCPSocketChild::RecvOnRead %p\n", this));
+
+ MOZ_ASSERT(mProxyCallbacks, "webrtc TCP callbacks should be non-null");
+ mProxyCallbacks->OnRead(std::move(aReadData));
+
+ return IPC_OK();
+}
+
+WebrtcTCPSocketChild::WebrtcTCPSocketChild(
+ WebrtcTCPSocketCallback* aProxyCallbacks)
+ : mProxyCallbacks(aProxyCallbacks) {
+ MOZ_COUNT_CTOR(WebrtcTCPSocketChild);
+
+ LOG(("WebrtcTCPSocketChild::WebrtcTCPSocketChild %p\n", this));
+}
+
+WebrtcTCPSocketChild::~WebrtcTCPSocketChild() {
+ MOZ_COUNT_DTOR(WebrtcTCPSocketChild);
+
+ LOG(("WebrtcTCPSocketChild::~WebrtcTCPSocketChild %p\n", this));
+}
+
+void WebrtcTCPSocketChild::AsyncOpen(
+ const nsACString& aHost, const int& aPort, const nsACString& aLocalAddress,
+ const int& aLocalPort, bool aUseTls,
+ const std::shared_ptr<NrSocketProxyConfig>& aProxyConfig) {
+ LOG(("WebrtcTCPSocketChild::AsyncOpen %p %s:%d\n", this,
+ PromiseFlatCString(aHost).get(), aPort));
+
+ MOZ_ASSERT(NS_IsMainThread(), "not main thread");
+
+ AddIPDLReference();
+
+ Maybe<net::WebrtcProxyConfig> proxyConfig;
+ Maybe<dom::TabId> tabId;
+ if (aProxyConfig) {
+ proxyConfig = Some(aProxyConfig->GetConfig());
+ tabId = Some(proxyConfig->tabId());
+ }
+
+ if (IsNeckoChild()) {
+ // We're on a content process
+ gNeckoChild->SendPWebrtcTCPSocketConstructor(this, tabId);
+ } else if (IsSocketProcessChild()) {
+ // We're on a socket process
+ SocketProcessChild::GetSingleton()->SendPWebrtcTCPSocketConstructor(this,
+ tabId);
+ }
+
+ SendAsyncOpen(aHost, aPort, aLocalAddress, aLocalPort, aUseTls, proxyConfig);
+}
+
+} // namespace mozilla::net
diff --git a/dom/media/webrtc/transport/ipc/WebrtcTCPSocketChild.h b/dom/media/webrtc/transport/ipc/WebrtcTCPSocketChild.h
new file mode 100644
index 0000000000..638ffcaac3
--- /dev/null
+++ b/dom/media/webrtc/transport/ipc/WebrtcTCPSocketChild.h
@@ -0,0 +1,47 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_net_WebrtcTCPSocketChild_h
+#define mozilla_net_WebrtcTCPSocketChild_h
+
+#include "mozilla/net/PWebrtcTCPSocketChild.h"
+#include "mozilla/dom/ipc/IdType.h"
+#include "transport/nr_socket_proxy_config.h"
+
+namespace mozilla::net {
+
+class WebrtcTCPSocketCallback;
+
+class WebrtcTCPSocketChild : public PWebrtcTCPSocketChild {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WebrtcTCPSocketChild)
+
+ mozilla::ipc::IPCResult RecvOnClose(const nsresult& aReason) override;
+
+ mozilla::ipc::IPCResult RecvOnConnected(
+ const nsACString& aProxyType) override;
+
+ mozilla::ipc::IPCResult RecvOnRead(nsTArray<uint8_t>&& aReadData) override;
+
+ explicit WebrtcTCPSocketChild(WebrtcTCPSocketCallback* aProxyCallbacks);
+
+ void AsyncOpen(const nsACString& aHost, const int& aPort,
+ const nsACString& aLocalAddress, const int& aLocalPort,
+ bool aUseTls,
+ const std::shared_ptr<NrSocketProxyConfig>& aProxyConfig);
+
+ void AddIPDLReference() { AddRef(); }
+ void ReleaseIPDLReference() { Release(); }
+
+ protected:
+ virtual ~WebrtcTCPSocketChild();
+
+ RefPtr<WebrtcTCPSocketCallback> mProxyCallbacks;
+};
+
+} // namespace mozilla::net
+
+#endif // mozilla_net_WebrtcTCPSocketChild_h
diff --git a/dom/media/webrtc/transport/ipc/WebrtcTCPSocketLog.cpp b/dom/media/webrtc/transport/ipc/WebrtcTCPSocketLog.cpp
new file mode 100644
index 0000000000..a6b0dcdb75
--- /dev/null
+++ b/dom/media/webrtc/transport/ipc/WebrtcTCPSocketLog.cpp
@@ -0,0 +1,11 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WebrtcTCPSocketLog.h"
+
+namespace mozilla::net {
+LazyLogModule webrtcTCPSocketLog("WebrtcTCPSocket");
+} // namespace mozilla::net
diff --git a/dom/media/webrtc/transport/ipc/WebrtcTCPSocketLog.h b/dom/media/webrtc/transport/ipc/WebrtcTCPSocketLog.h
new file mode 100644
index 0000000000..72d3d0064b
--- /dev/null
+++ b/dom/media/webrtc/transport/ipc/WebrtcTCPSocketLog.h
@@ -0,0 +1,20 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef webrtc_tcp_socket_log_h__
+#define webrtc_tcp_socket_log_h__
+
+#include "mozilla/Logging.h"
+
+namespace mozilla::net {
+extern LazyLogModule webrtcTCPSocketLog;
+} // namespace mozilla::net
+
+#undef LOG
+#define LOG(args) \
+ MOZ_LOG(mozilla::net::webrtcTCPSocketLog, mozilla::LogLevel::Debug, args)
+
+#endif // webrtc_tcp_socket_log_h__
diff --git a/dom/media/webrtc/transport/ipc/WebrtcTCPSocketParent.cpp b/dom/media/webrtc/transport/ipc/WebrtcTCPSocketParent.cpp
new file mode 100644
index 0000000000..0df8962757
--- /dev/null
+++ b/dom/media/webrtc/transport/ipc/WebrtcTCPSocketParent.cpp
@@ -0,0 +1,122 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WebrtcTCPSocketParent.h"
+
+#include "mozilla/net/NeckoParent.h"
+
+#include "WebrtcTCPSocket.h"
+#include "WebrtcTCPSocketLog.h"
+
+using namespace mozilla::dom;
+using namespace mozilla::ipc;
+
+namespace mozilla::net {
+
+mozilla::ipc::IPCResult WebrtcTCPSocketParent::RecvAsyncOpen(
+ const nsACString& aHost, const int& aPort, const nsACString& aLocalAddress,
+ const int& aLocalPort, const bool& aUseTls,
+ const Maybe<WebrtcProxyConfig>& aProxyConfig) {
+ LOG(("WebrtcTCPSocketParent::RecvAsyncOpen %p to %s:%d\n", this,
+ PromiseFlatCString(aHost).get(), aPort));
+
+ MOZ_ASSERT(mChannel, "webrtc TCP socket should be non-null");
+ if (!mChannel) {
+ return IPC_FAIL(this, "Called with null channel.");
+ }
+
+ mChannel->Open(aHost, aPort, aLocalAddress, aLocalPort, aUseTls,
+ aProxyConfig);
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebrtcTCPSocketParent::RecvWrite(
+ nsTArray<uint8_t>&& aWriteData) {
+ LOG(("WebrtcTCPSocketParent::RecvWrite %p for %zu\n", this,
+ aWriteData.Length()));
+
+ // Need to check this here in case there are Writes in the queue after OnClose
+ if (mChannel) {
+ mChannel->Write(std::move(aWriteData));
+ }
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult WebrtcTCPSocketParent::RecvClose() {
+ LOG(("WebrtcTCPSocketParent::RecvClose %p\n", this));
+
+ CleanupChannel();
+
+ IProtocol* mgr = Manager();
+ if (!Send__delete__(this)) {
+ return IPC_FAIL_NO_REASON(mgr);
+ }
+
+ return IPC_OK();
+}
+
+void WebrtcTCPSocketParent::ActorDestroy(ActorDestroyReason aWhy) {
+ LOG(("WebrtcTCPSocketParent::ActorDestroy %p for %d\n", this, aWhy));
+
+ CleanupChannel();
+}
+
+WebrtcTCPSocketParent::WebrtcTCPSocketParent(const Maybe<dom::TabId>& aTabId) {
+ MOZ_COUNT_CTOR(WebrtcTCPSocketParent);
+
+ LOG(("WebrtcTCPSocketParent::WebrtcTCPSocketParent %p\n", this));
+
+ mChannel = new WebrtcTCPSocket(this);
+ if (aTabId.isSome()) {
+ mChannel->SetTabId(*aTabId);
+ }
+}
+
+WebrtcTCPSocketParent::~WebrtcTCPSocketParent() {
+ MOZ_COUNT_DTOR(WebrtcTCPSocketParent);
+
+ LOG(("WebrtcTCPSocketParent::~WebrtcTCPSocketParent %p\n", this));
+
+ CleanupChannel();
+}
+
+// WebrtcTCPSocketCallback
+void WebrtcTCPSocketParent::OnClose(nsresult aReason) {
+ LOG(("WebrtcTCPSocketParent::OnClose %p\n", this));
+
+ if (mChannel) {
+ Unused << SendOnClose(aReason);
+ }
+
+ CleanupChannel();
+}
+
+void WebrtcTCPSocketParent::OnRead(nsTArray<uint8_t>&& aReadData) {
+ LOG(("WebrtcTCPSocketParent::OnRead %p %zu\n", this, aReadData.Length()));
+
+ if (mChannel && !SendOnRead(std::move(aReadData))) {
+ CleanupChannel();
+ }
+}
+
+void WebrtcTCPSocketParent::OnConnected(const nsACString& aProxyType) {
+ LOG(("WebrtcTCPSocketParent::OnConnected %p\n", this));
+
+ if (mChannel && !SendOnConnected(aProxyType)) {
+ CleanupChannel();
+ }
+}
+
+void WebrtcTCPSocketParent::CleanupChannel() {
+ if (mChannel) {
+ mChannel->Close();
+ mChannel = nullptr;
+ }
+}
+
+} // namespace mozilla::net
diff --git a/dom/media/webrtc/transport/ipc/WebrtcTCPSocketParent.h b/dom/media/webrtc/transport/ipc/WebrtcTCPSocketParent.h
new file mode 100644
index 0000000000..df4462609d
--- /dev/null
+++ b/dom/media/webrtc/transport/ipc/WebrtcTCPSocketParent.h
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_net_WebrtcTCPSocketParent_h
+#define mozilla_net_WebrtcTCPSocketParent_h
+
+#include "mozilla/net/PWebrtcTCPSocketParent.h"
+
+#include "WebrtcTCPSocketCallback.h"
+
+class nsIAuthPromptProvider;
+
+namespace mozilla::net {
+
+class WebrtcTCPSocket;
+
+class WebrtcTCPSocketParent : public PWebrtcTCPSocketParent,
+ public WebrtcTCPSocketCallback {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WebrtcTCPSocketParent, override)
+
+ mozilla::ipc::IPCResult RecvAsyncOpen(
+ const nsACString& aHost, const int& aPort,
+ const nsACString& aLocalAddress, const int& aLocalPort,
+ const bool& aUseTls,
+ const Maybe<WebrtcProxyConfig>& aProxyConfig) override;
+
+ mozilla::ipc::IPCResult RecvWrite(nsTArray<uint8_t>&& aWriteData) override;
+
+ mozilla::ipc::IPCResult RecvClose() override;
+
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ explicit WebrtcTCPSocketParent(const Maybe<dom::TabId>& aTabId);
+
+ // WebrtcTCPSocketCallback
+ void OnClose(nsresult aReason) override;
+ void OnConnected(const nsACString& aProxyType) override;
+ void OnRead(nsTArray<uint8_t>&& bytes) override;
+
+ void AddIPDLReference() { AddRef(); }
+ void ReleaseIPDLReference() { Release(); }
+
+ protected:
+ virtual ~WebrtcTCPSocketParent();
+
+ private:
+ void CleanupChannel();
+
+ // Indicates that IPC is open.
+ RefPtr<WebrtcTCPSocket> mChannel;
+};
+
+} // namespace mozilla::net
+
+#endif // mozilla_net_WebrtcTCPSocketParent_h
diff --git a/dom/media/webrtc/transport/ipc/moz.build b/dom/media/webrtc/transport/ipc/moz.build
new file mode 100644
index 0000000000..a2a72bb624
--- /dev/null
+++ b/dom/media/webrtc/transport/ipc/moz.build
@@ -0,0 +1,54 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS.mozilla.net += [
+ "NrIceStunAddrMessageUtils.h",
+ "PStunAddrsParams.h",
+ "StunAddrsRequestChild.h",
+ "StunAddrsRequestParent.h",
+ "WebrtcTCPSocket.h",
+ "WebrtcTCPSocketCallback.h",
+ "WebrtcTCPSocketChild.h",
+ "WebrtcTCPSocketParent.h",
+]
+
+UNIFIED_SOURCES += [
+ "StunAddrsRequestChild.cpp",
+ "StunAddrsRequestParent.cpp",
+ "WebrtcTCPSocket.cpp",
+ "WebrtcTCPSocketChild.cpp",
+ "WebrtcTCPSocketLog.cpp",
+ "WebrtcTCPSocketParent.cpp",
+]
+
+IPDL_SOURCES += [
+ "PStunAddrsRequest.ipdl",
+ "PWebrtcTCPSocket.ipdl",
+ "WebrtcProxyConfig.ipdlh",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+
+DEFINES["R_DEFINED_INT2"] = "int16_t"
+DEFINES["R_DEFINED_UINT2"] = "uint16_t"
+DEFINES["R_DEFINED_INT4"] = "int32_t"
+DEFINES["R_DEFINED_UINT4"] = "uint32_t"
+# These are defined to avoid a conflict between typedefs in winsock2.h and
+# r_types.h. This is safe because these types are unused by the code here,
+# but still deeply unfortunate. There is similar code in the win32 version of
+# csi_platform.h, but that trick does not work here, even if that file is
+# directly included.
+DEFINES["R_DEFINED_INT8"] = "int8_t"
+DEFINES["R_DEFINED_UINT8"] = "uint8_t"
+
+LOCAL_INCLUDES += [
+ "/dom/media/webrtc/jsapi",
+ "/dom/media/webrtc/transport/third_party/nICEr/src/net",
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr",
+ "/media/webrtc",
+ "/netwerk/base",
+ "/netwerk/protocol/http",
+]
diff --git a/dom/media/webrtc/transport/logging.h b/dom/media/webrtc/transport/logging.h
new file mode 100644
index 0000000000..98cbd13819
--- /dev/null
+++ b/dom/media/webrtc/transport/logging.h
@@ -0,0 +1,65 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+#ifndef logging_h__
+#define logging_h__
+
+#include <sstream>
+#include "mozilla/Logging.h"
+
+#ifdef MOZILLA_INTERNAL_API
+
+# define ML_ERROR mozilla::LogLevel::Error
+# define ML_WARNING mozilla::LogLevel::Warning
+# define ML_NOTICE mozilla::LogLevel::Info
+# define ML_INFO mozilla::LogLevel::Debug
+# define ML_DEBUG mozilla::LogLevel::Verbose
+
+# define MOZ_MTLOG_MODULE(n) \
+ static mozilla::LogModule* getLogModule() { \
+ static mozilla::LazyLogModule log(n); \
+ return static_cast<mozilla::LogModule*>(log); \
+ }
+
+# define MOZ_MTLOG(level, b) \
+ do { \
+ if (MOZ_LOG_TEST(getLogModule(), level)) { \
+ std::stringstream str; \
+ str << b; \
+ MOZ_LOG(getLogModule(), level, ("%s", str.str().c_str())); \
+ } \
+ } while (0)
+#else
+// When building mtransport outside of XUL, for example in stand-alone gtests,
+// PR_Logging needs to be used instead of mozilla logging.
+
+# include "prlog.h"
+
+# define ML_ERROR PR_LOG_ERROR
+# define ML_WARNING PR_LOG_WARNING
+# define ML_NOTICE PR_LOG_INFO
+# define ML_INFO PR_LOG_DEBUG
+# define ML_DEBUG PR_LOG_VERBOSE
+
+# define MOZ_MTLOG_MODULE(n) \
+ static PRLogModuleInfo* getLogModule() { \
+ static PRLogModuleInfo* log; \
+ if (!log) log = PR_NewLogModule(n); \
+ return log; \
+ }
+
+# define MOZ_MTLOG(level, b) \
+ do { \
+ if (PR_LOG_TEST(getLogModule(), level)) { \
+ std::stringstream str; \
+ str << b; \
+ PR_LOG(getLogModule(), level, ("%s", str.str().c_str())); \
+ } \
+ } while (0)
+#endif // MOZILLA_INTERNAL_API
+#endif // logging_h__
diff --git a/dom/media/webrtc/transport/m_cpp_utils.h b/dom/media/webrtc/transport/m_cpp_utils.h
new file mode 100644
index 0000000000..a1469f798a
--- /dev/null
+++ b/dom/media/webrtc/transport/m_cpp_utils.h
@@ -0,0 +1,25 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+#ifndef m_cpp_utils_h__
+#define m_cpp_utils_h__
+
+#include "mozilla/Attributes.h"
+
+namespace mozilla {
+
+#define DISALLOW_ASSIGNMENT(T) void operator=(const T& other) = delete
+
+#define DISALLOW_COPY(T) T(const T& other) = delete
+
+#define DISALLOW_COPY_ASSIGN(T) \
+ DISALLOW_COPY(T); \
+ DISALLOW_ASSIGNMENT(T)
+
+} // namespace mozilla
+#endif
diff --git a/dom/media/webrtc/transport/mdns_service/Cargo.toml b/dom/media/webrtc/transport/mdns_service/Cargo.toml
new file mode 100644
index 0000000000..ec7e182d4b
--- /dev/null
+++ b/dom/media/webrtc/transport/mdns_service/Cargo.toml
@@ -0,0 +1,14 @@
+[package]
+name = "mdns_service"
+version = "0.1.1"
+authors = ["Dan Minor <dminor@mozilla.com>"]
+edition = "2018"
+license = "MPL-2.0"
+
+[dependencies]
+byteorder = "1.3.1"
+dns-parser = "0.8.0"
+gecko-profiler = { path = "../../../../../tools/profiler/rust-api" }
+log = "0.4"
+socket2 = { version = "0.4", features = ["all"] }
+uuid = { version = "1.0", features = ["v4"] }
diff --git a/dom/media/webrtc/transport/mdns_service/mdns_service.h b/dom/media/webrtc/transport/mdns_service/mdns_service.h
new file mode 100644
index 0000000000..5e8c252ebc
--- /dev/null
+++ b/dom/media/webrtc/transport/mdns_service/mdns_service.h
@@ -0,0 +1,28 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <cstdarg>
+#include <cstdint>
+#include <cstdlib>
+#include <new>
+
+struct MDNSService;
+
+extern "C" {
+
+void mdns_service_register_hostname(MDNSService* serv, const char* hostname,
+ const char* addr);
+
+MDNSService* mdns_service_start(const char* ifaddr);
+
+void mdns_service_stop(MDNSService* serv);
+
+void mdns_service_query_hostname(
+ MDNSService* serv, void* data,
+ void (*resolved)(void* data, const char* hostname, const char* address),
+ void (*timedout)(void* data, const char* hostname), const char* hostname);
+
+void mdns_service_unregister_hostname(MDNSService* serv, const char* hostname);
+
+} // extern "C"
diff --git a/dom/media/webrtc/transport/mdns_service/src/lib.rs b/dom/media/webrtc/transport/mdns_service/src/lib.rs
new file mode 100644
index 0000000000..fbd07b45f8
--- /dev/null
+++ b/dom/media/webrtc/transport/mdns_service/src/lib.rs
@@ -0,0 +1,843 @@
+/* -*- Mode: rust; rust-indent-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+use byteorder::{BigEndian, WriteBytesExt};
+use socket2::{Domain, Socket, Type};
+use std::collections::HashMap;
+use std::collections::LinkedList;
+use std::ffi::{c_void, CStr, CString};
+use std::io;
+use std::net;
+use std::os::raw::c_char;
+use std::sync::mpsc::channel;
+use std::thread;
+use std::time;
+use uuid::Uuid;
+
+#[macro_use]
+extern crate log;
+
+struct Callback {
+ data: *const c_void,
+ resolved: unsafe extern "C" fn(*const c_void, *const c_char, *const c_char),
+ timedout: unsafe extern "C" fn(*const c_void, *const c_char),
+}
+
+unsafe impl Send for Callback {}
+
+fn hostname_resolved(callback: &Callback, hostname: &str, addr: &str) {
+ if let Ok(hostname) = CString::new(hostname) {
+ if let Ok(addr) = CString::new(addr) {
+ unsafe {
+ (callback.resolved)(callback.data, hostname.as_ptr(), addr.as_ptr());
+ }
+ }
+ }
+}
+
+fn hostname_timedout(callback: &Callback, hostname: &str) {
+ if let Ok(hostname) = CString::new(hostname) {
+ unsafe {
+ (callback.timedout)(callback.data, hostname.as_ptr());
+ }
+ }
+}
+
+// This code is derived from code for creating questions in the dns-parser
+// crate. It would be nice to upstream this, or something similar.
+fn create_answer(id: u16, answers: &[(String, &[u8])]) -> Result<Vec<u8>, io::Error> {
+ let mut buf = Vec::with_capacity(512);
+ let head = dns_parser::Header {
+ id,
+ query: false,
+ opcode: dns_parser::Opcode::StandardQuery,
+ authoritative: true,
+ truncated: false,
+ recursion_desired: false,
+ recursion_available: false,
+ authenticated_data: false,
+ checking_disabled: false,
+ response_code: dns_parser::ResponseCode::NoError,
+ questions: 0,
+ answers: answers.len() as u16,
+ nameservers: 0,
+ additional: 0,
+ };
+
+ buf.extend([0u8; 12].iter());
+ head.write(&mut buf[..12]);
+
+ for (name, addr) in answers {
+ for part in name.split('.') {
+ if part.len() > 62 {
+ return Err(io::Error::new(
+ io::ErrorKind::Other,
+ "Name part length too long",
+ ));
+ }
+ let ln = part.len() as u8;
+ buf.push(ln);
+ buf.extend(part.as_bytes());
+ }
+ buf.push(0);
+
+ if addr.len() == 4 {
+ buf.write_u16::<BigEndian>(dns_parser::Type::A as u16)?;
+ } else {
+ buf.write_u16::<BigEndian>(dns_parser::Type::AAAA as u16)?;
+ }
+ // set cache flush bit
+ buf.write_u16::<BigEndian>(dns_parser::Class::IN as u16 | (0x1 << 15))?;
+ buf.write_u32::<BigEndian>(120)?;
+ buf.write_u16::<BigEndian>(addr.len() as u16)?;
+ buf.extend(*addr);
+ }
+
+ Ok(buf)
+}
+
+fn create_query(id: u16, queries: &[String]) -> Result<Vec<u8>, io::Error> {
+ let mut buf = Vec::with_capacity(512);
+ let head = dns_parser::Header {
+ id,
+ query: true,
+ opcode: dns_parser::Opcode::StandardQuery,
+ authoritative: false,
+ truncated: false,
+ recursion_desired: false,
+ recursion_available: false,
+ authenticated_data: false,
+ checking_disabled: false,
+ response_code: dns_parser::ResponseCode::NoError,
+ questions: queries.len() as u16,
+ answers: 0,
+ nameservers: 0,
+ additional: 0,
+ };
+
+ buf.extend([0u8; 12].iter());
+ head.write(&mut buf[..12]);
+
+ for name in queries {
+ for part in name.split('.') {
+ assert!(part.len() < 63);
+ let ln = part.len() as u8;
+ buf.push(ln);
+ buf.extend(part.as_bytes());
+ }
+ buf.push(0);
+
+ buf.write_u16::<BigEndian>(dns_parser::QueryType::A as u16)?;
+ buf.write_u16::<BigEndian>(dns_parser::QueryClass::IN as u16)?;
+ }
+
+ Ok(buf)
+}
+
+fn handle_queries(
+ socket: &std::net::UdpSocket,
+ mdns_addr: &std::net::SocketAddr,
+ pending_queries: &mut HashMap<String, Query>,
+ unsent_queries: &mut LinkedList<Query>,
+) {
+ if pending_queries.len() < 50 {
+ let mut queries: Vec<Query> = Vec::new();
+ while queries.len() < 5 && !unsent_queries.is_empty() {
+ if let Some(query) = unsent_queries.pop_front() {
+ if !pending_queries.contains_key(&query.hostname) {
+ queries.push(query);
+ }
+ }
+ }
+ if !queries.is_empty() {
+ let query_hostnames: Vec<String> =
+ queries.iter().map(|q| q.hostname.to_string()).collect();
+
+ if let Ok(buf) = create_query(0, &query_hostnames) {
+ match socket.send_to(&buf, &mdns_addr) {
+ Ok(_) => {
+ for query in queries {
+ pending_queries.insert(query.hostname.to_string(), query);
+ }
+ }
+ Err(err) => {
+ warn!("Sending mDNS query failed: {}", err);
+ if err.kind() != io::ErrorKind::PermissionDenied {
+ for query in queries {
+ unsent_queries.push_back(query);
+ }
+ } else {
+ for query in queries {
+ hostname_timedout(&query.callback, &query.hostname);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ let now = time::Instant::now();
+ let expired: Vec<String> = pending_queries
+ .iter()
+ .filter(|(_, query)| now.duration_since(query.timestamp).as_secs() >= 3)
+ .map(|(hostname, _)| hostname.to_string())
+ .collect();
+ for hostname in expired {
+ if let Some(mut query) = pending_queries.remove(&hostname) {
+ query.attempts += 1;
+ if query.attempts < 3 {
+ query.timestamp = now;
+ unsent_queries.push_back(query);
+ } else {
+ hostname_timedout(&query.callback, &hostname);
+ }
+ }
+ }
+}
+
+fn handle_mdns_socket(
+ socket: &std::net::UdpSocket,
+ mdns_addr: &std::net::SocketAddr,
+ mut buffer: &mut [u8],
+ hosts: &mut HashMap<String, Vec<u8>>,
+ pending_queries: &mut HashMap<String, Query>,
+) -> bool {
+ // Record a simple marker to see how often this is called.
+ gecko_profiler::add_untyped_marker(
+ "handle_mdns_socket",
+ gecko_profiler::gecko_profiler_category!(Network),
+ Default::default(),
+ );
+
+ match socket.recv_from(&mut buffer) {
+ Ok((amt, _)) => {
+ if amt > 0 {
+ let buffer = &buffer[0..amt];
+ match dns_parser::Packet::parse(&buffer) {
+ Ok(parsed) => {
+ let mut answers: Vec<(String, &[u8])> = Vec::new();
+
+ // If a packet contains both both questions and
+ // answers, the questions should be ignored.
+ if parsed.answers.is_empty() {
+ parsed
+ .questions
+ .iter()
+ .filter(|question| question.qtype == dns_parser::QueryType::A)
+ .for_each(|question| {
+ let qname = question.qname.to_string();
+ trace!("mDNS question: {} {:?}", qname, question.qtype);
+ if let Some(octets) = hosts.get(&qname) {
+ trace!("Sending mDNS answer for {}: {:?}", qname, octets);
+ answers.push((qname, &octets));
+ }
+ });
+ }
+ for answer in parsed.answers {
+ let hostname = answer.name.to_string();
+ match pending_queries.get(&hostname) {
+ Some(query) => {
+ match answer.data {
+ dns_parser::RData::A(dns_parser::rdata::a::Record(
+ addr,
+ )) => {
+ let addr = addr.to_string();
+ trace!("mDNS response: {} {}", hostname, addr);
+ hostname_resolved(&query.callback, &hostname, &addr);
+ }
+ dns_parser::RData::AAAA(
+ dns_parser::rdata::aaaa::Record(addr),
+ ) => {
+ let addr = addr.to_string();
+ trace!("mDNS response: {} {}", hostname, addr);
+ hostname_resolved(&query.callback, &hostname, &addr);
+ }
+ _ => {}
+ }
+ pending_queries.remove(&hostname);
+ }
+ None => {
+ continue;
+ }
+ }
+ }
+ // TODO: If we did not answer every query in this
+ // question, we should wait for a random amount of time
+ // so as to not collide with someone else responding to
+ // this query.
+ if !answers.is_empty() {
+ if let Ok(buf) = create_answer(parsed.header.id, &answers) {
+ if let Err(err) = socket.send_to(&buf, &mdns_addr) {
+ warn!("Sending mDNS answer failed: {}", err);
+ }
+ }
+ }
+ }
+ Err(err) => {
+ warn!("Could not parse mDNS packet: {}", err);
+ }
+ }
+ }
+ }
+ Err(err) => {
+ if err.kind() != io::ErrorKind::Interrupted
+ && err.kind() != io::ErrorKind::TimedOut
+ && err.kind() != io::ErrorKind::WouldBlock
+ {
+ error!("Socket error: {}", err);
+ return false;
+ }
+ }
+ }
+
+ true
+}
+
+fn validate_hostname(hostname: &str) -> bool {
+ match hostname.find(".local") {
+ Some(index) => match hostname.get(0..index) {
+ Some(uuid) => match uuid.get(0..36) {
+ Some(initial) => match Uuid::parse_str(initial) {
+ Ok(_) => {
+ // Oddly enough, Safari does not generate valid UUIDs,
+ // the last part sometimes contains more than 12 digits.
+ match uuid.get(36..) {
+ Some(trailing) => {
+ for c in trailing.chars() {
+ if !c.is_ascii_hexdigit() {
+ return false;
+ }
+ }
+ true
+ }
+ None => true,
+ }
+ }
+ Err(_) => false,
+ },
+ None => false,
+ },
+ None => false,
+ },
+ None => false,
+ }
+}
+
+enum ServiceControl {
+ Register {
+ hostname: String,
+ address: String,
+ },
+ Query {
+ callback: Callback,
+ hostname: String,
+ },
+ Unregister {
+ hostname: String,
+ },
+ Stop,
+}
+
+struct Query {
+ hostname: String,
+ callback: Callback,
+ timestamp: time::Instant,
+ attempts: i32,
+}
+
+impl Query {
+ fn new(hostname: &str, callback: Callback) -> Query {
+ Query {
+ hostname: hostname.to_string(),
+ callback,
+ timestamp: time::Instant::now(),
+ attempts: 0,
+ }
+ }
+}
+
+pub struct MDNSService {
+ handle: Option<std::thread::JoinHandle<()>>,
+ sender: Option<std::sync::mpsc::Sender<ServiceControl>>,
+}
+
+impl MDNSService {
+ fn register_hostname(&mut self, hostname: &str, address: &str) {
+ if let Some(sender) = &self.sender {
+ if let Err(err) = sender.send(ServiceControl::Register {
+ hostname: hostname.to_string(),
+ address: address.to_string(),
+ }) {
+ warn!(
+ "Could not send register hostname {} message: {}",
+ hostname, err
+ );
+ }
+ }
+ }
+
+ fn query_hostname(&mut self, callback: Callback, hostname: &str) {
+ if let Some(sender) = &self.sender {
+ if let Err(err) = sender.send(ServiceControl::Query {
+ callback,
+ hostname: hostname.to_string(),
+ }) {
+ warn!(
+ "Could not send query hostname {} message: {}",
+ hostname, err
+ );
+ }
+ }
+ }
+
+ fn unregister_hostname(&mut self, hostname: &str) {
+ if let Some(sender) = &self.sender {
+ if let Err(err) = sender.send(ServiceControl::Unregister {
+ hostname: hostname.to_string(),
+ }) {
+ warn!(
+ "Could not send unregister hostname {} message: {}",
+ hostname, err
+ );
+ }
+ }
+ }
+
+ fn start(&mut self, addrs: Vec<std::net::Ipv4Addr>) -> io::Result<()> {
+ let (sender, receiver) = channel();
+ self.sender = Some(sender);
+
+ let mdns_addr = std::net::Ipv4Addr::new(224, 0, 0, 251);
+ let port = 5353;
+
+ let socket = Socket::new(Domain::IPV4, Type::DGRAM, None)?;
+ socket.set_reuse_address(true)?;
+
+ #[cfg(not(target_os = "windows"))]
+ socket.set_reuse_port(true)?;
+ socket.bind(&socket2::SockAddr::from(std::net::SocketAddr::from((
+ [0, 0, 0, 0],
+ port,
+ ))))?;
+
+ let socket = std::net::UdpSocket::from(socket);
+ socket.set_multicast_loop_v4(true)?;
+ socket.set_read_timeout(Some(time::Duration::from_millis(1)))?;
+ socket.set_write_timeout(Some(time::Duration::from_millis(1)))?;
+ for addr in addrs {
+ if let Err(err) = socket.join_multicast_v4(&mdns_addr, &addr) {
+ warn!(
+ "Could not join multicast group on interface: {:?}: {}",
+ addr, err
+ );
+ }
+ }
+
+ let thread_name = "mdns_service";
+ let builder = thread::Builder::new().name(thread_name.into());
+ self.handle = Some(builder.spawn(move || {
+ gecko_profiler::register_thread(thread_name);
+ let mdns_addr = std::net::SocketAddr::from(([224, 0, 0, 251], port));
+ let mut buffer: [u8; 9_000] = [0; 9_000];
+ let mut hosts = HashMap::new();
+ let mut unsent_queries = LinkedList::new();
+ let mut pending_queries = HashMap::new();
+ loop {
+ match receiver.try_recv() {
+ Ok(msg) => match msg {
+ ServiceControl::Register { hostname, address } => {
+ if !validate_hostname(&hostname) {
+ warn!("Not registering invalid hostname: {}", hostname);
+ continue;
+ }
+ trace!("Registering {} for: {}", hostname, address);
+ match address.parse().and_then(|ip| {
+ Ok(match ip {
+ net::IpAddr::V4(ip) => ip.octets().to_vec(),
+ net::IpAddr::V6(ip) => ip.octets().to_vec(),
+ })
+ }) {
+ Ok(octets) => {
+ let mut v = Vec::new();
+ v.extend(octets);
+ hosts.insert(hostname, v);
+ }
+ Err(err) => {
+ warn!(
+ "Could not parse address for {}: {}: {}",
+ hostname, address, err
+ );
+ }
+ }
+ }
+ ServiceControl::Query { callback, hostname } => {
+ trace!("Querying {}", hostname);
+ if !validate_hostname(&hostname) {
+ warn!("Not sending mDNS query for invalid hostname: {}", hostname);
+ continue;
+ }
+ unsent_queries.push_back(Query::new(&hostname, callback));
+ }
+ ServiceControl::Unregister { hostname } => {
+ trace!("Unregistering {}", hostname);
+ hosts.remove(&hostname);
+ }
+ ServiceControl::Stop => {
+ trace!("Stopping");
+ break;
+ }
+ },
+ Err(std::sync::mpsc::TryRecvError::Disconnected) => {
+ break;
+ }
+ Err(std::sync::mpsc::TryRecvError::Empty) => {}
+ }
+
+ handle_queries(
+ &socket,
+ &mdns_addr,
+ &mut pending_queries,
+ &mut unsent_queries,
+ );
+
+ if !handle_mdns_socket(
+ &socket,
+ &mdns_addr,
+ &mut buffer,
+ &mut hosts,
+ &mut pending_queries,
+ ) {
+ break;
+ }
+ }
+ gecko_profiler::unregister_thread();
+ })?);
+
+ Ok(())
+ }
+
+ fn stop(self) {
+ if let Some(sender) = self.sender {
+ if let Err(err) = sender.send(ServiceControl::Stop) {
+ warn!("Could not stop mDNS Service: {}", err);
+ }
+ if let Some(handle) = self.handle {
+ if handle.join().is_err() {
+ error!("Error on thread join");
+ }
+ }
+ }
+ }
+
+ fn new() -> MDNSService {
+ MDNSService {
+ handle: None,
+ sender: None,
+ }
+ }
+}
+
+/// # Safety
+///
+/// This function must only be called with a valid MDNSService pointer.
+/// This hostname and address arguments must be zero terminated strings.
+#[no_mangle]
+pub unsafe extern "C" fn mdns_service_register_hostname(
+ serv: *mut MDNSService,
+ hostname: *const c_char,
+ address: *const c_char,
+) {
+ assert!(!serv.is_null());
+ assert!(!hostname.is_null());
+ assert!(!address.is_null());
+ let hostname = CStr::from_ptr(hostname).to_string_lossy();
+ let address = CStr::from_ptr(address).to_string_lossy();
+ (*serv).register_hostname(&hostname, &address);
+}
+
+/// # Safety
+///
+/// This ifaddrs argument must be a zero terminated string.
+#[no_mangle]
+pub unsafe extern "C" fn mdns_service_start(ifaddrs: *const c_char) -> *mut MDNSService {
+ assert!(!ifaddrs.is_null());
+ let mut r = Box::new(MDNSService::new());
+ let ifaddrs = CStr::from_ptr(ifaddrs).to_string_lossy();
+ let addrs: Vec<std::net::Ipv4Addr> =
+ ifaddrs.split(';').filter_map(|x| x.parse().ok()).collect();
+
+ if addrs.is_empty() {
+ warn!("Could not parse interface addresses from: {}", ifaddrs);
+ } else if let Err(err) = r.start(addrs) {
+ warn!("Could not start mDNS Service: {}", err);
+ }
+
+ Box::into_raw(r)
+}
+
+/// # Safety
+///
+/// This function must only be called with a valid MDNSService pointer.
+#[no_mangle]
+pub unsafe extern "C" fn mdns_service_stop(serv: *mut MDNSService) {
+ assert!(!serv.is_null());
+ let boxed = Box::from_raw(serv);
+ boxed.stop();
+}
+
+/// # Safety
+///
+/// This function must only be called with a valid MDNSService pointer.
+/// The data argument will be passed back into the resolved and timedout
+/// functions. The object it points to must not be freed until the MDNSService
+/// has stopped.
+#[no_mangle]
+pub unsafe extern "C" fn mdns_service_query_hostname(
+ serv: *mut MDNSService,
+ data: *const c_void,
+ resolved: unsafe extern "C" fn(*const c_void, *const c_char, *const c_char),
+ timedout: unsafe extern "C" fn(*const c_void, *const c_char),
+ hostname: *const c_char,
+) {
+ assert!(!serv.is_null());
+ assert!(!data.is_null());
+ assert!(!hostname.is_null());
+ let hostname = CStr::from_ptr(hostname).to_string_lossy();
+ let callback = Callback {
+ data,
+ resolved,
+ timedout,
+ };
+ (*serv).query_hostname(callback, &hostname);
+}
+
+/// # Safety
+///
+/// This function must only be called with a valid MDNSService pointer.
+/// This function should only be called once per hostname.
+#[no_mangle]
+pub unsafe extern "C" fn mdns_service_unregister_hostname(
+ serv: *mut MDNSService,
+ hostname: *const c_char,
+) {
+ assert!(!serv.is_null());
+ assert!(!hostname.is_null());
+ let hostname = CStr::from_ptr(hostname).to_string_lossy();
+ (*serv).unregister_hostname(&hostname);
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::create_query;
+ use crate::validate_hostname;
+ use crate::Callback;
+ use crate::MDNSService;
+ use socket2::{Domain, Socket, Type};
+ use std::collections::HashSet;
+ use std::ffi::c_void;
+ use std::io;
+ use std::iter::FromIterator;
+ use std::os::raw::c_char;
+ use std::thread;
+ use std::time;
+ use uuid::Uuid;
+
+ #[no_mangle]
+ pub unsafe extern "C" fn mdns_service_resolved(
+ _: *const c_void,
+ _: *const c_char,
+ _: *const c_char,
+ ) -> () {
+ }
+
+ #[no_mangle]
+ pub unsafe extern "C" fn mdns_service_timedout(_: *const c_void, _: *const c_char) -> () {}
+
+ fn listen_until(addr: &std::net::Ipv4Addr, stop: u64) -> thread::JoinHandle<Vec<String>> {
+ let port = 5353;
+
+ let socket = Socket::new(Domain::IPV4, Type::DGRAM, None).unwrap();
+ socket.set_reuse_address(true).unwrap();
+
+ #[cfg(not(target_os = "windows"))]
+ socket.set_reuse_port(true).unwrap();
+ socket
+ .bind(&socket2::SockAddr::from(std::net::SocketAddr::from((
+ [0, 0, 0, 0],
+ port,
+ ))))
+ .unwrap();
+
+ let socket = std::net::UdpSocket::from(socket);
+ socket.set_multicast_loop_v4(true).unwrap();
+ socket
+ .set_read_timeout(Some(time::Duration::from_millis(10)))
+ .unwrap();
+ socket
+ .set_write_timeout(Some(time::Duration::from_millis(10)))
+ .unwrap();
+ socket
+ .join_multicast_v4(&std::net::Ipv4Addr::new(224, 0, 0, 251), &addr)
+ .unwrap();
+
+ let mut buffer: [u8; 9_000] = [0; 9_000];
+ thread::spawn(move || {
+ let start = time::Instant::now();
+ let mut questions = Vec::new();
+ while time::Instant::now().duration_since(start).as_secs() < stop {
+ match socket.recv_from(&mut buffer) {
+ Ok((amt, _)) => {
+ if amt > 0 {
+ let buffer = &buffer[0..amt];
+ match dns_parser::Packet::parse(&buffer) {
+ Ok(parsed) => {
+ parsed
+ .questions
+ .iter()
+ .filter(|question| {
+ question.qtype == dns_parser::QueryType::A
+ })
+ .for_each(|question| {
+ let qname = question.qname.to_string();
+ questions.push(qname);
+ });
+ }
+ Err(err) => {
+ warn!("Could not parse mDNS packet: {}", err);
+ }
+ }
+ }
+ }
+ Err(err) => {
+ if err.kind() != io::ErrorKind::WouldBlock
+ && err.kind() != io::ErrorKind::TimedOut
+ {
+ error!("Socket error: {}", err);
+ break;
+ }
+ }
+ }
+ }
+ questions
+ })
+ }
+
+ #[test]
+ fn test_validate_hostname() {
+ assert_eq!(
+ validate_hostname("e17f08d4-689a-4df6-ba31-35bb9f041100.local"),
+ true
+ );
+ assert_eq!(
+ validate_hostname("62240723-ae6d-4f6a-99b8-94a233e3f84a2.local"),
+ true
+ );
+ assert_eq!(
+ validate_hostname("62240723-ae6d-4f6a-99b8.94e3f84a2.local"),
+ false
+ );
+ assert_eq!(validate_hostname("hi there"), false);
+ }
+
+ #[test]
+ fn start_stop() {
+ let mut service = MDNSService::new();
+ let addr = "127.0.0.1".parse().unwrap();
+ service.start(vec![addr]).unwrap();
+ service.stop();
+ }
+
+ #[test]
+ fn simple_query() {
+ let mut service = MDNSService::new();
+ let addr = "127.0.0.1".parse().unwrap();
+ let handle = listen_until(&addr, 1);
+
+ service.start(vec![addr]).unwrap();
+
+ let callback = Callback {
+ data: 0 as *const c_void,
+ resolved: mdns_service_resolved,
+ timedout: mdns_service_timedout,
+ };
+ let hostname = Uuid::new_v4().as_hyphenated().to_string() + ".local";
+ service.query_hostname(callback, &hostname);
+ service.stop();
+ let questions = handle.join().unwrap();
+ assert!(questions.contains(&hostname));
+ }
+
+ #[test]
+ fn rate_limited_query() {
+ let mut service = MDNSService::new();
+ let addr = "127.0.0.1".parse().unwrap();
+ let handle = listen_until(&addr, 1);
+
+ service.start(vec![addr]).unwrap();
+
+ let mut hostnames = HashSet::new();
+ for _ in 0..100 {
+ let callback = Callback {
+ data: 0 as *const c_void,
+ resolved: mdns_service_resolved,
+ timedout: mdns_service_timedout,
+ };
+ let hostname = Uuid::new_v4().as_hyphenated().to_string() + ".local";
+ service.query_hostname(callback, &hostname);
+ hostnames.insert(hostname);
+ }
+ service.stop();
+ let questions = HashSet::from_iter(handle.join().unwrap().iter().map(|x| x.to_string()));
+ let intersection: HashSet<&String> = questions.intersection(&hostnames).collect();
+ assert_eq!(intersection.len(), 50);
+ }
+
+ #[test]
+ fn repeat_failed_query() {
+ let mut service = MDNSService::new();
+ let addr = "127.0.0.1".parse().unwrap();
+ let handle = listen_until(&addr, 4);
+
+ service.start(vec![addr]).unwrap();
+
+ let hostname = Uuid::new_v4().as_hyphenated().to_string() + ".local";
+ let callback = Callback {
+ data: 0 as *const c_void,
+ resolved: mdns_service_resolved,
+ timedout: mdns_service_timedout,
+ };
+ service.query_hostname(callback, &hostname);
+ thread::sleep(time::Duration::from_secs(4));
+ service.stop();
+
+ let questions: Vec<String> = handle
+ .join()
+ .unwrap()
+ .iter()
+ .filter(|x| *x == &hostname)
+ .map(|x| x.to_string())
+ .collect();
+ assert_eq!(questions.len(), 2);
+ }
+
+ #[test]
+ fn multiple_queries_in_a_single_packet() {
+ let mut hostnames: Vec<String> = Vec::new();
+ for _ in 0..100 {
+ let hostname = Uuid::new_v4().as_hyphenated().to_string() + ".local";
+ hostnames.push(hostname);
+ }
+
+ match create_query(42, &hostnames) {
+ Ok(q) => match dns_parser::Packet::parse(&q) {
+ Ok(parsed) => {
+ assert_eq!(parsed.questions.len(), 100);
+ }
+ Err(_) => assert!(false),
+ },
+ Err(_) => assert!(false),
+ }
+ }
+}
diff --git a/dom/media/webrtc/transport/mediapacket.cpp b/dom/media/webrtc/transport/mediapacket.cpp
new file mode 100644
index 0000000000..38ab65ed2a
--- /dev/null
+++ b/dom/media/webrtc/transport/mediapacket.cpp
@@ -0,0 +1,145 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mediapacket.h"
+
+#include <cstring>
+#include "ipc/IPCMessageUtils.h"
+
+namespace mozilla {
+
+void MediaPacket::Copy(const uint8_t* data, size_t len, size_t capacity) {
+ if (capacity < len) {
+ capacity = len;
+ }
+ data_.reset(new uint8_t[capacity]);
+ len_ = len;
+ capacity_ = capacity;
+ memcpy(data_.get(), data, len);
+}
+
+MediaPacket MediaPacket::Clone() const {
+ MediaPacket newPacket;
+ newPacket.type_ = type_;
+ newPacket.sdp_level_ = sdp_level_;
+ newPacket.Copy(data_.get(), len_, capacity_);
+ return newPacket;
+}
+
+void MediaPacket::Serialize(IPC::MessageWriter* aWriter) const {
+ aWriter->WriteUInt32(len_);
+ aWriter->WriteUInt32(capacity_);
+ if (len_) {
+ aWriter->WriteBytes(data_.get(), len_);
+ }
+ aWriter->WriteUInt32(encrypted_len_);
+ if (encrypted_len_) {
+ aWriter->WriteBytes(encrypted_data_.get(), encrypted_len_);
+ }
+ aWriter->WriteInt32(sdp_level_.isSome() ? *sdp_level_ : -1);
+ aWriter->WriteInt32(type_);
+}
+
+bool MediaPacket::Deserialize(IPC::MessageReader* aReader) {
+ Reset();
+ uint32_t len;
+ if (!aReader->ReadUInt32(&len)) {
+ return false;
+ }
+ uint32_t capacity;
+ if (!aReader->ReadUInt32(&capacity)) {
+ return false;
+ }
+ if (len) {
+ MOZ_RELEASE_ASSERT(capacity >= len);
+ UniquePtr<uint8_t[]> data(new uint8_t[capacity]);
+ if (!aReader->ReadBytesInto(data.get(), len)) {
+ return false;
+ }
+ data_ = std::move(data);
+ len_ = len;
+ capacity_ = capacity;
+ }
+
+ if (!aReader->ReadUInt32(&len)) {
+ return false;
+ }
+ if (len) {
+ UniquePtr<uint8_t[]> data(new uint8_t[len]);
+ if (!aReader->ReadBytesInto(data.get(), len)) {
+ return false;
+ }
+ encrypted_data_ = std::move(data);
+ encrypted_len_ = len;
+ }
+
+ int32_t sdp_level;
+ if (!aReader->ReadInt32(&sdp_level)) {
+ return false;
+ }
+
+ if (sdp_level >= 0) {
+ sdp_level_ = Some(sdp_level);
+ }
+
+ int32_t type;
+ if (!aReader->ReadInt32(&type)) {
+ return false;
+ }
+ type_ = static_cast<Type>(type);
+ return true;
+}
+
+static bool IsRtp(const uint8_t* data, size_t len) {
+ if (len < 2) return false;
+
+ // Check if this is a RTCP packet. Logic based on the types listed in
+ // media/webrtc/trunk/src/modules/rtp_rtcp/source/rtp_utility.cc
+
+ // Anything outside this range is RTP.
+ if ((data[1] < 192) || (data[1] > 207)) return true;
+
+ if (data[1] == 192) // FIR
+ return false;
+
+ if (data[1] == 193) // NACK, but could also be RTP. This makes us sad
+ return true; // but it's how webrtc.org behaves.
+
+ if (data[1] == 194) return true;
+
+ if (data[1] == 195) // IJ.
+ return false;
+
+ if ((data[1] > 195) && (data[1] < 200)) // the > 195 is redundant
+ return true;
+
+ if ((data[1] >= 200) && (data[1] <= 207)) // SR, RR, SDES, BYE,
+ return false; // APP, RTPFB, PSFB, XR
+
+ MOZ_ASSERT(false); // Not reached, belt and suspenders.
+ return true;
+}
+
+void MediaPacket::Categorize() {
+ SetType(MediaPacket::UNCLASSIFIED);
+
+ if (!data_ || len_ < 4) {
+ return;
+ }
+
+ if (data_[0] >= 20 && data_[0] <= 63) {
+ // DTLS per RFC 7983
+ SetType(MediaPacket::DTLS);
+ } else if (data_[0] > 127 && data_[0] < 192) {
+ // RTP/RTCP per RFC 7983
+ if (IsRtp(data_.get(), len_)) {
+ SetType(MediaPacket::SRTP);
+ } else {
+ SetType(MediaPacket::SRTCP);
+ }
+ }
+}
+} // namespace mozilla
diff --git a/dom/media/webrtc/transport/mediapacket.h b/dom/media/webrtc/transport/mediapacket.h
new file mode 100644
index 0000000000..563056c4a4
--- /dev/null
+++ b/dom/media/webrtc/transport/mediapacket.h
@@ -0,0 +1,117 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mediapacket_h__
+#define mediapacket_h__
+
+#include <cstddef>
+#include <cstdint>
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Maybe.h"
+
+class PickleIterator;
+
+namespace IPC {
+class Message;
+class MessageReader;
+class MessageWriter;
+} // namespace IPC
+
+namespace mozilla {
+
+// TODO: It might be worthwhile to teach this class how to "borrow" a buffer.
+// That would make it easier to misuse, however, so maybe not worth it.
+class MediaPacket {
+ public:
+ MediaPacket() = default;
+ MediaPacket(MediaPacket&& orig) = default;
+ MediaPacket& operator=(MediaPacket&& orig) = default;
+
+ MediaPacket Clone() const;
+
+ // Takes ownership of the passed-in data
+ void Take(UniquePtr<uint8_t[]>&& data, size_t len, size_t capacity = 0) {
+ data_ = std::move(data);
+ len_ = len;
+ if (capacity < len) {
+ capacity = len;
+ }
+ capacity_ = capacity;
+ }
+
+ void Reset() {
+ data_.reset();
+ len_ = 0;
+ capacity_ = 0;
+ encrypted_data_.reset();
+ encrypted_len_ = 0;
+ sdp_level_.reset();
+ }
+
+ // Copies the passed-in data
+ void Copy(const uint8_t* data, size_t len, size_t capacity = 0);
+
+ uint8_t* data() const { return data_.get(); }
+
+ size_t len() const { return len_; }
+
+ void SetLength(size_t length) { len_ = length; }
+
+ size_t capacity() const { return capacity_; }
+
+ Maybe<size_t>& sdp_level() { return sdp_level_; }
+
+ void CopyDataToEncrypted() {
+ encrypted_data_ = std::move(data_);
+ encrypted_len_ = len_;
+ Copy(encrypted_data_.get(), len_);
+ }
+
+ const uint8_t* encrypted_data() const { return encrypted_data_.get(); }
+
+ size_t encrypted_len() const { return encrypted_len_; }
+
+ enum Type { UNCLASSIFIED, SRTP, SRTCP, DTLS, RTP, RTCP, SCTP };
+
+ void Categorize();
+
+ void SetType(Type type) { type_ = type; }
+
+ Type type() const { return type_; }
+
+ void Serialize(IPC::MessageWriter* aWriter) const;
+ bool Deserialize(IPC::MessageReader* aReader);
+
+ private:
+ UniquePtr<uint8_t[]> data_;
+ size_t len_ = 0;
+ size_t capacity_ = 0;
+ // Encrypted form of the data, if there is one.
+ UniquePtr<uint8_t[]> encrypted_data_;
+ size_t encrypted_len_ = 0;
+ // SDP level that this packet belongs to, if known.
+ Maybe<size_t> sdp_level_;
+ Type type_ = UNCLASSIFIED;
+};
+} // namespace mozilla
+
+namespace IPC {
+template <typename>
+struct ParamTraits;
+
+template <>
+struct ParamTraits<mozilla::MediaPacket> {
+ static void Write(MessageWriter* aWriter,
+ const mozilla::MediaPacket& aParam) {
+ aParam.Serialize(aWriter);
+ }
+
+ static bool Read(MessageReader* aReader, mozilla::MediaPacket* aResult) {
+ return aResult->Deserialize(aReader);
+ }
+};
+} // namespace IPC
+#endif // mediapacket_h__
diff --git a/dom/media/webrtc/transport/moz.build b/dom/media/webrtc/transport/moz.build
new file mode 100644
index 0000000000..fcf265e22f
--- /dev/null
+++ b/dom/media/webrtc/transport/moz.build
@@ -0,0 +1,23 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "WebRTC: Networking")
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+DIRS += [
+ "/dom/media/webrtc/transport/third_party",
+ "/dom/media/webrtc/transport/build",
+ "/dom/media/webrtc/transport/ipc",
+ "/dom/media/webrtc/transport/srtp",
+]
+
+# Add libFuzzer configuration directives
+include("/tools/fuzzing/libfuzzer-config.mozbuild")
+
+if CONFIG["FUZZING_INTERFACES"]:
+ TEST_DIRS += ["fuzztest"]
diff --git a/dom/media/webrtc/transport/nr_socket_proxy_config.cpp b/dom/media/webrtc/transport/nr_socket_proxy_config.cpp
new file mode 100644
index 0000000000..8c91a5d975
--- /dev/null
+++ b/dom/media/webrtc/transport/nr_socket_proxy_config.cpp
@@ -0,0 +1,34 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nr_socket_proxy_config.h"
+
+#include "mozilla/net/WebrtcProxyConfig.h"
+
+namespace mozilla {
+
+class NrSocketProxyConfig::Private {
+ public:
+ net::WebrtcProxyConfig mProxyConfig;
+};
+
+NrSocketProxyConfig::NrSocketProxyConfig(
+ const net::WebrtcProxyConfig& aProxyConfig)
+ : mPrivate(new Private({aProxyConfig})) {}
+
+NrSocketProxyConfig::NrSocketProxyConfig(NrSocketProxyConfig&& aOrig)
+ : mPrivate(std::move(aOrig.mPrivate)) {}
+
+NrSocketProxyConfig::~NrSocketProxyConfig() = default;
+
+const net::WebrtcProxyConfig& NrSocketProxyConfig::GetConfig() const {
+ return mPrivate->mProxyConfig;
+}
+
+bool NrSocketProxyConfig::GetForceProxy() const {
+ return mPrivate->mProxyConfig.forceProxy();
+}
+} // namespace mozilla
diff --git a/dom/media/webrtc/transport/nr_socket_proxy_config.h b/dom/media/webrtc/transport/nr_socket_proxy_config.h
new file mode 100644
index 0000000000..55ef4fdcb1
--- /dev/null
+++ b/dom/media/webrtc/transport/nr_socket_proxy_config.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nr_socket_proxy_config__
+#define nr_socket_proxy_config__
+
+#include <memory>
+
+namespace mozilla {
+namespace net {
+class WebrtcProxyConfig;
+}
+
+class NrSocketProxyConfig {
+ public:
+ explicit NrSocketProxyConfig(const net::WebrtcProxyConfig& aProxyConfig);
+
+ // We need to actually write the default impl ourselves, because the compiler
+ // needs to know how to destroy mPrivate in case an exception is thrown, even
+ // though we disable exceptions in our build.
+ NrSocketProxyConfig(NrSocketProxyConfig&& aOrig);
+
+ ~NrSocketProxyConfig();
+
+ const net::WebrtcProxyConfig& GetConfig() const;
+ bool GetForceProxy() const;
+
+ private:
+ // dom::ProxyConfig includes stuff that conflicts with nICEr includes.
+ // Make it possible to include this header file without tripping over this
+ // problem.
+ class Private;
+ std::unique_ptr<Private> mPrivate;
+};
+
+} // namespace mozilla
+
+#endif // nr_socket_proxy_config__
diff --git a/dom/media/webrtc/transport/nr_socket_prsock.cpp b/dom/media/webrtc/transport/nr_socket_prsock.cpp
new file mode 100644
index 0000000000..3c99a2d0c8
--- /dev/null
+++ b/dom/media/webrtc/transport/nr_socket_prsock.cpp
@@ -0,0 +1,1785 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+/*
+Modified version of nr_socket_local, adapted for NSPR
+*/
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+Original code from nICEr and nrappkit.
+
+nICEr copyright:
+
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+nrappkit copyright:
+
+ Copyright (C) 2001-2003, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ ekr@rtfm.com Thu Dec 20 20:14:49 2001
+*/
+
+#include <csi_platform.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/types.h>
+#include <assert.h>
+#include <errno.h>
+#include <string>
+
+#include "nspr.h"
+#include "prerror.h"
+#include "prio.h"
+#include "prnetdb.h"
+
+#include "mozilla/net/DNS.h"
+#include "nsCOMPtr.h"
+#include "nsASocketHandler.h"
+#include "nsISocketTransportService.h"
+#include "nsNetCID.h"
+#include "nsISupportsImpl.h"
+#include "nsServiceManagerUtils.h"
+#include "nsComponentManagerUtils.h"
+#include "nsXPCOM.h"
+#include "nsXULAppAPI.h"
+#include "runnable_utils.h"
+#include "mozilla/SyncRunnable.h"
+#include "nsTArray.h"
+#include "nsISocketFilter.h"
+#include "nsDebug.h"
+#include "nsNetUtil.h"
+
+#ifdef XP_WIN
+# include "mozilla/WindowsVersion.h"
+#endif
+
+#if defined(MOZILLA_INTERNAL_API)
+// csi_platform.h deep in nrappkit defines LOG_INFO and LOG_WARNING
+# ifdef LOG_INFO
+# define LOG_TEMP_INFO LOG_INFO
+# undef LOG_INFO
+# endif
+# ifdef LOG_WARNING
+# define LOG_TEMP_WARNING LOG_WARNING
+# undef LOG_WARNING
+# endif
+# if defined(LOG_DEBUG)
+# define LOG_TEMP_DEBUG LOG_DEBUG
+# undef LOG_DEBUG
+# endif
+# undef strlcpy
+
+# include "mozilla/dom/network/UDPSocketChild.h"
+
+# ifdef LOG_TEMP_INFO
+# define LOG_INFO LOG_TEMP_INFO
+# endif
+# ifdef LOG_TEMP_WARNING
+# define LOG_WARNING LOG_TEMP_WARNING
+# endif
+
+# ifdef LOG_TEMP_DEBUG
+# define LOG_DEBUG LOG_TEMP_DEBUG
+# endif
+# ifdef XP_WIN
+# ifdef LOG_DEBUG
+# undef LOG_DEBUG
+# endif
+// cloned from csi_platform.h. Win32 doesn't like how we hide symbols
+# define LOG_DEBUG 7
+# endif
+#endif
+
+extern "C" {
+#include "nr_api.h"
+#include "async_wait.h"
+#include "nr_socket.h"
+#include "nr_socket_local.h"
+#include "stun_hint.h"
+}
+#include "nr_socket_prsock.h"
+#include "simpletokenbucket.h"
+#include "test_nr_socket.h"
+#include "nr_socket_tcp.h"
+#include "nr_socket_proxy_config.h"
+
+// Implement the nsISupports ref counting
+namespace mozilla {
+
+#if defined(MOZILLA_INTERNAL_API)
+class SingletonThreadHolder final {
+ private:
+ ~SingletonThreadHolder() {
+ r_log(LOG_GENERIC, LOG_DEBUG, "Deleting SingletonThreadHolder");
+ if (mThread) {
+ // Likely a connection is somehow being held in CC or GC
+ NS_WARNING(
+ "SingletonThreads should be Released and shut down before exit!");
+ mThread->Shutdown();
+ mThread = nullptr;
+ }
+ }
+
+ DISALLOW_COPY_ASSIGN(SingletonThreadHolder);
+
+ public:
+ // Must be threadsafe for StaticRefPtr/ClearOnShutdown
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SingletonThreadHolder)
+
+ explicit SingletonThreadHolder(const nsACString& aName) : mName(aName) {
+ mParentThread = NS_GetCurrentThread();
+ }
+
+ nsIThread* GetThread() { return mThread; }
+
+ /*
+ * Keep track of how many instances are using a SingletonThreadHolder.
+ * When no one is using it, shut it down
+ */
+ void AddUse() {
+ MOZ_ASSERT(mParentThread == NS_GetCurrentThread());
+ MOZ_ASSERT(int32_t(mUseCount) >= 0, "illegal refcnt");
+ nsrefcnt count = ++mUseCount;
+ if (count == 1) {
+ // idle -> in-use
+ nsresult rv = NS_NewNamedThread(mName, getter_AddRefs(mThread));
+ MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv) && mThread,
+ "Should successfully create mtransport I/O thread");
+ r_log(LOG_GENERIC, LOG_DEBUG, "Created wrapped SingletonThread %p",
+ mThread.get());
+ }
+ r_log(LOG_GENERIC, LOG_DEBUG, "AddUse_i: %lu", (unsigned long)count);
+ }
+
+ void ReleaseUse() {
+ MOZ_ASSERT(mParentThread == NS_GetCurrentThread());
+ nsrefcnt count = --mUseCount;
+ MOZ_ASSERT(int32_t(mUseCount) >= 0, "illegal refcnt");
+ if (mThread && count == 0) {
+ // in-use -> idle -- no one forcing it to remain instantiated
+ r_log(LOG_GENERIC, LOG_DEBUG, "Shutting down wrapped SingletonThread %p",
+ mThread.get());
+ mThread->AsyncShutdown();
+ mThread = nullptr;
+ // It'd be nice to use a timer instead... But be careful of
+ // xpcom-shutdown-threads in that case
+ }
+ r_log(LOG_GENERIC, LOG_DEBUG, "ReleaseUse_i: %lu", (unsigned long)count);
+ }
+
+ private:
+ nsCString mName;
+ nsAutoRefCnt mUseCount;
+ nsCOMPtr<nsIThread> mParentThread;
+ nsCOMPtr<nsIThread> mThread;
+};
+
+static StaticRefPtr<SingletonThreadHolder> sThread;
+
+static void ClearSingletonOnShutdown() {
+ // We expect everybody to have done ReleaseUse() at the latest during
+ // xpcom-shutdown-threads. So we need to live longer than that.
+ ClearOnShutdown(&sThread, ShutdownPhase::XPCOMShutdownFinal);
+}
+#endif
+
+static nsIThread* GetIOThreadAndAddUse_s() {
+ // Always runs on STS thread!
+#if defined(MOZILLA_INTERNAL_API)
+ // We need to safely release this on shutdown to avoid leaks
+ if (!sThread) {
+ sThread = new SingletonThreadHolder("mtransport"_ns);
+ NS_DispatchToMainThread(mozilla::WrapRunnableNM(&ClearSingletonOnShutdown));
+ }
+ // Mark that we're using the shared thread and need it to stick around
+ sThread->AddUse();
+ return sThread->GetThread();
+#else
+ static nsCOMPtr<nsIThread> sThread;
+ if (!sThread) {
+ (void)NS_NewNamedThread("mtransport", getter_AddRefs(sThread));
+ }
+ return sThread;
+#endif
+}
+
+NrSocketIpc::NrSocketIpc(nsIEventTarget* aThread) : io_thread_(aThread) {}
+
+static TimeStamp nr_socket_short_term_violation_time;
+static TimeStamp nr_socket_long_term_violation_time;
+
+TimeStamp NrSocketBase::short_term_violation_time() {
+ return nr_socket_short_term_violation_time;
+}
+
+TimeStamp NrSocketBase::long_term_violation_time() {
+ return nr_socket_long_term_violation_time;
+}
+
+// NrSocketBase implementation
+// async_event APIs
+int NrSocketBase::async_wait(int how, NR_async_cb cb, void* cb_arg,
+ char* function, int line) {
+ uint16_t flag;
+
+ switch (how) {
+ case NR_ASYNC_WAIT_READ:
+ flag = PR_POLL_READ;
+ break;
+ case NR_ASYNC_WAIT_WRITE:
+ flag = PR_POLL_WRITE;
+ break;
+ default:
+ return R_BAD_ARGS;
+ }
+
+ cbs_[how] = cb;
+ cb_args_[how] = cb_arg;
+ poll_flags_ |= flag;
+
+ return 0;
+}
+
+int NrSocketBase::cancel(int how) {
+ uint16_t flag;
+
+ switch (how) {
+ case NR_ASYNC_WAIT_READ:
+ flag = PR_POLL_READ;
+ break;
+ case NR_ASYNC_WAIT_WRITE:
+ flag = PR_POLL_WRITE;
+ break;
+ default:
+ return R_BAD_ARGS;
+ }
+
+ poll_flags_ &= ~flag;
+
+ return 0;
+}
+
+void NrSocketBase::fire_callback(int how) {
+ // This can't happen unless we are armed because we only set
+ // the flags if we are armed
+ MOZ_ASSERT(cbs_[how]);
+
+ // Now cancel so that we need to be re-armed. Note that
+ // the re-arming probably happens in the callback we are
+ // about to fire.
+ cancel(how);
+
+ cbs_[how](this, how, cb_args_[how]);
+}
+
+// NrSocket implementation
+NS_IMPL_QUERY_INTERFACE0(NrSocket)
+
+// The nsASocket callbacks
+void NrSocket::OnSocketReady(PRFileDesc* fd, int16_t outflags) {
+ if (outflags & PR_POLL_READ & poll_flags()) fire_callback(NR_ASYNC_WAIT_READ);
+ if (outflags & PR_POLL_WRITE & poll_flags())
+ fire_callback(NR_ASYNC_WAIT_WRITE);
+ if (outflags & (PR_POLL_ERR | PR_POLL_NVAL | PR_POLL_HUP))
+ // TODO: Bug 946423: how do we notify the upper layers about this?
+ close();
+}
+
+void NrSocket::OnSocketDetached(PRFileDesc* fd) {
+ r_log(LOG_GENERIC, LOG_DEBUG, "Socket %p detached", fd);
+}
+
+void NrSocket::IsLocal(bool* aIsLocal) {
+ // TODO(jesup): better check? Does it matter? (likely no)
+ *aIsLocal = false;
+}
+
+// async_event APIs
+int NrSocket::async_wait(int how, NR_async_cb cb, void* cb_arg, char* function,
+ int line) {
+ int r = NrSocketBase::async_wait(how, cb, cb_arg, function, line);
+
+ if (!r) {
+ mPollFlags = poll_flags();
+ }
+
+ return r;
+}
+
+int NrSocket::cancel(int how) {
+ int r = NrSocketBase::cancel(how);
+
+ if (!r) {
+ mPollFlags = poll_flags();
+ }
+
+ return r;
+}
+
+// Helper functions for addresses
+static int nr_transport_addr_to_praddr(const nr_transport_addr* addr,
+ PRNetAddr* naddr) {
+ int _status;
+
+ memset(naddr, 0, sizeof(*naddr));
+
+ switch (addr->protocol) {
+ case IPPROTO_TCP:
+ break;
+ case IPPROTO_UDP:
+ break;
+ default:
+ ABORT(R_BAD_ARGS);
+ }
+
+ switch (addr->ip_version) {
+ case NR_IPV4:
+ naddr->inet.family = PR_AF_INET;
+ naddr->inet.port = addr->u.addr4.sin_port;
+ naddr->inet.ip = addr->u.addr4.sin_addr.s_addr;
+ break;
+ case NR_IPV6:
+ naddr->ipv6.family = PR_AF_INET6;
+ naddr->ipv6.port = addr->u.addr6.sin6_port;
+ naddr->ipv6.flowinfo = addr->u.addr6.sin6_flowinfo;
+ memcpy(&naddr->ipv6.ip, &addr->u.addr6.sin6_addr, sizeof(in6_addr));
+ naddr->ipv6.scope_id = addr->u.addr6.sin6_scope_id;
+ break;
+ default:
+ ABORT(R_BAD_ARGS);
+ }
+
+ _status = 0;
+abort:
+ return (_status);
+}
+
+// XXX schien@mozilla.com: copy from PRNetAddrToNetAddr,
+// should be removed after fix the link error in signaling_unittests
+static int praddr_to_netaddr(const PRNetAddr* prAddr, net::NetAddr* addr) {
+ int _status;
+
+ switch (prAddr->raw.family) {
+ case PR_AF_INET:
+ addr->inet.family = AF_INET;
+ addr->inet.port = prAddr->inet.port;
+ addr->inet.ip = prAddr->inet.ip;
+ break;
+ case PR_AF_INET6:
+ addr->inet6.family = AF_INET6;
+ addr->inet6.port = prAddr->ipv6.port;
+ addr->inet6.flowinfo = prAddr->ipv6.flowinfo;
+ memcpy(&addr->inet6.ip, &prAddr->ipv6.ip, sizeof(addr->inet6.ip.u8));
+ addr->inet6.scope_id = prAddr->ipv6.scope_id;
+ break;
+ default:
+ MOZ_ASSERT(false);
+ ABORT(R_BAD_ARGS);
+ }
+
+ _status = 0;
+abort:
+ return (_status);
+}
+
+static int nr_transport_addr_to_netaddr(const nr_transport_addr* addr,
+ net::NetAddr* naddr) {
+ int r, _status;
+ PRNetAddr praddr;
+
+ if ((r = nr_transport_addr_to_praddr(addr, &praddr))) {
+ ABORT(r);
+ }
+
+ if ((r = praddr_to_netaddr(&praddr, naddr))) {
+ ABORT(r);
+ }
+
+ _status = 0;
+abort:
+ return (_status);
+}
+
+int nr_netaddr_to_transport_addr(const net::NetAddr* netaddr,
+ nr_transport_addr* addr, int protocol) {
+ int _status;
+ int r;
+
+ switch (netaddr->raw.family) {
+ case AF_INET:
+ if ((r = nr_ip4_port_to_transport_addr(ntohl(netaddr->inet.ip),
+ ntohs(netaddr->inet.port),
+ protocol, addr)))
+ ABORT(r);
+ break;
+ case AF_INET6:
+ if ((r = nr_ip6_port_to_transport_addr((in6_addr*)&netaddr->inet6.ip.u8,
+ ntohs(netaddr->inet6.port),
+ protocol, addr)))
+ ABORT(r);
+ break;
+ default:
+ MOZ_ASSERT(false);
+ ABORT(R_BAD_ARGS);
+ }
+ _status = 0;
+abort:
+ return (_status);
+}
+
+int nr_praddr_to_transport_addr(const PRNetAddr* praddr,
+ nr_transport_addr* addr, int protocol,
+ int keep) {
+ int _status;
+ int r;
+ struct sockaddr_in ip4;
+ struct sockaddr_in6 ip6;
+
+ switch (praddr->raw.family) {
+ case PR_AF_INET:
+ ip4.sin_family = PF_INET;
+ ip4.sin_addr.s_addr = praddr->inet.ip;
+ ip4.sin_port = praddr->inet.port;
+ if ((r = nr_sockaddr_to_transport_addr((sockaddr*)&ip4, protocol, keep,
+ addr)))
+ ABORT(r);
+ break;
+ case PR_AF_INET6:
+ ip6.sin6_family = PF_INET6;
+ ip6.sin6_port = praddr->ipv6.port;
+ ip6.sin6_flowinfo = praddr->ipv6.flowinfo;
+ memcpy(&ip6.sin6_addr, &praddr->ipv6.ip, sizeof(in6_addr));
+ ip6.sin6_scope_id = praddr->ipv6.scope_id;
+ if ((r = nr_sockaddr_to_transport_addr((sockaddr*)&ip6, protocol, keep,
+ addr)))
+ ABORT(r);
+ break;
+ default:
+ MOZ_ASSERT(false);
+ ABORT(R_BAD_ARGS);
+ }
+
+ _status = 0;
+abort:
+ return (_status);
+}
+
+/*
+ * nr_transport_addr_get_addrstring_and_port
+ * convert nr_transport_addr to IP address string and port number
+ */
+int nr_transport_addr_get_addrstring_and_port(const nr_transport_addr* addr,
+ nsACString* host, int32_t* port) {
+ int r, _status;
+ char addr_string[64];
+
+ // We cannot directly use |nr_transport_addr.as_string| because it contains
+ // more than ip address, therefore, we need to explicity convert it
+ // from |nr_transport_addr_get_addrstring|.
+ if ((r = nr_transport_addr_get_addrstring(addr, addr_string,
+ sizeof(addr_string)))) {
+ ABORT(r);
+ }
+
+ if ((r = nr_transport_addr_get_port(addr, port))) {
+ ABORT(r);
+ }
+
+ *host = addr_string;
+
+ _status = 0;
+abort:
+ return (_status);
+}
+
+// nr_socket APIs (as member functions)
+int NrSocket::create(nr_transport_addr* addr) {
+ int r, _status;
+
+ PRStatus status;
+ PRNetAddr naddr;
+
+ nsresult rv;
+ nsCOMPtr<nsISocketTransportService> stservice =
+ do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv);
+
+ if (!NS_SUCCEEDED(rv)) {
+ ABORT(R_INTERNAL);
+ }
+
+ if ((r = nr_transport_addr_to_praddr(addr, &naddr))) ABORT(r);
+
+ switch (addr->protocol) {
+ case IPPROTO_UDP:
+ if (!(fd_ = PR_OpenUDPSocket(naddr.raw.family))) {
+ r_log(LOG_GENERIC, LOG_CRIT,
+ "Couldn't create UDP socket, "
+ "family=%d, err=%d",
+ naddr.raw.family, PR_GetError());
+ ABORT(R_INTERNAL);
+ }
+#ifdef XP_WIN
+ if (!mozilla::IsWin8OrLater()) {
+ // Increase default send and receive buffer sizes on <= Win7 to be able
+ // to receive and send an unpaced HD (>= 720p = 1280x720 - I Frame ~ 21K
+ // size) stream without losing packets. Manual testing showed that 100K
+ // buffer size was not enough and the packet loss dis-appeared with 256K
+ // buffer size. See bug 1252769 for future improvements of this.
+ PRSize min_buffer_size = 256 * 1024;
+ PRSocketOptionData opt_rcvbuf;
+ opt_rcvbuf.option = PR_SockOpt_RecvBufferSize;
+ if ((status = PR_GetSocketOption(fd_, &opt_rcvbuf)) == PR_SUCCESS) {
+ if (opt_rcvbuf.value.recv_buffer_size < min_buffer_size) {
+ opt_rcvbuf.value.recv_buffer_size = min_buffer_size;
+ if ((status = PR_SetSocketOption(fd_, &opt_rcvbuf)) != PR_SUCCESS) {
+ r_log(LOG_GENERIC, LOG_CRIT,
+ "Couldn't set socket receive buffer size: %d", status);
+ }
+ } else {
+ r_log(LOG_GENERIC, LOG_INFO,
+ "Socket receive buffer size is already: %d",
+ opt_rcvbuf.value.recv_buffer_size);
+ }
+ } else {
+ r_log(LOG_GENERIC, LOG_CRIT,
+ "Couldn't get socket receive buffer size: %d", status);
+ }
+ PRSocketOptionData opt_sndbuf;
+ opt_sndbuf.option = PR_SockOpt_SendBufferSize;
+ if ((status = PR_GetSocketOption(fd_, &opt_sndbuf)) == PR_SUCCESS) {
+ if (opt_sndbuf.value.recv_buffer_size < min_buffer_size) {
+ opt_sndbuf.value.recv_buffer_size = min_buffer_size;
+ if ((status = PR_SetSocketOption(fd_, &opt_sndbuf)) != PR_SUCCESS) {
+ r_log(LOG_GENERIC, LOG_CRIT,
+ "Couldn't set socket send buffer size: %d", status);
+ }
+ } else {
+ r_log(LOG_GENERIC, LOG_INFO,
+ "Socket send buffer size is already: %d",
+ opt_sndbuf.value.recv_buffer_size);
+ }
+ } else {
+ r_log(LOG_GENERIC, LOG_CRIT,
+ "Couldn't get socket send buffer size: %d", status);
+ }
+ }
+#endif
+ break;
+ case IPPROTO_TCP:
+ // TODO: Rewrite this to use WebrtcTcpSocket.
+ // Also use the same logic for TLS.
+ if (my_addr_.fqdn[0] != '\0') ABORT(R_INTERNAL);
+
+ if (!(fd_ = PR_OpenTCPSocket(naddr.raw.family))) {
+ r_log(LOG_GENERIC, LOG_CRIT,
+ "Couldn't create TCP socket, "
+ "family=%d, err=%d",
+ naddr.raw.family, PR_GetError());
+ ABORT(R_INTERNAL);
+ }
+ // Set ReuseAddr for TCP sockets to enable having several
+ // sockets bound to same local IP and port
+ PRSocketOptionData opt_reuseaddr;
+ opt_reuseaddr.option = PR_SockOpt_Reuseaddr;
+ opt_reuseaddr.value.reuse_addr = PR_TRUE;
+ status = PR_SetSocketOption(fd_, &opt_reuseaddr);
+ if (status != PR_SUCCESS) {
+ r_log(LOG_GENERIC, LOG_CRIT,
+ "Couldn't set reuse addr socket option: %d", status);
+ ABORT(R_INTERNAL);
+ }
+ // And also set ReusePort for platforms supporting this socket option
+ PRSocketOptionData opt_reuseport;
+ opt_reuseport.option = PR_SockOpt_Reuseport;
+ opt_reuseport.value.reuse_port = PR_TRUE;
+ status = PR_SetSocketOption(fd_, &opt_reuseport);
+ if (status != PR_SUCCESS) {
+ if (PR_GetError() != PR_OPERATION_NOT_SUPPORTED_ERROR) {
+ r_log(LOG_GENERIC, LOG_CRIT,
+ "Couldn't set reuse port socket option: %d", status);
+ ABORT(R_INTERNAL);
+ }
+ }
+ // Try to speedup packet delivery by disabling TCP Nagle
+ PRSocketOptionData opt_nodelay;
+ opt_nodelay.option = PR_SockOpt_NoDelay;
+ opt_nodelay.value.no_delay = PR_TRUE;
+ status = PR_SetSocketOption(fd_, &opt_nodelay);
+ if (status != PR_SUCCESS) {
+ r_log(LOG_GENERIC, LOG_WARNING,
+ "Couldn't set Nodelay socket option: %d", status);
+ }
+ break;
+ default:
+ ABORT(R_INTERNAL);
+ }
+
+ status = PR_Bind(fd_, &naddr);
+ if (status != PR_SUCCESS) {
+ r_log(LOG_GENERIC, LOG_CRIT, "Couldn't bind socket to address %s",
+ addr->as_string);
+ ABORT(R_INTERNAL);
+ }
+
+ r_log(LOG_GENERIC, LOG_DEBUG, "Creating socket %p with addr %s", fd_,
+ addr->as_string);
+ nr_transport_addr_copy(&my_addr_, addr);
+
+ /* If we have a wildcard port, patch up the addr */
+ if (nr_transport_addr_is_wildcard(addr)) {
+ status = PR_GetSockName(fd_, &naddr);
+ if (status != PR_SUCCESS) {
+ r_log(LOG_GENERIC, LOG_CRIT, "Couldn't get sock name for socket");
+ ABORT(R_INTERNAL);
+ }
+
+ if ((r = nr_praddr_to_transport_addr(&naddr, &my_addr_, addr->protocol, 1)))
+ ABORT(r);
+ }
+
+ // Set nonblocking
+ PRSocketOptionData opt_nonblock;
+ opt_nonblock.option = PR_SockOpt_Nonblocking;
+ opt_nonblock.value.non_blocking = PR_TRUE;
+ status = PR_SetSocketOption(fd_, &opt_nonblock);
+ if (status != PR_SUCCESS) {
+ r_log(LOG_GENERIC, LOG_CRIT, "Couldn't make socket nonblocking");
+ ABORT(R_INTERNAL);
+ }
+
+ // Remember our thread.
+ ststhread_ = do_QueryInterface(stservice, &rv);
+ if (!NS_SUCCEEDED(rv)) ABORT(R_INTERNAL);
+
+ // Finally, register with the STS
+ rv = stservice->AttachSocket(fd_, this);
+ if (!NS_SUCCEEDED(rv)) {
+ r_log(LOG_GENERIC, LOG_CRIT, "Couldn't attach socket to STS, rv=%u",
+ static_cast<unsigned>(rv));
+ ABORT(R_INTERNAL);
+ }
+
+ _status = 0;
+
+abort:
+ return (_status);
+}
+
+static int ShouldDrop(size_t len) {
+ // Global rate limiting for stun requests, to mitigate the ice hammer DoS
+ // (see http://tools.ietf.org/html/draft-thomson-mmusic-ice-webrtc)
+
+ // Tolerate rate of 8k/sec, for one second.
+ static SimpleTokenBucket burst(16384 * 1, 16384);
+ // Tolerate rate of 7.2k/sec over twenty seconds.
+ static SimpleTokenBucket sustained(7372 * 20, 7372);
+
+ // Check number of tokens in each bucket.
+ if (burst.getTokens(UINT32_MAX) < len) {
+ r_log(LOG_GENERIC, LOG_ERR,
+ "Short term global rate limit for STUN requests exceeded.");
+#ifdef MOZILLA_INTERNAL_API
+ nr_socket_short_term_violation_time = TimeStamp::Now();
+#endif
+
+// Bug 1013007
+#if !EARLY_BETA_OR_EARLIER
+ return R_WOULDBLOCK;
+#else
+ MOZ_ASSERT(false,
+ "Short term global rate limit for STUN requests exceeded. Go "
+ "bug bcampen@mozilla.com if you weren't intentionally "
+ "spamming ICE candidates, or don't know what that means.");
+#endif
+ }
+
+ if (sustained.getTokens(UINT32_MAX) < len) {
+ r_log(LOG_GENERIC, LOG_ERR,
+ "Long term global rate limit for STUN requests exceeded.");
+#ifdef MOZILLA_INTERNAL_API
+ nr_socket_long_term_violation_time = TimeStamp::Now();
+#endif
+// Bug 1013007
+#if !EARLY_BETA_OR_EARLIER
+ return R_WOULDBLOCK;
+#else
+ MOZ_ASSERT(false,
+ "Long term global rate limit for STUN requests exceeded. Go "
+ "bug bcampen@mozilla.com if you weren't intentionally "
+ "spamming ICE candidates, or don't know what that means.");
+#endif
+ }
+
+ // Take len tokens from both buckets.
+ // (not threadsafe, but no problem since this is only called from STS)
+ burst.getTokens(len);
+ sustained.getTokens(len);
+ return 0;
+}
+
+// This should be called on the STS thread.
+int NrSocket::sendto(const void* msg, size_t len, int flags,
+ const nr_transport_addr* to) {
+ ASSERT_ON_THREAD(ststhread_);
+ int r, _status;
+ PRNetAddr naddr;
+ int32_t status;
+
+ if ((r = nr_transport_addr_to_praddr(to, &naddr))) ABORT(r);
+
+ if (fd_ == nullptr) ABORT(R_EOD);
+
+ if (nr_is_stun_request_message((UCHAR*)msg, len) && ShouldDrop(len)) {
+ ABORT(R_WOULDBLOCK);
+ }
+
+ // TODO: Convert flags?
+ status = PR_SendTo(fd_, msg, len, flags, &naddr, PR_INTERVAL_NO_WAIT);
+ if (status < 0 || (size_t)status != len) {
+ if (PR_GetError() == PR_WOULD_BLOCK_ERROR) ABORT(R_WOULDBLOCK);
+
+ r_log(LOG_GENERIC, LOG_INFO, "Error in sendto %s: %d", to->as_string,
+ PR_GetError());
+ ABORT(R_IO_ERROR);
+ }
+
+ _status = 0;
+abort:
+ return (_status);
+}
+
+int NrSocket::recvfrom(void* buf, size_t maxlen, size_t* len, int flags,
+ nr_transport_addr* from) {
+ ASSERT_ON_THREAD(ststhread_);
+ int r, _status;
+ PRNetAddr nfrom;
+ int32_t status;
+
+ status = PR_RecvFrom(fd_, buf, maxlen, flags, &nfrom, PR_INTERVAL_NO_WAIT);
+ if (status <= 0) {
+ if (PR_GetError() == PR_WOULD_BLOCK_ERROR) ABORT(R_WOULDBLOCK);
+ r_log(LOG_GENERIC, LOG_INFO, "Error in recvfrom: %d", (int)PR_GetError());
+ ABORT(R_IO_ERROR);
+ }
+ *len = status;
+
+ if ((r = nr_praddr_to_transport_addr(&nfrom, from, my_addr_.protocol, 0)))
+ ABORT(r);
+
+ // r_log(LOG_GENERIC,LOG_DEBUG,"Read %d bytes from %s",*len,addr->as_string);
+
+ _status = 0;
+abort:
+ return (_status);
+}
+
+int NrSocket::getaddr(nr_transport_addr* addrp) {
+ ASSERT_ON_THREAD(ststhread_);
+ return nr_transport_addr_copy(addrp, &my_addr_);
+}
+
+// Close the socket so that the STS will detach and then kill it
+void NrSocket::close() {
+ ASSERT_ON_THREAD(ststhread_);
+ mCondition = NS_BASE_STREAM_CLOSED;
+ cancel(NR_ASYNC_WAIT_READ);
+ cancel(NR_ASYNC_WAIT_WRITE);
+}
+
+int NrSocket::connect(const nr_transport_addr* addr) {
+ ASSERT_ON_THREAD(ststhread_);
+ int r, _status;
+ PRNetAddr naddr;
+ int32_t connect_status, getsockname_status;
+
+ if ((r = nr_transport_addr_to_praddr(addr, &naddr))) ABORT(r);
+
+ if (!fd_) ABORT(R_EOD);
+
+ // Note: this just means we tried to connect, not that we
+ // are actually live.
+ connect_invoked_ = true;
+ connect_status = PR_Connect(fd_, &naddr, PR_INTERVAL_NO_WAIT);
+ if (connect_status != PR_SUCCESS) {
+ if (PR_GetError() != PR_IN_PROGRESS_ERROR) {
+ r_log(LOG_GENERIC, LOG_CRIT, "PR_Connect failed: %d", PR_GetError());
+ ABORT(R_IO_ERROR);
+ }
+ }
+
+ // If our local address is wildcard, then fill in the
+ // address now.
+ if (nr_transport_addr_is_wildcard(&my_addr_)) {
+ getsockname_status = PR_GetSockName(fd_, &naddr);
+ if (getsockname_status != PR_SUCCESS) {
+ r_log(LOG_GENERIC, LOG_CRIT, "Couldn't get sock name for socket");
+ ABORT(R_INTERNAL);
+ }
+
+ if ((r = nr_praddr_to_transport_addr(&naddr, &my_addr_, addr->protocol, 1)))
+ ABORT(r);
+ }
+
+ // Now return the WOULDBLOCK if needed.
+ if (connect_status != PR_SUCCESS) {
+ ABORT(R_WOULDBLOCK);
+ }
+
+ _status = 0;
+abort:
+ return (_status);
+}
+
+int NrSocket::write(const void* msg, size_t len, size_t* written) {
+ ASSERT_ON_THREAD(ststhread_);
+ int _status;
+ int32_t status;
+
+ if (!connect_invoked_) ABORT(R_FAILED);
+
+ status = PR_Write(fd_, msg, len);
+ if (status < 0) {
+ if (PR_GetError() == PR_WOULD_BLOCK_ERROR) ABORT(R_WOULDBLOCK);
+ r_log(LOG_GENERIC, LOG_INFO, "Error in write");
+ ABORT(R_IO_ERROR);
+ }
+
+ *written = status;
+
+ _status = 0;
+abort:
+ return _status;
+}
+
+int NrSocket::read(void* buf, size_t maxlen, size_t* len) {
+ ASSERT_ON_THREAD(ststhread_);
+ int _status;
+ int32_t status;
+
+ if (!connect_invoked_) ABORT(R_FAILED);
+
+ status = PR_Read(fd_, buf, maxlen);
+ if (status < 0) {
+ if (PR_GetError() == PR_WOULD_BLOCK_ERROR) ABORT(R_WOULDBLOCK);
+ r_log(LOG_GENERIC, LOG_INFO, "Error in read");
+ ABORT(R_IO_ERROR);
+ }
+ if (status == 0) ABORT(R_EOD);
+
+ *len = (size_t)status; // Guaranteed to be > 0
+ _status = 0;
+abort:
+ return (_status);
+}
+
+int NrSocket::listen(int backlog) {
+ ASSERT_ON_THREAD(ststhread_);
+ int32_t status;
+ int _status;
+
+ assert(fd_);
+ status = PR_Listen(fd_, backlog);
+ if (status != PR_SUCCESS) {
+ r_log(LOG_GENERIC, LOG_CRIT, "%s: PR_GetError() == %d", __FUNCTION__,
+ PR_GetError());
+ ABORT(R_IO_ERROR);
+ }
+
+ _status = 0;
+abort:
+ return (_status);
+}
+
+int NrSocket::accept(nr_transport_addr* addrp, nr_socket** sockp) {
+ ASSERT_ON_THREAD(ststhread_);
+ int _status, r;
+ PRStatus status;
+ PRFileDesc* prfd;
+ PRNetAddr nfrom;
+ NrSocket* sock = nullptr;
+ nsresult rv;
+ PRSocketOptionData opt_nonblock, opt_nodelay;
+ nsCOMPtr<nsISocketTransportService> stservice =
+ do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv);
+
+ if (NS_FAILED(rv)) {
+ ABORT(R_INTERNAL);
+ }
+
+ if (!fd_) ABORT(R_EOD);
+
+ prfd = PR_Accept(fd_, &nfrom, PR_INTERVAL_NO_WAIT);
+
+ if (!prfd) {
+ if (PR_GetError() == PR_WOULD_BLOCK_ERROR) ABORT(R_WOULDBLOCK);
+
+ ABORT(R_IO_ERROR);
+ }
+
+ sock = new NrSocket();
+
+ sock->fd_ = prfd;
+ nr_transport_addr_copy(&sock->my_addr_, &my_addr_);
+
+ if ((r = nr_praddr_to_transport_addr(&nfrom, addrp, my_addr_.protocol, 0)))
+ ABORT(r);
+
+ // Set nonblocking
+ opt_nonblock.option = PR_SockOpt_Nonblocking;
+ opt_nonblock.value.non_blocking = PR_TRUE;
+ status = PR_SetSocketOption(prfd, &opt_nonblock);
+ if (status != PR_SUCCESS) {
+ r_log(LOG_GENERIC, LOG_CRIT,
+ "Failed to make accepted socket nonblocking: %d", status);
+ ABORT(R_INTERNAL);
+ }
+ // Disable TCP Nagle
+ opt_nodelay.option = PR_SockOpt_NoDelay;
+ opt_nodelay.value.no_delay = PR_TRUE;
+ status = PR_SetSocketOption(prfd, &opt_nodelay);
+ if (status != PR_SUCCESS) {
+ r_log(LOG_GENERIC, LOG_WARNING,
+ "Failed to set Nodelay on accepted socket: %d", status);
+ }
+
+ // Should fail only with OOM
+ if ((r = nr_socket_create_int(static_cast<void*>(sock), sock->vtbl(), sockp)))
+ ABORT(r);
+
+ // Remember our thread.
+ sock->ststhread_ = do_QueryInterface(stservice, &rv);
+ if (NS_FAILED(rv)) ABORT(R_INTERNAL);
+
+ // Finally, register with the STS
+ rv = stservice->AttachSocket(prfd, sock);
+ if (NS_FAILED(rv)) {
+ ABORT(R_INTERNAL);
+ }
+
+ sock->connect_invoked_ = true;
+
+ // Add a reference so that we can delete it in destroy()
+ sock->AddRef();
+ _status = 0;
+abort:
+ if (_status) {
+ delete sock;
+ }
+
+ return (_status);
+}
+
+NS_IMPL_ISUPPORTS(NrUdpSocketIpcProxy, nsIUDPSocketInternal)
+
+nsresult NrUdpSocketIpcProxy::Init(const RefPtr<NrUdpSocketIpc>& socket) {
+ nsresult rv;
+ sts_thread_ = do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) {
+ MOZ_ASSERT(false, "Failed to get STS thread");
+ return rv;
+ }
+
+ socket_ = socket;
+ return NS_OK;
+}
+
+NrUdpSocketIpcProxy::~NrUdpSocketIpcProxy() {
+ // Send our ref to STS to be released
+ RUN_ON_THREAD(sts_thread_, mozilla::WrapRelease(socket_.forget()),
+ NS_DISPATCH_NORMAL);
+}
+
+// IUDPSocketInternal interfaces
+// callback while error happened in UDP socket operation
+NS_IMETHODIMP NrUdpSocketIpcProxy::CallListenerError(const nsACString& message,
+ const nsACString& filename,
+ uint32_t line_number) {
+ return socket_->CallListenerError(message, filename, line_number);
+}
+
+// callback while receiving UDP packet
+NS_IMETHODIMP NrUdpSocketIpcProxy::CallListenerReceivedData(
+ const nsACString& host, uint16_t port, const nsTArray<uint8_t>& data) {
+ return socket_->CallListenerReceivedData(host, port, data);
+}
+
+// callback while UDP socket is opened
+NS_IMETHODIMP NrUdpSocketIpcProxy::CallListenerOpened() {
+ return socket_->CallListenerOpened();
+}
+
+// callback while UDP socket is connected
+NS_IMETHODIMP NrUdpSocketIpcProxy::CallListenerConnected() {
+ return socket_->CallListenerConnected();
+}
+
+// callback while UDP socket is closed
+NS_IMETHODIMP NrUdpSocketIpcProxy::CallListenerClosed() {
+ return socket_->CallListenerClosed();
+}
+
+// NrUdpSocketIpc Implementation
+NrUdpSocketIpc::NrUdpSocketIpc()
+ : NrSocketIpc(GetIOThreadAndAddUse_s()),
+ monitor_("NrUdpSocketIpc"),
+ err_(false),
+ state_(NR_INIT) {}
+
+NrUdpSocketIpc::~NrUdpSocketIpc() = default;
+
+void NrUdpSocketIpc::Destroy() {
+#if defined(MOZILLA_INTERNAL_API)
+ // destroy_i also dispatches back to STS to call ReleaseUse, to avoid shutting
+ // down the IO thread before close() runs.
+ // We use a NonOwning runnable because our refcount has already gone to 0.
+ io_thread_->Dispatch(NewNonOwningRunnableMethod(
+ "NrUdpSocketIpc::Destroy", this, &NrUdpSocketIpc::destroy_i));
+#endif
+}
+
+// IUDPSocketInternal interfaces
+// callback while error happened in UDP socket operation
+NS_IMETHODIMP NrUdpSocketIpc::CallListenerError(const nsACString& message,
+ const nsACString& filename,
+ uint32_t line_number) {
+ ASSERT_ON_THREAD(io_thread_);
+
+ r_log(LOG_GENERIC, LOG_ERR, "UDP socket error:%s at %s:%d this=%p",
+ message.BeginReading(), filename.BeginReading(), line_number,
+ (void*)this);
+
+ ReentrantMonitorAutoEnter mon(monitor_);
+ err_ = true;
+ monitor_.NotifyAll();
+
+ return NS_OK;
+}
+
+// callback while receiving UDP packet
+NS_IMETHODIMP NrUdpSocketIpc::CallListenerReceivedData(
+ const nsACString& host, uint16_t port, const nsTArray<uint8_t>& data) {
+ ASSERT_ON_THREAD(io_thread_);
+
+ PRNetAddr addr;
+ memset(&addr, 0, sizeof(addr));
+
+ {
+ ReentrantMonitorAutoEnter mon(monitor_);
+
+ if (PR_SUCCESS != PR_StringToNetAddr(host.BeginReading(), &addr)) {
+ err_ = true;
+ MOZ_ASSERT(false, "Failed to convert remote host to PRNetAddr");
+ return NS_OK;
+ }
+
+ // Use PR_IpAddrNull to avoid address being reset to 0.
+ if (PR_SUCCESS !=
+ PR_SetNetAddr(PR_IpAddrNull, addr.raw.family, port, &addr)) {
+ err_ = true;
+ MOZ_ASSERT(false, "Failed to set port in PRNetAddr");
+ return NS_OK;
+ }
+ }
+
+ auto buf = MakeUnique<MediaPacket>();
+ buf->Copy(data.Elements(), data.Length());
+ RefPtr<nr_udp_message> msg(new nr_udp_message(addr, std::move(buf)));
+
+ RUN_ON_THREAD(sts_thread_,
+ mozilla::WrapRunnable(RefPtr<NrUdpSocketIpc>(this),
+ &NrUdpSocketIpc::recv_callback_s, msg),
+ NS_DISPATCH_NORMAL);
+ return NS_OK;
+}
+
+nsresult NrUdpSocketIpc::SetAddress() {
+ uint16_t port = socket_child_->LocalPort();
+
+ nsAutoCString address(socket_child_->LocalAddress());
+
+ PRNetAddr praddr;
+ if (PR_SUCCESS != PR_InitializeNetAddr(PR_IpAddrAny, port, &praddr)) {
+ err_ = true;
+ MOZ_ASSERT(false, "Failed to set port in PRNetAddr");
+ return NS_OK;
+ }
+
+ if (PR_SUCCESS != PR_StringToNetAddr(address.BeginReading(), &praddr)) {
+ err_ = true;
+ MOZ_ASSERT(false, "Failed to convert local host to PRNetAddr");
+ return NS_OK;
+ }
+
+ nr_transport_addr expected_addr;
+ if (nr_transport_addr_copy(&expected_addr, &my_addr_)) {
+ err_ = true;
+ MOZ_ASSERT(false, "Failed to copy my_addr_");
+ }
+
+ if (nr_praddr_to_transport_addr(&praddr, &my_addr_, IPPROTO_UDP, 1)) {
+ err_ = true;
+ MOZ_ASSERT(false, "Failed to copy local host to my_addr_");
+ }
+
+ if (!nr_transport_addr_is_wildcard(&expected_addr) &&
+ nr_transport_addr_cmp(&expected_addr, &my_addr_,
+ NR_TRANSPORT_ADDR_CMP_MODE_ADDR)) {
+ err_ = true;
+ MOZ_ASSERT(false, "Address of opened socket is not expected");
+ }
+
+ return NS_OK;
+}
+
+// callback while UDP socket is opened
+NS_IMETHODIMP NrUdpSocketIpc::CallListenerOpened() {
+ ASSERT_ON_THREAD(io_thread_);
+ ReentrantMonitorAutoEnter mon(monitor_);
+
+ r_log(LOG_GENERIC, LOG_DEBUG, "UDP socket opened this=%p", (void*)this);
+ nsresult rv = SetAddress();
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ mon.NotifyAll();
+
+ return NS_OK;
+}
+
+// callback while UDP socket is connected
+NS_IMETHODIMP NrUdpSocketIpc::CallListenerConnected() {
+ ASSERT_ON_THREAD(io_thread_);
+
+ ReentrantMonitorAutoEnter mon(monitor_);
+
+ r_log(LOG_GENERIC, LOG_DEBUG, "UDP socket connected this=%p", (void*)this);
+ MOZ_ASSERT(state_ == NR_CONNECTED);
+
+ nsresult rv = SetAddress();
+ if (NS_FAILED(rv)) {
+ mon.NotifyAll();
+ return rv;
+ }
+
+ r_log(LOG_GENERIC, LOG_INFO, "Exit UDP socket connected");
+ mon.NotifyAll();
+
+ return NS_OK;
+}
+
+// callback while UDP socket is closed
+NS_IMETHODIMP NrUdpSocketIpc::CallListenerClosed() {
+ ASSERT_ON_THREAD(io_thread_);
+
+ ReentrantMonitorAutoEnter mon(monitor_);
+
+ r_log(LOG_GENERIC, LOG_DEBUG, "UDP socket closed this=%p", (void*)this);
+ MOZ_ASSERT(state_ == NR_CONNECTED || state_ == NR_CLOSING);
+ state_ = NR_CLOSED;
+
+ return NS_OK;
+}
+
+//
+// NrSocketBase methods.
+//
+int NrUdpSocketIpc::create(nr_transport_addr* addr) {
+ ASSERT_ON_THREAD(sts_thread_);
+
+ int r, _status;
+ nsresult rv;
+ int32_t port;
+ nsCString host;
+
+ ReentrantMonitorAutoEnter mon(monitor_);
+
+ if (state_ != NR_INIT) {
+ ABORT(R_INTERNAL);
+ }
+
+ sts_thread_ = do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) {
+ MOZ_ASSERT(false, "Failed to get STS thread");
+ ABORT(R_INTERNAL);
+ }
+
+ if ((r = nr_transport_addr_get_addrstring_and_port(addr, &host, &port))) {
+ ABORT(r);
+ }
+
+ // wildcard address will be resolved at NrUdpSocketIpc::CallListenerVoid
+ if ((r = nr_transport_addr_copy(&my_addr_, addr))) {
+ ABORT(r);
+ }
+
+ state_ = NR_CONNECTING;
+
+ MOZ_ASSERT(io_thread_);
+ RUN_ON_THREAD(io_thread_,
+ mozilla::WrapRunnable(RefPtr<NrUdpSocketIpc>(this),
+ &NrUdpSocketIpc::create_i, host,
+ static_cast<uint16_t>(port)),
+ NS_DISPATCH_NORMAL);
+
+ // Wait until socket creation complete.
+ mon.Wait();
+
+ if (err_) {
+ close();
+ ABORT(R_INTERNAL);
+ }
+
+ state_ = NR_CONNECTED;
+
+ _status = 0;
+abort:
+ return (_status);
+}
+
+int NrUdpSocketIpc::sendto(const void* msg, size_t len, int flags,
+ const nr_transport_addr* to) {
+ ASSERT_ON_THREAD(sts_thread_);
+
+ ReentrantMonitorAutoEnter mon(monitor_);
+
+ // If send err happened before, simply return the error.
+ if (err_) {
+ return R_IO_ERROR;
+ }
+
+ if (state_ != NR_CONNECTED) {
+ return R_INTERNAL;
+ }
+
+ int r;
+ net::NetAddr addr;
+ if ((r = nr_transport_addr_to_netaddr(to, &addr))) {
+ return r;
+ }
+
+ if (nr_is_stun_request_message((UCHAR*)msg, len) && ShouldDrop(len)) {
+ return R_WOULDBLOCK;
+ }
+
+ UniquePtr<MediaPacket> buf(new MediaPacket);
+ buf->Copy(static_cast<const uint8_t*>(msg), len);
+
+ RUN_ON_THREAD(
+ io_thread_,
+ mozilla::WrapRunnable(RefPtr<NrUdpSocketIpc>(this),
+ &NrUdpSocketIpc::sendto_i, addr, std::move(buf)),
+ NS_DISPATCH_NORMAL);
+ return 0;
+}
+
+void NrUdpSocketIpc::close() {
+ r_log(LOG_GENERIC, LOG_DEBUG, "NrUdpSocketIpc::close()");
+
+ ASSERT_ON_THREAD(sts_thread_);
+
+ ReentrantMonitorAutoEnter mon(monitor_);
+ state_ = NR_CLOSING;
+
+ RUN_ON_THREAD(io_thread_,
+ mozilla::WrapRunnable(RefPtr<NrUdpSocketIpc>(this),
+ &NrUdpSocketIpc::close_i),
+ NS_DISPATCH_NORMAL);
+
+ // remove all enqueued messages
+ std::queue<RefPtr<nr_udp_message>> empty;
+ std::swap(received_msgs_, empty);
+}
+
+int NrUdpSocketIpc::recvfrom(void* buf, size_t maxlen, size_t* len, int flags,
+ nr_transport_addr* from) {
+ ASSERT_ON_THREAD(sts_thread_);
+
+ ReentrantMonitorAutoEnter mon(monitor_);
+
+ int r, _status;
+ uint32_t consumed_len;
+
+ *len = 0;
+
+ if (state_ != NR_CONNECTED) {
+ ABORT(R_INTERNAL);
+ }
+
+ if (received_msgs_.empty()) {
+ ABORT(R_WOULDBLOCK);
+ }
+
+ {
+ RefPtr<nr_udp_message> msg(received_msgs_.front());
+
+ received_msgs_.pop();
+
+ if ((r = nr_praddr_to_transport_addr(&msg->from, from, IPPROTO_UDP, 0))) {
+ err_ = true;
+ MOZ_ASSERT(false, "Get bogus address for received UDP packet");
+ ABORT(r);
+ }
+
+ consumed_len = std::min(maxlen, msg->data->len());
+ if (consumed_len < msg->data->len()) {
+ r_log(LOG_GENERIC, LOG_DEBUG,
+ "Partial received UDP packet will be discard");
+ }
+
+ memcpy(buf, msg->data->data(), consumed_len);
+ *len = consumed_len;
+ }
+
+ _status = 0;
+abort:
+ return (_status);
+}
+
+int NrUdpSocketIpc::getaddr(nr_transport_addr* addrp) {
+ ASSERT_ON_THREAD(sts_thread_);
+
+ ReentrantMonitorAutoEnter mon(monitor_);
+
+ return nr_transport_addr_copy(addrp, &my_addr_);
+}
+
+int NrUdpSocketIpc::connect(const nr_transport_addr* addr) {
+ int r, _status;
+ int32_t port;
+ nsCString host;
+
+ ReentrantMonitorAutoEnter mon(monitor_);
+ r_log(LOG_GENERIC, LOG_DEBUG, "NrUdpSocketIpc::connect(%s) this=%p",
+ addr->as_string, (void*)this);
+
+ if ((r = nr_transport_addr_get_addrstring_and_port(addr, &host, &port))) {
+ ABORT(r);
+ }
+
+ RUN_ON_THREAD(io_thread_,
+ mozilla::WrapRunnable(RefPtr<NrUdpSocketIpc>(this),
+ &NrUdpSocketIpc::connect_i, host,
+ static_cast<uint16_t>(port)),
+ NS_DISPATCH_NORMAL);
+
+ // Wait until connect() completes.
+ mon.Wait();
+
+ r_log(LOG_GENERIC, LOG_DEBUG,
+ "NrUdpSocketIpc::connect this=%p completed err_ = %s", (void*)this,
+ err_ ? "true" : "false");
+
+ if (err_) {
+ ABORT(R_INTERNAL);
+ }
+
+ _status = 0;
+abort:
+ return _status;
+}
+
+int NrUdpSocketIpc::write(const void* msg, size_t len, size_t* written) {
+ MOZ_ASSERT(false);
+ return R_INTERNAL;
+}
+
+int NrUdpSocketIpc::read(void* buf, size_t maxlen, size_t* len) {
+ MOZ_ASSERT(false);
+ return R_INTERNAL;
+}
+
+int NrUdpSocketIpc::listen(int backlog) {
+ MOZ_ASSERT(false);
+ return R_INTERNAL;
+}
+
+int NrUdpSocketIpc::accept(nr_transport_addr* addrp, nr_socket** sockp) {
+ MOZ_ASSERT(false);
+ return R_INTERNAL;
+}
+
+// IO thread executors
+void NrUdpSocketIpc::create_i(const nsACString& host, const uint16_t port) {
+ ASSERT_ON_THREAD(io_thread_);
+
+ uint32_t minBuffSize = 0;
+ RefPtr<dom::UDPSocketChild> socketChild = new dom::UDPSocketChild();
+
+ // This can spin the event loop; don't do that with the monitor held
+ socketChild->SetBackgroundSpinsEvents();
+
+ ReentrantMonitorAutoEnter mon(monitor_);
+ if (!socket_child_) {
+ socket_child_ = socketChild;
+ socket_child_->SetFilterName(
+ nsCString(NS_NETWORK_SOCKET_FILTER_HANDLER_STUN_SUFFIX));
+ } else {
+ socketChild = nullptr;
+ }
+
+ RefPtr<NrUdpSocketIpcProxy> proxy(new NrUdpSocketIpcProxy);
+ nsresult rv = proxy->Init(this);
+ if (NS_FAILED(rv)) {
+ err_ = true;
+ mon.NotifyAll();
+ return;
+ }
+
+#ifdef XP_WIN
+ if (!mozilla::IsWin8OrLater()) {
+ // Increase default receive and send buffer size on <= Win7 to be able to
+ // receive and send an unpaced HD (>= 720p = 1280x720 - I Frame ~ 21K size)
+ // stream without losing packets.
+ // Manual testing showed that 100K buffer size was not enough and the
+ // packet loss dis-appeared with 256K buffer size.
+ // See bug 1252769 for future improvements of this.
+ minBuffSize = 256 * 1024;
+ }
+#endif
+ // XXX bug 1126232 - don't use null Principal!
+ if (NS_FAILED(socket_child_->Bind(proxy, nullptr, host, port,
+ /* addressReuse = */ false,
+ /* loopback = */ false,
+ /* recv buffer size */ minBuffSize,
+ /* send buffer size */ minBuffSize))) {
+ err_ = true;
+ MOZ_ASSERT(false, "Failed to create UDP socket");
+ mon.NotifyAll();
+ return;
+ }
+}
+
+void NrUdpSocketIpc::connect_i(const nsACString& host, const uint16_t port) {
+ ASSERT_ON_THREAD(io_thread_);
+ nsresult rv;
+ ReentrantMonitorAutoEnter mon(monitor_);
+
+ RefPtr<NrUdpSocketIpcProxy> proxy(new NrUdpSocketIpcProxy);
+ rv = proxy->Init(this);
+ if (NS_FAILED(rv)) {
+ err_ = true;
+ mon.NotifyAll();
+ return;
+ }
+
+ socket_child_->Connect(proxy, host, port);
+}
+
+void NrUdpSocketIpc::sendto_i(const net::NetAddr& addr,
+ UniquePtr<MediaPacket> buf) {
+ ASSERT_ON_THREAD(io_thread_);
+
+ ReentrantMonitorAutoEnter mon(monitor_);
+
+ if (!socket_child_) {
+ MOZ_ASSERT(false);
+ err_ = true;
+ return;
+ }
+ if (NS_FAILED(
+ socket_child_->SendWithAddress(&addr, buf->data(), buf->len()))) {
+ err_ = true;
+ }
+}
+
+void NrUdpSocketIpc::close_i() {
+ ASSERT_ON_THREAD(io_thread_);
+
+ if (socket_child_) {
+ socket_child_->Close();
+ socket_child_ = nullptr;
+ }
+}
+
+#if defined(MOZILLA_INTERNAL_API)
+
+static void ReleaseIOThread_s() { sThread->ReleaseUse(); }
+
+void NrUdpSocketIpc::destroy_i() {
+ close_i();
+
+ RUN_ON_THREAD(sts_thread_, WrapRunnableNM(&ReleaseIOThread_s),
+ NS_DISPATCH_NORMAL);
+}
+#endif
+
+void NrUdpSocketIpc::recv_callback_s(RefPtr<nr_udp_message> msg) {
+ ASSERT_ON_THREAD(sts_thread_);
+
+ {
+ ReentrantMonitorAutoEnter mon(monitor_);
+ if (state_ != NR_CONNECTED) {
+ return;
+ }
+ }
+
+ // enqueue received message
+ received_msgs_.push(msg);
+
+ if ((poll_flags() & PR_POLL_READ)) {
+ fire_callback(NR_ASYNC_WAIT_READ);
+ }
+}
+
+} // namespace mozilla
+
+using namespace mozilla;
+
+// Bridge to the nr_socket interface
+static int nr_socket_local_destroy(void** objp);
+static int nr_socket_local_sendto(void* obj, const void* msg, size_t len,
+ int flags, const nr_transport_addr* to);
+static int nr_socket_local_recvfrom(void* obj, void* restrict buf,
+ size_t maxlen, size_t* len, int flags,
+ nr_transport_addr* from);
+static int nr_socket_local_getfd(void* obj, NR_SOCKET* fd);
+static int nr_socket_local_getaddr(void* obj, nr_transport_addr* addrp);
+static int nr_socket_local_close(void* obj);
+static int nr_socket_local_connect(void* obj, const nr_transport_addr* addr);
+static int nr_socket_local_write(void* obj, const void* msg, size_t len,
+ size_t* written);
+static int nr_socket_local_read(void* obj, void* restrict buf, size_t maxlen,
+ size_t* len);
+static int nr_socket_local_listen(void* obj, int backlog);
+static int nr_socket_local_accept(void* obj, nr_transport_addr* addrp,
+ nr_socket** sockp);
+
+static nr_socket_vtbl nr_socket_local_vtbl = {2,
+ nr_socket_local_destroy,
+ nr_socket_local_sendto,
+ nr_socket_local_recvfrom,
+ nr_socket_local_getfd,
+ nr_socket_local_getaddr,
+ nr_socket_local_connect,
+ nr_socket_local_write,
+ nr_socket_local_read,
+ nr_socket_local_close,
+ nr_socket_local_listen,
+ nr_socket_local_accept};
+
+/* static */
+int NrSocketBase::CreateSocket(
+ nr_transport_addr* addr, RefPtr<NrSocketBase>* sock,
+ const std::shared_ptr<NrSocketProxyConfig>& config) {
+ int r, _status;
+
+ if (IsForbiddenAddress(addr)) {
+ ABORT(R_REJECTED);
+ }
+
+ if (config && config->GetForceProxy() && addr->protocol == IPPROTO_UDP) {
+ ABORT(R_REJECTED);
+ }
+
+ // create IPC bridge for content process
+ if (XRE_IsParentProcess()) {
+ // TODO: Make NrTcpSocket work on the parent process
+ *sock = new NrSocket();
+ } else if (XRE_IsSocketProcess()) {
+ if (addr->protocol == IPPROTO_TCP) {
+ *sock = new NrTcpSocket(config);
+ } else {
+ *sock = new NrSocket();
+ }
+ } else {
+ if (addr->protocol == IPPROTO_TCP) {
+ *sock = new NrTcpSocket(config);
+ } else {
+ *sock = new NrUdpSocketIpc();
+ }
+ }
+
+ r = (*sock)->create(addr);
+ if (r) ABORT(r);
+
+ _status = 0;
+abort:
+ if (_status) {
+ *sock = nullptr;
+ }
+ return _status;
+}
+
+// static
+bool NrSocketBase::IsForbiddenAddress(nr_transport_addr* addr) {
+ int r, port;
+
+ r = nr_transport_addr_get_port(addr, &port);
+ if (r) {
+ return true;
+ }
+
+ // allow auto assigned ports
+ if (port != 0) {
+ // Don't need to check an override scheme
+ nsresult rv = NS_CheckPortSafety(port, nullptr);
+ if (NS_FAILED(rv)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+static int nr_socket_local_destroy(void** objp) {
+ if (!objp || !*objp) return 0;
+
+ NrSocketBase* sock = static_cast<NrSocketBase*>(*objp);
+ *objp = nullptr;
+
+ sock->close(); // Signal STS that we want not to listen
+ sock->Release(); // Decrement the ref count
+
+ return 0;
+}
+
+static int nr_socket_local_sendto(void* obj, const void* msg, size_t len,
+ int flags, const nr_transport_addr* addr) {
+ NrSocketBase* sock = static_cast<NrSocketBase*>(obj);
+
+ return sock->sendto(msg, len, flags, addr);
+}
+
+static int nr_socket_local_recvfrom(void* obj, void* restrict buf,
+ size_t maxlen, size_t* len, int flags,
+ nr_transport_addr* addr) {
+ NrSocketBase* sock = static_cast<NrSocketBase*>(obj);
+
+ return sock->recvfrom(buf, maxlen, len, flags, addr);
+}
+
+static int nr_socket_local_getfd(void* obj, NR_SOCKET* fd) {
+ NrSocketBase* sock = static_cast<NrSocketBase*>(obj);
+
+ *fd = sock;
+
+ return 0;
+}
+
+static int nr_socket_local_getaddr(void* obj, nr_transport_addr* addrp) {
+ NrSocketBase* sock = static_cast<NrSocketBase*>(obj);
+
+ return sock->getaddr(addrp);
+}
+
+static int nr_socket_local_close(void* obj) {
+ NrSocketBase* sock = static_cast<NrSocketBase*>(obj);
+
+ sock->close();
+
+ return 0;
+}
+
+static int nr_socket_local_write(void* obj, const void* msg, size_t len,
+ size_t* written) {
+ NrSocketBase* sock = static_cast<NrSocketBase*>(obj);
+
+ return sock->write(msg, len, written);
+}
+
+static int nr_socket_local_read(void* obj, void* restrict buf, size_t maxlen,
+ size_t* len) {
+ NrSocketBase* sock = static_cast<NrSocketBase*>(obj);
+
+ return sock->read(buf, maxlen, len);
+}
+
+static int nr_socket_local_connect(void* obj, const nr_transport_addr* addr) {
+ NrSocketBase* sock = static_cast<NrSocketBase*>(obj);
+
+ return sock->connect(addr);
+}
+
+static int nr_socket_local_listen(void* obj, int backlog) {
+ NrSocketBase* sock = static_cast<NrSocketBase*>(obj);
+
+ return sock->listen(backlog);
+}
+
+static int nr_socket_local_accept(void* obj, nr_transport_addr* addrp,
+ nr_socket** sockp) {
+ NrSocketBase* sock = static_cast<NrSocketBase*>(obj);
+
+ return sock->accept(addrp, sockp);
+}
+
+// Implement async api
+int NR_async_wait(NR_SOCKET sock, int how, NR_async_cb cb, void* cb_arg,
+ char* function, int line) {
+ NrSocketBase* s = static_cast<NrSocketBase*>(sock);
+
+ return s->async_wait(how, cb, cb_arg, function, line);
+}
+
+int NR_async_cancel(NR_SOCKET sock, int how) {
+ NrSocketBase* s = static_cast<NrSocketBase*>(sock);
+
+ return s->cancel(how);
+}
+
+nr_socket_vtbl* NrSocketBase::vtbl() { return &nr_socket_local_vtbl; }
diff --git a/dom/media/webrtc/transport/nr_socket_prsock.h b/dom/media/webrtc/transport/nr_socket_prsock.h
new file mode 100644
index 0000000000..010fcb59bc
--- /dev/null
+++ b/dom/media/webrtc/transport/nr_socket_prsock.h
@@ -0,0 +1,320 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+/* Some source code here from nICEr. Copyright is:
+
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+// Implementation of nICEr/nr_socket that is tied to the Gecko
+// SocketTransportService.
+
+#ifndef nr_socket_prsock__
+#define nr_socket_prsock__
+
+#include <memory>
+#include <queue>
+
+#include "nspr.h"
+#include "prio.h"
+
+#include "nsCOMPtr.h"
+#include "nsASocketHandler.h"
+#include "nsXPCOM.h"
+#include "nsIEventTarget.h"
+#include "nsIUDPSocketChild.h"
+#include "nsProxyRelease.h"
+#include "nsThreadUtils.h"
+
+#include "mediapacket.h"
+#include "m_cpp_utils.h"
+#include "mozilla/ReentrantMonitor.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/ClearOnShutdown.h"
+
+// nICEr includes
+extern "C" {
+#include "transport_addr.h"
+#include "async_wait.h"
+}
+
+// Stub declaration for nICEr type
+typedef struct nr_socket_vtbl_ nr_socket_vtbl;
+typedef struct nr_socket_ nr_socket;
+
+#if defined(MOZILLA_INTERNAL_API)
+namespace mozilla {
+class NrSocketProxyConfig;
+} // namespace mozilla
+#endif
+
+namespace mozilla {
+
+namespace net {
+union NetAddr;
+}
+
+namespace dom {
+class UDPSocketChild;
+} // namespace dom
+
+class NrSocketBase {
+ public:
+ NrSocketBase() : connect_invoked_(false), poll_flags_(0) {
+ memset(cbs_, 0, sizeof(cbs_));
+ memset(cb_args_, 0, sizeof(cb_args_));
+ memset(&my_addr_, 0, sizeof(my_addr_));
+ }
+ virtual ~NrSocketBase() = default;
+
+ // Factory method; will create either an NrSocket, NrUdpSocketIpc, or
+ // NrTcpSocketIpc as appropriate.
+ static int CreateSocket(nr_transport_addr* addr, RefPtr<NrSocketBase>* sock,
+ const std::shared_ptr<NrSocketProxyConfig>& config);
+ static bool IsForbiddenAddress(nr_transport_addr* addr);
+
+ // the nr_socket APIs
+ virtual int create(nr_transport_addr* addr) = 0;
+ virtual int sendto(const void* msg, size_t len, int flags,
+ const nr_transport_addr* to) = 0;
+ virtual int recvfrom(void* buf, size_t maxlen, size_t* len, int flags,
+ nr_transport_addr* from) = 0;
+ virtual int getaddr(nr_transport_addr* addrp) = 0;
+ virtual void close() = 0;
+ virtual int connect(const nr_transport_addr* addr) = 0;
+ virtual int write(const void* msg, size_t len, size_t* written) = 0;
+ virtual int read(void* buf, size_t maxlen, size_t* len) = 0;
+ virtual int listen(int backlog) = 0;
+ virtual int accept(nr_transport_addr* addrp, nr_socket** sockp) = 0;
+
+ // Implementations of the async_event APIs
+ virtual int async_wait(int how, NR_async_cb cb, void* cb_arg, char* function,
+ int line);
+ virtual int cancel(int how);
+
+ // nsISupport reference counted interface
+ NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
+
+ uint32_t poll_flags() { return poll_flags_; }
+
+ virtual nr_socket_vtbl* vtbl(); // To access in test classes.
+
+ static TimeStamp short_term_violation_time();
+ static TimeStamp long_term_violation_time();
+ const nr_transport_addr& my_addr() const { return my_addr_; }
+
+ void fire_callback(int how);
+
+ protected:
+ bool connect_invoked_;
+ nr_transport_addr my_addr_;
+
+ private:
+ NR_async_cb cbs_[NR_ASYNC_WAIT_WRITE + 1];
+ void* cb_args_[NR_ASYNC_WAIT_WRITE + 1];
+ uint32_t poll_flags_;
+};
+
+class NrSocket : public NrSocketBase, public nsASocketHandler {
+ public:
+ NrSocket() : fd_(nullptr) {}
+
+ // Implement nsASocket
+ virtual void OnSocketReady(PRFileDesc* fd, int16_t outflags) override;
+ virtual void OnSocketDetached(PRFileDesc* fd) override;
+ virtual void IsLocal(bool* aIsLocal) override;
+ virtual uint64_t ByteCountSent() override { return 0; }
+ virtual uint64_t ByteCountReceived() override { return 0; }
+
+ // nsISupports methods
+ NS_IMETHOD QueryInterface(REFNSIID aIID, void** aInstancePtr) override;
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING_WITH_DESTROY(NrSocket, Destroy(),
+ override);
+ virtual void Destroy() { delete this; }
+
+ // Implementations of the async_event APIs
+ virtual int async_wait(int how, NR_async_cb cb, void* cb_arg, char* function,
+ int line) override;
+ virtual int cancel(int how) override;
+
+ // Implementations of the nr_socket APIs
+ virtual int create(nr_transport_addr* addr)
+ override; // (really init, but it's called create)
+ virtual int sendto(const void* msg, size_t len, int flags,
+ const nr_transport_addr* to) override;
+ virtual int recvfrom(void* buf, size_t maxlen, size_t* len, int flags,
+ nr_transport_addr* from) override;
+ virtual int getaddr(nr_transport_addr* addrp) override;
+ virtual void close() override;
+ virtual int connect(const nr_transport_addr* addr) override;
+ virtual int write(const void* msg, size_t len, size_t* written) override;
+ virtual int read(void* buf, size_t maxlen, size_t* len) override;
+ virtual int listen(int backlog) override;
+ virtual int accept(nr_transport_addr* addrp, nr_socket** sockp) override;
+
+ protected:
+ virtual ~NrSocket() {
+ if (fd_) PR_Close(fd_);
+ }
+
+ DISALLOW_COPY_ASSIGN(NrSocket);
+
+ PRFileDesc* fd_;
+ nsCOMPtr<nsIEventTarget> ststhread_;
+};
+
+struct nr_udp_message {
+ nr_udp_message(const PRNetAddr& from, UniquePtr<MediaPacket>&& data)
+ : from(from), data(std::move(data)) {}
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(nr_udp_message);
+
+ PRNetAddr from;
+ UniquePtr<MediaPacket> data;
+
+ private:
+ ~nr_udp_message() = default;
+ DISALLOW_COPY_ASSIGN(nr_udp_message);
+};
+
+class NrSocketIpc : public NrSocketBase {
+ public:
+ enum NrSocketIpcState {
+ NR_INIT,
+ NR_CONNECTING,
+ NR_CONNECTED,
+ NR_CLOSING,
+ NR_CLOSED,
+ };
+
+ NrSocketIpc(nsIEventTarget* aThread);
+
+ protected:
+ nsCOMPtr<nsIEventTarget> sts_thread_;
+ // Note: for UDP PBackground, this is a thread held by SingletonThreadHolder.
+ // For TCP PNecko, this is MainThread (and TCPSocket requires MainThread
+ // currently)
+ const nsCOMPtr<nsIEventTarget> io_thread_;
+ virtual ~NrSocketIpc() = default;
+
+ private:
+ DISALLOW_COPY_ASSIGN(NrSocketIpc);
+};
+
+class NrUdpSocketIpc : public NrSocketIpc {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(NrUdpSocketIpc, override)
+
+ NS_IMETHODIMP CallListenerError(const nsACString& message,
+ const nsACString& filename,
+ uint32_t line_number);
+ NS_IMETHODIMP CallListenerReceivedData(const nsACString& host, uint16_t port,
+ const nsTArray<uint8_t>& data);
+ NS_IMETHODIMP CallListenerOpened();
+ NS_IMETHODIMP CallListenerConnected();
+ NS_IMETHODIMP CallListenerClosed();
+
+ NrUdpSocketIpc();
+
+ // Implementations of the NrSocketBase APIs
+ virtual int create(nr_transport_addr* addr) override;
+ virtual int sendto(const void* msg, size_t len, int flags,
+ const nr_transport_addr* to) override;
+ virtual int recvfrom(void* buf, size_t maxlen, size_t* len, int flags,
+ nr_transport_addr* from) override;
+ virtual int getaddr(nr_transport_addr* addrp) override;
+ virtual void close() override;
+ virtual int connect(const nr_transport_addr* addr) override;
+ virtual int write(const void* msg, size_t len, size_t* written) override;
+ virtual int read(void* buf, size_t maxlen, size_t* len) override;
+ virtual int listen(int backlog) override;
+ virtual int accept(nr_transport_addr* addrp, nr_socket** sockp) override;
+
+ private:
+ virtual ~NrUdpSocketIpc();
+ virtual void Destroy();
+
+ DISALLOW_COPY_ASSIGN(NrUdpSocketIpc);
+
+ nsresult SetAddress(); // Set the local address from parent info.
+
+ // Main or private thread executors of the NrSocketBase APIs
+ void create_i(const nsACString& host, const uint16_t port);
+ void connect_i(const nsACString& host, const uint16_t port);
+ void sendto_i(const net::NetAddr& addr, UniquePtr<MediaPacket> buf);
+ void close_i();
+#if defined(MOZILLA_INTERNAL_API) && !defined(MOZILLA_XPCOMRT_API)
+ void destroy_i();
+#endif
+ // STS thread executor
+ void recv_callback_s(RefPtr<nr_udp_message> msg);
+
+ ReentrantMonitor monitor_ MOZ_UNANNOTATED; // protects err_and state_
+ bool err_;
+ NrSocketIpcState state_;
+
+ std::queue<RefPtr<nr_udp_message>> received_msgs_;
+
+ // only accessed from the io_thread
+ RefPtr<dom::UDPSocketChild> socket_child_;
+};
+
+// The socket child holds onto one of these, which just passes callbacks
+// through and makes sure the ref to the NrSocketIpc is released on STS.
+class NrUdpSocketIpcProxy : public nsIUDPSocketInternal {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIUDPSOCKETINTERNAL
+
+ nsresult Init(const RefPtr<NrUdpSocketIpc>& socket);
+
+ private:
+ virtual ~NrUdpSocketIpcProxy();
+
+ RefPtr<NrUdpSocketIpc> socket_;
+ nsCOMPtr<nsIEventTarget> sts_thread_;
+};
+
+int nr_netaddr_to_transport_addr(const net::NetAddr* netaddr,
+ nr_transport_addr* addr, int protocol);
+int nr_praddr_to_transport_addr(const PRNetAddr* praddr,
+ nr_transport_addr* addr, int protocol,
+ int keep);
+int nr_transport_addr_get_addrstring_and_port(const nr_transport_addr* addr,
+ nsACString* host, int32_t* port);
+} // namespace mozilla
+#endif
diff --git a/dom/media/webrtc/transport/nr_socket_tcp.cpp b/dom/media/webrtc/transport/nr_socket_tcp.cpp
new file mode 100644
index 0000000000..a9cc5db312
--- /dev/null
+++ b/dom/media/webrtc/transport/nr_socket_tcp.cpp
@@ -0,0 +1,310 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+Copyright (c) 2013, Mozilla
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance, Mozilla nor
+ the names of its contributors may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include "nr_socket_tcp.h"
+
+#include "mozilla/ErrorNames.h"
+
+#include "WebrtcTCPSocketWrapper.h"
+
+namespace mozilla {
+using namespace net;
+
+using std::shared_ptr;
+
+class NrTcpSocketData {
+ public:
+ explicit NrTcpSocketData(nsTArray<uint8_t>&& aData)
+ : mData(std::move(aData)) {
+ MOZ_COUNT_CTOR(NrTcpSocketData);
+ }
+
+ MOZ_COUNTED_DTOR(NrTcpSocketData)
+
+ const nsTArray<uint8_t>& GetData() const { return mData; }
+
+ private:
+ nsTArray<uint8_t> mData;
+};
+
+NrTcpSocket::NrTcpSocket(const shared_ptr<NrSocketProxyConfig>& aConfig)
+ : mClosed(false),
+ mReadOffset(0),
+ mConfig(aConfig),
+ mWebrtcTCPSocket(nullptr) {
+ r_log(LOG_GENERIC, LOG_DEBUG, "NrTcpSocket::NrTcpSocket %p\n", this);
+}
+
+NrTcpSocket::~NrTcpSocket() {
+ r_log(LOG_GENERIC, LOG_DEBUG, "NrTcpSocket::~NrTcpSocket %p\n", this);
+ MOZ_ASSERT(!mWebrtcTCPSocket, "webrtc TCP socket not null");
+}
+
+int NrTcpSocket::create(nr_transport_addr* aAddr) {
+ r_log(LOG_GENERIC, LOG_DEBUG, "NrTcpSocket::create %p\n", this);
+ int32_t port;
+ nsCString host;
+
+ // Sanity check
+ if (nr_transport_addr_get_addrstring_and_port(aAddr, &host, &port)) {
+ return R_FAILED;
+ }
+
+ if (nr_transport_addr_copy(&my_addr_, aAddr)) {
+ return R_FAILED;
+ }
+
+ return 0;
+}
+
+int NrTcpSocket::connect(const nr_transport_addr* aAddr) {
+ r_log(LOG_GENERIC, LOG_DEBUG, "NrTcpSocket::connect %p\n", this);
+
+ nsCString remote_host;
+ int remote_port;
+
+ if (NS_WARN_IF(nr_transport_addr_get_addrstring_and_port(aAddr, &remote_host,
+ &remote_port))) {
+ return R_FAILED;
+ }
+
+ bool use_tls = aAddr->tls;
+
+ nsCString local_addr;
+ int local_port;
+
+ if (NS_WARN_IF(nr_transport_addr_get_addrstring_and_port(
+ &my_addr_, &local_addr, &local_port))) {
+ return R_FAILED;
+ }
+
+ mWebrtcTCPSocket = new WebrtcTCPSocketWrapper(this);
+
+ mWebrtcTCPSocket->AsyncOpen(remote_host, remote_port, local_addr, local_port,
+ use_tls, mConfig);
+
+ // trigger nr_socket_buffered to set write/read callback
+ return R_WOULDBLOCK;
+}
+
+void NrTcpSocket::close() {
+ r_log(LOG_GENERIC, LOG_DEBUG, "NrTcpSocket::close %p\n", this);
+
+ if (mClosed) {
+ return;
+ }
+
+ mClosed = true;
+
+ // We're not always open at this point.
+ if (mWebrtcTCPSocket) {
+ mWebrtcTCPSocket->Close();
+ mWebrtcTCPSocket = nullptr;
+ }
+}
+
+int NrTcpSocket::write(const void* aBuffer, size_t aCount, size_t* aWrote) {
+ r_log(LOG_GENERIC, LOG_DEBUG, "NrTcpSocket::write %p count=%zu\n", this,
+ aCount);
+
+ if (mClosed) {
+ return R_FAILED;
+ }
+
+ if (!aWrote) {
+ return R_FAILED;
+ }
+
+ if (NS_WARN_IF(!mWebrtcTCPSocket)) {
+ return R_FAILED;
+ }
+
+ *aWrote = aCount;
+
+ if (aCount > 0) {
+ nsTArray<uint8_t> writeData;
+ writeData.SetLength(aCount);
+ memcpy(writeData.Elements(), aBuffer, aCount);
+
+ mWebrtcTCPSocket->SendWrite(std::move(writeData));
+ }
+
+ return 0;
+}
+
+int NrTcpSocket::read(void* aBuffer, size_t aCount, size_t* aRead) {
+ r_log(LOG_GENERIC, LOG_DEBUG, "NrTcpSocket::read %p\n", this);
+
+ if (mClosed) {
+ return R_FAILED;
+ }
+
+ if (!aRead) {
+ return R_FAILED;
+ }
+
+ *aRead = 0;
+
+ if (mReadQueue.empty()) {
+ return R_WOULDBLOCK;
+ }
+
+ while (aCount > 0 && !mReadQueue.empty()) {
+ const NrTcpSocketData& data = mReadQueue.front();
+
+ size_t remainingCount = data.GetData().Length() - mReadOffset;
+ size_t amountToCopy = std::min(aCount, remainingCount);
+
+ char* buffer = static_cast<char*>(aBuffer) + (*aRead);
+
+ memcpy(buffer, data.GetData().Elements() + mReadOffset, amountToCopy);
+
+ mReadOffset += amountToCopy;
+ *aRead += amountToCopy;
+ aCount -= amountToCopy;
+
+ if (remainingCount == amountToCopy) {
+ mReadOffset = 0;
+ mReadQueue.pop_front();
+ }
+ }
+
+ return 0;
+}
+
+int NrTcpSocket::getaddr(nr_transport_addr* aAddr) {
+ r_log(LOG_GENERIC, LOG_DEBUG, "NrTcpSocket::getaddr %p\n", this);
+ return nr_transport_addr_copy(aAddr, &my_addr_);
+}
+
+int NrTcpSocket::sendto(const void* aBuffer, size_t aCount, int aFlags,
+ const nr_transport_addr* aAddr) {
+ // never call this
+ MOZ_ASSERT(0);
+ return R_FAILED;
+}
+
+int NrTcpSocket::recvfrom(void* aBuffer, size_t aCount, size_t* aRead,
+ int aFlags, nr_transport_addr* aAddr) {
+ // never call this
+ MOZ_ASSERT(0);
+ return R_FAILED;
+}
+
+int NrTcpSocket::listen(int aBacklog) {
+ r_log(LOG_GENERIC, LOG_DEBUG, "NrTcpSocket::listen %p\n", this);
+ return R_INTERNAL;
+}
+
+int NrTcpSocket::accept(nr_transport_addr* aAddr, nr_socket** aSocket) {
+ r_log(LOG_GENERIC, LOG_DEBUG, "NrTcpSocket::accept %p\n", this);
+ return R_INTERNAL;
+}
+
+// WebrtcTCPSocketCallback
+void NrTcpSocket::OnClose(nsresult aReason) {
+ nsCString errorName;
+ GetErrorName(aReason, errorName);
+
+ r_log(LOG_GENERIC, LOG_ERR, "NrTcpSocket::OnClose %p reason=%u name=%s\n",
+ this, static_cast<uint32_t>(aReason), errorName.get());
+
+ close();
+
+ DoCallbacks();
+}
+
+void NrTcpSocket::OnConnected(const nsACString& aProxyType) {
+ r_log(LOG_GENERIC, LOG_DEBUG, "NrTcpSocket::OnConnected %p\n", this);
+ if (aProxyType != "" && aProxyType != "direct") {
+ my_addr_.is_proxied = true;
+ }
+
+ DoCallbacks();
+}
+
+void NrTcpSocket::OnRead(nsTArray<uint8_t>&& aReadData) {
+ r_log(LOG_GENERIC, LOG_DEBUG, "NrTcpSocket::OnRead %p read=%zu\n", this,
+ aReadData.Length());
+
+ mReadQueue.emplace_back(std::move(aReadData));
+
+ DoCallbacks();
+}
+
+void NrTcpSocket::DoCallbacks() {
+ size_t lastCount = -1;
+ size_t currentCount = 0;
+ while ((poll_flags() & PR_POLL_READ) != 0 &&
+ // Make sure whatever is reading knows we're closed. This doesn't need
+ // to happen for writes since ICE doesn't like a failing writable.
+ (mClosed || (currentCount = CountUnreadBytes()) > 0) &&
+ lastCount != currentCount) {
+ fire_callback(NR_ASYNC_WAIT_READ);
+ lastCount = currentCount;
+ }
+
+ // We're always ready to write after we're connected. The parent process will
+ // buffer writes for us.
+ if (!mClosed && mWebrtcTCPSocket && (poll_flags() & PR_POLL_WRITE) != 0) {
+ fire_callback(NR_ASYNC_WAIT_WRITE);
+ }
+}
+
+size_t NrTcpSocket::CountUnreadBytes() const {
+ size_t count = 0;
+
+ for (const NrTcpSocketData& data : mReadQueue) {
+ count += data.GetData().Length();
+ }
+
+ MOZ_ASSERT(count >= mReadOffset, "offset exceeds read buffer length");
+
+ count -= mReadOffset;
+
+ return count;
+}
+
+void NrTcpSocket::AssignChannel_DoNotUse(WebrtcTCPSocketWrapper* aWrapper) {
+ mWebrtcTCPSocket = aWrapper;
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/transport/nr_socket_tcp.h b/dom/media/webrtc/transport/nr_socket_tcp.h
new file mode 100644
index 0000000000..ebf806a924
--- /dev/null
+++ b/dom/media/webrtc/transport/nr_socket_tcp.h
@@ -0,0 +1,117 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set sw=2 ts=8 et tw=80 ft=cpp : */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+Copyright (c) 2013, Mozilla
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance, Mozilla nor
+ the names of its contributors may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#ifndef nr_socket_tcp_h__
+#define nr_socket_tcp_h__
+
+#include <list>
+
+#include "mozilla/net/WebrtcTCPSocketCallback.h"
+
+#include "nsTArray.h"
+
+extern "C" {
+#include "nr_api.h"
+#include "nr_socket.h"
+#include "transport_addr.h"
+}
+
+#include "nr_socket_prsock.h"
+
+namespace mozilla {
+using namespace net;
+
+namespace net {
+class WebrtcTCPSocketWrapper;
+} // namespace net
+
+class NrTcpSocketData;
+class NrSocketProxyConfig;
+
+class NrTcpSocket : public NrSocketBase, public WebrtcTCPSocketCallback {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(NrTcpSocket, override)
+
+ explicit NrTcpSocket(const std::shared_ptr<NrSocketProxyConfig>& aConfig);
+
+ // NrSocketBase
+ int create(nr_transport_addr* aAddr) override;
+ int connect(const nr_transport_addr* aAddr) override;
+ void close() override;
+ int write(const void* aBuffer, size_t aCount, size_t* aWrote) override;
+ int read(void* aBuffer, size_t aCount, size_t* aRead) override;
+ int getaddr(nr_transport_addr* aAddr) override;
+ int sendto(const void* aBuffer, size_t aCount, int aFlags,
+ const nr_transport_addr* aAddr) override;
+ int recvfrom(void* aBuffer, size_t aCount, size_t* aRead, int aFlags,
+ nr_transport_addr* aAddr) override;
+ int listen(int aBacklog) override;
+ int accept(nr_transport_addr* aAddr, nr_socket** aSocket) override;
+
+ // WebrtcTCPSocketCallback
+ void OnClose(nsresult aReason) override;
+ void OnConnected(const nsACString& aProxyType) override;
+ void OnRead(nsTArray<uint8_t>&& aReadData) override;
+
+ size_t CountUnreadBytes() const;
+
+ // for gtests
+ void AssignChannel_DoNotUse(WebrtcTCPSocketWrapper* aWrapper);
+
+ protected:
+ virtual ~NrTcpSocket();
+
+ private:
+ void DoCallbacks();
+
+ bool mClosed;
+
+ size_t mReadOffset;
+ std::list<NrTcpSocketData> mReadQueue;
+
+ std::shared_ptr<NrSocketProxyConfig> mConfig;
+
+ RefPtr<WebrtcTCPSocketWrapper> mWebrtcTCPSocket;
+};
+
+} // namespace mozilla
+
+#endif // nr_socket_tcp_h__
diff --git a/dom/media/webrtc/transport/nr_timer.cpp b/dom/media/webrtc/transport/nr_timer.cpp
new file mode 100644
index 0000000000..3d5671c7a4
--- /dev/null
+++ b/dom/media/webrtc/transport/nr_timer.cpp
@@ -0,0 +1,256 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original code by: ekr@rtfm.com
+
+// Implementation of the NR timer interface
+
+// Some code here copied from nrappkit. The license was.
+
+/**
+ Copyright (C) 2004, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ ekr@rtfm.com Sun Feb 22 19:35:24 2004
+ */
+
+#include <string>
+
+#include "nsCOMPtr.h"
+#include "nsServiceManagerUtils.h"
+#include "nsIEventTarget.h"
+#include "nsINamed.h"
+#include "nsITimer.h"
+#include "nsNetCID.h"
+#include "runnable_utils.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/UniquePtr.h"
+
+extern "C" {
+#include "async_wait.h"
+#include "async_timer.h"
+#include "r_errors.h"
+#include "r_log.h"
+}
+
+namespace mozilla {
+
+class nrappkitCallback {
+ public:
+ nrappkitCallback(NR_async_cb cb, void* cb_arg, const char* function, int line)
+ : cb_(cb), cb_arg_(cb_arg), function_(function), line_(line) {}
+ virtual ~nrappkitCallback() = default;
+
+ virtual void Cancel() = 0;
+
+ protected:
+ /* additional members */
+ NR_async_cb cb_;
+ void* cb_arg_;
+ std::string function_;
+ int line_;
+};
+
+class nrappkitTimerCallback : public nrappkitCallback,
+ public nsITimerCallback,
+ public nsINamed {
+ public:
+ // We're going to release ourself in the callback, so we need to be threadsafe
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSITIMERCALLBACK
+
+ nrappkitTimerCallback(NR_async_cb cb, void* cb_arg, const char* function,
+ int line)
+ : nrappkitCallback(cb, cb_arg, function, line), timer_(nullptr) {}
+
+ void SetTimer(already_AddRefed<nsITimer>&& timer) { timer_ = timer; }
+
+ virtual void Cancel() override {
+ AddRef(); // Cancelling the timer causes the callback it holds to
+ // be released. AddRef() keeps us alive.
+ timer_->Cancel();
+ timer_ = nullptr;
+ Release(); // Will cause deletion of this object.
+ }
+
+ NS_IMETHOD
+ GetName(nsACString& aName) override {
+ aName.AssignLiteral("nrappkitTimerCallback");
+ return NS_OK;
+ }
+
+ private:
+ nsCOMPtr<nsITimer> timer_;
+ virtual ~nrappkitTimerCallback() = default;
+};
+
+NS_IMPL_ISUPPORTS(nrappkitTimerCallback, nsITimerCallback, nsINamed)
+
+NS_IMETHODIMP nrappkitTimerCallback::Notify(nsITimer* timer) {
+ r_log(LOG_GENERIC, LOG_DEBUG, "Timer callback fired (set in %s:%d)",
+ function_.c_str(), line_);
+ MOZ_RELEASE_ASSERT(timer == timer_);
+ cb_(nullptr, 0, cb_arg_);
+
+ // Allow the timer to go away.
+ timer_ = nullptr;
+ return NS_OK;
+}
+
+class nrappkitScheduledCallback : public nrappkitCallback {
+ public:
+ nrappkitScheduledCallback(NR_async_cb cb, void* cb_arg, const char* function,
+ int line)
+ : nrappkitCallback(cb, cb_arg, function, line) {}
+
+ void Run() {
+ if (cb_) {
+ cb_(nullptr, 0, cb_arg_);
+ }
+ }
+
+ virtual void Cancel() override { cb_ = nullptr; }
+
+ ~nrappkitScheduledCallback() = default;
+};
+
+} // namespace mozilla
+
+using namespace mozilla;
+
+static nsCOMPtr<nsIEventTarget> GetSTSThread() {
+ nsresult rv;
+
+ nsCOMPtr<nsIEventTarget> sts_thread;
+
+ sts_thread = do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+
+ return sts_thread;
+}
+
+// These timers must only be used from the STS thread.
+// This function is a helper that enforces that.
+static void CheckSTSThread() {
+ DebugOnly<nsCOMPtr<nsIEventTarget>> sts_thread = GetSTSThread();
+
+ ASSERT_ON_THREAD(sts_thread.value);
+}
+
+static int nr_async_timer_set_zero(NR_async_cb cb, void* arg, char* func, int l,
+ nrappkitCallback** handle) {
+ nrappkitScheduledCallback* callback(
+ new nrappkitScheduledCallback(cb, arg, func, l));
+
+ nsresult rv = GetSTSThread()->Dispatch(
+ WrapRunnable(UniquePtr<nrappkitScheduledCallback>(callback),
+ &nrappkitScheduledCallback::Run),
+ NS_DISPATCH_NORMAL);
+ if (NS_FAILED(rv)) return R_FAILED;
+
+ *handle = callback;
+
+ // On exit to this function, the only strong reference to callback is in
+ // the Runnable. Because we are redispatching to the same thread,
+ // this is always safe.
+ return 0;
+}
+
+static int nr_async_timer_set_nonzero(int timeout, NR_async_cb cb, void* arg,
+ char* func, int l,
+ nrappkitCallback** handle) {
+ nsresult rv;
+ CheckSTSThread();
+
+ nrappkitTimerCallback* callback = new nrappkitTimerCallback(cb, arg, func, l);
+
+ nsCOMPtr<nsITimer> timer;
+ rv = NS_NewTimerWithCallback(getter_AddRefs(timer), callback, timeout,
+ nsITimer::TYPE_ONE_SHOT);
+ if (NS_FAILED(rv)) {
+ return R_FAILED;
+ }
+
+ // Move the ownership of the timer to the callback object, which holds the
+ // timer alive per spec.
+ callback->SetTimer(timer.forget());
+
+ *handle = callback;
+
+ return 0;
+}
+
+int NR_async_timer_set(int timeout, NR_async_cb cb, void* arg, char* func,
+ int l, void** handle) {
+ CheckSTSThread();
+
+ nrappkitCallback* callback;
+ int r;
+
+ if (!timeout) {
+ r = nr_async_timer_set_zero(cb, arg, func, l, &callback);
+ } else {
+ r = nr_async_timer_set_nonzero(timeout, cb, arg, func, l, &callback);
+ }
+
+ if (r) return r;
+
+ if (handle) *handle = callback;
+
+ return 0;
+}
+
+int NR_async_schedule(NR_async_cb cb, void* arg, char* func, int l) {
+ // No need to check the thread because we check it next in the
+ // timer set.
+ return NR_async_timer_set(0, cb, arg, func, l, nullptr);
+}
+
+int NR_async_timer_cancel(void* handle) {
+ // Check for the handle being nonzero because sometimes we get
+ // no-op cancels that aren't on the STS thread. This can be
+ // non-racy as long as the upper-level code is careful.
+ if (!handle) return 0;
+
+ CheckSTSThread();
+
+ nrappkitCallback* callback = static_cast<nrappkitCallback*>(handle);
+ callback->Cancel();
+
+ return 0;
+}
diff --git a/dom/media/webrtc/transport/nricectx.cpp b/dom/media/webrtc/transport/nricectx.cpp
new file mode 100644
index 0000000000..697eecdb07
--- /dev/null
+++ b/dom/media/webrtc/transport/nricectx.cpp
@@ -0,0 +1,1106 @@
+/* -*- mode: c++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+// Some of this code is cut-and-pasted from nICEr. Copyright is:
+
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <string>
+#include <vector>
+
+#include "nr_socket_proxy_config.h"
+#include "nsXULAppAPI.h"
+
+#include "logging.h"
+#include "pk11pub.h"
+#include "plbase64.h"
+
+#include "nsCOMPtr.h"
+#include "nsError.h"
+#include "nsNetCID.h"
+#include "nsServiceManagerUtils.h"
+#include "ScopedNSSTypes.h"
+#include "runnable_utils.h"
+#include "nsIUUIDGenerator.h"
+
+// nICEr includes
+extern "C" {
+#include "nr_api.h"
+#include "registry.h"
+#include "async_timer.h"
+#include "r_crc32.h"
+#include "r_memory.h"
+#include "ice_reg.h"
+#include "transport_addr.h"
+#include "nr_crypto.h"
+#include "nr_socket.h"
+#include "nr_socket_local.h"
+#include "stun_reg.h"
+#include "stun_util.h"
+#include "ice_codeword.h"
+#include "ice_ctx.h"
+#include "ice_candidate.h"
+}
+
+// Local includes
+#include "nricectx.h"
+#include "nricemediastream.h"
+#include "nr_socket_prsock.h"
+#include "nrinterfaceprioritizer.h"
+#include "rlogconnector.h"
+#include "test_nr_socket.h"
+
+namespace mozilla {
+
+using std::shared_ptr;
+
+TimeStamp nr_socket_short_term_violation_time() {
+ return NrSocketBase::short_term_violation_time();
+}
+
+TimeStamp nr_socket_long_term_violation_time() {
+ return NrSocketBase::long_term_violation_time();
+}
+
+MOZ_MTLOG_MODULE("mtransport")
+
+const char kNrIceTransportUdp[] = "udp";
+const char kNrIceTransportTcp[] = "tcp";
+const char kNrIceTransportTls[] = "tls";
+
+static bool initialized = false;
+
+static int noop(void** obj) { return 0; }
+
+static nr_socket_factory_vtbl ctx_socket_factory_vtbl = {nr_socket_local_create,
+ noop};
+
+// Implement NSPR-based crypto algorithms
+static int nr_crypto_nss_random_bytes(UCHAR* buf, size_t len) {
+ UniquePK11SlotInfo slot(PK11_GetInternalSlot());
+ if (!slot) return R_INTERNAL;
+
+ SECStatus rv = PK11_GenerateRandomOnSlot(slot.get(), buf, len);
+ if (rv != SECSuccess) return R_INTERNAL;
+
+ return 0;
+}
+
+static int nr_crypto_nss_hmac(UCHAR* key, size_t keyl, UCHAR* buf, size_t bufl,
+ UCHAR* result) {
+ CK_MECHANISM_TYPE mech = CKM_SHA_1_HMAC;
+ PK11SlotInfo* slot = nullptr;
+ MOZ_ASSERT(keyl > 0);
+ SECItem keyi = {siBuffer, key, static_cast<unsigned int>(keyl)};
+ PK11SymKey* skey = nullptr;
+ PK11Context* hmac_ctx = nullptr;
+ SECStatus status;
+ unsigned int hmac_len;
+ SECItem param = {siBuffer, nullptr, 0};
+ int err = R_INTERNAL;
+
+ slot = PK11_GetInternalKeySlot();
+ if (!slot) goto abort;
+
+ skey = PK11_ImportSymKey(slot, mech, PK11_OriginUnwrap, CKA_SIGN, &keyi,
+ nullptr);
+ if (!skey) goto abort;
+
+ hmac_ctx = PK11_CreateContextBySymKey(mech, CKA_SIGN, skey, &param);
+ if (!hmac_ctx) goto abort;
+
+ status = PK11_DigestBegin(hmac_ctx);
+ if (status != SECSuccess) goto abort;
+
+ status = PK11_DigestOp(hmac_ctx, buf, bufl);
+ if (status != SECSuccess) goto abort;
+
+ status = PK11_DigestFinal(hmac_ctx, result, &hmac_len, 20);
+ if (status != SECSuccess) goto abort;
+
+ MOZ_ASSERT(hmac_len == 20);
+
+ err = 0;
+
+abort:
+ if (hmac_ctx) PK11_DestroyContext(hmac_ctx, PR_TRUE);
+ if (skey) PK11_FreeSymKey(skey);
+ if (slot) PK11_FreeSlot(slot);
+
+ return err;
+}
+
+static int nr_crypto_nss_md5(UCHAR* buf, size_t bufl, UCHAR* result) {
+ int err = R_INTERNAL;
+ SECStatus rv;
+
+ const SECHashObject* ho = HASH_GetHashObject(HASH_AlgMD5);
+ MOZ_ASSERT(ho);
+ if (!ho) goto abort;
+
+ MOZ_ASSERT(ho->length == 16);
+
+ rv = HASH_HashBuf(ho->type, result, buf, bufl);
+ if (rv != SECSuccess) goto abort;
+
+ err = 0;
+abort:
+ return err;
+}
+
+static nr_ice_crypto_vtbl nr_ice_crypto_nss_vtbl = {
+ nr_crypto_nss_random_bytes, nr_crypto_nss_hmac, nr_crypto_nss_md5};
+
+nsresult NrIceStunServer::ToNicerStunStruct(nr_ice_stun_server* server) const {
+ int r;
+
+ memset(server, 0, sizeof(nr_ice_stun_server));
+ uint8_t protocol;
+ if (transport_ == kNrIceTransportUdp) {
+ protocol = IPPROTO_UDP;
+ } else if (transport_ == kNrIceTransportTcp) {
+ protocol = IPPROTO_TCP;
+ } else if (transport_ == kNrIceTransportTls) {
+ protocol = IPPROTO_TCP;
+ } else {
+ MOZ_MTLOG(ML_ERROR, "Unsupported STUN server transport: " << transport_);
+ return NS_ERROR_FAILURE;
+ }
+
+ if (has_addr_) {
+ if (transport_ == kNrIceTransportTls) {
+ // Refuse to try TLS without an FQDN
+ return NS_ERROR_INVALID_ARG;
+ }
+ r = nr_praddr_to_transport_addr(&addr_, &server->addr, protocol, 0);
+ if (r) {
+ return NS_ERROR_FAILURE;
+ }
+ } else {
+ MOZ_ASSERT(sizeof(server->addr.fqdn) > host_.size());
+ // Dummy information to keep nICEr happy
+ if (use_ipv6_if_fqdn_) {
+ nr_str_port_to_transport_addr("::", port_, protocol, &server->addr);
+ } else {
+ nr_str_port_to_transport_addr("0.0.0.0", port_, protocol, &server->addr);
+ }
+ PL_strncpyz(server->addr.fqdn, host_.c_str(), sizeof(server->addr.fqdn));
+ if (transport_ == kNrIceTransportTls) {
+ server->addr.tls = 1;
+ }
+ }
+
+ nr_transport_addr_fmt_addr_string(&server->addr);
+
+ return NS_OK;
+}
+
+nsresult NrIceTurnServer::ToNicerTurnStruct(nr_ice_turn_server* server) const {
+ memset(server, 0, sizeof(nr_ice_turn_server));
+
+ nsresult rv = ToNicerStunStruct(&server->turn_server);
+ if (NS_FAILED(rv)) return rv;
+
+ if (!(server->username = r_strdup(username_.c_str())))
+ return NS_ERROR_OUT_OF_MEMORY;
+
+ // TODO(ekr@rtfm.com): handle non-ASCII passwords somehow?
+ // STUN requires they be SASLpreped, but we don't know if
+ // they are at this point.
+
+ // C++03 23.2.4, Paragraph 1 stipulates that the elements
+ // in std::vector must be contiguous, and can therefore be
+ // used as input to functions expecting C arrays.
+ const UCHAR* data = password_.empty() ? nullptr : &password_[0];
+ int r = r_data_create(&server->password, data, password_.size());
+ if (r) {
+ RFREE(server->username);
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ return NS_OK;
+}
+
+NrIceCtx::NrIceCtx(const std::string& name)
+ : connection_state_(ICE_CTX_INIT),
+ gathering_state_(ICE_CTX_GATHER_INIT),
+ name_(name),
+ ice_controlling_set_(false),
+ streams_(),
+ ctx_(nullptr),
+ peer_(nullptr),
+ ice_handler_vtbl_(nullptr),
+ ice_handler_(nullptr),
+ trickle_(true),
+ config_(),
+ nat_(nullptr),
+ proxy_config_(nullptr) {}
+
+/* static */
+RefPtr<NrIceCtx> NrIceCtx::Create(const std::string& aName) {
+ RefPtr<NrIceCtx> ctx = new NrIceCtx(aName);
+
+ if (!ctx->Initialize()) {
+ return nullptr;
+ }
+
+ return ctx;
+}
+
+nsresult NrIceCtx::SetIceConfig(const Config& aConfig) {
+ config_ = aConfig;
+ switch (config_.mPolicy) {
+ case ICE_POLICY_RELAY:
+ MOZ_MTLOG(ML_DEBUG, "SetIceConfig: relay only");
+ nr_ice_ctx_remove_flags(ctx_, NR_ICE_CTX_FLAGS_DISABLE_HOST_CANDIDATES);
+ nr_ice_ctx_add_flags(ctx_, NR_ICE_CTX_FLAGS_RELAY_ONLY);
+ break;
+ case ICE_POLICY_NO_HOST:
+ MOZ_MTLOG(ML_DEBUG, "SetIceConfig: no host");
+ nr_ice_ctx_add_flags(ctx_, NR_ICE_CTX_FLAGS_DISABLE_HOST_CANDIDATES);
+ nr_ice_ctx_remove_flags(ctx_, NR_ICE_CTX_FLAGS_RELAY_ONLY);
+ break;
+ case ICE_POLICY_ALL:
+ MOZ_MTLOG(ML_DEBUG, "SetIceConfig: all");
+ nr_ice_ctx_remove_flags(ctx_, NR_ICE_CTX_FLAGS_DISABLE_HOST_CANDIDATES);
+ nr_ice_ctx_remove_flags(ctx_, NR_ICE_CTX_FLAGS_RELAY_ONLY);
+ break;
+ }
+
+ // TODO: Support re-configuring the test NAT someday?
+ if (!nat_ && config_.mNatSimulatorConfig.isSome()) {
+ TestNat* test_nat = new TestNat;
+ test_nat->filtering_type_ = TestNat::ToNatBehavior(
+ config_.mNatSimulatorConfig->mFilteringType.get());
+ test_nat->mapping_type_ =
+ TestNat::ToNatBehavior(config_.mNatSimulatorConfig->mMappingType.get());
+ test_nat->block_udp_ = config_.mNatSimulatorConfig->mBlockUdp;
+ test_nat->block_tcp_ = config_.mNatSimulatorConfig->mBlockTcp;
+ test_nat->block_tls_ = config_.mNatSimulatorConfig->mBlockTls;
+ test_nat->error_code_for_drop_ =
+ config_.mNatSimulatorConfig->mErrorCodeForDrop;
+ if (config_.mNatSimulatorConfig->mRedirectAddress.Length()) {
+ test_nat
+ ->stun_redirect_map_[config_.mNatSimulatorConfig->mRedirectAddress] =
+ config_.mNatSimulatorConfig->mRedirectTargets;
+ }
+ test_nat->enabled_ = true;
+ SetNat(test_nat);
+ }
+
+ return NS_OK;
+}
+
+RefPtr<NrIceMediaStream> NrIceCtx::CreateStream(const std::string& id,
+ const std::string& name,
+ int components) {
+ if (streams_.count(id)) {
+ MOZ_ASSERT(false);
+ return nullptr;
+ }
+
+ RefPtr<NrIceMediaStream> stream =
+ new NrIceMediaStream(this, id, name, components);
+ streams_[id] = stream;
+ return stream;
+}
+
+void NrIceCtx::DestroyStream(const std::string& id) {
+ auto it = streams_.find(id);
+ if (it != streams_.end()) {
+ auto preexisting_stream = it->second;
+ streams_.erase(it);
+ preexisting_stream->Close();
+ }
+
+ if (streams_.empty()) {
+ SetGatheringState(ICE_CTX_GATHER_INIT);
+ }
+}
+
+// Handler callbacks
+int NrIceCtx::select_pair(void* obj, nr_ice_media_stream* stream,
+ int component_id, nr_ice_cand_pair** potentials,
+ int potential_ct) {
+ MOZ_MTLOG(ML_DEBUG, "select pair called: potential_ct = " << potential_ct);
+ MOZ_ASSERT(stream->local_stream);
+ MOZ_ASSERT(!stream->local_stream->obsolete);
+
+ return 0;
+}
+
+int NrIceCtx::stream_ready(void* obj, nr_ice_media_stream* stream) {
+ MOZ_MTLOG(ML_DEBUG, "stream_ready called");
+ MOZ_ASSERT(!stream->local_stream);
+ MOZ_ASSERT(!stream->obsolete);
+
+ // Get the ICE ctx.
+ NrIceCtx* ctx = static_cast<NrIceCtx*>(obj);
+
+ RefPtr<NrIceMediaStream> s = ctx->FindStream(stream);
+
+ // Streams which do not exist should never be ready.
+ MOZ_ASSERT(s);
+
+ s->Ready();
+
+ return 0;
+}
+
+int NrIceCtx::stream_failed(void* obj, nr_ice_media_stream* stream) {
+ MOZ_MTLOG(ML_DEBUG, "stream_failed called");
+ MOZ_ASSERT(!stream->local_stream);
+ MOZ_ASSERT(!stream->obsolete);
+
+ // Get the ICE ctx
+ NrIceCtx* ctx = static_cast<NrIceCtx*>(obj);
+ RefPtr<NrIceMediaStream> s = ctx->FindStream(stream);
+
+ // Streams which do not exist should never fail.
+ MOZ_ASSERT(s);
+
+ ctx->SetConnectionState(ICE_CTX_FAILED);
+ s->Failed();
+ return 0;
+}
+
+int NrIceCtx::ice_checking(void* obj, nr_ice_peer_ctx* pctx) {
+ MOZ_MTLOG(ML_DEBUG, "ice_checking called");
+
+ // Get the ICE ctx
+ NrIceCtx* ctx = static_cast<NrIceCtx*>(obj);
+
+ ctx->SetConnectionState(ICE_CTX_CHECKING);
+
+ return 0;
+}
+
+int NrIceCtx::ice_connected(void* obj, nr_ice_peer_ctx* pctx) {
+ MOZ_MTLOG(ML_DEBUG, "ice_connected called");
+
+ // Get the ICE ctx
+ NrIceCtx* ctx = static_cast<NrIceCtx*>(obj);
+
+ // This is called even on failed contexts.
+ if (ctx->connection_state() != ICE_CTX_FAILED) {
+ ctx->SetConnectionState(ICE_CTX_CONNECTED);
+ }
+
+ return 0;
+}
+
+int NrIceCtx::ice_disconnected(void* obj, nr_ice_peer_ctx* pctx) {
+ MOZ_MTLOG(ML_DEBUG, "ice_disconnected called");
+
+ // Get the ICE ctx
+ NrIceCtx* ctx = static_cast<NrIceCtx*>(obj);
+
+ ctx->SetConnectionState(ICE_CTX_DISCONNECTED);
+
+ return 0;
+}
+
+int NrIceCtx::msg_recvd(void* obj, nr_ice_peer_ctx* pctx,
+ nr_ice_media_stream* stream, int component_id,
+ UCHAR* msg, int len) {
+ // Get the ICE ctx
+ NrIceCtx* ctx = static_cast<NrIceCtx*>(obj);
+ RefPtr<NrIceMediaStream> s = ctx->FindStream(stream);
+
+ // Streams which do not exist should never have packets.
+ MOZ_ASSERT(s);
+
+ s->SignalPacketReceived(s, component_id, msg, len);
+
+ return 0;
+}
+
+void NrIceCtx::trickle_cb(void* arg, nr_ice_ctx* ice_ctx,
+ nr_ice_media_stream* stream, int component_id,
+ nr_ice_candidate* candidate) {
+ if (stream->obsolete) {
+ // Stream was probably just marked obsolete, resulting in this callback
+ return;
+ }
+ // Get the ICE ctx
+ NrIceCtx* ctx = static_cast<NrIceCtx*>(arg);
+ RefPtr<NrIceMediaStream> s = ctx->FindStream(stream);
+
+ if (!s) {
+ // This stream has been removed because it is inactive
+ return;
+ }
+
+ if (!candidate) {
+ s->SignalCandidate(s, "", stream->ufrag, "", "");
+ return;
+ }
+
+ std::string actual_addr;
+ std::string mdns_addr;
+ ctx->GenerateObfuscatedAddress(candidate, &mdns_addr, &actual_addr);
+
+ // Format the candidate.
+ char candidate_str[NR_ICE_MAX_ATTRIBUTE_SIZE];
+ int r = nr_ice_format_candidate_attribute(
+ candidate, candidate_str, sizeof(candidate_str),
+ (ctx->ctx()->flags & NR_ICE_CTX_FLAGS_OBFUSCATE_HOST_ADDRESSES) ? 1 : 0);
+ MOZ_ASSERT(!r);
+ if (r) return;
+
+ MOZ_MTLOG(ML_INFO, "NrIceCtx(" << ctx->name_ << "): trickling candidate "
+ << candidate_str);
+
+ s->SignalCandidate(s, candidate_str, stream->ufrag, mdns_addr, actual_addr);
+}
+
+void NrIceCtx::InitializeGlobals(const GlobalConfig& aConfig) {
+ RLogConnector::CreateInstance();
+ // Initialize the crypto callbacks and logging stuff
+ if (!initialized) {
+ NR_reg_init(NR_REG_MODE_LOCAL);
+ nr_crypto_vtbl = &nr_ice_crypto_nss_vtbl;
+ initialized = true;
+
+ // Set the priorites for candidate type preferences.
+ // These numbers come from RFC 5245 S. 4.1.2.2
+ NR_reg_set_uchar((char*)NR_ICE_REG_PREF_TYPE_SRV_RFLX, 100);
+ NR_reg_set_uchar((char*)NR_ICE_REG_PREF_TYPE_PEER_RFLX, 110);
+ NR_reg_set_uchar((char*)NR_ICE_REG_PREF_TYPE_HOST, 126);
+ NR_reg_set_uchar((char*)NR_ICE_REG_PREF_TYPE_RELAYED, 5);
+ NR_reg_set_uchar((char*)NR_ICE_REG_PREF_TYPE_SRV_RFLX_TCP, 99);
+ NR_reg_set_uchar((char*)NR_ICE_REG_PREF_TYPE_PEER_RFLX_TCP, 109);
+ NR_reg_set_uchar((char*)NR_ICE_REG_PREF_TYPE_HOST_TCP, 125);
+ NR_reg_set_uchar((char*)NR_ICE_REG_PREF_TYPE_RELAYED_TCP, 0);
+ NR_reg_set_uint4((char*)"stun.client.maximum_transmits",
+ aConfig.mStunClientMaxTransmits);
+ NR_reg_set_uint4((char*)NR_ICE_REG_TRICKLE_GRACE_PERIOD,
+ aConfig.mTrickleIceGracePeriod);
+ NR_reg_set_int4((char*)NR_ICE_REG_ICE_TCP_SO_SOCK_COUNT,
+ aConfig.mIceTcpSoSockCount);
+ NR_reg_set_int4((char*)NR_ICE_REG_ICE_TCP_LISTEN_BACKLOG,
+ aConfig.mIceTcpListenBacklog);
+
+ NR_reg_set_char((char*)NR_ICE_REG_ICE_TCP_DISABLE, !aConfig.mTcpEnabled);
+
+ if (aConfig.mAllowLoopback) {
+ NR_reg_set_char((char*)NR_STUN_REG_PREF_ALLOW_LOOPBACK_ADDRS, 1);
+ }
+
+ if (aConfig.mAllowLinkLocal) {
+ NR_reg_set_char((char*)NR_STUN_REG_PREF_ALLOW_LINK_LOCAL_ADDRS, 1);
+ }
+ if (!aConfig.mForceNetInterface.Length()) {
+ NR_reg_set_string((char*)NR_ICE_REG_PREF_FORCE_INTERFACE_NAME,
+ const_cast<char*>(aConfig.mForceNetInterface.get()));
+ }
+
+ // For now, always use nr_resolver for UDP.
+ NR_reg_set_char((char*)NR_ICE_REG_USE_NR_RESOLVER_FOR_UDP, 1);
+
+ // Use nr_resolver for TCP only when not in e10s mode (for unit-tests)
+ if (XRE_IsParentProcess()) {
+ NR_reg_set_char((char*)NR_ICE_REG_USE_NR_RESOLVER_FOR_TCP, 1);
+ }
+ }
+}
+
+void NrIceCtx::SetTargetForDefaultLocalAddressLookup(
+ const std::string& target_ip, uint16_t target_port) {
+ nr_ice_set_target_for_default_local_address_lookup(ctx_, target_ip.c_str(),
+ target_port);
+}
+
+#define MAXADDRS 100 // mirrors setting in ice_ctx.c
+
+/* static */
+nsTArray<NrIceStunAddr> NrIceCtx::GetStunAddrs() {
+ nsTArray<NrIceStunAddr> addrs;
+
+ nr_local_addr local_addrs[MAXADDRS];
+ int addr_ct = 0;
+
+ // most likely running on parent process and need crypto vtbl
+ // initialized on Windows (Linux and OSX don't seem to care)
+ if (!initialized) {
+ nr_crypto_vtbl = &nr_ice_crypto_nss_vtbl;
+ }
+
+ MOZ_MTLOG(ML_INFO, "NrIceCtx static call to find local stun addresses");
+ if (nr_stun_find_local_addresses(local_addrs, MAXADDRS, &addr_ct)) {
+ MOZ_MTLOG(ML_INFO, "Error finding local stun addresses");
+ } else {
+ for (int i = 0; i < addr_ct; ++i) {
+ NrIceStunAddr addr(&local_addrs[i]);
+ addrs.AppendElement(addr);
+ }
+ }
+
+ return addrs;
+}
+
+void NrIceCtx::SetStunAddrs(const nsTArray<NrIceStunAddr>& addrs) {
+ nr_local_addr* local_addrs;
+ local_addrs = new nr_local_addr[addrs.Length()];
+
+ for (size_t i = 0; i < addrs.Length(); ++i) {
+ nr_local_addr_copy(&local_addrs[i],
+ const_cast<nr_local_addr*>(&addrs[i].localAddr()));
+ }
+ nr_ice_set_local_addresses(ctx_, local_addrs, addrs.Length());
+
+ delete[] local_addrs;
+}
+
+bool NrIceCtx::Initialize() {
+ // Create the ICE context
+ int r;
+
+ UINT4 flags = NR_ICE_CTX_FLAGS_AGGRESSIVE_NOMINATION;
+ r = nr_ice_ctx_create(const_cast<char*>(name_.c_str()), flags, &ctx_);
+
+ if (r) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't create ICE ctx for '" << name_ << "'");
+ return false;
+ }
+
+ // override default factory to capture optional proxy config when creating
+ // sockets.
+ nr_socket_factory* factory;
+ r = nr_socket_factory_create_int(this, &ctx_socket_factory_vtbl, &factory);
+
+ if (r) {
+ MOZ_MTLOG(LogLevel::Error, "Couldn't create ctx socket factory.");
+ return false;
+ }
+ nr_ice_ctx_set_socket_factory(ctx_, factory);
+
+ nr_interface_prioritizer* prioritizer = CreateInterfacePrioritizer();
+ if (!prioritizer) {
+ MOZ_MTLOG(LogLevel::Error, "Couldn't create interface prioritizer.");
+ return false;
+ }
+
+ r = nr_ice_ctx_set_interface_prioritizer(ctx_, prioritizer);
+ if (r) {
+ MOZ_MTLOG(LogLevel::Error, "Couldn't set interface prioritizer.");
+ return false;
+ }
+
+ if (generating_trickle()) {
+ r = nr_ice_ctx_set_trickle_cb(ctx_, &NrIceCtx::trickle_cb, this);
+ if (r) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't set trickle cb for '" << name_ << "'");
+ return false;
+ }
+ }
+
+ // Create the handler objects
+ ice_handler_vtbl_ = new nr_ice_handler_vtbl();
+ ice_handler_vtbl_->select_pair = &NrIceCtx::select_pair;
+ ice_handler_vtbl_->stream_ready = &NrIceCtx::stream_ready;
+ ice_handler_vtbl_->stream_failed = &NrIceCtx::stream_failed;
+ ice_handler_vtbl_->ice_connected = &NrIceCtx::ice_connected;
+ ice_handler_vtbl_->msg_recvd = &NrIceCtx::msg_recvd;
+ ice_handler_vtbl_->ice_checking = &NrIceCtx::ice_checking;
+ ice_handler_vtbl_->ice_disconnected = &NrIceCtx::ice_disconnected;
+
+ ice_handler_ = new nr_ice_handler();
+ ice_handler_->vtbl = ice_handler_vtbl_;
+ ice_handler_->obj = this;
+
+ // Create the peer ctx. Because we do not support parallel forking, we
+ // only have one peer ctx.
+ std::string peer_name = name_ + ":default";
+ r = nr_ice_peer_ctx_create(ctx_, ice_handler_,
+ const_cast<char*>(peer_name.c_str()), &peer_);
+ if (r) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't create ICE peer ctx for '" << name_ << "'");
+ return false;
+ }
+
+ nsresult rv;
+ sts_target_ = do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv);
+
+ if (!NS_SUCCEEDED(rv)) return false;
+
+ return true;
+}
+
+int NrIceCtx::SetNat(const RefPtr<TestNat>& aNat) {
+ nat_ = aNat;
+ nr_socket_factory* fac;
+ int r = nat_->create_socket_factory(&fac);
+ if (r) {
+ return r;
+ }
+ nr_ice_ctx_set_socket_factory(ctx_, fac);
+ return 0;
+}
+
+// ONLY USE THIS FOR TESTING. Will cause totally unpredictable and possibly very
+// bad effects if ICE is still live.
+void NrIceCtx::internal_DeinitializeGlobal() {
+ NR_reg_del((char*)"stun");
+ NR_reg_del((char*)"ice");
+ RLogConnector::DestroyInstance();
+ nr_crypto_vtbl = nullptr;
+ initialized = false;
+}
+
+void NrIceCtx::internal_SetTimerAccelarator(int divider) {
+ ctx_->test_timer_divider = divider;
+}
+
+void NrIceCtx::AccumulateStats(const NrIceStats& stats) {
+ nr_accumulate_count(&(ctx_->stats.stun_retransmits), stats.stun_retransmits);
+ nr_accumulate_count(&(ctx_->stats.turn_401s), stats.turn_401s);
+ nr_accumulate_count(&(ctx_->stats.turn_403s), stats.turn_403s);
+ nr_accumulate_count(&(ctx_->stats.turn_438s), stats.turn_438s);
+}
+
+NrIceStats NrIceCtx::Destroy() {
+ // designed to be called more than once so if stats are desired, this can be
+ // called just prior to the destructor
+ MOZ_MTLOG(ML_NOTICE, "NrIceCtx(" << name_ << "): " << __func__);
+
+ for (auto& idAndStream : streams_) {
+ idAndStream.second->Close();
+ }
+
+ NrIceStats stats;
+ if (ctx_) {
+ stats.stun_retransmits = ctx_->stats.stun_retransmits;
+ stats.turn_401s = ctx_->stats.turn_401s;
+ stats.turn_403s = ctx_->stats.turn_403s;
+ stats.turn_438s = ctx_->stats.turn_438s;
+ }
+
+ if (peer_) {
+ nr_ice_peer_ctx_destroy(&peer_);
+ }
+ if (ctx_) {
+ nr_ice_ctx_destroy(&ctx_);
+ }
+
+ delete ice_handler_vtbl_;
+ delete ice_handler_;
+
+ ice_handler_vtbl_ = nullptr;
+ ice_handler_ = nullptr;
+ proxy_config_ = nullptr;
+ streams_.clear();
+
+ return stats;
+}
+
+NrIceCtx::~NrIceCtx() = default;
+
+void NrIceCtx::destroy_peer_ctx() { nr_ice_peer_ctx_destroy(&peer_); }
+
+nsresult NrIceCtx::SetControlling(Controlling controlling) {
+ if (!ice_controlling_set_) {
+ peer_->controlling = (controlling == ICE_CONTROLLING) ? 1 : 0;
+ ice_controlling_set_ = true;
+
+ MOZ_MTLOG(ML_DEBUG,
+ "ICE ctx " << name_ << " setting controlling to" << controlling);
+ }
+ return NS_OK;
+}
+
+NrIceCtx::Controlling NrIceCtx::GetControlling() {
+ return (peer_->controlling) ? ICE_CONTROLLING : ICE_CONTROLLED;
+}
+
+nsresult NrIceCtx::SetStunServers(
+ const std::vector<NrIceStunServer>& stun_servers) {
+ MOZ_MTLOG(ML_NOTICE, "NrIceCtx(" << name_ << "): " << __func__);
+ // We assume nr_ice_stun_server is memmoveable. That's true right now.
+ std::vector<nr_ice_stun_server> servers;
+
+ for (size_t i = 0; i < stun_servers.size(); ++i) {
+ nr_ice_stun_server server;
+ nsresult rv = stun_servers[i].ToNicerStunStruct(&server);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't convert STUN server for '" << name_ << "'");
+ } else {
+ servers.push_back(server);
+ }
+ }
+
+ int r = nr_ice_ctx_set_stun_servers(ctx_, servers.data(),
+ static_cast<int>(servers.size()));
+ if (r) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't set STUN servers for '" << name_ << "'");
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+// TODO(ekr@rtfm.com): This is just SetStunServers with s/Stun/Turn
+// Could we do a template or something?
+nsresult NrIceCtx::SetTurnServers(
+ const std::vector<NrIceTurnServer>& turn_servers) {
+ MOZ_MTLOG(ML_NOTICE, "NrIceCtx(" << name_ << "): " << __func__);
+ // We assume nr_ice_turn_server is memmoveable. That's true right now.
+ std::vector<nr_ice_turn_server> servers;
+
+ for (size_t i = 0; i < turn_servers.size(); ++i) {
+ nr_ice_turn_server server;
+ nsresult rv = turn_servers[i].ToNicerTurnStruct(&server);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't convert TURN server for '" << name_ << "'");
+ } else {
+ servers.push_back(server);
+ }
+ }
+
+ int r = nr_ice_ctx_set_turn_servers(ctx_, servers.data(),
+ static_cast<int>(servers.size()));
+ if (r) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't set TURN servers for '" << name_ << "'");
+ // TODO(ekr@rtfm.com): This leaks the username/password. Need to free that.
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+nsresult NrIceCtx::SetResolver(nr_resolver* resolver) {
+ int r = nr_ice_ctx_set_resolver(ctx_, resolver);
+
+ if (r) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't set resolver for '" << name_ << "'");
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+nsresult NrIceCtx::SetProxyConfig(NrSocketProxyConfig&& config) {
+ proxy_config_.reset(new NrSocketProxyConfig(std::move(config)));
+ if (nat_) {
+ nat_->set_proxy_config(proxy_config_);
+ }
+
+ if (proxy_config_->GetForceProxy()) {
+ nr_ice_ctx_add_flags(ctx_, NR_ICE_CTX_FLAGS_ONLY_PROXY);
+ } else {
+ nr_ice_ctx_remove_flags(ctx_, NR_ICE_CTX_FLAGS_ONLY_PROXY);
+ }
+
+ return NS_OK;
+}
+
+void NrIceCtx::SetCtxFlags(bool default_route_only) {
+ ASSERT_ON_THREAD(sts_target_);
+
+ if (default_route_only) {
+ nr_ice_ctx_add_flags(ctx_, NR_ICE_CTX_FLAGS_ONLY_DEFAULT_ADDRS);
+ } else {
+ nr_ice_ctx_remove_flags(ctx_, NR_ICE_CTX_FLAGS_ONLY_DEFAULT_ADDRS);
+ }
+}
+
+nsresult NrIceCtx::StartGathering(bool default_route_only,
+ bool obfuscate_host_addresses) {
+ ASSERT_ON_THREAD(sts_target_);
+ MOZ_MTLOG(ML_NOTICE, "NrIceCtx(" << name_ << "): " << __func__);
+
+ if (obfuscate_host_addresses) {
+ nr_ice_ctx_add_flags(ctx_, NR_ICE_CTX_FLAGS_OBFUSCATE_HOST_ADDRESSES);
+ }
+
+ SetCtxFlags(default_route_only);
+
+ // This might start gathering for the first time, or again after
+ // renegotiation, or might do nothing at all if gathering has already
+ // finished.
+ int r = nr_ice_gather(ctx_, &NrIceCtx::gather_cb, this);
+
+ if (!r) {
+ SetGatheringState(ICE_CTX_GATHER_COMPLETE);
+ } else if (r == R_WOULDBLOCK) {
+ SetGatheringState(ICE_CTX_GATHER_STARTED);
+ } else {
+ SetGatheringState(ICE_CTX_GATHER_COMPLETE);
+ MOZ_MTLOG(ML_ERROR, "ICE FAILED: Couldn't gather ICE candidates for '"
+ << name_ << "', error=" << r);
+ SetConnectionState(ICE_CTX_FAILED);
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+RefPtr<NrIceMediaStream> NrIceCtx::FindStream(nr_ice_media_stream* stream) {
+ for (auto& idAndStream : streams_) {
+ if (idAndStream.second->HasStream(stream)) {
+ return idAndStream.second;
+ }
+ }
+
+ return nullptr;
+}
+
+std::vector<std::string> NrIceCtx::GetGlobalAttributes() {
+ char** attrs = nullptr;
+ int attrct;
+ int r;
+ std::vector<std::string> ret;
+
+ r = nr_ice_get_global_attributes(ctx_, &attrs, &attrct);
+ if (r) {
+ MOZ_MTLOG(ML_ERROR,
+ "Couldn't get ufrag and password for '" << name_ << "'");
+ return ret;
+ }
+
+ for (int i = 0; i < attrct; i++) {
+ ret.push_back(std::string(attrs[i]));
+ RFREE(attrs[i]);
+ }
+ RFREE(attrs);
+
+ return ret;
+}
+
+nsresult NrIceCtx::ParseGlobalAttributes(std::vector<std::string> attrs) {
+ std::vector<char*> attrs_in;
+ attrs_in.reserve(attrs.size());
+ for (auto& attr : attrs) {
+ attrs_in.push_back(const_cast<char*>(attr.c_str()));
+ }
+
+ int r = nr_ice_peer_ctx_parse_global_attributes(
+ peer_, attrs_in.empty() ? nullptr : &attrs_in[0], attrs_in.size());
+ if (r) {
+ MOZ_MTLOG(ML_ERROR,
+ "Couldn't parse global attributes for " << name_ << "'");
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+bool NrIceCtx::HasStreamsToConnect() const {
+ for (auto& idAndStream : streams_) {
+ if (idAndStream.second->state() != NrIceMediaStream::ICE_CLOSED) {
+ return true;
+ }
+ }
+ return false;
+}
+
+nsresult NrIceCtx::StartChecks() {
+ int r;
+ MOZ_MTLOG(ML_NOTICE, "NrIceCtx(" << name_ << "): " << __func__);
+
+ if (!HasStreamsToConnect()) {
+ MOZ_MTLOG(ML_NOTICE, "In StartChecks, nothing to do on " << name_);
+ return NS_OK;
+ }
+
+ r = nr_ice_peer_ctx_pair_candidates(peer_);
+ if (r) {
+ MOZ_MTLOG(ML_ERROR, "ICE FAILED: Couldn't pair candidates on " << name_);
+ SetConnectionState(ICE_CTX_FAILED);
+ return NS_ERROR_FAILURE;
+ }
+
+ r = nr_ice_peer_ctx_start_checks2(peer_, 1);
+ if (r) {
+ if (r == R_NOT_FOUND) {
+ MOZ_MTLOG(ML_INFO, "Couldn't start peer checks on "
+ << name_ << ", assuming trickle ICE");
+ } else {
+ MOZ_MTLOG(ML_ERROR,
+ "ICE FAILED: Couldn't start peer checks on " << name_);
+ SetConnectionState(ICE_CTX_FAILED);
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ return NS_OK;
+}
+
+void NrIceCtx::gather_cb(NR_SOCKET s, int h, void* arg) {
+ NrIceCtx* ctx = static_cast<NrIceCtx*>(arg);
+
+ ctx->SetGatheringState(ICE_CTX_GATHER_COMPLETE);
+}
+
+void NrIceCtx::UpdateNetworkState(bool online) {
+ MOZ_MTLOG(ML_NOTICE, "NrIceCtx(" << name_ << "): updating network state to "
+ << (online ? "online" : "offline"));
+ if (connection_state_ == ICE_CTX_CLOSED) {
+ return;
+ }
+
+ if (online) {
+ nr_ice_peer_ctx_refresh_consent_all_streams(peer_);
+ } else {
+ nr_ice_peer_ctx_disconnect_all_streams(peer_);
+ }
+}
+
+void NrIceCtx::SetConnectionState(ConnectionState state) {
+ if (state == connection_state_) return;
+
+ MOZ_MTLOG(ML_INFO, "NrIceCtx(" << name_ << "): state " << connection_state_
+ << "->" << state);
+ connection_state_ = state;
+
+ if (connection_state_ == ICE_CTX_FAILED) {
+ MOZ_MTLOG(ML_INFO,
+ "NrIceCtx(" << name_ << "): dumping r_log ringbuffer... ");
+ std::deque<std::string> logs;
+ RLogConnector::GetInstance()->GetAny(0, &logs);
+ for (auto& log : logs) {
+ MOZ_MTLOG(ML_INFO, log);
+ }
+ }
+
+ SignalConnectionStateChange(this, state);
+}
+
+void NrIceCtx::SetGatheringState(GatheringState state) {
+ if (state == gathering_state_) return;
+
+ MOZ_MTLOG(ML_DEBUG, "NrIceCtx(" << name_ << "): gathering state "
+ << gathering_state_ << "->" << state);
+ gathering_state_ = state;
+
+ SignalGatheringStateChange(this, state);
+}
+
+void NrIceCtx::GenerateObfuscatedAddress(nr_ice_candidate* candidate,
+ std::string* mdns_address,
+ std::string* actual_address) {
+ if (candidate->type == HOST &&
+ (ctx_->flags & NR_ICE_CTX_FLAGS_OBFUSCATE_HOST_ADDRESSES)) {
+ char addr[64];
+ if (nr_transport_addr_get_addrstring(&candidate->addr, addr,
+ sizeof(addr))) {
+ return;
+ }
+
+ *actual_address = addr;
+
+ const auto& iter = obfuscated_host_addresses_.find(*actual_address);
+ if (iter != obfuscated_host_addresses_.end()) {
+ *mdns_address = iter->second;
+ } else {
+ nsresult rv;
+ nsCOMPtr<nsIUUIDGenerator> uuidgen =
+ do_GetService("@mozilla.org/uuid-generator;1", &rv);
+ // If this fails, we'll return a zero UUID rather than something
+ // unexpected.
+ nsID id = {};
+ id.Clear();
+ if (NS_SUCCEEDED(rv)) {
+ rv = uuidgen->GenerateUUIDInPlace(&id);
+ if (NS_FAILED(rv)) {
+ id.Clear();
+ }
+ }
+
+ char chars[NSID_LENGTH];
+ id.ToProvidedString(chars);
+ // The string will look like {64888863-a253-424a-9b30-1ed285d20142},
+ // we want to trim off the braces.
+ const char* ptr_to_id = chars;
+ ++ptr_to_id;
+ chars[NSID_LENGTH - 2] = 0;
+
+ std::ostringstream o;
+ o << ptr_to_id << ".local";
+ *mdns_address = o.str();
+
+ obfuscated_host_addresses_[*actual_address] = *mdns_address;
+ }
+ candidate->mdns_addr = r_strdup(mdns_address->c_str());
+ }
+}
+
+} // namespace mozilla
+
+// Reimplement nr_ice_compute_codeword to avoid copyright issues
+void nr_ice_compute_codeword(char* buf, int len, char* codeword) {
+ UINT4 c;
+
+ r_crc32(buf, len, &c);
+
+ PL_Base64Encode(reinterpret_cast<char*>(&c), 3, codeword);
+ codeword[4] = 0;
+}
+
+int nr_socket_local_create(void* obj, nr_transport_addr* addr,
+ nr_socket** sockp) {
+ using namespace mozilla;
+
+ RefPtr<NrSocketBase> sock;
+ int r, _status;
+ shared_ptr<NrSocketProxyConfig> config = nullptr;
+
+ if (obj) {
+ config = static_cast<NrIceCtx*>(obj)->GetProxyConfig();
+ }
+
+ r = NrSocketBase::CreateSocket(addr, &sock, config);
+ if (r) {
+ ABORT(r);
+ }
+
+ r = nr_socket_create_int(static_cast<void*>(sock), sock->vtbl(), sockp);
+ if (r) ABORT(r);
+
+ _status = 0;
+
+ {
+ // We will release this reference in destroy(), not exactly the normal
+ // ownership model, but it is what it is.
+ NrSocketBase* dummy = sock.forget().take();
+ (void)dummy;
+ }
+
+abort:
+ return _status;
+}
diff --git a/dom/media/webrtc/transport/nricectx.h b/dom/media/webrtc/transport/nricectx.h
new file mode 100644
index 0000000000..a0a0b5b772
--- /dev/null
+++ b/dom/media/webrtc/transport/nricectx.h
@@ -0,0 +1,421 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+// Some of this code is cut-and-pasted from nICEr. Copyright is:
+
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+// This is a wrapper around the nICEr ICE stack
+#ifndef nricectx_h__
+#define nricectx_h__
+
+#include <memory>
+#include <string>
+#include <vector>
+#include <map>
+
+#include "sigslot.h"
+
+#include "prnetdb.h"
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "nsIEventTarget.h"
+#include "nsTArray.h"
+#include "mozilla/Maybe.h"
+
+#include "m_cpp_utils.h"
+#include "nricestunaddr.h"
+#include "nricemediastream.h"
+
+typedef struct nr_ice_ctx_ nr_ice_ctx;
+typedef struct nr_ice_peer_ctx_ nr_ice_peer_ctx;
+typedef struct nr_ice_media_stream_ nr_ice_media_stream;
+typedef struct nr_ice_handler_ nr_ice_handler;
+typedef struct nr_ice_handler_vtbl_ nr_ice_handler_vtbl;
+typedef struct nr_ice_candidate_ nr_ice_candidate;
+typedef struct nr_ice_cand_pair_ nr_ice_cand_pair;
+typedef struct nr_ice_stun_server_ nr_ice_stun_server;
+typedef struct nr_ice_turn_server_ nr_ice_turn_server;
+typedef struct nr_resolver_ nr_resolver;
+typedef struct nr_proxy_tunnel_config_ nr_proxy_tunnel_config;
+
+typedef void* NR_SOCKET;
+
+namespace mozilla {
+
+class NrSocketProxyConfig;
+
+class NrIceMediaStream;
+
+extern const char kNrIceTransportUdp[];
+extern const char kNrIceTransportTcp[];
+extern const char kNrIceTransportTls[];
+
+class NrIceStunServer {
+ public:
+ explicit NrIceStunServer(const PRNetAddr& addr) : has_addr_(true) {
+ memcpy(&addr_, &addr, sizeof(addr));
+ }
+
+ // The main function to use. Will take either an address or a hostname.
+ static UniquePtr<NrIceStunServer> Create(
+ const std::string& addr, uint16_t port,
+ const char* transport = kNrIceTransportUdp) {
+ UniquePtr<NrIceStunServer> server(new NrIceStunServer(transport));
+
+ nsresult rv = server->Init(addr, port);
+ if (NS_FAILED(rv)) return nullptr;
+
+ return server;
+ }
+
+ nsresult ToNicerStunStruct(nr_ice_stun_server* server) const;
+
+ bool HasFqdn() const { return !has_addr_; }
+
+ void SetUseIPv6IfFqdn() {
+ MOZ_ASSERT(HasFqdn());
+ use_ipv6_if_fqdn_ = true;
+ }
+
+ protected:
+ explicit NrIceStunServer(const char* transport)
+ : addr_(), transport_(transport) {}
+
+ nsresult Init(const std::string& addr, uint16_t port) {
+ PRStatus status = PR_StringToNetAddr(addr.c_str(), &addr_);
+ if (status == PR_SUCCESS) {
+ // Parseable as an address
+ addr_.inet.port = PR_htons(port);
+ port_ = port;
+ has_addr_ = true;
+ return NS_OK;
+ } else if (addr.size() < 256) {
+ // Apparently this is a hostname.
+ host_ = addr;
+ port_ = port;
+ has_addr_ = false;
+ return NS_OK;
+ }
+
+ return NS_ERROR_FAILURE;
+ }
+
+ bool has_addr_;
+ std::string host_;
+ uint16_t port_;
+ PRNetAddr addr_;
+ std::string transport_;
+ bool use_ipv6_if_fqdn_ = false;
+};
+
+class NrIceTurnServer : public NrIceStunServer {
+ public:
+ static UniquePtr<NrIceTurnServer> Create(
+ const std::string& addr, uint16_t port, const std::string& username,
+ const std::vector<unsigned char>& password,
+ const char* transport = kNrIceTransportUdp) {
+ UniquePtr<NrIceTurnServer> server(
+ new NrIceTurnServer(username, password, transport));
+
+ nsresult rv = server->Init(addr, port);
+ if (NS_FAILED(rv)) return nullptr;
+
+ return server;
+ }
+
+ nsresult ToNicerTurnStruct(nr_ice_turn_server* server) const;
+
+ private:
+ NrIceTurnServer(const std::string& username,
+ const std::vector<unsigned char>& password,
+ const char* transport)
+ : NrIceStunServer(transport), username_(username), password_(password) {}
+
+ std::string username_;
+ std::vector<unsigned char> password_;
+};
+
+class TestNat;
+
+class NrIceStats {
+ public:
+ uint16_t stun_retransmits = 0;
+ uint16_t turn_401s = 0;
+ uint16_t turn_403s = 0;
+ uint16_t turn_438s = 0;
+};
+
+class NrIceCtx {
+ public:
+ enum ConnectionState {
+ ICE_CTX_INIT,
+ ICE_CTX_CHECKING,
+ ICE_CTX_CONNECTED,
+ ICE_CTX_COMPLETED,
+ ICE_CTX_FAILED,
+ ICE_CTX_DISCONNECTED,
+ ICE_CTX_CLOSED
+ };
+
+ enum GatheringState {
+ ICE_CTX_GATHER_INIT,
+ ICE_CTX_GATHER_STARTED,
+ ICE_CTX_GATHER_COMPLETE
+ };
+
+ enum Controlling { ICE_CONTROLLING, ICE_CONTROLLED };
+
+ enum Policy { ICE_POLICY_RELAY, ICE_POLICY_NO_HOST, ICE_POLICY_ALL };
+
+ struct NatSimulatorConfig {
+ bool mBlockTcp = false;
+ bool mBlockUdp = false;
+ bool mBlockTls = false;
+ int mErrorCodeForDrop = 0;
+ nsCString mMappingType = "ENDPOINT_INDEPENDENT"_ns;
+ nsCString mFilteringType = "ENDPOINT_INDEPENDENT"_ns;
+ nsCString mRedirectAddress;
+ CopyableTArray<nsCString> mRedirectTargets;
+ };
+
+ struct Config {
+ NrIceCtx::Policy mPolicy = NrIceCtx::ICE_POLICY_ALL;
+ Maybe<NatSimulatorConfig> mNatSimulatorConfig;
+ };
+
+ static RefPtr<NrIceCtx> Create(const std::string& aName);
+
+ nsresult SetIceConfig(const Config& aConfig);
+
+ RefPtr<NrIceMediaStream> CreateStream(const std::string& id,
+ const std::string& name,
+ int components);
+ void DestroyStream(const std::string& id);
+
+ struct GlobalConfig {
+ bool mAllowLinkLocal = false;
+ bool mAllowLoopback = false;
+ bool mTcpEnabled = true;
+ int mStunClientMaxTransmits = 7;
+ int mTrickleIceGracePeriod = 5000;
+ int mIceTcpSoSockCount = 3;
+ int mIceTcpListenBacklog = 10;
+ nsCString mForceNetInterface;
+ };
+
+ // initialize ICE globals, crypto, and logging
+ static void InitializeGlobals(const GlobalConfig& aConfig);
+
+ void SetTargetForDefaultLocalAddressLookup(const std::string& target_ip,
+ uint16_t target_port);
+
+ // static GetStunAddrs for use in parent process to support
+ // sandboxing restrictions
+ static nsTArray<NrIceStunAddr> GetStunAddrs();
+ void SetStunAddrs(const nsTArray<NrIceStunAddr>& addrs);
+
+ bool Initialize();
+
+ int SetNat(const RefPtr<TestNat>& aNat);
+
+ // Deinitialize all ICE global state. Used only for testing.
+ static void internal_DeinitializeGlobal();
+
+ // Divide some timers to faster testing. Used only for testing.
+ void internal_SetTimerAccelarator(int divider);
+
+ nr_ice_ctx* ctx() { return ctx_; }
+ nr_ice_peer_ctx* peer() { return peer_; }
+
+ // Testing only.
+ void destroy_peer_ctx();
+
+ RefPtr<NrIceMediaStream> GetStream(const std::string& id) {
+ auto it = streams_.find(id);
+ if (it != streams_.end()) {
+ return it->second;
+ }
+ return nullptr;
+ }
+
+ std::vector<RefPtr<NrIceMediaStream>> GetStreams() const {
+ std::vector<RefPtr<NrIceMediaStream>> result;
+ for (auto& idAndStream : streams_) {
+ result.push_back(idAndStream.second);
+ }
+ return result;
+ }
+
+ bool HasStreamsToConnect() const;
+
+ // The name of the ctx
+ const std::string& name() const { return name_; }
+
+ // Current state
+ ConnectionState connection_state() const { return connection_state_; }
+
+ // Current state
+ GatheringState gathering_state() const { return gathering_state_; }
+
+ // Get the global attributes
+ std::vector<std::string> GetGlobalAttributes();
+
+ // Set the other side's global attributes
+ nsresult ParseGlobalAttributes(std::vector<std::string> attrs);
+
+ // Set whether we are controlling or not.
+ nsresult SetControlling(Controlling controlling);
+
+ Controlling GetControlling();
+
+ // Set the STUN servers. Must be called before StartGathering
+ // (if at all).
+ nsresult SetStunServers(const std::vector<NrIceStunServer>& stun_servers);
+
+ // Set the TURN servers. Must be called before StartGathering
+ // (if at all).
+ nsresult SetTurnServers(const std::vector<NrIceTurnServer>& turn_servers);
+
+ // Provide the resolution provider. Must be called before
+ // StartGathering.
+ nsresult SetResolver(nr_resolver* resolver);
+
+ // Provide the proxy address. Must be called before
+ // StartGathering.
+ nsresult SetProxyConfig(NrSocketProxyConfig&& config);
+
+ const std::shared_ptr<NrSocketProxyConfig>& GetProxyConfig() {
+ return proxy_config_;
+ }
+
+ void SetCtxFlags(bool default_route_only);
+
+ // Start ICE gathering
+ nsresult StartGathering(bool default_route_only,
+ bool obfuscate_host_addresses);
+
+ // Start checking
+ nsresult StartChecks();
+
+ // Notify that the network has gone online/offline
+ void UpdateNetworkState(bool online);
+
+ void AccumulateStats(const NrIceStats& stats);
+ NrIceStats Destroy();
+
+ // Are we trickling?
+ bool generating_trickle() const { return trickle_; }
+
+ // Signals to indicate events. API users can (and should)
+ // register for these.
+ sigslot::signal2<NrIceCtx*, NrIceCtx::GatheringState>
+ SignalGatheringStateChange;
+ sigslot::signal2<NrIceCtx*, NrIceCtx::ConnectionState>
+ SignalConnectionStateChange;
+
+ // The thread to direct method calls to
+ nsCOMPtr<nsIEventTarget> thread() { return sts_target_; }
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(NrIceCtx)
+
+ private:
+ explicit NrIceCtx(const std::string& name);
+
+ virtual ~NrIceCtx();
+
+ DISALLOW_COPY_ASSIGN(NrIceCtx);
+
+ // Callbacks for nICEr
+ static void gather_cb(NR_SOCKET s, int h, void* arg); // ICE gather complete
+
+ // Handler implementation
+ static int select_pair(void* obj, nr_ice_media_stream* stream,
+ int component_id, nr_ice_cand_pair** potentials,
+ int potential_ct);
+ static int stream_ready(void* obj, nr_ice_media_stream* stream);
+ static int stream_failed(void* obj, nr_ice_media_stream* stream);
+ static int ice_checking(void* obj, nr_ice_peer_ctx* pctx);
+ static int ice_connected(void* obj, nr_ice_peer_ctx* pctx);
+ static int ice_disconnected(void* obj, nr_ice_peer_ctx* pctx);
+ static int msg_recvd(void* obj, nr_ice_peer_ctx* pctx,
+ nr_ice_media_stream* stream, int component_id,
+ unsigned char* msg, int len);
+ static void trickle_cb(void* arg, nr_ice_ctx* ctx,
+ nr_ice_media_stream* stream, int component_id,
+ nr_ice_candidate* candidate);
+
+ // Find a media stream by stream ptr. Gross
+ RefPtr<NrIceMediaStream> FindStream(nr_ice_media_stream* stream);
+
+ // Set the state
+ void SetConnectionState(ConnectionState state);
+
+ // Set the state
+ void SetGatheringState(GatheringState state);
+
+ void GenerateObfuscatedAddress(nr_ice_candidate* candidate,
+ std::string* mdns_address,
+ std::string* actual_address);
+
+ ConnectionState connection_state_;
+ GatheringState gathering_state_;
+ const std::string name_;
+ bool ice_controlling_set_;
+ std::map<std::string, RefPtr<NrIceMediaStream>> streams_;
+ nr_ice_ctx* ctx_;
+ nr_ice_peer_ctx* peer_;
+ nr_ice_handler_vtbl* ice_handler_vtbl_; // Must be pointer
+ nr_ice_handler* ice_handler_; // Must be pointer
+ bool trickle_;
+ nsCOMPtr<nsIEventTarget> sts_target_; // The thread to run on
+ Config config_;
+ RefPtr<TestNat> nat_;
+ std::shared_ptr<NrSocketProxyConfig> proxy_config_;
+ std::map<std::string, std::string> obfuscated_host_addresses_;
+};
+
+} // namespace mozilla
+#endif
diff --git a/dom/media/webrtc/transport/nricemediastream.cpp b/dom/media/webrtc/transport/nricemediastream.cpp
new file mode 100644
index 0000000000..ef7980dcf3
--- /dev/null
+++ b/dom/media/webrtc/transport/nricemediastream.cpp
@@ -0,0 +1,709 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+// Some of this code is cut-and-pasted from nICEr. Copyright is:
+
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <string>
+#include <vector>
+
+#include "logging.h"
+#include "nsError.h"
+#include "nsThreadUtils.h"
+
+// nICEr includes
+extern "C" {
+#include "nr_api.h"
+#include "transport_addr.h"
+#include "nr_socket.h"
+#include "ice_ctx.h"
+#include "ice_candidate.h"
+#include "ice_handler.h"
+}
+
+// Local includes
+#include "nricectx.h"
+#include "nricemediastream.h"
+
+namespace mozilla {
+
+MOZ_MTLOG_MODULE("mtransport")
+
+static bool ToNrIceAddr(nr_transport_addr& addr, NrIceAddr* out) {
+ int r;
+ char addrstring[INET6_ADDRSTRLEN + 1];
+
+ r = nr_transport_addr_get_addrstring(&addr, addrstring, sizeof(addrstring));
+ if (r) return false;
+ out->host = addrstring;
+
+ int port;
+ r = nr_transport_addr_get_port(&addr, &port);
+ if (r) return false;
+
+ out->port = port;
+
+ switch (addr.protocol) {
+ case IPPROTO_TCP:
+ if (addr.tls) {
+ out->transport = kNrIceTransportTls;
+ } else {
+ out->transport = kNrIceTransportTcp;
+ }
+ break;
+ case IPPROTO_UDP:
+ out->transport = kNrIceTransportUdp;
+ break;
+ default:
+ MOZ_CRASH();
+ return false;
+ }
+
+ return true;
+}
+
+static bool ToNrIceCandidate(const nr_ice_candidate& candc,
+ NrIceCandidate* out) {
+ MOZ_ASSERT(out);
+ int r;
+ // Const-cast because the internal nICEr code isn't const-correct.
+ nr_ice_candidate* cand = const_cast<nr_ice_candidate*>(&candc);
+
+ if (!ToNrIceAddr(cand->addr, &out->cand_addr)) return false;
+
+ if (cand->mdns_addr) {
+ out->mdns_addr = cand->mdns_addr;
+ }
+
+ if (cand->isock) {
+ nr_transport_addr addr;
+ r = nr_socket_getaddr(cand->isock->sock, &addr);
+ if (r) return false;
+
+ out->is_proxied = addr.is_proxied;
+
+ if (!ToNrIceAddr(addr, &out->local_addr)) return false;
+ }
+
+ NrIceCandidate::Type type;
+
+ switch (cand->type) {
+ case HOST:
+ type = NrIceCandidate::ICE_HOST;
+ break;
+ case SERVER_REFLEXIVE:
+ type = NrIceCandidate::ICE_SERVER_REFLEXIVE;
+ break;
+ case PEER_REFLEXIVE:
+ type = NrIceCandidate::ICE_PEER_REFLEXIVE;
+ break;
+ case RELAYED:
+ type = NrIceCandidate::ICE_RELAYED;
+ break;
+ default:
+ return false;
+ }
+
+ NrIceCandidate::TcpType tcp_type;
+ switch (cand->tcp_type) {
+ case TCP_TYPE_ACTIVE:
+ tcp_type = NrIceCandidate::ICE_ACTIVE;
+ break;
+ case TCP_TYPE_PASSIVE:
+ tcp_type = NrIceCandidate::ICE_PASSIVE;
+ break;
+ case TCP_TYPE_SO:
+ tcp_type = NrIceCandidate::ICE_SO;
+ break;
+ default:
+ tcp_type = NrIceCandidate::ICE_NONE;
+ break;
+ }
+
+ out->type = type;
+ out->tcp_type = tcp_type;
+ out->codeword = candc.codeword;
+ out->label = candc.label;
+ out->trickled = candc.trickled;
+ out->priority = candc.priority;
+ return true;
+}
+
+// Make an NrIceCandidate from the candidate |cand|.
+// This is not a member fxn because we want to hide the
+// defn of nr_ice_candidate but we pass by reference.
+static UniquePtr<NrIceCandidate> MakeNrIceCandidate(
+ const nr_ice_candidate& candc) {
+ UniquePtr<NrIceCandidate> out(new NrIceCandidate());
+
+ if (!ToNrIceCandidate(candc, out.get())) {
+ return nullptr;
+ }
+ return out;
+}
+
+static bool Matches(const nr_ice_media_stream* stream, const std::string& ufrag,
+ const std::string& pwd) {
+ return stream && (stream->ufrag == ufrag) && (stream->pwd == pwd);
+}
+
+NrIceMediaStream::NrIceMediaStream(NrIceCtx* ctx, const std::string& id,
+ const std::string& name, size_t components)
+ : state_(ICE_CONNECTING),
+ ctx_(ctx),
+ name_(name),
+ components_(components),
+ stream_(nullptr),
+ old_stream_(nullptr),
+ id_(id) {}
+
+NrIceMediaStream::~NrIceMediaStream() {
+ // We do not need to destroy anything. All major resources
+ // are attached to the ice ctx.
+}
+
+nsresult NrIceMediaStream::ConnectToPeer(
+ const std::string& ufrag, const std::string& pwd,
+ const std::vector<std::string>& attributes) {
+ MOZ_ASSERT(stream_);
+
+ if (Matches(old_stream_, ufrag, pwd)) {
+ // (We swap before we close so we never have stream_ == nullptr)
+ MOZ_MTLOG(ML_DEBUG,
+ "Rolling back to old stream ufrag=" << ufrag << " " << name_);
+ std::swap(stream_, old_stream_);
+ CloseStream(&old_stream_);
+ } else if (old_stream_) {
+ // Right now we wait for ICE to complete before closing the old stream.
+ // It might be worth it to close it sooner, but we don't want to close it
+ // right away.
+ MOZ_MTLOG(ML_DEBUG,
+ "ICE restart committed, marking old stream as obsolete, "
+ "beginning switchover to ufrag="
+ << ufrag << " " << name_);
+ nr_ice_media_stream_set_obsolete(old_stream_);
+ }
+
+ nr_ice_media_stream* peer_stream;
+ if (nr_ice_peer_ctx_find_pstream(ctx_->peer(), stream_, &peer_stream)) {
+ // No peer yet
+ std::vector<char*> attributes_in;
+ attributes_in.reserve(attributes.size());
+ for (auto& attribute : attributes) {
+ MOZ_MTLOG(ML_DEBUG, "Setting " << attribute << " on stream " << name_);
+ attributes_in.push_back(const_cast<char*>(attribute.c_str()));
+ }
+
+ // Still need to call nr_ice_ctx_parse_stream_attributes.
+ int r = nr_ice_peer_ctx_parse_stream_attributes(
+ ctx_->peer(), stream_,
+ attributes_in.empty() ? nullptr : &attributes_in[0],
+ attributes_in.size());
+ if (r) {
+ MOZ_MTLOG(ML_ERROR,
+ "Couldn't parse attributes for stream " << name_ << "'");
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult NrIceMediaStream::SetIceCredentials(const std::string& ufrag,
+ const std::string& pwd) {
+ if (Matches(stream_, ufrag, pwd)) {
+ return NS_OK;
+ }
+
+ if (Matches(old_stream_, ufrag, pwd)) {
+ return NS_OK;
+ }
+
+ MOZ_MTLOG(ML_DEBUG, "Setting ICE credentials for " << name_ << " - " << ufrag
+ << ":" << pwd);
+ CloseStream(&old_stream_);
+ old_stream_ = stream_;
+
+ std::string name(name_ + " - " + ufrag + ":" + pwd);
+
+ int r = nr_ice_add_media_stream(ctx_->ctx(), name.c_str(), ufrag.c_str(),
+ pwd.c_str(), components_, &stream_);
+ if (r) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't create ICE media stream for '"
+ << name_ << "': error=" << r);
+ stream_ = old_stream_;
+ old_stream_ = nullptr;
+ return NS_ERROR_FAILURE;
+ }
+
+ state_ = ICE_CONNECTING;
+ return NS_OK;
+}
+
+// Parse trickle ICE candidate
+nsresult NrIceMediaStream::ParseTrickleCandidate(const std::string& candidate,
+ const std::string& ufrag,
+ const std::string& mdns_addr) {
+ nr_ice_media_stream* stream = GetStreamForRemoteUfrag(ufrag);
+ if (!stream) {
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_MTLOG(ML_NOTICE, "NrIceCtx(" << ctx_->ctx()->label << ")/STREAM("
+ << name() << ") : parsing trickle candidate "
+ << candidate);
+
+ int r = nr_ice_peer_ctx_parse_trickle_candidate(
+ ctx_->peer(), stream, const_cast<char*>(candidate.c_str()),
+ mdns_addr.c_str());
+
+ if (r) {
+ if (r == R_ALREADY) {
+ MOZ_MTLOG(ML_INFO, "Trickle candidate is redundant for stream '"
+ << name_
+ << "' because it is completed: " << candidate);
+ } else if (r == R_REJECTED) {
+ MOZ_MTLOG(ML_INFO,
+ "Trickle candidate is ignored for stream '"
+ << name_
+ << "', probably because it is for an unused component"
+ << ": " << candidate);
+ } else {
+ MOZ_MTLOG(ML_ERROR, "Couldn't parse trickle candidate for stream '"
+ << name_ << "': " << candidate);
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ return NS_OK;
+}
+
+// Returns NS_ERROR_NOT_AVAILABLE if component is unpaired or disabled.
+nsresult NrIceMediaStream::GetActivePair(int component,
+ UniquePtr<NrIceCandidate>* localp,
+ UniquePtr<NrIceCandidate>* remotep) {
+ int r;
+ nr_ice_candidate* local_int;
+ nr_ice_candidate* remote_int;
+
+ if (!stream_) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ r = nr_ice_media_stream_get_active(ctx_->peer(), stream_, component,
+ &local_int, &remote_int);
+ // If result is R_REJECTED then component is unpaired or disabled.
+ if (r == R_REJECTED) return NS_ERROR_NOT_AVAILABLE;
+
+ if (r) return NS_ERROR_FAILURE;
+
+ UniquePtr<NrIceCandidate> local(MakeNrIceCandidate(*local_int));
+ if (!local) return NS_ERROR_FAILURE;
+
+ UniquePtr<NrIceCandidate> remote(MakeNrIceCandidate(*remote_int));
+ if (!remote) return NS_ERROR_FAILURE;
+
+ if (localp) *localp = std::move(local);
+ if (remotep) *remotep = std::move(remote);
+
+ return NS_OK;
+}
+
+nsresult NrIceMediaStream::GetCandidatePairs(
+ std::vector<NrIceCandidatePair>* out_pairs) const {
+ MOZ_ASSERT(out_pairs);
+ if (!stream_) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // If we haven't at least started checking then there is nothing to report
+ if (ctx_->peer()->state != NR_ICE_PEER_STATE_PAIRED) {
+ return NS_OK;
+ }
+
+ // Get the check_list on the peer stream (this is where the check_list
+ // actually lives, not in stream_)
+ nr_ice_media_stream* peer_stream;
+ int r = nr_ice_peer_ctx_find_pstream(ctx_->peer(), stream_, &peer_stream);
+ if (r != 0) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nr_ice_cand_pair *p1, *p2;
+ out_pairs->clear();
+
+ TAILQ_FOREACH(p1, &peer_stream->check_list, check_queue_entry) {
+ MOZ_ASSERT(p1);
+ MOZ_ASSERT(p1->local);
+ MOZ_ASSERT(p1->remote);
+ NrIceCandidatePair pair;
+
+ p2 = TAILQ_FIRST(&peer_stream->check_list);
+ while (p2) {
+ if (p1 == p2) {
+ /* Don't compare with our self. */
+ p2 = TAILQ_NEXT(p2, check_queue_entry);
+ continue;
+ }
+ if (strncmp(p1->codeword, p2->codeword, sizeof(p1->codeword)) == 0) {
+ /* In case of duplicate pairs we only report the one winning pair */
+ if (((p2->remote->component && (p2->remote->component->active == p2)) &&
+ !(p1->remote->component &&
+ (p1->remote->component->active == p1))) ||
+ ((p2->peer_nominated || p2->nominated) &&
+ !(p1->peer_nominated || p1->nominated)) ||
+ (p2->priority > p1->priority) ||
+ ((p2->state == NR_ICE_PAIR_STATE_SUCCEEDED) &&
+ (p1->state != NR_ICE_PAIR_STATE_SUCCEEDED)) ||
+ ((p2->state != NR_ICE_PAIR_STATE_CANCELLED) &&
+ (p1->state == NR_ICE_PAIR_STATE_CANCELLED))) {
+ /* p2 is a better pair. */
+ break;
+ }
+ }
+ p2 = TAILQ_NEXT(p2, check_queue_entry);
+ }
+ if (p2) {
+ /* p2 points to a duplicate but better pair so skip this one */
+ continue;
+ }
+
+ switch (p1->state) {
+ case NR_ICE_PAIR_STATE_FROZEN:
+ pair.state = NrIceCandidatePair::State::STATE_FROZEN;
+ break;
+ case NR_ICE_PAIR_STATE_WAITING:
+ pair.state = NrIceCandidatePair::State::STATE_WAITING;
+ break;
+ case NR_ICE_PAIR_STATE_IN_PROGRESS:
+ pair.state = NrIceCandidatePair::State::STATE_IN_PROGRESS;
+ break;
+ case NR_ICE_PAIR_STATE_FAILED:
+ pair.state = NrIceCandidatePair::State::STATE_FAILED;
+ break;
+ case NR_ICE_PAIR_STATE_SUCCEEDED:
+ pair.state = NrIceCandidatePair::State::STATE_SUCCEEDED;
+ break;
+ case NR_ICE_PAIR_STATE_CANCELLED:
+ pair.state = NrIceCandidatePair::State::STATE_CANCELLED;
+ break;
+ default:
+ MOZ_ASSERT(0);
+ }
+
+ pair.priority = p1->priority;
+ pair.nominated = p1->peer_nominated || p1->nominated;
+ pair.component_id = p1->remote->component->component_id;
+
+ // As discussed with drno: a component's can_send field (set to true
+ // by ICE consent) is a very close approximation for writable and
+ // readable. Note: the component for the local candidate never has
+ // the can_send member set to true, remote for both readable and
+ // writable. (mjf)
+ pair.writable = p1->remote->component->can_send;
+ pair.readable = p1->remote->component->can_send;
+ pair.selected =
+ p1->remote->component && p1->remote->component->active == p1;
+ pair.codeword = p1->codeword;
+ pair.bytes_sent = p1->bytes_sent;
+ pair.bytes_recvd = p1->bytes_recvd;
+ pair.ms_since_last_send =
+ p1->last_sent.tv_sec * 1000 + p1->last_sent.tv_usec / 1000;
+ pair.ms_since_last_recv =
+ p1->last_recvd.tv_sec * 1000 + p1->last_recvd.tv_usec / 1000;
+
+ if (!ToNrIceCandidate(*(p1->local), &pair.local) ||
+ !ToNrIceCandidate(*(p1->remote), &pair.remote)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ out_pairs->push_back(pair);
+ }
+
+ return NS_OK;
+}
+
+nsresult NrIceMediaStream::GetDefaultCandidate(
+ int component, NrIceCandidate* candidate) const {
+ if (!stream_) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ nr_ice_candidate* cand;
+
+ int r = nr_ice_media_stream_get_default_candidate(stream_, component, &cand);
+ if (r) {
+ if (r == R_NOT_FOUND) {
+ MOZ_MTLOG(ML_INFO, "Couldn't get default ICE candidate for '"
+ << name_ << "', no candidates.");
+ } else {
+ MOZ_MTLOG(ML_ERROR, "Couldn't get default ICE candidate for '"
+ << name_ << "', " << r);
+ }
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!ToNrIceCandidate(*cand, candidate)) {
+ MOZ_MTLOG(ML_ERROR,
+ "Failed to convert default ICE candidate for '" << name_ << "'");
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+std::vector<std::string> NrIceMediaStream::GetAttributes() const {
+ char** attrs = nullptr;
+ int attrct;
+ int r;
+ std::vector<std::string> ret;
+
+ if (!stream_) {
+ return ret;
+ }
+
+ r = nr_ice_media_stream_get_attributes(stream_, &attrs, &attrct);
+ if (r) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't get ICE candidates for '" << name_ << "'");
+ return ret;
+ }
+
+ for (int i = 0; i < attrct; i++) {
+ ret.push_back(attrs[i]);
+ RFREE(attrs[i]);
+ }
+
+ RFREE(attrs);
+
+ return ret;
+}
+
+static nsresult GetCandidatesFromStream(
+ nr_ice_media_stream* stream, std::vector<NrIceCandidate>* candidates) {
+ MOZ_ASSERT(candidates);
+ nr_ice_component* comp = STAILQ_FIRST(&stream->components);
+ while (comp) {
+ if (comp->state != NR_ICE_COMPONENT_DISABLED) {
+ nr_ice_candidate* cand = TAILQ_FIRST(&comp->candidates);
+ while (cand) {
+ NrIceCandidate new_cand;
+ // This can fail if the candidate is server reflexive or relayed, and
+ // has not yet received a response (ie; it doesn't know its address
+ // yet). For the purposes of this code, this isn't a candidate we're
+ // interested in, since it is not fully baked yet.
+ if (ToNrIceCandidate(*cand, &new_cand)) {
+ candidates->push_back(new_cand);
+ }
+ cand = TAILQ_NEXT(cand, entry_comp);
+ }
+ }
+ comp = STAILQ_NEXT(comp, entry);
+ }
+
+ return NS_OK;
+}
+
+nsresult NrIceMediaStream::GetLocalCandidates(
+ std::vector<NrIceCandidate>* candidates) const {
+ if (!stream_) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return GetCandidatesFromStream(stream_, candidates);
+}
+
+nsresult NrIceMediaStream::GetRemoteCandidates(
+ std::vector<NrIceCandidate>* candidates) const {
+ if (!stream_) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // If we haven't at least started checking then there is nothing to report
+ if (ctx_->peer()->state != NR_ICE_PEER_STATE_PAIRED) {
+ return NS_OK;
+ }
+
+ nr_ice_media_stream* peer_stream;
+ int r = nr_ice_peer_ctx_find_pstream(ctx_->peer(), stream_, &peer_stream);
+ if (r != 0) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return GetCandidatesFromStream(peer_stream, candidates);
+}
+
+nsresult NrIceMediaStream::DisableComponent(int component_id) {
+ if (!stream_) return NS_ERROR_FAILURE;
+
+ int r = nr_ice_media_stream_disable_component(stream_, component_id);
+ if (r) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't disable '" << name_ << "':" << component_id);
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+nsresult NrIceMediaStream::GetConsentStatus(int component_id, bool* can_send,
+ struct timeval* ts) {
+ if (!stream_) return NS_ERROR_FAILURE;
+
+ nr_ice_media_stream* peer_stream;
+ int r = nr_ice_peer_ctx_find_pstream(ctx_->peer(), stream_, &peer_stream);
+ if (r) {
+ MOZ_MTLOG(ML_ERROR, "Failed to find peer stream for '"
+ << name_ << "':" << component_id);
+ return NS_ERROR_FAILURE;
+ }
+
+ int send = 0;
+ r = nr_ice_media_stream_get_consent_status(peer_stream, component_id, &send,
+ ts);
+ if (r) {
+ MOZ_MTLOG(ML_ERROR, "Failed to get consent status for '"
+ << name_ << "':" << component_id);
+ return NS_ERROR_FAILURE;
+ }
+ *can_send = !!send;
+
+ return NS_OK;
+}
+
+bool NrIceMediaStream::HasStream(nr_ice_media_stream* stream) const {
+ return (stream == stream_) || (stream == old_stream_);
+}
+
+nsresult NrIceMediaStream::SendPacket(int component_id,
+ const unsigned char* data, size_t len) {
+ nr_ice_media_stream* stream = old_stream_ ? old_stream_ : stream_;
+ if (!stream) {
+ return NS_ERROR_FAILURE;
+ }
+
+ int r = nr_ice_media_stream_send(ctx_->peer(), stream, component_id,
+ const_cast<unsigned char*>(data), len);
+ if (r) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't send media on '" << name_ << "'");
+ if (r == R_WOULDBLOCK) {
+ return NS_BASE_STREAM_WOULD_BLOCK;
+ }
+
+ return NS_BASE_STREAM_OSERROR;
+ }
+
+ return NS_OK;
+}
+
+void NrIceMediaStream::Ready() {
+ // This function is called whenever a stream becomes ready, but it
+ // gets fired multiple times when a stream gets nominated repeatedly.
+ if (state_ != ICE_OPEN) {
+ MOZ_MTLOG(ML_DEBUG, "Marking stream ready '" << name_ << "'");
+ state_ = ICE_OPEN;
+ NS_DispatchToCurrentThread(NewRunnableMethod<nr_ice_media_stream*>(
+ "NrIceMediaStream::DeferredCloseOldStream", this,
+ &NrIceMediaStream::DeferredCloseOldStream, old_stream_));
+ SignalReady(this);
+ } else {
+ MOZ_MTLOG(ML_DEBUG,
+ "Stream ready callback fired again for '" << name_ << "'");
+ }
+}
+
+void NrIceMediaStream::Failed() {
+ if (state_ != ICE_CLOSED) {
+ MOZ_MTLOG(ML_DEBUG, "Marking stream failed '" << name_ << "'");
+ state_ = ICE_CLOSED;
+ // We don't need the old stream anymore.
+ NS_DispatchToCurrentThread(NewRunnableMethod<nr_ice_media_stream*>(
+ "NrIceMediaStream::DeferredCloseOldStream", this,
+ &NrIceMediaStream::DeferredCloseOldStream, old_stream_));
+ SignalFailed(this);
+ }
+}
+
+void NrIceMediaStream::Close() {
+ MOZ_MTLOG(ML_DEBUG, "Marking stream closed '" << name_ << "'");
+ state_ = ICE_CLOSED;
+
+ CloseStream(&old_stream_);
+ CloseStream(&stream_);
+ ctx_ = nullptr;
+}
+
+void NrIceMediaStream::CloseStream(nr_ice_media_stream** stream) {
+ if (*stream) {
+ int r = nr_ice_remove_media_stream(ctx_->ctx(), stream);
+ if (r) {
+ MOZ_ASSERT(false, "Failed to remove stream");
+ MOZ_MTLOG(ML_ERROR, "Failed to remove stream, error=" << r);
+ }
+ *stream = nullptr;
+ }
+}
+
+void NrIceMediaStream::DeferredCloseOldStream(const nr_ice_media_stream* old) {
+ if (old == old_stream_) {
+ CloseStream(&old_stream_);
+ }
+}
+
+nr_ice_media_stream* NrIceMediaStream::GetStreamForRemoteUfrag(
+ const std::string& aUfrag) {
+ if (aUfrag.empty()) {
+ return stream_;
+ }
+
+ nr_ice_media_stream* peer_stream = nullptr;
+
+ if (!nr_ice_peer_ctx_find_pstream(ctx_->peer(), stream_, &peer_stream) &&
+ aUfrag == peer_stream->ufrag) {
+ return stream_;
+ }
+
+ if (old_stream_ &&
+ !nr_ice_peer_ctx_find_pstream(ctx_->peer(), old_stream_, &peer_stream) &&
+ aUfrag == peer_stream->ufrag) {
+ return old_stream_;
+ }
+
+ return nullptr;
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/transport/nricemediastream.h b/dom/media/webrtc/transport/nricemediastream.h
new file mode 100644
index 0000000000..f18b09c47f
--- /dev/null
+++ b/dom/media/webrtc/transport/nricemediastream.h
@@ -0,0 +1,225 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+// Some of this code is cut-and-pasted from nICEr. Copyright is:
+
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+// This is a wrapper around the nICEr ICE stack
+#ifndef nricemediastream_h__
+#define nricemediastream_h__
+
+#include <string>
+#include <vector>
+
+#include "sigslot.h"
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "nscore.h"
+
+#include "m_cpp_utils.h"
+
+namespace mozilla {
+
+typedef struct nr_ice_ctx_ nr_ice_ctx;
+typedef struct nr_ice_peer_ctx_ nr_ice_peer_ctx;
+typedef struct nr_ice_media_stream_ nr_ice_media_stream;
+
+class NrIceCtx;
+
+struct NrIceAddr {
+ std::string host;
+ uint16_t port;
+ std::string transport;
+};
+
+/* A summary of a candidate, for use in asking which candidate
+ pair is active */
+struct NrIceCandidate {
+ enum Type { ICE_HOST, ICE_SERVER_REFLEXIVE, ICE_PEER_REFLEXIVE, ICE_RELAYED };
+
+ enum TcpType { ICE_NONE, ICE_ACTIVE, ICE_PASSIVE, ICE_SO };
+
+ NrIceAddr cand_addr;
+ NrIceAddr local_addr;
+ std::string mdns_addr;
+ Type type;
+ TcpType tcp_type;
+ std::string codeword;
+ std::string label;
+ bool trickled;
+ uint32_t priority;
+ bool is_proxied = false;
+};
+
+struct NrIceCandidatePair {
+ enum State {
+ STATE_FROZEN,
+ STATE_WAITING,
+ STATE_IN_PROGRESS,
+ STATE_FAILED,
+ STATE_SUCCEEDED,
+ STATE_CANCELLED
+ };
+
+ State state;
+ uint64_t priority;
+ // Set regardless of who nominated it. Does not necessarily mean that it is
+ // ready to be selected (ie; nominated by peer, but our check has not
+ // succeeded yet.) Note: since this implementation uses aggressive nomination,
+ // when we are the controlling agent, this will always be set if the pair is
+ // in STATE_SUCCEEDED.
+ bool nominated;
+ bool writable;
+ bool readable;
+ // Set if this candidate pair has been selected. Note: Since we are using
+ // aggressive nomination, this could change frequently as ICE runs.
+ bool selected;
+ NrIceCandidate local;
+ NrIceCandidate remote;
+ // TODO(bcampen@mozilla.com): Is it important to put the foundation in here?
+ std::string codeword;
+ uint64_t component_id;
+
+ // for RTCIceCandidatePairStats
+ uint64_t bytes_sent;
+ uint64_t bytes_recvd;
+ uint64_t ms_since_last_send;
+ uint64_t ms_since_last_recv;
+};
+
+class NrIceMediaStream {
+ public:
+ NrIceMediaStream(NrIceCtx* ctx, const std::string& id,
+ const std::string& name, size_t components);
+
+ nsresult SetIceCredentials(const std::string& ufrag, const std::string& pwd);
+ nsresult ConnectToPeer(const std::string& ufrag, const std::string& pwd,
+ const std::vector<std::string>& peer_attrs);
+ enum State { ICE_CONNECTING, ICE_OPEN, ICE_CLOSED };
+
+ State state() const { return state_; }
+
+ // The name of the stream
+ const std::string& name() const { return name_; }
+
+ // Get all the ICE attributes; used for testing
+ std::vector<std::string> GetAttributes() const;
+
+ nsresult GetLocalCandidates(std::vector<NrIceCandidate>* candidates) const;
+ nsresult GetRemoteCandidates(std::vector<NrIceCandidate>* candidates) const;
+
+ // Get all candidate pairs, whether in the check list or triggered check
+ // queue, in priority order. |out_pairs| is cleared before being filled.
+ nsresult GetCandidatePairs(std::vector<NrIceCandidatePair>* out_pairs) const;
+
+ nsresult GetDefaultCandidate(int component, NrIceCandidate* candidate) const;
+
+ // Parse trickle ICE candidate
+ nsresult ParseTrickleCandidate(const std::string& candidate,
+ const std::string& ufrag,
+ const std::string& mdns_addr);
+
+ // Disable a component
+ nsresult DisableComponent(int component);
+
+ // Get the candidate pair currently active. It's the
+ // caller's responsibility to free these.
+ nsresult GetActivePair(int component, UniquePtr<NrIceCandidate>* local,
+ UniquePtr<NrIceCandidate>* remote);
+
+ // Get the current ICE consent send status plus the timeval of the last
+ // consent update time.
+ nsresult GetConsentStatus(int component, bool* can_send, struct timeval* ts);
+
+ // The number of components
+ size_t components() const { return components_; }
+
+ bool HasStream(nr_ice_media_stream* stream) const;
+ // Signals to indicate events. API users can (and should)
+ // register for these.
+
+ // Send a packet
+ nsresult SendPacket(int component_id, const unsigned char* data, size_t len);
+
+ // Set your state to ready. Called by the NrIceCtx;
+ void Ready();
+ void Failed();
+
+ // Close the stream. Called by the NrIceCtx.
+ // Different from the destructor because other people
+ // might be holding RefPtrs but we want those writes to fail once
+ // the context has been destroyed.
+ void Close();
+
+ // So the receiver of SignalCandidate can determine which transport
+ // the candidate belongs to.
+ const std::string& GetId() const { return id_; }
+
+ sigslot::signal5<NrIceMediaStream*, const std::string&, const std::string&,
+ const std::string&, const std::string&>
+ SignalCandidate; // A new ICE candidate:
+
+ sigslot::signal1<NrIceMediaStream*> SignalReady; // Candidate pair ready.
+ sigslot::signal1<NrIceMediaStream*> SignalFailed; // Candidate pair failed.
+ sigslot::signal4<NrIceMediaStream*, int, const unsigned char*, int>
+ SignalPacketReceived; // Incoming packet
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(NrIceMediaStream)
+
+ private:
+ ~NrIceMediaStream();
+
+ DISALLOW_COPY_ASSIGN(NrIceMediaStream);
+
+ void CloseStream(nr_ice_media_stream** stream);
+ void DeferredCloseOldStream(const nr_ice_media_stream* old);
+ nr_ice_media_stream* GetStreamForRemoteUfrag(const std::string& ufrag);
+
+ State state_;
+ RefPtr<NrIceCtx> ctx_;
+ const std::string name_;
+ const size_t components_;
+ nr_ice_media_stream* stream_;
+ nr_ice_media_stream* old_stream_;
+ const std::string id_;
+};
+
+} // namespace mozilla
+#endif
diff --git a/dom/media/webrtc/transport/nriceresolver.cpp b/dom/media/webrtc/transport/nriceresolver.cpp
new file mode 100644
index 0000000000..e1737e0e65
--- /dev/null
+++ b/dom/media/webrtc/transport/nriceresolver.cpp
@@ -0,0 +1,234 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original authors: jib@mozilla.com, ekr@rtfm.com
+
+// Some of this code is cut-and-pasted from nICEr. Copyright is:
+
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include "logging.h"
+#include "nspr.h"
+#include "prnetdb.h"
+
+#include "mozilla/Assertions.h"
+
+extern "C" {
+#include "nr_api.h"
+#include "async_timer.h"
+#include "nr_resolver.h"
+#include "transport_addr.h"
+}
+
+#include "mozilla/net/DNS.h" // TODO(jib@mozilla.com) down here because bug 848578
+#include "nsThreadUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsIDNSService.h"
+#include "nsIDNSListener.h"
+#include "nsIDNSRecord.h"
+#include "nsNetCID.h"
+#include "nsCOMPtr.h"
+#include "nriceresolver.h"
+#include "nr_socket_prsock.h"
+#include "transport/runnable_utils.h"
+
+namespace mozilla {
+
+MOZ_MTLOG_MODULE("mtransport")
+
+NrIceResolver::NrIceResolver()
+ : vtbl_(new nr_resolver_vtbl())
+#ifdef DEBUG
+ ,
+ allocated_resolvers_(0)
+#endif
+{
+ vtbl_->destroy = &NrIceResolver::destroy;
+ vtbl_->resolve = &NrIceResolver::resolve;
+ vtbl_->cancel = &NrIceResolver::cancel;
+}
+
+NrIceResolver::~NrIceResolver() {
+ MOZ_ASSERT(!allocated_resolvers_);
+ delete vtbl_;
+}
+
+nsresult NrIceResolver::Init() {
+ nsresult rv;
+
+ sts_thread_ = do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ dns_ = do_GetService(NS_DNSSERVICE_CONTRACTID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ MOZ_MTLOG(ML_ERROR, "Could not acquire DNS service");
+ }
+ return rv;
+}
+
+nr_resolver* NrIceResolver::AllocateResolver() {
+ nr_resolver* resolver;
+
+ int r = nr_resolver_create_int((void*)this, vtbl_, &resolver);
+ MOZ_ASSERT(!r);
+ if (r) {
+ MOZ_MTLOG(ML_ERROR, "nr_resolver_create_int failed");
+ return nullptr;
+ }
+ // We must be available to allocators until they all call DestroyResolver,
+ // because allocators may (and do) outlive the originator of NrIceResolver.
+ AddRef();
+#ifdef DEBUG
+ ++allocated_resolvers_;
+#endif
+ return resolver;
+}
+
+void NrIceResolver::DestroyResolver() {
+#ifdef DEBUG
+ --allocated_resolvers_;
+#endif
+ // Undoes Addref in AllocateResolver so the NrIceResolver can be freed.
+ Release();
+}
+
+int NrIceResolver::destroy(void** objp) {
+ if (!objp || !*objp) return 0;
+ NrIceResolver* resolver = static_cast<NrIceResolver*>(*objp);
+ *objp = nullptr;
+ resolver->DestroyResolver();
+ return 0;
+}
+
+int NrIceResolver::resolve(void* obj, nr_resolver_resource* resource,
+ int (*cb)(void* cb_arg, nr_transport_addr* addr),
+ void* cb_arg, void** handle) {
+ MOZ_ASSERT(obj);
+ return static_cast<NrIceResolver*>(obj)->resolve(resource, cb, cb_arg,
+ handle);
+}
+
+int NrIceResolver::resolve(nr_resolver_resource* resource,
+ int (*cb)(void* cb_arg, nr_transport_addr* addr),
+ void* cb_arg, void** handle) {
+ int _status;
+ MOZ_ASSERT(allocated_resolvers_ > 0);
+ ASSERT_ON_THREAD(sts_thread_);
+ RefPtr<PendingResolution> pr;
+ nsIDNSService::DNSFlags resolve_flags = nsIDNSService::RESOLVE_DEFAULT_FLAGS;
+ OriginAttributes attrs;
+
+ if (resource->transport_protocol != IPPROTO_UDP &&
+ resource->transport_protocol != IPPROTO_TCP) {
+ MOZ_MTLOG(ML_ERROR, "Only UDP and TCP are supported.");
+ ABORT(R_NOT_FOUND);
+ }
+ pr = new PendingResolution(
+ sts_thread_, resource->port ? resource->port : 3478,
+ resource->transport_protocol ? resource->transport_protocol : IPPROTO_UDP,
+ cb, cb_arg);
+
+ switch (resource->address_family) {
+ case AF_INET:
+ resolve_flags = nsIDNSService::RESOLVE_DISABLE_IPV6;
+ break;
+ case AF_INET6:
+ resolve_flags = nsIDNSService::RESOLVE_DISABLE_IPV4;
+ break;
+ default:
+ ABORT(R_BAD_ARGS);
+ }
+
+ if (NS_FAILED(dns_->AsyncResolveNative(
+ nsAutoCString(resource->domain_name),
+ nsIDNSService::RESOLVE_TYPE_DEFAULT, resolve_flags, nullptr, pr,
+ sts_thread_, attrs, getter_AddRefs(pr->request_)))) {
+ MOZ_MTLOG(ML_ERROR, "AsyncResolve failed.");
+ ABORT(R_NOT_FOUND);
+ }
+ // Because the C API offers no "finished" method to release the handle we
+ // return, we cannot return the request we got from AsyncResolve directly.
+ //
+ // Instead, we return an addref'ed reference to PendingResolution itself,
+ // which in turn holds the request and coordinates between cancel and
+ // OnLookupComplete to release it only once.
+ pr.forget(handle);
+
+ _status = 0;
+abort:
+ return _status;
+}
+
+nsresult NrIceResolver::PendingResolution::OnLookupComplete(
+ nsICancelable* request, nsIDNSRecord* aRecord, nsresult status) {
+ ASSERT_ON_THREAD(thread_);
+ // First check if we've been canceled. This is single-threaded on the STS
+ // thread, but cancel() cannot guarantee this event isn't on the queue.
+ if (request_) {
+ nr_transport_addr* cb_addr = nullptr;
+ nr_transport_addr ta;
+ // TODO(jib@mozilla.com): Revisit when we do TURN.
+ if (NS_SUCCEEDED(status)) {
+ net::NetAddr na;
+ nsCOMPtr<nsIDNSAddrRecord> record = do_QueryInterface(aRecord);
+ if (record && NS_SUCCEEDED(record->GetNextAddr(port_, &na))) {
+ MOZ_ALWAYS_TRUE(nr_netaddr_to_transport_addr(&na, &ta, transport_) ==
+ 0);
+ cb_addr = &ta;
+ }
+ }
+ cb_(cb_arg_, cb_addr);
+ request_ = nullptr;
+ Release();
+ }
+ return NS_OK;
+}
+
+int NrIceResolver::cancel(void* obj, void* handle) {
+ MOZ_ALWAYS_TRUE(obj);
+ MOZ_ASSERT(handle);
+ ASSERT_ON_THREAD(static_cast<NrIceResolver*>(obj)->sts_thread_);
+ return static_cast<PendingResolution*>(handle)->cancel();
+}
+
+int NrIceResolver::PendingResolution::cancel() {
+ request_->Cancel(NS_ERROR_ABORT);
+ request_ = nullptr;
+ Release();
+ return 0;
+}
+
+NS_IMPL_ISUPPORTS(NrIceResolver::PendingResolution, nsIDNSListener);
+} // End of namespace mozilla
diff --git a/dom/media/webrtc/transport/nriceresolver.h b/dom/media/webrtc/transport/nriceresolver.h
new file mode 100644
index 0000000000..7399c78050
--- /dev/null
+++ b/dom/media/webrtc/transport/nriceresolver.h
@@ -0,0 +1,119 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original authors: jib@mozilla.com, ekr@rtfm.com
+
+// Some of this code is cut-and-pasted from nICEr. Copyright is:
+
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#ifndef nriceresolver_h__
+#define nriceresolver_h__
+
+#include "nsIDNSService.h"
+#include "nsIDNSListener.h"
+#include "nsICancelable.h"
+#include "nricectx.h"
+
+typedef struct nr_resolver_ nr_resolver;
+typedef struct nr_resolver_vtbl_ nr_resolver_vtbl;
+typedef struct nr_transport_addr_ nr_transport_addr;
+typedef struct nr_resolver_resource_ nr_resolver_resource;
+
+namespace mozilla {
+
+class NrIceResolver {
+ private:
+ ~NrIceResolver();
+
+ public:
+ NrIceResolver();
+
+ nsresult Init();
+ nr_resolver* AllocateResolver();
+ void DestroyResolver();
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(NrIceResolver)
+
+ int resolve(nr_resolver_resource* resource,
+ int (*cb)(void* cb_arg, nr_transport_addr* addr), void* cb_arg,
+ void** handle);
+
+ private:
+ // Implementations of vtbl functions
+ static int destroy(void** objp);
+ static int resolve(void* obj, nr_resolver_resource* resource,
+ int (*cb)(void* cb_arg, nr_transport_addr* addr),
+ void* cb_arg, void** handle);
+ static void resolve_cb(NR_SOCKET s, int how, void* cb_arg);
+ static int cancel(void* obj, void* handle);
+
+ class PendingResolution : public nsIDNSListener {
+ public:
+ PendingResolution(nsIEventTarget* thread, uint16_t port, int transport,
+ int (*cb)(void* cb_arg, nr_transport_addr* addr),
+ void* cb_arg)
+ : thread_(thread),
+ port_(port),
+ transport_(transport),
+ cb_(cb),
+ cb_arg_(cb_arg) {}
+ NS_IMETHOD OnLookupComplete(nsICancelable* request, nsIDNSRecord* record,
+ nsresult status) override;
+
+ int cancel();
+ nsCOMPtr<nsICancelable> request_;
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ private:
+ virtual ~PendingResolution() = default;
+
+ nsCOMPtr<nsIEventTarget> thread_;
+ uint16_t port_;
+ int transport_;
+ int (*cb_)(void* cb_arg, nr_transport_addr* addr);
+ void* cb_arg_;
+ };
+
+ nr_resolver_vtbl* vtbl_;
+ nsCOMPtr<nsIEventTarget> sts_thread_;
+ nsCOMPtr<nsIDNSService> dns_;
+#ifdef DEBUG
+ int allocated_resolvers_;
+#endif
+};
+
+} // End of namespace mozilla
+#endif
diff --git a/dom/media/webrtc/transport/nriceresolverfake.cpp b/dom/media/webrtc/transport/nriceresolverfake.cpp
new file mode 100644
index 0000000000..93ef37efc1
--- /dev/null
+++ b/dom/media/webrtc/transport/nriceresolverfake.cpp
@@ -0,0 +1,174 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+// Some of this code is cut-and-pasted from nICEr. Copyright is:
+
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include "prio.h"
+#include "mozilla/Assertions.h"
+
+extern "C" {
+#include "async_wait.h"
+#include "async_timer.h"
+#include "nr_resolver.h"
+#include "r_macros.h"
+#include "transport_addr.h"
+}
+
+#include "nriceresolverfake.h"
+#include "nr_socket_prsock.h"
+
+namespace mozilla {
+
+NrIceResolverFake::NrIceResolverFake()
+ : vtbl_(new nr_resolver_vtbl),
+ addrs_(),
+ delay_ms_(100),
+ allocated_resolvers_(0) {
+ vtbl_->destroy = &NrIceResolverFake::destroy;
+ vtbl_->resolve = &NrIceResolverFake::resolve;
+ vtbl_->cancel = &NrIceResolverFake::cancel;
+}
+
+NrIceResolverFake::~NrIceResolverFake() {
+ MOZ_ASSERT(allocated_resolvers_ == 0);
+ delete vtbl_;
+}
+
+nr_resolver* NrIceResolverFake::AllocateResolver() {
+ nr_resolver* resolver;
+
+ int r = nr_resolver_create_int((void*)this, vtbl_, &resolver);
+ MOZ_ASSERT(!r);
+ if (r) return nullptr;
+
+ ++allocated_resolvers_;
+
+ return resolver;
+}
+
+void NrIceResolverFake::DestroyResolver() { --allocated_resolvers_; }
+
+int NrIceResolverFake::destroy(void** objp) {
+ if (!objp || !*objp) return 0;
+
+ NrIceResolverFake* fake = static_cast<NrIceResolverFake*>(*objp);
+ *objp = nullptr;
+
+ fake->DestroyResolver();
+
+ return 0;
+}
+
+int NrIceResolverFake::resolve(void* obj, nr_resolver_resource* resource,
+ int (*cb)(void* cb_arg, nr_transport_addr* addr),
+ void* cb_arg, void** handle) {
+ int r, _status;
+
+ MOZ_ASSERT(obj);
+ NrIceResolverFake* fake = static_cast<NrIceResolverFake*>(obj);
+
+ MOZ_ASSERT(fake->allocated_resolvers_ > 0);
+
+ PendingResolution* pending = new PendingResolution(
+ fake, resource->domain_name, resource->port ? resource->port : 3478,
+ resource->transport_protocol ? resource->transport_protocol : IPPROTO_UDP,
+ resource->address_family, cb, cb_arg);
+
+ if ((r = NR_ASYNC_TIMER_SET(fake->delay_ms_, NrIceResolverFake::resolve_cb,
+ (void*)pending, &pending->timer_handle_))) {
+ delete pending;
+ ABORT(r);
+ }
+ *handle = pending;
+
+ _status = 0;
+abort:
+ return (_status);
+}
+
+void NrIceResolverFake::resolve_cb(NR_SOCKET s, int how, void* cb_arg) {
+ MOZ_ASSERT(cb_arg);
+ PendingResolution* pending = static_cast<PendingResolution*>(cb_arg);
+
+ const PRNetAddr* addr =
+ pending->resolver_->Resolve(pending->hostname_, pending->address_family_);
+
+ if (addr) {
+ nr_transport_addr transport_addr;
+
+ int r = nr_praddr_to_transport_addr(addr, &transport_addr,
+ pending->transport_, 0);
+ MOZ_ASSERT(!r);
+ if (r) goto abort;
+
+ r = nr_transport_addr_set_port(&transport_addr, pending->port_);
+ MOZ_ASSERT(!r);
+ if (r) goto abort;
+
+ /* Fill in the address string */
+ r = nr_transport_addr_fmt_addr_string(&transport_addr);
+ MOZ_ASSERT(!r);
+ if (r) goto abort;
+
+ pending->cb_(pending->cb_arg_, &transport_addr);
+ delete pending;
+ return;
+ }
+
+abort:
+ // Resolution failed.
+ pending->cb_(pending->cb_arg_, nullptr);
+
+ delete pending;
+}
+
+int NrIceResolverFake::cancel(void* obj, void* handle) {
+ MOZ_ASSERT(obj);
+ MOZ_ASSERT(static_cast<NrIceResolverFake*>(obj)->allocated_resolvers_ > 0);
+
+ PendingResolution* pending = static_cast<PendingResolution*>(handle);
+
+ NR_async_timer_cancel(pending->timer_handle_);
+ delete pending;
+
+ return (0);
+}
+
+} // End of namespace mozilla
diff --git a/dom/media/webrtc/transport/nriceresolverfake.h b/dom/media/webrtc/transport/nriceresolverfake.h
new file mode 100644
index 0000000000..e46ce603de
--- /dev/null
+++ b/dom/media/webrtc/transport/nriceresolverfake.h
@@ -0,0 +1,137 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+// Some of this code is cut-and-pasted from nICEr. Copyright is:
+
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#ifndef nriceresolverfake_h__
+#define nriceresolverfake_h__
+
+#include <map>
+#include <string>
+
+#include "csi_platform.h"
+
+typedef struct nr_resolver_ nr_resolver;
+typedef struct nr_resolver_vtbl_ nr_resolver_vtbl;
+typedef struct nr_transport_addr_ nr_transport_addr;
+typedef struct nr_resolver_resource_ nr_resolver_resource;
+
+namespace mozilla {
+
+class NrIceResolverFake {
+ public:
+ NrIceResolverFake();
+ ~NrIceResolverFake();
+
+ void SetAddr(const std::string& hostname, const PRNetAddr& addr) {
+ switch (addr.raw.family) {
+ case AF_INET:
+ addrs_[hostname] = addr;
+ break;
+ case AF_INET6:
+ addrs6_[hostname] = addr;
+ break;
+ default:
+ MOZ_CRASH();
+ }
+ }
+
+ nr_resolver* AllocateResolver();
+
+ void DestroyResolver();
+
+ private:
+ // Implementations of vtbl functions
+ static int destroy(void** objp);
+ static int resolve(void* obj, nr_resolver_resource* resource,
+ int (*cb)(void* cb_arg, nr_transport_addr* addr),
+ void* cb_arg, void** handle);
+ static void resolve_cb(NR_SOCKET s, int how, void* cb_arg);
+ static int cancel(void* obj, void* handle);
+
+ // Get an address.
+ const PRNetAddr* Resolve(const std::string& hostname, int address_family) {
+ switch (address_family) {
+ case AF_INET:
+ if (!addrs_.count(hostname)) return nullptr;
+
+ return &addrs_[hostname];
+ case AF_INET6:
+ if (!addrs6_.count(hostname)) return nullptr;
+
+ return &addrs6_[hostname];
+ default:
+ MOZ_CRASH();
+ }
+ }
+
+ struct PendingResolution {
+ PendingResolution(NrIceResolverFake* resolver, const std::string& hostname,
+ uint16_t port, int transport, int address_family,
+ int (*cb)(void* cb_arg, nr_transport_addr* addr),
+ void* cb_arg)
+ : resolver_(resolver),
+ hostname_(hostname),
+ port_(port),
+ transport_(transport),
+ address_family_(address_family),
+ cb_(cb),
+ cb_arg_(cb_arg) {}
+
+ NrIceResolverFake* resolver_;
+ std::string hostname_;
+ uint16_t port_;
+ int transport_;
+ int address_family_;
+ int (*cb_)(void* cb_arg, nr_transport_addr* addr);
+ void* cb_arg_;
+ void* timer_handle_;
+ };
+
+ nr_resolver_vtbl* vtbl_;
+ std::map<std::string, PRNetAddr> addrs_;
+ std::map<std::string, PRNetAddr> addrs6_;
+ uint32_t delay_ms_;
+ int allocated_resolvers_;
+};
+
+} // End of namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/transport/nricestunaddr.cpp b/dom/media/webrtc/transport/nricestunaddr.cpp
new file mode 100644
index 0000000000..d1e8d3ac52
--- /dev/null
+++ b/dom/media/webrtc/transport/nricestunaddr.cpp
@@ -0,0 +1,93 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "logging.h"
+
+// nICEr includes
+extern "C" {
+#include "nr_api.h"
+#include "local_addr.h"
+}
+
+// Local includes
+#include "nricestunaddr.h"
+
+namespace mozilla {
+
+MOZ_MTLOG_MODULE("mtransport")
+
+NrIceStunAddr::NrIceStunAddr() : localAddr_(new nr_local_addr) {
+ memset(localAddr_, 0, sizeof(nr_local_addr));
+}
+
+NrIceStunAddr::NrIceStunAddr(const nr_local_addr* addr)
+ : localAddr_(new nr_local_addr) {
+ nr_local_addr_copy(localAddr_, const_cast<nr_local_addr*>(addr));
+}
+
+NrIceStunAddr::NrIceStunAddr(const NrIceStunAddr& rhs)
+ : localAddr_(new nr_local_addr) {
+ nr_local_addr_copy(localAddr_, const_cast<nr_local_addr*>(rhs.localAddr_));
+}
+
+NrIceStunAddr::~NrIceStunAddr() { delete localAddr_; }
+
+size_t NrIceStunAddr::SerializationBufferSize() const {
+ return sizeof(nr_local_addr);
+}
+
+nsresult NrIceStunAddr::Serialize(char* buffer, size_t buffer_size) const {
+ if (buffer_size != sizeof(nr_local_addr)) {
+ MOZ_MTLOG(ML_ERROR,
+ "Failed trying to serialize NrIceStunAddr, "
+ "input buffer length ("
+ << buffer_size << ") does not match required length ("
+ << sizeof(nr_local_addr) << ")");
+ MOZ_ASSERT(false, "Failed to serialize NrIceStunAddr, bad buffer size");
+ return NS_ERROR_FAILURE;
+ }
+
+ nr_local_addr* toAddr = (nr_local_addr*)buffer;
+ if (nr_local_addr_copy(toAddr, localAddr_)) {
+ MOZ_MTLOG(ML_ERROR,
+ "Failed trying to serialize NrIceStunAddr, "
+ "could not copy nr_local_addr.");
+ MOZ_ASSERT(false,
+ "Failed to serialize NrIceStunAddr, nr_local_addr_copy failed");
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+nsresult NrIceStunAddr::Deserialize(const char* buffer, size_t buffer_size) {
+ if (buffer_size != sizeof(nr_local_addr)) {
+ MOZ_MTLOG(ML_ERROR,
+ "Failed trying to deserialize NrIceStunAddr, "
+ "input buffer length ("
+ << buffer_size << ") does not match required length ("
+ << sizeof(nr_local_addr) << ")");
+ MOZ_ASSERT(false, "Failed to deserialize NrIceStunAddr, bad buffer size");
+ return NS_ERROR_FAILURE;
+ }
+
+ nr_local_addr* from_addr =
+ const_cast<nr_local_addr*>((const nr_local_addr*)buffer);
+
+ // At this point, from_addr->addr.addr is invalid (null), but will
+ // be fixed by nr_local_addr_copy.
+ if (nr_local_addr_copy(localAddr_, from_addr)) {
+ MOZ_MTLOG(ML_ERROR,
+ "Failed trying to deserialize NrIceStunAddr, "
+ "could not copy nr_local_addr.");
+ MOZ_ASSERT(
+ false,
+ "Failed to deserialize NrIceStunAddr, nr_local_addr_copy failed");
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/transport/nricestunaddr.h b/dom/media/webrtc/transport/nricestunaddr.h
new file mode 100644
index 0000000000..8ebcdd6fe7
--- /dev/null
+++ b/dom/media/webrtc/transport/nricestunaddr.h
@@ -0,0 +1,36 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nricestunaddr_h__
+#define nricestunaddr_h__
+
+#include "nsError.h" // for nsresult
+
+typedef struct nr_local_addr_ nr_local_addr;
+
+namespace mozilla {
+
+class NrIceStunAddr {
+ public:
+ NrIceStunAddr(); // needed for IPC deserialization
+ explicit NrIceStunAddr(const nr_local_addr* addr);
+ NrIceStunAddr(const NrIceStunAddr& rhs);
+
+ ~NrIceStunAddr();
+
+ const nr_local_addr& localAddr() const { return *localAddr_; }
+
+ // serialization/deserialization helper functions for use
+ // in dom/media/webrtc/transport/ipc/NrIceStunAddrMessagUtils.h
+ size_t SerializationBufferSize() const;
+ nsresult Serialize(char* buffer, size_t buffer_size) const;
+ nsresult Deserialize(const char* buffer, size_t buffer_size);
+
+ private:
+ nr_local_addr* localAddr_;
+};
+
+} // namespace mozilla
+
+#endif // nricestunaddr_h__
diff --git a/dom/media/webrtc/transport/nrinterfaceprioritizer.cpp b/dom/media/webrtc/transport/nrinterfaceprioritizer.cpp
new file mode 100644
index 0000000000..f022f8c29a
--- /dev/null
+++ b/dom/media/webrtc/transport/nrinterfaceprioritizer.cpp
@@ -0,0 +1,258 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include <algorithm>
+#include <map>
+#include <set>
+#include <string>
+#include <vector>
+#include "logging.h"
+#include "nr_api.h"
+#include "nrinterfaceprioritizer.h"
+
+MOZ_MTLOG_MODULE("mtransport")
+
+namespace {
+
+class LocalAddress {
+ public:
+ LocalAddress()
+ : ifname_(),
+ addr_(),
+ key_(),
+ is_vpn_(-1),
+ estimated_speed_(-1),
+ type_preference_(-1),
+ ip_version_(-1) {}
+
+ bool Init(const nr_local_addr& local_addr) {
+ ifname_ = local_addr.addr.ifname;
+
+ char buf[MAXIFNAME + 47];
+ int r = nr_transport_addr_fmt_ifname_addr_string(&local_addr.addr, buf,
+ sizeof(buf));
+ if (r) {
+ MOZ_MTLOG(ML_ERROR, "Error formatting interface key.");
+ return false;
+ }
+ key_ = buf;
+
+ r = nr_transport_addr_get_addrstring(&local_addr.addr, buf, sizeof(buf));
+ if (r) {
+ MOZ_MTLOG(ML_ERROR, "Error formatting address string.");
+ return false;
+ }
+ addr_ = buf;
+
+ is_vpn_ = (local_addr.interface.type & NR_INTERFACE_TYPE_VPN) != 0 ? 1 : 0;
+ estimated_speed_ = local_addr.interface.estimated_speed;
+ type_preference_ = GetNetworkTypePreference(local_addr.interface.type);
+ ip_version_ = local_addr.addr.ip_version;
+ return true;
+ }
+
+ bool operator<(const LocalAddress& rhs) const {
+ // Interface that is "less" here is preferred.
+ // If type preferences are different, we should simply sort by
+ // |type_preference_|.
+ if (type_preference_ != rhs.type_preference_) {
+ return type_preference_ < rhs.type_preference_;
+ }
+
+ // If type preferences are the same, the next thing we use to sort is vpn.
+ // If two LocalAddress are different in |is_vpn_|, the LocalAddress that is
+ // not in vpn gets priority.
+ if (is_vpn_ != rhs.is_vpn_) {
+ return is_vpn_ < rhs.is_vpn_;
+ }
+
+ // Compare estimated speed.
+ if (estimated_speed_ != rhs.estimated_speed_) {
+ return estimated_speed_ > rhs.estimated_speed_;
+ }
+
+ // See if our hard-coded pref list helps us.
+ auto thisindex = std::find(interface_preference_list().begin(),
+ interface_preference_list().end(), ifname_);
+ auto rhsindex = std::find(interface_preference_list().begin(),
+ interface_preference_list().end(), rhs.ifname_);
+ if (thisindex != rhsindex) {
+ return thisindex < rhsindex;
+ }
+
+ // Prefer IPV6 over IPV4
+ if (ip_version_ != rhs.ip_version_) {
+ return ip_version_ > rhs.ip_version_;
+ }
+
+ // Now we start getting into arbitrary stuff
+ if (ifname_ != rhs.ifname_) {
+ return ifname_ < rhs.ifname_;
+ }
+
+ return addr_ < rhs.addr_;
+ }
+
+ const std::string& GetKey() const { return key_; }
+
+ private:
+ // Getting the preference corresponding to a type. Getting lower number here
+ // means the type of network is preferred.
+ static inline int GetNetworkTypePreference(int type) {
+ if (type & NR_INTERFACE_TYPE_WIRED) {
+ return 1;
+ }
+ if (type & NR_INTERFACE_TYPE_WIFI) {
+ return 2;
+ }
+ if (type & NR_INTERFACE_TYPE_MOBILE) {
+ return 3;
+ }
+ if (type & NR_INTERFACE_TYPE_TEREDO) {
+ // Teredo gets penalty because it's IP relayed
+ return 5;
+ }
+ return 4;
+ }
+
+ // TODO(bug 895790): Once we can get useful interface properties on Darwin,
+ // we should remove this stuff.
+ static const std::vector<std::string>& interface_preference_list() {
+ static std::vector<std::string> list(build_interface_preference_list());
+ return list;
+ }
+
+ static std::vector<std::string> build_interface_preference_list() {
+ std::vector<std::string> result;
+ result.push_back("rl0");
+ result.push_back("wi0");
+ result.push_back("en0");
+ result.push_back("enp2s0");
+ result.push_back("enp3s0");
+ result.push_back("en1");
+ result.push_back("en2");
+ result.push_back("en3");
+ result.push_back("eth0");
+ result.push_back("eth1");
+ result.push_back("eth2");
+ result.push_back("em1");
+ result.push_back("em0");
+ result.push_back("ppp");
+ result.push_back("ppp0");
+ result.push_back("vmnet1");
+ result.push_back("vmnet0");
+ result.push_back("vmnet3");
+ result.push_back("vmnet4");
+ result.push_back("vmnet5");
+ result.push_back("vmnet6");
+ result.push_back("vmnet7");
+ result.push_back("vmnet8");
+ result.push_back("virbr0");
+ result.push_back("wlan0");
+ result.push_back("lo0");
+ return result;
+ }
+
+ std::string ifname_;
+ std::string addr_;
+ std::string key_;
+ int is_vpn_;
+ int estimated_speed_;
+ int type_preference_;
+ int ip_version_;
+};
+
+class InterfacePrioritizer {
+ public:
+ InterfacePrioritizer() : local_addrs_(), preference_map_(), sorted_(false) {}
+
+ int add(const nr_local_addr* iface) {
+ LocalAddress addr;
+ if (!addr.Init(*iface)) {
+ return R_FAILED;
+ }
+ std::pair<std::set<LocalAddress>::iterator, bool> r =
+ local_addrs_.insert(addr);
+ if (!r.second) {
+ return R_ALREADY; // This address is already in the set.
+ }
+ sorted_ = false;
+ return 0;
+ }
+
+ int sort() {
+ UCHAR tmp_pref = 127;
+ preference_map_.clear();
+ for (const auto& local_addr : local_addrs_) {
+ if (tmp_pref == 0) {
+ return R_FAILED;
+ }
+ preference_map_.insert(make_pair(local_addr.GetKey(), tmp_pref--));
+ }
+ sorted_ = true;
+ return 0;
+ }
+
+ int getPreference(const char* key, UCHAR* pref) {
+ if (!sorted_) {
+ return R_FAILED;
+ }
+ std::map<std::string, UCHAR>::iterator i = preference_map_.find(key);
+ if (i == preference_map_.end()) {
+ return R_NOT_FOUND;
+ }
+ *pref = i->second;
+ return 0;
+ }
+
+ private:
+ std::set<LocalAddress> local_addrs_;
+ std::map<std::string, UCHAR> preference_map_;
+ bool sorted_;
+};
+
+} // anonymous namespace
+
+static int add_interface(void* obj, nr_local_addr* iface) {
+ InterfacePrioritizer* ip = static_cast<InterfacePrioritizer*>(obj);
+ return ip->add(iface);
+}
+
+static int get_priority(void* obj, const char* key, UCHAR* pref) {
+ InterfacePrioritizer* ip = static_cast<InterfacePrioritizer*>(obj);
+ return ip->getPreference(key, pref);
+}
+
+static int sort_preference(void* obj) {
+ InterfacePrioritizer* ip = static_cast<InterfacePrioritizer*>(obj);
+ return ip->sort();
+}
+
+static int destroy(void** objp) {
+ if (!objp || !*objp) {
+ return 0;
+ }
+
+ InterfacePrioritizer* ip = static_cast<InterfacePrioritizer*>(*objp);
+ *objp = nullptr;
+ delete ip;
+
+ return 0;
+}
+
+static nr_interface_prioritizer_vtbl priorizer_vtbl = {
+ add_interface, get_priority, sort_preference, destroy};
+
+namespace mozilla {
+
+nr_interface_prioritizer* CreateInterfacePrioritizer() {
+ nr_interface_prioritizer* ip;
+ int r = nr_interface_prioritizer_create_int(new InterfacePrioritizer(),
+ &priorizer_vtbl, &ip);
+ if (r != 0) {
+ return nullptr;
+ }
+ return ip;
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/transport/nrinterfaceprioritizer.h b/dom/media/webrtc/transport/nrinterfaceprioritizer.h
new file mode 100644
index 0000000000..051b586445
--- /dev/null
+++ b/dom/media/webrtc/transport/nrinterfaceprioritizer.h
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef nrinterfacepriority_h__
+#define nrinterfacepriority_h__
+
+extern "C" {
+#include "nr_interface_prioritizer.h"
+}
+
+namespace mozilla {
+
+nr_interface_prioritizer* CreateInterfacePrioritizer();
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/transport/rlogconnector.cpp b/dom/media/webrtc/transport/rlogconnector.cpp
new file mode 100644
index 0000000000..bdb58aac56
--- /dev/null
+++ b/dom/media/webrtc/transport/rlogconnector.cpp
@@ -0,0 +1,186 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* Original author: bcampen@mozilla.com */
+
+#include "rlogconnector.h"
+
+#include <cstdarg>
+#include <deque>
+#include <string>
+#include <utility> // Pinch hitting for <utility> and std::move
+#include <vector>
+
+#include "logging.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/Sprintf.h"
+
+extern "C" {
+#include <csi_platform.h>
+#include "r_log.h"
+#include "registry.h"
+}
+
+/* Matches r_dest_vlog type defined in r_log.h */
+static int ringbuffer_vlog(int facility, int level, const char* format,
+ va_list ap) {
+ if (mozilla::RLogConnector::GetInstance()->ShouldLog(level)) {
+ // I could be evil and printf right into a std::string, but unless this
+ // shows up in profiling, it is not worth doing.
+ char temp[4096];
+ VsprintfLiteral(temp, format, ap);
+
+ mozilla::RLogConnector::GetInstance()->Log(level, std::string(temp));
+ }
+ return 0;
+}
+
+static mozilla::LogLevel rLogLvlToMozLogLvl(int level) {
+ switch (level) {
+ case LOG_EMERG:
+ case LOG_ALERT:
+ case LOG_CRIT:
+ case LOG_ERR:
+ return mozilla::LogLevel::Error;
+ case LOG_WARNING:
+ return mozilla::LogLevel::Warning;
+ case LOG_NOTICE:
+ return mozilla::LogLevel::Info;
+ case LOG_INFO:
+ return mozilla::LogLevel::Debug;
+ case LOG_DEBUG:
+ default:
+ return mozilla::LogLevel::Verbose;
+ }
+}
+
+MOZ_MTLOG_MODULE("nicer");
+
+namespace mozilla {
+
+RLogConnector* RLogConnector::instance;
+
+RLogConnector::RLogConnector()
+ : log_limit_(4096), mutex_("RLogConnector::mutex_"), disableCount_(0) {}
+
+RLogConnector::~RLogConnector() = default;
+
+void RLogConnector::SetLogLimit(uint32_t new_limit) {
+ OffTheBooksMutexAutoLock lock(mutex_);
+ log_limit_ = new_limit;
+ RemoveOld();
+}
+
+bool RLogConnector::ShouldLog(int level) const {
+ return level <= LOG_INFO ||
+ MOZ_LOG_TEST(getLogModule(), rLogLvlToMozLogLvl(level));
+}
+
+void RLogConnector::Log(int level, std::string&& log) {
+ MOZ_MTLOG(rLogLvlToMozLogLvl(level), log);
+ OffTheBooksMutexAutoLock lock(mutex_);
+ if (disableCount_ == 0) {
+ AddMsg(std::move(log));
+ }
+}
+
+void RLogConnector::AddMsg(std::string&& msg) {
+ log_messages_.push_front(std::move(msg));
+ RemoveOld();
+}
+
+inline void RLogConnector::RemoveOld() {
+ if (log_messages_.size() > log_limit_) {
+ log_messages_.resize(log_limit_);
+ }
+}
+
+RLogConnector* RLogConnector::CreateInstance() {
+ if (!instance) {
+ instance = new RLogConnector;
+ NR_reg_init(NR_REG_MODE_LOCAL);
+ r_log_set_extra_destination(LOG_DEBUG, &ringbuffer_vlog);
+ }
+ return instance;
+}
+
+RLogConnector* RLogConnector::GetInstance() { return instance; }
+
+void RLogConnector::DestroyInstance() {
+ // First param is ignored when passing null
+ r_log_set_extra_destination(LOG_DEBUG, nullptr);
+ delete instance;
+ instance = nullptr;
+}
+
+// As long as at least one PeerConnection exists in a Private Window rlog
+// messages will not be saved in the RLogConnector. This is necessary because
+// the log_messages buffer is shared across all instances of
+// PeerConnectionImpls. There is no way with the current structure of r_log to
+// run separate logs.
+
+void RLogConnector::EnterPrivateMode() {
+ OffTheBooksMutexAutoLock lock(mutex_);
+ ++disableCount_;
+ MOZ_ASSERT(disableCount_ != 0);
+
+ if (disableCount_ == 1) {
+ AddMsg("LOGGING SUSPENDED: a connection is active in a Private Window ***");
+ }
+}
+
+void RLogConnector::ExitPrivateMode() {
+ OffTheBooksMutexAutoLock lock(mutex_);
+ MOZ_ASSERT(disableCount_ != 0);
+
+ if (--disableCount_ == 0) {
+ AddMsg(
+ "LOGGING RESUMED: no connections are active in a Private Window ***");
+ }
+}
+
+void RLogConnector::Clear() {
+ OffTheBooksMutexAutoLock lock(mutex_);
+ log_messages_.clear();
+}
+
+void RLogConnector::Filter(const std::string& substring, uint32_t limit,
+ std::deque<std::string>* matching_logs) {
+ std::vector<std::string> substrings;
+ substrings.push_back(substring);
+ FilterAny(substrings, limit, matching_logs);
+}
+
+inline bool AnySubstringMatches(const std::vector<std::string>& substrings,
+ const std::string& string) {
+ for (auto sub = substrings.begin(); sub != substrings.end(); ++sub) {
+ if (string.find(*sub) != std::string::npos) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void RLogConnector::FilterAny(const std::vector<std::string>& substrings,
+ uint32_t limit,
+ std::deque<std::string>* matching_logs) {
+ OffTheBooksMutexAutoLock lock(mutex_);
+ if (limit == 0) {
+ // At a max, all of the log messages.
+ limit = log_limit_;
+ }
+
+ for (auto log = log_messages_.begin();
+ log != log_messages_.end() && matching_logs->size() < limit; ++log) {
+ if (AnySubstringMatches(substrings, *log)) {
+ matching_logs->push_front(*log);
+ }
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/transport/rlogconnector.h b/dom/media/webrtc/transport/rlogconnector.h
new file mode 100644
index 0000000000..8236eb2ab3
--- /dev/null
+++ b/dom/media/webrtc/transport/rlogconnector.h
@@ -0,0 +1,127 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Some of this code is cut-and-pasted from nICEr. Copyright is:
+
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* Original author: bcampen@mozilla.com */
+
+/*
+ This file defines an r_dest_vlog that can be used to accumulate log messages
+ for later inspection/filtering. The intent is to use this for interactive
+ debug purposes on an about:webrtc page or similar.
+*/
+
+#ifndef rlogconnector_h__
+#define rlogconnector_h__
+
+#include <stdint.h>
+
+#include <deque>
+#include <string>
+#include <vector>
+
+#include "mozilla/Mutex.h"
+
+#include "m_cpp_utils.h"
+
+namespace mozilla {
+
+class RLogConnector {
+ public:
+ /*
+ NB: These are not threadsafe, nor are they safe to call during static
+ init/deinit.
+ */
+ static RLogConnector* CreateInstance();
+ static RLogConnector* GetInstance();
+ static void DestroyInstance();
+
+ /*
+ Retrieves log statements that match a given substring, subject to a
+ limit. |matching_logs| will be filled in chronological order (front()
+ is oldest, back() is newest). |limit| == 0 will be interpreted as no
+ limit.
+ */
+ void Filter(const std::string& substring, uint32_t limit,
+ std::deque<std::string>* matching_logs);
+
+ void FilterAny(const std::vector<std::string>& substrings, uint32_t limit,
+ std::deque<std::string>* matching_logs);
+
+ inline void GetAny(uint32_t limit, std::deque<std::string>* matching_logs) {
+ Filter("", limit, matching_logs);
+ }
+
+ void SetLogLimit(uint32_t new_limit);
+ bool ShouldLog(int level) const;
+ void Log(int level, std::string&& log);
+ void Clear();
+
+ // Methods to signal when a PeerConnection exists in a Private Window.
+ void EnterPrivateMode();
+ void ExitPrivateMode();
+
+ private:
+ RLogConnector();
+ ~RLogConnector();
+ void RemoveOld();
+ void AddMsg(std::string&& msg);
+
+ static RLogConnector* instance;
+
+ /*
+ * Might be worthwhile making this a circular buffer, but I think it is
+ * preferable to take up as little space as possible if no logging is
+ * happening/the ringbuffer is not being used.
+ */
+ std::deque<std::string> log_messages_;
+ /* Max size of log buffer (should we use time-depth instead/also?) */
+ uint32_t log_limit_;
+ OffTheBooksMutex mutex_;
+ uint32_t disableCount_;
+
+ DISALLOW_COPY_ASSIGN(RLogConnector);
+}; // class RLogConnector
+
+} // namespace mozilla
+
+#endif // rlogconnector_h__
diff --git a/dom/media/webrtc/transport/runnable_utils.h b/dom/media/webrtc/transport/runnable_utils.h
new file mode 100644
index 0000000000..c7248f4f55
--- /dev/null
+++ b/dom/media/webrtc/transport/runnable_utils.h
@@ -0,0 +1,222 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+#ifndef runnable_utils_h__
+#define runnable_utils_h__
+
+#include <utility>
+
+#include "mozilla/RefPtr.h"
+#include "nsThreadUtils.h"
+#include <functional>
+#include <tuple>
+#include <type_traits>
+
+// Abstract base class for all of our templates
+namespace mozilla {
+
+namespace detail {
+
+enum RunnableResult { NoResult, ReturnsResult };
+
+static inline nsresult RunOnThreadInternal(nsIEventTarget* thread,
+ nsIRunnable* runnable,
+ uint32_t flags) {
+ return thread->Dispatch(runnable, flags);
+}
+
+template <RunnableResult result>
+class runnable_args_base : public Runnable {
+ public:
+ runnable_args_base() : Runnable("media-runnable_args_base") {}
+
+ NS_IMETHOD Run() final {
+ MOZ_ASSERT(!mHasRun, "Can only be run once");
+
+ RunInternal();
+#ifdef DEBUG
+ mHasRun = true;
+#endif
+
+ return NS_OK;
+ }
+
+ protected:
+ virtual void RunInternal() = 0;
+#ifdef DEBUG
+ bool mHasRun = false;
+#endif
+};
+
+} // namespace detail
+
+template <typename FunType, typename... Args>
+class runnable_args_func : public detail::runnable_args_base<detail::NoResult> {
+ public:
+ // |explicit| to pacify static analysis when there are no |args|.
+ template <typename... Arguments>
+ explicit runnable_args_func(FunType f, Arguments&&... args)
+ : mFunc(f), mArgs(std::forward<Arguments>(args)...) {}
+
+ protected:
+ void RunInternal() override {
+ std::apply(std::move(mFunc), std::move(mArgs));
+ }
+
+ private:
+ FunType mFunc;
+ std::tuple<Args...> mArgs;
+};
+
+template <typename FunType, typename... Args>
+runnable_args_func<FunType, std::decay_t<Args>...>* WrapRunnableNM(
+ FunType f, Args&&... args) {
+ return new runnable_args_func<FunType, std::decay_t<Args>...>(
+ f, std::forward<Args>(args)...);
+}
+
+template <typename Ret, typename FunType, typename... Args>
+class runnable_args_func_ret
+ : public detail::runnable_args_base<detail::ReturnsResult> {
+ public:
+ template <typename... Arguments>
+ runnable_args_func_ret(Ret* ret, FunType f, Arguments&&... args)
+ : mReturn(ret), mFunc(f), mArgs(std::forward<Arguments>(args)...) {}
+
+ protected:
+ void RunInternal() override {
+ *mReturn = std::apply(std::move(mFunc), std::move(mArgs));
+ }
+
+ private:
+ Ret* mReturn;
+ FunType mFunc;
+ std::tuple<Args...> mArgs;
+};
+
+template <typename R, typename FunType, typename... Args>
+runnable_args_func_ret<R, FunType, std::decay_t<Args>...>* WrapRunnableNMRet(
+ R* ret, FunType f, Args&&... args) {
+ return new runnable_args_func_ret<R, FunType, std::decay_t<Args>...>(
+ ret, f, std::forward<Args>(args)...);
+}
+
+template <typename Class, typename M, typename... Args>
+class runnable_args_memfn
+ : public detail::runnable_args_base<detail::NoResult> {
+ public:
+ template <typename... Arguments>
+ runnable_args_memfn(Class&& obj, M method, Arguments&&... args)
+ : mObj(std::forward<Class>(obj)),
+ mMethod(method),
+ mArgs(std::forward<Arguments>(args)...) {}
+
+ protected:
+ void RunInternal() override {
+ std::apply(std::mem_fn(mMethod),
+ std::tuple_cat(std::tie(mObj), std::move(mArgs)));
+ }
+
+ private:
+ // For holders such as RefPtr and UniquePtr make sure concrete copy is held
+ // rather than a potential dangling reference.
+ std::decay_t<Class> mObj;
+ M mMethod;
+ std::tuple<Args...> mArgs;
+};
+
+template <typename Class, typename M, typename... Args>
+runnable_args_memfn<Class, M, std::decay_t<Args>...>* WrapRunnable(
+ Class&& obj, M method, Args&&... args) {
+ return new runnable_args_memfn<Class, M, std::decay_t<Args>...>(
+ std::forward<Class>(obj), method, std::forward<Args>(args)...);
+}
+
+template <typename Ret, typename Class, typename M, typename... Args>
+class runnable_args_memfn_ret
+ : public detail::runnable_args_base<detail::ReturnsResult> {
+ public:
+ template <typename... Arguments>
+ runnable_args_memfn_ret(Ret* ret, Class&& obj, M method, Arguments... args)
+ : mReturn(ret),
+ mObj(std::forward<Class>(obj)),
+ mMethod(method),
+ mArgs(std::forward<Arguments>(args)...) {}
+
+ protected:
+ void RunInternal() override {
+ *mReturn = std::apply(std::mem_fn(mMethod),
+ std::tuple_cat(std::tie(mObj), std::move(mArgs)));
+ }
+
+ private:
+ Ret* mReturn;
+ // For holders such as RefPtr and UniquePtr make sure concrete copy is held
+ // rather than a potential dangling reference.
+ std::decay_t<Class> mObj;
+ M mMethod;
+ std::tuple<Args...> mArgs;
+};
+
+template <typename R, typename Class, typename M, typename... Args>
+runnable_args_memfn_ret<R, Class, M, std::decay_t<Args>...>* WrapRunnableRet(
+ R* ret, Class&& obj, M method, Args&&... args) {
+ return new runnable_args_memfn_ret<R, Class, M, std::decay_t<Args>...>(
+ ret, std::forward<Class>(obj), method, std::forward<Args>(args)...);
+}
+
+static inline nsresult RUN_ON_THREAD(
+ nsIEventTarget* thread,
+ detail::runnable_args_base<detail::NoResult>* runnable, uint32_t flags) {
+ return detail::RunOnThreadInternal(
+ thread, static_cast<nsIRunnable*>(runnable), flags);
+}
+
+static inline nsresult RUN_ON_THREAD(
+ nsIEventTarget* thread,
+ detail::runnable_args_base<detail::ReturnsResult>* runnable) {
+ return NS_DispatchAndSpinEventLoopUntilComplete(
+ "webrtc RUN_ON_THREAD"_ns, thread,
+ do_AddRef(static_cast<nsIRunnable*>(runnable)));
+}
+
+#ifdef DEBUG
+# define ASSERT_ON_THREAD(t) \
+ do { \
+ if (t) { \
+ bool on; \
+ nsresult rv; \
+ rv = t->IsOnCurrentThread(&on); \
+ MOZ_ASSERT(NS_SUCCEEDED(rv)); \
+ MOZ_ASSERT(on); \
+ } \
+ } while (0)
+#else
+# define ASSERT_ON_THREAD(t)
+#endif
+
+template <class T>
+class DispatchedRelease : public detail::runnable_args_base<detail::NoResult> {
+ public:
+ explicit DispatchedRelease(already_AddRefed<T>& ref) : ref_(ref) {}
+
+ protected:
+ void RunInternal() override { ref_ = nullptr; }
+
+ private:
+ RefPtr<T> ref_;
+};
+
+template <typename T>
+DispatchedRelease<T>* WrapRelease(already_AddRefed<T>&& ref) {
+ return new DispatchedRelease<T>(ref);
+}
+
+} /* namespace mozilla */
+
+#endif
diff --git a/dom/media/webrtc/transport/sigslot.h b/dom/media/webrtc/transport/sigslot.h
new file mode 100644
index 0000000000..448e8137fe
--- /dev/null
+++ b/dom/media/webrtc/transport/sigslot.h
@@ -0,0 +1,619 @@
+// sigslot.h: Signal/Slot classes
+//
+// Written by Sarah Thompson (sarah@telergy.com) 2002.
+//
+// License: Public domain. You are free to use this code however you like, with
+// the proviso that the author takes on no responsibility or liability for any
+// use.
+//
+// QUICK DOCUMENTATION
+//
+// (see also the full documentation at http://sigslot.sourceforge.net/)
+//
+// #define switches
+// SIGSLOT_PURE_ISO:
+// Define this to force ISO C++ compliance. This also disables all of
+// the thread safety support on platforms where it is available.
+//
+// SIGSLOT_USE_POSIX_THREADS:
+// Force use of Posix threads when using a C++ compiler other than gcc
+// on a platform that supports Posix threads. (When using gcc, this is
+// the default - use SIGSLOT_PURE_ISO to disable this if necessary)
+//
+// SIGSLOT_DEFAULT_MT_POLICY:
+// Where thread support is enabled, this defaults to
+// multi_threaded_global. Otherwise, the default is single_threaded.
+// #define this yourself to override the default. In pure ISO mode,
+// anything other than single_threaded will cause a compiler error.
+//
+// PLATFORM NOTES
+//
+// Win32:
+// On Win32, the WEBRTC_WIN symbol must be #defined. Most mainstream
+// compilers do this by default, but you may need to define it yourself
+// if your build environment is less standard. This causes the Win32
+// thread support to be compiled in and used automatically.
+//
+// Unix/Linux/BSD, etc.:
+// If you're using gcc, it is assumed that you have Posix threads
+// available, so they are used automatically. You can override this (as
+// under Windows) with the SIGSLOT_PURE_ISO switch. If you're using
+// something other than gcc but still want to use Posix threads, you
+// need to #define SIGSLOT_USE_POSIX_THREADS.
+//
+// ISO C++:
+// If none of the supported platforms are detected, or if
+// SIGSLOT_PURE_ISO is defined, all multithreading support is turned
+// off, along with any code that might cause a pure ISO C++ environment
+// to complain. Before you ask, gcc -ansi -pedantic won't compile this
+// library, but gcc -ansi is fine. Pedantic mode seems to throw a lot of
+// errors that aren't really there. If you feel like investigating this,
+// please contact the author.
+//
+//
+// THREADING MODES
+//
+// single_threaded:
+// Your program is assumed to be single threaded from the point of view
+// of signal/slot usage (i.e. all objects using signals and slots are
+// created and destroyed from a single thread). Behaviour if objects are
+// destroyed concurrently is undefined (i.e. you'll get the occasional
+// segmentation fault/memory exception).
+//
+// multi_threaded_global:
+// Your program is assumed to be multi threaded. Objects using signals
+// and slots can be safely created and destroyed from any thread, even
+// when connections exist. In multi_threaded_global mode, this is
+// achieved by a single global mutex (actually a critical section on
+// Windows because they are faster). This option uses less OS resources,
+// but results in more opportunities for contention, possibly resulting
+// in more context switches than are strictly necessary.
+//
+// multi_threaded_local:
+// Behaviour in this mode is essentially the same as
+// multi_threaded_global, except that each signal, and each object that
+// inherits has_slots, all have their own mutex/critical section. In
+// practice, this means that mutex collisions (and hence context
+// switches) only happen if they are absolutely essential. However, on
+// some platforms, creating a lot of mutexes can slow down the whole OS,
+// so use this option with care.
+//
+// USING THE LIBRARY
+//
+// See the full documentation at http://sigslot.sourceforge.net/
+//
+// Libjingle specific:
+//
+// This file has been modified such that has_slots and signalx do not have to be
+// using the same threading requirements. E.g. it is possible to connect a
+// has_slots<single_threaded> and signal0<multi_threaded_local> or
+// has_slots<multi_threaded_local> and signal0<single_threaded>.
+// If has_slots is single threaded the user must ensure that it is not trying
+// to connect or disconnect to signalx concurrently or data race may occur.
+// If signalx is single threaded the user must ensure that disconnect, connect
+// or signal is not happening concurrently or data race may occur.
+
+#ifndef RTC_BASE_SIGSLOT_H_
+#define RTC_BASE_SIGSLOT_H_
+
+#include <stdlib.h>
+#include <cstring>
+#include <list>
+#include <set>
+
+// On our copy of sigslot.h, we set single threading as default.
+#define SIGSLOT_DEFAULT_MT_POLICY single_threaded
+
+#if defined(SIGSLOT_PURE_ISO) || \
+ (!defined(WEBRTC_WIN) && !defined(__GNUG__) && \
+ !defined(SIGSLOT_USE_POSIX_THREADS))
+# define _SIGSLOT_SINGLE_THREADED
+#elif defined(WEBRTC_WIN)
+# define _SIGSLOT_HAS_WIN32_THREADS
+# if !defined(WIN32_LEAN_AND_MEAN)
+# define WIN32_LEAN_AND_MEAN
+# endif
+# include "rtc_base/win32.h"
+#elif defined(__GNUG__) || defined(SIGSLOT_USE_POSIX_THREADS)
+# define _SIGSLOT_HAS_POSIX_THREADS
+# include <pthread.h>
+#else
+# define _SIGSLOT_SINGLE_THREADED
+#endif
+
+#ifndef SIGSLOT_DEFAULT_MT_POLICY
+# ifdef _SIGSLOT_SINGLE_THREADED
+# define SIGSLOT_DEFAULT_MT_POLICY single_threaded
+# else
+# define SIGSLOT_DEFAULT_MT_POLICY multi_threaded_local
+# endif
+#endif
+
+// TODO: change this namespace to rtc?
+namespace sigslot {
+
+class single_threaded {
+ public:
+ void lock() {}
+ void unlock() {}
+};
+
+#ifdef _SIGSLOT_HAS_WIN32_THREADS
+// The multi threading policies only get compiled in if they are enabled.
+class multi_threaded_global {
+ public:
+ multi_threaded_global() {
+ static bool isinitialised = false;
+
+ if (!isinitialised) {
+ InitializeCriticalSection(get_critsec());
+ isinitialised = true;
+ }
+ }
+
+ void lock() { EnterCriticalSection(get_critsec()); }
+
+ void unlock() { LeaveCriticalSection(get_critsec()); }
+
+ private:
+ CRITICAL_SECTION* get_critsec() {
+ static CRITICAL_SECTION g_critsec;
+ return &g_critsec;
+ }
+};
+
+class multi_threaded_local {
+ public:
+ multi_threaded_local() { InitializeCriticalSection(&m_critsec); }
+
+ multi_threaded_local(const multi_threaded_local&) {
+ InitializeCriticalSection(&m_critsec);
+ }
+
+ ~multi_threaded_local() { DeleteCriticalSection(&m_critsec); }
+
+ void lock() { EnterCriticalSection(&m_critsec); }
+
+ void unlock() { LeaveCriticalSection(&m_critsec); }
+
+ private:
+ CRITICAL_SECTION m_critsec;
+};
+#endif // _SIGSLOT_HAS_WIN32_THREADS
+
+#ifdef _SIGSLOT_HAS_POSIX_THREADS
+// The multi threading policies only get compiled in if they are enabled.
+class multi_threaded_global {
+ public:
+ void lock() { pthread_mutex_lock(get_mutex()); }
+ void unlock() { pthread_mutex_unlock(get_mutex()); }
+
+ private:
+ static pthread_mutex_t* get_mutex();
+};
+
+class multi_threaded_local {
+ public:
+ multi_threaded_local() { pthread_mutex_init(&m_mutex, nullptr); }
+ multi_threaded_local(const multi_threaded_local&) {
+ pthread_mutex_init(&m_mutex, nullptr);
+ }
+ ~multi_threaded_local() { pthread_mutex_destroy(&m_mutex); }
+ void lock() { pthread_mutex_lock(&m_mutex); }
+ void unlock() { pthread_mutex_unlock(&m_mutex); }
+
+ private:
+ pthread_mutex_t m_mutex;
+};
+#endif // _SIGSLOT_HAS_POSIX_THREADS
+
+template <class mt_policy>
+class lock_block {
+ public:
+ mt_policy* m_mutex;
+
+ explicit lock_block(mt_policy* mtx) : m_mutex(mtx) { m_mutex->lock(); }
+
+ ~lock_block() { m_mutex->unlock(); }
+};
+
+class _signal_base_interface;
+
+class has_slots_interface {
+ private:
+ typedef void (*signal_connect_t)(has_slots_interface* self,
+ _signal_base_interface* sender);
+ typedef void (*signal_disconnect_t)(has_slots_interface* self,
+ _signal_base_interface* sender);
+ typedef void (*disconnect_all_t)(has_slots_interface* self);
+
+ const signal_connect_t m_signal_connect;
+ const signal_disconnect_t m_signal_disconnect;
+ const disconnect_all_t m_disconnect_all;
+
+ protected:
+ has_slots_interface(signal_connect_t conn, signal_disconnect_t disc,
+ disconnect_all_t disc_all)
+ : m_signal_connect(conn),
+ m_signal_disconnect(disc),
+ m_disconnect_all(disc_all) {}
+
+ // Doesn't really need to be virtual, but is for backwards compatibility
+ // (it was virtual in a previous version of sigslot).
+ virtual ~has_slots_interface() = default;
+
+ public:
+ void signal_connect(_signal_base_interface* sender) {
+ m_signal_connect(this, sender);
+ }
+
+ void signal_disconnect(_signal_base_interface* sender) {
+ m_signal_disconnect(this, sender);
+ }
+
+ void disconnect_all() { m_disconnect_all(this); }
+};
+
+class _signal_base_interface {
+ private:
+ typedef void (*slot_disconnect_t)(_signal_base_interface* self,
+ has_slots_interface* pslot);
+ typedef void (*slot_duplicate_t)(_signal_base_interface* self,
+ const has_slots_interface* poldslot,
+ has_slots_interface* pnewslot);
+
+ const slot_disconnect_t m_slot_disconnect;
+ const slot_duplicate_t m_slot_duplicate;
+
+ protected:
+ _signal_base_interface(slot_disconnect_t disc, slot_duplicate_t dupl)
+ : m_slot_disconnect(disc), m_slot_duplicate(dupl) {}
+
+ ~_signal_base_interface() = default;
+
+ public:
+ void slot_disconnect(has_slots_interface* pslot) {
+ m_slot_disconnect(this, pslot);
+ }
+
+ void slot_duplicate(const has_slots_interface* poldslot,
+ has_slots_interface* pnewslot) {
+ m_slot_duplicate(this, poldslot, pnewslot);
+ }
+};
+
+class _opaque_connection {
+ private:
+ typedef void (*emit_t)(const _opaque_connection*);
+ template <typename FromT, typename ToT>
+ union union_caster {
+ FromT from;
+ ToT to;
+ };
+
+ emit_t pemit;
+ has_slots_interface* pdest;
+ // Pointers to member functions may be up to 16 bytes for virtual classes,
+ // so make sure we have enough space to store it.
+ unsigned char pmethod[16];
+
+ public:
+ template <typename DestT, typename... Args>
+ _opaque_connection(DestT* pd, void (DestT::*pm)(Args...)) : pdest(pd) {
+ typedef void (DestT::*pm_t)(Args...);
+ static_assert(sizeof(pm_t) <= sizeof(pmethod),
+ "Size of slot function pointer too large.");
+
+ std::memcpy(pmethod, &pm, sizeof(pm_t));
+
+ typedef void (*em_t)(const _opaque_connection* self, Args...);
+ union_caster<em_t, emit_t> caster2;
+ caster2.from = &_opaque_connection::emitter<DestT, Args...>;
+ pemit = caster2.to;
+ }
+
+ has_slots_interface* getdest() const { return pdest; }
+
+ _opaque_connection duplicate(has_slots_interface* newtarget) const {
+ _opaque_connection res = *this;
+ res.pdest = newtarget;
+ return res;
+ }
+
+ // Just calls the stored "emitter" function pointer stored at construction
+ // time.
+ template <typename... Args>
+ void emit(Args... args) const {
+ typedef void (*em_t)(const _opaque_connection*, Args...);
+ union_caster<emit_t, em_t> caster;
+ caster.from = pemit;
+ (caster.to)(this, args...);
+ }
+
+ private:
+ template <typename DestT, typename... Args>
+ static void emitter(const _opaque_connection* self, Args... args) {
+ typedef void (DestT::*pm_t)(Args...);
+ pm_t pm;
+ std::memcpy(&pm, self->pmethod, sizeof(pm_t));
+ (static_cast<DestT*>(self->pdest)->*(pm))(args...);
+ }
+};
+
+template <class mt_policy>
+class _signal_base : public _signal_base_interface, public mt_policy {
+ protected:
+ typedef std::list<_opaque_connection> connections_list;
+
+ _signal_base()
+ : _signal_base_interface(&_signal_base::do_slot_disconnect,
+ &_signal_base::do_slot_duplicate),
+ m_current_iterator(m_connected_slots.end()) {}
+
+ ~_signal_base() { disconnect_all(); }
+
+ private:
+ _signal_base& operator=(_signal_base const& that);
+
+ public:
+ _signal_base(const _signal_base& o)
+ : _signal_base_interface(&_signal_base::do_slot_disconnect,
+ &_signal_base::do_slot_duplicate),
+ m_current_iterator(m_connected_slots.end()) {
+ lock_block<mt_policy> lock(this);
+ for (const auto& connection : o.m_connected_slots) {
+ connection.getdest()->signal_connect(this);
+ m_connected_slots.push_back(connection);
+ }
+ }
+
+ bool is_empty() {
+ lock_block<mt_policy> lock(this);
+ return m_connected_slots.empty();
+ }
+
+ void disconnect_all() {
+ lock_block<mt_policy> lock(this);
+
+ while (!m_connected_slots.empty()) {
+ has_slots_interface* pdest = m_connected_slots.front().getdest();
+ m_connected_slots.pop_front();
+ pdest->signal_disconnect(static_cast<_signal_base_interface*>(this));
+ }
+ // If disconnect_all is called while the signal is firing, advance the
+ // current slot iterator to the end to avoid an invalidated iterator from
+ // being dereferenced.
+ m_current_iterator = m_connected_slots.end();
+ }
+
+#if !defined(NDEBUG)
+ bool connected(has_slots_interface* pclass) {
+ lock_block<mt_policy> lock(this);
+ connections_list::const_iterator it = m_connected_slots.begin();
+ connections_list::const_iterator itEnd = m_connected_slots.end();
+ while (it != itEnd) {
+ if (it->getdest() == pclass) return true;
+ ++it;
+ }
+ return false;
+ }
+#endif
+
+ void disconnect(has_slots_interface* pclass) {
+ lock_block<mt_policy> lock(this);
+ connections_list::iterator it = m_connected_slots.begin();
+ connections_list::iterator itEnd = m_connected_slots.end();
+
+ while (it != itEnd) {
+ if (it->getdest() == pclass) {
+ // If we're currently using this iterator because the signal is firing,
+ // advance it to avoid it being invalidated.
+ if (m_current_iterator == it) {
+ m_current_iterator = m_connected_slots.erase(it);
+ } else {
+ m_connected_slots.erase(it);
+ }
+ pclass->signal_disconnect(static_cast<_signal_base_interface*>(this));
+ return;
+ }
+ ++it;
+ }
+ }
+
+ private:
+ static void do_slot_disconnect(_signal_base_interface* p,
+ has_slots_interface* pslot) {
+ _signal_base* const self = static_cast<_signal_base*>(p);
+ lock_block<mt_policy> lock(self);
+ connections_list::iterator it = self->m_connected_slots.begin();
+ connections_list::iterator itEnd = self->m_connected_slots.end();
+
+ while (it != itEnd) {
+ connections_list::iterator itNext = it;
+ ++itNext;
+
+ if (it->getdest() == pslot) {
+ // If we're currently using this iterator because the signal is firing,
+ // advance it to avoid it being invalidated.
+ if (self->m_current_iterator == it) {
+ self->m_current_iterator = self->m_connected_slots.erase(it);
+ } else {
+ self->m_connected_slots.erase(it);
+ }
+ }
+
+ it = itNext;
+ }
+ }
+
+ static void do_slot_duplicate(_signal_base_interface* p,
+ const has_slots_interface* oldtarget,
+ has_slots_interface* newtarget) {
+ _signal_base* const self = static_cast<_signal_base*>(p);
+ lock_block<mt_policy> lock(self);
+ connections_list::iterator it = self->m_connected_slots.begin();
+ connections_list::iterator itEnd = self->m_connected_slots.end();
+
+ while (it != itEnd) {
+ if (it->getdest() == oldtarget) {
+ self->m_connected_slots.push_back(it->duplicate(newtarget));
+ }
+
+ ++it;
+ }
+ }
+
+ protected:
+ connections_list m_connected_slots;
+
+ // Used to handle a slot being disconnected while a signal is
+ // firing (iterating m_connected_slots).
+ connections_list::iterator m_current_iterator;
+ bool m_erase_current_iterator = false;
+};
+
+template <class mt_policy = SIGSLOT_DEFAULT_MT_POLICY>
+class has_slots : public has_slots_interface, public mt_policy {
+ private:
+ typedef std::set<_signal_base_interface*> sender_set;
+ typedef sender_set::const_iterator const_iterator;
+
+ public:
+ has_slots()
+ : has_slots_interface(&has_slots::do_signal_connect,
+ &has_slots::do_signal_disconnect,
+ &has_slots::do_disconnect_all) {}
+
+ has_slots(has_slots const& o)
+ : has_slots_interface(&has_slots::do_signal_connect,
+ &has_slots::do_signal_disconnect,
+ &has_slots::do_disconnect_all) {
+ lock_block<mt_policy> lock(this);
+ for (auto* sender : o.m_senders) {
+ sender->slot_duplicate(&o, this);
+ m_senders.insert(sender);
+ }
+ }
+
+ ~has_slots() { this->disconnect_all(); }
+
+ private:
+ has_slots& operator=(has_slots const&);
+
+ static void do_signal_connect(has_slots_interface* p,
+ _signal_base_interface* sender) {
+ has_slots* const self = static_cast<has_slots*>(p);
+ lock_block<mt_policy> lock(self);
+ self->m_senders.insert(sender);
+ }
+
+ static void do_signal_disconnect(has_slots_interface* p,
+ _signal_base_interface* sender) {
+ has_slots* const self = static_cast<has_slots*>(p);
+ lock_block<mt_policy> lock(self);
+ self->m_senders.erase(sender);
+ }
+
+ static void do_disconnect_all(has_slots_interface* p) {
+ has_slots* const self = static_cast<has_slots*>(p);
+ lock_block<mt_policy> lock(self);
+ while (!self->m_senders.empty()) {
+ std::set<_signal_base_interface*> senders;
+ senders.swap(self->m_senders);
+ const_iterator it = senders.begin();
+ const_iterator itEnd = senders.end();
+
+ while (it != itEnd) {
+ _signal_base_interface* s = *it;
+ ++it;
+ s->slot_disconnect(p);
+ }
+ }
+ }
+
+ private:
+ sender_set m_senders;
+};
+
+template <class mt_policy, typename... Args>
+class signal_with_thread_policy : public _signal_base<mt_policy> {
+ private:
+ typedef _signal_base<mt_policy> base;
+
+ protected:
+ typedef typename base::connections_list connections_list;
+
+ public:
+ signal_with_thread_policy() = default;
+
+ template <class desttype>
+ void connect(desttype* pclass, void (desttype::*pmemfun)(Args...)) {
+ lock_block<mt_policy> lock(this);
+ this->m_connected_slots.push_back(_opaque_connection(pclass, pmemfun));
+ pclass->signal_connect(static_cast<_signal_base_interface*>(this));
+ }
+
+ void emit(Args... args) {
+ lock_block<mt_policy> lock(this);
+ this->m_current_iterator = this->m_connected_slots.begin();
+ while (this->m_current_iterator != this->m_connected_slots.end()) {
+ _opaque_connection const& conn = *this->m_current_iterator;
+ ++(this->m_current_iterator);
+ conn.emit<Args...>(args...);
+ }
+ }
+
+ void operator()(Args... args) { emit(args...); }
+};
+
+// Alias with default thread policy. Needed because both default arguments
+// and variadic template arguments must go at the end of the list, so we
+// can't have both at once.
+template <typename... Args>
+using signal = signal_with_thread_policy<SIGSLOT_DEFAULT_MT_POLICY, Args...>;
+
+// The previous verion of sigslot didn't use variadic templates, so you would
+// need to write "sigslot::signal2<Arg1, Arg2>", for example.
+// Now you can just write "sigslot::signal<Arg1, Arg2>", but these aliases
+// exist for backwards compatibility.
+template <typename mt_policy = SIGSLOT_DEFAULT_MT_POLICY>
+using signal0 = signal_with_thread_policy<mt_policy>;
+
+template <typename A1, typename mt_policy = SIGSLOT_DEFAULT_MT_POLICY>
+using signal1 = signal_with_thread_policy<mt_policy, A1>;
+
+template <typename A1, typename A2,
+ typename mt_policy = SIGSLOT_DEFAULT_MT_POLICY>
+using signal2 = signal_with_thread_policy<mt_policy, A1, A2>;
+
+template <typename A1, typename A2, typename A3,
+ typename mt_policy = SIGSLOT_DEFAULT_MT_POLICY>
+using signal3 = signal_with_thread_policy<mt_policy, A1, A2, A3>;
+
+template <typename A1, typename A2, typename A3, typename A4,
+ typename mt_policy = SIGSLOT_DEFAULT_MT_POLICY>
+using signal4 = signal_with_thread_policy<mt_policy, A1, A2, A3, A4>;
+
+template <typename A1, typename A2, typename A3, typename A4, typename A5,
+ typename mt_policy = SIGSLOT_DEFAULT_MT_POLICY>
+using signal5 = signal_with_thread_policy<mt_policy, A1, A2, A3, A4, A5>;
+
+template <typename A1, typename A2, typename A3, typename A4, typename A5,
+ typename A6, typename mt_policy = SIGSLOT_DEFAULT_MT_POLICY>
+using signal6 = signal_with_thread_policy<mt_policy, A1, A2, A3, A4, A5, A6>;
+
+template <typename A1, typename A2, typename A3, typename A4, typename A5,
+ typename A6, typename A7,
+ typename mt_policy = SIGSLOT_DEFAULT_MT_POLICY>
+using signal7 =
+ signal_with_thread_policy<mt_policy, A1, A2, A3, A4, A5, A6, A7>;
+
+template <typename A1, typename A2, typename A3, typename A4, typename A5,
+ typename A6, typename A7, typename A8,
+ typename mt_policy = SIGSLOT_DEFAULT_MT_POLICY>
+using signal8 =
+ signal_with_thread_policy<mt_policy, A1, A2, A3, A4, A5, A6, A7, A8>;
+
+} // namespace sigslot
+
+#endif // RTC_BASE_SIGSLOT_H_
diff --git a/dom/media/webrtc/transport/simpletokenbucket.cpp b/dom/media/webrtc/transport/simpletokenbucket.cpp
new file mode 100644
index 0000000000..0509697c7b
--- /dev/null
+++ b/dom/media/webrtc/transport/simpletokenbucket.cpp
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* Original author: bcampen@mozilla.com */
+
+#include "simpletokenbucket.h"
+
+#include <stdint.h>
+
+#include "prinrval.h"
+
+namespace mozilla {
+
+SimpleTokenBucket::SimpleTokenBucket(size_t bucket_size,
+ size_t tokens_per_second)
+ : max_tokens_(bucket_size),
+ num_tokens_(bucket_size),
+ tokens_per_second_(tokens_per_second),
+ last_time_tokens_added_(PR_IntervalNow()) {}
+
+size_t SimpleTokenBucket::getTokens(size_t num_requested_tokens) {
+ // Only fill if there isn't enough to satisfy the request.
+ // If we get tokens so seldomly that we are able to roll the timer all
+ // the way around its range, then we lose that entire range of time
+ // for token accumulation. Probably not the end of the world.
+ if (num_requested_tokens > num_tokens_) {
+ PRIntervalTime now = PR_IntervalNow();
+
+ // If we roll over the max, since everything in this calculation is the same
+ // unsigned type, this will still yield the elapsed time (unless we've
+ // wrapped more than once).
+ PRIntervalTime elapsed_ticks = now - last_time_tokens_added_;
+
+ uint32_t elapsed_milli_sec = PR_IntervalToMilliseconds(elapsed_ticks);
+ size_t tokens_to_add = (elapsed_milli_sec * tokens_per_second_) / 1000;
+
+ // Only update our timestamp if we added some tokens
+ // TODO:(bcampen@mozilla.com) Should we attempt to "save" leftover time?
+ if (tokens_to_add) {
+ num_tokens_ += tokens_to_add;
+ if (num_tokens_ > max_tokens_) {
+ num_tokens_ = max_tokens_;
+ }
+
+ last_time_tokens_added_ = now;
+ }
+
+ if (num_requested_tokens > num_tokens_) {
+ return num_tokens_;
+ }
+ }
+
+ num_tokens_ -= num_requested_tokens;
+ return num_requested_tokens;
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/transport/simpletokenbucket.h b/dom/media/webrtc/transport/simpletokenbucket.h
new file mode 100644
index 0000000000..7e809535b1
--- /dev/null
+++ b/dom/media/webrtc/transport/simpletokenbucket.h
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* Original author: bcampen@mozilla.com */
+
+/*
+ * This file defines a dirt-simple token bucket class.
+ */
+
+#ifndef simpletokenbucket_h__
+#define simpletokenbucket_h__
+
+#include <stdint.h>
+
+#include "prinrval.h"
+
+#include "m_cpp_utils.h"
+
+namespace mozilla {
+
+class SimpleTokenBucket {
+ public:
+ /*
+ * Create a SimpleTokenBucket with a given maximum size and
+ * token replenishment rate.
+ * (eg; if you want a maximum rate of 5 per second over a 7 second
+ * period, call SimpleTokenBucket b(5*7, 5);)
+ */
+ SimpleTokenBucket(size_t bucket_size, size_t tokens_per_second);
+
+ /*
+ * Attempt to acquire a number of tokens. If successful, returns
+ * |num_tokens|, otherwise returns the number of tokens currently
+ * in the bucket.
+ * Note: To get the number of tokens in the bucket, pass something
+ * like UINT32_MAX.
+ */
+ size_t getTokens(size_t num_tokens);
+
+ protected: // Allow testing to touch these.
+ uint64_t max_tokens_;
+ uint64_t num_tokens_;
+ size_t tokens_per_second_;
+ PRIntervalTime last_time_tokens_added_;
+
+ DISALLOW_COPY_ASSIGN(SimpleTokenBucket);
+};
+
+} // namespace mozilla
+
+#endif // simpletokenbucket_h__
diff --git a/dom/media/webrtc/transport/srtp/README_MOZILLA b/dom/media/webrtc/transport/srtp/README_MOZILLA
new file mode 100644
index 0000000000..8533b67c53
--- /dev/null
+++ b/dom/media/webrtc/transport/srtp/README_MOZILLA
@@ -0,0 +1,7 @@
+This directory contains build files for libsrtp. The actual library
+source is in $TOPSRCDIR/third_party/libsrtp/
+
+The upstream git repository is https://github.com/cisco/libsrtp
+
+TBD add code and instructions how to do a clean update import without manual
+intervention.
diff --git a/dom/media/webrtc/transport/srtp/moz.build b/dom/media/webrtc/transport/srtp/moz.build
new file mode 100644
index 0000000000..d3d4971a6d
--- /dev/null
+++ b/dom/media/webrtc/transport/srtp/moz.build
@@ -0,0 +1,8 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+if CONFIG["MOZ_SRTP"]:
+ DIRS += ["/third_party/libsrtp/src"]
diff --git a/dom/media/webrtc/transport/stun_socket_filter.cpp b/dom/media/webrtc/transport/stun_socket_filter.cpp
new file mode 100644
index 0000000000..b568f97a40
--- /dev/null
+++ b/dom/media/webrtc/transport/stun_socket_filter.cpp
@@ -0,0 +1,432 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include <string>
+#include <set>
+#include <iomanip>
+
+extern "C" {
+#include "nr_api.h"
+#include "transport_addr.h"
+#include "stun.h"
+}
+
+#include "logging.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/net/DNS.h"
+#include "stun_socket_filter.h"
+#include "nr_socket_prsock.h"
+
+namespace {
+
+MOZ_MTLOG_MODULE("mtransport")
+
+class NetAddrCompare {
+ public:
+ bool operator()(const mozilla::net::NetAddr& lhs,
+ const mozilla::net::NetAddr& rhs) const {
+ if (lhs.raw.family != rhs.raw.family) {
+ return lhs.raw.family < rhs.raw.family;
+ }
+
+ switch (lhs.raw.family) {
+ case AF_INET:
+ if (lhs.inet.port != rhs.inet.port) {
+ return lhs.inet.port < rhs.inet.port;
+ }
+ return lhs.inet.ip < rhs.inet.ip;
+ case AF_INET6:
+ if (lhs.inet6.port != rhs.inet6.port) {
+ return lhs.inet6.port < rhs.inet6.port;
+ }
+ return memcmp(&lhs.inet6.ip, &rhs.inet6.ip, sizeof(lhs.inet6.ip)) < 0;
+ default:
+ MOZ_ASSERT(false);
+ }
+ return false;
+ }
+};
+
+class PendingSTUNRequest {
+ public:
+ PendingSTUNRequest(const mozilla::net::NetAddr& netaddr, const UINT12& id)
+ : id_(id), net_addr_(netaddr), is_id_set_(true) {}
+
+ MOZ_IMPLICIT PendingSTUNRequest(const mozilla::net::NetAddr& netaddr)
+ : id_(), net_addr_(netaddr), is_id_set_(false) {}
+
+ bool operator<(const PendingSTUNRequest& rhs) const {
+ if (NetAddrCompare()(net_addr_, rhs.net_addr_)) {
+ return true;
+ }
+
+ if (NetAddrCompare()(rhs.net_addr_, net_addr_)) {
+ return false;
+ }
+
+ if (!is_id_set_ && !rhs.is_id_set_) {
+ // PendingSTUNRequest can be stored to set only when it has id,
+ // so comparing two PendingSTUNRequst without id is not going
+ // to happen.
+ MOZ_CRASH();
+ }
+
+ if (!(is_id_set_ && rhs.is_id_set_)) {
+ // one of operands doesn't have id, ignore the difference.
+ return false;
+ }
+
+ return memcmp(id_.octet, rhs.id_.octet, sizeof(id_.octet)) < 0;
+ }
+
+ private:
+ const UINT12 id_;
+ const mozilla::net::NetAddr net_addr_;
+ const bool is_id_set_;
+};
+
+static uint16_t GetPortInfallible(const mozilla::net::NetAddr& aAddr) {
+ uint16_t result = 0;
+ (void)aAddr.GetPort(&result);
+ return result;
+}
+
+static std::ostream& operator<<(std::ostream& aStream, UINT12 aId) {
+ for (int octet : aId.octet) {
+ aStream << std::hex << std::setfill('0') << std::setw(2) << octet;
+ }
+ return aStream;
+}
+
+class STUNUDPSocketFilter : public nsISocketFilter {
+ public:
+ STUNUDPSocketFilter() : white_list_(), pending_requests_() {}
+
+ // Allocated/freed and used on the PBackground IPC thread
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISOCKETFILTER
+
+ private:
+ virtual ~STUNUDPSocketFilter() = default;
+
+ bool filter_incoming_packet(const mozilla::net::NetAddr* remote_addr,
+ const uint8_t* data, uint32_t len);
+
+ bool filter_outgoing_packet(const mozilla::net::NetAddr* remote_addr,
+ const uint8_t* data, uint32_t len);
+
+ std::set<mozilla::net::NetAddr, NetAddrCompare> white_list_;
+ std::set<PendingSTUNRequest> pending_requests_;
+ std::set<PendingSTUNRequest> response_allowed_;
+};
+
+NS_IMPL_ISUPPORTS(STUNUDPSocketFilter, nsISocketFilter)
+
+NS_IMETHODIMP
+STUNUDPSocketFilter::FilterPacket(const mozilla::net::NetAddr* remote_addr,
+ const uint8_t* data, uint32_t len,
+ int32_t direction, bool* result) {
+ switch (direction) {
+ case nsISocketFilter::SF_INCOMING:
+ *result = filter_incoming_packet(remote_addr, data, len);
+ break;
+ case nsISocketFilter::SF_OUTGOING:
+ *result = filter_outgoing_packet(remote_addr, data, len);
+ break;
+ default:
+ MOZ_CRASH("Unknown packet direction");
+ }
+ return NS_OK;
+}
+
+bool STUNUDPSocketFilter::filter_incoming_packet(
+ const mozilla::net::NetAddr* remote_addr, const uint8_t* data,
+ uint32_t len) {
+ // Check white list
+ if (white_list_.find(*remote_addr) != white_list_.end()) {
+ MOZ_MTLOG(ML_DEBUG, __func__ << this << " Address in whitelist: "
+ << remote_addr->ToString() << ":"
+ << GetPortInfallible(*remote_addr));
+ return true;
+ }
+
+ // If it is a STUN response message and we can match its id with one of the
+ // pending requests, we can add this address into whitelist.
+ if (nr_is_stun_response_message(
+ reinterpret_cast<UCHAR*>(const_cast<uint8_t*>(data)), len)) {
+ const nr_stun_message_header* msg =
+ reinterpret_cast<const nr_stun_message_header*>(data);
+ PendingSTUNRequest pending_req(*remote_addr, msg->id);
+ std::set<PendingSTUNRequest>::iterator it =
+ pending_requests_.find(pending_req);
+ if (it != pending_requests_.end()) {
+ pending_requests_.erase(it);
+ response_allowed_.erase(pending_req);
+ white_list_.insert(*remote_addr);
+ MOZ_MTLOG(ML_DEBUG, __func__ << this
+ << " Allowing known STUN response, "
+ "remembering address in whitelist: "
+ << remote_addr->ToString() << ":"
+ << GetPortInfallible(*remote_addr)
+ << " id=" << msg->id);
+ return true;
+ }
+ }
+ // If it's an incoming STUN request we let it pass and add it to the list of
+ // pending response for white listing once we answer.
+ if (nr_is_stun_request_message(
+ reinterpret_cast<UCHAR*>(const_cast<uint8_t*>(data)), len)) {
+ const nr_stun_message_header* msg =
+ reinterpret_cast<const nr_stun_message_header*>(data);
+ response_allowed_.insert(PendingSTUNRequest(*remote_addr, msg->id));
+ MOZ_MTLOG(
+ ML_DEBUG,
+ __func__ << this
+ << " Allowing STUN request, will allow packets in return: "
+ << remote_addr->ToString() << ":"
+ << GetPortInfallible(*remote_addr) << " id=" << msg->id);
+ return true;
+ }
+ // Lastly if we have send a STUN request to the destination of this
+ // packet we allow it to send us anything back in case it's for example a
+ // DTLS message (but we don't white list).
+ std::set<PendingSTUNRequest>::iterator it =
+ pending_requests_.find(PendingSTUNRequest(*remote_addr));
+ if (it != pending_requests_.end()) {
+ MOZ_MTLOG(
+ ML_DEBUG,
+ __func__
+ << this
+ << " Allowing packet from source while waiting for a response: "
+ << remote_addr->ToString() << ":"
+ << GetPortInfallible(*remote_addr));
+ return true;
+ }
+
+ MOZ_MTLOG(
+ ML_DEBUG,
+ __func__
+ << " Disallowing packet that is neither a STUN request or response: "
+ << remote_addr->ToString() << ":" << GetPortInfallible(*remote_addr));
+ return false;
+}
+
+bool STUNUDPSocketFilter::filter_outgoing_packet(
+ const mozilla::net::NetAddr* remote_addr, const uint8_t* data,
+ uint32_t len) {
+ // Check white list
+ if (white_list_.find(*remote_addr) != white_list_.end()) {
+ MOZ_MTLOG(ML_DEBUG, __func__ << this << " Address in whitelist: "
+ << remote_addr->ToString() << ":"
+ << GetPortInfallible(*remote_addr));
+ return true;
+ }
+
+ // Check if it is a stun packet. If yes, we put it into a pending list and
+ // wait for response packet.
+ if (nr_is_stun_request_message(
+ reinterpret_cast<UCHAR*>(const_cast<uint8_t*>(data)), len)) {
+ const nr_stun_message_header* msg =
+ reinterpret_cast<const nr_stun_message_header*>(data);
+ pending_requests_.insert(PendingSTUNRequest(*remote_addr, msg->id));
+ MOZ_MTLOG(
+ ML_DEBUG,
+ __func__ << this
+ << " Allowing STUN request, will allow packets in return: "
+ << remote_addr->ToString() << ":"
+ << GetPortInfallible(*remote_addr) << " id=" << msg->id);
+ return true;
+ }
+
+ // If it is a stun response packet, and we had received the request before, we
+ // can allow it packet to pass filter.
+ if (nr_is_stun_response_message(
+ reinterpret_cast<UCHAR*>(const_cast<uint8_t*>(data)), len)) {
+ const nr_stun_message_header* msg =
+ reinterpret_cast<const nr_stun_message_header*>(data);
+ std::set<PendingSTUNRequest>::iterator it =
+ response_allowed_.find(PendingSTUNRequest(*remote_addr, msg->id));
+ if (it != response_allowed_.end()) {
+ white_list_.insert(*remote_addr);
+ response_allowed_.erase(it);
+ MOZ_MTLOG(ML_DEBUG, __func__ << this
+ << " Allowing known STUN response, "
+ "remembering address in whitelist: "
+ << remote_addr->ToString() << ":"
+ << GetPortInfallible(*remote_addr)
+ << " id=" << msg->id);
+ return true;
+ }
+
+ MOZ_MTLOG(ML_DEBUG,
+ __func__ << this << " Disallowing unknown STUN response: "
+ << remote_addr->ToString() << ":"
+ << GetPortInfallible(*remote_addr) << " id=" << msg->id);
+ return false;
+ }
+
+ MOZ_MTLOG(
+ ML_DEBUG,
+ __func__
+ << " Disallowing packet that is neither a STUN request or response: "
+ << remote_addr->ToString() << ":" << GetPortInfallible(*remote_addr));
+ return false;
+}
+
+class PendingSTUNId {
+ public:
+ explicit PendingSTUNId(const UINT12& id) : id_(id) {}
+
+ bool operator<(const PendingSTUNId& rhs) const {
+ return memcmp(id_.octet, rhs.id_.octet, sizeof(id_.octet)) < 0;
+ }
+
+ private:
+ const UINT12 id_;
+};
+
+class STUNTCPSocketFilter : public nsISocketFilter {
+ public:
+ STUNTCPSocketFilter()
+ : white_listed_(false), pending_request_ids_(), response_allowed_ids_() {}
+
+ // Allocated/freed and used on the PBackground IPC thread
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISOCKETFILTER
+
+ private:
+ virtual ~STUNTCPSocketFilter() = default;
+
+ bool filter_incoming_packet(const uint8_t* data, uint32_t len);
+
+ bool filter_outgoing_packet(const uint8_t* data, uint32_t len);
+
+ bool white_listed_;
+ std::set<PendingSTUNId> pending_request_ids_;
+ std::set<PendingSTUNId> response_allowed_ids_;
+};
+
+NS_IMPL_ISUPPORTS(STUNTCPSocketFilter, nsISocketFilter)
+
+NS_IMETHODIMP
+STUNTCPSocketFilter::FilterPacket(const mozilla::net::NetAddr* remote_addr,
+ const uint8_t* data, uint32_t len,
+ int32_t direction, bool* result) {
+ switch (direction) {
+ case nsISocketFilter::SF_INCOMING:
+ *result = filter_incoming_packet(data, len);
+ break;
+ case nsISocketFilter::SF_OUTGOING:
+ *result = filter_outgoing_packet(data, len);
+ break;
+ default:
+ MOZ_CRASH("Unknown packet direction");
+ }
+ return NS_OK;
+}
+
+bool STUNTCPSocketFilter::filter_incoming_packet(const uint8_t* data,
+ uint32_t len) {
+ // check if white listed already
+ if (white_listed_) {
+ return true;
+ }
+
+ UCHAR* stun = const_cast<uint8_t*>(data);
+ uint32_t length = len;
+ if (!nr_is_stun_message(stun, length)) {
+ stun += 2;
+ length -= 2;
+ if (!nr_is_stun_message(stun, length)) {
+ // Note: the UDP filter lets incoming packets pass, because order of
+ // packets is not guaranteed and the next packet is likely an important
+ // packet for DTLS (which is costly in terms of timing to wait for a
+ // retransmit). This does not apply to TCP with its guaranteed order. But
+ // we still let it pass, because otherwise we would have to buffer bytes
+ // here until the minimum STUN request size of bytes has been received.
+ return true;
+ }
+ }
+
+ const nr_stun_message_header* msg =
+ reinterpret_cast<const nr_stun_message_header*>(stun);
+
+ // If it is a STUN response message and we can match its id with one of the
+ // pending requests, we can add this address into whitelist.
+ if (nr_is_stun_response_message(stun, length)) {
+ std::set<PendingSTUNId>::iterator it =
+ pending_request_ids_.find(PendingSTUNId(msg->id));
+ if (it != pending_request_ids_.end()) {
+ pending_request_ids_.erase(it);
+ white_listed_ = true;
+ }
+ } else {
+ // If it is a STUN message, but not a response message, we add it into
+ // response allowed list and allow outgoing filter to send a response back.
+ response_allowed_ids_.insert(PendingSTUNId(msg->id));
+ }
+
+ return true;
+}
+
+bool STUNTCPSocketFilter::filter_outgoing_packet(const uint8_t* data,
+ uint32_t len) {
+ // check if white listed already
+ if (white_listed_) {
+ return true;
+ }
+
+ UCHAR* stun = const_cast<uint8_t*>(data);
+ uint32_t length = len;
+ if (!nr_is_stun_message(stun, length)) {
+ stun += 2;
+ length -= 2;
+ if (!nr_is_stun_message(stun, length)) {
+ return false;
+ }
+ }
+
+ const nr_stun_message_header* msg =
+ reinterpret_cast<const nr_stun_message_header*>(stun);
+
+ // Check if it is a stun request. If yes, we put it into a pending list and
+ // wait for response packet.
+ if (nr_is_stun_request_message(stun, length)) {
+ pending_request_ids_.insert(PendingSTUNId(msg->id));
+ return true;
+ }
+
+ // If it is a stun response packet, and we had received the request before, we
+ // can allow it packet to pass filter.
+ if (nr_is_stun_response_message(stun, length)) {
+ std::set<PendingSTUNId>::iterator it =
+ response_allowed_ids_.find(PendingSTUNId(msg->id));
+ if (it != response_allowed_ids_.end()) {
+ response_allowed_ids_.erase(it);
+ white_listed_ = true;
+ return true;
+ }
+ }
+
+ return false;
+}
+
+} // anonymous namespace
+
+NS_IMPL_ISUPPORTS(nsStunUDPSocketFilterHandler, nsISocketFilterHandler)
+
+NS_IMETHODIMP nsStunUDPSocketFilterHandler::NewFilter(
+ nsISocketFilter** result) {
+ nsISocketFilter* ret = new STUNUDPSocketFilter();
+ NS_ADDREF(*result = ret);
+ return NS_OK;
+}
+
+NS_IMPL_ISUPPORTS(nsStunTCPSocketFilterHandler, nsISocketFilterHandler)
+
+NS_IMETHODIMP nsStunTCPSocketFilterHandler::NewFilter(
+ nsISocketFilter** result) {
+ nsISocketFilter* ret = new STUNTCPSocketFilter();
+ NS_ADDREF(*result = ret);
+ return NS_OK;
+}
diff --git a/dom/media/webrtc/transport/stun_socket_filter.h b/dom/media/webrtc/transport/stun_socket_filter.h
new file mode 100644
index 0000000000..393257577d
--- /dev/null
+++ b/dom/media/webrtc/transport/stun_socket_filter.h
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef stun_socket_filter_h__
+#define stun_socket_filter_h__
+
+#include "nsISocketFilter.h"
+
+#define NS_STUN_UDP_SOCKET_FILTER_HANDLER_CID \
+ {0x3e43ee93, \
+ 0x829e, \
+ 0x4ea6, \
+ {0xa3, 0x4e, 0x62, 0xd9, 0xe4, 0xc9, 0xf9, 0x93}};
+
+class nsStunUDPSocketFilterHandler : public nsISocketFilterHandler {
+ public:
+ // Threadsafe because we create off-main-thread, but destroy on MainThread
+ // via FreeFactoryEntries()
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSISOCKETFILTERHANDLER
+ private:
+ virtual ~nsStunUDPSocketFilterHandler() = default;
+};
+
+#define NS_STUN_TCP_SOCKET_FILTER_HANDLER_CID \
+ {0x9fea635a, \
+ 0x2fc2, \
+ 0x4d08, \
+ {0x97, 0x21, 0xd2, 0x38, 0xd3, 0xf5, 0x2f, 0x92}};
+
+class nsStunTCPSocketFilterHandler : public nsISocketFilterHandler {
+ public:
+ // Threadsafe because we create off-main-thread, but destroy on MainThread
+ // via FreeFactoryEntries()
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSISOCKETFILTERHANDLER
+ private:
+ virtual ~nsStunTCPSocketFilterHandler() = default;
+};
+
+#endif // stun_socket_filter_h__
diff --git a/dom/media/webrtc/transport/test/TestSyncRunnable.cpp b/dom/media/webrtc/transport/test/TestSyncRunnable.cpp
new file mode 100644
index 0000000000..ca671b4e79
--- /dev/null
+++ b/dom/media/webrtc/transport/test/TestSyncRunnable.cpp
@@ -0,0 +1,56 @@
+/* -*- Mode: C++; tab-width: 12; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsIThread.h"
+#include "nsThreadUtils.h"
+#include "mozilla/SyncRunnable.h"
+
+#include "gtest/gtest.h"
+
+using namespace mozilla;
+
+nsIThread* gThread = nullptr;
+
+class TestRunnable : public Runnable {
+ public:
+ TestRunnable() : Runnable("TestRunnable"), ran_(false) {}
+
+ NS_IMETHOD Run() override {
+ ran_ = true;
+
+ return NS_OK;
+ }
+
+ bool ran() const { return ran_; }
+
+ private:
+ bool ran_;
+};
+
+class TestSyncRunnable : public ::testing::Test {
+ public:
+ static void SetUpTestCase() {
+ nsresult rv = NS_NewNamedThread("thread", &gThread);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+ }
+
+ static void TearDownTestCase() {
+ if (gThread) gThread->Shutdown();
+ }
+};
+
+TEST_F(TestSyncRunnable, TestDispatch) {
+ RefPtr<TestRunnable> r(new TestRunnable());
+ RefPtr<SyncRunnable> s(new SyncRunnable(r));
+ s->DispatchToThread(gThread);
+
+ ASSERT_TRUE(r->ran());
+}
+
+TEST_F(TestSyncRunnable, TestDispatchStatic) {
+ RefPtr<TestRunnable> r(new TestRunnable());
+ SyncRunnable::DispatchToThread(gThread, r);
+ ASSERT_TRUE(r->ran());
+}
diff --git a/dom/media/webrtc/transport/test/buffered_stun_socket_unittest.cpp b/dom/media/webrtc/transport/test/buffered_stun_socket_unittest.cpp
new file mode 100644
index 0000000000..e6a9cd38a2
--- /dev/null
+++ b/dom/media/webrtc/transport/test/buffered_stun_socket_unittest.cpp
@@ -0,0 +1,245 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+extern "C" {
+#include "nr_api.h"
+#include "nr_socket.h"
+#include "nr_socket_buffered_stun.h"
+#include "transport_addr.h"
+}
+
+#include "stun_msg.h"
+
+#include "dummysocket.h"
+
+#include "nr_socket_prsock.h"
+
+#define GTEST_HAS_RTTI 0
+#include "gtest/gtest.h"
+#include "gtest_utils.h"
+
+using namespace mozilla;
+
+static uint8_t kStunMessage[] = {0x00, 0x01, 0x00, 0x08, 0x21, 0x12, 0xa4,
+ 0x42, 0x9b, 0x90, 0xbe, 0x2c, 0xae, 0x1a,
+ 0x0c, 0xa8, 0xa0, 0xd6, 0x8b, 0x08, 0x80,
+ 0x28, 0x00, 0x04, 0xdb, 0x35, 0x5f, 0xaa};
+static size_t kStunMessageLen = sizeof(kStunMessage);
+
+class BufferedStunSocketTest : public MtransportTest {
+ public:
+ BufferedStunSocketTest()
+ : MtransportTest(), dummy_(nullptr), test_socket_(nullptr) {}
+
+ ~BufferedStunSocketTest() { nr_socket_destroy(&test_socket_); }
+
+ void SetUp() override {
+ MtransportTest::SetUp();
+
+ RefPtr<DummySocket> dummy(new DummySocket());
+
+ int r =
+ nr_socket_buffered_stun_create(dummy->get_nr_socket(), kStunMessageLen,
+ TURN_TCP_FRAMING, &test_socket_);
+ ASSERT_EQ(0, r);
+ dummy_ = std::move(dummy); // Now owned by test_socket_.
+
+ r = nr_str_port_to_transport_addr((char*)"192.0.2.133", 3333, IPPROTO_TCP,
+ &remote_addr_);
+ ASSERT_EQ(0, r);
+
+ r = nr_socket_connect(test_socket_, &remote_addr_);
+ ASSERT_EQ(0, r);
+ }
+
+ nr_socket* socket() { return test_socket_; }
+
+ protected:
+ RefPtr<DummySocket> dummy_;
+ nr_socket* test_socket_;
+ nr_transport_addr remote_addr_;
+};
+
+TEST_F(BufferedStunSocketTest, TestCreate) {}
+
+TEST_F(BufferedStunSocketTest, TestSendTo) {
+ int r = nr_socket_sendto(test_socket_, kStunMessage, kStunMessageLen, 0,
+ &remote_addr_);
+ ASSERT_EQ(0, r);
+
+ dummy_->CheckWriteBuffer(kStunMessage, kStunMessageLen);
+}
+
+TEST_F(BufferedStunSocketTest, TestSendToBuffered) {
+ dummy_->SetWritable(0);
+
+ int r = nr_socket_sendto(test_socket_, kStunMessage, kStunMessageLen, 0,
+ &remote_addr_);
+ ASSERT_EQ(0, r);
+
+ dummy_->CheckWriteBuffer(nullptr, 0);
+
+ dummy_->SetWritable(kStunMessageLen);
+ dummy_->FireWritableCb();
+ dummy_->CheckWriteBuffer(kStunMessage, kStunMessageLen);
+}
+
+TEST_F(BufferedStunSocketTest, TestSendFullThenDrain) {
+ dummy_->SetWritable(0);
+
+ for (;;) {
+ int r = nr_socket_sendto(test_socket_, kStunMessage, kStunMessageLen, 0,
+ &remote_addr_);
+ if (r == R_WOULDBLOCK) break;
+
+ ASSERT_EQ(0, r);
+ }
+
+ // Nothing was written.
+ dummy_->CheckWriteBuffer(nullptr, 0);
+
+ // Now flush.
+ dummy_->SetWritable(kStunMessageLen);
+ dummy_->FireWritableCb();
+ dummy_->ClearWriteBuffer();
+
+ // Verify we can write something.
+ int r = nr_socket_sendto(test_socket_, kStunMessage, kStunMessageLen, 0,
+ &remote_addr_);
+ ASSERT_EQ(0, r);
+
+ // And that it appears.
+ dummy_->CheckWriteBuffer(kStunMessage, kStunMessageLen);
+}
+
+TEST_F(BufferedStunSocketTest, TestSendToPartialBuffered) {
+ dummy_->SetWritable(10);
+
+ int r = nr_socket_sendto(test_socket_, kStunMessage, kStunMessageLen, 0,
+ &remote_addr_);
+ ASSERT_EQ(0, r);
+
+ dummy_->CheckWriteBuffer(kStunMessage, 10);
+ dummy_->ClearWriteBuffer();
+
+ dummy_->SetWritable(kStunMessageLen);
+ dummy_->FireWritableCb();
+ dummy_->CheckWriteBuffer(kStunMessage + 10, kStunMessageLen - 10);
+}
+
+TEST_F(BufferedStunSocketTest, TestSendToReject) {
+ dummy_->SetWritable(0);
+
+ int r = nr_socket_sendto(test_socket_, kStunMessage, kStunMessageLen, 0,
+ &remote_addr_);
+ ASSERT_EQ(0, r);
+
+ dummy_->CheckWriteBuffer(nullptr, 0);
+
+ r = nr_socket_sendto(test_socket_, kStunMessage, kStunMessageLen, 0,
+ &remote_addr_);
+ ASSERT_EQ(R_WOULDBLOCK, r);
+
+ dummy_->CheckWriteBuffer(nullptr, 0);
+}
+
+TEST_F(BufferedStunSocketTest, TestSendToWrongAddr) {
+ nr_transport_addr addr;
+
+ int r = nr_str_port_to_transport_addr((char*)"192.0.2.134", 3333, IPPROTO_TCP,
+ &addr);
+ ASSERT_EQ(0, r);
+
+ r = nr_socket_sendto(test_socket_, kStunMessage, kStunMessageLen, 0, &addr);
+ ASSERT_EQ(R_BAD_DATA, r);
+}
+
+TEST_F(BufferedStunSocketTest, TestReceiveRecvFrom) {
+ dummy_->SetReadBuffer(kStunMessage, kStunMessageLen);
+
+ unsigned char tmp[2048];
+ size_t len;
+ nr_transport_addr addr;
+
+ int r = nr_socket_recvfrom(test_socket_, tmp, sizeof(tmp), &len, 0, &addr);
+ ASSERT_EQ(0, r);
+ ASSERT_EQ(kStunMessageLen, len);
+ ASSERT_EQ(0, memcmp(kStunMessage, tmp, kStunMessageLen));
+ ASSERT_EQ(0, nr_transport_addr_cmp(&addr, &remote_addr_,
+ NR_TRANSPORT_ADDR_CMP_MODE_ALL));
+}
+
+TEST_F(BufferedStunSocketTest, TestReceiveRecvFromPartial) {
+ dummy_->SetReadBuffer(kStunMessage, 15);
+
+ unsigned char tmp[2048];
+ size_t len;
+ nr_transport_addr addr;
+
+ int r = nr_socket_recvfrom(test_socket_, tmp, sizeof(tmp), &len, 0, &addr);
+ ASSERT_EQ(R_WOULDBLOCK, r);
+
+ dummy_->SetReadBuffer(kStunMessage + 15, kStunMessageLen - 15);
+
+ r = nr_socket_recvfrom(test_socket_, tmp, sizeof(tmp), &len, 0, &addr);
+ ASSERT_EQ(0, r);
+ ASSERT_EQ(kStunMessageLen, len);
+ ASSERT_EQ(0, memcmp(kStunMessage, tmp, kStunMessageLen));
+ ASSERT_EQ(0, nr_transport_addr_cmp(&addr, &remote_addr_,
+ NR_TRANSPORT_ADDR_CMP_MODE_ALL));
+
+ r = nr_socket_recvfrom(test_socket_, tmp, sizeof(tmp), &len, 0, &addr);
+ ASSERT_EQ(R_WOULDBLOCK, r);
+}
+
+TEST_F(BufferedStunSocketTest, TestReceiveRecvFromGarbage) {
+ uint8_t garbage[50];
+ memset(garbage, 0xff, sizeof(garbage));
+
+ dummy_->SetReadBuffer(garbage, sizeof(garbage));
+
+ unsigned char tmp[2048];
+ size_t len;
+ nr_transport_addr addr;
+ int r = nr_socket_recvfrom(test_socket_, tmp, sizeof(tmp), &len, 0, &addr);
+ ASSERT_EQ(R_BAD_DATA, r);
+
+ r = nr_socket_recvfrom(test_socket_, tmp, sizeof(tmp), &len, 0, &addr);
+ ASSERT_EQ(R_FAILED, r);
+}
+
+TEST_F(BufferedStunSocketTest, TestReceiveRecvFromTooShort) {
+ dummy_->SetReadBuffer(kStunMessage, kStunMessageLen);
+
+ unsigned char tmp[2048];
+ size_t len;
+ nr_transport_addr addr;
+
+ int r = nr_socket_recvfrom(test_socket_, tmp, kStunMessageLen - 1, &len, 0,
+ &addr);
+ ASSERT_EQ(R_BAD_ARGS, r);
+}
+
+TEST_F(BufferedStunSocketTest, TestReceiveRecvFromReallyLong) {
+ uint8_t garbage[4096];
+ memset(garbage, 0xff, sizeof(garbage));
+ memcpy(garbage, kStunMessage, kStunMessageLen);
+ nr_stun_message_header* hdr =
+ reinterpret_cast<nr_stun_message_header*>(garbage);
+ hdr->length = htons(3000);
+
+ dummy_->SetReadBuffer(garbage, sizeof(garbage));
+
+ unsigned char tmp[4096];
+ size_t len;
+ nr_transport_addr addr;
+
+ int r = nr_socket_recvfrom(test_socket_, tmp, kStunMessageLen - 1, &len, 0,
+ &addr);
+ ASSERT_EQ(R_BAD_DATA, r);
+}
diff --git a/dom/media/webrtc/transport/test/dummysocket.h b/dom/media/webrtc/transport/test/dummysocket.h
new file mode 100644
index 0000000000..6e20a1f7e7
--- /dev/null
+++ b/dom/media/webrtc/transport/test/dummysocket.h
@@ -0,0 +1,217 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original authors: ekr@rtfm.com; ryan@tokbox.com
+
+#ifndef MTRANSPORT_DUMMY_SOCKET_H_
+#define MTRANSPORT_DUMMY_SOCKET_H_
+
+#include "nr_socket_prsock.h"
+
+extern "C" {
+#include "transport_addr.h"
+}
+
+#include "mediapacket.h"
+#include "mozilla/UniquePtr.h"
+
+#define GTEST_HAS_RTTI 0
+#include "gtest/gtest.h"
+#include "gtest_utils.h"
+
+namespace mozilla {
+
+static UniquePtr<MediaPacket> merge(UniquePtr<MediaPacket> a,
+ UniquePtr<MediaPacket> b) {
+ if (a && a->len() && b && b->len()) {
+ UniquePtr<uint8_t[]> data(new uint8_t[a->len() + b->len()]);
+ memcpy(data.get(), a->data(), a->len());
+ memcpy(data.get() + a->len(), b->data(), b->len());
+
+ UniquePtr<MediaPacket> merged(new MediaPacket);
+ merged->Take(std::move(data), a->len() + b->len());
+ return merged;
+ }
+
+ if (a && a->len()) {
+ return a;
+ }
+
+ if (b && b->len()) {
+ return b;
+ }
+
+ return nullptr;
+}
+
+class DummySocket : public NrSocketBase {
+ public:
+ DummySocket()
+ : writable_(UINT_MAX),
+ write_buffer_(nullptr),
+ readable_(UINT_MAX),
+ read_buffer_(nullptr),
+ cb_(nullptr),
+ cb_arg_(nullptr),
+ self_(nullptr) {}
+
+ // the nr_socket APIs
+ virtual int create(nr_transport_addr* addr) override { return 0; }
+
+ virtual int sendto(const void* msg, size_t len, int flags,
+ const nr_transport_addr* to) override {
+ MOZ_CRASH();
+ return 0;
+ }
+
+ virtual int recvfrom(void* buf, size_t maxlen, size_t* len, int flags,
+ nr_transport_addr* from) override {
+ MOZ_CRASH();
+ return 0;
+ }
+
+ virtual int getaddr(nr_transport_addr* addrp) override {
+ MOZ_CRASH();
+ return 0;
+ }
+
+ virtual void close() override {}
+
+ virtual int connect(const nr_transport_addr* addr) override {
+ nr_transport_addr_copy(&connect_addr_, addr);
+ return 0;
+ }
+
+ virtual int listen(int backlog) override { return 0; }
+
+ virtual int accept(nr_transport_addr* addrp, nr_socket** sockp) override {
+ return 0;
+ }
+
+ virtual int write(const void* msg, size_t len, size_t* written) override {
+ size_t to_write = std::min(len, writable_);
+
+ if (to_write) {
+ UniquePtr<MediaPacket> msgbuf(new MediaPacket);
+ msgbuf->Copy(static_cast<const uint8_t*>(msg), to_write);
+ write_buffer_ = merge(std::move(write_buffer_), std::move(msgbuf));
+ }
+
+ *written = to_write;
+
+ return 0;
+ }
+
+ virtual int read(void* buf, size_t maxlen, size_t* len) override {
+ if (!read_buffer_.get()) {
+ return R_WOULDBLOCK;
+ }
+
+ size_t to_read = std::min(read_buffer_->len(), std::min(maxlen, readable_));
+
+ memcpy(buf, read_buffer_->data(), to_read);
+ *len = to_read;
+
+ if (to_read < read_buffer_->len()) {
+ MediaPacket* newPacket = new MediaPacket;
+ newPacket->Copy(read_buffer_->data() + to_read,
+ read_buffer_->len() - to_read);
+ read_buffer_.reset(newPacket);
+ } else {
+ read_buffer_.reset();
+ }
+
+ return 0;
+ }
+
+ // Implementations of the async_event APIs.
+ // These are no-ops because we handle scheduling manually
+ // for test purposes.
+ virtual int async_wait(int how, NR_async_cb cb, void* cb_arg, char* function,
+ int line) override {
+ EXPECT_EQ(nullptr, cb_);
+ cb_ = cb;
+ cb_arg_ = cb_arg;
+
+ return 0;
+ }
+
+ virtual int cancel(int how) override {
+ cb_ = nullptr;
+ cb_arg_ = nullptr;
+
+ return 0;
+ }
+
+ // Read/Manipulate the current state.
+ void CheckWriteBuffer(const uint8_t* data, size_t len) {
+ if (!len) {
+ EXPECT_EQ(nullptr, write_buffer_.get());
+ } else {
+ EXPECT_NE(nullptr, write_buffer_.get());
+ ASSERT_EQ(len, write_buffer_->len());
+ ASSERT_EQ(0, memcmp(data, write_buffer_->data(), len));
+ }
+ }
+
+ void ClearWriteBuffer() { write_buffer_.reset(); }
+
+ void SetWritable(size_t val) { writable_ = val; }
+
+ void FireWritableCb() {
+ NR_async_cb cb = cb_;
+ void* cb_arg = cb_arg_;
+
+ cb_ = nullptr;
+ cb_arg_ = nullptr;
+
+ cb(this, NR_ASYNC_WAIT_WRITE, cb_arg);
+ }
+
+ void SetReadBuffer(const uint8_t* data, size_t len) {
+ EXPECT_EQ(nullptr, write_buffer_.get());
+ read_buffer_.reset(new MediaPacket);
+ read_buffer_->Copy(data, len);
+ }
+
+ void ClearReadBuffer() { read_buffer_.reset(); }
+
+ void SetReadable(size_t val) { readable_ = val; }
+
+ nr_socket* get_nr_socket() {
+ if (!self_) {
+ int r = nr_socket_create_int(this, vtbl(), &self_);
+ AddRef();
+ if (r) return nullptr;
+ }
+
+ return self_;
+ }
+
+ nr_transport_addr* get_connect_addr() { return &connect_addr_; }
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(DummySocket, override);
+
+ private:
+ ~DummySocket() = default;
+
+ DISALLOW_COPY_ASSIGN(DummySocket);
+
+ size_t writable_; // Amount we allow someone to write.
+ UniquePtr<MediaPacket> write_buffer_;
+ size_t readable_; // Amount we allow someone to read.
+ UniquePtr<MediaPacket> read_buffer_;
+
+ NR_async_cb cb_;
+ void* cb_arg_;
+ nr_socket* self_;
+
+ nr_transport_addr connect_addr_;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webrtc/transport/test/gtest_ringbuffer_dumper.h b/dom/media/webrtc/transport/test/gtest_ringbuffer_dumper.h
new file mode 100644
index 0000000000..25e85c2155
--- /dev/null
+++ b/dom/media/webrtc/transport/test/gtest_ringbuffer_dumper.h
@@ -0,0 +1,78 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: bcampen@mozilla.com
+
+#ifndef gtest_ringbuffer_dumper_h__
+#define gtest_ringbuffer_dumper_h__
+
+#include "mozilla/SyncRunnable.h"
+
+#define GTEST_HAS_RTTI 0
+#include "gtest/gtest.h"
+
+#include "mtransport_test_utils.h"
+#include "runnable_utils.h"
+#include "rlogconnector.h"
+
+using mozilla::RLogConnector;
+using mozilla::WrapRunnable;
+
+namespace test {
+class RingbufferDumper : public ::testing::EmptyTestEventListener {
+ public:
+ explicit RingbufferDumper(MtransportTestUtils* test_utils)
+ : test_utils_(test_utils) {}
+
+ void ClearRingBuffer_s() {
+ RLogConnector::CreateInstance();
+ // Set limit to zero to clear the ringbuffer
+ RLogConnector::GetInstance()->SetLogLimit(0);
+ RLogConnector::GetInstance()->SetLogLimit(UINT32_MAX);
+ }
+
+ void DestroyRingBuffer_s() { RLogConnector::DestroyInstance(); }
+
+ void DumpRingBuffer_s() {
+ std::deque<std::string> logs;
+ // Get an unlimited number of log lines, with no filter
+ RLogConnector::GetInstance()->GetAny(0, &logs);
+ for (auto l = logs.begin(); l != logs.end(); ++l) {
+ std::cout << *l << std::endl;
+ }
+ ClearRingBuffer_s();
+ }
+
+ virtual void OnTestStart(const ::testing::TestInfo& testInfo) override {
+ mozilla::SyncRunnable::DispatchToThread(
+ test_utils_->sts_target(),
+ WrapRunnable(this, &RingbufferDumper::ClearRingBuffer_s));
+ }
+
+ virtual void OnTestEnd(const ::testing::TestInfo& testInfo) override {
+ mozilla::SyncRunnable::DispatchToThread(
+ test_utils_->sts_target(),
+ WrapRunnable(this, &RingbufferDumper::DestroyRingBuffer_s));
+ }
+
+ // Called after a failed assertion or a SUCCEED() invocation.
+ virtual void OnTestPartResult(
+ const ::testing::TestPartResult& testResult) override {
+ if (testResult.failed()) {
+ // Dump (and empty) the RLogConnector
+ mozilla::SyncRunnable::DispatchToThread(
+ test_utils_->sts_target(),
+ WrapRunnable(this, &RingbufferDumper::DumpRingBuffer_s));
+ }
+ }
+
+ private:
+ MtransportTestUtils* test_utils_;
+};
+
+} // namespace test
+
+#endif // gtest_ringbuffer_dumper_h__
diff --git a/dom/media/webrtc/transport/test/gtest_utils.h b/dom/media/webrtc/transport/test/gtest_utils.h
new file mode 100644
index 0000000000..40c2570ea1
--- /dev/null
+++ b/dom/media/webrtc/transport/test/gtest_utils.h
@@ -0,0 +1,201 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Utilities to wrap gtest, based on libjingle's gunit
+
+// Some sections of this code are under the following license:
+
+/*
+ * libjingle
+ * Copyright 2004--2008, Google Inc.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright notice,
+ * this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ * 3. The name of the author may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
+ * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Original author: ekr@rtfm.com
+#ifndef gtest_utils__h__
+#define gtest_utils__h__
+
+#include <iostream>
+
+#include "nspr.h"
+#include "prinrval.h"
+#include "prthread.h"
+
+#define GTEST_HAS_RTTI 0
+#include "gtest/gtest.h"
+
+#include "gtest_ringbuffer_dumper.h"
+#include "mtransport_test_utils.h"
+#include "nss.h"
+#include "ssl.h"
+
+extern "C" {
+#include "registry.h"
+#include "transport_addr.h"
+}
+
+// Wait up to timeout seconds for expression to be true
+#define WAIT(expression, timeout) \
+ do { \
+ for (PRIntervalTime start = PR_IntervalNow(); \
+ !(expression) && !((PR_IntervalNow() - start) > \
+ PR_MillisecondsToInterval(timeout));) { \
+ PR_Sleep(10); \
+ } \
+ } while (0)
+
+// Same as GTEST_WAIT, but stores the result in res. Used when
+// you also want the result of expression but wish to avoid
+// double evaluation.
+#define WAIT_(expression, timeout, res) \
+ do { \
+ for (PRIntervalTime start = PR_IntervalNow(); \
+ !(res = (expression)) && !((PR_IntervalNow() - start) > \
+ PR_MillisecondsToInterval(timeout));) { \
+ PR_Sleep(10); \
+ } \
+ } while (0)
+
+#define ASSERT_TRUE_WAIT(expression, timeout) \
+ do { \
+ bool res; \
+ WAIT_(expression, timeout, res); \
+ ASSERT_TRUE(res); \
+ } while (0)
+
+#define EXPECT_TRUE_WAIT(expression, timeout) \
+ do { \
+ bool res; \
+ WAIT_(expression, timeout, res); \
+ EXPECT_TRUE(res); \
+ } while (0)
+
+#define ASSERT_EQ_WAIT(expected, actual, timeout) \
+ do { \
+ WAIT(expected == actual, timeout); \
+ ASSERT_EQ(expected, actual); \
+ } while (0)
+
+using test::RingbufferDumper;
+
+class MtransportTest : public ::testing::Test {
+ public:
+ MtransportTest() : test_utils_(nullptr), dumper_(nullptr) {}
+
+ void SetUp() override {
+ test_utils_ = new MtransportTestUtils();
+ NSS_NoDB_Init(nullptr);
+ NSS_SetDomesticPolicy();
+
+ NR_reg_init(NR_REG_MODE_LOCAL);
+
+ // Attempt to load env vars used by tests.
+ GetEnvironment("TURN_SERVER_ADDRESS", turn_server_);
+ GetEnvironment("TURN_SERVER_USER", turn_user_);
+ GetEnvironment("TURN_SERVER_PASSWORD", turn_password_);
+ GetEnvironment("STUN_SERVER_ADDRESS", stun_server_address_);
+ GetEnvironment("STUN_SERVER_HOSTNAME", stun_server_hostname_);
+
+ std::string disable_non_local;
+ GetEnvironment("MOZ_DISABLE_NONLOCAL_CONNECTIONS", disable_non_local);
+ std::string upload_dir;
+ GetEnvironment("MOZ_UPLOAD_DIR", upload_dir);
+
+ if ((!disable_non_local.empty() && disable_non_local != "0") ||
+ !upload_dir.empty()) {
+ // We're assuming that MOZ_UPLOAD_DIR is only set on tbpl;
+ // MOZ_DISABLE_NONLOCAL_CONNECTIONS probably should be set when running
+ // the cpp unit-tests, but is not presently.
+ stun_server_address_ = "";
+ stun_server_hostname_ = "";
+ turn_server_ = "";
+ }
+
+ // Some tests are flaky and need to check if they're supposed to run.
+ webrtc_enabled_ = CheckEnvironmentFlag("MOZ_WEBRTC_TESTS");
+
+ ::testing::TestEventListeners& listeners =
+ ::testing::UnitTest::GetInstance()->listeners();
+
+ dumper_ = new RingbufferDumper(test_utils_);
+ listeners.Append(dumper_);
+ }
+
+ void TearDown() override {
+ ::testing::UnitTest::GetInstance()->listeners().Release(dumper_);
+ delete dumper_;
+ delete test_utils_;
+ }
+
+ void GetEnvironment(const char* aVar, std::string& out) {
+ char* value = getenv(aVar);
+ if (value) {
+ out = value;
+ }
+ }
+
+ bool CheckEnvironmentFlag(const char* aVar) {
+ std::string value;
+ GetEnvironment(aVar, value);
+ return value == "1";
+ }
+
+ bool WarnIfTurnNotConfigured() const {
+ bool configured =
+ !turn_server_.empty() && !turn_user_.empty() && !turn_password_.empty();
+
+ if (configured) {
+ nr_transport_addr addr;
+ if (nr_str_port_to_transport_addr(turn_server_.c_str(), 3478, IPPROTO_UDP,
+ &addr)) {
+ printf(
+ "Invalid TURN_SERVER_ADDRESS \"%s\". Only IP numbers supported.\n",
+ turn_server_.c_str());
+ configured = false;
+ }
+ } else {
+ printf(
+ "Set TURN_SERVER_ADDRESS, TURN_SERVER_USER, and "
+ "TURN_SERVER_PASSWORD\n"
+ "environment variables to run this test\n");
+ }
+
+ return !configured;
+ }
+
+ MtransportTestUtils* test_utils_;
+ RingbufferDumper* dumper_;
+
+ std::string turn_server_;
+ std::string turn_user_;
+ std::string turn_password_;
+ std::string stun_server_address_;
+ std::string stun_server_hostname_;
+
+ bool webrtc_enabled_;
+};
+#endif
diff --git a/dom/media/webrtc/transport/test/ice_unittest.cpp b/dom/media/webrtc/transport/test/ice_unittest.cpp
new file mode 100644
index 0000000000..d87fa0b0da
--- /dev/null
+++ b/dom/media/webrtc/transport/test/ice_unittest.cpp
@@ -0,0 +1,4400 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+#include <algorithm>
+#include <deque>
+#include <iostream>
+#include <limits>
+#include <map>
+#include <string>
+#include <vector>
+
+#include "sigslot.h"
+
+#include "logging.h"
+#include "ssl.h"
+
+#include "mozilla/Preferences.h"
+#include "nsThreadUtils.h"
+#include "nsXPCOM.h"
+
+extern "C" {
+#include "r_types.h"
+#include "async_wait.h"
+#include "async_timer.h"
+#include "r_data.h"
+#include "util.h"
+#include "r_time.h"
+}
+
+#include "ice_ctx.h"
+#include "ice_peer_ctx.h"
+#include "ice_media_stream.h"
+
+#include "nricectx.h"
+#include "nricemediastream.h"
+#include "nriceresolverfake.h"
+#include "nriceresolver.h"
+#include "nrinterfaceprioritizer.h"
+#include "gtest_ringbuffer_dumper.h"
+#include "rlogconnector.h"
+#include "runnable_utils.h"
+#include "stunserver.h"
+#include "nr_socket_prsock.h"
+#include "test_nr_socket.h"
+#include "nsISocketFilter.h"
+#include "mozilla/net/DNS.h"
+
+#define GTEST_HAS_RTTI 0
+#include "gtest/gtest.h"
+#include "gtest_utils.h"
+
+using namespace mozilla;
+
+static unsigned int kDefaultTimeout = 7000;
+
+// TODO(nils@mozilla.com): This should get replaced with some non-external
+// solution like discussed in bug 860775.
+const std::string kDefaultStunServerHostname((char*)"stun.l.google.com");
+const std::string kBogusStunServerHostname(
+ (char*)"stun-server-nonexistent.invalid");
+const uint16_t kDefaultStunServerPort = 19305;
+const std::string kBogusIceCandidate(
+ (char*)"candidate:0 2 UDP 2113601790 192.168.178.20 50769 typ");
+
+const std::string kUnreachableHostIceCandidate(
+ (char*)"candidate:0 1 UDP 2113601790 192.168.178.20 50769 typ host");
+
+namespace {
+
+// DNS resolution helper code
+static std::string Resolve(const std::string& fqdn, int address_family) {
+ struct addrinfo hints;
+ memset(&hints, 0, sizeof(hints));
+ hints.ai_family = address_family;
+ hints.ai_protocol = IPPROTO_UDP;
+ struct addrinfo* res;
+ int err = getaddrinfo(fqdn.c_str(), nullptr, &hints, &res);
+ if (err) {
+ std::cerr << "Error in getaddrinfo: " << err << std::endl;
+ return "";
+ }
+
+ char str_addr[64] = {0};
+ switch (res->ai_family) {
+ case AF_INET:
+ inet_ntop(AF_INET,
+ &reinterpret_cast<struct sockaddr_in*>(res->ai_addr)->sin_addr,
+ str_addr, sizeof(str_addr));
+ break;
+ case AF_INET6:
+ inet_ntop(
+ AF_INET6,
+ &reinterpret_cast<struct sockaddr_in6*>(res->ai_addr)->sin6_addr,
+ str_addr, sizeof(str_addr));
+ break;
+ default:
+ std::cerr << "Got unexpected address family in DNS lookup: "
+ << res->ai_family << std::endl;
+ freeaddrinfo(res);
+ return "";
+ }
+
+ if (!strlen(str_addr)) {
+ std::cerr << "inet_ntop failed" << std::endl;
+ }
+
+ freeaddrinfo(res);
+ return str_addr;
+}
+
+class StunTest : public MtransportTest {
+ public:
+ StunTest() : MtransportTest() {}
+
+ void SetUp() override {
+ MtransportTest::SetUp();
+
+ stun_server_hostname_ = kDefaultStunServerHostname;
+ // If only a STUN server FQDN was provided, look up its IP address for the
+ // address-only tests.
+ if (stun_server_address_.empty() && !stun_server_hostname_.empty()) {
+ stun_server_address_ = Resolve(stun_server_hostname_, AF_INET);
+ ASSERT_TRUE(!stun_server_address_.empty());
+ }
+
+ // Make sure NrIceCtx is in a testable state.
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnableNM(&NrIceCtx::internal_DeinitializeGlobal));
+
+ // NB: NrIceCtx::internal_DeinitializeGlobal destroys the RLogConnector
+ // singleton.
+ RLogConnector::CreateInstance();
+
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnableNM(&TestStunServer::GetInstance, AF_INET));
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnableNM(&TestStunServer::GetInstance, AF_INET6));
+
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnableNM(&TestStunTcpServer::GetInstance, AF_INET));
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnableNM(&TestStunTcpServer::GetInstance, AF_INET6));
+ }
+
+ void TearDown() override {
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnableNM(&NrIceCtx::internal_DeinitializeGlobal));
+
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnableNM(&TestStunServer::ShutdownInstance));
+
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnableNM(&TestStunTcpServer::ShutdownInstance));
+
+ RLogConnector::DestroyInstance();
+
+ MtransportTest::TearDown();
+ }
+};
+
+enum TrickleMode { TRICKLE_NONE, TRICKLE_SIMULATE, TRICKLE_REAL };
+
+enum ConsentStatus { CONSENT_FRESH, CONSENT_STALE, CONSENT_EXPIRED };
+
+typedef std::string (*CandidateFilter)(const std::string& candidate);
+
+std::vector<std::string> split(const std::string& s, char delim) {
+ std::vector<std::string> elems;
+ std::stringstream ss(s);
+ std::string item;
+ while (std::getline(ss, item, delim)) {
+ elems.push_back(item);
+ }
+ return elems;
+}
+
+static std::string IsSrflxCandidate(const std::string& candidate) {
+ std::vector<std::string> tokens = split(candidate, ' ');
+ if ((tokens.at(6) == "typ") && (tokens.at(7) == "srflx")) {
+ return candidate;
+ }
+ return std::string();
+}
+
+static std::string IsRelayCandidate(const std::string& candidate) {
+ if (candidate.find("typ relay") != std::string::npos) {
+ return candidate;
+ }
+ return std::string();
+}
+
+static std::string IsTcpCandidate(const std::string& candidate) {
+ if (candidate.find("TCP") != std::string::npos) {
+ return candidate;
+ }
+ return std::string();
+}
+
+static std::string IsTcpSoCandidate(const std::string& candidate) {
+ if (candidate.find("tcptype so") != std::string::npos) {
+ return candidate;
+ }
+ return std::string();
+}
+
+static std::string IsLoopbackCandidate(const std::string& candidate) {
+ if (candidate.find("127.0.0.") != std::string::npos) {
+ return candidate;
+ }
+ return std::string();
+}
+
+static std::string IsIpv4Candidate(const std::string& candidate) {
+ std::vector<std::string> tokens = split(candidate, ' ');
+ if (tokens.at(4).find(':') == std::string::npos) {
+ return candidate;
+ }
+ return std::string();
+}
+
+static std::string SabotageHostCandidateAndDropReflexive(
+ const std::string& candidate) {
+ if (candidate.find("typ srflx") != std::string::npos) {
+ return std::string();
+ }
+
+ if (candidate.find("typ host") != std::string::npos) {
+ return kUnreachableHostIceCandidate;
+ }
+
+ return candidate;
+}
+
+bool ContainsSucceededPair(const std::vector<NrIceCandidatePair>& pairs) {
+ for (const auto& pair : pairs) {
+ if (pair.state == NrIceCandidatePair::STATE_SUCCEEDED) {
+ return true;
+ }
+ }
+ return false;
+}
+
+// Note: Does not correspond to any notion of prioritization; this is just
+// so we can use stl containers/algorithms that need a comparator
+bool operator<(const NrIceCandidate& lhs, const NrIceCandidate& rhs) {
+ if (lhs.cand_addr.host == rhs.cand_addr.host) {
+ if (lhs.cand_addr.port == rhs.cand_addr.port) {
+ if (lhs.cand_addr.transport == rhs.cand_addr.transport) {
+ if (lhs.type == rhs.type) {
+ return lhs.tcp_type < rhs.tcp_type;
+ }
+ return lhs.type < rhs.type;
+ }
+ return lhs.cand_addr.transport < rhs.cand_addr.transport;
+ }
+ return lhs.cand_addr.port < rhs.cand_addr.port;
+ }
+ return lhs.cand_addr.host < rhs.cand_addr.host;
+}
+
+bool operator==(const NrIceCandidate& lhs, const NrIceCandidate& rhs) {
+ return !((lhs < rhs) || (rhs < lhs));
+}
+
+class IceCandidatePairCompare {
+ public:
+ bool operator()(const NrIceCandidatePair& lhs,
+ const NrIceCandidatePair& rhs) const {
+ if (lhs.priority == rhs.priority) {
+ if (lhs.local == rhs.local) {
+ if (lhs.remote == rhs.remote) {
+ return lhs.codeword < rhs.codeword;
+ }
+ return lhs.remote < rhs.remote;
+ }
+ return lhs.local < rhs.local;
+ }
+ return lhs.priority < rhs.priority;
+ }
+};
+
+class IceTestPeer;
+
+class SchedulableTrickleCandidate {
+ public:
+ SchedulableTrickleCandidate(IceTestPeer* peer, size_t stream,
+ const std::string& candidate,
+ const std::string& ufrag,
+ MtransportTestUtils* utils)
+ : peer_(peer),
+ stream_(stream),
+ candidate_(candidate),
+ ufrag_(ufrag),
+ timer_handle_(nullptr),
+ test_utils_(utils) {}
+
+ ~SchedulableTrickleCandidate() {
+ if (timer_handle_) NR_async_timer_cancel(timer_handle_);
+ }
+
+ void Schedule(unsigned int ms) {
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(this, &SchedulableTrickleCandidate::Schedule_s, ms));
+ }
+
+ void Schedule_s(unsigned int ms) {
+ MOZ_ASSERT(!timer_handle_);
+ NR_ASYNC_TIMER_SET(ms, Trickle_cb, this, &timer_handle_);
+ }
+
+ static void Trickle_cb(NR_SOCKET s, int how, void* cb_arg) {
+ static_cast<SchedulableTrickleCandidate*>(cb_arg)->Trickle();
+ }
+
+ void Trickle();
+
+ std::string& Candidate() { return candidate_; }
+
+ const std::string& Candidate() const { return candidate_; }
+
+ bool IsHost() const {
+ return candidate_.find("typ host") != std::string::npos;
+ }
+
+ bool IsReflexive() const {
+ return candidate_.find("typ srflx") != std::string::npos;
+ }
+
+ bool IsRelay() const {
+ return candidate_.find("typ relay") != std::string::npos;
+ }
+
+ private:
+ IceTestPeer* peer_;
+ size_t stream_;
+ std::string candidate_;
+ std::string ufrag_;
+ void* timer_handle_;
+ MtransportTestUtils* test_utils_;
+
+ DISALLOW_COPY_ASSIGN(SchedulableTrickleCandidate);
+};
+
+class IceTestPeer : public sigslot::has_slots<> {
+ public:
+ IceTestPeer(const std::string& name, MtransportTestUtils* utils, bool offerer,
+ const NrIceCtx::Config& config)
+ : name_(name),
+ ice_ctx_(NrIceCtx::Create(name)),
+ offerer_(offerer),
+ candidates_(),
+ stream_counter_(0),
+ shutting_down_(false),
+ gathering_complete_(false),
+ ready_ct_(0),
+ ice_connected_(false),
+ ice_failed_(false),
+ ice_reached_checking_(false),
+ received_(0),
+ sent_(0),
+ fake_resolver_(),
+ dns_resolver_(new NrIceResolver()),
+ remote_(nullptr),
+ candidate_filter_(nullptr),
+ expected_local_type_(NrIceCandidate::ICE_HOST),
+ expected_local_transport_(kNrIceTransportUdp),
+ expected_remote_type_(NrIceCandidate::ICE_HOST),
+ trickle_mode_(TRICKLE_NONE),
+ simulate_ice_lite_(false),
+ nat_(new TestNat),
+ test_utils_(utils) {
+ ice_ctx_->SignalGatheringStateChange.connect(
+ this, &IceTestPeer::GatheringStateChange);
+ ice_ctx_->SignalConnectionStateChange.connect(
+ this, &IceTestPeer::ConnectionStateChange);
+
+ ice_ctx_->SetIceConfig(config);
+
+ consent_timestamp_.tv_sec = 0;
+ consent_timestamp_.tv_usec = 0;
+ int r = ice_ctx_->SetNat(nat_);
+ (void)r;
+ MOZ_ASSERT(!r);
+ }
+
+ ~IceTestPeer() {
+ test_utils_->SyncDispatchToSTS(WrapRunnable(this, &IceTestPeer::Shutdown));
+
+ // Give the ICE destruction callback time to fire before
+ // we destroy the resolver.
+ PR_Sleep(1000);
+ }
+
+ std::string MakeTransportId(size_t index) const {
+ char id[100];
+ snprintf(id, sizeof(id), "%s:stream%d", name_.c_str(), (int)index);
+ return id;
+ }
+
+ void SetIceCredentials_s(NrIceMediaStream& stream) {
+ static size_t counter = 0;
+ std::ostringstream prefix;
+ prefix << name_ << "-" << counter++;
+ std::string ufrag = prefix.str() + "-ufrag";
+ std::string pwd = prefix.str() + "-pwd";
+ if (mIceCredentials.count(stream.GetId())) {
+ mOldIceCredentials[stream.GetId()] = mIceCredentials[stream.GetId()];
+ }
+ mIceCredentials[stream.GetId()] = std::make_pair(ufrag, pwd);
+ stream.SetIceCredentials(ufrag, pwd);
+ }
+
+ void AddStream_s(int components) {
+ std::string id = MakeTransportId(stream_counter_++);
+
+ RefPtr<NrIceMediaStream> stream =
+ ice_ctx_->CreateStream(id, id, components);
+
+ ASSERT_TRUE(stream);
+ SetIceCredentials_s(*stream);
+
+ stream->SignalCandidate.connect(this, &IceTestPeer::CandidateInitialized);
+ stream->SignalReady.connect(this, &IceTestPeer::StreamReady);
+ stream->SignalFailed.connect(this, &IceTestPeer::StreamFailed);
+ stream->SignalPacketReceived.connect(this, &IceTestPeer::PacketReceived);
+ }
+
+ void AddStream(int components) {
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(this, &IceTestPeer::AddStream_s, components));
+ }
+
+ void RemoveStream_s(size_t index) {
+ ice_ctx_->DestroyStream(MakeTransportId(index));
+ }
+
+ void RemoveStream(size_t index) {
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(this, &IceTestPeer::RemoveStream_s, index));
+ }
+
+ RefPtr<NrIceMediaStream> GetStream_s(size_t index) {
+ std::string id = MakeTransportId(index);
+ return ice_ctx_->GetStream(id);
+ }
+
+ void SetStunServer(const std::string addr, uint16_t port,
+ const char* transport = kNrIceTransportUdp) {
+ if (addr.empty()) {
+ // Happens when MOZ_DISABLE_NONLOCAL_CONNECTIONS is set
+ return;
+ }
+
+ std::vector<NrIceStunServer> stun_servers;
+ UniquePtr<NrIceStunServer> server(
+ NrIceStunServer::Create(addr, port, transport));
+ stun_servers.push_back(*server);
+ SetStunServers(stun_servers);
+ }
+
+ void SetStunServers(const std::vector<NrIceStunServer>& servers) {
+ ASSERT_TRUE(NS_SUCCEEDED(ice_ctx_->SetStunServers(servers)));
+ }
+
+ void UseTestStunServer() {
+ SetStunServer(TestStunServer::GetInstance(AF_INET)->addr(),
+ TestStunServer::GetInstance(AF_INET)->port());
+ }
+
+ void SetTurnServer(const std::string addr, uint16_t port,
+ const std::string username, const std::string password,
+ const char* transport) {
+ std::vector<unsigned char> password_vec(password.begin(), password.end());
+ SetTurnServer(addr, port, username, password_vec, transport);
+ }
+
+ void SetTurnServer(const std::string addr, uint16_t port,
+ const std::string username,
+ const std::vector<unsigned char> password,
+ const char* transport) {
+ std::vector<NrIceTurnServer> turn_servers;
+ UniquePtr<NrIceTurnServer> server(
+ NrIceTurnServer::Create(addr, port, username, password, transport));
+ turn_servers.push_back(*server);
+ ASSERT_TRUE(NS_SUCCEEDED(ice_ctx_->SetTurnServers(turn_servers)));
+ }
+
+ void SetTurnServers(const std::vector<NrIceTurnServer> servers) {
+ ASSERT_TRUE(NS_SUCCEEDED(ice_ctx_->SetTurnServers(servers)));
+ }
+
+ void SetFakeResolver(const std::string& ip, const std::string& fqdn) {
+ ASSERT_TRUE(NS_SUCCEEDED(dns_resolver_->Init()));
+ if (!ip.empty() && !fqdn.empty()) {
+ PRNetAddr addr;
+ PRStatus status = PR_StringToNetAddr(ip.c_str(), &addr);
+ addr.inet.port = kDefaultStunServerPort;
+ ASSERT_EQ(PR_SUCCESS, status);
+ fake_resolver_.SetAddr(fqdn, addr);
+ }
+ ASSERT_TRUE(
+ NS_SUCCEEDED(ice_ctx_->SetResolver(fake_resolver_.AllocateResolver())));
+ }
+
+ void SetDNSResolver() {
+ ASSERT_TRUE(NS_SUCCEEDED(dns_resolver_->Init()));
+ ASSERT_TRUE(
+ NS_SUCCEEDED(ice_ctx_->SetResolver(dns_resolver_->AllocateResolver())));
+ }
+
+ void Gather(bool default_route_only = false,
+ bool obfuscate_host_addresses = false) {
+ nsresult res;
+
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnableRet(&res, ice_ctx_, &NrIceCtx::StartGathering,
+ default_route_only, obfuscate_host_addresses));
+
+ ASSERT_TRUE(NS_SUCCEEDED(res));
+ }
+
+ void SetCtxFlags(bool default_route_only) {
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(ice_ctx_, &NrIceCtx::SetCtxFlags, default_route_only));
+ }
+
+ nsTArray<NrIceStunAddr> GetStunAddrs() { return ice_ctx_->GetStunAddrs(); }
+
+ void SetStunAddrs(const nsTArray<NrIceStunAddr>& addrs) {
+ ice_ctx_->SetStunAddrs(addrs);
+ }
+
+ void UseNat() { nat_->enabled_ = true; }
+
+ void SetTimerDivider(int div) { ice_ctx_->internal_SetTimerAccelarator(div); }
+
+ void SetStunResponseDelay(uint32_t delay) {
+ nat_->delay_stun_resp_ms_ = delay;
+ }
+
+ void SetFilteringType(TestNat::NatBehavior type) {
+ MOZ_ASSERT(!nat_->has_port_mappings());
+ nat_->filtering_type_ = type;
+ }
+
+ void SetMappingType(TestNat::NatBehavior type) {
+ MOZ_ASSERT(!nat_->has_port_mappings());
+ nat_->mapping_type_ = type;
+ }
+
+ void SetBlockUdp(bool block) {
+ MOZ_ASSERT(!nat_->has_port_mappings());
+ nat_->block_udp_ = block;
+ }
+
+ void SetBlockStun(bool block) { nat_->block_stun_ = block; }
+
+ // Get various pieces of state
+ std::vector<std::string> GetGlobalAttributes() {
+ std::vector<std::string> attrs(ice_ctx_->GetGlobalAttributes());
+ if (simulate_ice_lite_) {
+ attrs.push_back("ice-lite");
+ }
+ return attrs;
+ }
+
+ std::vector<std::string> GetAttributes(size_t stream) {
+ std::vector<std::string> v;
+
+ RUN_ON_THREAD(
+ test_utils_->sts_target(),
+ WrapRunnableRet(&v, this, &IceTestPeer::GetAttributes_s, stream));
+
+ return v;
+ }
+
+ std::string FilterCandidate(const std::string& candidate) {
+ if (candidate_filter_) {
+ return candidate_filter_(candidate);
+ }
+ return candidate;
+ }
+
+ std::vector<std::string> GetAttributes_s(size_t index) {
+ std::vector<std::string> attributes;
+
+ auto stream = GetStream_s(index);
+ if (!stream) {
+ EXPECT_TRUE(false) << "No such stream " << index;
+ return attributes;
+ }
+
+ std::vector<std::string> attributes_in = stream->GetAttributes();
+
+ for (const auto& attribute : attributes_in) {
+ if (attribute.find("candidate:") != std::string::npos) {
+ std::string candidate(FilterCandidate(attribute));
+ if (!candidate.empty()) {
+ std::cerr << name_ << " Returning candidate: " << candidate
+ << std::endl;
+ attributes.push_back(candidate);
+ }
+ } else {
+ attributes.push_back(attribute);
+ }
+ }
+
+ return attributes;
+ }
+
+ void SetExpectedTypes(NrIceCandidate::Type local, NrIceCandidate::Type remote,
+ std::string local_transport = kNrIceTransportUdp) {
+ expected_local_type_ = local;
+ expected_local_transport_ = local_transport;
+ expected_remote_type_ = remote;
+ }
+
+ void SetExpectedRemoteCandidateAddr(const std::string& addr) {
+ expected_remote_addr_ = addr;
+ }
+
+ int GetCandidatesPrivateIpv4Range(size_t stream) {
+ std::vector<std::string> attributes = GetAttributes(stream);
+
+ int host_net = 0;
+ for (const auto& a : attributes) {
+ if (a.find("typ host") != std::string::npos) {
+ nr_transport_addr addr;
+ std::vector<std::string> tokens = split(a, ' ');
+ int r = nr_str_port_to_transport_addr(tokens.at(4).c_str(), 0,
+ IPPROTO_UDP, &addr);
+ MOZ_ASSERT(!r);
+ if (!r && (addr.ip_version == NR_IPV4)) {
+ int n = nr_transport_addr_get_private_addr_range(&addr);
+ if (n) {
+ if (host_net) {
+ // TODO: add support for multiple private interfaces
+ std::cerr
+ << "This test doesn't support multiple private interfaces";
+ return -1;
+ }
+ host_net = n;
+ }
+ }
+ }
+ }
+ return host_net;
+ }
+
+ bool gathering_complete() { return gathering_complete_; }
+ int ready_ct() { return ready_ct_; }
+ bool is_ready_s(size_t index) {
+ auto media_stream = GetStream_s(index);
+ if (!media_stream) {
+ EXPECT_TRUE(false) << "No such stream " << index;
+ return false;
+ }
+ return media_stream->state() == NrIceMediaStream::ICE_OPEN;
+ }
+ bool is_ready(size_t stream) {
+ bool result;
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnableRet(&result, this, &IceTestPeer::is_ready_s, stream));
+ return result;
+ }
+ bool ice_connected() { return ice_connected_; }
+ bool ice_failed() { return ice_failed_; }
+ bool ice_reached_checking() { return ice_reached_checking_; }
+ size_t received() { return received_; }
+ size_t sent() { return sent_; }
+
+ void RestartIce() {
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(this, &IceTestPeer::RestartIce_s));
+ }
+
+ void RestartIce_s() {
+ for (auto& stream : ice_ctx_->GetStreams()) {
+ SetIceCredentials_s(*stream);
+ }
+ // take care of some local bookkeeping
+ ready_ct_ = 0;
+ gathering_complete_ = false;
+ ice_connected_ = false;
+ ice_failed_ = false;
+ ice_reached_checking_ = false;
+ remote_ = nullptr;
+ }
+
+ void RollbackIceRestart() {
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(this, &IceTestPeer::RollbackIceRestart_s));
+ }
+
+ void RollbackIceRestart_s() {
+ for (auto& stream : ice_ctx_->GetStreams()) {
+ mIceCredentials[stream->GetId()] = mOldIceCredentials[stream->GetId()];
+ }
+ }
+
+ // Start connecting to another peer
+ void Connect_s(IceTestPeer* remote, TrickleMode trickle_mode,
+ bool start = true) {
+ nsresult res;
+
+ remote_ = remote;
+
+ trickle_mode_ = trickle_mode;
+ ice_connected_ = false;
+ ice_failed_ = false;
+ ice_reached_checking_ = false;
+ res = ice_ctx_->ParseGlobalAttributes(remote->GetGlobalAttributes());
+ ASSERT_FALSE(remote->simulate_ice_lite_ &&
+ (ice_ctx_->GetControlling() == NrIceCtx::ICE_CONTROLLED));
+ ASSERT_TRUE(NS_SUCCEEDED(res));
+
+ for (size_t i = 0; i < stream_counter_; ++i) {
+ auto aStream = GetStream_s(i);
+ if (aStream) {
+ std::vector<std::string> attributes = remote->GetAttributes(i);
+
+ for (auto it = attributes.begin(); it != attributes.end();) {
+ if (trickle_mode == TRICKLE_SIMULATE &&
+ it->find("candidate:") != std::string::npos) {
+ std::cerr << name_ << " Deferring remote candidate: " << *it
+ << std::endl;
+ attributes.erase(it);
+ } else {
+ std::cerr << name_ << " Adding remote attribute: " + *it
+ << std::endl;
+ ++it;
+ }
+ }
+ auto credentials = mIceCredentials[aStream->GetId()];
+ res = aStream->ConnectToPeer(credentials.first, credentials.second,
+ attributes);
+ ASSERT_TRUE(NS_SUCCEEDED(res));
+ }
+ }
+
+ if (start) {
+ ice_ctx_->SetControlling(offerer_ ? NrIceCtx::ICE_CONTROLLING
+ : NrIceCtx::ICE_CONTROLLED);
+ // Now start checks
+ res = ice_ctx_->StartChecks();
+ ASSERT_TRUE(NS_SUCCEEDED(res));
+ }
+ }
+
+ void Connect(IceTestPeer* remote, TrickleMode trickle_mode,
+ bool start = true) {
+ test_utils_->SyncDispatchToSTS(WrapRunnable(this, &IceTestPeer::Connect_s,
+ remote, trickle_mode, start));
+ }
+
+ void SimulateTrickle(size_t stream) {
+ std::cerr << name_ << " Doing trickle for stream " << stream << std::endl;
+ // If we are in trickle deferred mode, now trickle in the candidates
+ // for |stream|
+
+ std::vector<SchedulableTrickleCandidate*>& candidates =
+ ControlTrickle(stream);
+
+ for (auto& candidate : candidates) {
+ candidate->Schedule(0);
+ }
+ }
+
+ // Allows test case to completely control when/if candidates are trickled
+ // (test could also do things like insert extra trickle candidates, or
+ // change existing ones, or insert duplicates, really anything is fair game)
+ std::vector<SchedulableTrickleCandidate*>& ControlTrickle(size_t stream) {
+ std::cerr << "Doing controlled trickle for stream " << stream << std::endl;
+
+ std::vector<std::string> attributes = remote_->GetAttributes(stream);
+
+ for (const auto& attribute : attributes) {
+ if (attribute.find("candidate:") != std::string::npos) {
+ controlled_trickle_candidates_[stream].push_back(
+ new SchedulableTrickleCandidate(this, stream, attribute, "",
+ test_utils_));
+ }
+ }
+
+ return controlled_trickle_candidates_[stream];
+ }
+
+ nsresult TrickleCandidate_s(const std::string& candidate,
+ const std::string& ufrag, size_t index) {
+ auto stream = GetStream_s(index);
+ if (!stream) {
+ // stream might have gone away before the trickle timer popped
+ return NS_OK;
+ }
+ return stream->ParseTrickleCandidate(candidate, ufrag, "");
+ }
+
+ void DumpCandidate(std::string which, const NrIceCandidate& cand) {
+ std::string type;
+ std::string tcp_type;
+
+ std::string addr;
+ int port;
+
+ if (which.find("Remote") != std::string::npos) {
+ addr = cand.cand_addr.host;
+ port = cand.cand_addr.port;
+ } else {
+ addr = cand.local_addr.host;
+ port = cand.local_addr.port;
+ }
+ switch (cand.type) {
+ case NrIceCandidate::ICE_HOST:
+ type = "host";
+ break;
+ case NrIceCandidate::ICE_SERVER_REFLEXIVE:
+ type = "srflx";
+ break;
+ case NrIceCandidate::ICE_PEER_REFLEXIVE:
+ type = "prflx";
+ break;
+ case NrIceCandidate::ICE_RELAYED:
+ type = "relay";
+ if (which.find("Local") != std::string::npos) {
+ type += "(" + cand.local_addr.transport + ")";
+ }
+ break;
+ default:
+ FAIL();
+ };
+
+ switch (cand.tcp_type) {
+ case NrIceCandidate::ICE_NONE:
+ break;
+ case NrIceCandidate::ICE_ACTIVE:
+ tcp_type = " tcptype=active";
+ break;
+ case NrIceCandidate::ICE_PASSIVE:
+ tcp_type = " tcptype=passive";
+ break;
+ case NrIceCandidate::ICE_SO:
+ tcp_type = " tcptype=so";
+ break;
+ default:
+ FAIL();
+ };
+
+ std::cerr << which << " --> " << type << " " << addr << ":" << port << "/"
+ << cand.cand_addr.transport << tcp_type
+ << " codeword=" << cand.codeword << std::endl;
+ }
+
+ void DumpAndCheckActiveCandidates_s() {
+ std::cerr << name_ << " Active candidates:" << std::endl;
+ for (const auto& stream : ice_ctx_->GetStreams()) {
+ for (size_t j = 0; j < stream->components(); ++j) {
+ std::cerr << name_ << " Stream " << stream->GetId() << " component "
+ << j + 1 << std::endl;
+
+ UniquePtr<NrIceCandidate> local;
+ UniquePtr<NrIceCandidate> remote;
+
+ nsresult res = stream->GetActivePair(j + 1, &local, &remote);
+ if (res == NS_ERROR_NOT_AVAILABLE) {
+ std::cerr << "Component unpaired or disabled." << std::endl;
+ } else {
+ ASSERT_TRUE(NS_SUCCEEDED(res));
+ DumpCandidate("Local ", *local);
+ /* Depending on timing, and the whims of the network
+ * stack/configuration we're running on top of, prflx is always a
+ * possibility. */
+ if (expected_local_type_ == NrIceCandidate::ICE_HOST) {
+ ASSERT_NE(NrIceCandidate::ICE_SERVER_REFLEXIVE, local->type);
+ ASSERT_NE(NrIceCandidate::ICE_RELAYED, local->type);
+ } else {
+ ASSERT_EQ(expected_local_type_, local->type);
+ }
+ ASSERT_EQ(expected_local_transport_, local->local_addr.transport);
+ DumpCandidate("Remote ", *remote);
+ /* Depending on timing, and the whims of the network
+ * stack/configuration we're running on top of, prflx is always a
+ * possibility. */
+ if (expected_remote_type_ == NrIceCandidate::ICE_HOST) {
+ ASSERT_NE(NrIceCandidate::ICE_SERVER_REFLEXIVE, remote->type);
+ ASSERT_NE(NrIceCandidate::ICE_RELAYED, remote->type);
+ } else {
+ ASSERT_EQ(expected_remote_type_, remote->type);
+ }
+ if (!expected_remote_addr_.empty()) {
+ ASSERT_EQ(expected_remote_addr_, remote->cand_addr.host);
+ }
+ }
+ }
+ }
+ }
+
+ void DumpAndCheckActiveCandidates() {
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(this, &IceTestPeer::DumpAndCheckActiveCandidates_s));
+ }
+
+ void Close() {
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(ice_ctx_, &NrIceCtx::destroy_peer_ctx));
+ }
+
+ void Shutdown() {
+ std::cerr << name_ << " Shutdown" << std::endl;
+ shutting_down_ = true;
+ for (auto& controlled_trickle_candidate : controlled_trickle_candidates_) {
+ for (auto& cand : controlled_trickle_candidate.second) {
+ delete cand;
+ }
+ }
+
+ ice_ctx_->Destroy();
+ ice_ctx_ = nullptr;
+
+ if (remote_) {
+ remote_->UnsetRemote();
+ remote_ = nullptr;
+ }
+ }
+
+ void UnsetRemote() { remote_ = nullptr; }
+
+ void StartChecks() {
+ nsresult res;
+
+ test_utils_->SyncDispatchToSTS(WrapRunnableRet(
+ &res, ice_ctx_, &NrIceCtx::SetControlling,
+ offerer_ ? NrIceCtx::ICE_CONTROLLING : NrIceCtx::ICE_CONTROLLED));
+ // Now start checks
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnableRet(&res, ice_ctx_, &NrIceCtx::StartChecks));
+ ASSERT_TRUE(NS_SUCCEEDED(res));
+ }
+
+ // Handle events
+ void GatheringStateChange(NrIceCtx* ctx, NrIceCtx::GatheringState state) {
+ if (shutting_down_) {
+ return;
+ }
+ if (state != NrIceCtx::ICE_CTX_GATHER_COMPLETE) {
+ return;
+ }
+
+ std::cerr << name_ << " Gathering complete" << std::endl;
+ gathering_complete_ = true;
+
+ std::cerr << name_ << " ATTRIBUTES:" << std::endl;
+ for (const auto& stream : ice_ctx_->GetStreams()) {
+ std::cerr << "Stream " << stream->GetId() << std::endl;
+
+ std::vector<std::string> attributes = stream->GetAttributes();
+
+ for (const auto& attribute : attributes) {
+ std::cerr << attribute << std::endl;
+ }
+ }
+ std::cerr << std::endl;
+ }
+
+ void CandidateInitialized(NrIceMediaStream* stream,
+ const std::string& raw_candidate,
+ const std::string& ufrag,
+ const std::string& mdns_addr,
+ const std::string& actual_addr) {
+ std::string candidate(FilterCandidate(raw_candidate));
+ if (candidate.empty()) {
+ return;
+ }
+ std::cerr << "Candidate for stream " << stream->name()
+ << " initialized: " << candidate << std::endl;
+ candidates_[stream->name()].push_back(candidate);
+
+ // If we are connected, then try to trickle to the other side.
+ if (remote_ && remote_->remote_ && (trickle_mode_ != TRICKLE_SIMULATE)) {
+ // first, find the index of the stream we've been given so
+ // we can get the corresponding stream on the remote side
+ for (size_t i = 0; i < stream_counter_; ++i) {
+ if (GetStream_s(i) == stream) {
+ ASSERT_GT(remote_->stream_counter_, i);
+ nsresult res = remote_->GetStream_s(i)->ParseTrickleCandidate(
+ candidate, ufrag, "");
+ ASSERT_TRUE(NS_SUCCEEDED(res));
+ return;
+ }
+ }
+ ADD_FAILURE() << "No matching stream found for " << stream;
+ }
+ }
+
+ nsresult GetCandidatePairs_s(size_t stream_index,
+ std::vector<NrIceCandidatePair>* pairs) {
+ MOZ_ASSERT(pairs);
+ auto stream = GetStream_s(stream_index);
+ if (!stream) {
+ // Is there a better error for "no such index"?
+ ADD_FAILURE() << "No such media stream index: " << stream_index;
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ return stream->GetCandidatePairs(pairs);
+ }
+
+ nsresult GetCandidatePairs(size_t stream_index,
+ std::vector<NrIceCandidatePair>* pairs) {
+ nsresult v;
+ test_utils_->SyncDispatchToSTS(WrapRunnableRet(
+ &v, this, &IceTestPeer::GetCandidatePairs_s, stream_index, pairs));
+ return v;
+ }
+
+ void DumpCandidatePair(const NrIceCandidatePair& pair) {
+ std::cerr << std::endl;
+ DumpCandidate("Local", pair.local);
+ DumpCandidate("Remote", pair.remote);
+ std::cerr << "state = " << pair.state << " priority = " << pair.priority
+ << " nominated = " << pair.nominated
+ << " selected = " << pair.selected
+ << " codeword = " << pair.codeword << std::endl;
+ }
+
+ void DumpCandidatePairs_s(NrIceMediaStream* stream) {
+ std::vector<NrIceCandidatePair> pairs;
+ nsresult res = stream->GetCandidatePairs(&pairs);
+ ASSERT_TRUE(NS_SUCCEEDED(res));
+
+ std::cerr << "Begin list of candidate pairs [" << std::endl;
+
+ for (auto& pair : pairs) {
+ DumpCandidatePair(pair);
+ }
+ std::cerr << "]" << std::endl;
+ }
+
+ void DumpCandidatePairs_s() {
+ std::cerr << "Dumping candidate pairs for all streams [" << std::endl;
+ for (const auto& stream : ice_ctx_->GetStreams()) {
+ DumpCandidatePairs_s(stream.get());
+ }
+ std::cerr << "]" << std::endl;
+ }
+
+ bool CandidatePairsPriorityDescending(
+ const std::vector<NrIceCandidatePair>& pairs) {
+ // Verify that priority is descending
+ uint64_t priority = std::numeric_limits<uint64_t>::max();
+
+ for (size_t p = 0; p < pairs.size(); ++p) {
+ if (priority < pairs[p].priority) {
+ std::cerr << "Priority increased in subsequent pairs:" << std::endl;
+ DumpCandidatePair(pairs[p - 1]);
+ DumpCandidatePair(pairs[p]);
+ return false;
+ }
+ if (priority == pairs[p].priority) {
+ if (!IceCandidatePairCompare()(pairs[p], pairs[p - 1]) &&
+ !IceCandidatePairCompare()(pairs[p - 1], pairs[p])) {
+ std::cerr << "Ignoring identical pair from trigger check"
+ << std::endl;
+ } else {
+ std::cerr << "Duplicate priority in subseqent pairs:" << std::endl;
+ DumpCandidatePair(pairs[p - 1]);
+ DumpCandidatePair(pairs[p]);
+ return false;
+ }
+ }
+ priority = pairs[p].priority;
+ }
+ return true;
+ }
+
+ void UpdateAndValidateCandidatePairs(
+ size_t stream_index, std::vector<NrIceCandidatePair>* new_pairs) {
+ std::vector<NrIceCandidatePair> old_pairs = *new_pairs;
+ GetCandidatePairs(stream_index, new_pairs);
+ ASSERT_TRUE(CandidatePairsPriorityDescending(*new_pairs))
+ << "New list of "
+ "candidate pairs is either not sorted in priority order, or has "
+ "duplicate priorities.";
+ ASSERT_TRUE(CandidatePairsPriorityDescending(old_pairs))
+ << "Old list of "
+ "candidate pairs is either not sorted in priority order, or has "
+ "duplicate priorities. This indicates some bug in the test case.";
+ std::vector<NrIceCandidatePair> added_pairs;
+ std::vector<NrIceCandidatePair> removed_pairs;
+
+ // set_difference computes the set of elements that are present in the
+ // first set, but not the second
+ // NrIceCandidatePair::operator< compares based on the priority, local
+ // candidate, and remote candidate in that order. This means this will
+ // catch cases where the priority has remained the same, but one of the
+ // candidates has changed.
+ std::set_difference((*new_pairs).begin(), (*new_pairs).end(),
+ old_pairs.begin(), old_pairs.end(),
+ std::inserter(added_pairs, added_pairs.begin()),
+ IceCandidatePairCompare());
+
+ std::set_difference(old_pairs.begin(), old_pairs.end(),
+ (*new_pairs).begin(), (*new_pairs).end(),
+ std::inserter(removed_pairs, removed_pairs.begin()),
+ IceCandidatePairCompare());
+
+ for (auto& added_pair : added_pairs) {
+ std::cerr << "Found new candidate pair." << std::endl;
+ DumpCandidatePair(added_pair);
+ }
+
+ for (auto& removed_pair : removed_pairs) {
+ std::cerr << "Pre-existing candidate pair is now missing:" << std::endl;
+ DumpCandidatePair(removed_pair);
+ }
+
+ ASSERT_TRUE(removed_pairs.empty())
+ << "At least one candidate pair has "
+ "gone missing.";
+ }
+
+ void StreamReady(NrIceMediaStream* stream) {
+ ++ready_ct_;
+ std::cerr << name_ << " Stream ready for " << stream->name()
+ << " ct=" << ready_ct_ << std::endl;
+ DumpCandidatePairs_s(stream);
+ }
+ void StreamFailed(NrIceMediaStream* stream) {
+ std::cerr << name_ << " Stream failed for " << stream->name()
+ << " ct=" << ready_ct_ << std::endl;
+ DumpCandidatePairs_s(stream);
+ }
+
+ void ConnectionStateChange(NrIceCtx* ctx, NrIceCtx::ConnectionState state) {
+ (void)ctx;
+ switch (state) {
+ case NrIceCtx::ICE_CTX_INIT:
+ break;
+ case NrIceCtx::ICE_CTX_CHECKING:
+ std::cerr << name_ << " ICE reached checking" << std::endl;
+ ice_reached_checking_ = true;
+ break;
+ case NrIceCtx::ICE_CTX_CONNECTED:
+ std::cerr << name_ << " ICE connected" << std::endl;
+ ice_connected_ = true;
+ break;
+ case NrIceCtx::ICE_CTX_COMPLETED:
+ std::cerr << name_ << " ICE completed" << std::endl;
+ break;
+ case NrIceCtx::ICE_CTX_FAILED:
+ std::cerr << name_ << " ICE failed" << std::endl;
+ ice_failed_ = true;
+ break;
+ case NrIceCtx::ICE_CTX_DISCONNECTED:
+ std::cerr << name_ << " ICE disconnected" << std::endl;
+ ice_connected_ = false;
+ break;
+ default:
+ MOZ_CRASH();
+ }
+ }
+
+ void PacketReceived(NrIceMediaStream* stream, int component,
+ const unsigned char* data, int len) {
+ std::cerr << name_ << ": received " << len << " bytes" << std::endl;
+ ++received_;
+ }
+
+ void SendPacket(int stream, int component, const unsigned char* data,
+ int len) {
+ auto media_stream = GetStream_s(stream);
+ if (!media_stream) {
+ ADD_FAILURE() << "No such stream " << stream;
+ return;
+ }
+
+ ASSERT_TRUE(NS_SUCCEEDED(media_stream->SendPacket(component, data, len)));
+
+ ++sent_;
+ std::cerr << name_ << ": sent " << len << " bytes" << std::endl;
+ }
+
+ void SendFailure(int stream, int component) {
+ auto media_stream = GetStream_s(stream);
+ if (!media_stream) {
+ ADD_FAILURE() << "No such stream " << stream;
+ return;
+ }
+
+ const std::string d("FAIL");
+ ASSERT_TRUE(NS_FAILED(media_stream->SendPacket(
+ component, reinterpret_cast<const unsigned char*>(d.c_str()),
+ d.length())));
+
+ std::cerr << name_ << ": send failed as expected" << std::endl;
+ }
+
+ void SetCandidateFilter(CandidateFilter filter) {
+ candidate_filter_ = filter;
+ }
+
+ void ParseCandidate_s(size_t i, const std::string& candidate,
+ const std::string& mdns_addr) {
+ auto media_stream = GetStream_s(i);
+ ASSERT_TRUE(media_stream.get())
+ << "No such stream " << i;
+ media_stream->ParseTrickleCandidate(candidate, "", mdns_addr);
+ }
+
+ void ParseCandidate(size_t i, const std::string& candidate,
+ const std::string& mdns_addr) {
+ test_utils_->SyncDispatchToSTS(WrapRunnable(
+ this, &IceTestPeer::ParseCandidate_s, i, candidate, mdns_addr));
+ }
+
+ void DisableComponent_s(size_t index, int component_id) {
+ ASSERT_LT(index, stream_counter_);
+ auto stream = GetStream_s(index);
+ ASSERT_TRUE(stream.get())
+ << "No such stream " << index;
+ nsresult res = stream->DisableComponent(component_id);
+ ASSERT_TRUE(NS_SUCCEEDED(res));
+ }
+
+ void DisableComponent(size_t stream, int component_id) {
+ test_utils_->SyncDispatchToSTS(WrapRunnable(
+ this, &IceTestPeer::DisableComponent_s, stream, component_id));
+ }
+
+ void AssertConsentRefresh_s(size_t index, int component_id,
+ ConsentStatus status) {
+ ASSERT_LT(index, stream_counter_);
+ auto stream = GetStream_s(index);
+ ASSERT_TRUE(stream.get())
+ << "No such stream " << index;
+ bool can_send;
+ struct timeval timestamp;
+ nsresult res =
+ stream->GetConsentStatus(component_id, &can_send, &timestamp);
+ ASSERT_TRUE(NS_SUCCEEDED(res));
+ if (status == CONSENT_EXPIRED) {
+ ASSERT_EQ(can_send, 0);
+ } else {
+ ASSERT_EQ(can_send, 1);
+ }
+ if (consent_timestamp_.tv_sec) {
+ if (status == CONSENT_FRESH) {
+ ASSERT_EQ(r_timeval_cmp(&timestamp, &consent_timestamp_), 1);
+ } else {
+ ASSERT_EQ(r_timeval_cmp(&timestamp, &consent_timestamp_), 0);
+ }
+ }
+ consent_timestamp_.tv_sec = timestamp.tv_sec;
+ consent_timestamp_.tv_usec = timestamp.tv_usec;
+ std::cerr << name_
+ << ": new consent timestamp = " << consent_timestamp_.tv_sec
+ << "." << consent_timestamp_.tv_usec << std::endl;
+ }
+
+ void AssertConsentRefresh(ConsentStatus status) {
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(this, &IceTestPeer::AssertConsentRefresh_s, 0, 1, status));
+ }
+
+ void ChangeNetworkState_s(bool online) {
+ ice_ctx_->UpdateNetworkState(online);
+ }
+
+ void ChangeNetworkStateToOffline() {
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(this, &IceTestPeer::ChangeNetworkState_s, false));
+ }
+
+ void ChangeNetworkStateToOnline() {
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(this, &IceTestPeer::ChangeNetworkState_s, true));
+ }
+
+ void SetControlling(NrIceCtx::Controlling controlling) {
+ nsresult res;
+ test_utils_->SyncDispatchToSTS(WrapRunnableRet(
+ &res, ice_ctx_, &NrIceCtx::SetControlling, controlling));
+ ASSERT_TRUE(NS_SUCCEEDED(res));
+ }
+
+ NrIceCtx::Controlling GetControlling() { return ice_ctx_->GetControlling(); }
+
+ void SetTiebreaker(uint64_t tiebreaker) {
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(this, &IceTestPeer::SetTiebreaker_s, tiebreaker));
+ }
+
+ void SetTiebreaker_s(uint64_t tiebreaker) {
+ ice_ctx_->peer()->tiebreaker = tiebreaker;
+ }
+
+ void SimulateIceLite() {
+ simulate_ice_lite_ = true;
+ SetControlling(NrIceCtx::ICE_CONTROLLED);
+ }
+
+ nsresult GetDefaultCandidate(unsigned int stream, NrIceCandidate* cand) {
+ nsresult rv;
+
+ test_utils_->SyncDispatchToSTS(WrapRunnableRet(
+ &rv, this, &IceTestPeer::GetDefaultCandidate_s, stream, cand));
+
+ return rv;
+ }
+
+ nsresult GetDefaultCandidate_s(unsigned int index, NrIceCandidate* cand) {
+ return GetStream_s(index)->GetDefaultCandidate(1, cand);
+ }
+
+ private:
+ std::string name_;
+ RefPtr<NrIceCtx> ice_ctx_;
+ bool offerer_;
+ std::map<std::string, std::vector<std::string>> candidates_;
+ // Maps from stream id to list of remote trickle candidates
+ std::map<size_t, std::vector<SchedulableTrickleCandidate*>>
+ controlled_trickle_candidates_;
+ std::map<std::string, std::pair<std::string, std::string>> mIceCredentials;
+ std::map<std::string, std::pair<std::string, std::string>> mOldIceCredentials;
+ size_t stream_counter_;
+ bool shutting_down_;
+ bool gathering_complete_;
+ int ready_ct_;
+ bool ice_connected_;
+ bool ice_failed_;
+ bool ice_reached_checking_;
+ size_t received_;
+ size_t sent_;
+ struct timeval consent_timestamp_;
+ NrIceResolverFake fake_resolver_;
+ RefPtr<NrIceResolver> dns_resolver_;
+ IceTestPeer* remote_;
+ CandidateFilter candidate_filter_;
+ NrIceCandidate::Type expected_local_type_;
+ std::string expected_local_transport_;
+ NrIceCandidate::Type expected_remote_type_;
+ std::string expected_remote_addr_;
+ TrickleMode trickle_mode_;
+ bool simulate_ice_lite_;
+ RefPtr<mozilla::TestNat> nat_;
+ MtransportTestUtils* test_utils_;
+};
+
+void SchedulableTrickleCandidate::Trickle() {
+ timer_handle_ = nullptr;
+ nsresult res = peer_->TrickleCandidate_s(candidate_, ufrag_, stream_);
+ ASSERT_TRUE(NS_SUCCEEDED(res));
+}
+
+class WebRtcIceGatherTest : public StunTest {
+ public:
+ void SetUp() override {
+ StunTest::SetUp();
+
+ Preferences::SetInt("media.peerconnection.ice.tcp_so_sock_count", 3);
+
+ test_utils_->SyncDispatchToSTS(WrapRunnable(
+ TestStunServer::GetInstance(AF_INET), &TestStunServer::Reset));
+ if (TestStunServer::GetInstance(AF_INET6)) {
+ test_utils_->SyncDispatchToSTS(WrapRunnable(
+ TestStunServer::GetInstance(AF_INET6), &TestStunServer::Reset));
+ }
+ }
+
+ void TearDown() override {
+ peer_ = nullptr;
+ StunTest::TearDown();
+ }
+
+ void EnsurePeer() {
+ if (!peer_) {
+ peer_ =
+ MakeUnique<IceTestPeer>("P1", test_utils_, true, NrIceCtx::Config());
+ }
+ }
+
+ void Gather(unsigned int waitTime = kDefaultTimeout,
+ bool default_route_only = false,
+ bool obfuscate_host_addresses = false) {
+ EnsurePeer();
+ peer_->Gather(default_route_only, obfuscate_host_addresses);
+
+ if (waitTime) {
+ WaitForGather(waitTime);
+ }
+ }
+
+ void WaitForGather(unsigned int waitTime = kDefaultTimeout) {
+ ASSERT_TRUE_WAIT(peer_->gathering_complete(), waitTime);
+ }
+
+ void AddStunServerWithResponse(const std::string& fake_addr,
+ uint16_t fake_port, const std::string& fqdn,
+ const std::string& proto,
+ std::vector<NrIceStunServer>* stun_servers) {
+ int family;
+ if (fake_addr.find(':') != std::string::npos) {
+ family = AF_INET6;
+ } else {
+ family = AF_INET;
+ }
+
+ std::string stun_addr;
+ uint16_t stun_port;
+ if (proto == kNrIceTransportUdp) {
+ TestStunServer::GetInstance(family)->SetResponseAddr(fake_addr,
+ fake_port);
+ stun_addr = TestStunServer::GetInstance(family)->addr();
+ stun_port = TestStunServer::GetInstance(family)->port();
+ } else if (proto == kNrIceTransportTcp) {
+ TestStunTcpServer::GetInstance(family)->SetResponseAddr(fake_addr,
+ fake_port);
+ stun_addr = TestStunTcpServer::GetInstance(family)->addr();
+ stun_port = TestStunTcpServer::GetInstance(family)->port();
+ } else {
+ MOZ_CRASH();
+ }
+
+ if (!fqdn.empty()) {
+ peer_->SetFakeResolver(stun_addr, fqdn);
+ stun_addr = fqdn;
+ }
+
+ stun_servers->push_back(
+ *NrIceStunServer::Create(stun_addr, stun_port, proto.c_str()));
+
+ if (family == AF_INET6 && !fqdn.empty()) {
+ stun_servers->back().SetUseIPv6IfFqdn();
+ }
+ }
+
+ void UseFakeStunUdpServerWithResponse(
+ const std::string& fake_addr, uint16_t fake_port,
+ const std::string& fqdn = std::string()) {
+ EnsurePeer();
+ std::vector<NrIceStunServer> stun_servers;
+ AddStunServerWithResponse(fake_addr, fake_port, fqdn, "udp", &stun_servers);
+ peer_->SetStunServers(stun_servers);
+ }
+
+ void UseFakeStunTcpServerWithResponse(
+ const std::string& fake_addr, uint16_t fake_port,
+ const std::string& fqdn = std::string()) {
+ EnsurePeer();
+ std::vector<NrIceStunServer> stun_servers;
+ AddStunServerWithResponse(fake_addr, fake_port, fqdn, "tcp", &stun_servers);
+ peer_->SetStunServers(stun_servers);
+ }
+
+ void UseFakeStunUdpTcpServersWithResponse(const std::string& fake_udp_addr,
+ uint16_t fake_udp_port,
+ const std::string& fake_tcp_addr,
+ uint16_t fake_tcp_port) {
+ EnsurePeer();
+ std::vector<NrIceStunServer> stun_servers;
+ AddStunServerWithResponse(fake_udp_addr, fake_udp_port,
+ "", // no fqdn
+ "udp", &stun_servers);
+ AddStunServerWithResponse(fake_tcp_addr, fake_tcp_port,
+ "", // no fqdn
+ "tcp", &stun_servers);
+
+ peer_->SetStunServers(stun_servers);
+ }
+
+ void UseTestStunServer() {
+ TestStunServer::GetInstance(AF_INET)->Reset();
+ peer_->SetStunServer(TestStunServer::GetInstance(AF_INET)->addr(),
+ TestStunServer::GetInstance(AF_INET)->port());
+ }
+
+ // NB: Only does substring matching, watch out for stuff like "1.2.3.4"
+ // matching "21.2.3.47". " 1.2.3.4 " should not have false positives.
+ bool StreamHasMatchingCandidate(unsigned int stream, const std::string& match,
+ const std::string& match2 = "") {
+ std::vector<std::string> attributes = peer_->GetAttributes(stream);
+ for (auto& attribute : attributes) {
+ if (std::string::npos != attribute.find(match)) {
+ if (!match2.length() || std::string::npos != attribute.find(match2)) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ void DumpAttributes(unsigned int stream) {
+ std::vector<std::string> attributes = peer_->GetAttributes(stream);
+
+ std::cerr << "Attributes for stream " << stream << "->" << attributes.size()
+ << std::endl;
+
+ for (const auto& a : attributes) {
+ std::cerr << "Attribute: " << a << std::endl;
+ }
+ }
+
+ protected:
+ mozilla::UniquePtr<IceTestPeer> peer_;
+};
+
+class WebRtcIceConnectTest : public StunTest {
+ public:
+ WebRtcIceConnectTest()
+ : initted_(false),
+ test_stun_server_inited_(false),
+ use_nat_(false),
+ filtering_type_(TestNat::ENDPOINT_INDEPENDENT),
+ mapping_type_(TestNat::ENDPOINT_INDEPENDENT),
+ block_udp_(false) {}
+
+ void SetUp() override {
+ StunTest::SetUp();
+
+ nsresult rv;
+ target_ = do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+ }
+
+ void TearDown() override {
+ p1_ = nullptr;
+ p2_ = nullptr;
+
+ StunTest::TearDown();
+ }
+
+ void AddStream(int components) {
+ Init();
+ p1_->AddStream(components);
+ p2_->AddStream(components);
+ }
+
+ void RemoveStream(size_t index) {
+ p1_->RemoveStream(index);
+ p2_->RemoveStream(index);
+ }
+
+ void Init(bool setup_stun_servers = true,
+ NrIceCtx::Policy ice_policy = NrIceCtx::ICE_POLICY_ALL) {
+ if (initted_) {
+ return;
+ }
+
+ NrIceCtx::Config config;
+ config.mPolicy = ice_policy;
+
+ p1_ = MakeUnique<IceTestPeer>("P1", test_utils_, true, config);
+ p2_ = MakeUnique<IceTestPeer>("P2", test_utils_, false, config);
+ InitPeer(p1_.get(), setup_stun_servers);
+ InitPeer(p2_.get(), setup_stun_servers);
+
+ initted_ = true;
+ }
+
+ void InitPeer(IceTestPeer* peer, bool setup_stun_servers = true) {
+ if (use_nat_) {
+ // If we enable nat simulation, but still use a real STUN server somewhere
+ // on the internet, we will see failures if there is a real NAT in
+ // addition to our simulated one, particularly if it disallows
+ // hairpinning.
+ if (setup_stun_servers) {
+ InitTestStunServer();
+ peer->UseTestStunServer();
+ }
+ peer->UseNat();
+ peer->SetFilteringType(filtering_type_);
+ peer->SetMappingType(mapping_type_);
+ peer->SetBlockUdp(block_udp_);
+ } else if (setup_stun_servers) {
+ std::vector<NrIceStunServer> stun_servers;
+
+ stun_servers.push_back(*NrIceStunServer::Create(
+ stun_server_address_, kDefaultStunServerPort, kNrIceTransportUdp));
+
+ peer->SetStunServers(stun_servers);
+ }
+ }
+
+ bool Gather(unsigned int waitTime = kDefaultTimeout,
+ bool default_route_only = false) {
+ Init();
+
+ return GatherCallerAndCallee(p1_.get(), p2_.get(), waitTime,
+ default_route_only);
+ }
+
+ bool GatherCallerAndCallee(IceTestPeer* caller, IceTestPeer* callee,
+ unsigned int waitTime = kDefaultTimeout,
+ bool default_route_only = false) {
+ caller->Gather(default_route_only);
+ callee->Gather(default_route_only);
+
+ if (waitTime) {
+ EXPECT_TRUE_WAIT(caller->gathering_complete(), waitTime);
+ if (!caller->gathering_complete()) return false;
+ EXPECT_TRUE_WAIT(callee->gathering_complete(), waitTime);
+ if (!callee->gathering_complete()) return false;
+ }
+ return true;
+ }
+
+ void UseNat() {
+ // to be useful, this method should be called before Init
+ ASSERT_FALSE(initted_);
+ use_nat_ = true;
+ }
+
+ void SetFilteringType(TestNat::NatBehavior type) {
+ // to be useful, this method should be called before Init
+ ASSERT_FALSE(initted_);
+ filtering_type_ = type;
+ }
+
+ void SetMappingType(TestNat::NatBehavior type) {
+ // to be useful, this method should be called before Init
+ ASSERT_FALSE(initted_);
+ mapping_type_ = type;
+ }
+
+ void BlockUdp() {
+ // note: |block_udp_| is used only in InitPeer.
+ // Use IceTestPeer::SetBlockUdp to act on the peer directly.
+ block_udp_ = true;
+ }
+
+ void SetupAndCheckConsent() {
+ p1_->SetTimerDivider(10);
+ p2_->SetTimerDivider(10);
+ ASSERT_TRUE(Gather());
+ Connect();
+ p1_->AssertConsentRefresh(CONSENT_FRESH);
+ p2_->AssertConsentRefresh(CONSENT_FRESH);
+ SendReceive();
+ }
+
+ void AssertConsentRefresh(ConsentStatus status = CONSENT_FRESH) {
+ p1_->AssertConsentRefresh(status);
+ p2_->AssertConsentRefresh(status);
+ }
+
+ void InitTestStunServer() {
+ if (test_stun_server_inited_) {
+ return;
+ }
+
+ std::cerr << "Resetting TestStunServer" << std::endl;
+ TestStunServer::GetInstance(AF_INET)->Reset();
+ test_stun_server_inited_ = true;
+ }
+
+ void UseTestStunServer() {
+ InitTestStunServer();
+ p1_->UseTestStunServer();
+ p2_->UseTestStunServer();
+ }
+
+ void SetTurnServer(const std::string addr, uint16_t port,
+ const std::string username, const std::string password,
+ const char* transport = kNrIceTransportUdp) {
+ p1_->SetTurnServer(addr, port, username, password, transport);
+ p2_->SetTurnServer(addr, port, username, password, transport);
+ }
+
+ void SetTurnServers(const std::vector<NrIceTurnServer>& servers) {
+ p1_->SetTurnServers(servers);
+ p2_->SetTurnServers(servers);
+ }
+
+ void SetCandidateFilter(CandidateFilter filter, bool both = true) {
+ p1_->SetCandidateFilter(filter);
+ if (both) {
+ p2_->SetCandidateFilter(filter);
+ }
+ }
+
+ void Connect() { ConnectCallerAndCallee(p1_.get(), p2_.get()); }
+
+ void ConnectCallerAndCallee(IceTestPeer* caller, IceTestPeer* callee,
+ TrickleMode mode = TRICKLE_NONE) {
+ ASSERT_TRUE(caller->ready_ct() == 0);
+ ASSERT_TRUE(caller->ice_connected() == 0);
+ ASSERT_TRUE(caller->ice_reached_checking() == 0);
+ ASSERT_TRUE(callee->ready_ct() == 0);
+ ASSERT_TRUE(callee->ice_connected() == 0);
+ ASSERT_TRUE(callee->ice_reached_checking() == 0);
+
+ // IceTestPeer::Connect grabs attributes from the first arg, and
+ // gives them to |this|, meaning that callee->Connect(caller, ...)
+ // simulates caller sending an offer to callee. Order matters here
+ // because it determines which peer is controlling.
+ callee->Connect(caller, mode);
+ caller->Connect(callee, mode);
+
+ if (mode != TRICKLE_SIMULATE) {
+ ASSERT_TRUE_WAIT(caller->ice_connected() && callee->ice_connected(),
+ kDefaultTimeout);
+ ASSERT_TRUE(caller->ready_ct() >= 1 && callee->ready_ct() >= 1);
+ ASSERT_TRUE(caller->ice_reached_checking());
+ ASSERT_TRUE(callee->ice_reached_checking());
+
+ caller->DumpAndCheckActiveCandidates();
+ callee->DumpAndCheckActiveCandidates();
+ }
+ }
+
+ void SetExpectedTypes(NrIceCandidate::Type local, NrIceCandidate::Type remote,
+ std::string transport = kNrIceTransportUdp) {
+ p1_->SetExpectedTypes(local, remote, transport);
+ p2_->SetExpectedTypes(local, remote, transport);
+ }
+
+ void SetExpectedRemoteCandidateAddr(const std::string& addr) {
+ p1_->SetExpectedRemoteCandidateAddr(addr);
+ p2_->SetExpectedRemoteCandidateAddr(addr);
+ }
+
+ void ConnectP1(TrickleMode mode = TRICKLE_NONE) {
+ p1_->Connect(p2_.get(), mode);
+ }
+
+ void ConnectP2(TrickleMode mode = TRICKLE_NONE) {
+ p2_->Connect(p1_.get(), mode);
+ }
+
+ void WaitForConnectedStreams(int expected_streams = 1) {
+ ASSERT_TRUE_WAIT(p1_->ready_ct() == expected_streams &&
+ p2_->ready_ct() == expected_streams,
+ kDefaultTimeout);
+ ASSERT_TRUE_WAIT(p1_->ice_connected() && p2_->ice_connected(),
+ kDefaultTimeout);
+ }
+
+ void AssertCheckingReached() {
+ ASSERT_TRUE(p1_->ice_reached_checking());
+ ASSERT_TRUE(p2_->ice_reached_checking());
+ }
+
+ void WaitForConnected(unsigned int timeout = kDefaultTimeout) {
+ ASSERT_TRUE_WAIT(p1_->ice_connected(), timeout);
+ ASSERT_TRUE_WAIT(p2_->ice_connected(), timeout);
+ }
+
+ void WaitForGather() {
+ ASSERT_TRUE_WAIT(p1_->gathering_complete(), kDefaultTimeout);
+ ASSERT_TRUE_WAIT(p2_->gathering_complete(), kDefaultTimeout);
+ }
+
+ void WaitForDisconnected(unsigned int timeout = kDefaultTimeout) {
+ ASSERT_TRUE(p1_->ice_connected());
+ ASSERT_TRUE(p2_->ice_connected());
+ ASSERT_TRUE_WAIT(p1_->ice_connected() == 0 && p2_->ice_connected() == 0,
+ timeout);
+ }
+
+ void WaitForFailed(unsigned int timeout = kDefaultTimeout) {
+ ASSERT_TRUE_WAIT(p1_->ice_failed() && p2_->ice_failed(), timeout);
+ }
+
+ void ConnectTrickle(TrickleMode trickle = TRICKLE_SIMULATE) {
+ p2_->Connect(p1_.get(), trickle);
+ p1_->Connect(p2_.get(), trickle);
+ }
+
+ void SimulateTrickle(size_t stream) {
+ p1_->SimulateTrickle(stream);
+ p2_->SimulateTrickle(stream);
+ ASSERT_TRUE_WAIT(p1_->is_ready(stream), kDefaultTimeout);
+ ASSERT_TRUE_WAIT(p2_->is_ready(stream), kDefaultTimeout);
+ }
+
+ void SimulateTrickleP1(size_t stream) { p1_->SimulateTrickle(stream); }
+
+ void SimulateTrickleP2(size_t stream) { p2_->SimulateTrickle(stream); }
+
+ void CloseP1() { p1_->Close(); }
+
+ void ConnectThenDelete() {
+ p2_->Connect(p1_.get(), TRICKLE_NONE, false);
+ p1_->Connect(p2_.get(), TRICKLE_NONE, true);
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(this, &WebRtcIceConnectTest::CloseP1));
+ p2_->StartChecks();
+
+ // Wait to see if we crash
+ PR_Sleep(PR_MillisecondsToInterval(kDefaultTimeout));
+ }
+
+ // default is p1_ sending to p2_
+ void SendReceive() { SendReceive(p1_.get(), p2_.get()); }
+
+ void SendReceive(IceTestPeer* p1, IceTestPeer* p2,
+ bool expect_tx_failure = false,
+ bool expect_rx_failure = false) {
+ size_t previousSent = p1->sent();
+ size_t previousReceived = p2->received();
+
+ if (expect_tx_failure) {
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(p1, &IceTestPeer::SendFailure, 0, 1));
+ ASSERT_EQ(previousSent, p1->sent());
+ } else {
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(p1, &IceTestPeer::SendPacket, 0, 1,
+ reinterpret_cast<const unsigned char*>("TEST"), 4));
+ ASSERT_EQ(previousSent + 1, p1->sent());
+ }
+ if (expect_rx_failure) {
+ usleep(1000);
+ ASSERT_EQ(previousReceived, p2->received());
+ } else {
+ ASSERT_TRUE_WAIT(p2->received() == previousReceived + 1, 1000);
+ }
+ }
+
+ void SendFailure() {
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(p1_.get(), &IceTestPeer::SendFailure, 0, 1));
+ }
+
+ protected:
+ bool initted_;
+ bool test_stun_server_inited_;
+ nsCOMPtr<nsIEventTarget> target_;
+ mozilla::UniquePtr<IceTestPeer> p1_;
+ mozilla::UniquePtr<IceTestPeer> p2_;
+ bool use_nat_;
+ TestNat::NatBehavior filtering_type_;
+ TestNat::NatBehavior mapping_type_;
+ bool block_udp_;
+};
+
+class WebRtcIcePrioritizerTest : public StunTest {
+ public:
+ WebRtcIcePrioritizerTest() : prioritizer_(nullptr) {}
+
+ ~WebRtcIcePrioritizerTest() {
+ if (prioritizer_) {
+ nr_interface_prioritizer_destroy(&prioritizer_);
+ }
+ }
+
+ void SetPriorizer(nr_interface_prioritizer* prioritizer) {
+ prioritizer_ = prioritizer;
+ }
+
+ void AddInterface(const std::string& num, int type, int estimated_speed) {
+ std::string str_addr = "10.0.0." + num;
+ std::string ifname = "eth" + num;
+ nr_local_addr local_addr;
+ local_addr.interface.type = type;
+ local_addr.interface.estimated_speed = estimated_speed;
+
+ int r = nr_str_port_to_transport_addr(str_addr.c_str(), 0, IPPROTO_UDP,
+ &(local_addr.addr));
+ ASSERT_EQ(0, r);
+ strncpy(local_addr.addr.ifname, ifname.c_str(), MAXIFNAME - 1);
+ local_addr.addr.ifname[MAXIFNAME - 1] = '\0';
+
+ r = nr_interface_prioritizer_add_interface(prioritizer_, &local_addr);
+ ASSERT_EQ(0, r);
+ r = nr_interface_prioritizer_sort_preference(prioritizer_);
+ ASSERT_EQ(0, r);
+ }
+
+ void HasLowerPreference(const std::string& num1, const std::string& num2) {
+ std::string key1 = "eth" + num1 + ":10.0.0." + num1;
+ std::string key2 = "eth" + num2 + ":10.0.0." + num2;
+ UCHAR pref1, pref2;
+ int r = nr_interface_prioritizer_get_priority(prioritizer_, key1.c_str(),
+ &pref1);
+ ASSERT_EQ(0, r);
+ r = nr_interface_prioritizer_get_priority(prioritizer_, key2.c_str(),
+ &pref2);
+ ASSERT_EQ(0, r);
+ ASSERT_LE(pref1, pref2);
+ }
+
+ private:
+ nr_interface_prioritizer* prioritizer_;
+};
+
+class WebRtcIcePacketFilterTest : public StunTest {
+ public:
+ WebRtcIcePacketFilterTest() : udp_filter_(nullptr), tcp_filter_(nullptr) {}
+
+ void SetUp() {
+ StunTest::SetUp();
+
+ NrIceCtx::InitializeGlobals(NrIceCtx::GlobalConfig());
+
+ // Set up enough of the ICE ctx to allow the packet filter to work
+ ice_ctx_ = NrIceCtx::Create("test");
+
+ nsCOMPtr<nsISocketFilterHandler> udp_handler =
+ do_GetService(NS_STUN_UDP_SOCKET_FILTER_HANDLER_CONTRACTID);
+ ASSERT_TRUE(udp_handler);
+ udp_handler->NewFilter(getter_AddRefs(udp_filter_));
+
+ nsCOMPtr<nsISocketFilterHandler> tcp_handler =
+ do_GetService(NS_STUN_TCP_SOCKET_FILTER_HANDLER_CONTRACTID);
+ ASSERT_TRUE(tcp_handler);
+ tcp_handler->NewFilter(getter_AddRefs(tcp_filter_));
+ }
+
+ void TearDown() {
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(this, &WebRtcIcePacketFilterTest::TearDown_s));
+ StunTest::TearDown();
+ }
+
+ void TearDown_s() { ice_ctx_ = nullptr; }
+
+ void TestIncoming(const uint8_t* data, uint32_t len, uint8_t from_addr,
+ int from_port, bool expected_result) {
+ mozilla::net::NetAddr addr;
+ MakeNetAddr(&addr, from_addr, from_port);
+ bool result;
+ nsresult rv = udp_filter_->FilterPacket(
+ &addr, data, len, nsISocketFilter::SF_INCOMING, &result);
+ ASSERT_EQ(NS_OK, rv);
+ ASSERT_EQ(expected_result, result);
+ }
+
+ void TestIncomingTcp(const uint8_t* data, uint32_t len,
+ bool expected_result) {
+ mozilla::net::NetAddr addr;
+ bool result;
+ nsresult rv = tcp_filter_->FilterPacket(
+ &addr, data, len, nsISocketFilter::SF_INCOMING, &result);
+ ASSERT_EQ(NS_OK, rv);
+ ASSERT_EQ(expected_result, result);
+ }
+
+ void TestIncomingTcpFramed(const uint8_t* data, uint32_t len,
+ bool expected_result) {
+ mozilla::net::NetAddr addr;
+ bool result;
+ uint8_t* framed_data = new uint8_t[len + 2];
+ framed_data[0] = htons(len);
+ memcpy(&framed_data[2], data, len);
+ nsresult rv = tcp_filter_->FilterPacket(
+ &addr, framed_data, len + 2, nsISocketFilter::SF_INCOMING, &result);
+ ASSERT_EQ(NS_OK, rv);
+ ASSERT_EQ(expected_result, result);
+ delete[] framed_data;
+ }
+
+ void TestOutgoing(const uint8_t* data, uint32_t len, uint8_t to_addr,
+ int to_port, bool expected_result) {
+ mozilla::net::NetAddr addr;
+ MakeNetAddr(&addr, to_addr, to_port);
+ bool result;
+ nsresult rv = udp_filter_->FilterPacket(
+ &addr, data, len, nsISocketFilter::SF_OUTGOING, &result);
+ ASSERT_EQ(NS_OK, rv);
+ ASSERT_EQ(expected_result, result);
+ }
+
+ void TestOutgoingTcp(const uint8_t* data, uint32_t len,
+ bool expected_result) {
+ mozilla::net::NetAddr addr;
+ bool result;
+ nsresult rv = tcp_filter_->FilterPacket(
+ &addr, data, len, nsISocketFilter::SF_OUTGOING, &result);
+ ASSERT_EQ(NS_OK, rv);
+ ASSERT_EQ(expected_result, result);
+ }
+
+ void TestOutgoingTcpFramed(const uint8_t* data, uint32_t len,
+ bool expected_result) {
+ mozilla::net::NetAddr addr;
+ bool result;
+ uint8_t* framed_data = new uint8_t[len + 2];
+ framed_data[0] = htons(len);
+ memcpy(&framed_data[2], data, len);
+ nsresult rv = tcp_filter_->FilterPacket(
+ &addr, framed_data, len + 2, nsISocketFilter::SF_OUTGOING, &result);
+ ASSERT_EQ(NS_OK, rv);
+ ASSERT_EQ(expected_result, result);
+ delete[] framed_data;
+ }
+
+ private:
+ void MakeNetAddr(mozilla::net::NetAddr* net_addr, uint8_t last_digit,
+ uint16_t port) {
+ net_addr->inet.family = AF_INET;
+ net_addr->inet.ip = 192 << 24 | 168 << 16 | 1 << 8 | last_digit;
+ net_addr->inet.port = port;
+ }
+
+ nsCOMPtr<nsISocketFilter> udp_filter_;
+ nsCOMPtr<nsISocketFilter> tcp_filter_;
+ RefPtr<NrIceCtx> ice_ctx_;
+};
+} // end namespace
+
+TEST_F(WebRtcIceGatherTest, TestGatherFakeStunServerHostnameNoResolver) {
+ if (stun_server_hostname_.empty()) {
+ return;
+ }
+
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ EnsurePeer();
+ peer_->SetStunServer(stun_server_hostname_, kDefaultStunServerPort);
+ peer_->AddStream(1);
+ Gather();
+}
+
+// Disabled because google isn't running any TCP stun servers right now
+TEST_F(WebRtcIceGatherTest,
+ DISABLED_TestGatherFakeStunServerTcpHostnameNoResolver) {
+ if (stun_server_hostname_.empty()) {
+ return;
+ }
+
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = true;
+ NrIceCtx::InitializeGlobals(config);
+ EnsurePeer();
+ peer_->SetStunServer(stun_server_hostname_, kDefaultStunServerPort,
+ kNrIceTransportTcp);
+ peer_->AddStream(1);
+ Gather();
+ ASSERT_TRUE(StreamHasMatchingCandidate(0, " TCP "));
+}
+
+TEST_F(WebRtcIceGatherTest, TestGatherFakeStunServerIpAddress) {
+ if (stun_server_address_.empty()) {
+ return;
+ }
+
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ EnsurePeer();
+ peer_->SetStunServer(stun_server_address_, kDefaultStunServerPort);
+ peer_->SetFakeResolver(stun_server_address_, stun_server_hostname_);
+ peer_->AddStream(1);
+ Gather();
+}
+
+TEST_F(WebRtcIceGatherTest, TestGatherStunServerIpAddressNoHost) {
+ if (stun_server_address_.empty()) {
+ return;
+ }
+
+ {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ }
+
+ NrIceCtx::Config config;
+ config.mPolicy = NrIceCtx::ICE_POLICY_NO_HOST;
+ peer_ = MakeUnique<IceTestPeer>("P1", test_utils_, true, config);
+ peer_->AddStream(1);
+ peer_->SetStunServer(stun_server_address_, kDefaultStunServerPort);
+ peer_->SetFakeResolver(stun_server_address_, stun_server_hostname_);
+ Gather();
+ ASSERT_FALSE(StreamHasMatchingCandidate(0, " host "));
+}
+
+TEST_F(WebRtcIceGatherTest, TestGatherFakeStunServerHostname) {
+ if (stun_server_hostname_.empty()) {
+ return;
+ }
+
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ EnsurePeer();
+ peer_->SetStunServer(stun_server_hostname_, kDefaultStunServerPort);
+ peer_->SetFakeResolver(stun_server_address_, stun_server_hostname_);
+ peer_->AddStream(1);
+ Gather();
+}
+
+TEST_F(WebRtcIceGatherTest, TestGatherFakeStunBogusHostname) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ EnsurePeer();
+ peer_->SetStunServer(kBogusStunServerHostname, kDefaultStunServerPort);
+ peer_->SetFakeResolver(stun_server_address_, stun_server_hostname_);
+ peer_->AddStream(1);
+ Gather();
+}
+
+TEST_F(WebRtcIceGatherTest, TestGatherDNSStunServerIpAddress) {
+ if (stun_server_address_.empty()) {
+ return;
+ }
+
+ {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ }
+
+ // A srflx candidate is considered redundant and discarded if its address
+ // equals that of a host candidate. (Frequently, a srflx candidate and a host
+ // candidate have equal addresses when the agent is not behind a NAT.) So set
+ // ICE_POLICY_NO_HOST here to ensure that a srflx candidate is not falsely
+ // discarded in this test.
+ NrIceCtx::Config config;
+ config.mPolicy = NrIceCtx::ICE_POLICY_NO_HOST;
+ peer_ = MakeUnique<IceTestPeer>("P1", test_utils_, true, config);
+
+ peer_->SetStunServer(stun_server_address_, kDefaultStunServerPort);
+ peer_->SetDNSResolver();
+ peer_->AddStream(1);
+ Gather();
+ ASSERT_TRUE(StreamHasMatchingCandidate(0, " UDP "));
+ ASSERT_TRUE(StreamHasMatchingCandidate(0, "typ srflx raddr"));
+}
+
+// Disabled because google isn't running any TCP stun servers right now
+TEST_F(WebRtcIceGatherTest, DISABLED_TestGatherDNSStunServerIpAddressTcp) {
+ if (stun_server_address_.empty()) {
+ return;
+ }
+
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = true;
+ NrIceCtx::InitializeGlobals(config);
+ EnsurePeer();
+ peer_->SetStunServer(stun_server_address_, kDefaultStunServerPort,
+ kNrIceTransportTcp);
+ peer_->SetDNSResolver();
+ peer_->AddStream(1);
+ Gather();
+ ASSERT_TRUE(StreamHasMatchingCandidate(0, "tcptype passive"));
+ ASSERT_FALSE(StreamHasMatchingCandidate(0, "tcptype passive", " 9 "));
+ ASSERT_TRUE(StreamHasMatchingCandidate(0, "tcptype so"));
+ ASSERT_FALSE(StreamHasMatchingCandidate(0, "tcptype so", " 9 "));
+ ASSERT_TRUE(StreamHasMatchingCandidate(0, "tcptype active", " 9 "));
+}
+
+TEST_F(WebRtcIceGatherTest, TestGatherDNSStunServerHostname) {
+ if (stun_server_hostname_.empty()) {
+ return;
+ }
+
+ {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ }
+
+ // A srflx candidate is considered redundant and discarded if its address
+ // equals that of a host candidate. (Frequently, a srflx candidate and a host
+ // candidate have equal addresses when the agent is not behind a NAT.) So set
+ // ICE_POLICY_NO_HOST here to ensure that a srflx candidate is not falsely
+ // discarded in this test.
+ NrIceCtx::Config config;
+ config.mPolicy = NrIceCtx::ICE_POLICY_NO_HOST;
+ peer_ = MakeUnique<IceTestPeer>("P1", test_utils_, true, config);
+
+ peer_->SetStunServer(stun_server_hostname_, kDefaultStunServerPort);
+ peer_->SetDNSResolver();
+ peer_->AddStream(1);
+ Gather();
+ ASSERT_TRUE(StreamHasMatchingCandidate(0, " UDP "));
+ ASSERT_TRUE(StreamHasMatchingCandidate(0, "typ srflx raddr"));
+}
+
+// Disabled because google isn't running any TCP stun servers right now
+TEST_F(WebRtcIceGatherTest, DISABLED_TestGatherDNSStunServerHostnameTcp) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = true;
+ NrIceCtx::InitializeGlobals(config);
+ EnsurePeer();
+ peer_->SetStunServer(stun_server_hostname_, kDefaultStunServerPort,
+ kNrIceTransportTcp);
+ peer_->SetDNSResolver();
+ peer_->AddStream(1);
+ Gather();
+ ASSERT_TRUE(StreamHasMatchingCandidate(0, "tcptype passive"));
+ ASSERT_FALSE(StreamHasMatchingCandidate(0, "tcptype passive", " 9 "));
+ ASSERT_TRUE(StreamHasMatchingCandidate(0, "tcptype so"));
+ ASSERT_FALSE(StreamHasMatchingCandidate(0, "tcptype so", " 9 "));
+ ASSERT_TRUE(StreamHasMatchingCandidate(0, "tcptype active", " 9 "));
+}
+
+// Disabled because google isn't running any TCP stun servers right now
+TEST_F(WebRtcIceGatherTest,
+ DISABLED_TestGatherDNSStunServerHostnameBothUdpTcp) {
+ if (stun_server_hostname_.empty()) {
+ return;
+ }
+
+ std::vector<NrIceStunServer> stun_servers;
+
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = true;
+ NrIceCtx::InitializeGlobals(config);
+ EnsurePeer();
+ stun_servers.push_back(*NrIceStunServer::Create(
+ stun_server_hostname_, kDefaultStunServerPort, kNrIceTransportUdp));
+ stun_servers.push_back(*NrIceStunServer::Create(
+ stun_server_hostname_, kDefaultStunServerPort, kNrIceTransportTcp));
+ peer_->SetStunServers(stun_servers);
+ peer_->SetDNSResolver();
+ peer_->AddStream(1);
+ Gather();
+ ASSERT_TRUE(StreamHasMatchingCandidate(0, " UDP "));
+ ASSERT_TRUE(StreamHasMatchingCandidate(0, " TCP "));
+}
+
+// Disabled because google isn't running any TCP stun servers right now
+TEST_F(WebRtcIceGatherTest,
+ DISABLED_TestGatherDNSStunServerIpAddressBothUdpTcp) {
+ if (stun_server_address_.empty()) {
+ return;
+ }
+
+ std::vector<NrIceStunServer> stun_servers;
+
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = true;
+ NrIceCtx::InitializeGlobals(config);
+ EnsurePeer();
+ stun_servers.push_back(*NrIceStunServer::Create(
+ stun_server_address_, kDefaultStunServerPort, kNrIceTransportUdp));
+ stun_servers.push_back(*NrIceStunServer::Create(
+ stun_server_address_, kDefaultStunServerPort, kNrIceTransportTcp));
+ peer_->SetStunServers(stun_servers);
+ peer_->SetDNSResolver();
+ peer_->AddStream(1);
+ Gather();
+ ASSERT_TRUE(StreamHasMatchingCandidate(0, " UDP "));
+ ASSERT_TRUE(StreamHasMatchingCandidate(0, " TCP "));
+}
+
+TEST_F(WebRtcIceGatherTest, TestGatherDNSStunBogusHostname) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ EnsurePeer();
+ peer_->SetStunServer(kBogusStunServerHostname, kDefaultStunServerPort);
+ peer_->SetDNSResolver();
+ peer_->AddStream(1);
+ Gather();
+ ASSERT_TRUE(StreamHasMatchingCandidate(0, " UDP "));
+}
+
+// Disabled because google isn't running any TCP stun servers right now
+TEST_F(WebRtcIceGatherTest, DISABLED_TestGatherDNSStunBogusHostnameTcp) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = true;
+ NrIceCtx::InitializeGlobals(config);
+ EnsurePeer();
+ peer_->SetStunServer(kBogusStunServerHostname, kDefaultStunServerPort,
+ kNrIceTransportTcp);
+ peer_->SetDNSResolver();
+ peer_->AddStream(1);
+ Gather();
+ ASSERT_TRUE(StreamHasMatchingCandidate(0, " TCP "));
+}
+
+TEST_F(WebRtcIceGatherTest, TestDefaultCandidate) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ EnsurePeer();
+ peer_->SetStunServer(stun_server_hostname_, kDefaultStunServerPort);
+ peer_->AddStream(1);
+ Gather();
+ NrIceCandidate default_candidate;
+ ASSERT_TRUE(NS_SUCCEEDED(peer_->GetDefaultCandidate(0, &default_candidate)));
+}
+
+TEST_F(WebRtcIceGatherTest, TestGatherTurn) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ EnsurePeer();
+ if (turn_server_.empty()) return;
+ peer_->SetTurnServer(turn_server_, kDefaultStunServerPort, turn_user_,
+ turn_password_, kNrIceTransportUdp);
+ peer_->AddStream(1);
+ Gather();
+}
+
+TEST_F(WebRtcIceGatherTest, TestGatherTurnTcp) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ EnsurePeer();
+ if (turn_server_.empty()) return;
+ peer_->SetTurnServer(turn_server_, kDefaultStunServerPort, turn_user_,
+ turn_password_, kNrIceTransportTcp);
+ peer_->AddStream(1);
+ Gather();
+}
+
+TEST_F(WebRtcIceGatherTest, TestGatherDisableComponent) {
+ if (stun_server_hostname_.empty()) {
+ return;
+ }
+
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ EnsurePeer();
+ peer_->SetStunServer(stun_server_hostname_, kDefaultStunServerPort);
+ peer_->AddStream(1);
+ peer_->AddStream(2);
+ peer_->DisableComponent(1, 2);
+ Gather();
+ std::vector<std::string> attributes = peer_->GetAttributes(1);
+
+ for (auto& attribute : attributes) {
+ if (attribute.find("candidate:") != std::string::npos) {
+ size_t sp1 = attribute.find(' ');
+ ASSERT_EQ(0, attribute.compare(sp1 + 1, 1, "1", 1));
+ }
+ }
+}
+
+TEST_F(WebRtcIceGatherTest, TestGatherVerifyNoLoopback) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ EnsurePeer();
+ peer_->AddStream(1);
+ Gather();
+ ASSERT_FALSE(StreamHasMatchingCandidate(0, "127.0.0.1"));
+}
+
+TEST_F(WebRtcIceGatherTest, TestGatherAllowLoopback) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ config.mAllowLoopback = true;
+ NrIceCtx::InitializeGlobals(config);
+
+ // Set up peer with loopback allowed.
+ peer_ = MakeUnique<IceTestPeer>("P1", test_utils_, true, NrIceCtx::Config());
+ peer_->AddStream(1);
+ Gather();
+ ASSERT_TRUE(StreamHasMatchingCandidate(0, "127.0.0.1"));
+}
+
+TEST_F(WebRtcIceGatherTest, TestGatherTcpDisabledNoStun) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ EnsurePeer();
+ peer_->AddStream(1);
+ Gather();
+ ASSERT_FALSE(StreamHasMatchingCandidate(0, " TCP "));
+ ASSERT_TRUE(StreamHasMatchingCandidate(0, " UDP "));
+}
+
+TEST_F(WebRtcIceGatherTest, VerifyTestStunServer) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ UseFakeStunUdpServerWithResponse("192.0.2.133", 3333);
+ peer_->AddStream(1);
+ Gather();
+ ASSERT_TRUE(StreamHasMatchingCandidate(0, " 192.0.2.133 3333 "));
+}
+
+TEST_F(WebRtcIceGatherTest, VerifyTestStunTcpServer) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = true;
+ NrIceCtx::InitializeGlobals(config);
+ UseFakeStunTcpServerWithResponse("192.0.2.233", 3333);
+ peer_->AddStream(1);
+ Gather();
+ ASSERT_TRUE(StreamHasMatchingCandidate(0, " 192.0.2.233 3333 typ srflx",
+ " tcptype "));
+}
+
+TEST_F(WebRtcIceGatherTest, VerifyTestStunServerV6) {
+ if (!TestStunServer::GetInstance(AF_INET6)) {
+ // No V6 addresses
+ return;
+ }
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ UseFakeStunUdpServerWithResponse("beef::", 3333);
+ peer_->AddStream(1);
+ Gather();
+ ASSERT_TRUE(StreamHasMatchingCandidate(0, " beef:: 3333 "));
+}
+
+TEST_F(WebRtcIceGatherTest, VerifyTestStunServerFQDN) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ UseFakeStunUdpServerWithResponse("192.0.2.133", 3333, "stun.example.com");
+ peer_->AddStream(1);
+ Gather();
+ ASSERT_TRUE(StreamHasMatchingCandidate(0, " 192.0.2.133 3333 "));
+}
+
+TEST_F(WebRtcIceGatherTest, VerifyTestStunServerV6FQDN) {
+ if (!TestStunServer::GetInstance(AF_INET6)) {
+ // No V6 addresses
+ return;
+ }
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ UseFakeStunUdpServerWithResponse("beef::", 3333, "stun.example.com");
+ peer_->AddStream(1);
+ Gather();
+ ASSERT_TRUE(StreamHasMatchingCandidate(0, " beef:: 3333 "));
+}
+
+TEST_F(WebRtcIceGatherTest, TestStunServerReturnsWildcardAddr) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ UseFakeStunUdpServerWithResponse("0.0.0.0", 3333);
+ peer_->AddStream(1);
+ Gather(kDefaultTimeout * 3);
+ ASSERT_FALSE(StreamHasMatchingCandidate(0, " 0.0.0.0 "));
+}
+
+TEST_F(WebRtcIceGatherTest, TestStunServerReturnsWildcardAddrV6) {
+ if (!TestStunServer::GetInstance(AF_INET6)) {
+ // No V6 addresses
+ return;
+ }
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ UseFakeStunUdpServerWithResponse("::", 3333);
+ peer_->AddStream(1);
+ Gather(kDefaultTimeout * 3);
+ ASSERT_FALSE(StreamHasMatchingCandidate(0, " :: "));
+}
+
+TEST_F(WebRtcIceGatherTest, TestStunServerReturnsPort0) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ UseFakeStunUdpServerWithResponse("192.0.2.133", 0);
+ peer_->AddStream(1);
+ Gather(kDefaultTimeout * 3);
+ ASSERT_FALSE(StreamHasMatchingCandidate(0, " 192.0.2.133 0 "));
+}
+
+TEST_F(WebRtcIceGatherTest, TestStunServerReturnsLoopbackAddr) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ UseFakeStunUdpServerWithResponse("127.0.0.133", 3333);
+ peer_->AddStream(1);
+ Gather(kDefaultTimeout * 3);
+ ASSERT_FALSE(StreamHasMatchingCandidate(0, " 127.0.0.133 "));
+}
+
+TEST_F(WebRtcIceGatherTest, TestStunServerReturnsLoopbackAddrV6) {
+ if (!TestStunServer::GetInstance(AF_INET6)) {
+ // No V6 addresses
+ return;
+ }
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ UseFakeStunUdpServerWithResponse("::1", 3333);
+ peer_->AddStream(1);
+ Gather(kDefaultTimeout * 3);
+ ASSERT_FALSE(StreamHasMatchingCandidate(0, " ::1 "));
+}
+
+TEST_F(WebRtcIceGatherTest, TestStunServerTrickle) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ UseFakeStunUdpServerWithResponse("192.0.2.1", 3333);
+ peer_->AddStream(1);
+ TestStunServer::GetInstance(AF_INET)->SetDropInitialPackets(3);
+ Gather(0);
+ ASSERT_FALSE(StreamHasMatchingCandidate(0, "192.0.2.1"));
+ WaitForGather();
+ ASSERT_TRUE(StreamHasMatchingCandidate(0, "192.0.2.1"));
+}
+
+// Test no host with our fake STUN server and apparently NATted.
+TEST_F(WebRtcIceGatherTest, TestFakeStunServerNatedNoHost) {
+ {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ }
+
+ NrIceCtx::Config config;
+ config.mPolicy = NrIceCtx::ICE_POLICY_NO_HOST;
+ peer_ = MakeUnique<IceTestPeer>("P1", test_utils_, true, config);
+ UseFakeStunUdpServerWithResponse("192.0.2.1", 3333);
+ peer_->AddStream(1);
+ Gather(0);
+ WaitForGather();
+ DumpAttributes(0);
+ ASSERT_FALSE(StreamHasMatchingCandidate(0, "host"));
+ ASSERT_TRUE(StreamHasMatchingCandidate(0, "srflx"));
+ NrIceCandidate default_candidate;
+ nsresult rv = peer_->GetDefaultCandidate(0, &default_candidate);
+ if (NS_SUCCEEDED(rv)) {
+ ASSERT_NE(NrIceCandidate::ICE_HOST, default_candidate.type);
+ }
+}
+
+// Test no host with our fake STUN server and apparently non-NATted.
+TEST_F(WebRtcIceGatherTest, TestFakeStunServerNoNatNoHost) {
+ {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ }
+
+ NrIceCtx::Config config;
+ config.mPolicy = NrIceCtx::ICE_POLICY_NO_HOST;
+ peer_ = MakeUnique<IceTestPeer>("P1", test_utils_, true, config);
+ UseTestStunServer();
+ peer_->AddStream(1);
+ Gather(0);
+ WaitForGather();
+ DumpAttributes(0);
+ ASSERT_FALSE(StreamHasMatchingCandidate(0, "host"));
+ ASSERT_TRUE(StreamHasMatchingCandidate(0, "srflx"));
+}
+
+// Test that srflx candidate is discarded in non-NATted environment if host
+// address obfuscation is not enabled.
+TEST_F(WebRtcIceGatherTest,
+ TestSrflxCandidateDiscardedWithObfuscateHostAddressesNotEnabled) {
+ {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ }
+
+ NrIceCtx::Config config;
+ peer_ = MakeUnique<IceTestPeer>("P1", test_utils_, true, config);
+ UseTestStunServer();
+ peer_->AddStream(1);
+ Gather(0, false, false);
+ WaitForGather();
+ DumpAttributes(0);
+ EXPECT_TRUE(StreamHasMatchingCandidate(0, "host"));
+ EXPECT_FALSE(StreamHasMatchingCandidate(0, "srflx"));
+}
+
+// Test that srflx candidate is generated in non-NATted environment if host
+// address obfuscation is enabled.
+TEST_F(WebRtcIceGatherTest,
+ TestSrflxCandidateGeneratedWithObfuscateHostAddressesEnabled) {
+ {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ }
+
+ NrIceCtx::Config config;
+ peer_ = MakeUnique<IceTestPeer>("P1", test_utils_, true, config);
+ UseTestStunServer();
+ peer_->AddStream(1);
+ Gather(0, false, true);
+ WaitForGather();
+ DumpAttributes(0);
+ EXPECT_TRUE(StreamHasMatchingCandidate(0, "host"));
+ EXPECT_TRUE(StreamHasMatchingCandidate(0, "srflx"));
+}
+
+TEST_F(WebRtcIceGatherTest, TestStunTcpServerTrickle) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = true;
+ NrIceCtx::InitializeGlobals(config);
+ UseFakeStunTcpServerWithResponse("192.0.3.1", 3333);
+ TestStunTcpServer::GetInstance(AF_INET)->SetDelay(500);
+ peer_->AddStream(1);
+ Gather(0);
+ ASSERT_FALSE(StreamHasMatchingCandidate(0, " 192.0.3.1 ", " tcptype "));
+ WaitForGather();
+ ASSERT_TRUE(StreamHasMatchingCandidate(0, " 192.0.3.1 ", " tcptype "));
+}
+
+TEST_F(WebRtcIceGatherTest, TestStunTcpAndUdpServerTrickle) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = true;
+ NrIceCtx::InitializeGlobals(config);
+ UseFakeStunUdpTcpServersWithResponse("192.0.2.1", 3333, "192.0.3.1", 3333);
+ TestStunServer::GetInstance(AF_INET)->SetDropInitialPackets(3);
+ TestStunTcpServer::GetInstance(AF_INET)->SetDelay(500);
+ peer_->AddStream(1);
+ Gather(0);
+ ASSERT_FALSE(StreamHasMatchingCandidate(0, "192.0.2.1", "UDP"));
+ ASSERT_FALSE(StreamHasMatchingCandidate(0, " 192.0.3.1 ", " tcptype "));
+ WaitForGather();
+ ASSERT_TRUE(StreamHasMatchingCandidate(0, "192.0.2.1", "UDP"));
+ ASSERT_TRUE(StreamHasMatchingCandidate(0, " 192.0.3.1 ", " tcptype "));
+}
+
+TEST_F(WebRtcIceGatherTest, TestSetIceControlling) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ EnsurePeer();
+ peer_->AddStream(1);
+ peer_->SetControlling(NrIceCtx::ICE_CONTROLLING);
+ NrIceCtx::Controlling controlling = peer_->GetControlling();
+ ASSERT_EQ(NrIceCtx::ICE_CONTROLLING, controlling);
+ // SetControlling should only allow setting this once
+ peer_->SetControlling(NrIceCtx::ICE_CONTROLLED);
+ controlling = peer_->GetControlling();
+ ASSERT_EQ(NrIceCtx::ICE_CONTROLLING, controlling);
+}
+
+TEST_F(WebRtcIceGatherTest, TestSetIceControlled) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ EnsurePeer();
+ peer_->AddStream(1);
+ peer_->SetControlling(NrIceCtx::ICE_CONTROLLED);
+ NrIceCtx::Controlling controlling = peer_->GetControlling();
+ ASSERT_EQ(NrIceCtx::ICE_CONTROLLED, controlling);
+ // SetControlling should only allow setting this once
+ peer_->SetControlling(NrIceCtx::ICE_CONTROLLING);
+ controlling = peer_->GetControlling();
+ ASSERT_EQ(NrIceCtx::ICE_CONTROLLED, controlling);
+}
+
+TEST_F(WebRtcIceConnectTest, TestGather) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+}
+
+TEST_F(WebRtcIceConnectTest, TestGatherTcp) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = true;
+ NrIceCtx::InitializeGlobals(config);
+ Init();
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+}
+
+TEST_F(WebRtcIceConnectTest, TestGatherAutoPrioritize) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ Init();
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnect) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ Connect();
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectRestartIce) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ Connect();
+ SendReceive(p1_.get(), p2_.get());
+
+ p2_->RestartIce();
+ ASSERT_FALSE(p2_->gathering_complete());
+
+ // verify p1 and p2 streams are still connected after restarting ice on p2
+ SendReceive(p1_.get(), p2_.get());
+
+ mozilla::UniquePtr<IceTestPeer> p3_;
+ p3_ = MakeUnique<IceTestPeer>("P3", test_utils_, true, NrIceCtx::Config());
+ InitPeer(p3_.get());
+ p3_->AddStream(1);
+
+ ASSERT_TRUE(GatherCallerAndCallee(p2_.get(), p3_.get()));
+ std::cout << "-------------------------------------------------" << std::endl;
+ ConnectCallerAndCallee(p3_.get(), p2_.get(), TRICKLE_SIMULATE);
+ SendReceive(p1_.get(), p2_.get()); // p1 and p2 are still connected
+ SendReceive(p3_.get(), p2_.get(), true, true); // p3 and p2 not yet connected
+ p2_->SimulateTrickle(0);
+ p3_->SimulateTrickle(0);
+ ASSERT_TRUE_WAIT(p3_->is_ready(0), kDefaultTimeout);
+ ASSERT_TRUE_WAIT(p2_->is_ready(0), kDefaultTimeout);
+ SendReceive(p1_.get(), p2_.get(), false, true); // p1 and p2 not connected
+ SendReceive(p3_.get(), p2_.get()); // p3 and p2 are now connected
+
+ p3_ = nullptr;
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectRestartIceThenAbort) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ Connect();
+ SendReceive(p1_.get(), p2_.get());
+
+ p2_->RestartIce();
+ ASSERT_FALSE(p2_->gathering_complete());
+
+ // verify p1 and p2 streams are still connected after restarting ice on p2
+ SendReceive(p1_.get(), p2_.get());
+
+ mozilla::UniquePtr<IceTestPeer> p3_;
+ p3_ = MakeUnique<IceTestPeer>("P3", test_utils_, true, NrIceCtx::Config());
+ InitPeer(p3_.get());
+ p3_->AddStream(1);
+
+ ASSERT_TRUE(GatherCallerAndCallee(p2_.get(), p3_.get()));
+ std::cout << "-------------------------------------------------" << std::endl;
+ p2_->RollbackIceRestart();
+ p2_->Connect(p1_.get(), TRICKLE_NONE);
+ SendReceive(p1_.get(), p2_.get());
+ p3_ = nullptr;
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectIceRestartRoleConflict) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ // Just for fun lets do this with switched rolls
+ p1_->SetControlling(NrIceCtx::ICE_CONTROLLED);
+ p2_->SetControlling(NrIceCtx::ICE_CONTROLLING);
+ Connect();
+ SendReceive(p1_.get(), p2_.get());
+ // Set rolls should not switch by connecting
+ ASSERT_EQ(NrIceCtx::ICE_CONTROLLED, p1_->GetControlling());
+ ASSERT_EQ(NrIceCtx::ICE_CONTROLLING, p2_->GetControlling());
+
+ p2_->RestartIce();
+ ASSERT_FALSE(p2_->gathering_complete());
+ p2_->SetControlling(NrIceCtx::ICE_CONTROLLED);
+ ASSERT_EQ(NrIceCtx::ICE_CONTROLLING, p2_->GetControlling())
+ << "ICE restart should not allow role to change, unless ice-lite happens";
+
+ mozilla::UniquePtr<IceTestPeer> p3_;
+ p3_ = MakeUnique<IceTestPeer>("P3", test_utils_, true, NrIceCtx::Config());
+ InitPeer(p3_.get());
+ p3_->AddStream(1);
+ // Set control role for p3 accordingly (with role conflict)
+ p3_->SetControlling(NrIceCtx::ICE_CONTROLLING);
+ ASSERT_EQ(NrIceCtx::ICE_CONTROLLING, p3_->GetControlling());
+
+ ASSERT_TRUE(GatherCallerAndCallee(p2_.get(), p3_.get()));
+ std::cout << "-------------------------------------------------" << std::endl;
+ ConnectCallerAndCallee(p3_.get(), p2_.get());
+ auto p2role = p2_->GetControlling();
+ ASSERT_NE(p2role, p3_->GetControlling()) << "Conflict should be resolved";
+ ASSERT_EQ(NrIceCtx::ICE_CONTROLLED, p1_->GetControlling())
+ << "P1 should be unaffected by role conflict";
+
+ // And again we are not allowed to switch roles at this point any more
+ p1_->SetControlling(NrIceCtx::ICE_CONTROLLING);
+ ASSERT_EQ(NrIceCtx::ICE_CONTROLLED, p1_->GetControlling());
+ p3_->SetControlling(p2role);
+ ASSERT_NE(p2role, p3_->GetControlling());
+
+ p3_ = nullptr;
+}
+
+TEST_F(WebRtcIceConnectTest,
+ TestIceRestartWithMultipleInterfacesAndUserStartingScreenSharing) {
+ const char* FAKE_WIFI_ADDR = "10.0.0.1";
+ const char* FAKE_WIFI_IF_NAME = "wlan9";
+
+ // prepare a fake wifi interface
+ nr_local_addr wifi_addr;
+ wifi_addr.interface.type = NR_INTERFACE_TYPE_WIFI;
+ wifi_addr.interface.estimated_speed = 1000;
+
+ int r = nr_str_port_to_transport_addr(FAKE_WIFI_ADDR, 0, IPPROTO_UDP,
+ &(wifi_addr.addr));
+ ASSERT_EQ(0, r);
+ strncpy(wifi_addr.addr.ifname, FAKE_WIFI_IF_NAME, MAXIFNAME);
+
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ // setup initial ICE connection between p1_ and p2_
+ UseNat();
+ AddStream(1);
+ SetExpectedTypes(NrIceCandidate::Type::ICE_SERVER_REFLEXIVE,
+ NrIceCandidate::Type::ICE_SERVER_REFLEXIVE);
+ ASSERT_TRUE(Gather(kDefaultTimeout, true));
+ Connect();
+
+ // verify the connection is working
+ SendReceive(p1_.get(), p2_.get());
+
+ // simulate user accepting permissions for screen sharing
+ p2_->SetCtxFlags(false);
+
+ // and having an additional non-default interface
+ nsTArray<NrIceStunAddr> stunAddr = p2_->GetStunAddrs();
+ stunAddr.InsertElementAt(0, NrIceStunAddr(&wifi_addr));
+ p2_->SetStunAddrs(stunAddr);
+
+ std::cout << "-------------------------------------------------" << std::endl;
+
+ // now restart ICE
+ p2_->RestartIce();
+ ASSERT_FALSE(p2_->gathering_complete());
+
+ // verify that we can successfully gather candidates
+ p2_->Gather();
+ EXPECT_TRUE_WAIT(p2_->gathering_complete(), kDefaultTimeout);
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectTcp) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = true;
+ NrIceCtx::InitializeGlobals(config);
+ Init();
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ SetCandidateFilter(IsTcpCandidate);
+ SetExpectedTypes(NrIceCandidate::Type::ICE_HOST,
+ NrIceCandidate::Type::ICE_HOST, kNrIceTransportTcp);
+ Connect();
+}
+
+// TCP SO tests works on localhost only with delay applied:
+// tc qdisc add dev lo root netem delay 10ms
+TEST_F(WebRtcIceConnectTest, DISABLED_TestConnectTcpSo) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = true;
+ NrIceCtx::InitializeGlobals(config);
+ Init();
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ SetCandidateFilter(IsTcpSoCandidate);
+ SetExpectedTypes(NrIceCandidate::Type::ICE_HOST,
+ NrIceCandidate::Type::ICE_HOST, kNrIceTransportTcp);
+ Connect();
+}
+
+// Disabled because this breaks with hairpinning.
+TEST_F(WebRtcIceConnectTest, DISABLED_TestConnectNoHost) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ Init(false, NrIceCtx::ICE_POLICY_NO_HOST);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ SetExpectedTypes(NrIceCandidate::Type::ICE_SERVER_REFLEXIVE,
+ NrIceCandidate::Type::ICE_SERVER_REFLEXIVE,
+ kNrIceTransportTcp);
+ Connect();
+}
+
+TEST_F(WebRtcIceConnectTest, TestLoopbackOnlySortOf) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ config.mAllowLoopback = true;
+ NrIceCtx::InitializeGlobals(config);
+ Init(false);
+ AddStream(1);
+ SetCandidateFilter(IsLoopbackCandidate);
+ ASSERT_TRUE(Gather());
+ SetExpectedRemoteCandidateAddr("127.0.0.1");
+ Connect();
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectBothControllingP1Wins) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ p1_->SetTiebreaker(1);
+ p2_->SetTiebreaker(0);
+ ASSERT_TRUE(Gather());
+ p1_->SetControlling(NrIceCtx::ICE_CONTROLLING);
+ p2_->SetControlling(NrIceCtx::ICE_CONTROLLING);
+ Connect();
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectBothControllingP2Wins) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ p1_->SetTiebreaker(0);
+ p2_->SetTiebreaker(1);
+ ASSERT_TRUE(Gather());
+ p1_->SetControlling(NrIceCtx::ICE_CONTROLLING);
+ p2_->SetControlling(NrIceCtx::ICE_CONTROLLING);
+ Connect();
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectIceLiteOfferer) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ p1_->SimulateIceLite();
+ Connect();
+}
+
+TEST_F(WebRtcIceConnectTest, TestTrickleBothControllingP1Wins) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ p1_->SetTiebreaker(1);
+ p2_->SetTiebreaker(0);
+ ASSERT_TRUE(Gather());
+ p1_->SetControlling(NrIceCtx::ICE_CONTROLLING);
+ p2_->SetControlling(NrIceCtx::ICE_CONTROLLING);
+ ConnectTrickle();
+ SimulateTrickle(0);
+ WaitForConnected(1000);
+ AssertCheckingReached();
+}
+
+TEST_F(WebRtcIceConnectTest, TestTrickleBothControllingP2Wins) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ p1_->SetTiebreaker(0);
+ p2_->SetTiebreaker(1);
+ ASSERT_TRUE(Gather());
+ p1_->SetControlling(NrIceCtx::ICE_CONTROLLING);
+ p2_->SetControlling(NrIceCtx::ICE_CONTROLLING);
+ ConnectTrickle();
+ SimulateTrickle(0);
+ WaitForConnected(1000);
+ AssertCheckingReached();
+}
+
+TEST_F(WebRtcIceConnectTest, TestTrickleIceLiteOfferer) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ p1_->SimulateIceLite();
+ ConnectTrickle();
+ SimulateTrickle(0);
+ WaitForConnected(1000);
+ AssertCheckingReached();
+}
+
+TEST_F(WebRtcIceConnectTest, TestGatherFullCone) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ UseNat();
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+}
+
+TEST_F(WebRtcIceConnectTest, TestGatherFullConeAutoPrioritize) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ UseNat();
+ Init();
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectFullCone) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ UseNat();
+ AddStream(1);
+ SetExpectedTypes(NrIceCandidate::Type::ICE_SERVER_REFLEXIVE,
+ NrIceCandidate::Type::ICE_SERVER_REFLEXIVE);
+ ASSERT_TRUE(Gather());
+ Connect();
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectNoNatNoHost) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ Init(false, NrIceCtx::ICE_POLICY_NO_HOST);
+ UseTestStunServer();
+ // Because we are connecting from our host candidate to the
+ // other side's apparent srflx (which is also their host)
+ // we see a host/srflx pair.
+ SetExpectedTypes(NrIceCandidate::Type::ICE_HOST,
+ NrIceCandidate::Type::ICE_SERVER_REFLEXIVE);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ Connect();
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectFullConeNoHost) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ UseNat();
+ Init(false, NrIceCtx::ICE_POLICY_NO_HOST);
+ UseTestStunServer();
+ SetExpectedTypes(NrIceCandidate::Type::ICE_SERVER_REFLEXIVE,
+ NrIceCandidate::Type::ICE_SERVER_REFLEXIVE);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ Connect();
+}
+
+TEST_F(WebRtcIceConnectTest, TestGatherAddressRestrictedCone) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ UseNat();
+ SetFilteringType(TestNat::ADDRESS_DEPENDENT);
+ SetMappingType(TestNat::ENDPOINT_INDEPENDENT);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectAddressRestrictedCone) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ UseNat();
+ SetFilteringType(TestNat::ADDRESS_DEPENDENT);
+ SetMappingType(TestNat::ENDPOINT_INDEPENDENT);
+ AddStream(1);
+ SetExpectedTypes(NrIceCandidate::Type::ICE_SERVER_REFLEXIVE,
+ NrIceCandidate::Type::ICE_SERVER_REFLEXIVE);
+ ASSERT_TRUE(Gather());
+ Connect();
+}
+
+TEST_F(WebRtcIceConnectTest, TestGatherPortRestrictedCone) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ UseNat();
+ SetFilteringType(TestNat::PORT_DEPENDENT);
+ SetMappingType(TestNat::ENDPOINT_INDEPENDENT);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectPortRestrictedCone) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ UseNat();
+ SetFilteringType(TestNat::PORT_DEPENDENT);
+ SetMappingType(TestNat::ENDPOINT_INDEPENDENT);
+ AddStream(1);
+ SetExpectedTypes(NrIceCandidate::Type::ICE_SERVER_REFLEXIVE,
+ NrIceCandidate::Type::ICE_SERVER_REFLEXIVE);
+ ASSERT_TRUE(Gather());
+ Connect();
+}
+
+TEST_F(WebRtcIceConnectTest, TestGatherSymmetricNat) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ UseNat();
+ SetFilteringType(TestNat::PORT_DEPENDENT);
+ SetMappingType(TestNat::PORT_DEPENDENT);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectSymmetricNat) {
+ if (turn_server_.empty()) return;
+
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ UseNat();
+ SetFilteringType(TestNat::PORT_DEPENDENT);
+ SetMappingType(TestNat::PORT_DEPENDENT);
+ p1_->SetExpectedTypes(NrIceCandidate::Type::ICE_RELAYED,
+ NrIceCandidate::Type::ICE_RELAYED);
+ p2_->SetExpectedTypes(NrIceCandidate::Type::ICE_RELAYED,
+ NrIceCandidate::Type::ICE_RELAYED);
+ SetTurnServer(turn_server_, kDefaultStunServerPort, turn_user_,
+ turn_password_);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ Connect();
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectSymmetricNatAndNoNat) {
+ {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = true;
+ NrIceCtx::InitializeGlobals(config);
+ }
+
+ NrIceCtx::Config config;
+ p1_ = MakeUnique<IceTestPeer>("P1", test_utils_, true, config);
+ p1_->UseNat();
+ p1_->SetFilteringType(TestNat::PORT_DEPENDENT);
+ p1_->SetMappingType(TestNat::PORT_DEPENDENT);
+
+ p2_ = MakeUnique<IceTestPeer>("P2", test_utils_, false, config);
+ initted_ = true;
+
+ AddStream(1);
+ p1_->SetExpectedTypes(NrIceCandidate::Type::ICE_PEER_REFLEXIVE,
+ NrIceCandidate::Type::ICE_HOST);
+ p2_->SetExpectedTypes(NrIceCandidate::Type::ICE_HOST,
+ NrIceCandidate::Type::ICE_PEER_REFLEXIVE);
+ ASSERT_TRUE(Gather());
+ Connect();
+}
+
+TEST_F(WebRtcIceConnectTest, TestGatherNatBlocksUDP) {
+ if (turn_server_.empty()) return;
+
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ UseNat();
+ BlockUdp();
+ std::vector<NrIceTurnServer> turn_servers;
+ std::vector<unsigned char> password_vec(turn_password_.begin(),
+ turn_password_.end());
+ turn_servers.push_back(
+ *NrIceTurnServer::Create(turn_server_, kDefaultStunServerPort, turn_user_,
+ password_vec, kNrIceTransportTcp));
+ turn_servers.push_back(
+ *NrIceTurnServer::Create(turn_server_, kDefaultStunServerPort, turn_user_,
+ password_vec, kNrIceTransportUdp));
+ SetTurnServers(turn_servers);
+ AddStream(1);
+ // We have to wait for the UDP-based stuff to time out.
+ ASSERT_TRUE(Gather(kDefaultTimeout * 3));
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectNatBlocksUDP) {
+ if (turn_server_.empty()) return;
+
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ UseNat();
+ BlockUdp();
+ std::vector<NrIceTurnServer> turn_servers;
+ std::vector<unsigned char> password_vec(turn_password_.begin(),
+ turn_password_.end());
+ turn_servers.push_back(
+ *NrIceTurnServer::Create(turn_server_, kDefaultStunServerPort, turn_user_,
+ password_vec, kNrIceTransportTcp));
+ turn_servers.push_back(
+ *NrIceTurnServer::Create(turn_server_, kDefaultStunServerPort, turn_user_,
+ password_vec, kNrIceTransportUdp));
+ SetTurnServers(turn_servers);
+ p1_->SetExpectedTypes(NrIceCandidate::Type::ICE_RELAYED,
+ NrIceCandidate::Type::ICE_RELAYED, kNrIceTransportTcp);
+ p2_->SetExpectedTypes(NrIceCandidate::Type::ICE_RELAYED,
+ NrIceCandidate::Type::ICE_RELAYED, kNrIceTransportTcp);
+ AddStream(1);
+ ASSERT_TRUE(Gather(kDefaultTimeout * 3));
+ Connect();
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectTwoComponents) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(2);
+ ASSERT_TRUE(Gather());
+ Connect();
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectTwoComponentsDisableSecond) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(2);
+ ASSERT_TRUE(Gather());
+ p1_->DisableComponent(0, 2);
+ p2_->DisableComponent(0, 2);
+ Connect();
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectP2ThenP1) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ ConnectP2();
+ PR_Sleep(1000);
+ ConnectP1();
+ WaitForConnectedStreams();
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectP2ThenP1Trickle) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ ConnectP2();
+ PR_Sleep(1000);
+ ConnectP1(TRICKLE_SIMULATE);
+ SimulateTrickleP1(0);
+ WaitForConnectedStreams();
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectP2ThenP1TrickleTwoComponents) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ AddStream(2);
+ ASSERT_TRUE(Gather());
+ ConnectP2();
+ PR_Sleep(1000);
+ ConnectP1(TRICKLE_SIMULATE);
+ SimulateTrickleP1(0);
+ std::cerr << "Sleeping between trickle streams" << std::endl;
+ PR_Sleep(1000); // Give this some time to settle but not complete
+ // all of ICE.
+ SimulateTrickleP1(1);
+ WaitForConnectedStreams(2);
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectAutoPrioritize) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ Init();
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ Connect();
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectTrickleOneStreamOneComponent) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ ConnectTrickle();
+ SimulateTrickle(0);
+ WaitForConnected(1000);
+ AssertCheckingReached();
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectTrickleTwoStreamsOneComponent) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ ConnectTrickle();
+ SimulateTrickle(0);
+ SimulateTrickle(1);
+ WaitForConnected(1000);
+ AssertCheckingReached();
+}
+
+void RealisticTrickleDelay(
+ std::vector<SchedulableTrickleCandidate*>& candidates) {
+ for (size_t i = 0; i < candidates.size(); ++i) {
+ SchedulableTrickleCandidate* cand = candidates[i];
+ if (cand->IsHost()) {
+ cand->Schedule(i * 10);
+ } else if (cand->IsReflexive()) {
+ cand->Schedule(i * 10 + 100);
+ } else if (cand->IsRelay()) {
+ cand->Schedule(i * 10 + 200);
+ }
+ }
+}
+
+void DelayRelayCandidates(std::vector<SchedulableTrickleCandidate*>& candidates,
+ unsigned int ms) {
+ for (auto& candidate : candidates) {
+ if (candidate->IsRelay()) {
+ candidate->Schedule(ms);
+ } else {
+ candidate->Schedule(0);
+ }
+ }
+}
+
+void AddNonPairableCandidates(
+ std::vector<SchedulableTrickleCandidate*>& candidates, IceTestPeer* peer,
+ size_t stream, int net_type, MtransportTestUtils* test_utils_) {
+ for (int i = 1; i < 5; i++) {
+ if (net_type == i) continue;
+ switch (i) {
+ case 1:
+ candidates.push_back(new SchedulableTrickleCandidate(
+ peer, stream,
+ "candidate:0 1 UDP 2113601790 10.0.0.1 12345 typ host", "",
+ test_utils_));
+ break;
+ case 2:
+ candidates.push_back(new SchedulableTrickleCandidate(
+ peer, stream,
+ "candidate:0 1 UDP 2113601791 172.16.1.1 12345 typ host", "",
+ test_utils_));
+ break;
+ case 3:
+ candidates.push_back(new SchedulableTrickleCandidate(
+ peer, stream,
+ "candidate:0 1 UDP 2113601792 192.168.0.1 12345 typ host", "",
+ test_utils_));
+ break;
+ case 4:
+ candidates.push_back(new SchedulableTrickleCandidate(
+ peer, stream,
+ "candidate:0 1 UDP 2113601793 100.64.1.1 12345 typ host", "",
+ test_utils_));
+ break;
+ default:
+ NR_UNIMPLEMENTED;
+ }
+ }
+
+ for (auto i = candidates.rbegin(); i != candidates.rend(); ++i) {
+ std::cerr << "Scheduling candidate: " << (*i)->Candidate().c_str()
+ << std::endl;
+ (*i)->Schedule(0);
+ }
+}
+
+void DropTrickleCandidates(
+ std::vector<SchedulableTrickleCandidate*>& candidates) {}
+
+TEST_F(WebRtcIceConnectTest, TestConnectTrickleAddStreamDuringICE) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ ConnectTrickle();
+ RealisticTrickleDelay(p1_->ControlTrickle(0));
+ RealisticTrickleDelay(p2_->ControlTrickle(0));
+ AddStream(1);
+ RealisticTrickleDelay(p1_->ControlTrickle(1));
+ RealisticTrickleDelay(p2_->ControlTrickle(1));
+ WaitForConnected(1000);
+ AssertCheckingReached();
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectTrickleAddStreamAfterICE) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ ConnectTrickle();
+ RealisticTrickleDelay(p1_->ControlTrickle(0));
+ RealisticTrickleDelay(p2_->ControlTrickle(0));
+ WaitForConnected(1000);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ ConnectTrickle();
+ RealisticTrickleDelay(p1_->ControlTrickle(1));
+ RealisticTrickleDelay(p2_->ControlTrickle(1));
+ WaitForConnected(1000);
+ AssertCheckingReached();
+}
+
+TEST_F(WebRtcIceConnectTest, RemoveStream) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ ConnectTrickle();
+ RealisticTrickleDelay(p1_->ControlTrickle(0));
+ RealisticTrickleDelay(p2_->ControlTrickle(0));
+ RealisticTrickleDelay(p1_->ControlTrickle(1));
+ RealisticTrickleDelay(p2_->ControlTrickle(1));
+ WaitForConnected(1000);
+
+ RemoveStream(0);
+ ASSERT_TRUE(Gather());
+ ConnectTrickle();
+}
+
+TEST_F(WebRtcIceConnectTest, P1NoTrickle) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ ConnectTrickle();
+ DropTrickleCandidates(p1_->ControlTrickle(0));
+ RealisticTrickleDelay(p2_->ControlTrickle(0));
+ WaitForConnected(1000);
+}
+
+TEST_F(WebRtcIceConnectTest, P2NoTrickle) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ ConnectTrickle();
+ RealisticTrickleDelay(p1_->ControlTrickle(0));
+ DropTrickleCandidates(p2_->ControlTrickle(0));
+ WaitForConnected(1000);
+}
+
+TEST_F(WebRtcIceConnectTest, RemoveAndAddStream) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ ConnectTrickle();
+ RealisticTrickleDelay(p1_->ControlTrickle(0));
+ RealisticTrickleDelay(p2_->ControlTrickle(0));
+ RealisticTrickleDelay(p1_->ControlTrickle(1));
+ RealisticTrickleDelay(p2_->ControlTrickle(1));
+ WaitForConnected(1000);
+
+ RemoveStream(0);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ ConnectTrickle();
+ RealisticTrickleDelay(p1_->ControlTrickle(2));
+ RealisticTrickleDelay(p2_->ControlTrickle(2));
+ WaitForConnected(1000);
+}
+
+TEST_F(WebRtcIceConnectTest, RemoveStreamBeforeGather) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ AddStream(1);
+ ASSERT_TRUE(Gather(0));
+ RemoveStream(0);
+ WaitForGather();
+ ConnectTrickle();
+ RealisticTrickleDelay(p1_->ControlTrickle(1));
+ RealisticTrickleDelay(p2_->ControlTrickle(1));
+ WaitForConnected(1000);
+}
+
+TEST_F(WebRtcIceConnectTest, RemoveStreamDuringGather) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ AddStream(1);
+ RemoveStream(0);
+ ASSERT_TRUE(Gather());
+ ConnectTrickle();
+ RealisticTrickleDelay(p1_->ControlTrickle(1));
+ RealisticTrickleDelay(p2_->ControlTrickle(1));
+ WaitForConnected(1000);
+}
+
+TEST_F(WebRtcIceConnectTest, RemoveStreamDuringConnect) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ ConnectTrickle();
+ RealisticTrickleDelay(p1_->ControlTrickle(0));
+ RealisticTrickleDelay(p2_->ControlTrickle(0));
+ RealisticTrickleDelay(p1_->ControlTrickle(1));
+ RealisticTrickleDelay(p2_->ControlTrickle(1));
+ RemoveStream(0);
+ WaitForConnected(1000);
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectRealTrickleOneStreamOneComponent) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ AddStream(1);
+ ASSERT_TRUE(Gather(0));
+ ConnectTrickle(TRICKLE_REAL);
+ WaitForConnected();
+ WaitForGather(); // ICE can complete before we finish gathering.
+ AssertCheckingReached();
+}
+
+TEST_F(WebRtcIceConnectTest, TestSendReceive) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ Connect();
+ SendReceive();
+}
+
+TEST_F(WebRtcIceConnectTest, TestSendReceiveTcp) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = true;
+ NrIceCtx::InitializeGlobals(config);
+ Init();
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ SetCandidateFilter(IsTcpCandidate);
+ SetExpectedTypes(NrIceCandidate::Type::ICE_HOST,
+ NrIceCandidate::Type::ICE_HOST, kNrIceTransportTcp);
+ Connect();
+ SendReceive();
+}
+
+// TCP SO tests works on localhost only with delay applied:
+// tc qdisc add dev lo root netem delay 10ms
+TEST_F(WebRtcIceConnectTest, DISABLED_TestSendReceiveTcpSo) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = true;
+ NrIceCtx::InitializeGlobals(config);
+ Init();
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ SetCandidateFilter(IsTcpSoCandidate);
+ SetExpectedTypes(NrIceCandidate::Type::ICE_HOST,
+ NrIceCandidate::Type::ICE_HOST, kNrIceTransportTcp);
+ Connect();
+ SendReceive();
+}
+
+TEST_F(WebRtcIceConnectTest, TestConsent) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ SetupAndCheckConsent();
+ PR_Sleep(1500);
+ AssertConsentRefresh();
+ SendReceive();
+}
+
+TEST_F(WebRtcIceConnectTest, TestConsentTcp) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = true;
+ NrIceCtx::InitializeGlobals(config);
+ Init();
+ AddStream(1);
+ SetCandidateFilter(IsTcpCandidate);
+ SetExpectedTypes(NrIceCandidate::Type::ICE_HOST,
+ NrIceCandidate::Type::ICE_HOST, kNrIceTransportTcp);
+ SetupAndCheckConsent();
+ PR_Sleep(1500);
+ AssertConsentRefresh();
+ SendReceive();
+}
+
+TEST_F(WebRtcIceConnectTest, TestConsentIntermittent) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ SetupAndCheckConsent();
+ p1_->SetBlockStun(true);
+ p2_->SetBlockStun(true);
+ WaitForDisconnected();
+ AssertConsentRefresh(CONSENT_STALE);
+ SendReceive();
+ p1_->SetBlockStun(false);
+ p2_->SetBlockStun(false);
+ WaitForConnected();
+ AssertConsentRefresh();
+ SendReceive();
+ p1_->SetBlockStun(true);
+ p2_->SetBlockStun(true);
+ WaitForDisconnected();
+ AssertConsentRefresh(CONSENT_STALE);
+ SendReceive();
+ p1_->SetBlockStun(false);
+ p2_->SetBlockStun(false);
+ WaitForConnected();
+ AssertConsentRefresh();
+}
+
+TEST_F(WebRtcIceConnectTest, TestConsentTimeout) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ SetupAndCheckConsent();
+ p1_->SetBlockStun(true);
+ p2_->SetBlockStun(true);
+ WaitForDisconnected();
+ AssertConsentRefresh(CONSENT_STALE);
+ SendReceive();
+ WaitForFailed();
+ AssertConsentRefresh(CONSENT_EXPIRED);
+ SendFailure();
+}
+
+TEST_F(WebRtcIceConnectTest, TestConsentDelayed) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ SetupAndCheckConsent();
+ /* Note: We don't have a list of STUN transaction IDs of the previously timed
+ out consent requests. Thus responses after sending the next consent
+ request are ignored. */
+ p1_->SetStunResponseDelay(200);
+ p2_->SetStunResponseDelay(200);
+ PR_Sleep(1000);
+ AssertConsentRefresh();
+ SendReceive();
+}
+
+TEST_F(WebRtcIceConnectTest, TestNetworkForcedOfflineAndRecovery) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ SetupAndCheckConsent();
+ p1_->ChangeNetworkStateToOffline();
+ ASSERT_TRUE_WAIT(p1_->ice_connected() == 0, kDefaultTimeout);
+ // Next round of consent check should switch it back to online
+ ASSERT_TRUE_WAIT(p1_->ice_connected(), kDefaultTimeout);
+}
+
+TEST_F(WebRtcIceConnectTest, TestNetworkForcedOfflineTwice) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ SetupAndCheckConsent();
+ p2_->ChangeNetworkStateToOffline();
+ ASSERT_TRUE_WAIT(p2_->ice_connected() == 0, kDefaultTimeout);
+ p2_->ChangeNetworkStateToOffline();
+ ASSERT_TRUE_WAIT(p2_->ice_connected() == 0, kDefaultTimeout);
+}
+
+TEST_F(WebRtcIceConnectTest, TestNetworkOnlineDoesntChangeState) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ SetupAndCheckConsent();
+ p2_->ChangeNetworkStateToOnline();
+ ASSERT_TRUE(p2_->ice_connected());
+ PR_Sleep(1500);
+ p2_->ChangeNetworkStateToOnline();
+ ASSERT_TRUE(p2_->ice_connected());
+}
+
+TEST_F(WebRtcIceConnectTest, TestNetworkOnlineTriggersConsent) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ // Let's emulate audio + video w/o rtcp-mux
+ AddStream(2);
+ AddStream(2);
+ SetupAndCheckConsent();
+ p1_->ChangeNetworkStateToOffline();
+ p1_->SetBlockStun(true);
+ ASSERT_TRUE_WAIT(p1_->ice_connected() == 0, kDefaultTimeout);
+ PR_Sleep(1500);
+ ASSERT_TRUE(p1_->ice_connected() == 0);
+ p1_->SetBlockStun(false);
+ p1_->ChangeNetworkStateToOnline();
+ ASSERT_TRUE_WAIT(p1_->ice_connected(), 500);
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectTurn) {
+ if (turn_server_.empty()) return;
+
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ SetTurnServer(turn_server_, kDefaultStunServerPort, turn_user_,
+ turn_password_);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ Connect();
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectTurnWithDelay) {
+ if (turn_server_.empty()) return;
+
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ SetTurnServer(turn_server_, kDefaultStunServerPort, turn_user_,
+ turn_password_);
+ SetCandidateFilter(SabotageHostCandidateAndDropReflexive);
+ AddStream(1);
+ p1_->Gather();
+ PR_Sleep(500);
+ p2_->Gather();
+ ConnectTrickle(TRICKLE_REAL);
+ WaitForGather();
+ WaitForConnectedStreams();
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectTurnWithNormalTrickleDelay) {
+ if (turn_server_.empty()) return;
+
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ SetTurnServer(turn_server_, kDefaultStunServerPort, turn_user_,
+ turn_password_);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ ConnectTrickle();
+ RealisticTrickleDelay(p1_->ControlTrickle(0));
+ RealisticTrickleDelay(p2_->ControlTrickle(0));
+
+ WaitForConnected();
+ AssertCheckingReached();
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectTurnWithNormalTrickleDelayOneSided) {
+ if (turn_server_.empty()) return;
+
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ SetTurnServer(turn_server_, kDefaultStunServerPort, turn_user_,
+ turn_password_);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ ConnectTrickle();
+ RealisticTrickleDelay(p1_->ControlTrickle(0));
+ p2_->SimulateTrickle(0);
+
+ WaitForConnected();
+ AssertCheckingReached();
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectTurnWithLargeTrickleDelay) {
+ if (turn_server_.empty()) return;
+
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ SetTurnServer(turn_server_, kDefaultStunServerPort, turn_user_,
+ turn_password_);
+ SetCandidateFilter(SabotageHostCandidateAndDropReflexive);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ ConnectTrickle();
+ // Trickle host candidates immediately, but delay relay candidates
+ DelayRelayCandidates(p1_->ControlTrickle(0), 3700);
+ DelayRelayCandidates(p2_->ControlTrickle(0), 3700);
+
+ WaitForConnected();
+ AssertCheckingReached();
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectTurnTcp) {
+ if (turn_server_.empty()) return;
+
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ SetTurnServer(turn_server_, kDefaultStunServerPort, turn_user_,
+ turn_password_, kNrIceTransportTcp);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ Connect();
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectTurnOnly) {
+ if (turn_server_.empty()) return;
+
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ SetTurnServer(turn_server_, kDefaultStunServerPort, turn_user_,
+ turn_password_);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ SetCandidateFilter(IsRelayCandidate);
+ SetExpectedTypes(NrIceCandidate::Type::ICE_RELAYED,
+ NrIceCandidate::Type::ICE_RELAYED);
+ Connect();
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectTurnTcpOnly) {
+ if (turn_server_.empty()) return;
+
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ SetTurnServer(turn_server_, kDefaultStunServerPort, turn_user_,
+ turn_password_, kNrIceTransportTcp);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ SetCandidateFilter(IsRelayCandidate);
+ SetExpectedTypes(NrIceCandidate::Type::ICE_RELAYED,
+ NrIceCandidate::Type::ICE_RELAYED, kNrIceTransportTcp);
+ Connect();
+}
+
+TEST_F(WebRtcIceConnectTest, TestSendReceiveTurnOnly) {
+ if (turn_server_.empty()) return;
+
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ SetTurnServer(turn_server_, kDefaultStunServerPort, turn_user_,
+ turn_password_);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ SetCandidateFilter(IsRelayCandidate);
+ SetExpectedTypes(NrIceCandidate::Type::ICE_RELAYED,
+ NrIceCandidate::Type::ICE_RELAYED);
+ Connect();
+ SendReceive();
+}
+
+TEST_F(WebRtcIceConnectTest, TestSendReceiveTurnTcpOnly) {
+ if (turn_server_.empty()) return;
+
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ SetTurnServer(turn_server_, kDefaultStunServerPort, turn_user_,
+ turn_password_, kNrIceTransportTcp);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ SetCandidateFilter(IsRelayCandidate);
+ SetExpectedTypes(NrIceCandidate::Type::ICE_RELAYED,
+ NrIceCandidate::Type::ICE_RELAYED, kNrIceTransportTcp);
+ Connect();
+ SendReceive();
+}
+
+TEST_F(WebRtcIceConnectTest, TestSendReceiveTurnBothOnly) {
+ if (turn_server_.empty()) return;
+
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ std::vector<NrIceTurnServer> turn_servers;
+ std::vector<unsigned char> password_vec(turn_password_.begin(),
+ turn_password_.end());
+ turn_servers.push_back(
+ *NrIceTurnServer::Create(turn_server_, kDefaultStunServerPort, turn_user_,
+ password_vec, kNrIceTransportTcp));
+ turn_servers.push_back(
+ *NrIceTurnServer::Create(turn_server_, kDefaultStunServerPort, turn_user_,
+ password_vec, kNrIceTransportUdp));
+ SetTurnServers(turn_servers);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ SetCandidateFilter(IsRelayCandidate);
+ // UDP is preferred.
+ SetExpectedTypes(NrIceCandidate::Type::ICE_RELAYED,
+ NrIceCandidate::Type::ICE_RELAYED, kNrIceTransportUdp);
+ Connect();
+ SendReceive();
+}
+
+TEST_F(WebRtcIceConnectTest, TestConnectShutdownOneSide) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ ConnectThenDelete();
+}
+
+TEST_F(WebRtcIceConnectTest, TestPollCandPairsBeforeConnect) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+
+ std::vector<NrIceCandidatePair> pairs;
+ nsresult res = p1_->GetCandidatePairs(0, &pairs);
+ // There should be no candidate pairs prior to calling Connect()
+ ASSERT_EQ(NS_OK, res);
+ ASSERT_EQ(0U, pairs.size());
+
+ res = p2_->GetCandidatePairs(0, &pairs);
+ ASSERT_EQ(NS_OK, res);
+ ASSERT_EQ(0U, pairs.size());
+}
+
+TEST_F(WebRtcIceConnectTest, TestPollCandPairsAfterConnect) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ Connect();
+
+ std::vector<NrIceCandidatePair> pairs;
+ nsresult r = p1_->GetCandidatePairs(0, &pairs);
+ ASSERT_EQ(NS_OK, r);
+ // How detailed of a check do we want to do here? If the turn server is
+ // functioning, we'll get at least two pairs, but this is probably not
+ // something we should assume.
+ ASSERT_NE(0U, pairs.size());
+ ASSERT_TRUE(p1_->CandidatePairsPriorityDescending(pairs));
+ ASSERT_TRUE(ContainsSucceededPair(pairs));
+ pairs.clear();
+
+ r = p2_->GetCandidatePairs(0, &pairs);
+ ASSERT_EQ(NS_OK, r);
+ ASSERT_NE(0U, pairs.size());
+ ASSERT_TRUE(p2_->CandidatePairsPriorityDescending(pairs));
+ ASSERT_TRUE(ContainsSucceededPair(pairs));
+}
+
+// TODO Bug 1259842 - disabled until we find a better way to handle two
+// candidates from different RFC1918 ranges
+TEST_F(WebRtcIceConnectTest, DISABLED_TestHostCandPairingFilter) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ Init(false);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ SetCandidateFilter(IsIpv4Candidate);
+
+ int host_net = p1_->GetCandidatesPrivateIpv4Range(0);
+ if (host_net <= 0) {
+ // TODO bug 1226838: make this work with multiple private IPs
+ FAIL() << "This test needs exactly one private IPv4 host candidate to work"
+ << std::endl;
+ }
+
+ ConnectTrickle();
+ AddNonPairableCandidates(p1_->ControlTrickle(0), p1_.get(), 0, host_net,
+ test_utils_);
+ AddNonPairableCandidates(p2_->ControlTrickle(0), p2_.get(), 0, host_net,
+ test_utils_);
+
+ std::vector<NrIceCandidatePair> pairs;
+ p1_->GetCandidatePairs(0, &pairs);
+ for (auto p : pairs) {
+ std::cerr << "Verifying pair:" << std::endl;
+ p1_->DumpCandidatePair(p);
+ nr_transport_addr addr;
+ nr_str_port_to_transport_addr(p.local.local_addr.host.c_str(), 0,
+ IPPROTO_UDP, &addr);
+ ASSERT_TRUE(nr_transport_addr_get_private_addr_range(&addr) == host_net);
+ nr_str_port_to_transport_addr(p.remote.cand_addr.host.c_str(), 0,
+ IPPROTO_UDP, &addr);
+ ASSERT_TRUE(nr_transport_addr_get_private_addr_range(&addr) == host_net);
+ }
+}
+
+// TODO Bug 1226838 - See Comment 2 - this test can't work as written
+TEST_F(WebRtcIceConnectTest, DISABLED_TestSrflxCandPairingFilter) {
+ if (stun_server_address_.empty()) {
+ return;
+ }
+
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ Init(false);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+ SetCandidateFilter(IsSrflxCandidate);
+
+ if (p1_->GetCandidatesPrivateIpv4Range(0) <= 0) {
+ // TODO bug 1226838: make this work with public IP addresses
+ std::cerr << "Don't run this test at IETF meetings!" << std::endl;
+ FAIL() << "This test needs one private IPv4 host candidate to work"
+ << std::endl;
+ }
+
+ ConnectTrickle();
+ SimulateTrickleP1(0);
+ SimulateTrickleP2(0);
+
+ std::vector<NrIceCandidatePair> pairs;
+ p1_->GetCandidatePairs(0, &pairs);
+ for (auto p : pairs) {
+ std::cerr << "Verifying P1 pair:" << std::endl;
+ p1_->DumpCandidatePair(p);
+ nr_transport_addr addr;
+ nr_str_port_to_transport_addr(p.local.local_addr.host.c_str(), 0,
+ IPPROTO_UDP, &addr);
+ ASSERT_TRUE(nr_transport_addr_get_private_addr_range(&addr) != 0);
+ nr_str_port_to_transport_addr(p.remote.cand_addr.host.c_str(), 0,
+ IPPROTO_UDP, &addr);
+ ASSERT_TRUE(nr_transport_addr_get_private_addr_range(&addr) == 0);
+ }
+ p2_->GetCandidatePairs(0, &pairs);
+ for (auto p : pairs) {
+ std::cerr << "Verifying P2 pair:" << std::endl;
+ p2_->DumpCandidatePair(p);
+ nr_transport_addr addr;
+ nr_str_port_to_transport_addr(p.local.local_addr.host.c_str(), 0,
+ IPPROTO_UDP, &addr);
+ ASSERT_TRUE(nr_transport_addr_get_private_addr_range(&addr) != 0);
+ nr_str_port_to_transport_addr(p.remote.cand_addr.host.c_str(), 0,
+ IPPROTO_UDP, &addr);
+ ASSERT_TRUE(nr_transport_addr_get_private_addr_range(&addr) == 0);
+ }
+}
+
+TEST_F(WebRtcIceConnectTest, TestPollCandPairsDuringConnect) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+
+ p2_->Connect(p1_.get(), TRICKLE_NONE, false);
+ p1_->Connect(p2_.get(), TRICKLE_NONE, false);
+
+ std::vector<NrIceCandidatePair> pairs1;
+ std::vector<NrIceCandidatePair> pairs2;
+
+ p1_->StartChecks();
+ p1_->UpdateAndValidateCandidatePairs(0, &pairs1);
+ p2_->UpdateAndValidateCandidatePairs(0, &pairs2);
+
+ p2_->StartChecks();
+ p1_->UpdateAndValidateCandidatePairs(0, &pairs1);
+ p2_->UpdateAndValidateCandidatePairs(0, &pairs2);
+
+ WaitForConnectedStreams();
+ p1_->UpdateAndValidateCandidatePairs(0, &pairs1);
+ p2_->UpdateAndValidateCandidatePairs(0, &pairs2);
+ ASSERT_TRUE(ContainsSucceededPair(pairs1));
+ ASSERT_TRUE(ContainsSucceededPair(pairs2));
+}
+
+TEST_F(WebRtcIceConnectTest, TestRLogConnector) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ ASSERT_TRUE(Gather());
+
+ p2_->Connect(p1_.get(), TRICKLE_NONE, false);
+ p1_->Connect(p2_.get(), TRICKLE_NONE, false);
+
+ std::vector<NrIceCandidatePair> pairs1;
+ std::vector<NrIceCandidatePair> pairs2;
+
+ p1_->StartChecks();
+ p1_->UpdateAndValidateCandidatePairs(0, &pairs1);
+ p2_->UpdateAndValidateCandidatePairs(0, &pairs2);
+
+ p2_->StartChecks();
+ p1_->UpdateAndValidateCandidatePairs(0, &pairs1);
+ p2_->UpdateAndValidateCandidatePairs(0, &pairs2);
+
+ WaitForConnectedStreams();
+ p1_->UpdateAndValidateCandidatePairs(0, &pairs1);
+ p2_->UpdateAndValidateCandidatePairs(0, &pairs2);
+ ASSERT_TRUE(ContainsSucceededPair(pairs1));
+ ASSERT_TRUE(ContainsSucceededPair(pairs2));
+
+ for (auto& p : pairs1) {
+ std::deque<std::string> logs;
+ std::string substring("CAND-PAIR(");
+ substring += p.codeword;
+ RLogConnector::GetInstance()->Filter(substring, 0, &logs);
+ ASSERT_NE(0U, logs.size());
+ }
+
+ for (auto& p : pairs2) {
+ std::deque<std::string> logs;
+ std::string substring("CAND-PAIR(");
+ substring += p.codeword;
+ RLogConnector::GetInstance()->Filter(substring, 0, &logs);
+ ASSERT_NE(0U, logs.size());
+ }
+}
+
+// Verify that a bogus candidate doesn't cause crashes on the
+// main thread. See bug 856433.
+TEST_F(WebRtcIceConnectTest, TestBogusCandidate) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ Gather();
+ ConnectTrickle();
+ p1_->ParseCandidate(0, kBogusIceCandidate, "");
+
+ std::vector<NrIceCandidatePair> pairs;
+ nsresult res = p1_->GetCandidatePairs(0, &pairs);
+ ASSERT_EQ(NS_OK, res);
+ ASSERT_EQ(0U, pairs.size());
+}
+
+TEST_F(WebRtcIceConnectTest, TestNonMDNSCandidate) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ Gather();
+ ConnectTrickle();
+ p1_->ParseCandidate(0, kUnreachableHostIceCandidate, "");
+
+ std::vector<NrIceCandidatePair> pairs;
+ nsresult res = p1_->GetCandidatePairs(0, &pairs);
+ ASSERT_EQ(NS_OK, res);
+ ASSERT_EQ(1U, pairs.size());
+ ASSERT_EQ(pairs[0].remote.mdns_addr, "");
+}
+
+TEST_F(WebRtcIceConnectTest, TestMDNSCandidate) {
+ NrIceCtx::GlobalConfig config;
+ config.mTcpEnabled = false;
+ NrIceCtx::InitializeGlobals(config);
+ AddStream(1);
+ Gather();
+ ConnectTrickle();
+ p1_->ParseCandidate(0, kUnreachableHostIceCandidate, "host.local");
+
+ std::vector<NrIceCandidatePair> pairs;
+ nsresult res = p1_->GetCandidatePairs(0, &pairs);
+ ASSERT_EQ(NS_OK, res);
+ ASSERT_EQ(1U, pairs.size());
+ ASSERT_EQ(pairs[0].remote.mdns_addr, "host.local");
+}
+
+TEST_F(WebRtcIcePrioritizerTest, TestPrioritizer) {
+ SetPriorizer(::mozilla::CreateInterfacePrioritizer());
+
+ AddInterface("0", NR_INTERFACE_TYPE_VPN, 100); // unknown vpn
+ AddInterface("1", NR_INTERFACE_TYPE_VPN | NR_INTERFACE_TYPE_WIRED,
+ 100); // wired vpn
+ AddInterface("2", NR_INTERFACE_TYPE_VPN | NR_INTERFACE_TYPE_WIFI,
+ 100); // wifi vpn
+ AddInterface("3", NR_INTERFACE_TYPE_VPN | NR_INTERFACE_TYPE_MOBILE,
+ 100); // wifi vpn
+ AddInterface("4", NR_INTERFACE_TYPE_WIRED, 1000); // wired, high speed
+ AddInterface("5", NR_INTERFACE_TYPE_WIRED, 10); // wired, low speed
+ AddInterface("6", NR_INTERFACE_TYPE_WIFI, 10); // wifi, low speed
+ AddInterface("7", NR_INTERFACE_TYPE_WIFI, 1000); // wifi, high speed
+ AddInterface("8", NR_INTERFACE_TYPE_MOBILE, 10); // mobile, low speed
+ AddInterface("9", NR_INTERFACE_TYPE_MOBILE, 1000); // mobile, high speed
+ AddInterface("10", NR_INTERFACE_TYPE_UNKNOWN, 10); // unknown, low speed
+ AddInterface("11", NR_INTERFACE_TYPE_UNKNOWN, 1000); // unknown, high speed
+
+ // expected preference "4" > "5" > "1" > "7" > "6" > "2" > "9" > "8" > "3" >
+ // "11" > "10" > "0"
+
+ HasLowerPreference("0", "10");
+ HasLowerPreference("10", "11");
+ HasLowerPreference("11", "3");
+ HasLowerPreference("3", "8");
+ HasLowerPreference("8", "9");
+ HasLowerPreference("9", "2");
+ HasLowerPreference("2", "6");
+ HasLowerPreference("6", "7");
+ HasLowerPreference("7", "1");
+ HasLowerPreference("1", "5");
+ HasLowerPreference("5", "4");
+}
+
+TEST_F(WebRtcIcePacketFilterTest, TestSendNonStunPacket) {
+ const unsigned char data[] = "12345abcde";
+ TestOutgoing(data, sizeof(data), 123, 45, false);
+ TestOutgoingTcp(data, sizeof(data), false);
+}
+
+TEST_F(WebRtcIcePacketFilterTest, TestRecvNonStunPacket) {
+ const unsigned char data[] = "12345abcde";
+ TestIncoming(data, sizeof(data), 123, 45, false);
+ TestIncomingTcp(data, sizeof(data), true);
+}
+
+TEST_F(WebRtcIcePacketFilterTest, TestSendStunPacket) {
+ nr_stun_message* msg;
+ ASSERT_EQ(0, nr_stun_build_req_no_auth(nullptr, &msg));
+ msg->header.type = NR_STUN_MSG_BINDING_REQUEST;
+ ASSERT_EQ(0, nr_stun_encode_message(msg));
+ TestOutgoing(msg->buffer, msg->length, 123, 45, true);
+ TestOutgoingTcp(msg->buffer, msg->length, true);
+ TestOutgoingTcpFramed(msg->buffer, msg->length, true);
+ ASSERT_EQ(0, nr_stun_message_destroy(&msg));
+}
+
+TEST_F(WebRtcIcePacketFilterTest, TestRecvStunPacketWithoutAPendingId) {
+ nr_stun_message* msg;
+ ASSERT_EQ(0, nr_stun_build_req_no_auth(nullptr, &msg));
+
+ msg->header.id.octet[0] = 1;
+ msg->header.type = NR_STUN_MSG_BINDING_REQUEST;
+ ASSERT_EQ(0, nr_stun_encode_message(msg));
+ TestOutgoing(msg->buffer, msg->length, 123, 45, true);
+ TestOutgoingTcp(msg->buffer, msg->length, true);
+
+ msg->header.id.octet[0] = 0;
+ msg->header.type = NR_STUN_MSG_BINDING_RESPONSE;
+ ASSERT_EQ(0, nr_stun_encode_message(msg));
+ TestIncoming(msg->buffer, msg->length, 123, 45, true);
+ TestIncomingTcp(msg->buffer, msg->length, true);
+
+ ASSERT_EQ(0, nr_stun_message_destroy(&msg));
+}
+
+TEST_F(WebRtcIcePacketFilterTest, TestRecvStunBindingRequestWithoutAPendingId) {
+ nr_stun_message* msg;
+ ASSERT_EQ(0, nr_stun_build_req_no_auth(nullptr, &msg));
+
+ msg->header.id.octet[0] = 1;
+ msg->header.type = NR_STUN_MSG_BINDING_REQUEST;
+ ASSERT_EQ(0, nr_stun_encode_message(msg));
+ TestIncoming(msg->buffer, msg->length, 123, 45, true);
+ TestIncomingTcp(msg->buffer, msg->length, true);
+
+ msg->header.id.octet[0] = 1;
+ msg->header.type = NR_STUN_MSG_BINDING_RESPONSE;
+ ASSERT_EQ(0, nr_stun_encode_message(msg));
+ TestOutgoing(msg->buffer, msg->length, 123, 45, true);
+ TestOutgoingTcp(msg->buffer, msg->length, true);
+
+ ASSERT_EQ(0, nr_stun_message_destroy(&msg));
+}
+
+TEST_F(WebRtcIcePacketFilterTest,
+ TestRecvStunPacketWithoutAPendingIdTcpFramed) {
+ nr_stun_message* msg;
+ ASSERT_EQ(0, nr_stun_build_req_no_auth(nullptr, &msg));
+
+ msg->header.id.octet[0] = 1;
+ msg->header.type = NR_STUN_MSG_BINDING_REQUEST;
+ ASSERT_EQ(0, nr_stun_encode_message(msg));
+ TestOutgoingTcpFramed(msg->buffer, msg->length, true);
+
+ msg->header.id.octet[0] = 0;
+ msg->header.type = NR_STUN_MSG_BINDING_RESPONSE;
+ ASSERT_EQ(0, nr_stun_encode_message(msg));
+ TestIncomingTcpFramed(msg->buffer, msg->length, true);
+
+ ASSERT_EQ(0, nr_stun_message_destroy(&msg));
+}
+
+TEST_F(WebRtcIcePacketFilterTest, TestRecvStunPacketWithoutAPendingAddress) {
+ nr_stun_message* msg;
+ ASSERT_EQ(0, nr_stun_build_req_no_auth(nullptr, &msg));
+
+ msg->header.type = NR_STUN_MSG_BINDING_REQUEST;
+ ASSERT_EQ(0, nr_stun_encode_message(msg));
+ TestOutgoing(msg->buffer, msg->length, 123, 45, true);
+ // nothing to test here for the TCP filter
+
+ msg->header.type = NR_STUN_MSG_BINDING_RESPONSE;
+ ASSERT_EQ(0, nr_stun_encode_message(msg));
+ TestIncoming(msg->buffer, msg->length, 123, 46, false);
+ TestIncoming(msg->buffer, msg->length, 124, 45, false);
+
+ ASSERT_EQ(0, nr_stun_message_destroy(&msg));
+}
+
+TEST_F(WebRtcIcePacketFilterTest, TestRecvStunPacketWithPendingIdAndAddress) {
+ nr_stun_message* msg;
+ ASSERT_EQ(0, nr_stun_build_req_no_auth(nullptr, &msg));
+
+ msg->header.type = NR_STUN_MSG_BINDING_REQUEST;
+ ASSERT_EQ(0, nr_stun_encode_message(msg));
+ TestOutgoing(msg->buffer, msg->length, 123, 45, true);
+ TestOutgoingTcp(msg->buffer, msg->length, true);
+
+ msg->header.type = NR_STUN_MSG_BINDING_RESPONSE;
+ ASSERT_EQ(0, nr_stun_encode_message(msg));
+ TestIncoming(msg->buffer, msg->length, 123, 45, true);
+ TestIncomingTcp(msg->buffer, msg->length, true);
+
+ // Test whitelist by filtering non-stun packets.
+ const unsigned char data[] = "12345abcde";
+
+ // 123:45 is white-listed.
+ TestOutgoing(data, sizeof(data), 123, 45, true);
+ TestOutgoingTcp(data, sizeof(data), true);
+ TestIncoming(data, sizeof(data), 123, 45, true);
+ TestIncomingTcp(data, sizeof(data), true);
+
+ // Indications pass as well.
+ msg->header.type = NR_STUN_MSG_BINDING_INDICATION;
+ ASSERT_EQ(0, nr_stun_encode_message(msg));
+ TestOutgoing(msg->buffer, msg->length, 123, 45, true);
+ TestOutgoingTcp(msg->buffer, msg->length, true);
+ TestIncoming(msg->buffer, msg->length, 123, 45, true);
+ TestIncomingTcp(msg->buffer, msg->length, true);
+
+ // Packets from and to other address are still disallowed.
+ // Note: this doesn't apply for TCP connections
+ TestOutgoing(data, sizeof(data), 123, 46, false);
+ TestIncoming(data, sizeof(data), 123, 46, false);
+ TestOutgoing(data, sizeof(data), 124, 45, false);
+ TestIncoming(data, sizeof(data), 124, 45, false);
+
+ ASSERT_EQ(0, nr_stun_message_destroy(&msg));
+}
+
+TEST_F(WebRtcIcePacketFilterTest, TestRecvStunPacketWithPendingIdTcpFramed) {
+ nr_stun_message* msg;
+ ASSERT_EQ(0, nr_stun_build_req_no_auth(nullptr, &msg));
+
+ msg->header.type = NR_STUN_MSG_BINDING_REQUEST;
+ ASSERT_EQ(0, nr_stun_encode_message(msg));
+ TestOutgoingTcpFramed(msg->buffer, msg->length, true);
+
+ msg->header.type = NR_STUN_MSG_BINDING_RESPONSE;
+ ASSERT_EQ(0, nr_stun_encode_message(msg));
+ TestIncomingTcpFramed(msg->buffer, msg->length, true);
+
+ // Test whitelist by filtering non-stun packets.
+ const unsigned char data[] = "12345abcde";
+
+ TestOutgoingTcpFramed(data, sizeof(data), true);
+ TestIncomingTcpFramed(data, sizeof(data), true);
+
+ ASSERT_EQ(0, nr_stun_message_destroy(&msg));
+}
+
+TEST_F(WebRtcIcePacketFilterTest, TestSendNonRequestStunPacket) {
+ nr_stun_message* msg;
+ ASSERT_EQ(0, nr_stun_build_req_no_auth(nullptr, &msg));
+
+ msg->header.type = NR_STUN_MSG_BINDING_RESPONSE;
+ ASSERT_EQ(0, nr_stun_encode_message(msg));
+ TestOutgoing(msg->buffer, msg->length, 123, 45, false);
+ TestOutgoingTcp(msg->buffer, msg->length, false);
+
+ // Send a packet so we allow the incoming request.
+ msg->header.type = NR_STUN_MSG_BINDING_REQUEST;
+ ASSERT_EQ(0, nr_stun_encode_message(msg));
+ TestOutgoing(msg->buffer, msg->length, 123, 45, true);
+ TestOutgoingTcp(msg->buffer, msg->length, true);
+
+ // This packet makes us able to send a response.
+ msg->header.type = NR_STUN_MSG_BINDING_REQUEST;
+ ASSERT_EQ(0, nr_stun_encode_message(msg));
+ TestIncoming(msg->buffer, msg->length, 123, 45, true);
+ TestIncomingTcp(msg->buffer, msg->length, true);
+
+ msg->header.type = NR_STUN_MSG_BINDING_RESPONSE;
+ ASSERT_EQ(0, nr_stun_encode_message(msg));
+ TestOutgoing(msg->buffer, msg->length, 123, 45, true);
+ TestOutgoingTcp(msg->buffer, msg->length, true);
+
+ ASSERT_EQ(0, nr_stun_message_destroy(&msg));
+}
+
+TEST_F(WebRtcIcePacketFilterTest, TestRecvDataPacketWithAPendingAddress) {
+ nr_stun_message* msg;
+ ASSERT_EQ(0, nr_stun_build_req_no_auth(nullptr, &msg));
+
+ msg->header.type = NR_STUN_MSG_BINDING_REQUEST;
+ ASSERT_EQ(0, nr_stun_encode_message(msg));
+ TestOutgoing(msg->buffer, msg->length, 123, 45, true);
+ TestOutgoingTcp(msg->buffer, msg->length, true);
+
+ const unsigned char data[] = "12345abcde";
+ TestIncoming(data, sizeof(data), 123, 45, true);
+ TestIncomingTcp(data, sizeof(data), true);
+
+ ASSERT_EQ(0, nr_stun_message_destroy(&msg));
+}
+
+TEST(WebRtcIceInternalsTest, TestAddBogusAttribute)
+{
+ nr_stun_message* req;
+ ASSERT_EQ(0, nr_stun_message_create(&req));
+ Data* data;
+ ASSERT_EQ(0, r_data_alloc(&data, 3000));
+ memset(data->data, 'A', data->len);
+ ASSERT_TRUE(nr_stun_message_add_message_integrity_attribute(req, data));
+ ASSERT_EQ(0, r_data_destroy(&data));
+ ASSERT_EQ(0, nr_stun_message_destroy(&req));
+}
diff --git a/dom/media/webrtc/transport/test/moz.build b/dom/media/webrtc/transport/test/moz.build
new file mode 100644
index 0000000000..69d3a587a5
--- /dev/null
+++ b/dom/media/webrtc/transport/test/moz.build
@@ -0,0 +1,104 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+if CONFIG["OS_TARGET"] != "WINNT":
+
+ if CONFIG["OS_TARGET"] != "Android":
+ SOURCES += [
+ "ice_unittest.cpp",
+ ]
+
+ SOURCES += [
+ "buffered_stun_socket_unittest.cpp",
+ "multi_tcp_socket_unittest.cpp",
+ "nrappkit_unittest.cpp",
+ "proxy_tunnel_socket_unittest.cpp",
+ "rlogconnector_unittest.cpp",
+ "runnable_utils_unittest.cpp",
+ "simpletokenbucket_unittest.cpp",
+ "sockettransportservice_unittest.cpp",
+ "stunserver.cpp",
+ "test_nr_socket_ice_unittest.cpp",
+ "test_nr_socket_unittest.cpp",
+ "TestSyncRunnable.cpp",
+ "transport_unittests.cpp",
+ "turn_unittest.cpp",
+ "webrtcproxychannel_unittest.cpp",
+ ]
+
+ if CONFIG["MOZ_SCTP"]:
+ SOURCES += [
+ "sctp_unittest.cpp",
+ ]
+
+
+for var in ("HAVE_STRDUP", "NR_SOCKET_IS_VOID_PTR", "SCTP_DEBUG"):
+ DEFINES[var] = True
+
+if CONFIG["OS_TARGET"] == "Android":
+ DEFINES["LINUX"] = True
+ DEFINES["ANDROID"] = True
+ LOCAL_INCLUDES += [
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/port/android/include",
+ ]
+
+if CONFIG["OS_TARGET"] == "Linux":
+ DEFINES["LINUX"] = True
+ LOCAL_INCLUDES += [
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/port/linux/include",
+ ]
+
+if CONFIG["OS_TARGET"] == "Darwin":
+ LOCAL_INCLUDES += [
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/port/darwin/include",
+ ]
+
+if CONFIG["OS_TARGET"] in ("DragonFly", "FreeBSD", "NetBSD", "OpenBSD"):
+ if CONFIG["OS_TARGET"] == "Darwin":
+ DEFINES["DARWIN"] = True
+ else:
+ DEFINES["BSD"] = True
+ LOCAL_INCLUDES += [
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/port/darwin/include",
+ ]
+
+# SCTP DEFINES
+if CONFIG["OS_TARGET"] == "WINNT":
+ DEFINES["WIN"] = True
+ # for stun.h
+ DEFINES["WIN32"] = True
+ DEFINES["__Userspace_os_Windows"] = 1
+else:
+ # Works for Darwin, Linux, Android. Probably doesn't work for others.
+ DEFINES["__Userspace_os_%s" % CONFIG["OS_TARGET"]] = 1
+
+if CONFIG["OS_TARGET"] in ("Darwin", "Android"):
+ DEFINES["GTEST_USE_OWN_TR1_TUPLE"] = 1
+
+LOCAL_INCLUDES += [
+ "/dom/media/webrtc/transport/",
+ "/dom/media/webrtc/transport/third_party/",
+ "/dom/media/webrtc/transport/third_party/nICEr/src/crypto",
+ "/dom/media/webrtc/transport/third_party/nICEr/src/ice",
+ "/dom/media/webrtc/transport/third_party/nICEr/src/net",
+ "/dom/media/webrtc/transport/third_party/nICEr/src/stun",
+ "/dom/media/webrtc/transport/third_party/nICEr/src/util",
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/event",
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/log",
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/plugin",
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/port/generic/include",
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/registry",
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/share",
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/stats",
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/util/",
+ "/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr",
+ "/netwerk/sctp/src/",
+ "/xpcom/tests/",
+]
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/dom/media/webrtc/transport/test/mtransport_test_utils.h b/dom/media/webrtc/transport/test/mtransport_test_utils.h
new file mode 100644
index 0000000000..04031c0dc2
--- /dev/null
+++ b/dom/media/webrtc/transport/test/mtransport_test_utils.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+#ifndef mtransport_test_utils_h__
+#define mtransport_test_utils_h__
+
+#include "nsCOMPtr.h"
+#include "nsNetCID.h"
+
+#include "nsISerialEventTarget.h"
+#include "nsPISocketTransportService.h"
+#include "nsServiceManagerUtils.h"
+#include "nsThreadUtils.h"
+
+class MtransportTestUtils {
+ public:
+ MtransportTestUtils() { InitServices(); }
+
+ ~MtransportTestUtils() = default;
+
+ void InitServices() {
+ nsresult rv;
+ sts_target_ = do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ sts_ = do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+
+ nsISerialEventTarget* sts_target() { return sts_target_; }
+
+ nsresult SyncDispatchToSTS(nsIRunnable* aRunnable) {
+ return SyncDispatchToSTS(do_AddRef(aRunnable));
+ }
+ nsresult SyncDispatchToSTS(already_AddRefed<nsIRunnable>&& aRunnable) {
+ return NS_DispatchAndSpinEventLoopUntilComplete(
+ "MtransportTestUtils::SyncDispatchToSts"_ns, sts_target_,
+ std::move(aRunnable));
+ }
+
+ private:
+ nsCOMPtr<nsISerialEventTarget> sts_target_;
+ nsCOMPtr<nsPISocketTransportService> sts_;
+};
+
+#define CHECK_ENVIRONMENT_FLAG(envname) \
+ char* test_flag = getenv(envname); \
+ if (!test_flag || strcmp(test_flag, "1")) { \
+ printf("To run this test set %s=1 in your environment\n", envname); \
+ exit(0); \
+ }
+
+#endif
diff --git a/dom/media/webrtc/transport/test/multi_tcp_socket_unittest.cpp b/dom/media/webrtc/transport/test/multi_tcp_socket_unittest.cpp
new file mode 100644
index 0000000000..d0c3ae6e53
--- /dev/null
+++ b/dom/media/webrtc/transport/test/multi_tcp_socket_unittest.cpp
@@ -0,0 +1,501 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <iostream>
+#include <vector>
+
+#include "mozilla/Atomics.h"
+#include "runnable_utils.h"
+#include "pk11pub.h"
+
+extern "C" {
+#include "nr_api.h"
+#include "nr_socket.h"
+#include "transport_addr.h"
+#include "nr_socket_multi_tcp.h"
+}
+
+#include "stunserver.h"
+
+#include "nricectx.h"
+
+#define GTEST_HAS_RTTI 0
+#include "gtest/gtest.h"
+#include "gtest_utils.h"
+
+using namespace mozilla;
+
+namespace {
+
+class MultiTcpSocketTest : public MtransportTest {
+ public:
+ MultiTcpSocketTest()
+ : MtransportTest(), socks(3, nullptr), readable(false), ice_ctx_() {}
+
+ void SetUp() {
+ MtransportTest::SetUp();
+
+ NrIceCtx::InitializeGlobals(NrIceCtx::GlobalConfig());
+ ice_ctx_ = NrIceCtx::Create("stun");
+
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnableNM(&TestStunTcpServer::GetInstance, AF_INET));
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnableNM(&TestStunTcpServer::GetInstance, AF_INET6));
+ }
+
+ void TearDown() {
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(this, &MultiTcpSocketTest::Shutdown_s));
+
+ MtransportTest::TearDown();
+ }
+
+ DISALLOW_COPY_ASSIGN(MultiTcpSocketTest);
+
+ static void SockReadable(NR_SOCKET s, int how, void* arg) {
+ MultiTcpSocketTest* obj = static_cast<MultiTcpSocketTest*>(arg);
+ obj->SetReadable(true);
+ }
+
+ void Shutdown_s() {
+ ice_ctx_ = nullptr;
+ for (auto& sock : socks) {
+ nr_socket_destroy(&sock);
+ }
+ }
+
+ static uint16_t GetRandomPort() {
+ uint16_t result;
+ if (PK11_GenerateRandom((unsigned char*)&result, 2) != SECSuccess) {
+ MOZ_ASSERT(false);
+ return 0;
+ }
+ return result;
+ }
+
+ static uint16_t EnsureEphemeral(uint16_t port) {
+ // IANA ephemeral port range (49152 to 65535)
+ return port | 49152;
+ }
+
+ void Create_s(nr_socket_tcp_type tcp_type, std::string stun_server_addr,
+ uint16_t stun_server_port, nr_socket** sock) {
+ nr_transport_addr local;
+ // Get start of port range for test
+ static unsigned short port_s = GetRandomPort();
+ int r;
+
+ if (!stun_server_addr.empty()) {
+ std::vector<NrIceStunServer> stun_servers;
+ UniquePtr<NrIceStunServer> server(NrIceStunServer::Create(
+ stun_server_addr, stun_server_port, kNrIceTransportTcp));
+ stun_servers.push_back(*server);
+
+ ASSERT_TRUE(NS_SUCCEEDED(ice_ctx_->SetStunServers(stun_servers)));
+ }
+
+ r = 1;
+ for (int tries = 10; tries && r; --tries) {
+ r = nr_str_port_to_transport_addr(
+ (char*)"127.0.0.1", EnsureEphemeral(port_s++), IPPROTO_TCP, &local);
+ ASSERT_EQ(0, r);
+
+ r = nr_socket_multi_tcp_create(ice_ctx_->ctx(), nullptr, &local, tcp_type,
+ 1, 2048, sock);
+ }
+
+ ASSERT_EQ(0, r);
+ printf("Creating socket on %s\n", local.as_string);
+ r = nr_socket_multi_tcp_set_readable_cb(
+ *sock, &MultiTcpSocketTest::SockReadable, this);
+ ASSERT_EQ(0, r);
+ }
+
+ nr_socket* Create(nr_socket_tcp_type tcp_type,
+ std::string stun_server_addr = "",
+ uint16_t stun_server_port = 0) {
+ nr_socket* sock = nullptr;
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(this, &MultiTcpSocketTest::Create_s, tcp_type,
+ stun_server_addr, stun_server_port, &sock));
+ return sock;
+ }
+
+ void Listen_s(nr_socket* sock) {
+ nr_transport_addr addr;
+ int r = nr_socket_getaddr(sock, &addr);
+ ASSERT_EQ(0, r);
+ printf("Listening on %s\n", addr.as_string);
+ r = nr_socket_listen(sock, 5);
+ ASSERT_EQ(0, r);
+ }
+
+ void Listen(nr_socket* sock) {
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(this, &MultiTcpSocketTest::Listen_s, sock));
+ }
+
+ void Destroy_s(nr_socket* sock) {
+ int r = nr_socket_destroy(&sock);
+ ASSERT_EQ(0, r);
+ }
+
+ void Destroy(nr_socket* sock) {
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(this, &MultiTcpSocketTest::Destroy_s, sock));
+ }
+
+ void Connect_s(nr_socket* from, nr_socket* to) {
+ nr_transport_addr addr_to;
+ nr_transport_addr addr_from;
+ int r = nr_socket_getaddr(to, &addr_to);
+ ASSERT_EQ(0, r);
+ r = nr_socket_getaddr(from, &addr_from);
+ ASSERT_EQ(0, r);
+ printf("Connecting from %s to %s\n", addr_from.as_string,
+ addr_to.as_string);
+ r = nr_socket_connect(from, &addr_to);
+ ASSERT_EQ(0, r);
+ }
+
+ void Connect(nr_socket* from, nr_socket* to) {
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(this, &MultiTcpSocketTest::Connect_s, from, to));
+ }
+
+ void ConnectSo_s(nr_socket* so1, nr_socket* so2) {
+ nr_transport_addr addr_so1;
+ nr_transport_addr addr_so2;
+ int r = nr_socket_getaddr(so1, &addr_so1);
+ ASSERT_EQ(0, r);
+ r = nr_socket_getaddr(so2, &addr_so2);
+ ASSERT_EQ(0, r);
+ printf("Connecting SO %s <-> %s\n", addr_so1.as_string, addr_so2.as_string);
+ r = nr_socket_connect(so1, &addr_so2);
+ ASSERT_EQ(0, r);
+ r = nr_socket_connect(so2, &addr_so1);
+ ASSERT_EQ(0, r);
+ }
+
+ void ConnectSo(nr_socket* from, nr_socket* to) {
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(this, &MultiTcpSocketTest::ConnectSo_s, from, to));
+ }
+
+ void SendDataToAddress_s(nr_socket* from, nr_transport_addr* to,
+ const char* data, size_t len) {
+ nr_transport_addr addr_from;
+
+ int r = nr_socket_getaddr(from, &addr_from);
+ ASSERT_EQ(0, r);
+ printf("Sending %lu bytes %s -> %s\n", (unsigned long)len,
+ addr_from.as_string, to->as_string);
+ r = nr_socket_sendto(from, data, len, 0, to);
+ ASSERT_EQ(0, r);
+ }
+
+ void SendData(nr_socket* from, nr_transport_addr* to, const char* data,
+ size_t len) {
+ test_utils_->SyncDispatchToSTS(WrapRunnable(
+ this, &MultiTcpSocketTest::SendDataToAddress_s, from, to, data, len));
+ }
+
+ void SendDataToSocket_s(nr_socket* from, nr_socket* to, const char* data,
+ size_t len) {
+ nr_transport_addr addr_to;
+
+ int r = nr_socket_getaddr(to, &addr_to);
+ ASSERT_EQ(0, r);
+ SendDataToAddress_s(from, &addr_to, data, len);
+ }
+
+ void SendData(nr_socket* from, nr_socket* to, const char* data, size_t len) {
+ test_utils_->SyncDispatchToSTS(WrapRunnable(
+ this, &MultiTcpSocketTest::SendDataToSocket_s, from, to, data, len));
+ }
+
+ void RecvDataFromAddress_s(nr_transport_addr* expected_from,
+ nr_socket* sent_to, const char* expected_data,
+ size_t expected_len) {
+ SetReadable(false);
+ size_t buflen = expected_len ? expected_len + 1 : 100;
+ char received_data[buflen];
+ nr_transport_addr addr_to;
+ nr_transport_addr retaddr;
+ size_t retlen;
+
+ int r = nr_socket_getaddr(sent_to, &addr_to);
+ ASSERT_EQ(0, r);
+ printf("Receiving %lu bytes %s <- %s\n", (unsigned long)expected_len,
+ addr_to.as_string, expected_from->as_string);
+ r = nr_socket_recvfrom(sent_to, received_data, buflen, &retlen, 0,
+ &retaddr);
+ ASSERT_EQ(0, r);
+ r = nr_transport_addr_cmp(&retaddr, expected_from,
+ NR_TRANSPORT_ADDR_CMP_MODE_ALL);
+ ASSERT_EQ(0, r);
+ // expected_len == 0 means we just expected some data
+ if (expected_len == 0) {
+ ASSERT_GT(retlen, 0U);
+ } else {
+ ASSERT_EQ(expected_len, retlen);
+ r = memcmp(expected_data, received_data, retlen);
+ ASSERT_EQ(0, r);
+ }
+ }
+
+ void RecvData(nr_transport_addr* expected_from, nr_socket* sent_to,
+ const char* expected_data = nullptr, size_t expected_len = 0) {
+ ASSERT_TRUE_WAIT(IsReadable(), 1000);
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(this, &MultiTcpSocketTest::RecvDataFromAddress_s,
+ expected_from, sent_to, expected_data, expected_len));
+ }
+
+ void RecvDataFromSocket_s(nr_socket* expected_from, nr_socket* sent_to,
+ const char* expected_data, size_t expected_len) {
+ nr_transport_addr addr_from;
+
+ int r = nr_socket_getaddr(expected_from, &addr_from);
+ ASSERT_EQ(0, r);
+
+ RecvDataFromAddress_s(&addr_from, sent_to, expected_data, expected_len);
+ }
+
+ void RecvData(nr_socket* expected_from, nr_socket* sent_to,
+ const char* expected_data, size_t expected_len) {
+ ASSERT_TRUE_WAIT(IsReadable(), 1000);
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(this, &MultiTcpSocketTest::RecvDataFromSocket_s,
+ expected_from, sent_to, expected_data, expected_len));
+ }
+
+ void RecvDataFailed_s(nr_socket* sent_to, size_t expected_len,
+ int expected_err) {
+ SetReadable(false);
+ char received_data[expected_len + 1];
+ nr_transport_addr addr_to;
+ nr_transport_addr retaddr;
+ size_t retlen;
+
+ int r = nr_socket_getaddr(sent_to, &addr_to);
+ ASSERT_EQ(0, r);
+ r = nr_socket_recvfrom(sent_to, received_data, expected_len + 1, &retlen, 0,
+ &retaddr);
+ ASSERT_EQ(expected_err, r) << "Expecting receive failure " << expected_err
+ << " on " << addr_to.as_string;
+ }
+
+ void RecvDataFailed(nr_socket* sent_to, size_t expected_len,
+ int expected_err) {
+ ASSERT_TRUE_WAIT(IsReadable(), 1000);
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(this, &MultiTcpSocketTest::RecvDataFailed_s, sent_to,
+ expected_len, expected_err));
+ }
+
+ void TransferData(nr_socket* from, nr_socket* to, const char* data,
+ size_t len) {
+ SendData(from, to, data, len);
+ RecvData(from, to, data, len);
+ }
+
+ protected:
+ bool IsReadable() const { return readable; }
+ void SetReadable(bool r) { readable = r; }
+ std::vector<nr_socket*> socks;
+ Atomic<bool> readable;
+ RefPtr<NrIceCtx> ice_ctx_;
+};
+} // namespace
+
+TEST_F(MultiTcpSocketTest, TestListen) {
+ socks[0] = Create(TCP_TYPE_PASSIVE);
+ Listen(socks[0]);
+}
+
+TEST_F(MultiTcpSocketTest, TestConnect) {
+ socks[0] = Create(TCP_TYPE_PASSIVE);
+ socks[1] = Create(TCP_TYPE_ACTIVE);
+ socks[2] = Create(TCP_TYPE_ACTIVE);
+ Listen(socks[0]);
+ Connect(socks[1], socks[0]);
+ Connect(socks[2], socks[0]);
+}
+
+TEST_F(MultiTcpSocketTest, TestTransmit) {
+ const char data[] = "TestTransmit";
+ socks[0] = Create(TCP_TYPE_ACTIVE);
+ socks[1] = Create(TCP_TYPE_PASSIVE);
+ Listen(socks[1]);
+ Connect(socks[0], socks[1]);
+
+ TransferData(socks[0], socks[1], data, sizeof(data));
+ TransferData(socks[1], socks[0], data, sizeof(data));
+}
+
+TEST_F(MultiTcpSocketTest, TestClosePassive) {
+ const char data[] = "TestClosePassive";
+ socks[0] = Create(TCP_TYPE_ACTIVE);
+ socks[1] = Create(TCP_TYPE_PASSIVE);
+ Listen(socks[1]);
+ Connect(socks[0], socks[1]);
+
+ TransferData(socks[0], socks[1], data, sizeof(data));
+ TransferData(socks[1], socks[0], data, sizeof(data));
+
+ /* We have to destroy as only that calls PR_Close() */
+ std::cerr << "Destructing socket" << std::endl;
+ Destroy(socks[1]);
+
+ RecvDataFailed(socks[0], sizeof(data), R_EOD);
+
+ socks[1] = nullptr;
+}
+
+TEST_F(MultiTcpSocketTest, TestCloseActive) {
+ const char data[] = "TestCloseActive";
+ socks[0] = Create(TCP_TYPE_ACTIVE);
+ socks[1] = Create(TCP_TYPE_PASSIVE);
+ Listen(socks[1]);
+ Connect(socks[0], socks[1]);
+
+ TransferData(socks[0], socks[1], data, sizeof(data));
+ TransferData(socks[1], socks[0], data, sizeof(data));
+
+ /* We have to destroy as only that calls PR_Close() */
+ std::cerr << "Destructing socket" << std::endl;
+ Destroy(socks[0]);
+
+ RecvDataFailed(socks[1], sizeof(data), R_EOD);
+
+ socks[0] = nullptr;
+}
+
+TEST_F(MultiTcpSocketTest, TestTwoSendsBeforeReceives) {
+ const char data1[] = "TestTwoSendsBeforeReceives";
+ const char data2[] = "2nd data";
+ socks[0] = Create(TCP_TYPE_ACTIVE);
+ socks[1] = Create(TCP_TYPE_PASSIVE);
+ Listen(socks[1]);
+ Connect(socks[0], socks[1]);
+
+ SendData(socks[0], socks[1], data1, sizeof(data1));
+ SendData(socks[0], socks[1], data2, sizeof(data2));
+ RecvData(socks[0], socks[1], data1, sizeof(data1));
+ /* ICE TCP framing turns TCP effectively into datagram mode */
+ RecvData(socks[0], socks[1], data2, sizeof(data2));
+}
+
+TEST_F(MultiTcpSocketTest, TestTwoActiveBidirectionalTransmit) {
+ const char data1[] = "TestTwoActiveBidirectionalTransmit";
+ const char data2[] = "ReplyToTheFirstSocket";
+ const char data3[] = "TestMessageFromTheSecondSocket";
+ const char data4[] = "ThisIsAReplyToTheSecondSocket";
+ socks[0] = Create(TCP_TYPE_PASSIVE);
+ socks[1] = Create(TCP_TYPE_ACTIVE);
+ socks[2] = Create(TCP_TYPE_ACTIVE);
+ Listen(socks[0]);
+ Connect(socks[1], socks[0]);
+ Connect(socks[2], socks[0]);
+
+ TransferData(socks[1], socks[0], data1, sizeof(data1));
+ TransferData(socks[0], socks[1], data2, sizeof(data2));
+ TransferData(socks[2], socks[0], data3, sizeof(data3));
+ TransferData(socks[0], socks[2], data4, sizeof(data4));
+}
+
+TEST_F(MultiTcpSocketTest, TestTwoPassiveBidirectionalTransmit) {
+ const char data1[] = "TestTwoPassiveBidirectionalTransmit";
+ const char data2[] = "FirstReply";
+ const char data3[] = "TestTwoPassiveBidirectionalTransmitToTheSecondSock";
+ const char data4[] = "SecondReply";
+ socks[0] = Create(TCP_TYPE_PASSIVE);
+ socks[1] = Create(TCP_TYPE_PASSIVE);
+ socks[2] = Create(TCP_TYPE_ACTIVE);
+ Listen(socks[0]);
+ Listen(socks[1]);
+ Connect(socks[2], socks[0]);
+ Connect(socks[2], socks[1]);
+
+ TransferData(socks[2], socks[0], data1, sizeof(data1));
+ TransferData(socks[0], socks[2], data2, sizeof(data2));
+ TransferData(socks[2], socks[1], data3, sizeof(data3));
+ TransferData(socks[1], socks[2], data4, sizeof(data4));
+}
+
+TEST_F(MultiTcpSocketTest, TestActivePassiveWithStunServerMockup) {
+ /* Fake STUN message able to pass the nr_is_stun_msg check
+ used in nr_socket_buffered_stun */
+ const char stunMessage[] = {'\x00', '\x01', '\x00', '\x04', '\x21', '\x12',
+ '\xa4', '\x42', '\x00', '\x00', '\x00', '\x00',
+ '\x00', '\x00', '\x0c', '\x00', '\x00', '\x00',
+ '\x00', '\x00', '\x1c', '\xed', '\xca', '\xfe'};
+ const char data[] = "TestActivePassiveWithStunServerMockup";
+
+ nr_transport_addr stun_srv_addr;
+ std::string stun_addr;
+ uint16_t stun_port;
+ stun_addr = TestStunTcpServer::GetInstance(AF_INET)->addr();
+ stun_port = TestStunTcpServer::GetInstance(AF_INET)->port();
+ int r = nr_str_port_to_transport_addr(stun_addr.c_str(), stun_port,
+ IPPROTO_TCP, &stun_srv_addr);
+ ASSERT_EQ(0, r);
+
+ socks[0] = Create(TCP_TYPE_PASSIVE, stun_addr, stun_port);
+ Listen(socks[0]);
+ socks[1] = Create(TCP_TYPE_ACTIVE, stun_addr, stun_port);
+
+ /* Send a fake STUN request and expect a STUN error response */
+ SendData(socks[0], &stun_srv_addr, stunMessage, sizeof(stunMessage));
+ RecvData(&stun_srv_addr, socks[0]);
+
+ Connect(socks[1], socks[0]);
+ TransferData(socks[1], socks[0], data, sizeof(data));
+ TransferData(socks[0], socks[1], data, sizeof(data));
+}
+
+TEST_F(MultiTcpSocketTest, TestConnectTwoSo) {
+ socks[0] = Create(TCP_TYPE_SO);
+ socks[1] = Create(TCP_TYPE_SO);
+ ConnectSo(socks[0], socks[1]);
+}
+
+// test works on localhost only with delay applied:
+// tc qdisc add dev lo root netem delay 5ms
+TEST_F(MultiTcpSocketTest, DISABLED_TestTwoSoBidirectionalTransmit) {
+ const char data[] = "TestTwoSoBidirectionalTransmit";
+ socks[0] = Create(TCP_TYPE_SO);
+ socks[1] = Create(TCP_TYPE_SO);
+ ConnectSo(socks[0], socks[1]);
+ TransferData(socks[0], socks[1], data, sizeof(data));
+ TransferData(socks[1], socks[0], data, sizeof(data));
+}
+
+TEST_F(MultiTcpSocketTest, TestBigData) {
+ char buf1[2048];
+ char buf2[1024];
+
+ for (unsigned i = 0; i < sizeof(buf1); ++i) {
+ buf1[i] = i & 0xff;
+ }
+ for (unsigned i = 0; i < sizeof(buf2); ++i) {
+ buf2[i] = (i + 0x80) & 0xff;
+ }
+ socks[0] = Create(TCP_TYPE_ACTIVE);
+ socks[1] = Create(TCP_TYPE_PASSIVE);
+ Listen(socks[1]);
+ Connect(socks[0], socks[1]);
+
+ TransferData(socks[0], socks[1], buf1, sizeof(buf1));
+ TransferData(socks[0], socks[1], buf2, sizeof(buf2));
+ // opposite dir
+ SendData(socks[1], socks[0], buf2, sizeof(buf2));
+ SendData(socks[1], socks[0], buf1, sizeof(buf1));
+ RecvData(socks[1], socks[0], buf2, sizeof(buf2));
+ RecvData(socks[1], socks[0], buf1, sizeof(buf1));
+}
diff --git a/dom/media/webrtc/transport/test/nrappkit_unittest.cpp b/dom/media/webrtc/transport/test/nrappkit_unittest.cpp
new file mode 100644
index 0000000000..b6a63fb993
--- /dev/null
+++ b/dom/media/webrtc/transport/test/nrappkit_unittest.cpp
@@ -0,0 +1,123 @@
+
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+#include <iostream>
+
+// nrappkit includes
+extern "C" {
+#include "nr_api.h"
+#include "async_timer.h"
+}
+
+#include "runnable_utils.h"
+
+#define GTEST_HAS_RTTI 0
+#include "gtest/gtest.h"
+#include "gtest_utils.h"
+
+using namespace mozilla;
+
+namespace {
+
+class TimerTest : public MtransportTest {
+ public:
+ TimerTest() : MtransportTest(), handle_(nullptr), fired_(false) {}
+ virtual ~TimerTest() = default;
+
+ int ArmTimer(int timeout) {
+ int ret;
+
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnableRet(&ret, this, &TimerTest::ArmTimer_w, timeout));
+
+ return ret;
+ }
+
+ int ArmCancelTimer(int timeout) {
+ int ret;
+
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnableRet(&ret, this, &TimerTest::ArmCancelTimer_w, timeout));
+
+ return ret;
+ }
+
+ int ArmTimer_w(int timeout) {
+ return NR_ASYNC_TIMER_SET(timeout, cb, this, &handle_);
+ }
+
+ int ArmCancelTimer_w(int timeout) {
+ int r;
+ r = ArmTimer_w(timeout);
+ if (r) return r;
+
+ return CancelTimer_w();
+ }
+
+ int CancelTimer() {
+ int ret;
+
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnableRet(&ret, this, &TimerTest::CancelTimer_w));
+
+ return ret;
+ }
+
+ int CancelTimer_w() { return NR_async_timer_cancel(handle_); }
+
+ int Schedule() {
+ int ret;
+
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnableRet(&ret, this, &TimerTest::Schedule_w));
+
+ return ret;
+ }
+
+ int Schedule_w() {
+ NR_ASYNC_SCHEDULE(cb, this);
+
+ return 0;
+ }
+
+ static void cb(NR_SOCKET r, int how, void* arg) {
+ std::cerr << "Timer fired " << std::endl;
+
+ TimerTest* t = static_cast<TimerTest*>(arg);
+
+ t->fired_ = true;
+ }
+
+ protected:
+ void* handle_;
+ bool fired_;
+};
+} // namespace
+
+TEST_F(TimerTest, SimpleTimer) {
+ ArmTimer(100);
+ ASSERT_TRUE_WAIT(fired_, 1000);
+}
+
+TEST_F(TimerTest, CancelTimer) {
+ ArmTimer(1000);
+ CancelTimer();
+ PR_Sleep(2000);
+ ASSERT_FALSE(fired_);
+}
+
+TEST_F(TimerTest, CancelTimer0) {
+ ArmCancelTimer(0);
+ PR_Sleep(100);
+ ASSERT_FALSE(fired_);
+}
+
+TEST_F(TimerTest, ScheduleTest) {
+ Schedule();
+ ASSERT_TRUE_WAIT(fired_, 1000);
+}
diff --git a/dom/media/webrtc/transport/test/proxy_tunnel_socket_unittest.cpp b/dom/media/webrtc/transport/test/proxy_tunnel_socket_unittest.cpp
new file mode 100644
index 0000000000..1b54126dd6
--- /dev/null
+++ b/dom/media/webrtc/transport/test/proxy_tunnel_socket_unittest.cpp
@@ -0,0 +1,277 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original authors: ekr@rtfm.com; ryan@tokbox.com
+
+#include <vector>
+#include <numeric>
+
+#include "nr_socket_tcp.h"
+#include "WebrtcTCPSocketWrapper.h"
+
+#define GTEST_HAS_RTTI 0
+#include "gtest/gtest.h"
+#include "gtest_utils.h"
+
+using namespace mozilla;
+
+// update TestReadMultipleSizes if you change this
+const std::string kHelloMessage = "HELLO IS IT ME YOU'RE LOOKING FOR?";
+
+class NrTcpSocketTest : public MtransportTest {
+ public:
+ NrTcpSocketTest()
+ : mSProxy(nullptr),
+ nr_socket_(nullptr),
+ mEmptyArray(0),
+ mReadChunkSize(0),
+ mReadChunkSizeIncrement(1),
+ mReadAllowance(-1),
+ mConnected(false) {}
+
+ void SetUp() override {
+ mSProxy = new NrTcpSocket(nullptr);
+ int r = nr_socket_create_int((void*)mSProxy.get(), mSProxy->vtbl(),
+ &nr_socket_);
+ ASSERT_EQ(0, r);
+
+ // fake calling AsyncOpen() due to IPC calls. must be non-null
+ mSProxy->AssignChannel_DoNotUse(new WebrtcTCPSocketWrapper(nullptr));
+ }
+
+ void TearDown() override { mSProxy->close(); }
+
+ static void readable_cb(NR_SOCKET s, int how, void* cb_arg) {
+ NrTcpSocketTest* test = (NrTcpSocketTest*)cb_arg;
+ size_t capacity = std::min(test->mReadChunkSize, test->mReadAllowance);
+ nsTArray<uint8_t> array(capacity);
+ size_t read;
+
+ nr_socket_read(test->nr_socket_, (char*)array.Elements(), array.Capacity(),
+ &read, 0);
+
+ ASSERT_TRUE(read <= array.Capacity());
+ ASSERT_TRUE(test->mReadAllowance >= read);
+
+ array.SetLength(read);
+ test->mData.AppendElements(array);
+ test->mReadAllowance -= read;
+
+ // We may read more bytes each time we're called. This way we can ensure we
+ // consume buffers partially and across multiple buffers.
+ test->mReadChunkSize += test->mReadChunkSizeIncrement;
+
+ if (test->mReadAllowance > 0) {
+ NR_ASYNC_WAIT(s, how, &NrTcpSocketTest::readable_cb, cb_arg);
+ }
+ }
+
+ static void writable_cb(NR_SOCKET s, int how, void* cb_arg) {
+ NrTcpSocketTest* test = (NrTcpSocketTest*)cb_arg;
+ test->mConnected = true;
+ }
+
+ const std::string DataString() {
+ return std::string((char*)mData.Elements(), mData.Length());
+ }
+
+ protected:
+ RefPtr<NrTcpSocket> mSProxy;
+ nr_socket* nr_socket_;
+
+ nsTArray<uint8_t> mData;
+ nsTArray<uint8_t> mEmptyArray;
+
+ uint32_t mReadChunkSize;
+ uint32_t mReadChunkSizeIncrement;
+ uint32_t mReadAllowance;
+
+ bool mConnected;
+};
+
+TEST_F(NrTcpSocketTest, TestCreate) {}
+
+TEST_F(NrTcpSocketTest, TestConnected) {
+ ASSERT_TRUE(!mConnected);
+
+ NR_ASYNC_WAIT(mSProxy, NR_ASYNC_WAIT_WRITE, &NrTcpSocketTest::writable_cb,
+ this);
+
+ // still not connected just registered for writes...
+ ASSERT_TRUE(!mConnected);
+
+ mSProxy->OnConnected("http"_ns);
+
+ ASSERT_TRUE(mConnected);
+}
+
+TEST_F(NrTcpSocketTest, TestRead) {
+ nsTArray<uint8_t> array;
+ array.AppendElements(kHelloMessage.c_str(), kHelloMessage.length());
+
+ NR_ASYNC_WAIT(mSProxy, NR_ASYNC_WAIT_READ, &NrTcpSocketTest::readable_cb,
+ this);
+ // this will read 0 bytes here
+ mSProxy->OnRead(std::move(array));
+
+ ASSERT_EQ(kHelloMessage.length(), mSProxy->CountUnreadBytes());
+
+ // callback is still set but terminated due to 0 byte read
+ // start callbacks again (first read is 0 then 1,2,3,...)
+ mSProxy->OnRead(std::move(mEmptyArray));
+
+ ASSERT_EQ(kHelloMessage.length(), mData.Length());
+ ASSERT_EQ(kHelloMessage, DataString());
+}
+
+TEST_F(NrTcpSocketTest, TestReadConstantConsumeSize) {
+ std::string data;
+
+ // triangle number
+ const int kCount = 32;
+
+ // ~17kb
+ // triangle number formula n*(n+1)/2
+ for (int i = 0; i < kCount * (kCount + 1) / 2; ++i) {
+ data += kHelloMessage;
+ }
+
+ // decreasing buffer sizes
+ for (int i = 0, start = 0; i < kCount; ++i) {
+ int length = (kCount - i) * kHelloMessage.length();
+
+ nsTArray<uint8_t> array;
+ array.AppendElements(data.c_str() + start, length);
+ start += length;
+
+ mSProxy->OnRead(std::move(array));
+ }
+
+ ASSERT_EQ(data.length(), mSProxy->CountUnreadBytes());
+
+ // read same amount each callback
+ mReadChunkSize = 128;
+ mReadChunkSizeIncrement = 0;
+ NR_ASYNC_WAIT(mSProxy, NR_ASYNC_WAIT_READ, &NrTcpSocketTest::readable_cb,
+ this);
+
+ ASSERT_EQ(data.length(), mSProxy->CountUnreadBytes());
+
+ // start callbacks
+ mSProxy->OnRead(std::move(mEmptyArray));
+
+ ASSERT_EQ(data.length(), mData.Length());
+ ASSERT_EQ(data, DataString());
+}
+
+TEST_F(NrTcpSocketTest, TestReadNone) {
+ char buf[4096];
+ size_t read = 0;
+ int r = nr_socket_read(nr_socket_, buf, sizeof(buf), &read, 0);
+
+ ASSERT_EQ(R_WOULDBLOCK, r);
+
+ nsTArray<uint8_t> array;
+ array.AppendElements(kHelloMessage.c_str(), kHelloMessage.length());
+ mSProxy->OnRead(std::move(array));
+
+ ASSERT_EQ(kHelloMessage.length(), mSProxy->CountUnreadBytes());
+
+ r = nr_socket_read(nr_socket_, buf, sizeof(buf), &read, 0);
+
+ ASSERT_EQ(0, r);
+ ASSERT_EQ(kHelloMessage.length(), read);
+ ASSERT_EQ(kHelloMessage, std::string(buf, read));
+}
+
+TEST_F(NrTcpSocketTest, TestReadMultipleSizes) {
+ using namespace std;
+
+ string data;
+ // 515 * kHelloMessage.length() == 17510
+ const size_t kCount = 515;
+ // randomly generated numbers, sums to 17510, 20 numbers
+ vector<int> varyingSizes = {404, 622, 1463, 1597, 1676, 389, 389,
+ 1272, 781, 81, 1030, 1450, 256, 812,
+ 1571, 29, 1045, 911, 643, 1089};
+
+ // changing varyingSizes or the test message breaks this so check here
+ ASSERT_EQ(kCount, 17510 / kHelloMessage.length());
+ ASSERT_EQ(17510, accumulate(varyingSizes.begin(), varyingSizes.end(), 0));
+
+ // ~17kb
+ for (size_t i = 0; i < kCount; ++i) {
+ data += kHelloMessage;
+ }
+
+ nsTArray<uint8_t> array;
+ array.AppendElements(data.c_str(), data.length());
+
+ for (int amountToRead : varyingSizes) {
+ nsTArray<uint8_t> buffer;
+ buffer.AppendElements(array.Elements(), amountToRead);
+ array.RemoveElementsAt(0, amountToRead);
+ mSProxy->OnRead(std::move(buffer));
+ }
+
+ ASSERT_EQ(data.length(), mSProxy->CountUnreadBytes());
+
+ // don't need to read 0 on the first read, so start at 1 and keep going
+ mReadChunkSize = 1;
+ NR_ASYNC_WAIT(mSProxy, NR_ASYNC_WAIT_READ, &NrTcpSocketTest::readable_cb,
+ this);
+ // start callbacks
+ mSProxy->OnRead(std::move(mEmptyArray));
+
+ ASSERT_EQ(data.length(), mData.Length());
+ ASSERT_EQ(data, DataString());
+}
+
+TEST_F(NrTcpSocketTest, TestReadConsumeReadDrain) {
+ std::string data;
+ // ~26kb total; should be even
+ const int kCount = 512;
+
+ // there's some division by 2 here so check that kCount is even
+ ASSERT_EQ(0, kCount % 2);
+
+ for (int i = 0; i < kCount; ++i) {
+ data += kHelloMessage;
+ nsTArray<uint8_t> array;
+ array.AppendElements(kHelloMessage.c_str(), kHelloMessage.length());
+ mSProxy->OnRead(std::move(array));
+ }
+
+ // read half at first
+ mReadAllowance = kCount / 2 * kHelloMessage.length();
+ // start by reading 1 byte
+ mReadChunkSize = 1;
+ NR_ASYNC_WAIT(mSProxy, NR_ASYNC_WAIT_READ, &NrTcpSocketTest::readable_cb,
+ this);
+ mSProxy->OnRead(std::move(mEmptyArray));
+
+ ASSERT_EQ(data.length() / 2, mSProxy->CountUnreadBytes());
+ ASSERT_EQ(data.length() / 2, mData.Length());
+
+ // fill read buffer back up
+ for (int i = 0; i < kCount / 2; ++i) {
+ data += kHelloMessage;
+ nsTArray<uint8_t> array;
+ array.AppendElements(kHelloMessage.c_str(), kHelloMessage.length());
+ mSProxy->OnRead(std::move(array));
+ }
+
+ // remove read limit
+ mReadAllowance = -1;
+ // used entire read allowance so we need to setup a new await
+ NR_ASYNC_WAIT(mSProxy, NR_ASYNC_WAIT_READ, &NrTcpSocketTest::readable_cb,
+ this);
+ // start callbacks
+ mSProxy->OnRead(std::move(mEmptyArray));
+
+ ASSERT_EQ(data.length(), mData.Length());
+ ASSERT_EQ(data, DataString());
+}
diff --git a/dom/media/webrtc/transport/test/rlogconnector_unittest.cpp b/dom/media/webrtc/transport/test/rlogconnector_unittest.cpp
new file mode 100644
index 0000000000..93fabae481
--- /dev/null
+++ b/dom/media/webrtc/transport/test/rlogconnector_unittest.cpp
@@ -0,0 +1,255 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* Original author: bcampen@mozilla.com */
+
+#include "rlogconnector.h"
+
+extern "C" {
+#include "registry.h"
+#include "r_log.h"
+}
+
+#define GTEST_HAS_RTTI 0
+#include "gtest/gtest.h"
+
+#include <deque>
+#include <string>
+#include <vector>
+
+using mozilla::RLogConnector;
+
+int NR_LOG_TEST = 0;
+
+class RLogConnectorTest : public ::testing::Test {
+ public:
+ RLogConnectorTest() { Init(); }
+
+ ~RLogConnectorTest() { Free(); }
+
+ static void SetUpTestCase() {
+ NR_reg_init(NR_REG_MODE_LOCAL);
+ r_log_init();
+ /* Would be nice to be able to unregister in the fixture */
+ const char* facility = "rlogconnector_test";
+ r_log_register(const_cast<char*>(facility), &NR_LOG_TEST);
+ }
+
+ void Init() { RLogConnector::CreateInstance(); }
+
+ void Free() { RLogConnector::DestroyInstance(); }
+
+ void ReInit() {
+ Free();
+ Init();
+ }
+};
+
+TEST_F(RLogConnectorTest, TestGetFree) {
+ RLogConnector* instance = RLogConnector::GetInstance();
+ ASSERT_NE(nullptr, instance);
+}
+
+TEST_F(RLogConnectorTest, TestFilterEmpty) {
+ std::deque<std::string> logs;
+ RLogConnector::GetInstance()->GetAny(0, &logs);
+ ASSERT_EQ(0U, logs.size());
+}
+
+TEST_F(RLogConnectorTest, TestBasicFilter) {
+ r_log(NR_LOG_TEST, LOG_INFO, "Test");
+ std::deque<std::string> logs;
+ RLogConnector::GetInstance()->Filter("Test", 0, &logs);
+ ASSERT_EQ(1U, logs.size());
+}
+
+TEST_F(RLogConnectorTest, TestBasicFilterContent) {
+ r_log(NR_LOG_TEST, LOG_INFO, "Test");
+ std::deque<std::string> logs;
+ RLogConnector::GetInstance()->Filter("Test", 0, &logs);
+ ASSERT_EQ("Test", logs.back());
+}
+
+TEST_F(RLogConnectorTest, TestFilterAnyFrontMatch) {
+ r_log(NR_LOG_TEST, LOG_INFO, "Test");
+ std::vector<std::string> substrings;
+ substrings.push_back("foo");
+ substrings.push_back("Test");
+ std::deque<std::string> logs;
+ RLogConnector::GetInstance()->FilterAny(substrings, 0, &logs);
+ ASSERT_EQ("Test", logs.back());
+}
+
+TEST_F(RLogConnectorTest, TestFilterAnyBackMatch) {
+ r_log(NR_LOG_TEST, LOG_INFO, "Test");
+ std::vector<std::string> substrings;
+ substrings.push_back("Test");
+ substrings.push_back("foo");
+ std::deque<std::string> logs;
+ RLogConnector::GetInstance()->FilterAny(substrings, 0, &logs);
+ ASSERT_EQ("Test", logs.back());
+}
+
+TEST_F(RLogConnectorTest, TestFilterAnyBothMatch) {
+ r_log(NR_LOG_TEST, LOG_INFO, "Test");
+ std::vector<std::string> substrings;
+ substrings.push_back("Tes");
+ substrings.push_back("est");
+ std::deque<std::string> logs;
+ RLogConnector::GetInstance()->FilterAny(substrings, 0, &logs);
+ ASSERT_EQ("Test", logs.back());
+}
+
+TEST_F(RLogConnectorTest, TestFilterAnyNeitherMatch) {
+ r_log(NR_LOG_TEST, LOG_INFO, "Test");
+ std::vector<std::string> substrings;
+ substrings.push_back("tes");
+ substrings.push_back("esT");
+ std::deque<std::string> logs;
+ RLogConnector::GetInstance()->FilterAny(substrings, 0, &logs);
+ ASSERT_EQ(0U, logs.size());
+}
+
+TEST_F(RLogConnectorTest, TestAllMatch) {
+ r_log(NR_LOG_TEST, LOG_INFO, "Test1");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test2");
+ std::deque<std::string> logs;
+ RLogConnector::GetInstance()->GetAny(0, &logs);
+ ASSERT_EQ(2U, logs.size());
+}
+
+TEST_F(RLogConnectorTest, TestOrder) {
+ r_log(NR_LOG_TEST, LOG_INFO, "Test1");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test2");
+ std::deque<std::string> logs;
+ RLogConnector::GetInstance()->GetAny(0, &logs);
+ ASSERT_EQ("Test2", logs.back());
+ ASSERT_EQ("Test1", logs.front());
+}
+
+TEST_F(RLogConnectorTest, TestNoMatch) {
+ r_log(NR_LOG_TEST, LOG_INFO, "Test1");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test2");
+ std::deque<std::string> logs;
+ RLogConnector::GetInstance()->Filter("foo", 0, &logs);
+ ASSERT_EQ(0U, logs.size());
+}
+
+TEST_F(RLogConnectorTest, TestSubstringFilter) {
+ r_log(NR_LOG_TEST, LOG_INFO, "Test1");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test2");
+ std::deque<std::string> logs;
+ RLogConnector::GetInstance()->Filter("t1", 0, &logs);
+ ASSERT_EQ(1U, logs.size());
+ ASSERT_EQ("Test1", logs.back());
+}
+
+TEST_F(RLogConnectorTest, TestFilterLimit) {
+ r_log(NR_LOG_TEST, LOG_INFO, "Test1");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test2");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test3");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test4");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test5");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test6");
+ std::deque<std::string> logs;
+ RLogConnector::GetInstance()->Filter("Test", 2, &logs);
+ ASSERT_EQ(2U, logs.size());
+ ASSERT_EQ("Test6", logs.back());
+ ASSERT_EQ("Test5", logs.front());
+}
+
+TEST_F(RLogConnectorTest, TestFilterAnyLimit) {
+ r_log(NR_LOG_TEST, LOG_INFO, "TestOne");
+ r_log(NR_LOG_TEST, LOG_INFO, "TestTwo");
+ r_log(NR_LOG_TEST, LOG_INFO, "TestThree");
+ r_log(NR_LOG_TEST, LOG_INFO, "TestFour");
+ r_log(NR_LOG_TEST, LOG_INFO, "TestFive");
+ r_log(NR_LOG_TEST, LOG_INFO, "TestSix");
+ std::vector<std::string> substrings;
+ // Matches Two, Three, Four, and Six
+ substrings.push_back("tT");
+ substrings.push_back("o");
+ substrings.push_back("r");
+ substrings.push_back("S");
+ std::deque<std::string> logs;
+ RLogConnector::GetInstance()->FilterAny(substrings, 2, &logs);
+ ASSERT_EQ(2U, logs.size());
+ ASSERT_EQ("TestSix", logs.back());
+ ASSERT_EQ("TestFour", logs.front());
+}
+
+TEST_F(RLogConnectorTest, TestLimit) {
+ RLogConnector::GetInstance()->SetLogLimit(3);
+ r_log(NR_LOG_TEST, LOG_INFO, "Test1");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test2");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test3");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test4");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test5");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test6");
+ std::deque<std::string> logs;
+ RLogConnector::GetInstance()->GetAny(0, &logs);
+ ASSERT_EQ(3U, logs.size());
+ ASSERT_EQ("Test6", logs.back());
+ ASSERT_EQ("Test4", logs.front());
+}
+
+TEST_F(RLogConnectorTest, TestLimitBulkDiscard) {
+ r_log(NR_LOG_TEST, LOG_INFO, "Test1");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test2");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test3");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test4");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test5");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test6");
+ RLogConnector::GetInstance()->SetLogLimit(3);
+ std::deque<std::string> logs;
+ RLogConnector::GetInstance()->GetAny(0, &logs);
+ ASSERT_EQ(3U, logs.size());
+ ASSERT_EQ("Test6", logs.back());
+ ASSERT_EQ("Test4", logs.front());
+}
+
+TEST_F(RLogConnectorTest, TestIncreaseLimit) {
+ RLogConnector::GetInstance()->SetLogLimit(3);
+ r_log(NR_LOG_TEST, LOG_INFO, "Test1");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test2");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test3");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test4");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test5");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test6");
+ RLogConnector::GetInstance()->SetLogLimit(300);
+ std::deque<std::string> logs;
+ RLogConnector::GetInstance()->GetAny(0, &logs);
+ ASSERT_EQ(3U, logs.size());
+ ASSERT_EQ("Test6", logs.back());
+ ASSERT_EQ("Test4", logs.front());
+}
+
+TEST_F(RLogConnectorTest, TestClear) {
+ r_log(NR_LOG_TEST, LOG_INFO, "Test1");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test2");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test3");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test4");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test5");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test6");
+ RLogConnector::GetInstance()->SetLogLimit(0);
+ RLogConnector::GetInstance()->SetLogLimit(4096);
+ std::deque<std::string> logs;
+ RLogConnector::GetInstance()->GetAny(0, &logs);
+ ASSERT_EQ(0U, logs.size());
+}
+
+TEST_F(RLogConnectorTest, TestReInit) {
+ r_log(NR_LOG_TEST, LOG_INFO, "Test1");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test2");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test3");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test4");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test5");
+ r_log(NR_LOG_TEST, LOG_INFO, "Test6");
+ ReInit();
+ std::deque<std::string> logs;
+ RLogConnector::GetInstance()->GetAny(0, &logs);
+ ASSERT_EQ(0U, logs.size());
+}
diff --git a/dom/media/webrtc/transport/test/runnable_utils_unittest.cpp b/dom/media/webrtc/transport/test/runnable_utils_unittest.cpp
new file mode 100644
index 0000000000..70707b148f
--- /dev/null
+++ b/dom/media/webrtc/transport/test/runnable_utils_unittest.cpp
@@ -0,0 +1,353 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+#include <iostream>
+
+#include "nsCOMPtr.h"
+#include "nsNetCID.h"
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
+
+#include "nsServiceManagerUtils.h"
+#include "nsThreadUtils.h"
+
+#include "runnable_utils.h"
+
+#define GTEST_HAS_RTTI 0
+#include "gtest/gtest.h"
+#include "gtest_utils.h"
+
+using namespace mozilla;
+
+namespace {
+
+// Helper used to make sure args are properly copied and/or moved.
+struct CtorDtorState {
+ enum class State { Empty, Copy, Explicit, Move, Moved };
+
+ static const char* ToStr(const State& state) {
+ switch (state) {
+ case State::Empty:
+ return "empty";
+ case State::Copy:
+ return "copy";
+ case State::Explicit:
+ return "explicit";
+ case State::Move:
+ return "move";
+ case State::Moved:
+ return "moved";
+ default:
+ return "unknown";
+ }
+ }
+
+ void DumpState() const { std::cerr << ToStr(state_) << std::endl; }
+
+ CtorDtorState() { DumpState(); }
+
+ explicit CtorDtorState(int* destroyed)
+ : dtor_count_(destroyed), state_(State::Explicit) {
+ DumpState();
+ }
+
+ CtorDtorState(const CtorDtorState& other)
+ : dtor_count_(other.dtor_count_), state_(State::Copy) {
+ DumpState();
+ }
+
+ // Clear the other's dtor counter so it's not counted if moved.
+ CtorDtorState(CtorDtorState&& other)
+ : dtor_count_(std::exchange(other.dtor_count_, nullptr)),
+ state_(State::Move) {
+ other.state_ = State::Moved;
+ DumpState();
+ }
+
+ ~CtorDtorState() {
+ const char* const state = ToStr(state_);
+ std::cerr << "Destructor called with end state: " << state << std::endl;
+
+ if (dtor_count_) {
+ ++*dtor_count_;
+ }
+ }
+
+ int* dtor_count_ = nullptr;
+ State state_ = State::Empty;
+};
+
+class Destructor {
+ private:
+ ~Destructor() {
+ std::cerr << "Destructor called" << std::endl;
+ *destroyed_ = true;
+ }
+
+ public:
+ explicit Destructor(bool* destroyed) : destroyed_(destroyed) {}
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(Destructor)
+
+ private:
+ bool* destroyed_;
+};
+
+class TargetClass {
+ public:
+ explicit TargetClass(int* ran) : ran_(ran) {}
+
+ void m1(int x) {
+ std::cerr << __FUNCTION__ << " " << x << std::endl;
+ *ran_ = 1;
+ }
+
+ void m2(int x, int y) {
+ std::cerr << __FUNCTION__ << " " << x << " " << y << std::endl;
+ *ran_ = 2;
+ }
+
+ void m1set(bool* z) {
+ std::cerr << __FUNCTION__ << std::endl;
+ *z = true;
+ }
+ int return_int(int x) {
+ std::cerr << __FUNCTION__ << std::endl;
+ return x;
+ }
+
+ void destructor_target_ref(RefPtr<Destructor> destructor) {}
+
+ int* ran_;
+};
+
+class RunnableArgsTest : public MtransportTest {
+ public:
+ RunnableArgsTest() : MtransportTest(), ran_(0), cl_(&ran_) {}
+
+ void Test1Arg() {
+ Runnable* r = WrapRunnable(&cl_, &TargetClass::m1, 1);
+ r->Run();
+ ASSERT_EQ(1, ran_);
+ }
+
+ void Test2Args() {
+ Runnable* r = WrapRunnable(&cl_, &TargetClass::m2, 1, 2);
+ r->Run();
+ ASSERT_EQ(2, ran_);
+ }
+
+ private:
+ int ran_;
+ TargetClass cl_;
+};
+
+class DispatchTest : public MtransportTest {
+ public:
+ DispatchTest() : MtransportTest(), ran_(0), cl_(&ran_) {}
+
+ void SetUp() {
+ MtransportTest::SetUp();
+
+ nsresult rv;
+ target_ = do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+ }
+
+ void Test1Arg() {
+ Runnable* r = WrapRunnable(&cl_, &TargetClass::m1, 1);
+ NS_DispatchAndSpinEventLoopUntilComplete("DispatchTest::Test1Arg"_ns,
+ target_, do_AddRef(r));
+ ASSERT_EQ(1, ran_);
+ }
+
+ void Test2Args() {
+ Runnable* r = WrapRunnable(&cl_, &TargetClass::m2, 1, 2);
+ NS_DispatchAndSpinEventLoopUntilComplete("DispatchTest::Test2Args"_ns,
+ target_, do_AddRef(r));
+ ASSERT_EQ(2, ran_);
+ }
+
+ void Test1Set() {
+ bool x = false;
+ NS_DispatchAndSpinEventLoopUntilComplete(
+ "DispatchTest::Test1Set"_ns, target_,
+ do_AddRef(WrapRunnable(&cl_, &TargetClass::m1set, &x)));
+ ASSERT_TRUE(x);
+ }
+
+ void TestRet() {
+ int z;
+ int x = 10;
+
+ NS_DispatchAndSpinEventLoopUntilComplete(
+ "DispatchTest::TestRet"_ns, target_,
+ do_AddRef(WrapRunnableRet(&z, &cl_, &TargetClass::return_int, x)));
+ ASSERT_EQ(10, z);
+ }
+
+ protected:
+ int ran_;
+ TargetClass cl_;
+ nsCOMPtr<nsIEventTarget> target_;
+};
+
+TEST_F(RunnableArgsTest, OneArgument) { Test1Arg(); }
+
+TEST_F(RunnableArgsTest, TwoArguments) { Test2Args(); }
+
+TEST_F(DispatchTest, OneArgument) { Test1Arg(); }
+
+TEST_F(DispatchTest, TwoArguments) { Test2Args(); }
+
+TEST_F(DispatchTest, Test1Set) { Test1Set(); }
+
+TEST_F(DispatchTest, TestRet) { TestRet(); }
+
+void SetNonMethod(TargetClass* cl, int x) { cl->m1(x); }
+
+int SetNonMethodRet(TargetClass* cl, int x) {
+ cl->m1(x);
+
+ return x;
+}
+
+TEST_F(DispatchTest, TestNonMethod) {
+ test_utils_->SyncDispatchToSTS(WrapRunnableNM(SetNonMethod, &cl_, 10));
+
+ ASSERT_EQ(1, ran_);
+}
+
+TEST_F(DispatchTest, TestNonMethodRet) {
+ int z;
+
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnableNMRet(&z, SetNonMethodRet, &cl_, 10));
+
+ ASSERT_EQ(1, ran_);
+ ASSERT_EQ(10, z);
+}
+
+TEST_F(DispatchTest, TestDestructorRef) {
+ bool destroyed = false;
+ {
+ RefPtr<Destructor> destructor = new Destructor(&destroyed);
+ NS_DispatchAndSpinEventLoopUntilComplete(
+ "DispatchTest::TestDestructorRef"_ns, target_,
+ do_AddRef(WrapRunnable(&cl_, &TargetClass::destructor_target_ref,
+ destructor)));
+ ASSERT_FALSE(destroyed);
+ }
+ ASSERT_TRUE(destroyed);
+
+ // Now try with a move.
+ destroyed = false;
+ {
+ RefPtr<Destructor> destructor = new Destructor(&destroyed);
+ NS_DispatchAndSpinEventLoopUntilComplete(
+ "DispatchTest::TestDestructorRef"_ns, target_,
+ do_AddRef(WrapRunnable(&cl_, &TargetClass::destructor_target_ref,
+ std::move(destructor))));
+ ASSERT_TRUE(destroyed);
+ }
+}
+
+TEST_F(DispatchTest, TestMove) {
+ int destroyed = 0;
+ {
+ CtorDtorState state(&destroyed);
+
+ // Dispatch with:
+ // - moved arg
+ // - by-val capture in function should consume a move
+ // - expect destruction in the function scope
+ NS_DispatchAndSpinEventLoopUntilComplete(
+ "DispatchTest::TestMove"_ns, target_,
+ do_AddRef(WrapRunnableNM([](CtorDtorState s) {}, std::move(state))));
+ ASSERT_EQ(1, destroyed);
+ }
+ // Still shouldn't count when we go out of scope as it was moved.
+ ASSERT_EQ(1, destroyed);
+
+ {
+ CtorDtorState state(&destroyed);
+
+ // Dispatch with:
+ // - copied arg
+ // - by-val capture in function should consume a move
+ // - expect destruction in the function scope and call scope
+ NS_DispatchAndSpinEventLoopUntilComplete(
+ "DispatchTest::TestMove"_ns, target_,
+ do_AddRef(WrapRunnableNM([](CtorDtorState s) {}, state)));
+ ASSERT_EQ(2, destroyed);
+ }
+ // Original state should be destroyed
+ ASSERT_EQ(3, destroyed);
+
+ {
+ CtorDtorState state(&destroyed);
+
+ // Dispatch with:
+ // - moved arg
+ // - by-ref in function should accept a moved arg
+ // - expect destruction in the wrapper invocation scope
+ NS_DispatchAndSpinEventLoopUntilComplete(
+ "DispatchTest::TestMove"_ns, target_,
+ do_AddRef(
+ WrapRunnableNM([](const CtorDtorState& s) {}, std::move(state))));
+ ASSERT_EQ(4, destroyed);
+ }
+ // Still shouldn't count when we go out of scope as it was moved.
+ ASSERT_EQ(4, destroyed);
+
+ {
+ CtorDtorState state(&destroyed);
+
+ // Dispatch with:
+ // - moved arg
+ // - r-value function should accept a moved arg
+ // - expect destruction in the wrapper invocation scope
+ NS_DispatchAndSpinEventLoopUntilComplete(
+ "DispatchTest::TestMove"_ns, target_,
+ do_AddRef(WrapRunnableNM([](CtorDtorState&& s) {}, std::move(state))));
+ ASSERT_EQ(5, destroyed);
+ }
+ // Still shouldn't count when we go out of scope as it was moved.
+ ASSERT_EQ(5, destroyed);
+}
+
+TEST_F(DispatchTest, TestUniquePtr) {
+ // Test that holding the class in UniquePtr works
+ int ran = 0;
+ auto cl = MakeUnique<TargetClass>(&ran);
+
+ NS_DispatchAndSpinEventLoopUntilComplete(
+ "DispatchTest::TestUniquePtr"_ns, target_,
+ do_AddRef(WrapRunnable(std::move(cl), &TargetClass::m1, 1)));
+ ASSERT_EQ(1, ran);
+
+ // Test that UniquePtr works as a param to the runnable
+ int destroyed = 0;
+ {
+ auto state = MakeUnique<CtorDtorState>(&destroyed);
+
+ // Dispatch with:
+ // - moved arg
+ // - Function should move construct from arg
+ // - expect destruction in the wrapper invocation scope
+ NS_DispatchAndSpinEventLoopUntilComplete(
+ "DispatchTest::TestUniquePtr"_ns, target_,
+ do_AddRef(WrapRunnableNM([](UniquePtr<CtorDtorState> s) {},
+ std::move(state))));
+ ASSERT_EQ(1, destroyed);
+ }
+ // Still shouldn't count when we go out of scope as it was moved.
+ ASSERT_EQ(1, destroyed);
+}
+
+} // end of namespace
diff --git a/dom/media/webrtc/transport/test/sctp_unittest.cpp b/dom/media/webrtc/transport/test/sctp_unittest.cpp
new file mode 100644
index 0000000000..ea32565fb2
--- /dev/null
+++ b/dom/media/webrtc/transport/test/sctp_unittest.cpp
@@ -0,0 +1,381 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+#include <iostream>
+#include <string>
+
+#include "sigslot.h"
+
+#include "nsITimer.h"
+
+#include "transportflow.h"
+#include "transportlayer.h"
+#include "transportlayerloopback.h"
+
+#include "runnable_utils.h"
+#include "usrsctp.h"
+
+#define GTEST_HAS_RTTI 0
+#include "gtest/gtest.h"
+#include "gtest_utils.h"
+
+using namespace mozilla;
+
+static bool sctp_logging = false;
+static int port_number = 5000;
+
+namespace {
+
+class TransportTestPeer;
+
+class SendPeriodic : public nsITimerCallback, public nsINamed {
+ public:
+ SendPeriodic(TransportTestPeer* peer, int to_send)
+ : peer_(peer), to_send_(to_send) {}
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSITIMERCALLBACK
+ NS_DECL_NSINAMED
+
+ protected:
+ virtual ~SendPeriodic() = default;
+
+ TransportTestPeer* peer_;
+ int to_send_;
+};
+
+NS_IMPL_ISUPPORTS(SendPeriodic, nsITimerCallback, nsINamed)
+
+class TransportTestPeer : public sigslot::has_slots<> {
+ public:
+ TransportTestPeer(std::string name, int local_port, int remote_port,
+ MtransportTestUtils* utils)
+ : name_(name),
+ connected_(false),
+ sent_(0),
+ received_(0),
+ flow_(new TransportFlow()),
+ loopback_(new TransportLayerLoopback()),
+ sctp_(usrsctp_socket(AF_CONN, SOCK_STREAM, IPPROTO_SCTP, receive_cb,
+ nullptr, 0, nullptr)),
+ timer_(NS_NewTimer()),
+ periodic_(nullptr),
+ test_utils_(utils) {
+ std::cerr << "Creating TransportTestPeer; flow="
+ << static_cast<void*>(flow_.get()) << " local=" << local_port
+ << " remote=" << remote_port << std::endl;
+
+ usrsctp_register_address(static_cast<void*>(this));
+ int r = usrsctp_set_non_blocking(sctp_, 1);
+ EXPECT_GE(r, 0);
+
+ struct linger l;
+ l.l_onoff = 1;
+ l.l_linger = 0;
+ r = usrsctp_setsockopt(sctp_, SOL_SOCKET, SO_LINGER, &l,
+ (socklen_t)sizeof(l));
+ EXPECT_GE(r, 0);
+
+ struct sctp_event subscription;
+ memset(&subscription, 0, sizeof(subscription));
+ subscription.se_assoc_id = SCTP_ALL_ASSOC;
+ subscription.se_on = 1;
+ subscription.se_type = SCTP_ASSOC_CHANGE;
+ r = usrsctp_setsockopt(sctp_, IPPROTO_SCTP, SCTP_EVENT, &subscription,
+ sizeof(subscription));
+ EXPECT_GE(r, 0);
+
+ memset(&local_addr_, 0, sizeof(local_addr_));
+ local_addr_.sconn_family = AF_CONN;
+#if !defined(__Userspace_os_Linux) && !defined(__Userspace_os_Windows) && \
+ !defined(__Userspace_os_Android)
+ local_addr_.sconn_len = sizeof(struct sockaddr_conn);
+#endif
+ local_addr_.sconn_port = htons(local_port);
+ local_addr_.sconn_addr = static_cast<void*>(this);
+
+ memset(&remote_addr_, 0, sizeof(remote_addr_));
+ remote_addr_.sconn_family = AF_CONN;
+#if !defined(__Userspace_os_Linux) && !defined(__Userspace_os_Windows) && \
+ !defined(__Userspace_os_Android)
+ remote_addr_.sconn_len = sizeof(struct sockaddr_conn);
+#endif
+ remote_addr_.sconn_port = htons(remote_port);
+ remote_addr_.sconn_addr = static_cast<void*>(this);
+
+ nsresult res;
+ res = loopback_->Init();
+ EXPECT_EQ((nsresult)NS_OK, res);
+ }
+
+ ~TransportTestPeer() {
+ std::cerr << "Destroying sctp connection flow="
+ << static_cast<void*>(flow_.get()) << std::endl;
+ usrsctp_close(sctp_);
+ usrsctp_deregister_address(static_cast<void*>(this));
+
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(this, &TransportTestPeer::Disconnect_s));
+
+ std::cerr << "~TransportTestPeer() completed" << std::endl;
+ }
+
+ void ConnectSocket(TransportTestPeer* peer) {
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(this, &TransportTestPeer::ConnectSocket_s, peer));
+ }
+
+ void ConnectSocket_s(TransportTestPeer* peer) {
+ loopback_->Connect(peer->loopback_);
+ ASSERT_EQ((nsresult)NS_OK, loopback_->Init());
+ flow_->PushLayer(loopback_);
+
+ loopback_->SignalPacketReceived.connect(this,
+ &TransportTestPeer::PacketReceived);
+
+ // SCTP here!
+ ASSERT_TRUE(sctp_);
+ std::cerr << "Calling usrsctp_bind()" << std::endl;
+ int r =
+ usrsctp_bind(sctp_, reinterpret_cast<struct sockaddr*>(&local_addr_),
+ sizeof(local_addr_));
+ ASSERT_GE(0, r);
+
+ std::cerr << "Calling usrsctp_connect()" << std::endl;
+ r = usrsctp_connect(sctp_,
+ reinterpret_cast<struct sockaddr*>(&remote_addr_),
+ sizeof(remote_addr_));
+ ASSERT_GE(0, r);
+ }
+
+ void Disconnect_s() {
+ disconnect_all();
+ if (flow_) {
+ flow_ = nullptr;
+ }
+ }
+
+ void Disconnect() { loopback_->Disconnect(); }
+
+ void StartTransfer(size_t to_send) {
+ periodic_ = new SendPeriodic(this, to_send);
+ timer_->SetTarget(test_utils_->sts_target());
+ timer_->InitWithCallback(periodic_, 10, nsITimer::TYPE_REPEATING_SLACK);
+ }
+
+ void SendOne() {
+ unsigned char buf[100];
+ memset(buf, sent_ & 0xff, sizeof(buf));
+
+ struct sctp_sndinfo info;
+ info.snd_sid = 1;
+ info.snd_flags = 0;
+ info.snd_ppid = 50; // What the heck is this?
+ info.snd_context = 0;
+ info.snd_assoc_id = 0;
+
+ int r = usrsctp_sendv(sctp_, buf, sizeof(buf), nullptr, 0,
+ static_cast<void*>(&info), sizeof(info),
+ SCTP_SENDV_SNDINFO, 0);
+ ASSERT_TRUE(r >= 0);
+ ASSERT_EQ(sizeof(buf), (size_t)r);
+
+ ++sent_;
+ }
+
+ int sent() const { return sent_; }
+ int received() const { return received_; }
+ bool connected() const { return connected_; }
+
+ static TransportResult SendPacket_s(UniquePtr<MediaPacket> packet,
+ const RefPtr<TransportFlow>& flow,
+ TransportLayer* layer) {
+ return layer->SendPacket(*packet);
+ }
+
+ TransportResult SendPacket(const unsigned char* data, size_t len) {
+ UniquePtr<MediaPacket> packet(new MediaPacket);
+ packet->Copy(data, len);
+
+ // Uses DISPATCH_NORMAL to avoid possible deadlocks when we're called
+ // from MainThread especially during shutdown (same as DataChannels).
+ // RUN_ON_THREAD short-circuits if already on the STS thread, which is
+ // normal for most transfers outside of connect() and close(). Passes
+ // a refptr to flow_ to avoid any async deletion issues (since we can't
+ // make 'this' into a refptr as it isn't refcounted)
+ RUN_ON_THREAD(test_utils_->sts_target(),
+ WrapRunnableNM(&TransportTestPeer::SendPacket_s,
+ std::move(packet), flow_, loopback_),
+ NS_DISPATCH_NORMAL);
+
+ return 0;
+ }
+
+ void PacketReceived(TransportLayer* layer, MediaPacket& packet) {
+ std::cerr << "Received " << packet.len() << " bytes" << std::endl;
+
+ // Pass the data to SCTP
+
+ usrsctp_conninput(static_cast<void*>(this), packet.data(), packet.len(), 0);
+ }
+
+ // Process SCTP notification
+ void Notification(union sctp_notification* msg, size_t len) {
+ ASSERT_EQ(msg->sn_header.sn_length, len);
+
+ if (msg->sn_header.sn_type == SCTP_ASSOC_CHANGE) {
+ struct sctp_assoc_change* change = &msg->sn_assoc_change;
+
+ if (change->sac_state == SCTP_COMM_UP) {
+ std::cerr << "Connection up" << std::endl;
+ SetConnected(true);
+ } else {
+ std::cerr << "Connection down" << std::endl;
+ SetConnected(false);
+ }
+ }
+ }
+
+ void SetConnected(bool state) { connected_ = state; }
+
+ static int conn_output(void* addr, void* buffer, size_t length, uint8_t tos,
+ uint8_t set_df) {
+ TransportTestPeer* peer = static_cast<TransportTestPeer*>(addr);
+
+ peer->SendPacket(static_cast<unsigned char*>(buffer), length);
+
+ return 0;
+ }
+
+ static int receive_cb(struct socket* sock, union sctp_sockstore addr,
+ void* data, size_t datalen, struct sctp_rcvinfo rcv,
+ int flags, void* ulp_info) {
+ TransportTestPeer* me =
+ static_cast<TransportTestPeer*>(addr.sconn.sconn_addr);
+ MOZ_ASSERT(me);
+
+ if (flags & MSG_NOTIFICATION) {
+ union sctp_notification* notif =
+ static_cast<union sctp_notification*>(data);
+
+ me->Notification(notif, datalen);
+ return 0;
+ }
+
+ me->received_ += datalen;
+
+ std::cerr << "receive_cb: sock " << sock << " data " << data << "("
+ << datalen << ") total received bytes = " << me->received_
+ << std::endl;
+
+ return 0;
+ }
+
+ private:
+ std::string name_;
+ bool connected_;
+ size_t sent_;
+ size_t received_;
+ // Owns the TransportLayerLoopback, but basically does nothing else.
+ RefPtr<TransportFlow> flow_;
+ TransportLayerLoopback* loopback_;
+
+ struct sockaddr_conn local_addr_;
+ struct sockaddr_conn remote_addr_;
+ struct socket* sctp_;
+ nsCOMPtr<nsITimer> timer_;
+ RefPtr<SendPeriodic> periodic_;
+ MtransportTestUtils* test_utils_;
+};
+
+// Implemented here because it calls a method of TransportTestPeer
+NS_IMETHODIMP SendPeriodic::Notify(nsITimer* timer) {
+ peer_->SendOne();
+ --to_send_;
+ if (!to_send_) {
+ timer->Cancel();
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SendPeriodic::GetName(nsACString& aName) {
+ aName.AssignLiteral("SendPeriodic");
+ return NS_OK;
+}
+
+class SctpTransportTest : public MtransportTest {
+ public:
+ SctpTransportTest() = default;
+
+ ~SctpTransportTest() = default;
+
+ static void debug_printf(const char* format, ...) {
+ va_list ap;
+
+ va_start(ap, format);
+ vprintf(format, ap);
+ va_end(ap);
+ }
+
+ static void SetUpTestCase() {
+ if (sctp_logging) {
+ usrsctp_init(0, &TransportTestPeer::conn_output, debug_printf);
+ usrsctp_sysctl_set_sctp_debug_on(0xffffffff);
+ } else {
+ usrsctp_init(0, &TransportTestPeer::conn_output, nullptr);
+ }
+ }
+
+ void TearDown() override {
+ if (p1_) p1_->Disconnect();
+ if (p2_) p2_->Disconnect();
+ delete p1_;
+ delete p2_;
+
+ MtransportTest::TearDown();
+ }
+
+ void ConnectSocket(int p1port = 0, int p2port = 0) {
+ if (!p1port) p1port = port_number++;
+ if (!p2port) p2port = port_number++;
+
+ p1_ = new TransportTestPeer("P1", p1port, p2port, test_utils_);
+ p2_ = new TransportTestPeer("P2", p2port, p1port, test_utils_);
+
+ p1_->ConnectSocket(p2_);
+ p2_->ConnectSocket(p1_);
+ ASSERT_TRUE_WAIT(p1_->connected(), 2000);
+ ASSERT_TRUE_WAIT(p2_->connected(), 2000);
+ }
+
+ void TestTransfer(int expected = 1) {
+ std::cerr << "Starting trasnsfer test" << std::endl;
+ p1_->StartTransfer(expected);
+ ASSERT_TRUE_WAIT(p1_->sent() == expected, 10000);
+ ASSERT_TRUE_WAIT(p2_->received() == (expected * 100), 10000);
+ std::cerr << "P2 received " << p2_->received() << std::endl;
+ }
+
+ protected:
+ TransportTestPeer* p1_ = nullptr;
+ TransportTestPeer* p2_ = nullptr;
+};
+
+TEST_F(SctpTransportTest, TestConnect) { ConnectSocket(); }
+
+TEST_F(SctpTransportTest, TestConnectSymmetricalPorts) {
+ ConnectSocket(5002, 5002);
+}
+
+TEST_F(SctpTransportTest, TestTransfer) {
+ ConnectSocket();
+ TestTransfer(50);
+}
+
+} // end namespace
diff --git a/dom/media/webrtc/transport/test/simpletokenbucket_unittest.cpp b/dom/media/webrtc/transport/test/simpletokenbucket_unittest.cpp
new file mode 100644
index 0000000000..66622d795b
--- /dev/null
+++ b/dom/media/webrtc/transport/test/simpletokenbucket_unittest.cpp
@@ -0,0 +1,114 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* Original author: bcampen@mozilla.com */
+
+#include "simpletokenbucket.h"
+
+#define GTEST_HAS_RTTI 0
+#include "gtest/gtest.h"
+
+using mozilla::SimpleTokenBucket;
+
+class TestSimpleTokenBucket : public SimpleTokenBucket {
+ public:
+ TestSimpleTokenBucket(size_t bucketSize, size_t tokensPerSecond)
+ : SimpleTokenBucket(bucketSize, tokensPerSecond) {}
+
+ void fastForward(int32_t timeMilliSeconds) {
+ if (timeMilliSeconds >= 0) {
+ last_time_tokens_added_ -= PR_MillisecondsToInterval(timeMilliSeconds);
+ } else {
+ last_time_tokens_added_ += PR_MillisecondsToInterval(-timeMilliSeconds);
+ }
+ }
+};
+
+TEST(SimpleTokenBucketTest, TestConstruct)
+{ TestSimpleTokenBucket b(10, 1); }
+
+TEST(SimpleTokenBucketTest, TestGet)
+{
+ TestSimpleTokenBucket b(10, 1);
+ ASSERT_EQ(5U, b.getTokens(5));
+}
+
+TEST(SimpleTokenBucketTest, TestGetAll)
+{
+ TestSimpleTokenBucket b(10, 1);
+ ASSERT_EQ(10U, b.getTokens(10));
+}
+
+TEST(SimpleTokenBucketTest, TestGetInsufficient)
+{
+ TestSimpleTokenBucket b(10, 1);
+ ASSERT_EQ(5U, b.getTokens(5));
+ ASSERT_EQ(5U, b.getTokens(6));
+}
+
+TEST(SimpleTokenBucketTest, TestGetBucketCount)
+{
+ TestSimpleTokenBucket b(10, 1);
+ ASSERT_EQ(10U, b.getTokens(UINT32_MAX));
+ ASSERT_EQ(5U, b.getTokens(5));
+ ASSERT_EQ(5U, b.getTokens(UINT32_MAX));
+}
+
+TEST(SimpleTokenBucketTest, TestTokenRefill)
+{
+ TestSimpleTokenBucket b(10, 1);
+ ASSERT_EQ(5U, b.getTokens(5));
+ b.fastForward(1000);
+ ASSERT_EQ(6U, b.getTokens(6));
+}
+
+TEST(SimpleTokenBucketTest, TestNoTimeWasted)
+{
+ // Makes sure that when the time elapsed is insufficient to add any
+ // tokens to the bucket, the internal timestamp that is used in this
+ // calculation is not updated (ie; two subsequent 0.5 second elapsed times
+ // counts as a full second)
+ TestSimpleTokenBucket b(10, 1);
+ ASSERT_EQ(5U, b.getTokens(5));
+ b.fastForward(500);
+ ASSERT_EQ(5U, b.getTokens(6));
+ b.fastForward(500);
+ ASSERT_EQ(6U, b.getTokens(6));
+}
+
+TEST(SimpleTokenBucketTest, TestNegativeTime)
+{
+ TestSimpleTokenBucket b(10, 1);
+ b.fastForward(-1000);
+ // Make sure we don't end up with an invalid number of tokens, but otherwise
+ // permit anything.
+ ASSERT_GT(11U, b.getTokens(100));
+}
+
+TEST(SimpleTokenBucketTest, TestEmptyBucket)
+{
+ TestSimpleTokenBucket b(10, 1);
+ ASSERT_EQ(10U, b.getTokens(10));
+ ASSERT_EQ(0U, b.getTokens(10));
+}
+
+TEST(SimpleTokenBucketTest, TestEmptyThenFillBucket)
+{
+ TestSimpleTokenBucket b(10, 1);
+ ASSERT_EQ(10U, b.getTokens(10));
+ ASSERT_EQ(0U, b.getTokens(1));
+ b.fastForward(50000);
+ ASSERT_EQ(10U, b.getTokens(10));
+}
+
+TEST(SimpleTokenBucketTest, TestNoOverflow)
+{
+ TestSimpleTokenBucket b(10, 1);
+ ASSERT_EQ(10U, b.getTokens(10));
+ ASSERT_EQ(0U, b.getTokens(1));
+ b.fastForward(50000);
+ ASSERT_EQ(10U, b.getTokens(11));
+}
diff --git a/dom/media/webrtc/transport/test/sockettransportservice_unittest.cpp b/dom/media/webrtc/transport/test/sockettransportservice_unittest.cpp
new file mode 100644
index 0000000000..ffa87fe91f
--- /dev/null
+++ b/dom/media/webrtc/transport/test/sockettransportservice_unittest.cpp
@@ -0,0 +1,181 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+#include <iostream>
+
+#include "prio.h"
+
+#include "nsCOMPtr.h"
+#include "nsNetCID.h"
+
+#include "nsISocketTransportService.h"
+
+#include "nsASocketHandler.h"
+#include "nsServiceManagerUtils.h"
+#include "nsThreadUtils.h"
+
+#define GTEST_HAS_RTTI 0
+#include "gtest/gtest.h"
+#include "gtest_utils.h"
+
+using namespace mozilla;
+
+namespace {
+class SocketTransportServiceTest : public MtransportTest {
+ public:
+ SocketTransportServiceTest()
+ : MtransportTest(),
+ received_(0),
+ readpipe_(nullptr),
+ writepipe_(nullptr),
+ registered_(false) {}
+
+ ~SocketTransportServiceTest() {
+ if (readpipe_) PR_Close(readpipe_);
+ if (writepipe_) PR_Close(writepipe_);
+ }
+
+ void SetUp();
+ void RegisterHandler();
+ void SendEvent();
+ void SendPacket();
+
+ void ReceivePacket() { ++received_; }
+
+ void ReceiveEvent() { ++received_; }
+
+ size_t Received() { return received_; }
+
+ private:
+ nsCOMPtr<nsISocketTransportService> stservice_;
+ nsCOMPtr<nsIEventTarget> target_;
+ size_t received_;
+ PRFileDesc* readpipe_;
+ PRFileDesc* writepipe_;
+ bool registered_;
+};
+
+// Received an event.
+class EventReceived : public Runnable {
+ public:
+ explicit EventReceived(SocketTransportServiceTest* test)
+ : Runnable("EventReceived"), test_(test) {}
+
+ NS_IMETHOD Run() override {
+ test_->ReceiveEvent();
+ return NS_OK;
+ }
+
+ SocketTransportServiceTest* test_;
+};
+
+// Register our listener on the socket
+class RegisterEvent : public Runnable {
+ public:
+ explicit RegisterEvent(SocketTransportServiceTest* test)
+ : Runnable("RegisterEvent"), test_(test) {}
+
+ NS_IMETHOD Run() override {
+ test_->RegisterHandler();
+ return NS_OK;
+ }
+
+ SocketTransportServiceTest* test_;
+};
+
+class SocketHandler : public nsASocketHandler {
+ public:
+ explicit SocketHandler(SocketTransportServiceTest* test) : test_(test) {}
+
+ void OnSocketReady(PRFileDesc* fd, int16_t outflags) override {
+ unsigned char buf[1600];
+
+ int32_t rv;
+ rv = PR_Recv(fd, buf, sizeof(buf), 0, PR_INTERVAL_NO_WAIT);
+ if (rv > 0) {
+ std::cerr << "Read " << rv << " bytes" << std::endl;
+ test_->ReceivePacket();
+ }
+ }
+
+ void OnSocketDetached(PRFileDesc* fd) override {}
+
+ void IsLocal(bool* aIsLocal) override {
+ // TODO(jesup): better check? Does it matter? (likely no)
+ *aIsLocal = false;
+ }
+
+ virtual uint64_t ByteCountSent() override { return 0; }
+ virtual uint64_t ByteCountReceived() override { return 0; }
+
+ NS_DECL_ISUPPORTS
+
+ protected:
+ virtual ~SocketHandler() = default;
+
+ private:
+ SocketTransportServiceTest* test_;
+};
+
+NS_IMPL_ISUPPORTS0(SocketHandler)
+
+void SocketTransportServiceTest::SetUp() {
+ MtransportTest::SetUp();
+
+ // Get the transport service as a dispatch target
+ nsresult rv;
+ target_ = do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+ // Get the transport service as a transport service
+ stservice_ = do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+ // Create a loopback pipe
+ PRStatus status = PR_CreatePipe(&readpipe_, &writepipe_);
+ ASSERT_EQ(status, PR_SUCCESS);
+
+ // Register ourselves as a listener for the read side of the
+ // socket. The registration has to happen on the STS thread,
+ // hence this event stuff.
+ rv = target_->Dispatch(new RegisterEvent(this), 0);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+ ASSERT_TRUE_WAIT(registered_, 10000);
+}
+
+void SocketTransportServiceTest::RegisterHandler() {
+ nsresult rv;
+
+ rv = stservice_->AttachSocket(readpipe_, new SocketHandler(this));
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+ registered_ = true;
+}
+
+void SocketTransportServiceTest::SendEvent() {
+ nsresult rv;
+
+ rv = target_->Dispatch(new EventReceived(this), 0);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+ ASSERT_TRUE_WAIT(Received() == 1, 10000);
+}
+
+void SocketTransportServiceTest::SendPacket() {
+ unsigned char buffer[1024];
+ memset(buffer, 0, sizeof(buffer));
+
+ int32_t status = PR_Write(writepipe_, buffer, sizeof(buffer));
+ uint32_t size = status & 0xffff;
+ ASSERT_EQ(sizeof(buffer), size);
+}
+
+// The unit tests themselves
+TEST_F(SocketTransportServiceTest, SendEvent) { SendEvent(); }
+
+TEST_F(SocketTransportServiceTest, SendPacket) { SendPacket(); }
+
+} // end namespace
diff --git a/dom/media/webrtc/transport/test/stunserver.cpp b/dom/media/webrtc/transport/test/stunserver.cpp
new file mode 100644
index 0000000000..b5fce21e19
--- /dev/null
+++ b/dom/media/webrtc/transport/test/stunserver.cpp
@@ -0,0 +1,652 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+/*
+Original code from nICEr and nrappkit.
+
+nICEr copyright:
+
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+nrappkit copyright:
+
+ Copyright (C) 2001-2003, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ ekr@rtfm.com Thu Dec 20 20:14:49 2001
+*/
+#include "logging.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Unused.h"
+#include "mediapacket.h"
+
+// mozilla/utils.h defines this as well
+#ifdef UNIMPLEMENTED
+# undef UNIMPLEMENTED
+#endif
+
+extern "C" {
+#include "nr_api.h"
+#include "async_wait.h"
+#include "async_timer.h"
+#include "nr_socket.h"
+#include "nr_socket_local.h"
+#include "transport_addr.h"
+#include "stun_util.h"
+#include "registry.h"
+#include "nr_socket_buffered_stun.h"
+}
+
+#include "stunserver.h"
+
+#include <string>
+
+MOZ_MTLOG_MODULE("stunserver");
+
+namespace mozilla {
+
+// Wrapper nr_socket which allows us to lie to the stun server about the
+// IP address.
+struct nr_socket_wrapped {
+ nr_socket* sock_;
+ nr_transport_addr addr_;
+};
+
+static int nr_socket_wrapped_destroy(void** objp) {
+ if (!objp || !*objp) return 0;
+
+ nr_socket_wrapped* wrapped = static_cast<nr_socket_wrapped*>(*objp);
+ *objp = nullptr;
+
+ delete wrapped;
+
+ return 0;
+}
+
+static int nr_socket_wrapped_sendto(void* obj, const void* msg, size_t len,
+ int flags, const nr_transport_addr* addr) {
+ nr_socket_wrapped* wrapped = static_cast<nr_socket_wrapped*>(obj);
+
+ return nr_socket_sendto(wrapped->sock_, msg, len, flags, &wrapped->addr_);
+}
+
+static int nr_socket_wrapped_recvfrom(void* obj, void* restrict buf,
+ size_t maxlen, size_t* len, int flags,
+ nr_transport_addr* addr) {
+ nr_socket_wrapped* wrapped = static_cast<nr_socket_wrapped*>(obj);
+
+ return nr_socket_recvfrom(wrapped->sock_, buf, maxlen, len, flags, addr);
+}
+
+static int nr_socket_wrapped_getfd(void* obj, NR_SOCKET* fd) {
+ nr_socket_wrapped* wrapped = static_cast<nr_socket_wrapped*>(obj);
+
+ return nr_socket_getfd(wrapped->sock_, fd);
+}
+
+static int nr_socket_wrapped_getaddr(void* obj, nr_transport_addr* addrp) {
+ nr_socket_wrapped* wrapped = static_cast<nr_socket_wrapped*>(obj);
+
+ return nr_socket_getaddr(wrapped->sock_, addrp);
+}
+
+static int nr_socket_wrapped_close(void* obj) { MOZ_CRASH(); }
+
+static int nr_socket_wrapped_set_send_addr(nr_socket* sock,
+ nr_transport_addr* addr) {
+ nr_socket_wrapped* wrapped = static_cast<nr_socket_wrapped*>(sock->obj);
+
+ return nr_transport_addr_copy(&wrapped->addr_, addr);
+}
+
+static nr_socket_vtbl nr_socket_wrapped_vtbl = {2,
+ nr_socket_wrapped_destroy,
+ nr_socket_wrapped_sendto,
+ nr_socket_wrapped_recvfrom,
+ nr_socket_wrapped_getfd,
+ nr_socket_wrapped_getaddr,
+ nullptr,
+ nullptr,
+ nullptr,
+ nr_socket_wrapped_close,
+ nullptr,
+ nullptr};
+
+int nr_socket_wrapped_create(nr_socket* inner, nr_socket** outp) {
+ auto wrapped = MakeUnique<nr_socket_wrapped>();
+
+ wrapped->sock_ = inner;
+
+ int r = nr_socket_create_int(wrapped.get(), &nr_socket_wrapped_vtbl, outp);
+ if (r) return r;
+
+ Unused << wrapped.release();
+ return 0;
+}
+
+// Instance static.
+// Note: Calling Create() at static init time is not going to be safe, since
+// we have no reason to expect this will be initted to a nullptr yet.
+TestStunServer* TestStunServer::instance;
+TestStunTcpServer* TestStunTcpServer::instance;
+TestStunServer* TestStunServer::instance6;
+TestStunTcpServer* TestStunTcpServer::instance6;
+uint16_t TestStunServer::instance_port = 3478;
+uint16_t TestStunTcpServer::instance_port = 3478;
+
+TestStunServer::~TestStunServer() {
+ // TODO(ekr@rtfm.com): Put this on the right thread.
+
+ // Unhook callback from our listen socket.
+ if (listen_sock_) {
+ NR_SOCKET fd;
+ if (!nr_socket_getfd(listen_sock_, &fd)) {
+ NR_ASYNC_CANCEL(fd, NR_ASYNC_WAIT_READ);
+ }
+ }
+
+ // Free up stun context and network resources
+ nr_stun_server_ctx_destroy(&stun_server_);
+ nr_socket_destroy(&listen_sock_);
+ nr_socket_destroy(&send_sock_);
+
+ // Make sure we aren't still waiting on a deferred response timer to pop
+ if (timer_handle_) NR_async_timer_cancel(timer_handle_);
+
+ delete response_addr_;
+}
+
+int TestStunServer::SetInternalPort(nr_local_addr* addr, uint16_t port) {
+ if (nr_transport_addr_set_port(&addr->addr, port)) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't set port");
+ return R_INTERNAL;
+ }
+
+ if (nr_transport_addr_fmt_addr_string(&addr->addr)) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't re-set addr string");
+ return R_INTERNAL;
+ }
+
+ return 0;
+}
+
+int TestStunServer::TryOpenListenSocket(nr_local_addr* addr, uint16_t port) {
+ int r = SetInternalPort(addr, port);
+
+ if (r) return r;
+
+ if (nr_socket_local_create(nullptr, &addr->addr, &listen_sock_)) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't create listen socket");
+ return R_ALREADY;
+ }
+
+ return 0;
+}
+
+static int addressFamilyToIpVersion(int address_family) {
+ switch (address_family) {
+ case AF_INET:
+ return NR_IPV4;
+ case AF_INET6:
+ return NR_IPV6;
+ default:
+ MOZ_CRASH();
+ }
+ return NR_IPV4;
+}
+
+int TestStunServer::Initialize(int address_family) {
+ static const size_t max_addrs = 100;
+ nr_local_addr addrs[max_addrs];
+ int addr_ct;
+ int r;
+ int i;
+
+ r = nr_stun_find_local_addresses(addrs, max_addrs, &addr_ct);
+ if (r) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't retrieve addresses");
+ return R_INTERNAL;
+ }
+
+ // removes duplicates and, based on prefs, loopback and link_local addrs
+ r = nr_stun_filter_local_addresses(addrs, &addr_ct);
+ if (r) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't filter addresses");
+ return R_INTERNAL;
+ }
+
+ if (addr_ct < 1) {
+ MOZ_MTLOG(ML_ERROR, "No local addresses");
+ return R_INTERNAL;
+ }
+
+ for (i = 0; i < addr_ct; ++i) {
+ if (addrs[i].addr.ip_version == addressFamilyToIpVersion(address_family)) {
+ break;
+ }
+ }
+
+ if (i == addr_ct) {
+ MOZ_MTLOG(ML_ERROR, "No local addresses of the configured IP version");
+ return R_INTERNAL;
+ }
+
+ int tries = 100;
+ while (tries--) {
+ // Bind on configured port (default 3478)
+ r = TryOpenListenSocket(&addrs[i], instance_port);
+ // We interpret R_ALREADY to mean the addr is probably in use. Try another.
+ // Otherwise, it either worked or it didn't, and we check below.
+ if (r != R_ALREADY) {
+ break;
+ }
+ ++instance_port;
+ }
+
+ if (r) {
+ return R_INTERNAL;
+ }
+
+ r = nr_socket_wrapped_create(listen_sock_, &send_sock_);
+ if (r) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't create send socket");
+ return R_INTERNAL;
+ }
+
+ r = nr_stun_server_ctx_create(const_cast<char*>("Test STUN server"),
+ &stun_server_);
+ if (r) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't create STUN server");
+ return R_INTERNAL;
+ }
+
+ // Cache the address and port.
+ char addr_string[INET6_ADDRSTRLEN];
+ r = nr_transport_addr_get_addrstring(&addrs[i].addr, addr_string,
+ sizeof(addr_string));
+ if (r) {
+ MOZ_MTLOG(ML_ERROR,
+ "Failed to convert listen addr to a string representation");
+ return R_INTERNAL;
+ }
+
+ listen_addr_ = addr_string;
+ listen_port_ = instance_port;
+
+ return 0;
+}
+
+UniquePtr<TestStunServer> TestStunServer::Create(int address_family) {
+ NR_reg_init(NR_REG_MODE_LOCAL);
+
+ UniquePtr<TestStunServer> server(new TestStunServer());
+
+ if (server->Initialize(address_family)) return nullptr;
+
+ NR_SOCKET fd;
+ int r = nr_socket_getfd(server->listen_sock_, &fd);
+ if (r) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't get fd");
+ return nullptr;
+ }
+
+ NR_ASYNC_WAIT(fd, NR_ASYNC_WAIT_READ, &TestStunServer::readable_cb,
+ server.get());
+
+ return server;
+}
+
+void TestStunServer::ConfigurePort(uint16_t port) { instance_port = port; }
+
+TestStunServer* TestStunServer::GetInstance(int address_family) {
+ switch (address_family) {
+ case AF_INET:
+ if (!instance) instance = Create(address_family).release();
+
+ MOZ_ASSERT(instance);
+ return instance;
+ case AF_INET6:
+ if (!instance6) instance6 = Create(address_family).release();
+
+ return instance6;
+ default:
+ MOZ_CRASH();
+ }
+}
+
+void TestStunServer::ShutdownInstance() {
+ delete instance;
+ instance = nullptr;
+ delete instance6;
+ instance6 = nullptr;
+}
+
+struct DeferredStunOperation {
+ DeferredStunOperation(TestStunServer* server, const char* data, size_t len,
+ nr_transport_addr* addr, nr_socket* sock)
+ : server_(server), buffer_(), sock_(sock) {
+ buffer_.Copy(reinterpret_cast<const uint8_t*>(data), len);
+ nr_transport_addr_copy(&addr_, addr);
+ }
+
+ TestStunServer* server_;
+ MediaPacket buffer_;
+ nr_transport_addr addr_;
+ nr_socket* sock_;
+};
+
+void TestStunServer::Process(const uint8_t* msg, size_t len,
+ nr_transport_addr* addr, nr_socket* sock) {
+ if (!sock) {
+ sock = send_sock_;
+ }
+
+ // Set the wrapped address so that the response goes to the right place.
+ nr_socket_wrapped_set_send_addr(sock, addr);
+
+ nr_stun_server_process_request(
+ stun_server_, sock, const_cast<char*>(reinterpret_cast<const char*>(msg)),
+ len, response_addr_ ? response_addr_ : addr, NR_STUN_AUTH_RULE_OPTIONAL);
+}
+
+void TestStunServer::process_cb(NR_SOCKET s, int how, void* cb_arg) {
+ DeferredStunOperation* op = static_cast<DeferredStunOperation*>(cb_arg);
+ op->server_->timer_handle_ = nullptr;
+ op->server_->Process(op->buffer_.data(), op->buffer_.len(), &op->addr_,
+ op->sock_);
+
+ delete op;
+}
+
+nr_socket* TestStunServer::GetReceivingSocket(NR_SOCKET s) {
+ return listen_sock_;
+}
+
+nr_socket* TestStunServer::GetSendingSocket(nr_socket* sock) {
+ return send_sock_;
+}
+
+void TestStunServer::readable_cb(NR_SOCKET s, int how, void* cb_arg) {
+ TestStunServer* server = static_cast<TestStunServer*>(cb_arg);
+
+ char message[max_stun_message_size];
+ size_t message_len;
+ nr_transport_addr addr;
+ nr_socket* recv_sock = server->GetReceivingSocket(s);
+ if (!recv_sock) {
+ MOZ_MTLOG(ML_ERROR, "Failed to lookup receiving socket");
+ return;
+ }
+ nr_socket* send_sock = server->GetSendingSocket(recv_sock);
+
+ /* Re-arm. */
+ NR_ASYNC_WAIT(s, NR_ASYNC_WAIT_READ, &TestStunServer::readable_cb, server);
+
+ if (nr_socket_recvfrom(recv_sock, message, sizeof(message), &message_len, 0,
+ &addr)) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't read STUN message");
+ return;
+ }
+
+ MOZ_MTLOG(ML_DEBUG, "Received data of length " << message_len);
+
+ // If we have initial dropping set, check at this point.
+ std::string key(addr.as_string);
+
+ if (server->received_ct_.count(key) == 0) {
+ server->received_ct_[key] = 0;
+ }
+
+ ++server->received_ct_[key];
+
+ if (!server->active_ || (server->received_ct_[key] <= server->initial_ct_)) {
+ MOZ_MTLOG(ML_DEBUG, "Dropping message #" << server->received_ct_[key]
+ << " from " << key);
+ return;
+ }
+
+ if (server->delay_ms_) {
+ NR_ASYNC_TIMER_SET(server->delay_ms_, process_cb,
+ new DeferredStunOperation(server, message, message_len,
+ &addr, send_sock),
+ &server->timer_handle_);
+ } else {
+ server->Process(reinterpret_cast<const uint8_t*>(message), message_len,
+ &addr, send_sock);
+ }
+}
+
+void TestStunServer::SetActive(bool active) { active_ = active; }
+
+void TestStunServer::SetDelay(uint32_t delay_ms) { delay_ms_ = delay_ms; }
+
+void TestStunServer::SetDropInitialPackets(uint32_t count) {
+ initial_ct_ = count;
+}
+
+nsresult TestStunServer::SetResponseAddr(nr_transport_addr* addr) {
+ delete response_addr_;
+
+ response_addr_ = new nr_transport_addr();
+
+ int r = nr_transport_addr_copy(response_addr_, addr);
+ if (r) return NS_ERROR_FAILURE;
+
+ return NS_OK;
+}
+
+nsresult TestStunServer::SetResponseAddr(const std::string& addr,
+ uint16_t port) {
+ nr_transport_addr addr2;
+
+ int r =
+ nr_str_port_to_transport_addr(addr.c_str(), port, IPPROTO_UDP, &addr2);
+ if (r) return NS_ERROR_FAILURE;
+
+ return SetResponseAddr(&addr2);
+}
+
+void TestStunServer::Reset() {
+ delay_ms_ = 0;
+ if (timer_handle_) {
+ NR_async_timer_cancel(timer_handle_);
+ timer_handle_ = nullptr;
+ }
+ delete response_addr_;
+ response_addr_ = nullptr;
+ received_ct_.clear();
+}
+
+// TestStunTcpServer
+
+void TestStunTcpServer::ConfigurePort(uint16_t port) { instance_port = port; }
+
+TestStunTcpServer* TestStunTcpServer::GetInstance(int address_family) {
+ switch (address_family) {
+ case AF_INET:
+ if (!instance) instance = Create(address_family).release();
+
+ MOZ_ASSERT(instance);
+ return instance;
+ case AF_INET6:
+ if (!instance6) instance6 = Create(address_family).release();
+
+ return instance6;
+ default:
+ MOZ_CRASH();
+ }
+}
+
+void TestStunTcpServer::ShutdownInstance() {
+ delete instance;
+ instance = nullptr;
+ delete instance6;
+ instance6 = nullptr;
+}
+
+int TestStunTcpServer::TryOpenListenSocket(nr_local_addr* addr, uint16_t port) {
+ addr->addr.protocol = IPPROTO_TCP;
+
+ int r = SetInternalPort(addr, port);
+
+ if (r) return r;
+
+ nr_socket* sock;
+ if (nr_socket_local_create(nullptr, &addr->addr, &sock)) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't create listen tcp socket");
+ return R_ALREADY;
+ }
+
+ if (nr_socket_buffered_stun_create(sock, 2048, TURN_TCP_FRAMING,
+ &listen_sock_)) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't create listen tcp socket");
+ return R_ALREADY;
+ }
+
+ if (nr_socket_listen(listen_sock_, 10)) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't listen on socket");
+ return R_ALREADY;
+ }
+
+ return 0;
+}
+
+nr_socket* TestStunTcpServer::GetReceivingSocket(NR_SOCKET s) {
+ return connections_[s];
+}
+
+nr_socket* TestStunTcpServer::GetSendingSocket(nr_socket* sock) { return sock; }
+
+void TestStunTcpServer::accept_cb(NR_SOCKET s, int how, void* cb_arg) {
+ TestStunTcpServer* server = static_cast<TestStunTcpServer*>(cb_arg);
+ nr_socket *newsock, *bufsock, *wrapsock;
+ nr_transport_addr remote_addr;
+ NR_SOCKET fd;
+
+ /* rearm */
+ NR_ASYNC_WAIT(s, NR_ASYNC_WAIT_READ, &TestStunTcpServer::accept_cb, cb_arg);
+
+ /* accept */
+ if (nr_socket_accept(server->listen_sock_, &remote_addr, &newsock)) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't accept incoming tcp connection");
+ return;
+ }
+
+ if (nr_socket_buffered_stun_create(newsock, 2048, TURN_TCP_FRAMING,
+ &bufsock)) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't create connected tcp socket");
+ nr_socket_destroy(&newsock);
+ return;
+ }
+
+ nr_socket_buffered_set_connected_to(bufsock, &remote_addr);
+
+ if (nr_socket_wrapped_create(bufsock, &wrapsock)) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't wrap connected tcp socket");
+ nr_socket_destroy(&bufsock);
+ return;
+ }
+
+ if (nr_socket_getfd(wrapsock, &fd)) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't get fd from connected tcp socket");
+ nr_socket_destroy(&wrapsock);
+ return;
+ }
+
+ server->connections_[fd] = wrapsock;
+
+ NR_ASYNC_WAIT(fd, NR_ASYNC_WAIT_READ, &TestStunServer::readable_cb, server);
+}
+
+UniquePtr<TestStunTcpServer> TestStunTcpServer::Create(int address_family) {
+ NR_reg_init(NR_REG_MODE_LOCAL);
+
+ UniquePtr<TestStunTcpServer> server(new TestStunTcpServer());
+
+ if (server->Initialize(address_family)) {
+ return nullptr;
+ }
+
+ NR_SOCKET fd;
+ if (nr_socket_getfd(server->listen_sock_, &fd)) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't get tcp fd");
+ return nullptr;
+ }
+
+ NR_ASYNC_WAIT(fd, NR_ASYNC_WAIT_READ, &TestStunTcpServer::accept_cb,
+ server.get());
+
+ return server;
+}
+
+TestStunTcpServer::~TestStunTcpServer() {
+ for (auto it = connections_.begin(); it != connections_.end();) {
+ NR_ASYNC_CANCEL(it->first, NR_ASYNC_WAIT_READ);
+ nr_socket_destroy(&it->second);
+ connections_.erase(it++);
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/transport/test/stunserver.h b/dom/media/webrtc/transport/test/stunserver.h
new file mode 100644
index 0000000000..4903cb89ad
--- /dev/null
+++ b/dom/media/webrtc/transport/test/stunserver.h
@@ -0,0 +1,123 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+#ifndef stunserver_h__
+#define stunserver_h__
+
+#include <map>
+#include <string>
+#include "nsError.h"
+#include "mozilla/UniquePtr.h"
+
+typedef struct nr_stun_server_ctx_ nr_stun_server_ctx;
+typedef struct nr_socket_ nr_socket;
+typedef struct nr_local_addr_ nr_local_addr;
+
+namespace mozilla {
+
+class TestStunServer {
+ public:
+ // Generally, you should only call API in this class from the same thread that
+ // the initial |GetInstance| call was made from.
+ static TestStunServer* GetInstance(int address_family = AF_INET);
+ static void ShutdownInstance();
+ // |ConfigurePort| will only have an effect if called before the first call
+ // to |GetInstance| (possibly following a |ShutdownInstance| call)
+ static void ConfigurePort(uint16_t port);
+ // AF_INET, AF_INET6
+ static UniquePtr<TestStunServer> Create(int address_family);
+
+ virtual ~TestStunServer();
+
+ void SetActive(bool active);
+ void SetDelay(uint32_t delay_ms);
+ void SetDropInitialPackets(uint32_t count);
+ const std::string& addr() const { return listen_addr_; }
+ uint16_t port() const { return listen_port_; }
+
+ // These should only be called from the same thread as the initial
+ // |GetInstance| call.
+ nsresult SetResponseAddr(nr_transport_addr* addr);
+ nsresult SetResponseAddr(const std::string& addr, uint16_t port);
+
+ void Reset();
+
+ static const size_t max_stun_message_size = 4096;
+
+ virtual nr_socket* GetReceivingSocket(NR_SOCKET s);
+ virtual nr_socket* GetSendingSocket(nr_socket* sock);
+
+ protected:
+ TestStunServer()
+ : listen_port_(0),
+ listen_sock_(nullptr),
+ send_sock_(nullptr),
+ stun_server_(nullptr),
+ active_(true),
+ delay_ms_(0),
+ initial_ct_(0),
+ response_addr_(nullptr),
+ timer_handle_(nullptr) {}
+
+ int SetInternalPort(nr_local_addr* addr, uint16_t port);
+ int Initialize(int address_family);
+
+ static void readable_cb(NR_SOCKET sock, int how, void* cb_arg);
+
+ private:
+ void Process(const uint8_t* msg, size_t len, nr_transport_addr* addr_in,
+ nr_socket* sock);
+ virtual int TryOpenListenSocket(nr_local_addr* addr, uint16_t port);
+ static void process_cb(NR_SOCKET sock, int how, void* cb_arg);
+
+ protected:
+ std::string listen_addr_;
+ uint16_t listen_port_;
+ nr_socket* listen_sock_;
+ nr_socket* send_sock_;
+ nr_stun_server_ctx* stun_server_;
+
+ private:
+ bool active_;
+ uint32_t delay_ms_;
+ uint32_t initial_ct_;
+ nr_transport_addr* response_addr_;
+ void* timer_handle_;
+ std::map<std::string, uint32_t> received_ct_;
+
+ static TestStunServer* instance;
+ static TestStunServer* instance6;
+ static uint16_t instance_port;
+};
+
+class TestStunTcpServer : public TestStunServer {
+ public:
+ static TestStunTcpServer* GetInstance(int address_family);
+ static void ShutdownInstance();
+ static void ConfigurePort(uint16_t port);
+ virtual ~TestStunTcpServer();
+
+ virtual nr_socket* GetReceivingSocket(NR_SOCKET s);
+ virtual nr_socket* GetSendingSocket(nr_socket* sock);
+
+ protected:
+ TestStunTcpServer() = default;
+ static void accept_cb(NR_SOCKET sock, int how, void* cb_arg);
+
+ private:
+ virtual int TryOpenListenSocket(nr_local_addr* addr, uint16_t port);
+ static UniquePtr<TestStunTcpServer> Create(int address_family);
+
+ static TestStunTcpServer* instance;
+ static TestStunTcpServer* instance6;
+ static uint16_t instance_port;
+
+ std::map<NR_SOCKET, nr_socket*> connections_;
+};
+} // End of namespace mozilla
+#endif
diff --git a/dom/media/webrtc/transport/test/test_nr_socket_ice_unittest.cpp b/dom/media/webrtc/transport/test/test_nr_socket_ice_unittest.cpp
new file mode 100644
index 0000000000..b55b05f10c
--- /dev/null
+++ b/dom/media/webrtc/transport/test/test_nr_socket_ice_unittest.cpp
@@ -0,0 +1,409 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Some of this code is taken from nricectx.cpp and nricemediastream.cpp
+// which in turn contains code cut-and-pasted from nICEr. Copyright is:
+
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include "gtest/gtest.h"
+#include "gtest_utils.h"
+
+extern "C" {
+#include "ice_ctx.h"
+#include "ice_peer_ctx.h"
+#include "nICEr/src/net/transport_addr.h"
+}
+
+#include "mtransport_test_utils.h"
+#include "nricectx.h"
+#include "nricemediastream.h"
+#include "runnable_utils.h"
+#include "test_nr_socket.h"
+
+namespace mozilla {
+
+static unsigned int kDefaultTimeout = 7000;
+
+class IcePeer {
+ public:
+ IcePeer(const char* name, TestNat* nat, UINT4 flags,
+ MtransportTestUtils* test_utils)
+ : name_(name),
+ ice_checking_(false),
+ ice_connected_(false),
+ ice_disconnected_(false),
+ gather_cb_(false),
+ stream_ready_(false),
+ stream_failed_(false),
+ ice_ctx_(nullptr),
+ peer_ctx_(nullptr),
+ nat_(nat),
+ test_utils_(test_utils) {
+ nr_ice_ctx_create(const_cast<char*>(name_.c_str()), flags, &ice_ctx_);
+
+ if (nat_) {
+ nr_socket_factory* factory;
+ nat_->create_socket_factory(&factory);
+ nr_ice_ctx_set_socket_factory(ice_ctx_, factory);
+ }
+
+ // Create the handler objects
+ ice_handler_vtbl_ = new nr_ice_handler_vtbl();
+ ice_handler_vtbl_->select_pair = &IcePeer::select_pair;
+ ice_handler_vtbl_->stream_ready = &IcePeer::stream_ready;
+ ice_handler_vtbl_->stream_failed = &IcePeer::stream_failed;
+ ice_handler_vtbl_->ice_connected = &IcePeer::ice_connected;
+ ice_handler_vtbl_->msg_recvd = &IcePeer::msg_recvd;
+ ice_handler_vtbl_->ice_checking = &IcePeer::ice_checking;
+ ice_handler_vtbl_->ice_disconnected = &IcePeer::ice_disconnected;
+
+ ice_handler_ = new nr_ice_handler();
+ ice_handler_->vtbl = ice_handler_vtbl_;
+ ice_handler_->obj = this;
+
+ nr_ice_peer_ctx_create(ice_ctx_, ice_handler_,
+ const_cast<char*>(name_.c_str()), &peer_ctx_);
+
+ nr_ice_add_media_stream(ice_ctx_, const_cast<char*>(name_.c_str()), "ufrag",
+ "pass", 2, &ice_media_stream_);
+ EXPECT_EQ(2UL, GetStreamAttributes().size());
+
+ nr_ice_media_stream_initialize(ice_ctx_, ice_media_stream_);
+ }
+
+ virtual ~IcePeer() { Destroy(); }
+
+ void Destroy() {
+ test_utils_->SyncDispatchToSTS(WrapRunnable(this, &IcePeer::Destroy_s));
+ }
+
+ void Destroy_s() {
+ nr_ice_peer_ctx_destroy(&peer_ctx_);
+ delete ice_handler_;
+ delete ice_handler_vtbl_;
+ nr_ice_ctx_destroy(&ice_ctx_);
+ }
+
+ void Gather(bool default_route_only = false) {
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(this, &IcePeer::Gather_s, default_route_only));
+ }
+
+ void Gather_s(bool default_route_only = false) {
+ int r = nr_ice_gather(ice_ctx_, &IcePeer::gather_cb, this);
+ ASSERT_TRUE(r == 0 || r == R_WOULDBLOCK);
+ }
+
+ std::vector<std::string> GetStreamAttributes() {
+ std::vector<std::string> attributes;
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnableRet(&attributes, this, &IcePeer::GetStreamAttributes_s));
+ return attributes;
+ }
+
+ std::vector<std::string> GetStreamAttributes_s() {
+ char** attrs = nullptr;
+ int attrct;
+ std::vector<std::string> ret;
+
+ int r =
+ nr_ice_media_stream_get_attributes(ice_media_stream_, &attrs, &attrct);
+ EXPECT_EQ(0, r);
+
+ for (int i = 0; i < attrct; i++) {
+ ret.push_back(std::string(attrs[i]));
+ RFREE(attrs[i]);
+ }
+ RFREE(attrs);
+
+ return ret;
+ }
+
+ std::vector<std::string> GetGlobalAttributes() {
+ std::vector<std::string> attributes;
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnableRet(&attributes, this, &IcePeer::GetGlobalAttributes_s));
+ return attributes;
+ }
+
+ std::vector<std::string> GetGlobalAttributes_s() {
+ char** attrs = nullptr;
+ int attrct;
+ std::vector<std::string> ret;
+
+ nr_ice_get_global_attributes(ice_ctx_, &attrs, &attrct);
+
+ for (int i = 0; i < attrct; i++) {
+ ret.push_back(std::string(attrs[i]));
+ RFREE(attrs[i]);
+ }
+ RFREE(attrs);
+
+ return ret;
+ }
+
+ void ParseGlobalAttributes(std::vector<std::string> attrs) {
+ std::vector<char*> attrs_in;
+ attrs_in.reserve(attrs.size());
+ for (auto& attr : attrs) {
+ attrs_in.push_back(const_cast<char*>(attr.c_str()));
+ }
+
+ int r = nr_ice_peer_ctx_parse_global_attributes(
+ peer_ctx_, attrs_in.empty() ? nullptr : &attrs_in[0], attrs_in.size());
+ ASSERT_EQ(0, r);
+ }
+
+ void SetControlling(bool controlling) {
+ peer_ctx_->controlling = controlling ? 1 : 0;
+ }
+
+ void SetRemoteAttributes(std::vector<std::string> attributes) {
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(this, &IcePeer::SetRemoteAttributes_s, attributes));
+ }
+
+ void SetRemoteAttributes_s(std::vector<std::string> attributes) {
+ int r;
+
+ std::vector<char*> attrs;
+ attrs.reserve(attributes.size());
+ for (auto& attr : attributes) {
+ attrs.push_back(const_cast<char*>(attr.c_str()));
+ }
+
+ if (!attrs.empty()) {
+ r = nr_ice_peer_ctx_parse_stream_attributes(peer_ctx_, ice_media_stream_,
+ &attrs[0], attrs.size());
+ ASSERT_EQ(0, r);
+ }
+ }
+
+ void StartChecks() {
+ test_utils_->SyncDispatchToSTS(WrapRunnable(this, &IcePeer::StartChecks_s));
+ }
+
+ void StartChecks_s() {
+ int r = nr_ice_peer_ctx_pair_candidates(peer_ctx_);
+ ASSERT_EQ(0, r);
+
+ r = nr_ice_peer_ctx_start_checks2(peer_ctx_, 1);
+ ASSERT_EQ(0, r);
+ }
+
+ // Handler callbacks
+ static int select_pair(void* obj, nr_ice_media_stream* stream,
+ int component_id, nr_ice_cand_pair** potentials,
+ int potential_ct) {
+ return 0;
+ }
+
+ static int stream_ready(void* obj, nr_ice_media_stream* stream) {
+ IcePeer* peer = static_cast<IcePeer*>(obj);
+ peer->stream_ready_ = true;
+ return 0;
+ }
+
+ static int stream_failed(void* obj, nr_ice_media_stream* stream) {
+ IcePeer* peer = static_cast<IcePeer*>(obj);
+ peer->stream_failed_ = true;
+ return 0;
+ }
+
+ static int ice_checking(void* obj, nr_ice_peer_ctx* pctx) {
+ IcePeer* peer = static_cast<IcePeer*>(obj);
+ peer->ice_checking_ = true;
+ return 0;
+ }
+
+ static int ice_connected(void* obj, nr_ice_peer_ctx* pctx) {
+ IcePeer* peer = static_cast<IcePeer*>(obj);
+ peer->ice_connected_ = true;
+ return 0;
+ }
+
+ static int ice_disconnected(void* obj, nr_ice_peer_ctx* pctx) {
+ IcePeer* peer = static_cast<IcePeer*>(obj);
+ peer->ice_disconnected_ = true;
+ return 0;
+ }
+
+ static int msg_recvd(void* obj, nr_ice_peer_ctx* pctx,
+ nr_ice_media_stream* stream, int component_id,
+ UCHAR* msg, int len) {
+ return 0;
+ }
+
+ static void gather_cb(NR_SOCKET s, int h, void* arg) {
+ IcePeer* peer = static_cast<IcePeer*>(arg);
+ peer->gather_cb_ = true;
+ }
+
+ std::string name_;
+
+ bool ice_checking_;
+ bool ice_connected_;
+ bool ice_disconnected_;
+ bool gather_cb_;
+ bool stream_ready_;
+ bool stream_failed_;
+
+ nr_ice_ctx* ice_ctx_;
+ nr_ice_handler* ice_handler_;
+ nr_ice_handler_vtbl* ice_handler_vtbl_;
+ nr_ice_media_stream* ice_media_stream_;
+ nr_ice_peer_ctx* peer_ctx_;
+ TestNat* nat_;
+ MtransportTestUtils* test_utils_;
+};
+
+class TestNrSocketIceUnitTest : public ::testing::Test {
+ public:
+ void SetUp() override {
+ NSS_NoDB_Init(nullptr);
+ NSS_SetDomesticPolicy();
+
+ test_utils_ = new MtransportTestUtils();
+ test_utils2_ = new MtransportTestUtils();
+
+ NrIceCtx::InitializeGlobals(NrIceCtx::GlobalConfig());
+ }
+
+ void TearDown() override {
+ delete test_utils_;
+ delete test_utils2_;
+ }
+
+ MtransportTestUtils* test_utils_;
+ MtransportTestUtils* test_utils2_;
+};
+
+TEST_F(TestNrSocketIceUnitTest, TestIcePeer) {
+ IcePeer peer("IcePeer", nullptr, NR_ICE_CTX_FLAGS_AGGRESSIVE_NOMINATION,
+ test_utils_);
+ ASSERT_NE(peer.ice_ctx_, nullptr);
+ ASSERT_NE(peer.peer_ctx_, nullptr);
+ ASSERT_NE(peer.ice_media_stream_, nullptr);
+ ASSERT_EQ(2UL, peer.GetStreamAttributes().size())
+ << "Should have ice-ufrag and ice-pwd";
+ peer.Gather();
+ ASSERT_LT(2UL, peer.GetStreamAttributes().size())
+ << "Should have ice-ufrag, ice-pwd, and at least one candidate.";
+}
+
+TEST_F(TestNrSocketIceUnitTest, TestIcePeersNoNAT) {
+ IcePeer peer("IcePeer", nullptr, NR_ICE_CTX_FLAGS_AGGRESSIVE_NOMINATION,
+ test_utils_);
+ IcePeer peer2("IcePeer2", nullptr, NR_ICE_CTX_FLAGS_AGGRESSIVE_NOMINATION,
+ test_utils2_);
+ peer.SetControlling(true);
+ peer2.SetControlling(false);
+
+ peer.Gather();
+ peer2.Gather();
+ std::vector<std::string> attrs = peer.GetGlobalAttributes();
+ peer2.ParseGlobalAttributes(attrs);
+ std::vector<std::string> attributes = peer.GetStreamAttributes();
+ peer2.SetRemoteAttributes(attributes);
+
+ attrs = peer2.GetGlobalAttributes();
+ peer.ParseGlobalAttributes(attrs);
+ attributes = peer2.GetStreamAttributes();
+ peer.SetRemoteAttributes(attributes);
+ peer2.StartChecks();
+ peer.StartChecks();
+
+ ASSERT_TRUE_WAIT(peer.ice_connected_, kDefaultTimeout);
+ ASSERT_TRUE_WAIT(peer2.ice_connected_, kDefaultTimeout);
+}
+
+TEST_F(TestNrSocketIceUnitTest, TestIcePeersPacketLoss) {
+ IcePeer peer("IcePeer", nullptr, NR_ICE_CTX_FLAGS_AGGRESSIVE_NOMINATION,
+ test_utils_);
+
+ RefPtr<TestNat> nat(new TestNat);
+ class NatDelegate : public TestNat::NatDelegate {
+ public:
+ NatDelegate() : messages(0) {}
+
+ int on_read(TestNat* nat, void* buf, size_t maxlen, size_t* len) override {
+ return 0;
+ }
+
+ int on_sendto(TestNat* nat, const void* msg, size_t len, int flags,
+ const nr_transport_addr* to) override {
+ ++messages;
+ // 25% packet loss
+ if (messages % 4 == 0) {
+ return 1;
+ }
+ return 0;
+ }
+
+ int on_write(TestNat* nat, const void* msg, size_t len,
+ size_t* written) override {
+ return 0;
+ }
+
+ int messages;
+ } delegate;
+ nat->nat_delegate_ = &delegate;
+
+ IcePeer peer2("IcePeer2", nat, NR_ICE_CTX_FLAGS_AGGRESSIVE_NOMINATION,
+ test_utils2_);
+ peer.SetControlling(true);
+ peer2.SetControlling(false);
+
+ peer.Gather();
+ peer2.Gather();
+ std::vector<std::string> attrs = peer.GetGlobalAttributes();
+ peer2.ParseGlobalAttributes(attrs);
+ std::vector<std::string> attributes = peer.GetStreamAttributes();
+ peer2.SetRemoteAttributes(attributes);
+
+ attrs = peer2.GetGlobalAttributes();
+ peer.ParseGlobalAttributes(attrs);
+ attributes = peer2.GetStreamAttributes();
+ peer.SetRemoteAttributes(attributes);
+ peer2.StartChecks();
+ peer.StartChecks();
+
+ ASSERT_TRUE_WAIT(peer.ice_connected_, kDefaultTimeout);
+ ASSERT_TRUE_WAIT(peer2.ice_connected_, kDefaultTimeout);
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/transport/test/test_nr_socket_unittest.cpp b/dom/media/webrtc/transport/test/test_nr_socket_unittest.cpp
new file mode 100644
index 0000000000..af2779accd
--- /dev/null
+++ b/dom/media/webrtc/transport/test/test_nr_socket_unittest.cpp
@@ -0,0 +1,800 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: bcampen@mozilla.com
+
+#include <cstddef>
+
+extern "C" {
+#include "r_errors.h"
+#include "async_wait.h"
+}
+
+#include "test_nr_socket.h"
+
+#include "nsCOMPtr.h"
+#include "nsNetCID.h"
+#include "nsServiceManagerUtils.h"
+#include "runnable_utils.h"
+
+#include <vector>
+
+#define GTEST_HAS_RTTI 0
+#include "gtest/gtest.h"
+#include "gtest_utils.h"
+
+#define DATA_BUF_SIZE 1024
+
+namespace mozilla {
+
+class TestNrSocketTest : public MtransportTest {
+ public:
+ TestNrSocketTest()
+ : MtransportTest(),
+ wait_done_for_main_(false),
+ sts_(),
+ public_addrs_(),
+ private_addrs_(),
+ nats_() {}
+
+ void SetUp() override {
+ MtransportTest::SetUp();
+
+ // Get the transport service as a dispatch target
+ nsresult rv;
+ sts_ = do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv);
+ EXPECT_TRUE(NS_SUCCEEDED(rv)) << "Failed to get STS: " << (int)rv;
+ }
+
+ void TearDown() override {
+ SyncDispatchToSTS(WrapRunnable(this, &TestNrSocketTest::TearDown_s));
+
+ MtransportTest::TearDown();
+ }
+
+ void TearDown_s() {
+ public_addrs_.clear();
+ private_addrs_.clear();
+ nats_.clear();
+ sts_ = nullptr;
+ }
+
+ RefPtr<TestNrSocket> CreateTestNrSocket_s(const char* ip_str, int proto,
+ TestNat* nat) {
+ // If no nat is supplied, we create a default NAT which is disabled. This
+ // is how we simulate a non-natted socket.
+ RefPtr<TestNrSocket> sock(new TestNrSocket(nat ? nat : new TestNat));
+ nr_transport_addr address;
+ nr_str_port_to_transport_addr(ip_str, 0, proto, &address);
+ int r = sock->create(&address);
+ if (r) {
+ return nullptr;
+ }
+ return sock;
+ }
+
+ void CreatePublicAddrs(size_t count, const char* ip_str = "127.0.0.1",
+ int proto = IPPROTO_UDP) {
+ SyncDispatchToSTS(WrapRunnable(this, &TestNrSocketTest::CreatePublicAddrs_s,
+ count, ip_str, proto));
+ }
+
+ void CreatePublicAddrs_s(size_t count, const char* ip_str, int proto) {
+ while (count--) {
+ auto sock = CreateTestNrSocket_s(ip_str, proto, nullptr);
+ ASSERT_TRUE(sock)
+ << "Failed to create socket";
+ public_addrs_.push_back(sock);
+ }
+ }
+
+ RefPtr<TestNat> CreatePrivateAddrs(size_t size,
+ const char* ip_str = "127.0.0.1",
+ int proto = IPPROTO_UDP) {
+ RefPtr<TestNat> result;
+ SyncDispatchToSTS(WrapRunnableRet(&result, this,
+ &TestNrSocketTest::CreatePrivateAddrs_s,
+ size, ip_str, proto));
+ return result;
+ }
+
+ RefPtr<TestNat> CreatePrivateAddrs_s(size_t count, const char* ip_str,
+ int proto) {
+ RefPtr<TestNat> nat(new TestNat);
+ while (count--) {
+ auto sock = CreateTestNrSocket_s(ip_str, proto, nat);
+ if (!sock) {
+ EXPECT_TRUE(false) << "Failed to create socket";
+ break;
+ }
+ private_addrs_.push_back(sock);
+ }
+ nat->enabled_ = true;
+ nats_.push_back(nat);
+ return nat;
+ }
+
+ bool CheckConnectivityVia(
+ TestNrSocket* from, TestNrSocket* to, const nr_transport_addr& via,
+ nr_transport_addr* sender_external_address = nullptr) {
+ MOZ_ASSERT(from);
+
+ if (!WaitForWriteable(from)) {
+ return false;
+ }
+
+ int result = 0;
+ SyncDispatchToSTS(WrapRunnableRet(
+ &result, this, &TestNrSocketTest::SendData_s, from, via));
+ if (result) {
+ return false;
+ }
+
+ if (!WaitForReadable(to)) {
+ return false;
+ }
+
+ nr_transport_addr dummy_outparam;
+ if (!sender_external_address) {
+ sender_external_address = &dummy_outparam;
+ }
+
+ MOZ_ASSERT(to);
+ SyncDispatchToSTS(WrapRunnableRet(&result, this,
+ &TestNrSocketTest::RecvData_s, to,
+ sender_external_address));
+
+ return !result;
+ }
+
+ bool CheckConnectivity(TestNrSocket* from, TestNrSocket* to,
+ nr_transport_addr* sender_external_address = nullptr) {
+ nr_transport_addr destination_address;
+ int r = GetAddress(to, &destination_address);
+ if (r) {
+ return false;
+ }
+
+ return CheckConnectivityVia(from, to, destination_address,
+ sender_external_address);
+ }
+
+ bool CheckTcpConnectivity(TestNrSocket* from, TestNrSocket* to) {
+ NrSocketBase* accepted_sock;
+ if (!Connect(from, to, &accepted_sock)) {
+ std::cerr << "Connect failed" << std::endl;
+ return false;
+ }
+
+ // write on |from|, recv on |accepted_sock|
+ if (!WaitForWriteable(from)) {
+ std::cerr << __LINE__ << "WaitForWriteable (1) failed" << std::endl;
+ return false;
+ }
+
+ int r;
+ SyncDispatchToSTS(
+ WrapRunnableRet(&r, this, &TestNrSocketTest::SendDataTcp_s, from));
+ if (r) {
+ std::cerr << "SendDataTcp_s (1) failed" << std::endl;
+ return false;
+ }
+
+ if (!WaitForReadable(accepted_sock)) {
+ std::cerr << __LINE__ << "WaitForReadable (1) failed" << std::endl;
+ return false;
+ }
+
+ SyncDispatchToSTS(WrapRunnableRet(
+ &r, this, &TestNrSocketTest::RecvDataTcp_s, accepted_sock));
+ if (r) {
+ std::cerr << "RecvDataTcp_s (1) failed" << std::endl;
+ return false;
+ }
+
+ if (!WaitForWriteable(accepted_sock)) {
+ std::cerr << __LINE__ << "WaitForWriteable (2) failed" << std::endl;
+ return false;
+ }
+
+ SyncDispatchToSTS(WrapRunnableRet(
+ &r, this, &TestNrSocketTest::SendDataTcp_s, accepted_sock));
+ if (r) {
+ std::cerr << "SendDataTcp_s (2) failed" << std::endl;
+ return false;
+ }
+
+ if (!WaitForReadable(from)) {
+ std::cerr << __LINE__ << "WaitForReadable (2) failed" << std::endl;
+ return false;
+ }
+
+ SyncDispatchToSTS(
+ WrapRunnableRet(&r, this, &TestNrSocketTest::RecvDataTcp_s, from));
+ if (r) {
+ std::cerr << "RecvDataTcp_s (2) failed" << std::endl;
+ return false;
+ }
+
+ return true;
+ }
+
+ int GetAddress(TestNrSocket* sock, nr_transport_addr_* address) {
+ MOZ_ASSERT(sock);
+ MOZ_ASSERT(address);
+ int r;
+ SyncDispatchToSTS(WrapRunnableRet(&r, this, &TestNrSocketTest::GetAddress_s,
+ sock, address));
+ return r;
+ }
+
+ int GetAddress_s(TestNrSocket* sock, nr_transport_addr* address) {
+ return sock->getaddr(address);
+ }
+
+ int SendData_s(TestNrSocket* from, const nr_transport_addr& to) {
+ // It is up to caller to ensure that |from| is writeable.
+ const char buf[] = "foobajooba";
+ return from->sendto(buf, sizeof(buf), 0, &to);
+ }
+
+ int SendDataTcp_s(NrSocketBase* from) {
+ // It is up to caller to ensure that |from| is writeable.
+ const char buf[] = "foobajooba";
+ size_t written;
+ return from->write(buf, sizeof(buf), &written);
+ }
+
+ int RecvData_s(TestNrSocket* to, nr_transport_addr* from) {
+ // It is up to caller to ensure that |to| is readable
+ char buf[DATA_BUF_SIZE];
+ size_t len;
+ // Maybe check that data matches?
+ int r = to->recvfrom(buf, sizeof(buf), &len, 0, from);
+ if (!r && (len == 0)) {
+ r = R_INTERNAL;
+ }
+ return r;
+ }
+
+ int RecvDataTcp_s(NrSocketBase* to) {
+ // It is up to caller to ensure that |to| is readable
+ char buf[DATA_BUF_SIZE];
+ size_t len;
+ // Maybe check that data matches?
+ int r = to->read(buf, sizeof(buf), &len);
+ if (!r && (len == 0)) {
+ r = R_INTERNAL;
+ }
+ return r;
+ }
+
+ int Listen_s(TestNrSocket* to) {
+ // listen on |to|
+ int r = to->listen(1);
+ if (r) {
+ return r;
+ }
+ return 0;
+ }
+
+ int Connect_s(TestNrSocket* from, TestNrSocket* to) {
+ // connect on |from|
+ nr_transport_addr destination_address;
+ int r = to->getaddr(&destination_address);
+ if (r) {
+ return r;
+ }
+
+ r = from->connect(&destination_address);
+ if (r) {
+ return r;
+ }
+
+ return 0;
+ }
+
+ int Accept_s(TestNrSocket* to, NrSocketBase** accepted_sock) {
+ nr_socket* sock;
+ nr_transport_addr source_address;
+ int r = to->accept(&source_address, &sock);
+ if (r) {
+ return r;
+ }
+
+ *accepted_sock = reinterpret_cast<NrSocketBase*>(sock->obj);
+ return 0;
+ }
+
+ bool Connect(TestNrSocket* from, TestNrSocket* to,
+ NrSocketBase** accepted_sock) {
+ int r;
+ SyncDispatchToSTS(
+ WrapRunnableRet(&r, this, &TestNrSocketTest::Listen_s, to));
+ if (r) {
+ std::cerr << "Listen_s failed: " << r << std::endl;
+ return false;
+ }
+
+ SyncDispatchToSTS(
+ WrapRunnableRet(&r, this, &TestNrSocketTest::Connect_s, from, to));
+ if (r && r != R_WOULDBLOCK) {
+ std::cerr << "Connect_s failed: " << r << std::endl;
+ return false;
+ }
+
+ if (!WaitForReadable(to)) {
+ std::cerr << "WaitForReadable failed" << std::endl;
+ return false;
+ }
+
+ SyncDispatchToSTS(WrapRunnableRet(&r, this, &TestNrSocketTest::Accept_s, to,
+ accepted_sock));
+
+ if (r) {
+ std::cerr << "Accept_s failed: " << r << std::endl;
+ return false;
+ }
+ return true;
+ }
+
+ bool WaitForSocketState(NrSocketBase* sock, int state) {
+ MOZ_ASSERT(sock);
+ SyncDispatchToSTS(WrapRunnable(
+ this, &TestNrSocketTest::WaitForSocketState_s, sock, state));
+
+ bool res;
+ WAIT_(wait_done_for_main_, 500, res);
+ wait_done_for_main_ = false;
+
+ if (!res) {
+ SyncDispatchToSTS(
+ WrapRunnable(this, &TestNrSocketTest::CancelWait_s, sock, state));
+ }
+
+ return res;
+ }
+
+ void WaitForSocketState_s(NrSocketBase* sock, int state) {
+ NR_ASYNC_WAIT(sock, state, &WaitDone, this);
+ }
+
+ void CancelWait_s(NrSocketBase* sock, int state) { sock->cancel(state); }
+
+ bool WaitForReadable(NrSocketBase* sock) {
+ return WaitForSocketState(sock, NR_ASYNC_WAIT_READ);
+ }
+
+ bool WaitForWriteable(NrSocketBase* sock) {
+ return WaitForSocketState(sock, NR_ASYNC_WAIT_WRITE);
+ }
+
+ void SyncDispatchToSTS(nsIRunnable* runnable) {
+ NS_DispatchAndSpinEventLoopUntilComplete(
+ "TestNrSocketTest::SyncDispatchToSTS"_ns, sts_, do_AddRef(runnable));
+ }
+
+ static void WaitDone(void* sock, int how, void* test_fixture) {
+ TestNrSocketTest* test = static_cast<TestNrSocketTest*>(test_fixture);
+ test->wait_done_for_main_ = true;
+ }
+
+ // Simple busywait boolean for the test cases to spin on.
+ Atomic<bool> wait_done_for_main_;
+
+ nsCOMPtr<nsIEventTarget> sts_;
+ std::vector<RefPtr<TestNrSocket>> public_addrs_;
+ std::vector<RefPtr<TestNrSocket>> private_addrs_;
+ std::vector<RefPtr<TestNat>> nats_;
+};
+
+} // namespace mozilla
+
+using mozilla::NrSocketBase;
+using mozilla::TestNat;
+using mozilla::TestNrSocketTest;
+
+TEST_F(TestNrSocketTest, UnsafePortRejectedUDP) {
+ nr_transport_addr address;
+ ASSERT_FALSE(nr_str_port_to_transport_addr("127.0.0.1",
+ // ssh
+ 22, IPPROTO_UDP, &address));
+ ASSERT_TRUE(NrSocketBase::IsForbiddenAddress(&address));
+}
+
+TEST_F(TestNrSocketTest, UnsafePortRejectedTCP) {
+ nr_transport_addr address;
+ ASSERT_FALSE(nr_str_port_to_transport_addr("127.0.0.1",
+ // ssh
+ 22, IPPROTO_TCP, &address));
+ ASSERT_TRUE(NrSocketBase::IsForbiddenAddress(&address));
+}
+
+TEST_F(TestNrSocketTest, SafePortAcceptedUDP) {
+ nr_transport_addr address;
+ ASSERT_FALSE(nr_str_port_to_transport_addr("127.0.0.1",
+ // stuns
+ 5349, IPPROTO_UDP, &address));
+ ASSERT_FALSE(NrSocketBase::IsForbiddenAddress(&address));
+}
+
+TEST_F(TestNrSocketTest, SafePortAcceptedTCP) {
+ nr_transport_addr address;
+ ASSERT_FALSE(nr_str_port_to_transport_addr("127.0.0.1",
+ // turns
+ 5349, IPPROTO_TCP, &address));
+ ASSERT_FALSE(NrSocketBase::IsForbiddenAddress(&address));
+}
+
+TEST_F(TestNrSocketTest, PublicConnectivity) {
+ CreatePublicAddrs(2);
+
+ ASSERT_TRUE(CheckConnectivity(public_addrs_[0], public_addrs_[1]));
+ ASSERT_TRUE(CheckConnectivity(public_addrs_[1], public_addrs_[0]));
+ ASSERT_TRUE(CheckConnectivity(public_addrs_[0], public_addrs_[0]));
+ ASSERT_TRUE(CheckConnectivity(public_addrs_[1], public_addrs_[1]));
+}
+
+TEST_F(TestNrSocketTest, PrivateConnectivity) {
+ RefPtr<TestNat> nat(CreatePrivateAddrs(2));
+ nat->filtering_type_ = TestNat::ENDPOINT_INDEPENDENT;
+ nat->mapping_type_ = TestNat::ENDPOINT_INDEPENDENT;
+
+ ASSERT_TRUE(CheckConnectivity(private_addrs_[0], private_addrs_[1]));
+ ASSERT_TRUE(CheckConnectivity(private_addrs_[1], private_addrs_[0]));
+ ASSERT_TRUE(CheckConnectivity(private_addrs_[0], private_addrs_[0]));
+ ASSERT_TRUE(CheckConnectivity(private_addrs_[1], private_addrs_[1]));
+}
+
+TEST_F(TestNrSocketTest, NoConnectivityWithoutPinhole) {
+ RefPtr<TestNat> nat(CreatePrivateAddrs(1));
+ nat->filtering_type_ = TestNat::ENDPOINT_INDEPENDENT;
+ nat->mapping_type_ = TestNat::ENDPOINT_INDEPENDENT;
+ CreatePublicAddrs(1);
+
+ ASSERT_FALSE(CheckConnectivity(public_addrs_[0], private_addrs_[0]));
+}
+
+TEST_F(TestNrSocketTest, NoConnectivityBetweenSubnets) {
+ RefPtr<TestNat> nat1(CreatePrivateAddrs(1));
+ nat1->filtering_type_ = TestNat::ENDPOINT_INDEPENDENT;
+ nat1->mapping_type_ = TestNat::ENDPOINT_INDEPENDENT;
+ RefPtr<TestNat> nat2(CreatePrivateAddrs(1));
+ nat2->filtering_type_ = TestNat::ENDPOINT_INDEPENDENT;
+ nat2->mapping_type_ = TestNat::ENDPOINT_INDEPENDENT;
+
+ ASSERT_FALSE(CheckConnectivity(private_addrs_[0], private_addrs_[1]));
+ ASSERT_FALSE(CheckConnectivity(private_addrs_[1], private_addrs_[0]));
+ ASSERT_TRUE(CheckConnectivity(private_addrs_[0], private_addrs_[0]));
+ ASSERT_TRUE(CheckConnectivity(private_addrs_[1], private_addrs_[1]));
+}
+
+TEST_F(TestNrSocketTest, FullConeAcceptIngress) {
+ RefPtr<TestNat> nat(CreatePrivateAddrs(1));
+ nat->filtering_type_ = TestNat::ENDPOINT_INDEPENDENT;
+ nat->mapping_type_ = TestNat::ENDPOINT_INDEPENDENT;
+ CreatePublicAddrs(2);
+
+ nr_transport_addr sender_external_address;
+ // Open pinhole to public IP 0
+ ASSERT_TRUE(CheckConnectivity(private_addrs_[0], public_addrs_[0],
+ &sender_external_address));
+
+ // Verify that return traffic works
+ ASSERT_TRUE(CheckConnectivityVia(public_addrs_[0], private_addrs_[0],
+ sender_external_address));
+
+ // Verify that other public IP can use the pinhole
+ ASSERT_TRUE(CheckConnectivityVia(public_addrs_[1], private_addrs_[0],
+ sender_external_address));
+}
+
+TEST_F(TestNrSocketTest, FullConeOnePinhole) {
+ RefPtr<TestNat> nat(CreatePrivateAddrs(1));
+ nat->filtering_type_ = TestNat::ENDPOINT_INDEPENDENT;
+ nat->mapping_type_ = TestNat::ENDPOINT_INDEPENDENT;
+ CreatePublicAddrs(2);
+
+ nr_transport_addr sender_external_address;
+ // Open pinhole to public IP 0
+ ASSERT_TRUE(CheckConnectivity(private_addrs_[0], public_addrs_[0],
+ &sender_external_address));
+
+ // Verify that return traffic works
+ ASSERT_TRUE(CheckConnectivityVia(public_addrs_[0], private_addrs_[0],
+ sender_external_address));
+
+ // Send traffic to other public IP, verify that it uses the same pinhole
+ nr_transport_addr sender_external_address2;
+ ASSERT_TRUE(CheckConnectivity(private_addrs_[0], public_addrs_[1],
+ &sender_external_address2));
+ ASSERT_FALSE(nr_transport_addr_cmp(&sender_external_address,
+ &sender_external_address2,
+ NR_TRANSPORT_ADDR_CMP_MODE_ALL))
+ << "addr1: " << sender_external_address.as_string
+ << " addr2: " << sender_external_address2.as_string;
+}
+
+// OS 10.6 doesn't seem to allow us to open ports on 127.0.0.2, and while linux
+// does allow this, it has other behavior (see below) that prevents this test
+// from working.
+TEST_F(TestNrSocketTest, DISABLED_AddressRestrictedCone) {
+ RefPtr<TestNat> nat(CreatePrivateAddrs(1));
+ nat->filtering_type_ = TestNat::ADDRESS_DEPENDENT;
+ nat->mapping_type_ = TestNat::ENDPOINT_INDEPENDENT;
+ CreatePublicAddrs(2, "127.0.0.1");
+ CreatePublicAddrs(1, "127.0.0.2");
+
+ nr_transport_addr sender_external_address;
+ // Open pinhole to public IP 0
+ ASSERT_TRUE(CheckConnectivity(private_addrs_[0], public_addrs_[0],
+ &sender_external_address));
+
+ // Verify that return traffic works
+ ASSERT_TRUE(CheckConnectivityVia(public_addrs_[0], private_addrs_[0],
+ sender_external_address));
+
+ // Verify that another address on the same host can use the pinhole
+ ASSERT_TRUE(CheckConnectivityVia(public_addrs_[1], private_addrs_[0],
+ sender_external_address));
+
+ // Linux has a tendency to monkey around with source addresses, doing
+ // stuff like substituting 127.0.0.1 for packets sent by 127.0.0.2, and even
+ // going as far as substituting localhost for a packet sent from a real IP
+ // address when the destination is localhost. The only way to make this test
+ // work on linux is to have two real IP addresses.
+#ifndef __linux__
+ // Verify that an address on a different host can't use the pinhole
+ ASSERT_FALSE(CheckConnectivityVia(public_addrs_[2], private_addrs_[0],
+ sender_external_address));
+#endif
+
+ // Send traffic to other public IP, verify that it uses the same pinhole
+ nr_transport_addr sender_external_address2;
+ ASSERT_TRUE(CheckConnectivity(private_addrs_[0], public_addrs_[1],
+ &sender_external_address2));
+ ASSERT_FALSE(nr_transport_addr_cmp(&sender_external_address,
+ &sender_external_address2,
+ NR_TRANSPORT_ADDR_CMP_MODE_ALL))
+ << "addr1: " << sender_external_address.as_string
+ << " addr2: " << sender_external_address2.as_string;
+
+ // Verify that the other public IP can now use the pinhole
+ ASSERT_TRUE(CheckConnectivityVia(public_addrs_[1], private_addrs_[0],
+ sender_external_address2));
+
+ // Send traffic to other public IP, verify that it uses the same pinhole
+ nr_transport_addr sender_external_address3;
+ ASSERT_TRUE(CheckConnectivity(private_addrs_[0], public_addrs_[2],
+ &sender_external_address3));
+ ASSERT_FALSE(nr_transport_addr_cmp(&sender_external_address,
+ &sender_external_address3,
+ NR_TRANSPORT_ADDR_CMP_MODE_ALL))
+ << "addr1: " << sender_external_address.as_string
+ << " addr2: " << sender_external_address3.as_string;
+
+ // Verify that the other public IP can now use the pinhole
+ ASSERT_TRUE(CheckConnectivityVia(public_addrs_[2], private_addrs_[0],
+ sender_external_address3));
+}
+
+TEST_F(TestNrSocketTest, RestrictedCone) {
+ RefPtr<TestNat> nat(CreatePrivateAddrs(1));
+ nat->filtering_type_ = TestNat::PORT_DEPENDENT;
+ nat->mapping_type_ = TestNat::ENDPOINT_INDEPENDENT;
+ CreatePublicAddrs(2);
+
+ nr_transport_addr sender_external_address;
+ // Open pinhole to public IP 0
+ ASSERT_TRUE(CheckConnectivity(private_addrs_[0], public_addrs_[0],
+ &sender_external_address));
+
+ // Verify that return traffic works
+ ASSERT_TRUE(CheckConnectivityVia(public_addrs_[0], private_addrs_[0],
+ sender_external_address));
+
+ // Verify that other public IP cannot use the pinhole
+ ASSERT_FALSE(CheckConnectivityVia(public_addrs_[1], private_addrs_[0],
+ sender_external_address));
+
+ // Send traffic to other public IP, verify that it uses the same pinhole
+ nr_transport_addr sender_external_address2;
+ ASSERT_TRUE(CheckConnectivity(private_addrs_[0], public_addrs_[1],
+ &sender_external_address2));
+ ASSERT_FALSE(nr_transport_addr_cmp(&sender_external_address,
+ &sender_external_address2,
+ NR_TRANSPORT_ADDR_CMP_MODE_ALL))
+ << "addr1: " << sender_external_address.as_string
+ << " addr2: " << sender_external_address2.as_string;
+
+ // Verify that the other public IP can now use the pinhole
+ ASSERT_TRUE(CheckConnectivityVia(public_addrs_[1], private_addrs_[0],
+ sender_external_address2));
+}
+
+TEST_F(TestNrSocketTest, PortDependentMappingFullCone) {
+ RefPtr<TestNat> nat(CreatePrivateAddrs(1));
+ nat->filtering_type_ = TestNat::ENDPOINT_INDEPENDENT;
+ nat->mapping_type_ = TestNat::PORT_DEPENDENT;
+ CreatePublicAddrs(2);
+
+ nr_transport_addr sender_external_address0;
+ // Open pinhole to public IP 0
+ ASSERT_TRUE(CheckConnectivity(private_addrs_[0], public_addrs_[0],
+ &sender_external_address0));
+
+ // Verify that return traffic works
+ ASSERT_TRUE(CheckConnectivityVia(public_addrs_[0], private_addrs_[0],
+ sender_external_address0));
+
+ // Verify that other public IP can use the pinhole
+ ASSERT_TRUE(CheckConnectivityVia(public_addrs_[1], private_addrs_[0],
+ sender_external_address0));
+
+ // Send traffic to other public IP, verify that it uses a different pinhole
+ nr_transport_addr sender_external_address1;
+ ASSERT_TRUE(CheckConnectivity(private_addrs_[0], public_addrs_[1],
+ &sender_external_address1));
+ ASSERT_TRUE(nr_transport_addr_cmp(&sender_external_address0,
+ &sender_external_address1,
+ NR_TRANSPORT_ADDR_CMP_MODE_ALL))
+ << "addr1: " << sender_external_address0.as_string
+ << " addr2: " << sender_external_address1.as_string;
+
+ // Verify that return traffic works
+ ASSERT_TRUE(CheckConnectivityVia(public_addrs_[1], private_addrs_[0],
+ sender_external_address1));
+
+ // Verify that other public IP can use the original pinhole
+ ASSERT_TRUE(CheckConnectivityVia(public_addrs_[0], private_addrs_[0],
+ sender_external_address1));
+}
+
+TEST_F(TestNrSocketTest, Symmetric) {
+ RefPtr<TestNat> nat(CreatePrivateAddrs(1));
+ nat->filtering_type_ = TestNat::PORT_DEPENDENT;
+ nat->mapping_type_ = TestNat::PORT_DEPENDENT;
+ CreatePublicAddrs(2);
+
+ nr_transport_addr sender_external_address;
+ // Open pinhole to public IP 0
+ ASSERT_TRUE(CheckConnectivity(private_addrs_[0], public_addrs_[0],
+ &sender_external_address));
+
+ // Verify that return traffic works
+ ASSERT_TRUE(CheckConnectivityVia(public_addrs_[0], private_addrs_[0],
+ sender_external_address));
+
+ // Verify that other public IP cannot use the pinhole
+ ASSERT_FALSE(CheckConnectivityVia(public_addrs_[1], private_addrs_[0],
+ sender_external_address));
+
+ // Send traffic to other public IP, verify that it uses a new pinhole
+ nr_transport_addr sender_external_address2;
+ ASSERT_TRUE(CheckConnectivity(private_addrs_[0], public_addrs_[1],
+ &sender_external_address2));
+ ASSERT_TRUE(nr_transport_addr_cmp(&sender_external_address,
+ &sender_external_address2,
+ NR_TRANSPORT_ADDR_CMP_MODE_ALL));
+
+ // Verify that the other public IP can use the new pinhole
+ ASSERT_TRUE(CheckConnectivityVia(public_addrs_[1], private_addrs_[0],
+ sender_external_address2));
+}
+
+TEST_F(TestNrSocketTest, BlockUdp) {
+ RefPtr<TestNat> nat(CreatePrivateAddrs(2));
+ nat->block_udp_ = true;
+ CreatePublicAddrs(1);
+
+ nr_transport_addr sender_external_address;
+ ASSERT_FALSE(CheckConnectivity(private_addrs_[0], public_addrs_[0],
+ &sender_external_address));
+
+ // Make sure UDP behind the NAT still works
+ ASSERT_TRUE(CheckConnectivity(private_addrs_[0], private_addrs_[1]));
+ ASSERT_TRUE(CheckConnectivity(private_addrs_[1], private_addrs_[0]));
+}
+
+TEST_F(TestNrSocketTest, DenyHairpinning) {
+ RefPtr<TestNat> nat(CreatePrivateAddrs(2));
+ nat->filtering_type_ = TestNat::ENDPOINT_INDEPENDENT;
+ nat->mapping_type_ = TestNat::ENDPOINT_INDEPENDENT;
+ CreatePublicAddrs(1);
+
+ nr_transport_addr sender_external_address;
+ // Open pinhole to public IP 0
+ ASSERT_TRUE(CheckConnectivity(private_addrs_[0], public_addrs_[0],
+ &sender_external_address));
+
+ // Verify that hairpinning is disallowed
+ ASSERT_FALSE(CheckConnectivityVia(private_addrs_[1], private_addrs_[0],
+ sender_external_address));
+}
+
+TEST_F(TestNrSocketTest, AllowHairpinning) {
+ RefPtr<TestNat> nat(CreatePrivateAddrs(2));
+ nat->filtering_type_ = TestNat::ENDPOINT_INDEPENDENT;
+ nat->mapping_type_ = TestNat::ENDPOINT_INDEPENDENT;
+ nat->mapping_timeout_ = 30000;
+ nat->allow_hairpinning_ = true;
+ CreatePublicAddrs(1);
+
+ nr_transport_addr sender_external_address;
+ // Open pinhole to public IP 0, obtain external address
+ ASSERT_TRUE(CheckConnectivity(private_addrs_[0], public_addrs_[0],
+ &sender_external_address));
+
+ // Verify that hairpinning is allowed
+ ASSERT_TRUE(CheckConnectivityVia(private_addrs_[1], private_addrs_[0],
+ sender_external_address));
+}
+
+TEST_F(TestNrSocketTest, FullConeTimeout) {
+ RefPtr<TestNat> nat(CreatePrivateAddrs(1));
+ nat->filtering_type_ = TestNat::ENDPOINT_INDEPENDENT;
+ nat->mapping_type_ = TestNat::ENDPOINT_INDEPENDENT;
+ nat->mapping_timeout_ = 200;
+ CreatePublicAddrs(2);
+
+ nr_transport_addr sender_external_address;
+ // Open pinhole to public IP 0
+ ASSERT_TRUE(CheckConnectivity(private_addrs_[0], public_addrs_[0],
+ &sender_external_address));
+
+ // Verify that return traffic works
+ ASSERT_TRUE(CheckConnectivityVia(public_addrs_[0], private_addrs_[0],
+ sender_external_address));
+
+ PR_Sleep(201);
+
+ // Verify that return traffic does not work
+ ASSERT_FALSE(CheckConnectivityVia(public_addrs_[0], private_addrs_[0],
+ sender_external_address));
+}
+
+TEST_F(TestNrSocketTest, PublicConnectivityTcp) {
+ CreatePublicAddrs(2, "127.0.0.1", IPPROTO_TCP);
+
+ ASSERT_TRUE(CheckTcpConnectivity(public_addrs_[0], public_addrs_[1]));
+}
+
+TEST_F(TestNrSocketTest, PrivateConnectivityTcp) {
+ RefPtr<TestNat> nat(CreatePrivateAddrs(2, "127.0.0.1", IPPROTO_TCP));
+ nat->filtering_type_ = TestNat::ENDPOINT_INDEPENDENT;
+ nat->mapping_type_ = TestNat::ENDPOINT_INDEPENDENT;
+
+ ASSERT_TRUE(CheckTcpConnectivity(private_addrs_[0], private_addrs_[1]));
+}
+
+TEST_F(TestNrSocketTest, PrivateToPublicConnectivityTcp) {
+ RefPtr<TestNat> nat(CreatePrivateAddrs(1, "127.0.0.1", IPPROTO_TCP));
+ nat->filtering_type_ = TestNat::ENDPOINT_INDEPENDENT;
+ nat->mapping_type_ = TestNat::ENDPOINT_INDEPENDENT;
+ CreatePublicAddrs(1, "127.0.0.1", IPPROTO_TCP);
+
+ ASSERT_TRUE(CheckTcpConnectivity(private_addrs_[0], public_addrs_[0]));
+}
+
+TEST_F(TestNrSocketTest, NoConnectivityBetweenSubnetsTcp) {
+ RefPtr<TestNat> nat1(CreatePrivateAddrs(1, "127.0.0.1", IPPROTO_TCP));
+ nat1->filtering_type_ = TestNat::ENDPOINT_INDEPENDENT;
+ nat1->mapping_type_ = TestNat::ENDPOINT_INDEPENDENT;
+ RefPtr<TestNat> nat2(CreatePrivateAddrs(1, "127.0.0.1", IPPROTO_TCP));
+ nat2->filtering_type_ = TestNat::ENDPOINT_INDEPENDENT;
+ nat2->mapping_type_ = TestNat::ENDPOINT_INDEPENDENT;
+
+ ASSERT_FALSE(CheckTcpConnectivity(private_addrs_[0], private_addrs_[1]));
+}
+
+TEST_F(TestNrSocketTest, NoConnectivityPublicToPrivateTcp) {
+ RefPtr<TestNat> nat(CreatePrivateAddrs(1, "127.0.0.1", IPPROTO_TCP));
+ nat->filtering_type_ = TestNat::ENDPOINT_INDEPENDENT;
+ nat->mapping_type_ = TestNat::ENDPOINT_INDEPENDENT;
+ CreatePublicAddrs(1, "127.0.0.1", IPPROTO_TCP);
+
+ ASSERT_FALSE(CheckTcpConnectivity(public_addrs_[0], private_addrs_[0]));
+}
diff --git a/dom/media/webrtc/transport/test/transport_unittests.cpp b/dom/media/webrtc/transport/test/transport_unittests.cpp
new file mode 100644
index 0000000000..28f9359afb
--- /dev/null
+++ b/dom/media/webrtc/transport/test/transport_unittests.cpp
@@ -0,0 +1,1400 @@
+
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+#include <iostream>
+#include <string>
+#include <algorithm>
+#include <functional>
+
+#ifdef XP_MACOSX
+// ensure that Apple Security kit enum goes before "sslproto.h"
+# include <CoreFoundation/CFAvailability.h>
+# include <Security/CipherSuite.h>
+#endif
+
+#include "mozilla/UniquePtr.h"
+
+#include "sigslot.h"
+
+#include "logging.h"
+#include "ssl.h"
+#include "sslexp.h"
+#include "sslproto.h"
+
+#include "nsThreadUtils.h"
+
+#include "mediapacket.h"
+#include "dtlsidentity.h"
+#include "nricectx.h"
+#include "nricemediastream.h"
+#include "transportflow.h"
+#include "transportlayer.h"
+#include "transportlayerdtls.h"
+#include "transportlayerice.h"
+#include "transportlayerlog.h"
+#include "transportlayerloopback.h"
+
+#include "runnable_utils.h"
+
+#define GTEST_HAS_RTTI 0
+#include "gtest/gtest.h"
+#include "gtest_utils.h"
+
+using namespace mozilla;
+MOZ_MTLOG_MODULE("mtransport")
+
+const uint8_t kTlsChangeCipherSpecType = 0x14;
+const uint8_t kTlsHandshakeType = 0x16;
+
+const uint8_t kTlsHandshakeCertificate = 0x0b;
+const uint8_t kTlsHandshakeServerKeyExchange = 0x0c;
+
+const uint8_t kTlsFakeChangeCipherSpec[] = {
+ kTlsChangeCipherSpecType, // Type
+ 0xfe,
+ 0xff, // Version
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x10, // Fictitious sequence #
+ 0x00,
+ 0x01, // Length
+ 0x01 // Value
+};
+
+// Layer class which can't be initialized.
+class TransportLayerDummy : public TransportLayer {
+ public:
+ TransportLayerDummy(bool allow_init, bool* destroyed)
+ : allow_init_(allow_init), destroyed_(destroyed) {
+ *destroyed_ = false;
+ }
+
+ virtual ~TransportLayerDummy() { *destroyed_ = true; }
+
+ nsresult InitInternal() override {
+ return allow_init_ ? NS_OK : NS_ERROR_FAILURE;
+ }
+
+ TransportResult SendPacket(MediaPacket& packet) override {
+ MOZ_CRASH(); // Should never be called.
+ return 0;
+ }
+
+ TRANSPORT_LAYER_ID("lossy")
+
+ private:
+ bool allow_init_;
+ bool* destroyed_;
+};
+
+class Inspector {
+ public:
+ virtual ~Inspector() = default;
+
+ virtual void Inspect(TransportLayer* layer, const unsigned char* data,
+ size_t len) = 0;
+};
+
+// Class to simulate various kinds of network lossage
+class TransportLayerLossy : public TransportLayer {
+ public:
+ TransportLayerLossy() : loss_mask_(0), packet_(0), inspector_(nullptr) {}
+ ~TransportLayerLossy() = default;
+
+ TransportResult SendPacket(MediaPacket& packet) override {
+ MOZ_MTLOG(ML_NOTICE, LAYER_INFO << "SendPacket(" << packet.len() << ")");
+
+ if (loss_mask_ & (1 << (packet_ % 32))) {
+ MOZ_MTLOG(ML_NOTICE, "Dropping packet");
+ ++packet_;
+ return packet.len();
+ }
+ if (inspector_) {
+ inspector_->Inspect(this, packet.data(), packet.len());
+ }
+
+ ++packet_;
+
+ return downward_->SendPacket(packet);
+ }
+
+ void SetLoss(uint32_t packet) { loss_mask_ |= (1 << (packet & 32)); }
+
+ void SetInspector(UniquePtr<Inspector> inspector) {
+ inspector_ = std::move(inspector);
+ }
+
+ void StateChange(TransportLayer* layer, State state) { TL_SET_STATE(state); }
+
+ void PacketReceived(TransportLayer* layer, MediaPacket& packet) {
+ SignalPacketReceived(this, packet);
+ }
+
+ TRANSPORT_LAYER_ID("lossy")
+
+ protected:
+ void WasInserted() override {
+ downward_->SignalPacketReceived.connect(
+ this, &TransportLayerLossy::PacketReceived);
+ downward_->SignalStateChange.connect(this,
+ &TransportLayerLossy::StateChange);
+
+ TL_SET_STATE(downward_->state());
+ }
+
+ private:
+ uint32_t loss_mask_;
+ uint32_t packet_;
+ UniquePtr<Inspector> inspector_;
+};
+
+// Process DTLS Records
+#define CHECK_LENGTH(expected) \
+ do { \
+ EXPECT_GE(remaining(), expected); \
+ if (remaining() < expected) return false; \
+ } while (0)
+
+class TlsParser {
+ public:
+ TlsParser(const unsigned char* data, size_t len) : buffer_(), offset_(0) {
+ buffer_.Copy(data, len);
+ }
+
+ bool Read(unsigned char* val) {
+ if (remaining() < 1) {
+ return false;
+ }
+ *val = *ptr();
+ consume(1);
+ return true;
+ }
+
+ // Read an integral type of specified width.
+ bool Read(uint32_t* val, size_t len) {
+ if (len > sizeof(uint32_t)) return false;
+
+ *val = 0;
+
+ for (size_t i = 0; i < len; ++i) {
+ unsigned char tmp;
+
+ if (!Read(&tmp)) return false;
+
+ (*val) = ((*val) << 8) + tmp;
+ }
+
+ return true;
+ }
+
+ bool Read(unsigned char* val, size_t len) {
+ if (remaining() < len) {
+ return false;
+ }
+
+ if (val) {
+ memcpy(val, ptr(), len);
+ }
+ consume(len);
+
+ return true;
+ }
+
+ private:
+ size_t remaining() const { return buffer_.len() - offset_; }
+ const uint8_t* ptr() const { return buffer_.data() + offset_; }
+ void consume(size_t len) { offset_ += len; }
+
+ MediaPacket buffer_;
+ size_t offset_;
+};
+
+class DtlsRecordParser {
+ public:
+ DtlsRecordParser(const unsigned char* data, size_t len)
+ : buffer_(), offset_(0) {
+ buffer_.Copy(data, len);
+ }
+
+ bool NextRecord(uint8_t* ct, UniquePtr<MediaPacket>* buffer) {
+ if (!remaining()) return false;
+
+ CHECK_LENGTH(13U);
+ const uint8_t* ctp = reinterpret_cast<const uint8_t*>(ptr());
+ consume(11); // ct + version + length
+
+ const uint16_t* tmp = reinterpret_cast<const uint16_t*>(ptr());
+ size_t length = ntohs(*tmp);
+ consume(2);
+
+ CHECK_LENGTH(length);
+ auto db = MakeUnique<MediaPacket>();
+ db->Copy(ptr(), length);
+ consume(length);
+
+ *ct = *ctp;
+ *buffer = std::move(db);
+
+ return true;
+ }
+
+ private:
+ size_t remaining() const { return buffer_.len() - offset_; }
+ const uint8_t* ptr() const { return buffer_.data() + offset_; }
+ void consume(size_t len) { offset_ += len; }
+
+ MediaPacket buffer_;
+ size_t offset_;
+};
+
+// Inspector that parses out DTLS records and passes
+// them on.
+class DtlsRecordInspector : public Inspector {
+ public:
+ virtual void Inspect(TransportLayer* layer, const unsigned char* data,
+ size_t len) {
+ DtlsRecordParser parser(data, len);
+
+ uint8_t ct;
+ UniquePtr<MediaPacket> buf;
+ while (parser.NextRecord(&ct, &buf)) {
+ OnRecord(layer, ct, buf->data(), buf->len());
+ }
+ }
+
+ virtual void OnRecord(TransportLayer* layer, uint8_t content_type,
+ const unsigned char* record, size_t len) = 0;
+};
+
+// Inspector that injects arbitrary packets based on
+// DTLS records of various types.
+class DtlsInspectorInjector : public DtlsRecordInspector {
+ public:
+ DtlsInspectorInjector(uint8_t packet_type, uint8_t handshake_type,
+ const unsigned char* data, size_t len)
+ : packet_type_(packet_type), handshake_type_(handshake_type) {
+ packet_.Copy(data, len);
+ }
+
+ virtual void OnRecord(TransportLayer* layer, uint8_t content_type,
+ const unsigned char* data, size_t len) {
+ // Only inject once.
+ if (!packet_.data()) {
+ return;
+ }
+
+ // Check that the first byte is as requested.
+ if (content_type != packet_type_) {
+ return;
+ }
+
+ if (handshake_type_ != 0xff) {
+ // Check that the packet is plausibly long enough.
+ if (len < 1) {
+ return;
+ }
+
+ // Check that the handshake type is as requested.
+ if (data[0] != handshake_type_) {
+ return;
+ }
+ }
+
+ layer->SendPacket(packet_);
+ packet_.Reset();
+ }
+
+ private:
+ uint8_t packet_type_;
+ uint8_t handshake_type_;
+ MediaPacket packet_;
+};
+
+// Make a copy of the first instance of a message.
+class DtlsInspectorRecordHandshakeMessage : public DtlsRecordInspector {
+ public:
+ explicit DtlsInspectorRecordHandshakeMessage(uint8_t handshake_type)
+ : handshake_type_(handshake_type), buffer_() {}
+
+ virtual void OnRecord(TransportLayer* layer, uint8_t content_type,
+ const unsigned char* data, size_t len) {
+ // Only do this once.
+ if (buffer_.len()) {
+ return;
+ }
+
+ // Check that the first byte is as requested.
+ if (content_type != kTlsHandshakeType) {
+ return;
+ }
+
+ TlsParser parser(data, len);
+ unsigned char message_type;
+ // Read the handshake message type.
+ if (!parser.Read(&message_type)) {
+ return;
+ }
+ if (message_type != handshake_type_) {
+ return;
+ }
+
+ uint32_t length;
+ if (!parser.Read(&length, 3)) {
+ return;
+ }
+
+ uint32_t message_seq;
+ if (!parser.Read(&message_seq, 2)) {
+ return;
+ }
+
+ uint32_t fragment_offset;
+ if (!parser.Read(&fragment_offset, 3)) {
+ return;
+ }
+
+ uint32_t fragment_length;
+ if (!parser.Read(&fragment_length, 3)) {
+ return;
+ }
+
+ if ((fragment_offset != 0) || (fragment_length != length)) {
+ // This shouldn't happen because all current tests where we
+ // are using this code don't fragment.
+ return;
+ }
+
+ UniquePtr<uint8_t[]> buffer(new uint8_t[length]);
+ if (!parser.Read(buffer.get(), length)) {
+ return;
+ }
+ buffer_.Take(std::move(buffer), length);
+ }
+
+ const MediaPacket& buffer() { return buffer_; }
+
+ private:
+ uint8_t handshake_type_;
+ MediaPacket buffer_;
+};
+
+class TlsServerKeyExchangeECDHE {
+ public:
+ bool Parse(const unsigned char* data, size_t len) {
+ TlsParser parser(data, len);
+
+ uint8_t curve_type;
+ if (!parser.Read(&curve_type)) {
+ return false;
+ }
+
+ if (curve_type != 3) { // named_curve
+ return false;
+ }
+
+ uint32_t named_curve;
+ if (!parser.Read(&named_curve, 2)) {
+ return false;
+ }
+
+ uint32_t point_length;
+ if (!parser.Read(&point_length, 1)) {
+ return false;
+ }
+
+ UniquePtr<uint8_t[]> key(new uint8_t[point_length]);
+ if (!parser.Read(key.get(), point_length)) {
+ return false;
+ }
+ public_key_.Take(std::move(key), point_length);
+
+ return true;
+ }
+
+ MediaPacket public_key_;
+};
+
+namespace {
+class TransportTestPeer : public sigslot::has_slots<> {
+ public:
+ TransportTestPeer(nsCOMPtr<nsIEventTarget> target, std::string name,
+ MtransportTestUtils* utils)
+ : name_(name),
+ offerer_(name == "P1"),
+ target_(target),
+ received_packets_(0),
+ received_bytes_(0),
+ flow_(new TransportFlow(name)),
+ loopback_(new TransportLayerLoopback()),
+ logging_(new TransportLayerLogging()),
+ lossy_(new TransportLayerLossy()),
+ dtls_(new TransportLayerDtls()),
+ identity_(DtlsIdentity::Generate()),
+ ice_ctx_(),
+ streams_(),
+ peer_(nullptr),
+ gathering_complete_(false),
+ digest_("sha-1"),
+ enabled_cipersuites_(),
+ disabled_cipersuites_(),
+ test_utils_(utils) {
+ NrIceCtx::InitializeGlobals(NrIceCtx::GlobalConfig());
+ ice_ctx_ = NrIceCtx::Create(name);
+ std::vector<NrIceStunServer> stun_servers;
+ UniquePtr<NrIceStunServer> server(NrIceStunServer::Create(
+ std::string((char*)"stun.services.mozilla.com"), 3478));
+ stun_servers.push_back(*server);
+ EXPECT_TRUE(NS_SUCCEEDED(ice_ctx_->SetStunServers(stun_servers)));
+
+ dtls_->SetIdentity(identity_);
+ dtls_->SetRole(offerer_ ? TransportLayerDtls::SERVER
+ : TransportLayerDtls::CLIENT);
+
+ nsresult res = identity_->ComputeFingerprint(&digest_);
+ EXPECT_TRUE(NS_SUCCEEDED(res));
+ EXPECT_EQ(20u, digest_.value_.size());
+ }
+
+ ~TransportTestPeer() {
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(this, &TransportTestPeer::DestroyFlow));
+ }
+
+ void DestroyFlow() {
+ disconnect_all();
+ if (flow_) {
+ loopback_->Disconnect();
+ flow_ = nullptr;
+ }
+ ice_ctx_->Destroy();
+ ice_ctx_ = nullptr;
+ streams_.clear();
+ }
+
+ void DisconnectDestroyFlow() {
+ test_utils_->SyncDispatchToSTS(NS_NewRunnableFunction(__func__, [this] {
+ loopback_->Disconnect();
+ disconnect_all(); // Disconnect from the signals;
+ flow_ = nullptr;
+ }));
+ }
+
+ void SetDtlsAllowAll() {
+ nsresult res = dtls_->SetVerificationAllowAll();
+ ASSERT_TRUE(NS_SUCCEEDED(res));
+ }
+
+ void SetAlpn(std::string str, bool withDefault, std::string extra = "") {
+ std::set<std::string> alpn;
+ alpn.insert(str); // the one we want to select
+ if (!extra.empty()) {
+ alpn.insert(extra);
+ }
+ nsresult res = dtls_->SetAlpn(alpn, withDefault ? str : "");
+ ASSERT_EQ(NS_OK, res);
+ }
+
+ const std::string& GetAlpn() const { return dtls_->GetNegotiatedAlpn(); }
+
+ void SetDtlsPeer(TransportTestPeer* peer, int digests, unsigned int damage) {
+ unsigned int mask = 1;
+
+ for (int i = 0; i < digests; i++) {
+ DtlsDigest digest_to_set(peer->digest_);
+
+ if (damage & mask) digest_to_set.value_.data()[0]++;
+
+ nsresult res = dtls_->SetVerificationDigest(digest_to_set);
+
+ ASSERT_TRUE(NS_SUCCEEDED(res));
+
+ mask <<= 1;
+ }
+ }
+
+ void SetupSrtp() {
+ std::vector<uint16_t> srtp_ciphers =
+ TransportLayerDtls::GetDefaultSrtpCiphers();
+ SetSrtpCiphers(srtp_ciphers);
+ }
+
+ void SetSrtpCiphers(std::vector<uint16_t>& srtp_ciphers) {
+ ASSERT_TRUE(NS_SUCCEEDED(dtls_->SetSrtpCiphers(srtp_ciphers)));
+ }
+
+ void ConnectSocket_s(TransportTestPeer* peer) {
+ nsresult res;
+ res = loopback_->Init();
+ ASSERT_EQ((nsresult)NS_OK, res);
+
+ loopback_->Connect(peer->loopback_);
+ ASSERT_EQ((nsresult)NS_OK, loopback_->Init());
+ ASSERT_EQ((nsresult)NS_OK, logging_->Init());
+ ASSERT_EQ((nsresult)NS_OK, lossy_->Init());
+ ASSERT_EQ((nsresult)NS_OK, dtls_->Init());
+ dtls_->Chain(lossy_);
+ lossy_->Chain(logging_);
+ logging_->Chain(loopback_);
+
+ flow_->PushLayer(loopback_);
+ flow_->PushLayer(logging_);
+ flow_->PushLayer(lossy_);
+ flow_->PushLayer(dtls_);
+
+ if (dtls_->state() != TransportLayer::TS_ERROR) {
+ // Don't execute these blocks if DTLS didn't initialize.
+ TweakCiphers(dtls_->internal_fd());
+ if (post_setup_) {
+ post_setup_(dtls_->internal_fd());
+ }
+ }
+
+ dtls_->SignalPacketReceived.connect(this,
+ &TransportTestPeer::PacketReceived);
+ }
+
+ void TweakCiphers(PRFileDesc* fd) {
+ for (unsigned short& enabled_cipersuite : enabled_cipersuites_) {
+ SSL_CipherPrefSet(fd, enabled_cipersuite, PR_TRUE);
+ }
+ for (unsigned short& disabled_cipersuite : disabled_cipersuites_) {
+ SSL_CipherPrefSet(fd, disabled_cipersuite, PR_FALSE);
+ }
+ }
+
+ void ConnectSocket(TransportTestPeer* peer) {
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(this, &TransportTestPeer::ConnectSocket_s, peer));
+ }
+
+ nsresult InitIce_s() {
+ nsresult rv = ice_->Init();
+ NS_ENSURE_SUCCESS(rv, rv);
+ rv = dtls_->Init();
+ NS_ENSURE_SUCCESS(rv, rv);
+ dtls_->Chain(ice_);
+ flow_->PushLayer(ice_);
+ flow_->PushLayer(dtls_);
+ return NS_OK;
+ }
+
+ void InitIce() {
+ nsresult res;
+
+ // Attach our slots
+ ice_ctx_->SignalGatheringStateChange.connect(
+ this, &TransportTestPeer::GatheringStateChange);
+
+ char name[100];
+ snprintf(name, sizeof(name), "%s:stream%d", name_.c_str(),
+ (int)streams_.size());
+
+ // Create the media stream
+ RefPtr<NrIceMediaStream> stream = ice_ctx_->CreateStream(name, name, 1);
+
+ ASSERT_TRUE(stream != nullptr);
+ stream->SetIceCredentials("ufrag", "pass");
+ streams_.push_back(stream);
+
+ // Listen for candidates
+ stream->SignalCandidate.connect(this, &TransportTestPeer::GotCandidate);
+
+ // Create the transport layer
+ ice_ = new TransportLayerIce();
+ ice_->SetParameters(stream, 1);
+
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnableRet(&res, this, &TransportTestPeer::InitIce_s));
+
+ ASSERT_EQ((nsresult)NS_OK, res);
+
+ // Listen for media events
+ dtls_->SignalPacketReceived.connect(this,
+ &TransportTestPeer::PacketReceived);
+ dtls_->SignalStateChange.connect(this, &TransportTestPeer::StateChanged);
+
+ // Start gathering
+ test_utils_->SyncDispatchToSTS(WrapRunnableRet(
+ &res, ice_ctx_, &NrIceCtx::StartGathering, false, false));
+ ASSERT_TRUE(NS_SUCCEEDED(res));
+ }
+
+ void ConnectIce(TransportTestPeer* peer) {
+ peer_ = peer;
+
+ // If gathering is already complete, push the candidates over
+ if (gathering_complete_) GatheringComplete();
+ }
+
+ // New candidate
+ void GotCandidate(NrIceMediaStream* stream, const std::string& candidate,
+ const std::string& ufrag, const std::string& mdns_addr,
+ const std::string& actual_addr) {
+ std::cerr << "Got candidate " << candidate << " (ufrag=" << ufrag << ")"
+ << std::endl;
+ }
+
+ void GatheringStateChange(NrIceCtx* ctx, NrIceCtx::GatheringState state) {
+ (void)ctx;
+ if (state == NrIceCtx::ICE_CTX_GATHER_COMPLETE) {
+ GatheringComplete();
+ }
+ }
+
+ // Gathering complete, so send our candidates and start
+ // connecting on the other peer.
+ void GatheringComplete() {
+ nsresult res;
+
+ // Don't send to the other side
+ if (!peer_) {
+ gathering_complete_ = true;
+ return;
+ }
+
+ // First send attributes
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnableRet(&res, peer_->ice_ctx_, &NrIceCtx::ParseGlobalAttributes,
+ ice_ctx_->GetGlobalAttributes()));
+ ASSERT_TRUE(NS_SUCCEEDED(res));
+
+ for (size_t i = 0; i < streams_.size(); ++i) {
+ test_utils_->SyncDispatchToSTS(WrapRunnableRet(
+ &res, peer_->streams_[i], &NrIceMediaStream::ConnectToPeer, "ufrag",
+ "pass", streams_[i]->GetAttributes()));
+
+ ASSERT_TRUE(NS_SUCCEEDED(res));
+ }
+
+ // Start checks on the other peer.
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnableRet(&res, peer_->ice_ctx_, &NrIceCtx::StartChecks));
+ ASSERT_TRUE(NS_SUCCEEDED(res));
+ }
+
+ // WrapRunnable/lambda and move semantics (MediaPacket is not copyable) don't
+ // get along yet, so we need a wrapper. Gross.
+ static TransportResult SendPacketWrapper(TransportLayer* layer,
+ MediaPacket* packet) {
+ return layer->SendPacket(*packet);
+ }
+
+ TransportResult SendPacket(MediaPacket& packet) {
+ TransportResult ret;
+
+ test_utils_->SyncDispatchToSTS(WrapRunnableNMRet(
+ &ret, &TransportTestPeer::SendPacketWrapper, dtls_, &packet));
+
+ return ret;
+ }
+
+ void StateChanged(TransportLayer* layer, TransportLayer::State state) {
+ if (state == TransportLayer::TS_OPEN) {
+ std::cerr << "Now connected" << std::endl;
+ }
+ }
+
+ void PacketReceived(TransportLayer* layer, MediaPacket& packet) {
+ std::cerr << "Received " << packet.len() << " bytes" << std::endl;
+ ++received_packets_;
+ received_bytes_ += packet.len();
+ }
+
+ void SetLoss(uint32_t loss) { lossy_->SetLoss(loss); }
+
+ void SetCombinePackets(bool combine) { loopback_->CombinePackets(combine); }
+
+ void SetInspector(UniquePtr<Inspector> inspector) {
+ lossy_->SetInspector(std::move(inspector));
+ }
+
+ void SetInspector(Inspector* in) {
+ UniquePtr<Inspector> inspector(in);
+
+ lossy_->SetInspector(std::move(inspector));
+ }
+
+ void SetCipherSuiteChanges(const std::vector<uint16_t>& enableThese,
+ const std::vector<uint16_t>& disableThese) {
+ disabled_cipersuites_ = disableThese;
+ enabled_cipersuites_ = enableThese;
+ }
+
+ void SetPostSetup(const std::function<void(PRFileDesc*)>& setup) {
+ post_setup_ = std::move(setup);
+ }
+
+ TransportLayer::State state() {
+ TransportLayer::State tstate;
+
+ RUN_ON_THREAD(test_utils_->sts_target(),
+ WrapRunnableRet(&tstate, dtls_, &TransportLayer::state));
+
+ return tstate;
+ }
+
+ bool connected() { return state() == TransportLayer::TS_OPEN; }
+
+ bool failed() { return state() == TransportLayer::TS_ERROR; }
+
+ size_t receivedPackets() { return received_packets_; }
+
+ size_t receivedBytes() { return received_bytes_; }
+
+ uint16_t cipherSuite() const {
+ nsresult rv;
+ uint16_t cipher;
+ RUN_ON_THREAD(
+ test_utils_->sts_target(),
+ WrapRunnableRet(&rv, dtls_, &TransportLayerDtls::GetCipherSuite,
+ &cipher));
+
+ if (NS_FAILED(rv)) {
+ return TLS_NULL_WITH_NULL_NULL; // i.e., not good
+ }
+ return cipher;
+ }
+
+ uint16_t srtpCipher() const {
+ nsresult rv;
+ uint16_t cipher;
+ RUN_ON_THREAD(test_utils_->sts_target(),
+ WrapRunnableRet(&rv, dtls_,
+ &TransportLayerDtls::GetSrtpCipher, &cipher));
+ if (NS_FAILED(rv)) {
+ return 0; // the SRTP equivalent of TLS_NULL_WITH_NULL_NULL
+ }
+ return cipher;
+ }
+
+ private:
+ std::string name_;
+ bool offerer_;
+ nsCOMPtr<nsIEventTarget> target_;
+ size_t received_packets_;
+ size_t received_bytes_;
+ RefPtr<TransportFlow> flow_;
+ TransportLayerLoopback* loopback_;
+ TransportLayerLogging* logging_;
+ TransportLayerLossy* lossy_;
+ TransportLayerDtls* dtls_;
+ TransportLayerIce* ice_;
+ RefPtr<DtlsIdentity> identity_;
+ RefPtr<NrIceCtx> ice_ctx_;
+ std::vector<RefPtr<NrIceMediaStream> > streams_;
+ TransportTestPeer* peer_;
+ bool gathering_complete_;
+ DtlsDigest digest_;
+ std::vector<uint16_t> enabled_cipersuites_;
+ std::vector<uint16_t> disabled_cipersuites_;
+ MtransportTestUtils* test_utils_;
+ std::function<void(PRFileDesc* fd)> post_setup_ = nullptr;
+};
+
+class TransportTest : public MtransportTest {
+ public:
+ TransportTest() {
+ fds_[0] = nullptr;
+ fds_[1] = nullptr;
+ p1_ = nullptr;
+ p2_ = nullptr;
+ }
+
+ void TearDown() override {
+ delete p1_;
+ delete p2_;
+
+ // Can't detach these
+ // PR_Close(fds_[0]);
+ // PR_Close(fds_[1]);
+ MtransportTest::TearDown();
+ }
+
+ void DestroyPeerFlows() {
+ p1_->DisconnectDestroyFlow();
+ p2_->DisconnectDestroyFlow();
+ }
+
+ void SetUp() override {
+ MtransportTest::SetUp();
+
+ nsresult rv;
+ target_ = do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+ Reset();
+ }
+
+ void Reset() {
+ if (p1_) {
+ delete p1_;
+ }
+ if (p2_) {
+ delete p2_;
+ }
+ p1_ = new TransportTestPeer(target_, "P1", test_utils_);
+ p2_ = new TransportTestPeer(target_, "P2", test_utils_);
+ }
+
+ void SetupSrtp() {
+ p1_->SetupSrtp();
+ p2_->SetupSrtp();
+ }
+
+ void SetDtlsPeer(int digests = 1, unsigned int damage = 0) {
+ p1_->SetDtlsPeer(p2_, digests, damage);
+ p2_->SetDtlsPeer(p1_, digests, damage);
+ }
+
+ void SetDtlsAllowAll() {
+ p1_->SetDtlsAllowAll();
+ p2_->SetDtlsAllowAll();
+ }
+
+ void SetAlpn(std::string first, std::string second,
+ bool withDefaults = true) {
+ if (!first.empty()) {
+ p1_->SetAlpn(first, withDefaults, "bogus");
+ }
+ if (!second.empty()) {
+ p2_->SetAlpn(second, withDefaults);
+ }
+ }
+
+ void CheckAlpn(std::string first, std::string second) {
+ ASSERT_EQ(first, p1_->GetAlpn());
+ ASSERT_EQ(second, p2_->GetAlpn());
+ }
+
+ void ConnectSocket() {
+ ConnectSocketInternal();
+ ASSERT_TRUE_WAIT(p1_->connected(), 10000);
+ ASSERT_TRUE_WAIT(p2_->connected(), 10000);
+
+ ASSERT_EQ(p1_->cipherSuite(), p2_->cipherSuite());
+ ASSERT_EQ(p1_->srtpCipher(), p2_->srtpCipher());
+ }
+
+ void ConnectSocketExpectFail() {
+ ConnectSocketInternal();
+ ASSERT_TRUE_WAIT(p1_->failed(), 10000);
+ ASSERT_TRUE_WAIT(p2_->failed(), 10000);
+ }
+
+ void ConnectSocketExpectState(TransportLayer::State s1,
+ TransportLayer::State s2) {
+ ConnectSocketInternal();
+ ASSERT_EQ_WAIT(s1, p1_->state(), 10000);
+ ASSERT_EQ_WAIT(s2, p2_->state(), 10000);
+ }
+
+ void ConnectIce() {
+ p1_->InitIce();
+ p2_->InitIce();
+ p1_->ConnectIce(p2_);
+ p2_->ConnectIce(p1_);
+ ASSERT_TRUE_WAIT(p1_->connected(), 10000);
+ ASSERT_TRUE_WAIT(p2_->connected(), 10000);
+ }
+
+ void TransferTest(size_t count, size_t bytes = 1024) {
+ unsigned char buf[bytes];
+
+ for (size_t i = 0; i < count; ++i) {
+ memset(buf, count & 0xff, sizeof(buf));
+ MediaPacket packet;
+ packet.Copy(buf, sizeof(buf));
+ TransportResult rv = p1_->SendPacket(packet);
+ ASSERT_TRUE(rv > 0);
+ }
+
+ std::cerr << "Received == " << p2_->receivedPackets() << " packets"
+ << std::endl;
+ ASSERT_TRUE_WAIT(count == p2_->receivedPackets(), 10000);
+ ASSERT_TRUE((count * sizeof(buf)) == p2_->receivedBytes());
+ }
+
+ protected:
+ void ConnectSocketInternal() {
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(p1_, &TransportTestPeer::ConnectSocket, p2_));
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(p2_, &TransportTestPeer::ConnectSocket, p1_));
+ }
+
+ PRFileDesc* fds_[2];
+ TransportTestPeer* p1_;
+ TransportTestPeer* p2_;
+ nsCOMPtr<nsIEventTarget> target_;
+};
+
+TEST_F(TransportTest, TestNoDtlsVerificationSettings) {
+ ConnectSocketExpectFail();
+}
+
+static void DisableChaCha(TransportTestPeer* peer) {
+ // On ARM, ChaCha20Poly1305 might be preferred; disable it for the tests that
+ // want to check the cipher suite. It doesn't matter which peer disables the
+ // suite, disabling on either side has the same effect.
+ std::vector<uint16_t> chachaSuites;
+ chachaSuites.push_back(TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256);
+ chachaSuites.push_back(TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256);
+ peer->SetCipherSuiteChanges(std::vector<uint16_t>(), chachaSuites);
+}
+
+TEST_F(TransportTest, TestConnect) {
+ SetDtlsPeer();
+ DisableChaCha(p1_);
+ ConnectSocket();
+
+ // check that we got the right suite
+ ASSERT_EQ(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, p1_->cipherSuite());
+
+ // no SRTP on this one
+ ASSERT_EQ(0, p1_->srtpCipher());
+}
+
+TEST_F(TransportTest, TestConnectSrtp) {
+ SetupSrtp();
+ SetDtlsPeer();
+ DisableChaCha(p2_);
+ ConnectSocket();
+
+ ASSERT_EQ(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, p1_->cipherSuite());
+
+ // SRTP is on with default value
+ ASSERT_EQ(kDtlsSrtpAeadAes128Gcm, p1_->srtpCipher());
+}
+
+TEST_F(TransportTest, TestConnectDestroyFlowsMainThread) {
+ SetDtlsPeer();
+ ConnectSocket();
+ DestroyPeerFlows();
+}
+
+TEST_F(TransportTest, TestConnectAllowAll) {
+ SetDtlsAllowAll();
+ ConnectSocket();
+}
+
+TEST_F(TransportTest, TestConnectAlpn) {
+ SetDtlsPeer();
+ SetAlpn("a", "a");
+ ConnectSocket();
+ CheckAlpn("a", "a");
+}
+
+TEST_F(TransportTest, TestConnectAlpnMismatch) {
+ SetDtlsPeer();
+ SetAlpn("something", "different");
+ ConnectSocketExpectFail();
+}
+
+TEST_F(TransportTest, TestConnectAlpnServerDefault) {
+ SetDtlsPeer();
+ SetAlpn("def", "");
+ // server allows default, client doesn't support
+ ConnectSocket();
+ CheckAlpn("def", "");
+}
+
+TEST_F(TransportTest, TestConnectAlpnClientDefault) {
+ SetDtlsPeer();
+ SetAlpn("", "clientdef");
+ // client allows default, but server will ignore the extension
+ ConnectSocket();
+ CheckAlpn("", "clientdef");
+}
+
+TEST_F(TransportTest, TestConnectClientNoAlpn) {
+ SetDtlsPeer();
+ // Here the server has ALPN, but no default is allowed.
+ // Reminder: p1 == server, p2 == client
+ SetAlpn("server-nodefault", "", false);
+ // The server doesn't see the extension, so negotiates without it.
+ // But then the server is forced to close when it discovers that ALPN wasn't
+ // negotiated; the client sees a close.
+ ConnectSocketExpectState(TransportLayer::TS_ERROR, TransportLayer::TS_CLOSED);
+}
+
+TEST_F(TransportTest, TestConnectServerNoAlpn) {
+ SetDtlsPeer();
+ SetAlpn("", "client-nodefault", false);
+ // The client aborts; the server doesn't realize this is a problem and just
+ // sees the close.
+ ConnectSocketExpectState(TransportLayer::TS_CLOSED, TransportLayer::TS_ERROR);
+}
+
+TEST_F(TransportTest, TestConnectNoDigest) {
+ SetDtlsPeer(0, 0);
+
+ ConnectSocketExpectFail();
+}
+
+TEST_F(TransportTest, TestConnectBadDigest) {
+ SetDtlsPeer(1, 1);
+
+ ConnectSocketExpectFail();
+}
+
+TEST_F(TransportTest, TestConnectTwoDigests) {
+ SetDtlsPeer(2, 0);
+
+ ConnectSocket();
+}
+
+TEST_F(TransportTest, TestConnectTwoDigestsFirstBad) {
+ SetDtlsPeer(2, 1);
+
+ ConnectSocket();
+}
+
+TEST_F(TransportTest, TestConnectTwoDigestsSecondBad) {
+ SetDtlsPeer(2, 2);
+
+ ConnectSocket();
+}
+
+TEST_F(TransportTest, TestConnectTwoDigestsBothBad) {
+ SetDtlsPeer(2, 3);
+
+ ConnectSocketExpectFail();
+}
+
+TEST_F(TransportTest, TestConnectInjectCCS) {
+ SetDtlsPeer();
+ p2_->SetInspector(MakeUnique<DtlsInspectorInjector>(
+ kTlsHandshakeType, kTlsHandshakeCertificate, kTlsFakeChangeCipherSpec,
+ sizeof(kTlsFakeChangeCipherSpec)));
+
+ ConnectSocket();
+}
+
+TEST_F(TransportTest, TestConnectVerifyNewECDHE) {
+ SetDtlsPeer();
+ DtlsInspectorRecordHandshakeMessage* i1 =
+ new DtlsInspectorRecordHandshakeMessage(kTlsHandshakeServerKeyExchange);
+ p1_->SetInspector(i1);
+ ConnectSocket();
+ TlsServerKeyExchangeECDHE dhe1;
+ ASSERT_TRUE(dhe1.Parse(i1->buffer().data(), i1->buffer().len()));
+
+ Reset();
+ SetDtlsPeer();
+ DtlsInspectorRecordHandshakeMessage* i2 =
+ new DtlsInspectorRecordHandshakeMessage(kTlsHandshakeServerKeyExchange);
+ p1_->SetInspector(i2);
+ ConnectSocket();
+ TlsServerKeyExchangeECDHE dhe2;
+ ASSERT_TRUE(dhe2.Parse(i2->buffer().data(), i2->buffer().len()));
+
+ // Now compare these two to see if they are the same.
+ ASSERT_FALSE((dhe1.public_key_.len() == dhe2.public_key_.len()) &&
+ (!memcmp(dhe1.public_key_.data(), dhe2.public_key_.data(),
+ dhe1.public_key_.len())));
+}
+
+TEST_F(TransportTest, TestConnectVerifyReusedECDHE) {
+ auto set_reuse_ecdhe_key = [](PRFileDesc* fd) {
+ // TransportLayerDtls automatically sets this pref to false
+ // so set it back for test.
+ // This is pretty gross. Dig directly into the NSS FD. The problem
+ // is that we are testing a feature which TransaportLayerDtls doesn't
+ // expose.
+ SECStatus rv = SSL_OptionSet(fd, SSL_REUSE_SERVER_ECDHE_KEY, PR_TRUE);
+ ASSERT_EQ(SECSuccess, rv);
+ };
+
+ SetDtlsPeer();
+ DtlsInspectorRecordHandshakeMessage* i1 =
+ new DtlsInspectorRecordHandshakeMessage(kTlsHandshakeServerKeyExchange);
+ p1_->SetInspector(i1);
+ p1_->SetPostSetup(set_reuse_ecdhe_key);
+ ConnectSocket();
+ TlsServerKeyExchangeECDHE dhe1;
+ ASSERT_TRUE(dhe1.Parse(i1->buffer().data(), i1->buffer().len()));
+
+ Reset();
+ SetDtlsPeer();
+ DtlsInspectorRecordHandshakeMessage* i2 =
+ new DtlsInspectorRecordHandshakeMessage(kTlsHandshakeServerKeyExchange);
+
+ p1_->SetInspector(i2);
+ p1_->SetPostSetup(set_reuse_ecdhe_key);
+
+ ConnectSocket();
+ TlsServerKeyExchangeECDHE dhe2;
+ ASSERT_TRUE(dhe2.Parse(i2->buffer().data(), i2->buffer().len()));
+
+ // Now compare these two to see if they are the same.
+ ASSERT_EQ(dhe1.public_key_.len(), dhe2.public_key_.len());
+ ASSERT_TRUE(!memcmp(dhe1.public_key_.data(), dhe2.public_key_.data(),
+ dhe1.public_key_.len()));
+}
+
+TEST_F(TransportTest, TestTransfer) {
+ SetDtlsPeer();
+ ConnectSocket();
+ TransferTest(1);
+}
+
+TEST_F(TransportTest, TestTransferMaxSize) {
+ SetDtlsPeer();
+ ConnectSocket();
+ /* transportlayerdtls uses a 9216 bytes buffer - as this test uses the
+ * loopback implementation it does not have to take into account the extra
+ * bytes added by the DTLS layer below. */
+ TransferTest(1, 9216);
+}
+
+TEST_F(TransportTest, TestTransferMultiple) {
+ SetDtlsPeer();
+ ConnectSocket();
+ TransferTest(3);
+}
+
+TEST_F(TransportTest, TestTransferCombinedPackets) {
+ SetDtlsPeer();
+ ConnectSocket();
+ p2_->SetCombinePackets(true);
+ TransferTest(3);
+}
+
+TEST_F(TransportTest, TestConnectLoseFirst) {
+ SetDtlsPeer();
+ p1_->SetLoss(0);
+ ConnectSocket();
+ TransferTest(1);
+}
+
+TEST_F(TransportTest, TestConnectIce) {
+ SetDtlsPeer();
+ ConnectIce();
+}
+
+TEST_F(TransportTest, TestTransferIceMaxSize) {
+ SetDtlsPeer();
+ ConnectIce();
+ /* nICEr and transportlayerdtls both use 9216 bytes buffers. But the DTLS
+ * layer add extra bytes to the packet, which size depends on chosen cipher
+ * etc. Sending more then 9216 bytes works, but on the receiving side the call
+ * to PR_recvfrom() will truncate any packet bigger then nICEr's buffer size
+ * of 9216 bytes, which then results in the DTLS layer discarding the packet.
+ * Therefore we leave some headroom (according to
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=1214269#c29 256 bytes should
+ * be save choice) here for the DTLS bytes to make it safely into the
+ * receiving buffer in nICEr. */
+ TransferTest(1, 8960);
+}
+
+TEST_F(TransportTest, TestTransferIceMultiple) {
+ SetDtlsPeer();
+ ConnectIce();
+ TransferTest(3);
+}
+
+TEST_F(TransportTest, TestTransferIceCombinedPackets) {
+ SetDtlsPeer();
+ ConnectIce();
+ p2_->SetCombinePackets(true);
+ TransferTest(3);
+}
+
+// test the default configuration against a peer that supports only
+// one of the mandatory-to-implement suites, which should succeed
+static void ConfigureOneCipher(TransportTestPeer* peer, uint16_t suite) {
+ std::vector<uint16_t> justOne;
+ justOne.push_back(suite);
+ std::vector<uint16_t> everythingElse(
+ SSL_GetImplementedCiphers(),
+ SSL_GetImplementedCiphers() + SSL_GetNumImplementedCiphers());
+ std::remove(everythingElse.begin(), everythingElse.end(), suite);
+ peer->SetCipherSuiteChanges(justOne, everythingElse);
+}
+
+TEST_F(TransportTest, TestCipherMismatch) {
+ SetDtlsPeer();
+ ConfigureOneCipher(p1_, TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256);
+ ConfigureOneCipher(p2_, TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA);
+ ConnectSocketExpectFail();
+}
+
+TEST_F(TransportTest, TestCipherMandatoryOnlyGcm) {
+ SetDtlsPeer();
+ ConfigureOneCipher(p1_, TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256);
+ ConnectSocket();
+ ASSERT_EQ(TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, p1_->cipherSuite());
+}
+
+TEST_F(TransportTest, TestCipherMandatoryOnlyCbc) {
+ SetDtlsPeer();
+ ConfigureOneCipher(p1_, TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA);
+ ConnectSocket();
+ ASSERT_EQ(TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, p1_->cipherSuite());
+}
+
+TEST_F(TransportTest, TestSrtpMismatch) {
+ std::vector<uint16_t> setA;
+ setA.push_back(kDtlsSrtpAes128CmHmacSha1_80);
+ std::vector<uint16_t> setB;
+ setB.push_back(kDtlsSrtpAes128CmHmacSha1_32);
+
+ p1_->SetSrtpCiphers(setA);
+ p2_->SetSrtpCiphers(setB);
+ SetDtlsPeer();
+ ConnectSocketExpectFail();
+
+ ASSERT_EQ(0, p1_->srtpCipher());
+ ASSERT_EQ(0, p2_->srtpCipher());
+}
+
+static SECStatus NoopXtnHandler(PRFileDesc* fd, SSLHandshakeType message,
+ const uint8_t* data, unsigned int len,
+ SSLAlertDescription* alert, void* arg) {
+ return SECSuccess;
+}
+
+static PRBool WriteFixedXtn(PRFileDesc* fd, SSLHandshakeType message,
+ uint8_t* data, unsigned int* len,
+ unsigned int max_len, void* arg) {
+ // When we enable TLS 1.3, change ssl_hs_server_hello here to
+ // ssl_hs_encrypted_extensions. At the same time, add a test that writes to
+ // ssl_hs_server_hello, which should fail.
+ if (message != ssl_hs_client_hello && message != ssl_hs_server_hello) {
+ return false;
+ }
+
+ auto v = reinterpret_cast<std::vector<uint8_t>*>(arg);
+ memcpy(data, &((*v)[0]), v->size());
+ *len = v->size();
+ return true;
+}
+
+// Note that |value| needs to be readable after this function returns.
+static void InstallBadSrtpExtensionWriter(TransportTestPeer* peer,
+ std::vector<uint8_t>* value) {
+ peer->SetPostSetup([value](PRFileDesc* fd) {
+ // Override the handler that is installed by the DTLS setup.
+ SECStatus rv = SSL_InstallExtensionHooks(
+ fd, ssl_use_srtp_xtn, WriteFixedXtn, value, NoopXtnHandler, nullptr);
+ ASSERT_EQ(SECSuccess, rv);
+ });
+}
+
+TEST_F(TransportTest, TestSrtpErrorServerSendsTwoSrtpCiphers) {
+ // Server (p1_) sends an extension with two values, and empty MKI.
+ std::vector<uint8_t> xtn = {0x04, 0x00, 0x01, 0x00, 0x02, 0x00};
+ InstallBadSrtpExtensionWriter(p1_, &xtn);
+ SetupSrtp();
+ SetDtlsPeer();
+ ConnectSocketExpectFail();
+}
+
+TEST_F(TransportTest, TestSrtpErrorServerSendsTwoMki) {
+ // Server (p1_) sends an MKI.
+ std::vector<uint8_t> xtn = {0x02, 0x00, 0x01, 0x01, 0x00};
+ InstallBadSrtpExtensionWriter(p1_, &xtn);
+ SetupSrtp();
+ SetDtlsPeer();
+ ConnectSocketExpectFail();
+}
+
+TEST_F(TransportTest, TestSrtpErrorServerSendsUnknownValue) {
+ std::vector<uint8_t> xtn = {0x02, 0x9a, 0xf1, 0x00};
+ InstallBadSrtpExtensionWriter(p1_, &xtn);
+ SetupSrtp();
+ SetDtlsPeer();
+ ConnectSocketExpectFail();
+}
+
+TEST_F(TransportTest, TestSrtpErrorServerSendsOverflow) {
+ std::vector<uint8_t> xtn = {0x32, 0x00, 0x01, 0x00};
+ InstallBadSrtpExtensionWriter(p1_, &xtn);
+ SetupSrtp();
+ SetDtlsPeer();
+ ConnectSocketExpectFail();
+}
+
+TEST_F(TransportTest, TestSrtpErrorServerSendsUnevenList) {
+ std::vector<uint8_t> xtn = {0x01, 0x00, 0x00};
+ InstallBadSrtpExtensionWriter(p1_, &xtn);
+ SetupSrtp();
+ SetDtlsPeer();
+ ConnectSocketExpectFail();
+}
+
+TEST_F(TransportTest, TestSrtpErrorClientSendsUnevenList) {
+ std::vector<uint8_t> xtn = {0x01, 0x00, 0x00};
+ InstallBadSrtpExtensionWriter(p2_, &xtn);
+ SetupSrtp();
+ SetDtlsPeer();
+ ConnectSocketExpectFail();
+}
+
+TEST_F(TransportTest, OnlyServerSendsSrtpXtn) {
+ p1_->SetupSrtp();
+ SetDtlsPeer();
+ // This should connect, but with no SRTP extension neogtiated.
+ // The client side might negotiate a data channel only.
+ ConnectSocket();
+ ASSERT_NE(TLS_NULL_WITH_NULL_NULL, p1_->cipherSuite());
+ ASSERT_EQ(0, p1_->srtpCipher());
+}
+
+TEST_F(TransportTest, OnlyClientSendsSrtpXtn) {
+ p2_->SetupSrtp();
+ SetDtlsPeer();
+ // This should connect, but with no SRTP extension neogtiated.
+ // The server side might negotiate a data channel only.
+ ConnectSocket();
+ ASSERT_NE(TLS_NULL_WITH_NULL_NULL, p1_->cipherSuite());
+ ASSERT_EQ(0, p1_->srtpCipher());
+}
+
+class TransportSrtpParameterTest
+ : public TransportTest,
+ public ::testing::WithParamInterface<uint16_t> {};
+
+INSTANTIATE_TEST_SUITE_P(
+ SrtpParamInit, TransportSrtpParameterTest,
+ ::testing::ValuesIn(TransportLayerDtls::GetDefaultSrtpCiphers()));
+
+TEST_P(TransportSrtpParameterTest, TestSrtpCiphersMismatchCombinations) {
+ uint16_t cipher = GetParam();
+ std::cerr << "Checking cipher: " << cipher << std::endl;
+
+ p1_->SetupSrtp();
+
+ std::vector<uint16_t> setB;
+ setB.push_back(cipher);
+
+ p2_->SetSrtpCiphers(setB);
+ SetDtlsPeer();
+ ConnectSocket();
+
+ ASSERT_EQ(cipher, p1_->srtpCipher());
+ ASSERT_EQ(cipher, p2_->srtpCipher());
+}
+
+// NSS doesn't support DHE suites on the server end.
+// This checks to see if we barf when that's the only option available.
+TEST_F(TransportTest, TestDheOnlyFails) {
+ SetDtlsPeer();
+
+ // p2_ is the client
+ // setting this on p1_ (the server) causes NSS to assert
+ ConfigureOneCipher(p2_, TLS_DHE_RSA_WITH_AES_128_CBC_SHA);
+ ConnectSocketExpectFail();
+}
+
+} // end namespace
diff --git a/dom/media/webrtc/transport/test/turn_unittest.cpp b/dom/media/webrtc/transport/test/turn_unittest.cpp
new file mode 100644
index 0000000000..ae5d0386d9
--- /dev/null
+++ b/dom/media/webrtc/transport/test/turn_unittest.cpp
@@ -0,0 +1,432 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+// Some code copied from nICEr. License is:
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <stdlib.h>
+#include <iostream>
+
+#include "runnable_utils.h"
+
+#define GTEST_HAS_RTTI 0
+#include "gtest/gtest.h"
+#include "gtest_utils.h"
+
+#define USE_TURN
+
+// nICEr includes
+extern "C" {
+#include "nr_api.h"
+#include "transport_addr.h"
+#include "nr_crypto.h"
+#include "nr_socket.h"
+#include "nr_socket_local.h"
+#include "nr_socket_buffered_stun.h"
+#include "stun_client_ctx.h"
+#include "turn_client_ctx.h"
+}
+
+#include "nricectx.h"
+
+using namespace mozilla;
+
+static std::string kDummyTurnServer("192.0.2.1"); // From RFC 5737
+
+class TurnClient : public MtransportTest {
+ public:
+ TurnClient()
+ : MtransportTest(),
+ real_socket_(nullptr),
+ net_socket_(nullptr),
+ buffered_socket_(nullptr),
+ net_fd_(nullptr),
+ turn_ctx_(nullptr),
+ allocated_(false),
+ received_(0),
+ protocol_(IPPROTO_UDP) {}
+
+ ~TurnClient() = default;
+
+ static void SetUpTestCase() {
+ NrIceCtx::InitializeGlobals(NrIceCtx::GlobalConfig());
+ }
+
+ void SetTcp() { protocol_ = IPPROTO_TCP; }
+
+ void Init_s() {
+ int r;
+ nr_transport_addr addr;
+ r = nr_ip4_port_to_transport_addr(0, 0, protocol_, &addr);
+ ASSERT_EQ(0, r);
+
+ r = nr_socket_local_create(nullptr, &addr, &real_socket_);
+ ASSERT_EQ(0, r);
+
+ if (protocol_ == IPPROTO_TCP) {
+ int r = nr_socket_buffered_stun_create(
+ real_socket_, 100000, TURN_TCP_FRAMING, &buffered_socket_);
+ ASSERT_EQ(0, r);
+ net_socket_ = buffered_socket_;
+ } else {
+ net_socket_ = real_socket_;
+ }
+
+ r = nr_str_port_to_transport_addr(turn_server_.c_str(), 3478, protocol_,
+ &addr);
+ ASSERT_EQ(0, r);
+
+ std::vector<unsigned char> password_vec(turn_password_.begin(),
+ turn_password_.end());
+ Data password;
+ INIT_DATA(password, &password_vec[0], password_vec.size());
+ r = nr_turn_client_ctx_create("test", net_socket_, turn_user_.c_str(),
+ &password, &addr, nullptr, &turn_ctx_);
+ ASSERT_EQ(0, r);
+
+ r = nr_socket_getfd(net_socket_, &net_fd_);
+ ASSERT_EQ(0, r);
+
+ NR_ASYNC_WAIT(net_fd_, NR_ASYNC_WAIT_READ, socket_readable_cb, (void*)this);
+ }
+
+ void TearDown_s() {
+ nr_turn_client_ctx_destroy(&turn_ctx_);
+ if (net_fd_) {
+ NR_ASYNC_CANCEL(net_fd_, NR_ASYNC_WAIT_READ);
+ }
+
+ nr_socket_destroy(&buffered_socket_);
+ }
+
+ void TearDown() {
+ test_utils_->SyncDispatchToSTS(WrapRunnable(this, &TurnClient::TearDown_s));
+ }
+
+ void Allocate_s() {
+ Init_s();
+ ASSERT_TRUE(turn_ctx_);
+
+ int r = nr_turn_client_allocate(turn_ctx_, allocate_success_cb, this);
+ ASSERT_EQ(0, r);
+ }
+
+ void Allocate(bool expect_success = true) {
+ test_utils_->SyncDispatchToSTS(WrapRunnable(this, &TurnClient::Allocate_s));
+
+ if (expect_success) {
+ ASSERT_TRUE_WAIT(allocated_, 5000);
+ } else {
+ PR_Sleep(10000);
+ ASSERT_FALSE(allocated_);
+ }
+ }
+
+ void Allocated() {
+ if (turn_ctx_->state != NR_TURN_CLIENT_STATE_ALLOCATED) {
+ std::cerr << "Allocation failed" << std::endl;
+ return;
+ }
+ allocated_ = true;
+
+ int r;
+ nr_transport_addr addr;
+
+ r = nr_turn_client_get_relayed_address(turn_ctx_, &addr);
+ ASSERT_EQ(0, r);
+
+ relay_addr_ = addr.as_string;
+
+ std::cerr << "Allocation succeeded with addr=" << relay_addr_ << std::endl;
+ }
+
+ void Deallocate_s() {
+ ASSERT_TRUE(turn_ctx_);
+
+ std::cerr << "De-Allocating..." << std::endl;
+ int r = nr_turn_client_deallocate(turn_ctx_);
+ ASSERT_EQ(0, r);
+ }
+
+ void Deallocate() {
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(this, &TurnClient::Deallocate_s));
+ }
+
+ void RequestPermission_s(const std::string& target) {
+ nr_transport_addr addr;
+ int r;
+
+ // Expected pattern here is "IP4:127.0.0.1:3487"
+ ASSERT_EQ(0, target.compare(0, 4, "IP4:"));
+
+ size_t offset = target.rfind(':');
+ ASSERT_NE(std::string::npos, offset);
+
+ std::string host = target.substr(4, offset - 4);
+ std::string port = target.substr(offset + 1);
+
+ r = nr_str_port_to_transport_addr(host.c_str(), atoi(port.c_str()),
+ IPPROTO_UDP, &addr);
+ ASSERT_EQ(0, r);
+
+ r = nr_turn_client_ensure_perm(turn_ctx_, &addr);
+ ASSERT_EQ(0, r);
+ }
+
+ void RequestPermission(const std::string& target) {
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(this, &TurnClient::RequestPermission_s, target));
+ }
+
+ void Readable(NR_SOCKET s, int how, void* arg) {
+ // Re-arm
+ std::cerr << "Socket is readable" << std::endl;
+ NR_ASYNC_WAIT(s, how, socket_readable_cb, arg);
+
+ UCHAR buf[8192];
+ size_t len_s;
+ nr_transport_addr addr;
+
+ int r = nr_socket_recvfrom(net_socket_, buf, sizeof(buf), &len_s, 0, &addr);
+ if (r) {
+ std::cerr << "Error reading from socket" << std::endl;
+ return;
+ }
+
+ ASSERT_LT(len_s, (size_t)INT_MAX);
+ int len = (int)len_s;
+
+ if (nr_is_stun_response_message(buf, len)) {
+ std::cerr << "STUN response" << std::endl;
+ r = nr_turn_client_process_response(turn_ctx_, buf, len, &addr);
+
+ if (r && r != R_REJECTED && r != R_RETRY) {
+ std::cerr << "Error processing STUN: " << r << std::endl;
+ }
+ } else if (nr_is_stun_indication_message(buf, len)) {
+ std::cerr << "STUN indication" << std::endl;
+
+ /* Process the indication */
+ unsigned char data[NR_STUN_MAX_MESSAGE_SIZE];
+ size_t datal;
+ nr_transport_addr remote_addr;
+
+ r = nr_turn_client_parse_data_indication(
+ turn_ctx_, &addr, buf, len, data, &datal, sizeof(data), &remote_addr);
+ ASSERT_EQ(0, r);
+ std::cerr << "Received " << datal << " bytes from "
+ << remote_addr.as_string << std::endl;
+
+ received_ += datal;
+
+ for (size_t i = 0; i < datal; i++) {
+ ASSERT_EQ(i & 0xff, data[i]);
+ }
+ } else {
+ if (nr_is_stun_message(buf, len)) {
+ std::cerr << "STUN message of unexpected type" << std::endl;
+ } else {
+ std::cerr << "Not a STUN message" << std::endl;
+ }
+ return;
+ }
+ }
+
+ void SendTo_s(const std::string& target, int expect_return) {
+ nr_transport_addr addr;
+ int r;
+
+ // Expected pattern here is "IP4:127.0.0.1:3487"
+ ASSERT_EQ(0, target.compare(0, 4, "IP4:"));
+
+ size_t offset = target.rfind(':');
+ ASSERT_NE(std::string::npos, offset);
+
+ std::string host = target.substr(4, offset - 4);
+ std::string port = target.substr(offset + 1);
+
+ r = nr_str_port_to_transport_addr(host.c_str(), atoi(port.c_str()),
+ IPPROTO_UDP, &addr);
+ ASSERT_EQ(0, r);
+
+ unsigned char test[100];
+ for (size_t i = 0; i < sizeof(test); i++) {
+ test[i] = i & 0xff;
+ }
+
+ std::cerr << "Sending test message to " << target << " ..." << std::endl;
+
+ r = nr_turn_client_send_indication(turn_ctx_, test, sizeof(test), 0, &addr);
+ if (expect_return >= 0) {
+ ASSERT_EQ(expect_return, r);
+ }
+ }
+
+ void SendTo(const std::string& target, int expect_return = 0) {
+ test_utils_->SyncDispatchToSTS(
+ WrapRunnable(this, &TurnClient::SendTo_s, target, expect_return));
+ }
+
+ int received() const { return received_; }
+
+ static void socket_readable_cb(NR_SOCKET s, int how, void* arg) {
+ static_cast<TurnClient*>(arg)->Readable(s, how, arg);
+ }
+
+ static void allocate_success_cb(NR_SOCKET s, int how, void* arg) {
+ static_cast<TurnClient*>(arg)->Allocated();
+ }
+
+ protected:
+ std::string turn_server_;
+ nr_socket* real_socket_;
+ nr_socket* net_socket_;
+ nr_socket* buffered_socket_;
+ NR_SOCKET net_fd_;
+ nr_turn_client_ctx* turn_ctx_;
+ std::string relay_addr_;
+ bool allocated_;
+ int received_;
+ int protocol_;
+};
+
+TEST_F(TurnClient, Allocate) {
+ if (WarnIfTurnNotConfigured()) return;
+
+ Allocate();
+}
+
+TEST_F(TurnClient, AllocateTcp) {
+ if (WarnIfTurnNotConfigured()) return;
+
+ SetTcp();
+ Allocate();
+}
+
+TEST_F(TurnClient, AllocateAndHold) {
+ if (WarnIfTurnNotConfigured()) return;
+
+ Allocate();
+ PR_Sleep(20000);
+ ASSERT_TRUE(turn_ctx_->state == NR_TURN_CLIENT_STATE_ALLOCATED);
+}
+
+TEST_F(TurnClient, SendToSelf) {
+ if (WarnIfTurnNotConfigured()) return;
+
+ Allocate();
+ SendTo(relay_addr_);
+ ASSERT_TRUE_WAIT(received() == 100, 5000);
+ SendTo(relay_addr_);
+ ASSERT_TRUE_WAIT(received() == 200, 1000);
+}
+
+TEST_F(TurnClient, SendToSelfTcp) {
+ if (WarnIfTurnNotConfigured()) return;
+
+ SetTcp();
+ Allocate();
+ SendTo(relay_addr_);
+ ASSERT_TRUE_WAIT(received() == 100, 5000);
+ SendTo(relay_addr_);
+ ASSERT_TRUE_WAIT(received() == 200, 1000);
+}
+
+TEST_F(TurnClient, PermissionDenied) {
+ if (WarnIfTurnNotConfigured()) return;
+
+ Allocate();
+ RequestPermission(relay_addr_);
+ PR_Sleep(1000);
+
+ /* Fake a 403 response */
+ nr_turn_permission* perm;
+ perm = STAILQ_FIRST(&turn_ctx_->permissions);
+ ASSERT_TRUE(perm);
+ while (perm) {
+ perm->stun->last_error_code = 403;
+ std::cerr << "Set 403's on permission" << std::endl;
+ perm = STAILQ_NEXT(perm, entry);
+ }
+
+ SendTo(relay_addr_, R_NOT_PERMITTED);
+ ASSERT_TRUE(received() == 0);
+
+ // TODO: We should check if we can still send to a second destination, but
+ // we would need a second TURN client as one client can only handle one
+ // allocation (maybe as part of bug 1128128 ?).
+}
+
+TEST_F(TurnClient, DeallocateReceiveFailure) {
+ if (WarnIfTurnNotConfigured()) return;
+
+ Allocate();
+ SendTo(relay_addr_);
+ ASSERT_TRUE_WAIT(received() == 100, 5000);
+ Deallocate();
+ turn_ctx_->state = NR_TURN_CLIENT_STATE_ALLOCATED;
+ SendTo(relay_addr_);
+ PR_Sleep(1000);
+ ASSERT_TRUE(received() == 100);
+}
+
+TEST_F(TurnClient, DeallocateReceiveFailureTcp) {
+ if (WarnIfTurnNotConfigured()) return;
+
+ SetTcp();
+ Allocate();
+ SendTo(relay_addr_);
+ ASSERT_TRUE_WAIT(received() == 100, 5000);
+ Deallocate();
+ turn_ctx_->state = NR_TURN_CLIENT_STATE_ALLOCATED;
+ /* Either the connection got closed by the TURN server already, then the send
+ * is going to fail, which we simply ignore. Or the connection is still alive
+ * and we cand send the data, but it should not get forwarded to us. In either
+ * case we should not receive more data. */
+ SendTo(relay_addr_, -1);
+ PR_Sleep(1000);
+ ASSERT_TRUE(received() == 100);
+}
+
+TEST_F(TurnClient, AllocateDummyServer) {
+ if (WarnIfTurnNotConfigured()) return;
+
+ turn_server_ = kDummyTurnServer;
+ Allocate(false);
+}
diff --git a/dom/media/webrtc/transport/test/webrtcproxychannel_unittest.cpp b/dom/media/webrtc/transport/test/webrtcproxychannel_unittest.cpp
new file mode 100644
index 0000000000..5bfddc7a3f
--- /dev/null
+++ b/dom/media/webrtc/transport/test/webrtcproxychannel_unittest.cpp
@@ -0,0 +1,754 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <algorithm>
+#include <mutex>
+
+#include "mozilla/net/WebrtcTCPSocket.h"
+#include "mozilla/net/WebrtcTCPSocketCallback.h"
+
+#include "nsISocketTransport.h"
+
+#define GTEST_HAS_RTTI 0
+#include "gtest/gtest.h"
+#include "gtest_utils.h"
+
+static const uint32_t kDefaultTestTimeout = 2000;
+static const char kReadData[] = "Hello, World!";
+static const size_t kReadDataLength = sizeof(kReadData) - 1;
+static const std::string kReadDataString =
+ std::string(kReadData, kReadDataLength);
+static int kDataLargeOuterLoopCount = 128;
+static int kDataLargeInnerLoopCount = 1024;
+
+namespace mozilla {
+
+using namespace net;
+using namespace testing;
+
+class WebrtcTCPSocketTestCallback;
+
+class FakeSocketTransportProvider : public nsISocketTransport {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ // nsISocketTransport
+ NS_IMETHOD GetHost(nsACString& aHost) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD GetPort(int32_t* aPort) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD GetScriptableOriginAttributes(
+ JSContext* cx, JS::MutableHandle<JS::Value> aOriginAttributes) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD SetScriptableOriginAttributes(
+ JSContext* cx, JS::Handle<JS::Value> aOriginAttributes) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ virtual nsresult GetOriginAttributes(
+ mozilla::OriginAttributes* _retval) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ virtual nsresult SetOriginAttributes(
+ const mozilla::OriginAttributes& aOriginAttrs) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD GetPeerAddr(mozilla::net::NetAddr* _retval) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD GetSelfAddr(mozilla::net::NetAddr* _retval) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD Bind(mozilla::net::NetAddr* aLocalAddr) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD GetScriptablePeerAddr(nsINetAddr** _retval) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD GetScriptableSelfAddr(nsINetAddr** _retval) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD GetTlsSocketControl(
+ nsITLSSocketControl** aTLSSocketControl) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD GetSecurityCallbacks(
+ nsIInterfaceRequestor** aSecurityCallbacks) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD SetSecurityCallbacks(
+ nsIInterfaceRequestor* aSecurityCallbacks) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD IsAlive(bool* _retval) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD GetTimeout(uint32_t aType, uint32_t* _retval) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD SetTimeout(uint32_t aType, uint32_t aValue) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD SetLinger(bool aPolarity, int16_t aTimeout) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD SetReuseAddrPort(bool reuseAddrPort) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD GetConnectionFlags(uint32_t* aConnectionFlags) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD SetConnectionFlags(uint32_t aConnectionFlags) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD SetIsPrivate(bool) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD GetTlsFlags(uint32_t* aTlsFlags) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD SetTlsFlags(uint32_t aTlsFlags) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD GetQoSBits(uint8_t* aQoSBits) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD SetQoSBits(uint8_t aQoSBits) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD GetRecvBufferSize(uint32_t* aRecvBufferSize) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD GetSendBufferSize(uint32_t* aSendBufferSize) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD GetKeepaliveEnabled(bool* aKeepaliveEnabled) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD SetKeepaliveEnabled(bool aKeepaliveEnabled) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD SetKeepaliveVals(int32_t keepaliveIdleTime,
+ int32_t keepaliveRetryInterval) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD GetResetIPFamilyPreference(
+ bool* aResetIPFamilyPreference) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD GetEchConfigUsed(bool* aEchConfigUsed) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD SetEchConfig(const nsACString& aEchConfig) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD ResolvedByTRR(bool* _retval) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD GetEffectiveTRRMode(
+ nsIRequest::TRRMode* aEffectiveTRRMode) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD GetTrrSkipReason(nsITRRSkipReason::value* aSkipReason) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD GetRetryDnsIfPossible(bool* aRetryDns) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD GetStatus(nsresult* aStatus) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+
+ // nsITransport
+ NS_IMETHOD OpenInputStream(uint32_t aFlags, uint32_t aSegmentSize,
+ uint32_t aSegmentCount,
+ nsIInputStream** _retval) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD OpenOutputStream(uint32_t aFlags, uint32_t aSegmentSize,
+ uint32_t aSegmentCount,
+ nsIOutputStream** _retval) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+ NS_IMETHOD SetEventSink(nsITransportEventSink* aSink,
+ nsIEventTarget* aEventTarget) override {
+ MOZ_ASSERT(false);
+ return NS_OK;
+ }
+
+ // fake except for these methods which are OK to call
+ // nsISocketTransport
+ NS_IMETHOD SetRecvBufferSize(uint32_t aRecvBufferSize) override {
+ return NS_OK;
+ }
+ NS_IMETHOD SetSendBufferSize(uint32_t aSendBufferSize) override {
+ return NS_OK;
+ }
+ // nsITransport
+ NS_IMETHOD Close(nsresult aReason) override { return NS_OK; }
+
+ protected:
+ virtual ~FakeSocketTransportProvider() = default;
+};
+
+NS_IMPL_ISUPPORTS(FakeSocketTransportProvider, nsISocketTransport, nsITransport)
+
+// Implements some common elements to WebrtcTCPSocketTestOutputStream and
+// WebrtcTCPSocketTestInputStream.
+class WebrtcTCPSocketTestStream {
+ public:
+ WebrtcTCPSocketTestStream();
+
+ void Fail() { mMustFail = true; }
+
+ size_t DataLength();
+ template <typename T>
+ void AppendElements(const T* aBuffer, size_t aLength);
+
+ protected:
+ virtual ~WebrtcTCPSocketTestStream() = default;
+
+ nsTArray<uint8_t> mData;
+ std::mutex mDataMutex;
+
+ bool mMustFail;
+};
+
+WebrtcTCPSocketTestStream::WebrtcTCPSocketTestStream() : mMustFail(false) {}
+
+template <typename T>
+void WebrtcTCPSocketTestStream::AppendElements(const T* aBuffer,
+ size_t aLength) {
+ std::lock_guard<std::mutex> guard(mDataMutex);
+ mData.AppendElements(aBuffer, aLength);
+}
+
+size_t WebrtcTCPSocketTestStream::DataLength() {
+ std::lock_guard<std::mutex> guard(mDataMutex);
+ return mData.Length();
+}
+
+class WebrtcTCPSocketTestInputStream : public nsIAsyncInputStream,
+ public WebrtcTCPSocketTestStream {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIASYNCINPUTSTREAM
+ NS_DECL_NSIINPUTSTREAM
+
+ WebrtcTCPSocketTestInputStream()
+ : mMaxReadSize(1024 * 1024), mAllowCallbacks(false) {}
+
+ void DoCallback();
+ void CallCallback(const nsCOMPtr<nsIInputStreamCallback>& aCallback);
+ void AllowCallbacks() { mAllowCallbacks = true; }
+
+ size_t mMaxReadSize;
+
+ protected:
+ virtual ~WebrtcTCPSocketTestInputStream() = default;
+
+ private:
+ nsCOMPtr<nsIInputStreamCallback> mCallback;
+ nsCOMPtr<nsIEventTarget> mCallbackTarget;
+
+ bool mAllowCallbacks;
+};
+
+NS_IMPL_ISUPPORTS(WebrtcTCPSocketTestInputStream, nsIAsyncInputStream,
+ nsIInputStream)
+
+nsresult WebrtcTCPSocketTestInputStream::AsyncWait(
+ nsIInputStreamCallback* aCallback, uint32_t aFlags,
+ uint32_t aRequestedCount, nsIEventTarget* aEventTarget) {
+ MOZ_ASSERT(!aEventTarget, "no event target should be set");
+
+ mCallback = aCallback;
+ mCallbackTarget = NS_GetCurrentThread();
+
+ if (mAllowCallbacks && DataLength() > 0) {
+ DoCallback();
+ }
+
+ return NS_OK;
+}
+
+nsresult WebrtcTCPSocketTestInputStream::CloseWithStatus(nsresult aStatus) {
+ return Close();
+}
+
+nsresult WebrtcTCPSocketTestInputStream::Close() { return NS_OK; }
+
+nsresult WebrtcTCPSocketTestInputStream::Available(uint64_t* aAvailable) {
+ *aAvailable = DataLength();
+ return NS_OK;
+}
+
+nsresult WebrtcTCPSocketTestInputStream::StreamStatus() { return NS_OK; }
+
+nsresult WebrtcTCPSocketTestInputStream::Read(char* aBuffer, uint32_t aCount,
+ uint32_t* aRead) {
+ std::lock_guard<std::mutex> guard(mDataMutex);
+ if (mMustFail) {
+ return NS_ERROR_FAILURE;
+ }
+ *aRead = std::min({(size_t)aCount, mData.Length(), mMaxReadSize});
+ memcpy(aBuffer, mData.Elements(), *aRead);
+ mData.RemoveElementsAt(0, *aRead);
+ return *aRead > 0 ? NS_OK : NS_BASE_STREAM_WOULD_BLOCK;
+}
+
+nsresult WebrtcTCPSocketTestInputStream::ReadSegments(nsWriteSegmentFun aWriter,
+ void* aClosure,
+ uint32_t aCount,
+ uint32_t* _retval) {
+ MOZ_ASSERT(false);
+ return NS_OK;
+}
+
+nsresult WebrtcTCPSocketTestInputStream::IsNonBlocking(bool* aIsNonBlocking) {
+ *aIsNonBlocking = true;
+ return NS_OK;
+}
+
+void WebrtcTCPSocketTestInputStream::CallCallback(
+ const nsCOMPtr<nsIInputStreamCallback>& aCallback) {
+ aCallback->OnInputStreamReady(this);
+}
+
+void WebrtcTCPSocketTestInputStream::DoCallback() {
+ if (mCallback) {
+ mCallbackTarget->Dispatch(
+ NewRunnableMethod<const nsCOMPtr<nsIInputStreamCallback>&>(
+ "WebrtcTCPSocketTestInputStream::DoCallback", this,
+ &WebrtcTCPSocketTestInputStream::CallCallback,
+ std::move(mCallback)));
+
+ mCallbackTarget = nullptr;
+ }
+}
+
+class WebrtcTCPSocketTestOutputStream : public nsIAsyncOutputStream,
+ public WebrtcTCPSocketTestStream {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIASYNCOUTPUTSTREAM
+ NS_DECL_NSIOUTPUTSTREAM
+
+ WebrtcTCPSocketTestOutputStream() : mMaxWriteSize(1024 * 1024) {}
+
+ void DoCallback();
+ void CallCallback(const nsCOMPtr<nsIOutputStreamCallback>& aCallback);
+
+ std::string DataString();
+
+ uint32_t mMaxWriteSize;
+
+ protected:
+ virtual ~WebrtcTCPSocketTestOutputStream() = default;
+
+ private:
+ nsCOMPtr<nsIOutputStreamCallback> mCallback;
+ nsCOMPtr<nsIEventTarget> mCallbackTarget;
+};
+
+NS_IMPL_ISUPPORTS(WebrtcTCPSocketTestOutputStream, nsIAsyncOutputStream,
+ nsIOutputStream)
+
+nsresult WebrtcTCPSocketTestOutputStream::AsyncWait(
+ nsIOutputStreamCallback* aCallback, uint32_t aFlags,
+ uint32_t aRequestedCount, nsIEventTarget* aEventTarget) {
+ MOZ_ASSERT(!aEventTarget, "no event target should be set");
+
+ mCallback = aCallback;
+ mCallbackTarget = NS_GetCurrentThread();
+
+ return NS_OK;
+}
+
+nsresult WebrtcTCPSocketTestOutputStream::CloseWithStatus(nsresult aStatus) {
+ return Close();
+}
+
+nsresult WebrtcTCPSocketTestOutputStream::Close() { return NS_OK; }
+
+nsresult WebrtcTCPSocketTestOutputStream::Flush() { return NS_OK; }
+
+nsresult WebrtcTCPSocketTestOutputStream::StreamStatus() {
+ return mMustFail ? NS_ERROR_FAILURE : NS_OK;
+}
+
+nsresult WebrtcTCPSocketTestOutputStream::Write(const char* aBuffer,
+ uint32_t aCount,
+ uint32_t* aWrote) {
+ if (mMustFail) {
+ return NS_ERROR_FAILURE;
+ }
+ *aWrote = std::min(aCount, mMaxWriteSize);
+ AppendElements(aBuffer, *aWrote);
+ return NS_OK;
+}
+
+nsresult WebrtcTCPSocketTestOutputStream::WriteSegments(
+ nsReadSegmentFun aReader, void* aClosure, uint32_t aCount,
+ uint32_t* _retval) {
+ MOZ_ASSERT(false);
+ return NS_OK;
+}
+
+nsresult WebrtcTCPSocketTestOutputStream::WriteFrom(nsIInputStream* aFromStream,
+ uint32_t aCount,
+ uint32_t* _retval) {
+ MOZ_ASSERT(false);
+ return NS_OK;
+}
+
+nsresult WebrtcTCPSocketTestOutputStream::IsNonBlocking(bool* aIsNonBlocking) {
+ *aIsNonBlocking = true;
+ return NS_OK;
+}
+
+void WebrtcTCPSocketTestOutputStream::CallCallback(
+ const nsCOMPtr<nsIOutputStreamCallback>& aCallback) {
+ aCallback->OnOutputStreamReady(this);
+}
+
+void WebrtcTCPSocketTestOutputStream::DoCallback() {
+ if (mCallback) {
+ mCallbackTarget->Dispatch(
+ NewRunnableMethod<const nsCOMPtr<nsIOutputStreamCallback>&>(
+ "WebrtcTCPSocketTestOutputStream::CallCallback", this,
+ &WebrtcTCPSocketTestOutputStream::CallCallback,
+ std::move(mCallback)));
+
+ mCallbackTarget = nullptr;
+ }
+}
+
+std::string WebrtcTCPSocketTestOutputStream::DataString() {
+ std::lock_guard<std::mutex> guard(mDataMutex);
+ return std::string((char*)mData.Elements(), mData.Length());
+}
+
+// Fake as in not the real WebrtcTCPSocket but real enough
+class FakeWebrtcTCPSocket : public WebrtcTCPSocket {
+ public:
+ explicit FakeWebrtcTCPSocket(WebrtcTCPSocketCallback* aCallback)
+ : WebrtcTCPSocket(aCallback) {}
+
+ protected:
+ virtual ~FakeWebrtcTCPSocket() = default;
+
+ void InvokeOnClose(nsresult aReason) override;
+ void InvokeOnConnected() override;
+ void InvokeOnRead(nsTArray<uint8_t>&& aReadData) override;
+};
+
+void FakeWebrtcTCPSocket::InvokeOnClose(nsresult aReason) {
+ mProxyCallbacks->OnClose(aReason);
+}
+
+void FakeWebrtcTCPSocket::InvokeOnConnected() {
+ mProxyCallbacks->OnConnected("http"_ns);
+}
+
+void FakeWebrtcTCPSocket::InvokeOnRead(nsTArray<uint8_t>&& aReadData) {
+ mProxyCallbacks->OnRead(std::move(aReadData));
+}
+
+class WebrtcTCPSocketTest : public MtransportTest {
+ public:
+ WebrtcTCPSocketTest()
+ : MtransportTest(),
+ mSocketThread(nullptr),
+ mSocketTransport(nullptr),
+ mInputStream(nullptr),
+ mOutputStream(nullptr),
+ mChannel(nullptr),
+ mCallback(nullptr),
+ mOnCloseCalled(false),
+ mOnConnectedCalled(false) {}
+
+ // WebrtcTCPSocketCallback forwards from mCallback
+ void OnClose(nsresult aReason);
+ void OnConnected(const nsACString& aProxyType);
+ void OnRead(nsTArray<uint8_t>&& aReadData);
+
+ void SetUp() override;
+ void TearDown() override;
+
+ void DoTransportAvailable();
+
+ std::string ReadDataAsString();
+ std::string GetDataLarge();
+
+ nsCOMPtr<nsIEventTarget> mSocketThread;
+
+ nsCOMPtr<nsISocketTransport> mSocketTransport;
+ RefPtr<WebrtcTCPSocketTestInputStream> mInputStream;
+ RefPtr<WebrtcTCPSocketTestOutputStream> mOutputStream;
+ RefPtr<FakeWebrtcTCPSocket> mChannel;
+ RefPtr<WebrtcTCPSocketTestCallback> mCallback;
+
+ bool mOnCloseCalled;
+ bool mOnConnectedCalled;
+
+ size_t ReadDataLength();
+ template <typename T>
+ void AppendReadData(const T* aBuffer, size_t aLength);
+
+ private:
+ nsTArray<uint8_t> mReadData;
+ std::mutex mReadDataMutex;
+};
+
+class WebrtcTCPSocketTestCallback : public WebrtcTCPSocketCallback {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(WebrtcTCPSocketTestCallback, override)
+
+ explicit WebrtcTCPSocketTestCallback(WebrtcTCPSocketTest* aTest)
+ : mTest(aTest) {}
+
+ // WebrtcTCPSocketCallback
+ void OnClose(nsresult aReason) override;
+ void OnConnected(const nsACString& aProxyType) override;
+ void OnRead(nsTArray<uint8_t>&& aReadData) override;
+
+ protected:
+ virtual ~WebrtcTCPSocketTestCallback() = default;
+
+ private:
+ WebrtcTCPSocketTest* mTest;
+};
+
+void WebrtcTCPSocketTest::SetUp() {
+ nsresult rv;
+ // WebrtcTCPSocket's threading model is the same as mtransport
+ // all socket operations are done on the socket thread
+ // callbacks are invoked on the main thread
+ mSocketThread = do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv);
+ ASSERT_TRUE(NS_SUCCEEDED(rv));
+
+ mSocketTransport = new FakeSocketTransportProvider();
+ mInputStream = new WebrtcTCPSocketTestInputStream();
+ mOutputStream = new WebrtcTCPSocketTestOutputStream();
+ mCallback = new WebrtcTCPSocketTestCallback(this);
+ mChannel = new FakeWebrtcTCPSocket(mCallback.get());
+}
+
+void WebrtcTCPSocketTest::TearDown() {}
+
+// WebrtcTCPSocketCallback
+void WebrtcTCPSocketTest::OnRead(nsTArray<uint8_t>&& aReadData) {
+ AppendReadData(aReadData.Elements(), aReadData.Length());
+}
+
+void WebrtcTCPSocketTest::OnConnected(const nsACString& aProxyType) {
+ mOnConnectedCalled = true;
+}
+
+void WebrtcTCPSocketTest::OnClose(nsresult aReason) { mOnCloseCalled = true; }
+
+void WebrtcTCPSocketTest::DoTransportAvailable() {
+ if (!mSocketThread->IsOnCurrentThread()) {
+ mSocketThread->Dispatch(
+ NS_NewRunnableFunction("DoTransportAvailable", [this]() -> void {
+ nsresult rv;
+ rv = mChannel->OnTransportAvailable(mSocketTransport, mInputStream,
+ mOutputStream);
+ ASSERT_EQ(NS_OK, rv);
+ }));
+ } else {
+ // should always be called on the main thread
+ MOZ_ASSERT(0);
+ }
+}
+
+std::string WebrtcTCPSocketTest::ReadDataAsString() {
+ std::lock_guard<std::mutex> guard(mReadDataMutex);
+ return std::string((char*)mReadData.Elements(), mReadData.Length());
+}
+
+std::string WebrtcTCPSocketTest::GetDataLarge() {
+ std::string data;
+ for (int i = 0; i < kDataLargeOuterLoopCount * kDataLargeInnerLoopCount;
+ ++i) {
+ data += kReadData;
+ }
+ return data;
+}
+
+template <typename T>
+void WebrtcTCPSocketTest::AppendReadData(const T* aBuffer, size_t aLength) {
+ std::lock_guard<std::mutex> guard(mReadDataMutex);
+ mReadData.AppendElements(aBuffer, aLength);
+}
+
+size_t WebrtcTCPSocketTest::ReadDataLength() {
+ std::lock_guard<std::mutex> guard(mReadDataMutex);
+ return mReadData.Length();
+}
+
+void WebrtcTCPSocketTestCallback::OnClose(nsresult aReason) {
+ mTest->OnClose(aReason);
+}
+
+void WebrtcTCPSocketTestCallback::OnConnected(const nsACString& aProxyType) {
+ mTest->OnConnected(aProxyType);
+}
+
+void WebrtcTCPSocketTestCallback::OnRead(nsTArray<uint8_t>&& aReadData) {
+ mTest->OnRead(std::move(aReadData));
+}
+
+} // namespace mozilla
+
+typedef mozilla::WebrtcTCPSocketTest WebrtcTCPSocketTest;
+
+TEST_F(WebrtcTCPSocketTest, SetUp) {}
+
+TEST_F(WebrtcTCPSocketTest, TransportAvailable) {
+ DoTransportAvailable();
+ ASSERT_TRUE_WAIT(mOnConnectedCalled, kDefaultTestTimeout);
+}
+
+TEST_F(WebrtcTCPSocketTest, Read) {
+ DoTransportAvailable();
+ ASSERT_TRUE_WAIT(mOnConnectedCalled, kDefaultTestTimeout);
+
+ mInputStream->AppendElements(kReadData, kReadDataLength);
+ mInputStream->DoCallback();
+
+ ASSERT_TRUE_WAIT(ReadDataAsString() == kReadDataString, kDefaultTestTimeout);
+}
+
+TEST_F(WebrtcTCPSocketTest, Write) {
+ DoTransportAvailable();
+ ASSERT_TRUE_WAIT(mOnConnectedCalled, kDefaultTestTimeout);
+
+ nsTArray<uint8_t> data;
+ data.AppendElements(kReadData, kReadDataLength);
+ mChannel->Write(std::move(data));
+
+ ASSERT_TRUE_WAIT(mChannel->CountUnwrittenBytes() == kReadDataLength,
+ kDefaultTestTimeout);
+
+ mOutputStream->DoCallback();
+
+ ASSERT_TRUE_WAIT(mOutputStream->DataString() == kReadDataString,
+ kDefaultTestTimeout);
+}
+
+TEST_F(WebrtcTCPSocketTest, ReadFail) {
+ DoTransportAvailable();
+ ASSERT_TRUE_WAIT(mOnConnectedCalled, kDefaultTestTimeout);
+
+ mInputStream->AppendElements(kReadData, kReadDataLength);
+ mInputStream->Fail();
+ mInputStream->DoCallback();
+
+ ASSERT_TRUE_WAIT(mOnCloseCalled, kDefaultTestTimeout);
+ ASSERT_EQ(0U, ReadDataLength());
+}
+
+TEST_F(WebrtcTCPSocketTest, WriteFail) {
+ DoTransportAvailable();
+ ASSERT_TRUE_WAIT(mOnConnectedCalled, kDefaultTestTimeout);
+
+ nsTArray<uint8_t> array;
+ array.AppendElements(kReadData, kReadDataLength);
+ mChannel->Write(std::move(array));
+
+ ASSERT_TRUE_WAIT(mChannel->CountUnwrittenBytes() == kReadDataLength,
+ kDefaultTestTimeout);
+
+ mOutputStream->Fail();
+ mOutputStream->DoCallback();
+
+ ASSERT_TRUE_WAIT(mOnCloseCalled, kDefaultTestTimeout);
+ ASSERT_EQ(0U, mOutputStream->DataLength());
+}
+
+TEST_F(WebrtcTCPSocketTest, ReadLarge) {
+ DoTransportAvailable();
+ ASSERT_TRUE_WAIT(mOnConnectedCalled, kDefaultTestTimeout);
+
+ const std::string data = GetDataLarge();
+
+ mInputStream->AppendElements(data.c_str(), data.length());
+ // make sure reading loops more than once
+ mInputStream->mMaxReadSize = 3072;
+ mInputStream->AllowCallbacks();
+ mInputStream->DoCallback();
+
+ ASSERT_TRUE_WAIT(ReadDataAsString() == data, kDefaultTestTimeout);
+}
+
+TEST_F(WebrtcTCPSocketTest, WriteLarge) {
+ DoTransportAvailable();
+ ASSERT_TRUE_WAIT(mOnConnectedCalled, kDefaultTestTimeout);
+
+ const std::string data = GetDataLarge();
+
+ for (int i = 0; i < kDataLargeOuterLoopCount; ++i) {
+ nsTArray<uint8_t> array;
+ int chunkSize = kReadDataString.length() * kDataLargeInnerLoopCount;
+ int offset = i * chunkSize;
+ array.AppendElements(data.c_str() + offset, chunkSize);
+ mChannel->Write(std::move(array));
+ }
+
+ ASSERT_TRUE_WAIT(mChannel->CountUnwrittenBytes() == data.length(),
+ kDefaultTestTimeout);
+
+ // make sure writing loops more than once per write request
+ mOutputStream->mMaxWriteSize = 1024;
+ mOutputStream->DoCallback();
+
+ ASSERT_TRUE_WAIT(mOutputStream->DataString() == data, kDefaultTestTimeout);
+}
diff --git a/dom/media/webrtc/transport/test_nr_socket.cpp b/dom/media/webrtc/transport/test_nr_socket.cpp
new file mode 100644
index 0000000000..064013a8b7
--- /dev/null
+++ b/dom/media/webrtc/transport/test_nr_socket.cpp
@@ -0,0 +1,1135 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+/*
+ */
+
+/*
+Based partially on original code from nICEr and nrappkit.
+
+nICEr copyright:
+
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+nrappkit copyright:
+
+ Copyright (C) 2001-2003, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ ekr@rtfm.com Thu Dec 20 20:14:49 2001
+*/
+
+// Original author: bcampen@mozilla.com [:bwc]
+
+extern "C" {
+#include "stun_msg.h" // for NR_STUN_MAX_MESSAGE_SIZE
+#include "async_wait.h"
+#include "async_timer.h"
+#include "nr_socket.h"
+#include "stun.h"
+#include "transport_addr.h"
+}
+
+#include "mozilla/RefPtr.h"
+#include "test_nr_socket.h"
+
+namespace mozilla {
+
+static int test_nat_socket_create(void* obj, nr_transport_addr* addr,
+ nr_socket** sockp) {
+ RefPtr<NrSocketBase> sock = new TestNrSocket(static_cast<TestNat*>(obj));
+
+ int r, _status;
+
+ r = sock->create(addr);
+ if (r) ABORT(r);
+
+ r = nr_socket_create_int(static_cast<void*>(sock), sock->vtbl(), sockp);
+ if (r) ABORT(r);
+
+ _status = 0;
+
+ {
+ // We will release this reference in destroy(), not exactly the normal
+ // ownership model, but it is what it is.
+ NrSocketBase* dummy = sock.forget().take();
+ (void)dummy;
+ }
+
+abort:
+ return _status;
+}
+
+static int test_nat_socket_factory_destroy(void** obj) {
+ TestNat* nat = static_cast<TestNat*>(*obj);
+ *obj = nullptr;
+ nat->Release();
+ return 0;
+}
+
+static nr_socket_factory_vtbl test_nat_socket_factory_vtbl = {
+ test_nat_socket_create, test_nat_socket_factory_destroy};
+
+/* static */
+TestNat::NatBehavior TestNat::ToNatBehavior(const std::string& type) {
+ if (type.empty() || !type.compare("ENDPOINT_INDEPENDENT")) {
+ return TestNat::ENDPOINT_INDEPENDENT;
+ }
+ if (!type.compare("ADDRESS_DEPENDENT")) {
+ return TestNat::ADDRESS_DEPENDENT;
+ }
+ if (!type.compare("PORT_DEPENDENT")) {
+ return TestNat::PORT_DEPENDENT;
+ }
+
+ MOZ_ASSERT(false, "Invalid NAT behavior");
+ return TestNat::ENDPOINT_INDEPENDENT;
+}
+
+bool TestNat::has_port_mappings() const {
+ for (TestNrSocket* sock : sockets_) {
+ if (sock->has_port_mappings()) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool TestNat::is_my_external_tuple(const nr_transport_addr& addr) const {
+ for (TestNrSocket* sock : sockets_) {
+ if (sock->is_my_external_tuple(addr)) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool TestNat::is_an_internal_tuple(const nr_transport_addr& addr) const {
+ for (TestNrSocket* sock : sockets_) {
+ nr_transport_addr addr_behind_nat;
+ if (sock->getaddr(&addr_behind_nat)) {
+ MOZ_CRASH("TestNrSocket::getaddr failed!");
+ }
+
+ if (!nr_transport_addr_cmp(&addr, &addr_behind_nat,
+ NR_TRANSPORT_ADDR_CMP_MODE_ALL)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+int TestNat::create_socket_factory(nr_socket_factory** factorypp) {
+ int r = nr_socket_factory_create_int(this, &test_nat_socket_factory_vtbl,
+ factorypp);
+ if (!r) {
+ AddRef();
+ }
+ return r;
+}
+
+void TestNat::set_proxy_config(
+ std::shared_ptr<NrSocketProxyConfig> aProxyConfig) {
+ proxy_config_ = std::move(aProxyConfig);
+}
+
+TestNrSocket::TestNrSocket(TestNat* nat)
+ : nat_(nat), tls_(false), timer_handle_(nullptr) {
+ nat_->insert_socket(this);
+}
+
+TestNrSocket::~TestNrSocket() { nat_->erase_socket(this); }
+
+RefPtr<NrSocketBase> TestNrSocket::create_external_socket(
+ const nr_transport_addr& dest_addr) const {
+ MOZ_ASSERT(nat_->enabled_);
+ MOZ_ASSERT(!nat_->is_an_internal_tuple(dest_addr));
+
+ int r;
+ nr_transport_addr nat_external_addr;
+
+ // Open the socket on an arbitrary port, on the same address.
+ if ((r = nr_transport_addr_copy(&nat_external_addr,
+ &internal_socket_->my_addr()))) {
+ r_log(LOG_GENERIC, LOG_CRIT, "%s: Failure in nr_transport_addr_copy: %d",
+ __FUNCTION__, r);
+ return nullptr;
+ }
+
+ if ((r = nr_transport_addr_set_port(&nat_external_addr, 0))) {
+ r_log(LOG_GENERIC, LOG_CRIT,
+ "%s: Failure in nr_transport_addr_set_port: %d", __FUNCTION__, r);
+ return nullptr;
+ }
+
+ RefPtr<NrSocketBase> external_socket;
+ r = NrSocketBase::CreateSocket(&nat_external_addr, &external_socket,
+ nat_->proxy_config_);
+
+ if (r) {
+ r_log(LOG_GENERIC, LOG_CRIT, "%s: Failure in NrSocket::create: %d",
+ __FUNCTION__, r);
+ return nullptr;
+ }
+
+ return external_socket;
+}
+
+int TestNrSocket::create(nr_transport_addr* addr) {
+ tls_ = addr->tls;
+
+ r_log(LOG_GENERIC, LOG_DEBUG, "TestNrSocket %p create %s", this,
+ addr->as_string);
+ return NrSocketBase::CreateSocket(addr, &internal_socket_, nullptr);
+}
+
+int TestNrSocket::getaddr(nr_transport_addr* addrp) {
+ return internal_socket_->getaddr(addrp);
+}
+
+void TestNrSocket::close() {
+ r_log(LOG_GENERIC, LOG_DEBUG, "TestNrSocket %p %s closing", this,
+ internal_socket_->my_addr().as_string);
+ if (timer_handle_) {
+ NR_async_timer_cancel(timer_handle_);
+ timer_handle_ = nullptr;
+ }
+ internal_socket_->close();
+ for (RefPtr<PortMapping>& port_mapping : port_mappings_) {
+ port_mapping->external_socket_->close();
+ }
+}
+
+int TestNrSocket::listen(int backlog) {
+ MOZ_ASSERT(internal_socket_->my_addr().protocol == IPPROTO_TCP);
+ r_log(LOG_GENERIC, LOG_DEBUG, "TestNrSocket %p %s listening", this,
+ internal_socket_->my_addr().as_string);
+
+ return internal_socket_->listen(backlog);
+}
+
+int TestNrSocket::accept(nr_transport_addr* addrp, nr_socket** sockp) {
+ MOZ_ASSERT(internal_socket_->my_addr().protocol == IPPROTO_TCP);
+ int r = internal_socket_->accept(addrp, sockp);
+ if (r) {
+ return r;
+ }
+
+ if (nat_->enabled_ && !nat_->is_an_internal_tuple(*addrp)) {
+ nr_socket_destroy(sockp);
+ return R_IO_ERROR;
+ }
+
+ return 0;
+}
+
+void TestNrSocket::process_delayed_cb(NR_SOCKET s, int how, void* cb_arg) {
+ DeferredPacket* op = static_cast<DeferredPacket*>(cb_arg);
+ op->socket_->timer_handle_ = nullptr;
+ r_log(LOG_GENERIC, LOG_DEBUG, "TestNrSocket %s sending delayed STUN response",
+ op->internal_socket_->my_addr().as_string);
+ op->internal_socket_->sendto(op->buffer_.data(), op->buffer_.len(),
+ op->flags_, &op->to_);
+
+ delete op;
+}
+
+int TestNrSocket::sendto(const void* msg, size_t len, int flags,
+ const nr_transport_addr* to) {
+ MOZ_ASSERT(internal_socket_->my_addr().protocol != IPPROTO_TCP);
+ r_log(LOG_GENERIC, LOG_DEBUG, "TestNrSocket %p %s %s", this, __FUNCTION__,
+ to->as_string);
+
+ if (nat_->nat_delegate_ &&
+ nat_->nat_delegate_->on_sendto(nat_, msg, len, flags, to)) {
+ return nat_->error_code_for_drop_;
+ }
+
+ UCHAR* buf = static_cast<UCHAR*>(const_cast<void*>(msg));
+ if (nat_->block_stun_ && nr_is_stun_message(buf, len)) {
+ return nat_->error_code_for_drop_;
+ }
+
+ if (nr_is_stun_request_message(buf, len) &&
+ maybe_send_fake_response(buf, len, to)) {
+ return 0;
+ }
+
+ /* TODO: improve the functionality of this in bug 1253657 */
+ if (!nat_->enabled_ || nat_->is_an_internal_tuple(*to)) {
+ if (nat_->delay_stun_resp_ms_ && nr_is_stun_response_message(buf, len)) {
+ NR_ASYNC_TIMER_SET(
+ nat_->delay_stun_resp_ms_, process_delayed_cb,
+ new DeferredPacket(this, msg, len, flags, to, internal_socket_),
+ &timer_handle_);
+ return 0;
+ }
+ return internal_socket_->sendto(msg, len, flags, to);
+ }
+
+ destroy_stale_port_mappings();
+
+ if (to->protocol == IPPROTO_UDP && nat_->block_udp_) {
+ return nat_->error_code_for_drop_;
+ }
+
+ // Choose our port mapping based on our most selective criteria
+ PortMapping* port_mapping = get_port_mapping(
+ *to, std::max(nat_->filtering_type_, nat_->mapping_type_));
+
+ if (!port_mapping) {
+ // See if we have already made the external socket we need to use.
+ PortMapping* similar_port_mapping =
+ get_port_mapping(*to, nat_->mapping_type_);
+ RefPtr<NrSocketBase> external_socket;
+
+ if (similar_port_mapping) {
+ external_socket = similar_port_mapping->external_socket_;
+ } else {
+ external_socket = create_external_socket(*to);
+ if (!external_socket) {
+ MOZ_ASSERT(false);
+ return R_INTERNAL;
+ }
+ }
+
+ port_mapping = create_port_mapping(*to, external_socket);
+ port_mappings_.push_back(port_mapping);
+
+ if (poll_flags() & PR_POLL_READ) {
+ // Make sure the new port mapping is ready to receive traffic if the
+ // TestNrSocket is already waiting.
+ port_mapping->async_wait(NR_ASYNC_WAIT_READ, socket_readable_callback,
+ this, (char*)__FUNCTION__, __LINE__);
+ }
+ }
+
+ // We probably don't want to propagate the flags, since this is a simulated
+ // external IP address.
+ return port_mapping->sendto(msg, len, *to);
+}
+
+int TestNrSocket::recvfrom(void* buf, size_t maxlen, size_t* len, int flags,
+ nr_transport_addr* from) {
+ MOZ_ASSERT(internal_socket_->my_addr().protocol != IPPROTO_TCP);
+
+ if (!read_buffer_.empty()) {
+ UdpPacket& packet = read_buffer_.front();
+ *len = std::min(maxlen, packet.buffer_->len());
+ memcpy(buf, packet.buffer_->data(), *len);
+ nr_transport_addr_copy(from, &packet.remote_address_);
+ read_buffer_.pop_front();
+ return 0;
+ }
+
+ int r;
+ bool ingress_allowed = false;
+
+ if (readable_socket_) {
+ // If any of the external sockets got data, see if it will be passed through
+ r = readable_socket_->recvfrom(buf, maxlen, len, 0, from);
+ const nr_transport_addr to = readable_socket_->my_addr();
+ readable_socket_ = nullptr;
+ if (!r) {
+ PortMapping* port_mapping_used;
+ ingress_allowed = allow_ingress(to, *from, &port_mapping_used);
+ if (ingress_allowed) {
+ r_log(LOG_GENERIC, LOG_DEBUG, "TestNrSocket %s received from %s via %s",
+ internal_socket_->my_addr().as_string, from->as_string,
+ port_mapping_used->external_socket_->my_addr().as_string);
+ if (nat_->refresh_on_ingress_) {
+ port_mapping_used->last_used_ = PR_IntervalNow();
+ }
+ }
+ }
+ } else {
+ // If no external socket has data, see if there's any data that was sent
+ // directly to the TestNrSocket, and eat it if it isn't supposed to get
+ // through.
+ r = internal_socket_->recvfrom(buf, maxlen, len, flags, from);
+ if (!r) {
+ // We do not use allow_ingress() here because that only handles traffic
+ // landing on an external port.
+ ingress_allowed = (!nat_->enabled_ || nat_->is_an_internal_tuple(*from));
+ if (!ingress_allowed) {
+ r_log(LOG_GENERIC, LOG_INFO,
+ "TestNrSocket %s denying ingress from %s: "
+ "Not behind the same NAT",
+ internal_socket_->my_addr().as_string, from->as_string);
+ } else {
+ r_log(LOG_GENERIC, LOG_DEBUG, "TestNrSocket %s received from %s",
+ internal_socket_->my_addr().as_string, from->as_string);
+ }
+ }
+ }
+
+ // Kinda bad that we are forced to give the app a readable callback and then
+ // say "Oh, never mind...", but the alternative is to totally decouple the
+ // callbacks from STS and the callbacks the app sets. On the bright side, this
+ // speeds up unit tests where we are verifying that ingress is forbidden,
+ // since they'll get a readable callback and then an error, instead of having
+ // to wait for a timeout.
+ if (!ingress_allowed) {
+ *len = 0;
+ r = R_WOULDBLOCK;
+ }
+
+ return r;
+}
+
+bool TestNrSocket::allow_ingress(const nr_transport_addr& to,
+ const nr_transport_addr& from,
+ PortMapping** port_mapping_used) const {
+ // This is only called for traffic arriving at a port mapping
+ MOZ_ASSERT(nat_->enabled_);
+ MOZ_ASSERT(!nat_->is_an_internal_tuple(from));
+
+ // Find the port mapping (if any) that this packet landed on
+ for (PortMapping* port_mapping : port_mappings_) {
+ if (!nr_transport_addr_cmp(&to, &port_mapping->external_socket_->my_addr(),
+ NR_TRANSPORT_ADDR_CMP_MODE_ALL)) {
+ *port_mapping_used = port_mapping;
+ }
+ }
+
+ if (NS_WARN_IF(!(*port_mapping_used))) {
+ MOZ_ASSERT(false);
+ r_log(LOG_GENERIC, LOG_INFO,
+ "TestNrSocket %s denying ingress from %s: "
+ "No port mapping for this local port! What?",
+ internal_socket_->my_addr().as_string, from.as_string);
+ return false;
+ }
+
+ if (!port_mapping_matches(**port_mapping_used, from, nat_->filtering_type_)) {
+ r_log(LOG_GENERIC, LOG_INFO,
+ "TestNrSocket %s denying ingress from %s: "
+ "Filtered (no port mapping for source)",
+ internal_socket_->my_addr().as_string, from.as_string);
+ return false;
+ }
+
+ if (is_port_mapping_stale(**port_mapping_used)) {
+ r_log(LOG_GENERIC, LOG_INFO,
+ "TestNrSocket %s denying ingress from %s: "
+ "Stale port mapping",
+ internal_socket_->my_addr().as_string, from.as_string);
+ return false;
+ }
+
+ if (!nat_->allow_hairpinning_ && nat_->is_my_external_tuple(from)) {
+ r_log(LOG_GENERIC, LOG_INFO,
+ "TestNrSocket %s denying ingress from %s: "
+ "Hairpinning disallowed",
+ internal_socket_->my_addr().as_string, from.as_string);
+ return false;
+ }
+
+ return true;
+}
+
+int TestNrSocket::connect(const nr_transport_addr* addr) {
+ r_log(LOG_GENERIC, LOG_DEBUG, "TestNrSocket %p %s connecting to %s", this,
+ internal_socket_->my_addr().as_string, addr->as_string);
+
+ if (connect_invoked_ || !port_mappings_.empty()) {
+ MOZ_CRASH("TestNrSocket::connect() called more than once!");
+ return R_INTERNAL;
+ }
+
+ if (maybe_get_redirect_targets(addr).isSome()) {
+ // If we are simulating STUN redirects for |addr|, we need to pretend that
+ // the TCP connection worked, since |addr| probably does not actually point
+ // at something that exists.
+ connect_fake_stun_address_.reset(new nr_transport_addr);
+ nr_transport_addr_copy(connect_fake_stun_address_.get(), addr);
+
+ // We dispatch this, otherwise nICEr can trip over its shoelaces
+ GetCurrentSerialEventTarget()->Dispatch(
+ NS_NewRunnableFunction("Async writeable callback for TestNrSocket",
+ [this, self = RefPtr<TestNrSocket>(this)] {
+ if (poll_flags() & PR_POLL_WRITE) {
+ fire_callback(NR_ASYNC_WAIT_WRITE);
+ }
+ }));
+
+ return R_WOULDBLOCK;
+ }
+
+ if (!nat_->enabled_ ||
+ addr->protocol == IPPROTO_UDP // Horrible hack to allow default address
+ // discovery to work. Only works because
+ // we don't normally connect on UDP.
+ || nat_->is_an_internal_tuple(*addr)) {
+ // This will set connect_invoked_
+ return internal_socket_->connect(addr);
+ }
+
+ RefPtr<NrSocketBase> external_socket(create_external_socket(*addr));
+ if (!external_socket) {
+ return R_INTERNAL;
+ }
+
+ PortMapping* port_mapping = create_port_mapping(*addr, external_socket);
+ port_mappings_.push_back(port_mapping);
+ int r = port_mapping->external_socket_->connect(addr);
+ if (r && r != R_WOULDBLOCK) {
+ return r;
+ }
+
+ port_mapping->last_used_ = PR_IntervalNow();
+
+ if (poll_flags() & PR_POLL_READ) {
+ port_mapping->async_wait(NR_ASYNC_WAIT_READ,
+ port_mapping_tcp_passthrough_callback, this,
+ (char*)__FUNCTION__, __LINE__);
+ }
+
+ return r;
+}
+
+int TestNrSocket::write(const void* msg, size_t len, size_t* written) {
+ r_log(LOG_GENERIC, LOG_DEBUG, "TestNrSocket %p %s writing", this,
+ internal_socket_->my_addr().as_string);
+
+ UCHAR* buf = static_cast<UCHAR*>(const_cast<void*>(msg));
+
+ if (nat_->nat_delegate_ &&
+ nat_->nat_delegate_->on_write(nat_, msg, len, written)) {
+ return R_INTERNAL;
+ }
+
+ if (nat_->block_stun_ && nr_is_stun_message(buf, len)) {
+ // Should cause this socket to be abandoned
+ r_log(LOG_GENERIC, LOG_DEBUG,
+ "TestNrSocket %s dropping outgoing TCP "
+ "because it is configured to drop STUN",
+ my_addr().as_string);
+ return R_INTERNAL;
+ }
+
+ if (nr_is_stun_request_message(buf, len) && connect_fake_stun_address_ &&
+ maybe_send_fake_response(buf, len, connect_fake_stun_address_.get())) {
+ return 0;
+ }
+
+ if (nat_->block_tcp_ && !tls_) {
+ // Should cause this socket to be abandoned
+ r_log(LOG_GENERIC, LOG_DEBUG,
+ "TestNrSocket %s dropping outgoing TCP "
+ "because it is configured to drop TCP",
+ my_addr().as_string);
+ return R_INTERNAL;
+ }
+
+ if (nat_->block_tls_ && tls_) {
+ // Should cause this socket to be abandoned
+ r_log(LOG_GENERIC, LOG_DEBUG,
+ "TestNrSocket %s dropping outgoing TLS "
+ "because it is configured to drop TLS",
+ my_addr().as_string);
+ return R_INTERNAL;
+ }
+
+ if (port_mappings_.empty()) {
+ // The no-nat case, just pass call through.
+ r_log(LOG_GENERIC, LOG_DEBUG, "TestNrSocket %s writing",
+ my_addr().as_string);
+
+ return internal_socket_->write(msg, len, written);
+ }
+ destroy_stale_port_mappings();
+ if (port_mappings_.empty()) {
+ r_log(LOG_GENERIC, LOG_DEBUG,
+ "TestNrSocket %s dropping outgoing TCP "
+ "because the port mapping was stale",
+ my_addr().as_string);
+ return R_INTERNAL;
+ }
+ // This is TCP only
+ MOZ_ASSERT(port_mappings_.size() == 1);
+ r_log(LOG_GENERIC, LOG_DEBUG, "PortMapping %s -> %s writing",
+ port_mappings_.front()->external_socket_->my_addr().as_string,
+ port_mappings_.front()->remote_address_.as_string);
+ port_mappings_.front()->last_used_ = PR_IntervalNow();
+ return port_mappings_.front()->external_socket_->write(msg, len, written);
+}
+
+int TestNrSocket::read(void* buf, size_t maxlen, size_t* len) {
+ r_log(LOG_GENERIC, LOG_DEBUG, "TestNrSocket %p %s reading", this,
+ internal_socket_->my_addr().as_string);
+
+ if (!read_buffer_.empty()) {
+ r_log(LOG_GENERIC, LOG_DEBUG,
+ "TestNrSocket %p %s has stuff in read_buffer_", this,
+ internal_socket_->my_addr().as_string);
+ UdpPacket packet(std::move(read_buffer_.front()));
+ read_buffer_.pop_front();
+ *len = std::min(maxlen, packet.buffer_->len());
+ memcpy(buf, packet.buffer_->data(), *len);
+ if (*len != packet.buffer_->len()) {
+ // Put remaining bytes in new packet, at the front.
+ read_buffer_.emplace_front(packet.buffer_->data() + *len,
+ packet.buffer_->len() - *len,
+ packet.remote_address_);
+ }
+ return 0;
+ }
+
+ if (connect_fake_stun_address_) {
+ return R_WOULDBLOCK;
+ }
+
+ int r;
+
+ if (port_mappings_.empty()) {
+ r = internal_socket_->read(buf, maxlen, len);
+ } else {
+ MOZ_ASSERT(port_mappings_.size() == 1);
+ r = port_mappings_.front()->external_socket_->read(buf, maxlen, len);
+ if (!r && nat_->refresh_on_ingress_) {
+ port_mappings_.front()->last_used_ = PR_IntervalNow();
+ }
+ }
+
+ if (r) {
+ return r;
+ }
+
+ if (nat_->nat_delegate_ &&
+ nat_->nat_delegate_->on_read(nat_, buf, maxlen, len)) {
+ return R_INTERNAL;
+ }
+
+ if (nat_->block_tcp_ && !tls_) {
+ // Should cause this socket to be abandoned
+ return R_INTERNAL;
+ }
+
+ if (nat_->block_tls_ && tls_) {
+ // Should cause this socket to be abandoned
+ return R_INTERNAL;
+ }
+
+ UCHAR* cbuf = static_cast<UCHAR*>(const_cast<void*>(buf));
+ if (nat_->block_stun_ && nr_is_stun_message(cbuf, *len)) {
+ // Should cause this socket to be abandoned
+ return R_INTERNAL;
+ }
+
+ return r;
+}
+
+int TestNrSocket::async_wait(int how, NR_async_cb cb, void* cb_arg,
+ char* function, int line) {
+ r_log(LOG_GENERIC, LOG_DEBUG, "TestNrSocket %s waiting for %s",
+ internal_socket_->my_addr().as_string,
+ how == NR_ASYNC_WAIT_READ ? "read" : "write");
+
+ int r;
+
+ if (how == NR_ASYNC_WAIT_READ) {
+ NrSocketBase::async_wait(how, cb, cb_arg, function, line);
+ if (!read_buffer_.empty()) {
+ fire_readable_callback();
+ return 0;
+ }
+
+ // Make sure we're waiting on the socket for the internal address
+ r = internal_socket_->async_wait(how, socket_readable_callback, this,
+ function, line);
+ } else {
+ if (connect_fake_stun_address_) {
+ // Fake TCP connection case; register the callback on this socket, not
+ // a real one.
+ return NrSocketBase::async_wait(how, cb, cb_arg, function, line);
+ }
+
+ // For write, just use the readiness of the internal socket, since we queue
+ // everything for the port mappings.
+ r = internal_socket_->async_wait(how, cb, cb_arg, function, line);
+ }
+
+ if (r) {
+ r_log(LOG_GENERIC, LOG_ERR,
+ "TestNrSocket %s failed to async_wait for "
+ "internal socket: %d\n",
+ internal_socket_->my_addr().as_string, r);
+ return r;
+ }
+
+ if (is_tcp_connection_behind_nat()) {
+ // Bypass all port-mapping related logic
+ return 0;
+ }
+
+ if (internal_socket_->my_addr().protocol == IPPROTO_TCP) {
+ // For a TCP connection through a simulated NAT, these signals are
+ // just passed through.
+ MOZ_ASSERT(port_mappings_.size() == 1);
+
+ return port_mappings_.front()->async_wait(
+ how, port_mapping_tcp_passthrough_callback, this, function, line);
+ }
+ if (how == NR_ASYNC_WAIT_READ) {
+ // For UDP port mappings, we decouple the writeable callbacks
+ for (PortMapping* port_mapping : port_mappings_) {
+ // Be ready to receive traffic on our port mappings
+ r = port_mapping->async_wait(how, socket_readable_callback, this,
+ function, line);
+ if (r) {
+ r_log(LOG_GENERIC, LOG_ERR,
+ "TestNrSocket %s failed to async_wait for "
+ "port mapping: %d\n",
+ internal_socket_->my_addr().as_string, r);
+ return r;
+ }
+ }
+ }
+
+ return 0;
+}
+
+void TestNrSocket::cancel_port_mapping_async_wait(int how) {
+ for (PortMapping* port_mapping : port_mappings_) {
+ port_mapping->cancel(how);
+ }
+}
+
+int TestNrSocket::cancel(int how) {
+ r_log(LOG_GENERIC, LOG_DEBUG, "TestNrSocket %s stop waiting for %s",
+ internal_socket_->my_addr().as_string,
+ how == NR_ASYNC_WAIT_READ ? "read" : "write");
+
+ if (connect_fake_stun_address_) {
+ return NrSocketBase::cancel(how);
+ }
+
+ // Writable callbacks are decoupled except for the TCP case
+ if (how == NR_ASYNC_WAIT_READ ||
+ internal_socket_->my_addr().protocol == IPPROTO_TCP) {
+ cancel_port_mapping_async_wait(how);
+ }
+
+ return internal_socket_->cancel(how);
+}
+
+bool TestNrSocket::has_port_mappings() const { return !port_mappings_.empty(); }
+
+bool TestNrSocket::is_my_external_tuple(const nr_transport_addr& addr) const {
+ for (PortMapping* port_mapping : port_mappings_) {
+ nr_transport_addr port_mapping_addr;
+ if (port_mapping->external_socket_->getaddr(&port_mapping_addr)) {
+ MOZ_CRASH("NrSocket::getaddr failed!");
+ }
+
+ if (!nr_transport_addr_cmp(&addr, &port_mapping_addr,
+ NR_TRANSPORT_ADDR_CMP_MODE_ALL)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool TestNrSocket::is_port_mapping_stale(
+ const PortMapping& port_mapping) const {
+ PRIntervalTime now = PR_IntervalNow();
+ PRIntervalTime elapsed_ticks = now - port_mapping.last_used_;
+ uint32_t idle_duration = PR_IntervalToMilliseconds(elapsed_ticks);
+ return idle_duration > nat_->mapping_timeout_;
+}
+
+void TestNrSocket::destroy_stale_port_mappings() {
+ for (auto i = port_mappings_.begin(); i != port_mappings_.end();) {
+ auto temp = i;
+ ++i;
+ if (is_port_mapping_stale(**temp)) {
+ r_log(LOG_GENERIC, LOG_INFO,
+ "TestNrSocket %s destroying port mapping %s -> %s",
+ internal_socket_->my_addr().as_string,
+ (*temp)->external_socket_->my_addr().as_string,
+ (*temp)->remote_address_.as_string);
+
+ port_mappings_.erase(temp);
+ }
+ }
+}
+
+void TestNrSocket::socket_readable_callback(void* real_sock_v, int how,
+ void* test_sock_v) {
+ TestNrSocket* test_socket = static_cast<TestNrSocket*>(test_sock_v);
+ NrSocketBase* real_socket = static_cast<NrSocketBase*>(real_sock_v);
+
+ test_socket->on_socket_readable(real_socket);
+}
+
+void TestNrSocket::on_socket_readable(NrSocketBase* real_socket) {
+ if (!readable_socket_ && (real_socket != internal_socket_)) {
+ readable_socket_ = real_socket;
+ }
+
+ fire_readable_callback();
+}
+
+void TestNrSocket::fire_readable_callback() {
+ MOZ_ASSERT(poll_flags() & PR_POLL_READ);
+ r_log(LOG_GENERIC, LOG_DEBUG, "TestNrSocket %p %s ready for read", this,
+ internal_socket_->my_addr().as_string);
+ fire_callback(NR_ASYNC_WAIT_READ);
+}
+
+void TestNrSocket::port_mapping_writeable_callback(void* ext_sock_v, int how,
+ void* test_sock_v) {
+ TestNrSocket* test_socket = static_cast<TestNrSocket*>(test_sock_v);
+ NrSocketBase* external_socket = static_cast<NrSocketBase*>(ext_sock_v);
+
+ test_socket->write_to_port_mapping(external_socket);
+}
+
+void TestNrSocket::write_to_port_mapping(NrSocketBase* external_socket) {
+ MOZ_ASSERT(internal_socket_->my_addr().protocol != IPPROTO_TCP);
+
+ int r = 0;
+ for (PortMapping* port_mapping : port_mappings_) {
+ if (port_mapping->external_socket_ == external_socket) {
+ // If the send succeeds, or if there was nothing to send, we keep going
+ r = port_mapping->send_from_queue();
+ if (r) {
+ break;
+ }
+ }
+ }
+
+ if (r == R_WOULDBLOCK) {
+ // Re-register for writeable callbacks, since we still have stuff to send
+ NR_ASYNC_WAIT(external_socket, NR_ASYNC_WAIT_WRITE,
+ &TestNrSocket::port_mapping_writeable_callback, this);
+ }
+}
+
+void TestNrSocket::port_mapping_tcp_passthrough_callback(void* ext_sock_v,
+ int how,
+ void* test_sock_v) {
+ TestNrSocket* test_socket = static_cast<TestNrSocket*>(test_sock_v);
+ r_log(LOG_GENERIC, LOG_DEBUG, "TestNrSocket %s firing %s callback",
+ test_socket->internal_socket_->my_addr().as_string,
+ how == NR_ASYNC_WAIT_READ ? "readable" : "writeable");
+
+ test_socket->internal_socket_->fire_callback(how);
+}
+
+bool TestNrSocket::is_tcp_connection_behind_nat() const {
+ return internal_socket_->my_addr().protocol == IPPROTO_TCP &&
+ port_mappings_.empty();
+}
+
+TestNrSocket::PortMapping* TestNrSocket::get_port_mapping(
+ const nr_transport_addr& remote_address,
+ TestNat::NatBehavior filter) const {
+ for (PortMapping* port_mapping : port_mappings_) {
+ if (port_mapping_matches(*port_mapping, remote_address, filter)) {
+ return port_mapping;
+ }
+ }
+ return nullptr;
+}
+
+/* static */
+bool TestNrSocket::port_mapping_matches(const PortMapping& port_mapping,
+ const nr_transport_addr& remote_addr,
+ TestNat::NatBehavior filter) {
+ int compare_flags;
+ switch (filter) {
+ case TestNat::ENDPOINT_INDEPENDENT:
+ compare_flags = NR_TRANSPORT_ADDR_CMP_MODE_PROTOCOL;
+ break;
+ case TestNat::ADDRESS_DEPENDENT:
+ compare_flags = NR_TRANSPORT_ADDR_CMP_MODE_ADDR;
+ break;
+ case TestNat::PORT_DEPENDENT:
+ compare_flags = NR_TRANSPORT_ADDR_CMP_MODE_ALL;
+ break;
+ }
+
+ return !nr_transport_addr_cmp(&remote_addr, &port_mapping.remote_address_,
+ compare_flags);
+}
+
+TestNrSocket::PortMapping* TestNrSocket::create_port_mapping(
+ const nr_transport_addr& remote_address,
+ const RefPtr<NrSocketBase>& external_socket) const {
+ r_log(LOG_GENERIC, LOG_INFO, "TestNrSocket %s creating port mapping %s -> %s",
+ internal_socket_->my_addr().as_string,
+ external_socket->my_addr().as_string, remote_address.as_string);
+
+ return new PortMapping(remote_address, external_socket);
+}
+
+TestNrSocket::PortMapping::PortMapping(
+ const nr_transport_addr& remote_address,
+ const RefPtr<NrSocketBase>& external_socket)
+ : external_socket_(external_socket) {
+ nr_transport_addr_copy(&remote_address_, &remote_address);
+}
+
+int TestNrSocket::PortMapping::send_from_queue() {
+ MOZ_ASSERT(remote_address_.protocol != IPPROTO_TCP);
+ int r = 0;
+
+ while (!send_queue_.empty()) {
+ UdpPacket& packet = send_queue_.front();
+ r_log(LOG_GENERIC, LOG_DEBUG,
+ "PortMapping %s -> %s sending from queue to %s",
+ external_socket_->my_addr().as_string, remote_address_.as_string,
+ packet.remote_address_.as_string);
+
+ r = external_socket_->sendto(packet.buffer_->data(), packet.buffer_->len(),
+ 0, &packet.remote_address_);
+
+ if (r) {
+ if (r != R_WOULDBLOCK) {
+ r_log(LOG_GENERIC, LOG_ERR, "%s: Fatal error %d, stop trying",
+ __FUNCTION__, r);
+ send_queue_.clear();
+ } else {
+ r_log(LOG_GENERIC, LOG_DEBUG, "Would block, will retry later");
+ }
+ break;
+ }
+
+ send_queue_.pop_front();
+ }
+
+ return r;
+}
+
+int TestNrSocket::PortMapping::sendto(const void* msg, size_t len,
+ const nr_transport_addr& to) {
+ MOZ_ASSERT(remote_address_.protocol != IPPROTO_TCP);
+ r_log(LOG_GENERIC, LOG_DEBUG, "PortMapping %s -> %s sending to %s",
+ external_socket_->my_addr().as_string, remote_address_.as_string,
+ to.as_string);
+
+ last_used_ = PR_IntervalNow();
+ int r = external_socket_->sendto(msg, len, 0, &to);
+
+ if (r == R_WOULDBLOCK) {
+ r_log(LOG_GENERIC, LOG_DEBUG, "Enqueueing UDP packet to %s", to.as_string);
+ send_queue_.emplace_back(msg, len, to);
+ return 0;
+ }
+ if (r) {
+ r_log(LOG_GENERIC, LOG_ERR, "Error: %d", r);
+ }
+
+ return r;
+}
+
+int TestNrSocket::PortMapping::async_wait(int how, NR_async_cb cb, void* cb_arg,
+ char* function, int line) {
+ r_log(LOG_GENERIC, LOG_DEBUG, "PortMapping %s -> %s waiting for %s",
+ external_socket_->my_addr().as_string, remote_address_.as_string,
+ how == NR_ASYNC_WAIT_READ ? "read" : "write");
+
+ return external_socket_->async_wait(how, cb, cb_arg, function, line);
+}
+
+int TestNrSocket::PortMapping::cancel(int how) {
+ r_log(LOG_GENERIC, LOG_DEBUG, "PortMapping %s -> %s stop waiting for %s",
+ external_socket_->my_addr().as_string, remote_address_.as_string,
+ how == NR_ASYNC_WAIT_READ ? "read" : "write");
+
+ return external_socket_->cancel(how);
+}
+
+class nr_stun_message_deleter {
+ public:
+ nr_stun_message_deleter() = default;
+ void operator()(nr_stun_message* msg) const { nr_stun_message_destroy(&msg); }
+};
+
+bool TestNrSocket::maybe_send_fake_response(const void* msg, size_t len,
+ const nr_transport_addr* to) {
+ Maybe<nsTArray<nsCString>> redirect_targets = maybe_get_redirect_targets(to);
+ if (!redirect_targets.isSome()) {
+ return false;
+ }
+
+ std::unique_ptr<nr_stun_message, nr_stun_message_deleter> request;
+ {
+ nr_stun_message* temp = nullptr;
+ if (NS_WARN_IF(nr_stun_message_create2(&temp, (unsigned char*)msg, len))) {
+ return false;
+ }
+ request.reset(temp);
+ }
+
+ if (NS_WARN_IF(nr_stun_decode_message(request.get(), nullptr, nullptr))) {
+ return false;
+ }
+
+ std::unique_ptr<nr_stun_message, nr_stun_message_deleter> response;
+ {
+ nr_stun_message* temp = nullptr;
+ if (nr_stun_message_create(&temp)) {
+ MOZ_CRASH("nr_stun_message_create failed!");
+ }
+ response.reset(temp);
+ }
+
+ nr_stun_form_error_response(request.get(), response.get(), 300,
+ (char*)"Try alternate");
+
+ int port = 0;
+ if (nr_transport_addr_get_port(to, &port)) {
+ MOZ_CRASH();
+ }
+
+ for (const nsCString& address : *redirect_targets) {
+ r_log(LOG_GENERIC, LOG_DEBUG,
+ "TestNrSocket attempting to add alternate server %s", address.Data());
+ nr_transport_addr addr;
+ if (NS_WARN_IF(nr_str_port_to_transport_addr(address.Data(), port,
+ IPPROTO_UDP, &addr))) {
+ continue;
+ }
+ if (nr_stun_message_add_alternate_server_attribute(response.get(), &addr)) {
+ MOZ_CRASH("nr_stun_message_add_alternate_server_attribute failed!");
+ }
+ }
+
+ if (nr_stun_encode_message(response.get())) {
+ MOZ_CRASH("nr_stun_encode_message failed!");
+ }
+
+ nr_transport_addr response_from;
+ if (nr_transport_addr_is_wildcard(to)) {
+ // |to| points to an FQDN, and nICEr is delegating DNS lookup to us; we
+ // aren't _actually_ going to do that though, so we select a bogus address
+ // for the response to come from. TEST-NET is a fairly reasonable thing to
+ // use for this.
+ int port = 0;
+ if (nr_transport_addr_get_port(to, &port)) {
+ MOZ_CRASH();
+ }
+ switch (to->ip_version) {
+ case NR_IPV4:
+ if (nr_str_port_to_transport_addr("198.51.100.1", port, to->protocol,
+ &response_from)) {
+ MOZ_CRASH();
+ }
+ break;
+ case NR_IPV6:
+ if (nr_str_port_to_transport_addr("::ffff:198.51.100.1", port,
+ to->protocol, &response_from)) {
+ MOZ_CRASH();
+ }
+ break;
+ default:
+ MOZ_CRASH();
+ }
+ } else {
+ nr_transport_addr_copy(&response_from, to);
+ }
+
+ read_buffer_.emplace_back(response->buffer, response->length, response_from);
+
+ // We dispatch this, otherwise nICEr can trip over its shoelaces
+ r_log(LOG_GENERIC, LOG_DEBUG,
+ "TestNrSocket %p scheduling callback for redirect response", this);
+ GetCurrentSerialEventTarget()->Dispatch(NS_NewRunnableFunction(
+ "Async readable callback for TestNrSocket",
+ [this, self = RefPtr<TestNrSocket>(this)] {
+ if (poll_flags() & PR_POLL_READ) {
+ fire_readable_callback();
+ } else {
+ r_log(LOG_GENERIC, LOG_DEBUG,
+ "TestNrSocket %p deferring callback for redirect response",
+ this);
+ }
+ }));
+
+ return true;
+}
+
+Maybe<nsTArray<nsCString>> TestNrSocket::maybe_get_redirect_targets(
+ const nr_transport_addr* to) const {
+ Maybe<nsTArray<nsCString>> result;
+
+ // 256 is overkill, but it hardly matters
+ char addrstring[256];
+ if (nr_transport_addr_get_addrstring(to, addrstring, 256)) {
+ MOZ_CRASH("nr_transport_addr_get_addrstring failed!");
+ }
+
+ r_log(LOG_GENERIC, LOG_DEBUG, "TestNrSocket checking redirect rules for %s",
+ addrstring);
+ auto it = nat_->stun_redirect_map_.find(nsCString(addrstring));
+ if (it != nat_->stun_redirect_map_.end()) {
+ result = Some(it->second);
+ }
+
+ return result;
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/transport/test_nr_socket.h b/dom/media/webrtc/transport/test_nr_socket.h
new file mode 100644
index 0000000000..c6a796c063
--- /dev/null
+++ b/dom/media/webrtc/transport/test_nr_socket.h
@@ -0,0 +1,370 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+/*
+ */
+
+/*
+Based partially on original code from nICEr and nrappkit.
+
+nICEr copyright:
+
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+nrappkit copyright:
+
+ Copyright (C) 2001-2003, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ ekr@rtfm.com Thu Dec 20 20:14:49 2001
+*/
+
+// Original author: bcampen@mozilla.com [:bwc]
+
+#ifndef test_nr_socket__
+#define test_nr_socket__
+
+extern "C" {
+#include "transport_addr.h"
+}
+
+#include "nr_socket_prsock.h"
+
+extern "C" {
+#include "nr_socket.h"
+}
+
+#include <set>
+#include <map>
+#include <list>
+#include <string>
+
+#include "mozilla/UniquePtr.h"
+#include "prinrval.h"
+#include "mediapacket.h"
+
+namespace mozilla {
+
+class TestNrSocket;
+class NrSocketProxyConfig;
+
+/**
+ * A group of TestNrSockets that behave as if they were behind the same NAT.
+ * @note We deliberately avoid addref/release of TestNrSocket here to avoid
+ * masking lifetime errors elsewhere.
+ */
+class TestNat {
+ public:
+ /**
+ * This allows TestNat traffic to be passively inspected.
+ * If a non-zero (error) value is returned, the packet will be dropped,
+ * allowing for tests to extend how packet manipulation is done by
+ * TestNat with having to modify TestNat itself.
+ */
+ class NatDelegate {
+ public:
+ virtual int on_read(TestNat* nat, void* buf, size_t maxlen,
+ size_t* len) = 0;
+ virtual int on_sendto(TestNat* nat, const void* msg, size_t len, int flags,
+ const nr_transport_addr* to) = 0;
+ virtual int on_write(TestNat* nat, const void* msg, size_t len,
+ size_t* written) = 0;
+ };
+
+ typedef enum {
+ /** For mapping, one port is used for all destinations.
+ * For filtering, allow any external address/port. */
+ ENDPOINT_INDEPENDENT,
+
+ /** For mapping, one port for each destination address (for any port).
+ * For filtering, allow incoming traffic from addresses that outgoing
+ * traffic has been sent to. */
+ ADDRESS_DEPENDENT,
+
+ /** For mapping, one port for each destination address/port.
+ * For filtering, allow incoming traffic only from addresses/ports that
+ * outgoing traffic has been sent to. */
+ PORT_DEPENDENT,
+ } NatBehavior;
+
+ TestNat()
+ : enabled_(false),
+ filtering_type_(ENDPOINT_INDEPENDENT),
+ mapping_type_(ENDPOINT_INDEPENDENT),
+ mapping_timeout_(30000),
+ allow_hairpinning_(false),
+ refresh_on_ingress_(false),
+ block_udp_(false),
+ block_stun_(false),
+ block_tcp_(false),
+ block_tls_(false),
+ error_code_for_drop_(0),
+ delay_stun_resp_ms_(0),
+ nat_delegate_(nullptr),
+ sockets_() {}
+
+ bool has_port_mappings() const;
+
+ // Helps determine whether we're hairpinning
+ bool is_my_external_tuple(const nr_transport_addr& addr) const;
+ bool is_an_internal_tuple(const nr_transport_addr& addr) const;
+
+ int create_socket_factory(nr_socket_factory** factorypp);
+
+ void insert_socket(TestNrSocket* socket) { sockets_.insert(socket); }
+
+ void erase_socket(TestNrSocket* socket) { sockets_.erase(socket); }
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(TestNat);
+
+ static NatBehavior ToNatBehavior(const std::string& type);
+
+ void set_proxy_config(std::shared_ptr<NrSocketProxyConfig> aProxyConfig);
+
+ bool enabled_;
+ TestNat::NatBehavior filtering_type_;
+ TestNat::NatBehavior mapping_type_;
+ uint32_t mapping_timeout_;
+ bool allow_hairpinning_;
+ bool refresh_on_ingress_;
+ bool block_udp_;
+ bool block_stun_;
+ bool block_tcp_;
+ bool block_tls_;
+ bool error_code_for_drop_;
+ /* Note: this can only delay a single response so far (bug 1253657) */
+ uint32_t delay_stun_resp_ms_;
+
+ // When we see an outgoing STUN request with a destination address or
+ // destination FQDN that matches a key in this map, we respond with a STUN/300
+ // with a list of ALTERNATE-SERVER fields based on the value in this map.
+ std::map<nsCString, CopyableTArray<nsCString>> stun_redirect_map_;
+
+ NatDelegate* nat_delegate_;
+ std::shared_ptr<NrSocketProxyConfig> proxy_config_;
+
+ private:
+ std::set<TestNrSocket*> sockets_;
+
+ ~TestNat() = default;
+};
+
+/**
+ * Subclass of NrSocketBase that can simulate things like being behind a NAT,
+ * packet loss, latency, packet rewriting, etc. Also exposes some stuff that
+ * assists in diagnostics.
+ * This is accomplished by wrapping an "internal" socket (that handles traffic
+ * behind the NAT), and a collection of "external" sockets (that handle traffic
+ * into/out of the NAT)
+ */
+class TestNrSocket : public NrSocketBase {
+ public:
+ explicit TestNrSocket(TestNat* nat);
+
+ bool has_port_mappings() const;
+ bool is_my_external_tuple(const nr_transport_addr& addr) const;
+
+ // Overrides of NrSocketBase
+ int create(nr_transport_addr* addr) override;
+ int sendto(const void* msg, size_t len, int flags,
+ const nr_transport_addr* to) override;
+ int recvfrom(void* buf, size_t maxlen, size_t* len, int flags,
+ nr_transport_addr* from) override;
+ int getaddr(nr_transport_addr* addrp) override;
+ void close() override;
+ int connect(const nr_transport_addr* addr) override;
+ int write(const void* msg, size_t len, size_t* written) override;
+ int read(void* buf, size_t maxlen, size_t* len) override;
+
+ int listen(int backlog) override;
+ int accept(nr_transport_addr* addrp, nr_socket** sockp) override;
+ int async_wait(int how, NR_async_cb cb, void* cb_arg, char* function,
+ int line) override;
+ int cancel(int how) override;
+
+ // Need override since this is virtual in NrSocketBase
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(TestNrSocket, override)
+
+ private:
+ virtual ~TestNrSocket();
+
+ class UdpPacket {
+ public:
+ UdpPacket(const void* msg, size_t len, const nr_transport_addr& addr)
+ : buffer_(new MediaPacket) {
+ buffer_->Copy(static_cast<const uint8_t*>(msg), len);
+ nr_transport_addr_copy(&remote_address_, &addr);
+ }
+
+ UdpPacket(UdpPacket&& aOrig) = default;
+
+ ~UdpPacket() = default;
+
+ nr_transport_addr remote_address_;
+ UniquePtr<MediaPacket> buffer_;
+ };
+
+ class PortMapping {
+ public:
+ PortMapping(const nr_transport_addr& remote_address,
+ const RefPtr<NrSocketBase>& external_socket);
+
+ int sendto(const void* msg, size_t len, const nr_transport_addr& to);
+ int async_wait(int how, NR_async_cb cb, void* cb_arg, char* function,
+ int line);
+ int cancel(int how);
+ int send_from_queue();
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(PortMapping);
+
+ PRIntervalTime last_used_;
+ RefPtr<NrSocketBase> external_socket_;
+ // For non-symmetric, most of the data here doesn't matter
+ nr_transport_addr remote_address_;
+
+ private:
+ ~PortMapping() { external_socket_->close(); }
+
+ // If external_socket_ returns E_WOULDBLOCK, we don't want to propagate
+ // that to the code using the TestNrSocket. We can also perhaps use this
+ // to help simulate things like latency.
+ std::list<UdpPacket> send_queue_;
+ };
+
+ struct DeferredPacket {
+ DeferredPacket(TestNrSocket* sock, const void* data, size_t len, int flags,
+ const nr_transport_addr* addr,
+ RefPtr<NrSocketBase> internal_socket)
+ : socket_(sock),
+ buffer_(),
+ flags_(flags),
+ internal_socket_(internal_socket) {
+ buffer_.Copy(reinterpret_cast<const uint8_t*>(data), len);
+ nr_transport_addr_copy(&to_, addr);
+ }
+
+ TestNrSocket* socket_;
+ MediaPacket buffer_;
+ int flags_;
+ nr_transport_addr to_;
+ RefPtr<NrSocketBase> internal_socket_;
+ };
+
+ bool is_port_mapping_stale(const PortMapping& port_mapping) const;
+ bool allow_ingress(const nr_transport_addr& to, const nr_transport_addr& from,
+ PortMapping** port_mapping_used) const;
+ void destroy_stale_port_mappings();
+
+ static void socket_readable_callback(void* real_sock_v, int how,
+ void* test_sock_v);
+ void on_socket_readable(NrSocketBase* external_or_internal_socket);
+ void fire_readable_callback();
+
+ static void port_mapping_tcp_passthrough_callback(void* ext_sock_v, int how,
+ void* test_sock_v);
+ void cancel_port_mapping_async_wait(int how);
+
+ static void port_mapping_writeable_callback(void* ext_sock_v, int how,
+ void* test_sock_v);
+ void write_to_port_mapping(NrSocketBase* external_socket);
+ bool is_tcp_connection_behind_nat() const;
+
+ PortMapping* get_port_mapping(const nr_transport_addr& remote_addr,
+ TestNat::NatBehavior filter) const;
+ static bool port_mapping_matches(const PortMapping& port_mapping,
+ const nr_transport_addr& remote_addr,
+ TestNat::NatBehavior filter);
+ PortMapping* create_port_mapping(
+ const nr_transport_addr& remote_addr,
+ const RefPtr<NrSocketBase>& external_socket) const;
+ RefPtr<NrSocketBase> create_external_socket(
+ const nr_transport_addr& remote_addr) const;
+
+ static void process_delayed_cb(NR_SOCKET s, int how, void* cb_arg);
+
+ bool maybe_send_fake_response(const void* msg, size_t len,
+ const nr_transport_addr* to);
+ Maybe<nsTArray<nsCString>> maybe_get_redirect_targets(
+ const nr_transport_addr* to) const;
+
+ RefPtr<NrSocketBase> readable_socket_;
+ // The socket for the "internal" address; used to talk to stuff behind the
+ // same nat.
+ RefPtr<NrSocketBase> internal_socket_;
+ RefPtr<TestNat> nat_;
+ bool tls_;
+ // Since our comparison logic is different depending on what kind of NAT
+ // we simulate, and the STL does not make it very easy to switch out the
+ // comparison function at runtime, and these lists are going to be very
+ // small anyway, we just brute-force it.
+ std::list<RefPtr<PortMapping>> port_mappings_;
+
+ void* timer_handle_;
+
+ // Just used for fake stun responses right now. Not _necessarily_ just UDP
+ // stuff, UdpPacket just has what we need to make this work for UDP.
+ std::list<UdpPacket> read_buffer_;
+ std::unique_ptr<nr_transport_addr> connect_fake_stun_address_;
+};
+
+} // namespace mozilla
+
+#endif // test_nr_socket__
diff --git a/dom/media/webrtc/transport/third_party/moz.build b/dom/media/webrtc/transport/third_party/moz.build
new file mode 100644
index 0000000000..852ce4ac4b
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/moz.build
@@ -0,0 +1,39 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+include('/build/gyp.mozbuild')
+
+GYP_DIRS += [
+ 'nICEr',
+ 'nrappkit',
+]
+
+# Set gyp vars that webrtc needs when building under various analysis tools.
+# Primarily this prevents webrtc from setting NVALGRIND and breaking builds.
+gyp_vars_copy = gyp_vars.copy()
+if CONFIG['MOZ_VALGRIND']:
+ gyp_vars_copy.update(build_for_tool="memcheck")
+elif CONFIG['MOZ_ASAN']:
+ gyp_vars_copy.update(build_for_tool="asan")
+elif CONFIG['MOZ_TSAN']:
+ gyp_vars_copy.update(build_for_tool="tsan")
+
+# These files cannot be built in unified mode because of name clashes on RCSSTRING
+
+GYP_DIRS['nICEr'].input = 'nICEr/nicer.gyp'
+GYP_DIRS['nICEr'].variables = gyp_vars_copy
+GYP_DIRS['nICEr'].sandbox_vars['FINAL_LIBRARY'] = 'xul'
+GYP_DIRS['nrappkit'].input = 'nrappkit/nrappkit.gyp'
+GYP_DIRS['nrappkit'].variables = gyp_vars_copy
+GYP_DIRS['nrappkit'].sandbox_vars['FINAL_LIBRARY'] = 'xul'
+
+
+if CONFIG["CC_TYPE"] == "gcc":
+ GYP_DIRS['nICEr'].sandbox_vars['CFLAGS'] = ['-Wno-stringop-overflow']
+ GYP_DIRS['nrappkit'].sandbox_vars['CFLAGS'] = ['-Wno-stringop-overflow']
+ if int(CONFIG["CC_VERSION"].split(".")[0]) >= 8:
+ GYP_DIRS['nICEr'].sandbox_vars['CFLAGS'] += ['-Wno-stringop-truncation']
+ GYP_DIRS['nrappkit'].sandbox_vars['CFLAGS'] += ['-Wno-stringop-truncation']
diff --git a/dom/media/webrtc/transport/third_party/nICEr/COPYRIGHT b/dom/media/webrtc/transport/third_party/nICEr/COPYRIGHT
new file mode 100644
index 0000000000..2005fbd104
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/COPYRIGHT
@@ -0,0 +1,36 @@
+Portions of this software are subject to the following copyrights:
+
+ Copyright (C) 2007, Adobe Systems Inc.
+ Copyright (C) 2007-2008, Network Resonance, Inc.
+
+Each source file bears an individual copyright notice.
+
+The following license applies to this distribution as a whole.
+
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/dom/media/webrtc/transport/third_party/nICEr/README b/dom/media/webrtc/transport/third_party/nICEr/README
new file mode 100644
index 0000000000..390203dec5
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/README
@@ -0,0 +1,74 @@
+ nICEr 1.0
+
+PREREQUISITES:
+-------------
+You must first obtain and build the following packages:
+
+* nrappkit
+ - http://nrappkit.sourceforge.net/
+
+* OpenSSL-0.9.8g
+ - http://www.openssl.org/source/openssl-0.9.8g.tar.gz
+
+
+For best results, the "ice-<version>" directory should be at
+the same level as the "nrappkit" and "openssl-0.9.8g"
+directories. I.e., the directory structure should look like:
+
+ nrappkit/
+ ice-<version>/
+ openssl/
+ include/
+ lib/VC/
+
+
+BUILDING ON UNIX:
+----------------
+Once the prerequisite packages are built, 'cd' to the
+relevant build directory, one of:
+
+ ice-<version>/make/darwin
+ ice-<version>/make/linux-fedora
+ ice-<version>/make/ubuntu
+
+and simply do a "make".
+
+
+BUILDING ON WINDOWS:
+-------------------
+The Visual C++ project files are configured to expect the
+directory structure described above.
+
+Note: Binary Windows builds of OpenSSL can be found at:
+ http://www.slproweb.com/products/Win32OpenSSL.html
+
+Once the prerequisite packages are built, open the VC++ 9.0
+solution file: ICE/make/win32/ice.sln and build the solution.
+Note: Since the VC++ project/solution files are version 9.0,
+Visual Studio 2008 is required.
+
+
+STATUS:
+------
+The ICE code has been tested on the following platforms:
+-- Fedora Core 4 (Intel 32-bit)
+-- Fedora Core 6 (Intel 32-bit)
+-- Ubuntu 6.10
+-- MacOSX 10.4.9
+-- Windows Vista (Home Premium)
+-- Windows XP Pro
+-- Windows 2000 SP4
+
+
+KNOWN ISSUES:
+------------
+-- TURN SET-ACTIVE-DESTINATION mode not yet supported.
+
+-- Problems may exist with the TURN client implementation; the TURN code
+ has received minimal testing due to the unavailability of a real
+ TURN server to test against.
+
+-- The ICE-Lite implementation is not complete.
+
+-- The new "impatient" timeout has not yet been thoroughly tested.
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/moz.yaml b/dom/media/webrtc/transport/third_party/nICEr/moz.yaml
new file mode 100644
index 0000000000..873769f9c2
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/moz.yaml
@@ -0,0 +1,117 @@
+schema: 1
+
+bugzilla:
+ product: Core
+ component: "WebRTC: Networking"
+
+origin:
+ name: nICEr
+ description: FIXME
+
+ url: https://github.com/resiprocate/nICEr/
+
+ release: b14598f34d12373069693a1f0535fe354a3f1fd5
+ revision: b14598f34d12373069693a1f0535fe354a3f1fd5
+
+ license: BSD-3-Clause
+ license-file: COPYRIGHT
+
+vendoring:
+ url: https://github.com/resiprocate/nICEr/
+ source-hosting: github
+ tracking: commit
+
+ exclude:
+ - api.txt
+ - src/
+
+ keep:
+ - nicer.gyp
+ # Crypto
+ - src/crypto/nr_crypto.c
+ - src/crypto/nr_crypto.h
+ #./src/crypto/nr_crypto_openssl.c
+ #./src/crypto/nr_crypto_openssl.h
+
+ # ICE
+ - src/ice/ice_candidate.c
+ - src/ice/ice_candidate.h
+ - src/ice/ice_candidate_pair.c
+ - src/ice/ice_candidate_pair.h
+ - src/ice/ice_codeword.h
+ - src/ice/ice_component.c
+ - src/ice/ice_component.h
+ - src/ice/ice_ctx.c
+ - src/ice/ice_ctx.h
+ - src/ice/ice_handler.h
+ - src/ice/ice_media_stream.c
+ - src/ice/ice_media_stream.h
+ - src/ice/ice_parser.c
+ - src/ice/ice_peer_ctx.c
+ - src/ice/ice_peer_ctx.h
+ - src/ice/ice_reg.h
+ - src/ice/ice_socket.c
+ - src/ice/ice_socket.h
+
+ # Net
+ - src/net/nr_socket.c
+ - src/net/nr_socket.h
+ #./src/net/nr_socket_local.c
+ - src/net/nr_socket_local.h
+ - src/net/transport_addr.c
+ - src/net/transport_addr.h
+ - src/net/transport_addr_reg.c
+ - src/net/transport_addr_reg.h
+ - src/net/nr_interface_prioritizer.c
+ - src/net/nr_interface_prioritizer.h
+ - src/net/nr_resolver.h
+ - src/net/nr_resolver.c
+ - src/net/nr_socket_multi_tcp.h
+ - src/net/nr_socket_multi_tcp.c
+ - src/net/nr_socket_wrapper.h
+ - src/net/nr_socket_wrapper.c
+ - src/net/local_addr.h
+ - src/net/local_addr.c
+
+ # STUN
+ - src/stun/addrs.c
+ - src/stun/addrs.h
+ - src/stun/addrs-bsd.c
+ - src/stun/addrs-bsd.h
+ - src/stun/addrs-netlink.c
+ - src/stun/addrs-netlink.h
+ - src/stun/addrs-win32.c
+ - src/stun/addrs-win32.h
+ - src/stun/nr_socket_buffered_stun.h
+ - src/stun/nr_socket_buffered_stun.c
+ - src/stun/nr_socket_turn.c
+ - src/stun/nr_socket_turn.h
+ - src/stun/stun.h
+ - src/stun/stun_build.c
+ - src/stun/stun_build.h
+ - src/stun/stun_client_ctx.c
+ - src/stun/stun_client_ctx.h
+ - src/stun/stun_codec.c
+ - src/stun/stun_codec.h
+ - src/stun/stun_hint.c
+ - src/stun/stun_hint.h
+ - src/stun/stun_msg.c
+ - src/stun/stun_msg.h
+ - src/stun/stun_proc.c
+ - src/stun/stun_proc.h
+ - src/stun/stun_reg.h
+ - src/stun/stun_server_ctx.c
+ - src/stun/stun_server_ctx.h
+ - src/stun/stun_util.c
+ - src/stun/stun_util.h
+ - src/stun/turn_client_ctx.c
+ - src/stun/turn_client_ctx.h
+
+ # Util
+ - src/util/cb_args.c
+ - src/util/cb_args.h
+ - src/util/ice_util.c
+ - src/util/ice_util.h
+
+ patches:
+ - non-unified-build.patch
diff --git a/dom/media/webrtc/transport/third_party/nICEr/nicer.gyp b/dom/media/webrtc/transport/third_party/nICEr/nicer.gyp
new file mode 100644
index 0000000000..488b9b229c
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/nicer.gyp
@@ -0,0 +1,276 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# nrappkit.gyp
+#
+#
+{
+ 'variables' : {
+ 'have_ethtool_cmd_speed_hi%': 1
+ },
+ 'targets' : [
+ {
+ 'target_name' : 'nicer',
+ 'type' : 'static_library',
+
+ 'include_dirs' : [
+ ## EXTERNAL
+ # nrappkit
+ '../nrappkit/src/event',
+ '../nrappkit/src/log',
+ '../nrappkit/src/plugin',
+ '../nrappkit/src/registry',
+ '../nrappkit/src/share',
+ '../nrappkit/src/stats',
+ '../nrappkit/src/util',
+ '../nrappkit/src/util/libekr',
+ '../nrappkit/src/port/generic/include',
+
+ # INTERNAL
+ "./src/crypto",
+ "./src/ice",
+ "./src/net",
+ "./src/stun",
+ "./src/util",
+ ],
+
+ 'sources' : [
+ # Crypto
+ "./src/crypto/nr_crypto.c",
+ "./src/crypto/nr_crypto.h",
+ #"./src/crypto/nr_crypto_openssl.c",
+ #"./src/crypto/nr_crypto_openssl.h",
+
+ # ICE
+ "./src/ice/ice_candidate.c",
+ "./src/ice/ice_candidate.h",
+ "./src/ice/ice_candidate_pair.c",
+ "./src/ice/ice_candidate_pair.h",
+ "./src/ice/ice_codeword.h",
+ "./src/ice/ice_component.c",
+ "./src/ice/ice_component.h",
+ "./src/ice/ice_ctx.c",
+ "./src/ice/ice_ctx.h",
+ "./src/ice/ice_handler.h",
+ "./src/ice/ice_media_stream.c",
+ "./src/ice/ice_media_stream.h",
+ "./src/ice/ice_parser.c",
+ "./src/ice/ice_peer_ctx.c",
+ "./src/ice/ice_peer_ctx.h",
+ "./src/ice/ice_reg.h",
+ "./src/ice/ice_socket.c",
+ "./src/ice/ice_socket.h",
+
+ # Net
+ "./src/net/nr_resolver.c",
+ "./src/net/nr_resolver.h",
+ "./src/net/nr_socket_wrapper.c",
+ "./src/net/nr_socket_wrapper.h",
+ "./src/net/nr_socket.c",
+ "./src/net/nr_socket.h",
+ #"./src/net/nr_socket_local.c",
+ "./src/net/nr_socket_local.h",
+ "./src/net/nr_socket_multi_tcp.c",
+ "./src/net/nr_socket_multi_tcp.h",
+ "./src/net/transport_addr.c",
+ "./src/net/transport_addr.h",
+ "./src/net/transport_addr_reg.c",
+ "./src/net/transport_addr_reg.h",
+ "./src/net/local_addr.c",
+ "./src/net/local_addr.h",
+ "./src/net/nr_interface_prioritizer.c",
+ "./src/net/nr_interface_prioritizer.h",
+
+ # STUN
+ "./src/stun/addrs.c",
+ "./src/stun/addrs.h",
+ "./src/stun/addrs-bsd.c",
+ "./src/stun/addrs-bsd.h",
+ "./src/stun/addrs-netlink.c",
+ "./src/stun/addrs-netlink.h",
+ "./src/stun/addrs-win32.c",
+ "./src/stun/addrs-win32.h",
+ "./src/stun/nr_socket_turn.c",
+ "./src/stun/nr_socket_turn.h",
+ "./src/stun/nr_socket_buffered_stun.c",
+ "./src/stun/nr_socket_buffered_stun.h",
+ "./src/stun/stun.h",
+ "./src/stun/stun_build.c",
+ "./src/stun/stun_build.h",
+ "./src/stun/stun_client_ctx.c",
+ "./src/stun/stun_client_ctx.h",
+ "./src/stun/stun_codec.c",
+ "./src/stun/stun_codec.h",
+ "./src/stun/stun_hint.c",
+ "./src/stun/stun_hint.h",
+ "./src/stun/stun_msg.c",
+ "./src/stun/stun_msg.h",
+ "./src/stun/stun_proc.c",
+ "./src/stun/stun_proc.h",
+ "./src/stun/stun_reg.h",
+ "./src/stun/stun_server_ctx.c",
+ "./src/stun/stun_server_ctx.h",
+ "./src/stun/stun_util.c",
+ "./src/stun/stun_util.h",
+ "./src/stun/turn_client_ctx.c",
+ "./src/stun/turn_client_ctx.h",
+
+ # Util
+ "./src/util/cb_args.c",
+ "./src/util/cb_args.h",
+ "./src/util/ice_util.c",
+ "./src/util/ice_util.h",
+
+
+ ],
+
+ 'defines' : [
+ 'SANITY_CHECKS',
+ 'USE_TURN',
+ 'USE_ICE',
+ 'USE_RFC_3489_BACKWARDS_COMPATIBLE',
+ 'USE_STUND_0_96',
+ 'USE_STUN_PEDANTIC',
+ 'USE_TURN',
+ 'NR_SOCKET_IS_VOID_PTR',
+ 'restrict=',
+ 'R_PLATFORM_INT_TYPES=<stdint.h>',
+ 'R_DEFINED_INT2=int16_t',
+ 'R_DEFINED_UINT2=uint16_t',
+ 'R_DEFINED_INT4=int32_t',
+ 'R_DEFINED_UINT4=uint32_t',
+ 'R_DEFINED_INT8=int64_t',
+ 'R_DEFINED_UINT8=uint64_t',
+ ],
+
+ 'conditions' : [
+ ## Mac and BSDs
+ [ 'OS == "mac" or OS == "ios"', {
+ 'defines' : [
+ 'DARWIN',
+ ],
+ }],
+ [ 'os_bsd == 1', {
+ 'defines' : [
+ 'BSD',
+ ],
+ }],
+ [ 'OS == "mac" or OS == "ios" or os_bsd == 1', {
+ 'cflags_mozilla': [
+ '-Wall',
+ '-Wno-parentheses',
+ '-Wno-strict-prototypes',
+ '-Wmissing-prototypes',
+ '-Wno-format',
+ '-Wno-format-security',
+ ],
+ 'defines' : [
+ 'HAVE_LIBM=1',
+ 'HAVE_STRDUP=1',
+ 'HAVE_STRLCPY=1',
+ 'HAVE_SYS_TIME_H=1',
+ 'HAVE_VFPRINTF=1',
+ 'NEW_STDIO'
+ 'RETSIGTYPE=void',
+ 'TIME_WITH_SYS_TIME_H=1',
+ '__UNUSED__=__attribute__((unused))',
+ ],
+
+ 'include_dirs': [
+ '../nrappkit/src/port/darwin/include'
+ ],
+
+ 'sources': [
+ ],
+ }],
+
+ ## Win
+ [ 'OS == "win"', {
+ 'defines' : [
+ 'WIN32',
+ '_WINSOCK_DEPRECATED_NO_WARNINGS',
+ 'USE_ICE',
+ 'USE_TURN',
+ 'USE_RFC_3489_BACKWARDS_COMPATIBLE',
+ 'USE_STUND_0_96',
+ 'USE_STUN_PEDANTIC',
+ '_CRT_SECURE_NO_WARNINGS',
+ '__UNUSED__=',
+ 'HAVE_STRDUP',
+ 'NO_REG_RPC'
+ ],
+
+ 'include_dirs': [
+ '../nrappkit/src/port/win32/include'
+ ],
+ }],
+
+ # Windows, clang-cl build
+ [ 'clang_cl == 1', {
+ 'cflags_mozilla': [
+ '-Xclang',
+ '-Wall',
+ '-Xclang',
+ '-Wno-parentheses',
+ '-Wno-pointer-sign',
+ '-Wno-strict-prototypes',
+ '-Xclang',
+ '-Wno-unused-function',
+ '-Wmissing-prototypes',
+ '-Wno-format',
+ '-Wno-format-security',
+ ],
+ }],
+
+ ## Linux/Android
+ [ '(OS == "linux") or (OS == "android")', {
+ 'cflags_mozilla': [
+ '-Wall',
+ '-Wno-parentheses',
+ '-Wno-strict-prototypes',
+ '-Wmissing-prototypes',
+ '-Wno-format',
+ '-Wno-format-security',
+ ],
+ 'defines' : [
+ 'LINUX',
+ 'HAVE_LIBM=1',
+ 'HAVE_STRDUP=1',
+ 'HAVE_STRLCPY=1',
+ 'HAVE_SYS_TIME_H=1',
+ 'HAVE_VFPRINTF=1',
+ 'NEW_STDIO'
+ 'RETSIGTYPE=void',
+ 'TIME_WITH_SYS_TIME_H=1',
+ '__UNUSED__=__attribute__((unused))',
+ ],
+
+ 'include_dirs': [
+ '../nrappkit/src/port/linux/include'
+ ],
+
+ 'sources': [
+ ],
+ }],
+ ['have_ethtool_cmd_speed_hi==0', {
+ 'defines': [
+ "DONT_HAVE_ETHTOOL_SPEED_HI",
+ ]
+ }],
+ # libFuzzer instrumentation is not compatible with TSan.
+ # See also the comment in build/moz.configure/toolchain.configure.
+ ['(libfuzzer == 1) and (tsan == 0) and (libfuzzer_fuzzer_no_link_flag == 1)', {
+ 'cflags_mozilla': [
+ '-fsanitize=fuzzer-no-link'
+ ],
+ }],
+ ['(libfuzzer == 1) and (tsan == 0) and (libfuzzer_fuzzer_no_link_flag == 0)', {
+ 'cflags_mozilla': [
+ '-fsanitize-coverage=trace-pc-guard,trace-cmp'
+ ],
+ }],
+ ],
+ }]
+}
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/non-unified-build.patch b/dom/media/webrtc/transport/third_party/nICEr/non-unified-build.patch
new file mode 100644
index 0000000000..41f206e67a
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/non-unified-build.patch
@@ -0,0 +1,40 @@
+diff --git a/src/net/nr_socket_wrapper.c b/src/net/nr_socket_wrapper.c
+index 0c9ec5674407d..4ad59527c12ec 100644
+--- a/src/net/nr_socket_wrapper.c
++++ b/src/net/nr_socket_wrapper.c
+@@ -36,6 +36,8 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ #include <nr_api.h>
+ #include "nr_socket_wrapper.h"
+
++#include <assert.h>
++
+ int nr_socket_wrapper_factory_create_int(void *obj, nr_socket_wrapper_factory_vtbl *vtbl,
+ nr_socket_wrapper_factory **wrapperp)
+ {
+diff --git a/src/net/transport_addr.h b/nICEr/src/net/transport_addr.h
+index c75c70a1d94fa..1783d2e4506a8 100644
+--- a/src/net/transport_addr.h
++++ b/src/net/transport_addr.h
+@@ -38,7 +38,22 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ #include <stdbool.h>
+ #include <sys/types.h>
+ #ifdef WIN32
++
++// FIXME: This is dangerous, but exactly the pattern used in
++// nrappkit/src/port/win32/include/csi_platform.h
++// Not good because INT8 are typedefed to different values in
++// <winsock2.h> and <r_types.h>.
++// {
++
++#define UINT8 UBLAH_IGNORE_ME_PLEASE
++#define INT8 BLAH_IGNORE_ME_PLEASE
+ #include <winsock2.h>
++#undef UINT8
++#undef INT8
++#include <r_types.h>
++
++// }
++
+ #include <ws2tcpip.h>
+ #else
+ #include <sys/socket.h>
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/crypto/nr_crypto.c b/dom/media/webrtc/transport/third_party/nICEr/src/crypto/nr_crypto.c
new file mode 100644
index 0000000000..4c430b0e8b
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/crypto/nr_crypto.c
@@ -0,0 +1,67 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <nr_api.h>
+#include "nr_crypto.h"
+
+static int nr_ice_crypto_dummy_random_bytes(UCHAR *buf, size_t len)
+ {
+ fprintf(stderr,"Need to define crypto API implementation\n");
+
+ exit(1);
+ }
+
+static int nr_ice_crypto_dummy_hmac_sha1(UCHAR *key, size_t key_l, UCHAR *buf, size_t buf_l, UCHAR digest[20])
+ {
+ fprintf(stderr,"Need to define crypto API implementation\n");
+
+ exit(1);
+ }
+
+static int nr_ice_crypto_dummy_md5(UCHAR *buf, size_t buf_l, UCHAR digest[16])
+ {
+ fprintf(stderr,"Need to define crypto API implementation\n");
+
+ exit(1);
+ }
+
+static nr_ice_crypto_vtbl nr_ice_crypto_dummy_vtbl= {
+ nr_ice_crypto_dummy_random_bytes,
+ nr_ice_crypto_dummy_hmac_sha1,
+ nr_ice_crypto_dummy_md5
+};
+
+
+
+nr_ice_crypto_vtbl *nr_crypto_vtbl=&nr_ice_crypto_dummy_vtbl;
+
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/crypto/nr_crypto.h b/dom/media/webrtc/transport/third_party/nICEr/src/crypto/nr_crypto.h
new file mode 100644
index 0000000000..5fbbd8f097
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/crypto/nr_crypto.h
@@ -0,0 +1,52 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+
+#ifndef _nr_crypto_h
+#define _nr_crypto_h
+
+
+typedef struct nr_ice_crypto_vtbl_ {
+ int (*random_bytes)(UCHAR *buf, size_t len);
+ int (*hmac_sha1)(UCHAR *key, size_t key_l, UCHAR *buf, size_t buf_l, UCHAR digest[20]);
+ int (*md5)(UCHAR *buf, size_t buf_l, UCHAR digest[16]);
+} nr_ice_crypto_vtbl;
+
+extern nr_ice_crypto_vtbl *nr_crypto_vtbl;
+
+#define nr_crypto_random_bytes(a,b) nr_crypto_vtbl->random_bytes(a,b)
+#define nr_crypto_hmac_sha1(a,b,c,d,e) nr_crypto_vtbl->hmac_sha1(a,b,c,d,e)
+#define nr_crypto_md5(a,b,c) nr_crypto_vtbl->md5(a,b,c)
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_candidate.c b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_candidate.c
new file mode 100644
index 0000000000..b7cf2c9a99
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_candidate.c
@@ -0,0 +1,1052 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <csi_platform.h>
+#include <assert.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/queue.h>
+#include <sys/types.h>
+#ifdef WIN32
+#include <winsock2.h>
+#else
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#endif
+#include "nr_api.h"
+#include "registry.h"
+#include "nr_socket.h"
+#include "async_timer.h"
+
+#include "stun_client_ctx.h"
+#include "stun_server_ctx.h"
+#include "turn_client_ctx.h"
+#include "ice_ctx.h"
+#include "ice_candidate.h"
+#include "ice_codeword.h"
+#include "ice_reg.h"
+#include "ice_util.h"
+#include "nr_socket_turn.h"
+#include "nr_socket.h"
+#include "nr_socket_multi_tcp.h"
+
+static int next_automatic_preference = 127;
+
+static int nr_ice_candidate_initialize2(nr_ice_candidate *cand);
+static int nr_ice_get_foundation(nr_ice_ctx *ctx,nr_ice_candidate *cand);
+static int nr_ice_srvrflx_start_stun(nr_ice_candidate *cand);
+static void nr_ice_srvrflx_stun_finished_cb(NR_SOCKET sock, int how, void *cb_arg);
+#ifdef USE_TURN
+static int nr_ice_start_relay_turn(nr_ice_candidate *cand);
+static void nr_ice_turn_allocated_cb(NR_SOCKET sock, int how, void *cb_arg);
+static int nr_ice_candidate_resolved_cb(void *cb_arg, nr_transport_addr *addr);
+#endif /* USE_TURN */
+
+void nr_ice_candidate_compute_codeword(nr_ice_candidate *cand)
+ {
+ char as_string[1024];
+
+ snprintf(as_string,
+ sizeof(as_string),
+ "%s(%s)",
+ cand->addr.as_string,
+ cand->label);
+
+ nr_ice_compute_codeword(as_string,strlen(as_string),cand->codeword);
+ }
+
+char *nr_ice_candidate_type_names[]={0,"host","srflx","prflx","relay",0};
+char *nr_ice_candidate_tcp_type_names[]={0,"active","passive","so",0};
+
+static const char *nr_ctype_name(nr_ice_candidate_type ctype) {
+ assert(ctype<CTYPE_MAX && ctype>0);
+ if (ctype <= 0 || ctype >= CTYPE_MAX) {
+ return "ERROR";
+ }
+ return nr_ice_candidate_type_names[ctype];
+}
+
+static const char *nr_tcp_type_name(nr_socket_tcp_type tcp_type) {
+ assert(tcp_type<TCP_TYPE_MAX && tcp_type>0);
+ if (tcp_type <= 0 || tcp_type >= TCP_TYPE_MAX) {
+ return "ERROR";
+ }
+ return nr_ice_candidate_tcp_type_names[tcp_type];
+}
+
+static int nr_ice_candidate_format_stun_label(char *label, size_t size, nr_ice_candidate *cand)
+ {
+ *label = 0;
+ snprintf(label, size, "%s(%s|%s)", nr_ctype_name(cand->type),
+ cand->base.as_string, cand->stun_server->addr.as_string);
+
+ return (0);
+ }
+
+int nr_ice_candidate_create(nr_ice_ctx *ctx,nr_ice_component *comp,nr_ice_socket *isock, nr_socket *osock, nr_ice_candidate_type ctype, nr_socket_tcp_type tcp_type, nr_ice_stun_server *stun_server, UCHAR component_id, nr_ice_candidate **candp)
+ {
+ assert(!(comp->stream->flags & NR_ICE_CTX_FLAGS_RELAY_ONLY) || ctype == RELAYED);
+ nr_ice_candidate *cand=0;
+ nr_ice_candidate *tmp=0;
+ int r,_status;
+ char label[512];
+
+ if(!(cand=RCALLOC(sizeof(nr_ice_candidate))))
+ ABORT(R_NO_MEMORY);
+ cand->state=NR_ICE_CAND_STATE_CREATED;
+ cand->ctx=ctx;
+ cand->isock=isock;
+ cand->osock=osock;
+ cand->type=ctype;
+ cand->tcp_type=tcp_type;
+ cand->stun_server=stun_server;
+ cand->component_id=component_id;
+ cand->component=comp;
+ cand->stream=comp->stream;
+
+ /* Extract the addr as the base */
+ if(r=nr_socket_getaddr(cand->isock->sock,&cand->base))
+ ABORT(r);
+
+ switch(ctype) {
+ case HOST:
+ snprintf(label, sizeof(label), "host(%s)", cand->base.as_string);
+ break;
+
+ case SERVER_REFLEXIVE:
+ if(r=nr_ice_candidate_format_stun_label(label, sizeof(label), cand))
+ ABORT(r);
+ break;
+
+ case RELAYED:
+ if(r=nr_ice_candidate_format_stun_label(label, sizeof(label), cand))
+ ABORT(r);
+ break;
+
+ case PEER_REFLEXIVE:
+ snprintf(label, sizeof(label), "prflx");
+ break;
+
+ default:
+ assert(0); /* Can't happen */
+ ABORT(R_BAD_ARGS);
+ }
+
+ if (tcp_type) {
+ const char* ttype = nr_tcp_type_name(tcp_type);
+ const int tlen = strlen(ttype)+1; /* plus space */
+ const size_t llen=strlen(label);
+ if (snprintf(label+llen, sizeof(label)-llen, " %s", ttype) != tlen) {
+ r_log(LOG_ICE,LOG_ERR,"ICE(%s): truncated tcp type added to buffer",
+ ctx->label);
+ }
+ }
+
+ if(!(cand->label=r_strdup(label)))
+ ABORT(R_NO_MEMORY);
+
+ if(r=nr_ice_get_foundation(ctx,cand))
+ ABORT(r);
+ if(r=nr_ice_candidate_compute_priority(cand))
+ ABORT(r);
+
+ TAILQ_FOREACH(tmp,&isock->candidates,entry_sock){
+ if(cand->priority==tmp->priority){
+ r_log(LOG_ICE,LOG_ERR,"ICE(%s): duplicate priority %u candidate %s and candidate %s",
+ ctx->label,cand->priority,cand->label,tmp->label);
+ }
+ }
+
+ if(ctype==RELAYED)
+ cand->u.relayed.turn_sock=osock;
+
+
+ /* Add the candidate to the isock list*/
+ TAILQ_INSERT_TAIL(&isock->candidates,cand,entry_sock);
+
+ nr_ice_candidate_compute_codeword(cand);
+
+ r_log(LOG_ICE,LOG_DEBUG,"ICE(%s)/CAND(%s): created candidate %s with type %s",
+ ctx->label,cand->codeword,cand->label,nr_ctype_name(ctype));
+
+ *candp=cand;
+
+ _status=0;
+ abort:
+ if (_status){
+ r_log(LOG_ICE,LOG_ERR,"ICE(%s): Failed to create candidate of type %s", ctx->label,nr_ctype_name(ctype));
+ nr_ice_candidate_destroy(&cand);
+ }
+ return(_status);
+ }
+
+
+/* Create a peer reflexive candidate */
+int nr_ice_peer_peer_rflx_candidate_create(nr_ice_ctx *ctx,char *label, nr_ice_component *comp,nr_transport_addr *addr, nr_ice_candidate **candp)
+ {
+ nr_ice_candidate *cand=0;
+ nr_ice_candidate_type ctype=PEER_REFLEXIVE;
+ int r,_status;
+
+ if(!(cand=RCALLOC(sizeof(nr_ice_candidate))))
+ ABORT(R_NO_MEMORY);
+ if(!(cand->label=r_strdup(label)))
+ ABORT(R_NO_MEMORY);
+
+ cand->state=NR_ICE_CAND_STATE_INITIALIZED;
+ cand->ctx=ctx;
+ cand->type=ctype;
+ cand->component_id=comp->component_id;
+ cand->component=comp;
+ cand->stream=comp->stream;
+
+
+ r_log(LOG_ICE,LOG_DEBUG,"ICE(%s)/CAND(%s): creating candidate with type %s",
+ ctx->label,label,nr_ctype_name(ctype));
+
+ if(r=nr_transport_addr_copy(&cand->base,addr))
+ ABORT(r);
+ if(r=nr_transport_addr_copy(&cand->addr,addr))
+ ABORT(r);
+ /* Bogus foundation */
+ if(!(cand->foundation=r_strdup(cand->addr.as_string)))
+ ABORT(R_NO_MEMORY);
+
+ nr_ice_candidate_compute_codeword(cand);
+
+ *candp=cand;
+
+ _status=0;
+ abort:
+ if (_status){
+ nr_ice_candidate_destroy(&cand);
+ }
+ return(_status);
+ }
+
+static void nr_ice_candidate_mark_done(nr_ice_candidate *cand, int state)
+ {
+ if (!cand) {
+ assert(0);
+ return;
+ }
+
+ /* If this is a relay candidate, there's likely to be a srflx that is
+ * piggybacking on it. Make sure it is marked done too. */
+ if ((cand->type == RELAYED) && cand->u.relayed.srvflx_candidate) {
+ nr_ice_candidate *srflx=cand->u.relayed.srvflx_candidate;
+ if (state == NR_ICE_CAND_STATE_INITIALIZED &&
+ nr_turn_client_get_mapped_address(cand->u.relayed.turn,
+ &srflx->addr)) {
+ r_log(LOG_ICE, LOG_WARNING, "ICE(%s)/CAND(%s): Failed to get mapped address from TURN allocate response, srflx failed.", cand->ctx->label, cand->label);
+ nr_ice_candidate_mark_done(srflx, NR_ICE_CAND_STATE_FAILED);
+ } else {
+ nr_ice_candidate_mark_done(srflx, state);
+ }
+ }
+
+ NR_async_cb done_cb=cand->done_cb;
+ cand->done_cb=0;
+ cand->state=state;
+ /* This might destroy cand! */
+ if (done_cb) {
+ done_cb(0,0,cand->cb_arg);
+ }
+ }
+
+int nr_ice_candidate_destroy(nr_ice_candidate **candp)
+ {
+ nr_ice_candidate *cand=0;
+
+ if(!candp || !*candp)
+ return(0);
+
+ cand=*candp;
+
+ nr_ice_candidate_stop_gathering(cand);
+
+ switch(cand->type){
+ case HOST:
+ break;
+#ifdef USE_TURN
+ case RELAYED:
+ // record stats back to the ice ctx on destruction
+ if (cand->u.relayed.turn) {
+ nr_accumulate_count(&(cand->ctx->stats.turn_401s), cand->u.relayed.turn->cnt_401s);
+ nr_accumulate_count(&(cand->ctx->stats.turn_403s), cand->u.relayed.turn->cnt_403s);
+ nr_accumulate_count(&(cand->ctx->stats.turn_438s), cand->u.relayed.turn->cnt_438s);
+
+ nr_turn_stun_ctx* stun_ctx;
+ stun_ctx = STAILQ_FIRST(&cand->u.relayed.turn->stun_ctxs);
+ while (stun_ctx) {
+ nr_accumulate_count(&(cand->ctx->stats.stun_retransmits), stun_ctx->stun->retransmit_ct);
+
+ stun_ctx = STAILQ_NEXT(stun_ctx, entry);
+ }
+ }
+ if (cand->u.relayed.turn_handle)
+ nr_ice_socket_deregister(cand->isock, cand->u.relayed.turn_handle);
+ if (cand->u.relayed.srvflx_candidate)
+ cand->u.relayed.srvflx_candidate->u.srvrflx.relay_candidate=0;
+ nr_turn_client_ctx_destroy(&cand->u.relayed.turn);
+ nr_socket_destroy(&cand->u.relayed.turn_sock);
+ break;
+#endif /* USE_TURN */
+ case SERVER_REFLEXIVE:
+ if (cand->u.srvrflx.stun_handle)
+ nr_ice_socket_deregister(cand->isock, cand->u.srvrflx.stun_handle);
+ if (cand->u.srvrflx.relay_candidate)
+ cand->u.srvrflx.relay_candidate->u.relayed.srvflx_candidate=0;
+ nr_stun_client_ctx_destroy(&cand->u.srvrflx.stun);
+ break;
+ default:
+ break;
+ }
+
+ RFREE(cand->mdns_addr);
+ RFREE(cand->foundation);
+ RFREE(cand->label);
+ RFREE(cand);
+
+ return(0);
+ }
+
+void nr_ice_candidate_stop_gathering(nr_ice_candidate *cand)
+ {
+ if (cand->state == NR_ICE_CAND_STATE_INITIALIZING) {
+ /* Make sure the ICE ctx isn't still waiting around for this candidate
+ * to init. */
+ nr_ice_candidate_mark_done(cand, NR_ICE_CAND_STATE_FAILED);
+ }
+
+ NR_async_timer_cancel(cand->delay_timer);
+ cand->delay_timer=0;
+ NR_async_timer_cancel(cand->ready_cb_timer);
+ cand->ready_cb_timer=0;
+
+ if(cand->resolver_handle){
+ nr_resolver_cancel(cand->ctx->resolver,cand->resolver_handle);
+ cand->resolver_handle=0;
+ }
+ }
+
+/* This algorithm is not super-fast, but I don't think we need a hash
+ table just yet and it produces a small foundation string */
+static int nr_ice_get_foundation(nr_ice_ctx *ctx,nr_ice_candidate *cand)
+ {
+ nr_ice_foundation *foundation;
+ int i=0;
+ char fnd[20];
+ int _status;
+
+ foundation=STAILQ_FIRST(&ctx->foundations);
+ while(foundation){
+ if(nr_transport_addr_cmp(&cand->base,&foundation->addr,NR_TRANSPORT_ADDR_CMP_MODE_ADDR))
+ goto next;
+ // cast necessary because there is no guarantee that enum is signed.
+ // foundation->type should probably match nr_ice_candidate_type
+ if((int)cand->type != foundation->type)
+ goto next;
+ if(cand->type == SERVER_REFLEXIVE || cand->type == RELAYED) {
+ if(nr_transport_addr_cmp(&cand->stun_server->addr, &foundation->stun_server_addr, NR_TRANSPORT_ADDR_CMP_MODE_ADDR))
+ goto next;
+ }
+
+ snprintf(fnd,sizeof(fnd),"%d",i);
+ if(!(cand->foundation=r_strdup(fnd)))
+ ABORT(R_NO_MEMORY);
+ return(0);
+
+ next:
+ foundation=STAILQ_NEXT(foundation,entry);
+ i++;
+ }
+
+ if(!(foundation=RCALLOC(sizeof(nr_ice_foundation))))
+ ABORT(R_NO_MEMORY);
+ nr_transport_addr_copy(&foundation->addr,&cand->base);
+ foundation->type=cand->type;
+ if(cand->type == SERVER_REFLEXIVE || cand->type == RELAYED) {
+ nr_transport_addr_copy(&foundation->stun_server_addr, &cand->stun_server->addr);
+ }
+ STAILQ_INSERT_TAIL(&ctx->foundations,foundation,entry);
+
+ snprintf(fnd,sizeof(fnd),"%d",i);
+ if(!(cand->foundation=r_strdup(fnd)))
+ ABORT(R_NO_MEMORY);
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_ice_candidate_compute_priority(nr_ice_candidate *cand)
+ {
+ UCHAR type_preference;
+ UCHAR interface_preference;
+ UCHAR stun_priority;
+ UCHAR direction_priority=0;
+ int r,_status;
+
+ if (cand->base.protocol != IPPROTO_UDP && cand->base.protocol != IPPROTO_TCP){
+ r_log(LOG_ICE,LOG_ERR,"Unknown protocol type %u",
+ (unsigned int)cand->base.protocol);
+ ABORT(R_INTERNAL);
+ }
+
+ switch(cand->type){
+ case HOST:
+ if(cand->base.protocol == IPPROTO_UDP) {
+ if(r=NR_reg_get_uchar(NR_ICE_REG_PREF_TYPE_HOST,&type_preference))
+ ABORT(r);
+ } else if(cand->base.protocol == IPPROTO_TCP) {
+ if(r=NR_reg_get_uchar(NR_ICE_REG_PREF_TYPE_HOST_TCP,&type_preference))
+ ABORT(r);
+ }
+ stun_priority=0;
+ break;
+ case RELAYED:
+ if(cand->base.protocol == IPPROTO_UDP) {
+ if(r=NR_reg_get_uchar(NR_ICE_REG_PREF_TYPE_RELAYED,&type_preference))
+ ABORT(r);
+ } else if(cand->base.protocol == IPPROTO_TCP) {
+ if(r=NR_reg_get_uchar(NR_ICE_REG_PREF_TYPE_RELAYED_TCP,&type_preference))
+ ABORT(r);
+ }
+ stun_priority=31-cand->stun_server->id;
+ break;
+ case SERVER_REFLEXIVE:
+ if(cand->base.protocol == IPPROTO_UDP) {
+ if(r=NR_reg_get_uchar(NR_ICE_REG_PREF_TYPE_SRV_RFLX,&type_preference))
+ ABORT(r);
+ } else if(cand->base.protocol == IPPROTO_TCP) {
+ if(r=NR_reg_get_uchar(NR_ICE_REG_PREF_TYPE_SRV_RFLX_TCP,&type_preference))
+ ABORT(r);
+ }
+ stun_priority=31-cand->stun_server->id;
+ break;
+ case PEER_REFLEXIVE:
+ if(cand->base.protocol == IPPROTO_UDP) {
+ if(r=NR_reg_get_uchar(NR_ICE_REG_PREF_TYPE_PEER_RFLX,&type_preference))
+ ABORT(r);
+ } else if(cand->base.protocol == IPPROTO_TCP) {
+ if(r=NR_reg_get_uchar(NR_ICE_REG_PREF_TYPE_PEER_RFLX_TCP,&type_preference))
+ ABORT(r);
+ }
+ stun_priority=0;
+ break;
+ default:
+ ABORT(R_INTERNAL);
+ }
+
+ if(cand->base.protocol == IPPROTO_TCP){
+ switch (cand->tcp_type) {
+ case TCP_TYPE_ACTIVE:
+ if (cand->type == HOST)
+ direction_priority=6;
+ else
+ direction_priority=4;
+ break;
+ case TCP_TYPE_PASSIVE:
+ if (cand->type == HOST)
+ direction_priority=4;
+ else
+ direction_priority=2;
+ break;
+ case TCP_TYPE_SO:
+ if (cand->type == HOST)
+ direction_priority=2;
+ else
+ direction_priority=6;
+ break;
+ case TCP_TYPE_NONE:
+ break;
+ case TCP_TYPE_MAX:
+ default:
+ assert(0);
+ ABORT(R_INTERNAL);
+ }
+ }
+
+ if(type_preference > 126)
+ r_log(LOG_ICE,LOG_ERR,"Illegal type preference %d",type_preference);
+
+ if(!cand->ctx->interface_prioritizer) {
+ /* Prioritizer is not set, read from registry */
+ if(r=NR_reg_get2_uchar(NR_ICE_REG_PREF_INTERFACE_PRFX,cand->base.ifname,
+ &interface_preference)) {
+ if (r==R_NOT_FOUND) {
+ if (next_automatic_preference == 1) {
+ r_log(LOG_ICE,LOG_ERR,"Out of preference values. Can't assign one for interface %s",cand->base.ifname);
+ ABORT(R_NOT_FOUND);
+ }
+ r_log(LOG_ICE,LOG_DEBUG,"Automatically assigning preference for interface %s->%d",cand->base.ifname,
+ next_automatic_preference);
+ if (r=NR_reg_set2_uchar(NR_ICE_REG_PREF_INTERFACE_PRFX,cand->base.ifname,next_automatic_preference)){
+ ABORT(r);
+ }
+ interface_preference=next_automatic_preference << 1;
+ next_automatic_preference--;
+ if (cand->base.ip_version == NR_IPV6) {
+ /* Prefer IPV6 over IPV4 on the same interface. */
+ interface_preference += 1;
+ }
+ }
+ else {
+ ABORT(r);
+ }
+ }
+ }
+ else {
+ char key_of_interface[MAXIFNAME + 41];
+ nr_transport_addr addr;
+
+ if(r=nr_socket_getaddr(cand->isock->sock, &addr))
+ ABORT(r);
+
+ if(r=nr_transport_addr_fmt_ifname_addr_string(&addr,key_of_interface,
+ sizeof(key_of_interface))) {
+ ABORT(r);
+ }
+ if(r=nr_interface_prioritizer_get_priority(cand->ctx->interface_prioritizer,
+ key_of_interface,&interface_preference)) {
+ ABORT(r);
+ }
+ }
+
+ assert(stun_priority < 32);
+ assert(direction_priority < 8);
+
+ cand->priority=
+ (type_preference << 24) |
+ (interface_preference << 16) |
+ (direction_priority << 13) |
+ (stun_priority << 8) |
+ (256 - cand->component_id);
+
+ /* S 4.1.2 */
+ assert(cand->priority>=1&&cand->priority<=2147483647);
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+static void nr_ice_candidate_fire_ready_cb(NR_SOCKET s, int how, void *cb_arg)
+ {
+ nr_ice_candidate *cand = cb_arg;
+
+ cand->ready_cb_timer = 0;
+ cand->ready_cb(0, 0, cand->ready_cb_arg);
+ }
+
+static int nr_ice_candidate_use_nr_resolver(nr_transport_addr *addr)
+ {
+ if(addr->fqdn[0] == 0) {
+ return 0;
+ }
+
+ char use = 0;
+ if(addr->protocol == IPPROTO_UDP) {
+ NR_reg_get_char(NR_ICE_REG_USE_NR_RESOLVER_FOR_UDP, &use);
+ } else if(addr->protocol == IPPROTO_TCP) {
+ NR_reg_get_char(NR_ICE_REG_USE_NR_RESOLVER_FOR_TCP, &use);
+ } else {
+ assert(0);
+ }
+
+ return use;
+ }
+
+int nr_ice_candidate_initialize(nr_ice_candidate *cand, NR_async_cb ready_cb, void *cb_arg)
+ {
+ int r,_status;
+ int protocol=NR_RESOLVE_PROTOCOL_STUN;
+ cand->done_cb=ready_cb;
+ cand->cb_arg=cb_arg;
+ cand->state=NR_ICE_CAND_STATE_INITIALIZING;
+
+ switch(cand->type){
+ case HOST:
+ if(r=nr_socket_getaddr(cand->isock->sock,&cand->addr))
+ ABORT(r);
+ cand->osock=cand->isock->sock;
+ // Post this so that it doesn't happen in-line
+ cand->ready_cb = ready_cb;
+ cand->ready_cb_arg = cb_arg;
+ NR_ASYNC_TIMER_SET(0, nr_ice_candidate_fire_ready_cb, (void *)cand, &cand->ready_cb_timer);
+ break;
+#ifdef USE_TURN
+ case RELAYED:
+ protocol=NR_RESOLVE_PROTOCOL_TURN;
+ /* Fall through */
+#endif
+ case SERVER_REFLEXIVE:
+ if (nr_transport_addr_cmp(&cand->base, &cand->stun_server->addr,
+ NR_TRANSPORT_ADDR_CMP_MODE_PROTOCOL)) {
+ r_log(LOG_ICE, LOG_INFO,
+ "ICE-CANDIDATE(%s): Skipping srflx/relayed candidate because "
+ "of IP version/transport mis-match with STUN/TURN server "
+ "(%u/%s - %u/%s).",
+ cand->label, cand->base.ip_version,
+ cand->base.protocol == IPPROTO_UDP ? "UDP" : "TCP",
+ cand->stun_server->addr.ip_version,
+ cand->stun_server->addr.protocol == IPPROTO_UDP ? "UDP"
+ : "TCP");
+ ABORT(R_NOT_FOUND); /* Same error code when DNS lookup fails */
+ }
+
+ if(nr_ice_candidate_use_nr_resolver(&cand->stun_server->addr)) {
+ r_log(
+ LOG_ICE, LOG_DEBUG,
+ "ICE-CANDIDATE(%s): Starting DNS resolution (%u/%s - %u/%s).",
+ cand->label, cand->base.ip_version,
+ cand->base.protocol == IPPROTO_UDP ? "UDP" : "TCP",
+ cand->stun_server->addr.ip_version,
+ cand->stun_server->addr.protocol == IPPROTO_UDP ? "UDP" : "TCP");
+ nr_resolver_resource resource;
+ int port;
+ resource.domain_name = cand->stun_server->addr.fqdn;
+ if (r = nr_transport_addr_get_port(&cand->stun_server->addr, &port)) {
+ ABORT(r);
+ }
+ resource.port = (uint16_t)port;
+ resource.stun_turn=protocol;
+ resource.transport_protocol = cand->stun_server->addr.protocol;
+
+ switch (cand->base.ip_version) {
+ case NR_IPV4:
+ resource.address_family=AF_INET;
+ break;
+ case NR_IPV6:
+ resource.address_family=AF_INET6;
+ break;
+ default:
+ assert(0);
+ ABORT(R_BAD_ARGS);
+ }
+
+ /* Try to resolve */
+ if(!cand->ctx->resolver) {
+ r_log(LOG_ICE, LOG_ERR, "ICE-CANDIDATE(%s): Can't use DNS names without a resolver", cand->label);
+ ABORT(R_BAD_ARGS);
+ }
+
+ if(r=nr_resolver_resolve(cand->ctx->resolver,
+ &resource,
+ nr_ice_candidate_resolved_cb,
+ (void *)cand,
+ &cand->resolver_handle)){
+ r_log(LOG_ICE,LOG_ERR,"ICE-CANDIDATE(%s): Could not invoke DNS resolver",cand->label);
+ ABORT(r);
+ }
+ } else {
+ /* No nr_resolver for this, just copy the address and finish init */
+ if (r = nr_transport_addr_copy(&cand->stun_server_addr,
+ &cand->stun_server->addr)) {
+ r_log(LOG_ICE,LOG_ERR,"ICE-CANDIDATE(%s): Could not copy STUN server addr", cand->label);
+ ABORT(r);
+ }
+
+ if(r=nr_ice_candidate_initialize2(cand))
+ ABORT(r);
+ }
+ break;
+ default:
+ ABORT(R_INTERNAL);
+ }
+
+ nr_ice_candidate_compute_codeword(cand);
+
+ _status=0;
+ abort:
+ if(_status && _status!=R_WOULDBLOCK)
+ nr_ice_candidate_mark_done(cand, NR_ICE_CAND_STATE_FAILED);
+ return(_status);
+ }
+
+
+static int nr_ice_candidate_resolved_cb(void *cb_arg, nr_transport_addr *addr)
+ {
+ nr_ice_candidate *cand=cb_arg;
+ int r,_status;
+
+ cand->resolver_handle=0;
+
+ if(addr){
+ r_log(LOG_ICE,LOG_DEBUG,"ICE(%s): resolved candidate %s. addr=%s",
+ cand->ctx->label,cand->label,addr->as_string);
+ }
+ else {
+ r_log(LOG_ICE,LOG_WARNING,"ICE(%s): failed to resolve candidate %s.",
+ cand->ctx->label,cand->label);
+ ABORT(R_NOT_FOUND);
+ }
+
+ if (nr_transport_addr_check_compatibility(addr, &cand->base)) {
+ r_log(LOG_ICE,LOG_WARNING,"ICE(%s): Skipping STUN server because of link local mis-match for candidate %s",cand->ctx->label,cand->label);
+ ABORT(R_NOT_FOUND);
+ }
+
+ /* Copy the address */
+ if(r=nr_transport_addr_copy(&cand->stun_server_addr,addr))
+ ABORT(r);
+
+ if (cand->tcp_type == TCP_TYPE_PASSIVE || cand->tcp_type == TCP_TYPE_SO){
+ if (r=nr_socket_multi_tcp_stun_server_connect(cand->osock, addr))
+ ABORT(r);
+ }
+
+ /* Now start initializing */
+ if(r=nr_ice_candidate_initialize2(cand))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ if(_status && _status!=R_WOULDBLOCK) {
+ nr_ice_candidate_mark_done(cand, NR_ICE_CAND_STATE_FAILED);
+ }
+ return(_status);
+ }
+
+static int nr_ice_candidate_initialize2(nr_ice_candidate *cand)
+ {
+ int r,_status;
+
+ switch(cand->type){
+ case HOST:
+ assert(0); /* Can't happen */
+ ABORT(R_INTERNAL);
+ break;
+#ifdef USE_TURN
+ case RELAYED:
+ if(r=nr_ice_start_relay_turn(cand))
+ ABORT(r);
+ ABORT(R_WOULDBLOCK);
+ break;
+#endif /* USE_TURN */
+ case SERVER_REFLEXIVE:
+ /* Need to start stun */
+ if(r=nr_ice_srvrflx_start_stun(cand))
+ ABORT(r);
+ cand->osock=cand->isock->sock;
+ ABORT(R_WOULDBLOCK);
+ break;
+ default:
+ ABORT(R_INTERNAL);
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+static void nr_ice_srvrflx_start_stun_timer_cb(NR_SOCKET s, int how, void *cb_arg)
+ {
+ nr_ice_candidate *cand=cb_arg;
+ int r,_status;
+
+ cand->delay_timer=0;
+
+/* TODO: if the response is a BINDING-ERROR-RESPONSE, then restart
+ * TODO: using NR_STUN_CLIENT_MODE_BINDING_REQUEST because the
+ * TODO: server may not have understood the 0.96-style request */
+ if(r=nr_stun_client_start(cand->u.srvrflx.stun, NR_STUN_CLIENT_MODE_BINDING_REQUEST_NO_AUTH, nr_ice_srvrflx_stun_finished_cb, cand))
+ ABORT(r);
+
+ if(r=nr_ice_ctx_remember_id(cand->ctx, cand->u.srvrflx.stun->request))
+ ABORT(r);
+
+ if(r=nr_ice_socket_register_stun_client(cand->isock,cand->u.srvrflx.stun,&cand->u.srvrflx.stun_handle))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ if (_status && (cand->u.srvrflx.stun->state==NR_STUN_CLIENT_STATE_RUNNING)) {
+ nr_stun_client_failed(cand->u.srvrflx.stun);
+ }
+ return;
+ }
+
+static int nr_ice_srvrflx_start_stun(nr_ice_candidate *cand)
+ {
+ int r,_status;
+
+ assert(!cand->delay_timer);
+ if(r=nr_stun_client_ctx_create(cand->label, cand->isock->sock,
+ &cand->stun_server_addr, cand->stream->ctx->gather_rto,
+ &cand->u.srvrflx.stun))
+ ABORT(r);
+
+ NR_ASYNC_TIMER_SET(cand->stream->ctx->stun_delay,nr_ice_srvrflx_start_stun_timer_cb,cand,&cand->delay_timer);
+ cand->stream->ctx->stun_delay += cand->stream->ctx->Ta;
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+#ifdef USE_TURN
+static void nr_ice_start_relay_turn_timer_cb(NR_SOCKET s, int how, void *cb_arg)
+ {
+ nr_ice_candidate *cand=cb_arg;
+ int r,_status;
+
+ cand->delay_timer=0;
+
+ if(r=nr_turn_client_allocate(cand->u.relayed.turn, nr_ice_turn_allocated_cb, cb_arg))
+ ABORT(r);
+
+ if(r=nr_ice_socket_register_turn_client(cand->isock, cand->u.relayed.turn,
+ cand->osock, &cand->u.relayed.turn_handle))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ if(_status && (cand->u.relayed.turn->state==NR_TURN_CLIENT_STATE_ALLOCATING)){
+ nr_turn_client_failed(cand->u.relayed.turn);
+ }
+ return;
+ }
+
+static int nr_ice_start_relay_turn(nr_ice_candidate *cand)
+ {
+ int r,_status;
+ assert(!cand->delay_timer);
+ if(r=nr_turn_client_ctx_create(cand->label, cand->isock->sock,
+ cand->u.relayed.server->username,
+ cand->u.relayed.server->password,
+ &cand->stun_server_addr,
+ cand->component->ctx,
+ &cand->u.relayed.turn))
+ ABORT(r);
+
+ if(r=nr_socket_turn_set_ctx(cand->osock, cand->u.relayed.turn))
+ ABORT(r);
+
+ NR_ASYNC_TIMER_SET(cand->stream->ctx->stun_delay,nr_ice_start_relay_turn_timer_cb,cand,&cand->delay_timer);
+ cand->stream->ctx->stun_delay += cand->stream->ctx->Ta;
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+#endif /* USE_TURN */
+
+static void nr_ice_srvrflx_stun_finished_cb(NR_SOCKET sock, int how, void *cb_arg)
+ {
+ int _status;
+ nr_ice_candidate *cand=cb_arg;
+
+ r_log(LOG_ICE,LOG_DEBUG,"ICE(%s)/CAND(%s): %s",cand->ctx->label,cand->label,__FUNCTION__);
+
+ /* Deregister to suppress duplicates */
+ if(cand->u.srvrflx.stun_handle){ /* This test because we might have failed before CB registered */
+ nr_ice_socket_deregister(cand->isock,cand->u.srvrflx.stun_handle);
+ cand->u.srvrflx.stun_handle=0;
+ }
+
+ switch(cand->u.srvrflx.stun->state){
+ /* OK, we should have a mapped address */
+ case NR_STUN_CLIENT_STATE_DONE:
+ /* Copy the address */
+ nr_transport_addr_copy(&cand->addr, &cand->u.srvrflx.stun->results.stun_binding_response.mapped_addr);
+ cand->addr.protocol=cand->base.protocol;
+ nr_transport_addr_fmt_addr_string(&cand->addr);
+ nr_stun_client_ctx_destroy(&cand->u.srvrflx.stun);
+ nr_ice_candidate_mark_done(cand, NR_ICE_CAND_STATE_INITIALIZED);
+ cand=0;
+ break;
+
+ /* This failed, so go to the next STUN server if there is one */
+ case NR_STUN_CLIENT_STATE_FAILED:
+ ABORT(R_NOT_FOUND);
+ break;
+ default:
+ ABORT(R_INTERNAL);
+ }
+ _status = 0;
+ abort:
+ if(_status){
+ nr_ice_candidate_mark_done(cand, NR_ICE_CAND_STATE_FAILED);
+ }
+ }
+
+#ifdef USE_TURN
+static void nr_ice_turn_allocated_cb(NR_SOCKET s, int how, void *cb_arg)
+ {
+ int r,_status;
+ nr_ice_candidate *cand=cb_arg;
+ nr_turn_client_ctx *turn=cand->u.relayed.turn;
+ char *label;
+ nr_transport_addr relay_addr;
+
+ switch(turn->state){
+ /* OK, we should have a mapped address */
+ case NR_TURN_CLIENT_STATE_ALLOCATED:
+ if (r=nr_turn_client_get_relayed_address(turn, &relay_addr))
+ ABORT(r);
+
+ if(r=nr_concat_strings(&label,"turn-relay(",cand->base.as_string,"|",
+ relay_addr.as_string,")",NULL))
+ ABORT(r);
+
+ r_log(LOG_ICE,LOG_DEBUG,"TURN-CLIENT(%s)/CAND(%s): Switching from TURN to RELAY (%s)",cand->u.relayed.turn->label,cand->label,label);
+
+ /* Copy the relayed address into the candidate addr and
+ into the candidate base. Note that we need to keep the
+ ifname in the base. */
+ if (r=nr_transport_addr_copy(&cand->addr, &relay_addr))
+ ABORT(r);
+ if (r=nr_transport_addr_copy_keep_ifname(&cand->base, &relay_addr)) /* Need to keep interface for priority calculation */
+ ABORT(r);
+
+ r_log(LOG_ICE,LOG_DEBUG,"ICE(%s)/CAND(%s): new relay base=%s addr=%s", cand->ctx->label, cand->label, cand->base.as_string, cand->addr.as_string);
+
+ RFREE(cand->label);
+ cand->label=label;
+ nr_ice_candidate_mark_done(cand, NR_ICE_CAND_STATE_INITIALIZED);
+ cand = 0;
+
+ break;
+
+ case NR_TURN_CLIENT_STATE_FAILED:
+ case NR_TURN_CLIENT_STATE_CANCELLED:
+ r_log(NR_LOG_TURN, LOG_WARNING,
+ "ICE-CANDIDATE(%s): nr_turn_allocated_cb called with state %d",
+ cand->label, turn->state);
+ /* This failed, so go to the next TURN server if there is one */
+ ABORT(R_NOT_FOUND);
+ break;
+ default:
+ assert(0); /* should never happen */
+ ABORT(R_INTERNAL);
+ }
+
+ _status=0;
+ abort:
+ if(_status){
+ if (cand) {
+ r_log(NR_LOG_TURN, LOG_WARNING,
+ "ICE-CANDIDATE(%s): nr_turn_allocated_cb failed", cand->label);
+ nr_ice_candidate_mark_done(cand, NR_ICE_CAND_STATE_FAILED);
+ }
+ }
+ }
+#endif /* USE_TURN */
+
+/* Format the candidate attribute as per ICE S 15.1 */
+int nr_ice_format_candidate_attribute(nr_ice_candidate *cand, char *attr, int maxlen, int obfuscate_srflx_addr)
+ {
+ int r,_status;
+ char addr[64];
+ int port;
+ int len;
+ nr_transport_addr *raddr;
+
+ assert(!strcmp(nr_ice_candidate_type_names[HOST], "host"));
+ assert(!strcmp(nr_ice_candidate_type_names[RELAYED], "relay"));
+
+ if (cand->mdns_addr) {
+ /* mdns_addr is NSID_LENGTH which is 39, - 2 for removing the "{" and "}"
+ + 6 for ".local" for a total of 43. */
+ strncpy(addr, cand->mdns_addr, sizeof(addr) - 1);
+ } else {
+ if(r=nr_transport_addr_get_addrstring(&cand->addr,addr,sizeof(addr)))
+ ABORT(r);
+ }
+ if(r=nr_transport_addr_get_port(&cand->addr,&port))
+ ABORT(r);
+ /* https://tools.ietf.org/html/rfc6544#section-4.5 */
+ if (cand->base.protocol==IPPROTO_TCP && cand->tcp_type==TCP_TYPE_ACTIVE)
+ port=9;
+ snprintf(attr,maxlen,"candidate:%s %d %s %u %s %d typ %s",
+ cand->foundation, cand->component_id, cand->addr.protocol==IPPROTO_UDP?"UDP":"TCP",cand->priority, addr, port,
+ nr_ctype_name(cand->type));
+
+ len=strlen(attr); attr+=len; maxlen-=len;
+
+ /* raddr, rport */
+ raddr = (cand->stream->flags &
+ (NR_ICE_CTX_FLAGS_RELAY_ONLY |
+ NR_ICE_CTX_FLAGS_DISABLE_HOST_CANDIDATES)) ?
+ &cand->addr : &cand->base;
+
+ switch(cand->type){
+ case HOST:
+ break;
+ case SERVER_REFLEXIVE:
+ if (obfuscate_srflx_addr) {
+ snprintf(attr,maxlen," raddr 0.0.0.0 rport 0");
+ } else {
+ if(r=nr_transport_addr_get_addrstring(raddr,addr,sizeof(addr)))
+ ABORT(r);
+ if(r=nr_transport_addr_get_port(raddr,&port))
+ ABORT(r);
+ snprintf(attr,maxlen," raddr %s rport %d",addr,port);
+ }
+ break;
+ case PEER_REFLEXIVE:
+ if(r=nr_transport_addr_get_addrstring(raddr,addr,sizeof(addr)))
+ ABORT(r);
+ if(r=nr_transport_addr_get_port(raddr,&port))
+ ABORT(r);
+ snprintf(attr,maxlen," raddr %s rport %d",addr,port);
+ break;
+ case RELAYED:
+ // comes from XorMappedAddress via AllocateResponse
+ if(r=nr_transport_addr_get_addrstring(raddr,addr,sizeof(addr)))
+ ABORT(r);
+ if(r=nr_transport_addr_get_port(raddr,&port))
+ ABORT(r);
+
+ snprintf(attr,maxlen," raddr %s rport %d",addr,port);
+ break;
+ default:
+ assert(0);
+ ABORT(R_INTERNAL);
+ break;
+ }
+
+ if (cand->base.protocol==IPPROTO_TCP && cand->tcp_type){
+ len=strlen(attr);
+ attr+=len;
+ maxlen-=len;
+ snprintf(attr,maxlen," tcptype %s",nr_tcp_type_name(cand->tcp_type));
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_candidate.h b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_candidate.h
new file mode 100644
index 0000000000..40e6545e37
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_candidate.h
@@ -0,0 +1,124 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+
+#ifndef _ice_candidate_h
+#define _ice_candidate_h
+#ifdef __cplusplus
+using namespace std;
+extern "C" {
+#endif /* __cplusplus */
+
+typedef enum {HOST=1, SERVER_REFLEXIVE, PEER_REFLEXIVE, RELAYED, CTYPE_MAX} nr_ice_candidate_type;
+
+struct nr_ice_candidate_ {
+ char *label;
+ char codeword[5];
+ int state;
+ int trickled;
+#define NR_ICE_CAND_STATE_CREATED 1
+#define NR_ICE_CAND_STATE_INITIALIZING 2
+#define NR_ICE_CAND_STATE_INITIALIZED 3
+#define NR_ICE_CAND_STATE_FAILED 4
+#define NR_ICE_CAND_PEER_CANDIDATE_UNPAIRED 9
+#define NR_ICE_CAND_PEER_CANDIDATE_PAIRED 10
+ struct nr_ice_ctx_ *ctx;
+ nr_ice_socket *isock; /* The socket to read from
+ (it contains all other candidates
+ on this socket) */
+ nr_socket *osock; /* The socket to write to */
+ nr_ice_media_stream *stream; /* The media stream this is associated with */
+ nr_ice_component *component; /* The component this is associated with */
+ nr_ice_candidate_type type; /* The type of the candidate (S 4.1.1) */
+ nr_socket_tcp_type tcp_type;
+ UCHAR component_id; /* The component id (S 4.1.2.1) */
+ nr_transport_addr addr; /* The advertised address;
+ JDR calls this the candidate */
+ nr_transport_addr base; /* The base address (S 2.1)*/
+ char *mdns_addr; /* MDNS address, if any */
+ char *foundation; /* Foundation for the candidate (S 4) */
+ UINT4 priority; /* The priority value (S 5.4 */
+ nr_ice_stun_server *stun_server;
+ nr_transport_addr stun_server_addr; /* Resolved STUN server address */
+ void *delay_timer;
+ void *resolver_handle;
+
+ /* Holding data for STUN and TURN */
+ union {
+ struct {
+ nr_stun_client_ctx *stun;
+ void *stun_handle;
+ /* If this is a srflx that is piggybacking on a relay candidate, this is
+ * a back pointer to that relay candidate. */
+ nr_ice_candidate *relay_candidate;
+ } srvrflx;
+ struct {
+ nr_turn_client_ctx *turn;
+ nr_ice_turn_server *server;
+ nr_ice_candidate *srvflx_candidate;
+ nr_socket *turn_sock;
+ void *turn_handle;
+ } relayed;
+ } u;
+
+ NR_async_cb done_cb;
+ void *cb_arg;
+
+ NR_async_cb ready_cb;
+ void *ready_cb_arg;
+ void *ready_cb_timer;
+
+ TAILQ_ENTRY(nr_ice_candidate_) entry_sock;
+ TAILQ_ENTRY(nr_ice_candidate_) entry_comp;
+};
+
+extern char *nr_ice_candidate_type_names[];
+extern char *nr_ice_candidate_tcp_type_names[];
+
+
+int nr_ice_candidate_create(struct nr_ice_ctx_ *ctx,nr_ice_component *component, nr_ice_socket *isock, nr_socket *osock, nr_ice_candidate_type ctype, nr_socket_tcp_type tcp_type, nr_ice_stun_server *stun_server, UCHAR component_id, nr_ice_candidate **candp);
+int nr_ice_candidate_initialize(nr_ice_candidate *cand, NR_async_cb ready_cb, void *cb_arg);
+void nr_ice_candidate_compute_codeword(nr_ice_candidate *cand);
+int nr_ice_candidate_process_stun(nr_ice_candidate *cand, UCHAR *msg, int len, nr_transport_addr *faddr);
+int nr_ice_candidate_destroy(nr_ice_candidate **candp);
+void nr_ice_candidate_stop_gathering(nr_ice_candidate *cand);
+int nr_ice_format_candidate_attribute(nr_ice_candidate *cand, char *attr, int maxlen, int obfuscate_srflx_addr);
+int nr_ice_peer_candidate_from_attribute(nr_ice_ctx *ctx,char *attr,nr_ice_media_stream *stream,nr_ice_candidate **candp);
+int nr_ice_peer_peer_rflx_candidate_create(nr_ice_ctx *ctx,char *label, nr_ice_component *comp,nr_transport_addr *addr, nr_ice_candidate **candp);
+int nr_ice_candidate_compute_priority(nr_ice_candidate *cand);
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_candidate_pair.c b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_candidate_pair.c
new file mode 100644
index 0000000000..6a7233963d
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_candidate_pair.c
@@ -0,0 +1,689 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <assert.h>
+#include <string.h>
+#include <nr_api.h>
+#include "async_timer.h"
+#include "ice_ctx.h"
+#include "ice_util.h"
+#include "ice_codeword.h"
+#include "stun.h"
+
+static char *nr_ice_cand_pair_states[]={"UNKNOWN","FROZEN","WAITING","IN_PROGRESS","FAILED","SUCCEEDED","CANCELLED"};
+
+static void nr_ice_candidate_pair_restart_stun_role_change_cb(NR_SOCKET s, int how, void *cb_arg);
+static void nr_ice_candidate_pair_compute_codeword(nr_ice_cand_pair *pair,
+ nr_ice_candidate *lcand, nr_ice_candidate *rcand);
+
+static void nr_ice_candidate_pair_set_priority(nr_ice_cand_pair *pair)
+ {
+ /* Priority computation S 5.7.2 */
+ UINT8 controlling_priority, controlled_priority;
+ if(pair->pctx->controlling)
+ {
+ controlling_priority=pair->local->priority;
+ controlled_priority=pair->remote->priority;
+ }
+ else{
+ controlling_priority=pair->remote->priority;
+ controlled_priority=pair->local->priority;
+ }
+ pair->priority=(MIN(controlling_priority, controlled_priority))<<32 |
+ (MAX(controlling_priority, controlled_priority))<<1 |
+ (controlled_priority > controlling_priority?0:1);
+ }
+
+int nr_ice_candidate_pair_create(nr_ice_peer_ctx *pctx, nr_ice_candidate *lcand,nr_ice_candidate *rcand,nr_ice_cand_pair **pairp)
+ {
+ nr_ice_cand_pair *pair=0;
+ int r,_status;
+ UINT4 RTO;
+ nr_ice_candidate tmpcand;
+ UINT8 t_priority;
+
+ if(!(pair=RCALLOC(sizeof(nr_ice_cand_pair))))
+ ABORT(R_NO_MEMORY);
+
+ pair->pctx=pctx;
+
+ nr_ice_candidate_pair_compute_codeword(pair,lcand,rcand);
+
+ if(r=nr_concat_strings(&pair->as_string,pair->codeword,"|",lcand->addr.as_string,"|",
+ rcand->addr.as_string,"(",lcand->label,"|",rcand->label,")", NULL))
+ ABORT(r);
+
+ nr_ice_candidate_pair_set_state(pctx,pair,NR_ICE_PAIR_STATE_FROZEN);
+ pair->local=lcand;
+ pair->remote=rcand;
+
+ nr_ice_candidate_pair_set_priority(pair);
+
+ /*
+ TODO(bcampen@mozilla.com): Would be nice to log why this candidate was
+ created (initial pair generation, triggered check, and new trickle
+ candidate seem to be the possibilities here).
+ */
+ r_log(LOG_ICE,LOG_INFO,"ICE(%s)/CAND-PAIR(%s): Pairing candidate %s (%x):%s (%x) priority=%llu (%llx)",pctx->ctx->label,pair->codeword,lcand->addr.as_string,lcand->priority,rcand->addr.as_string,rcand->priority,pair->priority,pair->priority);
+
+ /* Foundation */
+ if(r=nr_concat_strings(&pair->foundation,lcand->foundation,"|",
+ rcand->foundation,NULL))
+ ABORT(r);
+
+ /* Compute the RTO per S 16 */
+ RTO = MAX(100, (pctx->ctx->Ta * pctx->waiting_pairs));
+
+ /* Make a bogus candidate to compute a theoretical peer reflexive
+ * priority per S 7.1.1.1 */
+ memcpy(&tmpcand, lcand, sizeof(tmpcand));
+ tmpcand.type = PEER_REFLEXIVE;
+ if (r=nr_ice_candidate_compute_priority(&tmpcand))
+ ABORT(r);
+ t_priority = tmpcand.priority;
+
+ /* Our sending context */
+ if(r=nr_stun_client_ctx_create(pair->as_string,
+ lcand->osock,
+ &rcand->addr,RTO,&pair->stun_client))
+ ABORT(r);
+ if(!(pair->stun_client->params.ice_binding_request.username=r_strdup(rcand->stream->l2r_user)))
+ ABORT(R_NO_MEMORY);
+ if(r=r_data_copy(&pair->stun_client->params.ice_binding_request.password,
+ &rcand->stream->l2r_pass))
+ ABORT(r);
+ /* TODO(ekr@rtfm.com): Do we need to frob this when we change role. Bug 890667 */
+ pair->stun_client->params.ice_binding_request.control = pctx->controlling?
+ NR_ICE_CONTROLLING:NR_ICE_CONTROLLED;
+ pair->stun_client->params.ice_binding_request.priority=t_priority;
+
+ pair->stun_client->params.ice_binding_request.tiebreaker=pctx->tiebreaker;
+
+ *pairp=pair;
+
+ _status=0;
+ abort:
+ if(_status){
+ nr_ice_candidate_pair_destroy(&pair);
+ }
+ return(_status);
+ }
+
+int nr_ice_candidate_pair_destroy(nr_ice_cand_pair **pairp)
+ {
+ nr_ice_cand_pair *pair;
+
+ if(!pairp || !*pairp)
+ return(0);
+
+ pair=*pairp;
+ *pairp=0;
+
+ // record stats back to the ice ctx on destruction
+ if (pair->stun_client) {
+ nr_accumulate_count(&(pair->local->ctx->stats.stun_retransmits), pair->stun_client->retransmit_ct);
+ }
+
+ RFREE(pair->as_string);
+ RFREE(pair->foundation);
+ nr_ice_socket_deregister(pair->local->isock,pair->stun_client_handle);
+ if (pair->stun_client) {
+ RFREE(pair->stun_client->params.ice_binding_request.username);
+ RFREE(pair->stun_client->params.ice_binding_request.password.data);
+ nr_stun_client_ctx_destroy(&pair->stun_client);
+ }
+
+ NR_async_timer_cancel(pair->stun_cb_timer);
+ NR_async_timer_cancel(pair->restart_role_change_cb_timer);
+ NR_async_timer_cancel(pair->restart_nominated_cb_timer);
+
+ RFREE(pair);
+ return(0);
+ }
+
+int nr_ice_candidate_pair_unfreeze(nr_ice_peer_ctx *pctx, nr_ice_cand_pair *pair)
+ {
+ assert(pair->state==NR_ICE_PAIR_STATE_FROZEN);
+
+ nr_ice_candidate_pair_set_state(pctx,pair,NR_ICE_PAIR_STATE_WAITING);
+
+ return(0);
+ }
+
+static void nr_ice_candidate_pair_stun_cb(NR_SOCKET s, int how, void *cb_arg)
+ {
+ int r,_status;
+ nr_ice_cand_pair *pair=cb_arg;
+ nr_ice_cand_pair *actual_pair=0;
+ nr_ice_candidate *cand=0;
+ nr_stun_message *sres;
+ nr_transport_addr *request_src;
+ nr_transport_addr *request_dst;
+ nr_transport_addr *response_src;
+ nr_transport_addr response_dst;
+ nr_stun_message_attribute *attr;
+
+ pair->stun_cb_timer=0;
+
+ r_log(LOG_ICE,LOG_DEBUG,"ICE-PEER(%s)/STREAM(%s)/CAND-PAIR(%s): STUN cb on pair addr = %s",
+ pair->pctx->label,pair->local->stream->label,pair->codeword,pair->as_string);
+
+ /* This ordinarily shouldn't happen, but can if we're
+ doing the second check to confirm nomination.
+ Just bail out */
+ if(pair->state==NR_ICE_PAIR_STATE_SUCCEEDED)
+ goto done;
+
+ switch(pair->stun_client->state){
+ case NR_STUN_CLIENT_STATE_FAILED:
+ sres=pair->stun_client->response;
+ if(sres && nr_stun_message_has_attribute(sres,NR_STUN_ATTR_ERROR_CODE,&attr)&&attr->u.error_code.number==487){
+
+ /*
+ * Flip the controlling bit; subsequent 487s for other pairs will be
+ * ignored, since we abandon their STUN transactions.
+ */
+ nr_ice_peer_ctx_switch_controlling_role(pair->pctx);
+
+ return;
+ }
+ /* Fall through */
+ case NR_STUN_CLIENT_STATE_TIMED_OUT:
+ nr_ice_candidate_pair_set_state(pair->pctx,pair,NR_ICE_PAIR_STATE_FAILED);
+ break;
+ case NR_STUN_CLIENT_STATE_DONE:
+ /* make sure the addresses match up S 7.1.2.2 */
+ response_src=&pair->stun_client->peer_addr;
+ request_dst=&pair->remote->addr;
+ if (nr_transport_addr_cmp(response_src,request_dst,NR_TRANSPORT_ADDR_CMP_MODE_ALL)){
+ r_log(LOG_ICE,LOG_WARNING,"ICE-PEER(%s)/CAND-PAIR(%s): Peer address mismatch %s != %s",pair->pctx->label,pair->codeword,response_src->as_string,request_dst->as_string);
+ nr_ice_candidate_pair_set_state(pair->pctx,pair,NR_ICE_PAIR_STATE_FAILED);
+ break;
+ }
+ request_src=&pair->stun_client->my_addr;
+ nr_socket_getaddr(pair->local->osock,&response_dst);
+ if (nr_transport_addr_cmp(request_src,&response_dst,NR_TRANSPORT_ADDR_CMP_MODE_ALL)){
+ r_log(LOG_ICE,LOG_WARNING,"ICE-PEER(%s)/CAND-PAIR(%s): Local address mismatch %s != %s",pair->pctx->label,pair->codeword,request_src->as_string,response_dst.as_string);
+ nr_ice_candidate_pair_set_state(pair->pctx,pair,NR_ICE_PAIR_STATE_FAILED);
+ break;
+ }
+
+ if(strlen(pair->stun_client->results.ice_binding_response.mapped_addr.as_string)==0){
+ /* we're using the mapped_addr returned by the server to lookup our
+ * candidate, but if the server fails to do that we can't perform
+ * the lookup -- this may be a BUG because if we've gotten here
+ * then the transaction ID check succeeded, and perhaps we should
+ * just assume that it's the server we're talking to and that our
+ * peer is ok, but I'm not sure how that'll interact with the
+ * peer reflexive logic below */
+ r_log(LOG_ICE,LOG_WARNING,"ICE-PEER(%s)/CAND-PAIR(%s): server failed to return mapped address on pair %s", pair->pctx->label,pair->codeword,pair->as_string);
+ nr_ice_candidate_pair_set_state(pair->pctx,pair,NR_ICE_PAIR_STATE_FAILED);
+ break;
+ }
+ else if(!nr_transport_addr_cmp(&pair->local->addr,&pair->stun_client->results.ice_binding_response.mapped_addr,NR_TRANSPORT_ADDR_CMP_MODE_ALL)){
+ nr_ice_candidate_pair_set_state(pair->pctx,pair,NR_ICE_PAIR_STATE_SUCCEEDED);
+ }
+ else if(pair->stun_client->state == NR_STUN_CLIENT_STATE_DONE) {
+ /* OK, this didn't correspond to a pair on the check list, but
+ it probably matches one of our candidates */
+
+ cand=TAILQ_FIRST(&pair->local->component->candidates);
+ while(cand){
+ if(!nr_transport_addr_cmp(&cand->addr,&pair->stun_client->results.ice_binding_response.mapped_addr,NR_TRANSPORT_ADDR_CMP_MODE_ALL)) {
+ r_log(LOG_ICE,LOG_DEBUG,"ICE-PEER(%s): found pre-existing local candidate of type %d for mapped address %s", pair->pctx->label,cand->type,cand->addr.as_string);
+ assert(cand->type != HOST);
+ break;
+ }
+
+ cand=TAILQ_NEXT(cand,entry_comp);
+ }
+
+ if(!cand) {
+ /* OK, nothing found, must be a new peer reflexive */
+ if (pair->local->stream->flags & NR_ICE_CTX_FLAGS_RELAY_ONLY) {
+ /* Any STUN response with a reflexive address in it is unwanted
+ when we'll send on relay only. Bail since cand is used below. */
+ goto done;
+ }
+ if(r=nr_ice_candidate_create(pair->pctx->ctx,
+ pair->local->component,pair->local->isock,pair->local->osock,
+ PEER_REFLEXIVE,pair->local->tcp_type,0,pair->local->component->component_id,&cand))
+ ABORT(r);
+ if(r=nr_transport_addr_copy(&cand->addr,&pair->stun_client->results.ice_binding_response.mapped_addr))
+ ABORT(r);
+ cand->state=NR_ICE_CAND_STATE_INITIALIZED;
+ TAILQ_INSERT_TAIL(&pair->local->component->candidates,cand,entry_comp);
+ } else {
+ /* Check if we have a pair for this candidate already. */
+ if(r=nr_ice_media_stream_find_pair(pair->remote->stream, cand, pair->remote, &actual_pair)) {
+ r_log(LOG_ICE,LOG_DEBUG,"ICE-PEER(%s): no pair exists for %s and %s", pair->pctx->label,cand->addr.as_string, pair->remote->addr.as_string);
+ }
+ }
+
+ if(!actual_pair) {
+ if(r=nr_ice_candidate_pair_create(pair->pctx,cand,pair->remote, &actual_pair))
+ ABORT(r);
+
+ if(r=nr_ice_component_insert_pair(actual_pair->remote->component,actual_pair))
+ ABORT(r);
+
+ /* If the original pair was nominated, make us nominated too. */
+ if(pair->peer_nominated)
+ actual_pair->peer_nominated=1;
+
+ /* Now mark the orig pair failed */
+ nr_ice_candidate_pair_set_state(pair->pctx,pair,NR_ICE_PAIR_STATE_FAILED);
+ }
+
+ assert(actual_pair);
+ nr_ice_candidate_pair_set_state(actual_pair->pctx,actual_pair,NR_ICE_PAIR_STATE_SUCCEEDED);
+ pair=actual_pair;
+
+ }
+
+ /* Should we set nominated? */
+ if(pair->pctx->controlling){
+ if(pair->pctx->ctx->flags & NR_ICE_CTX_FLAGS_AGGRESSIVE_NOMINATION)
+ pair->nominated=1;
+ }
+ else{
+ if(pair->peer_nominated)
+ pair->nominated=1;
+ }
+
+
+ /* increment the number of valid pairs in the component */
+ /* We don't bother to maintain a separate valid list */
+ pair->remote->component->valid_pairs++;
+
+ /* S 7.1.2.2: unfreeze other pairs with the same foundation*/
+ if(r=nr_ice_media_stream_unfreeze_pairs_foundation(pair->remote->stream,pair->foundation))
+ ABORT(r);
+
+ /* Deal with this pair being nominated */
+ if(pair->nominated){
+ if(r=nr_ice_component_nominated_pair(pair->remote->component, pair))
+ ABORT(r);
+ }
+
+ break;
+ default:
+ ABORT(R_INTERNAL);
+ }
+
+ /* If we're controlling but in regular mode, ask the handler
+ if he wants to nominate something and stop... */
+ if(pair->pctx->controlling && !(pair->pctx->ctx->flags & NR_ICE_CTX_FLAGS_AGGRESSIVE_NOMINATION)){
+
+ if(r=nr_ice_component_select_pair(pair->pctx,pair->remote->component)){
+ if(r!=R_NOT_FOUND)
+ ABORT(r);
+ }
+ }
+
+ done:
+ _status=0;
+ abort:
+ if (_status) {
+ // cb doesn't return anything, but we should probably log that we aborted
+ // This also quiets the unused variable warnings.
+ r_log(LOG_ICE,LOG_DEBUG,"ICE-PEER(%s)/STREAM(%s)/CAND-PAIR(%s): STUN cb pair addr = %s abort with status: %d",
+ pair->pctx->label,pair->local->stream->label,pair->codeword,pair->as_string, _status);
+ }
+ return;
+ }
+
+static void nr_ice_candidate_pair_restart(nr_ice_peer_ctx *pctx, nr_ice_cand_pair *pair)
+ {
+ int r,_status;
+ UINT4 mode;
+
+ nr_ice_candidate_pair_set_state(pctx,pair,NR_ICE_PAIR_STATE_IN_PROGRESS);
+
+ /* Start STUN */
+ if(pair->pctx->controlling && (pair->pctx->ctx->flags & NR_ICE_CTX_FLAGS_AGGRESSIVE_NOMINATION))
+ mode=NR_ICE_CLIENT_MODE_USE_CANDIDATE;
+ else
+ mode=NR_ICE_CLIENT_MODE_BINDING_REQUEST;
+
+ nr_stun_client_reset(pair->stun_client);
+
+ if(r=nr_stun_client_start(pair->stun_client,mode,nr_ice_candidate_pair_stun_cb,pair))
+ ABORT(r);
+
+ if ((r=nr_ice_ctx_remember_id(pair->pctx->ctx, pair->stun_client->request))) {
+ /* ignore if this fails (which it shouldn't) because it's only an
+ * optimization and the cleanup routines are not going to do the right
+ * thing if this fails */
+ assert(0);
+ }
+
+ _status=0;
+ abort:
+ if(_status){
+ /* Don't fire the CB, but schedule it to fire ASAP */
+ assert(!pair->stun_cb_timer);
+ NR_ASYNC_TIMER_SET(0,nr_ice_candidate_pair_stun_cb,pair, &pair->stun_cb_timer);
+ _status=0;
+ }
+ }
+
+int nr_ice_candidate_pair_start(nr_ice_peer_ctx *pctx, nr_ice_cand_pair *pair)
+ {
+ int r,_status;
+
+ /* Register the stun ctx for when responses come in*/
+ if(r=nr_ice_socket_register_stun_client(pair->local->isock,pair->stun_client,&pair->stun_client_handle))
+ ABORT(r);
+
+ nr_ice_candidate_pair_restart(pctx, pair);
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+static int nr_ice_candidate_copy_for_triggered_check(nr_ice_cand_pair *pair)
+ {
+ int r,_status;
+ nr_ice_cand_pair *copy;
+
+ if(r=nr_ice_candidate_pair_create(pair->pctx, pair->local, pair->remote, &copy))
+ ABORT(r);
+
+ /* Preserve nomination status */
+ copy->peer_nominated= pair->peer_nominated;
+ copy->nominated = pair->nominated;
+
+ r_log(LOG_ICE,LOG_INFO,"CAND-PAIR(%s): Adding pair to check list and trigger check queue: %s",pair->codeword,pair->as_string);
+ nr_ice_candidate_pair_insert(&pair->remote->stream->check_list,copy);
+ nr_ice_candidate_pair_trigger_check_append(&pair->remote->stream->trigger_check_queue,copy);
+
+ copy->triggered = 1;
+ nr_ice_candidate_pair_set_state(copy->pctx,copy,NR_ICE_PAIR_STATE_WAITING);
+
+ _status=0;
+ abort:
+ return(_status);
+}
+
+int nr_ice_candidate_pair_do_triggered_check(nr_ice_peer_ctx *pctx, nr_ice_cand_pair *pair)
+ {
+ int r,_status;
+
+ if(pair->state==NR_ICE_PAIR_STATE_CANCELLED) {
+ r_log(LOG_ICE,LOG_DEBUG,"ICE-PEER(%s)/CAND_PAIR(%s): Ignoring matching but canceled pair",pctx->label,pair->codeword);
+ return(0);
+ } else if(pair->state==NR_ICE_PAIR_STATE_SUCCEEDED) {
+ r_log(LOG_ICE,LOG_DEBUG,"ICE-PEER(%s)/CAND_PAIR(%s): No new trigger check for succeeded pair",pctx->label,pair->codeword);
+ return(0);
+ } else if (pair->local->stream->obsolete) {
+ r_log(LOG_ICE, LOG_DEBUG,
+ "ICE-PEER(%s)/CAND_PAIR(%s): No new trigger check for pair with "
+ "obsolete stream",
+ pctx->label, pair->codeword);
+ return (0);
+ }
+
+ /* Do not run this logic more than once on a given pair */
+ if(!pair->triggered){
+ r_log(LOG_ICE,LOG_INFO,"ICE-PEER(%s)/CAND-PAIR(%s): triggered check on %s",pctx->label,pair->codeword,pair->as_string);
+
+ pair->triggered=1;
+
+ switch(pair->state){
+ case NR_ICE_PAIR_STATE_FAILED:
+ /* OK, there was a pair, it's just invalid: According to Section
+ * 7.2.1.4, we need to resurrect it */
+ r_log(LOG_ICE,LOG_INFO,"ICE-PEER(%s)/CAND-PAIR(%s): received STUN check on failed pair, resurrecting: %s",pctx->label,pair->codeword,pair->as_string);
+ /* fall through */
+ case NR_ICE_PAIR_STATE_FROZEN:
+ nr_ice_candidate_pair_set_state(pctx,pair,NR_ICE_PAIR_STATE_WAITING);
+ /* fall through even further */
+ case NR_ICE_PAIR_STATE_WAITING:
+ /* Append it additionally to the trigger check queue */
+ r_log(LOG_ICE,LOG_INFO,"ICE-PEER(%s)/CAND-PAIR(%s): Inserting pair to trigger check queue: %s",pctx->label,pair->codeword,pair->as_string);
+ nr_ice_candidate_pair_trigger_check_append(&pair->remote->stream->trigger_check_queue,pair);
+ break;
+ case NR_ICE_PAIR_STATE_IN_PROGRESS:
+ /* Instead of trying to maintain two stun contexts on the same pair,
+ * and handling heterogenous responses and error conditions, we instead
+ * create a second pair that is identical except that it has the
+ * |triggered| bit set. We also cancel the original pair, but it can
+ * still succeed on its own in the special waiting state. */
+ if(r=nr_ice_candidate_copy_for_triggered_check(pair))
+ ABORT(r);
+ nr_ice_candidate_pair_cancel(pair->pctx,pair,1);
+ break;
+ default:
+ /* all states are handled - a new/unknown state should not
+ * automatically enter the start_checks() below */
+ assert(0);
+ break;
+ }
+
+ /* Ensure that the timers are running to start checks on the topmost entry
+ * of the triggered check queue. */
+ if(r=nr_ice_media_stream_start_checks(pair->pctx,pair->remote->stream))
+ ABORT(r);
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+void nr_ice_candidate_pair_cancel(nr_ice_peer_ctx *pctx,nr_ice_cand_pair *pair, int move_to_wait_state)
+ {
+ if(pair->state != NR_ICE_PAIR_STATE_FAILED){
+ /* If it's already running we need to terminate the stun */
+ if(pair->state==NR_ICE_PAIR_STATE_IN_PROGRESS){
+ if(move_to_wait_state) {
+ nr_stun_client_wait(pair->stun_client);
+ } else {
+ nr_stun_client_cancel(pair->stun_client);
+ }
+ }
+ nr_ice_candidate_pair_set_state(pctx,pair,NR_ICE_PAIR_STATE_CANCELLED);
+ }
+ }
+
+int nr_ice_candidate_pair_select(nr_ice_cand_pair *pair)
+ {
+ int r,_status;
+
+ if(!pair){
+ r_log(LOG_ICE,LOG_ERR,"ICE-PAIR: No pair chosen");
+ ABORT(R_BAD_ARGS);
+ }
+
+ if(pair->state!=NR_ICE_PAIR_STATE_SUCCEEDED){
+ r_log(LOG_ICE,LOG_ERR,"ICE-PEER(%s)/CAND-PAIR(%s): tried to install non-succeeded pair, ignoring: %s",pair->pctx->label,pair->codeword,pair->as_string);
+ }
+ else{
+ /* Ok, they chose one */
+ /* 1. Send a new request with nominated. Do it as a scheduled
+ event to avoid reentrancy issues. Only do this if it hasn't
+ happened already (though this shouldn't happen.)
+ */
+ if(!pair->restart_nominated_cb_timer)
+ NR_ASYNC_TIMER_SET(0,nr_ice_candidate_pair_restart_stun_nominated_cb,pair,&pair->restart_nominated_cb_timer);
+
+ /* 2. Tell ourselves this pair is ready */
+ if(r=nr_ice_component_nominated_pair(pair->remote->component, pair))
+ ABORT(r);
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+void nr_ice_candidate_pair_set_state(nr_ice_peer_ctx *pctx, nr_ice_cand_pair *pair, int state)
+ {
+ r_log(LOG_ICE,LOG_INFO,"ICE-PEER(%s)/CAND-PAIR(%s): setting pair to state %s: %s",
+ pctx->label,pair->codeword,nr_ice_cand_pair_states[state],pair->as_string);
+
+ /* NOTE: This function used to reference pctx->state instead of
+ pair->state and the assignment to pair->state was at the top
+ of this function. Because pctx->state was never changed, this seems to have
+ been a typo. The natural logic is "if the state changed
+ decrement the counter" so this implies we should be checking
+ the pair state rather than the pctx->state.
+
+ This didn't cause big problems because waiting_pairs was only
+ used for pacing, so the pacing just was kind of broken.
+
+ This note is here as a reminder until we do more testing
+ and make sure that in fact this was a typo.
+ */
+ if(pair->state!=NR_ICE_PAIR_STATE_WAITING){
+ if(state==NR_ICE_PAIR_STATE_WAITING)
+ pctx->waiting_pairs++;
+ }
+ else{
+ if(state!=NR_ICE_PAIR_STATE_WAITING)
+ pctx->waiting_pairs--;
+
+ assert(pctx->waiting_pairs>=0);
+ }
+ pair->state=state;
+
+
+ if(pair->state==NR_ICE_PAIR_STATE_FAILED ||
+ pair->state==NR_ICE_PAIR_STATE_CANCELLED){
+ nr_ice_component_failed_pair(pair->remote->component, pair);
+ }
+ }
+
+void nr_ice_candidate_pair_dump_state(nr_ice_cand_pair *pair, int log_level)
+ {
+ r_log(LOG_ICE,log_level,"CAND-PAIR(%s): pair %s: state=%s, priority=0x%llx\n",pair->codeword,pair->as_string,nr_ice_cand_pair_states[pair->state],pair->priority);
+ }
+
+
+int nr_ice_candidate_pair_trigger_check_append(nr_ice_cand_pair_head *head,nr_ice_cand_pair *pair)
+ {
+ if(pair->triggered_check_queue_entry.tqe_next ||
+ pair->triggered_check_queue_entry.tqe_prev)
+ return(0);
+
+ TAILQ_INSERT_TAIL(head,pair,triggered_check_queue_entry);
+
+ return(0);
+ }
+
+void nr_ice_candidate_pair_insert(nr_ice_cand_pair_head *head,nr_ice_cand_pair *pair)
+ {
+ nr_ice_cand_pair *c1;
+
+ c1=TAILQ_FIRST(head);
+ while(c1){
+ if(c1->priority < pair->priority){
+ TAILQ_INSERT_BEFORE(c1,pair,check_queue_entry);
+ break;
+ }
+
+ c1=TAILQ_NEXT(c1,check_queue_entry);
+ }
+ if(!c1) TAILQ_INSERT_TAIL(head,pair,check_queue_entry);
+ }
+
+void nr_ice_candidate_pair_restart_stun_nominated_cb(NR_SOCKET s, int how, void *cb_arg)
+ {
+ nr_ice_cand_pair *pair=cb_arg;
+ int r,_status;
+
+ pair->restart_nominated_cb_timer=0;
+
+ r_log(LOG_ICE,LOG_INFO,"ICE-PEER(%s)/STREAM(%s)/CAND-PAIR(%s)/COMP(%d): Restarting pair as nominated: %s",pair->pctx->label,pair->local->stream->label,pair->codeword,pair->remote->component->component_id,pair->as_string);
+
+ nr_stun_client_reset(pair->stun_client);
+
+ if(r=nr_stun_client_start(pair->stun_client,NR_ICE_CLIENT_MODE_USE_CANDIDATE,nr_ice_candidate_pair_stun_cb,pair))
+ ABORT(r);
+
+ if(r=nr_ice_ctx_remember_id(pair->pctx->ctx, pair->stun_client->request))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ if (_status) {
+ // cb doesn't return anything, but we should probably log that we aborted
+ // This also quiets the unused variable warnings.
+ r_log(LOG_ICE,LOG_DEBUG,"ICE-PEER(%s)/STREAM(%s)/CAND-PAIR(%s)/COMP(%d): STUN nominated cb pair as nominated: %s abort with status: %d",
+ pair->pctx->label,pair->local->stream->label,pair->codeword,pair->remote->component->component_id,pair->as_string, _status);
+ }
+ return;
+ }
+
+static void nr_ice_candidate_pair_restart_stun_role_change_cb(NR_SOCKET s, int how, void *cb_arg)
+ {
+ nr_ice_cand_pair *pair=cb_arg;
+
+ pair->restart_role_change_cb_timer=0;
+
+ r_log(LOG_ICE,LOG_INFO,"ICE-PEER(%s)/STREAM(%s)/CAND-PAIR(%s):COMP(%d): Restarting pair as %s: %s",pair->pctx->label,pair->local->stream->label,pair->codeword,pair->remote->component->component_id,pair->pctx->controlling ? "CONTROLLING" : "CONTROLLED",pair->as_string);
+
+ nr_ice_candidate_pair_restart(pair->pctx, pair);
+ }
+
+void nr_ice_candidate_pair_role_change(nr_ice_cand_pair *pair)
+ {
+ pair->stun_client->params.ice_binding_request.control = pair->pctx->controlling ? NR_ICE_CONTROLLING : NR_ICE_CONTROLLED;
+ nr_ice_candidate_pair_set_priority(pair);
+
+ if(pair->state == NR_ICE_PAIR_STATE_IN_PROGRESS) {
+ /* We could try only restarting in-progress pairs when they receive their
+ * 487, but this ends up being simpler, because any extra 487 are dropped.
+ */
+ if(!pair->restart_role_change_cb_timer)
+ NR_ASYNC_TIMER_SET(0,nr_ice_candidate_pair_restart_stun_role_change_cb,pair,&pair->restart_role_change_cb_timer);
+ }
+ }
+
+static void nr_ice_candidate_pair_compute_codeword(nr_ice_cand_pair *pair,
+ nr_ice_candidate *lcand, nr_ice_candidate *rcand)
+ {
+ char as_string[2048];
+
+ snprintf(as_string,
+ sizeof(as_string),
+ "%s|%s(%s|%s)",
+ lcand->addr.as_string,
+ rcand->addr.as_string,
+ lcand->label,
+ rcand->label);
+
+ nr_ice_compute_codeword(as_string,strlen(as_string),pair->codeword);
+ }
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_candidate_pair.h b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_candidate_pair.h
new file mode 100644
index 0000000000..49da1f7802
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_candidate_pair.h
@@ -0,0 +1,101 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+
+#ifndef _ice_candidate_pair_h
+#define _ice_candidate_pair_h
+#ifdef __cplusplus
+using namespace std;
+extern "C" {
+#endif /* __cplusplus */
+
+
+struct nr_ice_cand_pair_ {
+ nr_ice_peer_ctx *pctx;
+ char codeword[5];
+ char *as_string;
+ int state; /* The current state (S 5.7.3) */
+#define NR_ICE_PAIR_STATE_FROZEN 1
+#define NR_ICE_PAIR_STATE_WAITING 2
+#define NR_ICE_PAIR_STATE_IN_PROGRESS 3
+#define NR_ICE_PAIR_STATE_FAILED 4
+#define NR_ICE_PAIR_STATE_SUCCEEDED 5
+#define NR_ICE_PAIR_STATE_CANCELLED 6
+
+ UCHAR peer_nominated; /* The peer sent USE-CANDIDATE
+ on this check */
+ UCHAR nominated; /* Is this nominated or not */
+
+ UCHAR triggered; /* Ignore further trigger check requests */
+
+ UINT8 priority; /* The priority for this pair */
+ nr_ice_candidate *local; /* The local candidate */
+ nr_ice_candidate *remote; /* The remote candidate */
+ char *foundation; /* The combined foundations */
+
+ // for RTCIceCandidatePairStats
+ UINT8 bytes_sent;
+ UINT8 bytes_recvd;
+ struct timeval last_sent;
+ struct timeval last_recvd;
+
+ nr_stun_client_ctx *stun_client; /* STUN context when acting as a client */
+ void *stun_client_handle;
+
+ void *stun_cb_timer;
+ void *restart_role_change_cb_timer;
+ void *restart_nominated_cb_timer;
+
+ TAILQ_ENTRY(nr_ice_cand_pair_) check_queue_entry; /* the check list */
+ TAILQ_ENTRY(nr_ice_cand_pair_) triggered_check_queue_entry; /* the trigger check queue */
+};
+
+int nr_ice_candidate_pair_create(nr_ice_peer_ctx *pctx, nr_ice_candidate *lcand,nr_ice_candidate *rcand,nr_ice_cand_pair **pairp);
+int nr_ice_candidate_pair_unfreeze(nr_ice_peer_ctx *pctx, nr_ice_cand_pair *pair);
+int nr_ice_candidate_pair_start(nr_ice_peer_ctx *pctx, nr_ice_cand_pair *pair);
+void nr_ice_candidate_pair_set_state(nr_ice_peer_ctx *pctx, nr_ice_cand_pair *pair, int state);
+void nr_ice_candidate_pair_dump_state(nr_ice_cand_pair *pair, int log_level);
+void nr_ice_candidate_pair_cancel(nr_ice_peer_ctx *pctx,nr_ice_cand_pair *pair, int move_to_wait_state);
+int nr_ice_candidate_pair_select(nr_ice_cand_pair *pair);
+int nr_ice_candidate_pair_do_triggered_check(nr_ice_peer_ctx *pctx, nr_ice_cand_pair *pair);
+void nr_ice_candidate_pair_insert(nr_ice_cand_pair_head *head,nr_ice_cand_pair *pair);
+int nr_ice_candidate_pair_trigger_check_append(nr_ice_cand_pair_head *head,nr_ice_cand_pair *pair);
+void nr_ice_candidate_pair_restart_stun_nominated_cb(NR_SOCKET s, int how, void *cb_arg);
+int nr_ice_candidate_pair_destroy(nr_ice_cand_pair **pairp);
+void nr_ice_candidate_pair_role_change(nr_ice_cand_pair *pair);
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_codeword.h b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_codeword.h
new file mode 100644
index 0000000000..76fce0b69b
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_codeword.h
@@ -0,0 +1,41 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+
+#ifndef _ice_codeword_h
+#define _ice_codeword_h
+
+void nr_ice_compute_codeword(char *buf, int len,char *codeword);
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_component.c b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_component.c
new file mode 100644
index 0000000000..584a9466bc
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_component.c
@@ -0,0 +1,1786 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <string.h>
+#include <assert.h>
+#include <nr_api.h>
+#include <registry.h>
+#include <async_timer.h>
+#include "ice_ctx.h"
+#include "ice_codeword.h"
+#include "stun.h"
+#include "nr_socket_local.h"
+#include "nr_socket_turn.h"
+#include "nr_socket_wrapper.h"
+#include "nr_socket_buffered_stun.h"
+#include "nr_socket_multi_tcp.h"
+#include "ice_reg.h"
+#include "nr_crypto.h"
+#include "r_time.h"
+
+static void nr_ice_component_refresh_consent_cb(NR_SOCKET s, int how, void *cb_arg);
+static int nr_ice_component_stun_server_default_cb(void *cb_arg,nr_stun_server_ctx *stun_ctx,nr_socket *sock, nr_stun_server_request *req, int *dont_free, int *error);
+static int nr_ice_pre_answer_request_destroy(nr_ice_pre_answer_request **parp);
+int nr_ice_component_can_candidate_addr_pair(nr_transport_addr *local, nr_transport_addr *remote);
+int nr_ice_component_can_candidate_tcptype_pair(nr_socket_tcp_type left, nr_socket_tcp_type right);
+void nr_ice_component_consent_calc_consent_timer(nr_ice_component *comp);
+void nr_ice_component_consent_schedule_consent_timer(nr_ice_component *comp);
+int nr_ice_component_refresh_consent(nr_stun_client_ctx *ctx, NR_async_cb finished_cb, void *cb_arg);
+int nr_ice_component_setup_consent(nr_ice_component *comp);
+int nr_ice_pre_answer_enqueue(nr_ice_component *comp, nr_socket *sock, nr_stun_server_request *req, int *dont_free);
+
+/* This function takes ownership of the contents of req (but not req itself) */
+static int nr_ice_pre_answer_request_create(nr_transport_addr *dst, nr_stun_server_request *req, nr_ice_pre_answer_request **parp)
+ {
+ int r, _status;
+ nr_ice_pre_answer_request *par = 0;
+ nr_stun_message_attribute *attr;
+
+ if (!(par = RCALLOC(sizeof(nr_ice_pre_answer_request))))
+ ABORT(R_NO_MEMORY);
+
+ par->req = *req; /* Struct assignment */
+ memset(req, 0, sizeof(*req)); /* Zero contents to avoid confusion */
+
+ if (r=nr_transport_addr_copy(&par->local_addr, dst))
+ ABORT(r);
+ if (!nr_stun_message_has_attribute(par->req.request, NR_STUN_ATTR_USERNAME, &attr))
+ ABORT(R_INTERNAL);
+ if (!(par->username = r_strdup(attr->u.username)))
+ ABORT(R_NO_MEMORY);
+
+ *parp=par;
+ _status=0;
+ abort:
+ if (_status) {
+ /* Erase the request so we don't free it */
+ memset(&par->req, 0, sizeof(nr_stun_server_request));
+ nr_ice_pre_answer_request_destroy(&par);
+ }
+
+ return(_status);
+ }
+
+static int nr_ice_pre_answer_request_destroy(nr_ice_pre_answer_request **parp)
+ {
+ nr_ice_pre_answer_request *par;
+
+ if (!parp || !*parp)
+ return(0);
+
+ par = *parp;
+ *parp = 0;
+
+ nr_stun_message_destroy(&par->req.request);
+ nr_stun_message_destroy(&par->req.response);
+
+ RFREE(par->username);
+ RFREE(par);
+
+ return(0);
+ }
+
+int nr_ice_component_create(nr_ice_media_stream *stream, int component_id, nr_ice_component **componentp)
+ {
+ int _status;
+ nr_ice_component *comp=0;
+
+ if(!(comp=RCALLOC(sizeof(nr_ice_component))))
+ ABORT(R_NO_MEMORY);
+
+ comp->state=NR_ICE_COMPONENT_UNPAIRED;
+ comp->component_id=component_id;
+ comp->stream=stream;
+ comp->ctx=stream->ctx;
+
+ STAILQ_INIT(&comp->sockets);
+ TAILQ_INIT(&comp->candidates);
+ STAILQ_INIT(&comp->pre_answer_reqs);
+
+ STAILQ_INSERT_TAIL(&stream->components,comp,entry);
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_ice_component_destroy(nr_ice_component **componentp)
+ {
+ nr_ice_component *component;
+ nr_ice_socket *s1,*s2;
+ nr_ice_candidate *c1,*c2;
+ nr_ice_pre_answer_request *r1,*r2;
+
+ if(!componentp || !*componentp)
+ return(0);
+
+ component=*componentp;
+ *componentp=0;
+
+ nr_ice_component_consent_destroy(component);
+
+ /* Detach ourselves from the sockets */
+ if (component->local_component){
+ nr_ice_socket *isock=STAILQ_FIRST(&component->local_component->sockets);
+ while(isock){
+ nr_stun_server_remove_client(isock->stun_server, component);
+ isock=STAILQ_NEXT(isock, entry);
+ }
+ }
+
+ /* candidates MUST be destroyed before the sockets so that
+ they can deregister */
+ TAILQ_FOREACH_SAFE(c1, &component->candidates, entry_comp, c2){
+ TAILQ_REMOVE(&component->candidates,c1,entry_comp);
+ nr_ice_candidate_destroy(&c1);
+ }
+
+ STAILQ_FOREACH_SAFE(s1, &component->sockets, entry, s2){
+ STAILQ_REMOVE(&component->sockets,s1,nr_ice_socket_,entry);
+ nr_ice_socket_destroy(&s1);
+ }
+
+ STAILQ_FOREACH_SAFE(r1, &component->pre_answer_reqs, entry, r2){
+ STAILQ_REMOVE(&component->pre_answer_reqs,r1,nr_ice_pre_answer_request_, entry);
+ nr_ice_pre_answer_request_destroy(&r1);
+ }
+
+ RFREE(component);
+ return(0);
+ }
+
+static int nr_ice_component_create_stun_server_ctx(nr_ice_component *component, nr_ice_socket *isock, nr_transport_addr *addr, char *lufrag, Data *pwd)
+ {
+ char label[256];
+ int r,_status;
+
+ /* Create a STUN server context for this socket */
+ snprintf(label, sizeof(label), "server(%s)", addr->as_string);
+ if(r=nr_stun_server_ctx_create(label,&isock->stun_server))
+ ABORT(r);
+
+ /* Add the default STUN credentials so that we can respond before
+ we hear about the peer.*/
+ if(r=nr_stun_server_add_default_client(isock->stun_server, lufrag, pwd, nr_ice_component_stun_server_default_cb, component))
+ ABORT(r);
+
+ /* Do this last; if this function fails, we should not be taking a reference to isock */
+ if(r=nr_ice_socket_register_stun_server(isock,isock->stun_server,&isock->stun_server_handle))
+ ABORT(r);
+
+ _status = 0;
+ abort:
+ return(_status);
+ }
+
+static int nr_ice_component_initialize_udp(struct nr_ice_ctx_ *ctx,nr_ice_component *component, nr_local_addr *addrs, int addr_ct, char *lufrag, Data *pwd)
+ {
+ nr_socket *sock;
+ nr_ice_socket *isock=0;
+ nr_ice_candidate *cand=0;
+ int i;
+ int j;
+ int r,_status;
+
+ if(ctx->flags & NR_ICE_CTX_FLAGS_ONLY_PROXY) {
+ /* No UDP support if we must use a proxy */
+ return 0;
+ }
+
+ /* Now one ice_socket for each address */
+ for(i=0;i<addr_ct;i++){
+ char suppress;
+
+ if(r=NR_reg_get2_char(NR_ICE_REG_SUPPRESS_INTERFACE_PRFX,addrs[i].addr.ifname,&suppress)){
+ if(r!=R_NOT_FOUND)
+ ABORT(r);
+ }
+ else{
+ if(suppress)
+ continue;
+ }
+ r_log(LOG_ICE,LOG_DEBUG,"ICE-STREAM(%s): host address %s",component->stream->label,addrs[i].addr.as_string);
+ if((r=nr_socket_factory_create_socket(ctx->socket_factory,&addrs[i].addr,&sock))){
+ r_log(LOG_ICE,LOG_WARNING,"ICE-STREAM(%s): couldn't create socket for address %s",component->stream->label,addrs[i].addr.as_string);
+ continue;
+ }
+
+ if(r=nr_ice_socket_create(ctx,component,sock,NR_ICE_SOCKET_TYPE_DGRAM,&isock))
+ ABORT(r);
+
+ /* Create a STUN server context for this socket */
+ if ((r=nr_ice_component_create_stun_server_ctx(component,isock,&addrs[i].addr,lufrag,pwd)))
+ ABORT(r);
+
+ /* Make sure we don't leak this. Failures might result in it being
+ * unused, but we hand off references to this in enough places below
+ * that unwinding it all becomes impractical. */
+ STAILQ_INSERT_TAIL(&component->sockets,isock,entry);
+
+ if (!(component->stream->flags & NR_ICE_CTX_FLAGS_RELAY_ONLY)) {
+ /* Create one host candidate */
+ if(r=nr_ice_candidate_create(ctx,component,isock,sock,HOST,0,0,
+ component->component_id,&cand))
+ ABORT(r);
+
+ TAILQ_INSERT_TAIL(&component->candidates,cand,entry_comp);
+ component->candidate_ct++;
+ cand=0;
+
+ /* And a srvrflx candidate for each STUN server */
+ for(j=0;j<component->stream->stun_server_ct;j++){
+ r_log(LOG_ICE,LOG_DEBUG,"ICE-STREAM(%s): Checking STUN server %s %s", component->stream->label, component->stream->stun_servers[j].addr.fqdn, component->stream->stun_servers[j].addr.as_string);
+ /* Skip non-UDP */
+ if (component->stream->stun_servers[j].addr.protocol != IPPROTO_UDP) continue;
+
+ if (nr_transport_addr_check_compatibility(
+ &addrs[i].addr, &component->stream->stun_servers[j].addr)) {
+ r_log(LOG_ICE,LOG_INFO,"ICE-STREAM(%s): Skipping STUN server because of address type mis-match",component->stream->label);
+ continue;
+ }
+
+ /* Ensure id is set (nr_ice_ctx_set_stun_servers does not) */
+ component->stream->stun_servers[j].id = j;
+ if(r=nr_ice_candidate_create(ctx,component,
+ isock,sock,SERVER_REFLEXIVE,0,
+ &component->stream->stun_servers[j],component->component_id,&cand))
+ ABORT(r);
+ TAILQ_INSERT_TAIL(&component->candidates,cand,entry_comp);
+ component->candidate_ct++;
+ cand=0;
+ }
+ }
+ else{
+ r_log(LOG_ICE,LOG_WARNING,"ICE-STREAM(%s): relay only option results in no host candidate for %s",component->stream->label,addrs[i].addr.as_string);
+ }
+
+#ifdef USE_TURN
+ if ((component->stream->flags & NR_ICE_CTX_FLAGS_RELAY_ONLY) &&
+ (component->stream->turn_server_ct == 0)) {
+ r_log(LOG_ICE,LOG_ERR,"ICE-STREAM(%s): relay only option is set without any TURN server configured",component->stream->label);
+ }
+ /* And both a srvrflx and relayed candidate for each TURN server (unless
+ we're in relay-only mode, in which case just the relayed one) */
+ for(j=0;j<component->stream->turn_server_ct;j++){
+ nr_socket *turn_sock;
+ nr_ice_candidate *srvflx_cand=0;
+
+ r_log(LOG_ICE,LOG_DEBUG,"ICE-STREAM(%s): Checking TURN server %s %s", component->stream->label, component->stream->turn_servers[j].turn_server.addr.fqdn, component->stream->turn_servers[j].turn_server.addr.as_string);
+
+ /* Skip non-UDP */
+ if (component->stream->turn_servers[j].turn_server.addr.protocol != IPPROTO_UDP)
+ continue;
+
+ if (nr_transport_addr_check_compatibility(
+ &addrs[i].addr, &component->stream->turn_servers[j].turn_server.addr)) {
+ r_log(LOG_ICE,LOG_INFO,"ICE-STREAM(%s): Skipping TURN server because of address type mis-match",component->stream->label);
+ continue;
+ }
+
+ if (!(component->stream->flags & NR_ICE_CTX_FLAGS_RELAY_ONLY)) {
+ /* Ensure id is set with a unique value */
+ component->stream->turn_servers[j].turn_server.id = j + component->stream->stun_server_ct;
+ /* srvrflx */
+ if(r=nr_ice_candidate_create(ctx,component,
+ isock,sock,SERVER_REFLEXIVE,0,
+ &component->stream->turn_servers[j].turn_server,component->component_id,&cand))
+ ABORT(r);
+ cand->state=NR_ICE_CAND_STATE_INITIALIZING; /* Don't start */
+ cand->done_cb=nr_ice_gather_finished_cb;
+ cand->cb_arg=cand;
+
+ TAILQ_INSERT_TAIL(&component->candidates,cand,entry_comp);
+ component->candidate_ct++;
+ srvflx_cand=cand;
+ cand=0;
+ }
+ /* relayed*/
+ if(r=nr_socket_turn_create(&turn_sock))
+ ABORT(r);
+ if(r=nr_ice_candidate_create(ctx,component,
+ isock,turn_sock,RELAYED,0,
+ &component->stream->turn_servers[j].turn_server,component->component_id,&cand))
+ ABORT(r);
+ if (srvflx_cand) {
+ cand->u.relayed.srvflx_candidate=srvflx_cand;
+ srvflx_cand->u.srvrflx.relay_candidate=cand;
+ }
+ cand->u.relayed.server=&component->stream->turn_servers[j];
+ TAILQ_INSERT_TAIL(&component->candidates,cand,entry_comp);
+ component->candidate_ct++;
+
+ cand=0;
+ }
+#endif /* USE_TURN */
+ }
+
+ _status = 0;
+ abort:
+ return(_status);
+ }
+
+static int nr_ice_component_get_port_from_ephemeral_range(uint16_t *port)
+ {
+ int _status, r;
+ void *buf = port;
+ if(r=nr_crypto_random_bytes(buf, 2))
+ ABORT(r);
+ *port|=49152; /* make it fit into IANA ephemeral port range >= 49152 */
+ _status=0;
+abort:
+ return(_status);
+ }
+
+static int nr_ice_component_create_tcp_host_candidate(struct nr_ice_ctx_ *ctx,
+ nr_ice_component *component, nr_transport_addr *interface_addr, nr_socket_tcp_type tcp_type,
+ int backlog, int so_sock_ct, char *lufrag, Data *pwd, nr_ice_socket **isock)
+ {
+ int r,_status;
+ nr_ice_candidate *cand=0;
+ int tries=3;
+ nr_ice_socket *isock_tmp=0;
+ nr_socket *nrsock=0;
+ nr_transport_addr addr;
+ uint16_t local_port;
+
+ if ((r=nr_transport_addr_copy(&addr,interface_addr)))
+ ABORT(r);
+ addr.protocol=IPPROTO_TCP;
+
+ do{
+ if (!tries--)
+ ABORT(r);
+
+ if((r=nr_ice_component_get_port_from_ephemeral_range(&local_port)))
+ ABORT(r);
+
+ if ((r=nr_transport_addr_set_port(&addr, local_port)))
+ ABORT(r);
+
+ if((r=nr_transport_addr_fmt_addr_string(&addr)))
+ ABORT(r);
+
+ /* It would be better to stop trying if there is error other than
+ port already used, but it'd require significant work to support this. */
+ r=nr_socket_multi_tcp_create(ctx,component,&addr,tcp_type,so_sock_ct,NR_STUN_MAX_MESSAGE_SIZE,&nrsock);
+
+ } while(r);
+
+ if((tcp_type == TCP_TYPE_PASSIVE) && (r=nr_socket_listen(nrsock,backlog)))
+ ABORT(r);
+
+ if((r=nr_ice_socket_create(ctx,component,nrsock,NR_ICE_SOCKET_TYPE_STREAM_TCP,&isock_tmp)))
+ ABORT(r);
+
+ /* nr_ice_socket took ownership of nrsock */
+ nrsock=NULL;
+
+ /* Create a STUN server context for this socket */
+ if ((r=nr_ice_component_create_stun_server_ctx(component,isock_tmp,&addr,lufrag,pwd)))
+ ABORT(r);
+
+ if((r=nr_ice_candidate_create(ctx,component,isock_tmp,isock_tmp->sock,HOST,tcp_type,0,
+ component->component_id,&cand)))
+ ABORT(r);
+
+ if (isock)
+ *isock=isock_tmp;
+
+ TAILQ_INSERT_TAIL(&component->candidates,cand,entry_comp);
+ component->candidate_ct++;
+
+ STAILQ_INSERT_TAIL(&component->sockets,isock_tmp,entry);
+
+ _status=0;
+abort:
+ if (_status) {
+ nr_ice_socket_destroy(&isock_tmp);
+ nr_socket_destroy(&nrsock);
+ }
+ return(_status);
+ }
+
+static int nr_ice_component_initialize_tcp(struct nr_ice_ctx_ *ctx,nr_ice_component *component, nr_local_addr *addrs, int addr_ct, char *lufrag, Data *pwd)
+ {
+ nr_ice_candidate *cand=0;
+ int i;
+ int j;
+ int r,_status;
+ int so_sock_ct=0;
+ int backlog=10;
+ char ice_tcp_disabled=1;
+
+ r_log(LOG_ICE,LOG_DEBUG,"nr_ice_component_initialize_tcp");
+
+ if(r=NR_reg_get_int4(NR_ICE_REG_ICE_TCP_SO_SOCK_COUNT,&so_sock_ct)){
+ if(r!=R_NOT_FOUND)
+ ABORT(r);
+ }
+
+ if(r=NR_reg_get_int4(NR_ICE_REG_ICE_TCP_LISTEN_BACKLOG,&backlog)){
+ if(r!=R_NOT_FOUND)
+ ABORT(r);
+ }
+
+ if ((r=NR_reg_get_char(NR_ICE_REG_ICE_TCP_DISABLE, &ice_tcp_disabled))) {
+ if (r != R_NOT_FOUND)
+ ABORT(r);
+ }
+ if ((component->stream->flags & NR_ICE_CTX_FLAGS_RELAY_ONLY) ||
+ (component->stream->flags & NR_ICE_CTX_FLAGS_ONLY_PROXY)) {
+ r_log(LOG_ICE,LOG_WARNING,"ICE-STREAM(%s): relay/proxy only option results in ICE TCP being disabled",component->stream->label);
+ ice_tcp_disabled = 1;
+ }
+
+ for(i=0;i<addr_ct;i++){
+ char suppress;
+ nr_ice_socket *isock_psv=0;
+ nr_ice_socket *isock_so=0;
+
+ if(r=NR_reg_get2_char(NR_ICE_REG_SUPPRESS_INTERFACE_PRFX,addrs[i].addr.ifname,&suppress)){
+ if(r!=R_NOT_FOUND)
+ ABORT(r);
+ }
+ else if(suppress) {
+ continue;
+ }
+
+ if (!ice_tcp_disabled) {
+ /* passive host candidate */
+ if ((r=nr_ice_component_create_tcp_host_candidate(ctx, component, &addrs[i].addr,
+ TCP_TYPE_PASSIVE, backlog, 0, lufrag, pwd, &isock_psv))) {
+ r_log(LOG_ICE,LOG_WARNING,"ICE-STREAM(%s): failed to create passive TCP host candidate: %d",component->stream->label,r);
+ }
+
+ /* active host candidate */
+ if ((r=nr_ice_component_create_tcp_host_candidate(ctx, component, &addrs[i].addr,
+ TCP_TYPE_ACTIVE, 0, 0, lufrag, pwd, NULL))) {
+ r_log(LOG_ICE,LOG_WARNING,"ICE-STREAM(%s): failed to create active TCP host candidate: %d",component->stream->label,r);
+ }
+
+ /* simultaneous-open host candidate */
+ if (so_sock_ct) {
+ if ((r=nr_ice_component_create_tcp_host_candidate(ctx, component, &addrs[i].addr,
+ TCP_TYPE_SO, 0, so_sock_ct, lufrag, pwd, &isock_so))) {
+ r_log(LOG_ICE,LOG_WARNING,"ICE-STREAM(%s): failed to create simultanous open TCP host candidate: %d",component->stream->label,r);
+ }
+ }
+
+ /* And srvrflx candidates for each STUN server */
+ for(j=0;j<component->stream->stun_server_ct;j++){
+ if (component->stream->stun_servers[j].addr.protocol != IPPROTO_TCP) continue;
+
+ if (isock_psv) {
+ if(r=nr_ice_candidate_create(ctx,component,
+ isock_psv,isock_psv->sock,SERVER_REFLEXIVE,TCP_TYPE_PASSIVE,
+ &component->stream->stun_servers[j],component->component_id,&cand))
+ ABORT(r);
+ TAILQ_INSERT_TAIL(&component->candidates,cand,entry_comp);
+ component->candidate_ct++;
+ cand=0;
+ }
+
+ if (isock_so) {
+ if(r=nr_ice_candidate_create(ctx,component,
+ isock_so,isock_so->sock,SERVER_REFLEXIVE,TCP_TYPE_SO,
+ &component->stream->stun_servers[j],component->component_id,&cand))
+ ABORT(r);
+ TAILQ_INSERT_TAIL(&component->candidates,cand,entry_comp);
+ component->candidate_ct++;
+ cand=0;
+ }
+ }
+ }
+
+#ifdef USE_TURN
+ /* Create a new relayed candidate for each addr/TURN server pair */
+ for(j=0;j<component->stream->turn_server_ct;j++){
+ nr_transport_addr addr;
+ nr_socket *local_sock;
+ nr_socket *buffered_sock;
+ nr_socket *turn_sock;
+ nr_ice_socket *turn_isock;
+
+ r_log(LOG_ICE,LOG_DEBUG,"ICE-STREAM(%s): Checking TURN server %s %s", component->stream->label, component->stream->turn_servers[j].turn_server.addr.fqdn, component->stream->turn_servers[j].turn_server.addr.as_string);
+
+ /* Skip non-TCP */
+ if (component->stream->turn_servers[j].turn_server.addr.protocol != IPPROTO_TCP)
+ continue;
+
+ /* Create relay candidate */
+ if ((r=nr_transport_addr_copy(&addr, &addrs[i].addr)))
+ ABORT(r);
+ addr.protocol = IPPROTO_TCP;
+
+ if (nr_transport_addr_check_compatibility(
+ &addr, &component->stream->turn_servers[j].turn_server.addr)) {
+ r_log(LOG_ICE,LOG_INFO,"ICE-STREAM(%s): Skipping TURN server because of address type mis-match",component->stream->label);
+ continue;
+ }
+
+ if (!ice_tcp_disabled) {
+ /* Use TURN server to get srflx candidates */
+ if (isock_psv) {
+ if(r=nr_ice_candidate_create(ctx,component,
+ isock_psv,isock_psv->sock,SERVER_REFLEXIVE,TCP_TYPE_PASSIVE,
+ &component->stream->turn_servers[j].turn_server,component->component_id,&cand))
+ ABORT(r);
+ TAILQ_INSERT_TAIL(&component->candidates,cand,entry_comp);
+ component->candidate_ct++;
+ cand=0;
+ }
+
+ if (isock_so) {
+ if(r=nr_ice_candidate_create(ctx,component,
+ isock_so,isock_so->sock,SERVER_REFLEXIVE,TCP_TYPE_SO,
+ &component->stream->turn_servers[j].turn_server,component->component_id,&cand))
+ ABORT(r);
+ TAILQ_INSERT_TAIL(&component->candidates,cand,entry_comp);
+ component->candidate_ct++;
+ cand=0;
+ }
+ }
+
+ if (component->stream->turn_servers[j].turn_server.addr.fqdn[0] != 0) {
+ /* If we're going to use TLS, make sure that's recorded */
+ addr.tls = component->stream->turn_servers[j].turn_server.addr.tls;
+ }
+
+ if ((r=nr_transport_addr_fmt_addr_string(&addr)))
+ ABORT(r);
+
+ r_log(LOG_ICE, LOG_DEBUG,
+ "ICE-STREAM(%s): Creating socket for address %s (turn server %s)",
+ component->stream->label, addr.as_string,
+ component->stream->turn_servers[j].turn_server.addr.as_string);
+
+ /* Create a local socket */
+ if((r=nr_socket_factory_create_socket(ctx->socket_factory,&addr,&local_sock))){
+ r_log(LOG_ICE,LOG_DEBUG,"ICE-STREAM(%s): couldn't create socket for address %s",component->stream->label,addr.as_string);
+ continue;
+ }
+
+ r_log(LOG_ICE,LOG_DEBUG,"nr_ice_component_initialize_tcp creating TURN TCP wrappers");
+
+ /* The TCP buffered socket */
+ if((r=nr_socket_buffered_stun_create(local_sock, NR_STUN_MAX_MESSAGE_SIZE, TURN_TCP_FRAMING, &buffered_sock)))
+ ABORT(r);
+
+ /* The TURN socket */
+ if(r=nr_socket_turn_create(&turn_sock))
+ ABORT(r);
+
+ /* Create an ICE socket */
+ if((r=nr_ice_socket_create(ctx, component, buffered_sock, NR_ICE_SOCKET_TYPE_STREAM_TURN, &turn_isock)))
+ ABORT(r);
+
+
+ /* Create a STUN server context for this socket */
+ if ((r=nr_ice_component_create_stun_server_ctx(component,turn_isock,&addr,lufrag,pwd)))
+ ABORT(r);
+
+ /* Make sure we don't leak this. Failures might result in it being
+ * unused, but we hand off references to this in enough places below
+ * that unwinding it all becomes impractical. */
+ STAILQ_INSERT_TAIL(&component->sockets,turn_isock,entry);
+
+ /* Attach ourselves to it */
+ if(r=nr_ice_candidate_create(ctx,component,
+ turn_isock,turn_sock,RELAYED,TCP_TYPE_NONE,
+ &component->stream->turn_servers[j].turn_server,component->component_id,&cand))
+ ABORT(r);
+ cand->u.relayed.srvflx_candidate=NULL;
+ cand->u.relayed.server=&component->stream->turn_servers[j];
+ TAILQ_INSERT_TAIL(&component->candidates,cand,entry_comp);
+ component->candidate_ct++;
+ cand=0;
+ }
+#endif /* USE_TURN */
+ }
+
+ _status = 0;
+ abort:
+ return(_status);
+ }
+
+
+/* Make all the candidates we can make at the beginning */
+int nr_ice_component_initialize(struct nr_ice_ctx_ *ctx,nr_ice_component *component)
+ {
+ int r,_status;
+ nr_local_addr *addrs=ctx->local_addrs;
+ int addr_ct=ctx->local_addr_ct;
+ char *lufrag;
+ char *lpwd;
+ Data pwd;
+ nr_ice_candidate *cand;
+
+ if (component->candidate_ct) {
+ r_log(LOG_ICE,LOG_DEBUG,"ICE(%s): component with id %d already has candidates, probably restarting gathering because of a new stream",ctx->label,component->component_id);
+ return(0);
+ }
+
+ r_log(LOG_ICE,LOG_DEBUG,"ICE(%s): initializing component with id %d",ctx->label,component->component_id);
+
+ if(addr_ct==0){
+ r_log(LOG_ICE,LOG_ERR,"ICE(%s): no local addresses available",ctx->label);
+ ABORT(R_NOT_FOUND);
+ }
+
+ /* Note: we need to recompute these because
+ we have not yet computed the values in the peer media stream.*/
+ lufrag=component->stream->ufrag;
+ assert(lufrag);
+ if (!lufrag)
+ ABORT(R_INTERNAL);
+ lpwd=component->stream->pwd;
+ assert(lpwd);
+ if (!lpwd)
+ ABORT(R_INTERNAL);
+ INIT_DATA(pwd, (UCHAR *)lpwd, strlen(lpwd));
+
+ /* Initialize the UDP candidates */
+ if (r=nr_ice_component_initialize_udp(ctx, component, addrs, addr_ct, lufrag, &pwd))
+ r_log(LOG_ICE,LOG_INFO,"ICE(%s): failed to create UDP candidates with error %d",ctx->label,r);
+ /* And the TCP candidates */
+ if (r=nr_ice_component_initialize_tcp(ctx, component, addrs, addr_ct, lufrag, &pwd))
+ r_log(LOG_ICE,LOG_INFO,"ICE(%s): failed to create TCP candidates with error %d",ctx->label,r);
+
+ /* count the candidates that will be initialized */
+ cand=TAILQ_FIRST(&component->candidates);
+ if(!cand){
+ r_log(LOG_ICE,LOG_ERR,"ICE(%s): couldn't create any valid candidates",ctx->label);
+ ABORT(R_NOT_FOUND);
+ }
+
+ while(cand){
+ ctx->uninitialized_candidates++;
+ cand=TAILQ_NEXT(cand,entry_comp);
+ }
+
+ /* Now initialize all the candidates */
+ cand=TAILQ_FIRST(&component->candidates);
+ while(cand){
+ if(cand->state!=NR_ICE_CAND_STATE_INITIALIZING){
+ nr_ice_candidate_initialize(cand,nr_ice_gather_finished_cb,cand);
+ }
+ cand=TAILQ_NEXT(cand,entry_comp);
+ }
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+void nr_ice_component_stop_gathering(nr_ice_component *component)
+ {
+ nr_ice_candidate *c1,*c2;
+ TAILQ_FOREACH_SAFE(c1, &component->candidates, entry_comp, c2){
+ nr_ice_candidate_stop_gathering(c1);
+ }
+ }
+
+int nr_ice_component_is_done_gathering(nr_ice_component *comp)
+ {
+ nr_ice_candidate *cand=TAILQ_FIRST(&comp->candidates);
+ while(cand){
+ if(cand->state != NR_ICE_CAND_STATE_INITIALIZED &&
+ cand->state != NR_ICE_CAND_STATE_FAILED){
+ return 0;
+ }
+ cand=TAILQ_NEXT(cand,entry_comp);
+ }
+ return 1;
+ }
+
+
+static int nr_ice_any_peer_paired(nr_ice_candidate* cand) {
+ nr_ice_peer_ctx* pctx=STAILQ_FIRST(&cand->ctx->peers);
+ while(pctx && pctx->state == NR_ICE_PEER_STATE_UNPAIRED){
+ /* Is it worth actually looking through the check lists? Probably not. */
+ pctx=STAILQ_NEXT(pctx,entry);
+ }
+ return pctx != NULL;
+}
+
+/*
+ Compare this newly initialized candidate against the other initialized
+ candidates and discard the lower-priority one if they are redundant.
+
+ This algorithm combined with the other algorithms, favors
+ host > srflx > relay
+ */
+int nr_ice_component_maybe_prune_candidate(nr_ice_ctx *ctx, nr_ice_component *comp, nr_ice_candidate *c1, int *was_pruned)
+ {
+ nr_ice_candidate *c2, *tmp = NULL;
+
+ *was_pruned = 0;
+ c2 = TAILQ_FIRST(&comp->candidates);
+ while(c2){
+ if((c1 != c2) &&
+ (c2->state == NR_ICE_CAND_STATE_INITIALIZED) &&
+ !nr_transport_addr_cmp(&c1->base,&c2->base,NR_TRANSPORT_ADDR_CMP_MODE_ALL) &&
+ !nr_transport_addr_cmp(&c1->addr,&c2->addr,NR_TRANSPORT_ADDR_CMP_MODE_ALL)){
+
+ if((c1->type == c2->type) ||
+ (!(ctx->flags & NR_ICE_CTX_FLAGS_DISABLE_HOST_CANDIDATES) &&
+ !(ctx->flags & NR_ICE_CTX_FLAGS_OBFUSCATE_HOST_ADDRESSES) &&
+ ((c1->type==HOST && c2->type == SERVER_REFLEXIVE) ||
+ (c2->type==HOST && c1->type == SERVER_REFLEXIVE)))){
+
+ /*
+ These are redundant. Remove the lower pri one, or if pairing has
+ already occurred, remove the newest one.
+
+ Since this algorithmis run whenever a new candidate
+ is initialized, there should at most one duplicate.
+ */
+ if ((c1->priority <= c2->priority) || nr_ice_any_peer_paired(c2)) {
+ tmp = c1;
+ *was_pruned = 1;
+ }
+ else {
+ tmp = c2;
+ }
+ break;
+ }
+ }
+
+ c2=TAILQ_NEXT(c2,entry_comp);
+ }
+
+ if (tmp) {
+ r_log(LOG_ICE,LOG_DEBUG,"ICE(%s)/CAND(%s): Removing redundant candidate",
+ ctx->label,tmp->label);
+
+ TAILQ_REMOVE(&comp->candidates,tmp,entry_comp);
+ comp->candidate_ct--;
+ TAILQ_REMOVE(&tmp->isock->candidates,tmp,entry_sock);
+
+ nr_ice_candidate_destroy(&tmp);
+ }
+
+ return 0;
+ }
+
+static int nr_ice_component_pair_matches_check(nr_ice_component *comp, nr_ice_cand_pair *pair, nr_transport_addr *local_addr, nr_stun_server_request *req)
+ {
+ if(pair->remote->component->component_id!=comp->component_id)
+ return(0);
+
+ if(nr_transport_addr_cmp(&pair->local->base,local_addr,NR_TRANSPORT_ADDR_CMP_MODE_ALL))
+ return(0);
+
+ if(nr_transport_addr_cmp(&pair->remote->addr,&req->src_addr,NR_TRANSPORT_ADDR_CMP_MODE_ALL))
+ return(0);
+
+ return(1);
+ }
+
+static int nr_ice_component_handle_triggered_check(nr_ice_component *comp, nr_ice_cand_pair *pair, nr_stun_server_request *req, int *error)
+ {
+ nr_stun_message *sreq=req->request;
+ int r=0,_status;
+
+ if(nr_stun_message_has_attribute(sreq,NR_STUN_ATTR_USE_CANDIDATE,0)){
+ if(comp->stream->pctx->controlling){
+ r_log(LOG_ICE,LOG_WARNING,"ICE-PEER(%s)/CAND_PAIR(%s): Peer sent USE-CANDIDATE but is controlled",comp->stream->pctx->label, pair->codeword);
+ }
+ else{
+ /* If this is the first time we've noticed this is nominated...*/
+ pair->peer_nominated=1;
+
+ if(pair->state==NR_ICE_PAIR_STATE_SUCCEEDED && !pair->nominated){
+ pair->nominated=1;
+
+ if(r=nr_ice_component_nominated_pair(pair->remote->component, pair)) {
+ *error=(r==R_NO_MEMORY)?500:400;
+ ABORT(r);
+ }
+ }
+ }
+ }
+
+ /* Note: the RFC says to trigger first and then nominate. But in that case
+ * the canceled trigger pair would get nominated and the cloned trigger pair
+ * would not get the nomination status cloned with it.*/
+ if(r=nr_ice_candidate_pair_do_triggered_check(comp->stream->pctx,pair)) {
+ *error=(r==R_NO_MEMORY)?500:400;
+ ABORT(r);
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+/* Section 7.2.1 */
+static int nr_ice_component_process_incoming_check(nr_ice_component *comp, nr_transport_addr *local_addr, nr_stun_server_request *req, int *error)
+ {
+ nr_ice_cand_pair *pair;
+ nr_ice_candidate *pcand=0;
+ nr_stun_message *sreq=req->request;
+ nr_stun_message_attribute *attr;
+ int r=0,_status;
+ int found_valid=0;
+
+ r_log(LOG_ICE,LOG_DEBUG,"ICE-PEER(%s)/STREAM(%s)/COMP(%d): received request from %s",comp->stream->pctx->label,comp->stream->label,comp->component_id,req->src_addr.as_string);
+
+ if (comp->state == NR_ICE_COMPONENT_DISABLED)
+ ABORT(R_REJECTED);
+
+ /* Check for role conficts (7.2.1.1) */
+ if(comp->stream->pctx->controlling){
+ if(nr_stun_message_has_attribute(sreq,NR_STUN_ATTR_ICE_CONTROLLING,&attr)){
+ /* OK, there is a conflict. Who's right? */
+ r_log(LOG_ICE,LOG_INFO,"ICE-PEER(%s): role conflict, both controlling",comp->stream->pctx->label);
+
+ if(attr->u.ice_controlling > comp->stream->pctx->tiebreaker){
+ /* Update the peer ctx. This will propagate to all candidate pairs
+ in the context. */
+ nr_ice_peer_ctx_switch_controlling_role(comp->stream->pctx);
+ }
+ else {
+ /* We are: throw an error */
+ r_log(LOG_ICE,LOG_WARNING,"ICE-PEER(%s): returning 487 role conflict",comp->stream->pctx->label);
+
+ *error=487;
+ ABORT(R_REJECTED);
+ }
+ }
+ }
+ else{
+ if(nr_stun_message_has_attribute(sreq,NR_STUN_ATTR_ICE_CONTROLLED,&attr)){
+ /* OK, there is a conflict. Who's right? */
+ r_log(LOG_ICE,LOG_INFO,"ICE-PEER(%s): role conflict, both controlled",comp->stream->pctx->label);
+
+ if(attr->u.ice_controlled < comp->stream->pctx->tiebreaker){
+ /* Update the peer ctx. This will propagate to all candidate pairs
+ in the context. */
+ nr_ice_peer_ctx_switch_controlling_role(comp->stream->pctx);
+ }
+ else {
+ /* We are: throw an error */
+ r_log(LOG_ICE,LOG_WARNING,"ICE-PEER(%s): returning 487 role conflict",comp->stream->pctx->label);
+
+ *error=487;
+ ABORT(R_REJECTED);
+ }
+ }
+ }
+
+ r_log(LOG_ICE,LOG_DEBUG,"ICE-PEER(%s): This STUN request appears to map to local addr %s",comp->stream->pctx->label,local_addr->as_string);
+
+ pair=TAILQ_FIRST(&comp->stream->check_list);
+ while(pair){
+ /* Since triggered checks create duplicate pairs (in this implementation)
+ * we are willing to handle multiple matches here. */
+ if(nr_ice_component_pair_matches_check(comp, pair, local_addr, req)){
+ r_log(LOG_ICE,LOG_DEBUG,"ICE-PEER(%s)/CAND_PAIR(%s): Found a matching pair for received check: %s",comp->stream->pctx->label,pair->codeword,pair->as_string);
+ if(r=nr_ice_component_handle_triggered_check(comp, pair, req, error))
+ ABORT(r);
+ ++found_valid;
+ }
+ pair=TAILQ_NEXT(pair,check_queue_entry);
+ }
+
+ if(!found_valid){
+ /* There were no matching pairs, so we need to create a new peer
+ * reflexive candidate pair. */
+
+ if(!nr_stun_message_has_attribute(sreq,NR_STUN_ATTR_PRIORITY,&attr)){
+ r_log(LOG_ICE,LOG_WARNING,"ICE-PEER(%s): Rejecting stun request without priority",comp->stream->pctx->label);
+ *error=400;
+ ABORT(R_BAD_DATA);
+ }
+
+ /* Find our local component candidate */
+ nr_ice_candidate *cand;
+
+ r_log(LOG_ICE,LOG_DEBUG,"ICE-PEER(%s): no matching pair",comp->stream->pctx->label);
+ cand=TAILQ_FIRST(&comp->local_component->candidates);
+ while(cand){
+ if(!nr_transport_addr_cmp(&cand->addr,local_addr,NR_TRANSPORT_ADDR_CMP_MODE_ALL))
+ break;
+
+ cand=TAILQ_NEXT(cand,entry_comp);
+ }
+
+ /* Well, this really shouldn't happen, but it's an error from the
+ other side, so we just throw an error and keep going */
+ if(!cand){
+ r_log(LOG_ICE,LOG_WARNING,"ICE-PEER(%s): stun request to unknown local address %s, discarding",comp->stream->pctx->label,local_addr->as_string);
+
+ *error=400;
+ ABORT(R_NOT_FOUND);
+ }
+
+ /* Now make a peer reflexive (remote) candidate */
+ if(r=nr_ice_peer_peer_rflx_candidate_create(comp->stream->pctx->ctx,"prflx",comp,&req->src_addr,&pcand)) {
+ *error=(r==R_NO_MEMORY)?500:400;
+ ABORT(r);
+ }
+ pcand->priority=attr->u.priority;
+ pcand->state=NR_ICE_CAND_PEER_CANDIDATE_PAIRED;
+
+ /* Finally, create the candidate pair, insert into the check list, and
+ * apply the incoming check to it. */
+ if(r=nr_ice_candidate_pair_create(comp->stream->pctx,cand,pcand,
+ &pair)) {
+ *error=(r==R_NO_MEMORY)?500:400;
+ ABORT(r);
+ }
+
+ nr_ice_candidate_pair_set_state(pair->pctx,pair,NR_ICE_PAIR_STATE_FROZEN);
+ if(r=nr_ice_component_insert_pair(comp,pair)) {
+ *error=(r==R_NO_MEMORY)?500:400;
+ ABORT(r);
+ }
+
+ /* Do this last, since any call to ABORT will destroy pcand */
+ TAILQ_INSERT_TAIL(&comp->candidates,pcand,entry_comp);
+ pcand=0;
+
+ /* Finally start the trigger check if needed */
+ if(r=nr_ice_component_handle_triggered_check(comp, pair, req, error))
+ ABORT(r);
+ }
+
+ _status=0;
+ abort:
+ if(_status){
+ nr_ice_candidate_destroy(&pcand);
+ assert(*error != 0);
+ if(r!=R_NO_MEMORY) assert(*error != 500);
+ }
+ return(_status);
+ }
+
+static int nr_ice_component_stun_server_cb(void *cb_arg,nr_stun_server_ctx *stun_ctx,nr_socket *sock, nr_stun_server_request *req, int *dont_free, int *error)
+ {
+ nr_ice_component *pcomp=cb_arg;
+ nr_transport_addr local_addr;
+ int r,_status;
+
+ if(pcomp->state==NR_ICE_COMPONENT_FAILED) {
+ *error=400;
+ ABORT(R_REJECTED);
+ }
+
+ if (pcomp->local_component->stream->obsolete) {
+ /* Don't do any triggered check stuff in thiis case. */
+ return 0;
+ }
+
+ /* Find the candidate pair that this maps to */
+ if(r=nr_socket_getaddr(sock,&local_addr)) {
+ *error=500;
+ ABORT(r);
+ }
+
+ if (r=nr_ice_component_process_incoming_check(pcomp, &local_addr, req, error))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_ice_component_service_pre_answer_requests(nr_ice_peer_ctx *pctx, nr_ice_component *pcomp, char *username, int *serviced)
+ {
+ nr_ice_pre_answer_request *r1,*r2;
+ nr_ice_component *comp = pcomp->local_component;
+ int r,_status;
+
+ if (serviced)
+ *serviced = 0;
+
+ r_log(LOG_ICE,LOG_DEBUG,"ICE-PEER(%s)/STREAM(%s)/COMP(%d): looking for pre-answer requests",pctx->label,comp->stream->label,comp->component_id);
+
+ STAILQ_FOREACH_SAFE(r1, &comp->pre_answer_reqs, entry, r2) {
+ if (!strcmp(r1->username, username)) {
+ int error = 0;
+
+ r_log(LOG_ICE,LOG_DEBUG,"ICE-PEER(%s)/STREAM(%s)/COMP(%d): found pre-answer request",pctx->label,comp->stream->label,comp->component_id);
+ r = nr_ice_component_process_incoming_check(pcomp, &r1->local_addr, &r1->req, &error);
+ if (r) {
+ r_log(LOG_ICE,LOG_INFO,"ICE-PEER(%s)/STREAM(%s)/COMP(%d): error processing pre-answer request. Would have returned %d",pctx->label,comp->stream->label,comp->component_id, error);
+ }
+ (*serviced)++;
+ STAILQ_REMOVE(&comp->pre_answer_reqs,r1,nr_ice_pre_answer_request_, entry);
+ nr_ice_pre_answer_request_destroy(&r1);
+ }
+ }
+
+ _status=0;
+ return(_status);
+ }
+
+int nr_ice_component_can_candidate_tcptype_pair(nr_socket_tcp_type left, nr_socket_tcp_type right)
+ {
+ if (left && !right)
+ return(0);
+ if (!left && right)
+ return(0);
+ if (left == TCP_TYPE_ACTIVE && right != TCP_TYPE_PASSIVE)
+ return(0);
+ if (left == TCP_TYPE_SO && right != TCP_TYPE_SO)
+ return(0);
+ if (left == TCP_TYPE_PASSIVE)
+ return(0);
+
+ return(1);
+ }
+
+/* filter out pairings which won't work. */
+int nr_ice_component_can_candidate_addr_pair(nr_transport_addr *local, nr_transport_addr *remote)
+ {
+ if(local->ip_version != remote->ip_version)
+ return(0);
+ if(local->protocol != remote->protocol)
+ return(0);
+ if(nr_transport_addr_is_link_local(local) !=
+ nr_transport_addr_is_link_local(remote))
+ return(0);
+ /* This prevents our ice_unittest (or broken clients) from pairing a
+ * loopback with a host candidate. */
+ if(nr_transport_addr_is_loopback(local) !=
+ nr_transport_addr_is_loopback(remote))
+ return(0);
+
+ return(1);
+ }
+
+int nr_ice_component_pair_candidate(nr_ice_peer_ctx *pctx, nr_ice_component *pcomp, nr_ice_candidate *lcand, int pair_all_remote)
+ {
+ int r, _status;
+ nr_ice_candidate *pcand;
+ nr_ice_cand_pair *pair=0;
+ char codeword[5];
+
+ nr_ice_compute_codeword(lcand->label,strlen(lcand->label),codeword);
+ r_log(LOG_ICE,LOG_DEBUG,"ICE-PEER(%s)/CAND(%s): Pairing local candidate %s",pctx->label,codeword,lcand->label);
+
+ switch(lcand->type){
+ case HOST:
+ break;
+ case SERVER_REFLEXIVE:
+ case PEER_REFLEXIVE:
+ /* Don't actually pair these candidates */
+ goto done;
+ break;
+ case RELAYED:
+ break;
+ default:
+ assert(0);
+ ABORT(R_INTERNAL);
+ break;
+ }
+
+ TAILQ_FOREACH(pcand, &pcomp->candidates, entry_comp){
+ if(!nr_ice_component_can_candidate_addr_pair(&lcand->addr, &pcand->addr))
+ continue;
+ if(!nr_ice_component_can_candidate_tcptype_pair(lcand->tcp_type, pcand->tcp_type))
+ continue;
+
+ /* https://tools.ietf.org/html/draft-ietf-rtcweb-mdns-ice-candidates-03#section-3.3.2 */
+ if(lcand->type == RELAYED && pcand->mdns_addr && strlen(pcand->mdns_addr)) {
+ continue;
+ }
+
+ /*
+ Two modes, depending on |pair_all_remote|
+
+ 1. Pair remote candidates which have not been paired
+ (used in initial pairing or in processing the other side's
+ trickle candidates).
+ 2. Pair any remote candidate (used when processing our own
+ trickle candidates).
+ */
+ if (pair_all_remote || (pcand->state == NR_ICE_CAND_PEER_CANDIDATE_UNPAIRED)) {
+ if (pair_all_remote) {
+ /* When a remote candidate arrives after the start of checking, but
+ * before the gathering of local candidates, it can be in UNPAIRED */
+ pcand->state = NR_ICE_CAND_PEER_CANDIDATE_PAIRED;
+ }
+
+ nr_ice_compute_codeword(pcand->label,strlen(pcand->label),codeword);
+ r_log(LOG_ICE,LOG_DEBUG,"ICE-PEER(%s)/CAND(%s): Pairing with peer candidate %s", pctx->label, codeword, pcand->label);
+
+ if(r=nr_ice_candidate_pair_create(pctx,lcand,pcand,&pair))
+ ABORT(r);
+
+ if(r=nr_ice_component_insert_pair(pcomp, pair))
+ ABORT(r);
+ }
+ }
+
+ done:
+ _status = 0;
+ abort:
+ return(_status);
+ }
+
+int nr_ice_component_pair_candidates(nr_ice_peer_ctx *pctx, nr_ice_component *lcomp,nr_ice_component *pcomp)
+ {
+ nr_ice_candidate *lcand, *pcand;
+ nr_ice_socket *isock;
+ int r,_status;
+
+ r_log(LOG_ICE,LOG_DEBUG,"Pairing candidates======");
+
+ /* Create the candidate pairs */
+ lcand=TAILQ_FIRST(&lcomp->candidates);
+
+ if (!lcand) {
+ /* No local candidates, initialized or not! */
+ ABORT(R_FAILED);
+ }
+
+ while(lcand){
+ if (lcand->state == NR_ICE_CAND_STATE_INITIALIZED) {
+ if ((r = nr_ice_component_pair_candidate(pctx, pcomp, lcand, 0)))
+ ABORT(r);
+ }
+
+ lcand=TAILQ_NEXT(lcand,entry_comp);
+ }
+
+ /* Mark all peer candidates as paired */
+ pcand=TAILQ_FIRST(&pcomp->candidates);
+ while(pcand){
+ pcand->state = NR_ICE_CAND_PEER_CANDIDATE_PAIRED;
+
+ pcand=TAILQ_NEXT(pcand,entry_comp);
+
+ }
+
+ /* Now register the STUN server callback for this component.
+ Note that this is a per-component CB so we only need to
+ do this once.
+ */
+ if (pcomp->state != NR_ICE_COMPONENT_RUNNING) {
+ isock=STAILQ_FIRST(&lcomp->sockets);
+ while(isock){
+ if(r=nr_stun_server_add_client(isock->stun_server,pctx->label,
+ pcomp->stream->r2l_user,&pcomp->stream->r2l_pass,nr_ice_component_stun_server_cb,pcomp)) {
+ ABORT(r);
+ }
+ isock=STAILQ_NEXT(isock,entry);
+ }
+ }
+
+ pcomp->state = NR_ICE_COMPONENT_RUNNING;
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_ice_pre_answer_enqueue(nr_ice_component *comp, nr_socket *sock, nr_stun_server_request *req, int *dont_free)
+ {
+ int r = 0;
+ int _status;
+ nr_ice_pre_answer_request *r1, *r2;
+ nr_transport_addr dst_addr;
+ nr_ice_pre_answer_request *par = 0;
+
+ if (r=nr_socket_getaddr(sock, &dst_addr))
+ ABORT(r);
+
+ STAILQ_FOREACH_SAFE(r1, &comp->pre_answer_reqs, entry, r2) {
+ if (!nr_transport_addr_cmp(&r1->local_addr, &dst_addr,
+ NR_TRANSPORT_ADDR_CMP_MODE_ALL) &&
+ !nr_transport_addr_cmp(&r1->req.src_addr, &req->src_addr,
+ NR_TRANSPORT_ADDR_CMP_MODE_ALL)) {
+ return(0);
+ }
+ }
+
+ if (r=nr_ice_pre_answer_request_create(&dst_addr, req, &par))
+ ABORT(r);
+
+ r_log(LOG_ICE,LOG_DEBUG, "ICE(%s)/STREAM(%s)/COMP(%d): Enqueuing STUN request pre-answer from %s",
+ comp->ctx->label, comp->stream->label, comp->component_id,
+ req->src_addr.as_string);
+
+ *dont_free = 1;
+ STAILQ_INSERT_TAIL(&comp->pre_answer_reqs, par, entry);
+
+ _status=0;
+abort:
+ return(_status);
+ }
+
+/* Fires when we have an incoming candidate that doesn't correspond to an existing
+ remote peer. This is either pre-answer or just spurious. Store it in the
+ component for use when we see the actual answer, at which point we need
+ to do the procedures from S 7.2.1 in nr_ice_component_stun_server_cb.
+ */
+static int nr_ice_component_stun_server_default_cb(void *cb_arg,nr_stun_server_ctx *stun_ctx,nr_socket *sock, nr_stun_server_request *req, int *dont_free, int *error)
+ {
+ int r, _status;
+ nr_ice_component *comp = (nr_ice_component *)cb_arg;
+
+ r_log(LOG_ICE,LOG_DEBUG,"ICE(%s)/STREAM(%s)/COMP(%d): Received STUN request pre-answer from %s",
+ comp->ctx->label, comp->stream->label, comp->component_id,
+ req->src_addr.as_string);
+
+ if (r=nr_ice_pre_answer_enqueue(comp, sock, req, dont_free)) {
+ r_log(LOG_ICE,LOG_ERR,"ICE(%s)/STREAM(%s)/COMP(%d): Failed (%d) to enque pre-answer request from %s",
+ comp->ctx->label, comp->stream->label, comp->component_id, r,
+ req->src_addr.as_string);
+ ABORT(r);
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+#define NR_ICE_CONSENT_TIMER_DEFAULT 5000
+#define NR_ICE_CONSENT_TIMEOUT_DEFAULT 30000
+
+static void nr_ice_component_consent_failed(nr_ice_component *comp)
+ {
+ if (!comp->can_send) {
+ return;
+ }
+
+ r_log(LOG_ICE,LOG_INFO,"ICE(%s)/STREAM(%s)/COMP(%d): Consent refresh failed",
+ comp->ctx->label, comp->stream->label, comp->component_id);
+ comp->can_send = 0;
+
+ if (comp->consent_timeout) {
+ NR_async_timer_cancel(comp->consent_timeout);
+ comp->consent_timeout = 0;
+ }
+ if (comp->consent_timer) {
+ NR_async_timer_cancel(comp->consent_timer);
+ comp->consent_timer = 0;
+ }
+ /* We are turning the consent failure into a ICE component failure to
+ * alert the browser via ICE connection state change about this event. */
+ nr_ice_media_stream_component_failed(comp->stream, comp);
+ }
+
+static void nr_ice_component_consent_timeout_cb(NR_SOCKET s, int how, void *cb_arg)
+ {
+ nr_ice_component *comp=cb_arg;
+
+ comp->consent_timeout = 0;
+
+ r_log(LOG_ICE,LOG_WARNING,"ICE(%s)/STREAM(%s)/COMP(%d): Consent refresh final time out",
+ comp->ctx->label, comp->stream->label, comp->component_id);
+ nr_ice_component_consent_failed(comp);
+ }
+
+
+void nr_ice_component_disconnected(nr_ice_component *comp)
+ {
+ if (!comp->can_send) {
+ return;
+ }
+
+ if (comp->disconnected) {
+ return;
+ }
+
+ r_log(LOG_ICE,LOG_WARNING,"ICE(%s)/STREAM(%s)/COMP(%d): component disconnected",
+ comp->ctx->label, comp->stream->label, comp->component_id);
+ comp->disconnected = 1;
+
+ /* a single disconnected component disconnects the stream */
+ nr_ice_media_stream_set_disconnected(comp->stream, NR_ICE_MEDIA_STREAM_DISCONNECTED);
+ }
+
+static void nr_ice_component_consent_refreshed(nr_ice_component *comp)
+ {
+ uint16_t tval;
+
+ if (!comp->can_send) {
+ return;
+ }
+
+ gettimeofday(&comp->consent_last_seen, 0);
+ r_log(LOG_ICE,LOG_DEBUG,"ICE(%s)/STREAM(%s)/COMP(%d): consent_last_seen is now %lu",
+ comp->ctx->label, comp->stream->label, comp->component_id,
+ comp->consent_last_seen.tv_sec);
+
+ comp->disconnected = 0;
+
+ nr_ice_media_stream_check_if_connected(comp->stream);
+
+ if (comp->consent_timeout)
+ NR_async_timer_cancel(comp->consent_timeout);
+
+ tval = NR_ICE_CONSENT_TIMEOUT_DEFAULT;
+ if (comp->ctx->test_timer_divider)
+ tval = tval / comp->ctx->test_timer_divider;
+
+ NR_ASYNC_TIMER_SET(tval, nr_ice_component_consent_timeout_cb, comp,
+ &comp->consent_timeout);
+ }
+
+static void nr_ice_component_refresh_consent_cb(NR_SOCKET s, int how, void *cb_arg)
+ {
+ nr_ice_component *comp=cb_arg;
+
+ switch (comp->consent_ctx->state) {
+ case NR_STUN_CLIENT_STATE_FAILED:
+ if (comp->consent_ctx->error_code == 403) {
+ r_log(LOG_ICE, LOG_INFO, "ICE(%s)/STREAM(%s)/COMP(%d): Consent revoked by peer",
+ comp->ctx->label, comp->stream->label, comp->component_id);
+ nr_ice_component_consent_failed(comp);
+ }
+ break;
+ case NR_STUN_CLIENT_STATE_DONE:
+ r_log(LOG_ICE, LOG_INFO, "ICE(%s)/STREAM(%s)/COMP(%d): Consent refreshed",
+ comp->ctx->label, comp->stream->label, comp->component_id);
+ nr_ice_component_consent_refreshed(comp);
+ break;
+ case NR_STUN_CLIENT_STATE_TIMED_OUT:
+ r_log(LOG_ICE, LOG_INFO, "ICE(%s)/STREAM(%s)/COMP(%d): A single consent refresh request timed out",
+ comp->ctx->label, comp->stream->label, comp->component_id);
+ nr_ice_component_disconnected(comp);
+ break;
+ default:
+ break;
+ }
+ }
+
+int nr_ice_component_refresh_consent(nr_stun_client_ctx *ctx, NR_async_cb finished_cb, void *cb_arg)
+ {
+ int r,_status;
+
+ nr_stun_client_reset(ctx);
+
+ if (r=nr_stun_client_start(ctx, NR_ICE_CLIENT_MODE_BINDING_REQUEST, finished_cb, cb_arg))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+void nr_ice_component_consent_calc_consent_timer(nr_ice_component *comp)
+ {
+ uint16_t trange, trand, tval;
+
+ trange = NR_ICE_CONSENT_TIMER_DEFAULT * 20 / 100;
+ tval = NR_ICE_CONSENT_TIMER_DEFAULT - trange;
+ if (!nr_crypto_random_bytes((UCHAR*)&trand, sizeof(trand)))
+ tval += (trand % (trange * 2));
+
+ if (comp->ctx->test_timer_divider)
+ tval = tval / comp->ctx->test_timer_divider;
+
+ /* The timeout of the transaction is the maximum time until we send the
+ * next consent request. */
+ comp->consent_ctx->maximum_transmits_timeout_ms = tval;
+ }
+
+static void nr_ice_component_consent_timer_cb(NR_SOCKET s, int how, void *cb_arg)
+ {
+ nr_ice_component *comp=cb_arg;
+ int r;
+
+ if (!comp->consent_ctx) {
+ return;
+ }
+
+ if (comp->consent_timer) {
+ NR_async_timer_cancel(comp->consent_timer);
+ }
+ comp->consent_timer = 0;
+
+ comp->consent_ctx->params.ice_binding_request.username =
+ comp->stream->l2r_user;
+ comp->consent_ctx->params.ice_binding_request.password =
+ comp->stream->l2r_pass;
+ comp->consent_ctx->params.ice_binding_request.control =
+ comp->stream->pctx->controlling?
+ NR_ICE_CONTROLLING:NR_ICE_CONTROLLED;
+ comp->consent_ctx->params.ice_binding_request.tiebreaker =
+ comp->stream->pctx->tiebreaker;
+ comp->consent_ctx->params.ice_binding_request.priority =
+ comp->active->local->priority;
+
+ nr_ice_component_consent_calc_consent_timer(comp);
+
+ if (r=nr_ice_component_refresh_consent(comp->consent_ctx,
+ nr_ice_component_refresh_consent_cb,
+ comp)) {
+ r_log(LOG_ICE,LOG_ERR,"ICE(%s)/STREAM(%s)/COMP(%d): Refresh consent failed with %d",
+ comp->ctx->label, comp->stream->label, comp->component_id, r);
+ }
+
+ nr_ice_component_consent_schedule_consent_timer(comp);
+
+ }
+
+void nr_ice_component_consent_schedule_consent_timer(nr_ice_component *comp)
+ {
+ if (!comp->can_send) {
+ return;
+ }
+
+ NR_ASYNC_TIMER_SET(comp->consent_ctx->maximum_transmits_timeout_ms,
+ nr_ice_component_consent_timer_cb, comp,
+ &comp->consent_timer);
+ }
+
+void nr_ice_component_refresh_consent_now(nr_ice_component *comp)
+ {
+ nr_ice_component_consent_timer_cb(0, 0, comp);
+ }
+
+void nr_ice_component_consent_destroy(nr_ice_component *comp)
+ {
+ if (comp->consent_timer) {
+ NR_async_timer_cancel(comp->consent_timer);
+ comp->consent_timer = 0;
+ }
+ if (comp->consent_timeout) {
+ NR_async_timer_cancel(comp->consent_timeout);
+ comp->consent_timeout = 0;
+ }
+ if (comp->consent_handle) {
+ nr_ice_socket_deregister(comp->active->local->isock,
+ comp->consent_handle);
+ comp->consent_handle = 0;
+ }
+ if (comp->consent_ctx) {
+ nr_stun_client_ctx_destroy(&comp->consent_ctx);
+ comp->consent_ctx = 0;
+ }
+ }
+
+int nr_ice_component_setup_consent(nr_ice_component *comp)
+ {
+ int r,_status;
+
+ r_log(LOG_ICE,LOG_DEBUG,"ICE(%s)/STREAM(%s)/COMP(%d): Setting up refresh consent",
+ comp->ctx->label, comp->stream->label, comp->component_id);
+
+ nr_ice_component_consent_destroy(comp);
+
+ if (r=nr_stun_client_ctx_create("consent", comp->active->local->osock,
+ &comp->active->remote->addr, 0,
+ &comp->consent_ctx))
+ ABORT(r);
+ /* Consent request get send only once. */
+ comp->consent_ctx->maximum_transmits = 1;
+
+ if (r=nr_ice_socket_register_stun_client(comp->active->local->isock,
+ comp->consent_ctx, &comp->consent_handle))
+ ABORT(r);
+
+ comp->can_send = 1;
+ comp->disconnected = 0;
+ nr_ice_component_consent_refreshed(comp);
+
+ nr_ice_component_consent_calc_consent_timer(comp);
+ nr_ice_component_consent_schedule_consent_timer(comp);
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_ice_component_nominated_pair(nr_ice_component *comp, nr_ice_cand_pair *pair)
+ {
+ int r,_status;
+ nr_ice_cand_pair *p2;
+
+ /* Are we changing what the nominated pair is? */
+ if(comp->nominated){
+ if(comp->nominated->priority >= pair->priority)
+ return(0);
+ r_log(LOG_ICE,LOG_INFO,"ICE-PEER(%s)/STREAM(%s)/COMP(%d)/CAND-PAIR(%s): replacing pair %s with CAND-PAIR(%s)",comp->stream->pctx->label,comp->stream->label,comp->component_id,comp->nominated->codeword,comp->nominated->as_string,pair->codeword);
+ /* As consent doesn't hold a reference to its isock this needs to happen
+ * before making the new pair the active one. */
+ nr_ice_component_consent_destroy(comp);
+ }
+
+ /* Set the new nominated pair */
+ r_log(LOG_ICE,LOG_INFO,"ICE-PEER(%s)/STREAM(%s)/COMP(%d)/CAND-PAIR(%s): nominated pair is %s",comp->stream->pctx->label,comp->stream->label,comp->component_id,pair->codeword,pair->as_string);
+ comp->state=NR_ICE_COMPONENT_NOMINATED;
+ comp->nominated=pair;
+ comp->active=pair;
+
+ r_log(LOG_ICE,LOG_INFO,"ICE-PEER(%s)/STREAM(%s)/COMP(%d)/CAND-PAIR(%s): cancelling all pairs but %s",comp->stream->pctx->label,comp->stream->label,comp->component_id,pair->codeword,pair->as_string);
+
+ /* Cancel checks in WAITING and FROZEN per ICE S 8.1.2 */
+ p2=TAILQ_FIRST(&comp->stream->trigger_check_queue);
+ while(p2){
+ if((p2 != pair) &&
+ (p2->remote->component->component_id == comp->component_id)) {
+ assert(p2->state == NR_ICE_PAIR_STATE_WAITING ||
+ p2->state == NR_ICE_PAIR_STATE_CANCELLED);
+ r_log(LOG_ICE,LOG_INFO,"ICE-PEER(%s)/STREAM(%s)/COMP(%d)/CAND-PAIR(%s): cancelling FROZEN/WAITING pair %s in trigger check queue because CAND-PAIR(%s) was nominated.",comp->stream->pctx->label,comp->stream->label,comp->component_id,p2->codeword,p2->as_string,pair->codeword);
+
+ nr_ice_candidate_pair_cancel(pair->pctx,p2,0);
+ }
+
+ p2=TAILQ_NEXT(p2,triggered_check_queue_entry);
+ }
+ p2=TAILQ_FIRST(&comp->stream->check_list);
+ while(p2){
+ if((p2 != pair) &&
+ (p2->remote->component->component_id == comp->component_id) &&
+ ((p2->state == NR_ICE_PAIR_STATE_FROZEN) ||
+ (p2->state == NR_ICE_PAIR_STATE_WAITING))) {
+ r_log(LOG_ICE,LOG_INFO,"ICE-PEER(%s)/STREAM(%s)/COMP(%d)/CAND-PAIR(%s): cancelling FROZEN/WAITING pair %s because CAND-PAIR(%s) was nominated.",comp->stream->pctx->label,comp->stream->label,comp->component_id,p2->codeword,p2->as_string,pair->codeword);
+
+ nr_ice_candidate_pair_cancel(pair->pctx,p2,0);
+ }
+
+ p2=TAILQ_NEXT(p2,check_queue_entry);
+ }
+ r_log(LOG_ICE,LOG_DEBUG,"ICE-PEER(%s)/STREAM(%s)/COMP(%d): cancelling done",comp->stream->pctx->label,comp->stream->label,comp->component_id);
+
+ if(r=nr_ice_component_setup_consent(comp))
+ ABORT(r);
+
+ nr_ice_media_stream_component_nominated(comp->stream,comp);
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+static int nr_ice_component_have_all_pairs_failed(nr_ice_component *comp)
+ {
+ nr_ice_cand_pair *p2;
+
+ p2=TAILQ_FIRST(&comp->stream->check_list);
+ while(p2){
+ if(comp->component_id==p2->local->component_id){
+ switch(p2->state){
+ case NR_ICE_PAIR_STATE_FROZEN:
+ case NR_ICE_PAIR_STATE_WAITING:
+ case NR_ICE_PAIR_STATE_IN_PROGRESS:
+ case NR_ICE_PAIR_STATE_SUCCEEDED:
+ return(0);
+ case NR_ICE_PAIR_STATE_FAILED:
+ case NR_ICE_PAIR_STATE_CANCELLED:
+ /* states that will never be recovered from */
+ break;
+ default:
+ assert(0);
+ break;
+ }
+ }
+
+ p2=TAILQ_NEXT(p2,check_queue_entry);
+ }
+
+ return(1);
+ }
+
+void nr_ice_component_failed_pair(nr_ice_component *comp, nr_ice_cand_pair *pair)
+ {
+ nr_ice_component_check_if_failed(comp);
+ }
+
+void nr_ice_component_check_if_failed(nr_ice_component *comp)
+ {
+ if (comp->state == NR_ICE_COMPONENT_RUNNING) {
+ /* Don't do anything to streams that aren't currently running */
+ r_log(LOG_ICE,LOG_DEBUG,"ICE-PEER(%s)/STREAM(%s)/COMP(%d): Checking whether component needs to be marked failed.",comp->stream->pctx->label,comp->stream->label,comp->component_id);
+
+ if (!comp->stream->pctx->trickle_grace_period_timer &&
+ nr_ice_component_have_all_pairs_failed(comp)) {
+ r_log(LOG_ICE,LOG_INFO,"ICE-PEER(%s)/STREAM(%s)/COMP(%d): All pairs are failed, and grace period has elapsed. Marking component as failed.",comp->stream->pctx->label,comp->stream->label,comp->component_id);
+ nr_ice_media_stream_component_failed(comp->stream,comp);
+ }
+ }
+ }
+
+int nr_ice_component_select_pair(nr_ice_peer_ctx *pctx, nr_ice_component *comp)
+ {
+ nr_ice_cand_pair **pairs=0;
+ int ct=0;
+ nr_ice_cand_pair *pair;
+ int r,_status;
+
+ /* Size the array */
+ pair=TAILQ_FIRST(&comp->stream->check_list);
+ while(pair){
+ if (comp->component_id == pair->local->component_id)
+ ct++;
+
+ pair=TAILQ_NEXT(pair,check_queue_entry);
+ }
+
+ /* Make and fill the array */
+ if(!(pairs=RCALLOC(sizeof(nr_ice_cand_pair *)*ct)))
+ ABORT(R_NO_MEMORY);
+
+ ct=0;
+ pair=TAILQ_FIRST(&comp->stream->check_list);
+ while(pair){
+ if (comp->component_id == pair->local->component_id)
+ pairs[ct++]=pair;
+
+ pair=TAILQ_NEXT(pair,check_queue_entry);
+ }
+
+ if (pctx->handler) {
+ if(r=pctx->handler->vtbl->select_pair(pctx->handler->obj,
+ comp->stream,comp->component_id,pairs,ct))
+ ABORT(r);
+ }
+
+ _status=0;
+ abort:
+ RFREE(pairs);
+ return(_status);
+ }
+
+
+/* Close the underlying sockets for everything but the nominated candidate */
+int nr_ice_component_finalize(nr_ice_component *lcomp, nr_ice_component *rcomp)
+ {
+ nr_ice_socket *isock=0;
+ nr_ice_socket *s1,*s2;
+
+ if(rcomp->state==NR_ICE_COMPONENT_NOMINATED){
+ assert(rcomp->active == rcomp->nominated);
+ isock=rcomp->nominated->local->isock;
+ }
+
+ STAILQ_FOREACH_SAFE(s1, &lcomp->sockets, entry, s2){
+ if(s1!=isock){
+ STAILQ_REMOVE(&lcomp->sockets,s1,nr_ice_socket_,entry);
+ nr_ice_socket_destroy(&s1);
+ }
+ }
+
+ return(0);
+ }
+
+
+int nr_ice_component_insert_pair(nr_ice_component *pcomp, nr_ice_cand_pair *pair)
+ {
+ int _status;
+
+ /* Pairs for peer reflexive are marked SUCCEEDED immediately */
+ if (pair->state != NR_ICE_PAIR_STATE_FROZEN &&
+ pair->state != NR_ICE_PAIR_STATE_SUCCEEDED){
+ assert(0);
+ ABORT(R_BAD_ARGS);
+ }
+
+ /* We do not throw an error after this, because we've inserted the pair. */
+ nr_ice_candidate_pair_insert(&pair->remote->stream->check_list,pair);
+
+ /* Make sure the check timer is running, if the stream was previously
+ * started. We will not start streams just because a pair was created,
+ * unless it is the first pair to be created across all streams. */
+ r_log(LOG_ICE,LOG_DEBUG,"ICE-PEER(%s)/CAND-PAIR(%s): Ensure that check timer is running for new pair %s.",pair->remote->stream->pctx->label, pair->codeword, pair->as_string);
+
+ if(pair->remote->stream->ice_state == NR_ICE_MEDIA_STREAM_CHECKS_ACTIVE ||
+ (pair->remote->stream->ice_state == NR_ICE_MEDIA_STREAM_CHECKS_FROZEN &&
+ !pair->remote->stream->pctx->checks_started)){
+ if(nr_ice_media_stream_start_checks(pair->remote->stream->pctx, pair->remote->stream)) {
+ r_log(LOG_ICE,LOG_WARNING,"ICE-PEER(%s)/CAND-PAIR(%s): Could not restart checks for new pair %s.",pair->remote->stream->pctx->label, pair->codeword, pair->as_string);
+ }
+ }
+
+ _status=0;
+ abort:
+ if (_status) {
+ nr_ice_candidate_pair_destroy(&pair);
+ }
+ return(_status);
+ }
+
+int nr_ice_component_get_default_candidate(nr_ice_component *comp, nr_ice_candidate **candp, int ip_version)
+ {
+ int _status;
+ nr_ice_candidate *cand;
+ nr_ice_candidate *best_cand = NULL;
+
+ /* We have the component. Now find the "best" candidate, making
+ use of the fact that more "reliable" candidate types have
+ higher numbers. So, we sort by type and then priority within
+ type
+ */
+ cand=TAILQ_FIRST(&comp->candidates);
+ while(cand){
+ if (!nr_ice_ctx_hide_candidate(comp->ctx, cand) &&
+ cand->addr.ip_version == ip_version) {
+ if (!best_cand) {
+ best_cand = cand;
+ }
+ else if (best_cand->type < cand->type) {
+ best_cand = cand;
+ } else if (best_cand->type == cand->type &&
+ best_cand->priority < cand->priority) {
+ best_cand = cand;
+ }
+ }
+
+ cand=TAILQ_NEXT(cand,entry_comp);
+ }
+
+ /* No candidates */
+ if (!best_cand)
+ ABORT(R_NOT_FOUND);
+
+ *candp = best_cand;
+
+ _status=0;
+ abort:
+ return(_status);
+
+ }
+
+
+void nr_ice_component_dump_state(nr_ice_component *comp, int log_level)
+ {
+ nr_ice_candidate *cand;
+
+ if (comp->local_component) {
+ r_log(LOG_ICE,log_level,"ICE(%s)/ICE-STREAM(%s): Remote component %d in state %d - dumping candidates",comp->ctx->label,comp->stream->label,comp->component_id,comp->state);
+ } else {
+ r_log(LOG_ICE,log_level,"ICE(%s)/ICE-STREAM(%s): Local component %d - dumping candidates",comp->ctx->label,comp->stream->label,comp->component_id);
+ }
+
+ cand=TAILQ_FIRST(&comp->candidates);
+ while(cand){
+ r_log(LOG_ICE,log_level,"ICE(%s)/ICE-STREAM(%s)/CAND(%s): %s",comp->ctx->label,comp->stream->label,cand->codeword,cand->label);
+ cand=TAILQ_NEXT(cand,entry_comp);
+ }
+ }
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_component.h b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_component.h
new file mode 100644
index 0000000000..0b12a68d58
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_component.h
@@ -0,0 +1,111 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+
+#ifndef _ice_component_h
+#define _ice_component_h
+#ifdef __cplusplus
+using namespace std;
+extern "C" {
+#endif /* __cplusplus */
+
+typedef struct nr_ice_pre_answer_request_ {
+ nr_stun_server_request req;
+ char *username;
+ nr_transport_addr local_addr;
+
+ STAILQ_ENTRY(nr_ice_pre_answer_request_) entry;
+} nr_ice_pre_answer_request;
+
+typedef STAILQ_HEAD(nr_ice_pre_answer_request_head_, nr_ice_pre_answer_request_) nr_ice_pre_answer_request_head;
+
+struct nr_ice_component_ {
+ int state;
+#define NR_ICE_COMPONENT_UNPAIRED 0
+#define NR_ICE_COMPONENT_RUNNING 1
+#define NR_ICE_COMPONENT_NOMINATED 2
+#define NR_ICE_COMPONENT_FAILED 3
+#define NR_ICE_COMPONENT_DISABLED 4
+ struct nr_ice_ctx_ *ctx;
+ struct nr_ice_media_stream_ *stream;
+ nr_ice_component *local_component;
+
+ int component_id;
+ nr_ice_socket_head sockets;
+ nr_ice_candidate_head candidates;
+ int candidate_ct;
+ nr_ice_pre_answer_request_head pre_answer_reqs;
+
+ int valid_pairs;
+ struct nr_ice_cand_pair_ *nominated; /* Highest priority nomninated pair */
+ struct nr_ice_cand_pair_ *active;
+
+ nr_stun_client_ctx *consent_ctx;
+ void *consent_timer;
+ void *consent_timeout;
+ void *consent_handle;
+ int can_send;
+ int disconnected;
+ struct timeval consent_last_seen;
+
+ STAILQ_ENTRY(nr_ice_component_)entry;
+};
+
+typedef STAILQ_HEAD(nr_ice_component_head_,nr_ice_component_) nr_ice_component_head;
+
+int nr_ice_component_create(struct nr_ice_media_stream_ *stream, int component_id, nr_ice_component **componentp);
+int nr_ice_component_destroy(nr_ice_component **componentp);
+int nr_ice_component_initialize(struct nr_ice_ctx_ *ctx,nr_ice_component *component);
+void nr_ice_component_stop_gathering(nr_ice_component *component);
+int nr_ice_component_is_done_gathering(nr_ice_component *comp);
+int nr_ice_component_maybe_prune_candidate(nr_ice_ctx *ctx, nr_ice_component *comp, nr_ice_candidate *c1, int *was_pruned);
+int nr_ice_component_pair_candidate(nr_ice_peer_ctx *pctx, nr_ice_component *pcomp, nr_ice_candidate *lcand, int pair_all_remote);
+int nr_ice_component_pair_candidates(nr_ice_peer_ctx *pctx, nr_ice_component *lcomp, nr_ice_component *pcomp);
+int nr_ice_component_service_pre_answer_requests(nr_ice_peer_ctx *pctx, nr_ice_component *pcomp, char *username, int *serviced);
+int nr_ice_component_nominated_pair(nr_ice_component *comp, nr_ice_cand_pair *pair);
+void nr_ice_component_failed_pair(nr_ice_component *comp, nr_ice_cand_pair *pair);
+void nr_ice_component_check_if_failed(nr_ice_component *comp);
+int nr_ice_component_select_pair(nr_ice_peer_ctx *pctx, nr_ice_component *comp);
+int nr_ice_component_set_failed(nr_ice_component *comp);
+int nr_ice_component_finalize(nr_ice_component *lcomp, nr_ice_component *rcomp);
+int nr_ice_component_insert_pair(nr_ice_component *pcomp, nr_ice_cand_pair *pair);
+int nr_ice_component_get_default_candidate(nr_ice_component *comp, nr_ice_candidate **candp, int ip_version);
+void nr_ice_component_consent_destroy(nr_ice_component *comp);
+void nr_ice_component_refresh_consent_now(nr_ice_component *comp);
+void nr_ice_component_disconnected(nr_ice_component *comp);
+void nr_ice_component_dump_state(nr_ice_component *comp, int log_level);
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+#endif
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_ctx.c b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_ctx.c
new file mode 100644
index 0000000000..0d498845a4
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_ctx.c
@@ -0,0 +1,1125 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <csi_platform.h>
+#include <assert.h>
+#include <sys/types.h>
+#ifdef WIN32
+#include <winsock2.h>
+#else
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#endif
+#include <sys/queue.h>
+#include <string.h>
+#include <nr_api.h>
+#include <registry.h>
+#include "stun.h"
+#include "ice_ctx.h"
+#include "ice_reg.h"
+#include "nr_crypto.h"
+#include "async_timer.h"
+#include "util.h"
+#include "nr_socket_local.h"
+
+#define ICE_UFRAG_LEN 8
+#define ICE_PWD_LEN 32
+
+int LOG_ICE = 0;
+
+static int nr_ice_random_string(char *str, int len);
+static int nr_ice_fetch_stun_servers(int ct, nr_ice_stun_server **out);
+#ifdef USE_TURN
+static int nr_ice_fetch_turn_servers(int ct, nr_ice_turn_server **out);
+#endif /* USE_TURN */
+static int nr_ice_ctx_pair_new_trickle_candidates(nr_ice_ctx *ctx, nr_ice_candidate *cand);
+static int no_op(void **obj) {
+ return 0;
+}
+
+static nr_socket_factory_vtbl default_socket_factory_vtbl = {
+ nr_socket_local_create,
+ no_op
+};
+
+int nr_ice_fetch_stun_servers(int ct, nr_ice_stun_server **out)
+ {
+ int r,_status;
+ nr_ice_stun_server *servers = 0;
+ int i;
+ NR_registry child;
+ char *addr=0;
+ UINT2 port;
+ in_addr_t addr_int;
+
+ if(!(servers=RCALLOC(sizeof(nr_ice_stun_server)*ct)))
+ ABORT(R_NO_MEMORY);
+
+ for(i=0;i<ct;i++){
+ if(r=NR_reg_get_child_registry(NR_ICE_REG_STUN_SRV_PRFX,i,child))
+ ABORT(r);
+ /* Assume we have a v4 addr for now */
+ if(r=NR_reg_alloc2_string(child,"addr",&addr))
+ ABORT(r);
+ addr_int=inet_addr(addr);
+ if(addr_int==INADDR_NONE){
+ r_log(LOG_ICE,LOG_ERR,"Invalid address %s;",addr);
+ ABORT(R_BAD_ARGS);
+ }
+ if(r=NR_reg_get2_uint2(child,"port",&port)) {
+ if (r != R_NOT_FOUND)
+ ABORT(r);
+ port = 3478;
+ }
+ if (r = nr_ip4_port_to_transport_addr(ntohl(addr_int), port, IPPROTO_UDP,
+ &servers[i].addr))
+ ABORT(r);
+ RFREE(addr);
+ addr=0;
+ }
+
+ *out = servers;
+
+ _status=0;
+ abort:
+ RFREE(addr);
+ if (_status) RFREE(servers);
+ return(_status);
+ }
+
+int nr_ice_ctx_set_stun_servers(nr_ice_ctx *ctx,nr_ice_stun_server *servers,int ct)
+ {
+ int _status;
+
+ if(ctx->stun_servers_cfg){
+ RFREE(ctx->stun_servers_cfg);
+ ctx->stun_servers_cfg=NULL;
+ ctx->stun_server_ct_cfg=0;
+ }
+
+ if (ct) {
+ if(!(ctx->stun_servers_cfg=RCALLOC(sizeof(nr_ice_stun_server)*ct)))
+ ABORT(R_NO_MEMORY);
+
+ memcpy(ctx->stun_servers_cfg,servers,sizeof(nr_ice_stun_server)*ct);
+ ctx->stun_server_ct_cfg = ct;
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_ice_ctx_set_turn_servers(nr_ice_ctx *ctx,nr_ice_turn_server *servers,int ct)
+ {
+ int _status;
+
+ if(ctx->turn_servers_cfg){
+ for (int i = 0; i < ctx->turn_server_ct_cfg; i++) {
+ RFREE(ctx->turn_servers_cfg[i].username);
+ r_data_destroy(&ctx->turn_servers_cfg[i].password);
+ }
+ RFREE(ctx->turn_servers_cfg);
+ ctx->turn_servers_cfg=NULL;
+ ctx->turn_server_ct_cfg=0;
+ }
+
+ if(ct) {
+ if(!(ctx->turn_servers_cfg=RCALLOC(sizeof(nr_ice_turn_server)*ct)))
+ ABORT(R_NO_MEMORY);
+
+ memcpy(ctx->turn_servers_cfg,servers,sizeof(nr_ice_turn_server)*ct);
+ ctx->turn_server_ct_cfg = ct;
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_ice_ctx_copy_turn_servers(nr_ice_ctx *ctx, nr_ice_turn_server *servers, int ct)
+ {
+ int _status, i, r;
+
+ if (r = nr_ice_ctx_set_turn_servers(ctx, servers, ct)) {
+ ABORT(r);
+ }
+
+ // make copies of the username and password so they aren't freed twice
+ for (i = 0; i < ct; ++i) {
+ if (!(ctx->turn_servers_cfg[i].username = r_strdup(servers[i].username))) {
+ ABORT(R_NO_MEMORY);
+ }
+ if (r = r_data_create(&ctx->turn_servers_cfg[i].password,
+ servers[i].password->data,
+ servers[i].password->len)) {
+ ABORT(r);
+ }
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+static int nr_ice_ctx_set_local_addrs(nr_ice_ctx *ctx,nr_local_addr *addrs,int ct)
+ {
+ int _status,i,r;
+
+ if(ctx->local_addrs) {
+ RFREE(ctx->local_addrs);
+ ctx->local_addr_ct=0;
+ ctx->local_addrs=0;
+ }
+
+ if (ct) {
+ if(!(ctx->local_addrs=RCALLOC(sizeof(nr_local_addr)*ct)))
+ ABORT(R_NO_MEMORY);
+
+ for (i=0;i<ct;++i) {
+ if (r=nr_local_addr_copy(ctx->local_addrs+i,addrs+i)) {
+ ABORT(r);
+ }
+ }
+ ctx->local_addr_ct = ct;
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_ice_ctx_set_resolver(nr_ice_ctx *ctx, nr_resolver *resolver)
+ {
+ int _status;
+
+ if (ctx->resolver) {
+ ABORT(R_ALREADY);
+ }
+
+ ctx->resolver = resolver;
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_ice_ctx_set_interface_prioritizer(nr_ice_ctx *ctx, nr_interface_prioritizer *ip)
+ {
+ int _status;
+
+ if (ctx->interface_prioritizer) {
+ ABORT(R_ALREADY);
+ }
+
+ ctx->interface_prioritizer = ip;
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+void nr_ice_ctx_set_socket_factory(nr_ice_ctx *ctx, nr_socket_factory *factory)
+ {
+ nr_socket_factory_destroy(&ctx->socket_factory);
+ ctx->socket_factory = factory;
+ }
+
+#ifdef USE_TURN
+int nr_ice_fetch_turn_servers(int ct, nr_ice_turn_server **out)
+ {
+ int r,_status;
+ nr_ice_turn_server *servers = 0;
+ int i;
+ NR_registry child;
+ char *addr=0;
+ UINT2 port;
+ in_addr_t addr_int;
+ Data data={0};
+
+ if(!(servers=RCALLOC(sizeof(nr_ice_turn_server)*ct)))
+ ABORT(R_NO_MEMORY);
+
+ for(i=0;i<ct;i++){
+ if(r=NR_reg_get_child_registry(NR_ICE_REG_TURN_SRV_PRFX,i,child))
+ ABORT(r);
+ /* Assume we have a v4 addr for now */
+ if(r=NR_reg_alloc2_string(child,"addr",&addr))
+ ABORT(r);
+ addr_int=inet_addr(addr);
+ if(addr_int==INADDR_NONE){
+ r_log(LOG_ICE,LOG_ERR,"Invalid address %s",addr);
+ ABORT(R_BAD_ARGS);
+ }
+ if(r=NR_reg_get2_uint2(child,"port",&port)) {
+ if (r != R_NOT_FOUND)
+ ABORT(r);
+ port = 3478;
+ }
+ if (r = nr_ip4_port_to_transport_addr(ntohl(addr_int), port, IPPROTO_UDP,
+ &servers[i].turn_server.addr))
+ ABORT(r);
+
+
+ if(r=NR_reg_alloc2_string(child,NR_ICE_REG_TURN_SRV_USERNAME,&servers[i].username)){
+ if(r!=R_NOT_FOUND)
+ ABORT(r);
+ }
+
+ if(r=NR_reg_alloc2_data(child,NR_ICE_REG_TURN_SRV_PASSWORD,&data)){
+ if(r!=R_NOT_FOUND)
+ ABORT(r);
+ }
+ else {
+ servers[i].password=RCALLOC(sizeof(*servers[i].password));
+ if(!servers[i].password)
+ ABORT(R_NO_MEMORY);
+ servers[i].password->data = data.data;
+ servers[i].password->len = data.len;
+ data.data=0;
+ }
+
+ RFREE(addr);
+ addr=0;
+ }
+
+ *out = servers;
+
+ _status=0;
+ abort:
+ RFREE(data.data);
+ RFREE(addr);
+ if (_status) RFREE(servers);
+ return(_status);
+ }
+#endif /* USE_TURN */
+
+#define MAXADDRS 100 /* Ridiculously high */
+int nr_ice_ctx_create(char *label, UINT4 flags, nr_ice_ctx **ctxp)
+ {
+ nr_ice_ctx *ctx=0;
+ int r,_status;
+
+ if(r=r_log_register("ice", &LOG_ICE))
+ ABORT(r);
+
+ if(!(ctx=RCALLOC(sizeof(nr_ice_ctx))))
+ ABORT(R_NO_MEMORY);
+
+ ctx->flags=flags;
+
+ if(!(ctx->label=r_strdup(label)))
+ ABORT(R_NO_MEMORY);
+
+ /* Get the STUN servers */
+ if(r=NR_reg_get_child_count(NR_ICE_REG_STUN_SRV_PRFX,
+ (unsigned int *)&ctx->stun_server_ct_cfg)||ctx->stun_server_ct_cfg==0) {
+ r_log(LOG_ICE,LOG_DEBUG,"ICE(%s): No STUN servers specified in nICEr registry", ctx->label);
+ ctx->stun_server_ct_cfg=0;
+ }
+
+ /* 31 is the max for our priority algorithm */
+ if(ctx->stun_server_ct_cfg>31){
+ r_log(LOG_ICE,LOG_WARNING,"ICE(%s): Too many STUN servers specified: max=31", ctx->label);
+ ctx->stun_server_ct_cfg=31;
+ }
+
+ if(ctx->stun_server_ct_cfg>0){
+ if(r=nr_ice_fetch_stun_servers(ctx->stun_server_ct_cfg,&ctx->stun_servers_cfg)){
+ r_log(LOG_ICE,LOG_ERR,"ICE(%s): Couldn't load STUN servers from registry", ctx->label);
+ ctx->stun_server_ct_cfg=0;
+ ABORT(r);
+ }
+ }
+
+#ifdef USE_TURN
+ /* Get the TURN servers */
+ if(r=NR_reg_get_child_count(NR_ICE_REG_TURN_SRV_PRFX,
+ (unsigned int *)&ctx->turn_server_ct_cfg)||ctx->turn_server_ct_cfg==0) {
+ r_log(LOG_ICE,LOG_DEBUG,"ICE(%s): No TURN servers specified in nICEr registry", ctx->label);
+ ctx->turn_server_ct_cfg=0;
+ }
+#else
+ ctx->turn_server_ct_cfg=0;
+#endif /* USE_TURN */
+
+ ctx->local_addrs=0;
+ ctx->local_addr_ct=0;
+
+ /* 31 is the max for our priority algorithm */
+ if((ctx->stun_server_ct_cfg+ctx->turn_server_ct_cfg)>31){
+ r_log(LOG_ICE,LOG_WARNING,"ICE(%s): Too many STUN/TURN servers specified: max=31", ctx->label);
+ ctx->turn_server_ct_cfg=31-ctx->stun_server_ct_cfg;
+ }
+
+#ifdef USE_TURN
+ if(ctx->turn_server_ct_cfg>0){
+ if(r=nr_ice_fetch_turn_servers(ctx->turn_server_ct_cfg,&ctx->turn_servers_cfg)){
+ ctx->turn_server_ct_cfg=0;
+ r_log(LOG_ICE,LOG_ERR,"ICE(%s): Couldn't load TURN servers from registry", ctx->label);
+ ABORT(r);
+ }
+ }
+#endif /* USE_TURN */
+
+
+ ctx->Ta = 20;
+
+ ctx->test_timer_divider = 0;
+
+ if (r=nr_socket_factory_create_int(NULL, &default_socket_factory_vtbl, &ctx->socket_factory))
+ ABORT(r);
+
+ if ((r=NR_reg_get_string((char *)NR_ICE_REG_PREF_FORCE_INTERFACE_NAME, ctx->force_net_interface, sizeof(ctx->force_net_interface)))) {
+ if (r == R_NOT_FOUND) {
+ ctx->force_net_interface[0] = 0;
+ } else {
+ ABORT(r);
+ }
+ }
+
+ ctx->target_for_default_local_address_lookup=0;
+
+ STAILQ_INIT(&ctx->streams);
+ STAILQ_INIT(&ctx->sockets);
+ STAILQ_INIT(&ctx->foundations);
+ STAILQ_INIT(&ctx->peers);
+ STAILQ_INIT(&ctx->ids);
+
+ *ctxp=ctx;
+
+ _status=0;
+ abort:
+ if (_status && ctx) nr_ice_ctx_destroy(&ctx);
+
+ return(_status);
+ }
+
+ void nr_ice_ctx_add_flags(nr_ice_ctx* ctx, UINT4 flags) {
+ ctx->flags |= flags;
+ }
+
+ void nr_ice_ctx_remove_flags(nr_ice_ctx* ctx, UINT4 flags) {
+ ctx->flags &= ~flags;
+ }
+
+ void nr_ice_ctx_destroy(nr_ice_ctx** ctxp) {
+ if (!ctxp || !*ctxp) return;
+
+ nr_ice_ctx* ctx = *ctxp;
+ nr_ice_foundation *f1,*f2;
+ nr_ice_media_stream *s1,*s2;
+ int i;
+ nr_ice_stun_id *id1,*id2;
+
+ ctx->done_cb = 0;
+ ctx->trickle_cb = 0;
+
+ STAILQ_FOREACH_SAFE(s1, &ctx->streams, entry, s2){
+ STAILQ_REMOVE(&ctx->streams,s1,nr_ice_media_stream_,entry);
+ nr_ice_media_stream_destroy(&s1);
+ }
+
+ RFREE(ctx->label);
+
+ RFREE(ctx->stun_servers_cfg);
+
+ RFREE(ctx->local_addrs);
+
+ RFREE(ctx->target_for_default_local_address_lookup);
+
+ for (i = 0; i < ctx->turn_server_ct_cfg; i++) {
+ RFREE(ctx->turn_servers_cfg[i].username);
+ r_data_destroy(&ctx->turn_servers_cfg[i].password);
+ }
+ RFREE(ctx->turn_servers_cfg);
+
+ f1=STAILQ_FIRST(&ctx->foundations);
+ while(f1){
+ f2=STAILQ_NEXT(f1,entry);
+ RFREE(f1);
+ f1=f2;
+ }
+
+ STAILQ_FOREACH_SAFE(id1, &ctx->ids, entry, id2){
+ STAILQ_REMOVE(&ctx->ids,id1,nr_ice_stun_id_,entry);
+ RFREE(id1);
+ }
+
+ nr_resolver_destroy(&ctx->resolver);
+ nr_interface_prioritizer_destroy(&ctx->interface_prioritizer);
+ nr_socket_factory_destroy(&ctx->socket_factory);
+
+ RFREE(ctx);
+
+ *ctxp=0;
+ }
+
+void nr_ice_gather_finished_cb(NR_SOCKET s, int h, void *cb_arg)
+ {
+ int r;
+ nr_ice_candidate *cand=cb_arg;
+ nr_ice_ctx *ctx;
+ nr_ice_media_stream *stream;
+ int component_id;
+
+ assert(cb_arg);
+ if (!cb_arg)
+ return;
+ ctx = cand->ctx;
+ stream = cand->stream;
+ component_id = cand->component_id;
+
+ ctx->uninitialized_candidates--;
+ if (cand->state == NR_ICE_CAND_STATE_FAILED) {
+ r_log(LOG_ICE, LOG_WARNING,
+ "ICE(%s)/CAND(%s): failed to initialize, %d remaining", ctx->label,
+ cand->label, ctx->uninitialized_candidates);
+ } else {
+ r_log(LOG_ICE, LOG_DEBUG, "ICE(%s)/CAND(%s): initialized, %d remaining",
+ ctx->label, cand->label, ctx->uninitialized_candidates);
+ }
+
+ /* Avoid the need for yet another initialization function */
+ if (cand->state == NR_ICE_CAND_STATE_INITIALIZING && cand->type == HOST)
+ cand->state = NR_ICE_CAND_STATE_INITIALIZED;
+
+ if (cand->state == NR_ICE_CAND_STATE_INITIALIZED) {
+ int was_pruned = 0;
+
+ if (r=nr_ice_component_maybe_prune_candidate(ctx, cand->component,
+ cand, &was_pruned)) {
+ r_log(LOG_ICE, LOG_NOTICE, "ICE(%s): Problem pruning candidates",ctx->label);
+ }
+
+ if (was_pruned) {
+ cand = NULL;
+ }
+
+ /* If we are initialized, the candidate wasn't pruned,
+ and we have a trickle ICE callback fire the callback */
+ if (ctx->trickle_cb && cand &&
+ !nr_ice_ctx_hide_candidate(ctx, cand)) {
+ ctx->trickle_cb(ctx->trickle_cb_arg, ctx, cand->stream, cand->component_id, cand);
+
+ if (nr_ice_ctx_pair_new_trickle_candidates(ctx, cand)) {
+ r_log(LOG_ICE,LOG_ERR, "ICE(%s): All could not pair new trickle candidate",ctx->label);
+ /* But continue */
+ }
+ }
+ }
+
+ if (nr_ice_media_stream_is_done_gathering(stream) &&
+ ctx->trickle_cb) {
+ ctx->trickle_cb(ctx->trickle_cb_arg, ctx, stream, component_id, NULL);
+ }
+
+ if(ctx->uninitialized_candidates==0){
+ r_log(LOG_ICE, LOG_INFO, "ICE(%s): All candidates initialized",
+ ctx->label);
+ if (ctx->done_cb) {
+ ctx->done_cb(0,0,ctx->cb_arg);
+ }
+ else {
+ r_log(LOG_ICE, LOG_INFO,
+ "ICE(%s): No done_cb. We were probably destroyed.", ctx->label);
+ }
+ }
+ else {
+ r_log(LOG_ICE,LOG_DEBUG,"ICE(%s): Waiting for %d candidates to be initialized",ctx->label, ctx->uninitialized_candidates);
+ }
+ }
+
+static int nr_ice_ctx_pair_new_trickle_candidates(nr_ice_ctx *ctx, nr_ice_candidate *cand)
+ {
+ int r,_status;
+ nr_ice_peer_ctx *pctx;
+
+ pctx=STAILQ_FIRST(&ctx->peers);
+ while(pctx){
+ if (pctx->state == NR_ICE_PEER_STATE_PAIRED) {
+ r = nr_ice_peer_ctx_pair_new_trickle_candidate(ctx, pctx, cand);
+ if (r)
+ ABORT(r);
+ }
+
+ pctx=STAILQ_NEXT(pctx,entry);
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+/* Get the default address by creating a UDP socket, binding it to a wildcard
+ address, and connecting it to the remote IP. Because this is UDP, no packets
+ are sent. This lets us query the local address assigned to the socket by the
+ kernel.
+
+ If the context's remote address is NULL, then the application wasn't loaded
+ over the network, and we can fall back on connecting to a known public
+ address (namely Google's):
+
+ IPv4: 8.8.8.8
+ IPv6: 2001:4860:4860::8888
+*/
+static int nr_ice_get_default_address(nr_ice_ctx *ctx, int ip_version, nr_transport_addr* addrp)
+ {
+ int r,_status;
+ nr_transport_addr addr, known_remote_addr;
+ nr_transport_addr *remote_addr=ctx->target_for_default_local_address_lookup;
+ nr_socket *sock=0;
+
+ switch(ip_version) {
+ case NR_IPV4:
+ if ((r=nr_str_port_to_transport_addr("0.0.0.0", 0, IPPROTO_UDP, &addr)))
+ ABORT(r);
+ if (!remote_addr || nr_transport_addr_is_loopback(remote_addr)) {
+ if ((r=nr_str_port_to_transport_addr("8.8.8.8", 53, IPPROTO_UDP, &known_remote_addr)))
+ ABORT(r);
+ remote_addr=&known_remote_addr;
+ }
+ break;
+ case NR_IPV6:
+ if ((r=nr_str_port_to_transport_addr("::0", 0, IPPROTO_UDP, &addr)))
+ ABORT(r);
+ if (!remote_addr || nr_transport_addr_is_loopback(remote_addr)) {
+ if ((r=nr_str_port_to_transport_addr("2001:4860:4860::8888", 53, IPPROTO_UDP, &known_remote_addr)))
+ ABORT(r);
+ remote_addr=&known_remote_addr;
+ }
+ break;
+ default:
+ assert(0);
+ ABORT(R_INTERNAL);
+ }
+
+ if ((r=nr_socket_factory_create_socket(ctx->socket_factory, &addr, &sock)))
+ ABORT(r);
+ if ((r=nr_socket_connect(sock, remote_addr)))
+ ABORT(r);
+ if ((r=nr_socket_getaddr(sock, addrp)))
+ ABORT(r);
+
+ r_log(LOG_GENERIC, LOG_DEBUG, "Default address: %s", addrp->as_string);
+
+ _status=0;
+ abort:
+ nr_socket_destroy(&sock);
+ return(_status);
+ }
+
+static int nr_ice_get_default_local_address(nr_ice_ctx *ctx, int ip_version, nr_local_addr* addrs, int addr_ct, nr_local_addr *addrp)
+ {
+ int r,_status;
+ nr_transport_addr default_addr;
+ int i;
+
+ if ((r=nr_ice_get_default_address(ctx, ip_version, &default_addr)))
+ ABORT(r);
+
+ for (i=0; i < addr_ct; ++i) {
+ // if default addr is found in local addrs, copy the more fully
+ // complete local addr to the output arg. Don't need to worry
+ // about comparing ports here.
+ if (!nr_transport_addr_cmp(&default_addr, &addrs[i].addr,
+ NR_TRANSPORT_ADDR_CMP_MODE_ADDR)) {
+ if ((r=nr_local_addr_copy(addrp, &addrs[i])))
+ ABORT(r);
+ break;
+ }
+ }
+
+ // if default addr is not in local addrs, just copy the transport addr
+ // to output arg.
+ if (i == addr_ct) {
+ if ((r=nr_transport_addr_copy(&addrp->addr, &default_addr)))
+ ABORT(r);
+ (void)strlcpy(addrp->addr.ifname, "default route", sizeof(addrp->addr.ifname));
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+/* if handed a IPv4 default_local_addr, looks for IPv6 address on same interface
+ if handed a IPv6 default_local_addr, looks for IPv4 address on same interface
+*/
+static int nr_ice_get_assoc_interface_address(nr_local_addr* default_local_addr,
+ nr_local_addr* local_addrs, int addr_ct,
+ nr_local_addr* assoc_addrp)
+ {
+ int r, _status;
+ int i, ip_version;
+
+ if (!default_local_addr || !local_addrs || !addr_ct) {
+ ABORT(R_BAD_ARGS);
+ }
+
+ /* set _status to R_EOD in case we don't find an associated address */
+ _status = R_EOD;
+
+ /* look for IPv6 if we have IPv4, look for IPv4 if we have IPv6 */
+ ip_version = (NR_IPV4 == default_local_addr->addr.ip_version?NR_IPV6:NR_IPV4);
+
+ for (i=0; i<addr_ct; ++i) {
+ /* if we find the ip_version we're looking for on the matching interface,
+ copy it to assoc_addrp.
+ */
+ if (local_addrs[i].addr.ip_version == ip_version &&
+ !strcmp(local_addrs[i].addr.ifname, default_local_addr->addr.ifname)) {
+ if (r=nr_local_addr_copy(assoc_addrp, &local_addrs[i])) {
+ ABORT(r);
+ }
+ _status = 0;
+ break;
+ }
+ }
+
+ abort:
+ return(_status);
+ }
+
+int nr_ice_set_local_addresses(nr_ice_ctx *ctx,
+ nr_local_addr* stun_addrs, int stun_addr_ct)
+ {
+ int r,_status;
+ nr_local_addr local_addrs[MAXADDRS];
+ nr_local_addr *addrs = 0;
+ int i,addr_ct;
+ nr_local_addr default_addrs[2];
+ int default_addr_ct = 0;
+
+ if (!stun_addrs || !stun_addr_ct) {
+ r_log(LOG_ICE,LOG_ERR,"ICE(%s): no stun addrs provided",ctx->label);
+ ABORT(R_BAD_ARGS);
+ }
+
+ addr_ct = MIN(stun_addr_ct, MAXADDRS);
+ r_log(LOG_ICE, LOG_DEBUG, "ICE(%s): copy %d pre-fetched stun addrs", ctx->label, addr_ct);
+ for (i=0; i<addr_ct; ++i) {
+ if (r=nr_local_addr_copy(&local_addrs[i], &stun_addrs[i])) {
+ ABORT(r);
+ }
+ }
+
+ // removes duplicates and, based on prefs, loopback and link_local addrs
+ if (r=nr_stun_filter_local_addresses(local_addrs, &addr_ct)) {
+ ABORT(r);
+ }
+
+ if (ctx->force_net_interface[0] && addr_ct) {
+ /* Limit us to only addresses on a single interface */
+ int force_addr_ct = 0;
+ for(i=0;i<addr_ct;i++){
+ if (!strcmp(local_addrs[i].addr.ifname, ctx->force_net_interface)) {
+ // copy it down in the array, if needed
+ if (i != force_addr_ct) {
+ if (r=nr_local_addr_copy(&local_addrs[force_addr_ct], &local_addrs[i])) {
+ ABORT(r);
+ }
+ }
+ force_addr_ct++;
+ }
+ }
+ addr_ct = force_addr_ct;
+ }
+
+ r_log(LOG_ICE, LOG_DEBUG,
+ "ICE(%s): use only default local addresses: %s\n",
+ ctx->label,
+ (char*)(ctx->flags & NR_ICE_CTX_FLAGS_ONLY_DEFAULT_ADDRS?"yes":"no"));
+ if ((!addr_ct) || (ctx->flags & NR_ICE_CTX_FLAGS_ONLY_DEFAULT_ADDRS)) {
+ if (ctx->target_for_default_local_address_lookup) {
+ /* Get just the default IPv4 or IPv6 addr */
+ if(!nr_ice_get_default_local_address(
+ ctx, ctx->target_for_default_local_address_lookup->ip_version,
+ local_addrs, addr_ct, &default_addrs[default_addr_ct])) {
+ nr_local_addr *new_addr = &default_addrs[default_addr_ct];
+
+ ++default_addr_ct;
+
+ /* If we have a default target address, check for an associated
+ address on the same interface. For example, if the default
+ target address is IPv6, this will find an associated IPv4
+ address on the same interface.
+ This makes ICE w/ dual stacks work better - Bug 1609124.
+ */
+ if(!nr_ice_get_assoc_interface_address(
+ new_addr, local_addrs, addr_ct,
+ &default_addrs[default_addr_ct])) {
+ ++default_addr_ct;
+ }
+ }
+ } else {
+ /* Get just the default IPv4 and IPv6 addrs */
+ if(!nr_ice_get_default_local_address(ctx, NR_IPV4, local_addrs, addr_ct,
+ &default_addrs[default_addr_ct])) {
+ ++default_addr_ct;
+ }
+ if(!nr_ice_get_default_local_address(ctx, NR_IPV6, local_addrs, addr_ct,
+ &default_addrs[default_addr_ct])) {
+ ++default_addr_ct;
+ }
+ }
+ if (!default_addr_ct) {
+ r_log(LOG_ICE,LOG_ERR,"ICE(%s): failed to find default addresses",ctx->label);
+ ABORT(R_FAILED);
+ }
+ addrs = default_addrs;
+ addr_ct = default_addr_ct;
+ }
+ else {
+ addrs = local_addrs;
+ }
+
+ /* Sort interfaces by preference */
+ if(ctx->interface_prioritizer) {
+ for(i=0;i<addr_ct;i++){
+ if((r=nr_interface_prioritizer_add_interface(ctx->interface_prioritizer,addrs+i)) && (r!=R_ALREADY)) {
+ r_log(LOG_ICE,LOG_ERR,"ICE(%s): unable to add interface ",ctx->label);
+ ABORT(r);
+ }
+ }
+ if(r=nr_interface_prioritizer_sort_preference(ctx->interface_prioritizer)) {
+ r_log(LOG_ICE,LOG_ERR,"ICE(%s): unable to sort interface by preference",ctx->label);
+ ABORT(r);
+ }
+ }
+
+ if (r=nr_ice_ctx_set_local_addrs(ctx,addrs,addr_ct)) {
+ ABORT(r);
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_ice_set_target_for_default_local_address_lookup(nr_ice_ctx *ctx, const char *target_ip, UINT2 target_port)
+ {
+ int r,_status;
+
+ if (ctx->target_for_default_local_address_lookup) {
+ RFREE(ctx->target_for_default_local_address_lookup);
+ ctx->target_for_default_local_address_lookup=0;
+ }
+
+ if (!(ctx->target_for_default_local_address_lookup=RCALLOC(sizeof(nr_transport_addr))))
+ ABORT(R_NO_MEMORY);
+
+ if ((r=nr_str_port_to_transport_addr(target_ip, target_port, IPPROTO_UDP, ctx->target_for_default_local_address_lookup))) {
+ RFREE(ctx->target_for_default_local_address_lookup);
+ ctx->target_for_default_local_address_lookup=0;
+ ABORT(r);
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_ice_gather(nr_ice_ctx *ctx, NR_async_cb done_cb, void *cb_arg)
+ {
+ int r,_status;
+ nr_ice_media_stream *stream;
+ nr_local_addr stun_addrs[MAXADDRS];
+ int stun_addr_ct;
+
+ if (!ctx->local_addrs) {
+ if((r=nr_stun_find_local_addresses(stun_addrs,MAXADDRS,&stun_addr_ct))) {
+ ABORT(r);
+ }
+ if((r=nr_ice_set_local_addresses(ctx,stun_addrs,stun_addr_ct))) {
+ ABORT(r);
+ }
+ }
+
+ if(STAILQ_EMPTY(&ctx->streams)) {
+ r_log(LOG_ICE,LOG_ERR,"ICE(%s): Missing streams to initialize",ctx->label);
+ ABORT(R_BAD_ARGS);
+ }
+
+ r_log(LOG_ICE,LOG_DEBUG,"ICE(%s): Initializing candidates",ctx->label);
+ ctx->done_cb=done_cb;
+ ctx->cb_arg=cb_arg;
+
+ /* Initialize all the media stream/component pairs */
+ stream=STAILQ_FIRST(&ctx->streams);
+ while(stream){
+ if(!stream->obsolete) {
+ if(r=nr_ice_media_stream_initialize(ctx,stream)) {
+ r_log(LOG_ICE,LOG_ERR,"ICE(%s): Failed to initialize a stream; this might not be an unrecoverable error, if this stream will be marked obsolete soon due to an ICE restart.",ctx->label);
+ }
+ }
+
+ stream=STAILQ_NEXT(stream,entry);
+ }
+
+ if(ctx->uninitialized_candidates)
+ ABORT(R_WOULDBLOCK);
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_ice_add_media_stream(nr_ice_ctx *ctx,const char *label,const char *ufrag,const char *pwd,int components, nr_ice_media_stream **streamp)
+ {
+ int r,_status;
+
+ if(r=nr_ice_media_stream_create(ctx,label,ufrag,pwd,components,streamp))
+ ABORT(r);
+
+ STAILQ_INSERT_TAIL(&ctx->streams,*streamp,entry);
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_ice_remove_media_stream(nr_ice_ctx *ctx,nr_ice_media_stream **streamp)
+ {
+ int r,_status;
+ nr_ice_peer_ctx *pctx;
+ nr_ice_media_stream *peer_stream;
+
+ pctx=STAILQ_FIRST(&ctx->peers);
+ while(pctx){
+ if(!nr_ice_peer_ctx_find_pstream(pctx, *streamp, &peer_stream)) {
+ if(r=nr_ice_peer_ctx_remove_pstream(pctx, &peer_stream)) {
+ ABORT(r);
+ }
+ }
+
+ pctx=STAILQ_NEXT(pctx,entry);
+ }
+
+ STAILQ_REMOVE(&ctx->streams,*streamp,nr_ice_media_stream_,entry);
+ if(r=nr_ice_media_stream_destroy(streamp)) {
+ ABORT(r);
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_ice_get_global_attributes(nr_ice_ctx *ctx,char ***attrsp, int *attrctp)
+ {
+ *attrctp=0;
+ *attrsp=0;
+ return(0);
+ }
+
+static int nr_ice_random_string(char *str, int len)
+ {
+ unsigned char bytes[100];
+ size_t needed;
+ int r,_status;
+
+ if(len%2) ABORT(R_BAD_ARGS);
+ needed=len/2;
+
+ if(needed>sizeof(bytes)) ABORT(R_BAD_ARGS);
+
+ if(r=nr_crypto_random_bytes(bytes,needed))
+ ABORT(r);
+
+ if(r=nr_bin2hex(bytes,needed,(unsigned char *)str))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+/* This is incredibly annoying: we now have a datagram but we don't
+ know which peer it's from, and we need to be able to tell the
+ API user. So, offer it to each peer and if one bites, assume
+ the others don't want it
+*/
+int nr_ice_ctx_deliver_packet(nr_ice_ctx *ctx, nr_ice_component *comp, nr_transport_addr *source_addr, UCHAR *data, int len)
+ {
+ nr_ice_peer_ctx *pctx;
+ int r;
+
+ pctx=STAILQ_FIRST(&ctx->peers);
+ while(pctx){
+ r=nr_ice_peer_ctx_deliver_packet_maybe(pctx, comp, source_addr, data, len);
+ if(!r)
+ break;
+
+ pctx=STAILQ_NEXT(pctx,entry);
+ }
+
+ if(!pctx)
+ r_log(LOG_ICE,LOG_WARNING,"ICE(%s): Packet received from %s which doesn't match any known peer",ctx->label,source_addr->as_string);
+
+ return(0);
+ }
+
+int nr_ice_ctx_is_known_id(nr_ice_ctx *ctx, UCHAR id[12])
+ {
+ nr_ice_stun_id *xid;
+
+ xid=STAILQ_FIRST(&ctx->ids);
+ while(xid){
+ if (!memcmp(xid->id, id, 12))
+ return 1;
+
+ xid=STAILQ_NEXT(xid,entry);
+ }
+
+ return 0;
+ }
+
+int nr_ice_ctx_remember_id(nr_ice_ctx *ctx, nr_stun_message *msg)
+{
+ int _status;
+ nr_ice_stun_id *xid;
+
+ xid = RCALLOC(sizeof(*xid));
+ if (!xid)
+ ABORT(R_NO_MEMORY);
+
+ assert(sizeof(xid->id) == sizeof(msg->header.id));
+#if __STDC_VERSION__ >= 201112L
+ _Static_assert(sizeof(xid->id) == sizeof(msg->header.id),"Message ID Size Mismatch");
+#endif
+ memcpy(xid->id, &msg->header.id, sizeof(xid->id));
+
+ STAILQ_INSERT_TAIL(&ctx->ids,xid,entry);
+
+ _status=0;
+ abort:
+ return(_status);
+}
+
+
+/* Clean up some of the resources (mostly file descriptors) used
+ by candidates we didn't choose. Note that this still leaves
+ a fair amount of non-system stuff floating around. This gets
+ cleaned up when you destroy the ICE ctx */
+int nr_ice_ctx_finalize(nr_ice_ctx *ctx, nr_ice_peer_ctx *pctx)
+ {
+ nr_ice_media_stream *lstr,*rstr;
+
+ r_log(LOG_ICE,LOG_DEBUG,"Finalizing ICE ctx %s, peer=%s",ctx->label,pctx->label);
+ /*
+ First find the peer stream, if any
+ */
+ lstr=STAILQ_FIRST(&ctx->streams);
+ while(lstr){
+ rstr=STAILQ_FIRST(&pctx->peer_streams);
+
+ while(rstr){
+ if(rstr->local_stream==lstr)
+ break;
+
+ rstr=STAILQ_NEXT(rstr,entry);
+ }
+
+ nr_ice_media_stream_finalize(lstr,rstr);
+
+ lstr=STAILQ_NEXT(lstr,entry);
+ }
+
+ return(0);
+ }
+
+
+int nr_ice_ctx_set_trickle_cb(nr_ice_ctx *ctx, nr_ice_trickle_candidate_cb cb, void *cb_arg)
+{
+ ctx->trickle_cb = cb;
+ ctx->trickle_cb_arg = cb_arg;
+
+ return 0;
+}
+
+int nr_ice_ctx_hide_candidate(nr_ice_ctx *ctx, nr_ice_candidate *cand)
+ {
+ if (cand->state != NR_ICE_CAND_STATE_INITIALIZED) {
+ return 1;
+ }
+
+ if (ctx->flags & NR_ICE_CTX_FLAGS_DISABLE_HOST_CANDIDATES) {
+ if (cand->type == HOST)
+ return 1;
+ }
+
+ if (cand->stream->obsolete) {
+ return 1;
+ }
+
+ return 0;
+ }
+
+int nr_ice_get_new_ice_ufrag(char** ufrag)
+ {
+ int r,_status;
+ char buf[ICE_UFRAG_LEN+1];
+
+ if(r=nr_ice_random_string(buf,ICE_UFRAG_LEN))
+ ABORT(r);
+ if(!(*ufrag=r_strdup(buf)))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ if(_status) {
+ RFREE(*ufrag);
+ *ufrag = 0;
+ }
+ return(_status);
+ }
+
+int nr_ice_get_new_ice_pwd(char** pwd)
+ {
+ int r,_status;
+ char buf[ICE_PWD_LEN+1];
+
+ if(r=nr_ice_random_string(buf,ICE_PWD_LEN))
+ ABORT(r);
+ if(!(*pwd=r_strdup(buf)))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ if(_status) {
+ RFREE(*pwd);
+ *pwd = 0;
+ }
+ return(_status);
+ }
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_ctx.h b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_ctx.h
new file mode 100644
index 0000000000..8b3081f567
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_ctx.h
@@ -0,0 +1,188 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+
+#ifndef _ice_ctx_h
+#define _ice_ctx_h
+#ifdef __cplusplus
+using namespace std;
+extern "C" {
+#endif /* __cplusplus */
+
+/* Not good practice but making includes simpler */
+#include "transport_addr.h"
+#include "nr_socket.h"
+#include "nr_resolver.h"
+#include "nr_interface_prioritizer.h"
+#include "nr_socket_wrapper.h"
+#include "stun_client_ctx.h"
+#include "stun_server_ctx.h"
+#include "turn_client_ctx.h"
+
+typedef struct nr_ice_foundation_ {
+ int index;
+
+ nr_transport_addr addr;
+ int type;
+ /* ICE spec says that we only compare IP address, not port */
+ nr_transport_addr stun_server_addr;
+
+ STAILQ_ENTRY(nr_ice_foundation_) entry;
+} nr_ice_foundation;
+
+typedef STAILQ_HEAD(nr_ice_foundation_head_,nr_ice_foundation_) nr_ice_foundation_head;
+
+typedef TAILQ_HEAD(nr_ice_candidate_head_,nr_ice_candidate_) nr_ice_candidate_head;
+typedef TAILQ_HEAD(nr_ice_cand_pair_head_,nr_ice_cand_pair_) nr_ice_cand_pair_head;
+typedef struct nr_ice_component_ nr_ice_component;
+typedef struct nr_ice_media_stream_ nr_ice_media_stream;
+typedef struct nr_ice_ctx_ nr_ice_ctx;
+typedef struct nr_ice_peer_ctx_ nr_ice_peer_ctx;
+typedef struct nr_ice_candidate_ nr_ice_candidate;
+typedef struct nr_ice_cand_pair_ nr_ice_cand_pair;
+typedef void (*nr_ice_trickle_candidate_cb) (void *cb_arg,
+ nr_ice_ctx *ctx, nr_ice_media_stream *stream, int component_id,
+ nr_ice_candidate *candidate);
+
+#include "ice_socket.h"
+#include "ice_component.h"
+#include "ice_media_stream.h"
+#include "ice_candidate.h"
+#include "ice_candidate_pair.h"
+#include "ice_handler.h"
+#include "ice_peer_ctx.h"
+
+typedef struct nr_ice_stun_id_ {
+ UCHAR id[12];
+
+ STAILQ_ENTRY(nr_ice_stun_id_) entry;
+} nr_ice_stun_id;
+
+typedef STAILQ_HEAD(nr_ice_stun_id_head_,nr_ice_stun_id_) nr_ice_stun_id_head;
+
+typedef struct nr_ice_stats_ {
+ UINT2 stun_retransmits;
+ UINT2 turn_401s;
+ UINT2 turn_403s;
+ UINT2 turn_438s;
+} nr_ice_stats;
+
+struct nr_ice_ctx_ {
+ UINT4 flags;
+ char *label;
+
+ UINT4 Ta;
+
+ nr_ice_stun_server *stun_servers_cfg; /* The list of stun servers */
+ int stun_server_ct_cfg;
+ nr_ice_turn_server *turn_servers_cfg; /* The list of turn servers */
+ int turn_server_ct_cfg;
+ nr_local_addr *local_addrs; /* The list of available local addresses and corresponding interface information */
+ int local_addr_ct;
+
+ nr_resolver *resolver; /* The resolver to use */
+ nr_interface_prioritizer *interface_prioritizer; /* Priority decision logic */
+ nr_socket_factory *socket_factory;
+
+ nr_ice_foundation_head foundations;
+
+ nr_ice_media_stream_head streams; /* Media streams */
+ int stream_ct;
+ nr_ice_socket_head sockets; /* The sockets we're using */
+ int uninitialized_candidates;
+
+ UINT4 gather_rto;
+ UINT4 stun_delay;
+
+ UINT4 test_timer_divider;
+
+ nr_ice_peer_ctx_head peers;
+ nr_ice_stun_id_head ids;
+
+ NR_async_cb done_cb;
+ void *cb_arg;
+
+ nr_ice_trickle_candidate_cb trickle_cb;
+ void *trickle_cb_arg;
+
+ char force_net_interface[MAXIFNAME];
+ nr_ice_stats stats;
+
+ nr_transport_addr *target_for_default_local_address_lookup;
+};
+
+int nr_ice_ctx_create(char *label, UINT4 flags, nr_ice_ctx **ctxp);
+int nr_ice_ctx_create_with_credentials(char *label, UINT4 flags, char* ufrag, char* pwd, nr_ice_ctx **ctxp);
+#define NR_ICE_CTX_FLAGS_AGGRESSIVE_NOMINATION (1)
+#define NR_ICE_CTX_FLAGS_LITE (1<<1)
+#define NR_ICE_CTX_FLAGS_RELAY_ONLY (1<<2)
+#define NR_ICE_CTX_FLAGS_DISABLE_HOST_CANDIDATES (1<<3)
+#define NR_ICE_CTX_FLAGS_ONLY_DEFAULT_ADDRS (1<<4)
+#define NR_ICE_CTX_FLAGS_ONLY_PROXY (1<<5)
+#define NR_ICE_CTX_FLAGS_OBFUSCATE_HOST_ADDRESSES (1<<6)
+
+void nr_ice_ctx_add_flags(nr_ice_ctx *ctx, UINT4 flags);
+void nr_ice_ctx_remove_flags(nr_ice_ctx *ctx, UINT4 flags);
+void nr_ice_ctx_destroy(nr_ice_ctx** ctxp);
+int nr_ice_set_local_addresses(nr_ice_ctx *ctx, nr_local_addr* stun_addrs, int stun_addr_ct);
+int nr_ice_set_target_for_default_local_address_lookup(nr_ice_ctx *ctx, const char *target_ip, UINT2 target_port);
+int nr_ice_gather(nr_ice_ctx *ctx, NR_async_cb done_cb, void *cb_arg);
+int nr_ice_add_candidate(nr_ice_ctx *ctx, nr_ice_candidate *cand);
+void nr_ice_gather_finished_cb(NR_SOCKET s, int h, void *cb_arg);
+int nr_ice_add_media_stream(nr_ice_ctx *ctx,const char *label,const char *ufrag,const char *pwd,int components, nr_ice_media_stream **streamp);
+int nr_ice_remove_media_stream(nr_ice_ctx *ctx,nr_ice_media_stream **streamp);
+int nr_ice_get_global_attributes(nr_ice_ctx *ctx,char ***attrsp, int *attrctp);
+int nr_ice_ctx_deliver_packet(nr_ice_ctx *ctx, nr_ice_component *comp, nr_transport_addr *source_addr, UCHAR *data, int len);
+int nr_ice_ctx_is_known_id(nr_ice_ctx *ctx, UCHAR id[12]);
+int nr_ice_ctx_remember_id(nr_ice_ctx *ctx, nr_stun_message *msg);
+int nr_ice_ctx_finalize(nr_ice_ctx *ctx, nr_ice_peer_ctx *pctx);
+int nr_ice_ctx_set_stun_servers(nr_ice_ctx *ctx,nr_ice_stun_server *servers, int ct);
+int nr_ice_ctx_set_turn_servers(nr_ice_ctx *ctx,nr_ice_turn_server *servers, int ct);
+int nr_ice_ctx_copy_turn_servers(nr_ice_ctx *ctx, nr_ice_turn_server *servers, int ct);
+int nr_ice_ctx_set_resolver(nr_ice_ctx *ctx, nr_resolver *resolver);
+int nr_ice_ctx_set_interface_prioritizer(nr_ice_ctx *ctx, nr_interface_prioritizer *prioritizer);
+void nr_ice_ctx_set_socket_factory(nr_ice_ctx *ctx, nr_socket_factory *factory);
+int nr_ice_ctx_set_trickle_cb(nr_ice_ctx *ctx, nr_ice_trickle_candidate_cb cb, void *cb_arg);
+int nr_ice_ctx_hide_candidate(nr_ice_ctx *ctx, nr_ice_candidate *cand);
+int nr_ice_get_new_ice_ufrag(char** ufrag);
+int nr_ice_get_new_ice_pwd(char** pwd);
+
+#define NR_ICE_MAX_ATTRIBUTE_SIZE 256
+
+extern int LOG_ICE;
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_handler.h b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_handler.h
new file mode 100644
index 0000000000..5a0690adad
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_handler.h
@@ -0,0 +1,84 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+
+#ifndef _ice_h
+#define _ice_h
+#ifdef __cplusplus
+using namespace std;
+extern "C" {
+#endif /* __cplusplus */
+
+typedef struct nr_ice_handler_vtbl_ {
+ /* The checks on this media stream are done. The handler needs to
+ select a single pair to proceed with (regular nomination).
+ Once this returns the check starts and the pair can be
+ written on. Use nr_ice_candidate_pair_select() to perform the
+ selection.
+ TODO: !ekr! is this right?
+ */
+ int (*select_pair)(void *obj,nr_ice_media_stream *stream,
+int component_id, nr_ice_cand_pair **potentials,int potential_ct);
+
+ /* This media stream is ready to read/write (aggressive nomination).
+ May be called again if the nominated pair changes due to
+ ICE instability. TODO: !ekr! think about this
+ */
+ int (*stream_ready)(void *obj, nr_ice_media_stream *stream);
+
+ /* This media stream has failed */
+ int (*stream_failed)(void *obj, nr_ice_media_stream *stream);
+
+ /* ICE is connected for this peer ctx */
+ int (*ice_connected)(void *obj, nr_ice_peer_ctx *pctx);
+
+ /* A message was delivered to us */
+ int (*msg_recvd)(void *obj, nr_ice_peer_ctx *pctx, nr_ice_media_stream *stream, int component_id, UCHAR *msg, int len);
+
+ /* ICE has started checking. */
+ int (*ice_checking)(void *obj, nr_ice_peer_ctx *pctx);
+
+ /* ICE detected a (temporary?) disconnect. */
+ int (*ice_disconnected)(void *obj, nr_ice_peer_ctx *pctx);
+} nr_ice_handler_vtbl;
+
+typedef struct nr_ice_handler_ {
+ void *obj;
+ nr_ice_handler_vtbl *vtbl;
+} nr_ice_handler;
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_media_stream.c b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_media_stream.c
new file mode 100644
index 0000000000..62bfbad629
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_media_stream.c
@@ -0,0 +1,1087 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <string.h>
+#include <assert.h>
+#include <nr_api.h>
+#include <r_assoc.h>
+#include <async_timer.h>
+#include "ice_util.h"
+#include "ice_ctx.h"
+
+static char *nr_ice_media_stream_states[]={"INVALID",
+ "UNPAIRED","FROZEN","ACTIVE","CONNECTED","FAILED"
+};
+
+int nr_ice_media_stream_set_state(nr_ice_media_stream *str, int state);
+
+int nr_ice_media_stream_create(nr_ice_ctx *ctx,const char *label,const char *ufrag,const char *pwd,int components, nr_ice_media_stream **streamp)
+ {
+ int r,_status;
+ nr_ice_media_stream *stream=0;
+ nr_ice_component *comp=0;
+ int i;
+
+ if(!(stream=RCALLOC(sizeof(nr_ice_media_stream))))
+ ABORT(R_NO_MEMORY);
+
+ if(!(stream->label=r_strdup(label)))
+ ABORT(R_NO_MEMORY);
+
+ if(!(stream->ufrag=r_strdup(ufrag)))
+ ABORT(R_NO_MEMORY);
+
+ if(!(stream->pwd=r_strdup(pwd)))
+ ABORT(R_NO_MEMORY);
+
+ stream->ctx=ctx;
+
+ STAILQ_INIT(&stream->components);
+ for(i=0;i<components;i++){
+ /* component-id must be > 0, so increment by 1 */
+ if(r=nr_ice_component_create(stream, i+1, &comp))
+ ABORT(r);
+
+ }
+
+ TAILQ_INIT(&stream->check_list);
+ TAILQ_INIT(&stream->trigger_check_queue);
+
+ stream->disconnected = 0;
+ stream->component_ct=components;
+ stream->ice_state = NR_ICE_MEDIA_STREAM_UNPAIRED;
+ stream->obsolete = 0;
+ stream->r2l_user = 0;
+ stream->l2r_user = 0;
+ stream->flags = ctx->flags;
+ if(ctx->stun_server_ct_cfg) {
+ if(!(stream->stun_servers=RCALLOC(sizeof(nr_ice_stun_server)*(ctx->stun_server_ct_cfg))))
+ ABORT(R_NO_MEMORY);
+
+ memcpy(stream->stun_servers,ctx->stun_servers_cfg,sizeof(nr_ice_stun_server)*(ctx->stun_server_ct_cfg));
+ stream->stun_server_ct = ctx->stun_server_ct_cfg;
+ }
+
+ if(ctx->turn_server_ct_cfg) {
+ if(!(stream->turn_servers=RCALLOC(sizeof(nr_ice_turn_server)*(ctx->turn_server_ct_cfg))))
+ ABORT(R_NO_MEMORY);
+
+ for(int i = 0; i < ctx->turn_server_ct_cfg; ++i) {
+ nr_ice_turn_server *dst = &stream->turn_servers[i];
+ nr_ice_turn_server *src = &ctx->turn_servers_cfg[i];
+ memcpy(&dst->turn_server, &src->turn_server, sizeof(nr_ice_stun_server));
+ dst->username = r_strdup(src->username);
+ r_data_create(&dst->password, src->password->data, src->password->len);
+ }
+ stream->turn_server_ct = ctx->turn_server_ct_cfg;
+ }
+
+ r_log(LOG_ICE,LOG_DEBUG,"ICE-STREAM(%s): flags %d",stream->label,stream->flags);
+ *streamp=stream;
+
+ _status=0;
+ abort:
+ if(_status){
+ nr_ice_media_stream_destroy(&stream);
+ }
+ return(_status);
+ }
+
+int nr_ice_media_stream_destroy(nr_ice_media_stream **streamp)
+ {
+ nr_ice_media_stream *stream;
+ nr_ice_component *c1,*c2;
+ nr_ice_cand_pair *p1,*p2;
+ if(!streamp || !*streamp)
+ return(0);
+
+ stream=*streamp;
+ *streamp=0;
+
+ STAILQ_FOREACH_SAFE(c1, &stream->components, entry, c2){
+ STAILQ_REMOVE(&stream->components,c1,nr_ice_component_,entry);
+ nr_ice_component_destroy(&c1);
+ }
+
+ /* Note: all the entries from the trigger check queue are held in here as
+ * well, so we only clean up the super set. */
+ TAILQ_FOREACH_SAFE(p1, &stream->check_list, check_queue_entry, p2){
+ TAILQ_REMOVE(&stream->check_list,p1,check_queue_entry);
+ nr_ice_candidate_pair_destroy(&p1);
+ }
+
+ RFREE(stream->label);
+
+ RFREE(stream->ufrag);
+ RFREE(stream->pwd);
+ RFREE(stream->r2l_user);
+ RFREE(stream->l2r_user);
+ r_data_zfree(&stream->r2l_pass);
+ r_data_zfree(&stream->l2r_pass);
+
+ RFREE(stream->stun_servers);
+ for (int i = 0; i < stream->turn_server_ct; i++) {
+ RFREE(stream->turn_servers[i].username);
+ r_data_destroy(&stream->turn_servers[i].password);
+ }
+ RFREE(stream->turn_servers);
+
+ if(stream->timer)
+ NR_async_timer_cancel(stream->timer);
+
+ RFREE(stream);
+
+ return(0);
+ }
+
+int nr_ice_media_stream_initialize(nr_ice_ctx *ctx, nr_ice_media_stream *stream)
+ {
+ int r,_status;
+ nr_ice_component *comp;
+
+ assert(!stream->obsolete);
+
+ comp=STAILQ_FIRST(&stream->components);
+ while(comp){
+ if(r=nr_ice_component_initialize(ctx,comp))
+ ABORT(r);
+ comp=STAILQ_NEXT(comp,entry);
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+
+int nr_ice_media_stream_get_attributes(nr_ice_media_stream *stream, char ***attrsp, int *attrctp)
+ {
+ int attrct=2;
+ nr_ice_component *comp;
+ char **attrs=0;
+ int index=0;
+ nr_ice_candidate *cand;
+ int r,_status;
+ char *tmp=0;
+
+ *attrctp=0;
+
+ /* First find out how many attributes we need */
+ comp=STAILQ_FIRST(&stream->components);
+ while(comp){
+ if (comp->state != NR_ICE_COMPONENT_DISABLED) {
+ cand = TAILQ_FIRST(&comp->candidates);
+ while(cand){
+ if (!nr_ice_ctx_hide_candidate(stream->ctx, cand)) {
+ ++attrct;
+ }
+
+ cand = TAILQ_NEXT(cand, entry_comp);
+ }
+ }
+ comp=STAILQ_NEXT(comp,entry);
+ }
+
+ /* Make the array we'll need */
+ if(!(attrs=RCALLOC(sizeof(char *)*attrct)))
+ ABORT(R_NO_MEMORY);
+ for(index=0;index<attrct;index++){
+ if(!(attrs[index]=RMALLOC(NR_ICE_MAX_ATTRIBUTE_SIZE)))
+ ABORT(R_NO_MEMORY);
+ }
+
+ index=0;
+ /* Now format the attributes */
+ comp=STAILQ_FIRST(&stream->components);
+ while(comp){
+ if (comp->state != NR_ICE_COMPONENT_DISABLED) {
+ nr_ice_candidate *cand;
+
+ cand=TAILQ_FIRST(&comp->candidates);
+ while(cand){
+ if (!nr_ice_ctx_hide_candidate(stream->ctx, cand)) {
+ assert(index < attrct);
+
+ if (index >= attrct)
+ ABORT(R_INTERNAL);
+
+ if(r=nr_ice_format_candidate_attribute(cand, attrs[index],NR_ICE_MAX_ATTRIBUTE_SIZE, 0))
+ ABORT(r);
+
+ index++;
+ }
+
+ cand=TAILQ_NEXT(cand,entry_comp);
+ }
+ }
+ comp=STAILQ_NEXT(comp,entry);
+ }
+
+ /* Now, ufrag and pwd */
+ if(!(tmp=RMALLOC(100)))
+ ABORT(R_NO_MEMORY);
+ snprintf(tmp,100,"ice-ufrag:%s",stream->ufrag);
+ attrs[index++]=tmp;
+
+ if(!(tmp=RMALLOC(100)))
+ ABORT(R_NO_MEMORY);
+ snprintf(tmp,100,"ice-pwd:%s",stream->pwd);
+ attrs[index++]=tmp;
+
+ *attrsp=attrs;
+ *attrctp=attrct;
+
+ _status=0;
+ abort:
+ if(_status){
+ if(attrs){
+ for(index=0;index<attrct;index++){
+ RFREE(attrs[index]);
+ }
+ RFREE(attrs);
+ }
+ }
+ return(_status);
+ }
+
+/* Get a default candidate per 4.1.4 */
+int nr_ice_media_stream_get_default_candidate(nr_ice_media_stream *stream, int component, nr_ice_candidate **candp)
+ {
+ int r,_status;
+ nr_ice_component *comp;
+
+ comp=STAILQ_FIRST(&stream->components);
+ while(comp){
+ if (comp->component_id == component)
+ break;
+
+ comp=STAILQ_NEXT(comp,entry);
+ }
+
+ if (!comp)
+ ABORT(R_NOT_FOUND);
+
+ /* If there aren't any IPV4 candidates, try IPV6 */
+ if((r=nr_ice_component_get_default_candidate(comp, candp, NR_IPV4)) &&
+ (r=nr_ice_component_get_default_candidate(comp, candp, NR_IPV6))) {
+ ABORT(r);
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+
+int nr_ice_media_stream_pair_candidates(nr_ice_peer_ctx *pctx,nr_ice_media_stream *lstream,nr_ice_media_stream *pstream)
+ {
+ int r,_status;
+ nr_ice_component *pcomp,*lcomp;
+
+ pcomp=STAILQ_FIRST(&pstream->components);
+ lcomp=STAILQ_FIRST(&lstream->components);
+ while(pcomp){
+ if ((lcomp->state != NR_ICE_COMPONENT_DISABLED) &&
+ (pcomp->state != NR_ICE_COMPONENT_DISABLED)) {
+ if(r=nr_ice_component_pair_candidates(pctx,lcomp,pcomp))
+ ABORT(r);
+ }
+
+ lcomp=STAILQ_NEXT(lcomp,entry);
+ pcomp=STAILQ_NEXT(pcomp,entry);
+ };
+
+ if (pstream->ice_state == NR_ICE_MEDIA_STREAM_UNPAIRED) {
+ nr_ice_media_stream_set_state(pstream, NR_ICE_MEDIA_STREAM_CHECKS_FROZEN);
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_ice_media_stream_service_pre_answer_requests(nr_ice_peer_ctx *pctx, nr_ice_media_stream *lstream, nr_ice_media_stream *pstream, int *serviced)
+ {
+ nr_ice_component *pcomp;
+ int r,_status;
+ char *user = 0;
+
+ if (serviced)
+ *serviced = 0;
+
+ pcomp=STAILQ_FIRST(&pstream->components);
+ while(pcomp){
+ int serviced_inner=0;
+
+ /* Flush all the pre-answer requests */
+ if(r=nr_ice_component_service_pre_answer_requests(pctx, pcomp, pstream->r2l_user, &serviced_inner))
+ ABORT(r);
+ if (serviced)
+ *serviced += serviced_inner;
+
+ pcomp=STAILQ_NEXT(pcomp,entry);
+ }
+
+ _status=0;
+ abort:
+ RFREE(user);
+ return(_status);
+ }
+
+/* S 5.8 -- run the first pair from the triggered check queue (even after
+ * checks have completed S 8.1.2) or run the highest priority WAITING pair or
+ * if not available FROZEN pair from the check queue */
+static void nr_ice_media_stream_check_timer_cb(NR_SOCKET s, int h, void *cb_arg)
+ {
+ int r,_status;
+ nr_ice_media_stream *stream=cb_arg;
+ nr_ice_cand_pair *pair = 0;
+ int timer_multiplier=stream->pctx->active_streams ? stream->pctx->active_streams : 1;
+ int timer_val=stream->pctx->ctx->Ta*timer_multiplier;
+
+ assert(timer_val>0);
+
+ r_log(LOG_ICE,LOG_DEBUG,"ICE-PEER(%s): check timer expired for media stream %s",stream->pctx->label,stream->label);
+ stream->timer=0;
+
+ /* The trigger check queue has the highest priority */
+ pair=TAILQ_FIRST(&stream->trigger_check_queue);
+ while(pair){
+ if(pair->state==NR_ICE_PAIR_STATE_WAITING){
+ /* Remove the pair from he trigger check queue */
+ r_log(LOG_ICE,LOG_DEBUG,"ICE-PEER(%s): Removing pair from trigger check queue %s",stream->pctx->label,pair->as_string);
+ TAILQ_REMOVE(&stream->trigger_check_queue,pair,triggered_check_queue_entry);
+ break;
+ }
+ pair=TAILQ_NEXT(pair,triggered_check_queue_entry);
+ }
+
+ if (stream->ice_state != NR_ICE_MEDIA_STREAM_CHECKS_CONNECTED) {
+ if(!pair){
+ /* Find the highest priority WAITING check and move it to RUNNING */
+ pair=TAILQ_FIRST(&stream->check_list);
+ while(pair){
+ if(pair->state==NR_ICE_PAIR_STATE_WAITING)
+ break;
+ pair=TAILQ_NEXT(pair,check_queue_entry);
+ }
+ }
+
+ /* Hmmm... No WAITING. Let's look for FROZEN */
+ if(!pair){
+ pair=TAILQ_FIRST(&stream->check_list);
+
+ while(pair){
+ if(pair->state==NR_ICE_PAIR_STATE_FROZEN){
+ if(r=nr_ice_candidate_pair_unfreeze(stream->pctx,pair))
+ ABORT(r);
+ break;
+ }
+ pair=TAILQ_NEXT(pair,check_queue_entry);
+ }
+ }
+ }
+
+ if(pair){
+ nr_ice_candidate_pair_start(pair->pctx,pair); /* Ignore failures */
+ NR_ASYNC_TIMER_SET(timer_val,nr_ice_media_stream_check_timer_cb,cb_arg,&stream->timer);
+ }
+ else {
+ r_log(LOG_ICE,LOG_INFO,"ICE-PEER(%s): no FROZEN/WAITING pairs for %s",stream->pctx->label,stream->label);
+ }
+
+ _status=0;
+ abort:
+ if (_status) {
+ // cb doesn't return anything, but we should probably log that we aborted
+ // This also quiets the unused variable warnings.
+ r_log(LOG_ICE,LOG_DEBUG,"ICE-PEER(%s): check timer cb for media stream %s abort with status: %d",
+ stream->pctx->label,stream->label, _status);
+ }
+ return;
+ }
+
+/* Start checks for this media stream (aka check list) */
+int nr_ice_media_stream_start_checks(nr_ice_peer_ctx *pctx, nr_ice_media_stream *stream)
+ {
+ int r,_status;
+
+ /* Don't start the check timer if the stream is failed */
+ if (stream->ice_state == NR_ICE_MEDIA_STREAM_CHECKS_FAILED) {
+ assert(0);
+ ABORT(R_INTERNAL);
+ }
+
+ if (stream->local_stream->obsolete) {
+ assert(0);
+ ABORT(R_INTERNAL);
+ }
+
+ /* Even if the stream is completed already remote can still create a new
+ * triggered check request which needs to fire, but not change our stream
+ * state. */
+ if (stream->ice_state != NR_ICE_MEDIA_STREAM_CHECKS_CONNECTED) {
+ if(r=nr_ice_media_stream_set_state(stream,NR_ICE_MEDIA_STREAM_CHECKS_ACTIVE)) {
+ ABORT(r);
+ }
+ }
+
+ if (!stream->timer) {
+ r_log(LOG_ICE,LOG_INFO,"ICE-PEER(%s)/ICE-STREAM(%s): Starting check timer for stream.",pctx->label,stream->label);
+ nr_ice_media_stream_check_timer_cb(0,0,stream);
+ }
+
+ nr_ice_peer_ctx_stream_started_checks(pctx, stream);
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+/* Start checks for this media stream (aka check list) S 5.7 */
+int nr_ice_media_stream_unfreeze_pairs(nr_ice_peer_ctx *pctx, nr_ice_media_stream *stream)
+ {
+ int r,_status;
+ r_assoc *assoc=0;
+ nr_ice_cand_pair *pair=0;
+
+ /* Already seen assoc */
+ if(r=r_assoc_create(&assoc,r_assoc_crc32_hash_compute,5))
+ ABORT(r);
+
+ /* S 5.7.4. Set the highest priority pairs in each foundation to WAITING */
+ pair=TAILQ_FIRST(&stream->check_list);
+ while(pair){
+ void *v;
+
+ if(r=r_assoc_fetch(assoc,pair->foundation,strlen(pair->foundation),&v)){
+ if(r!=R_NOT_FOUND)
+ ABORT(r);
+ if(r=nr_ice_candidate_pair_unfreeze(pctx,pair))
+ ABORT(r);
+
+ if(r=r_assoc_insert(assoc,pair->foundation,strlen(pair->foundation),
+ 0,0,0,R_ASSOC_NEW))
+ ABORT(r);
+ }
+
+ /* Already exists... fall through */
+ pair=TAILQ_NEXT(pair,check_queue_entry);
+ }
+
+ _status=0;
+ abort:
+ r_assoc_destroy(&assoc);
+ return(_status);
+ }
+
+static int nr_ice_media_stream_unfreeze_pairs_match(nr_ice_media_stream *stream, char *foundation)
+ {
+ nr_ice_cand_pair *pair;
+ int r,_status;
+ int unfroze=0;
+
+ pair=TAILQ_FIRST(&stream->check_list);
+ while(pair){
+ if(pair->state==NR_ICE_PAIR_STATE_FROZEN &&
+ !strcmp(foundation,pair->foundation)){
+ if(r=nr_ice_candidate_pair_unfreeze(stream->pctx,pair))
+ ABORT(r);
+ unfroze++;
+ }
+ pair=TAILQ_NEXT(pair,check_queue_entry);
+ }
+
+ if(!unfroze)
+ return(R_NOT_FOUND);
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+/* S 7.1.2.2 */
+int nr_ice_media_stream_unfreeze_pairs_foundation(nr_ice_media_stream *stream, char *foundation)
+ {
+ int r,_status;
+ nr_ice_media_stream *str;
+
+ /* 1. Unfreeze all frozen pairs with the same foundation
+ in this stream */
+ if(r=nr_ice_media_stream_unfreeze_pairs_match(stream,foundation)){
+ if(r!=R_NOT_FOUND)
+ ABORT(r);
+ }
+
+ /* Now go through the check lists for the other streams */
+ str=STAILQ_FIRST(&stream->pctx->peer_streams);
+ while(str){
+ if(str!=stream && !str->local_stream->obsolete){
+ switch(str->ice_state){
+ case NR_ICE_MEDIA_STREAM_CHECKS_ACTIVE:
+ /* Unfreeze matching pairs */
+ if(r=nr_ice_media_stream_unfreeze_pairs_match(str,foundation)){
+ if(r!=R_NOT_FOUND)
+ ABORT(r);
+ }
+ break;
+ case NR_ICE_MEDIA_STREAM_CHECKS_FROZEN:
+ /* Unfreeze matching pairs if any */
+ r=nr_ice_media_stream_unfreeze_pairs_match(str,foundation);
+ if(r){
+ if(r!=R_NOT_FOUND)
+ ABORT(r);
+
+ /* OK, no matching pairs: execute the algorithm from 5.7
+ for this stream */
+ if(r=nr_ice_media_stream_unfreeze_pairs(str->pctx,str))
+ ABORT(r);
+ }
+ if(r=nr_ice_media_stream_start_checks(str->pctx,str))
+ ABORT(r);
+
+ break;
+ default:
+ break;
+ }
+ }
+
+ str=STAILQ_NEXT(str,entry);
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+
+void nr_ice_media_stream_dump_state(nr_ice_peer_ctx *pctx, nr_ice_media_stream *stream, int log_level)
+ {
+ nr_ice_cand_pair *pair;
+ nr_ice_component *comp;
+
+ if(stream->local_stream){
+ /* stream has a corresponding local_stream */
+ nr_ice_media_stream_dump_state(stream->local_stream->pctx,stream->local_stream, log_level);
+ r_log(LOG_ICE,log_level,"ICE-PEER(%s)/STREAM(%s): state dump", stream->pctx->label, stream->label);
+ } else {
+ r_log(LOG_ICE,log_level,"ICE(%s)/STREAM(%s): state dump", stream->ctx->label, stream->label);
+ }
+
+ pair=TAILQ_FIRST(&stream->check_list);
+ while(pair){
+ nr_ice_candidate_pair_dump_state(pair, log_level);
+ pair=TAILQ_NEXT(pair,check_queue_entry);
+ }
+
+ comp=STAILQ_FIRST(&stream->components);
+ while(comp){
+ nr_ice_component_dump_state(comp, log_level);
+ comp=STAILQ_NEXT(comp,entry);
+ }
+ }
+
+int nr_ice_media_stream_set_state(nr_ice_media_stream *str, int state)
+ {
+ /* Make no-change a no-op */
+ if (state == str->ice_state)
+ return 0;
+
+ assert((size_t)state < sizeof(nr_ice_media_stream_states)/sizeof(char *));
+ assert((size_t)str->ice_state < sizeof(nr_ice_media_stream_states)/sizeof(char *));
+
+ r_log(LOG_ICE,LOG_DEBUG,"ICE-PEER(%s): stream %s state %s->%s",
+ str->pctx->label,str->label,
+ nr_ice_media_stream_states[str->ice_state],
+ nr_ice_media_stream_states[state]);
+
+ if(state == NR_ICE_MEDIA_STREAM_CHECKS_ACTIVE)
+ str->pctx->active_streams++;
+ if(str->ice_state == NR_ICE_MEDIA_STREAM_CHECKS_ACTIVE)
+ str->pctx->active_streams--;
+
+ r_log(LOG_ICE,LOG_DEBUG,"ICE-PEER(%s): %d active streams",
+ str->pctx->label, str->pctx->active_streams);
+
+ str->ice_state=state;
+ if (state == NR_ICE_MEDIA_STREAM_CHECKS_FAILED) {
+ nr_ice_media_stream_dump_state(str->pctx,str,LOG_ERR);
+ }
+
+ return(0);
+ }
+
+void nr_ice_media_stream_stop_checking(nr_ice_media_stream *str)
+ {
+ nr_ice_cand_pair *p;
+ nr_ice_component *comp;
+
+ /* Cancel candidate pairs */
+ p=TAILQ_FIRST(&str->check_list);
+ while(p){
+ nr_ice_candidate_pair_cancel(p->pctx,p,0);
+ p=TAILQ_NEXT(p,check_queue_entry);
+ }
+
+ if(str->timer) {
+ NR_async_timer_cancel(str->timer);
+ str->timer = 0;
+ }
+
+ /* Cancel consent timers in case it is running already */
+ comp=STAILQ_FIRST(&str->components);
+ while(comp){
+ nr_ice_component_consent_destroy(comp);
+ comp=STAILQ_NEXT(comp,entry);
+ }
+ }
+
+void nr_ice_media_stream_set_obsolete(nr_ice_media_stream *str)
+ {
+ nr_ice_component *c1,*c2;
+ str->obsolete = 1;
+
+ STAILQ_FOREACH_SAFE(c1, &str->components, entry, c2){
+ nr_ice_component_stop_gathering(c1);
+ }
+
+ nr_ice_media_stream_stop_checking(str);
+ }
+
+int nr_ice_media_stream_is_done_gathering(nr_ice_media_stream *str)
+ {
+ nr_ice_component *comp;
+ comp=STAILQ_FIRST(&str->components);
+ while(comp){
+ if(!nr_ice_component_is_done_gathering(comp)) {
+ return 0;
+ }
+ comp=STAILQ_NEXT(comp,entry);
+ }
+ return 1;
+ }
+
+void nr_ice_media_stream_refresh_consent_all(nr_ice_media_stream *stream)
+ {
+ nr_ice_component *comp;
+
+ comp=STAILQ_FIRST(&stream->components);
+ while(comp){
+ if(comp->disconnected) {
+ nr_ice_component_refresh_consent_now(comp);
+ }
+
+ comp=STAILQ_NEXT(comp,entry);
+ }
+ }
+
+void nr_ice_media_stream_disconnect_all_components(nr_ice_media_stream *stream)
+ {
+ nr_ice_component *comp;
+
+ comp=STAILQ_FIRST(&stream->components);
+ while(comp){
+ comp->disconnected = 1;
+
+ comp=STAILQ_NEXT(comp,entry);
+ }
+ }
+
+void nr_ice_media_stream_set_disconnected(nr_ice_media_stream *stream, int disconnected)
+ {
+ if (stream->disconnected == disconnected) {
+ return;
+ }
+
+ if (stream->ice_state != NR_ICE_MEDIA_STREAM_CHECKS_CONNECTED) {
+ return;
+ }
+ stream->disconnected = disconnected;
+
+ if (disconnected == NR_ICE_MEDIA_STREAM_DISCONNECTED) {
+ if (!stream->local_stream->obsolete) {
+ nr_ice_peer_ctx_disconnected(stream->pctx);
+ }
+ } else {
+ nr_ice_peer_ctx_check_if_connected(stream->pctx);
+ }
+ }
+
+int nr_ice_media_stream_check_if_connected(nr_ice_media_stream *stream)
+ {
+ nr_ice_component *comp;
+
+ comp=STAILQ_FIRST(&stream->components);
+ while(comp){
+ if((comp->state != NR_ICE_COMPONENT_DISABLED) &&
+ (comp->local_component->state != NR_ICE_COMPONENT_DISABLED) &&
+ comp->disconnected)
+ break;
+
+ comp=STAILQ_NEXT(comp,entry);
+ }
+
+ /* At least one disconnected component */
+ if(comp)
+ goto done;
+
+ nr_ice_media_stream_set_disconnected(stream, NR_ICE_MEDIA_STREAM_CONNECTED);
+
+ done:
+ return(0);
+ }
+
+/* S OK, this component has a nominated. If every component has a nominated,
+ the stream is ready */
+void nr_ice_media_stream_component_nominated(nr_ice_media_stream *stream,nr_ice_component *component)
+ {
+ nr_ice_component *comp;
+
+ comp=STAILQ_FIRST(&stream->components);
+ while(comp){
+ if((comp->state != NR_ICE_COMPONENT_DISABLED) &&
+ (comp->local_component->state != NR_ICE_COMPONENT_DISABLED) &&
+ !comp->nominated)
+ break;
+
+ comp=STAILQ_NEXT(comp,entry);
+ }
+
+ /* At least one un-nominated component */
+ if(comp)
+ return;
+
+ /* All done... */
+ r_log(LOG_ICE,LOG_INFO,"ICE-PEER(%s)/ICE-STREAM(%s): all active components have nominated candidate pairs",stream->pctx->label,stream->label);
+ nr_ice_media_stream_set_state(stream,NR_ICE_MEDIA_STREAM_CHECKS_CONNECTED);
+
+ /* Cancel our timer */
+ if(stream->timer){
+ NR_async_timer_cancel(stream->timer);
+ stream->timer=0;
+ }
+
+ if (stream->pctx->handler && !stream->local_stream->obsolete) {
+ stream->pctx->handler->vtbl->stream_ready(stream->pctx->handler->obj,stream->local_stream);
+ }
+
+ /* Now tell the peer_ctx that we're connected */
+ nr_ice_peer_ctx_check_if_connected(stream->pctx);
+ }
+
+void nr_ice_media_stream_component_failed(nr_ice_media_stream *stream,nr_ice_component *component)
+ {
+ component->state=NR_ICE_COMPONENT_FAILED;
+
+ /* at least one component failed in this media stream, so the entire
+ * media stream is marked failed */
+
+ nr_ice_media_stream_set_state(stream,NR_ICE_MEDIA_STREAM_CHECKS_FAILED);
+
+ nr_ice_media_stream_stop_checking(stream);
+
+ if (stream->pctx->handler && !stream->local_stream->obsolete) {
+ stream->pctx->handler->vtbl->stream_failed(stream->pctx->handler->obj,stream->local_stream);
+ }
+
+ /* Now tell the peer_ctx that we've failed */
+ nr_ice_peer_ctx_check_if_connected(stream->pctx);
+ }
+
+int nr_ice_media_stream_get_best_candidate(nr_ice_media_stream *str, int component, nr_ice_candidate **candp)
+ {
+ nr_ice_candidate *cand;
+ nr_ice_candidate *best_cand=0;
+ nr_ice_component *comp;
+ int r,_status;
+
+ if(r=nr_ice_media_stream_find_component(str,component,&comp))
+ ABORT(r);
+
+ cand=TAILQ_FIRST(&comp->candidates);
+ while(cand){
+ if(cand->state==NR_ICE_CAND_STATE_INITIALIZED){
+ if(!best_cand || (cand->priority>best_cand->priority))
+ best_cand=cand;
+
+ }
+ cand=TAILQ_NEXT(cand,entry_comp);
+ }
+
+ if(!best_cand)
+ ABORT(R_NOT_FOUND);
+
+ *candp=best_cand;
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+
+/* OK, we have the stream the user created, but that reflects the base
+ ICE ctx, not the peer_ctx. So, find the related stream in the pctx,
+ and then find the component */
+int nr_ice_media_stream_find_component(nr_ice_media_stream *str, int comp_id, nr_ice_component **compp)
+ {
+ int _status;
+ nr_ice_component *comp;
+
+ comp=STAILQ_FIRST(&str->components);
+ while(comp){
+ if(comp->component_id==comp_id)
+ break;
+
+ comp=STAILQ_NEXT(comp,entry);
+ }
+ if(!comp)
+ ABORT(R_NOT_FOUND);
+
+ *compp=comp;
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_ice_media_stream_send(nr_ice_peer_ctx *pctx, nr_ice_media_stream *str, int component, UCHAR *data, int len)
+ {
+ int r,_status;
+ nr_ice_component *comp;
+
+ /* First find the peer component */
+ if(r=nr_ice_peer_ctx_find_component(pctx, str, component, &comp))
+ ABORT(r);
+
+ /* Do we have an active pair yet? We should... */
+ if(!comp->active)
+ ABORT(R_NOT_FOUND);
+
+ /* Does fresh ICE consent exist? */
+ if(!comp->can_send)
+ ABORT(R_FAILED);
+
+ /* OK, write to that pair, which means:
+ 1. Use the socket on our local side.
+ 2. Use the address on the remote side
+ */
+ if(r=nr_socket_sendto(comp->active->local->osock,data,len,0,
+ &comp->active->remote->addr)) {
+ if ((r==R_IO_ERROR) || (r==R_EOD)) {
+ nr_ice_component_disconnected(comp);
+ }
+ ABORT(r);
+ }
+
+ // accumulate the sent bytes for the active candidate pair
+ comp->active->bytes_sent += len;
+ gettimeofday(&comp->active->last_sent, 0);
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+/* Returns R_REJECTED if the component is unpaired or has been disabled. */
+int nr_ice_media_stream_get_active(nr_ice_peer_ctx *pctx, nr_ice_media_stream *str, int component, nr_ice_candidate **local, nr_ice_candidate **remote)
+ {
+ int r,_status;
+ nr_ice_component *comp;
+
+ /* First find the peer component */
+ if(r=nr_ice_peer_ctx_find_component(pctx, str, component, &comp))
+ ABORT(r);
+
+ if (comp->state == NR_ICE_COMPONENT_UNPAIRED ||
+ comp->state == NR_ICE_COMPONENT_DISABLED)
+ ABORT(R_REJECTED);
+
+ if(!comp->active)
+ ABORT(R_NOT_FOUND);
+
+ if (local) *local = comp->active->local;
+ if (remote) *remote = comp->active->remote;
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_ice_media_stream_addrs(nr_ice_peer_ctx *pctx, nr_ice_media_stream *str, int component, nr_transport_addr *local, nr_transport_addr *remote)
+ {
+ int r,_status;
+ nr_ice_component *comp;
+
+ /* First find the peer component */
+ if(r=nr_ice_peer_ctx_find_component(pctx, str, component, &comp))
+ ABORT(r);
+
+ /* Do we have an active pair yet? We should... */
+ if(!comp->active)
+ ABORT(R_BAD_ARGS);
+
+ /* Use the socket on our local side */
+ if(r=nr_socket_getaddr(comp->active->local->osock,local))
+ ABORT(r);
+
+ /* Use the address on the remote side */
+ if(r=nr_transport_addr_copy(remote,&comp->active->remote->addr))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+
+
+int nr_ice_media_stream_finalize(nr_ice_media_stream *lstr,nr_ice_media_stream *rstr)
+ {
+ nr_ice_component *lcomp,*rcomp;
+
+ r_log(LOG_ICE,LOG_DEBUG,"Finalizing media stream %s, peer=%s",lstr->label,
+ rstr?rstr->label:"NONE");
+
+ lcomp=STAILQ_FIRST(&lstr->components);
+ if(rstr)
+ rcomp=STAILQ_FIRST(&rstr->components);
+ else
+ rcomp=0;
+
+ while(lcomp){
+ nr_ice_component_finalize(lcomp,rcomp);
+
+ lcomp=STAILQ_NEXT(lcomp,entry);
+ if(rcomp){
+ rcomp=STAILQ_NEXT(rcomp,entry);
+ }
+ }
+
+ return(0);
+ }
+
+int nr_ice_media_stream_pair_new_trickle_candidate(nr_ice_peer_ctx *pctx, nr_ice_media_stream *pstream, nr_ice_candidate *cand)
+ {
+ int r,_status;
+ nr_ice_component *comp;
+
+ if ((r=nr_ice_media_stream_find_component(pstream, cand->component_id, &comp)))
+ ABORT(R_NOT_FOUND);
+
+ if (r=nr_ice_component_pair_candidate(pctx, comp, cand, 1))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_ice_media_stream_get_consent_status(nr_ice_media_stream *stream, int
+component_id, int *can_send, struct timeval *ts)
+ {
+ int r,_status;
+ nr_ice_component *comp;
+
+ if ((r=nr_ice_media_stream_find_component(stream, component_id, &comp)))
+ ABORT(r);
+
+ *can_send = comp->can_send;
+ ts->tv_sec = comp->consent_last_seen.tv_sec;
+ ts->tv_usec = comp->consent_last_seen.tv_usec;
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_ice_media_stream_disable_component(nr_ice_media_stream *stream, int component_id)
+ {
+ int r,_status;
+ nr_ice_component *comp;
+
+ if (stream->ice_state != NR_ICE_MEDIA_STREAM_UNPAIRED)
+ ABORT(R_FAILED);
+
+ if ((r=nr_ice_media_stream_find_component(stream, component_id, &comp)))
+ ABORT(r);
+
+ /* Can only disable before pairing */
+ if (comp->state != NR_ICE_COMPONENT_UNPAIRED &&
+ comp->state != NR_ICE_COMPONENT_DISABLED)
+ ABORT(R_FAILED);
+
+ comp->state = NR_ICE_COMPONENT_DISABLED;
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+void nr_ice_media_stream_role_change(nr_ice_media_stream *stream)
+ {
+ nr_ice_cand_pair *pair,*temp_pair;
+ /* Changing role causes candidate pair priority to change, which requires
+ * re-sorting the check list. */
+ nr_ice_cand_pair_head old_checklist;
+
+ /* Move check_list to old_checklist (not POD, have to do the hard way) */
+ TAILQ_INIT(&old_checklist);
+ TAILQ_FOREACH_SAFE(pair,&stream->check_list,check_queue_entry,temp_pair) {
+ TAILQ_REMOVE(&stream->check_list,pair,check_queue_entry);
+ TAILQ_INSERT_TAIL(&old_checklist,pair,check_queue_entry);
+ }
+
+ /* Re-insert into the check list */
+ TAILQ_FOREACH_SAFE(pair,&old_checklist,check_queue_entry,temp_pair) {
+ TAILQ_REMOVE(&old_checklist,pair,check_queue_entry);
+ nr_ice_candidate_pair_role_change(pair);
+ nr_ice_candidate_pair_insert(&stream->check_list,pair);
+ }
+ }
+
+int nr_ice_media_stream_find_pair(nr_ice_media_stream *str, nr_ice_candidate *lcand, nr_ice_candidate *rcand, nr_ice_cand_pair **pair)
+ {
+ nr_ice_cand_pair_head *head = &str->check_list;
+ nr_ice_cand_pair *c1;
+
+ c1=TAILQ_FIRST(head);
+ while(c1){
+ if(c1->local == lcand &&
+ c1->remote == rcand) {
+ *pair=c1;
+ return(0);
+ }
+
+ c1=TAILQ_NEXT(c1,check_queue_entry);
+ }
+
+ return(R_NOT_FOUND);
+ }
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_media_stream.h b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_media_stream.h
new file mode 100644
index 0000000000..99f906c100
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_media_stream.h
@@ -0,0 +1,146 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+
+#ifndef _ice_media_stream_h
+#define _ice_media_stream_h
+#ifdef __cplusplus
+using namespace std;
+extern "C" {
+#endif /* __cplusplus */
+
+#include "transport_addr.h"
+
+typedef struct nr_ice_stun_server_ {
+ nr_transport_addr addr;
+ int id;
+} nr_ice_stun_server;
+
+typedef struct nr_ice_turn_server_ {
+ nr_ice_stun_server turn_server;
+ char *username;
+ Data *password;
+} nr_ice_turn_server;
+
+struct nr_ice_media_stream_ {
+ char *label;
+ struct nr_ice_ctx_ *ctx;
+ struct nr_ice_peer_ctx_ *pctx;
+
+ struct nr_ice_media_stream_ *local_stream; /* used when this is a peer */
+ int component_ct;
+ nr_ice_component_head components;
+
+ char *ufrag; /* ICE username */
+ char *pwd; /* ICE password */
+ char *r2l_user; /* The username for incoming requests */
+ char *l2r_user; /* The username for outgoing requests */
+ Data r2l_pass; /* The password for incoming requests */
+ Data l2r_pass; /* The password for outcoming requests */
+ int ice_state;
+ /* The stream is being replaced by another, so it will not continue any ICE
+ * processing. If this stream is connected already, traffic can continue to
+ * flow for a limited time while the new stream gets ready. */
+ int obsolete;
+
+#define NR_ICE_MEDIA_STREAM_UNPAIRED 1
+#define NR_ICE_MEDIA_STREAM_CHECKS_FROZEN 2
+#define NR_ICE_MEDIA_STREAM_CHECKS_ACTIVE 3
+#define NR_ICE_MEDIA_STREAM_CHECKS_CONNECTED 4
+#define NR_ICE_MEDIA_STREAM_CHECKS_FAILED 5
+
+ int disconnected;
+
+#define NR_ICE_MEDIA_STREAM_CONNECTED 0
+#define NR_ICE_MEDIA_STREAM_DISCONNECTED 1
+
+ /* Copy of flags field from nr_ice_ctx */
+ int flags;
+
+ /* Copy of STUN/TURN servers from nr_ice_ctx */
+ nr_ice_stun_server *stun_servers; /* The list of stun servers */
+ int stun_server_ct;
+ nr_ice_turn_server *turn_servers; /* The list of turn servers */
+ int turn_server_ct;
+
+ nr_ice_cand_pair_head check_list;
+ nr_ice_cand_pair_head trigger_check_queue;
+ void *timer; /* Check list periodic timer */
+
+/* nr_ice_cand_pair_head valid_list; */
+
+ STAILQ_ENTRY(nr_ice_media_stream_) entry;
+};
+
+typedef STAILQ_HEAD(nr_ice_media_stream_head_,nr_ice_media_stream_) nr_ice_media_stream_head;
+
+int nr_ice_media_stream_create(struct nr_ice_ctx_ *ctx,const char *label,const char *ufrag,const char *pwd,int components, nr_ice_media_stream **streamp);
+int nr_ice_media_stream_destroy(nr_ice_media_stream **streamp);
+int nr_ice_media_stream_finalize(nr_ice_media_stream *lstr,nr_ice_media_stream *rstr);
+int nr_ice_media_stream_initialize(struct nr_ice_ctx_ *ctx, nr_ice_media_stream *stream);
+int nr_ice_media_stream_get_attributes(nr_ice_media_stream *stream, char ***attrsp,int *attrctp);
+int nr_ice_media_stream_get_default_candidate(nr_ice_media_stream *stream, int component, nr_ice_candidate **candp);
+int nr_ice_media_stream_pair_candidates(nr_ice_peer_ctx *pctx,nr_ice_media_stream *lstream,nr_ice_media_stream *pstream);
+int nr_ice_media_stream_start_checks(nr_ice_peer_ctx *pctx, nr_ice_media_stream *stream);
+int nr_ice_media_stream_service_pre_answer_requests(nr_ice_peer_ctx *pctx,nr_ice_media_stream *lstream,nr_ice_media_stream *pstream, int *serviced);
+int nr_ice_media_stream_unfreeze_pairs(nr_ice_peer_ctx *pctx, nr_ice_media_stream *stream);
+int nr_ice_media_stream_unfreeze_pairs_foundation(nr_ice_media_stream *stream, char *foundation);
+void nr_ice_media_stream_dump_state(nr_ice_peer_ctx *pctx, nr_ice_media_stream *stream, int log_level);
+void nr_ice_media_stream_component_nominated(nr_ice_media_stream *stream,nr_ice_component *component);
+void nr_ice_media_stream_component_failed(nr_ice_media_stream *stream,nr_ice_component *component);
+void nr_ice_media_stream_refresh_consent_all(nr_ice_media_stream *stream);
+void nr_ice_media_stream_disconnect_all_components(nr_ice_media_stream *stream);
+void nr_ice_media_stream_set_disconnected(nr_ice_media_stream *stream, int disconnected);
+int nr_ice_media_stream_check_if_connected(nr_ice_media_stream *stream);
+int nr_ice_media_stream_set_state(nr_ice_media_stream *str, int state);
+void nr_ice_media_stream_stop_checking(nr_ice_media_stream *str);
+void nr_ice_media_stream_set_obsolete(nr_ice_media_stream *str);
+int nr_ice_media_stream_is_done_gathering(nr_ice_media_stream *str);
+int nr_ice_media_stream_get_best_candidate(nr_ice_media_stream *str, int component, nr_ice_candidate **candp);
+int nr_ice_media_stream_send(nr_ice_peer_ctx *pctx, nr_ice_media_stream *str, int component, UCHAR *data, int len);
+int nr_ice_media_stream_get_active(nr_ice_peer_ctx *pctx, nr_ice_media_stream *str, int component, nr_ice_candidate **local, nr_ice_candidate **remote);
+int nr_ice_media_stream_find_component(nr_ice_media_stream *str, int comp_id, nr_ice_component **compp);
+int nr_ice_media_stream_find_pair(nr_ice_media_stream *str, nr_ice_candidate *local, nr_ice_candidate *remote, nr_ice_cand_pair **pair);
+int nr_ice_media_stream_addrs(nr_ice_peer_ctx *pctx, nr_ice_media_stream *str, int component, nr_transport_addr *local, nr_transport_addr *remote);
+int
+nr_ice_peer_ctx_parse_media_stream_attribute(nr_ice_peer_ctx *pctx, nr_ice_media_stream *stream, char *attr);
+int nr_ice_media_stream_get_consent_status(nr_ice_media_stream *stream, int component_id, int *can_send, struct timeval *ts);
+int nr_ice_media_stream_disable_component(nr_ice_media_stream *stream, int component_id);
+int nr_ice_media_stream_pair_new_trickle_candidate(nr_ice_peer_ctx *pctx, nr_ice_media_stream *pstream, nr_ice_candidate *cand);
+void nr_ice_media_stream_role_change(nr_ice_media_stream *stream);
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_parser.c b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_parser.c
new file mode 100644
index 0000000000..25cda3364a
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_parser.c
@@ -0,0 +1,564 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <csi_platform.h>
+#include <sys/types.h>
+#ifdef WIN32
+#include <winsock2.h>
+#else
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <strings.h>
+#endif
+#include <string.h>
+#include <assert.h>
+#include <ctype.h>
+#include "nr_api.h"
+#include "ice_ctx.h"
+#include "ice_candidate.h"
+#include "ice_reg.h"
+
+static void
+skip_whitespace(char **str)
+{
+ char *c = *str;
+ while (*c == ' ')
+ ++c;
+
+ *str = c;
+}
+
+static void
+fast_forward(char **str, int skip)
+{
+ char *c = *str;
+ while (*c != '\0' && skip-- > 0)
+ ++c;
+
+ *str = c;
+}
+
+static void
+skip_to_past_space(char **str)
+{
+ char *c = *str;
+ while (*c != ' ' && *c != '\0')
+ ++c;
+
+ *str = c;
+
+ skip_whitespace(str);
+}
+
+static int
+grab_token(char **str, char **out)
+{
+ int _status;
+ char *c = *str;
+ int len;
+ char *tmp;
+
+ while (*c != ' ' && *c != '\0')
+ ++c;
+
+ len = c - *str;
+
+ tmp = RMALLOC(len + 1);
+ if (!tmp)
+ ABORT(R_NO_MEMORY);
+
+ memcpy(tmp, *str, len);
+ tmp[len] = '\0';
+
+ *str = c;
+ *out = tmp;
+
+ _status = 0;
+abort:
+ return _status;
+}
+
+int
+nr_ice_peer_candidate_from_attribute(nr_ice_ctx *ctx,char *orig,nr_ice_media_stream *stream,nr_ice_candidate **candp)
+{
+ int r,_status;
+ char* str = orig;
+ nr_ice_candidate *cand;
+ char *connection_address=0;
+ unsigned int port;
+ int i;
+ unsigned int component_id;
+ char *rel_addr=0;
+ unsigned char transport;
+
+ if(!(cand=RCALLOC(sizeof(nr_ice_candidate))))
+ ABORT(R_NO_MEMORY);
+
+ if(!(cand->label=r_strdup(orig)))
+ ABORT(R_NO_MEMORY);
+
+ cand->ctx=ctx;
+ cand->isock=0;
+ cand->state=NR_ICE_CAND_PEER_CANDIDATE_UNPAIRED;
+ cand->stream=stream;
+ skip_whitespace(&str);
+
+ /* Skip a= if present */
+ if (!strncmp(str, "a=", 2))
+ str += 2;
+
+ /* Candidate attr */
+ if (strncasecmp(str, "candidate:", 10))
+ ABORT(R_BAD_DATA);
+
+ fast_forward(&str, 10);
+ if (*str == '\0')
+ ABORT(R_BAD_DATA);
+
+ skip_whitespace(&str);
+ if (*str == '\0')
+ ABORT(R_BAD_DATA);
+
+ /* Foundation */
+ if ((r=grab_token(&str, &cand->foundation)))
+ ABORT(r);
+
+ if (*str == '\0')
+ ABORT(R_BAD_DATA);
+
+ skip_whitespace(&str);
+ if (*str == '\0')
+ ABORT(R_BAD_DATA);
+
+ /* component */
+ if (sscanf(str, "%u", &component_id) != 1)
+ ABORT(R_BAD_DATA);
+
+ if (component_id < 1 || component_id > 256)
+ ABORT(R_BAD_DATA);
+
+ cand->component_id = (UCHAR)component_id;
+
+ skip_to_past_space(&str);
+ if (*str == '\0')
+ ABORT(R_BAD_DATA);
+
+ /* Protocol */
+ if (!strncasecmp(str, "UDP", 3))
+ transport=IPPROTO_UDP;
+ else if (!strncasecmp(str, "TCP", 3))
+ transport=IPPROTO_TCP;
+ else
+ ABORT(R_BAD_DATA);
+
+ fast_forward(&str, 3);
+ if (*str == '\0')
+ ABORT(R_BAD_DATA);
+
+ skip_whitespace(&str);
+ if (*str == '\0')
+ ABORT(R_BAD_DATA);
+
+ /* priority */
+ if (sscanf(str, "%u", &cand->priority) != 1)
+ ABORT(R_BAD_DATA);
+
+ if (cand->priority < 1)
+ ABORT(R_BAD_DATA);
+
+ skip_to_past_space(&str);
+ if (*str == '\0')
+ ABORT(R_BAD_DATA);
+
+ /* Peer address/port */
+ if ((r=grab_token(&str, &connection_address)))
+ ABORT(r);
+
+ if (*str == '\0')
+ ABORT(R_BAD_DATA);
+
+ skip_whitespace(&str);
+ if (*str == '\0')
+ ABORT(R_BAD_DATA);
+
+ if (sscanf(str, "%u", &port) != 1)
+ ABORT(R_BAD_DATA);
+
+ if (port < 1 || port > 0x0FFFF)
+ ABORT(R_BAD_DATA);
+
+ if ((r=nr_str_port_to_transport_addr(connection_address,port,transport,&cand->addr)))
+ ABORT(r);
+
+ skip_to_past_space(&str);
+ if (*str == '\0')
+ ABORT(R_BAD_DATA);
+
+ /* Type */
+ if (strncasecmp("typ", str, 3))
+ ABORT(R_BAD_DATA);
+
+ fast_forward(&str, 3);
+ if (*str == '\0')
+ ABORT(R_BAD_DATA);
+
+ skip_whitespace(&str);
+ if (*str == '\0')
+ ABORT(R_BAD_DATA);
+
+ assert(nr_ice_candidate_type_names[0] == 0);
+
+ for (i = 1; nr_ice_candidate_type_names[i]; ++i) {
+ if(!strncasecmp(nr_ice_candidate_type_names[i], str, strlen(nr_ice_candidate_type_names[i]))) {
+ cand->type=i;
+ break;
+ }
+ }
+ if (nr_ice_candidate_type_names[i] == 0)
+ ABORT(R_BAD_DATA);
+
+ fast_forward(&str, strlen(nr_ice_candidate_type_names[i]));
+
+ /* Look for the other side's raddr, rport */
+ /* raddr, rport */
+ switch (cand->type) {
+ case HOST:
+ break;
+ case SERVER_REFLEXIVE:
+ case PEER_REFLEXIVE:
+ case RELAYED:
+
+ skip_whitespace(&str);
+ if (*str == '\0')
+ ABORT(R_BAD_DATA);
+
+ if (strncasecmp("raddr", str, 5))
+ ABORT(R_BAD_DATA);
+
+ fast_forward(&str, 5);
+ if (*str == '\0')
+ ABORT(R_BAD_DATA);
+
+ skip_whitespace(&str);
+ if (*str == '\0')
+ ABORT(R_BAD_DATA);
+
+ if ((r=grab_token(&str, &rel_addr)))
+ ABORT(r);
+
+ if (*str == '\0')
+ ABORT(R_BAD_DATA);
+
+ skip_whitespace(&str);
+ if (*str == '\0')
+ ABORT(R_BAD_DATA);
+
+ if (strncasecmp("rport", str, 5))
+ ABORT(R_BAD_DATA);
+
+ fast_forward(&str, 5);
+ if (*str == '\0')
+ ABORT(R_BAD_DATA);
+
+ skip_whitespace(&str);
+ if (*str == '\0')
+ ABORT(R_BAD_DATA);
+
+ if (sscanf(str, "%u", &port) != 1)
+ ABORT(R_BAD_DATA);
+
+ if (port > 0x0FFFF)
+ ABORT(R_BAD_DATA);
+
+ if ((r=nr_str_port_to_transport_addr(rel_addr,port,transport,&cand->base)))
+ ABORT(r);
+
+ skip_to_past_space(&str);
+ /* it's expected to be at EOD at this point */
+
+ break;
+ default:
+ ABORT(R_INTERNAL);
+ break;
+ }
+
+ skip_whitespace(&str);
+
+ if (transport == IPPROTO_TCP && cand->type != RELAYED) {
+ /* Parse tcptype extension per RFC 6544 S 4.5 */
+ if (strncasecmp("tcptype ", str, 8))
+ ABORT(R_BAD_DATA);
+
+ fast_forward(&str, 8);
+ skip_whitespace(&str);
+
+ for (i = 1; nr_ice_candidate_tcp_type_names[i]; ++i) {
+ if(!strncasecmp(nr_ice_candidate_tcp_type_names[i], str, strlen(nr_ice_candidate_tcp_type_names[i]))) {
+ cand->tcp_type=i;
+ fast_forward(&str, strlen(nr_ice_candidate_tcp_type_names[i]));
+ break;
+ }
+ }
+
+ if (cand->tcp_type == 0)
+ ABORT(R_BAD_DATA);
+
+ if (*str && *str != ' ')
+ ABORT(R_BAD_DATA);
+ }
+ /* Ignore extensions per RFC 5245 S 15.1 */
+#if 0
+ /* This used to be an assert, but we don't want to exit on invalid
+ remote data */
+ if (strlen(str) != 0) {
+ ABORT(R_BAD_DATA);
+ }
+#endif
+
+ nr_ice_candidate_compute_codeword(cand);
+
+ *candp=cand;
+
+ _status=0;
+ abort:
+ if (_status){
+ r_log(LOG_ICE,LOG_WARNING,"ICE(%s): Error parsing attribute: %s",ctx->label,orig);
+ nr_ice_candidate_destroy(&cand);
+ }
+
+ RFREE(connection_address);
+ RFREE(rel_addr);
+ return(_status);
+}
+
+
+int
+nr_ice_peer_ctx_parse_media_stream_attribute(nr_ice_peer_ctx *pctx, nr_ice_media_stream *stream, char *attr)
+{
+ int r,_status;
+ char *orig = 0;
+ char *str;
+
+ orig = str = attr;
+
+ if (!strncasecmp(str, "ice-ufrag:", 10)) {
+ fast_forward(&str, 10);
+ if (*str == '\0')
+ ABORT(R_BAD_DATA);
+
+ skip_whitespace(&str);
+ if (*str == '\0')
+ ABORT(R_BAD_DATA);
+
+ RFREE(stream->ufrag);
+ if ((r=grab_token(&str, &stream->ufrag)))
+ ABORT(r);
+ }
+ else if (!strncasecmp(str, "ice-pwd:", 8)) {
+ fast_forward(&str, 8);
+ if (*str == '\0')
+ ABORT(R_BAD_DATA);
+
+ skip_whitespace(&str);
+ if (*str == '\0')
+ ABORT(R_BAD_DATA);
+
+ RFREE(stream->pwd);
+ if ((r=grab_token(&str, &stream->pwd)))
+ ABORT(r);
+ }
+ else {
+ ABORT(R_BAD_DATA);
+ }
+
+ skip_whitespace(&str);
+
+ /* RFC 5245 grammar doesn't have an extension point for ice-pwd or
+ ice-ufrag: if there's anything left on the line, we treat it as bad. */
+ if (str[0] != '\0') {
+ ABORT(R_BAD_DATA);
+ }
+
+ _status=0;
+ abort:
+ if (_status) {
+ if (orig)
+ r_log(LOG_ICE,LOG_WARNING,"ICE-PEER(%s): Error parsing attribute: %s",pctx->label,orig);
+ }
+
+ return(_status);
+}
+
+int
+nr_ice_peer_ctx_parse_global_attributes(nr_ice_peer_ctx *pctx, char **attrs, int attr_ct)
+{
+ int r,_status;
+ int i;
+ char *orig = 0;
+ char *str;
+ char *component_id = 0;
+ char *connection_address = 0;
+ unsigned int port;
+ in_addr_t addr;
+ char *ice_option_tag = 0;
+
+ for(i=0;i<attr_ct;i++){
+ orig = str = attrs[i];
+
+ component_id = 0;
+ connection_address = 0;
+ ice_option_tag = 0;
+
+ if (!strncasecmp(str, "remote-candidates:", 18)) {
+ fast_forward(&str, 18);
+ skip_whitespace(&str);
+
+ while (*str != '\0') {
+ if ((r=grab_token(&str, &component_id)))
+ ABORT(r);
+
+ if (*str == '\0')
+ ABORT(R_BAD_DATA);
+
+ skip_whitespace(&str);
+ if (*str == '\0')
+ ABORT(R_BAD_DATA);
+
+ if ((r=grab_token(&str, &connection_address)))
+ ABORT(r);
+
+ if (*str == '\0')
+ ABORT(R_BAD_DATA);
+
+ addr = inet_addr(connection_address);
+ if (addr == INADDR_NONE)
+ ABORT(R_BAD_DATA);
+
+ skip_whitespace(&str);
+ if (*str == '\0')
+ ABORT(R_BAD_DATA);
+
+ if (sscanf(str, "%u", &port) != 1)
+ ABORT(R_BAD_DATA);
+
+ if (port < 1 || port > 0x0FFFF)
+ ABORT(R_BAD_DATA);
+
+ skip_to_past_space(&str);
+
+#if 0
+ /* TODO: !nn! just drop on the floor for now, later put somewhere */
+ /* Assume v4 for now */
+ if(r=nr_ip4_port_to_transport_addr(ntohl(addr),port,IPPROTO_UDP,&candidate->base))
+ ABORT(r);
+
+ TAILQ_INSERT_TAIL(head, elm, field);
+#endif
+
+ component_id = 0; /* prevent free */
+ RFREE(connection_address);
+ connection_address = 0; /* prevent free */
+ }
+ }
+ else if (!strncasecmp(str, "ice-lite", 8)) {
+ pctx->peer_lite = 1;
+ pctx->controlling = 1;
+
+ fast_forward(&str, 8);
+ }
+ else if (!strncasecmp(str, "ice-mismatch", 12)) {
+ pctx->peer_ice_mismatch = 1;
+
+ fast_forward(&str, 12);
+ }
+ else if (!strncasecmp(str, "ice-ufrag:", 10)) {
+ fast_forward(&str, 10);
+ if (*str == '\0')
+ ABORT(R_BAD_DATA);
+
+ skip_whitespace(&str);
+ if (*str == '\0')
+ ABORT(R_BAD_DATA);
+ }
+ else if (!strncasecmp(str, "ice-pwd:", 8)) {
+ fast_forward(&str, 8);
+ if (*str == '\0')
+ ABORT(R_BAD_DATA);
+
+ skip_whitespace(&str);
+ if (*str == '\0')
+ ABORT(R_BAD_DATA);
+ }
+ else if (!strncasecmp(str, "ice-options:", 12)) {
+ fast_forward(&str, 12);
+ skip_whitespace(&str);
+
+ while (*str != '\0') {
+ if ((r=grab_token(&str, &ice_option_tag)))
+ ABORT(r);
+
+ skip_whitespace(&str);
+
+ //TODO: for now, just throw away; later put somewhere
+ RFREE(ice_option_tag);
+
+ ice_option_tag = 0; /* prevent free */
+ }
+ }
+ else {
+ ABORT(R_BAD_DATA);
+ }
+
+ skip_whitespace(&str);
+
+ /* RFC 5245 grammar doesn't have an extension point for any of the
+ preceding attributes: if there's anything left on the line, we
+ treat it as bad data. */
+ if (str[0] != '\0') {
+ ABORT(R_BAD_DATA);
+ }
+ }
+
+ _status=0;
+ abort:
+ if (_status) {
+ if (orig)
+ r_log(LOG_ICE,LOG_WARNING,"ICE-PEER(%s): Error parsing attribute: %s",pctx->label,orig);
+ }
+
+ RFREE(connection_address);
+ RFREE(component_id);
+ RFREE(ice_option_tag);
+ return(_status);
+}
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_peer_ctx.c b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_peer_ctx.c
new file mode 100644
index 0000000000..0bf97eb984
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_peer_ctx.c
@@ -0,0 +1,875 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <string.h>
+#include <assert.h>
+#include <registry.h>
+#include <nr_api.h>
+#include "ice_ctx.h"
+#include "ice_peer_ctx.h"
+#include "ice_media_stream.h"
+#include "ice_util.h"
+#include "nr_crypto.h"
+#include "async_timer.h"
+#include "ice_reg.h"
+
+static void nr_ice_peer_ctx_parse_stream_attributes_int(nr_ice_peer_ctx *pctx, nr_ice_media_stream *stream, nr_ice_media_stream *pstream, char **attrs, int attr_ct);
+static int nr_ice_ctx_parse_candidate(nr_ice_peer_ctx *pctx, nr_ice_media_stream *pstream, char *candidate, int trickled, const char *mdns_addr);
+static void nr_ice_peer_ctx_start_trickle_timer(nr_ice_peer_ctx *pctx);
+
+int nr_ice_peer_ctx_create(nr_ice_ctx *ctx, nr_ice_handler *handler,char *label, nr_ice_peer_ctx **pctxp)
+ {
+ int r,_status;
+ nr_ice_peer_ctx *pctx=0;
+
+ if(!(pctx=RCALLOC(sizeof(nr_ice_peer_ctx))))
+ ABORT(R_NO_MEMORY);
+
+ pctx->state = NR_ICE_PEER_STATE_UNPAIRED;
+
+ if(!(pctx->label=r_strdup(label)))
+ ABORT(R_NO_MEMORY);
+
+ pctx->ctx=ctx;
+ pctx->handler=handler;
+
+ /* Decide controlling vs. controlled */
+ if(ctx->flags & NR_ICE_CTX_FLAGS_LITE){
+ pctx->controlling=0;
+ } else {
+ pctx->controlling=1;
+ }
+ if(r=nr_crypto_random_bytes((UCHAR *)&pctx->tiebreaker,8))
+ ABORT(r);
+
+ STAILQ_INIT(&pctx->peer_streams);
+
+ STAILQ_INSERT_TAIL(&ctx->peers,pctx,entry);
+
+ *pctxp=pctx;
+
+ _status = 0;
+ abort:
+ if(_status){
+ nr_ice_peer_ctx_destroy(&pctx);
+ }
+ return(_status);
+ }
+
+
+
+int nr_ice_peer_ctx_parse_stream_attributes(nr_ice_peer_ctx *pctx, nr_ice_media_stream *stream, char **attrs, int attr_ct)
+ {
+ nr_ice_media_stream *pstream=0;
+ nr_ice_component *comp,*comp2;
+ char *lufrag,*rufrag;
+ char *lpwd,*rpwd;
+ int r,_status;
+
+ /*
+ Note: use component_ct from our own stream since components other
+ than this offered by the other side are unusable */
+ if(r=nr_ice_media_stream_create(pctx->ctx,stream->label,"","",stream->component_ct,&pstream))
+ ABORT(r);
+
+ /* Match up the local and remote components */
+ comp=STAILQ_FIRST(&stream->components);
+ comp2=STAILQ_FIRST(&pstream->components);
+ while(comp){
+ comp2->local_component=comp;
+
+ comp=STAILQ_NEXT(comp,entry);
+ comp2=STAILQ_NEXT(comp2,entry);
+ }
+
+ pstream->local_stream=stream;
+ pstream->pctx=pctx;
+
+ nr_ice_peer_ctx_parse_stream_attributes_int(pctx,stream,pstream,attrs,attr_ct);
+
+ /* Now that we have the ufrag and password, compute all the username/password
+ pairs */
+ lufrag=stream->ufrag;
+ lpwd=stream->pwd;
+ assert(lufrag);
+ assert(lpwd);
+ rufrag=pstream->ufrag;
+ rpwd=pstream->pwd;
+ if (!rufrag || !rpwd)
+ ABORT(R_BAD_DATA);
+
+ if(r=nr_concat_strings(&pstream->r2l_user,lufrag,":",rufrag,NULL))
+ ABORT(r);
+ if(r=nr_concat_strings(&pstream->l2r_user,rufrag,":",lufrag,NULL))
+ ABORT(r);
+ if(r=r_data_make(&pstream->r2l_pass, (UCHAR *)lpwd, strlen(lpwd)))
+ ABORT(r);
+ if(r=r_data_make(&pstream->l2r_pass, (UCHAR *)rpwd, strlen(rpwd)))
+ ABORT(r);
+
+ STAILQ_INSERT_TAIL(&pctx->peer_streams,pstream,entry);
+ pstream=0;
+
+ _status=0;
+ abort:
+ if (_status) {
+ nr_ice_media_stream_destroy(&pstream);
+ }
+ return(_status);
+ }
+
+static void nr_ice_peer_ctx_parse_stream_attributes_int(nr_ice_peer_ctx *pctx, nr_ice_media_stream *stream, nr_ice_media_stream *pstream, char **attrs, int attr_ct)
+ {
+ int r;
+ int i;
+
+ for(i=0;i<attr_ct;i++){
+ if(!strncmp(attrs[i],"ice-",4)){
+ if(r=nr_ice_peer_ctx_parse_media_stream_attribute(pctx,pstream,attrs[i])) {
+ r_log(LOG_ICE,LOG_WARNING,"ICE(%s): peer (%s) specified bogus ICE attribute",pctx->ctx->label,pctx->label);
+ continue;
+ }
+ }
+ else if (!strncmp(attrs[i],"candidate",9)){
+ if(r=nr_ice_ctx_parse_candidate(pctx,pstream,attrs[i],0,0)) {
+ r_log(LOG_ICE,LOG_WARNING,"ICE(%s): peer (%s) specified bogus candidate",pctx->ctx->label,pctx->label);
+ continue;
+ }
+ }
+ else {
+ r_log(LOG_ICE,LOG_WARNING,"ICE(%s): peer (%s) specified bogus attribute: %s",pctx->ctx->label,pctx->label,attrs[i]);
+ }
+ }
+
+ /* Doesn't fail because we just skip errors */
+ }
+
+static int nr_ice_ctx_parse_candidate(nr_ice_peer_ctx *pctx, nr_ice_media_stream *pstream, char *candidate, int trickled, const char *mdns_addr)
+ {
+ nr_ice_candidate *cand=0;
+ nr_ice_component *comp;
+ int j;
+ int r, _status;
+
+ if(r=nr_ice_peer_candidate_from_attribute(pctx->ctx,candidate,pstream,&cand))
+ ABORT(r);
+
+ /* set the trickled flag on the candidate */
+ cand->trickled = trickled;
+
+ if (mdns_addr) {
+ cand->mdns_addr = r_strdup(mdns_addr);
+ if (!cand->mdns_addr) {
+ ABORT(R_NO_MEMORY);
+ }
+ }
+
+ /* Not the fastest way to find a component, but it's what we got */
+ j=1;
+ for(comp=STAILQ_FIRST(&pstream->components);comp;comp=STAILQ_NEXT(comp,entry)){
+ if(j==cand->component_id)
+ break;
+
+ j++;
+ }
+
+ if(!comp){
+ /* Very common for the answerer when it uses rtcp-mux */
+ r_log(LOG_ICE,LOG_INFO,"ICE(%s): peer (%s) no such component for candidate %s",pctx->ctx->label,pctx->label, candidate);
+ ABORT(R_REJECTED);
+ }
+
+ if (comp->state == NR_ICE_COMPONENT_DISABLED) {
+ r_log(LOG_ICE,LOG_WARNING,"Peer offered candidate for disabled remote component: %s", candidate);
+ ABORT(R_BAD_DATA);
+ }
+ if (comp->local_component->state == NR_ICE_COMPONENT_DISABLED) {
+ r_log(LOG_ICE,LOG_WARNING,"Peer offered candidate for disabled local component: %s", candidate);
+ ABORT(R_BAD_DATA);
+ }
+
+ cand->component=comp;
+
+ TAILQ_INSERT_TAIL(&comp->candidates,cand,entry_comp);
+
+ r_log(LOG_ICE,LOG_DEBUG,"ICE-PEER(%s)/CAND(%s): creating peer candidate",
+ pctx->label,cand->label);
+
+ _status=0;
+ abort:
+ if (_status) {
+ nr_ice_candidate_destroy(&cand);
+ }
+ return(_status);
+ }
+
+int nr_ice_peer_ctx_find_pstream(nr_ice_peer_ctx *pctx, nr_ice_media_stream *stream, nr_ice_media_stream **pstreamp)
+ {
+ int _status;
+ nr_ice_media_stream *pstream;
+
+ /* Because we don't have forward pointers, iterate through all the
+ peer streams to find one that matches us */
+ pstream=STAILQ_FIRST(&pctx->peer_streams);
+
+ if(!pstream) {
+ /* No peer streams at all, presumably because they do not exist yet.
+ * Don't log a warning here. */
+ ABORT(R_NOT_FOUND);
+ }
+
+ while(pstream) {
+ if (pstream->local_stream == stream)
+ break;
+
+ pstream = STAILQ_NEXT(pstream, entry);
+ }
+
+ if (!pstream) {
+ r_log(LOG_ICE,LOG_WARNING,"ICE(%s): peer (%s) has no stream matching stream %s",pctx->ctx->label,pctx->label,stream->label);
+ ABORT(R_NOT_FOUND);
+ }
+
+ *pstreamp = pstream;
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_ice_peer_ctx_remove_pstream(nr_ice_peer_ctx *pctx, nr_ice_media_stream **pstreamp)
+ {
+ int r,_status;
+
+ STAILQ_REMOVE(&pctx->peer_streams,*pstreamp,nr_ice_media_stream_,entry);
+
+ if(r=nr_ice_media_stream_destroy(pstreamp)) {
+ ABORT(r);
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_ice_peer_ctx_parse_trickle_candidate(nr_ice_peer_ctx *pctx, nr_ice_media_stream *stream, char *candidate, const char *mdns_addr)
+ {
+ nr_ice_media_stream *pstream;
+ int r,_status;
+ int needs_pairing = 0;
+
+ if (stream->obsolete) {
+ return 0;
+ }
+
+ r_log(LOG_ICE,LOG_DEBUG,"ICE(%s): peer (%s) parsing trickle ICE candidate %s",pctx->ctx->label,pctx->label,candidate);
+ r = nr_ice_peer_ctx_find_pstream(pctx, stream, &pstream);
+ if (r)
+ ABORT(r);
+
+ switch(pstream->ice_state) {
+ case NR_ICE_MEDIA_STREAM_UNPAIRED:
+ break;
+ case NR_ICE_MEDIA_STREAM_CHECKS_FROZEN:
+ case NR_ICE_MEDIA_STREAM_CHECKS_ACTIVE:
+ needs_pairing = 1;
+ break;
+ default:
+ r_log(LOG_ICE,LOG_ERR,"ICE(%s): peer (%s), stream(%s) tried to trickle ICE in inappropriate state %d",pctx->ctx->label,pctx->label,stream->label,pstream->ice_state);
+ ABORT(R_ALREADY);
+ break;
+ }
+
+ if(r=nr_ice_ctx_parse_candidate(pctx,pstream,candidate,1,mdns_addr)){
+ ABORT(r);
+ }
+
+ /* If ICE is running (i.e., we are in FROZEN or ACTIVE states)
+ then we need to pair this new candidate. For now we
+ just re-pair the stream which is inefficient but still
+ fine because we suppress duplicate pairing */
+ if (needs_pairing) {
+ /* Start the remote trickle grace timeout if it hasn't been started by
+ another trickled candidate or from the SDP. */
+ if (!pctx->trickle_grace_period_timer) {
+ nr_ice_peer_ctx_start_trickle_timer(pctx);
+ }
+
+ if(r=nr_ice_media_stream_pair_candidates(pctx, stream, pstream)) {
+ r_log(LOG_ICE,LOG_ERR,"ICE(%s): peer (%s), stream(%s) failed to pair trickle ICE candidates",pctx->ctx->label,pctx->label,stream->label);
+ ABORT(r);
+ }
+
+ /* Start checks if this stream is not checking yet or if it has checked
+ all the available candidates but not had a completed check for all
+ components.
+
+ Note that this is not compliant with RFC 5245, but consistent with
+ the libjingle trickle ICE behavior. Note that we will not restart
+ checks if either (a) the stream has failed or (b) all components
+ have a successful pair because the switch statement above jumps
+ will in both states.
+
+ TODO(ekr@rtfm.com): restart checks.
+ TODO(ekr@rtfm.com): update when the trickle ICE RFC is published
+ */
+ if (!pstream->timer) {
+ if(r=nr_ice_media_stream_start_checks(pctx, pstream)) {
+ r_log(LOG_ICE,LOG_ERR,"ICE(%s): peer (%s), stream(%s) failed to start checks",pctx->ctx->label,pctx->label,stream->label);
+ ABORT(r);
+ }
+ }
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+
+ }
+
+
+static void nr_ice_peer_ctx_trickle_wait_cb(NR_SOCKET s, int how, void *cb_arg)
+ {
+ nr_ice_peer_ctx *pctx=cb_arg;
+ nr_ice_media_stream *stream;
+ nr_ice_component *comp;
+
+ pctx->trickle_grace_period_timer=0;
+
+ r_log(LOG_ICE,LOG_INFO,"ICE(%s): peer (%s) Trickle grace period is over; marking every component with only failed pairs as failed.",pctx->ctx->label,pctx->label);
+
+ stream=STAILQ_FIRST(&pctx->peer_streams);
+ while(stream){
+ comp=STAILQ_FIRST(&stream->components);
+ while(comp){
+ nr_ice_component_check_if_failed(comp);
+ comp=STAILQ_NEXT(comp,entry);
+ }
+ stream=STAILQ_NEXT(stream,entry);
+ }
+ }
+
+static void nr_ice_peer_ctx_start_trickle_timer(nr_ice_peer_ctx *pctx)
+ {
+ UINT4 grace_period_timeout=0;
+
+ if(pctx->trickle_grace_period_timer) {
+ NR_async_timer_cancel(pctx->trickle_grace_period_timer);
+ pctx->trickle_grace_period_timer=0;
+ }
+
+ NR_reg_get_uint4(NR_ICE_REG_TRICKLE_GRACE_PERIOD,&grace_period_timeout);
+
+ if (grace_period_timeout) {
+ r_log(LOG_ICE,LOG_INFO,"ICE(%s): peer (%s) starting grace period timer for %u ms",pctx->ctx->label,pctx->label, grace_period_timeout);
+ /* If we're doing trickle, we need to allow a grace period for new
+ * trickle candidates to arrive in case the pairs we have fail quickly. */
+ NR_ASYNC_TIMER_SET(grace_period_timeout,nr_ice_peer_ctx_trickle_wait_cb,pctx,&pctx->trickle_grace_period_timer);
+ }
+ }
+
+int nr_ice_peer_ctx_pair_candidates(nr_ice_peer_ctx *pctx)
+ {
+ nr_ice_media_stream *stream;
+ int r,_status;
+
+ if(pctx->peer_lite && !pctx->controlling) {
+ if(pctx->ctx->flags & NR_ICE_CTX_FLAGS_LITE){
+ r_log(LOG_ICE,LOG_ERR,"Both sides are ICE-Lite");
+ ABORT(R_BAD_DATA);
+ }
+ nr_ice_peer_ctx_switch_controlling_role(pctx);
+ }
+
+ r_log(LOG_ICE,LOG_DEBUG,"ICE(%s): peer (%s) pairing candidates",pctx->ctx->label,pctx->label);
+
+ if(STAILQ_EMPTY(&pctx->peer_streams)) {
+ r_log(LOG_ICE,LOG_ERR,"ICE(%s): peer (%s) received no media stream attributes",pctx->ctx->label,pctx->label);
+ ABORT(R_FAILED);
+ }
+
+ /* Set this first; if we fail partway through, we do not want to end
+ * up in UNPAIRED after creating some pairs. */
+ pctx->state = NR_ICE_PEER_STATE_PAIRED;
+
+ stream=STAILQ_FIRST(&pctx->peer_streams);
+ while(stream){
+ if(!stream->local_stream->obsolete) {
+ if(r=nr_ice_media_stream_pair_candidates(pctx, stream->local_stream,
+ stream))
+ ABORT(r);
+ }
+
+ stream=STAILQ_NEXT(stream,entry);
+ }
+
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+
+int nr_ice_peer_ctx_pair_new_trickle_candidate(nr_ice_ctx *ctx, nr_ice_peer_ctx *pctx, nr_ice_candidate *cand)
+ {
+ int r, _status;
+ nr_ice_media_stream *pstream;
+
+ r_log(LOG_ICE,LOG_ERR,"ICE(%s): peer (%s) pairing local trickle ICE candidate %s",pctx->ctx->label,pctx->label,cand->label);
+ if ((r = nr_ice_peer_ctx_find_pstream(pctx, cand->stream, &pstream)))
+ ABORT(r);
+
+ /* Start the remote trickle grace timeout if it hasn't been started
+ already. */
+ if (!pctx->trickle_grace_period_timer) {
+ nr_ice_peer_ctx_start_trickle_timer(pctx);
+ }
+
+ if ((r = nr_ice_media_stream_pair_new_trickle_candidate(pctx, pstream, cand)))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ return _status;
+ }
+
+int nr_ice_peer_ctx_disable_component(nr_ice_peer_ctx *pctx, nr_ice_media_stream *lstream, int component_id)
+ {
+ int r, _status;
+ nr_ice_media_stream *pstream;
+ nr_ice_component *component;
+
+ if ((r=nr_ice_peer_ctx_find_pstream(pctx, lstream, &pstream)))
+ ABORT(r);
+
+ /* We shouldn't be calling this after we have started pairing */
+ if (pstream->ice_state != NR_ICE_MEDIA_STREAM_UNPAIRED)
+ ABORT(R_FAILED);
+
+ if ((r=nr_ice_media_stream_find_component(pstream, component_id,
+ &component)))
+ ABORT(r);
+
+ component->state = NR_ICE_COMPONENT_DISABLED;
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+ void nr_ice_peer_ctx_destroy(nr_ice_peer_ctx** pctxp) {
+ if (!pctxp || !*pctxp) return;
+
+ nr_ice_peer_ctx* pctx = *pctxp;
+ nr_ice_media_stream *str1,*str2;
+
+ /* Stop calling the handler */
+ pctx->handler = 0;
+
+ NR_async_timer_cancel(pctx->connected_cb_timer);
+ RFREE(pctx->label);
+
+ STAILQ_FOREACH_SAFE(str1, &pctx->peer_streams, entry, str2){
+ STAILQ_REMOVE(&pctx->peer_streams,str1,nr_ice_media_stream_,entry);
+ nr_ice_media_stream_destroy(&str1);
+ }
+ assert(pctx->ctx);
+ if (pctx->ctx)
+ STAILQ_REMOVE(&pctx->ctx->peers, pctx, nr_ice_peer_ctx_, entry);
+
+ if(pctx->trickle_grace_period_timer) {
+ NR_async_timer_cancel(pctx->trickle_grace_period_timer);
+ pctx->trickle_grace_period_timer=0;
+ }
+
+ RFREE(pctx);
+
+ *pctxp=0;
+ }
+
+/* Start the checks for the first media stream (S 5.7)
+ The rest remain FROZEN */
+int nr_ice_peer_ctx_start_checks(nr_ice_peer_ctx *pctx)
+ {
+ return nr_ice_peer_ctx_start_checks2(pctx, 0);
+ }
+
+/* Start checks for some media stream.
+
+ If allow_non_first == 0, then we only look at the first stream,
+ which is 5245-complaint.
+
+ If allow_non_first == 1 then we find the first non-empty stream
+ This is not compliant with RFC 5245 but is necessary to make trickle ICE
+ work plausibly
+*/
+int nr_ice_peer_ctx_start_checks2(nr_ice_peer_ctx *pctx, int allow_non_first)
+ {
+ int r,_status;
+ nr_ice_media_stream *stream;
+ int started = 0;
+
+ /* Ensure that grace period timer is running. We might cancel this if we
+ * didn't actually start any pairs. */
+ nr_ice_peer_ctx_start_trickle_timer(pctx);
+
+ /* Might have added some streams */
+ pctx->reported_connected = 0;
+ NR_async_timer_cancel(pctx->connected_cb_timer);
+ pctx->connected_cb_timer = 0;
+ pctx->checks_started = 0;
+
+ nr_ice_peer_ctx_check_if_connected(pctx);
+
+ if (pctx->reported_connected) {
+ r_log(LOG_ICE,LOG_ERR,"ICE(%s): peer (%s) in %s all streams were done",pctx->ctx->label,pctx->label,__FUNCTION__);
+ return (0);
+ }
+
+ stream=STAILQ_FIRST(&pctx->peer_streams);
+ if(!stream)
+ ABORT(R_FAILED);
+
+ while (stream) {
+ if(!stream->local_stream->obsolete) {
+ assert(stream->ice_state != NR_ICE_MEDIA_STREAM_UNPAIRED);
+
+ if (stream->ice_state == NR_ICE_MEDIA_STREAM_CHECKS_FROZEN) {
+ if(!TAILQ_EMPTY(&stream->check_list))
+ break;
+
+ if(!allow_non_first){
+ /* This test applies if:
+
+ 1. allow_non_first is 0 (i.e., non-trickle ICE)
+ 2. the first stream has an empty check list.
+
+ But in the non-trickle ICE case, the other side should have provided
+ some candidates or ICE is pretty much not going to work and we're
+ just going to fail. Hence R_FAILED as opposed to R_NOT_FOUND and
+ immediate termination here.
+ */
+ r_log(LOG_ICE,LOG_ERR,"ICE(%s): peer (%s) first stream has empty check list",pctx->ctx->label,pctx->label);
+ ABORT(R_FAILED);
+ }
+ }
+ }
+
+ stream=STAILQ_NEXT(stream, entry);
+ }
+
+ if (!stream) {
+ /*
+ We fail above if we aren't doing trickle, and this is not all that
+ unusual in the trickle case.
+ */
+ r_log(LOG_ICE,LOG_NOTICE,"ICE(%s): peer (%s) no streams with non-empty check lists",pctx->ctx->label,pctx->label);
+ }
+ else if (stream->ice_state == NR_ICE_MEDIA_STREAM_CHECKS_FROZEN) {
+ if(r=nr_ice_media_stream_unfreeze_pairs(pctx,stream))
+ ABORT(r);
+ if(r=nr_ice_media_stream_start_checks(pctx,stream))
+ ABORT(r);
+ ++started;
+ }
+
+ stream=STAILQ_FIRST(&pctx->peer_streams);
+ while (stream) {
+ int serviced = 0;
+ if (r=nr_ice_media_stream_service_pre_answer_requests(pctx, stream->local_stream, stream, &serviced))
+ ABORT(r);
+
+ if (serviced) {
+ ++started;
+ }
+ else {
+ r_log(LOG_ICE,LOG_NOTICE,"ICE(%s): peer (%s) no streams with pre-answer requests",pctx->ctx->label,pctx->label);
+ }
+
+
+ stream=STAILQ_NEXT(stream, entry);
+ }
+
+ if (!started && pctx->ctx->uninitialized_candidates) {
+ r_log(LOG_ICE,LOG_INFO,"ICE(%s): peer (%s) no checks to start, but gathering is not done yet, cancelling grace period timer",pctx->ctx->label,pctx->label);
+ /* Never mind on the grace period timer */
+ NR_async_timer_cancel(pctx->trickle_grace_period_timer);
+ pctx->trickle_grace_period_timer=0;
+ ABORT(R_NOT_FOUND);
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+void nr_ice_peer_ctx_stream_started_checks(nr_ice_peer_ctx *pctx, nr_ice_media_stream *stream)
+ {
+ if (!pctx->checks_started) {
+ r_log(LOG_ICE,LOG_NOTICE,"ICE(%s): peer (%s) is now checking",pctx->ctx->label,pctx->label);
+ pctx->checks_started = 1;
+ if (pctx->handler && pctx->handler->vtbl->ice_checking) {
+ pctx->handler->vtbl->ice_checking(pctx->handler->obj, pctx);
+ }
+ }
+ }
+
+void nr_ice_peer_ctx_dump_state(nr_ice_peer_ctx *pctx, int log_level)
+ {
+ nr_ice_media_stream *stream;
+
+ r_log(LOG_ICE,log_level,"PEER %s STATE DUMP",pctx->label);
+ r_log(LOG_ICE,log_level,"==========================================");
+ stream=STAILQ_FIRST(&pctx->peer_streams);
+ while(stream){
+ nr_ice_media_stream_dump_state(pctx,stream,log_level);
+ }
+ r_log(LOG_ICE,log_level,"==========================================");
+ }
+
+void nr_ice_peer_ctx_refresh_consent_all_streams(nr_ice_peer_ctx *pctx)
+ {
+ nr_ice_media_stream *str;
+
+ r_log(LOG_ICE,LOG_INFO,"ICE-PEER(%s): refreshing consent on all streams",pctx->label);
+
+ str=STAILQ_FIRST(&pctx->peer_streams);
+ while(str) {
+ nr_ice_media_stream_refresh_consent_all(str);
+ str=STAILQ_NEXT(str,entry);
+ }
+ }
+
+void nr_ice_peer_ctx_disconnected(nr_ice_peer_ctx *pctx)
+ {
+ if (pctx->reported_connected &&
+ pctx->handler &&
+ pctx->handler->vtbl->ice_disconnected) {
+ pctx->handler->vtbl->ice_disconnected(pctx->handler->obj, pctx);
+
+ pctx->reported_connected = 0;
+ }
+ }
+
+void nr_ice_peer_ctx_disconnect_all_streams(nr_ice_peer_ctx *pctx)
+ {
+ nr_ice_media_stream *str;
+
+ r_log(LOG_ICE,LOG_INFO,"ICE-PEER(%s): disconnecting all streams",pctx->label);
+
+ str=STAILQ_FIRST(&pctx->peer_streams);
+ while(str) {
+ nr_ice_media_stream_disconnect_all_components(str);
+
+ /* The first stream to be disconnected will cause the peer ctx to signal
+ the disconnect up. */
+ nr_ice_media_stream_set_disconnected(str, NR_ICE_MEDIA_STREAM_DISCONNECTED);
+
+ str=STAILQ_NEXT(str,entry);
+ }
+ }
+
+void nr_ice_peer_ctx_connected(nr_ice_peer_ctx *pctx)
+ {
+ /* Fire the handler callback to say we're done */
+ if (pctx->reported_connected &&
+ pctx->handler &&
+ pctx->handler->vtbl->ice_connected) {
+ pctx->handler->vtbl->ice_connected(pctx->handler->obj, pctx);
+ }
+ }
+
+static void nr_ice_peer_ctx_fire_connected(NR_SOCKET s, int how, void *cb_arg)
+ {
+ nr_ice_peer_ctx *pctx=cb_arg;
+
+ pctx->connected_cb_timer=0;
+
+ nr_ice_peer_ctx_connected(pctx);
+ }
+
+/* Examine all the streams to see if we're
+ maybe miraculously connected */
+void nr_ice_peer_ctx_check_if_connected(nr_ice_peer_ctx *pctx)
+ {
+ nr_ice_media_stream *str;
+ int failed=0;
+ int succeeded=0;
+
+ str=STAILQ_FIRST(&pctx->peer_streams);
+ while(str){
+ if (!str->local_stream->obsolete){
+ if(str->ice_state==NR_ICE_MEDIA_STREAM_CHECKS_CONNECTED){
+ succeeded++;
+ }
+ else if(str->ice_state==NR_ICE_MEDIA_STREAM_CHECKS_FAILED){
+ failed++;
+ }
+ else{
+ break;
+ }
+ }
+ str=STAILQ_NEXT(str,entry);
+ }
+
+ if(str)
+ return; /* Something isn't done */
+
+ /* OK, we're finished, one way or another */
+ r_log(LOG_ICE,LOG_INFO,"ICE-PEER(%s): all checks completed success=%d fail=%d",pctx->label,succeeded,failed);
+
+ /* Make sure grace period timer is cancelled */
+ if(pctx->trickle_grace_period_timer) {
+ r_log(LOG_ICE,LOG_INFO,"ICE(%s): peer (%s) cancelling grace period timer",pctx->ctx->label,pctx->label);
+ NR_async_timer_cancel(pctx->trickle_grace_period_timer);
+ pctx->trickle_grace_period_timer=0;
+ }
+
+ /* Schedule a connected notification for the first connected event.
+ IMPORTANT: This is done in a callback because we expect destructors
+ of various kinds to be fired from here */
+ if (!pctx->reported_connected) {
+ pctx->reported_connected = 1;
+ assert(!pctx->connected_cb_timer);
+ NR_ASYNC_TIMER_SET(0,nr_ice_peer_ctx_fire_connected,pctx,&pctx->connected_cb_timer);
+ }
+ }
+
+
+/* Given a component in the main ICE ctx, find the relevant component in
+ the peer_ctx */
+int nr_ice_peer_ctx_find_component(nr_ice_peer_ctx *pctx, nr_ice_media_stream *str, int component_id, nr_ice_component **compp)
+ {
+ nr_ice_media_stream *pstr;
+ int r,_status;
+
+ pstr=STAILQ_FIRST(&pctx->peer_streams);
+ while(pstr){
+ if(pstr->local_stream==str)
+ break;
+
+ pstr=STAILQ_NEXT(pstr,entry);
+ }
+ if(!pstr)
+ ABORT(R_BAD_ARGS);
+
+ if(r=nr_ice_media_stream_find_component(pstr,component_id,compp))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+/*
+ This packet may be for us.
+
+ 1. Find the matching peer component
+ 2. Examine the packet source address to see if it matches
+ one of the peer candidates.
+ 3. Fire the relevant callback handler if there is a match
+
+ Return 0 if match, R_REJECTED if no match, other errors
+ if we can't even find the component or something like that.
+*/
+
+int nr_ice_peer_ctx_deliver_packet_maybe(nr_ice_peer_ctx *pctx, nr_ice_component *comp, nr_transport_addr *source_addr, UCHAR *data, int len)
+ {
+ nr_ice_component *peer_comp;
+ nr_ice_candidate *cand;
+ int r,_status;
+
+ if(r=nr_ice_peer_ctx_find_component(pctx, comp->stream, comp->component_id,
+ &peer_comp))
+ ABORT(r);
+
+ /* OK, we've found the component, now look for matches */
+ cand=TAILQ_FIRST(&peer_comp->candidates);
+ while(cand){
+ if(!nr_transport_addr_cmp(source_addr,&cand->addr,
+ NR_TRANSPORT_ADDR_CMP_MODE_ALL))
+ break;
+
+ cand=TAILQ_NEXT(cand,entry_comp);
+ }
+
+ if(!cand)
+ ABORT(R_REJECTED);
+
+ // accumulate the received bytes for the active candidate pair
+ if (peer_comp->active) {
+ peer_comp->active->bytes_recvd += len;
+ gettimeofday(&peer_comp->active->last_recvd, 0);
+ }
+
+ /* OK, there's a match. Call the handler */
+
+ if (pctx->handler) {
+ r_log(LOG_ICE,LOG_DEBUG,"ICE-PEER(%s): Delivering data", pctx->label);
+
+ pctx->handler->vtbl->msg_recvd(pctx->handler->obj,
+ pctx,comp->stream,comp->component_id,data,len);
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+void nr_ice_peer_ctx_switch_controlling_role(nr_ice_peer_ctx *pctx)
+ {
+ int controlling = !(pctx->controlling);
+ if(pctx->controlling_conflict_resolved) {
+ r_log(LOG_ICE,LOG_WARNING,"ICE(%s): peer (%s) %s called more than once; "
+ "this probably means the peer is confused. Not switching roles.",
+ pctx->ctx->label,pctx->label,__FUNCTION__);
+ return;
+ }
+
+ r_log(LOG_ICE,LOG_INFO,"ICE-PEER(%s): detected "
+ "role conflict. Switching to %s",
+ pctx->label,
+ controlling ? "controlling" : "controlled");
+
+ pctx->controlling = controlling;
+ pctx->controlling_conflict_resolved = 1;
+
+ if(pctx->state == NR_ICE_PEER_STATE_PAIRED) {
+ /* We have formed candidate pairs. We need to inform them. */
+ nr_ice_media_stream *pstream=STAILQ_FIRST(&pctx->peer_streams);
+ while(pstream) {
+ nr_ice_media_stream_role_change(pstream);
+ pstream = STAILQ_NEXT(pstream, entry);
+ }
+ }
+ }
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_peer_ctx.h b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_peer_ctx.h
new file mode 100644
index 0000000000..ec73d96a03
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_peer_ctx.h
@@ -0,0 +1,101 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+
+#ifndef _ice_peer_ctx_h
+#define _ice_peer_ctx_h
+#ifdef __cplusplus
+using namespace std;
+extern "C" {
+#endif /* __cplusplus */
+
+struct nr_ice_peer_ctx_ {
+ int state;
+#define NR_ICE_PEER_STATE_UNPAIRED 1
+#define NR_ICE_PEER_STATE_PAIRED 2
+
+ char *label;
+ nr_ice_ctx *ctx;
+ nr_ice_handler *handler;
+
+ UCHAR controlling; /* 1 for controlling, 0 for controlled */
+ UCHAR controlling_conflict_resolved;
+ UINT8 tiebreaker;
+
+ int peer_lite;
+ int peer_ice_mismatch;
+
+ nr_ice_media_stream_head peer_streams;
+ int active_streams;
+ int waiting_pairs;
+ UCHAR checks_started;
+
+ void *connected_cb_timer;
+ UCHAR reported_connected;
+ void *trickle_grace_period_timer;
+
+ STAILQ_ENTRY(nr_ice_peer_ctx_) entry;
+};
+
+typedef STAILQ_HEAD(nr_ice_peer_ctx_head_, nr_ice_peer_ctx_) nr_ice_peer_ctx_head;
+
+int nr_ice_peer_ctx_create(nr_ice_ctx *ctx, nr_ice_handler *handler,char *label, nr_ice_peer_ctx **pctxp);
+void nr_ice_peer_ctx_destroy(nr_ice_peer_ctx** pctxp);
+int nr_ice_peer_ctx_parse_stream_attributes(nr_ice_peer_ctx *pctx, nr_ice_media_stream *stream, char **attrs, int attr_ct);
+int nr_ice_peer_ctx_find_pstream(nr_ice_peer_ctx *pctx, nr_ice_media_stream *stream, nr_ice_media_stream **pstreamp);
+int nr_ice_peer_ctx_remove_pstream(nr_ice_peer_ctx *pctx, nr_ice_media_stream **pstreamp);
+int nr_ice_peer_ctx_parse_trickle_candidate(nr_ice_peer_ctx *pctx, nr_ice_media_stream *stream, char *cand, const char *mdns_addr);
+
+int nr_ice_peer_ctx_pair_candidates(nr_ice_peer_ctx *pctx);
+int nr_ice_peer_ctx_parse_global_attributes(nr_ice_peer_ctx *pctx, char **attrs, int attr_ct);
+int nr_ice_peer_ctx_start_checks(nr_ice_peer_ctx *pctx);
+int nr_ice_peer_ctx_start_checks2(nr_ice_peer_ctx *pctx, int allow_non_first);
+void nr_ice_peer_ctx_stream_started_checks(nr_ice_peer_ctx *pctx, nr_ice_media_stream *stream);
+void nr_ice_peer_ctx_refresh_consent_all_streams(nr_ice_peer_ctx *pctx);
+void nr_ice_peer_ctx_disconnect_all_streams(nr_ice_peer_ctx *pctx);
+void nr_ice_peer_ctx_disconnected(nr_ice_peer_ctx *pctx);
+void nr_ice_peer_ctx_connected(nr_ice_peer_ctx *pctx);
+void nr_ice_peer_ctx_dump_state(nr_ice_peer_ctx *pctx, int log_level);
+int nr_ice_peer_ctx_log_state(nr_ice_peer_ctx *pctx);
+void nr_ice_peer_ctx_check_if_connected(nr_ice_peer_ctx *pctx);
+int nr_ice_peer_ctx_find_component(nr_ice_peer_ctx *pctx, nr_ice_media_stream *str, int component_id, nr_ice_component **compp);
+int nr_ice_peer_ctx_deliver_packet_maybe(nr_ice_peer_ctx *pctx, nr_ice_component *comp, nr_transport_addr *source_addr, UCHAR *data, int len);
+int nr_ice_peer_ctx_disable_component(nr_ice_peer_ctx *pctx, nr_ice_media_stream *lstream, int component_id);
+int nr_ice_peer_ctx_pair_new_trickle_candidate(nr_ice_ctx *ctx, nr_ice_peer_ctx *pctx, nr_ice_candidate *cand);
+void nr_ice_peer_ctx_switch_controlling_role(nr_ice_peer_ctx *pctx);
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_reg.h b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_reg.h
new file mode 100644
index 0000000000..3acc02360a
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_reg.h
@@ -0,0 +1,81 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+
+#ifndef _ice_reg_h
+#define _ice_reg_h
+#ifdef __cplusplus
+using namespace std;
+extern "C" {
+#endif /* __cplusplus */
+
+#define NR_ICE_REG_PREF_TYPE_HOST "ice.pref.type.host"
+#define NR_ICE_REG_PREF_TYPE_RELAYED "ice.pref.type.relayed"
+#define NR_ICE_REG_PREF_TYPE_SRV_RFLX "ice.pref.type.srv_rflx"
+#define NR_ICE_REG_PREF_TYPE_PEER_RFLX "ice.pref.type.peer_rflx"
+#define NR_ICE_REG_PREF_TYPE_HOST_TCP "ice.pref.type.host_tcp"
+#define NR_ICE_REG_PREF_TYPE_RELAYED_TCP "ice.pref.type.relayed_tcp"
+#define NR_ICE_REG_PREF_TYPE_SRV_RFLX_TCP "ice.pref.type.srv_rflx_tcp"
+#define NR_ICE_REG_PREF_TYPE_PEER_RFLX_TCP "ice.pref.type.peer_rflx_tcp"
+
+#define NR_ICE_REG_PREF_INTERFACE_PRFX "ice.pref.interface"
+#define NR_ICE_REG_SUPPRESS_INTERFACE_PRFX "ice.suppress.interface"
+
+#define NR_ICE_REG_STUN_SRV_PRFX "ice.stun.server"
+#define NR_ICE_REG_STUN_SRV_ADDR "addr"
+#define NR_ICE_REG_STUN_SRV_PORT "port"
+
+#define NR_ICE_REG_TURN_SRV_PRFX "ice.turn.server"
+#define NR_ICE_REG_TURN_SRV_ADDR "addr"
+#define NR_ICE_REG_TURN_SRV_PORT "port"
+#define NR_ICE_REG_TURN_SRV_BANDWIDTH "bandwidth"
+#define NR_ICE_REG_TURN_SRV_LIFETIME "lifetime"
+#define NR_ICE_REG_TURN_SRV_USERNAME "username"
+#define NR_ICE_REG_TURN_SRV_PASSWORD "password"
+
+#define NR_ICE_REG_ICE_TCP_DISABLE "ice.tcp.disable"
+#define NR_ICE_REG_ICE_TCP_SO_SOCK_COUNT "ice.tcp.so_sock_count"
+#define NR_ICE_REG_ICE_TCP_LISTEN_BACKLOG "ice.tcp.listen_backlog"
+
+#define NR_ICE_REG_KEEPALIVE_TIMER "ice.keepalive_timer"
+
+#define NR_ICE_REG_TRICKLE_GRACE_PERIOD "ice.trickle_grace_period"
+#define NR_ICE_REG_PREF_FORCE_INTERFACE_NAME "ice.forced_interface_name"
+#define NR_ICE_REG_USE_NR_RESOLVER_FOR_TCP "ice.tcp.use_nr_resolver"
+#define NR_ICE_REG_USE_NR_RESOLVER_FOR_UDP "ice.udp.use_nr_resolver"
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_socket.c b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_socket.c
new file mode 100644
index 0000000000..a6c8513300
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_socket.c
@@ -0,0 +1,404 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <assert.h>
+#include <string.h>
+#include "nr_api.h"
+#include "ice_ctx.h"
+#include "stun.h"
+#include "nr_socket_buffered_stun.h"
+#include "nr_socket_multi_tcp.h"
+
+static void nr_ice_socket_readable_cb(NR_SOCKET s, int how, void *cb_arg)
+ {
+ int r;
+ nr_ice_stun_ctx *sc1,*sc2;
+ nr_ice_socket *sock=cb_arg;
+ UCHAR buf[9216];
+ char string[256];
+ nr_transport_addr addr;
+ int len;
+ size_t len_s;
+ int is_stun;
+ int is_req;
+ int is_ind;
+ int processed_indication=0;
+
+ nr_socket *stun_srv_sock=sock->sock;
+
+ r_log(LOG_ICE,LOG_DEBUG,"ICE(%s): Socket ready to read",sock->ctx->label);
+
+ if(r=nr_socket_recvfrom(sock->sock,buf,sizeof(buf),&len_s,0,&addr)){
+ if (r != R_WOULDBLOCK && (sock->type != NR_ICE_SOCKET_TYPE_DGRAM)) {
+ /* Report this error upward. Bug 946423 */
+ r_log(LOG_ICE,LOG_ERR,"ICE(%s): Error %d on reliable socket(%p). Abandoning.",sock->ctx->label, r, s);
+ nr_ice_socket_failed(sock);
+ return;
+ }
+ }
+
+ if (sock->type != NR_ICE_SOCKET_TYPE_STREAM_TCP) {
+ r_log(LOG_ICE,LOG_DEBUG,"ICE(%s): rearming",sock->ctx->label);
+ NR_ASYNC_WAIT(s,how,nr_ice_socket_readable_cb,cb_arg);
+ }
+
+ if (r) {
+ return;
+ }
+
+ /* Deal with the fact that sizeof(int) and sizeof(size_t) may not
+ be the same */
+ if (len_s > (size_t)INT_MAX)
+ return;
+
+ len = (int)len_s;
+
+#ifdef USE_TURN
+ re_process:
+#endif /* USE_TURN */
+ r_log(LOG_ICE,LOG_DEBUG,"ICE(%s): Read %d bytes %sfrom %s",sock->ctx->label,len,(processed_indication ? "relayed " : ""),addr.as_string);
+
+ /* First question: is this STUN or not? */
+ is_stun=nr_is_stun_message(buf,len);
+
+ if(is_stun){
+ is_req=nr_is_stun_request_message(buf,len);
+ is_ind=is_req?0:nr_is_stun_indication_message(buf,len);
+
+ snprintf(string, sizeof(string)-1, "ICE(%s): Message is STUN (%s)",sock->ctx->label,
+ is_req ? "request" : (is_ind ? "indication" : "other"));
+ r_dump(NR_LOG_STUN, LOG_DEBUG, string, (char*)buf, len);
+
+
+ /* We need to offer it to all of our stun contexts
+ to see who bites */
+ sc1=TAILQ_FIRST(&sock->stun_ctxs);
+ while(sc1){
+ sc2=TAILQ_NEXT(sc1,entry);
+
+ r=-1;
+ switch(sc1->type){
+ /* This has been deleted, prune... */
+ case NR_ICE_STUN_NONE:
+ TAILQ_REMOVE(&sock->stun_ctxs,sc1,entry);
+ RFREE(sc1);
+ break;
+
+ case NR_ICE_STUN_CLIENT:
+ if(!(is_req||is_ind)){
+ r=nr_stun_client_process_response(sc1->u.client,buf,len,&addr);
+ }
+ break;
+
+ case NR_ICE_STUN_SERVER:
+ if(is_req){
+ r=nr_stun_server_process_request(sc1->u.server,stun_srv_sock,(char *)buf,len,&addr,NR_STUN_AUTH_RULE_SHORT_TERM);
+ }
+ break;
+#ifdef USE_TURN
+ case NR_ICE_TURN_CLIENT:
+ /* data indications are ok, so don't ignore those */
+ /* Check that this is from the right TURN server address. Else
+ skip */
+ if (nr_transport_addr_cmp(
+ &sc1->u.turn_client.turn_client->turn_server_addr,
+ &addr, NR_TRANSPORT_ADDR_CMP_MODE_ALL))
+ break;
+
+ if(!is_req){
+ if(!is_ind)
+ r=nr_turn_client_process_response(sc1->u.turn_client.turn_client,buf,len,&addr);
+ else{
+ nr_transport_addr n_addr;
+ size_t n_len;
+
+ if (processed_indication) {
+ /* Don't allow recursively wrapped indications */
+ r_log(LOG_ICE, LOG_WARNING,
+ "ICE(%s): discarding recursively wrapped indication",
+ sock->ctx->label);
+ break;
+ }
+ /* This is a bit of a hack. If it's a data indication, strip
+ off the TURN framing and re-enter. This works because
+ all STUN processing is on the same physical socket.
+ We don't care about other kinds of indication */
+ r=nr_turn_client_parse_data_indication(
+ sc1->u.turn_client.turn_client, &addr,
+ buf, len, buf, &n_len, len, &n_addr);
+ if(!r){
+ r_log(LOG_ICE,LOG_DEBUG,"Unwrapped a data indication.");
+ len=n_len;
+ nr_transport_addr_copy(&addr,&n_addr);
+ stun_srv_sock=sc1->u.turn_client.turn_sock;
+ processed_indication=1;
+ goto re_process;
+ }
+ }
+ }
+ break;
+#endif /* USE_TURN */
+
+ default:
+ assert(0); /* Can't happen */
+ return;
+ }
+ if(!r) {
+ break;
+ }
+
+ sc1=sc2;
+ }
+ if(!sc1){
+ if (nr_ice_ctx_is_known_id(sock->ctx,((nr_stun_message_header*)buf)->id.octet))
+ r_log(LOG_ICE,LOG_DEBUG,"ICE(%s): Message is a retransmit",sock->ctx->label);
+ else
+ r_log(LOG_ICE,LOG_NOTICE,"ICE(%s): Message does not correspond to any registered stun ctx",sock->ctx->label);
+ }
+ }
+ else{
+ r_log(LOG_ICE,LOG_DEBUG,"ICE(%s): Message is not STUN",sock->ctx->label);
+
+ nr_ice_ctx_deliver_packet(sock->ctx, sock->component, &addr, buf, len);
+ }
+
+ return;
+ }
+
+int nr_ice_socket_create(nr_ice_ctx *ctx,nr_ice_component *comp, nr_socket *nsock, int type, nr_ice_socket **sockp)
+ {
+ nr_ice_socket *sock=0;
+ NR_SOCKET fd;
+ nr_transport_addr addr;
+ int r,_status;
+
+ if(!(sock=RCALLOC(sizeof(nr_ice_socket))))
+ ABORT(R_NO_MEMORY);
+
+ sock->sock=nsock;
+ sock->ctx=ctx;
+ sock->component=comp;
+
+ if(r=nr_socket_getaddr(nsock, &addr))
+ ABORT(r);
+
+ if (type == NR_ICE_SOCKET_TYPE_DGRAM) {
+ assert(addr.protocol == IPPROTO_UDP);
+ }
+ else {
+ assert(addr.protocol == IPPROTO_TCP);
+ }
+ sock->type=type;
+
+ TAILQ_INIT(&sock->candidates);
+ TAILQ_INIT(&sock->stun_ctxs);
+
+ if (sock->type == NR_ICE_SOCKET_TYPE_DGRAM){
+ if((r=nr_socket_getfd(nsock,&fd)))
+ ABORT(r);
+ NR_ASYNC_WAIT(fd,NR_ASYNC_WAIT_READ,nr_ice_socket_readable_cb,sock);
+ }
+ else if (sock->type == NR_ICE_SOCKET_TYPE_STREAM_TURN) {
+ /* some OS's (e.g. Linux) don't like to see un-connected TCP sockets in
+ * the poll socket set. */
+ nr_socket_buffered_stun_set_readable_cb(nsock,nr_ice_socket_readable_cb,sock);
+ }
+ else if (sock->type == NR_ICE_SOCKET_TYPE_STREAM_TCP) {
+ /* in this case we can't hook up using NR_ASYNC_WAIT, because nr_socket_multi_tcp
+ consists of multiple nr_sockets and file descriptors. */
+ if((r=nr_socket_multi_tcp_set_readable_cb(nsock,nr_ice_socket_readable_cb,sock)))
+ ABORT(r);
+ }
+
+ *sockp=sock;
+
+ _status=0;
+ abort:
+ if(_status) RFREE(sock);
+ return(_status);
+ }
+
+
+int nr_ice_socket_destroy(nr_ice_socket **isockp)
+ {
+ nr_ice_stun_ctx *s1,*s2;
+ nr_ice_socket *isock;
+
+ if(!isockp || !*isockp)
+ return(0);
+
+ isock=*isockp;
+ *isockp=0;
+
+ /* Close the socket */
+ nr_ice_socket_close(isock);
+
+ /* The STUN server */
+ nr_stun_server_ctx_destroy(&isock->stun_server);
+
+ /* Now clean up the STUN ctxs */
+ TAILQ_FOREACH_SAFE(s1, &isock->stun_ctxs, entry, s2){
+ TAILQ_REMOVE(&isock->stun_ctxs, s1, entry);
+ RFREE(s1);
+ }
+
+ RFREE(isock);
+
+ return(0);
+ }
+
+int nr_ice_socket_close(nr_ice_socket *isock)
+ {
+#ifdef NR_SOCKET_IS_VOID_PTR
+ NR_SOCKET fd=NULL;
+ NR_SOCKET no_socket = NULL;
+#else
+ NR_SOCKET fd=-1;
+ NR_SOCKET no_socket = -1;
+#endif
+
+ if (!isock||!isock->sock)
+ return(0);
+
+ if (isock->type != NR_ICE_SOCKET_TYPE_STREAM_TCP){
+ nr_socket_getfd(isock->sock,&fd);
+ assert(isock->sock!=0);
+ if(fd != no_socket){
+ NR_ASYNC_CANCEL(fd,NR_ASYNC_WAIT_READ);
+ NR_ASYNC_CANCEL(fd,NR_ASYNC_WAIT_WRITE);
+ }
+ }
+ nr_socket_destroy(&isock->sock);
+
+ return(0);
+ }
+
+int nr_ice_socket_register_stun_client(nr_ice_socket *sock, nr_stun_client_ctx *srv,void **handle)
+ {
+ nr_ice_stun_ctx *sc=0;
+ int _status;
+
+ if(!(sc=RCALLOC(sizeof(nr_ice_stun_ctx))))
+ ABORT(R_NO_MEMORY);
+
+ sc->type=NR_ICE_STUN_CLIENT;
+ sc->u.client=srv;
+
+ TAILQ_INSERT_TAIL(&sock->stun_ctxs,sc,entry);
+
+ *handle=sc;
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_ice_socket_register_stun_server(nr_ice_socket *sock, nr_stun_server_ctx *srv,void **handle)
+ {
+ nr_ice_stun_ctx *sc=0;
+ int _status;
+
+ if(!(sc=RCALLOC(sizeof(nr_ice_stun_ctx))))
+ ABORT(R_NO_MEMORY);
+
+ sc->type=NR_ICE_STUN_SERVER;
+ sc->u.server=srv;
+
+ TAILQ_INSERT_TAIL(&sock->stun_ctxs,sc,entry);
+
+ *handle=sc;
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_ice_socket_register_turn_client(nr_ice_socket *sock, nr_turn_client_ctx *srv,
+ nr_socket *turn_socket, void **handle)
+ {
+ nr_ice_stun_ctx *sc=0;
+ int _status;
+
+ if(!(sc=RCALLOC(sizeof(nr_ice_stun_ctx))))
+ ABORT(R_NO_MEMORY);
+
+ sc->type=NR_ICE_TURN_CLIENT;
+ sc->u.turn_client.turn_client=srv;
+ sc->u.turn_client.turn_sock=turn_socket;
+
+ TAILQ_INSERT_TAIL(&sock->stun_ctxs,sc,entry);
+
+ *handle=sc;
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+/* Just mark it deregistered. Don't delete it now because it's not safe
+ in the CB, which is where this is likely to be called */
+int nr_ice_socket_deregister(nr_ice_socket *sock, void *handle)
+ {
+ nr_ice_stun_ctx *sc=handle;
+
+ if(!sc)
+ return(0);
+
+ sc->type=NR_ICE_STUN_NONE;
+
+ return(0);
+ }
+
+void nr_ice_socket_failed(nr_ice_socket *sock)
+ {
+ nr_ice_stun_ctx *s1,*s2;
+ TAILQ_FOREACH_SAFE(s1, &sock->stun_ctxs, entry, s2){
+ switch (s1->type) {
+ case NR_ICE_STUN_NONE:
+ break;
+ case NR_ICE_STUN_CLIENT:
+ nr_stun_client_failed(s1->u.client);
+ break;
+ case NR_ICE_STUN_SERVER:
+ /* Nothing to do here? */
+ break;
+#ifdef USE_TURN
+ case NR_ICE_TURN_CLIENT:
+ nr_turn_client_failed(s1->u.turn_client.turn_client);
+ break;
+#endif
+ default:
+ assert(0);
+ }
+ }
+ }
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_socket.h b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_socket.h
new file mode 100644
index 0000000000..9b73a2690e
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/ice/ice_socket.h
@@ -0,0 +1,98 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+
+#ifndef _ice_socket_h
+#define _ice_socket_h
+#ifdef __cplusplus
+using namespace std;
+extern "C" {
+#endif /* __cplusplus */
+
+typedef struct nr_ice_stun_ctx_ {
+ int type;
+#define NR_ICE_STUN_NONE 0 /* Deregistered */
+#define NR_ICE_STUN_CLIENT 1
+#define NR_ICE_STUN_SERVER 2
+#define NR_ICE_TURN_CLIENT 3
+
+ union {
+ nr_stun_client_ctx *client;
+ nr_stun_server_ctx *server;
+ struct {
+ nr_turn_client_ctx *turn_client;
+ nr_socket *turn_sock; /* The nr_socket_turn wrapped around
+ turn_client */
+ } turn_client;
+ } u;
+
+ TAILQ_ENTRY(nr_ice_stun_ctx_) entry;
+} nr_ice_stun_ctx;
+
+
+typedef struct nr_ice_socket_ {
+ int type;
+#define NR_ICE_SOCKET_TYPE_DGRAM 1
+#define NR_ICE_SOCKET_TYPE_STREAM_TURN 2
+#define NR_ICE_SOCKET_TYPE_STREAM_TCP 3
+
+ nr_socket *sock;
+ nr_ice_ctx *ctx;
+
+ nr_ice_candidate_head candidates;
+ nr_ice_component *component;
+
+ TAILQ_HEAD(nr_ice_stun_ctx_head_,nr_ice_stun_ctx_) stun_ctxs;
+
+ nr_stun_server_ctx *stun_server;
+ void *stun_server_handle;
+
+ STAILQ_ENTRY(nr_ice_socket_) entry;
+} nr_ice_socket;
+
+typedef STAILQ_HEAD(nr_ice_socket_head_,nr_ice_socket_) nr_ice_socket_head;
+
+int nr_ice_socket_create(struct nr_ice_ctx_ *ctx, struct nr_ice_component_ *comp, nr_socket *nsock, int type, nr_ice_socket **sockp);
+int nr_ice_socket_destroy(nr_ice_socket **isock);
+int nr_ice_socket_close(nr_ice_socket *isock);
+int nr_ice_socket_register_stun_client(nr_ice_socket *sock, nr_stun_client_ctx *srv,void **handle);
+int nr_ice_socket_register_stun_server(nr_ice_socket *sock, nr_stun_server_ctx *srv,void **handle);
+int nr_ice_socket_register_turn_client(nr_ice_socket *sock, nr_turn_client_ctx *srv,nr_socket *turn_socket, void **handle);
+int nr_ice_socket_deregister(nr_ice_socket *sock, void *handle);
+void nr_ice_socket_failed(nr_ice_socket *sock);
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/net/local_addr.c b/dom/media/webrtc/transport/third_party/nICEr/src/net/local_addr.c
new file mode 100644
index 0000000000..a0896d5e9d
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/net/local_addr.c
@@ -0,0 +1,70 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+Copyright (c) 2013, Mozilla
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance, Mozilla nor
+ the names of its contributors may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <nr_api.h>
+#include <string.h>
+#include "local_addr.h"
+
+int nr_local_addr_copy(nr_local_addr *to, nr_local_addr *from)
+ {
+ int r,_status;
+
+ if (r=nr_transport_addr_copy(&(to->addr), &(from->addr))) {
+ ABORT(r);
+ }
+ to->interface = from->interface;
+ to->flags = from->flags;
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_local_addr_fmt_info_string(nr_local_addr *addr, char *buf, int len)
+ {
+ int addr_type = addr->interface.type;
+ const char *vpn = (addr_type & NR_INTERFACE_TYPE_VPN) ? "VPN on " : "";
+
+ const char *type = (addr_type & NR_INTERFACE_TYPE_WIRED) ? "wired" :
+ (addr_type & NR_INTERFACE_TYPE_WIFI) ? "wifi" :
+ (addr_type & NR_INTERFACE_TYPE_MOBILE) ? "mobile" :
+ "unknown";
+
+ snprintf(buf, len, "%s%s, estimated speed: %d kbps %s",
+ vpn, type, addr->interface.estimated_speed,
+ (addr->flags & NR_ADDR_FLAG_TEMPORARY ? "temporary" : ""));
+ buf[len - 1] = '\0';
+ return (0);
+ }
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/net/local_addr.h b/dom/media/webrtc/transport/third_party/nICEr/src/net/local_addr.h
new file mode 100644
index 0000000000..fb963e1115
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/net/local_addr.h
@@ -0,0 +1,62 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+Copyright (c) 2013, Mozilla
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance, Mozilla nor
+ the names of its contributors may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#ifndef _local_addr_h
+#define _local_addr_h
+
+#include "transport_addr.h"
+
+typedef struct nr_interface_ {
+ int type;
+#define NR_INTERFACE_TYPE_UNKNOWN 0
+#define NR_INTERFACE_TYPE_WIRED 1
+#define NR_INTERFACE_TYPE_WIFI 1 << 1
+#define NR_INTERFACE_TYPE_MOBILE 1 << 2
+#define NR_INTERFACE_TYPE_VPN 1 << 3
+#define NR_INTERFACE_TYPE_TEREDO 1 << 4
+ int estimated_speed; /* Speed in kbps */
+} nr_interface;
+
+typedef struct nr_local_addr_ {
+ nr_transport_addr addr;
+ nr_interface interface;
+#define NR_ADDR_FLAG_TEMPORARY 0x1
+ int flags;
+} nr_local_addr;
+
+int nr_local_addr_copy(nr_local_addr *to, nr_local_addr *from);
+int nr_local_addr_fmt_info_string(nr_local_addr *addr, char *buf, int len);
+
+#endif
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_interface_prioritizer.c b/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_interface_prioritizer.c
new file mode 100644
index 0000000000..75e5f95467
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_interface_prioritizer.c
@@ -0,0 +1,88 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+Copyright (c) 2013, Mozilla
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance, Mozilla nor
+ the names of its contributors may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include "nr_api.h"
+#include "nr_interface_prioritizer.h"
+#include "transport_addr.h"
+
+int nr_interface_prioritizer_create_int(void *obj,
+ nr_interface_prioritizer_vtbl *vtbl,nr_interface_prioritizer **ifpp)
+ {
+ int _status;
+ nr_interface_prioritizer *ifp=0;
+
+ if(!(ifp=RCALLOC(sizeof(nr_interface_prioritizer))))
+ ABORT(R_NO_MEMORY);
+
+ ifp->obj = obj;
+ ifp->vtbl = vtbl;
+
+ *ifpp = ifp;
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_interface_prioritizer_destroy(nr_interface_prioritizer **ifpp)
+ {
+ nr_interface_prioritizer *ifp;
+
+ if (!ifpp || !*ifpp)
+ return(0);
+
+ ifp = *ifpp;
+ *ifpp = 0;
+ ifp->vtbl->destroy(&ifp->obj);
+ RFREE(ifp);
+ return(0);
+ }
+
+int nr_interface_prioritizer_add_interface(nr_interface_prioritizer *ifp,
+ nr_local_addr *addr)
+ {
+ return ifp->vtbl->add_interface(ifp->obj, addr);
+ }
+
+int nr_interface_prioritizer_get_priority(nr_interface_prioritizer *ifp,
+ const char *key, UCHAR *interface_preference)
+ {
+ return ifp->vtbl->get_priority(ifp->obj,key,interface_preference);
+ }
+
+int nr_interface_prioritizer_sort_preference(nr_interface_prioritizer *ifp)
+ {
+ return ifp->vtbl->sort_preference(ifp->obj);
+ }
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_interface_prioritizer.h b/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_interface_prioritizer.h
new file mode 100644
index 0000000000..c8a36526cc
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_interface_prioritizer.h
@@ -0,0 +1,66 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+Copyright (c) 2013, Mozilla
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance, Mozilla nor
+ the names of its contributors may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#ifndef _nr_interface_prioritizer
+#define _nr_interface_prioritizer
+
+#include "transport_addr.h"
+#include "local_addr.h"
+
+typedef struct nr_interface_prioritizer_vtbl_ {
+ int (*add_interface)(void *obj, nr_local_addr *iface);
+ int (*get_priority)(void *obj, const char *key, UCHAR *pref);
+ int (*sort_preference)(void *obj);
+ int (*destroy)(void **obj);
+} nr_interface_prioritizer_vtbl;
+
+typedef struct nr_interface_prioritizer_ {
+ void *obj;
+ nr_interface_prioritizer_vtbl *vtbl;
+} nr_interface_prioritizer;
+
+int nr_interface_prioritizer_create_int(void *obj, nr_interface_prioritizer_vtbl *vtbl,
+ nr_interface_prioritizer **prioritizer);
+
+int nr_interface_prioritizer_destroy(nr_interface_prioritizer **prioritizer);
+
+int nr_interface_prioritizer_add_interface(nr_interface_prioritizer *prioritizer,
+ nr_local_addr *addr);
+
+int nr_interface_prioritizer_get_priority(nr_interface_prioritizer *prioritizer,
+ const char *key, UCHAR *interface_preference);
+
+int nr_interface_prioritizer_sort_preference(nr_interface_prioritizer *prioritizer);
+#endif
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_resolver.c b/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_resolver.c
new file mode 100644
index 0000000000..4dbf1bbe91
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_resolver.c
@@ -0,0 +1,85 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+Copyright (c) 2013, Mozilla
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance, Mozilla nor
+ the names of its contributors may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <nr_api.h>
+#include "nr_resolver.h"
+
+int nr_resolver_create_int(void *obj, nr_resolver_vtbl *vtbl, nr_resolver **resolverp)
+{
+ int _status;
+ nr_resolver *resolver=0;
+
+ if (!(resolver=RCALLOC(sizeof(nr_resolver))))
+ ABORT(R_NO_MEMORY);
+
+ resolver->obj=obj;
+ resolver->vtbl=vtbl;
+
+ *resolverp=resolver;
+ _status=0;
+abort:
+ return(_status);
+}
+
+int nr_resolver_destroy(nr_resolver **resolverp)
+{
+ nr_resolver *resolver;
+
+ if(!resolverp || !*resolverp)
+ return(0);
+
+ resolver=*resolverp;
+ *resolverp=0;
+
+ resolver->vtbl->destroy(&resolver->obj);
+
+ RFREE(resolver);
+
+ return(0);
+}
+
+int nr_resolver_resolve(nr_resolver *resolver,
+ nr_resolver_resource *resource,
+ int (*cb)(void *cb_arg, nr_transport_addr *addr),
+ void *cb_arg,
+ void **handle)
+{
+ return resolver->vtbl->resolve(resolver->obj, resource, cb, cb_arg, handle);
+}
+
+int nr_resolver_cancel(nr_resolver *resolver, void *handle)
+{
+ return resolver->vtbl->cancel(resolver->obj, handle);
+}
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_resolver.h b/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_resolver.h
new file mode 100644
index 0000000000..376ba9998b
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_resolver.h
@@ -0,0 +1,96 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+Copyright (c) 2013, Mozilla
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance, Mozilla nor
+ the names of its contributors may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#ifndef _nr_resolver_h
+#define _nr_resolver_h
+
+#include "transport_addr.h"
+
+#define NR_RESOLVE_PROTOCOL_STUN 1
+#define NR_RESOLVE_PROTOCOL_TURN 2
+
+typedef struct nr_resolver_resource_ {
+ const char *domain_name;
+ UINT2 port;
+ int stun_turn;
+ UCHAR transport_protocol;
+ UCHAR address_family;
+} nr_resolver_resource;
+
+typedef struct nr_resolver_vtbl_ {
+ int (*destroy)(void **obj);
+ int (*resolve)(void *obj,
+ nr_resolver_resource *resource,
+ int (*cb)(void *cb_arg, nr_transport_addr *addr),
+ void *cb_arg,
+ void **handle);
+ int (*cancel)(void *obj, void *handle);
+} nr_resolver_vtbl;
+
+typedef struct nr_resolver_ {
+ void *obj;
+ nr_resolver_vtbl *vtbl;
+} nr_resolver;
+
+
+/*
+ The convention here is that the provider of this interface
+ must generate a void *obj, and a vtbl and then call
+ nr_resolver_create_int() to allocate the generic wrapper
+ object.
+
+ The vtbl must contain implementations for all the functions
+ listed.
+
+ The nr_resolver_destroy() function (and hence vtbl->destroy)
+ will be called when the consumer of the resolver is done
+ with it. That is the signal that it is safe to clean up
+ the resources associated with obj. No other function will
+ be called afterwards.
+*/
+int nr_resolver_create_int(void *obj, nr_resolver_vtbl *vtbl,
+ nr_resolver **resolverp);
+int nr_resolver_destroy(nr_resolver **resolverp);
+
+/* Request resolution of a domain */
+int nr_resolver_resolve(nr_resolver *resolver,
+ nr_resolver_resource *resource,
+ int (*cb)(void *cb_arg, nr_transport_addr *addr),
+ void *cb_arg,
+ void **handle);
+
+/* Cancel a requested resolution. No callback will fire. */
+int nr_resolver_cancel(nr_resolver *resolver, void *handle);
+#endif
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_socket.c b/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_socket.c
new file mode 100644
index 0000000000..c9867610a6
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_socket.c
@@ -0,0 +1,187 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <assert.h>
+#include <nr_api.h>
+#include "nr_socket.h"
+#include "local_addr.h"
+
+#define CHECK_DEFINED(f) assert(sock->vtbl->f); if (!sock->vtbl->f) ERETURN(R_INTERNAL);
+int nr_socket_create_int(void *obj, nr_socket_vtbl *vtbl, nr_socket **sockp)
+ {
+ int _status;
+ nr_socket *sock=0;
+
+ if(!(sock=RCALLOC(sizeof(nr_socket))))
+ ABORT(R_NO_MEMORY);
+
+ assert(vtbl->version >= 1 && vtbl->version <= 2);
+ if (vtbl->version < 1 || vtbl->version > 2)
+ ABORT(R_INTERNAL);
+
+ sock->obj=obj;
+ sock->vtbl=vtbl;
+
+ *sockp=sock;
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_socket_destroy(nr_socket **sockp)
+ {
+ nr_socket *sock;
+
+ if(!sockp || !*sockp)
+ return(0);
+
+
+ sock=*sockp;
+ *sockp=0;
+
+ CHECK_DEFINED(destroy);
+
+ assert(sock->vtbl);
+ if (sock->vtbl)
+ sock->vtbl->destroy(&sock->obj);
+
+ RFREE(sock);
+
+ return(0);
+ }
+
+int nr_socket_sendto(nr_socket *sock,const void *msg, size_t len, int flags,
+ const nr_transport_addr *addr)
+ {
+ CHECK_DEFINED(ssendto);
+ return sock->vtbl->ssendto(sock->obj,msg,len,flags,addr);
+ }
+
+int nr_socket_recvfrom(nr_socket *sock,void * restrict buf, size_t maxlen,
+ size_t *len, int flags, nr_transport_addr *addr)
+ {
+ CHECK_DEFINED(srecvfrom);
+ return sock->vtbl->srecvfrom(sock->obj, buf, maxlen, len, flags, addr);
+ }
+
+int nr_socket_getfd(nr_socket *sock, NR_SOCKET *fd)
+ {
+ CHECK_DEFINED(getfd);
+ return sock->vtbl->getfd(sock->obj, fd);
+ }
+
+int nr_socket_getaddr(nr_socket *sock, nr_transport_addr *addrp)
+ {
+ CHECK_DEFINED(getaddr);
+ return sock->vtbl->getaddr(sock->obj, addrp);
+ }
+
+int nr_socket_close(nr_socket *sock)
+ {
+ CHECK_DEFINED(close);
+ return sock->vtbl->close(sock->obj);
+ }
+
+int nr_socket_connect(nr_socket *sock, const nr_transport_addr *addr)
+ {
+ CHECK_DEFINED(connect);
+ return sock->vtbl->connect(sock->obj, addr);
+ }
+
+int nr_socket_write(nr_socket *sock,const void *msg, size_t len, size_t *written, int flags)
+ {
+ CHECK_DEFINED(swrite);
+ return sock->vtbl->swrite(sock->obj, msg, len, written);
+ }
+
+
+int nr_socket_read(nr_socket *sock,void * restrict buf, size_t maxlen,
+ size_t *len, int flags)
+ {
+ CHECK_DEFINED(sread);
+ return sock->vtbl->sread(sock->obj, buf, maxlen, len);
+ }
+
+int nr_socket_listen(nr_socket *sock, int backlog)
+ {
+ assert(sock->vtbl->version >=2 );
+ CHECK_DEFINED(listen);
+ return sock->vtbl->listen(sock->obj, backlog);
+ }
+
+int nr_socket_accept(nr_socket *sock, nr_transport_addr *addrp, nr_socket **sockp)
+{
+ assert(sock->vtbl->version >= 2);
+ CHECK_DEFINED(accept);
+ return sock->vtbl->accept(sock->obj, addrp, sockp);
+}
+
+
+int nr_socket_factory_create_int(void *obj,
+ nr_socket_factory_vtbl *vtbl, nr_socket_factory **factorypp)
+ {
+ int _status;
+ nr_socket_factory *factoryp=0;
+
+ if(!(factoryp=RCALLOC(sizeof(nr_socket_factory))))
+ ABORT(R_NO_MEMORY);
+
+ factoryp->obj = obj;
+ factoryp->vtbl = vtbl;
+
+ *factorypp = factoryp;
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_socket_factory_destroy(nr_socket_factory **factorypp)
+ {
+ nr_socket_factory *factoryp;
+
+ if (!factorypp || !*factorypp)
+ return (0);
+
+ factoryp = *factorypp;
+ *factorypp = NULL;
+ factoryp->vtbl->destroy(&factoryp->obj);
+ RFREE(factoryp);
+ return (0);
+ }
+
+int nr_socket_factory_create_socket(nr_socket_factory *factory, nr_transport_addr *addr, nr_socket **sockp)
+ {
+ return factory->vtbl->create_socket(factory->obj, addr, sockp);
+ }
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_socket.h b/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_socket.h
new file mode 100644
index 0000000000..777837f6cc
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_socket.h
@@ -0,0 +1,123 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+
+#ifndef _nr_socket_h
+#define _nr_socket_h
+
+#include <sys/types.h>
+#ifdef WIN32
+#include <winsock2.h>
+#include <ws2tcpip.h>
+#else
+#include <sys/socket.h>
+#endif
+
+#include "transport_addr.h"
+#include "csi_platform.h"
+
+#ifdef __cplusplus
+#define restrict
+#elif defined(WIN32)
+/* Undef before defining to avoid a compiler warning */
+#undef restrict
+#define restrict __restrict
+#endif
+
+typedef enum {
+ TCP_TYPE_NONE=0,
+ TCP_TYPE_ACTIVE,
+ TCP_TYPE_PASSIVE,
+ TCP_TYPE_SO,
+ TCP_TYPE_MAX
+} nr_socket_tcp_type;
+
+typedef struct nr_socket_ nr_socket;
+
+typedef struct nr_socket_vtbl_ {
+ UINT4 version; /* Currently 2 */
+ int (*destroy)(void **obj);
+ int (*ssendto)(void *obj,const void *msg, size_t len, int flags,
+ const nr_transport_addr *addr);
+ int (*srecvfrom)(void *obj,void * restrict buf, size_t maxlen, size_t *len, int flags,
+ nr_transport_addr *addr);
+ int (*getfd)(void *obj, NR_SOCKET *fd);
+ int (*getaddr)(void *obj, nr_transport_addr *addrp);
+ int (*connect)(void *obj, const nr_transport_addr *addr);
+ int (*swrite)(void *obj,const void *msg, size_t len, size_t *written);
+ int (*sread)(void *obj,void * restrict buf, size_t maxlen, size_t *len);
+ int (*close)(void *obj);
+
+ /* available since version 2 */
+ int (*listen)(void *obj, int backlog);
+ int (*accept)(void *obj, nr_transport_addr *addrp, nr_socket **sockp);
+} nr_socket_vtbl;
+
+
+struct nr_socket_ {
+ void *obj;
+ nr_socket_vtbl *vtbl;
+};
+
+typedef struct nr_socket_factory_vtbl_ {
+ int (*create_socket)(void *obj, nr_transport_addr *addr, nr_socket **sockp);
+ int (*destroy)(void **obj);
+} nr_socket_factory_vtbl;
+
+typedef struct nr_socket_factory_ {
+ void *obj;
+ nr_socket_factory_vtbl *vtbl;
+} nr_socket_factory;
+
+/* To be called by constructors */
+int nr_socket_create_int(void *obj, nr_socket_vtbl *vtbl, nr_socket **sockp);
+int nr_socket_destroy(nr_socket **sockp);
+int nr_socket_sendto(nr_socket *sock,const void *msg, size_t len,
+ int flags, const nr_transport_addr *addr);
+int nr_socket_recvfrom(nr_socket *sock,void * restrict buf, size_t maxlen,
+ size_t *len, int flags, nr_transport_addr *addr);
+int nr_socket_getfd(nr_socket *sock, NR_SOCKET *fd);
+int nr_socket_getaddr(nr_socket *sock, nr_transport_addr *addrp);
+int nr_socket_close(nr_socket *sock);
+int nr_socket_connect(nr_socket *sock, const nr_transport_addr *addr);
+int nr_socket_write(nr_socket *sock,const void *msg, size_t len, size_t *written, int flags);
+int nr_socket_read(nr_socket *sock, void * restrict buf, size_t maxlen, size_t *len, int flags);
+int nr_socket_listen(nr_socket *sock, int backlog);
+int nr_socket_accept(nr_socket *sock, nr_transport_addr *addrp, nr_socket **sockp);
+
+int nr_socket_factory_create_int(void *obj, nr_socket_factory_vtbl *vtbl, nr_socket_factory **factorypp);
+int nr_socket_factory_destroy(nr_socket_factory **factoryp);
+int nr_socket_factory_create_socket(nr_socket_factory *factory, nr_transport_addr *addr, nr_socket **sockp);
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_socket_local.h b/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_socket_local.h
new file mode 100644
index 0000000000..a2f813ff66
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_socket_local.h
@@ -0,0 +1,41 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+
+#ifndef _nr_socket_local_h
+#define _nr_socket_local_h
+
+int nr_socket_local_create(void *obj, nr_transport_addr *addr, nr_socket **sockp);
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_socket_multi_tcp.c b/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_socket_multi_tcp.c
new file mode 100644
index 0000000000..9b2489b214
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_socket_multi_tcp.c
@@ -0,0 +1,642 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+Copyright (c) 2014, Mozilla
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <assert.h>
+#include <sys/types.h>
+
+#include "nr_api.h"
+#include "ice_ctx.h"
+#include "nr_socket.h"
+#include "nr_socket_local.h"
+#include "nr_socket_multi_tcp.h"
+#include "nr_socket_buffered_stun.h"
+#include "async_timer.h"
+
+typedef struct nr_tcp_socket_ctx_ {
+ nr_socket * inner;
+ nr_transport_addr remote_addr;
+ int is_framed;
+
+ TAILQ_ENTRY(nr_tcp_socket_ctx_) entry;
+} nr_tcp_socket_ctx;
+
+typedef TAILQ_HEAD(nr_tcp_socket_head_,nr_tcp_socket_ctx_) nr_tcp_socket_head;
+
+static void nr_tcp_socket_readable_cb(NR_SOCKET s, int how, void *arg);
+
+static int nr_tcp_socket_ctx_destroy(nr_tcp_socket_ctx **objp)
+ {
+ nr_tcp_socket_ctx *sock;
+
+ if (!objp || !*objp)
+ return(0);
+
+ sock=*objp;
+ *objp=0;
+
+ nr_socket_destroy(&sock->inner);
+
+ RFREE(sock);
+
+ return(0);
+ }
+
+/* This takes ownership of nrsock whether it fails or not. */
+static int nr_tcp_socket_ctx_create(nr_socket *nrsock, int is_framed,
+ int max_pending, nr_tcp_socket_ctx **sockp)
+ {
+ int r, _status;
+ nr_tcp_socket_ctx *sock = 0;
+ nr_socket *tcpsock;
+
+ if (!(sock = RCALLOC(sizeof(nr_tcp_socket_ctx)))) {
+ nr_socket_destroy(&nrsock);
+ ABORT(R_NO_MEMORY);
+ }
+
+ if ((r=nr_socket_buffered_stun_create(nrsock, max_pending, is_framed ? ICE_TCP_FRAMING : TURN_TCP_FRAMING, &tcpsock))){
+ nr_socket_destroy(&nrsock);
+ ABORT(r);
+ }
+
+ sock->inner=tcpsock;
+ sock->is_framed=is_framed;
+
+ if ((r=nr_ip4_port_to_transport_addr(ntohl(INADDR_ANY), 0, IPPROTO_TCP, &sock->remote_addr)))
+ ABORT(r);
+
+ *sockp=sock;
+
+ _status=0;
+abort:
+ if (_status) {
+ r_log(LOG_ICE,LOG_DEBUG,"%s:%d function %s failed with error %d",__FILE__,__LINE__,__FUNCTION__,_status);
+ nr_tcp_socket_ctx_destroy(&sock);
+ }
+ return(_status);
+ }
+
+static int nr_tcp_socket_ctx_initialize(nr_tcp_socket_ctx *tcpsock,
+ const nr_transport_addr *addr, void* cb_arg)
+ {
+ int r, _status;
+ NR_SOCKET fd;
+
+ if ((r=nr_transport_addr_copy(&tcpsock->remote_addr, addr)))
+ ABORT(r);
+ if ((r=nr_socket_getfd(tcpsock->inner, &fd)))
+ ABORT(r);
+ NR_ASYNC_WAIT(fd, NR_ASYNC_WAIT_READ, nr_tcp_socket_readable_cb, cb_arg);
+
+ _status=0;
+ abort:
+ if (_status)
+ r_log(LOG_ICE,LOG_DEBUG,"%s:%d function %s(addr:%s) failed with error %d",__FILE__,__LINE__,__FUNCTION__,addr->as_string,_status);
+ return(_status);
+ }
+
+typedef struct nr_socket_multi_tcp_ {
+ nr_ice_ctx *ctx;
+ nr_socket *listen_socket;
+ nr_tcp_socket_head sockets;
+ nr_socket_tcp_type tcp_type;
+ nr_transport_addr addr;
+ NR_async_cb readable_cb;
+ void *readable_cb_arg;
+ int max_pending;
+} nr_socket_multi_tcp;
+
+static int nr_socket_multi_tcp_destroy(void **objp);
+static int nr_socket_multi_tcp_sendto(void *obj,const void *msg, size_t len,
+ int flags, const nr_transport_addr *to);
+static int nr_socket_multi_tcp_recvfrom(void *obj,void * restrict buf,
+ size_t maxlen, size_t *len, int flags, nr_transport_addr *from);
+static int nr_socket_multi_tcp_getaddr(void *obj, nr_transport_addr *addrp);
+static int nr_socket_multi_tcp_close(void *obj);
+static int nr_socket_multi_tcp_connect(void *sock, const nr_transport_addr *addr);
+static int nr_socket_multi_tcp_listen(void *obj, int backlog);
+
+static nr_socket_vtbl nr_socket_multi_tcp_vtbl={
+ 2,
+ nr_socket_multi_tcp_destroy,
+ nr_socket_multi_tcp_sendto,
+ nr_socket_multi_tcp_recvfrom,
+ 0,
+ nr_socket_multi_tcp_getaddr,
+ nr_socket_multi_tcp_connect,
+ 0,
+ 0,
+ nr_socket_multi_tcp_close,
+ nr_socket_multi_tcp_listen,
+ 0
+};
+
+static int nr_socket_multi_tcp_create_stun_server_socket(
+ nr_socket_multi_tcp *sock, nr_ice_stun_server * stun_server,
+ nr_transport_addr *addr, int max_pending)
+ {
+ int r, _status;
+ nr_tcp_socket_ctx *tcp_socket_ctx=0;
+ nr_socket * nrsock;
+
+ if (stun_server->addr.protocol != IPPROTO_TCP) {
+ r_log(LOG_ICE, LOG_INFO,
+ "%s:%d function %s skipping UDP STUN server(addr:%s)", __FILE__,
+ __LINE__, __FUNCTION__, stun_server->addr.as_string);
+ ABORT(R_BAD_ARGS);
+ }
+
+ if (nr_transport_addr_cmp(&stun_server->addr, addr,
+ NR_TRANSPORT_ADDR_CMP_MODE_VERSION)) {
+ r_log(LOG_ICE, LOG_INFO,
+ "%s:%d function %s skipping STUN with different IP version (%u) "
+ "than local socket (%u),",
+ __FILE__, __LINE__, __FUNCTION__, stun_server->addr.ip_version,
+ addr->ip_version);
+ ABORT(R_BAD_ARGS);
+ }
+
+ if ((r=nr_socket_factory_create_socket(sock->ctx->socket_factory,addr, &nrsock)))
+ ABORT(r);
+
+ /* This takes ownership of nrsock whether it fails or not. */
+ if ((r=nr_tcp_socket_ctx_create(nrsock, 0, max_pending, &tcp_socket_ctx)))
+ ABORT(r);
+
+ nr_transport_addr stun_server_addr;
+
+ nr_transport_addr_copy(&stun_server_addr, &stun_server->addr);
+ r = nr_socket_connect(tcp_socket_ctx->inner, &stun_server_addr);
+ if (r && r != R_WOULDBLOCK) {
+ r_log(LOG_ICE, LOG_WARNING,
+ "%s:%d function %s connect to STUN server(addr:%s) failed with "
+ "error %d",
+ __FILE__, __LINE__, __FUNCTION__, stun_server_addr.as_string, r);
+ ABORT(r);
+ }
+
+ if ((r = nr_tcp_socket_ctx_initialize(tcp_socket_ctx, &stun_server_addr,
+ sock)))
+ ABORT(r);
+
+ TAILQ_INSERT_TAIL(&sock->sockets, tcp_socket_ctx, entry);
+
+ _status=0;
+ abort:
+ if (_status) {
+ nr_tcp_socket_ctx_destroy(&tcp_socket_ctx);
+ r_log(LOG_ICE,LOG_DEBUG,"%s:%d function %s(addr:%s) failed with error %d",__FILE__,__LINE__,__FUNCTION__,addr->as_string,_status);
+ }
+ return(_status);
+ }
+
+int nr_socket_multi_tcp_create(struct nr_ice_ctx_ *ctx,
+ struct nr_ice_component_ *component,
+ nr_transport_addr *addr, nr_socket_tcp_type tcp_type,
+ int precreated_so_count, int max_pending, nr_socket **sockp)
+ {
+ int i=0;
+ int r, _status;
+ nr_socket_multi_tcp *sock=0;
+ nr_tcp_socket_ctx *tcp_socket_ctx;
+ nr_socket * nrsock;
+
+ if (!(sock = RCALLOC(sizeof(nr_socket_multi_tcp))))
+ ABORT(R_NO_MEMORY);
+
+ TAILQ_INIT(&sock->sockets);
+
+ sock->ctx=ctx;
+ sock->max_pending=max_pending;
+ sock->tcp_type=tcp_type;
+ nr_transport_addr_copy(&sock->addr, addr);
+
+ if((tcp_type==TCP_TYPE_PASSIVE) &&
+ ((r=nr_socket_factory_create_socket(sock->ctx->socket_factory, addr, &sock->listen_socket))))
+ ABORT(r);
+
+ if (tcp_type!=TCP_TYPE_ACTIVE) {
+ nr_ice_stun_server *stun_servers;
+ nr_ice_turn_server *turn_servers;
+ int stun_server_ct, turn_server_ct;
+ if (component) {
+ stun_servers = component->stream->stun_servers;
+ turn_servers = component->stream->turn_servers;
+ stun_server_ct = component->stream->stun_server_ct;
+ turn_server_ct = component->stream->turn_server_ct;
+ } else {
+ /* Mainly for unit-testing */
+ stun_servers = ctx->stun_servers_cfg;
+ turn_servers = ctx->turn_servers_cfg;
+ stun_server_ct = ctx->stun_server_ct_cfg;
+ turn_server_ct = ctx->turn_server_ct_cfg;
+ }
+ if (stun_servers) {
+ for (i=0; i<stun_server_ct; ++i) {
+ if ((r=nr_socket_multi_tcp_create_stun_server_socket(sock,
+ stun_servers+i, addr, max_pending))) {
+ if (r!=R_BAD_ARGS) {
+ r_log(LOG_ICE,LOG_WARNING,"%s:%d function %s failed to connect STUN server from addr:%s with error %d",__FILE__,__LINE__,__FUNCTION__,addr->as_string,r);
+ }
+ }
+ }
+ }
+ if (turn_servers) {
+ for (i=0; i<turn_server_ct; ++i) {
+ if ((r=nr_socket_multi_tcp_create_stun_server_socket(sock,
+ &(turn_servers[i]).turn_server, addr, max_pending))) {
+ if (r!=R_BAD_ARGS) {
+ r_log(LOG_ICE,LOG_WARNING,"%s:%d function %s failed to connect TURN server from addr:%s with error %d",__FILE__,__LINE__,__FUNCTION__,addr->as_string,r);
+ }
+ }
+ }
+ }
+ }
+
+ if ((tcp_type==TCP_TYPE_SO)) {
+ for (i=0; i<precreated_so_count; ++i) {
+
+ if ((r=nr_socket_factory_create_socket(sock->ctx->socket_factory, addr, &nrsock)))
+ ABORT(r);
+
+ /* This takes ownership of nrsock whether it fails or not. */
+ if ((r=nr_tcp_socket_ctx_create(nrsock, 1, max_pending, &tcp_socket_ctx))){
+ ABORT(r);
+ }
+ TAILQ_INSERT_TAIL(&sock->sockets, tcp_socket_ctx, entry);
+ }
+ }
+
+ if((r=nr_socket_create_int(sock, &nr_socket_multi_tcp_vtbl, sockp)))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ if (_status) {
+ r_log(LOG_ICE,LOG_DEBUG,"%s:%d function %s(addr:%s) failed with error %d",__FILE__,__LINE__,__FUNCTION__,addr->as_string,_status);
+ nr_socket_multi_tcp_destroy((void**)&sock);
+ }
+ return(_status);
+ }
+
+int nr_socket_multi_tcp_set_readable_cb(nr_socket *sock,
+ NR_async_cb readable_cb, void *readable_cb_arg)
+ {
+ nr_socket_multi_tcp *mtcp_sock = (nr_socket_multi_tcp *)sock->obj;
+
+ mtcp_sock->readable_cb=readable_cb;
+ mtcp_sock->readable_cb_arg=readable_cb_arg;
+
+ return 0;
+ }
+
+#define PREALLOC_CONNECT_FRAMED 0
+#define PREALLOC_CONNECT_NON_FRAMED 1
+#define PREALLOC_DONT_CONNECT_UNLESS_SO 2
+
+static int nr_socket_multi_tcp_get_sock_connected_to(nr_socket_multi_tcp *sock,
+ const nr_transport_addr *to, int preallocated_connect_mode, nr_socket **ret_sock)
+ {
+ int r, _status;
+ nr_tcp_socket_ctx *tcp_sock_ctx;
+ nr_socket * nrsock;
+
+ TAILQ_FOREACH(tcp_sock_ctx, &sock->sockets, entry) {
+ if (!nr_transport_addr_is_wildcard(&tcp_sock_ctx->remote_addr)) {
+ if (!nr_transport_addr_cmp(to, &tcp_sock_ctx->remote_addr, NR_TRANSPORT_ADDR_CMP_MODE_ALL)) {
+ *ret_sock=tcp_sock_ctx->inner;
+ return(0);
+ }
+ }
+ }
+
+ tcp_sock_ctx=NULL;
+ /* not connected yet */
+ if (sock->tcp_type != TCP_TYPE_ACTIVE) {
+ if (preallocated_connect_mode == PREALLOC_DONT_CONNECT_UNLESS_SO && sock->tcp_type != TCP_TYPE_SO)
+ ABORT(R_FAILED);
+
+ /* find free preallocated socket and connect */
+ TAILQ_FOREACH(tcp_sock_ctx, &sock->sockets, entry) {
+ if (nr_transport_addr_is_wildcard(&tcp_sock_ctx->remote_addr)) {
+ if (preallocated_connect_mode == PREALLOC_CONNECT_NON_FRAMED && tcp_sock_ctx->is_framed)
+ continue;
+ if (preallocated_connect_mode != PREALLOC_CONNECT_NON_FRAMED && !tcp_sock_ctx->is_framed)
+ continue;
+
+ if ((r=nr_socket_connect(tcp_sock_ctx->inner, to))){
+ if (r!=R_WOULDBLOCK)
+ ABORT(r);
+ }
+
+ if ((r=nr_tcp_socket_ctx_initialize(tcp_sock_ctx, to, sock)))
+ ABORT(r);
+
+ *ret_sock=tcp_sock_ctx->inner;
+
+ return(0);
+ }
+ }
+ tcp_sock_ctx=NULL;
+ ABORT(R_FAILED);
+ }
+
+ /* if active type - create new socket for each new remote addr */
+ assert(sock->tcp_type == TCP_TYPE_ACTIVE);
+
+ if ((r=nr_socket_factory_create_socket(sock->ctx->socket_factory, &sock->addr, &nrsock)))
+ ABORT(r);
+
+ /* This takes ownership of nrsock whether it fails or not. */
+ if ((r=nr_tcp_socket_ctx_create(nrsock, 1, sock->max_pending, &tcp_sock_ctx))){
+ ABORT(r);
+ }
+
+ TAILQ_INSERT_TAIL(&sock->sockets, tcp_sock_ctx, entry);
+
+ if ((r=nr_socket_connect(tcp_sock_ctx->inner, to))){
+ if (r!=R_WOULDBLOCK)
+ ABORT(r);
+ }
+
+ if ((r=nr_tcp_socket_ctx_initialize(tcp_sock_ctx, to, sock)))
+ ABORT(r);
+
+ *ret_sock=tcp_sock_ctx->inner;
+ tcp_sock_ctx=NULL;
+
+ _status=0;
+ abort:
+ if (_status) {
+ if (tcp_sock_ctx) {
+ r_log(LOG_ICE,LOG_DEBUG,"%s:%d function %s failed with error %d, tcp_sock_ctx remote_addr: %s",__FILE__,__LINE__,__FUNCTION__,_status, tcp_sock_ctx->remote_addr.as_string);
+ TAILQ_REMOVE(&sock->sockets, tcp_sock_ctx, entry);
+ nr_tcp_socket_ctx_destroy(&tcp_sock_ctx);
+ } else {
+ r_log(LOG_ICE,LOG_DEBUG,"%s:%d function %s failed with error %d, tcp_sock_ctx=NULL",__FILE__,__LINE__,__FUNCTION__,_status);
+ }
+ }
+
+ return(_status);
+ }
+
+int nr_socket_multi_tcp_stun_server_connect(nr_socket *sock,
+ const nr_transport_addr *addr)
+ {
+ int r, _status;
+ nr_socket_multi_tcp *mtcp_sock = (nr_socket_multi_tcp *)sock->obj;
+ nr_socket *nrsock;
+
+ assert(mtcp_sock->tcp_type != TCP_TYPE_ACTIVE);
+ if (mtcp_sock->tcp_type == TCP_TYPE_ACTIVE)
+ ABORT(R_INTERNAL);
+
+ if ((r=nr_socket_multi_tcp_get_sock_connected_to(mtcp_sock,addr,PREALLOC_CONNECT_NON_FRAMED,&nrsock)))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ if (_status)
+ r_log(LOG_ICE,LOG_DEBUG,"%s:%d function %s(addr:%s) failed with error %d",__FILE__,__LINE__,__FUNCTION__,addr->as_string,_status);
+ return(_status);
+ }
+
+static int nr_socket_multi_tcp_destroy(void **objp)
+ {
+ nr_socket_multi_tcp *sock;
+ nr_tcp_socket_ctx *tcpsock;
+ NR_SOCKET fd;
+
+ if (!objp || !*objp)
+ return 0;
+
+ sock=(nr_socket_multi_tcp *)*objp;
+ *objp=0;
+
+ /* Cancel waiting on the socket */
+ if (sock->listen_socket && !nr_socket_getfd(sock->listen_socket, &fd)) {
+ NR_ASYNC_CANCEL(fd, NR_ASYNC_WAIT_READ);
+ }
+
+ nr_socket_destroy(&sock->listen_socket);
+
+ while (!TAILQ_EMPTY(&sock->sockets)) {
+
+ tcpsock = TAILQ_FIRST(&sock->sockets);
+ TAILQ_REMOVE(&sock->sockets, tcpsock, entry);
+
+ if (!nr_socket_getfd(tcpsock->inner, &fd)) {
+ NR_ASYNC_CANCEL(fd, NR_ASYNC_WAIT_READ);
+ }
+
+ nr_tcp_socket_ctx_destroy(&tcpsock);
+ }
+
+ RFREE(sock);
+
+ return 0;
+ }
+
+static int nr_socket_multi_tcp_sendto(void *obj, const void *msg, size_t len,
+ int flags, const nr_transport_addr *to)
+ {
+ int r, _status;
+ nr_socket_multi_tcp *sock=(nr_socket_multi_tcp *)obj;
+ nr_socket *nrsock;
+
+ if ((r=nr_socket_multi_tcp_get_sock_connected_to(sock, to,
+ PREALLOC_DONT_CONNECT_UNLESS_SO, &nrsock)))
+ ABORT(r);
+
+ if((r=nr_socket_sendto(nrsock, msg, len, flags, to)))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ if (_status)
+ r_log(LOG_ICE,LOG_DEBUG,"%s:%d function %s(to:%s) failed with error %d",__FILE__,__LINE__,__FUNCTION__,to->as_string,_status);
+
+ return(_status);
+}
+
+static int nr_socket_multi_tcp_recvfrom(void *obj,void * restrict buf,
+ size_t maxlen, size_t *len, int flags, nr_transport_addr *from)
+ {
+ int r, _status = 0;
+ nr_socket_multi_tcp *sock=(nr_socket_multi_tcp *)obj;
+ nr_tcp_socket_ctx *tcpsock;
+
+ if (TAILQ_EMPTY(&sock->sockets))
+ ABORT(R_FAILED);
+
+ TAILQ_FOREACH(tcpsock, &sock->sockets, entry) {
+ if (nr_transport_addr_is_wildcard(&tcpsock->remote_addr))
+ continue;
+ r=nr_socket_recvfrom(tcpsock->inner, buf, maxlen, len, flags, from);
+ if (!r)
+ return 0;
+
+ if (r!=R_WOULDBLOCK) {
+ NR_SOCKET fd;
+ r_log(LOG_ICE,LOG_DEBUG,
+ "%s:%d function %s(to:%s) failed with error %d",__FILE__,
+ __LINE__,__FUNCTION__,tcpsock->remote_addr.as_string,r);
+ if (!nr_socket_getfd(tcpsock->inner, &fd)) {
+ NR_ASYNC_CANCEL(fd, NR_ASYNC_WAIT_READ);
+ NR_ASYNC_CANCEL(fd, NR_ASYNC_WAIT_WRITE);
+ }
+
+ TAILQ_REMOVE(&sock->sockets, tcpsock, entry);
+ nr_tcp_socket_ctx_destroy(&tcpsock);
+ ABORT(r);
+ }
+ }
+
+ /* this also gets returned if all tcpsocks have wildcard remote_addr */
+ _status=R_WOULDBLOCK;
+ abort:
+
+ return(_status);
+ }
+
+static int nr_socket_multi_tcp_getaddr(void *obj, nr_transport_addr *addrp)
+ {
+ nr_socket_multi_tcp *sock=(nr_socket_multi_tcp *)obj;
+
+ return nr_transport_addr_copy(addrp,&sock->addr);
+ }
+
+static int nr_socket_multi_tcp_close(void *obj)
+ {
+ nr_socket_multi_tcp *sock=(nr_socket_multi_tcp *)obj;
+ nr_tcp_socket_ctx *tcpsock;
+
+ if(sock->listen_socket)
+ nr_socket_close(sock->listen_socket);
+
+ TAILQ_FOREACH(tcpsock, &sock->sockets, entry) {
+ nr_socket_close(tcpsock->inner); //ignore errors
+ }
+
+ return 0;
+ }
+
+static void nr_tcp_socket_readable_cb(NR_SOCKET s, int how, void *arg)
+ {
+ nr_socket_multi_tcp *sock=(nr_socket_multi_tcp *)arg;
+
+ // rearm
+ NR_ASYNC_WAIT(s, NR_ASYNC_WAIT_READ, nr_tcp_socket_readable_cb, arg);
+
+ if (sock->readable_cb)
+ sock->readable_cb(s, how, sock->readable_cb_arg);
+ }
+
+static int nr_socket_multi_tcp_connect(void *obj, const nr_transport_addr *addr)
+ {
+ int r, _status;
+ nr_socket_multi_tcp *sock=(nr_socket_multi_tcp *)obj;
+ nr_socket *nrsock;
+
+ if ((r=nr_socket_multi_tcp_get_sock_connected_to(sock,addr,PREALLOC_CONNECT_FRAMED,&nrsock)))
+ ABORT(r);
+
+ _status=0;
+abort:
+ if (_status)
+ r_log(LOG_ICE,LOG_DEBUG,"%s:%d function %s(addr:%s) failed with error %d",__FILE__,__LINE__,__FUNCTION__,addr->as_string,_status);
+
+ return(_status);
+ }
+
+static void nr_tcp_multi_lsocket_readable_cb(NR_SOCKET s, int how, void *arg)
+ {
+ nr_socket_multi_tcp *sock=(nr_socket_multi_tcp *)arg;
+ nr_socket *newsock;
+ nr_transport_addr remote_addr;
+ nr_tcp_socket_ctx *tcp_sock_ctx;
+ int r, _status;
+
+ // rearm
+ NR_ASYNC_WAIT(s, NR_ASYNC_WAIT_READ, nr_tcp_multi_lsocket_readable_cb, arg);
+
+ /* accept */
+ if ((r=nr_socket_accept(sock->listen_socket, &remote_addr, &newsock)))
+ ABORT(r);
+
+ /* This takes ownership of newsock whether it fails or not. */
+ if ((r=nr_tcp_socket_ctx_create(newsock, 1, sock->max_pending, &tcp_sock_ctx)))
+ ABORT(r);
+
+ nr_socket_buffered_set_connected_to(tcp_sock_ctx->inner, &remote_addr);
+
+ if ((r=nr_tcp_socket_ctx_initialize(tcp_sock_ctx, &remote_addr, sock))) {
+ nr_tcp_socket_ctx_destroy(&tcp_sock_ctx);
+ ABORT(r);
+ }
+
+ TAILQ_INSERT_HEAD(&sock->sockets, tcp_sock_ctx, entry);
+
+ _status=0;
+abort:
+ if (_status) {
+ r_log(LOG_ICE,LOG_WARNING,"%s:%d %s failed to accept new TCP connection: %d",__FILE__,__LINE__,__FUNCTION__,_status);
+ } else {
+ r_log(LOG_ICE,LOG_INFO,"%s:%d %s accepted new TCP connection from %s",__FILE__,__LINE__,__FUNCTION__,remote_addr.as_string);
+ }
+ }
+
+static int nr_socket_multi_tcp_listen(void *obj, int backlog)
+ {
+ int r, _status;
+ nr_socket_multi_tcp *sock=(nr_socket_multi_tcp *)obj;
+ NR_SOCKET fd;
+
+ if(!sock->listen_socket)
+ ABORT(R_FAILED);
+
+ if ((r=nr_socket_listen(sock->listen_socket, backlog)))
+ ABORT(r);
+
+ if ((r=nr_socket_getfd(sock->listen_socket, &fd)))
+ ABORT(r);
+
+ NR_ASYNC_WAIT(fd, NR_ASYNC_WAIT_READ, nr_tcp_multi_lsocket_readable_cb, sock);
+
+ _status=0;
+ abort:
+ if (_status)
+ r_log(LOG_ICE,LOG_WARNING,"%s:%d function %s failed with error %d",__FILE__,__LINE__,__FUNCTION__,_status);
+
+ return(_status);
+ }
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_socket_multi_tcp.h b/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_socket_multi_tcp.h
new file mode 100644
index 0000000000..8413e67293
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_socket_multi_tcp.h
@@ -0,0 +1,53 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+Copyright (c) 2014, Mozilla
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#ifndef _nr_socket_multi_tcp_h
+#define _nr_socket_multi_tcp_h
+
+#include "nr_socket.h"
+
+/* Argument use_framing is 0 only in call from test code (STUN TCP server
+ listening socket). For other purposes it should be always set to true */
+
+int nr_socket_multi_tcp_create(struct nr_ice_ctx_ *ctx,
+ struct nr_ice_component_ *component,
+ nr_transport_addr *addr, nr_socket_tcp_type tcp_type,
+ int precreated_so_count, int max_pending, nr_socket **sockp);
+
+int nr_socket_multi_tcp_set_readable_cb(nr_socket *sock,
+ NR_async_cb readable_cb,void *readable_cb_arg);
+
+int nr_socket_multi_tcp_stun_server_connect(nr_socket *sock,
+ const nr_transport_addr *addr);
+
+#endif
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_socket_wrapper.c b/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_socket_wrapper.c
new file mode 100644
index 0000000000..4ad59527c1
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_socket_wrapper.c
@@ -0,0 +1,84 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+Copyright (c) 2013, Mozilla
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance, Mozilla nor
+ the names of its contributors may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <nr_api.h>
+#include "nr_socket_wrapper.h"
+
+#include <assert.h>
+
+int nr_socket_wrapper_factory_create_int(void *obj, nr_socket_wrapper_factory_vtbl *vtbl,
+ nr_socket_wrapper_factory **wrapperp)
+{
+ int _status;
+ nr_socket_wrapper_factory *wrapper=0;
+
+ if (!(wrapper=RCALLOC(sizeof(nr_socket_wrapper_factory))))
+ ABORT(R_NO_MEMORY);
+
+ wrapper->obj=obj;
+ wrapper->vtbl=vtbl;
+
+ *wrapperp=wrapper;
+ _status=0;
+abort:
+ return(_status);
+}
+
+int nr_socket_wrapper_factory_wrap(nr_socket_wrapper_factory *wrapper,
+ nr_socket *inner,
+ nr_socket **socketp)
+{
+ return wrapper->vtbl->wrap(wrapper->obj, inner, socketp);
+}
+
+int nr_socket_wrapper_factory_destroy(nr_socket_wrapper_factory **wrapperp)
+{
+ nr_socket_wrapper_factory *wrapper;
+
+ if (!wrapperp || !*wrapperp)
+ return 0;
+
+ wrapper = *wrapperp;
+ *wrapperp = 0;
+
+ assert(wrapper->vtbl);
+ if (wrapper->vtbl)
+ wrapper->vtbl->destroy(&wrapper->obj);
+
+ RFREE(wrapper);
+
+ return 0;
+}
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_socket_wrapper.h b/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_socket_wrapper.h
new file mode 100644
index 0000000000..717518e23e
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/net/nr_socket_wrapper.h
@@ -0,0 +1,63 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+Copyright (c) 2013, Mozilla
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance, Mozilla nor
+ the names of its contributors may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#ifndef _nr_socket_wrapper_h
+#define _nr_socket_wrapper_h
+
+#include "nr_socket.h"
+
+typedef struct nr_socket_wrapper_factory_vtbl_ {
+ int (*wrap)(void *obj,
+ nr_socket *socket,
+ nr_socket **socketp);
+ int (*destroy)(void **obj);
+} nr_socket_wrapper_factory_vtbl;
+
+typedef struct nr_socket_wrapper_factory_ {
+ void *obj;
+ nr_socket_wrapper_factory_vtbl *vtbl;
+} nr_socket_wrapper_factory;
+
+
+int nr_socket_wrapper_factory_create_int(void *obj, nr_socket_wrapper_factory_vtbl *vtbl,
+ nr_socket_wrapper_factory **wrapperp);
+
+
+int nr_socket_wrapper_factory_wrap(nr_socket_wrapper_factory *wrapper, nr_socket *inner,
+ nr_socket **socketp);
+
+int nr_socket_wrapper_factory_destroy(nr_socket_wrapper_factory **wrapperp);
+
+#endif
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/net/transport_addr.c b/dom/media/webrtc/transport/third_party/nICEr/src/net/transport_addr.c
new file mode 100644
index 0000000000..efedb3782a
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/net/transport_addr.c
@@ -0,0 +1,559 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <csi_platform.h>
+#include <stdio.h>
+#include <memory.h>
+#include <sys/types.h>
+#include <errno.h>
+#ifdef WIN32
+#include <winsock2.h>
+#else
+#include <unistd.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#endif
+#include <assert.h>
+#include "nr_api.h"
+#include "util.h"
+#include "transport_addr.h"
+
+int nr_transport_addr_fmt_addr_string(nr_transport_addr *addr)
+ {
+ int _status;
+ /* Max length for normalized IPv6 address string representation is 39 */
+ char buffer[40];
+ const char *protocol;
+
+ switch(addr->protocol){
+ case IPPROTO_TCP:
+ if (addr->tls) {
+ protocol = "TLS";
+ } else {
+ protocol = "TCP";
+ }
+ break;
+ case IPPROTO_UDP:
+ protocol = "UDP";
+ break;
+ default:
+ ABORT(R_INTERNAL);
+ }
+
+ switch(addr->ip_version){
+ case NR_IPV4:
+ if (!inet_ntop(AF_INET, &addr->u.addr4.sin_addr,buffer,sizeof(buffer)))
+ strcpy(buffer, "[error]");
+ snprintf(addr->as_string,sizeof(addr->as_string),"IP4:%s:%d/%s",buffer,(int)ntohs(addr->u.addr4.sin_port),protocol);
+ break;
+ case NR_IPV6:
+ if (!inet_ntop(AF_INET6, &addr->u.addr6.sin6_addr,buffer,sizeof(buffer)))
+ strcpy(buffer, "[error]");
+ snprintf(addr->as_string,sizeof(addr->as_string),"IP6:[%s]:%d/%s",buffer,(int)ntohs(addr->u.addr6.sin6_port),protocol);
+ break;
+ default:
+ ABORT(R_INTERNAL);
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_transport_addr_fmt_ifname_addr_string(const nr_transport_addr *addr, char *buf, int len)
+ {
+ int _status;
+ /* leave room for a fully-expanded IPV4-mapped IPV6 address */
+ char buffer[46];
+
+ switch(addr->ip_version){
+ case NR_IPV4:
+ if (!inet_ntop(AF_INET, &addr->u.addr4.sin_addr,buffer,sizeof(buffer))) {
+ strncpy(buffer, "[error]", sizeof(buffer));
+ }
+ break;
+ case NR_IPV6:
+ if (!inet_ntop(AF_INET6, &addr->u.addr6.sin6_addr,buffer,sizeof(buffer))) {
+ strncpy(buffer, "[error]", sizeof(buffer));
+ }
+ break;
+ default:
+ ABORT(R_INTERNAL);
+ }
+ buffer[sizeof(buffer) - 1] = '\0';
+
+ snprintf(buf,len,"%s:%s",addr->ifname,buffer);
+ buf[len - 1] = '\0';
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_sockaddr_to_transport_addr(struct sockaddr *saddr, int protocol, int keep, nr_transport_addr *addr)
+ {
+ int r,_status;
+
+ if(!keep) memset(addr,0,sizeof(nr_transport_addr));
+
+ switch(protocol){
+ case IPPROTO_TCP:
+ case IPPROTO_UDP:
+ break;
+ default:
+ ABORT(R_BAD_ARGS);
+ }
+
+ addr->protocol=protocol;
+
+ if(saddr->sa_family==AF_INET){
+ addr->ip_version=NR_IPV4;
+
+ memcpy(&addr->u.addr4,saddr,sizeof(struct sockaddr_in));
+ }
+ else if(saddr->sa_family==AF_INET6){
+ addr->ip_version=NR_IPV6;
+
+ memcpy(&addr->u.addr6, saddr, sizeof(struct sockaddr_in6));
+ }
+ else
+ ABORT(R_BAD_ARGS);
+
+ if(r=nr_transport_addr_fmt_addr_string(addr))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+
+int nr_transport_addr_copy(nr_transport_addr *to, const nr_transport_addr *from)
+ {
+ memcpy(to,from,sizeof(nr_transport_addr));
+ return 0;
+ }
+
+int nr_transport_addr_copy_keep_ifname(nr_transport_addr *to, const nr_transport_addr *from)
+ {
+ int r,_status;
+ char save_ifname[MAXIFNAME];
+
+ strncpy(save_ifname, to->ifname, MAXIFNAME);
+ save_ifname[MAXIFNAME-1]=0; /* Ensure null termination */
+
+ if (r=nr_transport_addr_copy(to, from))
+ ABORT(r);
+
+ strncpy(to->ifname, save_ifname, MAXIFNAME);
+
+ if (r=nr_transport_addr_fmt_addr_string(to))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ return _status;
+ }
+
+int nr_transport_addr_copy_addrport(nr_transport_addr *to, const nr_transport_addr *from)
+ {
+ int r,_status;
+
+ switch (from->ip_version) {
+ case NR_IPV4:
+ memcpy(&to->u.addr4, &from->u.addr4, sizeof(to->u.addr4));
+ break;
+ case NR_IPV6:
+ memcpy(&to->u.addr6, &from->u.addr6, sizeof(to->u.addr6));
+ break;
+ default:
+ ABORT(R_BAD_ARGS);
+ }
+
+ to->ip_version = from->ip_version;
+
+ if (r=nr_transport_addr_fmt_addr_string(to)) {
+ ABORT(r);
+ }
+
+ _status=0;
+ abort:
+ return _status;
+ }
+
+/* Convenience fxn. Is this the right API?*/
+int nr_ip4_port_to_transport_addr(UINT4 ip4, UINT2 port, int protocol, nr_transport_addr *addr)
+ {
+ int r,_status;
+
+ memset(addr, 0, sizeof(nr_transport_addr));
+
+ addr->ip_version=NR_IPV4;
+ addr->protocol=protocol;
+#ifdef HAVE_SIN_LEN
+ addr->u.addr4.sin_len=sizeof(struct sockaddr_in);
+#endif
+ addr->u.addr4.sin_family=PF_INET;
+ addr->u.addr4.sin_port=htons(port);
+ addr->u.addr4.sin_addr.s_addr=htonl(ip4);
+
+ if(r=nr_transport_addr_fmt_addr_string(addr))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_str_port_to_transport_addr(const char *ip, UINT2 port, int protocol, nr_transport_addr *addr_out)
+ {
+ int r,_status;
+ struct in_addr addr;
+ struct in6_addr addr6;
+
+ if (inet_pton(AF_INET, ip, &addr) == 1) {
+ if(r=nr_ip4_port_to_transport_addr(ntohl(addr.s_addr),port,protocol,addr_out))
+ ABORT(r);
+ } else if (inet_pton(AF_INET6, ip, &addr6) == 1) {
+ if(r=nr_ip6_port_to_transport_addr(&addr6,port,protocol,addr_out))
+ ABORT(r);
+ } else {
+ ABORT(R_BAD_DATA);
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_ip6_port_to_transport_addr(struct in6_addr* addr6, UINT2 port, int protocol, nr_transport_addr *addr)
+ {
+ int r,_status;
+
+ memset(addr, 0, sizeof(nr_transport_addr));
+
+ addr->ip_version=NR_IPV6;
+ addr->protocol=protocol;
+ addr->u.addr6.sin6_family=PF_INET6;
+ addr->u.addr6.sin6_port=htons(port);
+ memcpy(addr->u.addr6.sin6_addr.s6_addr, addr6->s6_addr, sizeof(addr6->s6_addr));
+
+ if(r=nr_transport_addr_fmt_addr_string(addr))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_transport_addr_get_addrstring(const nr_transport_addr *addr, char *str, int maxlen)
+ {
+ int _status;
+
+ if (addr->fqdn[0]) {
+ strncpy(str, addr->fqdn, maxlen);
+ } else {
+ const char* res;
+ switch (addr->ip_version) {
+ case NR_IPV4:
+ res = inet_ntop(AF_INET, &addr->u.addr4.sin_addr, str, maxlen);
+ break;
+ case NR_IPV6:
+ res = inet_ntop(AF_INET6, &addr->u.addr6.sin6_addr, str, maxlen);
+ break;
+ default:
+ ABORT(R_INTERNAL);
+ }
+
+ if (!res) {
+ if (errno == ENOSPC) {
+ ABORT(R_BAD_ARGS);
+ }
+ ABORT(R_INTERNAL);
+ }
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_transport_addr_get_port(const nr_transport_addr *addr, int *port)
+ {
+ int _status;
+
+ switch(addr->ip_version){
+ case NR_IPV4:
+ *port=ntohs(addr->u.addr4.sin_port);
+ break;
+ case NR_IPV6:
+ *port=ntohs(addr->u.addr6.sin6_port);
+ break;
+ default:
+ ABORT(R_INTERNAL);
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_transport_addr_set_port(nr_transport_addr *addr, int port)
+ {
+ int _status;
+
+ switch(addr->ip_version){
+ case NR_IPV4:
+ addr->u.addr4.sin_port=htons(port);
+ break;
+ case NR_IPV6:
+ addr->u.addr6.sin6_port=htons(port);
+ break;
+ default:
+ ABORT(R_INTERNAL);
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+/* memcmp() may not work if, for instance, the string or interface
+ haven't been made. Hmmm.. */
+int nr_transport_addr_cmp(const nr_transport_addr *addr1,const nr_transport_addr *addr2,int mode)
+ {
+ assert(mode);
+
+ if(addr1->ip_version != addr2->ip_version)
+ return(1);
+
+ if(mode < NR_TRANSPORT_ADDR_CMP_MODE_PROTOCOL)
+ return(0);
+
+ if(addr1->protocol != addr2->protocol)
+ return(1);
+
+ if(mode < NR_TRANSPORT_ADDR_CMP_MODE_ADDR)
+ return(0);
+
+ switch(addr1->ip_version){
+ case NR_IPV4:
+ if(addr1->u.addr4.sin_addr.s_addr != addr2->u.addr4.sin_addr.s_addr)
+ return(1);
+ if(mode < NR_TRANSPORT_ADDR_CMP_MODE_ALL)
+ return(0);
+ if(addr1->u.addr4.sin_port != addr2->u.addr4.sin_port)
+ return(1);
+ break;
+ case NR_IPV6:
+ if(memcmp(addr1->u.addr6.sin6_addr.s6_addr,addr2->u.addr6.sin6_addr.s6_addr,sizeof(struct in6_addr)))
+ return(1);
+ if(mode < NR_TRANSPORT_ADDR_CMP_MODE_ALL)
+ return(0);
+ if(addr1->u.addr6.sin6_port != addr2->u.addr6.sin6_port)
+ return(1);
+ break;
+ default:
+ abort();
+ }
+
+ return(0);
+ }
+
+int nr_transport_addr_is_loopback(const nr_transport_addr *addr)
+ {
+ switch(addr->ip_version){
+ case NR_IPV4:
+ switch(addr->u.addr4.sin_family){
+ case AF_INET:
+ if (((ntohl(addr->u.addr4.sin_addr.s_addr)>>24)&0xff)==0x7f)
+ return 1;
+ break;
+ default:
+ NR_UNIMPLEMENTED;
+ break;
+ }
+ break;
+
+ case NR_IPV6:
+ if(!memcmp(addr->u.addr6.sin6_addr.s6_addr,in6addr_loopback.s6_addr,sizeof(struct in6_addr)))
+ return(1);
+ break;
+ default:
+ NR_UNIMPLEMENTED;
+ }
+
+ return(0);
+ }
+
+int nr_transport_addr_is_link_local(const nr_transport_addr *addr)
+ {
+ switch(addr->ip_version){
+ case NR_IPV4:
+ /* RFC3927: 169.254/16 */
+ if ((ntohl(addr->u.addr4.sin_addr.s_addr) & 0xFFFF0000) == 0xA9FE0000)
+ return(1);
+ break;
+ case NR_IPV6:
+ {
+ UINT4* addrTop = (UINT4*)(addr->u.addr6.sin6_addr.s6_addr);
+ if ((*addrTop & htonl(0xFFC00000)) == htonl(0xFE800000))
+ return(2);
+ }
+ break;
+ default:
+ NR_UNIMPLEMENTED;
+ }
+
+ return(0);
+ }
+
+int nr_transport_addr_is_mac_based(const nr_transport_addr *addr)
+ {
+ switch(addr->ip_version){
+ case NR_IPV4:
+ // IPv4 has no MAC based self assigned IP addresses
+ return(0);
+ case NR_IPV6:
+ {
+ // RFC 2373, Appendix A: lower 64bit 0x020000FFFE000000
+ // indicates a MAC based IPv6 address
+ UINT4* macCom = (UINT4*)(addr->u.addr6.sin6_addr.s6_addr + 8);
+ UINT4* macExt = (UINT4*)(addr->u.addr6.sin6_addr.s6_addr + 12);
+ if ((*macCom & htonl(0x020000FF)) == htonl(0x020000FF) &&
+ (*macExt & htonl(0xFF000000)) == htonl(0xFE000000)) {
+ return(1);
+ }
+ }
+ break;
+ default:
+ NR_UNIMPLEMENTED;
+ }
+ return(0);
+ }
+
+int nr_transport_addr_is_teredo(const nr_transport_addr *addr)
+ {
+ switch(addr->ip_version){
+ case NR_IPV4:
+ return(0);
+ case NR_IPV6:
+ {
+ UINT4* addrTop = (UINT4*)(addr->u.addr6.sin6_addr.s6_addr);
+ if ((*addrTop & htonl(0xFFFFFFFF)) == htonl(0x20010000))
+ return(1);
+ }
+ break;
+ default:
+ NR_UNIMPLEMENTED;
+ }
+
+ return(0);
+ }
+
+int nr_transport_addr_check_compatibility(const nr_transport_addr *addr1, const nr_transport_addr *addr2)
+ {
+ // first make sure we're comparing the same ip versions and protocols
+ if ((addr1->ip_version != addr2->ip_version) ||
+ (addr1->protocol != addr2->protocol)) {
+ return(1);
+ }
+
+ if (!addr1->fqdn[0] && !addr2->fqdn[0]) {
+ // now make sure the link local status matches
+ if (nr_transport_addr_is_link_local(addr1) !=
+ nr_transport_addr_is_link_local(addr2)) {
+ return(1);
+ }
+ }
+ return(0);
+ }
+
+int nr_transport_addr_is_wildcard(const nr_transport_addr *addr)
+ {
+ switch(addr->ip_version){
+ case NR_IPV4:
+ if(addr->u.addr4.sin_addr.s_addr==INADDR_ANY)
+ return(1);
+ if(addr->u.addr4.sin_port==0)
+ return(1);
+ break;
+ case NR_IPV6:
+ if(!memcmp(addr->u.addr6.sin6_addr.s6_addr,in6addr_any.s6_addr,sizeof(struct in6_addr)))
+ return(1);
+ if(addr->u.addr6.sin6_port==0)
+ return(1);
+ break;
+ default:
+ NR_UNIMPLEMENTED;
+ }
+
+ return(0);
+ }
+
+nr_transport_addr_mask nr_private_ipv4_addrs[] = {
+ /* RFC1918: 10/8 */
+ {0x0A000000, 0xFF000000},
+ /* RFC1918: 172.16/12 */
+ {0xAC100000, 0xFFF00000},
+ /* RFC1918: 192.168/16 */
+ {0xC0A80000, 0xFFFF0000},
+ /* RFC6598: 100.64/10 */
+ {0x64400000, 0xFFC00000}
+};
+
+int nr_transport_addr_get_private_addr_range(const nr_transport_addr *addr)
+ {
+ switch(addr->ip_version){
+ case NR_IPV4:
+ {
+ UINT4 ip = ntohl(addr->u.addr4.sin_addr.s_addr);
+ for (size_t i=0; i<(sizeof(nr_private_ipv4_addrs)/sizeof(nr_transport_addr_mask)); i++) {
+ if ((ip & nr_private_ipv4_addrs[i].mask) == nr_private_ipv4_addrs[i].addr)
+ return i + 1;
+ }
+ }
+ break;
+ case NR_IPV6:
+ return(0);
+ default:
+ NR_UNIMPLEMENTED;
+ }
+
+ return(0);
+ }
+
+int nr_transport_addr_is_reliable_transport(const nr_transport_addr *addr)
+ {
+ return addr->protocol == IPPROTO_TCP;
+ }
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/net/transport_addr.h b/dom/media/webrtc/transport/third_party/nICEr/src/net/transport_addr.h
new file mode 100644
index 0000000000..e8679a7e5a
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/net/transport_addr.h
@@ -0,0 +1,128 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+
+#ifndef _transport_addr_h
+#define _transport_addr_h
+
+#include <stdbool.h>
+#include <sys/types.h>
+#ifdef WIN32
+
+// FIXME: This is dangerous, but exactly the pattern used in
+// nrappkit/src/port/win32/include/csi_platform.h
+// Not good because INT8 are typedefed to different values in
+// <winsock2.h> and <r_types.h>.
+// {
+typedef unsigned char UBLAH_IGNORE_ME_PLEASE;
+typedef signed char BLAH_IGNORE_ME_PLEASE;
+#define UINT8 UBLAH_IGNORE_ME_PLEASE
+#define INT8 BLAH_IGNORE_ME_PLEASE
+#include <winsock2.h>
+#undef UINT8
+#undef INT8
+#include <r_types.h>
+
+// }
+
+#include <ws2tcpip.h>
+#else
+#include <sys/socket.h>
+#include <netinet/in.h>
+#endif
+
+#include "r_types.h"
+
+/* Length of a string hex representation of a MD5 hash */
+#define MAXIFNAME 33
+
+/* Generic transport address
+
+ This spans both sockaddr_in and sockaddr_in6
+ */
+typedef struct nr_transport_addr_ {
+ UCHAR ip_version; /* 4 or 6 */
+#define NR_IPV4 4
+#define NR_IPV6 6
+ UCHAR protocol; /* IPPROTO_TCP, IPPROTO_UDP */
+ union {
+ struct sockaddr_in addr4;
+ struct sockaddr_in6 addr6;
+ } u;
+ char ifname[MAXIFNAME];
+ /* A string version.
+ 56 = 5 ("IP6:[") + 39 (ipv6 address) + 2 ("]:") + 5 (port) + 4 (/UDP) + 1 (null) */
+ char as_string[56];
+ char fqdn[256];
+ bool is_proxied;
+ bool tls;
+} nr_transport_addr;
+
+typedef struct nr_transport_addr_mask_ {
+ UINT4 addr;
+ UINT4 mask;
+} nr_transport_addr_mask;
+
+int nr_sockaddr_to_transport_addr(struct sockaddr *saddr, int protocol, int keep, nr_transport_addr *addr);
+
+// addresses, ports in local byte order
+int nr_ip4_port_to_transport_addr(UINT4 ip4, UINT2 port, int protocol, nr_transport_addr *addr);
+int nr_str_port_to_transport_addr(const char *str, UINT2 port, int protocol, nr_transport_addr *addr);
+int nr_ip6_port_to_transport_addr(struct in6_addr* addr6, UINT2 port, int protocol, nr_transport_addr *addr);
+
+int nr_transport_addr_get_addrstring(const nr_transport_addr *addr, char *str, int maxlen);
+int nr_transport_addr_get_port(const nr_transport_addr *addr, int *port);
+int nr_transport_addr_cmp(const nr_transport_addr *addr1,const nr_transport_addr *addr2,int mode);
+#define NR_TRANSPORT_ADDR_CMP_MODE_VERSION 1
+#define NR_TRANSPORT_ADDR_CMP_MODE_PROTOCOL 2
+#define NR_TRANSPORT_ADDR_CMP_MODE_ADDR 3
+#define NR_TRANSPORT_ADDR_CMP_MODE_ALL 4
+
+int nr_transport_addr_is_wildcard(const nr_transport_addr *addr);
+int nr_transport_addr_is_loopback(const nr_transport_addr *addr);
+int nr_transport_addr_get_private_addr_range(const nr_transport_addr *addr);
+int nr_transport_addr_is_link_local(const nr_transport_addr *addr);
+int nr_transport_addr_is_mac_based(const nr_transport_addr *addr);
+int nr_transport_addr_is_teredo(const nr_transport_addr *addr);
+int nr_transport_addr_check_compatibility(const nr_transport_addr *addr1, const nr_transport_addr *addr2);
+int nr_transport_addr_copy(nr_transport_addr *to, const nr_transport_addr *from);
+int nr_transport_addr_copy_keep_ifname(nr_transport_addr *to, const nr_transport_addr *from);
+/* Copies _just_ the address and port (also handles IP version) */
+int nr_transport_addr_copy_addrport(nr_transport_addr *to, const nr_transport_addr *from);
+int nr_transport_addr_fmt_addr_string(nr_transport_addr *addr);
+int nr_transport_addr_fmt_ifname_addr_string(const nr_transport_addr *addr, char *buf, int len);
+int nr_transport_addr_set_port(nr_transport_addr *addr, int port);
+int nr_transport_addr_is_reliable_transport(const nr_transport_addr *addr);
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/net/transport_addr_reg.c b/dom/media/webrtc/transport/third_party/nICEr/src/net/transport_addr_reg.c
new file mode 100644
index 0000000000..10f93f1947
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/net/transport_addr_reg.c
@@ -0,0 +1,230 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <csi_platform.h>
+#include <stdio.h>
+#include <string.h>
+#include <memory.h>
+#include <sys/types.h>
+#ifdef WIN32
+#include <winsock2.h>
+#else
+#include <strings.h>
+#include <unistd.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#endif
+#include <assert.h>
+#include "nr_api.h"
+#include "util.h"
+#include "transport_addr.h"
+#include "transport_addr_reg.h"
+
+#ifndef INET6_ADDRSTRLEN
+#define INET6_ADDRSTRLEN 46 /* Value used by linux/BSD */
+#endif
+
+int
+nr_reg_get_transport_addr(NR_registry prefix, int keep, nr_transport_addr *addr)
+{
+ int r,_status;
+ unsigned int count;
+ char *address = 0;
+ UINT2 port = 0;
+ char *ifname = 0;
+ char *protocol = 0;
+ int p;
+
+ if ((r=NR_reg_get_child_count(prefix, &count)))
+ ABORT(r);
+
+ if (count == 0)
+ ABORT(R_NOT_FOUND);
+
+ if ((r=NR_reg_alloc2_string(prefix, "address", &address))) {
+ if (r != R_NOT_FOUND)
+ ABORT(r);
+ address = 0;
+ }
+
+ if ((r=NR_reg_alloc2_string(prefix, "ifname", &ifname))) {
+ if (r != R_NOT_FOUND)
+ ABORT(r);
+ ifname = 0;
+ }
+
+ if ((r=NR_reg_get2_uint2(prefix, "port", &port))) {
+ if (r != R_NOT_FOUND)
+ ABORT(r);
+ port = 0;
+ }
+
+ if ((r=NR_reg_alloc2_string(prefix, "protocol", &protocol))) {
+ if (r != R_NOT_FOUND)
+ ABORT(r);
+ p = IPPROTO_UDP;
+
+ protocol = 0;
+ }
+ else {
+ if (!strcasecmp("tcp", protocol))
+ p = IPPROTO_TCP;
+ else if (!strcasecmp("udp", protocol))
+ p = IPPROTO_UDP;
+ else
+ ABORT(R_BAD_DATA);
+ }
+
+ if (!keep) memset(addr, 0, sizeof(*addr));
+
+ if ((r=nr_str_port_to_transport_addr(address?address:"0.0.0.0", port, p, addr)))
+ ABORT(r);
+
+ if (ifname) {
+ (void)strlcpy(addr->ifname, ifname, sizeof(addr->ifname));
+ }
+
+ _status=0;
+ abort:
+ RFREE(protocol);
+ RFREE(ifname);
+ RFREE(address);
+ return(_status);
+}
+
+int
+nr_reg_set_transport_addr(NR_registry prefix, int keep, nr_transport_addr *addr)
+{
+ int r,_status;
+
+ if (! keep) {
+ if ((r=NR_reg_del(prefix)))
+ ABORT(r);
+ }
+
+ switch (addr->ip_version) {
+ case NR_IPV4:
+ if (!nr_transport_addr_is_wildcard(addr)) {
+ if ((r=NR_reg_set2_string(prefix, "address", inet_ntoa(addr->u.addr4.sin_addr))))
+ ABORT(r);
+ }
+
+ if (addr->u.addr4.sin_port != 0) {
+ if ((r=NR_reg_set2_uint2(prefix, "port", ntohs(addr->u.addr4.sin_port))))
+ ABORT(r);
+ }
+ break;
+
+ case NR_IPV6:
+ if (!nr_transport_addr_is_wildcard(addr)) {
+ char address[INET6_ADDRSTRLEN];
+ if(!inet_ntop(AF_INET6, &addr->u.addr6.sin6_addr,address,sizeof(address))) {
+ ABORT(R_BAD_DATA);
+ }
+
+ if ((r=NR_reg_set2_string(prefix, "address", address))) {
+ ABORT(r);
+ }
+ }
+
+ if (addr->u.addr6.sin6_port != 0) {
+ if ((r=NR_reg_set2_uint2(prefix, "port", ntohs(addr->u.addr6.sin6_port))))
+ ABORT(r);
+ }
+ break;
+ default:
+ ABORT(R_INTERNAL);
+ break;
+ }
+
+ /* We abort if neither NR_IPV4 or NR_IPV6 above */
+ switch (addr->protocol) {
+ case IPPROTO_TCP:
+ if ((r=NR_reg_set2_string(prefix, "protocol", "tcp")))
+ ABORT(r);
+ break;
+ case IPPROTO_UDP:
+ if ((r=NR_reg_set2_string(prefix, "protocol", "udp")))
+ ABORT(r);
+ break;
+ default:
+ NR_UNIMPLEMENTED;
+ break;
+ }
+
+ if (strlen(addr->ifname) > 0) {
+ if ((r=NR_reg_set2_string(prefix, "ifname", addr->ifname)))
+ ABORT(r);
+ }
+
+ _status=0;
+ abort:
+ if (_status)
+ NR_reg_del(prefix);
+ return _status;
+}
+
+int
+nr_reg_get_transport_addr2(NR_registry prefix, char *name, int keep, nr_transport_addr *addr)
+{
+ int r, _status;
+ NR_registry registry;
+
+ if ((r=NR_reg_make_registry(prefix, name, registry)))
+ ABORT(r);
+
+ if ((r=nr_reg_get_transport_addr(registry, keep, addr)))
+ ABORT(r);
+
+ _status = 0;
+abort:
+ return _status;
+}
+
+int
+nr_reg_set_transport_addr2(NR_registry prefix, char *name, int keep, nr_transport_addr *addr)
+{
+ int r, _status;
+ NR_registry registry;
+
+ if ((r=NR_reg_make_registry(prefix, name, registry)))
+ ABORT(r);
+
+ if ((r=nr_reg_set_transport_addr(registry, keep, addr)))
+ ABORT(r);
+
+ _status = 0;
+abort:
+ return _status;
+}
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/net/transport_addr_reg.h b/dom/media/webrtc/transport/third_party/nICEr/src/net/transport_addr_reg.h
new file mode 100644
index 0000000000..761953a9ce
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/net/transport_addr_reg.h
@@ -0,0 +1,46 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+
+#ifndef _transport_addr_reg_h
+#define _transport_addr_reg_h
+
+#include "registry.h"
+
+int nr_reg_get_transport_addr(NR_registry prefix, int keep, nr_transport_addr *addr);
+int nr_reg_set_transport_addr(NR_registry prefix, int keep, nr_transport_addr *addr);
+int nr_reg_get_transport_addr2(NR_registry prefix, char *name, int keep, nr_transport_addr *addr);
+int nr_reg_set_transport_addr2(NR_registry prefix, char *name, int keep, nr_transport_addr *addr);
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs-bsd.c b/dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs-bsd.c
new file mode 100644
index 0000000000..ec2d084445
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs-bsd.c
@@ -0,0 +1,110 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if defined(BSD) || defined(DARWIN)
+#include "addrs-bsd.h"
+#include <csi_platform.h>
+#include <assert.h>
+#include <string.h>
+#include "util.h"
+#include "stun_util.h"
+#include "util.h"
+#include <r_macros.h>
+
+#include <sys/types.h> /* getifaddrs */
+#include <ifaddrs.h> /* getifaddrs */
+#include <sys/socket.h>
+#include <sys/ioctl.h>
+#include <errno.h>
+#include <netinet/in.h>
+#include <netinet6/in6_var.h>
+
+static int
+stun_ifaddr_get_v6_flags(struct ifaddrs *ifaddr)
+{
+ if (ifaddr->ifa_addr->sa_family != AF_INET6) {
+ return 0;
+ }
+
+ int flags = 0;
+ int s = socket(AF_INET6, SOCK_DGRAM, 0);
+ if (!s) {
+ r_log(NR_LOG_STUN, LOG_ERR, "socket(AF_INET6, SOCK_DGRAM, 0) failed, errno=%d", errno);
+ assert(0);
+ return 0;
+ }
+ struct in6_ifreq ifr6;
+ memset(&ifr6, 0, sizeof(ifr6));
+ strncpy(ifr6.ifr_name, ifaddr->ifa_name, sizeof(ifr6.ifr_name));
+ /* ifr_addr is a sockaddr_in6, ifa_addr is a sockaddr* */
+ struct sockaddr_in6 *sin6 = (struct sockaddr_in6*)ifaddr->ifa_addr;
+ ifr6.ifr_addr = *sin6;
+ if (ioctl(s, SIOCGIFAFLAG_IN6, &ifr6) != -1) {
+ flags = ifr6.ifr_ifru.ifru_flags6;
+ } else {
+ r_log(NR_LOG_STUN, LOG_ERR, "ioctl(SIOCGIFAFLAG_IN6) failed, errno=%d", errno);
+ assert(0);
+ }
+ close(s);
+ return flags;
+}
+
+static int
+stun_ifaddr_is_disallowed_v6(int flags) {
+ return flags & (IN6_IFF_ANYCAST | IN6_IFF_TENTATIVE | IN6_IFF_DUPLICATED | IN6_IFF_DETACHED | IN6_IFF_DEPRECATED);
+}
+
+int stun_getaddrs_filtered(nr_local_addr addrs[], int maxaddrs, int *count)
+{
+ int r,_status,flags;
+ struct ifaddrs* if_addrs_head=NULL;
+ struct ifaddrs* if_addr;
+
+ *count = 0;
+
+ if (maxaddrs <= 0)
+ ABORT(R_BAD_ARGS);
+
+ if (getifaddrs(&if_addrs_head) == -1) {
+ r_log(NR_LOG_STUN, LOG_ERR, "getifaddrs error e = %d", errno);
+ ABORT(R_INTERNAL);
+ }
+
+ if_addr = if_addrs_head;
+
+ while (if_addr && *count < maxaddrs) {
+ /* This can be null */
+ if (if_addr->ifa_addr) {
+ switch (if_addr->ifa_addr->sa_family) {
+ case AF_INET:
+ case AF_INET6:
+ flags = stun_ifaddr_get_v6_flags(if_addr);
+ if (!stun_ifaddr_is_disallowed_v6(flags)) {
+ if (r=nr_sockaddr_to_transport_addr(if_addr->ifa_addr, IPPROTO_UDP, 0, &(addrs[*count].addr))) {
+ r_log(NR_LOG_STUN, LOG_ERR, "nr_sockaddr_to_transport_addr error r = %d", r);
+ } else {
+ if (flags & IN6_IFF_TEMPORARY) {
+ addrs[*count].flags |= NR_ADDR_FLAG_TEMPORARY;
+ }
+ (void)strlcpy(addrs[*count].addr.ifname, if_addr->ifa_name, sizeof(addrs[*count].addr.ifname));
+ ++(*count);
+ }
+ }
+ break;
+ default:
+ ;
+ }
+ }
+
+ if_addr = if_addr->ifa_next;
+ }
+
+ _status=0;
+abort:
+ if (if_addrs_head) {
+ freeifaddrs(if_addrs_head);
+ }
+ return(_status);
+}
+#endif
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs-bsd.h b/dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs-bsd.h
new file mode 100644
index 0000000000..b575586f48
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs-bsd.h
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef STUN_IFADDRS_BSD_H_
+#define STUN_IFADDRS_BSD_H_
+
+#include "local_addr.h"
+
+int stun_getaddrs_filtered(nr_local_addr addrs[], int maxaddrs, int *count);
+
+#endif /* STUN_IFADDRS_BSD_H_ */
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs-netlink.c b/dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs-netlink.c
new file mode 100644
index 0000000000..8d22e5979e
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs-netlink.c
@@ -0,0 +1,285 @@
+/*
+Copyright (c) 2011, The WebRTC project authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ * Neither the name of Google nor the names of its contributors may
+ be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#if defined(LINUX)
+#include <net/if.h>
+#include "addrs-netlink.h"
+#include <csi_platform.h>
+#include <assert.h>
+#include <string.h>
+#include "util.h"
+#include "stun_util.h"
+#include "util.h"
+#include <r_macros.h>
+
+#include <stdlib.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <sys/utsname.h>
+#include <sys/ioctl.h>
+#include <netinet/in.h>
+#include <unistd.h>
+#include <errno.h>
+#include <linux/netlink.h>
+#include <linux/rtnetlink.h>
+
+#ifdef ANDROID
+/* Work around an Android NDK < r8c bug */
+#undef __unused
+#else
+#include <linux/if.h> /* struct ifreq, IFF_POINTTOPOINT */
+#include <linux/wireless.h> /* struct iwreq */
+#include <linux/ethtool.h> /* struct ethtool_cmd */
+#include <linux/sockios.h> /* SIOCETHTOOL */
+#endif /* ANDROID */
+
+
+struct netlinkrequest {
+ struct nlmsghdr header;
+ struct ifaddrmsg msg;
+};
+
+static const int kMaxReadSize = 4096;
+
+static void set_ifname(nr_local_addr *addr, struct ifaddrmsg* msg) {
+ assert(sizeof(addr->addr.ifname) > IF_NAMESIZE);
+ if_indextoname(msg->ifa_index, addr->addr.ifname);
+}
+
+static int get_siocgifflags(nr_local_addr *addr) {
+ int fd = socket(AF_INET, SOCK_DGRAM, 0);
+ if (fd == -1) {
+ assert(0);
+ return 0;
+ }
+ struct ifreq ifr;
+ memset(&ifr, 0, sizeof(ifr));
+ strncpy(ifr.ifr_name, addr->addr.ifname, IFNAMSIZ - 1);
+ int rc = ioctl(fd, SIOCGIFFLAGS, &ifr);
+ close(fd);
+ if (rc == -1) {
+ assert(0);
+ return 0;
+ }
+ return ifr.ifr_flags;
+}
+
+static int set_sockaddr(nr_local_addr *addr, struct ifaddrmsg* msg, struct rtattr* rta) {
+ assert(rta->rta_type == IFA_ADDRESS || rta->rta_type == IFA_LOCAL);
+ void *data = RTA_DATA(rta);
+ size_t len = RTA_PAYLOAD(rta);
+ if (msg->ifa_family == AF_INET) {
+ struct sockaddr_in sa;
+ memset(&sa, 0, sizeof(struct sockaddr_in));
+ sa.sin_family = AF_INET;
+ memcpy(&sa.sin_addr, data, len);
+ return nr_sockaddr_to_transport_addr((struct sockaddr*)&sa, IPPROTO_UDP, 0, &(addr->addr));
+ } else if (msg->ifa_family == AF_INET6) {
+ struct sockaddr_in6 sa;
+ memset(&sa, 0, sizeof(struct sockaddr_in6));
+ sa.sin6_family = AF_INET6;
+ /* We do not set sin6_scope_id to ifa_index, because that is only valid for
+ * link local addresses, and we don't use those anyway */
+ memcpy(&sa.sin6_addr, data, len);
+ return nr_sockaddr_to_transport_addr((struct sockaddr*)&sa, IPPROTO_UDP, 0, &(addr->addr));
+ }
+
+ return R_BAD_ARGS;
+}
+
+static int
+stun_ifaddr_is_disallowed_v6(int flags) {
+ return flags & (IFA_F_TENTATIVE | IFA_F_OPTIMISTIC | IFA_F_DADFAILED | IFA_F_DEPRECATED);
+}
+
+static int
+stun_convert_netlink(nr_local_addr *addr, struct ifaddrmsg *address_msg, struct rtattr* rta)
+{
+ int r = set_sockaddr(addr, address_msg, rta);
+ if (r) {
+ r_log(NR_LOG_STUN, LOG_ERR, "set_sockaddr error r = %d", r);
+ return r;
+ }
+
+ set_ifname(addr, address_msg);
+
+ if (address_msg->ifa_flags & IFA_F_TEMPORARY) {
+ addr->flags |= NR_ADDR_FLAG_TEMPORARY;
+ }
+
+ int flags = get_siocgifflags(addr);
+ if (flags & IFF_POINTOPOINT)
+ {
+ addr->interface.type = NR_INTERFACE_TYPE_UNKNOWN | NR_INTERFACE_TYPE_VPN;
+ /* TODO (Bug 896913): find backend network type of this VPN */
+ }
+
+#if defined(LINUX) && !defined(ANDROID)
+ struct ethtool_cmd ecmd;
+ struct ifreq ifr;
+ struct iwreq wrq;
+ int e;
+ int s = socket(AF_INET, SOCK_DGRAM, 0);
+
+ strncpy(ifr.ifr_name, addr->addr.ifname, sizeof(ifr.ifr_name));
+ /* TODO (Bug 896851): interface property for Android */
+ /* Getting ethtool for ethernet information. */
+ ecmd.cmd = ETHTOOL_GSET;
+ /* In/out param */
+ ifr.ifr_data = (void*)&ecmd;
+
+ e = ioctl(s, SIOCETHTOOL, &ifr);
+ if (e == 0)
+ {
+ /* For wireless network, we won't get ethtool, it's a wired
+ * connection */
+ addr->interface.type = NR_INTERFACE_TYPE_WIRED;
+#ifdef DONT_HAVE_ETHTOOL_SPEED_HI
+ addr->interface.estimated_speed = ecmd.speed;
+#else
+ addr->interface.estimated_speed = ((ecmd.speed_hi << 16) | ecmd.speed) * 1000;
+#endif
+ }
+
+ strncpy(wrq.ifr_name, addr->addr.ifname, sizeof(wrq.ifr_name));
+ e = ioctl(s, SIOCGIWRATE, &wrq);
+ if (e == 0)
+ {
+ addr->interface.type = NR_INTERFACE_TYPE_WIFI;
+ addr->interface.estimated_speed = wrq.u.bitrate.value / 1000;
+ }
+
+ close(s);
+
+#else
+ addr->interface.type = NR_INTERFACE_TYPE_UNKNOWN;
+ addr->interface.estimated_speed = 0;
+#endif
+ return 0;
+}
+
+int
+stun_getaddrs_filtered(nr_local_addr addrs[], int maxaddrs, int *count)
+{
+ int _status;
+ int fd = 0;
+
+ /* Scope everything else since we're using ABORT. */
+ {
+ *count = 0;
+
+ if (maxaddrs <= 0)
+ ABORT(R_BAD_ARGS);
+
+ fd = socket(PF_NETLINK, SOCK_RAW, NETLINK_ROUTE);
+ if (fd < 0) {
+ ABORT(R_INTERNAL);
+ }
+
+ struct netlinkrequest ifaddr_request;
+ memset(&ifaddr_request, 0, sizeof(ifaddr_request));
+ ifaddr_request.header.nlmsg_flags = NLM_F_ROOT | NLM_F_REQUEST;
+ ifaddr_request.header.nlmsg_type = RTM_GETADDR;
+ ifaddr_request.header.nlmsg_len = NLMSG_LENGTH(sizeof(struct ifaddrmsg));
+
+ ssize_t bytes = send(fd, &ifaddr_request, ifaddr_request.header.nlmsg_len, 0);
+ if ((size_t)bytes != ifaddr_request.header.nlmsg_len) {
+ ABORT(R_INTERNAL);
+ }
+
+ char buf[kMaxReadSize];
+ ssize_t amount_read = recv(fd, &buf, kMaxReadSize, 0);
+ while ((amount_read > 0) && (*count != maxaddrs)) {
+ struct nlmsghdr* header = (struct nlmsghdr*)&buf[0];
+ size_t header_size = (size_t)amount_read;
+ for ( ; NLMSG_OK(header, header_size) && (*count != maxaddrs);
+ header = NLMSG_NEXT(header, header_size)) {
+ switch (header->nlmsg_type) {
+ case NLMSG_DONE:
+ /* Success. Return. */
+ close(fd);
+ return 0;
+ case NLMSG_ERROR:
+ ABORT(R_INTERNAL);
+ case RTM_NEWADDR: {
+ struct ifaddrmsg* address_msg =
+ (struct ifaddrmsg*)NLMSG_DATA(header);
+ struct rtattr* rta = IFA_RTA(address_msg);
+ ssize_t payload_len = IFA_PAYLOAD(header);
+ bool found = false;
+ while (RTA_OK(rta, payload_len)) {
+ // This is a bit convoluted. IFA_ADDRESS and IFA_LOCAL are the
+ // same thing except when using a POINTTOPOINT interface, in
+ // which case IFA_LOCAL is the local address, and IFA_ADDRESS is
+ // the remote address. In a reasonable world, that would mean we
+ // could just use IFA_LOCAL all the time. Sadly, IFA_LOCAL is not
+ // always set (IPv6 in particular). So, we have to be on the
+ // lookout for both, and prefer IFA_LOCAL when present.
+ if (rta->rta_type == IFA_ADDRESS || rta->rta_type == IFA_LOCAL) {
+ int family = address_msg->ifa_family;
+ if ((family == AF_INET || family == AF_INET6) &&
+ !stun_ifaddr_is_disallowed_v6(address_msg->ifa_flags) &&
+ !stun_convert_netlink(&addrs[*count], address_msg, rta)) {
+ found = true;
+ if (rta->rta_type == IFA_LOCAL) {
+ // IFA_LOCAL is what we really want; if we find it we're
+ // done. If this is IFA_ADDRESS instead, we do not proceed
+ // yet, and allow a subsequent IFA_LOCAL to overwrite what
+ // we just put in |addrs|.
+ break;
+ }
+ }
+ }
+ /* TODO: Use IFA_LABEL instead of if_indextoname? We would need
+ * to remember how many nr_local_addr we've converted for this
+ * ifaddrmsg, and set the label on all of them. */
+ rta = RTA_NEXT(rta, payload_len);
+ }
+
+ if (found) {
+ ++(*count);
+ }
+ break;
+ }
+ }
+ }
+ amount_read = recv(fd, &buf, kMaxReadSize, 0);
+ }
+ }
+
+ _status=0;
+abort:
+ close(fd);
+ return(_status);
+}
+
+#endif /* defined(LINUX) */
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs-netlink.h b/dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs-netlink.h
new file mode 100644
index 0000000000..b2b07bddc9
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs-netlink.h
@@ -0,0 +1,45 @@
+/*
+Copyright (c) 2011, The WebRTC project authors. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in
+ the documentation and/or other materials provided with the
+ distribution.
+
+ * Neither the name of Google nor the names of its contributors may
+ be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#ifndef WEBRTC_BASE_IFADDRS_NETLINK_H_
+#define WEBRTC_BASE_IFADDRS_NETLINK_H_
+
+#include <stdio.h>
+#include <sys/socket.h>
+#include "local_addr.h"
+
+/* for platforms with netlink (android and linux) */
+/* Filters out things like deprecated addresses, and stuff that doesn't pass
+ * IPv6 duplicate address detection */
+int stun_getaddrs_filtered(nr_local_addr addrs[], int maxaddrs, int *count);
+#endif /* WEBRTC_BASE_IFADDRS_NETLINK_H_ */
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs-win32.c b/dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs-win32.c
new file mode 100644
index 0000000000..4fdf5e5d44
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs-win32.c
@@ -0,0 +1,210 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifdef WIN32
+
+#include "addrs-win32.h"
+#include <csi_platform.h>
+#include <assert.h>
+#include <string.h>
+#include "util.h"
+#include "stun_util.h"
+#include "util.h"
+#include <r_macros.h>
+#include "nr_crypto.h"
+
+#include <winsock2.h>
+#include <iphlpapi.h>
+#include <tchar.h>
+
+#define WIN32_MAX_NUM_INTERFACES 20
+
+#define NR_MD5_HASH_LENGTH 16
+
+#define _NR_MAX_KEY_LENGTH 256
+#define _NR_MAX_NAME_LENGTH 512
+
+#define _ADAPTERS_BASE_REG "SYSTEM\\CurrentControlSet\\Control\\Network\\{4D36E972-E325-11CE-BFC1-08002BE10318}"
+
+static int nr_win32_get_adapter_friendly_name(char *adapter_GUID, char **friendly_name)
+{
+ int r,_status;
+ HKEY adapter_reg;
+ TCHAR adapter_key[_NR_MAX_KEY_LENGTH];
+ TCHAR keyval_buf[_NR_MAX_KEY_LENGTH];
+ TCHAR adapter_GUID_tchar[_NR_MAX_NAME_LENGTH];
+ DWORD keyval_len, key_type;
+ size_t converted_chars, newlen;
+ char *my_fn = 0;
+
+#ifdef _UNICODE
+ mbstowcs_s(&converted_chars, adapter_GUID_tchar, strlen(adapter_GUID)+1,
+ adapter_GUID, _TRUNCATE);
+#else
+ strlcpy(adapter_GUID_tchar, adapter_GUID, _NR_MAX_NAME_LENGTH);
+#endif
+
+ _tcscpy_s(adapter_key, _NR_MAX_KEY_LENGTH, TEXT(_ADAPTERS_BASE_REG));
+ _tcscat_s(adapter_key, _NR_MAX_KEY_LENGTH, TEXT("\\"));
+ _tcscat_s(adapter_key, _NR_MAX_KEY_LENGTH, adapter_GUID_tchar);
+ _tcscat_s(adapter_key, _NR_MAX_KEY_LENGTH, TEXT("\\Connection"));
+
+ r = RegOpenKeyEx(HKEY_LOCAL_MACHINE, adapter_key, 0, KEY_READ, &adapter_reg);
+
+ if (r != ERROR_SUCCESS) {
+ r_log(NR_LOG_STUN, LOG_ERR, "Got error %d opening adapter reg key\n", r);
+ ABORT(R_INTERNAL);
+ }
+
+ keyval_len = sizeof(keyval_buf);
+ r = RegQueryValueEx(adapter_reg, TEXT("Name"), NULL, &key_type,
+ (BYTE *)keyval_buf, &keyval_len);
+
+ RegCloseKey(adapter_reg);
+
+#ifdef UNICODE
+ newlen = wcslen(keyval_buf)+1;
+ my_fn = (char *) RCALLOC(newlen);
+ if (!my_fn) {
+ ABORT(R_NO_MEMORY);
+ }
+ wcstombs_s(&converted_chars, my_fn, newlen, keyval_buf, _TRUNCATE);
+#else
+ my_fn = r_strdup(keyval_buf);
+#endif
+
+ *friendly_name = my_fn;
+ _status=0;
+
+abort:
+ if (_status) {
+ if (my_fn) free(my_fn);
+ }
+ return(_status);
+}
+
+static int stun_win32_address_disallowed(IP_ADAPTER_UNICAST_ADDRESS *addr)
+{
+ return (addr->DadState != NldsPreferred) &&
+ (addr->DadState != IpDadStatePreferred);
+}
+
+static int stun_win32_address_temp_v6(IP_ADAPTER_UNICAST_ADDRESS *addr)
+{
+ return (addr->Address.lpSockaddr->sa_family == AF_INET6) &&
+ (addr->SuffixOrigin == IpSuffixOriginRandom);
+}
+
+int
+stun_getaddrs_filtered(nr_local_addr addrs[], int maxaddrs, int *count)
+{
+ int r, _status;
+ PIP_ADAPTER_ADDRESSES AdapterAddresses = NULL, tmpAddress = NULL;
+ // recomended per https://msdn.microsoft.com/en-us/library/windows/desktop/aa365915(v=vs.85).aspx
+ static const ULONG initialBufLen = 15000;
+ ULONG buflen = initialBufLen;
+ char bin_hashed_ifname[NR_MD5_HASH_LENGTH];
+ char hex_hashed_ifname[MAXIFNAME];
+ int n = 0;
+
+ *count = 0;
+
+ if (maxaddrs <= 0)
+ ABORT(R_BAD_ARGS);
+
+ /* According to MSDN (see above) we have try GetAdapterAddresses() multiple times */
+ for (n = 0; n < 5; n++) {
+ AdapterAddresses = (PIP_ADAPTER_ADDRESSES) RMALLOC(buflen);
+ if (AdapterAddresses == NULL) {
+ r_log(NR_LOG_STUN, LOG_ERR, "Error allocating buf for GetAdaptersAddresses()");
+ ABORT(R_NO_MEMORY);
+ }
+
+ r = GetAdaptersAddresses(AF_UNSPEC, GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_SKIP_DNS_SERVER, NULL, AdapterAddresses, &buflen);
+ if (r == NO_ERROR) {
+ break;
+ }
+ r_log(NR_LOG_STUN, LOG_ERR, "GetAdaptersAddresses() returned error (%d)", r);
+ RFREE(AdapterAddresses);
+ AdapterAddresses = NULL;
+ }
+
+ if (n >= 5) {
+ r_log(NR_LOG_STUN, LOG_ERR, "5 failures calling GetAdaptersAddresses()");
+ ABORT(R_INTERNAL);
+ }
+
+ n = 0;
+
+ /* Loop through the adapters */
+
+ for (tmpAddress = AdapterAddresses; tmpAddress != NULL; tmpAddress = tmpAddress->Next) {
+
+ if (tmpAddress->OperStatus != IfOperStatusUp)
+ continue;
+
+ if ((tmpAddress->IfIndex != 0) || (tmpAddress->Ipv6IfIndex != 0)) {
+ IP_ADAPTER_UNICAST_ADDRESS *u = 0;
+
+ if(r=nr_crypto_md5((UCHAR *)tmpAddress->FriendlyName,
+ wcslen(tmpAddress->FriendlyName) * sizeof(wchar_t),
+ bin_hashed_ifname))
+ ABORT(r);
+ if(r=nr_bin2hex(bin_hashed_ifname, sizeof(bin_hashed_ifname),
+ hex_hashed_ifname))
+ ABORT(r);
+
+ for (u = tmpAddress->FirstUnicastAddress; u != 0; u = u->Next) {
+ SOCKET_ADDRESS *sa_addr = &u->Address;
+
+ if ((sa_addr->lpSockaddr->sa_family != AF_INET) &&
+ (sa_addr->lpSockaddr->sa_family != AF_INET6)) {
+ r_log(NR_LOG_STUN, LOG_DEBUG, "Unrecognized sa_family for address on adapter %lu", tmpAddress->IfIndex);
+ continue;
+ }
+
+ if (stun_win32_address_disallowed(u)) {
+ continue;
+ }
+
+ if ((r=nr_sockaddr_to_transport_addr((struct sockaddr*)sa_addr->lpSockaddr, IPPROTO_UDP, 0, &(addrs[n].addr)))) {
+ ABORT(r);
+ }
+
+ strlcpy(addrs[n].addr.ifname, hex_hashed_ifname, sizeof(addrs[n].addr.ifname));
+ if (tmpAddress->IfType == IF_TYPE_ETHERNET_CSMACD) {
+ addrs[n].interface.type = NR_INTERFACE_TYPE_WIRED;
+ } else if (tmpAddress->IfType == IF_TYPE_IEEE80211) {
+ /* Note: this only works for >= Win Vista */
+ addrs[n].interface.type = NR_INTERFACE_TYPE_WIFI;
+ } else {
+ addrs[n].interface.type = NR_INTERFACE_TYPE_UNKNOWN;
+ }
+#if (_WIN32_WINNT >= 0x0600)
+ /* Note: only >= Vista provide link speed information */
+ addrs[n].interface.estimated_speed = tmpAddress->TransmitLinkSpeed / 1000;
+#else
+ addrs[n].interface.estimated_speed = 0;
+#endif
+ if (stun_win32_address_temp_v6(u)) {
+ addrs[n].flags |= NR_ADDR_FLAG_TEMPORARY;
+ }
+
+ if (++n >= maxaddrs)
+ goto done;
+ }
+ }
+ }
+
+ done:
+ *count = n;
+ _status = 0;
+
+ abort:
+ RFREE(AdapterAddresses);
+ return _status;
+}
+
+#endif //WIN32
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs-win32.h b/dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs-win32.h
new file mode 100644
index 0000000000..a00802192a
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs-win32.h
@@ -0,0 +1,13 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef STUN_IFADDRS_WIN32_H_
+#define STUN_IFADDRS_WIN32_H_
+
+#include "local_addr.h"
+
+int stun_getaddrs_filtered(nr_local_addr addrs[], int maxaddrs, int *count);
+
+#endif /* STUN_IFADDRS_WIN32_H_ */
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs.c b/dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs.c
new file mode 100644
index 0000000000..362b7d828e
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs.c
@@ -0,0 +1,176 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <csi_platform.h>
+#include <assert.h>
+#include <string.h>
+
+#ifdef WIN32
+#include "addrs-win32.h"
+#elif defined(BSD) || defined(DARWIN)
+#include "addrs-bsd.h"
+#else
+#include "addrs-netlink.h"
+#endif
+
+#include "stun.h"
+#include "addrs.h"
+#include "util.h"
+
+static int
+nr_stun_is_duplicate_addr(nr_local_addr addrs[], int count, nr_local_addr *addr)
+{
+ int i;
+ int different;
+
+ for (i = 0; i < count; ++i) {
+ different = nr_transport_addr_cmp(&addrs[i].addr, &(addr->addr),
+ NR_TRANSPORT_ADDR_CMP_MODE_ALL);
+ if (!different)
+ return 1; /* duplicate */
+ }
+
+ return 0;
+}
+
+int
+nr_stun_filter_addrs(nr_local_addr addrs[], int remove_loopback, int remove_link_local, int *count)
+{
+ int r, _status;
+ nr_local_addr *tmp = 0;
+ int i;
+ int n;
+ /* We prefer temp ipv6 for their privacy properties. If we cannot get
+ * that, we prefer ipv6 that are not based on mac address. */
+ int filter_mac_ipv6 = 0;
+ int filter_teredo_ipv6 = 0;
+ int filter_non_temp_ipv6 = 0;
+
+ tmp = RMALLOC(*count * sizeof(*tmp));
+ if (!tmp)
+ ABORT(R_NO_MEMORY);
+
+ for (i = 0; i < *count; ++i) {
+ if (addrs[i].addr.ip_version == NR_IPV6) {
+ if (nr_transport_addr_is_teredo(&addrs[i].addr)) {
+ addrs[i].interface.type |= NR_INTERFACE_TYPE_TEREDO;
+ /* Prefer teredo over mac-based address. Probably will never see
+ * both. */
+ filter_mac_ipv6 = 1;
+ } else {
+ filter_teredo_ipv6 = 1;
+ }
+
+ if (!nr_transport_addr_is_mac_based(&addrs[i].addr)) {
+ filter_mac_ipv6 = 1;
+ }
+
+ if (addrs[i].flags & NR_ADDR_FLAG_TEMPORARY) {
+ filter_non_temp_ipv6 = 1;
+ }
+ }
+ }
+
+ n = 0;
+ for (i = 0; i < *count; ++i) {
+ if (nr_stun_is_duplicate_addr(tmp, n, &addrs[i])) {
+ /* skip addrs[i], it's a duplicate */
+ }
+ else if (remove_loopback && nr_transport_addr_is_loopback(&addrs[i].addr)) {
+ /* skip addrs[i], it's a loopback */
+ }
+ else if (remove_link_local &&
+ nr_transport_addr_is_link_local(&addrs[i].addr)) {
+ /* skip addrs[i], it's a link-local address */
+ }
+ else if (filter_mac_ipv6 &&
+ nr_transport_addr_is_mac_based(&addrs[i].addr)) {
+ /* skip addrs[i], it's MAC based */
+ }
+ else if (filter_teredo_ipv6 &&
+ nr_transport_addr_is_teredo(&addrs[i].addr)) {
+ /* skip addrs[i], it's a Teredo address */
+ }
+ else if (filter_non_temp_ipv6 &&
+ (addrs[i].addr.ip_version == NR_IPV6) &&
+ !(addrs[i].flags & NR_ADDR_FLAG_TEMPORARY)) {
+ /* skip addrs[i], it's a non-temporary ipv6, and we have a temporary */
+ }
+ else {
+ /* otherwise, copy it to the temporary array */
+ if ((r=nr_local_addr_copy(&tmp[n], &addrs[i])))
+ ABORT(r);
+ ++n;
+ }
+ }
+
+ *count = n;
+
+ memset(addrs, 0, *count * sizeof(*addrs));
+ /* copy temporary array into passed in/out array */
+ for (i = 0; i < *count; ++i) {
+ if ((r=nr_local_addr_copy(&addrs[i], &tmp[i])))
+ ABORT(r);
+ }
+
+ _status = 0;
+ abort:
+ RFREE(tmp);
+ return _status;
+}
+
+#ifndef USE_PLATFORM_NR_STUN_GET_ADDRS
+
+int
+nr_stun_get_addrs(nr_local_addr addrs[], int maxaddrs, int *count)
+{
+ int _status=0;
+ int i;
+ char typestr[100];
+
+ // Ensure output records are always fully defined. See bug 1589990.
+ if (maxaddrs > 0) {
+ memset(addrs, 0, maxaddrs * sizeof(nr_local_addr));
+ }
+
+ _status = stun_getaddrs_filtered(addrs, maxaddrs, count);
+
+ for (i = 0; i < *count; ++i) {
+ nr_local_addr_fmt_info_string(addrs+i,typestr,sizeof(typestr));
+ r_log(NR_LOG_STUN, LOG_DEBUG, "Address %d: %s on %s, type: %s\n",
+ i,addrs[i].addr.as_string,addrs[i].addr.ifname,typestr);
+ }
+
+ return _status;
+}
+
+#endif
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs.h b/dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs.h
new file mode 100644
index 0000000000..522bd92fd5
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/stun/addrs.h
@@ -0,0 +1,43 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+#ifndef _addrs_h_
+#define _addrs_h_
+
+#include "transport_addr.h"
+#include "local_addr.h"
+
+int nr_stun_get_addrs(nr_local_addr addrs[], int maxaddrs, int *count);
+int nr_stun_filter_addrs(nr_local_addr addrs[], int remove_loopback, int remove_link_local, int *count);
+
+#endif
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/stun/nr_socket_buffered_stun.c b/dom/media/webrtc/transport/third_party/nICEr/src/stun/nr_socket_buffered_stun.c
new file mode 100644
index 0000000000..4b5b92fb75
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/stun/nr_socket_buffered_stun.c
@@ -0,0 +1,656 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+Copyright (c) 2013, Mozilla
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <nr_api.h>
+
+#include <stdio.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/queue.h>
+#include <assert.h>
+#include <inttypes.h>
+
+#include "p_buf.h"
+#include "nr_socket.h"
+#include "stun.h"
+#include "nr_socket_buffered_stun.h"
+
+#define NR_MAX_FRAME_SIZE 0xFFFF
+
+typedef struct nr_frame_header_ {
+ UINT2 frame_length;
+ char data[0];
+} nr_frame_header;
+
+typedef struct nr_socket_buffered_stun_ {
+ nr_socket *inner;
+ nr_transport_addr remote_addr;
+ int connected;
+
+ /* Read state */
+ int read_state;
+#define NR_ICE_SOCKET_READ_NONE 0
+#define NR_ICE_SOCKET_READ_HDR 1
+#define NR_ICE_SOCKET_READ_FAILED 2
+ UCHAR *buffer;
+ size_t buffer_size;
+ size_t bytes_needed;
+ size_t bytes_read;
+ NR_async_cb readable_cb;
+ void *readable_cb_arg;
+
+ /* Write state */
+ nr_p_buf_ctx *p_bufs;
+ nr_p_buf_head pending_writes;
+ size_t pending;
+ size_t max_pending;
+ nr_framing_type framing_type;
+} nr_socket_buffered_stun;
+
+static int nr_socket_buffered_stun_destroy(void **objp);
+static int nr_socket_buffered_stun_sendto(void *obj,const void *msg, size_t len,
+ int flags, const nr_transport_addr *to);
+static int nr_socket_buffered_stun_recvfrom(void *obj,void * restrict buf,
+ size_t maxlen, size_t *len, int flags, nr_transport_addr *from);
+static int nr_socket_buffered_stun_getfd(void *obj, NR_SOCKET *fd);
+static int nr_socket_buffered_stun_getaddr(void *obj, nr_transport_addr *addrp);
+static int nr_socket_buffered_stun_close(void *obj);
+static int nr_socket_buffered_stun_connect(void *sock, const nr_transport_addr *addr);
+static int nr_socket_buffered_stun_write(void *obj,const void *msg, size_t len, size_t *written);
+static void nr_socket_buffered_stun_writable_cb(NR_SOCKET s, int how, void *arg);
+static int nr_socket_buffered_stun_listen(void *obj, int backlog);
+static int nr_socket_buffered_stun_accept(void *obj, nr_transport_addr *addrp, nr_socket **sockp);
+
+static nr_socket_vtbl nr_socket_buffered_stun_vtbl={
+ 2,
+ nr_socket_buffered_stun_destroy,
+ nr_socket_buffered_stun_sendto,
+ nr_socket_buffered_stun_recvfrom,
+ nr_socket_buffered_stun_getfd,
+ nr_socket_buffered_stun_getaddr,
+ nr_socket_buffered_stun_connect,
+ 0,
+ 0,
+ nr_socket_buffered_stun_close,
+ nr_socket_buffered_stun_listen,
+ nr_socket_buffered_stun_accept
+};
+
+void nr_socket_buffered_stun_set_readable_cb(nr_socket *sock,
+ NR_async_cb readable_cb, void *readable_cb_arg)
+{
+ nr_socket_buffered_stun *buf_sock = (nr_socket_buffered_stun *)sock->obj;
+
+ buf_sock->readable_cb = readable_cb;
+ buf_sock->readable_cb_arg = readable_cb_arg;
+}
+
+int nr_socket_buffered_set_connected_to(nr_socket *sock, nr_transport_addr *remote_addr)
+{
+ nr_socket_buffered_stun *buf_sock = (nr_socket_buffered_stun *)sock->obj;
+ int r, _status;
+
+ if ((r=nr_transport_addr_copy(&buf_sock->remote_addr, remote_addr)))
+ ABORT(r);
+
+ buf_sock->connected = 1;
+
+ _status=0;
+abort:
+ return(_status);
+}
+
+int nr_socket_buffered_stun_create(nr_socket *inner, int max_pending,
+ nr_framing_type framing_type, nr_socket **sockp)
+{
+ int r, _status;
+ nr_socket_buffered_stun *sock = 0;
+ size_t frame_size;
+
+ if (!(sock = RCALLOC(sizeof(nr_socket_buffered_stun))))
+ ABORT(R_NO_MEMORY);
+
+ sock->inner = inner;
+ sock->framing_type = framing_type;
+
+ if ((r=nr_ip4_port_to_transport_addr(INADDR_ANY, 0, IPPROTO_TCP, &sock->remote_addr)))
+ ABORT(r);
+
+ switch (framing_type) {
+ case ICE_TCP_FRAMING:
+ frame_size = sizeof(nr_frame_header);
+ sock->buffer_size = sizeof(nr_frame_header) + NR_MAX_FRAME_SIZE;
+ sock->bytes_needed = sizeof(nr_frame_header);
+ break;
+ case TURN_TCP_FRAMING:
+ frame_size = 0;
+ sock->buffer_size = NR_STUN_MAX_MESSAGE_SIZE;
+ sock->bytes_needed = sizeof(nr_stun_message_header);
+ break;
+ default:
+ assert(0);
+ ABORT(R_BAD_ARGS);
+ }
+
+ /* TODO(ekr@rtfm.com): Check this */
+ if (!(sock->buffer = RMALLOC(sock->buffer_size)))
+ ABORT(R_NO_MEMORY);
+
+ sock->read_state = NR_ICE_SOCKET_READ_NONE;
+ sock->connected = 0;
+
+ STAILQ_INIT(&sock->pending_writes);
+ if ((r=nr_p_buf_ctx_create(sock->buffer_size, &sock->p_bufs)))
+ ABORT(r);
+ sock->max_pending = max_pending + frame_size;
+
+ if ((r=nr_socket_create_int(sock, &nr_socket_buffered_stun_vtbl, sockp)))
+ ABORT(r);
+
+ _status=0;
+abort:
+ if (_status && sock) {
+ void *sock_v = sock;
+ sock->inner = 0; /* Give up ownership so we don't destroy */
+ nr_socket_buffered_stun_destroy(&sock_v);
+ }
+ return(_status);
+}
+
+/* Note: This destroys the inner socket */
+int nr_socket_buffered_stun_destroy(void **objp)
+{
+ nr_socket_buffered_stun *sock;
+ NR_SOCKET fd;
+
+ if (!objp || !*objp)
+ return 0;
+
+ sock = (nr_socket_buffered_stun *)*objp;
+ *objp = 0;
+
+ /* Free the buffer if needed */
+ RFREE(sock->buffer);
+
+ /* Cancel waiting on the socket */
+ if (sock->inner && !nr_socket_getfd(sock->inner, &fd)) {
+ NR_ASYNC_CANCEL(fd, NR_ASYNC_WAIT_WRITE);
+ NR_ASYNC_CANCEL(fd, NR_ASYNC_WAIT_READ);
+ }
+
+ nr_p_buf_free_chain(sock->p_bufs, &sock->pending_writes);
+ nr_p_buf_ctx_destroy(&sock->p_bufs);
+ nr_socket_destroy(&sock->inner);
+ RFREE(sock);
+
+ return 0;
+}
+
+static int nr_socket_buffered_stun_sendto(void *obj,const void *msg, size_t len,
+ int flags, const nr_transport_addr *to)
+{
+ nr_socket_buffered_stun *sock = (nr_socket_buffered_stun *)obj;
+ int r, _status;
+ size_t written;
+ nr_frame_header *frame = NULL;
+
+ /* Check that we are writing to the connected address if
+ connected */
+ if (!nr_transport_addr_is_wildcard(&sock->remote_addr)) {
+ if (nr_transport_addr_cmp(&sock->remote_addr, to, NR_TRANSPORT_ADDR_CMP_MODE_ALL)) {
+ r_log(LOG_GENERIC, LOG_ERR, "Sendto on connected socket doesn't match");
+ ABORT(R_BAD_DATA);
+ }
+ }
+
+ if (sock->framing_type == ICE_TCP_FRAMING) {
+
+ assert(len <= NR_MAX_FRAME_SIZE);
+ if (len > NR_MAX_FRAME_SIZE)
+ ABORT(R_FAILED);
+
+ if (!(frame = RMALLOC(len + sizeof(nr_frame_header))))
+ ABORT(R_NO_MEMORY);
+
+ frame->frame_length = htons(len);
+ memcpy(frame->data, msg, len);
+ len += sizeof(nr_frame_header);
+ msg = frame;
+ }
+
+ if ((r=nr_socket_buffered_stun_write(obj, msg, len, &written)))
+ ABORT(r);
+
+ if (len != written)
+ ABORT(R_IO_ERROR);
+
+ _status=0;
+abort:
+ RFREE(frame);
+ return _status;
+}
+
+static void nr_socket_buffered_stun_failed(nr_socket_buffered_stun *sock)
+ {
+ NR_SOCKET fd;
+
+ sock->read_state = NR_ICE_SOCKET_READ_FAILED;
+
+ /* Cancel waiting on the socket */
+ if (sock->inner && !nr_socket_getfd(sock->inner, &fd)) {
+ NR_ASYNC_CANCEL(fd, NR_ASYNC_WAIT_WRITE);
+ NR_ASYNC_CANCEL(fd, NR_ASYNC_WAIT_READ);
+ }
+ }
+
+static int nr_socket_buffered_stun_recvfrom(void *obj,void * restrict buf,
+ size_t maxlen, size_t *len, int flags, nr_transport_addr *from)
+{
+ int r, _status;
+ size_t bytes_read;
+ nr_socket_buffered_stun *sock = (nr_socket_buffered_stun *)obj;
+ nr_frame_header *frame = (nr_frame_header *)sock->buffer;
+ size_t skip_hdr_size = (sock->framing_type == ICE_TCP_FRAMING) ? sizeof(nr_frame_header) : 0;
+
+ if (sock->read_state == NR_ICE_SOCKET_READ_FAILED) {
+ ABORT(R_FAILED);
+ }
+
+ while (sock->bytes_needed) {
+ /* Read all the expected bytes */
+ assert(sock->bytes_needed <= sock->buffer_size - sock->bytes_read);
+
+ if(r=nr_socket_read(sock->inner,
+ sock->buffer + sock->bytes_read,
+ sock->bytes_needed, &bytes_read, 0))
+ ABORT(r);
+
+ assert(bytes_read <= sock->bytes_needed);
+ sock->bytes_needed -= bytes_read;
+ sock->bytes_read += bytes_read;
+
+ /* Unfinished */
+ if (sock->bytes_needed)
+ ABORT(R_WOULDBLOCK);
+
+ /* No more bytes expected */
+ if (sock->read_state == NR_ICE_SOCKET_READ_NONE) {
+ size_t remaining_length;
+ if (sock->framing_type == ICE_TCP_FRAMING) {
+ if (sock->bytes_read < sizeof(nr_frame_header))
+ ABORT(R_BAD_DATA);
+ remaining_length = ntohs(frame->frame_length);
+ } else {
+ int tmp_length;
+
+ /* Parse the header */
+ if (r = nr_stun_message_length(sock->buffer, sock->bytes_read, &tmp_length))
+ ABORT(r);
+ assert(tmp_length >= 0);
+ if (tmp_length < 0)
+ ABORT(R_BAD_DATA);
+ remaining_length = tmp_length;
+
+ }
+ /* Check to see if we have enough room */
+ if ((sock->buffer_size - sock->bytes_read) < remaining_length)
+ ABORT(R_BAD_DATA);
+
+ sock->read_state = NR_ICE_SOCKET_READ_HDR;
+ /* Set ourselves up to read the rest of the data */
+ sock->bytes_needed = remaining_length;
+ }
+ }
+
+ assert(skip_hdr_size <= sock->bytes_read);
+ if (skip_hdr_size > sock->bytes_read)
+ ABORT(R_BAD_DATA);
+ sock->bytes_read -= skip_hdr_size;
+
+ if (maxlen < sock->bytes_read)
+ ABORT(R_BAD_ARGS);
+
+ *len = sock->bytes_read;
+ memcpy(buf, sock->buffer + skip_hdr_size, sock->bytes_read);
+
+ sock->bytes_read = 0;
+ sock->read_state = NR_ICE_SOCKET_READ_NONE;
+ sock->bytes_needed = (sock->framing_type == ICE_TCP_FRAMING) ? sizeof(nr_frame_header) : sizeof(nr_stun_message_header);
+
+ if ((r = nr_transport_addr_copy(from, &sock->remote_addr))) ABORT(r);
+
+ _status=0;
+abort:
+ if (_status && (_status != R_WOULDBLOCK)) {
+ nr_socket_buffered_stun_failed(sock);
+ }
+
+ return(_status);
+}
+
+static int nr_socket_buffered_stun_getfd(void *obj, NR_SOCKET *fd)
+{
+ nr_socket_buffered_stun *sock = (nr_socket_buffered_stun *)obj;
+
+ return nr_socket_getfd(sock->inner, fd);
+}
+
+static int nr_socket_buffered_stun_getaddr(void *obj, nr_transport_addr *addrp)
+{
+ nr_socket_buffered_stun *sock = (nr_socket_buffered_stun *)obj;
+
+ return nr_socket_getaddr(sock->inner, addrp);
+}
+
+static int nr_socket_buffered_stun_close(void *obj)
+{
+ nr_socket_buffered_stun *sock = (nr_socket_buffered_stun *)obj;
+ NR_SOCKET fd;
+
+ /* Cancel waiting on the socket */
+ if (sock->inner && !nr_socket_getfd(sock->inner, &fd)) {
+ NR_ASYNC_CANCEL(fd, NR_ASYNC_WAIT_WRITE);
+ }
+
+ return nr_socket_close(sock->inner);
+}
+
+static int nr_socket_buffered_stun_listen(void *obj, int backlog)
+{
+ int r, _status;
+ nr_socket_buffered_stun *sock = (nr_socket_buffered_stun *)obj;
+
+ if (!sock->inner)
+ ABORT(R_FAILED);
+
+ if ((r=nr_socket_listen(sock->inner, backlog)))
+ ABORT(r);
+
+ _status=0;
+abort:
+ return(_status);
+}
+
+
+static int nr_socket_buffered_stun_accept(void *obj, nr_transport_addr *addrp, nr_socket **sockp)
+{
+ nr_socket_buffered_stun *bsock = (nr_socket_buffered_stun *)obj;
+
+ return nr_socket_accept(bsock->inner, addrp, sockp);
+}
+
+static void nr_socket_buffered_stun_connected_cb(NR_SOCKET s, int how, void *arg)
+{
+ nr_socket_buffered_stun *sock = (nr_socket_buffered_stun *)arg;
+ int r, _status;
+ NR_SOCKET fd;
+
+ assert(!sock->connected);
+
+ sock->connected = 1;
+
+ if ((r=nr_socket_getfd(sock->inner, &fd)))
+ ABORT(r);
+ NR_ASYNC_CANCEL(fd, NR_ASYNC_WAIT_WRITE);
+
+ // once connected arm for read
+ if (sock->readable_cb) {
+ NR_ASYNC_WAIT(fd, NR_ASYNC_WAIT_READ, sock->readable_cb, sock->readable_cb_arg);
+ }
+
+ if (sock->pending) {
+ r_log(LOG_GENERIC, LOG_INFO, "Invoking writable_cb on connected (%u)", (uint32_t) sock->pending);
+ nr_socket_buffered_stun_writable_cb(s, how, arg);
+ }
+
+ _status=0;
+abort:
+ if (_status) {
+ r_log(LOG_GENERIC, LOG_ERR, "Failure in nr_socket_buffered_stun_connected_cb: %d", _status);
+
+ }
+}
+
+static int nr_socket_buffered_stun_connect(void *obj, const nr_transport_addr *addr)
+{
+ nr_socket_buffered_stun *sock = (nr_socket_buffered_stun *)obj;
+ int r, _status;
+
+ if ((r=nr_transport_addr_copy(&sock->remote_addr, addr)))
+ ABORT(r);
+
+ if ((r=nr_socket_connect(sock->inner, addr))) {
+ if (r == R_WOULDBLOCK) {
+ NR_SOCKET fd;
+
+ if ((r=nr_socket_getfd(sock->inner, &fd)))
+ ABORT(r);
+
+ NR_ASYNC_WAIT(fd, NR_ASYNC_WAIT_WRITE, nr_socket_buffered_stun_connected_cb, sock);
+ ABORT(R_WOULDBLOCK);
+ }
+ ABORT(r);
+ } else {
+ r_log(LOG_GENERIC, LOG_INFO, "Connected without blocking");
+ sock->connected = 1;
+ }
+
+ _status=0;
+abort:
+ return(_status);
+}
+
+static int nr_socket_buffered_stun_arm_writable_cb(nr_socket_buffered_stun *sock)
+{
+ int r, _status;
+ NR_SOCKET fd;
+
+ if ((r=nr_socket_getfd(sock->inner, &fd)))
+ ABORT(r);
+
+ NR_ASYNC_WAIT(fd, NR_ASYNC_WAIT_WRITE, nr_socket_buffered_stun_writable_cb, sock);
+
+ _status=0;
+abort:
+ return(_status);
+}
+
+static int nr_socket_buffered_stun_write(void *obj,const void *msg, size_t len, size_t *written)
+{
+ nr_socket_buffered_stun *sock = (nr_socket_buffered_stun *)obj;
+ int already_armed = 0;
+ int r,_status;
+ size_t written2 = 0;
+ size_t original_len = len;
+
+ /* Buffers are close to full, report error. Do this now so we never
+ get partial writes */
+ if ((sock->pending + len) > sock->max_pending) {
+ r_log(LOG_GENERIC, LOG_INFO, "Write buffer for %s full (%u + %u > %u) - re-arming @%p",
+ sock->remote_addr.as_string, (uint32_t)sock->pending, (uint32_t)len, (uint32_t)sock->max_pending,
+ &(sock->pending));
+ ABORT(R_WOULDBLOCK);
+ }
+
+
+ if (sock->connected && !sock->pending) {
+ r = nr_socket_write(sock->inner, msg, len, &written2, 0);
+ if (r) {
+ if (r != R_WOULDBLOCK) {
+ r_log(LOG_GENERIC, LOG_ERR, "Write error for %s - %d",
+ sock->remote_addr.as_string, r);
+ ABORT(r);
+ }
+ r_log(LOG_GENERIC, LOG_INFO, "Write of %" PRIu64 " blocked for %s",
+ (uint64_t) len, sock->remote_addr.as_string);
+
+ written2=0;
+ }
+ } else {
+ already_armed = 1;
+ }
+
+ /* Buffer what's left */
+ len -= written2;
+
+ if (len) {
+ if ((r=nr_p_buf_write_to_chain(sock->p_bufs, &sock->pending_writes,
+ ((UCHAR *)msg) + written2, len))) {
+ r_log(LOG_GENERIC, LOG_ERR, "Write_to_chain error for %s - %d",
+ sock->remote_addr.as_string, r);
+
+ ABORT(r);
+ }
+
+ sock->pending += len;
+ }
+
+ if (sock->pending) {
+ if (!already_armed) {
+ if ((r=nr_socket_buffered_stun_arm_writable_cb(sock)))
+ ABORT(r);
+ }
+ r_log(LOG_GENERIC, LOG_INFO, "Write buffer not empty for %s %u - %s armed (@%p),%s connected",
+ sock->remote_addr.as_string, (uint32_t)sock->pending,
+ already_armed ? "already" : "", &sock->pending,
+ sock->connected ? "" : " not");
+ }
+
+ *written = original_len;
+
+ _status=0;
+abort:
+ return _status;
+}
+
+static void nr_socket_buffered_stun_writable_cb(NR_SOCKET s, int how, void *arg)
+{
+ nr_socket_buffered_stun *sock = (nr_socket_buffered_stun *)arg;
+ int r,_status;
+ nr_p_buf *n1, *n2;
+
+ if (sock->read_state == NR_ICE_SOCKET_READ_FAILED) {
+ ABORT(R_FAILED);
+ }
+
+ /* Try to flush */
+ STAILQ_FOREACH_SAFE(n1, &sock->pending_writes, entry, n2) {
+ size_t written = 0;
+
+ if ((r=nr_socket_write(sock->inner, n1->data + n1->r_offset,
+ n1->length - n1->r_offset,
+ &written, 0))) {
+
+ r_log(LOG_GENERIC, LOG_ERR, "Write error for %s - %d",
+ sock->remote_addr.as_string, r);
+ ABORT(r);
+ }
+
+ n1->r_offset += written;
+ assert(sock->pending >= written);
+ sock->pending -= written;
+
+ if (n1->r_offset < n1->length) {
+ /* We wrote something, but not everything */
+ r_log(LOG_GENERIC, LOG_INFO, "Write in callback didn't write all (remaining %u of %u) for %s",
+ n1->length - n1->r_offset, n1->length,
+ sock->remote_addr.as_string);
+ ABORT(R_WOULDBLOCK);
+ }
+
+ /* We are done with this p_buf */
+ STAILQ_REMOVE_HEAD(&sock->pending_writes, entry);
+ nr_p_buf_free(sock->p_bufs, n1);
+ }
+
+ assert(!sock->pending);
+ _status=0;
+abort:
+ r_log(LOG_GENERIC, LOG_INFO, "Writable_cb %s (%u (%p) pending)",
+ sock->remote_addr.as_string, (uint32_t)sock->pending, &(sock->pending));
+ if (_status && _status != R_WOULDBLOCK) {
+ r_log(LOG_GENERIC, LOG_ERR, "Failure in writable_cb: %d", _status);
+ nr_socket_buffered_stun_failed(sock);
+ /* Report this failure up; the only way to do this is a readable callback.
+ * Once the user tries to read (using nr_socket_buffered_stun_recvfrom), it
+ * will notice that there has been a failure. */
+ if (sock->readable_cb) {
+ sock->readable_cb(s, NR_ASYNC_WAIT_READ, sock->readable_cb_arg);
+ }
+ } else if (sock->pending) {
+ nr_socket_buffered_stun_arm_writable_cb(sock);
+ }
+}
+
+int nr_socket_buffered_stun_reset(nr_socket* sock_arg, nr_socket* new_inner) {
+ int r, _status;
+ NR_SOCKET fd;
+
+ nr_socket_buffered_stun* sock = (nr_socket_buffered_stun*)sock_arg->obj;
+
+ if (sock->inner && !nr_socket_getfd(sock->inner, &fd)) {
+ r_log(LOG_GENERIC, LOG_DEBUG, "In %s, canceling wait on old socket", __FUNCTION__);
+ NR_ASYNC_CANCEL(fd, NR_ASYNC_WAIT_WRITE);
+ NR_ASYNC_CANCEL(fd, NR_ASYNC_WAIT_READ);
+ }
+
+ nr_socket_destroy(&sock->inner);
+ sock->inner = new_inner;
+
+ sock->read_state = NR_ICE_SOCKET_READ_NONE;
+ sock->connected = 0;
+
+ sock->bytes_read = 0;
+ sock->bytes_needed = (sock->framing_type == ICE_TCP_FRAMING)
+ ? sizeof(nr_frame_header)
+ : sizeof(nr_stun_message_header);
+ sock->pending = 0;
+
+ nr_p_buf_free_chain(sock->p_bufs, &sock->pending_writes);
+ nr_p_buf_ctx_destroy(&sock->p_bufs);
+
+ STAILQ_INIT(&sock->pending_writes);
+
+ if ((r = nr_p_buf_ctx_create(sock->buffer_size, &sock->p_bufs))) {
+ ABORT(r);
+ }
+
+ if ((r = nr_ip4_port_to_transport_addr(INADDR_ANY, 0, IPPROTO_TCP,
+ &sock->remote_addr))) {
+ ABORT(r);
+ }
+
+ _status = 0;
+abort:
+ return (_status);
+}
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/stun/nr_socket_buffered_stun.h b/dom/media/webrtc/transport/third_party/nICEr/src/stun/nr_socket_buffered_stun.h
new file mode 100644
index 0000000000..c635ee393d
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/stun/nr_socket_buffered_stun.h
@@ -0,0 +1,66 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+Copyright (c) 2013, Mozilla
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+
+#ifndef _nr_socket_buffered_stun_h
+#define _nr_socket_buffered_stun_h
+
+#include "nr_socket.h"
+
+/* Wrapper socket which provides buffered STUN-oriented I/O
+
+ 1. Writes don't block and are automatically flushed when needed.
+ 2. All reads are in units of STUN messages
+
+ This socket takes ownership of the inner socket |sock|.
+ */
+
+typedef enum {
+ TURN_TCP_FRAMING=0,
+ ICE_TCP_FRAMING
+} nr_framing_type;
+
+void nr_socket_buffered_stun_set_readable_cb(nr_socket *sock,
+ NR_async_cb readable_cb, void *readable_cb_arg);
+
+int nr_socket_buffered_stun_create(nr_socket *inner, int max_pending,
+ nr_framing_type framing_type, nr_socket **sockp);
+
+int nr_socket_buffered_set_connected_to(nr_socket *sock,
+ nr_transport_addr *remote_addr);
+
+int nr_socket_buffered_stun_reset(nr_socket *sock, nr_socket *new_inner);
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/stun/nr_socket_turn.c b/dom/media/webrtc/transport/third_party/nICEr/src/stun/nr_socket_turn.c
new file mode 100644
index 0000000000..1a0162b13e
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/stun/nr_socket_turn.c
@@ -0,0 +1,195 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#ifdef USE_TURN
+
+#include <csi_platform.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/types.h>
+#include <assert.h>
+
+#include "stun.h"
+#include "turn_client_ctx.h"
+#include "nr_socket_turn.h"
+
+
+static char *nr_socket_turn_magic_cookie = "nr_socket_turn";
+
+typedef struct nr_socket_turn_ {
+ char *magic_cookie;
+ nr_turn_client_ctx *turn;
+} nr_socket_turn;
+
+
+static int nr_socket_turn_destroy(void **objp);
+static int nr_socket_turn_sendto(void *obj,const void *msg, size_t len,
+ int flags, const nr_transport_addr *to);
+static int nr_socket_turn_recvfrom(void *obj,void * restrict buf,
+ size_t maxlen, size_t *len, int flags, nr_transport_addr *from);
+static int nr_socket_turn_getfd(void *obj, NR_SOCKET *fd);
+static int nr_socket_turn_getaddr(void *obj, nr_transport_addr *addrp);
+static int nr_socket_turn_close(void *obj);
+
+static nr_socket_vtbl nr_socket_turn_vtbl={
+ 2,
+ nr_socket_turn_destroy,
+ nr_socket_turn_sendto,
+ nr_socket_turn_recvfrom,
+ nr_socket_turn_getfd,
+ nr_socket_turn_getaddr,
+ 0,
+ 0,
+ 0,
+ nr_socket_turn_close,
+ 0,
+ 0
+};
+
+int nr_socket_turn_create(nr_socket **sockp)
+ {
+ int r,_status;
+ nr_socket_turn *sturn=0;
+
+ if(!(sturn=RCALLOC(sizeof(nr_socket_turn))))
+ ABORT(R_NO_MEMORY);
+
+ sturn->magic_cookie = nr_socket_turn_magic_cookie;
+
+ if(r=nr_socket_create_int(sturn, &nr_socket_turn_vtbl, sockp))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ if(_status){
+ nr_socket_turn_destroy((void **)&sturn);
+ }
+ return(_status);
+ }
+
+static int nr_socket_turn_destroy(void **objp)
+ {
+ int _status;
+ nr_socket_turn *sturn;
+
+ if(!objp || !*objp)
+ return(0);
+
+ sturn=*objp;
+ *objp=0;
+
+ assert(sturn->magic_cookie == nr_socket_turn_magic_cookie);
+
+ /* we don't own the socket, so don't destroy it */
+
+ RFREE(sturn);
+
+ _status=0;
+ return(_status);
+ }
+
+static int nr_socket_turn_sendto(void *obj,const void *msg, size_t len,
+ int flags, const nr_transport_addr *addr)
+ {
+ int r,_status;
+ nr_socket_turn *sturn=obj;
+
+ assert(sturn->magic_cookie == nr_socket_turn_magic_cookie);
+ assert(sturn->turn);
+
+ if ((r = nr_turn_client_send_indication(sturn->turn, msg, len, flags,
+ addr)))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+static int nr_socket_turn_recvfrom(void *obj,void * restrict buf,
+ size_t maxlen, size_t *len, int flags, nr_transport_addr *addr)
+ {
+ /* Reading from TURN sockets is done by the indication
+ processing code in turn_client_ctx. */
+ assert(0);
+
+ return(R_INTERNAL);
+ }
+
+static int nr_socket_turn_getfd(void *obj, NR_SOCKET *fd)
+ {
+ /* You should never directly be touching this fd. */
+ assert(0);
+
+ return(R_INTERNAL);
+ }
+
+static int nr_socket_turn_getaddr(void *obj, nr_transport_addr *addrp)
+ {
+ nr_socket_turn *sturn=obj;
+ int r, _status;
+
+ assert(sturn->magic_cookie == nr_socket_turn_magic_cookie);
+ assert(sturn->turn);
+
+ /* This returns the relayed address */
+ if ((r=nr_turn_client_get_relayed_address(sturn->turn, addrp)))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+static int nr_socket_turn_close(void *obj)
+ {
+ /* No-op */
+#ifndef NDEBUG
+ nr_socket_turn *sturn=obj;
+ assert(sturn->magic_cookie == nr_socket_turn_magic_cookie);
+#endif
+
+ return 0;
+ }
+
+int nr_socket_turn_set_ctx(nr_socket *sock, nr_turn_client_ctx *ctx)
+{
+ nr_socket_turn *sturn=(nr_socket_turn*)sock->obj;
+ assert(sturn->magic_cookie == nr_socket_turn_magic_cookie);
+ assert(!sturn->turn);
+
+ sturn->turn = ctx;
+
+ return 0;
+}
+
+#endif /* USE_TURN */
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/stun/nr_socket_turn.h b/dom/media/webrtc/transport/third_party/nICEr/src/stun/nr_socket_turn.h
new file mode 100644
index 0000000000..13506045a8
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/stun/nr_socket_turn.h
@@ -0,0 +1,48 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+
+#ifndef _nr_socket_turn_h
+#define _nr_socket_turn_h
+
+#include "nr_socket.h"
+
+/* This is a partial implementation of an nr_socket wrapped
+ around TURN. It implements only the nr_socket features
+ actually used by the ICE stack. You can't, for instance,
+ read off the socket */
+int nr_socket_turn_create(nr_socket **sockp);
+int nr_socket_turn_set_ctx(nr_socket *sock, nr_turn_client_ctx *ctx);
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun.h b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun.h
new file mode 100644
index 0000000000..a32751d795
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun.h
@@ -0,0 +1,218 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+#ifndef _STUN_H
+#define _STUN_H
+
+#ifdef WIN32
+#include <winsock2.h>
+#else
+#include <sys/param.h>
+#include <sys/socket.h>
+#ifndef LINUX
+#include <net/if.h>
+#ifdef DARWIN
+#include <net/if_var.h>
+#endif
+#include <net/if_dl.h>
+#include <net/if_types.h>
+#else
+#include <linux/if.h>
+#endif
+#ifndef BSD
+#include <net/route.h>
+#endif
+#include <netinet/in.h>
+#ifndef LINUX
+#include <netinet/in_var.h>
+#endif
+#include <arpa/inet.h>
+#include <netdb.h>
+#endif
+#include <time.h>
+
+#include "nr_api.h"
+#include "stun_msg.h"
+#include "stun_build.h"
+#include "stun_codec.h"
+#include "stun_hint.h"
+#include "stun_util.h"
+#include "nr_socket.h"
+#include "stun_client_ctx.h"
+#include "stun_server_ctx.h"
+#include "stun_proc.h"
+
+#define NR_STUN_VERSION "rfc3489bis-11"
+#define NR_STUN_PORT 3478
+
+/* STUN attributes */
+#define NR_STUN_ATTR_MAPPED_ADDRESS 0x0001
+#define NR_STUN_ATTR_USERNAME 0x0006
+#define NR_STUN_ATTR_MESSAGE_INTEGRITY 0x0008
+#define NR_STUN_ATTR_ERROR_CODE 0x0009
+#define NR_STUN_ATTR_UNKNOWN_ATTRIBUTES 0x000A
+#define NR_STUN_ATTR_REALM 0x0014
+#define NR_STUN_ATTR_NONCE 0x0015
+#define NR_STUN_ATTR_XOR_MAPPED_ADDRESS 0x0020
+#define NR_STUN_ATTR_SERVER 0x8022
+#define NR_STUN_ATTR_ALTERNATE_SERVER 0x8023
+#define NR_STUN_ATTR_FINGERPRINT 0x8028
+
+/* for backwards compatibility with obsolete versions of the STUN spec */
+#define NR_STUN_ATTR_OLD_XOR_MAPPED_ADDRESS 0x8020
+
+#ifdef USE_STUND_0_96
+#define NR_STUN_ATTR_OLD_CHANGE_REQUEST 0x0003
+#endif /* USE_STUND_0_96 */
+
+#ifdef USE_RFC_3489_BACKWARDS_COMPATIBLE
+/* for backwards compatibility with obsolete versions of the STUN spec */
+#define NR_STUN_ATTR_OLD_PASSWORD 0x0007
+#define NR_STUN_ATTR_OLD_RESPONSE_ADDRESS 0x0002
+#define NR_STUN_ATTR_OLD_SOURCE_ADDRESS 0x0004
+#define NR_STUN_ATTR_OLD_CHANGED_ADDRESS 0x0005
+#endif /* USE_RFC_3489_BACKWARDS_COMPATIBLE */
+
+#ifdef USE_ICE
+/* ICE attributes */
+#define NR_STUN_ATTR_PRIORITY 0x0024
+#define NR_STUN_ATTR_USE_CANDIDATE 0x0025
+#define NR_STUN_ATTR_ICE_CONTROLLED 0x8029
+#define NR_STUN_ATTR_ICE_CONTROLLING 0x802A
+#endif /* USE_ICE */
+
+#ifdef USE_TURN
+/* TURN attributes */
+#define NR_STUN_ATTR_LIFETIME 0x000d
+/* from an expired draft defined as optional, but in the required range */
+#define NR_STUN_ATTR_BANDWIDTH 0x0010
+#define NR_STUN_ATTR_XOR_PEER_ADDRESS 0x0012
+#define NR_STUN_ATTR_DATA 0x0013
+#define NR_STUN_ATTR_XOR_RELAY_ADDRESS 0x0016
+#define NR_STUN_ATTR_REQUESTED_TRANSPORT 0x0019
+
+#define NR_STUN_ATTR_REQUESTED_TRANSPORT_UDP 17
+#endif /* USE_TURN */
+
+/*
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ * |M|M|M|M|M|C|M|M|M|C|M|M|M|M|
+ * |1|1|9|8|7|1|6|5|4|0|3|2|1|0|
+ * |1|0| | | | | | | | | | | | |
+ * +-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+ *
+ * Figure 3: Format of STUN Message Type Field
+ */
+#define NR_STUN_METHOD_TYPE_BITS(m) \
+ ((((m) & 0xf80) << 2) | (((m) & 0x070) << 1) | ((m) & 0x00f))
+
+#define NR_STUN_CLASS_TYPE_BITS(c) \
+ ((((c) & 0x002) << 7) | (((c) & 0x001) << 4))
+
+#define NR_STUN_GET_TYPE_METHOD(t) \
+ ((((t) >> 2) & 0xf80) | (((t) >> 1) & 0x070) | ((t) & 0x00f))
+
+#define NR_STUN_GET_TYPE_CLASS(t) \
+ ((((t) >> 7) & 0x002) | (((t) >> 4) & 0x001))
+
+#define NR_STUN_TYPE(m,c) (NR_STUN_METHOD_TYPE_BITS((m)) | NR_STUN_CLASS_TYPE_BITS((c)))
+
+/* building blocks for message types */
+#define NR_METHOD_BINDING 0x001
+#define NR_CLASS_REQUEST 0x0
+#define NR_CLASS_INDICATION 0x1
+#define NR_CLASS_RESPONSE 0x2
+#define NR_CLASS_ERROR_RESPONSE 0x3
+
+/* define types for STUN messages */
+#define NR_STUN_MSG_BINDING_REQUEST NR_STUN_TYPE(NR_METHOD_BINDING, \
+ NR_CLASS_REQUEST)
+#define NR_STUN_MSG_BINDING_INDICATION NR_STUN_TYPE(NR_METHOD_BINDING, \
+ NR_CLASS_INDICATION)
+#define NR_STUN_MSG_BINDING_RESPONSE NR_STUN_TYPE(NR_METHOD_BINDING, \
+ NR_CLASS_RESPONSE)
+#define NR_STUN_MSG_BINDING_ERROR_RESPONSE NR_STUN_TYPE(NR_METHOD_BINDING, \
+ NR_CLASS_ERROR_RESPONSE)
+
+#ifdef USE_TURN
+/* building blocks for TURN message types */
+#define NR_METHOD_ALLOCATE 0x003
+#define NR_METHOD_REFRESH 0x004
+
+#define NR_METHOD_SEND 0x006
+#define NR_METHOD_DATA 0x007
+#define NR_METHOD_CREATE_PERMISSION 0x008
+#define NR_METHOD_CHANNEL_BIND 0x009
+
+/* define types for a TURN message */
+#define NR_STUN_MSG_ALLOCATE_REQUEST NR_STUN_TYPE(NR_METHOD_ALLOCATE, \
+ NR_CLASS_REQUEST)
+#define NR_STUN_MSG_ALLOCATE_RESPONSE NR_STUN_TYPE(NR_METHOD_ALLOCATE, \
+ NR_CLASS_RESPONSE)
+#define NR_STUN_MSG_ALLOCATE_ERROR_RESPONSE NR_STUN_TYPE(NR_METHOD_ALLOCATE, \
+ NR_CLASS_ERROR_RESPONSE)
+#define NR_STUN_MSG_REFRESH_REQUEST NR_STUN_TYPE(NR_METHOD_REFRESH, \
+ NR_CLASS_REQUEST)
+#define NR_STUN_MSG_REFRESH_RESPONSE NR_STUN_TYPE(NR_METHOD_REFRESH, \
+ NR_CLASS_RESPONSE)
+#define NR_STUN_MSG_REFRESH_ERROR_RESPONSE NR_STUN_TYPE(NR_METHOD_REFRESH, \
+ NR_CLASS_ERROR_RESPONSE)
+
+#define NR_STUN_MSG_SEND_INDICATION NR_STUN_TYPE(NR_METHOD_SEND, \
+ NR_CLASS_INDICATION)
+#define NR_STUN_MSG_DATA_INDICATION NR_STUN_TYPE(NR_METHOD_DATA, \
+ NR_CLASS_INDICATION)
+
+#define NR_STUN_MSG_PERMISSION_REQUEST NR_STUN_TYPE(NR_METHOD_CREATE_PERMISSION, \
+ NR_CLASS_REQUEST)
+#define NR_STUN_MSG_PERMISSION_RESPONSE NR_STUN_TYPE(NR_METHOD_CREATE_PERMISSION, \
+ NR_CLASS_RESPONSE)
+#define NR_STUN_MSG_PERMISSION_ERROR_RESPONSE NR_STUN_TYPE(NR_METHOD_CREATE_PERMISSION, \
+ NR_CLASS_ERROR_RESPONSE)
+
+#define NR_STUN_MSG_CHANNEL_BIND_REQUEST NR_STUN_TYPE(NR_METHOD_CHANNEL_BIND, \
+ NR_CLASS_REQUEST)
+#define NR_STUN_MSG_CHANNEL_BIND_RESPONSE NR_STUN_TYPE(NR_METHOD_CHANNEL_BIND, \
+ NR_CLASS_RESPONSE)
+#define NR_STUN_MSG_CHANNEL_BIND_ERROR_RESPONSE NR_STUN_TYPE(NR_METHOD_CHANNEL_BIND, \
+ NR_CLASS_ERROR_RESPONSE)
+
+
+#endif /* USE_TURN */
+
+
+#define NR_STUN_AUTH_RULE_OPTIONAL (1<<0)
+#define NR_STUN_AUTH_RULE_SHORT_TERM (1<<8)
+#define NR_STUN_AUTH_RULE_LONG_TERM (1<<9)
+
+#endif
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_build.c b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_build.c
new file mode 100644
index 0000000000..001b38e7c4
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_build.c
@@ -0,0 +1,611 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <csi_platform.h>
+#include <assert.h>
+#include <errno.h>
+#include <stdio.h>
+#include <string.h>
+#include <ctype.h>
+
+#include "nr_api.h"
+#include "stun.h"
+#include "registry.h"
+#include "stun_reg.h"
+#include "nr_crypto.h"
+
+
+/* draft-ietf-behave-rfc3489bis-10.txt S 7.1 */
+/* draft-ietf-behave-rfc3489bis-10.txt S 10.1.1 */
+/* note that S 10.1.1 states the message MUST include MESSAGE-INTEGRITY
+ * and USERNAME, but that's not correct -- for instance ICE keepalive
+ * messages don't include these (See draft-ietf-mmusic-ice-18.txt S 10:
+ * "If STUN is being used for keepalives, a STUN Binding Indication is
+ * used. The Indication MUST NOT utilize any authentication mechanism")
+ */
+int
+nr_stun_form_request_or_indication(int mode, int msg_type, nr_stun_message **msg)
+{
+ int r,_status;
+ nr_stun_message *req = 0;
+
+ assert(NR_STUN_GET_TYPE_CLASS(msg_type) == NR_CLASS_REQUEST
+ || NR_STUN_GET_TYPE_CLASS(msg_type) == NR_CLASS_INDICATION);
+
+ *msg = 0;
+
+ if ((r=nr_stun_message_create(&req)))
+ ABORT(r);
+
+ req->header.type = msg_type;
+
+ nr_crypto_random_bytes((UCHAR*)&req->header.id,sizeof(req->header.id));
+
+ switch (mode) {
+ default:
+ if ((r=nr_stun_message_add_fingerprint_attribute(req)))
+ ABORT(r);
+ /* fall through */
+ case NR_STUN_MODE_STUN_NO_AUTH:
+ req->header.magic_cookie = NR_STUN_MAGIC_COOKIE;
+ break;
+
+#ifdef USE_STUND_0_96
+ case NR_STUN_MODE_STUND_0_96:
+ req->header.magic_cookie = NR_STUN_MAGIC_COOKIE2;
+
+ /* actually, stund 0.96 just ignores the fingerprint
+ * attribute, but don't bother to send it */
+
+ break;
+#endif /* USE_STUND_0_96 */
+
+ }
+
+ *msg = req;
+
+ _status=0;
+ abort:
+ if (_status) RFREE(req);
+ return _status;
+}
+
+int
+nr_stun_build_req_lt_auth(nr_stun_client_stun_binding_request_params *params, nr_stun_message **msg)
+{
+ int r,_status;
+ nr_stun_message *req = 0;
+
+ if ((r=nr_stun_form_request_or_indication(NR_STUN_MODE_STUN, NR_STUN_MSG_BINDING_REQUEST, &req)))
+ ABORT(r);
+
+ if ((r=nr_stun_message_add_username_attribute(req, params->username)))
+ ABORT(r);
+
+ if (params->realm && params->nonce) {
+ if ((r=nr_stun_message_add_realm_attribute(req, params->realm)))
+ ABORT(r);
+
+ if ((r=nr_stun_message_add_nonce_attribute(req, params->nonce)))
+ ABORT(r);
+
+ if ((r=nr_stun_message_add_message_integrity_attribute(req, params->password)))
+ ABORT(r);
+ }
+
+ *msg = req;
+
+ _status=0;
+ abort:
+ if (_status) nr_stun_message_destroy(&req);
+ return _status;
+}
+
+int
+nr_stun_build_req_st_auth(nr_stun_client_stun_binding_request_params *params, nr_stun_message **msg)
+{
+ int r,_status;
+ nr_stun_message *req = 0;
+
+ if ((r=nr_stun_form_request_or_indication(NR_STUN_MODE_STUN, NR_STUN_MSG_BINDING_REQUEST, &req)))
+ ABORT(r);
+
+ if ((r=nr_stun_message_add_username_attribute(req, params->username)))
+ ABORT(r);
+
+ if (params->password) {
+ if ((r=nr_stun_message_add_message_integrity_attribute(req, params->password)))
+ ABORT(r);
+ }
+
+ *msg = req;
+
+ _status=0;
+ abort:
+ if (_status) nr_stun_message_destroy(&req);
+ return _status;
+}
+
+int
+nr_stun_build_req_no_auth(nr_stun_client_stun_binding_request_params *params, nr_stun_message **msg)
+{
+ int r,_status;
+ nr_stun_message *req = 0;
+
+ if ((r=nr_stun_form_request_or_indication(NR_STUN_MODE_STUN_NO_AUTH, NR_STUN_MSG_BINDING_REQUEST, &req)))
+ ABORT(r);
+
+ *msg = req;
+
+ _status=0;
+ abort:
+ if (_status) nr_stun_message_destroy(&req);
+ return _status;
+}
+
+int
+nr_stun_build_keepalive(nr_stun_client_stun_keepalive_params *params, nr_stun_message **msg)
+{
+ int r,_status;
+ nr_stun_message *ind = 0;
+
+ if ((r=nr_stun_form_request_or_indication(NR_STUN_MODE_STUN, NR_STUN_MSG_BINDING_INDICATION, &ind)))
+ ABORT(r);
+
+ *msg = ind;
+
+ _status=0;
+ abort:
+ if (_status) nr_stun_message_destroy(&ind);
+ return _status;
+}
+
+#ifdef USE_STUND_0_96
+int
+nr_stun_build_req_stund_0_96(nr_stun_client_stun_binding_request_stund_0_96_params *params, nr_stun_message **msg)
+{
+ int r,_status;
+ nr_stun_message *req = 0;
+
+ if ((r=nr_stun_form_request_or_indication(NR_STUN_MODE_STUND_0_96, NR_STUN_MSG_BINDING_REQUEST, &req)))
+ ABORT(r);
+
+ if ((r=nr_stun_message_add_change_request_attribute(req, 0)))
+ ABORT(r);
+
+ assert(! nr_stun_message_has_attribute(req, NR_STUN_ATTR_USERNAME, 0));
+ assert(! nr_stun_message_has_attribute(req, NR_STUN_ATTR_MESSAGE_INTEGRITY, 0));
+
+ *msg = req;
+
+ _status=0;
+ abort:
+ if (_status) nr_stun_message_destroy(&req);
+ return _status;
+}
+#endif /* USE_STUND_0_96 */
+
+#ifdef USE_ICE
+int
+nr_stun_build_use_candidate(nr_stun_client_ice_binding_request_params *params, nr_stun_message **msg)
+{
+ int r,_status;
+ nr_stun_message *req = 0;
+
+ if ((r=nr_stun_form_request_or_indication(NR_STUN_MODE_STUN, NR_STUN_MSG_BINDING_REQUEST, &req)))
+ ABORT(r);
+
+ if ((r=nr_stun_message_add_username_attribute(req, params->username)))
+ ABORT(r);
+
+ if ((r=nr_stun_message_add_message_integrity_attribute(req, &params->password)))
+ ABORT(r);
+
+ if ((r=nr_stun_message_add_use_candidate_attribute(req)))
+ ABORT(r);
+
+ if ((r=nr_stun_message_add_priority_attribute(req, params->priority)))
+ ABORT(r);
+
+ if ((r=nr_stun_message_add_ice_controlling_attribute(req, params->tiebreaker)))
+ ABORT(r);
+
+ *msg = req;
+
+ _status=0;
+ abort:
+ if (_status) nr_stun_message_destroy(&req);
+ return _status;
+}
+
+int
+nr_stun_build_req_ice(nr_stun_client_ice_binding_request_params *params, nr_stun_message **msg)
+{
+ int r,_status;
+ nr_stun_message *req = 0;
+
+ if ((r=nr_stun_form_request_or_indication(NR_STUN_MODE_STUN, NR_STUN_MSG_BINDING_REQUEST, &req)))
+ ABORT(r);
+
+ if ((r=nr_stun_message_add_username_attribute(req, params->username)))
+ ABORT(r);
+
+ if ((r=nr_stun_message_add_message_integrity_attribute(req, &params->password)))
+ ABORT(r);
+
+ if ((r=nr_stun_message_add_priority_attribute(req, params->priority)))
+ ABORT(r);
+
+ switch (params->control) {
+ case NR_ICE_CONTROLLING:
+ if ((r=nr_stun_message_add_ice_controlling_attribute(req, params->tiebreaker)))
+ ABORT(r);
+ break;
+ case NR_ICE_CONTROLLED:
+ if ((r=nr_stun_message_add_ice_controlled_attribute(req, params->tiebreaker)))
+ ABORT(r);
+ break;
+ default:
+ assert(0);
+ ABORT(R_INTERNAL);
+ }
+
+ *msg = req;
+
+ _status=0;
+ abort:
+ if (_status) nr_stun_message_destroy(&req);
+ return _status;
+}
+#endif /* USE_ICE */
+
+#ifdef USE_TURN
+
+#ifndef __isascii
+#define __isascii(c) (((c) & ~0x7F) == 0)
+#endif
+
+/* Long-term passwords are computed over the key:
+
+ key = MD5(username ":" realm ":" SASLprep(password))
+
+ Per RFC 5389 S 15.4
+*/
+int
+nr_stun_compute_lt_message_integrity_password(const char *username, const char *realm,
+ Data *password, Data *hmac_key)
+{
+ char digest_input[1000];
+ size_t i;
+ int r, _status;
+ size_t len;
+
+ /* First check that the password is ASCII. We are supposed to
+ SASLprep but we don't support this yet
+ TODO(ekr@rtfm.com): Add SASLprep for password.
+ */
+ for (i=0; i<password->len; i++) {
+ if (!__isascii(password->data[i]))
+ ABORT(R_BAD_DATA);
+ }
+
+ if (hmac_key->len < 16)
+ ABORT(R_BAD_ARGS);
+
+ snprintf(digest_input, sizeof(digest_input), "%s:%s:", username, realm);
+ if ((sizeof(digest_input) - strlen(digest_input)) < password->len)
+ ABORT(R_BAD_DATA);
+
+ len = strlen(digest_input);
+ memcpy(digest_input + len, password->data, password->len);
+
+
+ if (r=nr_crypto_md5((UCHAR *)digest_input, len + password->len, hmac_key->data))
+ ABORT(r);
+ hmac_key->len=16;
+
+ _status=0;
+abort:
+ return(_status);
+}
+
+static int
+nr_stun_build_auth_params(nr_stun_client_auth_params *auth, nr_stun_message *req)
+{
+ int r, _status;
+ UCHAR hmac_key_d[16];
+ Data hmac_key;
+
+ ATTACH_DATA(hmac_key, hmac_key_d);
+
+ if (!auth->authenticate)
+ goto done;
+
+ assert(auth->username);
+ assert(auth->password.len);
+ assert(auth->realm);
+ assert(auth->nonce);
+
+ if (r=nr_stun_compute_lt_message_integrity_password(auth->username,
+ auth->realm,
+ &auth->password,
+ &hmac_key))
+ ABORT(r);
+
+ if (!auth->username) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "STUN authentication requested but no username provided");
+ ABORT(R_INTERNAL);
+ }
+
+ if (!auth->password.len) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "STUN authentication requested but no password provided");
+ ABORT(R_INTERNAL);
+ }
+
+ if (!auth->realm) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "STUN authentication requested but no realm provided");
+ ABORT(R_INTERNAL);
+ }
+
+ if (!auth->nonce) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "STUN authentication requested but no nonce provided");
+ ABORT(R_INTERNAL);
+ }
+
+ if ((r=nr_stun_message_add_username_attribute(req, auth->username)))
+ ABORT(r);
+
+ if ((r=nr_stun_message_add_realm_attribute(req, auth->realm)))
+ ABORT(r);
+
+ if ((r=nr_stun_message_add_nonce_attribute(req, auth->nonce)))
+ ABORT(r);
+
+ if ((r=nr_stun_message_add_message_integrity_attribute(req, &hmac_key)))
+ ABORT(r);
+
+done:
+ _status=0;
+abort:
+ return(_status);
+}
+
+int
+nr_stun_build_allocate_request(nr_stun_client_auth_params *auth, nr_stun_client_allocate_request_params *params, nr_stun_message **msg)
+{
+ int r,_status;
+ nr_stun_message *req = 0;
+
+ if ((r=nr_stun_form_request_or_indication(NR_STUN_MODE_STUN, NR_STUN_MSG_ALLOCATE_REQUEST, &req)))
+ ABORT(r);
+
+ if ((r=nr_stun_message_add_requested_transport_attribute(req, NR_STUN_ATTR_REQUESTED_TRANSPORT_UDP)))
+ ABORT(r);
+
+ if ((r=nr_stun_message_add_lifetime_attribute(req, params->lifetime_secs)))
+ ABORT(r);
+
+ /* TODO(ekr@rtfm.com): Add the SOFTWARE attribute (Firefox bug 857666) */
+
+ if ((r=nr_stun_build_auth_params(auth, req)))
+ ABORT(r);
+
+ *msg = req;
+
+ _status=0;
+ abort:
+ if (_status) nr_stun_message_destroy(&req);
+ return _status;
+}
+
+
+int nr_stun_build_refresh_request(nr_stun_client_auth_params *auth, nr_stun_client_refresh_request_params *params, nr_stun_message **msg)
+{
+ int r,_status;
+ nr_stun_message *req = 0;
+
+ if ((r=nr_stun_form_request_or_indication(NR_STUN_MODE_STUN, NR_STUN_MSG_REFRESH_REQUEST, &req)))
+ ABORT(r);
+
+ if ((r=nr_stun_message_add_lifetime_attribute(req, params->lifetime_secs)))
+ ABORT(r);
+
+
+ /* TODO(ekr@rtfm.com): Add the SOFTWARE attribute (Firefox bug 857666) */
+
+ if ((r=nr_stun_build_auth_params(auth, req)))
+ ABORT(r);
+
+ *msg = req;
+
+ _status=0;
+ abort:
+ if (_status) nr_stun_message_destroy(&req);
+ return _status;
+}
+
+
+int nr_stun_build_permission_request(nr_stun_client_auth_params *auth, nr_stun_client_permission_request_params *params, nr_stun_message **msg)
+{
+ int r,_status;
+ nr_stun_message *req = 0;
+
+ if ((r=nr_stun_form_request_or_indication(NR_STUN_MODE_STUN, NR_STUN_MSG_PERMISSION_REQUEST, &req)))
+ ABORT(r);
+
+ if ((r=nr_stun_message_add_xor_peer_address_attribute(req, &params->remote_addr)))
+ ABORT(r);
+
+ if ((r=nr_stun_build_auth_params(auth, req)))
+ ABORT(r);
+
+ *msg = req;
+
+ _status=0;
+ abort:
+ if (_status) nr_stun_message_destroy(&req);
+ return _status;
+}
+
+int
+nr_stun_build_send_indication(nr_stun_client_send_indication_params *params, nr_stun_message **msg)
+{
+ int r,_status;
+ nr_stun_message *ind = 0;
+
+ if ((r=nr_stun_form_request_or_indication(NR_STUN_MODE_STUN, NR_STUN_MSG_SEND_INDICATION, &ind)))
+ ABORT(r);
+
+ if ((r=nr_stun_message_add_xor_peer_address_attribute(ind, &params->remote_addr)))
+ ABORT(r);
+
+ if ((r=nr_stun_message_add_data_attribute(ind, params->data.data, params->data.len)))
+ ABORT(r);
+
+ *msg = ind;
+
+ _status=0;
+ abort:
+ if (_status) nr_stun_message_destroy(&ind);
+ return _status;
+}
+
+int
+nr_stun_build_data_indication(nr_stun_client_data_indication_params *params, nr_stun_message **msg)
+{
+ int r,_status;
+ nr_stun_message *ind = 0;
+
+ if ((r=nr_stun_form_request_or_indication(NR_STUN_MODE_STUN, NR_STUN_MSG_DATA_INDICATION, &ind)))
+ ABORT(r);
+
+ if ((r=nr_stun_message_add_xor_peer_address_attribute(ind, &params->remote_addr)))
+ ABORT(r);
+
+ if ((r=nr_stun_message_add_data_attribute(ind, params->data.data, params->data.len)))
+ ABORT(r);
+
+ *msg = ind;
+
+ _status=0;
+ abort:
+ if (_status) nr_stun_message_destroy(&ind);
+ return _status;
+}
+
+#endif /* USE_TURN */
+
+/* draft-ietf-behave-rfc3489bis-10.txt S 7.3.1.1 */
+int
+nr_stun_form_success_response(nr_stun_message *req, nr_transport_addr *from, Data *password, nr_stun_message *res)
+{
+ int r,_status;
+ int request_method;
+ char server_name[NR_STUN_MAX_SERVER_BYTES+1]; /* +1 for \0 */
+
+ /* set up information for default response */
+
+ request_method = NR_STUN_GET_TYPE_METHOD(req->header.type);
+ res->header.type = NR_STUN_TYPE(request_method, NR_CLASS_RESPONSE);
+ res->header.magic_cookie = req->header.magic_cookie;
+ memcpy(&res->header.id, &req->header.id, sizeof(res->header.id));
+
+ r_log(NR_LOG_STUN, LOG_DEBUG, "Mapped Address = %s", from->as_string);
+
+ if ((r=nr_stun_message_add_xor_mapped_address_attribute(res, from)))
+ ABORT(r);
+
+ if (!NR_reg_get_string(NR_STUN_REG_PREF_SERVER_NAME, server_name, sizeof(server_name))) {
+ if ((r=nr_stun_message_add_server_attribute(res, server_name)))
+ ABORT(r);
+ }
+
+ if (res->header.magic_cookie == NR_STUN_MAGIC_COOKIE) {
+ if (password != 0) {
+ if ((r=nr_stun_message_add_message_integrity_attribute(res, password)))
+ ABORT(r);
+ }
+
+ if ((r=nr_stun_message_add_fingerprint_attribute(res)))
+ ABORT(r);
+ }
+
+ _status=0;
+ abort:
+ return _status;
+}
+
+/* draft-ietf-behave-rfc3489bis-10.txt S 7.3.1.1 */
+void
+nr_stun_form_error_response(nr_stun_message *req, nr_stun_message* res, int number, char* msg)
+{
+ char *str;
+ int request_method;
+ char server_name[NR_STUN_MAX_SERVER_BYTES+1]; /* +1 for \0 */
+
+ if (number < 300 || number > 699)
+ number = 500;
+
+ r_log(NR_LOG_STUN, LOG_INFO, "Responding with error %d: %s", number, msg);
+
+ request_method = NR_STUN_GET_TYPE_METHOD(req->header.type);
+ res->header.type = NR_STUN_TYPE(request_method, NR_CLASS_ERROR_RESPONSE);
+ res->header.magic_cookie = req->header.magic_cookie;
+ memcpy(&res->header.id, &req->header.id, sizeof(res->header.id));
+
+ /* during development we should never see 500s (hopefully not in deployment either) */
+
+ str = 0;
+ switch (number) {
+ case 300: str = "Try Alternate"; break;
+ case 400: str = "Bad Request"; break;
+ case 401: str = "Unauthorized"; break;
+ case 420: str = "Unknown Attribute"; break;
+ case 438: str = "Stale Nonce"; break;
+#ifdef USE_ICE
+ case 487: str = "Role Conflict"; break;
+#endif
+ case 500: str = "Server Error"; break;
+ }
+ if (str == 0) {
+ str = "Unknown";
+ }
+
+ if (nr_stun_message_add_error_code_attribute(res, number, str)) {
+ assert(0); /* should never happen */
+ }
+
+ if (!NR_reg_get_string(NR_STUN_REG_PREF_SERVER_NAME, server_name, sizeof(server_name))) {
+ nr_stun_message_add_server_attribute(res, server_name);
+ }
+}
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_build.h b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_build.h
new file mode 100644
index 0000000000..c3f91a87b0
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_build.h
@@ -0,0 +1,147 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+#ifndef _stun_build_h
+#define _stun_build_h
+
+#include "stun.h"
+
+#define NR_STUN_MODE_STUN 1
+#ifdef USE_STUND_0_96
+#define NR_STUN_MODE_STUND_0_96 2 /* backwards compatibility mode */
+#endif /* USE_STUND_0_96 */
+#define NR_STUN_MODE_STUN_NO_AUTH 3
+int nr_stun_form_request_or_indication(int mode, int msg_type, nr_stun_message **msg);
+
+typedef struct nr_stun_client_stun_binding_request_params_ {
+ char *username;
+ Data *password;
+ char *nonce;
+ char *realm;
+} nr_stun_client_stun_binding_request_params;
+
+int nr_stun_build_req_lt_auth(nr_stun_client_stun_binding_request_params *params, nr_stun_message **msg);
+int nr_stun_build_req_st_auth(nr_stun_client_stun_binding_request_params *params, nr_stun_message **msg);
+int nr_stun_build_req_no_auth(nr_stun_client_stun_binding_request_params *params, nr_stun_message **msg);
+
+
+typedef struct nr_stun_client_stun_keepalive_params_ {
+#if defined(WIN32) || defined(__clang__)
+ // VC++ and clang give error and warning respectively if no members
+ int dummy;
+#endif
+} nr_stun_client_stun_keepalive_params;
+
+int nr_stun_build_keepalive(nr_stun_client_stun_keepalive_params *params, nr_stun_message **msg);
+
+
+#ifdef USE_STUND_0_96
+typedef struct nr_stun_client_stun_binding_request_stund_0_96_params_ {
+#ifdef WIN32 // silly VC++ gives error if no members
+ int dummy;
+#endif
+} nr_stun_client_stun_binding_request_stund_0_96_params;
+
+int nr_stun_build_req_stund_0_96(nr_stun_client_stun_binding_request_stund_0_96_params *params, nr_stun_message **msg);
+#endif /* USE_STUND_0_96 */
+
+
+#ifdef USE_ICE
+typedef struct nr_stun_client_ice_binding_request_params_ {
+ char *username;
+ Data password;
+ UINT4 priority;
+ int control;
+#define NR_ICE_CONTROLLING 1
+#define NR_ICE_CONTROLLED 2
+ UINT8 tiebreaker;
+} nr_stun_client_ice_binding_request_params;
+
+int nr_stun_build_use_candidate(nr_stun_client_ice_binding_request_params *params, nr_stun_message **msg);
+
+int nr_stun_build_req_ice(nr_stun_client_ice_binding_request_params *params, nr_stun_message **msg);
+#endif /* USE_ICE */
+
+
+typedef struct nr_stun_client_auth_params_ {
+ char authenticate;
+ char *username;
+ char *realm;
+ char *nonce;
+ Data password;
+} nr_stun_client_auth_params;
+
+#ifdef USE_TURN
+typedef struct nr_stun_client_allocate_request_params_ {
+ UINT4 lifetime_secs;
+} nr_stun_client_allocate_request_params;
+
+int nr_stun_build_allocate_request(nr_stun_client_auth_params *auth, nr_stun_client_allocate_request_params *params, nr_stun_message **msg);
+
+
+typedef struct nr_stun_client_refresh_request_params_ {
+ UINT4 lifetime_secs;
+} nr_stun_client_refresh_request_params;
+
+int nr_stun_build_refresh_request(nr_stun_client_auth_params *auth, nr_stun_client_refresh_request_params *params, nr_stun_message **msg);
+
+
+
+typedef struct nr_stun_client_permission_request_params_ {
+ nr_transport_addr remote_addr;
+} nr_stun_client_permission_request_params;
+
+int nr_stun_build_permission_request(nr_stun_client_auth_params *auth, nr_stun_client_permission_request_params *params, nr_stun_message **msg);
+
+
+typedef struct nr_stun_client_send_indication_params_ {
+ nr_transport_addr remote_addr;
+ Data data;
+} nr_stun_client_send_indication_params;
+
+int nr_stun_build_send_indication(nr_stun_client_send_indication_params *params, nr_stun_message **msg);
+
+typedef struct nr_stun_client_data_indication_params_ {
+ nr_transport_addr remote_addr;
+ Data data;
+} nr_stun_client_data_indication_params;
+
+int nr_stun_build_data_indication(nr_stun_client_data_indication_params *params, nr_stun_message **msg);
+#endif /* USE_TURN */
+
+int nr_stun_form_success_response(nr_stun_message *req, nr_transport_addr *from, Data *password, nr_stun_message *res);
+void nr_stun_form_error_response(nr_stun_message *request, nr_stun_message* response, int number, char* msg);
+int nr_stun_compute_lt_message_integrity_password(const char *username, const char *realm,
+ Data *password, Data *hmac_key);
+
+#endif
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_client_ctx.c b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_client_ctx.c
new file mode 100644
index 0000000000..50b9f74a5b
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_client_ctx.c
@@ -0,0 +1,888 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <assert.h>
+#include <string.h>
+#include <math.h>
+
+#include <nr_api.h>
+#include "stun.h"
+#include "async_timer.h"
+#include "registry.h"
+#include "stun_reg.h"
+#include "nr_crypto.h"
+#include "r_time.h"
+
+static int nr_stun_client_send_request(nr_stun_client_ctx *ctx);
+static void nr_stun_client_timer_expired_cb(NR_SOCKET s, int b, void *cb_arg);
+static int nr_stun_client_get_password(void *arg, nr_stun_message *msg, Data **password);
+
+#define NR_STUN_TRANSPORT_ADDR_CHECK_WILDCARD 1
+#define NR_STUN_TRANSPORT_ADDR_CHECK_LOOPBACK 2
+
+int nr_stun_client_ctx_create(char *label, nr_socket *sock, nr_transport_addr *peer, UINT4 RTO, nr_stun_client_ctx **ctxp)
+ {
+ nr_stun_client_ctx *ctx=0;
+ char allow_loopback;
+ int r,_status;
+
+ if ((r=nr_stun_startup()))
+ ABORT(r);
+
+ if(!(ctx=RCALLOC(sizeof(nr_stun_client_ctx))))
+ ABORT(R_NO_MEMORY);
+
+ ctx->state=NR_STUN_CLIENT_STATE_INITTED;
+
+ if(!(ctx->label=r_strdup(label)))
+ ABORT(R_NO_MEMORY);
+
+ ctx->sock=sock;
+
+ nr_socket_getaddr(sock,&ctx->my_addr);
+ nr_transport_addr_copy(&ctx->peer_addr,peer);
+ assert(ctx->my_addr.protocol==ctx->peer_addr.protocol);
+
+ if (RTO != 0) {
+ ctx->rto_ms = RTO;
+ } else if (NR_reg_get_uint4(NR_STUN_REG_PREF_CLNT_RETRANSMIT_TIMEOUT, &ctx->rto_ms)) {
+ ctx->rto_ms = 100;
+ }
+
+ if (NR_reg_get_double(NR_STUN_REG_PREF_CLNT_RETRANSMIT_BACKOFF, &ctx->retransmission_backoff_factor))
+ ctx->retransmission_backoff_factor = 2.0;
+
+ if (NR_reg_get_uint4(NR_STUN_REG_PREF_CLNT_MAXIMUM_TRANSMITS, &ctx->maximum_transmits))
+ ctx->maximum_transmits = 7;
+
+ if (NR_reg_get_uint4(NR_STUN_REG_PREF_CLNT_FINAL_RETRANSMIT_BACKOFF, &ctx->maximum_transmits_timeout_ms))
+ ctx->maximum_transmits_timeout_ms = 16 * ctx->rto_ms;
+
+ ctx->mapped_addr_check_mask = NR_STUN_TRANSPORT_ADDR_CHECK_WILDCARD;
+ if (NR_reg_get_char(NR_STUN_REG_PREF_ALLOW_LOOPBACK_ADDRS, &allow_loopback) ||
+ !allow_loopback) {
+ ctx->mapped_addr_check_mask |= NR_STUN_TRANSPORT_ADDR_CHECK_LOOPBACK;
+ }
+
+ if (ctx->my_addr.protocol == IPPROTO_TCP) {
+ /* Because TCP is reliable there is only one final timeout value.
+ * We store the timeout value for TCP in here, because timeout_ms gets
+ * reset to 0 in client_reset() which gets called from client_start() */
+ ctx->maximum_transmits_timeout_ms = ctx->rto_ms *
+ pow(ctx->retransmission_backoff_factor,
+ ctx->maximum_transmits);
+ ctx->maximum_transmits = 1;
+ }
+
+ *ctxp=ctx;
+
+ _status=0;
+ abort:
+ if(_status){
+ nr_stun_client_ctx_destroy(&ctx);
+ }
+ return(_status);
+ }
+
+static void nr_stun_client_fire_finished_cb(nr_stun_client_ctx *ctx)
+ {
+ if (ctx->finished_cb) {
+ NR_async_cb finished_cb = ctx->finished_cb;
+ ctx->finished_cb = 0; /* prevent 2nd call */
+ /* finished_cb call must be absolutely last thing in function
+ * because as a side effect this ctx may be operated on in the
+ * callback */
+ finished_cb(0,0,ctx->cb_arg);
+ }
+ }
+
+int nr_stun_client_start(nr_stun_client_ctx *ctx, int mode, NR_async_cb finished_cb, void *cb_arg)
+ {
+ int r,_status;
+
+ if (ctx->state != NR_STUN_CLIENT_STATE_INITTED)
+ ABORT(R_NOT_PERMITTED);
+
+ ctx->mode=mode;
+
+ ctx->state=NR_STUN_CLIENT_STATE_RUNNING;
+ ctx->finished_cb=finished_cb;
+ ctx->cb_arg=cb_arg;
+
+ if(mode!=NR_STUN_CLIENT_MODE_KEEPALIVE){
+ if(r=nr_stun_client_send_request(ctx))
+ ABORT(r);
+ }
+
+ _status=0;
+ abort:
+ if (ctx->state != NR_STUN_CLIENT_STATE_RUNNING) {
+ nr_stun_client_fire_finished_cb(ctx);
+ }
+
+ return(_status);
+ }
+
+ int nr_stun_client_restart(nr_stun_client_ctx* ctx,
+ const nr_transport_addr* peer_addr) {
+ int r,_status;
+ int mode;
+ NR_async_cb finished_cb;
+ void *cb_arg;
+ if (ctx->state != NR_STUN_CLIENT_STATE_RUNNING)
+ ABORT(R_NOT_PERMITTED);
+
+ mode = ctx->mode;
+ finished_cb = ctx->finished_cb;
+ cb_arg = ctx->cb_arg;
+
+ nr_stun_client_reset(ctx);
+ nr_transport_addr_copy(&ctx->peer_addr, peer_addr);
+
+ if (r=nr_stun_client_start(ctx, mode, finished_cb, cb_arg))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int
+nr_stun_client_reset(nr_stun_client_ctx *ctx)
+{
+ /* Cancel the timer firing */
+ if (ctx->timer_handle){
+ NR_async_timer_cancel(ctx->timer_handle);
+ ctx->timer_handle=0;
+ }
+
+ nr_stun_message_destroy(&ctx->request);
+ ctx->request = 0;
+
+ nr_stun_message_destroy(&ctx->response);
+ ctx->response = 0;
+
+ memset(&ctx->results, 0, sizeof(ctx->results));
+
+ ctx->mode = 0;
+ ctx->finished_cb = 0;
+ ctx->cb_arg = 0;
+ ctx->request_ct = 0;
+ ctx->timeout_ms = 0;
+
+ ctx->state = NR_STUN_CLIENT_STATE_INITTED;
+
+ return 0;
+}
+
+static void nr_stun_client_timer_expired_cb(NR_SOCKET s, int b, void *cb_arg)
+ {
+ int _status;
+ nr_stun_client_ctx *ctx=cb_arg;
+ struct timeval now;
+ INT8 ms_waited;
+
+ /* Prevent this timer from being cancelled later */
+ ctx->timer_handle=0;
+
+ /* Shouldn't happen */
+ if(ctx->state==NR_STUN_CLIENT_STATE_CANCELLED)
+ ABORT(R_REJECTED);
+
+ gettimeofday(&now, 0);
+ if (r_timeval_diff_ms(&now, &ctx->timer_set, &ms_waited)) {
+ r_log(NR_LOG_STUN,LOG_DEBUG,"STUN-CLIENT(%s): Timer expired",ctx->label);
+ }
+ else {
+ r_log(NR_LOG_STUN,LOG_DEBUG,"STUN-CLIENT(%s): Timer expired (after %llu ms)",ctx->label, ms_waited);
+ }
+
+ if (ctx->request_ct >= ctx->maximum_transmits) {
+ r_log(NR_LOG_STUN,LOG_INFO,"STUN-CLIENT(%s): Timed out",ctx->label);
+ ctx->state=NR_STUN_CLIENT_STATE_TIMED_OUT;
+ ABORT(R_FAILED);
+ }
+
+ if (ctx->state != NR_STUN_CLIENT_STATE_RUNNING)
+ ABORT(R_NOT_PERMITTED);
+
+ // track retransmits for ice telemetry
+ nr_accumulate_count(&(ctx->retransmit_ct), 1);
+
+ /* as a side effect will reset the timer */
+ nr_stun_client_send_request(ctx);
+
+ _status = 0;
+ abort:
+ if (ctx->state != NR_STUN_CLIENT_STATE_RUNNING) {
+ /* Cancel the timer firing */
+ if (ctx->timer_handle){
+ NR_async_timer_cancel(ctx->timer_handle);
+ ctx->timer_handle=0;
+ }
+
+ nr_stun_client_fire_finished_cb(ctx);
+ }
+ if (_status) {
+ // cb doesn't return anything, but we should probably log that we aborted
+ // This also quiets the unused variable warnings.
+ r_log(NR_LOG_STUN,LOG_DEBUG,"STUN-CLIENT(%s): Timer expired cb abort with status: %d",
+ ctx->label, _status);
+ }
+ return;
+ }
+
+int nr_stun_client_force_retransmit(nr_stun_client_ctx *ctx)
+ {
+ int r,_status;
+
+ if (ctx->state != NR_STUN_CLIENT_STATE_RUNNING)
+ ABORT(R_NOT_PERMITTED);
+
+ if (ctx->request_ct > ctx->maximum_transmits) {
+ r_log(NR_LOG_STUN,LOG_INFO,"STUN-CLIENT(%s): Too many retransmit attempts",ctx->label);
+ ABORT(R_FAILED);
+ }
+
+ /* if there is a scheduled retransimt, get rid of the scheduled retransmit
+ * and retransmit immediately */
+ if (ctx->timer_handle) {
+ NR_async_timer_cancel(ctx->timer_handle);
+ ctx->timer_handle=0;
+
+ if (r=nr_stun_client_send_request(ctx))
+ ABORT(r);
+ }
+
+ _status=0;
+ abort:
+
+ return(_status);
+ }
+
+static int nr_stun_client_send_request(nr_stun_client_ctx *ctx)
+ {
+ int r,_status;
+ char string[256];
+
+ if (ctx->state != NR_STUN_CLIENT_STATE_RUNNING)
+ ABORT(R_NOT_PERMITTED);
+
+ r_log(NR_LOG_STUN,LOG_DEBUG,"STUN-CLIENT(%s): Sending check request (my_addr=%s,peer_addr=%s)",ctx->label,ctx->my_addr.as_string,ctx->peer_addr.as_string);
+
+ if (ctx->request == 0) {
+ switch (ctx->mode) {
+ case NR_STUN_CLIENT_MODE_BINDING_REQUEST_LONG_TERM_AUTH:
+ ctx->params.stun_binding_request.nonce = ctx->nonce;
+ ctx->params.stun_binding_request.realm = ctx->realm;
+ assert(0);
+ ABORT(R_INTERNAL);
+ /* TODO(ekr@rtfm.com): Need to implement long-term auth for binding
+ requests */
+ if ((r=nr_stun_build_req_lt_auth(&ctx->params.stun_binding_request, &ctx->request)))
+ ABORT(r);
+ break;
+ case NR_STUN_CLIENT_MODE_BINDING_REQUEST_SHORT_TERM_AUTH:
+ if ((r=nr_stun_build_req_st_auth(&ctx->params.stun_binding_request, &ctx->request)))
+ ABORT(r);
+ break;
+ case NR_STUN_CLIENT_MODE_BINDING_REQUEST_NO_AUTH:
+ if ((r=nr_stun_build_req_no_auth(&ctx->params.stun_binding_request, &ctx->request)))
+ ABORT(r);
+ break;
+ case NR_STUN_CLIENT_MODE_KEEPALIVE:
+ if ((r=nr_stun_build_keepalive(&ctx->params.stun_keepalive, &ctx->request)))
+ ABORT(r);
+ break;
+#ifdef USE_STUND_0_96
+ case NR_STUN_CLIENT_MODE_BINDING_REQUEST_STUND_0_96:
+ if ((r=nr_stun_build_req_stund_0_96(&ctx->params.stun_binding_request_stund_0_96, &ctx->request)))
+ ABORT(r);
+ break;
+#endif /* USE_STUND_0_96 */
+
+#ifdef USE_ICE
+ case NR_ICE_CLIENT_MODE_USE_CANDIDATE:
+ if ((r=nr_stun_build_use_candidate(&ctx->params.ice_binding_request, &ctx->request)))
+ ABORT(r);
+ break;
+ case NR_ICE_CLIENT_MODE_BINDING_REQUEST:
+ if ((r=nr_stun_build_req_ice(&ctx->params.ice_binding_request, &ctx->request)))
+ ABORT(r);
+ break;
+#endif /* USE_ICE */
+
+#ifdef USE_TURN
+ case NR_TURN_CLIENT_MODE_ALLOCATE_REQUEST:
+ if ((r=nr_stun_build_allocate_request(&ctx->auth_params, &ctx->params.allocate_request, &ctx->request)))
+ ABORT(r);
+ break;
+ case NR_TURN_CLIENT_MODE_REFRESH_REQUEST:
+ if ((r=nr_stun_build_refresh_request(&ctx->auth_params, &ctx->params.refresh_request, &ctx->request)))
+ ABORT(r);
+ break;
+ case NR_TURN_CLIENT_MODE_PERMISSION_REQUEST:
+ if ((r=nr_stun_build_permission_request(&ctx->auth_params, &ctx->params.permission_request, &ctx->request)))
+ ABORT(r);
+ break;
+ case NR_TURN_CLIENT_MODE_SEND_INDICATION:
+ if ((r=nr_stun_build_send_indication(&ctx->params.send_indication, &ctx->request)))
+ ABORT(r);
+ break;
+#endif /* USE_TURN */
+
+ default:
+ assert(0);
+ ABORT(R_FAILED);
+ break;
+ }
+ }
+
+ if (ctx->request->length == 0) {
+ if ((r=nr_stun_encode_message(ctx->request)))
+ ABORT(r);
+ }
+
+ snprintf(string, sizeof(string)-1, "STUN-CLIENT(%s): Sending to %s ", ctx->label, ctx->peer_addr.as_string);
+ r_dump(NR_LOG_STUN, LOG_DEBUG, string, (char*)ctx->request->buffer, ctx->request->length);
+
+ assert(ctx->my_addr.protocol==ctx->peer_addr.protocol);
+
+ if(r=nr_socket_sendto(ctx->sock, ctx->request->buffer, ctx->request->length, 0, &ctx->peer_addr)) {
+ if (r != R_WOULDBLOCK) {
+ ABORT(r);
+ }
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-CLIENT(%s): nr_socket_sendto blocked, treating as dropped packet",ctx->label);
+ }
+
+ ctx->request_ct++;
+
+ if (NR_STUN_GET_TYPE_CLASS(ctx->request->header.type) == NR_CLASS_INDICATION) {
+ /* no need to set the timer because indications don't receive a
+ * response */
+ }
+ else {
+ if (ctx->request_ct >= ctx->maximum_transmits) {
+ /* Reliable transport only get here once. Unreliable get here for
+ * their final timeout. */
+ ctx->timeout_ms += ctx->maximum_transmits_timeout_ms;
+ }
+ else if (ctx->timeout_ms) {
+ /* exponential backoff */
+ ctx->timeout_ms *= ctx->retransmission_backoff_factor;
+ }
+ else {
+ /* initial timeout unreliable transports */
+ ctx->timeout_ms = ctx->rto_ms;
+ }
+
+ r_log(NR_LOG_STUN,LOG_DEBUG,"STUN-CLIENT(%s): Next timer will fire in %u ms",ctx->label, ctx->timeout_ms);
+
+ gettimeofday(&ctx->timer_set, 0);
+
+ assert(ctx->timeout_ms);
+ NR_ASYNC_TIMER_SET(ctx->timeout_ms, nr_stun_client_timer_expired_cb, ctx, &ctx->timer_handle);
+ }
+
+ _status=0;
+ abort:
+ if (_status) {
+ nr_stun_client_failed(ctx);
+ }
+ return(_status);
+ }
+
+static int nr_stun_client_get_password(void *arg, nr_stun_message *msg, Data **password)
+{
+ *password = (Data*)arg;
+ if (!arg)
+ return(R_NOT_FOUND);
+ return(0);
+}
+
+int nr_stun_transport_addr_check(nr_transport_addr* addr, UINT4 check)
+ {
+ if((check & NR_STUN_TRANSPORT_ADDR_CHECK_WILDCARD) && nr_transport_addr_is_wildcard(addr))
+ return(R_BAD_DATA);
+
+ if ((check & NR_STUN_TRANSPORT_ADDR_CHECK_LOOPBACK) && nr_transport_addr_is_loopback(addr))
+ return(R_BAD_DATA);
+
+ return(0);
+ }
+
+int nr_stun_client_process_response(nr_stun_client_ctx *ctx, UCHAR *msg, int len, nr_transport_addr *peer_addr)
+ {
+ int r,_status;
+ char string[256];
+ char *username = 0;
+ Data *password = 0;
+ int allow_unauthed_redirect = 0;
+ nr_stun_message_attribute *attr;
+ nr_transport_addr *mapped_addr = 0;
+ int fail_on_error = 0;
+ UCHAR hmac_key_d[16];
+ Data hmac_key;
+ int compute_lt_key=0;
+ /* TODO(bcampen@mozilla.com): Bug 1023619, refactor this. */
+ int response_matched=0;
+
+ ATTACH_DATA(hmac_key, hmac_key_d);
+
+ if ((ctx->state != NR_STUN_CLIENT_STATE_RUNNING) &&
+ (ctx->state != NR_STUN_CLIENT_STATE_WAITING))
+ ABORT(R_REJECTED);
+
+ r_log(NR_LOG_STUN,LOG_DEBUG,"STUN-CLIENT(%s): Inspecting STUN response (my_addr=%s, peer_addr=%s)",ctx->label,ctx->my_addr.as_string,peer_addr->as_string);
+
+ snprintf(string, sizeof(string)-1, "STUN-CLIENT(%s): Received ", ctx->label);
+ r_dump(NR_LOG_STUN, LOG_DEBUG, string, (char*)msg, len);
+
+ /* determine password */
+ switch (ctx->mode) {
+ case NR_STUN_CLIENT_MODE_BINDING_REQUEST_LONG_TERM_AUTH:
+ /* If the STUN server responds with an error, give up, since we don't
+ * want to delay the completion of gathering. */
+ fail_on_error = 1;
+ compute_lt_key = 1;
+ /* Fall through */
+ case NR_STUN_CLIENT_MODE_BINDING_REQUEST_SHORT_TERM_AUTH:
+ password = ctx->params.stun_binding_request.password;
+ break;
+
+ case NR_STUN_CLIENT_MODE_BINDING_REQUEST_NO_AUTH:
+ /* If the STUN server responds with an error, give up, since we don't
+ * want to delay the completion of gathering. */
+ fail_on_error = 1;
+ break;
+
+ case NR_STUN_CLIENT_MODE_BINDING_REQUEST_STUND_0_96:
+ /* If the STUN server responds with an error, give up, since we don't
+ * want to delay the completion of gathering. */
+ fail_on_error = 1;
+ break;
+
+#ifdef USE_ICE
+ case NR_ICE_CLIENT_MODE_BINDING_REQUEST:
+ /* We do not set fail_on_error here. The error might be transient, and
+ * retrying isn't going to cause a slowdown. */
+ password = &ctx->params.ice_binding_request.password;
+ break;
+ case NR_ICE_CLIENT_MODE_USE_CANDIDATE:
+ /* We do not set fail_on_error here. The error might be transient, and
+ * retrying isn't going to cause a slowdown. */
+ password = &ctx->params.ice_binding_request.password;
+ break;
+#endif /* USE_ICE */
+
+#ifdef USE_TURN
+ case NR_TURN_CLIENT_MODE_ALLOCATE_REQUEST:
+ fail_on_error = 1;
+ compute_lt_key = 1;
+ /* Do not require mutual auth on redirect responses to Allocate requests. */
+ allow_unauthed_redirect = 1;
+ username = ctx->auth_params.username;
+ password = &ctx->auth_params.password;
+ /* do nothing */
+ break;
+ case NR_TURN_CLIENT_MODE_REFRESH_REQUEST:
+ fail_on_error = 1;
+ compute_lt_key = 1;
+ username = ctx->auth_params.username;
+ password = &ctx->auth_params.password;
+ /* do nothing */
+ break;
+ case NR_TURN_CLIENT_MODE_PERMISSION_REQUEST:
+ fail_on_error = 1;
+ compute_lt_key = 1;
+ username = ctx->auth_params.username;
+ password = &ctx->auth_params.password;
+ /* do nothing */
+ break;
+ case NR_TURN_CLIENT_MODE_SEND_INDICATION:
+ /* do nothing -- we just got our DATA-INDICATION */
+ break;
+#endif /* USE_TURN */
+
+ default:
+ assert(0);
+ ABORT(R_FAILED);
+ break;
+ }
+
+ if (compute_lt_key) {
+ if (!ctx->realm || !username) {
+ r_log(NR_LOG_STUN, LOG_DEBUG, "Long-term auth required but no realm/username specified. Randomizing key");
+ /* Fill the key with random bytes to guarantee non-match */
+ if (r=nr_crypto_random_bytes(hmac_key_d, sizeof(hmac_key_d)))
+ ABORT(r);
+ }
+ else {
+ if (r=nr_stun_compute_lt_message_integrity_password(username, ctx->realm,
+ password, &hmac_key))
+ ABORT(r);
+ }
+ password = &hmac_key;
+ }
+
+ if (ctx->response) {
+ nr_stun_message_destroy(&ctx->response);
+ }
+
+ /* TODO(bcampen@mozilla.com): Bug 1023619, refactor this. */
+ if ((r=nr_stun_message_create2(&ctx->response, msg, len)))
+ ABORT(r);
+
+ if ((r=nr_stun_decode_message(ctx->response, nr_stun_client_get_password, password))) {
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-CLIENT(%s): error decoding response",ctx->label);
+ ABORT(r);
+ }
+
+ /* This will return an error if request and response don't match,
+ which is how we reject responses that match other contexts. */
+ if ((r=nr_stun_receive_message(ctx->request, ctx->response))) {
+ r_log(NR_LOG_STUN,LOG_DEBUG,"STUN-CLIENT(%s): Response is not for us",ctx->label);
+ ABORT(r);
+ }
+
+ r_log(NR_LOG_STUN,LOG_INFO,
+ "STUN-CLIENT(%s): Received response; processing",ctx->label);
+ response_matched=1;
+
+ if (allow_unauthed_redirect &&
+ nr_stun_message_has_attribute(ctx->response, NR_STUN_ATTR_ERROR_CODE,
+ &attr) &&
+ (attr->u.error_code.number / 100 == 3)) {
+ password = 0;
+ }
+
+/* TODO: !nn! currently using password!=0 to mean that auth is required,
+ * TODO: !nn! but we should probably pass that in explicitly via the
+ * TODO: !nn! usage (ctx->mode?) */
+ if (password) {
+ if (nr_stun_message_has_attribute(ctx->response, NR_STUN_ATTR_NONCE, 0)) {
+ if ((r=nr_stun_receive_response_long_term_auth(ctx->response, ctx))) {
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-CLIENT(%s): long term auth failed",ctx->label);
+ ABORT(r);
+ }
+ }
+ else {
+ if ((r=nr_stun_receive_response_short_term_auth(ctx->response))) {
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-CLIENT(%s): short term auth failed",ctx->label);
+ ABORT(r);
+ }
+ }
+ }
+
+ if (NR_STUN_GET_TYPE_CLASS(ctx->response->header.type) == NR_CLASS_RESPONSE) {
+ if ((r=nr_stun_process_success_response(ctx->response))) {
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-CLIENT(%s): nr_stun_process_success_response failed",ctx->label);
+ ABORT(r);
+ }
+ }
+ else {
+ if (fail_on_error) {
+ ctx->state = NR_STUN_CLIENT_STATE_FAILED;
+ }
+ /* Note: most times we call process_error_response, we get r != 0.
+
+ However, if the error is to be discarded, we get r == 0, smash
+ the error code, and just keep going.
+ */
+ if ((r=nr_stun_process_error_response(ctx->response, &ctx->error_code))) {
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-CLIENT(%s): nr_stun_process_error_response failed",ctx->label);
+ ABORT(r);
+ }
+ else {
+ ctx->error_code = 0xffff;
+ /* drop the error on the floor */
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-CLIENT(%s): processed error response",ctx->label);
+ ABORT(R_FAILED);
+ }
+ }
+
+ r_log(NR_LOG_STUN,LOG_DEBUG,"STUN-CLIENT(%s): Successfully parsed mode=%d",ctx->label,ctx->mode);
+
+/* TODO: !nn! this should be moved to individual message receive/processing sections */
+ switch (ctx->mode) {
+ case NR_STUN_CLIENT_MODE_BINDING_REQUEST_LONG_TERM_AUTH:
+ case NR_STUN_CLIENT_MODE_BINDING_REQUEST_SHORT_TERM_AUTH:
+ if (! nr_stun_message_has_attribute(ctx->response, NR_STUN_ATTR_XOR_MAPPED_ADDRESS, 0)) {
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-CLIENT(%s): No XOR-MAPPED-ADDRESS",ctx->label);
+ ABORT(R_BAD_DATA);
+ }
+ if (! nr_stun_message_has_attribute(ctx->response, NR_STUN_ATTR_MESSAGE_INTEGRITY, 0)) {
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-CLIENT(%s): No MESSAGE-INTEGRITY",ctx->label);
+ ABORT(R_BAD_DATA);
+ }
+
+ mapped_addr = &ctx->results.stun_binding_response.mapped_addr;
+ break;
+
+ case NR_STUN_CLIENT_MODE_BINDING_REQUEST_NO_AUTH:
+ if (! nr_stun_message_has_attribute(ctx->response, NR_STUN_ATTR_XOR_MAPPED_ADDRESS, 0)) {
+ if (nr_stun_message_has_attribute(ctx->response, NR_STUN_ATTR_MAPPED_ADDRESS, 0)) {
+ /* Compensate for a bug in Google's STUN servers where they always respond with MAPPED-ADDRESS */
+ r_log(NR_LOG_STUN,LOG_INFO,"STUN-CLIENT(%s): No XOR-MAPPED-ADDRESS but MAPPED-ADDRESS. Falling back (though server is wrong).", ctx->label);
+ }
+ else {
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-CLIENT(%s): No XOR-MAPPED-ADDRESS or MAPPED-ADDRESS",ctx->label);
+ ABORT(R_BAD_DATA);
+ }
+ }
+
+ mapped_addr = &ctx->results.stun_binding_response.mapped_addr;
+ break;
+
+ case NR_STUN_CLIENT_MODE_BINDING_REQUEST_STUND_0_96:
+ if (! nr_stun_message_has_attribute(ctx->response, NR_STUN_ATTR_MAPPED_ADDRESS, 0) && ! nr_stun_message_has_attribute(ctx->response, NR_STUN_ATTR_XOR_MAPPED_ADDRESS, 0)) {
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-CLIENT(%s): No MAPPED-ADDRESS",ctx->label);
+ ABORT(R_BAD_DATA);
+ }
+
+ mapped_addr = &ctx->results.stun_binding_response_stund_0_96.mapped_addr;
+ break;
+
+#ifdef USE_ICE
+ case NR_ICE_CLIENT_MODE_BINDING_REQUEST:
+ if (! nr_stun_message_has_attribute(ctx->response, NR_STUN_ATTR_XOR_MAPPED_ADDRESS, 0)) {
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-CLIENT(%s): No XOR-MAPPED-ADDRESS",ctx->label);
+ ABORT(R_BAD_DATA);
+ }
+ if (! nr_stun_message_has_attribute(ctx->response, NR_STUN_ATTR_MESSAGE_INTEGRITY, 0)) {
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-CLIENT(%s): No MESSAGE-INTEGRITY",ctx->label);
+ ABORT(R_BAD_DATA);
+ }
+
+ mapped_addr = &ctx->results.stun_binding_response.mapped_addr;
+ break;
+ case NR_ICE_CLIENT_MODE_USE_CANDIDATE:
+ if (! nr_stun_message_has_attribute(ctx->response, NR_STUN_ATTR_XOR_MAPPED_ADDRESS, 0)) {
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-CLIENT(%s): No XOR-MAPPED-ADDRESS",ctx->label);
+ ABORT(R_BAD_DATA);
+ }
+ if (! nr_stun_message_has_attribute(ctx->response, NR_STUN_ATTR_MESSAGE_INTEGRITY, 0)) {
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-CLIENT(%s): No MESSAGE-INTEGRITY",ctx->label);
+ ABORT(R_BAD_DATA);
+ }
+
+ mapped_addr = &ctx->results.stun_binding_response.mapped_addr;
+ break;
+#endif /* USE_ICE */
+
+#ifdef USE_TURN
+ case NR_TURN_CLIENT_MODE_ALLOCATE_REQUEST:
+ if (! nr_stun_message_has_attribute(ctx->response, NR_STUN_ATTR_XOR_MAPPED_ADDRESS, 0)) {
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-CLIENT(%s): No XOR-MAPPED-ADDRESS",ctx->label);
+ ABORT(R_BAD_DATA);
+ }
+ if (! nr_stun_message_has_attribute(ctx->response, NR_STUN_ATTR_MESSAGE_INTEGRITY, 0)) {
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-CLIENT(%s): No MESSAGE-INTEGRITY",ctx->label);
+ ABORT(R_BAD_DATA);
+ }
+
+ if (!nr_stun_message_has_attribute(ctx->response, NR_STUN_ATTR_XOR_RELAY_ADDRESS, &attr)) {
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-CLIENT(%s): No XOR-RELAYED-ADDRESS",ctx->label);
+ ABORT(R_BAD_DATA);
+ }
+
+ if ((r=nr_stun_transport_addr_check(&attr->u.relay_address.unmasked,
+ ctx->mapped_addr_check_mask))) {
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-CLIENT(%s): nr_stun_transport_addr_check failed",ctx->label);
+ ABORT(r);
+ }
+
+ if ((r=nr_transport_addr_copy(
+ &ctx->results.allocate_response.relay_addr,
+ &attr->u.relay_address.unmasked))) {
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-CLIENT(%s): nr_transport_addr_copy failed",ctx->label);
+ ABORT(r);
+ }
+
+ if (!nr_stun_message_has_attribute(ctx->response, NR_STUN_ATTR_LIFETIME, &attr)) {
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-CLIENT(%s): No LIFETIME",ctx->label);
+ ABORT(R_BAD_DATA);
+ }
+ ctx->results.allocate_response.lifetime_secs=attr->u.lifetime_secs;
+
+ r_log(NR_LOG_STUN,LOG_DEBUG,"STUN-CLIENT(%s): Received relay address: %s", ctx->label, ctx->results.allocate_response.relay_addr.as_string);
+
+ mapped_addr = &ctx->results.allocate_response.mapped_addr;
+
+ break;
+ case NR_TURN_CLIENT_MODE_REFRESH_REQUEST:
+ if (! nr_stun_message_has_attribute(ctx->response, NR_STUN_ATTR_MESSAGE_INTEGRITY, 0)) {
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-CLIENT(%s): No MESSAGE-INTEGRITY",ctx->label);
+ ABORT(R_BAD_DATA);
+ }
+ if (!nr_stun_message_has_attribute(ctx->response, NR_STUN_ATTR_LIFETIME, &attr)) {
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-CLIENT(%s): No LIFETIME",ctx->label);
+ ABORT(R_BAD_DATA);
+ }
+ ctx->results.refresh_response.lifetime_secs=attr->u.lifetime_secs;
+ break;
+ case NR_TURN_CLIENT_MODE_PERMISSION_REQUEST:
+ if (! nr_stun_message_has_attribute(ctx->response, NR_STUN_ATTR_MESSAGE_INTEGRITY, 0)) {
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-CLIENT(%s): No MESSAGE-INTEGRITY",ctx->label);
+ ABORT(R_BAD_DATA);
+ }
+ break;
+#endif /* USE_TURN */
+
+ default:
+ assert(0);
+ ABORT(R_FAILED);
+ break;
+ }
+
+ /* make sure we have the most up-to-date address from this peer */
+ if (nr_transport_addr_cmp(&ctx->peer_addr, peer_addr, NR_TRANSPORT_ADDR_CMP_MODE_ALL)) {
+ r_log(NR_LOG_STUN,LOG_INFO,"STUN-CLIENT(%s): Peer moved from %s to %s", ctx->label, ctx->peer_addr.as_string, peer_addr->as_string);
+ nr_transport_addr_copy(&ctx->peer_addr, peer_addr);
+ }
+
+ if (mapped_addr) {
+ if (nr_stun_message_has_attribute(ctx->response, NR_STUN_ATTR_XOR_MAPPED_ADDRESS, &attr)) {
+ if ((r=nr_stun_transport_addr_check(&attr->u.xor_mapped_address.unmasked,
+ ctx->mapped_addr_check_mask))) {
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-CLIENT(%s): XOR-MAPPED-ADDRESS is bogus",ctx->label);
+ ABORT(r);
+ }
+
+ if ((r=nr_transport_addr_copy(mapped_addr, &attr->u.xor_mapped_address.unmasked))) {
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-CLIENT(%s): nr_transport_addr_copy failed",ctx->label);
+ ABORT(r);
+ }
+ }
+ else if (nr_stun_message_has_attribute(ctx->response, NR_STUN_ATTR_MAPPED_ADDRESS, &attr)) {
+ if ((r=nr_stun_transport_addr_check(&attr->u.mapped_address,
+ ctx->mapped_addr_check_mask))) {
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-CLIENT(%s): MAPPED-ADDRESS is bogus",ctx->label);
+ ABORT(r);
+ }
+
+ if ((r=nr_transport_addr_copy(mapped_addr, &attr->u.mapped_address))) {
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-CLIENT(%s): nr_transport_addr_copy failed",ctx->label);
+ ABORT(r);
+ }
+ }
+ else {
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-CLIENT(%s): No mapped address!",ctx->label);
+ ABORT(R_BAD_DATA);
+ }
+
+ // STUN doesn't distinguish protocol in mapped address, therefore
+ // assign used protocol from peer_addr
+ if (mapped_addr->protocol!=peer_addr->protocol){
+ mapped_addr->protocol=peer_addr->protocol;
+ nr_transport_addr_fmt_addr_string(mapped_addr);
+ }
+
+ r_log(NR_LOG_STUN,LOG_DEBUG,"STUN-CLIENT(%s): Received mapped address: %s", ctx->label, mapped_addr->as_string);
+ }
+
+ ctx->state=NR_STUN_CLIENT_STATE_DONE;
+
+ _status=0;
+ abort:
+ if(_status && response_matched){
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-CLIENT(%s): Error processing response: %s, stun error code %d.", ctx->label, nr_strerror(_status), (int)ctx->error_code);
+ }
+
+ if ((ctx->state != NR_STUN_CLIENT_STATE_RUNNING) &&
+ (ctx->state != NR_STUN_CLIENT_STATE_WAITING)) {
+ /* Cancel the timer firing */
+ if (ctx->timer_handle) {
+ NR_async_timer_cancel(ctx->timer_handle);
+ ctx->timer_handle = 0;
+ }
+
+ nr_stun_client_fire_finished_cb(ctx);
+ }
+
+ return(_status);
+ }
+
+int nr_stun_client_ctx_destroy(nr_stun_client_ctx **ctxp)
+ {
+ nr_stun_client_ctx *ctx;
+
+ if(!ctxp || !*ctxp)
+ return(0);
+
+ ctx=*ctxp;
+ *ctxp=0;
+
+ nr_stun_client_reset(ctx);
+
+ RFREE(ctx->nonce);
+ RFREE(ctx->realm);
+
+ RFREE(ctx->label);
+ RFREE(ctx);
+
+ return(0);
+ }
+
+
+int nr_stun_client_cancel(nr_stun_client_ctx *ctx)
+ {
+ /* Cancel the timer firing */
+ if (ctx->timer_handle){
+ NR_async_timer_cancel(ctx->timer_handle);
+ ctx->timer_handle=0;
+ }
+
+ /* Mark cancelled so we ignore any returned messsages */
+ ctx->state=NR_STUN_CLIENT_STATE_CANCELLED;
+ return(0);
+}
+
+int nr_stun_client_wait(nr_stun_client_ctx *ctx)
+ {
+ nr_stun_client_cancel(ctx);
+ ctx->state=NR_STUN_CLIENT_STATE_WAITING;
+
+ ctx->request_ct = ctx->maximum_transmits;
+ ctx->timeout_ms = ctx->maximum_transmits_timeout_ms;
+ NR_ASYNC_TIMER_SET(ctx->timeout_ms, nr_stun_client_timer_expired_cb, ctx, &ctx->timer_handle);
+
+ return(0);
+ }
+
+int nr_stun_client_failed(nr_stun_client_ctx *ctx)
+ {
+ nr_stun_client_cancel(ctx);
+ ctx->state=NR_STUN_CLIENT_STATE_FAILED;
+ nr_stun_client_fire_finished_cb(ctx);
+ return(0);
+ }
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_client_ctx.h b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_client_ctx.h
new file mode 100644
index 0000000000..0cc9045e2a
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_client_ctx.h
@@ -0,0 +1,200 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+
+#ifndef _stun_client_ctx_h
+#define _stun_client_ctx_h
+
+/* forward declaration */
+typedef struct nr_stun_client_ctx_ nr_stun_client_ctx;
+
+#include "stun.h"
+
+/* Checklist for adding new STUN transaction types
+
+ 1. Add new method type in stun.h (NR_METHOD_*)
+ 2. Add new MSGs in stun.h (NR_STUN_MSG_*)
+ 3. Add new messages to stun_util.c:nr_stun_msg_type
+ 4. Add new request type to stun_build.h
+ 4. Add new message builder to stun_build.c
+ 5. Add new response type to stun_client_ctx.h
+ 6. Add new arm to stun_client_ctx.c:nr_stun_client_send_request
+ 7. Add new arms to nr_stun_client_process_response
+ 8. Add new arms to stun_hint.c:nr_is_stun_message
+*/
+
+
+
+
+typedef union nr_stun_client_params_ {
+
+ nr_stun_client_stun_binding_request_params stun_binding_request;
+ nr_stun_client_stun_keepalive_params stun_keepalive;
+#ifdef USE_STUND_0_96
+ nr_stun_client_stun_binding_request_stund_0_96_params stun_binding_request_stund_0_96;
+#endif /* USE_STUND_0_96 */
+
+#ifdef USE_ICE
+ nr_stun_client_ice_binding_request_params ice_binding_request;
+#endif /* USE_ICE */
+
+#ifdef USE_TURN
+ nr_stun_client_allocate_request_params allocate_request;
+ nr_stun_client_refresh_request_params refresh_request;
+ nr_stun_client_permission_request_params permission_request;
+ nr_stun_client_send_indication_params send_indication;
+#endif /* USE_TURN */
+
+} nr_stun_client_params;
+
+typedef struct nr_stun_client_stun_binding_response_results_ {
+ nr_transport_addr mapped_addr;
+} nr_stun_client_stun_binding_response_results;
+
+typedef struct nr_stun_client_stun_binding_response_stund_0_96_results_ {
+ nr_transport_addr mapped_addr;
+} nr_stun_client_stun_binding_response_stund_0_96_results;
+
+#ifdef USE_ICE
+typedef struct nr_stun_client_ice_use_candidate_results_ {
+#ifdef WIN32 // silly VC++ gives error if no members
+ int dummy;
+#endif
+} nr_stun_client_ice_use_candidate_results;
+
+typedef struct nr_stun_client_ice_binding_response_results_ {
+ nr_transport_addr mapped_addr;
+} nr_stun_client_ice_binding_response_results;
+#endif /* USE_ICE */
+
+#ifdef USE_TURN
+typedef struct nr_stun_client_allocate_response_results_ {
+ nr_transport_addr relay_addr;
+ nr_transport_addr mapped_addr;
+ UINT4 lifetime_secs;
+} nr_stun_client_allocate_response_results;
+
+typedef struct nr_stun_client_refresh_response_results_ {
+ UINT4 lifetime_secs;
+} nr_stun_client_refresh_response_results;
+
+typedef struct nr_stun_client_permission_response_results_ {
+ UINT4 lifetime_secs;
+} nr_stun_client_permission_response_results;
+
+#endif /* USE_TURN */
+
+typedef union nr_stun_client_results_ {
+ nr_stun_client_stun_binding_response_results stun_binding_response;
+ nr_stun_client_stun_binding_response_stund_0_96_results stun_binding_response_stund_0_96;
+
+#ifdef USE_ICE
+ nr_stun_client_ice_use_candidate_results ice_use_candidate;
+ nr_stun_client_ice_binding_response_results ice_binding_response;
+#endif /* USE_ICE */
+
+#ifdef USE_TURN
+ nr_stun_client_allocate_response_results allocate_response;
+ nr_stun_client_refresh_response_results refresh_response;
+#endif /* USE_TURN */
+} nr_stun_client_results;
+
+struct nr_stun_client_ctx_ {
+ int state;
+#define NR_STUN_CLIENT_STATE_INITTED 0
+#define NR_STUN_CLIENT_STATE_RUNNING 1
+#define NR_STUN_CLIENT_STATE_DONE 2
+#define NR_STUN_CLIENT_STATE_FAILED 3
+#define NR_STUN_CLIENT_STATE_TIMED_OUT 4
+#define NR_STUN_CLIENT_STATE_CANCELLED 5
+#define NR_STUN_CLIENT_STATE_WAITING 6
+
+ int mode;
+#define NR_STUN_CLIENT_MODE_BINDING_REQUEST_SHORT_TERM_AUTH 1
+#define NR_STUN_CLIENT_MODE_BINDING_REQUEST_LONG_TERM_AUTH 2
+#define NR_STUN_CLIENT_MODE_BINDING_REQUEST_NO_AUTH 3
+#define NR_STUN_CLIENT_MODE_KEEPALIVE 4
+#define NR_STUN_CLIENT_MODE_BINDING_REQUEST_STUND_0_96 5
+#ifdef USE_ICE
+#define NR_ICE_CLIENT_MODE_USE_CANDIDATE 10
+#define NR_ICE_CLIENT_MODE_BINDING_REQUEST 11
+#endif /* USE_ICE */
+#ifdef USE_TURN
+#define NR_TURN_CLIENT_MODE_ALLOCATE_REQUEST 20
+#define NR_TURN_CLIENT_MODE_REFRESH_REQUEST 21
+#define NR_TURN_CLIENT_MODE_SEND_INDICATION 22
+#define NR_TURN_CLIENT_MODE_DATA_INDICATION 24
+#define NR_TURN_CLIENT_MODE_PERMISSION_REQUEST 25
+#endif /* USE_TURN */
+
+ char *label;
+ nr_transport_addr my_addr;
+ nr_transport_addr peer_addr;
+ nr_socket *sock;
+ nr_stun_client_auth_params auth_params;
+ nr_stun_client_params params;
+ nr_stun_client_results results;
+ char *nonce;
+ char *realm;
+ void *timer_handle;
+ UINT2 request_ct;
+ UINT2 retransmit_ct;
+ UINT4 rto_ms; /* retransmission time out */
+ double retransmission_backoff_factor;
+ UINT4 maximum_transmits;
+ UINT4 maximum_transmits_timeout_ms;
+ UINT4 mapped_addr_check_mask; /* What checks to run on mapped addresses */
+ int timeout_ms;
+ struct timeval timer_set;
+ NR_async_cb finished_cb;
+ void *cb_arg;
+ nr_stun_message *request;
+ nr_stun_message *response;
+ UINT2 error_code;
+};
+
+int nr_stun_client_ctx_create(char *label, nr_socket *sock, nr_transport_addr *peer, UINT4 RTO, nr_stun_client_ctx **ctxp);
+int nr_stun_client_start(nr_stun_client_ctx *ctx, int mode, NR_async_cb finished_cb, void *cb_arg);
+int nr_stun_client_restart(nr_stun_client_ctx* ctx,
+ const nr_transport_addr* peer_addr);
+int nr_stun_client_force_retransmit(nr_stun_client_ctx *ctx);
+int nr_stun_client_reset(nr_stun_client_ctx *ctx);
+int nr_stun_client_ctx_destroy(nr_stun_client_ctx **ctxp);
+int nr_stun_transport_addr_check(nr_transport_addr* addr, UINT4 mask);
+int nr_stun_client_process_response(nr_stun_client_ctx *ctx, UCHAR *msg, int len, nr_transport_addr *peer_addr);
+int nr_stun_client_cancel(nr_stun_client_ctx *ctx);
+int nr_stun_client_wait(nr_stun_client_ctx *ctx);
+int nr_stun_client_failed(nr_stun_client_ctx *ctx);
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_codec.c b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_codec.c
new file mode 100644
index 0000000000..ae748a667b
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_codec.c
@@ -0,0 +1,1550 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <errno.h>
+#include <csi_platform.h>
+
+#ifdef WIN32
+#include <winsock2.h>
+#include <stdlib.h>
+#include <io.h>
+#include <time.h>
+#else /* UNIX */
+#include <string.h>
+#endif /* end UNIX */
+#include <assert.h>
+#include <stddef.h>
+
+#include "nr_api.h"
+#include "stun.h"
+#include "byteorder.h"
+#include "r_crc32.h"
+#include "nr_crypto.h"
+
+#define NR_STUN_IPV4_FAMILY 0x01
+#define NR_STUN_IPV6_FAMILY 0x02
+
+#define SKIP_ATTRIBUTE_DECODE -1
+
+static int nr_stun_find_attr_info(UINT2 type, nr_stun_attr_info **info);
+
+static int nr_stun_fix_attribute_ordering(nr_stun_message *msg);
+
+static int nr_stun_encode_htons(UINT2 data, size_t buflen, UCHAR *buf, size_t *offset);
+static int nr_stun_encode_htonl(UINT4 data, size_t buflen, UCHAR *buf, size_t *offset);
+static int nr_stun_encode_htonll(UINT8 data, size_t buflen, UCHAR *buf, size_t *offset);
+static int nr_stun_encode(UCHAR *data, size_t length, size_t buflen, UCHAR *buf, size_t *offset);
+
+static int nr_stun_decode_htons(UCHAR *buf, size_t buflen, size_t *offset, UINT2 *data);
+static int nr_stun_decode_htonl(UCHAR *buf, size_t buflen, size_t *offset, UINT4 *data);
+static int nr_stun_decode_htonll(UCHAR *buf, size_t buflen, size_t *offset, UINT8 *data);
+static int nr_stun_decode(size_t length, UCHAR *buf, size_t buflen, size_t *offset, UCHAR *data);
+
+static int nr_stun_attr_string_illegal(nr_stun_attr_info *attr_info, size_t len, void *data, size_t max_bytes, size_t max_chars);
+
+static int nr_stun_attr_error_code_illegal(nr_stun_attr_info *attr_info, size_t attrlen, void *data);
+static int nr_stun_attr_nonce_illegal(nr_stun_attr_info *attr_info, size_t attrlen, void *data);
+static int nr_stun_attr_realm_illegal(nr_stun_attr_info *attr_info, size_t attrlen, void *data);
+static int nr_stun_attr_server_illegal(nr_stun_attr_info *attr_info, size_t attrlen, void *data);
+static int nr_stun_attr_username_illegal(nr_stun_attr_info *attr_info, size_t attrlen, void *data);
+static int
+nr_stun_attr_codec_fingerprint_decode(nr_stun_attr_info *attr_info, size_t attrlen, UCHAR *buf, size_t offset, size_t buflen, void *data);
+
+
+int
+nr_stun_encode_htons(UINT2 data, size_t buflen, UCHAR *buf, size_t *offset)
+{
+ UINT2 d = htons(data);
+
+ if (*offset + sizeof(d) >= buflen) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Attempted buffer overrun: %d + %zd >= %d", *offset, sizeof(d), buflen);
+ return R_BAD_DATA;
+ }
+
+ memcpy(&buf[*offset], &d, sizeof(d));
+ *offset += sizeof(d);
+
+ return 0;
+}
+
+int
+nr_stun_encode_htonl(UINT4 data, size_t buflen, UCHAR *buf, size_t *offset)
+{
+ UINT4 d = htonl(data);
+
+ if (*offset + sizeof(d) > buflen) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Attempted buffer overrun: %d + %zd > %d", *offset, sizeof(d), buflen);
+ return R_BAD_DATA;
+ }
+
+ memcpy(&buf[*offset], &d, sizeof(d));
+ *offset += sizeof(d);
+
+ return 0;
+}
+
+int
+nr_stun_encode_htonll(UINT8 data, size_t buflen, UCHAR *buf, size_t *offset)
+{
+ UINT8 d = nr_htonll(data);
+
+ if (*offset + sizeof(d) > buflen) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Attempted buffer overrun: %d + %zd > %d", *offset, sizeof(d), buflen);
+ return R_BAD_DATA;
+ }
+
+ memcpy(&buf[*offset], &d, sizeof(d));
+ *offset += sizeof(d);
+
+ return 0;
+}
+
+int
+nr_stun_encode(UCHAR *data, size_t length, size_t buflen, UCHAR *buf, size_t *offset)
+{
+ if (*offset + length > buflen) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Attempted buffer overrun: %d + %d > %d", *offset, length, buflen);
+ return R_BAD_DATA;
+ }
+
+ memcpy(&buf[*offset], data, length);
+ *offset += length;
+
+ return 0;
+}
+
+
+int
+nr_stun_decode_htons(UCHAR *buf, size_t buflen, size_t *offset, UINT2 *data)
+{
+ UINT2 d;
+
+ if (*offset + sizeof(d) > buflen) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Attempted buffer overrun: %d + %zd > %d", *offset, sizeof(d), buflen);
+ return R_BAD_DATA;
+ }
+
+ memcpy(&d, &buf[*offset], sizeof(d));
+ *offset += sizeof(d);
+ *data = htons(d);
+
+ return 0;
+}
+
+int
+nr_stun_decode_htonl(UCHAR *buf, size_t buflen, size_t *offset, UINT4 *data)
+{
+ UINT4 d;
+
+ if (*offset + sizeof(d) > buflen) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Attempted buffer overrun: %d + %zd > %d", *offset, sizeof(d), buflen);
+ return R_BAD_DATA;
+ }
+
+ memcpy(&d, &buf[*offset], sizeof(d));
+ *offset += sizeof(d);
+ *data = htonl(d);
+
+ return 0;
+}
+
+int
+nr_stun_decode_htonll(UCHAR *buf, size_t buflen, size_t *offset, UINT8 *data)
+{
+ UINT8 d;
+
+ if (*offset + sizeof(d) > buflen) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Attempted buffer overrun: %d + %zd > %d", *offset, sizeof(d), buflen);
+ return R_BAD_DATA;
+ }
+
+ memcpy(&d, &buf[*offset], sizeof(d));
+ *offset += sizeof(d);
+ *data = nr_htonll(d);
+
+ return 0;
+}
+
+int
+nr_stun_decode(size_t length, UCHAR *buf, size_t buflen, size_t *offset, UCHAR *data)
+{
+ if (*offset + length > buflen) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Attempted buffer overrun: %d + %d > %d", *offset, length, buflen);
+ return R_BAD_DATA;
+ }
+
+ memcpy(data, &buf[*offset], length);
+ *offset += length;
+
+ return 0;
+}
+
+/**
+ * The argument must be a non-null pointer to a zero-terminated string.
+ *
+ * If the argument is valid UTF-8, returns the number of code points in the
+ * string excluding the zero-terminator.
+ *
+ * If the argument is invalid UTF-8, returns a lower bound for the number of
+ * code points in the string. (If UTF-8 error handling was performed on the
+ * string, new REPLACEMENT CHARACTER code points could be introduced in
+ * a way that would increase the total number of code points compared to
+ * what this function counts.)
+ */
+size_t
+nr_count_utf8_code_points_without_validation(const char *s) {
+ size_t nchars = 0;
+ char c;
+ while ((c = *s)) {
+ if ((c & 0xC0) != 0x80) {
+ ++nchars;
+ }
+ ++s;
+ }
+ return nchars;
+}
+
+int
+nr_stun_attr_string_illegal(nr_stun_attr_info *attr_info, size_t len, void *data, size_t max_bytes, size_t max_chars)
+{
+ int _status;
+ char *s = data;
+ size_t nchars;
+
+ if (len > max_bytes) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "%s is too large: %d bytes", attr_info->name, len);
+ ABORT(R_FAILED);
+ }
+
+ nchars = nr_count_utf8_code_points_without_validation(s);
+ if (nchars > max_chars) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "%s is too large: %zd characters", attr_info->name, nchars);
+ ABORT(R_FAILED);
+ }
+
+ _status = 0;
+ abort:
+ return _status;
+}
+
+int
+nr_stun_attr_error_code_illegal(nr_stun_attr_info *attr_info, size_t attrlen, void *data)
+{
+ int r,_status;
+ nr_stun_attr_error_code *ec = data;
+
+ if (ec->number < 300 || ec->number > 699)
+ ABORT(R_FAILED);
+
+ if ((r=nr_stun_attr_string_illegal(attr_info, strlen(ec->reason), ec->reason, NR_STUN_MAX_ERROR_CODE_REASON_BYTES, NR_STUN_MAX_ERROR_CODE_REASON_CHARS)))
+ ABORT(r);
+
+ _status = 0;
+ abort:
+ return _status;
+}
+
+int
+nr_stun_attr_nonce_illegal(nr_stun_attr_info *attr_info, size_t attrlen, void *data)
+{
+ return nr_stun_attr_string_illegal(attr_info, attrlen, data, NR_STUN_MAX_NONCE_BYTES, NR_STUN_MAX_NONCE_CHARS);
+}
+
+int
+nr_stun_attr_realm_illegal(nr_stun_attr_info *attr_info, size_t attrlen, void *data)
+{
+ return nr_stun_attr_string_illegal(attr_info, attrlen, data, NR_STUN_MAX_REALM_BYTES, NR_STUN_MAX_REALM_CHARS);
+}
+
+int
+nr_stun_attr_server_illegal(nr_stun_attr_info *attr_info, size_t attrlen, void *data)
+{
+ return nr_stun_attr_string_illegal(attr_info, attrlen, data, NR_STUN_MAX_SERVER_BYTES, NR_STUN_MAX_SERVER_CHARS);
+}
+
+int
+nr_stun_attr_username_illegal(nr_stun_attr_info *attr_info, size_t attrlen, void *data)
+{
+ return nr_stun_attr_string_illegal(attr_info, attrlen, data, NR_STUN_MAX_USERNAME_BYTES, -1);
+}
+
+static int
+nr_stun_attr_codec_UCHAR_print(nr_stun_attr_info *attr_info, char *msg, void *data)
+{
+ r_log(NR_LOG_STUN, LOG_DEBUG, "%s %s: %u", msg, attr_info->name, *(UCHAR*)data);
+ return 0;
+}
+
+static int
+nr_stun_attr_codec_UCHAR_encode(nr_stun_attr_info *attr_info, void *data, size_t offset, size_t buflen, UCHAR *buf, size_t *attrlen)
+{
+ int start = offset;
+ UINT4 tmp = *((UCHAR *)data);
+ tmp <<= 24;
+
+ if (nr_stun_encode_htons(attr_info->type , buflen, buf, &offset)
+ || nr_stun_encode_htons(sizeof(UINT4) , buflen, buf, &offset)
+ || nr_stun_encode_htonl(tmp , buflen, buf, &offset))
+ return R_FAILED;
+
+ *attrlen = offset - start;
+
+ return 0;
+}
+
+static int
+nr_stun_attr_codec_UCHAR_decode(nr_stun_attr_info *attr_info, size_t attrlen, UCHAR *buf, size_t offset, size_t buflen, void *data)
+{
+ UINT4 tmp;
+
+ if (attrlen != sizeof(UINT4)) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Integer is illegal size: %d", attrlen);
+ return R_FAILED;
+ }
+
+ if (nr_stun_decode_htonl(buf, buflen, &offset, &tmp))
+ return R_FAILED;
+
+ *((UCHAR *)data) = (tmp >> 24) & 0xff;
+
+ return 0;
+}
+
+nr_stun_attr_codec nr_stun_attr_codec_UCHAR = {
+ "UCHAR",
+ nr_stun_attr_codec_UCHAR_print,
+ nr_stun_attr_codec_UCHAR_encode,
+ nr_stun_attr_codec_UCHAR_decode
+};
+
+static int
+nr_stun_attr_codec_UINT4_print(nr_stun_attr_info *attr_info, char *msg, void *data)
+{
+ r_log(NR_LOG_STUN, LOG_DEBUG, "%s %s: %u", msg, attr_info->name, *(UINT4*)data);
+ return 0;
+}
+
+static int
+nr_stun_attr_codec_UINT4_encode(nr_stun_attr_info *attr_info, void *data, size_t offset, size_t buflen, UCHAR *buf, size_t *attrlen)
+{
+ int start = offset;
+
+ if (nr_stun_encode_htons(attr_info->type , buflen, buf, &offset)
+ || nr_stun_encode_htons(sizeof(UINT4) , buflen, buf, &offset)
+ || nr_stun_encode_htonl(*(UINT4*)data , buflen, buf, &offset))
+ return R_FAILED;
+
+ *attrlen = offset - start;
+
+ return 0;
+}
+
+static int
+nr_stun_attr_codec_UINT4_decode(nr_stun_attr_info *attr_info, size_t attrlen, UCHAR *buf, size_t offset, size_t buflen, void *data)
+{
+ if (attrlen != sizeof(UINT4)) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Integer is illegal size: %d", attrlen);
+ return R_FAILED;
+ }
+
+ if (nr_stun_decode_htonl(buf, buflen, &offset, (UINT4*)data))
+ return R_FAILED;
+
+ return 0;
+}
+
+nr_stun_attr_codec nr_stun_attr_codec_UINT4 = {
+ "UINT4",
+ nr_stun_attr_codec_UINT4_print,
+ nr_stun_attr_codec_UINT4_encode,
+ nr_stun_attr_codec_UINT4_decode
+};
+
+static int
+nr_stun_attr_codec_UINT8_print(nr_stun_attr_info *attr_info, char *msg, void *data)
+{
+ r_log(NR_LOG_STUN, LOG_DEBUG, "%s %s: %llu", msg, attr_info->name, *(UINT8*)data);
+ return 0;
+}
+
+static int
+nr_stun_attr_codec_UINT8_encode(nr_stun_attr_info *attr_info, void *data, size_t offset, size_t buflen, UCHAR *buf, size_t *attrlen)
+{
+ int start = offset;
+
+ if (nr_stun_encode_htons(attr_info->type , buflen, buf, &offset)
+ || nr_stun_encode_htons(sizeof(UINT8) , buflen, buf, &offset)
+ || nr_stun_encode_htonll(*(UINT8*)data , buflen, buf, &offset))
+ return R_FAILED;
+
+ *attrlen = offset - start;
+
+ return 0;
+}
+
+static int
+nr_stun_attr_codec_UINT8_decode(nr_stun_attr_info *attr_info, size_t attrlen, UCHAR *buf, size_t offset, size_t buflen, void *data)
+{
+ if (attrlen != sizeof(UINT8)) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Integer is illegal size: %d", attrlen);
+ return R_FAILED;
+ }
+
+ if (nr_stun_decode_htonll(buf, buflen, &offset, (UINT8*)data))
+ return R_FAILED;
+
+ return 0;
+}
+
+nr_stun_attr_codec nr_stun_attr_codec_UINT8 = {
+ "UINT8",
+ nr_stun_attr_codec_UINT8_print,
+ nr_stun_attr_codec_UINT8_encode,
+ nr_stun_attr_codec_UINT8_decode
+};
+
+static int
+nr_stun_attr_codec_addr_print(nr_stun_attr_info *attr_info, char *msg, void *data)
+{
+ r_log(NR_LOG_STUN, LOG_DEBUG, "%s %s: %s", msg, attr_info->name, ((nr_transport_addr*)data)->as_string);
+ return 0;
+}
+
+static int
+nr_stun_attr_codec_addr_encode(nr_stun_attr_info *attr_info, void *data, size_t offset, size_t buflen, UCHAR *buf, size_t *attrlen)
+{
+ int r,_status;
+ int start = offset;
+ nr_transport_addr *addr = data;
+ UCHAR pad = '\0';
+ UCHAR family;
+
+ if ((r=nr_stun_encode_htons(attr_info->type, buflen, buf, &offset)))
+ ABORT(r);
+
+ switch (addr->ip_version) {
+ case NR_IPV4:
+ family = NR_STUN_IPV4_FAMILY;
+ if (nr_stun_encode_htons(8 , buflen, buf, &offset)
+ || nr_stun_encode(&pad, 1 , buflen, buf, &offset)
+ || nr_stun_encode(&family, 1 , buflen, buf, &offset)
+ || nr_stun_encode_htons(ntohs(addr->u.addr4.sin_port), buflen, buf, &offset)
+ || nr_stun_encode_htonl(ntohl(addr->u.addr4.sin_addr.s_addr), buflen, buf, &offset))
+ ABORT(R_FAILED);
+ break;
+
+ case NR_IPV6:
+ family = NR_STUN_IPV6_FAMILY;
+ if (nr_stun_encode_htons(20 , buflen, buf, &offset)
+ || nr_stun_encode(&pad, 1 , buflen, buf, &offset)
+ || nr_stun_encode(&family, 1 , buflen, buf, &offset)
+ || nr_stun_encode_htons(ntohs(addr->u.addr6.sin6_port), buflen, buf, &offset)
+ || nr_stun_encode(addr->u.addr6.sin6_addr.s6_addr, 16, buflen, buf, &offset))
+ ABORT(R_FAILED);
+ break;
+
+ default:
+ assert(0);
+ ABORT(R_INTERNAL);
+ break;
+ }
+
+ *attrlen = offset - start;
+
+ _status = 0;
+ abort:
+ return _status;
+}
+
+static int
+nr_stun_attr_codec_addr_decode(nr_stun_attr_info *attr_info, size_t attrlen, UCHAR *buf, size_t offset, size_t buflen, void *data)
+{
+ int _status;
+ UCHAR pad;
+ UCHAR family;
+ UINT2 port;
+ UINT4 addr4;
+ struct in6_addr addr6;
+ nr_transport_addr *result = data;
+
+ if (nr_stun_decode(1, buf, buflen, &offset, &pad)
+ || nr_stun_decode(1, buf, buflen, &offset, &family))
+ ABORT(R_FAILED);
+
+ switch (family) {
+ case NR_STUN_IPV4_FAMILY:
+ if (attrlen != 8) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Illegal attribute length: %d", attrlen);
+ ABORT(R_FAILED);
+ }
+
+ if (nr_stun_decode_htons(buf, buflen, &offset, &port)
+ || nr_stun_decode_htonl(buf, buflen, &offset, &addr4))
+ ABORT(R_FAILED);
+
+ if (nr_ip4_port_to_transport_addr(addr4, port, IPPROTO_UDP, result))
+ ABORT(R_FAILED);
+ break;
+
+ case NR_STUN_IPV6_FAMILY:
+ if (attrlen != 20) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Illegal attribute length: %d", attrlen);
+ ABORT(R_FAILED);
+ }
+
+ if (nr_stun_decode_htons(buf, buflen, &offset, &port)
+ || nr_stun_decode(16, buf, buflen, &offset, addr6.s6_addr))
+ ABORT(R_FAILED);
+
+ if (nr_ip6_port_to_transport_addr(&addr6, port, IPPROTO_UDP, result))
+ ABORT(R_FAILED);
+ break;
+
+ default:
+ r_log(NR_LOG_STUN, LOG_WARNING, "Illegal address family: %d", family);
+ ABORT(R_FAILED);
+ break;
+ }
+
+ _status = 0;
+ abort:
+ return _status;
+}
+
+nr_stun_attr_codec nr_stun_attr_codec_addr = {
+ "addr",
+ nr_stun_attr_codec_addr_print,
+ nr_stun_attr_codec_addr_encode,
+ nr_stun_attr_codec_addr_decode
+};
+
+static int
+nr_stun_attr_codec_data_print(nr_stun_attr_info *attr_info, char *msg, void *data)
+{
+ nr_stun_attr_data *d = data;
+ r_dump(NR_LOG_STUN, LOG_DEBUG, attr_info->name, (char*)d->data, d->length);
+ return 0;
+}
+
+static int
+nr_stun_attr_codec_data_encode(nr_stun_attr_info *attr_info, void *data, size_t offset, size_t buflen, UCHAR *buf, size_t *attrlen)
+{
+ nr_stun_attr_data *d = data;
+ int start = offset;
+
+ if (nr_stun_encode_htons(attr_info->type , buflen, buf, &offset)
+ || nr_stun_encode_htons(d->length , buflen, buf, &offset)
+ || nr_stun_encode(d->data, d->length , buflen, buf, &offset))
+ return R_FAILED;
+
+ *attrlen = offset - start;
+
+ return 0;
+}
+
+static int
+nr_stun_attr_codec_data_decode(nr_stun_attr_info *attr_info, size_t attrlen, UCHAR *buf, size_t offset, size_t buflen, void *data)
+{
+ int _status;
+ nr_stun_attr_data *result = data;
+
+ /* -1 because it is going to be null terminated just to be safe */
+ if (attrlen >= (sizeof(result->data) - 1)) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Too much data: %d bytes", attrlen);
+ ABORT(R_FAILED);
+ }
+
+ if (nr_stun_decode(attrlen, buf, buflen, &offset, result->data))
+ ABORT(R_FAILED);
+
+ result->length = attrlen;
+ result->data[attrlen] = '\0'; /* just to be nice */
+
+ _status=0;
+ abort:
+ return _status;
+}
+
+nr_stun_attr_codec nr_stun_attr_codec_data = {
+ "data",
+ nr_stun_attr_codec_data_print,
+ nr_stun_attr_codec_data_encode,
+ nr_stun_attr_codec_data_decode
+};
+
+static int
+nr_stun_attr_codec_error_code_print(nr_stun_attr_info *attr_info, char *msg, void *data)
+{
+ nr_stun_attr_error_code *error_code = data;
+ r_log(NR_LOG_STUN, LOG_DEBUG, "%s %s: %d %s",
+ msg, attr_info->name, error_code->number,
+ error_code->reason);
+ return 0;
+}
+
+static int
+nr_stun_attr_codec_error_code_encode(nr_stun_attr_info *attr_info, void *data, size_t offset, size_t buflen, UCHAR *buf, size_t *attrlen)
+{
+ nr_stun_attr_error_code *error_code = data;
+ int start = offset;
+ int length = strlen(error_code->reason);
+ UCHAR pad[2] = { 0 };
+ UCHAR class = error_code->number / 100;
+ UCHAR number = error_code->number % 100;
+
+ if (nr_stun_encode_htons(attr_info->type , buflen, buf, &offset)
+ || nr_stun_encode_htons(4 + length , buflen, buf, &offset)
+ || nr_stun_encode(pad, 2 , buflen, buf, &offset)
+ || nr_stun_encode(&class, 1 , buflen, buf, &offset)
+ || nr_stun_encode(&number, 1 , buflen, buf, &offset)
+ || nr_stun_encode((UCHAR*)error_code->reason, length, buflen, buf, &offset))
+ return R_FAILED;
+
+ *attrlen = offset - start;
+
+ return 0;
+}
+
+static int
+nr_stun_attr_codec_error_code_decode(nr_stun_attr_info *attr_info, size_t attrlen, UCHAR *buf, size_t offset, size_t buflen, void *data)
+{
+ int _status;
+ nr_stun_attr_error_code *result = data;
+ UCHAR pad[2];
+ UCHAR class;
+ UCHAR number;
+ size_t size_reason;
+
+ if (nr_stun_decode(2, buf, buflen, &offset, pad)
+ || nr_stun_decode(1, buf, buflen, &offset, &class)
+ || nr_stun_decode(1, buf, buflen, &offset, &number))
+ ABORT(R_FAILED);
+
+ result->number = (class * 100) + number;
+
+ size_reason = attrlen - 4;
+
+ /* -1 because the string will be null terminated */
+ if (size_reason > (sizeof(result->reason) - 1)) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Reason is too large, truncating");
+ /* don't fail, but instead truncate the reason */
+ size_reason = sizeof(result->reason) - 1;
+ }
+
+ if (nr_stun_decode(size_reason, buf, buflen, &offset, (UCHAR*)result->reason))
+ ABORT(R_FAILED);
+ result->reason[size_reason] = '\0';
+
+ _status=0;
+ abort:
+ return _status;
+}
+
+nr_stun_attr_codec nr_stun_attr_codec_error_code = {
+ "error_code",
+ nr_stun_attr_codec_error_code_print,
+ nr_stun_attr_codec_error_code_encode,
+ nr_stun_attr_codec_error_code_decode
+};
+
+static int
+nr_stun_attr_codec_fingerprint_print(nr_stun_attr_info *attr_info, char *msg, void *data)
+{
+ nr_stun_attr_fingerprint *fingerprint = data;
+ r_log(NR_LOG_STUN, LOG_DEBUG, "%s %s: %08x", msg, attr_info->name, fingerprint->checksum);
+ return 0;
+}
+
+static int
+nr_stun_attr_codec_fingerprint_encode(nr_stun_attr_info *attr_info, void *data, size_t offset, size_t buflen, UCHAR *buf, size_t *attrlen)
+{
+ UINT4 checksum;
+ nr_stun_attr_fingerprint *fingerprint = data;
+ nr_stun_message_header *header = (nr_stun_message_header*)buf;
+
+ /* the length must include the FINGERPRINT attribute when computing
+ * the fingerprint */
+ header->length = ntohs(header->length);
+ header->length += 8; /* Fingerprint */
+ header->length = htons(header->length);
+
+ if (r_crc32((char*)buf, offset, &checksum)) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Unable to compute fingerprint");
+ return R_FAILED;
+ }
+
+ fingerprint->checksum = checksum ^ 0x5354554e;
+
+ r_log(NR_LOG_STUN, LOG_DEBUG, "Computed FINGERPRINT %08x", fingerprint->checksum);
+
+ fingerprint->valid = 1;
+ return nr_stun_attr_codec_UINT4.encode(attr_info, &fingerprint->checksum, offset, buflen, buf, attrlen);
+}
+
+static int
+nr_stun_attr_codec_fingerprint_decode(nr_stun_attr_info *attr_info, size_t attrlen, UCHAR *buf, size_t offset, size_t buflen, void *data)
+{
+ int r,_status;
+ nr_stun_attr_fingerprint *fingerprint = data;
+ nr_stun_message_header *header = (nr_stun_message_header*)buf;
+ size_t length;
+ UINT4 checksum;
+
+ if ((r=nr_stun_attr_codec_UINT4.decode(attr_info, attrlen, buf, offset, buflen, &fingerprint->checksum)))
+ ABORT(r);
+
+ offset -= 4; /* rewind to before the length and type fields */
+
+ /* the length must include the FINGERPRINT attribute when computing
+ * the fingerprint */
+ length = offset; /* right before FINGERPRINT */
+ length -= sizeof(*header); /* remove header length */
+ length += 8; /* add length of Fingerprint */
+ header->length = htons(length);
+
+ /* make sure FINGERPRINT is final attribute in message */
+ if (length + sizeof(*header) != buflen) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Fingerprint is not final attribute in message");
+ ABORT(R_FAILED);
+ }
+
+ if (r_crc32((char*)buf, offset, &checksum)) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Unable to compute fingerprint");
+ ABORT(R_FAILED);
+ }
+
+ fingerprint->valid = (fingerprint->checksum == (checksum ^ 0x5354554e));
+
+ r_log(NR_LOG_STUN, LOG_DEBUG, "Computed FINGERPRINT %08x", (checksum ^ 0x5354554e));
+ if (! fingerprint->valid)
+ r_log(NR_LOG_STUN, LOG_WARNING, "Invalid FINGERPRINT %08x", fingerprint->checksum);
+
+ _status=0;
+ abort:
+ return _status;
+}
+
+nr_stun_attr_codec nr_stun_attr_codec_fingerprint = {
+ "fingerprint",
+ nr_stun_attr_codec_fingerprint_print,
+ nr_stun_attr_codec_fingerprint_encode,
+ nr_stun_attr_codec_fingerprint_decode
+};
+
+static int
+nr_stun_attr_codec_flag_print(nr_stun_attr_info *attr_info, char *msg, void *data)
+{
+ r_log(NR_LOG_STUN, LOG_DEBUG, "%s %s: on", msg, attr_info->name);
+ return 0;
+}
+
+static int
+nr_stun_attr_codec_flag_encode(nr_stun_attr_info *attr_info, void *data, size_t offset, size_t buflen, UCHAR *buf, size_t *attrlen)
+{
+ int start = offset;
+
+ if (nr_stun_encode_htons(attr_info->type , buflen, buf, &offset)
+ || nr_stun_encode_htons(0 , buflen, buf, &offset))
+ return R_FAILED;
+
+ *attrlen = offset - start;
+
+ return 0;
+}
+
+static int
+nr_stun_attr_codec_flag_decode(nr_stun_attr_info *attr_info, size_t attrlen, UCHAR *buf, size_t offset, size_t buflen, void *data)
+{
+ if (attrlen != 0) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Illegal flag length: %d", attrlen);
+ return R_FAILED;
+ }
+
+ return 0;
+}
+
+nr_stun_attr_codec nr_stun_attr_codec_flag = {
+ "flag",
+ nr_stun_attr_codec_flag_print,
+ nr_stun_attr_codec_flag_encode,
+ nr_stun_attr_codec_flag_decode
+};
+
+static int
+nr_stun_attr_codec_message_integrity_print(nr_stun_attr_info *attr_info, char *msg, void *data)
+{
+ nr_stun_attr_message_integrity *integrity = data;
+ r_dump(NR_LOG_STUN, LOG_DEBUG, attr_info->name, (char*)integrity->hash, sizeof(integrity->hash));
+ return 0;
+}
+
+static int
+nr_stun_compute_message_integrity(UCHAR *buf, int offset, UCHAR *password, int passwordlen, UCHAR *computedHMAC)
+{
+ int r,_status;
+ UINT2 hold;
+ UINT2 length;
+ nr_stun_message_header *header;
+
+ r_log(NR_LOG_STUN, LOG_DEBUG, "Computing MESSAGE-INTEGRITY");
+
+ header = (nr_stun_message_header*)buf;
+ hold = header->length;
+
+ /* adjust the length of the message */
+ length = offset;
+ length -= sizeof(*header);
+ length += 24; /* for MESSAGE-INTEGRITY attribute */
+ header->length = htons(length);
+
+ if ((r=nr_crypto_hmac_sha1((UCHAR*)password, passwordlen,
+ buf, offset, computedHMAC)))
+ ABORT(r);
+
+ r_dump(NR_LOG_STUN, LOG_DEBUG, "Computed MESSAGE-INTEGRITY ", (char*)computedHMAC, 20);
+
+ _status=0;
+ abort:
+ header->length = hold;
+ return _status;
+}
+
+static int
+nr_stun_attr_codec_message_integrity_encode(nr_stun_attr_info *attr_info, void *data, size_t offset, size_t buflen, UCHAR *buf, size_t *attrlen)
+{
+ int start = offset;
+ nr_stun_attr_message_integrity *integrity = data;
+
+ if (nr_stun_compute_message_integrity(buf, offset, integrity->password, integrity->passwordlen, integrity->hash))
+ return R_FAILED;
+
+ if (nr_stun_encode_htons(attr_info->type , buflen, buf, &offset)
+ || nr_stun_encode_htons(sizeof(integrity->hash) , buflen, buf, &offset)
+ || nr_stun_encode(integrity->hash, sizeof(integrity->hash) , buflen, buf, &offset))
+ return R_FAILED;
+
+ *attrlen = offset - start;
+
+ return 0;
+}
+
+static int
+nr_stun_attr_codec_message_integrity_decode(nr_stun_attr_info *attr_info, size_t attrlen, UCHAR *buf, size_t offset, size_t buflen, void *data)
+{
+ int _status;
+ int start;
+ nr_stun_attr_message_integrity *result = data;
+ UCHAR computedHMAC[20];
+
+ result->valid = 0;
+
+ if (attrlen != 20) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "%s must be 20 bytes, not %d", attr_info->name, attrlen);
+ ABORT(R_FAILED);
+ }
+
+ start = offset - 4; /* rewind to before the length and type fields */
+ if (start < 0)
+ ABORT(R_INTERNAL);
+
+ if (nr_stun_decode(attrlen, buf, buflen, &offset, result->hash))
+ ABORT(R_FAILED);
+
+ if (result->unknown_user) {
+ result->valid = 0;
+ }
+ else {
+ if (nr_stun_compute_message_integrity(buf, start, result->password, result->passwordlen, computedHMAC))
+ ABORT(R_FAILED);
+
+ assert(sizeof(computedHMAC) == sizeof(result->hash));
+
+ result->valid = (memcmp(computedHMAC, result->hash, 20) == 0);
+ }
+
+ _status=0;
+ abort:
+ return _status;
+}
+
+nr_stun_attr_codec nr_stun_attr_codec_message_integrity = {
+ "message_integrity",
+ nr_stun_attr_codec_message_integrity_print,
+ nr_stun_attr_codec_message_integrity_encode,
+ nr_stun_attr_codec_message_integrity_decode
+};
+
+static int
+nr_stun_attr_codec_noop_decode(nr_stun_attr_info *attr_info, size_t attrlen, UCHAR *buf, size_t offset, size_t buflen, void *data)
+{
+ return SKIP_ATTRIBUTE_DECODE;
+}
+
+nr_stun_attr_codec nr_stun_attr_codec_noop = {
+ "NOOP",
+ 0, /* ignore, never print these attributes */
+ 0, /* ignore, never encode these attributes */
+ nr_stun_attr_codec_noop_decode
+};
+
+static int
+nr_stun_attr_codec_quoted_string_print(nr_stun_attr_info *attr_info, char *msg, void *data)
+{
+ r_log(NR_LOG_STUN, LOG_DEBUG, "%s %s: %s",
+ msg, attr_info->name, (char*)data);
+ return 0;
+}
+
+static int
+nr_stun_attr_codec_quoted_string_encode(nr_stun_attr_info *attr_info, void *data, size_t offset, size_t buflen, UCHAR *buf, size_t *attrlen)
+{
+//TODO: !nn! syntax check, conversion if not quoted already?
+//We'll just restrict this in the API -- EKR
+ return nr_stun_attr_codec_string.encode(attr_info, data, offset, buflen, buf, attrlen);
+}
+
+static int
+nr_stun_attr_codec_quoted_string_decode(nr_stun_attr_info *attr_info, size_t attrlen, UCHAR *buf, size_t offset, size_t buflen, void *data)
+{
+//TODO: !nn! I don't see any need to unquote this but we may
+//find one later -- EKR
+ return nr_stun_attr_codec_string.decode(attr_info, attrlen, buf, offset, buflen, data);
+}
+
+nr_stun_attr_codec nr_stun_attr_codec_quoted_string = {
+ "quoted_string",
+ nr_stun_attr_codec_quoted_string_print,
+ nr_stun_attr_codec_quoted_string_encode,
+ nr_stun_attr_codec_quoted_string_decode
+};
+
+static int
+nr_stun_attr_codec_string_print(nr_stun_attr_info *attr_info, char *msg, void *data)
+{
+ r_log(NR_LOG_STUN, LOG_DEBUG, "%s %s: %s",
+ msg, attr_info->name, (char*)data);
+ return 0;
+}
+
+static int
+nr_stun_attr_codec_string_encode(nr_stun_attr_info *attr_info, void *data, size_t offset, size_t buflen, UCHAR *buf, size_t *attrlen)
+{
+ int start = offset;
+ char *str = data;
+ int length = strlen(str);
+
+ if (nr_stun_encode_htons(attr_info->type , buflen, buf, &offset)
+ || nr_stun_encode_htons(length , buflen, buf, &offset)
+ || nr_stun_encode((UCHAR*)str, length , buflen, buf, &offset))
+ return R_FAILED;
+
+ *attrlen = offset - start;
+
+ return 0;
+}
+
+static int
+nr_stun_attr_codec_string_decode(nr_stun_attr_info *attr_info, size_t attrlen, UCHAR *buf, size_t offset, size_t buflen, void *data)
+{
+ int _status;
+ char *result = data;
+
+ /* actual enforcement of the specific string size happens elsewhere */
+ if (attrlen >= NR_STUN_MAX_STRING_SIZE) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "String is too large: %d bytes", attrlen);
+ ABORT(R_FAILED);
+ }
+
+ if (nr_stun_decode(attrlen, buf, buflen, &offset, (UCHAR*)result))
+ ABORT(R_FAILED);
+ result[attrlen] = '\0'; /* just to be nice */
+
+ if (strlen(result) != attrlen) {
+ /* stund 0.96 sends a final null in the Server attribute, so
+ * only error if the null appears anywhere else in a string */
+ if (strlen(result) != attrlen-1) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Error in string: %zd/%d", strlen(result), attrlen);
+ ABORT(R_FAILED);
+ }
+ }
+
+ _status = 0;
+ abort:
+ return _status;
+}
+
+nr_stun_attr_codec nr_stun_attr_codec_string = {
+ "string",
+ nr_stun_attr_codec_string_print,
+ nr_stun_attr_codec_string_encode,
+ nr_stun_attr_codec_string_decode
+};
+
+static int
+nr_stun_attr_codec_unknown_attributes_print(nr_stun_attr_info *attr_info, char *msg, void *data)
+{
+ nr_stun_attr_unknown_attributes *unknown_attributes = data;
+ char type[9];
+ char str[64 + (NR_STUN_MAX_UNKNOWN_ATTRIBUTES * sizeof(type))];
+ int i;
+
+ snprintf(str, sizeof(str), "%s %s:", msg, attr_info->name);
+ for (i = 0; i < unknown_attributes->num_attributes; ++i) {
+ snprintf(type, sizeof(type), "%s 0x%04x", ((i>0)?",":""), unknown_attributes->attribute[i]);
+ strlcat(str, type, sizeof(str));
+ }
+
+ r_log(NR_LOG_STUN, LOG_DEBUG, "%s", str);
+ return 0;
+}
+
+static int
+nr_stun_attr_codec_unknown_attributes_encode(nr_stun_attr_info *attr_info, void *data, size_t offset, size_t buflen, UCHAR *buf, size_t *attrlen)
+{
+ int _status;
+ int start = offset;
+ nr_stun_attr_unknown_attributes *unknown_attributes = data;
+ int length = (2 * unknown_attributes->num_attributes);
+ int i;
+
+ if (unknown_attributes->num_attributes > NR_STUN_MAX_UNKNOWN_ATTRIBUTES) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Too many UNKNOWN-ATTRIBUTES: %d", unknown_attributes->num_attributes);
+ ABORT(R_FAILED);
+ }
+
+ if (nr_stun_encode_htons(attr_info->type , buflen, buf, &offset)
+ || nr_stun_encode_htons(length , buflen, buf, &offset))
+ ABORT(R_FAILED);
+
+ for (i = 0; i < unknown_attributes->num_attributes; ++i) {
+ if (nr_stun_encode_htons(unknown_attributes->attribute[i], buflen, buf, &offset))
+ ABORT(R_FAILED);
+ }
+
+ *attrlen = offset - start;
+
+ _status = 0;
+ abort:
+ return _status;
+}
+
+static int
+nr_stun_attr_codec_unknown_attributes_decode(nr_stun_attr_info *attr_info, size_t attrlen, UCHAR *buf, size_t offset, size_t buflen, void *data)
+{
+ int _status;
+ nr_stun_attr_unknown_attributes *unknown_attributes = data;
+ int i;
+ UINT2 *a;
+
+ if ((attrlen % 4) != 0) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Attribute is illegal size: %d", attrlen);
+ ABORT(R_REJECTED);
+ }
+
+ unknown_attributes->num_attributes = attrlen / 2;
+
+ if (unknown_attributes->num_attributes > NR_STUN_MAX_UNKNOWN_ATTRIBUTES) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Too many UNKNOWN-ATTRIBUTES: %d", unknown_attributes->num_attributes);
+ ABORT(R_REJECTED);
+ }
+
+ for (i = 0; i < unknown_attributes->num_attributes; ++i) {
+ a = &(unknown_attributes->attribute[i]);
+ if (nr_stun_decode_htons(buf, buflen, &offset, a))
+ return R_FAILED;
+ }
+
+ _status = 0;
+ abort:
+ return _status;
+}
+
+nr_stun_attr_codec nr_stun_attr_codec_unknown_attributes = {
+ "unknown_attributes",
+ nr_stun_attr_codec_unknown_attributes_print,
+ nr_stun_attr_codec_unknown_attributes_encode,
+ nr_stun_attr_codec_unknown_attributes_decode
+};
+
+static int
+nr_stun_attr_codec_xor_mapped_address_print(nr_stun_attr_info *attr_info, char *msg, void *data)
+{
+ nr_stun_attr_xor_mapped_address *xor_mapped_address = data;
+ r_log(NR_LOG_STUN, LOG_DEBUG, "%s %s: %s (unmasked) %s (masked)",
+ msg, attr_info->name,
+ xor_mapped_address->unmasked.as_string,
+ xor_mapped_address->masked.as_string);
+ return 0;
+}
+
+static int
+nr_stun_attr_codec_xor_mapped_address_encode(nr_stun_attr_info *attr_info, void *data, size_t offset, size_t buflen, UCHAR *buf, size_t *attrlen)
+{
+ nr_stun_attr_xor_mapped_address *xor_mapped_address = data;
+ nr_stun_message_header *header = (nr_stun_message_header*)buf;
+ UINT4 magic_cookie;
+
+ r_log(NR_LOG_STUN, LOG_DEBUG, "Unmasked XOR-MAPPED-ADDRESS = %s", xor_mapped_address->unmasked.as_string);
+
+ /* this needs to be the magic cookie in the header and not
+ * the MAGIC_COOKIE constant because if we're talking to
+ * older servers (that don't have a magic cookie) they use
+ * message ID for this */
+ magic_cookie = ntohl(header->magic_cookie);
+
+ nr_stun_xor_mapped_address(magic_cookie, header->id, &xor_mapped_address->unmasked, &xor_mapped_address->masked);
+
+ r_log(NR_LOG_STUN, LOG_DEBUG, "Masked XOR-MAPPED-ADDRESS = %s", xor_mapped_address->masked.as_string);
+
+ if (nr_stun_attr_codec_addr.encode(attr_info, &xor_mapped_address->masked, offset, buflen, buf, attrlen))
+ return R_FAILED;
+
+ return 0;
+}
+
+static int
+nr_stun_attr_codec_xor_mapped_address_decode(nr_stun_attr_info *attr_info, size_t attrlen, UCHAR *buf, size_t offset, size_t buflen, void *data)
+{
+ int r,_status;
+ nr_stun_attr_xor_mapped_address *xor_mapped_address = data;
+ nr_stun_message_header *header = (nr_stun_message_header*)buf;
+ UINT4 magic_cookie;
+
+ if ((r=nr_stun_attr_codec_addr.decode(attr_info, attrlen, buf, offset, buflen, &xor_mapped_address->masked)))
+ ABORT(r);
+
+ r_log(NR_LOG_STUN, LOG_DEBUG, "Masked XOR-MAPPED-ADDRESS = %s", xor_mapped_address->masked.as_string);
+
+ /* this needs to be the magic cookie in the header and not
+ * the MAGIC_COOKIE constant because if we're talking to
+ * older servers (that don't have a magic cookie) they use
+ * message ID for this */
+ magic_cookie = ntohl(header->magic_cookie);
+
+ nr_stun_xor_mapped_address(magic_cookie, header->id, &xor_mapped_address->masked, &xor_mapped_address->unmasked);
+
+ r_log(NR_LOG_STUN, LOG_DEBUG, "Unmasked XOR-MAPPED-ADDRESS = %s", xor_mapped_address->unmasked.as_string);
+
+ _status = 0;
+ abort:
+ return _status;
+}
+
+nr_stun_attr_codec nr_stun_attr_codec_xor_mapped_address = {
+ "xor_mapped_address",
+ nr_stun_attr_codec_xor_mapped_address_print,
+ nr_stun_attr_codec_xor_mapped_address_encode,
+ nr_stun_attr_codec_xor_mapped_address_decode
+};
+
+nr_stun_attr_codec nr_stun_attr_codec_old_xor_mapped_address = {
+ "xor_mapped_address",
+ nr_stun_attr_codec_xor_mapped_address_print,
+ 0, /* never encode this type */
+ nr_stun_attr_codec_xor_mapped_address_decode
+};
+
+nr_stun_attr_codec nr_stun_attr_codec_xor_peer_address = {
+ "xor_peer_address",
+ nr_stun_attr_codec_xor_mapped_address_print,
+ nr_stun_attr_codec_xor_mapped_address_encode,
+ nr_stun_attr_codec_xor_mapped_address_decode
+};
+
+#define NR_ADD_STUN_ATTRIBUTE(type, name, codec, illegal) \
+ { (type), (name), &(codec), illegal },
+
+#define NR_ADD_STUN_ATTRIBUTE_IGNORE(type, name) \
+ { (type), (name), &nr_stun_attr_codec_noop, 0 },
+
+
+static nr_stun_attr_info attrs[] = {
+ NR_ADD_STUN_ATTRIBUTE(NR_STUN_ATTR_ALTERNATE_SERVER, "ALTERNATE-SERVER", nr_stun_attr_codec_addr, 0)
+#ifdef USE_STUND_0_96
+ NR_ADD_STUN_ATTRIBUTE(NR_STUN_ATTR_OLD_CHANGE_REQUEST, "CHANGE-REQUEST", nr_stun_attr_codec_UINT4, 0)
+#endif
+ NR_ADD_STUN_ATTRIBUTE(NR_STUN_ATTR_ERROR_CODE, "ERROR-CODE", nr_stun_attr_codec_error_code, nr_stun_attr_error_code_illegal)
+ NR_ADD_STUN_ATTRIBUTE(NR_STUN_ATTR_FINGERPRINT, "FINGERPRINT", nr_stun_attr_codec_fingerprint, 0)
+ NR_ADD_STUN_ATTRIBUTE(NR_STUN_ATTR_MAPPED_ADDRESS, "MAPPED-ADDRESS", nr_stun_attr_codec_addr, 0)
+ NR_ADD_STUN_ATTRIBUTE(NR_STUN_ATTR_MESSAGE_INTEGRITY, "MESSAGE-INTEGRITY", nr_stun_attr_codec_message_integrity, 0)
+ NR_ADD_STUN_ATTRIBUTE(NR_STUN_ATTR_NONCE, "NONCE", nr_stun_attr_codec_quoted_string, nr_stun_attr_nonce_illegal)
+ NR_ADD_STUN_ATTRIBUTE(NR_STUN_ATTR_REALM, "REALM", nr_stun_attr_codec_quoted_string, nr_stun_attr_realm_illegal)
+ NR_ADD_STUN_ATTRIBUTE(NR_STUN_ATTR_SERVER, "SERVER", nr_stun_attr_codec_string, nr_stun_attr_server_illegal)
+ NR_ADD_STUN_ATTRIBUTE(NR_STUN_ATTR_UNKNOWN_ATTRIBUTES, "UNKNOWN-ATTRIBUTES", nr_stun_attr_codec_unknown_attributes, 0)
+ NR_ADD_STUN_ATTRIBUTE(NR_STUN_ATTR_USERNAME, "USERNAME", nr_stun_attr_codec_string, nr_stun_attr_username_illegal)
+ NR_ADD_STUN_ATTRIBUTE(NR_STUN_ATTR_XOR_MAPPED_ADDRESS, "XOR-MAPPED-ADDRESS", nr_stun_attr_codec_xor_mapped_address, 0)
+
+#ifdef USE_ICE
+ NR_ADD_STUN_ATTRIBUTE(NR_STUN_ATTR_ICE_CONTROLLED, "ICE-CONTROLLED", nr_stun_attr_codec_UINT8, 0)
+ NR_ADD_STUN_ATTRIBUTE(NR_STUN_ATTR_ICE_CONTROLLING, "ICE-CONTROLLING", nr_stun_attr_codec_UINT8, 0)
+ NR_ADD_STUN_ATTRIBUTE(NR_STUN_ATTR_PRIORITY, "PRIORITY", nr_stun_attr_codec_UINT4, 0)
+ NR_ADD_STUN_ATTRIBUTE(NR_STUN_ATTR_USE_CANDIDATE, "USE-CANDIDATE", nr_stun_attr_codec_flag, 0)
+#endif
+
+#ifdef USE_TURN
+ NR_ADD_STUN_ATTRIBUTE(NR_STUN_ATTR_DATA, "DATA", nr_stun_attr_codec_data, 0)
+ NR_ADD_STUN_ATTRIBUTE(NR_STUN_ATTR_LIFETIME, "LIFETIME", nr_stun_attr_codec_UINT4, 0)
+ NR_ADD_STUN_ATTRIBUTE(NR_STUN_ATTR_XOR_RELAY_ADDRESS, "XOR-RELAY-ADDRESS", nr_stun_attr_codec_xor_mapped_address, 0)
+ NR_ADD_STUN_ATTRIBUTE(NR_STUN_ATTR_XOR_PEER_ADDRESS, "XOR-PEER-ADDRESS", nr_stun_attr_codec_xor_peer_address, 0)
+ NR_ADD_STUN_ATTRIBUTE(NR_STUN_ATTR_REQUESTED_TRANSPORT, "REQUESTED-TRANSPORT", nr_stun_attr_codec_UCHAR, 0)
+ NR_ADD_STUN_ATTRIBUTE(NR_STUN_ATTR_BANDWIDTH, "BANDWIDTH", nr_stun_attr_codec_UINT4, 0)
+#endif /* USE_TURN */
+
+ /* for backwards compatibilty */
+ NR_ADD_STUN_ATTRIBUTE(NR_STUN_ATTR_OLD_XOR_MAPPED_ADDRESS, "Old XOR-MAPPED-ADDRESS", nr_stun_attr_codec_old_xor_mapped_address, 0)
+#ifdef USE_RFC_3489_BACKWARDS_COMPATIBLE
+ NR_ADD_STUN_ATTRIBUTE_IGNORE(NR_STUN_ATTR_OLD_RESPONSE_ADDRESS, "RESPONSE-ADDRESS")
+ NR_ADD_STUN_ATTRIBUTE_IGNORE(NR_STUN_ATTR_OLD_SOURCE_ADDRESS, "SOURCE-ADDRESS")
+ NR_ADD_STUN_ATTRIBUTE_IGNORE(NR_STUN_ATTR_OLD_CHANGED_ADDRESS, "CHANGED-ADDRESS")
+ NR_ADD_STUN_ATTRIBUTE_IGNORE(NR_STUN_ATTR_OLD_PASSWORD, "PASSWORD")
+#endif /* USE_RFC_3489_BACKWARDS_COMPATIBLE */
+};
+
+
+int
+nr_stun_find_attr_info(UINT2 type, nr_stun_attr_info **info)
+{
+ int _status;
+ size_t i;
+
+ *info = 0;
+ for (i = 0; i < sizeof(attrs)/sizeof(*attrs); ++i) {
+ if (type == attrs[i].type) {
+ *info = &attrs[i];
+ break;
+ }
+ }
+
+ if (*info == 0)
+ ABORT(R_NOT_FOUND);
+
+ _status=0;
+ abort:
+ return(_status);
+}
+
+int
+nr_stun_fix_attribute_ordering(nr_stun_message *msg)
+{
+ nr_stun_message_attribute *message_integrity;
+ nr_stun_message_attribute *fingerprint;
+
+ /* 2nd to the last */
+ if (nr_stun_message_has_attribute(msg, NR_STUN_ATTR_MESSAGE_INTEGRITY, &message_integrity)) {
+ TAILQ_REMOVE(&msg->attributes, message_integrity, entry);
+ TAILQ_INSERT_TAIL(&msg->attributes, message_integrity, entry);
+ }
+
+ /* last */
+ if (nr_stun_message_has_attribute(msg, NR_STUN_ATTR_FINGERPRINT, &fingerprint)) {
+ TAILQ_REMOVE(&msg->attributes, fingerprint, entry);
+ TAILQ_INSERT_TAIL(&msg->attributes, fingerprint, entry);
+ }
+
+ return 0;
+}
+
+// Since this sanity check is only a collection of assert statements and those
+// assert statements are compiled out in non-debug builds, undef SANITY_CHECKS
+// so we can avoid the warning that padding_bytes is never used in opt builds.
+#ifdef NDEBUG
+#undef SANITY_CHECKS
+#endif
+
+#ifdef SANITY_CHECKS
+static void sanity_check_encoding_stuff(nr_stun_message *msg)
+{
+ nr_stun_message_attribute *attr = 0;
+ int padding_bytes;
+ int l;
+
+ r_log(NR_LOG_STUN, LOG_DEBUG, "Starting to sanity check encoding");
+
+ l = 0;
+ TAILQ_FOREACH(attr, &msg->attributes, entry) {
+ padding_bytes = 0;
+ if ((attr->length % 4) != 0) {
+ padding_bytes = 4 - (attr->length % 4);
+ }
+ assert(attr->length == (attr->encoding_length - (4 + padding_bytes)));
+ assert(((void*)attr->encoding) == (msg->buffer + 20 + l));
+ l += attr->encoding_length;
+ assert((l % 4) == 0);
+ }
+ assert(l == msg->header.length);
+}
+#endif /* SANITY_CHECKS */
+
+
+int
+nr_stun_encode_message(nr_stun_message *msg)
+{
+ int r,_status;
+ size_t length_offset;
+ size_t length_offset_hold;
+ nr_stun_attr_info *attr_info;
+ nr_stun_message_attribute *attr;
+ int padding_bytes;
+
+ r_log(NR_LOG_STUN, LOG_DEBUG, "Encoding STUN message");
+
+ nr_stun_fix_attribute_ordering(msg);
+
+ msg->name = nr_stun_msg_type(msg->header.type);
+ msg->length = 0;
+ msg->header.length = 0;
+
+ if ((r=nr_stun_encode_htons(msg->header.type, sizeof(msg->buffer), msg->buffer, &msg->length)))
+ ABORT(r);
+ if (msg->name)
+ r_log(NR_LOG_STUN, LOG_DEBUG, "Encoded MsgType: %s", msg->name);
+ else
+ r_log(NR_LOG_STUN, LOG_DEBUG, "Encoded MsgType: 0x%03x", msg->header.type);
+
+ /* grab the offset to be used later to re-write the header length field */
+ length_offset_hold = msg->length;
+
+ if ((r=nr_stun_encode_htons(msg->header.length, sizeof(msg->buffer), msg->buffer, &msg->length)))
+ ABORT(r);
+
+ if ((r=nr_stun_encode_htonl(msg->header.magic_cookie, sizeof(msg->buffer), msg->buffer, &msg->length)))
+ ABORT(r);
+ r_log(NR_LOG_STUN, LOG_DEBUG, "Encoded Cookie: %08x", msg->header.magic_cookie);
+
+ if ((r=nr_stun_encode((UCHAR*)(&msg->header.id), sizeof(msg->header.id), sizeof(msg->buffer), msg->buffer, &msg->length)))
+ ABORT(r);
+ r_dump(NR_LOG_STUN, LOG_DEBUG, "Encoded ID", (void*)&msg->header.id, sizeof(msg->header.id));
+
+ TAILQ_FOREACH(attr, &msg->attributes, entry) {
+ if ((r=nr_stun_find_attr_info(attr->type, &attr_info))) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Unrecognized attribute: 0x%04x", attr->type);
+ ABORT(R_INTERNAL);
+ }
+
+ attr->name = attr_info->name;
+ attr->type_name = attr_info->codec->name;
+ attr->encoding = (nr_stun_encoded_attribute*)&msg->buffer[msg->length];
+
+ if (attr_info->codec->encode != 0) {
+ if ((r=attr_info->codec->encode(attr_info, &attr->u, msg->length, sizeof(msg->buffer), msg->buffer, &attr->encoding_length))) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Unable to encode %s", attr_info->name);
+ ABORT(r);
+ }
+
+ msg->length += attr->encoding_length;
+ attr->length = attr->encoding_length - 4; /* -4 for type and length fields */
+
+ if (attr_info->illegal) {
+ if ((r=attr_info->illegal(attr_info, attr->length, &attr->u)))
+ ABORT(r);
+ }
+
+ attr_info->codec->print(attr_info, "Encoded", &attr->u);
+
+ if ((attr->length % 4) == 0) {
+ padding_bytes = 0;
+ }
+ else {
+ padding_bytes = 4 - (attr->length % 4);
+ nr_stun_encode((UCHAR*)"\0\0\0\0", padding_bytes, sizeof(msg->buffer), msg->buffer, &msg->length);
+ attr->encoding_length += padding_bytes;
+ }
+
+ msg->header.length += attr->encoding_length;
+ length_offset = length_offset_hold;
+ (void)nr_stun_encode_htons(msg->header.length, sizeof(msg->buffer), msg->buffer, &length_offset);
+ }
+ else {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Missing encode function for attribute: %s", attr_info->name);
+ }
+ }
+
+ r_log(NR_LOG_STUN, LOG_DEBUG, "Encoded Length: %d", msg->header.length);
+
+ assert(msg->length < NR_STUN_MAX_MESSAGE_SIZE);
+
+#ifdef SANITY_CHECKS
+ sanity_check_encoding_stuff(msg);
+#endif /* SANITY_CHECKS */
+
+ _status=0;
+abort:
+ return _status;
+}
+
+int
+nr_stun_decode_message(nr_stun_message *msg, int (*get_password)(void *arg, nr_stun_message *msg, Data **password), void *arg)
+{
+ int r,_status;
+ int offset;
+ int size;
+ int padding_bytes;
+ nr_stun_message_attribute *attr;
+ nr_stun_attr_info *attr_info;
+ Data *password;
+
+ r_log(NR_LOG_STUN, LOG_DEBUG, "Parsing STUN message of %d bytes", msg->length);
+
+ if (!TAILQ_EMPTY(&msg->attributes))
+ ABORT(R_BAD_ARGS);
+
+ if (sizeof(nr_stun_message_header) > msg->length) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Message too small");
+ ABORT(R_FAILED);
+ }
+
+ memcpy(&msg->header, msg->buffer, sizeof(msg->header));
+ msg->header.type = ntohs(msg->header.type);
+ msg->header.length = ntohs(msg->header.length);
+ msg->header.magic_cookie = ntohl(msg->header.magic_cookie);
+
+ msg->name = nr_stun_msg_type(msg->header.type);
+
+ if (msg->name)
+ r_log(NR_LOG_STUN, LOG_DEBUG, "Parsed MsgType: %s", msg->name);
+ else
+ r_log(NR_LOG_STUN, LOG_DEBUG, "Parsed MsgType: 0x%03x", msg->header.type);
+ r_log(NR_LOG_STUN, LOG_DEBUG, "Parsed Length: %d", msg->header.length);
+ r_log(NR_LOG_STUN, LOG_DEBUG, "Parsed Cookie: %08x", msg->header.magic_cookie);
+ r_dump(NR_LOG_STUN, LOG_DEBUG, "Parsed ID", (void*)&msg->header.id, sizeof(msg->header.id));
+
+ if (msg->header.length + sizeof(msg->header) != msg->length) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Inconsistent message header length: %d/%d",
+ msg->header.length, msg->length);
+ ABORT(R_FAILED);
+ }
+
+ size = msg->header.length;
+
+ if ((size % 4) != 0) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Illegal message size: %d", msg->header.length);
+ ABORT(R_FAILED);
+ }
+
+ offset = sizeof(msg->header);
+
+ while (size > 0) {
+ r_log(NR_LOG_STUN, LOG_DEBUG, "size = %d", size);
+
+ if (size < 4) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Illegal message length: %d", size);
+ ABORT(R_FAILED);
+ }
+
+ if ((r=nr_stun_message_attribute_create(msg, &attr)))
+ ABORT(R_NO_MEMORY);
+
+ attr->encoding = (nr_stun_encoded_attribute*)&msg->buffer[offset];
+ attr->type = ntohs(attr->encoding->type);
+ attr->length = ntohs(attr->encoding->length);
+ attr->encoding_length = attr->length + 4;
+
+ if ((attr->length % 4) != 0) {
+ padding_bytes = 4 - (attr->length % 4);
+ attr->encoding_length += padding_bytes;
+ }
+
+ if ((attr->encoding_length) > (size_t)size) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Attribute length larger than remaining message size: %d/%d", attr->encoding_length, size);
+ ABORT(R_FAILED);
+ }
+
+ if ((r=nr_stun_find_attr_info(attr->type, &attr_info))) {
+ if (attr->type <= 0x7FFF)
+ ++msg->comprehension_required_unknown_attributes;
+ else
+ ++msg->comprehension_optional_unknown_attributes;
+ r_log(NR_LOG_STUN, LOG_INFO, "Unrecognized attribute: 0x%04x", attr->type);
+ }
+ else {
+ attr_info->name = attr_info->name;
+ attr->type_name = attr_info->codec->name;
+
+ if (attr->type == NR_STUN_ATTR_MESSAGE_INTEGRITY) {
+ if (get_password && get_password(arg, msg, &password) == 0) {
+ if (password->len > sizeof(attr->u.message_integrity.password)) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Password too long: %d bytes", password->len);
+ ABORT(R_FAILED);
+ }
+
+ memcpy(attr->u.message_integrity.password, password->data, password->len);
+ attr->u.message_integrity.passwordlen = password->len;
+ }
+ else {
+ /* set to user "not found" */
+ attr->u.message_integrity.unknown_user = 1;
+ }
+ }
+ else if (attr->type == NR_STUN_ATTR_OLD_XOR_MAPPED_ADDRESS) {
+ attr->type = NR_STUN_ATTR_XOR_MAPPED_ADDRESS;
+ r_log(NR_LOG_STUN, LOG_INFO, "Translating obsolete XOR-MAPPED-ADDRESS type");
+ }
+
+ if ((r=attr_info->codec->decode(attr_info, attr->length, msg->buffer, offset+4, msg->length, &attr->u))) {
+ if (r == SKIP_ATTRIBUTE_DECODE) {
+ r_log(NR_LOG_STUN, LOG_INFO, "Skipping %s", attr_info->name);
+ }
+ else {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Unable to parse %s", attr_info->name);
+ }
+
+ attr->invalid = 1;
+ }
+ else {
+ attr_info->codec->print(attr_info, "Parsed", &attr->u);
+
+#ifdef USE_STUN_PEDANTIC
+ r_log(NR_LOG_STUN, LOG_DEBUG, "Before pedantic attr_info checks");
+ if (attr_info->illegal) {
+ if ((r=attr_info->illegal(attr_info, attr->length, &attr->u))) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Failed pedantic attr_info checks");
+ ABORT(r);
+ }
+ }
+ r_log(NR_LOG_STUN, LOG_DEBUG, "After pedantic attr_info checks");
+#endif /* USE_STUN_PEDANTIC */
+ }
+ }
+
+ offset += attr->encoding_length;
+ size -= attr->encoding_length;
+ }
+
+#ifdef SANITY_CHECKS
+ sanity_check_encoding_stuff(msg);
+#endif /* SANITY_CHECKS */
+
+ _status=0;
+ abort:
+ return _status;
+}
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_codec.h b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_codec.h
new file mode 100644
index 0000000000..4e4ff60e0c
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_codec.h
@@ -0,0 +1,78 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+
+#ifndef _stun_codec_h
+#define _stun_codec_h
+
+#include "stun_msg.h"
+
+typedef struct nr_stun_attr_info_ nr_stun_attr_info;
+
+typedef struct nr_stun_attr_codec_ {
+ char *name;
+ int (*print)(nr_stun_attr_info *attr_info, char *msg, void *data);
+ int (*encode)(nr_stun_attr_info *attr_info, void *data, size_t offset, size_t buflen, UCHAR *buf, size_t *attrlen);
+ int (*decode)(nr_stun_attr_info *attr_info, size_t attrlen, UCHAR *buf, size_t offset, size_t buflen, void *data);
+} nr_stun_attr_codec;
+
+struct nr_stun_attr_info_ {
+ UINT2 type;
+ char *name;
+ nr_stun_attr_codec *codec;
+ int (*illegal)(nr_stun_attr_info *attr_info, size_t attrlen, void *data);
+};
+
+extern nr_stun_attr_codec nr_stun_attr_codec_UINT4;
+extern nr_stun_attr_codec nr_stun_attr_codec_UINT8;
+extern nr_stun_attr_codec nr_stun_attr_codec_addr;
+extern nr_stun_attr_codec nr_stun_attr_codec_bytes;
+extern nr_stun_attr_codec nr_stun_attr_codec_data;
+extern nr_stun_attr_codec nr_stun_attr_codec_error_code;
+extern nr_stun_attr_codec nr_stun_attr_codec_fingerprint;
+extern nr_stun_attr_codec nr_stun_attr_codec_flag;
+extern nr_stun_attr_codec nr_stun_attr_codec_message_integrity;
+extern nr_stun_attr_codec nr_stun_attr_codec_noop;
+extern nr_stun_attr_codec nr_stun_attr_codec_quoted_string;
+extern nr_stun_attr_codec nr_stun_attr_codec_string;
+extern nr_stun_attr_codec nr_stun_attr_codec_unknown_attributes;
+extern nr_stun_attr_codec nr_stun_attr_codec_xor_mapped_address;
+extern nr_stun_attr_codec nr_stun_attr_codec_xor_peer_address;
+extern nr_stun_attr_codec nr_stun_attr_codec_old_xor_mapped_address;
+
+size_t nr_count_utf8_code_points_without_validation(const char *s);
+int nr_stun_encode_message(nr_stun_message *msg);
+int nr_stun_decode_message(nr_stun_message *msg, int (*get_password)(void *arg, nr_stun_message *msg, Data **password), void *arg);
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_hint.c b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_hint.c
new file mode 100644
index 0000000000..8f118e8942
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_hint.c
@@ -0,0 +1,245 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <errno.h>
+#include <csi_platform.h>
+
+#ifdef WIN32
+#include <winsock2.h>
+#include <stdlib.h>
+#include <io.h>
+#include <time.h>
+#else /* UNIX */
+#include <string.h>
+#endif /* end UNIX */
+#include <assert.h>
+
+#include "stun.h"
+
+
+/* returns 0 if it's not a STUN message
+ * 1 if it's likely to be a STUN message
+ * 2 if it's super likely to be a STUN message
+ * 3 if it really is a STUN message */
+int
+nr_is_stun_message(UCHAR *buf, size_t len)
+{
+ const UINT4 cookie = htonl(NR_STUN_MAGIC_COOKIE);
+ const UINT4 cookie2 = htonl(NR_STUN_MAGIC_COOKIE2);
+#if 0
+ nr_stun_message msg;
+#endif
+ UINT2 type;
+ nr_stun_encoded_attribute* attr;
+ unsigned int attrLen;
+ int atrType;
+
+ if (sizeof(nr_stun_message_header) > len)
+ return 0;
+
+ if ((buf[0] & (0x80|0x40)) != 0)
+ return 0;
+
+ memcpy(&type, buf, 2);
+ type = ntohs(type);
+
+ switch (type) {
+ case NR_STUN_MSG_BINDING_REQUEST:
+ case NR_STUN_MSG_BINDING_INDICATION:
+ case NR_STUN_MSG_BINDING_RESPONSE:
+ case NR_STUN_MSG_BINDING_ERROR_RESPONSE:
+
+#ifdef USE_TURN
+ case NR_STUN_MSG_ALLOCATE_REQUEST:
+ case NR_STUN_MSG_ALLOCATE_RESPONSE:
+ case NR_STUN_MSG_ALLOCATE_ERROR_RESPONSE:
+ case NR_STUN_MSG_REFRESH_REQUEST:
+ case NR_STUN_MSG_REFRESH_RESPONSE:
+ case NR_STUN_MSG_REFRESH_ERROR_RESPONSE:
+ case NR_STUN_MSG_PERMISSION_REQUEST:
+ case NR_STUN_MSG_PERMISSION_RESPONSE:
+ case NR_STUN_MSG_PERMISSION_ERROR_RESPONSE:
+ case NR_STUN_MSG_CHANNEL_BIND_REQUEST:
+ case NR_STUN_MSG_CHANNEL_BIND_RESPONSE:
+ case NR_STUN_MSG_CHANNEL_BIND_ERROR_RESPONSE:
+ case NR_STUN_MSG_SEND_INDICATION:
+ case NR_STUN_MSG_DATA_INDICATION:
+#ifdef NR_STUN_MSG_CONNECT_REQUEST
+ case NR_STUN_MSG_CONNECT_REQUEST:
+#endif
+#ifdef NR_STUN_MSG_CONNECT_RESPONSE
+ case NR_STUN_MSG_CONNECT_RESPONSE:
+#endif
+#ifdef NR_STUN_MSG_CONNECT_ERROR_RESPONSE
+ case NR_STUN_MSG_CONNECT_ERROR_RESPONSE:
+#endif
+#ifdef NR_STUN_MSG_CONNECT_STATUS_INDICATION
+ case NR_STUN_MSG_CONNECT_STATUS_INDICATION:
+#endif
+#endif /* USE_TURN */
+
+ /* ok so far, continue */
+ break;
+ default:
+ return 0;
+ break;
+ }
+
+ if (!memcmp(&cookie2, &buf[4], sizeof(UINT4))) {
+ /* return here because if it's an old-style message then there will
+ * not be a fingerprint in the message */
+ return 1;
+ }
+
+ if (memcmp(&cookie, &buf[4], sizeof(UINT4)))
+ return 0;
+
+ /* the magic cookie was right, so it's pretty darn likely that what we've
+ * got here is a STUN message */
+
+ attr = (nr_stun_encoded_attribute*)(buf + (len - 8));
+ attrLen = ntohs(attr->length);
+ atrType = ntohs(attr->type);
+
+ if (atrType != NR_STUN_ATTR_FINGERPRINT || attrLen != 4)
+ return 1;
+
+ /* the fingerprint is in the right place and looks sane, so we can be quite
+ * sure we've got a STUN message */
+
+#if 0
+/* nevermind this check ... there's a reasonable chance that a NAT has modified
+ * the message (and thus the fingerprint check will fail), but it's still an
+ * otherwise-perfectly-good STUN message, so skip the check since we're going
+ * to return "true" whether the check succeeds or fails */
+
+ if (nr_stun_parse_attr_UINT4(buf + (len - 4), attrLen, &msg.fingerprint))
+ return 2;
+
+
+ if (nr_stun_compute_fingerprint(buf, len - 8, &computedFingerprint))
+ return 2;
+
+ if (msg.fingerprint.number != computedFingerprint)
+ return 2;
+
+ /* and the fingerprint is good, so it's gotta be a STUN message */
+#endif
+
+ return 3;
+}
+
+int
+nr_is_stun_request_message(UCHAR *buf, size_t len)
+{
+ UINT2 type;
+
+ if (sizeof(nr_stun_message_header) > len)
+ return 0;
+
+ if (!nr_is_stun_message(buf, len))
+ return 0;
+
+ memcpy(&type, buf, 2);
+ type = ntohs(type);
+
+ return NR_STUN_GET_TYPE_CLASS(type) == NR_CLASS_REQUEST;
+}
+
+int
+nr_is_stun_indication_message(UCHAR *buf, size_t len)
+{
+ UINT2 type;
+
+ if (sizeof(nr_stun_message_header) > len)
+ return 0;
+
+ if (!nr_is_stun_message(buf, len))
+ return 0;
+
+ memcpy(&type, buf, 2);
+ type = ntohs(type);
+
+ return NR_STUN_GET_TYPE_CLASS(type) == NR_CLASS_INDICATION;
+}
+
+int
+nr_is_stun_response_message(UCHAR *buf, size_t len)
+{
+ UINT2 type;
+
+ if (sizeof(nr_stun_message_header) > len)
+ return 0;
+
+ if (!nr_is_stun_message(buf, len))
+ return 0;
+
+ memcpy(&type, buf, 2);
+ type = ntohs(type);
+
+ return NR_STUN_GET_TYPE_CLASS(type) == NR_CLASS_RESPONSE
+ || NR_STUN_GET_TYPE_CLASS(type) == NR_CLASS_ERROR_RESPONSE;
+}
+
+int
+nr_has_stun_cookie(UCHAR *buf, size_t len)
+{
+ static UINT4 cookie;
+
+ cookie = htonl(NR_STUN_MAGIC_COOKIE);
+
+ if (sizeof(nr_stun_message_header) > len)
+ return 0;
+
+ if (memcmp(&cookie, &buf[4], sizeof(UINT4)))
+ return 0;
+
+ return 1;
+}
+
+int
+nr_stun_message_length(UCHAR *buf, int buf_len, int *msg_len)
+{
+ nr_stun_message_header *hdr;
+
+ if (!nr_is_stun_message(buf, buf_len))
+ return(R_BAD_DATA);
+
+ hdr = (nr_stun_message_header *)buf;
+
+ *msg_len = ntohs(hdr->length);
+
+ return(0);
+}
+
+
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_hint.h b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_hint.h
new file mode 100644
index 0000000000..c2badc1d2b
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_hint.h
@@ -0,0 +1,44 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+#ifndef _stun_hint_h
+#define _stun_hint_h
+
+int nr_is_stun_message(UCHAR *buf, size_t len);
+int nr_is_stun_request_message(UCHAR *buf, size_t len);
+int nr_is_stun_response_message(UCHAR *buf, size_t len);
+int nr_is_stun_indication_message(UCHAR *buf, size_t len);
+int nr_has_stun_cookie(UCHAR *buf, size_t len);
+int nr_stun_message_length(UCHAR *buf, int len, int *length);
+
+#endif
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_msg.c b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_msg.c
new file mode 100644
index 0000000000..7e01686109
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_msg.c
@@ -0,0 +1,364 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <errno.h>
+#include <csi_platform.h>
+
+#ifdef WIN32
+#include <winsock2.h>
+#include <stdlib.h>
+#include <io.h>
+#include <time.h>
+#else /* UNIX */
+#include <string.h>
+#endif /* end UNIX */
+#include <assert.h>
+
+#include "stun.h"
+
+
+int
+nr_stun_message_create(nr_stun_message **msg)
+{
+ int _status;
+ nr_stun_message *m = 0;
+
+ m = RCALLOC(sizeof(*m));
+ if (!m)
+ ABORT(R_NO_MEMORY);
+
+ TAILQ_INIT(&m->attributes);
+
+ *msg = m;
+
+ _status=0;
+ abort:
+ return(_status);
+}
+
+int
+nr_stun_message_create2(nr_stun_message **msg, UCHAR *buffer, size_t length)
+{
+ int r,_status;
+ nr_stun_message *m = 0;
+
+ if (length > sizeof(m->buffer)) {
+ ABORT(R_BAD_DATA);
+ }
+
+ if ((r=nr_stun_message_create(&m)))
+ ABORT(r);
+
+ memcpy(m->buffer, buffer, length);
+ m->length = length;
+
+ *msg = m;
+
+ _status=0;
+ abort:
+ return(_status);
+}
+
+int
+nr_stun_message_destroy(nr_stun_message **msg)
+{
+ int _status;
+ nr_stun_message_attribute_head *attrs;
+ nr_stun_message_attribute *attr;
+
+ if (msg && *msg) {
+ attrs = &(*msg)->attributes;
+ while (!TAILQ_EMPTY(attrs)) {
+ attr = TAILQ_FIRST(attrs);
+ nr_stun_message_attribute_destroy(*msg, &attr);
+ }
+
+ RFREE(*msg);
+
+ *msg = 0;
+ }
+
+ _status=0;
+/* abort: */
+ return(_status);
+}
+
+int
+nr_stun_message_attribute_create(nr_stun_message *msg, nr_stun_message_attribute **attr)
+{
+ int _status;
+ nr_stun_message_attribute *a = 0;
+
+ a = RCALLOC(sizeof(*a));
+ if (!a)
+ ABORT(R_NO_MEMORY);
+
+ TAILQ_INSERT_TAIL(&msg->attributes, a, entry);
+
+ *attr = a;
+
+ _status=0;
+ abort:
+ return(_status);
+}
+
+int
+nr_stun_message_attribute_destroy(nr_stun_message *msg, nr_stun_message_attribute **attr)
+{
+ int _status;
+ nr_stun_message_attribute *a = 0;
+
+ if (attr && *attr) {
+ a = *attr;
+ TAILQ_REMOVE(&msg->attributes, a, entry);
+
+ RFREE(a);
+
+ *attr = 0;
+ }
+
+ _status=0;
+/* abort: */
+ return(_status);
+}
+
+int
+nr_stun_message_has_attribute(nr_stun_message *msg, UINT2 type, nr_stun_message_attribute **attribute)
+{
+ nr_stun_message_attribute *attr = 0;
+ nr_stun_message_get_attribute(msg, type, 0, &attr);
+
+ if (attribute)
+ *attribute = attr;
+
+ return attr ? 1 : 0;
+}
+
+int
+nr_stun_message_get_attribute(nr_stun_message *msg, UINT2 type, UINT2 index, nr_stun_message_attribute **attribute)
+{
+ nr_stun_message_attribute *attr;
+ TAILQ_FOREACH(attr, &msg->attributes, entry) {
+ if (attr->type == type && !attr->invalid) {
+ if (!index) {
+ *attribute = attr;
+ return 0;
+ }
+ --index;
+ }
+ }
+ *attribute = 0;
+ return R_NOT_FOUND;
+}
+
+#define NR_STUN_MESSAGE_ADD_ATTRIBUTE(__type, __code) \
+ { \
+ int r,_status; \
+ nr_stun_message_attribute *attr = 0; \
+ if ((r=nr_stun_message_attribute_create(msg, &attr))) \
+ ABORT(r); \
+ attr->type = (__type); \
+ { __code } \
+ _status=0; \
+ abort: \
+ if (_status){ \
+ nr_stun_message_attribute_destroy(msg, &attr); \
+ } \
+ return(_status); \
+ }
+
+
+int
+nr_stun_message_add_alternate_server_attribute(nr_stun_message *msg, nr_transport_addr *alternate_server)
+NR_STUN_MESSAGE_ADD_ATTRIBUTE(
+ NR_STUN_ATTR_ALTERNATE_SERVER,
+ {
+ if ((r=nr_transport_addr_copy(&attr->u.alternate_server, alternate_server)))
+ ABORT(r);
+ }
+)
+
+int
+nr_stun_message_add_error_code_attribute(nr_stun_message *msg, UINT2 number, char *reason)
+NR_STUN_MESSAGE_ADD_ATTRIBUTE(
+ NR_STUN_ATTR_ERROR_CODE,
+ {
+ attr->u.error_code.number = number;
+ (void)strlcpy(attr->u.error_code.reason, reason, sizeof(attr->u.error_code.reason));
+ }
+)
+
+int
+nr_stun_message_add_fingerprint_attribute(nr_stun_message *msg)
+NR_STUN_MESSAGE_ADD_ATTRIBUTE(
+ NR_STUN_ATTR_FINGERPRINT,
+ {}
+)
+
+int
+nr_stun_message_add_message_integrity_attribute(nr_stun_message *msg, Data *password)
+NR_STUN_MESSAGE_ADD_ATTRIBUTE(
+ NR_STUN_ATTR_MESSAGE_INTEGRITY,
+ {
+ if (sizeof(attr->u.message_integrity.password) < password->len)
+ ABORT(R_BAD_DATA);
+
+ memcpy(attr->u.message_integrity.password, password->data, password->len);
+ attr->u.message_integrity.passwordlen = password->len;
+ }
+)
+
+int
+nr_stun_message_add_nonce_attribute(nr_stun_message *msg, char *nonce)
+NR_STUN_MESSAGE_ADD_ATTRIBUTE(
+ NR_STUN_ATTR_NONCE,
+ { (void)strlcpy(attr->u.nonce, nonce, sizeof(attr->u.nonce)); }
+)
+
+int
+nr_stun_message_add_realm_attribute(nr_stun_message *msg, char *realm)
+NR_STUN_MESSAGE_ADD_ATTRIBUTE(
+ NR_STUN_ATTR_REALM,
+ { (void)strlcpy(attr->u.realm, realm, sizeof(attr->u.realm)); }
+)
+
+int
+nr_stun_message_add_server_attribute(nr_stun_message *msg, char *server_name)
+NR_STUN_MESSAGE_ADD_ATTRIBUTE(
+ NR_STUN_ATTR_SERVER,
+ { (void)strlcpy(attr->u.server_name, server_name, sizeof(attr->u.server_name)); }
+)
+
+int
+nr_stun_message_add_unknown_attributes_attribute(nr_stun_message *msg, nr_stun_attr_unknown_attributes *unknown_attributes)
+NR_STUN_MESSAGE_ADD_ATTRIBUTE(
+ NR_STUN_ATTR_UNKNOWN_ATTRIBUTES,
+ { memcpy(&attr->u.unknown_attributes, unknown_attributes, sizeof(attr->u.unknown_attributes)); }
+)
+
+int
+nr_stun_message_add_username_attribute(nr_stun_message *msg, char *username)
+NR_STUN_MESSAGE_ADD_ATTRIBUTE(
+ NR_STUN_ATTR_USERNAME,
+ { (void)strlcpy(attr->u.username, username, sizeof(attr->u.username)); }
+)
+
+int
+nr_stun_message_add_requested_transport_attribute(nr_stun_message *msg, UCHAR protocol)
+NR_STUN_MESSAGE_ADD_ATTRIBUTE(
+ NR_STUN_ATTR_REQUESTED_TRANSPORT,
+ { attr->u.requested_transport = protocol; }
+)
+
+int
+nr_stun_message_add_xor_mapped_address_attribute(nr_stun_message *msg, nr_transport_addr *mapped_address)
+NR_STUN_MESSAGE_ADD_ATTRIBUTE(
+ NR_STUN_ATTR_XOR_MAPPED_ADDRESS,
+ {
+ if ((r=nr_transport_addr_copy(&attr->u.xor_mapped_address.unmasked, mapped_address)))
+ ABORT(r);
+ }
+)
+
+int
+nr_stun_message_add_xor_peer_address_attribute(nr_stun_message *msg, nr_transport_addr *peer_address)
+NR_STUN_MESSAGE_ADD_ATTRIBUTE(
+ NR_STUN_ATTR_XOR_PEER_ADDRESS,
+ {
+ if ((r=nr_transport_addr_copy(&attr->u.xor_mapped_address.unmasked, peer_address)))
+ ABORT(r);
+ }
+)
+
+#ifdef USE_ICE
+int
+nr_stun_message_add_ice_controlled_attribute(nr_stun_message *msg, UINT8 ice_controlled)
+NR_STUN_MESSAGE_ADD_ATTRIBUTE(
+ NR_STUN_ATTR_ICE_CONTROLLED,
+ { attr->u.ice_controlled = ice_controlled; }
+)
+
+int
+nr_stun_message_add_ice_controlling_attribute(nr_stun_message *msg, UINT8 ice_controlling)
+NR_STUN_MESSAGE_ADD_ATTRIBUTE(
+ NR_STUN_ATTR_ICE_CONTROLLING,
+ { attr->u.ice_controlling = ice_controlling; }
+)
+
+int
+nr_stun_message_add_priority_attribute(nr_stun_message *msg, UINT4 priority)
+NR_STUN_MESSAGE_ADD_ATTRIBUTE(
+ NR_STUN_ATTR_PRIORITY,
+ { attr->u.priority = priority; }
+)
+
+int
+nr_stun_message_add_use_candidate_attribute(nr_stun_message *msg)
+NR_STUN_MESSAGE_ADD_ATTRIBUTE(
+ NR_STUN_ATTR_USE_CANDIDATE,
+ {}
+)
+#endif /* USE_ICE */
+
+#ifdef USE_TURN
+int
+nr_stun_message_add_data_attribute(nr_stun_message *msg, UCHAR *data, int length)
+
+NR_STUN_MESSAGE_ADD_ATTRIBUTE(
+ NR_STUN_ATTR_DATA,
+ {
+ if (length > NR_STUN_MAX_MESSAGE_SIZE)
+ ABORT(R_BAD_ARGS);
+
+ memcpy(attr->u.data.data, data, length);
+ attr->u.data.length=length;
+ }
+)
+
+int
+nr_stun_message_add_lifetime_attribute(nr_stun_message *msg, UINT4 lifetime_secs)
+NR_STUN_MESSAGE_ADD_ATTRIBUTE(
+ NR_STUN_ATTR_LIFETIME,
+ { attr->u.lifetime_secs = lifetime_secs; }
+)
+
+#endif /* USE_TURN */
+
+#ifdef USE_STUND_0_96
+int
+nr_stun_message_add_change_request_attribute(nr_stun_message *msg, UINT4 change_request)
+NR_STUN_MESSAGE_ADD_ATTRIBUTE(
+ NR_STUN_ATTR_OLD_CHANGE_REQUEST,
+ { attr->u.change_request = change_request; }
+)
+#endif /* USE_STUND_0_96 */
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_msg.h b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_msg.h
new file mode 100644
index 0000000000..ffd68d3eee
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_msg.h
@@ -0,0 +1,208 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+
+#ifndef _stun_msg_h
+#define _stun_msg_h
+
+#include "csi_platform.h"
+#include "nr_api.h"
+#include "transport_addr.h"
+
+#define NR_STUN_MAX_USERNAME_BYTES 513
+#define NR_STUN_MAX_ERROR_CODE_REASON_BYTES 763
+#define NR_STUN_MAX_ERROR_CODE_REASON_CHARS 128
+#define NR_STUN_MAX_REALM_BYTES 763
+#define NR_STUN_MAX_REALM_CHARS 128
+#define NR_STUN_MAX_NONCE_BYTES 763
+#define NR_STUN_MAX_NONCE_CHARS 128
+#define NR_STUN_MAX_SERVER_BYTES 763
+#define NR_STUN_MAX_SERVER_CHARS 128
+#define NR_STUN_MAX_STRING_SIZE 763 /* any possible string */
+#define NR_STUN_MAX_UNKNOWN_ATTRIBUTES 16
+#define NR_STUN_MAX_MESSAGE_SIZE 2048
+
+#define NR_STUN_MAGIC_COOKIE 0x2112A442
+#define NR_STUN_MAGIC_COOKIE2 0xc5cb4e1d /* used recognize old stun messages */
+
+typedef struct { UCHAR octet[12]; } UINT12;
+
+typedef struct nr_stun_attr_error_code_ {
+ UINT2 number;
+ char reason[NR_STUN_MAX_ERROR_CODE_REASON_BYTES+1]; /* +1 for \0 */
+} nr_stun_attr_error_code;
+
+typedef struct nr_stun_attr_fingerprint_ {
+ UINT4 checksum;
+ int valid;
+} nr_stun_attr_fingerprint;
+
+typedef struct nr_stun_attr_message_integrity_ {
+ UCHAR hash[20];
+ int unknown_user;
+ UCHAR password[1024];
+ int passwordlen;
+ int valid;
+} nr_stun_attr_message_integrity;
+
+typedef struct nr_stun_attr_unknown_attributes_ {
+ UINT2 attribute[NR_STUN_MAX_UNKNOWN_ATTRIBUTES];
+ int num_attributes;
+} nr_stun_attr_unknown_attributes;
+
+typedef struct nr_stun_attr_xor_mapped_address_ {
+ nr_transport_addr masked;
+ nr_transport_addr unmasked;
+} nr_stun_attr_xor_mapped_address;
+
+typedef struct nr_stun_attr_data_ {
+ UCHAR data[NR_STUN_MAX_MESSAGE_SIZE];
+ size_t length;
+} nr_stun_attr_data;
+
+
+typedef struct nr_stun_encoded_attribute_ {
+ UINT2 type;
+ UINT2 length;
+ UCHAR value[NR_STUN_MAX_MESSAGE_SIZE];
+} nr_stun_encoded_attribute;
+
+typedef struct nr_stun_message_attribute_ {
+ UINT2 type;
+ UINT2 length;
+ union {
+ nr_transport_addr address;
+ nr_transport_addr alternate_server;
+ nr_stun_attr_error_code error_code;
+ nr_stun_attr_fingerprint fingerprint;
+ nr_transport_addr mapped_address;
+ nr_stun_attr_message_integrity message_integrity;
+ char nonce[NR_STUN_MAX_NONCE_BYTES+1]; /* +1 for \0 */
+ char realm[NR_STUN_MAX_REALM_BYTES+1]; /* +1 for \0 */
+ nr_stun_attr_xor_mapped_address relay_address;
+ char server_name[NR_STUN_MAX_SERVER_BYTES+1]; /* +1 for \0 */
+ nr_stun_attr_unknown_attributes unknown_attributes;
+ char username[NR_STUN_MAX_USERNAME_BYTES+1]; /* +1 for \0 */
+ nr_stun_attr_xor_mapped_address xor_mapped_address;
+
+#ifdef USE_ICE
+ UINT4 priority;
+ UINT8 ice_controlled;
+ UINT8 ice_controlling;
+#endif /* USE_ICE */
+
+#ifdef USE_TURN
+ UINT4 lifetime_secs;
+ nr_transport_addr remote_address;
+ UCHAR requested_transport;
+ nr_stun_attr_data data;
+#endif /* USE_TURN */
+
+#ifdef USE_STUND_0_96
+ UINT4 change_request;
+#endif /* USE_STUND_0_96 */
+
+ /* make sure there's enough room here to place any possible
+ * attribute */
+ UCHAR largest_possible_attribute[NR_STUN_MAX_MESSAGE_SIZE];
+ } u;
+ nr_stun_encoded_attribute *encoding;
+ size_t encoding_length;
+ char *name;
+ char *type_name;
+ int invalid;
+ TAILQ_ENTRY(nr_stun_message_attribute_) entry;
+} nr_stun_message_attribute;
+
+typedef TAILQ_HEAD(nr_stun_message_attribute_head_,nr_stun_message_attribute_) nr_stun_message_attribute_head;
+
+typedef struct nr_stun_message_header_ {
+ UINT2 type;
+ UINT2 length;
+ UINT4 magic_cookie;
+ UINT12 id;
+} nr_stun_message_header;
+
+typedef struct nr_stun_message_ {
+ char *name;
+ UCHAR buffer[NR_STUN_MAX_MESSAGE_SIZE];
+ size_t length;
+ nr_stun_message_header header;
+ int comprehension_required_unknown_attributes;
+ int comprehension_optional_unknown_attributes;
+ nr_stun_message_attribute_head attributes;
+} nr_stun_message;
+
+int nr_stun_message_create(nr_stun_message **msg);
+int nr_stun_message_create2(nr_stun_message **msg, UCHAR *buffer, size_t length);
+int nr_stun_message_destroy(nr_stun_message **msg);
+
+int nr_stun_message_attribute_create(nr_stun_message *msg, nr_stun_message_attribute **attr);
+int nr_stun_message_attribute_destroy(nr_stun_message *msg, nr_stun_message_attribute **attr);
+
+int nr_stun_message_has_attribute(nr_stun_message *msg, UINT2 type, nr_stun_message_attribute **attribute);
+
+int nr_stun_message_get_attribute(nr_stun_message *msg, UINT2 type, UINT2 index, nr_stun_message_attribute **attribute);
+
+int nr_stun_message_add_alternate_server_attribute(nr_stun_message *msg, nr_transport_addr *alternate_server);
+int nr_stun_message_add_error_code_attribute(nr_stun_message *msg, UINT2 number, char *reason);
+int nr_stun_message_add_fingerprint_attribute(nr_stun_message *msg);
+int nr_stun_message_add_message_integrity_attribute(nr_stun_message *msg, Data *password);
+int nr_stun_message_add_nonce_attribute(nr_stun_message *msg, char *nonce);
+int nr_stun_message_add_realm_attribute(nr_stun_message *msg, char *realm);
+int nr_stun_message_add_server_attribute(nr_stun_message *msg, char *server_name);
+int nr_stun_message_add_unknown_attributes_attribute(nr_stun_message *msg, nr_stun_attr_unknown_attributes *unknown_attributes);
+int nr_stun_message_add_username_attribute(nr_stun_message *msg, char *username);
+int nr_stun_message_add_xor_mapped_address_attribute(nr_stun_message *msg, nr_transport_addr *mapped_address);
+
+#ifdef USE_ICE
+int nr_stun_message_add_ice_controlled_attribute(nr_stun_message *msg, UINT8 ice_controlled);
+int nr_stun_message_add_ice_controlling_attribute(nr_stun_message *msg, UINT8 ice_controlling);
+int nr_stun_message_add_priority_attribute(nr_stun_message *msg, UINT4 priority);
+int nr_stun_message_add_use_candidate_attribute(nr_stun_message *msg);
+#endif /* USE_ICE */
+
+#ifdef USE_TURN
+int nr_stun_message_add_data_attribute(nr_stun_message *msg, UCHAR *data, int length);
+int nr_stun_message_add_lifetime_attribute(nr_stun_message *msg, UINT4 lifetime_secs);
+int nr_stun_message_add_requested_transport_attribute(nr_stun_message *msg, UCHAR transport);
+int
+nr_stun_message_add_xor_peer_address_attribute(nr_stun_message *msg, nr_transport_addr *peer_address);
+#endif /* USE_TURN */
+
+#ifdef USE_STUND_0_96
+int nr_stun_message_add_change_request_attribute(nr_stun_message *msg, UINT4 change_request);
+#endif /* USE_STUND_0_96 */
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_proc.c b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_proc.c
new file mode 100644
index 0000000000..13366e265d
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_proc.c
@@ -0,0 +1,554 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <errno.h>
+#include <csi_platform.h>
+
+#ifdef WIN32
+#include <winsock2.h>
+#include <stdlib.h>
+#include <io.h>
+#include <time.h>
+#else /* UNIX */
+#include <string.h>
+#endif /* end UNIX */
+#include <assert.h>
+
+#include "stun.h"
+#include "stun_reg.h"
+#include "registry.h"
+
+static int
+nr_stun_add_realm_and_nonce(int new_nonce, nr_stun_server_client *clnt, nr_stun_message *res);
+
+/* draft-ietf-behave-rfc3489bis-10.txt S 7.3 */
+int
+nr_stun_receive_message(nr_stun_message *req, nr_stun_message *msg)
+{
+ int _status;
+ nr_stun_message_attribute *attr;
+
+#ifdef USE_RFC_3489_BACKWARDS_COMPATIBLE
+ /* if this message was generated by an RFC 3489 impementation,
+ * the call to nr_is_stun_message will fail, so skip that
+ * check and puke elsewhere if the message can't be decoded */
+ if (msg->header.magic_cookie == NR_STUN_MAGIC_COOKIE
+ || msg->header.magic_cookie == NR_STUN_MAGIC_COOKIE2) {
+#endif /* USE_RFC_3489_BACKWARDS_COMPATIBLE */
+ if (!nr_is_stun_message(msg->buffer, msg->length)) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Not a STUN message");
+ ABORT(R_REJECTED);
+ }
+#ifdef USE_RFC_3489_BACKWARDS_COMPATIBLE
+ }
+#endif /* USE_RFC_3489_BACKWARDS_COMPATIBLE */
+
+ if (req == 0) {
+ if (NR_STUN_GET_TYPE_CLASS(msg->header.type) != NR_CLASS_REQUEST) {
+ r_log(NR_LOG_STUN,LOG_WARNING,"Illegal message type: %03x", msg->header.type);
+ ABORT(R_REJECTED);
+ }
+ }
+ else {
+ if (NR_STUN_GET_TYPE_CLASS(msg->header.type) != NR_CLASS_RESPONSE
+ && NR_STUN_GET_TYPE_CLASS(msg->header.type) != NR_CLASS_ERROR_RESPONSE) {
+ r_log(NR_LOG_STUN,LOG_WARNING,"Illegal message class: %03x", msg->header.type);
+ ABORT(R_REJECTED);
+ }
+
+ if (NR_STUN_GET_TYPE_METHOD(req->header.type) != NR_STUN_GET_TYPE_METHOD(msg->header.type)) {
+ r_log(NR_LOG_STUN,LOG_WARNING,"Inconsistent message method: %03x expected %03x", msg->header.type, req->header.type);
+ ABORT(R_REJECTED);
+ }
+
+ if (nr_stun_different_transaction(msg->buffer, msg->length, req)) {
+ r_log(NR_LOG_STUN, LOG_DEBUG, "Unrecognized STUN transaction");
+ ABORT(R_REJECTED);
+ }
+ }
+
+ switch (msg->header.magic_cookie) {
+ case NR_STUN_MAGIC_COOKIE:
+ /* basically draft-ietf-behave-rfc3489bis-10.txt S 6 rules */
+
+ if (nr_stun_message_has_attribute(msg, NR_STUN_ATTR_FINGERPRINT, &attr)
+ && !attr->u.fingerprint.valid) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Invalid fingerprint");
+ ABORT(R_REJECTED);
+ }
+
+ break;
+
+#ifdef USE_STUND_0_96
+ case NR_STUN_MAGIC_COOKIE2:
+ /* nothing to check in this case */
+ break;
+#endif /* USE_STUND_0_96 */
+
+ default:
+#ifdef USE_RFC_3489_BACKWARDS_COMPATIBLE
+ /* in RFC 3489 there is no magic cookie, it's part of the transaction ID */
+#else
+#ifdef NDEBUG
+ /* in deployment builds we should always see a recognized magic cookie */
+ r_log(NR_LOG_STUN, LOG_WARNING, "Missing Magic Cookie");
+ ABORT(R_REJECTED);
+#else
+ /* ignore this condition because sometimes we like to pretend we're
+ * a server talking to old clients and their messages don't contain
+ * a magic cookie at all but rather the magic cookie field is part
+ * of their ID and therefore random */
+#endif /* NDEBUG */
+#endif /* USE_RFC_3489_BACKWARDS_COMPATIBLE */
+ break;
+ }
+
+ _status=0;
+ abort:
+ return _status;
+}
+
+/* draft-ietf-behave-rfc3489bis-10.txt S 7.3.1 */
+int
+nr_stun_process_request(nr_stun_message *req, nr_stun_message *res)
+{
+ int _status;
+#ifdef USE_STUN_PEDANTIC
+ int r;
+ nr_stun_attr_unknown_attributes unknown_attributes = { { 0 } };
+ nr_stun_message_attribute *attr;
+
+ if (req->comprehension_required_unknown_attributes > 0) {
+ nr_stun_form_error_response(req, res, 420, "Unknown Attributes");
+ r_log(NR_LOG_STUN, LOG_WARNING, "Request contains comprehension required but unknown attributes");
+
+ TAILQ_FOREACH(attr, &req->attributes, entry) {
+ if (attr->name == 0) {
+ /* unrecognized attribute */
+
+ /* should never happen, but truncate if it ever were to occur */
+ if (unknown_attributes.num_attributes > NR_STUN_MAX_UNKNOWN_ATTRIBUTES)
+ break;
+
+ unknown_attributes.attribute[unknown_attributes.num_attributes++] = attr->type;
+ }
+ }
+
+ assert(req->comprehension_required_unknown_attributes + req->comprehension_optional_unknown_attributes == unknown_attributes.num_attributes);
+
+ if ((r=nr_stun_message_add_unknown_attributes_attribute(res, &unknown_attributes)))
+ ABORT(R_ALREADY);
+
+ ABORT(R_ALREADY);
+ }
+#endif /* USE_STUN_PEDANTIC */
+
+ _status=0;
+#ifdef USE_STUN_PEDANTIC
+ abort:
+#endif /* USE_STUN_PEDANTIC */
+ return _status;
+}
+
+/* draft-ietf-behave-rfc3489bis-10.txt S 7.3.2 */
+int
+nr_stun_process_indication(nr_stun_message *ind)
+{
+ int _status;
+#ifdef USE_STUN_PEDANTIC
+
+ if (ind->comprehension_required_unknown_attributes > 0) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Indication contains comprehension required but unknown attributes");
+ ABORT(R_REJECTED);
+ }
+#endif /* USE_STUN_PEDANTIC */
+
+ _status=0;
+#ifdef USE_STUN_PEDANTIC
+ abort:
+#endif /* USE_STUN_PEDANTIC */
+ return _status;
+}
+
+/* RFC5389 S 7.3.3, except that we *also* allow a MAPPED_ADDRESS
+ to compensate for a bug in Google's STUN server where it
+ always returns MAPPED_ADDRESS.
+
+ Mozilla bug: 888274.
+ */
+int
+nr_stun_process_success_response(nr_stun_message *res)
+{
+ int _status;
+
+#ifdef USE_STUN_PEDANTIC
+ if (res->comprehension_required_unknown_attributes > 0) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Response contains comprehension required but unknown attributes");
+ ABORT(R_REJECTED);
+ }
+#endif /* USE_STUN_PEDANTIC */
+
+ if (NR_STUN_GET_TYPE_METHOD(res->header.type) == NR_METHOD_BINDING) {
+ if (! nr_stun_message_has_attribute(res, NR_STUN_ATTR_XOR_MAPPED_ADDRESS, 0) &&
+ ! nr_stun_message_has_attribute(res, NR_STUN_ATTR_MAPPED_ADDRESS, 0)) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Missing XOR-MAPPED-ADDRESS and MAPPED_ADDRESS");
+ ABORT(R_REJECTED);
+ }
+ }
+
+ _status=0;
+ abort:
+ return _status;
+}
+
+/* draft-ietf-behave-rfc3489bis-10.txt S 7.3.4 */
+int
+nr_stun_process_error_response(nr_stun_message *res, UINT2 *error_code)
+{
+ int _status;
+ nr_stun_message_attribute *attr;
+
+ if (res->comprehension_required_unknown_attributes > 0) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Error response contains comprehension required but unknown attributes");
+ ABORT(R_REJECTED);
+ }
+
+ if (! nr_stun_message_has_attribute(res, NR_STUN_ATTR_ERROR_CODE, &attr)) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Missing ERROR-CODE");
+ ABORT(R_REJECTED);
+ }
+
+ *error_code = attr->u.error_code.number;
+
+ switch (attr->u.error_code.number / 100) {
+ case 3:
+ /* We do not treat STUN/300 as retryable. The TURN Allocate handling
+ * code will reset the ctx if appropriate. */
+ ABORT(R_REJECTED);
+ break;
+
+ case 4:
+ /* If the error code is 400 through 499, the client declares the
+ * transaction failed; in the case of 420 (Unknown Attribute), the
+ * response should contain a UNKNOWN-ATTRIBUTES attribute that gives
+ * additional information. */
+ if (attr->u.error_code.number == 420)
+ ABORT(R_REJECTED);
+
+ /* it may be possible to restart given the info that was received in
+ * this response, so retry */
+ ABORT(R_RETRY);
+ break;
+
+ case 5:
+ /* If the error code is 500 through 599, the client MAY resend the
+ * request; clients that do so MUST limit the number of times they do
+ * this. */
+ /* let the retransmit mechanism handle resending the request */
+ break;
+
+ default:
+ ABORT(R_REJECTED);
+ break;
+ }
+
+ /* the spec says: "The client then does any processing specified by the authentication
+ * mechanism (see Section 10). This may result in a new transaction
+ * attempt." -- but this is handled already elsewhere, so needn't be repeated
+ * in this function */
+
+ _status=0;
+ abort:
+ return _status;
+}
+
+/* draft-ietf-behave-rfc3489bis-10.txt S 10.1.2 */
+int
+nr_stun_receive_request_or_indication_short_term_auth(nr_stun_message *msg,
+ nr_stun_message *res)
+{
+ int _status;
+ nr_stun_message_attribute *attr;
+
+ switch (msg->header.magic_cookie) {
+ default:
+ /* in RFC 3489 there is no magic cookie, it's part of the transaction ID */
+ /* drop thru */
+ case NR_STUN_MAGIC_COOKIE:
+ if (!nr_stun_message_has_attribute(msg, NR_STUN_ATTR_MESSAGE_INTEGRITY, &attr)) {
+ nr_stun_form_error_response(msg, res, 400, "Missing MESSAGE-INTEGRITY");
+ ABORT(R_ALREADY);
+ }
+
+ if (!nr_stun_message_has_attribute(msg, NR_STUN_ATTR_USERNAME, 0)) {
+ nr_stun_form_error_response(msg, res, 400, "Missing USERNAME");
+ ABORT(R_ALREADY);
+ }
+
+ if (attr->u.message_integrity.unknown_user) {
+ nr_stun_form_error_response(msg, res, 401, "Unrecognized USERNAME");
+ ABORT(R_ALREADY);
+ }
+
+ if (!attr->u.message_integrity.valid) {
+ nr_stun_form_error_response(msg, res, 401, "Bad MESSAGE-INTEGRITY");
+ ABORT(R_ALREADY);
+ }
+
+ break;
+
+#ifdef USE_STUND_0_96
+ case NR_STUN_MAGIC_COOKIE2:
+ /* nothing to check in this case */
+ break;
+#endif /* USE_STUND_0_96 */
+ }
+
+ _status=0;
+ abort:
+ return _status;
+}
+
+/* draft-ietf-behave-rfc3489bis-10.txt S 10.1.3 */
+int
+nr_stun_receive_response_short_term_auth(nr_stun_message *res)
+{
+ int _status;
+ nr_stun_message_attribute *attr;
+
+ switch (res->header.magic_cookie) {
+ default:
+ /* in RFC 3489 there is no magic cookie, it's part of the transaction ID */
+ /* drop thru */
+ case NR_STUN_MAGIC_COOKIE:
+ if (!nr_stun_message_has_attribute(res, NR_STUN_ATTR_MESSAGE_INTEGRITY, &attr)) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Missing MESSAGE-INTEGRITY");
+ ABORT(R_REJECTED);
+ }
+
+ if (!attr->u.message_integrity.valid) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Bad MESSAGE-INTEGRITY");
+ ABORT(R_REJECTED);
+ }
+
+ break;
+
+#ifdef USE_STUND_0_96
+ case NR_STUN_MAGIC_COOKIE2:
+ /* nothing to check in this case */
+ break;
+#endif /* USE_STUND_0_96 */
+ }
+
+ _status=0;
+ abort:
+ return _status;
+}
+
+static int
+nr_stun_add_realm_and_nonce(int new_nonce, nr_stun_server_client *clnt, nr_stun_message *res)
+{
+ int r,_status;
+ char *realm = 0;
+ char *nonce;
+ UINT2 size;
+
+ if ((r=NR_reg_alloc_string(NR_STUN_REG_PREF_SERVER_REALM, &realm)))
+ ABORT(r);
+
+ if ((r=nr_stun_message_add_realm_attribute(res, realm)))
+ ABORT(r);
+
+ if (clnt) {
+ if (strlen(clnt->nonce) < 1)
+ new_nonce = 1;
+
+ if (new_nonce) {
+ if (NR_reg_get_uint2(NR_STUN_REG_PREF_SERVER_NONCE_SIZE, &size))
+ size = 48;
+
+ if (size > (sizeof(clnt->nonce) - 1))
+ size = sizeof(clnt->nonce) - 1;
+
+ nr_random_alphanum(clnt->nonce, size);
+ clnt->nonce[size] = '\0';
+ }
+
+ nonce = clnt->nonce;
+ }
+ else {
+ /* user is not known, so use a bogus nonce since there's no way to
+ * store a good nonce with the client-specific data -- this nonce
+ * will be recognized as stale if the client attempts another
+ * request */
+ nonce = "STALE";
+ }
+
+ if ((r=nr_stun_message_add_nonce_attribute(res, nonce)))
+ ABORT(r);
+
+ _status=0;
+ abort:
+#ifdef USE_TURN
+assert(_status == 0); /* TODO: !nn! cleanup after I reimplmement TURN */
+#endif
+ RFREE(realm);
+ return _status;
+}
+
+/* draft-ietf-behave-rfc3489bis-10.txt S 10.2.1 - 10.2.2 */
+int
+nr_stun_receive_request_long_term_auth(nr_stun_message *req, nr_stun_server_ctx *ctx, nr_stun_message *res)
+{
+ int r,_status;
+ nr_stun_message_attribute *mi;
+ nr_stun_message_attribute *n;
+ nr_stun_server_client *clnt = 0;
+
+ switch (req->header.magic_cookie) {
+ default:
+ /* in RFC 3489 there is no magic cookie, it's part of the transaction ID */
+ /* drop thru */
+ case NR_STUN_MAGIC_COOKIE:
+ if (!nr_stun_message_has_attribute(req, NR_STUN_ATTR_USERNAME, 0)) {
+ nr_stun_form_error_response(req, res, 400, "Missing USERNAME");
+ nr_stun_add_realm_and_nonce(0, 0, res);
+ ABORT(R_ALREADY);
+ }
+
+ if ((r=nr_stun_get_message_client(ctx, req, &clnt))) {
+ nr_stun_form_error_response(req, res, 401, "Unrecognized USERNAME");
+ nr_stun_add_realm_and_nonce(0, 0, res);
+ ABORT(R_ALREADY);
+ }
+
+ if (!nr_stun_message_has_attribute(req, NR_STUN_ATTR_MESSAGE_INTEGRITY, &mi)) {
+ nr_stun_form_error_response(req, res, 401, "Missing MESSAGE-INTEGRITY");
+ nr_stun_add_realm_and_nonce(0, clnt, res);
+ ABORT(R_ALREADY);
+ }
+
+ assert(!mi->u.message_integrity.unknown_user);
+
+ if (!nr_stun_message_has_attribute(req, NR_STUN_ATTR_REALM, 0)) {
+ nr_stun_form_error_response(req, res, 400, "Missing REALM");
+ ABORT(R_ALREADY);
+ }
+
+ if (!nr_stun_message_has_attribute(req, NR_STUN_ATTR_NONCE, &n)) {
+ nr_stun_form_error_response(req, res, 400, "Missing NONCE");
+ ABORT(R_ALREADY);
+ }
+
+ assert(sizeof(clnt->nonce) == sizeof(n->u.nonce));
+ if (strncmp(clnt->nonce, n->u.nonce, sizeof(n->u.nonce))) {
+ nr_stun_form_error_response(req, res, 438, "Stale NONCE");
+ nr_stun_add_realm_and_nonce(1, clnt, res);
+ ABORT(R_ALREADY);
+ }
+
+ if (!mi->u.message_integrity.valid) {
+ nr_stun_form_error_response(req, res, 401, "Bad MESSAGE-INTEGRITY");
+ nr_stun_add_realm_and_nonce(0, clnt, res);
+ ABORT(R_ALREADY);
+ }
+
+ break;
+
+#ifdef USE_STUND_0_96
+ case NR_STUN_MAGIC_COOKIE2:
+ /* nothing to do in this case */
+ break;
+#endif /* USE_STUND_0_96 */
+ }
+
+ _status=0;
+ abort:
+
+ return _status;
+}
+
+/* draft-ietf-behave-rfc3489bis-10.txt S 10.2.3 */
+int
+nr_stun_receive_response_long_term_auth(nr_stun_message *res, nr_stun_client_ctx *ctx)
+{
+ int _status;
+ nr_stun_message_attribute *attr;
+
+ switch (res->header.magic_cookie) {
+ default:
+ /* in RFC 3489 there is no magic cookie, it's part of the transaction ID */
+ /* drop thru */
+ case NR_STUN_MAGIC_COOKIE:
+ if (nr_stun_message_has_attribute(res, NR_STUN_ATTR_REALM, &attr)) {
+ RFREE(ctx->realm);
+ ctx->realm = r_strdup(attr->u.realm);
+ if (!ctx->realm)
+ ABORT(R_NO_MEMORY);
+ }
+ else {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Missing REALM");
+ ABORT(R_REJECTED);
+ }
+
+ if (nr_stun_message_has_attribute(res, NR_STUN_ATTR_NONCE, &attr)) {
+ RFREE(ctx->nonce);
+ ctx->nonce = r_strdup(attr->u.nonce);
+ if (!ctx->nonce)
+ ABORT(R_NO_MEMORY);
+ }
+ else {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Missing NONCE");
+ ABORT(R_REJECTED);
+ }
+
+ if (nr_stun_message_has_attribute(res, NR_STUN_ATTR_MESSAGE_INTEGRITY, &attr)) {
+ if (!attr->u.message_integrity.valid) {
+ r_log(NR_LOG_STUN, LOG_WARNING, "Bad MESSAGE-INTEGRITY");
+ ABORT(R_REJECTED);
+ }
+ }
+
+ break;
+
+#ifdef USE_STUND_0_96
+ case NR_STUN_MAGIC_COOKIE2:
+ /* nothing to check in this case */
+ break;
+#endif /* USE_STUND_0_96 */
+ }
+
+ _status=0;
+ abort:
+ return _status;
+}
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_proc.h b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_proc.h
new file mode 100644
index 0000000000..5975670779
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_proc.h
@@ -0,0 +1,53 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+
+#ifndef _stun_proc_h
+#define _stun_proc_h
+
+#include "stun.h"
+
+int nr_stun_receive_message(nr_stun_message *req, nr_stun_message *msg);
+int nr_stun_process_request(nr_stun_message *req, nr_stun_message *res);
+int nr_stun_process_indication(nr_stun_message *ind);
+int nr_stun_process_success_response(nr_stun_message *res);
+int nr_stun_process_error_response(nr_stun_message *res, UINT2 *error_code);
+
+int nr_stun_receive_request_or_indication_short_term_auth(nr_stun_message *msg, nr_stun_message *res);
+int nr_stun_receive_response_short_term_auth(nr_stun_message *res);
+
+int nr_stun_receive_request_long_term_auth(nr_stun_message *req, nr_stun_server_ctx *ctx, nr_stun_message *res);
+int nr_stun_receive_response_long_term_auth(nr_stun_message *res, nr_stun_client_ctx *ctx);
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_reg.h b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_reg.h
new file mode 100644
index 0000000000..2167fcd91b
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_reg.h
@@ -0,0 +1,58 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+
+#ifndef _stun_reg_h
+#define _stun_reg_h
+#ifdef __cplusplus
+using namespace std;
+extern "C" {
+#endif /* __cplusplus */
+
+#define NR_STUN_REG_PREF_CLNT_RETRANSMIT_TIMEOUT "stun.client.retransmission_timeout"
+#define NR_STUN_REG_PREF_CLNT_RETRANSMIT_BACKOFF "stun.client.retransmission_backoff_factor"
+#define NR_STUN_REG_PREF_CLNT_MAXIMUM_TRANSMITS "stun.client.maximum_transmits"
+#define NR_STUN_REG_PREF_CLNT_FINAL_RETRANSMIT_BACKOFF "stun.client.final_retransmit_backoff"
+
+#define NR_STUN_REG_PREF_ALLOW_LOOPBACK_ADDRS "stun.allow_loopback"
+#define NR_STUN_REG_PREF_ALLOW_LINK_LOCAL_ADDRS "stun.allow_link_local"
+#define NR_STUN_REG_PREF_ADDRESS_PRFX "stun.address"
+#define NR_STUN_REG_PREF_SERVER_NAME "stun.server.name"
+#define NR_STUN_REG_PREF_SERVER_NONCE_SIZE "stun.server.nonce_size"
+#define NR_STUN_REG_PREF_SERVER_REALM "stun.server.realm"
+
+#ifdef __cplusplus
+}
+#endif /* __cplusplus */
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_server_ctx.c b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_server_ctx.c
new file mode 100644
index 0000000000..b92b6b5ab6
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_server_ctx.c
@@ -0,0 +1,468 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <string.h>
+#include <assert.h>
+
+#include "nr_api.h"
+#include "stun.h"
+
+static int nr_stun_server_destroy_client(nr_stun_server_client *clnt);
+static int nr_stun_server_send_response(nr_stun_server_ctx *ctx, nr_socket *sock, nr_transport_addr *peer_addr, nr_stun_message *res, nr_stun_server_client *clnt);
+static int nr_stun_server_process_request_auth_checks(nr_stun_server_ctx *ctx, nr_stun_message *req, int auth_rule, nr_stun_message *res);
+
+
+int nr_stun_server_ctx_create(char *label, nr_stun_server_ctx **ctxp)
+ {
+ int r,_status;
+ nr_stun_server_ctx *ctx=0;
+
+ if ((r=nr_stun_startup()))
+ ABORT(r);
+
+ if(!(ctx=RCALLOC(sizeof(nr_stun_server_ctx))))
+ ABORT(R_NO_MEMORY);
+
+ if(!(ctx->label=r_strdup(label)))
+ ABORT(R_NO_MEMORY);
+
+ STAILQ_INIT(&ctx->clients);
+
+ *ctxp=ctx;
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_stun_server_ctx_destroy(nr_stun_server_ctx **ctxp)
+ {
+ nr_stun_server_ctx *ctx;
+ nr_stun_server_client *clnt1,*clnt2;
+
+ if(!ctxp || !*ctxp)
+ return(0);
+
+ ctx=*ctxp;
+
+ STAILQ_FOREACH_SAFE(clnt1, &ctx->clients, entry, clnt2) {
+ nr_stun_server_destroy_client(clnt1);
+ }
+
+ nr_stun_server_destroy_client(ctx->default_client);
+
+ RFREE(ctx->label);
+ RFREE(ctx);
+
+ return(0);
+ }
+
+static int nr_stun_server_client_create(nr_stun_server_ctx *ctx, char *client_label, char *user, Data *pass, nr_stun_server_cb cb, void *cb_arg, nr_stun_server_client **clntp)
+ {
+ nr_stun_server_client *clnt=0;
+ int r,_status;
+
+ if(!(clnt=RCALLOC(sizeof(nr_stun_server_client))))
+ ABORT(R_NO_MEMORY);
+
+ if(!(clnt->label=r_strdup(client_label)))
+ ABORT(R_NO_MEMORY);
+
+ if(!(clnt->username=r_strdup(user)))
+ ABORT(R_NO_MEMORY);
+
+ if(r=r_data_copy(&clnt->password,pass))
+ ABORT(r);
+
+ r_log(NR_LOG_STUN,LOG_DEBUG,"STUN-SERVER(%s)/CLIENT(%s): Adding client for %s",ctx->label, client_label, user);
+ clnt->stun_server_cb=cb;
+ clnt->cb_arg=cb_arg;
+
+ *clntp = clnt;
+ _status=0;
+ abort:
+ if(_status){
+ nr_stun_server_destroy_client(clnt);
+ }
+ return(_status);
+ }
+
+int nr_stun_server_add_client(nr_stun_server_ctx *ctx, char *client_label, char *user, Data *pass, nr_stun_server_cb cb, void *cb_arg)
+ {
+ int r,_status;
+ nr_stun_server_client *clnt;
+
+ if (r=nr_stun_server_client_create(ctx, client_label, user, pass, cb, cb_arg, &clnt))
+ ABORT(r);
+
+ STAILQ_INSERT_TAIL(&ctx->clients,clnt,entry);
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_stun_server_add_default_client(nr_stun_server_ctx *ctx, char *ufrag, Data *pass, nr_stun_server_cb cb, void *cb_arg)
+ {
+ int r,_status;
+ nr_stun_server_client *clnt;
+
+ assert(!ctx->default_client);
+ if (ctx->default_client)
+ ABORT(R_INTERNAL);
+
+ if (r=nr_stun_server_client_create(ctx, "default_client", ufrag, pass, cb, cb_arg, &clnt))
+ ABORT(r);
+
+ ctx->default_client = clnt;
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_stun_server_remove_client(nr_stun_server_ctx *ctx, void *cb_arg)
+ {
+ nr_stun_server_client *clnt1,*clnt2;
+ int found = 0;
+
+ STAILQ_FOREACH_SAFE(clnt1, &ctx->clients, entry, clnt2) {
+ if(clnt1->cb_arg == cb_arg) {
+ STAILQ_REMOVE(&ctx->clients, clnt1, nr_stun_server_client_, entry);
+ nr_stun_server_destroy_client(clnt1);
+ found++;
+ }
+ }
+
+ if (!found)
+ ERETURN(R_NOT_FOUND);
+
+ return 0;
+ }
+
+static int nr_stun_server_get_password(void *arg, nr_stun_message *msg, Data **password)
+ {
+ int _status;
+ nr_stun_server_ctx *ctx = (nr_stun_server_ctx*)arg;
+ nr_stun_server_client *clnt = 0;
+ nr_stun_message_attribute *username_attribute;
+
+ if ((nr_stun_get_message_client(ctx, msg, &clnt))) {
+ if (! nr_stun_message_has_attribute(msg, NR_STUN_ATTR_USERNAME, &username_attribute)) {
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-SERVER(%s): Missing Username",ctx->label);
+ ABORT(R_NOT_FOUND);
+ }
+
+ /* Although this is an exceptional condition, we'll already have seen a
+ * NOTICE-level log message about the unknown user, so additional log
+ * messages at any level higher than DEBUG are unnecessary. */
+
+ r_log(NR_LOG_STUN,LOG_DEBUG,"STUN-SERVER(%s): Unable to find password for unknown user: %s",ctx->label,username_attribute->u.username);
+ ABORT(R_NOT_FOUND);
+ }
+
+ *password = &clnt->password;
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_stun_server_process_request_auth_checks(nr_stun_server_ctx *ctx, nr_stun_message *req, int auth_rule, nr_stun_message *res)
+ {
+ int r,_status;
+
+ if (nr_stun_message_has_attribute(req, NR_STUN_ATTR_MESSAGE_INTEGRITY, 0)
+ || !(auth_rule & NR_STUN_AUTH_RULE_OPTIONAL)) {
+ /* favor long term credentials over short term, if both are supported */
+
+ if (auth_rule & NR_STUN_AUTH_RULE_LONG_TERM) {
+ if ((r=nr_stun_receive_request_long_term_auth(req, ctx, res)))
+ ABORT(r);
+ }
+ else if (auth_rule & NR_STUN_AUTH_RULE_SHORT_TERM) {
+ if ((r=nr_stun_receive_request_or_indication_short_term_auth(req, res)))
+ ABORT(r);
+ }
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_stun_server_process_request(nr_stun_server_ctx *ctx, nr_socket *sock, char *msg, int len, nr_transport_addr *peer_addr, int auth_rule)
+ {
+ int r,_status;
+ char string[256];
+ nr_stun_message *req = 0;
+ nr_stun_message *res = 0;
+ nr_stun_server_client *clnt = 0;
+ nr_stun_server_request info;
+ int error;
+ int dont_free = 0;
+ nr_transport_addr my_addr;
+
+ if ((r=nr_socket_getaddr(sock, &my_addr))) {
+ ABORT(r);
+ }
+
+ r_log(NR_LOG_STUN,LOG_DEBUG,"STUN-SERVER(%s): Received(my_addr=%s,peer_addr=%s)",ctx->label,my_addr.as_string,peer_addr->as_string);
+
+ snprintf(string, sizeof(string)-1, "STUN-SERVER(%s): Received ", ctx->label);
+ r_dump(NR_LOG_STUN, LOG_DEBUG, string, (char*)msg, len);
+
+ memset(&info,0,sizeof(info));
+
+ if ((r=nr_stun_message_create2(&req, (UCHAR*)msg, len)))
+ ABORT(r);
+
+ if ((r=nr_stun_message_create(&res)))
+ ABORT(r);
+
+ if ((r=nr_stun_decode_message(req, nr_stun_server_get_password, ctx))) {
+ /* RFC5389 S 7.3 says "If any errors are detected, the message is
+ * silently discarded." */
+#ifndef USE_STUN_PEDANTIC
+ /* ... but that seems like a bad idea, at least return a 400 so
+ * that the server isn't a black hole to the client */
+ nr_stun_form_error_response(req, res, 400, "Bad Request - Failed to decode request");
+ ABORT(R_ALREADY);
+#endif /* USE_STUN_PEDANTIC */
+ ABORT(R_REJECTED);
+ }
+
+ if ((r=nr_stun_receive_message(0, req))) {
+ /* RFC5389 S 7.3 says "If any errors are detected, the message is
+ * silently discarded." */
+#ifndef USE_STUN_PEDANTIC
+ /* ... but that seems like a bad idea, at least return a 400 so
+ * that the server isn't a black hole to the client */
+ nr_stun_form_error_response(req, res, 400, "Bad Request - Section 7.3 check failed");
+ ABORT(R_ALREADY);
+#endif /* USE_STUN_PEDANTIC */
+ ABORT(R_REJECTED);
+ }
+
+ if (NR_STUN_GET_TYPE_CLASS(req->header.type) != NR_CLASS_REQUEST
+ && NR_STUN_GET_TYPE_CLASS(req->header.type) != NR_CLASS_INDICATION) {
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-SERVER(%s): Illegal message type: %04x",ctx->label,req->header.type);
+ /* RFC5389 S 7.3 says "If any errors are detected, the message is
+ * silently discarded." */
+#ifndef USE_STUN_PEDANTIC
+ /* ... but that seems like a bad idea, at least return a 400 so
+ * that the server isn't a black hole to the client */
+ nr_stun_form_error_response(req, res, 400, "Bad Request - Unsupported message type");
+ ABORT(R_ALREADY);
+#endif /* USE_STUN_PEDANTIC */
+ ABORT(R_REJECTED);
+ }
+
+ /* "The STUN agent then does any checks that are required by a
+ * authentication mechanism that the usage has specified" */
+ if ((r=nr_stun_server_process_request_auth_checks(ctx, req, auth_rule, res)))
+ ABORT(r);
+
+ if (NR_STUN_GET_TYPE_CLASS(req->header.type) == NR_CLASS_INDICATION) {
+ if ((r=nr_stun_process_indication(req)))
+ ABORT(r);
+ }
+ else {
+ if ((r=nr_stun_process_request(req, res)))
+ ABORT(r);
+ }
+
+ assert(res->header.type == 0);
+
+ clnt = 0;
+ if (NR_STUN_GET_TYPE_CLASS(req->header.type) == NR_CLASS_REQUEST) {
+ if ((nr_stun_get_message_client(ctx, req, &clnt))) {
+ if ((r=nr_stun_form_success_response(req, peer_addr, 0, res)))
+ ABORT(r);
+ }
+ else {
+ if ((r=nr_stun_form_success_response(req, peer_addr, &clnt->password, res)))
+ ABORT(r);
+ }
+ }
+
+ if(clnt && clnt->stun_server_cb){
+ r_log(NR_LOG_STUN,LOG_DEBUG,"Entering STUN server callback");
+
+ /* Set up the info */
+ if(r=nr_transport_addr_copy(&info.src_addr,peer_addr))
+ ABORT(r);
+
+ info.request = req;
+ info.response = res;
+
+ error = 0;
+ dont_free = 0;
+ if (clnt->stun_server_cb(clnt->cb_arg,ctx,sock,&info,&dont_free,&error)) {
+ if (error == 0)
+ error = 500;
+
+ nr_stun_form_error_response(req, res, error, "ICE Failure");
+ ABORT(R_ALREADY);
+ }
+ }
+
+ _status=0;
+ abort:
+ if (!res)
+ goto skip_response;
+
+ if (NR_STUN_GET_TYPE_CLASS(req->header.type) == NR_CLASS_INDICATION)
+ goto skip_response;
+
+ /* Now respond */
+
+ if (_status != 0 && ! nr_stun_message_has_attribute(res, NR_STUN_ATTR_ERROR_CODE, 0))
+ nr_stun_form_error_response(req, res, 500, "Failed to specify error");
+
+ if ((r=nr_stun_server_send_response(ctx, sock, peer_addr, res, clnt))) {
+ r_log(NR_LOG_STUN,LOG_ERR,"STUN-SERVER(label=%s): Failed sending response (my_addr=%s,peer_addr=%s)",ctx->label,my_addr.as_string,peer_addr->as_string);
+ _status = R_FAILED;
+ }
+
+#if 0
+ /* EKR: suppressed these checks because if you have an error when
+ you are sending an error, things go wonky */
+#ifdef SANITY_CHECKS
+ if (_status == R_ALREADY) {
+ assert(NR_STUN_GET_TYPE_CLASS(res->header.type) == NR_CLASS_ERROR_RESPONSE);
+ assert(nr_stun_message_has_attribute(res, NR_STUN_ATTR_ERROR_CODE, 0));
+ }
+ else {
+ assert(NR_STUN_GET_TYPE_CLASS(res->header.type) == NR_CLASS_RESPONSE);
+ assert(!nr_stun_message_has_attribute(res, NR_STUN_ATTR_ERROR_CODE, 0));
+ }
+#endif /* SANITY_CHECKS */
+#endif
+
+ if (0) {
+ skip_response:
+ _status = 0;
+ }
+
+ if (!dont_free) {
+ nr_stun_message_destroy(&res);
+ nr_stun_message_destroy(&req);
+ }
+
+ return(_status);
+ }
+
+static int nr_stun_server_send_response(nr_stun_server_ctx *ctx, nr_socket *sock, nr_transport_addr *peer_addr, nr_stun_message *res, nr_stun_server_client *clnt)
+ {
+ int r,_status;
+ char string[256];
+ nr_transport_addr my_addr;
+
+ if ((r=nr_socket_getaddr(sock, &my_addr))) {
+ ABORT(r);
+ }
+
+ r_log(NR_LOG_STUN,LOG_DEBUG,"STUN-SERVER(label=%s): Sending(my_addr=%s,peer_addr=%s)",ctx->label,my_addr.as_string,peer_addr->as_string);
+
+ if ((r=nr_stun_encode_message(res))) {
+ /* should never happen */
+ r_log(NR_LOG_STUN,LOG_ERR,"STUN-SERVER(label=%s): Unable to encode message", ctx->label);
+ }
+ else {
+ snprintf(string, sizeof(string)-1, "STUN(%s): Sending to %s ", ctx->label, peer_addr->as_string);
+ r_dump(NR_LOG_STUN, LOG_DEBUG, string, (char*)res->buffer, res->length);
+
+ if(r=nr_socket_sendto(sock,res->buffer,res->length,0,peer_addr))
+ ABORT(r);
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+static int nr_stun_server_destroy_client(nr_stun_server_client *clnt)
+ {
+ if (!clnt)
+ return 0;
+
+ RFREE(clnt->label);
+ RFREE(clnt->username);
+ r_data_zfree(&clnt->password);
+
+ RFREE(clnt);
+ return(0);
+ }
+
+int nr_stun_get_message_client(nr_stun_server_ctx *ctx, nr_stun_message *req, nr_stun_server_client **out)
+ {
+ int _status;
+ nr_stun_message_attribute *attr;
+ nr_stun_server_client *clnt=0;
+
+ if (! nr_stun_message_has_attribute(req, NR_STUN_ATTR_USERNAME, &attr)) {
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-SERVER(%s): Missing Username",ctx->label);
+ ABORT(R_NOT_FOUND);
+ }
+
+ STAILQ_FOREACH(clnt, &ctx->clients, entry) {
+ if (!strncmp(clnt->username, attr->u.username,
+ sizeof(attr->u.username)))
+ break;
+ }
+
+ if (!clnt && ctx->default_client) {
+ /* If we can't find a specific client see if this matches the default,
+ which means that the username starts with our ufrag.
+ */
+ char *colon = strchr(attr->u.username, ':');
+ if (colon && !strncmp(ctx->default_client->username,
+ attr->u.username,
+ colon - attr->u.username)) {
+ clnt = ctx->default_client;
+ r_log(NR_LOG_STUN,LOG_NOTICE,"STUN-SERVER(%s): Falling back to default client, username=: %s",ctx->label,attr->u.username);
+ }
+ }
+
+ if (!clnt) {
+ r_log(NR_LOG_STUN,LOG_WARNING,"STUN-SERVER(%s): Request from unknown user: %s",ctx->label,attr->u.username);
+ ABORT(R_NOT_FOUND);
+ }
+
+ *out = clnt;
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_server_ctx.h b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_server_ctx.h
new file mode 100644
index 0000000000..328675ee22
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_server_ctx.h
@@ -0,0 +1,80 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+
+#ifndef _stun_server_ctx_h
+#define _stun_server_ctx_h
+
+typedef struct nr_stun_server_ctx_ nr_stun_server_ctx;
+typedef struct nr_stun_server_client_ nr_stun_server_client;
+
+#include "stun_util.h"
+
+
+typedef struct nr_stun_server_request_{
+ nr_transport_addr src_addr;
+ nr_stun_message *request;
+ nr_stun_message *response;
+} nr_stun_server_request;
+
+typedef int (*nr_stun_server_cb)(void *cb_arg, struct nr_stun_server_ctx_ *ctx,nr_socket *sock,nr_stun_server_request *req, int *dont_free, int *error);
+
+struct nr_stun_server_client_ {
+ char *label;
+ char *username;
+ Data password;
+ nr_stun_server_cb stun_server_cb;
+ void *cb_arg;
+ char nonce[NR_STUN_MAX_NONCE_BYTES+1]; /* +1 for \0 */
+ STAILQ_ENTRY(nr_stun_server_client_) entry;
+};
+
+typedef STAILQ_HEAD(nr_stun_server_client_head_, nr_stun_server_client_) nr_stun_server_client_head;
+
+struct nr_stun_server_ctx_ {
+ char *label;
+ nr_stun_server_client_head clients;
+ nr_stun_server_client *default_client;
+};
+
+
+int nr_stun_server_ctx_create(char *label, nr_stun_server_ctx **ctxp);
+int nr_stun_server_ctx_destroy(nr_stun_server_ctx **ctxp);
+int nr_stun_server_add_client(nr_stun_server_ctx *ctx, char *client_label, char *user, Data *pass, nr_stun_server_cb cb, void *cb_arg);
+int nr_stun_server_remove_client(nr_stun_server_ctx *ctx, void *cb_arg);
+int nr_stun_server_add_default_client(nr_stun_server_ctx *ctx, char *ufrag, Data *pass, nr_stun_server_cb cb, void *cb_arg);
+int nr_stun_server_process_request(nr_stun_server_ctx *ctx, nr_socket *sock, char *msg, int len, nr_transport_addr *peer_addr, int auth_rule);
+int nr_stun_get_message_client(nr_stun_server_ctx *ctx, nr_stun_message *req, nr_stun_server_client **clnt);
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_util.c b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_util.c
new file mode 100644
index 0000000000..cbb0128924
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_util.c
@@ -0,0 +1,352 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <errno.h>
+#include <csi_platform.h>
+
+#ifdef WIN32
+#include <winsock2.h>
+#include <stdlib.h>
+#include <io.h>
+#include <time.h>
+#else /* UNIX */
+#include <string.h>
+#endif /* end UNIX */
+#include <assert.h>
+
+#include "stun.h"
+#include "stun_reg.h"
+#include "registry.h"
+#include "addrs.h"
+#include "transport_addr_reg.h"
+#include "nr_crypto.h"
+#include "hex.h"
+
+
+int NR_LOG_STUN = 0;
+
+int
+nr_stun_startup(void)
+{
+ int r,_status;
+
+ if ((r=r_log_register("stun", &NR_LOG_STUN)))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ return _status;
+}
+
+int
+nr_stun_xor_mapped_address(UINT4 magicCookie, UINT12 transactionId, nr_transport_addr *from, nr_transport_addr *to)
+{
+ int _status;
+
+ switch (from->ip_version) {
+ case NR_IPV4:
+ nr_ip4_port_to_transport_addr(
+ (ntohl(from->u.addr4.sin_addr.s_addr) ^ magicCookie),
+ (ntohs(from->u.addr4.sin_port) ^ (magicCookie>>16)),
+ from->protocol, to);
+ break;
+ case NR_IPV6:
+ {
+ union {
+ unsigned char addr[16];
+ UINT4 addr32[4];
+ } maskedAddr;
+
+ maskedAddr.addr32[0] = htonl(magicCookie); /* Passed in host byte order */
+ memcpy(&maskedAddr.addr32[1], transactionId.octet, sizeof(transactionId));
+
+ /* We now have the mask in network byte order */
+ /* Xor the address in network byte order */
+ for (size_t i = 0; i < sizeof(maskedAddr); ++i) {
+ maskedAddr.addr[i] ^= from->u.addr6.sin6_addr.s6_addr[i];
+ }
+
+ nr_ip6_port_to_transport_addr(
+ (struct in6_addr*)&maskedAddr,
+ (ntohs(from->u.addr6.sin6_port) ^ (magicCookie>>16)),
+ from->protocol, to);
+ }
+ break;
+ default:
+ assert(0);
+ ABORT(R_INTERNAL);
+ break;
+ }
+
+ _status = 0;
+ abort:
+ return _status;
+}
+
+int
+nr_stun_filter_local_addresses(nr_local_addr addrs[], int *count)
+{
+ int r,_status;
+ char allow_loopback = 0;
+ char allow_link_local = 0;
+
+ if ((r=NR_reg_get_char(NR_STUN_REG_PREF_ALLOW_LOOPBACK_ADDRS,
+ &allow_loopback))) {
+ if (r != R_NOT_FOUND) {
+ ABORT(r);
+ }
+ }
+
+ if ((r=NR_reg_get_char(NR_STUN_REG_PREF_ALLOW_LINK_LOCAL_ADDRS,
+ &allow_link_local))) {
+ if (r != R_NOT_FOUND) {
+ ABORT(r);
+ }
+ }
+
+ if ((r=nr_stun_filter_addrs(addrs,
+ !allow_loopback,
+ !allow_link_local,
+ count))) {
+ ABORT(r);
+ }
+
+ _status=0;
+ abort:
+ return _status;
+}
+
+int
+nr_stun_find_local_addresses(nr_local_addr addrs[], int maxaddrs, int *count)
+{
+ int r,_status;
+ //NR_registry *children = 0;
+
+ *count = 0;
+
+#if 0
+ // this really goes with the code commented out below. (mjf)
+ if ((r=NR_reg_get_child_count(NR_STUN_REG_PREF_ADDRESS_PRFX, (unsigned int*)count)))
+ if (r != R_NOT_FOUND)
+ ABORT(r);
+#endif
+
+ if (*count == 0) {
+ if ((r=nr_stun_get_addrs(addrs, maxaddrs, count)))
+ ABORT(r);
+
+ goto done;
+ }
+
+ if (*count >= maxaddrs) {
+ r_log(NR_LOG_STUN, LOG_INFO, "Address list truncated from %d to %d", *count, maxaddrs);
+ *count = maxaddrs;
+ }
+
+#if 0
+ if (*count > 0) {
+ /* TODO(ekr@rtfm.com): Commented out 2012-07-26.
+
+ This code is currently not used in Firefox and needs to be
+ ported to 64-bit */
+ children = RCALLOC((*count + 10) * sizeof(*children));
+ if (!children)
+ ABORT(R_NO_MEMORY);
+
+ assert(sizeof(size_t) == sizeof(*count));
+
+ if ((r=NR_reg_get_children(NR_STUN_REG_PREF_ADDRESS_PRFX, children, (size_t)(*count + 10), (size_t*)count)))
+ ABORT(r);
+
+ for (i = 0; i < *count; ++i) {
+ if ((r=nr_reg_get_transport_addr(children[i], 0, &addrs[i].addr)))
+ ABORT(r);
+ }
+ }
+#endif
+
+ done:
+
+ _status=0;
+ abort:
+ //RFREE(children);
+ return _status;
+}
+
+int
+nr_stun_different_transaction(UCHAR *msg, size_t len, nr_stun_message *req)
+{
+ int _status;
+ nr_stun_message_header header;
+ char reqid[44];
+ char msgid[44];
+ size_t unused;
+
+ if (sizeof(header) > len)
+ ABORT(R_FAILED);
+
+ assert(sizeof(header.id) == sizeof(UINT12));
+
+ memcpy(&header, msg, sizeof(header));
+
+ if (memcmp(&req->header.id, &header.id, sizeof(header.id))) {
+ nr_nbin2hex((UCHAR*)&req->header.id, sizeof(req->header.id), reqid, sizeof(reqid), &unused);
+ nr_nbin2hex((UCHAR*)&header.id, sizeof(header.id), msgid, sizeof(msgid), &unused);
+ r_log(NR_LOG_STUN, LOG_DEBUG, "Mismatched message IDs %s/%s", reqid, msgid);
+ ABORT(R_NOT_FOUND);
+ }
+
+ _status=0;
+ abort:
+ return _status;
+}
+
+char*
+nr_stun_msg_type(int type)
+{
+ char *ret = 0;
+
+ switch (type) {
+ case NR_STUN_MSG_BINDING_REQUEST:
+ ret = "BINDING-REQUEST";
+ break;
+ case NR_STUN_MSG_BINDING_INDICATION:
+ ret = "BINDING-INDICATION";
+ break;
+ case NR_STUN_MSG_BINDING_RESPONSE:
+ ret = "BINDING-RESPONSE";
+ break;
+ case NR_STUN_MSG_BINDING_ERROR_RESPONSE:
+ ret = "BINDING-ERROR-RESPONSE";
+ break;
+
+#ifdef USE_TURN
+ case NR_STUN_MSG_ALLOCATE_REQUEST:
+ ret = "ALLOCATE-REQUEST";
+ break;
+ case NR_STUN_MSG_ALLOCATE_RESPONSE:
+ ret = "ALLOCATE-RESPONSE";
+ break;
+ case NR_STUN_MSG_ALLOCATE_ERROR_RESPONSE:
+ ret = "ALLOCATE-ERROR-RESPONSE";
+ break;
+ case NR_STUN_MSG_REFRESH_REQUEST:
+ ret = "REFRESH-REQUEST";
+ break;
+ case NR_STUN_MSG_REFRESH_RESPONSE:
+ ret = "REFRESH-RESPONSE";
+ break;
+ case NR_STUN_MSG_REFRESH_ERROR_RESPONSE:
+ ret = "REFRESH-ERROR-RESPONSE";
+ break;
+ case NR_STUN_MSG_SEND_INDICATION:
+ ret = "SEND-INDICATION";
+ break;
+ case NR_STUN_MSG_DATA_INDICATION:
+ ret = "DATA-INDICATION";
+ break;
+ case NR_STUN_MSG_PERMISSION_REQUEST:
+ ret = "PERMISSION-REQUEST";
+ break;
+ case NR_STUN_MSG_PERMISSION_RESPONSE:
+ ret = "PERMISSION-RESPONSE";
+ break;
+ case NR_STUN_MSG_PERMISSION_ERROR_RESPONSE:
+ ret = "PERMISSION-ERROR-RESPONSE";
+ break;
+#endif /* USE_TURN */
+
+ default:
+ /* ret remains 0 */
+ break;
+ }
+
+ return ret;
+}
+
+int
+nr_random_alphanum(char *alphanum, int size)
+{
+ static char alphanums[256] = {
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
+ 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
+ 'U', 'V', 'W', 'X', 'Y', 'Z',
+ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
+ 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
+ 'u', 'v', 'w', 'x', 'y', 'z',
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
+ 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
+ 'U', 'V', 'W', 'X', 'Y', 'Z',
+ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
+ 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
+ 'u', 'v', 'w', 'x', 'y', 'z',
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
+ 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
+ 'U', 'V', 'W', 'X', 'Y', 'Z',
+ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
+ 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
+ 'u', 'v', 'w', 'x', 'y', 'z',
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
+ 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
+ 'U', 'V', 'W', 'X', 'Y', 'Z',
+ 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j',
+ 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't',
+ 'u', 'v', 'w', 'x', 'y', 'z',
+ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H' };
+ int i;
+
+ nr_crypto_random_bytes((UCHAR*)alphanum, size);
+
+ /* now convert from binary to alphanumeric */
+ for (i = 0; i < size; ++i)
+ alphanum[i] = alphanums[(UCHAR)alphanum[i]];
+
+ return 0;
+}
+
+#ifndef UINT2_MAX
+#define UINT2_MAX ((UINT2)(65535U))
+#endif
+
+void nr_accumulate_count(UINT2* orig_count, UINT2 add_count)
+ {
+ if (UINT2_MAX - add_count < *orig_count) {
+ // don't rollover, just stop accumulating at MAX value
+ *orig_count = UINT2_MAX;
+ } else {
+ *orig_count += add_count;
+ }
+ }
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_util.h b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_util.h
new file mode 100644
index 0000000000..379b4fdd3d
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/stun/stun_util.h
@@ -0,0 +1,62 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+
+#ifndef _stun_util_h
+#define _stun_util_h
+
+#include "stun.h"
+#include "local_addr.h"
+
+extern int NR_LOG_STUN;
+
+int nr_stun_startup(void);
+
+int nr_stun_xor_mapped_address(UINT4 magicCookie, UINT12 transactionId, nr_transport_addr *from, nr_transport_addr *to);
+
+// removes duplicates, and based on prefs, loopback and link_local addresses
+int nr_stun_filter_local_addresses(nr_local_addr addrs[], int *count);
+
+int nr_stun_find_local_addresses(nr_local_addr addrs[], int maxaddrs, int *count);
+
+int nr_stun_different_transaction(UCHAR *msg, size_t len, nr_stun_message *req);
+
+char* nr_stun_msg_type(int type);
+
+int nr_random_alphanum(char *alphanum, int size);
+
+// accumulate a count without worrying about rollover
+void nr_accumulate_count(UINT2* orig_count, UINT2 add_count);
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/stun/turn_client_ctx.c b/dom/media/webrtc/transport/third_party/nICEr/src/stun/turn_client_ctx.c
new file mode 100644
index 0000000000..707d43a4a9
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/stun/turn_client_ctx.c
@@ -0,0 +1,1277 @@
+/*
+ Copyright (c) 2007, Adobe Systems, Incorporated
+ Copyright (c) 2013, Mozilla
+
+ All rights reserved.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions are
+ met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+ * Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+ * Neither the name of Adobe Systems, Network Resonance, Mozilla nor
+ the names of its contributors may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#ifdef USE_TURN
+
+#include <assert.h>
+#include <string.h>
+
+#include "nr_api.h"
+#include "r_time.h"
+#include "async_timer.h"
+#include "nr_socket_buffered_stun.h"
+#include "stun.h"
+#include "turn_client_ctx.h"
+#include "ice_ctx.h"
+
+int NR_LOG_TURN = 0;
+
+#define TURN_MAX_PENDING_BYTES 32000
+
+#define TURN_RTO 100 /* Hardcoded RTO estimate */
+#define TURN_LIFETIME_REQUEST_SECONDS 3600 /* One hour */
+#define TURN_USECS_PER_S 1000000
+#define TURN_REFRESH_SLACK_SECONDS 10 /* How long before expiry to refresh
+ allocations/permissions. The RFC 5766
+ Section 7 recommendation if 60 seconds,
+ but this is silly since the transaction
+ times out after about 5. */
+#define TURN_PERMISSION_LIFETIME_SECONDS 300 /* 5 minutes. From RFC 5766 2.3 */
+
+// Set to enable a temporary fix that will run the TURN reservation keep-alive
+// logic when data is received via a TURN relayed path: a DATA_INDICATION packet is received.
+// TODO(pkerr@mozilla.com) This should be replace/removed when bug 935806 is implemented.
+#define REFRESH_RESERVATION_ON_RECV 1
+
+static int nr_turn_stun_ctx_create(nr_turn_client_ctx *tctx, int type,
+ NR_async_cb success_cb,
+ NR_async_cb failure_cb,
+ nr_turn_stun_ctx **ctxp);
+static int nr_turn_stun_ctx_destroy(nr_turn_stun_ctx **ctxp);
+static void nr_turn_stun_ctx_cb(NR_SOCKET s, int how, void *arg);
+static int nr_turn_stun_set_auth_params(nr_turn_stun_ctx *ctx,
+ char *realm, char *nonce);
+static void nr_turn_client_refresh_timer_cb(NR_SOCKET s, int how, void *arg);
+static int nr_turn_client_refresh_setup(nr_turn_client_ctx *ctx,
+ nr_turn_stun_ctx **sctx);
+static int nr_turn_client_start_refresh_timer(nr_turn_client_ctx *ctx,
+ nr_turn_stun_ctx *sctx,
+ UINT4 lifetime);
+static int nr_turn_permission_create(nr_turn_client_ctx *ctx,
+ const nr_transport_addr *addr,
+ nr_turn_permission **permp);
+static int nr_turn_permission_find(nr_turn_client_ctx *ctx,
+ const nr_transport_addr *addr,
+ nr_turn_permission **permp);
+static int nr_turn_permission_destroy(nr_turn_permission **permp);
+static void nr_turn_client_refresh_cb(NR_SOCKET s, int how, void *arg);
+static void nr_turn_client_permissions_cb(NR_SOCKET s, int how, void *cb);
+static int nr_turn_client_send_stun_request(nr_turn_client_ctx *ctx,
+ nr_stun_message *req,
+ int flags);
+
+int nr_transport_addr_listnode_create(const nr_transport_addr *addr, nr_transport_addr_listnode **listnodep)
+{
+ nr_transport_addr_listnode *listnode = 0;
+ int r,_status;
+
+ if (!(listnode=RCALLOC(sizeof(nr_transport_addr_listnode)))) {
+ ABORT(R_NO_MEMORY);
+ }
+
+ if ((r = nr_transport_addr_copy(&listnode->value, addr))) {
+ ABORT(r);
+ }
+
+ *listnodep = listnode;
+ listnode = 0;
+ _status = 0;
+
+abort:
+ nr_transport_addr_listnode_destroy(&listnode);
+ return(_status);
+}
+
+void nr_transport_addr_listnode_destroy(nr_transport_addr_listnode **listnode)
+{
+ RFREE(*listnode);
+ *listnode = 0;
+}
+
+/* nr_turn_stun_ctx functions */
+static int nr_turn_stun_ctx_create(nr_turn_client_ctx *tctx, int mode,
+ NR_async_cb success_cb,
+ NR_async_cb error_cb,
+ nr_turn_stun_ctx **ctxp)
+{
+ nr_turn_stun_ctx *sctx = 0;
+ int r,_status;
+ char label[256];
+
+ if (!(sctx=RCALLOC(sizeof(nr_turn_stun_ctx))))
+ ABORT(R_NO_MEMORY);
+
+ /* TODO(ekr@rtfm.com): label by phase */
+ snprintf(label, sizeof(label), "%s:%s", tctx->label, ":TURN");
+
+ if ((r=nr_stun_client_ctx_create(label, tctx->sock, &tctx->turn_server_addr,
+ TURN_RTO, &sctx->stun))) {
+ ABORT(r);
+ }
+
+ /* Set the STUN auth parameters, but don't set authentication on.
+ For that we need the nonce, set in nr_turn_stun_set_auth_params.
+ */
+ sctx->stun->auth_params.username=tctx->username;
+ INIT_DATA(sctx->stun->auth_params.password,
+ tctx->password->data, tctx->password->len);
+
+ sctx->tctx=tctx;
+ sctx->success_cb=success_cb;
+ sctx->error_cb=error_cb;
+ sctx->mode=mode;
+ sctx->last_error_code=0;
+ STAILQ_INIT(&sctx->addresses_tried);
+
+ /* Add ourselves to the tctx's list */
+ STAILQ_INSERT_TAIL(&tctx->stun_ctxs, sctx, entry);
+ *ctxp=sctx;
+
+ _status=0;
+abort:
+ if (_status) {
+ nr_turn_stun_ctx_destroy(&sctx);
+ }
+ return(_status);
+}
+
+/* Note: this function does not pull us off the tctx's list. */
+static int nr_turn_stun_ctx_destroy(nr_turn_stun_ctx **ctxp)
+{
+ nr_turn_stun_ctx *ctx;
+
+ if (!ctxp || !*ctxp)
+ return 0;
+
+ ctx = *ctxp;
+ *ctxp = 0;
+
+ nr_stun_client_ctx_destroy(&ctx->stun);
+ RFREE(ctx->realm);
+ RFREE(ctx->nonce);
+
+ while (!STAILQ_EMPTY(&ctx->addresses_tried)) {
+ nr_transport_addr_listnode *listnode = STAILQ_FIRST(&ctx->addresses_tried);
+ STAILQ_REMOVE_HEAD(&ctx->addresses_tried, entry);
+ nr_transport_addr_listnode_destroy(&listnode);
+ }
+
+ RFREE(ctx);
+
+ return 0;
+}
+
+static int nr_turn_stun_set_auth_params(nr_turn_stun_ctx *ctx,
+ char *realm, char *nonce)
+{
+ int _status;
+
+ RFREE(ctx->realm);
+ RFREE(ctx->nonce);
+
+ assert(realm);
+ if (!realm)
+ ABORT(R_BAD_ARGS);
+ ctx->realm=r_strdup(realm);
+ if (!ctx->realm)
+ ABORT(R_NO_MEMORY);
+
+ assert(nonce);
+ if (!nonce)
+ ABORT(R_BAD_ARGS);
+ ctx->nonce=r_strdup(nonce);
+ if (!ctx->nonce)
+ ABORT(R_NO_MEMORY);
+
+ RFREE(ctx->stun->realm);
+ ctx->stun->realm = r_strdup(ctx->realm);
+ if (!ctx->stun->realm)
+ ABORT(R_NO_MEMORY);
+
+ ctx->stun->auth_params.realm = ctx->realm;
+ ctx->stun->auth_params.nonce = ctx->nonce;
+ ctx->stun->auth_params.authenticate = 1; /* May already be 1 */
+
+ _status=0;
+abort:
+ return(_status);
+}
+
+static int nr_turn_stun_ctx_start(nr_turn_stun_ctx *ctx)
+{
+ int r, _status;
+ nr_turn_client_ctx *tctx = ctx->tctx;
+ nr_transport_addr_listnode *address_tried = 0;
+
+ if ((r=nr_stun_client_reset(ctx->stun))) {
+ r_log(NR_LOG_TURN, LOG_ERR, "TURN(%s): Couldn't reset STUN",
+ tctx->label);
+ ABORT(r);
+ }
+
+ if ((r=nr_stun_client_start(ctx->stun, ctx->mode, nr_turn_stun_ctx_cb, ctx))) {
+ r_log(NR_LOG_TURN, LOG_ERR, "TURN(%s): Couldn't start STUN",
+ tctx->label);
+ ABORT(r);
+ }
+
+ if ((r=nr_transport_addr_listnode_create(&ctx->stun->peer_addr, &address_tried))) {
+ ABORT(r);
+ }
+
+ STAILQ_INSERT_TAIL(&ctx->addresses_tried, address_tried, entry);
+
+ _status=0;
+abort:
+ return _status;
+}
+
+static int nr_turn_stun_ctx_handle_redirect(nr_turn_stun_ctx *ctx)
+{
+ int r, _status;
+ nr_turn_client_ctx *tctx = ctx->tctx;
+ nr_stun_message_attribute *ec;
+ nr_stun_message_attribute *attr;
+ nr_transport_addr *alternate_addr = 0;
+ int index = 0;
+
+ if (!tctx->ctx) {
+ /* If we were to require TCP nr_sockets to allow multiple connect calls by
+ * disconnecting and re-connecting, we could avoid this requirement. */
+ r_log(NR_LOG_TURN, LOG_ERR,
+ "TURN(%s): nr_turn_stun_ctx_handle_redirect is not supported when "
+ "there is no ICE ctx, and by extension no socket factory, since we "
+ "need that to create new sockets in the TCP case",
+ tctx->label);
+ ABORT(R_BAD_ARGS);
+ }
+
+ if (ctx->stun->response->header.type != NR_STUN_MSG_ALLOCATE_ERROR_RESPONSE) {
+ r_log(NR_LOG_TURN, LOG_ERR,
+ "TURN(%s): nr_turn_stun_ctx_handle_redirect called for something "
+ "other than an Allocate error response (type=%d)",
+ tctx->label, ctx->stun->response->header.type);
+ ABORT(R_BAD_ARGS);
+ }
+
+ if (!nr_stun_message_has_attribute(ctx->stun->response,
+ NR_STUN_ATTR_ERROR_CODE, &ec) ||
+ (ec->u.error_code.number != 300)) {
+ r_log(NR_LOG_TURN, LOG_ERR,
+ "TURN(%s): nr_turn_stun_ctx_handle_redirect called without a "
+ "300 response",
+ tctx->label);
+ ABORT(R_BAD_ARGS);
+ }
+
+ while (!alternate_addr && !nr_stun_message_get_attribute(
+ ctx->stun->response, NR_STUN_ATTR_ALTERNATE_SERVER, index++, &attr)) {
+ alternate_addr = &attr->u.alternate_server;
+
+ // TODO: Someday we may need to handle IP version switching, but it is
+ // unclear how that is supposed to work with ICE when the IP version of
+ // the candidate's base address is fixed...
+ if (alternate_addr->ip_version != tctx->turn_server_addr.ip_version) {
+ r_log(NR_LOG_TURN, LOG_INFO,
+ "TURN(%s): nr_turn_stun_ctx_handle_redirect not trying %s, since it is a different IP version",
+ tctx->label, alternate_addr->as_string);
+ alternate_addr = 0;
+ continue;
+ }
+
+ /* Check if we've already tried this, and ignore if we have */
+ nr_transport_addr_listnode *address_tried = 0;
+ STAILQ_FOREACH(address_tried, &ctx->addresses_tried, entry) {
+ /* Ignore protocol */
+ alternate_addr->protocol = address_tried->value.protocol;
+ if (!nr_transport_addr_cmp(alternate_addr, &address_tried->value, NR_TRANSPORT_ADDR_CMP_MODE_ALL)) {
+ r_log(NR_LOG_TURN, LOG_INFO,
+ "TURN(%s): nr_turn_stun_ctx_handle_redirect already tried %s, ignoring",
+ tctx->label, alternate_addr->as_string);
+ alternate_addr = 0;
+ break;
+ }
+ }
+ }
+
+ if (!alternate_addr) {
+ /* Should we use a different error code depending on why we didn't find
+ * one? (eg; no ALTERNATE-SERVERS at all, none that we have not tried
+ * already, none that are of a compatible IP version?) */
+ r_log(NR_LOG_TURN, LOG_ERR,
+ "TURN(%s): nr_turn_stun_ctx_handle_redirect did not find a viable "
+ "ALTERNATE-SERVER",
+ tctx->label);
+ ABORT(R_FAILED);
+ }
+
+ r_log(NR_LOG_TURN, LOG_INFO,
+ "TURN(%s): nr_turn_stun_ctx_handle_redirect trying %s",
+ tctx->label, alternate_addr->as_string);
+
+ /* This also handles the call to nr_transport_addr_fmt_addr_string */
+ if ((r = nr_transport_addr_copy_addrport(&tctx->turn_server_addr,
+ alternate_addr))) {
+ r_log(NR_LOG_TURN, LOG_ERR,
+ "TURN(%s): nr_turn_stun_ctx_handle_redirect copying ALTERNATE-SERVER "
+ "failed(%d)!",
+ tctx->label, r);
+ assert(0);
+ ABORT(r);
+ }
+
+ /* TURN server address is now updated. Restart the STUN Allocate ctx. Note
+ * that we do not attempt to update the local address; if the TURN server
+ * redirects to something that is not best reached from the already-selected
+ * local address, oh well. */
+
+ if (tctx->turn_server_addr.protocol == IPPROTO_TCP) {
+ /* For TCP, we need to replace the underlying nr_socket, since we cannot
+ * un-connect it from the old server. */
+ /* If we were to require TCP nr_sockets to allow multiple connect calls by
+ * disconnecting and re-connecting, we could avoid this stuff, and just
+ * call nr_socket_connect. */
+ nr_transport_addr old_local_addr;
+ nr_socket* new_socket;
+ if ((r = nr_socket_getaddr(tctx->sock, &old_local_addr))) {
+ r_log(NR_LOG_TURN, LOG_ERR,
+ "TURN(%s): nr_turn_stun_ctx_handle_redirect "
+ "failed to get old local address (%d)!",
+ tctx->label, r);
+ assert(0);
+ ABORT(r);
+ }
+
+ if ((r = nr_socket_factory_create_socket(tctx->ctx->socket_factory,
+ &old_local_addr, &new_socket))) {
+ r_log(NR_LOG_TURN, LOG_ERR,
+ "TURN(%s): nr_turn_stun_ctx_handle_redirect "
+ "failed to create new raw TCP socket for redirect (%d)!",
+ tctx->label, r);
+ assert(0);
+ ABORT(r);
+ }
+
+ if ((r = nr_socket_buffered_stun_reset(tctx->sock, new_socket))) {
+ /* nr_socket_buffered_stun_reset always takes ownership of |new_socket| */
+ r_log(NR_LOG_TURN, LOG_ERR,
+ "TURN(%s): nr_turn_stun_ctx_handle_redirect "
+ "failed to update raw TCP socket (%d)!",
+ tctx->label, r);
+ assert(0);
+ ABORT(r);
+ }
+
+ if ((r = nr_socket_connect(tctx->sock, &tctx->turn_server_addr))) {
+ if (r != R_WOULDBLOCK) {
+ r_log(NR_LOG_TURN, LOG_ERR,
+ "TURN(%s): nr_turn_stun_ctx_handle_redirect nr_socket_connect "
+ "failed(%d)!",
+ tctx->label, r);
+ assert(0);
+ ABORT(r);
+ }
+ }
+ }
+
+ nr_transport_addr_copy(&ctx->stun->peer_addr, &tctx->turn_server_addr);
+
+ if ((r = nr_turn_stun_ctx_start(ctx))) {
+ r_log(NR_LOG_TURN, LOG_ERR,
+ "TURN(%s): nr_turn_stun_ctx_handle_redirect nr_turn_stun_ctx_start "
+ "failed(%d)!",
+ tctx->label, r);
+ assert(0);
+ ABORT(r);
+ }
+
+ _status = 0;
+abort:
+ return _status;
+}
+
+static void nr_turn_stun_ctx_cb(NR_SOCKET s, int how, void *arg)
+{
+ int r, _status;
+ nr_turn_stun_ctx *ctx = (nr_turn_stun_ctx *)arg;
+
+ ctx->last_error_code = ctx->stun->error_code;
+
+ switch (ctx->stun->state) {
+ case NR_STUN_CLIENT_STATE_DONE:
+ /* Save the realm and nonce */
+ if (ctx->stun->realm && (!ctx->tctx->realm || strcmp(ctx->stun->realm,
+ ctx->tctx->realm))) {
+ RFREE(ctx->tctx->realm);
+ ctx->tctx->realm = r_strdup(ctx->stun->realm);
+ if (!ctx->tctx->realm)
+ ABORT(R_NO_MEMORY);
+ }
+ if (ctx->stun->nonce && (!ctx->tctx->nonce || strcmp(ctx->stun->nonce,
+ ctx->tctx->nonce))) {
+ RFREE(ctx->tctx->nonce);
+ ctx->tctx->nonce = r_strdup(ctx->stun->nonce);
+ if (!ctx->tctx->nonce)
+ ABORT(R_NO_MEMORY);
+ }
+
+ ctx->retry_ct=0;
+ ctx->success_cb(0, 0, ctx);
+ break;
+
+ case NR_STUN_CLIENT_STATE_FAILED:
+ /* Special case: if this is an authentication error,
+ we retry once. This allows the 401/438 nonce retry
+ paradigm. After that, we fail */
+ /* TODO(ekr@rtfm.com): 401 needs a #define */
+ /* TODO(ekr@rtfm.com): Add alternate-server (Mozilla bug 857688) */
+ if (ctx->stun->error_code == 438) {
+ // track 438s for ice telemetry
+ nr_accumulate_count(&(ctx->tctx->cnt_438s), 1);
+ }
+ if (ctx->stun->error_code == 401 || ctx->stun->error_code == 438) {
+ if (ctx->retry_ct > 0) {
+ if (ctx->stun->error_code == 401) {
+ // track 401s for ice telemetry
+ nr_accumulate_count(&(ctx->tctx->cnt_401s), 1);
+ }
+ r_log(NR_LOG_TURN, LOG_WARNING, "TURN(%s): Exceeded the number of retries", ctx->tctx->label);
+ ABORT(R_FAILED);
+ }
+
+ if (!ctx->stun->nonce) {
+ r_log(NR_LOG_TURN, LOG_WARNING, "TURN(%s): 401 but no nonce", ctx->tctx->label);
+ ABORT(R_FAILED);
+ }
+ if (!ctx->stun->realm) {
+ r_log(NR_LOG_TURN, LOG_WARNING, "TURN(%s): 401 but no realm", ctx->tctx->label);
+ ABORT(R_FAILED);
+ }
+
+ /* Try to retry */
+ if ((r=nr_turn_stun_set_auth_params(ctx, ctx->stun->realm,
+ ctx->stun->nonce)))
+ ABORT(r);
+
+ ctx->stun->error_code = 0; /* Reset to avoid inf-looping */
+
+ if ((r=nr_turn_stun_ctx_start(ctx))) {
+ r_log(NR_LOG_TURN, LOG_ERR, "TURN(%s): Couldn't start STUN", ctx->tctx->label);
+ ABORT(r);
+ }
+
+ ctx->retry_ct++;
+ } else if (ctx->stun->error_code == 300) {
+ r_log(NR_LOG_TURN, LOG_INFO,
+ "TURN(%s): Redirect received, restarting TURN", ctx->tctx->label);
+ /* We don't limit this with a retry counter, we limit redirects by
+ * checking whether we've tried the ALTERNATE-SERVER yet. */
+ ctx->retry_ct = 0;
+ if ((r = nr_turn_stun_ctx_handle_redirect(ctx))) {
+ ABORT(r);
+ }
+ } else {
+ ABORT(R_FAILED);
+ }
+ break;
+
+ case NR_STUN_CLIENT_STATE_TIMED_OUT:
+ ABORT(R_FAILED);
+ break;
+
+ case NR_STUN_CLIENT_STATE_CANCELLED:
+ assert(0); /* Shouldn't happen */
+ return;
+ break;
+
+ default:
+ assert(0); /* Shouldn't happen */
+ return;
+ }
+
+ _status=0;
+abort:
+ if (_status) {
+ ctx->error_cb(0, 0, ctx);
+ }
+}
+
+/* nr_turn_client_ctx functions */
+int nr_turn_client_ctx_create(const char* label, nr_socket* sock,
+ const char* username, Data* password,
+ nr_transport_addr* addr, nr_ice_ctx* ice_ctx,
+ nr_turn_client_ctx** ctxp) {
+ nr_turn_client_ctx *ctx=0;
+ int r,_status;
+
+ if ((r=r_log_register("turn", &NR_LOG_TURN)))
+ ABORT(r);
+
+ if(!(ctx=RCALLOC(sizeof(nr_turn_client_ctx))))
+ ABORT(R_NO_MEMORY);
+
+ STAILQ_INIT(&ctx->stun_ctxs);
+ STAILQ_INIT(&ctx->permissions);
+
+ if(!(ctx->label=r_strdup(label)))
+ ABORT(R_NO_MEMORY);
+
+ ctx->sock=sock;
+ ctx->username = r_strdup(username);
+ if (!ctx->username)
+ ABORT(R_NO_MEMORY);
+
+ if ((r=r_data_create(&ctx->password, password->data, password->len)))
+ ABORT(r);
+ if ((r=nr_transport_addr_copy(&ctx->turn_server_addr, addr)))
+ ABORT(r);
+
+ ctx->state = NR_TURN_CLIENT_STATE_INITTED;
+ if (addr->protocol == IPPROTO_TCP) {
+ if ((r=nr_socket_connect(ctx->sock, &ctx->turn_server_addr))) {
+ if (r != R_WOULDBLOCK)
+ ABORT(r);
+ }
+ }
+
+ ctx->ctx = ice_ctx;
+
+ *ctxp=ctx;
+
+ _status=0;
+abort:
+ if(_status){
+ nr_turn_client_ctx_destroy(&ctx);
+ }
+ return(_status);
+}
+
+int
+nr_turn_client_ctx_destroy(nr_turn_client_ctx **ctxp)
+{
+ nr_turn_client_ctx *ctx;
+
+ if(!ctxp || !*ctxp)
+ return(0);
+
+ ctx=*ctxp;
+ *ctxp = 0;
+
+ if (ctx->label)
+ r_log(NR_LOG_TURN, LOG_DEBUG, "TURN(%s): destroy", ctx->label);
+
+ nr_turn_client_deallocate(ctx);
+
+ /* Cancel frees the rest of our data */
+ RFREE(ctx->label);
+ ctx->label = 0;
+
+ nr_turn_client_cancel(ctx);
+
+ RFREE(ctx->username);
+ ctx->username = 0;
+ r_data_destroy(&ctx->password);
+ RFREE(ctx->nonce);
+ ctx->nonce = 0;
+ RFREE(ctx->realm);
+ ctx->realm = 0;
+
+ /* Destroy the STUN client ctxs */
+ while (!STAILQ_EMPTY(&ctx->stun_ctxs)) {
+ nr_turn_stun_ctx *stun = STAILQ_FIRST(&ctx->stun_ctxs);
+ STAILQ_REMOVE_HEAD(&ctx->stun_ctxs, entry);
+ nr_turn_stun_ctx_destroy(&stun);
+ }
+
+ /* Destroy the permissions */
+ while (!STAILQ_EMPTY(&ctx->permissions)) {
+ nr_turn_permission *perm = STAILQ_FIRST(&ctx->permissions);
+ STAILQ_REMOVE_HEAD(&ctx->permissions, entry);
+ nr_turn_permission_destroy(&perm);
+ }
+
+ RFREE(ctx);
+
+ return(0);
+}
+
+int nr_turn_client_cancel(nr_turn_client_ctx *ctx)
+{
+ nr_turn_stun_ctx *stun = 0;
+
+ if (ctx->state == NR_TURN_CLIENT_STATE_CANCELLED ||
+ ctx->state == NR_TURN_CLIENT_STATE_FAILED)
+ return(0);
+
+ if (ctx->label)
+ r_log(NR_LOG_TURN, LOG_INFO, "TURN(%s): cancelling", ctx->label);
+
+ /* Cancel the STUN client ctxs */
+ stun = STAILQ_FIRST(&ctx->stun_ctxs);
+ while (stun) {
+ nr_stun_client_cancel(stun->stun);
+ stun = STAILQ_NEXT(stun, entry);
+ }
+
+ /* Cancel the timers, if not already cancelled */
+ NR_async_timer_cancel(ctx->connected_timer_handle);
+ NR_async_timer_cancel(ctx->refresh_timer_handle);
+
+ ctx->state = NR_TURN_CLIENT_STATE_CANCELLED;
+
+ return(0);
+}
+
+int nr_turn_client_send_stun_request(nr_turn_client_ctx *ctx,
+ nr_stun_message *req,
+ int flags)
+{
+ int r,_status;
+
+ if ((r=nr_stun_encode_message(req)))
+ ABORT(r);
+
+ if ((r=nr_socket_sendto(ctx->sock,
+ req->buffer, req->length, flags,
+ &ctx->turn_server_addr))) {
+ r_log(NR_LOG_TURN, LOG_WARNING, "TURN(%s): Failed sending request",
+ ctx->label);
+ ABORT(r);
+ }
+
+ _status=0;
+abort:
+ return(_status);
+}
+
+int nr_turn_client_deallocate(nr_turn_client_ctx *ctx)
+{
+ int r,_status;
+ nr_stun_message *aloc = 0;
+ nr_stun_client_auth_params auth;
+ nr_stun_client_refresh_request_params refresh;
+
+ if (ctx->state != NR_TURN_CLIENT_STATE_ALLOCATED)
+ return(0);
+
+ r_log(NR_LOG_TURN, LOG_INFO, "TURN(%s): deallocating", ctx->label);
+
+ refresh.lifetime_secs = 0;
+
+ auth.username = ctx->username;
+ INIT_DATA(auth.password, ctx->password->data, ctx->password->len);
+
+ auth.realm = ctx->realm;
+ auth.nonce = ctx->nonce;
+
+ auth.authenticate = 1;
+
+ if ((r=nr_stun_build_refresh_request(&auth, &refresh, &aloc)))
+ ABORT(r);
+
+ // We are only sending a single request here because we are in the process of
+ // shutting everything down. Theoretically we should probably start a seperate
+ // STUN transaction which outlives the TURN context.
+ if ((r=nr_turn_client_send_stun_request(ctx, aloc, 0)))
+ ABORT(r);
+
+ ctx->state = NR_TURN_CLIENT_STATE_DEALLOCATING;
+
+ _status=0;
+abort:
+ nr_stun_message_destroy(&aloc);
+ return(_status);
+}
+
+static void nr_turn_client_fire_finished_cb(nr_turn_client_ctx *ctx)
+ {
+ if (ctx->finished_cb) {
+ NR_async_cb finished_cb=ctx->finished_cb;
+ ctx->finished_cb=0;
+ finished_cb(0, 0, ctx->cb_arg);
+ }
+ }
+
+int nr_turn_client_failed(nr_turn_client_ctx *ctx)
+{
+ if (ctx->state == NR_TURN_CLIENT_STATE_FAILED ||
+ ctx->state == NR_TURN_CLIENT_STATE_CANCELLED)
+ return(0);
+
+ r_log(NR_LOG_TURN, LOG_WARNING, "TURN(%s) failed", ctx->label);
+ nr_turn_client_cancel(ctx);
+ ctx->state = NR_TURN_CLIENT_STATE_FAILED;
+ nr_turn_client_fire_finished_cb(ctx);
+
+ return(0);
+}
+
+int nr_turn_client_get_relayed_address(nr_turn_client_ctx *ctx,
+ nr_transport_addr *relayed_address)
+{
+ int r, _status;
+
+ if (ctx->state != NR_TURN_CLIENT_STATE_ALLOCATED)
+ ABORT(R_FAILED);
+
+ if (r=nr_transport_addr_copy(relayed_address, &ctx->relay_addr))
+ ABORT(r);
+
+ _status=0;
+abort:
+ return(_status);
+}
+
+int nr_turn_client_get_mapped_address(nr_turn_client_ctx *ctx,
+ nr_transport_addr *mapped_address)
+{
+ int r, _status;
+
+ if (ctx->state != NR_TURN_CLIENT_STATE_ALLOCATED)
+ ABORT(R_FAILED);
+
+ if (r=nr_transport_addr_copy(mapped_address, &ctx->mapped_addr))
+ ABORT(r);
+
+ _status=0;
+abort:
+ return(_status);
+}
+
+static void nr_turn_client_allocate_cb(NR_SOCKET s, int how, void *arg)
+{
+ nr_turn_stun_ctx *ctx = (nr_turn_stun_ctx *)arg;
+ nr_turn_stun_ctx *refresh_ctx;
+ int r,_status;
+
+ ctx->tctx->state = NR_TURN_CLIENT_STATE_ALLOCATED;
+
+ if ((r=nr_transport_addr_copy(
+ &ctx->tctx->relay_addr,
+ &ctx->stun->results.allocate_response.relay_addr)))
+ ABORT(r);
+
+ if ((r=nr_transport_addr_copy(
+ &ctx->tctx->mapped_addr,
+ &ctx->stun->results.allocate_response.mapped_addr)))
+ ABORT(r);
+
+ if ((r=nr_turn_client_refresh_setup(ctx->tctx, &refresh_ctx)))
+ ABORT(r);
+
+ if ((r=nr_turn_client_start_refresh_timer(
+ ctx->tctx, refresh_ctx,
+ ctx->stun->results.allocate_response.lifetime_secs)))
+ ABORT(r);
+
+ r_log(NR_LOG_TURN, LOG_INFO,
+ "TURN(%s): Succesfully allocated addr %s lifetime=%u",
+ ctx->tctx->label,
+ ctx->tctx->relay_addr.as_string,
+ ctx->stun->results.allocate_response.lifetime_secs);
+
+ nr_turn_client_fire_finished_cb(ctx->tctx);
+ _status=0;
+abort:
+ if (_status) {
+ nr_turn_client_failed(ctx->tctx);
+ }
+}
+
+static void nr_turn_client_error_cb(NR_SOCKET s, int how, void *arg)
+{
+ nr_turn_stun_ctx *ctx = (nr_turn_stun_ctx *)arg;
+
+ r_log(NR_LOG_TURN, LOG_WARNING, "TURN(%s): mode %d, %s",
+ ctx->tctx->label, ctx->mode, __FUNCTION__);
+
+ nr_turn_client_failed(ctx->tctx);
+}
+
+static void nr_turn_client_permission_error_cb(NR_SOCKET s, int how, void *arg)
+{
+ nr_turn_stun_ctx *ctx = (nr_turn_stun_ctx *)arg;
+
+ if (ctx->last_error_code == 403) {
+ // track 403s for ice telemetry
+ nr_accumulate_count(&(ctx->tctx->cnt_403s), 1);
+ r_log(NR_LOG_TURN, LOG_WARNING, "TURN(%s): mode %d, permission denied",
+ ctx->tctx->label, ctx->mode);
+
+ } else{
+ nr_turn_client_error_cb(0, 0, ctx);
+ }
+}
+
+int nr_turn_client_allocate(nr_turn_client_ctx *ctx,
+ NR_async_cb finished_cb, void *cb_arg)
+{
+ nr_turn_stun_ctx *stun = 0;
+ int r,_status;
+
+ if(ctx->state == NR_TURN_CLIENT_STATE_FAILED ||
+ ctx->state == NR_TURN_CLIENT_STATE_CANCELLED){
+ /* TURN TCP contexts can fail before we ever try to form an allocation,
+ * since the TCP connection can fail. It is also conceivable that a TURN
+ * TCP context could be cancelled before we are done forming all
+ * allocations (although we do not do this at the time this code was
+ * written) */
+ assert(ctx->turn_server_addr.protocol == IPPROTO_TCP);
+ ABORT(R_NOT_FOUND);
+ }
+
+ assert(ctx->state == NR_TURN_CLIENT_STATE_INITTED);
+
+ ctx->finished_cb=finished_cb;
+ ctx->cb_arg=cb_arg;
+
+ if ((r=nr_turn_stun_ctx_create(ctx, NR_TURN_CLIENT_MODE_ALLOCATE_REQUEST,
+ nr_turn_client_allocate_cb,
+ nr_turn_client_error_cb,
+ &stun)))
+ ABORT(r);
+ stun->stun->params.allocate_request.lifetime_secs =
+ TURN_LIFETIME_REQUEST_SECONDS;
+
+ if (ctx->state == NR_TURN_CLIENT_STATE_INITTED) {
+ if ((r=nr_turn_stun_ctx_start(stun)))
+ ABORT(r);
+ ctx->state = NR_TURN_CLIENT_STATE_ALLOCATING;
+ } else {
+ ABORT(R_ALREADY);
+ }
+
+ _status=0;
+abort:
+ if (_status) {
+ nr_turn_client_failed(ctx);
+ }
+
+ return(_status);
+}
+
+int nr_turn_client_process_response(nr_turn_client_ctx *ctx,
+ UCHAR *msg, int len,
+ nr_transport_addr *turn_server_addr)
+{
+ int r, _status;
+ nr_turn_stun_ctx *sc1;
+
+ switch (ctx->state) {
+ case NR_TURN_CLIENT_STATE_ALLOCATING:
+ case NR_TURN_CLIENT_STATE_ALLOCATED:
+ break;
+ default:
+ ABORT(R_FAILED);
+ }
+
+ sc1 = STAILQ_FIRST(&ctx->stun_ctxs);
+ while (sc1) {
+ r = nr_stun_client_process_response(sc1->stun, msg, len, turn_server_addr);
+ if (!r)
+ break;
+ if (r==R_RETRY) /* Likely a 401 and we will retry */
+ break;
+ if (r != R_REJECTED)
+ ABORT(r);
+ sc1 = STAILQ_NEXT(sc1, entry);
+ }
+ if (!sc1)
+ ABORT(R_REJECTED);
+
+ _status=0;
+abort:
+ return(_status);
+}
+
+static int nr_turn_client_refresh_setup(nr_turn_client_ctx *ctx,
+ nr_turn_stun_ctx **sctx)
+{
+ nr_turn_stun_ctx *stun = 0;
+ int r,_status;
+
+ assert(ctx->state == NR_TURN_CLIENT_STATE_ALLOCATED);
+ if (ctx->state != NR_TURN_CLIENT_STATE_ALLOCATED)
+ ABORT(R_NOT_PERMITTED);
+
+ if ((r=nr_turn_stun_ctx_create(ctx, NR_TURN_CLIENT_MODE_REFRESH_REQUEST,
+ nr_turn_client_refresh_cb,
+ nr_turn_client_error_cb,
+ &stun)))
+ ABORT(r);
+
+ if ((r=nr_turn_stun_set_auth_params(stun, ctx->realm, ctx->nonce)))
+ ABORT(r);
+
+ stun->stun->params.refresh_request.lifetime_secs =
+ TURN_LIFETIME_REQUEST_SECONDS;
+
+
+ *sctx=stun;
+
+ _status=0;
+abort:
+ return(_status);
+}
+
+static int nr_turn_client_start_refresh_timer(nr_turn_client_ctx *tctx,
+ nr_turn_stun_ctx *sctx,
+ UINT4 lifetime)
+{
+ int _status;
+
+ assert(!tctx->refresh_timer_handle);
+
+ if (lifetime <= TURN_REFRESH_SLACK_SECONDS) {
+ r_log(NR_LOG_TURN, LOG_ERR, "Too short lifetime specified for turn %u", lifetime);
+ ABORT(R_BAD_DATA);
+ }
+
+ if (lifetime > 3600)
+ lifetime = 3600;
+
+ lifetime -= TURN_REFRESH_SLACK_SECONDS;
+
+ r_log(NR_LOG_TURN, LOG_DEBUG, "TURN(%s): Setting refresh timer for %u seconds",
+ tctx->label, lifetime);
+ NR_ASYNC_TIMER_SET(lifetime * 1000, nr_turn_client_refresh_timer_cb, sctx,
+ &tctx->refresh_timer_handle);
+
+ _status=0;
+abort:
+ if (_status) {
+ nr_turn_client_failed(tctx);
+ }
+ return _status;
+}
+
+static void nr_turn_client_refresh_timer_cb(NR_SOCKET s, int how, void *arg)
+{
+ nr_turn_stun_ctx *ctx = (nr_turn_stun_ctx *)arg;
+ int r,_status;
+
+ r_log(NR_LOG_TURN, LOG_DEBUG, "TURN(%s): Refresh timer fired",
+ ctx->tctx->label);
+
+ ctx->tctx->refresh_timer_handle=0;
+ if ((r=nr_turn_stun_ctx_start(ctx))) {
+ ABORT(r);
+ }
+
+ _status=0;
+abort:
+ if (_status) {
+ nr_turn_client_failed(ctx->tctx);
+ }
+ return;
+}
+
+static void nr_turn_client_refresh_cb(NR_SOCKET s, int how, void *arg)
+{
+ int r, _status;
+ nr_turn_stun_ctx *ctx = (nr_turn_stun_ctx *)arg;
+ /* Save lifetime from the reset */
+ UINT4 lifetime = ctx->stun->results.refresh_response.lifetime_secs;
+
+ r_log(NR_LOG_TURN, LOG_DEBUG, "TURN(%s): Refresh succeeded. lifetime=%u",
+ ctx->tctx->label, lifetime);
+
+ if ((r=nr_turn_client_start_refresh_timer(
+ ctx->tctx, ctx, lifetime)))
+ ABORT(r);
+
+ _status=0;
+
+abort:
+ if (_status) {
+ nr_turn_client_failed(ctx->tctx);
+ }
+}
+
+/* TODO(ekr@rtfm.com): We currently don't support channels.
+ We might in the future. Mozilla bug 857736 */
+int nr_turn_client_send_indication(nr_turn_client_ctx *ctx,
+ const UCHAR *msg, size_t len,
+ int flags, const nr_transport_addr *remote_addr)
+{
+ int r,_status;
+ nr_stun_client_send_indication_params params = { { 0 } };
+ nr_stun_message *ind = 0;
+
+ if (ctx->state != NR_TURN_CLIENT_STATE_ALLOCATED)
+ ABORT(R_FAILED);
+
+ r_log(NR_LOG_TURN, LOG_DEBUG, "TURN(%s): Send indication len=%zu",
+ ctx->label, len);
+
+ if ((r=nr_turn_client_ensure_perm(ctx, remote_addr)))
+ ABORT(r);
+
+ if ((r=nr_transport_addr_copy(&params.remote_addr, remote_addr)))
+ ABORT(r);
+
+ params.data.data = (UCHAR*)msg;
+ params.data.len = len;
+
+ if ((r=nr_stun_build_send_indication(&params, &ind)))
+ ABORT(r);
+
+ if ((r=nr_turn_client_send_stun_request(ctx, ind, flags)))
+ ABORT(r);
+
+ _status=0;
+abort:
+ nr_stun_message_destroy(&ind);
+ return(_status);
+}
+
+int nr_turn_client_parse_data_indication(nr_turn_client_ctx *ctx,
+ nr_transport_addr *source_addr,
+ UCHAR *msg, size_t len,
+ UCHAR *newmsg, size_t *newlen,
+ size_t newsize,
+ nr_transport_addr *remote_addr)
+{
+ int r,_status;
+ nr_stun_message *ind=0;
+ nr_stun_message_attribute *attr;
+ nr_turn_permission *perm;
+
+ if (nr_transport_addr_cmp(&ctx->turn_server_addr, source_addr,
+ NR_TRANSPORT_ADDR_CMP_MODE_ALL)) {
+ r_log(NR_LOG_TURN, LOG_WARNING,
+ "TURN(%s): Indication from unexpected source addr %s (expected %s)",
+ ctx->label, source_addr->as_string, ctx->turn_server_addr.as_string);
+ ABORT(R_REJECTED);
+ }
+
+ if ((r=nr_stun_message_create2(&ind, msg, len)))
+ ABORT(r);
+ if ((r=nr_stun_decode_message(ind, 0, 0)))
+ ABORT(r);
+
+ if (ind->header.type != NR_STUN_MSG_DATA_INDICATION)
+ ABORT(R_BAD_ARGS);
+
+ if (!nr_stun_message_has_attribute(ind, NR_STUN_ATTR_XOR_PEER_ADDRESS, &attr))
+ ABORT(R_BAD_ARGS);
+
+ if ((r=nr_turn_permission_find(ctx, &attr->u.xor_mapped_address.unmasked,
+ &perm))) {
+ if (r == R_NOT_FOUND) {
+ r_log(NR_LOG_TURN, LOG_WARNING,
+ "TURN(%s): Indication from peer addr %s with no permission",
+ ctx->label, attr->u.xor_mapped_address.unmasked.as_string);
+ }
+ ABORT(r);
+ }
+
+ if ((r=nr_transport_addr_copy(remote_addr,
+ &attr->u.xor_mapped_address.unmasked)))
+ ABORT(r);
+
+#if REFRESH_RESERVATION_ON_RECV
+ if ((r=nr_turn_client_ensure_perm(ctx, remote_addr))) {
+ ABORT(r);
+ }
+#endif
+
+ if (!nr_stun_message_has_attribute(ind, NR_STUN_ATTR_DATA, &attr)) {
+ ABORT(R_BAD_DATA);
+ }
+
+ assert(newsize >= attr->u.data.length);
+ if (newsize < attr->u.data.length)
+ ABORT(R_BAD_ARGS);
+
+ memcpy(newmsg, attr->u.data.data, attr->u.data.length);
+ *newlen = attr->u.data.length;
+
+ _status=0;
+abort:
+ nr_stun_message_destroy(&ind);
+ return(_status);
+}
+
+
+
+/* The permissions model is as follows:
+
+ - We keep a list of all the permissions we have ever requested
+ along with when they were last established.
+ - Whenever someone sends a packet, we automatically create/
+ refresh the permission.
+
+ This means that permissions automatically time out if
+ unused.
+
+*/
+int nr_turn_client_ensure_perm(nr_turn_client_ctx *ctx, const nr_transport_addr *addr)
+{
+ int r, _status;
+ nr_turn_permission *perm = 0;
+ UINT8 now;
+ UINT8 turn_permission_refresh = (TURN_PERMISSION_LIFETIME_SECONDS -
+ TURN_REFRESH_SLACK_SECONDS) * TURN_USECS_PER_S;
+
+ if ((r=nr_turn_permission_find(ctx, addr, &perm))) {
+ if (r == R_NOT_FOUND) {
+ if ((r=nr_turn_permission_create(ctx, addr, &perm)))
+ ABORT(r);
+ }
+ else {
+ ABORT(r);
+ }
+ }
+
+ assert(perm);
+
+ /* Now check that the permission is up-to-date */
+ now = r_gettimeint();
+
+ if ((now - perm->last_used) > turn_permission_refresh) {
+ r_log(NR_LOG_TURN, LOG_DEBUG, "TURN(%s): Permission for %s requires refresh",
+ ctx->label, perm->addr.as_string);
+
+ if ((r=nr_turn_stun_ctx_start(perm->stun)))
+ ABORT(r);
+
+ perm->last_used = now; /* Update the time now so we don't retry on
+ next packet */
+ }
+
+ _status=0;
+abort:
+ return(_status);
+}
+
+static int nr_turn_permission_create(nr_turn_client_ctx *ctx, const nr_transport_addr *addr,
+ nr_turn_permission **permp)
+{
+ int r, _status;
+ nr_turn_permission *perm = 0;
+
+ assert(ctx->state == NR_TURN_CLIENT_STATE_ALLOCATED);
+
+ r_log(NR_LOG_TURN, LOG_INFO, "TURN(%s): Creating permission for %s",
+ ctx->label, addr->as_string);
+
+ if (!(perm = RCALLOC(sizeof(nr_turn_permission))))
+ ABORT(R_NO_MEMORY);
+
+ if ((r=nr_transport_addr_copy(&perm->addr, addr)))
+ ABORT(r);
+
+ perm->last_used = 0;
+
+ if ((r=nr_turn_stun_ctx_create(ctx, NR_TURN_CLIENT_MODE_PERMISSION_REQUEST,
+ nr_turn_client_permissions_cb,
+ nr_turn_client_permission_error_cb,
+ &perm->stun)))
+ ABORT(r);
+
+ /* We want to authenticate on the first packet */
+ if ((r=nr_turn_stun_set_auth_params(perm->stun, ctx->realm, ctx->nonce)))
+ ABORT(r);
+
+ if ((r=nr_transport_addr_copy(
+ &perm->stun->stun->params.permission_request.remote_addr, addr)))
+ ABORT(r);
+ STAILQ_INSERT_TAIL(&ctx->permissions, perm, entry);
+
+ *permp = perm;
+
+ _status=0;
+abort:
+ if (_status) {
+ nr_turn_permission_destroy(&perm);
+ }
+ return(_status);
+}
+
+
+static int nr_turn_permission_find(nr_turn_client_ctx *ctx, const nr_transport_addr *addr,
+ nr_turn_permission **permp)
+{
+ nr_turn_permission *perm;
+ int _status;
+
+ perm = STAILQ_FIRST(&ctx->permissions);
+ while (perm) {
+ if (!nr_transport_addr_cmp(&perm->addr, addr,
+ NR_TRANSPORT_ADDR_CMP_MODE_ADDR))
+ break;
+
+ perm = STAILQ_NEXT(perm, entry);
+ }
+
+ if (!perm) {
+ ABORT(R_NOT_FOUND);
+ }
+ if (perm->stun->last_error_code == 403) {
+ ABORT(R_NOT_PERMITTED);
+ }
+ *permp = perm;
+
+ _status=0;
+abort:
+ return(_status);
+}
+
+static void nr_turn_client_permissions_cb(NR_SOCKET s, int how, void *arg)
+{
+ nr_turn_stun_ctx *ctx = (nr_turn_stun_ctx *)arg;
+ r_log(NR_LOG_TURN, LOG_DEBUG, "TURN(%s): Successfully refreshed permission",
+ ctx->tctx->label);
+}
+
+/* Note that we don't destroy the nr_turn_stun_ctx. That is owned by the
+ nr_turn_client_ctx. */
+static int nr_turn_permission_destroy(nr_turn_permission **permp)
+{
+ nr_turn_permission *perm;
+
+ if (!permp || !*permp)
+ return(0);
+
+ perm = *permp;
+ *permp = 0;
+
+ RFREE(perm);
+
+ return(0);
+}
+
+#endif /* USE_TURN */
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/stun/turn_client_ctx.h b/dom/media/webrtc/transport/third_party/nICEr/src/stun/turn_client_ctx.h
new file mode 100644
index 0000000000..2049f1bdfb
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/stun/turn_client_ctx.h
@@ -0,0 +1,161 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+Copyright (c) 2013, Mozilla
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance, Mozilla nor
+ the names of its contributors may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+
+#ifndef _turn_client_ctx_h
+#define _turn_client_ctx_h
+
+struct nr_ice_ctx_;
+
+typedef struct nr_transport_addr_listnode_ {
+ nr_transport_addr value;
+ STAILQ_ENTRY(nr_transport_addr_listnode_) entry;
+} nr_transport_addr_listnode;
+typedef STAILQ_HEAD(nr_transport_addr_listnode_head_, nr_transport_addr_listnode_) nr_transport_addr_listnode_head;
+
+/*
+ Represents a single set of STUN transactions, i.e.,
+ Allocate, Refresh, Permission. It automatically handles
+ permissions and restarts.
+ */
+typedef struct nr_turn_stun_ctx_ {
+ struct nr_turn_client_ctx_ *tctx;
+ int mode; /* From stun_client_ctx.h, NR_TURN_CLIENT_MODE_* */
+ int retry_ct;
+ nr_stun_client_ctx *stun;
+ char *nonce;
+ char *realm;
+ NR_async_cb success_cb;
+ NR_async_cb error_cb;
+ int last_error_code;
+
+ nr_transport_addr_listnode_head addresses_tried;
+
+ STAILQ_ENTRY(nr_turn_stun_ctx_) entry;
+} nr_turn_stun_ctx;
+typedef STAILQ_HEAD(nr_turn_stun_ctx_head_, nr_turn_stun_ctx_)
+ nr_turn_stun_ctx_head;
+
+/* Represents a single TURN permission */
+typedef struct nr_turn_permission_ {
+ nr_transport_addr addr;
+ nr_turn_stun_ctx *stun;
+ UINT8 last_used;
+
+ STAILQ_ENTRY(nr_turn_permission_) entry;
+} nr_turn_permission;
+typedef STAILQ_HEAD(nr_turn_permission_head_, nr_turn_permission_)
+ nr_turn_permission_head;
+
+/* A single connection to a TURN server. Use one
+ turn_client_ctx per socket/server pair. */
+typedef struct nr_turn_client_ctx_ {
+ int state;
+#define NR_TURN_CLIENT_STATE_INITTED 1
+#define NR_TURN_CLIENT_STATE_ALLOCATING 2
+#define NR_TURN_CLIENT_STATE_ALLOCATED 3
+#define NR_TURN_CLIENT_STATE_FAILED 4
+#define NR_TURN_CLIENT_STATE_CANCELLED 5
+#define NR_TURN_CLIENT_STATE_DEALLOCATING 6
+
+ char *label;
+ nr_socket *sock;
+
+ char *username;
+ Data *password;
+ char *nonce;
+ char *realm;
+
+ nr_transport_addr turn_server_addr;
+ nr_transport_addr relay_addr;
+ nr_transport_addr mapped_addr;
+
+ nr_turn_stun_ctx_head stun_ctxs;
+ nr_turn_permission_head permissions;
+
+ /* We need access to the socket factory to create new TCP sockets for handling
+ * STUN/300 responses. */
+ /* If we were to require TCP nr_sockets to allow multiple connect calls by
+ * disconnecting and re-connecting, we could avoid this requirement. */
+ struct nr_ice_ctx_* ctx;
+
+ NR_async_cb finished_cb;
+ void *cb_arg;
+
+ void *connected_timer_handle;
+ void *refresh_timer_handle;
+
+ // ice telemetry
+ UINT2 cnt_401s;
+ UINT2 cnt_403s;
+ UINT2 cnt_438s;
+} nr_turn_client_ctx;
+
+extern int NR_LOG_TURN;
+
+int nr_transport_addr_listnode_create(const nr_transport_addr *addr, nr_transport_addr_listnode **listnodep);
+void nr_transport_addr_listnode_destroy(nr_transport_addr_listnode **listnode);
+int nr_turn_client_ctx_create(const char* label, nr_socket* sock,
+ const char* username, Data* password,
+ nr_transport_addr* addr,
+ struct nr_ice_ctx_* ice_ctx,
+ nr_turn_client_ctx** ctxp);
+int nr_turn_client_ctx_destroy(nr_turn_client_ctx **ctxp);
+int nr_turn_client_allocate(nr_turn_client_ctx *ctx,
+ NR_async_cb finished_cb, void *cb_arg);
+int nr_turn_client_get_relayed_address(nr_turn_client_ctx *ctx,
+ nr_transport_addr *relayed_address);
+int nr_turn_client_get_mapped_address(nr_turn_client_ctx *ctx,
+ nr_transport_addr *mapped_address);
+int nr_turn_client_process_response(nr_turn_client_ctx *ctx,
+ UCHAR *msg, int len,
+ nr_transport_addr *turn_server_addr);
+int nr_turn_client_cancel(nr_turn_client_ctx *ctx);
+int nr_turn_client_failed(nr_turn_client_ctx *ctx);
+int nr_turn_client_deallocate(nr_turn_client_ctx *ctx);
+int nr_turn_client_send_indication(nr_turn_client_ctx *ctx,
+ const UCHAR *msg, size_t len,
+ int flags, const nr_transport_addr *remote_addr);
+int nr_turn_client_parse_data_indication(nr_turn_client_ctx *ctx,
+ nr_transport_addr *source_addr,
+ UCHAR *msg, size_t len,
+ UCHAR *newmsg, size_t *newlen,
+ size_t newsize,
+ nr_transport_addr *remote_addr);
+int nr_turn_client_ensure_perm(nr_turn_client_ctx *ctx,
+ const nr_transport_addr *addr);
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/util/cb_args.c b/dom/media/webrtc/transport/third_party/nICEr/src/util/cb_args.c
new file mode 100644
index 0000000000..81f7731d74
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/util/cb_args.c
@@ -0,0 +1,57 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <stdarg.h>
+#include "nr_api.h"
+#include "cb_args.h"
+
+void **nr_pack_cb_args(int ct,...)
+ {
+ void **vlist;
+ va_list ap;
+ int i;
+
+ va_start(ap,ct);
+ if(!(vlist=RCALLOC(sizeof(void *)*ct+1)))
+ abort();
+
+ for(i=0;i<ct;i++){
+ vlist[i]=va_arg(ap, void *);
+ }
+
+ va_end(ap);
+
+ return(vlist);
+ }
+
+
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/util/cb_args.h b/dom/media/webrtc/transport/third_party/nICEr/src/util/cb_args.h
new file mode 100644
index 0000000000..83f7831ccc
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/util/cb_args.h
@@ -0,0 +1,41 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+
+#ifndef _cb_args_h
+#define _cb_args_h
+
+void **nr_pack_cb_args(int ct,...);
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/util/ice_util.c b/dom/media/webrtc/transport/third_party/nICEr/src/util/ice_util.c
new file mode 100644
index 0000000000..249cfe350f
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/util/ice_util.c
@@ -0,0 +1,71 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <stdarg.h>
+#include <string.h>
+#include "nr_api.h"
+#include "ice_util.h"
+
+int nr_concat_strings(char **outp,...)
+ {
+ va_list ap;
+ char *s,*out=0;
+ int len=0;
+ int _status;
+
+ va_start(ap,outp);
+ while(s=va_arg(ap,char *)){
+ len+=strlen(s);
+ }
+ va_end(ap);
+
+
+ if(!(out=RMALLOC(len+1)))
+ ABORT(R_NO_MEMORY);
+
+ *outp=out;
+
+ va_start(ap,outp);
+ while(s=va_arg(ap,char *)){
+ len=strlen(s);
+ memcpy(out,s,len);
+ out+=len;
+ }
+ va_end(ap);
+
+ *out=0;
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
diff --git a/dom/media/webrtc/transport/third_party/nICEr/src/util/ice_util.h b/dom/media/webrtc/transport/third_party/nICEr/src/util/ice_util.h
new file mode 100644
index 0000000000..44751edaf8
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nICEr/src/util/ice_util.h
@@ -0,0 +1,41 @@
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+
+
+#ifndef _ice_util_h
+#define _ice_util_h
+
+int nr_concat_strings(char **outp,...);
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/COPYRIGHT b/dom/media/webrtc/transport/third_party/nrappkit/COPYRIGHT
new file mode 100644
index 0000000000..b0bb25595e
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/COPYRIGHT
@@ -0,0 +1,159 @@
+
+Copyright (C) 2006, Network Resonance, Inc.
+All Rights Reserved
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+
+
+This distribution also contains material from ssldump, tcpdump, and
+FreeBSD. The licenses are on the individual source files but follow
+here as well.
+
+SSLDUMP LICENSE
+Copyright (C) 1999-2001 RTFM, Inc.
+All Rights Reserved
+
+This package is a SSLv3/TLS protocol analyzer written by Eric Rescorla
+<ekr@rtfm.com> and licensed by RTFM, Inc.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+3. All advertising materials mentioning features or use of this software
+ must display the following acknowledgement:
+
+ This product includes software developed by Eric Rescorla for
+ RTFM, Inc.
+
+4. Neither the name of RTFM, Inc. nor the name of Eric Rescorla may be
+ used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE ERIC RESCORLA AND RTFM ``AS IS'' AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGE.
+
+TCPDUMP LICENSE
+The manual page for this software is partially excerpted from
+the tcpdump manual page, which is subject to the following license:
+Copyright (c) 1987, 1988, 1989, 1990, 1991, 1992, 1994, 1995, 1996, 1997
+ The Regents of the University of California. All rights reserved.
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that: (1) source code distributions
+retain the above copyright notice and this paragraph in its entirety, (2)
+distributions including binary code include the above copyright notice and
+this paragraph in its entirety in the documentation or other materials
+provided with the distribution, and (3) all advertising materials mentioning
+features or use of this software display the following acknowledgement:
+``This product includes software developed by the University of California,
+Lawrence Berkeley Laboratory and its contributors.'' Neither the name of
+the University nor the names of its contributors may be used to endorse
+or promote products derived from this software without specific prior
+written permission.
+THIS SOFTWARE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR IMPLIED
+WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
+
+
+The compilation of software known as FreeBSD is distributed under the
+following terms:
+
+Copyright (C) 1992-2004 The FreeBSD Project. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL AUTHOR OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGE.
+
+The 4.4BSD and 4.4BSD-Lite software is distributed under the following
+terms:
+
+All of the documentation and software included in the 4.4BSD and 4.4BSD-Lite
+Releases is copyrighted by The Regents of the University of California.
+
+Copyright 1979, 1980, 1983, 1986, 1988, 1989, 1991, 1992, 1993, 1994
+ The Regents of the University of California. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+3. All advertising materials mentioning features or use of this software
+ must display the following acknowledgement:
+This product includes software developed by the University of
+California, Berkeley and its contributors.
+4. Neither the name of the University nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGE.
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/README b/dom/media/webrtc/transport/third_party/nrappkit/README
new file mode 100644
index 0000000000..9cf43319b3
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/README
@@ -0,0 +1,133 @@
+$Id: README,v 1.3 2007/11/21 00:09:10 adamcain Exp $
+
+nrappkit 1.0b2
+Copyright (C) 2006 Network Resonance, Inc.
+
+
+nrappkit is a toolkit for building standalone applications and
+appliances. It provides:
+
+- registry-based configuration (with change callbacks)
+- extensible command and configuration shell
+- extensible statistics system
+- configurable logging system
+- event and timer handling
+- generic plugin system
+- launcher daemon
+
+The contents of nrappkit were extracted from Network Resonance's
+product on the theory that they were generally useful for
+application developers.
+
+THIS PACKAGE DOES NOT GRANT A LICENSE OR RIGHT TO ANY OTHER NETWORK
+RESONANCE TECHNOLOGY OR SOFTWARE.
+
+
+
+BUILDING
+
+Builds are done semi-manually with port directories for each
+platform. There are pre-existing ports to FreeBSD, Linux (Ubuntu
+and Fedora Core), and Darwin (MacOSX). To build the system:
+
+ cd src/make/<platform>
+ gmake
+
+Some of the platforms come in several variants. Most notably,
+if a platform exists in "regular" and "-appliance" variant,
+this means that the regular variant just builds binaries intended
+to be run out of the make directory (for development) and the
+appliance variant is intended to be installed in a real system.
+
+By default we want to install things owned as user "pcecap".
+Either make this user or edit the Makefile to be a user you
+like (e.g., nobody).
+
+If you want to include the 'nrsh' command-line configuration
+tool in your build, you will need to make sure the line
+ BUILD_NRSH=yes
+appears (uncommented-out) in your platform Makefile. You will
+also need to to build OpenSSL and libedit and point your nrappkit
+Makefile to the correct paths. You can obtain these packages at:
+ openssl-0.9.7l
+ http://www.openssl.org/source/openssl-0.9.7l.tar.gz
+
+ libedit-20060829-2.9
+ http://freshmeat.net/redir/editline/53029/url_tgz/libedit-20060829-2.9.tar.gz
+
+
+INSTALLING
+If you're doing an appliance as opposed to a development build,
+you'll want to install it. This is easy:
+
+ su
+ gmake install
+
+Most binaries and libraries ends up in /usr/local/pcecap while
+data files are in /var/pcecap. However, you can tweak
+this in the Makefile. By default it's all owned by pcecap.
+
+To ensure that dynamic libraries are loaded correctly at runtime,
+you'd want to make sure the right directory is included in your
+LD_LIBRARY_PATH or via ldconfig.
+
+
+QUICK TOUR
+The build makes the following binaries that you may find useful:
+
+- captured -- the launcher (the name is historical)
+- registryd -- the registry daemon
+- nrregctl -- a registry control program
+- nrsh -- the command shell (when included in build)
+- nrstatsctl -- the stats control program
+
+Using the nrcapctl script is the easiest way to interact with
+the applications. It is run as "nrcapctl <command>" with the
+following commands recognized:
+
+ startup -- fires up captured, which in turn runs and
+ initializes the registry
+
+ shutdown -- kills captured and its child processes
+
+ status -- prints the running status of captured in
+ human-readable form
+
+ stat -- prints the running status of captured in
+ a form easily parsed by scripts
+
+ enable -- alters the mode.txt file so that captured
+ starts
+
+ disable -- alters the mode.txt file so that captured
+ does not start
+
+ clear-statistics -- equivalent to "nrstatsctl -z" (requires
+ that captured be running)
+
+Note: the "start" and "stop" nrcapctl commands do nothing as they
+use components not included in nrappkit. However the associated
+script logic in nrcapctl demonstrates how additional applications
+might be launched using nrcapctl and particular registry settings.
+
+
+EXTENDING
+When things come up, they're pretty dumb. You'll probably want to
+write your own applications, otherwise it's not clear why you're doing
+this. The general idea is that you write your application using the
+facilities that nrappkit provides and then write plugins to the
+nrappkit components as necessary. So, for example, say you want
+to write a network daemon. You would:
+
+ - configure the launcher to launch your daemon (using the registry,
+ naturally).
+ - make calls to the registry to get configuration data
+ - make calls to the logging system to log data
+ - implement a stats module to record statistics
+ - write a plugin to nrsh to let people configure your parameters
+
+Examples of some of this stuff can be found in examples/demo_plugin.
+Otherwise, read the source. More documentation will be on the way,
+hopefully.
+
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/README_MOZILLA b/dom/media/webrtc/transport/third_party/nrappkit/README_MOZILLA
new file mode 100644
index 0000000000..f7cdb4cabb
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/README_MOZILLA
@@ -0,0 +1,21 @@
+This import of nrappkit is a subset of the distribution at:
+
+ nrappkit.sourceforge.net
+
+
+The last revision included in this import was on Nov 25, 2008, version 1.0b2.
+
+The upstream project is now dead (no update since 2013), we can conisder this
+version as a fork.
+
+Out of the list in the README, we use:
+
+- registry-based configuration (with change callbacks)
+ [but without the registry daemon]
+- configurable logging system
+- event and timer handling
+ [though partly reimplemented]
+
+Also, we use a bunch of the generic utilities such as string handling,
+generic hash tables in C, yet another concrete type mapping, etc.
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/nrappkit.gyp b/dom/media/webrtc/transport/third_party/nrappkit/nrappkit.gyp
new file mode 100644
index 0000000000..c3f88af4bc
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/nrappkit.gyp
@@ -0,0 +1,251 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+#
+# nrappkit.gyp
+#
+#
+{
+ 'targets' : [
+ {
+ 'target_name' : 'nrappkit',
+ 'type' : 'static_library',
+
+ 'include_dirs' : [
+ # EXTERNAL
+ # INTERNAL
+ 'src/event',
+ 'src/log',
+ 'src/port/generic/include',
+ 'src/registry',
+ 'src/share',
+ 'src/stats',
+ 'src/util',
+ 'src/util/libekr',
+ ],
+
+ 'sources' : [
+ # Shared
+# './src/share/nr_api.h',
+ './src/share/nr_common.h',
+# './src/share/nr_dynlib.h',
+ './src/share/nr_reg_keys.h',
+# './src/share/nr_startup.c',
+# './src/share/nr_startup.h',
+# './src/share/nrappkit_static_plugins.c',
+ './src/port/generic/include'
+
+ # libekr
+ './src/util/libekr/assoc.h',
+# './src/util/libekr/debug.c',
+# './src/util/libekr/debug.h',
+ './src/util/libekr/r_assoc.c',
+ './src/util/libekr/r_assoc.h',
+# './src/util/libekr/r_assoc_test.c',
+ './src/util/libekr/r_common.h',
+ './src/util/libekr/r_crc32.c',
+ './src/util/libekr/r_crc32.h',
+ './src/util/libekr/r_data.c',
+ './src/util/libekr/r_data.h',
+ './src/util/libekr/r_defaults.h',
+ './src/util/libekr/r_errors.c',
+ './src/util/libekr/r_errors.h',
+ './src/util/libekr/r_includes.h',
+# './src/util/libekr/r_list.c',
+# './src/util/libekr/r_list.h',
+ './src/util/libekr/r_macros.h',
+ './src/util/libekr/r_memory.c',
+ './src/util/libekr/r_memory.h',
+ './src/util/libekr/r_replace.c',
+ './src/util/libekr/r_thread.h',
+ './src/util/libekr/r_time.c',
+ './src/util/libekr/r_time.h',
+ './src/util/libekr/r_types.h',
+
+ # Utilities
+ './src/util/byteorder.c',
+ './src/util/byteorder.h',
+ #'./src/util/escape.c',
+ #'./src/util/escape.h',
+ #'./src/util/filename.c',
+ #'./src/util/getopt.c',
+ #'./src/util/getopt.h',
+ './src/util/hex.c',
+ './src/util/hex.h',
+ #'./src/util/mem_util.c',
+ #'./src/util/mem_util.h',
+ #'./src/util/mutex.c',
+ #'./src/util/mutex.h',
+ './src/util/p_buf.c',
+ './src/util/p_buf.h',
+ #'./src/util/ssl_util.c',
+ #'./src/util/ssl_util.h',
+ './src/util/util.c',
+ './src/util/util.h',
+ #'./src/util/util_db.c',
+ #'./src/util/util_db.h',
+
+ # Events
+# './src/event/async_timer.c',
+ './src/event/async_timer.h',
+# './src/event/async_wait.c',
+ './src/event/async_wait.h',
+ './src/event/async_wait_int.h',
+
+ # Logging
+ './src/log/r_log.c',
+ './src/log/r_log.h',
+ #'./src/log/r_log_plugin.c',
+
+ # Registry
+ './src/registry/c2ru.c',
+ './src/registry/c2ru.h',
+ #'./src/registry/mod_registry/mod_registry.c',
+ #'./src/registry/nrregctl.c',
+ #'./src/registry/nrregistryctl.c',
+ './src/registry/registry.c',
+ './src/registry/registry.h',
+ './src/registry/registry_int.h',
+ './src/registry/registry_local.c',
+ #'./src/registry/registry_plugin.c',
+ './src/registry/registry_vtbl.h',
+ './src/registry/registrycb.c',
+ #'./src/registry/registryd.c',
+ #'./src/registry/regrpc.h',
+ #'./src/registry/regrpc_client.c',
+ #'./src/registry/regrpc_client.h',
+ #'./src/registry/regrpc_client_cb.c',
+ #'./src/registry/regrpc_clnt.c',
+ #'./src/registry/regrpc_server.c',
+ #'./src/registry/regrpc_svc.c',
+ #'./src/registry/regrpc_xdr.c',
+
+ # Statistics
+ #'./src/stats/nrstats.c',
+ #'./src/stats/nrstats.h',
+ #'./src/stats/nrstats_app.c',
+ #'./src/stats/nrstats_int.h',
+ #'./src/stats/nrstats_memory.c',
+ ],
+
+ 'defines' : [
+ 'SANITY_CHECKS',
+ 'R_PLATFORM_INT_TYPES=<stdint.h>',
+ 'R_DEFINED_INT2=int16_t',
+ 'R_DEFINED_UINT2=uint16_t',
+ 'R_DEFINED_INT4=int32_t',
+ 'R_DEFINED_UINT4=uint32_t',
+ 'R_DEFINED_INT8=int64_t',
+ 'R_DEFINED_UINT8=uint64_t',
+ ],
+
+ 'conditions' : [
+ ## Mac and BSDs
+ [ 'OS == "mac"', {
+ 'defines' : [
+ 'DARWIN',
+ ],
+ }],
+ [ 'os_bsd == 1', {
+ 'defines' : [
+ 'BSD',
+ ],
+ }],
+ [ 'OS == "mac" or OS == "ios" or os_bsd == 1', {
+ 'cflags_mozilla': [
+ '-Wall',
+ '-Wno-parentheses',
+ '-Wno-strict-prototypes',
+ '-Wmissing-prototypes',
+ '-Wno-format',
+ '-Wno-format-security',
+ ],
+ 'defines' : [
+ 'HAVE_LIBM=1',
+ 'HAVE_STRDUP=1',
+ 'HAVE_STRLCPY=1',
+ 'HAVE_SYS_TIME_H=1',
+ 'HAVE_VFPRINTF=1',
+ 'NEW_STDIO'
+ 'RETSIGTYPE=void',
+ 'TIME_WITH_SYS_TIME_H=1',
+ '__UNUSED__=__attribute__((unused))',
+ ],
+
+ 'include_dirs': [
+ 'src/port/darwin/include'
+ ],
+
+ 'sources': [
+ './src/port/darwin/include/csi_platform.h',
+ ],
+ }],
+
+ ## Win
+ [ 'OS == "win"', {
+ 'defines' : [
+ 'WIN',
+ '__UNUSED__=',
+ 'HAVE_STRDUP=1',
+ 'NO_REG_RPC'
+ ],
+
+ 'include_dirs': [
+ 'src/port/win32/include'
+ ],
+
+ 'sources': [
+ './src/port/win32/include/csi_platform.h',
+ ],
+ }],
+
+ # Windows, clang-cl build
+ [ 'clang_cl == 1', {
+ 'cflags_mozilla': [
+ '-Xclang',
+ '-Wall',
+ '-Xclang',
+ '-Wno-parentheses',
+ '-Wno-strict-prototypes',
+ '-Wmissing-prototypes',
+ '-Wno-format',
+ '-Wno-format-security',
+ ],
+ }],
+
+ ## Linux/Android
+ [ '(OS == "linux") or (OS == "android")', {
+ 'cflags_mozilla': [
+ '-Wall',
+ '-Wno-parentheses',
+ '-Wno-strict-prototypes',
+ '-Wmissing-prototypes',
+ '-Wno-format',
+ '-Wno-format-security',
+ ],
+ 'defines' : [
+ 'LINUX',
+ 'HAVE_LIBM=1',
+ 'HAVE_STRDUP=1',
+ 'HAVE_STRLCPY=1',
+ 'HAVE_SYS_TIME_H=1',
+ 'HAVE_VFPRINTF=1',
+ 'NEW_STDIO'
+ 'RETSIGTYPE=void',
+ 'TIME_WITH_SYS_TIME_H=1',
+ 'NO_REG_RPC=1',
+ '__UNUSED__=__attribute__((unused))',
+ ],
+
+ 'include_dirs': [
+ 'src/port/linux/include'
+ ],
+ 'sources': [
+ './src/port/linux/include/csi_platform.h',
+ ],
+ }]
+ ]
+ }]
+}
+
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/event/async_timer.h b/dom/media/webrtc/transport/third_party/nrappkit/src/event/async_timer.h
new file mode 100644
index 0000000000..544a3d44fd
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/event/async_timer.h
@@ -0,0 +1,54 @@
+/**
+ async_timer.h
+
+
+ Copyright (C) 2004, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ ekr@rtfm.com Sun Feb 22 19:35:24 2004
+ */
+
+
+#ifndef _async_timer_h
+#define _async_timer_h
+
+
+int NR_async_timer_init(void);
+int NR_async_timer_set(int delay_ms,NR_async_cb cb,void *cb_arg,char *function,int line,void **handle);
+int NR_async_timer_cancel(void *handle);
+int NR_async_timer_update_time(struct timeval *tv);
+int NR_async_timer_next_timeout(int *delay_ms);
+int NR_async_timer_sanity_check_for_cb_deleted(NR_async_cb cb,void *cb_arg);
+
+#define NR_ASYNC_TIMER_SET(d,c,a,hp) NR_async_timer_set(d,c,a,(char *)__FUNCTION__,__LINE__,hp)
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/event/async_wait.h b/dom/media/webrtc/transport/third_party/nrappkit/src/event/async_wait.h
new file mode 100644
index 0000000000..58bcd435eb
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/event/async_wait.h
@@ -0,0 +1,83 @@
+/**
+ async_wait.h
+
+
+ Copyright (C) 2001-2003, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ ekr@rtfm.com Thu Dec 20 20:14:49 2001
+ */
+
+
+#ifndef _async_wait_h
+#define _async_wait_h
+
+#ifdef HAVE_SYS_TIME_H
+#include <sys/time.h>
+#endif
+#include <csi_platform.h>
+
+typedef void (*NR_async_cb)(NR_SOCKET resource,int how,void *arg);
+
+#define NR_ASYNC_WAIT_READ 0
+#define NR_ASYNC_WAIT_WRITE 1
+
+
+int NR_async_wait_init(void);
+int NR_async_wait(NR_SOCKET sock, int how, NR_async_cb cb,void *cb_arg,
+ char *function,int line);
+int NR_async_cancel(NR_SOCKET sock,int how);
+int NR_async_schedule(NR_async_cb cb,void *arg,char *function,int line);
+int NR_async_event_wait(int *eventsp);
+int NR_async_event_wait2(int *eventsp,struct timeval *tv);
+
+
+#ifdef NR_DEBUG_ASYNC
+#define NR_ASYNC_WAIT(s,h,cb,arg) do { \
+fprintf(stderr,"NR_async_wait(%d,%s,%s) at %s(%d)\n",s,#h,#cb,__FUNCTION__,__LINE__); \
+ NR_async_wait(s,h,cb,arg,(char *)__FUNCTION__,__LINE__); \
+} while (0)
+#define NR_ASYNC_SCHEDULE(cb,arg) do { \
+fprintf(stderr,"NR_async_schedule(%s) at %s(%d)\n",#cb,__FUNCTION__,__LINE__);\
+ NR_async_schedule(cb,arg,(char *)__FUNCTION__,__LINE__); \
+} while (0)
+#define NR_ASYNC_CANCEL(s,h) do { \
+ fprintf(stderr,"NR_async_cancel(%d,%s) at %s(%d)\n",s,#h,(char *)__FUNCTION__,__LINE__); \
+NR_async_cancel(s,h); \
+} while (0)
+#else
+#define NR_ASYNC_WAIT(a,b,c,d) NR_async_wait(a,b,c,d,(char *)__FUNCTION__,__LINE__)
+#define NR_ASYNC_SCHEDULE(a,b) NR_async_schedule(a,b,(char *)__FUNCTION__,__LINE__)
+#define NR_ASYNC_CANCEL NR_async_cancel
+#endif
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/event/async_wait_int.h b/dom/media/webrtc/transport/third_party/nrappkit/src/event/async_wait_int.h
new file mode 100644
index 0000000000..d17372d183
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/event/async_wait_int.h
@@ -0,0 +1,62 @@
+/**
+ async_wait_int.h
+
+
+ Copyright (C) 2004, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ ekr@rtfm.com Sun Feb 22 19:16:01 2004
+ */
+
+
+#ifndef _async_wait_int_h
+#define _async_wait_int_h
+
+
+typedef struct callback_ {
+ NR_async_cb cb;
+ void *arg;
+ int how;
+ int resource;
+ void *backptr;
+
+ char *func;
+ int free_func;
+ int line;
+ TAILQ_ENTRY(callback_) entry;
+} callback;
+
+int nr_async_create_cb(NR_async_cb cb,void *arg,int how,
+ int resource,char *func,int line,callback **cbp);
+int nr_async_destroy_cb(callback **cbp);
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/log/r_log.c b/dom/media/webrtc/transport/third_party/nrappkit/src/log/r_log.c
new file mode 100644
index 0000000000..09bb24749f
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/log/r_log.c
@@ -0,0 +1,696 @@
+/**
+ r_log.c
+
+
+ Copyright (C) 2001, RTFM, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ ekr@rtfm.com Mon Dec 3 15:24:38 2001
+ */
+
+#ifdef LINUX
+#define _BSD_SOURCE
+#define _DEFAULT_SOURCE
+#endif
+
+#include "r_log.h"
+#include "hex.h"
+
+#include <string.h>
+#include <errno.h>
+#ifndef _MSC_VER
+#include <strings.h>
+#include <syslog.h>
+#endif
+#include <registry.h>
+#include <time.h>
+
+
+#include "nr_common.h"
+#include "nr_reg_keys.h"
+
+
+#define LOGGING_DEFAULT_LEVEL 5
+
+int NR_LOG_LOGGING = 0;
+
+static char *log_level_strings[]={
+ "EMERG",
+ "ALERT",
+ "CRIT",
+ "ERR",
+ "WARNING",
+ "NOTICE",
+ "INFO",
+ "DEBUG"
+};
+
+static char *log_level_reg_strings[]={
+ "emergency",
+ "alert",
+ "critical",
+ "error",
+ "warning",
+ "notice",
+ "info",
+ "debug"
+};
+
+#define LOGGING_REG_PREFIX "logging"
+
+#define MAX_ERROR_STRING_SIZE 512
+
+#define R_LOG_INITTED1 1
+#define R_LOG_INITTED2 2
+
+#define LOG_NUM_DESTINATIONS 3
+
+typedef struct log_type_ {
+ char *facility_name;
+ int level[LOG_NUM_DESTINATIONS];
+ NR_registry dest_facility_key[LOG_NUM_DESTINATIONS];
+} log_type;
+
+#define MAX_LOG_TYPES 16
+
+static log_type log_types[MAX_LOG_TYPES];
+static int log_type_ct;
+
+
+typedef struct log_destination_ {
+ char *dest_name;
+ int enabled;
+ int default_level;
+ r_dest_vlog *dest_vlog;
+} log_destination;
+
+
+#define LOG_LEVEL_UNDEFINED -1
+#define LOG_LEVEL_NONE -2
+#define LOG_LEVEL_USE_DEST_DEFAULT -3
+
+static int stderr_vlog(int facility,int level,const char *format,va_list ap);
+static int syslog_vlog(int facility,int level,const char *format,va_list ap);
+static int noop_vlog(int facility,int level,const char *format,va_list ap);
+
+static log_destination log_destinations[LOG_NUM_DESTINATIONS]={
+ {
+ "stderr",
+ 0,
+ LOGGING_DEFAULT_LEVEL,
+ stderr_vlog,
+ },
+ {
+ "syslog",
+#ifndef WIN32
+ 1,
+#else
+ 0,
+#endif
+ LOGGING_DEFAULT_LEVEL,
+ syslog_vlog,
+ },
+ {
+ "extra",
+ 0,
+ LOGGING_DEFAULT_LEVEL,
+ noop_vlog,
+ },
+};
+
+static int r_log_level=LOGGING_DEFAULT_LEVEL;
+static int r_log_level_environment=0;
+static int r_log_initted=0;
+static int r_log_env_verbose=0;
+
+static void r_log_facility_change_cb(void *cb_arg, char action, NR_registry name);
+static void r_log_facility_delete_cb(void *cb_arg, char action, NR_registry name);
+static void r_log_destination_change_cb(void *cb_arg, char action, NR_registry name);
+static void r_log_default_level_change_cb(void *cb_arg, char action, NR_registry name);
+static int r_log_get_default_level(void);
+static int r_log_get_destinations(int usereg);
+static int r_logging_dest(int dest_index, int facility, int level);
+static int _r_log_init(int usereg);
+static int r_log_get_reg_level(NR_registry name, int *level);
+
+int r_log_register(char *facility_name,int *log_facility)
+ {
+ int i,j;
+ int level;
+ int r,_status;
+ char *buf=0;
+ NR_registry dest_prefix, dest_facility_prefix;
+
+ for(i=0;i<log_type_ct;i++){
+ if(!strcmp(facility_name,log_types[i].facility_name)){
+ *log_facility=i;
+ return(0);
+ }
+ }
+
+ if(log_type_ct==MAX_LOG_TYPES){
+ ABORT(R_INTERNAL);
+ }
+
+ i=log_type_ct;
+
+ /* Initial registration completed, increment log_type_ct */
+ log_types[i].facility_name=r_strdup(facility_name);
+ *log_facility=log_type_ct;
+ log_type_ct++;
+
+ for(j=0; j<LOG_NUM_DESTINATIONS; j++){
+ log_types[i].level[j]=LOG_LEVEL_UNDEFINED;
+
+ if(NR_reg_initted()){
+
+ if((size_t)snprintf(dest_prefix,sizeof(NR_registry),
+ "logging.%s.facility",log_destinations[j].dest_name)>=sizeof(NR_registry))
+ ABORT(R_INTERNAL);
+
+ if (r=NR_reg_make_registry(dest_prefix,facility_name,dest_facility_prefix))
+ ABORT(r);
+
+ if((size_t)snprintf(log_types[i].dest_facility_key[j],sizeof(NR_registry),
+ "%s.level",dest_facility_prefix)>=sizeof(NR_registry))
+ ABORT(R_INTERNAL);
+
+ if(!r_log_get_reg_level(log_types[i].dest_facility_key[j],&level)){
+ log_types[i].level[j]=level;
+ }
+
+ /* Set a callback for the facility's level */
+ if(r=NR_reg_register_callback(log_types[i].dest_facility_key[j],
+ NR_REG_CB_ACTION_ADD|NR_REG_CB_ACTION_CHANGE,
+ r_log_facility_change_cb,(void *)&(log_types[i].level[j])))
+ ABORT(r);
+ if(r=NR_reg_register_callback(log_types[i].dest_facility_key[j],
+ NR_REG_CB_ACTION_DELETE,
+ r_log_facility_delete_cb,(void *)&(log_types[i].level[j])))
+ ABORT(r);
+
+ }
+ }
+
+ _status=0;
+ abort:
+ if(_status)
+ RFREE(buf);
+ return(_status);
+ }
+
+int r_log_facility(int facility,char **typename)
+ {
+ if(facility >= 0 && facility < log_type_ct){
+ *typename=log_types[facility].facility_name;
+ return(0);
+ }
+ return(R_NOT_FOUND);
+ }
+
+static int r_log_get_reg_level(NR_registry name, int *out)
+ {
+ char level[32];
+ int r,_status;
+ int i;
+
+ if(r=NR_reg_get_string(name,level,sizeof(level)))
+ ABORT(r);
+
+ if(!strcasecmp(level,"none")){
+ *out=LOG_LEVEL_NONE;
+ return(0);
+ }
+
+ for(i=0;i<=LOG_DEBUG;i++){
+ if(!strcasecmp(level,log_level_reg_strings[i])){
+ *out=(int)i;
+ return(0);
+ }
+ }
+
+ if(i>LOG_DEBUG){
+ *out=LOG_LEVEL_UNDEFINED;
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+/* Handle the case where a value changes */
+static void r_log_facility_change_cb(void *cb_arg, char action, NR_registry name)
+ {
+ int *lt_level=(int *)cb_arg;
+ int level;
+ int r,_status;
+
+ if(r=r_log_get_reg_level(name,&level))
+ ABORT(r);
+
+ *lt_level=level;
+
+ _status=0;
+ abort:
+ (void)_status; // to avoid unused variable warning and still conform to
+ // pattern of using ABORT
+ return;
+ }
+
+/* Handle the case where a value is deleted */
+static void r_log_facility_delete_cb(void *cb_arg, char action, NR_registry name)
+ {
+ int *lt_level=(int *)cb_arg;
+
+ *lt_level=LOG_LEVEL_UNDEFINED;
+ }
+
+int r_log(int facility,int level,const char *format,...)
+ {
+ va_list ap;
+
+ va_start(ap,format);
+
+ r_vlog(facility,level,format,ap);
+ va_end(ap);
+
+ return(0);
+ }
+
+int r_dump(int facility,int level,char *name,char *data,int len)
+ {
+ char *hex = 0;
+ size_t unused;
+
+ if(!r_logging(facility,level))
+ return(0);
+
+ hex=RMALLOC((len*2)+1);
+ if (!hex)
+ return(R_FAILED);
+
+ if (nr_nbin2hex((UCHAR*)data, len, hex, len*2+1, &unused))
+ strcpy(hex, "?");
+
+ if(name)
+ r_log(facility,level,"%s[%d]=%s",name,len,hex);
+ else
+ r_log(facility,level,"%s",hex);
+
+ RFREE(hex);
+ return(0);
+ }
+
+// Some platforms (notably WIN32) do not have this
+#ifndef va_copy
+ #ifdef WIN32
+ #define va_copy(dest, src) ( (dest) = (src) )
+ #else // WIN32
+ #error va_copy undefined, and semantics of assignment on va_list unknown
+ #endif //WIN32
+#endif //va_copy
+
+int r_vlog(int facility,int level,const char *format,va_list ap)
+ {
+ char log_fmt_buf[MAX_ERROR_STRING_SIZE];
+ char *level_str="unknown";
+ char *facility_str="unknown";
+ char *fmt_str=(char *)format;
+ int i;
+
+ if(r_log_env_verbose){
+ if((level>=LOG_EMERG) && (level<=LOG_DEBUG))
+ level_str=log_level_strings[level];
+
+ if(facility >= 0 && facility < log_type_ct)
+ facility_str=log_types[facility].facility_name;
+
+ snprintf(log_fmt_buf, MAX_ERROR_STRING_SIZE, "(%s/%s) %s",
+ facility_str,level_str,format);
+
+ log_fmt_buf[MAX_ERROR_STRING_SIZE-1]=0;
+ fmt_str=log_fmt_buf;
+ }
+
+ for(i=0; i<LOG_NUM_DESTINATIONS; i++){
+ if(r_logging_dest(i,facility,level)){
+ // Some platforms do not react well when you use a va_list more than
+ // once
+ va_list copy;
+ va_copy(copy, ap);
+ log_destinations[i].dest_vlog(facility,level,fmt_str,copy);
+ va_end(copy);
+ }
+ }
+ return(0);
+ }
+
+int stderr_vlog(int facility,int level,const char *format,va_list ap)
+ {
+#if 0 /* remove time stamping, for now */
+ char cbuf[30];
+ time_t tt;
+
+ tt=time(0);
+
+ ctime_r(&tt,cbuf);
+ cbuf[strlen(cbuf)-1]=0;
+
+ fprintf(stderr,"%s: ",cbuf);
+#endif
+
+ vfprintf(stderr,format,ap);
+ fprintf(stderr,"\n");
+ return(0);
+ }
+
+int syslog_vlog(int facility,int level,const char *format,va_list ap)
+ {
+#ifndef WIN32
+ vsyslog(level|LOG_LOCAL0,format,ap);
+#endif
+ return(0);
+ }
+
+int noop_vlog(int facility,int level,const char *format,va_list ap)
+ {
+ return(0);
+ }
+
+int r_log_e(int facility,int level,const char *format,...)
+ {
+ va_list ap;
+
+ va_start(ap,format);
+ r_vlog_e(facility,level,format,ap);
+ va_end(ap);
+
+ return(0);
+ }
+
+int r_vlog_e(int facility,int level,const char *format,va_list ap)
+ {
+ char log_fmt_buf[MAX_ERROR_STRING_SIZE];
+ if(r_logging(facility,level)) {
+ int formatlen = strlen(format);
+
+ if(formatlen+2 > MAX_ERROR_STRING_SIZE)
+ return(1);
+
+ strncpy(log_fmt_buf, format, formatlen);
+ strcpy(&log_fmt_buf[formatlen], ": ");
+ snprintf(&log_fmt_buf[formatlen+2], MAX_ERROR_STRING_SIZE - formatlen - 2, "%s",
+#ifdef WIN32
+ strerror(WSAGetLastError()));
+#else
+ strerror(errno));
+#endif
+ log_fmt_buf[MAX_ERROR_STRING_SIZE-1]=0;
+
+ r_vlog(facility,level,log_fmt_buf,ap);
+ }
+ return(0);
+ }
+
+int r_log_nr(int facility,int level,int r,const char *format,...)
+ {
+ va_list ap;
+
+ va_start(ap,format);
+ r_vlog_nr(facility,level,r,format,ap);
+ va_end(ap);
+
+ return(0);
+ }
+
+int r_vlog_nr(int facility,int level,int r,const char *format,va_list ap)
+ {
+ char log_fmt_buf[MAX_ERROR_STRING_SIZE];
+ if(r_logging(facility,level)) {
+ int formatlen = strlen(format);
+
+ if(formatlen+2 > MAX_ERROR_STRING_SIZE)
+ return(1);
+ strncpy(log_fmt_buf, format, formatlen);
+ strcpy(&log_fmt_buf[formatlen], ": ");
+ snprintf(&log_fmt_buf[formatlen+2], MAX_ERROR_STRING_SIZE - formatlen - 2, "%s",
+ nr_strerror(r));
+
+ log_fmt_buf[MAX_ERROR_STRING_SIZE-1]=0;
+
+ r_vlog(facility,level,log_fmt_buf,ap);
+ }
+ return(0);
+ }
+
+static int r_logging_dest(int dest_index, int facility, int level)
+ {
+ int thresh;
+
+ _r_log_init(0);
+
+ if(!log_destinations[dest_index].enabled)
+ return(0);
+
+ if(level <= r_log_level_environment)
+ return(1);
+
+ if(r_log_initted<R_LOG_INITTED2)
+ return(level<=r_log_level);
+
+ if(facility < 0 || facility > log_type_ct)
+ thresh=r_log_level;
+ else{
+ if(log_types[facility].level[dest_index]==LOG_LEVEL_NONE)
+ return(0);
+
+ if(log_types[facility].level[dest_index]>=0)
+ thresh=log_types[facility].level[dest_index];
+ else if(log_destinations[dest_index].default_level!=LOG_LEVEL_UNDEFINED)
+ thresh=log_destinations[dest_index].default_level;
+ else
+ thresh=r_log_level;
+ }
+
+ if(level<=thresh)
+ return(1);
+
+ return(0);
+ }
+
+int r_logging(int facility, int level)
+ {
+ int i;
+
+ _r_log_init(0);
+
+ /* return 1 if logging is on for any dest */
+
+ for(i=0; i<LOG_NUM_DESTINATIONS; i++){
+ if(r_logging_dest(i,facility,level))
+ return(1);
+ }
+
+ return(0);
+ }
+
+
+static int r_log_get_default_level(void)
+ {
+ char *log;
+ int _status;
+
+ log=getenv("R_LOG_LEVEL");
+
+ if(log){
+ r_log_level=atoi(log);
+ r_log_level_environment=atoi(log);
+ }
+ else{
+ r_log_level=LOGGING_DEFAULT_LEVEL;
+ }
+
+ _status=0;
+ //abort:
+ return(_status);
+ }
+
+
+static int r_log_get_destinations(int usereg)
+ {
+ char *log;
+ int i;
+ int r,_status;
+
+ log=getenv("R_LOG_DESTINATION");
+ if(log){
+ for(i=0; i<LOG_NUM_DESTINATIONS; i++)
+ log_destinations[i].enabled=!strcmp(log,log_destinations[i].dest_name);
+ }
+ else if(usereg){
+ NR_registry reg_key;
+ int i;
+ int value;
+ char c;
+
+ /* Get the data out of the registry */
+ for(i=0; i<LOG_NUM_DESTINATIONS; i++){
+ /* set callback for default level */
+ if((size_t)snprintf(reg_key,sizeof(reg_key),"%s.%s.level",LOGGING_REG_PREFIX,
+ log_destinations[i].dest_name)>=sizeof(reg_key))
+ ABORT(R_INTERNAL);
+
+ NR_reg_register_callback(reg_key,
+ NR_REG_CB_ACTION_ADD|NR_REG_CB_ACTION_CHANGE|NR_REG_CB_ACTION_DELETE,
+ r_log_default_level_change_cb,0);
+
+ if(r=r_log_get_reg_level(reg_key,&value)){
+ if(r==R_NOT_FOUND)
+ log_destinations[i].default_level=LOG_LEVEL_UNDEFINED;
+ else
+ ABORT(R_INTERNAL);
+ }
+ else
+ log_destinations[i].default_level=value;
+
+ /* set callback for the enabled key for this logging dest */
+ if((size_t)snprintf(reg_key,sizeof(reg_key),"%s.%s.enabled",LOGGING_REG_PREFIX,
+ log_destinations[i].dest_name)>=sizeof(reg_key))
+ ABORT(R_INTERNAL);
+
+ NR_reg_register_callback(reg_key,
+ NR_REG_CB_ACTION_ADD|NR_REG_CB_ACTION_CHANGE|NR_REG_CB_ACTION_DELETE,
+ r_log_destination_change_cb,0);
+
+ if(r=NR_reg_get_char(reg_key,&c)){
+ if(r==R_NOT_FOUND)
+ log_destinations[i].enabled=0;
+ else
+ ABORT(r);
+ }
+ else
+ log_destinations[i].enabled=c;
+ }
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+static void r_log_destination_change_cb(void *cb_arg, char action, NR_registry name)
+ {
+ r_log_get_destinations(1);
+ }
+
+static void r_log_default_level_change_cb(void *cb_arg, char action, NR_registry name)
+ {
+ r_log_get_destinations(1);
+ }
+
+
+int r_log_init()
+ {
+ _r_log_init(1);
+
+ return 0;
+ }
+
+int _r_log_init(int use_reg)
+ {
+#ifndef WIN32
+ char *log;
+#endif
+
+ if(r_log_initted==0) {
+#ifdef WIN32
+ r_log_env_verbose=1;
+#else
+ log=getenv("R_LOG_VERBOSE");
+ if(log)
+ r_log_env_verbose=atoi(log);
+#endif
+
+ }
+
+ if(!use_reg){
+ if(r_log_initted<R_LOG_INITTED1){
+ r_log_get_default_level();
+ r_log_get_destinations(0);
+
+ r_log_initted=R_LOG_INITTED1;
+ }
+ }
+ else{
+ if(r_log_initted<R_LOG_INITTED2){
+ int facility;
+
+ r_log_get_default_level();
+ r_log_get_destinations(1);
+
+ r_log_register("generic",&facility);
+ r_log_register("logging",&NR_LOG_LOGGING);
+
+ r_log_initted=R_LOG_INITTED2;
+ }
+ }
+
+ return(0);
+ }
+
+int r_log_set_extra_destination(int default_level, r_dest_vlog *dest_vlog)
+ {
+ int i;
+ log_destination *dest = 0;
+
+ for(i=0; i<LOG_NUM_DESTINATIONS; i++){
+ if(!strcmp("extra",log_destinations[i].dest_name)){
+ dest=&log_destinations[i];
+ break;
+ }
+ }
+
+ if(!dest)
+ return(R_INTERNAL);
+
+ if (dest_vlog==0){
+ dest->enabled=0;
+ dest->dest_vlog=noop_vlog;
+ }
+ else{
+ dest->enabled=1;
+ dest->default_level=default_level;
+ dest->dest_vlog=dest_vlog;
+ }
+
+ return(0);
+ }
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/log/r_log.h b/dom/media/webrtc/transport/third_party/nrappkit/src/log/r_log.h
new file mode 100644
index 0000000000..a72dfa0667
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/log/r_log.h
@@ -0,0 +1,85 @@
+/**
+ r_log.h
+
+
+ Copyright (C) 2001, RTFM, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ ekr@rtfm.com Mon Dec 3 15:14:45 2001
+ */
+
+
+#ifndef _r_log_h
+#define _r_log_h
+
+#ifndef WIN32
+#include <syslog.h>
+#endif
+#include <stdarg.h>
+#include <r_common.h>
+
+int r_log(int facility,int level,const char *fmt,...)
+#ifdef __GNUC__
+ __attribute__ ((format (printf, 3, 4)))
+#endif
+;
+
+int r_vlog(int facility,int level,const char *fmt,va_list ap);
+int r_dump(int facility,int level,char *name,char *data,int len);
+
+int r_log_e(int facility,int level,const char *fmt,...)
+#ifdef __GNUC__
+ __attribute__ ((format (printf, 3, 4)))
+#endif
+;
+
+int r_vlog_e(int facility,int level,const char *fmt,va_list ap);
+int r_log_nr(int facility,int level,int r,const char *fmt,...)
+#ifdef __GNUC__
+ __attribute__ ((format (printf, 4, 5)))
+#endif
+;
+
+int r_vlog_nr(int facility,int level,int r,const char *fmt,va_list ap);
+
+int r_log_register(char *tipename,int *facility);
+int r_log_facility(int facility,char **tipename);
+int r_logging(int facility, int level);
+int r_log_init(void);
+
+#define LOG_GENERIC 0
+#define LOG_COMMON 0
+
+typedef int r_dest_vlog(int facility,int level,const char *format,va_list ap);
+int r_log_set_extra_destination(int default_level, r_dest_vlog *dest_vlog);
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/plugin/nr_plugin.h b/dom/media/webrtc/transport/third_party/nrappkit/src/plugin/nr_plugin.h
new file mode 100644
index 0000000000..56da0624a6
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/plugin/nr_plugin.h
@@ -0,0 +1,57 @@
+/**
+ nr_plugin.h
+
+
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ ekr@networkresonance.com Mon Jun 19 18:18:54 2006
+ */
+
+
+#ifndef _nr_plugin_h
+#define _nr_plugin_h
+
+typedef int (NR_plugin_hook)(void);
+
+typedef struct NR_plugin_hook_def_ {
+ char *type;
+ NR_plugin_hook *func;
+} NR_plugin_hook_def;
+
+typedef struct NR_plugin_def_ {
+ int api_version; // Should be 1
+ char *name;
+ char *version;
+ NR_plugin_hook_def *hooks;
+} NR_plugin_def;
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/port/android/include/android_funcs.h b/dom/media/webrtc/transport/third_party/nrappkit/src/port/android/include/android_funcs.h
new file mode 100644
index 0000000000..d48bbe1373
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/port/android/include/android_funcs.h
@@ -0,0 +1,62 @@
+/**
+ linux_funcs.h
+
+
+ Copyright (C) 2004, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ ekr@rtfm.com Mon Dec 13 16:28:22 2004
+ */
+
+
+#ifndef _android_funcs_h
+#define _android_funcs_h
+
+#include <sys/types.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <string.h>
+#include <unistd.h>
+
+#define ETHERTYPE_VLAN 0x8100
+
+#define STDIO_BYTES_BUFFERED(fp) (fp->_IO_read_end - fp->_IO_read_ptr)
+
+size_t strlcat(char *dst, const char *src, size_t siz);
+#ifndef strlcpy
+#define strlcpy(a,b,c) \
+ (strncpy((a),(b),(c)), \
+ ((c)<= 0 ? 0 : ((a)[(c)-1]='\0')), \
+ strlen((b)))
+#endif
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/port/android/include/csi_platform.h b/dom/media/webrtc/transport/third_party/nrappkit/src/port/android/include/csi_platform.h
new file mode 100644
index 0000000000..bcfb2bce77
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/port/android/include/csi_platform.h
@@ -0,0 +1,55 @@
+/**
+ platform.h
+
+
+ Copyright (C) 2004, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ ekr@rtfm.com Mon Dec 13 17:26:51 2004
+ */
+
+
+#ifndef _platform_h
+#define _platform_h
+
+#include <android_funcs.h>
+
+#ifdef NR_SOCKET_IS_VOID_PTR
+typedef void* NR_SOCKET;
+#else
+typedef int NR_SOCKET;
+#define NR_SOCKET_READ(sock,buf,count) read((sock),(buf),(count))
+#define NR_SOCKET_WRITE(sock,buf,count) write((sock),(buf),(count))
+#define NR_SOCKET_CLOSE(sock) close(sock)
+#endif
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/port/android/include/sys/ttycom.h b/dom/media/webrtc/transport/third_party/nrappkit/src/port/android/include/sys/ttycom.h
new file mode 100644
index 0000000000..852bf9103b
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/port/android/include/sys/ttycom.h
@@ -0,0 +1,38 @@
+/*
+ *
+ * Copyright (C) 2006, Network Resonance, Inc.
+ * All Rights Reserved
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Network Resonance, Inc. nor the name of any
+ * contributors to this software may be used to endorse or promote
+ * products derived from this software without specific prior written
+ * permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+#ifndef __ANDROID_TTYCOM_H
+#define __ANDROID_TTYCOM_H
+
+#include <asm/ioctls.h>
+
+#endif
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/port/android/port-impl.mk b/dom/media/webrtc/transport/third_party/nrappkit/src/port/android/port-impl.mk
new file mode 100644
index 0000000000..6704cfedf9
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/port/android/port-impl.mk
@@ -0,0 +1,31 @@
+#
+# Copyright (C) 2006, Network Resonance, Inc.
+# All Rights Reserved
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# 3. Neither the name of Network Resonance, Inc. nor the name of any
+# contributors to this software may be used to endorse or promote
+# products derived from this software without specific prior written
+# permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+#EXTATTR_IMPL=xattr
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/port/darwin/include/csi_platform.h b/dom/media/webrtc/transport/third_party/nrappkit/src/port/darwin/include/csi_platform.h
new file mode 100644
index 0000000000..41f1a5b097
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/port/darwin/include/csi_platform.h
@@ -0,0 +1,57 @@
+/**
+ platform.h
+
+
+ Copyright (C) 2005, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ briank@network-resonance.com Tue Mar 15 14:28:12 PST 2005
+ */
+
+
+#ifndef _platform_h
+#define _platform_h
+
+#include <unistd.h>
+
+#define STDIO_BYTES_BUFFERED(fp) (fp->_r)
+
+#ifdef NR_SOCKET_IS_VOID_PTR
+typedef void* NR_SOCKET;
+#else
+typedef int NR_SOCKET;
+#define NR_SOCKET_READ(sock,buf,count) read((sock),(buf),(count))
+#define NR_SOCKET_WRITE(sock,buf,count) write((sock),(buf),(count))
+#define NR_SOCKET_CLOSE(sock) close(sock)
+#endif
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/port/generic/include/sys/queue.h b/dom/media/webrtc/transport/third_party/nrappkit/src/port/generic/include/sys/queue.h
new file mode 100644
index 0000000000..4d7b998a34
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/port/generic/include/sys/queue.h
@@ -0,0 +1,562 @@
+/*
+ * Copyright (c) 1991, 1993
+ * The Regents of the University of California. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 4. Neither the name of the University nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ *
+ * @(#)queue.h 8.5 (Berkeley) 8/20/94
+ * $FreeBSD: src/sys/sys/queue.h,v 1.58 2004/04/07 04:19:49 imp Exp $
+ */
+
+#ifndef _SYS_QUEUE_H_
+#define _SYS_QUEUE_H_
+
+#include <stddef.h>
+
+#ifndef offsetof
+#define offsetof(type, field) ((size_t)(&((type *)0)->field))
+#endif
+
+#define STAILQ_FOREACH_SAFE(var, head, field, tvar) \
+ for ((var) = STAILQ_FIRST((head)); \
+ (var) && ((tvar) = STAILQ_NEXT((var), field), 1); \
+ (var) = (tvar))
+
+/*
+ * This file defines four types of data structures: singly-linked lists,
+ * singly-linked tail queues, lists and tail queues.
+ *
+ * A singly-linked list is headed by a single forward pointer. The elements
+ * are singly linked for minimum space and pointer manipulation overhead at
+ * the expense of O(n) removal for arbitrary elements. New elements can be
+ * added to the list after an existing element or at the head of the list.
+ * Elements being removed from the head of the list should use the explicit
+ * macro for this purpose for optimum efficiency. A singly-linked list may
+ * only be traversed in the forward direction. Singly-linked lists are ideal
+ * for applications with large datasets and few or no removals or for
+ * implementing a LIFO queue.
+ *
+ * A singly-linked tail queue is headed by a pair of pointers, one to the
+ * head of the list and the other to the tail of the list. The elements are
+ * singly linked for minimum space and pointer manipulation overhead at the
+ * expense of O(n) removal for arbitrary elements. New elements can be added
+ * to the list after an existing element, at the head of the list, or at the
+ * end of the list. Elements being removed from the head of the tail queue
+ * should use the explicit macro for this purpose for optimum efficiency.
+ * A singly-linked tail queue may only be traversed in the forward direction.
+ * Singly-linked tail queues are ideal for applications with large datasets
+ * and few or no removals or for implementing a FIFO queue.
+ *
+ * A list is headed by a single forward pointer (or an array of forward
+ * pointers for a hash table header). The elements are doubly linked
+ * so that an arbitrary element can be removed without a need to
+ * traverse the list. New elements can be added to the list before
+ * or after an existing element or at the head of the list. A list
+ * may only be traversed in the forward direction.
+ *
+ * A tail queue is headed by a pair of pointers, one to the head of the
+ * list and the other to the tail of the list. The elements are doubly
+ * linked so that an arbitrary element can be removed without a need to
+ * traverse the list. New elements can be added to the list before or
+ * after an existing element, at the head of the list, or at the end of
+ * the list. A tail queue may be traversed in either direction.
+ *
+ * For details on the use of these macros, see the queue(3) manual page.
+ *
+ *
+ * SLIST LIST STAILQ TAILQ
+ * _HEAD + + + +
+ * _HEAD_INITIALIZER + + + +
+ * _ENTRY + + + +
+ * _INIT + + + +
+ * _EMPTY + + + +
+ * _FIRST + + + +
+ * _NEXT + + + +
+ * _PREV - - - +
+ * _LAST - - + +
+ * _FOREACH + + + +
+ * _FOREACH_SAFE + + + +
+ * _FOREACH_REVERSE - - - +
+ * _FOREACH_REVERSE_SAFE - - - +
+ * _INSERT_HEAD + + + +
+ * _INSERT_BEFORE - + - +
+ * _INSERT_AFTER + + + +
+ * _INSERT_TAIL - - + +
+ * _CONCAT - - + +
+ * _REMOVE_HEAD + - + -
+ * _REMOVE + + + +
+ *
+ */
+#define QUEUE_MACRO_DEBUG 0
+#if QUEUE_MACRO_DEBUG
+/* Store the last 2 places the queue element or head was altered */
+struct qm_trace {
+ char * lastfile;
+ int lastline;
+ char * prevfile;
+ int prevline;
+};
+
+#define TRACEBUF struct qm_trace trace;
+#define TRASHIT(x) do {(x) = (void *)-1;} while (0)
+
+#define QMD_TRACE_HEAD(head) do { \
+ (head)->trace.prevline = (head)->trace.lastline; \
+ (head)->trace.prevfile = (head)->trace.lastfile; \
+ (head)->trace.lastline = __LINE__; \
+ (head)->trace.lastfile = __FILE__; \
+} while (0)
+
+#define QMD_TRACE_ELEM(elem) do { \
+ (elem)->trace.prevline = (elem)->trace.lastline; \
+ (elem)->trace.prevfile = (elem)->trace.lastfile; \
+ (elem)->trace.lastline = __LINE__; \
+ (elem)->trace.lastfile = __FILE__; \
+} while (0)
+
+#else
+#define QMD_TRACE_ELEM(elem)
+#define QMD_TRACE_HEAD(head)
+#define TRACEBUF
+#define TRASHIT(x)
+#endif /* QUEUE_MACRO_DEBUG */
+
+/*
+ * Singly-linked List declarations.
+ */
+#define SLIST_HEAD(name, type) \
+struct name { \
+ struct type *slh_first; /* first element */ \
+}
+
+#define SLIST_HEAD_INITIALIZER(head) \
+ { NULL }
+
+#define SLIST_ENTRY(type) \
+struct { \
+ struct type *sle_next; /* next element */ \
+}
+
+/*
+ * Singly-linked List functions.
+ */
+#define SLIST_EMPTY(head) ((head)->slh_first == NULL)
+
+#define SLIST_FIRST(head) ((head)->slh_first)
+
+#define SLIST_FOREACH(var, head, field) \
+ for ((var) = SLIST_FIRST((head)); \
+ (var); \
+ (var) = SLIST_NEXT((var), field))
+
+#define SLIST_FOREACH_SAFE(var, head, field, tvar) \
+ for ((var) = SLIST_FIRST((head)); \
+ (var) && ((tvar) = SLIST_NEXT((var), field), 1); \
+ (var) = (tvar))
+
+#define SLIST_FOREACH_PREVPTR(var, varp, head, field) \
+ for ((varp) = &SLIST_FIRST((head)); \
+ ((var) = *(varp)) != NULL; \
+ (varp) = &SLIST_NEXT((var), field))
+
+#define SLIST_INIT(head) do { \
+ SLIST_FIRST((head)) = NULL; \
+} while (0)
+
+#define SLIST_INSERT_AFTER(slistelm, elm, field) do { \
+ SLIST_NEXT((elm), field) = SLIST_NEXT((slistelm), field); \
+ SLIST_NEXT((slistelm), field) = (elm); \
+} while (0)
+
+#define SLIST_INSERT_HEAD(head, elm, field) do { \
+ SLIST_NEXT((elm), field) = SLIST_FIRST((head)); \
+ SLIST_FIRST((head)) = (elm); \
+} while (0)
+
+#define SLIST_NEXT(elm, field) ((elm)->field.sle_next)
+
+#define SLIST_REMOVE(head, elm, type, field) do { \
+ if (SLIST_FIRST((head)) == (elm)) { \
+ SLIST_REMOVE_HEAD((head), field); \
+ } \
+ else { \
+ struct type *curelm = SLIST_FIRST((head)); \
+ while (SLIST_NEXT(curelm, field) != (elm)) \
+ curelm = SLIST_NEXT(curelm, field); \
+ SLIST_NEXT(curelm, field) = \
+ SLIST_NEXT(SLIST_NEXT(curelm, field), field); \
+ } \
+} while (0)
+
+#define SLIST_REMOVE_HEAD(head, field) do { \
+ SLIST_FIRST((head)) = SLIST_NEXT(SLIST_FIRST((head)), field); \
+} while (0)
+
+/*
+ * Singly-linked Tail queue declarations.
+ */
+#define STAILQ_HEAD(name, type) \
+struct name { \
+ struct type *stqh_first;/* first element */ \
+ struct type **stqh_last;/* addr of last next element */ \
+}
+
+#define STAILQ_HEAD_INITIALIZER(head) \
+ { NULL, &(head).stqh_first }
+
+#define STAILQ_ENTRY(type) \
+struct { \
+ struct type *stqe_next; /* next element */ \
+}
+
+/*
+ * Singly-linked Tail queue functions.
+ */
+#define STAILQ_CONCAT(head1, head2) do { \
+ if (!STAILQ_EMPTY((head2))) { \
+ *(head1)->stqh_last = (head2)->stqh_first; \
+ (head1)->stqh_last = (head2)->stqh_last; \
+ STAILQ_INIT((head2)); \
+ } \
+} while (0)
+
+#define STAILQ_EMPTY(head) ((head)->stqh_first == NULL)
+
+#define STAILQ_FIRST(head) ((head)->stqh_first)
+
+#define STAILQ_FOREACH(var, head, field) \
+ for((var) = STAILQ_FIRST((head)); \
+ (var); \
+ (var) = STAILQ_NEXT((var), field))
+
+
+#define STAILQ_FOREACH_SAFE(var, head, field, tvar) \
+ for ((var) = STAILQ_FIRST((head)); \
+ (var) && ((tvar) = STAILQ_NEXT((var), field), 1); \
+ (var) = (tvar))
+
+#define STAILQ_INIT(head) do { \
+ STAILQ_FIRST((head)) = NULL; \
+ (head)->stqh_last = &STAILQ_FIRST((head)); \
+} while (0)
+
+#define STAILQ_INSERT_AFTER(head, tqelm, elm, field) do { \
+ if ((STAILQ_NEXT((elm), field) = STAILQ_NEXT((tqelm), field)) == NULL)\
+ (head)->stqh_last = &STAILQ_NEXT((elm), field); \
+ STAILQ_NEXT((tqelm), field) = (elm); \
+} while (0)
+
+#define STAILQ_INSERT_HEAD(head, elm, field) do { \
+ if ((STAILQ_NEXT((elm), field) = STAILQ_FIRST((head))) == NULL) \
+ (head)->stqh_last = &STAILQ_NEXT((elm), field); \
+ STAILQ_FIRST((head)) = (elm); \
+} while (0)
+
+#define STAILQ_INSERT_TAIL(head, elm, field) do { \
+ STAILQ_NEXT((elm), field) = NULL; \
+ *(head)->stqh_last = (elm); \
+ (head)->stqh_last = &STAILQ_NEXT((elm), field); \
+} while (0)
+
+#define STAILQ_LAST(head, type, field) \
+ (STAILQ_EMPTY((head)) ? \
+ NULL : \
+ ((struct type *) \
+ ((char *)((head)->stqh_last) - offsetof(struct type, field))))
+
+#define STAILQ_NEXT(elm, field) ((elm)->field.stqe_next)
+
+#define STAILQ_REMOVE(head, elm, type, field) do { \
+ if (STAILQ_FIRST((head)) == (elm)) { \
+ STAILQ_REMOVE_HEAD((head), field); \
+ } \
+ else { \
+ struct type *curelm = STAILQ_FIRST((head)); \
+ while (STAILQ_NEXT(curelm, field) != (elm)) \
+ curelm = STAILQ_NEXT(curelm, field); \
+ if ((STAILQ_NEXT(curelm, field) = \
+ STAILQ_NEXT(STAILQ_NEXT(curelm, field), field)) == NULL)\
+ (head)->stqh_last = &STAILQ_NEXT((curelm), field);\
+ } \
+} while (0)
+
+#define STAILQ_REMOVE_HEAD(head, field) do { \
+ if ((STAILQ_FIRST((head)) = \
+ STAILQ_NEXT(STAILQ_FIRST((head)), field)) == NULL) \
+ (head)->stqh_last = &STAILQ_FIRST((head)); \
+} while (0)
+
+#define STAILQ_REMOVE_HEAD_UNTIL(head, elm, field) do { \
+ if ((STAILQ_FIRST((head)) = STAILQ_NEXT((elm), field)) == NULL) \
+ (head)->stqh_last = &STAILQ_FIRST((head)); \
+} while (0)
+
+/*
+ * List declarations.
+ */
+#define LIST_HEAD(name, type) \
+struct name { \
+ struct type *lh_first; /* first element */ \
+}
+
+#define LIST_HEAD_INITIALIZER(head) \
+ { NULL }
+
+#define LIST_ENTRY(type) \
+struct { \
+ struct type *le_next; /* next element */ \
+ struct type **le_prev; /* address of previous next element */ \
+}
+
+/*
+ * List functions.
+ */
+
+#define LIST_EMPTY(head) ((head)->lh_first == NULL)
+
+#define LIST_FIRST(head) ((head)->lh_first)
+
+#define LIST_FOREACH(var, head, field) \
+ for ((var) = LIST_FIRST((head)); \
+ (var); \
+ (var) = LIST_NEXT((var), field))
+
+#define LIST_FOREACH_SAFE(var, head, field, tvar) \
+ for ((var) = LIST_FIRST((head)); \
+ (var) && ((tvar) = LIST_NEXT((var), field), 1); \
+ (var) = (tvar))
+
+#define LIST_INIT(head) do { \
+ LIST_FIRST((head)) = NULL; \
+} while (0)
+
+#define LIST_INSERT_AFTER(listelm, elm, field) do { \
+ if ((LIST_NEXT((elm), field) = LIST_NEXT((listelm), field)) != NULL)\
+ LIST_NEXT((listelm), field)->field.le_prev = \
+ &LIST_NEXT((elm), field); \
+ LIST_NEXT((listelm), field) = (elm); \
+ (elm)->field.le_prev = &LIST_NEXT((listelm), field); \
+} while (0)
+
+#define LIST_INSERT_BEFORE(listelm, elm, field) do { \
+ (elm)->field.le_prev = (listelm)->field.le_prev; \
+ LIST_NEXT((elm), field) = (listelm); \
+ *(listelm)->field.le_prev = (elm); \
+ (listelm)->field.le_prev = &LIST_NEXT((elm), field); \
+} while (0)
+
+#define LIST_INSERT_HEAD(head, elm, field) do { \
+ if ((LIST_NEXT((elm), field) = LIST_FIRST((head))) != NULL) \
+ LIST_FIRST((head))->field.le_prev = &LIST_NEXT((elm), field);\
+ LIST_FIRST((head)) = (elm); \
+ (elm)->field.le_prev = &LIST_FIRST((head)); \
+} while (0)
+
+#define LIST_NEXT(elm, field) ((elm)->field.le_next)
+
+#define LIST_REMOVE(elm, field) do { \
+ if (LIST_NEXT((elm), field) != NULL) \
+ LIST_NEXT((elm), field)->field.le_prev = \
+ (elm)->field.le_prev; \
+ *(elm)->field.le_prev = LIST_NEXT((elm), field); \
+} while (0)
+
+/*
+ * Tail queue declarations.
+ */
+#define TAILQ_HEAD(name, type) \
+struct name { \
+ struct type *tqh_first; /* first element */ \
+ struct type **tqh_last; /* addr of last next element */ \
+ TRACEBUF \
+}
+
+#define TAILQ_HEAD_INITIALIZER(head) \
+ { NULL, &(head).tqh_first }
+
+#define TAILQ_ENTRY(type) \
+struct { \
+ struct type *tqe_next; /* next element */ \
+ struct type **tqe_prev; /* address of previous next element */ \
+ TRACEBUF \
+}
+
+/*
+ * Tail queue functions.
+ */
+#define TAILQ_CONCAT(head1, head2, field) do { \
+ if (!TAILQ_EMPTY(head2)) { \
+ *(head1)->tqh_last = (head2)->tqh_first; \
+ (head2)->tqh_first->field.tqe_prev = (head1)->tqh_last; \
+ (head1)->tqh_last = (head2)->tqh_last; \
+ TAILQ_INIT((head2)); \
+ QMD_TRACE_HEAD(head); \
+ QMD_TRACE_HEAD(head2); \
+ } \
+} while (0)
+
+#define TAILQ_EMPTY(head) ((head)->tqh_first == NULL)
+
+#define TAILQ_FIRST(head) ((head)->tqh_first)
+
+#define TAILQ_FOREACH(var, head, field) \
+ for ((var) = TAILQ_FIRST((head)); \
+ (var); \
+ (var) = TAILQ_NEXT((var), field))
+
+#define TAILQ_FOREACH_SAFE(var, head, field, tvar) \
+ for ((var) = TAILQ_FIRST((head)); \
+ (var) && ((tvar) = TAILQ_NEXT((var), field), 1); \
+ (var) = (tvar))
+
+#define TAILQ_FOREACH_REVERSE(var, head, headname, field) \
+ for ((var) = TAILQ_LAST((head), headname); \
+ (var); \
+ (var) = TAILQ_PREV((var), headname, field))
+
+#define TAILQ_FOREACH_REVERSE_SAFE(var, head, headname, field, tvar) \
+ for ((var) = TAILQ_LAST((head), headname); \
+ (var) && ((tvar) = TAILQ_PREV((var), headname, field), 1); \
+ (var) = (tvar))
+
+#define TAILQ_INIT(head) do { \
+ TAILQ_FIRST((head)) = NULL; \
+ (head)->tqh_last = &TAILQ_FIRST((head)); \
+ QMD_TRACE_HEAD(head); \
+} while (0)
+
+#define TAILQ_INSERT_AFTER(head, listelm, elm, field) do { \
+ if ((TAILQ_NEXT((elm), field) = TAILQ_NEXT((listelm), field)) != NULL)\
+ TAILQ_NEXT((elm), field)->field.tqe_prev = \
+ &TAILQ_NEXT((elm), field); \
+ else { \
+ (head)->tqh_last = &TAILQ_NEXT((elm), field); \
+ QMD_TRACE_HEAD(head); \
+ } \
+ TAILQ_NEXT((listelm), field) = (elm); \
+ (elm)->field.tqe_prev = &TAILQ_NEXT((listelm), field); \
+ QMD_TRACE_ELEM(&(elm)->field); \
+ QMD_TRACE_ELEM(&listelm->field); \
+} while (0)
+
+#define TAILQ_INSERT_BEFORE(listelm, elm, field) do { \
+ (elm)->field.tqe_prev = (listelm)->field.tqe_prev; \
+ TAILQ_NEXT((elm), field) = (listelm); \
+ *(listelm)->field.tqe_prev = (elm); \
+ (listelm)->field.tqe_prev = &TAILQ_NEXT((elm), field); \
+ QMD_TRACE_ELEM(&(elm)->field); \
+ QMD_TRACE_ELEM(&listelm->field); \
+} while (0)
+
+#define TAILQ_INSERT_HEAD(head, elm, field) do { \
+ if ((TAILQ_NEXT((elm), field) = TAILQ_FIRST((head))) != NULL) \
+ TAILQ_FIRST((head))->field.tqe_prev = \
+ &TAILQ_NEXT((elm), field); \
+ else \
+ (head)->tqh_last = &TAILQ_NEXT((elm), field); \
+ TAILQ_FIRST((head)) = (elm); \
+ (elm)->field.tqe_prev = &TAILQ_FIRST((head)); \
+ QMD_TRACE_HEAD(head); \
+ QMD_TRACE_ELEM(&(elm)->field); \
+} while (0)
+
+#define TAILQ_INSERT_TAIL(head, elm, field) do { \
+ TAILQ_NEXT((elm), field) = NULL; \
+ (elm)->field.tqe_prev = (head)->tqh_last; \
+ *(head)->tqh_last = (elm); \
+ (head)->tqh_last = &TAILQ_NEXT((elm), field); \
+ QMD_TRACE_HEAD(head); \
+ QMD_TRACE_ELEM(&(elm)->field); \
+} while (0)
+
+#define TAILQ_LAST(head, headname) \
+ (*(((struct headname *)((head)->tqh_last))->tqh_last))
+
+#define TAILQ_NEXT(elm, field) ((elm)->field.tqe_next)
+
+#define TAILQ_PREV(elm, headname, field) \
+ (*(((struct headname *)((elm)->field.tqe_prev))->tqh_last))
+
+#define TAILQ_REMOVE(head, elm, field) do { \
+ if ((TAILQ_NEXT((elm), field)) != NULL) \
+ TAILQ_NEXT((elm), field)->field.tqe_prev = \
+ (elm)->field.tqe_prev; \
+ else { \
+ (head)->tqh_last = (elm)->field.tqe_prev; \
+ QMD_TRACE_HEAD(head); \
+ } \
+ *(elm)->field.tqe_prev = TAILQ_NEXT((elm), field); \
+ TRASHIT((elm)->field.tqe_next); \
+ TRASHIT((elm)->field.tqe_prev); \
+ QMD_TRACE_ELEM(&(elm)->field); \
+} while (0)
+
+
+#ifdef _KERNEL
+
+/*
+ * XXX insque() and remque() are an old way of handling certain queues.
+ * They bogusly assumes that all queue heads look alike.
+ */
+
+struct quehead {
+ struct quehead *qh_link;
+ struct quehead *qh_rlink;
+};
+
+#if defined(__GNUC__) || defined(__INTEL_COMPILER)
+
+static __inline void
+insque(void *a, void *b)
+{
+ struct quehead *element = (struct quehead *)a,
+ *head = (struct quehead *)b;
+
+ element->qh_link = head->qh_link;
+ element->qh_rlink = head;
+ head->qh_link = element;
+ element->qh_link->qh_rlink = element;
+}
+
+static __inline void
+remque(void *a)
+{
+ struct quehead *element = (struct quehead *)a;
+
+ element->qh_link->qh_rlink = element->qh_rlink;
+ element->qh_rlink->qh_link = element->qh_link;
+ element->qh_rlink = 0;
+}
+
+#else /* !(__GNUC__ || __INTEL_COMPILER) */
+
+void insque(void *a, void *b);
+void remque(void *a);
+
+#endif /* __GNUC__ || __INTEL_COMPILER */
+
+#endif /* _KERNEL */
+
+#endif /* !_SYS_QUEUE_H_ */
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/port/linux/include/csi_platform.h b/dom/media/webrtc/transport/third_party/nrappkit/src/port/linux/include/csi_platform.h
new file mode 100644
index 0000000000..8aefaf9249
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/port/linux/include/csi_platform.h
@@ -0,0 +1,55 @@
+/**
+ platform.h
+
+
+ Copyright (C) 2004, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ ekr@rtfm.com Mon Dec 13 17:26:51 2004
+ */
+
+
+#ifndef _platform_h
+#define _platform_h
+
+#include <linux_funcs.h>
+
+#ifdef NR_SOCKET_IS_VOID_PTR
+typedef void* NR_SOCKET;
+#else
+typedef int NR_SOCKET;
+#define NR_SOCKET_READ(sock,buf,count) read((sock),(buf),(count))
+#define NR_SOCKET_WRITE(sock,buf,count) write((sock),(buf),(count))
+#define NR_SOCKET_CLOSE(sock) close(sock)
+#endif
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/port/linux/include/linux_funcs.h b/dom/media/webrtc/transport/third_party/nrappkit/src/port/linux/include/linux_funcs.h
new file mode 100644
index 0000000000..1619136306
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/port/linux/include/linux_funcs.h
@@ -0,0 +1,62 @@
+/**
+ linux_funcs.h
+
+
+ Copyright (C) 2004, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ ekr@rtfm.com Mon Dec 13 16:28:22 2004
+ */
+
+
+#ifndef _linux_funcs_h
+#define _linux_funcs_h
+
+#include <sys/types.h>
+#include <netinet/in.h>
+#include <arpa/inet.h>
+#include <string.h>
+#include <unistd.h>
+
+#define ETHERTYPE_VLAN 0x8100
+
+#define STDIO_BYTES_BUFFERED(fp) (fp->_IO_read_end - fp->_IO_read_ptr)
+
+size_t strlcat(char *dst, const char *src, size_t siz);
+#ifndef strlcpy
+#define strlcpy(a,b,c) \
+ (strncpy((a),(b),(c)), \
+ ((c)<= 0 ? 0 : ((a)[(c)-1]='\0')), \
+ strlen((b)))
+#endif
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/port/linux/include/sys/ttycom.h b/dom/media/webrtc/transport/third_party/nrappkit/src/port/linux/include/sys/ttycom.h
new file mode 100644
index 0000000000..6f8a3066d6
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/port/linux/include/sys/ttycom.h
@@ -0,0 +1,38 @@
+/*
+ *
+ * Copyright (C) 2006, Network Resonance, Inc.
+ * All Rights Reserved
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Network Resonance, Inc. nor the name of any
+ * contributors to this software may be used to endorse or promote
+ * products derived from this software without specific prior written
+ * permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ */
+#ifndef __LINUX_TTYCOM_H
+#define __LINUX_TTYCOM_H
+
+#include <asm/ioctls.h>
+
+#endif
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/port/linux/port-impl.mk b/dom/media/webrtc/transport/third_party/nrappkit/src/port/linux/port-impl.mk
new file mode 100644
index 0000000000..6704cfedf9
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/port/linux/port-impl.mk
@@ -0,0 +1,31 @@
+#
+# Copyright (C) 2006, Network Resonance, Inc.
+# All Rights Reserved
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions
+# are met:
+#
+# 1. Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# 3. Neither the name of Network Resonance, Inc. nor the name of any
+# contributors to this software may be used to endorse or promote
+# products derived from this software without specific prior written
+# permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+#
+#EXTATTR_IMPL=xattr
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/port/win32/include/csi_platform.h b/dom/media/webrtc/transport/third_party/nrappkit/src/port/win32/include/csi_platform.h
new file mode 100644
index 0000000000..27c64c53d9
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/port/win32/include/csi_platform.h
@@ -0,0 +1,107 @@
+/**
+ platform.h
+
+
+ Copyright (C) 2005, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ briank@network-resonance.com Tue Mar 15 14:28:12 PST 2005
+ */
+
+
+#ifndef _csi_platform_h
+#define _csi_platform_h
+
+#ifndef _WIN32_WINNT
+#define _WIN32_WINNT 0x0400 // This prevents weird "'TryEnterCriticalSection': identifier not found"
+ // compiler errors when poco/win32_mutex.h is included
+#endif
+
+#define UINT8 UBLAH_IGNORE_ME_PLEASE
+#define INT8 BLAH_IGNORE_ME_PLEASE
+#include <winsock2.h>
+#undef UINT8
+#undef INT8
+#include <r_types.h>
+#include <errno.h>
+
+#define strcasecmp _stricmp
+#define strncasecmp _strnicmp
+
+#define strcasestr stristr
+
+/* Hack version of strlcpy (in util/util.c) */
+size_t strlcat(char *dst, const char *src, size_t siz);
+
+/* Hack version of getopt() (in util/getopt.c) */
+int getopt(int argc, char *argv[], char *opstring);
+extern char *optarg;
+extern int optind;
+extern int opterr;
+
+/* Hack version of gettimeofday() (in util/util.c) */
+int gettimeofday(struct timeval *tv, void *tz);
+
+#ifdef NR_SOCKET_IS_VOID_PTR
+typedef void* NR_SOCKET;
+#else
+typedef SOCKET NR_SOCKET;
+#define NR_SOCKET_READ(sock,buf,count) recv((sock),(buf),(count),0)
+#define NR_SOCKET_WRITE(sock,buf,count) send((sock),(buf),(count),0)
+#define NR_SOCKET_CLOSE(sock) closesocket(sock)
+#endif
+
+#ifndef EHOSTUNREACH
+#define EHOSTUNREACH WSAEHOSTUNREACH
+#endif
+
+#define LOG_EMERG 0
+#define LOG_ALERT 1
+#define LOG_CRIT 2
+#define LOG_ERR 3
+#define LOG_WARNING 4
+#define LOG_NOTICE 5
+#define LOG_INFO 6
+#define LOG_DEBUG 7
+
+// Until we refine the Windows port....
+
+#define in_addr_t UINT4
+
+#ifndef strlcpy
+#define strlcpy(a,b,c) \
+ (strncpy((a),(b),(c)), \
+ ((c)<= 0 ? 0 : ((a)[(c)-1]='\0')), \
+ strlen((b)))
+#endif
+
+#endif /* _csi_platform_h */
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/registry/c2ru.c b/dom/media/webrtc/transport/third_party/nrappkit/src/registry/c2ru.c
new file mode 100644
index 0000000000..3e71ca37d9
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/registry/c2ru.c
@@ -0,0 +1,320 @@
+/*
+ *
+ * c2ru.c
+ *
+ * $Source: /Users/ekr/tmp/nrappkit-dump/nrappkit/src/registry/c2ru.c,v $
+ * $Revision: 1.3 $
+ * $Date: 2007/06/26 22:37:50 $
+ *
+ * c2r utility methods
+ *
+ *
+ * Copyright (C) 2005, Network Resonance, Inc.
+ * Copyright (C) 2006, Network Resonance, Inc.
+ * All Rights Reserved
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Network Resonance, Inc. nor the name of any
+ * contributors to this software may be used to endorse or promote
+ * products derived from this software without specific prior written
+ * permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ *
+ */
+
+#include <sys/queue.h>
+#include <string.h>
+#include <registry.h>
+#include "nr_common.h"
+#include <r_errors.h>
+#include <r_macros.h>
+#include <ctype.h>
+#include "c2ru.h"
+
+
+#define NRGET(func, type, get) \
+int \
+func(NR_registry parent, char *child, type **out) \
+{ \
+ int r, _status; \
+ NR_registry registry; \
+ type tmp; \
+ \
+ if ((r = nr_c2ru_make_registry(parent, child, registry))) \
+ ABORT(r); \
+ \
+ if ((r = get(registry, &tmp))) { \
+ if (r != R_NOT_FOUND) \
+ ABORT(r); \
+ *out = 0; \
+ } \
+ else { \
+ *out = RCALLOC(sizeof(tmp)); \
+ if (*out == 0) \
+ ABORT(R_NO_MEMORY); \
+ **out = tmp; \
+ } \
+ \
+ _status = 0; \
+abort: \
+ return (_status); \
+}
+
+int
+nr_c2ru_get_char(NR_registry parent, char *child, char **out)
+{
+ int r, _status;
+ NR_registry registry;
+ char tmp;
+
+ if ((r = nr_c2ru_make_registry(parent, child, registry)))
+ ABORT(r);
+
+ if ((r = NR_reg_get_char(registry, &tmp))) {
+ if (r != R_NOT_FOUND)
+ ABORT(r);
+ *out = 0;
+ }
+ else {
+ *out = RCALLOC(sizeof(tmp));
+ if (*out == 0)
+ ABORT(R_NO_MEMORY);
+ **out = tmp;
+ }
+
+ _status = 0;
+abort:
+ return (_status);
+}
+NRGET(nr_c2ru_get_uchar, UCHAR, NR_reg_get_uchar)
+NRGET(nr_c2ru_get_int2, INT2, NR_reg_get_int2)
+NRGET(nr_c2ru_get_uint2, UINT2, NR_reg_get_uint2)
+NRGET(nr_c2ru_get_int4, INT4, NR_reg_get_int4)
+NRGET(nr_c2ru_get_uint4, UINT4, NR_reg_get_uint4)
+NRGET(nr_c2ru_get_int8, INT8, NR_reg_get_int8)
+NRGET(nr_c2ru_get_uint8, UINT8, NR_reg_get_uint8)
+NRGET(nr_c2ru_get_double, double, NR_reg_get_double)
+NRGET(nr_c2ru_get_string, char*, NR_reg_alloc_string)
+NRGET(nr_c2ru_get_data, Data, NR_reg_alloc_data)
+
+
+#define NRSET(func, type, set) \
+int \
+func(NR_registry parent, char *child, type *in) \
+{ \
+ int r, _status; \
+ NR_registry registry; \
+ \
+ if (in == 0) \
+ return 0; \
+ \
+ if ((r = nr_c2ru_make_registry(parent, child, registry))) \
+ ABORT(r); \
+ \
+ if ((r = set(registry, *in))) \
+ ABORT(r); \
+ \
+ _status = 0; \
+abort: \
+ return (_status); \
+}
+
+NRSET(nr_c2ru_set_char, char, NR_reg_set_char)
+NRSET(nr_c2ru_set_uchar, UCHAR, NR_reg_set_uchar)
+NRSET(nr_c2ru_set_int2, INT2, NR_reg_set_int2)
+NRSET(nr_c2ru_set_uint2, UINT2, NR_reg_set_uint2)
+NRSET(nr_c2ru_set_int4, INT4, NR_reg_set_int4)
+NRSET(nr_c2ru_set_uint4, UINT4, NR_reg_set_uint4)
+NRSET(nr_c2ru_set_int8, INT8, NR_reg_set_int8)
+NRSET(nr_c2ru_set_uint8, UINT8, NR_reg_set_uint8)
+NRSET(nr_c2ru_set_double, double, NR_reg_set_double)
+NRSET(nr_c2ru_set_string, char*, NR_reg_set_string)
+
+int
+nr_c2ru_set_data(NR_registry parent, char *child, Data *in)
+{
+ int r, _status;
+ NR_registry registry;
+
+ if (in == 0)
+ return 0;
+
+ if ((r = nr_c2ru_make_registry(parent, child, registry)))
+ ABORT(r);
+
+ if ((r = NR_reg_set_bytes(registry, in->data, in->len)))
+ ABORT(r);
+
+ _status = 0;
+abort:
+ return (_status);
+}
+
+#define NRFREE(func, type) \
+int \
+func(type *in) \
+{ \
+ if (in) \
+ RFREE(in); \
+ return 0; \
+}
+
+NRFREE(nr_c2ru_free_char, char)
+NRFREE(nr_c2ru_free_uchar, UCHAR)
+NRFREE(nr_c2ru_free_int2, INT2)
+NRFREE(nr_c2ru_free_uint2, UINT2)
+NRFREE(nr_c2ru_free_int4, INT4)
+NRFREE(nr_c2ru_free_uint4, UINT4)
+NRFREE(nr_c2ru_free_int8, INT8)
+NRFREE(nr_c2ru_free_uint8, UINT8)
+NRFREE(nr_c2ru_free_double, double)
+
+
+int
+nr_c2ru_free_string(char **in)
+{
+ if (*in)
+ RFREE(*in);
+ if (in)
+ RFREE(in);
+ return 0;
+}
+
+int
+nr_c2ru_free_data(Data *in)
+{
+ int r, _status;
+
+ if (in) {
+ if ((r=r_data_destroy(&in)))
+ ABORT(r);
+ }
+
+ _status = 0;
+abort:
+ return (_status);
+}
+
+int
+nr_c2ru_get_children(NR_registry parent, char *child, void *ptr, size_t size, int (*get)(NR_registry, void*))
+{
+ int r, _status;
+ NR_registry registry;
+ unsigned int count;
+ unsigned int i;
+ NR_registry name;
+ struct entry { TAILQ_ENTRY(entry) entries; } *entry;
+ TAILQ_HEAD(, entry) *tailq = (void*)ptr;
+
+ TAILQ_INIT(tailq);
+
+ if ((r=nr_c2ru_make_registry(parent, child, registry)))
+ ABORT(r);
+
+ if ((r=NR_reg_get_child_count(registry, &count))) {
+ if (r != R_NOT_FOUND)
+ ABORT(r);
+ }
+ else {
+ for (i = 0; i < count; ++i) {
+ if ((r=NR_reg_get_child_registry(registry, i, name))) {
+ /* ignore R_NOT_FOUND errors */
+ if (r == R_NOT_FOUND)
+ continue;
+ else
+ ABORT(r);
+ }
+
+ if ((r=get(name, &entry))) {
+ /* ignore R_NOT_FOUND errors */
+ if (r == R_NOT_FOUND)
+ continue;
+ else
+ ABORT(r);
+ }
+
+ TAILQ_INSERT_TAIL(tailq, entry, entries);
+ }
+ }
+
+ _status = 0;
+abort:
+ return (_status);
+}
+
+int
+nr_c2ru_set_children(NR_registry parent, char *child, void *ptr, int (*set)(NR_registry, void*), int (*label)(NR_registry, void*, char[NR_REG_MAX_NR_REGISTRY_LEN]))
+{
+ int r, _status;
+ NR_registry registry;
+ int i;
+ NR_registry name;
+ char buffer[NR_REG_MAX_NR_REGISTRY_LEN];
+ struct entry { TAILQ_ENTRY(entry) entries; } *entry;
+ TAILQ_HEAD(, entry) *tailq = (void*)ptr;
+
+ if ((r=nr_c2ru_make_registry(parent, child, registry)))
+ ABORT(r);
+
+ (void)NR_reg_del(registry);
+
+ i = 0;
+ TAILQ_FOREACH(entry, tailq, entries) {
+ if (label == 0 || (r=label(registry, entry, buffer))) {
+ snprintf(buffer, sizeof(buffer), "%d", i);
+ }
+ if ((r=nr_c2ru_make_registry(registry, buffer, name)))
+ ABORT(r);
+
+ if ((r=set(name, entry)))
+ ABORT(r);
+
+ ++i;
+ }
+
+ _status = 0;
+abort:
+ return (_status);
+}
+
+int
+nr_c2ru_free_children(void *ptr, int (*free)(void*))
+{
+ struct entry { TAILQ_ENTRY(entry) entries; } *entry;
+ TAILQ_HEAD(, entry) *tailq = (void*)ptr;
+
+ while (! TAILQ_EMPTY(tailq)) {
+ entry = TAILQ_FIRST(tailq);
+ TAILQ_REMOVE(tailq, entry, entries);
+ (void)free(entry);
+ }
+
+ return 0;
+}
+
+/* requires parent already in legal form */
+int
+nr_c2ru_make_registry(NR_registry parent, char *child, NR_registry out)
+{
+ return NR_reg_make_registry(parent, child, out);
+}
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/registry/c2ru.h b/dom/media/webrtc/transport/third_party/nrappkit/src/registry/c2ru.h
new file mode 100644
index 0000000000..0f8c38ecc2
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/registry/c2ru.h
@@ -0,0 +1,96 @@
+/*
+ *
+ * c2ru.h
+ *
+ * $Source: /Users/ekr/tmp/nrappkit-dump/nrappkit/src/registry/c2ru.h,v $
+ * $Revision: 1.2 $
+ * $Date: 2006/08/16 19:39:13 $
+ *
+ * c2r utility methods
+ *
+ *
+ * Copyright (C) 2005, Network Resonance, Inc.
+ * Copyright (C) 2006, Network Resonance, Inc.
+ * All Rights Reserved
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Network Resonance, Inc. nor the name of any
+ * contributors to this software may be used to endorse or promote
+ * products derived from this software without specific prior written
+ * permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ *
+ */
+
+#ifndef __C2RU_H__
+#define __C2RU_H__
+
+#include <sys/types.h>
+#include <r_types.h>
+#include <r_data.h>
+#include "registry_int.h"
+
+int nr_c2ru_get_char(NR_registry parent, char *child, char **out);
+int nr_c2ru_get_uchar(NR_registry parent, char *child, UCHAR **out);
+int nr_c2ru_get_int2(NR_registry parent, char *child, INT2 **out);
+int nr_c2ru_get_uint2(NR_registry parent, char *child, UINT2 **out);
+int nr_c2ru_get_int4(NR_registry parent, char *child, INT4 **out);
+int nr_c2ru_get_uint4(NR_registry parent, char *child, UINT4 **out);
+int nr_c2ru_get_int8(NR_registry parent, char *child, INT8 **out);
+int nr_c2ru_get_uint8(NR_registry parent, char *child, UINT8 **out);
+int nr_c2ru_get_double(NR_registry parent, char *child, double **out);
+int nr_c2ru_get_data(NR_registry parent, char *child, Data **out);
+int nr_c2ru_get_string(NR_registry parent, char *child, char ***out);
+
+int nr_c2ru_set_char(NR_registry parent, char *child, char *data);
+int nr_c2ru_set_uchar(NR_registry parent, char *child, UCHAR *data);
+int nr_c2ru_set_int2(NR_registry parent, char *child, INT2 *data);
+int nr_c2ru_set_uint2(NR_registry parent, char *child, UINT2 *data);
+int nr_c2ru_set_int4(NR_registry parent, char *child, INT4 *data);
+int nr_c2ru_set_uint4(NR_registry parent, char *child, UINT4 *data);
+int nr_c2ru_set_int8(NR_registry parent, char *child, INT8 *data);
+int nr_c2ru_set_uint8(NR_registry parent, char *child, UINT8 *data);
+int nr_c2ru_set_double(NR_registry parent, char *child, double *data);
+int nr_c2ru_set_registry(NR_registry parent, char *child);
+int nr_c2ru_set_data(NR_registry parent, char *child, Data *data);
+int nr_c2ru_set_string(NR_registry parent, char *child, char **data);
+
+int nr_c2ru_free_char(char *data);
+int nr_c2ru_free_uchar(UCHAR *data);
+int nr_c2ru_free_int2(INT2 *data);
+int nr_c2ru_free_uint2(UINT2 *data);
+int nr_c2ru_free_int4(INT4 *data);
+int nr_c2ru_free_uint4(UINT4 *data);
+int nr_c2ru_free_int8(INT8 *data);
+int nr_c2ru_free_uint8(UINT8 *data);
+int nr_c2ru_free_double(double *data);
+int nr_c2ru_free_data(Data *data);
+int nr_c2ru_free_string(char **data);
+
+int nr_c2ru_get_children(NR_registry parent, char *child, void *ptr, size_t size, int (*get)(NR_registry, void*));
+int nr_c2ru_set_children(NR_registry parent, char *child, void *ptr, int (*set)(NR_registry, void*), int (*label)(NR_registry, void*, char[NR_REG_MAX_NR_REGISTRY_LEN]));
+int nr_c2ru_free_children(void *ptr, int (*free)(void*));
+
+int nr_c2ru_make_registry(NR_registry parent, char *child, NR_registry out);
+
+#endif
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/registry/registry.c b/dom/media/webrtc/transport/third_party/nrappkit/src/registry/registry.c
new file mode 100644
index 0000000000..709b1c3fb7
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/registry/registry.c
@@ -0,0 +1,604 @@
+/*
+ *
+ * registry.c
+ *
+ * $Source: /Users/ekr/tmp/nrappkit-dump/nrappkit/src/registry/registry.c,v $
+ * $Revision: 1.6 $
+ * $Date: 2007/11/21 00:09:12 $
+ *
+ * Datastore for tracking configuration and related info.
+ *
+ *
+ * Copyright (C) 2005, Network Resonance, Inc.
+ * Copyright (C) 2006, Network Resonance, Inc.
+ * All Rights Reserved
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Network Resonance, Inc. nor the name of any
+ * contributors to this software may be used to endorse or promote
+ * products derived from this software without specific prior written
+ * permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ *
+ */
+
+#include <assert.h>
+#include <string.h>
+#ifndef _MSC_VER
+#include <strings.h>
+#include <sys/param.h>
+#include <netinet/in.h>
+#endif
+#ifdef OPENSSL
+#include <openssl/ssl.h>
+#endif
+#include <ctype.h>
+#include "registry.h"
+#include "registry_int.h"
+#include "registry_vtbl.h"
+#include "r_assoc.h"
+#include "nr_common.h"
+#include "r_log.h"
+#include "r_errors.h"
+#include "r_macros.h"
+#include "c2ru.h"
+
+/* vtbl used to switch hit between local and remote invocations */
+static nr_registry_module *reg_vtbl = 0;
+
+/* must be in the order the types are numbered */
+static char *typenames[] = { "char", "UCHAR", "INT2", "UINT2", "INT4", "UINT4", "INT8", "UINT8", "double", "Data", "string", "registry" };
+
+int NR_LOG_REGISTRY=0;
+
+NR_registry NR_TOP_LEVEL_REGISTRY = "";
+
+int
+NR_reg_init(void *mode)
+{
+ int r, _status;
+ nr_registry_module *module = (nr_registry_module*)mode;
+#ifdef SANITY_CHECKS
+ NR_registry registry;
+#endif
+
+ if (reg_vtbl) {
+ if (reg_vtbl != module) {
+ r_log(LOG_GENERIC,LOG_ERR,"Can't reinitialize registry in different mode");
+ ABORT(R_INTERNAL);
+ }
+
+ return(0);
+ }
+
+ reg_vtbl = module;
+
+ if ((r=reg_vtbl->vtbl->init(mode)))
+ ABORT(r);
+
+#ifdef SANITY_CHECKS
+ if ((r=NR_reg_get_registry(NR_TOP_LEVEL_REGISTRY, registry)))
+ ABORT(r);
+ assert(strcmp(registry, NR_TOP_LEVEL_REGISTRY) == 0);
+#endif
+
+ r_log_init();
+ r_log_register("registry",&NR_LOG_REGISTRY);
+
+ _status=0;
+ abort:
+ r_log(NR_LOG_REGISTRY,
+ (_status ? LOG_ERR : LOG_INFO),
+ (_status ? "Couldn't initialize registry" : "Initialized registry"));
+ return(_status);
+}
+
+int
+NR_reg_initted(void)
+{
+ return reg_vtbl!=0;
+}
+
+#define NRREGGET(func, method, type) \
+int \
+func(NR_registry name, type *out) \
+{ \
+ return reg_vtbl->vtbl->method(name, out); \
+}
+
+NRREGGET(NR_reg_get_char, get_char, char)
+NRREGGET(NR_reg_get_uchar, get_uchar, UCHAR)
+NRREGGET(NR_reg_get_int2, get_int2, INT2)
+NRREGGET(NR_reg_get_uint2, get_uint2, UINT2)
+NRREGGET(NR_reg_get_int4, get_int4, INT4)
+NRREGGET(NR_reg_get_uint4, get_uint4, UINT4)
+NRREGGET(NR_reg_get_int8, get_int8, INT8)
+NRREGGET(NR_reg_get_uint8, get_uint8, UINT8)
+NRREGGET(NR_reg_get_double, get_double, double)
+
+int
+NR_reg_get_registry(NR_registry name, NR_registry out)
+{
+ return reg_vtbl->vtbl->get_registry(name, out);
+}
+
+int
+NR_reg_get_bytes(NR_registry name, UCHAR *out, size_t size, size_t *length)
+{
+ return reg_vtbl->vtbl->get_bytes(name, out, size, length);
+}
+
+int
+NR_reg_get_string(NR_registry name, char *out, size_t size)
+{
+ return reg_vtbl->vtbl->get_string(name, out, size);
+}
+
+int
+NR_reg_get_length(NR_registry name, size_t *length)
+{
+ return reg_vtbl->vtbl->get_length(name, length);
+}
+
+int
+NR_reg_get_type(NR_registry name, NR_registry_type type)
+{
+ return reg_vtbl->vtbl->get_type(name, type);
+}
+
+#define NRREGSET(func, method, type) \
+int \
+func(NR_registry name, type data) \
+{ \
+ return reg_vtbl->vtbl->method(name, data); \
+}
+
+NRREGSET(NR_reg_set_char, set_char, char)
+NRREGSET(NR_reg_set_uchar, set_uchar, UCHAR)
+NRREGSET(NR_reg_set_int2, set_int2, INT2)
+NRREGSET(NR_reg_set_uint2, set_uint2, UINT2)
+NRREGSET(NR_reg_set_int4, set_int4, INT4)
+NRREGSET(NR_reg_set_uint4, set_uint4, UINT4)
+NRREGSET(NR_reg_set_int8, set_int8, INT8)
+NRREGSET(NR_reg_set_uint8, set_uint8, UINT8)
+NRREGSET(NR_reg_set_double, set_double, double)
+NRREGSET(NR_reg_set_string, set_string, char*)
+
+int
+NR_reg_set_registry(NR_registry name)
+{
+ return reg_vtbl->vtbl->set_registry(name);
+}
+
+int
+NR_reg_set_bytes(NR_registry name, unsigned char *data, size_t length)
+{
+ return reg_vtbl->vtbl->set_bytes(name, data, length);
+}
+
+
+int
+NR_reg_del(NR_registry name)
+{
+ return reg_vtbl->vtbl->del(name);
+}
+
+int
+NR_reg_fin(NR_registry name)
+{
+ return reg_vtbl->vtbl->fin(name);
+}
+
+int
+NR_reg_get_child_count(NR_registry parent, unsigned int *count)
+{
+ assert(sizeof(count) == sizeof(size_t));
+ return reg_vtbl->vtbl->get_child_count(parent, (size_t*)count);
+}
+
+int
+NR_reg_get_child_registry(NR_registry parent, unsigned int i, NR_registry child)
+{
+ int r, _status;
+ size_t count;
+ NR_registry *children=0;
+
+ if ((r=reg_vtbl->vtbl->get_child_count(parent, &count)))
+ ABORT(r);
+
+ if (i >= count)
+ ABORT(R_NOT_FOUND);
+ else {
+ count++;
+ children = (NR_registry *)RCALLOC(count * sizeof(NR_registry));
+ if (!children)
+ ABORT(R_NO_MEMORY);
+
+ if ((r=reg_vtbl->vtbl->get_children(parent, children, count, &count)))
+ ABORT(r);
+
+ if (i >= count)
+ ABORT(R_NOT_FOUND);
+
+ strncpy(child, children[i], sizeof(NR_registry));
+ }
+
+ _status=0;
+ abort:
+ RFREE(children);
+ return(_status);
+}
+
+int
+NR_reg_get_children(NR_registry parent, NR_registry *children, size_t size, size_t *length)
+{
+ return reg_vtbl->vtbl->get_children(parent, children, size, length);
+}
+
+int
+NR_reg_dump()
+{
+ int r, _status;
+
+ if ((r=reg_vtbl->vtbl->dump(0)))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ return(_status);
+}
+
+// convenience methods, call RFREE on the returned data
+int
+NR_reg_alloc_data(NR_registry name, Data *data)
+{
+ int r, _status;
+ size_t length;
+ UCHAR *tmp = 0;
+ size_t sanity_check;
+
+ if ((r=NR_reg_get_length(name, &length)))
+ ABORT(r);
+
+ if (!(tmp = (void*)RMALLOC(length)))
+ ABORT(R_NO_MEMORY);
+
+ if ((r=NR_reg_get_bytes(name, tmp, length, &sanity_check)))
+ ABORT(r);
+
+ assert(length == sanity_check);
+
+ data->len = length;
+ data->data = tmp;
+
+ _status=0;
+ abort:
+ if (_status) {
+ if (tmp) RFREE(tmp);
+ }
+ return(_status);
+}
+
+int
+NR_reg_alloc_string(NR_registry name, char **data)
+{
+ int r, _status;
+ size_t length;
+ char *tmp = 0;
+
+ if ((r=NR_reg_get_length(name, &length)))
+ ABORT(r);
+
+ if (!(tmp = (void*)RMALLOC(length+1)))
+ ABORT(R_NO_MEMORY);
+
+ if ((r=NR_reg_get_string(name, tmp, length+1)))
+ ABORT(r);
+
+ assert(length == strlen(tmp));
+
+ *data = tmp;
+
+ _status=0;
+ abort:
+ if (_status) {
+ if (tmp) RFREE(tmp);
+ }
+ return(_status);
+}
+
+
+char *
+nr_reg_type_name(int type)
+{
+ if ((type < NR_REG_TYPE_CHAR) || (type > NR_REG_TYPE_REGISTRY))
+ return(NULL);
+
+ return(typenames[type]);
+}
+
+int
+nr_reg_compute_type(char *typename, int *type)
+{
+ int _status;
+ size_t i;
+
+#ifdef SANITY_CHECKS
+ assert(!strcasecmp(typenames[NR_REG_TYPE_CHAR], "char"));
+ assert(!strcasecmp(typenames[NR_REG_TYPE_UCHAR], "UCHAR"));
+ assert(!strcasecmp(typenames[NR_REG_TYPE_INT2], "INT2"));
+ assert(!strcasecmp(typenames[NR_REG_TYPE_UINT2], "UINT2"));
+ assert(!strcasecmp(typenames[NR_REG_TYPE_INT4], "INT4"));
+ assert(!strcasecmp(typenames[NR_REG_TYPE_UINT4], "UINT4"));
+ assert(!strcasecmp(typenames[NR_REG_TYPE_INT8], "INT8"));
+ assert(!strcasecmp(typenames[NR_REG_TYPE_UINT8], "UINT8"));
+ assert(!strcasecmp(typenames[NR_REG_TYPE_DOUBLE], "double"));
+ assert(!strcasecmp(typenames[NR_REG_TYPE_BYTES], "Data"));
+ assert(!strcasecmp(typenames[NR_REG_TYPE_STRING], "string"));
+ assert(!strcasecmp(typenames[NR_REG_TYPE_REGISTRY], "registry"));
+ assert(sizeof(typenames)/sizeof(*typenames) == (NR_REG_TYPE_REGISTRY+1));
+#endif
+
+ for (i = 0; i < sizeof(typenames)/sizeof(*typenames); ++i) {
+ if (!strcasecmp(typenames[i], typename)) {
+ *type = i;
+ return 0;
+ }
+ }
+ ABORT(R_BAD_ARGS);
+
+ _status=0;
+ abort:
+ return(_status);
+}
+
+/* More convenience functions: the same as their parents but they
+ take a prefix and a suffix */
+#define NRGET2(func, type, get) \
+int \
+func(NR_registry parent, char *child, type *out) \
+{ \
+ int r, _status; \
+ NR_registry registry; \
+ \
+ if ((r = NR_reg_make_registry(parent, child, registry))) \
+ ABORT(r); \
+ \
+ if ((r = get(registry, out))) { \
+ ABORT(r); \
+ } \
+ \
+ _status = 0; \
+abort: \
+ return (_status); \
+}
+
+NRGET2(NR_reg_get2_char, char, NR_reg_get_char)
+NRGET2(NR_reg_get2_uchar, UCHAR, NR_reg_get_uchar)
+NRGET2(NR_reg_get2_int2, INT2, NR_reg_get_int2)
+NRGET2(NR_reg_get2_uint2, UINT2, NR_reg_get_uint2)
+NRGET2(NR_reg_get2_int4, INT4, NR_reg_get_int4)
+NRGET2(NR_reg_get2_uint4, UINT4, NR_reg_get_uint4)
+NRGET2(NR_reg_get2_int8, INT8, NR_reg_get_int8)
+NRGET2(NR_reg_get2_uint8, UINT8, NR_reg_get_uint8)
+NRGET2(NR_reg_get2_double, double, NR_reg_get_double)
+NRGET2(NR_reg_alloc2_string, char*, NR_reg_alloc_string)
+NRGET2(NR_reg_alloc2_data, Data, NR_reg_alloc_data)
+
+int
+NR_reg_get2_bytes(NR_registry parent, char *child, UCHAR *out, size_t size, size_t *length)
+{
+ int r, _status;
+ NR_registry registry;
+
+ if ((r=NR_reg_make_registry(parent, child, registry)))
+ ABORT(r);
+
+ if ((r=NR_reg_get_bytes(registry, out, size, length)))
+ ABORT(r);
+
+ _status = 0;
+abort:
+ return (_status);
+}
+
+int
+NR_reg_get2_string(NR_registry parent, char *child, char *out, size_t size)
+{
+ int r, _status;
+ NR_registry registry;
+
+ if ((r=NR_reg_make_registry(parent, child, registry)))
+ ABORT(r);
+
+ if ((r=NR_reg_get_string(registry, out, size)))
+ ABORT(r);
+
+ _status = 0;
+abort:
+ return (_status);
+}
+
+/* More convenience functions: the same as their parents but they
+ take a prefix and a suffix */
+#define NRSET2(func, type, set) \
+int \
+func(NR_registry parent, char *child, type in) \
+{ \
+ int r, _status; \
+ NR_registry registry; \
+ \
+ if ((r = NR_reg_make_registry(parent, child, registry))) \
+ ABORT(r); \
+ \
+ if ((r = set(registry, in))) { \
+ ABORT(r); \
+ } \
+ \
+ _status = 0; \
+abort: \
+ return (_status); \
+}
+
+NRSET2(NR_reg_set2_char, char, NR_reg_set_char)
+NRSET2(NR_reg_set2_uchar, UCHAR, NR_reg_set_uchar)
+NRSET2(NR_reg_set2_int2, INT2, NR_reg_set_int2)
+NRSET2(NR_reg_set2_uint2, UINT2, NR_reg_set_uint2)
+NRSET2(NR_reg_set2_int4, INT4, NR_reg_set_int4)
+NRSET2(NR_reg_set2_uint4, UINT4, NR_reg_set_uint4)
+NRSET2(NR_reg_set2_int8, INT8, NR_reg_set_int8)
+NRSET2(NR_reg_set2_uint8, UINT8, NR_reg_set_uint8)
+NRSET2(NR_reg_set2_double, double, NR_reg_set_double)
+NRSET2(NR_reg_set2_string, char*, NR_reg_set_string)
+
+int
+NR_reg_set2_bytes(NR_registry prefix, char *name, UCHAR *data, size_t length)
+{
+ int r, _status;
+ NR_registry registry;
+
+ if ((r = NR_reg_make_registry(prefix, name, registry)))
+ ABORT(r);
+
+ if ((r = NR_reg_set_bytes(registry, data, length)))
+ ABORT(r);
+
+ _status = 0;
+abort:
+ return (_status);
+}
+
+
+int
+NR_reg_make_child_registry(NR_registry parent, NR_registry descendant, unsigned int generation, NR_registry child)
+{
+ int _status;
+ size_t length;
+
+ length = strlen(parent);
+
+ if (strncasecmp(parent, descendant, length))
+ ABORT(R_BAD_ARGS);
+
+ while (descendant[length] != '\0') {
+ if (descendant[length] == '.') {
+ if (generation == 0)
+ break;
+
+ --generation;
+ }
+
+ ++length;
+ if (length >= sizeof(NR_registry))
+ ABORT(R_BAD_ARGS);
+ }
+
+ strncpy(child, descendant, length);
+ child[length] = '\0';
+
+ _status=0;
+ abort:
+ return(_status);
+}
+
+int
+NR_reg_get2_child_count(NR_registry base, NR_registry name, unsigned int *count)
+ {
+ int r, _status;
+ NR_registry registry;
+
+ if ((r=nr_c2ru_make_registry(base, name, registry)))
+ ABORT(r);
+
+ if (r=NR_reg_get_child_count(registry,count))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int
+NR_reg_get2_child_registry(NR_registry base, NR_registry name, unsigned int i, NR_registry child)
+ {
+ int r, _status;
+ NR_registry registry;
+
+ if ((r=nr_c2ru_make_registry(base, name, registry)))
+ ABORT(r);
+
+ if (r=NR_reg_get_child_registry(registry, i, child))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+
+/* requires parent already in legal form */
+int
+NR_reg_make_registry(NR_registry parent, char *child, NR_registry out)
+{
+ int r, _status;
+ size_t plen;
+ size_t clen;
+ char *c;
+ size_t i;
+
+ if ((r=nr_reg_is_valid(parent)))
+ ABORT(r);
+
+ if (*child == '.')
+ ABORT(R_BAD_ARGS);
+
+ clen = strlen(child);
+ if (!clen)
+ ABORT(R_BAD_ARGS);
+ plen = strlen(parent);
+ if ((plen + clen + 2) > sizeof(NR_registry))
+ ABORT(R_BAD_ARGS);
+
+ if (out != parent)
+ strcpy(out, parent);
+
+ c = &(out[plen]);
+
+ if (parent[0] != '\0') {
+ *c = '.';
+ ++c;
+ }
+
+ for (i = 0; i < clen; ++i, ++c) {
+ *c = child[i];
+ if (isspace(*c) || *c == '.' || *c == '/' || ! isprint(*c))
+ *c = '_';
+ }
+
+ *c = '\0';
+
+ _status = 0;
+abort:
+ return _status;
+}
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/registry/registry.h b/dom/media/webrtc/transport/third_party/nrappkit/src/registry/registry.h
new file mode 100644
index 0000000000..b48893ba72
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/registry/registry.h
@@ -0,0 +1,154 @@
+/*
+ *
+ * registry.h
+ *
+ * $Source: /Users/ekr/tmp/nrappkit-dump/nrappkit/src/registry/registry.h,v $
+ * $Revision: 1.3 $
+ * $Date: 2007/07/17 17:58:16 $
+ *
+ * Datastore for tracking configuration and related info.
+ *
+ *
+ * Copyright (C) 2005, Network Resonance, Inc.
+ * Copyright (C) 2006, Network Resonance, Inc.
+ * All Rights Reserved
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Network Resonance, Inc. nor the name of any
+ * contributors to this software may be used to endorse or promote
+ * products derived from this software without specific prior written
+ * permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ *
+ */
+
+#ifndef __REGISTRY_H__
+#define __REGISTRY_H__
+
+#include <stdio.h>
+#include <sys/types.h>
+#include <r_types.h>
+#include <r_data.h>
+
+#define NR_REG_MAX_NR_REGISTRY_LEN 128
+#define NR_REG_MAX_TYPE_LEN 32
+
+typedef char NR_registry[NR_REG_MAX_NR_REGISTRY_LEN];
+typedef char NR_registry_type[NR_REG_MAX_TYPE_LEN];
+
+extern NR_registry NR_TOP_LEVEL_REGISTRY;
+
+extern void *NR_REG_MODE_LOCAL;
+extern void *NR_REG_MODE_REMOTE;
+
+int NR_reg_init(void *mode);
+
+int NR_reg_initted(void);
+
+int NR_reg_get_char(NR_registry name, char *out);
+int NR_reg_get_uchar(NR_registry name, UCHAR *out);
+int NR_reg_get_int2(NR_registry name, INT2 *out);
+int NR_reg_get_uint2(NR_registry name, UINT2 *out);
+int NR_reg_get_int4(NR_registry name, INT4 *out);
+int NR_reg_get_uint4(NR_registry name, UINT4 *out);
+int NR_reg_get_int8(NR_registry name, INT8 *out);
+int NR_reg_get_uint8(NR_registry name, UINT8 *out);
+int NR_reg_get_double(NR_registry name, double *out);
+int NR_reg_get_registry(NR_registry name, NR_registry out);
+
+int NR_reg_get_bytes(NR_registry name, UCHAR *out, size_t size, size_t *length);
+int NR_reg_get_string(NR_registry name, char *out, size_t size);
+int NR_reg_get_length(NR_registry name, size_t *length);
+int NR_reg_get_type(NR_registry name, NR_registry_type type);
+
+
+int NR_reg_get2_char(NR_registry prefix, char *name, char *);
+int NR_reg_get2_uchar(NR_registry prefix, char *name, UCHAR *);
+int NR_reg_get2_int2(NR_registry prefix, char *name, INT2 *);
+int NR_reg_get2_uint2(NR_registry prefix, char *name, UINT2 *);
+int NR_reg_get2_int4(NR_registry prefix, char *name, INT4 *);
+int NR_reg_get2_uint4(NR_registry prefix, char *name, UINT4 *);
+int NR_reg_get2_int8(NR_registry prefix, char *name, INT8 *);
+int NR_reg_get2_uint8(NR_registry prefix, char *name, UINT8 *);
+int NR_reg_get2_double(NR_registry prefix, char *name, double *);
+int NR_reg_get2_bytes(NR_registry prefix, char *name, UCHAR *out, size_t size, size_t *length);
+int NR_reg_get2_string(NR_registry prefix, char *name, char *out, size_t size);
+
+int NR_reg_alloc2_string(NR_registry prefix, char *name, char **);
+int NR_reg_alloc2_data(NR_registry prefix, char *name, Data *);
+
+int NR_reg_set_char(NR_registry name, char data);
+int NR_reg_set_uchar(NR_registry name, UCHAR data);
+int NR_reg_set_int2(NR_registry name, INT2 data);
+int NR_reg_set_uint2(NR_registry name, UINT2 data);
+int NR_reg_set_int4(NR_registry name, INT4 data);
+int NR_reg_set_uint4(NR_registry name, UINT4 data);
+int NR_reg_set_int8(NR_registry name, INT8 data);
+int NR_reg_set_uint8(NR_registry name, UINT8 data);
+int NR_reg_set_double(NR_registry name, double data);
+
+int NR_reg_set_registry(NR_registry name);
+
+int NR_reg_set_bytes(NR_registry name, UCHAR *data, size_t length);
+int NR_reg_set_string(NR_registry name, char *data);
+
+int NR_reg_set2_char(NR_registry prefix, char *name, char data);
+int NR_reg_set2_uchar(NR_registry prefix, char *name, UCHAR data);
+int NR_reg_set2_int2(NR_registry prefix, char *name, INT2 data);
+int NR_reg_set2_uint2(NR_registry prefix, char *name, UINT2 data);
+int NR_reg_set2_int4(NR_registry prefix, char *name, INT4 data);
+int NR_reg_set2_uint4(NR_registry prefix, char *name, UINT4 data);
+int NR_reg_set2_int8(NR_registry prefix, char *name, INT8 data);
+int NR_reg_set2_uint8(NR_registry prefix, char *name, UINT8 data);
+int NR_reg_set2_double(NR_registry prefix, char *name, double data);
+
+int NR_reg_set2_bytes(NR_registry prefix, char *name, UCHAR *data, size_t length);
+int NR_reg_set2_string(NR_registry prefix, char *name, char *data);
+
+int NR_reg_del(NR_registry name);
+
+int NR_reg_fin(NR_registry name);
+
+int NR_reg_get_child_count(NR_registry parent, unsigned int *count);
+int NR_reg_get_child_registry(NR_registry parent, unsigned int i, NR_registry child);
+int NR_reg_get2_child_count(NR_registry base, NR_registry name, unsigned int *count);
+int NR_reg_get2_child_registry(NR_registry base, NR_registry name, unsigned int i, NR_registry child);
+int NR_reg_get_children(NR_registry parent, NR_registry children[], size_t size, size_t *length);
+
+int NR_reg_dump(void);
+
+/* convenience methods, call RFREE on the returned data */
+int NR_reg_alloc_data(NR_registry name, Data *data);
+int NR_reg_alloc_string(NR_registry name, char **data);
+
+#define NR_REG_CB_ACTION_ADD (1<<0)
+#define NR_REG_CB_ACTION_CHANGE (1<<1)
+#define NR_REG_CB_ACTION_DELETE (1<<2)
+#define NR_REG_CB_ACTION_FINAL (1<<6)
+int NR_reg_register_callback(NR_registry name, char action, void (*cb)(void *cb_arg, char action, NR_registry name), void *cb_arg);
+int NR_reg_unregister_callback(NR_registry name, char action, void (*cb)(void *cb_arg, char action, NR_registry name));
+
+int NR_reg_make_registry(NR_registry parent, char *child, NR_registry out);
+int NR_reg_make_child_registry(NR_registry parent, NR_registry descendant, unsigned int generation, NR_registry child);
+
+#endif
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/registry/registry_int.h b/dom/media/webrtc/transport/third_party/nrappkit/src/registry/registry_int.h
new file mode 100644
index 0000000000..d6d412c543
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/registry/registry_int.h
@@ -0,0 +1,97 @@
+#if 0
+#define NR_LOG_REGISTRY BLAHBLAH()
+#define LOG_REGISTRY BLAHBLAH()
+static int BLAHBLAH() {
+int blahblah;
+r_log_register("registry",&blahblah);
+return blahblah;
+}
+#endif
+
+/*
+ *
+ * registry_int.h
+ *
+ * $Source: /Users/ekr/tmp/nrappkit-dump/nrappkit/src/registry/registry_int.h,v $
+ * $Revision: 1.3 $
+ * $Date: 2007/06/26 22:37:51 $
+ *
+ * Callback-related functions
+ *
+ *
+ * Copyright (C) 2005, Network Resonance, Inc.
+ * Copyright (C) 2006, Network Resonance, Inc.
+ * All Rights Reserved
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Network Resonance, Inc. nor the name of any
+ * contributors to this software may be used to endorse or promote
+ * products derived from this software without specific prior written
+ * permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ *
+ */
+
+#ifndef __REGISTRY_INT_H__
+#define __REGISTRY_INT_H__
+
+#include <sys/types.h>
+#include <r_types.h>
+#ifndef NO_REG_RPC
+#include <rpc/rpc.h>
+#endif
+
+extern int NR_LOG_REGISTRY;
+
+int nr_reg_is_valid(NR_registry name);
+
+#define NR_REG_TYPE_CHAR 0
+#define NR_REG_TYPE_UCHAR 1
+#define NR_REG_TYPE_INT2 2
+#define NR_REG_TYPE_UINT2 3
+#define NR_REG_TYPE_INT4 4
+#define NR_REG_TYPE_UINT4 5
+#define NR_REG_TYPE_INT8 6
+#define NR_REG_TYPE_UINT8 7
+#define NR_REG_TYPE_DOUBLE 8
+#define NR_REG_TYPE_BYTES 9
+#define NR_REG_TYPE_STRING 10
+#define NR_REG_TYPE_REGISTRY 11
+char *nr_reg_type_name(int type);
+int nr_reg_compute_type(char *type_name, int *type);
+
+char *nr_reg_action_name(int action);
+
+int nr_reg_cb_init(void);
+int nr_reg_client_cb_init(void);
+int nr_reg_register_for_callbacks(int fd, int connect_to_port);
+int nr_reg_raise_event(NR_registry name, int action);
+#ifndef NO_REG_RPC
+int nr_reg_get_client(CLIENT **client);
+#endif
+
+#define CALLBACK_SERVER_ADDR "127.0.0.1"
+#define CALLBACK_SERVER_PORT 8082
+#define CALLBACK_SERVER_BACKLOG 32
+
+#endif
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/registry/registry_local.c b/dom/media/webrtc/transport/third_party/nrappkit/src/registry/registry_local.c
new file mode 100644
index 0000000000..ed6e19aaa0
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/registry/registry_local.c
@@ -0,0 +1,1168 @@
+/*
+ *
+ * registry.c
+ *
+ * $Source: /Users/ekr/tmp/nrappkit-dump/nrappkit/src/registry/registry_local.c,v $
+ * $Revision: 1.4 $
+ * $Date: 2007/11/21 00:09:13 $
+ *
+ * Datastore for tracking configuration and related info.
+ *
+ *
+ * Copyright (C) 2005, Network Resonance, Inc.
+ * Copyright (C) 2006, Network Resonance, Inc.
+ * All Rights Reserved
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Network Resonance, Inc. nor the name of any
+ * contributors to this software may be used to endorse or promote
+ * products derived from this software without specific prior written
+ * permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ *
+ */
+
+#include <assert.h>
+#include <string.h>
+#ifndef WIN32
+#include <strings.h>
+#include <sys/param.h>
+#include <netinet/in.h>
+#endif
+#ifdef OPENSSL
+#include <openssl/ssl.h>
+#endif
+#include <ctype.h>
+#include "registry.h"
+#include "registry_int.h"
+#include "registry_vtbl.h"
+#include "r_assoc.h"
+#include "nr_common.h"
+#include "r_log.h"
+#include "r_errors.h"
+#include "r_macros.h"
+
+/* if C were an object-oriented language, nr_scalar_registry_node and
+ * nr_array_registry_node would subclass nr_registry_node, but it isn't
+ * object-oriented language, so this is used in cases where the pointer
+ * could be of either type */
+typedef struct nr_registry_node_ {
+ unsigned char type;
+} nr_registry_node;
+
+typedef struct nr_scalar_registry_node_ {
+ unsigned char type;
+ union {
+ char _char;
+ UCHAR _uchar;
+ INT2 _nr_int2;
+ UINT2 _nr_uint2;
+ INT4 _nr_int4;
+ UINT4 _nr_uint4;
+ INT8 _nr_int8;
+ UINT8 _nr_uint8;
+ double _double;
+ } scalar;
+} nr_scalar_registry_node;
+
+/* string, bytes */
+typedef struct nr_array_registry_node_ {
+ unsigned char type;
+ struct {
+ unsigned int length;
+ unsigned char data[1];
+ } array;
+} nr_array_registry_node;
+
+static int nr_reg_local_init(nr_registry_module *me);
+static int nr_reg_local_get_char(NR_registry name, char *data);
+static int nr_reg_local_get_uchar(NR_registry name, UCHAR *data);
+static int nr_reg_local_get_int2(NR_registry name, INT2 *data);
+static int nr_reg_local_get_uint2(NR_registry name, UINT2 *data);
+static int nr_reg_local_get_int4(NR_registry name, INT4 *data);
+static int nr_reg_local_get_uint4(NR_registry name, UINT4 *data);
+static int nr_reg_local_get_int8(NR_registry name, INT8 *data);
+static int nr_reg_local_get_uint8(NR_registry name, UINT8 *data);
+static int nr_reg_local_get_double(NR_registry name, double *data);
+static int nr_reg_local_get_registry(NR_registry name, NR_registry data);
+static int nr_reg_local_get_bytes(NR_registry name, UCHAR *data, size_t size, size_t *length);
+static int nr_reg_local_get_string(NR_registry name, char *data, size_t size);
+static int nr_reg_local_get_length(NR_registry name, size_t *len);
+static int nr_reg_local_get_type(NR_registry name, NR_registry_type type);
+static int nr_reg_local_set_char(NR_registry name, char data);
+static int nr_reg_local_set_uchar(NR_registry name, UCHAR data);
+static int nr_reg_local_set_int2(NR_registry name, INT2 data);
+static int nr_reg_local_set_uint2(NR_registry name, UINT2 data);
+static int nr_reg_local_set_int4(NR_registry name, INT4 data);
+static int nr_reg_local_set_uint4(NR_registry name, UINT4 data);
+static int nr_reg_local_set_int8(NR_registry name, INT8 data);
+static int nr_reg_local_set_uint8(NR_registry name, UINT8 data);
+static int nr_reg_local_set_double(NR_registry name, double data);
+static int nr_reg_local_set_registry(NR_registry name);
+static int nr_reg_local_set_bytes(NR_registry name, UCHAR *data, size_t length);
+static int nr_reg_local_set_string(NR_registry name, char *data);
+static int nr_reg_local_del(NR_registry name);
+static int nr_reg_local_get_child_count(NR_registry parent, size_t *count);
+static int nr_reg_local_get_children(NR_registry parent, NR_registry *data, size_t size, size_t *length);
+static int nr_reg_local_fin(NR_registry name);
+static int nr_reg_local_dump(int sorted);
+static int nr_reg_insert_node(char *name, void *node);
+static int nr_reg_change_node(char *name, void *node, void *old);
+static int nr_reg_get(char *name, int type, void *out);
+static int nr_reg_get_data(NR_registry name, nr_scalar_registry_node *node, void *out);
+static int nr_reg_get_array(char *name, unsigned char type, UCHAR *out, size_t size, size_t *length);
+static int nr_reg_set(char *name, int type, void *data);
+static int nr_reg_set_array(char *name, unsigned char type, UCHAR *data, size_t length);
+static int nr_reg_set_parent_registries(char *name);
+
+/* make these static OLD_REGISTRY */
+#if 0
+static int nr_reg_fetch_node(char *name, unsigned char type, nr_registry_node **node, int *free_node);
+static char *nr_reg_alloc_node_data(char *name, nr_registry_node *node, int *freeit);
+#else
+int nr_reg_fetch_node(char *name, unsigned char type, nr_registry_node **node, int *free_node);
+char *nr_reg_alloc_node_data(char *name, nr_registry_node *node, int *freeit);
+#endif
+static int nr_reg_rfree(void *ptr);
+#if 0 /* Unused currently */
+static int nr_reg_noop(void *ptr);
+#endif
+static int nr_reg_compute_length(char *name, nr_registry_node *node, size_t *length);
+char *nr_reg_action_name(int action);
+
+/* the registry, containing mappings like "foo.bar.baz" to registry
+ * nodes, which are either of type nr_scalar_registry_node or
+ * nr_array_registry_node */
+static r_assoc *nr_registry = 0;
+
+#if 0 /* Unused currently */
+static nr_array_registry_node nr_top_level_node;
+#endif
+
+typedef struct nr_reg_find_children_arg_ {
+ size_t size;
+ NR_registry *children;
+ size_t length;
+} nr_reg_find_children_arg;
+
+static int nr_reg_local_iter(NR_registry prefix, int (*action)(void *ptr, r_assoc_iterator *iter, char *prefix, char *name, nr_registry_node *node), void *ptr);
+static int nr_reg_local_iter_delete(void *ptr, r_assoc_iterator *iter, char *prefix, char *name, nr_registry_node *node);
+static int nr_reg_local_find_children(void *ptr, r_assoc_iterator *iter, char *prefix, char *name, nr_registry_node *node);
+static int nr_reg_local_count_children(void *ptr, r_assoc_iterator *iter, char *prefix, char *name, nr_registry_node *node);
+static int nr_reg_local_dump_print(void *ptr, r_assoc_iterator *iter, char *prefix, char *name, nr_registry_node *node);
+
+
+
+int
+nr_reg_local_iter(NR_registry prefix, int (*action)(void *ptr, r_assoc_iterator *iter, char *prefix, char *name, nr_registry_node *node), void *ptr)
+{
+ int r, _status;
+ r_assoc_iterator iter;
+ char *name;
+ int namel;
+ nr_registry_node *node;
+ int prefixl;
+
+ if (prefix == 0)
+ ABORT(R_INTERNAL);
+
+ if ((r=r_assoc_init_iter(nr_registry, &iter)))
+ ABORT(r);
+
+ prefixl = strlen(prefix);
+
+ for (;;) {
+ if ((r=r_assoc_iter(&iter, (void*)&name, &namel, (void*)&node))) {
+ if (r == R_EOD)
+ break;
+ else
+ ABORT(r);
+ }
+
+ /* subtract to remove the '\0' character from the string length */
+ --namel;
+
+ /* sanity check that the name is null-terminated */
+ assert(namel >= 0);
+ assert(name[namel] == '\0');
+
+ if (namel < 0 || name[namel] != '\0' || node == 0)
+ break;
+
+ /* 3 cases where action will be called:
+ * 1) prefix == ""
+ * 2) prefix == name
+ * 3) name == prefix + '.'
+ */
+ if (prefixl == 0
+ || ((namel == prefixl || (namel > prefixl && name[prefixl] == '.'))
+ && !strncmp(prefix, name, prefixl))) {
+ if ((r=action(ptr, &iter, prefix, name, node)))
+ ABORT(r);
+ }
+ }
+
+ _status=0;
+ abort:
+
+ return(_status);
+}
+
+int
+nr_reg_local_iter_delete(void *ptr, r_assoc_iterator *iter, char *prefix, char *name, nr_registry_node *node)
+{
+ int r, _status;
+
+ if ((r=r_assoc_iter_delete(iter)))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ return(_status);
+}
+
+int
+nr_reg_local_find_children(void *ptr, r_assoc_iterator *iter, char *prefix, char *name, nr_registry_node *node)
+{
+ int _status;
+ int prefixl = strlen(prefix);
+ char *dot;
+ nr_reg_find_children_arg *arg = (void*)ptr;
+
+ assert(sizeof(*(arg->children)) == sizeof(NR_registry));
+
+ /* only grovel through immediate children */
+ if (prefixl == 0 || name[prefixl] == '.') {
+ if (name[prefixl] != '\0') {
+ dot = strchr(&name[prefixl+1], '.');
+ if (dot == 0) {
+ strncpy(arg->children[arg->length], name, sizeof(NR_registry)-1);
+ ++arg->length;
+
+ /* only grab as many as there are room for */
+ if (arg->length >= arg->size)
+ ABORT(R_INTERRUPTED);
+ }
+ }
+ }
+
+ _status = 0;
+ abort:
+ return _status;
+}
+
+int
+nr_reg_local_count_children(void *ptr, r_assoc_iterator *iter, char *prefix, char *name, nr_registry_node *node)
+{
+ int prefixl = strlen(prefix);
+ char *dot;
+
+ /* only count children */
+ if (name[prefixl] == '.') {
+ dot = strchr(&name[prefixl+1], '.');
+ if (dot == 0)
+ ++(*(unsigned int *)ptr);
+ }
+ else if (name[0] != '\0') {
+ if (prefixl == 0)
+ ++(*(unsigned int *)ptr);
+ }
+
+ return 0;
+}
+
+int
+nr_reg_local_dump_print(void *ptr, r_assoc_iterator *iter, char *prefix, char *name, nr_registry_node *node)
+{
+ int _status;
+ int freeit = 0;
+ char *data;
+
+ /* only print leaf nodes */
+ if (node->type != NR_REG_TYPE_REGISTRY) {
+ data = nr_reg_alloc_node_data(name, node, &freeit);
+ if (ptr)
+ fprintf((FILE*)ptr, "%s: %s\n", name, data);
+ else
+ r_log(NR_LOG_REGISTRY, LOG_INFO, "%s: %s", name, data);
+ if (freeit)
+ RFREE(data);
+ }
+
+ _status=0;
+ //abort:
+ return(_status);
+}
+
+
+#if 0 /* Unused currently */
+int
+nr_reg_noop(void *ptr)
+{
+ return 0;
+}
+#endif
+
+int
+nr_reg_rfree(void *ptr)
+{
+ RFREE(ptr);
+ return 0;
+}
+
+int
+nr_reg_fetch_node(char *name, unsigned char type, nr_registry_node **node, int *free_node)
+{
+ int r, _status;
+
+ *node = 0;
+ *free_node = 0;
+
+ if ((r=nr_reg_is_valid(name)))
+ ABORT(r);
+
+ if ((r=r_assoc_fetch(nr_registry, name, strlen(name)+1, (void*)node)))
+ ABORT(r);
+
+ if ((*node)->type != type)
+ ABORT(R_FAILED);
+
+ _status=0;
+ abort:
+ if (_status) {
+ if (*node)
+ r_log(NR_LOG_REGISTRY, LOG_DEBUG, "Couldn't fetch node '%s' ('%s'), found '%s' instead",
+ name, nr_reg_type_name(type), nr_reg_type_name((*node)->type));
+ else
+ r_log(NR_LOG_REGISTRY, LOG_DEBUG, "Couldn't fetch node '%s' ('%s')",
+ name, nr_reg_type_name(type));
+ }
+ else {
+ r_log(NR_LOG_REGISTRY, LOG_DEBUG, "Fetched node '%s' ('%s')",
+ name, nr_reg_type_name(type));
+ }
+ return(_status);
+}
+
+int
+nr_reg_insert_node(char *name, void *node)
+{
+ int r, _status;
+
+ if ((r=nr_reg_is_valid(name)))
+ ABORT(r);
+
+ /* since the registry application is not multi-threaded, a node being
+ * inserted should always be a new node because the registry app must
+ * have looked for a node with this key but not found it, so it is
+ * being created/inserted now using R_ASSOC_NEW */
+ if ((r=r_assoc_insert(nr_registry, name, strlen(name)+1, node, 0, nr_reg_rfree, R_ASSOC_NEW)))
+ ABORT(r);
+
+ if ((r=nr_reg_set_parent_registries(name)))
+ ABORT(r);
+
+ if ((r=nr_reg_raise_event(name, NR_REG_CB_ACTION_ADD)))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ if (r_logging(NR_LOG_REGISTRY, LOG_INFO)) {
+ int freeit;
+ char *data = nr_reg_alloc_node_data(name, (void*)node, &freeit);
+ r_log(NR_LOG_REGISTRY, LOG_INFO,
+ "insert '%s' (%s) %s: %s", name,
+ nr_reg_type_name(((nr_registry_node*)node)->type),
+ (_status ? "FAILED" : "succeeded"), data);
+ if (freeit)
+ RFREE(data);
+ }
+ return(_status);
+}
+
+int
+nr_reg_change_node(char *name, void *node, void *old)
+{
+ int r, _status;
+
+ if ((r=nr_reg_is_valid(name)))
+ ABORT(r);
+
+ if (old != node) {
+ if ((r=r_assoc_insert(nr_registry, name, strlen(name)+1, node, 0, nr_reg_rfree, R_ASSOC_REPLACE)))
+ ABORT(r);
+ }
+
+ if ((r=nr_reg_raise_event(name, NR_REG_CB_ACTION_CHANGE)))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ if (r_logging(NR_LOG_REGISTRY, LOG_INFO)) {
+ int freeit;
+ char *data = nr_reg_alloc_node_data(name, (void*)node, &freeit);
+ r_log(NR_LOG_REGISTRY, LOG_INFO,
+ "change '%s' (%s) %s: %s", name,
+ nr_reg_type_name(((nr_registry_node*)node)->type),
+ (_status ? "FAILED" : "succeeded"), data);
+ if (freeit)
+ RFREE(data);
+ }
+ return(_status);
+}
+
+char *
+nr_reg_alloc_node_data(char *name, nr_registry_node *node, int *freeit)
+{
+ char *s = 0;
+ int len;
+ int alloc = 0;
+ unsigned int i;
+
+ *freeit = 0;
+
+ switch (node->type) {
+ default:
+ alloc = 100; /* plenty of room for any of the scalar types */
+ break;
+ case NR_REG_TYPE_REGISTRY:
+ alloc = strlen(name) + 1;
+ break;
+ case NR_REG_TYPE_BYTES:
+ alloc = (2 * ((nr_array_registry_node*)node)->array.length) + 1;
+ break;
+ case NR_REG_TYPE_STRING:
+ alloc = 0;
+ break;
+ }
+
+ if (alloc > 0) {
+ s = (void*)RMALLOC(alloc);
+ if (!s)
+ return "";
+
+ *freeit = 1;
+ }
+
+ len = alloc;
+
+ switch (node->type) {
+ case NR_REG_TYPE_CHAR:
+ i = ((nr_scalar_registry_node*)node)->scalar._char;
+ if (isprint(i) && ! isspace(i))
+ snprintf(s, len, "%c", (char)i);
+ else
+ snprintf(s, len, "\\%03o", (char)i);
+ break;
+ case NR_REG_TYPE_UCHAR:
+ snprintf(s, len, "0x%02x", ((nr_scalar_registry_node*)node)->scalar._uchar);
+ break;
+ case NR_REG_TYPE_INT2:
+ snprintf(s, len, "%d", ((nr_scalar_registry_node*)node)->scalar._nr_int2);
+ break;
+ case NR_REG_TYPE_UINT2:
+ snprintf(s, len, "%u", ((nr_scalar_registry_node*)node)->scalar._nr_uint2);
+ break;
+ case NR_REG_TYPE_INT4:
+ snprintf(s, len, "%d", ((nr_scalar_registry_node*)node)->scalar._nr_int4);
+ break;
+ case NR_REG_TYPE_UINT4:
+ snprintf(s, len, "%u", ((nr_scalar_registry_node*)node)->scalar._nr_uint4);
+ break;
+ case NR_REG_TYPE_INT8:
+ snprintf(s, len, "%lld", ((nr_scalar_registry_node*)node)->scalar._nr_int8);
+ break;
+ case NR_REG_TYPE_UINT8:
+ snprintf(s, len, "%llu", ((nr_scalar_registry_node*)node)->scalar._nr_uint8);
+ break;
+ case NR_REG_TYPE_DOUBLE:
+ snprintf(s, len, "%#f", ((nr_scalar_registry_node*)node)->scalar._double);
+ break;
+ case NR_REG_TYPE_REGISTRY:
+ snprintf(s, len, "%s", name);
+ break;
+ case NR_REG_TYPE_BYTES:
+ for (i = 0; i < ((nr_array_registry_node*)node)->array.length; ++i) {
+ sprintf(&s[2*i], "%02x", ((nr_array_registry_node*)node)->array.data[i]);
+ }
+ break;
+ case NR_REG_TYPE_STRING:
+ s = (char*)((nr_array_registry_node*)node)->array.data;
+ break;
+ default:
+ assert(0); /* bad value */
+ *freeit = 0;
+ s = "";
+ break;
+ }
+
+ return s;
+}
+
+int
+nr_reg_get(char *name, int type, void *out)
+{
+ int r, _status;
+ nr_scalar_registry_node *node = 0;
+ int free_node = 0;
+
+ if ((r=nr_reg_fetch_node(name, type, (void*)&node, &free_node)))
+ ABORT(r);
+
+ if ((r=nr_reg_get_data(name, node, out)))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ if (free_node) RFREE(node);
+ return(_status);
+}
+
+int
+nr_reg_get_data(NR_registry name, nr_scalar_registry_node *node, void *out)
+{
+ int _status;
+
+ switch (node->type) {
+ case NR_REG_TYPE_CHAR:
+ *(char*)out = node->scalar._char;
+ break;
+ case NR_REG_TYPE_UCHAR:
+ *(UCHAR*)out = node->scalar._uchar;
+ break;
+ case NR_REG_TYPE_INT2:
+ *(INT2*)out = node->scalar._nr_int2;
+ break;
+ case NR_REG_TYPE_UINT2:
+ *(UINT2*)out = node->scalar._nr_uint2;
+ break;
+ case NR_REG_TYPE_INT4:
+ *(INT4*)out = node->scalar._nr_int4;
+ break;
+ case NR_REG_TYPE_UINT4:
+ *(UINT4*)out = node->scalar._nr_uint4;
+ break;
+ case NR_REG_TYPE_INT8:
+ *(INT8*)out = node->scalar._nr_int8;
+ break;
+ case NR_REG_TYPE_UINT8:
+ *(UINT8*)out = node->scalar._nr_uint8;
+ break;
+ case NR_REG_TYPE_DOUBLE:
+ *(double*)out = node->scalar._double;
+ break;
+ default:
+ ABORT(R_INTERNAL);
+ break;
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+}
+
+int
+nr_reg_get_array(char *name, unsigned char type, unsigned char *out, size_t size, size_t *length)
+{
+ int r, _status;
+ nr_array_registry_node *node = 0;
+ int free_node = 0;
+
+ if ((r=nr_reg_fetch_node(name, type, (void*)&node, &free_node)))
+ ABORT(r);
+
+ if (size < node->array.length)
+ ABORT(R_BAD_ARGS);
+
+ if (out != 0)
+ memcpy(out, node->array.data, node->array.length);
+ if (length != 0)
+ *length = node->array.length;
+
+ _status=0;
+ abort:
+ if (node && free_node) RFREE(node);
+ return(_status);
+}
+
+int
+nr_reg_set(char *name, int type, void *data)
+{
+ int r, _status;
+ nr_scalar_registry_node *node = 0;
+ int create_node = 0;
+ int changed = 0;
+ int free_node = 0;
+
+ if ((r=nr_reg_fetch_node(name, type, (void*)&node, &free_node)))
+ if (r == R_NOT_FOUND) {
+ create_node = 1;
+ free_node = 1;
+ }
+ else
+ ABORT(r);
+
+ if (create_node) {
+ if (!(node=(void*)RCALLOC(sizeof(nr_scalar_registry_node))))
+ ABORT(R_NO_MEMORY);
+
+ node->type = type;
+ }
+ else {
+ if (node->type != type)
+ ABORT(R_BAD_ARGS);
+ }
+
+ switch (type) {
+#define CASE(TYPE, _name, type) \
+ case TYPE: \
+ if (node->scalar._name != *(type*)data) { \
+ node->scalar._name = *(type*)data; \
+ if (! create_node) \
+ changed = 1; \
+ } \
+ break;
+ CASE(NR_REG_TYPE_CHAR, _char, char)
+ CASE(NR_REG_TYPE_UCHAR, _uchar, UCHAR)
+ CASE(NR_REG_TYPE_INT2, _nr_int2, INT2)
+ CASE(NR_REG_TYPE_UINT2, _nr_uint2, UINT2)
+ CASE(NR_REG_TYPE_INT4, _nr_int4, INT4)
+ CASE(NR_REG_TYPE_UINT4, _nr_uint4, UINT4)
+ CASE(NR_REG_TYPE_INT8, _nr_int8, INT8)
+ CASE(NR_REG_TYPE_UINT8, _nr_uint8, UINT8)
+ CASE(NR_REG_TYPE_DOUBLE, _double, double)
+#undef CASE
+
+ case NR_REG_TYPE_REGISTRY:
+ /* do nothing */
+ break;
+
+ default:
+ ABORT(R_INTERNAL);
+ break;
+ }
+
+ if (create_node) {
+ if ((r=nr_reg_insert_node(name, node)))
+ ABORT(r);
+ free_node = 0;
+ }
+ else {
+ if (changed) {
+ if ((r=nr_reg_change_node(name, node, node)))
+ ABORT(r);
+ free_node = 0;
+ }
+ }
+
+ _status=0;
+ abort:
+ if (_status) {
+ if (node && free_node) RFREE(node);
+ }
+ return(_status);
+}
+
+int
+nr_reg_set_array(char *name, unsigned char type, UCHAR *data, size_t length)
+{
+ int r, _status;
+ nr_array_registry_node *old = 0;
+ nr_array_registry_node *node = 0;
+ int free_node = 0;
+ int added = 0;
+ int changed = 0;
+
+ if ((r=nr_reg_fetch_node(name, type, (void*)&old, &free_node))) {
+ if (r != R_NOT_FOUND)
+ ABORT(r);
+ }
+ else {
+ assert(free_node == 0);
+ }
+
+ if (old) {
+ if (old->type != type)
+ ABORT(R_BAD_ARGS);
+
+ if (old->array.length != length
+ || memcmp(old->array.data, data, length)) {
+ changed = 1;
+
+ if (old->array.length < length) {
+ if (!(node=(void*)RCALLOC(sizeof(nr_array_registry_node)+length)))
+ ABORT(R_NO_MEMORY);
+ }
+ else {
+ node = old;
+ }
+ }
+ }
+ else {
+ if (!(node=(void*)RCALLOC(sizeof(nr_array_registry_node)+length)))
+ ABORT(R_NO_MEMORY);
+
+ added = 1;
+ }
+
+ if (added || changed) {
+ node->type = type;
+ node->array.length = length;
+ memcpy(node->array.data, data, length);
+ }
+
+ if (added) {
+ if ((r=nr_reg_insert_node(name, node)))
+ ABORT(r);
+ }
+ else if (changed) {
+ if ((r=nr_reg_change_node(name, node, old)))
+ ABORT(r);
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+}
+
+int
+nr_reg_set_parent_registries(char *name)
+{
+ int r, _status;
+ char *parent = 0;
+ char *dot;
+
+ if ((parent = r_strdup(name)) == 0)
+ ABORT(R_NO_MEMORY);
+
+ if ((dot = strrchr(parent, '.')) != 0) {
+ *dot = '\0';
+ if ((r=NR_reg_set_registry(parent)))
+ ABORT(r);
+ }
+
+ _status=0;
+ abort:
+ if (parent) RFREE(parent);
+ return(_status);
+}
+
+
+
+
+
+/* NON-STATIC METHODS */
+
+int
+nr_reg_is_valid(NR_registry name)
+{
+ int _status;
+ unsigned int length;
+ unsigned int i;
+
+ if (name == 0)
+ ABORT(R_BAD_ARGS);
+
+ /* make sure the key is null-terminated */
+ if (memchr(name, '\0', sizeof(NR_registry)) == 0)
+ ABORT(R_BAD_ARGS);
+
+ length = strlen(name);
+
+ /* cannot begin or end with a period */
+ if (name[0] == '.')
+ ABORT(R_BAD_ARGS);
+ if (strlen(name) > 0 && name[length-1] == '.')
+ ABORT(R_BAD_ARGS);
+
+ /* all characters cannot be space, and must be printable and not / */
+ for (i = 0; i < length; ++i) {
+ if (isspace(name[i]) || !isprint(name[i]) || name[i] == '/')
+ ABORT(R_BAD_ARGS);
+ }
+
+ _status=0;
+ abort:
+ if (_status) {
+ r_log(NR_LOG_REGISTRY, LOG_DEBUG, "invalid name '%s'", name);
+ }
+ return(_status);
+}
+
+
+int
+nr_reg_compute_length(char *name, nr_registry_node *in, size_t *length)
+{
+ int _status;
+ nr_array_registry_node *node = (nr_array_registry_node*)in;
+
+ switch (node->type) {
+ case NR_REG_TYPE_STRING:
+ *length = node->array.length - 1;
+ break;
+ case NR_REG_TYPE_BYTES:
+ *length = node->array.length;
+ break;
+ case NR_REG_TYPE_CHAR:
+ *length = sizeof(char);
+ break;
+ case NR_REG_TYPE_UCHAR:
+ *length = sizeof(UCHAR);
+ break;
+ case NR_REG_TYPE_INT2:
+ case NR_REG_TYPE_UINT2:
+ *length = 2;
+ break;
+ case NR_REG_TYPE_INT4:
+ case NR_REG_TYPE_UINT4:
+ *length = 4;
+ break;
+ case NR_REG_TYPE_INT8:
+ case NR_REG_TYPE_UINT8:
+ *length = 8;
+ break;
+ case NR_REG_TYPE_DOUBLE:
+ *length = sizeof(double);
+ break;
+ case NR_REG_TYPE_REGISTRY:
+ *length = strlen(name);
+ break;
+ default:
+ ABORT(R_INTERNAL);
+ break;
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+}
+
+
+/* VTBL METHODS */
+
+int
+nr_reg_local_init(nr_registry_module *me)
+{
+ int r, _status;
+
+ if (nr_registry == 0) {
+ if ((r=r_assoc_create(&nr_registry, r_assoc_crc32_hash_compute, 12)))
+ ABORT(r);
+
+ if ((r=nr_reg_cb_init()))
+ ABORT(r);
+
+ /* make sure NR_TOP_LEVEL_REGISTRY always exists */
+ if ((r=nr_reg_local_set_registry(NR_TOP_LEVEL_REGISTRY)))
+ ABORT(r);
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+}
+
+#define NRREGLOCALGET(func, TYPE, type) \
+int \
+func(NR_registry name, type *out) \
+{ \
+ return nr_reg_get(name, TYPE, out); \
+}
+
+NRREGLOCALGET(nr_reg_local_get_char, NR_REG_TYPE_CHAR, char)
+NRREGLOCALGET(nr_reg_local_get_uchar, NR_REG_TYPE_UCHAR, UCHAR)
+NRREGLOCALGET(nr_reg_local_get_int2, NR_REG_TYPE_INT2, INT2)
+NRREGLOCALGET(nr_reg_local_get_uint2, NR_REG_TYPE_UINT2, UINT2)
+NRREGLOCALGET(nr_reg_local_get_int4, NR_REG_TYPE_INT4, INT4)
+NRREGLOCALGET(nr_reg_local_get_uint4, NR_REG_TYPE_UINT4, UINT4)
+NRREGLOCALGET(nr_reg_local_get_int8, NR_REG_TYPE_INT8, INT8)
+NRREGLOCALGET(nr_reg_local_get_uint8, NR_REG_TYPE_UINT8, UINT8)
+NRREGLOCALGET(nr_reg_local_get_double, NR_REG_TYPE_DOUBLE, double)
+
+int
+nr_reg_local_get_registry(NR_registry name, NR_registry out)
+{
+ int r, _status;
+ nr_scalar_registry_node *node = 0;
+ int free_node = 0;
+
+ if ((r=nr_reg_fetch_node(name, NR_REG_TYPE_REGISTRY, (void*)&node, &free_node)))
+ ABORT(r);
+
+ strncpy(out, name, sizeof(NR_registry));
+
+ _status=0;
+ abort:
+ if (free_node) RFREE(node);
+ return(_status);
+
+}
+
+int
+nr_reg_local_get_bytes(NR_registry name, UCHAR *out, size_t size, size_t *length)
+{
+ return nr_reg_get_array(name, NR_REG_TYPE_BYTES, out, size, length);
+}
+
+int
+nr_reg_local_get_string(NR_registry name, char *out, size_t size)
+{
+ return nr_reg_get_array(name, NR_REG_TYPE_STRING, (UCHAR*)out, size, 0);
+}
+
+int
+nr_reg_local_get_length(NR_registry name, size_t *length)
+{
+ int r, _status;
+ nr_registry_node *node = 0;
+
+ if ((r=nr_reg_is_valid(name)))
+ ABORT(r);
+
+ if ((r=r_assoc_fetch(nr_registry, name, strlen(name)+1, (void*)&node)))
+ ABORT(r);
+
+ if ((r=nr_reg_compute_length(name, node, length)))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ return(_status);
+}
+
+int
+nr_reg_local_get_type(NR_registry name, NR_registry_type type)
+{
+ int r, _status;
+ nr_registry_node *node = 0;
+ char *str;
+
+ if ((r=nr_reg_is_valid(name)))
+ ABORT(r);
+
+ if ((r=r_assoc_fetch(nr_registry, name, strlen(name)+1, (void*)&node)))
+ ABORT(r);
+
+ str = nr_reg_type_name(node->type);
+ if (! str)
+ ABORT(R_BAD_ARGS);
+
+ strncpy(type, str, sizeof(NR_registry_type));
+
+ _status=0;
+ abort:
+ return(_status);
+}
+
+
+#define NRREGLOCALSET(func, TYPE, type) \
+int \
+func(NR_registry name, type data) \
+{ \
+ return nr_reg_set(name, TYPE, &data); \
+}
+
+NRREGLOCALSET(nr_reg_local_set_char, NR_REG_TYPE_CHAR, char)
+NRREGLOCALSET(nr_reg_local_set_uchar, NR_REG_TYPE_UCHAR, UCHAR)
+NRREGLOCALSET(nr_reg_local_set_int2, NR_REG_TYPE_INT2, INT2)
+NRREGLOCALSET(nr_reg_local_set_uint2, NR_REG_TYPE_UINT2, UINT2)
+NRREGLOCALSET(nr_reg_local_set_int4, NR_REG_TYPE_INT4, INT4)
+NRREGLOCALSET(nr_reg_local_set_uint4, NR_REG_TYPE_UINT4, UINT4)
+NRREGLOCALSET(nr_reg_local_set_int8, NR_REG_TYPE_INT8, INT8)
+NRREGLOCALSET(nr_reg_local_set_uint8, NR_REG_TYPE_UINT8, UINT8)
+NRREGLOCALSET(nr_reg_local_set_double, NR_REG_TYPE_DOUBLE, double)
+
+int
+nr_reg_local_set_registry(NR_registry name)
+{
+ return nr_reg_set(name, NR_REG_TYPE_REGISTRY, 0);
+}
+
+int
+nr_reg_local_set_bytes(NR_registry name, unsigned char *data, size_t length)
+{
+ return nr_reg_set_array(name, NR_REG_TYPE_BYTES, data, length);
+}
+
+int
+nr_reg_local_set_string(NR_registry name, char *data)
+{
+ return nr_reg_set_array(name, NR_REG_TYPE_STRING, (UCHAR*)data, strlen(data)+1);
+}
+
+int
+nr_reg_local_del(NR_registry name)
+{
+ int r, _status;
+
+ if ((r=nr_reg_is_valid(name)))
+ ABORT(r);
+
+ /* delete from NR_registry */
+ if ((r=nr_reg_local_iter(name, nr_reg_local_iter_delete, 0)))
+ ABORT(r);
+
+ if ((r=nr_reg_raise_event(name, NR_REG_CB_ACTION_DELETE)))
+ ABORT(r);
+
+ /* if deleting from the root, re-insert the root */
+ if (! strcasecmp(name, NR_TOP_LEVEL_REGISTRY)) {
+ if ((r=nr_reg_local_set_registry(NR_TOP_LEVEL_REGISTRY)))
+ ABORT(r);
+ }
+
+ _status=0;
+ abort:
+ r_log(NR_LOG_REGISTRY,
+ (_status ? LOG_INFO : LOG_INFO),
+ "delete of '%s' %s", name,
+ (_status ? "FAILED" : "succeeded"));
+ return(_status);
+}
+
+int
+nr_reg_local_get_child_count(NR_registry parent, size_t *count)
+{
+ int r, _status;
+ nr_registry_node *ignore1;
+ int ignore2;
+
+
+ if ((r=nr_reg_is_valid(parent)))
+ ABORT(r);
+
+ /* test to see whether it is present */
+ if ((r=nr_reg_fetch_node(parent, NR_REG_TYPE_REGISTRY, &ignore1, &ignore2)))
+ ABORT(r);
+
+ /* sanity check that there isn't any memory to free */
+ assert(ignore2 == 0);
+
+ *count = 0;
+
+ if ((r=nr_reg_local_iter(parent, nr_reg_local_count_children, count)))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ return(_status);
+}
+
+int
+nr_reg_local_get_children(NR_registry parent, NR_registry *data, size_t size, size_t *length)
+{
+ int r, _status;
+ nr_reg_find_children_arg arg;
+
+ if ((r=nr_reg_is_valid(parent)))
+ ABORT(r);
+
+ arg.children = data;
+ arg.size = size;
+ arg.length = 0;
+
+ if ((r=nr_reg_local_iter(parent, nr_reg_local_find_children, (void*)&arg))) {
+ if (r == R_INTERRUPTED)
+ ABORT(R_BAD_ARGS);
+ else
+ ABORT(r);
+ }
+
+ assert(sizeof(*arg.children) == sizeof(NR_registry));
+ qsort(arg.children, arg.length, sizeof(*arg.children), (void*)strcasecmp);
+
+ *length = arg.length;
+
+ _status = 0;
+ abort:
+ return(_status);
+}
+
+int
+nr_reg_local_fin(NR_registry name)
+{
+ int r, _status;
+
+ if ((r=nr_reg_raise_event(name, NR_REG_CB_ACTION_FINAL)))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ return(_status);
+}
+
+int
+nr_reg_local_dump(int sorted)
+{
+ int r, _status;
+
+ if ((r=nr_reg_local_iter(NR_TOP_LEVEL_REGISTRY, nr_reg_local_dump_print, 0)))
+ ABORT(r);
+
+ _status=0;
+ abort:
+ return(_status);
+}
+
+
+
+static nr_registry_module_vtbl nr_reg_local_vtbl = {
+ nr_reg_local_init,
+ nr_reg_local_get_char,
+ nr_reg_local_get_uchar,
+ nr_reg_local_get_int2,
+ nr_reg_local_get_uint2,
+ nr_reg_local_get_int4,
+ nr_reg_local_get_uint4,
+ nr_reg_local_get_int8,
+ nr_reg_local_get_uint8,
+ nr_reg_local_get_double,
+ nr_reg_local_get_registry,
+ nr_reg_local_get_bytes,
+ nr_reg_local_get_string,
+ nr_reg_local_get_length,
+ nr_reg_local_get_type,
+ nr_reg_local_set_char,
+ nr_reg_local_set_uchar,
+ nr_reg_local_set_int2,
+ nr_reg_local_set_uint2,
+ nr_reg_local_set_int4,
+ nr_reg_local_set_uint4,
+ nr_reg_local_set_int8,
+ nr_reg_local_set_uint8,
+ nr_reg_local_set_double,
+ nr_reg_local_set_registry,
+ nr_reg_local_set_bytes,
+ nr_reg_local_set_string,
+ nr_reg_local_del,
+ nr_reg_local_get_child_count,
+ nr_reg_local_get_children,
+ nr_reg_local_fin,
+ nr_reg_local_dump
+};
+
+static nr_registry_module nr_reg_local_module = { 0, &nr_reg_local_vtbl };
+
+void *NR_REG_MODE_LOCAL = &nr_reg_local_module;
+
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/registry/registry_vtbl.h b/dom/media/webrtc/transport/third_party/nrappkit/src/registry/registry_vtbl.h
new file mode 100644
index 0000000000..0f75a03b95
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/registry/registry_vtbl.h
@@ -0,0 +1,96 @@
+/*
+ *
+ * registry_vtbl.h
+ *
+ * $Source: /Users/ekr/tmp/nrappkit-dump/nrappkit/src/registry/registry_vtbl.h,v $
+ * $Revision: 1.2 $
+ * $Date: 2006/08/16 19:39:14 $
+ *
+ *
+ * Copyright (C) 2006, Network Resonance, Inc.
+ * All Rights Reserved
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Network Resonance, Inc. nor the name of any
+ * contributors to this software may be used to endorse or promote
+ * products derived from this software without specific prior written
+ * permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ *
+ */
+
+#ifndef __REGISTRY_VTBL_H__
+#define __REGISTRY_VTBL_H__
+
+typedef struct nr_registry_module_ nr_registry_module;
+
+typedef struct nr_registry_module_vtbl_ {
+ int (*init)(nr_registry_module*);
+
+ int (*get_char)(NR_registry name, char *out);
+ int (*get_uchar)(NR_registry name, UCHAR *out);
+ int (*get_int2)(NR_registry name, INT2 *out);
+ int (*get_uint2)(NR_registry name, UINT2 *out);
+ int (*get_int4)(NR_registry name, INT4 *out);
+ int (*get_uint4)(NR_registry name, UINT4 *out);
+ int (*get_int8)(NR_registry name, INT8 *out);
+ int (*get_uint8)(NR_registry name, UINT8 *out);
+ int (*get_double)(NR_registry name, double *out);
+ int (*get_registry)(NR_registry name, NR_registry out);
+
+ int (*get_bytes)(NR_registry name, UCHAR *out, size_t size, size_t *length);
+ int (*get_string)(NR_registry name, char *out, size_t size);
+ int (*get_length)(NR_registry name, size_t *length);
+ int (*get_type)(NR_registry name, NR_registry_type type);
+
+ int (*set_char)(NR_registry name, char data);
+ int (*set_uchar)(NR_registry name, UCHAR data);
+ int (*set_int2)(NR_registry name, INT2 data);
+ int (*set_uint2)(NR_registry name, UINT2 data);
+ int (*set_int4)(NR_registry name, INT4 data);
+ int (*set_uint4)(NR_registry name, UINT4 data);
+ int (*set_int8)(NR_registry name, INT8 data);
+ int (*set_uint8)(NR_registry name, UINT8 data);
+ int (*set_double)(NR_registry name, double data);
+ int (*set_registry)(NR_registry name);
+
+ int (*set_bytes)(NR_registry name, UCHAR *data, size_t length);
+ int (*set_string)(NR_registry name, char *data);
+
+ int (*del)(NR_registry name);
+
+ int (*get_child_count)(NR_registry parent, size_t *count);
+ int (*get_children)(NR_registry parent, NR_registry *data, size_t size, size_t *length);
+
+ int (*fin)(NR_registry name);
+
+ int (*dump)(int sorted);
+} nr_registry_module_vtbl;
+
+struct nr_registry_module_ {
+ void *handle;
+ nr_registry_module_vtbl *vtbl;
+};
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/registry/registrycb.c b/dom/media/webrtc/transport/third_party/nrappkit/src/registry/registrycb.c
new file mode 100644
index 0000000000..4b326a1ee2
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/registry/registrycb.c
@@ -0,0 +1,440 @@
+/*
+ *
+ * registrycb.c
+ *
+ * $Source: /Users/ekr/tmp/nrappkit-dump/nrappkit/src/registry/registrycb.c,v $
+ * $Revision: 1.3 $
+ * $Date: 2007/06/26 22:37:51 $
+ *
+ * Callback-related functions
+ *
+ *
+ * Copyright (C) 2005, Network Resonance, Inc.
+ * Copyright (C) 2006, Network Resonance, Inc.
+ * All Rights Reserved
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Network Resonance, Inc. nor the name of any
+ * contributors to this software may be used to endorse or promote
+ * products derived from this software without specific prior written
+ * permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ *
+ */
+
+#include <assert.h>
+#include <string.h>
+#include "registry.h"
+#include "registry_int.h"
+#include "r_assoc.h"
+#include "r_errors.h"
+#include "nr_common.h"
+#include "r_log.h"
+#include "r_macros.h"
+
+static char CB_ACTIONS[] = { NR_REG_CB_ACTION_ADD,
+ NR_REG_CB_ACTION_DELETE,
+ NR_REG_CB_ACTION_CHANGE,
+ NR_REG_CB_ACTION_FINAL };
+
+typedef struct nr_reg_cb_info_ {
+ char action;
+ void (*cb)(void *cb_arg, char action, NR_registry name);
+ void *cb_arg;
+ NR_registry name;
+} nr_reg_cb_info;
+
+/* callbacks that are registered, a mapping from names like "foo.bar.baz"
+ * to an r_assoc which contains possibly several nr_reg_cb_info*'s */
+static r_assoc *nr_registry_callbacks = 0;
+
+//static size_t SIZEOF_CB_ID = (sizeof(void (*)()) + 1);
+#define SIZEOF_CB_ID (sizeof(void (*)()) + 1)
+
+static int nr_reg_validate_action(char action);
+static int nr_reg_assoc_destroy(void *ptr);
+static int compute_cb_id(void *cb, char action, unsigned char cb_id[SIZEOF_CB_ID]);
+static int nr_reg_info_free(void *ptr);
+static int nr_reg_raise_event_recurse(char *name, char *tmp, int action);
+static int nr_reg_register_callback(NR_registry name, char action, void (*cb)(void *cb_arg, char action, NR_registry name), void *cb_arg);
+static int nr_reg_unregister_callback(char *name, char action, void (*cb)(void *cb_arg, char action, NR_registry name));
+
+int
+nr_reg_cb_init()
+{
+ int r, _status;
+
+ if (nr_registry_callbacks == 0) {
+ if ((r=r_assoc_create(&nr_registry_callbacks, r_assoc_crc32_hash_compute, 12)))
+ ABORT(r);
+ }
+
+ _status=0;
+ abort:
+ if (_status) {
+ r_log(NR_LOG_REGISTRY, LOG_DEBUG, "Couldn't init notifications: %s", nr_strerror(_status));
+ }
+ return(_status);
+}
+
+int
+nr_reg_validate_action(char action)
+{
+ int _status;
+ size_t i;
+
+ for (i = 0; i < sizeof(CB_ACTIONS); ++i) {
+ if (action == CB_ACTIONS[i])
+ return 0;
+ }
+ ABORT(R_BAD_ARGS);
+
+ _status=0;
+ abort:
+ return(_status);
+}
+
+int
+nr_reg_register_callback(NR_registry name, char action, void (*cb)(void *cb_arg, char action, NR_registry name), void *cb_arg)
+{
+ int r, _status;
+ r_assoc *assoc;
+ int create_assoc = 0;
+ nr_reg_cb_info *info;
+ int create_info = 0;
+ unsigned char cb_id[SIZEOF_CB_ID];
+
+ if (name == 0 || cb == 0)
+ ABORT(R_BAD_ARGS);
+
+ if (nr_registry_callbacks == 0)
+ ABORT(R_FAILED);
+
+ if ((r=nr_reg_is_valid(name)))
+ ABORT(r);
+
+ if ((r=nr_reg_validate_action(action)))
+ ABORT(r);
+
+ if ((r=r_assoc_fetch(nr_registry_callbacks, name, strlen(name)+1, (void*)&assoc))) {
+ if (r == R_NOT_FOUND)
+ create_assoc = 1;
+ else
+ ABORT(r);
+ }
+
+ if (create_assoc) {
+ if ((r=r_assoc_create(&assoc, r_assoc_crc32_hash_compute, 5)))
+ ABORT(r);
+
+ if ((r=r_assoc_insert(nr_registry_callbacks, name, strlen(name)+1, assoc, 0, nr_reg_assoc_destroy, R_ASSOC_NEW)))
+ ABORT(r);
+ }
+
+ if ((r=compute_cb_id(cb, action, cb_id)))
+ ABORT(r);
+
+ if ((r=r_assoc_fetch(assoc, (char*)cb_id, SIZEOF_CB_ID, (void*)&info))) {
+ if (r == R_NOT_FOUND)
+ create_info = 1;
+ else
+ ABORT(r);
+ }
+
+ if (create_info) {
+ if (!(info=(void*)RCALLOC(sizeof(*info))))
+ ABORT(R_NO_MEMORY);
+ }
+
+ strncpy(info->name, name, sizeof(info->name));
+ info->action = action;
+ info->cb = cb;
+ info->cb_arg = cb_arg;
+
+ if (create_info) {
+ if ((r=r_assoc_insert(assoc, (char*)cb_id, SIZEOF_CB_ID, info, 0, nr_reg_info_free, R_ASSOC_NEW)))
+ ABORT(r);
+ }
+
+ _status=0;
+ abort:
+ r_log(NR_LOG_REGISTRY, LOG_DEBUG, "register callback %p on '%s' for '%s' %s", cb, name, nr_reg_action_name(action), (_status ? "FAILED" : "succeeded"));
+
+ if (_status) {
+ if (create_info && info) RFREE(info);
+ if (create_assoc && assoc) nr_reg_assoc_destroy(&assoc);
+ }
+ return(_status);
+}
+
+int
+nr_reg_unregister_callback(char *name, char action, void (*cb)(void *cb_arg, char action, NR_registry name))
+{
+ int r, _status;
+ r_assoc *assoc;
+ int size;
+ unsigned char cb_id[SIZEOF_CB_ID];
+
+ if (name == 0 || cb == 0)
+ ABORT(R_BAD_ARGS);
+
+ if (nr_registry_callbacks == 0)
+ ABORT(R_FAILED);
+
+ if ((r=nr_reg_is_valid(name)))
+ ABORT(r);
+
+ if ((r=nr_reg_validate_action(action)))
+ ABORT(r);
+
+ if ((r=r_assoc_fetch(nr_registry_callbacks, name, strlen(name)+1, (void*)&assoc))) {
+ if (r != R_NOT_FOUND)
+ ABORT(r);
+ }
+ else {
+ if ((r=compute_cb_id(cb, action, cb_id)))
+ ABORT(r);
+
+ if ((r=r_assoc_delete(assoc, (char*)cb_id, SIZEOF_CB_ID))) {
+ if (r != R_NOT_FOUND)
+ ABORT(r);
+ }
+
+ if ((r=r_assoc_num_elements(assoc, &size)))
+ ABORT(r);
+
+ if (size == 0) {
+ if ((r=r_assoc_delete(nr_registry_callbacks, name, strlen(name)+1)))
+ ABORT(r);
+ }
+ }
+
+ _status=0;
+ abort:
+ r_log(NR_LOG_REGISTRY, LOG_DEBUG, "unregister callback %p on '%s' for '%s' %s", cb, name, nr_reg_action_name(action), (_status ? "FAILED" : "succeeded"));
+
+ return(_status);
+}
+
+int
+compute_cb_id(void *cb, char action, unsigned char cb_id[SIZEOF_CB_ID])
+{
+ /* callbacks are identified by the pointer to the cb function plus
+ * the action being watched */
+ assert(sizeof(cb) == sizeof(void (*)()));
+ assert(sizeof(cb) == (SIZEOF_CB_ID - 1));
+
+ memcpy(cb_id, &(cb), sizeof(cb));
+ cb_id[SIZEOF_CB_ID-1] = action;
+
+ return 0;
+}
+
+char *
+nr_reg_action_name(int action)
+{
+ char *name = "*Unknown*";
+
+ switch (action) {
+ case NR_REG_CB_ACTION_ADD: name = "add"; break;
+ case NR_REG_CB_ACTION_DELETE: name = "delete"; break;
+ case NR_REG_CB_ACTION_CHANGE: name = "change"; break;
+ case NR_REG_CB_ACTION_FINAL: name = "final"; break;
+ }
+
+ return name;
+}
+
+int
+nr_reg_assoc_destroy(void *ptr)
+{
+ return r_assoc_destroy((r_assoc**)&ptr);
+}
+
+int
+nr_reg_info_free(void *ptr)
+{
+ RFREE(ptr);
+ return 0;
+}
+
+/* call with tmp=0 */
+int
+nr_reg_raise_event_recurse(char *name, char *tmp, int action)
+{
+ int r, _status;
+ r_assoc *assoc;
+ nr_reg_cb_info *info;
+ r_assoc_iterator iter;
+ char *key;
+ int keyl;
+ char *c;
+ int free_tmp = 0;
+ int count;
+
+ if (tmp == 0) {
+ if (!(tmp = (char*)r_strdup(name)))
+ ABORT(R_NO_MEMORY);
+ free_tmp = 1;
+ }
+
+ if ((r=r_assoc_fetch(nr_registry_callbacks, tmp, strlen(tmp)+1, (void*)&assoc))) {
+ if (r != R_NOT_FOUND)
+ ABORT(r);
+
+ r_log(NR_LOG_REGISTRY, LOG_DEBUG, "No callbacks found on '%s'", tmp);
+ }
+ else {
+ if (!r_assoc_num_elements(assoc, &count)) {
+ r_log(NR_LOG_REGISTRY, LOG_DEBUG, "%d callback%s found on '%s'",
+ count, ((count == 1) ? "" : "s"), tmp);
+ }
+
+ if ((r=r_assoc_init_iter(assoc, &iter)))
+ ABORT(r);
+
+ for (;;) {
+ if ((r=r_assoc_iter(&iter, (void*)&key, &keyl, (void*)&info))) {
+ if (r == R_EOD)
+ break;
+ else
+ ABORT(r);
+ }
+
+ if (info->action == action) {
+ r_log(NR_LOG_REGISTRY, LOG_DEBUG,
+ "Invoking callback %p for '%s'",
+ info->cb,
+ nr_reg_action_name(info->action));
+
+ (void)info->cb(info->cb_arg, action, name);
+ }
+ else {
+ r_log(NR_LOG_REGISTRY, LOG_DEBUG,
+ "Skipping callback %p for '%s'",
+ info->cb,
+ nr_reg_action_name(info->action));
+ }
+ }
+ }
+
+ if (strlen(tmp) > 0) {
+ c = strrchr(tmp, '.');
+ if (c != 0)
+ *c = '\0';
+ else
+ tmp[0] = '\0';
+
+ if ((r=nr_reg_raise_event_recurse(name, tmp, action)))
+ ABORT(r);
+ }
+
+ _status=0;
+ abort:
+ if (free_tmp && tmp != 0) RFREE(tmp);
+ return(_status);
+}
+
+
+/* NON-STATIC METHODS */
+
+int
+nr_reg_raise_event(NR_registry name, int action)
+{
+ int r, _status;
+ int count;
+ char *event = nr_reg_action_name(action);
+
+ r_log(NR_LOG_REGISTRY, LOG_DEBUG, "raising event '%s' on '%s'", event, name);
+
+ if (name == 0)
+ ABORT(R_BAD_ARGS);
+
+ if ((r=nr_reg_validate_action(action)))
+ ABORT(r);
+
+ if ((r=r_assoc_num_elements(nr_registry_callbacks, &count)))
+ ABORT(r);
+
+ if (count > 0) {
+ if ((r=nr_reg_raise_event_recurse(name, 0, action)))
+ ABORT(r);
+ }
+ else {
+ r_log(NR_LOG_REGISTRY, LOG_DEBUG, "No callbacks found");
+ return 0;
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+}
+
+
+/* PUBLIC METHODS */
+
+int
+NR_reg_register_callback(NR_registry name, char action, void (*cb)(void *cb_arg, char action, NR_registry name), void *cb_arg)
+{
+ int r, _status;
+ size_t i;
+
+ for (i = 0; i < sizeof(CB_ACTIONS); ++i) {
+ if (action & CB_ACTIONS[i]) {
+ if ((r=nr_reg_register_callback(name, CB_ACTIONS[i], cb, cb_arg)))
+ ABORT(r);
+
+ action &= ~(CB_ACTIONS[i]);
+ }
+ }
+
+ if (action)
+ ABORT(R_BAD_ARGS);
+
+ _status=0;
+ abort:
+ return(_status);
+}
+
+int
+NR_reg_unregister_callback(NR_registry name, char action, void (*cb)(void *cb_arg, char action, NR_registry name))
+{
+ int r, _status;
+ size_t i;
+
+ for (i = 0; i < sizeof(CB_ACTIONS); ++i) {
+ if (action & CB_ACTIONS[i]) {
+ if ((r=nr_reg_unregister_callback(name, CB_ACTIONS[i], cb)))
+ ABORT(r);
+
+ action &= ~(CB_ACTIONS[i]);
+ }
+ }
+
+ if (action)
+ ABORT(R_BAD_ARGS);
+
+ _status=0;
+ abort:
+ return(_status);
+}
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/share/nr_api.h b/dom/media/webrtc/transport/third_party/nrappkit/src/share/nr_api.h
new file mode 100644
index 0000000000..ce6055f0d9
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/share/nr_api.h
@@ -0,0 +1,51 @@
+/**
+ nr_pce.h
+
+
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ ekr@networkresonance.com Wed Jul 19 13:18:39 2006
+ */
+
+
+#ifndef _nr_pce_h
+#define _nr_pce_h
+
+#include <sys/queue.h>
+#include <csi_platform.h>
+#include <r_common.h>
+#include <r_log.h>
+#include <nrstats.h>
+#include <nr_plugin.h>
+#include <async_wait.h>
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/share/nr_common.h b/dom/media/webrtc/transport/third_party/nrappkit/src/share/nr_common.h
new file mode 100644
index 0000000000..ac3ff44004
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/share/nr_common.h
@@ -0,0 +1,108 @@
+/**
+ nr_common.h
+
+
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+ */
+
+
+#ifndef _nr_common_h
+#define _nr_common_h
+
+#include <csi_platform.h>
+
+#ifdef USE_MPATROL
+#define USEDEBUG 1
+#include <mpatrol.h>
+#endif
+
+#ifdef USE_DMALLOC
+#include <dmalloc.h>
+#endif
+
+#include <string.h>
+#include <time.h>
+
+#ifdef WIN32
+#include <winsock2.h>
+#include <errno.h>
+#else
+#include <sys/errno.h>
+#endif
+
+#ifdef HAVE_SYS_TIME_H
+#include <sys/time.h>
+#endif
+
+#include <sys/types.h>
+#include <sys/queue.h>
+#include <r_log.h>
+
+extern int NR_LOG_REASSD;
+
+#include "registry.h"
+#include "nrstats.h"
+
+typedef struct nr_captured_packet_ {
+ UCHAR cap_interface; /* 1 for primary, 2 for secondary */
+ struct timeval ts; /* The time this packet was captured */
+ UINT4 len; /* The length of the packet */
+ UINT8 packet_number; /* The listener's packet index */
+} nr_captured_packet;
+
+#ifndef NR_ROOT_PATH
+#define NR_ROOT_PATH "/usr/local/ctc/"
+#endif
+
+#define NR_ARCHIVE_DIR NR_ROOT_PATH "archive/"
+#define NR_TEMP_DIR NR_ROOT_PATH "tmp/"
+#define NR_ARCHIVE_STATEFILE NR_ROOT_PATH "archive/state"
+#define NR_CAPTURED_PID_FILENAME NR_ROOT_PATH "captured.pid"
+#define NR_REASSD_PID_FILENAME NR_ROOT_PATH "reassd.pid"
+#define NR_MODE_FILENAME NR_ROOT_PATH "mode.txt"
+
+char *nr_revision_number(void);
+
+
+
+
+/* Memory buckets for CTC memory types */
+#define NR_MEM_TCP 1
+#define NR_MEM_HTTP 2
+#define NR_MEM_DELIVERY 3
+#define NR_MEM_OUT_HM 4
+#define NR_MEM_OUT_SSL 5
+#define NR_MEM_SSL 7
+#define NR_MEM_COMMON 8
+#define NR_MEM_CODEC 9
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/share/nr_reg_keys.h b/dom/media/webrtc/transport/third_party/nrappkit/src/share/nr_reg_keys.h
new file mode 100644
index 0000000000..b051f51d4a
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/share/nr_reg_keys.h
@@ -0,0 +1,167 @@
+/*
+ *
+ * nr_reg_keys.h
+ *
+ * $Source: /Users/ekr/tmp/nrappkit-dump/nrappkit/src/share/nr_reg_keys.h,v $
+ * $Revision: 1.3 $
+ * $Date: 2008/01/29 00:34:00 $
+ *
+ *
+ * Copyright (C) 2006, Network Resonance, Inc.
+ * All Rights Reserved
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Network Resonance, Inc. nor the name of any
+ * contributors to this software may be used to endorse or promote
+ * products derived from this software without specific prior written
+ * permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ *
+ */
+
+#ifndef __NR_REG_KEYS_H__
+#define __NR_REG_KEYS_H__
+
+#include <stdio.h>
+
+#define NR_REG_NAME_LENGTH_MIN 1
+#define NR_REG_NAME_LENGTH_MAX 32
+
+#define NR_REG_HOSTNAME "hostname"
+#define NR_REG_ADDRESS "address"
+#define NR_REG_NETMASKSIZE "netmasksize"
+#define NR_REG_ADDRESS_NETMASKSIZE "address/netmasksize" /* use only in clic files */
+#define NR_REG_PORT "port"
+
+#define NR_REG_LOGGING_SYSLOG_ENABLED "logging.syslog.enabled"
+#define NR_REG_LOGGING_SYSLOG_SERVERS "logging.syslog.servers"
+#define NR_REG_LOGGING_SYSLOG_FACILITY "logging.syslog.facility"
+#define NR_REG_LOGGING_SYSLOG_LEVEL "logging.syslog.level"
+
+#define NR_REG_CAPTURED_DAEMONS "captured.daemons"
+
+#define NR_REG_LISTEND_ENABLED "listend.enabled"
+
+#define NR_REG_LISTEND_MAX_INPUT_BUFFER_SIZE "listend.max_input_buffer_size"
+#define NR_REG_LISTEND_MAX_INPUT_BUFFER_SIZE_MIN 1 // 1 byte?
+#define NR_REG_LISTEND_MAX_INPUT_BUFFER_SIZE_MAX (10ULL*1024*1024*1024) // 10 GB
+
+#define NR_REG_LISTEND_INTERFACE "listend.interface"
+#define NR_REG_LISTEND_INTERFACE_PRIMARY "listend.interface.primary"
+#define NR_REG_LISTEND_INTERFACE_SECONDARY "listend.interface.secondary"
+#define NR_REG_LISTEND_LISTEN_ON_BOTH_INTERFACES "listend.interface.listen_on_both_interfaces"
+#define NR_REG_LISTEND_ENABLE_VLAN "listend.enable_vlan"
+
+#define NR_REG_LISTEND_LISTEN_TO "listend.listen_to"
+#define NR_REG_LISTEND_IGNORE "listend.ignore"
+
+#define NR_REG_LISTEND_PORT_MIN 0
+#define NR_REG_LISTEND_PORT_MAX 65535
+
+#define NR_REG_REASSD_MAX_MEMORY_CONSUMPTION "reassd.max_memory_consumption"
+#define NR_REG_REASSD_MAX_MEMORY_CONSUMPTION_MIN (10*1024) // 100 KB
+#define NR_REG_REASSD_MAX_MEMORY_CONSUMPTION_MAX (10ULL*1024*1024*1024) // 10 GB
+
+#define NR_REG_REASSD_DECODER_TCP_IGNORE_CHECKSUMS "reassd.decoder.tcp.ignore_checksums"
+
+#define NR_REG_REASSD_DECODER_TCP_MAX_CONNECTIONS_IN_SYN_STATE "reassd.decoder.tcp.max_connections_in_syn_state"
+#define NR_REG_REASSD_DECODER_TCP_MAX_CONNECTIONS_IN_SYN_STATE_MIN 1
+#define NR_REG_REASSD_DECODER_TCP_MAX_CONNECTIONS_IN_SYN_STATE_MAX 500000
+
+#define NR_REG_REASSD_DECODER_TCP_MAX_SIMULTANEOUS_CONNECTIONS "reassd.decoder.tcp.max_simultaneous_connections"
+#define NR_REG_REASSD_DECODER_TCP_MAX_SIMULTANEOUS_CONNECTIONS_MIN 1
+#define NR_REG_REASSD_DECODER_TCP_MAX_SIMULTANEOUS_CONNECTIONS_MAX 1000000 // 1 million
+
+#define NR_REG_REASSD_DECODER_SSL_MAX_SESSION_CACHE_SIZE "reassd.decoder.ssl.max_session_cache_size"
+#define NR_REG_REASSD_DECODER_SSL_MAX_SESSION_CACHE_SIZE_MIN 0
+#define NR_REG_REASSD_DECODER_SSL_MAX_SESSION_CACHE_SIZE_MAX (1ULL*1024*1024*1024) // 1GB
+
+#define NR_REG_REASSD_DECODER_SSL_REVEAL_LOCAL_KEYS "reassd.decoder.ssl.reveal.local.keys"
+
+#define NR_REG_REASSD_DECODER_HTTP_HANGING_RESPONSE_TIMEOUT "reassd.decoder.http.hanging_response_timeout"
+#define NR_REG_REASSD_DECODER_HTTP_HANGING_RESPONSE_TIMEOUT_MIN 1
+#define NR_REG_REASSD_DECODER_HTTP_HANGING_RESPONSE_TIMEOUT_MAX 1023
+
+#define NR_REG_REASSD_DECODER_HTTP_HANGING_TRANSMISSION_TIMEOUT "reassd.decoder.http.hanging_transmission_timeout"
+#define NR_REG_REASSD_DECODER_HTTP_HANGING_TRANSMISSION_TIMEOUT_MIN 1
+#define NR_REG_REASSD_DECODER_HTTP_HANGING_TRANSMISSION_TIMEOUT_MAX 1023
+
+#define NR_REG_REASSD_DECODER_HTTP_MAX_HTTP_MESSAGE_SIZE "reassd.decoder.http.max_http_message_size"
+#define NR_REG_REASSD_DECODER_HTTP_MAX_HTTP_MESSAGE_SIZE_MIN 0
+#define NR_REG_REASSD_DECODER_HTTP_MAX_HTTP_MESSAGE_SIZE_MAX (10ULL*1024*1024*1024)
+
+/* PCE-only: */
+#define NR_REG_LISTEND_ARCHIIVE "listend.archive"
+#define NR_REG_LISTEND_ARCHIVE_MAX_SIZE "listend.archive.max_size"
+#define NR_REG_LISTEND_ARCHIVE_MAX_SIZE_MIN 0
+#define NR_REG_LISTEND_ARCHIVE_MAX_SIZE_MAX (10ULL*1024*1024*1024*1024) // 10 TB
+
+#define NR_REG_LISTEND_ARCHIVE_RECORDING_ENABLED "listend.archive.recording_enabled"
+
+#define NR_REG_REASSD_DECODER_NET_DELIVER_BATCH_INTERVAL "reassd.decoder.net_deliver.batch_interval"
+#define NR_REG_REASSD_DECODER_NET_DELIVER_BATCH_INTERVAL_MIN 0
+#define NR_REG_REASSD_DECODER_NET_DELIVER_BATCH_INTERVAL_MAX 1023
+
+#define NR_REG_REASSD_DECODER_NET_DELIVER_MAX_QUEUE_DEPTH "reassd.decoder.net_deliver.max_queue_depth"
+#define NR_REG_REASSD_DECODER_NET_DELIVER_MAX_QUEUE_DEPTH_MIN 0
+#define NR_REG_REASSD_DECODER_NET_DELIVER_MAX_QUEUE_DEPTH_MAX (1ULL*1024*1024*1024) // 1 GB
+
+#define NR_REG_REASSD_DECODER_NET_DELIVER_MY_KEY_CERTIFICATE "reassd.decoder.net_deliver.my_key.certificate"
+#define NR_REG_REASSD_DECODER_NET_DELIVER_MY_KEY_PRIVATE_KEY "reassd.decoder.net_deliver.my_key.private_key"
+#define NR_REG_REASSD_DECODER_NET_DELIVER_PEER "reassd.decoder.net_deliver.peer"
+#define NR_REG_REASSD_DECODER_NET_DELIVER_PEER_PORT_MIN 0
+#define NR_REG_REASSD_DECODER_NET_DELIVER_PEER_PORT_MAX 65535
+
+#define NR_REG_REASSD_DECODER_NET_DELIVER_POLLING_INTERVAL "reassd.decoder.net_deliver.polling_interval"
+#define NR_REG_REASSD_DECODER_NET_DELIVER_POLLING_INTERVAL_MIN 1
+#define NR_REG_REASSD_DECODER_NET_DELIVER_POLLING_INTERVAL_MAX 1023
+
+#define NR_REG_REASSD_DECODER_NET_DELIVER_STATELESS "reassd.decoder.net_deliver.stateless"
+#define NR_REG_REASSD_DECODER_NET_DELIVER_WATCHDOG_TIMER "reassd.decoder.net_deliver.watchdog_timer"
+#define NR_REG_REASSD_DECODER_NET_DELIVER_WATCHDOG_TIMER_MIN 0
+#define NR_REG_REASSD_DECODER_NET_DELIVER_WATCHDOG_TIMER_MAX 666
+
+/* ASA-only: */
+#define NR_REG_MIGRATE_ENABLED "migrate.enabled"
+
+#define NR_REG_MIGRATE_INACTIVITY_TIMEOUT "migrate.inactivity_timeout"
+#define NR_REG_MIGRATE_INACTIVITY_TIMEOUT_MIN 0
+#define NR_REG_MIGRATE_INACTIVITY_TIMEOUT_MAX 1023
+
+#define NR_REG_MIGRATE_MIN_LOCAL_SIZE "migrate.min_local_size"
+#define NR_REG_MIGRATE_MIN_OVERLAP_SIZE "migrate.min_overlap_size"
+#define NR_REG_MIGRATE_RETRANSMIT_FREQUENCY "migrate.retransmit_frequency"
+#define NR_REG_MIGRATE_RETRIES "migrate.retries"
+#define NR_MIGRATE_LOCATION_NUMBER_MIN 0
+#define NR_MIGRATE_LOCATION_NUMBER_MAX 255
+
+#define NR_REG_REVELATION_ENABLE "revelation.enabled"
+#define NR_REG_REVELATION_MAX_PER_HOUR "revelation.max_per_hour"
+#define NR_REG_REVELATION_MAX_PER_HOUR_PER_PORTAL "revelation.max_per_hour_per_portal"
+
+/* Appliance-only: */
+#define NR_REG_SNMP_ENABLED "snmp.enabled"
+#define NR_REG_CLOCK_TIMEZONE "clock.timezone"
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/stats/nrstats.h b/dom/media/webrtc/transport/third_party/nrappkit/src/stats/nrstats.h
new file mode 100644
index 0000000000..0b6c2bc3c3
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/stats/nrstats.h
@@ -0,0 +1,118 @@
+/*
+ *
+ * nrstats.h
+ *
+ * $Source: /Users/ekr/tmp/nrappkit-dump/nrappkit/src/stats/nrstats.h,v $
+ * $Revision: 1.4 $
+ * $Date: 2007/06/26 22:37:55 $
+ *
+ * API for keeping and sharing statistics
+ *
+ *
+ * Copyright (C) 2003-2005, Network Resonance, Inc.
+ * Copyright (C) 2006, Network Resonance, Inc.
+ * All Rights Reserved
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ *
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of Network Resonance, Inc. nor the name of any
+ * contributors to this software may be used to endorse or promote
+ * products derived from this software without specific prior written
+ * permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ * POSSIBILITY OF SUCH DAMAGE.
+ *
+ *
+ */
+
+#ifndef __NRSTATS_H__
+#define __NRSTATS_H__
+
+#include <sys/types.h>
+#ifdef WIN32
+#include <time.h>
+#else
+#include <sys/time.h>
+#endif
+#include <r_types.h>
+
+#ifndef CAPTURE_USER
+#define CAPTURE_USER "pcecap"
+#endif
+
+#define NR_MAX_STATS_TYPES 256 /* max number of stats objects */
+#define NR_MAX_STATS_TYPE_NAME 26
+
+typedef struct NR_stats_type_ {
+ char name[NR_MAX_STATS_TYPE_NAME];
+ int (*reset)(void *stats);
+ int (*print)(void *stats, char *stat_namespace, void (*output)(void *handle, const char *fmt, ...), void *handle);
+ int (*get_lib_name)(char **libname);
+ unsigned int size;
+} NR_stats_type;
+
+typedef struct NR_stats_app_ {
+ time_t last_counter_reset;
+ time_t last_restart;
+ UINT8 total_restarts;
+ char version[64];
+} NR_stats_app;
+
+extern NR_stats_type *NR_stats_type_app;
+
+/* everything measured in bytes */
+typedef struct NR_stats_memory_ {
+ UINT8 current_size;
+ UINT8 max_size;
+ UINT8 in_use;
+ UINT8 in_use_max;
+} NR_stats_memory;
+
+extern NR_stats_type *NR_stats_type_memory;
+
+/* all functions below return 0 on success, else they return an
+ * error code */
+
+/* if errprintf is null, warnings and errors will be sent to /dev/null */
+extern int NR_stats_startup(char *app_name, char *user_name, void (*errprintf)(void *handle, const char *fmt, ...), void *errhandle);
+extern int NR_stats_shutdown(void);
+#define NR_STATS_CREATE (1<<0)
+extern int NR_stats_get(char *module_name, NR_stats_type *type, int flag, void **stats);
+extern int NR_stats_clear(void *stats); /* zeroizes */
+extern int NR_stats_reset(void *stats); /* zeros "speedometers" */
+extern int NR_stats_register(NR_stats_type *type);
+extern int NR_stats_acquire_mutex(void *stats);
+extern int NR_stats_release_mutex(void *stats);
+// TODO: should _get_names take an app_name argument (0==all)????
+extern int NR_stats_get_names(unsigned int *nnames, char ***names);
+extern int NR_stats_get_by_name(char *name, NR_stats_type **type, void **stats);
+extern int NR_stats_get_lib_name(void *stats, char **lib_name);
+extern int NR_stats_rmids(void);
+
+extern char *NR_prefix_to_stats_module(char *prefix);
+
+#define NR_INCREMENT_STAT(stat) do { \
+ stat++; if(stat>stat##_max) stat##_max=stat; \
+ } while (0)
+#define NR_UPDATE_STAT(stat,newval) do { \
+ stat=newval; if(stat>stat##_max) stat##_max=stat; \
+ } while (0)
+
+#endif
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/util/byteorder.c b/dom/media/webrtc/transport/third_party/nrappkit/src/util/byteorder.c
new file mode 100644
index 0000000000..64689accfa
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/util/byteorder.c
@@ -0,0 +1,73 @@
+/**
+ byteorder.c
+
+
+ Copyright (C) 2007, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ briank@networkresonance.com Wed May 16 16:46:00 PDT 2007
+ */
+
+#include "nr_common.h"
+#ifndef WIN32
+#include <arpa/inet.h>
+#endif
+#include "r_types.h"
+#include "byteorder.h"
+
+#define IS_BIG_ENDIAN (htonl(0x1) == 0x1)
+
+#define BYTE(n,i) (((UCHAR*)&(n))[(i)])
+#define SWAP(n,x,y) tmp=BYTE((n),(x)), \
+ BYTE((n),(x))=BYTE((n),(y)), \
+ BYTE((n),(y))=tmp
+
+UINT8
+nr_htonll(UINT8 hostlonglong)
+{
+ UINT8 netlonglong = hostlonglong;
+ UCHAR tmp;
+
+ if (!IS_BIG_ENDIAN) {
+ SWAP(netlonglong, 0, 7);
+ SWAP(netlonglong, 1, 6);
+ SWAP(netlonglong, 2, 5);
+ SWAP(netlonglong, 3, 4);
+ }
+
+ return netlonglong;
+}
+
+UINT8
+nr_ntohll(UINT8 netlonglong)
+{
+ return nr_htonll(netlonglong);
+}
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/util/byteorder.h b/dom/media/webrtc/transport/third_party/nrappkit/src/util/byteorder.h
new file mode 100644
index 0000000000..8df0589a63
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/util/byteorder.h
@@ -0,0 +1,47 @@
+/**
+ byteorder.h
+
+
+ Copyright (C) 2007, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ briank@networkresonance.com Wed May 16 16:46:00 PDT 2007
+ */
+
+
+#ifndef _byteorder_h
+#define _byteorder_h
+
+UINT8 nr_htonll(UINT8 hostlonglong);
+
+UINT8 nr_ntohll(UINT8 netlonglong);
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/util/hex.c b/dom/media/webrtc/transport/third_party/nrappkit/src/util/hex.c
new file mode 100644
index 0000000000..8212988eda
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/util/hex.c
@@ -0,0 +1,109 @@
+/**
+ hex.c
+
+
+ Copyright (C) 2001-2003, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ briank@network-resonance.com
+ */
+
+#include <sys/types.h>
+#include <string.h>
+#include <ctype.h>
+#include "r_common.h"
+#include "hex.h"
+#include "r_log.h"
+
+static char bin2hex_map[][3] = { "00", "01", "02", "03", "04", "05", "06", "07", "08", "09", "0a", "0b", "0c", "0d", "0e", "0f", "10", "11", "12", "13", "14", "15", "16", "17", "18", "19", "1a", "1b", "1c", "1d", "1e", "1f", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "2a", "2b", "2c", "2d", "2e", "2f", "30", "31", "32", "33", "34", "35", "36", "37", "38", "39", "3a", "3b", "3c", "3d", "3e", "3f", "40", "41", "42", "43", "44", "45", "46", "47", "48", "49", "4a", "4b", "4c", "4d", "4e", "4f", "50", "51", "52", "53", "54", "55", "56", "57", "58", "59", "5a", "5b", "5c", "5d", "5e", "5f", "60", "61", "62", "63", "64", "65", "66", "67", "68", "69", "6a", "6b", "6c", "6d", "6e", "6f", "70", "71", "72", "73", "74", "75", "76", "77", "78", "79", "7a", "7b", "7c", "7d", "7e", "7f", "80", "81", "82", "83", "84", "85", "86", "87", "88", "89", "8a", "8b", "8c", "8d", "8e", "8f", "90", "91", "92", "93", "94", "95", "96", "97", "98", "99", "9a", "9b", "9c", "9d", "9e", "9f", "a0", "a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "aa", "ab", "ac", "ad", "ae", "af", "b0", "b1", "b2", "b3", "b4", "b5", "b6", "b7", "b8", "b9", "ba", "bb", "bc", "bd", "be", "bf", "c0", "c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9", "ca", "cb", "cc", "cd", "ce", "cf", "d0", "d1", "d2", "d3", "d4", "d5", "d6", "d7", "d8", "d9", "da", "db", "dc", "dd", "de", "df", "e0", "e1", "e2", "e3", "e4", "e5", "e6", "e7", "e8", "e9", "ea", "eb", "ec", "ed", "ee", "ef", "f0", "f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "fa", "fb", "fc", "fd", "fe", "ff" };
+
+static int hex2bin_map[] = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 0 /* '0' */, 1 /* '1' */, 2 /* '2' */, 3 /* '3' */, 4 /* '4' */, 5 /* '5' */, 6 /* '6' */, 7 /* '7' */, 8 /* '8' */, 9 /* '9' */, -1, -1, -1, -1, -1, -1, -1, 10 /* 'A' */, 11 /* 'B' */, 12 /* 'C' */, 13 /* 'D' */, 14 /* 'E' */, 15 /* 'F' */, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 10 /* 'a' */, 11 /* 'b' */, 12 /* 'c' */, 13 /* 'd' */, 14 /* 'e' */, 15 /* 'f' */, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1 };
+
+int
+nr_nbin2hex(UCHAR *bin, size_t binlen, char hex[], size_t size, size_t *len)
+{
+ int _status;
+ size_t i;
+ size_t hexlen;
+
+ hexlen = 2*binlen;
+ if (size < hexlen)
+ ABORT(R_BAD_ARGS);
+
+ for (i = 0; i < binlen; ++i) {
+ *hex++ = bin2hex_map[bin[i]][0];
+ *hex++ = bin2hex_map[bin[i]][1];
+ }
+
+ if (size >= hexlen+1)
+ *hex = '\0';
+
+ *len = hexlen;
+
+ _status=0;
+ abort:
+ return(_status);
+}
+
+
+int
+nr_nhex2bin(char *hex, size_t hexlen, UCHAR bin[], size_t size, size_t *len)
+{
+ int _status;
+ size_t binlen;
+ int h1;
+ int h2;
+ size_t i;
+
+ if (hexlen % 2)
+ ABORT(R_BAD_ARGS);
+
+ binlen = hexlen/2;
+
+ if (size < binlen)
+ ABORT(R_BAD_ARGS);
+
+ for (i = 0; i < binlen; ++i) {
+ h1 = hex2bin_map[(int)*hex++];
+ h2 = hex2bin_map[(int)*hex++];
+
+ if (h1 == -1 || h2 == -1)
+ ABORT(R_BAD_ARGS);
+
+ bin[i] = (h1 << 4) | h2;
+ }
+
+ *len = binlen;
+
+ _status=0;
+ abort:
+ return(_status);
+}
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/util/hex.h b/dom/media/webrtc/transport/third_party/nrappkit/src/util/hex.h
new file mode 100644
index 0000000000..8c493b533b
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/util/hex.h
@@ -0,0 +1,47 @@
+/**
+ hex.h
+
+
+ Copyright (C) 2001-2003, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ briank@network-resonance.com
+ */
+
+
+#ifndef _hex_h
+#define _hex_h
+
+int nr_nbin2hex(UCHAR *bin, size_t binlen, char hex[], size_t size, size_t *len);
+int nr_nhex2bin(char *hex, size_t hexlen, UCHAR bin[], size_t size, size_t *len);
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/assoc.h b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/assoc.h
new file mode 100644
index 0000000000..013e788685
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/assoc.h
@@ -0,0 +1,90 @@
+/**
+ assoc.h
+
+
+ Copyright (C) 2002-2003, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ */
+
+/**
+ assoc.h
+
+
+ Copyright (C) 1999-2000 RTFM, Inc.
+ All Rights Reserved
+
+ This package is a SSLv3/TLS protocol analyzer written by Eric Rescorla
+ <ekr@rtfm.com> and licensed by RTFM, Inc.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. All advertising materials mentioning features or use of this software
+ must display the following acknowledgement:
+
+ This product includes software developed by Eric Rescorla for
+ RTFM, Inc.
+
+ 4. Neither the name of RTFM, Inc. nor the name of Eric Rescorla may be
+ used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY ERIC RESCORLA AND RTFM, INC. ``AS IS'' AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY SUCH DAMAGE.
+
+ $Id: assoc.h,v 1.2 2006/08/16 19:39:17 adamcain Exp $
+
+
+ ekr@rtfm.com Sun Jan 17 17:56:35 1999
+ */
+
+
+#ifndef _assoc_h
+#define _assoc_h
+
+typedef struct assoc_ assoc;
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/debug.c b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/debug.c
new file mode 100644
index 0000000000..fa796217ea
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/debug.c
@@ -0,0 +1,127 @@
+/**
+ debug.c
+
+
+ Copyright (C) 2002-2003, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ */
+
+/**
+ debug.c
+
+
+ Copyright (C) 1999-2000 RTFM, Inc.
+ All Rights Reserved
+
+ This package is a SSLv3/TLS protocol analyzer written by Eric Rescorla
+ <ekr@rtfm.com> and licensed by RTFM, Inc.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. All advertising materials mentioning features or use of this software
+ must display the following acknowledgement:
+
+ This product includes software developed by Eric Rescorla for
+ RTFM, Inc.
+
+ 4. Neither the name of RTFM, Inc. nor the name of Eric Rescorla may be
+ used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY ERIC RESCORLA AND RTFM, INC. ``AS IS'' AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY SUCH DAMAGE.
+
+ $Id: debug.c,v 1.3 2007/06/26 22:37:57 adamcain Exp $
+
+
+ ekr@rtfm.com Wed Jan 6 17:08:58 1999
+ */
+
+#include <stdarg.h>
+#include <stdio.h>
+#include "r_common.h"
+#include "debug.h"
+
+int nr_debug(int class,char *format,...)
+ {
+ va_list ap;
+
+ va_start(ap,format);
+#ifdef WIN32
+ vprintf(format,ap);
+ printf("\n");
+#else
+ vfprintf(stderr,format,ap);
+ fprintf(stderr,"\n");
+#endif
+ return(0);
+ }
+
+int nr_xdump(name,data,len)
+ char *name;
+ UCHAR *data;
+ int len;
+ {
+ int i;
+
+ if(name){
+ printf("%s[%d]=\n",name,len);
+ }
+ for(i=0;i<len;i++){
+
+ if((len>8) && i && !(i%12)){
+ printf("\n");
+ }
+ printf("%.2x ",data[i]&255);
+ }
+ if(i%12)
+ printf("\n");
+ return(0);
+ }
+
+
+
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/debug.h b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/debug.h
new file mode 100644
index 0000000000..34f7b2fb54
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/debug.h
@@ -0,0 +1,94 @@
+/**
+ debug.h
+
+
+ Copyright (C) 2002-2003, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ */
+
+/**
+ debug.h
+
+
+ Copyright (C) 1999-2000 RTFM, Inc.
+ All Rights Reserved
+
+ This package is a SSLv3/TLS protocol analyzer written by Eric Rescorla
+ <ekr@rtfm.com> and licensed by RTFM, Inc.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. All advertising materials mentioning features or use of this software
+ must display the following acknowledgement:
+
+ This product includes software developed by Eric Rescorla for
+ RTFM, Inc.
+
+ 4. Neither the name of RTFM, Inc. nor the name of Eric Rescorla may be
+ used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY ERIC RESCORLA AND RTFM, INC. ``AS IS'' AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY SUCH DAMAGE.
+
+ $Id: debug.h,v 1.2 2006/08/16 19:39:17 adamcain Exp $
+
+
+ ekr@rtfm.com Wed Jan 6 17:13:00 1999
+ */
+
+
+#ifndef _debug_h
+#define _debug_h
+
+// Remove debugging mess from ssldump
+// #define DBG(a)
+
+int nr_debug(int class,char *format,...);
+int nr_xdump(char *name,UCHAR *data, int len);
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_assoc.c b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_assoc.c
new file mode 100644
index 0000000000..25b3827d50
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_assoc.c
@@ -0,0 +1,539 @@
+/**
+ r_assoc.c
+
+
+ Copyright (C) 2002-2003, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ */
+
+/**
+ r_assoc.c
+
+ This is an associative array implementation, using an open-chained
+ hash bucket technique.
+
+ Note that this implementation permits each data entry to have
+ separate copy constructors and destructors. This currently wastes
+ space, but could be implemented while saving space by using
+ the high order bit of the length value or somesuch.
+
+ The major problem with this code is it's not resizable, though it
+ could be made so.
+
+
+ Copyright (C) 1999-2000 RTFM, Inc.
+ All Rights Reserved
+
+ This package is a SSLv3/TLS protocol analyzer written by Eric Rescorla
+ <ekr@rtfm.com> and licensed by RTFM, Inc.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. All advertising materials mentioning features or use of this software
+ must display the following acknowledgement:
+
+ This product includes software developed by Eric Rescorla for
+ RTFM, Inc.
+
+ 4. Neither the name of RTFM, Inc. nor the name of Eric Rescorla may be
+ used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY ERIC RESCORLA AND RTFM, INC. ``AS IS'' AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY SUCH DAMAGE.
+
+ $Id: r_assoc.c,v 1.4 2007/06/08 17:41:49 adamcain Exp $
+
+ ekr@rtfm.com Sun Jan 17 17:57:15 1999
+ */
+
+#include <r_common.h>
+#include <string.h>
+#include "r_assoc.h"
+
+typedef struct r_assoc_el_ {
+ char *key;
+ int key_len;
+ void *data;
+ struct r_assoc_el_ *prev;
+ struct r_assoc_el_ *next;
+ int (*copy)(void **n,void *old);
+ int (*destroy)(void *ptr);
+} r_assoc_el;
+
+struct r_assoc_ {
+ int size;
+ int bits;
+ int (*hash_func)(char *key,int len,int size);
+ r_assoc_el **chains;
+ UINT4 num_elements;
+};
+
+#define DEFAULT_TABLE_BITS 5
+
+static int destroy_assoc_chain(r_assoc_el *chain);
+static int r_assoc_fetch_bucket(r_assoc *assoc,
+ char *key,int len,r_assoc_el **bucketp);
+static int copy_assoc_chain(r_assoc_el **knewp, r_assoc_el *old);
+
+int r_assoc_create(assocp,hash_func,bits)
+ r_assoc **assocp;
+ int (*hash_func)(char *key,int len,int size);
+ int bits;
+ {
+ r_assoc *assoc=0;
+ int _status;
+
+ if(!(assoc=(r_assoc *)RCALLOC(sizeof(r_assoc))))
+ ABORT(R_NO_MEMORY);
+ assoc->size=(1<<bits);
+ assoc->bits=bits;
+ assoc->hash_func=hash_func;
+
+ if(!(assoc->chains=(r_assoc_el **)RCALLOC(sizeof(r_assoc_el *)*
+ assoc->size)))
+ ABORT(R_NO_MEMORY);
+
+ *assocp=assoc;
+
+ _status=0;
+ abort:
+ if(_status){
+ r_assoc_destroy(&assoc);
+ }
+ return(_status);
+ }
+
+int r_assoc_destroy(assocp)
+ r_assoc **assocp;
+ {
+ r_assoc *assoc;
+ int i;
+
+ if(!assocp || !*assocp)
+ return(0);
+
+ assoc=*assocp;
+ for(i=0;i<assoc->size;i++)
+ destroy_assoc_chain(assoc->chains[i]);
+
+ RFREE(assoc->chains);
+ RFREE(*assocp);
+
+ return(0);
+ }
+
+static int destroy_assoc_chain(chain)
+ r_assoc_el *chain;
+ {
+ r_assoc_el *nxt;
+
+ while(chain){
+ nxt=chain->next;
+
+ if(chain->destroy)
+ chain->destroy(chain->data);
+
+ RFREE(chain->key);
+
+ RFREE(chain);
+ chain=nxt;
+ }
+
+ return(0);
+ }
+
+static int copy_assoc_chain(knewp,old)
+ r_assoc_el **knewp;
+ r_assoc_el *old;
+ {
+ r_assoc_el *knew=0,*ptr,*tmp;
+ int r,_status;
+
+ ptr=0; /* Pacify GCC's uninitialized warning.
+ It's not correct */
+ if(!old) {
+ *knewp=0;
+ return(0);
+ }
+ for(;old;old=old->next){
+ if(!(tmp=(r_assoc_el *)RCALLOC(sizeof(r_assoc_el))))
+ ABORT(R_NO_MEMORY);
+
+ if(!knew){
+ knew=tmp;
+ ptr=knew;
+ }
+ else{
+ ptr->next=tmp;
+ tmp->prev=ptr;
+ ptr=tmp;
+ }
+
+ ptr->destroy=old->destroy;
+ ptr->copy=old->copy;
+
+ if(old->copy){
+ if(r=old->copy(&ptr->data,old->data))
+ ABORT(r);
+ }
+ else
+ ptr->data=old->data;
+
+ if(!(ptr->key=(char *)RMALLOC(old->key_len)))
+ ABORT(R_NO_MEMORY);
+ memcpy(ptr->key,old->key,ptr->key_len=old->key_len);
+ }
+
+ *knewp=knew;
+
+ _status=0;
+ abort:
+ if(_status){
+ destroy_assoc_chain(knew);
+ }
+ return(_status);
+ }
+
+static int r_assoc_fetch_bucket(assoc,key,len,bucketp)
+ r_assoc *assoc;
+ char *key;
+ int len;
+ r_assoc_el **bucketp;
+ {
+ UINT4 hash_value;
+ r_assoc_el *bucket;
+
+ hash_value=assoc->hash_func(key,len,assoc->bits);
+
+ for(bucket=assoc->chains[hash_value];bucket;bucket=bucket->next){
+ if(bucket->key_len == len && !memcmp(bucket->key,key,len)){
+ *bucketp=bucket;
+ return(0);
+ }
+ }
+
+ return(R_NOT_FOUND);
+ }
+
+int r_assoc_fetch(assoc,key,len,datap)
+ r_assoc *assoc;
+ char *key;
+ int len;
+ void **datap;
+ {
+ r_assoc_el *bucket;
+ int r;
+
+ if(r=r_assoc_fetch_bucket(assoc,key,len,&bucket)){
+ if(r!=R_NOT_FOUND)
+ ERETURN(r);
+ return(r);
+ }
+
+ *datap=bucket->data;
+ return(0);
+ }
+
+int r_assoc_insert(assoc,key,len,data,copy,destroy,how)
+ r_assoc *assoc;
+ char *key;
+ int len;
+ void *data;
+ int (*copy)(void **knew,void *old);
+ int (*destroy)(void *ptr);
+ int how;
+ {
+ r_assoc_el *bucket,*new_bucket=0;
+ int r,_status;
+
+ if(r=r_assoc_fetch_bucket(assoc,key,len,&bucket)){
+ /*Note that we compute the hash value twice*/
+ UINT4 hash_value;
+
+ if(r!=R_NOT_FOUND)
+ ABORT(r);
+ hash_value=assoc->hash_func(key,len,assoc->bits);
+
+ if(!(new_bucket=(r_assoc_el *)RCALLOC(sizeof(r_assoc_el))))
+ ABORT(R_NO_MEMORY);
+ if(!(new_bucket->key=(char *)RMALLOC(len)))
+ ABORT(R_NO_MEMORY);
+ memcpy(new_bucket->key,key,len);
+ new_bucket->key_len=len;
+
+ /*Insert at the list head. Is FIFO a good algorithm?*/
+ if(assoc->chains[hash_value])
+ assoc->chains[hash_value]->prev=new_bucket;
+ new_bucket->next=assoc->chains[hash_value];
+ assoc->chains[hash_value]=new_bucket;
+ bucket=new_bucket;
+ }
+ else{
+ if(!(how&R_ASSOC_REPLACE))
+ ABORT(R_ALREADY);
+
+ if(bucket->destroy)
+ bucket->destroy(bucket->data);
+ }
+
+ bucket->data=data;
+ bucket->copy=copy;
+ bucket->destroy=destroy;
+ assoc->num_elements++;
+
+ _status=0;
+ abort:
+ if(_status && new_bucket){
+ RFREE(new_bucket->key);
+ RFREE(new_bucket);
+ }
+ return(_status);
+ }
+
+int r_assoc_delete(assoc,key,len)
+ r_assoc *assoc;
+ char *key;
+ int len;
+ {
+ int r;
+ r_assoc_el *bucket;
+ UINT4 hash_value;
+
+ if(r=r_assoc_fetch_bucket(assoc,key,len,&bucket)){
+ if(r!=R_NOT_FOUND)
+ ERETURN(r);
+ return(r);
+ }
+
+ /* Now remove the element from the hash chain */
+ if(bucket->prev){
+ bucket->prev->next=bucket->next;
+ }
+ else {
+ hash_value=assoc->hash_func(key,len,assoc->bits);
+ assoc->chains[hash_value]=bucket->next;
+ }
+
+ if(bucket->next)
+ bucket->next->prev=bucket->prev;
+
+ /* Remove the data */
+ if(bucket->destroy)
+ bucket->destroy(bucket->data);
+
+ RFREE(bucket->key);
+ RFREE(bucket);
+ assoc->num_elements--;
+
+ return(0);
+ }
+
+int r_assoc_copy(knewp,old)
+ r_assoc **knewp;
+ r_assoc *old;
+ {
+ int r,_status,i;
+ r_assoc *knew;
+
+ if(!(knew=(r_assoc *)RCALLOC(sizeof(r_assoc))))
+ ABORT(R_NO_MEMORY);
+ knew->size=old->size;
+ knew->bits=old->bits;
+ knew->hash_func=old->hash_func;
+
+ if(!(knew->chains=(r_assoc_el **)RCALLOC(sizeof(r_assoc_el)*old->size)))
+ ABORT(R_NO_MEMORY);
+ for(i=0;i<knew->size;i++){
+ if(r=copy_assoc_chain(knew->chains+i,old->chains[i]))
+ ABORT(r);
+ }
+ knew->num_elements=old->num_elements;
+
+ *knewp=knew;
+
+ _status=0;
+ abort:
+ if(_status){
+ r_assoc_destroy(&knew);
+ }
+ return(_status);
+ }
+
+int r_assoc_num_elements(r_assoc *assoc,int *sizep)
+ {
+ *sizep=assoc->num_elements;
+
+ return(0);
+ }
+
+int r_assoc_init_iter(assoc,iter)
+ r_assoc *assoc;
+ r_assoc_iterator *iter;
+ {
+ int i;
+
+ iter->assoc=assoc;
+ iter->prev_chain=-1;
+ iter->prev=0;
+
+ iter->next_chain=assoc->size;
+ iter->next=0;
+
+ for(i=0;i<assoc->size;i++){
+ if(assoc->chains[i]!=0){
+ iter->next_chain=i;
+ iter->next=assoc->chains[i];
+ break;
+ }
+ }
+
+ return(0);
+ }
+
+int r_assoc_iter(iter,key,keyl,val)
+ r_assoc_iterator *iter;
+ void **key;
+ int *keyl;
+ void **val;
+ {
+ int i;
+ r_assoc_el *ret;
+
+ if(!iter->next)
+ return(R_EOD);
+ ret=iter->next;
+
+ *key=ret->key;
+ *keyl=ret->key_len;
+ *val=ret->data;
+
+ /* Now increment */
+ iter->prev_chain=iter->next_chain;
+ iter->prev=iter->next;
+
+ /* More on this chain */
+ if(iter->next->next){
+ iter->next=iter->next->next;
+ }
+ else{
+ iter->next=0;
+
+ /* FInd the next occupied chain*/
+ for(i=iter->next_chain+1;i<iter->assoc->size;i++){
+ if(iter->assoc->chains[i]){
+ iter->next_chain=i;
+ iter->next=iter->assoc->chains[i];
+ break;
+ }
+ }
+ }
+
+ return(0);
+ }
+
+/* Delete the last returned value*/
+int r_assoc_iter_delete(iter)
+ r_assoc_iterator *iter;
+ {
+ /* First unhook it from the list*/
+ if(!iter->prev->prev){
+ /* First element*/
+ iter->assoc->chains[iter->prev_chain]=iter->prev->next;
+ }
+ else{
+ iter->prev->prev->next=iter->prev->next;
+ }
+
+ if(iter->prev->next){
+ iter->prev->next->prev=iter->prev->prev;
+ }
+
+ if (iter->prev->destroy)
+ iter->prev->destroy(iter->prev->data);
+
+ iter->assoc->num_elements--;
+ RFREE(iter->prev->key);
+ RFREE(iter->prev);
+ return(0);
+ }
+
+
+/*This is a hack from AMS. Supposedly, it's pretty good for strings, even
+ though it doesn't take into account all the data*/
+int r_assoc_simple_hash_compute(key,len,bits)
+ char *key;
+ int len;
+ int bits;
+ {
+ UINT4 h=0;
+
+ h=key[0] +(key[len-1] * len);
+
+ h &= (1<<bits) - 1;
+
+ return(h);
+ }
+
+
+int r_crc32(char *data,int len,UINT4 *crcval);
+
+int r_assoc_crc32_hash_compute(data,len,bits)
+ char *data;
+ int len;
+ int bits;
+ {
+ UINT4 res;
+ UINT4 mask;
+
+ /* First compute the CRC value */
+ if(r_crc32(data,len,&res))
+ ERETURN(R_INTERNAL);
+
+ mask=~(0xffffffff<<bits);
+
+ return(res & mask);
+ }
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_assoc.h b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_assoc.h
new file mode 100644
index 0000000000..5311542c9d
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_assoc.h
@@ -0,0 +1,126 @@
+/**
+ r_assoc.h
+
+
+ Copyright (C) 2002-2003, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ */
+
+/**
+ r_assoc.h
+
+ Associative array code. This code has the advantage that different
+ elements can have different create and destroy operators. Unfortunately,
+ this can waste space.
+
+
+ Copyright (C) 1999-2000 RTFM, Inc.
+ All Rights Reserved
+
+ This package is a SSLv3/TLS protocol analyzer written by Eric Rescorla
+ <ekr@rtfm.com> and licensed by RTFM, Inc.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. All advertising materials mentioning features or use of this software
+ must display the following acknowledgement:
+
+ This product includes software developed by Eric Rescorla for
+ RTFM, Inc.
+
+ 4. Neither the name of RTFM, Inc. nor the name of Eric Rescorla may be
+ used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY ERIC RESCORLA AND RTFM, INC. ``AS IS'' AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY SUCH DAMAGE.
+
+ $Id: r_assoc.h,v 1.3 2007/06/08 22:16:08 adamcain Exp $
+
+
+ ekr@rtfm.com Sun Jan 17 17:57:18 1999
+ */
+
+
+#ifndef _r_assoc_h
+#define _r_assoc_h
+
+typedef struct r_assoc_ r_assoc;
+
+int r_assoc_create(r_assoc **assocp,
+ int (*hash_func)(char *,int,int),
+ int bits);
+int r_assoc_insert(r_assoc *assoc,char *key,int len,
+ void *value,int (*copy)(void **knew,void *old),
+ int (*destroy)(void *ptr),int how);
+#define R_ASSOC_REPLACE 0x1
+#define R_ASSOC_NEW 0x2
+
+int r_assoc_fetch(r_assoc *assoc,char *key, int len, void **value);
+int r_assoc_delete(r_assoc *assoc,char *key, int len);
+
+int r_assoc_copy(r_assoc **knew,r_assoc *old);
+int r_assoc_destroy(r_assoc **assocp);
+int r_assoc_simple_hash_compute(char *key, int len,int bits);
+int r_assoc_crc32_hash_compute(char *key, int len,int bits);
+
+/*We need iterators, but I haven't written them yet*/
+typedef struct r_assoc_iterator_ {
+ r_assoc *assoc;
+ int prev_chain;
+ struct r_assoc_el_ *prev;
+ int next_chain;
+ struct r_assoc_el_ *next;
+} r_assoc_iterator;
+
+int r_assoc_init_iter(r_assoc *assoc,r_assoc_iterator *);
+int r_assoc_iter(r_assoc_iterator *iter,void **key,int *keyl, void **val);
+int r_assoc_iter_delete(r_assoc_iterator *);
+
+int r_assoc_num_elements(r_assoc *assoc,int *sizep);
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_common.h b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_common.h
new file mode 100644
index 0000000000..c11bb3d0fd
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_common.h
@@ -0,0 +1,100 @@
+/**
+ r_common.h
+
+
+ Copyright (C) 2002-2003, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ */
+
+/**
+ r_common.h
+
+
+ Copyright (C) 1999-2000 RTFM, Inc.
+ All Rights Reserved
+
+ This package is a SSLv3/TLS protocol analyzer written by Eric Rescorla
+ <ekr@rtfm.com> and licensed by RTFM, Inc.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. All advertising materials mentioning features or use of this software
+ must display the following acknowledgement:
+
+ This product includes software developed by Eric Rescorla for
+ RTFM, Inc.
+
+ 4. Neither the name of RTFM, Inc. nor the name of Eric Rescorla may be
+ used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY ERIC RESCORLA AND RTFM, INC. ``AS IS'' AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY SUCH DAMAGE.
+
+ $Id: r_common.h,v 1.2 2006/08/16 19:39:17 adamcain Exp $
+
+
+ ekr@rtfm.com Tue Dec 22 10:40:07 1998
+ */
+
+
+#ifndef _r_common_h
+#define _r_common_h
+
+#include "r_defaults.h"
+#include "r_includes.h"
+#include "r_types.h"
+#include "r_macros.h"
+#include "r_errors.h"
+#include "r_data.h"
+
+/* defines for possibly replaced functions */
+#ifndef HAVE_STRDUP
+char *strdup(char *in);
+#endif
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_crc32.c b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_crc32.c
new file mode 100644
index 0000000000..38d3e4da38
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_crc32.c
@@ -0,0 +1,175 @@
+/**
+ r_crc32.c
+
+
+ Copyright (C) 2002-2003, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ */
+
+/**
+ crc32.c
+
+ Copyright (C) 2003, RTFM, Inc.
+ All Rights Reserved.
+
+ ekr@rtfm.com Fri Jan 31 13:57:43 2003
+
+ THE FOLLOWING CODE WAS EXTRACTED FROM FreeBSD
+ The comment below was the original header
+
+ The main function was modified to process a buffer
+ rather than a file
+ */
+
+/*
+ * This code implements the AUTODIN II polynomial used by Ethernet,
+ * and can be used to calculate multicast address hash indices.
+ * It assumes that the low order bits will be transmitted first,
+ * and consequently the low byte should be sent first when
+ * the crc computation is finished. The crc should be complemented
+ * before transmission.
+ * The variable corresponding to the macro argument "crc" should
+ * be an unsigned long and should be preset to all ones for Ethernet
+ * use. An error-free packet will leave 0xDEBB20E3 in the crc.
+ * Spencer Garrett <srg@quick.com>
+ */
+
+
+#include <sys/types.h>
+#include <r_types.h>
+
+#ifdef WIN32
+#define u_int32_t UINT4
+#endif
+
+#define CRC(crc, ch) (crc = (crc >> 8) ^ crctab[(crc ^ (ch)) & 0xff])
+
+/* generated using the AUTODIN II polynomial
+ * x^32 + x^26 + x^23 + x^22 + x^16 +
+ * x^12 + x^11 + x^10 + x^8 + x^7 + x^5 + x^4 + x^2 + x^1 + 1
+ */
+static const u_int32_t crctab[256] = {
+ 0x00000000, 0x77073096, 0xee0e612c, 0x990951ba,
+ 0x076dc419, 0x706af48f, 0xe963a535, 0x9e6495a3,
+ 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
+ 0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91,
+ 0x1db71064, 0x6ab020f2, 0xf3b97148, 0x84be41de,
+ 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
+ 0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec,
+ 0x14015c4f, 0x63066cd9, 0xfa0f3d63, 0x8d080df5,
+ 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
+ 0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b,
+ 0x35b5a8fa, 0x42b2986c, 0xdbbbc9d6, 0xacbcf940,
+ 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
+ 0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116,
+ 0x21b4f4b5, 0x56b3c423, 0xcfba9599, 0xb8bda50f,
+ 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
+ 0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d,
+ 0x76dc4190, 0x01db7106, 0x98d220bc, 0xefd5102a,
+ 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
+ 0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818,
+ 0x7f6a0dbb, 0x086d3d2d, 0x91646c97, 0xe6635c01,
+ 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
+ 0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457,
+ 0x65b0d9c6, 0x12b7e950, 0x8bbeb8ea, 0xfcb9887c,
+ 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
+ 0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2,
+ 0x4adfa541, 0x3dd895d7, 0xa4d1c46d, 0xd3d6f4fb,
+ 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
+ 0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9,
+ 0x5005713c, 0x270241aa, 0xbe0b1010, 0xc90c2086,
+ 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
+ 0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4,
+ 0x59b33d17, 0x2eb40d81, 0xb7bd5c3b, 0xc0ba6cad,
+ 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
+ 0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683,
+ 0xe3630b12, 0x94643b84, 0x0d6d6a3e, 0x7a6a5aa8,
+ 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
+ 0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe,
+ 0xf762575d, 0x806567cb, 0x196c3671, 0x6e6b06e7,
+ 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
+ 0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5,
+ 0xd6d6a3e8, 0xa1d1937e, 0x38d8c2c4, 0x4fdff252,
+ 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
+ 0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60,
+ 0xdf60efc3, 0xa867df55, 0x316e8eef, 0x4669be79,
+ 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
+ 0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f,
+ 0xc5ba3bbe, 0xb2bd0b28, 0x2bb45a92, 0x5cb36a04,
+ 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
+ 0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a,
+ 0x9c0906a9, 0xeb0e363f, 0x72076785, 0x05005713,
+ 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
+ 0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21,
+ 0x86d3d2d4, 0xf1d4e242, 0x68ddb3f8, 0x1fda836e,
+ 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
+ 0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c,
+ 0x8f659eff, 0xf862ae69, 0x616bffd3, 0x166ccf45,
+ 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
+ 0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db,
+ 0xaed16a4a, 0xd9d65adc, 0x40df0b66, 0x37d83bf0,
+ 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
+ 0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6,
+ 0xbad03605, 0xcdd70693, 0x54de5729, 0x23d967bf,
+ 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
+ 0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d,
+};
+
+#include <stdio.h>
+#include <sys/types.h>
+
+
+int r_crc32 (char *buf,int dlen,u_int32_t *cval);
+
+int
+r_crc32(buf, dlen, cval)
+ char *buf;
+ int dlen;
+ u_int32_t *cval;
+{
+ u_int32_t crc = ~0;
+ char *p ;
+ int i;
+ u_int32_t crc32_total = 0 ;
+
+ p=buf;
+
+ for(i=0;i<dlen;i++){
+ CRC(crc, *p) ;
+ CRC(crc32_total, *p) ;
+ p++;
+ }
+
+ *cval = ~crc ;
+
+ return 0 ;
+}
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_crc32.h b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_crc32.h
new file mode 100644
index 0000000000..c11be4e04d
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_crc32.h
@@ -0,0 +1,14 @@
+/**
+ r_crc32.h
+
+ Copyright (C) 2002-2003, Network Resonance, Inc.
+ All Rights Reserved.
+
+ */
+
+#ifndef _r_crc32_
+#define _r_crc32_
+
+int r_crc32 (char *buf,int dlen, UINT4 *cval);
+
+#endif
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_data.c b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_data.c
new file mode 100644
index 0000000000..dfb7af2d5c
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_data.c
@@ -0,0 +1,248 @@
+/**
+ r_data.c
+
+
+ Copyright (C) 2002-2003, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ */
+
+/**
+ r_data.c
+
+
+ Copyright (C) 1999-2000 RTFM, Inc.
+ All Rights Reserved
+
+ This package is a SSLv3/TLS protocol analyzer written by Eric Rescorla
+ <ekr@rtfm.com> and licensed by RTFM, Inc.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. All advertising materials mentioning features or use of this software
+ must display the following acknowledgement:
+
+ This product includes software developed by Eric Rescorla for
+ RTFM, Inc.
+
+ 4. Neither the name of RTFM, Inc. nor the name of Eric Rescorla may be
+ used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY ERIC RESCORLA AND RTFM, INC. ``AS IS'' AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY SUCH DAMAGE.
+
+ $Id: r_data.c,v 1.2 2006/08/16 19:39:17 adamcain Exp $
+
+ ekr@rtfm.com Tue Aug 17 15:39:50 1999
+ */
+
+#include <string.h>
+#include <r_common.h>
+#include <r_data.h>
+#include <string.h>
+
+int r_data_create(dp,d,l)
+ Data **dp;
+ const UCHAR *d;
+ size_t l;
+ {
+ Data *d_=0;
+ int _status;
+
+ if(!(d_=(Data *)RCALLOC(sizeof(Data))))
+ ABORT(R_NO_MEMORY);
+ if(!(d_->data=(UCHAR *)RMALLOC(l)))
+ ABORT(R_NO_MEMORY);
+
+ if (d) {
+ memcpy(d_->data,d,l);
+ }
+ d_->len=l;
+
+ *dp=d_;
+
+ _status=0;
+ abort:
+ if(_status)
+ r_data_destroy(&d_);
+
+ return(_status);
+ }
+
+
+int r_data_alloc_mem(d,l)
+ Data *d;
+ size_t l;
+ {
+ int _status;
+
+ if(!(d->data=(UCHAR *)RMALLOC(l)))
+ ABORT(R_NO_MEMORY);
+ d->len=l;
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int r_data_alloc(dp,l)
+ Data **dp;
+ size_t l;
+ {
+ Data *d_=0;
+ int _status;
+
+ if(!(d_=(Data *)RCALLOC(sizeof(Data))))
+ ABORT(R_NO_MEMORY);
+ if(!(d_->data=(UCHAR *)RCALLOC(l)))
+ ABORT(R_NO_MEMORY);
+
+ d_->len=l;
+
+ *dp=d_;
+ _status=0;
+ abort:
+ if(_status)
+ r_data_destroy(&d_);
+
+ return(_status);
+ }
+
+int r_data_make(dp,d,l)
+ Data *dp;
+ const UCHAR *d;
+ size_t l;
+ {
+ if(!(dp->data=(UCHAR *)RMALLOC(l)))
+ ERETURN(R_NO_MEMORY);
+
+ memcpy(dp->data,d,l);
+ dp->len=l;
+
+ return(0);
+ }
+
+int r_data_destroy(dp)
+ Data **dp;
+ {
+ if(!dp || !*dp)
+ return(0);
+
+ if((*dp)->data)
+ RFREE((*dp)->data);
+
+ RFREE(*dp);
+ *dp=0;
+
+ return(0);
+ }
+
+int r_data_destroy_v(v)
+ void *v;
+ {
+ Data *d;
+
+ if(!v)
+ return(0);
+
+ d=(Data *)v;
+ r_data_zfree(d);
+
+ RFREE(d);
+
+ return(0);
+ }
+
+int r_data_destroy_vp(v)
+ void **v;
+ {
+ Data *d;
+
+ if(!v || !*v)
+ return(0);
+
+ d=(Data *)*v;
+ r_data_zfree(d);
+
+ *v=0;
+ RFREE(d);
+
+ return(0);
+ }
+
+int r_data_copy(dst,src)
+ Data *dst;
+ Data *src;
+ {
+ if(!(dst->data=(UCHAR *)RMALLOC(src->len)))
+ ERETURN(R_NO_MEMORY);
+ memcpy(dst->data,src->data,dst->len=src->len);
+ return(0);
+ }
+
+int r_data_zfree(d)
+ Data *d;
+ {
+ if(!d)
+ return(0);
+ if(!d->data)
+ return(0);
+ memset(d->data,0,d->len);
+ RFREE(d->data);
+ return(0);
+ }
+
+int r_data_compare(d1,d2)
+ Data *d1;
+ Data *d2;
+ {
+ if(d1->len<d2->len)
+ return(-1);
+ if(d2->len<d1->len)
+ return(-1);
+ return(memcmp(d1->data,d2->data,d1->len));
+ }
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_data.h b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_data.h
new file mode 100644
index 0000000000..3edf7b287a
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_data.h
@@ -0,0 +1,108 @@
+/**
+ r_data.h
+
+
+ Copyright (C) 2002-2003, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ */
+
+/**
+ r_data.h
+
+
+ Copyright (C) 1999-2000 RTFM, Inc.
+ All Rights Reserved
+
+ This package is a SSLv3/TLS protocol analyzer written by Eric Rescorla
+ <ekr@rtfm.com> and licensed by RTFM, Inc.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. All advertising materials mentioning features or use of this software
+ must display the following acknowledgement:
+
+ This product includes software developed by Eric Rescorla for
+ RTFM, Inc.
+
+ 4. Neither the name of RTFM, Inc. nor the name of Eric Rescorla may be
+ used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY ERIC RESCORLA AND RTFM, INC. ``AS IS'' AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY SUCH DAMAGE.
+
+ $Id: r_data.h,v 1.2 2006/08/16 19:39:17 adamcain Exp $
+
+
+ ekr@rtfm.com Wed Feb 10 14:18:19 1999
+ */
+
+
+#ifndef _r_data_h
+#define _r_data_h
+
+typedef struct Data_ {
+ UCHAR *data;
+ size_t len;
+} Data;
+
+int r_data_create(Data **dp,const UCHAR *d,size_t l);
+int r_data_alloc(Data **dp, size_t l);
+int r_data_make(Data *dp, const UCHAR *d,size_t l);
+int r_data_alloc_mem(Data *d,size_t l);
+int r_data_destroy(Data **dp);
+int r_data_destroy_v(void *v);
+int r_data_destroy_vp(void **vp);
+int r_data_copy(Data *dst,Data *src);
+int r_data_zfree(Data *d);
+int r_data_compare(Data *d1,Data *d2);
+
+#define INIT_DATA(a,b,c) (a).data=b; (a).len=c
+#define ATTACH_DATA(a,b) (a).data=b; (a).len=sizeof(b)
+#define ZERO_DATA(a) (a).data=0; (a).len=0
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_defaults.h b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_defaults.h
new file mode 100644
index 0000000000..0ec91a0331
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_defaults.h
@@ -0,0 +1,91 @@
+/**
+ r_defaults.h
+
+
+ Copyright (C) 2002-2003, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ */
+
+/**
+ r_defaults.h
+
+
+ Copyright (C) 1999-2000 RTFM, Inc.
+ All Rights Reserved
+
+ This package is a SSLv3/TLS protocol analyzer written by Eric Rescorla
+ <ekr@rtfm.com> and licensed by RTFM, Inc.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. All advertising materials mentioning features or use of this software
+ must display the following acknowledgement:
+
+ This product includes software developed by Eric Rescorla for
+ RTFM, Inc.
+
+ 4. Neither the name of RTFM, Inc. nor the name of Eric Rescorla may be
+ used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY ERIC RESCORLA AND RTFM, INC. ``AS IS'' AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY SUCH DAMAGE.
+
+ $Id: r_defaults.h,v 1.2 2006/08/16 19:39:17 adamcain Exp $
+
+
+ ekr@rtfm.com Tue Dec 22 10:39:14 1998
+ */
+
+
+#ifndef _r_defaults_h
+#define _r_defaults_h
+
+/*The needs defines don't belong here*/
+#define R_NEEDS_STDLIB_H
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_errors.c b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_errors.c
new file mode 100644
index 0000000000..e770e02438
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_errors.c
@@ -0,0 +1,136 @@
+/**
+ r_errors.c
+
+
+ Copyright (C) 2002-2003, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ */
+
+/**
+ r_errors.c
+
+
+ Copyright (C) 1999-2000 RTFM, Inc.
+ All Rights Reserved
+
+ This package is a SSLv3/TLS protocol analyzer written by Eric Rescorla
+ <ekr@rtfm.com> and licensed by RTFM, Inc.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. All advertising materials mentioning features or use of this software
+ must display the following acknowledgement:
+
+ This product includes software developed by Eric Rescorla for
+ RTFM, Inc.
+
+ 4. Neither the name of RTFM, Inc. nor the name of Eric Rescorla may be
+ used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY ERIC RESCORLA AND RTFM, INC. ``AS IS'' AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY SUCH DAMAGE.
+
+ $Id: r_errors.c,v 1.5 2008/11/26 03:22:02 adamcain Exp $
+
+
+ ekr@rtfm.com Tue Feb 16 16:37:05 1999
+ */
+
+#include <stdio.h>
+#include <stdarg.h>
+#include <string.h>
+#include "r_common.h"
+#include "r_errors.h"
+
+static struct {
+ int errnum;
+ char *str;
+} errors[] = NR_ERROR_MAPPING;
+
+int nr_verr_exit(char *fmt,...)
+ {
+ va_list ap;
+
+ va_start(ap,fmt);
+ vfprintf(stderr,fmt,ap);
+
+ if (fmt[0] != '\0' && fmt[strlen(fmt)-1] != '\n')
+ fprintf(stderr,"\n");
+
+ exit(1);
+ }
+
+char *
+nr_strerror(int errnum)
+{
+ static char unknown_error[256];
+ size_t i;
+ char *error = 0;
+
+ for (i = 0; i < sizeof(errors)/sizeof(*errors); ++i) {
+ if (errnum == errors[i].errnum) {
+ error = errors[i].str;
+ break;
+ }
+ }
+
+ if (! error) {
+ snprintf(unknown_error, sizeof(unknown_error), "Unknown error: %d", errnum);
+ error = unknown_error;
+ }
+
+ return error;
+}
+
+int
+nr_strerror_r(int errnum, char *strerrbuf, size_t buflen)
+{
+ char *error = nr_strerror(errnum);
+ snprintf(strerrbuf, buflen, "%s", error);
+ return 0;
+}
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_errors.h b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_errors.h
new file mode 100644
index 0000000000..f52375b98d
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_errors.h
@@ -0,0 +1,127 @@
+/**
+ r_errors.h
+
+
+ Copyright (C) 2002-2003, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ */
+
+/**
+ r_errors.h
+
+
+ Copyright (C) 1999-2000 RTFM, Inc.
+ All Rights Reserved
+
+ This package is a SSLv3/TLS protocol analyzer written by Eric Rescorla
+ <ekr@rtfm.com> and licensed by RTFM, Inc.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. All advertising materials mentioning features or use of this software
+ must display the following acknowledgement:
+
+ This product includes software developed by Eric Rescorla for
+ RTFM, Inc.
+
+ 4. Neither the name of RTFM, Inc. nor the name of Eric Rescorla may be
+ used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY ERIC RESCORLA AND RTFM, INC. ``AS IS'' AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY SUCH DAMAGE.
+
+ $Id: r_errors.h,v 1.4 2007/10/12 20:53:24 adamcain Exp $
+
+
+ ekr@rtfm.com Tue Dec 22 10:59:49 1998
+ */
+
+
+#ifndef _r_errors_h
+#define _r_errors_h
+
+#define R_NO_MEMORY 1 /*out of memory*/
+#define R_NOT_FOUND 2 /*Item not found*/
+#define R_INTERNAL 3 /*Unspecified internal error*/
+#define R_ALREADY 4 /*Action already done*/
+#define R_EOD 5 /*end of data*/
+#define R_BAD_ARGS 6 /*Bad arguments*/
+#define R_BAD_DATA 7 /*Bad data*/
+#define R_WOULDBLOCK 8 /*Operation would block */
+#define R_QUEUED 9 /*Operation was queued */
+#define R_FAILED 10 /*Operation failed */
+#define R_REJECTED 11 /* We don't care about this */
+#define R_INTERRUPTED 12 /* Operation interrupted */
+#define R_IO_ERROR 13 /* I/O Error */
+#define R_NOT_PERMITTED 14 /* Permission denied */
+#define R_RETRY 15 /* Retry possible */
+
+#define NR_ERROR_MAPPING {\
+ { R_NO_MEMORY, "Cannot allocate memory" },\
+ { R_NOT_FOUND, "Item not found" },\
+ { R_INTERNAL, "Internal failure" },\
+ { R_ALREADY, "Action already performed" },\
+ { R_EOD, "End of data" },\
+ { R_BAD_ARGS, "Invalid argument" },\
+ { R_BAD_DATA, "Invalid data" },\
+ { R_WOULDBLOCK, "Operation would block" },\
+ { R_QUEUED, "Operation queued" },\
+ { R_FAILED, "Operation failed" },\
+ { R_REJECTED, "Operation rejected" },\
+ { R_INTERRUPTED, "Operation interrupted" },\
+ { R_IO_ERROR, "I/O error" },\
+ { R_NOT_PERMITTED, "Permission Denied" },\
+ { R_RETRY, "Retry may be possible" },\
+ }
+
+int nr_verr_exit(char *fmt,...);
+
+char *nr_strerror(int errnum);
+int nr_strerror_r(int errnum, char *strerrbuf, size_t buflen);
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_includes.h b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_includes.h
new file mode 100644
index 0000000000..4fb7af5643
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_includes.h
@@ -0,0 +1,98 @@
+/**
+ r_includes.h
+
+
+ Copyright (C) 2002-2003, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ */
+
+/**
+ r_includes.h
+
+
+ Copyright (C) 1999-2000 RTFM, Inc.
+ All Rights Reserved
+
+ This package is a SSLv3/TLS protocol analyzer written by Eric Rescorla
+ <ekr@rtfm.com> and licensed by RTFM, Inc.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. All advertising materials mentioning features or use of this software
+ must display the following acknowledgement:
+
+ This product includes software developed by Eric Rescorla for
+ RTFM, Inc.
+
+ 4. Neither the name of RTFM, Inc. nor the name of Eric Rescorla may be
+ used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY ERIC RESCORLA AND RTFM, INC. ``AS IS'' AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY SUCH DAMAGE.
+
+ $Id: r_includes.h,v 1.2 2006/08/16 19:39:17 adamcain Exp $
+
+
+ ekr@rtfm.com Tue Dec 22 11:38:50 1998
+ */
+
+
+#ifndef _r_includes_h
+#define _r_includes_h
+
+#ifdef R_NEEDS_STDLIB_H
+#include <stdlib.h>
+#endif
+
+#ifdef R_NEEDS_MEMORY_H
+#include <memory.h>
+#endif
+
+#include <stdio.h>
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_list.c b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_list.c
new file mode 100644
index 0000000000..4e71d67030
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_list.c
@@ -0,0 +1,273 @@
+/**
+ r_list.c
+
+
+ Copyright (C) 2002-2003, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ */
+
+/**
+ r_list.c
+
+
+ Copyright (C) 1999-2000 RTFM, Inc.
+ All Rights Reserved
+
+ This package is a SSLv3/TLS protocol analyzer written by Eric Rescorla
+ <ekr@rtfm.com> and licensed by RTFM, Inc.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. All advertising materials mentioning features or use of this software
+ must display the following acknowledgement:
+
+ This product includes software developed by Eric Rescorla for
+ RTFM, Inc.
+
+ 4. Neither the name of RTFM, Inc. nor the name of Eric Rescorla may be
+ used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY ERIC RESCORLA AND RTFM, INC. ``AS IS'' AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY SUCH DAMAGE.
+
+ $Id: r_list.c,v 1.2 2006/08/16 19:39:17 adamcain Exp $
+
+
+ ekr@rtfm.com Tue Jan 19 08:36:39 1999
+ */
+
+#include <r_common.h>
+#include "r_list.h"
+
+typedef struct r_list_el_ {
+ void *data;
+ struct r_list_el_ *next;
+ struct r_list_el_ *prev;
+ int (*copy)(void **new,void *old);
+ int (*destroy)(void **ptr);
+} r_list_el;
+
+struct r_list_ {
+ struct r_list_el_ *first;
+ struct r_list_el_ *last;
+};
+
+int r_list_create(listp)
+ r_list **listp;
+ {
+ r_list *list=0;
+ int _status;
+
+ if(!(list=(r_list *)RCALLOC(sizeof(r_list))))
+ ABORT(R_NO_MEMORY);
+
+ list->first=0;
+ list->last=0;
+ *listp=list;
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int r_list_destroy(listp)
+ r_list **listp;
+ {
+ r_list *list;
+ r_list_el *el;
+
+ if(!listp || !*listp)
+ return(0);
+ list=*listp;
+
+ el=list->first;
+
+ while(el){
+ r_list_el *el_t;
+
+ if(el->destroy && el->data)
+ el->destroy(&el->data);
+ el_t=el;
+ el=el->next;
+ RFREE(el_t);
+ }
+
+ RFREE(list);
+ *listp=0;
+
+ return(0);
+ }
+
+int r_list_copy(outp,in)
+ r_list**outp;
+ r_list *in;
+ {
+ r_list *out=0;
+ r_list_el *el,*el2,*last=0;
+ int r, _status;
+
+ if(!in){
+ *outp=0;
+ return(0);
+ }
+
+ if(r=r_list_create(&out))
+ ABORT(r);
+
+ for(el=in->first;el;el=el->next){
+ if(!(el2=(r_list_el *)RCALLOC(sizeof(r_list_el))))
+ ABORT(R_NO_MEMORY);
+
+ if(el->copy && el->data){
+ if(r=el->copy(&el2->data,el->data))
+ ABORT(r);
+ }
+
+ el2->copy=el->copy;
+ el2->destroy=el->destroy;
+
+ if(!(out->first))
+ out->first=el2;
+
+ el2->prev=last;
+ if(last) last->next=el2;
+ last=el2;
+ }
+
+ out->last=last;
+
+ *outp=out;
+
+ _status=0;
+ abort:
+ if(_status)
+ r_list_destroy(&out);
+ return(_status);
+ }
+
+int r_list_insert(list,value,copy,destroy)
+ r_list *list;
+ void *value;
+ int (*copy)(void **out, void *in);
+ int (*destroy)(void **val);
+ {
+ r_list_el *el=0;
+ int _status;
+
+ if(!(el=(r_list_el *)RCALLOC(sizeof(r_list_el))))
+ ABORT(R_NO_MEMORY);
+ el->data=value;
+ el->copy=copy;
+ el->destroy=destroy;
+
+ el->prev=0;
+ el->next=list->first;
+ if(list->first){
+ list->first->prev=el;
+ }
+ list->first=el;
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int r_list_append(list,value,copy,destroy)
+ r_list *list;
+ void *value;
+ int (*copy)(void **out, void *in);
+ int (*destroy)(void **val);
+ {
+ r_list_el *el=0;
+ int _status;
+
+ if(!(el=(r_list_el *)RCALLOC(sizeof(r_list_el))))
+ ABORT(R_NO_MEMORY);
+ el->data=value;
+ el->copy=copy;
+ el->destroy=destroy;
+
+ el->prev=list->last;
+ el->next=0;
+
+ if(list->last) list->last->next=el;
+ else list->first=el;
+
+ list->last=el;
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int r_list_init_iter(list,iter)
+ r_list *list;
+ r_list_iterator *iter;
+ {
+ iter->list=list;
+ iter->ptr=list->first;
+
+ return(0);
+ }
+
+int r_list_iter(iter,val)
+ r_list_iterator *iter;
+ void **val;
+ {
+ if(!iter->ptr)
+ return(R_EOD);
+
+ *val=iter->ptr->data;
+ iter->ptr=iter->ptr->next;
+
+ return(0);
+ }
+
+
+
+
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_list.h b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_list.h
new file mode 100644
index 0000000000..bbefc6c39f
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_list.h
@@ -0,0 +1,106 @@
+/**
+ r_list.h
+
+
+ Copyright (C) 2002-2003, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ */
+
+/**
+ r_list.h
+
+
+ Copyright (C) 1999-2000 RTFM, Inc.
+ All Rights Reserved
+
+ This package is a SSLv3/TLS protocol analyzer written by Eric Rescorla
+ <ekr@rtfm.com> and licensed by RTFM, Inc.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. All advertising materials mentioning features or use of this software
+ must display the following acknowledgement:
+
+ This product includes software developed by Eric Rescorla for
+ RTFM, Inc.
+
+ 4. Neither the name of RTFM, Inc. nor the name of Eric Rescorla may be
+ used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY ERIC RESCORLA AND RTFM, INC. ``AS IS'' AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY SUCH DAMAGE.
+
+ $Id: r_list.h,v 1.3 2007/06/08 17:41:49 adamcain Exp $
+
+
+ ekr@rtfm.com Tue Jan 19 08:36:48 1999
+ */
+
+
+#ifndef _r_list_h
+#define _r_list_h
+
+typedef struct r_list_ r_list;
+
+typedef struct r_list_iterator_ {
+ r_list *list;
+ struct r_list_el_ *ptr;
+} r_list_iterator;
+
+int r_list_create(r_list **listp);
+int r_list_destroy(r_list **listp);
+int r_list_copy(r_list **out,r_list *in);
+int r_list_insert(r_list *list,void *value,
+ int (*copy)(void **knew,void *old),
+ int (*destroy)(void **ptr));
+int r_list_append(r_list *list,void *value,
+ int (*copy)(void **knew,void *old),
+ int (*destroy)(void **ptr));
+int r_list_init_iter(r_list *list,r_list_iterator *iter);
+int r_list_iter(r_list_iterator *iter,void **val);
+
+#endif
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_macros.h b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_macros.h
new file mode 100644
index 0000000000..ddfedd3c36
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_macros.h
@@ -0,0 +1,137 @@
+/**
+ r_macros.h
+
+
+ Copyright (C) 2002-2003, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ */
+
+/**
+ r_macros.h
+
+
+ Copyright (C) 1999-2000 RTFM, Inc.
+ All Rights Reserved
+
+ This package is a SSLv3/TLS protocol analyzer written by Eric Rescorla
+ <ekr@rtfm.com> and licensed by RTFM, Inc.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. All advertising materials mentioning features or use of this software
+ must display the following acknowledgement:
+
+ This product includes software developed by Eric Rescorla for
+ RTFM, Inc.
+
+ 4. Neither the name of RTFM, Inc. nor the name of Eric Rescorla may be
+ used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY ERIC RESCORLA AND RTFM, INC. ``AS IS'' AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY SUCH DAMAGE.
+
+ $Id: r_macros.h,v 1.3 2007/06/26 22:37:57 adamcain Exp $
+
+ ekr@rtfm.com Tue Dec 22 10:37:32 1998
+ */
+
+
+#ifndef _r_macros_h
+#define _r_macros_h
+
+/* Haven't removed all PROTO_LIST defs yet */
+#define PROTO_LIST(a) a
+
+#ifndef WIN32
+#ifndef __GNUC__
+#define __FUNCTION__ "unknown"
+#endif
+#endif
+
+#ifdef R_TRACE_ERRORS
+#ifdef WIN32
+#define REPORT_ERROR_(caller,a) printf("%s: error %d at %s:%d (function %s)\n", \
+ caller,a,__FILE__,__LINE__,__FUNCTION__)
+#else
+#define REPORT_ERROR_(caller,a) fprintf(stderr,"%s: error %d at %s:%d (function %s)\n", \
+ caller,a,__FILE__,__LINE__,__FUNCTION__)
+#endif
+#else
+#define REPORT_ERROR_(caller,a)
+#endif
+
+#ifndef ERETURN
+#define ERETURN(a) do {int _r=a; if(!_r) _r=-1; REPORT_ERROR_("ERETURN",_r); return(_r);} while(0)
+#endif
+
+#ifndef ABORT
+#define ABORT(a) do { int _r=a; if(!_r) _r=-1; REPORT_ERROR_("ABORT",_r); _status=_r; goto abort;} while(0)
+#endif
+
+#ifndef FREE
+#define FREE(a) if(a) free(a)
+#endif
+#ifndef MIN
+#define MIN(a,b) ((a)>(b))?(b):(a)
+#endif
+
+#ifndef MAX
+#define MAX(a,b) ((b)>(a))?(b):(a)
+#endif
+
+#ifdef DEBUG
+#define DBG(a) debug a
+int debug(int cls, char *format,...);
+#else
+#define DBG(a)
+#endif
+
+#define NR_UNIMPLEMENTED do { fprintf(stderr,"%s:%d Function %s unimplemented\n",__FILE__,__LINE__,__FUNCTION__); abort(); } while(0)
+
+#include "r_memory.h"
+
+#endif
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_memory.c b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_memory.c
new file mode 100644
index 0000000000..53846fc019
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_memory.c
@@ -0,0 +1,198 @@
+/**
+ r_memory.c
+
+
+ Copyright (C) 2004, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ ekr@rtfm.com Thu Apr 22 20:40:45 2004
+ */
+
+#include <string.h>
+#include <stddef.h>
+#include <assert.h>
+#include "r_common.h"
+#include "r_memory.h"
+
+typedef struct r_malloc_chunk_ {
+#ifdef SANITY_CHECKS
+ UINT4 hdr;
+#endif
+ UCHAR type;
+ UINT4 size;
+ UCHAR memory[1];
+} r_malloc_chunk;
+
+#define CHUNK_MEMORY_OFFSET offsetof(struct r_malloc_chunk_, memory)
+#define GET_CHUNK_ADDR_FROM_MEM_ADDR(memp) \
+ ((struct r_malloc_chunk *)(((unsigned char*)(memp))-CHUNK_MEMORY_OFFSET))
+#define CHUNK_SIZE(size) (size+sizeof(r_malloc_chunk))
+
+#define HDR_FLAG 0x464c4147
+
+static UINT4 mem_usage; /* Includes our header */
+static UINT4 mem_stats[256]; /* Does not include our header */
+
+void *r_malloc(type,size)
+ int type;
+ size_t size;
+ {
+ size_t total;
+ r_malloc_chunk *chunk;
+
+ total=size+sizeof(r_malloc_chunk);
+
+ if(!(chunk=malloc(total)))
+ return(0);
+
+#ifdef SANITY_CHECKS
+ chunk->hdr=HDR_FLAG;
+#endif
+ chunk->type=type;
+ chunk->size=size;
+
+ mem_usage+=CHUNK_SIZE(size);
+ mem_stats[type]+=size;
+
+ return(chunk->memory);
+ }
+
+void *r_calloc(type,number,size)
+ int type;
+ size_t number;
+ size_t size;
+ {
+ void *ret;
+ size_t total;
+
+ total=number*size;
+
+ if(!(ret=r_malloc(type,total)))
+ return(0);
+
+ memset(ret,0,size);
+
+ return(ret);
+ }
+
+void r_free(ptr)
+ void *ptr;
+ {
+ r_malloc_chunk *chunk;
+
+ if(!ptr) return;
+
+ chunk=(r_malloc_chunk *)GET_CHUNK_ADDR_FROM_MEM_ADDR(ptr);
+#ifdef SANITY_CHECKS
+ assert(chunk->hdr==HDR_FLAG);
+#endif
+
+ mem_usage-=CHUNK_SIZE(chunk->size);
+ mem_stats[chunk->type]-=chunk->size;
+
+ free(chunk);
+ }
+
+void *r_realloc(ptr,size)
+ void *ptr;
+ size_t size;
+ {
+ r_malloc_chunk *chunk,*nchunk;
+ size_t total;
+
+ if(!ptr) return(r_malloc(255,size));
+
+ chunk=(r_malloc_chunk *)GET_CHUNK_ADDR_FROM_MEM_ADDR(ptr);
+#ifdef SANITY_CHECKS
+ assert(chunk->hdr==HDR_FLAG);
+#endif
+
+ total=size + sizeof(r_malloc_chunk);
+
+ if(!(nchunk=realloc(chunk,total)))
+ return(0);
+
+ mem_usage-=CHUNK_SIZE(nchunk->size);
+ mem_stats[nchunk->type]-=nchunk->size;
+
+ nchunk->size=size;
+ mem_usage+=CHUNK_SIZE(nchunk->size);
+ mem_stats[nchunk->type]+=nchunk->size;
+
+ return(nchunk->memory);
+ }
+
+char *r_strdup(str)
+ const char *str;
+ {
+ int len;
+ char *nstr;
+
+ if(!str)
+ return(0);
+
+ len=strlen(str)+1;
+
+ if(!(nstr=r_malloc(0,len)))
+ return(0);
+
+ memcpy(nstr,str,len);
+
+ return(nstr);
+ }
+
+int r_mem_get_usage(usagep)
+ UINT4 *usagep;
+ {
+ *usagep=mem_usage;
+
+ return(0);
+ }
+
+int r_memory_dump_stats()
+ {
+ int i;
+
+ printf("Total memory usage: %d\n",mem_usage);
+ printf("Memory usage by bucket\n");
+ for(i=0;i<256;i++){
+ if(mem_stats[i]){
+ printf("%d\t%d\n",i,mem_stats[i]);
+ }
+ }
+ return(0);
+ }
+
+void *r_malloc_compat(size)
+ size_t size;
+ {
+ return(r_malloc(255,size));
+ }
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_memory.h b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_memory.h
new file mode 100644
index 0000000000..4357070767
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_memory.h
@@ -0,0 +1,101 @@
+/**
+ r_memory.h
+
+
+ Copyright (C) 2004, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ ekr@rtfm.com Sat Apr 24 08:30:00 2004
+ */
+
+
+#ifndef _r_memory_h
+#define _r_memory_h
+
+#define R_MALLOC_X 2
+
+#include "r_types.h"
+
+void *r_malloc(int type, size_t size);
+void *r_malloc_compat(size_t size);
+void *r_calloc(int type,size_t number,size_t size);
+void r_free (void *ptr);
+void *r_realloc(void *ptr,size_t size);
+char *r_strdup(const char *str);
+int r_mem_get_usage(UINT4 *usage);
+int r_memory_dump_stats(void);
+
+#ifdef NO_MALLOC_REPLACE
+
+#ifndef RMALLOC
+#define RMALLOC(a) malloc(a)
+#endif
+
+#ifndef RCALLOC
+#define RCALLOC(a) calloc(1,a)
+#endif
+
+#ifndef RFREE
+#define RFREE(a) if(a) free(a)
+#endif
+
+#ifndef RREALLOC
+#define RREALLOC(a,b) realloc(a,b)
+#endif
+
+#else
+
+
+#ifndef R_MALLOC_TYPE
+#define R_MALLOC_TYPE 0
+#endif
+
+#ifndef RMALLOC
+#define RMALLOC(a) r_malloc(R_MALLOC_TYPE,a)
+#endif
+
+#ifndef RCALLOC
+#define RCALLOC(a) r_calloc(R_MALLOC_TYPE,1,a)
+#endif
+
+#ifndef RFREE
+#define RFREE(a) if(a) r_free(a)
+#endif
+
+#ifndef RREALLOC
+#define RREALLOC(a,b) r_realloc(a,b)
+#endif
+
+#endif
+
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_replace.c b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_replace.c
new file mode 100644
index 0000000000..8916b884cc
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_replace.c
@@ -0,0 +1,107 @@
+/**
+ r_replace.c
+
+
+ Copyright (C) 2002-2003, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ */
+
+/**
+ r_replace.c
+
+
+ Copyright (C) 1999-2000 RTFM, Inc.
+ All Rights Reserved
+
+ This package is a SSLv3/TLS protocol analyzer written by Eric Rescorla
+ <ekr@rtfm.com> and licensed by RTFM, Inc.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. All advertising materials mentioning features or use of this software
+ must display the following acknowledgement:
+
+ This product includes software developed by Eric Rescorla for
+ RTFM, Inc.
+
+ 4. Neither the name of RTFM, Inc. nor the name of Eric Rescorla may be
+ used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY ERIC RESCORLA AND RTFM, INC. ``AS IS'' AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY SUCH DAMAGE.
+
+ $Id: r_replace.c,v 1.2 2006/08/16 19:39:17 adamcain Exp $
+
+
+ ekr@rtfm.com Sun Oct 1 11:18:49 2000
+ */
+
+#include "r_common.h"
+
+#ifndef HAVE_STRDUP
+
+char *strdup(str)
+ char *str;
+ {
+ int len=strlen(str);
+ char *n;
+
+ if(!(n=(char *)malloc(len+1)))
+ return(0);
+
+ memcpy(n,str,len+1);
+
+ return(n);
+ }
+#endif
+
+
+#ifdef SUPPLY_ATEXIT
+int atexit(void (*func)(void)){
+ ;
+}
+#endif
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_thread.h b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_thread.h
new file mode 100644
index 0000000000..212900bcc4
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_thread.h
@@ -0,0 +1,68 @@
+/**
+ r_thread.h
+
+
+ Copyright (C) 1999, RTFM, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ ekr@rtfm.com Tue Feb 23 14:58:36 1999
+ */
+
+
+#ifndef _r_thread_h
+#define _r_thread_h
+
+typedef void *r_thread;
+typedef void *r_rwlock;
+typedef void * r_cond;
+
+int r_thread_fork (void (*func)(void *),void *arg,
+ r_thread *tid);
+int r_thread_destroy (r_thread tid);
+int r_thread_yield (void);
+int r_thread_exit (void);
+int r_thread_wait_last (void);
+int r_thread_self (void);
+
+int r_rwlock_create (r_rwlock **lockp);
+int r_rwlock_destroy (r_rwlock **lock);
+int r_rwlock_lock (r_rwlock *lock,int action);
+
+int r_cond_init (r_cond *cond);
+int r_cond_wait (r_cond cond);
+int r_cond_signal (r_cond cond);
+
+#define R_RWLOCK_UNLOCK 0
+#define R_RWLOCK_RLOCK 1
+#define R_RWLOCK_WLOCK 2
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_time.c b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_time.c
new file mode 100644
index 0000000000..f873585c4b
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_time.c
@@ -0,0 +1,235 @@
+/**
+ r_time.c
+
+
+ Copyright (C) 2002-2003, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ */
+
+/**
+ r_time.c
+
+
+ Copyright (C) 1999-2000 RTFM, Inc.
+ All Rights Reserved
+
+ This package is a SSLv3/TLS protocol analyzer written by Eric Rescorla
+ <ekr@rtfm.com> and licensed by RTFM, Inc.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. All advertising materials mentioning features or use of this software
+ must display the following acknowledgement:
+
+ This product includes software developed by Eric Rescorla for
+ RTFM, Inc.
+
+ 4. Neither the name of RTFM, Inc. nor the name of Eric Rescorla may be
+ used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY ERIC RESCORLA AND RTFM, INC. ``AS IS'' AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY SUCH DAMAGE.
+
+ $Id: r_time.c,v 1.5 2008/11/26 03:22:02 adamcain Exp $
+
+ ekr@rtfm.com Thu Mar 4 08:43:46 1999
+ */
+
+#include <r_common.h>
+#include <r_time.h>
+
+/*Note that t1 must be > t0 */
+int r_timeval_diff(t1,t0,diff)
+ struct timeval *t1;
+ struct timeval *t0;
+ struct timeval *diff;
+ {
+ long d;
+
+ if(t0->tv_sec > t1->tv_sec)
+ ERETURN(R_BAD_ARGS);
+ if((t0->tv_sec == t1->tv_sec) && (t0->tv_usec > t1->tv_usec))
+ ERETURN(R_BAD_ARGS);
+
+ /*Easy case*/
+ if(t0->tv_usec <= t1->tv_usec){
+ diff->tv_sec=t1->tv_sec - t0->tv_sec;
+ diff->tv_usec=t1->tv_usec - t0->tv_usec;
+ return(0);
+ }
+
+ /*Hard case*/
+ d=t0->tv_usec - t1->tv_usec;
+ if(t1->tv_sec < (t0->tv_sec + 1))
+ ERETURN(R_BAD_ARGS);
+ diff->tv_sec=t1->tv_sec - (t0->tv_sec + 1);
+ diff->tv_usec=1000000 - d;
+
+ return(0);
+ }
+
+int r_timeval_add(t1,t2,sum)
+ struct timeval *t1;
+ struct timeval *t2;
+ struct timeval *sum;
+ {
+ long tv_sec,tv_usec,d;
+
+ tv_sec=t1->tv_sec + t2->tv_sec;
+
+ d=t1->tv_usec + t2->tv_usec;
+ if(d>1000000){
+ tv_sec++;
+ tv_usec=d-1000000;
+ }
+ else{
+ tv_usec=d;
+ }
+
+ sum->tv_sec=tv_sec;
+ sum->tv_usec=tv_usec;
+
+ return(0);
+ }
+
+int r_timeval_cmp(t1,t2)
+ struct timeval *t1;
+ struct timeval *t2;
+ {
+ if(t1->tv_sec>t2->tv_sec)
+ return(1);
+ if(t1->tv_sec<t2->tv_sec)
+ return(-1);
+ if(t1->tv_usec>t2->tv_usec)
+ return(1);
+ if(t1->tv_usec<t2->tv_usec)
+ return(-1);
+ return(0);
+ }
+
+
+UINT8 r_timeval2int(tv)
+ struct timeval *tv;
+ {
+ UINT8 r=0;
+
+ r=(tv->tv_sec);
+ r*=1000000;
+ r+=tv->tv_usec;
+
+ return r;
+ }
+
+int r_int2timeval(UINT8 t,struct timeval *tv)
+ {
+ tv->tv_sec=t/1000000;
+ tv->tv_usec=t%1000000;
+
+ return(0);
+ }
+
+UINT8 r_gettimeint()
+ {
+ struct timeval tv;
+
+ gettimeofday(&tv,0);
+
+ return r_timeval2int(&tv);
+ }
+
+/* t1-t0 in microseconds */
+int r_timeval_diff_usec(struct timeval *t1, struct timeval *t0, INT8 *diff)
+ {
+ int r,_status;
+ int sign;
+ struct timeval tmp;
+
+ sign = 1;
+ if (r=r_timeval_diff(t1, t0, &tmp)) {
+ if (r == R_BAD_ARGS) {
+ sign = -1;
+ if (r=r_timeval_diff(t0, t1, &tmp))
+ ABORT(r);
+ }
+ }
+
+ /* 1 second = 1000 milliseconds
+ * 1 milliseconds = 1000 microseconds */
+
+ *diff = ((tmp.tv_sec * (1000*1000)) + tmp.tv_usec) * sign;
+
+ _status = 0;
+ abort:
+ return(_status);
+ }
+
+/* t1-t0 in milliseconds */
+int r_timeval_diff_ms(struct timeval *t1, struct timeval *t0, INT8 *diff)
+ {
+ int r,_status;
+ int sign;
+ struct timeval tmp;
+
+ sign = 1;
+ if (r=r_timeval_diff(t1, t0, &tmp)) {
+ if (r == R_BAD_ARGS) {
+ sign = -1;
+ if (r=r_timeval_diff(t0, t1, &tmp))
+ ABORT(r);
+ }
+ }
+
+ /* 1 second = 1000 milliseconds
+ * 1 milliseconds = 1000 microseconds */
+
+ *diff = ((tmp.tv_sec * 1000) + (tmp.tv_usec / 1000)) * sign;
+
+ _status = 0;
+ abort:
+ return(_status);
+ }
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_time.h b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_time.h
new file mode 100644
index 0000000000..1ee1e778fe
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_time.h
@@ -0,0 +1,109 @@
+/**
+ r_time.h
+
+
+ Copyright (C) 2002-2003, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ */
+
+/**
+ r_time.h
+
+
+ Copyright (C) 1999-2000 RTFM, Inc.
+ All Rights Reserved
+
+ This package is a SSLv3/TLS protocol analyzer written by Eric Rescorla
+ <ekr@rtfm.com> and licensed by RTFM, Inc.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. All advertising materials mentioning features or use of this software
+ must display the following acknowledgement:
+
+ This product includes software developed by Eric Rescorla for
+ RTFM, Inc.
+
+ 4. Neither the name of RTFM, Inc. nor the name of Eric Rescorla may be
+ used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY ERIC RESCORLA AND RTFM, INC. ``AS IS'' AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY SUCH DAMAGE.
+
+ $Id: r_time.h,v 1.4 2007/06/26 22:37:57 adamcain Exp $
+
+
+ ekr@rtfm.com Thu Mar 4 08:45:41 1999
+ */
+
+
+#ifndef _r_time_h
+#define _r_time_h
+
+#include <csi_platform.h>
+
+#ifndef WIN32
+#include <sys/time.h>
+#include <time.h>
+#endif
+
+int r_timeval_diff(struct timeval *t1,struct timeval *t0, struct timeval *diff);
+int r_timeval_add(struct timeval *t1,struct timeval *t2, struct timeval *sum);
+int r_timeval_cmp(struct timeval *t1,struct timeval *t2);
+
+UINT8 r_timeval2int(struct timeval *tv);
+int r_int2timeval(UINT8 t,struct timeval *tv);
+UINT8 r_gettimeint(void);
+
+/* t1-t0 in microseconds */
+int r_timeval_diff_usec(struct timeval *t1, struct timeval *t0, INT8 *diff);
+
+/* t1-t0 in milliseconds */
+int r_timeval_diff_ms(struct timeval *t1, struct timeval *t0, INT8 *diff);
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_types.h b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_types.h
new file mode 100644
index 0000000000..d00810a7a2
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/util/libekr/r_types.h
@@ -0,0 +1,213 @@
+/**
+ r_types.h
+
+
+ Copyright (C) 2002-2003, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ */
+
+/**
+ r_types.h
+
+
+ Copyright (C) 1999-2000 RTFM, Inc.
+ All Rights Reserved
+
+ This package is a SSLv3/TLS protocol analyzer written by Eric Rescorla
+ <ekr@rtfm.com> and licensed by RTFM, Inc.
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. All advertising materials mentioning features or use of this software
+ must display the following acknowledgement:
+
+ This product includes software developed by Eric Rescorla for
+ RTFM, Inc.
+
+ 4. Neither the name of RTFM, Inc. nor the name of Eric Rescorla may be
+ used to endorse or promote products derived from this
+ software without specific prior written permission.
+
+ THIS SOFTWARE IS PROVIDED BY ERIC RESCORLA AND RTFM, INC. ``AS IS'' AND
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY SUCH DAMAGE.
+
+ $Id: r_types.h,v 1.2 2006/08/16 19:39:18 adamcain Exp $
+
+
+ ekr@rtfm.com Tue Dec 22 10:36:02 1998
+ */
+
+
+#ifndef _r_types_h
+#define _r_types_h
+
+/* Either define R_PLATFORM_INT_TYPES or be on a platform that
+ has stdint.h */
+#ifdef R_PLATFORM_INT_TYPES
+#include R_PLATFORM_INT_TYPES
+#else
+#include <stdint.h>
+#endif
+
+#ifndef R_DEFINED_INT2
+#ifndef SIZEOF_INT
+typedef short INT2;
+#else
+# if (SIZEOF_INT==2)
+typedef int INT2;
+# elif (SIZEOF_SHORT==2)
+typedef short INT2;
+# elif (SIZEOF_LONG==2)
+typedef long INT2;
+# else
+# error no type for INT2
+# endif
+#endif
+#else
+typedef R_DEFINED_INT2 INT2;
+#endif
+
+#ifndef R_DEFINED_UINT2
+#ifndef SIZEOF_UNSIGNED_INT
+typedef unsigned short UINT2;
+#else
+# if (SIZEOF_UNSIGNED_INT==2)
+typedef unsigned int UINT2;
+# elif (SIZEOF_UNSIGNED_SHORT==2)
+typedef unsigned short UINT2;
+# elif (SIZEOF_UNSIGNED_LONG==2)
+typedef unsigned long UINT2;
+# else
+# error no type for UINT2
+# endif
+#endif
+#else
+typedef R_DEFINED_UINT2 UINT2;
+#endif
+
+#ifndef R_DEFINED_INT4
+#ifndef SIZEOF_INT
+typedef int INT4;
+#else
+# if (SIZEOF_INT==4)
+typedef int INT4;
+# elif (SIZEOF_SHORT==4)
+typedef short INT4;
+# elif (SIZEOF_LONG==4)
+typedef long INT4;
+# else
+# error no type for INT4
+# endif
+#endif
+#else
+typedef R_DEFINED_INT4 INT4;
+#endif
+
+#ifndef R_DEFINED_UINT4
+#ifndef SIZEOF_UNSIGNED_INT
+typedef unsigned int UINT4;
+#else
+# if (SIZEOF_UNSIGNED_INT==4)
+typedef unsigned int UINT4;
+# elif (SIZEOF_UNSIGNED_SHORT==4)
+typedef unsigned short UINT4;
+# elif (SIZEOF_UNSIGNED_LONG==4)
+typedef unsigned long UINT4;
+# else
+# error no type for UINT4
+# endif
+#endif
+#else
+typedef R_DEFINED_UINT4 UINT4;
+#endif
+
+#ifndef R_DEFINED_INT8
+#ifndef SIZEOF_INT
+typedef long long INT8;
+#else
+# if (SIZEOF_INT==8)
+typedef int INT8;
+# elif (SIZEOF_SHORT==8)
+typedef short INT8;
+# elif (SIZEOF_LONG==8)
+typedef long INT8;
+# elif (SIZEOF_LONG_LONG==8)
+typedef long long INT8;
+# else
+# error no type for INT8
+# endif
+#endif
+#else
+typedef R_DEFINED_INT8 INT8;
+#endif
+
+#ifndef R_DEFINED_UINT8
+#ifndef SIZEOF_UNSIGNED_INT
+typedef unsigned long long UINT8;
+#else
+# if (SIZEOF_UNSIGNED_INT==8)
+typedef unsigned int UINT8;
+# elif (SIZEOF_UNSIGNED_SHORT==8)
+typedef unsigned short UINT8;
+# elif (SIZEOF_UNSIGNED_LONG==8)
+typedef unsigned long UINT8;
+# elif (SIZEOF_UNSIGNED_LONG_LONG==8)
+typedef unsigned long long UINT8;
+# else
+# error no type for UINT8
+# endif
+#endif
+#else
+typedef R_DEFINED_UINT8 UINT8;
+#endif
+
+#ifndef R_DEFINED_UCHAR
+typedef unsigned char UCHAR;
+#else
+typedef R_DEFINED_UCHAR UCHAR;
+#endif
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/util/p_buf.c b/dom/media/webrtc/transport/third_party/nrappkit/src/util/p_buf.c
new file mode 100644
index 0000000000..459baecdda
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/util/p_buf.c
@@ -0,0 +1,215 @@
+/**
+ p_buf.c
+
+
+ Copyright (C) 2003, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ All Rights Reserved.
+
+ ekr@rtfm.com Tue Nov 25 16:33:08 2003
+ */
+
+#include <string.h>
+#include <stddef.h>
+#include "nr_common.h"
+#include "p_buf.h"
+
+
+static int nr_p_buf_destroy_chain(nr_p_buf_head *head);
+static int nr_p_buf_destroy(nr_p_buf *buf);
+
+int nr_p_buf_ctx_create(size,ctxp)
+ int size;
+ nr_p_buf_ctx **ctxp;
+ {
+ int _status;
+ nr_p_buf_ctx *ctx=0;
+
+ if(!(ctx=(nr_p_buf_ctx *)RCALLOC(sizeof(nr_p_buf_ctx))))
+ ABORT(R_NO_MEMORY);
+
+ ctx->buf_size=size;
+ STAILQ_INIT(&ctx->free_list);
+
+ *ctxp=ctx;
+ _status=0;
+ abort:
+ if(_status){
+ nr_p_buf_ctx_destroy(&ctx);
+ }
+ return(_status);
+ }
+
+int nr_p_buf_ctx_destroy(ctxp)
+ nr_p_buf_ctx **ctxp;
+ {
+ nr_p_buf_ctx *ctx;
+
+ if(!ctxp || !*ctxp)
+ return(0);
+
+ ctx=*ctxp;
+
+ nr_p_buf_destroy_chain(&ctx->free_list);
+
+ RFREE(ctx);
+ *ctxp=0;
+
+ return(0);
+ }
+
+int nr_p_buf_alloc(ctx,bufp)
+ nr_p_buf_ctx *ctx;
+ nr_p_buf **bufp;
+ {
+ int _status;
+ nr_p_buf *buf=0;
+
+ if(!STAILQ_EMPTY(&ctx->free_list)){
+ buf=STAILQ_FIRST(&ctx->free_list);
+ STAILQ_REMOVE_HEAD(&ctx->free_list,entry);
+ goto ok;
+ }
+ else {
+ if(!(buf=(nr_p_buf *)RCALLOC(sizeof(nr_p_buf))))
+ ABORT(R_NO_MEMORY);
+ if(!(buf->data=(UCHAR *)RMALLOC(ctx->buf_size)))
+ ABORT(R_NO_MEMORY);
+ buf->size=ctx->buf_size;
+ }
+
+ ok:
+ buf->r_offset=0;
+ buf->length=0;
+
+ *bufp=buf;
+ _status=0;
+ abort:
+ if(_status){
+ nr_p_buf_destroy(buf);
+ }
+ return(_status);
+ }
+
+int nr_p_buf_free(ctx,buf)
+ nr_p_buf_ctx *ctx;
+ nr_p_buf *buf;
+ {
+ STAILQ_INSERT_TAIL(&ctx->free_list,buf,entry);
+
+ return(0);
+ }
+
+int nr_p_buf_free_chain(ctx,head)
+ nr_p_buf_ctx *ctx;
+ nr_p_buf_head *head;
+ {
+ nr_p_buf *n1,*n2;
+
+ n1=STAILQ_FIRST(head);
+ while(n1){
+ n2=STAILQ_NEXT(n1,entry);
+
+ nr_p_buf_free(ctx,n1);
+
+ n1=n2;
+ }
+
+ return(0);
+ }
+
+
+int nr_p_buf_write_to_chain(ctx,chain,data,len)
+ nr_p_buf_ctx *ctx;
+ nr_p_buf_head *chain;
+ UCHAR *data;
+ UINT4 len;
+ {
+ int r,_status;
+ nr_p_buf *buf;
+
+ buf=STAILQ_LAST(chain,nr_p_buf_,entry);
+ while(len){
+ int towrite;
+
+ if(!buf){
+ if(r=nr_p_buf_alloc(ctx,&buf))
+ ABORT(r);
+ STAILQ_INSERT_TAIL(chain,buf,entry);
+ }
+
+ towrite=MIN(len,(buf->size-(buf->length+buf->r_offset)));
+
+ memcpy(buf->data+buf->length+buf->r_offset,data,towrite);
+ len-=towrite;
+ data+=towrite;
+ buf->length+=towrite;
+
+ r_log(LOG_COMMON,LOG_DEBUG,"Wrote %d bytes to buffer %p",towrite,buf);
+ buf=0;
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+static int nr_p_buf_destroy_chain(head)
+ nr_p_buf_head *head;
+ {
+ nr_p_buf *n1,*n2;
+
+ n1=STAILQ_FIRST(head);
+ while(n1){
+ n2=STAILQ_NEXT(n1,entry);
+
+ nr_p_buf_destroy(n1);
+
+ n1=n2;
+ }
+
+ return(0);
+ }
+
+static int nr_p_buf_destroy(buf)
+ nr_p_buf *buf;
+ {
+ if(!buf)
+ return(0);
+
+ RFREE(buf->data);
+ RFREE(buf);
+
+ return(0);
+ }
+
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/util/p_buf.h b/dom/media/webrtc/transport/third_party/nrappkit/src/util/p_buf.h
new file mode 100644
index 0000000000..29881960c6
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/util/p_buf.h
@@ -0,0 +1,72 @@
+/**
+ p_buf.h
+
+
+ Copyright (C) 2003, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ ekr@rtfm.com Tue Nov 25 15:58:37 2003
+ */
+
+
+#ifndef _p_buf_h
+#define _p_buf_h
+
+typedef struct nr_p_buf_ {
+ UCHAR *data; /*The pointer to the buffer where the data lives */
+ UINT4 size; /*The size of the buffer */
+ UINT4 r_offset; /*The offset into the buffer where the data starts
+ when reading */
+ UINT4 length; /*The length of the data portion */
+
+ STAILQ_ENTRY(nr_p_buf_) entry;
+} nr_p_buf;
+
+typedef STAILQ_HEAD(nr_p_buf_head_,nr_p_buf_) nr_p_buf_head;
+
+
+typedef struct nr_p_buf_ctx_ {
+ int buf_size;
+
+ nr_p_buf_head free_list;
+} nr_p_buf_ctx;
+
+int nr_p_buf_ctx_create(int size,nr_p_buf_ctx **ctxp);
+int nr_p_buf_ctx_destroy(nr_p_buf_ctx **ctxp);
+int nr_p_buf_alloc(nr_p_buf_ctx *ctx,nr_p_buf **bufp);
+int nr_p_buf_free(nr_p_buf_ctx *ctx,nr_p_buf *buf);
+int nr_p_buf_free_chain(nr_p_buf_ctx *ctx,nr_p_buf_head *chain);
+int nr_p_buf_write_to_chain(nr_p_buf_ctx *ctx,
+ nr_p_buf_head *chain,
+ UCHAR *data,UINT4 len);
+
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/util/util.c b/dom/media/webrtc/transport/third_party/nrappkit/src/util/util.c
new file mode 100644
index 0000000000..17d49639fd
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/util/util.c
@@ -0,0 +1,775 @@
+/**
+ util.c
+
+
+ Copyright (C) 2001-2003, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ ekr@rtfm.com Wed Dec 26 17:19:36 2001
+ */
+
+#ifndef WIN32
+#include <sys/uio.h>
+#include <pwd.h>
+#include <dirent.h>
+#endif
+#include <string.h>
+#include <errno.h>
+#include <ctype.h>
+#include <sys/stat.h>
+#ifdef OPENSSL
+#include <openssl/evp.h>
+#endif
+#include "nr_common.h"
+#include "r_common.h"
+#include "registry.h"
+#include "util.h"
+#include "r_log.h"
+
+int nr_util_default_log_facility=LOG_COMMON;
+
+int nr_get_filename(base,name,namep)
+ char *base;
+ char *name;
+ char **namep;
+ {
+ int len=strlen(base)+strlen(name)+2;
+ char *ret=0;
+ int _status;
+
+ if(!(ret=(char *)RMALLOC(len)))
+ ABORT(R_NO_MEMORY);
+ if(base[strlen(base)-1]!='/'){
+ sprintf(ret,"%s/%s",base,name);
+ }
+ else{
+ sprintf(ret,"%s%s",base,name);
+ }
+ *namep=ret;
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+#if 0
+int read_RSA_private_key(base,name,keyp)
+ char *base;
+ char *name;
+ RSA **keyp;
+ {
+ char *keyfile=0;
+ BIO *bio=0;
+ FILE *fp=0;
+ RSA *rsa=0;
+ int r,_status;
+
+ /* Load the keyfile */
+ if(r=get_filename(base,name,&keyfile))
+ ABORT(r);
+ if(!(fp=fopen(keyfile,"r")))
+ ABORT(R_NOT_FOUND);
+ if(!(bio=BIO_new(BIO_s_file())))
+ ABORT(R_NO_MEMORY);
+ BIO_set_fp(bio,fp,BIO_NOCLOSE);
+
+ if(!(rsa=PEM_read_bio_RSAPrivateKey(bio,0,0,0)))
+ ABORT(R_NOT_FOUND);
+
+ *keyp=rsa;
+ _status=0;
+ abort:
+ return(_status);
+ }
+#endif
+
+
+void nr_errprintf_log(const char *format,...)
+ {
+ va_list ap;
+
+ va_start(ap,format);
+
+ r_vlog(nr_util_default_log_facility,LOG_ERR,format,ap);
+
+ va_end(ap);
+ }
+
+void nr_errprintf_log2(void *ignore, const char *format,...)
+ {
+ va_list ap;
+
+ va_start(ap,format);
+
+ r_vlog(nr_util_default_log_facility,LOG_ERR,format,ap);
+
+ va_end(ap);
+ }
+
+
+int nr_fwrite_all(FILE *fp,UCHAR *buf,int len)
+ {
+ int r,_status;
+
+ while(len){
+ r=fwrite(buf,1,len,fp);
+ if(r==0)
+ ABORT(R_IO_ERROR);
+
+ len-=r;
+ buf+=r;
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+int nr_read_data(fd,buf,len)
+ int fd;
+ char *buf;
+ int len;
+ {
+ int r,_status;
+
+ while(len){
+ r=NR_SOCKET_READ(fd,buf,len);
+ if(r<=0)
+ ABORT(R_EOD);
+
+ buf+=r;
+ len-=r;
+ }
+
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+
+#ifdef WIN32
+ // TODO
+#else
+int nr_drop_privileges(char *username)
+ {
+ int _status;
+
+ /* Drop privileges */
+ if ((getuid() == 0) || geteuid()==0) {
+ struct passwd *passwd;
+
+ if ((passwd = getpwnam(CAPTURE_USER)) == 0){
+ r_log(LOG_GENERIC,LOG_EMERG,"Couldn't get user %s",CAPTURE_USER);
+ ABORT(R_INTERNAL);
+ }
+
+ if(setuid(passwd->pw_uid)!=0){
+ r_log(LOG_GENERIC,LOG_EMERG,"Couldn't drop privileges");
+ ABORT(R_INTERNAL);
+ }
+ }
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+#endif
+
+int nr_bin2hex(UCHAR *in,int len,UCHAR *out)
+ {
+ while(len){
+ sprintf((char*)out,"%.2x",in[0] & 0xff);
+
+ in+=1;
+ out+=2;
+
+ len--;
+ }
+
+ return(0);
+ }
+
+int nr_hex_ascii_dump(Data *data)
+ {
+ UCHAR *ptr=data->data;
+ int len=data->len;
+
+ while(len){
+ int i;
+ int bytes=MIN(len,16);
+
+ for(i=0;i<bytes;i++)
+ printf("%.2x ",ptr[i]&255);
+ /* Fill */
+ for(i=0;i<(16-bytes);i++)
+ printf(" ");
+ printf(" ");
+
+ for(i=0;i<bytes;i++){
+ if(isprint(ptr[i]))
+ printf("%c",ptr[i]);
+ else
+ printf(".");
+ }
+ printf("\n");
+
+ len-=bytes;
+ ptr+=bytes;
+ }
+ return(0);
+ }
+
+#ifdef OPENSSL
+int nr_sha1_file(char *filename,UCHAR *out)
+ {
+ EVP_MD_CTX md_ctx;
+ FILE *fp=0;
+ int r,_status;
+ UCHAR buf[1024];
+ int out_len;
+
+ EVP_MD_CTX_init(&md_ctx);
+
+ if(!(fp=fopen(filename,"r"))){
+ r_log(LOG_COMMON,LOG_ERR,"Couldn't open file %s",filename);
+ ABORT(R_NOT_FOUND);
+ }
+
+ EVP_DigestInit_ex(&md_ctx,EVP_sha1(),0);
+
+ while(1){
+ r=fread(buf,1,sizeof(buf),fp);
+
+ if(r<0){
+ r_log(LOG_COMMON,LOG_ERR,"Error reading from %s",filename);
+ ABORT(R_INTERNAL);
+ }
+
+ if(!r)
+ break;
+
+ EVP_DigestUpdate(&md_ctx,buf,r);
+ }
+
+ EVP_DigestFinal(&md_ctx,out,(unsigned int*)&out_len);
+ if(out_len!=20)
+ ABORT(R_INTERNAL);
+
+ _status=0;
+ abort:
+ EVP_MD_CTX_cleanup(&md_ctx);
+ if(fp) fclose(fp);
+
+ return(_status);
+ }
+
+#endif
+
+#ifdef WIN32
+ // TODO
+#else
+
+#if 0
+
+#include <fts.h>
+
+int nr_rm_tree(char *path)
+ {
+ FTS *fts=0;
+ FTSENT *p;
+ int failed=0;
+ int _status;
+ char *argv[2];
+
+ argv[0]=path;
+ argv[1]=0;
+
+ if(!(fts=fts_open(argv,0,NULL))){
+ r_log_e(LOG_COMMON,LOG_ERR,"Couldn't open directory %s",path);
+ ABORT(R_FAILED);
+ }
+
+ while(p=fts_read(fts)){
+ switch(p->fts_info){
+ case FTS_D:
+ break;
+ case FTS_DOT:
+ break;
+ case FTS_ERR:
+ r_log_e(LOG_COMMON,LOG_ERR,"Problem reading %s",p->fts_path);
+ break;
+ default:
+ r_log(LOG_COMMON,LOG_DEBUG,"Removing %s",p->fts_path);
+ errno=0;
+ if(remove(p->fts_path)){
+ r_log_e(LOG_COMMON,LOG_ERR,"Problem removing %s",p->fts_path);
+ failed=1;
+ }
+ }
+ }
+
+ if(failed)
+ ABORT(R_FAILED);
+
+ _status=0;
+ abort:
+ if(fts) fts_close(fts);
+ return(_status);
+ }
+#endif
+
+int nr_write_pid_file(char *pid_filename)
+ {
+ FILE *fp;
+ int _status;
+
+ if(!pid_filename)
+ ABORT(R_BAD_ARGS);
+
+ unlink(pid_filename);
+
+ if(!(fp=fopen(pid_filename,"w"))){
+ r_log(LOG_GENERIC,LOG_CRIT,"Couldn't open PID file: %s",strerror(errno));
+ ABORT(R_NOT_FOUND);
+ }
+
+ fprintf(fp,"%d\n",getpid());
+
+ fclose(fp);
+
+ chmod(pid_filename,S_IRUSR | S_IRGRP | S_IROTH);
+
+ _status=0;
+ abort:
+ return(_status);
+ }
+#endif
+
+int nr_reg_uint4_fetch_and_check(NR_registry key, UINT4 min, UINT4 max, int log_fac, int die, UINT4 *val)
+ {
+ int r,_status;
+ UINT4 my_val;
+
+ if(r=NR_reg_get_uint4(key,&my_val)){
+ r_log(log_fac,LOG_ERR,"Couldn't get key '%s', error %d",key,r);
+ ABORT(r);
+ }
+
+ if((min>0) && (my_val<min)){
+ r_log(log_fac,LOG_ERR,"Invalid value for key '%s'=%lu, (min = %lu)",key,(unsigned long)my_val,(unsigned long)min);
+ ABORT(R_BAD_DATA);
+ }
+
+ if(my_val>max){
+ r_log(log_fac,LOG_ERR,"Invalid value for key '%s'=%lu, (max = %lu)",key,(unsigned long)my_val,(unsigned long)max);
+ ABORT(R_BAD_DATA);
+ }
+
+ *val=my_val;
+ _status=0;
+
+ abort:
+ if(die && _status){
+ r_log(log_fac,LOG_CRIT,"Exiting due to invalid configuration (key '%s')",key);
+ exit(1);
+ }
+ return(_status);
+ }
+
+int nr_reg_uint8_fetch_and_check(NR_registry key, UINT8 min, UINT8 max, int log_fac, int die, UINT8 *val)
+ {
+ int r,_status;
+ UINT8 my_val;
+
+ if(r=NR_reg_get_uint8(key,&my_val)){
+ r_log(log_fac,LOG_ERR,"Couldn't get key '%s', error %d",key,r);
+ ABORT(r);
+ }
+
+ if(my_val<min){
+ r_log(log_fac,LOG_ERR,"Invalid value for key '%s'=%llu, (min = %llu)",key,my_val,min);
+ ABORT(R_BAD_DATA);
+ }
+
+ if(my_val>max){
+ r_log(log_fac,LOG_ERR,"Invalid value for key '%s'=%llu, (max = %llu)",key,my_val,max);
+ ABORT(R_BAD_DATA);
+ }
+
+ *val=my_val;
+ _status=0;
+
+ abort:
+ if(die && _status){
+ r_log(log_fac,LOG_CRIT,"Exiting due to invalid configuration (key '%s')",key);
+ exit(1);
+ }
+ return(_status);
+ }
+
+#if defined(LINUX) || defined(WIN32)
+/*-
+ * Copyright (c) 1998 Todd C. Miller <Todd.Miller@courtesan.com>
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. The name of the author may not be used to endorse or promote products
+ * derived from this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES,
+ * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
+ * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
+ * THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+ * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+ * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+ * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+ * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+
+/*
+ * Appends src to string dst of size siz (unlike strncat, siz is the
+ * full size of dst, not space left). At most siz-1 characters
+ * will be copied. Always NUL terminates (unless siz <= strlen(dst)).
+ * Returns strlen(src) + MIN(siz, strlen(initial dst)).
+ * If retval >= siz, truncation occurred.
+ */
+size_t
+strlcat(dst, src, siz)
+ char *dst;
+ const char *src;
+ size_t siz;
+{
+ char *d = dst;
+ const char *s = src;
+ size_t n = siz;
+ size_t dlen;
+
+ /* Find the end of dst and adjust bytes left but don't go past end */
+ while (n-- != 0 && *d != '\0')
+ d++;
+ dlen = d - dst;
+ n = siz - dlen;
+
+ if (n == 0)
+ return(dlen + strlen(s));
+ while (*s != '\0') {
+ if (n != 1) {
+ *d++ = *s;
+ n--;
+ }
+ s++;
+ }
+ *d = '\0';
+
+ return(dlen + (s - src)); /* count does not include NUL */
+}
+
+#endif /* LINUX or WIN32 */
+
+#if defined(USE_OWN_INET_NTOP) || (defined(WIN32) && WINVER < 0x0600)
+#include <errno.h>
+#ifdef WIN32
+#include <Ws2ipdef.h>
+#ifndef EAFNOSUPPORT
+#define EAFNOSUPPORT WSAEAFNOSUPPORT
+#endif
+#else
+#include <sys/socket.h>
+#endif
+#define INET6
+
+/* inet_ntop implementation from NetBSD */
+
+/*
+ * Copyright (c) 2004 by Internet Systems Consortium, Inc. ("ISC")
+ * Copyright (c) 1996-1999 by Internet Software Consortium.
+ *
+ * Permission to use, copy, modify, and distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+ * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+ */
+
+#if !defined(NS_INADDRSZ)
+# define NS_INADDRSZ 4
+#endif
+#if !defined(NS_IN6ADDRSZ)
+# define NS_IN6ADDRSZ 16
+#endif
+#if !defined(NS_INT16SZ)
+# define NS_INT16SZ 2
+#endif
+
+/*
+ * WARNING: Don't even consider trying to compile this on a system where
+ * sizeof(int) < 4. sizeof(int) > 4 is fine; all the world's not a VAX.
+ */
+
+static const char *inet_ntop4(const unsigned char *src, char *dst, size_t size);
+#ifdef INET6
+static const char *inet_ntop6(const unsigned char *src, char *dst, size_t size);
+#endif /* INET6 */
+
+/* char *
+ * inet_ntop(af, src, dst, size)
+ * convert a network format address to presentation format.
+ * return:
+ * pointer to presentation format address (`dst'), or NULL (see errno).
+ * author:
+ * Paul Vixie, 1996.
+ */
+const char *
+inet_ntop(int af, const void *src, char *dst, size_t size)
+{
+
+ switch (af) {
+ case AF_INET:
+ return (inet_ntop4(src, dst, size));
+#ifdef INET6
+ case AF_INET6:
+ return (inet_ntop6(src, dst, size));
+#endif /* INET6 */
+ default:
+ errno = EAFNOSUPPORT;
+ return (NULL);
+ }
+ /* NOTREACHED */
+}
+
+/* const char *
+ * inet_ntop4(src, dst, size)
+ * format an IPv4 address, more or less like inet_ntoa()
+ * return:
+ * `dst' (as a const)
+ * notes:
+ * (1) uses no statics
+ * (2) takes a unsigned char* not an in_addr as input
+ * author:
+ * Paul Vixie, 1996.
+ */
+static const char *
+inet_ntop4(const unsigned char *src, char *dst, size_t size)
+{
+ char tmp[sizeof "255.255.255.255"];
+ int l;
+
+ l = snprintf(tmp, sizeof(tmp), "%u.%u.%u.%u",
+ src[0], src[1], src[2], src[3]);
+ if (l <= 0 || (size_t) l >= size) {
+ errno = ENOSPC;
+ return (NULL);
+ }
+ strlcpy(dst, tmp, size);
+ return (dst);
+}
+
+#ifdef INET6
+/* const char *
+ * inet_ntop6(src, dst, size)
+ * convert IPv6 binary address into presentation (printable) format
+ * author:
+ * Paul Vixie, 1996.
+ */
+static const char *
+inet_ntop6(const unsigned char *src, char *dst, size_t size)
+{
+ /*
+ * Note that int32_t and int16_t need only be "at least" large enough
+ * to contain a value of the specified size. On some systems, like
+ * Crays, there is no such thing as an integer variable with 16 bits.
+ * Keep this in mind if you think this function should have been coded
+ * to use pointer overlays. All the world's not a VAX.
+ */
+ char tmp[sizeof "ffff:ffff:ffff:ffff:ffff:ffff:255.255.255.255"];
+ char *tp, *ep;
+ struct { int base, len; } best, cur;
+ unsigned int words[NS_IN6ADDRSZ / NS_INT16SZ];
+ int i;
+ int advance;
+
+ /*
+ * Preprocess:
+ * Copy the input (bytewise) array into a wordwise array.
+ * Find the longest run of 0x00's in src[] for :: shorthanding.
+ */
+ memset(words, '\0', sizeof words);
+ for (i = 0; i < NS_IN6ADDRSZ; i++)
+ words[i / 2] |= (src[i] << ((1 - (i % 2)) << 3));
+ best.base = -1;
+ cur.base = -1;
+ best.len = -1; /* XXX gcc */
+ cur.len = -1; /* XXX gcc */
+ for (i = 0; i < (NS_IN6ADDRSZ / NS_INT16SZ); i++) {
+ if (words[i] == 0) {
+ if (cur.base == -1)
+ cur.base = i, cur.len = 1;
+ else
+ cur.len++;
+ } else {
+ if (cur.base != -1) {
+ if (best.base == -1 || cur.len > best.len)
+ best = cur;
+ cur.base = -1;
+ }
+ }
+ }
+ if (cur.base != -1) {
+ if (best.base == -1 || cur.len > best.len)
+ best = cur;
+ }
+ if (best.base != -1 && best.len < 2)
+ best.base = -1;
+
+ /*
+ * Format the result.
+ */
+ tp = tmp;
+ ep = tmp + sizeof(tmp);
+ for (i = 0; i < (NS_IN6ADDRSZ / NS_INT16SZ); i++) {
+ /* Are we inside the best run of 0x00's? */
+ if (best.base != -1 && i >= best.base &&
+ i < (best.base + best.len)) {
+ if (i == best.base)
+ *tp++ = ':';
+ continue;
+ }
+ /* Are we following an initial run of 0x00s or any real hex? */
+ if (i != 0) {
+ if (tp + 1 >= ep)
+ return (NULL);
+ *tp++ = ':';
+ }
+ /* Is this address an encapsulated IPv4? */
+ if (i == 6 && best.base == 0 &&
+ (best.len == 6 ||
+ (best.len == 7 && words[7] != 0x0001) ||
+ (best.len == 5 && words[5] == 0xffff))) {
+ if (!inet_ntop4(src+12, tp, (size_t)(ep - tp)))
+ return (NULL);
+ tp += strlen(tp);
+ break;
+ }
+ advance = snprintf(tp, (size_t)(ep - tp), "%x", words[i]);
+ if (advance <= 0 || advance >= ep - tp)
+ return (NULL);
+ tp += advance;
+ }
+ /* Was it a trailing run of 0x00's? */
+ if (best.base != -1 && (best.base + best.len) ==
+ (NS_IN6ADDRSZ / NS_INT16SZ)) {
+ if (tp + 1 >= ep)
+ return (NULL);
+ *tp++ = ':';
+ }
+ if (tp + 1 >= ep)
+ return (NULL);
+ *tp++ = '\0';
+
+ /*
+ * Check for overflow, copy, and we're done.
+ */
+ if ((size_t)(tp - tmp) > size) {
+ errno = ENOSPC;
+ return (NULL);
+ }
+ strlcpy(dst, tmp, size);
+ return (dst);
+}
+#endif /* INET6 */
+
+#ifdef WIN32
+/* Not exactly, will forgive stuff like <addr>:<port> */
+int inet_pton(int af, const char *src, void *dst)
+{
+ struct sockaddr_storage ss;
+ int addrlen = sizeof(ss);
+
+ if (af != AF_INET && af != AF_INET6) {
+ return -1;
+ }
+
+ if (!WSAStringToAddressA(src, af, NULL, (struct sockaddr*)&ss, &addrlen)) {
+ if (af == AF_INET) {
+ struct sockaddr_in *in = (struct sockaddr_in*)&ss;
+ memcpy(dst, &in->sin_addr, sizeof(struct in_addr));
+ } else {
+ struct sockaddr_in6 *in6 = (struct sockaddr_in6*)&ss;
+ memcpy(dst, &in6->sin6_addr, sizeof(struct in6_addr));
+ }
+ return 1;
+ }
+ return 0;
+}
+#endif /* WIN32 */
+
+#endif
+
+#ifdef WIN32
+#include <time.h>
+/* this is only millisecond-accurate, but that should be OK */
+
+int gettimeofday(struct timeval *tv, void *tz)
+ {
+ SYSTEMTIME st;
+ FILETIME ft;
+ ULARGE_INTEGER u;
+
+ GetLocalTime (&st);
+
+ /* strangely, the FILETIME is the number of 100 nanosecond (0.1 us) intervals
+ * since the Epoch */
+ SystemTimeToFileTime(&st, &ft);
+ u.HighPart = ft.dwHighDateTime;
+ u.LowPart = ft.dwLowDateTime;
+
+ tv->tv_sec = (long) (u.QuadPart / 10000000L);
+ tv->tv_usec = (long) (st.wMilliseconds * 1000);;
+
+ return 0;
+ }
+#endif
+
diff --git a/dom/media/webrtc/transport/third_party/nrappkit/src/util/util.h b/dom/media/webrtc/transport/third_party/nrappkit/src/util/util.h
new file mode 100644
index 0000000000..975baa4aa2
--- /dev/null
+++ b/dom/media/webrtc/transport/third_party/nrappkit/src/util/util.h
@@ -0,0 +1,73 @@
+/**
+ util.h
+
+
+ Copyright (C) 2001-2003, Network Resonance, Inc.
+ Copyright (C) 2006, Network Resonance, Inc.
+ All Rights Reserved
+
+ Redistribution and use in source and binary forms, with or without
+ modification, are permitted provided that the following conditions
+ are met:
+
+ 1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ 2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+ 3. Neither the name of Network Resonance, Inc. nor the name of any
+ contributors to this software may be used to endorse or promote
+ products derived from this software without specific prior written
+ permission.
+
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS ``AS IS''
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+ LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+ CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+ SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+ CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+ POSSIBILITY OF SUCH DAMAGE.
+
+
+ ekr@rtfm.com Wed Dec 26 17:20:23 2001
+ */
+
+
+#ifndef _util_h
+#define _util_h
+
+#include "registry.h"
+
+int nr_get_filename(char *base,char *name, char **namep);
+#if 0
+#include <openssl/ssl.h>
+
+int read_RSA_private_key(char *base, char *name,RSA **keyp);
+#endif
+void nr_errprintf_log(const char *fmt,...);
+void nr_errprintf_log2(void *ignore, const char *fmt,...);
+extern int nr_util_default_log_facility;
+
+int nr_read_data(int fd,char *buf,int len);
+int nr_drop_privileges(char *username);
+int nr_hex_ascii_dump(Data *data);
+int nr_fwrite_all(FILE *fp,UCHAR *buf,int len);
+int nr_sha1_file(char *filename,UCHAR *out);
+int nr_bin2hex(UCHAR *in,int len,UCHAR *out);
+int nr_rm_tree(char *path);
+int nr_write_pid_file(char *pid_filename);
+
+int nr_reg_uint4_fetch_and_check(NR_registry key, UINT4 min, UINT4 max, int log_fac, int die, UINT4 *val);
+int nr_reg_uint8_fetch_and_check(NR_registry key, UINT8 min, UINT8 max, int log_fac, int die, UINT8 *val);
+
+#if defined(WIN32) && WINVER < 0x0600
+const char *inet_ntop(int af, const void *src, char *dst, size_t size);
+int inet_pton(int af, const char *src, void *dst);
+#endif
+
+#endif
+
diff --git a/dom/media/webrtc/transport/transportflow.cpp b/dom/media/webrtc/transport/transportflow.cpp
new file mode 100644
index 0000000000..8b263c8cfe
--- /dev/null
+++ b/dom/media/webrtc/transport/transportflow.cpp
@@ -0,0 +1,74 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+#include <deque>
+
+#include "transportflow.h"
+#include "transportlayer.h"
+
+namespace mozilla {
+
+NS_IMPL_ISUPPORTS0(TransportFlow)
+
+// There are some hacks here to allow destruction off of
+// the main thread.
+TransportFlow::~TransportFlow() {
+ // Push the destruction onto the STS thread. Note that there
+ // is still some possibility that someone is accessing this
+ // object simultaneously, but as long as smart pointer discipline
+ // is maintained, it shouldn't be possible to access and
+ // destroy it simultaneously. The conversion to a UniquePtr
+ // ensures automatic destruction of the queue at exit of
+ // DestroyFinal.
+ CheckThread();
+ ClearLayers(layers_.get());
+}
+
+void TransportFlow::DestroyFinal(
+ UniquePtr<std::deque<TransportLayer*>> layers) {
+ ClearLayers(layers.get());
+}
+
+void TransportFlow::ClearLayers(std::deque<TransportLayer*>* layers) {
+ while (!layers->empty()) {
+ delete layers->front();
+ layers->pop_front();
+ }
+}
+
+void TransportFlow::PushLayer(TransportLayer* layer) {
+ CheckThread();
+ layers_->push_front(layer);
+ EnsureSameThread(layer);
+ layer->SetFlowId(id_);
+}
+
+TransportLayer* TransportFlow::GetLayer(const std::string& id) const {
+ CheckThread();
+
+ if (layers_) {
+ for (TransportLayer* layer : *layers_) {
+ if (layer->id() == id) return layer;
+ }
+ }
+
+ return nullptr;
+}
+
+void TransportFlow::EnsureSameThread(TransportLayer* layer) {
+ // Enforce that if any of the layers have a thread binding,
+ // they all have the same binding.
+ if (target_) {
+ const nsCOMPtr<nsIEventTarget>& lthread = layer->GetThread();
+
+ if (lthread && (lthread != target_)) MOZ_CRASH();
+ } else {
+ target_ = layer->GetThread();
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/transport/transportflow.h b/dom/media/webrtc/transport/transportflow.h
new file mode 100644
index 0000000000..43253a260b
--- /dev/null
+++ b/dom/media/webrtc/transport/transportflow.h
@@ -0,0 +1,105 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+#ifndef transportflow_h__
+#define transportflow_h__
+
+#include <deque>
+#include <string>
+
+#include "nscore.h"
+#include "nsISupportsImpl.h"
+#include "mozilla/UniquePtr.h"
+#include "transportlayer.h"
+#include "m_cpp_utils.h"
+
+// A stack of transport layers acts as a flow.
+// Generally, one reads and writes to the top layer.
+
+// This code has a confusing hybrid threading model which
+// probably needs some eventual refactoring.
+// TODO(ekr@rtfm.com): Bug 844891
+//
+// TransportFlows are not inherently bound to a thread *but*
+// TransportLayers can be. If any layer in a flow is bound
+// to a given thread, then all layers in the flow MUST be
+// bound to that thread and you can only manipulate the
+// flow (push layers, write, etc.) on that thread.
+//
+// The sole official exception to this is that you are
+// allowed to *destroy* a flow off the bound thread provided
+// that there are no listeners on its signals. This exception
+// is designed to allow idioms where you create the flow
+// and then something goes wrong and you destroy it and
+// you don't want to bother with a thread dispatch.
+//
+// Eventually we hope to relax the "no listeners"
+// restriction by thread-locking the signals, but previous
+// attempts have caused deadlocks.
+//
+// Most of these invariants are enforced by hard asserts
+// (i.e., those which fire even in production builds).
+
+namespace mozilla {
+
+class TransportFlow final : public nsISupports {
+ public:
+ TransportFlow()
+ : id_("(anonymous)"), layers_(new std::deque<TransportLayer*>) {}
+ explicit TransportFlow(const std::string id)
+ : id_(id), layers_(new std::deque<TransportLayer*>) {}
+
+ const std::string& id() const { return id_; }
+
+ // Layer management. Note PushLayer() is not thread protected, so
+ // either:
+ // (a) Do it in the thread handling the I/O
+ // (b) Do it before you activate the I/O system
+ //
+ // The flow takes ownership of the layers after a successful
+ // push.
+ void PushLayer(TransportLayer* layer);
+
+ TransportLayer* GetLayer(const std::string& id) const;
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+
+ private:
+ ~TransportFlow();
+
+ DISALLOW_COPY_ASSIGN(TransportFlow);
+
+ // Check if we are on the right thread
+ void CheckThread() const {
+ if (!CheckThreadInt()) MOZ_CRASH();
+ }
+
+ bool CheckThreadInt() const {
+ bool on;
+
+ if (!target_) // OK if no thread set.
+ return true;
+ if (NS_FAILED(target_->IsOnCurrentThread(&on))) return false;
+
+ return on;
+ }
+
+ void EnsureSameThread(TransportLayer* layer);
+
+ static void DestroyFinal(UniquePtr<std::deque<TransportLayer*>> layers);
+
+ // Overload needed because we use deque internally and queue externally.
+ static void ClearLayers(std::deque<TransportLayer*>* layers);
+
+ std::string id_;
+ UniquePtr<std::deque<TransportLayer*>> layers_;
+ nsCOMPtr<nsIEventTarget> target_;
+};
+
+} // namespace mozilla
+#endif
diff --git a/dom/media/webrtc/transport/transportlayer.cpp b/dom/media/webrtc/transport/transportlayer.cpp
new file mode 100644
index 0000000000..8816f24754
--- /dev/null
+++ b/dom/media/webrtc/transport/transportlayer.cpp
@@ -0,0 +1,49 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+#include "logging.h"
+#include "transportlayer.h"
+
+// Logging context
+namespace mozilla {
+
+MOZ_MTLOG_MODULE("mtransport")
+
+nsresult TransportLayer::Init() {
+ if (state_ != TS_NONE) return state_ == TS_ERROR ? NS_ERROR_FAILURE : NS_OK;
+
+ nsresult rv = InitInternal();
+
+ if (!NS_SUCCEEDED(rv)) {
+ state_ = TS_ERROR;
+ return rv;
+ }
+ state_ = TS_INIT;
+
+ return NS_OK;
+}
+
+void TransportLayer::Chain(TransportLayer* downward) {
+ downward_ = downward;
+ MOZ_MTLOG(ML_DEBUG, LAYER_INFO << "Inserted: downward='"
+ << (downward ? downward->id() : "none")
+ << "'");
+
+ WasInserted();
+}
+
+void TransportLayer::SetState(State state, const char* file, unsigned line) {
+ if (state != state_) {
+ MOZ_MTLOG(state == TS_ERROR ? ML_ERROR : ML_DEBUG,
+ file << ":" << line << ": " << LAYER_INFO << "state " << state_
+ << "->" << state);
+ state_ = state;
+ SignalStateChange(this, state);
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/transport/transportlayer.h b/dom/media/webrtc/transport/transportlayer.h
new file mode 100644
index 0000000000..47287ab4e0
--- /dev/null
+++ b/dom/media/webrtc/transport/transportlayer.h
@@ -0,0 +1,108 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+#ifndef transportlayer_h__
+#define transportlayer_h__
+
+#include "sigslot.h"
+
+#include "nsCOMPtr.h"
+#include "nsIEventTarget.h"
+
+#include "m_cpp_utils.h"
+#include "mediapacket.h"
+
+namespace mozilla {
+
+class TransportFlow;
+
+typedef int TransportResult;
+
+enum { TE_WOULDBLOCK = -1, TE_ERROR = -2, TE_INTERNAL = -3 };
+
+#define TRANSPORT_LAYER_ID(name) \
+ const std::string id() const override { return name; } \
+ static std::string ID() { return name; }
+
+// Abstract base class for network transport layers.
+class TransportLayer : public sigslot::has_slots<> {
+ public:
+ // The state of the transport flow
+ // We can't use "ERROR" because Windows has a macro named "ERROR"
+ enum State { TS_NONE, TS_INIT, TS_CONNECTING, TS_OPEN, TS_CLOSED, TS_ERROR };
+
+ // Is this a stream or datagram flow
+ TransportLayer() : state_(TS_NONE), flow_id_(), downward_(nullptr) {}
+
+ virtual ~TransportLayer() = default;
+
+ // Called to initialize
+ nsresult Init(); // Called by Insert() to set up -- do not override
+ virtual nsresult InitInternal() { return NS_OK; } // Called by Init
+
+ void SetFlowId(const std::string& flow_id) { flow_id_ = flow_id; }
+
+ virtual void Chain(TransportLayer* downward);
+
+ // Downward interface
+ TransportLayer* downward() { return downward_; }
+
+ // Get the state
+ State state() const { return state_; }
+ // Must be implemented by derived classes
+ virtual TransportResult SendPacket(MediaPacket& packet) = 0;
+
+ // Get the thread.
+ const nsCOMPtr<nsIEventTarget> GetThread() const { return target_; }
+
+ // Event definitions that one can register for
+ // State has changed
+ sigslot::signal2<TransportLayer*, State> SignalStateChange;
+ // Data received on the flow
+ sigslot::signal2<TransportLayer*, MediaPacket&> SignalPacketReceived;
+
+ // Return the layer id for this layer
+ virtual const std::string id() const = 0;
+
+ // The id of the flow
+ const std::string& flow_id() const { return flow_id_; }
+
+ protected:
+ virtual void WasInserted() {}
+ virtual void SetState(State state, const char* file, unsigned line);
+ // Check if we are on the right thread
+ void CheckThread() const { MOZ_ASSERT(CheckThreadInt(), "Wrong thread"); }
+
+ State state_;
+ std::string flow_id_;
+ TransportLayer* downward_; // The next layer in the stack
+ nsCOMPtr<nsIEventTarget> target_;
+
+ private:
+ DISALLOW_COPY_ASSIGN(TransportLayer);
+
+ bool CheckThreadInt() const {
+ bool on;
+
+ if (!target_) // OK if no thread set.
+ return true;
+
+ NS_ENSURE_SUCCESS(target_->IsOnCurrentThread(&on), false);
+ NS_ENSURE_TRUE(on, false);
+
+ return true;
+ }
+};
+
+#define LAYER_INFO \
+ "Flow[" << flow_id() << "(none)" \
+ << "]; Layer[" << id() << "]: "
+#define TL_SET_STATE(x) SetState((x), __FILE__, __LINE__)
+
+} // namespace mozilla
+#endif
diff --git a/dom/media/webrtc/transport/transportlayerdtls.cpp b/dom/media/webrtc/transport/transportlayerdtls.cpp
new file mode 100644
index 0000000000..4ab8aaa029
--- /dev/null
+++ b/dom/media/webrtc/transport/transportlayerdtls.cpp
@@ -0,0 +1,1558 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+#include "transportlayerdtls.h"
+
+#include <algorithm>
+#include <queue>
+#include <sstream>
+
+#include "dtlsidentity.h"
+#include "keyhi.h"
+#include "logging.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/Unused.h"
+#include "nsCOMPtr.h"
+#include "nsNetCID.h"
+#include "nsServiceManagerUtils.h"
+#include "sslexp.h"
+#include "sslproto.h"
+
+namespace mozilla {
+
+MOZ_MTLOG_MODULE("mtransport")
+
+static PRDescIdentity transport_layer_identity = PR_INVALID_IO_LAYER;
+
+// TODO: Implement a mode for this where
+// the channel is not ready until confirmed externally
+// (e.g., after cert check).
+
+#define UNIMPLEMENTED \
+ MOZ_MTLOG(ML_ERROR, "Call to unimplemented function " << __FUNCTION__); \
+ MOZ_ASSERT(false); \
+ PR_SetError(PR_NOT_IMPLEMENTED_ERROR, 0)
+
+#define MAX_ALPN_LENGTH 255
+
+// We need to adapt the NSPR/libssl model to the TransportFlow model.
+// The former wants pull semantics and TransportFlow wants push.
+//
+// - A TransportLayerDtls assumes it is sitting on top of another
+// TransportLayer, which means that events come in asynchronously.
+// - NSS (libssl) wants to sit on top of a PRFileDesc and poll.
+// - The TransportLayerNSPRAdapter is a PRFileDesc containing a
+// FIFO.
+// - When TransportLayerDtls.PacketReceived() is called, we insert
+// the packets in the FIFO and then do a PR_Recv() on the NSS
+// PRFileDesc, which eventually reads off the FIFO.
+//
+// All of this stuff is assumed to happen solely in a single thread
+// (generally the SocketTransportService thread)
+
+void TransportLayerNSPRAdapter::PacketReceived(MediaPacket& packet) {
+ if (enabled_) {
+ input_.push(new MediaPacket(std::move(packet)));
+ }
+}
+
+int32_t TransportLayerNSPRAdapter::Recv(void* buf, int32_t buflen) {
+ if (input_.empty()) {
+ PR_SetError(PR_WOULD_BLOCK_ERROR, 0);
+ return -1;
+ }
+
+ MediaPacket* front = input_.front();
+ int32_t count = static_cast<int32_t>(front->len());
+
+ if (buflen < count) {
+ MOZ_ASSERT(false, "Not enough buffer space to receive into");
+ PR_SetError(PR_BUFFER_OVERFLOW_ERROR, 0);
+ return -1;
+ }
+
+ memcpy(buf, front->data(), count);
+
+ input_.pop();
+ delete front;
+
+ return count;
+}
+
+int32_t TransportLayerNSPRAdapter::Write(const void* buf, int32_t length) {
+ if (!enabled_) {
+ MOZ_MTLOG(ML_WARNING, "Writing to disabled transport layer");
+ return -1;
+ }
+
+ MediaPacket packet;
+ // Copies. Oh well.
+ packet.Copy(static_cast<const uint8_t*>(buf), static_cast<size_t>(length));
+ packet.SetType(MediaPacket::DTLS);
+
+ TransportResult r = output_->SendPacket(packet);
+ if (r >= 0) {
+ return r;
+ }
+
+ if (r == TE_WOULDBLOCK) {
+ PR_SetError(PR_WOULD_BLOCK_ERROR, 0);
+ } else {
+ PR_SetError(PR_IO_ERROR, 0);
+ }
+
+ return -1;
+}
+
+// Implementation of NSPR methods
+static PRStatus TransportLayerClose(PRFileDesc* f) {
+ f->dtor(f);
+ return PR_SUCCESS;
+}
+
+static int32_t TransportLayerRead(PRFileDesc* f, void* buf, int32_t length) {
+ UNIMPLEMENTED;
+ return -1;
+}
+
+static int32_t TransportLayerWrite(PRFileDesc* f, const void* buf,
+ int32_t length) {
+ TransportLayerNSPRAdapter* io =
+ reinterpret_cast<TransportLayerNSPRAdapter*>(f->secret);
+ return io->Write(buf, length);
+}
+
+static int32_t TransportLayerAvailable(PRFileDesc* f) {
+ UNIMPLEMENTED;
+ return -1;
+}
+
+int64_t TransportLayerAvailable64(PRFileDesc* f) {
+ UNIMPLEMENTED;
+ return -1;
+}
+
+static PRStatus TransportLayerSync(PRFileDesc* f) {
+ UNIMPLEMENTED;
+ return PR_FAILURE;
+}
+
+static int32_t TransportLayerSeek(PRFileDesc* f, int32_t offset,
+ PRSeekWhence how) {
+ UNIMPLEMENTED;
+ return -1;
+}
+
+static int64_t TransportLayerSeek64(PRFileDesc* f, int64_t offset,
+ PRSeekWhence how) {
+ UNIMPLEMENTED;
+ return -1;
+}
+
+static PRStatus TransportLayerFileInfo(PRFileDesc* f, PRFileInfo* info) {
+ UNIMPLEMENTED;
+ return PR_FAILURE;
+}
+
+static PRStatus TransportLayerFileInfo64(PRFileDesc* f, PRFileInfo64* info) {
+ UNIMPLEMENTED;
+ return PR_FAILURE;
+}
+
+static int32_t TransportLayerWritev(PRFileDesc* f, const PRIOVec* iov,
+ int32_t iov_size, PRIntervalTime to) {
+ UNIMPLEMENTED;
+ return -1;
+}
+
+static PRStatus TransportLayerConnect(PRFileDesc* f, const PRNetAddr* addr,
+ PRIntervalTime to) {
+ UNIMPLEMENTED;
+ return PR_FAILURE;
+}
+
+static PRFileDesc* TransportLayerAccept(PRFileDesc* sd, PRNetAddr* addr,
+ PRIntervalTime to) {
+ UNIMPLEMENTED;
+ return nullptr;
+}
+
+static PRStatus TransportLayerBind(PRFileDesc* f, const PRNetAddr* addr) {
+ UNIMPLEMENTED;
+ return PR_FAILURE;
+}
+
+static PRStatus TransportLayerListen(PRFileDesc* f, int32_t depth) {
+ UNIMPLEMENTED;
+ return PR_FAILURE;
+}
+
+static PRStatus TransportLayerShutdown(PRFileDesc* f, int32_t how) {
+ // This is only called from NSS when we are the server and the client refuses
+ // to provide a certificate. In this case, the handshake is destined for
+ // failure, so we will just let this pass.
+ TransportLayerNSPRAdapter* io =
+ reinterpret_cast<TransportLayerNSPRAdapter*>(f->secret);
+ io->SetEnabled(false);
+ return PR_SUCCESS;
+}
+
+// This function does not support peek, or waiting until `to`
+static int32_t TransportLayerRecv(PRFileDesc* f, void* buf, int32_t buflen,
+ int32_t flags, PRIntervalTime to) {
+ MOZ_ASSERT(flags == 0);
+ if (flags != 0) {
+ PR_SetError(PR_NOT_IMPLEMENTED_ERROR, 0);
+ return -1;
+ }
+
+ TransportLayerNSPRAdapter* io =
+ reinterpret_cast<TransportLayerNSPRAdapter*>(f->secret);
+ return io->Recv(buf, buflen);
+}
+
+// Note: this is always nonblocking and assumes a zero timeout.
+static int32_t TransportLayerSend(PRFileDesc* f, const void* buf,
+ int32_t amount, int32_t flags,
+ PRIntervalTime to) {
+ int32_t written = TransportLayerWrite(f, buf, amount);
+ return written;
+}
+
+static int32_t TransportLayerRecvfrom(PRFileDesc* f, void* buf, int32_t amount,
+ int32_t flags, PRNetAddr* addr,
+ PRIntervalTime to) {
+ UNIMPLEMENTED;
+ return -1;
+}
+
+static int32_t TransportLayerSendto(PRFileDesc* f, const void* buf,
+ int32_t amount, int32_t flags,
+ const PRNetAddr* addr, PRIntervalTime to) {
+ UNIMPLEMENTED;
+ return -1;
+}
+
+static int16_t TransportLayerPoll(PRFileDesc* f, int16_t in_flags,
+ int16_t* out_flags) {
+ UNIMPLEMENTED;
+ return -1;
+}
+
+static int32_t TransportLayerAcceptRead(PRFileDesc* sd, PRFileDesc** nd,
+ PRNetAddr** raddr, void* buf,
+ int32_t amount, PRIntervalTime t) {
+ UNIMPLEMENTED;
+ return -1;
+}
+
+static int32_t TransportLayerTransmitFile(PRFileDesc* sd, PRFileDesc* f,
+ const void* headers, int32_t hlen,
+ PRTransmitFileFlags flags,
+ PRIntervalTime t) {
+ UNIMPLEMENTED;
+ return -1;
+}
+
+static PRStatus TransportLayerGetpeername(PRFileDesc* f, PRNetAddr* addr) {
+ // TODO: Modify to return unique names for each channel
+ // somehow, as opposed to always the same static address. The current
+ // implementation messes up the session cache, which is why it's off
+ // elsewhere
+ addr->inet.family = PR_AF_INET;
+ addr->inet.port = 0;
+ addr->inet.ip = 0;
+
+ return PR_SUCCESS;
+}
+
+static PRStatus TransportLayerGetsockname(PRFileDesc* f, PRNetAddr* addr) {
+ UNIMPLEMENTED;
+ return PR_FAILURE;
+}
+
+static PRStatus TransportLayerGetsockoption(PRFileDesc* f,
+ PRSocketOptionData* opt) {
+ switch (opt->option) {
+ case PR_SockOpt_Nonblocking:
+ opt->value.non_blocking = PR_TRUE;
+ return PR_SUCCESS;
+ default:
+ UNIMPLEMENTED;
+ break;
+ }
+
+ return PR_FAILURE;
+}
+
+// Imitate setting socket options. These are mostly noops.
+static PRStatus TransportLayerSetsockoption(PRFileDesc* f,
+ const PRSocketOptionData* opt) {
+ switch (opt->option) {
+ case PR_SockOpt_Nonblocking:
+ return PR_SUCCESS;
+ case PR_SockOpt_NoDelay:
+ return PR_SUCCESS;
+ default:
+ UNIMPLEMENTED;
+ break;
+ }
+
+ return PR_FAILURE;
+}
+
+static int32_t TransportLayerSendfile(PRFileDesc* out, PRSendFileData* in,
+ PRTransmitFileFlags flags,
+ PRIntervalTime to) {
+ UNIMPLEMENTED;
+ return -1;
+}
+
+static PRStatus TransportLayerConnectContinue(PRFileDesc* f, int16_t flags) {
+ UNIMPLEMENTED;
+ return PR_FAILURE;
+}
+
+static int32_t TransportLayerReserved(PRFileDesc* f) {
+ UNIMPLEMENTED;
+ return -1;
+}
+
+static const struct PRIOMethods TransportLayerMethods = {
+ PR_DESC_LAYERED,
+ TransportLayerClose,
+ TransportLayerRead,
+ TransportLayerWrite,
+ TransportLayerAvailable,
+ TransportLayerAvailable64,
+ TransportLayerSync,
+ TransportLayerSeek,
+ TransportLayerSeek64,
+ TransportLayerFileInfo,
+ TransportLayerFileInfo64,
+ TransportLayerWritev,
+ TransportLayerConnect,
+ TransportLayerAccept,
+ TransportLayerBind,
+ TransportLayerListen,
+ TransportLayerShutdown,
+ TransportLayerRecv,
+ TransportLayerSend,
+ TransportLayerRecvfrom,
+ TransportLayerSendto,
+ TransportLayerPoll,
+ TransportLayerAcceptRead,
+ TransportLayerTransmitFile,
+ TransportLayerGetsockname,
+ TransportLayerGetpeername,
+ TransportLayerReserved,
+ TransportLayerReserved,
+ TransportLayerGetsockoption,
+ TransportLayerSetsockoption,
+ TransportLayerSendfile,
+ TransportLayerConnectContinue,
+ TransportLayerReserved,
+ TransportLayerReserved,
+ TransportLayerReserved,
+ TransportLayerReserved};
+
+TransportLayerDtls::~TransportLayerDtls() {
+ // Destroy the NSS instance first so it can still send out an alert before
+ // we disable the nspr_io_adapter_.
+ ssl_fd_ = nullptr;
+ nspr_io_adapter_->SetEnabled(false);
+ if (timer_) {
+ timer_->Cancel();
+ }
+}
+
+nsresult TransportLayerDtls::InitInternal() {
+ // Get the transport service as an event target
+ nsresult rv;
+ target_ = do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv);
+
+ if (NS_FAILED(rv)) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't get socket transport service");
+ return rv;
+ }
+
+ timer_ = NS_NewTimer();
+ if (!timer_) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't get timer");
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+void TransportLayerDtls::WasInserted() {
+ // Connect to the lower layers
+ if (!Setup()) {
+ TL_SET_STATE(TS_ERROR);
+ }
+}
+
+// Set the permitted and default ALPN identifiers.
+// The default is here to allow for peers that don't want to negotiate ALPN
+// in that case, the default string will be reported from GetNegotiatedAlpn().
+// Setting the default to the empty string causes the transport layer to fail
+// if ALPN is not negotiated.
+// Note: we only support Unicode strings here, which are encoded into UTF-8,
+// even though ALPN ostensibly allows arbitrary octet sequences.
+nsresult TransportLayerDtls::SetAlpn(const std::set<std::string>& alpn_allowed,
+ const std::string& alpn_default) {
+ alpn_allowed_ = alpn_allowed;
+ alpn_default_ = alpn_default;
+
+ return NS_OK;
+}
+
+nsresult TransportLayerDtls::SetVerificationAllowAll() {
+ // Defensive programming
+ if (verification_mode_ != VERIFY_UNSET) return NS_ERROR_ALREADY_INITIALIZED;
+
+ verification_mode_ = VERIFY_ALLOW_ALL;
+
+ return NS_OK;
+}
+
+nsresult TransportLayerDtls::SetVerificationDigest(const DtlsDigest& digest) {
+ // Defensive programming
+ if (verification_mode_ != VERIFY_UNSET &&
+ verification_mode_ != VERIFY_DIGEST) {
+ return NS_ERROR_ALREADY_INITIALIZED;
+ }
+
+ digests_.push_back(digest);
+ verification_mode_ = VERIFY_DIGEST;
+ return NS_OK;
+}
+
+void TransportLayerDtls::SetMinMaxVersion(Version min_version,
+ Version max_version) {
+ if (min_version < Version::DTLS_1_0 || min_version > Version::DTLS_1_3 ||
+ max_version < Version::DTLS_1_0 || max_version > Version::DTLS_1_3 ||
+ min_version > max_version || max_version < min_version) {
+ return;
+ }
+ minVersion_ = min_version;
+ maxVersion_ = max_version;
+}
+
+// These are the named groups that we will allow.
+static const SSLNamedGroup NamedGroupPreferences[] = {
+ ssl_grp_ec_curve25519, ssl_grp_ec_secp256r1, ssl_grp_ec_secp384r1,
+ ssl_grp_ffdhe_2048, ssl_grp_ffdhe_3072};
+
+// TODO: make sure this is called from STS. Otherwise
+// we have thread safety issues
+bool TransportLayerDtls::Setup() {
+ CheckThread();
+ SECStatus rv;
+
+ if (!downward_) {
+ MOZ_MTLOG(ML_ERROR, "DTLS layer with nothing below. This is useless");
+ return false;
+ }
+ nspr_io_adapter_ = MakeUnique<TransportLayerNSPRAdapter>(downward_);
+
+ if (!identity_) {
+ MOZ_MTLOG(ML_ERROR, "Can't start DTLS without an identity");
+ return false;
+ }
+
+ if (verification_mode_ == VERIFY_UNSET) {
+ MOZ_MTLOG(ML_ERROR,
+ "Can't start DTLS without specifying a verification mode");
+ return false;
+ }
+
+ if (transport_layer_identity == PR_INVALID_IO_LAYER) {
+ transport_layer_identity = PR_GetUniqueIdentity("nssstreamadapter");
+ }
+
+ UniquePRFileDesc pr_fd(
+ PR_CreateIOLayerStub(transport_layer_identity, &TransportLayerMethods));
+ MOZ_ASSERT(pr_fd != nullptr);
+ if (!pr_fd) return false;
+ pr_fd->secret = reinterpret_cast<PRFilePrivate*>(nspr_io_adapter_.get());
+
+ UniquePRFileDesc ssl_fd(DTLS_ImportFD(nullptr, pr_fd.get()));
+ MOZ_ASSERT(ssl_fd != nullptr); // This should never happen
+ if (!ssl_fd) {
+ return false;
+ }
+
+ Unused << pr_fd.release(); // ownership transfered to ssl_fd;
+
+ if (role_ == CLIENT) {
+ MOZ_MTLOG(ML_INFO, "Setting up DTLS as client");
+ rv = SSL_GetClientAuthDataHook(ssl_fd.get(), GetClientAuthDataHook, this);
+ if (rv != SECSuccess) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't set identity");
+ return false;
+ }
+
+ if (maxVersion_ >= Version::DTLS_1_3) {
+ MOZ_MTLOG(ML_INFO, "Setting DTLS1.3 supported_versions workaround");
+ rv = SSL_SetDtls13VersionWorkaround(ssl_fd.get(), PR_TRUE);
+ if (rv != SECSuccess) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't set DTLS1.3 workaround");
+ return false;
+ }
+ }
+ } else {
+ MOZ_MTLOG(ML_INFO, "Setting up DTLS as server");
+ // Server side
+ rv = SSL_ConfigSecureServer(ssl_fd.get(), identity_->cert().get(),
+ identity_->privkey().get(),
+ identity_->auth_type());
+ if (rv != SECSuccess) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't set identity");
+ return false;
+ }
+
+ UniqueCERTCertList zero_certs(CERT_NewCertList());
+ rv = SSL_SetTrustAnchors(ssl_fd.get(), zero_certs.get());
+ if (rv != SECSuccess) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't set trust anchors");
+ return false;
+ }
+
+ // Insist on a certificate from the client
+ rv = SSL_OptionSet(ssl_fd.get(), SSL_REQUEST_CERTIFICATE, PR_TRUE);
+ if (rv != SECSuccess) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't request certificate");
+ return false;
+ }
+
+ rv = SSL_OptionSet(ssl_fd.get(), SSL_REQUIRE_CERTIFICATE, PR_TRUE);
+ if (rv != SECSuccess) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't require certificate");
+ return false;
+ }
+ }
+
+ SSLVersionRange version_range = {static_cast<PRUint16>(minVersion_),
+ static_cast<PRUint16>(maxVersion_)};
+
+ rv = SSL_VersionRangeSet(ssl_fd.get(), &version_range);
+ if (rv != SECSuccess) {
+ MOZ_MTLOG(ML_ERROR, "Can't disable SSLv3");
+ return false;
+ }
+
+ rv = SSL_OptionSet(ssl_fd.get(), SSL_ENABLE_SESSION_TICKETS, PR_FALSE);
+ if (rv != SECSuccess) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't disable session tickets");
+ return false;
+ }
+
+ rv = SSL_OptionSet(ssl_fd.get(), SSL_NO_CACHE, PR_TRUE);
+ if (rv != SECSuccess) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't disable session caching");
+ return false;
+ }
+
+ rv = SSL_OptionSet(ssl_fd.get(), SSL_ENABLE_DEFLATE, PR_FALSE);
+ if (rv != SECSuccess) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't disable deflate");
+ return false;
+ }
+
+ rv = SSL_OptionSet(ssl_fd.get(), SSL_ENABLE_RENEGOTIATION,
+ SSL_RENEGOTIATE_NEVER);
+ if (rv != SECSuccess) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't disable renegotiation");
+ return false;
+ }
+
+ rv = SSL_OptionSet(ssl_fd.get(), SSL_ENABLE_FALSE_START, PR_FALSE);
+ if (rv != SECSuccess) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't disable false start");
+ return false;
+ }
+
+ rv = SSL_OptionSet(ssl_fd.get(), SSL_NO_LOCKS, PR_TRUE);
+ if (rv != SECSuccess) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't disable locks");
+ return false;
+ }
+
+ rv = SSL_OptionSet(ssl_fd.get(), SSL_REUSE_SERVER_ECDHE_KEY, PR_FALSE);
+ if (rv != SECSuccess) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't disable ECDHE key reuse");
+ return false;
+ }
+
+ if (!SetupCipherSuites(ssl_fd)) {
+ return false;
+ }
+
+ rv = SSL_NamedGroupConfig(ssl_fd.get(), NamedGroupPreferences,
+ mozilla::ArrayLength(NamedGroupPreferences));
+ if (rv != SECSuccess) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't set named groups");
+ return false;
+ }
+
+ // Certificate validation
+ rv = SSL_AuthCertificateHook(ssl_fd.get(), AuthCertificateHook,
+ reinterpret_cast<void*>(this));
+ if (rv != SECSuccess) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't set certificate validation hook");
+ return false;
+ }
+
+ if (!SetupAlpn(ssl_fd)) {
+ return false;
+ }
+
+ // Now start the handshake
+ rv = SSL_ResetHandshake(ssl_fd.get(), role_ == SERVER ? PR_TRUE : PR_FALSE);
+ if (rv != SECSuccess) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't reset handshake");
+ return false;
+ }
+ ssl_fd_ = std::move(ssl_fd);
+
+ // Finally, get ready to receive data
+ downward_->SignalStateChange.connect(this, &TransportLayerDtls::StateChange);
+ downward_->SignalPacketReceived.connect(this,
+ &TransportLayerDtls::PacketReceived);
+
+ if (downward_->state() == TS_OPEN) {
+ TL_SET_STATE(TS_CONNECTING);
+ Handshake();
+ }
+
+ return true;
+}
+
+bool TransportLayerDtls::SetupAlpn(UniquePRFileDesc& ssl_fd) const {
+ if (alpn_allowed_.empty()) {
+ return true;
+ }
+
+ SECStatus rv = SSL_OptionSet(ssl_fd.get(), SSL_ENABLE_NPN, PR_FALSE);
+ if (rv != SECSuccess) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't disable NPN");
+ return false;
+ }
+
+ rv = SSL_OptionSet(ssl_fd.get(), SSL_ENABLE_ALPN, PR_TRUE);
+ if (rv != SECSuccess) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't enable ALPN");
+ return false;
+ }
+
+ unsigned char buf[MAX_ALPN_LENGTH];
+ size_t offset = 0;
+ for (const auto& tag : alpn_allowed_) {
+ if ((offset + 1 + tag.length()) >= sizeof(buf)) {
+ MOZ_MTLOG(ML_ERROR, "ALPN too long");
+ return false;
+ }
+ buf[offset++] = tag.length();
+ memcpy(buf + offset, tag.c_str(), tag.length());
+ offset += tag.length();
+ }
+ rv = SSL_SetNextProtoNego(ssl_fd.get(), buf, offset);
+ if (rv != SECSuccess) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't set ALPN string");
+ return false;
+ }
+ return true;
+}
+
+// Ciphers we need to enable. These are on by default in standard firefox
+// builds, but can be disabled with prefs and they aren't on in our unit tests
+// since that uses NSS default configuration.
+//
+// Only override prefs to comply with MUST statements in the security-arch doc.
+// Anything outside this list is governed by the usual combination of policy
+// and user preferences.
+static const uint32_t EnabledCiphers[] = {
+ TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
+ TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA};
+
+// Disable all NSS suites modes without PFS or with old and rusty ciphersuites.
+// Anything outside this list is governed by the usual combination of policy
+// and user preferences.
+static const uint32_t DisabledCiphers[] = {
+ // Bug 1310061: disable all SHA384 ciphers until fixed
+ TLS_AES_256_GCM_SHA384,
+ TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
+ TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+ TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384,
+ TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384,
+ TLS_DHE_RSA_WITH_AES_256_GCM_SHA384,
+ TLS_DHE_DSS_WITH_AES_256_GCM_SHA384,
+
+ TLS_DHE_RSA_WITH_AES_128_CBC_SHA,
+ TLS_DHE_RSA_WITH_AES_256_CBC_SHA,
+
+ TLS_ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA,
+ TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
+ TLS_ECDHE_ECDSA_WITH_RC4_128_SHA,
+ TLS_ECDHE_RSA_WITH_RC4_128_SHA,
+
+ TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA,
+ TLS_DHE_DSS_WITH_3DES_EDE_CBC_SHA,
+ TLS_DHE_DSS_WITH_RC4_128_SHA,
+
+ TLS_ECDH_ECDSA_WITH_AES_128_CBC_SHA,
+ TLS_ECDH_RSA_WITH_AES_128_CBC_SHA,
+ TLS_ECDH_ECDSA_WITH_AES_256_CBC_SHA,
+ TLS_ECDH_RSA_WITH_AES_256_CBC_SHA,
+ TLS_ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA,
+ TLS_ECDH_RSA_WITH_3DES_EDE_CBC_SHA,
+ TLS_ECDH_ECDSA_WITH_RC4_128_SHA,
+ TLS_ECDH_RSA_WITH_RC4_128_SHA,
+
+ TLS_RSA_WITH_AES_128_GCM_SHA256,
+ TLS_RSA_WITH_AES_256_GCM_SHA384,
+ TLS_RSA_WITH_AES_128_CBC_SHA,
+ TLS_RSA_WITH_AES_128_CBC_SHA256,
+ TLS_RSA_WITH_CAMELLIA_128_CBC_SHA,
+ TLS_RSA_WITH_AES_256_CBC_SHA,
+ TLS_RSA_WITH_AES_256_CBC_SHA256,
+ TLS_RSA_WITH_CAMELLIA_256_CBC_SHA,
+ TLS_RSA_WITH_SEED_CBC_SHA,
+ TLS_RSA_WITH_3DES_EDE_CBC_SHA,
+ TLS_RSA_WITH_RC4_128_SHA,
+ TLS_RSA_WITH_RC4_128_MD5,
+
+ TLS_DHE_RSA_WITH_DES_CBC_SHA,
+ TLS_DHE_DSS_WITH_DES_CBC_SHA,
+ TLS_RSA_WITH_DES_CBC_SHA,
+
+ TLS_ECDHE_ECDSA_WITH_NULL_SHA,
+ TLS_ECDHE_RSA_WITH_NULL_SHA,
+ TLS_ECDH_ECDSA_WITH_NULL_SHA,
+ TLS_ECDH_RSA_WITH_NULL_SHA,
+ TLS_RSA_WITH_NULL_SHA,
+ TLS_RSA_WITH_NULL_SHA256,
+ TLS_RSA_WITH_NULL_MD5,
+};
+
+bool TransportLayerDtls::SetupCipherSuites(UniquePRFileDesc& ssl_fd) {
+ SECStatus rv;
+
+ // Set the SRTP ciphers
+ if (!enabled_srtp_ciphers_.empty()) {
+ rv = SSL_InstallExtensionHooks(ssl_fd.get(), ssl_use_srtp_xtn,
+ TransportLayerDtls::WriteSrtpXtn, this,
+ TransportLayerDtls::HandleSrtpXtn, this);
+ if (rv != SECSuccess) {
+ MOZ_MTLOG(ML_ERROR, LAYER_INFO << "unable to set SRTP extension handler");
+ return false;
+ }
+ }
+
+ for (const auto& cipher : EnabledCiphers) {
+ MOZ_MTLOG(ML_DEBUG, LAYER_INFO << "Enabling: " << cipher);
+ rv = SSL_CipherPrefSet(ssl_fd.get(), cipher, PR_TRUE);
+ if (rv != SECSuccess) {
+ MOZ_MTLOG(ML_ERROR, LAYER_INFO << "Unable to enable suite: " << cipher);
+ return false;
+ }
+ }
+
+ for (const auto& cipher : DisabledCiphers) {
+ MOZ_MTLOG(ML_DEBUG, LAYER_INFO << "Disabling: " << cipher);
+
+ PRBool enabled = false;
+ rv = SSL_CipherPrefGet(ssl_fd.get(), cipher, &enabled);
+ if (rv != SECSuccess) {
+ MOZ_MTLOG(ML_NOTICE, LAYER_INFO << "Unable to check if suite is enabled: "
+ << cipher);
+ return false;
+ }
+ if (enabled) {
+ rv = SSL_CipherPrefSet(ssl_fd.get(), cipher, PR_FALSE);
+ if (rv != SECSuccess) {
+ MOZ_MTLOG(ML_NOTICE,
+ LAYER_INFO << "Unable to disable suite: " << cipher);
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+nsresult TransportLayerDtls::GetCipherSuite(uint16_t* cipherSuite) const {
+ CheckThread();
+ if (!cipherSuite) {
+ MOZ_MTLOG(ML_ERROR, LAYER_INFO << "GetCipherSuite passed a nullptr");
+ return NS_ERROR_NULL_POINTER;
+ }
+ if (state_ != TS_OPEN) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ SSLChannelInfo info;
+ SECStatus rv = SSL_GetChannelInfo(ssl_fd_.get(), &info, sizeof(info));
+ if (rv != SECSuccess) {
+ MOZ_MTLOG(ML_NOTICE, LAYER_INFO << "GetCipherSuite can't get channel info");
+ return NS_ERROR_FAILURE;
+ }
+ *cipherSuite = info.cipherSuite;
+ return NS_OK;
+}
+
+std::vector<uint16_t> TransportLayerDtls::GetDefaultSrtpCiphers() {
+ std::vector<uint16_t> ciphers;
+
+ ciphers.push_back(kDtlsSrtpAeadAes128Gcm);
+ // Since we don't support DTLS 1.3 or SHA384 ciphers (see bug 1312976)
+ // we don't really enough entropy to prefer this over 128 bit
+ ciphers.push_back(kDtlsSrtpAeadAes256Gcm);
+ ciphers.push_back(kDtlsSrtpAes128CmHmacSha1_80);
+#ifndef NIGHTLY_BUILD
+ // To support bug 1491583 lets try to find out if we get bug reports if we no
+ // longer offer this in Nightly builds.
+ ciphers.push_back(kDtlsSrtpAes128CmHmacSha1_32);
+#endif
+
+ return ciphers;
+}
+
+void TransportLayerDtls::StateChange(TransportLayer* layer, State state) {
+ switch (state) {
+ case TS_NONE:
+ MOZ_ASSERT(false); // Can't happen
+ break;
+
+ case TS_INIT:
+ MOZ_MTLOG(ML_ERROR,
+ LAYER_INFO << "State change of lower layer to INIT forbidden");
+ TL_SET_STATE(TS_ERROR);
+ break;
+
+ case TS_CONNECTING:
+ MOZ_MTLOG(ML_INFO, LAYER_INFO << "Lower layer is connecting.");
+ break;
+
+ case TS_OPEN:
+ if (timer_) {
+ MOZ_MTLOG(ML_INFO,
+ LAYER_INFO << "Lower layer is now open; starting TLS");
+ timer_->Cancel();
+ timer_->SetTarget(target_);
+ // Async, since the ICE layer might need to send a STUN response, and we
+ // don't want the handshake to start until that is sent.
+ timer_->InitWithNamedFuncCallback(TimerCallback, this, 0,
+ nsITimer::TYPE_ONE_SHOT,
+ "TransportLayerDtls::TimerCallback");
+ TL_SET_STATE(TS_CONNECTING);
+ } else {
+ // We have already completed DTLS. Can happen if the ICE layer failed
+ // due to a loss of network, and then recovered.
+ TL_SET_STATE(TS_OPEN);
+ }
+ break;
+
+ case TS_CLOSED:
+ MOZ_MTLOG(ML_INFO, LAYER_INFO << "Lower layer is now closed");
+ TL_SET_STATE(TS_CLOSED);
+ break;
+
+ case TS_ERROR:
+ MOZ_MTLOG(ML_ERROR, LAYER_INFO << "Lower layer experienced an error");
+ TL_SET_STATE(TS_ERROR);
+ break;
+ }
+}
+
+void TransportLayerDtls::Handshake() {
+ if (!timer_) {
+ // We are done with DTLS, regardless of the state changes of lower layers
+ return;
+ }
+
+ // Clear the retransmit timer
+ timer_->Cancel();
+
+ MOZ_ASSERT(state_ == TS_CONNECTING);
+
+ SECStatus rv = SSL_ForceHandshake(ssl_fd_.get());
+
+ if (rv == SECSuccess) {
+ MOZ_MTLOG(ML_NOTICE, LAYER_INFO << "****** SSL handshake completed ******");
+ if (!cert_ok_) {
+ MOZ_MTLOG(ML_ERROR, LAYER_INFO << "Certificate check never occurred");
+ TL_SET_STATE(TS_ERROR);
+ return;
+ }
+ if (!CheckAlpn()) {
+ // Despite connecting, the connection doesn't have a valid ALPN label.
+ // Forcibly close the connection so that the peer isn't left hanging
+ // (assuming the close_notify isn't dropped).
+ ssl_fd_ = nullptr;
+ TL_SET_STATE(TS_ERROR);
+ return;
+ }
+
+ TL_SET_STATE(TS_OPEN);
+
+ RecordTlsTelemetry();
+ timer_ = nullptr;
+ } else {
+ int32_t err = PR_GetError();
+ switch (err) {
+ case SSL_ERROR_RX_MALFORMED_HANDSHAKE:
+ MOZ_MTLOG(ML_ERROR, LAYER_INFO << "Malformed DTLS message; ignoring");
+ // If this were TLS (and not DTLS), this would be fatal, but
+ // here we're required to ignore bad messages, so fall through
+ [[fallthrough]];
+ case PR_WOULD_BLOCK_ERROR:
+ MOZ_MTLOG(ML_NOTICE, LAYER_INFO << "Handshake would have blocked");
+ PRIntervalTime timeout;
+ rv = DTLS_GetHandshakeTimeout(ssl_fd_.get(), &timeout);
+ if (rv == SECSuccess) {
+ uint32_t timeout_ms = PR_IntervalToMilliseconds(timeout);
+
+ MOZ_MTLOG(ML_DEBUG,
+ LAYER_INFO << "Setting DTLS timeout to " << timeout_ms);
+ timer_->SetTarget(target_);
+ timer_->InitWithNamedFuncCallback(
+ TimerCallback, this, timeout_ms, nsITimer::TYPE_ONE_SHOT,
+ "TransportLayerDtls::TimerCallback");
+ }
+ break;
+ default:
+ const char* err_msg = PR_ErrorToName(err);
+ MOZ_MTLOG(ML_ERROR, LAYER_INFO << "DTLS handshake error " << err << " ("
+ << err_msg << ")");
+ TL_SET_STATE(TS_ERROR);
+ break;
+ }
+ }
+}
+
+// Checks if ALPN was negotiated correctly and returns false if it wasn't.
+// After this returns successfully, alpn_ will be set to the negotiated
+// protocol.
+bool TransportLayerDtls::CheckAlpn() {
+ if (alpn_allowed_.empty()) {
+ return true;
+ }
+
+ SSLNextProtoState alpnState;
+ char chosenAlpn[MAX_ALPN_LENGTH];
+ unsigned int chosenAlpnLen;
+ SECStatus rv = SSL_GetNextProto(ssl_fd_.get(), &alpnState,
+ reinterpret_cast<unsigned char*>(chosenAlpn),
+ &chosenAlpnLen, sizeof(chosenAlpn));
+ if (rv != SECSuccess) {
+ MOZ_MTLOG(ML_ERROR, LAYER_INFO << "ALPN error");
+ return false;
+ }
+ switch (alpnState) {
+ case SSL_NEXT_PROTO_SELECTED:
+ case SSL_NEXT_PROTO_NEGOTIATED:
+ break; // OK
+
+ case SSL_NEXT_PROTO_NO_SUPPORT:
+ MOZ_MTLOG(ML_NOTICE,
+ LAYER_INFO << "ALPN not negotiated, "
+ << (alpn_default_.empty() ? "failing"
+ : "selecting default"));
+ alpn_ = alpn_default_;
+ return !alpn_.empty();
+
+ case SSL_NEXT_PROTO_NO_OVERLAP:
+ // This only happens if there is a custom NPN/ALPN callback installed and
+ // that callback doesn't properly handle ALPN.
+ MOZ_MTLOG(ML_ERROR, LAYER_INFO << "error in ALPN selection callback");
+ return false;
+
+ case SSL_NEXT_PROTO_EARLY_VALUE:
+ MOZ_CRASH("Unexpected 0-RTT ALPN value");
+ return false;
+ }
+
+ // Warning: NSS won't null terminate the ALPN string for us.
+ std::string chosen(chosenAlpn, chosenAlpnLen);
+ MOZ_MTLOG(ML_NOTICE, LAYER_INFO << "Selected ALPN string: " << chosen);
+ if (alpn_allowed_.find(chosen) == alpn_allowed_.end()) {
+ // Maybe our peer chose a protocol we didn't offer (when we are client), or
+ // something is seriously wrong.
+ std::ostringstream ss;
+ for (auto i = alpn_allowed_.begin(); i != alpn_allowed_.end(); ++i) {
+ ss << (i == alpn_allowed_.begin() ? " '" : ", '") << *i << "'";
+ }
+ MOZ_MTLOG(ML_ERROR, LAYER_INFO << "Bad ALPN string: '" << chosen
+ << "'; permitted:" << ss.str());
+ return false;
+ }
+ alpn_ = chosen;
+ return true;
+}
+
+void TransportLayerDtls::PacketReceived(TransportLayer* layer,
+ MediaPacket& packet) {
+ CheckThread();
+ MOZ_MTLOG(ML_DEBUG, LAYER_INFO << "PacketReceived(" << packet.len() << ")");
+
+ if (state_ != TS_CONNECTING && state_ != TS_OPEN) {
+ MOZ_MTLOG(ML_DEBUG,
+ LAYER_INFO << "Discarding packet in inappropriate state");
+ return;
+ }
+
+ if (!packet.data()) {
+ // Something ate this, probably the SRTP layer
+ return;
+ }
+
+ if (packet.type() != MediaPacket::DTLS) {
+ return;
+ }
+
+ nspr_io_adapter_->PacketReceived(packet);
+ GetDecryptedPackets();
+}
+
+void TransportLayerDtls::GetDecryptedPackets() {
+ // If we're still connecting, try to handshake
+ if (state_ == TS_CONNECTING) {
+ Handshake();
+ }
+
+ // Now try a recv if we're open, since there might be data left
+ if (state_ == TS_OPEN) {
+ int32_t rv;
+ // One packet might contain several DTLS packets
+ do {
+ // nICEr uses a 9216 bytes buffer to allow support for jumbo frames
+ // Can we peek to get a better idea of the actual size?
+ static const size_t kBufferSize = 9216;
+ auto buffer = MakeUnique<uint8_t[]>(kBufferSize);
+ rv = PR_Recv(ssl_fd_.get(), buffer.get(), kBufferSize, 0,
+ PR_INTERVAL_NO_WAIT);
+ if (rv > 0) {
+ // We have data
+ MOZ_MTLOG(ML_DEBUG, LAYER_INFO << "Read " << rv << " bytes from NSS");
+ MediaPacket packet;
+ packet.SetType(MediaPacket::SCTP);
+ packet.Take(std::move(buffer), static_cast<size_t>(rv));
+ SignalPacketReceived(this, packet);
+ } else if (rv == 0) {
+ TL_SET_STATE(TS_CLOSED);
+ } else {
+ int32_t err = PR_GetError();
+
+ if (err == PR_WOULD_BLOCK_ERROR) {
+ // This gets ignored
+ MOZ_MTLOG(ML_DEBUG, LAYER_INFO << "Receive would have blocked");
+ } else {
+ MOZ_MTLOG(ML_NOTICE, LAYER_INFO << "NSS Error " << err);
+ TL_SET_STATE(TS_ERROR);
+ }
+ }
+ } while (rv > 0);
+ }
+}
+
+void TransportLayerDtls::SetState(State state, const char* file,
+ unsigned line) {
+ if (timer_) {
+ switch (state) {
+ case TS_NONE:
+ case TS_INIT:
+ MOZ_ASSERT(false);
+ break;
+ case TS_CONNECTING:
+ break;
+ case TS_OPEN:
+ case TS_CLOSED:
+ case TS_ERROR:
+ timer_->Cancel();
+ break;
+ }
+ }
+
+ TransportLayer::SetState(state, file, line);
+}
+
+TransportResult TransportLayerDtls::SendPacket(MediaPacket& packet) {
+ CheckThread();
+ if (state_ != TS_OPEN) {
+ MOZ_MTLOG(ML_ERROR,
+ LAYER_INFO << "Can't call SendPacket() in state " << state_);
+ return TE_ERROR;
+ }
+
+ int32_t rv = PR_Send(ssl_fd_.get(), packet.data(), packet.len(), 0,
+ PR_INTERVAL_NO_WAIT);
+
+ if (rv > 0) {
+ // We have data
+ MOZ_MTLOG(ML_DEBUG, LAYER_INFO << "Wrote " << rv << " bytes to SSL Layer");
+ return rv;
+ }
+
+ if (rv == 0) {
+ TL_SET_STATE(TS_CLOSED);
+ return 0;
+ }
+
+ int32_t err = PR_GetError();
+
+ if (err == PR_WOULD_BLOCK_ERROR) {
+ // This gets ignored
+ MOZ_MTLOG(ML_DEBUG, LAYER_INFO << "Send would have blocked");
+ return TE_WOULDBLOCK;
+ }
+
+ MOZ_MTLOG(ML_NOTICE, LAYER_INFO << "NSS Error " << err);
+ TL_SET_STATE(TS_ERROR);
+ return TE_ERROR;
+}
+
+SECStatus TransportLayerDtls::GetClientAuthDataHook(
+ void* arg, PRFileDesc* fd, CERTDistNames* caNames,
+ CERTCertificate** pRetCert, SECKEYPrivateKey** pRetKey) {
+ MOZ_MTLOG(ML_DEBUG, "Server requested client auth");
+
+ TransportLayerDtls* stream = reinterpret_cast<TransportLayerDtls*>(arg);
+ stream->CheckThread();
+
+ if (!stream->identity_) {
+ MOZ_MTLOG(ML_ERROR, "No identity available");
+ PR_SetError(SSL_ERROR_NO_CERTIFICATE, 0);
+ return SECFailure;
+ }
+
+ *pRetCert = CERT_DupCertificate(stream->identity_->cert().get());
+ if (!*pRetCert) {
+ PR_SetError(PR_OUT_OF_MEMORY_ERROR, 0);
+ return SECFailure;
+ }
+
+ *pRetKey = SECKEY_CopyPrivateKey(stream->identity_->privkey().get());
+ if (!*pRetKey) {
+ CERT_DestroyCertificate(*pRetCert);
+ *pRetCert = nullptr;
+ PR_SetError(PR_OUT_OF_MEMORY_ERROR, 0);
+ return SECFailure;
+ }
+
+ return SECSuccess;
+}
+
+nsresult TransportLayerDtls::SetSrtpCiphers(
+ const std::vector<uint16_t>& ciphers) {
+ enabled_srtp_ciphers_ = std::move(ciphers);
+ return NS_OK;
+}
+
+nsresult TransportLayerDtls::GetSrtpCipher(uint16_t* cipher) const {
+ CheckThread();
+ if (srtp_cipher_ == 0) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ *cipher = srtp_cipher_;
+ return NS_OK;
+}
+
+static uint8_t* WriteUint16(uint8_t* cursor, uint16_t v) {
+ *cursor++ = v >> 8;
+ *cursor++ = v & 0xff;
+ return cursor;
+}
+
+static SSLHandshakeType SrtpXtnServerMessage(PRFileDesc* fd) {
+ SSLPreliminaryChannelInfo preinfo;
+ SECStatus rv = SSL_GetPreliminaryChannelInfo(fd, &preinfo, sizeof(preinfo));
+ if (rv != SECSuccess) {
+ MOZ_ASSERT(false, "Can't get version info");
+ return ssl_hs_client_hello;
+ }
+ return (preinfo.protocolVersion >= SSL_LIBRARY_VERSION_TLS_1_3)
+ ? ssl_hs_encrypted_extensions
+ : ssl_hs_server_hello;
+}
+
+/* static */
+PRBool TransportLayerDtls::WriteSrtpXtn(PRFileDesc* fd,
+ SSLHandshakeType message, uint8_t* data,
+ unsigned int* len, unsigned int max_len,
+ void* arg) {
+ auto self = reinterpret_cast<TransportLayerDtls*>(arg);
+
+ // ClientHello: send all supported versions.
+ if (message == ssl_hs_client_hello) {
+ MOZ_ASSERT(self->role_ == CLIENT);
+ MOZ_ASSERT(self->enabled_srtp_ciphers_.size(), "Haven't enabled SRTP");
+ // We will take 2 octets for each cipher, plus a 2 octet length and 1 octet
+ // for the length of the empty MKI.
+ if (max_len < self->enabled_srtp_ciphers_.size() * 2 + 3) {
+ MOZ_ASSERT(false, "Not enough space to send SRTP extension");
+ return false;
+ }
+ uint8_t* cursor = WriteUint16(data, self->enabled_srtp_ciphers_.size() * 2);
+ for (auto cs : self->enabled_srtp_ciphers_) {
+ cursor = WriteUint16(cursor, cs);
+ }
+ *cursor++ = 0; // MKI is empty
+ *len = cursor - data;
+ return true;
+ }
+
+ if (message == SrtpXtnServerMessage(fd)) {
+ MOZ_ASSERT(self->role_ == SERVER);
+ if (!self->srtp_cipher_) {
+ // Not negotiated. Definitely bad, but the connection can fail later.
+ return false;
+ }
+ if (max_len < 5) {
+ MOZ_ASSERT(false, "Not enough space to send SRTP extension");
+ return false;
+ }
+
+ uint8_t* cursor = WriteUint16(data, 2); // Length = 2.
+ cursor = WriteUint16(cursor, self->srtp_cipher_);
+ *cursor++ = 0; // No MKI
+ *len = cursor - data;
+ return true;
+ }
+
+ return false;
+}
+
+class TlsParser {
+ public:
+ TlsParser(const uint8_t* data, size_t len) : cursor_(data), remaining_(len) {}
+
+ bool error() const { return error_; }
+ size_t remaining() const { return remaining_; }
+
+ template <typename T,
+ class = typename std::enable_if<std::is_unsigned<T>::value>::type>
+ void Read(T* v, size_t sz = sizeof(T)) {
+ MOZ_ASSERT(sz <= sizeof(T),
+ "Type is too small to hold the value requested");
+ if (remaining_ < sz) {
+ error_ = true;
+ return;
+ }
+
+ T result = 0;
+ for (size_t i = 0; i < sz; ++i) {
+ result = (result << 8) | *cursor_++;
+ remaining_--;
+ }
+ *v = result;
+ }
+
+ template <typename T,
+ class = typename std::enable_if<std::is_unsigned<T>::value>::type>
+ void ReadVector(std::vector<T>* v, size_t w) {
+ MOZ_ASSERT(v->empty(), "vector needs to be empty");
+
+ uint32_t len;
+ Read(&len, w);
+ if (error_ || len % sizeof(T) != 0 || len > remaining_) {
+ error_ = true;
+ return;
+ }
+
+ size_t count = len / sizeof(T);
+ v->reserve(count);
+ for (T i = 0; !error_ && i < count; ++i) {
+ T item;
+ Read(&item);
+ if (!error_) {
+ v->push_back(item);
+ }
+ }
+ }
+
+ void Skip(size_t n) {
+ if (remaining_ < n) {
+ error_ = true;
+ } else {
+ cursor_ += n;
+ remaining_ -= n;
+ }
+ }
+
+ size_t SkipVector(size_t w) {
+ uint32_t len = 0;
+ Read(&len, w);
+ Skip(len);
+ return len;
+ }
+
+ private:
+ const uint8_t* cursor_;
+ size_t remaining_;
+ bool error_ = false;
+};
+
+/* static */
+SECStatus TransportLayerDtls::HandleSrtpXtn(
+ PRFileDesc* fd, SSLHandshakeType message, const uint8_t* data,
+ unsigned int len, SSLAlertDescription* alert, void* arg) {
+ static const uint8_t kTlsAlertHandshakeFailure = 40;
+ static const uint8_t kTlsAlertIllegalParameter = 47;
+ static const uint8_t kTlsAlertDecodeError = 50;
+ static const uint8_t kTlsAlertUnsupportedExtension = 110;
+
+ auto self = reinterpret_cast<TransportLayerDtls*>(arg);
+
+ // Parse the extension.
+ TlsParser parser(data, len);
+ std::vector<uint16_t> advertised;
+ parser.ReadVector(&advertised, 2);
+ size_t mki_len = parser.SkipVector(1);
+ if (parser.error() || parser.remaining() > 0) {
+ *alert = kTlsAlertDecodeError;
+ return SECFailure;
+ }
+
+ if (message == ssl_hs_client_hello) {
+ MOZ_ASSERT(self->role_ == SERVER);
+ if (self->enabled_srtp_ciphers_.empty()) {
+ // We don't have SRTP enabled, which is probably bad, but no sense in
+ // having the handshake fail at this point, let the client decide if this
+ // is a problem.
+ return SECSuccess;
+ }
+
+ for (auto supported : self->enabled_srtp_ciphers_) {
+ auto it = std::find(advertised.begin(), advertised.end(), supported);
+ if (it != advertised.end()) {
+ self->srtp_cipher_ = supported;
+ return SECSuccess;
+ }
+ }
+
+ // No common cipher.
+ *alert = kTlsAlertHandshakeFailure;
+ return SECFailure;
+ }
+
+ if (message == SrtpXtnServerMessage(fd)) {
+ MOZ_ASSERT(self->role_ == CLIENT);
+ if (advertised.size() != 1 || mki_len > 0) {
+ *alert = kTlsAlertIllegalParameter;
+ return SECFailure;
+ }
+ self->srtp_cipher_ = advertised[0];
+ return SECSuccess;
+ }
+
+ *alert = kTlsAlertUnsupportedExtension;
+ return SECFailure;
+}
+
+nsresult TransportLayerDtls::ExportKeyingMaterial(const std::string& label,
+ bool use_context,
+ const std::string& context,
+ unsigned char* out,
+ unsigned int outlen) {
+ CheckThread();
+ if (state_ != TS_OPEN) {
+ MOZ_ASSERT(false, "Transport must be open for ExportKeyingMaterial");
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ SECStatus rv = SSL_ExportKeyingMaterial(
+ ssl_fd_.get(), label.c_str(), label.size(), use_context,
+ reinterpret_cast<const unsigned char*>(context.c_str()), context.size(),
+ out, outlen);
+ if (rv != SECSuccess) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't export SSL keying material");
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+SECStatus TransportLayerDtls::AuthCertificateHook(void* arg, PRFileDesc* fd,
+ PRBool checksig,
+ PRBool isServer) {
+ TransportLayerDtls* stream = reinterpret_cast<TransportLayerDtls*>(arg);
+ stream->CheckThread();
+ return stream->AuthCertificateHook(fd, checksig, isServer);
+}
+
+SECStatus TransportLayerDtls::CheckDigest(
+ const DtlsDigest& digest, UniqueCERTCertificate& peer_cert) const {
+ DtlsDigest computed_digest(digest.algorithm_);
+
+ MOZ_MTLOG(ML_DEBUG,
+ LAYER_INFO << "Checking digest, algorithm=" << digest.algorithm_);
+ nsresult res = DtlsIdentity::ComputeFingerprint(peer_cert, &computed_digest);
+ if (NS_FAILED(res)) {
+ MOZ_MTLOG(ML_ERROR, "Could not compute peer fingerprint for digest "
+ << digest.algorithm_);
+ // Go to end
+ PR_SetError(SSL_ERROR_BAD_CERTIFICATE, 0);
+ return SECFailure;
+ }
+
+ if (computed_digest != digest) {
+ MOZ_MTLOG(ML_ERROR, "Digest does not match");
+ PR_SetError(SSL_ERROR_BAD_CERTIFICATE, 0);
+ return SECFailure;
+ }
+
+ return SECSuccess;
+}
+
+SECStatus TransportLayerDtls::AuthCertificateHook(PRFileDesc* fd,
+ PRBool checksig,
+ PRBool isServer) {
+ CheckThread();
+ UniqueCERTCertificate peer_cert(SSL_PeerCertificate(fd));
+
+ // We are not set up to take this being called multiple
+ // times. Change this if we ever add renegotiation.
+ MOZ_ASSERT(!auth_hook_called_);
+ if (auth_hook_called_) {
+ PR_SetError(PR_UNKNOWN_ERROR, 0);
+ return SECFailure;
+ }
+ auth_hook_called_ = true;
+
+ MOZ_ASSERT(verification_mode_ != VERIFY_UNSET);
+
+ switch (verification_mode_) {
+ case VERIFY_UNSET:
+ // Break out to error exit
+ PR_SetError(PR_UNKNOWN_ERROR, 0);
+ break;
+
+ case VERIFY_ALLOW_ALL:
+ cert_ok_ = true;
+ return SECSuccess;
+
+ case VERIFY_DIGEST: {
+ MOZ_ASSERT(!digests_.empty());
+ // Check all the provided digests
+
+ // Checking functions call PR_SetError()
+ SECStatus rv = SECFailure;
+ for (auto digest : digests_) {
+ rv = CheckDigest(digest, peer_cert);
+
+ // Matches a digest, we are good to go
+ if (rv == SECSuccess) {
+ cert_ok_ = true;
+ return SECSuccess;
+ }
+ }
+ } break;
+ default:
+ MOZ_CRASH(); // Can't happen
+ }
+
+ return SECFailure;
+}
+
+void TransportLayerDtls::TimerCallback(nsITimer* timer, void* arg) {
+ TransportLayerDtls* dtls = reinterpret_cast<TransportLayerDtls*>(arg);
+
+ MOZ_MTLOG(ML_DEBUG, "DTLS timer expired");
+
+ dtls->Handshake();
+}
+
+void TransportLayerDtls::RecordTlsTelemetry() {
+ MOZ_ASSERT(state_ == TS_OPEN);
+ SSLChannelInfo info;
+ SECStatus ss = SSL_GetChannelInfo(ssl_fd_.get(), &info, sizeof(info));
+ if (ss != SECSuccess) {
+ MOZ_MTLOG(ML_NOTICE,
+ LAYER_INFO << "RecordTlsTelemetry failed to get channel info");
+ return;
+ }
+
+ uint16_t telemetry_cipher = 0;
+
+ switch (info.cipherSuite) {
+ /* Old DHE ciphers: candidates for removal, see bug 1227519 */
+ case TLS_DHE_RSA_WITH_AES_128_CBC_SHA:
+ telemetry_cipher = 1;
+ break;
+ case TLS_DHE_RSA_WITH_AES_256_CBC_SHA:
+ telemetry_cipher = 2;
+ break;
+ /* Current ciphers */
+ case TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA:
+ telemetry_cipher = 3;
+ break;
+ case TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA:
+ telemetry_cipher = 4;
+ break;
+ case TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA:
+ telemetry_cipher = 5;
+ break;
+ case TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA:
+ telemetry_cipher = 6;
+ break;
+ case TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:
+ telemetry_cipher = 7;
+ break;
+ case TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:
+ telemetry_cipher = 8;
+ break;
+ case TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256:
+ telemetry_cipher = 9;
+ break;
+ case TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:
+ telemetry_cipher = 10;
+ break;
+ /* TLS 1.3 ciphers */
+ case TLS_AES_128_GCM_SHA256:
+ telemetry_cipher = 11;
+ break;
+ case TLS_CHACHA20_POLY1305_SHA256:
+ telemetry_cipher = 12;
+ break;
+ case TLS_AES_256_GCM_SHA384:
+ telemetry_cipher = 13;
+ break;
+ }
+
+ Telemetry::Accumulate(Telemetry::WEBRTC_DTLS_CIPHER, telemetry_cipher);
+
+ uint16_t cipher;
+ nsresult rv = GetSrtpCipher(&cipher);
+
+ if (NS_FAILED(rv)) {
+ MOZ_MTLOG(ML_DEBUG, "No SRTP cipher suite");
+ return;
+ }
+
+ auto cipher_label = mozilla::Telemetry::LABELS_WEBRTC_SRTP_CIPHER::Unknown;
+
+ switch (cipher) {
+ case kDtlsSrtpAes128CmHmacSha1_80:
+ cipher_label = Telemetry::LABELS_WEBRTC_SRTP_CIPHER::Aes128CmHmacSha1_80;
+ break;
+ case kDtlsSrtpAes128CmHmacSha1_32:
+ cipher_label = Telemetry::LABELS_WEBRTC_SRTP_CIPHER::Aes128CmHmacSha1_32;
+ break;
+ case kDtlsSrtpAeadAes128Gcm:
+ cipher_label = Telemetry::LABELS_WEBRTC_SRTP_CIPHER::AeadAes128Gcm;
+ break;
+ case kDtlsSrtpAeadAes256Gcm:
+ cipher_label = Telemetry::LABELS_WEBRTC_SRTP_CIPHER::AeadAes256Gcm;
+ break;
+ }
+
+ Telemetry::AccumulateCategorical(cipher_label);
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/transport/transportlayerdtls.h b/dom/media/webrtc/transport/transportlayerdtls.h
new file mode 100644
index 0000000000..078b266e8b
--- /dev/null
+++ b/dom/media/webrtc/transport/transportlayerdtls.h
@@ -0,0 +1,187 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+#ifndef transportlayerdtls_h__
+#define transportlayerdtls_h__
+
+#include <queue>
+#include <set>
+
+#ifdef XP_MACOSX
+// ensure that Apple Security kit enum goes before "sslproto.h"
+# include <CoreFoundation/CFAvailability.h>
+# include <Security/CipherSuite.h>
+#endif
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/UniquePtr.h"
+#include "nsCOMPtr.h"
+#include "nsITimer.h"
+#include "ScopedNSSTypes.h"
+#include "m_cpp_utils.h"
+#include "dtlsidentity.h"
+#include "transportlayer.h"
+#include "ssl.h"
+#include "sslproto.h"
+
+namespace mozilla {
+
+// RFC 5764 (we don't support the NULL cipher)
+static const uint16_t kDtlsSrtpAes128CmHmacSha1_80 = 0x0001;
+static const uint16_t kDtlsSrtpAes128CmHmacSha1_32 = 0x0002;
+// RFC 7714
+static const uint16_t kDtlsSrtpAeadAes128Gcm = 0x0007;
+static const uint16_t kDtlsSrtpAeadAes256Gcm = 0x0008;
+
+struct Packet;
+
+class TransportLayerNSPRAdapter {
+ public:
+ explicit TransportLayerNSPRAdapter(TransportLayer* output)
+ : output_(output), input_(), enabled_(true) {}
+
+ void PacketReceived(MediaPacket& packet);
+ int32_t Recv(void* buf, int32_t buflen);
+ int32_t Write(const void* buf, int32_t length);
+ void SetEnabled(bool enabled) { enabled_ = enabled; }
+
+ private:
+ DISALLOW_COPY_ASSIGN(TransportLayerNSPRAdapter);
+
+ TransportLayer* output_;
+ std::queue<MediaPacket*> input_;
+ bool enabled_;
+};
+
+class TransportLayerDtls final : public TransportLayer {
+ public:
+ TransportLayerDtls() = default;
+
+ virtual ~TransportLayerDtls();
+
+ enum Role { CLIENT, SERVER };
+ enum Verification { VERIFY_UNSET, VERIFY_ALLOW_ALL, VERIFY_DIGEST };
+
+ // DTLS-specific operations
+ void SetRole(Role role) { role_ = role; }
+ Role role() { return role_; }
+
+ enum class Version : uint16_t {
+ DTLS_1_0 = SSL_LIBRARY_VERSION_DTLS_1_0,
+ DTLS_1_2 = SSL_LIBRARY_VERSION_DTLS_1_2,
+ DTLS_1_3 = SSL_LIBRARY_VERSION_DTLS_1_3
+ };
+ void SetMinMaxVersion(Version min_version, Version max_version);
+
+ void SetIdentity(const RefPtr<DtlsIdentity>& identity) {
+ identity_ = identity;
+ }
+ nsresult SetAlpn(const std::set<std::string>& allowedAlpn,
+ const std::string& alpnDefault);
+ const std::string& GetNegotiatedAlpn() const { return alpn_; }
+
+ nsresult SetVerificationAllowAll();
+
+ nsresult SetVerificationDigest(const DtlsDigest& digest);
+
+ nsresult GetCipherSuite(uint16_t* cipherSuite) const;
+
+ nsresult SetSrtpCiphers(const std::vector<uint16_t>& ciphers);
+ nsresult GetSrtpCipher(uint16_t* cipher) const;
+ static std::vector<uint16_t> GetDefaultSrtpCiphers();
+
+ nsresult ExportKeyingMaterial(const std::string& label, bool use_context,
+ const std::string& context, unsigned char* out,
+ unsigned int outlen);
+
+ // Transport layer overrides.
+ nsresult InitInternal() override;
+ void WasInserted() override;
+ TransportResult SendPacket(MediaPacket& packet) override;
+
+ // Signals
+ void StateChange(TransportLayer* layer, State state);
+ void PacketReceived(TransportLayer* layer, MediaPacket& packet);
+
+ // For testing use only. Returns the fd.
+ PRFileDesc* internal_fd() {
+ CheckThread();
+ return ssl_fd_.get();
+ }
+
+ TRANSPORT_LAYER_ID("dtls")
+
+ protected:
+ void SetState(State state, const char* file, unsigned line) override;
+
+ private:
+ DISALLOW_COPY_ASSIGN(TransportLayerDtls);
+
+ bool Setup();
+ bool SetupCipherSuites(UniquePRFileDesc& ssl_fd);
+ bool SetupAlpn(UniquePRFileDesc& ssl_fd) const;
+ void GetDecryptedPackets();
+ void Handshake();
+
+ bool CheckAlpn();
+
+ static SECStatus GetClientAuthDataHook(void* arg, PRFileDesc* fd,
+ CERTDistNames* caNames,
+ CERTCertificate** pRetCert,
+ SECKEYPrivateKey** pRetKey);
+ static SECStatus AuthCertificateHook(void* arg, PRFileDesc* fd,
+ PRBool checksig, PRBool isServer);
+ SECStatus AuthCertificateHook(PRFileDesc* fd, PRBool checksig,
+ PRBool isServer);
+
+ static void TimerCallback(nsITimer* timer, void* arg);
+
+ SECStatus CheckDigest(const DtlsDigest& digest,
+ UniqueCERTCertificate& cert) const;
+
+ void RecordHandshakeCompletionTelemetry(TransportLayer::State endState);
+ void RecordTlsTelemetry();
+
+ static PRBool WriteSrtpXtn(PRFileDesc* fd, SSLHandshakeType message,
+ uint8_t* data, unsigned int* len,
+ unsigned int max_len, void* arg);
+
+ static SECStatus HandleSrtpXtn(PRFileDesc* fd, SSLHandshakeType message,
+ const uint8_t* data, unsigned int len,
+ SSLAlertDescription* alert, void* arg);
+
+ RefPtr<DtlsIdentity> identity_;
+ // What ALPN identifiers are permitted.
+ std::set<std::string> alpn_allowed_;
+ // What ALPN identifier is used if ALPN is not supported.
+ // The empty string indicates that ALPN is required.
+ std::string alpn_default_;
+ // What ALPN string was negotiated.
+ std::string alpn_;
+ std::vector<uint16_t> enabled_srtp_ciphers_;
+ uint16_t srtp_cipher_ = 0;
+
+ Role role_ = CLIENT;
+ Verification verification_mode_ = VERIFY_UNSET;
+ std::vector<DtlsDigest> digests_;
+
+ Version minVersion_ = Version::DTLS_1_0;
+ Version maxVersion_ = Version::DTLS_1_2;
+
+ // Must delete nspr_io_adapter after ssl_fd_ b/c ssl_fd_ causes an alert
+ // (ssl_fd_ contains an un-owning pointer to nspr_io_adapter_)
+ UniquePtr<TransportLayerNSPRAdapter> nspr_io_adapter_ = nullptr;
+ UniquePRFileDesc ssl_fd_ = nullptr;
+
+ nsCOMPtr<nsITimer> timer_ = nullptr;
+ bool auth_hook_called_ = false;
+ bool cert_ok_ = false;
+};
+
+} // namespace mozilla
+#endif
diff --git a/dom/media/webrtc/transport/transportlayerice.cpp b/dom/media/webrtc/transport/transportlayerice.cpp
new file mode 100644
index 0000000000..03d8131ead
--- /dev/null
+++ b/dom/media/webrtc/transport/transportlayerice.cpp
@@ -0,0 +1,168 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+// Some of this code is cut-and-pasted from nICEr. Copyright is:
+
+/*
+Copyright (c) 2007, Adobe Systems, Incorporated
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+* Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+* Neither the name of Adobe Systems, Network Resonance nor the names of its
+ contributors may be used to endorse or promote products derived from
+ this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+*/
+
+#include <string>
+
+#include "nsComponentManagerUtils.h"
+#include "nsError.h"
+#include "nsNetCID.h"
+
+// nICEr includes
+extern "C" {
+#include "nr_api.h"
+#include "ice_util.h"
+#include "transport_addr.h"
+}
+
+// Local includes
+#include "logging.h"
+#include "nricemediastream.h"
+#include "transportlayerice.h"
+
+namespace mozilla {
+
+#ifdef ERROR
+# undef ERROR
+#endif
+
+MOZ_MTLOG_MODULE("mtransport")
+
+TransportLayerIce::TransportLayerIce() : stream_(nullptr), component_(0) {
+ // setup happens later
+}
+
+TransportLayerIce::~TransportLayerIce() {
+ // No need to do anything here, since we use smart pointers
+}
+
+void TransportLayerIce::SetParameters(RefPtr<NrIceMediaStream> stream,
+ int component) {
+ // Stream could be null in the case of some badly written js that causes
+ // us to be in an ICE restart case, but not have valid streams due to
+ // not calling PeerConnectionImpl::EnsureTransports if
+ // PeerConnectionImpl::SetSignalingState_m thinks the conditions were
+ // not correct. We also solved a case where an incoming answer was
+ // incorrectly beginning an ICE restart when the offer did not indicate one.
+ if (!stream) {
+ MOZ_ASSERT(false);
+ return;
+ }
+
+ stream_ = stream;
+ component_ = component;
+
+ PostSetup();
+}
+
+void TransportLayerIce::PostSetup() {
+ stream_->SignalReady.connect(this, &TransportLayerIce::IceReady);
+ stream_->SignalFailed.connect(this, &TransportLayerIce::IceFailed);
+ stream_->SignalPacketReceived.connect(this,
+ &TransportLayerIce::IcePacketReceived);
+ if (stream_->state() == NrIceMediaStream::ICE_OPEN) {
+ TL_SET_STATE(TS_OPEN);
+ }
+}
+
+TransportResult TransportLayerIce::SendPacket(MediaPacket& packet) {
+ CheckThread();
+ SignalPacketSending(this, packet);
+ nsresult res = stream_->SendPacket(component_, packet.data(), packet.len());
+
+ if (!NS_SUCCEEDED(res)) {
+ return (res == NS_BASE_STREAM_WOULD_BLOCK) ? TE_WOULDBLOCK : TE_ERROR;
+ }
+
+ MOZ_MTLOG(ML_DEBUG,
+ LAYER_INFO << " SendPacket(" << packet.len() << ") succeeded");
+
+ return packet.len();
+}
+
+void TransportLayerIce::IceCandidate(NrIceMediaStream* stream,
+ const std::string&) {
+ // NO-OP for now
+}
+
+void TransportLayerIce::IceReady(NrIceMediaStream* stream) {
+ CheckThread();
+ // only handle the current stream (not the old stream during restart)
+ if (stream != stream_) {
+ return;
+ }
+ MOZ_MTLOG(ML_INFO, LAYER_INFO << "ICE Ready(" << stream->name() << ","
+ << component_ << ")");
+ TL_SET_STATE(TS_OPEN);
+}
+
+void TransportLayerIce::IceFailed(NrIceMediaStream* stream) {
+ CheckThread();
+ // only handle the current stream (not the old stream during restart)
+ if (stream != stream_) {
+ return;
+ }
+ MOZ_MTLOG(ML_INFO, LAYER_INFO << "ICE Failed(" << stream->name() << ","
+ << component_ << ")");
+ TL_SET_STATE(TS_ERROR);
+}
+
+void TransportLayerIce::IcePacketReceived(NrIceMediaStream* stream,
+ int component,
+ const unsigned char* data, int len) {
+ CheckThread();
+ // We get packets for both components, so ignore the ones that aren't
+ // for us.
+ if (component_ != component) return;
+
+ MOZ_MTLOG(ML_DEBUG, LAYER_INFO << "PacketReceived(" << stream->name() << ","
+ << component << "," << len << ")");
+ // Might be useful to allow MediaPacket to borrow a buffer (ie; not take
+ // ownership, but copy it if the MediaPacket is moved). This could be a
+ // footgun though with MediaPackets that end up on the heap.
+ MediaPacket packet;
+ packet.Copy(data, len);
+ packet.Categorize();
+
+ SignalPacketReceived(this, packet);
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/transport/transportlayerice.h b/dom/media/webrtc/transport/transportlayerice.h
new file mode 100644
index 0000000000..1c55b71ce8
--- /dev/null
+++ b/dom/media/webrtc/transport/transportlayerice.h
@@ -0,0 +1,60 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+// This is a wrapper around the nICEr ICE stack
+#ifndef transportlayerice_h__
+#define transportlayerice_h__
+
+#include "sigslot.h"
+
+#include "mozilla/RefPtr.h"
+
+#include "m_cpp_utils.h"
+
+#include "nricemediastream.h"
+#include "transportlayer.h"
+
+// An ICE transport layer -- corresponds to a single ICE
+namespace mozilla {
+
+class TransportLayerIce : public TransportLayer {
+ public:
+ TransportLayerIce();
+
+ virtual ~TransportLayerIce();
+
+ void SetParameters(RefPtr<NrIceMediaStream> stream, int component);
+
+ void ResetOldStream(); // called after successful ice restart
+ void RestoreOldStream(); // called after unsuccessful ice restart
+
+ // Transport layer overrides.
+ TransportResult SendPacket(MediaPacket& packet) override;
+
+ // Slots for ICE
+ void IceCandidate(NrIceMediaStream* stream, const std::string&);
+ void IceReady(NrIceMediaStream* stream);
+ void IceFailed(NrIceMediaStream* stream);
+ void IcePacketReceived(NrIceMediaStream* stream, int component,
+ const unsigned char* data, int len);
+
+ // Useful for capturing encrypted packets
+ sigslot::signal2<TransportLayer*, MediaPacket&> SignalPacketSending;
+
+ TRANSPORT_LAYER_ID("ice")
+
+ private:
+ DISALLOW_COPY_ASSIGN(TransportLayerIce);
+ void PostSetup();
+
+ RefPtr<NrIceMediaStream> stream_;
+ int component_;
+};
+
+} // namespace mozilla
+#endif
diff --git a/dom/media/webrtc/transport/transportlayerlog.cpp b/dom/media/webrtc/transport/transportlayerlog.cpp
new file mode 100644
index 0000000000..d78e722dca
--- /dev/null
+++ b/dom/media/webrtc/transport/transportlayerlog.cpp
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+#include "logging.h"
+#include "transportlayerlog.h"
+
+namespace mozilla {
+
+MOZ_MTLOG_MODULE("mtransport")
+
+void TransportLayerLogging::WasInserted() {
+ if (downward_) {
+ downward_->SignalStateChange.connect(this,
+ &TransportLayerLogging::StateChange);
+ downward_->SignalPacketReceived.connect(
+ this, &TransportLayerLogging::PacketReceived);
+ TL_SET_STATE(downward_->state());
+ }
+}
+
+TransportResult TransportLayerLogging::SendPacket(MediaPacket& packet) {
+ MOZ_MTLOG(ML_DEBUG, LAYER_INFO << "SendPacket(" << packet.len() << ")");
+
+ if (downward_) {
+ return downward_->SendPacket(packet);
+ }
+ return static_cast<TransportResult>(packet.len());
+}
+
+void TransportLayerLogging::StateChange(TransportLayer* layer, State state) {
+ MOZ_MTLOG(ML_DEBUG, LAYER_INFO << "Received StateChange to " << state);
+
+ TL_SET_STATE(state);
+}
+
+void TransportLayerLogging::PacketReceived(TransportLayer* layer,
+ MediaPacket& packet) {
+ MOZ_MTLOG(ML_DEBUG, LAYER_INFO << "PacketReceived(" << packet.len() << ")");
+
+ SignalPacketReceived(this, packet);
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/transport/transportlayerlog.h b/dom/media/webrtc/transport/transportlayerlog.h
new file mode 100644
index 0000000000..4d0b3fd930
--- /dev/null
+++ b/dom/media/webrtc/transport/transportlayerlog.h
@@ -0,0 +1,38 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+#ifndef transportlayerlog_h__
+#define transportlayerlog_h__
+
+#include "m_cpp_utils.h"
+#include "transportlayer.h"
+
+namespace mozilla {
+
+class TransportLayerLogging : public TransportLayer {
+ public:
+ TransportLayerLogging() = default;
+
+ // Overrides for TransportLayer
+ TransportResult SendPacket(MediaPacket& packet) override;
+
+ // Signals (forwarded to upper layer)
+ void StateChange(TransportLayer* layer, State state);
+ void PacketReceived(TransportLayer* layer, MediaPacket& packet);
+
+ TRANSPORT_LAYER_ID("log")
+
+ protected:
+ void WasInserted() override;
+
+ private:
+ DISALLOW_COPY_ASSIGN(TransportLayerLogging);
+};
+
+} // namespace mozilla
+#endif
diff --git a/dom/media/webrtc/transport/transportlayerloopback.cpp b/dom/media/webrtc/transport/transportlayerloopback.cpp
new file mode 100644
index 0000000000..ab2a52c17e
--- /dev/null
+++ b/dom/media/webrtc/transport/transportlayerloopback.cpp
@@ -0,0 +1,119 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+#include "logging.h"
+#include "prlock.h"
+
+#include "nsNetCID.h"
+#include "nsServiceManagerUtils.h"
+#include "nsString.h"
+
+#include "transportlayerloopback.h"
+
+namespace mozilla {
+
+MOZ_MTLOG_MODULE("mtransport")
+
+nsresult TransportLayerLoopback::Init() {
+ nsresult rv;
+ target_ = do_GetService(NS_SOCKETTRANSPORTSERVICE_CONTRACTID, &rv);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ if (!NS_SUCCEEDED(rv)) return rv;
+
+ timer_ = NS_NewTimer(target_);
+ MOZ_ASSERT(timer_);
+ if (!timer_) return NS_ERROR_FAILURE;
+
+ packets_lock_ = PR_NewLock();
+ MOZ_ASSERT(packets_lock_);
+ if (!packets_lock_) return NS_ERROR_FAILURE;
+
+ deliverer_ = new Deliverer(this);
+
+ timer_->InitWithCallback(deliverer_, 100, nsITimer::TYPE_REPEATING_SLACK);
+
+ return NS_OK;
+}
+
+// Connect to the other side
+void TransportLayerLoopback::Connect(TransportLayerLoopback* peer) {
+ peer_ = peer;
+
+ TL_SET_STATE(TS_OPEN);
+}
+
+TransportResult TransportLayerLoopback::SendPacket(MediaPacket& packet) {
+ MOZ_MTLOG(ML_DEBUG, LAYER_INFO << "SendPacket(" << packet.len() << ")");
+
+ if (!peer_) {
+ MOZ_MTLOG(ML_ERROR, "Discarding packet because peer not attached");
+ return TE_ERROR;
+ }
+
+ size_t len = packet.len();
+ nsresult res = peer_->QueuePacket(packet);
+ if (!NS_SUCCEEDED(res)) return TE_ERROR;
+
+ return static_cast<TransportResult>(len);
+}
+
+nsresult TransportLayerLoopback::QueuePacket(MediaPacket& packet) {
+ MOZ_ASSERT(packets_lock_);
+
+ PR_Lock(packets_lock_);
+
+ if (combinePackets_ && !packets_.empty()) {
+ MediaPacket* prevPacket = packets_.front();
+
+ MOZ_MTLOG(ML_DEBUG, LAYER_INFO << " Enqueuing combined packets of length "
+ << prevPacket->len() << " and "
+ << packet.len());
+ auto combined = MakeUnique<uint8_t[]>(prevPacket->len() + packet.len());
+ memcpy(combined.get(), prevPacket->data(), prevPacket->len());
+ memcpy(combined.get() + prevPacket->len(), packet.data(), packet.len());
+ prevPacket->Take(std::move(combined), prevPacket->len() + packet.len());
+ } else {
+ MOZ_MTLOG(ML_DEBUG,
+ LAYER_INFO << " Enqueuing packet of length " << packet.len());
+ packets_.push(new MediaPacket(std::move(packet)));
+ }
+
+ PRStatus r = PR_Unlock(packets_lock_);
+ MOZ_ASSERT(r == PR_SUCCESS);
+ if (r != PR_SUCCESS) return NS_ERROR_FAILURE;
+
+ return NS_OK;
+}
+
+void TransportLayerLoopback::DeliverPackets() {
+ while (!packets_.empty()) {
+ UniquePtr<MediaPacket> packet(packets_.front());
+ packets_.pop();
+
+ MOZ_MTLOG(ML_DEBUG,
+ LAYER_INFO << " Delivering packet of length " << packet->len());
+ SignalPacketReceived(this, *packet);
+ }
+}
+
+NS_IMPL_ISUPPORTS(TransportLayerLoopback::Deliverer, nsITimerCallback, nsINamed)
+
+NS_IMETHODIMP TransportLayerLoopback::Deliverer::Notify(nsITimer* timer) {
+ if (!layer_) return NS_OK;
+
+ layer_->DeliverPackets();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP TransportLayerLoopback::Deliverer::GetName(nsACString& aName) {
+ aName.AssignLiteral("TransportLayerLoopback::Deliverer");
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/transport/transportlayerloopback.h b/dom/media/webrtc/transport/transportlayerloopback.h
new file mode 100644
index 0000000000..6801af8189
--- /dev/null
+++ b/dom/media/webrtc/transport/transportlayerloopback.h
@@ -0,0 +1,109 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+#ifndef transportlayerloopback_h__
+#define transportlayerloopback_h__
+
+#include "prlock.h"
+
+#include <queue>
+
+#include "nsCOMPtr.h"
+#include "nsINamed.h"
+#include "nsITimer.h"
+
+#include "m_cpp_utils.h"
+#include "transportlayer.h"
+
+// A simple loopback transport layer that is used for testing.
+namespace mozilla {
+
+class TransportLayerLoopback : public TransportLayer {
+ public:
+ TransportLayerLoopback()
+ : peer_(nullptr),
+ timer_(nullptr),
+ packets_(),
+ packets_lock_(nullptr),
+ deliverer_(nullptr),
+ combinePackets_(false) {}
+
+ ~TransportLayerLoopback() {
+ while (!packets_.empty()) {
+ MediaPacket* packet = packets_.front();
+ packets_.pop();
+ delete packet;
+ }
+ if (packets_lock_) {
+ PR_DestroyLock(packets_lock_);
+ }
+ timer_->Cancel();
+ deliverer_->Detach();
+ }
+
+ // Init
+ nsresult Init();
+
+ // Connect to the other side
+ void Connect(TransportLayerLoopback* peer);
+
+ // Disconnect
+ void Disconnect() {
+ TransportLayerLoopback* peer = peer_;
+
+ peer_ = nullptr;
+ if (peer) {
+ peer->Disconnect();
+ }
+ }
+
+ void CombinePackets(bool combine) { combinePackets_ = combine; }
+
+ // Overrides for TransportLayer
+ TransportResult SendPacket(MediaPacket& packet) override;
+
+ // Deliver queued packets
+ void DeliverPackets();
+
+ TRANSPORT_LAYER_ID("loopback")
+
+ private:
+ DISALLOW_COPY_ASSIGN(TransportLayerLoopback);
+
+ // A timer to deliver packets if some are available
+ // Fires every 100 ms
+ class Deliverer : public nsITimerCallback, public nsINamed {
+ public:
+ explicit Deliverer(TransportLayerLoopback* layer) : layer_(layer) {}
+ void Detach() { layer_ = nullptr; }
+
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSITIMERCALLBACK
+ NS_DECL_NSINAMED
+
+ private:
+ virtual ~Deliverer() = default;
+
+ DISALLOW_COPY_ASSIGN(Deliverer);
+
+ TransportLayerLoopback* layer_;
+ };
+
+ // Queue a packet for delivery
+ nsresult QueuePacket(MediaPacket& packet);
+
+ TransportLayerLoopback* peer_;
+ nsCOMPtr<nsITimer> timer_;
+ std::queue<MediaPacket*> packets_;
+ PRLock* packets_lock_;
+ RefPtr<Deliverer> deliverer_;
+ bool combinePackets_;
+};
+
+} // namespace mozilla
+#endif
diff --git a/dom/media/webrtc/transport/transportlayersrtp.cpp b/dom/media/webrtc/transport/transportlayersrtp.cpp
new file mode 100644
index 0000000000..25830a2dde
--- /dev/null
+++ b/dom/media/webrtc/transport/transportlayersrtp.cpp
@@ -0,0 +1,222 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+#include "transportlayersrtp.h"
+#include "transportlayerdtls.h"
+
+#include "logging.h"
+#include "nsError.h"
+#include "mozilla/Assertions.h"
+
+namespace mozilla {
+
+MOZ_MTLOG_MODULE("mtransport")
+
+static char kDTLSExporterLabel[] = "EXTRACTOR-dtls_srtp";
+
+TransportLayerSrtp::TransportLayerSrtp(TransportLayerDtls& dtls) {
+ // We need to connect to the dtls layer, not the ice layer, because even
+ // though the packets that DTLS decrypts don't flow through us, we do base our
+ // keying information on the keying information established by the DTLS layer.
+ dtls.SignalStateChange.connect(this, &TransportLayerSrtp::StateChange);
+
+ TL_SET_STATE(dtls.state());
+}
+
+void TransportLayerSrtp::WasInserted() {
+ // Connect to the lower layers
+ if (!Setup()) {
+ TL_SET_STATE(TS_ERROR);
+ }
+}
+
+bool TransportLayerSrtp::Setup() {
+ CheckThread();
+ if (!downward_) {
+ MOZ_MTLOG(ML_ERROR, "SRTP layer with nothing below. This is useless");
+ return false;
+ }
+
+ // downward_ is the TransportLayerIce
+ downward_->SignalPacketReceived.connect(this,
+ &TransportLayerSrtp::PacketReceived);
+
+ return true;
+}
+
+TransportResult TransportLayerSrtp::SendPacket(MediaPacket& packet) {
+ if (state() != TS_OPEN) {
+ return TE_ERROR;
+ }
+
+ if (packet.len() < 4) {
+ MOZ_ASSERT(false);
+ return TE_ERROR;
+ }
+
+ MOZ_ASSERT(packet.capacity() - packet.len() >= SRTP_MAX_EXPANSION);
+
+ int out_len;
+ nsresult res;
+ switch (packet.type()) {
+ case MediaPacket::RTP:
+ res = mSendSrtp->ProtectRtp(packet.data(), packet.len(),
+ packet.capacity(), &out_len);
+ packet.SetType(MediaPacket::SRTP);
+ break;
+ case MediaPacket::RTCP:
+ res = mSendSrtp->ProtectRtcp(packet.data(), packet.len(),
+ packet.capacity(), &out_len);
+ packet.SetType(MediaPacket::SRTCP);
+ break;
+ default:
+ MOZ_CRASH("SRTP layer asked to send packet that is neither RTP or RTCP");
+ }
+
+ if (NS_FAILED(res)) {
+ MOZ_MTLOG(ML_ERROR,
+ "Error protecting "
+ << (packet.type() == MediaPacket::RTP ? "RTP" : "RTCP")
+ << " len=" << packet.len() << "[" << std::hex
+ << packet.data()[0] << " " << packet.data()[1] << " "
+ << packet.data()[2] << " " << packet.data()[3] << "]");
+ return TE_ERROR;
+ }
+
+ size_t unencrypted_len = packet.len();
+ packet.SetLength(out_len);
+
+ TransportResult bytes = downward_->SendPacket(packet);
+ if (bytes == out_len) {
+ // Whole packet was written, but the encrypted length might be different.
+ // Don't confuse the caller.
+ return unencrypted_len;
+ }
+
+ if (bytes == TE_WOULDBLOCK) {
+ return TE_WOULDBLOCK;
+ }
+
+ return TE_ERROR;
+}
+
+void TransportLayerSrtp::StateChange(TransportLayer* layer, State state) {
+ if (state == TS_OPEN && !mSendSrtp) {
+ TransportLayerDtls* dtls = static_cast<TransportLayerDtls*>(layer);
+ MOZ_ASSERT(dtls); // DTLS is mandatory
+
+ uint16_t cipher_suite;
+ nsresult res = dtls->GetSrtpCipher(&cipher_suite);
+ if (NS_FAILED(res)) {
+ MOZ_MTLOG(ML_DEBUG, "DTLS-SRTP disabled");
+ TL_SET_STATE(TS_ERROR);
+ return;
+ }
+
+ unsigned int key_size = SrtpFlow::KeySize(cipher_suite);
+ unsigned int salt_size = SrtpFlow::SaltSize(cipher_suite);
+ unsigned int master_key_size = key_size + salt_size;
+ MOZ_ASSERT(master_key_size <= SRTP_MAX_KEY_LENGTH);
+
+ // SRTP Key Exporter as per RFC 5764 S 4.2
+ unsigned char srtp_block[SRTP_MAX_KEY_LENGTH * 2];
+ res = dtls->ExportKeyingMaterial(kDTLSExporterLabel, false, "", srtp_block,
+ sizeof(srtp_block));
+ if (NS_FAILED(res)) {
+ MOZ_MTLOG(ML_ERROR, "Failed to compute DTLS-SRTP keys. This is an error");
+ TL_SET_STATE(TS_ERROR);
+ return;
+ }
+
+ // Slice and dice as per RFC 5764 S 4.2
+ unsigned char client_write_key[SRTP_MAX_KEY_LENGTH];
+ unsigned char server_write_key[SRTP_MAX_KEY_LENGTH];
+ unsigned int offset = 0;
+ memcpy(client_write_key, srtp_block + offset, key_size);
+ offset += key_size;
+ memcpy(server_write_key, srtp_block + offset, key_size);
+ offset += key_size;
+ memcpy(client_write_key + key_size, srtp_block + offset, salt_size);
+ offset += salt_size;
+ memcpy(server_write_key + key_size, srtp_block + offset, salt_size);
+ MOZ_ASSERT((offset + salt_size) == (2 * master_key_size));
+
+ unsigned char* write_key;
+ unsigned char* read_key;
+
+ if (dtls->role() == TransportLayerDtls::CLIENT) {
+ write_key = client_write_key;
+ read_key = server_write_key;
+ } else {
+ write_key = server_write_key;
+ read_key = client_write_key;
+ }
+
+ MOZ_ASSERT(!mSendSrtp && !mRecvSrtp);
+ mSendSrtp =
+ SrtpFlow::Create(cipher_suite, false, write_key, master_key_size);
+ mRecvSrtp = SrtpFlow::Create(cipher_suite, true, read_key, master_key_size);
+ if (!mSendSrtp || !mRecvSrtp) {
+ MOZ_MTLOG(ML_ERROR, "Couldn't create SRTP flow.");
+ TL_SET_STATE(TS_ERROR);
+ return;
+ }
+
+ MOZ_MTLOG(ML_INFO, "Created SRTP flow!");
+ }
+
+ TL_SET_STATE(state);
+}
+
+void TransportLayerSrtp::PacketReceived(TransportLayer* layer,
+ MediaPacket& packet) {
+ if (state() != TS_OPEN) {
+ return;
+ }
+
+ if (!packet.data()) {
+ // Something ate this, probably the DTLS layer
+ return;
+ }
+
+ if (packet.type() != MediaPacket::SRTP &&
+ packet.type() != MediaPacket::SRTCP) {
+ return;
+ }
+
+ // We want to keep the encrypted packet around for packet dumping
+ packet.CopyDataToEncrypted();
+ int outLen;
+ nsresult res;
+
+ if (packet.type() == MediaPacket::SRTP) {
+ packet.SetType(MediaPacket::RTP);
+ res = mRecvSrtp->UnprotectRtp(packet.data(), packet.len(), packet.len(),
+ &outLen);
+ } else {
+ packet.SetType(MediaPacket::RTCP);
+ res = mRecvSrtp->UnprotectRtcp(packet.data(), packet.len(), packet.len(),
+ &outLen);
+ }
+
+ if (NS_SUCCEEDED(res)) {
+ packet.SetLength(outLen);
+ SignalPacketReceived(this, packet);
+ } else {
+ // TODO: What do we do wrt packet dumping here? Maybe signal an empty
+ // packet? Signal the still-encrypted packet?
+ MOZ_MTLOG(ML_ERROR,
+ "Error unprotecting "
+ << (packet.type() == MediaPacket::RTP ? "RTP" : "RTCP")
+ << " len=" << packet.len() << "[" << std::hex
+ << packet.data()[0] << " " << packet.data()[1] << " "
+ << packet.data()[2] << " " << packet.data()[3] << "]");
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/transport/transportlayersrtp.h b/dom/media/webrtc/transport/transportlayersrtp.h
new file mode 100644
index 0000000000..a5ba035930
--- /dev/null
+++ b/dom/media/webrtc/transport/transportlayersrtp.h
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef transportlayersrtp_h__
+#define transportlayersrtp_h__
+
+#include <string>
+
+#include "transportlayer.h"
+#include "mozilla/RefPtr.h"
+#include "SrtpFlow.h"
+
+namespace mozilla {
+
+class TransportLayerDtls;
+
+class TransportLayerSrtp final : public TransportLayer {
+ public:
+ explicit TransportLayerSrtp(TransportLayerDtls& dtls);
+ virtual ~TransportLayerSrtp() = default;
+
+ // Transport layer overrides.
+ void WasInserted() override;
+ TransportResult SendPacket(MediaPacket& packet) override;
+
+ // Signals
+ void StateChange(TransportLayer* layer, State state);
+ void PacketReceived(TransportLayer* layer, MediaPacket& packet);
+
+ TRANSPORT_LAYER_ID("srtp")
+
+ private:
+ bool Setup();
+ DISALLOW_COPY_ASSIGN(TransportLayerSrtp);
+ RefPtr<SrtpFlow> mSendSrtp;
+ RefPtr<SrtpFlow> mRecvSrtp;
+};
+
+} // namespace mozilla
+#endif
diff --git a/dom/media/webrtc/transportbridge/MediaPipeline.cpp b/dom/media/webrtc/transportbridge/MediaPipeline.cpp
new file mode 100644
index 0000000000..ec861f69bd
--- /dev/null
+++ b/dom/media/webrtc/transportbridge/MediaPipeline.cpp
@@ -0,0 +1,1655 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+#include "MediaPipeline.h"
+
+#include <inttypes.h>
+#include <math.h>
+#include <sstream>
+
+#include "AudioSegment.h"
+#include "AudioConverter.h"
+#include "DOMMediaStream.h"
+#include "ImageContainer.h"
+#include "ImageTypes.h"
+#include "MediaEngine.h"
+#include "MediaSegment.h"
+#include "MediaTrackGraphImpl.h"
+#include "MediaTrackListener.h"
+#include "MediaStreamTrack.h"
+#include "RtpLogger.h"
+#include "VideoFrameConverter.h"
+#include "VideoSegment.h"
+#include "VideoStreamTrack.h"
+#include "VideoUtils.h"
+#include "mozilla/Logging.h"
+#include "mozilla/NullPrincipal.h"
+#include "mozilla/PeerIdentity.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/SharedThreadPool.h"
+#include "mozilla/Sprintf.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/TaskQueue.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/UniquePtrExtensions.h"
+#include "mozilla/dom/RTCStatsReportBinding.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/gfx/Point.h"
+#include "mozilla/gfx/Types.h"
+#include "nsError.h"
+#include "nsThreadUtils.h"
+#include "transport/runnable_utils.h"
+#include "jsapi/MediaTransportHandler.h"
+#include "jsapi/PeerConnectionImpl.h"
+#include "Tracing.h"
+#include "libwebrtcglue/WebrtcImageBuffer.h"
+#include "libwebrtcglue/MediaConduitInterface.h"
+#include "common_video/include/video_frame_buffer.h"
+#include "modules/rtp_rtcp/include/rtp_rtcp.h"
+#include "modules/rtp_rtcp/include/rtp_header_extension_map.h"
+#include "modules/rtp_rtcp/source/rtp_packet_received.h"
+
+// Max size given stereo is 480*2*2 = 1920 (10ms of 16-bits stereo audio at
+// 48KHz)
+#define AUDIO_SAMPLE_BUFFER_MAX_BYTES (480 * 2 * 2)
+static_assert((WEBRTC_MAX_SAMPLE_RATE / 100) * sizeof(uint16_t) * 2 <=
+ AUDIO_SAMPLE_BUFFER_MAX_BYTES,
+ "AUDIO_SAMPLE_BUFFER_MAX_BYTES is not large enough");
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using namespace mozilla::gfx;
+using namespace mozilla::layers;
+
+mozilla::LazyLogModule gMediaPipelineLog("MediaPipeline");
+
+namespace mozilla {
+
+// An async inserter for audio data, to avoid running audio codec encoders
+// on the MTG/input audio thread. Basically just bounces all the audio
+// data to a single audio processing/input queue. We could if we wanted to
+// use multiple threads and a TaskQueue.
+class AudioProxyThread {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AudioProxyThread)
+
+ explicit AudioProxyThread(RefPtr<AudioSessionConduit> aConduit)
+ : mConduit(std::move(aConduit)),
+ mTaskQueue(TaskQueue::Create(
+ GetMediaThreadPool(MediaThreadType::WEBRTC_WORKER), "AudioProxy")),
+ mAudioConverter(nullptr) {
+ MOZ_ASSERT(mConduit);
+ MOZ_COUNT_CTOR(AudioProxyThread);
+ }
+
+ // This function is the identity if aInputRate is supported.
+ // Else, it returns a rate that is supported, that ensure no loss in audio
+ // quality: the sampling rate returned is always greater to the inputed
+ // sampling-rate, if they differ..
+ uint32_t AppropriateSendingRateForInputRate(uint32_t aInputRate) {
+ AudioSessionConduit* conduit =
+ static_cast<AudioSessionConduit*>(mConduit.get());
+ if (conduit->IsSamplingFreqSupported(aInputRate)) {
+ return aInputRate;
+ }
+ if (aInputRate < 16000) {
+ return 16000;
+ }
+ if (aInputRate < 32000) {
+ return 32000;
+ }
+ if (aInputRate < 44100) {
+ return 44100;
+ }
+ return 48000;
+ }
+
+ // From an arbitrary AudioChunk at sampling-rate aRate, process the audio into
+ // something the conduit can work with (or send silence if the track is not
+ // enabled), and send the audio in 10ms chunks to the conduit.
+ void InternalProcessAudioChunk(TrackRate aRate, const AudioChunk& aChunk,
+ bool aEnabled) {
+ MOZ_ASSERT(mTaskQueue->IsCurrentThreadIn());
+
+ // Convert to interleaved 16-bits integer audio, with a maximum of two
+ // channels (since the WebRTC.org code below makes the assumption that the
+ // input audio is either mono or stereo), with a sample-rate rate that is
+ // 16, 32, 44.1, or 48kHz.
+ uint32_t outputChannels = aChunk.ChannelCount() == 1 ? 1 : 2;
+ int32_t transmissionRate = AppropriateSendingRateForInputRate(aRate);
+
+ // We take advantage of the fact that the common case (microphone directly
+ // to PeerConnection, that is, a normal call), the samples are already
+ // 16-bits mono, so the representation in interleaved and planar is the
+ // same, and we can just use that.
+ if (aEnabled && outputChannels == 1 &&
+ aChunk.mBufferFormat == AUDIO_FORMAT_S16 && transmissionRate == aRate) {
+ const int16_t* samples = aChunk.ChannelData<int16_t>().Elements()[0];
+ PacketizeAndSend(samples, transmissionRate, outputChannels,
+ aChunk.mDuration);
+ return;
+ }
+
+ uint32_t sampleCount = aChunk.mDuration * outputChannels;
+ if (mInterleavedAudio.Length() < sampleCount) {
+ mInterleavedAudio.SetLength(sampleCount);
+ }
+
+ if (!aEnabled || aChunk.mBufferFormat == AUDIO_FORMAT_SILENCE) {
+ PodZero(mInterleavedAudio.Elements(), sampleCount);
+ } else if (aChunk.mBufferFormat == AUDIO_FORMAT_FLOAT32) {
+ DownmixAndInterleave(aChunk.ChannelData<float>(), aChunk.mDuration,
+ aChunk.mVolume, outputChannels,
+ mInterleavedAudio.Elements());
+ } else if (aChunk.mBufferFormat == AUDIO_FORMAT_S16) {
+ DownmixAndInterleave(aChunk.ChannelData<int16_t>(), aChunk.mDuration,
+ aChunk.mVolume, outputChannels,
+ mInterleavedAudio.Elements());
+ }
+ int16_t* inputAudio = mInterleavedAudio.Elements();
+ size_t inputAudioFrameCount = aChunk.mDuration;
+
+ AudioConfig inputConfig(AudioConfig::ChannelLayout(outputChannels), aRate,
+ AudioConfig::FORMAT_S16);
+ AudioConfig outputConfig(AudioConfig::ChannelLayout(outputChannels),
+ transmissionRate, AudioConfig::FORMAT_S16);
+ // Resample to an acceptable sample-rate for the sending side
+ if (!mAudioConverter || mAudioConverter->InputConfig() != inputConfig ||
+ mAudioConverter->OutputConfig() != outputConfig) {
+ mAudioConverter = MakeUnique<AudioConverter>(inputConfig, outputConfig);
+ }
+
+ int16_t* processedAudio = nullptr;
+ size_t framesProcessed =
+ mAudioConverter->Process(inputAudio, inputAudioFrameCount);
+
+ if (framesProcessed == 0) {
+ // In place conversion not possible, use a buffer.
+ framesProcessed = mAudioConverter->Process(mOutputAudio, inputAudio,
+ inputAudioFrameCount);
+ processedAudio = mOutputAudio.Data();
+ } else {
+ processedAudio = inputAudio;
+ }
+
+ PacketizeAndSend(processedAudio, transmissionRate, outputChannels,
+ framesProcessed);
+ }
+
+ // This packetizes aAudioData in 10ms chunks and sends it.
+ // aAudioData is interleaved audio data at a rate and with a channel count
+ // that is appropriate to send with the conduit.
+ void PacketizeAndSend(const int16_t* aAudioData, uint32_t aRate,
+ uint32_t aChannels, uint32_t aFrameCount) {
+ MOZ_ASSERT(AppropriateSendingRateForInputRate(aRate) == aRate);
+ MOZ_ASSERT(aChannels == 1 || aChannels == 2);
+ MOZ_ASSERT(aAudioData);
+
+ uint32_t audio_10ms = aRate / 100;
+
+ if (!mPacketizer || mPacketizer->mPacketSize != audio_10ms ||
+ mPacketizer->mChannels != aChannels) {
+ // It's the right thing to drop the bit of audio still in the packetizer:
+ // we don't want to send to the conduit audio that has two different
+ // rates while telling it that it has a constante rate.
+ mPacketizer =
+ MakeUnique<AudioPacketizer<int16_t, int16_t>>(audio_10ms, aChannels);
+ mPacket = MakeUnique<int16_t[]>(audio_10ms * aChannels);
+ }
+
+ mPacketizer->Input(aAudioData, aFrameCount);
+
+ while (mPacketizer->PacketsAvailable()) {
+ mPacketizer->Output(mPacket.get());
+ auto frame = std::make_unique<webrtc::AudioFrame>();
+ // UpdateFrame makes a copy of the audio data.
+ frame->UpdateFrame(frame->timestamp_, mPacket.get(),
+ mPacketizer->mPacketSize, aRate, frame->speech_type_,
+ frame->vad_activity_, mPacketizer->mChannels);
+ mConduit->SendAudioFrame(std::move(frame));
+ }
+ }
+
+ void QueueAudioChunk(TrackRate aRate, const AudioChunk& aChunk,
+ bool aEnabled) {
+ RefPtr<AudioProxyThread> self = this;
+ nsresult rv = mTaskQueue->Dispatch(NS_NewRunnableFunction(
+ "AudioProxyThread::QueueAudioChunk", [self, aRate, aChunk, aEnabled]() {
+ self->InternalProcessAudioChunk(aRate, aChunk, aEnabled);
+ }));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ }
+
+ protected:
+ virtual ~AudioProxyThread() { MOZ_COUNT_DTOR(AudioProxyThread); }
+
+ const RefPtr<AudioSessionConduit> mConduit;
+ const RefPtr<TaskQueue> mTaskQueue;
+ // Only accessed on mTaskQueue
+ UniquePtr<AudioPacketizer<int16_t, int16_t>> mPacketizer;
+ // A buffer to hold a single packet of audio.
+ UniquePtr<int16_t[]> mPacket;
+ nsTArray<int16_t> mInterleavedAudio;
+ AlignedShortBuffer mOutputAudio;
+ UniquePtr<AudioConverter> mAudioConverter;
+};
+
+#define INIT_MIRROR(name, val) \
+ name(AbstractThread::MainThread(), val, "MediaPipeline::" #name " (Mirror)")
+
+MediaPipeline::MediaPipeline(const std::string& aPc,
+ RefPtr<MediaTransportHandler> aTransportHandler,
+ DirectionType aDirection,
+ RefPtr<AbstractThread> aCallThread,
+ RefPtr<nsISerialEventTarget> aStsThread,
+ RefPtr<MediaSessionConduit> aConduit)
+ : mConduit(std::move(aConduit)),
+ mDirection(aDirection),
+ mCallThread(std::move(aCallThread)),
+ mStsThread(std::move(aStsThread)),
+ INIT_MIRROR(mActive, false),
+ mLevel(0),
+ mTransportHandler(std::move(aTransportHandler)),
+ mRtpPacketsSent(0),
+ mRtcpPacketsSent(0),
+ mRtpPacketsReceived(0),
+ mRtcpPacketsReceived(0),
+ mRtpBytesSent(0),
+ mRtpBytesReceived(0),
+ mPc(aPc),
+ mFilter(),
+ mRtpHeaderExtensionMap(new webrtc::RtpHeaderExtensionMap()),
+ mPacketDumper(PacketDumper::GetPacketDumper(mPc)) {}
+
+#undef INIT_MIRROR
+
+MediaPipeline::~MediaPipeline() {
+ MOZ_LOG(gMediaPipelineLog, LogLevel::Info,
+ ("Destroying MediaPipeline: %s", mDescription.c_str()));
+}
+
+void MediaPipeline::Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mActive.DisconnectIfConnected();
+ RUN_ON_THREAD(mStsThread,
+ WrapRunnable(RefPtr<MediaPipeline>(this),
+ &MediaPipeline::DetachTransport_s),
+ NS_DISPATCH_NORMAL);
+}
+
+void MediaPipeline::DetachTransport_s() {
+ ASSERT_ON_THREAD(mStsThread);
+
+ MOZ_LOG(gMediaPipelineLog, LogLevel::Info,
+ ("%s in %s", mDescription.c_str(), __FUNCTION__));
+
+ disconnect_all();
+ mRtpState = TransportLayer::TS_NONE;
+ mRtcpState = TransportLayer::TS_NONE;
+ mTransportId.clear();
+ mConduit->SetTransportActive(false);
+ mRtpSendEventListener.DisconnectIfExists();
+ mSenderRtcpSendEventListener.DisconnectIfExists();
+ mReceiverRtcpSendEventListener.DisconnectIfExists();
+}
+
+void MediaPipeline::UpdateTransport_m(
+ const std::string& aTransportId, UniquePtr<MediaPipelineFilter>&& aFilter) {
+ mStsThread->Dispatch(NS_NewRunnableFunction(
+ __func__, [aTransportId, filter = std::move(aFilter),
+ self = RefPtr<MediaPipeline>(this)]() mutable {
+ self->UpdateTransport_s(aTransportId, std::move(filter));
+ }));
+}
+
+void MediaPipeline::UpdateTransport_s(
+ const std::string& aTransportId, UniquePtr<MediaPipelineFilter>&& aFilter) {
+ ASSERT_ON_THREAD(mStsThread);
+ if (!mSignalsConnected) {
+ mTransportHandler->SignalStateChange.connect(
+ this, &MediaPipeline::RtpStateChange);
+ mTransportHandler->SignalRtcpStateChange.connect(
+ this, &MediaPipeline::RtcpStateChange);
+ mTransportHandler->SignalEncryptedSending.connect(
+ this, &MediaPipeline::EncryptedPacketSending);
+ mTransportHandler->SignalPacketReceived.connect(
+ this, &MediaPipeline::PacketReceived);
+ mTransportHandler->SignalAlpnNegotiated.connect(
+ this, &MediaPipeline::AlpnNegotiated);
+ mSignalsConnected = true;
+ }
+
+ if (aTransportId != mTransportId) {
+ mTransportId = aTransportId;
+ mRtpState = mTransportHandler->GetState(mTransportId, false);
+ mRtcpState = mTransportHandler->GetState(mTransportId, true);
+ CheckTransportStates();
+ }
+
+ if (mFilter) {
+ for (const auto& extension : mFilter->GetExtmap()) {
+ mRtpHeaderExtensionMap->Deregister(extension.uri);
+ }
+ }
+ if (mFilter && aFilter) {
+ // Use the new filter, but don't forget any remote SSRCs that we've learned
+ // by receiving traffic.
+ mFilter->Update(*aFilter);
+ } else {
+ mFilter = std::move(aFilter);
+ }
+ if (mFilter) {
+ for (const auto& extension : mFilter->GetExtmap()) {
+ mRtpHeaderExtensionMap->RegisterByUri(extension.id, extension.uri);
+ }
+ }
+}
+
+void MediaPipeline::GetContributingSourceStats(
+ const nsString& aInboundRtpStreamId,
+ FallibleTArray<dom::RTCRTPContributingSourceStats>& aArr) const {
+ ASSERT_ON_THREAD(mStsThread);
+ // Get the expiry from now
+ DOMHighResTimeStamp expiry =
+ RtpCSRCStats::GetExpiryFromTime(GetTimestampMaker().GetNow().ToDom());
+ for (auto info : mCsrcStats) {
+ if (!info.second.Expired(expiry)) {
+ RTCRTPContributingSourceStats stats;
+ info.second.GetWebidlInstance(stats, aInboundRtpStreamId);
+ if (!aArr.AppendElement(stats, fallible)) {
+ mozalloc_handle_oom(0);
+ }
+ }
+ }
+}
+
+void MediaPipeline::RtpStateChange(const std::string& aTransportId,
+ TransportLayer::State aState) {
+ if (mTransportId != aTransportId) {
+ return;
+ }
+ mRtpState = aState;
+ CheckTransportStates();
+}
+
+void MediaPipeline::RtcpStateChange(const std::string& aTransportId,
+ TransportLayer::State aState) {
+ if (mTransportId != aTransportId) {
+ return;
+ }
+ mRtcpState = aState;
+ CheckTransportStates();
+}
+
+void MediaPipeline::CheckTransportStates() {
+ ASSERT_ON_THREAD(mStsThread);
+
+ if (mRtpState == TransportLayer::TS_CLOSED ||
+ mRtpState == TransportLayer::TS_ERROR ||
+ mRtcpState == TransportLayer::TS_CLOSED ||
+ mRtcpState == TransportLayer::TS_ERROR) {
+ MOZ_LOG(gMediaPipelineLog, LogLevel::Warning,
+ ("RTP Transport failed for pipeline %p flow %s", this,
+ mDescription.c_str()));
+
+ NS_WARNING(
+ "MediaPipeline Transport failed. This is not properly cleaned up yet");
+ // TODO(ekr@rtfm.com): SECURITY: Figure out how to clean up if the
+ // connection was good and now it is bad.
+ // TODO(ekr@rtfm.com): Report up so that the PC knows we
+ // have experienced an error.
+ mConduit->SetTransportActive(false);
+ mRtpSendEventListener.DisconnectIfExists();
+ mSenderRtcpSendEventListener.DisconnectIfExists();
+ mReceiverRtcpSendEventListener.DisconnectIfExists();
+ return;
+ }
+
+ if (mRtpState == TransportLayer::TS_OPEN) {
+ MOZ_LOG(gMediaPipelineLog, LogLevel::Info,
+ ("RTP Transport ready for pipeline %p flow %s", this,
+ mDescription.c_str()));
+ }
+
+ if (mRtcpState == TransportLayer::TS_OPEN) {
+ MOZ_LOG(gMediaPipelineLog, LogLevel::Info,
+ ("RTCP Transport ready for pipeline %p flow %s", this,
+ mDescription.c_str()));
+ }
+
+ if (mRtpState == TransportLayer::TS_OPEN && mRtcpState == mRtpState) {
+ if (mDirection == DirectionType::TRANSMIT) {
+ mConduit->ConnectSenderRtcpEvent(mSenderRtcpReceiveEvent);
+ mRtpSendEventListener = mConduit->SenderRtpSendEvent().Connect(
+ mStsThread, this, &MediaPipeline::SendPacket);
+ mSenderRtcpSendEventListener = mConduit->SenderRtcpSendEvent().Connect(
+ mStsThread, this, &MediaPipeline::SendPacket);
+ } else {
+ mConduit->ConnectReceiverRtcpEvent(mReceiverRtcpReceiveEvent);
+ mConduit->ConnectReceiverRtpEvent(mRtpReceiveEvent);
+ mReceiverRtcpSendEventListener =
+ mConduit->ReceiverRtcpSendEvent().Connect(mStsThread, this,
+ &MediaPipeline::SendPacket);
+ }
+ mConduit->SetTransportActive(true);
+ TransportReady_s();
+ }
+}
+
+void MediaPipeline::SendPacket(MediaPacket&& aPacket) {
+ ASSERT_ON_THREAD(mStsThread);
+
+ const bool isRtp = aPacket.type() == MediaPacket::RTP;
+
+ if (isRtp && mRtpState != TransportLayer::TS_OPEN) {
+ return;
+ }
+
+ if (!isRtp && mRtcpState != TransportLayer::TS_OPEN) {
+ return;
+ }
+
+ aPacket.sdp_level() = Some(Level());
+
+ if (RtpLogger::IsPacketLoggingOn()) {
+ RtpLogger::LogPacket(aPacket, false, mDescription);
+ }
+
+ if (isRtp) {
+ mPacketDumper->Dump(Level(), dom::mozPacketDumpType::Rtp, true,
+ aPacket.data(), aPacket.len());
+ IncrementRtpPacketsSent(aPacket);
+ } else {
+ mPacketDumper->Dump(Level(), dom::mozPacketDumpType::Rtcp, true,
+ aPacket.data(), aPacket.len());
+ IncrementRtcpPacketsSent();
+ }
+
+ MOZ_LOG(
+ gMediaPipelineLog, LogLevel::Debug,
+ ("%s sending %s packet", mDescription.c_str(), (isRtp ? "RTP" : "RTCP")));
+
+ mTransportHandler->SendPacket(mTransportId, std::move(aPacket));
+}
+
+void MediaPipeline::IncrementRtpPacketsSent(const MediaPacket& aPacket) {
+ ASSERT_ON_THREAD(mStsThread);
+ ++mRtpPacketsSent;
+ mRtpBytesSent += aPacket.len();
+
+ if (!(mRtpPacketsSent % 100)) {
+ MOZ_LOG(gMediaPipelineLog, LogLevel::Info,
+ ("RTP sent packet count for %s Pipeline %p: %u (%" PRId64 " bytes)",
+ mDescription.c_str(), this, mRtpPacketsSent, mRtpBytesSent));
+ }
+}
+
+void MediaPipeline::IncrementRtcpPacketsSent() {
+ ASSERT_ON_THREAD(mStsThread);
+ ++mRtcpPacketsSent;
+ if (!(mRtcpPacketsSent % 100)) {
+ MOZ_LOG(gMediaPipelineLog, LogLevel::Info,
+ ("RTCP sent packet count for %s Pipeline %p: %u",
+ mDescription.c_str(), this, mRtcpPacketsSent));
+ }
+}
+
+void MediaPipeline::IncrementRtpPacketsReceived(int32_t aBytes) {
+ ASSERT_ON_THREAD(mStsThread);
+ ++mRtpPacketsReceived;
+ mRtpBytesReceived += aBytes;
+ if (!(mRtpPacketsReceived % 100)) {
+ MOZ_LOG(
+ gMediaPipelineLog, LogLevel::Info,
+ ("RTP received packet count for %s Pipeline %p: %u (%" PRId64 " bytes)",
+ mDescription.c_str(), this, mRtpPacketsReceived, mRtpBytesReceived));
+ }
+}
+
+void MediaPipeline::IncrementRtcpPacketsReceived() {
+ ASSERT_ON_THREAD(mStsThread);
+ ++mRtcpPacketsReceived;
+ if (!(mRtcpPacketsReceived % 100)) {
+ MOZ_LOG(gMediaPipelineLog, LogLevel::Info,
+ ("RTCP received packet count for %s Pipeline %p: %u",
+ mDescription.c_str(), this, mRtcpPacketsReceived));
+ }
+}
+
+void MediaPipeline::RtpPacketReceived(const MediaPacket& packet) {
+ ASSERT_ON_THREAD(mStsThread);
+
+ if (mDirection == DirectionType::TRANSMIT) {
+ return;
+ }
+
+ if (!packet.len()) {
+ return;
+ }
+
+ webrtc::RTPHeader header;
+ rtc::CopyOnWriteBuffer packet_buffer(packet.data(), packet.len());
+ webrtc::RtpPacketReceived parsedPacket(mRtpHeaderExtensionMap.get());
+ if (!parsedPacket.Parse(packet_buffer)) {
+ return;
+ }
+ parsedPacket.GetHeader(&header);
+
+ if (mFilter && !mFilter->Filter(header)) {
+ return;
+ }
+
+ auto now = GetTimestampMaker().GetNow();
+ parsedPacket.set_arrival_time(now.ToRealtime());
+ if (IsVideo()) {
+ parsedPacket.set_payload_type_frequency(webrtc::kVideoPayloadTypeFrequency);
+ }
+
+ // Remove expired RtpCSRCStats
+ if (!mCsrcStats.empty()) {
+ auto expiry = RtpCSRCStats::GetExpiryFromTime(now.ToDom());
+ for (auto p = mCsrcStats.begin(); p != mCsrcStats.end();) {
+ if (p->second.Expired(expiry)) {
+ p = mCsrcStats.erase(p);
+ continue;
+ }
+ p++;
+ }
+ }
+
+ // Add new RtpCSRCStats
+ if (header.numCSRCs) {
+ for (auto i = 0; i < header.numCSRCs; i++) {
+ auto csrcInfo = mCsrcStats.find(header.arrOfCSRCs[i]);
+ if (csrcInfo == mCsrcStats.end()) {
+ mCsrcStats.insert(
+ std::make_pair(header.arrOfCSRCs[i],
+ RtpCSRCStats(header.arrOfCSRCs[i], now.ToDom())));
+ } else {
+ csrcInfo->second.SetTimestamp(now.ToDom());
+ }
+ }
+ }
+
+ MOZ_LOG(gMediaPipelineLog, LogLevel::Debug,
+ ("%s received RTP packet.", mDescription.c_str()));
+ IncrementRtpPacketsReceived(packet.len());
+
+ RtpLogger::LogPacket(packet, true, mDescription);
+
+ // Might be nice to pass ownership of the buffer in this case, but it is a
+ // small optimization in a rare case.
+ mPacketDumper->Dump(mLevel, dom::mozPacketDumpType::Srtp, false,
+ packet.encrypted_data(), packet.encrypted_len());
+
+ mPacketDumper->Dump(mLevel, dom::mozPacketDumpType::Rtp, false, packet.data(),
+ packet.len());
+
+ mRtpReceiveEvent.Notify(std::move(parsedPacket), header);
+}
+
+void MediaPipeline::RtcpPacketReceived(const MediaPacket& packet) {
+ ASSERT_ON_THREAD(mStsThread);
+
+ if (!packet.len()) {
+ return;
+ }
+
+ // We do not filter RTCP. This is because a compound RTCP packet can contain
+ // any collection of RTCP packets, and webrtc.org already knows how to filter
+ // out what it is interested in, and what it is not. Maybe someday we should
+ // have a TransportLayer that breaks up compound RTCP so we can filter them
+ // individually, but I doubt that will matter much.
+
+ MOZ_LOG(gMediaPipelineLog, LogLevel::Debug,
+ ("%s received RTCP packet.", mDescription.c_str()));
+ IncrementRtcpPacketsReceived();
+
+ RtpLogger::LogPacket(packet, true, mDescription);
+
+ // Might be nice to pass ownership of the buffer in this case, but it is a
+ // small optimization in a rare case.
+ mPacketDumper->Dump(mLevel, dom::mozPacketDumpType::Srtcp, false,
+ packet.encrypted_data(), packet.encrypted_len());
+
+ mPacketDumper->Dump(mLevel, dom::mozPacketDumpType::Rtcp, false,
+ packet.data(), packet.len());
+
+ if (StaticPrefs::media_webrtc_net_force_disable_rtcp_reception()) {
+ MOZ_LOG(gMediaPipelineLog, LogLevel::Debug,
+ ("%s RTCP packet forced to be dropped", mDescription.c_str()));
+ return;
+ }
+
+ if (mDirection == DirectionType::TRANSMIT) {
+ mSenderRtcpReceiveEvent.Notify(packet.Clone());
+ } else {
+ mReceiverRtcpReceiveEvent.Notify(packet.Clone());
+ }
+}
+
+void MediaPipeline::PacketReceived(const std::string& aTransportId,
+ const MediaPacket& packet) {
+ ASSERT_ON_THREAD(mStsThread);
+
+ if (mTransportId != aTransportId) {
+ return;
+ }
+
+ MOZ_ASSERT(mRtpState == TransportLayer::TS_OPEN);
+ MOZ_ASSERT(mRtcpState == mRtpState);
+
+ switch (packet.type()) {
+ case MediaPacket::RTP:
+ RtpPacketReceived(packet);
+ break;
+ case MediaPacket::RTCP:
+ RtcpPacketReceived(packet);
+ break;
+ default:;
+ }
+}
+
+void MediaPipeline::AlpnNegotiated(const std::string& aAlpn,
+ bool aPrivacyRequested) {
+ ASSERT_ON_THREAD(mStsThread);
+
+ if (aPrivacyRequested && Direction() == DirectionType::RECEIVE) {
+ // This will force the receive pipelines to drop data until they have
+ // received a private PrincipalHandle from RTCRtpReceiver (which takes a
+ // detour via main thread).
+ static_cast<MediaPipelineReceive*>(this)->OnPrivacyRequested_s();
+ }
+}
+
+void MediaPipeline::EncryptedPacketSending(const std::string& aTransportId,
+ const MediaPacket& aPacket) {
+ ASSERT_ON_THREAD(mStsThread);
+
+ if (mTransportId == aTransportId) {
+ dom::mozPacketDumpType type;
+ if (aPacket.type() == MediaPacket::SRTP) {
+ type = dom::mozPacketDumpType::Srtp;
+ } else if (aPacket.type() == MediaPacket::SRTCP) {
+ type = dom::mozPacketDumpType::Srtcp;
+ } else if (aPacket.type() == MediaPacket::DTLS) {
+ // TODO(bug 1497936): Implement packet dump for DTLS
+ return;
+ } else {
+ MOZ_ASSERT(false);
+ return;
+ }
+ mPacketDumper->Dump(Level(), type, true, aPacket.data(), aPacket.len());
+ }
+}
+
+class MediaPipelineTransmit::PipelineListener
+ : public DirectMediaTrackListener {
+ friend class MediaPipelineTransmit;
+
+ public:
+ explicit PipelineListener(RefPtr<MediaSessionConduit> aConduit)
+ : mConduit(std::move(aConduit)),
+ mActive(false),
+ mEnabled(false),
+ mDirectConnect(false) {}
+
+ ~PipelineListener() {
+ if (mConverter) {
+ mConverter->Shutdown();
+ }
+ }
+
+ void SetActive(bool aActive) {
+ mActive = aActive;
+ if (mConverter) {
+ mConverter->SetActive(aActive);
+ }
+ }
+ void SetEnabled(bool aEnabled) { mEnabled = aEnabled; }
+
+ // These are needed since nested classes don't have access to any particular
+ // instance of the parent
+ void SetAudioProxy(RefPtr<AudioProxyThread> aProxy) {
+ mAudioProcessing = std::move(aProxy);
+ }
+
+ void SetVideoFrameConverter(RefPtr<VideoFrameConverter> aConverter) {
+ mConverter = std::move(aConverter);
+ }
+
+ void OnVideoFrameConverted(webrtc::VideoFrame aVideoFrame) {
+ MOZ_RELEASE_ASSERT(mConduit->type() == MediaSessionConduit::VIDEO);
+ static_cast<VideoSessionConduit*>(mConduit.get())
+ ->SendVideoFrame(std::move(aVideoFrame));
+ }
+
+ // Implement MediaTrackListener
+ void NotifyQueuedChanges(MediaTrackGraph* aGraph, TrackTime aOffset,
+ const MediaSegment& aQueuedMedia) override;
+ void NotifyEnabledStateChanged(MediaTrackGraph* aGraph,
+ bool aEnabled) override;
+
+ // Implement DirectMediaTrackListener
+ void NotifyRealtimeTrackData(MediaTrackGraph* aGraph, TrackTime aOffset,
+ const MediaSegment& aMedia) override;
+ void NotifyDirectListenerInstalled(InstallationResult aResult) override;
+ void NotifyDirectListenerUninstalled() override;
+
+ private:
+ void NewData(const MediaSegment& aMedia, TrackRate aRate = 0);
+
+ const RefPtr<MediaSessionConduit> mConduit;
+ RefPtr<AudioProxyThread> mAudioProcessing;
+ RefPtr<VideoFrameConverter> mConverter;
+
+ // active is true if there is a transport to send on
+ mozilla::Atomic<bool> mActive;
+ // enabled is true if the media access control permits sending
+ // actual content; when false you get black/silence
+ mozilla::Atomic<bool> mEnabled;
+
+ // Written and read on the MediaTrackGraph thread
+ bool mDirectConnect;
+};
+
+MediaPipelineTransmit::MediaPipelineTransmit(
+ const std::string& aPc, RefPtr<MediaTransportHandler> aTransportHandler,
+ RefPtr<AbstractThread> aCallThread, RefPtr<nsISerialEventTarget> aStsThread,
+ bool aIsVideo, RefPtr<MediaSessionConduit> aConduit)
+ : MediaPipeline(aPc, std::move(aTransportHandler), DirectionType::TRANSMIT,
+ std::move(aCallThread), std::move(aStsThread),
+ std::move(aConduit)),
+ mWatchManager(this, AbstractThread::MainThread()),
+ mIsVideo(aIsVideo),
+ mListener(new PipelineListener(mConduit)),
+ mDomTrack(nullptr, "MediaPipelineTransmit::mDomTrack"),
+ mSendTrackOverride(nullptr, "MediaPipelineTransmit::mSendTrackOverride") {
+ if (!IsVideo()) {
+ mAudioProcessing =
+ MakeAndAddRef<AudioProxyThread>(*mConduit->AsAudioSessionConduit());
+ mListener->SetAudioProxy(mAudioProcessing);
+ } else { // Video
+ mConverter = MakeAndAddRef<VideoFrameConverter>(GetTimestampMaker());
+ mFrameListener = mConverter->VideoFrameConvertedEvent().Connect(
+ mConverter->mTaskQueue,
+ [listener = mListener](webrtc::VideoFrame aFrame) {
+ listener->OnVideoFrameConverted(std::move(aFrame));
+ });
+ mListener->SetVideoFrameConverter(mConverter);
+ }
+
+ mWatchManager.Watch(mActive, &MediaPipelineTransmit::UpdateSendState);
+ mWatchManager.Watch(mDomTrack, &MediaPipelineTransmit::UpdateSendState);
+ mWatchManager.Watch(mSendTrackOverride,
+ &MediaPipelineTransmit::UpdateSendState);
+
+ mDescription = GenerateDescription();
+}
+
+MediaPipelineTransmit::~MediaPipelineTransmit() {
+ mFrameListener.DisconnectIfExists();
+
+ MOZ_ASSERT(!mTransmitting);
+ MOZ_ASSERT(!mDomTrack.Ref());
+}
+
+void MediaPipelineTransmit::InitControl(
+ MediaPipelineTransmitControlInterface* aControl) {
+ mActive.Connect(aControl->CanonicalTransmitting());
+}
+
+void MediaPipelineTransmit::Shutdown() {
+ MediaPipeline::Shutdown();
+ mWatchManager.Shutdown();
+ if (mDomTrack.Ref()) {
+ mDomTrack.Ref()->RemovePrincipalChangeObserver(this);
+ mDomTrack = nullptr;
+ }
+ mUnsettingSendTrack = false;
+ UpdateSendState();
+ MOZ_ASSERT(!mTransmitting);
+}
+
+void MediaPipeline::SetDescription_s(const std::string& description) {
+ ASSERT_ON_THREAD(mStsThread);
+ mDescription = description;
+}
+
+std::string MediaPipelineTransmit::GenerateDescription() const {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ std::stringstream description;
+ description << mPc << "| ";
+ description << (mIsVideo ? "Transmit video[" : "Transmit audio[");
+
+ if (mDomTrack.Ref()) {
+ nsString nsTrackId;
+ mDomTrack.Ref()->GetId(nsTrackId);
+ description << NS_ConvertUTF16toUTF8(nsTrackId).get();
+ } else if (mSendTrackOverride.Ref()) {
+ description << "override " << mSendTrackOverride.Ref().get();
+ } else {
+ description << "no track";
+ }
+
+ description << "]";
+
+ return description.str();
+}
+
+void MediaPipelineTransmit::UpdateSendState() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // This runs because either mActive, mDomTrack or mSendTrackOverride changed,
+ // or because mSendTrack was unset async. Based on these inputs this method
+ // is responsible for hooking up mSendTrack to mListener in order to feed data
+ // to the conduit.
+ //
+ // If we are inactive, or if the send track does not match what we want to
+ // send (mDomTrack or mSendTrackOverride), we must stop feeding data to the
+ // conduit. NB that removing the listener from mSendTrack is async, and we
+ // must wait for it to resolve before adding mListener to another track.
+ // mUnsettingSendTrack gates us until the listener has been removed from
+ // mSendTrack.
+ //
+ // If we are active and the send track does match what we want to send, we
+ // make sure mListener is added to the send track. Either now, or if we're
+ // still waiting for another send track to be removed, during a future call to
+ // this method.
+
+ if (mUnsettingSendTrack) {
+ // We must wait for the send track to be unset before we can set it again,
+ // to avoid races. Once unset this function is triggered again.
+ return;
+ }
+
+ const bool wasTransmitting = mTransmitting;
+
+ const bool haveLiveSendTrack = mSendTrack && !mSendTrack->IsDestroyed();
+ const bool haveLiveDomTrack = mDomTrack.Ref() && !mDomTrack.Ref()->Ended();
+ const bool haveLiveOverrideTrack =
+ mSendTrackOverride.Ref() && !mSendTrackOverride.Ref()->IsDestroyed();
+ const bool mustRemoveSendTrack =
+ haveLiveSendTrack && !mSendTrackOverride.Ref() &&
+ (!haveLiveDomTrack || mDomTrack.Ref()->GetTrack() != mSendPortSource);
+
+ mTransmitting = mActive && (haveLiveDomTrack || haveLiveOverrideTrack) &&
+ !mustRemoveSendTrack;
+
+ MOZ_LOG(gMediaPipelineLog, LogLevel::Debug,
+ ("MediaPipeline %p UpdateSendState wasTransmitting=%d, active=%d, "
+ "sendTrack=%p (%s), domTrack=%p (%s), "
+ "sendTrackOverride=%p (%s), mustRemove=%d, mTransmitting=%d",
+ this, wasTransmitting, mActive.Ref(), mSendTrack.get(),
+ haveLiveSendTrack ? "live" : "ended", mDomTrack.Ref().get(),
+ haveLiveDomTrack ? "live" : "ended", mSendTrackOverride.Ref().get(),
+ haveLiveOverrideTrack ? "live" : "ended", mustRemoveSendTrack,
+ mTransmitting));
+
+ if (!wasTransmitting && mTransmitting) {
+ MOZ_LOG(gMediaPipelineLog, LogLevel::Debug,
+ ("Attaching pipeline %p to track %p conduit type=%s", this,
+ mDomTrack.Ref().get(), mIsVideo ? "video" : "audio"));
+ if (mDescriptionInvalidated) {
+ // Only update the description when we attach to a track, as detaching is
+ // always a longer async step than updating the description. Updating on
+ // detach would cause the wrong track id to be attributed in logs.
+ RUN_ON_THREAD(mStsThread,
+ WrapRunnable(RefPtr<MediaPipeline>(this),
+ &MediaPipelineTransmit::SetDescription_s,
+ GenerateDescription()),
+ NS_DISPATCH_NORMAL);
+ mDescriptionInvalidated = false;
+ }
+ if (mSendTrackOverride.Ref()) {
+ // Special path that allows unittests to avoid mDomTrack and the graph by
+ // manually calling SetSendTrack.
+ mSendTrack = mSendTrackOverride.Ref();
+ } else {
+ mSendTrack = mDomTrack.Ref()->Graph()->CreateForwardedInputTrack(
+ mDomTrack.Ref()->GetTrack()->mType);
+ mSendPortSource = mDomTrack.Ref()->GetTrack();
+ mSendPort = mSendTrack->AllocateInputPort(mSendPortSource.get());
+ }
+ if (mIsVideo) {
+ mConverter->SetTrackingId(mDomTrack.Ref()->GetSource().mTrackingId);
+ }
+ mSendTrack->QueueSetAutoend(false);
+ if (mIsVideo) {
+ mSendTrack->AddDirectListener(mListener);
+ }
+ mSendTrack->AddListener(mListener);
+ }
+
+ if (wasTransmitting && !mTransmitting) {
+ MOZ_LOG(gMediaPipelineLog, LogLevel::Debug,
+ ("Detaching pipeline %p from track %p conduit type=%s", this,
+ mDomTrack.Ref().get(), mIsVideo ? "video" : "audio"));
+ mUnsettingSendTrack = true;
+ if (mIsVideo) {
+ mSendTrack->RemoveDirectListener(mListener);
+ }
+ mSendTrack->RemoveListener(mListener)->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [this, self = RefPtr<MediaPipelineTransmit>(this)] {
+ mUnsettingSendTrack = false;
+ mSendTrack = nullptr;
+ if (!mWatchManager.IsShutdown()) {
+ mWatchManager.ManualNotify(&MediaPipelineTransmit::UpdateSendState);
+ }
+ });
+ if (!mSendTrackOverride.Ref()) {
+ // If an override is set it may be re-used.
+ mSendTrack->Destroy();
+ mSendPort->Destroy();
+ mSendPort = nullptr;
+ mSendPortSource = nullptr;
+ }
+ }
+}
+
+bool MediaPipelineTransmit::Transmitting() const {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ return mActive;
+}
+
+bool MediaPipelineTransmit::IsVideo() const { return mIsVideo; }
+
+void MediaPipelineTransmit::PrincipalChanged(dom::MediaStreamTrack* aTrack) {
+ MOZ_ASSERT(aTrack && aTrack == mDomTrack.Ref());
+
+ PeerConnectionWrapper pcw(mPc);
+ if (pcw.impl()) {
+ Document* doc = pcw.impl()->GetParentObject()->GetExtantDoc();
+ if (doc) {
+ UpdateSinkIdentity(doc->NodePrincipal(), pcw.impl()->GetPeerIdentity());
+ } else {
+ MOZ_LOG(gMediaPipelineLog, LogLevel::Info,
+ ("Can't update sink principal; document gone"));
+ }
+ }
+}
+
+void MediaPipelineTransmit::UpdateSinkIdentity(
+ nsIPrincipal* aPrincipal, const PeerIdentity* aSinkIdentity) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mDomTrack.Ref()) {
+ // Nothing to do here
+ return;
+ }
+
+ bool enableTrack = aPrincipal->Subsumes(mDomTrack.Ref()->GetPrincipal());
+ if (!enableTrack) {
+ // first try didn't work, but there's a chance that this is still available
+ // if our track is bound to a peerIdentity, and the peer connection (our
+ // sink) is bound to the same identity, then we can enable the track.
+ const PeerIdentity* trackIdentity = mDomTrack.Ref()->GetPeerIdentity();
+ if (aSinkIdentity && trackIdentity) {
+ enableTrack = (*aSinkIdentity == *trackIdentity);
+ }
+ }
+
+ mListener->SetEnabled(enableTrack);
+}
+
+void MediaPipelineTransmit::TransportReady_s() {
+ ASSERT_ON_THREAD(mStsThread);
+ // Call base ready function.
+ MediaPipeline::TransportReady_s();
+ mListener->SetActive(true);
+}
+
+nsresult MediaPipelineTransmit::SetTrack(
+ const RefPtr<MediaStreamTrack>& aDomTrack) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mDomTrack.Ref()) {
+ mDomTrack.Ref()->RemovePrincipalChangeObserver(this);
+ }
+
+ if (aDomTrack) {
+ nsString nsTrackId;
+ aDomTrack->GetId(nsTrackId);
+ MOZ_LOG(gMediaPipelineLog, LogLevel::Debug,
+ ("Reattaching pipeline to track %p track %s conduit type: %s",
+ aDomTrack.get(), NS_ConvertUTF16toUTF8(nsTrackId).get(),
+ mIsVideo ? "video" : "audio"));
+ }
+
+ mDescriptionInvalidated = true;
+ mDomTrack = aDomTrack;
+ if (mDomTrack.Ref()) {
+ mDomTrack.Ref()->AddPrincipalChangeObserver(this);
+ PrincipalChanged(mDomTrack.Ref());
+ }
+
+ return NS_OK;
+}
+
+RefPtr<dom::MediaStreamTrack> MediaPipelineTransmit::GetTrack() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mDomTrack;
+}
+
+void MediaPipelineTransmit::SetSendTrackOverride(
+ const RefPtr<ProcessedMediaTrack>& aSendTrack) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_RELEASE_ASSERT(!mSendTrack);
+ MOZ_RELEASE_ASSERT(!mSendPort);
+ MOZ_RELEASE_ASSERT(!mSendTrackOverride.Ref());
+ mDescriptionInvalidated = true;
+ mSendTrackOverride = aSendTrack;
+}
+
+// Called if we're attached with AddDirectListener()
+void MediaPipelineTransmit::PipelineListener::NotifyRealtimeTrackData(
+ MediaTrackGraph* aGraph, TrackTime aOffset, const MediaSegment& aMedia) {
+ MOZ_LOG(
+ gMediaPipelineLog, LogLevel::Debug,
+ ("MediaPipeline::NotifyRealtimeTrackData() listener=%p, offset=%" PRId64
+ ", duration=%" PRId64,
+ this, aOffset, aMedia.GetDuration()));
+ TRACE_COMMENT(
+ "MediaPipelineTransmit::PipelineListener::NotifyRealtimeTrackData", "%s",
+ aMedia.GetType() == MediaSegment::VIDEO ? "Video" : "Audio");
+ NewData(aMedia, aGraph->GraphRate());
+}
+
+void MediaPipelineTransmit::PipelineListener::NotifyQueuedChanges(
+ MediaTrackGraph* aGraph, TrackTime aOffset,
+ const MediaSegment& aQueuedMedia) {
+ MOZ_LOG(gMediaPipelineLog, LogLevel::Debug,
+ ("MediaPipeline::NotifyQueuedChanges()"));
+
+ if (aQueuedMedia.GetType() == MediaSegment::VIDEO) {
+ // We always get video from the direct listener.
+ return;
+ }
+
+ TRACE("MediaPipelineTransmit::PipelineListener::NotifyQueuedChanges (Audio)");
+
+ if (mDirectConnect) {
+ // ignore non-direct data if we're also getting direct data
+ return;
+ }
+
+ size_t rate;
+ if (aGraph) {
+ rate = aGraph->GraphRate();
+ } else {
+ // When running tests, graph may be null. In that case use a default.
+ rate = 16000;
+ }
+ NewData(aQueuedMedia, rate);
+}
+
+void MediaPipelineTransmit::PipelineListener::NotifyEnabledStateChanged(
+ MediaTrackGraph* aGraph, bool aEnabled) {
+ if (mConduit->type() != MediaSessionConduit::VIDEO) {
+ return;
+ }
+ MOZ_ASSERT(mConverter);
+ mConverter->SetTrackEnabled(aEnabled);
+}
+
+void MediaPipelineTransmit::PipelineListener::NotifyDirectListenerInstalled(
+ InstallationResult aResult) {
+ MOZ_LOG(gMediaPipelineLog, LogLevel::Info,
+ ("MediaPipeline::NotifyDirectListenerInstalled() listener=%p,"
+ " result=%d",
+ this, static_cast<int32_t>(aResult)));
+
+ mDirectConnect = InstallationResult::SUCCESS == aResult;
+}
+
+void MediaPipelineTransmit::PipelineListener::
+ NotifyDirectListenerUninstalled() {
+ MOZ_LOG(
+ gMediaPipelineLog, LogLevel::Info,
+ ("MediaPipeline::NotifyDirectListenerUninstalled() listener=%p", this));
+
+ if (mConduit->type() == MediaSessionConduit::VIDEO) {
+ // Reset the converter's track-enabled state. If re-added to a new track
+ // later and that track is disabled, we will be signaled explicitly.
+ MOZ_ASSERT(mConverter);
+ mConverter->SetTrackEnabled(true);
+ }
+
+ mDirectConnect = false;
+}
+
+void MediaPipelineTransmit::PipelineListener::NewData(
+ const MediaSegment& aMedia, TrackRate aRate /* = 0 */) {
+ if (mConduit->type() != (aMedia.GetType() == MediaSegment::AUDIO
+ ? MediaSessionConduit::AUDIO
+ : MediaSessionConduit::VIDEO)) {
+ MOZ_ASSERT(false,
+ "The media type should always be correct since the "
+ "listener is locked to a specific track");
+ return;
+ }
+
+ // TODO(ekr@rtfm.com): For now assume that we have only one
+ // track type and it's destined for us
+ // See bug 784517
+ if (aMedia.GetType() == MediaSegment::AUDIO) {
+ MOZ_RELEASE_ASSERT(aRate > 0);
+
+ if (!mActive) {
+ MOZ_LOG(gMediaPipelineLog, LogLevel::Debug,
+ ("Discarding audio packets because transport not ready"));
+ return;
+ }
+
+ const AudioSegment* audio = static_cast<const AudioSegment*>(&aMedia);
+ for (AudioSegment::ConstChunkIterator iter(*audio); !iter.IsEnded();
+ iter.Next()) {
+ mAudioProcessing->QueueAudioChunk(aRate, *iter, mEnabled);
+ }
+ } else {
+ const VideoSegment* video = static_cast<const VideoSegment*>(&aMedia);
+
+ for (VideoSegment::ConstChunkIterator iter(*video); !iter.IsEnded();
+ iter.Next()) {
+ mConverter->QueueVideoChunk(*iter, !mEnabled);
+ }
+ }
+}
+
+class GenericReceiveListener : public MediaTrackListener {
+ public:
+ GenericReceiveListener(RefPtr<SourceMediaTrack> aSource,
+ TrackingId aTrackingId)
+ : mSource(std::move(aSource)),
+ mTrackingId(std::move(aTrackingId)),
+ mIsAudio(mSource->mType == MediaSegment::AUDIO),
+ mEnabled(false) {
+ MOZ_DIAGNOSTIC_ASSERT(NS_IsMainThread());
+ MOZ_DIAGNOSTIC_ASSERT(mSource, "Must be used with a SourceMediaTrack");
+ }
+
+ virtual ~GenericReceiveListener() = default;
+
+ void Init() { mSource->AddListener(this); }
+ void Shutdown() { mSource->RemoveListener(this); }
+
+ void SetEnabled(bool aEnabled) {
+ if (mEnabled == aEnabled) {
+ return;
+ }
+ mEnabled = aEnabled;
+ if (mIsAudio && !mSource->IsDestroyed()) {
+ mSource->SetPullingEnabled(mEnabled);
+ }
+ }
+
+ protected:
+ const RefPtr<SourceMediaTrack> mSource;
+ const TrackingId mTrackingId;
+ const bool mIsAudio;
+ // Main thread only.
+ bool mEnabled;
+};
+
+MediaPipelineReceive::MediaPipelineReceive(
+ const std::string& aPc, RefPtr<MediaTransportHandler> aTransportHandler,
+ RefPtr<AbstractThread> aCallThread, RefPtr<nsISerialEventTarget> aStsThread,
+ RefPtr<MediaSessionConduit> aConduit)
+ : MediaPipeline(aPc, std::move(aTransportHandler), DirectionType::RECEIVE,
+ std::move(aCallThread), std::move(aStsThread),
+ std::move(aConduit)),
+ mWatchManager(this, AbstractThread::MainThread()) {
+ mWatchManager.Watch(mActive, &MediaPipelineReceive::UpdateListener);
+}
+
+MediaPipelineReceive::~MediaPipelineReceive() = default;
+
+void MediaPipelineReceive::InitControl(
+ MediaPipelineReceiveControlInterface* aControl) {
+ mActive.Connect(aControl->CanonicalReceiving());
+}
+
+void MediaPipelineReceive::Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MediaPipeline::Shutdown();
+ mWatchManager.Shutdown();
+}
+
+class MediaPipelineReceiveAudio::PipelineListener
+ : public GenericReceiveListener {
+ public:
+ PipelineListener(RefPtr<SourceMediaTrack> aSource, TrackingId aTrackingId,
+ RefPtr<MediaSessionConduit> aConduit,
+ PrincipalHandle aPrincipalHandle, PrincipalPrivacy aPrivacy)
+ : GenericReceiveListener(std::move(aSource), std::move(aTrackingId)),
+ mConduit(std::move(aConduit)),
+ // AudioSession conduit only supports 16, 32, 44.1 and 48kHz
+ // This is an artificial limitation, it would however require more
+ // changes to support any rates. If the sampling rate is not-supported,
+ // we will use 48kHz instead.
+ mRate(static_cast<AudioSessionConduit*>(mConduit.get())
+ ->IsSamplingFreqSupported(mSource->Graph()->GraphRate())
+ ? mSource->Graph()->GraphRate()
+ : WEBRTC_MAX_SAMPLE_RATE),
+ mTaskQueue(TaskQueue::Create(
+ GetMediaThreadPool(MediaThreadType::WEBRTC_WORKER),
+ "AudioPipelineListener")),
+ mPlayedTicks(0),
+ mAudioFrame(std::make_unique<webrtc::AudioFrame>()),
+ mPrincipalHandle(std::move(aPrincipalHandle)),
+ mPrivacy(aPrivacy),
+ mForceSilence(false) {}
+
+ void Init() {
+ GenericReceiveListener::Init();
+ mSource->SetAppendDataSourceRate(mRate);
+ }
+
+ // Implement MediaTrackListener
+ void NotifyPull(MediaTrackGraph* aGraph, TrackTime aEndOfAppendedData,
+ TrackTime aDesiredTime) override {
+ NotifyPullImpl(aDesiredTime);
+ }
+
+ void OnPrivacyRequested_s() {
+ if (mPrivacy == PrincipalPrivacy::Private) {
+ return;
+ }
+ mForceSilence = true;
+ }
+
+ void SetPrivatePrincipal(PrincipalHandle aHandle) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ class Message : public ControlMessage {
+ public:
+ Message(RefPtr<PipelineListener> aListener,
+ PrincipalHandle aPrivatePrincipal)
+ : ControlMessage(nullptr),
+ mListener(std::move(aListener)),
+ mPrivatePrincipal(std::move(aPrivatePrincipal)) {}
+
+ void Run() override {
+ if (mListener->mPrivacy == PrincipalPrivacy::Private) {
+ return;
+ }
+ mListener->mPrincipalHandle = mPrivatePrincipal;
+ mListener->mPrivacy = PrincipalPrivacy::Private;
+ mListener->mForceSilence = false;
+ }
+
+ const RefPtr<PipelineListener> mListener;
+ PrincipalHandle mPrivatePrincipal;
+ };
+
+ if (mSource->IsDestroyed()) {
+ return;
+ }
+
+ mSource->GraphImpl()->AppendMessage(
+ MakeUnique<Message>(this, std::move(aHandle)));
+ }
+
+ private:
+ ~PipelineListener() = default;
+
+ void NotifyPullImpl(TrackTime aDesiredTime) {
+ TRACE_COMMENT("PiplineListener::NotifyPullImpl", "PipelineListener %p",
+ this);
+ uint32_t samplesPer10ms = mRate / 100;
+
+ // mSource's rate is not necessarily the same as the graph rate, since there
+ // are sample-rate constraints on the inbound audio: only 16, 32, 44.1 and
+ // 48kHz are supported. The audio frames we get here is going to be
+ // resampled when inserted into the graph. aDesiredTime and mPlayedTicks are
+ // in the graph rate.
+
+ while (mPlayedTicks < aDesiredTime) {
+ // This fetches 10ms of data, either mono or stereo
+ MediaConduitErrorCode err =
+ static_cast<AudioSessionConduit*>(mConduit.get())
+ ->GetAudioFrame(mRate, mAudioFrame.get());
+
+ if (err != kMediaConduitNoError) {
+ // Insert silence on conduit/GIPS failure (extremely unlikely)
+ MOZ_LOG(gMediaPipelineLog, LogLevel::Error,
+ ("Audio conduit failed (%d) to return data @ %" PRId64
+ " (desired %" PRId64 " -> %f)",
+ err, mPlayedTicks, aDesiredTime,
+ mSource->TrackTimeToSeconds(aDesiredTime)));
+ constexpr size_t mono = 1;
+ mAudioFrame->UpdateFrame(
+ mAudioFrame->timestamp_, nullptr, samplesPer10ms, mRate,
+ mAudioFrame->speech_type_, mAudioFrame->vad_activity_,
+ std::max(mono, mAudioFrame->num_channels()));
+ }
+
+ MOZ_LOG(
+ gMediaPipelineLog, LogLevel::Debug,
+ ("Audio conduit returned buffer for %zu channels, %zu frames",
+ mAudioFrame->num_channels(), mAudioFrame->samples_per_channel()));
+
+ AudioSegment segment;
+ if (mForceSilence || mAudioFrame->muted()) {
+ segment.AppendNullData(mAudioFrame->samples_per_channel());
+ } else {
+ CheckedInt<size_t> bufferSize(sizeof(uint16_t));
+ bufferSize *= mAudioFrame->samples_per_channel();
+ bufferSize *= mAudioFrame->num_channels();
+ RefPtr<SharedBuffer> samples = SharedBuffer::Create(bufferSize);
+ int16_t* samplesData = static_cast<int16_t*>(samples->Data());
+ AutoTArray<int16_t*, 2> channels;
+ AutoTArray<const int16_t*, 2> outputChannels;
+
+ channels.SetLength(mAudioFrame->num_channels());
+
+ size_t offset = 0;
+ for (size_t i = 0; i < mAudioFrame->num_channels(); i++) {
+ channels[i] = samplesData + offset;
+ offset += mAudioFrame->samples_per_channel();
+ }
+
+ DeinterleaveAndConvertBuffer(
+ mAudioFrame->data(), mAudioFrame->samples_per_channel(),
+ mAudioFrame->num_channels(), channels.Elements());
+
+ outputChannels.AppendElements(channels);
+
+ segment.AppendFrames(samples.forget(), outputChannels,
+ mAudioFrame->samples_per_channel(),
+ mPrincipalHandle);
+ }
+
+ // Handle track not actually added yet or removed/finished
+ if (TrackTime appended = mSource->AppendData(&segment)) {
+ mPlayedTicks += appended;
+ } else {
+ MOZ_LOG(gMediaPipelineLog, LogLevel::Error, ("AppendData failed"));
+ // we can't un-read the data, but that's ok since we don't want to
+ // buffer - but don't i-loop!
+ break;
+ }
+ }
+ }
+
+ const RefPtr<MediaSessionConduit> mConduit;
+ // This conduit's sampling rate. This is either 16, 32, 44.1 or 48kHz, and
+ // tries to be the same as the graph rate. If the graph rate is higher than
+ // 48kHz, mRate is capped to 48kHz. If mRate does not match the graph rate,
+ // audio is resampled to the graph rate.
+ const TrackRate mRate;
+ const RefPtr<TaskQueue> mTaskQueue;
+ // Number of frames of data that has been added to the SourceMediaTrack in
+ // the graph's rate. Graph thread only.
+ TrackTicks mPlayedTicks;
+ // Allocation of an audio frame used as a scratch buffer when reading data out
+ // of libwebrtc for forwarding into the graph. Graph thread only.
+ std::unique_ptr<webrtc::AudioFrame> mAudioFrame;
+ // Principal handle used when appending data to the SourceMediaTrack. Graph
+ // thread only.
+ PrincipalHandle mPrincipalHandle;
+ // Privacy of mPrincipalHandle. Graph thread only.
+ PrincipalPrivacy mPrivacy;
+ // Set to true on the sts thread if privacy is requested when ALPN was
+ // negotiated. Set to false again when mPrincipalHandle is private.
+ Atomic<bool> mForceSilence;
+};
+
+MediaPipelineReceiveAudio::MediaPipelineReceiveAudio(
+ const std::string& aPc, RefPtr<MediaTransportHandler> aTransportHandler,
+ RefPtr<AbstractThread> aCallThread, RefPtr<nsISerialEventTarget> aStsThread,
+ RefPtr<AudioSessionConduit> aConduit, RefPtr<SourceMediaTrack> aSource,
+ TrackingId aTrackingId, PrincipalHandle aPrincipalHandle,
+ PrincipalPrivacy aPrivacy)
+ : MediaPipelineReceive(aPc, std::move(aTransportHandler),
+ std::move(aCallThread), std::move(aStsThread),
+ std::move(aConduit)),
+ mListener(aSource ? new PipelineListener(
+ std::move(aSource), std::move(aTrackingId),
+ mConduit, std::move(aPrincipalHandle), aPrivacy)
+ : nullptr) {
+ mDescription = mPc + "| Receive audio";
+ if (mListener) {
+ mListener->Init();
+ }
+}
+
+void MediaPipelineReceiveAudio::Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MediaPipelineReceive::Shutdown();
+ if (mListener) {
+ mListener->Shutdown();
+ }
+}
+
+void MediaPipelineReceiveAudio::OnPrivacyRequested_s() {
+ ASSERT_ON_THREAD(mStsThread);
+ if (mListener) {
+ mListener->OnPrivacyRequested_s();
+ }
+}
+
+void MediaPipelineReceiveAudio::SetPrivatePrincipal(PrincipalHandle aHandle) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mListener) {
+ mListener->SetPrivatePrincipal(std::move(aHandle));
+ }
+}
+
+void MediaPipelineReceiveAudio::UpdateListener() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mListener) {
+ mListener->SetEnabled(mActive.Ref());
+ }
+}
+
+class MediaPipelineReceiveVideo::PipelineListener
+ : public GenericReceiveListener {
+ public:
+ PipelineListener(RefPtr<SourceMediaTrack> aSource, TrackingId aTrackingId,
+ PrincipalHandle aPrincipalHandle, PrincipalPrivacy aPrivacy)
+ : GenericReceiveListener(std::move(aSource), std::move(aTrackingId)),
+ mImageContainer(
+ MakeAndAddRef<ImageContainer>(ImageContainer::ASYNCHRONOUS)),
+ mMutex("MediaPipelineReceiveVideo::PipelineListener::mMutex"),
+ mPrincipalHandle(std::move(aPrincipalHandle)),
+ mPrivacy(aPrivacy) {}
+ void OnPrivacyRequested_s() {
+ MutexAutoLock lock(mMutex);
+ if (mPrivacy == PrincipalPrivacy::Private) {
+ return;
+ }
+ mForceDropFrames = true;
+ }
+
+ void SetPrivatePrincipal(PrincipalHandle aHandle) {
+ MutexAutoLock lock(mMutex);
+ if (mPrivacy == PrincipalPrivacy::Private) {
+ return;
+ }
+ mPrincipalHandle = std::move(aHandle);
+ mPrivacy = PrincipalPrivacy::Private;
+ mForceDropFrames = false;
+ }
+
+ void RenderVideoFrame(const webrtc::VideoFrameBuffer& aBuffer,
+ uint32_t aTimeStamp, int64_t aRenderTime) {
+ PrincipalHandle principal;
+ {
+ MutexAutoLock lock(mMutex);
+ if (mForceDropFrames) {
+ return;
+ }
+ principal = mPrincipalHandle;
+ }
+ RefPtr<Image> image;
+ if (aBuffer.type() == webrtc::VideoFrameBuffer::Type::kNative) {
+ // We assume that only native handles are used with the
+ // WebrtcMediaDataCodec decoder.
+ const ImageBuffer* imageBuffer =
+ static_cast<const ImageBuffer*>(&aBuffer);
+ image = imageBuffer->GetNativeImage();
+ } else {
+ MOZ_ASSERT(aBuffer.type() == webrtc::VideoFrameBuffer::Type::kI420);
+ rtc::scoped_refptr<const webrtc::I420BufferInterface> i420(
+ aBuffer.GetI420());
+
+ MOZ_ASSERT(i420->DataY());
+ // Create a video frame using |buffer|.
+ PerformanceRecorder<CopyVideoStage> rec(
+ "MediaPipelineReceiveVideo::CopyToImage"_ns, mTrackingId,
+ i420->width(), i420->height());
+
+ RefPtr<PlanarYCbCrImage> yuvImage =
+ mImageContainer->CreatePlanarYCbCrImage();
+
+ PlanarYCbCrData yuvData;
+ yuvData.mYChannel = const_cast<uint8_t*>(i420->DataY());
+ yuvData.mYStride = i420->StrideY();
+ MOZ_ASSERT(i420->StrideU() == i420->StrideV());
+ yuvData.mCbCrStride = i420->StrideU();
+ yuvData.mCbChannel = const_cast<uint8_t*>(i420->DataU());
+ yuvData.mCrChannel = const_cast<uint8_t*>(i420->DataV());
+ yuvData.mPictureRect = IntRect(0, 0, i420->width(), i420->height());
+ yuvData.mStereoMode = StereoMode::MONO;
+ // This isn't the best default.
+ yuvData.mYUVColorSpace = gfx::YUVColorSpace::BT601;
+ yuvData.mChromaSubsampling =
+ gfx::ChromaSubsampling::HALF_WIDTH_AND_HEIGHT;
+
+ if (!yuvImage->CopyData(yuvData)) {
+ MOZ_ASSERT(false);
+ return;
+ }
+ rec.Record();
+
+ image = std::move(yuvImage);
+ }
+
+ VideoSegment segment;
+ auto size = image->GetSize();
+ segment.AppendFrame(image.forget(), size, principal);
+ mSource->AppendData(&segment);
+ }
+
+ private:
+ RefPtr<layers::ImageContainer> mImageContainer;
+ Mutex mMutex;
+ PrincipalHandle mPrincipalHandle MOZ_GUARDED_BY(mMutex);
+ PrincipalPrivacy mPrivacy MOZ_GUARDED_BY(mMutex);
+ // Set to true on the sts thread if privacy is requested when ALPN was
+ // negotiated. Set to false again when mPrincipalHandle is private.
+ bool mForceDropFrames MOZ_GUARDED_BY(mMutex) = false;
+};
+
+class MediaPipelineReceiveVideo::PipelineRenderer
+ : public mozilla::VideoRenderer {
+ public:
+ explicit PipelineRenderer(MediaPipelineReceiveVideo* aPipeline)
+ : mPipeline(aPipeline) {}
+
+ void Detach() { mPipeline = nullptr; }
+
+ // Implement VideoRenderer
+ void FrameSizeChange(unsigned int aWidth, unsigned int aHeight) override {}
+ void RenderVideoFrame(const webrtc::VideoFrameBuffer& aBuffer,
+ uint32_t aTimeStamp, int64_t aRenderTime) override {
+ mPipeline->mListener->RenderVideoFrame(aBuffer, aTimeStamp, aRenderTime);
+ }
+
+ private:
+ MediaPipelineReceiveVideo* mPipeline; // Raw pointer to avoid cycles
+};
+
+MediaPipelineReceiveVideo::MediaPipelineReceiveVideo(
+ const std::string& aPc, RefPtr<MediaTransportHandler> aTransportHandler,
+ RefPtr<AbstractThread> aCallThread, RefPtr<nsISerialEventTarget> aStsThread,
+ RefPtr<VideoSessionConduit> aConduit, RefPtr<SourceMediaTrack> aSource,
+ TrackingId aTrackingId, PrincipalHandle aPrincipalHandle,
+ PrincipalPrivacy aPrivacy)
+ : MediaPipelineReceive(aPc, std::move(aTransportHandler),
+ std::move(aCallThread), std::move(aStsThread),
+ std::move(aConduit)),
+ mRenderer(new PipelineRenderer(this)),
+ mListener(aSource ? new PipelineListener(
+ std::move(aSource), std::move(aTrackingId),
+ std::move(aPrincipalHandle), aPrivacy)
+ : nullptr) {
+ mDescription = mPc + "| Receive video";
+ if (mListener) {
+ mListener->Init();
+ }
+ static_cast<VideoSessionConduit*>(mConduit.get())->AttachRenderer(mRenderer);
+}
+
+void MediaPipelineReceiveVideo::Shutdown() {
+ MOZ_ASSERT(NS_IsMainThread());
+ MediaPipelineReceive::Shutdown();
+ if (mListener) {
+ mListener->Shutdown();
+ }
+
+ // stop generating video and thus stop invoking the PipelineRenderer
+ // and PipelineListener - the renderer has a raw ptr to the Pipeline to
+ // avoid cycles, and the render callbacks are invoked from a different
+ // thread so simple null-checks would cause TSAN bugs without locks.
+ static_cast<VideoSessionConduit*>(mConduit.get())->DetachRenderer();
+}
+
+void MediaPipelineReceiveVideo::OnPrivacyRequested_s() {
+ ASSERT_ON_THREAD(mStsThread);
+ if (mListener) {
+ mListener->OnPrivacyRequested_s();
+ }
+}
+
+void MediaPipelineReceiveVideo::SetPrivatePrincipal(PrincipalHandle aHandle) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mListener) {
+ mListener->SetPrivatePrincipal(std::move(aHandle));
+ }
+}
+
+void MediaPipelineReceiveVideo::UpdateListener() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mListener) {
+ mListener->SetEnabled(mActive.Ref());
+ }
+}
+
+const dom::RTCStatsTimestampMaker& MediaPipeline::GetTimestampMaker() const {
+ return mConduit->GetTimestampMaker();
+}
+
+DOMHighResTimeStamp MediaPipeline::RtpCSRCStats::GetExpiryFromTime(
+ const DOMHighResTimeStamp aTime) {
+ // DOMHighResTimeStamp is a unit measured in ms
+ return aTime + EXPIRY_TIME_MILLISECONDS;
+}
+
+MediaPipeline::RtpCSRCStats::RtpCSRCStats(const uint32_t aCsrc,
+ const DOMHighResTimeStamp aTime)
+ : mCsrc(aCsrc), mTimestamp(aTime) {}
+
+void MediaPipeline::RtpCSRCStats::GetWebidlInstance(
+ dom::RTCRTPContributingSourceStats& aWebidlObj,
+ const nsString& aInboundRtpStreamId) const {
+ nsString statId = u"csrc_"_ns + aInboundRtpStreamId;
+ statId.AppendLiteral("_");
+ statId.AppendInt(mCsrc);
+ aWebidlObj.mId.Construct(statId);
+ aWebidlObj.mType.Construct(RTCStatsType::Csrc);
+ aWebidlObj.mTimestamp.Construct(mTimestamp);
+ aWebidlObj.mContributorSsrc.Construct(mCsrc);
+ aWebidlObj.mInboundRtpStreamId.Construct(aInboundRtpStreamId);
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/transportbridge/MediaPipeline.h b/dom/media/webrtc/transportbridge/MediaPipeline.h
new file mode 100644
index 0000000000..d58fd12ea3
--- /dev/null
+++ b/dom/media/webrtc/transportbridge/MediaPipeline.h
@@ -0,0 +1,454 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: ekr@rtfm.com
+
+#ifndef mediapipeline_h__
+#define mediapipeline_h__
+
+#include <map>
+
+#include "transport/sigslot.h"
+#include "transport/transportlayer.h" // For TransportLayer::State
+
+#include "libwebrtcglue/MediaConduitControl.h"
+#include "mozilla/ReentrantMonitor.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/StateMirroring.h"
+#include "transport/mediapacket.h"
+#include "transport/runnable_utils.h"
+#include "AudioPacketizer.h"
+#include "MediaEventSource.h"
+#include "MediaPipelineFilter.h"
+#include "MediaSegment.h"
+#include "PrincipalChangeObserver.h"
+#include "jsapi/PacketDumper.h"
+#include "PerformanceRecorder.h"
+
+// Should come from MediaEngine.h, but that's a pain to include here
+// because of the MOZILLA_EXTERNAL_LINKAGE stuff.
+#define WEBRTC_MAX_SAMPLE_RATE 48000
+
+class nsIPrincipal;
+
+namespace webrtc {
+struct RTPHeader;
+class RtpHeaderExtensionMap;
+class RtpPacketReceived;
+} // namespace webrtc
+
+namespace mozilla {
+class AudioProxyThread;
+class MediaInputPort;
+class MediaPipelineFilter;
+class MediaTransportHandler;
+class PeerIdentity;
+class ProcessedMediaTrack;
+class SourceMediaTrack;
+class VideoFrameConverter;
+class MediaSessionConduit;
+class AudioSessionConduit;
+class VideoSessionConduit;
+
+namespace dom {
+class MediaStreamTrack;
+struct RTCRTPContributingSourceStats;
+class RTCStatsTimestampMaker;
+} // namespace dom
+
+struct MediaPipelineReceiveControlInterface {
+ virtual AbstractCanonical<bool>* CanonicalReceiving() = 0;
+};
+
+struct MediaPipelineTransmitControlInterface {
+ virtual AbstractCanonical<bool>* CanonicalTransmitting() = 0;
+};
+
+// A class that represents the pipeline of audio and video
+// The dataflow looks like:
+//
+// TRANSMIT
+// CaptureDevice -> stream -> [us] -> conduit -> [us] -> transport -> network
+//
+// RECEIVE
+// network -> transport -> [us] -> conduit -> [us] -> stream -> Playout
+//
+// The boxes labeled [us] are just bridge logic implemented in this class
+//
+// We have to deal with a number of threads:
+//
+// GSM:
+// * Assembles the pipeline
+// SocketTransportService
+// * Receives notification that ICE and DTLS have completed
+// * Processes incoming network data and passes it to the conduit
+// * Processes outgoing RTP and RTCP
+// MediaTrackGraph
+// * Receives outgoing data from the MediaTrackGraph
+// * Receives pull requests for more data from the
+// MediaTrackGraph
+// One or another GIPS threads
+// * Receives RTCP messages to send to the other side
+// * Processes video frames GIPS wants to render
+//
+// For a transmitting conduit, "output" is RTP and "input" is RTCP.
+// For a receiving conduit, "input" is RTP and "output" is RTCP.
+//
+
+class MediaPipeline : public sigslot::has_slots<> {
+ public:
+ enum class DirectionType { TRANSMIT, RECEIVE };
+ MediaPipeline(const std::string& aPc,
+ RefPtr<MediaTransportHandler> aTransportHandler,
+ DirectionType aDirection, RefPtr<AbstractThread> aCallThread,
+ RefPtr<nsISerialEventTarget> aStsThread,
+ RefPtr<MediaSessionConduit> aConduit);
+
+ void SetLevel(size_t aLevel) { mLevel = aLevel; }
+
+ // Main thread shutdown.
+ virtual void Shutdown();
+
+ void UpdateTransport_m(const std::string& aTransportId,
+ UniquePtr<MediaPipelineFilter>&& aFilter);
+
+ void UpdateTransport_s(const std::string& aTransportId,
+ UniquePtr<MediaPipelineFilter>&& aFilter);
+
+ virtual DirectionType Direction() const { return mDirection; }
+ size_t Level() const { return mLevel; }
+ virtual bool IsVideo() const = 0;
+
+ class RtpCSRCStats {
+ public:
+ // Gets an expiration time for CRC info given a reference time,
+ // this reference time would normally be the time of calling.
+ // This value can then be used to check if a RtpCSRCStats
+ // has expired via Expired(...)
+ static DOMHighResTimeStamp GetExpiryFromTime(
+ const DOMHighResTimeStamp aTime);
+
+ RtpCSRCStats(const uint32_t aCsrc, const DOMHighResTimeStamp aTime);
+ ~RtpCSRCStats() = default;
+ // Initialize a webidl representation suitable for adding to a report.
+ // This assumes that the webidl object is empty.
+ // @param aWebidlObj the webidl binding object to popluate
+ // @param aInboundRtpStreamId the associated RTCInboundRTPStreamStats.id
+ void GetWebidlInstance(dom::RTCRTPContributingSourceStats& aWebidlObj,
+ const nsString& aInboundRtpStreamId) const;
+ void SetTimestamp(const DOMHighResTimeStamp aTime) { mTimestamp = aTime; }
+ // Check if the RtpCSRCStats has expired, checks against a
+ // given expiration time.
+ bool Expired(const DOMHighResTimeStamp aExpiry) const {
+ return mTimestamp < aExpiry;
+ }
+
+ private:
+ static const double constexpr EXPIRY_TIME_MILLISECONDS = 10 * 1000;
+ const uint32_t mCsrc;
+ DOMHighResTimeStamp mTimestamp;
+ };
+
+ // Gets the gathered contributing source stats for the last expiration period.
+ // @param aId the stream id to use for populating inboundRtpStreamId field
+ // @param aArr the array to append the stats objects to
+ void GetContributingSourceStats(
+ const nsString& aInboundRtpStreamId,
+ FallibleTArray<dom::RTCRTPContributingSourceStats>& aArr) const;
+
+ int32_t RtpPacketsSent() const { return mRtpPacketsSent; }
+ int64_t RtpBytesSent() const { return mRtpBytesSent; }
+ int32_t RtcpPacketsSent() const { return mRtcpPacketsSent; }
+ int32_t RtpPacketsReceived() const { return mRtpPacketsReceived; }
+ int64_t RtpBytesReceived() const { return mRtpBytesReceived; }
+ int32_t RtcpPacketsReceived() const { return mRtcpPacketsReceived; }
+
+ const dom::RTCStatsTimestampMaker& GetTimestampMaker() const;
+
+ // Thread counting
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MediaPipeline)
+
+ protected:
+ virtual ~MediaPipeline();
+
+ // The transport is ready
+ virtual void TransportReady_s() {}
+
+ void IncrementRtpPacketsSent(const MediaPacket& aPacket);
+ void IncrementRtcpPacketsSent();
+ void IncrementRtpPacketsReceived(int aBytes);
+ void IncrementRtcpPacketsReceived();
+
+ virtual void SendPacket(MediaPacket&& packet);
+
+ // Process slots on transports
+ void RtpStateChange(const std::string& aTransportId, TransportLayer::State);
+ void RtcpStateChange(const std::string& aTransportId, TransportLayer::State);
+ virtual void CheckTransportStates();
+ void PacketReceived(const std::string& aTransportId,
+ const MediaPacket& packet);
+ void AlpnNegotiated(const std::string& aAlpn, bool aPrivacyRequested);
+
+ void RtpPacketReceived(const MediaPacket& packet);
+ void RtcpPacketReceived(const MediaPacket& packet);
+
+ void EncryptedPacketSending(const std::string& aTransportId,
+ const MediaPacket& aPacket);
+
+ void SetDescription_s(const std::string& description);
+
+ public:
+ const RefPtr<MediaSessionConduit> mConduit;
+ const DirectionType mDirection;
+
+ // Pointers to the threads we need. Initialized at creation
+ // and used all over the place.
+ const RefPtr<AbstractThread> mCallThread;
+ const RefPtr<nsISerialEventTarget> mStsThread;
+
+ protected:
+ // True if we should be actively transmitting or receiving data. Main thread
+ // only.
+ Mirror<bool> mActive;
+ Atomic<size_t> mLevel;
+ std::string mTransportId;
+ const RefPtr<MediaTransportHandler> mTransportHandler;
+
+ TransportLayer::State mRtpState = TransportLayer::TS_NONE;
+ TransportLayer::State mRtcpState = TransportLayer::TS_NONE;
+ bool mSignalsConnected = false;
+
+ // Only safe to access from STS thread.
+ int32_t mRtpPacketsSent;
+ int32_t mRtcpPacketsSent;
+ int32_t mRtpPacketsReceived;
+ int32_t mRtcpPacketsReceived;
+ int64_t mRtpBytesSent;
+ int64_t mRtpBytesReceived;
+
+ // Only safe to access from STS thread.
+ std::map<uint32_t, RtpCSRCStats> mCsrcStats;
+
+ // Written in c'tor. Read on STS and main thread.
+ const std::string mPc;
+
+ // String describing this MediaPipeline for logging purposes. Only safe to
+ // access from STS thread.
+ std::string mDescription;
+
+ // Written in c'tor, all following accesses are on the STS thread.
+ UniquePtr<MediaPipelineFilter> mFilter;
+ const UniquePtr<webrtc::RtpHeaderExtensionMap> mRtpHeaderExtensionMap;
+
+ RefPtr<PacketDumper> mPacketDumper;
+
+ MediaEventProducerExc<webrtc::RtpPacketReceived, webrtc::RTPHeader>
+ mRtpReceiveEvent;
+ MediaEventProducerExc<MediaPacket> mSenderRtcpReceiveEvent;
+ MediaEventProducerExc<MediaPacket> mReceiverRtcpReceiveEvent;
+
+ MediaEventListener mRtpSendEventListener;
+ MediaEventListener mSenderRtcpSendEventListener;
+ MediaEventListener mReceiverRtcpSendEventListener;
+
+ private:
+ bool IsRtp(const unsigned char* aData, size_t aLen) const;
+ // Must be called on the STS thread. Must be called after Shutdown().
+ void DetachTransport_s();
+};
+
+// A specialization of pipeline for reading from an input device
+// and transmitting to the network.
+class MediaPipelineTransmit
+ : public MediaPipeline,
+ public dom::PrincipalChangeObserver<dom::MediaStreamTrack> {
+ public:
+ // Set aRtcpTransport to nullptr to use rtcp-mux
+ MediaPipelineTransmit(const std::string& aPc,
+ RefPtr<MediaTransportHandler> aTransportHandler,
+ RefPtr<AbstractThread> aCallThread,
+ RefPtr<nsISerialEventTarget> aStsThread, bool aIsVideo,
+ RefPtr<MediaSessionConduit> aConduit);
+
+ void InitControl(MediaPipelineTransmitControlInterface* aControl);
+
+ void Shutdown() override;
+
+ bool Transmitting() const;
+
+ // written and used from MainThread
+ bool IsVideo() const override;
+
+ // When the principal of the domtrack changes, it calls through to here
+ // so that we can determine whether to enable track transmission.
+ // In cases where the peer isn't yet identified, we disable the pipeline (not
+ // the stream, that would potentially affect others), so that it sends
+ // black/silence. Once the peer is identified, re-enable those streams.
+ virtual void UpdateSinkIdentity(nsIPrincipal* aPrincipal,
+ const PeerIdentity* aSinkIdentity);
+
+ // for monitoring changes in track ownership
+ void PrincipalChanged(dom::MediaStreamTrack* aTrack) override;
+
+ // Override MediaPipeline::TransportReady_s.
+ void TransportReady_s() override;
+
+ // Replace a track with a different one.
+ nsresult SetTrack(const RefPtr<dom::MediaStreamTrack>& aDomTrack);
+
+ // Used to correlate stats
+ RefPtr<dom::MediaStreamTrack> GetTrack() const;
+
+ // For test use only. This allows a send track to be set without a
+ // corresponding dom track.
+ void SetSendTrackOverride(const RefPtr<ProcessedMediaTrack>& aSendTrack);
+
+ // Separate classes to allow ref counting
+ class PipelineListener;
+ class VideoFrameFeeder;
+
+ protected:
+ ~MediaPipelineTransmit();
+
+ // Updates mDescription (async) with information about the track we are
+ // transmitting.
+ std::string GenerateDescription() const;
+
+ // Sets up mSendPort and mSendTrack to feed mConduit if we are transmitting
+ // and have a dom track but no send track. Main thread only.
+ void UpdateSendState();
+
+ private:
+ WatchManager<MediaPipelineTransmit> mWatchManager;
+ const bool mIsVideo;
+ const RefPtr<PipelineListener> mListener;
+ RefPtr<AudioProxyThread> mAudioProcessing;
+ RefPtr<VideoFrameConverter> mConverter;
+ MediaEventListener mFrameListener;
+ Watchable<RefPtr<dom::MediaStreamTrack>> mDomTrack;
+ // Input port connecting mDomTrack's MediaTrack to mSendTrack.
+ RefPtr<MediaInputPort> mSendPort;
+ // The source track of the mSendTrack. Main thread only.
+ RefPtr<ProcessedMediaTrack> mSendPortSource;
+ // True if a parameter affecting mDescription has changed. To avoid updating
+ // the description unnecessarily. Main thread only.
+ bool mDescriptionInvalidated = true;
+ // Set true once we trigger the async removal of mSendTrack. Set false once
+ // the async removal is done. Main thread only.
+ bool mUnsettingSendTrack = false;
+ // MediaTrack that we send over the network. This allows changing mDomTrack.
+ // Because changing mSendTrack is async and can be racy (when changing from a
+ // track in one graph to a track in another graph), it is set very strictly.
+ // If mSendTrack is null it can be set by UpdateSendState().
+ // If it is non-null it can only be set to null, and only by the
+ // RemoveListener MozPromise handler, as seen in UpdateSendState.
+ RefPtr<ProcessedMediaTrack> mSendTrack;
+ // When this is set and we are active, this track will be used as mSendTrack.
+ // Allows unittests to insert a send track without requiring a dom track or a
+ // graph. Main thread only.
+ Watchable<RefPtr<ProcessedMediaTrack>> mSendTrackOverride;
+ // True when mSendTrack is set, not destroyed and mActive is true. mListener
+ // is attached to mSendTrack when this is true. Main thread only.
+ bool mTransmitting = false;
+};
+
+// A specialization of pipeline for reading from the network and
+// rendering media.
+class MediaPipelineReceive : public MediaPipeline {
+ public:
+ // Set aRtcpTransport to nullptr to use rtcp-mux
+ MediaPipelineReceive(const std::string& aPc,
+ RefPtr<MediaTransportHandler> aTransportHandler,
+ RefPtr<AbstractThread> aCallThread,
+ RefPtr<nsISerialEventTarget> aStsThread,
+ RefPtr<MediaSessionConduit> aConduit);
+
+ void InitControl(MediaPipelineReceiveControlInterface* aControl);
+
+ // Called when ALPN is negotiated and is requesting privacy, so receive
+ // pipelines do not enter data into the graph under a content principal.
+ virtual void OnPrivacyRequested_s() = 0;
+
+ // Called after privacy has been requested, with the updated private
+ // principal.
+ virtual void SetPrivatePrincipal(PrincipalHandle aHandle) = 0;
+
+ void Shutdown() override;
+
+ protected:
+ ~MediaPipelineReceive();
+
+ virtual void UpdateListener() = 0;
+
+ private:
+ WatchManager<MediaPipelineReceive> mWatchManager;
+};
+
+// A specialization of pipeline for reading from the network and
+// rendering audio.
+class MediaPipelineReceiveAudio : public MediaPipelineReceive {
+ public:
+ MediaPipelineReceiveAudio(const std::string& aPc,
+ RefPtr<MediaTransportHandler> aTransportHandler,
+ RefPtr<AbstractThread> aCallThread,
+ RefPtr<nsISerialEventTarget> aStsThread,
+ RefPtr<AudioSessionConduit> aConduit,
+ RefPtr<SourceMediaTrack> aSource,
+ TrackingId aTrackingId,
+ PrincipalHandle aPrincipalHandle,
+ PrincipalPrivacy aPrivacy);
+
+ void Shutdown() override;
+
+ bool IsVideo() const override { return false; }
+
+ void OnPrivacyRequested_s() override;
+ void SetPrivatePrincipal(PrincipalHandle aHandle) override;
+
+ private:
+ void UpdateListener() override;
+
+ // Separate class to allow ref counting
+ class PipelineListener;
+
+ const RefPtr<PipelineListener> mListener;
+};
+
+// A specialization of pipeline for reading from the network and
+// rendering video.
+class MediaPipelineReceiveVideo : public MediaPipelineReceive {
+ public:
+ MediaPipelineReceiveVideo(const std::string& aPc,
+ RefPtr<MediaTransportHandler> aTransportHandler,
+ RefPtr<AbstractThread> aCallThread,
+ RefPtr<nsISerialEventTarget> aStsThread,
+ RefPtr<VideoSessionConduit> aConduit,
+ RefPtr<SourceMediaTrack> aSource,
+ TrackingId aTrackingId,
+ PrincipalHandle aPrincipalHandle,
+ PrincipalPrivacy aPrivacy);
+
+ void Shutdown() override;
+
+ bool IsVideo() const override { return true; }
+
+ void OnPrivacyRequested_s() override;
+ void SetPrivatePrincipal(PrincipalHandle aHandle) override;
+
+ private:
+ void UpdateListener() override;
+
+ class PipelineRenderer;
+ friend class PipelineRenderer;
+
+ // Separate class to allow ref counting
+ class PipelineListener;
+
+ const RefPtr<PipelineRenderer> mRenderer;
+ const RefPtr<PipelineListener> mListener;
+};
+
+} // namespace mozilla
+#endif
diff --git a/dom/media/webrtc/transportbridge/MediaPipelineFilter.cpp b/dom/media/webrtc/transportbridge/MediaPipelineFilter.cpp
new file mode 100644
index 0000000000..1acb73e9f2
--- /dev/null
+++ b/dom/media/webrtc/transportbridge/MediaPipelineFilter.cpp
@@ -0,0 +1,153 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: softtabstop=2:shiftwidth=2:expandtab
+ * */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: bcampen@mozilla.com
+
+#include "MediaPipelineFilter.h"
+
+#include "api/rtp_headers.h"
+#include "api/rtp_parameters.h"
+#include "mozilla/Logging.h"
+
+// defined in MediaPipeline.cpp
+extern mozilla::LazyLogModule gMediaPipelineLog;
+
+#define DEBUG_LOG(x) MOZ_LOG(gMediaPipelineLog, LogLevel::Debug, x)
+
+namespace mozilla {
+MediaPipelineFilter::MediaPipelineFilter(
+ const std::vector<webrtc::RtpExtension>& aExtMap)
+ : mExtMap(aExtMap) {}
+
+void MediaPipelineFilter::SetRemoteMediaStreamId(
+ const Maybe<std::string>& aMid) {
+ if (aMid != mRemoteMid) {
+ DEBUG_LOG(("MediaPipelineFilter added new remote RTP MID: '%s'.",
+ aMid.valueOr("").c_str()));
+ mRemoteMid = aMid;
+ mRemoteMidBindings.clear();
+ }
+}
+
+bool MediaPipelineFilter::Filter(const webrtc::RTPHeader& header) {
+ DEBUG_LOG(("MediaPipelineFilter inspecting seq# %u SSRC: %u",
+ header.sequenceNumber, header.ssrc));
+
+ auto fromStreamId = [](const std::string& aId) {
+ return Maybe<std::string>(aId.empty() ? Nothing() : Some(aId));
+ };
+
+ //
+ // MID Based Filtering
+ //
+
+ const auto mid = fromStreamId(header.extension.mid);
+
+ // Check to see if a bound SSRC is moved to a new MID
+ if (mRemoteMidBindings.count(header.ssrc) == 1 && mid && mRemoteMid != mid) {
+ mRemoteMidBindings.erase(header.ssrc);
+ }
+ // Bind an SSRC if a matching MID is found
+ if (mid && mRemoteMid == mid) {
+ DEBUG_LOG(("MediaPipelineFilter learned SSRC: %u for MID: '%s'",
+ header.ssrc, mRemoteMid.value().c_str()));
+ mRemoteMidBindings.insert(header.ssrc);
+ }
+ // Check for matching MID
+ if (!mRemoteMidBindings.empty()) {
+ MOZ_ASSERT(mRemoteMid != Nothing());
+ if (mRemoteMidBindings.count(header.ssrc) == 1) {
+ DEBUG_LOG(
+ ("MediaPipelineFilter SSRC: %u matched for MID: '%s'."
+ " passing packet",
+ header.ssrc, mRemoteMid.value().c_str()));
+ return true;
+ }
+ DEBUG_LOG(
+ ("MediaPipelineFilter SSRC: %u did not match bound SSRC(s) for"
+ " MID: '%s'. ignoring packet",
+ header.ssrc, mRemoteMid.value().c_str()));
+ for (const uint32_t ssrc : mRemoteMidBindings) {
+ DEBUG_LOG(("MID %s is associated with SSRC: %u",
+ mRemoteMid.value().c_str(), ssrc));
+ }
+ return false;
+ }
+
+ //
+ // RTP-STREAM-ID based filtering (for tests only)
+ //
+
+ //
+ // Remote SSRC based filtering
+ //
+
+ if (remote_ssrc_set_.count(header.ssrc)) {
+ DEBUG_LOG(
+ ("MediaPipelineFilter SSRC: %u matched remote SSRC set."
+ " passing packet",
+ header.ssrc));
+ return true;
+ }
+ DEBUG_LOG(
+ ("MediaPipelineFilter SSRC: %u did not match any of %zu"
+ " remote SSRCS.",
+ header.ssrc, remote_ssrc_set_.size()));
+
+ //
+ // PT, payload type, last ditch effort filtering
+ //
+
+ if (payload_type_set_.count(header.payloadType)) {
+ DEBUG_LOG(
+ ("MediaPipelineFilter payload-type: %u matched %zu"
+ " unique payload type. learning ssrc. passing packet",
+ header.ssrc, remote_ssrc_set_.size()));
+ // Actual match. We need to update the ssrc map so we can route rtcp
+ // sender reports correctly (these use a different payload-type field)
+ AddRemoteSSRC(header.ssrc);
+ return true;
+ }
+ DEBUG_LOG(
+ ("MediaPipelineFilter payload-type: %u did not match any of %zu"
+ " unique payload-types.",
+ header.payloadType, payload_type_set_.size()));
+ DEBUG_LOG(
+ ("MediaPipelineFilter packet failed to match any criteria."
+ " ignoring packet"));
+ return false;
+}
+
+void MediaPipelineFilter::AddRemoteSSRC(uint32_t ssrc) {
+ remote_ssrc_set_.insert(ssrc);
+}
+
+void MediaPipelineFilter::AddUniquePT(uint8_t payload_type) {
+ payload_type_set_.insert(payload_type);
+}
+
+void MediaPipelineFilter::Update(const MediaPipelineFilter& filter_update) {
+ // We will not stomp the remote_ssrc_set_ if the update has no ssrcs,
+ // because we don't want to unlearn any remote ssrcs unless the other end
+ // has explicitly given us a new set.
+ if (!filter_update.remote_ssrc_set_.empty()) {
+ remote_ssrc_set_ = filter_update.remote_ssrc_set_;
+ }
+ // We don't want to overwrite the learned binding unless we have changed MIDs
+ // or the update contains a MID binding.
+ if (!filter_update.mRemoteMidBindings.empty() ||
+ (filter_update.mRemoteMid && filter_update.mRemoteMid != mRemoteMid)) {
+ mRemoteMid = filter_update.mRemoteMid;
+ mRemoteMidBindings = filter_update.mRemoteMidBindings;
+ }
+ payload_type_set_ = filter_update.payload_type_set_;
+
+ // Use extmapping from new filter
+ mExtMap = filter_update.mExtMap;
+}
+
+} // end namespace mozilla
diff --git a/dom/media/webrtc/transportbridge/MediaPipelineFilter.h b/dom/media/webrtc/transportbridge/MediaPipelineFilter.h
new file mode 100644
index 0000000000..9b40bceda8
--- /dev/null
+++ b/dom/media/webrtc/transportbridge/MediaPipelineFilter.h
@@ -0,0 +1,89 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: softtabstop=2:shiftwidth=2:expandtab
+ * */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: bcampen@mozilla.com
+
+#ifndef mediapipelinefilter_h__
+#define mediapipelinefilter_h__
+
+#include <cstddef>
+#include <stdint.h>
+#include <string>
+
+#include <set>
+#include <vector>
+
+#include "mozilla/Maybe.h"
+
+namespace webrtc {
+struct RTPHeader;
+struct RtpExtension;
+} // namespace webrtc
+
+namespace mozilla {
+
+// TODO @@NG update documentation after initial review
+
+// A class that handles the work of filtering RTP packets that arrive at a
+// MediaPipeline. This is primarily important for the use of BUNDLE (ie;
+// multiple m-lines share the same RTP stream). There are three ways that this
+// can work;
+//
+// 1) In our SDP, we include a media-level extmap parameter with a unique
+// integer of our choosing, with the hope that the other side will include
+// this value in a header in the first few RTP packets it sends us. This
+// allows us to perform correlation in cases where the other side has not
+// informed us of the ssrcs it will be sending (either because it did not
+// include them in its SDP, or their SDP has not arrived yet)
+// and also gives us the opportunity to learn SSRCs from packets so adorned.
+//
+// 2) If the remote endpoint includes SSRC media-level attributes in its SDP,
+// we can simply use this information to populate the filter. The only
+// shortcoming here is when RTP packets arrive before the answer does. See
+// above.
+//
+// 3) As a fallback, we can try to use payload type IDs to perform correlation,
+// but only when the type id is unique to this media section.
+// This too allows us to learn about SSRCs (mostly useful for filtering
+// sender reports later).
+class MediaPipelineFilter {
+ public:
+ MediaPipelineFilter() = default;
+ explicit MediaPipelineFilter(
+ const std::vector<webrtc::RtpExtension>& aExtMap);
+
+ // Checks whether this packet passes the filter, possibly updating the filter
+ // in the process (if the MID or payload types are used, they can teach
+ // the filter about ssrcs)
+ bool Filter(const webrtc::RTPHeader& header);
+
+ void AddRemoteSSRC(uint32_t ssrc);
+
+ void SetRemoteMediaStreamId(const Maybe<std::string>& aMid);
+
+ // When a payload type id is unique to our media section, add it here.
+ void AddUniquePT(uint8_t payload_type);
+
+ void Update(const MediaPipelineFilter& filter_update);
+
+ std::vector<webrtc::RtpExtension> GetExtmap() const { return mExtMap; }
+
+ private:
+ // The number of filters we manage here is quite small, so I am optimizing
+ // for readability.
+ std::set<uint32_t> remote_ssrc_set_;
+ std::set<uint8_t> payload_type_set_;
+ Maybe<std::string> mRemoteMid;
+ std::set<uint32_t> mRemoteMidBindings;
+ // RID extension can be set by tests and is sticky, the rest of
+ // the mapping is not.
+ std::vector<webrtc::RtpExtension> mExtMap;
+};
+
+} // end namespace mozilla
+
+#endif // mediapipelinefilter_h__
diff --git a/dom/media/webrtc/transportbridge/RtpLogger.cpp b/dom/media/webrtc/transportbridge/RtpLogger.cpp
new file mode 100644
index 0000000000..aac1fad197
--- /dev/null
+++ b/dom/media/webrtc/transportbridge/RtpLogger.cpp
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: nohlmeier@mozilla.com
+
+#include "RtpLogger.h"
+#include "mozilla/Logging.h"
+
+#include <ctime>
+#include <iomanip>
+#include <sstream>
+#ifdef _WIN32
+# include <time.h>
+# include <sys/timeb.h>
+#else
+# include <sys/time.h>
+#endif
+
+// Logging context
+using namespace mozilla;
+
+mozilla::LazyLogModule gRtpLoggerLog("RtpLogger");
+
+namespace mozilla {
+
+bool RtpLogger::IsPacketLoggingOn() {
+ return MOZ_LOG_TEST(gRtpLoggerLog, LogLevel::Debug);
+}
+
+void RtpLogger::LogPacket(const MediaPacket& packet, bool input,
+ std::string desc) {
+ if (MOZ_LOG_TEST(gRtpLoggerLog, LogLevel::Debug)) {
+ bool isRtp = (packet.type() == MediaPacket::RTP);
+ std::stringstream ss;
+ /* This creates text2pcap compatible format, e.g.:
+ * RTCP_PACKET O 10:36:26.864934 000000 80 c8 00 06 6d ...
+ */
+ ss << (input ? "I " : "O ");
+ std::time_t t = std::time(nullptr);
+ std::tm tm = *std::localtime(&t);
+ char buf[9];
+ if (0 < strftime(buf, sizeof(buf), "%H:%M:%S", &tm)) {
+ ss << buf;
+ }
+ ss << std::setfill('0');
+#ifdef _WIN32
+ struct timeb tb;
+ ftime(&tb);
+ ss << "." << (tb.millitm) << " ";
+#else
+ struct timeval tv;
+ gettimeofday(&tv, NULL);
+ ss << "." << (tv.tv_usec) << " ";
+#endif
+ ss << " 000000";
+ ss << std::hex << std::setfill('0');
+ for (size_t i = 0; i < packet.len(); ++i) {
+ ss << " " << std::setw(2) << (int)packet.data()[i];
+ }
+ MOZ_LOG(gRtpLoggerLog, LogLevel::Debug,
+ ("%s%s%s", desc.c_str(), (isRtp ? " RTP_PACKET " : " RTCP_PACKET "),
+ ss.str().c_str()));
+ }
+}
+
+} // namespace mozilla
diff --git a/dom/media/webrtc/transportbridge/RtpLogger.h b/dom/media/webrtc/transportbridge/RtpLogger.h
new file mode 100644
index 0000000000..fcfaede6e2
--- /dev/null
+++ b/dom/media/webrtc/transportbridge/RtpLogger.h
@@ -0,0 +1,28 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Original author: nohlmeier@mozilla.com
+
+#ifndef rtplogger_h__
+#define rtplogger_h__
+
+#include "transport/mediapacket.h"
+
+namespace mozilla {
+
+/* This class logs RTP and RTCP packets in hex in a format compatible to
+ * text2pcap.
+ * Example to convert the MOZ log file into a PCAP file:
+ * egrep '(RTP_PACKET|RTCP_PACKET)' moz.log | \
+ * text2pcap -D -n -l 1 -i 17 -u 1234,1235 -t '%H:%M:%S.' - rtp.pcap
+ */
+class RtpLogger {
+ public:
+ static bool IsPacketLoggingOn();
+ static void LogPacket(const MediaPacket& packet, bool input,
+ std::string desc);
+};
+
+} // namespace mozilla
+#endif
diff --git a/dom/media/webrtc/transportbridge/moz.build b/dom/media/webrtc/transportbridge/moz.build
new file mode 100644
index 0000000000..290dd26a0a
--- /dev/null
+++ b/dom/media/webrtc/transportbridge/moz.build
@@ -0,0 +1,27 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+include("/dom/media/webrtc/third_party_build/webrtc.mozbuild")
+
+LOCAL_INCLUDES += [
+ "!/ipc/ipdl/_ipdlheaders",
+ "/dom/media",
+ "/dom/media/webrtc",
+ "/ipc/chromium/src",
+ "/media/libyuv/libyuv/include",
+ "/media/webrtc",
+ "/third_party/libsrtp/src/crypto/include",
+ "/third_party/libsrtp/src/include",
+ "/third_party/libwebrtc",
+ "/third_party/libwebrtc/third_party/abseil-cpp",
+]
+
+UNIFIED_SOURCES += [
+ "MediaPipeline.cpp",
+ "MediaPipelineFilter.cpp",
+ "RtpLogger.cpp",
+]
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/webspeech/moz.build b/dom/media/webspeech/moz.build
new file mode 100644
index 0000000000..26856a0598
--- /dev/null
+++ b/dom/media/webspeech/moz.build
@@ -0,0 +1,12 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS = ["synth"]
+
+if CONFIG["MOZ_WEBSPEECH"]:
+ DIRS += ["recognition"]
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "Web Speech")
diff --git a/dom/media/webspeech/recognition/OnlineSpeechRecognitionService.cpp b/dom/media/webspeech/recognition/OnlineSpeechRecognitionService.cpp
new file mode 100644
index 0000000000..e68ccc417e
--- /dev/null
+++ b/dom/media/webspeech/recognition/OnlineSpeechRecognitionService.cpp
@@ -0,0 +1,462 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsThreadUtils.h"
+#include "nsXPCOMCIDInternal.h"
+#include "OnlineSpeechRecognitionService.h"
+#include "nsIFile.h"
+#include "SpeechGrammar.h"
+#include "SpeechRecognition.h"
+#include "SpeechRecognitionAlternative.h"
+#include "SpeechRecognitionResult.h"
+#include "SpeechRecognitionResultList.h"
+#include "nsIObserverService.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/ScopeExit.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/Services.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsDirectoryServiceUtils.h"
+#include "nsNetUtil.h"
+#include "nsContentUtils.h"
+#include "nsIChannel.h"
+#include "nsIHttpChannel.h"
+#include "nsIPrincipal.h"
+#include "nsIStreamListener.h"
+#include "nsIUploadChannel2.h"
+#include "mozilla/dom/ClientIPCTypes.h"
+#include "nsStringStream.h"
+#include "nsIOutputStream.h"
+#include "nsStreamUtils.h"
+#include "OpusTrackEncoder.h"
+#include "OggWriter.h"
+#include "nsIClassOfService.h"
+#include <json/json.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+namespace mozilla {
+
+using namespace dom;
+
+#define PREFERENCE_DEFAULT_RECOGNITION_ENDPOINT \
+ "media.webspeech.service.endpoint"
+#define DEFAULT_RECOGNITION_ENDPOINT "https://speaktome-2.services.mozilla.com/"
+#define MAX_LISTENING_TIME_MS 10000
+
+NS_IMPL_ISUPPORTS(OnlineSpeechRecognitionService, nsISpeechRecognitionService,
+ nsIStreamListener)
+
+NS_IMETHODIMP
+OnlineSpeechRecognitionService::OnStartRequest(nsIRequest* aRequest) {
+ MOZ_ASSERT(NS_IsMainThread());
+ return NS_OK;
+}
+
+static nsresult AssignResponseToBuffer(nsIInputStream* aIn, void* aClosure,
+ const char* aFromRawSegment,
+ uint32_t aToOffset, uint32_t aCount,
+ uint32_t* aWriteCount) {
+ nsCString* buf = static_cast<nsCString*>(aClosure);
+ buf->Append(aFromRawSegment, aCount);
+ *aWriteCount = aCount;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+OnlineSpeechRecognitionService::OnDataAvailable(nsIRequest* aRequest,
+ nsIInputStream* aInputStream,
+ uint64_t aOffset,
+ uint32_t aCount) {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsresult rv;
+ uint32_t readCount;
+ rv = aInputStream->ReadSegments(AssignResponseToBuffer, &mBuf, aCount,
+ &readCount);
+ NS_ENSURE_SUCCESS(rv, rv);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+OnlineSpeechRecognitionService::OnStopRequest(nsIRequest* aRequest,
+ nsresult aStatusCode) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ auto clearBuf = MakeScopeExit([&] { mBuf.Truncate(); });
+
+ if (mAborted) {
+ return NS_OK;
+ }
+
+ bool success;
+ float confidence = 0;
+ Json::Value root;
+ Json::CharReaderBuilder builder;
+ bool parsingSuccessful;
+ nsAutoCString result;
+ nsAutoCString hypoValue;
+ nsAutoCString errorMsg;
+ SpeechRecognitionErrorCode errorCode;
+
+ SR_LOG("STT Result: %s", mBuf.get());
+
+ if (NS_FAILED(aStatusCode)) {
+ success = false;
+ errorMsg.AssignLiteral("Error connecting to the service.");
+ errorCode = SpeechRecognitionErrorCode::Network;
+ } else {
+ success = true;
+ UniquePtr<Json::CharReader> const reader(builder.newCharReader());
+ parsingSuccessful =
+ reader->parse(mBuf.BeginReading(), mBuf.EndReading(), &root, nullptr);
+ if (!parsingSuccessful) {
+ // there's an internal server error
+ success = false;
+ errorMsg.AssignLiteral("Internal server error");
+ errorCode = SpeechRecognitionErrorCode::Network;
+ } else {
+ result.Assign(root.get("status", "error").asString().c_str());
+ if (result.EqualsLiteral("ok")) {
+ // ok, we have a result
+ if (!root["data"].empty()) {
+ hypoValue.Assign(root["data"][0].get("text", "").asString().c_str());
+ confidence = root["data"][0].get("confidence", "0").asFloat();
+ } else {
+ success = false;
+ errorMsg.AssignLiteral("Error reading result data.");
+ errorCode = SpeechRecognitionErrorCode::Network;
+ }
+ } else {
+ success = false;
+ errorMsg.Assign(root.get("message", "").asString().c_str());
+ errorCode = SpeechRecognitionErrorCode::No_speech;
+ }
+ }
+ }
+
+ if (!success) {
+ mRecognition->DispatchError(
+ SpeechRecognition::EVENT_RECOGNITIONSERVICE_ERROR, errorCode, errorMsg);
+ } else {
+ // Declare javascript result events
+ RefPtr<SpeechEvent> event = new SpeechEvent(
+ mRecognition, SpeechRecognition::EVENT_RECOGNITIONSERVICE_FINAL_RESULT);
+ SpeechRecognitionResultList* resultList =
+ new SpeechRecognitionResultList(mRecognition);
+ SpeechRecognitionResult* result = new SpeechRecognitionResult(mRecognition);
+
+ if (mRecognition->MaxAlternatives() > 0) {
+ SpeechRecognitionAlternative* alternative =
+ new SpeechRecognitionAlternative(mRecognition);
+
+ alternative->mTranscript = NS_ConvertUTF8toUTF16(hypoValue);
+ alternative->mConfidence = confidence;
+
+ result->mItems.AppendElement(alternative);
+ }
+ resultList->mItems.AppendElement(result);
+
+ event->mRecognitionResultList = resultList;
+ NS_DispatchToMainThread(event);
+ }
+
+ return NS_OK;
+}
+
+OnlineSpeechRecognitionService::OnlineSpeechRecognitionService() = default;
+OnlineSpeechRecognitionService::~OnlineSpeechRecognitionService() = default;
+
+NS_IMETHODIMP
+OnlineSpeechRecognitionService::Initialize(
+ WeakPtr<SpeechRecognition> aSpeechRecognition) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mWriter = MakeUnique<OggWriter>();
+ mRecognition = new nsMainThreadPtrHolder<SpeechRecognition>(
+ "OnlineSpeechRecognitionService::mRecognition", aSpeechRecognition);
+ mEncodeTaskQueue = mRecognition->GetTaskQueueForEncoding();
+ MOZ_ASSERT(mEncodeTaskQueue);
+ return NS_OK;
+}
+
+void OnlineSpeechRecognitionService::EncoderFinished() {
+ MOZ_ASSERT(!NS_IsMainThread());
+ MOZ_ASSERT(mEncodedAudioQueue.IsFinished());
+
+ while (RefPtr<EncodedFrame> frame = mEncodedAudioQueue.PopFront()) {
+ AutoTArray<RefPtr<EncodedFrame>, 1> frames({frame});
+ DebugOnly<nsresult> rv =
+ mWriter->WriteEncodedTrack(frames, mEncodedAudioQueue.AtEndOfStream()
+ ? ContainerWriter::END_OF_STREAM
+ : 0);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ }
+
+ mWriter->GetContainerData(&mEncodedData, ContainerWriter::FLUSH_NEEDED);
+ MOZ_ASSERT(mWriter->IsWritingComplete());
+
+ NS_DispatchToMainThread(
+ NewRunnableMethod("OnlineSpeechRecognitionService::DoSTT", this,
+ &OnlineSpeechRecognitionService::DoSTT));
+}
+
+void OnlineSpeechRecognitionService::EncoderInitialized() {
+ MOZ_ASSERT(!NS_IsMainThread());
+ AutoTArray<RefPtr<TrackMetadataBase>, 1> metadata;
+ metadata.AppendElement(mAudioEncoder->GetMetadata());
+ if (metadata[0]->GetKind() != TrackMetadataBase::METADATA_OPUS) {
+ SR_LOG("wrong meta data type!");
+ MOZ_ASSERT_UNREACHABLE();
+ }
+
+ nsresult rv = mWriter->SetMetadata(metadata);
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+
+ rv = mWriter->GetContainerData(&mEncodedData, ContainerWriter::GET_HEADER);
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+
+ Unused << rv;
+}
+
+void OnlineSpeechRecognitionService::EncoderError() {
+ MOZ_ASSERT(!NS_IsMainThread());
+ SR_LOG("Error encoding frames.");
+ mEncodedData.Clear();
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "SpeechRecognition::DispatchError",
+ [this, self = RefPtr<OnlineSpeechRecognitionService>(this)]() {
+ if (!mRecognition) {
+ return;
+ }
+ mRecognition->DispatchError(
+ SpeechRecognition::EVENT_RECOGNITIONSERVICE_ERROR,
+ SpeechRecognitionErrorCode::Audio_capture, "Encoder error");
+ }));
+}
+
+NS_IMETHODIMP
+OnlineSpeechRecognitionService::ProcessAudioSegment(AudioSegment* aAudioSegment,
+ int32_t aSampleRate) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ int64_t duration = aAudioSegment->GetDuration();
+ if (duration <= 0) {
+ return NS_OK;
+ }
+
+ if (!mAudioEncoder) {
+ mSpeechEncoderListener = new SpeechEncoderListener(this);
+ mAudioEncoder =
+ MakeUnique<OpusTrackEncoder>(aSampleRate, mEncodedAudioQueue);
+ RefPtr<AbstractThread> mEncoderThread = AbstractThread::GetCurrent();
+ mAudioEncoder->SetWorkerThread(mEncoderThread);
+ mAudioEncoder->RegisterListener(mSpeechEncoderListener);
+ }
+
+ mAudioEncoder->AppendAudioSegment(std::move(*aAudioSegment));
+
+ TimeStamp now = TimeStamp::Now();
+ if (mFirstIteration.IsNull()) {
+ mFirstIteration = now;
+ }
+
+ if ((now - mFirstIteration).ToMilliseconds() >= MAX_LISTENING_TIME_MS) {
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "SpeechRecognition::Stop",
+ [this, self = RefPtr<OnlineSpeechRecognitionService>(this)]() {
+ if (!mRecognition) {
+ return;
+ }
+ mRecognition->Stop();
+ }));
+
+ return NS_OK;
+ }
+
+ return NS_OK;
+}
+
+void OnlineSpeechRecognitionService::DoSTT() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (mAborted) {
+ return;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsIChannel> chan;
+ nsCOMPtr<nsIURI> uri;
+ nsAutoCString speechRecognitionEndpoint;
+ nsAutoCString prefEndpoint;
+ nsAutoString language;
+
+ Preferences::GetCString(PREFERENCE_DEFAULT_RECOGNITION_ENDPOINT,
+ prefEndpoint);
+
+ if (!prefEndpoint.IsEmpty()) {
+ speechRecognitionEndpoint = prefEndpoint;
+ } else {
+ speechRecognitionEndpoint = DEFAULT_RECOGNITION_ENDPOINT;
+ }
+
+ rv = NS_NewURI(getter_AddRefs(uri), speechRecognitionEndpoint, nullptr,
+ nullptr);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ mRecognition->DispatchError(
+ SpeechRecognition::EVENT_RECOGNITIONSERVICE_ERROR,
+ SpeechRecognitionErrorCode::Network, "Unknown URI");
+ return;
+ }
+
+ nsSecurityFlags secFlags = nsILoadInfo::SEC_REQUIRE_CORS_INHERITS_SEC_CONTEXT;
+ nsLoadFlags loadFlags =
+ nsIRequest::LOAD_NORMAL | nsIChannel::LOAD_BYPASS_SERVICE_WORKER;
+ nsContentPolicyType contentPolicy = nsIContentPolicy::TYPE_OTHER;
+
+ nsPIDOMWindowInner* window = mRecognition->GetOwner();
+ if (NS_WARN_IF(!window)) {
+ mRecognition->DispatchError(
+ SpeechRecognition::EVENT_RECOGNITIONSERVICE_ERROR,
+ SpeechRecognitionErrorCode::Aborted, "No window");
+ return;
+ }
+
+ Document* doc = window->GetExtantDoc();
+ if (NS_WARN_IF(!doc)) {
+ mRecognition->DispatchError(
+ SpeechRecognition::EVENT_RECOGNITIONSERVICE_ERROR,
+ SpeechRecognitionErrorCode::Aborted, "No document");
+ }
+ rv = NS_NewChannel(getter_AddRefs(chan), uri, doc->NodePrincipal(), secFlags,
+ contentPolicy, nullptr, nullptr, nullptr, nullptr,
+ loadFlags);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ mRecognition->DispatchError(
+ SpeechRecognition::EVENT_RECOGNITIONSERVICE_ERROR,
+ SpeechRecognitionErrorCode::Network, "Failed to open channel");
+ return;
+ }
+
+ nsCOMPtr<nsIHttpChannel> httpChan = do_QueryInterface(chan);
+ if (httpChan) {
+ rv = httpChan->SetRequestMethod("POST"_ns);
+ MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
+ }
+
+ if (httpChan) {
+ mRecognition->GetLang(language);
+ // Accept-Language-STT is a custom header of our backend server used to set
+ // the language of the speech sample being submitted by the client
+ rv = httpChan->SetRequestHeader("Accept-Language-STT"_ns,
+ NS_ConvertUTF16toUTF8(language), false);
+ MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
+ // Tell the server to not store the transcription by default
+ rv = httpChan->SetRequestHeader("Store-Transcription"_ns, "0"_ns, false);
+ MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
+ // Tell the server to not store the sample by default
+ rv = httpChan->SetRequestHeader("Store-Sample"_ns, "0"_ns, false);
+ MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
+ // Set the product tag as teh web speech api
+ rv = httpChan->SetRequestHeader("Product-Tag"_ns, "wsa"_ns, false);
+ MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
+ }
+
+ nsCOMPtr<nsIClassOfService> cos(do_QueryInterface(chan));
+ if (cos) {
+ cos->AddClassFlags(nsIClassOfService::UrgentStart);
+ }
+
+ nsCOMPtr<nsIUploadChannel2> uploadChan = do_QueryInterface(chan);
+ if (uploadChan) {
+ nsCOMPtr<nsIInputStream> bodyStream;
+ uint32_t length = 0;
+ for (const nsTArray<uint8_t>& chunk : mEncodedData) {
+ length += chunk.Length();
+ }
+
+ nsTArray<uint8_t> audio;
+ if (!audio.SetCapacity(length, fallible)) {
+ mRecognition->DispatchError(
+ SpeechRecognition::EVENT_RECOGNITIONSERVICE_ERROR,
+ SpeechRecognitionErrorCode::Audio_capture, "Allocation error");
+ return;
+ }
+
+ for (const nsTArray<uint8_t>& chunk : mEncodedData) {
+ audio.AppendElements(chunk);
+ }
+
+ mEncodedData.Clear();
+
+ rv = NS_NewByteInputStream(getter_AddRefs(bodyStream), std::move(audio));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ mRecognition->DispatchError(
+ SpeechRecognition::EVENT_RECOGNITIONSERVICE_ERROR,
+ SpeechRecognitionErrorCode::Network, "Failed to open stream");
+ return;
+ }
+ if (bodyStream) {
+ rv = uploadChan->ExplicitSetUploadStream(bodyStream, "audio/ogg"_ns,
+ length, "POST"_ns, false);
+ MOZ_RELEASE_ASSERT(NS_SUCCEEDED(rv));
+ }
+ }
+
+ rv = chan->AsyncOpen(this);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ mRecognition->DispatchError(
+ SpeechRecognition::EVENT_RECOGNITIONSERVICE_ERROR,
+ SpeechRecognitionErrorCode::Network, "Internal server error");
+ }
+}
+
+NS_IMETHODIMP
+OnlineSpeechRecognitionService::SoundEnd() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mEncodeTaskQueue) {
+ // Not initialized
+ return NS_OK;
+ }
+
+ nsresult rv = mEncodeTaskQueue->Dispatch(NS_NewRunnableFunction(
+ "OnlineSpeechRecognitionService::SoundEnd",
+ [this, self = RefPtr<OnlineSpeechRecognitionService>(this)]() {
+ if (mAudioEncoder) {
+ mAudioEncoder->NotifyEndOfStream();
+ mAudioEncoder->UnregisterListener(mSpeechEncoderListener);
+ mSpeechEncoderListener = nullptr;
+ mAudioEncoder = nullptr;
+ EncoderFinished();
+ }
+ }));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+
+ mEncodeTaskQueue = nullptr;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+OnlineSpeechRecognitionService::ValidateAndSetGrammarList(
+ SpeechGrammar* aSpeechGrammar,
+ nsISpeechGrammarCompilationCallback* aCallback) {
+ // This is an online LVCSR (STT) service,
+ // so we don't need to set a grammar
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+OnlineSpeechRecognitionService::Abort() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mAborted) {
+ return NS_OK;
+ }
+ mAborted = true;
+ return SoundEnd();
+}
+} // namespace mozilla
diff --git a/dom/media/webspeech/recognition/OnlineSpeechRecognitionService.h b/dom/media/webspeech/recognition/OnlineSpeechRecognitionService.h
new file mode 100644
index 0000000000..c049e5046a
--- /dev/null
+++ b/dom/media/webspeech/recognition/OnlineSpeechRecognitionService.h
@@ -0,0 +1,132 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_OnlineRecognitionService_h
+#define mozilla_dom_OnlineRecognitionService_h
+
+#include "nsCOMPtr.h"
+#include "nsTArray.h"
+#include "nsISpeechRecognitionService.h"
+#include "speex/speex_resampler.h"
+#include "nsIStreamListener.h"
+#include "OpusTrackEncoder.h"
+#include "ContainerWriter.h"
+
+#define NS_ONLINE_SPEECH_RECOGNITION_SERVICE_CID \
+ {0x0ff5ce56, \
+ 0x5b09, \
+ 0x4db8, \
+ {0xad, 0xc6, 0x82, 0x66, 0xaf, 0x95, 0xf8, 0x64}};
+
+namespace mozilla {
+
+namespace ipc {
+class PrincipalInfo;
+} // namespace ipc
+
+/**
+ * Online implementation of the nsISpeechRecognitionService interface
+ */
+class OnlineSpeechRecognitionService : public nsISpeechRecognitionService,
+ public nsIStreamListener {
+ public:
+ // Add XPCOM glue code
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSISPEECHRECOGNITIONSERVICE
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+
+ /**
+ * Listener responsible for handling the events raised by the TrackEncoder
+ */
+ class SpeechEncoderListener : public TrackEncoderListener {
+ public:
+ explicit SpeechEncoderListener(OnlineSpeechRecognitionService* aService)
+ : mService(aService), mOwningThread(AbstractThread::GetCurrent()) {}
+
+ void Started(TrackEncoder* aEncoder) override {}
+
+ void Initialized(TrackEncoder* aEncoder) override {
+ MOZ_ASSERT(mOwningThread->IsCurrentThreadIn());
+ mService->EncoderInitialized();
+ }
+
+ void Error(TrackEncoder* aEncoder) override {
+ MOZ_ASSERT(mOwningThread->IsCurrentThreadIn());
+ mService->EncoderError();
+ }
+
+ private:
+ const RefPtr<OnlineSpeechRecognitionService> mService;
+ const RefPtr<AbstractThread> mOwningThread;
+ };
+
+ /**
+ * Default constructs a OnlineSpeechRecognitionService
+ */
+ OnlineSpeechRecognitionService();
+
+ /**
+ * Called by SpeechEncoderListener when the AudioTrackEncoder has been
+ * initialized.
+ */
+ void EncoderInitialized();
+
+ /**
+ * Called after the AudioTrackEncoder has encoded all data for us to wrap in a
+ * container and pass along.
+ */
+ void EncoderFinished();
+
+ /**
+ * Called by SpeechEncoderListener when the AudioTrackEncoder has
+ * encountered an error.
+ */
+ void EncoderError();
+
+ private:
+ /**
+ * Private destructor to prevent bypassing of reference counting
+ */
+ virtual ~OnlineSpeechRecognitionService();
+
+ /** The associated SpeechRecognition */
+ nsMainThreadPtrHandle<dom::SpeechRecognition> mRecognition;
+
+ /**
+ * Builds a mock SpeechRecognitionResultList
+ */
+ dom::SpeechRecognitionResultList* BuildMockResultList();
+
+ /**
+ * Method responsible for uploading the audio to the remote endpoint
+ */
+ void DoSTT();
+
+ // Encoded and packaged ogg audio data
+ nsTArray<nsTArray<uint8_t>> mEncodedData;
+ // Member responsible for holding a reference to the TrackEncoderListener
+ RefPtr<SpeechEncoderListener> mSpeechEncoderListener;
+ // MediaQueue fed encoded data by mAudioEncoder
+ MediaQueue<EncodedFrame> mEncodedAudioQueue;
+ // Encoder responsible for encoding the frames from pcm to opus which is the
+ // format supported by our backend
+ UniquePtr<AudioTrackEncoder> mAudioEncoder;
+ // Object responsible for wrapping the opus frames into an ogg container
+ UniquePtr<ContainerWriter> mWriter;
+ // Member responsible for storing the json string returned by the endpoint
+ nsCString mBuf;
+ // Used to calculate a ceiling on the time spent listening.
+ TimeStamp mFirstIteration;
+ // flag responsible to control if the user choose to abort
+ bool mAborted = false;
+ // reference to the audio encoder queue
+ RefPtr<TaskQueue> mEncodeTaskQueue;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webspeech/recognition/SpeechGrammar.cpp b/dom/media/webspeech/recognition/SpeechGrammar.cpp
new file mode 100644
index 0000000000..de6e9fa30f
--- /dev/null
+++ b/dom/media/webspeech/recognition/SpeechGrammar.cpp
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SpeechGrammar.h"
+
+#include "mozilla/ErrorResult.h"
+#include "mozilla/dom/SpeechGrammarBinding.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(SpeechGrammar, mParent)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(SpeechGrammar)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(SpeechGrammar)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SpeechGrammar)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+SpeechGrammar::SpeechGrammar(nsISupports* aParent) : mParent(aParent) {}
+
+SpeechGrammar::~SpeechGrammar() = default;
+
+already_AddRefed<SpeechGrammar> SpeechGrammar::Constructor(
+ const GlobalObject& aGlobal) {
+ RefPtr<SpeechGrammar> speechGrammar =
+ new SpeechGrammar(aGlobal.GetAsSupports());
+ return speechGrammar.forget();
+}
+
+nsISupports* SpeechGrammar::GetParentObject() const { return mParent; }
+
+JSObject* SpeechGrammar::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return SpeechGrammar_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void SpeechGrammar::GetSrc(nsString& aRetVal, ErrorResult& aRv) const {
+ aRetVal = mSrc;
+}
+
+void SpeechGrammar::SetSrc(const nsAString& aArg, ErrorResult& aRv) {
+ mSrc = aArg;
+}
+
+float SpeechGrammar::GetWeight(ErrorResult& aRv) const {
+ aRv.Throw(NS_ERROR_NOT_IMPLEMENTED);
+ return 0;
+}
+
+void SpeechGrammar::SetWeight(float aArg, ErrorResult& aRv) {
+ aRv.Throw(NS_ERROR_NOT_IMPLEMENTED);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webspeech/recognition/SpeechGrammar.h b/dom/media/webspeech/recognition/SpeechGrammar.h
new file mode 100644
index 0000000000..0dee1e9792
--- /dev/null
+++ b/dom/media/webspeech/recognition/SpeechGrammar.h
@@ -0,0 +1,64 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_SpeechGrammar_h
+#define mozilla_dom_SpeechGrammar_h
+
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsString.h"
+#include "nsWrapperCache.h"
+#include "js/TypeDecls.h"
+
+#include "mozilla/Attributes.h"
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+
+class GlobalObject;
+
+class SpeechGrammar final : public nsISupports, public nsWrapperCache {
+ public:
+ explicit SpeechGrammar(nsISupports* aParent);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(SpeechGrammar)
+
+ nsISupports* GetParentObject() const;
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ static already_AddRefed<SpeechGrammar> Constructor(
+ const GlobalObject& aGlobal);
+
+ static already_AddRefed<SpeechGrammar> WebkitSpeechGrammar(
+ const GlobalObject& aGlobal, ErrorResult& aRv) {
+ return Constructor(aGlobal);
+ }
+
+ void GetSrc(nsString& aRetVal, ErrorResult& aRv) const;
+
+ void SetSrc(const nsAString& aArg, ErrorResult& aRv);
+
+ float GetWeight(ErrorResult& aRv) const;
+
+ void SetWeight(float aArg, ErrorResult& aRv);
+
+ private:
+ ~SpeechGrammar();
+
+ nsCOMPtr<nsISupports> mParent;
+
+ nsString mSrc;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webspeech/recognition/SpeechGrammarList.cpp b/dom/media/webspeech/recognition/SpeechGrammarList.cpp
new file mode 100644
index 0000000000..4317452057
--- /dev/null
+++ b/dom/media/webspeech/recognition/SpeechGrammarList.cpp
@@ -0,0 +1,76 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SpeechGrammarList.h"
+
+#include "mozilla/dom/SpeechGrammar.h"
+#include "mozilla/dom/SpeechGrammarListBinding.h"
+#include "mozilla/ErrorResult.h"
+#include "nsCOMPtr.h"
+#include "SpeechRecognition.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(SpeechGrammarList, mParent, mItems)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(SpeechGrammarList)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(SpeechGrammarList)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SpeechGrammarList)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+SpeechGrammarList::SpeechGrammarList(nsISupports* aParent) : mParent(aParent) {}
+
+SpeechGrammarList::~SpeechGrammarList() = default;
+
+already_AddRefed<SpeechGrammarList> SpeechGrammarList::Constructor(
+ const GlobalObject& aGlobal) {
+ RefPtr<SpeechGrammarList> speechGrammarList =
+ new SpeechGrammarList(aGlobal.GetAsSupports());
+ return speechGrammarList.forget();
+}
+
+JSObject* SpeechGrammarList::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return SpeechGrammarList_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsISupports* SpeechGrammarList::GetParentObject() const { return mParent; }
+
+uint32_t SpeechGrammarList::Length() const { return mItems.Length(); }
+
+already_AddRefed<SpeechGrammar> SpeechGrammarList::Item(uint32_t aIndex,
+ ErrorResult& aRv) {
+ RefPtr<SpeechGrammar> result = mItems.ElementAt(aIndex);
+ return result.forget();
+}
+
+void SpeechGrammarList::AddFromURI(const nsAString& aSrc,
+ const Optional<float>& aWeight,
+ ErrorResult& aRv) {
+ aRv.Throw(NS_ERROR_NOT_IMPLEMENTED);
+}
+
+void SpeechGrammarList::AddFromString(const nsAString& aString,
+ const Optional<float>& aWeight,
+ ErrorResult& aRv) {
+ SpeechGrammar* speechGrammar = new SpeechGrammar(mParent);
+ speechGrammar->SetSrc(aString, aRv);
+ mItems.AppendElement(speechGrammar);
+}
+
+already_AddRefed<SpeechGrammar> SpeechGrammarList::IndexedGetter(
+ uint32_t aIndex, bool& aPresent, ErrorResult& aRv) {
+ if (aIndex >= Length()) {
+ aPresent = false;
+ return nullptr;
+ }
+ ErrorResult rv;
+ aPresent = true;
+ return Item(aIndex, rv);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webspeech/recognition/SpeechGrammarList.h b/dom/media/webspeech/recognition/SpeechGrammarList.h
new file mode 100644
index 0000000000..7f1e09cd9e
--- /dev/null
+++ b/dom/media/webspeech/recognition/SpeechGrammarList.h
@@ -0,0 +1,73 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_SpeechGrammarList_h
+#define mozilla_dom_SpeechGrammarList_h
+
+#include "mozilla/Attributes.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsTArray.h"
+#include "nsWrapperCache.h"
+
+struct JSContext;
+
+namespace mozilla {
+
+class ErrorResult;
+
+namespace dom {
+
+class GlobalObject;
+class SpeechGrammar;
+template <typename>
+class Optional;
+
+class SpeechGrammarList final : public nsISupports, public nsWrapperCache {
+ public:
+ explicit SpeechGrammarList(nsISupports* aParent);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(SpeechGrammarList)
+
+ static already_AddRefed<SpeechGrammarList> Constructor(
+ const GlobalObject& aGlobal);
+
+ static already_AddRefed<SpeechGrammarList> WebkitSpeechGrammarList(
+ const GlobalObject& aGlobal, ErrorResult& aRv) {
+ return Constructor(aGlobal);
+ }
+
+ nsISupports* GetParentObject() const;
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ uint32_t Length() const;
+
+ already_AddRefed<SpeechGrammar> Item(uint32_t aIndex, ErrorResult& aRv);
+
+ void AddFromURI(const nsAString& aSrc, const Optional<float>& aWeight,
+ ErrorResult& aRv);
+
+ void AddFromString(const nsAString& aString, const Optional<float>& aWeight,
+ ErrorResult& aRv);
+
+ already_AddRefed<SpeechGrammar> IndexedGetter(uint32_t aIndex, bool& aPresent,
+ ErrorResult& aRv);
+
+ private:
+ ~SpeechGrammarList();
+
+ nsCOMPtr<nsISupports> mParent;
+
+ nsTArray<RefPtr<SpeechGrammar>> mItems;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webspeech/recognition/SpeechRecognition.cpp b/dom/media/webspeech/recognition/SpeechRecognition.cpp
new file mode 100644
index 0000000000..e3bf531218
--- /dev/null
+++ b/dom/media/webspeech/recognition/SpeechRecognition.cpp
@@ -0,0 +1,1170 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SpeechRecognition.h"
+
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+
+#include "mozilla/dom/AudioStreamTrack.h"
+#include "mozilla/dom/BindingUtils.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/SpeechRecognitionBinding.h"
+#include "mozilla/dom/MediaStreamTrackBinding.h"
+#include "mozilla/dom/MediaStreamError.h"
+#include "mozilla/dom/RootedDictionary.h"
+#include "mozilla/dom/SpeechGrammar.h"
+#include "mozilla/MediaManager.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/ResultVariant.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/AbstractThread.h"
+#include "VideoUtils.h"
+#include "AudioSegment.h"
+#include "MediaEnginePrefs.h"
+#include "endpointer.h"
+
+#include "mozilla/dom/SpeechRecognitionEvent.h"
+#include "nsComponentManagerUtils.h"
+#include "nsContentUtils.h"
+#include "mozilla/dom/Document.h"
+#include "nsIObserverService.h"
+#include "nsIPermissionManager.h"
+#include "nsIPrincipal.h"
+#include "nsPIDOMWindow.h"
+#include "nsServiceManagerUtils.h"
+#include "nsQueryObject.h"
+#include "SpeechTrackListener.h"
+
+#include <algorithm>
+
+// Undo the windows.h damage
+#if defined(XP_WIN) && defined(GetMessage)
+# undef GetMessage
+#endif
+
+namespace mozilla::dom {
+
+#define PREFERENCE_DEFAULT_RECOGNITION_SERVICE "media.webspeech.service.default"
+#define DEFAULT_RECOGNITION_SERVICE "online"
+
+#define PREFERENCE_ENDPOINTER_SILENCE_LENGTH "media.webspeech.silence_length"
+#define PREFERENCE_ENDPOINTER_LONG_SILENCE_LENGTH \
+ "media.webspeech.long_silence_length"
+#define PREFERENCE_ENDPOINTER_LONG_SPEECH_LENGTH \
+ "media.webspeech.long_speech_length"
+#define PREFERENCE_SPEECH_DETECTION_TIMEOUT_MS \
+ "media.webspeech.recognition.timeout"
+
+static const uint32_t kSAMPLE_RATE = 16000;
+
+// number of frames corresponding to 300ms of audio to send to endpointer while
+// it's in environment estimation mode
+// kSAMPLE_RATE frames = 1s, kESTIMATION_FRAMES frames = 300ms
+static const uint32_t kESTIMATION_SAMPLES = 300 * kSAMPLE_RATE / 1000;
+
+LogModule* GetSpeechRecognitionLog() {
+ static LazyLogModule sLog("SpeechRecognition");
+ return sLog;
+}
+#define SR_LOG(...) \
+ MOZ_LOG(GetSpeechRecognitionLog(), mozilla::LogLevel::Debug, (__VA_ARGS__))
+
+namespace {
+class SpeechRecognitionShutdownBlocker : public media::ShutdownBlocker {
+ public:
+ SpeechRecognitionShutdownBlocker(SpeechRecognition* aRecognition,
+ const nsString& aName)
+ : media::ShutdownBlocker(aName), mRecognition(aRecognition) {}
+
+ NS_IMETHOD BlockShutdown(nsIAsyncShutdownClient*) override {
+ MOZ_ASSERT(NS_IsMainThread());
+ // AbortSilently will eventually clear the blocker.
+ mRecognition->Abort();
+ return NS_OK;
+ }
+
+ private:
+ const RefPtr<SpeechRecognition> mRecognition;
+};
+
+enum class ServiceCreationError {
+ ServiceNotFound,
+};
+
+Result<nsCOMPtr<nsISpeechRecognitionService>, ServiceCreationError>
+CreateSpeechRecognitionService(nsPIDOMWindowInner* aWindow,
+ SpeechRecognition* aRecognition,
+ const nsAString& aLang) {
+ nsAutoCString speechRecognitionServiceCID;
+
+ nsAutoCString prefValue;
+ Preferences::GetCString(PREFERENCE_DEFAULT_RECOGNITION_SERVICE, prefValue);
+ nsAutoCString speechRecognitionService;
+
+ if (!prefValue.IsEmpty()) {
+ speechRecognitionService = prefValue;
+ } else {
+ speechRecognitionService = DEFAULT_RECOGNITION_SERVICE;
+ }
+
+ if (StaticPrefs::media_webspeech_test_fake_recognition_service()) {
+ speechRecognitionServiceCID =
+ NS_SPEECH_RECOGNITION_SERVICE_CONTRACTID_PREFIX "fake";
+ } else {
+ speechRecognitionServiceCID =
+ nsLiteralCString(NS_SPEECH_RECOGNITION_SERVICE_CONTRACTID_PREFIX) +
+ speechRecognitionService;
+ }
+
+ nsresult rv;
+ nsCOMPtr<nsISpeechRecognitionService> recognitionService;
+ recognitionService =
+ do_CreateInstance(speechRecognitionServiceCID.get(), &rv);
+ if (!recognitionService) {
+ return Err(ServiceCreationError::ServiceNotFound);
+ }
+
+ return recognitionService;
+}
+} // namespace
+
+NS_IMPL_CYCLE_COLLECTION_WEAK_PTR_INHERITED(SpeechRecognition,
+ DOMEventTargetHelper, mStream,
+ mTrack, mRecognitionService,
+ mSpeechGrammarList)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SpeechRecognition)
+ NS_INTERFACE_MAP_ENTRY(nsIObserver)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+NS_IMPL_ADDREF_INHERITED(SpeechRecognition, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(SpeechRecognition, DOMEventTargetHelper)
+
+SpeechRecognition::SpeechRecognition(nsPIDOMWindowInner* aOwnerWindow)
+ : DOMEventTargetHelper(aOwnerWindow),
+ mEndpointer(kSAMPLE_RATE),
+ mAudioSamplesPerChunk(mEndpointer.FrameSize()),
+ mSpeechDetectionTimer(NS_NewTimer()),
+ mSpeechGrammarList(new SpeechGrammarList(GetOwner())),
+ mContinuous(false),
+ mInterimResults(false),
+ mMaxAlternatives(1) {
+ SR_LOG("created SpeechRecognition");
+
+ if (StaticPrefs::media_webspeech_test_enable()) {
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ obs->AddObserver(this, SPEECH_RECOGNITION_TEST_EVENT_REQUEST_TOPIC, false);
+ obs->AddObserver(this, SPEECH_RECOGNITION_TEST_END_TOPIC, false);
+ }
+
+ mEndpointer.set_speech_input_complete_silence_length(
+ Preferences::GetInt(PREFERENCE_ENDPOINTER_SILENCE_LENGTH, 1250000));
+ mEndpointer.set_long_speech_input_complete_silence_length(
+ Preferences::GetInt(PREFERENCE_ENDPOINTER_LONG_SILENCE_LENGTH, 2500000));
+ mEndpointer.set_long_speech_length(
+ Preferences::GetInt(PREFERENCE_ENDPOINTER_SILENCE_LENGTH, 3 * 1000000));
+
+ mSpeechDetectionTimeoutMs =
+ Preferences::GetInt(PREFERENCE_SPEECH_DETECTION_TIMEOUT_MS, 10000);
+
+ Reset();
+}
+
+SpeechRecognition::~SpeechRecognition() = default;
+
+bool SpeechRecognition::StateBetween(FSMState begin, FSMState end) {
+ return mCurrentState >= begin && mCurrentState <= end;
+}
+
+void SpeechRecognition::SetState(FSMState state) {
+ mCurrentState = state;
+ SR_LOG("Transitioned to state %s", GetName(mCurrentState));
+}
+
+JSObject* SpeechRecognition::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return SpeechRecognition_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+bool SpeechRecognition::IsAuthorized(JSContext* aCx, JSObject* aGlobal) {
+ nsCOMPtr<nsIPrincipal> principal = nsContentUtils::ObjectPrincipal(aGlobal);
+
+ nsresult rv;
+ nsCOMPtr<nsIPermissionManager> mgr =
+ do_GetService(NS_PERMISSIONMANAGER_CONTRACTID, &rv);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ uint32_t speechRecognition = nsIPermissionManager::UNKNOWN_ACTION;
+ rv = mgr->TestExactPermissionFromPrincipal(principal, "speech-recognition"_ns,
+ &speechRecognition);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+
+ bool hasPermission =
+ (speechRecognition == nsIPermissionManager::ALLOW_ACTION);
+
+ return (hasPermission ||
+ StaticPrefs::media_webspeech_recognition_force_enable() ||
+ StaticPrefs::media_webspeech_test_enable()) &&
+ StaticPrefs::media_webspeech_recognition_enable();
+}
+
+already_AddRefed<SpeechRecognition> SpeechRecognition::Constructor(
+ const GlobalObject& aGlobal, ErrorResult& aRv) {
+ nsCOMPtr<nsPIDOMWindowInner> win = do_QueryInterface(aGlobal.GetAsSupports());
+ if (!win) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ RefPtr<SpeechRecognition> object = new SpeechRecognition(win);
+ return object.forget();
+}
+
+void SpeechRecognition::ProcessEvent(SpeechEvent* aEvent) {
+ SR_LOG("Processing %s, current state is %s", GetName(aEvent),
+ GetName(mCurrentState));
+
+ if (mAborted && aEvent->mType != EVENT_ABORT) {
+ // ignore all events while aborting
+ return;
+ }
+
+ Transition(aEvent);
+}
+
+void SpeechRecognition::Transition(SpeechEvent* aEvent) {
+ switch (mCurrentState) {
+ case STATE_IDLE:
+ switch (aEvent->mType) {
+ case EVENT_START:
+ // TODO: may want to time out if we wait too long
+ // for user to approve
+ WaitForAudioData(aEvent);
+ break;
+ case EVENT_STOP:
+ case EVENT_ABORT:
+ case EVENT_AUDIO_DATA:
+ case EVENT_RECOGNITIONSERVICE_INTERMEDIATE_RESULT:
+ case EVENT_RECOGNITIONSERVICE_FINAL_RESULT:
+ DoNothing(aEvent);
+ break;
+ case EVENT_AUDIO_ERROR:
+ case EVENT_RECOGNITIONSERVICE_ERROR:
+ AbortError(aEvent);
+ break;
+ default:
+ MOZ_CRASH("Invalid event");
+ }
+ break;
+ case STATE_STARTING:
+ switch (aEvent->mType) {
+ case EVENT_AUDIO_DATA:
+ StartedAudioCapture(aEvent);
+ break;
+ case EVENT_AUDIO_ERROR:
+ case EVENT_RECOGNITIONSERVICE_ERROR:
+ AbortError(aEvent);
+ break;
+ case EVENT_ABORT:
+ AbortSilently(aEvent);
+ break;
+ case EVENT_STOP:
+ ResetAndEnd();
+ break;
+ case EVENT_RECOGNITIONSERVICE_INTERMEDIATE_RESULT:
+ case EVENT_RECOGNITIONSERVICE_FINAL_RESULT:
+ DoNothing(aEvent);
+ break;
+ case EVENT_START:
+ SR_LOG("STATE_STARTING: Unhandled event %s", GetName(aEvent));
+ MOZ_CRASH();
+ default:
+ MOZ_CRASH("Invalid event");
+ }
+ break;
+ case STATE_ESTIMATING:
+ switch (aEvent->mType) {
+ case EVENT_AUDIO_DATA:
+ WaitForEstimation(aEvent);
+ break;
+ case EVENT_STOP:
+ StopRecordingAndRecognize(aEvent);
+ break;
+ case EVENT_ABORT:
+ AbortSilently(aEvent);
+ break;
+ case EVENT_RECOGNITIONSERVICE_INTERMEDIATE_RESULT:
+ case EVENT_RECOGNITIONSERVICE_FINAL_RESULT:
+ case EVENT_RECOGNITIONSERVICE_ERROR:
+ DoNothing(aEvent);
+ break;
+ case EVENT_AUDIO_ERROR:
+ AbortError(aEvent);
+ break;
+ case EVENT_START:
+ SR_LOG("STATE_ESTIMATING: Unhandled event %d", aEvent->mType);
+ MOZ_CRASH();
+ default:
+ MOZ_CRASH("Invalid event");
+ }
+ break;
+ case STATE_WAITING_FOR_SPEECH:
+ switch (aEvent->mType) {
+ case EVENT_AUDIO_DATA:
+ DetectSpeech(aEvent);
+ break;
+ case EVENT_STOP:
+ StopRecordingAndRecognize(aEvent);
+ break;
+ case EVENT_ABORT:
+ AbortSilently(aEvent);
+ break;
+ case EVENT_AUDIO_ERROR:
+ AbortError(aEvent);
+ break;
+ case EVENT_RECOGNITIONSERVICE_INTERMEDIATE_RESULT:
+ case EVENT_RECOGNITIONSERVICE_FINAL_RESULT:
+ case EVENT_RECOGNITIONSERVICE_ERROR:
+ DoNothing(aEvent);
+ break;
+ case EVENT_START:
+ SR_LOG("STATE_STARTING: Unhandled event %s", GetName(aEvent));
+ MOZ_CRASH();
+ default:
+ MOZ_CRASH("Invalid event");
+ }
+ break;
+ case STATE_RECOGNIZING:
+ switch (aEvent->mType) {
+ case EVENT_AUDIO_DATA:
+ WaitForSpeechEnd(aEvent);
+ break;
+ case EVENT_STOP:
+ StopRecordingAndRecognize(aEvent);
+ break;
+ case EVENT_AUDIO_ERROR:
+ case EVENT_RECOGNITIONSERVICE_ERROR:
+ AbortError(aEvent);
+ break;
+ case EVENT_ABORT:
+ AbortSilently(aEvent);
+ break;
+ case EVENT_RECOGNITIONSERVICE_FINAL_RESULT:
+ case EVENT_RECOGNITIONSERVICE_INTERMEDIATE_RESULT:
+ DoNothing(aEvent);
+ break;
+ case EVENT_START:
+ SR_LOG("STATE_RECOGNIZING: Unhandled aEvent %s", GetName(aEvent));
+ MOZ_CRASH();
+ default:
+ MOZ_CRASH("Invalid event");
+ }
+ break;
+ case STATE_WAITING_FOR_RESULT:
+ switch (aEvent->mType) {
+ case EVENT_STOP:
+ DoNothing(aEvent);
+ break;
+ case EVENT_AUDIO_ERROR:
+ case EVENT_RECOGNITIONSERVICE_ERROR:
+ AbortError(aEvent);
+ break;
+ case EVENT_RECOGNITIONSERVICE_FINAL_RESULT:
+ NotifyFinalResult(aEvent);
+ break;
+ case EVENT_AUDIO_DATA:
+ DoNothing(aEvent);
+ break;
+ case EVENT_ABORT:
+ AbortSilently(aEvent);
+ break;
+ case EVENT_START:
+ case EVENT_RECOGNITIONSERVICE_INTERMEDIATE_RESULT:
+ SR_LOG("STATE_WAITING_FOR_RESULT: Unhandled aEvent %s",
+ GetName(aEvent));
+ MOZ_CRASH();
+ default:
+ MOZ_CRASH("Invalid event");
+ }
+ break;
+ case STATE_ABORTING:
+ switch (aEvent->mType) {
+ case EVENT_STOP:
+ case EVENT_ABORT:
+ case EVENT_AUDIO_DATA:
+ case EVENT_AUDIO_ERROR:
+ case EVENT_RECOGNITIONSERVICE_INTERMEDIATE_RESULT:
+ case EVENT_RECOGNITIONSERVICE_FINAL_RESULT:
+ case EVENT_RECOGNITIONSERVICE_ERROR:
+ DoNothing(aEvent);
+ break;
+ case EVENT_START:
+ SR_LOG("STATE_ABORTING: Unhandled aEvent %s", GetName(aEvent));
+ MOZ_CRASH();
+ default:
+ MOZ_CRASH("Invalid event");
+ }
+ break;
+ default:
+ MOZ_CRASH("Invalid state");
+ }
+}
+
+/*
+ * Handle a segment of recorded audio data.
+ * Returns the number of samples that were processed.
+ */
+uint32_t SpeechRecognition::ProcessAudioSegment(AudioSegment* aSegment,
+ TrackRate aTrackRate) {
+ AudioSegment::ChunkIterator iterator(*aSegment);
+ uint32_t samples = 0;
+ while (!iterator.IsEnded()) {
+ float out;
+ mEndpointer.ProcessAudio(*iterator, &out);
+ samples += iterator->GetDuration();
+ iterator.Next();
+ }
+
+ // we need to call the nsISpeechRecognitionService::ProcessAudioSegment
+ // in a separate thread so that any eventual encoding or pre-processing
+ // of the audio does not block the main thread
+ nsresult rv = mEncodeTaskQueue->Dispatch(
+ NewRunnableMethod<StoreCopyPassByPtr<AudioSegment>, TrackRate>(
+ "nsISpeechRecognitionService::ProcessAudioSegment",
+ mRecognitionService,
+ &nsISpeechRecognitionService::ProcessAudioSegment,
+ std::move(*aSegment), aTrackRate));
+ MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
+ Unused << rv;
+ return samples;
+}
+
+/****************************************************************************
+ * FSM Transition functions
+ *
+ * If a transition function may cause a DOM event to be fired,
+ * it may also be re-entered, since the event handler may cause the
+ * event loop to spin and new SpeechEvents to be processed.
+ *
+ * Rules:
+ * 1) These methods should call SetState as soon as possible.
+ * 2) If these methods dispatch DOM events, or call methods that dispatch
+ * DOM events, that should be done as late as possible.
+ * 3) If anything must happen after dispatching a DOM event, make sure
+ * the state is still what the method expected it to be.
+ ****************************************************************************/
+
+void SpeechRecognition::Reset() {
+ SetState(STATE_IDLE);
+
+ // This breaks potential ref-cycles.
+ mRecognitionService = nullptr;
+
+ ++mStreamGeneration;
+ if (mStream) {
+ mStream->UnregisterTrackListener(this);
+ mStream = nullptr;
+ }
+ mTrack = nullptr;
+ mTrackIsOwned = false;
+ mStopRecordingPromise = nullptr;
+ mEncodeTaskQueue = nullptr;
+ mEstimationSamples = 0;
+ mBufferedSamples = 0;
+ mSpeechDetectionTimer->Cancel();
+ mAborted = false;
+}
+
+void SpeechRecognition::ResetAndEnd() {
+ Reset();
+ DispatchTrustedEvent(u"end"_ns);
+}
+
+void SpeechRecognition::WaitForAudioData(SpeechEvent* aEvent) {
+ SetState(STATE_STARTING);
+}
+
+void SpeechRecognition::StartedAudioCapture(SpeechEvent* aEvent) {
+ SetState(STATE_ESTIMATING);
+
+ mEndpointer.SetEnvironmentEstimationMode();
+ mEstimationSamples +=
+ ProcessAudioSegment(aEvent->mAudioSegment, aEvent->mTrackRate);
+
+ DispatchTrustedEvent(u"audiostart"_ns);
+ if (mCurrentState == STATE_ESTIMATING) {
+ DispatchTrustedEvent(u"start"_ns);
+ }
+}
+
+void SpeechRecognition::StopRecordingAndRecognize(SpeechEvent* aEvent) {
+ SetState(STATE_WAITING_FOR_RESULT);
+
+ MOZ_ASSERT(mRecognitionService, "Service deleted before recording done");
+
+ // This will run SoundEnd on the service just before StopRecording begins
+ // shutting the encode thread down.
+ mSpeechListener->mRemovedPromise->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [service = mRecognitionService] { service->SoundEnd(); });
+
+ StopRecording();
+}
+
+void SpeechRecognition::WaitForEstimation(SpeechEvent* aEvent) {
+ SetState(STATE_ESTIMATING);
+
+ mEstimationSamples +=
+ ProcessAudioSegment(aEvent->mAudioSegment, aEvent->mTrackRate);
+ if (mEstimationSamples > kESTIMATION_SAMPLES) {
+ mEndpointer.SetUserInputMode();
+ SetState(STATE_WAITING_FOR_SPEECH);
+ }
+}
+
+void SpeechRecognition::DetectSpeech(SpeechEvent* aEvent) {
+ SetState(STATE_WAITING_FOR_SPEECH);
+
+ ProcessAudioSegment(aEvent->mAudioSegment, aEvent->mTrackRate);
+ if (mEndpointer.DidStartReceivingSpeech()) {
+ mSpeechDetectionTimer->Cancel();
+ SetState(STATE_RECOGNIZING);
+ DispatchTrustedEvent(u"speechstart"_ns);
+ }
+}
+
+void SpeechRecognition::WaitForSpeechEnd(SpeechEvent* aEvent) {
+ SetState(STATE_RECOGNIZING);
+
+ ProcessAudioSegment(aEvent->mAudioSegment, aEvent->mTrackRate);
+ if (mEndpointer.speech_input_complete()) {
+ DispatchTrustedEvent(u"speechend"_ns);
+
+ if (mCurrentState == STATE_RECOGNIZING) {
+ // FIXME: StopRecordingAndRecognize should only be called for single
+ // shot services for continuous we should just inform the service
+ StopRecordingAndRecognize(aEvent);
+ }
+ }
+}
+
+void SpeechRecognition::NotifyFinalResult(SpeechEvent* aEvent) {
+ ResetAndEnd();
+
+ RootedDictionary<SpeechRecognitionEventInit> init(RootingCx());
+ init.mBubbles = true;
+ init.mCancelable = false;
+ // init.mResultIndex = 0;
+ init.mResults = aEvent->mRecognitionResultList;
+ init.mInterpretation = JS::NullValue();
+ // init.mEmma = nullptr;
+
+ RefPtr<SpeechRecognitionEvent> event =
+ SpeechRecognitionEvent::Constructor(this, u"result"_ns, init);
+ event->SetTrusted(true);
+
+ DispatchEvent(*event);
+}
+
+void SpeechRecognition::DoNothing(SpeechEvent* aEvent) {}
+
+void SpeechRecognition::AbortSilently(SpeechEvent* aEvent) {
+ if (mRecognitionService) {
+ if (mTrack) {
+ // This will run Abort on the service just before StopRecording begins
+ // shutting the encode thread down.
+ mSpeechListener->mRemovedPromise->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [service = mRecognitionService] { service->Abort(); });
+ } else {
+ // Recording hasn't started yet. We can just call Abort().
+ mRecognitionService->Abort();
+ }
+ }
+
+ StopRecording()->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr<SpeechRecognition>(this), this] { ResetAndEnd(); });
+
+ SetState(STATE_ABORTING);
+}
+
+void SpeechRecognition::AbortError(SpeechEvent* aEvent) {
+ AbortSilently(aEvent);
+ NotifyError(aEvent);
+}
+
+void SpeechRecognition::NotifyError(SpeechEvent* aEvent) {
+ aEvent->mError->SetTrusted(true);
+
+ DispatchEvent(*aEvent->mError);
+}
+
+/**************************************
+ * Event triggers and other functions *
+ **************************************/
+NS_IMETHODIMP
+SpeechRecognition::StartRecording(RefPtr<AudioStreamTrack>& aTrack) {
+ // hold a reference so that the underlying track doesn't get collected.
+ mTrack = aTrack;
+ MOZ_ASSERT(!mTrack->Ended());
+
+ mSpeechListener = new SpeechTrackListener(this);
+ mTrack->AddListener(mSpeechListener);
+
+ nsString blockerName;
+ blockerName.AppendPrintf("SpeechRecognition %p shutdown", this);
+ mShutdownBlocker =
+ MakeAndAddRef<SpeechRecognitionShutdownBlocker>(this, blockerName);
+ media::MustGetShutdownBarrier()->AddBlocker(
+ mShutdownBlocker, NS_LITERAL_STRING_FROM_CSTRING(__FILE__), __LINE__,
+ u"SpeechRecognition shutdown"_ns);
+
+ mEndpointer.StartSession();
+
+ return mSpeechDetectionTimer->Init(this, mSpeechDetectionTimeoutMs,
+ nsITimer::TYPE_ONE_SHOT);
+}
+
+RefPtr<GenericNonExclusivePromise> SpeechRecognition::StopRecording() {
+ if (!mTrack) {
+ // Recording wasn't started, or has already been stopped.
+ if (mStream) {
+ // Ensure we don't start recording because a track became available
+ // before we get reset.
+ mStream->UnregisterTrackListener(this);
+ }
+ return GenericNonExclusivePromise::CreateAndResolve(true, __func__);
+ }
+
+ if (mStopRecordingPromise) {
+ return mStopRecordingPromise;
+ }
+
+ mTrack->RemoveListener(mSpeechListener);
+ if (mTrackIsOwned) {
+ mTrack->Stop();
+ }
+
+ mEndpointer.EndSession();
+ DispatchTrustedEvent(u"audioend"_ns);
+
+ // Block shutdown until the speech track listener has been removed from the
+ // MSG, as it holds a reference to us, and we reference the world, which we
+ // don't want to leak.
+ mStopRecordingPromise =
+ mSpeechListener->mRemovedPromise
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr<SpeechRecognition>(this), this] {
+ SR_LOG("Shutting down encoding thread");
+ return mEncodeTaskQueue->BeginShutdown();
+ },
+ [] {
+ MOZ_CRASH("Unexpected rejection");
+ return ShutdownPromise::CreateAndResolve(false, __func__);
+ })
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr<SpeechRecognition>(this), this] {
+ media::MustGetShutdownBarrier()->RemoveBlocker(
+ mShutdownBlocker);
+ mShutdownBlocker = nullptr;
+
+ MOZ_DIAGNOSTIC_ASSERT(mCurrentState != STATE_IDLE);
+ return GenericNonExclusivePromise::CreateAndResolve(true,
+ __func__);
+ },
+ [] {
+ MOZ_CRASH("Unexpected rejection");
+ return GenericNonExclusivePromise::CreateAndResolve(false,
+ __func__);
+ });
+ return mStopRecordingPromise;
+}
+
+NS_IMETHODIMP
+SpeechRecognition::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ MOZ_ASSERT(NS_IsMainThread(), "Observer invoked off the main thread");
+
+ if (!strcmp(aTopic, NS_TIMER_CALLBACK_TOPIC) &&
+ StateBetween(STATE_IDLE, STATE_WAITING_FOR_SPEECH)) {
+ DispatchError(SpeechRecognition::EVENT_AUDIO_ERROR,
+ SpeechRecognitionErrorCode::No_speech,
+ "No speech detected (timeout)");
+ } else if (!strcmp(aTopic, SPEECH_RECOGNITION_TEST_END_TOPIC)) {
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ obs->RemoveObserver(this, SPEECH_RECOGNITION_TEST_EVENT_REQUEST_TOPIC);
+ obs->RemoveObserver(this, SPEECH_RECOGNITION_TEST_END_TOPIC);
+ } else if (StaticPrefs::media_webspeech_test_fake_fsm_events() &&
+ !strcmp(aTopic, SPEECH_RECOGNITION_TEST_EVENT_REQUEST_TOPIC)) {
+ ProcessTestEventRequest(aSubject, nsDependentString(aData));
+ }
+
+ return NS_OK;
+}
+
+void SpeechRecognition::ProcessTestEventRequest(nsISupports* aSubject,
+ const nsAString& aEventName) {
+ if (aEventName.EqualsLiteral("EVENT_ABORT")) {
+ Abort();
+ } else if (aEventName.EqualsLiteral("EVENT_AUDIO_ERROR")) {
+ DispatchError(
+ SpeechRecognition::EVENT_AUDIO_ERROR,
+ SpeechRecognitionErrorCode::Audio_capture, // TODO different codes?
+ "AUDIO_ERROR test event");
+ } else {
+ NS_ASSERTION(StaticPrefs::media_webspeech_test_fake_recognition_service(),
+ "Got request for fake recognition service event, but "
+ "media.webspeech.test.fake_recognition_service is unset");
+
+ // let the fake recognition service handle the request
+ }
+}
+
+already_AddRefed<SpeechGrammarList> SpeechRecognition::Grammars() const {
+ RefPtr<SpeechGrammarList> speechGrammarList = mSpeechGrammarList;
+ return speechGrammarList.forget();
+}
+
+void SpeechRecognition::SetGrammars(SpeechGrammarList& aArg) {
+ mSpeechGrammarList = &aArg;
+}
+
+void SpeechRecognition::GetLang(nsString& aRetVal) const { aRetVal = mLang; }
+
+void SpeechRecognition::SetLang(const nsAString& aArg) { mLang = aArg; }
+
+bool SpeechRecognition::GetContinuous(ErrorResult& aRv) const {
+ return mContinuous;
+}
+
+void SpeechRecognition::SetContinuous(bool aArg, ErrorResult& aRv) {
+ mContinuous = aArg;
+}
+
+bool SpeechRecognition::InterimResults() const { return mInterimResults; }
+
+void SpeechRecognition::SetInterimResults(bool aArg) { mInterimResults = aArg; }
+
+uint32_t SpeechRecognition::MaxAlternatives() const { return mMaxAlternatives; }
+
+void SpeechRecognition::SetMaxAlternatives(uint32_t aArg) {
+ mMaxAlternatives = aArg;
+}
+
+void SpeechRecognition::GetServiceURI(nsString& aRetVal,
+ ErrorResult& aRv) const {
+ aRv.Throw(NS_ERROR_NOT_IMPLEMENTED);
+}
+
+void SpeechRecognition::SetServiceURI(const nsAString& aArg, ErrorResult& aRv) {
+ aRv.Throw(NS_ERROR_NOT_IMPLEMENTED);
+}
+
+void SpeechRecognition::Start(const Optional<NonNull<DOMMediaStream>>& aStream,
+ CallerType aCallerType, ErrorResult& aRv) {
+ if (mCurrentState != STATE_IDLE) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ if (!SetRecognitionService(aRv)) {
+ return;
+ }
+
+ if (!ValidateAndSetGrammarList(aRv)) {
+ return;
+ }
+
+ mEncodeTaskQueue =
+ TaskQueue::Create(GetMediaThreadPool(MediaThreadType::WEBRTC_WORKER),
+ "WebSpeechEncoderThread");
+
+ nsresult rv;
+ rv = mRecognitionService->Initialize(this);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ MediaStreamConstraints constraints;
+ constraints.mAudio.SetAsBoolean() = true;
+
+ if (aStream.WasPassed()) {
+ mStream = &aStream.Value();
+ mTrackIsOwned = false;
+ mStream->RegisterTrackListener(this);
+ nsTArray<RefPtr<AudioStreamTrack>> tracks;
+ mStream->GetAudioTracks(tracks);
+ for (const RefPtr<AudioStreamTrack>& track : tracks) {
+ if (!track->Ended()) {
+ NotifyTrackAdded(track);
+ break;
+ }
+ }
+ } else {
+ mTrackIsOwned = true;
+ nsPIDOMWindowInner* win = GetOwner();
+ if (!win || !win->IsFullyActive()) {
+ aRv.ThrowInvalidStateError("The document is not fully active.");
+ return;
+ }
+ AutoNoJSAPI nojsapi;
+ RefPtr<SpeechRecognition> self(this);
+ MediaManager::Get()
+ ->GetUserMedia(win, constraints, aCallerType)
+ ->Then(
+ GetCurrentSerialEventTarget(), __func__,
+ [this, self,
+ generation = mStreamGeneration](RefPtr<DOMMediaStream>&& aStream) {
+ nsTArray<RefPtr<AudioStreamTrack>> tracks;
+ aStream->GetAudioTracks(tracks);
+ if (mAborted || mCurrentState != STATE_STARTING ||
+ mStreamGeneration != generation) {
+ // We were probably aborted. Exit early.
+ for (const RefPtr<AudioStreamTrack>& track : tracks) {
+ track->Stop();
+ }
+ return;
+ }
+ mStream = std::move(aStream);
+ mStream->RegisterTrackListener(this);
+ for (const RefPtr<AudioStreamTrack>& track : tracks) {
+ if (!track->Ended()) {
+ NotifyTrackAdded(track);
+ }
+ }
+ },
+ [this, self,
+ generation = mStreamGeneration](RefPtr<MediaMgrError>&& error) {
+ if (mAborted || mCurrentState != STATE_STARTING ||
+ mStreamGeneration != generation) {
+ // We were probably aborted. Exit early.
+ return;
+ }
+ SpeechRecognitionErrorCode errorCode;
+
+ if (error->mName == MediaMgrError::Name::NotAllowedError) {
+ errorCode = SpeechRecognitionErrorCode::Not_allowed;
+ } else {
+ errorCode = SpeechRecognitionErrorCode::Audio_capture;
+ }
+ DispatchError(SpeechRecognition::EVENT_AUDIO_ERROR, errorCode,
+ error->mMessage);
+ });
+ }
+
+ RefPtr<SpeechEvent> event = new SpeechEvent(this, EVENT_START);
+ NS_DispatchToMainThread(event);
+}
+
+bool SpeechRecognition::SetRecognitionService(ErrorResult& aRv) {
+ if (!GetOwner()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return false;
+ }
+
+ // See:
+ // https://dvcs.w3.org/hg/speech-api/raw-file/tip/webspeechapi.html#dfn-lang
+ nsAutoString lang;
+ if (!mLang.IsEmpty()) {
+ lang = mLang;
+ } else {
+ nsCOMPtr<Document> document = GetOwner()->GetExtantDoc();
+ if (!document) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return false;
+ }
+ nsCOMPtr<Element> element = document->GetRootElement();
+ if (!element) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return false;
+ }
+
+ nsAutoString lang;
+ element->GetLang(lang);
+ }
+
+ auto result = CreateSpeechRecognitionService(GetOwner(), this, lang);
+
+ if (result.isErr()) {
+ switch (result.unwrapErr()) {
+ case ServiceCreationError::ServiceNotFound:
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ break;
+ default:
+ MOZ_CRASH("Unknown error");
+ }
+ return false;
+ }
+
+ mRecognitionService = result.unwrap();
+ MOZ_DIAGNOSTIC_ASSERT(mRecognitionService);
+ return true;
+}
+
+bool SpeechRecognition::ValidateAndSetGrammarList(ErrorResult& aRv) {
+ if (!mSpeechGrammarList) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return false;
+ }
+
+ uint32_t grammarListLength = mSpeechGrammarList->Length();
+ for (uint32_t count = 0; count < grammarListLength; ++count) {
+ RefPtr<SpeechGrammar> speechGrammar = mSpeechGrammarList->Item(count, aRv);
+ if (aRv.Failed()) {
+ return false;
+ }
+ if (NS_FAILED(mRecognitionService->ValidateAndSetGrammarList(
+ speechGrammar.get(), nullptr))) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void SpeechRecognition::Stop() {
+ RefPtr<SpeechEvent> event = new SpeechEvent(this, EVENT_STOP);
+ NS_DispatchToMainThread(event);
+}
+
+void SpeechRecognition::Abort() {
+ if (mAborted) {
+ return;
+ }
+
+ mAborted = true;
+
+ RefPtr<SpeechEvent> event = new SpeechEvent(this, EVENT_ABORT);
+ NS_DispatchToMainThread(event);
+}
+
+void SpeechRecognition::NotifyTrackAdded(
+ const RefPtr<MediaStreamTrack>& aTrack) {
+ if (mTrack) {
+ return;
+ }
+
+ RefPtr<AudioStreamTrack> audioTrack = aTrack->AsAudioStreamTrack();
+ if (!audioTrack) {
+ return;
+ }
+
+ if (audioTrack->Ended()) {
+ return;
+ }
+
+ StartRecording(audioTrack);
+}
+
+void SpeechRecognition::DispatchError(EventType aErrorType,
+ SpeechRecognitionErrorCode aErrorCode,
+ const nsACString& aMessage) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aErrorType == EVENT_RECOGNITIONSERVICE_ERROR ||
+ aErrorType == EVENT_AUDIO_ERROR,
+ "Invalid error type!");
+
+ RefPtr<SpeechRecognitionError> srError =
+ new SpeechRecognitionError(nullptr, nullptr, nullptr);
+
+ srError->InitSpeechRecognitionError(u"error"_ns, true, false, aErrorCode,
+ aMessage);
+
+ RefPtr<SpeechEvent> event = new SpeechEvent(this, aErrorType);
+ event->mError = srError;
+ NS_DispatchToMainThread(event);
+}
+
+/*
+ * Buffer audio samples into mAudioSamplesBuffer until aBufferSize.
+ * Updates mBufferedSamples and returns the number of samples that were
+ * buffered.
+ */
+uint32_t SpeechRecognition::FillSamplesBuffer(const int16_t* aSamples,
+ uint32_t aSampleCount) {
+ MOZ_ASSERT(mBufferedSamples < mAudioSamplesPerChunk);
+ MOZ_ASSERT(mAudioSamplesBuffer);
+
+ int16_t* samplesBuffer = static_cast<int16_t*>(mAudioSamplesBuffer->Data());
+ size_t samplesToCopy =
+ std::min(aSampleCount, mAudioSamplesPerChunk - mBufferedSamples);
+
+ PodCopy(samplesBuffer + mBufferedSamples, aSamples, samplesToCopy);
+
+ mBufferedSamples += samplesToCopy;
+ return samplesToCopy;
+}
+
+/*
+ * Split a samples buffer starting of a given size into
+ * chunks of equal size. The chunks are stored in the array
+ * received as argument.
+ * Returns the offset of the end of the last chunk that was
+ * created.
+ */
+uint32_t SpeechRecognition::SplitSamplesBuffer(
+ const int16_t* aSamplesBuffer, uint32_t aSampleCount,
+ nsTArray<RefPtr<SharedBuffer>>& aResult) {
+ uint32_t chunkStart = 0;
+
+ while (chunkStart + mAudioSamplesPerChunk <= aSampleCount) {
+ CheckedInt<size_t> bufferSize(sizeof(int16_t));
+ bufferSize *= mAudioSamplesPerChunk;
+ RefPtr<SharedBuffer> chunk = SharedBuffer::Create(bufferSize);
+
+ PodCopy(static_cast<short*>(chunk->Data()), aSamplesBuffer + chunkStart,
+ mAudioSamplesPerChunk);
+
+ aResult.AppendElement(chunk.forget());
+ chunkStart += mAudioSamplesPerChunk;
+ }
+
+ return chunkStart;
+}
+
+AudioSegment* SpeechRecognition::CreateAudioSegment(
+ nsTArray<RefPtr<SharedBuffer>>& aChunks) {
+ AudioSegment* segment = new AudioSegment();
+ for (uint32_t i = 0; i < aChunks.Length(); ++i) {
+ RefPtr<SharedBuffer> buffer = aChunks[i];
+ const int16_t* chunkData = static_cast<const int16_t*>(buffer->Data());
+
+ AutoTArray<const int16_t*, 1> channels;
+ channels.AppendElement(chunkData);
+ segment->AppendFrames(buffer.forget(), channels, mAudioSamplesPerChunk,
+ PRINCIPAL_HANDLE_NONE);
+ }
+
+ return segment;
+}
+
+void SpeechRecognition::FeedAudioData(
+ nsMainThreadPtrHandle<SpeechRecognition>& aRecognition,
+ already_AddRefed<SharedBuffer> aSamples, uint32_t aDuration,
+ MediaTrackListener* aProvider, TrackRate aTrackRate) {
+ NS_ASSERTION(!NS_IsMainThread(),
+ "FeedAudioData should not be called in the main thread");
+
+ // Endpointer expects to receive samples in chunks whose size is a
+ // multiple of its frame size.
+ // Since we can't assume we will receive the frames in appropriate-sized
+ // chunks, we must buffer and split them in chunks of mAudioSamplesPerChunk
+ // (a multiple of Endpointer's frame size) before feeding to Endpointer.
+
+ // ensure aSamples is deleted
+ RefPtr<SharedBuffer> refSamples = aSamples;
+
+ uint32_t samplesIndex = 0;
+ const int16_t* samples = static_cast<int16_t*>(refSamples->Data());
+ AutoTArray<RefPtr<SharedBuffer>, 5> chunksToSend;
+
+ // fill up our buffer and make a chunk out of it, if possible
+ if (mBufferedSamples > 0) {
+ samplesIndex += FillSamplesBuffer(samples, aDuration);
+
+ if (mBufferedSamples == mAudioSamplesPerChunk) {
+ chunksToSend.AppendElement(mAudioSamplesBuffer.forget());
+ mBufferedSamples = 0;
+ }
+ }
+
+ // create sample chunks of correct size
+ if (samplesIndex < aDuration) {
+ samplesIndex += SplitSamplesBuffer(samples + samplesIndex,
+ aDuration - samplesIndex, chunksToSend);
+ }
+
+ // buffer remaining samples
+ if (samplesIndex < aDuration) {
+ mBufferedSamples = 0;
+ CheckedInt<size_t> bufferSize(sizeof(int16_t));
+ bufferSize *= mAudioSamplesPerChunk;
+ mAudioSamplesBuffer = SharedBuffer::Create(bufferSize);
+
+ FillSamplesBuffer(samples + samplesIndex, aDuration - samplesIndex);
+ }
+
+ AudioSegment* segment = CreateAudioSegment(chunksToSend);
+ RefPtr<SpeechEvent> event = new SpeechEvent(aRecognition, EVENT_AUDIO_DATA);
+ event->mAudioSegment = segment;
+ event->mProvider = aProvider;
+ event->mTrackRate = aTrackRate;
+ NS_DispatchToMainThread(event);
+}
+
+const char* SpeechRecognition::GetName(FSMState aId) {
+ static const char* names[] = {
+ "STATE_IDLE", "STATE_STARTING",
+ "STATE_ESTIMATING", "STATE_WAITING_FOR_SPEECH",
+ "STATE_RECOGNIZING", "STATE_WAITING_FOR_RESULT",
+ "STATE_ABORTING",
+ };
+
+ MOZ_ASSERT(aId < STATE_COUNT);
+ MOZ_ASSERT(ArrayLength(names) == STATE_COUNT);
+ return names[aId];
+}
+
+const char* SpeechRecognition::GetName(SpeechEvent* aEvent) {
+ static const char* names[] = {"EVENT_START",
+ "EVENT_STOP",
+ "EVENT_ABORT",
+ "EVENT_AUDIO_DATA",
+ "EVENT_AUDIO_ERROR",
+ "EVENT_RECOGNITIONSERVICE_INTERMEDIATE_RESULT",
+ "EVENT_RECOGNITIONSERVICE_FINAL_RESULT",
+ "EVENT_RECOGNITIONSERVICE_ERROR"};
+
+ MOZ_ASSERT(aEvent->mType < EVENT_COUNT);
+ MOZ_ASSERT(ArrayLength(names) == EVENT_COUNT);
+ return names[aEvent->mType];
+}
+
+TaskQueue* SpeechRecognition::GetTaskQueueForEncoding() const {
+ MOZ_ASSERT(NS_IsMainThread());
+ return mEncodeTaskQueue;
+}
+
+SpeechEvent::SpeechEvent(SpeechRecognition* aRecognition,
+ SpeechRecognition::EventType aType)
+ : Runnable("dom::SpeechEvent"),
+ mAudioSegment(nullptr),
+ mRecognitionResultList(nullptr),
+ mError(nullptr),
+ mRecognition(new nsMainThreadPtrHolder<SpeechRecognition>(
+ "SpeechEvent::SpeechEvent", aRecognition)),
+ mType(aType),
+ mTrackRate(0) {}
+
+SpeechEvent::SpeechEvent(nsMainThreadPtrHandle<SpeechRecognition>& aRecognition,
+ SpeechRecognition::EventType aType)
+ : Runnable("dom::SpeechEvent"),
+ mAudioSegment(nullptr),
+ mRecognitionResultList(nullptr),
+ mError(nullptr),
+ mRecognition(aRecognition),
+ mType(aType),
+ mTrackRate(0) {}
+
+SpeechEvent::~SpeechEvent() { delete mAudioSegment; }
+
+NS_IMETHODIMP
+SpeechEvent::Run() {
+ mRecognition->ProcessEvent(this);
+ return NS_OK;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webspeech/recognition/SpeechRecognition.h b/dom/media/webspeech/recognition/SpeechRecognition.h
new file mode 100644
index 0000000000..687f38041e
--- /dev/null
+++ b/dom/media/webspeech/recognition/SpeechRecognition.h
@@ -0,0 +1,314 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_SpeechRecognition_h
+#define mozilla_dom_SpeechRecognition_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/DOMEventTargetHelper.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsWrapperCache.h"
+#include "nsTArray.h"
+#include "js/TypeDecls.h"
+#include "nsProxyRelease.h"
+#include "DOMMediaStream.h"
+#include "nsITimer.h"
+#include "MediaTrackGraph.h"
+#include "AudioSegment.h"
+#include "mozilla/WeakPtr.h"
+
+#include "SpeechGrammarList.h"
+#include "SpeechRecognitionResultList.h"
+#include "nsISpeechRecognitionService.h"
+#include "endpointer.h"
+
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/SpeechRecognitionError.h"
+
+namespace mozilla {
+
+namespace media {
+class ShutdownBlocker;
+}
+
+namespace dom {
+
+#define SPEECH_RECOGNITION_TEST_EVENT_REQUEST_TOPIC \
+ "SpeechRecognitionTest:RequestEvent"
+#define SPEECH_RECOGNITION_TEST_END_TOPIC "SpeechRecognitionTest:End"
+
+class GlobalObject;
+class AudioStreamTrack;
+class SpeechEvent;
+class SpeechTrackListener;
+
+LogModule* GetSpeechRecognitionLog();
+#define SR_LOG(...) \
+ MOZ_LOG(GetSpeechRecognitionLog(), mozilla::LogLevel::Debug, (__VA_ARGS__))
+
+class SpeechRecognition final : public DOMEventTargetHelper,
+ public nsIObserver,
+ public DOMMediaStream::TrackListener,
+ public SupportsWeakPtr {
+ public:
+ explicit SpeechRecognition(nsPIDOMWindowInner* aOwnerWindow);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(SpeechRecognition,
+ DOMEventTargetHelper)
+
+ NS_DECL_NSIOBSERVER
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ static bool IsAuthorized(JSContext* aCx, JSObject* aGlobal);
+
+ static already_AddRefed<SpeechRecognition> Constructor(
+ const GlobalObject& aGlobal, ErrorResult& aRv);
+
+ static already_AddRefed<SpeechRecognition> WebkitSpeechRecognition(
+ const GlobalObject& aGlobal, ErrorResult& aRv) {
+ return Constructor(aGlobal, aRv);
+ }
+
+ already_AddRefed<SpeechGrammarList> Grammars() const;
+
+ void SetGrammars(mozilla::dom::SpeechGrammarList& aArg);
+
+ void GetLang(nsString& aRetVal) const;
+
+ void SetLang(const nsAString& aArg);
+
+ bool GetContinuous(ErrorResult& aRv) const;
+
+ void SetContinuous(bool aArg, ErrorResult& aRv);
+
+ bool InterimResults() const;
+
+ void SetInterimResults(bool aArg);
+
+ uint32_t MaxAlternatives() const;
+
+ TaskQueue* GetTaskQueueForEncoding() const;
+
+ void SetMaxAlternatives(uint32_t aArg);
+
+ void GetServiceURI(nsString& aRetVal, ErrorResult& aRv) const;
+
+ void SetServiceURI(const nsAString& aArg, ErrorResult& aRv);
+
+ void Start(const Optional<NonNull<DOMMediaStream>>& aStream,
+ CallerType aCallerType, ErrorResult& aRv);
+
+ void Stop();
+
+ void Abort();
+
+ IMPL_EVENT_HANDLER(audiostart)
+ IMPL_EVENT_HANDLER(soundstart)
+ IMPL_EVENT_HANDLER(speechstart)
+ IMPL_EVENT_HANDLER(speechend)
+ IMPL_EVENT_HANDLER(soundend)
+ IMPL_EVENT_HANDLER(audioend)
+ IMPL_EVENT_HANDLER(result)
+ IMPL_EVENT_HANDLER(nomatch)
+ IMPL_EVENT_HANDLER(error)
+ IMPL_EVENT_HANDLER(start)
+ IMPL_EVENT_HANDLER(end)
+
+ enum EventType {
+ EVENT_START,
+ EVENT_STOP,
+ EVENT_ABORT,
+ EVENT_AUDIO_DATA,
+ EVENT_AUDIO_ERROR,
+ EVENT_RECOGNITIONSERVICE_INTERMEDIATE_RESULT,
+ EVENT_RECOGNITIONSERVICE_FINAL_RESULT,
+ EVENT_RECOGNITIONSERVICE_ERROR,
+ EVENT_COUNT
+ };
+
+ void NotifyTrackAdded(const RefPtr<MediaStreamTrack>& aTrack) override;
+ // aMessage should be valid UTF-8, but invalid UTF-8 byte sequences are
+ // replaced with the REPLACEMENT CHARACTER on conversion to UTF-16.
+ void DispatchError(EventType aErrorType,
+ SpeechRecognitionErrorCode aErrorCode,
+ const nsACString& aMessage);
+ template <int N>
+ void DispatchError(EventType aErrorType,
+ SpeechRecognitionErrorCode aErrorCode,
+ const char (&aMessage)[N]) {
+ DispatchError(aErrorType, aErrorCode, nsLiteralCString(aMessage));
+ }
+ uint32_t FillSamplesBuffer(const int16_t* aSamples, uint32_t aSampleCount);
+ uint32_t SplitSamplesBuffer(const int16_t* aSamplesBuffer,
+ uint32_t aSampleCount,
+ nsTArray<RefPtr<SharedBuffer>>& aResult);
+ AudioSegment* CreateAudioSegment(nsTArray<RefPtr<SharedBuffer>>& aChunks);
+ void FeedAudioData(nsMainThreadPtrHandle<SpeechRecognition>& aRecognition,
+ already_AddRefed<SharedBuffer> aSamples,
+ uint32_t aDuration, MediaTrackListener* aProvider,
+ TrackRate aTrackRate);
+
+ friend class SpeechEvent;
+
+ private:
+ virtual ~SpeechRecognition();
+
+ enum FSMState {
+ STATE_IDLE,
+ STATE_STARTING,
+ STATE_ESTIMATING,
+ STATE_WAITING_FOR_SPEECH,
+ STATE_RECOGNIZING,
+ STATE_WAITING_FOR_RESULT,
+ STATE_ABORTING,
+ STATE_COUNT
+ };
+
+ void SetState(FSMState state);
+ bool StateBetween(FSMState begin, FSMState end);
+
+ bool SetRecognitionService(ErrorResult& aRv);
+ bool ValidateAndSetGrammarList(ErrorResult& aRv);
+
+ NS_IMETHOD StartRecording(RefPtr<AudioStreamTrack>& aDOMStream);
+ RefPtr<GenericNonExclusivePromise> StopRecording();
+
+ uint32_t ProcessAudioSegment(AudioSegment* aSegment, TrackRate aTrackRate);
+ void NotifyError(SpeechEvent* aEvent);
+
+ void ProcessEvent(SpeechEvent* aEvent);
+ void Transition(SpeechEvent* aEvent);
+
+ void Reset();
+ void ResetAndEnd();
+ void WaitForAudioData(SpeechEvent* aEvent);
+ void StartedAudioCapture(SpeechEvent* aEvent);
+ void StopRecordingAndRecognize(SpeechEvent* aEvent);
+ void WaitForEstimation(SpeechEvent* aEvent);
+ void DetectSpeech(SpeechEvent* aEvent);
+ void WaitForSpeechEnd(SpeechEvent* aEvent);
+ void NotifyFinalResult(SpeechEvent* aEvent);
+ void DoNothing(SpeechEvent* aEvent);
+ void AbortSilently(SpeechEvent* aEvent);
+ void AbortError(SpeechEvent* aEvent);
+
+ RefPtr<DOMMediaStream> mStream;
+ RefPtr<AudioStreamTrack> mTrack;
+ bool mTrackIsOwned = false;
+ RefPtr<GenericNonExclusivePromise> mStopRecordingPromise;
+ RefPtr<SpeechTrackListener> mSpeechListener;
+ nsCOMPtr<nsISpeechRecognitionService> mRecognitionService;
+ RefPtr<media::ShutdownBlocker> mShutdownBlocker;
+ // TaskQueue responsible for pre-processing the samples by the service
+ // it runs in a separate thread from the main thread
+ RefPtr<TaskQueue> mEncodeTaskQueue;
+
+ // A generation ID of the MediaStream a started session is for, so that
+ // a gUM request that resolves after the session has stopped, and a new
+ // one has started, can exit early. Main thread only. Can wrap.
+ uint8_t mStreamGeneration = 0;
+
+ FSMState mCurrentState;
+
+ Endpointer mEndpointer;
+ uint32_t mEstimationSamples;
+
+ uint32_t mAudioSamplesPerChunk;
+
+ // maximum amount of seconds the engine will wait for voice
+ // until returning a 'no speech detected' error
+ uint32_t mSpeechDetectionTimeoutMs;
+
+ // buffer holds one chunk of mAudioSamplesPerChunk
+ // samples before feeding it to mEndpointer
+ RefPtr<SharedBuffer> mAudioSamplesBuffer;
+ uint32_t mBufferedSamples;
+
+ nsCOMPtr<nsITimer> mSpeechDetectionTimer;
+ bool mAborted;
+
+ nsString mLang;
+
+ RefPtr<SpeechGrammarList> mSpeechGrammarList;
+
+ // private flag used to hold if the user called the setContinuous() method
+ // of the API
+ bool mContinuous;
+
+ // WebSpeechAPI (http://bit.ly/1gIl7DC) states:
+ //
+ // 1. Default value MUST be false
+ // 2. If true, interim results SHOULD be returned
+ // 3. If false, interim results MUST NOT be returned
+ //
+ // Pocketsphinx does not return interm results; so, defaulting
+ // mInterimResults to false, then ignoring its subsequent value
+ // is a conforming implementation.
+ bool mInterimResults;
+
+ // WebSpeechAPI (http://bit.ly/1JAiqeo) states:
+ //
+ // 1. Default value is 1
+ // 2. Subsequent value is the "maximum number of SpeechRecognitionAlternatives
+ // per result"
+ //
+ // Pocketsphinx can only return at maximum a single
+ // SpeechRecognitionAlternative per SpeechRecognitionResult. So defaulting
+ // mMaxAlternatives to 1, for all non zero values ignoring mMaxAlternatives
+ // while for a 0 value returning no SpeechRecognitionAlternative per result is
+ // a conforming implementation.
+ uint32_t mMaxAlternatives;
+
+ void ProcessTestEventRequest(nsISupports* aSubject,
+ const nsAString& aEventName);
+
+ const char* GetName(FSMState aId);
+ const char* GetName(SpeechEvent* aEvent);
+};
+
+class SpeechEvent : public Runnable {
+ public:
+ SpeechEvent(SpeechRecognition* aRecognition,
+ SpeechRecognition::EventType aType);
+ SpeechEvent(nsMainThreadPtrHandle<SpeechRecognition>& aRecognition,
+ SpeechRecognition::EventType aType);
+
+ ~SpeechEvent();
+
+ NS_IMETHOD Run() override;
+ AudioSegment* mAudioSegment;
+ RefPtr<SpeechRecognitionResultList>
+ mRecognitionResultList; // TODO: make this a session being passed which
+ // also has index and stuff
+ RefPtr<SpeechRecognitionError> mError;
+
+ friend class SpeechRecognition;
+
+ private:
+ nsMainThreadPtrHandle<SpeechRecognition> mRecognition;
+
+ // for AUDIO_DATA events, keep a reference to the provider
+ // of the data (i.e., the SpeechTrackListener) to ensure it
+ // is kept alive (and keeps SpeechRecognition alive) until this
+ // event gets processed.
+ RefPtr<MediaTrackListener> mProvider;
+ SpeechRecognition::EventType mType;
+ TrackRate mTrackRate;
+};
+
+} // namespace dom
+
+inline nsISupports* ToSupports(dom::SpeechRecognition* aRec) {
+ return ToSupports(static_cast<DOMEventTargetHelper*>(aRec));
+}
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webspeech/recognition/SpeechRecognitionAlternative.cpp b/dom/media/webspeech/recognition/SpeechRecognitionAlternative.cpp
new file mode 100644
index 0000000000..4dee9090a7
--- /dev/null
+++ b/dom/media/webspeech/recognition/SpeechRecognitionAlternative.cpp
@@ -0,0 +1,44 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SpeechRecognitionAlternative.h"
+
+#include "mozilla/dom/SpeechRecognitionAlternativeBinding.h"
+
+#include "SpeechRecognition.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(SpeechRecognitionAlternative, mParent)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(SpeechRecognitionAlternative)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(SpeechRecognitionAlternative)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SpeechRecognitionAlternative)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+SpeechRecognitionAlternative::SpeechRecognitionAlternative(
+ SpeechRecognition* aParent)
+ : mConfidence(0), mParent(aParent) {}
+
+SpeechRecognitionAlternative::~SpeechRecognitionAlternative() = default;
+
+JSObject* SpeechRecognitionAlternative::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return SpeechRecognitionAlternative_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsISupports* SpeechRecognitionAlternative::GetParentObject() const {
+ return static_cast<EventTarget*>(mParent.get());
+}
+
+void SpeechRecognitionAlternative::GetTranscript(nsString& aRetVal) const {
+ aRetVal = mTranscript;
+}
+
+float SpeechRecognitionAlternative::Confidence() const { return mConfidence; }
+
+} // namespace mozilla::dom
diff --git a/dom/media/webspeech/recognition/SpeechRecognitionAlternative.h b/dom/media/webspeech/recognition/SpeechRecognitionAlternative.h
new file mode 100644
index 0000000000..017d869943
--- /dev/null
+++ b/dom/media/webspeech/recognition/SpeechRecognitionAlternative.h
@@ -0,0 +1,49 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_SpeechRecognitionAlternative_h
+#define mozilla_dom_SpeechRecognitionAlternative_h
+
+#include "nsCycleCollectionParticipant.h"
+#include "nsString.h"
+#include "nsWrapperCache.h"
+#include "js/TypeDecls.h"
+
+#include "mozilla/Attributes.h"
+
+namespace mozilla::dom {
+
+class SpeechRecognition;
+
+class SpeechRecognitionAlternative final : public nsISupports,
+ public nsWrapperCache {
+ public:
+ explicit SpeechRecognitionAlternative(SpeechRecognition* aParent);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(SpeechRecognitionAlternative)
+
+ nsISupports* GetParentObject() const;
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ void GetTranscript(nsString& aRetVal) const;
+
+ float Confidence() const;
+
+ nsString mTranscript;
+ float mConfidence;
+
+ private:
+ ~SpeechRecognitionAlternative();
+
+ RefPtr<SpeechRecognition> mParent;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webspeech/recognition/SpeechRecognitionResult.cpp b/dom/media/webspeech/recognition/SpeechRecognitionResult.cpp
new file mode 100644
index 0000000000..009281b234
--- /dev/null
+++ b/dom/media/webspeech/recognition/SpeechRecognitionResult.cpp
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SpeechRecognitionResult.h"
+#include "mozilla/dom/SpeechRecognitionResultBinding.h"
+
+#include "SpeechRecognition.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(SpeechRecognitionResult, mParent)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(SpeechRecognitionResult)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(SpeechRecognitionResult)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SpeechRecognitionResult)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+SpeechRecognitionResult::SpeechRecognitionResult(SpeechRecognition* aParent)
+ : mParent(aParent) {}
+
+SpeechRecognitionResult::~SpeechRecognitionResult() = default;
+
+JSObject* SpeechRecognitionResult::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return SpeechRecognitionResult_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsISupports* SpeechRecognitionResult::GetParentObject() const {
+ return static_cast<EventTarget*>(mParent.get());
+}
+
+already_AddRefed<SpeechRecognitionAlternative>
+SpeechRecognitionResult::IndexedGetter(uint32_t aIndex, bool& aPresent) {
+ if (aIndex >= Length()) {
+ aPresent = false;
+ return nullptr;
+ }
+
+ aPresent = true;
+ return Item(aIndex);
+}
+
+uint32_t SpeechRecognitionResult::Length() const { return mItems.Length(); }
+
+already_AddRefed<SpeechRecognitionAlternative> SpeechRecognitionResult::Item(
+ uint32_t aIndex) {
+ RefPtr<SpeechRecognitionAlternative> alternative = mItems.ElementAt(aIndex);
+ return alternative.forget();
+}
+
+bool SpeechRecognitionResult::IsFinal() const {
+ return true; // TODO
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webspeech/recognition/SpeechRecognitionResult.h b/dom/media/webspeech/recognition/SpeechRecognitionResult.h
new file mode 100644
index 0000000000..fc9e8fd660
--- /dev/null
+++ b/dom/media/webspeech/recognition/SpeechRecognitionResult.h
@@ -0,0 +1,54 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_SpeechRecognitionResult_h
+#define mozilla_dom_SpeechRecognitionResult_h
+
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsWrapperCache.h"
+#include "nsTArray.h"
+#include "js/TypeDecls.h"
+
+#include "mozilla/Attributes.h"
+
+#include "SpeechRecognitionAlternative.h"
+
+namespace mozilla::dom {
+
+class SpeechRecognitionResult final : public nsISupports,
+ public nsWrapperCache {
+ public:
+ explicit SpeechRecognitionResult(SpeechRecognition* aParent);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(SpeechRecognitionResult)
+
+ nsISupports* GetParentObject() const;
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ uint32_t Length() const;
+
+ already_AddRefed<SpeechRecognitionAlternative> Item(uint32_t aIndex);
+
+ bool IsFinal() const;
+
+ already_AddRefed<SpeechRecognitionAlternative> IndexedGetter(uint32_t aIndex,
+ bool& aPresent);
+
+ nsTArray<RefPtr<SpeechRecognitionAlternative>> mItems;
+
+ private:
+ ~SpeechRecognitionResult();
+
+ RefPtr<SpeechRecognition> mParent;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webspeech/recognition/SpeechRecognitionResultList.cpp b/dom/media/webspeech/recognition/SpeechRecognitionResultList.cpp
new file mode 100644
index 0000000000..2aa81a5982
--- /dev/null
+++ b/dom/media/webspeech/recognition/SpeechRecognitionResultList.cpp
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SpeechRecognitionResultList.h"
+
+#include "mozilla/dom/SpeechRecognitionResultListBinding.h"
+
+#include "SpeechRecognition.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(SpeechRecognitionResultList, mParent,
+ mItems)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(SpeechRecognitionResultList)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(SpeechRecognitionResultList)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SpeechRecognitionResultList)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+SpeechRecognitionResultList::SpeechRecognitionResultList(
+ SpeechRecognition* aParent)
+ : mParent(aParent) {}
+
+SpeechRecognitionResultList::~SpeechRecognitionResultList() = default;
+
+nsISupports* SpeechRecognitionResultList::GetParentObject() const {
+ return static_cast<EventTarget*>(mParent.get());
+}
+
+JSObject* SpeechRecognitionResultList::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return SpeechRecognitionResultList_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+already_AddRefed<SpeechRecognitionResult>
+SpeechRecognitionResultList::IndexedGetter(uint32_t aIndex, bool& aPresent) {
+ if (aIndex >= Length()) {
+ aPresent = false;
+ return nullptr;
+ }
+
+ aPresent = true;
+ return Item(aIndex);
+}
+
+uint32_t SpeechRecognitionResultList::Length() const { return mItems.Length(); }
+
+already_AddRefed<SpeechRecognitionResult> SpeechRecognitionResultList::Item(
+ uint32_t aIndex) {
+ RefPtr<SpeechRecognitionResult> result = mItems.ElementAt(aIndex);
+ return result.forget();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webspeech/recognition/SpeechRecognitionResultList.h b/dom/media/webspeech/recognition/SpeechRecognitionResultList.h
new file mode 100644
index 0000000000..b45659564b
--- /dev/null
+++ b/dom/media/webspeech/recognition/SpeechRecognitionResultList.h
@@ -0,0 +1,53 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_SpeechRecognitionResultList_h
+#define mozilla_dom_SpeechRecognitionResultList_h
+
+#include "nsCycleCollectionParticipant.h"
+#include "nsWrapperCache.h"
+#include "nsTArray.h"
+#include "js/TypeDecls.h"
+
+#include "mozilla/Attributes.h"
+
+#include "SpeechRecognitionResult.h"
+
+namespace mozilla::dom {
+
+class SpeechRecognition;
+
+class SpeechRecognitionResultList final : public nsISupports,
+ public nsWrapperCache {
+ public:
+ explicit SpeechRecognitionResultList(SpeechRecognition* aParent);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(SpeechRecognitionResultList)
+
+ nsISupports* GetParentObject() const;
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ uint32_t Length() const;
+
+ already_AddRefed<SpeechRecognitionResult> Item(uint32_t aIndex);
+
+ already_AddRefed<SpeechRecognitionResult> IndexedGetter(uint32_t aIndex,
+ bool& aPresent);
+
+ nsTArray<RefPtr<SpeechRecognitionResult>> mItems;
+
+ private:
+ ~SpeechRecognitionResultList();
+
+ RefPtr<SpeechRecognition> mParent;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webspeech/recognition/SpeechTrackListener.cpp b/dom/media/webspeech/recognition/SpeechTrackListener.cpp
new file mode 100644
index 0000000000..036ff753ba
--- /dev/null
+++ b/dom/media/webspeech/recognition/SpeechTrackListener.cpp
@@ -0,0 +1,92 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SpeechTrackListener.h"
+
+#include "SpeechRecognition.h"
+#include "nsProxyRelease.h"
+
+namespace mozilla::dom {
+
+SpeechTrackListener::SpeechTrackListener(SpeechRecognition* aRecognition)
+ : mRecognition(new nsMainThreadPtrHolder<SpeechRecognition>(
+ "SpeechTrackListener::SpeechTrackListener", aRecognition, false)),
+ mRemovedPromise(
+ mRemovedHolder.Ensure("SpeechTrackListener::mRemovedPromise")) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mRemovedPromise->Then(GetCurrentSerialEventTarget(), __func__,
+ [self = RefPtr<SpeechTrackListener>(this), this] {
+ mRecognition = nullptr;
+ });
+}
+
+void SpeechTrackListener::NotifyQueuedChanges(
+ MediaTrackGraph* aGraph, TrackTime aTrackOffset,
+ const MediaSegment& aQueuedMedia) {
+ AudioSegment* audio = const_cast<AudioSegment*>(
+ static_cast<const AudioSegment*>(&aQueuedMedia));
+
+ AudioSegment::ChunkIterator iterator(*audio);
+ while (!iterator.IsEnded()) {
+ // Skip over-large chunks so we don't crash!
+ if (iterator->GetDuration() > INT_MAX) {
+ continue;
+ }
+ int duration = int(iterator->GetDuration());
+
+ if (iterator->IsNull()) {
+ nsTArray<int16_t> nullData;
+ PodZero(nullData.AppendElements(duration), duration);
+ ConvertAndDispatchAudioChunk(duration, iterator->mVolume,
+ nullData.Elements(), aGraph->GraphRate());
+ } else {
+ AudioSampleFormat format = iterator->mBufferFormat;
+
+ MOZ_ASSERT(format == AUDIO_FORMAT_S16 || format == AUDIO_FORMAT_FLOAT32);
+
+ if (format == AUDIO_FORMAT_S16) {
+ ConvertAndDispatchAudioChunk(
+ duration, iterator->mVolume,
+ static_cast<const int16_t*>(iterator->mChannelData[0]),
+ aGraph->GraphRate());
+ } else if (format == AUDIO_FORMAT_FLOAT32) {
+ ConvertAndDispatchAudioChunk(
+ duration, iterator->mVolume,
+ static_cast<const float*>(iterator->mChannelData[0]),
+ aGraph->GraphRate());
+ }
+ }
+
+ iterator.Next();
+ }
+}
+
+template <typename SampleFormatType>
+void SpeechTrackListener::ConvertAndDispatchAudioChunk(int aDuration,
+ float aVolume,
+ SampleFormatType* aData,
+ TrackRate aTrackRate) {
+ CheckedInt<size_t> bufferSize(sizeof(int16_t));
+ bufferSize *= aDuration;
+ bufferSize *= 1; // channel
+ RefPtr<SharedBuffer> samples(SharedBuffer::Create(bufferSize));
+
+ int16_t* to = static_cast<int16_t*>(samples->Data());
+ ConvertAudioSamplesWithScale(aData, to, aDuration, aVolume);
+
+ mRecognition->FeedAudioData(mRecognition, samples.forget(), aDuration, this,
+ aTrackRate);
+}
+
+void SpeechTrackListener::NotifyEnded(MediaTrackGraph* aGraph) {
+ // TODO dispatch SpeechEnd event so services can be informed
+}
+
+void SpeechTrackListener::NotifyRemoved(MediaTrackGraph* aGraph) {
+ mRemovedHolder.ResolveIfExists(true, __func__);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webspeech/recognition/SpeechTrackListener.h b/dom/media/webspeech/recognition/SpeechTrackListener.h
new file mode 100644
index 0000000000..423a5b0317
--- /dev/null
+++ b/dom/media/webspeech/recognition/SpeechTrackListener.h
@@ -0,0 +1,50 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_SpeechStreamListener_h
+#define mozilla_dom_SpeechStreamListener_h
+
+#include "MediaTrackGraph.h"
+#include "MediaTrackListener.h"
+#include "AudioSegment.h"
+#include "mozilla/MozPromise.h"
+
+namespace mozilla {
+
+class AudioSegment;
+
+namespace dom {
+
+class SpeechRecognition;
+
+class SpeechTrackListener : public MediaTrackListener {
+ public:
+ explicit SpeechTrackListener(SpeechRecognition* aRecognition);
+ ~SpeechTrackListener() = default;
+
+ void NotifyQueuedChanges(MediaTrackGraph* aGraph, TrackTime aTrackOffset,
+ const MediaSegment& aQueuedMedia) override;
+
+ void NotifyEnded(MediaTrackGraph* aGraph) override;
+
+ void NotifyRemoved(MediaTrackGraph* aGraph) override;
+
+ private:
+ template <typename SampleFormatType>
+ void ConvertAndDispatchAudioChunk(int aDuration, float aVolume,
+ SampleFormatType* aData,
+ TrackRate aTrackRate);
+ nsMainThreadPtrHandle<SpeechRecognition> mRecognition;
+ MozPromiseHolder<GenericNonExclusivePromise> mRemovedHolder;
+
+ public:
+ const RefPtr<GenericNonExclusivePromise> mRemovedPromise;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webspeech/recognition/endpointer.cc b/dom/media/webspeech/recognition/endpointer.cc
new file mode 100644
index 0000000000..2347043d4b
--- /dev/null
+++ b/dom/media/webspeech/recognition/endpointer.cc
@@ -0,0 +1,193 @@
+// Copyright (c) 2013 The Chromium Authors. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "endpointer.h"
+
+#include "AudioSegment.h"
+
+namespace {
+const int kFrameRate = 200; // 1 frame = 5ms of audio.
+}
+
+namespace mozilla {
+
+Endpointer::Endpointer(int sample_rate)
+ : speech_input_possibly_complete_silence_length_us_(-1),
+ speech_input_complete_silence_length_us_(-1),
+ audio_frame_time_us_(0),
+ sample_rate_(sample_rate),
+ frame_size_(0) {
+ Reset();
+
+ frame_size_ = static_cast<int>(sample_rate / static_cast<float>(kFrameRate));
+
+ speech_input_minimum_length_us_ =
+ static_cast<int64_t>(1.7 * 1000000);
+ speech_input_complete_silence_length_us_ =
+ static_cast<int64_t>(0.5 * 1000000);
+ long_speech_input_complete_silence_length_us_ = -1;
+ long_speech_length_us_ = -1;
+ speech_input_possibly_complete_silence_length_us_ =
+ 1 * 1000000;
+
+ // Set the default configuration for Push To Talk mode.
+ EnergyEndpointerParams ep_config;
+ ep_config.set_frame_period(1.0f / static_cast<float>(kFrameRate));
+ ep_config.set_frame_duration(1.0f / static_cast<float>(kFrameRate));
+ ep_config.set_endpoint_margin(0.2f);
+ ep_config.set_onset_window(0.15f);
+ ep_config.set_speech_on_window(0.4f);
+ ep_config.set_offset_window(0.15f);
+ ep_config.set_onset_detect_dur(0.09f);
+ ep_config.set_onset_confirm_dur(0.075f);
+ ep_config.set_on_maintain_dur(0.10f);
+ ep_config.set_offset_confirm_dur(0.12f);
+ ep_config.set_decision_threshold(1000.0f);
+ ep_config.set_min_decision_threshold(50.0f);
+ ep_config.set_fast_update_dur(0.2f);
+ ep_config.set_sample_rate(static_cast<float>(sample_rate));
+ ep_config.set_min_fundamental_frequency(57.143f);
+ ep_config.set_max_fundamental_frequency(400.0f);
+ ep_config.set_contamination_rejection_period(0.25f);
+ energy_endpointer_.Init(ep_config);
+}
+
+void Endpointer::Reset() {
+ old_ep_status_ = EP_PRE_SPEECH;
+ waiting_for_speech_possibly_complete_timeout_ = false;
+ waiting_for_speech_complete_timeout_ = false;
+ speech_previously_detected_ = false;
+ speech_input_complete_ = false;
+ audio_frame_time_us_ = 0; // Reset time for packets sent to endpointer.
+ speech_end_time_us_ = -1;
+ speech_start_time_us_ = -1;
+}
+
+void Endpointer::StartSession() {
+ Reset();
+ energy_endpointer_.StartSession();
+}
+
+void Endpointer::EndSession() {
+ energy_endpointer_.EndSession();
+}
+
+void Endpointer::SetEnvironmentEstimationMode() {
+ Reset();
+ energy_endpointer_.SetEnvironmentEstimationMode();
+}
+
+void Endpointer::SetUserInputMode() {
+ energy_endpointer_.SetUserInputMode();
+}
+
+EpStatus Endpointer::Status(int64_t *time) {
+ return energy_endpointer_.Status(time);
+}
+
+EpStatus Endpointer::ProcessAudio(const AudioChunk& raw_audio, float* rms_out) {
+ MOZ_ASSERT(raw_audio.mBufferFormat == AUDIO_FORMAT_S16, "Audio is not in 16 bit format");
+ const int16_t* audio_data = static_cast<const int16_t*>(raw_audio.mChannelData[0]);
+ const int num_samples = raw_audio.mDuration;
+ EpStatus ep_status = EP_PRE_SPEECH;
+
+ // Process the input data in blocks of frame_size_, dropping any incomplete
+ // frames at the end (which is ok since typically the caller will be recording
+ // audio in multiples of our frame size).
+ int sample_index = 0;
+ while (sample_index + frame_size_ <= num_samples) {
+ // Have the endpointer process the frame.
+ energy_endpointer_.ProcessAudioFrame(audio_frame_time_us_,
+ audio_data + sample_index,
+ frame_size_,
+ rms_out);
+ sample_index += frame_size_;
+ audio_frame_time_us_ += (frame_size_ * 1000000) /
+ sample_rate_;
+
+ // Get the status of the endpointer.
+ int64_t ep_time;
+ ep_status = energy_endpointer_.Status(&ep_time);
+ if (old_ep_status_ != ep_status)
+ fprintf(stderr, "Status changed old= %d, new= %d\n", old_ep_status_, ep_status);
+
+ // Handle state changes.
+ if ((EP_SPEECH_PRESENT == ep_status) &&
+ (EP_POSSIBLE_ONSET == old_ep_status_)) {
+ speech_end_time_us_ = -1;
+ waiting_for_speech_possibly_complete_timeout_ = false;
+ waiting_for_speech_complete_timeout_ = false;
+ // Trigger SpeechInputDidStart event on first detection.
+ if (false == speech_previously_detected_) {
+ speech_previously_detected_ = true;
+ speech_start_time_us_ = ep_time;
+ }
+ }
+ if ((EP_PRE_SPEECH == ep_status) &&
+ (EP_POSSIBLE_OFFSET == old_ep_status_)) {
+ speech_end_time_us_ = ep_time;
+ waiting_for_speech_possibly_complete_timeout_ = true;
+ waiting_for_speech_complete_timeout_ = true;
+ }
+ if (ep_time > speech_input_minimum_length_us_) {
+ // Speech possibly complete timeout.
+ if ((waiting_for_speech_possibly_complete_timeout_) &&
+ (ep_time - speech_end_time_us_ >
+ speech_input_possibly_complete_silence_length_us_)) {
+ waiting_for_speech_possibly_complete_timeout_ = false;
+ }
+ if (waiting_for_speech_complete_timeout_) {
+ // The length of the silence timeout period can be held constant, or it
+ // can be changed after a fixed amount of time from the beginning of
+ // speech.
+ bool has_stepped_silence =
+ (long_speech_length_us_ > 0) &&
+ (long_speech_input_complete_silence_length_us_ > 0);
+ int64_t requested_silence_length;
+ if (has_stepped_silence &&
+ (ep_time - speech_start_time_us_) > long_speech_length_us_) {
+ requested_silence_length =
+ long_speech_input_complete_silence_length_us_;
+ } else {
+ requested_silence_length =
+ speech_input_complete_silence_length_us_;
+ }
+
+ // Speech complete timeout.
+ if ((ep_time - speech_end_time_us_) > requested_silence_length) {
+ waiting_for_speech_complete_timeout_ = false;
+ speech_input_complete_ = true;
+ }
+ }
+ }
+ old_ep_status_ = ep_status;
+ }
+ return ep_status;
+}
+
+} // namespace mozilla
diff --git a/dom/media/webspeech/recognition/endpointer.h b/dom/media/webspeech/recognition/endpointer.h
new file mode 100644
index 0000000000..7879d6b9f3
--- /dev/null
+++ b/dom/media/webspeech/recognition/endpointer.h
@@ -0,0 +1,180 @@
+// Copyright (c) 2013 The Chromium Authors. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef CONTENT_BROWSER_SPEECH_ENDPOINTER_ENDPOINTER_H_
+#define CONTENT_BROWSER_SPEECH_ENDPOINTER_ENDPOINTER_H_
+
+#include "energy_endpointer.h"
+
+namespace mozilla {
+
+struct AudioChunk;
+
+// A simple interface to the underlying energy-endpointer implementation, this
+// class lets callers provide audio as being recorded and let them poll to find
+// when the user has stopped speaking.
+//
+// There are two events that may trigger the end of speech:
+//
+// speechInputPossiblyComplete event:
+//
+// Signals that silence/noise has been detected for a *short* amount of
+// time after some speech has been detected. It can be used for low latency
+// UI feedback. To disable it, set it to a large amount.
+//
+// speechInputComplete event:
+//
+// This event is intended to signal end of input and to stop recording.
+// The amount of time to wait after speech is set by
+// speech_input_complete_silence_length_ and optionally two other
+// parameters (see below).
+// This time can be held constant, or can change as more speech is detected.
+// In the latter case, the time changes after a set amount of time from the
+// *beginning* of speech. This is motivated by the expectation that there
+// will be two distinct types of inputs: short search queries and longer
+// dictation style input.
+//
+// Three parameters are used to define the piecewise constant timeout function.
+// The timeout length is speech_input_complete_silence_length until
+// long_speech_length, when it changes to
+// long_speech_input_complete_silence_length.
+class Endpointer {
+ public:
+ explicit Endpointer(int sample_rate);
+
+ // Start the endpointer. This should be called at the beginning of a session.
+ void StartSession();
+
+ // Stop the endpointer.
+ void EndSession();
+
+ // Start environment estimation. Audio will be used for environment estimation
+ // i.e. noise level estimation.
+ void SetEnvironmentEstimationMode();
+
+ // Start user input. This should be called when the user indicates start of
+ // input, e.g. by pressing a button.
+ void SetUserInputMode();
+
+ // Process a segment of audio, which may be more than one frame.
+ // The status of the last frame will be returned.
+ EpStatus ProcessAudio(const AudioChunk& raw_audio, float* rms_out);
+
+ // Get the status of the endpointer.
+ EpStatus Status(int64_t *time_us);
+
+ // Get the expected frame size for audio chunks. Audio chunks are expected
+ // to contain a number of samples that is a multiple of this number, and extra
+ // samples will be dropped.
+ int32_t FrameSize() const {
+ return frame_size_;
+ }
+
+ // Returns true if the endpointer detected reasonable audio levels above
+ // background noise which could be user speech, false if not.
+ bool DidStartReceivingSpeech() const {
+ return speech_previously_detected_;
+ }
+
+ bool IsEstimatingEnvironment() const {
+ return energy_endpointer_.estimating_environment();
+ }
+
+ void set_speech_input_complete_silence_length(int64_t time_us) {
+ speech_input_complete_silence_length_us_ = time_us;
+ }
+
+ void set_long_speech_input_complete_silence_length(int64_t time_us) {
+ long_speech_input_complete_silence_length_us_ = time_us;
+ }
+
+ void set_speech_input_possibly_complete_silence_length(int64_t time_us) {
+ speech_input_possibly_complete_silence_length_us_ = time_us;
+ }
+
+ void set_long_speech_length(int64_t time_us) {
+ long_speech_length_us_ = time_us;
+ }
+
+ bool speech_input_complete() const {
+ return speech_input_complete_;
+ }
+
+ // RMS background noise level in dB.
+ float NoiseLevelDb() const { return energy_endpointer_.GetNoiseLevelDb(); }
+
+ private:
+ // Reset internal states. Helper method common to initial input utterance
+ // and following input utternaces.
+ void Reset();
+
+ // Minimum allowable length of speech input.
+ int64_t speech_input_minimum_length_us_;
+
+ // The speechInputPossiblyComplete event signals that silence/noise has been
+ // detected for a *short* amount of time after some speech has been detected.
+ // This proporty specifies the time period.
+ int64_t speech_input_possibly_complete_silence_length_us_;
+
+ // The speechInputComplete event signals that silence/noise has been
+ // detected for a *long* amount of time after some speech has been detected.
+ // This property specifies the time period.
+ int64_t speech_input_complete_silence_length_us_;
+
+ // Same as above, this specifies the required silence period after speech
+ // detection. This period is used instead of
+ // speech_input_complete_silence_length_ when the utterance is longer than
+ // long_speech_length_. This parameter is optional.
+ int64_t long_speech_input_complete_silence_length_us_;
+
+ // The period of time after which the endpointer should consider
+ // long_speech_input_complete_silence_length_ as a valid silence period
+ // instead of speech_input_complete_silence_length_. This parameter is
+ // optional.
+ int64_t long_speech_length_us_;
+
+ // First speech onset time, used in determination of speech complete timeout.
+ int64_t speech_start_time_us_;
+
+ // Most recent end time, used in determination of speech complete timeout.
+ int64_t speech_end_time_us_;
+
+ int64_t audio_frame_time_us_;
+ EpStatus old_ep_status_;
+ bool waiting_for_speech_possibly_complete_timeout_;
+ bool waiting_for_speech_complete_timeout_;
+ bool speech_previously_detected_;
+ bool speech_input_complete_;
+ EnergyEndpointer energy_endpointer_;
+ int sample_rate_;
+ int32_t frame_size_;
+};
+
+} // namespace mozilla
+
+#endif // CONTENT_BROWSER_SPEECH_ENDPOINTER_ENDPOINTER_H_
diff --git a/dom/media/webspeech/recognition/energy_endpointer.cc b/dom/media/webspeech/recognition/energy_endpointer.cc
new file mode 100644
index 0000000000..b1c1ee0bcf
--- /dev/null
+++ b/dom/media/webspeech/recognition/energy_endpointer.cc
@@ -0,0 +1,393 @@
+// Copyright (c) 2013 The Chromium Authors. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "energy_endpointer.h"
+
+#include <math.h>
+
+namespace {
+
+// Returns the RMS (quadratic mean) of the input signal.
+float RMS(const int16_t* samples, int num_samples) {
+ int64_t ssq_int64_t = 0;
+ int64_t sum_int64_t = 0;
+ for (int i = 0; i < num_samples; ++i) {
+ sum_int64_t += samples[i];
+ ssq_int64_t += samples[i] * samples[i];
+ }
+ // now convert to floats.
+ double sum = static_cast<double>(sum_int64_t);
+ sum /= num_samples;
+ double ssq = static_cast<double>(ssq_int64_t);
+ return static_cast<float>(sqrt((ssq / num_samples) - (sum * sum)));
+}
+
+int64_t Secs2Usecs(float seconds) {
+ return static_cast<int64_t>(0.5 + (1.0e6 * seconds));
+}
+
+float GetDecibel(float value) {
+ if (value > 1.0e-100)
+ return 20 * log10(value);
+ return -2000.0;
+}
+
+} // namespace
+
+namespace mozilla {
+
+// Stores threshold-crossing histories for making decisions about the speech
+// state.
+class EnergyEndpointer::HistoryRing {
+ public:
+ HistoryRing() : insertion_index_(0) {}
+
+ // Resets the ring to |size| elements each with state |initial_state|
+ void SetRing(int size, bool initial_state);
+
+ // Inserts a new entry into the ring and drops the oldest entry.
+ void Insert(int64_t time_us, bool decision);
+
+ // Returns the time in microseconds of the most recently added entry.
+ int64_t EndTime() const;
+
+ // Returns the sum of all intervals during which 'decision' is true within
+ // the time in seconds specified by 'duration'. The returned interval is
+ // in seconds.
+ float RingSum(float duration_sec);
+
+ private:
+ struct DecisionPoint {
+ int64_t time_us;
+ bool decision;
+ };
+
+ std::vector<DecisionPoint> decision_points_;
+ int insertion_index_; // Index at which the next item gets added/inserted.
+
+ HistoryRing(const HistoryRing&);
+ void operator=(const HistoryRing&);
+};
+
+void EnergyEndpointer::HistoryRing::SetRing(int size, bool initial_state) {
+ insertion_index_ = 0;
+ decision_points_.clear();
+ DecisionPoint init = { -1, initial_state };
+ decision_points_.resize(size, init);
+}
+
+void EnergyEndpointer::HistoryRing::Insert(int64_t time_us, bool decision) {
+ decision_points_[insertion_index_].time_us = time_us;
+ decision_points_[insertion_index_].decision = decision;
+ insertion_index_ = (insertion_index_ + 1) % decision_points_.size();
+}
+
+int64_t EnergyEndpointer::HistoryRing::EndTime() const {
+ int ind = insertion_index_ - 1;
+ if (ind < 0)
+ ind = decision_points_.size() - 1;
+ return decision_points_[ind].time_us;
+}
+
+float EnergyEndpointer::HistoryRing::RingSum(float duration_sec) {
+ if (decision_points_.empty())
+ return 0.0;
+
+ int64_t sum_us = 0;
+ int ind = insertion_index_ - 1;
+ if (ind < 0)
+ ind = decision_points_.size() - 1;
+ int64_t end_us = decision_points_[ind].time_us;
+ bool is_on = decision_points_[ind].decision;
+ int64_t start_us = end_us - static_cast<int64_t>(0.5 + (1.0e6 * duration_sec));
+ if (start_us < 0)
+ start_us = 0;
+ size_t n_summed = 1; // n points ==> (n-1) intervals
+ while ((decision_points_[ind].time_us > start_us) &&
+ (n_summed < decision_points_.size())) {
+ --ind;
+ if (ind < 0)
+ ind = decision_points_.size() - 1;
+ if (is_on)
+ sum_us += end_us - decision_points_[ind].time_us;
+ is_on = decision_points_[ind].decision;
+ end_us = decision_points_[ind].time_us;
+ n_summed++;
+ }
+
+ return 1.0e-6f * sum_us; // Returns total time that was super threshold.
+}
+
+EnergyEndpointer::EnergyEndpointer()
+ : status_(EP_PRE_SPEECH),
+ offset_confirm_dur_sec_(0),
+ endpointer_time_us_(0),
+ fast_update_frames_(0),
+ frame_counter_(0),
+ max_window_dur_(4.0),
+ sample_rate_(0),
+ history_(new HistoryRing()),
+ decision_threshold_(0),
+ estimating_environment_(false),
+ noise_level_(0),
+ rms_adapt_(0),
+ start_lag_(0),
+ end_lag_(0),
+ user_input_start_time_us_(0) {
+}
+
+EnergyEndpointer::~EnergyEndpointer() {
+}
+
+int EnergyEndpointer::TimeToFrame(float time) const {
+ return static_cast<int32_t>(0.5 + (time / params_.frame_period()));
+}
+
+void EnergyEndpointer::Restart(bool reset_threshold) {
+ status_ = EP_PRE_SPEECH;
+ user_input_start_time_us_ = 0;
+
+ if (reset_threshold) {
+ decision_threshold_ = params_.decision_threshold();
+ rms_adapt_ = decision_threshold_;
+ noise_level_ = params_.decision_threshold() / 2.0f;
+ frame_counter_ = 0; // Used for rapid initial update of levels.
+ }
+
+ // Set up the memories to hold the history windows.
+ history_->SetRing(TimeToFrame(max_window_dur_), false);
+
+ // Flag that indicates that current input should be used for
+ // estimating the environment. The user has not yet started input
+ // by e.g. pressed the push-to-talk button. By default, this is
+ // false for backward compatibility.
+ estimating_environment_ = false;
+}
+
+void EnergyEndpointer::Init(const EnergyEndpointerParams& params) {
+ params_ = params;
+
+ // Find the longest history interval to be used, and make the ring
+ // large enough to accommodate that number of frames. NOTE: This
+ // depends upon ep_frame_period being set correctly in the factory
+ // that did this instantiation.
+ max_window_dur_ = params_.onset_window();
+ if (params_.speech_on_window() > max_window_dur_)
+ max_window_dur_ = params_.speech_on_window();
+ if (params_.offset_window() > max_window_dur_)
+ max_window_dur_ = params_.offset_window();
+ Restart(true);
+
+ offset_confirm_dur_sec_ = params_.offset_window() -
+ params_.offset_confirm_dur();
+ if (offset_confirm_dur_sec_ < 0.0)
+ offset_confirm_dur_sec_ = 0.0;
+
+ user_input_start_time_us_ = 0;
+
+ // Flag that indicates that current input should be used for
+ // estimating the environment. The user has not yet started input
+ // by e.g. pressed the push-to-talk button. By default, this is
+ // false for backward compatibility.
+ estimating_environment_ = false;
+ // The initial value of the noise and speech levels is inconsequential.
+ // The level of the first frame will overwrite these values.
+ noise_level_ = params_.decision_threshold() / 2.0f;
+ fast_update_frames_ =
+ static_cast<int64_t>(params_.fast_update_dur() / params_.frame_period());
+
+ frame_counter_ = 0; // Used for rapid initial update of levels.
+
+ sample_rate_ = params_.sample_rate();
+ start_lag_ = static_cast<int>(sample_rate_ /
+ params_.max_fundamental_frequency());
+ end_lag_ = static_cast<int>(sample_rate_ /
+ params_.min_fundamental_frequency());
+}
+
+void EnergyEndpointer::StartSession() {
+ Restart(true);
+}
+
+void EnergyEndpointer::EndSession() {
+ status_ = EP_POST_SPEECH;
+}
+
+void EnergyEndpointer::SetEnvironmentEstimationMode() {
+ Restart(true);
+ estimating_environment_ = true;
+}
+
+void EnergyEndpointer::SetUserInputMode() {
+ estimating_environment_ = false;
+ user_input_start_time_us_ = endpointer_time_us_;
+}
+
+void EnergyEndpointer::ProcessAudioFrame(int64_t time_us,
+ const int16_t* samples,
+ int num_samples,
+ float* rms_out) {
+ endpointer_time_us_ = time_us;
+ float rms = RMS(samples, num_samples);
+
+ // Check that this is user input audio vs. pre-input adaptation audio.
+ // Input audio starts when the user indicates start of input, by e.g.
+ // pressing push-to-talk. Audio recieved prior to that is used to update
+ // noise and speech level estimates.
+ if (!estimating_environment_) {
+ bool decision = false;
+ if ((endpointer_time_us_ - user_input_start_time_us_) <
+ Secs2Usecs(params_.contamination_rejection_period())) {
+ decision = false;
+ //PR_LOG(GetSpeechRecognitionLog(), PR_LOG_DEBUG, ("decision: forced to false, time: %d", endpointer_time_us_));
+ } else {
+ decision = (rms > decision_threshold_);
+ }
+
+ history_->Insert(endpointer_time_us_, decision);
+
+ switch (status_) {
+ case EP_PRE_SPEECH:
+ if (history_->RingSum(params_.onset_window()) >
+ params_.onset_detect_dur()) {
+ status_ = EP_POSSIBLE_ONSET;
+ }
+ break;
+
+ case EP_POSSIBLE_ONSET: {
+ float tsum = history_->RingSum(params_.onset_window());
+ if (tsum > params_.onset_confirm_dur()) {
+ status_ = EP_SPEECH_PRESENT;
+ } else { // If signal is not maintained, drop back to pre-speech.
+ if (tsum <= params_.onset_detect_dur())
+ status_ = EP_PRE_SPEECH;
+ }
+ break;
+ }
+
+ case EP_SPEECH_PRESENT: {
+ // To induce hysteresis in the state residency, we allow a
+ // smaller residency time in the on_ring, than was required to
+ // enter the SPEECH_PERSENT state.
+ float on_time = history_->RingSum(params_.speech_on_window());
+ if (on_time < params_.on_maintain_dur())
+ status_ = EP_POSSIBLE_OFFSET;
+ break;
+ }
+
+ case EP_POSSIBLE_OFFSET:
+ if (history_->RingSum(params_.offset_window()) <=
+ offset_confirm_dur_sec_) {
+ // Note that this offset time may be beyond the end
+ // of the input buffer in a real-time system. It will be up
+ // to the RecognizerSession to decide what to do.
+ status_ = EP_PRE_SPEECH; // Automatically reset for next utterance.
+ } else { // If speech picks up again we allow return to SPEECH_PRESENT.
+ if (history_->RingSum(params_.speech_on_window()) >=
+ params_.on_maintain_dur())
+ status_ = EP_SPEECH_PRESENT;
+ }
+ break;
+
+ default:
+ break;
+ }
+
+ // If this is a quiet, non-speech region, slowly adapt the detection
+ // threshold to be about 6dB above the average RMS.
+ if ((!decision) && (status_ == EP_PRE_SPEECH)) {
+ decision_threshold_ = (0.98f * decision_threshold_) + (0.02f * 2 * rms);
+ rms_adapt_ = decision_threshold_;
+ } else {
+ // If this is in a speech region, adapt the decision threshold to
+ // be about 10dB below the average RMS. If the noise level is high,
+ // the threshold is pushed up.
+ // Adaptation up to a higher level is 5 times faster than decay to
+ // a lower level.
+ if ((status_ == EP_SPEECH_PRESENT) && decision) {
+ if (rms_adapt_ > rms) {
+ rms_adapt_ = (0.99f * rms_adapt_) + (0.01f * rms);
+ } else {
+ rms_adapt_ = (0.95f * rms_adapt_) + (0.05f * rms);
+ }
+ float target_threshold = 0.3f * rms_adapt_ + noise_level_;
+ decision_threshold_ = (.90f * decision_threshold_) +
+ (0.10f * target_threshold);
+ }
+ }
+
+ // Set a floor
+ if (decision_threshold_ < params_.min_decision_threshold())
+ decision_threshold_ = params_.min_decision_threshold();
+ }
+
+ // Update speech and noise levels.
+ UpdateLevels(rms);
+ ++frame_counter_;
+
+ if (rms_out)
+ *rms_out = GetDecibel(rms);
+}
+
+float EnergyEndpointer::GetNoiseLevelDb() const {
+ return GetDecibel(noise_level_);
+}
+
+void EnergyEndpointer::UpdateLevels(float rms) {
+ // Update quickly initially. We assume this is noise and that
+ // speech is 6dB above the noise.
+ if (frame_counter_ < fast_update_frames_) {
+ // Alpha increases from 0 to (k-1)/k where k is the number of time
+ // steps in the initial adaptation period.
+ float alpha = static_cast<float>(frame_counter_) /
+ static_cast<float>(fast_update_frames_);
+ noise_level_ = (alpha * noise_level_) + ((1 - alpha) * rms);
+ //PR_LOG(GetSpeechRecognitionLog(), PR_LOG_DEBUG, ("FAST UPDATE, frame_counter_ %d, fast_update_frames_ %d", frame_counter_, fast_update_frames_));
+ } else {
+ // Update Noise level. The noise level adapts quickly downward, but
+ // slowly upward. The noise_level_ parameter is not currently used
+ // for threshold adaptation. It is used for UI feedback.
+ if (noise_level_ < rms)
+ noise_level_ = (0.999f * noise_level_) + (0.001f * rms);
+ else
+ noise_level_ = (0.95f * noise_level_) + (0.05f * rms);
+ }
+ if (estimating_environment_ || (frame_counter_ < fast_update_frames_)) {
+ decision_threshold_ = noise_level_ * 2; // 6dB above noise level.
+ // Set a floor
+ if (decision_threshold_ < params_.min_decision_threshold())
+ decision_threshold_ = params_.min_decision_threshold();
+ }
+}
+
+EpStatus EnergyEndpointer::Status(int64_t* status_time) const {
+ *status_time = history_->EndTime();
+ return status_;
+}
+
+} // namespace mozilla
diff --git a/dom/media/webspeech/recognition/energy_endpointer.h b/dom/media/webspeech/recognition/energy_endpointer.h
new file mode 100644
index 0000000000..12d3c736e3
--- /dev/null
+++ b/dom/media/webspeech/recognition/energy_endpointer.h
@@ -0,0 +1,180 @@
+// Copyright (c) 2013 The Chromium Authors. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// The EnergyEndpointer class finds likely speech onset and offset points.
+//
+// The implementation described here is about the simplest possible.
+// It is based on timings of threshold crossings for overall signal
+// RMS. It is suitable for light weight applications.
+//
+// As written, the basic idea is that one specifies intervals that
+// must be occupied by super- and sub-threshold energy levels, and
+// defers decisions re onset and offset times until these
+// specifications have been met. Three basic intervals are tested: an
+// onset window, a speech-on window, and an offset window. We require
+// super-threshold to exceed some mimimum total durations in the onset
+// and speech-on windows before declaring the speech onset time, and
+// we specify a required sub-threshold residency in the offset window
+// before declaring speech offset. As the various residency requirements are
+// met, the EnergyEndpointer instance assumes various states, and can return the
+// ID of these states to the client (see EpStatus below).
+//
+// The levels of the speech and background noise are continuously updated. It is
+// important that the background noise level be estimated initially for
+// robustness in noisy conditions. The first frames are assumed to be background
+// noise and a fast update rate is used for the noise level. The duration for
+// fast update is controlled by the fast_update_dur_ paramter.
+//
+// If used in noisy conditions, the endpointer should be started and run in the
+// EnvironmentEstimation mode, for at least 200ms, before switching to
+// UserInputMode.
+// Audio feedback contamination can appear in the input audio, if not cut
+// out or handled by echo cancellation. Audio feedback can trigger a false
+// accept. The false accepts can be ignored by setting
+// ep_contamination_rejection_period.
+
+#ifndef CONTENT_BROWSER_SPEECH_ENDPOINTER_ENERGY_ENDPOINTER_H_
+#define CONTENT_BROWSER_SPEECH_ENDPOINTER_ENERGY_ENDPOINTER_H_
+
+#include <vector>
+
+#include "mozilla/UniquePtr.h"
+
+#include "energy_endpointer_params.h"
+
+namespace mozilla {
+
+// Endpointer status codes
+enum EpStatus {
+ EP_PRE_SPEECH = 10,
+ EP_POSSIBLE_ONSET,
+ EP_SPEECH_PRESENT,
+ EP_POSSIBLE_OFFSET,
+ EP_POST_SPEECH,
+};
+
+class EnergyEndpointer {
+ public:
+ // The default construction MUST be followed by Init(), before any
+ // other use can be made of the instance.
+ EnergyEndpointer();
+ virtual ~EnergyEndpointer();
+
+ void Init(const EnergyEndpointerParams& params);
+
+ // Start the endpointer. This should be called at the beginning of a session.
+ void StartSession();
+
+ // Stop the endpointer.
+ void EndSession();
+
+ // Start environment estimation. Audio will be used for environment estimation
+ // i.e. noise level estimation.
+ void SetEnvironmentEstimationMode();
+
+ // Start user input. This should be called when the user indicates start of
+ // input, e.g. by pressing a button.
+ void SetUserInputMode();
+
+ // Computes the next input frame and modifies EnergyEndpointer status as
+ // appropriate based on the computation.
+ void ProcessAudioFrame(int64_t time_us,
+ const int16_t* samples, int num_samples,
+ float* rms_out);
+
+ // Returns the current state of the EnergyEndpointer and the time
+ // corresponding to the most recently computed frame.
+ EpStatus Status(int64_t* status_time_us) const;
+
+ bool estimating_environment() const {
+ return estimating_environment_;
+ }
+
+ // Returns estimated noise level in dB.
+ float GetNoiseLevelDb() const;
+
+ private:
+ class HistoryRing;
+
+ // Resets the endpointer internal state. If reset_threshold is true, the
+ // state will be reset completely, including adaptive thresholds and the
+ // removal of all history information.
+ void Restart(bool reset_threshold);
+
+ // Update internal speech and noise levels.
+ void UpdateLevels(float rms);
+
+ // Returns the number of frames (or frame number) corresponding to
+ // the 'time' (in seconds).
+ int TimeToFrame(float time) const;
+
+ EpStatus status_; // The current state of this instance.
+ float offset_confirm_dur_sec_; // max on time allowed to confirm POST_SPEECH
+ int64_t endpointer_time_us_; // Time of the most recently received audio frame.
+ int64_t fast_update_frames_; // Number of frames for initial level adaptation.
+ int64_t frame_counter_; // Number of frames seen. Used for initial adaptation.
+ float max_window_dur_; // Largest search window size (seconds)
+ float sample_rate_; // Sampling rate.
+
+ // Ring buffers to hold the speech activity history.
+ UniquePtr<HistoryRing> history_;
+
+ // Configuration parameters.
+ EnergyEndpointerParams params_;
+
+ // RMS which must be exceeded to conclude frame is speech.
+ float decision_threshold_;
+
+ // Flag to indicate that audio should be used to estimate environment, prior
+ // to receiving user input.
+ bool estimating_environment_;
+
+ // Estimate of the background noise level. Used externally for UI feedback.
+ float noise_level_;
+
+ // An adaptive threshold used to update decision_threshold_ when appropriate.
+ float rms_adapt_;
+
+ // Start lag corresponds to the highest fundamental frequency.
+ int start_lag_;
+
+ // End lag corresponds to the lowest fundamental frequency.
+ int end_lag_;
+
+ // Time when mode switched from environment estimation to user input. This
+ // is used to time forced rejection of audio feedback contamination.
+ int64_t user_input_start_time_us_;
+
+ // prevent copy constructor and assignment
+ EnergyEndpointer(const EnergyEndpointer&);
+ void operator=(const EnergyEndpointer&);
+};
+
+} // namespace mozilla
+
+#endif // CONTENT_BROWSER_SPEECH_ENDPOINTER_ENERGY_ENDPOINTER_H_
diff --git a/dom/media/webspeech/recognition/energy_endpointer_params.cc b/dom/media/webspeech/recognition/energy_endpointer_params.cc
new file mode 100644
index 0000000000..cac4f1b238
--- /dev/null
+++ b/dom/media/webspeech/recognition/energy_endpointer_params.cc
@@ -0,0 +1,77 @@
+// Copyright (c) 2013 The Chromium Authors. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#include "energy_endpointer_params.h"
+
+namespace mozilla {
+
+EnergyEndpointerParams::EnergyEndpointerParams() {
+ SetDefaults();
+}
+
+void EnergyEndpointerParams::SetDefaults() {
+ frame_period_ = 0.01f;
+ frame_duration_ = 0.01f;
+ endpoint_margin_ = 0.2f;
+ onset_window_ = 0.15f;
+ speech_on_window_ = 0.4f;
+ offset_window_ = 0.15f;
+ onset_detect_dur_ = 0.09f;
+ onset_confirm_dur_ = 0.075f;
+ on_maintain_dur_ = 0.10f;
+ offset_confirm_dur_ = 0.12f;
+ decision_threshold_ = 150.0f;
+ min_decision_threshold_ = 50.0f;
+ fast_update_dur_ = 0.2f;
+ sample_rate_ = 8000.0f;
+ min_fundamental_frequency_ = 57.143f;
+ max_fundamental_frequency_ = 400.0f;
+ contamination_rejection_period_ = 0.25f;
+}
+
+void EnergyEndpointerParams::operator=(const EnergyEndpointerParams& source) {
+ frame_period_ = source.frame_period();
+ frame_duration_ = source.frame_duration();
+ endpoint_margin_ = source.endpoint_margin();
+ onset_window_ = source.onset_window();
+ speech_on_window_ = source.speech_on_window();
+ offset_window_ = source.offset_window();
+ onset_detect_dur_ = source.onset_detect_dur();
+ onset_confirm_dur_ = source.onset_confirm_dur();
+ on_maintain_dur_ = source.on_maintain_dur();
+ offset_confirm_dur_ = source.offset_confirm_dur();
+ decision_threshold_ = source.decision_threshold();
+ min_decision_threshold_ = source.min_decision_threshold();
+ fast_update_dur_ = source.fast_update_dur();
+ sample_rate_ = source.sample_rate();
+ min_fundamental_frequency_ = source.min_fundamental_frequency();
+ max_fundamental_frequency_ = source.max_fundamental_frequency();
+ contamination_rejection_period_ = source.contamination_rejection_period();
+}
+
+} // namespace mozilla
diff --git a/dom/media/webspeech/recognition/energy_endpointer_params.h b/dom/media/webspeech/recognition/energy_endpointer_params.h
new file mode 100644
index 0000000000..6437c6dc0f
--- /dev/null
+++ b/dom/media/webspeech/recognition/energy_endpointer_params.h
@@ -0,0 +1,159 @@
+// Copyright (c) 2013 The Chromium Authors. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+#ifndef CONTENT_BROWSER_SPEECH_ENDPOINTER_ENERGY_ENDPOINTER_PARAMS_H_
+#define CONTENT_BROWSER_SPEECH_ENDPOINTER_ENERGY_ENDPOINTER_PARAMS_H_
+
+namespace mozilla {
+
+// Input parameters for the EnergyEndpointer class.
+class EnergyEndpointerParams {
+ public:
+ EnergyEndpointerParams();
+
+ void SetDefaults();
+
+ void operator=(const EnergyEndpointerParams& source);
+
+ // Accessors and mutators
+ float frame_period() const { return frame_period_; }
+ void set_frame_period(float frame_period) {
+ frame_period_ = frame_period;
+ }
+
+ float frame_duration() const { return frame_duration_; }
+ void set_frame_duration(float frame_duration) {
+ frame_duration_ = frame_duration;
+ }
+
+ float endpoint_margin() const { return endpoint_margin_; }
+ void set_endpoint_margin(float endpoint_margin) {
+ endpoint_margin_ = endpoint_margin;
+ }
+
+ float onset_window() const { return onset_window_; }
+ void set_onset_window(float onset_window) { onset_window_ = onset_window; }
+
+ float speech_on_window() const { return speech_on_window_; }
+ void set_speech_on_window(float speech_on_window) {
+ speech_on_window_ = speech_on_window;
+ }
+
+ float offset_window() const { return offset_window_; }
+ void set_offset_window(float offset_window) {
+ offset_window_ = offset_window;
+ }
+
+ float onset_detect_dur() const { return onset_detect_dur_; }
+ void set_onset_detect_dur(float onset_detect_dur) {
+ onset_detect_dur_ = onset_detect_dur;
+ }
+
+ float onset_confirm_dur() const { return onset_confirm_dur_; }
+ void set_onset_confirm_dur(float onset_confirm_dur) {
+ onset_confirm_dur_ = onset_confirm_dur;
+ }
+
+ float on_maintain_dur() const { return on_maintain_dur_; }
+ void set_on_maintain_dur(float on_maintain_dur) {
+ on_maintain_dur_ = on_maintain_dur;
+ }
+
+ float offset_confirm_dur() const { return offset_confirm_dur_; }
+ void set_offset_confirm_dur(float offset_confirm_dur) {
+ offset_confirm_dur_ = offset_confirm_dur;
+ }
+
+ float decision_threshold() const { return decision_threshold_; }
+ void set_decision_threshold(float decision_threshold) {
+ decision_threshold_ = decision_threshold;
+ }
+
+ float min_decision_threshold() const { return min_decision_threshold_; }
+ void set_min_decision_threshold(float min_decision_threshold) {
+ min_decision_threshold_ = min_decision_threshold;
+ }
+
+ float fast_update_dur() const { return fast_update_dur_; }
+ void set_fast_update_dur(float fast_update_dur) {
+ fast_update_dur_ = fast_update_dur;
+ }
+
+ float sample_rate() const { return sample_rate_; }
+ void set_sample_rate(float sample_rate) { sample_rate_ = sample_rate; }
+
+ float min_fundamental_frequency() const { return min_fundamental_frequency_; }
+ void set_min_fundamental_frequency(float min_fundamental_frequency) {
+ min_fundamental_frequency_ = min_fundamental_frequency;
+ }
+
+ float max_fundamental_frequency() const { return max_fundamental_frequency_; }
+ void set_max_fundamental_frequency(float max_fundamental_frequency) {
+ max_fundamental_frequency_ = max_fundamental_frequency;
+ }
+
+ float contamination_rejection_period() const {
+ return contamination_rejection_period_;
+ }
+ void set_contamination_rejection_period(
+ float contamination_rejection_period) {
+ contamination_rejection_period_ = contamination_rejection_period;
+ }
+
+ private:
+ float frame_period_; // Frame period
+ float frame_duration_; // Window size
+ float onset_window_; // Interval scanned for onset activity
+ float speech_on_window_; // Inverval scanned for ongoing speech
+ float offset_window_; // Interval scanned for offset evidence
+ float offset_confirm_dur_; // Silence duration required to confirm offset
+ float decision_threshold_; // Initial rms detection threshold
+ float min_decision_threshold_; // Minimum rms detection threshold
+ float fast_update_dur_; // Period for initial estimation of levels.
+ float sample_rate_; // Expected sample rate.
+
+ // Time to add on either side of endpoint threshold crossings
+ float endpoint_margin_;
+ // Total dur within onset_window required to enter ONSET state
+ float onset_detect_dur_;
+ // Total on time within onset_window required to enter SPEECH_ON state
+ float onset_confirm_dur_;
+ // Minimum dur in SPEECH_ON state required to maintain ON state
+ float on_maintain_dur_;
+ // Minimum fundamental frequency for autocorrelation.
+ float min_fundamental_frequency_;
+ // Maximum fundamental frequency for autocorrelation.
+ float max_fundamental_frequency_;
+ // Period after start of user input that above threshold values are ignored.
+ // This is to reject audio feedback contamination.
+ float contamination_rejection_period_;
+};
+
+} // namespace mozilla
+
+#endif // CONTENT_BROWSER_SPEECH_ENDPOINTER_ENERGY_ENDPOINTER_PARAMS_H_
diff --git a/dom/media/webspeech/recognition/moz.build b/dom/media/webspeech/recognition/moz.build
new file mode 100644
index 0000000000..5fdf8fdd47
--- /dev/null
+++ b/dom/media/webspeech/recognition/moz.build
@@ -0,0 +1,64 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+MOCHITEST_MANIFESTS += ["test/mochitest.ini"]
+
+XPIDL_MODULE = "dom_webspeechrecognition"
+
+XPIDL_SOURCES = ["nsISpeechRecognitionService.idl"]
+
+EXPORTS.mozilla.dom += [
+ "OnlineSpeechRecognitionService.h",
+ "SpeechGrammar.h",
+ "SpeechGrammarList.h",
+ "SpeechRecognition.h",
+ "SpeechRecognitionAlternative.h",
+ "SpeechRecognitionResult.h",
+ "SpeechRecognitionResultList.h",
+ "SpeechTrackListener.h",
+]
+
+EXPORTS += [
+ "endpointer.h",
+ "energy_endpointer.h",
+ "energy_endpointer_params.h",
+]
+
+if CONFIG["MOZ_WEBSPEECH_TEST_BACKEND"]:
+ EXPORTS.mozilla.dom += [
+ "test/FakeSpeechRecognitionService.h",
+ ]
+
+UNIFIED_SOURCES += [
+ "endpointer.cc",
+ "energy_endpointer.cc",
+ "energy_endpointer_params.cc",
+ "OnlineSpeechRecognitionService.cpp",
+ "SpeechGrammar.cpp",
+ "SpeechGrammarList.cpp",
+ "SpeechRecognition.cpp",
+ "SpeechRecognitionAlternative.cpp",
+ "SpeechRecognitionResult.cpp",
+ "SpeechRecognitionResultList.cpp",
+ "SpeechTrackListener.cpp",
+]
+
+if CONFIG["MOZ_WEBSPEECH_TEST_BACKEND"]:
+ UNIFIED_SOURCES += [
+ "test/FakeSpeechRecognitionService.cpp",
+ ]
+
+USE_LIBS += [
+ "jsoncpp",
+]
+
+LOCAL_INCLUDES += [
+ "/dom/base",
+ "/toolkit/components/jsoncpp/include",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/webspeech/recognition/nsISpeechRecognitionService.idl b/dom/media/webspeech/recognition/nsISpeechRecognitionService.idl
new file mode 100644
index 0000000000..a43d277da0
--- /dev/null
+++ b/dom/media/webspeech/recognition/nsISpeechRecognitionService.idl
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+%{C++
+#include "mozilla/WeakPtr.h"
+
+namespace mozilla {
+class AudioSegment;
+namespace dom {
+class SpeechRecognition;
+class SpeechRecognitionResultList;
+class SpeechGrammarList;
+class SpeechGrammar;
+}
+}
+%}
+
+native SpeechRecognitionWeakPtr(mozilla::WeakPtr<mozilla::dom::SpeechRecognition>);
+[ptr] native AudioSegmentPtr(mozilla::AudioSegment);
+[ptr] native SpeechGrammarPtr(mozilla::dom::SpeechGrammar);
+[ptr] native SpeechGrammarListPtr(mozilla::dom::SpeechGrammarList);
+
+[uuid(6fcb6ee8-a6db-49ba-9f06-355d7ee18ea7)]
+interface nsISpeechGrammarCompilationCallback : nsISupports {
+ void grammarCompilationEnd(in SpeechGrammarPtr grammarObject, in boolean success);
+};
+
+[uuid(8e97f287-f322-44e8-8888-8344fa408ef8)]
+interface nsISpeechRecognitionService : nsISupports {
+ void initialize(in SpeechRecognitionWeakPtr aSpeechRecognition);
+ void processAudioSegment(in AudioSegmentPtr aAudioSegment, in long aSampleRate);
+ void validateAndSetGrammarList(in SpeechGrammarPtr aSpeechGrammar, in nsISpeechGrammarCompilationCallback aCallback);
+ void soundEnd();
+ void abort();
+};
+
+%{C++
+#define NS_SPEECH_RECOGNITION_SERVICE_CONTRACTID_PREFIX "@mozilla.org/webspeech/service;1?name="
+%}
diff --git a/dom/media/webspeech/recognition/test/FakeSpeechRecognitionService.cpp b/dom/media/webspeech/recognition/test/FakeSpeechRecognitionService.cpp
new file mode 100644
index 0000000000..cf14cb3750
--- /dev/null
+++ b/dom/media/webspeech/recognition/test/FakeSpeechRecognitionService.cpp
@@ -0,0 +1,118 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsThreadUtils.h"
+
+#include "FakeSpeechRecognitionService.h"
+
+#include "SpeechRecognition.h"
+#include "SpeechRecognitionAlternative.h"
+#include "SpeechRecognitionResult.h"
+#include "SpeechRecognitionResultList.h"
+#include "nsIObserverService.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_media.h"
+
+namespace mozilla {
+
+using namespace dom;
+
+NS_IMPL_ISUPPORTS(FakeSpeechRecognitionService, nsISpeechRecognitionService,
+ nsIObserver)
+
+FakeSpeechRecognitionService::FakeSpeechRecognitionService() = default;
+
+FakeSpeechRecognitionService::~FakeSpeechRecognitionService() = default;
+
+NS_IMETHODIMP
+FakeSpeechRecognitionService::Initialize(
+ WeakPtr<SpeechRecognition> aSpeechRecognition) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mRecognition = aSpeechRecognition;
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ obs->AddObserver(this, SPEECH_RECOGNITION_TEST_EVENT_REQUEST_TOPIC, false);
+ obs->AddObserver(this, SPEECH_RECOGNITION_TEST_END_TOPIC, false);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+FakeSpeechRecognitionService::ProcessAudioSegment(AudioSegment* aAudioSegment,
+ int32_t aSampleRate) {
+ MOZ_ASSERT(!NS_IsMainThread());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+FakeSpeechRecognitionService::SoundEnd() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+FakeSpeechRecognitionService::ValidateAndSetGrammarList(
+ mozilla::dom::SpeechGrammar*, nsISpeechGrammarCompilationCallback*) {
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+FakeSpeechRecognitionService::Abort() {
+ MOZ_ASSERT(NS_IsMainThread());
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+FakeSpeechRecognitionService::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ MOZ_ASSERT(StaticPrefs::media_webspeech_test_fake_recognition_service(),
+ "Got request to fake recognition service event, but "
+ "media.webspeech.test.fake_recognition_service is not set");
+
+ if (!strcmp(aTopic, SPEECH_RECOGNITION_TEST_END_TOPIC)) {
+ nsCOMPtr<nsIObserverService> obs = services::GetObserverService();
+ obs->RemoveObserver(this, SPEECH_RECOGNITION_TEST_EVENT_REQUEST_TOPIC);
+ obs->RemoveObserver(this, SPEECH_RECOGNITION_TEST_END_TOPIC);
+
+ return NS_OK;
+ }
+
+ const nsDependentString eventName = nsDependentString(aData);
+
+ if (eventName.EqualsLiteral("EVENT_RECOGNITIONSERVICE_ERROR")) {
+ mRecognition->DispatchError(
+ SpeechRecognition::EVENT_RECOGNITIONSERVICE_ERROR,
+ SpeechRecognitionErrorCode::Network, // TODO different codes?
+ "RECOGNITIONSERVICE_ERROR test event");
+
+ } else if (eventName.EqualsLiteral("EVENT_RECOGNITIONSERVICE_FINAL_RESULT")) {
+ RefPtr<SpeechEvent> event = new SpeechEvent(
+ mRecognition, SpeechRecognition::EVENT_RECOGNITIONSERVICE_FINAL_RESULT);
+
+ event->mRecognitionResultList = BuildMockResultList();
+ NS_DispatchToMainThread(event);
+ }
+ return NS_OK;
+}
+
+SpeechRecognitionResultList*
+FakeSpeechRecognitionService::BuildMockResultList() {
+ SpeechRecognitionResultList* resultList =
+ new SpeechRecognitionResultList(mRecognition);
+ SpeechRecognitionResult* result = new SpeechRecognitionResult(mRecognition);
+ if (0 < mRecognition->MaxAlternatives()) {
+ SpeechRecognitionAlternative* alternative =
+ new SpeechRecognitionAlternative(mRecognition);
+
+ alternative->mTranscript = u"Mock final result"_ns;
+ alternative->mConfidence = 0.0f;
+
+ result->mItems.AppendElement(alternative);
+ }
+ resultList->mItems.AppendElement(result);
+
+ return resultList;
+}
+
+} // namespace mozilla
diff --git a/dom/media/webspeech/recognition/test/FakeSpeechRecognitionService.h b/dom/media/webspeech/recognition/test/FakeSpeechRecognitionService.h
new file mode 100644
index 0000000000..69e2786b76
--- /dev/null
+++ b/dom/media/webspeech/recognition/test/FakeSpeechRecognitionService.h
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_FakeSpeechRecognitionService_h
+#define mozilla_dom_FakeSpeechRecognitionService_h
+
+#include "nsCOMPtr.h"
+#include "nsIObserver.h"
+#include "nsISpeechRecognitionService.h"
+
+#define NS_FAKE_SPEECH_RECOGNITION_SERVICE_CID \
+ {0x48c345e7, \
+ 0x9929, \
+ 0x4f9a, \
+ {0xa5, 0x63, 0xf4, 0x78, 0x22, 0x2d, 0xab, 0xcd}};
+
+namespace mozilla {
+
+class FakeSpeechRecognitionService : public nsISpeechRecognitionService,
+ public nsIObserver {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSISPEECHRECOGNITIONSERVICE
+ NS_DECL_NSIOBSERVER
+
+ FakeSpeechRecognitionService();
+
+ private:
+ virtual ~FakeSpeechRecognitionService();
+
+ WeakPtr<dom::SpeechRecognition> mRecognition;
+ dom::SpeechRecognitionResultList* BuildMockResultList();
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webspeech/recognition/test/head.js b/dom/media/webspeech/recognition/test/head.js
new file mode 100644
index 0000000000..c77a7ee926
--- /dev/null
+++ b/dom/media/webspeech/recognition/test/head.js
@@ -0,0 +1,200 @@
+"use strict";
+
+const DEFAULT_AUDIO_SAMPLE_FILE = "hello.ogg";
+const SPEECH_RECOGNITION_TEST_REQUEST_EVENT_TOPIC =
+ "SpeechRecognitionTest:RequestEvent";
+const SPEECH_RECOGNITION_TEST_END_TOPIC = "SpeechRecognitionTest:End";
+
+var errorCodes = {
+ NO_SPEECH: "no-speech",
+ ABORTED: "aborted",
+ AUDIO_CAPTURE: "audio-capture",
+ NETWORK: "network",
+ NOT_ALLOWED: "not-allowed",
+ SERVICE_NOT_ALLOWED: "service-not-allowed",
+ BAD_GRAMMAR: "bad-grammar",
+ LANGUAGE_NOT_SUPPORTED: "language-not-supported",
+};
+
+var Services = SpecialPowers.Services;
+
+function EventManager(sr) {
+ var self = this;
+ var nEventsExpected = 0;
+ self.eventsReceived = [];
+
+ var allEvents = [
+ "audiostart",
+ "soundstart",
+ "speechstart",
+ "speechend",
+ "soundend",
+ "audioend",
+ "result",
+ "nomatch",
+ "error",
+ "start",
+ "end",
+ ];
+
+ var eventDependencies = {
+ speechend: "speechstart",
+ soundend: "soundstart",
+ audioend: "audiostart",
+ };
+
+ var isDone = false;
+
+ // set up grammar
+ var sgl = new SpeechGrammarList();
+ sgl.addFromString("#JSGF V1.0; grammar test; public <simple> = hello ;", 1);
+ sr.grammars = sgl;
+
+ // AUDIO_DATA events are asynchronous,
+ // so we queue events requested while they are being
+ // issued to make them seem synchronous
+ var isSendingAudioData = false;
+ var queuedEventRequests = [];
+
+ // register default handlers
+ for (var i = 0; i < allEvents.length; i++) {
+ (function (eventName) {
+ sr["on" + eventName] = function (evt) {
+ var message = "unexpected event: " + eventName;
+ if (eventName == "error") {
+ message += " -- " + evt.message;
+ }
+
+ ok(false, message);
+ if (self.doneFunc && !isDone) {
+ isDone = true;
+ self.doneFunc();
+ }
+ };
+ })(allEvents[i]);
+ }
+
+ self.expect = function EventManager_expect(eventName, cb) {
+ nEventsExpected++;
+
+ sr["on" + eventName] = function (evt) {
+ self.eventsReceived.push(eventName);
+ ok(true, "received event " + eventName);
+
+ var dep = eventDependencies[eventName];
+ if (dep) {
+ ok(
+ self.eventsReceived.includes(dep),
+ eventName + " must come after " + dep
+ );
+ }
+
+ cb && cb(evt, sr);
+ if (
+ self.doneFunc &&
+ !isDone &&
+ nEventsExpected === self.eventsReceived.length
+ ) {
+ isDone = true;
+ self.doneFunc();
+ }
+ };
+ };
+
+ self.start = function EventManager_start() {
+ isSendingAudioData = true;
+ var audioTag = document.createElement("audio");
+ audioTag.src = self.audioSampleFile;
+
+ var stream = audioTag.mozCaptureStreamUntilEnded();
+ audioTag.addEventListener("ended", function () {
+ info("Sample stream ended, requesting queued events");
+ isSendingAudioData = false;
+ while (queuedEventRequests.length) {
+ self.requestFSMEvent(queuedEventRequests.shift());
+ }
+ });
+
+ audioTag.play();
+ sr.start(stream);
+ };
+
+ self.requestFSMEvent = function EventManager_requestFSMEvent(eventName) {
+ if (isSendingAudioData) {
+ info(
+ "Queuing event " + eventName + " until we're done sending audio data"
+ );
+ queuedEventRequests.push(eventName);
+ return;
+ }
+
+ info("requesting " + eventName);
+ Services.obs.notifyObservers(
+ null,
+ SPEECH_RECOGNITION_TEST_REQUEST_EVENT_TOPIC,
+ eventName
+ );
+ };
+
+ self.requestTestEnd = function EventManager_requestTestEnd() {
+ Services.obs.notifyObservers(null, SPEECH_RECOGNITION_TEST_END_TOPIC);
+ };
+}
+
+function buildResultCallback(transcript) {
+ return function (evt) {
+ is(evt.results[0][0].transcript, transcript, "expect correct transcript");
+ };
+}
+
+function buildErrorCallback(errcode) {
+ return function (err) {
+ is(err.error, errcode, "expect correct error code");
+ };
+}
+
+function performTest(options) {
+ var prefs = options.prefs;
+
+ prefs.unshift(
+ ["media.webspeech.recognition.enable", true],
+ ["media.webspeech.test.enable", true]
+ );
+
+ SpecialPowers.pushPrefEnv({ set: prefs }, function () {
+ var sr;
+ if (!options.webkit) {
+ sr = new SpeechRecognition();
+ } else {
+ sr = new webkitSpeechRecognition();
+ var grammar = new webkitSpeechGrammar();
+ var speechrecognitionlist = new webkitSpeechGrammarList();
+ speechrecognitionlist.addFromString("", 1);
+ sr.grammars = speechrecognitionlist;
+ }
+ var em = new EventManager(sr);
+
+ for (var eventName in options.expectedEvents) {
+ var cb = options.expectedEvents[eventName];
+ em.expect(eventName, cb);
+ }
+
+ em.doneFunc = function () {
+ em.requestTestEnd();
+ if (options.doneFunc) {
+ options.doneFunc();
+ }
+ };
+
+ em.audioSampleFile = DEFAULT_AUDIO_SAMPLE_FILE;
+ if (options.audioSampleFile) {
+ em.audioSampleFile = options.audioSampleFile;
+ }
+
+ em.start();
+
+ for (var i = 0; i < options.eventsToRequest.length; i++) {
+ em.requestFSMEvent(options.eventsToRequest[i]);
+ }
+ });
+}
diff --git a/dom/media/webspeech/recognition/test/hello.ogg b/dom/media/webspeech/recognition/test/hello.ogg
new file mode 100644
index 0000000000..7a80926065
--- /dev/null
+++ b/dom/media/webspeech/recognition/test/hello.ogg
Binary files differ
diff --git a/dom/media/webspeech/recognition/test/hello.ogg^headers^ b/dom/media/webspeech/recognition/test/hello.ogg^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/webspeech/recognition/test/hello.ogg^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/webspeech/recognition/test/http_requesthandler.sjs b/dom/media/webspeech/recognition/test/http_requesthandler.sjs
new file mode 100644
index 0000000000..3400df50ec
--- /dev/null
+++ b/dom/media/webspeech/recognition/test/http_requesthandler.sjs
@@ -0,0 +1,85 @@
+const CC = Components.Constructor;
+
+// Context structure - we need to set this up properly to pass to setObjectState
+const ctx = {
+ QueryInterface(iid) {
+ if (iid.equals(Components.interfaces.nsISupports)) {
+ return this;
+ }
+ throw Components.Exception("", Components.results.NS_ERROR_NO_INTERFACE);
+ },
+};
+
+function setRequest(request) {
+ setObjectState(key, request);
+}
+function getRequest() {
+ let request;
+ getObjectState(v => {
+ request = v;
+ });
+ return request;
+}
+
+function handleRequest(request, response) {
+ response.processAsync();
+ if (request.queryString == "save") {
+ // Get the context structure and finish the old request
+ getObjectState("context", function (obj) {
+ savedCtx = obj.wrappedJSObject;
+ request = savedCtx.request;
+
+ response.setHeader("Content-Type", "application/octet-stream", false);
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setStatusLine(request.httpVersion, 200, "OK");
+
+ const input = request.bodyInputStream;
+ const output = response.bodyOutputStream;
+ let bodyAvail;
+ while ((bodyAvail = input.available()) > 0) {
+ output.writeFrom(input, bodyAvail);
+ }
+ response.finish();
+ });
+ return;
+ } else if (
+ request.queryString == "malformedresult=1" ||
+ request.queryString == "emptyresult=1"
+ ) {
+ jsonOK =
+ request.queryString == "malformedresult=1"
+ ? '{"status":"ok","dat'
+ : '{"status":"ok","data":[]}';
+ response.setHeader("Content-Length", String(jsonOK.length), false);
+ response.setHeader("Content-Type", "application/json", false);
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(jsonOK, jsonOK.length);
+ response.finish();
+ } else if (request.queryString == "hangup=1") {
+ response.finish();
+ } else if (request.queryString == "return400=1") {
+ jsonOK = "{'message':'Bad header:accept-language-stt'}";
+ response.setHeader("Content-Length", String(jsonOK.length), false);
+ response.setHeader("Content-Type", "application/json", false);
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setStatusLine(request.httpVersion, 400, "Bad Request");
+ response.write(jsonOK, jsonOK.length);
+ response.finish();
+ } else {
+ ctx.wrappedJSObject = ctx;
+ ctx.request = request;
+ setObjectState("context", ctx);
+ jsonOK = '{"status":"ok","data":[{"confidence":0.9085610,"text":"hello"}]}';
+ response.setHeader("Content-Length", String(jsonOK.length), false);
+ response.setHeader("Content-Type", "application/json", false);
+ response.setHeader("Access-Control-Allow-Origin", "*", false);
+ response.setHeader("Cache-Control", "no-cache", false);
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(jsonOK, jsonOK.length);
+ response.finish();
+ }
+}
diff --git a/dom/media/webspeech/recognition/test/mochitest.ini b/dom/media/webspeech/recognition/test/mochitest.ini
new file mode 100644
index 0000000000..6af13b906c
--- /dev/null
+++ b/dom/media/webspeech/recognition/test/mochitest.ini
@@ -0,0 +1,35 @@
+[DEFAULT]
+tags=mtg
+subsuite = media
+support-files =
+ head.js
+ hello.ogg
+ hello.ogg^headers^
+ http_requesthandler.sjs
+ sinoid+hello.ogg
+ sinoid+hello.ogg^headers^
+ silence.ogg
+ silence.ogg^headers^
+[test_abort.html]
+skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1538363
+[test_audio_capture_error.html]
+skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1538360
+[test_call_start_from_end_handler.html]
+tags=capturestream
+skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1538363
+[test_nested_eventloop.html]
+skip-if = toolkit == 'android'
+[test_online_400_response.html]
+[test_online_hangup.html]
+[test_online_http.html]
+[test_online_http_webkit.html]
+[test_online_malformed_result_handling.html]
+[test_online_empty_result_handling.html]
+[test_preference_enable.html]
+[test_recognition_service_error.html]
+skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1538360
+[test_success_without_recognition_service.html]
+skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1538360
+[test_timeout.html]
+skip-if =
+ os == "linux" # Bug 1307991 - low frequency on try pushes
diff --git a/dom/media/webspeech/recognition/test/silence.ogg b/dom/media/webspeech/recognition/test/silence.ogg
new file mode 100644
index 0000000000..e6da3a5022
--- /dev/null
+++ b/dom/media/webspeech/recognition/test/silence.ogg
Binary files differ
diff --git a/dom/media/webspeech/recognition/test/silence.ogg^headers^ b/dom/media/webspeech/recognition/test/silence.ogg^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/webspeech/recognition/test/silence.ogg^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/webspeech/recognition/test/sinoid+hello.ogg b/dom/media/webspeech/recognition/test/sinoid+hello.ogg
new file mode 100644
index 0000000000..7092e82f30
--- /dev/null
+++ b/dom/media/webspeech/recognition/test/sinoid+hello.ogg
Binary files differ
diff --git a/dom/media/webspeech/recognition/test/sinoid+hello.ogg^headers^ b/dom/media/webspeech/recognition/test/sinoid+hello.ogg^headers^
new file mode 100644
index 0000000000..4030ea1d3d
--- /dev/null
+++ b/dom/media/webspeech/recognition/test/sinoid+hello.ogg^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/webspeech/recognition/test/test_abort.html b/dom/media/webspeech/recognition/test/test_abort.html
new file mode 100644
index 0000000000..0f22770cc7
--- /dev/null
+++ b/dom/media/webspeech/recognition/test/test_abort.html
@@ -0,0 +1,73 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=650295
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 650295 -- Call abort from inside handlers</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="head.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=650295">Mozilla Bug 650295</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="text/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ // Abort inside event handlers, should't get a
+ // result after that
+
+ var nextEventIdx = 0;
+ var eventsToAbortOn = [
+ "start",
+ "audiostart",
+ "speechstart",
+ "speechend",
+ "audioend"
+ ];
+
+ function doNextTest() {
+ var nextEvent = eventsToAbortOn[nextEventIdx];
+ var expectedEvents = {
+ "start": null,
+ "audiostart": null,
+ "audioend": null,
+ "end": null
+ };
+
+ if (nextEventIdx >= eventsToAbortOn.indexOf("speechstart")) {
+ expectedEvents.speechstart = null;
+ }
+
+ if (nextEventIdx >= eventsToAbortOn.indexOf("speechend")) {
+ expectedEvents.speechend = null;
+ }
+
+ info("Aborting on " + nextEvent);
+ expectedEvents[nextEvent] = function(evt, sr) {
+ sr.abort();
+ };
+
+ nextEventIdx++;
+
+ performTest({
+ eventsToRequest: [],
+ expectedEvents,
+ doneFunc: (nextEventIdx < eventsToAbortOn.length) ? doNextTest : SimpleTest.finish,
+ prefs: [["media.webspeech.test.fake_fsm_events", true],
+ ["media.webspeech.test.fake_recognition_service", true],
+ ["media.webspeech.recognition.timeout", 100000]]
+ });
+ }
+
+ doNextTest();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webspeech/recognition/test/test_audio_capture_error.html b/dom/media/webspeech/recognition/test/test_audio_capture_error.html
new file mode 100644
index 0000000000..0c054dbf0b
--- /dev/null
+++ b/dom/media/webspeech/recognition/test/test_audio_capture_error.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=650295
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 650295 -- Behavior on audio error</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="head.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=650295">Mozilla Bug 650295</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="text/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ performTest({
+ eventsToRequest: ['EVENT_AUDIO_ERROR'],
+ expectedEvents: {
+ 'start': null,
+ 'audiostart': null,
+ 'speechstart': null,
+ 'speechend': null,
+ 'audioend': null,
+ 'error': buildErrorCallback(errorCodes.AUDIO_CAPTURE),
+ 'end': null
+ },
+ doneFunc: SimpleTest.finish,
+ prefs: [["media.webspeech.test.fake_fsm_events", true],
+ ["media.webspeech.test.fake_recognition_service", true],
+ ["media.webspeech.recognition.timeout", 100000]]
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webspeech/recognition/test/test_call_start_from_end_handler.html b/dom/media/webspeech/recognition/test/test_call_start_from_end_handler.html
new file mode 100644
index 0000000000..895648ad9e
--- /dev/null
+++ b/dom/media/webspeech/recognition/test/test_call_start_from_end_handler.html
@@ -0,0 +1,102 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=650295
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 650295 -- Restart recognition from end handler</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="head.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=650295">Mozilla Bug 650295</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="text/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ function createAudioStream() {
+ var audioTag = document.createElement("audio");
+ audioTag.src = DEFAULT_AUDIO_SAMPLE_FILE;
+
+ var stream = audioTag.mozCaptureStreamUntilEnded();
+ audioTag.play();
+
+ return stream;
+ }
+
+ var done = false;
+ function endHandler(evt, sr) {
+ if (done) {
+ SimpleTest.finish();
+ return;
+ }
+
+ try {
+ var stream = createAudioStream();
+ sr.start(stream); // shouldn't fail
+ } catch (err) {
+ ok(false, "Failed to start() from end() callback");
+ }
+
+ // calling start() may cause some callbacks to fire, but we're
+ // no longer interested in them, except for onend, which is where
+ // we'll conclude the test.
+ sr.onstart = null;
+ sr.onaudiostart = null;
+ sr.onspeechstart = null;
+ sr.onspeechend = null;
+ sr.onaudioend = null;
+ sr.onresult = null;
+
+ // FIXME(ggp) the state transition caused by start() is async,
+ // but abort() is sync (see bug 1055093). until we normalize
+ // state transitions, we need to setTimeout here to make sure
+ // abort() finds the speech recognition object in the correct
+ // state (namely, STATE_STARTING).
+ setTimeout(function() {
+ sr.abort();
+ done = true;
+ });
+
+ info("Successfully start() from end() callback");
+ }
+
+ function expectExceptionHandler(evt, sr) {
+ try {
+ sr.start(createAudioStream());
+ } catch (err) {
+ is(err.name, "InvalidStateError");
+ return;
+ }
+
+ ok(false, "Calling start() didn't raise InvalidStateError");
+ }
+
+ performTest({
+ eventsToRequest: [
+ 'EVENT_RECOGNITIONSERVICE_FINAL_RESULT'
+ ],
+ expectedEvents: {
+ 'start': expectExceptionHandler,
+ 'audiostart': expectExceptionHandler,
+ 'speechstart': expectExceptionHandler,
+ 'speechend': expectExceptionHandler,
+ 'audioend': expectExceptionHandler,
+ 'result': buildResultCallback("Mock final result"),
+ 'end': endHandler,
+ },
+ prefs: [["media.webspeech.test.fake_fsm_events", true],
+ ["media.webspeech.test.fake_recognition_service", true],
+ ["media.webspeech.recognition.timeout", 100000]]
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webspeech/recognition/test/test_nested_eventloop.html b/dom/media/webspeech/recognition/test/test_nested_eventloop.html
new file mode 100644
index 0000000000..4924766b44
--- /dev/null
+++ b/dom/media/webspeech/recognition/test/test_nested_eventloop.html
@@ -0,0 +1,82 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=650295
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 650295 -- Spin the event loop from inside a callback</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="head.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=650295">Mozilla Bug 650295</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="text/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ /*
+ * SpecialPowers.spinEventLoop can be used to spin the event loop, causing
+ * queued SpeechEvents (such as those created by calls to start(), stop()
+ * or abort()) to be processed immediately.
+ * When this is done from inside DOM event handlers, it is possible to
+ * cause reentrancy in our C++ code, which we should be able to withstand.
+ */
+ function abortAndSpinEventLoop(evt, sr) {
+ sr.abort();
+ SpecialPowers.spinEventLoop(window);
+ }
+ function doneFunc() {
+ // Trigger gc now and wait some time to make sure this test gets the blame
+ // for any assertions caused by spinning the event loop.
+ //
+ // NB - The assertions should be gone, but this looks too scary to touch
+ // during batch cleanup.
+ var count = 0, GC_COUNT = 4;
+
+ function triggerGCOrFinish() {
+ SpecialPowers.gc();
+ count++;
+
+ if (count == GC_COUNT) {
+ SimpleTest.finish();
+ }
+ }
+
+ for (var i = 0; i < GC_COUNT; i++) {
+ setTimeout(triggerGCOrFinish, 0);
+ }
+ }
+
+ /*
+ * We start by performing a normal start, then abort from the audiostart
+ * callback and force the EVENT_ABORT to be processed while still inside
+ * the event handler. This causes the recording to stop, which raises
+ * the audioend and (later on) end events.
+ * Then, we abort (once again spinning the event loop) from the audioend
+ * handler, attempting to cause a re-entry into the abort code. This second
+ * call should be ignored, and we get the end callback and finish.
+ */
+
+ performTest({
+ eventsToRequest: [],
+ expectedEvents: {
+ "audiostart": abortAndSpinEventLoop,
+ "audioend": abortAndSpinEventLoop,
+ "end": null
+ },
+ doneFunc,
+ prefs: [["media.webspeech.test.fake_fsm_events", true],
+ ["media.webspeech.test.fake_recognition_service", true],
+ ["media.webspeech.recognition.timeout", 100000]]
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webspeech/recognition/test/test_online_400_response.html b/dom/media/webspeech/recognition/test/test_online_400_response.html
new file mode 100644
index 0000000000..1a7d0ed452
--- /dev/null
+++ b/dom/media/webspeech/recognition/test/test_online_400_response.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1248897
+The intent of this file is to test the speech recognition service behavior
+whenever the server returns a 400 error
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1248897 -- Online speech service</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="head.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1248897">Mozilla Bug 1248897</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="text/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ performTest({
+ eventsToRequest: [],
+ expectedEvents: {
+ "start": null,
+ "audiostart": null,
+ "audioend": null,
+ "end": null,
+ 'error': buildErrorCallback(errorCodes.NETWORK),
+ "speechstart": null,
+ "speechend": null
+ },
+ doneFunc: SimpleTest.finish,
+ prefs: [["media.webspeech.recognition.enable", true],
+ ["media.webspeech.recognition.force_enable", true],
+ ["media.webspeech.service.endpoint",
+ "http://mochi.test:8888/tests/dom/media/webspeech/recognition/test/http_requesthandler.sjs?return400=1"],
+ ["media.webspeech.recognition.timeout", 100000]]
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webspeech/recognition/test/test_online_empty_result_handling.html b/dom/media/webspeech/recognition/test/test_online_empty_result_handling.html
new file mode 100644
index 0000000000..46f1e7e0cb
--- /dev/null
+++ b/dom/media/webspeech/recognition/test/test_online_empty_result_handling.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1248897
+The intent of this file is to test the speech recognition service behavior
+whenever the server returns a valid json object, but without any transcription
+results on it, for example: `{"status":"ok","data":[]}`
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1248897 -- Online speech service</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="head.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1248897">Mozilla Bug 1248897</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="text/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ performTest({
+ eventsToRequest: [],
+ expectedEvents: {
+ "start": null,
+ "audiostart": null,
+ "audioend": null,
+ "end": null,
+ 'error': buildErrorCallback(errorCodes.NETWORK),
+ "speechstart": null,
+ "speechend": null
+ },
+ doneFunc: SimpleTest.finish,
+ prefs: [["media.webspeech.recognition.enable", true],
+ ["media.webspeech.recognition.force_enable", true],
+ ["media.webspeech.service.endpoint",
+ "http://mochi.test:8888/tests/dom/media/webspeech/recognition/test/http_requesthandler.sjs?emptyresult=1"],
+ ["media.webspeech.recognition.timeout", 100000]]
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webspeech/recognition/test/test_online_hangup.html b/dom/media/webspeech/recognition/test/test_online_hangup.html
new file mode 100644
index 0000000000..4a46f80f8f
--- /dev/null
+++ b/dom/media/webspeech/recognition/test/test_online_hangup.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1248897
+The intent of this file is to test the speech recognition service behavior
+whenever the server hangups the connection without sending any response
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1248897 -- Online speech service</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="head.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1248897">Mozilla Bug 1248897</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="text/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ performTest({
+ eventsToRequest: [],
+ expectedEvents: {
+ "start": null,
+ "audiostart": null,
+ "audioend": null,
+ "end": null,
+ 'error': buildErrorCallback(errorCodes.NETWORK),
+ "speechstart": null,
+ "speechend": null
+ },
+ doneFunc: SimpleTest.finish,
+ prefs: [["media.webspeech.recognition.enable", true],
+ ["media.webspeech.recognition.force_enable", true],
+ ["media.webspeech.service.endpoint",
+ "http://mochi.test:8888/tests/dom/media/webspeech/recognition/test/http_requesthandler.sjs?hangup=1"],
+ ["media.webspeech.recognition.timeout", 100000]]
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webspeech/recognition/test/test_online_http.html b/dom/media/webspeech/recognition/test/test_online_http.html
new file mode 100644
index 0000000000..43be7a656a
--- /dev/null
+++ b/dom/media/webspeech/recognition/test/test_online_http.html
@@ -0,0 +1,89 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1248897
+The intent of this file is to test a successfull speech recognition request and
+that audio is being properly encoded
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1248897 -- Online speech service</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="head.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1248897">Mozilla Bug 1248897</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="text/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ async function validateRawAudio(buffer) {
+ const ac = new AudioContext();
+ const decodedData = await ac.decodeAudioData(buffer);
+ const source = ac.createBufferSource();
+ source.buffer = decodedData;
+ source.loop = true;
+ const analyser = ac.createAnalyser();
+ analyser.smoothingTimeConstant = 0.2;
+ analyser.fftSize = 1024;
+ source.connect(analyser);
+ const binIndexForFrequency = frequency =>
+ 1 + Math.round(frequency * analyser.fftSize / ac.sampleRate);
+ source.start();
+ const data = new Uint8Array(analyser.frequencyBinCount);
+ const start = performance.now();
+ while (true) {
+ if (performance.now() - start > 10000) {
+ return false;
+ break;
+ }
+ analyser.getByteFrequencyData(data);
+ if (data[binIndexForFrequency(200)] < 50 &&
+ data[binIndexForFrequency(440)] > 180 &&
+ data[binIndexForFrequency(1000)] < 50) {
+ return true;
+ break;
+ }
+ await new Promise(r => requestAnimationFrame(r));
+ }
+ }
+
+ async function verifyEncodedAudio(requestUrl) {
+ try {
+ const response = await fetch(requestUrl);
+ const buffer = await response.arrayBuffer();
+ ok(await validateRawAudio(buffer), "Audio encoding is valid");
+ } catch(e) {
+ ok(false, e);
+ } finally {
+ SimpleTest.finish();
+ }
+ }
+
+ performTest({
+ eventsToRequest: {},
+ expectedEvents: {
+ "start": null,
+ "audiostart": null,
+ "audioend": null,
+ "end": null,
+ "result": () => verifyEncodedAudio("http_requesthandler.sjs?save"),
+ "speechstart": null,
+ "speechend": null
+ },
+ audioSampleFile: "sinoid+hello.ogg",
+ prefs: [["media.webspeech.recognition.enable", true],
+ ["media.webspeech.recognition.force_enable", true],
+ ["media.webspeech.service.endpoint",
+ "http://mochi.test:8888/tests/dom/media/webspeech/recognition/test/http_requesthandler.sjs"],
+ ["media.webspeech.recognition.timeout", 100000]]
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webspeech/recognition/test/test_online_http_webkit.html b/dom/media/webspeech/recognition/test/test_online_http_webkit.html
new file mode 100644
index 0000000000..7f6c7e6d7d
--- /dev/null
+++ b/dom/media/webspeech/recognition/test/test_online_http_webkit.html
@@ -0,0 +1,90 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1248897
+The intent of this file is to test a successfull speech recognition request and
+that audio is being properly encoded
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1248897 -- Online speech service</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="head.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1248897">Mozilla Bug 1248897</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="text/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ async function validateRawAudio(buffer) {
+ const ac = new AudioContext();
+ const decodedData = await ac.decodeAudioData(buffer);
+ const source = ac.createBufferSource();
+ source.buffer = decodedData;
+ source.loop = true;
+ const analyser = ac.createAnalyser();
+ analyser.smoothingTimeConstant = 0.2;
+ analyser.fftSize = 1024;
+ source.connect(analyser);
+ const binIndexForFrequency = frequency =>
+ 1 + Math.round(frequency * analyser.fftSize / ac.sampleRate);
+ source.start();
+ const data = new Uint8Array(analyser.frequencyBinCount);
+ const start = performance.now();
+ while (true) {
+ if (performance.now() - start > 10000) {
+ return false;
+ break;
+ }
+ analyser.getByteFrequencyData(data);
+ if (data[binIndexForFrequency(200)] < 50 &&
+ data[binIndexForFrequency(440)] > 180 &&
+ data[binIndexForFrequency(1000)] < 50) {
+ return true;
+ break;
+ }
+ await new Promise(r => requestAnimationFrame(r));
+ }
+ }
+
+ async function verifyEncodedAudio(requestUrl) {
+ try {
+ const response = await fetch(requestUrl);
+ const buffer = await response.arrayBuffer();
+ ok(await validateRawAudio(buffer), "Audio encoding is valid");
+ } catch(e) {
+ ok(false, e);
+ } finally {
+ SimpleTest.finish();
+ }
+ }
+
+ performTest({
+ eventsToRequest: {},
+ expectedEvents: {
+ "start": null,
+ "audiostart": null,
+ "audioend": null,
+ "end": null,
+ "result": () => verifyEncodedAudio("http_requesthandler.sjs?save"),
+ "speechstart": null,
+ "speechend": null
+ },
+ audioSampleFile: "sinoid+hello.ogg",
+ prefs: [["media.webspeech.recognition.enable", true],
+ ["media.webspeech.recognition.force_enable", true],
+ ["media.webspeech.service.endpoint",
+ "http://mochi.test:8888/tests/dom/media/webspeech/recognition/test/http_requesthandler.sjs"],
+ ["media.webspeech.recognition.timeout", 100000]],
+ webkit: true
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webspeech/recognition/test/test_online_malformed_result_handling.html b/dom/media/webspeech/recognition/test/test_online_malformed_result_handling.html
new file mode 100644
index 0000000000..b071a46ea3
--- /dev/null
+++ b/dom/media/webspeech/recognition/test/test_online_malformed_result_handling.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1248897
+The intent of this file is to test the speech recognition service behavior
+whenever the server returns an invalid/corrupted json object, for example:
+`{"status":"ok","dat`
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1248897 -- Online speech service</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="head.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1248897">Mozilla Bug 1248897</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="text/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ performTest({
+ eventsToRequest: [],
+ expectedEvents: {
+ "start": null,
+ "audiostart": null,
+ "audioend": null,
+ "end": null,
+ 'error': buildErrorCallback(errorCodes.NETWORK),
+ "speechstart": null,
+ "speechend": null
+ },
+ doneFunc: SimpleTest.finish,
+ prefs: [["media.webspeech.recognition.enable", true],
+ ["media.webspeech.recognition.force_enable", true],
+ ["media.webspeech.service.endpoint",
+ "http://mochi.test:8888/tests/dom/media/webspeech/recognition/test/http_requesthandler.sjs?malformedresult=1"],
+ ["media.webspeech.recognition.timeout", 100000]]
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webspeech/recognition/test/test_preference_enable.html b/dom/media/webspeech/recognition/test/test_preference_enable.html
new file mode 100644
index 0000000000..2b56f82e2c
--- /dev/null
+++ b/dom/media/webspeech/recognition/test/test_preference_enable.html
@@ -0,0 +1,43 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=650295
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 650295 -- No objects should be visible with preference disabled</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=650295">Mozilla Bug 650295</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="text/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ SpecialPowers.pushPrefEnv({
+ set: [["media.webspeech.recognition.enable", false]]
+ }, function() {
+ var objects = [
+ "SpeechRecognition",
+ "SpeechGrammar",
+ "SpeechRecognitionResult",
+ "SpeechRecognitionResultList",
+ "SpeechRecognitionAlternative"
+ ];
+
+ for (var i = 0; i < objects.length; i++) {
+ is(window[objects[i]], undefined,
+ objects[i] + " should be undefined with pref off");
+ }
+
+ SimpleTest.finish();
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webspeech/recognition/test/test_recognition_service_error.html b/dom/media/webspeech/recognition/test/test_recognition_service_error.html
new file mode 100644
index 0000000000..e8e59e2afc
--- /dev/null
+++ b/dom/media/webspeech/recognition/test/test_recognition_service_error.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=650295
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 650295 -- Behavior on recognition service error</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="head.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=650295">Mozilla Bug 650295</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="text/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ performTest({
+ eventsToRequest: [
+ 'EVENT_RECOGNITIONSERVICE_ERROR'
+ ],
+ expectedEvents: {
+ 'start': null,
+ 'audiostart': null,
+ 'speechstart': null,
+ 'speechend': null,
+ 'audioend': null,
+ 'error': buildErrorCallback(errorCodes.NETWORK),
+ 'end': null
+ },
+ doneFunc: SimpleTest.finish,
+ prefs: [["media.webspeech.test.fake_fsm_events", true],
+ ["media.webspeech.test.fake_recognition_service", true],
+ ["media.webspeech.recognition.timeout", 100000]]
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webspeech/recognition/test/test_success_without_recognition_service.html b/dom/media/webspeech/recognition/test/test_success_without_recognition_service.html
new file mode 100644
index 0000000000..38748ed5cb
--- /dev/null
+++ b/dom/media/webspeech/recognition/test/test_success_without_recognition_service.html
@@ -0,0 +1,45 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=650295
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 650295 -- Success with fake recognition service</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="head.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=650295">Mozilla Bug 650295</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="text/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ performTest({
+ eventsToRequest: [
+ 'EVENT_RECOGNITIONSERVICE_FINAL_RESULT'
+ ],
+ expectedEvents: {
+ 'start': null,
+ 'audiostart': null,
+ 'speechstart': null,
+ 'speechend': null,
+ 'audioend': null,
+ 'result': buildResultCallback("Mock final result"),
+ 'end': null
+ },
+ doneFunc:SimpleTest.finish,
+ prefs: [["media.webspeech.test.fake_fsm_events", true],
+ ["media.webspeech.test.fake_recognition_service", true],
+ ["media.webspeech.recognition.timeout", 100000]]
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webspeech/recognition/test/test_timeout.html b/dom/media/webspeech/recognition/test/test_timeout.html
new file mode 100644
index 0000000000..8334c9e779
--- /dev/null
+++ b/dom/media/webspeech/recognition/test/test_timeout.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=650295
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 650295 -- Timeout for user speech</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript" src="head.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=650295">Mozilla Bug 650295</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="text/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ performTest({
+ eventsToRequest: [],
+ expectedEvents: {
+ "start": null,
+ "audiostart": null,
+ "audioend": null,
+ "error": buildErrorCallback(errorCodes.NO_SPEECH),
+ "end": null
+ },
+ doneFunc: SimpleTest.finish,
+ audioSampleFile: "silence.ogg",
+ prefs: [["media.webspeech.test.fake_fsm_events", true],
+ ["media.webspeech.test.fake_recognition_service", true],
+ ["media.webspeech.recognition.timeout", 1000]]
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webspeech/synth/SpeechSynthesis.cpp b/dom/media/webspeech/synth/SpeechSynthesis.cpp
new file mode 100644
index 0000000000..20e3ef754b
--- /dev/null
+++ b/dom/media/webspeech/synth/SpeechSynthesis.cpp
@@ -0,0 +1,315 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupportsPrimitives.h"
+#include "nsSpeechTask.h"
+#include "mozilla/Logging.h"
+
+#include "mozilla/dom/Element.h"
+
+#include "mozilla/dom/SpeechSynthesisBinding.h"
+#include "mozilla/dom/WindowGlobalChild.h"
+#include "SpeechSynthesis.h"
+#include "nsContentUtils.h"
+#include "nsSynthVoiceRegistry.h"
+#include "mozilla/dom/Document.h"
+#include "nsIDocShell.h"
+
+#undef LOG
+mozilla::LogModule* GetSpeechSynthLog() {
+ static mozilla::LazyLogModule sLog("SpeechSynthesis");
+
+ return sLog;
+}
+#define LOG(type, msg) MOZ_LOG(GetSpeechSynthLog(), type, msg)
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(SpeechSynthesis)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(SpeechSynthesis,
+ DOMEventTargetHelper)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mCurrentTask)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mSpeechQueue)
+ tmp->mVoiceCache.Clear();
+ NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(SpeechSynthesis,
+ DOMEventTargetHelper)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCurrentTask)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSpeechQueue)
+ for (SpeechSynthesisVoice* voice : tmp->mVoiceCache.Values()) {
+ cb.NoteXPCOMChild(voice);
+ }
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SpeechSynthesis)
+ NS_INTERFACE_MAP_ENTRY(nsIObserver)
+ NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+NS_IMPL_ADDREF_INHERITED(SpeechSynthesis, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(SpeechSynthesis, DOMEventTargetHelper)
+
+SpeechSynthesis::SpeechSynthesis(nsPIDOMWindowInner* aParent)
+ : DOMEventTargetHelper(aParent),
+ mHoldQueue(false),
+ mInnerID(aParent->WindowID()) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->AddObserver(this, "inner-window-destroyed", true);
+ obs->AddObserver(this, "synth-voices-changed", true);
+ }
+}
+
+SpeechSynthesis::~SpeechSynthesis() = default;
+
+JSObject* SpeechSynthesis::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return SpeechSynthesis_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+bool SpeechSynthesis::Pending() const {
+ // If we don't have any task, nothing is pending. If we have only one task,
+ // check if that task is currently pending. If we have more than one task,
+ // then the tasks after the first one are definitely pending.
+ return mSpeechQueue.Length() > 1 ||
+ (mSpeechQueue.Length() == 1 &&
+ (!mCurrentTask || mCurrentTask->IsPending()));
+}
+
+bool SpeechSynthesis::Speaking() const {
+ // Check global speaking state if there is no active speaking task.
+ return (!mSpeechQueue.IsEmpty() && HasSpeakingTask()) ||
+ nsSynthVoiceRegistry::GetInstance()->IsSpeaking();
+}
+
+bool SpeechSynthesis::Paused() const {
+ return mHoldQueue || (mCurrentTask && mCurrentTask->IsPrePaused()) ||
+ (!mSpeechQueue.IsEmpty() && mSpeechQueue.ElementAt(0)->IsPaused());
+}
+
+bool SpeechSynthesis::HasEmptyQueue() const {
+ return mSpeechQueue.Length() == 0;
+}
+
+bool SpeechSynthesis::HasVoices() const {
+ uint32_t voiceCount = mVoiceCache.Count();
+ if (voiceCount == 0) {
+ nsresult rv =
+ nsSynthVoiceRegistry::GetInstance()->GetVoiceCount(&voiceCount);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return false;
+ }
+ }
+
+ return voiceCount != 0;
+}
+
+void SpeechSynthesis::Speak(SpeechSynthesisUtterance& aUtterance) {
+ if (!mInnerID) {
+ return;
+ }
+
+ mSpeechQueue.AppendElement(&aUtterance);
+
+ if (mSpeechQueue.Length() == 1) {
+ RefPtr<WindowGlobalChild> wgc =
+ WindowGlobalChild::GetByInnerWindowId(mInnerID);
+ if (wgc) {
+ wgc->BlockBFCacheFor(BFCacheStatus::HAS_ACTIVE_SPEECH_SYNTHESIS);
+ }
+
+ // If we only have one item in the queue, we aren't pre-paused, and
+ // we have voices available, speak it.
+ if (!mCurrentTask && !mHoldQueue && HasVoices()) {
+ AdvanceQueue();
+ }
+ }
+}
+
+void SpeechSynthesis::AdvanceQueue() {
+ LOG(LogLevel::Debug,
+ ("SpeechSynthesis::AdvanceQueue length=%zu", mSpeechQueue.Length()));
+
+ if (mSpeechQueue.IsEmpty()) {
+ return;
+ }
+
+ RefPtr<SpeechSynthesisUtterance> utterance = mSpeechQueue.ElementAt(0);
+
+ nsAutoString docLang;
+ nsCOMPtr<nsPIDOMWindowInner> window = GetOwner();
+ if (Document* doc = window ? window->GetExtantDoc() : nullptr) {
+ if (Element* elm = doc->GetHtmlElement()) {
+ elm->GetLang(docLang);
+ }
+ }
+
+ mCurrentTask =
+ nsSynthVoiceRegistry::GetInstance()->SpeakUtterance(*utterance, docLang);
+
+ if (mCurrentTask) {
+ mCurrentTask->SetSpeechSynthesis(this);
+ }
+}
+
+void SpeechSynthesis::Cancel() {
+ if (!mSpeechQueue.IsEmpty() && HasSpeakingTask()) {
+ // Remove all queued utterances except for current one, we will remove it
+ // in OnEnd
+ mSpeechQueue.RemoveLastElements(mSpeechQueue.Length() - 1);
+ } else {
+ mSpeechQueue.Clear();
+ }
+
+ if (mCurrentTask) {
+ mCurrentTask->Cancel();
+ }
+}
+
+void SpeechSynthesis::Pause() {
+ if (Paused()) {
+ return;
+ }
+
+ if (!mSpeechQueue.IsEmpty() && HasSpeakingTask()) {
+ mCurrentTask->Pause();
+ } else {
+ mHoldQueue = true;
+ }
+}
+
+void SpeechSynthesis::Resume() {
+ if (!Paused()) {
+ return;
+ }
+
+ mHoldQueue = false;
+
+ if (mCurrentTask) {
+ mCurrentTask->Resume();
+ } else {
+ AdvanceQueue();
+ }
+}
+
+void SpeechSynthesis::OnEnd(const nsSpeechTask* aTask) {
+ MOZ_ASSERT(mCurrentTask == aTask);
+
+ if (!mSpeechQueue.IsEmpty()) {
+ mSpeechQueue.RemoveElementAt(0);
+ if (mSpeechQueue.IsEmpty()) {
+ RefPtr<WindowGlobalChild> wgc =
+ WindowGlobalChild::GetByInnerWindowId(mInnerID);
+ if (wgc) {
+ wgc->UnblockBFCacheFor(BFCacheStatus::HAS_ACTIVE_SPEECH_SYNTHESIS);
+ }
+ }
+ }
+
+ mCurrentTask = nullptr;
+ AdvanceQueue();
+}
+
+void SpeechSynthesis::GetVoices(
+ nsTArray<RefPtr<SpeechSynthesisVoice> >& aResult) {
+ aResult.Clear();
+ uint32_t voiceCount = 0;
+ nsCOMPtr<nsPIDOMWindowInner> window = GetOwner();
+ nsCOMPtr<nsIDocShell> docShell = window ? window->GetDocShell() : nullptr;
+
+ if (nsContentUtils::ShouldResistFingerprinting(docShell,
+ RFPTarget::SpeechSynthesis)) {
+ return;
+ }
+
+ nsresult rv = nsSynthVoiceRegistry::GetInstance()->GetVoiceCount(&voiceCount);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ nsISupports* voiceParent = NS_ISUPPORTS_CAST(nsIObserver*, this);
+
+ for (uint32_t i = 0; i < voiceCount; i++) {
+ nsAutoString uri;
+ rv = nsSynthVoiceRegistry::GetInstance()->GetVoice(i, uri);
+
+ if (NS_FAILED(rv)) {
+ NS_WARNING("Failed to retrieve voice from registry");
+ continue;
+ }
+
+ SpeechSynthesisVoice* voice = mVoiceCache.GetWeak(uri);
+
+ if (!voice) {
+ voice = new SpeechSynthesisVoice(voiceParent, uri);
+ }
+
+ aResult.AppendElement(voice);
+ }
+
+ mVoiceCache.Clear();
+
+ for (uint32_t i = 0; i < aResult.Length(); i++) {
+ SpeechSynthesisVoice* voice = aResult[i];
+ mVoiceCache.InsertOrUpdate(voice->mUri, RefPtr{voice});
+ }
+}
+
+// For testing purposes, allows us to cancel the current task that is
+// misbehaving, and flush the queue.
+void SpeechSynthesis::ForceEnd() {
+ if (mCurrentTask) {
+ mCurrentTask->ForceEnd();
+ }
+}
+
+NS_IMETHODIMP
+SpeechSynthesis::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (strcmp(aTopic, "inner-window-destroyed") == 0) {
+ nsCOMPtr<nsISupportsPRUint64> wrapper = do_QueryInterface(aSubject);
+ NS_ENSURE_TRUE(wrapper, NS_ERROR_FAILURE);
+
+ uint64_t innerID;
+ nsresult rv = wrapper->GetData(&innerID);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ if (innerID == mInnerID) {
+ mInnerID = 0;
+ Cancel();
+
+ nsCOMPtr<nsIObserverService> obs =
+ mozilla::services::GetObserverService();
+ if (obs) {
+ obs->RemoveObserver(this, "inner-window-destroyed");
+ }
+ }
+ } else if (strcmp(aTopic, "synth-voices-changed") == 0) {
+ LOG(LogLevel::Debug, ("SpeechSynthesis::onvoiceschanged"));
+ nsCOMPtr<nsPIDOMWindowInner> window = GetOwner();
+ nsCOMPtr<nsIDocShell> docShell = window ? window->GetDocShell() : nullptr;
+
+ if (!nsContentUtils::ShouldResistFingerprinting(
+ docShell, RFPTarget::SpeechSynthesis)) {
+ DispatchTrustedEvent(u"voiceschanged"_ns);
+ // If we have a pending item, and voices become available, speak it.
+ if (!mCurrentTask && !mHoldQueue && HasVoices()) {
+ AdvanceQueue();
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webspeech/synth/SpeechSynthesis.h b/dom/media/webspeech/synth/SpeechSynthesis.h
new file mode 100644
index 0000000000..1227261b59
--- /dev/null
+++ b/dom/media/webspeech/synth/SpeechSynthesis.h
@@ -0,0 +1,88 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_SpeechSynthesis_h
+#define mozilla_dom_SpeechSynthesis_h
+
+#include "nsCOMPtr.h"
+#include "nsIObserver.h"
+#include "nsRefPtrHashtable.h"
+#include "nsString.h"
+#include "nsWeakReference.h"
+#include "nsWrapperCache.h"
+#include "js/TypeDecls.h"
+
+#include "SpeechSynthesisUtterance.h"
+#include "SpeechSynthesisVoice.h"
+
+class nsIDOMWindow;
+
+namespace mozilla::dom {
+
+class nsSpeechTask;
+
+class SpeechSynthesis final : public DOMEventTargetHelper,
+ public nsIObserver,
+ public nsSupportsWeakReference {
+ public:
+ explicit SpeechSynthesis(nsPIDOMWindowInner* aParent);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(SpeechSynthesis,
+ DOMEventTargetHelper)
+ NS_DECL_NSIOBSERVER
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ bool Pending() const;
+
+ bool Speaking() const;
+
+ bool Paused() const;
+
+ bool HasEmptyQueue() const;
+
+ void Speak(SpeechSynthesisUtterance& aUtterance);
+
+ void Cancel();
+
+ void Pause();
+
+ void Resume();
+
+ void OnEnd(const nsSpeechTask* aTask);
+
+ void GetVoices(nsTArray<RefPtr<SpeechSynthesisVoice> >& aResult);
+
+ void ForceEnd();
+
+ IMPL_EVENT_HANDLER(voiceschanged)
+
+ private:
+ virtual ~SpeechSynthesis();
+
+ void AdvanceQueue();
+
+ bool HasVoices() const;
+
+ bool HasSpeakingTask() const {
+ return mCurrentTask && mCurrentTask->IsSpeaking();
+ }
+
+ nsTArray<RefPtr<SpeechSynthesisUtterance> > mSpeechQueue;
+
+ RefPtr<nsSpeechTask> mCurrentTask;
+
+ nsRefPtrHashtable<nsStringHashKey, SpeechSynthesisVoice> mVoiceCache;
+
+ bool mHoldQueue;
+
+ uint64_t mInnerID;
+};
+
+} // namespace mozilla::dom
+#endif
diff --git a/dom/media/webspeech/synth/SpeechSynthesisUtterance.cpp b/dom/media/webspeech/synth/SpeechSynthesisUtterance.cpp
new file mode 100644
index 0000000000..4d8dcd5c12
--- /dev/null
+++ b/dom/media/webspeech/synth/SpeechSynthesisUtterance.cpp
@@ -0,0 +1,137 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsGkAtoms.h"
+
+#include "mozilla/dom/SpeechSynthesisEvent.h"
+#include "mozilla/dom/SpeechSynthesisUtteranceBinding.h"
+#include "SpeechSynthesisUtterance.h"
+#include "SpeechSynthesisVoice.h"
+
+#include <stdlib.h>
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(SpeechSynthesisUtterance,
+ DOMEventTargetHelper, mVoice);
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SpeechSynthesisUtterance)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+NS_IMPL_ADDREF_INHERITED(SpeechSynthesisUtterance, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(SpeechSynthesisUtterance, DOMEventTargetHelper)
+
+SpeechSynthesisUtterance::SpeechSynthesisUtterance(
+ nsPIDOMWindowInner* aOwnerWindow, const nsAString& text)
+ : DOMEventTargetHelper(aOwnerWindow),
+ mText(text),
+ mVolume(1),
+ mRate(1),
+ mPitch(1),
+ mPaused(false),
+ mShouldResistFingerprinting(
+ aOwnerWindow->AsGlobal()->ShouldResistFingerprinting(
+ RFPTarget::SpeechSynthesis)) {}
+
+SpeechSynthesisUtterance::~SpeechSynthesisUtterance() = default;
+
+JSObject* SpeechSynthesisUtterance::WrapObject(
+ JSContext* aCx, JS::Handle<JSObject*> aGivenProto) {
+ return SpeechSynthesisUtterance_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsISupports* SpeechSynthesisUtterance::GetParentObject() const {
+ return GetOwner();
+}
+
+already_AddRefed<SpeechSynthesisUtterance>
+SpeechSynthesisUtterance::Constructor(GlobalObject& aGlobal, ErrorResult& aRv) {
+ return Constructor(aGlobal, u""_ns, aRv);
+}
+
+already_AddRefed<SpeechSynthesisUtterance>
+SpeechSynthesisUtterance::Constructor(GlobalObject& aGlobal,
+ const nsAString& aText,
+ ErrorResult& aRv) {
+ nsCOMPtr<nsPIDOMWindowInner> win = do_QueryInterface(aGlobal.GetAsSupports());
+
+ if (!win) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ RefPtr<SpeechSynthesisUtterance> object =
+ new SpeechSynthesisUtterance(win, aText);
+ return object.forget();
+}
+
+void SpeechSynthesisUtterance::GetText(nsString& aResult) const {
+ aResult = mText;
+}
+
+void SpeechSynthesisUtterance::SetText(const nsAString& aText) {
+ mText = aText;
+}
+
+void SpeechSynthesisUtterance::GetLang(nsString& aResult) const {
+ aResult = mLang;
+}
+
+void SpeechSynthesisUtterance::SetLang(const nsAString& aLang) {
+ mLang = aLang;
+}
+
+SpeechSynthesisVoice* SpeechSynthesisUtterance::GetVoice() const {
+ return mVoice;
+}
+
+void SpeechSynthesisUtterance::SetVoice(SpeechSynthesisVoice* aVoice) {
+ mVoice = aVoice;
+}
+
+float SpeechSynthesisUtterance::Volume() const { return mVolume; }
+
+void SpeechSynthesisUtterance::SetVolume(float aVolume) {
+ mVolume = std::max<float>(std::min<float>(aVolume, 1), 0);
+}
+
+float SpeechSynthesisUtterance::Rate() const { return mRate; }
+
+void SpeechSynthesisUtterance::SetRate(float aRate) {
+ mRate = std::max<float>(std::min<float>(aRate, 10), 0.1f);
+}
+
+float SpeechSynthesisUtterance::Pitch() const { return mPitch; }
+
+void SpeechSynthesisUtterance::SetPitch(float aPitch) {
+ mPitch = std::max<float>(std::min<float>(aPitch, 2), 0);
+}
+
+void SpeechSynthesisUtterance::GetChosenVoiceURI(nsString& aResult) const {
+ aResult = mChosenVoiceURI;
+}
+
+void SpeechSynthesisUtterance::DispatchSpeechSynthesisEvent(
+ const nsAString& aEventType, uint32_t aCharIndex,
+ const Nullable<uint32_t>& aCharLength, float aElapsedTime,
+ const nsAString& aName) {
+ SpeechSynthesisEventInit init;
+ init.mBubbles = false;
+ init.mCancelable = false;
+ init.mUtterance = this;
+ init.mCharIndex = aCharIndex;
+ init.mCharLength = aCharLength;
+ init.mElapsedTime = aElapsedTime;
+ init.mName = aName;
+
+ RefPtr<SpeechSynthesisEvent> event =
+ SpeechSynthesisEvent::Constructor(this, aEventType, init);
+ DispatchTrustedEvent(event);
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webspeech/synth/SpeechSynthesisUtterance.h b/dom/media/webspeech/synth/SpeechSynthesisUtterance.h
new file mode 100644
index 0000000000..17958a3b32
--- /dev/null
+++ b/dom/media/webspeech/synth/SpeechSynthesisUtterance.h
@@ -0,0 +1,115 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_SpeechSynthesisUtterance_h
+#define mozilla_dom_SpeechSynthesisUtterance_h
+
+#include "mozilla/DOMEventTargetHelper.h"
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "js/TypeDecls.h"
+
+#include "nsSpeechTask.h"
+
+namespace mozilla::dom {
+
+class SpeechSynthesisVoice;
+class SpeechSynthesis;
+class nsSynthVoiceRegistry;
+
+class SpeechSynthesisUtterance final : public DOMEventTargetHelper {
+ friend class SpeechSynthesis;
+ friend class nsSpeechTask;
+ friend class nsSynthVoiceRegistry;
+
+ public:
+ SpeechSynthesisUtterance(nsPIDOMWindowInner* aOwnerWindow,
+ const nsAString& aText);
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(SpeechSynthesisUtterance,
+ DOMEventTargetHelper)
+
+ nsISupports* GetParentObject() const;
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ static already_AddRefed<SpeechSynthesisUtterance> Constructor(
+ GlobalObject& aGlobal, ErrorResult& aRv);
+ static already_AddRefed<SpeechSynthesisUtterance> Constructor(
+ GlobalObject& aGlobal, const nsAString& aText, ErrorResult& aRv);
+
+ void GetText(nsString& aResult) const;
+
+ void SetText(const nsAString& aText);
+
+ void GetLang(nsString& aResult) const;
+
+ void SetLang(const nsAString& aLang);
+
+ SpeechSynthesisVoice* GetVoice() const;
+
+ void SetVoice(SpeechSynthesisVoice* aVoice);
+
+ float Volume() const;
+
+ void SetVolume(float aVolume);
+
+ float Rate() const;
+
+ void SetRate(float aRate);
+
+ float Pitch() const;
+
+ void SetPitch(float aPitch);
+
+ void GetChosenVoiceURI(nsString& aResult) const;
+
+ bool IsPaused() { return mPaused; }
+
+ bool ShouldResistFingerprinting() const {
+ return mShouldResistFingerprinting;
+ }
+
+ IMPL_EVENT_HANDLER(start)
+ IMPL_EVENT_HANDLER(end)
+ IMPL_EVENT_HANDLER(error)
+ IMPL_EVENT_HANDLER(pause)
+ IMPL_EVENT_HANDLER(resume)
+ IMPL_EVENT_HANDLER(mark)
+ IMPL_EVENT_HANDLER(boundary)
+
+ private:
+ virtual ~SpeechSynthesisUtterance();
+
+ void DispatchSpeechSynthesisEvent(const nsAString& aEventType,
+ uint32_t aCharIndex,
+ const Nullable<uint32_t>& aCharLength,
+ float aElapsedTime, const nsAString& aName);
+
+ nsString mText;
+
+ nsString mLang;
+
+ float mVolume;
+
+ float mRate;
+
+ float mPitch;
+
+ nsString mChosenVoiceURI;
+
+ bool mPaused;
+
+ RefPtr<SpeechSynthesisVoice> mVoice;
+
+ bool mShouldResistFingerprinting;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webspeech/synth/SpeechSynthesisVoice.cpp b/dom/media/webspeech/synth/SpeechSynthesisVoice.cpp
new file mode 100644
index 0000000000..a309daca26
--- /dev/null
+++ b/dom/media/webspeech/synth/SpeechSynthesisVoice.cpp
@@ -0,0 +1,72 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SpeechSynthesis.h"
+#include "nsSynthVoiceRegistry.h"
+#include "mozilla/dom/SpeechSynthesisVoiceBinding.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(SpeechSynthesisVoice, mParent)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(SpeechSynthesisVoice)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(SpeechSynthesisVoice)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SpeechSynthesisVoice)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+SpeechSynthesisVoice::SpeechSynthesisVoice(nsISupports* aParent,
+ const nsAString& aUri)
+ : mParent(aParent), mUri(aUri) {}
+
+SpeechSynthesisVoice::~SpeechSynthesisVoice() = default;
+
+JSObject* SpeechSynthesisVoice::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return SpeechSynthesisVoice_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+nsISupports* SpeechSynthesisVoice::GetParentObject() const { return mParent; }
+
+void SpeechSynthesisVoice::GetVoiceURI(nsString& aRetval) const {
+ aRetval = mUri;
+}
+
+void SpeechSynthesisVoice::GetName(nsString& aRetval) const {
+ DebugOnly<nsresult> rv =
+ nsSynthVoiceRegistry::GetInstance()->GetVoiceName(mUri, aRetval);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "Failed to get SpeechSynthesisVoice.name");
+}
+
+void SpeechSynthesisVoice::GetLang(nsString& aRetval) const {
+ DebugOnly<nsresult> rv =
+ nsSynthVoiceRegistry::GetInstance()->GetVoiceLang(mUri, aRetval);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "Failed to get SpeechSynthesisVoice.lang");
+}
+
+bool SpeechSynthesisVoice::LocalService() const {
+ bool isLocal;
+ DebugOnly<nsresult> rv =
+ nsSynthVoiceRegistry::GetInstance()->IsLocalVoice(mUri, &isLocal);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "Failed to get SpeechSynthesisVoice.localService");
+
+ return isLocal;
+}
+
+bool SpeechSynthesisVoice::Default() const {
+ bool isDefault;
+ DebugOnly<nsresult> rv =
+ nsSynthVoiceRegistry::GetInstance()->IsDefaultVoice(mUri, &isDefault);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "Failed to get SpeechSynthesisVoice.default");
+
+ return isDefault;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webspeech/synth/SpeechSynthesisVoice.h b/dom/media/webspeech/synth/SpeechSynthesisVoice.h
new file mode 100644
index 0000000000..079e5f49ea
--- /dev/null
+++ b/dom/media/webspeech/synth/SpeechSynthesisVoice.h
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_SpeechSynthesisVoice_h
+#define mozilla_dom_SpeechSynthesisVoice_h
+
+#include "nsCOMPtr.h"
+#include "nsString.h"
+#include "nsWrapperCache.h"
+#include "js/TypeDecls.h"
+
+namespace mozilla::dom {
+
+class nsSynthVoiceRegistry;
+class SpeechSynthesis;
+
+class SpeechSynthesisVoice final : public nsISupports, public nsWrapperCache {
+ friend class nsSynthVoiceRegistry;
+ friend class SpeechSynthesis;
+
+ public:
+ SpeechSynthesisVoice(nsISupports* aParent, const nsAString& aUri);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(SpeechSynthesisVoice)
+
+ nsISupports* GetParentObject() const;
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ void GetVoiceURI(nsString& aRetval) const;
+
+ void GetName(nsString& aRetval) const;
+
+ void GetLang(nsString& aRetval) const;
+
+ bool LocalService() const;
+
+ bool Default() const;
+
+ private:
+ virtual ~SpeechSynthesisVoice();
+
+ nsCOMPtr<nsISupports> mParent;
+
+ nsString mUri;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webspeech/synth/android/SpeechSynthesisService.cpp b/dom/media/webspeech/synth/android/SpeechSynthesisService.cpp
new file mode 100644
index 0000000000..1b6e4b6125
--- /dev/null
+++ b/dom/media/webspeech/synth/android/SpeechSynthesisService.cpp
@@ -0,0 +1,215 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SpeechSynthesisService.h"
+
+#include <android/log.h>
+
+#include "nsXULAppAPI.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/dom/nsSynthVoiceRegistry.h"
+#include "mozilla/jni/Utils.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_media.h"
+
+#define ALOG(args...) \
+ __android_log_print(ANDROID_LOG_INFO, "GeckoSpeechSynthesis", ##args)
+
+namespace mozilla {
+namespace dom {
+
+StaticRefPtr<SpeechSynthesisService> SpeechSynthesisService::sSingleton;
+
+class AndroidSpeechCallback final : public nsISpeechTaskCallback {
+ public:
+ AndroidSpeechCallback() {}
+
+ NS_DECL_ISUPPORTS
+
+ NS_IMETHOD OnResume() override { return NS_OK; }
+
+ NS_IMETHOD OnPause() override { return NS_OK; }
+
+ NS_IMETHOD OnCancel() override {
+ java::SpeechSynthesisService::Stop();
+ return NS_OK;
+ }
+
+ NS_IMETHOD OnVolumeChanged(float aVolume) override { return NS_OK; }
+
+ private:
+ ~AndroidSpeechCallback() {}
+};
+
+NS_IMPL_ISUPPORTS(AndroidSpeechCallback, nsISpeechTaskCallback)
+
+NS_IMPL_ISUPPORTS(SpeechSynthesisService, nsISpeechService)
+
+void SpeechSynthesisService::Setup() {
+ ALOG("SpeechSynthesisService::Setup");
+
+ if (!StaticPrefs::media_webspeech_synth_enabled() ||
+ Preferences::GetBool("media.webspeech.synth.test")) {
+ return;
+ }
+
+ if (!jni::IsAvailable()) {
+ NS_WARNING("Failed to initialize speech synthesis");
+ return;
+ }
+
+ Init();
+ java::SpeechSynthesisService::InitSynth();
+}
+
+// nsISpeechService
+
+NS_IMETHODIMP
+SpeechSynthesisService::Speak(const nsAString& aText, const nsAString& aUri,
+ float aVolume, float aRate, float aPitch,
+ nsISpeechTask* aTask) {
+ if (mTask) {
+ NS_WARNING("Service only supports one speech task at a time.");
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ RefPtr<AndroidSpeechCallback> callback = new AndroidSpeechCallback();
+ nsresult rv = aTask->Setup(callback);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ jni::String::LocalRef utteranceId =
+ java::SpeechSynthesisService::Speak(aUri, aText, aRate, aPitch, aVolume);
+ if (!utteranceId) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ mTaskUtteranceId = utteranceId->ToCString();
+ mTask = aTask;
+ mTaskTextLength = aText.Length();
+ mTaskTextOffset = 0;
+
+ return NS_OK;
+}
+
+SpeechSynthesisService* SpeechSynthesisService::GetInstance(bool aCreate) {
+ if (XRE_GetProcessType() != GeckoProcessType_Default) {
+ MOZ_ASSERT(
+ false,
+ "SpeechSynthesisService can only be started on main gecko process");
+ return nullptr;
+ }
+
+ if (!sSingleton && aCreate) {
+ sSingleton = new SpeechSynthesisService();
+ sSingleton->Setup();
+ ClearOnShutdown(&sSingleton);
+ }
+
+ return sSingleton;
+}
+
+already_AddRefed<SpeechSynthesisService>
+SpeechSynthesisService::GetInstanceForService() {
+ MOZ_ASSERT(NS_IsMainThread());
+ RefPtr<SpeechSynthesisService> sapiService = GetInstance();
+ return sapiService.forget();
+}
+
+// JNI
+
+void SpeechSynthesisService::RegisterVoice(jni::String::Param aUri,
+ jni::String::Param aName,
+ jni::String::Param aLocale,
+ bool aIsNetwork, bool aIsDefault) {
+ nsSynthVoiceRegistry* registry = nsSynthVoiceRegistry::GetInstance();
+ SpeechSynthesisService* service = SpeechSynthesisService::GetInstance(false);
+ // This service can only speak one utterance at a time, so we set
+ // aQueuesUtterances to true in order to track global state and schedule
+ // access to this service.
+ DebugOnly<nsresult> rv =
+ registry->AddVoice(service, aUri->ToString(), aName->ToString(),
+ aLocale->ToString(), !aIsNetwork, true);
+
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to add voice");
+
+ if (aIsDefault) {
+ DebugOnly<nsresult> rv = registry->SetDefaultVoice(aUri->ToString(), true);
+
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to set voice as default");
+ }
+}
+
+void SpeechSynthesisService::DoneRegisteringVoices() {
+ nsSynthVoiceRegistry* registry = nsSynthVoiceRegistry::GetInstance();
+ registry->NotifyVoicesChanged();
+}
+
+void SpeechSynthesisService::DispatchStart(jni::String::Param aUtteranceId) {
+ if (sSingleton) {
+ MOZ_ASSERT(sSingleton->mTaskUtteranceId.Equals(aUtteranceId->ToCString()));
+ nsCOMPtr<nsISpeechTask> task = sSingleton->mTask;
+ if (task) {
+ sSingleton->mTaskStartTime = TimeStamp::Now();
+ DebugOnly<nsresult> rv = task->DispatchStart();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Unable to dispatch start");
+ }
+ }
+}
+
+void SpeechSynthesisService::DispatchEnd(jni::String::Param aUtteranceId) {
+ if (sSingleton) {
+ // In API older than 23, we will sometimes call this function
+ // without providing an utterance ID.
+ MOZ_ASSERT(!aUtteranceId ||
+ sSingleton->mTaskUtteranceId.Equals(aUtteranceId->ToCString()));
+ nsCOMPtr<nsISpeechTask> task = sSingleton->mTask;
+ sSingleton->mTask = nullptr;
+ if (task) {
+ TimeStamp startTime = sSingleton->mTaskStartTime;
+ DebugOnly<nsresult> rv =
+ task->DispatchEnd((TimeStamp::Now() - startTime).ToSeconds(),
+ sSingleton->mTaskTextLength);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Unable to dispatch start");
+ }
+ }
+}
+
+void SpeechSynthesisService::DispatchError(jni::String::Param aUtteranceId) {
+ if (sSingleton) {
+ MOZ_ASSERT(sSingleton->mTaskUtteranceId.Equals(aUtteranceId->ToCString()));
+ nsCOMPtr<nsISpeechTask> task = sSingleton->mTask;
+ sSingleton->mTask = nullptr;
+ if (task) {
+ TimeStamp startTime = sSingleton->mTaskStartTime;
+ DebugOnly<nsresult> rv =
+ task->DispatchError((TimeStamp::Now() - startTime).ToSeconds(),
+ sSingleton->mTaskTextOffset);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Unable to dispatch start");
+ }
+ }
+}
+
+void SpeechSynthesisService::DispatchBoundary(jni::String::Param aUtteranceId,
+ int32_t aStart, int32_t aEnd) {
+ if (sSingleton) {
+ MOZ_ASSERT(sSingleton->mTaskUtteranceId.Equals(aUtteranceId->ToCString()));
+ nsCOMPtr<nsISpeechTask> task = sSingleton->mTask;
+ if (task) {
+ TimeStamp startTime = sSingleton->mTaskStartTime;
+ sSingleton->mTaskTextOffset = aStart;
+ DebugOnly<nsresult> rv = task->DispatchBoundary(
+ u"word"_ns, (TimeStamp::Now() - startTime).ToSeconds(), aStart,
+ aEnd - aStart, 1);
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Unable to dispatch boundary");
+ }
+ }
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/media/webspeech/synth/android/SpeechSynthesisService.h b/dom/media/webspeech/synth/android/SpeechSynthesisService.h
new file mode 100644
index 0000000000..98c5143cf6
--- /dev/null
+++ b/dom/media/webspeech/synth/android/SpeechSynthesisService.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_SpeechSynthesisService_h
+#define mozilla_dom_SpeechSynthesisService_h
+
+#include "nsISpeechService.h"
+#include "mozilla/java/SpeechSynthesisServiceNatives.h"
+#include "mozilla/StaticPtr.h"
+
+namespace mozilla {
+namespace dom {
+
+class SpeechSynthesisService final
+ : public nsISpeechService,
+ public java::SpeechSynthesisService::Natives<SpeechSynthesisService> {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISPEECHSERVICE
+
+ SpeechSynthesisService(){};
+
+ void Setup();
+
+ static void DoneRegisteringVoices();
+
+ static void RegisterVoice(jni::String::Param aUri, jni::String::Param aName,
+ jni::String::Param aLocale, bool aIsNetwork,
+ bool aIsDefault);
+
+ static void DispatchStart(jni::String::Param aUtteranceId);
+
+ static void DispatchEnd(jni::String::Param aUtteranceId);
+
+ static void DispatchError(jni::String::Param aUtteranceId);
+
+ static void DispatchBoundary(jni::String::Param aUtteranceId, int32_t aStart,
+ int32_t aEnd);
+
+ static SpeechSynthesisService* GetInstance(bool aCreate = true);
+ static already_AddRefed<SpeechSynthesisService> GetInstanceForService();
+
+ static StaticRefPtr<SpeechSynthesisService> sSingleton;
+
+ private:
+ virtual ~SpeechSynthesisService(){};
+
+ nsCOMPtr<nsISpeechTask> mTask;
+
+ // Unique ID assigned to utterance when it is sent to system service.
+ nsCString mTaskUtteranceId;
+
+ // Time stamp from the moment the utterance is started.
+ TimeStamp mTaskStartTime;
+
+ // Length of text of the utterance.
+ uint32_t mTaskTextLength;
+
+ // Current offset in characters of what has been spoken.
+ uint32_t mTaskTextOffset;
+};
+
+} // namespace dom
+} // namespace mozilla
+#endif
diff --git a/dom/media/webspeech/synth/android/components.conf b/dom/media/webspeech/synth/android/components.conf
new file mode 100644
index 0000000000..4c35954fcc
--- /dev/null
+++ b/dom/media/webspeech/synth/android/components.conf
@@ -0,0 +1,17 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+ {
+ 'cid': '{311b2dab-f4d3-4be4-8123-6732313d95c2}',
+ 'contract_ids': ['@mozilla.org/androidspeechsynth;1'],
+ 'singleton': True,
+ 'type': 'mozilla::dom::SpeechSynthesisService',
+ 'headers': ['/dom/media/webspeech/synth/android/SpeechSynthesisService.h'],
+ 'constructor': 'mozilla::dom::SpeechSynthesisService::GetInstanceForService',
+ 'categories': {"speech-synth-started": 'Android Speech Synth'},
+ },
+]
diff --git a/dom/media/webspeech/synth/android/moz.build b/dom/media/webspeech/synth/android/moz.build
new file mode 100644
index 0000000000..348c157f3c
--- /dev/null
+++ b/dom/media/webspeech/synth/android/moz.build
@@ -0,0 +1,19 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS.mozilla.dom += ["SpeechSynthesisService.h"]
+
+UNIFIED_SOURCES += [
+ "SpeechSynthesisService.cpp",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/webspeech/synth/cocoa/OSXSpeechSynthesizerService.h b/dom/media/webspeech/synth/cocoa/OSXSpeechSynthesizerService.h
new file mode 100644
index 0000000000..6148d59c92
--- /dev/null
+++ b/dom/media/webspeech/synth/cocoa/OSXSpeechSynthesizerService.h
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_OsxSpeechSynthesizerService_h
+#define mozilla_dom_OsxSpeechSynthesizerService_h
+
+#include "nsISpeechService.h"
+#include "nsIObserver.h"
+#include "mozilla/StaticPtr.h"
+
+namespace mozilla {
+namespace dom {
+
+class OSXSpeechSynthesizerService final : public nsISpeechService,
+ public nsIObserver {
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSISPEECHSERVICE
+ NS_DECL_NSIOBSERVER
+
+ bool Init();
+
+ static OSXSpeechSynthesizerService* GetInstance();
+ static already_AddRefed<OSXSpeechSynthesizerService> GetInstanceForService();
+
+ private:
+ OSXSpeechSynthesizerService();
+ virtual ~OSXSpeechSynthesizerService() = default;
+
+ bool RegisterVoices();
+
+ bool mInitialized;
+ static mozilla::StaticRefPtr<OSXSpeechSynthesizerService> sSingleton;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webspeech/synth/cocoa/OSXSpeechSynthesizerService.mm b/dom/media/webspeech/synth/cocoa/OSXSpeechSynthesizerService.mm
new file mode 100644
index 0000000000..a815c68644
--- /dev/null
+++ b/dom/media/webspeech/synth/cocoa/OSXSpeechSynthesizerService.mm
@@ -0,0 +1,431 @@
+/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 sw=2 et tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.h"
+#include "nsServiceManagerUtils.h"
+#include "nsObjCExceptions.h"
+#include "nsCocoaUtils.h"
+#include "nsIThread.h"
+#include "nsThreadUtils.h"
+#include "nsXULAppAPI.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/dom/nsSynthVoiceRegistry.h"
+#include "mozilla/dom/nsSpeechTask.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/Assertions.h"
+#include "OSXSpeechSynthesizerService.h"
+
+#import <Cocoa/Cocoa.h>
+
+@class SpeechDelegate;
+
+// We can escape the default delimiters ("[[" and "]]") by temporarily
+// changing the delimiters just before they appear, and changing them back
+// just after.
+#define DLIM_ESCAPE_START "[[dlim (( ))]]"
+#define DLIM_ESCAPE_END "((dlim [[ ]]))"
+
+using namespace mozilla;
+
+class SpeechTaskCallback final : public nsISpeechTaskCallback {
+ public:
+ SpeechTaskCallback(nsISpeechTask* aTask, NSSpeechSynthesizer* aSynth,
+ const nsTArray<size_t>& aOffsets);
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(SpeechTaskCallback, nsISpeechTaskCallback)
+
+ NS_DECL_NSISPEECHTASKCALLBACK
+
+ void OnWillSpeakWord(uint32_t aIndex, uint32_t aLength);
+ void OnError(uint32_t aIndex);
+ void OnDidFinishSpeaking();
+
+ private:
+ virtual ~SpeechTaskCallback();
+
+ float GetTimeDurationFromStart();
+
+ nsCOMPtr<nsISpeechTask> mTask;
+ NSSpeechSynthesizer* mSpeechSynthesizer;
+ SpeechDelegate* mDelegate;
+ TimeStamp mStartingTime;
+ uint32_t mCurrentIndex;
+ nsTArray<size_t> mOffsets;
+};
+
+@interface SpeechDelegate : NSObject <NSSpeechSynthesizerDelegate> {
+ @private
+ SpeechTaskCallback* mCallback;
+}
+
+- (id)initWithCallback:(SpeechTaskCallback*)aCallback;
+@end
+
+@implementation SpeechDelegate
+- (id)initWithCallback:(SpeechTaskCallback*)aCallback {
+ [super init];
+ mCallback = aCallback;
+ return self;
+}
+
+- (void)speechSynthesizer:(NSSpeechSynthesizer*)aSender
+ willSpeakWord:(NSRange)aRange
+ ofString:(NSString*)aString {
+ mCallback->OnWillSpeakWord(aRange.location, aRange.length);
+}
+
+- (void)speechSynthesizer:(NSSpeechSynthesizer*)aSender didFinishSpeaking:(BOOL)aFinishedSpeaking {
+ mCallback->OnDidFinishSpeaking();
+}
+
+- (void)speechSynthesizer:(NSSpeechSynthesizer*)aSender
+ didEncounterErrorAtIndex:(NSUInteger)aCharacterIndex
+ ofString:(NSString*)aString
+ message:(NSString*)aMessage {
+ mCallback->OnError(aCharacterIndex);
+}
+@end
+
+NS_IMPL_CYCLE_COLLECTION(SpeechTaskCallback, mTask);
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SpeechTaskCallback)
+ NS_INTERFACE_MAP_ENTRY(nsISpeechTaskCallback)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsISpeechTaskCallback)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(SpeechTaskCallback)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(SpeechTaskCallback)
+
+SpeechTaskCallback::SpeechTaskCallback(nsISpeechTask* aTask, NSSpeechSynthesizer* aSynth,
+ const nsTArray<size_t>& aOffsets)
+ : mTask(aTask), mSpeechSynthesizer(aSynth), mCurrentIndex(0), mOffsets(aOffsets.Clone()) {
+ mDelegate = [[SpeechDelegate alloc] initWithCallback:this];
+ [mSpeechSynthesizer setDelegate:mDelegate];
+ mStartingTime = TimeStamp::Now();
+}
+
+SpeechTaskCallback::~SpeechTaskCallback() {
+ [mSpeechSynthesizer setDelegate:nil];
+ [mDelegate release];
+ [mSpeechSynthesizer release];
+}
+
+NS_IMETHODIMP
+SpeechTaskCallback::OnCancel() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ [mSpeechSynthesizer stopSpeaking];
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+SpeechTaskCallback::OnPause() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ [mSpeechSynthesizer pauseSpeakingAtBoundary:NSSpeechImmediateBoundary];
+ if (!mTask) {
+ // When calling pause() on child porcess, it may not receive end event
+ // from chrome process yet.
+ return NS_ERROR_FAILURE;
+ }
+ mTask->DispatchPause(GetTimeDurationFromStart(), mCurrentIndex);
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+SpeechTaskCallback::OnResume() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ [mSpeechSynthesizer continueSpeaking];
+ if (!mTask) {
+ // When calling resume() on child porcess, it may not receive end event
+ // from chrome process yet.
+ return NS_ERROR_FAILURE;
+ }
+ mTask->DispatchResume(GetTimeDurationFromStart(), mCurrentIndex);
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+SpeechTaskCallback::OnVolumeChanged(float aVolume) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ [mSpeechSynthesizer setObject:[NSNumber numberWithFloat:aVolume]
+ forProperty:NSSpeechVolumeProperty
+ error:nil];
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+float SpeechTaskCallback::GetTimeDurationFromStart() {
+ TimeDuration duration = TimeStamp::Now() - mStartingTime;
+ return duration.ToSeconds();
+}
+
+void SpeechTaskCallback::OnWillSpeakWord(uint32_t aIndex, uint32_t aLength) {
+ mCurrentIndex = aIndex < mOffsets.Length() ? mOffsets[aIndex] : mCurrentIndex;
+ if (!mTask) {
+ return;
+ }
+ mTask->DispatchBoundary(u"word"_ns, GetTimeDurationFromStart(), mCurrentIndex, aLength, 1);
+}
+
+void SpeechTaskCallback::OnError(uint32_t aIndex) {
+ if (!mTask) {
+ return;
+ }
+ mTask->DispatchError(GetTimeDurationFromStart(), aIndex);
+}
+
+void SpeechTaskCallback::OnDidFinishSpeaking() {
+ mTask->DispatchEnd(GetTimeDurationFromStart(), mCurrentIndex);
+ // no longer needed
+ [mSpeechSynthesizer setDelegate:nil];
+ mTask = nullptr;
+}
+
+namespace mozilla {
+namespace dom {
+
+struct OSXVoice {
+ OSXVoice() : mIsDefault(false) {}
+
+ nsString mUri;
+ nsString mName;
+ nsString mLocale;
+ bool mIsDefault;
+};
+
+class RegisterVoicesRunnable final : public Runnable {
+ public:
+ RegisterVoicesRunnable(OSXSpeechSynthesizerService* aSpeechService, nsTArray<OSXVoice>& aList)
+ : Runnable("RegisterVoicesRunnable"), mSpeechService(aSpeechService), mVoices(aList) {}
+
+ NS_IMETHOD Run() override;
+
+ private:
+ ~RegisterVoicesRunnable() override = default;
+
+ // This runnable always use sync mode. It is unnecesarry to reference object
+ OSXSpeechSynthesizerService* mSpeechService;
+ nsTArray<OSXVoice>& mVoices;
+};
+
+NS_IMETHODIMP
+RegisterVoicesRunnable::Run() {
+ nsresult rv;
+ nsCOMPtr<nsISynthVoiceRegistry> registry = do_GetService(NS_SYNTHVOICEREGISTRY_CONTRACTID, &rv);
+ if (!registry) {
+ return rv;
+ }
+
+ for (OSXVoice voice : mVoices) {
+ rv = registry->AddVoice(mSpeechService, voice.mUri, voice.mName, voice.mLocale, true, false);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ continue;
+ }
+
+ if (voice.mIsDefault) {
+ registry->SetDefaultVoice(voice.mUri, true);
+ }
+ }
+
+ registry->NotifyVoicesChanged();
+
+ return NS_OK;
+}
+
+class EnumVoicesRunnable final : public Runnable {
+ public:
+ explicit EnumVoicesRunnable(OSXSpeechSynthesizerService* aSpeechService)
+ : Runnable("EnumVoicesRunnable"), mSpeechService(aSpeechService) {}
+
+ NS_IMETHOD Run() override;
+
+ private:
+ ~EnumVoicesRunnable() override = default;
+
+ RefPtr<OSXSpeechSynthesizerService> mSpeechService;
+};
+
+NS_IMETHODIMP
+EnumVoicesRunnable::Run() {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ AutoTArray<OSXVoice, 64> list;
+
+ NSArray* voices = [NSSpeechSynthesizer availableVoices];
+ NSString* defaultVoice = [NSSpeechSynthesizer defaultVoice];
+
+ for (NSString* voice in voices) {
+ OSXVoice item;
+
+ NSDictionary* attr = [NSSpeechSynthesizer attributesForVoice:voice];
+
+ nsAutoString identifier;
+ nsCocoaUtils::GetStringForNSString([attr objectForKey:NSVoiceIdentifier], identifier);
+
+ nsCocoaUtils::GetStringForNSString([attr objectForKey:NSVoiceName], item.mName);
+
+ nsCocoaUtils::GetStringForNSString([attr objectForKey:NSVoiceLocaleIdentifier], item.mLocale);
+ item.mLocale.ReplaceChar('_', '-');
+
+ item.mUri.AssignLiteral("urn:moz-tts:osx:");
+ item.mUri.Append(identifier);
+
+ if ([voice isEqualToString:defaultVoice]) {
+ item.mIsDefault = true;
+ }
+
+ list.AppendElement(item);
+ }
+
+ RefPtr<RegisterVoicesRunnable> runnable = new RegisterVoicesRunnable(mSpeechService, list);
+ NS_DispatchAndSpinEventLoopUntilComplete("EnumVoicesRunnable"_ns,
+ GetMainThreadSerialEventTarget(), runnable.forget());
+
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+StaticRefPtr<OSXSpeechSynthesizerService> OSXSpeechSynthesizerService::sSingleton;
+
+NS_INTERFACE_MAP_BEGIN(OSXSpeechSynthesizerService)
+ NS_INTERFACE_MAP_ENTRY(nsISpeechService)
+ NS_INTERFACE_MAP_ENTRY(nsIObserver)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsISpeechService)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_ADDREF(OSXSpeechSynthesizerService)
+NS_IMPL_RELEASE(OSXSpeechSynthesizerService)
+
+OSXSpeechSynthesizerService::OSXSpeechSynthesizerService() : mInitialized(false) {}
+
+bool OSXSpeechSynthesizerService::Init() {
+ if (Preferences::GetBool("media.webspeech.synth.test") ||
+ !StaticPrefs::media_webspeech_synth_enabled()) {
+ // When test is enabled, we shouldn't add OS backend (Bug 1160844)
+ return false;
+ }
+
+ nsCOMPtr<nsIThread> thread;
+ if (NS_FAILED(NS_NewNamedThread("SpeechWorker", getter_AddRefs(thread)))) {
+ return false;
+ }
+
+ // Get all the voices and register in the SynthVoiceRegistry
+ nsCOMPtr<nsIRunnable> runnable = new EnumVoicesRunnable(this);
+ thread->Dispatch(runnable, NS_DISPATCH_NORMAL);
+
+ mInitialized = true;
+ return true;
+}
+
+NS_IMETHODIMP
+OSXSpeechSynthesizerService::Speak(const nsAString& aText, const nsAString& aUri, float aVolume,
+ float aRate, float aPitch, nsISpeechTask* aTask) {
+ NS_OBJC_BEGIN_TRY_BLOCK_RETURN;
+
+ MOZ_ASSERT(StringBeginsWith(aUri, u"urn:moz-tts:osx:"_ns),
+ "OSXSpeechSynthesizerService doesn't allow this voice URI");
+
+ NSSpeechSynthesizer* synth = [[NSSpeechSynthesizer alloc] init];
+ // strlen("urn:moz-tts:osx:") == 16
+ NSString* identifier = nsCocoaUtils::ToNSString(Substring(aUri, 16));
+ [synth setVoice:identifier];
+
+ // default rate is 180-220
+ [synth setObject:[NSNumber numberWithInt:aRate * 200] forProperty:NSSpeechRateProperty error:nil];
+ // volume allows 0.0-1.0
+ [synth setObject:[NSNumber numberWithFloat:aVolume] forProperty:NSSpeechVolumeProperty error:nil];
+ // Use default pitch value to calculate this
+ NSNumber* defaultPitch = [synth objectForProperty:NSSpeechPitchBaseProperty error:nil];
+ if (defaultPitch) {
+ int newPitch = [defaultPitch intValue] * (aPitch / 2 + 0.5);
+ [synth setObject:[NSNumber numberWithInt:newPitch]
+ forProperty:NSSpeechPitchBaseProperty
+ error:nil];
+ }
+
+ nsAutoString escapedText;
+ // We need to map the the offsets from the given text to the escaped text.
+ // The index of the offsets array is the position in the escaped text,
+ // the element value is the position in the user-supplied text.
+ nsTArray<size_t> offsets;
+ offsets.SetCapacity(aText.Length());
+
+ // This loop looks for occurances of "[[" or "]]", escapes them, and
+ // populates the offsets array to supply a map to the original offsets.
+ for (size_t i = 0; i < aText.Length(); i++) {
+ if (aText.Length() > i + 1 &&
+ ((aText[i] == ']' && aText[i + 1] == ']') || (aText[i] == '[' && aText[i + 1] == '['))) {
+ escapedText.AppendLiteral(DLIM_ESCAPE_START);
+ offsets.AppendElements(strlen(DLIM_ESCAPE_START));
+ escapedText.Append(aText[i]);
+ offsets.AppendElement(i);
+ escapedText.Append(aText[++i]);
+ offsets.AppendElement(i);
+ escapedText.AppendLiteral(DLIM_ESCAPE_END);
+ offsets.AppendElements(strlen(DLIM_ESCAPE_END));
+ } else {
+ escapedText.Append(aText[i]);
+ offsets.AppendElement(i);
+ }
+ }
+
+ RefPtr<SpeechTaskCallback> callback = new SpeechTaskCallback(aTask, synth, offsets);
+ nsresult rv = aTask->Setup(callback);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ NSString* text = nsCocoaUtils::ToNSString(escapedText);
+ BOOL success = [synth startSpeakingString:text];
+ NS_ENSURE_TRUE(success, NS_ERROR_FAILURE);
+
+ aTask->DispatchStart();
+ return NS_OK;
+
+ NS_OBJC_END_TRY_BLOCK_RETURN(NS_ERROR_FAILURE);
+}
+
+NS_IMETHODIMP
+OSXSpeechSynthesizerService::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ return NS_OK;
+}
+
+OSXSpeechSynthesizerService* OSXSpeechSynthesizerService::GetInstance() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (XRE_GetProcessType() != GeckoProcessType_Default) {
+ return nullptr;
+ }
+
+ if (!sSingleton) {
+ RefPtr<OSXSpeechSynthesizerService> speechService = new OSXSpeechSynthesizerService();
+ if (speechService->Init()) {
+ sSingleton = speechService;
+ ClearOnShutdown(&sSingleton);
+ }
+ }
+ return sSingleton;
+}
+
+already_AddRefed<OSXSpeechSynthesizerService> OSXSpeechSynthesizerService::GetInstanceForService() {
+ RefPtr<OSXSpeechSynthesizerService> speechService = GetInstance();
+ return speechService.forget();
+}
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/media/webspeech/synth/cocoa/components.conf b/dom/media/webspeech/synth/cocoa/components.conf
new file mode 100644
index 0000000000..c9b0fa5ef0
--- /dev/null
+++ b/dom/media/webspeech/synth/cocoa/components.conf
@@ -0,0 +1,17 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+ {
+ 'cid': '{914e73b4-6337-4bef-97f3-4d069e053a12}',
+ 'contract_ids': ['@mozilla.org/synthsystem;1'],
+ 'singleton': True,
+ 'type': 'mozilla::dom::OSXSpeechSynthesizerService',
+ 'headers': ['/dom/media/webspeech/synth/cocoa/OSXSpeechSynthesizerService.h'],
+ 'constructor': 'mozilla::dom::OSXSpeechSynthesizerService::GetInstanceForService',
+ 'categories': {"speech-synth-started": 'OSX Speech Synth'},
+ },
+]
diff --git a/dom/media/webspeech/synth/cocoa/moz.build b/dom/media/webspeech/synth/cocoa/moz.build
new file mode 100644
index 0000000000..4d59f7a389
--- /dev/null
+++ b/dom/media/webspeech/synth/cocoa/moz.build
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+SOURCES += [
+ "OSXSpeechSynthesizerService.mm",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/webspeech/synth/crashtests/1230428.html b/dom/media/webspeech/synth/crashtests/1230428.html
new file mode 100644
index 0000000000..40fa000710
--- /dev/null
+++ b/dom/media/webspeech/synth/crashtests/1230428.html
@@ -0,0 +1,32 @@
+<!DOCTYPE html>
+<html class="reftest-wait">
+<head>
+<meta charset="utf-8">
+<script type="application/javascript">
+function f()
+{
+ if (speechSynthesis.getVoices().length == 0) {
+ // No synthesis backend to test this
+ document.documentElement.removeAttribute('class');
+ return;
+ }
+
+ var s = new SpeechSynthesisUtterance("hello world");
+ s.onerror = () => {
+ // No synthesis backend to test this
+ document.documentElement.removeAttribute('class');
+ return;
+ }
+ s.onend = () => {
+ document.documentElement.removeAttribute('class');
+ };
+ speechSynthesis.speak(s);
+ speechSynthesis.cancel();
+ speechSynthesis.pause();
+ speechSynthesis.resume();
+}
+ </script>
+</head>
+<body onload="f();">
+</body>
+</html>
diff --git a/dom/media/webspeech/synth/crashtests/crashtests.list b/dom/media/webspeech/synth/crashtests/crashtests.list
new file mode 100644
index 0000000000..07e931c929
--- /dev/null
+++ b/dom/media/webspeech/synth/crashtests/crashtests.list
@@ -0,0 +1 @@
+skip-if(!cocoaWidget) pref(media.webspeech.synth.enabled,true) load 1230428.html # bug 1230428
diff --git a/dom/media/webspeech/synth/ipc/PSpeechSynthesis.ipdl b/dom/media/webspeech/synth/ipc/PSpeechSynthesis.ipdl
new file mode 100644
index 0000000000..38e360bf4c
--- /dev/null
+++ b/dom/media/webspeech/synth/ipc/PSpeechSynthesis.ipdl
@@ -0,0 +1,50 @@
+/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PContent;
+include protocol PSpeechSynthesisRequest;
+
+namespace mozilla {
+namespace dom {
+
+struct RemoteVoice {
+ nsString voiceURI;
+ nsString name;
+ nsString lang;
+ bool localService;
+ bool queued;
+};
+
+[ManualDealloc]
+sync protocol PSpeechSynthesis
+{
+ manager PContent;
+ manages PSpeechSynthesisRequest;
+
+child:
+
+ async VoiceAdded(RemoteVoice aVoice);
+
+ async VoiceRemoved(nsString aUri);
+
+ async SetDefaultVoice(nsString aUri, bool aIsDefault);
+
+ async IsSpeakingChanged(bool aIsSpeaking);
+
+ async NotifyVoicesChanged();
+
+ async InitialVoicesAndState(RemoteVoice[] aVoices, nsString[] aDefaults,
+ bool aIsSpeaking);
+
+parent:
+ async __delete__();
+
+ async PSpeechSynthesisRequest(nsString aText, nsString aUri, nsString aLang,
+ float aVolume, float aRate, float aPitch, bool aShouldResistFingerprinting);
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/media/webspeech/synth/ipc/PSpeechSynthesisRequest.ipdl b/dom/media/webspeech/synth/ipc/PSpeechSynthesisRequest.ipdl
new file mode 100644
index 0000000000..8543eebc5b
--- /dev/null
+++ b/dom/media/webspeech/synth/ipc/PSpeechSynthesisRequest.ipdl
@@ -0,0 +1,48 @@
+/* -*- Mode: c++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 40 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+include protocol PSpeechSynthesis;
+
+namespace mozilla {
+namespace dom {
+
+[ManualDealloc, ChildImpl=virtual, ParentImpl=virtual]
+async protocol PSpeechSynthesisRequest
+{
+ manager PSpeechSynthesis;
+
+ parent:
+
+ async __delete__();
+
+ async Pause();
+
+ async Resume();
+
+ async Cancel();
+
+ async ForceEnd();
+
+ async SetAudioOutputVolume(float aVolume);
+
+ child:
+
+ async OnEnd(bool aIsError, float aElapsedTime, uint32_t aCharIndex);
+
+ async OnStart(nsString aUri);
+
+ async OnPause(float aElapsedTime, uint32_t aCharIndex);
+
+ async OnResume(float aElapsedTime, uint32_t aCharIndex);
+
+ async OnBoundary(nsString aName, float aElapsedTime, uint32_t aCharIndex,
+ uint32_t aCharLength, uint8_t argc);
+
+ async OnMark(nsString aName, float aElapsedTime, uint32_t aCharIndex);
+};
+
+} // namespace dom
+} // namespace mozilla
diff --git a/dom/media/webspeech/synth/ipc/SpeechSynthesisChild.cpp b/dom/media/webspeech/synth/ipc/SpeechSynthesisChild.cpp
new file mode 100644
index 0000000000..9a9e9b6fe2
--- /dev/null
+++ b/dom/media/webspeech/synth/ipc/SpeechSynthesisChild.cpp
@@ -0,0 +1,169 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SpeechSynthesisChild.h"
+#include "nsSynthVoiceRegistry.h"
+
+namespace mozilla::dom {
+
+SpeechSynthesisChild::SpeechSynthesisChild() {
+ MOZ_COUNT_CTOR(SpeechSynthesisChild);
+}
+
+SpeechSynthesisChild::~SpeechSynthesisChild() {
+ MOZ_COUNT_DTOR(SpeechSynthesisChild);
+}
+
+mozilla::ipc::IPCResult SpeechSynthesisChild::RecvInitialVoicesAndState(
+ nsTArray<RemoteVoice>&& aVoices, nsTArray<nsString>&& aDefaults,
+ const bool& aIsSpeaking) {
+ nsSynthVoiceRegistry::RecvInitialVoicesAndState(aVoices, aDefaults,
+ aIsSpeaking);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult SpeechSynthesisChild::RecvVoiceAdded(
+ const RemoteVoice& aVoice) {
+ nsSynthVoiceRegistry::RecvAddVoice(aVoice);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult SpeechSynthesisChild::RecvVoiceRemoved(
+ const nsAString& aUri) {
+ nsSynthVoiceRegistry::RecvRemoveVoice(aUri);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult SpeechSynthesisChild::RecvSetDefaultVoice(
+ const nsAString& aUri, const bool& aIsDefault) {
+ nsSynthVoiceRegistry::RecvSetDefaultVoice(aUri, aIsDefault);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult SpeechSynthesisChild::RecvIsSpeakingChanged(
+ const bool& aIsSpeaking) {
+ nsSynthVoiceRegistry::RecvIsSpeakingChanged(aIsSpeaking);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult SpeechSynthesisChild::RecvNotifyVoicesChanged() {
+ nsSynthVoiceRegistry::RecvNotifyVoicesChanged();
+ return IPC_OK();
+}
+
+PSpeechSynthesisRequestChild*
+SpeechSynthesisChild::AllocPSpeechSynthesisRequestChild(
+ const nsAString& aText, const nsAString& aLang, const nsAString& aUri,
+ const float& aVolume, const float& aRate, const float& aPitch,
+ const bool& aShouldResistFingerprinting) {
+ MOZ_CRASH("Caller is supposed to manually construct a request!");
+}
+
+bool SpeechSynthesisChild::DeallocPSpeechSynthesisRequestChild(
+ PSpeechSynthesisRequestChild* aActor) {
+ delete aActor;
+ return true;
+}
+
+// SpeechSynthesisRequestChild
+
+SpeechSynthesisRequestChild::SpeechSynthesisRequestChild(SpeechTaskChild* aTask)
+ : mTask(aTask) {
+ mTask->mActor = this;
+ MOZ_COUNT_CTOR(SpeechSynthesisRequestChild);
+}
+
+SpeechSynthesisRequestChild::~SpeechSynthesisRequestChild() {
+ MOZ_COUNT_DTOR(SpeechSynthesisRequestChild);
+}
+
+mozilla::ipc::IPCResult SpeechSynthesisRequestChild::RecvOnStart(
+ const nsAString& aUri) {
+ mTask->DispatchStartImpl(aUri);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult SpeechSynthesisRequestChild::RecvOnEnd(
+ const bool& aIsError, const float& aElapsedTime,
+ const uint32_t& aCharIndex) {
+ SpeechSynthesisRequestChild* actor = mTask->mActor;
+ mTask->mActor = nullptr;
+
+ if (aIsError) {
+ mTask->DispatchErrorImpl(aElapsedTime, aCharIndex);
+ } else {
+ mTask->DispatchEndImpl(aElapsedTime, aCharIndex);
+ }
+
+ SpeechSynthesisRequestChild::Send__delete__(actor);
+
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult SpeechSynthesisRequestChild::RecvOnPause(
+ const float& aElapsedTime, const uint32_t& aCharIndex) {
+ mTask->DispatchPauseImpl(aElapsedTime, aCharIndex);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult SpeechSynthesisRequestChild::RecvOnResume(
+ const float& aElapsedTime, const uint32_t& aCharIndex) {
+ mTask->DispatchResumeImpl(aElapsedTime, aCharIndex);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult SpeechSynthesisRequestChild::RecvOnBoundary(
+ const nsAString& aName, const float& aElapsedTime,
+ const uint32_t& aCharIndex, const uint32_t& aCharLength,
+ const uint8_t& argc) {
+ mTask->DispatchBoundaryImpl(aName, aElapsedTime, aCharIndex, aCharLength,
+ argc);
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult SpeechSynthesisRequestChild::RecvOnMark(
+ const nsAString& aName, const float& aElapsedTime,
+ const uint32_t& aCharIndex) {
+ mTask->DispatchMarkImpl(aName, aElapsedTime, aCharIndex);
+ return IPC_OK();
+}
+
+// SpeechTaskChild
+
+SpeechTaskChild::SpeechTaskChild(SpeechSynthesisUtterance* aUtterance,
+ bool aShouldResistFingerprinting)
+ : nsSpeechTask(aUtterance, aShouldResistFingerprinting), mActor(nullptr) {}
+
+NS_IMETHODIMP
+SpeechTaskChild::Setup(nsISpeechTaskCallback* aCallback) {
+ MOZ_CRASH("Should never be called from child");
+}
+
+void SpeechTaskChild::Pause() {
+ MOZ_ASSERT(mActor);
+ mActor->SendPause();
+}
+
+void SpeechTaskChild::Resume() {
+ MOZ_ASSERT(mActor);
+ mActor->SendResume();
+}
+
+void SpeechTaskChild::Cancel() {
+ MOZ_ASSERT(mActor);
+ mActor->SendCancel();
+}
+
+void SpeechTaskChild::ForceEnd() {
+ MOZ_ASSERT(mActor);
+ mActor->SendForceEnd();
+}
+
+void SpeechTaskChild::SetAudioOutputVolume(float aVolume) {
+ if (mActor) {
+ mActor->SendSetAudioOutputVolume(aVolume);
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webspeech/synth/ipc/SpeechSynthesisChild.h b/dom/media/webspeech/synth/ipc/SpeechSynthesisChild.h
new file mode 100644
index 0000000000..f57582932a
--- /dev/null
+++ b/dom/media/webspeech/synth/ipc/SpeechSynthesisChild.h
@@ -0,0 +1,107 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_SpeechSynthesisChild_h
+#define mozilla_dom_SpeechSynthesisChild_h
+
+#include "mozilla/Attributes.h"
+#include "mozilla/dom/PSpeechSynthesisChild.h"
+#include "mozilla/dom/PSpeechSynthesisRequestChild.h"
+#include "nsSpeechTask.h"
+
+namespace mozilla::dom {
+
+class nsSynthVoiceRegistry;
+class SpeechSynthesisRequestChild;
+class SpeechTaskChild;
+
+class SpeechSynthesisChild : public PSpeechSynthesisChild {
+ friend class nsSynthVoiceRegistry;
+ friend class PSpeechSynthesisChild;
+
+ public:
+ mozilla::ipc::IPCResult RecvInitialVoicesAndState(
+ nsTArray<RemoteVoice>&& aVoices, nsTArray<nsString>&& aDefaults,
+ const bool& aIsSpeaking);
+
+ mozilla::ipc::IPCResult RecvVoiceAdded(const RemoteVoice& aVoice);
+
+ mozilla::ipc::IPCResult RecvVoiceRemoved(const nsAString& aUri);
+
+ mozilla::ipc::IPCResult RecvSetDefaultVoice(const nsAString& aUri,
+ const bool& aIsDefault);
+
+ mozilla::ipc::IPCResult RecvIsSpeakingChanged(const bool& aIsSpeaking);
+
+ mozilla::ipc::IPCResult RecvNotifyVoicesChanged();
+
+ protected:
+ SpeechSynthesisChild();
+ virtual ~SpeechSynthesisChild();
+
+ PSpeechSynthesisRequestChild* AllocPSpeechSynthesisRequestChild(
+ const nsAString& aLang, const nsAString& aUri, const nsAString& aText,
+ const float& aVolume, const float& aPitch, const float& aRate,
+ const bool& aShouldResistFingerprinting);
+ bool DeallocPSpeechSynthesisRequestChild(
+ PSpeechSynthesisRequestChild* aActor);
+};
+
+class SpeechSynthesisRequestChild : public PSpeechSynthesisRequestChild {
+ public:
+ explicit SpeechSynthesisRequestChild(SpeechTaskChild* aTask);
+ virtual ~SpeechSynthesisRequestChild();
+
+ protected:
+ mozilla::ipc::IPCResult RecvOnStart(const nsAString& aUri) override;
+
+ mozilla::ipc::IPCResult RecvOnEnd(const bool& aIsError,
+ const float& aElapsedTime,
+ const uint32_t& aCharIndex) override;
+
+ mozilla::ipc::IPCResult RecvOnPause(const float& aElapsedTime,
+ const uint32_t& aCharIndex) override;
+
+ mozilla::ipc::IPCResult RecvOnResume(const float& aElapsedTime,
+ const uint32_t& aCharIndex) override;
+
+ mozilla::ipc::IPCResult RecvOnBoundary(const nsAString& aName,
+ const float& aElapsedTime,
+ const uint32_t& aCharIndex,
+ const uint32_t& aCharLength,
+ const uint8_t& argc) override;
+
+ mozilla::ipc::IPCResult RecvOnMark(const nsAString& aName,
+ const float& aElapsedTime,
+ const uint32_t& aCharIndex) override;
+
+ RefPtr<SpeechTaskChild> mTask;
+};
+
+class SpeechTaskChild : public nsSpeechTask {
+ friend class SpeechSynthesisRequestChild;
+
+ public:
+ explicit SpeechTaskChild(SpeechSynthesisUtterance* aUtterance,
+ bool aShouldResistFingerprinting);
+
+ NS_IMETHOD Setup(nsISpeechTaskCallback* aCallback) override;
+
+ void Pause() override;
+
+ void Resume() override;
+
+ void Cancel() override;
+
+ void ForceEnd() override;
+
+ void SetAudioOutputVolume(float aVolume) override;
+
+ private:
+ SpeechSynthesisRequestChild* mActor;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webspeech/synth/ipc/SpeechSynthesisParent.cpp b/dom/media/webspeech/synth/ipc/SpeechSynthesisParent.cpp
new file mode 100644
index 0000000000..a9eb53c5b7
--- /dev/null
+++ b/dom/media/webspeech/synth/ipc/SpeechSynthesisParent.cpp
@@ -0,0 +1,221 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SpeechSynthesisParent.h"
+#include "nsSynthVoiceRegistry.h"
+
+namespace mozilla::dom {
+
+SpeechSynthesisParent::SpeechSynthesisParent() {
+ MOZ_COUNT_CTOR(SpeechSynthesisParent);
+}
+
+SpeechSynthesisParent::~SpeechSynthesisParent() {
+ MOZ_COUNT_DTOR(SpeechSynthesisParent);
+}
+
+void SpeechSynthesisParent::ActorDestroy(ActorDestroyReason aWhy) {
+ // Implement me! Bug 1005141
+}
+
+bool SpeechSynthesisParent::SendInit() {
+ return nsSynthVoiceRegistry::GetInstance()->SendInitialVoicesAndState(this);
+}
+
+PSpeechSynthesisRequestParent*
+SpeechSynthesisParent::AllocPSpeechSynthesisRequestParent(
+ const nsAString& aText, const nsAString& aLang, const nsAString& aUri,
+ const float& aVolume, const float& aRate, const float& aPitch,
+ const bool& aShouldResistFingerprinting) {
+ RefPtr<SpeechTaskParent> task =
+ new SpeechTaskParent(aVolume, aText, aShouldResistFingerprinting);
+ SpeechSynthesisRequestParent* actor = new SpeechSynthesisRequestParent(task);
+ return actor;
+}
+
+bool SpeechSynthesisParent::DeallocPSpeechSynthesisRequestParent(
+ PSpeechSynthesisRequestParent* aActor) {
+ delete aActor;
+ return true;
+}
+
+mozilla::ipc::IPCResult
+SpeechSynthesisParent::RecvPSpeechSynthesisRequestConstructor(
+ PSpeechSynthesisRequestParent* aActor, const nsAString& aText,
+ const nsAString& aLang, const nsAString& aUri, const float& aVolume,
+ const float& aRate, const float& aPitch,
+ const bool& aShouldResistFingerprinting) {
+ MOZ_ASSERT(aActor);
+ SpeechSynthesisRequestParent* actor =
+ static_cast<SpeechSynthesisRequestParent*>(aActor);
+ nsSynthVoiceRegistry::GetInstance()->Speak(aText, aLang, aUri, aVolume, aRate,
+ aPitch, actor->mTask);
+ return IPC_OK();
+}
+
+// SpeechSynthesisRequestParent
+
+SpeechSynthesisRequestParent::SpeechSynthesisRequestParent(
+ SpeechTaskParent* aTask)
+ : mTask(aTask) {
+ mTask->mActor = this;
+ MOZ_COUNT_CTOR(SpeechSynthesisRequestParent);
+}
+
+SpeechSynthesisRequestParent::~SpeechSynthesisRequestParent() {
+ if (mTask) {
+ mTask->mActor = nullptr;
+ // If we still have a task, cancel it.
+ mTask->Cancel();
+ }
+ MOZ_COUNT_DTOR(SpeechSynthesisRequestParent);
+}
+
+void SpeechSynthesisRequestParent::ActorDestroy(ActorDestroyReason aWhy) {
+ // Implement me! Bug 1005141
+}
+
+mozilla::ipc::IPCResult SpeechSynthesisRequestParent::RecvPause() {
+ MOZ_ASSERT(mTask);
+ mTask->Pause();
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult SpeechSynthesisRequestParent::Recv__delete__() {
+ MOZ_ASSERT(mTask);
+ mTask->mActor = nullptr;
+ mTask = nullptr;
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult SpeechSynthesisRequestParent::RecvResume() {
+ MOZ_ASSERT(mTask);
+ mTask->Resume();
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult SpeechSynthesisRequestParent::RecvCancel() {
+ MOZ_ASSERT(mTask);
+ mTask->Cancel();
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult SpeechSynthesisRequestParent::RecvForceEnd() {
+ MOZ_ASSERT(mTask);
+ mTask->ForceEnd();
+ return IPC_OK();
+}
+
+mozilla::ipc::IPCResult SpeechSynthesisRequestParent::RecvSetAudioOutputVolume(
+ const float& aVolume) {
+ MOZ_ASSERT(mTask);
+ mTask->SetAudioOutputVolume(aVolume);
+ return IPC_OK();
+}
+
+// SpeechTaskParent
+
+nsresult SpeechTaskParent::DispatchStartImpl(const nsAString& aUri) {
+ if (!mActor) {
+ // Child is already gone.
+ return NS_OK;
+ }
+
+ if (NS_WARN_IF(!(mActor->SendOnStart(aUri)))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+nsresult SpeechTaskParent::DispatchEndImpl(float aElapsedTime,
+ uint32_t aCharIndex) {
+ if (!mActor) {
+ // Child is already gone.
+ return NS_OK;
+ }
+
+ if (NS_WARN_IF(!(mActor->SendOnEnd(false, aElapsedTime, aCharIndex)))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+nsresult SpeechTaskParent::DispatchPauseImpl(float aElapsedTime,
+ uint32_t aCharIndex) {
+ if (!mActor) {
+ // Child is already gone.
+ return NS_OK;
+ }
+
+ if (NS_WARN_IF(!(mActor->SendOnPause(aElapsedTime, aCharIndex)))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+nsresult SpeechTaskParent::DispatchResumeImpl(float aElapsedTime,
+ uint32_t aCharIndex) {
+ if (!mActor) {
+ // Child is already gone.
+ return NS_OK;
+ }
+
+ if (NS_WARN_IF(!(mActor->SendOnResume(aElapsedTime, aCharIndex)))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+nsresult SpeechTaskParent::DispatchErrorImpl(float aElapsedTime,
+ uint32_t aCharIndex) {
+ if (!mActor) {
+ // Child is already gone.
+ return NS_OK;
+ }
+
+ if (NS_WARN_IF(!(mActor->SendOnEnd(true, aElapsedTime, aCharIndex)))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+nsresult SpeechTaskParent::DispatchBoundaryImpl(const nsAString& aName,
+ float aElapsedTime,
+ uint32_t aCharIndex,
+ uint32_t aCharLength,
+ uint8_t argc) {
+ if (!mActor) {
+ // Child is already gone.
+ return NS_OK;
+ }
+
+ if (NS_WARN_IF(!(mActor->SendOnBoundary(aName, aElapsedTime, aCharIndex,
+ aCharLength, argc)))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+nsresult SpeechTaskParent::DispatchMarkImpl(const nsAString& aName,
+ float aElapsedTime,
+ uint32_t aCharIndex) {
+ if (!mActor) {
+ // Child is already gone.
+ return NS_OK;
+ }
+
+ if (NS_WARN_IF(!(mActor->SendOnMark(aName, aElapsedTime, aCharIndex)))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webspeech/synth/ipc/SpeechSynthesisParent.h b/dom/media/webspeech/synth/ipc/SpeechSynthesisParent.h
new file mode 100644
index 0000000000..6ae4d38bbc
--- /dev/null
+++ b/dom/media/webspeech/synth/ipc/SpeechSynthesisParent.h
@@ -0,0 +1,102 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_SpeechSynthesisParent_h
+#define mozilla_dom_SpeechSynthesisParent_h
+
+#include "mozilla/dom/PSpeechSynthesisParent.h"
+#include "mozilla/dom/PSpeechSynthesisRequestParent.h"
+#include "nsSpeechTask.h"
+
+namespace mozilla::dom {
+
+class ContentParent;
+class SpeechTaskParent;
+class SpeechSynthesisRequestParent;
+
+class SpeechSynthesisParent : public PSpeechSynthesisParent {
+ friend class ContentParent;
+ friend class SpeechSynthesisRequestParent;
+ friend class PSpeechSynthesisParent;
+
+ public:
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ bool SendInit();
+
+ protected:
+ SpeechSynthesisParent();
+ virtual ~SpeechSynthesisParent();
+ PSpeechSynthesisRequestParent* AllocPSpeechSynthesisRequestParent(
+ const nsAString& aText, const nsAString& aLang, const nsAString& aUri,
+ const float& aVolume, const float& aRate, const float& aPitch,
+ const bool& aShouldResistFingerprinting);
+
+ bool DeallocPSpeechSynthesisRequestParent(
+ PSpeechSynthesisRequestParent* aActor);
+
+ mozilla::ipc::IPCResult RecvPSpeechSynthesisRequestConstructor(
+ PSpeechSynthesisRequestParent* aActor, const nsAString& aText,
+ const nsAString& aLang, const nsAString& aUri, const float& aVolume,
+ const float& aRate, const float& aPitch,
+ const bool& aShouldResistFingerprinting) override;
+};
+
+class SpeechSynthesisRequestParent : public PSpeechSynthesisRequestParent {
+ public:
+ explicit SpeechSynthesisRequestParent(SpeechTaskParent* aTask);
+ virtual ~SpeechSynthesisRequestParent();
+
+ RefPtr<SpeechTaskParent> mTask;
+
+ protected:
+ void ActorDestroy(ActorDestroyReason aWhy) override;
+
+ mozilla::ipc::IPCResult RecvPause() override;
+
+ mozilla::ipc::IPCResult RecvResume() override;
+
+ mozilla::ipc::IPCResult RecvCancel() override;
+
+ mozilla::ipc::IPCResult RecvForceEnd() override;
+
+ mozilla::ipc::IPCResult RecvSetAudioOutputVolume(
+ const float& aVolume) override;
+
+ mozilla::ipc::IPCResult Recv__delete__() override;
+};
+
+class SpeechTaskParent : public nsSpeechTask {
+ friend class SpeechSynthesisRequestParent;
+
+ public:
+ SpeechTaskParent(float aVolume, const nsAString& aUtterance,
+ bool aShouldResistFingerprinting)
+ : nsSpeechTask(aVolume, aUtterance, aShouldResistFingerprinting),
+ mActor(nullptr) {}
+
+ nsresult DispatchStartImpl(const nsAString& aUri) override;
+
+ nsresult DispatchEndImpl(float aElapsedTime, uint32_t aCharIndex) override;
+
+ nsresult DispatchPauseImpl(float aElapsedTime, uint32_t aCharIndex) override;
+
+ nsresult DispatchResumeImpl(float aElapsedTime, uint32_t aCharIndex) override;
+
+ nsresult DispatchErrorImpl(float aElapsedTime, uint32_t aCharIndex) override;
+
+ nsresult DispatchBoundaryImpl(const nsAString& aName, float aElapsedTime,
+ uint32_t aCharIndex, uint32_t aCharLength,
+ uint8_t argc) override;
+
+ nsresult DispatchMarkImpl(const nsAString& aName, float aElapsedTime,
+ uint32_t aCharIndex) override;
+
+ private:
+ SpeechSynthesisRequestParent* mActor;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webspeech/synth/moz.build b/dom/media/webspeech/synth/moz.build
new file mode 100644
index 0000000000..dde668668a
--- /dev/null
+++ b/dom/media/webspeech/synth/moz.build
@@ -0,0 +1,65 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+if CONFIG["MOZ_WEBSPEECH"]:
+ MOCHITEST_MANIFESTS += [
+ "test/mochitest.ini",
+ "test/startup/mochitest.ini",
+ ]
+
+ XPIDL_MODULE = "dom_webspeechsynth"
+
+ XPIDL_SOURCES += ["nsISpeechService.idl", "nsISynthVoiceRegistry.idl"]
+
+ EXPORTS.mozilla.dom += [
+ "ipc/SpeechSynthesisChild.h",
+ "ipc/SpeechSynthesisParent.h",
+ "nsSpeechTask.h",
+ "nsSynthVoiceRegistry.h",
+ "SpeechSynthesis.h",
+ "SpeechSynthesisUtterance.h",
+ "SpeechSynthesisVoice.h",
+ ]
+
+ UNIFIED_SOURCES += [
+ "ipc/SpeechSynthesisChild.cpp",
+ "ipc/SpeechSynthesisParent.cpp",
+ "nsSpeechTask.cpp",
+ "nsSynthVoiceRegistry.cpp",
+ "SpeechSynthesis.cpp",
+ "SpeechSynthesisUtterance.cpp",
+ "SpeechSynthesisVoice.cpp",
+ ]
+
+ if CONFIG["MOZ_WEBSPEECH_TEST_BACKEND"]:
+ UNIFIED_SOURCES += ["test/nsFakeSynthServices.cpp"]
+
+ XPCOM_MANIFESTS += [
+ "test/components.conf",
+ ]
+
+ if CONFIG["MOZ_WIDGET_TOOLKIT"] == "windows":
+ DIRS += ["windows"]
+
+ if CONFIG["MOZ_WIDGET_TOOLKIT"] == "cocoa":
+ DIRS += ["cocoa"]
+
+ if CONFIG["MOZ_WIDGET_TOOLKIT"] == "android":
+ DIRS += ["android"]
+
+ if CONFIG["MOZ_SYNTH_SPEECHD"]:
+ DIRS += ["speechd"]
+
+ IPDL_SOURCES += [
+ "ipc/PSpeechSynthesis.ipdl",
+ "ipc/PSpeechSynthesisRequest.ipdl",
+ ]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+LOCAL_INCLUDES += [
+ "ipc",
+]
diff --git a/dom/media/webspeech/synth/nsISpeechService.idl b/dom/media/webspeech/synth/nsISpeechService.idl
new file mode 100644
index 0000000000..b69973b6d2
--- /dev/null
+++ b/dom/media/webspeech/synth/nsISpeechService.idl
@@ -0,0 +1,143 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+/**
+ * A callback is implemented by the service.
+ */
+[scriptable, uuid(c576de0c-8a3d-4570-be7e-9876d3e5bed2)]
+interface nsISpeechTaskCallback : nsISupports
+{
+ /**
+ * The user or application has paused the speech.
+ */
+ void onPause();
+
+ /**
+ * The user or application has resumed the speech.
+ */
+ void onResume();
+
+ /**
+ * The user or application has canceled the speech.
+ */
+ void onCancel();
+
+ /**
+ * The user or application has changed the volume of this speech.
+ */
+ void onVolumeChanged(in float aVolume);
+};
+
+
+/**
+ * A task is associated with a single utterance. It is provided by the browser
+ * to the service in the speak() method.
+ */
+[scriptable, builtinclass, uuid(ad59949c-2437-4b35-8eeb-d760caab75c5)]
+interface nsISpeechTask : nsISupports
+{
+ /**
+ * Prepare browser for speech.
+ *
+ * @param aCallback callback object for mid-speech operations.
+ */
+ void setup(in nsISpeechTaskCallback aCallback);
+
+ /**
+ * Dispatch start event.
+ */
+ void dispatchStart();
+
+ /**
+ * Dispatch end event.
+ *
+ * @param aElapsedTime time in seconds since speech has started.
+ * @param aCharIndex offset of spoken characters.
+ */
+ void dispatchEnd(in float aElapsedTime, in unsigned long aCharIndex);
+
+ /**
+ * Dispatch pause event.
+ *
+ * @param aElapsedTime time in seconds since speech has started.
+ * @param aCharIndex offset of spoken characters.
+ */
+ void dispatchPause(in float aElapsedTime, in unsigned long aCharIndex);
+
+ /**
+ * Dispatch resume event.
+ *
+ * @param aElapsedTime time in seconds since speech has started.
+ * @param aCharIndex offset of spoken characters.
+ */
+ void dispatchResume(in float aElapsedTime, in unsigned long aCharIndex);
+
+ /**
+ * Dispatch error event.
+ *
+ * @param aElapsedTime time in seconds since speech has started.
+ * @param aCharIndex offset of spoken characters.
+ */
+ void dispatchError(in float aElapsedTime, in unsigned long aCharIndex);
+
+ /**
+ * Dispatch boundary event.
+ *
+ * @param aName name of boundary, 'word' or 'sentence'
+ * @param aElapsedTime time in seconds since speech has started.
+ * @param aCharIndex offset of spoken characters.
+ * @param aCharLength length of text in boundary event to be spoken.
+ */
+ [optional_argc] void dispatchBoundary(in AString aName, in float aElapsedTime,
+ in unsigned long aCharIndex,
+ [optional] in unsigned long aCharLength);
+
+ /**
+ * Dispatch mark event.
+ *
+ * @param aName mark identifier.
+ * @param aElapsedTime time in seconds since speech has started.
+ * @param aCharIndex offset of spoken characters.
+ */
+ void dispatchMark(in AString aName, in float aElapsedTime, in unsigned long aCharIndex);
+};
+
+/**
+ * The main interface of a speech synthesis service.
+ *
+ * A service is responsible for outputting audio.
+ * The service dispatches events, starting with dispatchStart() and ending with
+ * dispatchEnd or dispatchError().
+ * A service must also respond with the currect actions and events in response
+ * to implemented callback methods.
+ */
+[scriptable, uuid(9b7d59db-88ff-43d0-b6ee-9f63d042d08f)]
+interface nsISpeechService : nsISupports
+{
+ /**
+ * Speak the given text using the voice identified byu the given uri. See
+ * W3C Speech API spec for information about pitch and rate.
+ * https://dvcs.w3.org/hg/speech-api/raw-file/tip/speechapi.html#utterance-attributes
+ *
+ * @param aText text to utter.
+ * @param aUri unique voice identifier.
+ * @param aVolume volume to speak voice in. Only relevant for indirect audio.
+ * @param aRate rate to speak voice in.
+ * @param aPitch pitch to speak voice in.
+ * @param aTask task instance for utterance, used for sending events or audio
+ * data back to browser.
+ */
+ void speak(in AString aText, in AString aUri,
+ in float aVolume, in float aRate, in float aPitch,
+ in nsISpeechTask aTask);
+};
+
+%{C++
+// This is the service category speech services could use to start up as
+// a component.
+#define NS_SPEECH_SYNTH_STARTED "speech-synth-started"
+%}
diff --git a/dom/media/webspeech/synth/nsISynthVoiceRegistry.idl b/dom/media/webspeech/synth/nsISynthVoiceRegistry.idl
new file mode 100644
index 0000000000..8dd3a0426c
--- /dev/null
+++ b/dom/media/webspeech/synth/nsISynthVoiceRegistry.idl
@@ -0,0 +1,77 @@
+/* -*- Mode: IDL; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsISpeechService;
+
+[scriptable, builtinclass, uuid(5d7a0b38-77e5-4ee5-897c-ce5db9b85d44)]
+interface nsISynthVoiceRegistry : nsISupports
+{
+ /**
+ * Register a speech synthesis voice.
+ *
+ * @param aService the service that provides this voice.
+ * @param aUri a unique identifier for this voice.
+ * @param aName human-readable name for this voice.
+ * @param aLang a BCP 47 language tag.
+ * @param aLocalService true if service does not require network.
+ * @param aQueuesUtterances true if voice only speaks one utterance at a time
+ */
+ void addVoice(in nsISpeechService aService, in AString aUri,
+ in AString aName, in AString aLang,
+ in boolean aLocalService, in boolean aQueuesUtterances);
+
+ /**
+ * Remove a speech synthesis voice.
+ *
+ * @param aService the service that was used to add the voice.
+ * @param aUri a unique identifier of an existing voice.
+ */
+ void removeVoice(in nsISpeechService aService, in AString aUri);
+
+ /**
+ * Notify content of voice availability changes. This allows content
+ * to be notified of voice catalog changes in real time.
+ */
+ void notifyVoicesChanged();
+
+ /**
+ * Set a voice as default.
+ *
+ * @param aUri a unique identifier of an existing voice.
+ * @param aIsDefault true if this voice should be toggled as default.
+ */
+ void setDefaultVoice(in AString aUri, in boolean aIsDefault);
+
+ readonly attribute uint32_t voiceCount;
+
+ AString getVoice(in uint32_t aIndex);
+
+ bool isDefaultVoice(in AString aUri);
+
+ bool isLocalVoice(in AString aUri);
+
+ AString getVoiceLang(in AString aUri);
+
+ AString getVoiceName(in AString aUri);
+};
+
+%{C++
+#define NS_SYNTHVOICEREGISTRY_CID \
+ { /* {7090524d-5574-4492-a77f-d8d558ced59d} */ \
+ 0x7090524d, \
+ 0x5574, \
+ 0x4492, \
+ { 0xa7, 0x7f, 0xd8, 0xd5, 0x58, 0xce, 0xd5, 0x9d } \
+ }
+
+#define NS_SYNTHVOICEREGISTRY_CONTRACTID \
+ "@mozilla.org/synth-voice-registry;1"
+
+#define NS_SYNTHVOICEREGISTRY_CLASSNAME \
+ "Speech Synthesis Voice Registry"
+
+%}
diff --git a/dom/media/webspeech/synth/nsSpeechTask.cpp b/dom/media/webspeech/synth/nsSpeechTask.cpp
new file mode 100644
index 0000000000..b102172466
--- /dev/null
+++ b/dom/media/webspeech/synth/nsSpeechTask.cpp
@@ -0,0 +1,389 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "AudioChannelService.h"
+#include "AudioSegment.h"
+#include "nsSpeechTask.h"
+#include "nsSynthVoiceRegistry.h"
+#include "nsXULAppAPI.h"
+#include "SharedBuffer.h"
+#include "SpeechSynthesis.h"
+
+#undef LOG
+extern mozilla::LogModule* GetSpeechSynthLog();
+#define LOG(type, msg) MOZ_LOG(GetSpeechSynthLog(), type, msg)
+
+#define AUDIO_TRACK 1
+
+namespace mozilla::dom {
+
+// nsSpeechTask
+
+NS_IMPL_CYCLE_COLLECTION_WEAK(nsSpeechTask, mSpeechSynthesis, mUtterance,
+ mCallback)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(nsSpeechTask)
+ NS_INTERFACE_MAP_ENTRY(nsISpeechTask)
+ NS_INTERFACE_MAP_ENTRY(nsIAudioChannelAgentCallback)
+ NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsISpeechTask)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(nsSpeechTask)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(nsSpeechTask)
+
+nsSpeechTask::nsSpeechTask(SpeechSynthesisUtterance* aUtterance,
+ bool aShouldResistFingerprinting)
+ : mUtterance(aUtterance),
+ mInited(false),
+ mPrePaused(false),
+ mPreCanceled(false),
+ mCallback(nullptr),
+ mShouldResistFingerprinting(aShouldResistFingerprinting),
+ mState(STATE_PENDING) {
+ mText = aUtterance->mText;
+ mVolume = aUtterance->Volume();
+}
+
+nsSpeechTask::nsSpeechTask(float aVolume, const nsAString& aText,
+ bool aShouldResistFingerprinting)
+ : mUtterance(nullptr),
+ mVolume(aVolume),
+ mText(aText),
+ mInited(false),
+ mPrePaused(false),
+ mPreCanceled(false),
+ mCallback(nullptr),
+ mShouldResistFingerprinting(aShouldResistFingerprinting),
+ mState(STATE_PENDING) {}
+
+nsSpeechTask::~nsSpeechTask() { LOG(LogLevel::Debug, ("~nsSpeechTask")); }
+
+void nsSpeechTask::Init() { mInited = true; }
+
+void nsSpeechTask::SetChosenVoiceURI(const nsAString& aUri) {
+ mChosenVoiceURI = aUri;
+}
+
+NS_IMETHODIMP
+nsSpeechTask::Setup(nsISpeechTaskCallback* aCallback) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ LOG(LogLevel::Debug, ("nsSpeechTask::Setup"));
+
+ mCallback = aCallback;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSpeechTask::DispatchStart() {
+ nsSynthVoiceRegistry::GetInstance()->SetIsSpeaking(true);
+ return DispatchStartImpl();
+}
+
+nsresult nsSpeechTask::DispatchStartImpl() {
+ return DispatchStartImpl(mChosenVoiceURI);
+}
+
+nsresult nsSpeechTask::DispatchStartImpl(const nsAString& aUri) {
+ LOG(LogLevel::Debug, ("nsSpeechTask::DispatchStartImpl"));
+
+ MOZ_ASSERT(mUtterance);
+ if (NS_WARN_IF(mState != STATE_PENDING)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ CreateAudioChannelAgent();
+
+ mState = STATE_SPEAKING;
+ mUtterance->mChosenVoiceURI = aUri;
+ mUtterance->DispatchSpeechSynthesisEvent(u"start"_ns, 0, nullptr, 0, u""_ns);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSpeechTask::DispatchEnd(float aElapsedTime, uint32_t aCharIndex) {
+ // After we end, no callback functions should go through.
+ mCallback = nullptr;
+
+ if (!mPreCanceled) {
+ nsSynthVoiceRegistry::GetInstance()->SpeakNext();
+ }
+
+ return DispatchEndImpl(aElapsedTime, aCharIndex);
+}
+
+nsresult nsSpeechTask::DispatchEndImpl(float aElapsedTime,
+ uint32_t aCharIndex) {
+ LOG(LogLevel::Debug, ("nsSpeechTask::DispatchEndImpl"));
+
+ DestroyAudioChannelAgent();
+
+ MOZ_ASSERT(mUtterance);
+ if (NS_WARN_IF(mState == STATE_ENDED)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ RefPtr<SpeechSynthesisUtterance> utterance = mUtterance;
+
+ if (mSpeechSynthesis) {
+ mSpeechSynthesis->OnEnd(this);
+ }
+
+ mState = STATE_ENDED;
+ utterance->DispatchSpeechSynthesisEvent(u"end"_ns, aCharIndex, nullptr,
+ aElapsedTime, u""_ns);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSpeechTask::DispatchPause(float aElapsedTime, uint32_t aCharIndex) {
+ return DispatchPauseImpl(aElapsedTime, aCharIndex);
+}
+
+nsresult nsSpeechTask::DispatchPauseImpl(float aElapsedTime,
+ uint32_t aCharIndex) {
+ LOG(LogLevel::Debug, ("nsSpeechTask::DispatchPauseImpl"));
+ MOZ_ASSERT(mUtterance);
+ if (NS_WARN_IF(mUtterance->mPaused)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ if (NS_WARN_IF(mState == STATE_ENDED)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ mUtterance->mPaused = true;
+ if (mState == STATE_SPEAKING) {
+ mUtterance->DispatchSpeechSynthesisEvent(u"pause"_ns, aCharIndex, nullptr,
+ aElapsedTime, u""_ns);
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSpeechTask::DispatchResume(float aElapsedTime, uint32_t aCharIndex) {
+ return DispatchResumeImpl(aElapsedTime, aCharIndex);
+}
+
+nsresult nsSpeechTask::DispatchResumeImpl(float aElapsedTime,
+ uint32_t aCharIndex) {
+ LOG(LogLevel::Debug, ("nsSpeechTask::DispatchResumeImpl"));
+ MOZ_ASSERT(mUtterance);
+ if (NS_WARN_IF(!(mUtterance->mPaused))) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ if (NS_WARN_IF(mState == STATE_ENDED)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ mUtterance->mPaused = false;
+ if (mState == STATE_SPEAKING) {
+ mUtterance->DispatchSpeechSynthesisEvent(u"resume"_ns, aCharIndex, nullptr,
+ aElapsedTime, u""_ns);
+ }
+
+ return NS_OK;
+}
+
+void nsSpeechTask::ForceError(float aElapsedTime, uint32_t aCharIndex) {
+ DispatchError(aElapsedTime, aCharIndex);
+}
+
+NS_IMETHODIMP
+nsSpeechTask::DispatchError(float aElapsedTime, uint32_t aCharIndex) {
+ if (!mPreCanceled) {
+ nsSynthVoiceRegistry::GetInstance()->SpeakNext();
+ }
+
+ return DispatchErrorImpl(aElapsedTime, aCharIndex);
+}
+
+nsresult nsSpeechTask::DispatchErrorImpl(float aElapsedTime,
+ uint32_t aCharIndex) {
+ LOG(LogLevel::Debug, ("nsSpeechTask::DispatchErrorImpl"));
+
+ DestroyAudioChannelAgent();
+
+ MOZ_ASSERT(mUtterance);
+ if (NS_WARN_IF(mState == STATE_ENDED)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ if (mSpeechSynthesis) {
+ mSpeechSynthesis->OnEnd(this);
+ }
+
+ mState = STATE_ENDED;
+ mUtterance->DispatchSpeechSynthesisEvent(u"error"_ns, aCharIndex, nullptr,
+ aElapsedTime, u""_ns);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSpeechTask::DispatchBoundary(const nsAString& aName, float aElapsedTime,
+ uint32_t aCharIndex, uint32_t aCharLength,
+ uint8_t argc) {
+ return DispatchBoundaryImpl(aName, aElapsedTime, aCharIndex, aCharLength,
+ argc);
+}
+
+nsresult nsSpeechTask::DispatchBoundaryImpl(const nsAString& aName,
+ float aElapsedTime,
+ uint32_t aCharIndex,
+ uint32_t aCharLength,
+ uint8_t argc) {
+ MOZ_ASSERT(mUtterance);
+ if (NS_WARN_IF(mState != STATE_SPEAKING)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ mUtterance->DispatchSpeechSynthesisEvent(
+ u"boundary"_ns, aCharIndex,
+ argc ? static_cast<Nullable<uint32_t> >(aCharLength) : nullptr,
+ aElapsedTime, aName);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSpeechTask::DispatchMark(const nsAString& aName, float aElapsedTime,
+ uint32_t aCharIndex) {
+ return DispatchMarkImpl(aName, aElapsedTime, aCharIndex);
+}
+
+nsresult nsSpeechTask::DispatchMarkImpl(const nsAString& aName,
+ float aElapsedTime,
+ uint32_t aCharIndex) {
+ MOZ_ASSERT(mUtterance);
+ if (NS_WARN_IF(mState != STATE_SPEAKING)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ mUtterance->DispatchSpeechSynthesisEvent(u"mark"_ns, aCharIndex, nullptr,
+ aElapsedTime, aName);
+ return NS_OK;
+}
+
+void nsSpeechTask::Pause() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ if (mCallback) {
+ DebugOnly<nsresult> rv = mCallback->OnPause();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Unable to call onPause() callback");
+ }
+
+ if (!mInited) {
+ mPrePaused = true;
+ }
+}
+
+void nsSpeechTask::Resume() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ if (mCallback) {
+ DebugOnly<nsresult> rv = mCallback->OnResume();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "Unable to call onResume() callback");
+ }
+
+ if (mPrePaused) {
+ mPrePaused = false;
+ nsSynthVoiceRegistry::GetInstance()->ResumeQueue();
+ }
+}
+
+void nsSpeechTask::Cancel() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ LOG(LogLevel::Debug, ("nsSpeechTask::Cancel"));
+
+ if (mCallback) {
+ DebugOnly<nsresult> rv = mCallback->OnCancel();
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
+ "Unable to call onCancel() callback");
+ }
+
+ if (!mInited) {
+ mPreCanceled = true;
+ }
+}
+
+void nsSpeechTask::ForceEnd() {
+ if (!mInited) {
+ mPreCanceled = true;
+ }
+
+ DispatchEnd(0, 0);
+}
+
+void nsSpeechTask::SetSpeechSynthesis(SpeechSynthesis* aSpeechSynthesis) {
+ mSpeechSynthesis = aSpeechSynthesis;
+}
+
+void nsSpeechTask::CreateAudioChannelAgent() {
+ if (!mUtterance) {
+ return;
+ }
+
+ if (mAudioChannelAgent) {
+ mAudioChannelAgent->NotifyStoppedPlaying();
+ }
+
+ mAudioChannelAgent = new AudioChannelAgent();
+ mAudioChannelAgent->InitWithWeakCallback(mUtterance->GetOwner(), this);
+
+ nsresult rv = mAudioChannelAgent->NotifyStartedPlaying(
+ AudioChannelService::AudibleState::eAudible);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return;
+ }
+
+ mAudioChannelAgent->PullInitialUpdate();
+}
+
+void nsSpeechTask::DestroyAudioChannelAgent() {
+ if (mAudioChannelAgent) {
+ mAudioChannelAgent->NotifyStoppedPlaying();
+ mAudioChannelAgent = nullptr;
+ }
+}
+
+NS_IMETHODIMP
+nsSpeechTask::WindowVolumeChanged(float aVolume, bool aMuted) {
+ SetAudioOutputVolume(aMuted ? 0.0 : mVolume * aVolume);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSpeechTask::WindowSuspendChanged(nsSuspendedTypes aSuspend) {
+ if (!mUtterance) {
+ return NS_OK;
+ }
+
+ if (aSuspend == nsISuspendedTypes::NONE_SUSPENDED && mUtterance->mPaused) {
+ Resume();
+ } else if (aSuspend != nsISuspendedTypes::NONE_SUSPENDED &&
+ !mUtterance->mPaused) {
+ Pause();
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSpeechTask::WindowAudioCaptureChanged(bool aCapture) {
+ // This is not supported yet.
+ return NS_OK;
+}
+
+void nsSpeechTask::SetAudioOutputVolume(float aVolume) {
+ if (mCallback) {
+ mCallback->OnVolumeChanged(aVolume);
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webspeech/synth/nsSpeechTask.h b/dom/media/webspeech/synth/nsSpeechTask.h
new file mode 100644
index 0000000000..fc121cf8f1
--- /dev/null
+++ b/dom/media/webspeech/synth/nsSpeechTask.h
@@ -0,0 +1,128 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_nsSpeechTask_h
+#define mozilla_dom_nsSpeechTask_h
+
+#include "SpeechSynthesisUtterance.h"
+#include "AudioChannelAgent.h"
+#include "nsISpeechService.h"
+#include "nsWeakReference.h"
+
+namespace mozilla {
+
+class SharedBuffer;
+
+namespace dom {
+
+class SpeechSynthesisUtterance;
+class SpeechSynthesis;
+
+class nsSpeechTask : public nsISpeechTask,
+ public nsIAudioChannelAgentCallback,
+ public nsSupportsWeakReference {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(nsSpeechTask, nsISpeechTask)
+
+ NS_DECL_NSISPEECHTASK
+ NS_DECL_NSIAUDIOCHANNELAGENTCALLBACK
+
+ explicit nsSpeechTask(SpeechSynthesisUtterance* aUtterance,
+ bool aShouldResistFingerprinting);
+ nsSpeechTask(float aVolume, const nsAString& aText,
+ bool aShouldResistFingerprinting);
+
+ virtual void Pause();
+
+ virtual void Resume();
+
+ virtual void Cancel();
+
+ virtual void ForceEnd();
+
+ void SetSpeechSynthesis(SpeechSynthesis* aSpeechSynthesis);
+
+ void Init();
+
+ void SetChosenVoiceURI(const nsAString& aUri);
+
+ virtual void SetAudioOutputVolume(float aVolume);
+
+ void ForceError(float aElapsedTime, uint32_t aCharIndex);
+
+ bool IsPreCanceled() { return mPreCanceled; };
+
+ bool IsPrePaused() { return mPrePaused; }
+
+ bool ShouldResistFingerprinting() { return mShouldResistFingerprinting; }
+
+ enum { STATE_PENDING, STATE_SPEAKING, STATE_ENDED };
+
+ uint32_t GetState() const { return mState; }
+
+ bool IsSpeaking() const { return mState == STATE_SPEAKING; }
+
+ bool IsPending() const { return mState == STATE_PENDING; }
+
+ protected:
+ virtual ~nsSpeechTask();
+
+ nsresult DispatchStartImpl();
+
+ virtual nsresult DispatchStartImpl(const nsAString& aUri);
+
+ virtual nsresult DispatchEndImpl(float aElapsedTime, uint32_t aCharIndex);
+
+ virtual nsresult DispatchPauseImpl(float aElapsedTime, uint32_t aCharIndex);
+
+ virtual nsresult DispatchResumeImpl(float aElapsedTime, uint32_t aCharIndex);
+
+ virtual nsresult DispatchErrorImpl(float aElapsedTime, uint32_t aCharIndex);
+
+ virtual nsresult DispatchBoundaryImpl(const nsAString& aName,
+ float aElapsedTime, uint32_t aCharIndex,
+ uint32_t aCharLength, uint8_t argc);
+
+ virtual nsresult DispatchMarkImpl(const nsAString& aName, float aElapsedTime,
+ uint32_t aCharIndex);
+
+ RefPtr<SpeechSynthesisUtterance> mUtterance;
+
+ float mVolume;
+
+ nsString mText;
+
+ bool mInited;
+
+ bool mPrePaused;
+
+ bool mPreCanceled;
+
+ private:
+ void End();
+
+ void CreateAudioChannelAgent();
+
+ void DestroyAudioChannelAgent();
+
+ nsCOMPtr<nsISpeechTaskCallback> mCallback;
+
+ RefPtr<mozilla::dom::AudioChannelAgent> mAudioChannelAgent;
+
+ RefPtr<SpeechSynthesis> mSpeechSynthesis;
+
+ nsString mChosenVoiceURI;
+
+ bool mShouldResistFingerprinting;
+
+ uint32_t mState;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/webspeech/synth/nsSynthVoiceRegistry.cpp b/dom/media/webspeech/synth/nsSynthVoiceRegistry.cpp
new file mode 100644
index 0000000000..d289c81655
--- /dev/null
+++ b/dom/media/webspeech/synth/nsSynthVoiceRegistry.cpp
@@ -0,0 +1,762 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISpeechService.h"
+#include "nsServiceManagerUtils.h"
+#include "nsCategoryManagerUtils.h"
+
+#include "SpeechSynthesisUtterance.h"
+#include "SpeechSynthesisVoice.h"
+#include "nsContentUtils.h"
+#include "nsSynthVoiceRegistry.h"
+#include "nsSpeechTask.h"
+#include "AudioChannelService.h"
+
+#include "nsString.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/intl/LocaleService.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/Unused.h"
+
+#include "SpeechSynthesisChild.h"
+#include "SpeechSynthesisParent.h"
+
+using mozilla::intl::LocaleService;
+
+#undef LOG
+extern mozilla::LogModule* GetSpeechSynthLog();
+#define LOG(type, msg) MOZ_LOG(GetSpeechSynthLog(), type, msg)
+
+namespace {
+
+void GetAllSpeechSynthActors(
+ nsTArray<mozilla::dom::SpeechSynthesisParent*>& aActors) {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aActors.IsEmpty());
+
+ AutoTArray<mozilla::dom::ContentParent*, 20> contentActors;
+ mozilla::dom::ContentParent::GetAll(contentActors);
+
+ for (uint32_t contentIndex = 0; contentIndex < contentActors.Length();
+ ++contentIndex) {
+ MOZ_ASSERT(contentActors[contentIndex]);
+
+ AutoTArray<mozilla::dom::PSpeechSynthesisParent*, 5> speechsynthActors;
+ contentActors[contentIndex]->ManagedPSpeechSynthesisParent(
+ speechsynthActors);
+
+ for (uint32_t speechsynthIndex = 0;
+ speechsynthIndex < speechsynthActors.Length(); ++speechsynthIndex) {
+ MOZ_ASSERT(speechsynthActors[speechsynthIndex]);
+
+ mozilla::dom::SpeechSynthesisParent* actor =
+ static_cast<mozilla::dom::SpeechSynthesisParent*>(
+ speechsynthActors[speechsynthIndex]);
+ aActors.AppendElement(actor);
+ }
+ }
+}
+
+} // namespace
+
+namespace mozilla::dom {
+
+// VoiceData
+
+class VoiceData final {
+ private:
+ // Private destructor, to discourage deletion outside of Release():
+ ~VoiceData() = default;
+
+ public:
+ VoiceData(nsISpeechService* aService, const nsAString& aUri,
+ const nsAString& aName, const nsAString& aLang, bool aIsLocal,
+ bool aQueuesUtterances)
+ : mService(aService),
+ mUri(aUri),
+ mName(aName),
+ mLang(aLang),
+ mIsLocal(aIsLocal),
+ mIsQueued(aQueuesUtterances) {}
+
+ NS_INLINE_DECL_REFCOUNTING(VoiceData)
+
+ nsCOMPtr<nsISpeechService> mService;
+
+ nsString mUri;
+
+ nsString mName;
+
+ nsString mLang;
+
+ bool mIsLocal;
+
+ bool mIsQueued;
+};
+
+// GlobalQueueItem
+
+class GlobalQueueItem final {
+ private:
+ // Private destructor, to discourage deletion outside of Release():
+ ~GlobalQueueItem() = default;
+
+ public:
+ GlobalQueueItem(VoiceData* aVoice, nsSpeechTask* aTask,
+ const nsAString& aText, const float& aVolume,
+ const float& aRate, const float& aPitch)
+ : mVoice(aVoice),
+ mTask(aTask),
+ mText(aText),
+ mVolume(aVolume),
+ mRate(aRate),
+ mPitch(aPitch),
+ mIsLocal(false) {}
+
+ NS_INLINE_DECL_REFCOUNTING(GlobalQueueItem)
+
+ RefPtr<VoiceData> mVoice;
+
+ RefPtr<nsSpeechTask> mTask;
+
+ nsString mText;
+
+ float mVolume;
+
+ float mRate;
+
+ float mPitch;
+
+ bool mIsLocal;
+};
+
+// nsSynthVoiceRegistry
+
+static StaticRefPtr<nsSynthVoiceRegistry> gSynthVoiceRegistry;
+
+NS_IMPL_ISUPPORTS(nsSynthVoiceRegistry, nsISynthVoiceRegistry)
+
+nsSynthVoiceRegistry::nsSynthVoiceRegistry()
+ : mSpeechSynthChild(nullptr), mUseGlobalQueue(false), mIsSpeaking(false) {
+ if (XRE_IsContentProcess()) {
+ mSpeechSynthChild = new SpeechSynthesisChild();
+ ContentChild::GetSingleton()->SendPSpeechSynthesisConstructor(
+ mSpeechSynthChild);
+ }
+}
+
+nsSynthVoiceRegistry::~nsSynthVoiceRegistry() {
+ LOG(LogLevel::Debug, ("~nsSynthVoiceRegistry"));
+
+ // mSpeechSynthChild's lifecycle is managed by the Content protocol.
+ mSpeechSynthChild = nullptr;
+
+ mUriVoiceMap.Clear();
+}
+
+nsSynthVoiceRegistry* nsSynthVoiceRegistry::GetInstance() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!gSynthVoiceRegistry) {
+ gSynthVoiceRegistry = new nsSynthVoiceRegistry();
+ ClearOnShutdown(&gSynthVoiceRegistry);
+ if (XRE_IsParentProcess()) {
+ // Start up all speech synth services.
+ NS_CreateServicesFromCategory(NS_SPEECH_SYNTH_STARTED, nullptr,
+ NS_SPEECH_SYNTH_STARTED);
+ }
+ }
+
+ return gSynthVoiceRegistry;
+}
+
+already_AddRefed<nsSynthVoiceRegistry>
+nsSynthVoiceRegistry::GetInstanceForService() {
+ RefPtr<nsSynthVoiceRegistry> registry = GetInstance();
+
+ return registry.forget();
+}
+
+bool nsSynthVoiceRegistry::SendInitialVoicesAndState(
+ SpeechSynthesisParent* aParent) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ nsTArray<RemoteVoice> voices;
+ nsTArray<nsString> defaults;
+
+ for (uint32_t i = 0; i < mVoices.Length(); ++i) {
+ RefPtr<VoiceData> voice = mVoices[i];
+
+ voices.AppendElement(RemoteVoice(voice->mUri, voice->mName, voice->mLang,
+ voice->mIsLocal, voice->mIsQueued));
+ }
+
+ for (uint32_t i = 0; i < mDefaultVoices.Length(); ++i) {
+ defaults.AppendElement(mDefaultVoices[i]->mUri);
+ }
+
+ return aParent->SendInitialVoicesAndState(voices, defaults, IsSpeaking());
+}
+
+void nsSynthVoiceRegistry::RecvInitialVoicesAndState(
+ const nsTArray<RemoteVoice>& aVoices, const nsTArray<nsString>& aDefaults,
+ const bool& aIsSpeaking) {
+ // We really should have a local instance since this is a directed response to
+ // an Init() call.
+ MOZ_ASSERT(gSynthVoiceRegistry);
+
+ for (uint32_t i = 0; i < aVoices.Length(); ++i) {
+ RemoteVoice voice = aVoices[i];
+ gSynthVoiceRegistry->AddVoiceImpl(nullptr, voice.voiceURI(), voice.name(),
+ voice.lang(), voice.localService(),
+ voice.queued());
+ }
+
+ for (uint32_t i = 0; i < aDefaults.Length(); ++i) {
+ gSynthVoiceRegistry->SetDefaultVoice(aDefaults[i], true);
+ }
+
+ gSynthVoiceRegistry->mIsSpeaking = aIsSpeaking;
+
+ if (aVoices.Length()) {
+ gSynthVoiceRegistry->NotifyVoicesChanged();
+ }
+}
+
+void nsSynthVoiceRegistry::RecvRemoveVoice(const nsAString& aUri) {
+ // If we dont have a local instance of the registry yet, we will recieve
+ // current voices at contruction time.
+ if (!gSynthVoiceRegistry) {
+ return;
+ }
+
+ gSynthVoiceRegistry->RemoveVoice(nullptr, aUri);
+}
+
+void nsSynthVoiceRegistry::RecvAddVoice(const RemoteVoice& aVoice) {
+ // If we dont have a local instance of the registry yet, we will recieve
+ // current voices at contruction time.
+ if (!gSynthVoiceRegistry) {
+ return;
+ }
+
+ gSynthVoiceRegistry->AddVoiceImpl(nullptr, aVoice.voiceURI(), aVoice.name(),
+ aVoice.lang(), aVoice.localService(),
+ aVoice.queued());
+}
+
+void nsSynthVoiceRegistry::RecvSetDefaultVoice(const nsAString& aUri,
+ bool aIsDefault) {
+ // If we dont have a local instance of the registry yet, we will recieve
+ // current voices at contruction time.
+ if (!gSynthVoiceRegistry) {
+ return;
+ }
+
+ gSynthVoiceRegistry->SetDefaultVoice(aUri, aIsDefault);
+}
+
+void nsSynthVoiceRegistry::RecvIsSpeakingChanged(bool aIsSpeaking) {
+ // If we dont have a local instance of the registry yet, we will get the
+ // speaking state on construction.
+ if (!gSynthVoiceRegistry) {
+ return;
+ }
+
+ gSynthVoiceRegistry->mIsSpeaking = aIsSpeaking;
+}
+
+void nsSynthVoiceRegistry::RecvNotifyVoicesChanged() {
+ // If we dont have a local instance of the registry yet, we don't care.
+ if (!gSynthVoiceRegistry) {
+ return;
+ }
+
+ gSynthVoiceRegistry->NotifyVoicesChanged();
+}
+
+NS_IMETHODIMP
+nsSynthVoiceRegistry::AddVoice(nsISpeechService* aService,
+ const nsAString& aUri, const nsAString& aName,
+ const nsAString& aLang, bool aLocalService,
+ bool aQueuesUtterances) {
+ LOG(LogLevel::Debug,
+ ("nsSynthVoiceRegistry::AddVoice uri='%s' name='%s' lang='%s' local=%s "
+ "queued=%s",
+ NS_ConvertUTF16toUTF8(aUri).get(), NS_ConvertUTF16toUTF8(aName).get(),
+ NS_ConvertUTF16toUTF8(aLang).get(), aLocalService ? "true" : "false",
+ aQueuesUtterances ? "true" : "false"));
+
+ if (NS_WARN_IF(XRE_IsContentProcess())) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ return AddVoiceImpl(aService, aUri, aName, aLang, aLocalService,
+ aQueuesUtterances);
+}
+
+NS_IMETHODIMP
+nsSynthVoiceRegistry::RemoveVoice(nsISpeechService* aService,
+ const nsAString& aUri) {
+ LOG(LogLevel::Debug, ("nsSynthVoiceRegistry::RemoveVoice uri='%s' (%s)",
+ NS_ConvertUTF16toUTF8(aUri).get(),
+ (XRE_IsContentProcess()) ? "child" : "parent"));
+
+ bool found = false;
+ VoiceData* retval = mUriVoiceMap.GetWeak(aUri, &found);
+
+ if (NS_WARN_IF(!(found))) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ if (NS_WARN_IF(!(aService == retval->mService))) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ mVoices.RemoveElement(retval);
+ mDefaultVoices.RemoveElement(retval);
+ mUriVoiceMap.Remove(aUri);
+
+ if (retval->mIsQueued &&
+ !StaticPrefs::media_webspeech_synth_force_global_queue()) {
+ // Check if this is the last queued voice, and disable the global queue if
+ // it is.
+ bool queued = false;
+ for (uint32_t i = 0; i < mVoices.Length(); i++) {
+ VoiceData* voice = mVoices[i];
+ if (voice->mIsQueued) {
+ queued = true;
+ break;
+ }
+ }
+ if (!queued) {
+ mUseGlobalQueue = false;
+ }
+ }
+
+ nsTArray<SpeechSynthesisParent*> ssplist;
+ GetAllSpeechSynthActors(ssplist);
+
+ for (uint32_t i = 0; i < ssplist.Length(); ++i)
+ Unused << ssplist[i]->SendVoiceRemoved(aUri);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSynthVoiceRegistry::NotifyVoicesChanged() {
+ if (XRE_IsParentProcess()) {
+ nsTArray<SpeechSynthesisParent*> ssplist;
+ GetAllSpeechSynthActors(ssplist);
+
+ for (uint32_t i = 0; i < ssplist.Length(); ++i)
+ Unused << ssplist[i]->SendNotifyVoicesChanged();
+ }
+
+ nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (NS_WARN_IF(!(obs))) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ obs->NotifyObservers(nullptr, "synth-voices-changed", nullptr);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSynthVoiceRegistry::SetDefaultVoice(const nsAString& aUri, bool aIsDefault) {
+ bool found = false;
+ VoiceData* retval = mUriVoiceMap.GetWeak(aUri, &found);
+ if (NS_WARN_IF(!(found))) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ mDefaultVoices.RemoveElement(retval);
+
+ LOG(LogLevel::Debug,
+ ("nsSynthVoiceRegistry::SetDefaultVoice %s %s",
+ NS_ConvertUTF16toUTF8(aUri).get(), aIsDefault ? "true" : "false"));
+
+ if (aIsDefault) {
+ mDefaultVoices.AppendElement(retval);
+ }
+
+ if (XRE_IsParentProcess()) {
+ nsTArray<SpeechSynthesisParent*> ssplist;
+ GetAllSpeechSynthActors(ssplist);
+
+ for (uint32_t i = 0; i < ssplist.Length(); ++i) {
+ Unused << ssplist[i]->SendSetDefaultVoice(aUri, aIsDefault);
+ }
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSynthVoiceRegistry::GetVoiceCount(uint32_t* aRetval) {
+ *aRetval = mVoices.Length();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSynthVoiceRegistry::GetVoice(uint32_t aIndex, nsAString& aRetval) {
+ if (NS_WARN_IF(!(aIndex < mVoices.Length()))) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ aRetval = mVoices[aIndex]->mUri;
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSynthVoiceRegistry::IsDefaultVoice(const nsAString& aUri, bool* aRetval) {
+ bool found;
+ VoiceData* voice = mUriVoiceMap.GetWeak(aUri, &found);
+ if (NS_WARN_IF(!(found))) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ for (int32_t i = mDefaultVoices.Length(); i > 0;) {
+ VoiceData* defaultVoice = mDefaultVoices[--i];
+
+ if (voice->mLang.Equals(defaultVoice->mLang)) {
+ *aRetval = voice == defaultVoice;
+ return NS_OK;
+ }
+ }
+
+ *aRetval = false;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSynthVoiceRegistry::IsLocalVoice(const nsAString& aUri, bool* aRetval) {
+ bool found;
+ VoiceData* voice = mUriVoiceMap.GetWeak(aUri, &found);
+ if (NS_WARN_IF(!(found))) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ *aRetval = voice->mIsLocal;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSynthVoiceRegistry::GetVoiceLang(const nsAString& aUri, nsAString& aRetval) {
+ bool found;
+ VoiceData* voice = mUriVoiceMap.GetWeak(aUri, &found);
+ if (NS_WARN_IF(!(found))) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ aRetval = voice->mLang;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+nsSynthVoiceRegistry::GetVoiceName(const nsAString& aUri, nsAString& aRetval) {
+ bool found;
+ VoiceData* voice = mUriVoiceMap.GetWeak(aUri, &found);
+ if (NS_WARN_IF(!(found))) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ aRetval = voice->mName;
+ return NS_OK;
+}
+
+nsresult nsSynthVoiceRegistry::AddVoiceImpl(
+ nsISpeechService* aService, const nsAString& aUri, const nsAString& aName,
+ const nsAString& aLang, bool aLocalService, bool aQueuesUtterances) {
+ const bool found = mUriVoiceMap.Contains(aUri);
+ if (NS_WARN_IF(found)) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ RefPtr<VoiceData> voice = new VoiceData(aService, aUri, aName, aLang,
+ aLocalService, aQueuesUtterances);
+
+ mVoices.AppendElement(voice);
+ mUriVoiceMap.InsertOrUpdate(aUri, std::move(voice));
+ mUseGlobalQueue |= aQueuesUtterances;
+
+ nsTArray<SpeechSynthesisParent*> ssplist;
+ GetAllSpeechSynthActors(ssplist);
+
+ if (!ssplist.IsEmpty()) {
+ mozilla::dom::RemoteVoice ssvoice(nsString(aUri), nsString(aName),
+ nsString(aLang), aLocalService,
+ aQueuesUtterances);
+
+ for (uint32_t i = 0; i < ssplist.Length(); ++i) {
+ Unused << ssplist[i]->SendVoiceAdded(ssvoice);
+ }
+ }
+
+ return NS_OK;
+}
+
+bool nsSynthVoiceRegistry::FindVoiceByLang(const nsAString& aLang,
+ VoiceData** aRetval) {
+ nsAString::const_iterator dashPos, start, end;
+ aLang.BeginReading(start);
+ aLang.EndReading(end);
+
+ while (true) {
+ nsAutoString langPrefix(Substring(start, end));
+
+ for (int32_t i = mDefaultVoices.Length(); i > 0;) {
+ VoiceData* voice = mDefaultVoices[--i];
+
+ if (StringBeginsWith(voice->mLang, langPrefix)) {
+ *aRetval = voice;
+ return true;
+ }
+ }
+
+ for (int32_t i = mVoices.Length(); i > 0;) {
+ VoiceData* voice = mVoices[--i];
+
+ if (StringBeginsWith(voice->mLang, langPrefix)) {
+ *aRetval = voice;
+ return true;
+ }
+ }
+
+ dashPos = end;
+ end = start;
+
+ if (!RFindInReadable(u"-"_ns, end, dashPos)) {
+ break;
+ }
+ }
+
+ return false;
+}
+
+VoiceData* nsSynthVoiceRegistry::FindBestMatch(const nsAString& aUri,
+ const nsAString& aLang) {
+ if (mVoices.IsEmpty()) {
+ return nullptr;
+ }
+
+ bool found = false;
+ VoiceData* retval = mUriVoiceMap.GetWeak(aUri, &found);
+
+ if (found) {
+ LOG(LogLevel::Debug, ("nsSynthVoiceRegistry::FindBestMatch - Matched URI"));
+ return retval;
+ }
+
+ // Try finding a match for given voice.
+ if (!aLang.IsVoid() && !aLang.IsEmpty()) {
+ if (FindVoiceByLang(aLang, &retval)) {
+ LOG(LogLevel::Debug,
+ ("nsSynthVoiceRegistry::FindBestMatch - Matched language (%s ~= %s)",
+ NS_ConvertUTF16toUTF8(aLang).get(),
+ NS_ConvertUTF16toUTF8(retval->mLang).get()));
+
+ return retval;
+ }
+ }
+
+ // Try UI language.
+ nsAutoCString uiLang;
+ LocaleService::GetInstance()->GetAppLocaleAsBCP47(uiLang);
+
+ if (FindVoiceByLang(NS_ConvertASCIItoUTF16(uiLang), &retval)) {
+ LOG(LogLevel::Debug,
+ ("nsSynthVoiceRegistry::FindBestMatch - Matched UI language (%s ~= %s)",
+ uiLang.get(), NS_ConvertUTF16toUTF8(retval->mLang).get()));
+
+ return retval;
+ }
+
+ // Try en-US, the language of locale "C"
+ if (FindVoiceByLang(u"en-US"_ns, &retval)) {
+ LOG(LogLevel::Debug, ("nsSynthVoiceRegistry::FindBestMatch - Matched C "
+ "locale language (en-US ~= %s)",
+ NS_ConvertUTF16toUTF8(retval->mLang).get()));
+
+ return retval;
+ }
+
+ // The top default voice is better than nothing...
+ if (!mDefaultVoices.IsEmpty()) {
+ return mDefaultVoices.LastElement();
+ }
+
+ return nullptr;
+}
+
+already_AddRefed<nsSpeechTask> nsSynthVoiceRegistry::SpeakUtterance(
+ SpeechSynthesisUtterance& aUtterance, const nsAString& aDocLang) {
+ nsString lang =
+ nsString(aUtterance.mLang.IsEmpty() ? aDocLang : aUtterance.mLang);
+ nsAutoString uri;
+
+ if (aUtterance.mVoice) {
+ aUtterance.mVoice->GetVoiceURI(uri);
+ }
+
+ // Get current audio volume to apply speech call
+ float volume = aUtterance.Volume();
+ RefPtr<AudioChannelService> service = AudioChannelService::GetOrCreate();
+ if (service) {
+ if (nsCOMPtr<nsPIDOMWindowInner> topWindow = aUtterance.GetOwner()) {
+ // TODO : use audio channel agent, open new bug to fix it.
+ AudioPlaybackConfig config =
+ service->GetMediaConfig(topWindow->GetOuterWindow());
+ volume = config.mMuted ? 0.0f : config.mVolume * volume;
+ }
+ }
+
+ RefPtr<nsSpeechTask> task;
+ if (XRE_IsContentProcess()) {
+ task = new SpeechTaskChild(&aUtterance,
+ aUtterance.ShouldResistFingerprinting());
+ SpeechSynthesisRequestChild* actor = new SpeechSynthesisRequestChild(
+ static_cast<SpeechTaskChild*>(task.get()));
+ mSpeechSynthChild->SendPSpeechSynthesisRequestConstructor(
+ actor, aUtterance.mText, lang, uri, volume, aUtterance.Rate(),
+ aUtterance.Pitch(), aUtterance.ShouldResistFingerprinting());
+ } else {
+ task =
+ new nsSpeechTask(&aUtterance, aUtterance.ShouldResistFingerprinting());
+ Speak(aUtterance.mText, lang, uri, volume, aUtterance.Rate(),
+ aUtterance.Pitch(), task);
+ }
+
+ return task.forget();
+}
+
+void nsSynthVoiceRegistry::Speak(const nsAString& aText, const nsAString& aLang,
+ const nsAString& aUri, const float& aVolume,
+ const float& aRate, const float& aPitch,
+ nsSpeechTask* aTask) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ if (aTask->ShouldResistFingerprinting()) {
+ aTask->ForceError(0, 0);
+ return;
+ }
+
+ VoiceData* voice = FindBestMatch(aUri, aLang);
+
+ if (!voice) {
+ NS_WARNING("No voices found.");
+ aTask->ForceError(0, 0);
+ return;
+ }
+
+ aTask->SetChosenVoiceURI(voice->mUri);
+
+ if (mUseGlobalQueue ||
+ StaticPrefs::media_webspeech_synth_force_global_queue()) {
+ LOG(LogLevel::Debug,
+ ("nsSynthVoiceRegistry::Speak queueing text='%s' lang='%s' uri='%s' "
+ "rate=%f pitch=%f",
+ NS_ConvertUTF16toUTF8(aText).get(), NS_ConvertUTF16toUTF8(aLang).get(),
+ NS_ConvertUTF16toUTF8(aUri).get(), aRate, aPitch));
+ RefPtr<GlobalQueueItem> item =
+ new GlobalQueueItem(voice, aTask, aText, aVolume, aRate, aPitch);
+ mGlobalQueue.AppendElement(item);
+
+ if (mGlobalQueue.Length() == 1) {
+ SpeakImpl(item->mVoice, item->mTask, item->mText, item->mVolume,
+ item->mRate, item->mPitch);
+ }
+ } else {
+ SpeakImpl(voice, aTask, aText, aVolume, aRate, aPitch);
+ }
+}
+
+void nsSynthVoiceRegistry::SpeakNext() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ LOG(LogLevel::Debug,
+ ("nsSynthVoiceRegistry::SpeakNext %d", mGlobalQueue.IsEmpty()));
+
+ SetIsSpeaking(false);
+
+ if (mGlobalQueue.IsEmpty()) {
+ return;
+ }
+
+ mGlobalQueue.RemoveElementAt(0);
+
+ while (!mGlobalQueue.IsEmpty()) {
+ RefPtr<GlobalQueueItem> item = mGlobalQueue.ElementAt(0);
+ if (item->mTask->IsPreCanceled()) {
+ mGlobalQueue.RemoveElementAt(0);
+ continue;
+ }
+ if (!item->mTask->IsPrePaused()) {
+ SpeakImpl(item->mVoice, item->mTask, item->mText, item->mVolume,
+ item->mRate, item->mPitch);
+ }
+ break;
+ }
+}
+
+void nsSynthVoiceRegistry::ResumeQueue() {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ LOG(LogLevel::Debug,
+ ("nsSynthVoiceRegistry::ResumeQueue %d", mGlobalQueue.IsEmpty()));
+
+ if (mGlobalQueue.IsEmpty()) {
+ return;
+ }
+
+ RefPtr<GlobalQueueItem> item = mGlobalQueue.ElementAt(0);
+ if (!item->mTask->IsPrePaused()) {
+ SpeakImpl(item->mVoice, item->mTask, item->mText, item->mVolume,
+ item->mRate, item->mPitch);
+ }
+}
+
+bool nsSynthVoiceRegistry::IsSpeaking() { return mIsSpeaking; }
+
+void nsSynthVoiceRegistry::SetIsSpeaking(bool aIsSpeaking) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ // Only set to 'true' if global queue is enabled.
+ mIsSpeaking =
+ aIsSpeaking && (mUseGlobalQueue ||
+ StaticPrefs::media_webspeech_synth_force_global_queue());
+
+ nsTArray<SpeechSynthesisParent*> ssplist;
+ GetAllSpeechSynthActors(ssplist);
+ for (uint32_t i = 0; i < ssplist.Length(); ++i) {
+ Unused << ssplist[i]->SendIsSpeakingChanged(aIsSpeaking);
+ }
+}
+
+void nsSynthVoiceRegistry::SpeakImpl(VoiceData* aVoice, nsSpeechTask* aTask,
+ const nsAString& aText,
+ const float& aVolume, const float& aRate,
+ const float& aPitch) {
+ LOG(LogLevel::Debug,
+ ("nsSynthVoiceRegistry::SpeakImpl queueing text='%s' uri='%s' rate=%f "
+ "pitch=%f",
+ NS_ConvertUTF16toUTF8(aText).get(),
+ NS_ConvertUTF16toUTF8(aVoice->mUri).get(), aRate, aPitch));
+
+ aTask->Init();
+
+ if (NS_FAILED(aVoice->mService->Speak(aText, aVoice->mUri, aVolume, aRate,
+ aPitch, aTask))) {
+ aTask->DispatchError(0, 0);
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webspeech/synth/nsSynthVoiceRegistry.h b/dom/media/webspeech/synth/nsSynthVoiceRegistry.h
new file mode 100644
index 0000000000..85c67c087f
--- /dev/null
+++ b/dom/media/webspeech/synth/nsSynthVoiceRegistry.h
@@ -0,0 +1,99 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_nsSynthVoiceRegistry_h
+#define mozilla_dom_nsSynthVoiceRegistry_h
+
+#include "nsISynthVoiceRegistry.h"
+#include "nsRefPtrHashtable.h"
+#include "nsTArray.h"
+
+class nsISpeechService;
+
+namespace mozilla::dom {
+
+class RemoteVoice;
+class SpeechSynthesisUtterance;
+class SpeechSynthesisChild;
+class SpeechSynthesisParent;
+class nsSpeechTask;
+class VoiceData;
+class GlobalQueueItem;
+
+class nsSynthVoiceRegistry final : public nsISynthVoiceRegistry {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISYNTHVOICEREGISTRY
+
+ nsSynthVoiceRegistry();
+
+ already_AddRefed<nsSpeechTask> SpeakUtterance(
+ SpeechSynthesisUtterance& aUtterance, const nsAString& aDocLang);
+
+ void Speak(const nsAString& aText, const nsAString& aLang,
+ const nsAString& aUri, const float& aVolume, const float& aRate,
+ const float& aPitch, nsSpeechTask* aTask);
+
+ bool SendInitialVoicesAndState(SpeechSynthesisParent* aParent);
+
+ void SpeakNext();
+
+ void ResumeQueue();
+
+ bool IsSpeaking();
+
+ void SetIsSpeaking(bool aIsSpeaking);
+
+ static nsSynthVoiceRegistry* GetInstance();
+
+ static already_AddRefed<nsSynthVoiceRegistry> GetInstanceForService();
+
+ static void RecvInitialVoicesAndState(const nsTArray<RemoteVoice>& aVoices,
+ const nsTArray<nsString>& aDefaults,
+ const bool& aIsSpeaking);
+
+ static void RecvRemoveVoice(const nsAString& aUri);
+
+ static void RecvAddVoice(const RemoteVoice& aVoice);
+
+ static void RecvSetDefaultVoice(const nsAString& aUri, bool aIsDefault);
+
+ static void RecvIsSpeakingChanged(bool aIsSpeaking);
+
+ static void RecvNotifyVoicesChanged();
+
+ private:
+ virtual ~nsSynthVoiceRegistry();
+
+ VoiceData* FindBestMatch(const nsAString& aUri, const nsAString& lang);
+
+ bool FindVoiceByLang(const nsAString& aLang, VoiceData** aRetval);
+
+ nsresult AddVoiceImpl(nsISpeechService* aService, const nsAString& aUri,
+ const nsAString& aName, const nsAString& aLang,
+ bool aLocalService, bool aQueuesUtterances);
+
+ void SpeakImpl(VoiceData* aVoice, nsSpeechTask* aTask, const nsAString& aText,
+ const float& aVolume, const float& aRate, const float& aPitch);
+
+ nsTArray<RefPtr<VoiceData>> mVoices;
+
+ nsTArray<RefPtr<VoiceData>> mDefaultVoices;
+
+ nsRefPtrHashtable<nsStringHashKey, VoiceData> mUriVoiceMap;
+
+ SpeechSynthesisChild* mSpeechSynthChild;
+
+ bool mUseGlobalQueue;
+
+ nsTArray<RefPtr<GlobalQueueItem>> mGlobalQueue;
+
+ bool mIsSpeaking;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webspeech/synth/speechd/SpeechDispatcherService.cpp b/dom/media/webspeech/synth/speechd/SpeechDispatcherService.cpp
new file mode 100644
index 0000000000..e0d5488748
--- /dev/null
+++ b/dom/media/webspeech/synth/speechd/SpeechDispatcherService.cpp
@@ -0,0 +1,538 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SpeechDispatcherService.h"
+
+#include "mozilla/dom/nsSpeechTask.h"
+#include "mozilla/dom/nsSynthVoiceRegistry.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "nsEscape.h"
+#include "nsISupports.h"
+#include "nsPrintfCString.h"
+#include "nsReadableUtils.h"
+#include "nsServiceManagerUtils.h"
+#include "nsThreadUtils.h"
+#include "nsXULAppAPI.h"
+#include "prlink.h"
+
+#include <math.h>
+#include <stdlib.h>
+
+#define URI_PREFIX "urn:moz-tts:speechd:"
+
+#define MAX_RATE static_cast<float>(2.5)
+#define MIN_RATE static_cast<float>(0.5)
+
+// Some structures for libspeechd
+typedef enum {
+ SPD_EVENT_BEGIN,
+ SPD_EVENT_END,
+ SPD_EVENT_INDEX_MARK,
+ SPD_EVENT_CANCEL,
+ SPD_EVENT_PAUSE,
+ SPD_EVENT_RESUME
+} SPDNotificationType;
+
+typedef enum {
+ SPD_BEGIN = 1,
+ SPD_END = 2,
+ SPD_INDEX_MARKS = 4,
+ SPD_CANCEL = 8,
+ SPD_PAUSE = 16,
+ SPD_RESUME = 32,
+
+ SPD_ALL = 0x3f
+} SPDNotification;
+
+typedef enum { SPD_MODE_SINGLE = 0, SPD_MODE_THREADED = 1 } SPDConnectionMode;
+
+typedef void (*SPDCallback)(size_t msg_id, size_t client_id,
+ SPDNotificationType state);
+
+typedef void (*SPDCallbackIM)(size_t msg_id, size_t client_id,
+ SPDNotificationType state, char* index_mark);
+
+struct SPDConnection {
+ SPDCallback callback_begin;
+ SPDCallback callback_end;
+ SPDCallback callback_cancel;
+ SPDCallback callback_pause;
+ SPDCallback callback_resume;
+ SPDCallbackIM callback_im;
+
+ /* partial, more private fields in structure */
+};
+
+struct SPDVoice {
+ char* name;
+ char* language;
+ char* variant;
+};
+
+typedef enum {
+ SPD_IMPORTANT = 1,
+ SPD_MESSAGE = 2,
+ SPD_TEXT = 3,
+ SPD_NOTIFICATION = 4,
+ SPD_PROGRESS = 5
+} SPDPriority;
+
+#define SPEECHD_FUNCTIONS \
+ FUNC(spd_open, SPDConnection*, \
+ (const char*, const char*, const char*, SPDConnectionMode)) \
+ FUNC(spd_close, void, (SPDConnection*)) \
+ FUNC(spd_list_synthesis_voices, SPDVoice**, (SPDConnection*)) \
+ FUNC(spd_say, int, (SPDConnection*, SPDPriority, const char*)) \
+ FUNC(spd_cancel, int, (SPDConnection*)) \
+ FUNC(spd_set_volume, int, (SPDConnection*, int)) \
+ FUNC(spd_set_voice_rate, int, (SPDConnection*, int)) \
+ FUNC(spd_set_voice_pitch, int, (SPDConnection*, int)) \
+ FUNC(spd_set_synthesis_voice, int, (SPDConnection*, const char*)) \
+ FUNC(spd_set_notification_on, int, (SPDConnection*, SPDNotification))
+
+#define FUNC(name, type, params) \
+ typedef type(*_##name##_fn) params; \
+ static _##name##_fn _##name;
+
+SPEECHD_FUNCTIONS
+
+#undef FUNC
+
+#define spd_open _spd_open
+#define spd_close _spd_close
+#define spd_list_synthesis_voices _spd_list_synthesis_voices
+#define spd_say _spd_say
+#define spd_cancel _spd_cancel
+#define spd_set_volume _spd_set_volume
+#define spd_set_voice_rate _spd_set_voice_rate
+#define spd_set_voice_pitch _spd_set_voice_pitch
+#define spd_set_synthesis_voice _spd_set_synthesis_voice
+#define spd_set_notification_on _spd_set_notification_on
+
+static PRLibrary* speechdLib = nullptr;
+
+typedef void (*nsSpeechDispatcherFunc)();
+struct nsSpeechDispatcherDynamicFunction {
+ const char* functionName;
+ nsSpeechDispatcherFunc* function;
+};
+
+namespace mozilla::dom {
+
+StaticRefPtr<SpeechDispatcherService> SpeechDispatcherService::sSingleton;
+
+class SpeechDispatcherVoice {
+ public:
+ SpeechDispatcherVoice(const nsAString& aName, const nsAString& aLanguage)
+ : mName(aName), mLanguage(aLanguage) {}
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SpeechDispatcherVoice)
+
+ // Voice name
+ nsString mName;
+
+ // Voice language, in BCP-47 syntax
+ nsString mLanguage;
+
+ private:
+ ~SpeechDispatcherVoice() = default;
+};
+
+class SpeechDispatcherCallback final : public nsISpeechTaskCallback {
+ public:
+ SpeechDispatcherCallback(nsISpeechTask* aTask,
+ SpeechDispatcherService* aService)
+ : mTask(aTask), mService(aService) {}
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(SpeechDispatcherCallback,
+ nsISpeechTaskCallback)
+
+ NS_DECL_NSISPEECHTASKCALLBACK
+
+ bool OnSpeechEvent(SPDNotificationType state);
+
+ private:
+ ~SpeechDispatcherCallback() = default;
+
+ // This pointer is used to dispatch events
+ nsCOMPtr<nsISpeechTask> mTask;
+
+ // By holding a strong reference to the service we guarantee that it won't be
+ // destroyed before this runnable.
+ RefPtr<SpeechDispatcherService> mService;
+
+ TimeStamp mStartTime;
+};
+
+NS_IMPL_CYCLE_COLLECTION(SpeechDispatcherCallback, mTask);
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SpeechDispatcherCallback)
+ NS_INTERFACE_MAP_ENTRY(nsISpeechTaskCallback)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsISpeechTaskCallback)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(SpeechDispatcherCallback)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(SpeechDispatcherCallback)
+
+NS_IMETHODIMP
+SpeechDispatcherCallback::OnPause() {
+ // XXX: Speech dispatcher does not pause immediately, but waits for the speech
+ // to reach an index mark so that it could resume from that offset.
+ // There is no support for word or sentence boundaries, so index marks would
+ // only occur in explicit SSML marks, and we don't support that yet.
+ // What in actuality happens, is that if you call spd_pause(), it will speak
+ // the utterance in its entirety, dispatch an end event, and then put speechd
+ // in a 'paused' state. Since it is after the utterance ended, we don't get
+ // that state change, and our speech api is in an unrecoverable state.
+ // So, since it is useless anyway, I am not implementing pause.
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SpeechDispatcherCallback::OnResume() {
+ // XXX: Unsupported, see OnPause().
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SpeechDispatcherCallback::OnCancel() {
+ if (spd_cancel(mService->mSpeechdClient) < 0) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SpeechDispatcherCallback::OnVolumeChanged(float aVolume) {
+ // XXX: This currently does not change the volume mid-utterance, but it
+ // doesn't do anything bad either. So we could put this here with the hopes
+ // that speechd supports this in the future.
+ if (spd_set_volume(mService->mSpeechdClient,
+ static_cast<int>(aVolume * 100)) < 0) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+bool SpeechDispatcherCallback::OnSpeechEvent(SPDNotificationType state) {
+ bool remove = false;
+
+ switch (state) {
+ case SPD_EVENT_BEGIN:
+ mStartTime = TimeStamp::Now();
+ mTask->DispatchStart();
+ break;
+
+ case SPD_EVENT_PAUSE:
+ mTask->DispatchPause((TimeStamp::Now() - mStartTime).ToSeconds(), 0);
+ break;
+
+ case SPD_EVENT_RESUME:
+ mTask->DispatchResume((TimeStamp::Now() - mStartTime).ToSeconds(), 0);
+ break;
+
+ case SPD_EVENT_CANCEL:
+ case SPD_EVENT_END:
+ mTask->DispatchEnd((TimeStamp::Now() - mStartTime).ToSeconds(), 0);
+ remove = true;
+ break;
+
+ case SPD_EVENT_INDEX_MARK:
+ // Not yet supported
+ break;
+
+ default:
+ break;
+ }
+
+ return remove;
+}
+
+static void speechd_cb(size_t msg_id, size_t client_id,
+ SPDNotificationType state) {
+ SpeechDispatcherService* service =
+ SpeechDispatcherService::GetInstance(false);
+
+ if (service) {
+ NS_DispatchToMainThread(NewRunnableMethod<uint32_t, SPDNotificationType>(
+ "dom::SpeechDispatcherService::EventNotify", service,
+ &SpeechDispatcherService::EventNotify, static_cast<uint32_t>(msg_id),
+ state));
+ }
+}
+
+NS_INTERFACE_MAP_BEGIN(SpeechDispatcherService)
+ NS_INTERFACE_MAP_ENTRY(nsISpeechService)
+ NS_INTERFACE_MAP_ENTRY(nsIObserver)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_ADDREF(SpeechDispatcherService)
+NS_IMPL_RELEASE(SpeechDispatcherService)
+
+SpeechDispatcherService::SpeechDispatcherService()
+ : mInitialized(false), mSpeechdClient(nullptr) {}
+
+void SpeechDispatcherService::Init() {
+ if (!StaticPrefs::media_webspeech_synth_enabled() ||
+ Preferences::GetBool("media.webspeech.synth.test")) {
+ return;
+ }
+
+ // While speech dispatcher has a "threaded" mode, only spd_say() is async.
+ // Since synchronous socket i/o could impact startup time, we do
+ // initialization in a separate thread.
+ DebugOnly<nsresult> rv =
+ NS_NewNamedThread("speechd init", getter_AddRefs(mInitThread));
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ rv = mInitThread->Dispatch(
+ NewRunnableMethod("dom::SpeechDispatcherService::Setup", this,
+ &SpeechDispatcherService::Setup),
+ NS_DISPATCH_NORMAL);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+}
+
+SpeechDispatcherService::~SpeechDispatcherService() {
+ if (mInitThread) {
+ mInitThread->Shutdown();
+ }
+
+ if (mSpeechdClient) {
+ spd_close(mSpeechdClient);
+ }
+}
+
+void SpeechDispatcherService::Setup() {
+#define FUNC(name, type, params) {#name, (nsSpeechDispatcherFunc*)&_##name},
+ static const nsSpeechDispatcherDynamicFunction kSpeechDispatcherSymbols[] = {
+ SPEECHD_FUNCTIONS};
+#undef FUNC
+
+ MOZ_ASSERT(!mInitialized);
+
+ speechdLib = PR_LoadLibrary("libspeechd.so.2");
+
+ if (!speechdLib) {
+ NS_WARNING("Failed to load speechd library");
+ return;
+ }
+
+ if (!PR_FindFunctionSymbol(speechdLib, "spd_get_volume")) {
+ // There is no version getter function, so we rely on a symbol that was
+ // introduced in release 0.8.2 in order to check for ABI compatibility.
+ NS_WARNING("Unsupported version of speechd detected");
+ return;
+ }
+
+ for (uint32_t i = 0; i < ArrayLength(kSpeechDispatcherSymbols); i++) {
+ *kSpeechDispatcherSymbols[i].function = PR_FindFunctionSymbol(
+ speechdLib, kSpeechDispatcherSymbols[i].functionName);
+
+ if (!*kSpeechDispatcherSymbols[i].function) {
+ NS_WARNING(nsPrintfCString("Failed to find speechd symbol for'%s'",
+ kSpeechDispatcherSymbols[i].functionName)
+ .get());
+ return;
+ }
+ }
+
+ mSpeechdClient =
+ spd_open("firefox", "web speech api", "who", SPD_MODE_THREADED);
+ if (!mSpeechdClient) {
+ NS_WARNING("Failed to call spd_open");
+ return;
+ }
+
+ // Get all the voices from sapi and register in the SynthVoiceRegistry
+ SPDVoice** list = spd_list_synthesis_voices(mSpeechdClient);
+
+ mSpeechdClient->callback_begin = speechd_cb;
+ mSpeechdClient->callback_end = speechd_cb;
+ mSpeechdClient->callback_cancel = speechd_cb;
+ mSpeechdClient->callback_pause = speechd_cb;
+ mSpeechdClient->callback_resume = speechd_cb;
+
+ spd_set_notification_on(mSpeechdClient, SPD_BEGIN);
+ spd_set_notification_on(mSpeechdClient, SPD_END);
+ spd_set_notification_on(mSpeechdClient, SPD_CANCEL);
+
+ if (list != NULL) {
+ for (int i = 0; list[i]; i++) {
+ nsAutoString uri;
+
+ uri.AssignLiteral(URI_PREFIX);
+ nsAutoCString name;
+ NS_EscapeURL(list[i]->name, -1,
+ esc_OnlyNonASCII | esc_Spaces | esc_AlwaysCopy, name);
+ uri.Append(NS_ConvertUTF8toUTF16(name));
+
+ uri.AppendLiteral("?");
+
+ nsAutoCString lang(list[i]->language);
+
+ uri.Append(NS_ConvertUTF8toUTF16(lang));
+
+ mVoices.InsertOrUpdate(uri, MakeRefPtr<SpeechDispatcherVoice>(
+ NS_ConvertUTF8toUTF16(list[i]->name),
+ NS_ConvertUTF8toUTF16(lang)));
+ }
+ }
+
+ NS_DispatchToMainThread(
+ NewRunnableMethod("dom::SpeechDispatcherService::RegisterVoices", this,
+ &SpeechDispatcherService::RegisterVoices));
+
+ // mInitialized = true;
+}
+
+// private methods
+
+void SpeechDispatcherService::RegisterVoices() {
+ RefPtr<nsSynthVoiceRegistry> registry = nsSynthVoiceRegistry::GetInstance();
+ for (const auto& entry : mVoices) {
+ const RefPtr<SpeechDispatcherVoice>& voice = entry.GetData();
+
+ // This service can only speak one utterance at a time, so we set
+ // aQueuesUtterances to true in order to track global state and schedule
+ // access to this service.
+ DebugOnly<nsresult> rv =
+ registry->AddVoice(this, entry.GetKey(), voice->mName, voice->mLanguage,
+ voice->mName.EqualsLiteral("default"), true);
+
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to add voice");
+ }
+
+ mInitThread->Shutdown();
+ mInitThread = nullptr;
+
+ mInitialized = true;
+
+ registry->NotifyVoicesChanged();
+}
+
+// nsIObserver
+
+NS_IMETHODIMP
+SpeechDispatcherService::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ return NS_OK;
+}
+
+// nsISpeechService
+
+// TODO: Support SSML
+NS_IMETHODIMP
+SpeechDispatcherService::Speak(const nsAString& aText, const nsAString& aUri,
+ float aVolume, float aRate, float aPitch,
+ nsISpeechTask* aTask) {
+ if (NS_WARN_IF(!mInitialized)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ RefPtr<SpeechDispatcherCallback> callback =
+ new SpeechDispatcherCallback(aTask, this);
+
+ bool found = false;
+ SpeechDispatcherVoice* voice = mVoices.GetWeak(aUri, &found);
+
+ if (NS_WARN_IF(!(found))) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ spd_set_synthesis_voice(mSpeechdClient,
+ NS_ConvertUTF16toUTF8(voice->mName).get());
+
+ // We provide a volume of 0.0 to 1.0, speech-dispatcher expects 0 - 100.
+ spd_set_volume(mSpeechdClient, static_cast<int>(aVolume * 100));
+
+ // aRate is a value of 0.1 (0.1x) to 10 (10x) with 1 (1x) being normal rate.
+ // speechd expects -100 to 100 with 0 being normal rate.
+ float rate = 0;
+ if (aRate > 1) {
+ // Each step to 100 is logarithmically distributed up to 2.5x.
+ rate = log10(std::min(aRate, MAX_RATE)) / log10(MAX_RATE) * 100;
+ } else if (aRate < 1) {
+ // Each step to -100 is logarithmically distributed down to 0.5x.
+ rate = log10(std::max(aRate, MIN_RATE)) / log10(MIN_RATE) * -100;
+ }
+
+ spd_set_voice_rate(mSpeechdClient, static_cast<int>(rate));
+
+ // We provide a pitch of 0 to 2 with 1 being the default.
+ // speech-dispatcher expects -100 to 100 with 0 being default.
+ spd_set_voice_pitch(mSpeechdClient, static_cast<int>((aPitch - 1) * 100));
+
+ nsresult rv = aTask->Setup(callback);
+
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ if (aText.Length()) {
+ int msg_id = spd_say(mSpeechdClient, SPD_MESSAGE,
+ NS_ConvertUTF16toUTF8(aText).get());
+
+ if (msg_id < 0) {
+ return NS_ERROR_FAILURE;
+ }
+
+ mCallbacks.InsertOrUpdate(msg_id, std::move(callback));
+ } else {
+ // Speech dispatcher does not work well with empty strings.
+ // In that case, don't send empty string to speechd,
+ // and just emulate a speechd start and end event.
+ NS_DispatchToMainThread(NewRunnableMethod<SPDNotificationType>(
+ "dom::SpeechDispatcherCallback::OnSpeechEvent", callback,
+ &SpeechDispatcherCallback::OnSpeechEvent, SPD_EVENT_BEGIN));
+
+ NS_DispatchToMainThread(NewRunnableMethod<SPDNotificationType>(
+ "dom::SpeechDispatcherCallback::OnSpeechEvent", callback,
+ &SpeechDispatcherCallback::OnSpeechEvent, SPD_EVENT_END));
+ }
+
+ return NS_OK;
+}
+
+SpeechDispatcherService* SpeechDispatcherService::GetInstance(bool create) {
+ if (XRE_GetProcessType() != GeckoProcessType_Default) {
+ MOZ_ASSERT(
+ false,
+ "SpeechDispatcherService can only be started on main gecko process");
+ return nullptr;
+ }
+
+ if (!sSingleton && create) {
+ sSingleton = new SpeechDispatcherService();
+ sSingleton->Init();
+ ClearOnShutdown(&sSingleton);
+ }
+
+ return sSingleton;
+}
+
+already_AddRefed<SpeechDispatcherService>
+SpeechDispatcherService::GetInstanceForService() {
+ MOZ_ASSERT(NS_IsMainThread());
+ RefPtr<SpeechDispatcherService> sapiService = GetInstance();
+ return sapiService.forget();
+}
+
+void SpeechDispatcherService::EventNotify(uint32_t aMsgId, uint32_t aState) {
+ SpeechDispatcherCallback* callback = mCallbacks.GetWeak(aMsgId);
+
+ if (callback) {
+ if (callback->OnSpeechEvent((SPDNotificationType)aState)) {
+ mCallbacks.Remove(aMsgId);
+ }
+ }
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webspeech/synth/speechd/SpeechDispatcherService.h b/dom/media/webspeech/synth/speechd/SpeechDispatcherService.h
new file mode 100644
index 0000000000..2922053c80
--- /dev/null
+++ b/dom/media/webspeech/synth/speechd/SpeechDispatcherService.h
@@ -0,0 +1,65 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_SpeechDispatcherService_h
+#define mozilla_dom_SpeechDispatcherService_h
+
+#include "mozilla/StaticPtr.h"
+#include "nsIObserver.h"
+#include "nsISpeechService.h"
+#include "nsIThread.h"
+#include "nsRefPtrHashtable.h"
+#include "nsTArray.h"
+
+struct SPDConnection;
+
+namespace mozilla {
+namespace dom {
+
+class SpeechDispatcherCallback;
+class SpeechDispatcherVoice;
+
+class SpeechDispatcherService final : public nsIObserver,
+ public nsISpeechService {
+ friend class SpeechDispatcherCallback;
+
+ public:
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+ NS_DECL_NSISPEECHSERVICE
+
+ SpeechDispatcherService();
+
+ void Init();
+
+ void Setup();
+
+ void EventNotify(uint32_t aMsgId, uint32_t aState);
+
+ static SpeechDispatcherService* GetInstance(bool create = true);
+ static already_AddRefed<SpeechDispatcherService> GetInstanceForService();
+
+ static StaticRefPtr<SpeechDispatcherService> sSingleton;
+
+ private:
+ virtual ~SpeechDispatcherService();
+
+ void RegisterVoices();
+
+ bool mInitialized;
+
+ SPDConnection* mSpeechdClient;
+
+ nsRefPtrHashtable<nsUint32HashKey, SpeechDispatcherCallback> mCallbacks;
+
+ nsCOMPtr<nsIThread> mInitThread;
+
+ nsRefPtrHashtable<nsStringHashKey, SpeechDispatcherVoice> mVoices;
+};
+
+} // namespace dom
+} // namespace mozilla
+#endif
diff --git a/dom/media/webspeech/synth/speechd/components.conf b/dom/media/webspeech/synth/speechd/components.conf
new file mode 100644
index 0000000000..56b01ba5cb
--- /dev/null
+++ b/dom/media/webspeech/synth/speechd/components.conf
@@ -0,0 +1,17 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+ {
+ 'cid': '{8817b1cf-5ada-43bf-bd73-607657703d0d}',
+ 'contract_ids': ['@mozilla.org/synthspeechdispatcher;1'],
+ 'singleton': True,
+ 'type': 'mozilla::dom::SpeechDispatcherService',
+ 'headers': ['/dom/media/webspeech/synth/speechd/SpeechDispatcherService.h'],
+ 'constructor': 'mozilla::dom::SpeechDispatcherService::GetInstanceForService',
+ 'categories': {"speech-synth-started": 'SpeechDispatcher Speech Synth'},
+ },
+]
diff --git a/dom/media/webspeech/synth/speechd/moz.build b/dom/media/webspeech/synth/speechd/moz.build
new file mode 100644
index 0000000000..0d9632a488
--- /dev/null
+++ b/dom/media/webspeech/synth/speechd/moz.build
@@ -0,0 +1,15 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES += ["SpeechDispatcherService.cpp"]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/webspeech/synth/test/common.js b/dom/media/webspeech/synth/test/common.js
new file mode 100644
index 0000000000..c22b0b488c
--- /dev/null
+++ b/dom/media/webspeech/synth/test/common.js
@@ -0,0 +1,104 @@
+function synthTestQueue(aTestArgs, aEndFunc) {
+ var utterances = [];
+ for (var i in aTestArgs) {
+ var uargs = aTestArgs[i][0];
+ var win = uargs.win || window;
+ var u = new win.SpeechSynthesisUtterance(uargs.text);
+
+ if (uargs.args) {
+ for (var attr in uargs.args) {
+ u[attr] = uargs.args[attr];
+ }
+ }
+
+ function onend_handler(e) {
+ is(e.target, utterances.shift(), "Target matches utterances");
+ ok(!speechSynthesis.speaking, "speechSynthesis is not speaking.");
+
+ if (utterances.length) {
+ ok(speechSynthesis.pending, "other utterances queued");
+ } else {
+ ok(!speechSynthesis.pending, "queue is empty, nothing pending.");
+ if (aEndFunc) {
+ aEndFunc();
+ }
+ }
+ }
+
+ u.addEventListener(
+ "start",
+ (function (expectedUri) {
+ return function (e) {
+ if (expectedUri) {
+ var chosenVoice = SpecialPowers.wrap(e).target.chosenVoiceURI;
+ is(chosenVoice, expectedUri, "Incorrect URI is used");
+ }
+ };
+ })(aTestArgs[i][1] ? aTestArgs[i][1].uri : null)
+ );
+
+ u.addEventListener("end", onend_handler);
+ u.addEventListener("error", onend_handler);
+
+ u.addEventListener(
+ "error",
+ (function (expectedError) {
+ return function onerror_handler(e) {
+ ok(
+ expectedError,
+ "Error in speech utterance '" + e.target.text + "'"
+ );
+ };
+ })(aTestArgs[i][1] ? aTestArgs[i][1].err : false)
+ );
+
+ utterances.push(u);
+ win.speechSynthesis.speak(u);
+ }
+
+ ok(!speechSynthesis.speaking, "speechSynthesis is not speaking yet.");
+ ok(speechSynthesis.pending, "speechSynthesis has an utterance queued.");
+}
+
+function loadFrame(frameId) {
+ return new Promise(function (resolve, reject) {
+ var frame = document.getElementById(frameId);
+ frame.addEventListener("load", function (e) {
+ frame.contentWindow.document.title = frameId;
+ resolve(frame);
+ });
+ frame.src = "about:blank";
+ });
+}
+
+function waitForVoices(win) {
+ return new Promise(resolve => {
+ function resolver() {
+ if (win.speechSynthesis.getVoices().length) {
+ win.speechSynthesis.removeEventListener("voiceschanged", resolver);
+ resolve();
+ }
+ }
+
+ win.speechSynthesis.addEventListener("voiceschanged", resolver);
+ resolver();
+ });
+}
+
+function loadSpeechTest(fileName, prefs, frameId = "testFrame") {
+ loadFrame(frameId).then(frame => {
+ waitForVoices(frame.contentWindow).then(
+ () => (document.getElementById("testFrame").src = fileName)
+ );
+ });
+}
+
+function testSynthState(win, expectedState) {
+ for (var attr in expectedState) {
+ is(
+ win.speechSynthesis[attr],
+ expectedState[attr],
+ win.document.title + ": '" + attr + '" does not match'
+ );
+ }
+}
diff --git a/dom/media/webspeech/synth/test/components.conf b/dom/media/webspeech/synth/test/components.conf
new file mode 100644
index 0000000000..f37e4eafae
--- /dev/null
+++ b/dom/media/webspeech/synth/test/components.conf
@@ -0,0 +1,17 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+ {
+ 'cid': '{e7d52d9e-c148-47d8-ab2a-95d7f40ea53d}',
+ 'contract_ids': ['@mozilla.org/fakesynth;1'],
+ 'singleton': True,
+ 'type': 'mozilla::dom::nsFakeSynthServices',
+ 'headers': ['/dom/media/webspeech/synth/test/nsFakeSynthServices.h'],
+ 'constructor': 'mozilla::dom::nsFakeSynthServices::GetInstanceForService',
+ 'categories': {'speech-synth-started': 'Fake Speech Synth'},
+ },
+]
diff --git a/dom/media/webspeech/synth/test/file_bfcache_page1.html b/dom/media/webspeech/synth/test/file_bfcache_page1.html
new file mode 100644
index 0000000000..d6229eeeda
--- /dev/null
+++ b/dom/media/webspeech/synth/test/file_bfcache_page1.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <script type="application/javascript">
+ addEventListener('pageshow', function onshow(evt) {
+ var u = new SpeechSynthesisUtterance('hello');
+ u.lang = 'it-IT-noend';
+ u.addEventListener('start', function() {
+ location = "file_bfcache_page2.html";
+ });
+ speechSynthesis.speak(u);
+ });
+ </script>
+</head>
+<body>
+</body>
+</html>
diff --git a/dom/media/webspeech/synth/test/file_bfcache_page2.html b/dom/media/webspeech/synth/test/file_bfcache_page2.html
new file mode 100644
index 0000000000..30b9aa9117
--- /dev/null
+++ b/dom/media/webspeech/synth/test/file_bfcache_page2.html
@@ -0,0 +1,14 @@
+<html>
+<script>
+var frameUnloaded = function() {
+ var u = new SpeechSynthesisUtterance('hi');
+ u.addEventListener('end', function () {
+ opener.ok(true, 'Successfully spoke utterance from new frame.');
+ opener.onDone();
+ });
+ speechSynthesis.speak(u);
+};
+</script>
+
+<body onpageshow="frameUnloaded()"></body></html>
+
diff --git a/dom/media/webspeech/synth/test/file_global_queue.html b/dom/media/webspeech/synth/test/file_global_queue.html
new file mode 100644
index 0000000000..5d762c0d51
--- /dev/null
+++ b/dom/media/webspeech/synth/test/file_global_queue.html
@@ -0,0 +1,69 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1188099
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1188099: Global queue should correctly schedule utterances</title>
+ <script type="application/javascript">
+ window.SimpleTest = parent.SimpleTest;
+ window.info = parent.info;
+ window.is = parent.is;
+ window.isnot = parent.isnot;
+ window.ok = parent.ok;
+ window.todo = parent.todo;
+ </script>
+ <script type="application/javascript" src="common.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1188099">Mozilla Bug 1188099</a>
+<iframe id="frame1"></iframe>
+<iframe id="frame2"></iframe>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+ Promise.all([loadFrame('frame1'), loadFrame('frame2')]).then(function ([frame1, frame2]) {
+ var win1 = frame1.contentWindow;
+ var win2 = frame2.contentWindow;
+ var utterance1 = new win1.SpeechSynthesisUtterance("hello, losers");
+ var utterance2 = new win1.SpeechSynthesisUtterance("hello, losers three");
+ var utterance3 = new win2.SpeechSynthesisUtterance("hello, losers too");
+ var eventOrder = ['start1', 'end1', 'start3', 'end3', 'start2', 'end2'];
+ utterance1.addEventListener('start', function(e) {
+ is(eventOrder.shift(), 'start1', 'start1');
+ testSynthState(win1, { speaking: true, pending: true });
+ testSynthState(win2, { speaking: true, pending: true });
+ });
+ utterance1.addEventListener('end', function(e) {
+ is(eventOrder.shift(), 'end1', 'end1');
+ });
+ utterance3.addEventListener('start', function(e) {
+ is(eventOrder.shift(), 'start3', 'start3');
+ testSynthState(win1, { speaking: true, pending: true });
+ testSynthState(win2, { speaking: true, pending: false });
+ });
+ utterance3.addEventListener('end', function(e) {
+ is(eventOrder.shift(), 'end3', 'end3');
+ });
+ utterance2.addEventListener('start', function(e) {
+ is(eventOrder.shift(), 'start2', 'start2');
+ testSynthState(win1, { speaking: true, pending: false });
+ testSynthState(win2, { speaking: true, pending: false });
+ });
+ utterance2.addEventListener('end', function(e) {
+ is(eventOrder.shift(), 'end2', 'end2');
+ testSynthState(win1, { speaking: false, pending: false });
+ testSynthState(win2, { speaking: false, pending: false });
+ SimpleTest.finish();
+ });
+ win1.speechSynthesis.speak(utterance1);
+ win1.speechSynthesis.speak(utterance2);
+ win2.speechSynthesis.speak(utterance3);
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webspeech/synth/test/file_global_queue_cancel.html b/dom/media/webspeech/synth/test/file_global_queue_cancel.html
new file mode 100644
index 0000000000..03b77ba2fc
--- /dev/null
+++ b/dom/media/webspeech/synth/test/file_global_queue_cancel.html
@@ -0,0 +1,88 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1188099
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1188099: Calling cancel() should work correctly with global queue</title>
+ <script type="application/javascript">
+ window.SimpleTest = parent.SimpleTest;
+ window.info = parent.info;
+ window.is = parent.is;
+ window.isnot = parent.isnot;
+ window.ok = parent.ok;
+ window.todo = parent.todo;
+ </script>
+ <script type="application/javascript" src="common.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1188099">Mozilla Bug 1188099</a>
+<iframe id="frame1"></iframe>
+<iframe id="frame2"></iframe>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+ Promise.all([loadFrame('frame1'), loadFrame('frame2')]).then(function ([frame1, frame2]) {
+ var win1 = frame1.contentWindow;
+ var win2 = frame2.contentWindow;
+
+ var utterance1 = new win1.SpeechSynthesisUtterance(
+ "u1: Donec ac nunc feugiat, posuere");
+ utterance1.lang = 'it-IT-noend';
+ var utterance2 = new win1.SpeechSynthesisUtterance("u2: hello, losers too");
+ utterance2.lang = 'it-IT-noend';
+ var utterance3 = new win1.SpeechSynthesisUtterance("u3: hello, losers three");
+
+ var utterance4 = new win2.SpeechSynthesisUtterance("u4: hello, losers same!");
+ utterance4.lang = 'it-IT-noend';
+ var utterance5 = new win2.SpeechSynthesisUtterance("u5: hello, losers too");
+ utterance5.lang = 'it-IT-noend';
+
+ var eventOrder = ['start1', 'end1', 'start2', 'end2'];
+ utterance1.addEventListener('start', function(e) {
+ is(eventOrder.shift(), 'start1', 'start1');
+ testSynthState(win1, { speaking: true, pending: true });
+ testSynthState(win2, { speaking: true, pending: true });
+ win2.speechSynthesis.cancel();
+ SpecialPowers.wrap(win1.speechSynthesis).forceEnd();
+
+ });
+ utterance1.addEventListener('end', function(e) {
+ is(eventOrder.shift(), 'end1', 'end1');
+ testSynthState(win1, { pending: true });
+ testSynthState(win2, { pending: false });
+ });
+ utterance2.addEventListener('start', function(e) {
+ is(eventOrder.shift(), 'start2', 'start2');
+ testSynthState(win1, { speaking: true, pending: true });
+ testSynthState(win2, { speaking: true, pending: false });
+ win1.speechSynthesis.cancel();
+ });
+ utterance2.addEventListener('end', function(e) {
+ is(eventOrder.shift(), 'end2', 'end2');
+ testSynthState(win1, { speaking: false, pending: false });
+ testSynthState(win2, { speaking: false, pending: false });
+ SimpleTest.finish();
+ });
+
+ function wrongUtterance(e) {
+ ok(false, 'This shall not be uttered: "' + e.target.text + '"');
+ }
+
+ utterance3.addEventListener('start', wrongUtterance);
+ utterance4.addEventListener('start', wrongUtterance);
+ utterance5.addEventListener('start', wrongUtterance);
+
+ win1.speechSynthesis.speak(utterance1);
+ win1.speechSynthesis.speak(utterance2);
+ win1.speechSynthesis.speak(utterance3);
+ win2.speechSynthesis.speak(utterance4);
+ win2.speechSynthesis.speak(utterance5);
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webspeech/synth/test/file_global_queue_pause.html b/dom/media/webspeech/synth/test/file_global_queue_pause.html
new file mode 100644
index 0000000000..e345eb4c98
--- /dev/null
+++ b/dom/media/webspeech/synth/test/file_global_queue_pause.html
@@ -0,0 +1,130 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1188099
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1188099: Calling pause() should work correctly with global queue</title>
+ <script type="application/javascript">
+ window.SimpleTest = parent.SimpleTest;
+ window.info = parent.info;
+ window.is = parent.is;
+ window.isnot = parent.isnot;
+ window.ok = parent.ok;
+ window.todo = parent.todo;
+ </script>
+ <script type="application/javascript" src="common.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1188099">Mozilla Bug 1188099</a>
+<iframe id="frame1"></iframe>
+<iframe id="frame2"></iframe>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+ Promise.all([loadFrame('frame1'), loadFrame('frame2')]).then(function ([frame1, frame2]) {
+ var win1 = frame1.contentWindow;
+ var win2 = frame2.contentWindow;
+
+ var utterance1 = new win1.SpeechSynthesisUtterance("Speak utterance 1.");
+ utterance1.lang = 'it-IT-noend';
+ var utterance2 = new win2.SpeechSynthesisUtterance("Speak utterance 2.");
+ var utterance3 = new win1.SpeechSynthesisUtterance("Speak utterance 3.");
+ var utterance4 = new win2.SpeechSynthesisUtterance("Speak utterance 4.");
+ var eventOrder = ['start1', 'pause1', 'resume1', 'end1', 'start2', 'end2',
+ 'start4', 'end4', 'start3', 'end3'];
+
+ utterance1.addEventListener('start', function(e) {
+ is(eventOrder.shift(), 'start1', 'start1');
+ win1.speechSynthesis.pause();
+ });
+ utterance1.addEventListener('pause', function(e) {
+ var expectedEvent = eventOrder.shift()
+ is(expectedEvent, 'pause1', 'pause1');
+ testSynthState(win1, { speaking: true, pending: false, paused: true});
+ testSynthState(win2, { speaking: true, pending: true, paused: false});
+
+ if (expectedEvent == 'pause1') {
+ win1.speechSynthesis.resume();
+ }
+ });
+ utterance1.addEventListener('resume', function(e) {
+ is(eventOrder.shift(), 'resume1', 'resume1');
+ testSynthState(win1, { speaking: true, pending: false, paused: false});
+ testSynthState(win2, { speaking: true, pending: true, paused: false});
+
+ win2.speechSynthesis.pause();
+
+ testSynthState(win1, { speaking: true, pending: false, paused: false});
+ testSynthState(win2, { speaking: true, pending: true, paused: true });
+
+ // We now make the utterance end.
+ SpecialPowers.wrap(win1.speechSynthesis).forceEnd();
+ });
+ utterance1.addEventListener('end', function(e) {
+ is(eventOrder.shift(), 'end1', 'end1');
+ testSynthState(win1, { speaking: false, pending: false, paused: false});
+ testSynthState(win2, { speaking: false, pending: true, paused: true});
+
+ win2.speechSynthesis.resume();
+ });
+
+ utterance2.addEventListener('start', function(e) {
+ is(eventOrder.shift(), 'start2', 'start2');
+ testSynthState(win1, { speaking: true, pending: false, paused: false});
+ testSynthState(win2, { speaking: true, pending: false, paused: false});
+ });
+ utterance2.addEventListener('end', function(e) {
+ is(eventOrder.shift(), 'end2', 'end2');
+ testSynthState(win1, { speaking: false, pending: false, paused: false});
+ testSynthState(win2, { speaking: false, pending: false, paused: false});
+
+ win1.speechSynthesis.pause();
+
+ testSynthState(win1, { speaking: false, pending: false, paused: true});
+ testSynthState(win2, { speaking: false, pending: false, paused: false});
+
+ win1.speechSynthesis.speak(utterance3);
+ win2.speechSynthesis.speak(utterance4);
+
+ testSynthState(win1, { speaking: false, pending: true, paused: true});
+ testSynthState(win2, { speaking: false, pending: true, paused: false});
+ });
+
+ utterance4.addEventListener('start', function(e) {
+ is(eventOrder.shift(), 'start4', 'start4');
+ testSynthState(win1, { speaking: true, pending: true, paused: true});
+ testSynthState(win2, { speaking: true, pending: false, paused: false});
+
+ win1.speechSynthesis.resume();
+ });
+ utterance4.addEventListener('end', function(e) {
+ is(eventOrder.shift(), 'end4', 'end4');
+ testSynthState(win1, { speaking: false, pending: true, paused: false});
+ testSynthState(win2, { speaking: false, pending: false, paused: false});
+ });
+
+ utterance3.addEventListener('start', function(e) {
+ is(eventOrder.shift(), 'start3', 'start3');
+ testSynthState(win1, { speaking: true, pending: false, paused: false});
+ testSynthState(win2, { speaking: true, pending: false, paused: false});
+ });
+
+ utterance3.addEventListener('end', function(e) {
+ is(eventOrder.shift(), 'end3', 'end3');
+ testSynthState(win1, { speaking: false, pending: false, paused: false});
+ testSynthState(win2, { speaking: false, pending: false, paused: false});
+
+ SimpleTest.finish();
+ });
+
+ win1.speechSynthesis.speak(utterance1);
+ win2.speechSynthesis.speak(utterance2);
+ });
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webspeech/synth/test/file_indirect_service_events.html b/dom/media/webspeech/synth/test/file_indirect_service_events.html
new file mode 100644
index 0000000000..5ed7812757
--- /dev/null
+++ b/dom/media/webspeech/synth/test/file_indirect_service_events.html
@@ -0,0 +1,102 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1155034
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1155034: Check that indirect audio services dispatch their own events</title>
+ <script type="application/javascript">
+ window.SimpleTest = parent.SimpleTest;
+ window.info = parent.info;
+ window.is = parent.is;
+ window.isnot = parent.isnot;
+ window.ok = parent.ok;
+ </script>
+ <script type="application/javascript" src="common.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1155034">Mozilla Bug 1155034</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 1155034 **/
+
+function testFunc(done_cb) {
+ function test_with_events() {
+ info('test_with_events');
+ var utterance = new SpeechSynthesisUtterance("never end, callback events");
+ utterance.lang = 'it-IT-noend';
+
+ utterance.addEventListener('start', function(e) {
+ info('start test_with_events');
+ speechSynthesis.pause();
+ // Wait to see if we get some bad events we didn't expect.
+ });
+
+ utterance.addEventListener('pause', function(e) {
+ is(e.charIndex, 1, 'pause event charIndex matches service arguments');
+ is(e.elapsedTime, 1.5, 'pause event elapsedTime matches service arguments');
+ speechSynthesis.resume();
+ });
+
+ utterance.addEventListener('resume', function(e) {
+ is(e.charIndex, 1, 'resume event charIndex matches service arguments');
+ is(e.elapsedTime, 1.5, 'resume event elapsedTime matches service arguments');
+ speechSynthesis.cancel();
+ });
+
+ utterance.addEventListener('end', function(e) {
+ is(e.charIndex, 1, 'resume event charIndex matches service arguments');
+ is(e.elapsedTime, 1.5, 'end event elapsedTime matches service arguments');
+ test_no_events();
+ });
+
+ info('start speak');
+ speechSynthesis.speak(utterance);
+ }
+
+ function forbiddenEvent(e) {
+ ok(false, 'no "' + e.type + '" event was explicitly dispatched from the service')
+ }
+
+ function test_no_events() {
+ info('test_no_events');
+ var utterance = new SpeechSynthesisUtterance("never end");
+ utterance.lang = "it-IT-noevents-noend";
+ utterance.addEventListener('start', function(e) {
+ speechSynthesis.pause();
+ // Wait to see if we get some bad events we didn't expect.
+ setTimeout(function() {
+ ok(true, 'didn\'t get any unwanted events');
+ utterance.removeEventListener('end', forbiddenEvent);
+ SpecialPowers.wrap(speechSynthesis).forceEnd();
+ done_cb();
+ }, 1000);
+ });
+
+ utterance.addEventListener('pause', forbiddenEvent);
+ utterance.addEventListener('end', forbiddenEvent);
+
+ speechSynthesis.speak(utterance);
+ }
+
+ test_with_events();
+}
+
+// Run test with no global queue, and then run it with a global queue.
+testFunc(function() {
+ SpecialPowers.pushPrefEnv(
+ { set: [['media.webspeech.synth.force_global_queue', true]] }, function() {
+ testFunc(SimpleTest.finish)
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webspeech/synth/test/file_setup.html b/dom/media/webspeech/synth/test/file_setup.html
new file mode 100644
index 0000000000..da8c2c6824
--- /dev/null
+++ b/dom/media/webspeech/synth/test/file_setup.html
@@ -0,0 +1,96 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=525444
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 525444: Web Speech API check all classes are present</title>
+ <script type="application/javascript">
+ window.SimpleTest = parent.SimpleTest;
+ window.is = parent.is;
+ window.isnot = parent.isnot;
+ window.ok = parent.ok;
+ </script>
+ <script type="application/javascript" src="common.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=650295">Mozilla Bug 650295</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 525444 **/
+
+ok(SpeechSynthesis, "SpeechSynthesis exists in global scope");
+ok(SpeechSynthesisVoice, "SpeechSynthesisVoice exists in global scope");
+ok(SpeechSynthesisErrorEvent, "SpeechSynthesisErrorEvent exists in global scope");
+ok(SpeechSynthesisEvent, "SpeechSynthesisEvent exists in global scope");
+
+// SpeechSynthesisUtterance is the only type that has a constructor
+// and writable properties
+ok(SpeechSynthesisUtterance, "SpeechSynthesisUtterance exists in global scope");
+var ssu = new SpeechSynthesisUtterance("hello world");
+is(typeof ssu, "object", "SpeechSynthesisUtterance instance is an object");
+is(ssu.text, "hello world", "SpeechSynthesisUtterance.text is correct");
+is(ssu.volume, 1, "SpeechSynthesisUtterance.volume default is correct");
+is(ssu.rate, 1, "SpeechSynthesisUtterance.rate default is correct");
+is(ssu.pitch, 1, "SpeechSynthesisUtterance.pitch default is correct");
+ssu.lang = "he-IL";
+ssu.volume = 0.5;
+ssu.rate = 2.0;
+ssu.pitch = 1.5;
+is(ssu.lang, "he-IL", "SpeechSynthesisUtterance.lang is correct");
+is(ssu.volume, 0.5, "SpeechSynthesisUtterance.volume is correct");
+is(ssu.rate, 2.0, "SpeechSynthesisUtterance.rate is correct");
+is(ssu.pitch, 1.5, "SpeechSynthesisUtterance.pitch is correct");
+
+// Assign a rate that is out of bounds
+ssu.rate = 20;
+is(ssu.rate, 10, "SpeechSynthesisUtterance.rate enforces max of 10");
+ssu.rate = 0;
+is(ssu.rate.toPrecision(1), "0.1", "SpeechSynthesisUtterance.rate enforces min of 0.1");
+
+// Assign a volume which is out of bounds
+ssu.volume = 2;
+is(ssu.volume, 1, "SpeechSynthesisUtterance.volume enforces max of 1");
+ssu.volume = -1;
+is(ssu.volume, 0, "SpeechSynthesisUtterance.volume enforces min of 0");
+
+// Assign a pitch which is out of bounds
+ssu.pitch = 2.1;
+is(ssu.pitch, 2, "SpeechSynthesisUtterance.pitch enforces max of 2");
+ssu.pitch = -1;
+is(ssu.pitch, 0, "SpeechSynthesisUtterance.pitch enforces min of 0");
+
+// Test for singleton instance hanging off of window.
+ok(speechSynthesis, "speechSynthesis exists in global scope");
+is(typeof speechSynthesis, "object", "speechSynthesis instance is an object");
+is(typeof speechSynthesis.speak, "function", "speechSynthesis.speak is a function");
+is(typeof speechSynthesis.cancel, "function", "speechSynthesis.cancel is a function");
+is(typeof speechSynthesis.pause, "function", "speechSynthesis.pause is a function");
+is(typeof speechSynthesis.resume, "function", "speechSynthesis.resume is a function");
+is(typeof speechSynthesis.getVoices, "function", "speechSynthesis.getVoices is a function");
+
+is(typeof speechSynthesis.pending, "boolean", "speechSynthesis.pending is a boolean");
+is(typeof speechSynthesis.speaking, "boolean", "speechSynthesis.speaking is a boolean");
+is(typeof speechSynthesis.paused, "boolean", "speechSynthesis.paused is a boolean");
+
+var voices1 = speechSynthesis.getVoices();
+var voices2 = speechSynthesis.getVoices();
+
+ok(!!voices1.length, "More than one voice found");
+ok(voices1.length == voices2.length, "Voice count matches");
+
+for (var i in voices1) {
+ ok(voices1[i] == voices2[i], "Voice instance matches");
+}
+
+SimpleTest.finish();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webspeech/synth/test/file_speech_cancel.html b/dom/media/webspeech/synth/test/file_speech_cancel.html
new file mode 100644
index 0000000000..2ab0e1d0a8
--- /dev/null
+++ b/dom/media/webspeech/synth/test/file_speech_cancel.html
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1150315
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1150315: Check that successive cancel/speak calls work</title>
+ <script type="application/javascript">
+ window.SimpleTest = parent.SimpleTest;
+ window.info = parent.info;
+ window.is = parent.is;
+ window.isnot = parent.isnot;
+ window.ok = parent.ok;
+ </script>
+ <script type="application/javascript" src="common.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1150315">Mozilla Bug 1150315</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 1150315 **/
+
+function testFunc(done_cb) {
+ var gotEndEvent = false;
+ // A long utterance that we will interrupt.
+ var utterance = new SpeechSynthesisUtterance("Donec ac nunc feugiat, posuere " +
+ "mauris id, pharetra velit. Donec fermentum orci nunc, sit amet maximus" +
+ "dui tincidunt ut. Sed ultricies ac nisi a laoreet. Proin interdum," +
+ "libero maximus hendrerit posuere, lorem risus egestas nisl, a" +
+ "ultricies massa justo eu nisi. Duis mattis nibh a ligula tincidunt" +
+ "tincidunt non eu erat. Sed bibendum varius vulputate. Cras leo magna," +
+ "ornare ac posuere vel, luctus id metus. Mauris nec quam ac augue" +
+ "consectetur bibendum. Integer a commodo tortor. Duis semper dolor eu" +
+ "facilisis facilisis. Etiam venenatis turpis est, quis tincidunt velit" +
+ "suscipit a. Cras semper orci in sapien rhoncus bibendum. Suspendisse" +
+ "eu ex lobortis, finibus enim in, condimentum quam. Maecenas eget dui" +
+ "ipsum. Aliquam tortor leo, interdum eget congue ut, tempor id elit.");
+ utterance.addEventListener('start', function(e) {
+ ok(true, 'start utterance 1');
+ speechSynthesis.cancel();
+ info('cancel!');
+ speechSynthesis.speak(utterance2);
+ info('speak??');
+ });
+
+ var utterance2 = new SpeechSynthesisUtterance("Proin ornare neque vitae " +
+ "risus mattis rutrum. Suspendisse a velit ut est convallis aliquet." +
+ "Nullam ante elit, malesuada vel luctus rutrum, ultricies nec libero." +
+ "Praesent eu iaculis orci. Sed nisl diam, sodales ac purus et," +
+ "volutpat interdum tortor. Nullam aliquam porta elit et maximus. Cras" +
+ "risus lectus, elementum vel sodales vel, ultricies eget lectus." +
+ "Curabitur velit lacus, mollis vel finibus et, molestie sit amet" +
+ "sapien. Proin vitae dolor ac augue posuere efficitur ac scelerisque" +
+ "diam. Nulla sed odio elit.");
+ utterance2.addEventListener('start', function() {
+ info('start');
+ speechSynthesis.cancel();
+ speechSynthesis.speak(utterance3);
+ });
+ utterance2.addEventListener('end', function(e) {
+ gotEndEvent = true;
+ });
+
+ var utterance3 = new SpeechSynthesisUtterance("Hello, world 3!");
+ utterance3.addEventListener('start', function() {
+ ok(gotEndEvent, "didn't get start event for this utterance");
+ });
+ utterance3.addEventListener('end', done_cb);
+
+ // Speak/cancel while paused (Bug 1187105)
+ speechSynthesis.pause();
+ speechSynthesis.speak(new SpeechSynthesisUtterance("hello."));
+ ok(speechSynthesis.pending, "paused speechSynthesis has an utterance queued.");
+ speechSynthesis.cancel();
+ ok(!speechSynthesis.pending, "paused speechSynthesis has no utterance queued.");
+ speechSynthesis.resume();
+
+ speechSynthesis.speak(utterance);
+ ok(!speechSynthesis.speaking, "speechSynthesis is not speaking yet.");
+ ok(speechSynthesis.pending, "speechSynthesis has an utterance queued.");
+}
+
+// Run test with no global queue, and then run it with a global queue.
+testFunc(function() {
+ SpecialPowers.pushPrefEnv(
+ { set: [['media.webspeech.synth.force_global_queue', true]] }, function() {
+ testFunc(SimpleTest.finish)
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webspeech/synth/test/file_speech_error.html b/dom/media/webspeech/synth/test/file_speech_error.html
new file mode 100644
index 0000000000..b98ec2fac0
--- /dev/null
+++ b/dom/media/webspeech/synth/test/file_speech_error.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1226015
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1226015</title>
+ <script type="application/javascript">
+ window.SimpleTest = parent.SimpleTest;
+ window.info = parent.info;
+ window.is = parent.is;
+ window.isnot = parent.isnot;
+ window.ok = parent.ok;
+ </script>
+ <script type="application/javascript" src="common.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1226015">Mozilla Bug 1226015</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 1226015 **/
+
+function testFunc(done_cb) {
+ var utterance = new SpeechSynthesisUtterance();
+ utterance.lang = 'it-IT-failatstart';
+
+ speechSynthesis.speak(utterance);
+ speechSynthesis.cancel();
+
+ ok(true, "we didn't crash, that is good.")
+ SimpleTest.finish();
+}
+
+// Run test with no global queue, and then run it with a global queue.
+testFunc();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webspeech/synth/test/file_speech_queue.html b/dom/media/webspeech/synth/test/file_speech_queue.html
new file mode 100644
index 0000000000..a471034dcf
--- /dev/null
+++ b/dom/media/webspeech/synth/test/file_speech_queue.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML>
+<html lang="en-US">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=525444
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 525444: Web Speech API, check speech synth queue</title>
+ <script type="application/javascript">
+ window.SimpleTest = parent.SimpleTest;
+ window.is = parent.is;
+ window.isnot = parent.isnot;
+ window.ok = parent.ok;
+ </script>
+ <script type="application/javascript" src="common.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=525444">Mozilla Bug 525444</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 525444 **/
+
+// XXX: Rate and pitch are not tested.
+
+var langUriMap = {};
+
+for (let voice of speechSynthesis.getVoices()) {
+ langUriMap[voice.lang] = voice.voiceURI;
+ ok(true, voice.lang + ' ' + voice.voiceURI + ' ' + voice.default);
+ is(voice.default, voice.lang == 'en-JM', 'Only Jamaican voice should be default');
+}
+
+ok(langUriMap['en-JM'], 'No English-Jamaican voice');
+ok(langUriMap['en-GB'], 'No English-British voice');
+ok(langUriMap['en-CA'], 'No English-Canadian voice');
+ok(langUriMap['fr-CA'], 'No French-Canadian voice');
+ok(langUriMap['es-MX'], 'No Spanish-Mexican voice');
+ok(langUriMap['it-IT-fail'], 'No Failing Italian voice');
+
+function testFunc(done_cb) {
+ synthTestQueue(
+ [[{text: "Hello, world."},
+ { uri: langUriMap['en-JM'] }],
+ [{text: "Bonjour tout le monde .",
+ args: { lang: "fr", rate: 0.5, pitch: 0.75 }},
+ { uri: langUriMap['fr-CA'], rate: 0.5, pitch: 0.75}],
+ [{text: "How are you doing?", args: { lang: "en-GB" } },
+ { rate: 1, pitch: 1, uri: langUriMap['en-GB']}],
+ [{text: "Come stai?", args: { lang: "it-IT-fail" } },
+ { rate: 1, pitch: 1, uri: langUriMap['it-IT-fail'], err: true }],
+ [{text: "¡hasta mañana!", args: { lang: "es-MX" } },
+ { uri: langUriMap['es-MX'] }]],
+ function () {
+ var test_data = [];
+ var voices = speechSynthesis.getVoices();
+ for (let voice of voices) {
+ if (voice.lang.split("-").length > 2) {
+ // Skip voices that don't automatically end with success
+ continue;
+ }
+ test_data.push([{text: "Hello world", args: { voice} },
+ {uri: voice.voiceURI}]);
+ }
+
+ synthTestQueue(test_data, done_cb);
+ });
+}
+
+// Run test with no global queue, and then run it with a global queue.
+testFunc(function() {
+ SpecialPowers.pushPrefEnv(
+ { set: [['media.webspeech.synth.force_global_queue', true]] }, function() {
+ testFunc(SimpleTest.finish)
+ });
+});
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webspeech/synth/test/file_speech_repeating_utterance.html b/dom/media/webspeech/synth/test/file_speech_repeating_utterance.html
new file mode 100644
index 0000000000..6e37653057
--- /dev/null
+++ b/dom/media/webspeech/synth/test/file_speech_repeating_utterance.html
@@ -0,0 +1,26 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1305344: Utterance not repeating in Firefox</title>
+ <script type="application/javascript">
+ window.SimpleTest = parent.SimpleTest;
+ window.ok = parent.ok;
+ </script>
+ <script src="common.js"></script>
+</head>
+<body>
+ <script>
+ var utterance = new SpeechSynthesisUtterance("repeating?");
+ var counter = 0;
+ utterance.addEventListener('start', function(e) {
+ if (counter++ === 1) {
+ ok(true)
+ SimpleTest.finish();
+ }
+ });
+ speechSynthesis.speak(utterance);
+ speechSynthesis.speak(utterance);
+ </script>
+</body>
+</html>
diff --git a/dom/media/webspeech/synth/test/file_speech_simple.html b/dom/media/webspeech/synth/test/file_speech_simple.html
new file mode 100644
index 0000000000..c3f240ccdc
--- /dev/null
+++ b/dom/media/webspeech/synth/test/file_speech_simple.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=650295
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 650295: Web Speech API check all classes are present</title>
+ <script type="application/javascript">
+ window.SimpleTest = parent.SimpleTest;
+ window.info = parent.info;
+ window.is = parent.is;
+ window.isnot = parent.isnot;
+ window.ok = parent.ok;
+ </script>
+ <script type="application/javascript" src="common.js"></script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=650295">Mozilla Bug 650295</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 525444 **/
+
+var gotStartEvent = false;
+var gotBoundaryEvent = false;
+var utterance = new SpeechSynthesisUtterance("Hello, world!");
+utterance.addEventListener('start', function(e) {
+ ok(speechSynthesis.speaking, "speechSynthesis is speaking.");
+ ok(!speechSynthesis.pending, "speechSynthesis has no other utterances queued.");
+ gotStartEvent = true;
+});
+
+utterance.addEventListener('end', function(e) {
+ ok(!speechSynthesis.speaking, "speechSynthesis is not speaking.");
+ ok(!speechSynthesis.pending, "speechSynthesis has no other utterances queued.");
+ ok(gotStartEvent, "Got 'start' event.");
+ info('end ' + e.elapsedTime);
+ SimpleTest.finish();
+});
+
+speechSynthesis.speak(utterance);
+ok(!speechSynthesis.speaking, "speechSynthesis is not speaking yet.");
+ok(speechSynthesis.pending, "speechSynthesis has an utterance queued.");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webspeech/synth/test/mochitest.ini b/dom/media/webspeech/synth/test/mochitest.ini
new file mode 100644
index 0000000000..2f188dac67
--- /dev/null
+++ b/dom/media/webspeech/synth/test/mochitest.ini
@@ -0,0 +1,29 @@
+[DEFAULT]
+tags=mtg
+subsuite = media
+support-files =
+ common.js
+ file_bfcache_page1.html
+ file_bfcache_page2.html
+ file_setup.html
+ file_speech_queue.html
+ file_speech_simple.html
+ file_speech_cancel.html
+ file_speech_error.html
+ file_indirect_service_events.html
+ file_global_queue.html
+ file_global_queue_cancel.html
+ file_global_queue_pause.html
+ file_speech_repeating_utterance.html
+
+[test_setup.html]
+[test_speech_queue.html]
+[test_speech_simple.html]
+[test_speech_cancel.html]
+[test_speech_error.html]
+[test_indirect_service_events.html]
+[test_global_queue.html]
+[test_global_queue_cancel.html]
+[test_global_queue_pause.html]
+[test_bfcache.html]
+[test_speech_repeating_utterance.html]
diff --git a/dom/media/webspeech/synth/test/nsFakeSynthServices.cpp b/dom/media/webspeech/synth/test/nsFakeSynthServices.cpp
new file mode 100644
index 0000000000..075e8aa878
--- /dev/null
+++ b/dom/media/webspeech/synth/test/nsFakeSynthServices.cpp
@@ -0,0 +1,288 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.h"
+#include "nsFakeSynthServices.h"
+#include "nsPrintfCString.h"
+#include "SharedBuffer.h"
+
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/dom/nsSynthVoiceRegistry.h"
+#include "mozilla/dom/nsSpeechTask.h"
+
+#include "nsThreadUtils.h"
+#include "nsXULAppAPI.h"
+#include "prenv.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/DebugOnly.h"
+
+#define CHANNELS 1
+#define SAMPLERATE 1600
+
+namespace mozilla::dom {
+
+StaticRefPtr<nsFakeSynthServices> nsFakeSynthServices::sSingleton;
+
+enum VoiceFlags {
+ eSuppressEvents = 1,
+ eSuppressEnd = 2,
+ eFailAtStart = 4,
+ eFail = 8
+};
+
+struct VoiceDetails {
+ const char* uri;
+ const char* name;
+ const char* lang;
+ bool defaultVoice;
+ uint32_t flags;
+};
+
+static const VoiceDetails sVoices[] = {
+ {"urn:moz-tts:fake:bob", "Bob Marley", "en-JM", true, 0},
+ {"urn:moz-tts:fake:amy", "Amy Winehouse", "en-GB", false, 0},
+ {"urn:moz-tts:fake:lenny", "Leonard Cohen", "en-CA", false, 0},
+ {"urn:moz-tts:fake:celine", "Celine Dion", "fr-CA", false, 0},
+ {
+ "urn:moz-tts:fake:julie",
+ "Julieta Venegas",
+ "es-MX",
+ false,
+ },
+ {"urn:moz-tts:fake:zanetta", "Zanetta Farussi", "it-IT", false, 0},
+ {"urn:moz-tts:fake:margherita", "Margherita Durastanti",
+ "it-IT-noevents-noend", false, eSuppressEvents | eSuppressEnd},
+ {"urn:moz-tts:fake:teresa", "Teresa Cornelys", "it-IT-noend", false,
+ eSuppressEnd},
+ {"urn:moz-tts:fake:cecilia", "Cecilia Bartoli", "it-IT-failatstart", false,
+ eFailAtStart},
+ {"urn:moz-tts:fake:gottardo", "Gottardo Aldighieri", "it-IT-fail", false,
+ eFail},
+};
+
+// FakeSynthCallback
+class FakeSynthCallback : public nsISpeechTaskCallback {
+ public:
+ explicit FakeSynthCallback(nsISpeechTask* aTask) : mTask(aTask) {}
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(FakeSynthCallback,
+ nsISpeechTaskCallback)
+
+ NS_IMETHOD OnPause() override {
+ if (mTask) {
+ mTask->DispatchPause(1.5, 1);
+ }
+
+ return NS_OK;
+ }
+
+ NS_IMETHOD OnResume() override {
+ if (mTask) {
+ mTask->DispatchResume(1.5, 1);
+ }
+
+ return NS_OK;
+ }
+
+ NS_IMETHOD OnCancel() override {
+ if (mTask) {
+ mTask->DispatchEnd(1.5, 1);
+ }
+
+ return NS_OK;
+ }
+
+ NS_IMETHOD OnVolumeChanged(float aVolume) override { return NS_OK; }
+
+ private:
+ virtual ~FakeSynthCallback() = default;
+
+ nsCOMPtr<nsISpeechTask> mTask;
+};
+
+NS_IMPL_CYCLE_COLLECTION(FakeSynthCallback, mTask);
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(FakeSynthCallback)
+ NS_INTERFACE_MAP_ENTRY(nsISpeechTaskCallback)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsISpeechTaskCallback)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(FakeSynthCallback)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(FakeSynthCallback)
+
+// FakeSpeechSynth
+
+class FakeSpeechSynth : public nsISpeechService {
+ public:
+ FakeSpeechSynth() = default;
+
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISPEECHSERVICE
+
+ private:
+ virtual ~FakeSpeechSynth() = default;
+};
+
+NS_IMPL_ISUPPORTS(FakeSpeechSynth, nsISpeechService)
+
+NS_IMETHODIMP
+FakeSpeechSynth::Speak(const nsAString& aText, const nsAString& aUri,
+ float aVolume, float aRate, float aPitch,
+ nsISpeechTask* aTask) {
+ class DispatchStart final : public Runnable {
+ public:
+ explicit DispatchStart(nsISpeechTask* aTask)
+ : mozilla::Runnable("DispatchStart"), mTask(aTask) {}
+
+ NS_IMETHOD Run() override {
+ mTask->DispatchStart();
+
+ return NS_OK;
+ }
+
+ private:
+ nsCOMPtr<nsISpeechTask> mTask;
+ };
+
+ class DispatchEnd final : public Runnable {
+ public:
+ DispatchEnd(nsISpeechTask* aTask, const nsAString& aText)
+ : mozilla::Runnable("DispatchEnd"), mTask(aTask), mText(aText) {}
+
+ NS_IMETHOD Run() override {
+ mTask->DispatchEnd(mText.Length() / 2, mText.Length());
+
+ return NS_OK;
+ }
+
+ private:
+ nsCOMPtr<nsISpeechTask> mTask;
+ nsString mText;
+ };
+
+ class DispatchError final : public Runnable {
+ public:
+ DispatchError(nsISpeechTask* aTask, const nsAString& aText)
+ : mozilla::Runnable("DispatchError"), mTask(aTask), mText(aText) {}
+
+ NS_IMETHOD Run() override {
+ mTask->DispatchError(mText.Length() / 2, mText.Length());
+
+ return NS_OK;
+ }
+
+ private:
+ nsCOMPtr<nsISpeechTask> mTask;
+ nsString mText;
+ };
+
+ uint32_t flags = 0;
+ for (VoiceDetails voice : sVoices) {
+ if (aUri.EqualsASCII(voice.uri)) {
+ flags = voice.flags;
+ break;
+ }
+ }
+
+ if (flags & eFailAtStart) {
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<FakeSynthCallback> cb =
+ new FakeSynthCallback((flags & eSuppressEvents) ? nullptr : aTask);
+
+ aTask->Setup(cb);
+
+ nsCOMPtr<nsIRunnable> runnable = new DispatchStart(aTask);
+ NS_DispatchToMainThread(runnable);
+
+ if (flags & eFail) {
+ runnable = new DispatchError(aTask, aText);
+ NS_DispatchToMainThread(runnable);
+ } else if ((flags & eSuppressEnd) == 0) {
+ runnable = new DispatchEnd(aTask, aText);
+ NS_DispatchToMainThread(runnable);
+ }
+
+ return NS_OK;
+}
+
+// nsFakeSynthService
+
+NS_INTERFACE_MAP_BEGIN(nsFakeSynthServices)
+ NS_INTERFACE_MAP_ENTRY(nsIObserver)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_ADDREF(nsFakeSynthServices)
+NS_IMPL_RELEASE(nsFakeSynthServices)
+
+static void AddVoices(nsISpeechService* aService, const VoiceDetails* aVoices,
+ uint32_t aLength) {
+ RefPtr<nsSynthVoiceRegistry> registry = nsSynthVoiceRegistry::GetInstance();
+ for (uint32_t i = 0; i < aLength; i++) {
+ NS_ConvertUTF8toUTF16 name(aVoices[i].name);
+ NS_ConvertUTF8toUTF16 uri(aVoices[i].uri);
+ NS_ConvertUTF8toUTF16 lang(aVoices[i].lang);
+ // These services can handle more than one utterance at a time and have
+ // several speaking simultaniously. So, aQueuesUtterances == false
+ registry->AddVoice(aService, uri, name, lang, true, false);
+ if (aVoices[i].defaultVoice) {
+ registry->SetDefaultVoice(uri, true);
+ }
+ }
+
+ registry->NotifyVoicesChanged();
+}
+
+void nsFakeSynthServices::Init() {
+ mSynthService = new FakeSpeechSynth();
+ AddVoices(mSynthService, sVoices, ArrayLength(sVoices));
+}
+
+// nsIObserver
+
+NS_IMETHODIMP
+nsFakeSynthServices::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (NS_WARN_IF(!(!strcmp(aTopic, "speech-synth-started")))) {
+ return NS_ERROR_UNEXPECTED;
+ }
+
+ if (Preferences::GetBool("media.webspeech.synth.test")) {
+ NS_DispatchToMainThread(NewRunnableMethod(
+ "dom::nsFakeSynthServices::Init", this, &nsFakeSynthServices::Init));
+ }
+
+ return NS_OK;
+}
+
+// static methods
+
+nsFakeSynthServices* nsFakeSynthServices::GetInstance() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!XRE_IsParentProcess()) {
+ MOZ_ASSERT(false,
+ "nsFakeSynthServices can only be started on main gecko process");
+ return nullptr;
+ }
+
+ if (!sSingleton) {
+ sSingleton = new nsFakeSynthServices();
+ ClearOnShutdown(&sSingleton);
+ }
+
+ return sSingleton;
+}
+
+already_AddRefed<nsFakeSynthServices>
+nsFakeSynthServices::GetInstanceForService() {
+ RefPtr<nsFakeSynthServices> picoService = GetInstance();
+ return picoService.forget();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webspeech/synth/test/nsFakeSynthServices.h b/dom/media/webspeech/synth/test/nsFakeSynthServices.h
new file mode 100644
index 0000000000..f7e1ca7da6
--- /dev/null
+++ b/dom/media/webspeech/synth/test/nsFakeSynthServices.h
@@ -0,0 +1,42 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef nsFakeSynthServices_h
+#define nsFakeSynthServices_h
+
+#include "nsTArray.h"
+#include "nsIObserver.h"
+#include "nsISpeechService.h"
+#include "nsRefPtrHashtable.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/Monitor.h"
+
+namespace mozilla::dom {
+
+class nsFakeSynthServices : public nsIObserver {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+ nsFakeSynthServices() = default;
+
+ static nsFakeSynthServices* GetInstance();
+
+ static already_AddRefed<nsFakeSynthServices> GetInstanceForService();
+
+ private:
+ virtual ~nsFakeSynthServices() = default;
+
+ void Init();
+
+ nsCOMPtr<nsISpeechService> mSynthService;
+
+ static StaticRefPtr<nsFakeSynthServices> sSingleton;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webspeech/synth/test/startup/file_voiceschanged.html b/dom/media/webspeech/synth/test/startup/file_voiceschanged.html
new file mode 100644
index 0000000000..6bb25462e4
--- /dev/null
+++ b/dom/media/webspeech/synth/test/startup/file_voiceschanged.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1254378
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1254378: Web Speech API check all classes are present</title>
+ <script type="application/javascript">
+ window.SimpleTest = parent.SimpleTest;
+ window.is = parent.is;
+ window.isnot = parent.isnot;
+ window.ok = parent.ok;
+ </script>
+</head>
+<body>
+<script type="application/javascript">
+
+/** Test for Bug 1254378 **/
+
+function onVoicesChanged() {
+ isnot(speechSynthesis.getVoices().length, 0, "Voices added");
+ speechSynthesis.removeEventListener("voiceschanged", onVoicesChanged);
+ SimpleTest.finish();
+}
+
+speechSynthesis.addEventListener("voiceschanged", onVoicesChanged);
+
+is(speechSynthesis.getVoices().length, 0, "No voices added initially");
+</script>
+</body>
+</html>
diff --git a/dom/media/webspeech/synth/test/startup/mochitest.ini b/dom/media/webspeech/synth/test/startup/mochitest.ini
new file mode 100644
index 0000000000..ec4285b772
--- /dev/null
+++ b/dom/media/webspeech/synth/test/startup/mochitest.ini
@@ -0,0 +1,8 @@
+[DEFAULT]
+tags=mtg
+subsuite = media
+support-files =
+ file_voiceschanged.html
+
+[test_voiceschanged.html]
+skip-if = verify
diff --git a/dom/media/webspeech/synth/test/startup/test_voiceschanged.html b/dom/media/webspeech/synth/test/startup/test_voiceschanged.html
new file mode 100644
index 0000000000..a60252ea7e
--- /dev/null
+++ b/dom/media/webspeech/synth/test/startup/test_voiceschanged.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1254378
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1254378: Emit onvoiceschanged when voices first added</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1254378">Mozilla Bug 1254378</a>
+<p id="display"></p>
+<iframe id="testFrame"></iframe>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 1254378 **/
+
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv({ set: [['media.webspeech.synth.enabled', true]] },
+ function() { document.getElementById("testFrame").src = "file_voiceschanged.html"; });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webspeech/synth/test/test_bfcache.html b/dom/media/webspeech/synth/test/test_bfcache.html
new file mode 100644
index 0000000000..ba5981a42b
--- /dev/null
+++ b/dom/media/webspeech/synth/test/test_bfcache.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1230533
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1230533: Test speech is stopped from a window when unloaded</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1230533">Mozilla Bug 1230533</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 525444 **/
+
+SimpleTest.waitForExplicitFinish();
+let testWin;
+
+function onDone() {
+ testWin.close();
+ SimpleTest.finish();
+}
+
+SpecialPowers.pushPrefEnv({ set: [
+ ['media.webspeech.synth.enabled', true],
+ ['media.webspeech.synth.force_global_queue', true]] },
+ function() {
+ testWin = window.open("about:blank", "testWin");
+ testWin.onload = function(e) {
+ waitForVoices(testWin)
+ .then(() => testWin.location = "file_bfcache_page1.html")
+ };
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webspeech/synth/test/test_global_queue.html b/dom/media/webspeech/synth/test/test_global_queue.html
new file mode 100644
index 0000000000..177f79b399
--- /dev/null
+++ b/dom/media/webspeech/synth/test/test_global_queue.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1188099
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1188099: Global queue should correctly schedule utterances</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1188099">Mozilla Bug 1188099</a>
+<p id="display"></p>
+<iframe id="testFrame"></iframe>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 525444 **/
+
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv(
+ { set: [['media.webspeech.synth.enabled', true],
+ ['media.webspeech.synth.force_global_queue', true]] },
+ function() { loadSpeechTest("file_global_queue.html"); });
+
+</script>
+</pre>
+</body>
+</html> \ No newline at end of file
diff --git a/dom/media/webspeech/synth/test/test_global_queue_cancel.html b/dom/media/webspeech/synth/test/test_global_queue_cancel.html
new file mode 100644
index 0000000000..748d1367b5
--- /dev/null
+++ b/dom/media/webspeech/synth/test/test_global_queue_cancel.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1188099
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1188099: Calling cancel() should work correctly with global queue</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1188099">Mozilla Bug 1188099</a>
+<p id="display"></p>
+<iframe id="testFrame"></iframe>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 525444 **/
+
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv(
+ { set: [['media.webspeech.synth.enabled', true],
+ ['media.webspeech.synth.force_global_queue', true]] },
+ function() { loadSpeechTest("file_global_queue_cancel.html"); });
+
+</script>
+</pre>
+</body>
+</html> \ No newline at end of file
diff --git a/dom/media/webspeech/synth/test/test_global_queue_pause.html b/dom/media/webspeech/synth/test/test_global_queue_pause.html
new file mode 100644
index 0000000000..9632d85127
--- /dev/null
+++ b/dom/media/webspeech/synth/test/test_global_queue_pause.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1188099
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1188099: Calling pause() should work correctly with global queue</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1188099">Mozilla Bug 1188099</a>
+<p id="display"></p>
+<iframe id="testFrame"></iframe>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 525444 **/
+
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv(
+ { set: [['media.webspeech.synth.enabled', true],
+ ['media.webspeech.synth.force_global_queue', true]] },
+ function() { loadSpeechTest("file_global_queue_pause.html"); });
+
+</script>
+</pre>
+</body>
+</html> \ No newline at end of file
diff --git a/dom/media/webspeech/synth/test/test_indirect_service_events.html b/dom/media/webspeech/synth/test/test_indirect_service_events.html
new file mode 100644
index 0000000000..e5b32e70f0
--- /dev/null
+++ b/dom/media/webspeech/synth/test/test_indirect_service_events.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1155034
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1155034: Check that indirect audio services dispatch their own events</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1155034">Mozilla Bug 1155034</a>
+<p id="display"></p>
+<iframe id="testFrame"></iframe>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 1155034 **/
+
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv(
+ { set: [['media.webspeech.synth.enabled', true],
+ ['media.webspeech.synth.force_global_queue', false]] },
+ function() { loadSpeechTest("file_indirect_service_events.html"); });
+
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webspeech/synth/test/test_setup.html b/dom/media/webspeech/synth/test/test_setup.html
new file mode 100644
index 0000000000..da07687750
--- /dev/null
+++ b/dom/media/webspeech/synth/test/test_setup.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=525444
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 525444: Web Speech API check all classes are present</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=650295">Mozilla Bug 650295</a>
+<p id="display"></p>
+<iframe id="testFrame"></iframe>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 525444 **/
+
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv({ set: [['media.webspeech.synth.enabled', true]] },
+ function() { document.getElementById("testFrame").src = "file_setup.html"; });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webspeech/synth/test/test_speech_cancel.html b/dom/media/webspeech/synth/test/test_speech_cancel.html
new file mode 100644
index 0000000000..ced952c736
--- /dev/null
+++ b/dom/media/webspeech/synth/test/test_speech_cancel.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1150315
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1150315: Web Speech API check all classes are present</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1150315">Mozilla Bug 1150315</a>
+<p id="display"></p>
+<iframe id="testFrame"></iframe>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 1150315 **/
+
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv(
+ { set: [['media.webspeech.synth.enabled', true],
+ ['media.webspeech.synth.force_global_queue', false]] },
+ function() { loadSpeechTest("file_speech_cancel.html"); });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webspeech/synth/test/test_speech_error.html b/dom/media/webspeech/synth/test/test_speech_error.html
new file mode 100644
index 0000000000..e2ce156dc6
--- /dev/null
+++ b/dom/media/webspeech/synth/test/test_speech_error.html
@@ -0,0 +1,35 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1226015
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1150315: Web Speech API check all classes are present</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1226015">Mozilla Bug 1226015</a>
+<p id="display"></p>
+<iframe id="testFrame"></iframe>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 1226015 **/
+
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv(
+ { set: [['media.webspeech.synth.enabled', true],
+ ['media.webspeech.synth.force_global_queue', false]] },
+ function() { loadSpeechTest("file_speech_error.html"); });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webspeech/synth/test/test_speech_queue.html b/dom/media/webspeech/synth/test/test_speech_queue.html
new file mode 100644
index 0000000000..3bca9e0ce2
--- /dev/null
+++ b/dom/media/webspeech/synth/test/test_speech_queue.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html lang="en-US">
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=525444
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 525444: Web Speech API, check speech synth queue</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=525444">Mozilla Bug 525444</a>
+<p id="display"></p>
+<iframe id="testFrame"></iframe>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 525444 **/
+
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv(
+ { set: [['media.webspeech.synth.enabled', true],
+ ['media.webspeech.synth.force_global_queue', false]] },
+ function() {
+ loadSpeechTest("file_speech_queue.html");
+ });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webspeech/synth/test/test_speech_repeating_utterance.html b/dom/media/webspeech/synth/test/test_speech_repeating_utterance.html
new file mode 100644
index 0000000000..6313a275c1
--- /dev/null
+++ b/dom/media/webspeech/synth/test/test_speech_repeating_utterance.html
@@ -0,0 +1,18 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1305344: Utterance not repeating in Firefox</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="common.js"></script>
+ <link rel="stylesheet" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+ <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1305344">Mozilla Bug 1305344</a>
+ <iframe id="testFrame"></iframe>
+ <script>
+ SimpleTest.waitForExplicitFinish();
+ loadSpeechTest('file_speech_repeating_utterance.html');
+ </script>
+</body>
+</html>
diff --git a/dom/media/webspeech/synth/test/test_speech_simple.html b/dom/media/webspeech/synth/test/test_speech_simple.html
new file mode 100644
index 0000000000..c6c0e3a5be
--- /dev/null
+++ b/dom/media/webspeech/synth/test/test_speech_simple.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=650295
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 650295: Web Speech API check all classes are present</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="application/javascript" src="common.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=650295">Mozilla Bug 650295</a>
+<p id="display"></p>
+<iframe id="testFrame"></iframe>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 525444 **/
+
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv(
+ { set: [['media.webspeech.synth.enabled', true]] },
+ function() { loadSpeechTest("file_speech_simple.html"); });
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webspeech/synth/windows/SapiService.cpp b/dom/media/webspeech/synth/windows/SapiService.cpp
new file mode 100644
index 0000000000..f1e44213d1
--- /dev/null
+++ b/dom/media/webspeech/synth/windows/SapiService.cpp
@@ -0,0 +1,445 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.h"
+#include "SapiService.h"
+#include "nsServiceManagerUtils.h"
+#include "nsEscape.h"
+#include "nsXULAppAPI.h"
+
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/dom/nsSynthVoiceRegistry.h"
+#include "mozilla/dom/nsSpeechTask.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/ProfilerLabels.h"
+#include "mozilla/StaticPrefs_media.h"
+
+namespace mozilla::dom {
+
+constexpr static WCHAR kSpCategoryOneCoreVoices[] =
+ L"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech_OneCore\\Voices";
+
+StaticRefPtr<SapiService> SapiService::sSingleton;
+
+class SapiCallback final : public nsISpeechTaskCallback {
+ public:
+ SapiCallback(nsISpeechTask* aTask, ISpVoice* aSapiClient,
+ uint32_t aTextOffset, uint32_t aSpeakTextLen)
+ : mTask(aTask),
+ mSapiClient(aSapiClient),
+ mTextOffset(aTextOffset),
+ mSpeakTextLen(aSpeakTextLen),
+ mCurrentIndex(0),
+ mStreamNum(0) {
+ mStartingTime = TimeStamp::Now();
+ }
+
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(SapiCallback, nsISpeechTaskCallback)
+
+ NS_DECL_NSISPEECHTASKCALLBACK
+
+ ULONG GetStreamNum() const { return mStreamNum; }
+ void SetStreamNum(ULONG aValue) { mStreamNum = aValue; }
+
+ void OnSpeechEvent(const SPEVENT& speechEvent);
+
+ private:
+ ~SapiCallback() {}
+
+ float GetTimeDurationFromStart() const {
+ TimeDuration duration = TimeStamp::Now() - mStartingTime;
+ return duration.ToSeconds();
+ }
+
+ // This pointer is used to dispatch events
+ nsCOMPtr<nsISpeechTask> mTask;
+ RefPtr<ISpVoice> mSapiClient;
+
+ uint32_t mTextOffset;
+ uint32_t mSpeakTextLen;
+
+ // Used for calculating the time taken to speak the utterance
+ TimeStamp mStartingTime;
+ uint32_t mCurrentIndex;
+
+ ULONG mStreamNum;
+};
+
+NS_IMPL_CYCLE_COLLECTION(SapiCallback, mTask);
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SapiCallback)
+ NS_INTERFACE_MAP_ENTRY(nsISpeechTaskCallback)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsISpeechTaskCallback)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(SapiCallback)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(SapiCallback)
+
+NS_IMETHODIMP
+SapiCallback::OnPause() {
+ if (FAILED(mSapiClient->Pause())) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!mTask) {
+ // When calling pause() on child porcess, it may not receive end event
+ // from chrome process yet.
+ return NS_ERROR_FAILURE;
+ }
+ mTask->DispatchPause(GetTimeDurationFromStart(), mCurrentIndex);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SapiCallback::OnResume() {
+ if (FAILED(mSapiClient->Resume())) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!mTask) {
+ // When calling resume() on child porcess, it may not receive end event
+ // from chrome process yet.
+ return NS_ERROR_FAILURE;
+ }
+ mTask->DispatchResume(GetTimeDurationFromStart(), mCurrentIndex);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SapiCallback::OnCancel() {
+ // After cancel, mCurrentIndex may be updated.
+ // At cancel case, use mCurrentIndex for DispatchEnd.
+ mSpeakTextLen = 0;
+ // Purge all the previous utterances and speak an empty string
+ if (FAILED(mSapiClient->Speak(nullptr, SPF_PURGEBEFORESPEAK, nullptr))) {
+ return NS_ERROR_FAILURE;
+ }
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SapiCallback::OnVolumeChanged(float aVolume) {
+ mSapiClient->SetVolume(static_cast<USHORT>(aVolume * 100));
+ return NS_OK;
+}
+
+void SapiCallback::OnSpeechEvent(const SPEVENT& speechEvent) {
+ switch (speechEvent.eEventId) {
+ case SPEI_START_INPUT_STREAM:
+ mTask->DispatchStart();
+ break;
+ case SPEI_END_INPUT_STREAM:
+ if (mSpeakTextLen) {
+ mCurrentIndex = mSpeakTextLen;
+ }
+ mTask->DispatchEnd(GetTimeDurationFromStart(), mCurrentIndex);
+ mTask = nullptr;
+ break;
+ case SPEI_TTS_BOOKMARK:
+ mCurrentIndex = static_cast<ULONG>(speechEvent.lParam) - mTextOffset;
+ mTask->DispatchBoundary(u"mark"_ns, GetTimeDurationFromStart(),
+ mCurrentIndex, 0, 0);
+ break;
+ case SPEI_WORD_BOUNDARY:
+ mCurrentIndex = static_cast<ULONG>(speechEvent.lParam) - mTextOffset;
+ mTask->DispatchBoundary(u"word"_ns, GetTimeDurationFromStart(),
+ mCurrentIndex,
+ static_cast<ULONG>(speechEvent.wParam), 1);
+ break;
+ case SPEI_SENTENCE_BOUNDARY:
+ mCurrentIndex = static_cast<ULONG>(speechEvent.lParam) - mTextOffset;
+ mTask->DispatchBoundary(u"sentence"_ns, GetTimeDurationFromStart(),
+ mCurrentIndex,
+ static_cast<ULONG>(speechEvent.wParam), 1);
+ break;
+ default:
+ break;
+ }
+}
+
+// static
+void __stdcall SapiService::SpeechEventCallback(WPARAM aWParam,
+ LPARAM aLParam) {
+ RefPtr<ISpVoice> spVoice = (ISpVoice*)aWParam;
+ RefPtr<SapiService> service = (SapiService*)aLParam;
+
+ SPEVENT speechEvent;
+ while (spVoice->GetEvents(1, &speechEvent, nullptr) == S_OK) {
+ for (size_t i = 0; i < service->mCallbacks.Length(); i++) {
+ RefPtr<SapiCallback> callback = service->mCallbacks[i];
+ if (callback->GetStreamNum() == speechEvent.ulStreamNum) {
+ callback->OnSpeechEvent(speechEvent);
+ if (speechEvent.eEventId == SPEI_END_INPUT_STREAM) {
+ service->mCallbacks.RemoveElementAt(i);
+ }
+ break;
+ }
+ }
+ }
+}
+
+NS_INTERFACE_MAP_BEGIN(SapiService)
+ NS_INTERFACE_MAP_ENTRY(nsISpeechService)
+ NS_INTERFACE_MAP_ENTRY(nsIObserver)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsISpeechService)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_ADDREF(SapiService)
+NS_IMPL_RELEASE(SapiService)
+
+SapiService::SapiService() : mInitialized(false) {}
+
+SapiService::~SapiService() {}
+
+bool SapiService::Init() {
+ AUTO_PROFILER_LABEL("SapiService::Init", OTHER);
+
+ MOZ_ASSERT(!mInitialized);
+
+ if (Preferences::GetBool("media.webspeech.synth.test") ||
+ !StaticPrefs::media_webspeech_synth_enabled()) {
+ // When enabled, we shouldn't add OS backend (Bug 1160844)
+ return false;
+ }
+
+ // Get all the voices from sapi and register in the SynthVoiceRegistry
+ if (!RegisterVoices()) {
+ return false;
+ }
+
+ mInitialized = true;
+ return true;
+}
+
+already_AddRefed<ISpVoice> SapiService::InitSapiInstance() {
+ RefPtr<ISpVoice> spVoice;
+ if (FAILED(CoCreateInstance(CLSID_SpVoice, nullptr, CLSCTX_ALL, IID_ISpVoice,
+ getter_AddRefs(spVoice)))) {
+ return nullptr;
+ }
+
+ // Set interest for all the events we are interested in
+ ULONGLONG eventMask = SPFEI(SPEI_START_INPUT_STREAM) |
+ SPFEI(SPEI_TTS_BOOKMARK) | SPFEI(SPEI_WORD_BOUNDARY) |
+ SPFEI(SPEI_SENTENCE_BOUNDARY) |
+ SPFEI(SPEI_END_INPUT_STREAM);
+
+ if (FAILED(spVoice->SetInterest(eventMask, eventMask))) {
+ return nullptr;
+ }
+
+ // Set the callback function for receiving the events
+ spVoice->SetNotifyCallbackFunction(
+ (SPNOTIFYCALLBACK*)SapiService::SpeechEventCallback,
+ (WPARAM)spVoice.get(), (LPARAM)this);
+
+ return spVoice.forget();
+}
+
+bool SapiService::RegisterVoices() {
+ nsCOMPtr<nsISynthVoiceRegistry> registry =
+ do_GetService(NS_SYNTHVOICEREGISTRY_CONTRACTID);
+ if (!registry) {
+ return false;
+ }
+ bool result = RegisterVoices(registry, kSpCategoryOneCoreVoices);
+ result |= RegisterVoices(registry, SPCAT_VOICES);
+ if (result) {
+ registry->NotifyVoicesChanged();
+ }
+ return result;
+}
+
+bool SapiService::RegisterVoices(nsCOMPtr<nsISynthVoiceRegistry>& registry,
+ const WCHAR* categoryId) {
+ nsresult rv;
+
+ RefPtr<ISpObjectTokenCategory> category;
+ if (FAILED(CoCreateInstance(CLSID_SpObjectTokenCategory, nullptr, CLSCTX_ALL,
+ IID_ISpObjectTokenCategory,
+ getter_AddRefs(category)))) {
+ return false;
+ }
+ if (FAILED(category->SetId(categoryId, FALSE))) {
+ return false;
+ }
+
+ RefPtr<IEnumSpObjectTokens> voiceTokens;
+ if (FAILED(category->EnumTokens(nullptr, nullptr,
+ getter_AddRefs(voiceTokens)))) {
+ return false;
+ }
+
+ WCHAR locale[LOCALE_NAME_MAX_LENGTH];
+ while (true) {
+ RefPtr<ISpObjectToken> voiceToken;
+ if (voiceTokens->Next(1, getter_AddRefs(voiceToken), nullptr) != S_OK) {
+ break;
+ }
+
+ RefPtr<ISpDataKey> attributes;
+ if (FAILED(
+ voiceToken->OpenKey(L"Attributes", getter_AddRefs(attributes)))) {
+ continue;
+ }
+
+ WCHAR* language = nullptr;
+ if (FAILED(attributes->GetStringValue(L"Language", &language))) {
+ continue;
+ }
+
+ // Language attribute is LCID by hex. So we need convert to locale
+ // name.
+ nsAutoString hexLcid;
+ LCID lcid = wcstol(language, nullptr, 16);
+ CoTaskMemFree(language);
+ if (NS_WARN_IF(
+ !LCIDToLocaleName(lcid, locale, LOCALE_NAME_MAX_LENGTH, 0))) {
+ continue;
+ }
+
+ WCHAR* description = nullptr;
+ if (FAILED(voiceToken->GetStringValue(nullptr, &description))) {
+ continue;
+ }
+
+ nsAutoString uri;
+ uri.AssignLiteral("urn:moz-tts:sapi:");
+ uri.Append(description);
+ uri.AppendLiteral("?");
+ uri.Append(locale);
+
+ // This service can only speak one utterance at a time, se we set
+ // aQueuesUtterances to true in order to track global state and schedule
+ // access to this service.
+ rv = registry->AddVoice(this, uri, nsDependentString(description),
+ nsDependentString(locale), true, true);
+ CoTaskMemFree(description);
+ if (NS_FAILED(rv)) {
+ continue;
+ }
+
+ mVoices.InsertOrUpdate(uri, std::move(voiceToken));
+ }
+
+ return true;
+}
+
+NS_IMETHODIMP
+SapiService::Speak(const nsAString& aText, const nsAString& aUri, float aVolume,
+ float aRate, float aPitch, nsISpeechTask* aTask) {
+ NS_ENSURE_TRUE(mInitialized, NS_ERROR_NOT_AVAILABLE);
+
+ RefPtr<ISpObjectToken> voiceToken;
+ if (!mVoices.Get(aUri, getter_AddRefs(voiceToken))) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ RefPtr<ISpVoice> spVoice = InitSapiInstance();
+ if (!spVoice) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (FAILED(spVoice->SetVoice(voiceToken))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (FAILED(spVoice->SetVolume(static_cast<USHORT>(aVolume * 100)))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // The max supported rate in SAPI engines is 3x, and the min is 1/3x. It is
+ // expressed by an integer. 0 being normal rate, -10 is 1/3 and 10 is 3x.
+ // Values below and above that are allowed, but the engine may clip the rate
+ // to its maximum capable value.
+ // "Each increment between -10 and +10 is logarithmically distributed such
+ // that incrementing or decrementing by 1 is multiplying or dividing the
+ // rate by the 10th root of 3"
+ // https://msdn.microsoft.com/en-us/library/ee431826(v=vs.85).aspx
+ long rate = aRate != 0 ? static_cast<long>(10 * log10(aRate) / log10(3)) : 0;
+ if (FAILED(spVoice->SetRate(rate))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Set the pitch using xml
+ nsAutoString xml;
+ xml.AssignLiteral("<pitch absmiddle=\"");
+ // absmiddle doesn't allow float type
+ xml.AppendInt(static_cast<int32_t>(aPitch * 10.0f - 10.0f));
+ xml.AppendLiteral("\">");
+ uint32_t textOffset = xml.Length();
+
+ for (size_t i = 0; i < aText.Length(); i++) {
+ switch (aText[i]) {
+ case '&':
+ xml.AppendLiteral("&amp;");
+ break;
+ case '<':
+ xml.AppendLiteral("&lt;");
+ break;
+ case '>':
+ xml.AppendLiteral("&gt;");
+ break;
+ default:
+ xml.Append(aText[i]);
+ break;
+ }
+ }
+
+ xml.AppendLiteral("</pitch>");
+
+ RefPtr<SapiCallback> callback =
+ new SapiCallback(aTask, spVoice, textOffset, aText.Length());
+
+ // The last three parameters doesn't matter for an indirect service
+ nsresult rv = aTask->Setup(callback);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ ULONG streamNum;
+ if (FAILED(spVoice->Speak(xml.get(), SPF_ASYNC, &streamNum))) {
+ aTask->Setup(nullptr);
+ return NS_ERROR_FAILURE;
+ }
+
+ callback->SetStreamNum(streamNum);
+ // streamNum reassigns same value when last stream is finished even if
+ // callback for stream end isn't called
+ // So we cannot use data hashtable and has to add it to vector at last.
+ mCallbacks.AppendElement(callback);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+SapiService::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ return NS_OK;
+}
+
+SapiService* SapiService::GetInstance() {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (XRE_GetProcessType() != GeckoProcessType_Default) {
+ MOZ_ASSERT(false, "SapiService can only be started on main gecko process");
+ return nullptr;
+ }
+
+ if (!sSingleton) {
+ RefPtr<SapiService> service = new SapiService();
+ if (service->Init()) {
+ sSingleton = service;
+ ClearOnShutdown(&sSingleton);
+ }
+ }
+ return sSingleton;
+}
+
+already_AddRefed<SapiService> SapiService::GetInstanceForService() {
+ RefPtr<SapiService> sapiService = GetInstance();
+ return sapiService.forget();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webspeech/synth/windows/SapiService.h b/dom/media/webspeech/synth/windows/SapiService.h
new file mode 100644
index 0000000000..79cc20917b
--- /dev/null
+++ b/dom/media/webspeech/synth/windows/SapiService.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_SapiService_h
+#define mozilla_dom_SapiService_h
+
+#include "nsISpeechService.h"
+#include "nsIObserver.h"
+#include "nsRefPtrHashtable.h"
+#include "nsTArray.h"
+#include "mozilla/StaticPtr.h"
+
+#include <windows.h>
+#include <sapi.h>
+
+class nsISynthVoiceRegistry;
+
+namespace mozilla::dom {
+
+class SapiCallback;
+
+class SapiService final : public nsISpeechService, public nsIObserver {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSISPEECHSERVICE
+ NS_DECL_NSIOBSERVER
+
+ SapiService();
+ bool Init();
+
+ static SapiService* GetInstance();
+ static already_AddRefed<SapiService> GetInstanceForService();
+
+ static void __stdcall SpeechEventCallback(WPARAM aWParam, LPARAM aLParam);
+
+ private:
+ virtual ~SapiService();
+
+ already_AddRefed<ISpVoice> InitSapiInstance();
+ bool RegisterVoices();
+ bool RegisterVoices(nsCOMPtr<nsISynthVoiceRegistry>& registry,
+ const WCHAR* categoryId);
+
+ nsRefPtrHashtable<nsStringHashKey, ISpObjectToken> mVoices;
+ nsTArray<RefPtr<SapiCallback>> mCallbacks;
+
+ bool mInitialized;
+
+ static StaticRefPtr<SapiService> sSingleton;
+};
+
+} // namespace mozilla::dom
+
+#endif
diff --git a/dom/media/webspeech/synth/windows/components.conf b/dom/media/webspeech/synth/windows/components.conf
new file mode 100644
index 0000000000..bc9b83a43a
--- /dev/null
+++ b/dom/media/webspeech/synth/windows/components.conf
@@ -0,0 +1,17 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+ {
+ 'cid': '{21b4a45b-9806-4021-a706-d768ab0548f9}',
+ 'contract_ids': ['@mozilla.org/synthsapi;1'],
+ 'singleton': True,
+ 'type': 'mozilla::dom::SapiService',
+ 'headers': ['/dom/media/webspeech/synth/windows/SapiService.h'],
+ 'constructor': 'mozilla::dom::SapiService::GetInstanceForService',
+ 'categories': {"speech-synth-started": 'Sapi Speech Synth'},
+ },
+]
diff --git a/dom/media/webspeech/synth/windows/moz.build b/dom/media/webspeech/synth/windows/moz.build
new file mode 100644
index 0000000000..90bafe9ca7
--- /dev/null
+++ b/dom/media/webspeech/synth/windows/moz.build
@@ -0,0 +1,17 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES += [
+ "SapiService.cpp",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/webvtt/TextTrack.cpp b/dom/media/webvtt/TextTrack.cpp
new file mode 100644
index 0000000000..5a6bb461b0
--- /dev/null
+++ b/dom/media/webvtt/TextTrack.cpp
@@ -0,0 +1,385 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 et tw=78: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/AsyncEventDispatcher.h"
+#include "mozilla/dom/TextTrack.h"
+#include "mozilla/dom/TextTrackBinding.h"
+#include "mozilla/dom/TextTrackList.h"
+#include "mozilla/dom/TextTrackCue.h"
+#include "mozilla/dom/TextTrackCueList.h"
+#include "mozilla/dom/TextTrackRegion.h"
+#include "mozilla/dom/HTMLMediaElement.h"
+#include "mozilla/dom/HTMLTrackElement.h"
+#include "nsGlobalWindow.h"
+
+extern mozilla::LazyLogModule gTextTrackLog;
+
+#define WEBVTT_LOG(msg, ...) \
+ MOZ_LOG(gTextTrackLog, LogLevel::Debug, \
+ ("TextTrack=%p, " msg, this, ##__VA_ARGS__))
+
+namespace mozilla::dom {
+
+static const char* ToStateStr(const TextTrackMode aMode) {
+ switch (aMode) {
+ case TextTrackMode::Disabled:
+ return "DISABLED";
+ case TextTrackMode::Hidden:
+ return "HIDDEN";
+ case TextTrackMode::Showing:
+ return "SHOWING";
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid state.");
+ }
+ return "Unknown";
+}
+
+static const char* ToReadyStateStr(const TextTrackReadyState aState) {
+ switch (aState) {
+ case TextTrackReadyState::NotLoaded:
+ return "NotLoaded";
+ case TextTrackReadyState::Loading:
+ return "Loading";
+ case TextTrackReadyState::Loaded:
+ return "Loaded";
+ case TextTrackReadyState::FailedToLoad:
+ return "FailedToLoad";
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid state.");
+ }
+ return "Unknown";
+}
+
+static const char* ToTextTrackKindStr(const TextTrackKind aKind) {
+ switch (aKind) {
+ case TextTrackKind::Subtitles:
+ return "Subtitles";
+ case TextTrackKind::Captions:
+ return "Captions";
+ case TextTrackKind::Descriptions:
+ return "Descriptions";
+ case TextTrackKind::Chapters:
+ return "Chapters";
+ case TextTrackKind::Metadata:
+ return "Metadata";
+ default:
+ MOZ_ASSERT_UNREACHABLE("Invalid kind.");
+ }
+ return "Unknown";
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(TextTrack, DOMEventTargetHelper, mCueList,
+ mActiveCueList, mTextTrackList,
+ mTrackElement)
+
+NS_IMPL_ADDREF_INHERITED(TextTrack, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(TextTrack, DOMEventTargetHelper)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TextTrack)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+TextTrack::TextTrack(nsPIDOMWindowInner* aOwnerWindow, TextTrackKind aKind,
+ const nsAString& aLabel, const nsAString& aLanguage,
+ TextTrackMode aMode, TextTrackReadyState aReadyState,
+ TextTrackSource aTextTrackSource)
+ : DOMEventTargetHelper(aOwnerWindow),
+ mKind(aKind),
+ mLabel(aLabel),
+ mLanguage(aLanguage),
+ mMode(aMode),
+ mReadyState(aReadyState),
+ mTextTrackSource(aTextTrackSource) {
+ SetDefaultSettings();
+}
+
+TextTrack::TextTrack(nsPIDOMWindowInner* aOwnerWindow,
+ TextTrackList* aTextTrackList, TextTrackKind aKind,
+ const nsAString& aLabel, const nsAString& aLanguage,
+ TextTrackMode aMode, TextTrackReadyState aReadyState,
+ TextTrackSource aTextTrackSource)
+ : DOMEventTargetHelper(aOwnerWindow),
+ mTextTrackList(aTextTrackList),
+ mKind(aKind),
+ mLabel(aLabel),
+ mLanguage(aLanguage),
+ mMode(aMode),
+ mReadyState(aReadyState),
+ mTextTrackSource(aTextTrackSource) {
+ SetDefaultSettings();
+}
+
+TextTrack::~TextTrack() = default;
+
+void TextTrack::SetDefaultSettings() {
+ nsPIDOMWindowInner* ownerWindow = GetOwner();
+ mCueList = new TextTrackCueList(ownerWindow);
+ mActiveCueList = new TextTrackCueList(ownerWindow);
+ mCuePos = 0;
+ mDirty = false;
+}
+
+JSObject* TextTrack::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return TextTrack_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+void TextTrack::SetMode(TextTrackMode aValue) {
+ if (mMode == aValue) {
+ return;
+ }
+ WEBVTT_LOG("Set mode=%s for track kind %s", ToStateStr(aValue),
+ ToTextTrackKindStr(mKind));
+ mMode = aValue;
+
+ HTMLMediaElement* mediaElement = GetMediaElement();
+ if (aValue == TextTrackMode::Disabled) {
+ for (size_t i = 0; i < mCueList->Length() && mediaElement; ++i) {
+ mediaElement->NotifyCueRemoved(*(*mCueList)[i]);
+ }
+ SetCuesInactive();
+ } else {
+ for (size_t i = 0; i < mCueList->Length() && mediaElement; ++i) {
+ mediaElement->NotifyCueAdded(*(*mCueList)[i]);
+ }
+ }
+ if (mediaElement) {
+ mediaElement->NotifyTextTrackModeChanged();
+ }
+ // https://html.spec.whatwg.org/multipage/media.html#sourcing-out-of-band-text-tracks:start-the-track-processing-model
+ // Run the `start-the-track-processing-model` to track's corresponding track
+ // element whenever track's mode changes.
+ if (mTrackElement) {
+ mTrackElement->MaybeDispatchLoadResource();
+ }
+ // Ensure the TimeMarchesOn is called in case that the mCueList
+ // is empty.
+ NotifyCueUpdated(nullptr);
+}
+
+void TextTrack::GetId(nsAString& aId) const {
+ // If the track has a track element then its id should be the same as the
+ // track element's id.
+ if (mTrackElement) {
+ mTrackElement->GetAttr(nsGkAtoms::id, aId);
+ }
+}
+
+void TextTrack::AddCue(TextTrackCue& aCue) {
+ WEBVTT_LOG("AddCue %p [%f:%f]", &aCue, aCue.StartTime(), aCue.EndTime());
+ TextTrack* oldTextTrack = aCue.GetTrack();
+ if (oldTextTrack) {
+ ErrorResult dummy;
+ oldTextTrack->RemoveCue(aCue, dummy);
+ }
+ mCueList->AddCue(aCue);
+ aCue.SetTrack(this);
+ HTMLMediaElement* mediaElement = GetMediaElement();
+ if (mediaElement && (mMode != TextTrackMode::Disabled)) {
+ mediaElement->NotifyCueAdded(aCue);
+ }
+}
+
+void TextTrack::RemoveCue(TextTrackCue& aCue, ErrorResult& aRv) {
+ WEBVTT_LOG("RemoveCue %p", &aCue);
+ // Bug1304948, check the aCue belongs to the TextTrack.
+ mCueList->RemoveCue(aCue, aRv);
+ if (aRv.Failed()) {
+ return;
+ }
+ aCue.SetActive(false);
+ aCue.SetTrack(nullptr);
+ HTMLMediaElement* mediaElement = GetMediaElement();
+ if (mediaElement) {
+ mediaElement->NotifyCueRemoved(aCue);
+ }
+}
+
+void TextTrack::ClearAllCues() {
+ WEBVTT_LOG("ClearAllCues");
+ ErrorResult dummy;
+ while (!mCueList->IsEmpty()) {
+ RemoveCue(*(*mCueList)[0], dummy);
+ }
+}
+
+void TextTrack::SetCuesDirty() {
+ for (uint32_t i = 0; i < mCueList->Length(); i++) {
+ ((*mCueList)[i])->Reset();
+ }
+}
+
+TextTrackCueList* TextTrack::GetActiveCues() {
+ if (mMode != TextTrackMode::Disabled) {
+ return mActiveCueList;
+ }
+ return nullptr;
+}
+
+void TextTrack::GetActiveCueArray(nsTArray<RefPtr<TextTrackCue> >& aCues) {
+ if (mMode != TextTrackMode::Disabled) {
+ mActiveCueList->GetArray(aCues);
+ }
+}
+
+TextTrackReadyState TextTrack::ReadyState() const { return mReadyState; }
+
+void TextTrack::SetReadyState(TextTrackReadyState aState) {
+ WEBVTT_LOG("SetReadyState=%s", ToReadyStateStr(aState));
+ mReadyState = aState;
+ HTMLMediaElement* mediaElement = GetMediaElement();
+ if (mediaElement && (mReadyState == TextTrackReadyState::Loaded ||
+ mReadyState == TextTrackReadyState::FailedToLoad)) {
+ mediaElement->RemoveTextTrack(this, true);
+ mediaElement->UpdateReadyState();
+ }
+}
+
+TextTrackList* TextTrack::GetTextTrackList() { return mTextTrackList; }
+
+void TextTrack::SetTextTrackList(TextTrackList* aTextTrackList) {
+ mTextTrackList = aTextTrackList;
+}
+
+HTMLTrackElement* TextTrack::GetTrackElement() { return mTrackElement; }
+
+void TextTrack::SetTrackElement(HTMLTrackElement* aTrackElement) {
+ mTrackElement = aTrackElement;
+}
+
+void TextTrack::SetCuesInactive() {
+ WEBVTT_LOG("SetCuesInactive");
+ mCueList->SetCuesInactive();
+}
+
+void TextTrack::NotifyCueUpdated(TextTrackCue* aCue) {
+ WEBVTT_LOG("NotifyCueUpdated, cue=%p", aCue);
+ mCueList->NotifyCueUpdated(aCue);
+ HTMLMediaElement* mediaElement = GetMediaElement();
+ if (mediaElement) {
+ mediaElement->NotifyCueUpdated(aCue);
+ }
+}
+
+void TextTrack::GetLabel(nsAString& aLabel) const {
+ if (mTrackElement) {
+ mTrackElement->GetLabel(aLabel);
+ } else {
+ aLabel = mLabel;
+ }
+}
+void TextTrack::GetLanguage(nsAString& aLanguage) const {
+ if (mTrackElement) {
+ mTrackElement->GetSrclang(aLanguage);
+ } else {
+ aLanguage = mLanguage;
+ }
+}
+
+void TextTrack::DispatchAsyncTrustedEvent(const nsString& aEventName) {
+ nsPIDOMWindowInner* win = GetOwner();
+ if (!win) {
+ return;
+ }
+ RefPtr<TextTrack> self = this;
+ nsGlobalWindowInner::Cast(win)->Dispatch(
+ TaskCategory::Other,
+ NS_NewRunnableFunction(
+ "dom::TextTrack::DispatchAsyncTrustedEvent",
+ [self, aEventName]() { self->DispatchTrustedEvent(aEventName); }));
+}
+
+bool TextTrack::IsLoaded() {
+ if (mMode == TextTrackMode::Disabled) {
+ return true;
+ }
+ // If the TrackElement's src is null, we can not block the
+ // MediaElement.
+ if (mTrackElement) {
+ nsAutoString src;
+ if (!(mTrackElement->GetAttr(kNameSpaceID_None, nsGkAtoms::src, src))) {
+ return true;
+ }
+ }
+ return mReadyState >= TextTrackReadyState::Loaded;
+}
+
+void TextTrack::NotifyCueActiveStateChanged(TextTrackCue* aCue) {
+ MOZ_ASSERT(aCue);
+ if (aCue->GetActive()) {
+ MOZ_ASSERT(!mActiveCueList->IsCueExist(aCue));
+ WEBVTT_LOG("NotifyCueActiveStateChanged, add cue %p to the active list",
+ aCue);
+ mActiveCueList->AddCue(*aCue);
+ } else {
+ MOZ_ASSERT(mActiveCueList->IsCueExist(aCue));
+ WEBVTT_LOG(
+ "NotifyCueActiveStateChanged, remove cue %p from the active list",
+ aCue);
+ mActiveCueList->RemoveCue(*aCue);
+ }
+}
+
+void TextTrack::GetCurrentCuesAndOtherCues(
+ RefPtr<TextTrackCueList>& aCurrentCues,
+ RefPtr<TextTrackCueList>& aOtherCues,
+ const media::TimeInterval& aInterval) const {
+ const HTMLMediaElement* mediaElement = GetMediaElement();
+ if (!mediaElement) {
+ return;
+ }
+
+ if (Mode() == TextTrackMode::Disabled) {
+ return;
+ }
+
+ // According to `time marches on` step1, current cue list contains the cues
+ // whose start times are less than or equal to the current playback position
+ // and whose end times are greater than the current playback position.
+ // https://html.spec.whatwg.org/multipage/media.html#time-marches-on
+ MOZ_ASSERT(aCurrentCues && aOtherCues);
+ const double playbackTime = mediaElement->CurrentTime();
+ for (uint32_t idx = 0; idx < mCueList->Length(); idx++) {
+ TextTrackCue* cue = (*mCueList)[idx];
+ WEBVTT_LOG("cue %p [%f:%f], playbackTime=%f", cue, cue->StartTime(),
+ cue->EndTime(), playbackTime);
+ if (cue->StartTime() <= playbackTime && cue->EndTime() > playbackTime) {
+ WEBVTT_LOG("Add cue %p [%f:%f] to current cue list", cue,
+ cue->StartTime(), cue->EndTime());
+ aCurrentCues->AddCue(*cue);
+ } else {
+ // As the spec didn't have a restriction for the negative duration, it
+ // does happen sometime if user sets it explictly. It would be treated as
+ // a `missing cue` later in the `TimeMarchesOn` but it won't be displayed.
+ if (cue->EndTime() < cue->StartTime()) {
+ // Add cue into `otherCue` only when its start time is contained by the
+ // current time interval.
+ if (aInterval.Contains(
+ media::TimeUnit::FromSeconds(cue->StartTime()))) {
+ WEBVTT_LOG("[Negative duration] Add cue %p [%f:%f] to other cue list",
+ cue, cue->StartTime(), cue->EndTime());
+ aOtherCues->AddCue(*cue);
+ }
+ continue;
+ }
+ media::TimeInterval cueInterval(
+ media::TimeUnit::FromSeconds(cue->StartTime()),
+ media::TimeUnit::FromSeconds(cue->EndTime()));
+ // cues are completely outside the time interval.
+ if (!aInterval.Touches(cueInterval)) {
+ continue;
+ }
+ // contains any cues which are overlapping within the time interval.
+ WEBVTT_LOG("Add cue %p [%f:%f] to other cue list", cue, cue->StartTime(),
+ cue->EndTime());
+ aOtherCues->AddCue(*cue);
+ }
+ }
+}
+
+HTMLMediaElement* TextTrack::GetMediaElement() const {
+ return mTextTrackList ? mTextTrackList->GetMediaElement() : nullptr;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webvtt/TextTrack.h b/dom/media/webvtt/TextTrack.h
new file mode 100644
index 0000000000..1adf8f1838
--- /dev/null
+++ b/dom/media/webvtt/TextTrack.h
@@ -0,0 +1,148 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 et tw=78: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_TextTrack_h
+#define mozilla_dom_TextTrack_h
+
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/dom/TextTrackBinding.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsString.h"
+#include "TimeUnits.h"
+
+namespace mozilla::dom {
+
+class TextTrackList;
+class TextTrackCue;
+class TextTrackCueList;
+class HTMLTrackElement;
+class HTMLMediaElement;
+
+enum class TextTrackSource : uint8_t {
+ Track,
+ AddTextTrack,
+ MediaResourceSpecific,
+};
+
+// Constants for numeric readyState property values.
+enum class TextTrackReadyState : uint8_t {
+ NotLoaded = 0U,
+ Loading = 1U,
+ Loaded = 2U,
+ FailedToLoad = 3U
+};
+
+class TextTrack final : public DOMEventTargetHelper {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(TextTrack, DOMEventTargetHelper)
+
+ TextTrack(nsPIDOMWindowInner* aOwnerWindow, TextTrackKind aKind,
+ const nsAString& aLabel, const nsAString& aLanguage,
+ TextTrackMode aMode, TextTrackReadyState aReadyState,
+ TextTrackSource aTextTrackSource);
+ TextTrack(nsPIDOMWindowInner* aOwnerWindow, TextTrackList* aTextTrackList,
+ TextTrackKind aKind, const nsAString& aLabel,
+ const nsAString& aLanguage, TextTrackMode aMode,
+ TextTrackReadyState aReadyState, TextTrackSource aTextTrackSource);
+
+ void SetDefaultSettings();
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ TextTrackKind Kind() const { return mKind; }
+ void GetLabel(nsAString& aLabel) const;
+ void GetLanguage(nsAString& aLanguage) const;
+ void GetInBandMetadataTrackDispatchType(nsAString& aType) const {
+ aType = mType;
+ }
+ void GetId(nsAString& aId) const;
+
+ TextTrackMode Mode() const { return mMode; }
+ void SetMode(TextTrackMode aValue);
+
+ TextTrackCueList* GetCues() const {
+ if (mMode == TextTrackMode::Disabled) {
+ return nullptr;
+ }
+ return mCueList;
+ }
+
+ TextTrackCueList* GetActiveCues();
+ void GetActiveCueArray(nsTArray<RefPtr<TextTrackCue> >& aCues);
+
+ TextTrackReadyState ReadyState() const;
+ void SetReadyState(TextTrackReadyState aState);
+
+ void AddCue(TextTrackCue& aCue);
+ void RemoveCue(TextTrackCue& aCue, ErrorResult& aRv);
+ void SetDirty() { mDirty = true; }
+ void SetCuesDirty();
+
+ TextTrackList* GetTextTrackList();
+ void SetTextTrackList(TextTrackList* aTextTrackList);
+
+ IMPL_EVENT_HANDLER(cuechange)
+
+ HTMLTrackElement* GetTrackElement();
+ void SetTrackElement(HTMLTrackElement* aTrackElement);
+
+ TextTrackSource GetTextTrackSource() { return mTextTrackSource; }
+
+ void SetCuesInactive();
+
+ void NotifyCueUpdated(TextTrackCue* aCue);
+
+ void DispatchAsyncTrustedEvent(const nsString& aEventName);
+
+ bool IsLoaded();
+
+ // Called when associated cue's active flag has been changed, and then we
+ // would add or remove the cue to the active cue list.
+ void NotifyCueActiveStateChanged(TextTrackCue* aCue);
+
+ // Use this function to request current cues, which start time are less than
+ // or equal to the current playback position and whose end times are greater
+ // than the current playback position, and other cues, which are not in the
+ // current cues. Because there would be LOTS of cues in the other cues, and we
+ // don't actually need all of them. Therefore, we use a time interval to get
+ // the cues which are overlapping within the time interval.
+ void GetCurrentCuesAndOtherCues(RefPtr<TextTrackCueList>& aCurrentCues,
+ RefPtr<TextTrackCueList>& aOtherCues,
+ const media::TimeInterval& aInterval) const;
+
+ void ClearAllCues();
+
+ private:
+ ~TextTrack();
+
+ HTMLMediaElement* GetMediaElement() const;
+
+ RefPtr<TextTrackList> mTextTrackList;
+
+ TextTrackKind mKind;
+ nsString mLabel;
+ nsString mLanguage;
+ nsString mType;
+ TextTrackMode mMode;
+
+ RefPtr<TextTrackCueList> mCueList;
+ RefPtr<TextTrackCueList> mActiveCueList;
+ RefPtr<HTMLTrackElement> mTrackElement;
+
+ uint32_t mCuePos;
+ TextTrackReadyState mReadyState;
+ bool mDirty;
+
+ // An enum that represents where the track was sourced from.
+ TextTrackSource mTextTrackSource;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_TextTrack_h
diff --git a/dom/media/webvtt/TextTrackCue.cpp b/dom/media/webvtt/TextTrackCue.cpp
new file mode 100644
index 0000000000..434337a337
--- /dev/null
+++ b/dom/media/webvtt/TextTrackCue.cpp
@@ -0,0 +1,260 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/TextTrackCue.h"
+
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/HTMLTrackElement.h"
+#include "mozilla/dom/TextTrackList.h"
+#include "mozilla/dom/TextTrackRegion.h"
+#include "nsComponentManagerUtils.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/intl/Bidi.h"
+
+extern mozilla::LazyLogModule gTextTrackLog;
+
+#define LOG(msg, ...) \
+ MOZ_LOG(gTextTrackLog, LogLevel::Debug, \
+ ("TextTrackCue=%p, " msg, this, ##__VA_ARGS__))
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(TextTrackCue, DOMEventTargetHelper,
+ mDocument, mTrack, mTrackElement,
+ mDisplayState, mRegion)
+
+NS_IMPL_ADDREF_INHERITED(TextTrackCue, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(TextTrackCue, DOMEventTargetHelper)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TextTrackCue)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+StaticRefPtr<nsIWebVTTParserWrapper> TextTrackCue::sParserWrapper;
+
+// Set default value for cue, spec https://w3c.github.io/webvtt/#model-cues
+void TextTrackCue::SetDefaultCueSettings() {
+ mPositionIsAutoKeyword = true;
+ // Spec https://www.w3.org/TR/webvtt1/#webvtt-cue-position-automatic-alignment
+ mPositionAlign = PositionAlignSetting::Auto;
+ mSize = 100.0;
+ mPauseOnExit = false;
+ mSnapToLines = true;
+ mLineIsAutoKeyword = true;
+ mAlign = AlignSetting::Center;
+ mLineAlign = LineAlignSetting::Start;
+ mVertical = DirectionSetting::_empty;
+ mActive = false;
+}
+
+TextTrackCue::TextTrackCue(nsPIDOMWindowInner* aOwnerWindow, double aStartTime,
+ double aEndTime, const nsAString& aText,
+ ErrorResult& aRv)
+ : DOMEventTargetHelper(aOwnerWindow),
+ mText(aText),
+ mStartTime(aStartTime),
+ mEndTime(aEndTime),
+ mPosition(0.0),
+ mLine(0.0),
+ mReset(false, "TextTrackCue::mReset"),
+ mHaveStartedWatcher(false),
+ mWatchManager(
+ this, GetOwnerGlobal()->AbstractMainThreadFor(TaskCategory::Other)) {
+ LOG("create TextTrackCue");
+ SetDefaultCueSettings();
+ MOZ_ASSERT(aOwnerWindow);
+ if (NS_FAILED(StashDocument())) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ }
+}
+
+TextTrackCue::TextTrackCue(nsPIDOMWindowInner* aOwnerWindow, double aStartTime,
+ double aEndTime, const nsAString& aText,
+ HTMLTrackElement* aTrackElement, ErrorResult& aRv)
+ : DOMEventTargetHelper(aOwnerWindow),
+ mText(aText),
+ mStartTime(aStartTime),
+ mEndTime(aEndTime),
+ mTrackElement(aTrackElement),
+ mPosition(0.0),
+ mLine(0.0),
+ mReset(false, "TextTrackCue::mReset"),
+ mHaveStartedWatcher(false),
+ mWatchManager(
+ this, GetOwnerGlobal()->AbstractMainThreadFor(TaskCategory::Other)) {
+ LOG("create TextTrackCue");
+ SetDefaultCueSettings();
+ MOZ_ASSERT(aOwnerWindow);
+ if (NS_FAILED(StashDocument())) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ }
+}
+
+TextTrackCue::~TextTrackCue() = default;
+
+/** Save a reference to our creating document so we don't have to
+ * keep getting it from our window.
+ */
+nsresult TextTrackCue::StashDocument() {
+ nsPIDOMWindowInner* window = GetOwner();
+ if (!window) {
+ return NS_ERROR_NO_INTERFACE;
+ }
+ mDocument = window->GetDoc();
+ if (!mDocument) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ return NS_OK;
+}
+
+already_AddRefed<DocumentFragment> TextTrackCue::GetCueAsHTML() {
+ // mDocument may be null during cycle collector shutdown.
+ // See bug 941701.
+ if (!mDocument) {
+ return nullptr;
+ }
+
+ if (!sParserWrapper) {
+ nsresult rv;
+ nsCOMPtr<nsIWebVTTParserWrapper> parserWrapper =
+ do_CreateInstance(NS_WEBVTTPARSERWRAPPER_CONTRACTID, &rv);
+ if (NS_FAILED(rv)) {
+ return mDocument->CreateDocumentFragment();
+ }
+ sParserWrapper = parserWrapper;
+ ClearOnShutdown(&sParserWrapper);
+ }
+
+ nsPIDOMWindowInner* window = mDocument->GetInnerWindow();
+ if (!window) {
+ return mDocument->CreateDocumentFragment();
+ }
+
+ RefPtr<DocumentFragment> frag;
+ sParserWrapper->ConvertCueToDOMTree(window, static_cast<EventTarget*>(this),
+ getter_AddRefs(frag));
+ if (!frag) {
+ return mDocument->CreateDocumentFragment();
+ }
+ return frag.forget();
+}
+
+void TextTrackCue::SetTrackElement(HTMLTrackElement* aTrackElement) {
+ mTrackElement = aTrackElement;
+}
+
+JSObject* TextTrackCue::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return VTTCue_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+TextTrackRegion* TextTrackCue::GetRegion() { return mRegion; }
+
+void TextTrackCue::SetRegion(TextTrackRegion* aRegion) {
+ if (mRegion == aRegion) {
+ return;
+ }
+ mRegion = aRegion;
+ mReset = true;
+}
+
+double TextTrackCue::ComputedLine() {
+ // See spec https://w3c.github.io/webvtt/#cue-computed-line
+ if (!mLineIsAutoKeyword && !mSnapToLines && (mLine < 0.0 || mLine > 100.0)) {
+ return 100.0;
+ } else if (!mLineIsAutoKeyword) {
+ return mLine;
+ } else if (mLineIsAutoKeyword && !mSnapToLines) {
+ return 100.0;
+ } else if (!mTrack || !mTrack->GetTextTrackList() ||
+ !mTrack->GetTextTrackList()->GetMediaElement()) {
+ return -1.0;
+ }
+
+ RefPtr<TextTrackList> trackList = mTrack->GetTextTrackList();
+ bool dummy;
+ uint32_t showingTracksNum = 0;
+ for (uint32_t idx = 0; idx < trackList->Length(); idx++) {
+ RefPtr<TextTrack> track = trackList->IndexedGetter(idx, dummy);
+ if (track->Mode() == TextTrackMode::Showing) {
+ showingTracksNum++;
+ }
+
+ if (mTrack == track) {
+ break;
+ }
+ }
+
+ return (-1.0) * showingTracksNum;
+}
+
+double TextTrackCue::ComputedPosition() {
+ // See spec https://w3c.github.io/webvtt/#cue-computed-position
+ if (!mPositionIsAutoKeyword) {
+ return mPosition;
+ }
+ if (ComputedPositionAlign() == PositionAlignSetting::Line_left) {
+ return 0.0;
+ }
+ if (ComputedPositionAlign() == PositionAlignSetting::Line_right) {
+ return 100.0;
+ }
+ return 50.0;
+}
+
+PositionAlignSetting TextTrackCue::ComputedPositionAlign() {
+ // See spec https://w3c.github.io/webvtt/#cue-computed-position-alignment
+ if (mPositionAlign != PositionAlignSetting::Auto) {
+ return mPositionAlign;
+ } else if (mAlign == AlignSetting::Left) {
+ return PositionAlignSetting::Line_left;
+ } else if (mAlign == AlignSetting::Right) {
+ return PositionAlignSetting::Line_right;
+ } else if (mAlign == AlignSetting::Start) {
+ return IsTextBaseDirectionLTR() ? PositionAlignSetting::Line_left
+ : PositionAlignSetting::Line_right;
+ } else if (mAlign == AlignSetting::End) {
+ return IsTextBaseDirectionLTR() ? PositionAlignSetting::Line_right
+ : PositionAlignSetting::Line_left;
+ }
+ return PositionAlignSetting::Center;
+}
+
+bool TextTrackCue::IsTextBaseDirectionLTR() const {
+ // The returned result by `ubidi_getBaseDirection` might be `neutral` if the
+ // text only contains netural charaters. In this case, we would treat its
+ // base direction as LTR.
+ return intl::Bidi::GetBaseDirection(mText) != intl::Bidi::BaseDirection::RTL;
+}
+
+void TextTrackCue::NotifyDisplayStatesChanged() {
+ if (!mReset) {
+ return;
+ }
+
+ if (!mTrack || !mTrack->GetTextTrackList() ||
+ !mTrack->GetTextTrackList()->GetMediaElement()) {
+ return;
+ }
+
+ mTrack->GetTextTrackList()
+ ->GetMediaElement()
+ ->NotifyCueDisplayStatesChanged();
+}
+
+void TextTrackCue::SetActive(bool aActive) {
+ if (mActive == aActive) {
+ return;
+ }
+
+ LOG("TextTrackCue, SetActive=%d", aActive);
+ mActive = aActive;
+ mDisplayState = mActive ? mDisplayState : nullptr;
+ if (mTrack) {
+ mTrack->NotifyCueActiveStateChanged(this);
+ }
+}
+
+#undef LOG
+
+} // namespace mozilla::dom
diff --git a/dom/media/webvtt/TextTrackCue.h b/dom/media/webvtt/TextTrackCue.h
new file mode 100644
index 0000000000..90ce0a571d
--- /dev/null
+++ b/dom/media/webvtt/TextTrackCue.h
@@ -0,0 +1,342 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 et tw=78: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_TextTrackCue_h
+#define mozilla_dom_TextTrackCue_h
+
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/dom/DocumentFragment.h"
+#include "mozilla/dom/VTTCueBinding.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsIWebVTTParserWrapper.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/dom/HTMLDivElement.h"
+#include "mozilla/dom/TextTrack.h"
+#include "mozilla/StateWatching.h"
+
+namespace mozilla::dom {
+
+class Document;
+class HTMLTrackElement;
+class TextTrackRegion;
+
+class TextTrackCue final : public DOMEventTargetHelper {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(TextTrackCue, DOMEventTargetHelper)
+
+ // TextTrackCue WebIDL
+ // See bug 868509 about splitting out the WebVTT-specific interfaces.
+ static already_AddRefed<TextTrackCue> Constructor(GlobalObject& aGlobal,
+ double aStartTime,
+ double aEndTime,
+ const nsAString& aText,
+ ErrorResult& aRv) {
+ nsCOMPtr<nsPIDOMWindowInner> window =
+ do_QueryInterface(aGlobal.GetAsSupports());
+ RefPtr<TextTrackCue> ttcue =
+ new TextTrackCue(window, aStartTime, aEndTime, aText, aRv);
+ return ttcue.forget();
+ }
+ TextTrackCue(nsPIDOMWindowInner* aGlobal, double aStartTime, double aEndTime,
+ const nsAString& aText, ErrorResult& aRv);
+
+ TextTrackCue(nsPIDOMWindowInner* aGlobal, double aStartTime, double aEndTime,
+ const nsAString& aText, HTMLTrackElement* aTrackElement,
+ ErrorResult& aRv);
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ TextTrack* GetTrack() const { return mTrack; }
+
+ void GetId(nsAString& aId) const { aId = mId; }
+
+ void SetId(const nsAString& aId) {
+ if (mId == aId) {
+ return;
+ }
+
+ mId = aId;
+ }
+
+ double StartTime() const { return mStartTime; }
+
+ void SetStartTime(double aStartTime) {
+ if (mStartTime == aStartTime) {
+ return;
+ }
+
+ mStartTime = aStartTime;
+ mReset = true;
+ NotifyCueUpdated(this);
+ }
+
+ double EndTime() const { return mEndTime; }
+
+ void SetEndTime(double aEndTime) {
+ if (mEndTime == aEndTime) {
+ return;
+ }
+
+ mEndTime = aEndTime;
+ mReset = true;
+ NotifyCueUpdated(this);
+ }
+
+ bool PauseOnExit() { return mPauseOnExit; }
+
+ void SetPauseOnExit(bool aPauseOnExit) {
+ if (mPauseOnExit == aPauseOnExit) {
+ return;
+ }
+
+ mPauseOnExit = aPauseOnExit;
+ NotifyCueUpdated(nullptr);
+ }
+
+ TextTrackRegion* GetRegion();
+ void SetRegion(TextTrackRegion* aRegion);
+
+ DirectionSetting Vertical() const { return mVertical; }
+
+ void SetVertical(const DirectionSetting& aVertical) {
+ if (mVertical == aVertical) {
+ return;
+ }
+
+ mReset = true;
+ mVertical = aVertical;
+ }
+
+ bool SnapToLines() { return mSnapToLines; }
+
+ void SetSnapToLines(bool aSnapToLines) {
+ if (mSnapToLines == aSnapToLines) {
+ return;
+ }
+
+ mReset = true;
+ mSnapToLines = aSnapToLines;
+ }
+
+ void GetLine(OwningDoubleOrAutoKeyword& aLine) const {
+ if (mLineIsAutoKeyword) {
+ aLine.SetAsAutoKeyword() = AutoKeyword::Auto;
+ return;
+ }
+ aLine.SetAsDouble() = mLine;
+ }
+
+ void SetLine(const DoubleOrAutoKeyword& aLine) {
+ if (aLine.IsDouble() &&
+ (mLineIsAutoKeyword || (aLine.GetAsDouble() != mLine))) {
+ mLineIsAutoKeyword = false;
+ mLine = aLine.GetAsDouble();
+ mReset = true;
+ return;
+ }
+ if (aLine.IsAutoKeyword() && !mLineIsAutoKeyword) {
+ mLineIsAutoKeyword = true;
+ mReset = true;
+ }
+ }
+
+ LineAlignSetting LineAlign() const { return mLineAlign; }
+
+ void SetLineAlign(LineAlignSetting& aLineAlign, ErrorResult& aRv) {
+ if (mLineAlign == aLineAlign) {
+ return;
+ }
+
+ mReset = true;
+ mLineAlign = aLineAlign;
+ }
+
+ void GetPosition(OwningDoubleOrAutoKeyword& aPosition) const {
+ if (mPositionIsAutoKeyword) {
+ aPosition.SetAsAutoKeyword() = AutoKeyword::Auto;
+ return;
+ }
+ aPosition.SetAsDouble() = mPosition;
+ }
+
+ void SetPosition(const DoubleOrAutoKeyword& aPosition, ErrorResult& aRv) {
+ if (!aPosition.IsAutoKeyword() &&
+ (aPosition.GetAsDouble() > 100.0 || aPosition.GetAsDouble() < 0.0)) {
+ aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
+ return;
+ }
+
+ if (aPosition.IsDouble() &&
+ (mPositionIsAutoKeyword || (aPosition.GetAsDouble() != mPosition))) {
+ mPositionIsAutoKeyword = false;
+ mPosition = aPosition.GetAsDouble();
+ mReset = true;
+ return;
+ }
+
+ if (aPosition.IsAutoKeyword() && !mPositionIsAutoKeyword) {
+ mPositionIsAutoKeyword = true;
+ mReset = true;
+ }
+ }
+
+ PositionAlignSetting PositionAlign() const { return mPositionAlign; }
+
+ void SetPositionAlign(PositionAlignSetting aPositionAlign, ErrorResult& aRv) {
+ if (mPositionAlign == aPositionAlign) {
+ return;
+ }
+
+ mReset = true;
+ mPositionAlign = aPositionAlign;
+ }
+
+ double Size() const { return mSize; }
+
+ void SetSize(double aSize, ErrorResult& aRv) {
+ if (mSize == aSize) {
+ return;
+ }
+
+ if (aSize < 0.0 || aSize > 100.0) {
+ aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
+ return;
+ }
+
+ mReset = true;
+ mSize = aSize;
+ }
+
+ AlignSetting Align() const { return mAlign; }
+
+ void SetAlign(AlignSetting& aAlign) {
+ if (mAlign == aAlign) {
+ return;
+ }
+
+ mReset = true;
+ mAlign = aAlign;
+ }
+
+ void GetText(nsAString& aText) const { aText = mText; }
+
+ void SetText(const nsAString& aText) {
+ if (mText == aText) {
+ return;
+ }
+
+ mReset = true;
+ mText = aText;
+ }
+
+ IMPL_EVENT_HANDLER(enter)
+ IMPL_EVENT_HANDLER(exit)
+
+ HTMLDivElement* GetDisplayState() {
+ return static_cast<HTMLDivElement*>(mDisplayState.get());
+ }
+
+ void SetDisplayState(HTMLDivElement* aDisplayState) {
+ mDisplayState = aDisplayState;
+ mReset = false;
+ }
+
+ void Reset() { mReset = true; }
+
+ bool HasBeenReset() { return mReset; }
+
+ double ComputedLine();
+ double ComputedPosition();
+ PositionAlignSetting ComputedPositionAlign();
+
+ // Helper functions for implementation.
+ const nsAString& Id() const { return mId; }
+
+ void SetTrack(TextTrack* aTextTrack) {
+ mTrack = aTextTrack;
+ if (!mHaveStartedWatcher && aTextTrack) {
+ mHaveStartedWatcher = true;
+ mWatchManager.Watch(mReset, &TextTrackCue::NotifyDisplayStatesChanged);
+ } else if (mHaveStartedWatcher && !aTextTrack) {
+ mHaveStartedWatcher = false;
+ mWatchManager.Unwatch(mReset, &TextTrackCue::NotifyDisplayStatesChanged);
+ }
+ }
+
+ /**
+ * Produces a tree of anonymous content based on the tree of the processed
+ * cue text.
+ *
+ * Returns a DocumentFragment that is the head of the tree of anonymous
+ * content.
+ */
+ already_AddRefed<DocumentFragment> GetCueAsHTML();
+
+ void SetTrackElement(HTMLTrackElement* aTrackElement);
+
+ void SetActive(bool aActive);
+
+ bool GetActive() { return mActive; }
+
+ private:
+ ~TextTrackCue();
+
+ void NotifyCueUpdated(TextTrackCue* aCue) {
+ if (mTrack) {
+ mTrack->NotifyCueUpdated(aCue);
+ }
+ }
+
+ void NotifyDisplayStatesChanged();
+
+ void SetDefaultCueSettings();
+ nsresult StashDocument();
+
+ bool IsTextBaseDirectionLTR() const;
+
+ RefPtr<Document> mDocument;
+ nsString mText;
+ double mStartTime;
+ double mEndTime;
+
+ RefPtr<TextTrack> mTrack;
+ RefPtr<HTMLTrackElement> mTrackElement;
+ nsString mId;
+ double mPosition;
+ bool mPositionIsAutoKeyword;
+ PositionAlignSetting mPositionAlign;
+ double mSize;
+ bool mPauseOnExit;
+ bool mSnapToLines;
+ RefPtr<TextTrackRegion> mRegion;
+ DirectionSetting mVertical;
+ bool mLineIsAutoKeyword;
+ double mLine;
+ AlignSetting mAlign;
+ LineAlignSetting mLineAlign;
+
+ // Holds the computed DOM elements that represent the parsed cue text.
+ // http://www.whatwg.org/specs/web-apps/current-work/#text-track-cue-display-state
+ RefPtr<nsGenericHTMLElement> mDisplayState;
+ // Tells whether or not we need to recompute mDisplayState. This is set
+ // anytime a property that relates to the display of the TextTrackCue is
+ // changed.
+ Watchable<bool> mReset;
+
+ bool mActive;
+
+ static StaticRefPtr<nsIWebVTTParserWrapper> sParserWrapper;
+
+ // Only start watcher after the cue has text track.
+ bool mHaveStartedWatcher;
+ WatchManager<TextTrackCue> mWatchManager;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_TextTrackCue_h
diff --git a/dom/media/webvtt/TextTrackCueList.cpp b/dom/media/webvtt/TextTrackCueList.cpp
new file mode 100644
index 0000000000..d6fb8baedc
--- /dev/null
+++ b/dom/media/webvtt/TextTrackCueList.cpp
@@ -0,0 +1,125 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/TextTrackCueList.h"
+#include "mozilla/dom/TextTrackCueListBinding.h"
+#include "mozilla/dom/TextTrackCue.h"
+
+namespace mozilla::dom {
+
+class CompareCuesByTime {
+ public:
+ bool Equals(TextTrackCue* aOne, TextTrackCue* aTwo) const { return false; }
+ bool LessThan(TextTrackCue* aOne, TextTrackCue* aTwo) const {
+ return aOne->StartTime() < aTwo->StartTime() ||
+ (aOne->StartTime() == aTwo->StartTime() &&
+ aOne->EndTime() >= aTwo->EndTime());
+ }
+};
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(TextTrackCueList, mParent, mList)
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(TextTrackCueList)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(TextTrackCueList)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TextTrackCueList)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+TextTrackCueList::TextTrackCueList(nsISupports* aParent) : mParent(aParent) {}
+
+TextTrackCueList::~TextTrackCueList() = default;
+
+JSObject* TextTrackCueList::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return TextTrackCueList_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+TextTrackCue* TextTrackCueList::IndexedGetter(uint32_t aIndex, bool& aFound) {
+ aFound = aIndex < mList.Length();
+ if (!aFound) {
+ return nullptr;
+ }
+ return mList[aIndex];
+}
+
+TextTrackCue* TextTrackCueList::operator[](uint32_t aIndex) {
+ return mList.SafeElementAt(aIndex, nullptr);
+}
+
+TextTrackCueList& TextTrackCueList::operator=(const TextTrackCueList& aOther) {
+ mList = aOther.mList.Clone();
+ return *this;
+}
+
+TextTrackCue* TextTrackCueList::GetCueById(const nsAString& aId) {
+ if (aId.IsEmpty()) {
+ return nullptr;
+ }
+
+ for (uint32_t i = 0; i < mList.Length(); i++) {
+ if (aId.Equals(mList[i]->Id())) {
+ return mList[i];
+ }
+ }
+ return nullptr;
+}
+
+void TextTrackCueList::AddCue(TextTrackCue& aCue) {
+ if (mList.Contains(&aCue)) {
+ return;
+ }
+ mList.InsertElementSorted(&aCue, CompareCuesByTime());
+}
+
+void TextTrackCueList::RemoveCue(TextTrackCue& aCue, ErrorResult& aRv) {
+ if (!mList.Contains(&aCue)) {
+ aRv.Throw(NS_ERROR_DOM_NOT_FOUND_ERR);
+ return;
+ }
+ mList.RemoveElement(&aCue);
+}
+
+void TextTrackCueList::RemoveCue(TextTrackCue& aCue) {
+ mList.RemoveElement(&aCue);
+}
+
+void TextTrackCueList::RemoveCueAt(uint32_t aIndex) {
+ if (aIndex < mList.Length()) {
+ mList.RemoveElementAt(aIndex);
+ }
+}
+
+void TextTrackCueList::RemoveAll() { mList.Clear(); }
+
+void TextTrackCueList::GetArray(nsTArray<RefPtr<TextTrackCue>>& aCues) {
+ aCues = mList.Clone();
+}
+
+void TextTrackCueList::SetCuesInactive() {
+ for (uint32_t i = 0; i < mList.Length(); ++i) {
+ mList[i]->SetActive(false);
+ }
+}
+
+void TextTrackCueList::NotifyCueUpdated(TextTrackCue* aCue) {
+ if (aCue) {
+ mList.RemoveElement(aCue);
+ mList.InsertElementSorted(aCue, CompareCuesByTime());
+ }
+}
+
+bool TextTrackCueList::IsCueExist(TextTrackCue* aCue) {
+ if (aCue && mList.Contains(aCue)) {
+ return true;
+ }
+ return false;
+}
+
+nsTArray<RefPtr<TextTrackCue>>& TextTrackCueList::GetCuesArray() {
+ return mList;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webvtt/TextTrackCueList.h b/dom/media/webvtt/TextTrackCueList.h
new file mode 100644
index 0000000000..f590f94d8c
--- /dev/null
+++ b/dom/media/webvtt/TextTrackCueList.h
@@ -0,0 +1,73 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 et tw=78: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_TextTrackCueList_h
+#define mozilla_dom_TextTrackCueList_h
+
+#include "nsTArray.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsWrapperCache.h"
+
+namespace mozilla {
+class ErrorResult;
+
+namespace dom {
+
+class TextTrackCue;
+
+class TextTrackCueList final : public nsISupports, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(TextTrackCueList)
+
+ // TextTrackCueList WebIDL
+ explicit TextTrackCueList(nsISupports* aParent);
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ nsISupports* GetParentObject() const { return mParent; }
+
+ uint32_t Length() const { return mList.Length(); }
+
+ bool IsEmpty() const { return mList.Length() == 0; }
+
+ TextTrackCue* IndexedGetter(uint32_t aIndex, bool& aFound);
+ TextTrackCue* operator[](uint32_t aIndex);
+ TextTrackCue* GetCueById(const nsAString& aId);
+ TextTrackCueList& operator=(const TextTrackCueList& aOther);
+ // Adds a cue to mList by performing an insertion sort on mList.
+ // We expect most files to already be sorted, so an insertion sort starting
+ // from the end of the current array should be more efficient than a general
+ // sort step after all cues are loaded.
+ void AddCue(TextTrackCue& aCue);
+ void RemoveCue(TextTrackCue& aCue);
+ void RemoveCue(TextTrackCue& aCue, ErrorResult& aRv);
+ void RemoveCueAt(uint32_t aIndex);
+ void RemoveAll();
+ void GetArray(nsTArray<RefPtr<TextTrackCue>>& aCues);
+
+ void SetCuesInactive();
+
+ void NotifyCueUpdated(TextTrackCue* aCue);
+ bool IsCueExist(TextTrackCue* aCue);
+ nsTArray<RefPtr<TextTrackCue>>& GetCuesArray();
+
+ private:
+ ~TextTrackCueList();
+
+ nsCOMPtr<nsISupports> mParent;
+
+ // A sorted list of TextTrackCues sorted by earliest start time. If the start
+ // times are equal then it will be sorted by end time, earliest first.
+ nsTArray<RefPtr<TextTrackCue>> mList;
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_TextTrackCueList_h
diff --git a/dom/media/webvtt/TextTrackList.cpp b/dom/media/webvtt/TextTrackList.cpp
new file mode 100644
index 0000000000..d5611bdaf9
--- /dev/null
+++ b/dom/media/webvtt/TextTrackList.cpp
@@ -0,0 +1,192 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/TextTrackList.h"
+
+#include "mozilla/DebugOnly.h"
+#include "mozilla/dom/Event.h"
+#include "mozilla/dom/TextTrackListBinding.h"
+#include "mozilla/dom/TrackEvent.h"
+#include "nsThreadUtils.h"
+#include "nsGlobalWindow.h"
+#include "mozilla/dom/TextTrackCue.h"
+#include "mozilla/dom/TextTrackManager.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(TextTrackList, DOMEventTargetHelper,
+ mTextTracks, mTextTrackManager)
+
+NS_IMPL_ADDREF_INHERITED(TextTrackList, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(TextTrackList, DOMEventTargetHelper)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TextTrackList)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+TextTrackList::TextTrackList(nsPIDOMWindowInner* aOwnerWindow)
+ : DOMEventTargetHelper(aOwnerWindow) {}
+
+TextTrackList::TextTrackList(nsPIDOMWindowInner* aOwnerWindow,
+ TextTrackManager* aTextTrackManager)
+ : DOMEventTargetHelper(aOwnerWindow),
+ mTextTrackManager(aTextTrackManager) {}
+
+TextTrackList::~TextTrackList() = default;
+
+void TextTrackList::GetShowingCues(nsTArray<RefPtr<TextTrackCue>>& aCues) {
+ // Only Subtitles and Captions can show on the screen.
+ nsTArray<RefPtr<TextTrackCue>> cues;
+ for (uint32_t i = 0; i < Length(); i++) {
+ if (mTextTracks[i]->Mode() == TextTrackMode::Showing &&
+ (mTextTracks[i]->Kind() == TextTrackKind::Subtitles ||
+ mTextTracks[i]->Kind() == TextTrackKind::Captions)) {
+ mTextTracks[i]->GetActiveCueArray(cues);
+ aCues.AppendElements(cues);
+ }
+ }
+}
+
+JSObject* TextTrackList::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return TextTrackList_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+TextTrack* TextTrackList::IndexedGetter(uint32_t aIndex, bool& aFound) {
+ aFound = aIndex < mTextTracks.Length();
+ if (!aFound) {
+ return nullptr;
+ }
+ return mTextTracks[aIndex];
+}
+
+TextTrack* TextTrackList::operator[](uint32_t aIndex) {
+ return mTextTracks.SafeElementAt(aIndex, nullptr);
+}
+
+already_AddRefed<TextTrack> TextTrackList::AddTextTrack(
+ TextTrackKind aKind, const nsAString& aLabel, const nsAString& aLanguage,
+ TextTrackMode aMode, TextTrackReadyState aReadyState,
+ TextTrackSource aTextTrackSource, const CompareTextTracks& aCompareTT) {
+ RefPtr<TextTrack> track =
+ new TextTrack(GetOwner(), this, aKind, aLabel, aLanguage, aMode,
+ aReadyState, aTextTrackSource);
+ AddTextTrack(track, aCompareTT);
+ return track.forget();
+}
+
+void TextTrackList::AddTextTrack(TextTrack* aTextTrack,
+ const CompareTextTracks& aCompareTT) {
+ if (mTextTracks.Contains(aTextTrack)) {
+ return;
+ }
+ mTextTracks.InsertElementSorted(aTextTrack, aCompareTT);
+ aTextTrack->SetTextTrackList(this);
+ CreateAndDispatchTrackEventRunner(aTextTrack, u"addtrack"_ns);
+}
+
+TextTrack* TextTrackList::GetTrackById(const nsAString& aId) {
+ nsAutoString id;
+ for (uint32_t i = 0; i < Length(); i++) {
+ mTextTracks[i]->GetId(id);
+ if (aId.Equals(id)) {
+ return mTextTracks[i];
+ }
+ }
+ return nullptr;
+}
+
+void TextTrackList::RemoveTextTrack(TextTrack* aTrack) {
+ if (mTextTracks.RemoveElement(aTrack)) {
+ CreateAndDispatchTrackEventRunner(aTrack, u"removetrack"_ns);
+ }
+}
+
+class TrackEventRunner : public Runnable {
+ public:
+ TrackEventRunner(TextTrackList* aList, Event* aEvent)
+ : Runnable("dom::TrackEventRunner"), mList(aList), mEvent(aEvent) {}
+
+ NS_IMETHOD Run() override { return mList->DispatchTrackEvent(mEvent); }
+
+ RefPtr<TextTrackList> mList;
+
+ private:
+ RefPtr<Event> mEvent;
+};
+
+nsresult TextTrackList::DispatchTrackEvent(Event* aEvent) {
+ return DispatchTrustedEvent(aEvent);
+}
+
+void TextTrackList::CreateAndDispatchChangeEvent() {
+ MOZ_ASSERT(NS_IsMainThread());
+ nsPIDOMWindowInner* win = GetOwner();
+ if (!win) {
+ return;
+ }
+
+ RefPtr<Event> event = NS_NewDOMEvent(this, nullptr, nullptr);
+
+ event->InitEvent(u"change"_ns, false, false);
+ event->SetTrusted(true);
+
+ nsCOMPtr<nsIRunnable> eventRunner = new TrackEventRunner(this, event);
+ nsGlobalWindowInner::Cast(win)->Dispatch(TaskCategory::Other,
+ eventRunner.forget());
+}
+
+void TextTrackList::CreateAndDispatchTrackEventRunner(
+ TextTrack* aTrack, const nsAString& aEventName) {
+ DebugOnly<nsresult> rv;
+ nsCOMPtr<nsIEventTarget> target = GetMainThreadSerialEventTarget();
+ if (!target) {
+ // If we are not able to get the main-thread object we are shutting down.
+ return;
+ }
+
+ TrackEventInit eventInit;
+ eventInit.mTrack.SetValue().SetAsTextTrack() = aTrack;
+ RefPtr<TrackEvent> event =
+ TrackEvent::Constructor(this, aEventName, eventInit);
+
+ // Dispatch the TrackEvent asynchronously.
+ rv = target->Dispatch(do_AddRef(new TrackEventRunner(this, event)),
+ NS_DISPATCH_NORMAL);
+
+ // If we are shutting down this can file but it's still ok.
+ NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Dispatch failed");
+}
+
+HTMLMediaElement* TextTrackList::GetMediaElement() {
+ if (mTextTrackManager) {
+ return mTextTrackManager->mMediaElement;
+ }
+ return nullptr;
+}
+
+void TextTrackList::SetTextTrackManager(TextTrackManager* aTextTrackManager) {
+ mTextTrackManager = aTextTrackManager;
+}
+
+void TextTrackList::SetCuesInactive() {
+ for (uint32_t i = 0; i < Length(); i++) {
+ mTextTracks[i]->SetCuesInactive();
+ }
+}
+
+bool TextTrackList::AreTextTracksLoaded() {
+ // Return false if any texttrack is not loaded.
+ for (uint32_t i = 0; i < Length(); i++) {
+ if (!mTextTracks[i]->IsLoaded()) {
+ return false;
+ }
+ }
+ return true;
+}
+
+nsTArray<RefPtr<TextTrack>>& TextTrackList::GetTextTrackArray() {
+ return mTextTracks;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webvtt/TextTrackList.h b/dom/media/webvtt/TextTrackList.h
new file mode 100644
index 0000000000..712e3ae9c4
--- /dev/null
+++ b/dom/media/webvtt/TextTrackList.h
@@ -0,0 +1,79 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 et tw=78: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_TextTrackList_h
+#define mozilla_dom_TextTrackList_h
+
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/dom/TextTrack.h"
+#include "nsCycleCollectionParticipant.h"
+
+namespace mozilla::dom {
+
+class Event;
+class HTMLMediaElement;
+class TextTrackManager;
+class CompareTextTracks;
+class TrackEvent;
+class TrackEventRunner;
+
+class TextTrackList final : public DOMEventTargetHelper {
+ public:
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(TextTrackList, DOMEventTargetHelper)
+
+ explicit TextTrackList(nsPIDOMWindowInner* aOwnerWindow);
+ TextTrackList(nsPIDOMWindowInner* aOwnerWindow,
+ TextTrackManager* aTextTrackManager);
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ uint32_t Length() const { return mTextTracks.Length(); }
+
+ // Get all the current active cues.
+ void GetShowingCues(nsTArray<RefPtr<TextTrackCue>>& aCues);
+
+ TextTrack* IndexedGetter(uint32_t aIndex, bool& aFound);
+ TextTrack* operator[](uint32_t aIndex);
+
+ already_AddRefed<TextTrack> AddTextTrack(
+ TextTrackKind aKind, const nsAString& aLabel, const nsAString& aLanguage,
+ TextTrackMode aMode, TextTrackReadyState aReadyState,
+ TextTrackSource aTextTrackSource, const CompareTextTracks& aCompareTT);
+ TextTrack* GetTrackById(const nsAString& aId);
+
+ void AddTextTrack(TextTrack* aTextTrack, const CompareTextTracks& aCompareTT);
+
+ void RemoveTextTrack(TextTrack* aTrack);
+
+ HTMLMediaElement* GetMediaElement();
+ void SetTextTrackManager(TextTrackManager* aTextTrackManager);
+
+ nsresult DispatchTrackEvent(Event* aEvent);
+ void CreateAndDispatchChangeEvent();
+ void SetCuesInactive();
+
+ bool AreTextTracksLoaded();
+ nsTArray<RefPtr<TextTrack>>& GetTextTrackArray();
+
+ IMPL_EVENT_HANDLER(change)
+ IMPL_EVENT_HANDLER(addtrack)
+ IMPL_EVENT_HANDLER(removetrack)
+
+ private:
+ ~TextTrackList();
+
+ nsTArray<RefPtr<TextTrack>> mTextTracks;
+ RefPtr<TextTrackManager> mTextTrackManager;
+
+ void CreateAndDispatchTrackEventRunner(TextTrack* aTrack,
+ const nsAString& aEventName);
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_TextTrackList_h
diff --git a/dom/media/webvtt/TextTrackRegion.cpp b/dom/media/webvtt/TextTrackRegion.cpp
new file mode 100644
index 0000000000..d883659579
--- /dev/null
+++ b/dom/media/webvtt/TextTrackRegion.cpp
@@ -0,0 +1,58 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 et tw=78: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/dom/TextTrackRegion.h"
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(TextTrackRegion, mParent)
+NS_IMPL_CYCLE_COLLECTING_ADDREF(TextTrackRegion)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(TextTrackRegion)
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TextTrackRegion)
+ NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
+ NS_INTERFACE_MAP_ENTRY(nsISupports)
+NS_INTERFACE_MAP_END
+
+JSObject* TextTrackRegion::WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) {
+ return VTTRegion_Binding::Wrap(aCx, this, aGivenProto);
+}
+
+already_AddRefed<TextTrackRegion> TextTrackRegion::Constructor(
+ const GlobalObject& aGlobal, ErrorResult& aRv) {
+ nsCOMPtr<nsPIDOMWindowInner> window =
+ do_QueryInterface(aGlobal.GetAsSupports());
+ if (!window) {
+ aRv.Throw(NS_ERROR_FAILURE);
+ return nullptr;
+ }
+
+ RefPtr<TextTrackRegion> region = new TextTrackRegion(aGlobal.GetAsSupports());
+ return region.forget();
+}
+
+TextTrackRegion::TextTrackRegion(nsISupports* aGlobal)
+ : mParent(aGlobal),
+ mWidth(100),
+ mLines(3),
+ mRegionAnchorX(0),
+ mRegionAnchorY(100),
+ mViewportAnchorX(0),
+ mViewportAnchorY(100),
+ mScroll(ScrollSetting::_empty) {}
+
+void TextTrackRegion::CopyValues(TextTrackRegion& aRegion) {
+ mId = aRegion.Id();
+ mWidth = aRegion.Width();
+ mLines = aRegion.Lines();
+ mRegionAnchorX = aRegion.RegionAnchorX();
+ mRegionAnchorY = aRegion.RegionAnchorY();
+ mViewportAnchorX = aRegion.ViewportAnchorX();
+ mViewportAnchorY = aRegion.ViewportAnchorY();
+ mScroll = aRegion.Scroll();
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webvtt/TextTrackRegion.h b/dom/media/webvtt/TextTrackRegion.h
new file mode 100644
index 0000000000..d316d7a30c
--- /dev/null
+++ b/dom/media/webvtt/TextTrackRegion.h
@@ -0,0 +1,138 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 et tw=78: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_TextTrackRegion_h
+#define mozilla_dom_TextTrackRegion_h
+
+#include "nsCycleCollectionParticipant.h"
+#include "nsString.h"
+#include "nsWrapperCache.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/dom/TextTrack.h"
+#include "mozilla/dom/VTTRegionBinding.h"
+#include "mozilla/Preferences.h"
+
+namespace mozilla::dom {
+
+class GlobalObject;
+class TextTrack;
+
+class TextTrackRegion final : public nsISupports, public nsWrapperCache {
+ public:
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(TextTrackRegion)
+
+ JSObject* WrapObject(JSContext* aCx,
+ JS::Handle<JSObject*> aGivenProto) override;
+
+ nsISupports* GetParentObject() const { return mParent; }
+
+ explicit TextTrackRegion(nsISupports* aGlobal);
+
+ /** WebIDL Methods. */
+
+ static already_AddRefed<TextTrackRegion> Constructor(
+ const GlobalObject& aGlobal, ErrorResult& aRv);
+
+ double Lines() const { return mLines; }
+
+ void SetLines(double aLines, ErrorResult& aRv) {
+ if (aLines < 0) {
+ aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
+ } else {
+ mLines = aLines;
+ }
+ }
+
+ double Width() const { return mWidth; }
+
+ void SetWidth(double aWidth, ErrorResult& aRv) {
+ if (!InvalidValue(aWidth, aRv)) {
+ mWidth = aWidth;
+ }
+ }
+
+ double RegionAnchorX() const { return mRegionAnchorX; }
+
+ void SetRegionAnchorX(double aVal, ErrorResult& aRv) {
+ if (!InvalidValue(aVal, aRv)) {
+ mRegionAnchorX = aVal;
+ }
+ }
+
+ double RegionAnchorY() const { return mRegionAnchorY; }
+
+ void SetRegionAnchorY(double aVal, ErrorResult& aRv) {
+ if (!InvalidValue(aVal, aRv)) {
+ mRegionAnchorY = aVal;
+ }
+ }
+
+ double ViewportAnchorX() const { return mViewportAnchorX; }
+
+ void SetViewportAnchorX(double aVal, ErrorResult& aRv) {
+ if (!InvalidValue(aVal, aRv)) {
+ mViewportAnchorX = aVal;
+ }
+ }
+
+ double ViewportAnchorY() const { return mViewportAnchorY; }
+
+ void SetViewportAnchorY(double aVal, ErrorResult& aRv) {
+ if (!InvalidValue(aVal, aRv)) {
+ mViewportAnchorY = aVal;
+ }
+ }
+
+ ScrollSetting Scroll() const { return mScroll; }
+
+ void SetScroll(const ScrollSetting& aScroll) {
+ if (aScroll == ScrollSetting::_empty || aScroll == ScrollSetting::Up) {
+ mScroll = aScroll;
+ }
+ }
+
+ void GetId(nsAString& aId) const { aId = mId; }
+
+ void SetId(const nsAString& aId) { mId = aId; }
+
+ /** end WebIDL Methods. */
+
+ // Helper to aid copying of a given TextTrackRegion's width, lines,
+ // anchor, viewport and scroll values.
+ void CopyValues(TextTrackRegion& aRegion);
+
+ // -----helpers-------
+ const nsAString& Id() const { return mId; }
+
+ private:
+ ~TextTrackRegion() = default;
+
+ nsCOMPtr<nsISupports> mParent;
+ nsString mId;
+ double mWidth;
+ long mLines;
+ double mRegionAnchorX;
+ double mRegionAnchorY;
+ double mViewportAnchorX;
+ double mViewportAnchorY;
+ ScrollSetting mScroll;
+
+ // Helper to ensure new value is in the range: 0.0% - 100.0%; throws
+ // an IndexSizeError otherwise.
+ inline bool InvalidValue(double aValue, ErrorResult& aRv) {
+ if (aValue < 0.0 || aValue > 100.0) {
+ aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR);
+ return true;
+ }
+
+ return false;
+ }
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_TextTrackRegion_h
diff --git a/dom/media/webvtt/WebVTTListener.cpp b/dom/media/webvtt/WebVTTListener.cpp
new file mode 100644
index 0000000000..3f8d99a8f3
--- /dev/null
+++ b/dom/media/webvtt/WebVTTListener.cpp
@@ -0,0 +1,212 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "WebVTTListener.h"
+#include "mozilla/CycleCollectedJSContext.h"
+#include "mozilla/dom/Document.h"
+#include "mozilla/dom/HTMLTrackElement.h"
+#include "mozilla/dom/TextTrackCue.h"
+#include "mozilla/dom/TextTrackRegion.h"
+#include "mozilla/dom/VTTRegionBinding.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIAsyncVerifyRedirectCallback.h"
+#include "nsIInputStream.h"
+
+extern mozilla::LazyLogModule gTextTrackLog;
+#define LOG(msg, ...) \
+ MOZ_LOG(gTextTrackLog, LogLevel::Debug, \
+ ("WebVTTListener=%p, " msg, this, ##__VA_ARGS__))
+#define LOG_WIHTOUT_ADDRESS(msg, ...) \
+ MOZ_LOG(gTextTrackLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
+
+namespace mozilla::dom {
+
+NS_IMPL_CYCLE_COLLECTION(WebVTTListener, mElement, mParserWrapper)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(WebVTTListener)
+ NS_INTERFACE_MAP_ENTRY(nsIWebVTTListener)
+ NS_INTERFACE_MAP_ENTRY(nsIStreamListener)
+ NS_INTERFACE_MAP_ENTRY(nsIChannelEventSink)
+ NS_INTERFACE_MAP_ENTRY(nsIInterfaceRequestor)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIWebVTTListener)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_CYCLE_COLLECTING_ADDREF(WebVTTListener)
+NS_IMPL_CYCLE_COLLECTING_RELEASE(WebVTTListener)
+
+WebVTTListener::WebVTTListener(HTMLTrackElement* aElement)
+ : mElement(aElement), mParserWrapperError(NS_OK) {
+ MOZ_ASSERT(mElement, "Must pass an element to the callback");
+ LOG("Created listener for track element %p", aElement);
+ MOZ_DIAGNOSTIC_ASSERT(
+ CycleCollectedJSContext::Get() &&
+ !CycleCollectedJSContext::Get()->IsInStableOrMetaStableState());
+ mParserWrapper = do_CreateInstance(NS_WEBVTTPARSERWRAPPER_CONTRACTID,
+ &mParserWrapperError);
+ if (NS_SUCCEEDED(mParserWrapperError)) {
+ nsPIDOMWindowInner* window = mElement->OwnerDoc()->GetInnerWindow();
+ mParserWrapperError = mParserWrapper->LoadParser(window);
+ }
+ if (NS_SUCCEEDED(mParserWrapperError)) {
+ mParserWrapperError = mParserWrapper->Watch(this);
+ }
+}
+
+WebVTTListener::~WebVTTListener() { LOG("destroyed."); }
+
+NS_IMETHODIMP
+WebVTTListener::GetInterface(const nsIID& aIID, void** aResult) {
+ return QueryInterface(aIID, aResult);
+}
+
+nsresult WebVTTListener::LoadResource() {
+ if (IsCanceled()) {
+ return NS_OK;
+ }
+ // Exit if we failed to create the WebVTTParserWrapper (vtt.jsm)
+ NS_ENSURE_SUCCESS(mParserWrapperError, mParserWrapperError);
+
+ mElement->SetReadyState(TextTrackReadyState::Loading);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WebVTTListener::AsyncOnChannelRedirect(nsIChannel* aOldChannel,
+ nsIChannel* aNewChannel, uint32_t aFlags,
+ nsIAsyncVerifyRedirectCallback* cb) {
+ if (IsCanceled()) {
+ return NS_OK;
+ }
+ if (mElement) {
+ mElement->OnChannelRedirect(aOldChannel, aNewChannel, aFlags);
+ }
+ cb->OnRedirectVerifyCallback(NS_OK);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WebVTTListener::OnStartRequest(nsIRequest* aRequest) {
+ if (IsCanceled()) {
+ return NS_OK;
+ }
+
+ LOG("OnStartRequest");
+ mElement->DispatchTestEvent(u"mozStartedLoadingTextTrack"_ns);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WebVTTListener::OnStopRequest(nsIRequest* aRequest, nsresult aStatus) {
+ if (IsCanceled()) {
+ return NS_OK;
+ }
+
+ LOG("OnStopRequest");
+ if (NS_FAILED(aStatus)) {
+ LOG("Got error status");
+ mElement->SetReadyState(TextTrackReadyState::FailedToLoad);
+ }
+ // Attempt to parse any final data the parser might still have.
+ mParserWrapper->Flush();
+ if (mElement->ReadyState() != TextTrackReadyState::FailedToLoad) {
+ mElement->SetReadyState(TextTrackReadyState::Loaded);
+ }
+
+ mElement->CancelChannelAndListener();
+
+ return aStatus;
+}
+
+nsresult WebVTTListener::ParseChunk(nsIInputStream* aInStream, void* aClosure,
+ const char* aFromSegment,
+ uint32_t aToOffset, uint32_t aCount,
+ uint32_t* aWriteCount) {
+ nsCString buffer(aFromSegment, aCount);
+ WebVTTListener* listener = static_cast<WebVTTListener*>(aClosure);
+ MOZ_ASSERT(!listener->IsCanceled());
+
+ if (NS_FAILED(listener->mParserWrapper->Parse(buffer))) {
+ LOG_WIHTOUT_ADDRESS(
+ "WebVTTListener=%p, Unable to parse chunk of WEBVTT text. Aborting.",
+ listener);
+ *aWriteCount = 0;
+ return NS_ERROR_FAILURE;
+ }
+
+ *aWriteCount = aCount;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WebVTTListener::OnDataAvailable(nsIRequest* aRequest, nsIInputStream* aStream,
+ uint64_t aOffset, uint32_t aCount) {
+ if (IsCanceled()) {
+ return NS_OK;
+ }
+
+ LOG("OnDataAvailable");
+ uint32_t count = aCount;
+ while (count > 0) {
+ uint32_t read;
+ nsresult rv = aStream->ReadSegments(ParseChunk, this, count, &read);
+ NS_ENSURE_SUCCESS(rv, rv);
+ if (!read) {
+ return NS_ERROR_FAILURE;
+ }
+ count -= read;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WebVTTListener::OnCue(JS::Handle<JS::Value> aCue, JSContext* aCx) {
+ MOZ_ASSERT(!IsCanceled());
+ if (!aCue.isObject()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ JS::Rooted<JSObject*> obj(aCx, &aCue.toObject());
+ TextTrackCue* cue = nullptr;
+ nsresult rv = UNWRAP_OBJECT(VTTCue, &obj, cue);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ cue->SetTrackElement(mElement);
+ mElement->mTrack->AddCue(*cue);
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WebVTTListener::OnRegion(JS::Handle<JS::Value> aRegion, JSContext* aCx) {
+ MOZ_ASSERT(!IsCanceled());
+ // Nothing for this callback to do.
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+WebVTTListener::OnParsingError(int32_t errorCode, JSContext* cx) {
+ MOZ_ASSERT(!IsCanceled());
+ // We only care about files that have a bad WebVTT file signature right now
+ // as that means the file failed to load.
+ if (errorCode == ErrorCodes::BadSignature) {
+ LOG("parsing error");
+ mElement->SetReadyState(TextTrackReadyState::FailedToLoad);
+ }
+ return NS_OK;
+}
+
+bool WebVTTListener::IsCanceled() const { return mCancel; }
+
+void WebVTTListener::Cancel() {
+ MOZ_ASSERT(!IsCanceled(), "Do not cancel canceled listener again!");
+ LOG("Cancel listen to channel's response.");
+ mCancel = true;
+ mParserWrapper->Cancel();
+ mParserWrapper = nullptr;
+ mElement = nullptr;
+}
+
+} // namespace mozilla::dom
diff --git a/dom/media/webvtt/WebVTTListener.h b/dom/media/webvtt/WebVTTListener.h
new file mode 100644
index 0000000000..45f6bc90d6
--- /dev/null
+++ b/dom/media/webvtt/WebVTTListener.h
@@ -0,0 +1,69 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_WebVTTLoadListener_h
+#define mozilla_dom_WebVTTLoadListener_h
+
+#include "nsIWebVTTListener.h"
+#include "nsIStreamListener.h"
+#include "nsIChannelEventSink.h"
+#include "nsIInterfaceRequestor.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionParticipant.h"
+
+class nsIWebVTTParserWrapper;
+
+namespace mozilla::dom {
+
+class HTMLTrackElement;
+
+class WebVTTListener final : public nsIWebVTTListener,
+ public nsIStreamListener,
+ public nsIChannelEventSink,
+ public nsIInterfaceRequestor {
+ NS_DECL_CYCLE_COLLECTING_ISUPPORTS
+ NS_DECL_NSIWEBVTTLISTENER
+ NS_DECL_NSIREQUESTOBSERVER
+ NS_DECL_NSISTREAMLISTENER
+ NS_DECL_NSICHANNELEVENTSINK
+ NS_DECL_NSIINTERFACEREQUESTOR
+
+ NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(WebVTTListener, nsIStreamListener)
+
+ public:
+ explicit WebVTTListener(HTMLTrackElement* aElement);
+
+ /**
+ * Loads the WebVTTListener. Must call this in order for the listener to be
+ * ready to parse data that is passed to it.
+ */
+ nsresult LoadResource();
+
+ /**
+ * When this listener is not going to be used anymore, its owner should take
+ * a responsibility to call `Cancel()` to prevent this listener making any
+ * changes for the track element.
+ */
+ bool IsCanceled() const;
+ void Cancel();
+
+ private:
+ ~WebVTTListener();
+
+ // List of error codes returned from the WebVTT parser that we care about.
+ enum ErrorCodes { BadSignature = 0 };
+ static nsresult ParseChunk(nsIInputStream* aInStream, void* aClosure,
+ const char* aFromSegment, uint32_t aToOffset,
+ uint32_t aCount, uint32_t* aWriteCount);
+
+ RefPtr<HTMLTrackElement> mElement;
+ nsCOMPtr<nsIWebVTTParserWrapper> mParserWrapper;
+ nsresult mParserWrapperError;
+ bool mCancel = false;
+};
+
+} // namespace mozilla::dom
+
+#endif // mozilla_dom_WebVTTListener_h
diff --git a/dom/media/webvtt/WebVTTParserWrapper.sys.mjs b/dom/media/webvtt/WebVTTParserWrapper.sys.mjs
new file mode 100644
index 0000000000..19cf08ca55
--- /dev/null
+++ b/dom/media/webvtt/WebVTTParserWrapper.sys.mjs
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { WebVTT } from "resource://gre/modules/vtt.sys.mjs";
+
+export function WebVTTParserWrapper() {
+ // Nothing
+}
+
+WebVTTParserWrapper.prototype = {
+ loadParser(window) {
+ this.parser = new WebVTT.Parser(window, new TextDecoder("utf8"));
+ },
+
+ parse(data) {
+ // We can safely translate the string data to a Uint8Array as we are
+ // guaranteed character codes only from \u0000 => \u00ff
+ var buffer = new Uint8Array(data.length);
+ for (var i = 0; i < data.length; i++) {
+ buffer[i] = data.charCodeAt(i);
+ }
+
+ this.parser.parse(buffer);
+ },
+
+ flush() {
+ this.parser.flush();
+ },
+
+ watch(callback) {
+ this.parser.oncue = callback.onCue;
+ this.parser.onregion = callback.onRegion;
+ this.parser.onparsingerror = function (e) {
+ // Passing the just the error code back is enough for our needs.
+ callback.onParsingError("code" in e ? e.code : -1);
+ };
+ },
+
+ cancel() {
+ this.parser.oncue = null;
+ this.parser.onregion = null;
+ this.parser.onparsingerror = null;
+ },
+
+ convertCueToDOMTree(window, cue) {
+ return WebVTT.convertCueToDOMTree(window, cue.text);
+ },
+
+ processCues(window, cues, overlay, controls) {
+ WebVTT.processCues(window, cues, overlay, controls);
+ },
+
+ classDescription: "Wrapper for the JS WebVTT implementation (vtt.js)",
+ QueryInterface: ChromeUtils.generateQI(["nsIWebVTTParserWrapper"]),
+};
diff --git a/dom/media/webvtt/components.conf b/dom/media/webvtt/components.conf
new file mode 100644
index 0000000000..21fc95c13d
--- /dev/null
+++ b/dom/media/webvtt/components.conf
@@ -0,0 +1,14 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+ {
+ 'cid': '{acf6e493-0092-4b26-b172-241e375c57ab}',
+ 'contract_ids': ['@mozilla.org/webvttParserWrapper;1'],
+ 'esModule': 'resource://gre/modules/WebVTTParserWrapper.sys.mjs',
+ 'constructor': 'WebVTTParserWrapper',
+ },
+]
diff --git a/dom/media/webvtt/moz.build b/dom/media/webvtt/moz.build
new file mode 100644
index 0000000000..6125406b7a
--- /dev/null
+++ b/dom/media/webvtt/moz.build
@@ -0,0 +1,52 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS.mozilla.dom += [
+ "TextTrack.h",
+ "TextTrackCue.h",
+ "TextTrackCueList.h",
+ "TextTrackList.h",
+ "TextTrackRegion.h",
+ "WebVTTListener.h",
+]
+
+UNIFIED_SOURCES += [
+ "TextTrack.cpp",
+ "TextTrackCue.cpp",
+ "TextTrackCueList.cpp",
+ "TextTrackList.cpp",
+ "TextTrackRegion.cpp",
+ "WebVTTListener.cpp",
+]
+
+XPIDL_SOURCES += [
+ "nsIWebVTTListener.idl",
+ "nsIWebVTTParserWrapper.idl",
+]
+
+XPIDL_MODULE = "webvtt"
+
+EXTRA_JS_MODULES += [
+ "WebVTTParserWrapper.sys.mjs",
+]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+]
+
+EXTRA_JS_MODULES += [
+ "vtt.sys.mjs",
+]
+
+XPCSHELL_TESTS_MANIFESTS += ["test/xpcshell/xpcshell.ini"]
+
+MOCHITEST_MANIFESTS += ["test/mochitest/mochitest.ini"]
+
+REFTEST_MANIFESTS += ["test/reftest/reftest.list"]
+
+CRASHTEST_MANIFESTS += ["test/crashtests/crashtests.list"]
+
+FINAL_LIBRARY = "xul"
diff --git a/dom/media/webvtt/nsIWebVTTListener.idl b/dom/media/webvtt/nsIWebVTTListener.idl
new file mode 100644
index 0000000000..b99d4cf517
--- /dev/null
+++ b/dom/media/webvtt/nsIWebVTTListener.idl
@@ -0,0 +1,37 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+/**
+ * Listener for a JS WebVTT parser (vtt.js).
+ */
+[scriptable, uuid(8a2d7780-2045-4a29-99f4-df15cae5fc49)]
+interface nsIWebVTTListener : nsISupports
+{
+ /**
+ * Is called when the WebVTTParser successfully parses a WebVTT cue.
+ *
+ * @param cue An object representing the data of a parsed WebVTT cue.
+ */
+ [implicit_jscontext]
+ void onCue(in jsval cue);
+
+ /**
+ * Is called when the WebVTT parser successfully parses a WebVTT region.
+ *
+ * @param region An object representing the data of a parsed
+ * WebVTT region.
+ */
+ [implicit_jscontext]
+ void onRegion(in jsval region);
+
+ /**
+ * Is called when the WebVTT parser encounters a parsing error.
+ *
+ * @param error The error code of the ParserError the occured.
+ */
+ [implicit_jscontext]
+ void onParsingError(in long errorCode);
+};
diff --git a/dom/media/webvtt/nsIWebVTTParserWrapper.idl b/dom/media/webvtt/nsIWebVTTParserWrapper.idl
new file mode 100644
index 0000000000..76725d568b
--- /dev/null
+++ b/dom/media/webvtt/nsIWebVTTParserWrapper.idl
@@ -0,0 +1,94 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIWebVTTListener;
+interface mozIDOMWindow;
+interface nsIVariant;
+
+webidl DocumentFragment;
+
+/**
+ * Interface for a wrapper of a JS WebVTT parser (vtt.js).
+ */
+[scriptable, uuid(8dfe016e-1701-4618-9f5e-9a6154e853f0)]
+interface nsIWebVTTParserWrapper : nsISupports
+{
+ /**
+ * Loads the JS WebVTTParser and sets it to use the passed window to create
+ * VTTRegions and VTTCues. This function must be called before calling
+ * parse, flush, or watch.
+ *
+ * @param window The window that the parser will use to create VTTCues and
+ * VTTRegions.
+ *
+ */
+ void loadParser(in mozIDOMWindow window);
+
+ /**
+ * Attempts to parse the stream's data as WebVTT format. When it successfully
+ * parses a WebVTT region or WebVTT cue it will create a VTTRegion or VTTCue
+ * object and pass it back to the callee through its callbacks.
+ *
+ * @param data The buffer that contains the WebVTT data received by the
+ * Necko consumer so far.
+ */
+ void parse(in ACString data);
+
+ /**
+ * Flush indicates that no more data is expected from the stream. As such the
+ * parser should try to parse any kind of partial data it has.
+ */
+ void flush();
+
+ /**
+ * Set this parser object to use an nsIWebVTTListener object for its onCue
+ * and onRegion callbacks.
+ *
+ * @param callback The nsIWebVTTListener object that exposes onCue and
+ * onRegion callbacks for the parser.
+ */
+ void watch(in nsIWebVTTListener callback);
+
+ /**
+ * Cancel watching notifications which parser would send.
+ */
+ void cancel();
+
+ /**
+ * Convert the text content of a WebVTT cue to a document fragment so that
+ * we can display it on the page.
+ *
+ * @param window A window object with which the document fragment will be
+ * created.
+ * @param cue The cue whose content will be converted to a document
+ * fragment.
+ */
+ DocumentFragment convertCueToDOMTree(in mozIDOMWindow window,
+ in nsISupports cue);
+
+
+ /**
+ * Compute the display state of the VTTCues in cues along with any VTTRegions
+ * that they might be in. First, it computes the positioning and styling of
+ * the cues and regions passed and converts them into a DOM tree rooted at
+ * a containing HTMLDivElement. It then adjusts those computed divs for
+ * overlap avoidance using the dimensions of 'overlay'. Finally, it adds the
+ * computed divs to the VTTCues display state property for use later.
+ *
+ * @param window A window object with which it will create the DOM tree
+ * and containing div element.
+ * @param cues An array of VTTCues who need there display state to be
+ * computed.
+ * @param overlay The HTMLElement that the cues will be displayed within.
+ * @param controls The video control element that will affect cues position.
+ */
+ void processCues(in mozIDOMWindow window, in nsIVariant cues,
+ in nsISupports overlay, in nsISupports controls);
+};
+
+%{C++
+#define NS_WEBVTTPARSERWRAPPER_CONTRACTID "@mozilla.org/webvttParserWrapper;1"
+%}
diff --git a/dom/media/webvtt/package.json b/dom/media/webvtt/package.json
new file mode 100644
index 0000000000..2952b78c01
--- /dev/null
+++ b/dom/media/webvtt/package.json
@@ -0,0 +1,6 @@
+{
+ "devDependencies": {
+ "gift": "~0.0.6",
+ "optimist": "~0.6.0"
+ }
+}
diff --git a/dom/media/webvtt/test/crashtests/1304948.html b/dom/media/webvtt/test/crashtests/1304948.html
new file mode 100644
index 0000000000..667a13d06a
--- /dev/null
+++ b/dom/media/webvtt/test/crashtests/1304948.html
@@ -0,0 +1,33 @@
+<html class="reftest-wait">
+<head>
+ <title> Bug 1304948 : Crash if a texttrack remove a cue not belongs to it. </title>
+</head>
+<meta charset="utf-8">
+<script type="text/javascript">
+
+window.onload = function() {
+ var a = document.createElementNS('http://www.w3.org/1999/xhtml', 'video');
+ a.src = "";
+ document.body.appendChild(a);
+ var b = a.addTextTrack('chapters', "AAAAAAAAAAAAAAAA", "de");
+ var c = new VTTCue(0.6, 0.3, "AA");
+ b.addCue(c);
+ var d = document.createElementNS('http://www.w3.org/1999/xhtml', 'video');
+ var e = d.addTextTrack('chapters', "AAAA", "en-US");
+ a.currentTime = 2;
+ a.play();
+ try {
+ // This will queue a TimeMarchesOn task on mainthread, so use
+ // timer to wait the TimeMarchesOn crash.
+ e.removeCue(c);
+ } catch (e) {
+ if (e.name == "NotFoundError") {
+ setTimeout(function() {
+ document.documentElement.removeAttribute("class");}, 0);
+ }
+ }
+};
+
+</script>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/crashtests/1319486.html b/dom/media/webvtt/test/crashtests/1319486.html
new file mode 100644
index 0000000000..74bdb2147c
--- /dev/null
+++ b/dom/media/webvtt/test/crashtests/1319486.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <title> Bug 1319486 : Crash if a texttrackcue added to different texttracks. </title>
+</head>
+<body>
+<video id=v1></video>
+<video id=v2></video>
+</body>
+<meta charset="utf-8">
+<script type="text/javascript">
+
+addEventListener('DOMContentLoaded', function(){
+ let cue,tt1,tt2;
+ v1.play();
+ tt1 = v1.addTextTrack('metadata', "", "");
+ v1.play();
+ v1.currentTime = 8;
+ cue = new VTTCue(0, 0.5, "");
+ tt1.addCue(cue);
+ tt2 = v2.addTextTrack('captions', "", "");
+ tt2.addCue(cue);
+ tt2.removeCue(cue);
+ tt1.addCue(new VTTCue(0.7, 2, ""));
+});
+</script>
+</html>
diff --git a/dom/media/webvtt/test/crashtests/1533909.html b/dom/media/webvtt/test/crashtests/1533909.html
new file mode 100644
index 0000000000..ee73ecb7b8
--- /dev/null
+++ b/dom/media/webvtt/test/crashtests/1533909.html
@@ -0,0 +1,17 @@
+<script>
+function eh1() {
+ d.track.addCue(new VTTCue(0.01, 0.69, "Y"))
+}
+function eh2() {
+ a.currentTime = 0.43
+ c.addEventListener("DOMNodeRemoved", eh1)
+ a.append(b)
+ a.appendChild(c)
+}
+</script>
+<canvas id="c">
+<q id="b">
+</canvas>
+<audio id="a" src="data:audio/mpeg;base64,/+M4wAAAAAAAAAAAAEluZm8AAAAPAAAAAwAAAbAAqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV1dXV////////////////////////////////////////////AAAAAExhdmM1Ny4zOAAAAAAAAAAAAAAAACQAAAAAAAAAAAGwU/hwzwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/+MYxAAMkI7huAhMDGQAndv+H5jfIAPmPzGBGeTAZNN7yjsnqd6Fny79M+o4UifnMu/TlDiIP5Q5l9Z/Kdv6VY0gE3JIAAwE/+MYxAgMYJr6WBhGIivjMy/CEK+zM39VVXjMArKgq6DQdrBV0THlqBp0ShrBp8qGsTPw7lXZFVZZY6GytZZYGChgYIGZaiJI/+MYxBEMUJnAABmMYUigMQLIE1yVpphA1UQMRVVVV000iVu000VVVX///6aaVUxBTUUzLjk5LjVVVVVVVVVVVVVVVVVVVVVV">
+<track id="d"></track>
+<iframe onload="eh2()">
diff --git a/dom/media/webvtt/test/crashtests/882549.html b/dom/media/webvtt/test/crashtests/882549.html
new file mode 100644
index 0000000000..8a720c3e11
--- /dev/null
+++ b/dom/media/webvtt/test/crashtests/882549.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <script>
+ var o0 = new VTTCue(0.000, 1.000, 'Bug882549');
+ var o1 = o0.getCueAsHTML();
+ </script>
+ </head>
+ <body>
+ </body>
+</html>
+<script>
+</script>
diff --git a/dom/media/webvtt/test/crashtests/894104.html b/dom/media/webvtt/test/crashtests/894104.html
new file mode 100644
index 0000000000..d021994e74
--- /dev/null
+++ b/dom/media/webvtt/test/crashtests/894104.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<script>
+
+function boom()
+{
+ var frame = document.getElementById("f");
+ var frameWin = frame.contentWindow;
+ frameWin.VTTCue;
+ document.body.removeChild(frame);
+ new frameWin.VTTCue(0, 1, "Bug 894104").getCueAsHTML();
+}
+
+</script>
+</head>
+
+<body onload="boom();"><iframe id="f" src="data:text/html,1"></iframe></body>
+</html>
diff --git a/dom/media/webvtt/test/crashtests/crashtests.list b/dom/media/webvtt/test/crashtests/crashtests.list
new file mode 100644
index 0000000000..c9776984bc
--- /dev/null
+++ b/dom/media/webvtt/test/crashtests/crashtests.list
@@ -0,0 +1,5 @@
+load 1304948.html
+load 1319486.html
+load 1533909.html
+load 882549.html
+load 894104.html
diff --git a/dom/media/webvtt/test/mochitest/bad-signature.vtt b/dom/media/webvtt/test/mochitest/bad-signature.vtt
new file mode 100644
index 0000000000..c9a59b35e9
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/bad-signature.vtt
@@ -0,0 +1 @@
+WEB
diff --git a/dom/media/webvtt/test/mochitest/basic.vtt b/dom/media/webvtt/test/mochitest/basic.vtt
new file mode 100644
index 0000000000..45646ab868
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/basic.vtt
@@ -0,0 +1,29 @@
+WEBVTT
+REGION
+id:testOne lines:2 width:30%
+REGION
+id:testTwo lines:4 width:20%
+
+1
+00:00.500 --> 00:00.700 region:testOne
+This
+
+2
+00:01.200 --> 00:02.400 region:testTwo
+Is
+
+2.5
+00:02.000 --> 00:03.500 region:testOne
+(Over here?!)
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+5
+00:03.217 --> 00:03.989
+And more!
diff --git a/dom/media/webvtt/test/mochitest/bug883173.vtt b/dom/media/webvtt/test/mochitest/bug883173.vtt
new file mode 100644
index 0000000000..61f086bcce
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/bug883173.vtt
@@ -0,0 +1,16 @@
+WEBVTT
+
+00:03.000 --> 00:04.000
+Should display fifth.
+
+00:01.000 --> 00:02.000
+Should display first.
+
+00:01.000 --> 00:03.000
+Should display second.
+
+00:02.000 --> 00:04.000
+Should display forth.
+
+00:02.000 --> 00:03.000
+Should display third.
diff --git a/dom/media/webvtt/test/mochitest/long.vtt b/dom/media/webvtt/test/mochitest/long.vtt
new file mode 100644
index 0000000000..23984b0c8d
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/long.vtt
@@ -0,0 +1,8001 @@
+WEBVTT
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
+
+1
+00:00.500 --> 00:00.700
+This
+
+2
+00:01.200 --> 00:02.400
+Is
+
+3
+00:02.710 --> 00:02.910
+A
+
+4
+00:03.217 --> 00:03.989
+Test
diff --git a/dom/media/webvtt/test/mochitest/manifest.js b/dom/media/webvtt/test/mochitest/manifest.js
new file mode 100644
index 0000000000..91c481feb9
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/manifest.js
@@ -0,0 +1,27 @@
+// Force releasing decoder to avoid timeout in waiting for decoding resource.
+function removeNodeAndSource(n) {
+ n.remove();
+ // reset |srcObject| first since it takes precedence over |src|.
+ n.srcObject = null;
+ n.removeAttribute("src");
+ n.load();
+ while (n.firstChild) {
+ n.firstChild.remove();
+ }
+}
+
+function once(target, name, cb) {
+ var p = new Promise(function (resolve, reject) {
+ target.addEventListener(
+ name,
+ function () {
+ resolve();
+ },
+ { once: true }
+ );
+ });
+ if (cb) {
+ p.then(cb);
+ }
+ return p;
+}
diff --git a/dom/media/webvtt/test/mochitest/mochitest.ini b/dom/media/webvtt/test/mochitest/mochitest.ini
new file mode 100644
index 0000000000..ee905144b8
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/mochitest.ini
@@ -0,0 +1,50 @@
+[DEFAULT]
+subsuite = media
+tags = webvtt
+skip-if = (os == "win" && processor == "aarch64") # aarch64 due to 1536604
+support-files =
+ ../../../test/gizmo.mp4
+ ../../../test/seek.webm
+ ../../../test/vp9cake.webm
+ bad-signature.vtt
+ basic.vtt
+ bug883173.vtt
+ long.vtt
+ manifest.js
+ parser.vtt
+ region.vtt
+ sequential.vtt
+ vttPositionAlign.vtt
+
+[test_bug1018933.html]
+[test_bug1242594.html]
+[test_bug883173.html]
+skip-if = (android_version == '25' && debug) # android(bug 1232305)
+[test_bug895091.html]
+skip-if = (android_version == '25' && debug) # android(bug 1232305)
+[test_bug957847.html]
+skip-if = (android_version == '25' && debug) # android(bug 1232305)
+[test_trackelementevent.html]
+[test_trackelementsrc.html]
+[test_texttrack.html]
+[test_texttrackcue.html]
+[test_texttrackcue_moz.html]
+[test_trackevent.html]
+[test_texttrackevents_video.html]
+[test_texttracklist.html]
+[test_texttracklist_moz.html]
+[test_texttrackregion.html]
+[test_testtrack_cors_no_response.html]
+[test_texttrack_cors_preload_none.html]
+[test_texttrack_mode_change_during_loading.html]
+skip-if = true
+ toolkit == 'android' # Bug 1636572, android(bug 1562021)
+[test_texttrack_moz.html]
+[test_vttparser.html]
+[test_webvtt_empty_displaystate.html]
+[test_webvtt_event_same_time.html]
+[test_webvtt_infinite_processing_loop.html]
+[test_webvtt_overlapping_time.html]
+[test_webvtt_positionalign.html]
+[test_webvtt_seeking.html]
+[test_webvtt_update_display_after_adding_or_removing_cue.html]
diff --git a/dom/media/webvtt/test/mochitest/parser.vtt b/dom/media/webvtt/test/mochitest/parser.vtt
new file mode 100644
index 0000000000..2a56c65f80
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/parser.vtt
@@ -0,0 +1,6 @@
+WEBVTT
+
+00:00.500 --> 00:00.700
+Test
+00:00.500 --> 00:00.700
+Stuff
diff --git a/dom/media/webvtt/test/mochitest/region.vtt b/dom/media/webvtt/test/mochitest/region.vtt
new file mode 100644
index 0000000000..1e351dbcfb
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/region.vtt
@@ -0,0 +1,6 @@
+WEBVTT
+REGION
+id:fred width:62% lines:5 regionanchor:4%,78% viewportanchor:10%,90% scroll:up
+
+00:01.000 --> 00:02.000 region:fred
+Test here.
diff --git a/dom/media/webvtt/test/mochitest/sequential.vtt b/dom/media/webvtt/test/mochitest/sequential.vtt
new file mode 100644
index 0000000000..94e92e38ae
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/sequential.vtt
@@ -0,0 +1,10 @@
+WEBVTT
+
+00:01.000 --> 00:02.000
+This
+
+00:03.000 --> 00:04.000
+Is
+
+00:05.000 --> 00:06.000
+A Test
diff --git a/dom/media/webvtt/test/mochitest/test_bug1018933.html b/dom/media/webvtt/test/mochitest/test_bug1018933.html
new file mode 100644
index 0000000000..bff1db6021
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/test_bug1018933.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1018933
+-->
+<head>
+ <meta charset='utf-8'>
+ <title>Regression test for bug 1018933 - HTMLTrackElement should create only one TextTrack</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+var video = document.createElement("video");
+video.src = "seek.webm";
+video.preload = "auto";
+
+var trackElement = document.createElement("track");
+trackElement.src = "basic.vtt";
+trackElement.kind = "subtitles";
+
+document.getElementById("content").appendChild(video);
+video.appendChild(trackElement);
+
+// Accessing the track now would have caused the bug as the track element
+// shouldn't have had time to bind to the tree yet.
+trackElement.track.mode = 'showing';
+
+video.addEventListener("loadedmetadata", function run_tests() {
+ // Re-que run_tests() at the end of the event loop until the track
+ // element has loaded its data.
+ if (trackElement.readyState == 1) {
+ setTimeout(run_tests, 0);
+ return;
+ }
+
+ is(video.textTracks.length, 1, "Video should have one TextTrack.");
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/mochitest/test_bug1242594.html b/dom/media/webvtt/test/mochitest/test_bug1242594.html
new file mode 100644
index 0000000000..25c47948bb
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/test_bug1242594.html
@@ -0,0 +1,46 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=1242594
+-->
+<head>
+ <meta charset='utf-8'>
+ <title>Bug 1242594 - Unbind a video element with HTMLTrackElement
+ should not remove the TextTrack</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+var video = document.createElement("video");
+video.src = "seek.webm";
+video.preload = "auto";
+
+var trackElement = document.createElement("track");
+trackElement.src = "basic.vtt";
+trackElement.kind = "subtitles";
+
+document.getElementById("content").appendChild(video);
+video.appendChild(trackElement);
+
+// Bug 1242599, access video.textTracks.length immediately after
+// the track element binds into the media element.
+is(video.textTracks.length, 1, "Video should have one TextTrack.");
+var parent = video.parentNode;
+parent.removeChild(video);
+is(video.textTracks.length, 1, "After unbind the video element, should have one TextTrack.");
+parent.appendChild(video);
+is(video.textTracks.length, 1, "After bind the video element, should have one TextTrack.");
+SimpleTest.finish();
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/mochitest/test_bug883173.html b/dom/media/webvtt/test/mochitest/test_bug883173.html
new file mode 100644
index 0000000000..6e95f4e3ca
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/test_bug883173.html
@@ -0,0 +1,39 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Bug 883173 - TextTrackCue(List) Sorting</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<video id="v" src="seek.webm" preload="metadata">
+ <track src="bug883173.vtt" kind="subtitles" id="default" default>
+</video>
+<script type="text/javascript">
+/**
+ * This test is used to ensure that the cues in the cue list should be sorted by
+ * cues' start time and end time, not the present order in the file.
+ */
+function runTest() {
+ let trackElement = document.getElementById("default");
+ is(trackElement.readyState, 2, "Track::ReadyState should be set to LOADED.");
+
+ let expected = [[1, 3], [1, 2], [2, 4], [2, 3], [3, 4]];
+ let cueList = trackElement.track.cues;
+ is(cueList.length, expected.length, "Cue list length should be 5.");
+
+ for (let i = 0; i < expected.length; i++) {
+ is(cueList[i].startTime, expected[i][0],
+ `Cue's start time should be ${expected[i][0]}`);
+ is(cueList[i].endTime, expected[i][1],
+ `Cue's end time should be ${expected[i][1]}`);
+ }
+
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+onload = runTest;
+</script>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/mochitest/test_bug895091.html b/dom/media/webvtt/test/mochitest/test_bug895091.html
new file mode 100644
index 0000000000..6fa2629283
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/test_bug895091.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Bug 895091 - Integrating vtt.js</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="manifest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<video id="v" src="seek.webm" preload="metadata">
+ <track src="long.vtt" kind="subtitles" id="track1">
+ <track src="long.vtt" kind="subtitles" id="track2">
+</video>
+<script type="text/javascript">
+/**
+ * This test is used to ensure that we can load two track elements with large
+ * amount of cues at same time. In this test, both tracks are disable by default,
+ * we have to enable them in order to start loading.
+ */
+var trackElement = document.getElementById("track1");
+var trackElementTwo = document.getElementById("track2");
+
+async function runTest() {
+ enableBothTracks();
+ await waitUntilBothTracksLoaded();
+ checkTrackReadyStateShouldBeLoaded();
+ checkCuesAmount();
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+onload = runTest;
+
+/**
+ * The following are test helper functions.
+ */
+function enableBothTracks() {
+ // All tracks are `disable` on default. As we won't start loading for disabled
+ // tracks, we have to change their mode in order to start loading.
+ trackElement.track.mode = "hidden";
+ trackElementTwo.track.mode = "hidden";
+}
+
+async function waitUntilBothTracksLoaded() {
+ info(`wait until both tracks finish loading`);
+ await Promise.all([once(trackElement, "load"), once(trackElementTwo, "load")]);
+}
+
+function checkTrackReadyStateShouldBeLoaded() {
+ is(trackElement.readyState, 2, "Track::ReadyState should be set to LOADED.");
+ is(trackElementTwo.readyState, 2, "Track::ReadyState should be set to LOADED.");
+}
+
+function checkCuesAmount() {
+ is(trackElement.track.cues.length, 2000, "Cue list length should be 2000.");
+ is(trackElementTwo.track.cues.length, 2000, "Cue list length should be 2000.");
+}
+</script>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/mochitest/test_bug957847.html b/dom/media/webvtt/test/mochitest/test_bug957847.html
new file mode 100644
index 0000000000..8bbea81bf9
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/test_bug957847.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=957847
+-->
+<head>
+ <meta charset='utf-8'>
+ <title>Regression test for bug 957847 - Crash on TextTrack::AddCue </title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+var trackElement = document.createElement('track');
+trackElement.track.addCue(new VTTCue(0, 1, "A"));
+
+// We need to assert something for Mochitest to be happy.
+ok(true);
+SimpleTest.finish();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/mochitest/test_testtrack_cors_no_response.html b/dom/media/webvtt/test/mochitest/test_testtrack_cors_no_response.html
new file mode 100644
index 0000000000..e047f74eb3
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/test_testtrack_cors_no_response.html
@@ -0,0 +1,41 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Should not load CORS vtt file when server doesn't respond with correct CORS header</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="manifest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<video preload="none" crossorigin="anonymous">
+ <track src="http://example.com/tests/dom/canvas/test/crossorigin/video.sjs?name=tests/dom/media/webvtt/test/mochitest/basic.vtt&type=text/vtt" kind="subtitles" id="default" default>
+</video>
+<script type="text/javascript">
+/**
+ * This test is used to ensure that we shouldn't load CORS resource if server
+ * doesn't respond with correct CORS header. In this situation, loading should
+ * be expected to fail.
+ */
+async function runTest() {
+ await waitUntiTrackLoadError();
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+runTest();
+
+/**
+ * The following are test helper functions.
+ */
+async function waitUntiTrackLoadError() {
+ const trackElement = document.getElementById("default");
+ if (trackElement.readyState != 3) {
+ info(`wait until receiving error event`);
+ await once(trackElement, "error");
+ }
+ is(trackElement.readyState, 3, "Track::ReadyState should be set to ERROR.");
+ is(trackElement.track.cues.length, 0, "Cue list length should be 0.");
+}
+</script>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/mochitest/test_texttrack.html b/dom/media/webvtt/test/mochitest/test_texttrack.html
new file mode 100644
index 0000000000..69c4f24bec
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/test_texttrack.html
@@ -0,0 +1,158 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Bug 833386 - TextTrackList</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="manifest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<video id="v">
+<script type="text/javascript">
+/**
+ * This test is used to check different things.
+ * (1) the default value of track element's attributes
+ * (2) readonly attributes can't be modifted
+ * (3) the order of tracks in the media element's track list
+ */
+var enabledTrackElement = null;
+
+async function runTest() {
+ addFourTextTrackElementsToVideo();
+ startLoadingVideo();
+ await waitUntilEnableTrackLoaded();
+ checkTracksStatus();
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+onload = runTest;
+
+/**
+ * The following are test helper functions.
+ */
+function addFourTextTrackElementsToVideo() {
+ let video = document.getElementById("v");
+ isnot(video.textTracks, undefined,
+ "HTMLMediaElement::TextTrack() property should be available.")
+
+ let trackList = video.textTracks;
+ is(trackList.length, 0, "Length should be 0.");
+
+ ok(typeof video.addTextTrack == "function",
+ "HTMLMediaElement::AddTextTrack() function should be available.")
+
+ // Insert some tracks in an order that is not sorted, we will test if they
+ // are sorted later.
+ info(`- Add a track element with label 'third' -`);
+ video.addTextTrack("subtitles", "third", "en-CA");
+ is(trackList.length, 1, "Length should be 1.");
+
+ let textTrack = video.textTracks[0];
+ checkAttributesDefaultValue(textTrack);
+ checkTextTrackMode(textTrack);
+ checkReadOnlyAttributes(textTrack);
+
+ info(`- Add a track element with label 'first' -`);
+ let trackOne = document.createElement("track");
+ video.appendChild(trackOne);
+ trackOne.label = "first";
+ trackOne.src = "basic.vtt";
+ trackOne.default = true;
+ trackOne.id = "2";
+ // The automatic track selection would choose the first track element with
+ // `default` attribute, so this track would be enable later.
+ enabledTrackElement = trackOne;
+
+ info(`- Add a track element with label 'fourth' -`);
+ video.addTextTrack("subtitles", "fourth", "en-CA");
+
+ info(`- Add a track element with label 'second' -`);
+ let trackTwo = document.createElement("track");
+ video.appendChild(trackTwo);
+ trackTwo.label = "second";
+ trackTwo.src = "basic.vtt";
+ // Although this track has `default` attribute as well, it won't be enable by
+ // the automatic track selection because it's not the first default track in
+ // the media element's track list.
+ trackTwo.default = true;
+}
+
+function checkAttributesDefaultValue(track) {
+ is(track.label, "third", "Label should be set to third.");
+ is(track.language, "en-CA", "Language should be en-CA.");
+ is(track.kind, "subtitles", "Default kind should be subtitles.");
+ is(track.mode, "hidden", "Default mode should be hidden.");
+}
+
+function checkTextTrackMode(track) {
+ // Mode should not allow a bogus value.
+ track.mode = 'bogus';
+ is(track.mode, 'hidden', "Mode should be not allow a bogus value.");
+
+ // Should allow all these values for mode.
+ changeTextTrackMode("showing");
+ changeTextTrackMode("disabled");
+ changeTextTrackMode("hidden");
+
+ function changeTextTrackMode(mode) {
+ track.mode = mode;
+ is(track.mode, mode, `Mode should allow \"${mode}\"`);
+ }
+}
+
+function checkReadOnlyAttributes(track) {
+ // All below are read-only properties and so should not allow setting.
+ track.label = "French subtitles";
+ is(track.label, "third", "Label is read-only so should still be \"label\".");
+ track.language = "en";
+ is(track.language, "en-CA", "Language is read-only so should still be \"en-CA\".");
+ track.kind = "captions";
+ is(track.kind, "subtitles", "Kind is read-only so should still be \"subtitles\"");
+}
+
+function startLoadingVideo() {
+ let video = document.getElementById("v");
+ video.src = "seek.webm";
+ video.preload = "metadata";
+}
+
+async function waitUntilEnableTrackLoaded() {
+ info(`wait until the enabled track finishes loading`);
+ await once(enabledTrackElement, "load");
+ is(enabledTrackElement.readyState, 2, "Track::ReadyState should be set to LOADED.");
+}
+
+function checkTracksStatus() {
+ // We're testing two things here,
+ // (1) the tracks created from a track element have a default mode 'disabled'
+ // and tracks created from 'addTextTrack' method have a default
+ // mode of 'hidden'.
+ // (2) we're testing that the tracks are sorted properly. For the tracks to
+ // be sorted the first two tracks, added through a TrackElement, must occupy
+ // the first two indexes in their TrackElement tree order. The second two
+ // tracks, added through the 'addTextTrack' method, will occupy the last two
+ // indexes in the order that they were added in.
+ let trackData = [
+ { label: "first", mode: "showing", id: "2" },
+ { label: "second", mode: "disabled", id: "" },
+ { label: "third", mode: "hidden", id: "" },
+ { label: "fourth", mode: "hidden", id: "" }
+ ];
+ let video = document.getElementById("v");
+ is(video.textTracks.length, trackData.length,
+ `TextTracks length should be ${trackData.length}`);
+ for (let i = 0; i < trackData.length; i++) {
+ let track = video.textTracks[i];
+ isnot(track, null, `Video should have a text track at index ${i}`);
+ let info = trackData[i];
+ for (let key in info) {
+ is(track[key], info[key],
+ `Track at index ${i} should have a '${key}' property with a value of '${info[key]}'.`);
+ }
+ }
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/mochitest/test_texttrack_cors_preload_none.html b/dom/media/webvtt/test/mochitest/test_texttrack_cors_preload_none.html
new file mode 100644
index 0000000000..6743e8c4cd
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/test_texttrack_cors_preload_none.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>load CORS Text track correctly when its parent media element's preload is none</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="manifest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<video preload="none" crossorigin="anonymous">
+ <track src="http://example.com/tests/dom/canvas/test/crossorigin/video.sjs?name=tests/dom/media/webvtt/test/mochitest/basic.vtt&type=text/vtt&cors=anonymous" kind="subtitles" id="default" default>
+</video>
+<script type="text/javascript">
+/**
+ * This test is used to test the text track element with CORS resource can starts
+ * loaded correctly when its parent media element's preload attribute is none.
+ */
+async function runTest() {
+ await waitUntiTrackLoaded();
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+runTest();
+
+/**
+ * The following are test helper functions.
+ */
+async function waitUntiTrackLoaded() {
+ let trackElement = document.getElementById("default");
+ if (trackElement.readyState != 2) {
+ info(`wait until the track finishes loading`);
+ await once(trackElement, "load");
+ }
+ is(trackElement.readyState, 2, "Track::ReadyState should be set to LOADED.");
+ is(trackElement.track.cues.length, 6, "Cue list length should be 6.");
+}
+</script>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/mochitest/test_texttrack_mode_change_during_loading.html b/dom/media/webvtt/test/mochitest/test_texttrack_mode_change_during_loading.html
new file mode 100644
index 0000000000..974f452092
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/test_texttrack_mode_change_during_loading.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebVTT : changing track's mode during loading</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="manifest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+/**
+ * This test is to ensure that we won't get `error` event when we change track's
+ * mode during loading. In this test, track element starts loading after setting
+ * the src and we would start another load later just after the channel which is
+ * used to fetch data starts. The second load is triggered by mode changes, and
+ * it should stop the prevous load and won't generate any error.
+ */
+async function startTest() {
+ const video = createVideo();
+ const trackElement = createAndAppendtrackElemententToVideo(video);
+
+ await changeTrackModeDuringLoading(trackElement);
+ await waitUntilTrackLoaded(trackElement);
+
+ removeNodeAndSource(video);
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({"set": [["media.webvtt.testing.events", true]]},
+ startTest);
+
+/**
+ * The following are test helper functions.
+ */
+function createVideo() {
+ info(`create video`);
+ let video = document.createElement("video");
+ video.src = "gizmo.mp4";
+ document.body.appendChild(video);
+ return video;
+}
+
+function createAndAppendtrackElemententToVideo(video) {
+ let trackElement = document.createElement("track");
+ trackElement.default = true;
+ video.append(trackElement);
+ return trackElement;
+}
+
+async function changeTrackModeDuringLoading(trackElement) {
+ info(`set src to start loading`);
+ trackElement.src = "basic.vtt";
+
+ info(`wait until starting loading resource.`);
+ await once(trackElement, "mozStartedLoadingTextTrack");
+
+ info(`changeing track's mode during loading should not cause loading failed.`);
+ trackElement.onerror = () => {
+ ok(false, `Should not get error event!`);
+ }
+ trackElement.track.mode = "hidden";
+}
+
+async function waitUntilTrackLoaded(trackElement) {
+ if (trackElement.readyState != 2) {
+ info(`wait until the track finishes loading`);
+ await once(trackElement, "load");
+ }
+ is(trackElement.readyState, 2, "Track::ReadyState should be set to LOADED.");
+ is(trackElement.track.cues.length, 6, "Cue list length should be 6.");
+}
+</script>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/mochitest/test_texttrack_moz.html b/dom/media/webvtt/test/mochitest/test_texttrack_moz.html
new file mode 100644
index 0000000000..b39562b3a4
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/test_texttrack_moz.html
@@ -0,0 +1,60 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Bug 881976 - TextTrackList</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="manifest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<video id="v" src="seek.webm" preload="metadata">
+<script type="text/javascript">
+/**
+ * This test is used to ensure the text track list we got from video is as same
+ * as the one in the text track.
+ */
+var video = document.getElementById("v");
+
+async function runTest() {
+ addTrackViaAddTrackAPI();
+ await addTrackViaTrackElement();
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+onload = runTest;
+
+/**
+ * The following are test helper functions.
+ */
+function addTrackViaAddTrackAPI() {
+ // Check if adding a text track manually sets the TextTrackList correctly.
+ video.addTextTrack("subtitles", "", "");
+ // TextTrack.textTrackList is an extension available only to privileged code,
+ // so we need to access it through the SpecialPowers object.
+ is(SpecialPowers.unwrap(SpecialPowers.wrap(video.textTracks[0]).textTrackList),
+ video.textTracks,
+ "The Track's TextTrackList should be the Video's TextTrackList.");
+}
+
+async function addTrackViaTrackElement() {
+ // Check if loading a Track via a TrackElement sets the TextTrackList correctly.
+ let trackElement = document.createElement("track");
+ trackElement.src = "basic.vtt";
+ trackElement.kind = "subtitles";
+ trackElement.default = true;
+ video.appendChild(trackElement);
+
+ info(`wait until the track finishes loading`);
+ await once(trackElement, "load");
+
+ is(trackElement.readyState, HTMLTrackElement.LOADED,
+ "Track::ReadyState should be set to LOADED.");
+ is(SpecialPowers.unwrap(SpecialPowers.wrap(trackElement.track).textTrackList),
+ video.textTracks,
+ "TrackElement's Track's TextTrackList should be the Video's TextTrackList.");
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/mochitest/test_texttrackcue.html b/dom/media/webvtt/test/mochitest/test_texttrackcue.html
new file mode 100644
index 0000000000..3d9783c07d
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/test_texttrackcue.html
@@ -0,0 +1,298 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Bug 833386 - HTMLTrackElement</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="manifest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<video id="v" src="seek.webm" preload="metadata">
+ <track src="basic.vtt" kind="subtitles" id="default" default>
+</video>
+<script type="text/javascript">
+/**
+ * This test is used to test the VTTCue's different behaviors and check whether
+ * cues would be activatived correctly during video playback.
+ */
+var video = document.getElementById("v");
+var trackElement = document.getElementById("default");
+
+async function runTest() {
+ await waitUntiTrackLoaded();
+ checkCueDefinition();
+ checkFirstCueParsedContent();
+ checkCueStartTimeAndEndtime();
+ checkCueSizeAndPosition();
+ checkCueSnapToLines();
+ checkCueAlignmentAndWritingDirection();
+ checkCueLine();
+ checkCreatingNewCue();
+ checkRemoveNonExistCue();
+ checkActiveCues();
+ checkCueRegion();
+ await checkCActiveCuesDuringVideoPlaying();
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({"set": [["media.webvtt.regions.enabled", true]]},
+ runTest);
+/**
+ * The following are test helper functions.
+ */
+async function waitUntiTrackLoaded() {
+ if (trackElement.readyState != 2) {
+ info(`wait until the track finishes loading`);
+ await once(trackElement, "load");
+ }
+ is(trackElement.readyState, 2, "Track::ReadyState should be set to LOADED.");
+ is(trackElement.track.cues.length, 6, "Cue list length should be 6.");
+}
+
+function checkCueDefinition() {
+ // Check that the typedef of TextTrackCue works in Gecko.
+ isnot(window.TextTrackCue, undefined, "TextTrackCue should be defined.");
+ isnot(window.VTTCue, undefined, "VTTCue should be defined.");
+}
+
+function checkFirstCueParsedContent() {
+ // Check if first cue was parsed correctly.
+ const cue = trackElement.track.cues[0];
+ ok(cue instanceof TextTrackCue, "Cue should be an instanceof TextTrackCue.");
+ ok(cue instanceof VTTCue, "Cue should be an instanceof VTTCue.");
+ is(cue.id, "1", "Cue's ID should be 1.");
+ is(cue.startTime, 0.5, "Cue's start time should be 0.5.");
+ is(cue.endTime, 0.7, "Cue's end time should be 0.7.");
+ is(cue.pauseOnExit, false, "Cue's pause on exit flag should be false.");
+ is(cue.text, "This", "Cue's text should be set correctly.");
+ is(cue.track, trackElement.track, "Cue's track should be defined.");
+ cue.track = null;
+ isnot(cue.track, null, "Cue's track should not be able to be set.");
+}
+
+function checkCueStartTimeAndEndtime() {
+ const cueList = trackElement.track.cues;
+ // Check that all cue times were not rounded
+ is(cueList[1].startTime, 1.2, "Second cue's start time should be 1.2.");
+ is(cueList[1].endTime, 2.4, "Second cue's end time should be 2.4.");
+ is(cueList[2].startTime, 2, "Third cue's start time should be 2.");
+ is(cueList[2].endTime, 3.5, "Third cue's end time should be 3.5.");
+ is(cueList[3].startTime, 2.71, "Fourth cue's start time should be 2.71.");
+ is(cueList[3].endTime, 2.91, "Fourth cue's end time should be 2.91.");
+ is(cueList[4].startTime, 3.217, "Fifth cue's start time should be 3.217.");
+ is(cueList[4].endTime, 3.989, "Fifth cue's end time should be 3.989.");
+ is(cueList[5].startTime, 3.217, "Sixth cue's start time should be 3.217.");
+ is(cueList[5].endTime, 3.989, "Sixth cue's end time should be 3.989.");
+
+ // Check that Cue setters are working correctly.
+ const cue = trackElement.track.cues[0];
+ cue.id = "Cue 01";
+ is(cue.id, "Cue 01", "Cue's ID should be 'Cue 01'.");
+ cue.startTime = 0.51;
+ is(cue.startTime, 0.51, "Cue's start time should be 0.51.");
+ cue.endTime = 0.71;
+ is(cue.endTime, 0.71, "Cue's end time should be 0.71.");
+ cue.pauseOnExit = true;
+ is(cue.pauseOnExit, true, "Cue's pause on exit flag should be true.");
+ video.addEventListener("pause", function() {
+ video.play();
+ }, {once: true});
+}
+
+function checkCueSizeAndPosition() {
+ function checkPercentageValue(prop, initialVal) {
+ ok(prop in cue, prop + " should be a property on VTTCue.");
+ cue[prop] = initialVal;
+ is(cue[prop], initialVal, `Cue's ${prop} should initially be ${initialVal}`);
+ [ 101, -1 ].forEach(function(val) {
+ let exceptionHappened = false;
+ try {
+ cue[prop] = val;
+ } catch(e) {
+ exceptionHappened = true;
+ is(e.name, "IndexSizeError", "Should have thrown IndexSizeError.");
+ }
+ ok(exceptionHappened, "Exception should have happened.");
+ });
+ }
+
+ const cue = trackElement.track.cues[0];
+ checkPercentageValue("size", 100.0);
+ cue.size = 50.5;
+ is(cue.size, 50.5, "Cue's size should be 50.5.")
+
+ // Check cue.position
+ checkPercentageValue("position", "auto");
+ cue.position = 50.5;
+ is(cue.position, 50.5, "Cue's position value should now be 50.5.");
+}
+
+function checkCueSnapToLines() {
+ const cue = trackElement.track.cues[0];
+ ok(cue.snapToLines, "Cue's snapToLines should be set by set.");
+ cue.snapToLines = false;
+ ok(!cue.snapToLines, "Cue's snapToLines should not be set.");
+}
+
+function checkCueAlignmentAndWritingDirection() {
+ function checkEnumValue(prop, initialVal, acceptedValues) {
+ ok(prop in cue, `${prop} should be a property on VTTCue.`);
+ is(cue[prop], initialVal, `Cue's ${prop} should be ${initialVal}`);
+ cue[prop] = "bogus";
+ is(cue[prop], initialVal, `Cue's ${prop} should be ${initialVal}`);
+ acceptedValues.forEach(function(val) {
+ cue[prop] = val;
+ is(cue[prop], val, `Cue's ${prop} should be ${val}`);
+ if (typeof val === "string") {
+ cue[prop] = val.toUpperCase();
+ is(cue[prop], val, `Cue's ${prop} should be ${val}`);
+ }
+ });
+ }
+
+ const cue = trackElement.track.cues[0];
+ checkEnumValue("align", "center", [ "start", "left", "center", "right", "end" ]);
+ checkEnumValue("lineAlign", "start", [ "start", "center", "end" ]);
+ checkEnumValue("vertical", "", [ "", "lr", "rl" ]);
+
+ cue.lineAlign = "center";
+ is(cue.lineAlign, "center", "Cue's line align should be center.");
+ cue.lineAlign = "START";
+ is(cue.lineAlign, "center", "Cue's line align should be center.");
+ cue.lineAlign = "end";
+ is(cue.lineAlign, "end", "Cue's line align should be end.");
+
+ // Check that cue position align works properly
+ is(cue.positionAlign, "auto", "Cue's default position alignment should be auto.");
+
+ cue.positionAlign = "line-left";
+ is(cue.positionAlign, "line-left", "Cue's position align should be line-left.");
+ cue.positionAlign = "auto";
+ is(cue.positionAlign, "auto", "Cue's position align should be auto.");
+ cue.positionAlign = "line-right";
+ is(cue.positionAlign, "line-right", "Cue's position align should be line-right.");
+}
+
+function checkCueLine() {
+ const cue = trackElement.track.cues[0];
+ // Check cue.line
+ is(cue.line, "auto", "Cue's line value should initially be auto.");
+ cue.line = 0.5;
+ is(cue.line, 0.5, "Cue's line value should now be 0.5.");
+ cue.line = "auto";
+ is(cue.line, "auto", "Cue's line value should now be auto.");
+}
+
+function checkCreatingNewCue() {
+ const cueList = trackElement.track.cues;
+
+ // Check that we can create and add new VTTCues
+ let vttCue = new VTTCue(3.999, 4, "foo");
+ is(vttCue.track, null, "Cue's track should be null.");
+ trackElement.track.addCue(vttCue);
+ is(vttCue.track, trackElement.track, "Cue's track should be defined.");
+ is(cueList.length, 7, "Cue list length should now be 7.");
+
+ // Check that new VTTCue was added correctly
+ let cue = cueList[6];
+ is(cue.startTime, 3.999, "Cue's start time should be 3.999.");
+ is(cue.endTime, 4, "Cue's end time should be 4.");
+ is(cue.text, "foo", "Cue's text should be foo.");
+
+ // Adding the same cue again should not increase the cue count.
+ trackElement.track.addCue(vttCue);
+ is(cueList.length, 7, "Cue list length should be 7.");
+
+ // Check that we are able to remove cues.
+ trackElement.track.removeCue(cue);
+ is(cueList.length, 6, "Cue list length should be 6.");
+}
+
+function checkRemoveNonExistCue() {
+ is(trackElement.track.cues.length, 6, "Cue list length should be 6.");
+ let exceptionHappened = false;
+ try {
+ // We should not be able to remove a cue that is not in the list.
+ trackElement.track.removeCue(new VTTCue(1, 2, "foo"));
+ } catch (e) {
+ // "NotFoundError" should be thrown when trying to remove a cue that is
+ // not in the list.
+ is(e.name, "NotFoundError", "Should have thrown NotFoundError.");
+ exceptionHappened = true;
+ }
+ // If this is false then we did not throw an error and probably removed a cue
+ // when we shouln't have.
+ ok(exceptionHappened, "Exception should have happened.");
+ is(trackElement.track.cues.length, 6, "Cue list length should still be 6.");
+}
+
+function checkActiveCues() {
+ video.currentTime = 2;
+ isnot(trackElement.track.activeCues, null);
+
+ trackElement.track.mode = "disabled";
+ is(trackElement.track.activeCues, null, "No active cue when track is disabled.");
+ trackElement.track.mode = "showing";
+}
+
+function checkCueRegion() {
+ let regionInfo = [
+ { lines: 2, width: 30 },
+ { lines: 4, width: 20 },
+ { lines: 2, width: 30 }
+ ];
+
+ for (let i = 0; i < regionInfo.length; i++) {
+ let cue = trackElement.track.cues[i];
+ isnot(cue.region, null, `Cue at ${i} should have a region.`);
+ for (let key in regionInfo[i]) {
+ is(cue.region[key], regionInfo[i][key],
+ `Region should have a ${key} property with a value of ${regionInfo[i][key]}`);
+ }
+ }
+}
+
+async function checkCActiveCuesDuringVideoPlaying() {
+ // Test TextTrack::ActiveCues.
+ let cueInfo = [
+ { startTime: 0.51, endTime: 0.71, ids: ["Cue 01"] },
+ { startTime: 0.72, endTime: 1.19, ids: [] },
+ { startTime: 1.2, endTime: 1.9, ids: [2] },
+ { startTime: 2, endTime: 2.4, ids: [2, 2.5] },
+ { startTime: 2.41, endTime: 2.70, ids: [2.5] },
+ { startTime: 2.71, endTime: 2.91, ids: [2.5, 3] },
+ { startTime: 2.92, endTime: 3.216, ids: [2.5] },
+ { startTime: 3.217, endTime: 3.5, ids: [2.5, 4, 5] },
+ { startTime: 3.51, endTime: 3.989, ids: [4, 5] },
+ { startTime: 3.99, endTime: 4, ids: [] }
+ ];
+
+ video.addEventListener("timeupdate", function() {
+ let activeCues = trackElement.track.activeCues,
+ playbackTime = video.currentTime;
+
+ for (let i = 0; i < cueInfo.length; i++) {
+ let cue = cueInfo[i];
+ if (playbackTime >= cue.startTime && playbackTime < cue.endTime) {
+ is(activeCues.length, cue.ids.length, `There should be ${cue.ids.length} currently active cue(s).`);
+ for (let j = 0; j < cue.ids.length; j++) {
+ isnot(activeCues.getCueById(cue.ids[j]), undefined,
+ `The cue with ID ${cue.ids[j]} should be active.`);
+ }
+ break;
+ }
+ }
+ });
+
+ info(`start video from 0s.`);
+ video.currentTime = 0;
+ video.play();
+ await once(video, "playing");
+ info(`video starts playing.`);
+ await once(video, "ended");
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/mochitest/test_texttrackcue_moz.html b/dom/media/webvtt/test/mochitest/test_texttrackcue_moz.html
new file mode 100644
index 0000000000..29f6661f85
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/test_texttrackcue_moz.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=967157
+-->
+<head>
+ <meta charset='utf-8'>
+ <title>Test for Bug 967157 - Setting TextTrackCue::DisplayState should set TextTrackCue::HasBeenReset to false</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+ SimpleTest.waitForExplicitFinish();
+
+ var cue = SpecialPowers.wrap(new VTTCue(0, 1, "Some text."));
+ is(cue.hasBeenReset, false, "Cue's hasBeenReset flag should be false.");
+ is(cue.displayState, null, "Cue's displayState should be null.");
+
+ cue.startTime = 0.5;
+ is(cue.hasBeenReset, true, "Cue's hasBeenReset flag should now be true.");
+
+ cue.displayState = document.createElement("div");
+ is(cue.hasBeenReset, false, "Cue's hasBeenReset flag should now be false.");
+
+ SimpleTest.finish();
+</script>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/mochitest/test_texttrackevents_video.html b/dom/media/webvtt/test/mochitest/test_texttrackevents_video.html
new file mode 100644
index 0000000000..83104633a4
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/test_texttrackevents_video.html
@@ -0,0 +1,91 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Tests for TextTrack DOM Events</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+var video = document.createElement("video");
+video.src = "vp9cake.webm";
+video.preload = "auto";
+video.controls = true;
+var trackElement = document.createElement("track");
+trackElement.src = "sequential.vtt";
+trackElement.kind = "subtitles";
+trackElement.default = true;
+document.getElementById("content").appendChild(video);
+video.appendChild(trackElement);
+
+var trackElementCueChangeCount = 0;
+var trackCueChangeCount = 0;
+var cueEnterCount = 0;
+var cueExitCount = 0;
+
+video.addEventListener("loadedmetadata", function run_tests() {
+ // Re-queue run_tests() at the end of the event loop until the track
+ // element has loaded its data.
+ if (trackElement.readyState == 1) {
+ setTimeout(run_tests, 0);
+ return;
+ }
+ is(trackElement.readyState, 2, "Track::ReadyState should be set to LOADED.");
+ ok('oncuechange' in trackElement.track, "Track::OnCueChange should exist.");
+
+ var textTrack = trackElement.track;
+ is(textTrack.cues.length, 3, "textTrack.cues.length should 3.");
+ textTrack.cues[0].onenter = function() {
+ ++cueEnterCount;
+ };
+ textTrack.cues[0].onexit = function() {
+ ++cueExitCount;
+ };
+ textTrack.cues[1].onenter = function() {
+ ++cueEnterCount;
+ };
+ textTrack.cues[1].onexit = function() {
+ ++cueExitCount;
+ };
+ textTrack.cues[2].onenter = function() {
+ ++cueEnterCount;
+ };
+ textTrack.cues[2].onexit = function() {
+ ++cueExitCount;
+ };
+
+ trackElement.track.oncuechange = function() {
+ ++trackElementCueChangeCount;
+ };
+
+ trackElement.addEventListener("cuechange", function() {
+ ++trackCueChangeCount;
+ });
+
+ video.play();
+});
+
+video.addEventListener('ended', function() {
+ // Should be fired 1 to 6 times, as there are 3 cues,
+ // with a change event for when it is activated/deactivated
+ // (6 events at most).
+ isnot(trackElementCueChangeCount, 0, "TrackElement should fire cue change at least one time.");
+ ok(trackElementCueChangeCount <= 6, 'trackElementCueChangeCount should <= 6');
+ isnot(trackCueChangeCount, 0, "TrackElement.track should fire cue change at least one time.");
+ ok(trackCueChangeCount <= 6, 'trackCueChangeCount should <= 6');
+ is(cueEnterCount, 3, "cueEnterCount should fire three times.");
+ is(cueExitCount, 3, "cueExitCount should fire three times.");
+ SimpleTest.finish()
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/mochitest/test_texttracklist.html b/dom/media/webvtt/test/mochitest/test_texttracklist.html
new file mode 100644
index 0000000000..c1d2296289
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/test_texttracklist.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=882703
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Media test: TextTrackList change event</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+let video = document.createElement("video");
+
+isnot(video.textTracks, null, "Video should have a list of TextTracks.");
+
+video.addTextTrack("subtitles", "", "");
+
+let track = video.textTracks[0];
+video.textTracks.addEventListener("change", changed);
+
+is(track.mode, "hidden", "New TextTrack's mode should be hidden.");
+track.mode = "showing";
+// Bug882674: change the mode again to see if we receive only one
+// change event.
+track.mode = "hidden";
+
+var eventCount = 0;
+function changed(event) {
+ eventCount++;
+ is(eventCount, 1, "change event dispatched multiple times.");
+ is(event.target, video.textTracks, "change event's target should be video.textTracks.");
+ ok(event instanceof window.Event, "change event should be a simple event.");
+ ok(!event.bubbles, "change event should not bubble.");
+ ok(event.isTrusted, "change event should be trusted.");
+ ok(!event.cancelable, "change event should not be cancelable.");
+
+ // Delay the finish function call for testing the change event count.
+ setTimeout(SimpleTest.finish, 0);
+}
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/mochitest/test_texttracklist_moz.html b/dom/media/webvtt/test/mochitest/test_texttracklist_moz.html
new file mode 100644
index 0000000000..6cae8323fd
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/test_texttracklist_moz.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=881976
+-->
+<head>
+ <meta charset='utf-8'>
+ <title>Test for Bug 881976 - TextTrackCue Computed Position</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+var video = document.createElement("video");
+var trackList = video.textTracks;
+ok(trackList instanceof TextTrackList,
+ "Video's textTracks should be a TextTrackList");
+var trackParent = SpecialPowers.unwrap(
+ SpecialPowers.wrap(trackList).mediaElement
+);
+is(trackParent, video,
+ "Video's TextTrackList's MediaElement reference should be set to the video.");
+SimpleTest.finish();
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/mochitest/test_texttrackregion.html b/dom/media/webvtt/test/mochitest/test_texttrackregion.html
new file mode 100644
index 0000000000..f41c404445
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/test_texttrackregion.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Bug 917945 - VTTRegion</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="manifest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<video id="v" src="seek.webm" preload="auto">
+ <track src="region.vtt" kind="subtitles" id="default" default>
+</video>
+<script type="text/javascript">
+/**
+ * This test is used to ensure that we can parse VTT region attributes correctly
+ * from vtt file.
+ */
+var trackElement = document.getElementById("default");
+
+async function runTest() {
+ await waitUntiTrackLoaded();
+ checkRegionAttributes();
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+SpecialPowers.pushPrefEnv({"set": [["media.webvtt.regions.enabled", true]]},
+ runTest);
+/**
+ * The following are test helper functions.
+ */
+async function waitUntiTrackLoaded() {
+ if (trackElement.readyState != 2) {
+ info(`wait until the track finishes loading`);
+ await once(trackElement, "load");
+ }
+ is(trackElement.readyState, 2, "Track::ReadyState should be set to LOADED.");
+}
+
+function checkRegionAttributes() {
+ let cues = trackElement.track.cues;
+ is(cues.length, 1, "Cue list length should be 1.");
+
+ let region = cues[0].region;
+ isnot(region, null, "Region should not be null.");
+ is(region.width, 62, "Region width should be 50.");
+ is(region.lines, 5, "Region lines should be 5.");
+ is(region.regionAnchorX, 4, "Region regionAnchorX should be 4.");
+ is(region.regionAnchorY, 78, "Region regionAnchorY should be 78.");
+ is(region.viewportAnchorX, 10, "Region viewportAnchorX should be 10.");
+ is(region.viewportAnchorY, 90, "Region viewportAnchorY should be 90.");
+ is(region.scroll, "up", "Region scroll should be 'up'");
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/mochitest/test_trackelementevent.html b/dom/media/webvtt/test/mochitest/test_trackelementevent.html
new file mode 100644
index 0000000000..f78033a89d
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/test_trackelementevent.html
@@ -0,0 +1,77 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Bug 882677 - Implement the 'sourcing out of band text tracks' algorithm</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="manifest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<video id="v" src="seek.webm" preload="auto">
+<script type="text/javascript">
+/**
+ * This test is used to ensure that we can load resource from vtt files correctly
+ * and will dispatch `error` event for invalid vtt files.
+ */
+var video = document.getElementById("v");
+
+async function runTest() {
+ let tracks = createTextTrackElements();
+ appendTracksToVideo(tracks);
+ await waitUntilsTrackLoadedOrGetError(tracks);
+ SimpleTest.finish()
+}
+
+SimpleTest.waitForExplicitFinish();
+onload = runTest;
+
+/**
+ * The following are test helper functions.
+ */
+function createTextTrackElements() {
+ // Only first track has valid vtt resource, other tracks should get the error
+ // event because of invalid vtt resources.
+ let trackOne = document.createElement("track");
+ trackOne.src = "basic.vtt";
+ trackOne.kind = "subtitles";
+ trackOne.expectedLoaded = true;
+
+ let trackTwo = document.createElement("track");
+ trackTwo.src = "bad-signature.vtt";
+ trackTwo.kind = "captions";
+ trackTwo.expectedLoaded = false;
+
+ let trackThree = document.createElement("track");
+ trackThree.src = "bad.vtt";
+ trackThree.kind = "chapters";
+ trackThree.expectedLoaded = false;
+
+ return [trackOne, trackTwo, trackThree];
+}
+
+function appendTracksToVideo(tracks) {
+ for (let track of tracks) {
+ video.appendChild(track);
+ }
+}
+
+async function waitUntilsTrackLoadedOrGetError(tracks) {
+ let promises = [];
+ for (let track of tracks) {
+ // explictly enable those track in order to start loading.
+ track.track.mode = "hidden";
+ if (track.expectedLoaded) {
+ info(`adding 'load' event to wait list.`);
+ promises.push(once(track, "load"));
+ } else {
+ info(`adding 'error' event to wait list.`);
+ promises.push(once(track, "error"));
+ }
+ }
+ info(`wait until tracks finish loading or get error.`);
+ await Promise.all(promises);
+ ok(true, "all tracks finish loading or get error.");
+}
+</script>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/mochitest/test_trackelementsrc.html b/dom/media/webvtt/test/mochitest/test_trackelementsrc.html
new file mode 100644
index 0000000000..f98e79c605
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/test_trackelementsrc.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 1281418 - Change the src attribue for TrackElement.</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+SpecialPowers.pushPrefEnv({"set": [["media.webvtt.regions.enabled", true]]},
+ function() {
+ var video = document.createElement("video");
+ video.src = "seek.webm";
+ video.preload = "metadata";
+ var trackElement = document.createElement("track");
+ trackElement.src = "basic.vtt";
+ trackElement.default = true;
+
+ document.getElementById("content").appendChild(video);
+ video.appendChild(trackElement);
+
+ video.addEventListener("loadedmetadata", function metadata() {
+ if (trackElement.readyState <= 1) {
+ setTimeout(metadata, 0);
+ return;
+ }
+ is(video.textTracks.length, 1, "Length should be 1.");
+ is(video.textTracks[0].cues.length, 6, "Cue length should be 6.");
+
+ trackElement.src = "sequential.vtt";
+ trackElement.track.mode = "showing";
+ video.play();
+ });
+
+ video.addEventListener("ended", function end() {
+ is(trackElement.readyState, 2, "readyState should be 2.")
+ is(video.textTracks.length, 1, "Length should be 1.");
+ is(video.textTracks[0].cues.length, 3, "Cue length should be 3.");
+ SimpleTest.finish();
+ });
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/mochitest/test_trackevent.html b/dom/media/webvtt/test/mochitest/test_trackevent.html
new file mode 100644
index 0000000000..ffba7d33ec
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/test_trackevent.html
@@ -0,0 +1,69 @@
+
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for Bug 893309 - Implement TrackEvent</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content" style="display: none">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+var video = document.createElement("video");
+isnot(video.textTracks, undefined, "HTMLMediaElement::TextTrack() property should be available.")
+ok(typeof video.addTextTrack == "function", "HTMLMediaElement::AddTextTrack() function should be available.")
+
+var trackList = video.textTracks;
+is(trackList.length, 0, "Length should be 0.");
+
+var evtTextTrack, numOfCalls = 0, tt;
+trackList.onaddtrack = function(event) {
+ ok(event instanceof TrackEvent, "Fired event from onaddtrack should be a TrackEvent");
+ is(event.type, "addtrack", "Event type should be addtrack");
+ ok(event.isTrusted, "Event should be trusted!");
+ ok(!event.bubbles, "Event shouldn't bubble!");
+ ok(!event.cancelable, "Event shouldn't be cancelable!");
+
+ evtTextTrack = event.track;
+ tt = textTrack[numOfCalls].track || textTrack[numOfCalls];
+
+ ok(tt === evtTextTrack, "Text tracks should be the same");
+ is(evtTextTrack.label, label[numOfCalls], "Label should be set to "+ label[numOfCalls]);
+ is(evtTextTrack.language, language[numOfCalls], "Language should be " + language[numOfCalls]);
+ is(evtTextTrack.kind, kind[numOfCalls], "Kind should be " + kind[numOfCalls]);
+
+ if (++numOfCalls == 4) {
+ SimpleTest.finish();
+ }
+};
+
+var label = ["Oasis", "Coldplay", "t.A.T.u", ""];
+var language = ["en-CA", "en-GB", "ru", ""];
+var kind = ["subtitles", "captions", "chapters", "subtitles"];
+
+var textTrack = new Array(4);
+for (var i = 0; i < 3; ++i) {
+ textTrack[i] = video.addTextTrack(kind[i], label[i], language[i]);
+ is(trackList.length, i + 1, "Length should be " + (i+1));
+}
+
+video.src = "seek.webm";
+video.preload = "auto";
+var trackElement = document.createElement("track");
+trackElement.src = "basic.vtt";
+textTrack[3] = trackElement;
+
+document.getElementById("content").appendChild(video);
+video.appendChild(trackElement);
+
+//TODO: Tests for removetrack event to be added along with bug 882677
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/mochitest/test_vttparser.html b/dom/media/webvtt/test/mochitest/test_vttparser.html
new file mode 100644
index 0000000000..419723a1d4
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/test_vttparser.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset='utf-8'>
+ <title>WebVTT Parser Regression Tests</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<p id="display"></p>
+<div id="content">
+</div>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+var video = document.createElement("video");
+video.src = "seek.webm";
+video.preload = "auto";
+
+var trackElement = document.createElement("track");
+trackElement.src = "parser.vtt";
+trackElement.kind = "subtitles";
+trackElement.default = true;
+
+document.getElementById("content").appendChild(video);
+video.appendChild(trackElement);
+video.addEventListener("loadedmetadata", function run_tests() {
+ // Re-que run_tests() at the end of the event loop until the track
+ // element has loaded its data.
+ if (trackElement.readyState == 1) {
+ setTimeout(run_tests, 0);
+ return;
+ }
+
+ is(trackElement.readyState, 2, "Track::ReadyState should be set to LOADED.");
+ is(trackElement.track.cues.length, 2, "Track should have two Cues.");
+ SimpleTest.finish();
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/mochitest/test_webvtt_empty_displaystate.html b/dom/media/webvtt/test/mochitest/test_webvtt_empty_displaystate.html
new file mode 100644
index 0000000000..e1a8ddc555
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/test_webvtt_empty_displaystate.html
@@ -0,0 +1,98 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset='utf-8'>
+ <title>WebVTT : cue's displaystate should be empty when its active flag is unset</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<div id="content">
+</div>
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+var isReceivedOnEnterEvent = false;
+var isReceivedOnExitEvent = false;
+
+function checkCueEvents() {
+ ok(isReceivedOnEnterEvent, "Already received cue's onEnter event.");
+ ok(isReceivedOnExitEvent, "Already received cue's onExit event.");
+ SimpleTest.finish();
+}
+
+function checkCueDisplayState(cue, expectedState) {
+ var cueChrome = SpecialPowers.wrap(cue);
+ if (expectedState) {
+ ok(cueChrome.displayState, "Cue's displayState shouldn't be empty.");
+ } else {
+ ok(!cueChrome.displayState, "Cue's displayState should be empty.");
+ }
+}
+
+function runTest() {
+ info("--- create video ---");
+ var video = document.createElement("video");
+ video.src = "seek.webm";
+ video.autoplay = true;
+ document.getElementById("content").appendChild(video);
+
+ video.onended = function () {
+ video.onended = null;
+ checkCueEvents();
+ };
+
+ video.onpause = function () {
+ video.onpause = null;
+ checkCueEvents();
+ }
+
+ video.onloadedmetadata = function () {
+ ok(video.duration > 2, "video.duration should larger than 2");
+ }
+
+ info("--- create the type of track ---");
+ isnot(window.TextTrack, undefined, "TextTrack should be defined.");
+
+ var track = video.addTextTrack("subtitles", "A", "en");
+ track.mode = "showing";
+ ok(track instanceof TextTrack, "Track should be an instanceof TextTrack.");
+
+ info("--- check the type of cue ---");
+ isnot(window.TextTrackCue, undefined, "TextTrackCue should be defined.");
+ isnot(window.VTTCue, undefined, "VTTCue should be defined.");
+
+ var cue = new VTTCue(1, 2, "Test cue");
+ ok(cue instanceof TextTrackCue, "Cue should be an instanceof TextTrackCue.");
+ ok(cue instanceof VTTCue, "Cue should be an instanceof VTTCue.");
+
+ info("--- add cue ---");
+ track.addCue(cue);
+ video.ontimeupdate = function () {
+ info("--- video.currentTime is " + video.currentTime);
+ };
+ cue.onenter = function () {
+ cue.onenter = null;
+ isReceivedOnEnterEvent = true;
+ var cueChrome = SpecialPowers.wrap(cue);
+ info("cueChrome.getActive " + cueChrome.getActive);
+ if (cueChrome.getActive) {
+ checkCueDisplayState(cue, true /* has display-state */);
+ } else {
+ info("This is a missing cue, video.currentTime is "+ video.currentTime);
+ }
+
+ cue.onexit = function () {
+ cue.onexit = null;
+ isReceivedOnExitEvent = true;
+ checkCueDisplayState(cue, false /* no display-state */);
+ video.pause();
+ }
+ }
+}
+
+onload = runTest;
+</script>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/mochitest/test_webvtt_event_same_time.html b/dom/media/webvtt/test/mochitest/test_webvtt_event_same_time.html
new file mode 100644
index 0000000000..e70ff558e1
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/test_webvtt_event_same_time.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset='utf-8'>
+ <title>WebVTT : cue's onenter/onexit event order </title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<div id="content">
+</div>
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+var c1exit = false;
+var c3enter = false;
+
+function runTest() {
+ info("--- create video ---");
+ var video = document.createElement("video");
+ video.src = "seek.webm";
+ video.autoplay = true;
+ document.getElementById("content").appendChild(video);
+
+ var track = video.addTextTrack("subtitles", "A", "en");
+ track.mode = "showing";
+
+ var cue1 = new VTTCue(1, 2, "Test cue1");
+ var cue2 = new VTTCue(2, 3, "Test cue2");
+ track.addCue(cue1);
+ track.addCue(cue2);
+
+ cue1.onexit = function () {
+ cue1.onexit = null;
+ c1exit = true;
+ }
+ cue2.onenter = function () {
+ cue2.onenter = null;
+ ok(c1exit, "cue1 onexit event before than cue2 onenter");
+ video.pause();
+ SimpleTest.finish();
+ }
+
+ var cue3 = new VTTCue(1, 2, "Test cue3");
+ var cue4 = new VTTCue(1, 2, "Test cue4");
+ track.addCue(cue3);
+ track.addCue(cue4);
+
+ cue3.onenter = function () {
+ cue3.onenter = null;
+ c3enter = true;
+ }
+ cue4.onenter = function () {
+ cue4.onenter = null;
+ ok(c3enter, "cue3 onenter event before than cue4 onenter");
+ }
+}
+
+onload = runTest;
+</script>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/mochitest/test_webvtt_infinite_processing_loop.html b/dom/media/webvtt/test/mochitest/test_webvtt_infinite_processing_loop.html
new file mode 100644
index 0000000000..c8a9380ca2
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/test_webvtt_infinite_processing_loop.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Bug 1580015 - video hangs infinitely during playing subtitle</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="manifest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <style>
+ .container {
+ width: 500px;
+ height: 300px;
+ background: pink;
+ display: flex;
+ justify-content: center;
+ }
+
+ video {
+ min-width: 95%;
+ max-width: 95%;
+ max-height: 95%;
+ }
+ </style>
+</head>
+<body>
+<div class="container">
+<video id="v" src="gizmo.mp4" controls>
+ <track src="basic.vtt" kind="subtitles" default>
+</video>
+</div>
+<script type="text/javascript">
+/**
+ * This test is used to ensure that we don't go into an infinite processing loop
+ * during playing subtitle when setting those CSS properties on video.
+ */
+SimpleTest.waitForExplicitFinish();
+
+let video = document.getElementById("v");
+// We don't need to play whole video, in order to reduce test time, we can start
+// from the half, which can also reproduce the issue.
+video.currentTime = 3.0;
+video.play();
+video.onended = () => {
+ ok(true, "video ends without entering an infinite processing loop");
+ SimpleTest.finish();
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/mochitest/test_webvtt_overlapping_time.html b/dom/media/webvtt/test/mochitest/test_webvtt_overlapping_time.html
new file mode 100644
index 0000000000..5ce08ae77a
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/test_webvtt_overlapping_time.html
@@ -0,0 +1,100 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebVTT : cues with overlapping time should be displayed correctly </title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="manifest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<video id ="v" src="gizmo.mp4" controls>
+<script class="testbody" type="text/javascript">
+/**
+ * This test is used to ensure that when cues with overlapping times, the one
+ * with earlier end timestamp should disappear when the media time reaches its
+ * end time. In this test, we have two cues with overlapping time, when the video
+ * starts, both cues should be displayed. When the time passes 1 seconds, the
+ * first cue should disappear and the second cues should be still displayed.
+ */
+var CUES_INFO = [
+ { id: 0, startTime: 0, endTime: 1, text: "This is cue 0."},
+ { id: 1, startTime: 0, endTime: 6, text: "This is cue 1."},
+];
+
+var video = document.getElementById("v");
+
+async function startTest() {
+ const cues = createCues();
+ await startVideo();
+
+ await waitUntilCueIsShowing(cues[0]);
+ await waitUntilCueIsShowing(cues[1]);
+
+ await waitUntilCueIsHiding(cues[0]);
+ await waitUntilCueIsShowing(cues[1]);
+ IsVideoStillPlaying();
+
+ endTestAndClearVideo();
+}
+
+SimpleTest.waitForExplicitFinish();
+onload = startTest;
+
+/**
+ * The following are test helper functions.
+ */
+function createCues() {
+ let track = video.addTextTrack("subtitles");
+ track.mode = "showing";
+ let cue0 = new VTTCue(CUES_INFO[0].startTime, CUES_INFO[0].endTime,
+ CUES_INFO[0].text);
+ cue0.id = CUES_INFO[0].id;
+ let cue1 = new VTTCue(CUES_INFO[1].startTime, CUES_INFO[1].endTime,
+ CUES_INFO[1].text);
+ cue1.id = CUES_INFO[1].id;
+ track.addCue(cue0);
+ track.addCue(cue1);
+ // Convert them to chrome objects in order to use chrome privilege APIs.
+ cue0 = SpecialPowers.wrap(cue0);
+ cue1 = SpecialPowers.wrap(cue1);
+ return [cue0, cue1];
+}
+
+async function startVideo() {
+ info(`start play video`);
+ const played = video && await video.play().then(() => true, () => false);
+ ok(played, "video has started playing");
+}
+
+async function waitUntilCueIsShowing(cue) {
+ info(`wait until cue ${cue.id} is showing`);
+ // cue has not been showing yet.
+ if (!cue.getActive) {
+ await once(cue, "enter");
+ }
+ info(`video current time=${video.currentTime}`);
+ ok(cue.getActive, `cue ${cue.id} is showing`);
+}
+
+async function waitUntilCueIsHiding(cue) {
+ info(`wait until cue ${cue.id} is hiding`);
+ // cue has not been hidden yet.
+ if (cue.getActive) {
+ await once(cue, "exit");
+ }
+ info(`video current time=${video.currentTime}`);
+ ok(!cue.getActive, `cue ${cue.id} is hidding`);
+}
+
+function IsVideoStillPlaying() {
+ ok(!video.paused, `video is still playing, currentTime=${video.currentTime}`);
+}
+
+function endTestAndClearVideo() {
+ removeNodeAndSource(video);
+ SimpleTest.finish();
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/mochitest/test_webvtt_positionalign.html b/dom/media/webvtt/test/mochitest/test_webvtt_positionalign.html
new file mode 100644
index 0000000000..267fd52f93
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/test_webvtt_positionalign.html
@@ -0,0 +1,113 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta charset='utf-8'>
+ <title>WebVTT : position align test</title>
+ <script type="text/javascript" src="/MochiKit/MochiKit.js"></script>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<div id="content">
+</div>
+<script class="testbody" type="text/javascript">
+SimpleTest.waitForExplicitFinish();
+
+var video = document.createElement("video");
+var trackElement = document.createElement("track");
+var cuesNumber = 22;
+
+function isTrackElemenLoaded() {
+ // Re-que isTrackElemenLoaded() at the end of the event loop until the track
+ // element has loaded its data.
+ if (trackElement.readyState == 1) {
+ setTimeout(isTrackElemenLoaded, 0);
+ return;
+ }
+
+ is(trackElement.readyState, 2, "Track::ReadyState should be set to LOADED.");
+ runTest();
+}
+
+function runTest() {
+ info("--- check cues number ---");
+ var cues = trackElement.track.cues;
+ is(cues.length, cuesNumber, "Cues number is correct.");
+
+ info("--- check the typedef of TextTrackCue and VTTCue ---");
+ isnot(window.TextTrackCue, undefined, "TextTrackCue should be defined.");
+ isnot(window.VTTCue, undefined, "VTTCue should be defined.");
+
+ info("--- check the type of first parsed cue ---");
+ ok(cues[0] instanceof TextTrackCue, "Cue should be an instanceof TextTrackCue.");
+ ok(cues[0] instanceof VTTCue, "Cue should be an instanceof VTTCue.");
+
+ info("--- check the cue's position alignment ---");
+ let expectedAlignment = ["auto", "line-left", "center", "line-right", "auto"];
+ let idx = 0;
+ for (;idx < expectedAlignment.length; idx++) {
+ is(cues[idx].positionAlign, expectedAlignment[idx], cues[idx].text);
+ }
+
+ info("--- check the cue's computed position alignment ---");
+ // The "computedPositionAlign" is the chrome-only attributes, we need to get
+ // the chrome privilege for cues.
+ let cuesChrome = SpecialPowers.wrap(cues);
+ expectedAlignment.push("line-left", "line-right", "center");
+ for (;idx < expectedAlignment.length; idx++) {
+ is(cuesChrome[idx].computedPositionAlign, expectedAlignment[idx],
+ cuesChrome[idx].text);
+ }
+
+ info(`test only setting text alignment with "start"`);
+ expectedAlignment.push("line-left", "line-right", "line-left", "line-right",
+ "line-left", "line-left", "line-right");
+ for (;idx < expectedAlignment.length; idx++) {
+ is(cuesChrome[idx].computedPositionAlign, expectedAlignment[idx],
+ cuesChrome[idx].text);
+ }
+
+ info(`test only setting text alignment with "end"`);
+ expectedAlignment.push("line-right", "line-left", "line-right", "line-left",
+ "line-right", "line-right", "line-left");
+ for (;idx < expectedAlignment.length; idx++) {
+ is(cuesChrome[idx].computedPositionAlign, expectedAlignment[idx],
+ cuesChrome[idx].text);
+ }
+ is(idx, cuesNumber, "finished checking all cues");
+
+ info("--- check the cue's computed position alignment from DOM API ---");
+ is(cuesChrome[0].computedPositionAlign, "center", "Cue's computedPositionAlign align is center.");
+
+ cuesChrome[0].positionAlign = "auto";
+ is(cuesChrome[0].positionAlign, "auto", "Change cue's position align to \"auto\"");
+
+ cuesChrome[0].align = "left";
+ is(cuesChrome[0].align, "left", "Change cue's align to \"left\".");
+
+ is(cuesChrome[0].computedPositionAlign, "line-left", "Cue's computedPositionAlign becomes to \"line-left\"");
+
+ info("--- finish test ---");
+ SimpleTest.finish();
+}
+
+function setupTest() {
+ info("--- setup test ---");
+ video.src = "seek.webm";
+ video.preload = "auto";
+
+ trackElement.src = "vttPositionAlign.vtt";
+ trackElement.kind = "subtitles";
+ trackElement.default = true;
+
+ document.getElementById("content").appendChild(video);
+ video.appendChild(trackElement);
+ video.addEventListener("loadedmetadata", function() {
+ isTrackElemenLoaded();
+ }, {once: true});
+}
+
+onload = setupTest;
+</script>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/mochitest/test_webvtt_seeking.html b/dom/media/webvtt/test/mochitest/test_webvtt_seeking.html
new file mode 100644
index 0000000000..1f85969743
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/test_webvtt_seeking.html
@@ -0,0 +1,110 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebVTT : cue should be displayed properly after seeking</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="manifest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+/**
+ * This test is used to ensure that the cue should be showed or hid correctly
+ * after seeking. In this test, we have two cues which are not overlapped, so
+ * there should only have one cue showing at a time.
+ */
+var CUES_INFO = [
+ { id: 0, startTime: 1, endTime: 3, text: "This is cue 0."},
+ { id: 1, startTime: 4, endTime: 6, text: "This is cue 1."},
+];
+
+async function startTest() {
+ const video = createVideo();
+ const cues = createCues(video);
+ await startVideo(video);
+
+ await seekVideo(video, cues[0].startTime);
+ await waitUntilCueIsShowing(cues[0]);
+ checkActiveCueAndInactiveCue(cues[0], cues[1]);
+
+ await seekVideo(video, cues[1].startTime);
+ await waitUntilCueIsShowing(cues[1]);
+ checkActiveCueAndInactiveCue(cues[1], cues[0]);
+
+ // seek forward again
+ await seekVideo(video, cues[0].startTime);
+ await waitUntilCueIsShowing(cues[0]);
+ checkActiveCueAndInactiveCue(cues[0], cues[1]);
+
+ removeNodeAndSource(video);
+ SimpleTest.finish();
+}
+
+SimpleTest.waitForExplicitFinish();
+onload = startTest;
+/**
+ * The following are test helper functions.
+ */
+function checkActiveCueAndInactiveCue(activeCue, inactiveCue) {
+ ok(activeCue.getActive,
+ `cue ${activeCue.id} [${activeCue.startTime}:${activeCue.endTime}] is active`);
+ ok(!inactiveCue.getActive,
+ `cue ${inactiveCue.id} [${inactiveCue.startTime}:${inactiveCue.endTime}] is inactive`);
+}
+
+function createVideo() {
+ let video = document.createElement("video");
+ video.src = "gizmo.mp4";
+ video.controls = true;
+ document.body.appendChild(video);
+ return video;
+}
+
+function createCues(video) {
+ let track = video.addTextTrack("subtitles");
+ track.mode = "showing";
+ let cue0 = new VTTCue(CUES_INFO[0].startTime, CUES_INFO[0].endTime,
+ CUES_INFO[0].text);
+ cue0.id = CUES_INFO[0].id;
+ let cue1 = new VTTCue(CUES_INFO[1].startTime, CUES_INFO[1].endTime,
+ CUES_INFO[1].text);
+ cue1.id = CUES_INFO[1].id;
+ track.addCue(cue0);
+ track.addCue(cue1);
+ // Convert them to chrome objects in order to use chrome privilege APIs.
+ cue0 = SpecialPowers.wrap(cue0);
+ cue1 = SpecialPowers.wrap(cue1);
+ return [cue0, cue1];
+}
+
+async function startVideo(video) {
+ info(`start play video`);
+ const played = video && await video.play().then(() => true, () => false);
+ ok(played, "video has started playing");
+}
+
+async function waitUntilCueIsShowing(cue) {
+ info(`wait until cue ${cue.id} shows`);
+ // cue has not been showing yet.
+ if (!cue.getActive) {
+ await once(cue, "enter");
+ }
+ info(`cue ${cue.id} is showing`);
+}
+
+async function seekVideo(video, time) {
+ ok(isInRange(time, CUES_INFO[0].startTime, CUES_INFO[0].endTime) ||
+ isInRange(time, CUES_INFO[1].startTime, CUES_INFO[1].endTime),
+ `seek target time ${time} is within the correct range`)
+ info(`seek video to ${time}`);
+ video.currentTime = time;
+ await once(video, "seeked");
+ info(`seek succeeded, current time=${video.currentTime}`);
+}
+
+function isInRange(value, lowerBound, higherBound) {
+ return lowerBound <= value && value <= higherBound;
+}
+</script>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/mochitest/test_webvtt_update_display_after_adding_or_removing_cue.html b/dom/media/webvtt/test/mochitest/test_webvtt_update_display_after_adding_or_removing_cue.html
new file mode 100644
index 0000000000..7fb8e6761b
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/test_webvtt_update_display_after_adding_or_removing_cue.html
@@ -0,0 +1,93 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>WebVTT : cue display should be updated immediately after adding or removing cue</title>
+ <script src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script src="manifest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<script class="testbody" type="text/javascript">
+/**
+ * This test is used to ensure that we will update cue display immediately after
+ * adding or removing cue after video starts, because `show-poster` flag would be
+ * reset after video starts, which allows us to process cues instead of showing
+ * a poster. In this test, we start with adding a cue [0:5] to video, which
+ * should be showed in the beginning, and then remove the cue later. The cue
+ * should be removed immediately, not show
+ */
+async function startTest() {
+ const video = await createVideo();
+ await startVideo(video);
+
+ info(`cue should be showed immediately after it was added.`);
+ const cue = createCueAndAddCueToVideo(video);
+ await waitUntilCueShows(cue);
+
+ info(`cue should be hid immediately after it was removed.`);
+ removeCueFromVideo(cue, video);
+ checkIfCueHides(cue, video);
+
+ endTestAndClearVideo(video);
+}
+
+SimpleTest.waitForExplicitFinish();
+onload = startTest;
+
+/**
+ * The following are test helper functions.
+ */
+async function createVideo() {
+ let video = document.createElement("video");
+ video.src = "gizmo.mp4";
+ video.controls = true;
+ document.body.appendChild(video);
+ // wait until media has loaded any data, because we won't update cue if it has
+ // not got any data.
+ await once(video, "loadedmetadata");
+ return video;
+}
+
+async function startVideo(video) {
+ info(`start play video`);
+ const played = video && await video.play().then(() => true, () => false);
+ ok(played, "video has started playing");
+}
+
+function createCueAndAddCueToVideo(video) {
+ let track = video.addTextTrack("subtitles");
+ track.mode = "showing";
+ let cue = new VTTCue(0, 5, "Test");
+ track.addCue(cue);
+ return cue;
+}
+
+function removeCueFromVideo(cue, video) {
+ let track = video.textTracks[0];
+ track.removeCue(cue);
+}
+
+async function waitUntilCueShows(cue) {
+ info(`wait until cue shows`);
+ // cue has not been showed yet.
+ cue = SpecialPowers.wrap(cue);
+ if (!cue.getActive) {
+ await once(cue, "enter");
+ }
+ ok(cue.getActive, `cue has been showed,`);
+}
+
+function checkIfCueHides(cue, video) {
+ ok(!SpecialPowers.wrap(cue).getActive, `cue has been hidden.`);
+ ok(video.currentTime < cue.endTime,
+ `cue is removed at ${video.currentTime}s before reaching its endtime.`);
+}
+
+function endTestAndClearVideo(video) {
+ removeNodeAndSource(video);
+ SimpleTest.finish();
+}
+
+</script>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/mochitest/vttPositionAlign.vtt b/dom/media/webvtt/test/mochitest/vttPositionAlign.vtt
new file mode 100644
index 0000000000..7613f4e7c5
--- /dev/null
+++ b/dom/media/webvtt/test/mochitest/vttPositionAlign.vtt
@@ -0,0 +1,86 @@
+WEBVTT
+
+00:00.000 --> 00:00.500
+Cue 0 : PositionAlign should be "auto".
+
+00:00.700 --> 00:00.800 position:50%,line-left
+Cue 1 : PositionAlign should be "line-left".
+
+00:00.700 --> 00:00.800 position:50%,center
+Cue 2 : PositionAlign should be "center".
+
+00:00.700 --> 00:00.800 position:50%,line-right
+Cue 3 : PositionAlign should be "line-right".
+
+00:00.700 --> 00:00.800 position:50%,auto
+Cue 4 : PositionAlign should be "auto"
+
+00:00.700 --> 00:00.800 position:50%,auto align:left
+Cue 5 : PositionAlign should be "auto", but computedPositionAlign should be "line-left".
+
+00:00.700 --> 00:00.800 position:50%,auto align:right
+Cue 6 : PositionAlign should be "auto", but computedPositionAlign should be "line-right".
+
+00:00.700 --> 00:00.800 position:50%,auto align:middle
+Cue 7 : PositionAlign should be "auto", but computedPositionAlign should be "center".
+
+NOTE ### These following cues are set with `align:start` ###
+
+00:00.700 --> 00:00.800 align:start
+LTR character in the beginning and align is "start", so computedPositionAlign should be "line-left".
+
+00:00.700 --> 00:00.800 align:start
+שלום RTL character in the beginning and align is "start", so computedPositionAlign should be "line-right".
+
+00:00.700 --> 00:00.800 align:start
+@ neutral charater in the beginning, but the first strong charater is LTR in "align:start". So computedPositionAlign should be "line-left".
+
+00:00.700 --> 00:00.800 align:start
+@ש neutral charater in the beginning, but the first strong charater is RTL in "align:start". So computedPositionAlign should be "line-right".
+
+NOTE
+This line contains only neutral charater, we would treat its base direction as
+LTR. However, if there are other following lines contains non-neutral
+charaters, we would detemine the base direction by the following line.
+
+00:00.700 --> 00:00.800 align:start
+@
+
+00:00.700 --> 00:00.800 align:start
+@
+The second line starts with LTR charater, computedPositionAlign should be "line-left".
+
+00:00.700 --> 00:00.800 align:start
+@
+שThe second line starts with RTL charater, computedPositionAlign should be "line-right".
+
+NOTE ### These following cues are set with `align:end` ###
+
+00:00.700 --> 00:00.800 align:end
+LTR character in the beginning and align is "end", so computedPositionAlign should be "line-right".
+
+00:00.700 --> 00:00.800 align:end
+ש RTL character in the beginning and align is "end", so computedPositionAlign should be "line-left".
+
+00:00.700 --> 00:00.800 align:end
+@ neutral charater in the beginning, but the first strong charater is LTR in "align:end". So computedPositionAlign should be "line-right".
+
+00:00.700 --> 00:00.800 align:end
+@ש neutral charater in the beginning, but the first strong charater is RTL in "align:end". So computedPositionAlign should be "line-left".
+
+NOTE
+This line contains only neutral charater, we would treat its base direction as
+LTR. However, if there are other following lines contains non-neutral
+charaters, we would detemine the base direction by the following line.
+
+00:00.700 --> 00:00.800 align:end
+@
+
+00:00.700 --> 00:00.800 align:end
+@
+The second line starts with LTR charater, computedPositionAlign should be "line-right".
+
+00:00.700 --> 00:00.800 align:end
+@
+שThe second line starts with RTL charater, computedPositionAlign should be "line-left".
+
diff --git a/dom/media/webvtt/test/reftest/black.mp4 b/dom/media/webvtt/test/reftest/black.mp4
new file mode 100644
index 0000000000..24eb3be139
--- /dev/null
+++ b/dom/media/webvtt/test/reftest/black.mp4
Binary files differ
diff --git a/dom/media/webvtt/test/reftest/cues_time_overlapping.webvtt b/dom/media/webvtt/test/reftest/cues_time_overlapping.webvtt
new file mode 100644
index 0000000000..8ed899a9d8
--- /dev/null
+++ b/dom/media/webvtt/test/reftest/cues_time_overlapping.webvtt
@@ -0,0 +1,7 @@
+WEBVTT FILE
+
+00:00:00.000 --> 00:00:01.000
+First cue
+
+00:00:00.000 --> 00:00:04.000
+Second cue
diff --git a/dom/media/webvtt/test/reftest/reftest.list b/dom/media/webvtt/test/reftest/reftest.list
new file mode 100644
index 0000000000..8311bac10f
--- /dev/null
+++ b/dom/media/webvtt/test/reftest/reftest.list
@@ -0,0 +1,3 @@
+skip-if(Android) fuzzy-if(/^Windows\x20NT\x2010\.0/.test(http.oscpu)&&/^aarch64-msvc/.test(xulRuntime.XPCOMABI),0-136,0-427680) == vtt_update_display_after_removed_cue.html vtt_update_display_after_removed_cue_ref.html
+skip-if(Android) fuzzy-if(winWidget,0-170,0-170) == vtt_overlapping_time.html vtt_overlapping_time-ref.html
+skip-if(Android) != vtt_reflow_display.html vtt_reflow_display-ref.html
diff --git a/dom/media/webvtt/test/reftest/vtt_overlapping_time-ref.html b/dom/media/webvtt/test/reftest/vtt_overlapping_time-ref.html
new file mode 100644
index 0000000000..a44e24678b
--- /dev/null
+++ b/dom/media/webvtt/test/reftest/vtt_overlapping_time-ref.html
@@ -0,0 +1,29 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+</head>
+<body>
+<video id="v1" src="black.mp4" width="320" height="180">
+ <track label="English" src="cues_time_overlapping.webvtt" default>
+</video>
+<script type="text/javascript">
+/**
+ * This test is to ensure that when cues with overlapping times, the one with
+ * earlier end timestamp should disappear when the media time reaches its
+ * end time. This vtt file contains two cues, the first cue is [0,1], the second
+ * cue is [0,4], so if we seek video to 2s, only cue2 should be displayed.
+ */
+async function testTimeOverlappingCues() {
+ const video = document.getElementById("v1");
+ video.currentTime = 2;
+ video.onseeked = () => {
+ video.onseeked = null;
+ document.documentElement.removeAttribute('class');
+ }
+};
+
+window.addEventListener("MozReftestInvalidate",
+ testTimeOverlappingCues);
+</script>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/reftest/vtt_overlapping_time.html b/dom/media/webvtt/test/reftest/vtt_overlapping_time.html
new file mode 100644
index 0000000000..f973b398d5
--- /dev/null
+++ b/dom/media/webvtt/test/reftest/vtt_overlapping_time.html
@@ -0,0 +1,30 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+</head>
+<body>
+<video id="v1" src="black.mp4" autoplay width="320" height="180">
+ <track label="English" src="cues_time_overlapping.webvtt" default>
+</video>
+<script type="text/javascript">
+/**
+ * This test is to ensure that when cues with overlapping times, the one with
+ * earlier end timestamp should disappear when the media time reaches its
+ * end time. This vtt file contains two cues, the first cue is [0,1], the second
+ * cue is [0,4], so after video is playing over 1s, only cue2 should be displayed.
+ */
+async function testTimeOverlappingCues() {
+ const video = document.getElementById("v1");
+ video.ontimeupdate = () => {
+ if (video.currentTime > 1.0) {
+ document.documentElement.removeAttribute('class');
+ video.ontimeupdate = null;
+ }
+ }
+};
+
+window.addEventListener("MozReftestInvalidate",
+ testTimeOverlappingCues);
+</script>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/reftest/vtt_reflow_display-ref.html b/dom/media/webvtt/test/reftest/vtt_reflow_display-ref.html
new file mode 100644
index 0000000000..19f3208e6b
--- /dev/null
+++ b/dom/media/webvtt/test/reftest/vtt_reflow_display-ref.html
@@ -0,0 +1,28 @@
+<!DOCTYPE HTML>
+<html>
+<link rel="stylesheet" href="vtt_reflow_display.css">
+<body>
+<div class="video-player">
+ <div class="video-layer">
+ <video id="v1" autoplay controls></video>
+ </div>
+</div>
+<script>
+/**
+ * Simply play and pause a video without any cues.
+ */
+async function testDisplayCueDuringFrequentReflowRef() {
+ const video = document.getElementById("v1");
+ video.src = "white.webm";
+ video.onplay = _ => {
+ video.onplay = null;
+ video.pause();
+ document.documentElement.removeAttribute('class');
+ }
+};
+
+window.addEventListener("MozReftestInvalidate",
+ testDisplayCueDuringFrequentReflowRef);
+</script>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/reftest/vtt_reflow_display.css b/dom/media/webvtt/test/reftest/vtt_reflow_display.css
new file mode 100644
index 0000000000..4b66a07bd4
--- /dev/null
+++ b/dom/media/webvtt/test/reftest/vtt_reflow_display.css
@@ -0,0 +1,33 @@
+body {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ max-height: 100%;
+ width: 100vw;
+ height: 100vh;
+}
+.video-player {
+ display: flex;
+ max-height: calc(100% - 400px);
+ flex: 1 1 0;
+ flex-direction: column;
+ position: relative;
+ max-width: 100%;
+ height: 0;
+}
+.video-layer {
+ position: relative;
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ height: 100%;
+ flex: 1 1 0;
+}
+video {
+ object-fit: contain;
+ display: flex;
+ flex: auto;
+ max-width: 100%;
+ min-height: 0;
+ min-width: 0;
+}
diff --git a/dom/media/webvtt/test/reftest/vtt_reflow_display.html b/dom/media/webvtt/test/reftest/vtt_reflow_display.html
new file mode 100644
index 0000000000..e7ec496bc1
--- /dev/null
+++ b/dom/media/webvtt/test/reftest/vtt_reflow_display.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+</head>
+<link rel="stylesheet" href="vtt_reflow_display.css">
+<body>
+<div class="video-player">
+ <div class="video-layer">
+ <video id="v1" autoplay controls></video>
+ </div>
+</div>
+<script>
+/**
+ * In bug 1733232, setting some CSS properties (from bug 1733232 comment17)
+ * would cause video frame's reflow called very frequently, which crashed the
+ * video control and caused no cue showing. We compare this test with another
+ * white video without any cues, and they should NOT be equal.
+ */
+function testDisplayCueDuringFrequentReflow() {
+ let video = document.getElementById("v1");
+ video.src = "white.webm";
+ let cue = new VTTCue(0, 4, "hello testing");
+ cue.onenter = _ => {
+ cue.onenter = null;
+ video.pause();
+ document.documentElement.removeAttribute('class');
+ }
+ let track = video.addTextTrack("captions");
+ track.mode = "showing";
+ track.addCue(cue);
+};
+
+window.addEventListener("MozReftestInvalidate",
+ testDisplayCueDuringFrequentReflow);
+</script>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/reftest/vtt_update_display_after_removed_cue.html b/dom/media/webvtt/test/reftest/vtt_update_display_after_removed_cue.html
new file mode 100644
index 0000000000..4cd5d5bbd2
--- /dev/null
+++ b/dom/media/webvtt/test/reftest/vtt_update_display_after_removed_cue.html
@@ -0,0 +1,36 @@
+<!DOCTYPE HTML>
+<html class="reftest-wait">
+<head>
+</head>
+<body>
+<video id="v1" autoplay></video>
+<script type="text/javascript">
+
+/**
+ * This test is used to ensure we would update the cue display after removing
+ * cue from the text track, the removed cue should not display on the video's
+ * rendering area.
+ */
+function testUpdateDisplayAfterRemovedCue() {
+ let video = document.getElementById("v1");
+ video.src = "black.mp4";
+ let cue = new VTTCue(0, 4, "hello testing");
+ let track = video.addTextTrack("captions");
+ track.mode = "showing";
+ track.addCue(cue);
+ cue.onenter = () => {
+ cue.onenter = null;
+ track.removeCue(cue);
+ video.pause();
+ video.onpause = () => {
+ video.onpause = null;
+ document.documentElement.removeAttribute('class');
+ }
+ }
+};
+
+window.addEventListener("MozReftestInvalidate",
+ testUpdateDisplayAfterRemovedCue);
+</script>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/reftest/vtt_update_display_after_removed_cue_ref.html b/dom/media/webvtt/test/reftest/vtt_update_display_after_removed_cue_ref.html
new file mode 100644
index 0000000000..0b1afdc568
--- /dev/null
+++ b/dom/media/webvtt/test/reftest/vtt_update_display_after_removed_cue_ref.html
@@ -0,0 +1,6 @@
+<!DOCTYPE HTML>
+<html>
+<body>
+<video id="v1" src="black.mp4"></video>
+</body>
+</html>
diff --git a/dom/media/webvtt/test/reftest/white.webm b/dom/media/webvtt/test/reftest/white.webm
new file mode 100644
index 0000000000..bbacad7ffd
--- /dev/null
+++ b/dom/media/webvtt/test/reftest/white.webm
Binary files differ
diff --git a/dom/media/webvtt/test/xpcshell/test_parser.js b/dom/media/webvtt/test/xpcshell/test_parser.js
new file mode 100644
index 0000000000..5ae70f9762
--- /dev/null
+++ b/dom/media/webvtt/test/xpcshell/test_parser.js
@@ -0,0 +1,158 @@
+"use strict";
+
+const { WebVTT } = ChromeUtils.importESModule(
+ "resource://gre/modules/vtt.sys.mjs"
+);
+
+let fakeWindow = {
+ /* eslint-disable object-shorthand */
+ VTTCue: function () {},
+ VTTRegion: function () {},
+ /* eslint-enable object-shorthand */
+};
+
+// We have a better parser check in WPT. Here I want to check that incomplete
+// lines are correctly parsable.
+let tests = [
+ // Signature
+ { input: ["WEBVTT"], cue: 0, region: 0 },
+ { input: ["", "WE", "BVT", "T"], cue: 0, region: 0 },
+ { input: ["WEBVTT - This file has no cues."], cue: 0, region: 0 },
+ { input: ["WEBVTT", " - ", "This file has no cues."], cue: 0, region: 0 },
+
+ // Body with IDs
+ {
+ input: [
+ "WEB",
+ "VTT - This file has cues.\n",
+ "\n",
+ "14\n",
+ "00:01:14",
+ ".815 --> 00:0",
+ "1:18.114\n",
+ "- What?\n",
+ "- Where are we now?\n",
+ "\n",
+ "15\n",
+ "00:01:18.171 --> 00:01:20.991\n",
+ "- T",
+ "his is big bat country.\n",
+ "\n",
+ "16\n",
+ "00:01:21.058 --> 00:01:23.868\n",
+ "- [ Bat",
+ "s Screeching ]\n",
+ "- They won't get in your hair. They're after the bug",
+ "s.\n",
+ ],
+ cue: 3,
+ region: 0,
+ },
+
+ // Body without IDs
+ {
+ input: [
+ "WEBVTT - This file has c",
+ "ues.\n",
+ "\n",
+ "00:01:14.815 --> 00:01:18.114\n",
+ "- What?\n",
+ "- Where are we now?\n",
+ "\n",
+ "00:01:18.171 --> 00:01:2",
+ "0.991\n",
+ "- ",
+ "This is big bat country.\n",
+ "\n",
+ "00:01:21.058 --> 00:01:23.868\n",
+ "- [ Bats S",
+ "creeching ]\n",
+ "- They won't get in your hair. They're after the bugs.\n",
+ ],
+ cue: 3,
+ region: 0,
+ },
+
+ // Note
+ {
+ input: ["WEBVTT - This file has no cues.\n", "\n", "NOTE what"],
+ cue: 0,
+ region: 0,
+ },
+
+ // Regions - This vtt is taken from a WPT
+ {
+ input: [
+ "WE",
+ "BVTT\n",
+ "\n",
+ "REGION\n",
+ "id:0\n",
+ "\n",
+ "REGION\n",
+ "id:1\n",
+ "region",
+ "an",
+ "chor:0%,0%\n",
+ "\n",
+ "R",
+ "EGION\n",
+ "id:2\n",
+ "regionanchor:18446744073709552000%,18446744",
+ "073709552000%\n",
+ "\n",
+ "REGION\n",
+ "id:3\n",
+ "regionanchor: 100%,100%\n",
+ "regio",
+ "nanchor :100%,100%\n",
+ "regionanchor:100% ,100%\n",
+ "regionanchor:100%, 100%\n",
+ "regionanchor:100 %,100%\n",
+ "regionanchor:10",
+ "0%,100 %\n",
+ "\n",
+ "00:00:00.000 --> 00:00:01.000",
+ " region:0\n",
+ "text\n",
+ "\n",
+ "00:00:00.000 --> 00:00:01.000 region:1\n",
+ "text\n",
+ "\n",
+ "00:00:00.000 --> 00:00:01.000 region:3\n",
+ "text\n",
+ ],
+ cue: 3,
+ region: 4,
+ },
+];
+
+function run_test() {
+ tests.forEach(test => {
+ let parser = new WebVTT.Parser(fakeWindow, null);
+ ok(!!parser, "Ok... this is a good starting point");
+
+ let cue = 0;
+ parser.oncue = () => {
+ ++cue;
+ };
+
+ let region = 0;
+ parser.onregion = () => {
+ ++region;
+ };
+
+ parser.onparsingerror = () => {
+ ok(false, "No error accepted");
+ };
+
+ test.input.forEach(input => {
+ parser.parse(new TextEncoder().encode(input));
+ });
+
+ parser.flush();
+
+ equal(cue, test.cue, "Cue value matches");
+ equal(region, test.region, "Region value matches");
+ });
+}
diff --git a/dom/media/webvtt/test/xpcshell/xpcshell.ini b/dom/media/webvtt/test/xpcshell/xpcshell.ini
new file mode 100644
index 0000000000..07aa5d80d8
--- /dev/null
+++ b/dom/media/webvtt/test/xpcshell/xpcshell.ini
@@ -0,0 +1,3 @@
+[DEFAULT]
+
+[test_parser.js]
diff --git a/dom/media/webvtt/update-webvtt.js b/dom/media/webvtt/update-webvtt.js
new file mode 100644
index 0000000000..20a3e2669f
--- /dev/null
+++ b/dom/media/webvtt/update-webvtt.js
@@ -0,0 +1,61 @@
+#!/usr/bin/env node
+
+/* eslint-env node */
+
+var gift = require("gift"),
+ fs = require("fs"),
+ argv = require("optimist")
+ .usage(
+ "Update vtt.jsm with the latest from a vtt.js directory.\nUsage:" +
+ " $0 -d [dir]"
+ )
+ .demand("d")
+ .options("d", {
+ alias: "dir",
+ describe: "Path to WebVTT directory.",
+ })
+ .options("r", {
+ alias: "rev",
+ describe: "Revision to update to.",
+ default: "master",
+ })
+ .options("w", {
+ alias: "write",
+ describe: "Path to file to write to.",
+ default: "./vtt.jsm",
+ }).argv;
+
+var repo = gift(argv.d);
+repo.status(function (err, status) {
+ if (!status.clean) {
+ console.log("The repository's working directory is not clean. Aborting.");
+ process.exit(1);
+ }
+ repo.checkout(argv.r, function () {
+ repo.commits(argv.r, 1, function (err, commits) {
+ var vttjs = fs.readFileSync(argv.d + "/lib/vtt.js", "utf8");
+
+ // Remove settings for VIM and Emacs.
+ vttjs = vttjs.replace(/\/\* -\*-.*-\*- \*\/\n/, "");
+ vttjs = vttjs.replace(/\/\* vim:.* \*\/\n/, "");
+
+ // Concatenate header and vttjs code.
+ vttjs =
+ "/* This Source Code Form is subject to the terms of the Mozilla Public\n" +
+ " * License, v. 2.0. If a copy of the MPL was not distributed with this\n" +
+ " * file, You can obtain one at http://mozilla.org/MPL/2.0/. */\n\n" +
+ "export var WebVTT;" +
+ "/**\n" +
+ " * Code below is vtt.js the JS WebVTT implementation.\n" +
+ " * Current source code can be found at http://github.com/mozilla/vtt.js\n" +
+ " *\n" +
+ " * Code taken from commit " +
+ commits[0].id +
+ "\n" +
+ " */\n" +
+ vttjs;
+
+ fs.writeFileSync(argv.w, vttjs);
+ });
+ });
+});
diff --git a/dom/media/webvtt/vtt.sys.mjs b/dom/media/webvtt/vtt.sys.mjs
new file mode 100644
index 0000000000..8b4a830e7f
--- /dev/null
+++ b/dom/media/webvtt/vtt.sys.mjs
@@ -0,0 +1,1663 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Code below is vtt.js the JS WebVTT implementation.
+ * Current source code can be found at http://github.com/mozilla/vtt.js
+ *
+ * Code taken from commit b89bfd06cd788a68c67e03f44561afe833db0849
+ */
+/**
+ * Copyright 2013 vtt.js Contributors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+XPCOMUtils.defineLazyPreferenceGetter(lazy, "supportPseudo",
+ "media.webvtt.pseudo.enabled", false);
+XPCOMUtils.defineLazyPreferenceGetter(lazy, "DEBUG_LOG",
+ "media.webvtt.debug.logging", false);
+
+function LOG(message) {
+ if (lazy.DEBUG_LOG) {
+ dump("[vtt] " + message + "\n");
+ }
+}
+
+var _objCreate = Object.create || (function() {
+ function F() {}
+ return function(o) {
+ if (arguments.length !== 1) {
+ throw new Error('Object.create shim only accepts one parameter.');
+ }
+ F.prototype = o;
+ return new F();
+ };
+})();
+
+// Creates a new ParserError object from an errorData object. The errorData
+// object should have default code and message properties. The default message
+// property can be overriden by passing in a message parameter.
+// See ParsingError.Errors below for acceptable errors.
+function ParsingError(errorData, message) {
+ this.name = "ParsingError";
+ this.code = errorData.code;
+ this.message = message || errorData.message;
+}
+ParsingError.prototype = _objCreate(Error.prototype);
+ParsingError.prototype.constructor = ParsingError;
+
+// ParsingError metadata for acceptable ParsingErrors.
+ParsingError.Errors = {
+ BadSignature: {
+ code: 0,
+ message: "Malformed WebVTT signature."
+ },
+ BadTimeStamp: {
+ code: 1,
+ message: "Malformed time stamp."
+ }
+};
+
+// See spec, https://w3c.github.io/webvtt/#collect-a-webvtt-timestamp.
+function collectTimeStamp(input) {
+ function computeSeconds(h, m, s, f) {
+ if (m > 59 || s > 59) {
+ return null;
+ }
+ // The attribute of the milli-seconds can only be three digits.
+ if (f.length !== 3) {
+ return null;
+ }
+ return (h | 0) * 3600 + (m | 0) * 60 + (s | 0) + (f | 0) / 1000;
+ }
+
+ let timestamp = input.match(/^(\d+:)?(\d{2}):(\d{2})\.(\d+)/);
+ if (!timestamp || timestamp.length !== 5) {
+ return null;
+ }
+
+ let hours = timestamp[1]? timestamp[1].replace(":", "") : 0;
+ let minutes = timestamp[2];
+ let seconds = timestamp[3];
+ let milliSeconds = timestamp[4];
+
+ return computeSeconds(hours, minutes, seconds, milliSeconds);
+}
+
+// A settings object holds key/value pairs and will ignore anything but the first
+// assignment to a specific key.
+function Settings() {
+ this.values = _objCreate(null);
+}
+
+Settings.prototype = {
+ set: function(k, v) {
+ if (v !== "") {
+ this.values[k] = v;
+ }
+ },
+ // Return the value for a key, or a default value.
+ // If 'defaultKey' is passed then 'dflt' is assumed to be an object with
+ // a number of possible default values as properties where 'defaultKey' is
+ // the key of the property that will be chosen; otherwise it's assumed to be
+ // a single value.
+ get: function(k, dflt, defaultKey) {
+ if (defaultKey) {
+ return this.has(k) ? this.values[k] : dflt[defaultKey];
+ }
+ return this.has(k) ? this.values[k] : dflt;
+ },
+ // Check whether we have a value for a key.
+ has: function(k) {
+ return k in this.values;
+ },
+ // Accept a setting if its one of the given alternatives.
+ alt: function(k, v, a) {
+ for (let n = 0; n < a.length; ++n) {
+ if (v === a[n]) {
+ this.set(k, v);
+ return true;
+ }
+ }
+ return false;
+ },
+ // Accept a setting if its a valid digits value (int or float)
+ digitsValue: function(k, v) {
+ if (/^-0+(\.[0]*)?$/.test(v)) { // special case for -0.0
+ this.set(k, 0.0);
+ } else if (/^-?\d+(\.[\d]*)?$/.test(v)) {
+ this.set(k, parseFloat(v));
+ }
+ },
+ // Accept a setting if its a valid percentage.
+ percent: function(k, v) {
+ let m;
+ if ((m = v.match(/^([\d]{1,3})(\.[\d]*)?%$/))) {
+ v = parseFloat(v);
+ if (v >= 0 && v <= 100) {
+ this.set(k, v);
+ return true;
+ }
+ }
+ return false;
+ },
+ // Delete a setting
+ del: function (k) {
+ if (this.has(k)) {
+ delete this.values[k];
+ }
+ },
+};
+
+// Helper function to parse input into groups separated by 'groupDelim', and
+// interprete each group as a key/value pair separated by 'keyValueDelim'.
+function parseOptions(input, callback, keyValueDelim, groupDelim) {
+ let groups = groupDelim ? input.split(groupDelim) : [input];
+ for (let i in groups) {
+ if (typeof groups[i] !== "string") {
+ continue;
+ }
+ let kv = groups[i].split(keyValueDelim);
+ if (kv.length !== 2) {
+ continue;
+ }
+ let k = kv[0];
+ let v = kv[1];
+ callback(k, v);
+ }
+}
+
+function parseCue(input, cue, regionList) {
+ // Remember the original input if we need to throw an error.
+ let oInput = input;
+ // 4.1 WebVTT timestamp
+ function consumeTimeStamp() {
+ let ts = collectTimeStamp(input);
+ if (ts === null) {
+ throw new ParsingError(ParsingError.Errors.BadTimeStamp,
+ "Malformed timestamp: " + oInput);
+ }
+ // Remove time stamp from input.
+ input = input.replace(/^[^\s\uFFFDa-zA-Z-]+/, "");
+ return ts;
+ }
+
+ // 4.4.2 WebVTT cue settings
+ function consumeCueSettings(input, cue) {
+ let settings = new Settings();
+ parseOptions(input, function (k, v) {
+ switch (k) {
+ case "region":
+ // Find the last region we parsed with the same region id.
+ for (let i = regionList.length - 1; i >= 0; i--) {
+ if (regionList[i].id === v) {
+ settings.set(k, regionList[i].region);
+ break;
+ }
+ }
+ break;
+ case "vertical":
+ settings.alt(k, v, ["rl", "lr"]);
+ break;
+ case "line": {
+ let vals = v.split(",");
+ let vals0 = vals[0];
+ settings.digitsValue(k, vals0);
+ settings.percent(k, vals0) ? settings.set("snapToLines", false) : null;
+ settings.alt(k, vals0, ["auto"]);
+ if (vals.length === 2) {
+ settings.alt("lineAlign", vals[1], ["start", "center", "end"]);
+ }
+ break;
+ }
+ case "position": {
+ let vals = v.split(",");
+ if (settings.percent(k, vals[0])) {
+ if (vals.length === 2) {
+ if (!settings.alt("positionAlign", vals[1], ["line-left", "center", "line-right"])) {
+ // Remove the "position" value because the "positionAlign" is not expected value.
+ // It will be set to default value below.
+ settings.del(k);
+ }
+ }
+ }
+ break;
+ }
+ case "size":
+ settings.percent(k, v);
+ break;
+ case "align":
+ settings.alt(k, v, ["start", "center", "end", "left", "right"]);
+ break;
+ }
+ }, /:/, /\t|\n|\f|\r| /); // groupDelim is ASCII whitespace
+
+ // Apply default values for any missing fields.
+ // https://w3c.github.io/webvtt/#collect-a-webvtt-block step 11.4.1.3
+ cue.region = settings.get("region", null);
+ cue.vertical = settings.get("vertical", "");
+ cue.line = settings.get("line", "auto");
+ cue.lineAlign = settings.get("lineAlign", "start");
+ cue.snapToLines = settings.get("snapToLines", true);
+ cue.size = settings.get("size", 100);
+ cue.align = settings.get("align", "center");
+ cue.position = settings.get("position", "auto");
+ cue.positionAlign = settings.get("positionAlign", "auto");
+ }
+
+ function skipWhitespace() {
+ input = input.replace(/^[ \f\n\r\t]+/, "");
+ }
+
+ // 4.1 WebVTT cue timings.
+ skipWhitespace();
+ cue.startTime = consumeTimeStamp(); // (1) collect cue start time
+ skipWhitespace();
+ if (input.substr(0, 3) !== "-->") { // (3) next characters must match "-->"
+ throw new ParsingError(ParsingError.Errors.BadTimeStamp,
+ "Malformed time stamp (time stamps must be separated by '-->'): " +
+ oInput);
+ }
+ input = input.substr(3);
+ skipWhitespace();
+ cue.endTime = consumeTimeStamp(); // (5) collect cue end time
+
+ // 4.1 WebVTT cue settings list.
+ skipWhitespace();
+ consumeCueSettings(input, cue);
+}
+
+function emptyOrOnlyContainsWhiteSpaces(input) {
+ return input == "" || /^[ \f\n\r\t]+$/.test(input);
+}
+
+function containsTimeDirectionSymbol(input) {
+ return input.includes("-->");
+}
+
+function maybeIsTimeStampFormat(input) {
+ return /^\s*(\d+:)?(\d{2}):(\d{2})\.(\d+)\s*-->\s*(\d+:)?(\d{2}):(\d{2})\.(\d+)\s*/.test(input);
+}
+
+var ESCAPE = {
+ "&amp;": "&",
+ "&lt;": "<",
+ "&gt;": ">",
+ "&lrm;": "\u200e",
+ "&rlm;": "\u200f",
+ "&nbsp;": "\u00a0"
+};
+
+var TAG_NAME = {
+ c: "span",
+ i: "i",
+ b: "b",
+ u: "u",
+ ruby: "ruby",
+ rt: "rt",
+ v: "span",
+ lang: "span"
+};
+
+var TAG_ANNOTATION = {
+ v: "title",
+ lang: "lang"
+};
+
+var NEEDS_PARENT = {
+ rt: "ruby"
+};
+
+const PARSE_CONTENT_MODE = {
+ NORMAL_CUE: "normal_cue",
+ PSUEDO_CUE: "pseudo_cue",
+ DOCUMENT_FRAGMENT: "document_fragment",
+ REGION_CUE: "region_cue",
+}
+// Parse content into a document fragment.
+function parseContent(window, input, mode) {
+ function nextToken() {
+ // Check for end-of-string.
+ if (!input) {
+ return null;
+ }
+
+ // Consume 'n' characters from the input.
+ function consume(result) {
+ input = input.substr(result.length);
+ return result;
+ }
+
+ let m = input.match(/^([^<]*)(<[^>]+>?)?/);
+ // The input doesn't contain a complete tag.
+ if (!m[0]) {
+ return null;
+ }
+ // If there is some text before the next tag, return it, otherwise return
+ // the tag.
+ return consume(m[1] ? m[1] : m[2]);
+ }
+
+ // Unescape a string 's'.
+ function unescape1(e) {
+ return ESCAPE[e];
+ }
+ function unescape(s) {
+ let m;
+ while ((m = s.match(/&(amp|lt|gt|lrm|rlm|nbsp);/))) {
+ s = s.replace(m[0], unescape1);
+ }
+ return s;
+ }
+
+ function shouldAdd(current, element) {
+ return !NEEDS_PARENT[element.localName] ||
+ NEEDS_PARENT[element.localName] === current.localName;
+ }
+
+ // Create an element for this tag.
+ function createElement(type, annotation) {
+ let tagName = TAG_NAME[type];
+ if (!tagName) {
+ return null;
+ }
+ let element = window.document.createElement(tagName);
+ let name = TAG_ANNOTATION[type];
+ if (name) {
+ element[name] = annotation ? annotation.trim() : "";
+ }
+ return element;
+ }
+
+ // https://w3c.github.io/webvtt/#webvtt-timestamp-object
+ // Return hhhhh:mm:ss.fff
+ function normalizedTimeStamp(secondsWithFrag) {
+ let totalsec = parseInt(secondsWithFrag, 10);
+ let hours = Math.floor(totalsec / 3600);
+ let minutes = Math.floor(totalsec % 3600 / 60);
+ let seconds = Math.floor(totalsec % 60);
+ if (hours < 10) {
+ hours = "0" + hours;
+ }
+ if (minutes < 10) {
+ minutes = "0" + minutes;
+ }
+ if (seconds < 10) {
+ seconds = "0" + seconds;
+ }
+ let f = secondsWithFrag.toString().split(".");
+ if (f[1]) {
+ f = f[1].slice(0, 3).padEnd(3, "0");
+ } else {
+ f = "000";
+ }
+ return hours + ':' + minutes + ':' + seconds + '.' + f;
+ }
+
+ let root;
+ switch (mode) {
+ case PARSE_CONTENT_MODE.PSUEDO_CUE:
+ root = window.document.createElement("span", {pseudo: "::cue"});
+ break;
+ case PARSE_CONTENT_MODE.NORMAL_CUE:
+ case PARSE_CONTENT_MODE.REGION_CUE:
+ root = window.document.createElement("span");
+ break;
+ case PARSE_CONTENT_MODE.DOCUMENT_FRAGMENT:
+ root = window.document.createDocumentFragment();
+ break;
+ }
+
+ if (!input) {
+ root.appendChild(window.document.createTextNode(""));
+ return root;
+ }
+
+ let current = root,
+ t,
+ tagStack = [];
+
+ while ((t = nextToken()) !== null) {
+ if (t[0] === '<') {
+ if (t[1] === "/") {
+ // If the closing tag matches, move back up to the parent node.
+ if (tagStack.length &&
+ tagStack[tagStack.length - 1] === t.substr(2).replace(">", "")) {
+ tagStack.pop();
+ current = current.parentNode;
+ }
+ // Otherwise just ignore the end tag.
+ continue;
+ }
+ let ts = collectTimeStamp(t.substr(1, t.length - 1));
+ let node;
+ if (ts) {
+ // Timestamps are lead nodes as well.
+ node = window.document.createProcessingInstruction("timestamp", normalizedTimeStamp(ts));
+ current.appendChild(node);
+ continue;
+ }
+ let m = t.match(/^<([^.\s/0-9>]+)(\.[^\s\\>]+)?([^>\\]+)?(\\?)>?$/);
+ // If we can't parse the tag, skip to the next tag.
+ if (!m) {
+ continue;
+ }
+ // Try to construct an element, and ignore the tag if we couldn't.
+ node = createElement(m[1], m[3]);
+ if (!node) {
+ continue;
+ }
+ // Determine if the tag should be added based on the context of where it
+ // is placed in the cuetext.
+ if (!shouldAdd(current, node)) {
+ continue;
+ }
+ // Set the class list (as a list of classes, separated by space).
+ if (m[2]) {
+ node.className = m[2].substr(1).replace('.', ' ');
+ }
+ // Append the node to the current node, and enter the scope of the new
+ // node.
+ tagStack.push(m[1]);
+ current.appendChild(node);
+ current = node;
+ continue;
+ }
+
+ // Text nodes are leaf nodes.
+ current.appendChild(window.document.createTextNode(unescape(t)));
+ }
+
+ return root;
+}
+
+function StyleBox() {
+}
+
+// Apply styles to a div. If there is no div passed then it defaults to the
+// div on 'this'.
+StyleBox.prototype.applyStyles = function(styles, div) {
+ div = div || this.div;
+ for (let prop in styles) {
+ if (styles.hasOwnProperty(prop)) {
+ div.style[prop] = styles[prop];
+ }
+ }
+};
+
+StyleBox.prototype.formatStyle = function(val, unit) {
+ return val === 0 ? 0 : val + unit;
+};
+
+// TODO(alwu): remove StyleBox and change other style box to class-based.
+class StyleBoxBase {
+ applyStyles(styles, div) {
+ div = div || this.div;
+ Object.assign(div.style, styles);
+ }
+
+ formatStyle(val, unit) {
+ return val === 0 ? 0 : val + unit;
+ }
+}
+
+// Constructs the computed display state of the cue (a div). Places the div
+// into the overlay which should be a block level element (usually a div).
+class CueStyleBox extends StyleBoxBase {
+ constructor(window, cue, containerBox) {
+ super();
+ this.cue = cue;
+ this.div = window.document.createElement("div");
+ this.cueDiv = parseContent(window, cue.text, lazy.supportPseudo ?
+ PARSE_CONTENT_MODE.PSUEDO_CUE : PARSE_CONTENT_MODE.NORMAL_CUE);
+ this.div.appendChild(this.cueDiv);
+
+ this.containerHeight = containerBox.height;
+ this.containerWidth = containerBox.width;
+ this.fontSize = this._getFontSize(containerBox);
+ this.isCueStyleBox = true;
+
+ // As pseudo element won't inherit the parent div's style, so we have to
+ // set the font size explicitly.
+ if (lazy.supportPseudo) {
+ this._applyDefaultStylesOnPseudoBackgroundNode();
+ } else {
+ this._applyDefaultStylesOnNonPseudoBackgroundNode();
+ }
+ this._applyDefaultStylesOnRootNode();
+ }
+
+ getCueBoxPositionAndSize() {
+ // As `top`, `left`, `width` and `height` are all represented by the
+ // percentage of the container, we need to convert them to the actual
+ // number according to the container's size.
+ const isWritingDirectionHorizontal = this.cue.vertical == "";
+ let top =
+ this.containerHeight * this._tranferPercentageToFloat(this.div.style.top),
+ left =
+ this.containerWidth * this._tranferPercentageToFloat(this.div.style.left),
+ width = isWritingDirectionHorizontal ?
+ this.containerWidth * this._tranferPercentageToFloat(this.div.style.width) :
+ this.div.clientWidthDouble,
+ height = isWritingDirectionHorizontal ?
+ this.div.clientHeightDouble :
+ this.containerHeight * this._tranferPercentageToFloat(this.div.style.height);
+ return { top, left, width, height };
+ }
+
+ getFirstLineBoxSize() {
+ // This size would be automatically adjusted by writing direction. When
+ // direction is horizontal, it represents box's height. When direction is
+ // vertical, it represents box's width.
+ return this.div.firstLineBoxBSize;
+ }
+
+ setBidiRule() {
+ // This function is a workaround which is used to force the reflow in order
+ // to use the correct alignment for bidi text. Now this function would be
+ // called after calculating the final position of the cue box to ensure the
+ // rendering result is correct. See bug1557882 comment3 for more details.
+ // TODO : remove this function and set `unicode-bidi` when initiailizing
+ // the CueStyleBox, after fixing bug1558431.
+ this.applyStyles({ "unicode-bidi": "plaintext" });
+ }
+
+ /**
+ * Following methods are private functions, should not use them outside this
+ * class.
+ */
+ _tranferPercentageToFloat(input) {
+ return input.replace("%", "") / 100.0;
+ }
+
+ _getFontSize(containerBox) {
+ // In https://www.w3.org/TR/webvtt1/#applying-css-properties, the spec
+ // said the font size is '5vh', which means 5% of the viewport height.
+ // However, if we use 'vh' as a basic unit, it would eventually become
+ // 5% of screen height, instead of video's viewport height. Therefore, we
+ // have to use 'px' here to make sure we have the correct font size.
+ return containerBox.height * 0.05 + "px";
+ }
+
+ _applyDefaultStylesOnPseudoBackgroundNode() {
+ // most of the properties have been defined in `::cue` in `html.css`, but
+ // there are some css variables we have to set them dynamically.
+ this.cueDiv.style.setProperty("--cue-font-size", this.fontSize, "important");
+ this.cueDiv.style.setProperty("--cue-writing-mode", this._getCueWritingMode(), "important");
+ }
+
+ _applyDefaultStylesOnNonPseudoBackgroundNode() {
+ // If cue div is not a pseudo element, we should set the default css style
+ // for it, the reason we need to set these attributes to cueDiv is because
+ // if we set background on the root node directly, if would cause filling
+ // too large area for the background color as the size of root node won't
+ // be adjusted by cue size.
+ this.applyStyles({
+ "background-color": "rgba(0, 0, 0, 0.8)",
+ }, this.cueDiv);
+ }
+
+ // spec https://www.w3.org/TR/webvtt1/#applying-css-properties
+ _applyDefaultStylesOnRootNode() {
+ // The variables writing-mode, top, left, width, and height are calculated
+ // in the spec 7.2, https://www.w3.org/TR/webvtt1/#processing-cue-settings
+ // spec 7.2.1, calculate 'writing-mode'.
+ const writingMode = this._getCueWritingMode();
+
+ // spec 7.2.2 ~ 7.2.7, calculate 'width', 'height', 'left' and 'top'.
+ const {width, height, left, top} = this._getCueSizeAndPosition();
+
+ this.applyStyles({
+ "position": "absolute",
+ // "unicode-bidi": "plaintext", (uncomment this line after fixing bug1558431)
+ "writing-mode": writingMode,
+ "top": top,
+ "left": left,
+ "width": width,
+ "height": height,
+ "overflow-wrap": "break-word",
+ // "text-wrap": "balance", (we haven't supported this CSS attribute yet)
+ "white-space": "pre-line",
+ "font": this.fontSize + " sans-serif",
+ "color": "rgba(255, 255, 255, 1)",
+ "white-space": "pre-line",
+ "text-align": this.cue.align,
+ });
+ }
+
+ _getCueWritingMode() {
+ const cue = this.cue;
+ if (cue.vertical == "") {
+ return "horizontal-tb";
+ }
+ return cue.vertical == "lr" ? "vertical-lr" : "vertical-rl";
+ }
+
+ _getCueSizeAndPosition() {
+ const cue = this.cue;
+ // spec 7.2.2, determine the value of maximum size for cue as per the
+ // appropriate rules from the following list.
+ let maximumSize;
+ let computedPosition = cue.computedPosition;
+ switch (cue.computedPositionAlign) {
+ case "line-left":
+ maximumSize = 100 - computedPosition;
+ break;
+ case "line-right":
+ maximumSize = computedPosition;
+ break;
+ case "center":
+ maximumSize = computedPosition <= 50 ?
+ computedPosition * 2 : (100 - computedPosition) * 2;
+ break;
+ }
+ const size = Math.min(cue.size, maximumSize);
+
+ // spec 7.2.5, determine the value of x-position or y-position for cue as
+ // per the appropriate rules from the following list.
+ let xPosition = 0.0, yPosition = 0.0;
+ const isWritingDirectionHorizontal = cue.vertical == "";
+ switch (cue.computedPositionAlign) {
+ case "line-left":
+ if (isWritingDirectionHorizontal) {
+ xPosition = cue.computedPosition;
+ } else {
+ yPosition = cue.computedPosition;
+ }
+ break;
+ case "center":
+ if (isWritingDirectionHorizontal) {
+ xPosition = cue.computedPosition - (size / 2);
+ } else {
+ yPosition = cue.computedPosition - (size / 2);
+ }
+ break;
+ case "line-right":
+ if (isWritingDirectionHorizontal) {
+ xPosition = cue.computedPosition - size;
+ } else {
+ yPosition = cue.computedPosition - size;
+ }
+ break;
+ }
+
+ // spec 7.2.6, determine the value of whichever of x-position or
+ // y-position is not yet calculated for cue as per the appropriate rules
+ // from the following list.
+ if (!cue.snapToLines) {
+ if (isWritingDirectionHorizontal) {
+ yPosition = cue.computedLine;
+ } else {
+ xPosition = cue.computedLine;
+ }
+ } else {
+ if (isWritingDirectionHorizontal) {
+ yPosition = 0;
+ } else {
+ xPosition = 0;
+ }
+ }
+ return {
+ left: xPosition + "%",
+ top: yPosition + "%",
+ width: isWritingDirectionHorizontal ? size + "%" : "auto",
+ height: isWritingDirectionHorizontal ? "auto" : size + "%",
+ };
+ }
+}
+
+function RegionNodeBox(window, region, container) {
+ StyleBox.call(this);
+
+ let boxLineHeight = container.height * 0.0533 // 0.0533vh ? 5.33vh
+ let boxHeight = boxLineHeight * region.lines;
+ let boxWidth = container.width * region.width / 100; // convert percentage to px
+
+ let regionNodeStyles = {
+ position: "absolute",
+ height: boxHeight + "px",
+ width: boxWidth + "px",
+ top: (region.viewportAnchorY * container.height / 100) - (region.regionAnchorY * boxHeight / 100) + "px",
+ left: (region.viewportAnchorX * container.width / 100) - (region.regionAnchorX * boxWidth / 100) + "px",
+ lineHeight: boxLineHeight + "px",
+ writingMode: "horizontal-tb",
+ backgroundColor: "rgba(0, 0, 0, 0.8)",
+ wordWrap: "break-word",
+ overflowWrap: "break-word",
+ font: (boxLineHeight/1.3) + "px sans-serif",
+ color: "rgba(255, 255, 255, 1)",
+ overflow: "hidden",
+ minHeight: "0px",
+ maxHeight: boxHeight + "px",
+ display: "inline-flex",
+ flexFlow: "column",
+ justifyContent: "flex-end",
+ };
+
+ this.div = window.document.createElement("div");
+ this.div.id = region.id; // useless?
+ this.applyStyles(regionNodeStyles);
+}
+RegionNodeBox.prototype = _objCreate(StyleBox.prototype);
+RegionNodeBox.prototype.constructor = RegionNodeBox;
+
+function RegionCueStyleBox(window, cue) {
+ StyleBox.call(this);
+ this.cueDiv = parseContent(window, cue.text, PARSE_CONTENT_MODE.REGION_CUE);
+
+ let regionCueStyles = {
+ position: "relative",
+ writingMode: "horizontal-tb",
+ unicodeBidi: "plaintext",
+ width: "auto",
+ height: "auto",
+ textAlign: cue.align,
+ };
+ // TODO: fix me, LTR and RTL ? using margin replace the "left/right"
+ // 6.1.14.3.3
+ let offset = cue.computedPosition * cue.region.width / 100;
+ // 6.1.14.3.4
+ switch (cue.align) {
+ case "start":
+ case "left":
+ regionCueStyles.left = offset + "%";
+ regionCueStyles.right = "auto";
+ break;
+ case "end":
+ case "right":
+ regionCueStyles.left = "auto";
+ regionCueStyles.right = offset + "%";
+ break;
+ case "middle":
+ break;
+ }
+
+ this.div = window.document.createElement("div");
+ this.applyStyles(regionCueStyles);
+ this.div.appendChild(this.cueDiv);
+}
+RegionCueStyleBox.prototype = _objCreate(StyleBox.prototype);
+RegionCueStyleBox.prototype.constructor = RegionCueStyleBox;
+
+// Represents the co-ordinates of an Element in a way that we can easily
+// compute things with such as if it overlaps or intersects with other boxes.
+class BoxPosition {
+ constructor(obj) {
+ // Get dimensions by calling getCueBoxPositionAndSize on a CueStyleBox, by
+ // getting offset properties from an HTMLElement (from the object or its
+ // `div` property), otherwise look at the regular box properties on the
+ // object.
+ const isHTMLElement = !obj.isCueStyleBox && (obj.div || obj.tagName);
+ obj = obj.isCueStyleBox ? obj.getCueBoxPositionAndSize() : obj.div || obj;
+ this.top = isHTMLElement ? obj.offsetTop : obj.top;
+ this.left = isHTMLElement ? obj.offsetLeft : obj.left;
+ this.width = isHTMLElement ? obj.offsetWidth : obj.width;
+ this.height = isHTMLElement ? obj.offsetHeight : obj.height;
+ // This value is smaller than 1 app unit (~= 0.0166 px).
+ this.fuzz = 0.01;
+ }
+
+ get bottom() {
+ return this.top + this.height;
+ }
+
+ get right() {
+ return this.left + this.width;
+ }
+
+ // This function is used for debugging, it will return the box's information.
+ getBoxInfoInChars() {
+ return `top=${this.top}, bottom=${this.bottom}, left=${this.left}, ` +
+ `right=${this.right}, width=${this.width}, height=${this.height}`;
+ }
+
+ // Move the box along a particular axis. Optionally pass in an amount to move
+ // the box. If no amount is passed then the default is the line height of the
+ // box.
+ move(axis, toMove) {
+ switch (axis) {
+ case "+x":
+ LOG(`box's left moved from ${this.left} to ${this.left + toMove}`);
+ this.left += toMove;
+ break;
+ case "-x":
+ LOG(`box's left moved from ${this.left} to ${this.left - toMove}`);
+ this.left -= toMove;
+ break;
+ case "+y":
+ LOG(`box's top moved from ${this.top} to ${this.top + toMove}`);
+ this.top += toMove;
+ break;
+ case "-y":
+ LOG(`box's top moved from ${this.top} to ${this.top - toMove}`);
+ this.top -= toMove;
+ break;
+ }
+ }
+
+ // Check if this box overlaps another box, b2.
+ overlaps(b2) {
+ return (this.left < b2.right - this.fuzz) &&
+ (this.right > b2.left + this.fuzz) &&
+ (this.top < b2.bottom - this.fuzz) &&
+ (this.bottom > b2.top + this.fuzz);
+ }
+
+ // Check if this box overlaps any other boxes in boxes.
+ overlapsAny(boxes) {
+ for (let i = 0; i < boxes.length; i++) {
+ if (this.overlaps(boxes[i])) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // Check if this box is within another box.
+ within(container) {
+ return (this.top >= container.top - this.fuzz) &&
+ (this.bottom <= container.bottom + this.fuzz) &&
+ (this.left >= container.left - this.fuzz) &&
+ (this.right <= container.right + this.fuzz);
+ }
+
+ // Check whether this box is passed over the specfic axis boundary. The axis
+ // is based on the canvas coordinates, the `+x` is rightward and `+y` is
+ // downward.
+ isOutsideTheAxisBoundary(container, axis) {
+ switch (axis) {
+ case "+x":
+ return this.right > container.right + this.fuzz;
+ case "-x":
+ return this.left < container.left - this.fuzz;
+ case "+y":
+ return this.bottom > container.bottom + this.fuzz;
+ case "-y":
+ return this.top < container.top - this.fuzz;
+ }
+ }
+
+ // Find the percentage of the area that this box is overlapping with another
+ // box.
+ intersectPercentage(b2) {
+ let x = Math.max(0, Math.min(this.right, b2.right) - Math.max(this.left, b2.left)),
+ y = Math.max(0, Math.min(this.bottom, b2.bottom) - Math.max(this.top, b2.top)),
+ intersectArea = x * y;
+ return intersectArea / (this.height * this.width);
+ }
+}
+
+BoxPosition.prototype.clone = function(){
+ return new BoxPosition(this);
+};
+
+function adjustBoxPosition(styleBox, containerBox, controlBarBox, outputBoxes) {
+ const cue = styleBox.cue;
+ const isWritingDirectionHorizontal = cue.vertical == "";
+ let box = new BoxPosition(styleBox);
+ if (!box.width || !box.height) {
+ LOG(`No way to adjust a box with zero width or height.`);
+ return;
+ }
+
+ // Spec 7.2.10, adjust the positions of boxes according to the appropriate
+ // steps from the following list. Also, we use offsetHeight/offsetWidth here
+ // in order to prevent the incorrect positioning caused by CSS transform
+ // scale.
+ const fullDimension = isWritingDirectionHorizontal ?
+ containerBox.height : containerBox.width;
+ if (cue.snapToLines) {
+ LOG(`Adjust position when 'snap-to-lines' is true.`);
+ // The step is the height or width of the line box. We should use font
+ // size directly, instead of using text box's width or height, because the
+ // width or height of the box would be changed when the text is wrapped to
+ // different line. Ex. if text is wrapped to two line, the height or width
+ // of the box would become 2 times of font size.
+ let step = styleBox.getFirstLineBoxSize();
+ if (step == 0) {
+ return;
+ }
+
+ // spec 7.2.10.4 ~ 7.2.10.6
+ let line = Math.floor(cue.computedLine + 0.5);
+ if (cue.vertical == "rl") {
+ line = -1 * (line + 1);
+ }
+
+ // spec 7.2.10.7 ~ 7.2.10.8
+ let position = step * line;
+ if (cue.vertical == "rl") {
+ position = position - box.width + step;
+ }
+
+ // spec 7.2.10.9
+ if (line < 0) {
+ position += fullDimension;
+ step = -1 * step;
+ }
+
+ // spec 7.2.10.10, move the box to the specific position along the direction.
+ const movingDirection = isWritingDirectionHorizontal ? "+y" : "+x";
+ box.move(movingDirection, position);
+
+ // spec 7.2.10.11, remember the position as specified position.
+ let specifiedPosition = box.clone();
+
+ // spec 7.2.10.12, let title area be a box that covers all of the video’s
+ // rendering area.
+ const titleAreaBox = containerBox.clone();
+ if (controlBarBox) {
+ titleAreaBox.height -= controlBarBox.height;
+ }
+
+ function isBoxOutsideTheRenderingArea() {
+ if (isWritingDirectionHorizontal) {
+ // the top side of the box is above the rendering area, or the bottom
+ // side of the box is below the rendering area.
+ return step < 0 && box.top < 0 ||
+ step > 0 && box.bottom > fullDimension;
+ }
+ // the left side of the box is outside the left side of the rendering
+ // area, or the right side of the box is outside the right side of the
+ // rendering area.
+ return step < 0 && box.left < 0 ||
+ step > 0 && box.right > fullDimension;
+ }
+
+ // spec 7.2.10.13, if none of the boxes in boxes would overlap any of the
+ // boxes in output, and all of the boxes in boxes are entirely within the
+ // title area box.
+ let switched = false;
+ while (!box.within(titleAreaBox) || box.overlapsAny(outputBoxes)) {
+ // spec 7.2.10.14, check if we need to switch the direction.
+ if (isBoxOutsideTheRenderingArea()) {
+ // spec 7.2.10.17, if `switched` is true, remove all the boxes in
+ // `boxes`, which means we shouldn't apply any CSS boxes for this cue.
+ // Therefore, returns null box.
+ if (switched) {
+ return null;
+ }
+ // spec 7.2.10.18 ~ 7.2.10.20
+ switched = true;
+ box = specifiedPosition.clone();
+ step = -1 * step;
+ }
+ // spec 7.2.10.15, moving box along the specific direction.
+ box.move(movingDirection, step);
+ }
+
+ if (isWritingDirectionHorizontal) {
+ styleBox.applyStyles({
+ top: getPercentagePosition(box.top, fullDimension),
+ });
+ } else {
+ styleBox.applyStyles({
+ left: getPercentagePosition(box.left, fullDimension),
+ });
+ }
+ } else {
+ LOG(`Adjust position when 'snap-to-lines' is false.`);
+ // (snap-to-lines if false) spec 7.2.10.1 ~ 7.2.10.2
+ if (cue.lineAlign != "start") {
+ const isCenterAlign = cue.lineAlign == "center";
+ const movingDirection = isWritingDirectionHorizontal ? "-y" : "-x";
+ if (isWritingDirectionHorizontal) {
+ box.move(movingDirection, isCenterAlign ? box.height : box.height / 2);
+ } else {
+ box.move(movingDirection, isCenterAlign ? box.width : box.width / 2);
+ }
+ }
+
+ // spec 7.2.10.3
+ let bestPosition = {},
+ specifiedPosition = box.clone(),
+ outsideAreaPercentage = 1; // Highest possible so the first thing we get is better.
+ let hasFoundBestPosition = false;
+
+ // For the different writing directions, we should have different priority
+ // for the moving direction. For example, if the writing direction is
+ // horizontal, which means the cues will grow from the top to the bottom,
+ // then moving cues along the `y` axis should be more important than moving
+ // cues along the `x` axis, and vice versa for those cues growing from the
+ // left to right, or from the right to the left. We don't follow the exact
+ // way which the spec requires, see the reason in bug1575460.
+ function getAxis(writingDirection) {
+ if (writingDirection == "") {
+ return ["+y", "-y", "+x", "-x"];
+ }
+ // Growing from left to right.
+ if (writingDirection == "lr") {
+ return ["+x", "-x", "+y", "-y"];
+ }
+ // Growing from right to left.
+ return ["-x", "+x", "+y", "-y"];
+ }
+ const axis = getAxis(cue.vertical);
+
+ // This factor effects the granularity of the moving unit, when using the
+ // factor=1 often moves too much and results in too many redudant spaces
+ // between boxes. So we can increase the factor to slightly reduce the
+ // move we do every time, but still can preverse the reasonable spaces
+ // between boxes.
+ const factor = 4;
+ const toMove = styleBox.getFirstLineBoxSize() / factor;
+ for (let i = 0; i < axis.length && !hasFoundBestPosition; i++) {
+ while (!box.isOutsideTheAxisBoundary(containerBox, axis[i]) &&
+ (!box.within(containerBox) || box.overlapsAny(outputBoxes))) {
+ box.move(axis[i], toMove);
+ }
+ // We found a spot where we aren't overlapping anything. This is our
+ // best position.
+ if (box.within(containerBox)) {
+ bestPosition = box.clone();
+ hasFoundBestPosition = true;
+ break;
+ }
+ let p = box.intersectPercentage(containerBox);
+ // If we're outside the container box less then we were on our last try
+ // then remember this position as the best position.
+ if (outsideAreaPercentage > p) {
+ bestPosition = box.clone();
+ outsideAreaPercentage = p;
+ }
+ // Reset the box position to the specified position.
+ box = specifiedPosition.clone();
+ }
+
+ // Can not find a place to place this box inside the rendering area.
+ if (!box.within(containerBox)) {
+ return null;
+ }
+
+ styleBox.applyStyles({
+ top: getPercentagePosition(box.top, containerBox.height),
+ left: getPercentagePosition(box.left, containerBox.width),
+ });
+ }
+
+ // In order to not be affected by CSS scale, so we use '%' to make sure the
+ // cue can stick in the right position.
+ function getPercentagePosition(position, fullDimension) {
+ return (position / fullDimension) * 100 + "%";
+ }
+
+ return box;
+}
+
+export function WebVTT() {
+ this.isProcessingCues = false;
+ // Nothing
+}
+
+// Helper to allow strings to be decoded instead of the default binary utf8 data.
+WebVTT.StringDecoder = function() {
+ return {
+ decode: function(data) {
+ if (!data) {
+ return "";
+ }
+ if (typeof data !== "string") {
+ throw new Error("Error - expected string data.");
+ }
+ return decodeURIComponent(encodeURIComponent(data));
+ }
+ };
+};
+
+WebVTT.convertCueToDOMTree = function(window, cuetext) {
+ if (!window) {
+ return null;
+ }
+ return parseContent(window, cuetext, PARSE_CONTENT_MODE.DOCUMENT_FRAGMENT);
+};
+
+function clearAllCuesDiv(overlay) {
+ while (overlay.firstChild) {
+ overlay.firstChild.remove();
+ }
+}
+
+// It's used to record how many cues we process in the last `processCues` run.
+var lastDisplayedCueNums = 0;
+
+const DIV_COMPUTING_STATE = {
+ REUSE : 0,
+ REUSE_AND_CLEAR : 1,
+ COMPUTE_AND_CLEAR : 2
+};
+
+// Runs the processing model over the cues and regions passed to it.
+// Spec https://www.w3.org/TR/webvtt1/#processing-model
+// @parem window : JS window
+// @param cues : the VTT cues are going to be displayed.
+// @param overlay : A block level element (usually a div) that the computed cues
+// and regions will be placed into.
+// @param controls : A Control bar element. Cues' position will be
+// affected and repositioned according to it.
+function processCuesInternal(window, cues, overlay, controls) {
+ LOG(`=== processCues ===`);
+ if (!cues) {
+ LOG(`clear display and abort processing because of no cue.`);
+ clearAllCuesDiv(overlay);
+ lastDisplayedCueNums = 0;
+ return;
+ }
+
+ let controlBar, controlBarShown;
+ if (controls) {
+ // controls is a <div> that is the children of the UA Widget Shadow Root.
+ controlBar = controls.parentNode.getElementById("controlBar");
+ controlBarShown = controlBar ? !controlBar.hidden : false;
+ } else {
+ // There is no controls element. This only happen to UA Widget because
+ // it is created lazily.
+ controlBarShown = false;
+ }
+
+ /**
+ * This function is used to tell us if we have to recompute or reuse current
+ * cue's display state. Display state is a DIV element with corresponding
+ * CSS style to display cue on the screen. When the cue is being displayed
+ * first time, we will compute its display state. After that, we could reuse
+ * its state until following conditions happen.
+ * (1) control changes : it means the rendering area changes so we should
+ * recompute cues' position.
+ * (2) cue's `hasBeenReset` flag is true : it means cues' line or position
+ * property has been modified, we also need to recompute cues' position.
+ * (3) the amount of showing cues changes : it means some cue would disappear
+ * but other cues should stay at the same place without recomputing, so we
+ * can resume their display state.
+ */
+ function getDIVComputingState(cues) {
+ if (overlay.lastControlBarShownStatus != controlBarShown) {
+ return DIV_COMPUTING_STATE.COMPUTE_AND_CLEAR;
+ }
+
+ for (let i = 0; i < cues.length; i++) {
+ if (cues[i].hasBeenReset || !cues[i].displayState) {
+ return DIV_COMPUTING_STATE.COMPUTE_AND_CLEAR;
+ }
+ }
+
+ if (lastDisplayedCueNums != cues.length) {
+ return DIV_COMPUTING_STATE.REUSE_AND_CLEAR;
+ }
+ return DIV_COMPUTING_STATE.REUSE;
+ }
+
+ const divState = getDIVComputingState(cues);
+ overlay.lastControlBarShownStatus = controlBarShown;
+
+ if (divState == DIV_COMPUTING_STATE.REUSE) {
+ LOG(`reuse current cue's display state and abort processing`);
+ return;
+ }
+
+ clearAllCuesDiv(overlay);
+ let rootOfCues = window.document.createElement("div");
+ rootOfCues.style.position = "absolute";
+ rootOfCues.style.left = "0";
+ rootOfCues.style.right = "0";
+ rootOfCues.style.top = "0";
+ rootOfCues.style.bottom = "0";
+ overlay.appendChild(rootOfCues);
+
+ if (divState == DIV_COMPUTING_STATE.REUSE_AND_CLEAR) {
+ LOG(`clear display but reuse cues' display state.`);
+ for (let cue of cues) {
+ rootOfCues.appendChild(cue.displayState);
+ }
+ } else if (divState == DIV_COMPUTING_STATE.COMPUTE_AND_CLEAR) {
+ LOG(`clear display and recompute cues' display state.`);
+ let boxPositions = [],
+ containerBox = new BoxPosition(rootOfCues);
+
+ let styleBox, cue, controlBarBox;
+ if (controlBarShown) {
+ controlBarBox = new BoxPosition(controlBar);
+ // Add an empty output box that cover the same region as video control bar.
+ boxPositions.push(controlBarBox);
+ }
+
+ // https://w3c.github.io/webvtt/#processing-model 6.1.12.1
+ // Create regionNode
+ let regionNodeBoxes = {};
+ let regionNodeBox;
+
+ LOG(`lastDisplayedCueNums=${lastDisplayedCueNums}, currentCueNums=${cues.length}`);
+ lastDisplayedCueNums = cues.length;
+ for (let i = 0; i < cues.length; i++) {
+ cue = cues[i];
+ if (cue.region != null) {
+ // 6.1.14.1
+ styleBox = new RegionCueStyleBox(window, cue);
+
+ if (!regionNodeBoxes[cue.region.id]) {
+ // create regionNode
+ // Adjust the container hieght to exclude the controlBar
+ let adjustContainerBox = new BoxPosition(rootOfCues);
+ if (controlBarShown) {
+ adjustContainerBox.height -= controlBarBox.height;
+ adjustContainerBox.bottom += controlBarBox.height;
+ }
+ regionNodeBox = new RegionNodeBox(window, cue.region, adjustContainerBox);
+ regionNodeBoxes[cue.region.id] = regionNodeBox;
+ }
+ // 6.1.14.3
+ let currentRegionBox = regionNodeBoxes[cue.region.id];
+ let currentRegionNodeDiv = currentRegionBox.div;
+ // 6.1.14.3.2
+ // TODO: fix me, it looks like the we need to set/change "top" attribute at the styleBox.div
+ // to do the "scroll up", however, we do not implement it yet?
+ if (cue.region.scroll == "up" && currentRegionNodeDiv.childElementCount > 0) {
+ styleBox.div.style.transitionProperty = "top";
+ styleBox.div.style.transitionDuration = "0.433s";
+ }
+
+ currentRegionNodeDiv.appendChild(styleBox.div);
+ rootOfCues.appendChild(currentRegionNodeDiv);
+ cue.displayState = styleBox.div;
+ boxPositions.push(new BoxPosition(currentRegionBox));
+ } else {
+ // Compute the intial position and styles of the cue div.
+ styleBox = new CueStyleBox(window, cue, containerBox);
+ rootOfCues.appendChild(styleBox.div);
+
+ // Move the cue to correct position, we might get the null box if the
+ // result of algorithm doesn't want us to show the cue when we don't
+ // have any room for this cue.
+ let cueBox = adjustBoxPosition(styleBox, containerBox, controlBarBox, boxPositions);
+ if (cueBox) {
+ styleBox.setBidiRule();
+ // Remember the computed div so that we don't have to recompute it later
+ // if we don't have too.
+ cue.displayState = styleBox.div;
+ boxPositions.push(cueBox);
+ LOG(`cue ${i}, ` + cueBox.getBoxInfoInChars());
+ } else {
+ LOG(`can not find a proper position to place cue ${i}`);
+ // Clear the display state and clear the reset flag in the cue as well,
+ // which controls whether the task for updating the cue display is
+ // dispatched.
+ cue.displayState = null;
+ rootOfCues.removeChild(styleBox.div);
+ }
+ }
+ }
+ } else {
+ LOG(`[ERROR] unknown div computing state`);
+ }
+};
+
+WebVTT.processCues = function(window, cues, overlay, controls) {
+ // When accessing `offsetXXX` attributes of element, it would trigger reflow
+ // and might result in a re-entry of this function. In order to avoid doing
+ // redundant computation, we would only do one processing at a time.
+ if (this.isProcessingCues) {
+ return;
+ }
+ this.isProcessingCues = true;
+ processCuesInternal(window, cues, overlay, controls);
+ this.isProcessingCues = false;
+};
+
+WebVTT.Parser = function(window, decoder) {
+ this.window = window;
+ this.state = "INITIAL";
+ this.substate = "";
+ this.substatebuffer = "";
+ this.buffer = "";
+ this.decoder = decoder || new TextDecoder("utf8");
+ this.regionList = [];
+ this.isPrevLineBlank = false;
+};
+
+WebVTT.Parser.prototype = {
+ // If the error is a ParsingError then report it to the consumer if
+ // possible. If it's not a ParsingError then throw it like normal.
+ reportOrThrowError: function(e) {
+ if (e instanceof ParsingError) {
+ this.onparsingerror && this.onparsingerror(e);
+ } else {
+ throw e;
+ }
+ },
+ parse: function (data) {
+ // If there is no data then we won't decode it, but will just try to parse
+ // whatever is in buffer already. This may occur in circumstances, for
+ // example when flush() is called.
+ if (data) {
+ // Try to decode the data that we received.
+ this.buffer += this.decoder.decode(data, {stream: true});
+ }
+
+ // This parser is line-based. Let's see if we have a line to parse.
+ while (/\r\n|\n|\r/.test(this.buffer)) {
+ let buffer = this.buffer;
+ let pos = 0;
+ while (buffer[pos] !== '\r' && buffer[pos] !== '\n') {
+ ++pos;
+ }
+ let line = buffer.substr(0, pos);
+ // Advance the buffer early in case we fail below.
+ if (buffer[pos] === '\r') {
+ ++pos;
+ }
+ if (buffer[pos] === '\n') {
+ ++pos;
+ }
+ this.buffer = buffer.substr(pos);
+
+ // Spec defined replacement.
+ line = line.replace(/[\u0000]/g, "\uFFFD");
+
+ // Detect the comment. We parse line on the fly, so we only check if the
+ // comment block is preceded by a blank line and won't check if it's
+ // followed by another blank line.
+ // https://www.w3.org/TR/webvtt1/#introduction-comments
+ // TODO (1703895): according to the spec, the comment represents as a
+ // comment block, so we need to refactor the parser in order to better
+ // handle the comment block.
+ if (this.isPrevLineBlank && /^NOTE($|[ \t])/.test(line)) {
+ LOG("Ignore comment that starts with 'NOTE'");
+ } else {
+ this.parseLine(line);
+ }
+ this.isPrevLineBlank = emptyOrOnlyContainsWhiteSpaces(line);
+ }
+
+ return this;
+ },
+ parseLine: function(line) {
+ let self = this;
+
+ function createCueIfNeeded() {
+ if (!self.cue) {
+ self.cue = new self.window.VTTCue(0, 0, "");
+ }
+ }
+
+ // Parsing cue identifier and the identifier should be unique.
+ // Return true if the input is a cue identifier.
+ function parseCueIdentifier(input) {
+ if (maybeIsTimeStampFormat(input)) {
+ self.state = "CUE";
+ return false;
+ }
+
+ createCueIfNeeded();
+ // TODO : ensure the cue identifier is unique among all cue identifiers.
+ self.cue.id = containsTimeDirectionSymbol(input) ? "" : input;
+ self.state = "CUE";
+ return true;
+ }
+
+ // Parsing the timestamp and cue settings.
+ // See spec, https://w3c.github.io/webvtt/#collect-webvtt-cue-timings-and-settings
+ function parseCueMayThrow(input) {
+ try {
+ createCueIfNeeded();
+ parseCue(input, self.cue, self.regionList);
+ self.state = "CUETEXT";
+ } catch (e) {
+ self.reportOrThrowError(e);
+ // In case of an error ignore rest of the cue.
+ self.cue = null;
+ self.state = "BADCUE";
+ }
+ }
+
+ // 3.4 WebVTT region and WebVTT region settings syntax
+ function parseRegion(input) {
+ let settings = new Settings();
+ parseOptions(input, function (k, v) {
+ switch (k) {
+ case "id":
+ settings.set(k, v);
+ break;
+ case "width":
+ settings.percent(k, v);
+ break;
+ case "lines":
+ settings.digitsValue(k, v);
+ break;
+ case "regionanchor":
+ case "viewportanchor": {
+ let xy = v.split(',');
+ if (xy.length !== 2) {
+ break;
+ }
+ // We have to make sure both x and y parse, so use a temporary
+ // settings object here.
+ let anchor = new Settings();
+ anchor.percent("x", xy[0]);
+ anchor.percent("y", xy[1]);
+ if (!anchor.has("x") || !anchor.has("y")) {
+ break;
+ }
+ settings.set(k + "X", anchor.get("x"));
+ settings.set(k + "Y", anchor.get("y"));
+ break;
+ }
+ case "scroll":
+ settings.alt(k, v, ["up"]);
+ break;
+ }
+ }, /:/, /\t|\n|\f|\r| /); // groupDelim is ASCII whitespace
+ // https://infra.spec.whatwg.org/#ascii-whitespace, U+0009 TAB, U+000A LF, U+000C FF, U+000D CR, U+0020 SPACE
+
+ // Create the region, using default values for any values that were not
+ // specified.
+ if (settings.has("id")) {
+ try {
+ let region = new self.window.VTTRegion();
+ region.id = settings.get("id", "");
+ region.width = settings.get("width", 100);
+ region.lines = settings.get("lines", 3);
+ region.regionAnchorX = settings.get("regionanchorX", 0);
+ region.regionAnchorY = settings.get("regionanchorY", 100);
+ region.viewportAnchorX = settings.get("viewportanchorX", 0);
+ region.viewportAnchorY = settings.get("viewportanchorY", 100);
+ region.scroll = settings.get("scroll", "");
+ // Register the region.
+ self.onregion && self.onregion(region);
+ // Remember the VTTRegion for later in case we parse any VTTCues that
+ // reference it.
+ self.regionList.push({
+ id: settings.get("id"),
+ region: region
+ });
+ } catch(e) {
+ dump("VTTRegion Error " + e + "\n");
+ let regionPref = Services.prefs.getBoolPref("media.webvtt.regions.enabled");
+ dump("regionPref " + regionPref + "\n");
+ }
+ }
+ }
+
+ // Parsing the WebVTT signature, it contains parsing algo step1 to step9.
+ // See spec, https://w3c.github.io/webvtt/#file-parsing
+ function parseSignatureMayThrow(signature) {
+ if (!/^WEBVTT([ \t].*)?$/.test(signature)) {
+ throw new ParsingError(ParsingError.Errors.BadSignature);
+ } else {
+ self.state = "HEADER";
+ }
+ }
+
+ function parseRegionOrStyle(input) {
+ switch (self.substate) {
+ case "REGION":
+ parseRegion(input);
+ break;
+ case "STYLE":
+ // TODO : not supported yet.
+ break;
+ }
+ }
+ // Parsing the region and style information.
+ // See spec, https://w3c.github.io/webvtt/#collect-a-webvtt-block
+ //
+ // There are sereval things would appear in header,
+ // 1. Region or Style setting
+ // 2. Garbage (meaningless string)
+ // 3. Empty line
+ // 4. Cue's timestamp
+ // The case 4 happens when there is no line interval between the header
+ // and the cue blocks. In this case, we should preserve the line for the
+ // next phase parsing, returning "true".
+ function parseHeader(line) {
+ if (!self.substate && /^REGION|^STYLE/.test(line)) {
+ self.substate = /^REGION/.test(line) ? "REGION" : "STYLE";
+ return false;
+ }
+
+ if (self.substate === "REGION" || self.substate === "STYLE") {
+ if (maybeIsTimeStampFormat(line) ||
+ emptyOrOnlyContainsWhiteSpaces(line) ||
+ containsTimeDirectionSymbol(line)) {
+ parseRegionOrStyle(self.substatebuffer);
+ self.substatebuffer = "";
+ self.substate = null;
+
+ // This is the end of the region or style state.
+ return parseHeader(line);
+ }
+
+ if (/^REGION|^STYLE/.test(line)) {
+ // The line is another REGION/STYLE, parse and reset substatebuffer.
+ // Don't break the while loop to parse the next REGION/STYLE.
+ parseRegionOrStyle(self.substatebuffer);
+ self.substatebuffer = "";
+ self.substate = /^REGION/.test(line) ? "REGION" : "STYLE";
+ return false;
+ }
+
+ // We weren't able to parse the line as a header. Accumulate and
+ // return.
+ self.substatebuffer += " " + line;
+ return false;
+ }
+
+ if (emptyOrOnlyContainsWhiteSpaces(line)) {
+ // empty line, whitespaces, nothing to do.
+ return false;
+ }
+
+ if (maybeIsTimeStampFormat(line)) {
+ self.state = "CUE";
+ // We want to process the same line again.
+ return true;
+ }
+
+ // string contains "-->" or an ID
+ self.state = "ID";
+ return true;
+ }
+
+ try {
+ LOG(`state=${self.state}, line=${line}`)
+ // 5.1 WebVTT file parsing.
+ if (self.state === "INITIAL") {
+ parseSignatureMayThrow(line);
+ return;
+ }
+
+ if (self.state === "HEADER") {
+ // parseHeader returns false if the same line doesn't need to be
+ // parsed again.
+ if (!parseHeader(line)) {
+ return;
+ }
+ }
+
+ if (self.state === "ID") {
+ // If there is no cue identifier, read the next line.
+ if (line == "") {
+ return;
+ }
+
+ // If there is no cue identifier, parse the line again.
+ if (!parseCueIdentifier(line)) {
+ return self.parseLine(line);
+ }
+ return;
+ }
+
+ if (self.state === "CUE") {
+ parseCueMayThrow(line);
+ return;
+ }
+
+ if (self.state === "CUETEXT") {
+ // Report the cue when (1) get an empty line (2) get the "-->""
+ if (emptyOrOnlyContainsWhiteSpaces(line) ||
+ containsTimeDirectionSymbol(line)) {
+ // We are done parsing self cue.
+ self.oncue && self.oncue(self.cue);
+ self.cue = null;
+ self.state = "ID";
+
+ if (emptyOrOnlyContainsWhiteSpaces(line)) {
+ return;
+ }
+
+ // Reuse the same line.
+ return self.parseLine(line);
+ }
+ if (self.cue.text) {
+ self.cue.text += "\n";
+ }
+ self.cue.text += line;
+ return;
+ }
+
+ if (self.state === "BADCUE") {
+ // 54-62 - Collect and discard the remaining cue.
+ self.state = "ID";
+ return self.parseLine(line);
+ }
+ } catch (e) {
+ self.reportOrThrowError(e);
+
+ // If we are currently parsing a cue, report what we have.
+ if (self.state === "CUETEXT" && self.cue && self.oncue) {
+ self.oncue(self.cue);
+ }
+ self.cue = null;
+ // Enter BADWEBVTT state if header was not parsed correctly otherwise
+ // another exception occurred so enter BADCUE state.
+ self.state = self.state === "INITIAL" ? "BADWEBVTT" : "BADCUE";
+ }
+ return this;
+ },
+ flush: function () {
+ let self = this;
+ try {
+ // Finish decoding the stream.
+ self.buffer += self.decoder.decode();
+ self.buffer += "\n\n";
+ self.parse();
+ } catch(e) {
+ self.reportOrThrowError(e);
+ }
+ self.isPrevLineBlank = false;
+ self.onflush && self.onflush();
+ return this;
+ }
+};